Go语言向导: 通过例子学习Golang

什么是Go?

Go(也叫Golang)是由Google开发的一款开源的编程语言。它是一款静态编译型语言。Go支持并发编程, 即它允许多个进程同时运行, 这是通过使用通道、协程等实现。Go有垃圾回收机制,它自己实现内存管理并且允许函数的延迟执行。

如何下载以及安装Go

到https://golang.org/dl/下载你操作系统对应的二进制文件。(由于网络原因, 国内下载请前往Go语言中文网, 梯子除外)

第一个Go程序

创建一个名为studyGo的文件夹, 你将会在这个文件夹内创建我们的go程序, Go文件以 .go 后缀名创建, 你可以使用下面的语法运行Go程序。

$ go run main.go

注: go run的go文件所在的package必须为package main, 并且必须包含main()函数, 否则无法编译通过

创建一个名为 first.go 的文件, 添加以下代码到文件里并保存。

package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello World! This is my first Go program")
}

在终端导航到这个文件夹中, 使用下面的命令运行程序。

$ go run first.go

你可以看到输出打印:

Hello World! This is my first Go program

对于上面的程序:

package main - 每个go程序都应该以这个包名开始。Go允许我们使用其他go程序中的package, 所以支持代码重用。go程序的执行开始于main package。

import fmt - 导入fmt包, 这个package实现了I/O功能。

func main() - 这是go程序开始执行的入口, main函数应该总是出现在main包中, 在main()下面,你可以在{}中写代码。

fmt.Println() - 通过fmt包中的Println函数可以将文本打印到屏幕上, 并且自动换行。

数据类型

类型(数据类型)表示存储在变量中的值类型, 函数返回的值类型等等。

Go中有三种基本类型。

数值类型 - 表示包括整型、浮点型和复数。各种数值类型有:

关键字 类型
int8 8位有符号整型
int16 16位有符号整型
int32 32位有符号整型
int64 64位有符号整型
uint8 8位无符号整型
uint16 16位无符号整型
uint32 32位无符号整型
uint64 64位无符号整型
float32 32位浮点型
float64 64位浮点型
complex64 实部和虚部都由float32组成
complex128 实部和虚部都由float64组成

字符串类型

表示由字符组成的序列。你可以在字符串上进行拼接、截取子串等一系列操作。

布尔类型

表示true或者false

变量

变量指存储值的一块内存地址, 在下面的语法中, 类型参数表示可以存储在内存中的值类型。

变量可以使用下面的语法声明:

var      

一旦你声明了一个变量, 你可以赋给它任何该类型的值。在声明过程中,你也可以通过下面的语法给一个初始值。

var   = 

如果你声明的时候赋予了初始值, Go内部会自动根据值类型确定变量的类型, 所以在声明时你可以通过下面的语法忽略类型:

var  = 

同样, 你可以通过下面的语法声明多个变量:

var ,  = , 

下面的例子有一些关于变量声明的例子:

package main
import "fmt"

func main() {
    //声明一个整型变量 x
    var x int
    x=3 //将3赋值给x 
    fmt.Println("x:", x) //打印出3
    
    //在一个单独的声明中声明一个整型变量y并初始化值为20, 然后打印
    var y int=20
    fmt.Println("y:", y)
    
    //声明一个变量z并赋值50, 然后打印
    //这里类型并没有显示声明 
    var z=50
    fmt.Println("z:", z)
    
    //多个变量在一行中声明, i为整型, j为字符串
    var i, j = 100,"hello"
    fmt.Println("i and j:", i,j)
}

上面的输出为:

x: 3
y: 50
i and j: 100 hello

Go同样提供一个简单的方法声明变量并赋值, 该方法将忽略var关键字:

<变量名> := <值>

注意: 使用:=替代=, 你不能使用:=给一个已经声明过的变量赋值。

创建一个名为assign.go的文件, 并写入下列代码:

package main
import "fmt"

func main() {
    a := 20
    fmt.Println(a)
    
    //这里将会报错因为a已经是声明过的了
    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, while, do while但是Go只支持for循环

for循环的语法是:

for 初始表达式; 终止条件表达式; 迭代表达式 {
    //执行代码块
}  

初始表达式第一个被执行, 并且只执行一次

终止条件表达式在每次执行循环代码块前都会先进行一次判断, 如果结果为true, 则执行循环内的代码块。

迭代表达式从第一次循环结束开始, 以后每次循环结束都会执行该迭代语句, 改变迭代变量的值, 直到终止条件表达式的值为false。

复制下面的代码到go文件, 并执行。 程序将会循环打印输出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

Go语言中经常会用到无限循环用于持续接收数据之类的场景:

for {
    //无限循环
}

//或者
for true {
    //无限循环
}

if else

条件语句if else的语法为:

if condition{
    //满足condition时执行的语句
}else{
    //不满足condition时执行的语句
}

if后面可以不用跟着else, 也可以使用链式的if else语句。下面的程序将会解释更多关于if else的使用方法。

下面的程序将会检查整型变量x, 如果小于10, 程序将会打印x is less than 10

package main
import "fmt"

func main() {  
    var x = 50
    if x < 10 {
        //x < 10时执行
        fmt.Println("x is less than 10")
    } 
}

这里如果x的值大于10, if中的语句将不会被执行。

下面的程序添加了else, 如果if条件不满足, 则会执行else中的语句。

package main
import "fmt"

func main() {  
    var x = 50
    if x < 10 {
        //x < 10时执行
        fmt.Println("x is less than 10")
    } else {
        //x >= 10时执行
        fmt.Println("x is greater than or equals 10")
    }
}

上面的程序将会输出:

x is greater than or equals 10

下面是带有多个if else(链式if else)的程序, 执行下面的例子, 程序将会检查x是否小于10或者在10-90之间或者大于90。

package main
import "fmt"

func main() {  
    var x = 100
    if x < 10 {
        //x < 10时执行
        fmt.Println("x is less than 10")
    } else if x >= 10 && x <= 90 {
        //x >= 10 并且 <= 90时执行
        fmt.Println("x is between 10 and 90")
    } else {
        //不满足上面的两个条件则执行这里(x > 90时)
        fmt.Println("x is greater than 90")
    }
}

程序将会一次检查每个条件, 直到找到满足的条件, 然后执行该分支下面的代码块。上面的程序将会输出:

x is greater than 90

switch

switch是Go中的另一个条件语句, 与其他编程语言中一样, switch语句将会计算表达式的值, 然后与每个case分支中的值进行比较, 如果比较结果为true, 则会执行对应的代码块。如果没有匹配的case分支, 则什么也不执行, 这种情况下, 可以在switch语句中添加default分支, 当没有满足条件的case分支时, 程序会执行default分支中的代码块。关于switch语句的语法为:

switch 表达式 {
    case 值1:
        执行语句1
    case 值2:
        执行语句2
    case 值3:
        执行语句3
    default:
        //default语句
    }

注: 表达式必须是可计算的

还有另外一种写法是, 省略switch后面的表达式, 在每个case语句后面添加表达式:

switch {
    case 表达式1:
        执行语句1
    case 表达式2:
        执行语句2
    case 表达式3:
        执行语句3
    default:
        //default语句
    }

这种情况下, switch语句将会判断每个分支, 直到找到找到case中的条件表达式为true时, 然后执行对应的代码块, 如果没有结果为truecase分支, 则会执行default分支。

注: 每个case中的表达式必须是可计算的, 并且计算结果为true或者false

执行下面的代码:

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

第二种写法:

package main
import "fmt"

func main() {  
    var a, b = 2, 0
    switch  {
    case a == 2:
        fmt.Println("a is 2")
    case b == 1:
        fmt.Println("b is 1")
    default:
        fmt.Println("default")
    }
}

程序输出:

a is 2

如果多个case的处理相同, 则可以将case条件语句放在同一个case中, 用,分隔。 如果不放在一起, 则可以将两个case挨着放, 然后在上面的case分支中通过关键字fallthrough让程序执行下面挨着的case代码块。

package main
import "fmt"

func main() {  
    var a, b int
    switch  {
    case a == 2:
        fallthrough
    case b == 1:
        fmt.Println("b == 1")
    default:
        fmt.Println("default")
    }
}

程序输出:

b == 1

数组

数组表示长度固定, 元素数据类型相同的一组数据序列。在数组中, 不能同时包含整型元素和字符元素, 数组一旦定义好了则不能改变其长度以及对应的元素类型。

声明数组的语法如下:

var arrayName [size]type

对于声明好的数组, 每个元素可以通过下面的语法赋值:

var arr [10]int

arr[0] = 1

数组下表从0开始到size - 1

在数组声明的同时可以进行赋值:

arrayName := [size]type{v_0, v_1, ..., v_size-1}

数组声明的时候也可以省略size而使用赋值的方式替代, Go会自动识别数组的size, 语法如下:

arrayName := [...]int{1, 2, 3, 4} //size为4

注: Go中对...的应用主要有三种情形:

1. 数组声明中, 用于表示该数组的长度由花括号中元素个数决定, 而不是通过显示的方式指定

2. 用在函数或者方法的形参中, 用于说明该参数为指定类型的不定个数参数(参数值个数>=0), func test(params ...int), 表示params包含int型的一个或者多个参数值

3. 用在append(array1, array2...)中, 表示将array2中的所有元素append到array1中

通过调用Go内置函数len()可以获取数组的size:

arraySize := len(arrayName)

通过下面的代码更好的理解数组:

package main
import "fmt"

func main() {  
    var numbers [3] string //声明一个字符串类型长度为3的数组, 并往里添加元素 
    numbers[0] = "One"
    numbers[1] = "Two"
    numbers[2] = "Three"
    fmt.Println(numbers[1]) //打印 Two
    fmt.Println(len(numbers)) //打印 Three
    fmt.Println(numbers) // 打印 [One Two Three]
    
    directions := [...] int {1,2,3,4,5} // 创建一个整型数组, 并在创建的同时指定所有元素, 此时数组的长度也被确定 
    fmt.Println(directions) //打印 [1 2 3 4 5]
    fmt.Println(len(directions)) //打印 5
    
    //执行下面注释中的代码将会出现数组越界的错误
    //fmt.Println(directions[5]) 
}

上面程序的输出:

Two
3
[One Two Three]
[1 2 3 4 5]
5

切片

切片是数组的一部分, 切片底层指向一个数组, 像数组一样, 切片元素的类型也是固定的, 切片元素也可以通过切片名和下标来访问, 与数组不同的是, 切片长度可以改变。

切片实际上是一个指向底层数组元素的指针, 也就是说如果你改变了切片元素的值, 底层数组的值也会被改变。

注: 实际使用中, 如果多个切片共用一个底层数组, 需要注意值的改变对切片之间带来的影响。

切片创建的语法如下:

var sliceName []type = arrayName[start:end]

上面的语法将会使用数组arrayName下标startend-1的元素创建一个名为sliceName, 元素类型为type的切片(实际中可以省略type)

执行下面的代码, 程序将会从数组创建一个切片并打印, 同时更改了切片元素的值, 数组的值也会跟着改变:

package main
import "fmt"

func main() {  
    //声明一个数组
    a := [5] string {"one", "two", "three", "four", "five"}
    fmt.Println("Array after creation:",a)
    
    var b [] string = a[1:4] //创建数组a的切片b
    fmt.Println("Slice after creation:",b)
    
    b[0]="changed" // 更改切片元素的值
    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]

Go中有一些可以应用在切片上的内置函数:

len(sliceName) - 返回切片长度

append(sliceName, v1, v2) - 往切片追加元素v1, v2

append(slice1, slice2...) - 将slice2中的元素追加到slice1中

执行下面的代码:

package main
import "fmt"

func main() {  
    a := [5] string {"1","2","3","4","5"}
    sliceA := a[1:3]
    b := [5] string {"one","two","three","four","five"}
    sliceB := b[1:3]
    
    fmt.Println("sliceA:", sliceA)
    fmt.Println("sliceB:", sliceB)
    fmt.Println("Length of sliceA:", len(sliceA))
    fmt.Println("Length of sliceB:", len(sliceB))
    
    sliceA = append(sliceA,sliceB...) // appending slice
    fmt.Println("New sliceA after appending sliceB :", sliceA)
    
    sliceA = append(sliceA,"text1") // appending value
    fmt.Println("New sliceA after appending text1 :", sliceA)
}

输出:

sliceA: [2 3]
sliceB: [two three]
Length of sliceA: 2
Length of sliceB: 2
New sliceA after appending sliceB : [2 3 two three]
New sliceA after appending text1 : [2 3 two three text1]

函数

函数表示执行特定任务的一个代码块。函数声明指定函数名, 返回值类型以及传入的参数(形参)。函数定义指定了函数要执行的代码块, 函数声明的语法如下:

func funcName(param1 type, param2 type) returnType {
    //code block
}

形参和返回类型是可选的, 根据实际情况做选择。Go语言支持函数有多个返回值。下面的代码中函数接收两个参数并且计算加法和减法, 并且返回两个值。

package main
import "fmt"

//calc是函数名, 接收两个整型参数, 并且返回两个整型的结果值
func calc(num1 int, num2 int)(int, int) {  
    sum := num1 + num2
    diff := num1 - num2
    return sum, diff
}

func main() {  
    x,y := 15,10

    sum, diff := calc(x,y) 
    fmt.Println("Sum",sum)
    fmt.Println("Diff",diff) 
}

输出为:

Sum 25
Diff 5

package

Go语言中的package是用来组织代码结构的, 在一个大型项目中, 将所有代码写在一个文件里是不可行的, Go语言允许我们在不同的package下组织我们的代码。package的应用增加了代码的可读性以及复用性。Go的可执行程序应该包含main package和程序的执行入口main函数, 导入package的语法如下:

import packageName

下面的例子将讨论如何创建和使用package

  1. 创建一个名为package_example.go的文件并且添加如下代码:

    package main
    import "fmt"
    import "calculation" //待创建的包名
    
    func main() {  
       x,y := 15,10
       //calculation包中将包含函数Do_add()
       sum := calculation.DoAdd(x,y)
       fmt.Println("Sum",sum) 
    }
    

    上面的程序中fmt包是Go提供给我们的主要用于I/O格式化输出功能的, 还包含了calculation包, 该包中包含Do_add()函数, 并且该函数在main包被调用: sum := calculation.DoAdd(x,y)

  2. 在$GOPATH目录下的src目录中创建一个名为calculation的文件夹, 在该文件夹下创建名为calculation.go的文件, 并写入代码:

    package calculation
     
    func DoAdd(num1 int, num2 int) int {
       sum := num1 + num2
       return sum
    }
    
  3. 回到package_example.go目录中, 执行go run package_example.go, 程序将会输出: Sum 25

对于使用go module的项目, 则需要在go.mod文件中require该package, 值得注意的是, 对于自己的本地项目, 不仅需要在go.mod文件中require, 而且需要将该packagereplace为本地路径。

注意: DoAdd()函数的首字母必须大写, 因为Go语言中的对于想让其他package可以访问的函数、变量、结构体字段等, 其命名必须以大写字母开头, 上面的代码中如果DoAdd()开头为小写: doAdd(), 则其他package无法访问, 上面的程序将会编译出错:

cannot refer to unexported name calculation.calc..

defer

defer语句是用来延迟函数执行的, 在defer中的代码块将会被延迟到包含该defer语句的函数返回前执行, 即: defer将会在包含它的函数返回前最后执行。

package main
import "fmt"

func sample() {  
    fmt.Println("Inside the sample()")
}
func main() {  
    //sample只会在main中的所有语句执行完之后再执行
    defer sample()
    fmt.Println("Inside the main()")
}

输出为:

Inside the main()
Inside the sample()

这里sample()的执行被延迟到main()函数体执行完且在return返回之前执行

当存在多个defer语句时, Go将所有的defer放进调用栈中, 当主函数调用完成后, defer调用栈按照先进后出(LIFO)的顺序执行各个defer, 如下:

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

指针

在介绍指针前, 先看一下&操作符, &操作符被用来获取变量的地址, 即&a将会获取到存储变量a的内存地址。

下面的程序将会展示一个变量的值和它的地址:

package main
import "fmt"

func main() {
    a := 20
    fmt.Println("Address:",&a)
    fmt.Println("Value:",a)
}

输出为:

Address: 0xc000078008
Value: 20

指针变量存储另一个变量的内存地址, 定义指针的语法如下:

var pName *type

*操作符表示该变量是一个指针, 看下面代码:

package main
import "fmt"

func main() {
    //创建一个初始值为20的整型变量
    a := 20
    
    //创建一个指针变量并赋值为a的地址(实际使用中可以省略*int)
    var b *int = &a
    
    //打印a的值和地址
    fmt.Println("Address of a:",&a)
    fmt.Println("Value of a:",a)
    
    //打印包含a内存地址的变量b
    fmt.Println("Address of pointer b:",b)
    
    //*b打印出它所代表的内存地址中存储的值
    fmt.Println("Value of pointer b",*b)
    
    //通过b给变量a加1
    *b = *b+1
    
    //使用*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

结构体

结构体是开发者自己定义的数据类型, 结构体可以包含一个或者多个相同或者不同类型的字段。当你想将多个数据存储在一起的时候可以使用结构体。考虑员工信息, 一般包含姓名、年龄和地址, 你可以通过两种方式存储:

  1. 通过三个数组, 一个数组存储姓名, 一个数组存储年龄, 另一个存储地址, 每个数组中下标相同的元素表示同一个员工的信息
  2. 声明一个包含姓名、年龄、地址三个字段的结构体, 通过结构体的数组来表示员工信息, 数组的每个元素表示一个员工信息

第一种方法并不高效, 在这种场景冲, 结构体更加高效。

声明结构体的语法如下:

type structName struct {
    v1 type
    v2 type 
    v3 type
}

上面的员工信息结构体可以声明为:

type emp struct {
    name string 
    address string 
    age int
}

现在可以通过结构体创建一个存储员工信息的变量:

var empName emp

对结构体中每个字段的赋值语法如下:

empName.name = "John"
empName.address = "Street-1, Bangalore"
empName.age = 30

也可以在声明变量的时候同时赋初值:

empData := emp{"Raj", "Building-1, Delhi", 25}

在上面的声明过程中, 必须保证字段的顺序与结构体声明的顺序一致。

执行下面的代码:

package main
import "fmt"

//声明一个名为emp的结构体
type emp struct {
    name string
    address string
    age int
}       

//函数接收一个emp类型的参数, 并且打印name字段
func display(e emp) {
    fmt.Println(e.name)
}

func main() {
    //声明一个emp类型的变量empData1
    var empData1 emp
    //给结构体的成员变量赋值
    empData1.name = "John"
    empData1.address = "Street-1, London"
    empData1.age = 30
    
    //声明并赋值给emp类型的变量empData2
    empData2 := emp{"Raj", "Building-1, Paris", 25}
    
    //prints the member name of empdata1 and empdata2 using display function
    //打印empData1, empData2的成员变量name
    display(empData1)
    display(empData2)
}

输出:

John
Raj

方法(不是函数)

方法是带有接收者的函数, 语法为:

func (variable variableType)methodName(parammeter1 parammeter1Type) (returnValue1 returnValue1Type){
    //code block
}

上面的方法等价于

func methodName(variable variableType, parammeter1 parammeter1Type) (returnValue1 returnValue1Type){
    //code block
}

将上面的员工相关的函数变为方法

package main
import "fmt"

//声明一个名为emp的结构体
type emp struct {
    name string
    address string
    age int
}

//声明一个带有接收者的函数
func(e emp) display() {
    fmt.Println(e.name)
}

func main() {
    //声明一个emp类型的变量empData1
    var empData1 emp
    
    //给结构体的成员变量赋值
    empData1.name = "John"
    empData1.address = "Street-1, Lodon"
    empData1.age = 30

    //声明并赋值给emp类型的变量empData2
    empData2 := emp {
        "Raj", "Building-1, Paris", 25}

    //通过接收者调用方法, 语法为: variable.methodname()
    empData1.display()
    empData2.display()
}

Go语言不是面向对象的编程语言, 没有class的概念, 方法调用给人一种在面向对象编程时调用class的方法的感觉。

并发

Go支持任务的并发执行, 意味着Go可以同时执行多个任务。这与并行的概念不同, 在并行中, 一个任务被分成多个更小的子任务并且并行的被执行, 但是在并发中, 多个任务是同时被执行的, 在Go中并发通过通道和协程实现。

协程

协程是一个可以与其他函数同时执行的函数。通常当一个函数被调用的时候, 控制权将转移到该函数中, 一旦被调用函数执行完毕, 控制权将返还给调用函数, 调用函数继续执行, 在调用函数继续执行剩下的代码前它将等待被调用函数执行完毕。

但是在协程使用中, 调用函数将不用等待被调用函数执行完毕, 它将会继续执行后面的代码, 在一个程序中可以有多个协程。

在Go程序中, main协程(主协程)执行完它包含的所有的代码后将会退出, 并不会等待其他协程执行完毕再退出。

开启一个协程是使用关键字go后面紧跟着函数调用:

go add(x, y)

通过下面的代卖你将更好的理解协程:

package main
import "fmt"
    
func display() {
    for i:=0; i<5; i++ {
        fmt.Println("In display")
    }
}

func main() {
    //开启一个协程
    go display()
    //主协程main并不等待display的执行而继续往下执行
    for i:=0; i<5; i++ {
        fmt.Println("In main")
    }
}

输出:

In main
In main
In main
In main
In main

这里main协程甚至在display协程开始执行前就已经执行完毕了, display协程是通过下面的语法调用的:

go funcName(parammeter list)

在上面的代码中, 因为在display协程执行前, main函数就已经执行完毕了, 所以打印内容中并没有display中打印的内容。

现在对上面的代码进行修改, 在main的循环中每次循环增加2秒的延时, 在display的循环中每次循环增加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() {
    //开启协程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

通道

通道是函数彼此通信的一种方式。它可以看作是一个中间区域, 一个协程往这个中间区域放数据, 另一个协程从中获取数据。

注: 通道只能传输同一种类型的数据

通道的声明语法为:

channelName := make(chan dataType)

例如:

ch := make(chan int)

向通道发送数据的语法为:

chanVariable <- data

例如:

ch <- x

从通道接收数据的语法为:

data := <- chanVariable

例如:

data := <- ch

在上面的例子中, main函数并不会等待协程的执行, 但是这是在没有使用通道的情形下, 如果一个协程发送数据到通道里, main函数将会在通道接收操作那里等待, 直到接收到数据。

在下面的例子中, 观察一下使用和不实用通道的区别:

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函数在协程执行前已经退出, 所以并没有打印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, 直到在display中往通道发送了数据。

通过关闭通道, 往通道发送数据的发送着可以告知接收者不会再有数据被发送了, 这主要用在当你在一个循环中发送数据到通道中的时候。一个通道可以通过调用close()内置函数关闭通道:

close(chName)

接收者在接收数据的时候可以通过可选的变量来判断通道是否关闭了:

variableName, status := <- ch

如果状态为true, 则表示通道未关闭且数据有效, 如果状态未false, 则表示通道已关闭。

通道同样可以用在协程之间的通信, 有发送数据的通道, 也有接收数据的通道。

package main
import "fmt"
import "time"

//发送数字0到9到通道然后关闭通道
func addToChannel(ch chan int) {    
    fmt.Println("Send data")
    for i:=0; i<10; i++ {
        ch <- i //发送数据到通道
    }
    close(ch) //关闭通道
}

//从通道接收数据并打印
func fetchFromChannel(ch chan int) {
    fmt.Println("Read data")
    for {
        //接收数据
        x, flag := <- ch

        //flag等于true则表明数据有效否则无效
        if flag == true {
            fmt.Println(x)
        }else{
            fmt.Println("Empty channel")
            break   
        }   
    }
}

func main() {
    //创建一个通道变量用于传输整型数据
    ch := make(chan int)
    
    //执行子协程, 这些协程在同时执行
    go addToChannel(ch)
    go fetchFromChannel(ch)
    
    //延时是为了防止在子协程执行完毕前, 主协程已经退出
    time.Sleep(5 * time.Second)
    fmt.Println("Inside main()")
}

输出为:

Read data
Send data
0
1
2
3
4
5
6
7
8
9
Empty channel
Inside main()

注: 关于通道的更多使用, 请参考Go语言中的通道和Go语言如何优雅的关闭通道

select

select可以看作是用于通道的switch语句。这里的case语句必须是通道操作, 通常情况下, 每个case将会尝试从通道中读取数据, 当任何一个case语句对应的通道准备好之后, 该case语句将会被执行, 如果存在多个case语句, select将会随机选择一个执行。与普通switch一样, 当没有case语句可以执行的时候, default分支将会被默认执行。

package main
import "fmt"
import "time"

//延时4秒后向通道发送数据
func data1(ch chan string) {  
    time.Sleep(4 * time.Second)
    ch <- "from data1()"
}

//延时2秒后向通道发送数据
func data2(ch chan string) {  
    time.Sleep(2 * time.Second)
    ch <- "from data2()"
}

func main() {
    //创建一个string类型的通道, 用于传输数据
    chan1 := make(chan string)
    chan2 := make(chan string)
    
    //开启协程
    go data1(chan1)
    go data2(chan2)
    
    //两个case语句分别等待从chan1和chan2中接收数据
    //chan2先获得数据, 因为data2()只延时来2秒, 所以第二个case将会被执行
    select {
    case x := <-chan1:
        fmt.Println(x)
    case y := <-chan2:
        fmt.Println(y)
    }
}

程序输出:

from data2()

为上面程序中的select添加default分支, 因为data1()data2()都有至少2秒的延时, 对于select来说, 因为case中的通道都没有数据(未准备好), 所以default分支将会被执行:

package main
import "fmt"
import "time"

//延时4秒后向通道发送数据
func data1(ch chan string) {  
    time.Sleep(4 * time.Second)
    ch <- "from data1()"
}

//延时2秒后向通道发送数据
func data2(ch chan string) {  
    time.Sleep(2 * time.Second)
    ch <- "from data2()"
}

func main() {
    //创建一个string类型的通道, 用于传输数据
    chan1 := make(chan string)
    chan2 := make(chan string)
    
    //开启协程
    go data1(chan1)
    go data2(chan2)
    
    //两个case语句分别等待从chan1和chan2中接收数据
    //但是因为case中的通道都没有数据, 此时default分支将会被执行
    select {
    case x := <-chan1:
        fmt.Println(x)
    case y := <-chan2:
        fmt.Println(y)
    default:
        fmt.Println("Default case executed")
    }
}

程序输出:

Default case executed

mutex

mutex包含在sync包中, 根据包名可以看出, mutex是Go中用于控制互斥的锁, 即互斥锁。当你不想让一个资源同时被多个子协程访问时, 便可以通过互斥锁来实现。互斥锁有两个方法: LockUnlock, 在LockUnlock中的代码块将会被唯一的执行, 即在同一时刻只有有一个任务执行该段代码块。

下面的例子将会对循环的执行次数进行计数, 在例子中我们期望循环10次, 开启了三个协程, 总的执行次数应该是30, 总的执行次数被存放在一个全局变量中。

没有互斥锁:

package main
import "fmt"
import "time"
import "strconv"
import "math/rand"

//声明一个可以被所有协程访问的变量
var count = 0

//将count的值拷贝给temp, 执行完后将值存回count变量中, 在读写之间添加了随机的延时
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() {
    //调用三次循环任务
    for i := 1; i < 4; i++ {
        go process(i)
    }
    
    //延时25秒等待所有协程执行完毕
    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

每次执行的结果可能不同, 但都不是预期的值

在上面的程序中, 对于count值的变更有三个步骤:
1. 将值拷贝给temp变量
2. 对temp进行加1操作
3. 将temp的值存回count

因为存在上面的三个步骤, 并且同时是三个协程在访问并更改count变量, 所以存在互斥: 可能值被协程1改变了, 但此时协程2持有的仍然是协程1改变之前的旧值, 此时协程2将会对协程1更改的值进行覆盖。

下面加入互斥锁的实现:

package main
import "fmt"
import "time"
import "sync"
import "strconv"
import "math/rand"

//声明一个全局的互斥锁实例
var mu sync.Mutex

//声明一个可以被所有协程访问的变量
var count = 0

//将count的值拷贝给temp, 执行完后将值存回count变量中, 在读写之间添加了随机的延时
func process(n int) {
    for i := 0; i < 10; i++ {
        time.Sleep(time.Duration(rand.Int31n(2)) * time.Second)
        //这里开启互斥锁
        mu.Lock()
        temp := count
        temp++
        time.Sleep(time.Duration(rand.Int31n(2)) * time.Second)
        count = temp
        //这里关闭互斥锁
        mu.Unlock()
    }
    fmt.Println("Count after i="+strconv.Itoa(n)+" Count:", strconv.Itoa(count))
}

func main() {
    //调用三次循环任务
    for i := 1; i < 4; i++ {
        go process(i)
    }

    //延时25秒等待所有协程执行完毕
    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

这里的输出符合我们的预期, 因为在全局变量的访问与写入中我们加入了互斥锁, 防止在同一时刻该段代码被多次执行

错误处理

错误表示出现了不符合预期的异常情况, 比如: 关闭一个未打开的文件, 打开一个不存在的文件等等。函数通常将错误当作最后一个返回参数返回。

package main
import "fmt"
import "os"

//函数接收一个文件名并且试图打开该文件
func fileOpen(name string) {
    f, err := os.Open(name)
    
    //如果文件存在则返回的err为空, 否则将会是一个error类型的值  
    if err != nil {
        fmt.Println(err)
        return
    }else{
        fmt.Println("file opened", f.Name())
    }
}

func main() {  
    fileOpen("invalid.txt")
}

输出:

open /invalid.txt: no such file or directory

自定义错误

对于Go程序开发者而言, 可以通过调用errors包中的New()函数来自定义错误内容。

package main
import "fmt"
import "os"
import "errors"

//函数接收一个文件名并且试图打开该文件
func fileOpen(name string) (string, error) {
    f, er := os.Open(name)
    
    //如果文件存在则返回的err为空, 否则将会是一个error类型的值  
    if er != nil {
        //创建一个自己的错误变量并返回  
        return "", errors.New("custom error message: File name is wrong")
    }else{
        return f.Name(),nil
    }
}

func main() {  
    filename, err := fileOpen("invalid.txt")
    if err != nil {
        //这里返回的err将会是开发者自己定义的错误
        fmt.Println(err)
    }else{
        fmt.Println("file opened", filename)
    }  
}

输出:

custom error message:File name is wrong

文件读取

文件用来存放数据, 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")读取文件中的数据并且返回一个字节序列, 在输出打印时将字节序列转换为string输出。

输出:

Contents of file: Line one
Line two
Line three

文件写入

查看下面的代码:

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)
        return
    }
    fmt.Println(l, "bytes written")
    err = f.Close()
    if err != nil {
        fmt.Println(err)
        return
    }
}

上面将会在当前目录下创建一个名为file1.txt的文件, 并且向文件中写入: Write Line one, 如果file1.txt已经存在, 则该文件内容将会被覆盖。

注: 如果文件打开成功, 在操作完毕后需要调用Close()方法来关闭该文件。

参考资料

google-go-tutorial

你可能感兴趣的:(Go语言向导: 通过例子学习Golang)