@

说明:本篇的意义是为开发提供一些经过验证的开发规则和建议,让开发在开发过程中避免低级错误,从而提高代码的质量保证和性能效率

4.1.1 代码质量保证优先原则

【原则 4.1.1】代码质量保证优先原则:
(1)正确性,指程序要实现设计要求的功能。
(2)简洁性,指程序易于理解并且易于实现。
(3)可维护性,指程序被修改的能力,包括纠错、改进、新需求或功能规格变化的适应能力。
(4)可靠性,指程序在给定时间间隔和环境条件下,按设计要求成功运行程序的概率。
(5)代码可测试性,指软件发现故障并隔离、定位故障的能力,以及在一定的时间和成本前提下,进行测试设计、测试执行的能力。
(6)代码性能高效,指是尽可能少地占用系统资源,包括内存和执行时间。
(7)可移植性,指为了在原来设计的特定环境之外运行,对系统进行修改的能力。

4.1.2 对外接口原则

【原则 4.1.2】对于主要功能模块抽象模块接口,通过 interface 提供对外功能。

说明:Go 语言其中一个特殊的功能就是 interface,它让面向对象,内容组织实现非常的方便。正确的使用这个特性可以使模块的可测试性和可维护性得到很大的提升。对于主要功能包(模块),在 package 包主文件中通过 interface 对外提供功能。

示例:在 buffer 包的 buffer.go 中定义如下内容

  1. package buffer
  2. import (
  3. "policy_engine/models"
  4. )
  5. type MetricsBuffer interface {
  6. Store(metric *DataPoint) error
  7. Get(dataRange models.MatchPolicyDataRange) (*MetricDataBuf, error)
  8. Clear(redisKey string) error
  9. Stop()
  10. Stats() []MetrisBufferStat
  11. GetByKey(metricKey string) []DataPoint
  12. }

使用 buffer package 的代码示例,通过 interface 定义,可以在不影响调用者使用的情况下替换 package。基于这个特性,在测试过程中,也可以通过实现符合 interface 要求的类来打桩实现测试目的。

  1. package metrics
  2. import (
  3. ...
  4. "policy_engine/worker/metrics/buffer"
  5. )
  6. type MetricsClient struct {
  7. logger lager.Logger
  8. redisClient *store.RedisClient
  9. conf *config.Config
  10. metricsBuffer buffer.MetricsBuffer
  11. metricsStatClient *metricstat.MetricsStatClient
  12. stopSignal chan struct{}
  13. }
  14. func New(workerId string, redisClient *store.RedisClient, logger lager.Logger, conf *config.Config) *MetricsClient {
  15. var metricsBuffer MetricsBuffer
  16. if conf.MetricsBufferConfig.StoreType == config.METRICS_MEM_STORE {
  17. metricsBuffer = NewMemBuffer(logger, conf)
  18. } else if conf.MetricsBufferConfig.StoreType == config.METRICS_REDIS_STORE {
  19. metricsBuffer = NewRedisBuffer(redisClient, logger, conf)
  20. } else {
  21. ...
  22. }
  23. ...
  24. }

4.1.3 值与指针(T/*T)的使用原则

关于接收者对指针和值的规则是这样的,值方法可以在指针和值上进行调用,而指针方法只能在指针上调用。这是因为指针方法可以修改接收者;使用拷贝的值来调用它们,将会导致那些修改会被丢弃。

对于使用 T 还是*T 作为接收者,下面是一些建议:

【建议 4.1.3.1】基本类型传递时,尽量使用值传递。

【建议 4.1.3.2】如果传递字符串或者接口对象时,建议直接实例传递而不是指针传递。

【建议 4.1.3.3】如果是 map、func、chan,那么直接用 T。

【建议 4.1.3.4】如果是 slice,method 里面不重新 reslice 之类的就用 T。

【建议 4.1.3.5】如果想通过 method 改变里面的属性,那么请使用*T。

【建议 4.1.3.6】如果是 struct,并且里面包含了 sync.Mutex 之类的同步原语,那么请使用*T,避免 copy。

【建议 4.1.3.7】如果是一个大型的 struct 或者 array,那么使用*T 会比较轻量,效率更高。

【建议 4.1.3.8】如果是 struct、slice、array 里面的元素是一个指针类型,然后调用函数又会改变这个数据,那么对于读者来说采用*T 比较容易懂。

【建议 4.1.3.9】其它情况下,建议采用*T。

参考:https://github.com/golang/go/wiki/CodeReviewComments#pass-values

4.1.4 init 的使用原则

每个源文件可以定义自己的不带参数的 init 函数,来设置它所需的状态。init 是在程序包中所有变量声明都被初始化,以及所有被导入的程序包中的变量初始化之后才被调用。

除了用于无法通过声明来表示的初始化以外,init 函数的一个常用法是在真正执行之前进行验证或者修复程序状态的正确性。

【规则 4.1.4.1】一个文件只定义一个 init 函数。

【规则 4.1.4.2】一个包内的如果存在多个 init 函数,不能有任何的依赖关系。

注意如果包内有多个 init,每个 init 的执行顺序是不确定的。

4.1.5 defer 的使用原则

【建议 4.1.5.1】如果函数存在多个返回的地方,则采用 defer 来完成如关闭资源、解锁等清理操作。

说明:Go 的 defer 语句用来调度一个函数调用(被延期的函数),在函数即将返回之前 defer 才被运行。这是一种不寻常但又很有效的方法,用于处理类似于不管函数通过哪个执行路径返回,资源都必须要被释放的情况。典型的例子是对一个互斥解锁,或者关闭一个文件。

【建议 4.1.5.2】defer 会消耗更多的系统资源,不建议用于频繁调用的方法中。

【建议 4.1.5.3】避免在 for 循环中使用 defer。

说明:一个完整 defer 过程要处理缓存对象、参数拷贝,以及多次函数调用,要比直接函数调用慢得多。

错误示例:实现一个加解锁函数,解锁过程使用 defer 处理。这是一个非常小的函数,并且能够预知解锁的位置,使用 defer 编译后会使处理产生很多无用的过程导致性能下降。

  1. var lock sync.Mutex
  2. func testdefer() {
  3. lock.Lock()
  4. defer lock.Unlock()
  5. }
  6. func BenchmarkTestDefer(b *testing.B) {
  7. for i := 0; i < b.N; i++ {
  8. testdefer()
  9. }
  10. }
  11. BenchmarkTestDefer 10000000 211 ns/op

推荐做法:如果能够明确函数退出的位置,可以选择不使用 defer 处理。保证功能不变的情况下,性能明显提升,是耗时是使用 defer 的 1/3。

  1. var lock sync.Mutex
  2. func testdefer() {
  3. lock.Lock()
  4. lock.Unlock()
  5. }
  6. func BenchmarkTestDefer(b *testing.B) {
  7. for i := 0; i < b.N; i++ {
  8. testdefer()
  9. }
  10. }
  11. BenchmarkTest" 30000000 43.5 ns/op

4.1.6 Goroutine 使用原则

【规则 4.1.6.1】确保每个 goroutine 都能退出。

说明:Goroutine 是 Go 并行设计的核心,在实现功能时不可避免会使用到,执行 goroutine 时会占用一定的栈内存。

启动 goroutine 就相当于启动了一个线程,如果不设置线程退出的条件就相当于这个线程失去了控制,占用的资源将无法回收,导致内存泄露。

错误示例:示例中 ready() 启动了一个 goroutine 循环打印信息到屏幕上,这个 goroutine 无法终止退出。

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func ready(w string, sec int) {
  7. go func() {
  8. for {
  9. time.Sleep(time.Duration(sec) * time.Second)
  10. fmt.Println(w, "is ready! ")
  11. }
  12. }()
  13. }
  14. func main() {
  15. ready("Tea", 2)
  16. ready("Coffee", 1)
  17. fmt.Println("I'm waiting")
  18. time.Sleep(5 * time.Second)
  19. }

推荐做法:对于每个 goroutine 都需要有退出机制,能够通过控制 goroutine 的退出,从而回收资源。通常退出的方式有:
 使用标志位的方式;
 信号量;
 通过 channel 通道通知;

注意:channel 是一个消息队列,一个 goroutine 获取 signal 后,另一个 goroutine 将无法获取 signal,以下场景下每个 channel 对应一个 goroutine

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func ready(w string, sec int, signal chan struct{}) {
  7. go func() {
  8. for {
  9. select {
  10. case <-time.Tick(time.Duration(sec) * time.Second):
  11. fmt.Println(w, "is ready! ")
  12. case <-signal:
  13. fmt.Println(w, "is close goroutine!")
  14. return
  15. }
  16. }
  17. }()
  18. }
  19. func main() {
  20. signal1 := make(chan struct{})
  21. ready("Tea", 2, signal1)
  22. signal2 := make(chan struct{})
  23. ready("Coffee", 1, signal2)
  24. fmt.Println("I'm waiting")
  25. time.Sleep(4 * time.Second)
  26. signal1 <- struct{}{}
  27. signal2 <- struct{}{}
  28. time.Sleep(4 * time.Second)
  29. }

【规则 4.1.6.2】禁止在闭包中直接引用闭包外部的循环变量。

说明: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.1.7 Channel 使用原则

【规则 4.1.7.1】传递 channel 类型的参数时应该区分其职责。

在只发送的功能中, 传递 channel 类型限定为: c chan<- int
在只接收的功能中, 传递 channel 类型限定为: c <-chan int

【规则 4.1.7.2】确保对 channel 是否关闭做检查。

说明:在调用方法时不能想当然地认为它们都会执行成功,当错误发生时往往会出现意想不到的行为,因此必须严格校验并合适处理函数的返回值。例如:channel 在关闭后仍然支持读操作,如果 channel 中的数据已经被读取,再次读取时会立即返回 0 值与一个 channel 关闭指示。如果不对 channel 关闭指示进行判断,可能会误认为收到一个合法的值。因此在使用 channel 时,需要判断 channel 是否已经关闭。

错误示例:下面代码中若 cc 已被关闭,如果不对 cc 是否关闭做检查,则会产生死循环。

  1. package main
  2. import (
  3. "errors"
  4. "fmt"
  5. "time"
  6. )
  7. func main() {
  8. var cc = make(chan int)
  9. go client(cc)
  10. for {
  11. select {
  12. case <-cc:
  13. fmt.Println("continue")
  14. case <-time.After(5 * time.Second):
  15. fmt.Println("timeout")
  16. }
  17. }
  18. }
  19. func client(c chan int) {
  20. defer close(c)
  21. for {
  22. err := processBusiness()
  23. if err != nil {
  24. c <- 0
  25. return
  26. }
  27. c <- 1
  28. }
  29. }
  30. func processBusiness() error {
  31. return errors.New("domo")
  32. }

推荐做法:对通道增加关闭判断。

  1. for {
  2. select {
  3. case _, ok := <-cc:
  4. if ok == false {
  5. fmt.Println("channel closed")
  6. return
  7. }
  8. fmt.Println("continue")
  9. case <-time.After(5 * time.Second):
  10. fmt.Println("timeout")
  11. }
  12. }

【规则 4.1.7.3】禁止重复释放 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. for {
  4. err := processBusiness()
  5. if err != nil {
  6. c <- 0
  7. return
  8. }
  9. c <- 1
  10. }
  11. }

4.1.8 其它

【建议 4.1.8.1】使用 go vet —shadow 检查变量覆盖,以避免无意的变量覆盖。

GO 的变量赋值和声明可以通过”:=” 同时完成,但是由于 Go 可以初始化多个变量,所以这个语法容易引发错误。下面的例子是一个典型的变量覆盖引起的错误,第二个 val 的作用域只限于 for 循环内部,赋值没有影响到之前的 val。

  1. package main
  2. import "fmt"
  3. import "strconv"
  4. func main() {
  5. var val int64
  6. if val, err := strconv.ParseInt("FF", 16, 64); nil != err {
  7. fmt.Printf("parse int failed with error %v\n", err)
  8. } else {
  9. fmt.Printf("inside : val is %d\n", val)
  10. }
  11. fmt.Printf("outside : val is %d \n", val)
  12. }
  13. 执行结果:
  14. inside : val is 255
  15. outside : val is 0

正确的做法:

  1. package main
  2. import "fmt"
  3. import "strconv"
  4. func main() {
  5. var val int64
  6. var err error
  7. if val, err = strconv.ParseInt("FF", 16, 64); nil != err {
  8. fmt.Printf("parse int failed with error %v\n", err)
  9. } else {
  10. fmt.Printf("inside : val is %d\n", val)
  11. }
  12. fmt.Printf("outside : val is %d \n", val)
  13. }
  14. 执行结果:
  15. inside : val is 255
  16. outside : val is 255

【建议 4.1.8.2】GO 的结构体中控制使用 Slice 和 Map。

GO 的 slice 和 map 等变量在赋值时,传递的是引用。从结果上看,是浅拷贝,会导致复制前后的两个变量指向同一片数据。这一点和 Go 的数组、C/C++ 的数组行为不同,很容易出错。

  1. package main
  2. import "fmt"
  3. type Student struct {
  4. Name string
  5. Subjects []string
  6. }
  7. func main() {
  8. sam := Student{
  9. Name: "Sam", Subjects: []string{"Math", "Music"},
  10. }
  11. clark := sam
  12. clark.Name = "Clark"
  13. clark.Subjects[1] = "Philosophy"
  14. fmt.Printf("Sam : %v\n", sam)
  15. fmt.Printf("Clark : %v\n", clark)
  16. }
  17. 执行结果:
  18. Sam : {Sam [Math Philosophy]}
  19. Clark : {Clark [Math Philosophy]}

作为对比,请看作为 Array 定义的 Subjects 的行为:

  1. package main
  2. import "fmt"
  3. type Student struct {
  4. Name string
  5. Subjects [2]string
  6. }
  7. func main() {
  8. var clark Student
  9. sam := Student{
  10. Name: "Sam", Subjects: [2]string{"Math", "Music"},
  11. }
  12. clark = sam
  13. clark.Name = "Clark"
  14. clark.Subjects[1] = "Philosophy"
  15. fmt.Printf("Sam : %v\n", sam)
  16. fmt.Printf("Clark : %v\n", clark)
  17. }
  18. 执行结果:
  19. Sam : {Sam [Math Music]}
  20. Clark : {Clark [Math Philosophy]}

编写代码时,建议这样规避上述问题:
 结构体内尽可能不定义 Slice、Maps 成员;
 如果结构体有 Slice、Maps 成员,尽可能以小写开头、控制其访问;
 结构体的赋值和复制,尽可能通过自定义的深度拷贝函数进行;

【规则 4.1.8.3】避免在循环引用调用 runtime.SetFinalizer。

说明:指针构成的 “循环引用” 加上 runtime.SetFinalizer 会导致内存泄露。

runtime.SetFinalizer 用于在一个对象 obj 被从内存移除前执行一些特殊操作,比如写到日志文件中。在对象被 GC 进程选中并从内存中移除以前,SetFinalizer 都不会执行,即使程序正常结束或者发生错误。

错误示例:垃圾回收器能正确处理 “指针循环引用”,但无法确定 Finalizer 依赖次序,也就无法调用 Finalizer 函数,这会导致目标对象无法变成不可达状态,其所占用内存无法被回收。

  1. package main
  2. import (
  3. "fmt"
  4. "runtime"
  5. "time"
  6. )
  7. type Data struct {
  8. d [1024 * 100]byte
  9. o *Data
  10. }
  11. func test() {
  12. var a, b Data
  13. a.o = &b
  14. b.o = &a
  15. runtime.SetFinalizer(&a, func(d *Data) { fmt.Printf("a %p final.\n", d) })
  16. runtime.SetFinalizer(&b, func(d *Data) { fmt.Printf("b %p final.\n", d) })
  17. }
  18. func main() {
  19. for {
  20. test()
  21. time.Sleep(time.Millisecond)
  22. }
  23. }

通过跟踪 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.1.8.4】避免在 for 循环中使用 time.Tick() 函数。

如果在 for 循环中使用 time.Tick(),它会每次创建一个新的对象返回,应该在 for 循环之外初始化一个 ticker 后,再在循环中使用:

  1. ticker := time.Tick(time.Second)
  2. for {
  3. select {
  4. case <-ticker:
  5. }
  6. }

4.2.1 Memory 优化

【建议 4.2.1.1】将多次分配小对象组合为一次分配大对象。

比如, 将 *bytes.Buffer 结构体成员替换为 bytes。缓冲区 (你可以预分配然后通过调用 bytes.Buffer.Grow 为写做准备) 。这将减少很多内存分配 (更快) 并且减缓垃圾回收器的压力(更快的垃圾回收) 。

【建议 4.2.1.2】将多个不同的小对象绑成一个大结构,可以减少内存分配的次数。

比如:将

  1. for k, v := range m {
  2. k, v := k, v
  3. go func() {
  4. }()
  5. }

替换为:

  1. for k, v := range m {
  2. x := struct{ k, v string }{k, v}
  3. go func() {
  4. }()
  5. }

这就将多次内存分配(分别为 k、v 分配内存)替换为了一次(为 x 分配内存)。然而,这样的优化方式会影响代码的可读性,因此要合理地使用它。

【建议 4.2.1.3】组合内存分配的一个特殊情形是对分片数组进行预分配。

如果清楚一个特定的分片的大小,可以对数组进行预分配:

  1. type X struct {
  2. buf []byte
  3. bufArray [16]byte
  4. }
  5. func MakeX() *X {
  6. x := &X{}
  7. x.buf = x.bufArray[:0]
  8. return x
  9. }

【建议 4.2.1.4】尽可能使用小数据类型,并尽可能满足硬件流水线(Pipeline)的操作,如对齐数据预取边界。

说明:不包含任何指针的对象 (注意 strings,slices,maps 和 chans 包含隐含指针) 不会被垃圾回收器扫描到。

比如,1GB 的分片实际上不会影响垃圾回收时间。因此如果你删除被频繁使用的对象指针,它会对垃圾回收时间造成影响。一些建议:使用索引替换指针,将对象分割为其中之一不含指针的两部分。

【建议 4.2.1.5】使用对象池来重用临时对象,减少内存分配。

标准库包含的 sync.Pool 类型可以实现垃圾回收期间多次重用同一个对象。然而需要注意的是,对于任何手动内存管理的方案来说,不正确地使用 sync.Pool 会导致 use-after-free bug。

4.2.2 GC 优化

【建议 4.2.2.1】设置 GOMAXPROCS 为 CPU 的核心数目,或者稍高的数值。

GC 是并行的,而且一般在并行硬件上具有良好可扩展性。所以给 GOMAXPROCS 设置较高的值是有意义的,就算是对连续的程序来说也能够提高垃圾回收速度。但是,要注意,目前垃圾回收器线程的数量被限制在 8 个以内。

【建议 4.2.2.2】避免频繁创建对象导致 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. type tables struct {
  2. tableA *tableA
  3. tableB *tableB
  4. tableC *tableC
  5. }
  6. type tableA struct {
  7. fieldA int
  8. fieldB string
  9. }
  10. type tableB struct {
  11. city string
  12. code int
  13. next *tableB
  14. }
  15. type tableC struct {
  16. id int
  17. value int64
  18. }

建议一对一表用结构体,一对多表用 slice,每个表都加一个_is_nil 的字段,用来表示当前的数据是否是有用的数据,这样修改的结果是,一万个玩家,产生的对象总量是 1w(tables)+1w([]tablesB),跟前面的差别很明显:

  1. type tables struct {
  2. tableA tableA
  3. tableB []tableB
  4. tableC tableC
  5. }
  6. type tableA struct {
  7. _is_nil bool
  8. fieldA int
  9. fieldB string
  10. }
  11. type tableB struct {
  12. _is_nil bool
  13. city string
  14. code int
  15. next *tableB
  16. }
  17. type tableC struct {
  18. _is_nil bool
  19. id int
  20. value int64
  21. }

4.2.3 其它优化建议

【建议 4.2.3.1】减少[]byte 和 string 之间的转换,尽量使用[]byte 来处理字符。

说明:Go 里面 string 类型是 immutable 类型,而[]byte 是切片类型,是可以修改的,所以 Go 为了保证语法上面没有二义性,在 string 和[]byte 之间进行转换的时候是一个实实在在的值 copy,所以我们要尽量的减少不必要的这个转变。

下面这个例子展示了传递 slice 但是进行了 string 的转化,

  1. func PrefixForBytes(b []byte) string {
  2. return "Hello" + string(b)
  3. }

所以我们可以有两种方式,一种是保持全部的都是 slice 的操作,如下:

  1. func PrefixForBytes(b []byte) []byte {
  2. return append([]byte(“Hello”,b…))
  3. }

还有一种就是全部是 string 的操作方式

  1. func PrefixForBytes(str string) string {
  2. return "Hello" + str
  3. }

推荐阅读:https://blog.golang.org/strings

【建议 4.2.3.2】make 申请 slice/map 时,根据预估大小来申请合适内存。

说明:map 和数组不同,可以根据新增的 对动态的伸缩,因此它不存在固定长度或者最大限制。

map 的空间扩展是一个相对复杂的过程,每次扩容会增加到上次大小的两倍。它的结构体中有一个 buckets 和 oldbuckets,用来实现增量扩容,正常情况下直接使用 buckets,oldbuckets 为空,如果当前哈希表正在扩容,则 oldbuckets 不为空,且 buckets 大小是 oldbuckets 大小的两倍。对于大的 map 或者会快速扩张的 map,即便只是大概知道容量,也最好先标明。

slice 是一个 C 语言动态数组的实现,在对 slice 进行 append 等操作时,可能会造成 slice 的自动扩容,其扩容规则:
 如果新的大小是当前大小 2 倍以上,则大小增长为新大小
 否则循环以下操作:如果当前大小小于 1024,按每次 2 倍增长,否则每次按当前大小 1/4 增长,直到增长的大小超过或者等于新大小

推荐做法:在初始化 map 时指明 map 的容量。

  1. map := make(map[string]float, 100)

【建议 4.2.3.3】字符串拼接优先考虑 bytes.Buffer。

Golang 字符串拼接常见有如下方式:
 fmt.Sprintf
 strings.Join
 string +
 bytes.Buffer

fmt.Sprintf 会动态解析参数,效率通常是最差的,而 string 是只读的,string + 会导致多次对象分配与值拷贝,而 bytes.Buffer 在预设大小情况下,通常只会有一次拷贝和分配,不会重复拷贝和复制,故效率是最佳的。

推荐做法:优先使用 bytes.Buffer,非关键路径,若考虑简洁,可考虑其它方式,比如错误日志拼接使用 fmt.Sprintf,但接口日志使用就不合适。

【建议 4.2.3.4】避免使用 CGO 或者减少跨 CGO 调用次数。

说明:Go 可以调用 C 库函数,但是 Go 带有垃圾收集器且 Go 的栈是可变长,跟 C 实际是不能直接对接的,Go 的环境转入 C 代码执行前,必须为 C 新创建一个新的调用栈,把栈变量赋值给 C 调用栈,调用结束后再拷贝回来,这个调用开销非常大,相比直接 GO 语言调用,单纯的调用开销,可能有 2 个甚至 3 个数量级以上,且 Go 目前还存在版本兼容性问题。

推荐做法:尽量避免使用 CGO,无法避免时,要减少跨 CGO 调用次数。

【建议 4.2.3.5】避免高并发调用同步系统接口。

说明:编程世界同步场景更普遍,GO 提供了轻量级的 routine,用同步来模拟异步操作,故在高并发下的,相比线程,同步模拟代价比较小,可以轻易创建数万个并发调用。然而有些 API 是系统函数,而这些系统函数未提供异步实现,程序中最常见的 posix 规范的文件读写都是同步,epoll 异步可解决网络 IO,而对 regular file 是无法工作的。Go 的运行时环境不可能提供超越操作系统 API 的能力,它依赖于系统 syscall 文件中暴露的 api 能力,而 1.6 版本还是多线程模拟,线程创建切换的代价也非常巨大,开源库中有 filepoller 来模拟异步其实也基于这两种思路,效率上也会大打折扣。

推荐做法:把诸如写文件这样的同步系统调用,要隔离到可控的 routine 中,而不是直接高并发调用。

【建议 4.2.3.6】高并发时避免共享对象互斥。

说明:在 Go 中,可以轻易创建 10000 个 routine 而对系统资源通常就是 100M 的内存要求,但是并发数多了,在多线程中,当并发冲突在 4 个到 8 个线程间时,性能可能就开始出现拐点,急剧下降,这同样适应于 Go,Go 可以轻易创建 routine,但对并发冲突的风险必须要做实现的处理。

推荐做法:routine 需要是独立的,无冲突的执行,若 routine 间有并发冲突,则必须控制可能发生冲突的并发 routine 个数,避免出现性能恶化拐点。

【建议 4.2.3.7】长调用链或在函数中避免申明较多较大临时变量。

routine 的调用栈默认大小 1.7 版本已修改为 2K,当栈大小不够时,Go 运行时环境会做扩栈处理,创建 10000 个 routine 占用空间才 20M,所以 routine 非常轻量级,可以创建大量的并发执行逻辑。而线程栈默认大小是 1M,当然也可以设置到 8K(有些系统可以设置 4K),一般不会这么做,因为线程栈大小是固定的,不能随需而变大,不过实际 CPU 核一般都在 100 以内,线程数是足够的。

routine 是怎么实现可变长栈呢?当栈大小不够时,它会新创建一个栈,通常是 2 倍大小增长,然后把栈赋值过来,而栈中的指针变量需要搜索出来重新指向新的栈地址,好处不是随便有的,这里就明显有性能开销,而且这个开销不小。

说明:频繁创建的 routine,要注意栈生长带来的性能风险,比如栈最终是 2M 大小,极端情况下就会有数 10 次扩栈操作,从而让性能急剧下降。所以必须控制调用栈和函数的复杂度,routine 就意味着轻量级。

对于比较稳定的 routine,也要注意它的栈生长后会导致内存飙升。

【建议 4.2.3.8】为高并发的轻量级任务处理创建 routine 池。

说明:Routine 是轻量级的,但对于高并发的轻量级任务处理,频繁创建 routine 来执行,执行效率也是非常低效率的。

推荐做法:高并发的轻量级任务处理,需要使用 routine 池,避免对调度和 GC 带来冲击。

【建议 4.2.3.9】建议版本提供性能 / 内存监控的功能,并动态开启关闭,但不要长期开启 pprof 提供的 CPU 与 MEM profile 功能。

Go 提供了 pprof 工具包,可以运行时开启 CPU 与内存的 profile 信息,便于定位热点函数的性能问题,而 MEM 的 profile 可以定位内存分配和泄漏相关问题。开启相关统计,跟 GC 一样,也会严重干扰性能,因而不要长期开启。

推荐做法:做测试和问题定位时短暂开启,现网运行,可以开启短暂时间收集相关信息,同时要确保能够自动关闭掉,避免长期打开。
https://www.cnblogs.com/Survivalist/articles/10596131.html