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étodoNotifycorrespondente é umNotifier.- 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 aT. Atribuir umTfalha commethod 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 deinterface{}e guarda um valor de qualquer tipo. Recuperas o valor comv, ok := x.(T)ou com um type switch.- Uma interface só é
nilquando o tipo e o valor estão ambos por definir. Devolver um*ValidationErrornil comoerrordá-te umerr != nilque étrue. - As interfaces com listas de tipos como
~int64 | ~float64sã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.gogopackage 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.texttextslack #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.gogotype 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.gogotype 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.gogovar _ 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
Tcontém os métodos declarados com um recetor de valor(t T). - O conjunto de métodos de
*Tconté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.gogotype 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:
| Interface | Métodos | Implementada por |
|---|---|---|
io.Reader | Read | ficheiros, ligações de rede, corpos HTTP, leitores gzip, bytes.Buffer |
io.Writer | Write | ficheiros, http.ResponseWriter, hashes, bytes.Buffer |
error | Error | *fs.PathError, *json.SyntaxError, os teus próprios tipos de erro |
fmt.Stringer | String | qualquer tipo que controle a forma como o fmt o imprime |
http.Handler | ServeHTTP | routers, 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.gogotype 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.texttextpaid 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.gogotype 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.gogotype 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.texttexttrue <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.gogovar 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.texttextpanic: 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.gogofunc 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.texttextuser_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.gogopackage 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.texttextvalidation 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.gogofunc 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.gogoerr := 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.gogotype 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.texttext6549 [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
- The Go Programming Language Specification, Interface types: https://go.dev/ref/spec#Interface_types
- The Go Programming Language Specification, Method sets: https://go.dev/ref/spec#Method_sets
- The Go Programming Language Specification, Type assertions: https://go.dev/ref/spec#Type_assertions
- The Go Programming Language Specification, Type switches: https://go.dev/ref/spec#Type_switches
- Go FAQ, Why is my nil error value not equal to nil?: https://go.dev/doc/faq#nil_error
- The Go Blog, An Introduction To Generics: https://go.dev/blog/intro-generics
- Effective Go, Interfaces and other types: https://go.dev/doc/effective_go#interfaces_and_types
- Go Proverbs: https://go-proverbs.github.io/
- io package: https://pkg.go.dev/io
