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.goque 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
internalrestringe 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,miscymodelsno 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.Orderyorder.New, así que no nombres las cosasorder.OrderServiceniorder.NewOrder. La librería estándar lo hace de forma coherente:http.Server, nohttp.HTTPServer. - El nombre del directorio y el del paquete deberían coincidir, salvo
package mainen 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 orderenorder_test.go: un test interno, que puede usar identificadores no exportados.package order_testen 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 comotestdata/big.json.
Otros archivos de la raíz
| Archivo o directorio | Para qué sirve |
|---|---|
go.mod, go.sum | definición del módulo y checksums de las dependencias, siempre en la raíz |
README.md, LICENSE | como en cualquier proyecto |
Makefile o Taskfile.yml | atajos de compilación opcionales |
Dockerfile | normalmente en la raíz en los servicios |
migrations/, web/, docs/ | recursos que no son Go, con el nombre de lo que contienen |
tools.go | la forma antigua de fijar dependencias de herramientas; Go 1.24 la sustituye por líneas tool en go.mod (go get -tool) |
go.work | un 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
utilsocommon. 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.