Blog

Interfaces em Go: implícitas, pequenas e a armadilha do nil

Uma interface em Go é uma lista de métodos, e qualquer tipo com esses métodos a satisfaz sem precisar dizer isso. Veja method sets, Stringer, io.Reader, type switches e por que um error com um ponteiro nil dentro não é nil.

Uma interface em Go diz o que um valor sabe fazer, não o que ele é. Qualquer tipo com os métodos certos serve, e ele nunca precisa anunciar isso. Essa única regra é o motivo de o código Go costumar ter interfaces pequenas e poucas hierarquias de tipos.

Este post cobre como os tipos satisfazem interfaces, a regra dos receivers de ponteiro que faz as pessoas tropeçarem, as interfaces pequenas da biblioteca padrão, any e type switches, e a armadilha do nil que já pegou quase todo programador Go uma vez. Todo programa abaixo rodou no Go 1.26, e a saída foi colada da execução.

Uma interface é um conjunto de métodos

Um tipo interface lista assinaturas de métodos, e um valor de qualquer tipo que tenha todos esses métodos pode ser guardado nele.

package main

import (
	"fmt"
	"math"
)

type Shape interface {
	Area() float64
	Perimeter() float64
}

type Rect struct {
	W, H float64
}

func (r Rect) Area() float64      { return r.W * r.H }
func (r Rect) Perimeter() float64 { return 2 * (r.W + r.H) }

type Circle struct {
	R float64
}

func (c Circle) Area() float64      { return math.Pi * c.R * c.R }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.R }

func describe(s Shape) {
	fmt.Printf("%T: area %.2f, perimeter %.2f\n", s, s.Area(), s.Perimeter())
}

func main() {
	describe(Rect{W: 3, H: 4})
	describe(Circle{R: 1})

	shapes := []Shape{Rect{W: 2, H: 2}, Circle{R: 2}}
	total := 0.0
	for _, s := range shapes {
		total += s.Area()
	}
	fmt.Printf("total area %.2f\n", total)
}

Ele imprime:

main.Rect: area 12.00, perimeter 14.00
main.Circle: area 3.14, perimeter 6.28
total area 16.57

Repare no que está faltando. Rect e Circle nunca mencionam Shape. Não existe implements Shape em lugar nenhum. Eles têm um método Area e um método Perimeter com as assinaturas certas, e isso basta.

describe aceita qualquer Shape, e %T mostra o tipo concreto que está de fato lá dentro. O slice []Shape guarda um Rect e um Circle lado a lado, e o laço chama Area em cada um sem saber qual é qual.

Como a satisfação é implícita, você pode definir uma interface depois que os tipos já existem, até num outro pacote do qual os autores dos tipos nunca ouviram falar. Os tipos não precisam mudar.

O compilador confere onde você usa

Implícito não quer dizer sem conferência. No momento em que você usa um valor como interface, o compilador confere se o tipo dele tem todos os métodos, e um método faltando para o build.

package main

import "fmt"

type Shape interface {
	Area() float64
	Perimeter() float64
}

type Square struct {
	Side float64
}

func (s Square) Area() float64 { return s.Side * s.Side }

func describe(s Shape) {
	fmt.Println(s.Area(), s.Perimeter())
}

func main() {
	describe(Square{Side: 2})
}

O build falha com:

./main.go:21:11: cannot use Square{…} (value of struct type Square) as Shape value in argument to describe: Square does not implement Shape (missing method Perimeter)

A mensagem diz o nome do método que falta. Não há surpresa em tempo de execução aqui: um tipo que não se encaixa é um erro de compilação na linha que tentou usá-lo.

Conferindo sem um ponto de uso

Às vezes nada no seu pacote passa o tipo como a interface ainda. Um tipo de biblioteca pode ser usado assim só por quem chama. Para ter a conferência mesmo assim, programadores Go escrevem uma linha como esta:

package main

import "fmt"

type Shape interface {
	Area() float64
	Perimeter() float64
}

type Square struct {
	Side float64
}

func (s *Square) Area() float64 { return s.Side * s.Side }

var _ Shape = (*Square)(nil)

func main() {
	fmt.Println("never gets here")
}

O build falha com:

./main.go:16:15: cannot use (*Square)(nil) (value of type *Square) as Shape value in variable declaration: *Square does not implement Shape (missing method Perimeter)

var _ Shape = (*Square)(nil) declara uma variável que você não pode usar, porque o nome dela é o identificador em branco _. O valor é um *Square nil, que não custa nada. O único propósito da linha é a atribuição, que obriga o compilador a conferir se *Square satisfaz Shape. Adicione Perimeter e a linha compila para nada.

Receivers de ponteiro e method sets

Se os métodos de um tipo têm receivers de ponteiro, só o tipo ponteiro satisfaz a interface, e um valor comum não.

package main

import "fmt"

type Counter interface {
	Inc()
	Value() int
}

type Clicks struct {
	n int
}

func (c *Clicks) Inc()       { c.n++ }
func (c *Clicks) Value() int { return c.n }

func main() {
	var c Counter = Clicks{}
	c.Inc()
	fmt.Println(c.Value())
}

O build falha com:

./main.go:18:18: cannot use Clicks{} (value of struct type Clicks) as Counter value in variable declaration: Clicks does not implement Counter (method Inc has pointer receiver)

Esse surpreende, porque numa variável comum você pode chamar c.Inc() num valor Clicks e Go pega o endereço por você. Esse atalho não vale para interfaces. Guarde um ponteiro no lugar:

package main

import "fmt"

type Counter interface {
	Inc()
	Value() int
}

type Clicks struct {
	n int
}

func (c *Clicks) Inc()       { c.n++ }
func (c *Clicks) Value() int { return c.n }

func main() {
	var c Counter = &Clicks{}
	c.Inc()
	c.Inc()
	fmt.Println(c.Value())
}

Ele imprime:

2

A regra tem nome: o method set. O method set de T tem os métodos com receivers de valor. O method set de *T tem esses mais os que têm receivers de ponteiro. Um tipo satisfaz uma interface quando todos os métodos da interface estão no method set dele.

O motivo é o que uma interface guarda. Colocar Clicks{} numa interface copia a struct para dentro dela. Se Go deixasse Inc rodar nessa cópia, c.n++ mudaria uma cópia escondida, nada que você pudesse ver, e a contagem ficaria em zero sem avisar. Recusar a compilação é a resposta mais gentil. A parte sobre structs e métodos explica quando escolher um receiver de ponteiro, para começo de conversa.

Interfaces pequenas da biblioteca padrão

As interfaces mais úteis de Go têm um método só, e a biblioteca padrão é construída em cima de um punhado delas.

fmt.Stringer

fmt.Stringer é declarada como interface { String() string }. O pacote fmt procura por ela, então qualquer tipo com um método String controla como é impresso.

package main

import "fmt"

type Temp float64

func (t Temp) String() string {
	return fmt.Sprintf("%.1f°C", float64(t))
}

type Point struct {
	X, Y int
}

func main() {
	t := Temp(21.456)
	fmt.Println(t)
	fmt.Printf("%v and %s\n", t, t)
	fmt.Println(Point{X: 1, Y: 2})
	fmt.Println(float64(t))
}

Ele imprime:

21.5°C
21.5°C and 21.5°C
{1 2}
21.456

Temp tem um método String, então Println, %v e %s usam esse método. Point não tem, então fica com o formato padrão de struct. A última linha merece uma segunda olhada. Converter t para float64 dá um valor de outro tipo, e float64 não tem método String, então volta o número cru.

Dentro de String chamamos Sprintf com float64(t), não t, pelo mesmo motivo. Passar t com %v chamaria String de novo, que chamaria Sprintf de novo, para sempre.

io.Reader e io.Writer

io.Reader tem um método, Read(p []byte) (n int, err error), e io.Writer tem um método, Write(p []byte) (n int, err error). Arquivos, conexões de rede, corpos HTTP, buffers e strings satisfazem uma delas ou as duas.

package main

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"strings"
)

type upperWriter struct {
	w io.Writer
}

func (u upperWriter) Write(p []byte) (int, error) {
	return u.w.Write(bytes.ToUpper(p))
}

func main() {
	r := strings.NewReader("hello from a string\n")
	n, err := io.Copy(os.Stdout, r)
	fmt.Println(n, err)

	loud := upperWriter{w: os.Stdout}
	io.Copy(loud, strings.NewReader("hello again\n"))
}

Ele imprime:

hello from a string
20 <nil>
HELLO AGAIN

io.Copy recebe um Writer e um Reader e move bytes de um para o outro até o reader acabar. Ele não sabe que está lendo uma string ou escrevendo no terminal. Ele retornou 20, o número de bytes copiados.

upperWriter é o nosso próprio Writer. O único método dele passa os bytes para maiúsculas e os entrega ao writer que ele envolve. Quatro linhas fizeram um tipo que io.Copy, e qualquer outra função que recebe um Writer, consegue usar.

Aceite interfaces, retorne structs

Uma orientação comum em Go é que funções devem aceitar interfaces e retornar tipos concretos. Aceitar uma interface deixa quem chama passar o que tiver em mãos.

package main

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"strings"
)

func countLines(r io.Reader) (int, error) {
	sc := bufio.NewScanner(r)
	n := 0
	for sc.Scan() {
		n++
	}
	return n, sc.Err()
}

func main() {
	n, err := countLines(strings.NewReader("one\ntwo\nthree\n"))
	fmt.Println(n, err)

	var buf bytes.Buffer
	buf.WriteString("alpha\nbeta\n")
	n, err = countLines(&buf)
	fmt.Println(n, err)
}

Ele imprime:

3 <nil>
2 <nil>

countLines só precisa de Read, então pede um io.Reader. A mesma função conta linhas numa string, num buffer, num arquivo aberto ou no corpo de uma requisição, e os testes podem entregar um strings.Reader no lugar de um arquivo de verdade.

Retornar uma struct, como *bytes.Buffer ou *bufio.Scanner, vai no sentido contrário. Quem chama recebe todos os métodos que o tipo tem, e pode guardá-lo na interface pequena de que precisar. Se você retorna uma interface, esconde esses métodos e decide por quem chama o que ele pode fazer.

É uma orientação, não uma lei. error é uma interface, e funções a retornam o tempo todo.

any, type assertions e comma-ok

any é outro nome para interface{}, a interface sem métodos, então todo tipo a satisfaz. Você já a viu em fmt.Println(a ...any). Para tirar um valor concreto de volta, você usa uma type assertion.

package main

import "fmt"

func main() {
	var x any = "gopher"

	s, ok := x.(string)
	fmt.Printf("%q %v\n", s, ok)

	n, ok := x.(int)
	fmt.Println(n, ok)

	x = 42
	fmt.Println(x.(int) + 1)
	fmt.Println(x.(string))
}

Imprime três linhas e para:

"gopher" true
0 false
43
panic: interface conversion: interface {} is int, not string

x.(string) pergunta “o valor dentro de x é uma string?”. Com dois resultados, s, ok := x.(string), você recebe o valor e true quando é. Quando não é, como em x.(int), você recebe o zero value e false, e nada quebra. É a mesma forma comma-ok que os maps usam.

Com um resultado só não há onde informar a falha, então um palpite errado causa panic. A mensagem do panic ainda diz interface {}, a grafia antiga, porque any é só um alias. Use a forma de um resultado só quando um tipo errado seria um bug no seu próprio código.

Type switches, um nível mais fundo

Um type switch é uma cadeia de type assertions, e os cases dele podem nomear interfaces além de tipos concretos. A parte sobre controle de fluxo mostrou o formato básico. Veja o que mais ele faz.

package main

import (
	"errors"
	"fmt"
	"strconv"
)

type Celsius float64

func (c Celsius) String() string { return strconv.FormatFloat(float64(c), 'f', 1, 64) + "°C" }

func describe(x any) string {
	switch v := x.(type) {
	case nil:
		return "nil"
	case int, int64:
		return fmt.Sprintf("a whole number, %T %v", v, v)
	case error:
		return "an error: " + v.Error()
	case fmt.Stringer:
		return "a Stringer: " + v.String()
	default:
		return fmt.Sprintf("something else: %T", v)
	}
}

func main() {
	fmt.Println(describe(nil))
	fmt.Println(describe(7))
	fmt.Println(describe(int64(7)))
	fmt.Println(describe(Celsius(21.5)))
	fmt.Println(describe(errors.New("disk full")))
	fmt.Println(describe([]int{1, 2}))
}

Ele imprime:

nil
a whole number, int 7
a whole number, int64 7
a Stringer: 21.5°C
an error: disk full
something else: []int

Três coisas estão acontecendo:

  • Um case com um tipo dá esse tipo a v. Em case error, v é um error, então v.Error() compila.
  • Um case com vários tipos, como case int, int64, não consegue escolher um, então v continua any. Você pode imprimi-lo, mas não pode fazer contas com ele sem outra assertion.
  • Um case com uma interface casa com qualquer valor cujo tipo tenha aqueles métodos. Celsius casou com fmt.Stringer sem nunca nomeá-la.

Os cases são testados em ordem, e o primeiro que casar vence. Um tipo com um método Error e um método String cairia em case error aqui, porque ele vem primeiro.

Use um type switch quando um valor pode mesmo ser de vários tipos sem relação entre si, como um valor JSON decodificado. Quando os tipos compartilham comportamento, coloque esse comportamento num método de interface e chame o método. Foi o que Shape fez no começo.

A armadilha do nil

Uma interface que guarda um ponteiro nil não é, ela mesma, nil, e isso faz uma função que retorna error informar uma falha que nunca aconteceu.

package main

import "fmt"

type MyError struct {
	Msg string
}

func (e *MyError) Error() string { return e.Msg }

func checkAge(age int) error {
	var e *MyError
	if age < 0 {
		e = &MyError{Msg: "age can't be negative"}
	}
	return e
}

func main() {
	err := checkAge(30)
	if err != nil {
		fmt.Println("failed:", err)
		return
	}
	fmt.Println("age is fine")
}

Ele imprime:

failed: <nil>

A idade estava certa. e continuou um ponteiro nil. Mesmo assim err != nil deu verdadeiro, e o programa entrou no caminho de falha.

O <nil> é uma segunda surpresa. fmt chamou Error num *MyError nil, e e.Msg desreferenciou nil. fmt se recupera desse panic específico e imprime <nil> no lugar. Chame err.Error() você mesmo e o programa quebra com uma desreferência de ponteiro nil.

Explicado como se você tivesse dez anos

Imagine uma interface como uma caixa com uma etiqueta na tampa. A caixa tem dois slots: a etiqueta, que diz que tipo de coisa está dentro, e a coisa em si.

Uma caixa vazia não tem nada na etiqueta e nada dentro. Essa é a única caixa que Go chama de nil.

Agora pegue uma caixa, escreva “MyError” na etiqueta e não coloque nada dentro. Ela está vazia? Alguém que confere “a etiqueta está em branco e a caixa está vazia?” diz que não. A etiqueta está preenchida. Então err == nil é falso, mesmo sem nada dentro.

A versão precisa

Um valor de interface são duas palavras. A primeira diz qual tipo concreto está guardado, e a segunda é o valor desse tipo (para um ponteiro, o próprio ponteiro). Uma interface é nil só quando as duas palavras estão vazias.

return e converte o *MyError em um error. Essa conversão sempre registra o tipo, *MyError, na primeira palavra. A segunda palavra guarda o ponteiro nil. Uma palavra está preenchida, então a interface não é nil.

Onde a analogia falha: uma caixa de verdade sem nada dentro não serve para nada, mas uma interface com um ponteiro nil nem sempre é um erro. Você pode chamar métodos nela, e um método escrito para lidar com um receiver nil funciona bem. A armadilha está só em compará-la com nil e esperar verdadeiro.

Observando as duas palavras

As duas palavras de um valor de interface mudam a cada passo da armadilha, e a animação abaixo acompanha isso:

tipo valor err (error) nil nil p (*MyError) seu tipo ponteiro *MyError nil *MyError nil err == nil dá true p == nil dá true err tem tipo *MyError, valor nil err == nil dá false err == nil: true de novo passo 1: var err error deixa tudo vazio passo 2: var p *MyError = nil não aponta para nada passo 3: err = p põe *MyError no slot do tipo; o valor é nil passo 4: o slot do tipo tem algo, e err não é nil a correção: return nil, e tudo fica vazio

Um valor de interface desenhado como duas células, tipo e valor. Um err vazio é igual a nil. Atribuir um *MyError nil preenche a célula do tipo com *MyError enquanto a célula do valor continua nil, então err deixa de ser igual a nil. Retornar um nil puro deixa as duas células vazias.

Aqui estão esses passos em palavras, caso a animação não rode para você:

  1. var err error cria uma interface com os dois slots vazios. err == nil é verdadeiro.
  2. var p *MyError = nil cria um ponteiro que não aponta para nada. p == nil é verdadeiro.
  3. err = p guarda o tipo *MyError no slot do tipo e o ponteiro nil no slot do valor.
  4. Agora err == nil é falso, porque o slot do tipo não está vazio.
  5. A correção: return nil no lugar do ponteiro, e os dois slots ficam vazios.

Este programa percorre os quatro primeiros passos:

package main

import "fmt"

type MyError struct {
	Msg string
}

func (e *MyError) Error() string { return e.Msg }

func main() {
	var err error
	fmt.Println("step 1: err == nil is", err == nil)

	var p *MyError = nil
	fmt.Println("step 2: p == nil is", p == nil)

	err = p
	fmt.Printf("step 3: err holds type %T\n", err)

	fmt.Println("step 4: err == nil is", err == nil)
}

Ele imprime:

step 1: err == nil is true
step 2: p == nil is true
step 3: err holds type *main.MyError
step 4: err == nil is false

p == nil compara um ponteiro com nil, e dá verdadeiro. err == nil compara uma interface com a interface vazia, e dá falso. O mesmo nil à direita quer dizer duas coisas diferentes, dependendo do tipo à esquerda.

A correção: retorne um nil puro

Não guarde um erro numa variável do tipo ponteiro concreto para depois retorná-la. Retorne o próprio nil quando der certo:

package main

import "fmt"

type MyError struct {
	Msg string
}

func (e *MyError) Error() string { return e.Msg }

func checkAge(age int) error {
	if age < 0 {
		return &MyError{Msg: "age can't be negative"}
	}
	return nil
}

func main() {
	for _, age := range []int{30, -1} {
		if err := checkAge(age); err != nil {
			fmt.Println(age, "failed:", err)
			continue
		}
		fmt.Println(age, "is fine")
	}
}

Ele imprime:

30 is fine
-1 failed: age can't be negative

return nil numa função cujo tipo de resultado é error produz uma interface com as duas palavras vazias. A regra para guardar é simples: se uma função retorna error, as variáveis de erro dela também devem ter o tipo error, nunca *MyError. A parte sobre erros parte daqui, com wrapping, errors.Is e errors.As.

O que lembrar

  • Uma interface é um conjunto de métodos. Um tipo a satisfaz por ter esses métodos, sem palavra-chave implements.
  • O compilador confere no ponto de uso. var _ Shape = (*Square)(nil) força a conferência quando não há ponto de uso.
  • Métodos com receiver de ponteiro pertencem só ao method set do tipo ponteiro, então guarde &T{} na interface, não T{}.
  • Interfaces pequenas fazem a maior parte do trabalho: fmt.Stringer, io.Reader, io.Writer. Aceite-as como parâmetros e retorne tipos concretos.
  • Use a forma comma-ok, v, ok := x.(T), a não ser que um tipo errado seja mesmo um bug. Um case de type switch com vários tipos deixa v como any.
  • Uma interface é nil só quando o tipo e o valor dela estão vazios. Um *MyError nil retornado como error não é nil, então retorne um nil puro.

Uma interface só é nil quando não guarda nem tipo nem valor.

Quanto este post te ajudou?

Clique em um coração para avaliar!

Média das avaliações 0 / 5. Total de votos: 0

Nenhum voto até agora. Seja o primeiro a avaliar este post.