Removendo chave de map: como funciona internamente no Go
\n\nVocê já se perguntou o que realmente acontece quando você escreve delete(myMap, "key") em Go? A chave simplesmente desaparece da memória? Ou fica um "buraco"? E se você tentar remover uma chave de um nil map? Se você já se fez essas perguntas, não está sozinho. O tópico remover chave de map já gerou milhares de visualizações no Stack Overflow, e hoje vamos explorar os bastidores dessa operação aparentemente simples.
Neste artigo otimizado para SEO, vamos analisar como a hash table funciona em Go (estrutura hmap), como a remoção de chave é realizada, para onde vai a memória e por que você não deve ter medo de usar delete em loops. Vamos nessa!
Uma visão geral: como o map funciona no Go
\n\nAntes de falarmos sobre remoção, vamos lembrar como o map em Go armazena dados. Internamente, é uma hash table, representada pela estrutura hmap. Ela consiste em um array de "baldes" (buckets), cada um capaz de armazenar até 8 pares chave-valor. Quando você adiciona uma chave, o Go calcula o hash, encontra o balde e insere o par. Se o balde transborda, uma cadeia de baldes de estouro (overflow bucket) é criada.
Essa é uma implementação clássica, mas com alguns truques que discutiremos a seguir.
\n\nEstrutura hmap (simplificada)
\n\ntype hmap struct {\n count int // número de elementos\n flags uint8\n B uint8 // logaritmo da quantidade de baldes\n buckets unsafe.Pointer // ponteiro para o array de baldes\n oldbuckets unsafe.Pointer // durante o crescimento do map\n nevacuate uintptr\n // ...\n}\n\nO ponto chave: buckets é um array contínuo. A remoção não compacta esse array, apenas marca a célula como "vazia".
Removendo chave de map: como funciona no nível do código
\n\nA chamada delete(m, k) em Go é uma função embutida que é compilada para uma chamada de runtime. Internamente, acontece mais ou menos o seguinte:
- \n
- Cálculo do hash da chave — o Go usa uma função de hash específica para o tipo da chave. \n
- Localização do balde — os bits menos significativos do hash determinam o número do balde. \n
- Busca linear dentro do balde (e das cadeias de estouro) — encontra a célula com a chave desejada. \n
- Limpeza da memória — o Go zera a memória da chave e do valor para que o garbage collector possa coletá-los. \n
- Marcação da célula como "vazia" — usando flags especiais como
emptyRestouemptyOne. \n - Decremento do contador —
m.count--. \n
Tudo isso ocorre em O(1) na média, mas considerando colisões, pode chegar a O(n) no pior caso.
\n\nExemplo de código: delete em ação
\n\npackage main\n\nimport "fmt"\n\nfunc main() {\n m := map[string]int{\n "apple": 5,\n "banana": 3,\n "cherry": 7,\n }\n\n fmt.Println("Antes da remoção:", m)\n delete(m, "banana")\n fmt.Println("Após a remoção:", m)\n\n // Tentando remover uma chave inexistente\n delete(m, "grape") // nada acontece, sem erro\n fmt.Println("Após remover chave inexistente:", m)\n}\n\nObserve: remover chave de map para uma chave inexistente é uma operação segura. Ela simplesmente não faz nada. Esse é um dos equívocos comuns entre iniciantes.
\n\nO que acontece com a memória? Mitos e realidade
\n\nMuitos desenvolvedores temem que após um delete o map fique com "buracos", causando vazamentos de memória. Vamos esclarecer isso.
Memória para chaves e valores
\n\nQuando você remove uma chave, o Go zera a memória daquele elemento. Se a chave ou o valor forem ponteiros ou contiverem ponteiros (strings, slices, maps), o garbage collector poderá coletá-los. A célula em si no balde permanece, mas é marcada como vazia.
\n\nIsso significa que o map não diminui de tamanho após a remoção. O array de baldes continua do mesmo tamanho. No entanto, a memória ocupada pelos dados antigos é liberada para o GC.