Go Modules: go.mod, go.sum e gerenciamento de dependências

Go Modules: go.mod, go.sum e gerenciamento de dependências

Go Modules: go.mod e go.sum, go get e go mod tidy, versionamento semântico e o /v2 no caminho, a seleção de versão mínima, replace que só vale no módulo principal, workspaces e o go.work fora do git, GOPRIVATE para módulos privados e a diretiva tool do Go 1.24.
Golang

• • 15 min de leitura

Antes dos módulos, Go usava o GOPATH — um diretório global onde todos os projetos e dependências conviviam. Não havia versionamento de dependências, não havia isolamento entre projetos e atualizar uma biblioteca podia quebrar todos os projetos simultaneamente. Era um sistema funcional para projetos pequenos e experimentos, mas inadequado para desenvolvimento profissional em escala.

Go Modules, introduzido como experimental no Go 1.11 e tornado padrão no Go 1.16, resolveu todos esses problemas. Cada projeto tem seu próprio conjunto de dependências versionadas, isolado dos demais. Builds são reproduzíveis — o mesmo código usa exatamente as mesmas versões de dependências em qualquer máquina. Atualizações são explícitas e controladas.

O arquivo go.mod

O go.mod é o manifesto do módulo. Ele declara o nome do módulo, a versão mínima do Go necessária e as dependências — as diretas e, desde o Go 1.17, também as indiretas necessárias para o build, marcadas com // indirect:

module github.com/ricardomattos/loja-api

go 1.22

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/golang-jwt/jwt/v5 v5.2.0
    gorm.io/gorm v1.25.7
    gorm.io/driver/postgres v1.5.6
)

require (
    // dependências indiretas — usadas por dependências diretas
    github.com/bytedance/sonic v1.11.3 // indirect
    github.com/gabriel-vasile/mimetype v1.4.3 // indirect
    golang.org/x/net v0.22.0 // indirect
)

O caminho do módulo — github.com/ricardomattos/loja-api — é o identificador único do módulo. Por convenção, projetos publicados usam o caminho do repositório onde o código reside. Para projetos privados ou locais, qualquer nome descritivo funciona.

O arquivo go.sum

O go.sum é o guardião da integridade das dependências. Para cada módulo listado no go.mod, o go.sum armazena o hash criptográfico do conteúdo esperado:

github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=

Quando você ou um colega executa go build ou go test, o Go verifica se os módulos baixados correspondem aos hashes registrados. Se alguém alterar o conteúdo de uma dependência publicada — seja maliciosamente ou por acidente — a verificação falha e o build é recusado.

Nunca edite o go.sum manualmente. Ele é gerenciado automaticamente pelos comandos go. Sempre faça commit de ambos go.mod e go.sum no controle de versão.

Comandos essenciais

Inicializando um módulo:

mkdir loja-api
cd loja-api
go mod init github.com/ricardomattos/loja-api

Adicionando uma dependência:

A forma mais direta é simplesmente importar o pacote no código e executar go mod tidy. Ou adicionar explicitamente com go get:

go get github.com/gin-gonic/gin@v1.9.1
go get gorm.io/gorm@latest

O sufixo @versão aceita: - @v1.9.1 — versão exata - @latest — versão mais recente estável - @main — branch específica (evite em produção) - @v1.9 — prefixo de versão

Removendo dependências não utilizadas e adicionando as faltantes:

go mod tidy

Este é o comando mais usado no dia a dia. Ele analisa todo o código do módulo, remove do go.mod dependências que não são mais importadas, adiciona as que estão sendo importadas mas não declaradas e atualiza o go.sum. Execute go mod tidy antes de todo commit.

Atualizando dependências:

# Atualizar uma dependência específica para a versão mais recente
go get github.com/gin-gonic/gin@latest

# Atualizar as dependências dos pacotes do módulo, indiretas inclusive, para o patch mais recente
go get -u=patch ./...

# O mesmo, aceitando versões minor novas
go get -u ./...

Verificando dependências:

# Lista todas as dependências e suas versões
go list -m all

# Mostra por que uma dependência está no go.mod
go mod why github.com/gin-gonic/gin

# Verifica se o go.sum está correto
go mod verify

Versionamento semântico

Go Modules adota versionamento semântico — semver — com três componentes: MAJOR.MINOR.PATCH.

  • PATCH — correções de bugs sem quebrar compatibilidade: v1.2.3 → v1.2.4
  • MINOR — novas funcionalidades sem quebrar compatibilidade: v1.2.3 → v1.3.0
  • MAJOR — mudanças incompatíveis com versões anteriores: v1.2.3 → v2.0.0

Go trata versões MAJOR diferentes como módulos completamente distintos. Um módulo na versão v2 ou superior deve ter /v2 no final do caminho do módulo:

github.com/autor/biblioteca       ← v0 e v1
github.com/autor/biblioteca/v2    ← v2
github.com/autor/biblioteca/v3    ← v3

Isso permite que um projeto importe v1 e v2 de uma mesma biblioteca simultaneamente sem conflito — cada versão major é tratada como um módulo independente.

Algoritmo de versão mínima (MVS)

O Go usa o Minimum Version Selection para resolver versões de dependências, em vez de "pegar a mais recente". Dado que o projeto A requer B@v1.2 e C@v1.0, e que C@v1.0 requer B@v1.3, o Go seleciona B@v1.3 — a versão mínima que satisfaz todos os requisitos.

Esse algoritmo tem uma propriedade valiosa: ele é determinístico e conservador. Atualizar uma dependência direta nunca atualiza automaticamente outras dependências além do necessário. Isso torna os builds previsíveis e as atualizações controláveis.

Replace e exclude

O go.mod suporta diretivas avançadas para situações especiais.

replace — substitui um módulo por outro, útil para desenvolvimento local ou para usar um fork:

replace (
    // Usar versão local durante desenvolvimento
    github.com/ricardomattos/pacote-compartilhado => ../pacote-compartilhado

    // Usar um fork em vez do original
    github.com/original/biblioteca => github.com/meufork/biblioteca v1.0.0-fork
)

Remova todas as diretivas replace de desenvolvimento antes de publicar o módulo — elas não funcionam para quem baixar sua biblioteca.

exclude — exclui uma versão específica de uma dependência, geralmente por conter um bug crítico:

exclude github.com/alguma/biblioteca v1.5.2

Workspaces: múltiplos módulos locais

Go 1.18 introduziu workspaces (go work) para facilitar o desenvolvimento simultâneo de múltiplos módulos relacionados sem precisar usar replace:

# Na raiz que contém os dois módulos
go work init ./loja-api ./pacote-compartilhado

Isso cria um arquivo go.work:

go 1.22

use (
    ./loja-api
    ./pacote-compartilhado
)

Com o workspace ativo, mudanças em pacote-compartilhado são imediatamente visíveis em loja-api sem necessidade de publicar ou usar replace. O go.work não deve ser commitado — adicione-o ao .gitignore.

Cache de módulos

Go mantém um cache local de todos os módulos baixados em $GOPATH/pkg/mod. Uma vez baixado, um módulo não é baixado novamente — mesmo em projetos diferentes que usam a mesma versão.

# Ver o diretório do cache
go env GOMODCACHE

# Limpar o cache completamente
go clean -modcache

O cache é somente-leitura após o download, o que evita modificações acidentais. Se precisar editar uma dependência, faça um fork e use replace.

Proxy de módulos

Por padrão, Go baixa módulos através do proxy oficial proxy.golang.org, que serve como cache e espelho de módulos públicos. Isso garante disponibilidade mesmo se o repositório original for removido.

A variável GOPROXY controla quais proxies são usados:

# Padrão — proxy oficial com fallback direto
GOPROXY=https://proxy.golang.org,direct

# Sem proxy — acesso direto aos repositórios
GOPROXY=direct

# Proxy corporativo para ambientes fechados
GOPROXY=https://proxy.minha-empresa.com,direct

Para módulos privados, que o proxy público não alcança e a base de checksums pública não conhece, a variável que resolve os dois lados de uma vez é GOPRIVATE: go env -w GOPRIVATE=github.com/minha-empresa/* faz o Go buscar esses módulos direto no repositório e dispensar a consulta à base de checksums. Por baixo, ela define os valores padrão de GONOPROXY e GONOSUMDB, que existem para quando os dois comportamentos precisam ser ajustados separadamente.

Exemplo completo: projeto real com dependências

mkdir api-tarefas
cd api-tarefas
go mod init github.com/ricardomattos/api-tarefas
// main.go
package main

import (
    "github.com/ricardomattos/api-tarefas/internal/handler"
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()
    r.GET("/tarefas", handler.ListarTarefas)
    r.POST("/tarefas", handler.CriarTarefa)
    r.Run(":8080")
}
# Adicionar dependências detectadas no código
go mod tidy

Após go mod tidy, o go.mod será atualizado automaticamente com Gin e todas as suas dependências transitivas. O go.sum será gerado com os hashes correspondentes.

# Verificar o resultado
cat go.mod

# Confirmar que tudo está correto
go build ./...
go test ./...

Com módulos, as dependências de um projeto Go ficam descritas em dois arquivos de texto que entram no controle de versão: o go.mod diz quais versões usar, o go.sum garante que o conteúdo delas é o esperado. O resto decorre de escolhas conservadoras — a seleção de versão mínima, que só muda o build quando alguém pede, o caminho com /v2 para cada quebra de compatibilidade e o replace que só vale no projeto principal. No dia a dia, go mod tidy antes do commit e um diff do go.mod revisado com atenção resolvem quase tudo.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Um projeto exige B v1.2.0 e usa C, que exige B v1.3.0. Na semana seguinte, sai o B v1.9.0. Um desenvolvedor vindo do npm teme que o próximo go build no CI puxe a 1.9 sem ninguém pedir. Qual versão de B entra no build hoje, qual entra na semana que vem, e o que precisaria acontecer para a 1.9 aparecer?

Ver resposta

✓ Resposta: Hoje e na semana que vem entra a mesma: B v1.3.0. Pela seleção de versão mínima que o artigo descreve, o Go junta todas as exigências do grafo e escolhe, para cada módulo, a maior das mínimas declaradas: a 1.2 do projeto e a 1.3 de C resultam em 1.3. A existência de uma 1.9 no mundo não entra na conta, porque ninguém no grafo a pediu. É a diferença para o ^1.2.0 do npm, que aceita qualquer versão compatível mais nova e, sem lockfile, muda o build sozinho. No Go, o próprio go.mod já funciona como trava, e o go.sum garante que o conteúdo de cada versão é o mesmo em todas as máquinas. Para a 1.9 entrar, alguém precisa pedir de forma explícita, com go get B@v1.9.0, go get B@latest ou go get -u, e essa mudança aparece como diff no go.mod, que passa pela revisão como qualquer outro código.

Exercício 2

Uma biblioteca interna quebra a compatibilidade da API e o time cria a tag v2.0.0 sem mexer no go.mod, que continua declarando module github.com/empresa/lib. Os consumidores não conseguem atualizar. O que ficou faltando, e por que o Go exige esse passo?

Ver resposta

✓ Resposta: Faltou o sufixo de versão maior no caminho do módulo. A partir da v2, o go.mod precisa declarar module github.com/empresa/lib/v2, e os consumidores importam github.com/empresa/lib/v2/.... Sem isso, o Go recusa a tag v2 para aquele caminho, porque um módulo com go.mod só pode ter versões v0 e v1 no caminho sem sufixo. A regra vem do que o artigo chama de módulos distintos: se a v2 quebra a API, ela é tratada como outro módulo, com outro caminho, e um programa pode até ter v1 e v2 ao mesmo tempo, uma usada por uma dependência antiga e outra pelo código novo, sem conflito. O passo completo é mudar a linha module, atualizar os imports internos da própria biblioteca para o caminho com /v2 e só então criar a tag. Do lado dos consumidores, migrar é trocar os imports, e go get github.com/empresa/lib/v2@latest traz a versão nova.

Exercício 3

Uma biblioteca publicada tem no seu go.mod a linha replace github.com/outro/dep => github.com/empresa/dep-corrigida v1.0.1, com uma correção importante. Um projeto que usa a biblioteca continua recebendo o bug. Por que o replace não teve efeito, e como fazer a correção chegar aos consumidores?

Ver resposta

✓ Resposta: Diretivas replace e exclude só valem no go.mod do módulo principal, aquele em que o go build está sendo executado. Os replace das dependências são ignorados, e o artigo aponta esse efeito ao pedir que se removam os replace antes de publicar. A regra existe para que nenhuma dependência consiga desviar em silêncio o código que o seu projeto usa: só o dono do build decide substituições. Há três saídas. A melhor é levar a correção para o módulo original e, depois que ela for publicada, subir a versão mínima no require da biblioteca. Enquanto isso não acontece, cada projeto consumidor pode pôr o mesmo replace no próprio go.mod, conscientemente. A terceira é a biblioteca passar a depender do fork com o caminho do fork, o que funciona para todos mas dificulta voltar ao original depois.

Exercício 4

Um desenvolvedor usa go work para trabalhar em dois módulos ao mesmo tempo e, por engano, faz commit do go.work, que aponta para ../pacote-compartilhado. Na máquina dele tudo compila; no CI, o build quebra ou, pior, usa uma versão diferente da declarada no go.mod. O que aconteceu, e como o CI pode se proteger?

Ver resposta

✓ Resposta: Quando existe um go.work no diretório atual ou em algum acima, o comando go entra em modo workspace e usa os módulos listados em use a partir das pastas locais, no lugar das versões do require. Na máquina do desenvolvedor, ../pacote-compartilhado existe e tem as mudanças em andamento. No CI, ou a pasta não existe e o build falha, ou existe com outro conteúdo e o build passa com um código que não corresponde a nenhuma versão publicada, o que é o pior cenário, porque ninguém percebe. Por isso o artigo recomenda manter o go.work fora do controle de versão, no .gitignore. Para o CI não depender da disciplina de cada um, basta rodar os comandos com GOWORK=off, que desliga o modo workspace mesmo que o arquivo exista, e ter também um passo com go mod tidy seguido de git diff --exit-code, que falha se o go.mod ou o go.sum commitados não baterem com o código.

Exercício 5

O projeto usa o gerador stringer e o golangci-lint, e cada desenvolvedor instala as ferramentas com go install ...@latest, cada um numa versão diferente. Como fixar a versão dessas ferramentas no próprio módulo, com os recursos do Go atual?

Ver resposta

✓ Resposta: Desde o Go 1.24, o go.mod tem a diretiva tool. go get -tool golang.org/x/tools/cmd/stringer@v0.x.y acrescenta uma linha tool com o caminho do pacote e registra o módulo no require com a versão escolhida, e o go.sum ganha os hashes, como em qualquer dependência. A partir daí, go tool stringer compila e executa exatamente aquela versão, em qualquer máquina e no CI, sem instalação à parte; e go generate pode chamar go tool stringer nas diretivas //go:generate. Atualizar vira um go get -tool ...@nova-versão, revisado no diff do go.mod como qualquer outra mudança, e go get -tool caminho@none remove a ferramenta. Antes disso, o truque era um arquivo tools.go com imports em branco e uma build tag para excluí-lo do binário; ainda aparece em projetos antigos e pode ser migrado. Ferramentas com muitas dependências, como linters grandes, às vezes ficam num módulo separado, para não misturar o grafo delas com o do projeto.

Comentários

Mais em Golang

Compilação cruzada, binários estáticos e Docker com Go
Compilação cruzada, binários estáticos e Docker com Go

Compilação cruzada e Docker com Go: -ldflags e o que o debug.ReadBuildInfo já…

Goroutines: concorrência leve e o modelo de execução do Go
Goroutines: concorrência leve e o modelo de execução do Go

Goroutines em Go: o custo comparado ao de threads, o scheduler M:N e a…

Linting, formatação e análise estática: gofmt, golangci-lint
Linting, formatação e análise estática: gofmt, golangci-lint

Qualidade de código em Go: gofmt, goimports, go vet e staticcheck com exemplos…