Um módulo Go é uma árvore de pacotes sob um único go.mod. Veja como funcionam import paths, nomes exportados e pastas internal, e depois teste o código com testes table-driven, subtests, examples e benchmarks.
Até aqui, todo programa desta série coube em um único arquivo chamado main.go. Código Go de verdade não fica tão pequeno. Você divide o código em pacotes, os pacotes vivem em um módulo, e o módulo vem com testes que o go test executa.
Este post monta um módulo pequeno, um contador de palavras com uma biblioteca, um pacote auxiliar privado, um comando e testes, e passa por cada arquivo. Todo programa abaixo rodou no Go 1.26, e a saída foi colada da execução. O código mostrado vem direto dos arquivos do módulo, e esses arquivos passam em go vet, go test e go test -race.
Um pacote é uma pasta, um módulo é uma árvore delas
Um pacote Go é o conjunto de arquivos .go de uma pasta, e um módulo é uma árvore de pastas de pacotes com um arquivo go.mod no topo. Este é o módulo inteiro que o post usa:
11-wordcount/
├── go.mod
├── wordcount.go
├── wordcount_test.go
├── example_test.go
├── internal/
│ └── tokenize/
│ ├── tokenize.go
│ └── tokenize_test.go
└── cmd/
└── wc/
├── main.go
└── main_test.go
São três pacotes. Os arquivos do topo formam o pacote wordcount, a biblioteca. internal/tokenize divide o texto em palavras. cmd/wc é um programa pequeno que usa a biblioteca.
Todo arquivo de uma pasta precisa declarar o mesmo nome de pacote na primeira linha, e a pasta guarda exatamente um pacote. A única exceção são os arquivos de teste, que aparecem mais abaixo.
O arquivo go.mod tem duas linhas:
module example.com/wordcount
go 1.26
A linha module é o module path. Ele é o prefixo de todo import path dentro do módulo. Some a pasta de um pacote ao final, e você tem o import path dele. O go list mostra todos:
$ go list ./...
example.com/wordcount
example.com/wordcount/cmd/wc
example.com/wordcount/internal/tokenize
./... quer dizer “esta pasta e todas as pastas abaixo dela”. Você também vai usar isso com go test, go vet e go build.
Então o import path não é um nome de arquivo, e também não é um caminho no seu disco. É o module path unido ao caminho da pasta. O nome do pacote, a palavra depois de package, costuma ser a última parte desse caminho, e é o que você digita antes do ponto: tokenize.Words, wordcount.Count.
Nomes exportados são a única porta de entrada
Código em um pacote Go só pode usar os nomes de outro pacote quando eles começam com letra maiúscula. Você viu essa regra com strings.ToUpper na primeira parte. Ela vale do mesmo jeito para os pacotes que você escreve. Esta é a biblioteca:
// Package wordcount counts words in text.
package wordcount
import (
"cmp"
"slices"
"strings"
"example.com/wordcount/internal/tokenize"
)
// Pair is one word and how many times it appeared.
type Pair struct {
Word string
Count int
}
// Count returns how many times each word appears in text.
// Words are compared without regard to case.
func Count(text string) map[string]int {
counts := map[string]int{}
for _, w := range tokenize.Words(text) {
counts[normalize(w)]++
}
return counts
}
// Top returns the n most frequent words, most frequent first.
// Words with the same count are sorted alphabetically.
func Top(counts map[string]int, n int) []Pair {
pairs := make([]Pair, 0, len(counts))
for w, c := range counts {
pairs = append(pairs, Pair{Word: w, Count: c})
}
slices.SortFunc(pairs, func(a, b Pair) int {
if c := cmp.Compare(b.Count, a.Count); c != 0 {
return c
}
return cmp.Compare(a.Word, b.Word)
})
return pairs[:min(n, len(pairs))]
}
// normalize is unexported: only code in package wordcount can call it.
func normalize(word string) string {
return strings.ToLower(word)
}
Pair, Count e Top começam com maiúscula, então são exportados. Os campos Word e Count também. normalize começa com minúscula, então só código do pacote wordcount pode chamá-la. Count chama normalize à vontade, porque as duas vivem no mesmo pacote.
Os empates em Top são desfeitos em ordem alfabética de propósito. A ordem de iteração de um map muda de uma execução para outra, e uma função que devolve resultados em ordem aleatória é difícil de testar.
Para ver a regra em ação, adicionei uma example function a um arquivo de teste que fica fora do pacote e chama wordcount.normalize("Go"). O go test se recusou a compilar:
$ go test .
# example.com/wordcount_test [example.com/wordcount.test]
./example_test.go:27:24: undefined: wordcount.normalize
FAIL example.com/wordcount [build failed]
Repare no que a mensagem diz. Ela não diz “normalize é privada”. Ela diz undefined. De fora do pacote, um nome não exportado simplesmente não existe.
É isso que torna seguro mudar um nome não exportado. Você pode renomear normalize, mudar os argumentos ou apagá-la, e nenhum código fora do pacote quebra, porque nenhum consegue alcançá-la.
internal/: pacotes que só este módulo pode importar
Uma pasta chamada internal faz com que os pacotes abaixo dela só possam ser importados por código que está sob a pasta que contém o internal. Aqui o internal fica no topo do módulo, então todo pacote do módulo pode importar tokenize, e nada de fora pode:
// Package tokenize splits text into words.
package tokenize
import (
"strings"
"unicode"
)
// Words splits text into words. A word is a run of letters, digits and
// apostrophes. Apostrophes at either end of a word are dropped.
func Words(text string) []string {
fields := strings.FieldsFunc(text, func(r rune) bool {
return !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '\''
})
words := fields[:0]
for _, f := range fields {
if w := strings.Trim(f, "'"); w != "" {
words = append(words, w)
}
}
return words
}
Words é exportada, então o pacote wordcount pode chamar tokenize.Words. Mas ela só é exportada dentro do módulo.
Para conferir, criei um segundo módulo, example.com/other, que importa os dois pacotes:
$ go build .
package example.com/other
main.go:7:2: use of internal package example.com/wordcount/internal/tokenize not allowed
Importar o próprio example.com/wordcount funcionou. O import de internal/tokenize foi recusado pelo comando go antes de o compilador olhar uma única linha.
Isso dá a você um lugar para o código que quer compartilhar entre os seus próprios pacotes sem prometer nada a mais ninguém. Se tokenize fosse um pacote normal, alguém poderia importá-lo, e mudar Words quebraria o código dessa pessoa. Dentro de internal, você muda quando quiser.
Explicado como se você tivesse dez anos
Pense em um módulo como uma casa, e em cada pacote como um cômodo dela.
As coisas de um cômodo com nome em minúscula ficam naquele cômodo. Ninguém de outro cômodo pode mexer nelas. Nem consegue vê-las.
As coisas com letra maiúscula ficam junto à porta do cômodo, viradas para o corredor. Qualquer um que chegue à porta pode usá-las. É isso que exportado quer dizer.
Os cômodos internal são os cômodos da família. As portas deles também dão para o corredor, mas uma placa na porta da frente diz “só a família”. Quem mora na casa pode entrar. Uma visita de outra casa é barrada na porta da frente.
A versão precisa
Um nome declarado no nível mais alto de um pacote é exportado quando o primeiro caractere é uma letra maiúscula. Só nomes exportados podem ser referenciados de outro pacote. Quem verifica isso é o compilador.
Um import path que contém um elemento internal só pode ser importado por pacotes cujo path começa com a parte antes de internal. example.com/wordcount/internal/tokenize pode ser importado por example.com/wordcount e por qualquer coisa abaixo dele. Quem verifica isso é o comando go, quando resolve os imports.
As duas regras se somam. Um nome dentro de um pacote internal precisa de letra maiúscula e de um importador permitido.
Onde a analogia falha: os cômodos de uma casa de verdade ficam lado a lado. A regra do Go segue a árvore de pastas. Uma pasta internal três níveis abaixo só deixa entrar pacotes da pasta acima dela, então a “família” pode ser uma parte pequena da casa, não a casa inteira.
Um comando que usa a biblioteca
Uma pasta cujos arquivos declaram package main gera um programa, e dentro de um módulo ela importa a biblioteca pelo import path, como qualquer outro pacote. Colocar os comandos em cmd/<name> é um layout comum, não uma regra:
// Command wc prints the most frequent words read from standard input.
package main
import (
"fmt"
"io"
"log"
"os"
"example.com/wordcount"
)
func main() {
if err := run(os.Stdin, os.Stdout, 3); err != nil {
log.Fatal(err)
}
}
// run reads all of r and writes the n most frequent words to w.
func run(r io.Reader, w io.Writer, n int) error {
text, err := io.ReadAll(r)
if err != nil {
return err
}
counts := wordcount.Count(string(text))
for _, p := range wordcount.Top(counts, n) {
fmt.Fprintf(w, "%-8s %d\n", p.Word, p.Count)
}
return nil
}
main quase não faz nada. O trabalho está em run, que recebe um io.Reader e um io.Writer em vez de usar os.Stdin e os.Stdout diretamente. Isso facilita o teste, como você vai ver mais adiante.
Passe para ele a frase da parte sobre maps:
$ printf 'the cat sat on the mat and the cat slept' | go run ./cmd/wc
the 3
cat 2
and 1
go run ./cmd/wc aponta para a pasta do pacote. Você não escreveu nada no go.mod para o import funcionar, porque example.com/wordcount está dentro do módulo em que você está.
Código de outras pessoas: go get, go mod tidy e go.sum
Esta série usa só a biblioteca padrão, então o módulo não tem dependências. Mesmo assim, você vai adicioná-las em projetos reais, e os passos são curtos.
Você adiciona um import do pacote no seu código e roda go mod tidy. Ele lê todos os imports do módulo, baixa os módulos que fornecem os que faltam e escreve uma linha require para cada um no go.mod. Ele também remove as linhas require que ninguém mais importa. go get example.com/some/module@v1.2.3 faz a adição diretamente, quando você quer uma versão específica.
Neste módulo, go mod tidy não imprime nada e não muda nada, porque não há nada para adicionar ou remover. Rode mesmo assim antes de fazer commit. É o jeito mais rápido de manter o go.mod fiel à realidade.
Na primeira vez que um módulo ganha uma dependência, aparece um segundo arquivo: go.sum. Ele guarda um hash criptográfico de cada versão de módulo que o build usa. Da próxima vez que alguém baixar essa versão, em qualquer máquina, o comando go confere o conteúdo com o hash e para se ele mudou. Faça commit do go.sum junto com o go.mod, e não edite o arquivo à mão.
Seu primeiro teste
Um teste Go fica em um arquivo cujo nome termina em _test.go, na mesma pasta do código que ele testa. Um teste é uma função cujo nome começa com Test e que recebe um *testing.T:
package tokenize
import (
"slices"
"testing"
)
func TestWords(t *testing.T) {
got := Words("Don't panic -- 'quoted' words, 42 times!")
want := []string{"Don't", "panic", "quoted", "words", "42", "times"}
if !slices.Equal(got, want) {
t.Errorf("Words() = %q, want %q", got, want)
}
}
Não existe biblioteca de assertions. Você mesmo compara os valores e chama t.Errorf quando estão errados. A mensagem segue um hábito do Go: diga o que você chamou, o que recebeu e o que queria.
O go build ignora arquivos _test.go, então os testes nunca vão parar no seu programa. O go test compila esses arquivos junto com o pacote e roda cada função Test. go test ./... faz isso para todos os pacotes do módulo:
$ go test ./...
ok example.com/wordcount 0.005s
ok example.com/wordcount/cmd/wc 0.004s
ok example.com/wordcount/internal/tokenize 0.004s
A duração no fim de cada linha muda a cada execução. Rode de novo sem mudar nada, e cada linha passa a dizer (cached). O go test lembra de um resultado aprovado até o código ou o teste mudar. Adicione -count=1 quando quiser que os testes rodem de novo mesmo assim.
Testes table-driven com subtests
Um teste table-driven coloca os casos em um slice de structs e roda a mesma verificação em cada um. É o formato de teste mais comum em código Go:
package wordcount
import (
"maps"
"testing"
)
func TestCount(t *testing.T) {
tests := []struct {
name string
text string
want map[string]int
}{
{"empty", "", map[string]int{}},
{"one word", "go", map[string]int{"go": 1}},
{"mixed case", "Go go GO", map[string]int{"go": 3}},
{"punctuation", "stop. Stop, stop!", map[string]int{"stop": 3}},
{"apostrophes", "don't 'quote' me", map[string]int{"don't": 1, "quote": 1, "me": 1}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Count(tt.text)
if !maps.Equal(got, tt.want) {
t.Errorf("Count(%q) = %v, want %v", tt.text, got, tt.want)
}
})
}
}
Adicionar um caso é uma linha na tabela. t.Run roda cada caso como um subtest com nome, com o seu próprio t. Um caso que falha informa o nome, e os outros casos continuam rodando.
Este arquivo declara package wordcount, o mesmo do código que ele testa. Isso coloca o teste dentro do cômodo, então ele também pode testar nomes não exportados. Este é o TestNormalize inteiro:
func TestNormalize(t *testing.T) {
if got := normalize("HeLLo"); got != "hello" {
t.Errorf("normalize(%q) = %q, want %q", "HeLLo", got, "hello")
}
}
-v lista cada teste e subtest enquanto eles rodam:
$ go test -v -run TestCount .
=== RUN TestCount
=== RUN TestCount/empty
=== RUN TestCount/one_word
=== RUN TestCount/mixed_case
=== RUN TestCount/punctuation
=== RUN TestCount/apostrophes
--- PASS: TestCount (0.00s)
--- PASS: TestCount/empty (0.00s)
--- PASS: TestCount/one_word (0.00s)
--- PASS: TestCount/mixed_case (0.00s)
--- PASS: TestCount/punctuation (0.00s)
--- PASS: TestCount/apostrophes (0.00s)
PASS
Depois vem uma linha final ok com a duração, como antes.
-run recebe uma expressão regular e roda só os testes cujos nomes casam com ela. Uma barra no padrão chega aos subtests. Os nomes dos casos tinham espaços, e o Go trocou os espaços por underscores. Você pode digitar qualquer uma das formas. -run 'TestCount/mixed case' e -run TestCount/mixed_case escolhem o mesmo subtest, porque o padrão passa pela mesma troca:
$ go test -v -run 'TestCount/mixed case' .
=== RUN TestCount
=== RUN TestCount/mixed_case
--- PASS: TestCount (0.00s)
--- PASS: TestCount/mixed_case (0.00s)
PASS
t.Errorf, t.Fatalf e t.Helper
t.Errorf marca o teste como falho e continua, enquanto t.Fatalf marca o teste como falho e o interrompe na hora. O helper que confere os resultados de Top usa os dois:
// assertPairs fails the test if got and want differ.
func assertPairs(t *testing.T, got, want []Pair) {
t.Helper()
if len(got) != len(want) {
t.Fatalf("got %d pairs, want %d: %v", len(got), len(want), got)
}
for i := range want {
if got[i] != want[i] {
t.Errorf("pair %d = %v, want %v", i, got[i], want[i])
}
}
}
func TestTop(t *testing.T) {
counts := map[string]int{"a": 1, "b": 3, "c": 3, "d": 2}
assertPairs(t, Top(counts, 2), []Pair{{"b", 3}, {"c", 3}})
assertPairs(t, Top(counts, 10), []Pair{{"b", 3}, {"c", 3}, {"d", 2}, {"a", 1}})
assertPairs(t, Top(counts, 0), []Pair{})
}
Se os tamanhos forem diferentes, o loop logo abaixo acessaria um índice além do fim de um slice e daria panic. Por isso essa verificação usa t.Fatalf. Se os tamanhos batem, vale a pena ver cada par errado, então o loop usa t.Errorf.
Para ver a diferença, mudei o esperado da segunda chamada para três pares, e o da terceira para um par. Só uma falha foi reportada:
$ go test -run TestTop .
--- FAIL: TestTop (0.00s)
wordcount_test.go:53: got 4 pairs, want 3: [{b 3} {c 3} {d 2} {a 1}]
FAIL
t.Fatalf interrompeu TestTop na linha 53, então a verificação quebrada da linha 54 nunca rodou. t.Fatalf interrompe o teste ou subtest atual, não a execução inteira. Os outros testes continuam rodando.
O número da linha é obra do t.Helper(). Ele marca assertPairs como helper, então as falhas apontam a linha que chamou o helper. Com os testes restaurados, troquei a ordem esperada na primeira chamada, na linha 52:
$ go test -run TestTop .
--- FAIL: TestTop (0.00s)
wordcount_test.go:52: pair 0 = {b 3}, want {c 3}
wordcount_test.go:52: pair 1 = {c 3}, want {b 3}
FAIL
Depois comentei o t.Helper() e rodei de novo:
$ go test -run TestTop .
--- FAIL: TestTop (0.00s)
wordcount_test.go:44: pair 0 = {b 3}, want {c 3}
wordcount_test.go:44: pair 1 = {c 3}, want {b 3}
FAIL
A linha 44 é o t.Errorf dentro do helper. Ela é a mesma para as três chamadas, então não diz qual chamada falhou. Coloque t.Helper() no início de todo helper de teste.
Testando o comando
Um package main também pode ter testes, e a função run facilita isso. O teste entrega a ela um reader de string e coleta o que ela escreve:
package main
import (
"strings"
"testing"
)
func TestRun(t *testing.T) {
in := strings.NewReader("one two two three three three")
var out strings.Builder
if err := run(in, &out, 2); err != nil {
t.Fatalf("run: %v", err)
}
want := "three 3\ntwo 2\n"
if got := out.String(); got != want {
t.Errorf("run wrote %q, want %q", got, want)
}
}
strings.Builder é um io.Writer, então pode ficar no lugar de os.Stdout. Manter o main minúsculo e passar readers e writers adiante é como programas Go continuam testáveis sem iniciar um processo de verdade.
Example functions são documentação que é verificada
Uma example function começa com Example, não recebe argumentos e termina com um comentário // Output:. O go test roda a função e compara o que ela imprimiu com esse comentário:
package wordcount_test
import (
"fmt"
"example.com/wordcount"
)
func ExampleCount() {
counts := wordcount.Count("The cat saw the other cat.")
fmt.Println(counts)
// Output: map[cat:2 other:1 saw:1 the:2]
}
func ExampleTop() {
counts := wordcount.Count("to be or not to be, that is the question")
for _, p := range wordcount.Top(counts, 3) {
fmt.Println(p.Word, p.Count)
}
// Output:
// be 2
// to 2
// is 1
}
Este arquivo declara package wordcount_test, com o sufixo _test. É o único caso em que uma pasta pode ter dois nomes de pacote. O pacote _test é compilado à parte e importa wordcount como qualquer código de fora faria. Então os examples usam só nomes exportados, exatamente como faria quem lê a sua documentação. Este também é o arquivo em que a chamada a normalize não compilou, mais acima.
fmt.Println em um map é seguro aqui, porque o fmt ordena as chaves do map antes de imprimi-las.
Para ver a verificação funcionando, mudei a saída esperada de ExampleCount para algo errado:
$ go test -run ExampleCount .
--- FAIL: ExampleCount (0.00s)
got:
map[cat:2 other:1 saw:1 the:2]
want:
map[cat:2 other:1 saw:1 The:1 the:1]
FAIL
Esse é o objetivo dos examples. O go doc e o pkg.go.dev mostram ExampleCount ao lado de Count como documentação de uso, e o go test falha no momento em que a documentação deixa de ser verdade. Um example sem comentário // Output: é compilado, mas não roda.
Benchmarks com b.Loop
Um benchmark é uma função cujo nome começa com Benchmark e que recebe um *testing.B. Ele fica no mesmo arquivo de teste:
var sample = "the quick brown fox jumps over the lazy dog and the dog sleeps"
func BenchmarkCount(b *testing.B) {
for b.Loop() {
Count(sample)
}
}
for b.Loop() roda o corpo quantas vezes o pacote testing precisar para obter uma medição estável. Ele chegou no Go 1.24. Código mais antigo usa for i := 0; i < b.N; i++, que ainda funciona. b.Loop é a melhor escolha hoje: ele deixa o código de preparação antes do loop fora da medição e impede que o compilador otimize e elimine uma chamada cujo resultado você ignora.
O go test não roda benchmarks a menos que você peça:
$ go test -bench=. -run='^$' .
-bench=. roda todos os benchmarks, e -run='^$' não casa com nenhum nome de teste, então os testes normais são pulados. A saída começa com linhas que dizem o seu sistema operacional, a arquitetura da CPU, o pacote e o modelo da CPU. Depois vem uma linha por benchmark: o nome com o número de CPUs como sufixo, como BenchmarkCount-12, quantas vezes o loop rodou e o tempo médio por execução em ns/op. Adicione -benchmem e a linha também mostra bytes e alocações por execução.
Não colei os números, porque eles dependem da máquina e mudam de uma execução para outra. Só compare resultados de benchmark da mesma máquina, rodados algumas vezes.
go vet roda dentro do go test
O go test roda um conjunto de verificações do go vet no pacote antes de rodar qualquer teste, e um problema apontado pelo vet faz o build falhar. Para ver isso, mudei TestNormalize para passar uma string a um verbo %d:
$ go test .
# example.com/wordcount
# [example.com/wordcount]
./wordcount_test.go:32:29: (*testing.common).Errorf format %d has arg got of wrong type string
FAIL example.com/wordcount [build failed]
Nenhum teste rodou. O mesmo erro em um programa normal teria impresso uma mensagem embaralhada em tempo de execução. Em um teste, ele barra você antes de qualquer coisa rodar. O go test roda só uma parte das verificações do vet, as que quase nunca erram, então rode go vet ./... você mesmo também.
go test -race ./... compila os testes com o race detector ligado. Este módulo não tem goroutines, então passa e tem pouco a encontrar. Ele se torna essencial na parte sobre goroutines.
O que lembrar
- Um pacote é uma pasta de arquivos
.go. Um módulo é uma árvore de pacotes com umgo.mod. Um import path é o module path mais o caminho da pasta. - Uma primeira letra maiúscula exporta um nome. De fora do pacote, um nome não exportado é
undefined. - Pacotes sob
internal/só podem ser importados de dentro da árvore que contém aquela pastainternal. go mod tidymantém ogo.modde acordo com os seus imports.go.sumregistra os hashes das suas dependências. Faça commit dos dois.- Testes ficam em arquivos
_test.gocomofunc TestX(t *testing.T). Use tabelas et.Run,t.Errorfpara continuar,t.Fatalfpara parar et.Helpernos helpers. go test ./...roda tudo,-runescolhe testes,-vlista os testes, e as verificações do vet rodam primeiro. Examples com// Output:são verificados, e benchmarks usamfor b.Loop().
Em Go, a pasta decide o pacote, a primeira letra decide quem enxerga um nome, e o
go testconfere as duas coisas.