Interfaces em Go: pequenas, implícitas e usadas corretamente
1. Visão geral
Interfaces em Go diferem de Java ou C# em um ponto fundamental: não há a palavra-chave implements. Um tipo precisa apenas ter os métodos corretos para satisfazer implicitamente a interface. Essa característica leva a uma abordagem de design única: interfaces pequenas, declaradas onde são usadas, não onde são implementadas.
Após ler este artigo, você saberá como escrever interfaces ao estilo Go, usá-las para testar sem bibliotecas de mock, e evitar erros comuns como “nil que não é nil”.
2. Interfaces são satisfeitas implicitamente
Comece com uma interface para enviar notificações a clientes:
package main
import "fmt"
type Notifier interface {
Notify(customer, message string) error
}
type EmailNotifier struct {
From string
}
func (e EmailNotifier) Notify(customer, message string) error {
fmt.Printf("[email de %s] para %s: %s\n", e.From, customer, message)
return nil
}
type SMSNotifier struct{}
func (SMSNotifier) Notify(customer, message string) error {
fmt.Printf("[sms] para %s: %s\n", customer, message)
return nil
}
func main() {
notifiers := []Notifier{
EmailNotifier{From: "shop@nonla.dev"},
SMSNotifier{},
}
for _, n := range notifiers {
_ = n.Notify("An", "Pedido #42 foi confirmado")
}
}
Resultado:
[email de shop@nonla.dev] para An: Pedido #42 foi confirmado
[sms] para An: Pedido #42 foi confirmado
Em nenhum lugar em EmailNotifier ou SMSNotifier há menção a Notifier. O compilador verifica automaticamente: qualquer tipo que tenha o método Notify(string, string) error pode ser atribuído a uma variável do tipo Notifier.
A consequência importante é que o lado que implementa não precisa conhecer a interface. Você pode declarar uma nova interface para um tipo existente em uma biblioteca padrão ou de terceiros sem modificar o código deles.
3. Mantenha interfaces pequenas
A biblioteca padrão do Go é o melhor exemplo. io.Reader e io.Writer têm apenas um método cada:
type Writer interface {
Write(p []byte) (n int, err error)
}
Por ser tão pequena, muitos tipos a satisfazem: *os.File, *bytes.Buffer, *strings.Builder, http.ResponseWriter, *gzip.Writer… Uma função que aceita io.Writer funciona com todos esses tipos:
package main
import (
"fmt"
"io"
"os"
"strings"
)
type Order struct {
ID int
Customer string
Total int64
}
func WriteReceipt(w io.Writer, o Order) error {
_, err := fmt.Fprintf(w, "Pedido #%d - %s - %d VND\n", o.ID, o.Customer, o.Total)
return err
}
func main() {
order := Order{ID: 42, Customer: "An", Total: 350000}
_ = WriteReceipt(os.Stdout, order) // imprimir no terminal
var sb strings.Builder
_ = WriteReceipt(&sb, order) // escrever em memória
fmt.Printf("Tamanho do recibo: %d bytes\n", sb.Len())
}
Pedido #42 - An - 350000 VND
Tamanho do recibo: 26 bytes
WriteReceipt precisa apenas da capacidade de “escrever bytes”, então declara exatamente isso. Quer escrever em um arquivo, resposta HTTP ou arquivo compactado, você não precisa modificar a função.
Quando você precisa de vários comportamentos, componha interfaces pequenas em vez de escrever uma grande de uma vez:
type OrderFinder interface {
FindOrder(id int) (Order, error)
}
type OrderSaver interface {
SaveOrder(o Order) error
}
type OrderStore interface {
OrderFinder
OrderSaver
}
Uma função que exibe um pedido aceita apenas OrderFinder. Uma função que atualiza o status aceita OrderStore. Cada função deixa claro o que precisa, e criar fakes para testar fica muito mais simples.
4. Declare interfaces no lado do consumidor
Em Java, você geralmente escreve PaymentGateway e depois StripePaymentGateway implements PaymentGateway, um ao lado do outro. Em Go, a abordagem comum é o oposto: o package que implementa retorna um struct concreto, e o package que usa declara a interface que precisa.
Suponha que o package payment forneça um cliente com muitos métodos:
// package payment
type Client struct {
apiKey string
}
func NewClient(apiKey string) *Client { return &Client{apiKey: apiKey} }
func (c *Client) Charge(customer string, amount int64) (string, error) {
return fmt.Sprintf("txn-%s-%d", customer, amount), nil
}
func (c *Client) Refund(txnID string) error { return nil }
func (c *Client) ListTransactions() ([]string, error) { return nil, nil }
O package checkout precisa apenas cobrar, então declara uma interface de um método:
// package checkout
var ErrInvalidTotal = errors.New("total deve ser maior que 0")
type PaymentGateway interface {
Charge(customer string, amount int64) (string, error)
}
type Service struct {
gateway PaymentGateway
}
func NewService(gw PaymentGateway) *Service {
return &Service{gateway: gw}
}
func (s *Service) Checkout(o Order) (string, error) {
if o.Total <= 0 {
return "", ErrInvalidTotal
}
txnID, err := s.gateway.Charge(o.Customer, o.Total)
if err != nil {
return "", fmt.Errorf("pagamento do pedido #%d: %w", o.ID, err)
}
return txnID, nil
}
Quando inicializar em main:
svc := checkout.NewService(payment.NewClient("sk_test_123"))
txn, err := svc.Checkout(Order{ID: 42, Customer: "An", Total: 350000})
fmt.Println(txn, err)
txn-An-350000 <nil>
*payment.Client satisfaz checkout.PaymentGateway implicitamente. O package payment não importa checkout e nem precisa saber que ele existe.
Esse princípio é frequentemente resumido como “aceitar interfaces, retornar structs”: funções aceitam interfaces para flexibilidade, mas retornam tipos concretos para que o chamador possa usar todos os métodos e fields.
5. Teste com fakes, sem bibliotecas de mock
Interfaces pequenas no lado do consumidor facilitam muito o teste. Você apenas escreve um struct com exatamente um método:
package checkout
import (
"errors"
"testing"
)
type fakeGateway struct {
charged []int64
err error
}
func (f *fakeGateway) Charge(customer string, amount int64) (string, error) {
if f.err != nil {
return "", f.err
}
f.charged = append(f.charged, amount)
return "txn-test", nil
}
func TestCheckout_ChargesOrderTotal(t *testing.T) {
gw := &fakeGateway{}
svc := NewService(gw)
txn, err := svc.Checkout(Order{ID: 1, Customer: "An", Total: 200000})
if err != nil {
t.Fatalf("erro inesperado: %v", err)
}
if txn != "txn-test" {
t.Errorf("txn = %q, esperado %q", txn, "txn-test")
}
if len(gw.charged) != 1 || gw.charged[0] != 200000 {
t.Errorf("charged = %v, esperado [200000]", gw.charged)
}
}
func TestCheckout_RejectsZeroTotal(t *testing.T) {
gw := &fakeGateway{}
svc := NewService(gw)
_, err := svc.Checkout(Order{ID: 2, Customer: "Bình", Total: 0})
if !errors.Is(err, ErrInvalidTotal) {
t.Fatalf("err = %v, esperado ErrInvalidTotal", err)
}
if len(gw.charged) != 0 {
t.Errorf("não deve chamar Charge, mas charged = %v", gw.charged)
}
}
func TestCheckout_WrapsGatewayError(t *testing.T) {
gwErr := errors.New("cartão recusado")
svc := NewService(&fakeGateway{err: gwErr})
_, err := svc.Checkout(Order{ID: 3, Customer: "Chi", Total: 100000})
if !errors.Is(err, gwErr) {
t.Fatalf("err = %v, esperado %v encapsulado", err, gwErr)
}
}
Execute os testes:
go test ./checkout -v
=== RUN TestCheckout_ChargesOrderTotal
--- PASS: TestCheckout_ChargesOrderTotal (0.00s)
=== RUN TestCheckout_RejectsZeroTotal
--- PASS: TestCheckout_RejectsZeroTotal (0.00s)
=== RUN TestCheckout_WrapsGatewayError
--- PASS: TestCheckout_WrapsGatewayError (0.00s)
PASS
Se PaymentGateway tivesse dez métodos, o fake também precisaria implementar dez. Isso é o sinal mais claro de que a interface está muito grande.
6. Type assertion, type switch e verificação em tempo de compilação
6.1. Interfaces opcionais com type assertion
Às vezes você quer usar uma capacidade adicional se o tipo concreto a suportar. Go usa type assertion com dois valores:
type Refunder interface {
Refund(txnID string) error
}
func (s *Service) Cancel(txnID string) error {
r, ok := s.gateway.(Refunder)
if !ok {
return errors.New("gateway de pagamento não suporta reembolso")
}
return r.Refund(txnID)
}
Com *payment.Client, ok é true porque tem o método Refund. Com fakeGateway, ok é false e a função retorna um erro em vez de fazer panic. A biblioteca padrão usa muito essa técnica, por exemplo io.Copy verifica se o argumento é io.WriterTo para escolher um caminho mais rápido.
Atenção: A forma com um valor
r := s.gateway.(Refunder)fará panic se o tipo não corresponder. Sempre use a formar, ok := …a menos que você tenha certeza absoluta.
6.2. Type switch
Quando você precisa lidar com vários tipos diferentes, use type switch:
func describe(n Notifier) string {
switch v := n.(type) {
case EmailNotifier:
return "email de " + v.From
case SMSNotifier:
return "sms"
default:
return fmt.Sprintf("desconhecido: %T", v)
}
}
Se você achar que está escrevendo type switch sobre tipos de negócio em vários lugares, talvez você esteja faltando um método na interface.
6.3. Garantir satisfação em tempo de compilação
Como a satisfação é implícita, se alguém mudar a assinatura de um método, o erro só aparecerá no ponto de atribuição. Para pegar o erro mais cedo, no arquivo de implementação, adicione uma linha como:
var _ checkout.PaymentGateway = (*payment.Client)(nil)
Essa linha não cria valor algum em tempo de execução. Ela apenas força o compilador a verificar se *payment.Client satisfaz PaymentGateway. Use apenas quando o package de implementação realmente quer se comprometer com a interface. Caso contrário, criará uma importação cíclica desnecessária.
7. Erros comuns
7.1. Interface contendo nil pointer não é nil
Este é o erro clássico mais comum do Go:
type ValidationError struct{ Field string }
func (e *ValidationError) Error() string {
return "campo inválido: " + e.Field
}
func validate(o Order) error {
var verr *ValidationError
if o.Total <= 0 {
verr = &ValidationError{Field: "Total"}
}
return verr // ERRADO
}
func main() {
err := validate(Order{Total: 100})
fmt.Println(err == nil)
}
false
Um valor interface é composto por duas partes: tipo e valor. Uma interface é nil apenas quando ambas estão vazias. Aqui o tipo é *ValidationError e o valor é um ponteiro nil, então err != nil. A solução é retornar nil explicitamente:
func validate(o Order) error {
if o.Total <= 0 {
return &ValidationError{Field: "Total"}
}
return nil
}
7.2. Pointer receiver e method set
type AuditNotifier struct{ count int }
func (a *AuditNotifier) Notify(customer, message string) error {
a.count++
return nil
}
var n Notifier = AuditNotifier{} // erro de compilação
cannot use AuditNotifier{} (value of struct type AuditNotifier) as Notifier value
in variable declaration: AuditNotifier does not implement Notifier
(method Notify has pointer receiver)
Métodos com pointer receiver só pertencem ao method set de *AuditNotifier. Você precisa atribuir &AuditNotifier{}. O oposto é verdadeiro: métodos com value receiver funcionam com valor e ponteiro.
7.3. Escrever interfaces antes de precisar
Código com hábitos de Java frequentemente parece assim:
type OrderService interface {
Create(o Order) error
Cancel(id int) error
Find(id int) (Order, error)
// … mais 10 métodos aqui
}
type orderServiceImpl struct{ /* … */ }
func NewOrderService() OrderService { return &orderServiceImpl{} }
Essa abordagem tem três problemas. A interface tem apenas uma implementação. Fica no lado da implementação em vez do consumidor. O construtor retorna interface, então o chamador perde acesso ao tipo concreto. Retorne *OrderService como struct, e deixe cada package consumidor declarar sua própria interface pequena quando realmente precisar substituir, por exemplo para testar.
7.4. Abuso de any
any (alias para interface{}) aceita qualquer tipo, então o compilador não pode ajudar mais. A partir do Go 1.18, se você precisar de uma função que funcione com muitos tipos, considere generics primeiro:
func SumTotals[T interface{ GetTotal() int64 }](items []T) int64 {
var sum int64
for _, it := range items {
sum += it.GetTotal()
}
return sum
}
Use any apenas quando os dados realmente têm tipo desconhecido, como ao decodificar JSON arbitrário.
8. Conclusão
Interfaces em Go são poderosas precisamente porque são simples. Pontos para lembrar:
- Tipos satisfazem interfaces implicitamente, sem
implements. - Interfaces devem ser pequenas, idealmente um ou dois métodos. Quando precisar de mais, componha interfaces pequenas.
- Declare interfaces no lado do consumidor. Funções aceitam interfaces, construtores retornam structs.
- Interfaces pequenas facilitam teste com fakes escritos à mão, sem bibliotecas de mock.
- Tome cuidado com interface contendo ponteiro nil e com method set de pointer receiver.
Da próxima vez que for escrever uma interface, pergunte-se: “Quem a usa, e quais métodos eles realmente precisam?” A resposta geralmente é menos do que você pensa.