Preasignación de Slices, strings.Builder y Pre-dimensionamiento de Mapas
Muchas rutas críticas de Go pasan más tiempo asignando que computando.
Busca en todas las páginas de la documentación
Muchas rutas críticas de Go pasan más tiempo asignando que computando.
Pre-dimensiona slices y mapas cuando se conozca la longitud, construye cadenas con strings.Builder y demuestra las ganancias con -benchmem en lugar de adivinar.
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
// Slice: indicación de capacidad
out := make([]int, 0, len(in))
// Mapa: indicación de tamaño
m := make(map[string]int, len(keys))
// Cadena: Builder con Grow opcional
var b strings.Builder
b.Grow(estimatedLen)
b.WriteString("hola")
_ = b.String()go test -bench=. -benchmem ./...Cuándo usar esto:
allocs/op es alto en benchmarks o perfiles de heappackage alloc
import (
"strings"
"testing"
)
func JoinNaive(parts []string) string {
s := ""
for _, p := range parts {
s += p + ","
}
return s
}
func JoinBuilder(parts []string) string {
var b strings.Builder
b.Grow(len(parts) * 8)
for i, p := range parts {
if i > 0 {
b.WriteByte(',')
}
b.WriteString(p)
}
return b.String()
}
func FilterNaive(in []int) []int {
var out []int
for _, v := range in {
if v%2 == 0 {
out = append(out, v)
}
}
return out
}
func FilterPrealloc(in []int) []int {
out := make([]int, 0, len(in)/2)
for _, v := range in {
if v%2 == 0 {
out = append(out, v)
}
}
return out
}
func BenchmarkJoinNaive(b *testing.B) {
parts := []string{"alfa", "beta", "gamma", "delta"}
b.ReportAllocs()
for b.Loop() {
JoinNaive(parts)
}
}
func BenchmarkJoinBuilder(b *testing.B) {
parts := []string{"alfa", "beta", "gamma", "delta"}
b.ReportAllocs()
for b.Loop() {
JoinBuilder(parts)
}
}Lo que esto demuestra:
+= en bucles asigna una nueva cadena en cada iteración.strings.Builder amortiza el crecimiento con un único buffer de respaldo.make([]T, 0, cap) evita copias repetidas de crecimiento de slices cuando el tamaño es estimable.append de Slice duplica la capacidad cuando está lleno, copiando elementos en cada crecimiento.make([]T, 0, n) asigna un array de respaldo para hasta n elementos.strings.Builder mantiene un buffer []byte; Grow extiende previamente la capacidad antes de escribir.make(map[K]V, hint) reduce los pasos de rehash cuando la indicación es precisa.| Estructura | Fuente de la indicación | Riesgo si es incorrecto |
|---|---|---|
[]T salida | len(input) o fracción filtrada | La capacidad desperdiciada usa memoria |
map[K]V | recuento de claves distintas | Una indicación excesiva desperdicia buckets |
Builder.Grow | suma de longitudes de partes + separadores | Un crecimiento insuficiente aún crece, solo que más tarde |
// Reutilizar Builder - Reset borra el buffer pero mantiene la capacidad
var builderPool sync.Pool
builderPool.New = func() any { return new(strings.Builder) }
func FormatID(id int) string {
b := builderPool.Get().(*strings.Builder)
b.Reset()
b.WriteString("id:")
b.WriteString(strconv.Itoa(id))
s := b.String()
builderPool.Put(b)
return s
}sync.Pool es opcional y solo después de que benchmem demuestre beneficio - los builders en pool deben ser Reset antes de la reutilización.strings.Join es idiomático y ya está optimizado.Builder.String() en un bucle ajustado sin reutilización - Todavía asigna la cadena final (inmutable). Solución: Eso es esperado; eliminar cadenas intermedias, no el resultado.append([]T(nil), s...) - Asigna cada vez. Solución: Pre-dimensionar el destino: dst := make([]T, len(s)); copy(dst, s).bytes.Buffer ciegamente - bytes.Buffer está bien para binarios; strings.Builder evita la copia de []byte a string en String(). Solución: Elegir según el tipo de salida.| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
strings.Join | []string conocido una vez | Construyendo incrementalmente con lógica entre partes |
bytes.Buffer | Protocolos binarios | La salida final debe ser string sin copia |
Buffer de pila fijo [N]byte | IDs formateados pequeños | Tamaño desconocido o grande |
sync.Pool para []byte | Buffers de codificador en la ruta RPC | Uso raro - la sobrecarga del pool gana |
Establezca la capacidad cuando conozca o pueda limitar la longitud final.
Para añadidos raros, la claridad predeterminada de append está bien.
No.
Un builder por goroutine, o proteger con un mutex - el pooling por goroutine de solicitud es típico.
Elimina las copias de crecimiento hasta el tamaño crecido.
El String() final todavía asigna la cadena inmutable resultante.
Use las claves únicas esperadas, por ejemplo, len(ids) cuando las claves son IDs.
Las indicaciones de tamaño excesivo desperdician memoria de los buckets.
make([]T, n) establece la longitud n (incluye valores cero).
Use make([]T, 0, n) cuando vaya a añadir n elementos.
Compare -benchmem antes y después con benchstat.
Adjunte números a las PRs de optimización.
Menos asignaciones reducen la frecuencia de GC y el trabajo de marcado.
Empareje con perfiles de heap para confirmar que la rotación ha disminuido.
Los mapas no tienen append.
Pre-dimensiona con make, luego asigna claves en el bucle.
Escriba directamente en http.ResponseWriter cuando sea posible.
Evite construir cadenas gigantes solo para copiarlas al cable.
Pruebas, salida de CLI y logs únicos con cadenas pequeñas.
Optimice solo las rutas críticas medidas.
Versiones de Stack: Esta página fue escrita para Go 1.26.x (predeterminado Green Tea GC, go fix modernizers - verificar parche en la compilación), chi (última - verificar en la compilación), gin (última - verificar en la compilación), echo (última - verificar en la compilación), google.golang.org/grpc (última - verificar en la compilación), sigs.k8s.io/controller-runtime (última - verificar en la compilación), kubebuilder (última - verificar en la compilación), tinygo (última - verificar objetivos de placa en la compilación), wazero (última - verificar en la compilación), y golangci-lint (última - verificar conjunto de linters en la compilación).
Revisado por Chris St. John·Última actualización: 18 jul 2026