Estrutura de projetos em Go: convenções, internal, cmd e organização por pacotes

Capítulo 16

Tempo estimado de leitura: 8 minutos

+ Exercício

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/.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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: parser em vez de parse.
  • 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, prefira config.Load() em vez de config.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/app vira example.com/acme/greeter/internal/app
  • pkg/greeter vira example.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, cli depende de app; app depende de domain; store implementa interfaces usadas por app.
  • 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.go

Decisões de design (o “porquê”)

  • cmd/greeter/main.go fica mínimo: apenas cria dependências e chama cli.Run.
  • internal/cli não contém regra de negócio: só interpreta argumentos e chama a camada de aplicação.
  • internal/app orquestra: recebe entradas já validadas/interpretadas e usa serviços (ex.: greeter.Service).
  • internal/greeter concentra a lógica: gera mensagem; não sabe nada sobre terminal/flags.
  • internal/platform/clock isola 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ê)

PacoteImportaNão deve importar
cmd/greeterinternal/cli, internal/platform/clockregras de negócio diretamente (opcional, mas evite)
internal/cliinternal/app, flag, oscmd/...
internal/appinternal/greeter, internal/platform/clock, iointernal/cli
internal/greetertime, fmtos, 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).

Agora responda o exercício sobre o conteúdo:

Em um projeto Go que segue as convenções de cmd/ e internal/, qual organização melhor evita ciclos de importação e mantém limites claros entre entrada (CLI) e regras de negócio?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

A direção única de dependências evita ciclos: a borda (cmd/ e internal/cli) interpreta entradas e monta dependências, enquanto a camada internal/app orquestra casos de uso sem importar a CLI, delegando a regras internas.

Próximo capitúlo

Boas práticas de código em Go: formatação, comentários, documentação e lint básico

Arrow Right Icon
Capa do Ebook gratuito Go (Golang) para Iniciantes: Fundamentos, Concorrência e Estrutura de Projetos
89%

Go (Golang) para Iniciantes: Fundamentos, Concorrência e Estrutura de Projetos

Novo curso

18 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.