Interfaces avançado
Esta aula aprofunda o uso de interfaces em Go, abordando type assertions, type switches, composição de interfaces e boas práticas. Você aprenderá a verificar tipos dinamicamente, combinar interfaces e escrever código mais flexível e seguro.
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
okpara 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
- Type assertions - A Tour of Go
- Type switches - A Tour of Go
- Effective Go: Interfaces
- Go Specification: Interface types
- Go Code Review Comments: Interfaces
- Package io: ReadWriter interface
Exercícios
-
Escreva uma função que recebe um valor
interface{}e retorna uma string descrevendo o tipo. Use type switch para cobrirint,string,boolefloat64. 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" } } -
Crie uma interface
Formacom métodoArea() float64. Depois crie tiposCirculoeRetanguloque implementem essa interface. Em seguida, escreva uma função que recebe um slice deFormae 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) } -
Componha duas interfaces:
Leitorcom métodoLer() stringeEscritorcom métodoEscrever(string). Crie uma interfaceLeitorEscritorque as combina. Implemente um tipoBufferque satisfaçaLeitorEscritore 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()) } -
Explique por que é importante usar a forma com
okem type assertions ao invés da forma simples (semok). Dê um exemplo de código que causaria pânico sem ook.✓ Resposta:A forma simples de type assertion (sem
ok) causa pânico se a asserção falhar. A forma comokretorna 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") } -
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.