# 技巧 本节包含一些优化 Go 代码的技巧。 ## 减少分配 确保你的 APIs 不会给调用方增加垃圾。 考虑这两个 Read 方法 ```go func (r *Reader) Read() ([]byte, error) func (r *Reader) Read(buf []byte) (int, error) ``` 第一个 Read 方法不带参数,并将一些数据作为`[]byte`返回。 第二个采用`[]byte`缓冲区并返回读取的字节数。 第一个 Read 方法总是会分配一个缓冲区,这会给 GC 带来压力。 第二个填充传入的缓冲区。 ## strings vs []bytes Go 语言中 `string` 是不可改变的,而 `[]byte` 是可变的。 大多数程序喜欢使用 `string`,而大多数 IO 操作更喜欢使用 `[]byte`。 尽可能避免 `[]byte` 到 `string` 的转换,对于一个值来说,最好选定一种表示方式,要么是`[]byte`,要么是`string`。 通常情况下,如果你从网络或磁盘读取数据,将使用`[]byte` 表示。 [`bytes`][2] 包也有一些和 [`strings`][3] 包相同的操作函数—— `Split`, `Compare`, `HasPrefix`, `Trim`等。 实际上, `strings` 使用和 `bytes` 包相同的汇编原语。 ## 使用 []byte 当做 map 的 key 使用 `string` 作为 map 的 key 是很常见的,但有时你拿到的是一个 `[]byte`。 编译器为这种情况实现特定的优化: ```go var m map[string]string v, ok := m[string(bytes)] ``` 如上面这样写,编译器会避免将字节切片转换为字符串到 map 中查找,这是非常特定的细节,如果你像下面这样写,这个优化就会失效: ```go key := string(bytes) val, ok := m[key] ``` ## 优化字符串连接操作 Go 的字符串是不可变的。连接两个字符串就会生成第三个字符串。下面哪种写法是最快的呢? ```go s := request.ID s += " " + client.Addr().String() s += " " + time.Now().String() r = s ``` ```go var b bytes.Buffer fmt.Fprintf(&b, "%s %v %v", request.ID, client.Addr(), time.Now()) r = b.String() ``` ```go r = fmt.Sprintf("%s %v %v", request.ID, client.Addr(), time.Now()) ``` ```go b := make([]byte, 0, 40) b = append(b, request.ID...) b = append(b, ' ') b = append(b, client.Addr().String()...) b = append(b, ' ') b = time.Now().AppendFormat(b, "2006-01-02 15:04:05.999999999 -0700 MST") r = string(b) ``` ``` % go test -bench=. ./examples/concat/ ``` 我的测试结果: - go 1.10.3 ``` goos: darwin goarch: amd64 pkg: test/benchmark BenchmarkConcatenate-8 2000000 873 ns/op 272 B/op 10 allocs/op BenchmarkFprintf-8 1000000 1509 ns/op 496 B/op 13 allocs/op BenchmarkSprintf-8 1000000 1316 ns/op 304 B/op 11 allocs/op BenchmarkStrconv-8 2000000 620 ns/op 165 B/op 5 allocs/op PASS ``` - go 1.11 ``` goos: darwin goarch: amd64 pkg: test/benchmark BenchmarkConcatenate-8 1000000 1027 ns/op 271 B/op 10 allocs/op BenchmarkFprintf-8 1000000 1707 ns/op 496 B/op 12 allocs/op BenchmarkSprintf-8 1000000 1412 ns/op 304 B/op 11 allocs/op BenchmarkStrconv-8 2000000 707 ns/op 165 B/op 5 allocs/op PASS ``` 所有的基准测试在1.11版本下都变慢了? ## 已知长度时,切片一次分配好 Append 操作虽然方便,但是有代价。 切片的增长在元素到达 1024 个之前一直是两倍左右地变化,在到达 1024 个之后之后大约是 25% 地增长。在我们 append 之后的容量是多少呢? ```go func main() { b := make([]int, 1024) fmt.Println("len:", len(b), "cap:", cap(b)) b = append(b, 99) fmt.Println("len:", len(b), "cap:", cap(b)) } output: len: 1024 cap: 1024 len: 1025 cap: 1280 ``` 如果你使用 append,你可能会复制大量数据并产生大量垃圾。 如果事先知道片的长度,最好预先分配大小以避免复制,并确保目标的大小完全正确。 _Before:_ ```go var s []string for _, v := range fn() { s = append(s, v) } return s ``` _After:_ ```go vals := fn() s := make([]string, len(vals)) for i, v := range vals { s[i] = v } return s ``` ## Goroutines 使 Go 非常适合现代硬件的关键特性是 goroutines。goroutine 很容易使用,成本也很低,你可以认为它们几乎是没有成本的。 Go 运行时是为运行数以万计的 goroutines 所设计的,即使有上十万也在意料之中。 但是,每个 goroutine 确实消耗了 goroutine 栈的最小内存量,目前至少为 2k。 2048 * 1,000,000 goroutines == 2GB 内存,什么都不干的情况下。 这也许算多,也许不算多,同时取决于机器上其他耗费内存的应用。 ## 要了解 goroutine 什么时候退出 虽然 goroutine 的启动和运行成本都很低,但它们的内存占用是有限的;你不可能创建无限数量的 goroutine。 每次在程序中使用`go`关键字启动 goroutine 时,你都必须知道这个 goroutine 将如何退出,以及何时退出。 如果你不知道,那这就是潜在的内存泄漏。 在你的设计中,一些 goroutine 可能会一直运行到程序退出。这样的 goroutine 不应该太多 **永远不要在不知道该什么时候停止它的情况下启动一个 goroutine** 实现此目的的一个好方法是利用如 [run.Group][4], [workgroup.Group][5] 这类的东西。 Peter Bourgon has a great presentation on the design behing run.Group from GopherCon EU ### 进一步阅读 - [Concurrency Made Easy][0] (视频) - [Concurrency Made Easy][1] (幻灯片) ## Go 对一些请求使用高效的网络轮询 Go 运行时使用高效的操作系统轮询机制(kqueue,epoll,windows IOCP等)处理网络IO。 许多等待的 goroutine 将由一个操作系统线程提供服务。 但是,对于本地文件IO(channel 除外),Go 不实现任何 IO 轮询。每一个`*os.File`在运行时都消耗一个操作系统线程。 大量使用本地文件IO会导致程序产生数百或数千个线程;这可能会超过操作系统的最大值限制。 您的磁盘子系统可能处理不数百或数千个并发IO请求。 ## 注意程序中的 IO 复杂度 如果你写的是服务端程序,那么其主要工作是复用网络连接客户端和存储在应用程序中的数据。 大多数服务端程序都是接受请求,进行一些处理,然后返回结果。这听起来很简单,但有的时候,这样做会让客户端在服务器上消耗大量(可能无限制)的资源。下面有一些注意事项: - 每个请求的IO操作数量;单个客户端请求生成多少个IO事件? 如果使用缓存,则它可能平均为1,或者可能小于1。 - 服务查询所需的读取量;它是固定的?N + 1的?还是线性的(读取整个表格以生成结果的最后一页)? 如果内存都不算快,那么相对来说,IO操作就太慢了,你应该不惜一切代价避免这样做。 最重要的是避免在请求的上下文中执行IO——不要让用户等待磁盘子系统写入磁盘,甚至连读取都不要做。 ## 使用流式 IO 接口 尽可能避免将数据读入`[]byte` 并传递使用它。 根据请求的不同,你最终可能会将兆字节(或更多)的数据读入内存。这会给GC带来巨大的压力,并且会增加应用程序的平均延迟。 作为替代,最好使用`io.Reader`和`io.Writer`构建数据处理流,以限制每个请求使用的内存量。 如果你使用了大量的`io.Copy`,那么为了提高效率,请考虑实现`io.ReaderFrom` / `io.WriterTo`。 这些接口效率更高,并避免将内存复制到临时缓冲区。 ## 超时,超时,还是超时 永远不要在不知道需要多长时间才能完成的情况下执行 IO 操作。 你要在使用`SetDeadline`,`SetReadDeadline`,`SetWriteDeadline`进行的每个网络请求上设置超时。 您要限制所使用的阻塞IO的数量。 使用 goroutine 池或带缓冲的 channel 作为信号量。 ```go var semaphore = make(chan struct{}, 10) func processRequest(work *Work) { semaphore <- struct{}{} // 持有信号量 // 执行请求 <-semaphore // 释放信号量 } ``` ## Defer 操作成本如何? `defer` 是有成本的,因为它必须为其执行参数构造一个闭包去执行。 ``` defer mu.Unlock() ``` 相当于 ``` defer func() { mu.Unlock() }() ``` 如果你用它干的事情很少,`defer` 的成本就会显得比较高。一个经典的例子是使用`defer`对 struct 或 map 进行`mutex unlock` 操作。 你可以在这些情况下避免使用`defer` 当然,这是为了提高性能而牺牲可读性和维护性的情况。 #### 总是重新考虑这些决定。 ## 避免使用 Finalizers 终结器是一种将行为附加到即将被垃圾收集的对象的技术。  因此,终结器是非确定性的。 要运行 Finalizers,要保证任何东西都不会访问该对象。 如果你不小心在 map 中保留了对象的引用,则 Finalizers 无法执行。 Finalizers 作为 gc 的一部分运行,这意味着它们在运行时是不可预测的,并且它会与_减少 gc 时间_的目标相悖。 当你有一个非常大的堆块,并且已经优化过你的程序使之减少生成垃圾,Finalizers 可能才会很快结束。 _提示_:参考 [SetFinalizer][6] ## 最小化 cgo cgo 允许 Go 程序调用 C 语言库。 C 代码和 Go 代码存在于两个不同的世界中,cgo 用来转换它们。 这种转换不是没有代价的,主要取决于它在代码中的位置,有时成本可能很高。 cgo 调用类似于阻塞IO,它们在操作期间消耗一个系统线程。 不要在一个 [tight loop][7] 中调用 C 代码。 ## 实际上,避免使用 cgo cgo 的开销很高。 为了获得最佳性能,我建议你在应用中避免使用cgo。 - 如果C代码需要很长时间,那么 cgo 本身的开销就不那么重要了。 - 如果你使用 cgo 来调用非常短的C函数,那么cgo本身的开销就会显得非常突出,那么最好的办法是在 Go 中重写该代码。(因为很短,重写也没什么成本。 - 如果你就是要使用大量高开销成本的C代码在 tight loop 中调用,为什么使用 Go?(直接用 C 写就好了被。 ## 始终使用最新版发布的 Go 版本 Go 的旧版本永远不会变得更好。他们永远不会得到错误修复或优化。 - Go 1.4 不应该再使用。 - Go 1.5 和 1.6 编译器的速度更慢,但它产生更快的代码,并具有更快的 GC。 - Go 1.7 的编译速度比 1.6 提高了大约 30%,链接速度提高了2倍(优于之前的Go版本)。 - Go 1.8 在编译速度方面带来较小的改进,且在非Intel体系结构的代码质量方面有显著的改进。 - Go 1.9,1.10,1.11 继续降低 GC 暂停时间并提高生成代码的质量。 Go 的旧版本不会有任何更新。 不要使用它们。 使用最新版本,你将获得最佳性能。 [0]: https://www.youtube.com/watch?v=yKQOunhhf4A&index=16&list=PLq2Nv-Sh8EbZEjZdPLaQt1qh_ohZFMDj8 [1]: https://dave.cheney.net/paste/concurrency-made-easy.pdf [2]: https://golang.org/pkg/bytes [3]: https://golang.org/pkg/strings [4]: https://github.com/oklog/run [5]: https://github.com/heptio/workgroup [6]: https://golang.org/pkg/runtime/#SetFinalizer [7]:https://en.wiktionary.org/wiki/tight_loop