Menu

Interfaces em Golang: implementação implícita, any e a armadilha do nil

Interfaces em Go são satisfeitas de forma implícita: qualquer tipo com os métodos certos as implementa. Veja interfaces pequenas como io.Reader e fmt.Stringer, a interface vazia any, a armadilha da interface nil e a regra aceite interfaces, devolva structs.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Uma interface é um conjunto de métodos

Um tipo interface lista assinaturas de métodos. Qualquer tipo que tenha esses métodos satisfaz a interface, sem nenhuma declaração dizendo isso.

Nem Rect nem Circle mencionam Shape. Essa é a implementação implícita, a característica que define as interfaces em Go. Ela permite definir no seu pacote uma interface que tipos de outros pacotes já satisfazem, sem mexer no código deles.

Interfaces pequenas da biblioteca padrão

Código Go prefere interfaces com um ou dois métodos. As mais importantes:

InterfaceMétodoUsada por
fmt.StringerString() stringimpressão do fmt
errorError() stringtoda função que pode falhar
io.ReaderRead(p []byte) (n int, err error)arquivos, rede, gzip, corpos HTTP
io.WriterWrite(p []byte) (n int, err error)arquivos, buffers, hashes, respostas HTTP
sort.InterfaceLen, Less, Swapo pacote sort
http.HandlerServeHTTP(w, r)net/http

Como io.Reader tem um só método, dezenas de tipos o implementam, e qualquer função que recebe um io.Reader funciona com todos eles:

O provérbio do Go diz: "quanto maior a interface, mais fraca a abstração". Interfaces maiores são montadas combinando as pequenas: io.ReadWriter é Reader mais Writer, escrita embutindo uma interface em outra.

any: a interface vazia

interface{} não tem métodos, então todo tipo a satisfaz. O Go 1.18 acrescentou any como alias; os dois são idênticos.

Um valor any pode guardar qualquer coisa, mas você não consegue fazer quase nada com ele até recuperar o tipo concreto com uma type assertion ou um type switch. Prefira uma interface de verdade ou generics quando o conjunto de tipos é conhecido. any serve para dados realmente dinâmicos, como um JSON decodificado de formato desconhecido, e para impressão.

O que um valor de interface contém

Um valor de interface é um par: um tipo dinâmico e um valor dinâmico. var s Shape = Rect{3, 4} guarda o tipo Rect e uma cópia do valor. Chamar s.Area() procura o método de Rect em tempo de execução.

Uma interface só é nil quando as duas partes estão vazias. Essa regra causa o bug mais confuso do Go.

A armadilha da interface nil

Um ponteiro nil guardado em uma interface gera uma interface que não é nil.

Saída:

false
*main.MyError true
true

validate(true) devolve uma interface error que guarda o tipo *MyError e o valor nil. A interface tem um tipo, então não é igual a nil, e o ramo if err != nil de quem chama executa. Chamar err.Error() ali causaria panic no acesso ao campo do receiver nil.

A correção é simples: declare a variável como error, não como o tipo ponteiro concreto, ou devolva um nil literal no caminho de sucesso. Nunca devolva um tipo ponteiro de erro concreto em uma função cujo resultado é error. A mesma armadilha vale para qualquer interface, não só para erros.

Verificando que um tipo implementa uma interface

A implementação é verificada onde um valor é atribuído a uma interface. Se nenhum código faz isso ainda, um erro em uma assinatura de método passa despercebido. Uma atribuição em branco no nível de pacote torna a verificação explícita:

var _ io.Writer = (*LogWriter)(nil)
var _ fmt.Stringer = Temp(0)

Isso não custa nada em tempo de execução. Se *LogWriter tiver Write(p []byte) error em vez de Write(p []byte) (int, error), o build falha:

cannot use (*LogWriter)(nil) (value of type *LogWriter) as io.Writer value in variable declaration: *LogWriter does not implement io.Writer (wrong type for method Write)
		have Write([]byte) error
		want Write([]byte) (int, error)

Receivers ponteiro e interfaces

Se um método tem receiver ponteiro, só o tipo ponteiro tem esse método. *Counter satisfaz uma interface por meio dele; Counter não. O compilador diz Counter does not implement Incrementer (method Inc has pointer receiver). Guarde &Counter{} na interface. A página de métodos explica os conjuntos de métodos.

Aceite interfaces, devolva structs

Uma diretriz comum em Go: funções devem receber parâmetros interface e devolver tipos concretos.

  • Aceitar uma interface permite que quem chama passe qualquer coisa que se encaixe, inclusive fakes de teste. Uma função que lê dados deve receber um io.Reader, não um *os.File.
  • Devolver um tipo concreto permite que quem chama use todos os métodos e campos dele, e evita a armadilha da interface nil. os.Open devolve *os.File, não io.Reader.

Um hábito relacionado: defina interfaces onde elas são usadas, não onde são implementadas. Se o seu serviço precisa de algo que consiga fazer Get(id) de um usuário, declare uma interface de um método no pacote do serviço e deixe o pacote do banco de dados apenas exportar a sua struct.

Comparando valores de interface

Dois valores de interface são iguais quando os tipos dinâmicos são idênticos e os valores dinâmicos são iguais. Se o tipo dinâmico não for comparável (um slice, um map), == compila, mas causa panic em tempo de execução: runtime error: comparing uncomparable type []int.

Erros comuns

  • Devolver um ponteiro nil tipado como interface. Devolva nil literal.
  • Interfaces cedo demais. Escreva o tipo concreto primeiro. Acrescente uma interface quando uma segunda implementação ou um teste precisar.
  • Ponteiro para interface. *io.Reader quase nunca está certo. Uma interface já guarda um ponteiro quando você guarda um nela.
  • Interfaces grandes. Interfaces de dez métodos são difíceis de implementar e de simular em testes. Divida-as.

Perguntas frequentes

Como implementar uma interface em Go?

Defina no seu tipo os métodos que a interface lista, com os mesmos nomes e assinaturas. Não existe a palavra-chave implements. Se *File tem Read(p []byte) (int, error), ele é um io.Reader, automaticamente. O compilador verifica isso onde quer que você atribua o valor ao tipo interface.

O que é a interface vazia ou any em Go?

interface{} não tem métodos, então todo tipo a satisfaz. Desde o Go 1.18, any é um alias embutido de interface{}. Um valor do tipo any pode guardar qualquer coisa, mas você precisa de uma type assertion ou de um type switch para recuperar um tipo concreto.

Por que minha interface Go não é nil quando atribuí um ponteiro nil?

Um valor de interface guarda um tipo e um valor. Atribuir um *MyError nil a um error gera uma interface cujo tipo é *MyError e cujo valor é nil, e essa interface não é igual a nil. Devolva um nil literal em vez de um ponteiro nil tipado quando não houver erro.

Como verificar em tempo de compilação que um tipo implementa uma interface?

Acrescente uma atribuição em branco no nível de pacote: var _ io.Reader = (*MyReader)(nil). Se faltar um método em *MyReader, o build falha com uma mensagem que nomeia o método ausente.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR