Por que a estrutura do repositório importa
Em Go, a organização do projeto influencia diretamente legibilidade, acoplamento entre pacotes, facilidade de manutenção e clareza do que é “aplicação” (executáveis) versus “biblioteca” (código reutilizável). Uma estrutura alinhada às convenções ajuda novos contribuidores a entenderem rapidamente onde colocar cada coisa e reduz a chance de ciclos de importação.
Convenções comuns: cmd/, internal/ e pkg/
cmd/: pontos de entrada (executáveis)
Use cmd/ para cada binário que seu repositório gera. Cada subpasta representa um executável e costuma conter um main.go pequeno, que apenas “monta” dependências e chama o código de aplicação.
cmd/appname/main.go(um binário)cmd/worker/main.go(outro binário, se existir)
internal/: código não importável de fora do módulo
internal/ é uma convenção suportada pela toolchain: qualquer pacote dentro de internal/ só pode ser importado por código que esteja dentro do diretório pai (ou subdiretórios) que contém internal. Isso cria uma “fronteira” clara: o que está em internal/ é detalhe de implementação do seu módulo, não uma API pública.
Exemplo: se seu módulo é example.com/acme/greeter, então example.com/acme/greeter/internal/cli não pode ser importado por outro módulo externo.
pkg/: API reutilizável (quando fizer sentido)
pkg/ é opcional. Use quando você quer sinalizar explicitamente que certos pacotes são pensados para consumo externo (ou por múltiplos módulos internos) e têm compromisso maior de estabilidade. Se o repositório é apenas uma aplicação, muitas vezes você pode não precisar de pkg/ e manter tudo em internal/.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
Organização por responsabilidades (camadas e limites)
Uma regra prática: pacotes devem ter uma responsabilidade clara e um conjunto pequeno de motivos para mudar. Em vez de separar por “tipo” (ex.: handlers, models, utils genéricos), prefira separar por domínio/funcionalidade e por fronteiras técnicas bem definidas.
- Aplicação/Use cases: orquestra regras e fluxos (ex.:
internal/app). - Interfaces de entrada: CLI, HTTP, gRPC (ex.:
internal/cli). - Infra/adapters: acesso a disco, rede, banco, APIs externas (ex.:
internal/store,internal/httpclient). - Domínio: tipos e regras centrais (ex.:
internal/domain), quando houver.
Nem todo projeto precisa de todas essas pastas; o importante é manter limites nítidos e dependências apontando para dentro (camadas externas dependem das internas, não o contrário).
Nomes de pacotes e import paths
Regras práticas para nomes de pacotes
- Curto, em minúsculas, sem underscore:
cli,app,store,greeter. - Nome descreve o que o pacote é, não o que ele faz:
parserem vez deparse. - Evite nomes genéricos como
utils,common,helpers. Se algo é “utilitário”, provavelmente pertence a um domínio específico (ex.:textfmt,clock,slug). - Evite gagueira no uso: se o pacote é
config, prefiraconfig.Load()em vez deconfig.ConfigLoad().
Import path: como ele se forma
O import path é baseado no módulo (definido no go.mod) + caminho do diretório do pacote. Exemplo: módulo example.com/acme/greeter.
internal/appviraexample.com/acme/greeter/internal/apppkg/greeterviraexample.com/acme/greeter/pkg/greeter
Em Go, o nome do pacote (cláusula package) não precisa ser igual ao nome do diretório, mas manter igual evita confusão.
Como evitar ciclos de importação
Ciclos acontecem quando A importa B e B importa A (direta ou indiretamente). Go não permite isso, e geralmente é um sinal de que as responsabilidades estão misturadas.
Padrões que ajudam
- Dependa de interfaces, não de implementações: defina interfaces no pacote que as consome (ou em um pacote de domínio), e implemente em pacotes de infraestrutura.
- Direção única de dependências: por exemplo,
clidepende deapp;appdepende dedomain;storeimplementa interfaces usadas porapp. - Extraia tipos compartilhados: se dois pacotes precisam do mesmo tipo, ele provavelmente pertence a um pacote mais central (ex.:
internal/domain). - Evite “pacote deus”: um pacote que todo mundo importa e que importa todo mundo é um gerador de ciclos.
Exemplo de ciclo e como quebrar
Problema: internal/cli importa internal/app para executar comandos, mas internal/app importa internal/cli para acessar flags/args. Isso cria ciclo.
Solução: cli deve traduzir argumentos em uma estrutura simples (DTO) e chamar app. O pacote app não deve conhecer CLI; ele recebe dados já interpretados.
Exemplo prático: projeto com CLI simples e pacotes bem delimitados
Objetivo: criar um binário greeter com um comando simples: imprimir uma saudação. Vamos separar: cmd/ (entrada), internal/cli (parsing e roteamento de comandos), internal/app (casos de uso), internal/greeter (regras de saudação) e internal/platform/clock (dependência de tempo para facilitar testes e evitar acoplamento).
Estrutura de diretórios
greeter/ (raiz do módulo) cmd/ greeter/ main.go internal/ app/ greet.go cli/ run.go greeter/ greeter.go platform/ clock/ clock.goDecisões de design (o “porquê”)
cmd/greeter/main.gofica mínimo: apenas cria dependências e chamacli.Run.internal/clinão contém regra de negócio: só interpreta argumentos e chama a camada de aplicação.internal/apporquestra: recebe entradas já validadas/interpretadas e usa serviços (ex.:greeter.Service).internal/greeterconcentra a lógica: gera mensagem; não sabe nada sobre terminal/flags.internal/platform/clockisola dependência externa: o tempo atual vira uma interface; isso evita acoplamento e facilita testes.
Passo a passo: implementando os pacotes
1) Ponto de entrada: cmd/greeter/main.go
package main import ( "os" "example.com/acme/greeter/internal/cli" "example.com/acme/greeter/internal/platform/clock" ) func main() { c := clock.Real{} code := cli.Run(os.Args[1:], c) os.Exit(code) }Repare que main não faz parsing detalhado nem monta mensagens. Ele injeta dependências e delega.
2) Dependência de tempo: internal/platform/clock/clock.go
package clock import "time" type Clock interface { Now() time.Time } type Real struct{} func (Real) Now() time.Time { return time.Now() }O resto do código depende de clock.Clock, não de time.Now diretamente.
3) Regra de negócio: internal/greeter/greeter.go
package greeter import ( "fmt" "time" ) type Service struct{} func (Service) Message(name string, t time.Time) string { if name == "" { name = "mundo" } hour := t.Hour() part := "Olá" switch { case hour < 12: part = "Bom dia" case hour < 18: part = "Boa tarde" default: part = "Boa noite" } return fmt.Sprintf("%s, %s!", part, name) }Esse pacote não sabe nada sobre CLI, argumentos ou saída. Ele só recebe dados e retorna uma string.
4) Caso de uso: internal/app/greet.go
package app import ( "io" "example.com/acme/greeter/internal/greeter" "example.com/acme/greeter/internal/platform/clock" ) type Greet struct { Clock clock.Clock Greeter greeter.Service Out io.Writer } type GreetInput struct { Name string } func (g Greet) Run(in GreetInput) error { msg := g.Greeter.Message(in.Name, g.Clock.Now()) _, err := io.WriteString(g.Out, msg+"\n") return err }A camada app orquestra: pega entrada, chama serviço e escreve no destino. Note que ela recebe io.Writer, o que permite escrever no terminal, em buffer, arquivo etc., sem acoplamento.
5) CLI: internal/cli/run.go
package cli import ( "flag" "fmt" "io" "os" "example.com/acme/greeter/internal/app" "example.com/acme/greeter/internal/greeter" "example.com/acme/greeter/internal/platform/clock" ) func Run(args []string, c clock.Clock) int { fs := flag.NewFlagSet("greeter", flag.ContinueOnError) fs.SetOutput(io.Discard) name := fs.String("name", "", "nome para a saudação") if err := fs.Parse(args); err != nil { fmt.Fprintln(os.Stderr, "uso: greeter -name=SEU_NOME") return 2 } useCase := app.Greet{ Clock: c, Greeter: greeter.Service{}, Out: os.Stdout, } if err := useCase.Run(app.GreetInput{Name: *name}); err != nil { fmt.Fprintln(os.Stderr, "erro:", err) return 1 } return 0 }Aqui a CLI faz o trabalho “de borda”: parse de flags, mensagens de uso e códigos de saída. Ela monta o caso de uso e chama Run.
Checagem de limites e importações (o que importa o quê)
| Pacote | Importa | Não deve importar |
|---|---|---|
cmd/greeter | internal/cli, internal/platform/clock | regras de negócio diretamente (opcional, mas evite) |
internal/cli | internal/app, flag, os | cmd/... |
internal/app | internal/greeter, internal/platform/clock, io | internal/cli |
internal/greeter | time, fmt | os, flag |
Essa direção de dependências evita ciclos e mantém cada pacote com uma responsabilidade clara.
Quando usar pkg/ neste exemplo
Se você quiser que outros projetos usem a lógica de saudação como biblioteca, você poderia mover internal/greeter para pkg/greeter e tratar sua API como pública. Nesse caso, pense em estabilidade: nomes, assinaturas e comportamento passam a ser um “contrato”. Se a lógica é apenas detalhe da sua aplicação, mantenha em internal/.
Padrões de legibilidade e manutenção
- main pequeno: quanto menos lógica em
cmd/, melhor. - Construa dependências na borda: CLI/HTTP monta implementações concretas; camadas internas recebem interfaces/abstrações simples.
- Evite pacotes “catch-all”: prefira pacotes com nomes que descrevam o domínio.
- Arquivos por assunto: em pacotes pequenos, um arquivo pode bastar; em pacotes maiores, separe por caso de uso (
greet.go,version.go), não por “tipo” genérico. - Import paths estáveis: mover pacotes muda imports; defina cedo onde fica o que é público (
pkg) e o que é interno (internal).