Interfaces são um dos pilares da programação em Go, permitindo definir contratos comportamentais sem acoplamento rígido. Diferente de linguagens como Java ou C#, Go adota uma abordagem única: a implementação de interfaces é implícita, ou seja, um tipo satisfaz uma interface automaticamente se ele implementar todos os métodos exigidos. Isso torna o código mais flexível e modular.

Nesta aula, vamos explorar desde a definição de interfaces até o uso da interface vazia (any) e como o polimorfismo é alcançado em Go. Ao final, você será capaz de criar sistemas que dependem de comportamentos, não de tipos concretos.

Definição

Uma interface em Go é um tipo abstrato que define um conjunto de assinaturas de métodos. Qualquer tipo que implemente todos esses métodos é considerado compatível com a interface. A sintaxe é simples: usamos a palavra-chave type seguida do nome da interface e a palavra interface, com os métodos dentro de chaves.

Por exemplo, podemos definir uma interface Animal que exige um método Falar() que retorna uma string:

type Animal interface {
    Falar() string
}

Qualquer tipo que tenha um método Falar() string automaticamente satisfaz a interface Animal. Não é necessário declarar explicitamente que o tipo implementa a interface; isso é verificado em tempo de compilação.

Interfaces podem conter zero ou mais métodos. Quanto mais métodos, mais específico é o contrato. É uma boa prática manter interfaces pequenas (princípio da segregação de interfaces).

Implementação implícita

Em Go, não usamos palavras-chave como implements. A implementação é implícita: se um tipo possui todos os métodos exigidos por uma interface, ele automaticamente satisfaz essa interface. Isso permite que tipos criados em pacotes diferentes implementem interfaces sem depender delas.

Vamos criar dois tipos que implementam a interface Animal:

type Cachorro struct{}

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

type Gato struct{}

func (g Gato) Falar() string {
    return "Miau"
}

Agora, qualquer função que aceite um parâmetro do tipo Animal pode receber tanto Cachorro quanto Gato:

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

func main() {
    cachorro := Cachorro{}
    gato := Gato{}
    Apresentar(cachorro)
    Apresentar(gato)
}

Isso demonstra o polimorfismo: o mesmo código funciona com diferentes tipos concretos, desde que eles implementem a interface. A implementação implícita permite que bibliotecas de terceiros implementem suas interfaces sem precisar importá-las.

Interface vazia (any)

A interface vazia, representada como interface{} ou, a partir do Go 1.18, como any, é uma interface que não exige nenhum método. Portanto, todos os tipos a satisfazem automaticamente. Ela é útil quando precisamos armazenar valores de tipos arbitrários, como em containers heterogêneos.

Exemplo de uso com any:

func PrintValue(v any) {
    fmt.Printf("Valor: %v, Tipo: %T\n", v, v)
}

func main() {
    PrintValue(42)
    PrintValue("hello")
    PrintValue(3.14)
}

No entanto, ao usar any, perdemos a segurança de tipos. Para recuperar o valor original, precisamos fazer uma type assertion ou usar type switches. Exemplo de type assertion:

var x any = 10
numero, ok := x.(int)
if ok {
    fmt.Println("É um inteiro:", numero)
} else {
    fmt.Println("Não é inteiro")
}

Type switch é uma construção que permite testar múltiplos tipos:

switch v := x.(type) {
case int:
    fmt.Println("Inteiro:", v)
case string:
    fmt.Println("String:", v)
default:
    fmt.Println("Desconhecido")
}

Evite usar any desnecessariamente; prefira interfaces específicas para manter a segurança de tipos.

Polimorfismo

Polimorfismo é a capacidade de um objeto assumir diferentes formas. Em Go, ele é alcançado através de interfaces. Uma variável de tipo interface pode armazenar qualquer valor que implemente essa interface, e o método chamado será o correspondente ao tipo concreto armazenado.

Vamos estender o exemplo dos animais para incluir um método Andar() e demonstrar polimorfismo com uma slice de interfaces:

type Animal interface {
    Falar() string
    Andar() string
}

type Cachorro struct{}

func (c Cachorro) Falar() string { return "Au au" }
func (c Cachorro) Andar() string { return "Cachorro andando" }

type Gato struct{}

func (g Gato) Falar() string { return "Miau" }
func (g Gato) Andar() string { return "Gato andando" }

type Passaro struct{}

func (p Passaro) Falar() string { return "Piu piu" }
func (p Passaro) Andar() string { return "Pássaro voando" }

func main() {
    animais := []Animal{Cachorro{}, Gato{}, Passaro{}}
    for _, a := range animais {
        fmt.Println(a.Falar(), "-", a.Andar())
    }
}

Isso imprime comportamentos diferentes para cada animal, demonstrando polimorfismo. O tipo Animal é a interface que abstrai o comportamento comum.

Outro exemplo comum é a interface error da biblioteca padrão, que permite que qualquer tipo que implemente o método Error() string seja tratado como erro. Isso possibilita criar tipos de erro customizados.

Boas práticas: projete interfaces pequenas e focadas em comportamento. A biblioteca padrão do Go segue esse princípio, como io.Reader e io.Writer.

Referências

Exercícios

  1. Crie uma interface Forma com um método Area() float64. Implemente-a para os tipos Retangulo (com campos Largura e Altura) e Circulo (com campo Raio). Escreva uma função que receba uma Forma e imprima sua área.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    type Forma interface {
        Area() float64
    }
    
    type Retangulo struct {
        Largura, Altura float64
    }
    
    func (r Retangulo) Area() float64 {
        return r.Largura * r.Altura
    }
    
    type Circulo struct {
        Raio float64
    }
    
    func (c Circulo) Area() float64 {
        return 3.14159 * c.Raio * c.Raio
    }
    
    func ImprimirArea(f Forma) {
        fmt.Printf("Área: %v\n", f.Area())
    }
    
    func main() {
        r := Retangulo{Largura: 3, Altura: 4}
        c := Circulo{Raio: 5}
        ImprimirArea(r)
        ImprimirArea(c)
    }
  2. Explique por que a seguinte afirmação é verdadeira: "Em Go, interfaces são implementadas implicitamente". Dê um exemplo.

    ✓ Resposta:Em Go, não é necessário declarar explicitamente que um tipo implementa uma interface. Basta que o tipo possua todos os métodos exigidos pela interface. O compilador verifica isso automaticamente. Exemplo: a interface Stringer da biblioteca padrão exige o método String() string. Qualquer tipo que tenha esse método implementa Stringer sem precisar de uma declaração implements.
  3. O que é a interface vazia (any) e quando devemos utilizá-la? Cite um exemplo de uso adequado e outro inadequado.

    ✓ Resposta:A interface vazia (any) não exige nenhum método, portanto todos os tipos a implementam. Deve ser usada quando precisamos armazenar valores de tipos arbitrários, como em funções de logging genéricas ou containers heterogêneos. Exemplo adequado: fmt.Println usa any. Exemplo inadequado: usar any para parâmetros de funções que poderiam ser tipados, pois perde segurança de tipos.
  4. Escreva um programa que utilize uma interface Imprimivel com método Imprimir() string. Crie dois tipos concretos que implementem essa interface e uma função que receba um slice de Imprimivel e imprima todos os elementos.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    type Imprimivel interface {
        Imprimir() string
    }
    
    type Documento struct {
        Conteudo string
    }
    
    func (d Documento) Imprimir() string {
        return d.Conteudo
    }
    
    type Foto struct {
        Nome string
    }
    
    func (f Foto) Imprimir() string {
        return "[Foto: " + f.Nome + "]"
    }
    
    func ImprimirTudo(itens []Imprimivel) {
        for _, item := range itens {
            fmt.Println(item.Imprimir())
        }
    }
    
    func main() {
        itens := []Imprimivel{
            Documento{Conteudo: "Texto do documento"},
            Foto{Nome: "praia.jpg"},
        }
        ImprimirTudo(itens)
    }
  5. Como o polimorfismo é alcançado em Go? Compare com linguagens como Java ou C++.

    ✓ Resposta:Em Go, o polimorfismo é alcançado através de interfaces. Uma variável de tipo interface pode armazenar qualquer valor que implemente essa interface, e a chamada de método é resolvida dinamicamente (dispatch virtual). Diferente de Java (herança de classe) ou C++ (herança e funções virtuais), Go não possui herança de classes; o polimorfismo é baseado em composição e interfaces implícitas. Isso torna o código mais flexível e menos acoplado.