Entendendo Interfaces em Go
Interfaces são um dos recursos mais poderosos do Go. Elas permitem escrever código flexível, desacoplado e altamente testável. Vamos explorar como funcionam e como usá-las efetivamente.
O Que São Interfaces?
Uma interface em Go é um tipo que especifica um conjunto de métodos. Qualquer tipo que implemente esses métodos automaticamente satisfaz a interface - não há necessidade de declaração explícita.
// Interface simples
type Speaker interface {
Speak() string
}
// Tipos que implementam Speaker
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return "Au au!"
}
type Cat struct {
Name string
}
func (c Cat) Speak() string {
return "Miau!"
}
// Ambos Dog e Cat implementam Speaker implicitamente
Duck Typing
Go usa "duck typing": se parece um pato e faz quack como um pato, então é um pato.
func MakeSound(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
dog := Dog{Name: "Rex"}
cat := Cat{Name: "Mimi"}
MakeSound(dog) // Au au!
MakeSound(cat) // Miau!
}
Interface Vazia
A interface vazia any (alias para interface{}) pode conter qualquer tipo:
func PrintAnything(v any) {
fmt.Printf("Tipo: %T, Valor: %v\n", v, v)
}
PrintAnything(42) // Tipo: int, Valor: 42
PrintAnything("texto") // Tipo: string, Valor: texto
PrintAnything([]int{1,2}) // Tipo: []int, Valor: [1 2]
Type Assertions
Para recuperar o tipo concreto de uma interface:
func Process(v any) {
// Type assertion com verificação
str, ok := v.(string)
if ok {
fmt.Println("É uma string:", str)
return
}
// Type switch para múltiplos tipos
switch val := v.(type) {
case int:
fmt.Println("É um int:", val)
case float64:
fmt.Println("É um float:", val)
default:
fmt.Println("Tipo desconhecido")
}
}
Interfaces da Biblioteca Padrão
Go tem várias interfaces importantes na biblioteca padrão:
io.Reader e io.Writer
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
Exemplo de uso:
func CopyData(w io.Writer, r io.Reader) error {
buf := make([]byte, 1024)
for {
n, err := r.Read(buf)
if err == io.EOF {
break
}
if err != nil {
return err
}
_, err = w.Write(buf[:n])
if err != nil {
return err
}
}
return nil
}
// Funciona com qualquer Reader/Writer
// Arquivos, conexões de rede, buffers, etc.
Stringer
type Stringer interface {
String() string
}
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s (%d anos)", p.Name, p.Age)
}
func main() {
p := Person{Name: "Paulo", Age: 30}
fmt.Println(p) // Paulo (30 anos)
}
error
type error interface {
Error() string
}
// Erro customizado
type ValidationError struct {
Field string
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("validação falhou em %s: %s", e.Field, e.Message)
}
Composição de Interfaces
Interfaces podem ser compostas:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// Composição
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
Interfaces para Testabilidade
Um dos principais usos de interfaces é facilitar testes:
// Interface para serviço de email
type EmailSender interface {
Send(to, subject, body string) error
}
// Implementação real
type SMTPSender struct {
host string
port int
}
func (s *SMTPSender) Send(to, subject, body string) error {
// Envia email via SMTP
return nil
}
// Mock para testes
type MockEmailSender struct {
SentEmails []struct {
To, Subject, Body string
}
}
func (m *MockEmailSender) Send(to, subject, body string) error {
m.SentEmails = append(m.SentEmails, struct {
To, Subject, Body string
}{to, subject, body})
return nil
}
// Caso de uso que usa a interface
type UserService struct {
emailSender EmailSender
}
func (s *UserService) RegisterUser(email string) error {
// ... lógica de registro
return s.emailSender.Send(email, "Bem-vindo!", "Obrigado por se registrar")
}
// Teste
func TestRegisterUser(t *testing.T) {
mock := &MockEmailSender{}
service := &UserService{emailSender: mock}
err := service.RegisterUser("teste@exemplo.com")
if err != nil {
t.Fatal(err)
}
if len(mock.SentEmails) != 1 {
t.Error("Deveria ter enviado um email")
}
if mock.SentEmails[0].To != "teste@exemplo.com" {
t.Error("Email enviado para destinatário errado")
}
}
Boas Práticas
1. Interfaces Pequenas
// Bom: interfaces pequenas e focadas
type Reader interface {
Read(p []byte) (n int, err error)
}
// Evite: interfaces grandes
type DoEverything interface {
Read() error
Write() error
Delete() error
Update() error
List() error
// ... muitos métodos
}
2. Aceite Interfaces, Retorne Tipos Concretos
// Bom
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
// Evite
func NewUserService(repo UserRepository) UserServiceInterface {
return &UserService{repo: repo}
}
3. Defina Interfaces no Consumidor
// Pacote consumer - define a interface que precisa
package consumer
type DataStore interface {
Get(key string) (string, error)
Set(key string, value string) error
}
type Service struct {
store DataStore
}
// Pacote producer - implementa sem conhecer a interface
package producer
type RedisStore struct {
// ...
}
func (r *RedisStore) Get(key string) (string, error) { /* ... */ }
func (r *RedisStore) Set(key string, value string) error { /* ... */ }
4. Use Type Embedding
type Logger interface {
Log(message string)
}
type EnhancedLogger struct {
Logger // Embeds Logger interface
prefix string
}
func (e *EnhancedLogger) LogWithPrefix(message string) {
e.Log(e.prefix + ": " + message)
}
Conclusão
Interfaces em Go são poderosas por sua simplicidade:
- Implícitas: Não precisa declarar que implementa
- Composíveis: Combine interfaces pequenas
- Testáveis: Facilita mocks e stubs
- Desacopladas: Reduz dependências entre pacotes
Use interfaces para definir comportamentos, não dados. Mantenha-as pequenas e focadas. Defina-as onde são consumidas, não onde são implementadas.