Nesta aula, vamos nos aprofundar em tópicos avançados de interfaces em Go. Você aprenderá como extrair e verificar tipos concretos de valores de interface usando type assertions, como usar type switches para lidar com múltiplos tipos de forma elegante, como compor interfaces para criar abstrações mais poderosas e, por fim, boas práticas para projetar e utilizar interfaces de maneira eficaz.

As interfaces em Go são uma ferramenta poderosa para polimorfismo e desacoplamento. Dominar esses conceitos avançados permitirá que você escreva código mais flexível, reutilizável e de fácil manutenção.

Type assertions

Uma type assertion é uma operação que permite acessar o valor concreto subjacente de um valor de interface. A sintaxe é valor.(Tipo), onde valor é uma interface e Tipo é o tipo concreto que você deseja extrair. Se a interface não contiver um valor do tipo especificado, a operação pode causar um pânico, a menos que você use a forma de duas variáveis.

A forma de duas variáveis v, ok := valor.(Tipo) retorna um booleano ok que indica se a asserção foi bem-sucedida, evitando pânico. Isso é útil para verificar dinamicamente o tipo de um valor.

package main

import "fmt"

func main() {
    var i interface{} = "hello"

    // Type assertion com verificação
    s, ok := i.(string)
    if ok {
        fmt.Println("Valor é string:", s)
    } else {
        fmt.Println("Valor não é string")
    }

    // Type assertion sem verificação (pode causar pânico)
    // s2 := i.(string) // seguro, pois i é string
    // n := i.(int)     // pânico!

    // Exemplo com múltiplos tipos
    var val interface{} = 42
    if str, ok := val.(string); ok {
        fmt.Println("É string:", str)
    } else if num, ok := val.(int); ok {
        fmt.Println("É int:", num)
    } else {
        fmt.Println("Tipo desconhecido")
    }
}

Type switch

Um type switch é uma construção que permite comparar o tipo concreto de uma interface com vários tipos possíveis. A sintaxe é switch v := valor.(type) { ... }. Dentro de cada case, a variável v é automaticamente convertida para o tipo correspondente, permitindo acesso direto aos seus campos e métodos.

Type switches são mais limpos e seguros do que uma sequência de type assertions. Eles são especialmente úteis quando você precisa tratar diferentes tipos de maneira específica, como em funções que processam entradas variadas.

package main

import "fmt"

func printType(val interface{}) {
    switch v := val.(type) {
    case string:
        fmt.Println("String:", v)
    case int:
        fmt.Println("Int:", v)
    case float64:
        fmt.Println("Float64:", v)
    default:
        fmt.Println("Tipo desconhecido")
    }
}

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

Composição de interfaces

Go permite compor interfaces a partir de outras interfaces, criando interfaces maiores que exigem a implementação de todos os métodos das interfaces incorporadas. Isso é feito simplesmente listando as interfaces dentro da definição da nova interface, sem métodos adicionais. A composição promove reutilização e permite construir abstrações hierárquicas.

Por exemplo, podemos ter uma interface Reader e Writer e compô-las em ReadWriter. Qualquer tipo que implemente ambos os conjuntos de métodos automaticamente satisfaz a interface composta.

package main

import "fmt"

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// Composição de interfaces
type ReadWriter interface {
    Reader
    Writer
}

// Implementação concreta
type MyReadWriter struct{}

func (m MyReadWriter) Read(p []byte) (n int, err error) {
    return len(p), nil
}

func (m MyReadWriter) Write(p []byte) (n int, err error) {
    return len(p), nil
}

func main() {
    var rw ReadWriter = MyReadWriter{}
    fmt.Println(rw)
}

Boas práticas

Ao trabalhar com interfaces em Go, algumas práticas recomendadas incluem: definir interfaces pequenas e focadas (princípio da segregação de interfaces), aceitar interfaces e retornar structs (para flexibilidade e simplicidade), e usar interfaces apenas quando necessário (evitar abstrações prematuras).

Além disso, prefira type switches a longas cadeias de type assertions, e evite usar interfaces vazias (interface{}) a menos que seja realmente necessário (por exemplo, para funções genéricas). Lembre-se de que interfaces em Go são satisfeitas implicitamente; documente bem os contratos esperados.

package main

import "fmt"

// Boa prática: interface pequena e focada
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() {
    var s Stringer = Person{"Alice", 30}
    fmt.Println(s.String())
}

Referências

Exercícios

  1. Escreva uma função que receba um valor do tipo interface{} e retorne uma string descrevendo o tipo: "string", "int", "float64", "bool" ou "outro". Use type switch.

    ✓ Resposta:
    func describeType(val interface{}) string {
        switch val.(type) {
        case string:
            return "string"
        case int:
            return "int"
        case float64:
            return "float64"
        case bool:
            return "bool"
        default:
            return "outro"
        }
    }
  2. Crie uma interface Shape com método Area() float64. Depois, crie uma interface Perimeter com método Perimeter() float64. Componha ambas em Geometry. Implemente essas interfaces para um tipo Rectangle (com campos Largura e Altura).

    ✓ Resposta:
    type Shape interface {
        Area() float64
    }
    
    type Perimeter interface {
        Perimeter() float64
    }
    
    type Geometry interface {
        Shape
        Perimeter
    }
    
    type Rectangle struct {
        Width, Height float64
    }
    
    func (r Rectangle) Area() float64 {
        return r.Width * r.Height
    }
    
    func (r Rectangle) Perimeter() float64 {
        return 2 * (r.Width + r.Height)
    }
  3. Escreva um programa que use type assertion para verificar se um valor interface{} é um int e, se for, imprima seu quadrado. Use a forma segura (com ok).

    ✓ Resposta:
    package main
    
    import "fmt"
    
    func main() {
        var val interface{} = 5
        if num, ok := val.(int); ok {
            fmt.Println(num * num)
        } else {
            fmt.Println("não é int")
        }
    }
  4. Explique por que é preferível usar type switch em vez de múltiplas type assertions quando se tem vários tipos possíveis. Dê um exemplo de código que ilustre a diferença.

    ✓ Resposta:

    Type switch é mais limpo e menos repetitivo. Com múltiplas type assertions, você precisa escrever várias verificações if-else e repetir a variável. Exemplo:

    // Com type switch
    func process(val interface{}) {
        switch v := val.(type) {
        case string:
            fmt.Println("String:", v)
        case int:
            fmt.Println("Int:", v)
        }
    }
    
    // Com múltiplas type assertions
    func process2(val interface{}) {
        if v, ok := val.(string); ok {
            fmt.Println("String:", v)
        } else if v, ok := val.(int); ok {
            fmt.Println("Int:", v)
        }
    }
  5. Implemente uma função que aceite um io.Writer e escreva nele uma mensagem de saudação. Em seguida, chame essa função com os.Stdout e com um buffer de string (bytes.Buffer). Dica: use a interface io.Writer da biblioteca padrão.

    ✓ Resposta:
    package main
    
    import (
        "bytes"
        "fmt"
        "io"
        "os"
    )
    
    func greet(w io.Writer) {
        fmt.Fprintln(w, "Olá, mundo!")
    }
    
    func main() {
        greet(os.Stdout)
    
        var buf bytes.Buffer
        greet(&buf)
        fmt.Print(buf.String())
    }