O que é Make em Go?
A função make é uma das funções built-in mais importantes de Go. Ela é responsável por inicializar e alocar memória para três tipos de referência da linguagem: slices, maps e channels. Diferente de new, que apenas aloca memória zerada e retorna um ponteiro, make inicializa a estrutura interna do tipo e retorna o valor pronto para uso — não um ponteiro.
A razão pela qual esses três tipos precisam de make é que todos possuem estruturas internas complexas que precisam ser inicializadas antes do uso. Um slice tem um array subjacente, um comprimento e uma capacidade. Um map tem uma hash table interna com buckets. Um channel tem um buffer circular e primitivas de sincronização. Simplesmente alocar memória zerada para essas estruturas resultaria em valores inutilizáveis — é por isso que make existe.
Entender quando e como usar make corretamente é fundamental para escrever código Go performático. A pré-alocação adequada de capacidade pode eliminar realocações desnecessárias e reduzir significativamente a pressão sobre o garbage collector.
Sintaxe do make
// Sintaxe geral
make(T, args...)
// Para slices
make([]T, length) // length == capacity
make([]T, length, capacity)
// Para maps
make(map[K]V) // map vazio
make(map[K]V, hint) // map com hint de capacidade
// Para channels
make(chan T) // channel sem buffer
make(chan T, buffer) // channel com buffer
O primeiro argumento é sempre o tipo. Os argumentos subsequentes dependem do tipo sendo criado e controlam tamanho, capacidade ou buffer.
Make para slices
A forma mais comum de usar make é criar slices com tamanho e capacidade pré-definidos:
// Slice com length 5, capacity 5
s1 := make([]int, 5)
fmt.Println(len(s1), cap(s1)) // 5, 5
fmt.Println(s1) // [0 0 0 0 0]
// Slice com length 0, capacity 10
s2 := make([]int, 0, 10)
fmt.Println(len(s2), cap(s2)) // 0, 10
fmt.Println(s2) // []
Quando pré-alocar capacidade
Pré-alocar capacidade é uma das otimizações mais impactantes em Go. Cada vez que um append excede a capacidade, Go aloca um novo array subjacente (geralmente 2x maior), copia todos os elementos e descarta o antigo:
// SEM pré-alocação: múltiplas realocações
func semPreAlocacao(n int) []int {
var resultado []int // capacity = 0
for i := 0; i < n; i++ {
resultado = append(resultado, i) // realloc em 1, 2, 4, 8, 16...
}
return resultado
}
// COM pré-alocação: zero realocações
func comPreAlocacao(n int) []int {
resultado := make([]int, 0, n) // capacity = n
for i := 0; i < n; i++ {
resultado = append(resultado, i) // nunca realloca
}
return resultado
}
Em benchmarks, a versão com pré-alocação pode ser 3-5x mais rápida para slices grandes, com significativamente menos alocações de memória e menor pressão no garbage collector.
Padrão make com length vs capacity
// Padrão 1: make([]T, n) — quando você sabe os valores antecipadamente
indices := make([]int, 10)
for i := range indices {
indices[i] = i * 2 // atribui diretamente por índice
}
// Padrão 2: make([]T, 0, n) — quando vai usar append
resultados := make([]string, 0, len(dados))
for _, d := range dados {
if d.Ativo {
resultados = append(resultados, d.Nome)
}
}
Use make([]T, n) quando sabe exatamente quantos elementos terá e vai preencher por índice. Use make([]T, 0, n) quando vai usar append e sabe (ou pode estimar) a capacidade máxima.
Make para maps
Maps em Go precisam ser inicializados antes do uso. Atribuir a um map nil causa panic:
// PANIC: assignment to entry in nil map
var m map[string]int
m["chave"] = 1 // panic!
// CORRETO: inicializar com make
m := make(map[string]int)
m["chave"] = 1 // OK
Hint de capacidade
O segundo argumento de make para maps é um hint de capacidade inicial — não um limite rígido:
// Sem hint: começa com capacidade padrão do runtime
usuarios := make(map[int]string)
// Com hint: pré-aloca buckets para ~100 entradas
usuarios := make(map[int]string, 100)
O hint ajuda o runtime a dimensionar a hash table interna, evitando rehashing conforme o map cresce. Use quando souber antecipadamente quantos elementos o map terá:
// Convertendo slice para map — capacidade exata conhecida
func sliceParaMap(pessoas []Pessoa) map[int]Pessoa {
resultado := make(map[int]Pessoa, len(pessoas))
for _, p := range pessoas {
resultado[p.ID] = p
}
return resultado
}
Para maps que crescem gradualmente ao longo do tempo, o hint é menos crítico. Mas para processamento em lote onde você carrega milhares de entradas de uma vez, o hint pode fazer diferença significativa no desempenho.
Make para channels
Channels são criados com make, e o segundo argumento opcional define o tamanho do buffer:
// Channel sem buffer (síncrono) — bloqueia até receiver estar pronto
ch := make(chan string)
// Channel com buffer de 10 — pode enviar até 10 sem bloquear
buffered := make(chan string, 10)
// Channel direcional (definido no tipo, não no make)
var enviar chan<- int = make(chan int)
var receber <-chan int = make(chan int)
Escolhendo o tamanho do buffer
// Sem buffer: sincronização forte entre goroutines
sync := make(chan struct{})
// Buffer pequeno (1-10): desacoplar ligeiramente producer/consumer
trabalho := make(chan Tarefa, 5)
// Buffer grande: quando producer é mais rápido que consumer
logs := make(chan LogEntry, 1000)
A escolha do buffer afeta diretamente o comportamento de concorrência. Channels sem buffer garantem sincronização ponto-a-ponto, enquanto buffered channels permitem que goroutines avancem de forma mais independente.
Make vs New
Essa é uma dúvida frequente entre desenvolvedores Go. As duas funções têm propósitos distintos:
| Aspecto | make | new |
|---|---|---|
| Tipos suportados | slice, map, channel | Qualquer tipo |
| Retorna | Valor inicializado | Ponteiro para valor zerado |
| Inicializa | Sim (estrutura interna) | Não (apenas zera memória) |
| Uso principal | Criar coleções prontas | Alocar memória para tipos simples |
// make: retorna []int inicializado
s := make([]int, 5) // []int com 5 zeros
// new: retorna *[]int apontando para nil slice
p := new([]int) // *[]int → &[]int(nil)
Na prática, make é muito mais usado que new no código Go idiomático. Veja mais detalhes na entrada sobre new.
Make vs sintaxe literal
Go também permite criar slices e maps com sintaxe literal. Quando usar cada um?
// Literal: quando você tem os valores já definidos
cores := []string{"vermelho", "verde", "azul"}
config := map[string]int{"timeout": 30, "retries": 3}
// Make: quando vai preencher dinamicamente
resultados := make([]string, 0, 100)
cache := make(map[string][]byte, 50)
Use literais quando tem valores conhecidos em tempo de compilação. Use make quando precisa controlar capacidade ou vai preencher a coleção dinamicamente. Ambas as formas são idiomáticas — a escolha depende do contexto.
Padrões comuns com make
Pool de workers com channel
func workerPool(numWorkers int, tarefas []Tarefa) []Resultado {
jobs := make(chan Tarefa, len(tarefas))
resultados := make(chan Resultado, len(tarefas))
// Iniciar workers
for w := 0; w < numWorkers; w++ {
go func() {
for tarefa := range jobs {
resultados <- processar(tarefa)
}
}()
}
// Enviar tarefas
for _, t := range tarefas {
jobs <- t
}
close(jobs)
// Coletar resultados
output := make([]Resultado, 0, len(tarefas))
for i := 0; i < len(tarefas); i++ {
output = append(output, <-resultados)
}
return output
}
Construir map de lookup
func construirIndice(docs []Documento) map[string][]int {
indice := make(map[string][]int, len(docs)*10) // estimativa generosa
for i, doc := range docs {
for _, palavra := range strings.Fields(doc.Conteudo) {
indice[palavra] = append(indice[palavra], i)
}
}
return indice
}
Slice como buffer reutilizável
func processarLotes(dados []byte, tamLote int) {
buffer := make([]byte, tamLote)
for offset := 0; offset < len(dados); offset += tamLote {
n := copy(buffer, dados[offset:])
processar(buffer[:n])
}
}
Performance e benchmarks
Demonstrando o impacto de make com pré-alocação:
func BenchmarkSemMake(b *testing.B) {
for i := 0; i < b.N; i++ {
var s []int
for j := 0; j < 10000; j++ {
s = append(s, j)
}
}
}
func BenchmarkComMake(b *testing.B) {
for i := 0; i < b.N; i++ {
s := make([]int, 0, 10000)
for j := 0; j < 10000; j++ {
s = append(s, j)
}
}
}
Resultado típico: BenchmarkComMake executa ~3x mais rápido com 1 alocação vs ~14 alocações na versão sem make. Use profiling e go test -bench -benchmem para medir em seus próprios cenários.
Boas práticas com make
- Sempre pré-aloque quando souber o tamanho — evita realocações e reduz GC
- Use hint em maps grandes — previne rehashing custoso
- Nunca use map sem inicializar — causará panic em runtime
- Prefira
make([]T, 0, n)+ append sobremake([]T, n)+ index quando o tamanho final é incerto - Dimensione buffers de channel com base no throughput — nem muito pequeno (gargalo) nem muito grande (desperdício)
- Documente a razão da capacidade com comentários quando o valor não for óbvio
Para se aprofundar em otimização de alocações, explore PGO (Profile-Guided Optimization) e como o garbage collector Green Tea lida com alocações em Go 1.25+.
Perguntas frequentes (FAQ)
Qual a diferença entre make e new em Go?
make inicializa e retorna o valor pronto para uso de slices, maps e channels. new aloca memória zerada para qualquer tipo e retorna um ponteiro. Na prática, use make para coleções e channels, e new raramente — prefira &T{} para criar ponteiros para structs.
O que acontece se eu não usar make para criar um map?
Um map declarado sem inicialização (var m map[string]int) tem valor nil. Leitura de um nil map retorna o zero value normalmente, mas qualquer tentativa de escrita causa panic: assignment to entry in nil map. Sempre use make ou sintaxe literal para inicializar maps antes de escrever neles.
Quando devo pré-alocar capacidade com make?
Sempre que souber ou puder estimar razoavelmente o número de elementos. Para slices, isso evita múltiplas realocações e cópias do array interno. Para maps, evita rehashing. A regra prática: se está processando N itens e o resultado terá no máximo N elementos, use make([]T, 0, N) ou make(map[K]V, N).
Make pode criar structs ou tipos primitivos?
Não. make funciona exclusivamente com slices, maps e channels — os três tipos de referência que precisam de inicialização interna. Para structs e tipos primitivos, use declaração direta (var x T), literal (T{campo: valor}) ou new (new(T)). Tentar usar make com outro tipo resulta em erro de compilação.