Go Performance Optimization Techniques: sync.Pool, Map Key Choices, Code Generation, strings.Builder, strconv, and Slice Allocation
This article presents a collection of practical Go performance‑tuning techniques—including reuse of objects with sync.Pool, avoiding pointer‑heavy map keys, generating marshal code to bypass reflection, using strings.Builder for efficient concatenation, preferring strconv over fmt, and pre‑allocating slices—to significantly reduce allocation overhead and garbage‑collection time.
Before applying any changes, establish a baseline benchmark using pprof or custom tests to measure current performance.
1. Reuse allocated objects with sync.Pool
The pool provides a free list that can recycle previously allocated structures, reducing allocation and GC pressure. Example:
var bufpool = sync.Pool{
New: func() interface{} {
buf := make([]byte, 512)
return &buf
},
}
bp := bufpool.Get().(*[]byte)
b := *bp
defer func() {
*bp = b
bufpool.Put(bp)
}()
buf := bytes.NewBuffer(b)When pooling objects that contain fields, reset them before returning to the pool to avoid leaking stale data.
type AuthenticationResponse struct {
Token string
UserID string
}
func (a *AuthenticationResponse) reset() {
a.Token = ""
a.UserID = ""
}
rsp := authPool.Get().(*AuthenticationResponse)
defer func() {
rsp.reset()
authPool.Put(rsp)
}()2. Avoid using pointer‑heavy keys in large maps
GC must scan every pointer in a map[string]int . Replacing the string key with an integer reduces scanning overhead. Example comparison shows GC time dropping from ~100 ms to ~4 ms when switching to map[int]int .
var foo = map[string]int{}
// populate foo with 10,000,000 entries
// ...
// GC timing codeResulting GC times improve dramatically after the change.
3. Generate marshal code to avoid runtime reflection
Using code generators like easyjson creates type‑specific marshal/unmarshal functions that implement the json.Marshaller interface, eliminating the reflection overhead of json.Marshal and json.Unmarshal .
easyjson -all $file.goThis produces $file_easyjson.go with optimized functions.
4. Build strings with strings.Builder
Strings are immutable; concatenating them repeatedly allocates new memory. strings.Builder writes to an internal byte buffer and creates the final string only once. Benchmarks show a 4.7× speedup and an 8× reduction in allocations.
func buildStrNaive() string {
var s string
for _, v := range strs { s += v }
return s
}
func buildStrBuilder() string {
var b strings.Builder
b.Grow(60)
for _, v := range strs { b.WriteString(v) }
return b.String()
}5. Prefer strconv over fmt for numeric‑to‑string conversion
Benchmarks demonstrate that strconv.Itoa is about 3.5× faster and allocates fewer bytes than fmt.Sprintf for simple integer formatting.
func strconvFmt(a string, b int) string { return a + ":" + strconv.Itoa(b) }
func fmtFmt(a string, b int) string { return fmt.Sprintf("%s:%d", a, b) }6. Pre‑allocate slice capacity with make
Creating a slice with zero capacity causes repeated allocations as elements are appended. Using make([]T, 0, expectedLen) allocates the needed backing array once, avoiding extra copies and allocations.
userIDs := make([]string, 0, len(rsp.Users))
for _, u := range rsp.Users { userIDs = append(userIDs, u.ID) }7. Use APIs that accept byte slices
Methods like time.AppendFormat allow reusing a pre‑allocated buffer, reducing temporary allocations compared to functions that return new strings.
Conclusion
Applying these techniques—object pooling, pointer‑light data structures, code generation, efficient string building, low‑level conversion functions, and careful slice allocation—can dramatically improve Go program performance and reduce garbage‑collector overhead, leading to faster, more scalable services.
360 Tech Engineering
Official tech channel of 360, building the most professional technology aggregation platform for the brand.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.