O que é Type Switch e Por que Usar
Type switch é um mecanismo em Go que permite você determinar o tipo concreto de um valor em tempo de execução quando você trabalha com interfaces. Diferente de linguagens orientadas a objetos tradicionais onde você conhece o tipo em tempo de compilação, Go oferece uma forma elegante e segura de fazer essa discriminação através da instrução switch com uma sintaxe especial.
A principal razão para usar type switch é quando você trabalha com a interface interface{} ou com interfaces customizadas. Imagine que sua função recebe um valor genérico e você precisa executar ações diferentes baseado em seu tipo real. Type switch resolve isso de forma muito mais legível do que fazer múltiplas verificações com reflect ou type assertions sequenciais. É um padrão tão comum em Go que a linguagem ofereceu uma sintaxe própria para isso.
Type Switch na Prática: Sintaxe e Estrutura
Sintaxe Básica
A sintaxe de type switch é similar ao switch tradicional, mas em vez de comparar valores, você compara tipos:
package main
import "fmt"
func descreverValor(v interface{}) {
switch valor := v.(type) {
case int:
fmt.Printf("É um inteiro com valor: %d\n", valor)
case string:
fmt.Printf("É uma string com valor: %s\n", valor)
case float64:
fmt.Printf("É um float com valor: %f\n", valor)
case bool:
fmt.Printf("É um booleano com valor: %v\n", valor)
default:
fmt.Printf("Tipo desconhecido: %T\n", v)
}
}
func main() {
descreverValor(42)
descreverValor("hello")
descreverValor(3.14)
descreverValor(true)
}
A sintaxe v.(type) é exclusiva do type switch e só pode ser usada dentro de um switch. Note que a variável valor recebe automaticamente o valor convertido para o tipo específico do case. Se você não usar a variável, pode omitir: case int: sem a atribuição.
Casos Avançados
Type switch funciona perfeitamente com tipos customizados e interfaces. Aqui você vê como discriminar entre diferentes implementações de uma interface:
package main
import "fmt"
type Pagavel interface {
Pagar() string
}
type CartaoCredito struct {
numero string
}
func (c CartaoCredito) Pagar() string {
return "Pagamento com cartão " + c.numero
}
type Boleto struct {
codigo string
}
func (b Boleto) Pagar() string {
return "Pagamento com boleto " + b.codigo
}
type Bitcoin struct {
endereco string
}
func (b Bitcoin) Pagar() string {
return "Pagamento com Bitcoin " + b.endereco
}
func processarPagamento(p Pagavel) {
switch metodo := p.(type) {
case CartaoCredito:
fmt.Printf("Processando cartão: %s\n", metodo.numero)
fmt.Println("Cobrança realizada imediatamente")
case Boleto:
fmt.Printf("Processando boleto: %s\n", metodo.codigo)
fmt.Println("Prazo: 3 dias úteis")
case Bitcoin:
fmt.Printf("Processando Bitcoin: %s\n", metodo.endereco)
fmt.Println("Confirmação na blockchain")
default:
fmt.Println("Método de pagamento não suportado")
}
}
func main() {
processarPagamento(CartaoCredito{"1234-5678-9012-3456"})
processarPagamento(Boleto{"12345.67890 12345.678901 12345.678901 1 12345678901234"})
processarPagamento(Bitcoin{"1A1z7agoat2GPFH7F06FvDB7YPrZjZsSE"})
}
Neste exemplo, você vê que processarPagamento recebe uma interface Pagavel e usa type switch para fazer ações específicas para cada implementação. Isso é muito mais legível do que usar reflect.TypeOf() ou fazer multiple type assertions.
Padrões Comuns e Boas Práticas
Quando Usar Type Switch vs Polimorfismo
A tentação é grande de usar type switch quando você tem uma interface, mas na maioria dos casos você deveria confiar no polimorfismo (chamar métodos na interface diretamente). Use type switch apenas quando você realmente precisa de comportamentos radicalmente diferentes que não fazem sentido implementar como métodos na interface:
package main
import "fmt"
// ❌ Abordagem ruim - usando type switch desnecessariamente
type Animal interface {
Som() string
}
type Gato struct{}
func (g Gato) Som() string { return "Miau" }
type Cachorro struct{}
func (c Cachorro) Som() string { return "Au au" }
func fazerAnimalFalar(a Animal) {
// NÃO FAÇA ASSIM
switch a.(type) {
case Gato:
fmt.Println(a.Som())
case Cachorro:
fmt.Println(a.Som())
}
}
// ✅ Abordagem correta
func fazerAnimalFalarCorreto(a Animal) {
fmt.Println(a.Som())
}
func main() {
fazerAnimalFalarCorreto(Gato{})
}
O padrão correto é deixar a interface resolver. Use type switch para casos reais como serialização, logging com comportamento diferente, ou integração com sistemas que realmente precisam saber o tipo.
Tratando nil e Type Assertions Falhadas
Um caso edge importante: quando você quer verificar se um valor é nil ou se é um tipo específico:
package main
import "fmt"
func inspecionar(v interface{}) {
switch t := v.(type) {
case nil:
fmt.Println("Valor é nil")
case int:
fmt.Printf("Inteiro: %d\n", t)
case string:
fmt.Printf("String: %s\n", t)
default:
fmt.Printf("Tipo: %T\n", v)
}
}
func main() {
inspecionar(nil)
inspecionar(10)
inspecionar("hello")
inspecionar([]int{1, 2, 3})
}
O case nil é especial e detecta quando o valor é efetivamente nil. Isso é extremamente útil em APIs que retornam interface{}.
Exemplos Práticos de Uso no Mundo Real
Logger com Diferenciação de Tipos
Um caso de uso real é um logger que formata diferentes tipos de dados de formas distintas:
package main
import (
"fmt"
"time"
)
type LogEntry struct {
timestamp time.Time
message interface{}
}
func logArquivo(entry LogEntry) string {
switch msg := entry.message.(type) {
case error:
return fmt.Sprintf("[%s] ERROR: %v", entry.timestamp.Format("15:04:05"), msg)
case string:
return fmt.Sprintf("[%s] INFO: %s", entry.timestamp.Format("15:04:05"), msg)
case int:
return fmt.Sprintf("[%s] COUNT: %d", entry.timestamp.Format("15:04:05"), msg)
case map[string]interface{}:
return fmt.Sprintf("[%s] DATA: %v", entry.timestamp.Format("15:04:05"), msg)
default:
return fmt.Sprintf("[%s] UNKNOWN: %T = %v", entry.timestamp.Format("15:04:05"), entry.message, entry.message)
}
}
func main() {
logs := []LogEntry{
{time.Now(), "Sistema iniciado"},
{time.Now(), 42},
{time.Now(), fmt.Errorf("conexão recusada")},
{time.Now(), map[string]interface{}{"usuario": "joao", "acao": "login"}},
}
for _, log := range logs {
fmt.Println(logArquivo(log))
}
}
Aqui você vê type switch tratando diferentes tipos de mensagens de forma apropriada. Erros recebem tratamento especial, números são formatados como contadores, e maps são exibidos como dados estruturados.
Parser de Configuração Genérico
Outro exemplo real: um parser que precisa lidar com diferentes tipos de valores de configuração:
package main
import (
"fmt"
"strconv"
)
func converterValorConfig(chave string, valor interface{}) interface{} {
switch v := valor.(type) {
case string:
// Tenta converter string para tipos mais específicos
if v == "true" {
return true
} else if v == "false" {
return false
}
if num, err := strconv.Atoi(v); err == nil {
return num
}
return v
case float64:
// JSON desserializa números como float64
if v == float64(int(v)) {
return int(v)
}
return v
case []interface{}:
// Converte slice genérico para slice tipado
resultado := make([]string, len(v))
for i, item := range v {
resultado[i] = fmt.Sprintf("%v", item)
}
return resultado
default:
return v
}
}
func main() {
valores := map[string]interface{}{
"porta": 8080.0,
"debug": "true",
"timeout": "30",
"hosts": []interface{}{"localhost", "127.0.0.1"},
"versao": "1.2.3",
}
for chave, valor := range valores {
convertido := converterValorConfig(chave, valor)
fmt.Printf("%s: %v (tipo: %T)\n", chave, convertido, convertido)
}
}
Este padrão é comum ao trabalhar com JSON ou YAML onde tudo vem como interface{} e você precisa fazer conversões inteligentes baseado no tipo real.
Conclusão
Type switch é um recurso elegante de Go que resolve um problema específico: discriminar tipos em tempo de execução. Os três pontos principais que você deve levar para casa são: primeiro, use type switch apenas quando realmente precisa de comportamentos diferentes baseado no tipo — na maioria dos casos, polimorfismo através de interfaces é a solução correta. Segundo, type switch funciona perfeitamente com tipos customizados e interfaces, permitindo que você construa APIs genéricas que ainda mantêm segurança de tipos para casos específicos. Terceiro, padrões como conversão de tipos genéricos, logging estruturado e parsing de configurações se beneficiam enormemente dessa abordagem, tornando o código mais legível e mantível do que alternativas baseadas em reflect.