Home
Jul 27, 2026 - 22 MIN READ
Array ou slice? Capacidade, crescimento e as armadilhas do append

Array ou slice? Capacidade, crescimento e as armadilhas do append

Como slices funcionam por dentro em Go: o cabeçalho de três palavras, a diferença entre len e cap, como o append faz o slice crescer, e por que dois slices podem enxergar o mesmo array sem você perceber.

No artigo sobre zero values e pointer semantics eu comentei de passagem que copiar um slice copia só o seu cabeçalho, não o array por baixo, e que aquilo rendia um artigo inteiro. É este aqui.

Vamos começar pelo fim, com um bug real. Imagine um router HTTP em que cada grupo de rotas herda os middlewares do grupo pai:

func (r *Router) Group(m Middleware) *Router {
    return &Router{middlewares: append(r.middlewares, m)}
}

A função tem uma linha, não tem ponteiro solto, não tem concorrência, e devolve um Router novo em vez de mutar o antigo. Parece o tipo de código que não tem como dar errado. Agora o uso:

admin := root.Group(requireAdmin)
public := root.Group(rateLimit)
root: len=3 cap=4 [logging recover requestID]

admin : [logging recover requestID rateLimit]
public: [logging recover requestID rateLimit]

O router de admin ficou sem o middleware de autenticação. Ninguém sobrescreveu nada de propósito, e o código está sintaticamente correto. O que aconteceu é que os dois append escreveram na mesma posição do mesmo array, e o segundo passou por cima do primeiro.

Repare no cap=4 com len=3. Se fossem iguais, o bug não aconteceria. Ou seja: esse código funciona ou não dependendo de quantos middlewares o pai tinha. Esse é o tipo de bug que passa em teste, passa em code review e aparece em produção seis meses depois, quando alguém adiciona um middleware a mais.

A proposta deste artigo é destrinchar exatamente isso: primeiro a diferença entre array e slice, depois o que é capacidade e por que ela não é um detalhe, como o append faz o slice crescer, e por fim como dois slices acabam enxergando a mesma memória — e o que fazer a respeito.

Tudo aqui foi verificado com Go 1.26 no gc (o compilador oficial). Números concretos de capacidade são detalhes de implementação e já mudaram entre versões; existe uma seção sobre isso mais adiante.

Duas coisas diferentes com nomes parecidos

Antes de falar do append, vale separar dois tipos que a sintaxe faz parecer irmãos e que não são a mesma coisa.

Um array é um valor autocontido, e o tamanho faz parte do tipo. A spec é direta:

"O tamanho faz parte do tipo do array; ele precisa ser avaliado como uma constante não negativa representável por um valor do tipo int."

The Go Programming Language Specification, Array types

Isso tem uma consequência que assusta quem vem de C: [3]int e [4]int são tipos diferentes. Não dá para passar um onde se espera o outro, e não dá nem para compará-los.

Um slice, por outro lado, não guarda os dados. Ele descreve um pedaço de um array que mora em outro lugar:

"Um slice é um descritor para um segmento contíguo de um array subjacente e dá acesso a uma sequência numerada de elementos daquele array. (...) Um slice, uma vez inicializado, está sempre associado a um array subjacente que guarda seus elementos. Um slice, portanto, compartilha armazenamento com seu array e com outros slices do mesmo array; por outro lado, arrays distintos sempre representam armazenamentos distintos."

The Go Programming Language Specification, Slice types

Essa última frase é o artigo inteiro em uma linha. Guarde ela.

A diferença aparece na hora de medir:

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var arr [1000]int
    var sl []int

    fmt.Println(unsafe.Sizeof(arr)) // 8000
    fmt.Println(unsafe.Sizeof(sl))  // 24
}

O array ocupa os 8000 bytes que ele promete. O slice ocupa 24 bytes em qualquer plataforma de 64 bits, independentemente de descrever três elementos ou três milhões, porque ele é só um ponteiro e dois inteiros.

E isso não é curiosidade acadêmica: array é valor, então passar um array para uma função copia o array inteiro.

package arrbench

type Buffer [4096]byte

//go:noinline
func firstArray(b Buffer) byte { return b[0] } // copia 4 KiB para ler 1 byte

//go:noinline
func firstSlice(b []byte) byte { return b[0] } // copia 24 bytes de cabeçalho
BenchmarkArrayParam-8    23730294    48.16  ns/op    0 B/op    0 allocs/op
BenchmarkSliceParam-8   927475783     1.375 ns/op    0 B/op    0 allocs/op

Os dois corpos são idênticos. A diferença de 47 nanossegundos é a cópia dos 4 KiB que acontece antes de a função começar. Não há alocação em nenhum dos dois casos — o custo é puramente de cópia, e é por isso que arrays grandes quase nunca aparecem como parâmetro em código Go.

Em compensação, ser um valor autocontido dá ao array duas capacidades que o slice não tem:

a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
fmt.Println(a == b) // true

routes := map[[2]string]int{
    {"GET", "/users"}: 1, // array como chave de map
}

Arrays são comparáveis se o tipo dos elementos for comparável, e por isso servem como chave de map. Slices não:

invalid operation: a == b (slice can only be compared to nil)

O motivo é o mesmo compartilhamento de armazenamento citado na spec: dois slices podem apontar para o mesmo array, ou para arrays diferentes com o mesmo conteúdo, e não existe uma resposta óbvia para o que == deveria significar aí. Para comparar conteúdo existe slices.Equal.

A restrição é contagiosa. Uma struct só é comparável se todos os campos forem comparáveis, então basta um campo []string para o tipo inteiro deixar de servir como chave de map:
invalid operation: x == y (struct containing []string cannot be compared)

Resumindo a diferença:

Array [N]TSlice []T
Tamanhofixo, faz parte do tipovariável em tempo de execução
O que a variável guardaos elementosponteiro, len e cap
Custo de copiarproporcional ao tamanho24 bytes (64 bits)
Comparável com ==sim, se T for comparávelsó contra nil
Zero valuetodos os elementos zeradosnil

Parte 1: o cabeçalho de três palavras

Aqueles 24 bytes são três palavras: um ponteiro para o início do segmento, o comprimento e a capacidade. O blog oficial descreve o slice assim:

"Uma slice é uma estrutura de dados que descreve uma seção contígua de um array guardado separadamente da própria variável slice. Uma slice não é um array. Uma slice descreve um pedaço de um array."

Go Blog, Arrays, slices (and strings): The mechanics of 'append'

E aqui está o ponto que reorganiza o entendimento, e que é a continuação direta do artigo anterior: o cabeçalho também é um valor. Quando você passa um slice para uma função, o cabeçalho é copiado. Os dois cabeçalhos apontam para o mesmo array, mas são cabeçalhos independentes.

Isso explica um comportamento que parece contraditório:

package main

import "fmt"

func mutate(s []int) {
    s[0] = 99            // o chamador vê
    s = append(s, 4)     // o chamador NÃO vê
}

func main() {
    s := []int{1, 2, 3}
    mutate(s)
    fmt.Println(s, len(s)) // [99 2 3] 3
}

As duas linhas parecem mexer no mesmo slice, e só uma tem efeito lá fora. s[0] = 99 escreve no array, que é compartilhado. Já append devolve um cabeçalho novo, com len igual a 4, e atribui esse cabeçalho à cópia local. O main continua com o cabeçalho antigo, de len 3.

É exatamente por isso que append devolve um valor em vez de mutar o slice no lugar, e por que ignorar esse retorno não é um estilo alternativo, é um erro de compilação:

append(s, 2) (value of type []int) is not used
Vale reforçar a assimetria, porque ela é a origem de metade da confusão com slices: os elementos são compartilhados, o cabeçalho não é. Passar um slice adiante permite que a função altere seus dados, mas não permite que ela altere o seu len. Se você precisa disso, devolva o slice (o que é idiomático) ou receba um *[]T (o que raramente é).

Parte 2: len não é cap

O comprimento é quantos elementos o slice tem. A capacidade é quantos ele poderia ter sem trocar de array — é a distância entre o começo do slice e o fim do array subjacente.

A distinção parece burocrática até você escrever, quase sempre sem perceber, esta função:

// Errado: make com len, e não com cap.
func orderIDs(orders []Order) []int64 {
    out := make([]int64, len(orders))
    for _, o := range orders {
        out = append(out, o.ID)
    }
    return out
}

O make([]int64, len(orders)) cria um slice com len(orders) elementos já preenchidos com o zero value. Cada append acrescenta depois deles:

[0 0 0 10 20 30] len=6 cap=6

A intenção era reservar espaço; o que aconteceu foi criar os elementos. A forma correta é make([]int64, 0, len(orders)): comprimento zero, capacidade reservada.

O detalhe é que esse bug não é só de correção, ele é mais lento do que não ter otimizado nada. Coletando 1000 Order (32 bytes cada):

BenchmarkNoPrealloc-8       114913     8935 ns/op     70208 B/op    10 allocs/op
BenchmarkPrealloc-8         290004     3953 ns/op     32768 B/op     1 allocs/op
BenchmarkWrongPrealloc-8     77742    15990 ns/op    155648 B/op     3 allocs/op

A versão sem pré-alocação faz 10 alocações, porque o slice precisou crescer 10 vezes ao longo do caminho. A versão correta faz uma só, e é 2,3× mais rápida.

A versão com o make errado é a mais interessante das três. Ela termina com um slice de 2000 elementos — os 1000 zeros na frente e os 1000 valores reais depois — e, para chegar lá, alocou três vezes: os 1000 elementos iniciais, e mais duas realocações de crescimento (capacidade 1000 → 1536 → 2304). São os 155.648 B/op que o benchmark reporta, para produzir um resultado que, além de tudo, está errado. A tentativa de otimizar saiu quase 2× mais cara do que não ter feito nada.

go vet não pega esse caso. O linter makezero, disponível no golangci-lint, existe justamente para isso: ele acusa slices criados com comprimento diferente de zero que depois recebem append.

A capacidade também é um pedaço de array que ainda está lá

Uma consequência menos óbvia: os elementos entre len e cap não desapareceram, eles só não são acessíveis pelo slice atual. Refatiar até a capacidade os traz de volta:

s := []int{1, 2, 3, 4, 5}
t := s[:2]

fmt.Println(t)          // [1 2]
fmt.Println(t[:cap(t)]) // [1 2 3 4 5]

t tem len 2 e cap 5. Encurtar um slice não apaga nada; apenas move o len. É por isso que "cortar" dados sensíveis com s = s[:n] não é uma forma de descartá-los.

A expressão de três índices

Para limitar a capacidade existe a full slice expression, a[low:high:max]:

"Adicionalmente, ela controla a capacidade do slice resultante, definindo-a como max - low."

The Go Programming Language Specification, Full slice expressions

all := []int{1, 2, 3, 4, 5}

head := all[:2]     // len=2, cap=5
safe := all[:2:2]   // len=2, cap=2

A diferença entre esses dois é a diferença entre o bug da introdução e o código correto, e é para lá que vamos na Parte 4. Antes, precisamos entender o que o append faz quando a capacidade acaba.

Parte 3: como o slice cresce

A spec descreve o comportamento em uma frase, e note o que ela não promete:

"Se a capacidade de s não for grande o suficiente para caber os valores adicionais, append aloca um novo array subjacente, suficientemente grande, que caiba tanto os elementos existentes quanto os adicionais. Caso contrário, append reutiliza o array subjacente."

The Go Programming Language Specification, Appending to and copying slices

"Suficientemente grande" é tudo o que a linguagem garante. Quanto exatamente é decisão do runtime, e ela mudou ao longo das versões.

No Go 1.26, quem decide é nextslicecap, em runtime/slice.go:

func nextslicecap(newLen, oldCap int) int {
    newcap := oldCap
    doublecap := newcap + newcap
    if newLen > doublecap {
        return newLen
    }

    const threshold = 256
    if oldCap < threshold {
        return doublecap
    }
    for {
        // Transition from growing 2x for small slices
        // to growing 1.25x for large slices. This formula
        // gives a smooth-ish transition between the two.
        newcap += (newcap + 3*threshold) >> 2
        // ...
    }
}

São três regras, em ordem:

  1. Se o que você pediu já passa do dobro da capacidade atual, o runtime usa exatamente o que foi pedido. Adicionar 100 elementos a um slice de capacidade 4 não vai gerar capacidade 8.
  2. Abaixo de 256 elementos de capacidade, dobra.
  3. A partir de 256, aplica newcap += (newcap + 768) / 4, que começa em fator 2 e vai caindo suavemente em direção a 1,25.

O objetivo da terceira regra é evitar o degrau que existia antes: até o Go 1.17, o fator caía de 2 para 1,25 de uma vez em 1024 elementos.

O que você realmente observa

Só que nextslicecap não é a última palavra. O valor que ela devolve vira uma quantidade de bytes, e o alocador do Go arredonda essa quantidade para a size class mais próxima, porque ele não aloca blocos de tamanho arbitrário. A capacidade final é o quanto cabe no bloco arredondado.

Medindo o crescimento de um []int a partir de nil:

len=1     cap=1
len=2     cap=2
len=3     cap=4
len=5     cap=8
...
len=257   cap=512
len=513   cap=848      <- a fórmula diz 832
len=849   cap=1280
len=1281  cap=1792

Em cap=512, a fórmula dá 512 + (512 + 768)/4 = 832. Mas 832 elementos de 8 bytes são 6656 bytes, e a size class imediatamente acima é 6784 — que comporta 848 elementos. O runtime não joga fora os 16 elementos que sobraram no bloco, então cap vira 848.

Por isso a sequência depende do tipo do elemento, não só da quantidade. Crescendo um elemento por vez a partir de nil:

TipoTamanho do elementoCapacidades observadas
[]byte1 B8, 16, 32, 64, 128, 256, 512, 896, 1408, 2048
[]int8 B1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 848, 1280, 1792
[]Order32 B1, 2, 4, 8, 16, 35, 71, 151, 303, 575, 1023, 1536

Três coisas para reparar nessa tabela. []byte começa em 8, e não em 1, porque a menor size class é de 8 bytes — pedir 1 byte e receber 8 é o piso do alocador. []int segue as potências de dois até 512, onde a fórmula assume e o arredondamento começa a aparecer. E []Order sai de 16 direto para 35, um número que não é potência de dois nem múltiplo de nada óbvio: são 32 elementos pela fórmula, arredondados para cima pelo tamanho do bloco, e ainda descontados de um cabeçalho de 8 bytes que o alocador acrescenta porque Order contém uma string — elementos com ponteiros precisam desse metadado para o GC saber o que varrer.

A moral prática dessa seção não é decorar a tabela, é o contrário: cap depois de um append é um detalhe de implementação. Se o seu código depende de um valor específico de capacidade, ele vai quebrar em alguma versão futura do Go. O que você pode confiar é no comportamento: o crescimento é geométrico, então uma sequência de N append custa O(N) no total, e não O(N²).

Uma surpresa que vem do artigo anterior

Se você rodar o teste acima em uma função onde o slice não escapa, os números mudam:

//go:noinline
func firstCapLocal() int {
    var s []int
    s = append(s, 1)
    return cap(s) // 4
}

Quatro, e não um. E com []byte o primeiro append devolve capacidade 32, não 8.

Isso não é o growslice — é o escape analysis em ação. Desde o Go 1.25, quando o compilador prova que o slice não sobrevive à função, ele reserva um buffer fixo na stack e usa esse buffer como array inicial. O tamanho padrão é de 32 bytes, definido por VariableMakeThreshold em cmd/compile/internal/base/flag.go, e a capacidade resultante é o número de elementos que cabem nele:

K := maxStackSize / et.Size() // cmd/compile/internal/ssagen/ssa.go

32 / 8 = 4 para []int, 32 / 1 = 32 para []byte, 32 / 32 = 1 para []Order. Confere com o observado. Basta fazer o slice escapar — atribuindo a uma variável global, por exemplo — para as capacidades voltarem a 1, 8 e 1.

É o mesmo padrão que apareceu no artigo sobre escape analysis: a mesma linha de código produz resultados diferentes conforme o contexto da chamada. Aqui isso é ainda mais uma razão para não tratar cap como um número previsível.

Parte 4: aliasing, ou quando dois slices enxergam o mesmo array

Agora dá para voltar ao bug da introdução com as ferramentas certas.

func (r *Router) Group(m Middleware) *Router {
    return &Router{middlewares: append(r.middlewares, m)}
}

r.middlewares tem len=3 e cap=4. O append encontra espaço sobrando, então não aloca nada: ele escreve m na posição 3 do array existente e devolve um cabeçalho com len=4 apontando para o mesmo array.

Quando Group é chamado uma segunda vez a partir do mesmo pai, o cabeçalho de r.middlewares continua com len=3 — ele nunca mudou. O append escreve de novo na posição 3, por cima do middleware anterior. Os dois routers filhos terminam com cabeçalhos diferentes, mas apontando para o mesmo array, e o array só tem uma posição 3.

A correção é dizer ao append que ele não tem permissão para usar a capacidade sobrando:

func (r *Router) Group(m Middleware) *Router {
    base := r.middlewares[:len(r.middlewares):len(r.middlewares)]
    return &Router{middlewares: append(base, m)}
}

Com cap == len, o append é obrigado a alocar um array novo e copiar, e cada filho passa a ter o seu. A biblioteca padrão tem um nome para essa operação desde o Go 1.21:

// Clip removes unused capacity from the slice, returning s[:len(s):len(s)].
func Clip[S ~[]E, E any](s S) S

Então a versão que eu escreveria de verdade é:

func (r *Router) Group(m Middleware) *Router {
    return &Router{middlewares: append(slices.Clip(r.middlewares), m)}
}
admin : [logging recover requestID requireAdmin]
public: [logging recover requestID rateLimit]
O custo dessa correção é uma alocação por chamada, e ela é o ponto. slices.Clip transforma "às vezes compartilha, às vezes não" em "nunca compartilha". A regra que eu uso: se um slice vai ser guardado em uma struct de vida longa e derivado depois, ele precisa ser clipado ou clonado na fronteira. Pagar uma alocação previsível é melhor do que depender de um cap que ninguém está olhando.

O mesmo mecanismo, do lado de dentro

O aliasing não é só sobre append acidental. Ele também aparece em um padrão bastante recomendado — o filtro sem alocação:

// activeOnly reaproveita o array de origem para não alocar.
func activeOnly(orders []Order) []Order {
    out := orders[:0]
    for _, o := range orders {
        if o.Status != "cancelled" {
            out = append(out, o)
        }
    }
    return out
}

Ele funciona porque out sempre escreve em uma posição que já foi lida. E ele destrói a entrada:

active: [paid paid]
all   : [paid paid paid]  <- o chamador perdeu o pedido 2

Não é um bug do padrão, é o preço dele. O problema aparece quando essa função mora em um pacote de utilidades, com um nome como FilterActive, e quem chama não faz ideia. Se a função é pública, ou você documenta que ela consome a entrada, ou você não reaproveita o array:

func activeOnly(orders []Order) []Order {
    return slices.DeleteFunc(slices.Clone(orders), func(o Order) bool {
        return o.Status == "cancelled"
    })
}

Segurar um pedaço é segurar o array inteiro

A última forma de aliasing é a que não corrompe nada, só consome memória. Como o slice guarda um ponteiro para o array, o coletor de lixo não pode liberar o array enquanto qualquer slice apontar para ele — mesmo que esse slice descreva 16 bytes de um array de 10 MiB.

O blog oficial já alertava para isso:

"refatiar um slice não faz uma cópia do array subjacente. O array completo será mantido em memória até que ele não seja mais referenciado."

Go Blog, Go Slices: usage and internals

Um caso concreto: uma função que lê um payload e guarda só um identificador dele.

func extractID(payload []byte) []byte {
    return payload[:16] // mantém o payload inteiro vivo
}

func extractIDSafe(payload []byte) []byte {
    return slices.Clone(payload[:16]) // 16 bytes, e só
}

Guardando 20 desses identificadores, com payloads de 10 MiB:

20 fatias de 16 bytes (sub-slice):    heap = 200.2 MiB
20 fatias de 16 bytes (slices.Clone): heap =   0.2 MiB

Não há vazamento no sentido clássico — tudo será liberado quando os 20 slices morrerem. Mas enquanto eles viverem, o processo carrega 200 MiB para guardar 320 bytes de informação útil. É o tipo de coisa que aparece como "uso de memória alto e estável" e não como um leak crescente, o que torna o diagnóstico bem mais chato.

O mesmo vale para string, que também é um ponteiro mais um comprimento. Fatiar uma string enorme e guardar o pedaço mantém a original viva. A saída é strings.Clone, do Go 1.18.

Limitações, exceções e armadilhas

O append compartilha ou copia, e você não controla qual. Esta é a armadilha-mãe, e vale enunciá-la de forma explícita: depois de b := append(a, x), a e b podem ou não compartilhar o array, dependendo de cap(a). Não existe um jeito de perguntar "esse append alocou?" que seja útil em produção. Trate o resultado do append como um slice novo e o original como inválido, ou clipe antes.

Encurtar um slice não libera nada. s = s[:0] zera o len e mantém a capacidade — o que é ótimo para reaproveitar buffers e péssimo se os elementos forem ponteiros, porque o array continua referenciando os objetos. Se o elemento contém ponteiros e o slice tem vida longa, zere a cauda ou use as funções do pacote slices, que fazem isso desde o Go 1.22:

remoção manual com append: slot além do len = &{d}   <- ainda referenciado
slices.Delete:             slot além do len = <nil>  <- zerado

slices.Clone é uma cópia rasa. A documentação diz que "os elementos são copiados por atribuição". Clonar um []*Order te dá um array novo de ponteiros para os mesmos Order. Se o objetivo era isolamento de verdade, o clone raso não resolve.

nil e slice vazio não são a mesma coisa, exceto quando são.

var nilSlice []int
emptySlice := []int{}

nilSlice == nil   // true
emptySlice == nil // false
len(nilSlice) == len(emptySlice) // true
slices.Equal(nilSlice, emptySlice) // true

append, len, cap e range tratam os dois igual, então na maior parte do código a diferença não importa. Ela importa em dois lugares: comparações explícitas contra nil e serialização. O encoding/json gera null para um slice nil e [] para um vazio, e essa diferença já quebrou muito cliente. Se a sua API devolve listas, garanta o slice vazio.

range copia o elemento. Não é específico de slices, mas é onde mais aparece:

for _, o := range orders {
    o.Status = "paid" // altera a cópia, não o slice
}
for i := range orders {
    orders[i].Status = "paid" // altera o slice
}

Para elementos grandes isso também tem custo de cópia, e é um dos poucos casos em que for i := range é preferível por performance, não só por semântica.

Reservar capacidade demais é um custo, não um seguro. make([]T, 0, 1_000_000) aloca o array inteiro na hora, mesmo que você use dez posições. slices.Grow sofre do mesmo, e é por isso que ele é a ferramenta certa só quando você tem uma estimativa razoável. Quando não tem, deixe o append crescer: o algoritmo geométrico já é uma boa aposta.

Não otimize capacidade sem medir. Vale aqui o mesmo critério do artigo sobre escape analysis. allocs/op só importa em caminhos quentes, e a diferença entre 10 alocações e 1 em uma função chamada três vezes por requisição não paga a complexidade de calcular o tamanho certo. Comece pelo -benchmem e pelo pprof, não pelo código.

Mudanças entre versões

Boa parte do que está aqui é comportamento observável, não contrato, e mudou ao longo do tempo:

VersãoO que mudou
Go 1.18A fórmula de crescimento passou a ser suave: em vez de dobrar até 1024 e depois multiplicar por 1,25, o fator começa a cair em 256. As release notes dizem apenas que "a nova fórmula é menos propensa a transições bruscas no comportamento de alocação"; os detalhes estão no commit 2dda92f.
Go 1.21O pacote slices entrou na biblioteca padrão, com Clone, Clip, Grow, Delete, Equal, Insert e companhia. Antes disso, cada projeto tinha a sua versão de Clip.
Go 1.22Delete, DeleteFunc, Compact, CompactFunc e Replace passaram a zerar os elementos entre o novo e o antigo comprimento, resolvendo a retenção de ponteiros na cauda. Também chegou slices.Concat.
Go 1.25O compilador passou a alocar o array subjacente de slices na stack em mais situações ("Faster slices" nas release notes). É o que faz o primeiro append em um []int local devolver cap=4. Pode ser desligado com -gcflags=all=-d=variablemakehash=n.
Go 1.26A mesma otimização foi estendida a mais casos. Nada mudou na semântica; mudou o que você observa em cap.

Se você mantém código que nasceu antes do Go 1.21, vale uma busca por implementações caseiras de Clip e por remoções feitas com append(s[:i], s[i+1:]...). As primeiras podem ser trocadas por slices.Clip; as segundas provavelmente estão segurando ponteiros na cauda, e slices.Delete resolve.

Juntando tudo: o caminho mental

Se eu tivesse que resumir o raciocínio em uma sequência de perguntas:

  1. Eu preciso mesmo de um array? Quase sempre não. Array faz sentido quando o tamanho é fixo por natureza (um hash de 32 bytes, um par método/rota como chave de map) ou quando você quer comparabilidade. Fora isso, slice.
  2. Esse slice vai ser guardado em algum lugar que sobrevive à função? Se vai, decida na fronteira: slices.Clone se você precisa de dados próprios, slices.Clip se você só quer impedir que um append futuro pise em memória alheia.
  3. Eu sei quantos elementos vão entrar? Se sei, make([]T, 0, n) — com o zero no meio. Se não sei, deixe crescer.
  4. Essa função muta o slice que recebeu? Se muta, isso faz parte do contrato dela e precisa estar documentado. "Recebe []T e devolve []T" não diz se a entrada continua válida.
  5. Estou guardando um pedaço de algo grande? Se estou, clone. O array subjacente inteiro fica vivo enquanto qualquer fatia dele existir.

O que eu levaria deste artigo é que quase todo problema com slices vem de tratar o cabeçalho como se fosse o dado. Ele não é: ele é uma vista sobre um dado que mora em outro lugar e pode ser visto por mais gente. len e cap diferentes não são um detalhe de eficiência, são a informação de que existe memória ali que o seu slice não está mostrando — e que o próximo append pode usar.

Nos zero values o ganho era não precisar lembrar de inicializar. No escape analysis era não precisar decidir onde as coisas moram. Com slices o acordo é diferente: Go te dá uma abstração barata sobre memória compartilhada e, em troca, espera que você saiba que ela é compartilhada. É a única das três em que a linguagem não decide por você.

Referências

Copyright © 2026 Jairo Blatt