Skip to main content

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.

When converting primitives to/from strings, strconv is faster than fmt.

BadGood
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/opBenchmarkStrconv-4 64.2 ns/op 1 allocs/op

Do not create byte slices from a fixed string repeatedly. Instead, perform the conversion once and capture the result.

BadGood
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/opBenchmarkGood-4 500000000 3.25 ns/op

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.

BadGood
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.

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.

BadGood
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
.}

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).

BadGood
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)
.}

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.

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.

BadGood
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)
.}