golang入门笔记
参考《Go 程序设计语言》
看本文前最好先看 google 的a tour of go
一、程序结构
- go 程序使用驼峰式命名风格
- 零值,变量的初始值。数字是 0,字符串是"",布尔值是 false,接口(interface)和引用类型(slice、指针、map、通道、函数)是 nil,数组和结构体的零值是其所有元素或成员的零值
- 注意,零值也是有类型的,nil 也是
- 短变量声明,
a,b := f(),a 和 b 中至少有一个得是新变量,不能全是已经声明的变量 - 一个例外,如果 a,b 是在外层作用域声明的,
:=会将它们声明为新变量 - 指针。函数返回局部变量的地址是安全的(见 6.变量的生命周期)
- new 函数,new 函数创建一个新值并返回其地址
- 变量的生命周期通过其是否可达确定(变量可以在其初次声明的作用域之外存活),编译器根据变量生命周期确定变量在栈上还是堆上分配,而不是根据声明变量的时候使用的是 var 还是 new
- 多重赋值,例如
a,b := 1,"xxx"或x,y = y,x,后者用来交换变量的值 - 类型转换,
var a T = T(b),每个类型都会提供T(x)将 x 的值转换为 T(前提是允许这种转换) - 导出的标识符才能在包外被访问到,导出的标识符以大写字母开头
- 包变量初始化,从初始化包级别变量开始,优先按照依赖顺序初始化变量,然后按照声明顺序初始化变量
- init 函数,可以有任意个,在程序启动时按照声明顺序自动执行
- 包的初始化按导入顺序进行,依赖顺序优先(类似包级别变量初始化)
二、基本数据
2.1 整型
- int8、int16、int32、int64、uint8、uint16、uint32、uint64
- int 和 uint,在不同平台上大小不同(通常是 32 位或 64 位)
- rune,等价于 int32,表示一个 unicode 码点
- byte,等价于 uint8,表示一个原始的字节
- uintptr,可以存放一个指针,用于底层编程
- golang 中%运算结果的正负号总是和被除数一致(取余,不是取模)
- 位运算。
&是与运算(AND),|是或运算(OR),^是异或运算和非运算(XOR,NOT),&^是与非运算(AND NOT),<<是左移,>>是右移 - 1 和 a 做异或等价于对 a 取反,即 1^a = ^a(这里的 1 和 a 是单独的一位)
- 与非运算的作用是按右操作数的位分布清空左操作数中的对应位
- 右移操作
>>- 有符号数左移是逻辑移位,符号位参与移位,低位补 0
- 有符号数右移是算术移位按符号位填补空位(因为移的是补码,补码补 1 就等于原码补 0)
- 无符号数都是逻辑移位
- 右移操作
x>>n等价于x/2^n,向下取整(朝负无穷方向取整,例如-5>>1 结果为-3)
2.2 浮点型
- 两种浮点型,float32 和 float64
- math 包给出了浮点型的极限值,例如
math.MaxFloat32 - 特殊值:正无穷、负无穷、无意义(+Inf、-Inf、NaN)。超出极限值的数和除以零的商归为正负无穷,0/0 或 sqrt(-1)为无意义
2.3 复数
- 两种复数,complex64,complex128
- 写法:
3.14i、1+2i - 可以使用
==和!=判断是否等值
2.4 布尔
- 逻辑运算的短路行为,如果运算符左边的操作数能直接确定最终结果,则右边的操作数不会计算在内
&&比||优先级高,助记技巧:&&是逻辑乘法,||是逻辑加法
2.5 字符串
- len 函数返回的是字节数,不是字符数
- 生成子串操作
s[i:j]取的是字节,不是字符。 - 下标访问操作
s[i]访问的也是第 i 个字节,不是字符 - range 循环
i,r := range "xxx"中的 i 表示字节序号,r 是字符(rune) - 字符串可以通过
==、<等比较运算符比较,比较运算按字节进行,结果服从其字典序排序 - 字符串值无法改变(字符串值所包含的字节序列永不可变),只能将一个新字符串赋值给字符串变量(例如,
s+="xxx"只是将+=运算新生成的字符串赋值给了 s,并没有改变 s 原有的字符串值)。这么设计的好处是: - 两个字符串变量能够安全地共用同一段底层内存
- 字符串拷贝的开销小
- 字符串字面量
- 转义。除了常见的
'\n'等转义字符以外,'\xhh'用 16 进制数hh表示这个字节,'\ooo'用八进制数ooo表示这个字节,这两者都表示单字节 - 原生字符串字面量用反引号
`...`书写。原生字符串中转义不起作用,可以包含换行,字符串内容和书写内容完全一致。 - utf-8
- utf-8 是 go 的默认编码
- unicode 字符有两种表示形式,
'\uhhhh'表示 16 位码点,'\Uhhhhhhhh'表示 32 位码点,区别是小写的 u 和大写的 U - 以下字符串是等价的,注意:直接用 16 进制转义(\x)书写的字符串是"世界"经 utf-8 编码后的实际字节,而\u 和\U 后面跟的是 unicode 码点,并不是实际的 utf-8 字节
go "世界" "\xe4\xb8\x96\xe7\x95\x8c" "\u4e16\u754c" "\U00004e16\U0000754c" - utf-8 编码规则(只有"xxx"部分才是 unicode 码点信息)
0xxxxxxx 110xxxxx 10xxxxxx 1110xxxx 10xxxxxx 10xxxxxx 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx - 使用函数
utf8.RuneCountInString(s)计算字符串中的字符数量 - utf-8 解码器在遇到一个不合理的字节时,会用一个专门的 unicode 符号
\uFFFD作为替换 - byte 和 rune 的区别,rune 保存的是 unicode 码点,并不是 utf-8 编码后的字节,因此,字符串转换为
[]rune之后,使用fmt.Printf("%x",s)输出的结果并不一样 string、[]byte、[]rune之间可以相互转换,byte、rune可以转换为 string- 字面量
'A'是 rune 类型 - 四个标准包:strings、bytes、strconv、unicode
- strings 包提供了字符串操作函数
- bytes 包提供了字节 slice 操作函数、可变字符串类型 bytes.Buffer
- strconv 包主要用于 string 和其他类型的转换
- unicode 包用于判别文字符号值
2.6 常量
- 常量是在编译阶段确定值的数据类型,因此常量可以出现在涉及到类型声明的地方(例如数组长度)
- 常量生成器 iota,用于创建一系列连续的常量值,从 0 开始取值,逐项加 1。iota 可以用在表达式里。
- 无类型常量,无类型常量可以有比基本类型更高的精度,至少 256 位(可以超过基本类型的最值)
三、复合数据类型
数组、slice、map、结构体
3.1 数组
var a [3]int = [3]int{1,2,3},也可以a := [...]int{1,2,3}- 使用
len(a)获取数组长度 - 初始化时可以同时指定索引,例如
a := [...]string{2:"hello",3:"world"} - 如果数组元素类型可比较,则数组也是可比较的,只能用
==和!= - 传数组参数时不是传引用,而是值传递,拷贝一份副本
- 数组长度不可变
3.2 slice
- slice 是一种可变长度的序列。slice 是一种轻量的数据结构,底层是个数组。
- slice 有三个属性,指针、长度和容量,长度是指 slice 的元素个数,长度小于等于容量。容量是从起始元素到底层数组的最后一个元素见元素的个数。使用
len和cap获取长度和容量。 - 一个底层数组可以对应多个 slice,slice 的范围可以相互重叠。
- slice 之间无法比较,可以用 bytes.Equal 比较两个字节 slice
- slice 值为 nil 时,长度和容量都为 0,也有不为 nil 但是长度和容量都是 0 的 slice,例如
[]int{}、make([]int,3)[3:] make([]T,len)、make([]T,len,cap)- append 可以用来追加元素。append 可能会引起底层数据扩容,也可能不会,因此每次调用 append 都必须更新 slice。
- 小知识,
func(y ...int),...代表 y 接受一个可变长度的参数列表
3.3 map
a := map[string]int{"hello":1,"world":5,}- 创建空 map:
make(map[K]V)、map[K]V{} - 通过下标的方式插入和访问,使用 delete 移除元素。即使 key 不存在,这些操作也是安全的,访问一个不存在的键时,其值为零值。
- map 中元素的迭代顺序不固定
- 小知识,使用 range 迭代时可以使用空白标识符
_忽略一个变量 - 判断一个 key 是否存在,
age,ok := a[xxx] - key 的类型必须是能用
==比较的类型,所以 key 不能是 slice - 可以通过将 slice 映射成一个字符串来解决,例如
fmt.Sprintf("%q",[]int{1,2,3}),谓词 q 是将一个值转换为对应的字符形式的字面值
3.4 结构体
- 点号既可以用在结构体上,也可以用在结构体指针上
- 成员首字母大写说明变量可导出
- 成员变量的顺序对结构体同一性很重要
- 结构体的零值由结构体成员的零值组成
- 没有任何成员的结构体被称为空结构体,
struct{},没有长度 - 如果结构体所有成员都能比较,那么结构体就可以比较(使用
==或!=) - 匿名成员
- 如果一个成员是结构体,且不带名称,则为匿名成员
- 可以直接访问匿名结构体的成员,而不用经过匿名结构体
- 但初始化时必须把所有中间成员都列出来
- 成员重名问题,如果匿名成员内部的一个成员和外部的一个成员重名了,访问时访问的是外部成员。如果两个匿名成员之间有成员重名了,则不允许直接访问重名成员,必须要指定中间成员
3.5 其他
- json
data,err := json.Marshal(xxx),Marshal 生成一个字节 slice。json.MarshalIndent()可以生成一个格式化的 json 字符串。err := json.Unmarshal(data,&xxx),Marshal 的逆操作- 结构体只有导出的成员才能被 Marshal,json 字段名称默认为成员名(区分大小写)
- 可以用成员标签定义字段名,例如
golang type a struct{ Year int `json:"released"` }(成员标签由一组空格分割的 key:"value"组成) - 模板(实现格式和代码彻底分离)
- 模板是一个字符串
- 一个
{{...}}称为操作,可以输出值、选择结构体成员、调用函数、提供控制逻辑、实例化其他模板等 - 点号
'.'表示当前值,最开始表示模板的输入参数 '|'将前一个操作的结果当作下一个操作的输入- 将模板定义为一个字符串或文件、初始化时解析模板(并指定自定义函数)、运行时执行模板(提供输入输出)
- html/template 包会自动对 html 元字符转义,text/template 包不会,一定要用对包。(可以将输入数据指定为
template.HTML类型不让 html/template 自动转义,也就是说,template.HTML类型会被看做是 html 数据,而不是纯文本)
四、函数
- 可以对函数返回值命名,有命名的返回值可以不写在 return 语句里(但是这么做会降低代码可读性)。
- 错误处理策略
- 出错后应当能提供一个错误链
- 错误消息首字母不应该被大写,而且尽量避免换行(方便使用 grep 这样的工具找错误)- 重试 + 超时
- 匿名函数
- 可以用来实现闭包
- 可以在函数内部定义函数,例如
golang d := func() int { var x int return x * x } fmt.Println(d()) - 当匿名函数需要递归时,必须先按照上面这样把函数赋值给一个变量
- 捕获变量时要注意,如果捕获的是迭代变量,迭代变量是会不断更新的!
- 变长函数
- 在参数列表最后的类型名称之前使用省略号
"...",例如func sum(vals ... int) int{...} - 如何调用变长函数:
golang sum(1,2,3) //或 values := []int{1,2,3} sum(values...) - 延迟函数调用,
defer - panic,panic 发生时,正常程序会终止执行,goroutine 会执行所有 defer 函数,程序会异常退出并留下日志消息
- 可以手动触发 panic,例如
panic(fmt.Sprint("xxx")) - defer 函数以倒序执行,从调用栈最外层的函数开始,一直到 main
- panic 消息输出到标准错误流,包含调用栈信息
- runtime 包提供了获取调用栈的方法,例如
golang var buf [4096]byte n := runtime.Stack(buf[:],false) os.Stdout.Write(buf[:n]) - 从 panic 中恢复,如果在发生 panic 的函数的 defer 语句中调用 recover 函数,panic 发生时程序就不会异常退出,而是将 panic 消息作为 recover 的返回值,如果没有 panic,recover 就返回 nil(可以在 defer 函数中修改函数返回值!),例如
golang func Parse() err error { defer func(){ if p := recover(); p!=nil{ err = fmt.Errorf("%v",p) //可以在recover时修改函数返回值 } }() } - 处理 panic 的一般原则是,不应该去恢复从另一个包发生的 panic,也不应该去恢复不是你维护的代码发生的 panic
- 有选择性地处理 panic(但还是强调,预期之内的错误不应该通过 panic 来处理)
golang type bailout struct{} ... panic(bailout{}) //在某个地方发生了panic ... defer func() { switch p := recover(); p { case nil: // no panic case bailout{}: // "expected" panic err = fmt.Errorf("multiple title elements") default: panic(p) // unexpected panic; carry on panicking } }()
五、方法
- 可以将一个方法绑定到一个类型上,例如
func (p Point) Distance(a int) int,参数 p 被称为方法的接收者。 - go 可以将方法绑定到任何类型上,包括切片类型、甚至是函数类型。
- 接收者可以是指针类型,但为了避免混淆,不允许绑定本身是指针类型的类型,例如
golang type P *int func (p P) f(){...} //不允许这样 - 接收者是指针类型时,可以使用变量本身调用方法,编译器会对变量隐式转换为指针。反过来也是一样的。
- 设计原则是,同一个类型的方法,接收者要么都是指针类型,要么都是值类型
- 注意,指针类型和对应的非指针类型是两种类型,只是在调方法时编译器会做隐式转换。
- nil 也是合法的接收者(但是要在代码中对 nil 做专门的处理)
- 结构体嵌套匿名结构体,例如以下嵌套
golang
type Point struct{ X,Y float }
type ColorPoint struct{
Point
Color color.RGBA
}
可以直接通过 ColorPoint 调用 Point 的方法
- 可以创建一个方法变量,不用提供接收者就能调方法,例如
golang
dis := p.Distance //方法变量
dis(q)
- 可以创建一个方法表达式,调用时将接收者作为第一个参数,例如
golang
dis := Point.Distance //方法表达式
dis(p, q)
- fmt 默认调用 String 方法输出类型的值,因此,如果想自定义类型的格式化,可以给类型自定义 String 方法(注意,必须是为值类型定义 String 方法,而不是指针类型)
- go 封装的单元是包而不是类型,结构体内的字段不管导没导出,对包内的所有代码都是可见的
- 注意,
type关键字并不是定义了一个别名,而是定义了一种新类型,例如,type Newint int定义了一种新类型Newint,绑定到Newint上的方法并不会绑定到int上
六、接口
- 接口是一种抽象类型,里面都是方法,没有数据。
- 结构体中可以嵌套一个接口,和嵌套匿名结构体类似,也可以嵌套一个匿名接口,可以直接访问匿名接口的方法。
- 接口 A 内部可以嵌套接口 B,这和把 B 的方法直接列在 A 里是一样的,并且接口中方法定义的顺序也无所谓。
- 如果一个类型实现了一个接口的所有方法,那么这个类型实现了这个接口。
- 当一个类型实现了接口时,该类型才能赋值给接口
T和*T是两个类型,用T实现了一个接口,并不能认为*T也实现了。- 调用方法时,可以通过指针调用接收者为值类型的方法,反过来也一样,这只是语法糖在起作用(注意这和接口无关,这里说的不是通过接口类型调用方法)。
- 关于接口实现的结论,实现了接收者是值类型的方法,相当于自动实现了接收者是指针类型的方法;而实现了接收者是指针类型的方法,不会自动生成对应接收者是值类型的方法。
- 空接口类型
interface{}可以用来代表任意类型 - 接口例子
io.writer接口,fmt.Fprintf的第一个参数是io.writer类型fmt.Printf和fmt.Sprintf都是对fmt.Fprintf的封装。fmt.Printf提供的是*os.File类型(os.Stdout),fmt.Sprintf提供的是*bytes.Buffer。
fmt.Stringer接口,这个接口有一个方法String() string,可以让一个类型自定义输出自己的方法。flag.Value接口,用于表示一个命令行参数类型,同样有String() string方法。sort.Interface接口,用于排序。http.Handler接口,一个http.Handler就是一个 web 接口,可以用ServeMux将多个 handler 组合起来。- web 服务器每次都会用一个新的 goroutine 来调用处理函数,因此处理函数要注意并发问题
error接口,go 预先定义了一些实现了 error 接口的类型,例如*errorString、Errno- 对于一个接口变量来说,它有两个类型,静态类型和动态类型,静态类型就是这个接口类型,动态类型是它的实际类型
- 接口类型的零值是 nil,对于一个 nil 接口,它的动态类型和动态值都是 nil,静态类型还是接口类型
- 接口值可以使用
==和!=比较,当接口值动态类型和动态值都相等时两个接口值相等- 如果动态类型不可比较,比较时会 panic
- 注意区分接口值为 nil 和接口动态值为 nil,容易引起 bug
-
类型断言,
x.(T)- 把接口值持有的具体类型 T 的值提取出来
- 或者把接口转换为另外一个接口,保留接口的动态类型和动态值(前提是动态类型实现了要转换的接口)
- 如果接口 A 是接口 B 的子集,接口 B 可以直接赋值给接口 A,不需要类型断言,反过来不行
- 操作数为空接口值时类型断言失败
- 可以获取类型断言的结果,
f,ok := x.(T),类型断言失败,ok 为 false,可以利用这个来检查接口是否能转换成一个具体类型或另外一个接口类型 - 可以使用类型分支来简化一长串类型断言,例如
golang switch x.(type) { case nil: case int: case bool: default: } //扩展写法,在这种写法中,x被赋予的是接口的动态值而不是类型,可以把这个值拿到case块中去使用 switch x:=x.(type) { case nil: case int: case bool: default: }
另外,关于 golang 中的组合
组合是指结构体嵌套结构体、结构体嵌套接口
这里面值得注意的匿名结构体和匿名接口的组合
假设结构体 A 实现了接口 I,结构体B中嵌套了匿名结构体A,结构体C中嵌套了匿名结构I,那么B和C都被视为实现了接口I,并且B和C都可以重写接口方法
七、协程
主要是 goroutine 和 channel
- 程序启动时只有一个协程,即 main 函数所在的协程,称为主协程
- 启动一个协程,
go f() - main 函数返回时,所有协程终止
- channel 用于协程间通信。channel 有类型,例如
chan int是 int 类型的 channel。同种类型的 channel 可以使用==比较,这时如果它们的引用相同,结果为真,否则为假。 - channel 操作
- 创建 channel,
ch := make(chan int) - 把 x 发送给 channel ch,
ch <- x - 从 channel ch 中接受数据并赋值给 x,
x := <- ch - 接受并丢弃结果,
<- ch - 关闭 channel,
close(ch) - 创建 channel 时可以指定容量,例如
ch := make(chan int,3),不指定容量时容量默认为 0,创建出来的叫无缓冲通道 - 无缓冲通道上的发送操作会阻塞,直到消息被接收,发送协程才会继续执行。反过来,接收操作也会阻塞,直到有协程向 channel 发送一个消息
- 换句话说,无缓冲通道会将发送和接收协程同步化,因此无缓冲通道又称为同步通道
- 通道被关闭后,能收不能发
- 向关闭的通道发数据会 panic
- 通道被关闭后,未接收完的数据会被继续接收,然后还能继续接收数据,只不过接收到的是零值
x, ok := <- ch,当通道被关闭且数据被接受完后,ok 的值为 false- 也可以采用直接在通道上迭代的形式,通道关闭并且数据全部接收完后退出循环
golang for x := range ch { ... } - close 操作不是必须的,只是一种同步手段而已(不像文件,打开后一定要关闭)
- 单向通道,类型
chan <- int只能发送,类型<- chan int只能接收 - 可以用在函数参数类型上,让参数的用途更清晰
- 双向通道可以转换为单向的,反过来不行
-
缓冲通道
-
缓冲通道满了以后,发送操作会阻塞
- 使用 cap 函数获取通道容量
-
使用 len 函数获取通道内的数据个数
-
sync.WaitGroup,有时候创建的协程数量不固定,可以用它来对协程计数,它是并发安全的,下面是例子golang r := make(chan int) var wg sync.WaitGroup //主协程,从一个channel获取数据处理,因此事先不知道到底要创建多少协程 for m := range ch { wg.Add(1) //创建协程前计数+1 go func(m string){ defer wg.Done() //在defer中对计数-1,确保一定能-1 r <- 1 }(m) } //等待协程结束必须单独起一个协程,如果把等待操作放在主协程,放在下面这个循环之前,由于channel r是一个无缓冲channel,channel中的数据得不到处理,会导致所有协程都结束不了,如果放在循环后面,由于没人关闭channel r,循环结束不了,所以执行不到等待操作。(由于我们事先不知道到底有多少协程,因此也没办法使用缓冲channel) go func(){ wg.Wait() //等待所有协程结束 close(r) }() total := 0 //等r被关闭后才能结束循环 for i := range r { total += i } -
可以利用缓冲通道限制并发数,例如
golang //设定并发数为20 var tokens = make(chan struct{}, 20) //获取一个token token <- struct{}{} //协程处理... //处理完成后释放token <- token -
select 多路复用(注意不是 switch),如下所示,每个 case 指定一次通道的接收或发送操作,select 一次执行一个 case,如果同时满足多个 case,select 随机选择一个
golang select { case <- ch1: case x := <-ch2: case ch3 <- y: default: }当我们不想在一个通道还没准备好的情况下被阻塞时,可以使用 select 多路复用
-
nil 通道
- 在 nil 通道上收发是合法的,只是会永远阻塞
- select 中的 nil 通道永远不会被选择
-
小知识:go 中的标签不光可以用于 goto 语句
-
可以让 break 跳出好几层,例如
golang loop: for{ select{ case _,ok := <- ch : if !ok { break loop } } } -
continue 同理,可以跳出多层
-
关闭通道操作可以作为一种广播机制,创建一个通道,不往里面发送任何数据,只要一关闭通道,所有监听这个通道的协程都会接收到一个零值,它们就知道通道被关闭了
- goroutine 调式技巧,执行一个 panic 调用,运行时将转储程序中所有 goroutine 的栈
总结一下,实现 goroutine 之间的同步可以采用以下方式:
- 使用 channel 发送消息
- 利用 channel 关闭实现广播机制
- 利用 sync.WaitGroup 对 goroutine 计数
- 利用缓冲 channel 实现一个计数信号量,来限制并发数
- 容量为 1 的 channel 被称为二进制信号量
- 利用 select 同时处理多个通道的读写操作
八、并发时如何共享变量
- 竞态,竞态是指并发导致对数据的操作出现冲突的情况。
- 如何避免竞态(2、3 是两种重要方案)
- 并发前把变量初始化好,并发期间不修改变量
- 避免多个 goroutine 访问同一个变量,(go 箴言,不要通过共享内存来通信,应该通过通信来共享内存)。一种方法是让一个 goroutine 代理一个共享变量的操作,其他 goroutine 通过通道来对这个变量进行操作,这个代理 goroutine 被称作监控 goroutine。
- 使用互斥机制
- 互斥锁
sync.Mutex - 有一种代替的办法是使用一个容量为 1 的 channel 作为二进制信号量,把对共享变量的并发访问数限制到 1
- mutex 的用法
golang var mu sync.Mutex mu.Lock() //...临界区域 mu.UnLock() - 为了确保释放锁,unlock 操作经常放在 defer 里
- 读写锁
sync.RWMutex,读锁RLock()、RUnlock(),读写锁Lock()、Unlock() - 对一个变量,如果写操作加了锁,那么读操作也应该加锁,原因有两点
- 防止读操作插入到写操作序列中
- 现代 cpu 各个核心有各自独立的缓存,通道通信和互斥量操作等同步原语会导致处理器把积累的写操作刷回到内存,保证操作结果对运行在其他核心的 goroutine 可见。但是如果不使用同步原语,就有可能发生共享变量在各个核心上的缓存不一致的问题
sync.Once,是一个针对一次性初始化问题的解决方案- 一次性初始化问题,实际开发中经常会遇到这样的场景,访问一个共享变量前要先去判断这个变量有没有初始化,如果没有,要先将其初始化然后再访问,这是并发不安全的,用法:
golang var loadonce sync.Once loadonce.Do(initfunc) //initfunc用于初始化变量 //...访问操作 - sync.Once 内部包含一个互斥量和一个 bool 变量,bool 变量用于标记共享变量是否已完成初始化
- 竞态检测器
- 用于分析程序是否存在竞态
- 在
go build、go run、go test命令后面加上-race参数即可 - 竞态检测器会记录所有对共享变量的访问,会记录所有同步操作
- 竞态检测器只能检查出运行时发生的竞态,检查不出来没发生的竞态
九、goroutine 和 OS 线程
9.1 栈
每个 OS 线程都有一个固定大小的栈空间(通常为 2MB),这个栈空间对 goroutine 来说太大了(go 中一次创建十万个 goroutine 也是常见的),但是对很多递归深度比较深的函数又太小了
goroutine 也有栈,但是大小不固定。goroutine 刚创建出来时栈很小(典型情况只有 2KB),并且可以按需扩大和缩小,最大限制甚至可以达到 1GB
9.2 调度
OS 线程由操作系统内核来调度,线程切换需要完整的上下文切换,这个操作很耗时。
goroutine 由 go runtime 调度,goroutine 运行在线程上,是一种 m:n 的调度。goroutine 调度在用户态完成,开销很小。
Go 调度器使用 GOMAXPROCS 参数确定需要使用多少 OS 线程,默认是 cpu 核数
9.3 标识
线程都有标识,goroutine 没有。(不鼓励 threadlocal 这种东西)
十、包和 go 工具
10.1 go 为什么编译快
- 所有包依赖必须列在文件头部,编译器分析包依赖时不需要读取整个文件
- 没有循环依赖,依赖构成一个有向无环图,可以包之间可以单独编译甚至并行编译
- 编译出的目标文件不仅包含它自己的导出信息,还包含依赖包的导出信息,因此 go 编译一个包时,go 只需要去看导入依赖对应的目标文件,不需要层层去找依赖的目标文件
10.2 包
- 每个目录下面只能有一个包。
- main 包会告诉 go build 调用连接器生成一个可执行文件。
- 一个目录下可以有一个额外的 test 包,包名以_test 结尾,文件名以_test.go 结尾,这会告诉 go test 两个包都需要构建。
- 有些依赖管理工具会在包名末尾加上一个版本后缀,实际的包名不包含这个后缀,例如
xx/xx/yaml.v2的包名应该是yaml - 重命名导入,解决包名冲突
- 空导入,只导入但是不引用包中的名字,例如
import _ "image/png"(有时候导入仅仅是为了执行包的初始化) - 包的导入路径是相对于
$GOPATH/src - 包可以自定义导入域名,防止因托管网站的变化导致导入路径变化
- 常用 go 工具
go envgo getgo build、go installgo doc、godocgo doc命令用于查看包、成员、方法的声明和注释godoc命令用于在本地启动一个文档服务器,例如godoc -http=localhost:6060,里面包括所有标准库的包和用户自己的包,需要单独安装这个命令。
go list- vendor 目录,维护依赖的本地副本
- 内部包,位于 internal 目录中,只能被 internal 的父目录下的包引用
十一、测试
一个典型的测试命令:go test -v -run="xxx" xxx_test.go,-run是一个正则表达式,用于过滤要测试的函数名,被测试对象不光可以是一个文件,也可以是一个包。
11.1 包内测试和外部测试
- 包内测试是指测试代码和产品代码的包名一致,外部测试是指测试代码在一个单独的包中,以产品包名拼上_test 作为包名
- 有时候会将产品包内的一些方法暴露给测试包,这些方法一般写在一个单独的包内测试文件 export_test.go 中
11.2 测试覆盖率
- 著名计算机科学家 Edsger Dijkstra 说,“测试的目的是发现 bug,而不是证明其不存在”
- 使用 go tool cover 查看覆盖工具的使用方法
- 简单的测试覆盖率命令,
go test -cover - 复杂的测试覆盖率命令,
go test -coverprofile=c.out -covermode=count,-coverprofile参数将覆盖率数据输出到文件中,-covermode=count表示每个语句块的执行测试将被计数 go tool cover -html=c.out命令将生成一个 html 版的测试报告
11.3 其他
- testing 包还可以用来做基准测试
- 测试函数前缀是
Benchmark,参数是*testing.B - 测试命令
go test -bench=xxx - 性能优化工具 pprof
- 测试命令
bash go test -cpuprofile=cpu.out go test -blockprofile=block.out go test -memprofile=mem.out - 一般都是针对基准测试进行性能分析
bash go test -run=NONE -bench=xxx -cpuprofile=cpu.out - 使用 pprof 工具生成性能分析报告
bash go tool pprof ...各种参数 cpu.out - 示例函数,函数名的格式为
Example拼上被演示函数的函数名,没有参数和返回值, - 目的是作为文档,godoc 会将示例函数和函数关联到一起
- 在函数体末尾加上以下注释可以让 go test 运行示例函数并检查实际输出和注释中的输出是否匹配
golang // Output: 或 Unordered Output: // Ava // Jess // [Jess Sarah Zoe]
十二、反射
reflect.Typereflect.Type是一个接口类型,这个接口只有一个实现,即类型描述符,接口值中的动态类型就是类型描述符reflect.TypeOf(x)返回一个类型描述符,并且只会返回具体类型,不会返回接口类型refect.Valuereflect.ValueOf(x)返回一个reflect.Value类型,ValueOf 从接口值中提取值部分,所以永远返回一个具体的值reflect.Value.Type()方法会返回 value 的类型,返回值是reflect.Type类型reflect.Value.Interface()方法将一个 Value 类型转为一个 interface{}接口值,是 valueof 的逆操作reflect.Value.Kind()方法会返回类型分类(类型分类是reflect.Kind类型,其实是个 uint 类型,其零值是 reflect.Invalid)- 使用反射的例子可以参考
gopl.io/ch12/format和gopl.io/ch12/display - 非导出字段在反射下也是可见的
- 接口类型的值可以通过从其他 Value 类型值间接获得
- 使用
reflect.Value设置值golang x := 2 //获取一个可寻址的x d := reflect.ValueOf(&x).Elem() //方法1,通过指针修改x的值 px := d.Addr().Interface().(*int) *px = 3 //方法2,通过set方法修改d d.Set(reflect.ValueOf(4)) - 在不可寻址的 reflect.Value 上调用 Set 会崩溃
- (在指向 interface{}的 reflect.Value 上调用 SetInt 等特化 Set 方法时会崩溃)
- 不能更新结构体未导出字段的值
reflect.Value.CanAddr()方法判断变量是否可寻址reflect.Value.CanSet()方法判断变量是否可寻址且可修改- 可以通过
reflect.Type.Field方法获取结构体字段名和 tag - 可以通过
reflect.Type.Method方法获取类型的方法,但是只描述方法名和类型 - 也可以通过
reflect.Value.Method方法获取到绑定了接收者的方法,但是只描述方法名和类型,可以用reflect.Value.Call调用函数 -
慎用反射
-
反射中存在的类型错误是编译时检查不出来的,只能在运行时以崩溃的方式报告
- 反射会造成代码的可读性降低
- 反射慢
十三、低级编程
本章主要介绍 unsafe 包和 cgo 工具
unsafe 是提供了对 go 内置特性的访问,这些特性不安全,因为他们暴露了 go 的内存布局。unsafe 广泛用在和操作系统交互的底层包中(runtime、os、syscall、net)
cgo 工具用来调用 c 程序