原文连接: https://github.com/sxs2473/go...
本文使用 Creative Commons Attribution-ShareAlike 4.0 International 协议进行受权许可。
本节包含一些优化 Go 代码的技巧。git
确保你的 APIs 不会给调用方增长垃圾。github
考虑这两个 Read 方法golang
func (r *Reader) Read() ([]byte, error) func (r *Reader) Read(buf []byte) (int, error)
第一个 Read 方法不带参数,并将一些数据做为[]byte
返回。 第二个采用[]byte
缓冲区并返回读取的字节数。windows
第一个 Read 方法老是会分配一个缓冲区,这会给 GC 带来压力。 第二个填充传入的缓冲区。缓存
Go 语言中 string
是不可改变的,而 []byte
是可变的。服务器
大多数程序喜欢使用 string
,而大多数 IO 操做更喜欢使用 []byte
。网络
尽量避免 []byte
到 string
的转换,对于一个值来讲,最好选定一种表示方式,要么是[]byte
,要么是string
。 一般状况下,若是你从网络或磁盘读取数据,将使用[]byte
表示。闭包
bytes
包也有一些和 strings
包相同的操做函数—— Split
, Compare
, HasPrefix
, Trim
等。并发
实际上, strings
使用和 bytes
包相同的汇编原语。app
使用 string
做为 map 的 key 是很常见的,但有时你拿到的是一个 []byte
。
编译器为这种状况实现特定的优化:
var m map[string]string v, ok := m[string(bytes)]
如上面这样写,编译器会避免将字节切片转换为字符串到 map 中查找,这是很是特定的细节,若是你像下面这样写,这个优化就会失效:
key := string(bytes) val, ok := m[key]
Go 的字符串是不可变的。链接两个字符串就会生成第三个字符串。下面哪一种写法是最快的呢?
s := request.ID s += " " + client.Addr().String() s += " " + time.Now().String() r = s
var b bytes.Buffer fmt.Fprintf(&b, "%s %v %v", request.ID, client.Addr(), time.Now()) r = b.String()
r = fmt.Sprintf("%s %v %v", request.ID, client.Addr(), time.Now())
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/
个人测试结果:
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
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 以后的容量是多少呢?
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:
var s []string for _, v := range fn() { s = append(s, v) } return s
After:
vals := fn() s := make([]string, len(vals)) for i, v := range vals { s[i] = v } return s
使 Go 很是适合现代硬件的关键特性是 goroutines。goroutine 很容易使用,成本也很低,你能够认为它们几乎是没有成本的。
Go 运行时是为运行数以万计的 goroutines 所设计的,即便有上十万也在乎料之中。
可是,每一个 goroutine 确实消耗了 goroutine 栈的最小内存量,目前至少为 2k。
2048 * 1,000,000 goroutines == 2GB 内存,什么都不干的状况下。
这也许算多,也许不算多,同时取决于机器上其余耗费内存的应用。
虽然 goroutine 的启动和运行成本都很低,但它们的内存占用是有限的;你不可能建立无限数量的 goroutine。
每次在程序中使用go
关键字启动 goroutine 时,你都必须知道这个 goroutine 将如何退出,以及什么时候退出。
若是你不知道,那这就是潜在的内存泄漏。
在你的设计中,一些 goroutine 可能会一直运行到程序退出。这样的 goroutine 不该该太多
永远不要在不知道该何时中止它的状况下启动一个 goroutine
实现此目的的一个好方法是利用如 run.Group, workgroup.Group 这类的东西。
Peter Bourgon has a great presentation on the design behing run.Group from GopherCon EU
Go 运行时使用高效的操做系统轮询机制(kqueue,epoll,windows IOCP等)处理网络IO。 许多等待的 goroutine 将由一个操做系统线程提供服务。
可是,对于本地文件IO(channel 除外),Go 不实现任何 IO 轮询。每个*os.File
在运行时都消耗一个操做系统线程。
大量使用本地文件IO会致使程序产生数百或数千个线程;这可能会超过操做系统的最大值限制。
您的磁盘子系统可能处理不数百或数千个并发IO请求。
若是你写的是服务端程序,那么其主要工做是复用网络链接客户端和存储在应用程序中的数据。
大多数服务端程序都是接受请求,进行一些处理,而后返回结果。这听起来很简单,但有的时候,这样作会让客户端在服务器上消耗大量(可能无限制)的资源。下面有一些注意事项:
若是内存都不算快,那么相对来讲,IO操做就太慢了,你应该不惜一切代价避免这样作。 最重要的是避免在请求的上下文中执行IO——不要让用户等待磁盘子系统写入磁盘,甚至连读取都不要作。
尽量避免将数据读入[]byte
并传递使用它。
根据请求的不一样,你最终可能会将兆字节(或更多)的数据读入内存。这会给GC带来巨大的压力,而且会增长应用程序的平均延迟。
做为替代,最好使用io.Reader
和io.Writer
构建数据处理流,以限制每一个请求使用的内存量。
若是你使用了大量的io.Copy
,那么为了提升效率,请考虑实现io.ReaderFrom
/ io.WriterTo
。 这些接口效率更高,并避免将内存复制到临时缓冲区。
永远不要在不知道须要多长时间才能完成的状况下执行 IO 操做。
你要在使用SetDeadline
,SetReadDeadline
,SetWriteDeadline
进行的每一个网络请求上设置超时。
您要限制所使用的阻塞IO的数量。 使用 goroutine 池或带缓冲的 channel 做为信号量。
var semaphore = make(chan struct{}, 10) func processRequest(work *Work) { semaphore <- struct{}{} // 持有信号量 // 执行请求 <-semaphore // 释放信号量 }
defer
是有成本的,由于它必须为其执行参数构造一个闭包去执行。
defer mu.Unlock()
至关于
defer func() { mu.Unlock() }()
若是你用它干的事情不多,defer
的成本就会显得比较高。一个经典的例子是使用defer
对 struct 或 map 进行mutex unlock
操做。 你能够在这些状况下避免使用defer
固然,这是为了提升性能而牺牲可读性和维护性的状况。
终结器是一种将行为附加到即将被垃圾收集的对象的技术。
所以,终结器是非肯定性的。
要运行 Finalizers,要保证任何东西都不会访问该对象。 若是你不当心在 map 中保留了对象的引用,则 Finalizers 没法执行。
Finalizers 做为 gc 的一部分运行,这意味着它们在运行时是不可预测的,而且它会与 减小 gc 时间 的目标相悖。
当你有一个很是大的堆块,而且已经优化过你的程序使之减小生成垃圾,Finalizers 可能才会很快结束。
提示 :参考 SetFinalizer
cgo 容许 Go 程序调用 C 语言库。
C 代码和 Go 代码存在于两个不一样的世界中,cgo 用来转换它们。
这种转换不是没有代价的,主要取决于它在代码中的位置,有时成本可能很高。
cgo 调用相似于阻塞IO,它们在操做期间消耗一个系统线程。
不要在一个 tight loop 中调用 C 代码。
cgo 的开销很高。
为了得到最佳性能,我建议你在应用中避免使用cgo。
Go 的旧版本永远不会变得更好。他们永远不会获得错误修复或优化。
Go 的旧版本不会有任何更新。 不要使用它们。 使用最新版本,你将得到最佳性能。