go-performance
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGo Performance Patterns
Go语言性能优化模式
Source: Uber Go Style Guide
Performance-specific guidelines apply only to the hot path. Don't prematurely optimize—focus these patterns where they matter most.
来源:Uber Go 风格指南
性能相关的准则仅适用于热点路径。不要过早进行优化——将这些模式应用在真正有需要的地方。
Prefer strconv over fmt
优先使用strconv而非fmt
When converting primitives to/from strings, is faster than .
strconvfmtSource: Uber Go Style Guide
Bad:
go
for i := 0; i < b.N; i++ {
s := fmt.Sprint(rand.Int())
}Good:
go
for i := 0; i < b.N; i++ {
s := strconv.Itoa(rand.Int())
}Benchmark comparison:
| Approach | Speed | Allocations |
|---|---|---|
| 143 ns/op | 2 allocs/op |
| 64.2 ns/op | 1 allocs/op |
在将基本类型与字符串相互转换时,比更快。
strconvfmt来源:Uber Go 风格指南
不良写法:
go
for i := 0; i < b.N; i++ {
s := fmt.Sprint(rand.Int())
}推荐写法:
go
for i := 0; i < b.N; i++ {
s := strconv.Itoa(rand.Int())
}基准测试对比:
| 写法 | 速度 | 内存分配 |
|---|---|---|
| 143 ns/op | 2 allocs/op |
| 64.2 ns/op | 1 allocs/op |
Avoid Repeated String-to-Byte Conversions
避免重复的字符串转字节切片操作
Do not create byte slices from a fixed string repeatedly. Instead, perform the conversion once and capture the result.
Source: Uber Go Style Guide
Bad:
go
for i := 0; i < b.N; i++ {
w.Write([]byte("Hello world"))
}Good:
go
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
w.Write(data)
}Benchmark comparison:
| Approach | Speed |
|---|---|
| Repeated conversion | 22.2 ns/op |
| Single conversion | 3.25 ns/op |
The good version is ~7x faster because it avoids allocating a new byte slice on each iteration.
不要反复从固定字符串创建字节切片。应只执行一次转换并复用结果。
来源:Uber Go 风格指南
不良写法:
go
for i := 0; i < b.N; i++ {
w.Write([]byte("Hello world"))
}推荐写法:
go
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
w.Write(data)
}基准测试对比:
| 写法 | 速度 |
|---|---|
| 重复转换 | 22.2 ns/op |
| 单次转换 | 3.25 ns/op |
推荐写法的速度约快7倍,因为它避免了在每次循环中分配新的字节切片。
Prefer Specifying Container Capacity
优先指定容器容量
Specify container capacity where possible to allocate memory up front. This minimizes subsequent allocations from copying and resizing as elements are added.
Source: Uber Go Style Guide
尽可能指定容器容量,提前分配内存。这能减少后续添加元素时因复制和扩容产生的内存分配。
来源:Uber Go 风格指南
Map Capacity Hints
Map容量提示
Provide capacity hints when initializing maps with .
make()go
make(map[T1]T2, hint)Note: Unlike slices, map capacity hints do not guarantee complete preemptive allocation—they approximate the number of hashmap buckets required.
Bad:
go
files, _ := os.ReadDir("./files")
m := make(map[string]os.DirEntry)
for _, f := range files {
m[f.Name()] = f
}
// Map resizes dynamically, causing multiple allocationsGood:
go
files, _ := os.ReadDir("./files")
m := make(map[string]os.DirEntry, len(files))
for _, f := range files {
m[f.Name()] = f
}
// Map is right-sized at initialization, fewer allocations使用初始化Map时提供容量提示。
make()go
make(map[T1]T2, hint)注意:与Slice不同,Map的容量提示并不能保证完全预分配内存——它只是近似估算所需的哈希桶数量。
不良写法:
go
files, _ := os.ReadDir("./files")
m := make(map[string]os.DirEntry)
for _, f := range files {
m[f.Name()] = f
}
// Map会动态扩容,导致多次内存分配推荐写法:
go
files, _ := os.ReadDir("./files")
m := make(map[string]os.DirEntry, len(files))
for _, f := range files {
m[f.Name()] = f
}
// Map在初始化时就分配了合适的大小,减少内存分配次数Slice Capacity
Slice容量
Provide capacity hints when initializing slices with , particularly when appending.
make()go
make([]T, length, capacity)Unlike maps, slice capacity is not a hint—the compiler allocates exactly that much memory. Subsequent operations incur zero allocations until capacity is reached.
append()Bad:
go
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++ {
data = append(data, k)
}
}Good:
go
for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++ {
data = append(data, k)
}
}Benchmark comparison:
| Approach | Time (100M iterations) |
|---|---|
| No capacity | 2.48s |
| With capacity | 0.21s |
The good version is ~12x faster due to zero reallocations during append.
使用初始化Slice时提供容量提示,尤其是在需要追加元素的场景下。
make()go
make([]T, length, capacity)与Map不同,Slice的容量不是提示——编译器会精确分配指定大小的内存。在容量耗尽前,后续的操作不会产生任何内存分配。
append()不良写法:
go
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++ {
data = append(data, k)
}
}推荐写法:
go
for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++ {
data = append(data, k)
}
}基准测试对比:
| 写法 | 耗时(1亿次迭代) |
|---|---|
| 未指定容量 | 2.48s |
| 指定容量 | 0.21s |
推荐写法的速度约快12倍,因为追加过程中没有重新分配内存。
Pass Values
传递值类型
Source: Go Wiki CodeReviewComments (Advisory)
Don't pass pointers as function arguments just to save a few bytes. If a function refers to its argument only as throughout, then the argument shouldn't be a pointer.
x*xCommon instances where values should be passed directly:
- Pointer to a string () — strings are already small fixed-size headers
*string - Pointer to an interface value () — interfaces are fixed-size (type + data pointers)
*io.Reader
Bad:
go
func process(s *string) {
fmt.Println(*s) // only dereferences, never modifies
}Good:
go
func process(s string) {
fmt.Println(s)
}Exceptions:
- Large structs where copying is expensive
- Small structs that might grow in the future
来源:Go Wiki CodeReviewComments(建议)
不要仅仅为了节省几个字节就将指针作为函数参数传递。如果函数在整个执行过程中仅通过来引用参数,那么该参数不应该是指针类型。
*xx适合直接传递值类型的常见场景:
- 字符串指针()——字符串本身已是固定大小的头部结构
*string - 接口值指针()——接口是固定大小的(类型指针+数据指针)
*io.Reader
不良写法:
go
func process(s *string) {
fmt.Println(*s) // 仅解引用,从不修改
}推荐写法:
go
func process(s string) {
fmt.Println(s)
}例外情况:
- 大型结构体,复制成本较高
- 未来可能会扩容的小型结构体
Quick Reference
快速参考
| Pattern | Bad | Good | Improvement |
|---|---|---|---|
| Int to string | | | ~2x faster |
Repeated | | Convert once outside | ~7x faster |
| Map initialization | | | Fewer allocs |
| Slice initialization | | | ~12x faster |
| Small fixed-size args | | | No indirection |
| 模式 | 不良写法 | 推荐写法 | 优化效果 |
|---|---|---|---|
| 整数转字符串 | | | 约2倍提速 |
重复创建 | 循环内执行 | 提前转换一次并复用 | 约7倍提速 |
| Map初始化 | | | 减少内存分配次数 |
| Slice初始化 | | | 约12倍提速 |
| 小型固定大小参数 | | | 避免间接引用 |
See Also
参考链接
- For core style principles:
go-style-core - For naming conventions:
go-naming
- 核心风格原则:
go-style-core - 命名规范:
go-naming