Curso Completo de Go (Golang) — do projeto ao código de produção
Para quem já domina Python — Este material assume que você programa em Python em nível avançado (FastAPI, OOP, async, AWS). Cada seção segue o mesmo formato: conceito → por que existe → comparação com Python → código. Os exemplos de banco usam MySQL (relacional) e DynamoDB (não relacional), como pedido.
1. Anatomia de um projeto Go
1.1 Versões e instalação
Go segue versionamento semântico simples: go1.21, go1.22, go1.23... Não existe o conceito de "Python 2 vs 3"; a linguagem é extremamente estável — código de 10 anos atrás ainda compila. Você verifica a versão instalada com:
go version
O arquivo go.mod (explicado abaixo) fixa a versão mínima de Go que o projeto exige, de forma parecida com o python_requires do setup.py, mas é o próprio toolchain do Go que respeita isso — não existe pyenv/venv separado: o binário go já gerencia tudo.
1.2 Go Modules: o equivalente a requirements.txt + venv
Antes de 2019, Go usava GOPATH (uma pasta global única para todo código Go — doloroso). Hoje, Go Modules é o padrão e resolve dependências por projeto, como um pyproject.toml + poetry.lock combinados.
go mod init github.com/seuusuario/seuprojeto # cria o go.mod
go get github.com/go-sql-driver/mysql@v1.8.1 # adiciona uma dependência
go mod tidy # remove o que não é usado, resolve versões
Isso gera dois arquivos na raiz do projeto:
go.mod— declara o nome do módulo, a versão de Go e as dependências diretas. É o seurequirements.txt.go.sum— hashes criptográficos de cada dependência (transitiva também), garantindo build reprodutível. É o seupoetry.lock/Pipfile.lock.
// go.mod
module github.com/seuusuario/seuprojeto
go 1.23
require (
github.com/go-sql-driver/mysql v1.8.1
github.com/aws/aws-sdk-go-v2 v1.30.0
)
Diferente do Python, não existe ambiente virtual: as dependências ficam num cache global ($GOPATH/pkg/mod), mas cada módulo referencia versões exatas via go.sum, então não há conflito entre projetos — o equivalente a cada projeto ter seu próprio "venv" é automático e embutido na linguagem.
1.3 Estrutura de pastas (padrão de mercado)
Não existe um framework "Django-like" que impõe estrutura. A comunidade convergiu para o golang-standards/project-layout. Para uma API (equivalente a um projeto FastAPI):
seuprojeto/
├── go.mod
├── go.sum
├── cmd/
│ └── api/
│ └── main.go # ponto de entrada — equivalente a main.py / app.py
├── internal/ # código privado, não importável por outros módulos
│ ├── handler/ # equivalente a routers/ no FastAPI
│ │ └── user_handler.go
│ ├── service/ # regra de negócio — equivalente a services/
│ │ └── user_service.go
│ ├── repository/ # acesso a dados — equivalente a repositories/ ou models/
│ │ ├── mysql/
│ │ └── dynamo/
│ └── domain/ # structs de domínio — equivalente a schemas.py / models.py
│ └── user.go
├── pkg/ # código público, reutilizável por outros projetos
├── config/
│ └── config.go
└── Makefile
Pontos-chave que não existem em Python:
internal/é uma convenção reforçada pelo compilador: qualquer pacote dentro de uma pastainternal/só pode ser importado por código que esteja na mesma árvore acima dela. Python não tem equivalente — o mais próximo é o_prefixado por convenção (sem garantia real).- Não existe
__init__.py. Cada pasta é um pacote (package nomedapasta), e todo arquivo.godentro dela declara isso na primeira linha. A importação é pelo caminho do módulo + pasta, nunca pelo nome do arquivo.
1.4 Comandos do dia a dia
| Comando | Equivalente Python |
|---|---|
go run main.go |
python main.py (compila em memória e executa) |
go build -o app ./cmd/api |
pyinstaller (mas gera binário nativo estático, sem runtime necessário) |
go test ./... |
pytest |
go fmt ./... |
black . (mas é parte da toolchain oficial, sem debate de estilo) |
go vet ./... |
pylint/mypy (análise estática básica) |
go mod tidy |
pip-compile / poetry lock |
A diferença mais impactante no dia a dia: go build gera um único binário executável, sem dependência de runtime instalado no servidor de destino. Você não precisa de Python instalado, nem pip install -r requirements.txt no Dockerfile final — copia o binário e roda. Isso muda drasticamente a imagem Docker final (de ~150MB com Python+libs para poucos MB com scratch/alpine).
1.5 Anatomia de um arquivo .go
package main // toda pasta declara seu pacote. "main" é especial: marca um executável
import (
"fmt"
"net/http"
"github.com/seuusuario/seuprojeto/internal/handler"
)
func main() { // ponto de entrada obrigatório se package == main
fmt.Println("servidor iniciando")
http.ListenAndServe(":8080", handler.Router())
}
Regras que não têm paralelo direto em Python:
- Se o pacote é
main, precisa existir uma funçãomain()sem parâmetros e sem retorno — é o entrypoint, equivalente aoif __name__ == "__main__":, mas obrigatório e único. - Import não usado = erro de compilação. Não é warning, o build quebra. Isso elimina uma classe inteira de "lint debt" que se acumula em projetos Python.
- Variável declarada e não usada = erro de compilação, também. Go é radicalmente mais estrito que Python nesse ponto.
- Identificadores que começam com letra maiúscula são exportados (públicos, equivalente a não ter underscore em Python); minúscula = privado ao pacote. Não existe
_nomepor convenção fraca — é regra de visibilidade real, garantida pelo compilador.
2. Tipos, variáveis e sintaxe básica
Go é estaticamente tipado e compilado — a maior mudança mental vindo de Python. Não existe "duck typing" em tempo de execução para tipos primitivos: o compilador verifica tudo antes de gerar o binário.
var nome string = "Leo" // declaração explícita
var idade int = 30
idade2 := 30 // inferência de tipo — Go deduz "int"
const Pi = 3.14159 // constante, resolvida em tempo de compilação
var ativo bool // zero value: false
var contador int // zero value: 0
var rotulo string // zero value: "" (string vazia, nunca nil)
var ponteiro *int // zero value: nil
Zero values são o conceito sem equivalente direto em Python: toda variável declarada sem valor inicial já nasce com um valor padrão definido pelo tipo (não existe None/undefined implícito para tipos primitivos). Isso elimina a classe de bugs NameError/AttributeError: NoneType para tipos básicos — mas exige atenção redobrada com ponteiros e interfaces, que podem ser nil.
Se você tratar um ponteiro como trataria uma variável Python qualquer e pular o check, desreferenciar um ponteiro nil não levanta uma exceção capturável como o AttributeError de acessar atributo em None — gera um panic em runtime que derruba a goroutine inteira (e o processo, se não houver recover no lugar certo).
| Python | Go |
|---|---|
| Tipagem dinâmica, checada em runtime | Tipagem estática, checada em compilação |
type hints são opcionais e não impedem execução |
Tipos são obrigatórios e impedem o build |
int é arbitrariamente grande |
int, int32, int64 têm tamanho fixo — overflow é possível |
None é universal |
nil só existe para ponteiros, slices, maps, channels, funcs, interfaces |
str é imutável |
string também é imutável em Go |
3. Condicionais
idade := 25
if idade >= 18 {
fmt.Println("maior de idade")
} else if idade >= 13 {
fmt.Println("adolescente")
} else {
fmt.Println("criança")
}
// forma idiomática: declarar uma variável escopada só para o if
if valor, ok := buscarValor("x"); ok {
fmt.Println(valor)
} // 'valor' não existe fora deste bloco
switch diaSemana {
case "segunda", "terca": // múltiplos valores no mesmo case
fmt.Println("começo de semana")
case "sabado", "domingo":
fmt.Println("fim de semana")
default:
fmt.Println("meio de semana")
}
Diferenças relevantes vindo de Python:
- Go não tem operador ternário (
x if cond else y). É decisão de design deliberada dos criadores da linguagem para forçar legibilidade — você sempre escreveif/elsecompleto. switchem Go não tem fallthrough automático (diferente de C, e diferente domatchdo Python que não tem fallthrough nenhum). Cadacasejá é um "break" implícito; se você quiser continuar para o próximo case, usa a palavra-chavefallthroughexplicitamente.- Parênteses em
if/switchsão opcionais e geralmente omitidos; chaves{}são obrigatórias, mesmo para um único comando — elimina o bug clássico de C de "if sem chaves pegando só a próxima linha".
4. Loops
Go tem uma única palavra-chave de loop: for. Ela assume 4 formas:
// 1) "for" clássico, como o for(;;) do C
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// 2) "for" como while do Python
contador := 0
for contador < 5 {
contador++
}
// 3) "for" infinito, equivalente a "while True"
for {
if condicaoDeParada() {
break
}
}
// 4) "for range" — equivalente ao for-each / for x in lista
numeros := []int{1, 2, 3, 4}
for indice, valor := range numeros {
fmt.Println(indice, valor)
}
// range em map — equivalente a dict.items()
config := map[string]string{"env": "prod", "regiao": "us-east-1"}
for chave, valor := range config {
fmt.Println(chave, valor)
}
| Python | Go |
|---|---|
for, while (duas palavras-chave) |
só for (uma palavra-chave, 4 formas) |
for x in lista: |
for _, x := range lista |
for i, x in enumerate(lista): |
for i, x := range lista (enumerate é nativo do range) |
List comprehension [x*2 for x in l] |
não existe — você escreve o loop explícito sempre |
continue, break |
iguais, mas Go também suporta labels para break/continue em loops aninhados |
Loops aninhados com break rotulado (não existe em Python — lá você precisaria de flags ou exceptions):
busca:
for _, lista := range matriz {
for _, item := range lista {
if item == alvo {
break busca // sai dos dois loops de uma vez
}
}
}
5. Funções e métodos
5.1 Funções
func somar(a int, b int) int {
return a + b
}
// múltiplos retornos — usadíssimo para retornar (resultado, erro)
func dividir(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("divisão por zero")
}
return a / b, nil
}
// retornos nomeados — o "return" sozinho devolve as variáveis já nomeadas
func dividirNomeado(a, b float64) (resultado float64, err error) {
if b == 0 {
err = fmt.Errorf("divisão por zero")
return
}
resultado = a / b
return
}
// variádica — equivalente a *args
func somarTodos(numeros ...int) int {
total := 0
for _, n := range numeros {
total += n
}
return total
}
Não existe **kwargs (named arbitrary args) em Go. O padrão idiomático para isso é passar uma struct de opções, ou o functional options pattern:
type OpcoesServidor struct {
Porta int
Timeout time.Duration
}
type Opcao func(*OpcoesServidor)
func ComPorta(p int) Opcao {
return func(o *OpcoesServidor) { o.Porta = p }
}
func NovoServidor(opcoes ...Opcao) *OpcoesServidor {
config := &OpcoesServidor{Porta: 8080, Timeout: 30 * time.Second} // defaults
for _, aplicar := range opcoes {
aplicar(config)
}
return config
}
// uso: NovoServidor(ComPorta(9000))
Isso é o equivalente funcional a def __init__(self, porta=8080, timeout=30, **kwargs).
Closures existem e funcionam como em Python:
func contadorFabrica() func() int {
contador := 0
return func() int {
contador++
return contador
}
}
proximo := contadorFabrica()
proximo() // 1
proximo() // 2
5.2 Métodos: o que substitui self
Go não tem classes, mas tem métodos: funções associadas a um tipo via um "receiver".
type ContaBancaria struct {
Titular string
Saldo float64
}
// receiver de VALOR — recebe uma cópia. Mudanças não persistem fora do método.
func (c ContaBancaria) Resumo() string {
return fmt.Sprintf("%s: R$%.2f", c.Titular, c.Saldo)
}
// receiver de PONTEIRO — recebe a referência. Mudanças persistem.
func (c *ContaBancaria) Depositar(valor float64) {
c.Saldo += valor
}
conta := ContaBancaria{Titular: "Leo", Saldo: 100}
conta.Depositar(50) // Go desreferencia automaticamente: equivale a (&conta).Depositar(50)
fmt.Println(conta.Resumo()) // Leo: R$150.00
Regra prática: se o método precisa alterar o estado do struct, ou se o struct é "pesado" (evitar copiar), use receiver de ponteiro (c *ContaBancaria). Em Python isso nunca é uma decisão — self sempre é a referência ao objeto, nunca uma cópia. Em Go, escolher errado entre valor/ponteiro é uma fonte clássica de bugs para quem migra de Python.
Usar receiver de valor num método que deveria alterar o estado (hábito de quem nunca precisou pensar nisso porque self em Python é sempre referência) não gera erro nem warning de compilação — o método simplesmente modifica uma cópia e descarta a mudança. O bug é silencioso: o código compila, os testes podem passar se você não checar o objeto original depois da chamada.
6. Orientação a objetos em Go: structs, interfaces, composição e polimorfismo
Esta é a seção que mais quebra expectativas de quem vem de Python/Java/C#. Go não tem classes, não tem herança, não tem extends. A OOP em Go é construída sobre três pilares: structs (estado), métodos (comportamento) e interfaces (abstração/polimorfismo).
6.1 Structs como "classes sem herança"
type Usuario struct {
Nome string
Email string
idade int // minúsculo = privado ao pacote (equivalente a _idade em Python, mas garantido)
}
func NovoUsuario(nome, email string, idade int) *Usuario { // "construtor" por convenção
return &Usuario{Nome: nome, Email: email, idade: idade}
}
Não existe __init__. A convenção da comunidade é uma função NovoX(...) *X (chamada "constructor function") que monta e retorna o struct já preenchido — você escolhe os parâmetros, valida o que precisa, e devolve um ponteiro.
6.2 Composição em vez de herança ("embedding")
Onde Python usaria class Gerente(Funcionario):, Go usa embedding — você "encaixa" um struct dentro de outro, e os campos/métodos do struct embutido "sobem" automaticamente.
type Funcionario struct {
Nome string
Salario float64
}
func (f Funcionario) Apresentar() string {
return fmt.Sprintf("Sou %s", f.Nome)
}
type Gerente struct {
Funcionario // embedding — NÃO é "herança", é composição com promoção de campos/métodos
Equipe []string
}
g := Gerente{
Funcionario: Funcionario{Nome: "Leo", Salario: 12000},
Equipe: []string{"Ana", "Bruno"},
}
fmt.Println(g.Nome) // acessa campo promovido — funciona como se Gerente "tivesse" Nome
fmt.Println(g.Apresentar()) // acessa método promovido
A diferença filosófica crucial: em herança clássica (Python), Gerente é um Funcionario por definição de tipo (relação "is-a", checada por isinstance). Em Go, Gerente tem um Funcionario embutido (relação "has-a" com sintaxe de atalho) — não existe upcasting/downcasting de tipo. Se uma função espera Funcionario, um Gerente não pode ser passado diretamente nessa posição (a não ser que você passe g.Funcionario explicitamente). Isso evita toda a complexidade de herança múltipla, MRO (method resolution order) e os problemas clássicos de "diamond inheritance" que Python tem que resolver com super() e C3 linearization.
Tentar passar um Gerente onde uma função espera Funcionario — esperando um upcast automático como em Python/Java — não compila: cannot use g (type Gerente) as type Funcionario. Embedding não cria uma relação de tipos, só promove campos e métodos; você precisa passar g.Funcionario explicitamente.
6.3 Interfaces: abstração e "duck typing estático"
Esta é a peça central da OOP em Go.
type Notificador interface {
Enviar(mensagem string) error
}
Uma interface define apenas comportamento (assinatura de métodos), nunca estado. Qualquer tipo que implemente esses métodos satisfaz a interface automaticamente, implicitamente — não existe implements explícito como em Java/C#.
type EmailNotificador struct{ Remetente string }
func (e EmailNotificador) Enviar(mensagem string) error {
fmt.Println("enviando email de", e.Remetente, ":", mensagem)
return nil
}
type SMSNotificador struct{ Numero string }
func (s SMSNotificador) Enviar(mensagem string) error {
fmt.Println("enviando SMS para", s.Numero, ":", mensagem)
return nil
}
// função que aceita QUALQUER coisa que satisfaça Notificador
func Alertar(n Notificador, msg string) {
n.Enviar(msg)
}
Alertar(EmailNotificador{Remetente: "alertas@empresa.com"}, "servidor caiu")
Alertar(SMSNotificador{Numero: "+5511999999999"}, "servidor caiu")
Isso é polimorfismo: a mesma chamada n.Enviar(msg) dentro de Alertar se comporta diferente dependendo do tipo concreto passado, exatamente como notificador.enviar() polimórfico em Python. A diferença é o momento da verificação: em Python isso é checado em runtime (duck typing dinâmico — se o objeto não tiver .enviar(), quebra na hora da chamada); em Go é checado em tempo de compilação (duck typing estático — se EmailNotificador não implementar Enviar(string) error exatamente, o build nem completa).
| Conceito Python | Equivalente Go |
|---|---|
class Animal(ABC): @abstractmethod def fazer_som(self) |
type Animal interface { FazerSom() string } |
Protocol (PEP 544, structural typing) |
Interface (é o conceito nativo, não um "extra") |
Herança múltipla (class C(A, B)) |
Embedding múltiplo de structs, ou implementar múltiplas interfaces |
isinstance(obj, Classe) |
Type assertion: valor, ok := obj.(TipoConcreto) |
| Duck typing dinâmico (runtime) | Duck typing estático (compile-time) |
Interface vazia universal (object) |
interface{} ou any (Go 1.18+) — aceita qualquer tipo |
6.4 Abstração prática: inversão de dependência
O padrão mais comum em produção (e que você já usa em FastAPI com injeção de dependência) é depender de uma interface, nunca de uma implementação concreta:
type RepositorioUsuario interface {
BuscarPorID(ctx context.Context, id string) (*Usuario, error)
Salvar(ctx context.Context, u *Usuario) error
}
type ServicoUsuario struct {
repo RepositorioUsuario // depende da ABSTRAÇÃO, não de MySQLRepo ou DynamoRepo
}
func NovoServicoUsuario(repo RepositorioUsuario) *ServicoUsuario {
return &ServicoUsuario{repo: repo}
}
Isso permite trocar MySQLRepositorioUsuario por DynamoRepositorioUsuario (seções 8 e 9) ou por um mock em teste, sem tocar em ServicoUsuario — exatamente o mesmo princípio de Protocol/dependency injection em Python.
7. Validações e tratamento de erros
7.1 Erros são valores, não exceções
A maior mudança de paradigma vindo de Python: Go não tem try/except para fluxo normal de erro. Erros são retornados como valor comum, e você os checa explicitamente.
import "errors"
var ErrSaldoInsuficiente = errors.New("saldo insuficiente")
func (c *ContaBancaria) Sacar(valor float64) error {
if valor > c.Saldo {
return ErrSaldoInsuficiente
}
c.Saldo -= valor
return nil
}
if err := conta.Sacar(1000); err != nil {
if errors.Is(err, ErrSaldoInsuficiente) {
fmt.Println("trate o saldo insuficiente especificamente")
}
return err // "propagar a exceção" em Go é simplesmente: devolver o erro
}
Wrapping de erros (equivalente a raise NovaExcecao() from erro_original):
if err := repo.Salvar(ctx, usuario); err != nil {
return fmt.Errorf("falha ao salvar usuário %s: %w", usuario.ID, err) // %w preserva a cadeia
}
| Python | Go |
|---|---|
try/except Exception as e: |
if err != nil { ... } (checagem explícita, sem stack unwinding) |
raise CustomError("msg") from original |
fmt.Errorf("msg: %w", original) |
except SpecificError: |
errors.Is(err, ErrEspecifico) ou errors.As(err, &tipoEspecifico) |
| Exceções não tratadas crasham o processo | panic não tratado também crasha — mas é reservado para erros realmente irrecuperáveis (índice fora do array, nil pointer) |
finally: |
defer (sempre executa, mesmo com panic) |
panic/recover existem mas não são o "try/except" do dia a dia — são para situações excepcionais de verdade (bug de programação, corrupção de estado). Usar panic para erro de validação de input é considerado anti-padrão em Go.
func processarComSeguranca() {
defer func() {
if r := recover(); r != nil {
log.Printf("recuperado de panic: %v", r)
}
}()
// código que pode panic
}
Tratar panic como o raise genérico de Python para qualquer erro de validação é o erro mais comum de quem migra. Sem um recover() no ponto certo da pilha de chamadas, um panic em qualquer requisição HTTP derruba a goroutine que atende aquele request — e, dependendo de onde acontece, pode tirar o processo inteiro do ar, diferente de uma exceção HTTP que o FastAPI já intercepta e transforma em 500 automaticamente.
7.2 Validação de structs
Go não tem Pydantic embutido. Para validação declarativa parecida, a comunidade usa struct tags + a lib go-playground/validator (a mais usada em produção):
import "github.com/go-playground/validator/v10"
type CriarUsuarioInput struct {
Nome string `json:"nome" validate:"required,min=2,max=100"`
Email string `json:"email" validate:"required,email"`
Idade int `json:"idade" validate:"gte=0,lte=130"`
}
var validate = validator.New()
func ValidarInput(input CriarUsuarioInput) error {
if err := validate.Struct(input); err != nil {
return fmt.Errorf("validação falhou: %w", err)
}
return nil
}
| Pydantic (Python) | Go |
|---|---|
class CriarUsuarioInput(BaseModel): nome: str = Field(min_length=2) |
struct + tags validate:"min=2" |
Validação automática na desserialização (model_validate) |
Validação manual, você chama validate.Struct() explicitamente após json.Unmarshal |
@field_validator para regra customizada |
Implementa a interface validator.CustomTypeFunc ou valida manualmente no handler |
| Erros agregados automaticamente com path do campo | validator também agrega (err.(validator.ValidationErrors)), mas com menos "açúcar" |
Struct tags (json:"nome", validate:"required") são strings interpretadas via reflection em runtime pelas libs — é o mecanismo que substitui decorators/metaclasses do Python para esse tipo de metaprogramação declarativa.
8. CRUD com banco relacional (MySQL)
Go usa a interface padrão database/sql da biblioteca padrão + um driver específico do banco (aqui, go-sql-driver/mysql). É conceitualmente parecido com usar sqlalchemy.create_engine + driver pymysql, mas sem ORM por padrão — você escreve SQL puro (ou usa gorm/sqlx opcionalmente, citados ao final).
import (
"context"
"database/sql"
"time"
_ "github.com/go-sql-driver/mysql" // import "em branco": só registra o driver
)
func ConectarMySQL(dsn string) (*sql.DB, error) {
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, fmt.Errorf("erro abrindo conexão: %w", err)
}
db.SetMaxOpenConns(25) // equivalente a pool_size do SQLAlchemy
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute) // equivalente a pool_recycle
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
return nil, fmt.Errorf("falha no ping ao MySQL: %w", err)
}
return db, nil
}
sql.Open não conecta de fato — só prepara o pool. A conexão real só acontece na primeira query, por isso o PingContext explícito (equivalente a forçar uma conexão de teste no startup, como o pool_pre_ping do SQLAlchemy).
CREATE
func (r *MySQLRepositorioUsuario) Salvar(ctx context.Context, u *Usuario) error {
query := `INSERT INTO usuarios (id, nome, email, criado_em) VALUES (?, ?, ?, ?)`
_, err := r.db.ExecContext(ctx, query, u.ID, u.Nome, u.Email, time.Now())
if err != nil {
return fmt.Errorf("erro ao inserir usuário: %w", err)
}
return nil
}
READ
func (r *MySQLRepositorioUsuario) BuscarPorID(ctx context.Context, id string) (*Usuario, error) {
query := `SELECT id, nome, email FROM usuarios WHERE id = ?`
row := r.db.QueryRowContext(ctx, query, id)
var u Usuario
if err := row.Scan(&u.ID, &u.Nome, &u.Email); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("usuário %s não encontrado", id)
}
return nil, fmt.Errorf("erro ao buscar usuário: %w", err)
}
return &u, nil
}
func (r *MySQLRepositorioUsuario) ListarTodos(ctx context.Context) ([]Usuario, error) {
rows, err := r.db.QueryContext(ctx, `SELECT id, nome, email FROM usuarios`)
if err != nil {
return nil, err
}
defer rows.Close() // CRÍTICO: sem isso, a conexão fica presa no pool ("connection leak")
var usuarios []Usuario
for rows.Next() {
var u Usuario
if err := rows.Scan(&u.ID, &u.Nome, &u.Email); err != nil {
return nil, err
}
usuarios = append(usuarios, u)
}
return usuarios, rows.Err() // checar erro acumulado da iteração
}
Esquecer o rows.Close() não quebra nada nos testes locais nem no primeiro deploy — o bug só aparece sob carga real, quando o pool de conexões esgota silenciosamente. O sintoma em produção é timeouts e 500 intermitentes em endpoints que nem usam essa query, dias depois do deploy, porque todas as conexões do pool estão presas esperando um Close() que nunca veio.
UPDATE
func (r *MySQLRepositorioUsuario) Atualizar(ctx context.Context, u *Usuario) error {
query := `UPDATE usuarios SET nome = ?, email = ? WHERE id = ?`
resultado, err := r.db.ExecContext(ctx, query, u.Nome, u.Email, u.ID)
if err != nil {
return err
}
linhas, _ := resultado.RowsAffected()
if linhas == 0 {
return fmt.Errorf("nenhum usuário com id %s para atualizar", u.ID)
}
return nil
}
DELETE
func (r *MySQLRepositorioUsuario) Deletar(ctx context.Context, id string) error {
_, err := r.db.ExecContext(ctx, `DELETE FROM usuarios WHERE id = ?`, id)
return err
}
Transações (equivalente a with session.begin():)
func TransferirSaldo(ctx context.Context, db *sql.DB, origemID, destinoID string, valor float64) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback() // se algo der "return err" antes do Commit, isso desfaz tudo
if _, err := tx.ExecContext(ctx, `UPDATE contas SET saldo = saldo - ? WHERE id = ?`, valor, origemID); err != nil {
return err
}
if _, err := tx.ExecContext(ctx, `UPDATE contas SET saldo = saldo + ? WHERE id = ?`, valor, destinoID); err != nil {
return err
}
return tx.Commit()
}
| SQLAlchemy / psycopg2 (Python) | database/sql (Go) |
|---|---|
| ORM com modelos declarativos | Sem ORM nativo — SQL explícito (ou sqlx/gorm opcionalmente) |
session.query(Usuario).filter(...) |
db.QueryContext(ctx, "SELECT ... WHERE ...", args) |
Connection pool configurado no create_engine |
SetMaxOpenConns/SetMaxIdleConns no *sql.DB |
with session.begin(): (context manager) |
tx, _ := db.BeginTx(...) + defer tx.Rollback() manual |
| Lazy loading de relacionamentos | Não existe — você escreve o JOIN ou faz queries separadas |
9. CRUD com banco não relacional (DynamoDB)
Usando o AWS SDK for Go v2 (equivalente direto ao boto3 do Python), com a mesma filosofia que você já usa: chave de partição (PK) + chave de ordenação (SK).
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
)
func NovoClienteDynamo(ctx context.Context) (*dynamodb.Client, error) {
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("us-east-1"))
if err != nil {
return nil, err
}
return dynamodb.NewFromConfig(cfg), nil
}
CREATE (PutItem)
type ItemUsuario struct {
PK string `dynamodbav:"PK"` // ex: "USER#123"
SK string `dynamodbav:"SK"` // ex: "PROFILE"
Nome string `dynamodbav:"nome"`
Email string `dynamodbav:"email"`
}
func (r *DynamoRepositorioUsuario) Salvar(ctx context.Context, u *Usuario) error {
item := ItemUsuario{PK: "USER#" + u.ID, SK: "PROFILE", Nome: u.Nome, Email: u.Email}
av, err := attributevalue.MarshalMap(item) // equivalente a boto3's TypeSerializer
if err != nil {
return fmt.Errorf("erro ao serializar item: %w", err)
}
_, err = r.client.PutItem(ctx, &dynamodb.PutItemInput{
TableName: aws.String("usuarios"),
Item: av,
})
return err
}
READ (GetItem)
func (r *DynamoRepositorioUsuario) BuscarPorID(ctx context.Context, id string) (*Usuario, error) {
chave, _ := attributevalue.MarshalMap(map[string]string{
"PK": "USER#" + id,
"SK": "PROFILE",
})
resultado, err := r.client.GetItem(ctx, &dynamodb.GetItemInput{
TableName: aws.String("usuarios"),
Key: chave,
})
if err != nil {
return nil, fmt.Errorf("erro ao buscar item: %w", err)
}
if resultado.Item == nil {
return nil, fmt.Errorf("usuário %s não encontrado", id)
}
var item ItemUsuario
if err := attributevalue.UnmarshalMap(resultado.Item, &item); err != nil {
return nil, err
}
return &Usuario{ID: id, Nome: item.Nome, Email: item.Email}, nil
}
READ múltiplo (Query — busca por partição)
func (r *DynamoRepositorioUsuario) ListarPedidosDoUsuario(ctx context.Context, userID string) ([]Pedido, error) {
resultado, err := r.client.Query(ctx, &dynamodb.QueryInput{
TableName: aws.String("usuarios"),
KeyConditionExpression: aws.String("PK = :pk AND begins_with(SK, :prefixo)"),
ExpressionAttributeValues: map[string]types.AttributeValue{
":pk": &types.AttributeValueMemberS{Value: "USER#" + userID},
":prefixo": &types.AttributeValueMemberS{Value: "PEDIDO#"},
},
})
if err != nil {
return nil, err
}
var pedidos []Pedido
err = attributevalue.UnmarshalListOfMaps(resultado.Items, &pedidos)
return pedidos, err
}
UPDATE (UpdateItem — atualização parcial e atômica)
func (r *DynamoRepositorioUsuario) AtualizarNome(ctx context.Context, id, novoNome string) error {
chave, _ := attributevalue.MarshalMap(map[string]string{"PK": "USER#" + id, "SK": "PROFILE"})
_, err := r.client.UpdateItem(ctx, &dynamodb.UpdateItemInput{
TableName: aws.String("usuarios"),
Key: chave,
UpdateExpression: aws.String("SET nome = :n"),
ExpressionAttributeValues: map[string]types.AttributeValue{
":n": &types.AttributeValueMemberS{Value: novoNome},
},
ConditionExpression: aws.String("attribute_exists(PK)"), // evita criar item "fantasma"
})
return err
}
DELETE
func (r *DynamoRepositorioUsuario) Deletar(ctx context.Context, id string) error {
chave, _ := attributevalue.MarshalMap(map[string]string{"PK": "USER#" + id, "SK": "PROFILE"})
_, err := r.client.DeleteItem(ctx, &dynamodb.DeleteItemInput{
TableName: aws.String("usuarios"),
Key: chave,
})
return err
}
| boto3 (Python) | aws-sdk-go-v2 (Go) |
|---|---|
boto3.resource("dynamodb") (alto nível, com serialização automática) |
dynamodb.NewFromConfig (baixo nível — você sempre lida com AttributeValue) |
table.put_item(Item={...}) (dict puro) |
attributevalue.MarshalMap(struct) — exige struct + tags dynamodbav |
table.get_item(Key={...}) |
client.GetItem(ctx, &dynamodb.GetItemInput{...}) |
Resposta já vem como dict Python nativo |
Resposta vem tipada (types.AttributeValueMemberS, etc.) — mais verboso, porém com checagem em compile-time |
Paginadores (paginator.paginate()) |
LastEvaluatedKey manual em loop, ou usa o pacote auxiliar de paginators do SDK v2 |
10. Concorrência: goroutines e channels
Aqui está a maior vantagem competitiva de Go sobre Python para sistemas de alta concorrência.
10.1 Goroutines vs threads/asyncio
func main() {
go fazerAlgo() // "go" + chamada de função = dispara em uma goroutine
fazerOutraCoisa()
time.Sleep(time.Second) // só para o exemplo não terminar antes da goroutine rodar
}
Uma goroutine é uma "thread verde": gerenciada pelo runtime do Go (não pelo SO), com stack inicial de ~2KB (cresce dinamicamente) — você pode rodar centenas de milhares de goroutines simultaneamente sem o overhead de threads de SO. O runtime do Go faz o scheduling dessas goroutines sobre um pool de threads reais do sistema operacional (controlado por GOMAXPROCS, ver seção 11).
| Python | Go |
|---|---|
threading.Thread — thread real do SO, presa pelo GIL (só uma executa Python por vez) |
Goroutine — leve, sem GIL, paralelismo real em múltiplos cores |
asyncio — concorrência cooperativa em 1 thread, precisa de async/await em toda a cadeia de chamadas |
Goroutines são concorrentes E paralelas nativamente, sem sintaxe async/await — qualquer função pode ser chamada com go |
multiprocessing — contorna o GIL gerando processos (caro: memória duplicada, IPC) |
Não precisa: uma única goroutine já usa múltiplos cores de verdade |
| Custo de uma thread: ~8MB de stack | Custo de uma goroutine: ~2KB de stack inicial |
Esta é a diferença estrutural mais importante: em Python você escolhe entre asyncio (rápido, mas I/O-bound apenas, sem usar múltiplos cores) ou multiprocessing (usa múltiplos cores, mas caro). Em Go, goroutines resolvem os dois casos com o mesmo modelo de programação.
10.2 Channels: comunicação entre goroutines
Channels são filas tipadas e seguras para concorrência — o mecanismo oficial de comunicação entre goroutines (a filosofia de Go: "don't communicate by sharing memory; share memory by communicating").
canal := make(chan string) // channel sem buffer (bloqueia até alguém receber)
canalComBuffer := make(chan string, 10) // buffer de 10 — não bloqueia até estar cheio
go func() {
canal <- "resultado processado" // envia
}()
mensagem := <-canal // recebe (bloqueia até ter algo)
fmt.Println(mensagem)
canal := make(chan int)
go func() {
for i := 0; i < 5; i++ {
canal <- i
}
close(canal) // sinaliza "não vem mais nada"
}()
for valor := range canal { // itera até o channel ser fechado
fmt.Println(valor)
}
select é o switch para channels — espera em múltiplos channels simultaneamente:
select {
case msg := <-canalA:
fmt.Println("recebido de A:", msg)
case msg := <-canalB:
fmt.Println("recebido de B:", msg)
case <-time.After(2 * time.Second):
fmt.Println("timeout — nenhum canal respondeu em 2s")
}
Não existe equivalente direto disso no Python padrão — select em Go é parecido conceitualmente com asyncio.wait([...], return_when=FIRST_COMPLETED), mas é uma construção nativa da linguagem, não uma função de biblioteca.
10.3 sync.WaitGroup e sync.Mutex
import "sync"
func ProcessarEmParalelo(itens []string) {
var wg sync.WaitGroup // equivalente a asyncio.gather, mas para goroutines
for _, item := range itens {
wg.Add(1)
go func(it string) { // SEMPRE passe a variável de loop como parâmetro (evita bug clássico de closure)
defer wg.Done()
processar(it)
}(item)
}
wg.Wait() // bloqueia até todas as goroutines chamarem Done()
}
type ContadorSeguro struct {
mu sync.Mutex // equivalente a threading.Lock()
contador int
}
func (c *ContadorSeguro) Incrementar() {
c.mu.Lock()
defer c.mu.Unlock()
c.contador++
}
Sem o sync.Mutex, duas goroutines podem ler o mesmo valor de c.contador antes de qualquer uma escrever de volta — você perde incrementos silenciosamente (o contador final fica menor que o esperado), sem nenhum erro, crash ou stack trace apontando o problema. Em Python com threads, o GIL mascara boa parte desses casos simples; em Go, paralelismo real significa que essa proteção é obrigatória desde o primeiro código concorrente.
sync.Mutex é necessário porque, sem o GIL, acessar a mesma variável de múltiplas goroutines simultaneamente é uma race condition real (detectável com a flag go run -race). Em Python com threads isso também acontece, mas o GIL mascara parte dos casos mais simples; em Go, paralelismo real significa que você precisa pensar em sincronização desde o primeiro dia.
11. Paralelismo, throttling e padrões multi-task
11.1 GOMAXPROCS — paralelismo real
import "runtime"
func init() {
fmt.Println("CPUs disponíveis:", runtime.NumCPU())
fmt.Println("GOMAXPROCS atual:", runtime.GOMAXPROCS(0))
}
GOMAXPROCS define quantas threads do SO o scheduler do Go usa para executar goroutines em paralelo de verdade. Por padrão, é igual ao número de CPUs disponíveis (e, desde Go 1.21+/1.5, respeita automaticamente cgroups/limites de CPU do container — importante em ECS/Kubernetes, onde sem isso o runtime poderia "achar" que tem mais CPU do que o container realmente recebeu).
11.2 Worker pool — multi-tasking controlado
O padrão central para processar muitas tarefas sem explodir o número de goroutines simultâneas (equivalente ao ThreadPoolExecutor(max_workers=N) ou asyncio.Semaphore(N) do Python):
func WorkerPool(ctx context.Context, tarefas <-chan Tarefa, numWorkers int) <-chan Resultado {
resultados := make(chan Resultado, numWorkers)
var wg sync.WaitGroup
for w := 0; w < numWorkers; w++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for tarefa := range tarefas { // cada worker consome do mesmo channel — distribuição automática
select {
case <-ctx.Done():
return // cancelamento propagado
default:
resultados <- processarTarefa(tarefa)
}
}
}(w)
}
go func() {
wg.Wait()
close(resultados)
}()
return resultados
}
tarefas := make(chan Tarefa, 100)
go func() {
for _, t := range listaDeTarefas {
tarefas <- t
}
close(tarefas)
}()
for resultado := range WorkerPool(ctx, tarefas, 10) { // 10 workers fixos — "multi-tasking" limitado
fmt.Println(resultado)
}
11.3 Rate limiting — "multi-throttling"
Para limitar taxa (não apenas concorrência simultânea), Go tem o pacote oficial golang.org/x/time/rate (algoritmo token bucket, o mesmo conceito do aiolimiter/ratelimit em Python):
import "golang.org/x/time/rate"
limitador := rate.NewLimiter(rate.Limit(50), 10) // 50 req/s sustentado, burst de 10
func ChamarAPIExterna(ctx context.Context, requisicao Requisicao) error {
if err := limitador.Wait(ctx); err != nil { // bloqueia até ter "token" disponível, respeitando ctx
return fmt.Errorf("rate limit: %w", err)
}
return executarChamada(requisicao)
}
Para limitar múltiplos recursos com taxas diferentes ("multi-throttling" — ex: um limitador por tenant, ou um por endpoint downstream):
type LimitadorMultiTenant struct {
mu sync.Mutex
limitadores map[string]*rate.Limiter
}
func (l *LimitadorMultiTenant) Para(tenantID string) *rate.Limiter {
l.mu.Lock()
defer l.mu.Unlock()
if lim, existe := l.limitadores[tenantID]; existe {
return lim
}
novo := rate.NewLimiter(rate.Limit(10), 5) // 10 req/s por tenant, burst 5
l.limitadores[tenantID] = novo
return novo
}
11.4 Semáforos via channel bufferizado (alternativa simples ao rate limiter)
semaforo := make(chan struct{}, 5) // permite só 5 operações concorrentes
func ChamarComLimite(ctx context.Context, fn func() error) error {
select {
case semaforo <- struct{}{}: // "adquire" uma vaga
defer func() { <-semaforo }() // "libera" a vaga
return fn()
case <-ctx.Done():
return ctx.Err()
}
}
Equivalente direto a asyncio.Semaphore(5) do Python — mas usando a primitiva nativa de channel em vez de uma classe de biblioteca.
11.5 context.Context — timeout, cancelamento e deadline propagation
context.Context é a peça que amarra tudo: propaga timeout/cancelamento por toda a cadeia de chamadas (incluindo entre goroutines), exatamente como você já faz com asyncio.timeout()/anyio cancel scopes, ou como o trace_id propagado entre containers no seu sistema multi-agente.
func BuscarComTimeout(parent context.Context, id string) (*Usuario, error) {
ctx, cancel := context.WithTimeout(parent, 3*time.Second)
defer cancel() // SEMPRE chame cancel, mesmo se o timeout não disparar — libera recursos
return repositorio.BuscarPorID(ctx, id) // o context "viaja" até a query SQL/HTTP downstream
}
Esquecer o defer cancel() não trava o programa na hora — o timer interno do context continua vivo até o timeout original expirar por conta própria. Em um serviço de alto throughput, isso vira um vazamento lento de goroutines/timers que só fica visível em métricas de memória depois de horas em produção, nunca em ambiente de desenvolvimento.
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(1 * time.Second)
cancel() // cancela manualmente — toda goroutine que escuta ctx.Done() é notificada
}()
11.6 Fan-out / fan-in (pipeline de processamento)
func FanOut(ctx context.Context, entrada <-chan int, numWorkers int) []<-chan int {
saidas := make([]<-chan int, numWorkers)
for i := 0; i < numWorkers; i++ {
saida := make(chan int)
saidas[i] = saida
go func() {
defer close(saida)
for v := range entrada {
saida <- v * v // exemplo: processamento qualquer
}
}()
}
return saidas
}
func FanIn(canais ...<-chan int) <-chan int {
saida := make(chan int)
var wg sync.WaitGroup
for _, c := range canais {
wg.Add(1)
go func(canal <-chan int) {
defer wg.Done()
for v := range canal {
saida <- v
}
}(c)
}
go func() {
wg.Wait()
close(saida)
}()
return saida
}
| Python | Go |
|---|---|
asyncio.Semaphore(N) |
chan struct{} bufferizado com capacidade N |
ThreadPoolExecutor(max_workers=N) |
Worker pool com N goroutines lendo do mesmo channel |
aiolimiter.AsyncLimiter |
golang.org/x/time/rate.Limiter |
asyncio.wait_for(coro, timeout=N) |
context.WithTimeout |
asyncio.gather(*tasks) |
sync.WaitGroup + goroutines |
| GIL impede paralelismo real em threads | Sem GIL — paralelismo real, controlado por GOMAXPROCS |
12. Escalabilidade horizontal e arquitetura
Os princípios são os mesmos que você já aplica com FastAPI + ECS — a diferença é onde cada responsabilidade vive.
12.1 Stateless services
Assim como em FastAPI, cada instância do serviço Go não deve guardar estado de sessão em memória local — estado vai para Redis/DynamoDB, exatamente como na sua arquitetura atual de memória de agentes. Goroutines/threads HTTP de uma instância não compartilham nada com outra instância — escalar horizontalmente é simplesmente subir mais réplicas do binário atrás de um load balancer (ALB/NLB), sem nenhuma configuração especial do lado do Go.
12.2 Graceful shutdown
Equivalente ao shutdown handler do Uvicorn/Gunicorn, mas escrito explicitamente:
func main() {
servidor := &http.Server{Addr: ":8080", Handler: Router()}
go func() {
if err := servidor.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("erro no servidor: %v", err)
}
}()
sinal := make(chan os.Signal, 1)
signal.Notify(sinal, syscall.SIGINT, syscall.SIGTERM) // captura o sinal que o ECS envia no deploy
<-sinal
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := servidor.Shutdown(ctx); err != nil { // espera requisições em andamento terminarem
log.Printf("forçando encerramento: %v", err)
}
}
Isso é o equivalente direto ao lifespan do FastAPI + o tratamento de SIGTERM que o ECS já manda no seu setup atual antes de matar uma task.
Sem esse tratamento de SIGTERM, o ECS mata o processo imediatamente em todo deploy ou scale-in, cortando conexões em andamento na marra. O sintoma não é um incidente raro: é um pico de connection reset/502 a cada deploy, porque requisições que estavam sendo processadas no exato momento do sinal nunca recebem resposta.
12.3 Health checks (readiness vs liveness)
func HandlerSaude(db *sql.DB, redisClient *redis.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
http.Error(w, "MySQL indisponível", http.StatusServiceUnavailable)
return
}
if err := redisClient.Ping(ctx).Err(); err != nil {
http.Error(w, "Redis indisponível", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
}
}
Mesma lógica de /health que você já tem no FastAPI — o ALB/health check do ECS consome esse endpoint da mesma forma, independente da linguagem.
12.4 O ganho real de escalabilidade vindo de Go
A diferença prática que você vai sentir em produção: um binário Go consome frações da memória de um processo Python equivalente (sem interpretador, sem GC tão pesado, sem múltiplos workers Gunicorn para contornar o GIL). Em FastAPI, escalar verticalmente dentro de uma task ECS exige múltiplos workers Uvicorn (cada um um processo separado, cada um com seu próprio heap). Em Go, uma única instância do binário já usa todos os cores da task via goroutines — você tipicamente precisa de menos réplicas/menos vCPU para o mesmo throughput, o que se traduz diretamente em menor custo de ECS Fargate.
13. Logs e monitoramento
13.1 log/slog — logging estruturado nativo (Go 1.21+)
Antes do slog, Go só tinha o pacote log (texto puro, sem campos estruturados). Hoje slog é o equivalente nativo ao structlog/logging com JSONFormatter do Python:
import "log/slog"
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
logger.Info("requisição processada",
slog.String("thread_id", threadID), // exatamente o seu eixo de correlação atual
slog.String("usuario_id", usuarioID),
slog.Int("status", 200),
slog.Duration("duracao", tempoGasto),
)
// saída: {"time":"...","level":"INFO","msg":"requisição processada","thread_id":"...","usuario_id":"...","status":200,"duracao":"45ms"}
loggerComContexto := logger.With(slog.String("servico", "api-usuarios")) // equivalente a logger.bind() do structlog
loggerComContexto.Error("falha ao salvar no MySQL", slog.String("erro", err.Error()))
| structlog / logging (Python) | log/slog (Go) |
|---|---|
logger.bind(thread_id=...) |
logger.With(slog.String("thread_id", ...)) |
JSONRenderer() |
slog.NewJSONHandler(...) |
logging.getLogger(__name__) |
Geralmente um logger por pacote/serviço, passado explicitamente (sem registry global automático) |
Nível configurado via logging.basicConfig(level=...) |
slog.HandlerOptions{Level: slog.LevelInfo} |
13.2 OpenTelemetry em Go (tracing/metrics)
Mesmo SDK conceitual que você já usa em Python, com a mesma propagação W3C TraceContext entre serviços:
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)
var tracer = otel.Tracer("servico-usuarios")
func BuscarUsuario(ctx context.Context, id string) (*Usuario, error) {
ctx, span := tracer.Start(ctx, "BuscarUsuario", trace.WithAttributes(
attribute.String("usuario.id", id),
))
defer span.End()
usuario, err := repositorio.BuscarPorID(ctx, id) // ctx propaga o span para a query SQL
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}
return usuario, nil
}
A propagação de trace entre containers Go ↔ Python (caso seu sistema venha a ser híbrido) funciona pelo mesmo header HTTP traceparent do W3C TraceContext — o protocolo é o mesmo independente da linguagem, exatamente como já acontece entre seus containers ECS de agentes hoje.
| Python (OTel) | Go (OTel) |
|---|---|
tracer.start_as_current_span("nome") (context manager) |
ctx, span := tracer.Start(ctx, "nome") + defer span.End() |
Propagação automática via instrumentation libs (FastAPI, httpx) |
Frequentemente manual: você passa ctx explicitamente em cada chamada (otelhttp instrumenta o http.Client/http.Server automaticamente) |
| Exporter para X-Ray/OTLP configurado no SDK | Mesmo conceito — go.opentelemetry.io/contrib/exporters/autoexport ou exporter AWS X-Ray dedicado |
14. Cheat sheet final — Go vs Python
| Tema | Python | Go |
|---|---|---|
| Tipagem | Dinâmica, hints opcionais | Estática, obrigatória, checada em compilação |
| Execução | Interpretado (CPython) | Compilado para binário nativo |
| OOP | Classes + herança (única e múltipla) | Structs + interfaces + composição (sem herança) |
| Polimorfismo | Duck typing dinâmico (runtime) | Duck typing estático (compile-time, via interfaces) |
| Erros | Exceções (try/except) |
Valores de retorno (if err != nil) |
| Concorrência I/O-bound | asyncio (cooperativo, 1 thread) |
Goroutines (cooperativo E paralelo) |
| Concorrência CPU-bound | multiprocessing (processos separados) |
Goroutines (mesmo processo, múltiplos cores) |
| Gerenciador de dependências | pip/poetry + venv | Go Modules (go.mod/go.sum), sem venv |
| Deploy | Precisa do interpretador Python no destino | Binário único, sem runtime necessário |
| Validação de dados | Pydantic (automática) | go-playground/validator (manual, via tags) |
| Logging estruturado | structlog / logging + JSONFormatter | log/slog (nativo desde 1.21) |
| Null/None | None universal |
nil só para ponteiro/slice/map/chan/func/interface; zero values para o resto |
Próximos passos sugeridos
- Reescrever um endpoint simples do seu FastAPI atual (ex: CRUD de usuário) em Go usando
net/httppuro ou o routerchi/gin, comparando linha a linha. - Rodar
go run -racenum exemplo com goroutines + Mutex para ver a detecção de race condition na prática. - Medir consumo de memória de um binário Go vs um processo Uvicorn equivalente, no mesmo container ECS — esse número costuma ser o argumento mais convincente para adoção em produção.