Menu

Struttura di un progetto Golang: cmd, internal e organizzazione dei package

Come organizzare un progetto Go: parti piatto, dividi in package quando c'è un motivo, usa cmd/ per più binari e internal/ per il codice che nessun altro deve importare, dai buoni nomi ai package e tieni i test accanto al codice.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Parti da un solo file

Un programma Go può vivere interamente in un solo main.go, e i piccoli strumenti dovrebbero farlo. Questo programma completo analizza l'input, fa il suo lavoro e stampa un report, in un solo package:

Quando cresce, dividilo in più file nella stessa cartella e nello stesso package (parse.go, report.go, main.go). I file di un package condividono ogni identificatore, quindi non serve esportare né importare niente. Spesso è tutta la struttura di cui ha bisogno un progetto di qualche migliaio di righe.

Ricorda che un package main su più file va eseguito come package: go run ., non go run main.go. Altrimenti i file vengono compilati da soli e ottieni errori undefined per i nomi definiti negli altri file.

Non esiste una struttura obbligatoria

Go non richiede nessuna struttura di cartelle oltre a "un package per cartella". La guida ufficiale è la pagina Organizing a Go module su go.dev, e descrive alcune forme, non delle regole.

Il repository GitHub golang-standards/project-layout è molto copiato e molto spesso scambiato per uno standard. È una raccolta della comunità di convenzioni prese da grandi progetti, e Russ Cox, allora tech lead di Go, ha aperto lì una issue per dichiarare che non è uno standard di Go. La maggior parte delle sue cartelle (pkg/, api/, build/, deployments/) ha senso solo per codebase grandi. Copiare l'intero scheletro in un nuovo progetto crea cartelle vuote e percorsi di import profondi senza alcun beneficio.

Le regole imposte dal tool sono poche:

  • Una cartella è un package. Tutti i file .go al suo interno (tranne i file _test.go che usano il suffisso _test) condividono il nome del package.
  • Il percorso di import è il percorso del modulo più il percorso della cartella.
  • Una cartella chiamata internal limita chi può importare ciò che contiene.
  • Le cartelle chiamate testdata, e quelle che iniziano con . o _, vengono ignorate dal tool go.

Le forme comuni

Un singolo comando

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

Piatto, tutto in package main. go install github.com/you/todo@latest funziona e il binario si chiama todo.

Una libreria

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

Il package sta nella radice del modulo, quindi chi lo usa importa github.com/you/slug e chiama slug.Make(...). Tutto ciò che non vuoi supportare come API pubblica va sotto internal/.

Un servizio o più binari

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 una cartella per ogni binario, e il nome della cartella diventa il nome del binario (go build ./cmd/shop-api produce shop-api). Ogni main resta snello: legge la configurazione, costruisce le dipendenze, avvia il server. Il codice vero vive nei package sotto internal/, dove entrambi i binari possono usarlo e nessun altro modulo può farlo.

È la struttura verso cui convergono la maggior parte dei servizi Go. Usala quando hai un secondo binario o un vero motivo per dividere i package, non dal primo giorno.

internal/ è imposto dal comando go

Un package sotto internal/ può essere importato solo dal codice dell'albero che ha radice nel genitore di internal. Da un altro modulo, la build fallisce:

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

Questo fa di internal/ lo strumento per tenere piccola la superficie della tua API. Il codice lì dentro può cambiare liberamente, perché sai che ogni chiamante è nel tuo repository. Per un'applicazione, mettere quasi tutto in internal/ è ragionevole. Per una libreria, separa ciò che prometti di mantenere stabile da ciò che non prometti.

internal funziona a qualsiasi profondità: shop/internal/order è visibile a tutto shop/, mentre shop/internal/order/internal/pricing è visibile solo dentro shop/internal/order/.

Dare un nome ai package

Il nome del package fa parte di ogni punto di chiamata, quindi conta più dell'albero delle cartelle.

  • Breve, in minuscolo, una parola: order, store, httpapi. Niente underscore e niente mixedCaps.
  • Chiamalo per ciò che fornisce, non per ciò che contiene. util, common, helpers, misc e models non dicono nulla e diventano discariche; le linee guida di stile del team di Go li sconsigliano esplicitamente. Metti una funzione di supporto nel package che la usa, o in un package che prende il nome dal suo scopo (slug, retry).
  • Evita le ripetizioni. Chi chiama scrive order.Order e order.New, quindi non chiamare le cose order.OrderService o order.NewOrder. La libreria standard lo fa in modo coerente: http.Server, non http.HTTPServer.
  • Il nome della cartella e il nome del package dovrebbero coincidere, a parte package main nelle cartelle dei comandi. Quando differiscono, chi legge deve aprire un file per scoprire quale identificatore introduce l'import.

Dividi i package per responsabilità, non per tipologia. Una divisione in models/, controllers/, services/ (comune in altri ecosistemi) costringe ogni funzionalità a toccare tre package e tende a creare cicli di import, che Go vieta. I package organizzati attorno a un concetto di dominio (order, payment, user) tengono insieme ciascuno i propri tipi e la propria logica.

Dove vanno i test

I test vivono accanto al codice nella stessa cartella, in file chiamati *_test.go. Non c'è un albero tests/.

  • package order in order_test.go: un test interno, che può usare identificatori non esportati.
  • package order_test nella stessa cartella: un test esterno, che vede solo l'API esportata, come la vedrebbe un chiamante. Utile per gli esempi e per evitare cicli di import nei test.
  • I file di fixture vanno in una cartella testdata/ accanto ai test. Il tool go la ignora, e i test girano con la cartella del package come cartella di lavoro, quindi percorsi relativi come testdata/big.json funzionano.

Altri file nella radice

File o cartellaScopo
go.mod, go.sumdefinizione del modulo e checksum delle dipendenze, sempre nella radice
README.md, LICENSEcome in qualsiasi progetto
Makefile o Taskfile.ymlscorciatoie di build facoltative
Dockerfiledi solito nella radice per i servizi
migrations/, web/, docs/risorse non Go, con il nome di ciò che contengono
tools.govecchio modo per fissare le dipendenze degli strumenti; Go 1.24 lo sostituisce con le righe tool in go.mod (go get -tool)
go.workun workspace per sviluppare più moduli insieme in locale; di solito non si fa commit nei progetti con un solo modulo

Errori comuni

  • Copiare un template grande per un progetto piccolo. Parti piatto e aggiungi cartelle quando compare un secondo binario o un confine reale.
  • Package chiamati utils o common. Dai ai package il nome di ciò che fanno.
  • Un package per file o per tipo. I package Go sono pensati come unità più grandi delle classi Java. Un package con dieci file è normale.
  • Cicli di import. Due package non possono importarsi a vicenda. Di solito significa che vanno insieme, che un tipo condiviso dovrebbe spostarsi in un package di livello più basso, o che una delle due parti dovrebbe dipendere da una piccola interface invece che dall'altro package.
  • pkg/ per riflesso. Aggiunge un segmento a ogni percorso di import senza aggiungere significato.
  • Test in una cartella separata. Lì non possono vedere il codice non esportato e gli strumenti non se li aspettano.

Domande frequenti

Esiste una struttura ufficiale per i progetti Go?

Non una obbligatoria. Il team di Go pubblica delle indicazioni su go.dev/doc/modules/layout, che descrivono alcune forme comuni (un singolo package, un comando, più comandi con internal/). Il popolare repository golang-standards/project-layout è un progetto della comunità, non uno standard di Go, e il team di Go lo ha detto pubblicamente.

Cos'è la cartella internal in Go?

Un package il cui percorso di import contiene un elemento internal può essere importato solo dal codice che ha radice nel genitore di quella cartella internal. example.com/app/internal/store si può importare ovunque sotto example.com/app/, e il comando go rifiuta gli import da qualsiasi altro modulo con use of internal package ... not allowed.

Dovrei usare una cartella pkg in Go?

È facoltativa e aggiunge un segmento al percorso senza aggiungere significato. Alcuni grandi progetti usano pkg/ per separare il codice di libreria pubblico dal resto, ma la libreria standard di Go e la maggior parte dei progetti moderni non lo fanno. Metti i package importabili nella radice del modulo o in cartelle con un nome, e tutto ciò che è privato in internal/.

Dove vanno i file di test in un progetto Go?

Nella stessa cartella del codice che testano, con nomi *_test.go. Go non ha un albero separato per i test. I file di fixture vanno in una cartella testdata accanto ai test, che il tool go ignora quando cerca i package.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA