除了用于无法通过声明来表示的初始化以外,init函数的一个常用法是在真正执行之前进行验证或者修复程序状态的正确性。
【规则4.1】一个文件只定义一个init函数。
【规则4.2】一个包内的如果存在多个init函数,不能有任何的依赖关系。
注意如果包内有多个init,每个init的执行顺序是不确定的。
4.5 defer的使用原则
【建议4.10】如果函数存在多个返回的地方,则采用defer来完成如关闭资源、解锁等清理操作。
说明:Go的defer语句用来调度一个函数调用(被延期的函数),在函数即将返回之前defer才被运行。这是一种不寻常但又很有效的方法,用于处理类似于不管函数通过哪个执行路径返回,资源都必须要被释放的情况。典型的例子是对一个互斥解锁,或者关闭一个文件。
【建议4.11】defer会消耗更多的系统资源,不建议用于频繁调用的方法中。
【建议4.12】避免在for循环中使用defer。
说明:一个完整defer过程要处理缓存对象、参数拷贝,以及多次函数调用,要比直接函数调用慢得多。
错误示例:实现一个加解锁函数,解锁过程使用defer处理。这是一个非常小的函数,并且能够预知解锁的位置,使用defer编译后会使处理产生很多无用的过程导致性能下降。
1. var lock sync.Mutex
2. func testdefer() {
3. lock.Lock()
4. defer lock.Unlock()
5. }
6.
7. func BenchmarkTestDefer(b *testing.B) {
8. for i := 0; i < b.N; i++ {
9. testdefer()
10. }
11. }
12.
13. // 耗时结果
14. BenchmarkTestDefer 10000000 211 ns/op
推荐做法:如果能够明确函数退出的位置,可以选择不使用defer处理。保证功能不变的情况下,性能明显提升,是耗时是使用defer的1/3。
1. var lock sync.Mutex
2. func testdefer() {
3. lock.Lock()
4. lock.Unlock() // 【修改】去除defer
5. }
6.
7. func BenchmarkTestDefer(b *testing.B) {
8. for i := 0; i < b.N; i++ {
9. testdefer()
10. }
11. }
12.
13. // 耗时结果
14. BenchmarkTest" 30000000 43.5 ns/op
4.6 Goroutine使用原则
【规则4.3】确保每个goroutine都能退出。
说明:Goroutine是Go并行设计的核心,在实现功能时不可避免会使用到,执行goroutine时会占用一定的栈内存。
启动goroutine就相当于启动了一个线程,如果不设置线程退出的条件就相当于这个线程失去了控制,占用的资源将无法回收,导致内存泄露。
错误示例:示例中ready()启动了一个goroutine循环打印信息到屏幕上,这个goroutine无法终止退出。
1. package main
2.
3. import (
4. "fmt"
5. "time"
6. )
7.
8. func ready(w string, sec int) {
9. go func() { // 【错误】goroutine启动之后无法终止
10. for {
11. time.Sleep(time.Duration(sec) * time.Second)
12. fmt.Println(w, "is ready! ")
13. }
14. }()
15. }
16.
17. func main() {
18. ready("Tea", 2)
19. ready("Coffee", 1)
20. fmt.Println("I'm waiting")
21. time.Sleep(5 * time.Second)
22. }
推荐做法:对于每个goroutine都需要有退出机制,能够通过控制goroutine的退出,从而回收资源。通常退出的方式有:
使用标志位的方式;
信号量;
通过channel通道通知;
注意:channel是一个消息队列,一个goroutine获取signal后,另一个goroutine将无法获取signal,以下场景下每个channel对应一个goroutine
1. package main
2.
3. import (
4. "fmt"
5. "time"
6. )
7.
8. func ready(w string, sec int, signal chan struct{}) {
9. go func() {
10. for {
11. select {
12. case <-time.Tick(time.Duration(sec) * time.Second):
13. fmt.Println(w, "is ready! ")
14. case <-signal: // 对每个goroutie增加一个退出选项
15. fmt.Println(w, "is close goroutine!")
16. return
17. }
18. }
19. }()
20. }
21.
22. func main() {
23. signal1 := make(chan struct{}) // 增加一个signal
24. ready("Tea", 2, signal1)
25.
26. signal2 := make(chan struct{}) // 增加一个signal
27. ready("Coffee", 1, signal2)
28.
29. fmt.Println("I'm waiting")
30. time.Sleep(4 * time.Second)
31. signal1 <- struct{}{}
32. signal2 <- struct{}{}
33. time.Sleep(4 * time.Second)
34. }
【规则4.4】禁止在闭包中直接引用闭包外部的循环变量。
说明:Go语言的特性决定了它会出现其它语言不存在的一些问题,比如在循环中启动协程,当协程中使用到了循环的索引值,往往会出现意想不到的问题,通常需要程序员显式地进行变量调用。
1. for i := 0; i < limit; i++ {
2. go func() { DoSomething(i) }() //错误做法
3. go func(i int) { DoSomething(i)}(i) //正确做法
4. }
参考:http://golang.org/doc/articles/race_detector.html#Race_on_loop_counter
4.7 Channel使用原则
【规则4.5】传递channel类型的参数时应该区分其职责。
在只发送的功能中,传递channel类型限定为: c chan<- int
在只接收的功能中,传递channel类型限定为: c <-chan int
【规则4.6】确保对channel是否关闭做检查。
说明:在调用方法时不能想当然地认为它们都会执行成功,当错误发生时往往会出现意想不到的行为,因此必须严格校验并合适处理函数的返回值。例如:channel在关闭后仍然支持读操作,如果channel中的数据已经被读取,再次读取时会立即返回0值与一个channel关闭指示。如果不对channel关闭指示进行判断,可能会误认为收到一个合法的值。因此在使用channel时,需要判断channel是否已经关闭。
错误示例:下面代码中若cc已被关闭,如果不对cc是否关闭做检查,则会产生死循环。
1. package main
2. import (
3. "errors"
4. "fmt"
5. "time"
6. )
7.
8. func main() {
9. var cc = make(chan int)
10. go client(cc)
11.
12. for {
13. select {
14. case <-cc: //【错误】当channel cc被关闭后如果不做检查则造成死循环
15. fmt.Println("continue")
16. case <-time.After(5 * time.Second):
17. fmt.Println("timeout")
18. }
19. }
20. }
21.
22. func client(c chan int) {
23. defer close(c)
24.
25. for {
26. err := processBusiness()
27. if err != nil {
28. c <- 0
29. return
30. }
31. c <- 1
32. }
33. }
34.
35. func processBusiness() error {
36. return errors.New("domo")
37. }
推荐做法:对通道增加关闭判断。
1. // 前面代码略……
2. for {
3. select {
4. case _, ok := <-cc:
5. // 增加对chnnel关闭的判断,防止死循环
6. if ok == false {
7. fmt.Println("channel closed")
8. return
9. }
10. fmt.Println("continue")
11. case <-time.After(5 * time.Second):
12. fmt.Println("timeout")
13. }
14. }
15. // 后面代码略……
【规则4.7】禁止重复释放channel。
说明:重复释放channel会触发run-time panic,导致程序异常退出。重复释放一般存在于异常流程判断中,如果恶意攻击者能够构造成异常条件,则会利用程序的重复释放漏洞实施DoS攻击。
错误示例:
1. func client(c chan int) {
2. defer close(c)
3. for {
4. err := processBusiness()
5. if err != nil {
6. c <- 0
7. close(c) // 【错误】可能会产生双重释放
8. return
9. }
10. c <- 1
11. }
12. }
推荐做法:确保创建的channel只释放一次。
1. func client(c chan int) {
2. defer close(c)
3.
4. for {
5. err := processBusiness()
6. if err != nil {
7. c <- 0 // 【修改】使用defer延迟close后,不再单独进行close
8. return
9. }
10. c <- 1
11. }
12. }
4.8 其它
【建议4.13】使用go vet --shadow检查变量覆盖,以避免无意的变量覆盖。
GO的变量赋值和声明可以通过”:=”同时完成,但是由于Go可以初始化多个变量,所以这个语法容易引发错误。下面的例子是一个典型的变量覆盖引起的错误,第二个val的作用域只限于for循环内部,赋值没有影响到之前的val。
1. package main
2.
3. import "fmt"
4. import "strconv"
5.
6. func main() {
7. var val int64
8.
9. if val, err := strconv.ParseInt("FF", 16, 64); nil != err {
10. fmt.Printf("parse int failed with error %v\n", err)
11. } else {
12. fmt.Printf("inside : val is %d\n", val)
13. }
14. fmt.Printf("outside : val is %d \n", val)
15. }
16.
17. 执行结果:
18. inside : val is 255
19. outside : val is 0
正确的做法:
1. package main
2.
3. import "fmt"
4. import "strconv"
5.
6. func main() {
7. var val int64
8. var err error
9.
10. if val, err = strconv.ParseInt("FF", 16, 64); nil != err {
11. fmt.Printf("parse int failed with error %v\n", err)
12. } else {
13. fmt.Printf("inside : val is %d\n", val)
14. }
15. fmt.Printf("outside : val is %d \n", val)
16. }
17.
18. 执行结果:
19. inside : val is 255
20. outside : val is 255
【建议4.14】GO的结构体中控制使用Slice和Map。
GO的slice和map等变量在赋值时,传递的是引用。从结果上看,是浅拷贝,会导致复制前后的两个变量指向同一片数据。这一点和Go的数组、C/C++的数组行为不同,很容易出错。
1. package main
2. import "fmt"
3.
4. type Student struct {
5. Name string
6. Subjects []string
7. }
8.
9. func main() {
10. sam := Student{
11. Name: "Sam", Subjects: []string{"Math", "Music"},
12. }
13. clark := sam //clark.Subject和sam.Subject是同一个Slice的引用!
14. clark.Name = "Clark"
15. clark.Subjects[1] = "Philosophy" //sam.Subject[1]也变了!
16. fmt.Printf("Sam : %v\n", sam)
17. fmt.Printf("Clark : %v\n", clark)
18. }
19.
20. 执行结果:
21. Sam : {Sam [Math Philosophy]}
22. Clark : {Clark [Math Philosophy]}
作为对比,请看作为Array定义的Subjects的行为:
1. package main
2. import "fmt"
3.
4. type Student struct {
5. Name string
6. Subjects [2]string
7. }
8.
9. func main() {
10. var clark Student
11. sam := Student{
12. Name: "Sam", Subjects: [2]string{"Math", "Music"},
13. }
14.
15. clark = sam //clark.Subject和sam.Subject不同的Array
16. clark.Name = "Clark"
17. clark.Subjects[1] = "Philosophy" //sam.Subject不受影响!
18. fmt.Printf("Sam : %v\n", sam)
19. fmt.Printf("Clark : %v\n", clark)
20. }
21.
22. 执行结果:
23. Sam : {Sam [Math Music]}
24. Clark : {Clark [Math Philosophy]}
编写代码时,建议这样规避上述问题:
结构体内尽可能不定义Slice、Maps成员;
如果结构体有Slice、Maps成员,尽可能以小写开头、控制其访问;
结构体的赋值和复制,尽可能通过自定义的深度拷贝函数进行;
【规则4.8】避免在循环引用调用 runtime.SetFinalizer。
说明:指针构成的 "循环引用" 加上 runtime.SetFinalizer 会导致内存泄露。
runtime.SetFinalizer用于在一个对象 obj 被从内存移除前执行一些特殊操作,比如写到日志文件中。在对象被 GC 进程选中并从内存中移除以前,SetFinalizer 都不会执行,即使程序正常结束或者发生错误。
错误示例:垃圾回收器能正确处理 "指针循环引用",但无法确定 Finalizer 依赖次序,也就无法调用Finalizer 函数,这会导致目标对象无法变成不可达状态,其所占用内存无法被回收。
1. package main
2.
3. import (
4. "fmt"
5. "runtime"
6. "time"
7. )
8.
9. type Data struct {
10. d [1024 * 100]byte
11. o *Data
12. }
13.
14. func test() {
15. var a, b Data
16. a.o = &b
17. b.o = &a
18.
19. // 【错误】循环和SetFinalize同时使用
20. runtime.SetFinalizer(&a, func(d *Data) { fmt.Printf("a %p final.\n", d) })
21. runtime.SetFinalizer(&b, func(d *Data) { fmt.Printf("b %p final.\n", d) })
22. }
23.
24. func main() {
25. for { // 【错误】循环和SetFinalize同时使用
26. test()
27. time.Sleep(time.Millisecond)
28. }
29. }
通过跟踪GC的处理过程,可以看到如上代码内存在不断的泄露:
go build -gcflags "-N -l" && GODEBUG="gctrace=1" ./test
gc11(1): 2+0+0 ms, 104 -> 104 MB 1127 -> 1127 (1180-53) objects
gc12(1): 4+0+0 ms, 208 -> 208 MB 2151 -> 2151 (2226-75) objects
gc13(1): 8+0+1 ms, 416 -> 416 MB 4198 -> 4198 (4307-109) objects
以上结果标红的部分代表对象数量,我们在代码中申请的对象都是局部变量,在正常处理过程中GC会持续的回收局部变量占用的内存。但是在当前的处理过程中,内存无法被GC回收,目标对象无法变成不可达状态。
推荐做法:需要避免内存指针的循环引用以及runtime.SetFinalizer同时使用。
【规则4.9】避免在for循环中使用time.Tick()函数。
如果在for循环中使用time.Tick(),它会每次创建一个新的对象返回,应该在for循环之外初始化一个ticker后,再在循环中使用:
1. ticker := time.Tick(time.Second)
2. for {
3. select {
4. case <-ticker:
5. // …
6. }
7. }
5 性能效率
5.1 Memory优化
【建议5.1】将多次分配小对象组合为一次分配大对象。
比如, 将 *bytes.Buffer 结构体成员替换为bytes。缓冲区 (你可以预分配然后通过调用bytes.Buffer.Grow为写做准备) 。这将减少很多内存分配(更快)并且减缓垃圾回收器的压力(更快的垃圾回收) 。
【建议5.2】将多个不同的小对象绑成一个大结构,可以减少内存分配的次数。
比如:将
1. for k, v := range m {
2. k, v := k, v // copy for capturing by the goroutine
3. go func() {
4. // use k and v
5. }()
6. }
替换为:
1. for k, v := range m {
2. x := struct{ k, v string }{k, v} // copy for capturing by the goroutine
3. go func() {
4. // use x.k and x.v
5. }()
6. }
这就将多次内存分配(分别为k、v分配内存)替换为了一次(为x分配内存)。然而,这样的优化方式会影响代码的可读性,因此要合理地使用它。
【建议5.3】组合内存分配的一个特殊情形是对分片数组进行预分配。
如果清楚一个特定的分片的大小,可以对数组进行预分配:
1. type X struct {
2. buf []byte
3. bufArray [16]byte // Buf usually does not grow beyond 16 bytes.
4. }
5.
6.
7. func MakeX() *X {
8. x := &X{}
9. // Preinitialize buf with the backing array.
10. x.buf = x.bufArray[:0]
11. return x
12. }
【建议5.4】尽可能使用小数据类型,并尽可能满足硬件流水线(Pipeline)的操作,如对齐数据预取边界。
说明:不包含任何指针的对象(注意 strings,slices,maps 和 chans 包含隐含指针)不会被垃圾回收器扫描到。
比如,1GB 的分片实际上不会影响垃圾回收时间。因此如果你删除被频繁使用的对象指针,它会对垃圾回收时间造成影响。一些建议:使用索引替换指针,将对象分割为其中之一不含指针的两部分。
【建议5.5】使用对象池来重用临时对象,减少内存分配。
标准库包含的sync.Pool类型可以实现垃圾回收期间多次重用同一个对象。然而需要注意的是,对于任何手动内存管理的方案来说,不正确地使用sync.Pool会导致 use-after-free bug。
5.2 GC 优化
【建议5.6】设置GOMAXPROCS为CPU的核心数目,或者稍高的数值。
GC是并行的,而且一般在并行硬件上具有良好可扩展性。所以给 GOMAXPROCS 设置较高的值是有意义的,就算是对连续的程序来说也能够提高垃圾回收速度。但是,要注意,目前垃圾回收器线程的数量被限制在 8 个以内。
【建议5.7】避免频繁创建对象导致GC处理性能问题。
说明:尽可能少的申请内存,减少内存增量,可以减少甚至避免GC的性能冲击,提升性能。
Go语言申请的临时局部变量(对象)内存,都会受GC(垃圾回收)控制内存的回收,其实我们在编程实现功能时申请的大部分内存都属于局部变量,所以与GC有很大的关系。
Go在GC的时候会发生Stop the world,整个程序会暂停,然后去标记整个内存里面可以被回收的变量,标记完成之后再恢复程序执行,最后异步地去回收内存。(暂停的时间主要取决于需要标记的临时变量个数,临时变量数量越多,时间越长。Go 1.7以上的版本大幅优化了GC的停顿时间, Go 1.8下,通常的GC停顿的时间<100μs)
目前GC的优化方式原则就是尽可能少的声明临时变量:
局部变量尽量利用
如果局部变量过多,可以把这些变量放到一个大结构体内,这样扫描的时候可以只扫描一个变量,回收掉它包含的很多内存
本规则所说的创建对象包含:
&obj{}
new(abc{})
make()
我们在编程实现功能时申请的大部分内存都属于局部变量,下面这个例子说明的是我们实现功能时需要注意的一个问题,适当的调整可以减少GC的性能消耗。
错误示例:
代码中定义了一个tables对象,每个tables对象里面有一堆类似tableA和tableC这样的一对一的数据,也有一堆类似tableB这样的一对多的数据。假设有1万个玩家,每个玩家都有一条tableA和一条tableC的数据,又各有10条tableB的数据,那么将总的产生1w (tables) + 1w (tableA) + 1w (tableC) + 10w (tableB)的对象。
不好的例子:
1. // 对象数据表的集合
2. type tables struct {
3. tableA *tableA
4. tableB *tableB
5. tableC *tableC
6. // 此处省略一些表
7. }
8.
9. // 每个对象只会有一条tableA记录
10. type tableA struct {
11. fieldA int
12. fieldB string
13. }
14.
15. // 每个对象有多条tableB记录
16. type tableB struct {
17. city string
18. code int
19. next *tableB // 指向下一条记录
20. }
21.
22. // 每个对象只有一条tableC记录
23. type tableC struct {
24. id int
25. value int64
26. }
建议一对一表用结构体,一对多表用slice,每个表都加一个_is_nil的字段,用来表示当前的数据是否是有用的数据,这样修改的结果是,一万个玩家,产生的对象总量是1w(tables)+1w([]tablesB),跟前面的差别很明显:
1. // 对象数据表的集合
2. type tables struct {
3. tableA tableA
4. tableB []tableB
5. tableC tableC
6. // 此处省略一些表
7. }
8.
9. // 每个对象只会有一条tableA记录
10. type tableA struct {
11. _is_nil bool
12. fieldA int
13. fieldB string
14. }
15.