trilha

Fundamentos de Programação/01 - Fundamentos Duros/Semana 01 - Diagnóstico e o que Acontece por Baixo7 min

Tipos e Sistema de Tipos

Perguntas-guia
  • 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. Metros e Pés são incompatíveis mesmo sendo ambos float.
  • 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 map nil → 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)
  • switch sobre iota não é exaustivo — Go não tem enum
  • interface não-nil contendo ponteiro nilerr != nil verdadeiro com um *MeuErro nil 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 a int64, 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 * 2 com qualquer numérico) e paga com time.Sleep(5).
  • any compra 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


Parte de Semana 01 - Diagnóstico e o que Acontece por Baixo · 00 - MOC Fundamentos de Programação

Buscar

Busca por título, seção e texto das notas