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 forma r, 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.

golanginterfacedesigntesting