Voltar ao blog

var vs make em Go: quando usar cada um (com exemplos)

O var dá-te o valor zero e o make inicializa slices, maps e channels. Quando usar cada um em Go e porque é que escrever num map nil provoca panic, enquanto fazer append a um slice nil não.

var vs make em Go: quando usar cada um (com exemplos)

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?

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.gogo
package 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 Tmake(T, ...)
Funciona comqualquer tipoapenas slices, maps e channels
Dá-teo valor zeroum valor inicializado e utilizável
Resultado para sliceslice nil (len 0, cap 0)slice não nil com array subjacente
Resultado para mapmap nil (escrever provoca panic)map onde se pode escrever
Resultado para channelchannel nil (bloqueia para sempre)channel utilizável
Devolveo valoro 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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 integradaFunciona comDevolveO resultado é utilizável?
var x Tqualquer tipoo valor zeroutilizável se o valor zero o for (não para escritas em maps)
make(T, ...)slice, map, channelo valor Tsim, inicializado
new(T)qualquer tipo*T, um ponteiroaponta para um T com valor zero
new(expr) (Go 1.26)qualquer expressãoponteiro para esse valoraponta 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:

  1. Um map onde vais escrever chaves? Usa make(map[K]V). Um map nil provoca panic na primeira escrita.
  2. Um channel para onde goroutines vão enviar valores? Usa make(chan T) ou make(chan T, n). Um channel nil bloqueia para sempre.
  3. Um slice que queres pré-dimensionado, por velocidade ou para atribuir por índice? Usa make([]T, n) ou make([]T, 0, n).
  4. Um slice que só vais fazer crescer com append? Usa var s []T. Um slice nil funciona com append.
  5. Um ponteiro para um valor novo? Usa new(T), ou new(expr) no Go 1.26 para apontar para um literal.
  6. 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):

Escreve Go como um engenheiro sénior

Lições interativas no navegador. As primeiras são grátis.

Experimenta uma lição grátisOu cria uma conta gratuita