Golang 教程
什么是 Go?
Go(也称为 Golang)是由 Google 开发的一种开源编程语言。它是一种静态类型的编译型语言。Go 支持并发编程,即它允许同时运行多个进程。这是通过使用通道、goroutine 等实现的。Go 语言具有垃圾回收功能,它能自行进行内存管理,并允许函数的延迟执行。
在本 Go 语言学习教程中,我们将学习 Golang 的所有基础知识。
如何下载和安装 GO
步骤 1) 前往 https://golang.ac.cn/dl/。为您的操作系统下载二进制文件。
步骤 2) 双击安装程序并点击“运行”。
步骤 3) 点击“下一步”。
步骤 4) 选择安装文件夹并点击“下一步”。
步骤 5) 安装完成后点击“完成”。
步骤 6) 安装完成后,您可以通过打开终端并输入以下命令来验证
go version
这将显示已安装的 go 版本
你的第一个 Go 程序 – Go Hello World!
创建一个名为 studyGo 的文件夹。在本 Go 语言教程中,我们将在此文件夹内创建我们的 go 程序。Go 文件以 .go 扩展名创建。您可以使用以下语法运行 Go 程序
go run <filename>
创建一个名为 first.go 的文件,并将以下代码添加到其中并保存
package main import ("fmt") func main() { fmt.Println("Hello World! This is my first Go program\n") }
在您的终端中导航到此文件夹。使用以下命令运行程序
go run first.go
您可以看到输出打印
Hello World! This is my first Go program
现在让我们来讨论一下上面的程序。
package main – 每个 Go 语言程序都应该以包名开始。Go 允许我们在其他 go 程序中使用包,从而支持代码重用。Go 程序的执行从名为 main 的包内的代码开始。
import fmt – 导入 fmt 包。此包实现了 I/O 函数。
func main() – 这是程序执行开始的函数。main 函数应始终放在 main 包中。在 main() 下,您可以在 { } 内编写代码。
fmt.Println – 这将通过 fmt 的 Println 函数在屏幕上打印文本。
注意:在本 Go 教程的以下部分中,当我们提到执行/运行代码时,意味着将代码保存在一个扩展名为 .go 的文件中,并使用以下语法运行它
go run <filename>
数据类型
类型(数据类型)表示存储在变量中的值的类型、函数返回值的类型等。
Go 语言中有三种基本类型
数字类型 – 表示数值,包括整数、浮点数和复数值。各种数字类型有
int8 – 8 位有符号整数。
int16 – 16 位有符号整数。
int32 – 32 位有符号整数。
int64 – 64 位有符号整数。
uint8 – 8 位无符号整数。
uint16 – 16 位无符号整数。
uint32 – 32 位无符号整数。
uint64 – 64 位无符号整数。
float32 – 32 位浮点数。
float64 – 64 位浮点数。
complex64 – 具有 float32 实部和虚部。
complex128 – 具有 float32 实部和虚部。
字符串类型 – 表示一个字节(字符)序列。您可以对字符串执行各种操作,如字符串连接、提取子字符串等
布尔类型 – 表示两个值,true 或 false。
Golang 接口
Golang 接口是一个方法签名的集合,由类型用来实现对象的行为。Golang 接口的主要目标是提供带有名称、参数和返回类型的方法签名。由类型来声明和实现方法。在 Golang 中,可以使用关键字“interface”来声明接口。
变量
变量指向一个存储某种值的内存位置。类型参数(在下面的语法中)表示可以存储在该内存位置的值的类型。
可以使用以下语法声明变量
var <variable_name> <type>
一旦您声明了一个类型的变量,您就可以将该类型的任何值赋给该变量。
您还可以在声明期间为变量赋一个初始值
var <variable_name> <type> = <value>
如果您在声明变量时带有初始值,Go 可以从赋给变量的值的类型中推断出变量的类型。因此,您可以在声明时省略类型,使用以下语法
var <variable_name> = <value>
此外,您可以使用以下语法声明多个变量
var <variable_name1>, <variable_name2> = <value1>, <value2>
本 Go 教程中的以下程序提供了一些 Golang 变量声明的示例
package main import "fmt" func main() { //declaring a integer variable x var x int x=3 //assigning x the value 3 fmt.Println("x:", x) //prints 3 //declaring a integer variable y with value 20 in a single statement and prints it var y int=20 fmt.Println("y:", y) //declaring a variable z with value 50 and prints it //Here type int is not explicitly mentioned var z=50 fmt.Println("z:", z) //Multiple variables are assigned in single line- i with an integer and j with a string var i, j = 100,"hello" fmt.Println("i and j:", i,j) }
输出将是
x: 3 y: 20 z: 50 i and j: 100 hello
Go 语言还提供了一种简单的方式来声明并赋值变量,即省略 var 关键字,使用
<variable_name> := <value>
请注意,我们使用了 := 而不是 =。您不能仅使用 := 为已声明的变量赋值。:= 用于声明和赋值。
创建一个名为 assign.go 的文件,并包含以下代码
package main import ("fmt") func main() { a := 20 fmt.Println(a) //gives error since a is already declared a := 30 fmt.Println(a) }
执行 go run assign.go,您将看到结果为
./assign.go:7:4: no new variables on left side of :=
未赋初始值的变量,其默认值对于数字类型为 0,对于布尔类型为 false,对于字符串为空字符串
常量
常量变量是指一旦赋值后其值就不能再被改变的变量。在 Go 编程语言中,常量使用关键字 “const” 声明
创建一个名为 constant.go 的文件,并包含以下代码
package main import ("fmt") func main() { const b =10 fmt.Println(b) b = 30 fmt.Println(b) }
执行 go run constant.go,您将看到结果为
.constant.go:7:4: cannot assign to b
For 循环示例
循环用于根据条件重复执行一个语句块。大多数编程语言提供 3 种类型的循环——for、while、do while。但 Go 编程语言只支持 for 循环。
Golang for 循环的语法是
for initialisation_expression; evaluation_expression; iteration_expression{ // one or more statement }
在 Golang for 循环中,初始化表达式首先(且仅执行一次)被执行。
然后评估表达式被求值,如果为真,则执行块内的代码。
迭代表达式被执行,然后再次评估求值表达式。如果为真,语句块会再次执行。这将持续到求值表达式变为假为止。
将以下程序复制到一个文件中并执行,以查看 Golang for 循环打印从 1 到 5 的数字
package main import "fmt" func main() { var i int for i = 1; i <= 5; i++ { fmt.Println(i) } }
输出是
1 2 3 4 5
If else
If else 是一个条件语句。语法如下
if condition{ // statements_1 }else{ // statements_2 }
这里条件被求值,如果为真,则执行 statements_1,否则执行 statements_2。
您也可以使用不带 else 的 if 语句。您还可以有链式的 if else 语句。下面的程序将更详细地解释 if else。
执行下面的程序。它检查一个数字 x 是否小于 10。如果是,它将打印“x 小于 10”
package main import "fmt" func main() { var x = 50 if x < 10 { //Executes if x < 10 fmt.Println("x is less than 10") } }
这里由于 x 的值大于 10,if 块条件内的语句将不会被执行。
现在看下面的程序。在这个 Go 编程语言教程中,我们有一个 else 块,它会在 if 评估失败时执行。
package main import "fmt" func main() { var x = 50 if x < 10 { //Executes if x is less than 10 fmt.Println("x is less than 10") } else { //Executes if x >= 10 fmt.Println("x is greater than or equals 10") } }
这个程序会给你输出
x is greater than or equals 10
现在,在本 Go 教程中,我们将看到一个带有多个 if else 块(链式 if else)的程序。执行下面的 Go 示例。它检查一个数字是小于 10,还是在 10-90 之间,或是大于 90。
package main import "fmt" func main() { var x = 100 if x < 10 { //Executes if x is less than 10 fmt.Println("x is less than 10") } else if x >= 10 && x <= 90 { //Executes if x >= 10 and x<=90 fmt.Println("x is between 10 and 90") } else { //Executes if both above cases fail i.e x>90 fmt.Println("x is greater than 90") } }
这里首先 if 条件检查 x 是否小于 10,但它不是。所以它检查下一个条件(else if)是否在 10 和 90 之间,这也是假的。因此,它接着执行 else 部分下的代码块,输出结果
x is greater than 90
Switch
Switch 是另一种条件语句。Switch 语句评估一个表达式,并将结果与一组可用的值(case)进行比较。一旦找到匹配项,与该匹配项(case)关联的语句就会被执行。如果未找到匹配项,则什么都不会执行。您还可以向 switch 添加一个 default case,如果未找到其他匹配项,它将被执行。switch 的语法是
switch expression { case value_1: statements_1 case value_2: statements_2 case value_n: statements_n default: statements_default }
在这里,表达式的值与每个 case 中的值进行比较。一旦找到匹配项,与该 case 相关联的语句就会被执行。如果未找到匹配项,则执行 default 部分下的语句。
执行以下程序
package main import "fmt" func main() { a,b := 2,1 switch a+b { case 1: fmt.Println("Sum is 1") case 2: fmt.Println("Sum is 2") case 3: fmt.Println("Sum is 3") default: fmt.Println("Printing default") } }
你将会得到以下输出
Sum is 3
将 a 和 b 的值改为 3,结果将是
Printing default
你也可以在一个 case 中有多个值,用逗号分隔它们。
数组
数组表示一个固定大小、有名称的相同类型元素的序列。你不能有一个同时包含整数和字符的数组。一旦你定义了数组的大小,就不能改变它。
声明数组的语法是
var arrayname [size] type
每个数组元素可以使用以下语法赋值
arrayname [index] = value
数组索引从 0 到 size-1。
您可以在声明期间使用以下语法为数组元素赋值
arrayname := [size] type {value_0,value_1,…,value_size-1}
在声明带有值的数组时,你也可以忽略大小参数,方法是用 ... 替换大小,编译器将根据值的数量来确定长度。语法如下
arrayname := […] type {value_0,value_1,…,value_size-1}
你可以使用以下语法找到数组的长度
len(arrayname)
执行下面的 Go 示例以理解数组
package main import "fmt" func main() { var numbers [3] string //Declaring a string array of size 3 and adding elements numbers[0] = "One" numbers[1] = "Two" numbers[2] = "Three" fmt.Println(numbers[1]) //prints Two fmt.Println(len(numbers)) //prints 3 fmt.Println(numbers) // prints [One Two Three] directions := [...] int {1,2,3,4,5} // creating an integer array and the size of the array is defined by the number of elements fmt.Println(directions) //prints [1 2 3 4 5] fmt.Println(len(directions)) //prints 5 //Executing the below commented statement prints invalid array index 5 (out of bounds for 5-element array) //fmt.Println(directions[5]) }
输出
Two 3 [One Two Three] [1 2 3 4 5] 5
Golang 切片和 Append 函数
切片是数组的一部分或片段。或者说,它是对其所指向的底层数组的一个视图或部分视图。你可以像操作数组一样,使用切片名称和索引号来访问切片的元素。你不能改变数组的长度,但可以改变切片的大小。
切片的内容实际上是指向数组元素的指针。这意味着如果你改变切片中的任何元素,底层数组的内容也会受到影响。
创建切片的语法是
var slice_name [] type = array_name[start:end]
这将从名为 array_name 的数组中创建一个名为 slice_name 的切片,其中包含从索引 start 到 end-1 的元素。
现在,在本 Golang 教程中,我们将执行以下程序。该程序将从数组中创建一个切片并打印它。此外,您可以看到修改切片中的内容会修改实际的数组。
package main import "fmt" func main() { // declaring array a := [5] string {"one", "two", "three", "four", "five"} fmt.Println("Array after creation:",a) var b [] string = a[1:4] //created a slice named b fmt.Println("Slice after creation:",b) b[0]="changed" // changed the slice data fmt.Println("Slice after modifying:",b) fmt.Println("Array after slice modification:",a) }
这将打印出结果为
Array after creation: [one two three four five] Slice after creation: [two three four] Slice after modifying: [changed three four] Array after slice modification: [one changed three four five]
有一些函数,如 Golang len、Golang append,可以应用于切片
len(slice_name) – 返回切片的长度
append(slice_name, value_1, value_2) – Golang append 用于将 value_1 和 value_2 追加到现有切片中。
append(slice_nale1,slice_name2...) – 将 slice_name2 追加到 slice_name1
执行以下程序。
package main import "fmt" func main() { a := [5] string {"1","2","3","4","5"} slice_a := a[1:3] b := [5] string {"one","two","three","four","five"} slice_b := b[1:3] fmt.Println("Slice_a:", slice_a) fmt.Println("Slice_b:", slice_b) fmt.Println("Length of slice_a:", len(slice_a)) fmt.Println("Length of slice_b:", len(slice_b)) slice_a = append(slice_a,slice_b...) // appending slice fmt.Println("New Slice_a after appending slice_b :", slice_a) slice_a = append(slice_a,"text1") // appending value fmt.Println("New Slice_a after appending text1 :", slice_a) }
输出将是
Slice_a: [2 3] Slice_b: [two three] Length of slice_a: 2 Length of slice_b: 2 New Slice_a after appending slice_b : [2 3 two three] New Slice_a after appending text1 : [2 3 two three text1]
该程序首先创建了 2 个切片并打印了它们的长度。然后它将一个切片追加到另一个切片,再将一个字符串追加到结果切片中。
函数
函数表示执行特定任务的一组语句块。函数声明告诉我们函数名、返回类型和输入参数。函数定义表示函数中包含的代码。声明函数的语法是
func function_name(parameter_1 type, parameter_n type) return_type { //statements }
参数和返回类型是可选的。此外,一个函数可以返回多个值。
现在,在本 Golang 教程中,让我们运行以下 Golang 示例。这里名为 calc 的函数将接受 2 个数字,执行加法和减法,并返回这两个值。
package main import "fmt" //calc is the function name which accepts two integers num1 and num2 //(int, int) says that the function returns two values, both of integer type. func calc(num1 int, num2 int)(int, int) { sum := num1 + num2 diff := num1 - num2 return sum, diff } func main() { x,y := 15,10 //calls the function calc with x and y an d gets sum, diff as output sum, diff := calc(x,y) fmt.Println("Sum",sum) fmt.Println("Diff",diff) }
输出将是
Sum 25 Diff 5
包
包用于组织代码。在一个大项目中,将代码写在单个文件中是不可行的。Go 编程语言允许我们将代码组织在不同的包下。这增加了代码的可读性和可重用性。一个可执行的 Go 程序应该包含一个名为 main 的包,并且程序执行从名为 main 的函数开始。您可以使用以下语法在我们的程序中导入其他包
import package_name
在本 Golang 教程中,我们将通过以下 Golang 示例来了解和讨论如何创建和使用包。
步骤 1) 创建一个名为 package_example.go 的文件并添加以下代码
package main import "fmt" //the package to be created import "calculation" func main() { x,y := 15,10 //the package will have function Do_add() sum := calculation.Do_add(x,y) fmt.Println("Sum",sum) }
在上述程序中,fmt 是 Go 编程语言提供给我们的一个包,主要用于 I/O 操作。此外,您可以看到一个名为 calculation 的包。在 main() 内部,您可以看到一个步骤 sum := calculation.Do_add(x,y)。这意味着您正在调用 calculation 包中的 Do_add 函数。
步骤 2) 首先,你应该在 go 的 src 文件夹下创建一个同名文件夹来创建 calculation 包。go 的安装路径可以从 PATH 变量中找到。
对于 Mac,通过执行 echo $PATH 查找路径
所以路径是 /usr/local/go
对于 Windows,通过执行 echo %GOROOT% 查找路径
这里的路径是 C:\Go\
步骤 3) 导航到 src 文件夹(Mac 是 /usr/local/go/src,Windows 是 C:\Go\src)。现在从代码中看,包名是 calculation。Go 要求包应放在 src 目录下同名的目录中。在 src 文件夹中创建一个名为 calculation 的目录。
步骤 4) 在 calculation 目录下创建一个名为 calc.go 的文件(你可以给它任何名字,但代码中的包名很重要。这里它应该是 calculation),并添加以下代码
package calculation func Do_add(num1 int, num2 int)(int) { sum := num1 + num2 return sum }
步骤 5) 从 calculation 目录运行命令 go install,这将编译 calc.go。
步骤 6) 现在回到 package_example.go 并运行 go run package_example.go。输出将是 Sum 25。
请注意,函数 Do_add 的名称以大写字母开头。这是因为在 Go 中,如果函数名以大写字母开头,意味着其他程序可以看见(访问)它,否则其他程序无法访问。如果函数名是 do_add,那么你会得到错误
无法引用未导出的名称 calculation.calc..
Defer 和 defer 栈
Defer 语句用于延迟一个函数调用的执行,直到包含该 defer 语句的函数执行完毕。
让我们通过一个例子来学习这个
package main import "fmt" func sample() { fmt.Println("Inside the sample()") } func main() { //sample() will be invoked only after executing the statements of main() defer sample() fmt.Println("Inside the main()") }
输出将是
Inside the main() Inside the sample()
这里 sample() 的执行被推迟到其所在函数 (main()) 执行完成之后。
堆叠 defer 是指使用多个 defer 语句。假设你在一个函数内部有多个 defer 语句。Go 会将所有延迟的函数调用放入一个栈中,一旦外围函数返回,栈中的函数就会以后进先出 (LIFO) 的顺序执行。你可以在下面的例子中看到这一点。
执行以下代码
package main import "fmt" func display(a int) { fmt.Println(a) } func main() { defer display(1) defer display(2) defer display(3) fmt.Println(4) }
输出将是
4 3 2 1
这里 main() 内部的代码首先执行,然后延迟的函数调用以相反的顺序执行,即 4, 3, 2, 1。
指针
在解释指针之前,我们先讨论一下 ‘&’ 操作符。‘&’ 操作符用于获取变量的地址。这意味着 ‘&a’ 将打印变量 a 的内存地址。
在本 Golang 教程中,我们将执行以下程序以显示变量的值和该变量的地址
package main import "fmt" func main() { a := 20 fmt.Println("Address:",&a) fmt.Println("Value:",a) }
结果将是
Address: 0xc000078008 Value: 20
指针变量存储另一个变量的内存地址。你可以使用以下语法定义一个指针
var variable_name *type
星号(*)表示该变量是一个指针。通过执行下面的程序,你会理解得更清楚。
package main import "fmt" func main() { //Create an integer variable a with value 20 a := 20 //Create a pointer variable b and assigned the address of a var b *int = &a //print address of a(&a) and value of a fmt.Println("Address of a:",&a) fmt.Println("Value of a:",a) //print b which contains the memory address of a i.e. &a fmt.Println("Address of pointer b:",b) //*b prints the value in memory address which b contains i.e. the value of a fmt.Println("Value of pointer b",*b) //increment the value of variable a using the variable b *b = *b+1 //prints the new value using a and *b fmt.Println("Value of pointer b",*b) fmt.Println("Value of a:",a)}
输出将是
Address of a: 0x416020 Value of a: 20 Address of pointer b: 0x416020 Value of pointer b 20 Value of pointer b 21 Value of a: 21
结构体
结构体是一种用户定义的数据类型,它本身包含一个或多个相同或不同类型的元素。
使用结构体是一个两步过程。
首先,创建(声明)一个结构体类型
其次,创建该类型的变量以存储值。
结构体主要用于当你想要将相关数据存储在一起时。
考虑一条员工信息,其中包含姓名、年龄和地址。您可以用两种方式处理这个问题
创建 3 个数组——一个数组存储员工姓名,一个存储年龄,第三个存储地址。
声明一个包含 3 个字段的结构体类型——姓名、地址和年龄。创建一个该结构体类型的数组,其中每个元素都是一个具有姓名、地址和年龄的结构体对象。
第一种方法效率不高。在这种情况下,结构体更方便。
声明结构体的语法是
type structname struct { variable_1 variable_1_type variable_2 variable_2_type variable_n variable_n_type }
结构体声明的一个例子是
type emp struct { name string address string age int }
这里创建了一个名为 emp 的新用户定义类型。现在,你可以使用以下语法创建 emp 类型的变量
var variable_name struct_name
一个例子是
var empdata1 emp
您可以像这样为 empdata1 设置值
empdata1.name = "John" empdata1.address = "Street-1, Bangalore" empdata1.age = 30
你也可以通过以下方式创建一个结构体变量并赋值
empdata2 := emp{"Raj", "Building-1, Delhi", 25}
在这里,你需要保持元素的顺序。Raj 将被映射到 name,下一个元素映射到 address,最后一个映射到 age。
执行下面的代码
package main import "fmt" //declared the structure named emp type emp struct { name string address string age int } //function which accepts variable of emp type and prints name property func display(e emp) { fmt.Println(e.name) } func main() { // declares a variable, empdata1, of the type emp var empdata1 emp //assign values to members of empdata1 empdata1.name = "John" empdata1.address = "Street-1, London" empdata1.age = 30 //declares and assign values to variable empdata2 of type emp empdata2 := emp{"Raj", "Building-1, Paris", 25} //prints the member name of empdata1 and empdata2 using display function display(empdata1) display(empdata2) }
输出
John Raj
方法 (非函数)
方法是带有接收者参数的函数。在结构上,它位于 func 关键字和方法名之间。方法的语法是
func (variable variabletype) methodName(parameter1 paramether1type) { }
让我们将上面的示例程序转换为使用方法而不是函数。
package main import "fmt" //declared the structure named emp type emp struct { name string address string age int } //Declaring a function with receiver of the type emp func(e emp) display() { fmt.Println(e.name) } func main() { //declaring a variable of type emp var empdata1 emp //Assign values to members empdata1.name = "John" empdata1.address = "Street-1, Lodon" empdata1.age = 30 //declaring a variable of type emp and assign values to members empdata2 := emp { "Raj", "Building-1, Paris", 25} //Invoking the method using the receiver of the type emp // syntax is variable.methodname() empdata1.display() empdata2.display() }
Go 不是一门面向对象的语言,它没有类的概念。方法提供了一种类似于面向对象编程中通过 `objectname.functionname()` 调用类函数的感觉。
并发
Go 支持任务的并发执行。这意味着 Go 可以同时执行多个任务。这与并行性的概念不同。在并行性中,一个任务被分解成小的子任务并并行执行。但在并发性中,多个任务是同时执行的。在 Go 中,并发性是通过 Goroutines 和 Channels 实现的。
Goroutines
goroutine 是一个可以与其他函数并发运行的函数。通常,当一个函数被调用时,控制权会转移到被调用的函数中,一旦其执行完成,控制权会返回给调用函数。然后调用函数继续其执行。调用函数会等待被调用函数完成执行,然后才继续执行其余的语句。
但在 goroutine 的情况下,调用函数不会等待被调用函数的执行完成。它将继续执行下一条语句。你可以在一个程序中有多个 goroutine。
此外,主程序一旦完成其语句的执行就会退出,它不会等待被调用的 goroutine 完成。
Goroutine 通过关键字 go 后跟一个函数调用来调用。
示例
go add(x,y)
通过下面的 Golang 示例,你将理解 goroutines。执行下面的程序
package main import "fmt" func display() { for i:=0; i<5; i++ { fmt.Println("In display") } } func main() { //invoking the goroutine display() go display() //The main() continues without waiting for display() for i:=0; i<5; i++ { fmt.Println("In main") } }
输出将是
In main In main In main In main In main
这里主程序在 goroutine 开始之前就完成了执行。display() 是一个 goroutine,通过以下语法调用
go function_name(parameter list)
在上面的代码中,main() 函数没有等待 display() 函数完成,并且 main() 在 display() 执行其代码之前就已经完成了执行。所以 display() 内部的打印语句没有被打印出来。
现在我们修改程序,使其也能打印出 display() 中的语句。我们在 main() 的 for 循环中添加了 2 秒的时间延迟,在 display() 的 for 循环中添加了 1 秒的延迟。
package main import "fmt" import "time" func display() { for i:=0; i<5; i++ { time.Sleep(1 * time.Second) fmt.Println("In display") } } func main() { //invoking the goroutine display() go display() for i:=0; i<5; i++ { time.Sleep(2 * time.Second) fmt.Println("In main") } }
输出结果将类似于
In display In main In display In display In main In display In display In main In main In main
在这里你可以看到两个循环由于并发执行而以重叠的方式执行。
通道
通道是函数之间进行通信的一种方式。它可以被看作是一种媒介,一个例程将数据放入其中,而另一个例程在 Golang 服务器中访问它。
通道可以使用以下语法声明
channel_variable := make(chan datatype)
示例
ch := make(chan int)
您可以使用以下语法向通道发送数据
channel_variable <- variable_name
示例
ch <- x
您可以使用以下语法从通道接收数据
variable_name := <- channel_variable
示例
y := <- ch
在上面 goroutine 的 Go 语言示例中,你已经看到主程序不会等待 goroutine。但当涉及到通道时,情况就不同了。假设一个 goroutine 向通道推送数据,main() 会在接收通道数据的语句上等待,直到它获取到数据。
你将在下面的 Go 语言示例中看到这一点。首先,编写一个普通的 goroutine 并观察其行为。然后修改程序以使用通道并观察其行为。
执行以下程序
package main import "fmt" import "time" func display() { time.Sleep(5 * time.Second) fmt.Println("Inside display()") } func main() { go display() fmt.Println("Inside main()") }
输出将是
Inside main()
main() 在 goroutine 执行之前就完成了执行并退出了。所以 display() 内部的打印语句没有被执行。
现在修改上面的程序以使用通道并观察其行为。
package main import "fmt" import "time" func display(ch chan int) { time.Sleep(5 * time.Second) fmt.Println("Inside display()") ch <- 1234 } func main() { ch := make(chan int) go display(ch) x := <-ch fmt.Println("Inside main()") fmt.Println("Printing x in main() after taking from channel:",x) }
输出将是
Inside display() Inside main() Printing x in main() after taking from channel: 1234
这里发生的情况是,main() 在到达 x := <-ch 时会等待通道 ch 上的数据。display() 有 5 秒的等待时间,然后将数据推送到通道 ch。main() 在从通道接收到数据后被解除阻塞并继续执行。
将数据推入通道的发送方可以通过关闭通道来通知接收方,不会再有数据添加到该通道。这主要用于当你使用循环向通道推送数据时。可以使用以下方式关闭一个通道
close(channel_name)
在接收端,可以通过使用一个额外的变量来检查通道是否已关闭,同时从通道获取数据
variable_name, status := <- channel_variable
如果状态为 True,意味着你从通道接收到了数据。如果为 false,意味着你正试图从一个已关闭的通道读取数据。
你也可以使用通道在 goroutines 之间进行通信。需要使用 2 个 goroutines——一个向通道推送数据,另一个从通道接收数据。请看下面的程序
package main import "fmt" import "time" //This subroutine pushes numbers 0 to 9 to the channel and closes the channel func add_to_channel(ch chan int) { fmt.Println("Send data") for i:=0; i<10; i++ { ch <- i //pushing data to channel } close(ch) //closing the channel } //This subroutine fetches data from the channel and prints it. func fetch_from_channel(ch chan int) { fmt.Println("Read data") for { //fetch data from channel x, flag := <- ch //flag is true if data is received from the channel //flag is false when the channel is closed if flag == true { fmt.Println(x) }else{ fmt.Println("Empty channel") break } } } func main() { //creating a channel variable to transport integer values ch := make(chan int) //invoking the subroutines to add and fetch from the channel //These routines execute simultaneously go add_to_channel(ch) go fetch_from_channel(ch) //delay is to prevent the exiting of main() before goroutines finish time.Sleep(5 * time.Second) fmt.Println("Inside main()") }
这里有两个子例程,一个向通道推送数据,另一个从通道打印数据。函数 add_to_channel 添加从 0 到 9 的数字并关闭通道。同时,函数 fetch_from_channel 在以下位置等待
x, flag := <- ch 一旦数据可用,它就会打印数据。一旦标志为假,它就会退出,这意味着通道已关闭。
在 main() 中的等待是为了防止 main() 在 goroutines 完成执行之前退出。
执行代码并查看输出为
Read data Send data 0 1 2 3 4 5 6 7 8 9 Empty channel Inside main()
Select
Select 可以看作是一个作用于通道的 switch 语句。这里的 case 语句将是一个通道操作。通常,每个 case 语句都是从通道读取数据的尝试。当任何一个 case 准备就绪(即通道可读)时,与该 case 关联的语句就会被执行。如果多个 case 都准备就绪,它会随机选择一个。你可以有一个 default case,如果没有任何 case 准备就绪,它就会被执行。
让我们看看下面的代码
package main import "fmt" import "time" //push data to channel with a 4 second delay func data1(ch chan string) { time.Sleep(4 * time.Second) ch <- "from data1()" } //push data to channel with a 2 second delay func data2(ch chan string) { time.Sleep(2 * time.Second) ch <- "from data2()" } func main() { //creating channel variables for transporting string values chan1 := make(chan string) chan2 := make(chan string) //invoking the subroutines with channel variables go data1(chan1) go data2(chan2) //Both case statements wait for data in the chan1 or chan2. //chan2 gets data first since the delay is only 2 sec in data2(). //So the second case will execute and exits the select block select { case x := <-chan1: fmt.Println(x) case y := <-chan2: fmt.Println(y) } }
执行上述程序将得到以下输出
from data2()
这里的 select 语句等待任何一个通道中有数据可用。data2() 在休眠 2 秒后向通道添加数据,这将导致第二个 case 执行。
在同一个程序中向 select 添加一个 default case 并查看输出。这里,当到达 select 块时,如果没有 case 的通道上有数据准备好,它将执行 default 块,而不会等待任何通道上有数据可用。
package main import "fmt" import "time" //push data to channel with a 4 second delay func data1(ch chan string) { time.Sleep(4 * time.Second) ch <- "from data1()" } //push data to channel with a 2 second delay func data2(ch chan string) { time.Sleep(2 * time.Second) ch <- "from data2()" } func main() { //creating channel variables for transporting string values chan1 := make(chan string) chan2 := make(chan string) //invoking the subroutines with channel variables go data1(chan1) go data2(chan2) //Both case statements check for data in chan1 or chan2. //But data is not available (both routines have a delay of 2 and 4 sec) //So the default block will be executed without waiting for data in channels. select { case x := <-chan1: fmt.Println(x) case y := <-chan2: fmt.Println(y) default: fmt.Println("Default case executed") } }
这个程序会给出输出
Default case executed
这是因为当 select 块到达时,没有通道有数据可供读取。所以,执行了 default case。
互斥锁
Mutex 是 mutual exclusion(互斥)的缩写。当你不想让一个资源同时被多个子程序访问时,就会使用 Mutex。Mutex 有两个方法——Lock 和 Unlock。Mutex 包含在 sync 包中。所以,你必须导入 sync 包。需要互斥执行的语句可以放在 mutex.Lock() 和 mutex.Unlock() 之间。
让我们通过一个计算循环执行次数的例子来学习互斥锁。在这个程序中,我们期望例程运行循环 10 次,并将计数存储在 sum 中。你调用这个例程 3 次,所以总计数应该是 30。计数存储在一个全局变量 count 中。
首先,我们运行不带互斥锁的程序
package main import "fmt" import "time" import "strconv" import "math/rand" //declare count variable, which is accessed by all the routine instances var count = 0 //copies count to temp, do some processing(increment) and store back to count //random delay is added between reading and writing of count variable func process(n int) { //loop incrementing the count by 10 for i := 0; i < 10; i++ { time.Sleep(time.Duration(rand.Int31n(2)) * time.Second) temp := count temp++ time.Sleep(time.Duration(rand.Int31n(2)) * time.Second) count = temp } fmt.Println("Count after i="+strconv.Itoa(n)+" Count:", strconv.Itoa(count)) } func main() { //loop calling the process() 3 times for i := 1; i < 4; i++ { go process(i) } //delay to wait for the routines to complete time.Sleep(25 * time.Second) fmt.Println("Final Count:", count) }
看结果
Count after i=1 Count: 11 Count after i=3 Count: 12 Count after i=2 Count: 13 Final Count: 13
当你执行它时,结果可能会有所不同,但最终结果不会是 30。
这里发生的是,3个goroutine正试图增加存储在变量count中的循环计数。假设某一时刻count为5,goroutine1将要将count增加到6。主要步骤包括
将 count 复制到 temp
增加 temp
将 temp 存储回 count
假设在 goroutine1 执行完步骤 3 后,另一个 goroutine 可能有一个旧值,比如 3,执行上述步骤并存回 4,这是错误的。这可以通过使用互斥锁来防止,它会使其他例程在某个例程已经在使用该变量时等待。
现在你将运行带有互斥锁的程序。这里,上述的 3 个步骤在一个互斥锁中执行。
package main import "fmt" import "time" import "sync" import "strconv" import "math/rand" //declare a mutex instance var mu sync.Mutex //declare count variable, which is accessed by all the routine instances var count = 0 //copies count to temp, do some processing(increment) and store back to count //random delay is added between reading and writing of count variable func process(n int) { //loop incrementing the count by 10 for i := 0; i < 10; i++ { time.Sleep(time.Duration(rand.Int31n(2)) * time.Second) //lock starts here mu.Lock() temp := count temp++ time.Sleep(time.Duration(rand.Int31n(2)) * time.Second) count = temp //lock ends here mu.Unlock() } fmt.Println("Count after i="+strconv.Itoa(n)+" Count:", strconv.Itoa(count)) } func main() { //loop calling the process() 3 times for i := 1; i < 4; i++ { go process(i) } //delay to wait for the routines to complete time.Sleep(25 * time.Second) fmt.Println("Final Count:", count) }
现在输出将是
Count after i=3 Count: 21 Count after i=2 Count: 28 Count after i=1 Count: 30 Final Count: 30
在这里,我们得到了预期的结果作为最终输出。因为读取、增加和写回 count 的语句是在一个互斥锁中执行的。
错误处理
错误是异常情况,比如关闭一个未打开的文件,打开一个不存在的文件等。函数通常将错误作为最后一个返回值返回。
下面的例子更多地解释了关于错误。
package main import "fmt" import "os" //function accepts a filename and tries to open it. func fileopen(name string) { f, er := os.Open(name) //er will be nil if the file exists else it returns an error object if er != nil { fmt.Println(er) return }else{ fmt.Println("file opened", f.Name()) } } func main() { fileopen("invalid.txt") }
输出将是
open /invalid.txt: no such file or directory
在这里,我们试图打开一个不存在的文件,它将错误返回给了 er 变量。如果文件有效,那么错误将为 null
自定义错误
使用此功能,您可以创建自定义错误。这是通过使用 error 包的 New() 函数完成的。我们将重写上面的程序以利用自定义错误。
运行下面的程序
package main import "fmt" import "os" import "errors" //function accepts a filename and tries to open it. func fileopen(name string) (string, error) { f, er := os.Open(name) //er will be nil if the file exists else it returns an error object if er != nil { //created a new error object and returns it return "", errors.New("Custom error message: File name is wrong") }else{ return f.Name(),nil } } func main() { //receives custom error or nil after trying to open the file filename, error := fileopen("invalid.txt") if error != nil { fmt.Println(error) }else{ fmt.Println("file opened", filename) } }
输出将是
Custom error message:File name is wrong
这里的 area() 函数返回正方形的面积。如果输入小于 1,那么 area() 会返回一个错误信息。
读取文件
文件用于存储数据。Go 允许我们从文件中读取数据
首先,在你当前的目录下创建一个名为 data.txt 的文件,内容如下。
Line one Line two Line three
现在运行下面的程序,你会看到它将整个文件的内容作为输出打印出来。
package main import "fmt" import "io/ioutil" func main() { data, err := ioutil.ReadFile("data.txt") if err != nil { fmt.Println("File reading error", err) return } fmt.Println("Contents of file:", string(data)) }
这里的 data, err := ioutil.ReadFile("data.txt") 读取数据并返回一个字节序列。在打印时,它被转换成字符串格式。
写入文件
你将通过一个程序看到这一点
package main import "fmt" import "os" func main() { f, err := os.Create("file1.txt") if err != nil { fmt.Println(err) return } l, err := f.WriteString("Write Line one") if err != nil { fmt.Println(err) f.Close() return } fmt.Println(l, "bytes written") err = f.Close() if err != nil { fmt.Println(err) return } }
这里创建了一个文件,test.txt。如果该文件已经存在,那么文件的内容将被截断。Writeline() 用于将内容写入文件。之后,我们使用 Close() 关闭了文件。
速查表
在这个 Go 教程中,我们涵盖了,
主题 | 描述 | 语法 |
---|---|---|
基本类型 | 数字、字符串、布尔 | |
变量 | 声明并为变量赋值 | var 变量名 类型 var 变量名 类型 = 值 var 变量名1, 变量名2 = 值1, 值2 变量名 := 值 |
常量 | 值一旦分配就不能更改的变量 | const 变量 = 值 |
For 循环 | 在循环中执行语句。 | for 初始化表达式; 求值表达式; 迭代表达式{ // 一条或多条语句 } |
If else | 这是一个条件语句 | if 条件{ // 语句_1 }else{ // 语句_2 } |
switch | 具有多种情况的条件语句 | switch 表达式 { case 值_1 语句_1 case 值_2 语句_2 case 值_n 语句_n default 默认语句 } |
数组 | 固定大小的同类型元素命名序列 | 数组名 := [大小] 类型 {值_0,值_1,…,值_大小-1} |
切片 | 数组的一部分或片段 | var slice_name [] type = array_name[start:end] |
函数 | 执行特定任务的语句块 | func 函数名(参数_1 类型, 参数_n 类型) 返回类型 { //语句 } |
包 | 用于组织代码。提高代码可读性和可重用性 | import 包名 |
Defer | 将一个函数的执行推迟到包含它的函数执行完毕之后 | defer 函数名(参数列表) |
指针 | 存储另一个变量的内存地址。 | var 变量名 *类型 |
结构体 | 用户定义的数据类型,其本身包含一个或多个相同或不同类型的元素 | type 结构体名 struct { 变量_1 变量_1_类型 变量_2 变量_2_类型 变量_n 变量_n_类型 } |
方法 | 方法是带有接收者参数的函数 | func (变量 变量类型) 方法名(参数列表) { } |
Goroutine | 一个可以与其他函数并发运行的函数。 | go 函数名(参数列表) |
通道 | 函数之间通信的方式。一个例程放置数据,另一个例程访问的媒介。 | 声明 ch := make(chan int) 向通道发送数据 通道变量 <- 变量名 从通道接收 变量名 := <- 通道变量 |
Select | 适用于通道的 switch 语句。case 语句将是一个通道操作。当任何一个通道准备好数据时,与该 case 相关的语句就会被执行。 | select { case x := <-chan1 fmt.Println(x) case y := <-chan2 fmt.Println(y) } |
互斥锁 (Mutex) | 当你不想让一个资源同时被多个子程序访问时,就会使用互斥锁。互斥锁有两个方法——Lock 和 Unlock | mutex.Lock() //语句 mutex.Unlock(). |
读取文件 | 读取数据并返回一个字节序列。 | 数据, 错误 := ioutil.ReadFile(文件名) |
写入文件 | 将数据写入文件 | l, err := f.WriteString(要写入的文本) |