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(要写入的文本)