Skip to content

Erro de Ponteiro nil

Introdução

Durante um processo de escrita de código, precisei chamar o método Close() para fechar múltiplos objetos, como no código abaixo:

go
type A struct {
  b B
  c C
  d D
}

func (a A) Close() error {
  if a.b != nil {
    if err := a.b.Close(); err != nil {
      return err
    }
  }

  if a.c != nil {
    if err := a.c.Close(); err != nil {
      return err
    }
  }

    if a.d != nil {
        if err := a.d.Close(); err != nil {
            return err
        }
    }

  return nil
}

Mas escrever tantos julgamentos if parecia pouco elegante. B, C e D todos implementam o método Close, então deveria ser possível fazer de forma mais concisa. Assim, os coloquei em um slice e usei loop para julgamento:

go
func (a A) Close() error {
  closers := []io.Closer{
    a.b,
    a.c,
    a.d,
  }

  for _, closer := range closers {
    if closer != nil {
      if err := closer.Close(); err != nil {
        return err
      }
    }
  }
  return nil
}

Isso parece melhor. Vamos executar e ver:

go
func main() {
  var a A
  if err := a.Close(); err != nil {
    panic(err)
  }
  fmt.Println("success")
}

O resultado foi surpreendente - o programa quebrou. A mensagem de erro é如下, indicando que não se pode chamar método em receptor nil. O if closer != nil no loop parece não ter funcionado como filtro:

panic: value method main.B.Close called using nil *B pointer

O exemplo acima é uma versão simplificada de um bug que encontrei anteriormente. Muitos iniciantes podem cometer este erro como eu. Abaixo explicarei o que realmente acontece.

Interface

Em capítulos anteriores mencionamos que nil é o valor zero para tipos de referência, como slice, map, channel, função, ponteiro e interface. Para slice, map, channel e função, podem ser todos vistos como ponteiros, apontando para implementações específicas.

Mas apenas a interface é diferente. Interface é composta por duas coisas: tipo e valor.

Quando tenta-se atribuir nil a uma variável, a compilação falha e mostra a seguinte mensagem:

use of untyped nil in assignment

O conteúdo basicamente diz que não se pode declarar uma variável com valor untyped nil. Se existe untyped nil, certamente existe typed nil. Esta situação frequentemente ocorre com interfaces. Veja um exemplo simples abaixo:

go
func main() {
  var p *int
  fmt.Println(p)
  fmt.Println(p == nil)
  var pa any
  pa = p
  fmt.Println(pa)
  fmt.Println(pa == nil)
}

Saída:

<nil>
true
<nil>
false

O resultado é muito estranho. Claramente a saída de pa é nil, mas ele não é igual a nil. Podemos usar reflexão para ver o que realmente é:

go
func main() {
  var p *int
  fmt.Println(p)
  fmt.Println(p == nil)
  var pa any
  pa = p
  fmt.Println(reflect.TypeOf(pa))
  fmt.Println(reflect.ValueOf(pa))
}

Saída:

<nil>
true
*int
<nil>

Do resultado podemos ver que é na verdade (*int)(nil). Ou seja, pa armazena o tipo *int, e seu valor real é nil. Quando se faz operação de igualdade em valores de tipo interface, primeiro julga-se se os tipos são iguais. Se os tipos não forem iguais, diretamente considera-se não igual. Depois julga-se se os valores são iguais. A lógica de julgamento de interface pode ser referenciada na função cmd/compile/internal/walk.walkCompare.

Portanto, se quiser que uma interface seja igual a nil, é necessário que tanto seu valor quanto seu tipo sejam nil. Porque o tipo na interface é na verdade também um ponteiro:

go
type iface struct {
  tab  *itab
  data unsafe.Pointer
}

Se quiser contornar o tipo e julgar diretamente se o valor é nil, pode-se usar reflexão. Abaixo está um exemplo:

go
func main() {
  var p *int
  fmt.Println(p)
  fmt.Println(p == nil)
  var pa any
  pa = p
  fmt.Println(reflect.ValueOf(pa).IsNil())
}

Através de IsNil() pode-se julgar diretamente se o valor é nil. Assim não ocorre o problema mencionado acima. Portanto, ao usar normalmente, se o valor de retorno de uma função for tipo interface, e quiser retornar um valor zero, melhor retornar diretamente nil, não retornar valor zero de qualquer implementação específica. Mesmo que implemente a interface, nunca será igual a nil, o que pode causar o erro do exemplo.

Resumo

Resolvido o problema acima, vamos ver os seguintes exemplos:

Quando o receptor do método da struct for receptor ponteiro, nil é utilizável. Veja o exemplo abaixo:

go
type A struct {

}

func (a *A) Do()  {

}

func main() {
  var a *A
  a.Do()
}

Este código pode executar normalmente sem reportar erro de ponteiro nulo.

Quando o slice for nil, pode-se acessar seu comprimento e capacidade, e também adicionar elementos:

go
func main() {
  var s []int
  fmt.Println(len(s))
  fmt.Println(cap(s))
  s = append(s, 1)
}

Quando o map for nil, ainda pode-se acessá-lo, mas map nil é somente leitura. Uma vez que tente escrever, causa panic:

go
func main() {
  var s map[string]int
  i, ok := s[""]
  fmt.Println(i, ok)
  fmt.Println(len(s))

  // Ao tentar escrever, causa panic
  s["a"] = 1 // panic: assignment to entry in nil map

}

Estas características relacionadas a nil nos exemplos acima podem causar confusão, especialmente para iniciantes em Go. nil representa o valor zero dos tipos acima, ou seja, valor padrão. Valores padrão devem exibir comportamento padrão. Isso é exatamente o que os designers do Go queriam: tornar nil mais útil, em vez de simplesmente lançar erro de ponteiro nulo. Esta filosofia também se reflete na biblioteca padrão. Por exemplo, para iniciar um servidor HTTP pode-se escrever assim:

go
http.ListenAndServe(":8080", nil)

Podemos diretamente passar um nil Handler, e a biblioteca http usará o Handler padrão para processar requisições HTTP.

TIP

Interessados podem assistir este vídeo Understanding nil - Gopher Conference 2016, que é muito claro e fácil de entender.

Golang por www.golangdev.cn edit