Voltar ao blog

A palavra-chave interface em Go: interfaces implícitas e armadilhas com nil

Como funciona a palavra-chave interface do Go: satisfação implícita, recetores ponteiro, interfaces pequenas, any, type assertions, a armadilha do erro nil e restrições genéricas.

A palavra-chave interface em Go: interfaces implícitas e armadilhas com nil

A palavra-chave interface declara um conjunto de métodos. Qualquer tipo que tenha esses métodos satisfaz a interface automaticamente. Não existe uma palavra-chave implements nem uma lista de interfaces no próprio tipo, por isso uma struct escrita anos antes de a interface existir continua a poder satisfazê-la. Desde o Go 1.18, uma interface também pode listar tipos além de métodos, o que a transforma numa restrição para código genérico (especificação do Go).

Resumo rápido

  • type Notifier interface { Notify(...) error } declara uma interface. Qualquer tipo com um método Notify correspondente é um Notifier.
  • A satisfação é implícita. var _ Notifier = (*SlackNotifier)(nil) obriga o compilador a verificá-la no momento da compilação.
  • Um método com recetor ponteiro pertence a *T, não a T. Atribuir um T falha com method Notify has pointer receiver.
  • Mantém as interfaces pequenas e declara-as no pacote que as usa. Aceita interfaces, devolve tipos concretos.
  • any é um alias de interface{} e guarda um valor de qualquer tipo. Recuperas o valor com v, ok := x.(T) ou com um type switch.
  • Uma interface só é nil quando o tipo e o valor estão ambos por definir. Devolver um *ValidationError nil como error dá-te um err != nil que é true.
  • As interfaces com listas de tipos como ~int64 | ~float64 são restrições. Funcionam em listas de parâmetros de tipo e em mais lado nenhum.

Como se declara uma interface em Go?

Escreve type, um nome e interface, seguidos das assinaturas dos métodos. Eis um sistema de alertas que envia a mesma mensagem para o Slack e por email:

example.gogo
package main

import (
	"context"
	"fmt"
)

type Notifier interface {
	Notify(ctx context.Context, msg string) error
}

type SlackNotifier struct {
	Channel string
}

func (s SlackNotifier) Notify(ctx context.Context, msg string) error {
	fmt.Printf("slack #%s: %s\n", s.Channel, msg)
	return nil
}

type EmailNotifier struct {
	To string
}

func (e EmailNotifier) Notify(ctx context.Context, msg string) error {
	fmt.Printf("email %s: %s\n", e.To, msg)
	return nil
}

func alertAll(ctx context.Context, notifiers []Notifier, msg string) {
	for _, n := range notifiers {
		if err := n.Notify(ctx, msg); err != nil {
			fmt.Println("notify failed:", err)
		}
	}
}

func main() {
	notifiers := []Notifier{
		SlackNotifier{Channel: "oncall"},
		EmailNotifier{To: "[email protected]"},
	}
	alertAll(context.Background(), notifiers, "payment API p99 above 2s")
}
example.texttext
slack #oncall: payment API p99 above 2s
email [email protected]: payment API p99 above 2s

O alertAll só conhece o método Notify. Não lhe interessa se está a falar com o Slack, com o email ou com um cliente do PagerDuty que alguém acrescente no próximo mês. Cada valor do slice leva consigo o seu tipo concreto, e a chamada vai parar ao método desse tipo em tempo de execução.

Uma interface pode incorporar outras interfaces. Os conjuntos de métodos incorporados são fundidos, e é assim que a biblioteca padrão constrói o io.ReadWriter a partir de io.Reader e io.Writer:

example.gogo
type ReadWriter interface {
	Reader
	Writer
}

Um tipo satisfaz io.ReadWriter quando tem Read e Write. O *os.File e o net.Conn têm os dois. O *strings.Reader não, porque não tem um método Write.

Como é que um tipo implementa uma interface em Go?

Tendo todos os métodos da interface no seu conjunto de métodos, exatamente com a mesma assinatura. O Go chama a isto satisfação implícita, e é a principal diferença em relação a Java ou C#, onde uma classe tem de indicar cada interface que implementa.

Assim os pacotes ficam desacoplados. O SlackNotifier não importa o pacote que declara Notifier e nem precisa de saber que o Notifier existe. Podes escrever hoje uma interface que tipos da biblioteca padrão ou de um pacote de terceiros já satisfazem.

Quando falta um método, o compilador avisa-te no ponto em que o valor é usado como interface:

example.gogo
type PagerDutyNotifier struct {
	RoutingKey string
}

func (p PagerDutyNotifier) Send(ctx context.Context, msg string) error {
	return nil
}

var _ Notifier = PagerDutyNotifier{}
example.texttext
./main.go:20:18: cannot use PagerDutyNotifier{} (value of struct type PagerDutyNotifier) as Notifier value in variable declaration: PagerDutyNotifier does not implement Notifier (missing method Notify)

Se o método existe mas a assinatura é diferente, o erro mostra as duas versões lado a lado:

example.texttext
./main.go:16:18: cannot use PagerDutyNotifier{} (value of struct type PagerDutyNotifier) as Notifier value in variable declaration: PagerDutyNotifier does not implement Notifier (wrong type for method Notify)
		have Notify(string) error
		want Notify(context.Context, string) error

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

Declara uma variável ao nível do pacote com o identificador vazio (blank identifier):

example.gogo
var _ Notifier = (*SlackNotifier)(nil)

Esta linha não aloca nada e nunca executa código. Só pergunta ao compilador se um *SlackNotifier pode ser atribuído a um Notifier, por isso a compilação falha assim que alguém mudar o nome ou a assinatura de Notify. Sem ela, só descobres o problema onde o tipo é usado pela primeira vez como Notifier, o que pode ser noutro pacote. A palavra-chave var em Go mostra o mesmo truque com http.Handler.

Porque é que um recetor ponteiro impede a satisfação de uma interface?

Porque o método pertence ao tipo ponteiro. Todos os tipos têm um conjunto de métodos, e as regras são estas (especificação do Go):

  • O conjunto de métodos de T contém os métodos declarados com um recetor de valor (t T).
  • O conjunto de métodos de *T contém esses e ainda os métodos declarados com um recetor ponteiro (t *T).

Um notifier que agrupa mensagens tem de se modificar a si próprio, por isso usa um recetor ponteiro:

example.gogo
type BatchNotifier struct {
	mu      sync.Mutex
	pending []string
}

func (b *BatchNotifier) Notify(ctx context.Context, msg string) error {
	b.mu.Lock()
	defer b.mu.Unlock()
	b.pending = append(b.pending, msg)
	return nil
}

func main() {
	var n Notifier = BatchNotifier{}
	fmt.Println(n)
}
example.texttext
./main.go:26:19: cannot use BatchNotifier{} (value of struct type BatchNotifier) as Notifier value in variable declaration: BatchNotifier does not implement Notifier (method Notify has pointer receiver)

A correção é var n Notifier = &BatchNotifier{}. O Go recusa a versão por valor de propósito. Uma interface que guardasse um BatchNotifier guardaria uma cópia, e o Notify acrescentaria mensagens ao slice pending da cópia sem que o original mudasse. Passar o Notify para um recetor de valor também não resolve. Copiaria o sync.Mutex em cada chamada, e o go vet assinala isso como Notify passes lock by value.

Um tipo com métodos de recetor ponteiro tem de ser verificado através do seu ponteiro, como em var _ Notifier = (*BatchNotifier)(nil). A forma com ponteiro também funciona para tipos com recetor de valor como o SlackNotifier, porque um *T tem todos os métodos de T. É por isso que (*T)(nil) é a forma habitual de escrever a verificação.

Como deves desenhar interfaces em Go?

O provérbio do Go diz «the bigger the interface, the weaker the abstraction», ou seja, quanto maior a interface, mais fraca a abstração (Go Proverbs). As interfaces mais usadas da biblioteca padrão têm um ou dois métodos:

InterfaceMétodosImplementada por
io.ReaderReadficheiros, ligações de rede, corpos HTTP, leitores gzip, bytes.Buffer
io.WriterWriteficheiros, http.ResponseWriter, hashes, bytes.Buffer
errorError*fs.PathError, *json.SyntaxError, os teus próprios tipos de erro
fmt.StringerStringqualquer tipo que controle a forma como o fmt o imprime
http.HandlerServeHTTProuters, middleware, http.HandlerFunc

Uma interface com um só método é fácil de satisfazer, por isso há muitos tipos que a satisfazem. O io.Copy funciona com qualquer origem e destino porque pede muito pouco. Uma interface Storage com doze métodos costuma ter uma única implementação real, mais um mock que tem de mudar sempre que a interface muda. O http.Handler funciona da mesma forma. Um middleware recebe um e devolve outro, por isso o middleware em Go de qualquer pacote empilha-se à volta de qualquer router.

O fmt.Stringer mostra que basta pouco para um tipo se integrar com a biblioteca padrão. Dá um método String a um enum e todos os verbos do fmt passam a usá-lo:

example.gogo
type OrderStatus int

const (
	Pending OrderStatus = iota
	Paid
	Refunded
)

func (s OrderStatus) String() string {
	switch s {
	case Pending:
		return "pending"
	case Paid:
		return "paid"
	case Refunded:
		return "refunded"
	}
	return fmt.Sprintf("OrderStatus(%d)", int(s))
}

func main() {
	fmt.Println(Paid)
	fmt.Printf("order 1001 is %v\n", Refunded)
}
example.texttext
paid
order 1001 is refunded

Onde se deve declarar uma interface em Go?

No pacote que a usa, não no pacote que a implementa. Como a satisfação é implícita, quem consome a interface pode listar só os métodos que chama.

Um serviço de reembolsos só precisa de carregar uma encomenda. Declara uma interface de um só método para isso e recebe-a no construtor:

example.gogo
type orderGetter interface {
	GetOrder(ctx context.Context, id int64) (Order, error)
}

type RefundService struct {
	orders orderGetter
}

func NewRefundService(orders orderGetter) *RefundService {
	return &RefundService{orders: orders}
}

func (s *RefundService) CanRefund(ctx context.Context, id int64) (bool, error) {
	o, err := s.orders.GetOrder(ctx, id)
	if err != nil {
		return false, fmt.Errorf("load order %d: %w", id, err)
	}
	return o.Status == "paid", nil
}

Em produção passas o store do Postgres, que tem mais vinte métodos que o serviço de reembolsos nunca vê. Num teste, basta um map:

example.gogo
type fakeOrders map[int64]Order

func (f fakeOrders) GetOrder(ctx context.Context, id int64) (Order, error) {
	o, ok := f[id]
	if !ok {
		return Order{}, ErrNotFound
	}
	return o, nil
}

func main() {
	svc := NewRefundService(fakeOrders{1001: {ID: 1001, Status: "paid"}})
	fmt.Println(svc.CanRefund(context.Background(), 1001))
	fmt.Println(svc.CanRefund(context.Background(), 1002))
}
example.texttext
true <nil>
false load order 1002: order not found

O fakeOrders é um tipo map definido com um método (vê a palavra-chave type em Go), por isso o teste não precisa de uma biblioteca de mocks.

O que significa «aceita interfaces, devolve structs»?

É a outra metade da mesma ideia. As funções recebem interfaces como parâmetros, para que quem as chama possa passar qualquer coisa que encaixe. Os construtores devolvem tipos concretos como *RefundService ou *PostgresStore, para que quem os chama tenha acesso a todos os métodos e a todos os campos exportados, e possa decidir por si próprio de que interface pequena precisa.

Devolver uma interface a partir de um construtor esconde os métodos que a interface não lista e obriga quem chama a usar a única abstração que escolheste. A principal exceção é o error. As funções devolvem error em vez de *ValidationError, e a secção sobre nil mais abaixo explica porquê.

O que é o any em Go?

O any é um alias de interface{}, a interface sem métodos. Todos os tipos têm pelo menos zero métodos, por isso uma variável do tipo any pode guardar qualquer valor. O any foi acrescentado no Go 1.18, e as duas formas de escrever são intercambiáveis (especificação do Go).

O any aparece onde o tipo só é conhecido em tempo de execução. O fmt.Println(a ...any) recebe argumentos de qualquer tipo, e um json.Unmarshal para um map[string]any produz valores cujos tipos dependem do JSON.

O preço é que o compilador deixa de verificar. Cada leitura de um any precisa de uma type assertion, e um palpite errado passa a ser um erro em tempo de execução em vez de um erro de compilação. Quando uma função trabalha com vários tipos mas as mesmas operações se aplicam a todos, usa antes um parâmetro de tipo. func Sum[T Amount](values []T) T, explicado na secção sobre restrições mais abaixo, mantém a verificação de tipos completa, enquanto func Sum(values []any) any empurra todas as verificações para o tempo de execução.

Como funcionam as type assertions e os type switches?

Uma type assertion tira o valor concreto de dentro de uma interface. A forma com dois valores nunca entra em pânico:

example.gogo
var payload map[string]any
body := `{"user_id": 42, "email": "[email protected]", "roles": ["admin", "billing"], "manager": null}`
if err := json.Unmarshal([]byte(body), &payload); err != nil {
	log.Fatal(err)
}

email, ok := payload["email"].(string)
fmt.Println(email, ok)

id, ok := payload["user_id"].(int)
fmt.Println(id, ok)
fmt.Printf("%T\n", payload["user_id"])
example.texttext
[email protected] true
0 false
float64

A segunda assertion falha porque o encoding/json descodifica todos os números JSON como float64 quando o destino é any. Com o ok, a falha é apenas false e o valor zero. A forma com um só valor, payload["user_id"].(int), entra em pânico:

example.texttext
panic: interface conversion: interface {} is float64, not int

As assertions também funcionam com interfaces. r.(io.Writer) pergunta se o valor dentro de r tem um método Write, e a biblioteca padrão usa isto para procurar comportamento opcional. O io.Copy verifica se a origem é um io.WriterTo e, se for, usa o caminho mais rápido.

Um type switch executa várias assertions numa só instrução. Encaixa bem no mesmo payload JSON, onde um valor pode ser de um pequeno conjunto de tipos:

example.gogo
func describe(v any) string {
	switch v := v.(type) {
	case nil:
		return "null"
	case float64:
		return fmt.Sprintf("number %g", v)
	case string:
		return fmt.Sprintf("string of %d bytes", len(v))
	case []any:
		return fmt.Sprintf("array of %d items", len(v))
	case map[string]any:
		return fmt.Sprintf("object with %d keys", len(v))
	default:
		return fmt.Sprintf("other %T", v)
	}
}

func main() {
	// payload decoded as above
	for _, key := range []string{"user_id", "email", "roles", "manager"} {
		fmt.Println(key+":", describe(payload[key]))
	}
}
example.texttext
user_id: number 42
email: string of 15 bytes
roles: array of 2 items
manager: null

Em cada caso, o v tem o tipo desse caso, por isso len(v) compila para a string, o slice e o map. Os type switches sobre erros funcionam da mesma forma, mas só veem o erro mais exterior. A palavra-chave type em Go mostra esse caso e explica porque é que o errors.As costuma ser a melhor ferramenta para erros embrulhados.

Porque é que o meu erro nil não é nil em Go?

Um valor de interface tem duas partes, um tipo dinâmico e um valor dinâmico. Só é igual a nil quando as duas estão por definir. Se guardares um ponteiro nil numa interface, a parte do tipo fica definida, por isso a interface não é nil (FAQ do Go).

Isto apanha sobretudo quem usa tipos de erro personalizados. Aqui, a função mantém uma variável *ValidationError e devolve-a, quer lhe tenha sido atribuído um valor, quer não:

example.gogo
package main

import (
	"fmt"
	"strings"
)

type ValidationError struct {
	Field string
}

func (e *ValidationError) Error() string {
	return "invalid " + e.Field
}

func validate(email string) error {
	var verr *ValidationError
	if !strings.Contains(email, "@") {
		verr = &ValidationError{Field: "email"}
	}
	return verr
}

func main() {
	err := validate("[email protected]")
	if err != nil {
		fmt.Printf("validation failed: %v (%T)\n", err, err)
		return
	}
	fmt.Println("ok")
}
example.texttext
validation failed: <nil> (*main.ValidationError)

O email é válido e o verr é nil, mas o error devolvido guarda o tipo *main.ValidationError com um ponteiro nil lá dentro. O err != nil compara a interface inteira com uma interface sem tipo, por isso dá true. Quem chama rejeita um registo válido, e a linha de log diz <nil>, o que torna o bug difícil de detetar.

A correção é devolver um nil literal no caminho de sucesso e nunca fazer passar um ponteiro tipado por um retorno error:

example.gogo
func validate(email string) error {
	if !strings.Contains(email, "@") {
		return &ValidationError{Field: "email"}
	}
	return nil
}

Declara o tipo de retorno como error, não como *ValidationError, e devolve nil diretamente. Quem precisa do campo continua a obtê-lo com errors.As, que também encontra o erro depois de ter sido embrulhado:

example.gogo
err := fmt.Errorf("signup: %w", validate("ana.example.com"))
var verr *ValidationError
if errors.As(err, &verr) {
	fmt.Println("bad field:", verr.Field)
}

Isto imprime bad field: email. A mesma armadilha existe para qualquer interface. Um var store *PostgresStore que nunca é definido, passado onde se espera um orderGetter, também produz uma interface que não é nil.

O caso contrário é uma interface que é mesmo nil, como um campo de struct do tipo Notifier que ninguém definiu. Chamar um método sobre ela entra em pânico com invalid memory address or nil pointer dereference, porque não há nenhum tipo onde procurar o método. Um construtor que exige a dependência, como NewRefundService(orders), evita isso.

Como se usam as interfaces como restrições genéricas?

Desde o Go 1.18, uma interface pode listar tipos além de métodos. A lista chama-se conjunto de tipos (type set), e ~T significa «qualquer tipo cujo tipo subjacente seja T»:

example.gogo
type Cents int64

type Amount interface {
	~int64 | ~float64
}

func Sum[T Amount](values []T) T {
	var total T
	for _, v := range values {
		total += v
	}
	return total
}

func Dedupe[T comparable](ids []T) []T {
	seen := make(map[T]struct{}, len(ids))
	out := make([]T, 0, len(ids))
	for _, id := range ids {
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		out = append(out, id)
	}
	return out
}

func main() {
	fmt.Println(Sum([]Cents{4999, 1250, 300}))
	fmt.Println(Dedupe([]string{"u_42", "u_7", "u_42"}))
}
example.texttext
6549
[u_42 u_7]

O Amount permite o + porque todos os tipos do seu conjunto o suportam. O comparable é uma restrição pré-declarada para tipos que funcionam com ==, que é o que uma chave de map precisa. O compilador recusa um tipo fora do conjunto e diz qual é o problema:

example.texttext
./main.go:27:17: string does not satisfy Amount (string missing in ~int64 | ~float64)

Uma interface com uma lista de tipos só pode ser usada como restrição. Declarar uma variável desse tipo falha:

example.texttext
./main.go:10:8: cannot use type Amount outside a type constraint: interface contains type constraints

Na prática, o Go tem dois tipos de interface com a mesma palavra-chave. As interfaces só com métodos funcionam em todo o lado, como tipos de variáveis, parâmetros e restrições. As interfaces com listas de tipos só aparecem entre parênteses retos. A introdução aos genéricos no blog do Go descreve como os conjuntos de tipos unificam as duas.

Onde entra o LevelUpGo

O LevelUpGo ensina Go através de exercícios que executam código Go real no navegador. O Interfaces & Polymorphism aborda a satisfação implícita, os conjuntos de métodos, o any, as type assertions e a composição de interfaces. O Interface Design aborda interfaces pequenas, declará-las onde são usadas e evitar interfaces que só existem para os mocks. O Go Generics Masterclass aprofunda as restrições e os conjuntos de tipos. O Training Ground tem exercícios curtos e independentes sobre interfaces para praticares fora de um curso. Para as outras 24 palavras reservadas, vê Palavras-chave do Go: as 25 explicadas.

Perguntas frequentes

O interface é uma palavra-chave em Go?

Sim. O interface é uma das 25 palavras-chave reservadas do Go, por isso não o podes usar como nome de variável ou de função. Aparece em declarações de tipos interface como type Notifier interface { ... }, no literal interface{} e em restrições inline como [T interface{ ~int | ~string }].

O Go tem uma palavra-chave implements?

Não. Um tipo satisfaz uma interface simplesmente por ter todos os seus métodos com assinaturas correspondentes. Para que o compilador verifique que um tipo satisfaz uma interface, acrescenta var _ Notifier = (*SlackNotifier)(nil) junto ao tipo.

Qual é a diferença entre any e interface{} em Go?

Nenhuma. O any é um alias pré-declarado de interface{}, acrescentado no Go 1.18, por isso os dois são o mesmo tipo. O código mais recente usa any porque é mais curto, e o gofmt -r 'interface{} -> any' consegue reescrever código mais antigo.

Porque é que o Go diz "method has pointer receiver"?

O método está declarado em *T, por isso só um *T o tem. Tentaste usar um valor T como interface. Passa antes um ponteiro, como em &BatchNotifier{}, ou muda o método para um recetor de valor se ele não modificar o recetor.

Porque é que o meu erro não é nil quando devolvi um ponteiro nil?

Uma interface só é nil quando o tipo e o valor estão ambos por definir. Devolver um *ValidationError nil a partir de uma função cujo tipo de retorno é error define o tipo como *ValidationError, por isso err != nil é true. Devolve um nil literal no caminho de sucesso em vez de uma variável de ponteiro tipada.

As interfaces em Go podem ter campos?

Não. As interfaces só descrevem métodos, ou conjuntos de tipos quando são usadas como restrições. Se quem chama precisa de um valor, acrescenta um método getter como ID() string, ou aceita a struct concreta em vez de uma interface.

Fontes

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