Interfaces em Go são uma ferramenta poderosa para abstração e polimorfismo. Nesta aula, vamos explorar conceitos avançados como type assertions e type switches, que permitem trabalhar com tipos dinâmicos de forma segura. Também veremos como compor interfaces para criar abstrações mais complexas e discutiremos boas práticas para usar interfaces de maneira eficiente.

Dominar esses tópicos é essencial para escrever código Go idiomático e flexível, especialmente em bibliotecas e frameworks. Vamos começar!

Type assertions

Type assertion é uma operação que permite acessar o valor concreto subjacente de uma interface. A sintaxe é valor, ok := interface.(Tipo), onde T é o tipo que você espera que o valor implemente. Se a asserção falhar, ok será false (sem pânico).

Type assertions são úteis quando você precisa executar operações específicas de um tipo concreto que não estão disponíveis na interface. Por exemplo, se você tem uma interface error e quer acessar detalhes de um tipo de erro específico, como *os.PathError.

package main

import (
	"fmt"
	"os"
)

func main() {
	var err error = &os.PathError{Op: "open", Path: "/tmp/file", Err: os.ErrNotExist}

	// Type assertion para *os.PathError
	if pe, ok := err.(*os.PathError); ok {
		fmt.Printf("PathError: Op=%s, Path=%s, Err=%v\n", pe.Op, pe.Path, pe.Err)
	} else {
		fmt.Println("Not a PathError")
	}
}

No exemplo acima, extraímos o tipo concreto *os.PathError da interface error. Se a asserção falhar (por exemplo, se err for um outro tipo de erro), o ok será falso e evitamos pânico.

Type switch

Type switch é uma estrutura de controle que permite testar o tipo concreto de uma interface contra múltiplos tipos. É como um switch normal, mas os cases são tipos. A sintaxe usa interface.(type) na expressão do switch.

Type switches são ideais quando você precisa lidar com vários tipos possíveis de forma limpa e legível. Eles são frequentemente usados em funções que aceitam interface{} (qualquer tipo) e precisam tratar cada tipo de forma diferente.

package main

import "fmt"

func describe(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Printf("Inteiro: %d\n", v)
	case string:
		fmt.Printf("String: %s\n", v)
	case bool:
		fmt.Printf("Booleano: %t\n", v)
	default:
		fmt.Printf("Tipo desconhecido: %T\n", v)
	}
}

func main() {
	describe(42)
	describe("hello")
	describe(true)
	describe(3.14)
}

A variável v em cada case já é do tipo correspondente, então você pode usar métodos ou campos específicos sem precisar de type assertion adicional. O case default captura qualquer tipo não listado.

Composição de interfaces

Em Go, você pode compor interfaces maiores a partir de interfaces menores. Isso é feito simplesmente incluindo uma interface dentro de outra na declaração. A nova interface herda todos os métodos das interfaces componentes.

Composição de interfaces promove reuso e modularidade. Por exemplo, a interface io.ReadWriter é composta de io.Reader e io.Writer. Você pode criar suas próprias combinações.

package main

import "fmt"

type Animal interface {
	Som() string
}

type Movimento interface {
	Mover() string
}

type SerVivo interface {
	Animal
	Movimento
}

type Cachorro struct{}

func (c Cachorro) Som() string { return "Au Au" }
func (c Cachorro) Mover() string { return "Correndo" }

func main() {
	var s SerVivo = Cachorro{}
	fmt.Println(s.Som())
	fmt.Println(s.Mover())
}

No exemplo, SerVivo combina as interfaces Animal e Movimento. Qualquer tipo que implemente todos os métodos das interfaces componentes satisfaz SerVivo. Isso é poderoso para construir abstrações enxutas.

Boas práticas

Ao usar interfaces em Go, siga estas boas práticas:

  • Interfaces pequenas e focadas: Prefira interfaces com um ou poucos métodos (ex.: io.Reader, fmt.Stringer). Isso facilita a implementação e reutilização.
  • Defina interfaces onde são usadas: Coloque a interface no pacote consumidor, não no produtor. Isso evita dependências desnecessárias.
  • Evite interfaces grandes: Se uma interface tem muitos métodos, considere quebrá-la em interfaces menores e compô-las.
  • Use type assertions com cuidado: Prefira type switches quando houver múltiplos tipos. Sempre use a forma com ok para evitar pânico.
  • Não exporte interfaces desnecessariamente: Se uma interface só é usada internamente, mantenha-a privada.
  • Prefira receber interfaces e retornar structs: Isso promove flexibilidade na entrada e simplicidade na saída.

Essas práticas tornam seu código mais idiomático e fácil de manter.

Referências

Exercícios

  1. Escreva uma função que recebe um valor interface{} e retorna uma string descrevendo o tipo. Use type switch para cobrir int, string, bool e float64. Para outros tipos, retorne "tipo desconhecido".

    ✓ Resposta:
    func describeType(i interface{}) string {
        switch i.(type) {
        case int:
            return "int"
        case string:
            return "string"
        case bool:
            return "bool"
        case float64:
            return "float64"
        default:
            return "tipo desconhecido"
        }
    }
  2. Crie uma interface Forma com método Area() float64. Depois crie tipos Circulo e Retangulo que implementem essa interface. Em seguida, escreva uma função que recebe um slice de Forma e imprime a área de cada uma.

    ✓ Resposta:
    package main
    
    import (
        "fmt"
        "math"
    )
    
    type Forma interface {
        Area() float64
    }
    
    type Circulo struct {
        Raio float64
    }
    
    func (c Circulo) Area() float64 {
        return math.Pi * c.Raio * c.Raio
    }
    
    type Retangulo struct {
        Largura, Altura float64
    }
    
    func (r Retangulo) Area() float64 {
        return r.Largura * r.Altura
    }
    
    func imprimirAreas(formas []Forma) {
        for _, f := range formas {
            fmt.Printf("Área: %.2f\n", f.Area())
        }
    }
    
    func main() {
        formas := []Forma{Circulo{Raio: 5}, Retangulo{Largura: 3, Altura: 4}}
        imprimirAreas(formas)
    }
  3. Componha duas interfaces: Leitor com método Ler() string e Escritor com método Escrever(string). Crie uma interface LeitorEscritor que as combina. Implemente um tipo Buffer que satisfaça LeitorEscritor e teste.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    type Leitor interface {
        Ler() string
    }
    
    type Escritor interface {
        Escrever(s string)
    }
    
    type LeitorEscritor interface {
        Leitor
        Escritor
    }
    
    type Buffer struct {
        conteudo string
    }
    
    func (b *Buffer) Ler() string {
        return b.conteudo
    }
    
    func (b *Buffer) Escrever(s string) {
        b.conteudo = s
    }
    
    func main() {
        var le LeitorEscritor = &Buffer{}
        le.Escrever("Olá, mundo!")
        fmt.Println(le.Ler())
    }
  4. Explique por que é importante usar a forma com ok em type assertions ao invés da forma simples (sem ok). Dê um exemplo de código que causaria pânico sem o ok.

    ✓ Resposta:

    A forma simples de type assertion (sem ok) causa pânico se a asserção falhar. A forma com ok retorna um booleano indicando sucesso, permitindo tratamento seguro. Exemplo que causa pânico:

    var i interface{} = "hello"
    numero := i.(int) // pânico: interface conversion: string is not int
    fmt.Println(numero)

    Com ok, evitamos o pânico:

    if numero, ok := i.(int); ok {
        fmt.Println(numero)
    } else {
        fmt.Println("não é int")
    }
  5. Refatore o código abaixo para usar type switch ao invés de múltiplos type assertions:

    func process(v interface{}) {
        if s, ok := v.(string); ok {
            fmt.Println("String:", s)
        }
        if i, ok := v.(int); ok {
            fmt.Println("Int:", i)
        }
        if b, ok := v.(bool); ok {
            fmt.Println("Bool:", b)
        }
    }

    ✓ Resposta:
    func process(v interface{}) {
        switch val := v.(type) {
        case string:
            fmt.Println("String:", val)
        case int:
            fmt.Println("Int:", val)
        case bool:
            fmt.Println("Bool:", val)
        default:
            fmt.Println("Tipo desconhecido")
        }
    }

Observações finais: Interfaces avançadas são a base para escrever código genérico e reutilizável em Go. Pratique type assertions e type switches para manipular tipos dinâmicos com segurança. A composição de interfaces permite construir abstrações elegantes. Lembre-se das boas práticas para manter seu código limpo e idiomático.