Fundamentos de Programação/01 - Fundamentos Duros/Semana 01 - Diagnóstico e o que Acontece por Baixo7 min
Tipos e Sistema de Tipos
- Sua linguagem é estática ou dinâmica? Fortemente ou fracamente tipada?
- O que o tipo garante em tempo de compilação e o que ele não garante em runtime?
- Quando um tipo é só documentação e quando ele muda o código gerado?
- Que bug do seu último mês um tipo teria pegado de graça?
Conceito
Um tipo é três coisas ao mesmo tempo: um conjunto de valores possíveis, o conjunto de operações permitidas sobre eles, e uma representação em memória. Um sistema de tipos é o conjunto de regras que decide quais programas são legais.
Duas perguntas independentes que quase todo mundo mistura numa só:
| Eixo | Extremos | A pergunta |
|---|---|---|
| Quando verifica | estático ↔ dinâmico | o erro aparece na compilação ou na execução? |
| Quanto converte sozinho | forte ↔ fraco | a linguagem aceita "1" + 1? |
São ortogonais, e a matriz completa existe de verdade:
| Forte | Fraco | |
|---|---|---|
| Estático | Go, Java, Rust, Haskell | C, C++ |
| Dinâmico | Python, Ruby | JavaScript, PHP |
Um terceiro eixo, menos falado e mais decisivo no dia a dia:
- Nominal — dois tipos são iguais se têm o mesmo nome.
MetrosePéssão incompatíveis mesmo sendo ambosfloat. - Estrutural — dois tipos são iguais se têm a mesma forma. Qualquer coisa que tenha os membros certos serve, sem declarar parentesco.
O que um sistema de tipos compra: uma prova mecânica de que uma classe inteira de erros não pode acontecer — sem escrever teste, sem rodar o programa. É a única forma de verificação que cobre 100% dos caminhos do código.
O que ele cobra: todo sistema de tipos é conservador. Ele rejeita programas que funcionariam, porque decidir perfeitamente é indecidível. Toda reclamação de "o compilador está no meu caminho" é sobre esse preço, e o preço é real.
O limite fundamental: tipo verifica forma, não significado. Um int não sabe
que é um ID de usuário, nem que é uma quantidade de segundos. Ele aceita somar um com
o outro. Se você quer que o compilador saiba a diferença, precisa criar um tipo
distinto — e é exatamente aí que tipo deixa de ser burocracia e passa a pegar bug.
Inferência de tipo não é tipagem dinâmica. O tipo continua fixo e verificado em compilação; você só não escreveu ele.
Em Go
Go é estático, forte, nominal para tipos nomeados e estrutural para interfaces. Essa mistura é incomum e explica quase tudo que surpreende.
Nominal, e a sério. Não existe conversão numérica implícita — nem entre tipos do mesmo tamanho:
var i int = 1
var j int64 = i // erro: cannot use i (variable of type int) as int64 value
var k int64 = int64(i) // ok
Mais rígido que C, Java ou TypeScript.
A brecha: constantes não tipadas. O literal 5 não tem tipo até ser usado — ele
assume o tipo do contexto. É a única flexibilização implícita da linguagem, e é a
origem do bug mais popular de Go: time.Sleep(5) compila e dorme 5 nanossegundos.
Tipo definido vs alias — parecem iguais, não são:
| Declaração | O que é | Protege? |
|---|---|---|
type UserID int64 |
tipo novo e distinto, com method set próprio | sim |
type UserID = int64 |
alias: outro nome pro mesmo tipo | não, zero |
Estrutural nas interfaces: um tipo satisfaz uma interface por ter os métodos, sem declarar nada. É o que permite definir a interface no consumidor, não no produtor.
O que Go não tem: enum. iota + tipo definido é convenção; nada impede
Status(999). O compilador não fecha o conjunto e o switch não é exaustivo.
Respostas às perguntas-guia
1. Sua linguagem é estática ou dinâmica? Fortemente ou fracamente tipada?
Conceito: são dois eixos independentes — quando a verificação acontece, e quanto de coerção implícita é aceita.
Em Go: estática e forte, com inferência local (:=). O dinamismo existe mas é
opt-in e verificado em runtime: any, type assertion, type switch, reflect.
2. O que o tipo garante em tempo de compilação e o que ele não garante em runtime?
Conceito: o tipo garante o que é decidível estaticamente — forma, aridade, compatibilidade. Não garante nada que dependa de valor em runtime: índice, nulidade, faixa numérica. Um sistema de tipos mais expressivo (dependent types, refinement types) move parte disso pra compilação, ao custo de complexidade.
Em Go, garante: tipos casam, interface satisfeita, campo/método existe, nenhuma conversão implícita entre tipos nomeados. Não garante:
- escrita em
mapnil → panic - índice fora do range → panic
- type assertion sem
, ok→ panic - desreferência de ponteiro nil → panic
- overflow de inteiro é silencioso — sem panic, sem erro (
int8(127)+1 == -128) switchsobreiotanão é exaustivo — Go não tem enum- interface não-nil contendo ponteiro nil —
err != nilverdadeiro com um*MeuErronil dentro
3. Quando um tipo é só documentação e quando ele muda o código gerado?
Conceito: depende de o tipo ser apagado ou reificado. Tipo apagado (type erasure) existe só para o compilador e desaparece — custo zero, valor só de verificação. Tipo reificado sobrevive em runtime, precisa de metadados e frequentemente de indireção — custo real. Generics em Java são apagados; em C++ são monomorfizados; interfaces são reificadas em qualquer linguagem.
Em Go:
type UserID int64— representação idêntica aint64, custo zero em runtime. É documentação com dentes: muda apenas o que compila.- interface /
any— muda o código gerado: boxing num par (ponteiro de tipo, ponteiro de dado), dispatch dinâmico via itab, e frequentemente alocação no heap (2. Stack vs Heap). - genéricos ficam no meio: stenciling por GC shape — todos os tipos ponteiro compartilham uma instância e recebem um dictionary; tipos de valor distintos geram código próprio.
4. Que bug do seu último mês um tipo teria pegado de graça?
Conceito: a família inteira é a mesma — dois valores com a mesma representação e significados diferentes. Unidade (metros vs pés, segundos vs milissegundos), identidade (userID vs orderID), estado (string validada vs string crua). Um tipo distinto por significado elimina a classe toda.
Em Go, os três clássicos: time.Sleep(5) em vez de 5*time.Second; trocar
userID por orderID, ambos int64; segundos onde se esperava milissegundos.
go vet não pega o primeiro — quem pega é o staticcheck.
Trade-offs
Do conceito:
- Verificação estática cobre todos os caminhos, mas rejeita programas corretos. Verificação dinâmica aceita tudo e descobre no cliente.
- Tipagem nominal dá segurança de significado ao preço de conversões explícitas nas bordas. Estrutural dá flexibilidade ao preço de acoplamento acidental — dois tipos com a mesma forma passam a ser intercambiáveis sem ninguém ter decidido isso.
- Mais expressividade no tipo = mais erro pego em compilação e mais tempo lutando com o verificador. O ponto ótimo depende do custo de um bug em produção no seu contexto.
Da escolha de Go:
- Tipo definido dá segurança a custo zero de runtime, e espalha conversões explícitas nas bordas (JSON, SQL, libs). É ruído real, não imaginário.
- Constante não tipada compra ergonomia (
x * 2com qualquer numérico) e paga comtime.Sleep(5). anycompra flexibilidade e paga com alocação e verificação em runtime.- Sem enum, exaustividade vira trabalho de linter — o compilador não ajuda.
Exemplo prático
package main
import (
"fmt"
"time"
)
type UserID int64
type OrderID int64
type Segundos = int64 // ALIAS: não protege nada
func carregaUsuario(id UserID) { fmt.Println("user", id) }
func main() {
var u UserID = 7
var o OrderID = 7
carregaUsuario(u)
// carregaUsuario(o) // erro: cannot use o (OrderID) as UserID
// carregaUsuario(int64(7)) // erro: cannot use int64 as UserID
carregaUsuario(UserID(7)) // ok: conversão explícita
carregaUsuario(7) // ok: 7 é constante NÃO TIPADA
// A armadilha das constantes não tipadas:
time.Sleep(5) // 5 NANOSSEGUNDOS. Compila. Não faz nada.
time.Sleep(5 * time.Second) // o que você queria
// Alias não cria tipo novo:
var s Segundos = 30
var n int64 = s // compila: Segundos e int64 são o MESMO tipo
fmt.Println(n, o)
// Overflow é silencioso:
var i8 int8 = 127
i8++
fmt.Println(i8) // -128
}
A interface não-nil com ponteiro nil — vale rodar uma vez na vida:
type MeuErro struct{}
func (e *MeuErro) Error() string { return "boom" }
func falha() error {
var e *MeuErro = nil
return e // devolve interface com (tipo=*MeuErro, dado=nil)
}
func main() {
if err := falha(); err != nil {
fmt.Println("entrou aqui!", err == nil) // entrou aqui! false
}
}
A interface só é nil quando os dois ponteiros são nil. É a razão de nunca
declarar var e *MeuErro e devolver e — devolva nil literal.
Relacionado
- 2. Stack vs Heap — interface é tipo reificado: boxing e frequentemente alocação
- 3. Passagem por Valor vs por Referência — o tipo define o tamanho, e o tamanho é o que é copiado
- 4. Mutabilidade —
constem Go existe só para tipos básicos - 3. Interfaces — semana 5, tipagem estrutural aplicada a design
Parte de Semana 01 - Diagnóstico e o que Acontece por Baixo · 00 - MOC Fundamentos de Programação