O que é Nil em Go?
Nil é o valor zero para tipos de referência em Go. Ele representa a ausência de valor — semelhante a null em Java/C# ou None em Python, mas com características únicas na linguagem Go. Em Go, nil é o zero value para seis tipos específicos: ponteiros, slices, maps, channels, funções e interfaces.
Diferente de muitas linguagens onde null é uma armadilha constante (o famoso “billion dollar mistake” de Tony Hoare), Go aborda nil de forma mais segura e previsível. Muitas operações com valores nil são válidas — ler de um nil map retorna zero value, len() de um nil slice retorna 0, e você pode até chamar métodos em nil receivers. Essa abordagem permite escrever código mais robusto sem verificações nil obsessivas em todo lugar.
Porém, nil ainda pode causar panics se usado incorretamente. Entender exatamente quando cada tipo se comporta de forma segura ou perigosa com nil é conhecimento essencial para qualquer desenvolvedor Go. Este guia cobre todos os cenários com exemplos práticos.
Nil para cada tipo
Ponteiros nil
Um ponteiro nil não aponta para nenhum endereço de memória válido. Desreferenciar um ponteiro nil causa panic:
var p *int // nil
fmt.Println(p) // <nil>
fmt.Println(*p) // PANIC: runtime error: invalid memory address
// Verificação segura
if p != nil {
fmt.Println(*p)
}
// Inicializar quando necessário
p = new(int)
*p = 42 // OK
Slices nil
Um slice nil é diferente de um slice vazio, mas muitas operações funcionam em ambos:
var s []int // nil slice
// Operações SEGURAS em nil slice
fmt.Println(len(s)) // 0
fmt.Println(cap(s)) // 0
fmt.Println(s == nil) // true
// append funciona em nil slice!
s = append(s, 1, 2, 3)
fmt.Println(s) // [1 2 3]
// range sobre nil slice — zero iterações
for _, v := range s {
fmt.Println(v) // nunca executa se s for nil
}
Nil slice vs slice vazio
var nilSlice []int // nil
emptySlice := []int{} // não nil, mas vazio
makeSlice := make([]int, 0) // não nil, mas vazio
fmt.Println(nilSlice == nil) // true
fmt.Println(emptySlice == nil) // false
fmt.Println(makeSlice == nil) // false
// Porém, comportamento funcional é idêntico:
fmt.Println(len(nilSlice)) // 0
fmt.Println(len(emptySlice)) // 0
Convenção Go: retorne nil slice (não []int{}) quando não há dados. Isso evita alocação desnecessária. Porém, para serialização JSON, nil produz null enquanto []int{} produz []:
json.Marshal(nilSlice) // null
json.Marshal(emptySlice) // []
Se sua API REST precisa retornar arrays vazios (não null) no JSON, use []int{} ou make([]int, 0).
Maps nil
Um map nil permite leitura mas não escrita:
var m map[string]int // nil
// SEGURO: leitura retorna zero value
valor := m["chave"]
fmt.Println(valor) // 0
// SEGURO: verificar existência
valor, ok := m["chave"]
fmt.Println(ok) // false
// SEGURO: len
fmt.Println(len(m)) // 0
// SEGURO: range — zero iterações
for k, v := range m {
fmt.Println(k, v) // nunca executa
}
// SEGURO: delete (no-op)
delete(m, "chave") // não faz nada, sem panic
// PANIC: escrita em nil map
m["chave"] = 1 // panic: assignment to entry in nil map
Regra de ouro: sempre inicialize maps com make ou literal antes de escrever:
// Correto
m := make(map[string]int)
m["chave"] = 1
// Ou
m := map[string]int{"chave": 1}
Channels nil
Um channel nil tem comportamento especial — todas as operações bloqueiam permanentemente:
var ch chan int // nil
// BLOQUEIA PARA SEMPRE: enviar
ch <- 1 // deadlock se nenhuma outra goroutine existir
// BLOQUEIA PARA SEMPRE: receber
valor := <-ch // deadlock
// close de nil channel causa PANIC
close(ch) // panic: close of nil channel
Esse comportamento aparentemente estranho é extremamente útil com select para desabilitar cases dinamicamente:
func merge(ch1, ch2 <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for ch1 != nil || ch2 != nil {
select {
case v, ok := <-ch1:
if !ok {
ch1 = nil // desabilita este case
continue
}
out <- v
case v, ok := <-ch2:
if !ok {
ch2 = nil // desabilita este case
continue
}
out <- v
}
}
}()
return out
}
Quando um canal é setado para nil dentro do select, aquele case nunca mais será selecionado — o select simplesmente o ignora. Esse é um padrão idiomático para fan-in de múltiplos channels.
Funções nil
Variáveis de tipo func podem ser nil:
var callback func(string)
// Verificar antes de chamar
if callback != nil {
callback("olá")
}
// Chamar nil function causa PANIC
callback("olá") // panic: runtime error: invalid memory address
Esse padrão é comum em configuração de hooks e callbacks opcionais:
type Logger struct {
OnError func(error)
OnSuccess func(string)
}
func (l *Logger) Log(msg string, err error) {
if err != nil && l.OnError != nil {
l.OnError(err)
}
if err == nil && l.OnSuccess != nil {
l.OnSuccess(msg)
}
}
Interfaces nil — a armadilha mais perigosa
A armadilha mais sutil de nil em Go envolve interfaces. Uma interface em Go é composta por dois campos internos: tipo e valor. Uma interface só é nil quando ambos são nil:
var i interface{} // nil: tipo=nil, valor=nil
fmt.Println(i == nil) // true
var p *int = nil
i = p // tipo=*int, valor=nil
fmt.Println(i == nil) // FALSE! — interface não é nil
Esse é o famoso nil interface gotcha que confunde até desenvolvedores experientes:
type MeuErro struct {
Msg string
}
func (e *MeuErro) Error() string {
return e.Msg
}
func operacao() error {
var err *MeuErro // nil
// ... lógica que pode ou não setar err
return err // PROBLEMA: retorna interface com tipo mas valor nil
}
func main() {
err := operacao()
if err != nil {
// ENTRA AQUI mesmo com err sendo "nil"!
fmt.Println(err) // panic se tentar acessar campos
}
}
Solução: sempre retorne nil explicitamente, nunca um ponteiro nil tipado:
func operacao() error {
var err *MeuErro
// ... lógica
if err != nil {
return err
}
return nil // retorna interface nil corretamente
}
Nil receiver — métodos em valores nil
Go permite chamar métodos em receivers nil, desde que o método não acesse campos sem verificar:
type Lista struct {
itens []string
}
func (l *Lista) Adicionar(item string) {
if l == nil {
return // ou panic, ou criar nova lista
}
l.itens = append(l.itens, item)
}
func (l *Lista) Tamanho() int {
if l == nil {
return 0
}
return len(l.itens)
}
var lista *Lista // nil
fmt.Println(lista.Tamanho()) // 0 — não causa panic!
Esse padrão é usado na biblioteca padrão. Por exemplo, (*bytes.Buffer).Write verifica nil antes de operar. É útil para implementar o null object pattern em Go.
Comparações com nil
// Tipos comparáveis com nil
var p *int
var s []int
var m map[string]int
var ch chan int
var f func()
var i interface{}
fmt.Println(p == nil) // true
fmt.Println(s == nil) // true
fmt.Println(m == nil) // true
fmt.Println(ch == nil) // true
fmt.Println(f == nil) // true
fmt.Println(i == nil) // true
// Slices, maps e funcs NÃO são comparáveis entre si
// s1 == s2 // ERRO: não compila
// m1 == m2 // ERRO: não compila
// Mas podem ser comparados com nil
Nil em contexto de concorrência
Nil tem papel importante em padrões de concorrência com goroutines:
// Padrão: done channel nil para desabilitar timeout
func buscar(ctx context.Context, url string) ([]byte, error) {
var timeout <-chan time.Time // nil — nunca dispara
if deadline, ok := ctx.Deadline(); ok {
timer := time.NewTimer(time.Until(deadline))
defer timer.Stop()
timeout = timer.C
}
select {
case resultado := <-fazerRequisicao(url):
return resultado, nil
case <-timeout: // se nil, este case é ignorado
return nil, errors.New("timeout")
case <-ctx.Done():
return nil, ctx.Err()
}
}
O uso de channels nil com select é um padrão avançado mas extremamente útil para controle de fluxo dinâmico em código concorrente.
Boas práticas com nil
- Sempre verifique ponteiros antes de desreferenciar — previne panics
- Retorne nil explícito para interfaces — nunca retorne ponteiro nil tipado como interface
- Inicialize maps antes de escrever — use make ou literal
- Projete nil receivers seguros — verifique nil em métodos de ponteiro
- Use nil channel para desabilitar select cases — padrão idiomático
- Prefira nil slice a empty slice — exceto quando JSON precisa de
[] - Documente comportamento com nil — deixe claro se nil é válido como input
Para complementar, estude como o tratamento de erros em Go se relaciona com nil (o padrão if err != nil) e como context usa nil para indicar ausência de parent context.
Perguntas frequentes (FAQ)
Qual a diferença entre nil slice e slice vazio em Go?
Um nil slice (var s []int) tem ponteiro interno nil, enquanto um slice vazio ([]int{} ou make([]int, 0)) tem ponteiro válido para array de tamanho zero. Funcionalmente são equivalentes — ambos têm len 0, suportam append e range. A diferença prática é na serialização JSON: nil vira null, vazio vira [].
Por que minha interface não é nil mesmo com valor nil dentro?
Porque uma interface em Go é composta por (tipo, valor). Se você atribui um ponteiro nil tipado a uma interface, o tipo é preenchido (*int, *MeuErro, etc.) mesmo que o valor seja nil. A interface só é nil quando ambos campos são nil. Sempre retorne nil literal (não variável tipada) para indicar ausência em retornos de interface.
É seguro usar range em nil slice ou nil map?
Sim. range sobre nil slice ou nil map simplesmente não executa nenhuma iteração — é equivalente a iterar sobre coleção vazia. Isso é por design e elimina a necessidade de verificar nil antes de loops for range. Operações de leitura como len() também retornam 0 para nil slices e maps.
Por que nil channel bloqueia ao invés de causar panic?
Esse comportamento é intencional e extremamente útil. Um channel nil no select faz o case ser efetivamente desabilitado — nunca será selecionado. Isso permite implementar padrões dinâmicos onde channels são “ligados” e “desligados” durante a execução, como fan-in com fechamento gracioso de múltiplas fontes.