Interfaces: contratos implícitos

1. O que são interfaces em Go?

Em Go, uma interface define um contrato comportamental — um conjunto de métodos que um tipo deve implementar para satisfazer a interface. Diferente de linguagens como Java ou C#, Go não exige que você declare explicitamente que um tipo implementa uma interface. A implementação é implícita: se um tipo possui todos os métodos declarados na interface, ele automaticamente a satisfaz.

A sintaxe básica é simples:

type Animal interface {
    EmitirSom() string
}

Qualquer tipo que tenha um método EmitirSom() string automaticamente implementa Animal.

2. Implementação implícita: o coração das interfaces

A implementação implícita é o que torna as interfaces em Go tão poderosas. Vejamos um exemplo prático:

package main

import "fmt"

type Animal interface {
    EmitirSom() string
}

type Cachorro struct {
    Nome string
}

func (c Cachorro) EmitirSom() string {
    return "Au au!"
}

type Gato struct {
    Nome string
}

func (g Gato) EmitirSom() string {
    return "Miau!"
}

func Apresentar(a Animal) {
    fmt.Println(a.EmitirSom())
}

func main() {
    cachorro := Cachorro{Nome: "Rex"}
    gato := Gato{Nome: "Mimi"}

    Apresentar(cachorro) // Au au!
    Apresentar(gato)     // Miau!
}

Note que nem Cachorro nem Gato declaram explicitamente que implementam Animal. A satisfação é automática. Isso traz desacoplamento, flexibilidade e código mais limpo.

3. Composição de interfaces

Go permite compor interfaces através de embedding. Uma interface pode incorporar outras interfaces, formando contratos mais complexos:

package main

import (
    "fmt"
    "io"
    "strings"
)

type LeitorEscritor interface {
    io.Reader
    io.Writer
}

func ProcessarDados(rw LeitorEscritor, dados []byte) (int, error) {
    n, err := rw.Write(dados)
    if err != nil {
        return 0, err
    }

    buf := make([]byte, n)
    _, err = rw.Read(buf)
    if err != nil {
        return 0, err
    }

    fmt.Printf("Dados lidos: %s\n", buf)
    return n, nil
}

func main() {
    var buf strings.Builder
    ProcessarDados(&buf, []byte("Olá, Go!"))
}

A stdlib é rica em exemplos: io.ReadWriter, io.ReadCloser, io.WriteCloser são todas interfaces compostas.

4. Satisfação de interface e type assertions

Em tempo de execução, podemos verificar se um valor implementa uma interface específica usando type assertion:

package main

import "fmt"

type Stringer interface {
    String() string
}

func Descrever(v interface{}) {
    if s, ok := v.(Stringer); ok {
        fmt.Println("Implementa Stringer:", s.String())
    } else {
        fmt.Println("Não implementa Stringer")
    }
}

type Pessoa struct {
    Nome string
}

func (p Pessoa) String() string {
    return p.Nome
}

func main() {
    Descrever(Pessoa{Nome: "João"})  // Implementa Stringer: João
    Descrever(42)                     // Não implementa Stringer
}

O padrão valor.(TipoInterface) retorna dois valores: o valor convertido e um booleano indicando sucesso. Isso evita pânicos.

5. Interfaces nil vs valores concretos nil

Uma das armadilhas mais comuns em Go é a diferença entre uma interface com valor nil e uma interface que contém um ponteiro nil:

package main

import "fmt"

type Animal interface {
    EmitirSom() string
}

type Cachorro struct{}

func (c *Cachorro) EmitirSom() string {
    return "Au au!"
}

func VerificarAnimal(a Animal) {
    if a == nil {
        fmt.Println("Animal é nil")
    } else {
        fmt.Printf("Animal não é nil, tipo: %T, valor: %v\n", a, a)
    }
}

func main() {
    var a1 Animal          // interface nil
    var c *Cachorro = nil  // ponteiro nil

    VerificarAnimal(a1)    // Animal é nil

    var a2 Animal = c      // interface com valor concreto nil
    VerificarAnimal(a2)    // Animal não é nil, tipo: *main.Cachorro, valor: <nil>
}

Para evitar bugs, use reflexão ou type switch para verificar valores concretos nil:

import "reflect"

func IsNil(a Animal) bool {
    return a == nil || reflect.ValueOf(a).IsNil()
}

6. Type switches com interfaces

O type switch permite tratar diferentes tipos concretos que implementam a mesma interface:

package main

import (
    "fmt"
    "math"
)

type Forma interface {
    Area() float64
}

type Quadrado struct {
    Lado float64
}

func (q Quadrado) Area() float64 {
    return q.Lado * q.Lado
}

type Circulo struct {
    Raio float64
}

func (c Circulo) Area() float64 {
    return math.Pi * c.Raio * c.Raio
}

type Triangulo struct {
    Base   float64
    Altura float64
}

func (t Triangulo) Area() float64 {
    return (t.Base * t.Altura) / 2
}

func DescreverForma(f Forma) {
    switch v := f.(type) {
    case Quadrado:
        fmt.Printf("Quadrado de lado %.2f, área: %.2f\n", v.Lado, v.Area())
    case Circulo:
        fmt.Printf("Círculo de raio %.2f, área: %.2f\n", v.Raio, v.Area())
    case Triangulo:
        fmt.Printf("Triângulo de base %.2f e altura %.2f, área: %.2f\n", v.Base, v.Altura, v.Area())
    default:
        fmt.Printf("Forma desconhecida, área: %.2f\n", f.Area())
    }
}

func main() {
    formas := []Forma{
        Quadrado{Lado: 4},
        Circulo{Raio: 3},
        Triangulo{Base: 5, Altura: 6},
    }

    for _, f := range formas {
        DescreverForma(f)
    }
}

7. Boas práticas e padrões comuns

  • Interfaces pequenas e focadas: Siga o Princípio da Segregação de Interfaces (ISP). Uma interface com 1-3 métodos é ideal.
  • Nomeação com sufixo -er: Para interfaces de ação, use o sufixo -er: Stringer, Reader, Writer, Closer.
  • Prefira interfaces em parâmetros, tipos concretos em retornos: Aceite interfaces, retorne structs.
  • Não crie interfaces desnecessárias: Espere até que um padrão de uso justifique a criação de uma interface.

Exemplo de boa prática:

// Aceita interface, retorna struct
func Salvar(w io.Writer, dados []byte) error {
    _, err := w.Write(dados)
    return err
}

Referências