Go modules: gerenciamento de dependências e versionamento

1. Visão geral

Desde Go 1.11, Go modules é o mecanismo oficial para gerenciar dependências. A partir de Go 1.16, é o modo padrão, substituindo completamente a abordagem antiga baseada em GOPATH. Este artigo percorre o ciclo de vida completo de um módulo: inicialização, adição de dependências, seleção e atualização de versões, tratamento de major versions v2+, e trabalho com código local ou repositórios privados. Após ler, você entenderá o que go.mod e go.sum contêm. Você também saberá por que Go escolhe uma versão específica e como corrigir os erros comuns de dependências.

2. Inicializando um módulo

Um módulo é uma árvore de diretórios com um arquivo go.mod na raiz. Crie um novo módulo com go mod init:

mkdir order-service && cd order-service
go mod init github.com/nonla/order-service

O arquivo go.mod é criado:

module github.com/nonla/order-service

go 1.24

O significado de cada linha:

  • module é o module path. Este é o prefixo de import para todos os pacotes dentro do módulo. Se o módulo for importado por outras pessoas com go get, este caminho deve corresponder à URL do repositório.
  • go 1.24 é a versão mínima da linguagem que o módulo requer. A partir de Go 1.21, esta linha é obrigatória. Se sua toolchain for mais antiga, Go carregará automaticamente a toolchain apropriada (quando GOTOOLCHAIN=auto) ou relatará um erro.

Nota: Para aplicações internas que nunca serão importadas de fora, você pode usar um caminho curto como order-service. No entanto, usar um caminho em forma de URL desde o início evitará ter que renomear muitos imports depois.

3. Adicionando dependências

3.1. Usando go get

Suponha que você precisa gerar IDs para pedidos usando UUID. Adicione a biblioteca github.com/google/uuid:

go get github.com/google/uuid@v1.6.0

Em seguida, escreva o código que usa a biblioteca:

// order/order.go
package order

import "github.com/google/uuid"

type Order struct {
	ID       string
	Customer string
	Total    int64 // unidade: đồng
}

func NewOrder(customer string, total int64) Order {
	return Order{
		ID:       uuid.NewString(),
		Customer: customer,
		Total:    total,
	}
}

Agora go.mod tem um bloco adicional require:

module github.com/nonla/order-service

go 1.24

require github.com/google/uuid v1.6.0

Você também pode pular a etapa go get: escreva o import primeiro, depois execute go mod tidy. Go encontrará automaticamente a versão mais recente e a gravará em go.mod.

3.2. O arquivo go.sum

Ao mesmo tempo, Go cria o arquivo go.sum:

github.com/google/uuid v1.6.0 h1:<hash nội dung module>
github.com/google/uuid v1.6.0/go.mod h1:<hash của file go.mod>

Cada dependência tem duas linhas. A primeira é o hash de todo o conteúdo do módulo, a segunda é o hash do arquivo go.mod. Ao fazer download de um módulo, Go compara o hash real com go.sum. Também verifica com o banco de dados de checksum público (sum.golang.org). Se alguém modificar o conteúdo de uma tag já lançada, o build falhará em vez de silenciosamente usar código incorreto.

Nota: Sempre faça commit de go.mod e go.sum. Não adicione go.sum ao .gitignore. Se este arquivo estiver faltando, a máquina CI não conseguirá verificar as dependências.

3.3. Dependências diretas e indiretas

Adicione testify para escrever testes:

go get github.com/stretchr/testify@v1.10.0

go.mod agora terá dois blocos require:

require (
	github.com/google/uuid v1.6.0
	github.com/stretchr/testify v1.10.0
)

require (
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)

O comentário // indirect marca um módulo que seu código não importa diretamente, mas que é necessário para outra dependência. Go os grava em go.mod para que o build seja totalmente reproduzível. Assim, Go não precisa ler o go.mod de cada dependência novamente para construir o gráfico.

4. Versioning e como Go escolhe versões

4.1. Semantic versioning

Go modules usa semantic versioning no formato vMAJOR.MINOR.PATCH:

  • PATCH (v1.6.0 → v1.6.1): correções de bugs, sem alterações de API.
  • MINOR (v1.6.0 → v1.7.0): adiciona recursos, mantém compatibilidade reversa.
  • MAJOR (v1 → v2): mudanças que quebram compatibilidade.

As versões devem ser git tags com o prefixo v. Se você apontar para um commit sem tag, Go cria uma pseudo-versão:

go get github.com/google/uuid@0f3b7a2
github.com/google/uuid v1.6.1-0.20240815101530-0f3b7a2c1d4e

A pseudo-versão tem três partes: versão base, timestamp UTC do commit e os 12 primeiros caracteres do hash. Assim, a pseudo-versão ainda ordena corretamente pela hora.

4.2. Seleção de versão mínima

Esta é a maior diferença em relação ao npm ou Maven. Go usa o algoritmo Minimal Version Selection (MVS). Para cada módulo, Go escolhe a versão mais alta entre aquelas que são necessárias, não a versão mais recente que existe.

Por exemplo, seu projeto tem duas dependências:

order-service
├── payment-sdk  → require github.com/google/uuid v1.3.0
└── shipping-sdk → require github.com/google/uuid v1.5.0

Mesmo que uuid v1.6.0 tenha sido lançado, Go ainda escolhe v1.5.0. Razão: essa é a versão mais baixa que satisfaz todos os requisitos. Consequências:

  • O build é reproduzível. Uma biblioteca lançar uma nova versão não muda automaticamente seu build.
  • Não é necessário um arquivo de lock separado. go.mod é suficiente para determinar as versões.
  • Para usar uma versão mais recente, você deve fazer upgrade de forma proativa.

Veja quais versões foram realmente escolhidas com o comando:

go list -m all
github.com/nonla/order-service
github.com/davecgh/go-spew v1.1.1
github.com/google/uuid v1.5.0
...

4.3. Upgrading e downgrading

Verifique quais dependências têm novas versões:

go list -m -u all
github.com/google/uuid v1.5.0 [v1.6.0]
github.com/stretchr/testify v1.10.0

A versão entre colchetes é a versão mais recente que você pode fazer upgrade. Os comandos de upgrade mais comuns são:

go get github.com/google/uuid@latest   # versão mais recente
go get github.com/google/uuid@v1.5.0   # versão específica (pode fazer downgrade)
go get -u=patch ./...                  # apenas PATCH para todas as dependências
go get -u ./...                        # MINOR + PATCH para todas as dependências
go get github.com/google/uuid@none     # remove a dependência

Nota: go get -u ./... atualiza todas as dependências, incluindo as indiretas. O diff pode ser muito grande. Em projetos de produção, você deve atualizar cada módulo, executar testes e só então fazer commit.

5. Major version v2 e acima

Go aplica a regra de import compatibility: duas versões com o mesmo import path devem ser compatíveis para trás. Portanto, a partir de v2, a major version faz parte do module path.

Por exemplo, com o router chi:

go get github.com/go-chi/chi/v5@v5.1.0
package main

import (
	"net/http"

	"github.com/go-chi/chi/v5"
)

func main() {
	r := chi.NewRouter()
	r.Get("/orders", func(w http.ResponseWriter, _ *http.Request) {
		w.Write([]byte("lista de pedidos"))
	})
	http.ListenAndServe(":8080", r)
}

O sufixo /v5 está no import path. O nome do pacote ainda é chi, então você chama chi.NewRouter() normalmente. Já que chi/v4 e chi/v5 têm paths diferentes, ambas as versões podem coexistir no mesmo binário. Isso permite que você migre gradualmente.

Se você é o autor de uma biblioteca e deseja lançar v2:

module github.com/nonla/pricing/v2

go 1.24

Você muda a linha module para um path com o sufixo /v2, atualiza os imports internos e marca a tag v2.0.0.

Nota: Um erro comum é apenas marcar a tag v2.0.0 e esquecer de alterar o module path. Nesse caso, go get github.com/nonla/pricing@v2.0.0 relatará um erro como invalid version: module contains a go.mod file, so module path must match major version.

6. Comandos úteis de go mod

go mod tidy      # adiciona módulos faltando, remove módulos não utilizados
go mod why -m gopkg.in/yaml.v3   # explica por que o módulo está presente
go mod graph     # imprime o gráfico de dependências completo
go mod verify    # verifica se cache local corresponde com go.sum
go mod download  # faz download das dependências antecipadamente (útil para camadas Docker)

Exemplo de saída de go mod why:

# gopkg.in/yaml.v3
github.com/nonla/order-service/order
github.com/stretchr/testify/assert
gopkg.in/yaml.v3

Leia de cima para baixo: o pacote order (no teste) importa testify/assert, e testify/assert por sua vez importa yaml.v3.

Execute go mod tidy antes de cada commit. No CI, você pode verificar se go.mod já está limpo:

go mod tidy -diff

A partir de Go 1.23, a flag -diff imprime a diferença e retorna um código de saída diferente de 0 se o arquivo ainda não está tidy. Este comando não sobrescreve o arquivo.

7. Trabalhando com código local: replace e go.work

7.1. A diretiva replace

Suponha que você está corrigindo a biblioteca pricing em um diretório adjacente e quer que order-service use a versão que está sendo corrigida:

require github.com/nonla/pricing v1.2.0

replace github.com/nonla/pricing => ../pricing

Go lerá o código de ../pricing em vez de fazer download da rede. replace também pode apontar para um fork:

replace github.com/nonla/pricing => github.com/minh/pricing v1.2.1-fix

Nota: replace só funciona no módulo principal. Se seu módulo é uma biblioteca, os usuários não receberão o replace. Além disso, não faça commit de replace apontando para caminhos locais. O build no CI falhará porque o diretório ../pricing não existirá lá.

7.2. Workspace com go.work

A partir de Go 1.18, workspace é uma forma mais limpa de desenvolver múltiplos módulos simultaneamente:

go work init ./order-service ./pricing

O arquivo go.work é criado:

go 1.24

use (
	./order-service
	./pricing
)

Ao executar go build ou go test no workspace, qualquer import de github.com/nonla/pricing usará o código local. Você não precisa alterar go.mod. Normalmente go.work não é feito commit; cada desenvolvedor cria seu próprio baseado em sua organização de diretórios.

7.3. exclude e retract

exclude é usado no módulo principal para descartar uma versão específica:

exclude github.com/google/uuid v1.4.0

retract é declarado pelo autor da biblioteca para indicar que uma versão já lançada não deve ser usada:

retract v1.2.0 // cálculo incorreto de imposto VAT para pedidos internacionais

Depois de você lançar uma nova versão contendo esta linha, go get @latest ignorará v1.2.0. O comando go list -m -u também alertará qualquer pessoa que esteja usando essa versão.

8. Módulos privados e proxy

Por padrão, Go faz download de módulos via GOPROXY=https://proxy.golang.org,direct e verifica checksums via sum.golang.org. Para repositórios internos, o proxy público não pode acessar, e você também não quer expor os nomes dos módulos. Declare:

go env -w GOPRIVATE=github.com/nonla/*,gitlab.nonla.internal/*

Módulos que correspondem a padrões em GOPRIVATE serão baixados diretamente via git e ignorarão o banco de dados de checksums. Você ainda precisa configurar autenticação para git, por exemplo via ~/.netrc ou SSH:

git config --global url."git@github.com:".insteadOf "https://github.com/"

Se sua empresa tem um proxy privado (Athens, Artifactory…), coloque esse proxy no início de GOPROXY:

go env -w GOPROXY=https://goproxy.nonla.internal,https://proxy.golang.org,direct

9. Testando comportamento com dependências

Escreva testes para NewOrder usando testify adicionado na seção 3.3:

// order/order_test.go
package order

import (
	"testing"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestNewOrder_GeneratesValidUniqueID(t *testing.T) {
	first := NewOrder("Nguyễn Văn An", 250_000)
	second := NewOrder("Trần Thị Bình", 120_000)

	_, err := uuid.Parse(first.ID)
	require.NoError(t, err)

	assert.NotEqual(t, first.ID, second.ID)
	assert.Equal(t, "Nguyễn Văn An", first.Customer)
	assert.EqualValues(t, 250_000, first.Total)
}

Execute os testes:

go test ./...
ok  	github.com/nonla/order-service/order	0.004s

O teste verifica três coisas: o ID é um UUID válido, os dois pedidos têm IDs diferentes e os outros campos foram atribuídos corretamente. require.NoError interrompe o teste imediatamente se o ID não for analisável. assert registra o erro e continua executando. Na primeira execução em uma máquina nova, Go baixará a dependência com a versão exata em go.mod e verificará o hash com go.sum antes de compilar.

10. Erros comuns

  • missing go.sum entry for module providing package: go.sum está faltando a linha correspondente, geralmente porque alguém editou go.mod manualmente. Solução: execute go mod tidy.
  • checksum mismatch: o conteúdo do módulo não corresponde ao hash. Tente go clean -modcache. Se ainda falhar com um módulo público, considere como um sinal sério e não remova a linha de go.sum para contornar.
  • go: updates to go.mod needed: CI geralmente é executado com -mod=readonly, enquanto go.mod ainda não foi tidy. Execute go mod tidy localmente e faça commit novamente.
  • Importar a major version errada: usar github.com/go-chi/chi em vez de .../chi/v5. Você receberá uma versão v1 antiga ou uma pseudo-versão estranha. Verifique o import path primeiro.
  • Abuso de vendor/: go mod vendor ainda é útil quando você precisa fazer um build completamente offline. Caso contrário, o proxy combinado com go.sum já garante reprodutibilidade, portanto você não precisa fazer commit de milhares de arquivos vendor.

11. Conclusão

Go modules resolve o problema de dependências com algumas ideias simples. go.mod declara requisitos, go.sum garante integridade, e Minimal Version Selection torna o build reproduzível sem a necessidade de um arquivo de lock. A major version está diretamente no import path, portanto mudanças que quebram compatibilidade sempre aparecem claramente no código.

Para o trabalho diário, você só precisa lembrar de quatro hábitos:

  • Use go get module@version para adicionar ou atualizar dependências intencionalmente.
  • Execute go mod tidy antes de fazer commit.
  • Use go.work em vez de replace ao desenvolver múltiplos módulos simultaneamente.
  • Configure GOPRIVATE para repositórios internos.
golanggo modulesdependencyversioning