O Go tem duas formas de criar uma variável, var e make, e escolher a errada provoca muitos bugs de principiante. O caso clássico é um map que provoca panic na primeira escrita. Logo a seguir vem um channel que bloqueia para sempre. Este artigo mostra quando usar cada um e porque é que slices, maps e channels são os casos especiais. Todos os exemplos são executáveis.
Índice
- Qual é a diferença entre var e make em Go?
- Quando deves usar var?
- Quando deves usar make?
- Porque é que escrever num map nil provoca panic, mas fazer append a um slice nil funciona?
- E o new? (var vs make vs new)
- Uma regra rápida para decidir
- Perguntas frequentes
- Fontes
Qual é a diferença entre var e make em Go?
var declara uma variável e atribui-lhe o valor zero do tipo. Funciona com todos os tipos da linguagem. make só aceita um slice, map ou channel, os três tipos integrados que precisam de preparação em runtime, e devolve um valor que pode guardar dados de imediato. O valor zero desses três é nil, por isso var fica sem a preparação que make faz.
Em concreto, make aloca a tabela de hash por trás de um map, prepara as filas de envio e receção de um channel e dá a um slice um array subjacente. Se declarares qualquer um dos três com var, ficas com nil.
O mesmo map, declarado das duas formas:
example.gogopackage main import "fmt" func main() { var counts map[string]int // nil map, no hash table yet made := make(map[string]int) // initialized, ready to write fmt.Println(counts == nil) // true fmt.Println(made == nil) // false made["/health"] = 1 // fine, made is initialized fmt.Println(made["/health"]) // 1 // counts["/health"] = 1 // panic: assignment to entry in nil map }
Muita gente espera que uma alocação devolva um ponteiro. make não o faz. Aloca e depois dá-te o valor diretamente.
Assim, make([]int, 3) devolve um []int simples, não um *[]int, e usa-lo sem desreferenciar nada. make(map[string]int) devolve um map onde podes escrever de imediato. A função integrada que devolve mesmo um ponteiro é new, que vemos mais abaixo.
var x T | make(T, ...) | |
|---|---|---|
| Funciona com | qualquer tipo | apenas slices, maps e channels |
| Dá-te | o valor zero | um valor inicializado e utilizável |
| Resultado para slice | slice nil (len 0, cap 0) | slice não nil com array subjacente |
| Resultado para map | map nil (escrever provoca panic) | map onde se pode escrever |
| Resultado para channel | channel nil (bloqueia para sempre) | channel utilizável |
| Devolve | o valor | o valor, nunca um ponteiro |
Quando deves usar var?
Usa var para qualquer tipo cujo valor zero já é útil: números, booleanos, strings, structs, ponteiros e interfaces. O Go foi desenhado para que o valor zero seja um estado inicial válido, o que significa que raramente tens de inicializar alguma coisa primeiro.
bytes.Buffer é um bom exemplo. Não tem construtor. Declara-lo com var e começas a escrever:
example.gogopackage main import ( "bytes" "fmt" ) func main() { var buf bytes.Buffer // zero value is ready to use buf.WriteString("status=") buf.WriteString("ok") fmt.Println(buf.String()) // status=ok }
sync.Mutex funciona da mesma maneira. O seu valor zero é um mutex desbloqueado, e não existe nenhum construtor NewMutex porque não precisas de um.
var também serve para um slice, desde que só o faças crescer com append. Um slice nil é um slice vazio válido, com comprimento 0 e capacidade 0, e append aloca um array subjacente quando adicionas o primeiro elemento. Mas não lhe podes aceder por índice. ids[0] = 3 provoca um panic por índice fora dos limites, porque o comprimento continua a ser 0. Se precisas de atribuir por índice, usa make([]int, n) para que o slice tenha um comprimento real desde o início. Para acumular elementos com append, um slice nil serve bem:
example.gogopackage main import "fmt" func main() { var ids []int // nil slice, but append-ready for i := 1; i <= 3; i++ { ids = append(ids, i*10) } fmt.Println(ids) // [10 20 30] fmt.Println(ids == nil) // false, append allocated it }
Escrever ids := make([]int, 0) aqui não te traz nada. O slice nil é mais curto e comporta-se da mesma forma assim que o primeiro append é executado. Se quiseres praticar isto, o curso Composite Types tem um conjunto completo de exercícios sobre o comportamento de slices e maps.
Quando deves usar make?
Usa make quando um slice, map ou channel tem de guardar dados antes de se fazer qualquer append. Na prática, isso significa um map onde vais escrever chaves, um channel para onde goroutines vão enviar valores ou um slice que queres pré-dimensionado, para que o array subjacente exista logo à partida.
Os maps são o caso mais frequente, porque não há nada equivalente a append que aloque um por ti. Se vais atribuir qualquer chave, tens de criar o map primeiro com make. Contar pedidos por caminho num handler HTTP é um caso típico:
example.gogopackage main import "fmt" func main() { hits := make(map[string]int) // must initialize before writing for _, path := range []string{"/api", "/api", "/health"} { hits[path]++ // safe: map is initialized } fmt.Println(hits["/api"]) // 2 fmt.Println(hits["/health"]) // 1 }
Com os channels é parecido. Um channel para onde vais enviar valores tem de ser criado com make, porque um channel nil bloqueia para sempre (mais sobre isso abaixo). Um fan-out de workers começa com um channel criado com make:
example.gogopackage main import "fmt" func main() { jobs := make(chan int, 3) // buffered channel, capacity 3 jobs <- 1 jobs <- 2 close(jobs) for j := range jobs { fmt.Println("processing job", j) } }
Para slices, make tem duas formas. make([]T, n) dá-te um slice de comprimento n preenchido com valores zero, onde acedes diretamente por índice. make([]T, 0, n) dá-te um slice vazio com espaço para n elementos, onde fazes append sem realocar. Usa a primeira quando vais preencher todas as posições por índice, como ao ler um número fixo de bytes:
example.gogopackage main import "fmt" func main() { // length 4, index into it directly buf := make([]byte, 4) copy(buf, []byte("data")) fmt.Println(string(buf)) // data // length 0, capacity 8, append without regrowing sums := make([]int, 0, 8) for i := 0; i < 8; i++ { sums = append(sums, i*i) } fmt.Println(sums) // [0 1 4 9 16 25 36 49] }
A segunda forma tem a ver com desempenho. À medida que um slice cresce, o append vai-o copiando para arrays maiores. Se sabes mais ou menos quantos elementos vêm aí, definir a capacidade logo à partida poupa essas cópias. O comportamento do programa não muda, só a velocidade.
Porque é que escrever num map nil provoca panic, mas fazer append a um slice nil funciona?
Os dois foram desenhados de forma diferente. O append sabe lidar com um slice nil. Na primeira chamada aloca um array subjacente e devolve um slice real, por isso escrever através de append funciona sempre. Os maps não têm nada parecido. Uma escrita vai diretamente para uma tabela de hash, um map nil não tem nenhuma, e o runtime provoca panic em vez de tentar adivinhar o que querias.
A maioria das pessoas depara-se com isto na primeira semana com Go. Eis o que faz cada tipo nil, a começar pelo slice nil, que podes ler e ao qual podes fazer append sem problemas:
example.gogopackage main import "fmt" func main() { var s []int // nil slice fmt.Println(len(s), cap(s)) // 0 0 fmt.Println(s == nil) // true s = append(s, 42) // works: append allocates fmt.Println(s) // [42] }
Um map nil é diferente. Ler dele devolve o valor zero do tipo de valor e percorrê-lo com range não faz nada, mas a primeira escrita provoca panic:
example.gogopackage main import "fmt" func main() { var m map[string]int // nil map fmt.Println(m["missing"]) // 0, reading nil map is safe fmt.Println(len(m)) // 0, also safe m["key"] = 1 // panic: assignment to entry in nil map fmt.Println(m) }
Essa divisão entre leituras seguras e escritas com panic é o que torna o bug fácil de deixar passar. Um map nil passa em todos os testes que só leem dele e depois provoca panic na primeira vez que chegam dados reais. A correção não muda: inicializa-o com make antes da primeira escrita ou aceita um map que quem chama já criou.
Os channels nil têm o comportamento mais estranho dos três. Não provocam panic. Um envio ou uma receção num channel nil simplesmente nunca avança, o que numa única goroutine é um deadlock:
example.gogopackage main func main() { var ch chan int // nil channel ch <- 1 // blocks forever: fatal deadlock }
Parece uma armadilha, mas é útil dentro de um select. Um channel nil nunca está pronto, por isso pôr uma variável de channel a nil desliga o respetivo case, e assim podes desativar um ramo em tempo de execução. Cuidado com o close, porém: fechar um channel nil provoca panic, por isso fecha apenas channels que criaste. O curso Concurrency Fundamentals aborda a fundo o padrão de select com channels nil. O par map nil e slice nil também está na nossa lista de erros comuns em Go a evitar, já que também apanha programadores experientes.
E o new? (var vs make vs new)
new é a terceira função integrada de alocação e tem um papel diferente do de make. new(T) aloca memória com valor zero para um T e devolve um *T. make só trata de slices, maps e channels e devolve o valor. new funciona com qualquer tipo e devolve sempre um ponteiro.
A razão habitual para o usar é obter um ponteiro para um valor zero novo sem declarar primeiro uma variável com nome:
example.gogopackage main import "fmt" func main() { p := new(int) // *int pointing at a zeroed int fmt.Println(*p) // 0 *p = 7 fmt.Println(*p) // 7 }
Desde o Go 1.26, new também aceita uma expressão. new(expr) aloca espaço, guarda lá o valor da expressão e devolve um ponteiro para ele. Isso torna dispensáveis as antigas funções auxiliares ao estilo de intPtr, que só existiam para obter um ponteiro para um literal:
example.gogopackage main import "fmt" func main() { port := new(8080) // *int pointing at 8080 (Go 1.26) enabled := new(true) // *bool pointing at true fmt.Println(*port) // 8080 fmt.Println(*enabled) // true }
Isto é útil para campos opcionais de structs, onde um ponteiro nil significa «não definido» e um ponteiro não nil significa que alguém escolheu um valor. O curso Pointers & Memory tem um exercício de valores por omissão de configuração construído sobre este padrão. As três funções integradas lado a lado:
| Função integrada | Funciona com | Devolve | O resultado é utilizável? |
|---|---|---|---|
var x T | qualquer tipo | o valor zero | utilizável se o valor zero o for (não para escritas em maps) |
make(T, ...) | slice, map, channel | o valor T | sim, inicializado |
new(T) | qualquer tipo | *T, um ponteiro | aponta para um T com valor zero |
new(expr) (Go 1.26) | qualquer expressão | ponteiro para esse valor | aponta para o valor indicado |
Para um slice, map ou channel que estás prestes a preencher, quase sempre queres make. Usa new quando precisas especificamente de um ponteiro, na maioria das vezes um ponteiro para um literal num campo opcional.
Uma regra rápida para decidir
Se não tens a certeza de qual usar, percorre esta lista e para na primeira linha que se aplica:
- Um map onde vais escrever chaves? Usa
make(map[K]V). Um map nil provoca panic na primeira escrita. - Um channel para onde goroutines vão enviar valores? Usa
make(chan T)oumake(chan T, n). Um channel nil bloqueia para sempre. - Um slice que queres pré-dimensionado, por velocidade ou para atribuir por índice? Usa
make([]T, n)oumake([]T, 0, n). - Um slice que só vais fazer crescer com
append? Usavar s []T. Um slice nil funciona comappend. - Um ponteiro para um valor novo? Usa
new(T), ounew(expr)no Go 1.26 para apontar para um literal. - Qualquer outra coisa (int, string, bool, struct, a maioria dos tipos da biblioteca padrão)? Usa
var, porque o valor zero está pronto a usar.
Se só te lembrares de uma coisa, que seja esta: antes de escreveres uma chave num map, verifica de onde veio o map. Um map recebido como parâmetro foi, presumivelmente, inicializado por quem chama. Um map que declaraste tu com var é nil, e a primeira escrita vai provocar panic.
Perguntas frequentes
Posso usar var para um map?
Podes declarar um map com var m map[string]int. Ler dele, percorrê-lo com range e chamar len sobre ele são operações seguras. Escrever uma chave não é: m["x"] = 1 provoca panic com assignment to entry in nil map. Se só lês, var serve. Se escreves qualquer chave, inicializa o map primeiro com make ou recebe um que quem chama já criou.
make([]int, 0) é o mesmo que var s []int?
Ambos têm comprimento 0 e comportam-se da mesma forma depois de fazeres append. A diferença aparece quando comparas com nil. var s []int é nil, enquanto make([]int, 0) é um slice vazio não nil. Isso importa se o teu código ou um teste verifica s == nil, ou se codificas para JSON, onde nil passa a null e o slice vazio passa a []. Para um simples append, prefere var.
O make funciona com structs?
Não. make só aceita slices, maps e channels, por isso make(MyStruct) dá erro de compilação. Uma struct não precisa da preparação em runtime que make faz. Cria-a com um literal composto como MyStruct{}, declara-a com var s MyStruct para obter o valor zero ou usa new(MyStruct) quando queres um *MyStruct.
make vs new: qual usar para um ponteiro?
Usa new quando queres um ponteiro. new(T) devolve um *T que aponta para um valor zero e, no Go 1.26, new(expr) devolve um ponteiro para o valor dessa expressão. make nunca devolve um ponteiro. Devolve um slice, map ou channel inicializado. Os dois não se sobrepõem: make dá-te uma coleção utilizável e new dá-te um ponteiro.
Fontes
Referências primárias citadas neste artigo (última verificação a 3 de julho de 2026):
- The Go Programming Language Specification: Making slices, maps and channels, consultado a 03/07/2026
- The Go Programming Language Specification: Allocation (new), consultado a 03/07/2026
- The Go Programming Language Specification: The zero value, consultado a 03/07/2026
- Effective Go: Allocation with make and new, consultado a 03/07/2026
- Notas de lançamento do Go 1.26: alterações à linguagem, consultado a 03/07/2026
