O que é Type em Go?

A palavra-chave type é um dos pilares do sistema de tipos de Go. Ela permite criar novos tipos nomeados, definir structs, declarar interfaces, criar aliases de tipos e estabelecer constraints para generics. O sistema de tipos de Go é fundamentalmente diferente de linguagens com herança de classes — em Go, tipos são compostos e combinados através de embedding e implementação implícita de interfaces.

Em Go, type não é apenas um mecanismo de nomenclatura. Criar um novo tipo com type gera um tipo distinto que não é intercambiável com o tipo original, mesmo que compartilhem a mesma representação em memória. Isso fornece segurança em tempo de compilação — o compilador impede misturas acidentais entre tipos semanticamente diferentes como Celsius e Fahrenheit, mesmo que ambos sejam float64 internamente.

Essa abordagem permite que Go ofereça tipagem forte com uma sintaxe leve, sem o peso de hierarquias de classe complexas. Combinado com interfaces implícitas e composição via embedding, o sistema de tipos de Go é expressivo o suficiente para modelar domínios complexos mantendo a simplicidade que define a linguagem.

Definição de tipo (type definition)

A forma mais comum de usar type é criar um novo tipo nomeado:

// Tipo baseado em tipo primitivo
type Celsius float64
type Fahrenheit float64
type UserID int64
type Email string

// O compilador impede misturas acidentais
var temp Celsius = 100
var fahr Fahrenheit = 212

// ERRO: cannot use temp (type Celsius) as type Fahrenheit
// fahr = temp // não compila!

// Conversão explícita necessária
fahr = Fahrenheit(temp*9/5 + 32) // OK

Métodos em tipos nomeados

Uma das vantagens de criar tipos nomeados é poder adicionar métodos a eles:

type Celsius float64

func (c Celsius) ParaFahrenheit() Fahrenheit {
    return Fahrenheit(c*9/5 + 32)
}

func (c Celsius) String() string {
    return fmt.Sprintf("%.1f°C", c)
}

temp := Celsius(100)
fmt.Println(temp)                // 100.0°C
fmt.Println(temp.ParaFahrenheit()) // 212.0°F

Isso não é possível com tipos primitivos diretamente — você não pode adicionar métodos a float64. Criar um tipo nomeado resolve essa limitação e torna o código mais expressivo e seguro.

Tipos para domínio (Domain Types)

type Dinheiro int64 // centavos para evitar floating point

func (d Dinheiro) String() string {
    reais := d / 100
    centavos := d % 100
    return fmt.Sprintf("R$ %d,%02d", reais, centavos)
}

func (d Dinheiro) Somar(outro Dinheiro) Dinheiro {
    return d + outro
}

preco := Dinheiro(1999) // R$ 19,99
desconto := Dinheiro(500) // R$ 5,00
fmt.Println(preco.Somar(-desconto)) // R$ 14,99

Definição de struct

A aplicação mais frequente de type é definir structs:

type Pessoa struct {
    Nome      string
    Email     Email
    Idade     int
    Ativo     bool
    CriadoEm time.Time
}

type Endereco struct {
    Rua    string
    Cidade string
    Estado string
    CEP    string
}

// Composição via embedding
type Cliente struct {
    Pessoa           // embedded — promove campos e métodos
    Endereco         // embedded
    LimiteCredito Dinheiro
}

cli := Cliente{
    Pessoa:        Pessoa{Nome: "Ana", Email: "ana@email.com"},
    Endereco:      Endereco{Cidade: "São Paulo", Estado: "SP"},
    LimiteCredito: Dinheiro(500000), // R$ 5.000,00
}
fmt.Println(cli.Nome)   // "Ana" — campo promovido de Pessoa
fmt.Println(cli.Cidade) // "São Paulo" — campo promovido de Endereco

Definição de interface

Interfaces são definidas com type e especificam um conjunto de métodos:

type Leitor interface {
    Ler(p []byte) (n int, err error)
}

type Escritor interface {
    Escrever(p []byte) (n int, err error)
}

// Composição de interfaces
type LeitorEscritor interface {
    Leitor
    Escritor
}

// Interface com métodos múltiplos
type Repositorio interface {
    Buscar(id int) (*Entidade, error)
    Salvar(e *Entidade) error
    Deletar(id int) error
    Listar(filtro Filtro) ([]*Entidade, error)
}

Em Go, interfaces são implementadas implicitamente — não há implements keyword. Qualquer tipo que possua todos os métodos de uma interface automaticamente a satisfaz.

Type alias vs type definition

Go tem duas formas de criar tipos com type, e elas são fundamentalmente diferentes:

// TYPE DEFINITION — cria tipo NOVO e distinto
type MeuInt int

// TYPE ALIAS — cria nome alternativo para o MESMO tipo
type MeuAlias = int

Diferenças práticas

type NovoTipo int    // tipo distinto
type Alias = int     // mesmo tipo

var a NovoTipo = 10
var b int = 10
var c Alias = 10

// a = b    // ERRO: tipos diferentes
a = NovoTipo(b) // OK: conversão explícita

// c = b    // OK: são o mesmo tipo
// b = c    // OK: são o mesmo tipo

// Métodos
func (n NovoTipo) Dobro() NovoTipo { return n * 2 } // OK
// func (a Alias) Dobro() Alias { ... } // ERRO: não pode adicionar método a tipo não-local

Quando usar alias

Type aliases são úteis em refatorações graduais e compatibilidade:

// Migração gradual — pacote antigo redireciona para novo
package antigo

import "projeto/novo"

// Alias mantém compatibilidade durante migração
type Config = novo.Config
type Server = novo.Server

Na biblioteca padrão, byte é um alias para uint8 e rune é alias para int32:

// Definidos na spec da linguagem
type byte = uint8
type rune = int32

Type assertion

Type assertion extrai o valor concreto de uma interface:

var i interface{} = "olá, Go!"

// Type assertion com panic se falhar
s := i.(string)
fmt.Println(s) // "olá, Go!"

// Type assertion segura com ok
n, ok := i.(int)
fmt.Println(n, ok) // 0, false — não é int

// Padrão idiomático
if s, ok := i.(string); ok {
    fmt.Printf("É string: %q\n", s)
}

Type assertions são essenciais ao trabalhar com interfaces genéricas como interface{} (ou any no Go 1.18+) e ao implementar padrões como unmarshaling customizado:

type Validador interface {
    Validar() error
}

func processar(dados interface{}) error {
    // Verificar se implementa interface opcional
    if v, ok := dados.(Validador); ok {
        if err := v.Validar(); err != nil {
            return fmt.Errorf("validação falhou: %w", err)
        }
    }
    // processar dados...
    return nil
}

Type switch

O type switch é uma extensão poderosa do select que permite branching baseado no tipo concreto:

func descrever(i interface{}) string {
    switch v := i.(type) {
    case int:
        return fmt.Sprintf("inteiro: %d", v)
    case string:
        return fmt.Sprintf("string: %q (len=%d)", v, len(v))
    case bool:
        if v {
            return "verdadeiro"
        }
        return "falso"
    case []int:
        return fmt.Sprintf("slice de %d inteiros", len(v))
    case nil:
        return "nil"
    default:
        return fmt.Sprintf("tipo desconhecido: %T", v)
    }
}

Type switch com interfaces

type Forma interface {
    Area() float64
}

type Circulo struct{ Raio float64 }
type Retangulo struct{ Largura, Altura float64 }
type Triangulo struct{ Base, Altura float64 }

func (c Circulo) Area() float64    { return math.Pi * c.Raio * c.Raio }
func (r Retangulo) Area() float64  { return r.Largura * r.Altura }
func (t Triangulo) Area() float64  { return t.Base * t.Altura / 2 }

func descreverForma(f Forma) string {
    switch forma := f.(type) {
    case Circulo:
        return fmt.Sprintf("Círculo com raio %.1f", forma.Raio)
    case Retangulo:
        return fmt.Sprintf("Retângulo %g×%g", forma.Largura, forma.Altura)
    case Triangulo:
        return fmt.Sprintf("Triângulo base=%.1f", forma.Base)
    default:
        return "Forma desconhecida"
    }
}

Type constraints (Generics — Go 1.18+)

Com a introdução de generics, type ganhou papel na definição de constraints:

// Constraint básica usando interface
type Numero interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~float32 | ~float64
}

func Somar[T Numero](valores []T) T {
    var soma T
    for _, v := range valores {
        soma += v
    }
    return soma
}

// Funciona com qualquer tipo numérico
fmt.Println(Somar([]int{1, 2, 3, 4, 5}))           // 15
fmt.Println(Somar([]float64{1.1, 2.2, 3.3}))       // 6.6

O operador ~ (tilde)

O ~ em constraints significa “qualquer tipo cujo tipo subjacente seja”:

type Ordenavel interface {
    ~int | ~string | ~float64
}

// Funciona com tipos nomeados também
type Idade int
type Nome string

func Menor[T Ordenavel](a, b T) T {
    if a < b {
        return a
    }
    return b
}

var idade1, idade2 Idade = 25, 30
fmt.Println(Menor(idade1, idade2)) // 25 — funciona com tipo Idade

Constraints com métodos

type Stringifier interface {
    String() string
}

type Comparable[T any] interface {
    CompareTo(T) int
}

func MaxPor[T Comparable[T]](itens []T) T {
    max := itens[0]
    for _, item := range itens[1:] {
        if item.CompareTo(max) > 0 {
            max = item
        }
    }
    return max
}

Underlying type (tipo subjacente)

Todo tipo em Go tem um tipo subjacente (underlying type). Para tipos predeclarados (int, string, etc.), o tipo subjacente é ele mesmo. Para tipos definidos com type, é o tipo na definição:

type MeuInt int           // underlying: int
type MeuSlice []string    // underlying: []string
type MeuMap map[string]int // underlying: map[string]int

// Conversão é possível entre tipos com mesmo underlying type
type Celsius float64
type Fahrenheit float64

c := Celsius(100)
f := Fahrenheit(c) // OK: ambos têm underlying float64

Esse conceito é crucial para entender quando conversões são possíveis e como constraints genéricos com ~ funcionam.

Type embedding (composição)

Go usa composição ao invés de herança. Type embedding é como Go “herda” comportamento:

type Animal struct {
    Nome string
    Idade int
}

func (a Animal) Apresentar() string {
    return fmt.Sprintf("Sou %s, tenho %d anos", a.Nome, a.Idade)
}

type Cachorro struct {
    Animal        // embedded — promove campos e métodos
    Raca string
}

rex := Cachorro{
    Animal: Animal{Nome: "Rex", Idade: 5},
    Raca:   "Labrador",
}

// Campos e métodos promovidos
fmt.Println(rex.Nome)         // "Rex" — de Animal
fmt.Println(rex.Apresentar()) // "Sou Rex, tenho 5 anos"
fmt.Println(rex.Raca)         // "Labrador" — próprio

Embedding funciona com interfaces também, permitindo composição de contratos:

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

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

type ReadWriter interface {
    Reader
    Writer
}

Padrões avançados com type

Enum pattern

Go não tem enums nativos, mas tipos nomeados com constantes simulam o padrão:

type Status int

const (
    StatusPendente Status = iota
    StatusAtivo
    StatusInativo
    StatusBloqueado
)

func (s Status) String() string {
    nomes := [...]string{"Pendente", "Ativo", "Inativo", "Bloqueado"}
    if int(s) < len(nomes) {
        return nomes[s]
    }
    return "Desconhecido"
}

func (s Status) EhValido() bool {
    return s >= StatusPendente && s <= StatusBloqueado
}

Functional options com type

type Opcao func(*Servidor)

type Servidor struct {
    host    string
    port    int
    timeout time.Duration
}

func ComTimeout(d time.Duration) Opcao {
    return func(s *Servidor) {
        s.timeout = d
    }
}

func ComPorta(port int) Opcao {
    return func(s *Servidor) {
        s.port = port
    }
}

func NovoServidor(host string, opts ...Opcao) *Servidor {
    s := &Servidor{host: host, port: 8080, timeout: 30 * time.Second}
    for _, opt := range opts {
        opt(s)
    }
    return s
}

Tipo para validação

type CPF string

func NovoCPF(valor string) (CPF, error) {
    limpo := strings.Map(func(r rune) rune {
        if r >= '0' && r <= '9' {
            return r
        }
        return -1
    }, valor)

    if len(limpo) != 11 {
        return "", fmt.Errorf("CPF inválido: deve ter 11 dígitos")
    }
    // ... validação de dígitos verificadores
    return CPF(limpo), nil
}

func (c CPF) Formatado() string {
    s := string(c)
    return fmt.Sprintf("%s.%s.%s-%s", s[:3], s[3:6], s[6:9], s[9:])
}

Boas práticas com type

  1. Crie tipos de domínio para valores semanticamente distintos (UserID, Email, Dinheiro)
  2. Use type assertion com forma segura (comma-ok) para evitar panics
  3. Prefira type switch sobre múltiplas type assertions encadeadas
  4. Use composição (embedding) ao invés de simular herança
  5. Defina constraints mínimas em generics — peça apenas o necessário
  6. Mantenha interfaces pequenas — 1-3 métodos é o ideal em Go
  7. Documente o zero value dos tipos que você cria

Para aprofundar, explore como types se integram com interfaces para polimorfismo, com structs para modelagem de dados e com generics para código reutilizável.

Perguntas frequentes (FAQ)

Qual a diferença entre type definition e type alias em Go?

Uma type definition (type NovoTipo int) cria um tipo completamente novo e distinto — não é intercambiável com o tipo original sem conversão explícita e pode ter métodos próprios. Um type alias (type Alias = int) cria apenas um nome alternativo para o mesmo tipo — é totalmente intercambiável e não pode ter métodos adicionais. Use definitions para segurança de tipo e aliases para migrações graduais.

Como funciona type assertion em Go?

Type assertion (valor.(Tipo)) extrai o valor concreto de uma interface. Use a forma segura v, ok := i.(Tipo) que retorna false em ok se o tipo não corresponder, evitando panic. Type assertion só funciona em valores de interface — não em tipos concretos. Para múltiplas verificações de tipo, prefira type switch.

Quando usar generics vs interface em Go?

Use interfaces quando precisa de polimorfismo em runtime (diferentes implementações trocadas dinamicamente) e generics (type constraints) quando precisa de polimorfismo em compile-time com performance máxima (sem boxing/unboxing). Regra prática: se o comportamento varia, use interface. Se apenas o tipo de dado varia mas a lógica é idêntica, use generics.

Posso adicionar métodos a tipos de outros pacotes?

Não diretamente. Go só permite adicionar métodos a tipos definidos no mesmo package. Para “estender” tipos externos, crie um novo tipo baseado nele: type MeuSlice []string e adicione métodos a MeuSlice. Isso é proposital — evita modificações imprevisíveis de tipos de terceiros e mantém o código Go previsível e fácil de entender.