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:

Aspectomakenew
Tipos suportadosslice, map, channelQualquer tipo
RetornaValor inicializadoPonteiro para valor zerado
InicializaSim (estrutura interna)Não (apenas zera memória)
Uso principalCriar coleções prontasAlocar 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

  1. Sempre pré-aloque quando souber o tamanho — evita realocações e reduz GC
  2. Use hint em maps grandes — previne rehashing custoso
  3. Nunca use map sem inicializar — causará panic em runtime
  4. Prefira make([]T, 0, n) + append sobre make([]T, n) + index quando o tamanho final é incerto
  5. Dimensione buffers de channel com base no throughput — nem muito pequeno (gargalo) nem muito grande (desperdício)
  6. 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.