How To Code Go Part 3: Perfomance
Introduction
Go's performance is a key factor in its popularity and suitability for a wide range of applications. Its speed, efficiency, concurrency support, and small footprint contribute to better responsiveness, scalability, and resource utilization, making it a strong choice for modern software development, especially in cloud-native and microservices environments.
RECOMMENDED Prefer strconv over fmt
When converting primitives to/from strings, strconv
is faster than fmt
.
Bad | Good |
---|---|
for i := 0; i < b.N; i++ { . s := fmt.Sprint(rand.Int()) . } | for i := 0; i < b.N; i++ { . s := strconv.Itoa(rand.Int()) . } |
BenchmarkFmtSprint-4 143 ns/op 2 allocs/op | BenchmarkStrconv-4 64.2 ns/op 1 allocs/op |
RECOMMENDED Avoid string-to-byte conversion
Do not create byte slices from a fixed string repeatedly. Instead, perform the conversion once and capture the result.
Bad | Good |
---|---|
for i := 0; i < b.N; i++ { . w.Write([]byte("Hello world")) . } | data := []byte("Hello world") . for i := 0; i < b.N; i++ { . w.Write(data) . } |
BenchmarkBad-4 50000000 22.2 ns/op | BenchmarkGood-4 500000000 3.25 ns/op |
RECOMMENDED Concatenating and Building Strings with strings.Builder
Because strings in Go are immutable, we need to create an entirely new string when we want to change a string or add contents to it, such as s += str
. The performance will be poor, so we should use string.Builder
instead.
Bad | Good |
---|---|
func join(strs ...string) string { . var ret string . for _, str := range strs { . ret += str . } . return ret . } | func join(strs ...string) string { . var sb strings.Builder . for _, str := range strs { . sb.WriteString(str) . } . return sb.String() . } |
Prefer Specifying Container Capacity
Specify container capacity where possible in order to allocate memory for the container up front. This minimizes subsequent allocations (by copying and resizing of the container) as elements are added.
RECOMMENDED Specifying Map Capacity Hints
Where possible, provide capacity hints when initializing maps with make()
.
Providing a capacity hint to make()
tries to right-size the map at initialization time, which reduces the need for growing the map and allocations as elements are added to the map.
Note that, unlike slices, map capacity hints do not guarantee complete, preemptive allocation, but are used to approximate the number of hashmap buckets required. Consequently, allocations may still occur when adding elements to the map, even up to the specified capacity.
Bad | Good |
---|---|
m := make(map[string]os.FileInfo) . files, _ := ioutil.ReadDir("./files") . for _, f := range files { . m[f.Name()] = f . } | files, _ := ioutil.ReadDir("./files") . m := make(map[string]os.FileInfo, len(files)) . for _, f := range files { . m[f.Name()] = f . } |
RECOMMENDED Specifying Slice Capacity
Where possible, provide capacity hints when initializing slices with make()
, particularly when appending.
make([]T, length, capacity)
Unlike maps, slice capacity is not a hint: the compiler will allocate enough memory for the capacity of the slice as provided to make()
, which means that subsequent append()
operations will incur zero allocations (until the length of the slice matches the capacity, after which any appends will require a resize to hold additional elements).
Bad | Good |
---|---|
for n := 0; n < b.N; n++ { . data := make([]int, 0) . for k := 0; k < size; k++ { . data = append(data, k) . } | for n := 0; n < b.N; n++ { . data := make([]int, 0, size) . for k := 0; k < size; k++ { . data = append(data, k) . } |
RECOMMENDED Use Index for Range when the Value Size is Large
Range
keyword traversal every item and make a value copy, so it will be cost expensive if the value is not a point or reference and is large. Use index for read or modify the value instead of a value copy.
RECOMMENDED Use sync.Pool when necessary
When you need re-malloc and destroy resource repeatedly, like temporary object、connections etc, you maybe need use sync.Pool
to avoid the performance loss, such as object malloc and gc or tcp connection establish.
Bad | Good |
---|---|
var data = make([]byte, 10000) . for n := 0; n < b.N; n++ { . var buf bytes.Buffer . buf.Write(data) . } | var bufferPool = sync.Pool{ . New: func() interface{} { . return &bytes.Buffer{} . }, . } . var data = make([]byte, 10000) . for n := 0; n < b.N; n++ { . buf := bufferPool.Get().(*bytes.Buffer) . buf.Write(data) . buf.Reset() . bufferPool.Put(buf) . } |