容易出错的细节 #
创建对象 #
新建一个对象在go里面有好几种方法,让人迷惑,而且似乎和简洁这一设计原则违背。我们按照对象类型讨论一下:
- 对于结构体,
new(T)
和&T{}
是等价的,都会给对象赋零值(一般人很少用new)。 Note:直接var obj T;&T
也是等价的,只不过变量有可能在堆上,有可能在栈上 - 对于slice、map、chan,
make(map[string]int)
和map[string]int{}
等价,会对对象进行初始化。
var a []int // nil
a := []int{} // not nil
a := *new([]int) // nil
a := make([]int,0) // not nil
零值 #
零值和未初始化的值并不相同。不同类型的零值是什么?
- 布尔类型是false,整型是0,字符串是""
- 指针、函数、interface、slice、channel和map的零值都是nil
- 结构体的零值是递归生成的,每个成员都是对应的零值
我们来看一个例子。一个为nil的slice和map能做什么操作:
// 一个为nil的slice,除了不能索引外,其他的操作都是可以的
// Note: 如果这个slice是个指针,不适用这里的规则
var a []int
fmt.Printf("len(a):%d, cap(a):%d, a==nil:%v\n", len(a),cap(a), a == nil) //0 0 true
for _, v := range a{// 不会panic
fmt.Println(v)
}
aa := a[0:0] // 也不会panic,只要索引都是0
// nil的map,我们可以简单把它看成是一个只读的map
var b map[string]string
if val, ok := b["notexist"];ok{// 不会panic
fmt.Println(val)
}
for k, v := range b{// 不会panic
fmt.Println(k,v)
}
delete(b, "foo") // 也不会panic
fmt.Printf("len(b):%d, b==nil:%v\n", len(b), b == nil) // 0 true
值传递 #
Go语言中所有的传参都是值传递,都是原值的一个副本,或者说一个拷贝。传入的数据能不能在函数内被修改,取决于是不是指针或者含有指针的类型(指针被值传递复制后依然指向同一块地址)。这就让人很疑惑,什么时候传入的参数修改会生效,什么时候不会生效? slice类型在 值传递的时候len和cap不会变,所以函数内append没有用:
type slice struct {
array unsafe.Pointer
len int
cap int
}
// badcase
func appendMe(s []int){
s = append(s, -1)
}
map 和 chan类型,本来就是个指针,所以函数内修改一定会生效:
// map实际上是一个 *hmap
func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap {
//省略无关代码
}
// chan实际上是个 *hchan
func makechan(t *chantype, size int64) *hchan {
//省略无关代码
}
再比如一个结构体作为参数:
// 这是一个典型的指针包裹类型
type Person struct {
name string
age *int
}
func modify(x Person){
x.name = "modified"
*x.age = 66
}
这个结构体里的age是个指针类型,所以在函数内会被修改。 这种含有指针的结构体类型,里面的指针指向了其他的内存。在发生拷贝的时候,只有结构体本身的内存会被拷贝,指向的内存是和原值共享的。 更多细节参考 :值部 但是我们一般希望的是,要么结构体的成员一起改变(这个简单,参数传person的指针),要么一起不改变(深拷贝)。那么另一个让人头疼的问题来了,那我如何深拷贝这个对象?
深拷贝 #
对于slice,go提供了似乎还不错的方式:
// 自己复制
s1 := []int{1,2,3}
s2 := append([]int{}, s1...)
// 效率更高的复制
s1 := []int{1,2,3}
s2 := make([]int, len(s1))
copy(s2, s1)
如果你要拷贝一个map,只能用for循环依次把键值对赋值到新map里。 切记:需要拷贝map一定要深拷贝,不然如果后续在不同的协程里操作map会panic 如果有其他更复杂的结构体需要深拷贝呢?目前还没有很好的办法:
- 自己写一个复制值的函数
- 用序列化/反序列化的方法来做,json,bson
- 用反射来做
age := 22
p := &Person{"Bob", &age}
v := reflect.ValueOf(p).Elem()
vp2 := reflect.New(v.Type())
vp2.Elem().Set(v)
小心interface判等 #
go实现接口的时候有两个属性,type T和value V,判等的时候两个属性都要比较。比如一个interface存了3,那么T=int,v=3。只有当两个值都没有设置才等于nil。
var pi *int = nil
var pb *bool = nil
var x interface{} = pi
var y interface{} = pb
var z interface{} = nil
fmt.Println(x == y) // false
fmt.Println(x == nil) // false
fmt.Println(x == z) // false
// badcase
type error interface {
Error() string
}
func returnsError() error {
var p *MyError = nil
if bad() {
p = ErrBad
}
return p // Will always return a non-nil error.
}
还有一种常见的场景是我们容易漏掉的。int64和int的interface也不相等:
var int1,int2 interface{}
int1 = int64(0)
int2 = int(0)
fmt.Printf("%v %v = %v", int1, int2, int1 == int2) // 0 0 false
// 如果函数参数用了interface,如果我们很容易犯错
func (m *Map) Load(key, value interface{}) {
if e, ok := read.m[key]; ok {
...
}
}
// badcase 1: key的类型不一致导致缓存无法取出
m := sync.Map{}
m.Store(0, "ManualCache")
val, ok := m.Load(int64(0)) // nil false
// badcase 2: value的类型不一致导致断言失败
m.Store("key", 0)
if val, ok := m.Load("key"); ok {
_ = val.(int64) // panic
}
点点点 #
...
是个很常用的语法糖,能帮我们节省很多代码。
用作展开:
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...) //而不是for循环
x = append(x, 4, 5, 6) //等价于上面的
用作可变参数列表:
// Println prints to the standard logger in the manner of fmt.Println.
func Println(v ...interface{}) {
std.Output(2, fmt.Sprintln(v...)) // Output takes parameters (int, string)
}
用作简化数组声明:
var _ = [...]language{
{"C", 1972},
{"Python", 1991},
{"Go", 2009},
}
var b = [...]string{0: "foo", 2: "foo"} // [3]string{"foo", "", "foo"}
闭包里的局部变量是引用 #
闭包里起的go协程里面引用的是变量i的地址。所有的go协程启动后等待调用,在上面的协程中,部分协程很可能在for循环完成之后才被调用,所以输出结果很多都是最后一个i的值
// bad case
done := make(chan bool)
for i := 0; i < 5; i++ {
go func() {
println(i)
done <- true
}()
}
for _ = range values {
<-done
}
// 5 5 5 5 5
// good sample 1
for i := 0; i < 5; i++ {
defer func(i int) {
println(i)
done <- true
}(i)
}
// good sample 2
for i := 0; i < 5; i++ {
i := i // 新建变量
go func() {
println(i)
done <- true
}()
}
//1 3 5 4 2
不要引用大数组 #
被切片引用的数据不会被释放(即使你仅仅引用了很小一部分),会大幅降低代码性能
headerMap := make(map[string][]byte)
for i := 0; i < 5; i++ {
name := "/path/to/file"
data, err := ioutil.ReadFile(name)
if err != nil {
log.Fatal(err)
}
headerMap[name] = data[:1]
// better: headerMap[name] = append([]byte{}, data[:1]...)
}
赋值不是原子操作 #
在64位的机器上,赋值很可能被拆成mov两次的汇编代码,因此不是原子的。我们可以用atomic里的方法帮助我们做原子操作。
考虑一个内存cache定时刷新的协程:因为随时有请求在读cache,所以刷新cache的时候需要保证cache的指针存取是原子操作。
举例:mycache *map[string]*Cache
// 加载(读取)
var _ = (*T)(atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(mycache))))
// 存储(修改)
atomic.StorePointer(
(*unsafe.Pointer)(unsafe.Pointer(mycache)), unsafe.Pointer(&newMycache))
所有的操作,只要存在同时存在多个goroutine同时操作一个资源(临界区),除了带有sync,atomic,或者channel关键字的,都不安全。包括但不限于:
- 并发读写map
- 并发append切片
- 自增变量
- 赋值
接收器用指针还是值 #
Go的接收器可以传指针进来,也可以传值。注意传值的时候接收器不会被改变。官方推荐下面两种情况该用指针:
- MyStruct很大,需要拷贝的成本太高
- 方法需要修改MyStruct
否则Go推荐使用值接收器 Note:如果对象有可能并发执行方法,指针接收器中可能产生数据竞争,记得加锁
func(s * MyStruct)pointerMethod(){ // 指针方法
s.Age = -1 // useful
}
func(s MyStruct)valueMethod(){ // 值方法
s.Age = -1 // no use
}
for循环里的变量都是副本 #
for key, element = range aContainer {...}
关于上面for循环有几个点:
- 实际遍历的aContainer是原始值的一个副本
- element是遍历到的元素的原始值的一个副本
- key和element整个循环都是同一个变量,而不是每次迭代都生成新变量
这里涉及到几个问题。一个是aContainer和element的拷贝成本。aContainer是数组的时候的拷贝成本比较大,而切片和map的拷贝成本比较小。如果想要缩小拷贝成本,我们有几个建议:
- 遍历大数组时,可以先创建大数组的切片再放在range后面
- element结构比较大的时候,直接用下标key遍历,舍弃element
还有一个问题是遍历的时候修改,能不能生效?
- 当aContainer是数组时,因为数组是整个复制,所以直接修改aContainer不会生效
- 直接修改key或者element,?
- 因为切片和map是浅复制,在循环中操作aContainer或者aContainer[key]可以生效
因为循环里的副本和函数参数的副本非常类似,所以我们可以参考上面的“值传递”中的内容来判断修改副本是否会使得修改达到想要的效果。
map的值不可取址 #
map是哈希表实现的,所以值的地址在哈希表动态调整的时候可能会产生变化。因此。存着map值的地址是没有意义的,go中直接禁止了map的值的取地址。这些类型都不能取址:
- map元素
- string的字节元素
- 常量(有名常量和字面量都不可以)
- 中间结果值(函数调用、显式值转换、各种操作)
// 下面这几行编译不通过。
_ = &[3]int{2, 3, 5}[0] //字面量
_ = &map[int]bool{1: true}[1] //字面量
const pi = 3.14
_ = &pi //有名常量
m := map[int]bool{1: true}
_ = &m[1] //map的value
lt := [3]int{2, 3, 5}
_ = <[1:1] //切片操作
一般来说,一个不可寻址的值的直接部分是不可修改的。但是map的元素是个例外。 map的元素虽然不可寻址,但是每个映射元素可以被整个修改(但不可以被部分修改):
type T struct{age int}
mt := map[string]T{}
mt["John"] = T{age: 29} // 整体修改是允许的
ma := map[int][5]int{}
ma[1] = [5]int{1: 789} // 整体修改是允许的
// 这两个赋值编译不通过,因为部分修改一个映射元素是非法的。这看上去确实有些反直觉。
ma[1][1] = 123 // error
mt["John"].age = 30 // error
// 读取映射元素的元素或者字段是没问题的。
fmt.Println(ma[1][1]) // 789
fmt.Println(mt["John"].age) // 29
逃逸分析 #
关心变量在栈或者堆上有助于我们对变量的生命周期有所了解,写出更好性能的代码。比如一些短周期的变量的指针如果和长生命周期的变量绑定,就会使得这个变量迟迟不能回收,影响性能。 Go在栈上的变量不会产生GC成本,因为变量会随着函数的退出一起销毁(当然这样性能也是最高的)。但是,变量是否在栈上,不能简单的通过是否局部变量或者是否使用new构建的引用类型来判断。有一个基本的判断原则: 情况1:如果变量的引用被声明它的函数返回了,那么这个变量就会逃逸到堆上
func ref(z S) *S {
return &z
}
// go run -gcflags '-m -l' main.go
./escape.go:10: moved to heap: z
./escape.go:11: &z escapes to heap
情况2:返回的结构体引用的对象会逃逸
func refStruct(y int) (z S) {
z.M = &y
return z
}
// go run -gcflags '-m -l' main.go
./escape.go:12: moved to heap: y
./escape.go:13: &y escapes to heap
情况3:map、slice、chan引用的对象会逃逸
func main() {
a := make([]*int,1)
b := 12
a[0] = &b
}
// go run -gcflags '-m -l' maint.go
./maint.go:5:2: moved to heap: b
./maint.go:4:11: make([]*int, 1) does not escape
我们看一个例子,逃逸使得性能下降了不少:
func BenchmarkHeap(b *testing.B) {
b.ResetTimer()
c := make(chan *T, b.N)
// c := make(chan T, b.N)
for i := 0; i < b.N; i++ {
b := T{a: 3, b: 5}
c <- &b
// c <- b
}
}
// go test -bench=. -run=none
BenchmarkStack-12 32297865 32.1 ns/op
BenchmarkHeap-12 28062832 40.2 ns/op
routine #
Golang并发注意点 #
- 最好确认routine任务的开销大于上下文切换的开销时,才使用routine。
- 要尽量控制routine的数量,不然会起到反效果
- channel要注意缓冲区的大小和每次写入的数量,尽量打包写入
防止泄漏 #
如果routine在运行中被阻塞,或者速度很慢,就会发生泄漏(routine的数量会迅速线性增长)
- routinue卡死在读取chan却没数据 理想情况下,我们设计的读取chan的routine会把所有的内容读取完毕后才会关闭。但是,一旦读取者在读取完成之前退出,写入方写满chan之后就会卡死。
- routinue处理的速度过慢 这个情况有点类似消息队列消费者的堆积,如果新起的routine处理速度比主协程还慢的话,堆积起来的routine会越来越多,最终打爆内存
复用timer来替代timer.After #
timer.After会创建很多的timer,引发很大的GC消耗。
// 如果有100w个msg推进来,就会有100w个timer被销毁
func longRunning(messages <-chan string) {
for {
select {
// 消息间隔超过1min会return
case <-time.After(time.Minute):
return
case msg := <-messages:
fmt.Println(msg)
}
}
}
func longRunning(messages <-chan string) {
timer := time.NewTimer(time.Minute)
defer timer.Stop()
for {
select {
case <-timer.C: // 过期了
return
case msg := <-messages:
fmt.Println(msg)
// 此if代码块很重要。
if !timer.Stop() {
<-timer.C
}
}
// 必须重置以复用。
timer.Reset(time.Minute)
}
}
- 我们在每次处理完消息后调用timer.Stop()以便于复用。如果timer已经过期,stop会返回false,C里面还有一条过期消息,我们需要把它取出来;如果timer没有过期,stop会返回true,继续执行循环
- 在一个Timer终止(stopped)之后并且在重置和重用此Timer值之前,我们应该确保此Timer的通道C中肯定不存在过期的通知
常用的仓库 #
演化中的错误处理 #
满足下面的诉求:
- 可以把异常传递下去,并不丢失自己的类型
- 可以保存堆栈信息
Go的错误处理一直在讨论和演进,目前官方已经有几种不同的方案。对于反复写错误处理代码的问题,有几种解决的设想,可以看看上面的(Go语⾔将⾛向何⽅?)
import (
"golang.org/x/xerrors"
)
func bar() error {
if err := foo(); err != nil {
return xerrors.Errorf("bar failed: %w", foo())
}
return nil
}
func foo() error {
return xerrors.Errorf("foo failed: %w", sql.ErrNoRows)
}
func main() {
err := bar()
if xerrors.Is(err, sql.ErrNoRows) {
fmt.Printf("data not found, %v\n", err)
fmt.Printf("%+v\n", err)
return
}
}
/* Outputs:data not found, bar failed: foo failed: sql: no rows in result set
bar failed:
main.bar
/usr/four/main.go:12
- foo failed:
main.foo
/usr/four/main.go:18
- sql: no rows in result set
*/