Menu

Estructura de un proyecto Golang: cmd, internal y paquetes

Cómo organizar un proyecto Go: empieza plano, divide en paquetes cuando haya un motivo, usa cmd/ para varios binarios e internal/ para el código que nadie más debe importar, pon buenos nombres a los paquetes y deja los tests junto al código.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Empieza con un archivo

Un programa Go puede vivir entero en un único main.go, y las herramientas pequeñas deberían hacerlo. Este programa completo analiza la entrada, hace su trabajo e imprime un informe, en un solo paquete:

Cuando crezca, divídelo en más archivos en el mismo directorio y el mismo paquete (parse.go, report.go, main.go). Los archivos de un paquete comparten todos sus identificadores, así que no hace falta exportar ni importar nada. A menudo es toda la estructura que necesita un proyecto de unos pocos miles de líneas.

Recuerda que un package main de varios archivos se tiene que ejecutar como paquete: go run ., no go run main.go. Si no, los archivos se compilan solos y obtienes errores undefined para los nombres definidos en los otros archivos.

No hay una estructura obligatoria

Go no exige ninguna estructura de directorios más allá de "un paquete por directorio". La guía oficial es la página Organizing a Go module de go.dev, que describe unas pocas formas, no reglas.

El repositorio de GitHub golang-standards/project-layout se copia muchísimo y muchísimas veces se confunde con un estándar. Es una colección de convenciones de proyectos grandes hecha por la comunidad, y Russ Cox, entonces responsable técnico de Go, abrió allí un issue aclarando que no es un estándar de Go. La mayoría de sus directorios (pkg/, api/, build/, deployments/) solo tienen sentido en bases de código grandes. Copiar el esqueleto entero en un proyecto nuevo crea carpetas vacías y rutas de import profundas sin ningún beneficio.

Las reglas que la herramienta sí impone son pocas:

  • Un directorio es un paquete. Todos sus archivos .go (salvo los _test.go que usan el sufijo _test) comparten nombre de paquete.
  • La ruta de import es la ruta del módulo más la ruta del directorio.
  • Un directorio llamado internal restringe quién puede importar lo que contiene.
  • Los directorios llamados testdata, y los que empiezan por . o _, los ignora la herramienta go.

Las formas habituales

Un solo comando

todo/
├── go.mod        module github.com/you/todo
├── main.go
├── parse.go
├── report.go
└── parse_test.go

Plano, todo en package main. go install github.com/you/todo@latest funciona y el binario se llama todo.

Una librería

slug/
├── go.mod        module github.com/you/slug
├── slug.go       package slug
├── slug_test.go
├── example_test.go
└── internal/
    └── table/    helpers the public package uses, hidden from users

El paquete está en la raíz del módulo, así que los usuarios importan github.com/you/slug y llaman a slug.Make(...). Todo lo que no quieras mantener como API pública va bajo internal/.

Un servicio o varios binarios

shop/
├── go.mod                module github.com/you/shop
├── cmd/
│   ├── shop-api/
│   │   └── main.go       package main: flags, config, wiring
│   └── shop-worker/
│       └── main.go
├── internal/
│   ├── order/            package order: domain types and logic
│   │   ├── order.go
│   │   └── order_test.go
│   ├── store/            package store: database access
│   └── httpapi/          package httpapi: handlers, routing
├── migrations/
└── README.md

cmd/<name>/main.go contiene un directorio por binario, y el nombre del directorio pasa a ser el nombre del binario (go build ./cmd/shop-api produce shop-api). Cada main se mantiene fino: lee la configuración, construye las dependencias y arranca el servidor. El código real vive en paquetes bajo internal/, donde los dos binarios pueden usarlo y ningún otro módulo puede.

Es la estructura a la que acaban llegando la mayoría de servicios Go. Recurre a ella cuando tengas un segundo binario o un motivo real para dividir en paquetes, no el primer día.

internal/ lo impone el comando go

Un paquete bajo internal/ solo puede importarse desde código del árbol que cuelga del padre de internal. Desde otro módulo, la compilación falla:

package example.com/b
	main.go:6:2: use of internal package example.com/a/internal/secret not allowed

Eso convierte a internal/ en la herramienta para mantener pequeña la superficie de tu API. El código que hay ahí puede cambiar libremente, porque sabes que todos los que lo llaman están en tu propio repositorio. En una aplicación, poner casi todo en internal/ es razonable. En una librería, separa lo que prometes mantener estable de lo que no.

internal funciona a cualquier profundidad: shop/internal/order es visible para todo shop/, mientras que shop/internal/order/internal/pricing solo es visible dentro de shop/internal/order/.

Nombrar los paquetes

El nombre del paquete forma parte de cada llamada, así que importa más que el árbol de directorios.

  • Corto, en minúscula, una palabra: order, store, httpapi. Sin guiones bajos ni mixedCaps.
  • Nómbralo por lo que ofrece, no por lo que contiene. util, common, helpers, misc y models no dicen nada y acaban siendo cajones de sastre; la guía de estilo del equipo de Go los desaconseja de forma explícita. Pon una función auxiliar en el paquete que la usa, o en un paquete con el nombre de su propósito (slug, retry).
  • Evita la repetición. Quien llama escribe order.Order y order.New, así que no nombres las cosas order.OrderService ni order.NewOrder. La librería estándar lo hace de forma coherente: http.Server, no http.HTTPServer.
  • El nombre del directorio y el del paquete deberían coincidir, salvo package main en los directorios de comandos. Cuando difieren, quien lee tiene que abrir un archivo para saber qué identificador introduce el import.

Divide los paquetes por responsabilidad, no por tipo. Una división models/, controllers/, services/ (habitual en otros ecosistemas) obliga a que cada funcionalidad toque tres paquetes y tiende a crear ciclos de import, que Go prohíbe. Los paquetes organizados alrededor de un concepto del dominio (order, payment, user) guardan juntos sus tipos y su lógica.

Dónde van los tests

Los tests viven junto al código, en el mismo directorio, en archivos llamados *_test.go. No hay un árbol tests/.

  • package order en order_test.go: un test interno, que puede usar identificadores no exportados.
  • package order_test en el mismo directorio: un test externo, que solo ve la API exportada, igual que quien la llama. Útil para los ejemplos y para evitar ciclos de import en los tests.
  • Los archivos de datos de prueba van en un directorio testdata/ junto a los tests. La herramienta go lo ignora, y los tests se ejecutan con el directorio del paquete como directorio de trabajo, así que funcionan rutas relativas como testdata/big.json.

Otros archivos de la raíz

Archivo o directorioPara qué sirve
go.mod, go.sumdefinición del módulo y checksums de las dependencias, siempre en la raíz
README.md, LICENSEcomo en cualquier proyecto
Makefile o Taskfile.ymlatajos de compilación opcionales
Dockerfilenormalmente en la raíz en los servicios
migrations/, web/, docs/recursos que no son Go, con el nombre de lo que contienen
tools.gola forma antigua de fijar dependencias de herramientas; Go 1.24 la sustituye por líneas tool en go.mod (go get -tool)
go.workun workspace para desarrollar varios módulos juntos en local; normalmente no se sube en proyectos de un solo módulo

Errores comunes

  • Copiar una plantilla grande para un proyecto pequeño. Empieza plano y añade directorios cuando aparezca un segundo binario o un límite real.
  • Paquetes llamados utils o common. Nombra los paquetes por lo que hacen.
  • Un paquete por archivo o por tipo. Los paquetes de Go están pensados como unidades más grandes que las clases de Java. Un paquete con diez archivos es normal.
  • Ciclos de import. Dos paquetes no pueden importarse mutuamente. Suele significar que van juntos, que un tipo compartido debería bajar a un paquete de nivel inferior, o que uno de los lados debería depender de una pequeña interfaz en lugar del otro paquete.
  • pkg/ por reflejo. Añade un segmento a cada ruta de import sin añadir significado.
  • Tests en un directorio aparte. Allí no pueden ver el código no exportado y las herramientas no los esperan.

Preguntas frecuentes

¿Existe una estructura oficial de proyecto en Go?

No una obligatoria. El equipo de Go publica una guía en go.dev/doc/modules/layout, que describe unas pocas formas habituales (un solo paquete, un comando, varios comandos con internal/). El popular repositorio golang-standards/project-layout es un proyecto de la comunidad, no un estándar de Go, y el equipo de Go lo ha dicho públicamente.

¿Qué es el directorio internal en Go?

Un paquete cuya ruta de import contiene un elemento internal solo puede importarse desde código que cuelga del padre de ese directorio internal. example.com/app/internal/store se puede importar desde cualquier sitio bajo example.com/app/, y el comando go rechaza los imports desde cualquier otro módulo con use of internal package ... not allowed.

¿Debo usar un directorio pkg en Go?

Es opcional y añade un segmento a la ruta sin añadir significado. Algunos proyectos grandes usan pkg/ para separar el código de librería público del resto, pero ni la librería estándar de Go ni la mayoría de proyectos modernos lo hacen. Pon los paquetes importables en la raíz del módulo o en directorios con nombre, y todo lo privado en internal/.

¿Dónde van los archivos de test en un proyecto Go?

En el mismo directorio que el código que prueban, con nombre *_test.go. Go no tiene un árbol de tests aparte. Los archivos de datos de prueba van en un directorio testdata junto a los tests, que la herramienta go ignora al buscar paquetes.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR