Aprendendo Go como Engenheiro de Dados: o que eu aprendi em 10 dias
Há algum tempo venho usando Python para praticamente tudo em engenharia de dados: carga de arquivos, automações, integrações com APIs, scripts no GCP, alertas, validações e por aí vai. Python continua sendo muito produtivo para esse tipo de trabalho.
O problema começa quando aquele script que era "só uma rotina rápida" vira um processo que roda todo dia, lê arquivo grande, chama serviço externo, envia alerta e precisa ser acompanhado por mais gente. Se você trabalha com dados, provavelmente já viu esse filme. No começo é um arquivo .py, depois tem configuração, log, retry, variáveis de ambiente, deploy, monitoramento e, quando vê, virou aplicação.
Foi pensando nesse tipo de cenário que comecei uma mentoria prática de Go. A ideia não foi trocar Python por Go, mas entender onde Go pode fazer sentido para engenharia de dados: performance, concorrência, APIs, workers e ferramentas de linha de comando.
A mentoria está organizada em aulas diárias, em dias úteis. Nestes primeiros 10 dias, passei pelo básico da linguagem e comecei a ligar os pontos com problemas reais do dia a dia. Bom, chega de enrolar e vamos ao que interessa.
Resumo dos 10 dias
- Dia 01: instalação do Go, teste com
go version, primeiroHello Worlde a estrutura básica de um programa. - Dia 02: tipos, variáveis, constantes e controle de fluxo. Aqui já aparece a diferença para Python: o compilador participa mais da conversa.
- Dia 03: arrays, slices e maps. Começou a ficar com cara de rotina de dados, principalmente quando entramos em slices.
- Dia 04: funções e múltiplos retornos. O tratamento explícito de erro incomoda no começo, mas faz sentido.
- Dia 05: structs e métodos. Foi quando parei de pensar em "dicionário" para tudo.
- Dia 06: interfaces. Um assunto simples na sintaxe, mas que exige um pouco mais de cabeça para usar bem.
- Dia 07: pacotes e organização de código. Nada muito mirabolante, mas importante para o projeto não virar um arquivo gigante.
- Dia 08: manipulação de arquivos e IO. Leitura linha a linha, buffer e cuidado com memória.
- Dia 09: goroutines. A primeira impressão foi: "só isso para rodar em paralelo?".
- Dia 10: channels. Aí a brincadeira fica mais interessante, porque entra comunicação entre as goroutines.
Aprendizados principais
Diferenças entre Go e Python
A primeira diferença é a tipagem. Em Python, eu consigo montar um dicionário, passar para uma função, alterar no meio do caminho e seguir trabalhando. Isso é ótimo para velocidade. Em Go, as coisas precisam estar mais claras: tipo do campo, retorno da função, erro possível, estrutura esperada.
No começo parece burocracia. Depois de alguns exercícios, começa a parecer revisão preventiva. O compilador avisa antes de você descobrir o problema rodando a rotina às 2 da manhã.
Outra coisa que chama atenção é a simplicidade. Go não tem tantos recursos quanto Python, e isso às vezes dá saudade. Por outro lado, tem menos formas diferentes de resolver o mesmo problema. Em time, isso ajuda.
Sobre performance, ainda não dá para sair por aí prometendo milagre depois de 10 dias de estudo. Mas já dá para entender o motivo de Go aparecer tanto em serviços, CLIs e workers: binário único, inicialização rápida, bom uso de CPU e concorrência nativa. Para quem precisa processar arquivo, consumir fila ou expor uma API de ingestão, isso chama atenção.
Estruturas de dados
Arrays existem, mas nos exercícios o que apareceu de verdade foram os slices. Para quem vem de Python, dá vontade de chamar slice de lista e seguir a vida. Funciona como comparação inicial, mas não é a mesma coisa.
Slice tem tamanho, capacidade e usa um array por baixo. Quando usamos append, pode acontecer realocação. Parece detalhe de linguagem, mas em processamento de dados detalhe de memória uma hora aparece na conta.
Maps são mais tranquilos para quem já usa dicionário em Python. Usei o mesmo raciocínio para contador, deduplicação, agrupamento simples e índice em memória. A diferença é que em Go você define logo o tipo da chave e do valor. Menos flexível, sim. Mais previsível também.
Modelagem de dados
Structs foram um dos pontos em que a ficha caiu melhor. Em Python, muitas vezes eu começo com dict porque é rápido e resolve. Em Go, declarar uma struct obriga a pensar melhor no formato do dado:
- um evento de ingestão;
- uma linha lida de um arquivo;
- uma mensagem recebida de uma fila;
- uma métrica calculada no pipeline.
Métodos entram bem aqui. Uma validação, uma conversão de formato ou uma regra pequena pode ficar perto do dado que ela manipula. Nada de criar uma arquitetura enorme só para validar meia dúzia de campos.
Para mim, a mudança foi sair do "depois eu vejo quais campos chegam" para "qual contrato esse registro precisa cumprir?". Quem já depurou pipeline quebrando por campo nulo sabe que isso não é detalhe.
Arquitetura de código
No sétimo dia entramos em pacotes. Parece assunto simples, mas resolve um problema comum: onde colocar cada coisa. Em Python, dependendo da pressa, é muito fácil um script crescer até virar um arquivo enorme. Já fiz isso mais vezes do que gostaria.
Em Go, o nome do pacote, a visibilidade por letra maiúscula ou minúscula e o uso de módulos criam uma disciplina natural. Não faz mágica, mas ajuda a evitar bagunça logo no início.
Para um projeto de dados, uma organização inicial razoável poderia separar:
- leitura e validação de entrada;
- transformação;
- escrita ou publicação;
- tipos de domínio;
- comandos ou APIs de entrada.
Não precisa começar com arquitetura de astronauta. A ideia é só evitar que leitura de arquivo, regra de negócio, log, chamada externa e tratamento de erro fiquem todos embolados no mesmo lugar.
Concorrência
Concorrência foi a parte que mais justificou a mentoria até aqui. Em Python, quando preciso paralelizar algo, começo pensando em threading, multiprocessing, asyncio, fila externa ou alguma gambiarra controlada. Cada opção tem seu custo e suas limitações.
Em Go, goroutine aparece de forma muito natural. Você coloca go antes da chamada e pronto: aquilo roda em paralelo. Viu como é simples? Pois é, simples até demais. A facilidade de criar concorrência não elimina o trabalho de desenhar bem o fluxo.
Channels foram a virada. Em vez de compartilhar estado por todo lado, a ideia é passar mensagens. Para pipeline de dados, isso conversa bem com o desenho mental que já usamos:
- uma etapa lê dados;
- outra transforma;
- outra valida;
- outra envia para um destino.
O ponto que anotei em letras grandes foi: concorrência simples não é concorrência automática. Precisa pensar em fechamento de channel, limite de workers, erro, timeout e cancelamento. Mesmo assim, o modelo me pareceu mais direto do que boa parte das soluções que costumo montar em Python.
Exemplos práticos
Manipulação de slice em dados de pipeline
Vamos para um exemplo simples. Imagine que recebi alguns eventos de uma etapa anterior do pipeline. Alguns vieram vazios e eu não quero carregar sujeira para a próxima fase.
package main
import "fmt"
func main() {
events := []string{
"user_created",
"",
"payment_approved",
"subscription_canceled",
"",
}
validEvents := make([]string, 0, len(events))
for _, event := range events {
if event == "" {
continue
}
validEvents = append(validEvents, event)
}
fmt.Println(validEvents)
}
O código é pequeno, mas tem um detalhe interessante: usei make com capacidade inicial porque já sei o tamanho máximo possível da saída. Com meia dúzia de registros tanto faz. Com volume maior, esse tipo de coisa começa a aparecer.
Struct representando uma linha de tabela
Agora um exemplo com struct. Pense em uma linha já normalizada, pronta para ser gravada em uma tabela, publicada em uma fila ou enviada para uma API interna.
package main
import (
"fmt"
"time"
)
type OrderEvent struct {
OrderID string
CustomerID string
AmountCents int64
CreatedAt time.Time
}
func (event OrderEvent) IsValid() bool {
return event.OrderID != "" &&
event.CustomerID != "" &&
event.AmountCents > 0 &&
!event.CreatedAt.IsZero()
}
func main() {
event := OrderEvent{
OrderID: "ord_123",
CustomerID: "cus_456",
AmountCents: 12990,
CreatedAt: time.Now(),
}
if !event.IsValid() {
fmt.Println("evento invalido")
return
}
fmt.Printf("evento pronto para ingestao: %+v\n", event)
}
Em Python, provavelmente eu começaria com um dicionário e talvez migrasse para uma dataclass depois. Em Go, essa decisão vem antes. O contrato do dado fica mais explícito.
Concorrência com goroutines e channel
Agora a parte mais legal: concorrência. A ideia é processar uma lista de arquivos usando alguns workers e receber os resultados conforme cada worker termina o trabalho.
package main
import (
"fmt"
"sync"
"time"
)
type ProcessResult struct {
FileName string
Rows int
}
func worker(id int, files <-chan string, results chan<- ProcessResult, wg *sync.WaitGroup) {
defer wg.Done()
for fileName := range files {
// Simula uma leitura ou transformacao custosa.
time.Sleep(200 * time.Millisecond)
results <- ProcessResult{
FileName: fileName,
Rows: len(fileName) * 100,
}
fmt.Printf("worker %d processou %s\n", id, fileName)
}
}
func main() {
fileNames := []string{
"orders_2026_05_01.csv",
"customers_2026_05_01.csv",
"payments_2026_05_01.csv",
}
files := make(chan string)
results := make(chan ProcessResult)
var wg sync.WaitGroup
workerCount := 2
for id := 1; id <= workerCount; id++ {
wg.Add(1)
go worker(id, files, results, &wg)
}
go func() {
for _, fileName := range fileNames {
files <- fileName
}
close(files)
}()
go func() {
wg.Wait()
close(results)
}()
for result := range results {
fmt.Printf("resultado: %s com %d linhas\n", result.FileName, result.Rows)
}
}
Esse desenho já parece um pipeline pequeno: uma fila de trabalho, alguns workers e um canal de resultados. Ainda faltaria bastante coisa para produção, principalmente erro por arquivo, contexto de cancelamento, log estruturado e métricas. Mas o caminho fica claro.
Onde Go já faz sentido na Engenharia de Dados
Depois desses 10 dias, eu não colocaria Go como substituto de Python. O ponto não é esse. Para mim, Go começa a fazer sentido quando a rotina deixa de ser "um script que alguém roda" e vira um componente de plataforma.
Alguns cenários em que eu já consigo imaginar uso sem forçar a barra:
- ETL de alta performance, principalmente quando a etapa é leitura, validação e transformação de arquivo grande.
- Workers concorrentes consumindo filas, tópicos ou lotes de arquivos.
- APIs de ingestão que recebem eventos e precisam responder rápido.
- Ferramentas CLI para automações internas, validações, deploys e rotinas operacionais.
- Serviços pequenos em que distribuição simples e baixo consumo de memória fazem diferença.
Para notebook, análise exploratória e bibliotecas estatísticas, Python continua muito mais confortável. Para componente executável, concorrente e fácil de distribuir, Go começa a ocupar um espaço interessante.
Dificuldades encontradas
A primeira dificuldade foi aceitar que algumas facilidades do Python simplesmente não estão ali. Senti falta de list comprehension, de manipular dicionário livremente e daquela velocidade de testar uma ideia em poucos minutos.
Também achei alguns pontos verbosos. Tratamento explícito de erro é o exemplo mais óbvio. Quem vem de try/except estranha ver if err != nil tantas vezes. Por outro lado, isso força uma pergunta que em Python às vezes fica escondida: o que eu faço se essa etapa falhar?
A maior curva mental, sem dúvida, foi concorrência. Criar goroutine é fácil. Saber quem fecha o channel, quem espera quem, onde o erro aparece e como evitar bloqueio já é outra conversa.
Essa parte ainda merece muito estudo. Foi justamente onde vi mais valor, mas também onde dá para errar bonito se a implementação for feita no automático.
Próximos passos
Depois dessa primeira etapa, quero avançar para pontos mais próximos do uso real:
- testes em Go, incluindo testes de unidade e tabelados;
- integração com bancos relacionais;
- leitura e escrita de formatos comuns em pipelines;
- criação de APIs HTTP;
- projetos reais com workers, filas e observabilidade.
Também quero fazer alguns testes comparando a mesma rotina em Python e Go. Não só tempo de execução, mas consumo de memória, simplicidade de deploy e manutenção do código. Às vezes a versão mais rápida não é a melhor para o time. Às vezes é exatamente ela que evita dor de cabeça.
Conclusão
Minha impressão, por enquanto, é bem prática: Go não substitui Python em engenharia de dados. Pelo menos não no meu dia a dia. Python continua sendo a escolha mais natural para explorar dados, testar hipótese, trabalhar com bibliotecas maduras e resolver automação pequena sem cerimônia.
Go entra em outro espaço. Quando a rotina precisa virar worker, API, CLI ou serviço com execução concorrente e distribuição simples, a conversa muda. A linguagem força um pouco mais de disciplina, mas devolve previsibilidade.
Depois desses 10 dias, a conclusão é simples: aprender Go não é abandonar Python. É aumentar o repertório para escolher melhor. Faça seus próprios testes, compare com problemas reais e veja onde a linguagem encaixa. É assim que a ferramenta deixa de ser curiosidade e começa a virar opção de trabalho.