Go语言实战学习笔记
关于Go语言的介绍
1. 用Go解决现代编程问题
- 编译器速度非常快,能显著减少等待项目构建的时间
- 内置并发机制,不用被迫使用特定的线程库就可以让软件扩展,使用更多的资源
- 类型系统简单且高效,不需要面向对象开发付出额外的心智,让开发者专注于代码复用
- 自带垃圾回收器,不需要用户自己管理内存
快速开始一个Go程序
- 学习如何写一个复杂的Go程序
- 声明类型、变量、函数和方法
- 启动并同步操作goroutine
- 使用接口写通用的代码
- 处理程序逻辑和错误
程序架构
项目结构
1
2
3
4
5
6
7
8
9
10
11
12
├── README.md
├── data
│ └── data.json -- 包含一组数据源
├── main
├── main.go -- 程序的入口
├── matchers
│ └── rss.go -- 搜索 rss 源的匹配器
└── search
├── default.go -- 搜索数据用的默认匹配器
├── feed.go -- 用于读取 json 数据文件
├── match.go -- 用于支持不同匹配器的接口
└── search.go -- 执行搜索的主控制逻辑
小结
- 每个代码文件都属于一个包,而包名应该与代码文件所在的文件夹同名
- Go语言提供了多种声明和初始化变量的方式,如果变量的值没有显式初始化,编译器会将变量初始化为零值
- 使用指针可以在函数间或者goroutine间共享数据
- 通过启动goroutine和使用通道完成并发和同步
- Go语言提供了内置函数来支持Go语言内部的数据结构
- 标准库包含很多包,能做很多很有用的事情
- 使用Go接口可以编写通用的代码和框架
Go语言命令行操作命令
Go版本号
1
go version go1.10.1 darwin/amd64
Go命令操作工具
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Go is a tool for managing Go source code.
Usage:
go command [arguments]
The commands are:
build compile packages and dependencies
clean remove object files and cached files
doc show documentation for package or symbol
env print Go environment information
bug start a bug report
fix update packages to use new APIs
fmt gofmt (reformat) package sources
generate generate Go files by processing source
get download and install packages and dependencies
install compile and install packages and dependencies
list list packages
run compile and run Go program
test test packages
tool run specified go tool
version print Go version
vet report likely mistakes in packages
Use "go help [command]" for more information about a command.
Additional help topics:
c calling between Go and C
buildmode build modes
cache build and test caching
filetype file types
gopath GOPATH environment variable
environment environment variables
importpath import path syntax
packages package lists
testflag testing flags
testfunc testing functions
Use "go help [topic]" for more information about that topic.
go build
用于测试编译。在包的编译过程中,若有必要,会同时编译与之相关联的包。
- 如果是普通包,当你执行go build之后,它不会产生任何文件。如果你需要在$GOPATH/pkg下生成相应的文件,得执行go install。
- 如果是main包,当你执行go build之后,它就会在当前目录下生成一个可执行文件。如果你需要在$GOPATH/bin下生成相应的文件,需要执行go install,或者使用go build -o 路径/a.exe。
- go build命令默认会编译当前目录下的所有go文件,如果某个项目文件夹下有多个文件,而你只想编译某个文件,就可在go build之后加上文件名
- go build会忽略目录下以“_”或“.”开头的go文件。
- 如果你的源代码针对不同的操作系统需要不同的处理,那么你可以根据不同的操作系统后缀来命名文件。go build的时候会选择性地编译以系统名结尾的文件(Linux、Darwin、Windows、Freebsd)。例如Linux系统下面编译只会选择array_linux.go文件,其它系统命名后缀文件全部忽略。
go clean
用来移除当前源码包里面编译生成的文件。
这些文件包括:
1
2
3
4
5
6
7
8
9
_obj/ 旧的object目录,由Makefiles遗留
_test/ 旧的test目录,由Makefiles遗留
_testmain.go 旧的gotest文件,由Makefiles遗留
test.out 旧的test记录,由Makefiles遗留
build.out 旧的test记录,由Makefiles遗留
*.[568ao] object文件,由Makefiles遗留
DIR(.exe) 由go build产生
DIR.test(.exe) 由go test -c产生
MAINFILE(.exe) 由go build MAINFILE.go产生
go doc
强大的文档工具
- 查看相应package的文档,例如
builtin
包,那么执行go doc builtin
- 查看某一个包里面的函数,那么执行
godoc fmt Printf
,或者godoc -src fmt Printf
- 查看golang.org的本地copy版本,执行
godoc -http=:端口号
,如果设置了GOPATH
,在pkg分类下,不但会列出标准包的文档,还会列出你本地GOPATH中所有项目的相关文档
go env
查看当前go的环境变量
go bug
go fix
用来修复以前老版本的代码到新版本,例如go1之前老版本的代码转化到go1
go fmt
格式化写好的代码文件
go generate
go get
用来动态获取远程代码包,目前支持的有 BitBucket
、 GitHub
、 Google Code
和 Launchpad
。
下载源码包的go工具会自动根据不同的域名调用不同的源码工具,对应关系如下:
1
2
3
4
BitBucket (Mercurial Git)
GitHub (Git)
Google Code Project Hosting (Git, Mercurial, Subversion)
Launchpad (Bazaar)
为了 go get
能正常工作,必须确保安装了合适的源码管理工具,并同时把这些命令加入你的PATH中。go get
支持自定义域名的功能,具体参见 go help remote
。
go install
在内部实际上分成了两步操作:第一步是生成结果文件(可执行文件或者.a包),第二步会把编译好的结果移到 $GOPATH/pkg
或者 $GOPATH/bin
。
go list
列出当前全部安装的package
go run
编译并运行Go程序
go test
执行这个命令,会自动读取源码目录下面名为 *_test.go
的文件,生成并运行测试用的可执行文件。
输出的信息类似:
1
2
3
4
ok archive/tar 0.011s
FAIL archive/zip 0.022s
ok compress/gzip 0.033s
...
go tool
go version
查看go当前的版本
go vet
vet
命令会帮开发人员检测代码的常见错误。
捕获的类型错误:
Printf
类函数调用时,类型匹配错误的参数。 - 定义常用的方法时,方法签名的错误。
- 错误的结构标签。
- 没有指定字段名的结构字面量。
数组、切片和映射
- 数组的内部实现和基础功能
- 使用切片管理数据集合
- 使用映射管理键值对
数组的内部实现和基础功能
内部实现
- 在 Go 语言里,数组是一个长度固定的数据类型,用于存储一段具有相同的类型的元素的连续块。
- 数组存储的类型可以是内置类型,如整型或者字符串,也可以是某种结构类型。
- 数组占用的内存是连续分配的。
声明和初始化
- 声明数组时需要指定内部存储的数据的类型,以及需要存储的元素的数量,这个数量也称为数组的长度。
- 一旦声明,数组里存储的数据类型和数组长度就都不能改变了。如果需要存储更多的元素,就需要先创建一个更长的数组,再把原来数组里的值复制到新数组里。
- 在 Go 语言中声明变量时,总会使用对应类型的零值来对变量进行初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 声明一个包含 5 个元素的整型数组
// 用具体值初始化每个元素
array := [5]int{10, 20, 30, 40, 50}
// 声明一个整型数组
// 用具体值初始化每个元素
// 容量由初始化值的数量决定
array := [...]int{10, 20, 30, 40, 50}
// 声明一个有 5 个元素的数组
// 用具体值初始化索引为 1 和 2 的元素
// 其余元素保持零值
array := [5]int{1: 10, 2: 20}
使用数组
因为内存布局是连续的,所以数组是效率很高的数据结构。在访问数组里任意元素的时候,这种高效都是数组的优势。要访问数组里某个单独元素,使用[]运算符。
- 访问数组元素
1 2 3 4 5
// 声明一个包含 5 个元素的整型数组 // 用具体值初始为每个元素 array := [5]int{10, 20, 30, 40, 50} // 修改索引为 2 的元素的值 array[2] = 35
- 访问指针数组的元素
1 2 3 4 5 6
// 声明包含 5 个元素的指向整数的数组 // 用整型指针初始化索引为 0 和 1 的数组元素 array := [5]*int{0: new(int), 1: new(int)} // 为索引为0和1的元素赋值 *array[0] = 10 *array[1] = 20
- 把同样类型的一个数组赋值给另外一个数组
1
2
3
4
5
6
7
8
9
10
11
// 数组变量的类型包括数组长度和每个元素的类型。
// 只有这两部分都相同的数组,才是类型相同的数组,才能互相赋值
// 声明第一个包含 5 个元素的字符串数组
var array1 [5]string
// 声明第二个包含 5 个元素的字符串数组
// 用颜色初始化数组
array2 := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}
// 把 array2 的值复制到 array1
// 复制之后,两个数组的值完全一样
array1 = array2
- 编译器会阻止类型不同的数组互相赋值 ```go // 声明第一个包含 4 个元素的字符串数组 var array1 [4]string // 声明第二个包含 5 个元素的字符串数组 // 使用颜色初始化数组 array2 := [5]string{“Red”, “Blue”, “Green”, “Yellow”, “Pink”} // 将 array2 复制给 array1 array1 = array2
Compiler Error: cannot use array2 (type [5]string) as type [4]string in assignment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 把一个指针数组赋值给另一个
```go
// 复制数组指针,只会复制指针的值,而不会复制指针所指向的值
// 复制之后,两个数组指向同一组字符串
// 声明第一个包含 3 个元素的指向字符串的指针数组
var array1 [3]*string
// 声明第二个包含 3 个元素的指向字符串的指针数组
// 使用字符串指针初始化这个数组
array2 := [3]*string{new(string), new(string), new(string)}
// 使用颜色为每个元素赋值
*array2[0] = "Red"
*array2[1] = "Blue"
*array2[2] = "Green"
// 将 array2 复制给 array1
array1 = array2
多维数组
数组本身只有一个维度,不过可以组合多个数组创建多维数组。 多维数组很容易管理具有父子关系的数据或者与坐标系相关联的数据。
- 声明二维数组
1 2 3 4 5 6
// 声明一个二维整型数组,两个维度分别存储 4 个元素和 2 个元素 var array [4][2]int // 声明并初始化外层数组中索引为 1 个和 3 的元素 array := [4][2]int{1: {20, 21}, 3: {40, 41}} // 声明并初始化外层数组和内层数组的单个元素 array := [4][2]int{1: {0: 20}, 3: {1: 41}}
- 访问二维数组的元素 ```go // 声明一个 2×2 的二维整型数组 var array [2][2]int // 设置每个元素的整型值 array[0][0] = 10
array[0][1] = 20 array[1][0] = 30 array[1][1] = 40
1
2
3
4
5
6
7
- 使用索引为多维数组赋值
```go
// 将 array1 的索引为 1 的维度复制到一个同类型的新数组里
var array3 [2]int = array1[1]
// 将外层数组的索引为 1、内层数组的索引为 0 的整型值复制到新的整型变量里
var value int = array1[1][0]
在函数间传递数组
根据内存和性能来看,在函数间传递数组是一个开销很大的操作。在函数之间传递变量时,总是以值的方式传递的。 可以只传入指向数组的指针,这样只需要复制8字节的数据而不是8MB的内存数据到栈上。
- 使用指针在函数间传递大数组
1 2 3 4 5 6 7
//分配一个需要8 MB的数组 var array [1e6]int // 将数组的地址传递给函数 foo foo(&array) // 函数 foo 接受一个指向 100 万个整型值的数组的指针 func foo(array *[1e6]int) { ... }
切片的内部实现和基础功能
内部实现
- 切片是一种数据结构,这种数据结构便于使用和管理数据集合。
- 切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。
- 切片的动态增长是通过内置函数 append 来实现的。
- 这个函数可以快速且高效地增长切片。还可以通过对切片再次切片来缩小一个切片的大小。
- 切片的底层内存也是在连续块中分配的,所以切片还能获得索引、迭代以及为垃圾回收优化的好处。
切片有3个字段的数据结构,这些数据结构包含 Go 语言需要操作底层数组的元数据。 这3个字段分别是指向底层数组的指针、切片访问的元素的个数(即长度)和切片允许增长到的元素个数(即容量)。
创建和初始化
是否能提前知道切片需要的容量通常会决定 要如何创建切片。
make 和切片字面量
- 使用长度声明一个字符串切片
1 2 3
// 创建一个字符串切片 // 其长度和容量都是 5 个元素 slice := make([]string, 5)
- 使用长度和容量声明整型切片
1 2 3
// 创建一个整型切片 // 其长度为 3 个元素,容量为 5 个元素 slice := make([]int, 3, 5)
- 容量小于长度的切片会在编译时报错 ```go // 创建一个整型切片 // 使其长度大于容量 slice := make([]int, 5, 3)
Compiler Error: len larger than cap in make([]int)
1
2
3
4
5
6
7
8
9
- 通过切片字面量来声明切片
```go
// 创建字符串切片
// 其长度和容量都是 5 个元素
slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}
// 创建一个整型切片
// 其长度和容量都是 3 个元素
slice := []int{10, 20, 30}
- 使用索引声明切片
1 2 3
// 创建字符串切片 // 使用空字符串初始化第 100 个元素 slice := []string{99: ""}
- 声明数组和声明切片的不同
1 2 3 4
// 创建有 3 个元素的整型数组 array := [3]int{10, 20, 30} // 创建长度和容量都是 3 的整型切片 slice := []int{10, 20, 30}
nil 和空切片
- 创建 nil 切片
1 2
// 创建 nil 整型切片 var slice []int
- 声明空切片
1 2 3 4
// 使用 make 创建空的整型切片 slice := make([]int, 0) // 使用切片字面量创建空的整型切片 slice := []int{}
使用切片
赋值和切片
对切片里某个索引指向的元素赋值和对数组里某个索引指向的元素赋值的方法完全一样。 切片之所以被称为切片,是因为创建一个新的切片就是把底层数组切出一部分。
- 使用切片字面量来声明切片
1 2 3 4 5
// 创建一个整型切片 // 其容量和长度都是 5 个元素 slice := []int{10, 20, 30, 40, 50} // 改变索引为 1 的元素的值 slice[1] = 25
- 使用切片创建切片
1 2 3 4 5 6
// 创建一个整型切片 // 其长度和容量都是 5 个元素 slice := []int{10, 20, 30, 40, 50} // 创建一个新切片 // 其长度为 2 个元素,容量为 4 个元素 newSlice := slice[1:3]
切片增长
相对于数组而言,使用切片的一个好处是,可以按需增加切片的容量。Go 语言内置的 append 函数会处理增加长度时的所有操作细节。
- 使用 append 向切片增加元素
1 2 3 4 5 6 7 8 9
// 创建一个整型切片 // 其长度和容量都是 5 个元素 slice := []int{10, 20, 30, 40, 50} // 创建一个新切片 // 其长度为 2 个元素,容量为 4 个元素 newSlice := slice[1:3] // 使用原有的容量来分配一个新元素 // 将新元素赋值为 60 newSlice = append(newSlice, 60)
- 使用 append 同时增加切片的长度和容量 ```go // 创建一个整型切片 // 其长度和容量都是 4 个元素 slice := []int{10, 20, 30, 40}
// 向切片追加一个新元素 // 将新元素赋值为 50 newSlice := append(slice, 50)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
##### 创建切片时的 3 个索引
在创建切片时,还可以使用之前我们没有提及的第三个索引选项。第三个索引可以用来控制 新切片的容量。其目的并不是要增加容量,而是要限制容量。
##### 迭代切片
切片是一个集合,可以迭代其中的元素。Go 语言有个特殊的关键字 range,它可以配合关键字 for 来迭代切片里的元素。
- 使用for range迭代切片
```go
// 创建一个整型切片
// 其长度和容量都是 4 个元素
slice := []int{10, 20, 30, 40}
// 迭代每一个元素,并显示其值
for index, value := range slice {
fmt.Printf("Index: %d Value: %d\n", index, value)
}
Output:
Index: 0 Value: 10
Index: 1 Value: 20
Index: 2 Value: 30
Index: 3 Value: 40
需要强调的是,range 创建了每个元素的副本,而不是直接返回对该元素的引用。
多维切片
和数组一样,切片是一维的,和之前对数组的讨论一样,可以组合多个切片形成多维切片。
在函数间传递切片
在函数间传递切片就是要在函数间以值的方式传递切片。由于切片的尺寸很小,在函数间复 制和传递切片成本也很低。
映射的内部实现和基础功能
映射是一种数据结构,用于存储一系列无序的键值对。 映射里基于键来存储值。 映射使用两个数据结构来存储数据。第一 个数据结构是一个数组,内部存储的是用于选择桶的散列键的高八位值。 这个数组用于区分每个 键值对要存在哪个桶里。第二个数据结构是一个字节数组,用于存储键值对。
内部实现
- 映射是一个集合,可以使用类似处理数组和切片的方式迭代映射中的元素。
- 映射是无序的集合,意味着没有办法预测键值对被返回的顺序。
- 无序的原因是映射的实现使用了散列表。
创建和初始化
Go 语言中有很多种方法可以创建并初始化映射,可以使用内置的 make 函数,也可以使用映射字面量。
- 使用 make 声明映射
1 2 3 4 5
// 创建一个映射,键的类型是 string,值的类型是 int dict := make(map[string]int) // 创建一个映射,键和值的类型都是 string // 使用两个键值对初始化映射 dict := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}
- 使用映射字面量声明空映射 ```go // 创建一个映射,使用字符串切片作为映射的键 dict := map[[]string]int{}
Compiler Exception: invalid map key type []string
1
2
3
4
5
6
7
8
9
10
#### 使用映射
键值对赋值给映射,是通过指定适当类型的键并给这个键赋一个值来完成的。
- 为映射赋值
```go
// 创建一个空映射,用来存储颜色以及颜色对应的十六进制代码
colors := map[string]string{}
// 将 Red 的代码加入到映射
colors["Red"] = "#da1337"
在函数间传递映射
在函数间传递映射并不会制造出该映射的一个副本。实际上,当传递映射给一个函数,并对 这个映射做了修改时,所有对这个映射的引用都会察觉到这个修改。
- 在函数间传递映射 ```go func main() { // 创建一个映射,存储颜色以及颜色对应的十六进制代码 colors := map[string]string{ “AliceBlue”: “#f0f8ff”, “Coral”: “#ff7F50”, “DarkGray”: “#a9a9a9”, } “ForestGreen”: “#228b22”, // 显示映射里的所有颜色 for key, value := range colors { fmt.Printf(“Key: %s Value: %s\n”, key, value) } // 调用函数来移除指定的键 removeColor(colors, “Coral”)
// 显示映射里的所有颜色 for key, value := range colors { fmt.Printf(“Key: %s Value: %s\n”, key, value) } } // removeColor将指定映射里的键删除 func removeColor(colors map[string]string, key string) { delete(colors, key) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
### 小结
- 数组是构建切片和映射的基石。
- Go语言里切片经常用来处理数据的集合,映射用来处理具有键值对结构的数据。
- 内置函数make可以创建切片和映射,并指定原始的长度和容量。也可以直接使用切片和映射字面量,或者使用字面量作为变量的初始值。
- 切片有容量限制,不过可以使用内置的append函数扩展容量。
- 映射的增长没有容量或者任何限制。
- 内置函数len可以用来获取切片或者映射的长度。
- 内置函数cap只能用于切片。
- 通过组合,可以创建多维数组和多维切片。也可以使用切片或者其他映射作为映射的值。但是切片不能作为映射的值。
- 将切皮或者映射传递给函数成本很低,并且不会复制底层的数据结构。
## Go语言的类型系统
- 声明新的用户定义的类型
- 使用方法,为类型增加新的行为
- 了解何时使用指针,何时使用值
- 通过接口实现多态
- 通过组合来扩展或改变类型
- 公开或者未公开的标志符
Go 语言是一种静态类型的编程语言。这意味着,编译器需要在编译时知晓程序里每个值的类型。
值的类型给编译器提供两部分信息:第一部分,需要分配多少内存给这个值(即值的规模); 第二部分,这段内存表示什么。
### 用户定义的类型
- 当用户声明一个新类型时,这个声明就给编译器提供了一个框架,告知必要的内存大小和表示信息。
- Go语言里声明用户定义的类型有两种方法。
#### 关键字 struct
结构类型通过组合一系列固定且唯一的字段来声明。
结构里每个字段 都会用一个已知类型声明。
这个已知类型可以是内置类型,也可以是其他用户定义的类型。
- 声明一个结构类型
```go
// user在程序里定义一个用户类型
type user struct {
name string
email string
ext int
privileged bool
}
// admin需要一个user类型作为管理者,并附加权限
type admin struct {
person user
level string
}
//声明admin类型的变量
fred := admin{
person: user{
name: "Lisa",
email: "lisa@email.com",
ext: 123,
privileged: true,
},
level: "super",
}
基于一个已有的类型,将其作为新类型的类型说明
- 基于
int64
声明一个新类型1
type Duration int64
虽然 int64
类型是基础类型,Duration类型依然是一个独立的类型。两种不同类型的值即便互相兼容,也不能互相赋值。编译器不会对不同类型的值做隐式转换。
方法
方法能给用户定义的类型添加新的行为。 方法实际上也是函数,只是在声明时,在关键字func和方法名之间增加了一个参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 这个程序展示如何声明并使用方法
package main
import "fmt"
// user 在程序里定义一个用户类型
type user struct {
name string
email string
}
// notify使用值接受者实现了一个方法
func (u user) notify() {
fmt.Printf("Sending User Email To %s <%s>\n", u.name, u.email)
}
func (u *user) changeEmail(email string) {
u.email = email
}
// main是应用程序的入口
func main() {
// user 类型的值可以用来调用
// 使用者接受者声明的方法
bill := user{"Bill", "bill@email.com"}
bill.notify()
// 指向user类型值的指针也可以用来调用
// 使用者接受者声明的方法
lisa := &user{"Lisa", "lisa@email.com"}
lisa.notify()
// user 类型的值可以直接用来调用
// 使用指针接受者的方法
bill.changeEmail("bill@newdomain.com")
bill.notify()
// 指向user类型值的指针可以用来调用
lisa.changeEmail("lisa@newdomain.com")
lisa.notify()
}
类型的本质
在声明一个新类型之后,声明一个该类型的方法之前,需要先回答一个问题:这个类型的本”质是什么。 程序内部传递这个类型的值的方式:是按值做传递,还是按指针做传递。 这个背后的原则是,不要只关注某个方法是如何处理这个值,而是要关注这个值的本质是什么。
内置类型
- 内置类型是由语言提供的一组类型。
- 这些类型本质上是原始的类型,分别是数值类型、字符串类型和布尔类型。
- 因此,当对这些值进行增加或者删除的时候,会创建一个新值。
基于这个结论,当把这些类型的值传递给方法或者函数时,应该传递一个对应值的副本。
引用类型
Go语言里的引用类型有如下几个:切片、映射、通道、接口和函数类型。
- 当声明上述类型的变量时,创建的变量被称作标头(header)值。
- 每个引用类型创建的标头值是包含一个指向底层数据结构的指针。
- 每个引用类型还包含一组独特的字段,用于管理底层数据结构。
标头值里包含一个指针,因此通过复制来传递一个引用类型的值的副本,本质上就是在共享底层数据结构。
结构类型
结构类型可以用来描述一组数据值,这组值的本质即可以是原始的,也可以是非原始的。
是使用值接收者还是指针接收者,不应该由该方法是否修改了接收到的值来决定。这个决策应该基于该类型的本质。 这条规则的一个例外是,需要让类型值符合某个接口的时候,即便类型的本质是非原始本质的,也可以选择使用值接收者声明方法。
接口
多态是指代码可以根据类型的具体实现采取不同行为的能力。
标准库
标准库 io
和 bytes
使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main
import (
"bytes"
"fmt"
"io"
"log"
"net/http"
"os"
)
// init 函数在main函数之前调用
func init() {
if len(os.Args) != 2 {
fmt.Println("Usage go run listing34.go <url>")
os.Exit(-1)
}
}
// main函数是程序的入库
func main() {
var b bytes.Buffer
// 将字符串写入Buffer
b.Write([]byte("starting"))
io.Copy(os.Stdout, &b)
// 从Web服务器得到响应
r, err := http.Get(os.Args[1])
if err != nil {
fmt.Println(err)
return
}
// 从Body复制到stdout
io.Copy(os.Stdout, r.Body)
if err := r.Body.Close(); err != nil {
fmt.Println(err)
}
log.Println("ending")
}
实现
接口是用来定义行为的类型。 这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。 对接口值方法的调用会执行接口值里存储的用户定义的类型的值对应的方法。
方法集
方法集定义了接口的接受规则。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main
import "fmt"
// notifier是一个定义了通知类型为的接口
type notifier interface {
notify()
}
// user在程序里定义一个用户类型
type person struct {
name string
email string
}
// notify 是使用指针接受者实现的方法
func (p *person) notify() {
fmt.Printf("Sending User Email By Pointer Type Method.\n")
fmt.Printf("Sending User Email %s <%s> \n", p.name, p.email)
}
// main 是程序的入口
func main() {
// 创建一个user类型的值,并发送通知
p := person{"Bill", "bill@email.com"}
sendNotification(&p)
}
// sendNotification 接收一个实现了notifier接口的值,并发送通知
func sendNotification(n notifier) {
n.notify()
}
多态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main
import "fmt"
type notifier interface {
notify()
}
type user struct {
name string
email string
}
type admin struct {
name string
email string
}
func (u *user) notify() {
fmt.Printf("Sending User Email to %s <%s> \n", u.name, u.email)
}
func (a *admin) notify() {
fmt.Printf("Sending Admin Email to %s <%s> \n", a.name, a.email)
}
func main() {
bill := user{"Bill", "bill@email.com"}
sendNotification(&bill)
lisa := admin{"Lisa", "lisa@email.com"}
sendNotification(&lisa)
}
func sendNotification(n notifier) {
n.notify()
}
嵌入类型
- Go 语言允许用户扩展或者修改已有类型的行为。
- 这个功能是通过嵌入类型(type embedding)完成的。
- 嵌入类 型是将已有的类型直接声明在新的结构类型里。
- 被嵌入的类型被称为新的外部类型的内部类型。
公开或未公开的标识符
要想设计出好的API,需要使用某种规则来控制声明后的标识符的可见性。 Go 语言支持从包里公开或者隐藏标识符。 在这种情况,需要一种方法,将这些标识符声明为包外不可见,这时需要将这些标识符声明为未公开的。
小结
- 使用关键字 struct 或者通过指定已经存在的类型,可以声明用户定义的类型。
- 方法提供了一种给用户定义的类型增加行为的方式。
- 设计类型时需要确认类型的本质是原始的,还是非原始的。
- 接口是声明了一组行为并支持多态的类型。
- 嵌入类型提供了扩展类型的能力,而无需使用继承。
- 标识符要么是从包里公开的,要么是在包里未公开的。
并发
- 利用goroutine运行程序
- 检测并修正竞争状态
- 利用通道共享数据
Go 语言的并发同步模型来自一个叫作通信顺序进程(Communicating Sequential Processes,CSP) 的范型(paradigm)。 CSP 是一种消息传递模型,通过在 goroutine 之间传递数据来传递消息,而不是 对数据进行加锁来实现同步访问。 用于在 goroutine 之间同步和传递数据的关键数据类型叫作通道(channel)。
并发与并行
操作系统会在物理处理器上调度线程来运行,而 Go 语言的运行时会在逻辑处理器上调度 goroutine来运行。
Go语言的 运行时默认会为每个可用的物理处理器分配一个逻辑处理器。在 1.5 版本之前的版本中,默认给 整个应用程序只分配一个逻辑处理器。这些逻辑处理器会用于执行所有被创建的goroutine。即便只有一个逻辑处理器,Go也可以以神奇的效率和性能,并发调度无数个goroutine。
如果创建一 个 goroutine 并准备运行,这个 goroutine 就会被放到调度器的全局运行队列中。之后,调度器就 将这些队列中的 goroutine 分配给一个逻辑处理器,并放到这个逻辑处理器对应的本地运行队列中。本地运行队列中的 goroutine 会一直等待直到自己被分配的逻辑处理器执行。
有时,正在运行的 goroutine 需要执行一个阻塞的系统调用,如打开一个文件。当这类调用 发生时,线程和 goroutine 会从逻辑处理器上分离,该线程会继续阻塞,等待系统调用的返回。 与此同时,这个逻辑处理器就失去了用来运行的线程。所以,调度器会创建一个新线程,并将其 绑定到该逻辑处理器上。之后,调度器会从本地运行队列里选择另一个 goroutine 来运行。一旦 被阻塞的系统调用执行完成并返回,对应的 goroutine 会放回到本地运行队列,而之前的线程会 保存好,以便之后可以继续使用。
如果一个 goroutine 需要做一个网络 I/O 调用,流程上会有些不一样。在这种情况下,goroutine 会和逻辑处理器分离,并移到集成了网络轮询器的运行时。一旦该轮询器指示某个网络读或者写 操作已经就绪,对应的 goroutine 就会重新分配到逻辑处理器上来完成操作。调度器对可以创建 的逻辑处理器的数量没有限制,但语言运行时默认限制每个程序最多创建 10 000 个线程。这个 限制值可以通过调用 runtime/debug 包的 SetMaxThreads 方法来更改。如果程序试图使用 更多的线程,就会崩溃。
并发(concurrency)不是并行(parallelism)。
- 并行是让不同的代码片段同时在不同的物理处 理器上执行。
- 并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做 了一半就被暂停去做别的事情了。
在很多情况下,并发的效果比并行好,因为操作系统和硬件的 总资源一般很少,但能支持系统同时做很多事情。
如果希望让 goroutine 并行,必须使用多于一个逻辑处理器。当有多个逻辑处理器时,调度器会将 goroutine 平等分配到每个逻辑处理器上。这会让 goroutine 在不同的线程上运行。
不过要想真的实现并行的效果,用户需要让自己的程序运行在有多个物理处理器的机器上。否则,哪怕 Go 语 言运行时使用多个线程,goroutine 依然会在同一个物理处理器上并发运行,达不到并行的效果。
goroutine
了解一下调度器的行为,以及调度器是如何创建 goroutine 并管理其寿命的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
// 分配一个逻辑处理器给调度器使用
// 调用了 runtime 包的 GOMAXPROCS 函数。
// 这个函数允许程序 更改调度器可以使用的逻辑处理器的数量。
// 如果不想在代码里做这个调用,也可以通过修改和这个函数名字一样的环境变量的值来更改逻辑处理器的数量。
// 给这个函数传入1,是通知调度器只 能为该程序使用一个逻辑处理器。
runtime.GOMAXPROCS(2)
// wg用来等待程序完成
// 计数加2,表示要等待两个goroutine
var wg sync.WaitGroup
wg.Add(2)
fmt.Println("Start Goroutines")
// 声明一个匿名函数,并创建一个goroutine
go func() {
// 在函数退出时,调用Done来通知main函数工作已经完成
defer wg.Done()
// 显示字母表3次
for count := 0; count < 3; count++ {
for char := 'a'; char < 'a'+26; char++ {
fmt.Printf("%c ", char)
}
}
}()
// 声明一个匿名函数,并创建一个goroutine
go func() {
// 在函数退出时,调用Done来通知main函数工作已经完成
// 关键字 defer 会修改函数调用时机,在正在执行的函数返回时才真正调用 defer 声明的函 数。
// 对这里的示例程序来说,我们使用关键字 defer 保证,每个 goroutine 一旦完成其工作就调 用 Done 方法。
defer wg.Done()
// 基于调度器的内部算法,一个正运行的 goroutine 在工作结束前,可以被停止并重新调度。
// 调度器这样做的目的是防止某个 goroutine 长时间占用逻辑处理器。
// 当 goroutine 占用时间过长时, 调度器会停止当前正运行的 goroutine,并给其他可运行的 goroutine 运行的机会。
// 显示字母表3次
for count := 0; count < 3; count++ {
for char := 'A'; char < 'A'+26; char++ {
fmt.Printf("%c ", char)
}
}
}()
// 等待goroutine结束
fmt.Println("Waiting To Finish")
// WaitGroup 是一个计数信号量,可以用来记录并维护运行的 goroutine。
// 如果 WaitGroup 的值大于 0,Wait 方法就会阻塞。
wg.Wait()
fmt.Println("\nTerminating Program")
}
- 如何修改逻辑处理器的数量
1
2
3
import "runtime"
// 给每个可用的核心分配一个逻辑处理器
runtime.GOMAXPROCS(runtime.NumCPU())
包 runtime 提供了修改 Go 语言运行时配置参数的能力。
函数 NumCPU 返回可以使用的物 理处理器的数量。因此,调用 GOMAXPROCS 函数就为每个可用的物理处理器创建一个逻辑处理 器。需要强调的是,使用多个逻辑处理器并不意味着性能更好。在修改任何语言运行时配置参数 的时候,都需要配合基准测试来评估程序的运行效果。
竞争状态
如果两个或者多个 goroutine 在没有互相同步的情况下,访问某个共享的资源,并试图同时 读和写这个资源,就处于相互竞争的状态,这种情况被称作竞争状态(race candition)。
对一个共享资源的读和写操作必须是原子化的,换句话说,同一时刻只能有一个 goroutine 对共享资源进行读和写操作。
锁住共享资源
Go 语言提供了传统的同步 goroutine 的机制,就是对共享资源加锁。 如果需要顺序访问一个 整型变量或者一段代码,atomic 和 sync 包里的函数提供了很好的解决方案。
原子函数
原子函数能够以很底层的加锁机制来同步访问整型变量和指针。
使用了 atmoic 包的 AddInt64 函数。这个函数会同步整型值的加法, 方法是强制同一时刻只能有一个 goroutine 运行并完成这个加法操作。 当 goroutine 试图去调用任 何原子函数时,这些 goroutine 都会自动根据所引用的变量做同步处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
)
var (
counter int64
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go incCounter(1)
go incCounter(2)
wg.Wait()
fmt.Println("Final Counter:", counter)
}
func incCounter(id int) {
defer wg.Done()
for count := 0; count < 2; count++ {
atomic.AddInt64(&counter, 1)
runtime.Gosched()
}
}
另外两个有用的原子函数是 LoadInt64 和 StoreInt64。这两个函数提供了一种安全地读 和写一个整型值的方式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
var (
// shutdown 是通知正在执行的goroutine停止工作的标志
shutdown int64
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go dbWork("A")
go dbWork("B")
time.Sleep(1 * time.Second)
fmt.Println("Shutdown Now")
atomic.StoreInt64(&shutdown, 1)
wg.Wait()
}
func dbWork(name string) {
defer wg.Done()
for {
fmt.Printf("Doing %s Work\n", name)
time.Sleep(250 * time.Millisecond)
fmt.Println("Shutdown:", shutdown)
if atomic.LoadInt64(&shutdown) == 1 {
fmt.Printf("Shutting %s Down\n", name)
break
}
}
}
互斥锁
另一种同步访问共享资源的方式是使用互斥锁(mutex)。互斥锁这个名字来自互斥(mutual exclusion)的概念。 互斥锁用于在代码上创建一个临界区,保证同一时间只有一个 goroutine 可以执行这个临界区代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 这个程序展示如何使用互斥锁
// 定义一段需要同步访问的代码临界区
// 资源的同步访问
package main
import (
"fmt"
"runtime"
"sync"
)
var (
counter int
wg sync.WaitGroup
// mutex 用来定义一段代码临界区
mutex sync.Mutex
)
func main() {
wg.Add(2)
go incCounter(1)
go incCounter(2)
wg.Wait()
fmt.Printf("Final Counter: %d \n", counter)
}
// incCounter 使用互斥锁来同步并保证安全访问
// 增加包里的counter变量的值
func incCounter(id int) {
defer wg.Done()
for count := 0; count < 2; count++ {
// 同一时刻只允许一个goroutine进入这个临界区
mutex.Lock()
{
value := counter
runtime.Gosched()
value++
counter = value
}
// 释放锁,允许其他正在等待的goroutine进入临界区
mutex.Unlock()
}
}
// 同一时刻只有一 个 goroutine 可以进入临界区。
// 之后,直到调用 Unlock()函数之后,其他 goroutine 才能进入临 界区。
// 当强制将当前 goroutine 退出当前线程后,调度器会再次分配这个 goroutine 继续运 行。
// 当程序结束时,我们得到正确的值 4,竞争状态不再存在。
通道
原子函数和互斥锁都能工作,但是依靠它们都不会让编写并发程序变得更简单,更不容易出 错,或者更有趣。 在 Go 语言里,你不仅可以使用原子函数和互斥锁来保证对共享资源的安全访 问以及消除竞争状态,还可以使用通道,通过发送和接收需要共享的资源,在goroutine之间做同步。
当一个资源需要在 goroutine 之间共享时,通道在 goroutine 之间架起了一个管道,并提供了确保同步交换数据的机制。 声明通道时,需要指定将要被共享的数据的类型。可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针。
- 使用make创建通道 ```go // 无缓冲的整型通道 unbuffered := make(chan int)
// 有缓冲的字符串通道 buffered := make(chan string, 10)
// 通过通道发送一个字符串 buffered <- “Gopher”
// 从通道接收一个字符串 // 当从通道里接收一个值或者指针时,<-运算符在要操作的通道变量的左侧 value := <-buffered
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#### 无缓冲的通道
- 无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道。
- 这种类型的通 道要求发送 goroutine 和接收 goroutine 同时准备好,才能完成发送和接收操作。
- 如果两个 goroutine 没有同时准备好,通道会导致先执行发送或接收操作的 goroutine 阻塞等待。
- 这种对通道进行发送 和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在
```go
// 程序展示如何使用无缓冲的通道来模拟
// 2个goroutine间的网球比赛
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var wg sync.WaitGroup
func init() {
rand.Seed(time.Now().UnixNano())
}
func main() {
court := make(chan int)
wg.Add(2)
// 启动两个选手
go player("Nadal", court)
go player("Djokovic", court)
// 发球
court <- 1
wg.Wait()
}
// player 模拟一个选手在打网球
func player(name string, court chan int) {
defer wg.Done()
for {
// 等待球被击打过来
ball, ok := <-court
if !ok {
fmt.Printf("Player %s Won\n", name)
return
}
// 选随机数,然后用这个数来判断我们是否丢球
n := rand.Intn(100)
if n%13 == 0 {
fmt.Printf("Player %s Missed\n", name)
close(court)
return
}
fmt.Printf("Player %s Hit %d\n", name, ball)
ball++
court <- ball
}
}
有缓冲的通道
- 有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个值的通道。
- 这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。
- 通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。
- 只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。 这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 程序展示如何使用有缓冲的通道和固定数目的
// goroutine来处理一堆工作
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
const (
numberGoroutines = 4 // 要使用的goroutine的数量
taskLoad = 10 // 要处理的工作的数量
)
var wg sync.WaitGroup
func init() {
// 初始化随机数种子
rand.Seed(time.Now().Unix())
}
func main() {
// 创建一个有缓冲的通道来管理工作
tasks := make(chan string, taskLoad)
// 启动goroutine来处理工作
wg.Add(numberGoroutines)
for gr := 1; gr <= numberGoroutines; gr++ {
go worker(tasks, gr)
}
// 增加一组要完成的工作
for post := 1; post <= taskLoad; post++ {
tasks <- fmt.Sprintf("Tasks: %d", post)
}
// 当所有工作都处理完时关闭通道
// 以便所有goroutine退出
close(tasks)
// 等待所有工作完成
wg.Wait()
}
// worker 作为goroutine启动来处理
// 从有缓冲区的通道传入的工作
func worker(tasks chan string, worker int) {
defer wg.Done()
for {
task, ok := <-tasks
if !ok {
fmt.Printf("Worker: %d: Shutting Down\n", worker)
return
}
// 显示开始工作了
sleep := rand.Int63n(100)
time.Sleep(time.Duration((sleep)) * time.Millisecond)
// 显示完成了工作
fmt.Printf("Worker: %d : COmpleted %s:\n", worker, task)
}
}
由于程序和 Go 语言的调度器带有随机成分,这个程序每次执行得到的输出会不一样。不过, 通过有缓冲的通道,使用所有 4 个 goroutine 来完成工作,这个流程不会变。 从输出可以看到每个 goroutine 是如何接收从通道里分发的工作。
小结
- 并发是指goroutine运行的时候是相互独立的。
- 使用关键字go创建goroutine来运行函数。
- goroutine在逻辑处理器上执行,而逻辑处理器具有独立的系统线程和运行队列。
- 竞争状态是指两个或者多个goroutine试图访问同一个资源。
- 原子函数和互斥锁提供了一种防止出现竞争状态的方法。
- 通道提供了一种在两个goroutine之间共享数据的简单方法。
- 无缓冲的通道保证同时交换数据,而有缓冲的通道不做这种保证。
并发模式
- 控制程序的生命周期
- 管理可复用的资源池
- 创建可以处理任务的goroutine池
runner
- runner包用于展示如何使用通道来监视程序的执行时间,如果程序运行时间太长,也可以用runner包来终止程序。
- 当开发需要调度后台处理任务的程序的时候,这种模式会很有用。
- 这个程序可能会作为 cron 作业执行,或者在基于定时任务的云环境(如 iron.io)里执行。
runner.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// runner包管理处理任务的运行和生命周期
package runner
import (
"os"
"time"
"github.com/pkg/errors"
"os/signal"
)
// Runner在给定的超时时间内执行一组任务
// 并且在操作系统发送中断信号时结束这些任务
type Runner struct {
// interrupt通道报告从操作系统发送的信号
interrupt chan os.Signal
// complete通道报告处理任务已经完成
complete chan error
// timeout报告处理任务已经超时
timeout <-chan time.Time
// tasks持有一组以索引顺序依次执行的函数
tasks []func(int2 int)
}
var ErrTimeout = errors.New("received timeout")
var ErrInterrupt = errors.New("received interrupt")
// New会返回一个新的准备使用的Runner
func New(d time.Duration) *Runner {
return &Runner{
interrupt: make(chan os.Signal, 1),
complete: make(chan error),
timeout: time.After(d),
}
}
// Add将一个任务附加到Runner上。这个任务是一个接收一个int类型的ID作为参数的函数
func (r *Runner) Add(tasks ...func(int2 int)) {
r.tasks = append(r.tasks, tasks...)
}
// Start执行所有任务,并监视通道事件
func (r *Runner) Start() error {
// 我们希望接收所有中断信号
signal.Notify(r.interrupt, os.Interrupt)
// 用不同的goroutine执行不同的任务
go func() {
r.complete <- r.run()
}()
select {
// 当任务处理完成时发出的信号
case err := <- r.complete:
return err
// 当任务处理程序运行超时时发出的信号
case <-r.timeout:
return ErrTimeout
}
}
// run执行每一个已注册的任务
func (r *Runner) run() error {
for id, task := range r.tasks {
// 检测操作系统的中断信号
if r.gotInterrupt() {
return ErrInterrupt
}
// 执行已注册的任务
task(id)
}
return nil
}
// gotInterrupt验证是否接收到了中断信号
func (r *Runner) gotInterrupt() bool {
select {
// 当中断事件被触发时发出的信号
case <- r.interrupt:
// 停止接收后续的任何信号
signal.Stop(r.interrupt):
return true
default:
return false
}
}
// 程序可以在分配的时间内完成工作,正常终止
// 程序没有及时完成工作,“自杀”
// 接收到操作系统发送的中断事件,程序立刻试图清理状态并停止工作
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 程序演示如何使用通道来监视
// 程序运行的时间,以在程序运行时间过长
// 时如何终止程序
package main
import (
"runner"
"log"
"os"
"time"
)
const timeout = 3 * time.Second
func main() {
log.Println("Starting work.")
r := runner.New(timeout)
r.Add(createTask(), createTask(), createTask())
if err := r.Start(); err != nil {
switch err {
case runner.ErrTimeout:
log.Println("Terminating due to timeout")
os.Exit(1)
case runner.ErrInterrupt:
log.Println("Terminating due to interrupt")
os.Exit(2)
}
}
log.Println("Process ended.")
}
func createTask() func(int2 int) {
return func(id int) {
log.Printf("Processor")
time.Sleep(time.Duration(id) * time.Second)
}
}
pool
- pool包用于展示如何使用有缓冲的通道实现资源池,来管理可以在任意数量的goroutine之间共享及独立使用的资源。
- 这种模式在需要共享一组静态资源的情况(如共享数据库连接或者内存缓冲区)下非常有用。
- 如果goroutine需要从池里得到这些资源中的一个,它可以从池里申请,使用完后归还到资源池里。
pool.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package pool
import (
"github.com/pkg/errors"
"io"
"log"
"sync"
)
// Pool管理一组可以安全地在多个goroutine间
// 共享的资源。被管理的资源必须
// 实现io.Closer接口
type Pool struct {
m sync.Mutex
resources chan io.Closer
factory func() (io.Closer, error)
closed bool
}
// ErrPoolClosed表示请求(Acquire)了一个已经关闭的池
var ErrPoolClosed = errors.New("Pool has been closed")
// New创建一个用来管理资源的池。
// 这个池需要一个可以分配新资源的函数,
// 并规定池的大小
func New(fn func() (io.Closer, error), size uint) (*Pool, error) {
if size <= 0 {
return nil, errors.New("Size value too small")
}
return &Pool{
factory: fn,
resources: make(chan io.Closer, size),
}, nil
}
// Acquire 从池中获取一个资源
func (p *Pool) Acquire() (io.Closer, error) {
select {
// 检查是否有空余的资源
case r, ok := <-p.resources:
log.Println("Acquire:", "Shared Resource")
if !ok {
return nil, ErrPoolClosed
}
return r, nil
default:
log.Println("Acquire:", "New Resource")
return p.factory()
}
}
// Release将一个使用后的资源放回池里
func (p *Pool) Release(r io.Closer) {
// 保证本操作和Close操作的安全
p.m.Lock()
defer p.m.Unlock()
// 如果池已经被关闭,销毁这个资源
if p.closed {
r.Close()
return
}
select {
// 试图将这个资源放进队列
case p.resources <- r:
log.Println("Release:", "In Queue")
// 如果队列已满,则关闭这个资源
default:
log.Println("Release:", "Closing")
r.Close()
}
}
// Close 会让资源池停止工作,并关闭所有现有的资源
func (p *Pool) Close() {
p.m.Lock()
defer p.m.Unlock()
if p.closed {
return
}
p.closed = true
close(p.resources)
for r := range p.resources {
r.Close()
}
}
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// 程序展示如何使用pool包
// 来共享一组模拟的数据库连接
package main
import (
"io"
"log"
"math/rand"
"sync"
"sync/atomic"
"time"
"pool"
)
const (
// 要使用的goroutine的数量
maxGoroutines = 25
// 池中的资源的数量
pooledResources = 2
)
// dbConnection模拟要共享的资源
type dbConnection struct {
ID int32
}
// idCounter用来给每个连接分配一个独一无二的id
var idCounter int32
func (dbConn *dbConnection) Close() error {
log.Println("Close: Connection", dbConn.ID)
return nil
}
// createConnection是一个工厂函数,
// 当需要一个新连接时,资源池会调用这个函数
func createConnection() (io.Closer, error) {
id := atomic.AddInt32(&idCounter, 1)
log.Println("Create: New Connection", id)
return &dbConnection{id}, nil
}
func main() {
var wg sync.WaitGroup
wg.Add(maxGoroutines)
// 创建用来管理连接的池
p, err := pool.New(createConnection, pooledResources)
if err != nil {
log.Println(err)
}
// 使用池里的连接来完成查询
for query := 0; query < maxGoroutines; query++ {
go func(q int) {
performQueries(q, p)
wg.Done()
}(query)
}
wg.Wait()
log.Println("Shutdown Program.")
p.Close()
}
// performQueries 用来测试连接的资源池
func performQueries(query int, p *pool.Pool) {
// 从池里请求一个连接
conn, err := p.Acquire()
if err != nil {
log.Println(err)
return
}
// 将该连接释放回池里
defer p.Release(conn)
// 用等待来模拟查询响应
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
log.Printf("QID[%d] CID[%d]\n", query, conn.(*dbConnection).ID)
}
worker
- work包的目的是展示如何使用无缓冲的通道来创建一个 goroutine 池,这些goroutine执行并控制一组工作,让其并发执行。
- 无缓冲的通道保证两个 goroutine 之间的数据交换。
- 使用无缓冲的通道不会有工作在队列里丢失或者卡住,所有工作都会被处理。
worker.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// work包管理一个goroutine池来完成工作
package worker
import "sync"
// Worker必须满足接口类型,才能使用工作池
type Worker interface {
Task()
}
// Pool提供一个goroutine池,这个池可以完成任何已提交的Worker任务
type Pool struct {
work chan Worker
wg sync.WaitGroup
}
func New(maxGoroutines int) *Pool {
p := Pool{
work: make(chan Worker),
}
p.wg.Add(maxGoroutines)
for i := 0; i < maxGoroutines; i++ {
go func() {
for w := range p.work {
w.Task()
}
p.wg.Done()
}()
}
return &p
}
func (p *Pool) Run(w Worker) {
p.work <- w
}
func (p *Pool) Shutdown() {
close(p.work)
p.wg.Wait()
}
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main
import (
"log"
"time"
"worker"
"sync"
)
var names = []string{
"steve",
"bob",
"mary",
"therese",
"jason",
}
type namePrinter struct {
name string
}
func (m *namePrinter) Task() {
log.Println(m.name)
time.Sleep(time.Second)
}
func main() {
p := work.New(2)
var wg sync.WaitGroup
wg.Add(100 * len(names))
for i := 0; i< 100; i++ {
for _, name := range names {
np := namePrinter{
name: name,
}
go func() {
p.Run(&np)
wg.Done()
}()
}
}
wg.Wait()
p.Shutdown()
}
小结
- 可以使用通道来控制程序的生命周期。
- 带 default 分支的 select 语句可以用来尝试向通道发送或者接收数据,而不会阻塞。
- 有缓冲的通道可以用来管理一组可复用的资源。
- 语言运行时会处理好通道的协作和同步。
- 使用无缓冲的通道来创建完成工作的 goroutine 池。
- 任何时间都可以用无缓冲的通道来让两个 goroutine 交换数据,在通道操作完成时一定保证对方接收到了数据。
标准库
- 输出日志以及纪录日志
- 对JSON进行编码和解码
- 处理输入和输出,并以流的方式处理数据
- 让标准库里多个包协同工作
Go 标准库是一组核心包,用来扩展和增强语言的能力。由于这些包和语言绑在一起发布,它们会得到以下特殊的保证:
- 每次语言更新,哪怕是小更新,都会带有标准库;
- 这些标准库会严格遵守向后兼容的承诺;
- 标准库是 Go 语言开发、构建、发布过程的一部分;
- 标准库由 Go 的构建者们维护和评审;
- 每次 Go 语言发布新版本时,标准库都会被测试,并评估性能。
文档与源代码
- 标准库里的顶级目录和包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
archive debug hash mime sort time bufio encoding html net strconv unicode bytes errors image os strings unsafe compress expvar index path sync container flag io reflect syscall crypto database fmt go log math regexp runtime testing text
Go 语言团队在网站上维护了一个文档,参见 http://golang.org/pkg/。
记录日志
- 即便没有表现出来,你的程序依旧可能有bug。
- 日志是一种找到这些 bug,更好地了解程序工作状态的方法。
- 日志是开发人员的眼睛和耳朵,可以用来跟踪、调试和分析代码。 基于此,标准库提供了log包,可以对日志做一些最基本的配置。根据特殊需要,开发人员还可以自己定制日志记录器。
log 包
- 跟踪日志的样例
1
TRACE: 2009/11/10 23:00:00.000000 /tmpfs/gosandbox-/prog.go:14: message
- 使用最基本的log包 ```go package main
import “log”
func init() { log.SetPrefix(“Trace: “) log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile) }
func main() { // Println写到标准日志记录器 log.Println(“message”)
1
2
3
4
5
// Fatalln 在调用 Println()之后会接着调用 os.Exit(1)
log.Fatalln("fatal message")
// Panicln 在调用 Println()之后会接着调用 panic()
log.Panicln("panic message") }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
- 定制的日志记录器
要想创建一个定制的日志记录器,需要创建一个 Logger 类型值。可以给每个日志记录器配 置一个单独的目的地,并独立设置其前缀和标志。
```go
package main
import (
"log"
"os"
"io/ioutil"
"io"
)
var (
Trace *log.Logger // 纪录所有日志
Info *log.Logger // 重要信息
Warning *log.Logger // 需要注意的信息
Error *log.Logger // 非常严重的问题
)
func init() {
file, err := os.OpenFile("error.txt", os.O_CREATE | os.O_WRONLY | os.O_APPEND, 0666)
if err != nil {
log.Fatalln("Failed to open error log file:", err)
}
Trace = log.New(ioutil.Discard, "Trace: ", log.Ldate|log.Ltime|log.Lshortfile)
Info = log.New(os.Stdout, "Info", log.Ldate|log.Ltime|log.Lshortfile)
Warning = log.New(os.Stdout, "Warning", log.Ldate|log.Ltime|log.Lshortfile)
Error = log.New(io.MultiWriter(file, os.Stderr), "Error", log.Ldate|log.Ltime|log.Lshortfile)
}
func main() {
Trace.Println("I have something standard to say")
Info.Println("Special Information")
Warning.Println("There is something you need to know about")
Error.Println("Something has failed")
}
编码/解码
如果程序需要处理 XML 或者 JSON,可以使用标准库里名为 xml 和 json 的包,它们可以处理这些格式的数据。 如果想实现自己的数据格式的编解码,可以将这些包的实现作为指导。
-
解码 JSON 使用 json 包的NewDecoder函数以及 Decode 方法进行解码。
-
编码 JSON 使用 json 包的 MarshalIndent 函数进行编码。 这个函数可以很方便地将 Go 语言的 map 类型的值或者结构类型的值转换为易读格式的 JSON 文档。 序列化(marshal)是指将数据转换为 JSON 字符串的过程。
结论
- 在标准库里都已经提供了处理 JSON 和 XML 格式所需要的诸如解码、反序列化以及序列化 数据的功能。
- 随着每次 Go 语言新版本的发布,这些包的执行速度也越来越快。
- 这些包是处理 JSON 和 XML 的最佳选择。
输入和输出
- 类 UNIX 的操作系统如此伟大的一个原因是,一个程序的输出可以是另一个程序的输入这一理念。
- 同样的理念扩展到了标准库的 io 包,而且提供的功能很神奇。这个包可以以流的方式高效 处理数据,而不用考虑数据是什么,数据来自哪里,以及数据要发送到哪里的问题。
Writer 和 Reader 接口
io 包是围绕着实现了 io.Writer 和 io.Reader 接口类型的值而构建的。 由于 io.Writer 和 io.Reader 提供了足够的抽象,这些 io 包里的函数和方法并不知道数据的类型,也不知道 这些数据在物理上是如何读和写的。
结论
应该花时间看一下标准库中提供了些什么,以及它 是如何实现的——不仅要防止重新造轮子,还要理解 Go 语言的设计者的习惯,并将这些习惯应 用到自己的包和 API 的设计上。
小结
- 标准库有特殊保证,并且被社区广泛应用
- 使用标准库的包会让你的代码更加有利于管理,别人也会更加信任你的代码
- 100余个包被合理组织,分布到38个类别里面
- 标准库log包拥有纪录日志所需的所有功能
- 标准库里的 xml 和 json 包让处理这两种数据格式变得很简单
- io 包支持以流的方式高效处理数据。
- 接口允许你的代码组合已有的功能。
- 阅读标准库的代码是熟悉 Go 语言习惯的好方法。
测试和性能
- 编写单元测试来验证代码的正确性
- 使用 httptest 来模拟基于 HTTP 的请求和响应
- 使用示例代码来给包写文档
- 通过基准测试来检查性能
使用 Go 语言的测试 框架,可以在开发的过程中就进行单元测试和基准测试。 和go build命令类似,go test命 令可以用来执行写好的测试代码,需要做的就是遵守一些规则来写测试。
单元测试
- 单元测试是用来测试包或者程序的一部分代码或者一组代码的函数。
-
测试的目的是确认目标 代码在给定的场景下,有没有按照期望工作。
- 一个场景是正向路经测试,就是在正常执行的情况 下,保证代码不产生错误的测试。
- 另外一些单元测试可能会测试负向路径的场景,保证代码不仅会产生错误,而且是预期的错误。
基础单元测试
基础测试(basic test)只使用一组参数和结果来测试一段代码。
表组测试
表组测试(table test)也会测试一段代码,但是会使用多组参数和结果进行测试。 表组测试除了会有一组不同的输入值和期望结果之外,其余部分都很像基础单元测试。 测试会依次迭代不同的值,来运行要测试的代码。 每次迭代的时候,都会检测返回的结果。
这便 于在一个函数里测试不同的输入值和条件。
模仿调用
- 不能总是假设运行测试的机器可以访问互联网。
- 此外,依赖不属于你的或者你无法操作的服 务来进行测试,也不是一个好习惯。
这两点会严重影响测试持续集成和部署的自动化。 如果突然断网,导致测试失败,就没办法部署新构建的程序。
为了修正这个问题,标准库包含一个名为 httptest 的包,它让开发人员可以模仿基于 HTTP 的网络调用。 模仿(mocking)是一个很常用的技术手段,用来在运行测试时模拟访问不 可用的资源。
测试服务端点
服务端点(endpoint)是指与服务宿主信息无关,用来分辨某个服务的地址,一般是不包含宿主的一个路径。 如果在构造网络API,你会希望直接测试自己的服务的所有服务端点,而不用启动整个网络服务。
包 httptest 正好提供了做到这一点的机制。
基准测试
基准测试是一种测试代码性能的方法。
- 想要测试解决同一问题的不同方案的性能,以及查看哪种解决方案的性能更好时,基准测试就会很有用。
- 基准测试也可以用来识别某段代码的 CPU或者内存效率问题,而这段代码的效率可能会严重影响整个应用程序的性能。
- 许多开发人员会用 基准测试来测试不同的并发模式,或者用基准测试来辅助配置工作池的数量,以保证能最大化系统的吞吐量。
小结
- 测试功能被内置到 Go 语言中,Go 语言提供了必要的测试工具。
- go test工具用来运行测试。
- 测试文件总是以_test.go 作为文件名的结尾。
- 表组测试是利用一个测试函数测试多组值的好办法。
- 包中的示例代码,既能用于测试,也能用于文档。
- 基准测试提供了探查代码性能的机制。