Een goede project structuur in Go maakt het verschil tussen een codebase die jaren meegaat en eentje die na zes maanden al onoverzichtelijk is. Go geeft je veel vrijheid in hoe je je code organiseert, maar juist die vrijheid zorgt bij beginners vaak voor verwarring. Waar zet je je main-bestand? Wat doet die internal map precies? En wanneer gebruik je pkg?
In dit artikel leer je hoe je een Go project opzet dat schaalbaar, onderhoudbaar en herkenbaar is voor andere Go developers. We behandelen de standaard conventies, het verschil tussen de belangrijkste mappen en praktische voorbeelden die je direct kunt toepassen.
Waarom project structuur in Go belangrijk is
Anders dan bij talen als Java of C# legt Go geen structuur op. Je mag in principe alle code in één bestand zetten of kiezen voor een diepe mappenboom. Toch heeft de Go community in de loop der jaren conventies ontwikkeld die breed geaccepteerd zijn.
Deze conventies helpen je op drie manieren. Ten eerste weten nieuwe teamleden direct waar ze moeten zoeken. Ten tweede dwingt de structuur je om na te denken over dependencies en zichtbaarheid. Ten derde sluit je project aan op tooling zoals go build, go test en IDE's die deze layout herkennen.
Als je nog maar net begint met Go, lees dan eerst onze handleiding over installatie en je eerste Go programma. Voor een breder perspectief op wanneer Go de juiste keuze is, hebben we een artikel geschreven over waarom Go populair is voor backends.
Modules: de basis van elk Go project
Sinds Go 1.11 draait alles om modules. Een module is een verzameling packages die samen versioned en gedistribueerd worden. Elke module heeft een go.mod bestand in de root.
Je maakt een nieuwe module met:
go mod init github.com/jouwnaam/projectnaam
Dit genereert een go.mod bestand:
module github.com/jouwnaam/projectnaam
go 1.22
De module path (hier github.com/jouwnaam/projectnaam) is cruciaal. Alle imports binnen je project gebruiken dit als prefix. Zelfs als je het project nooit op GitHub publiceert, is een domein-achtige naam de standaard. Dat voorkomt naamconflicten met andere modules.
Naast go.mod wordt er bij de eerste dependency een go.sum bestand aangemaakt. Dit bevat checksums voor reproduceerbare builds en hoort gewoon in je versiebeheer.
De standaard mappenstructuur
Een typisch Go project voor een backend service ziet er zo uit:
projectnaam/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── handler/
│ ├── service/
│ └── repository/
├── pkg/
│ └── validator/
├── api/
├── configs/
├── scripts/
├── go.mod
├── go.sum
└── README.md
Niet elk project heeft al deze mappen nodig. Voor een simpel CLI-tool kun je prima volstaan met alleen main.go in de root. Schaalt je project, dan groeit de structuur mee.
De cmd directory
De cmd map bevat de main packages voor je executables. Elk subdirectory in cmd vertegenwoordigt één binary.
cmd/
├── server/
│ └── main.go // de HTTP server
├── worker/
│ └── main.go // background worker
└── migrate/
└── main.go // database migrations
Je bouwt dan één specifieke binary met:
go build -o bin/server ./cmd/server
Belangrijk: houd de code in cmd/*/main.go zo klein mogelijk. De main functie regelt alleen het opstarten, het parsen van flags en het aanroepen van de daadwerkelijke logica die elders staat.
De internal directory
De internal map is een bijzondere map in Go. De compiler blokkeert automatisch imports vanuit andere modules. Alleen code binnen jouw eigen module kan packages uit internal importeren.
Dit is enorm waardevol. Stel je hebt een internal/auth package met sessie-logica. Een externe gebruiker van je library kan die nooit importeren, ook niet per ongeluk. Je blijft dus vrij om de API te veranderen zonder dat dat breaking changes oplevert voor anderen.
Voor de meeste applicaties hoort het grootste deel van je code in internal thuis. Alleen wat je bewust wilt delen gaat erbuiten.
De pkg directory
De pkg map is bedoeld voor code die veilig door externe projecten geïmporteerd mag worden. Hier zet je bijvoorbeeld utility packages, clients voor je eigen API, of herbruikbare componenten.
Let op: pkg is controversieel. Veel Go developers, waaronder enkele core contributors, vinden het onnodige ruis. Als je geen library publiceert en alle code alleen intern gebruikt, kun je pkg gerust overslaan en alles in internal plaatsen of direct in de root van je module.
Packages: één verantwoordelijkheid per package
Een package in Go is een map met één of meer .go bestanden die hetzelfde package statement bovenaan hebben. De mapnaam is tegelijk de package naam in imports.
Goede packages hebben drie eigenschappen:
- Klein en cohesief: één duidelijke verantwoordelijkheid
- Korte naam: bij voorkeur één woord, lowercase, zonder underscores
- Kleine publieke API: exporteer alleen wat nodig is (hoofdletter = public)
Vermijd generieke namen zoals utils, helpers of common. Die worden onvermijdelijk dumpplekken voor losse functies zonder samenhang.
Een voorbeeld van een goede package indeling voor een webshop:
internal/
├── cart/ // winkelwagenlogica
├── checkout/ // afrekenproces
├── inventory/ // voorraadbeheer
├── payment/ // betaalintegraties
└── user/ // gebruikersaccounts
Elk van deze packages heeft een duidelijk domein. In onze gids over structs en interfaces in Go lees je hoe je binnen zo'n package je types effectief opzet.
Een praktisch voorbeeld: web service
Laten we een concrete structuur bekijken voor een typische REST API:
shop-api/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── config/
│ │ └── config.go
│ ├── handler/
│ │ ├── product.go
│ │ └── order.go
│ ├── service/
│ │ ├── product.go
│ │ └── order.go
│ ├── repository/
│ │ ├── product.go
│ │ └── order.go
│ └── model/
│ ├── product.go
│ └── order.go
├── migrations/
│ └── 001_init.sql
├── go.mod
└── go.sum
De flow is duidelijk gelaagd: handler ontvangt HTTP requests, roept een service aan voor business logica, die op zijn beurt een repository gebruikt voor database toegang. Als je nog niet bekend bent met HTTP in Go, lees dan onze uitleg over HTTP servers bouwen in Go. Voor het repository-patroon met databases helpt ons artikel over database toegang in Go.
De main functie compact houden
Een goede main.go leest als een samenvatting van je applicatie:
package main
import (
"log"
"github.com/jouwnaam/shop-api/internal/config"
"github.com/jouwnaam/shop-api/internal/handler"
"github.com/jouwnaam/shop-api/internal/repository"
"github.com/jouwnaam/shop-api/internal/service"
)
func main() {
cfg, err := config.Load()
if err != nil {
log.Fatalf("config laden mislukt: %v", err)
}
db, err := repository.NewDatabase(cfg.DatabaseURL)
if err != nil {
log.Fatalf("database connectie mislukt: %v", err)
}
defer db.Close()
productRepo := repository.NewProductRepository(db)
productSvc := service.NewProductService(productRepo)
productHandler := handler.NewProductHandler(productSvc)
srv := handler.NewServer(productHandler)
log.Fatal(srv.ListenAndServe())
}
Deze stijl heet dependency injection via constructors. Je wiring zit op één plek en je onderliggende packages hoeven niets te weten van elkaars concrete implementaties.
Naamconventies en stijl
Go heeft strikte conventies die door tooling worden afgedwongen:
- Bestandsnamen: lowercase, underscores alleen voor test- of platformbestanden (
user_test.go,file_linux.go) - Package namen: lowercase, kort, enkelvoud (
user, nietusers) - Exported vs unexported: begint een naam met een hoofdletter, dan is het publiek
- Geen circulaire imports: package A kan niet importeren uit B als B ook A importeert
Gebruik altijd gofmt of goimports om je code te formatteren. Dat gebeurt automatisch in vrijwel elke Go IDE. Voor een uitgebreide officiële richtlijn zie de Effective Go documentatie en de Go Modules Reference.
Testbestanden en testorganisatie
Tests horen in hetzelfde package als de code die ze testen, in bestanden met suffix _test.go:
internal/cart/
├── cart.go
├── cart_test.go
├── discount.go
└── discount_test.go
Voor integration tests of tests die alleen de publieke API raken, kun je een apart cart_test package gebruiken (in hetzelfde bestand, ander package statement). Zo dwing je jezelf om via de publieke API te testen.
Configuratie en environment
Houd configuratie gescheiden van code. Een veelgebruikt patroon:
package config
type Config struct {
DatabaseURL string
Port string
LogLevel string
}
func Load() (*Config, error) {
return &Config{
DatabaseURL: os.Getenv("DATABASE_URL"),
Port: getEnv("PORT", "8080"),
LogLevel: getEnv("LOG_LEVEL", "info"),
}, nil
}
Voor grotere projecten kun je libraries zoals Viper overwegen, maar voor veel services is os.Getenv met sensible defaults meer dan voldoende.
Veelgemaakte fouten
Let op deze valkuilen bij het opzetten van je project:
- Te vroeg opsplitsen: begin simpel, refactor wanneer de pijn reëel is
- Generieke package-namen:
utilsenhelpersworden altijd een rommelbak - Business logica in main:
main.gohoort klein te blijven, orchestratie only - Alles in één package: een grote
apppackage verslaat het doel van package-isolatie - Circulaire dependencies: vaak een teken dat je abstractie verkeerd zit
Als je goroutines of channels gebruikt in je services, raden we onze artikelen over goroutines en concurrency basics en channels diep uitgelegd aan voor veilige concurrent code.
Voorbeeldprojecten ter inspiratie
Wil je voorbeelden zien van echte Go projecten? De golang-standards/project-layout repository is een veelgebruikte referentie, al benadrukken de maintainers zelf dat het geen officiële standaard is. Kijk ook naar open source projecten zoals Kubernetes, Prometheus of Hugo om te zien hoe grote codebases georganiseerd zijn.
Veelgestelde vragen
Wat is de standaard project structuur in Go?
Go heeft geen officiële verplichte structuur, maar er is een community-conventie met mappen zoals cmd, internal en pkg. Deze layout wordt breed gebruikt en maakt projecten voorspelbaar voor andere Go developers.
Waarvoor dient de internal map in Go?
De internal map bevat code die alleen door jouw eigen project gebruikt mag worden. De Go compiler blokkeert imports vanuit andere modules, waardoor je interne implementatiedetails afschermt en je publieke API klein houdt.
Moet ik altijd een pkg map gebruiken?
Nee, de pkg map is optioneel en zelfs omstreden. Voor kleine projecten voegt het vaak alleen ruis toe. Gebruik pkg alleen als je bewust herbruikbare packages publiceert die ook buiten je project bruikbaar zijn.
Hoe werkt een Go module in relatie tot de projectstructuur?
Een module wordt gedefinieerd door go.mod in de root van je project en bepaalt het import-pad. Alle packages binnen die module gebruiken dit pad als prefix, ongeacht hoe diep ze in de mappenstructuur zitten.
Wanneer splits ik code op in meerdere packages?
Splits zodra een package meerdere duidelijk verschillende verantwoordelijkheden krijgt of te groot wordt om snel te overzien. Packages horen één cohesief doel te hebben en een duidelijke, kleine publieke API aan te bieden.
Conclusie
Een doordachte project structuur in Go is geen kwestie van regels volgen, maar van intentie tonen. Begin klein met een enkele main.go, introduceer internal zodra je packages wilt afschermen, en voeg cmd toe als je meerdere binaries nodig hebt.
De belangrijkste regel: laat je structuur meegroeien met de complexiteit van je project. Forceer geen layout die niet past bij de omvang van je code, maar wees ook niet bang om te refactoren zodra de huidige structuur je in de weg zit.