Go-projectstructuur: Praktijken & Patronen

Structureer uw Go-projecten voor schaalbaarheid en duidelijkheid

Inhoud

Het effectief structureren van een Go-project is essentieel voor de lange termijn onderhoudbaarheid, teamcollaboratie en schaalbaarheid. In tegenstelling tot frameworks die een starre directorystructuur afdwingen, omarmt Go flexibiliteit—maar met die vrijheid komt de verantwoordelijkheid om patronen te kiezen die passen bij de specifieke behoeften van uw project.

project tree

Het begrip van Go’s filosofie rond projectstructuur

De minimalistische designfilosofie van Go strekt zich ook uit tot projectorganisatie. De taal schrijft geen specifieke structuur voor, maar vertrouwt erop dat ontwikkelaars weloverwogen beslissingen nemen. Deze aanpak heeft de gemeenschap ertoe gebracht verschillende bewezen patronen te ontwikkelen, van eenvoudige platte layouts voor kleine projecten tot geavanceerde architecturen voor enterprise-systemen.

Het leidende principe is eerst simpliciteit, complexiteit pas wanneer nodig. Veel ontwikkelaars vallen in de val van overengineering van hun initiële structuur, waardoor diep geneste directories en voortijdige abstracties ontstaan. Begin met wat u vandaag nodig heeft en refactor uw project naarmate het groeit.

Wanneer moet ik de internal/-directory gebruiken versus pkg/?

De internal/-directory dient een specifiek doel in Go’s ontwerp: het bevat pakketten die niet kunnen worden geïmporteerd door externe projecten. Go’s compiler afdwingt deze restrictie, waardoor internal/ perfect is voor private applicatielogica, bedrijfsregels en hulpprogramma’s die niet bedoeld zijn voor hergebruik buiten uw project.

De pkg/-directory daarentegen signalert dat code bedoeld is voor extern gebruik. Plaats code hier alleen als u een bibliotheek of herbruikbare componenten bouwt die u wilt dat anderen importeren. Veel projecten hebben pkg/ helemaal niet nodig—als uw code niet extern wordt gebruikt, is het cleaner om het op de root of in internal/ te houden. Bij het bouwen van herbruikbare bibliotheken, overweeg dan het gebruik van Go generics om type-safe, herbruikbare componenten te creëren.

De standaard Go-projectlayout

Het meest bekende patroon is de golang-standards/project-layout, hoewel het belangrijk is op te merken dat dit geen officiële standaard is. Hier ziet u hoe een typische structuur eruitziet:

myproject/
├── cmd/
│   ├── api/
│   │   └── main.go
│   └── worker/
│       └── main.go
├── internal/
│   ├── auth/
│   ├── storage/
│   └── transport/
├── pkg/
│   ├── logger/
│   └── crypto/
├── api/
│   └── openapi.yaml
├── config/
│   └── config.yaml
├── scripts/
│   └── deploy.sh
├── go.mod
├── go.sum
└── README.md

Veel teams houden een klein gedeeld logging-pakket onder internal/ (bijvoorbeeld internal/logx) zodat binaries in cmd/ bij het opstarten één logger koppelen; pkg/logger/ in het bovenstaande schema is alleen geschikt als die code bedoeld is om door andere modules te worden geïmporteerd. Voor een productie-gerichte log/slog-opstelling (JSON-regels, redactie, context- en tracevelden, en signalen die goed samenwerken met monitoringstacks), zie Gestructureerd loggen in Go met slog voor observabiliteit en alerting.

De cmd/-directory

De cmd/-directory bevat de entry points van uw applicatie. Elke subdirectory vertegenwoordigt een aparte uitvoerbare binary. Bijvoorbeeld, cmd/api/main.go bouwt uw API-server, terwijl cmd/worker/main.go mogelijk een achtergrondjobprocessor bouwt.

Beste praktijk: Houd uw main.go-bestanden minimaal—slechts genoeg om afhankelijkheden te koppelen, configuratie te laden en de applicatie te starten. Alle substantiële logica behoort thuis in pakketten die main.go importeert.

// cmd/api/main.go
package main

import (
    "log"
    "myproject/internal/server"
    "myproject/internal/config"
)

func main() {
    cfg, err := config.Load()
    if err != nil {
        log.Fatal(err)
    }
    
    srv := server.New(cfg)
    if err := srv.Start(); err != nil {
        log.Fatal(err)
    }
}

De internal/-directory

Hier woont uw private applicatiecode. De Go-compiler voorkomt dat externe projecten pakketten binnen internal/ importeren, wat het ideaal maakt voor:

  • Bedrijfslogica en domeinmodellen
  • Applicatieservices
  • Interne API’s en interfaces
  • Database repositories (voor het kiezen van de juiste ORM, zie onze vergelijking van Go ORMs voor PostgreSQL)
  • Authenticatie- en autorisatielogica

Organiseer internal/ op basis van functie of domein, niet op technische laag. In plaats van internal/handlers/, internal/services/, internal/repositories/, geeft u de voorkeur aan internal/user/, internal/order/, internal/payment/ waarbij elk pakket zijn eigen handlers, services en repositories bevat.

Moet ik beginnen met een complexe directorystructuur?

Absoluut niet. Als u een klein hulpmiddel of prototype bouwt, begin dan met:

myproject/
├── main.go
├── go.mod
└── go.sum

Naarmate uw project groeit en u logische groeperingen identificeert, voegt u directories toe. U kunt een db/-pakket toevoegen wanneer de databaselogie substantieel wordt, of een api/-pakket wanneer HTTP-handlers vermenigvuldigen. Laat de structuur natuurlijk ontstaan in plaats van deze vooraf op te leggen.

Plat versus genest: de balans vinden

Een van de meest voorkomende fouten in Go-projectstructuur is overmatige nesting. Go geeft de voorkeur aan ondiepe hiërarchieën—typisch één of twee niveaus diep. Diepe nesting verhoogt de cognitieve belasting en maakt imports omslachtig.

Wat zijn de meest voorkomende fouten?

Over-nesting van directories: Vermijd structuren zoals internal/services/user/handlers/http/v1/. Dit creëert onnodige navigatiecomplexiteit. Gebruik in plaats daarvan internal/user/handler.go.

Generieke pakketnamen: Namen zoals utils, helpers, common of base zijn code smells. Ze communiceren geen specifieke functionaliteit en worden vaak stortplaatsen voor niet-verwante code. Gebruik beschrijvende namen: validator, auth, storage, cache.

Circulaire afhankelijkheden: Als pakket A pakket B importeert, en B importeert A, heeft u een circulaire afhankelijkheid—een compilatiefout in Go. Dit signalereert meestal een slechte scheiding van verantwoordelijkheden. Voer interfaces in of extrah gedeelde typen naar een apart pakket.

Mengen van verantwoordelijkheden: Houd HTTP-handlers gericht op HTTP-zaken, database repositories op data-toegang en bedrijfslogica in servicepakketten. Het plaatsen van bedrijfsregels in handlers maakt testen moeilijk en koppel uw domeinlogica aan HTTP.

Domain-Driven Design en Hexagonale Architectuur

Voor grotere applicaties, vooral microservices, biedt Domain-Driven Design (DDD) met Hexagonale Architectuur een duidelijke scheiding van verantwoordelijkheden.

Hoe structureer ik een microservice volgens Domain-Driven Design?

Hexagonale Architectuur organiseert code in concentrische lagen met afhankelijkheden die naar binnen stromen:

internal/
├── domain/
│   └── user/
│       ├── entity.go        # Domain models and value objects
│       ├── repository.go    # Repository interface (port)
│       └── service.go       # Domain services
├── application/
│   └── user/
│       ├── create_user.go   # Use case: create user
│       ├── get_user.go      # Use case: retrieve user
│       └── service.go       # Application service orchestration
├── adapter/
│   ├── http/
│   │   └── user_handler.go  # HTTP adapter (REST endpoints)
│   ├── postgres/
│   │   └── user_repo.go     # Database adapter (implements repository port)
│   └── redis/
│       └── cache.go         # Cache adapter
└── api/
    └── http/
        └── router.go        # Route configuration and middleware

Domeinlaag (domain/): Kernbedrijfslogica, entiteiten, value objects en domain service interfaces. Deze laag heeft geen afhankelijkheden van externe systemen—geen HTTP, geen database-imports. Het definieert repository interfaces (poorten) die adapters implementeren.

Applicatielaag (application/): Gevallen (use cases) die domeinobjecten orkestreren. Elk use case (bijv. “gebruiker aanmaken”, “betaling verwerken”) is een apart bestand of pakket. Deze laag coördineert domeinobjecten maar bevat zelf geen bedrijfsregels.

Adapterlaag (adapter/): Implementeert interfaces die door de binnenste lagen zijn gedefinieerd. HTTP-handlers converteren verzoeken naar domeinobjecten, database repositories implementeren persistantie, message queues verwerken asynchrone communicatie. Deze laag bevat alle framework-specifieke en infrastructuurcode.

API-laag (api/): Routes, middleware, DTO’s (Data Transfer Objects), API-versionering en OpenAPI-specificaties.

Deze structuur zorgt ervoor dat uw kernbedrijfslogica testbaar blijft en onafhankelijk van frameworks, databases of externe services. U kunt PostgreSQL vervangen door MongoDB, of REST door gRPC, zonder de domeincode aan te raken. Als u CQRS binnen deze layout adopteert, toont Implementeren van CQRS in Go hoe de application/-laag natuurlijk in kaart wordt gebracht naar aparte command- en query-handler pakketten, waarbij de command-zijde strikt blijft en de query-zijde DTO-gericht is.

Praktische patronen voor verschillende projecttypes

Kleine CLI-tool

Voor command-line applicaties wilt u een structuur die meerdere commando’s en subcommando’s ondersteunt. Overweeg het gebruik van frameworks zoals Cobra voor commandostructuur en Viper voor configuratiebeheer. Onze gids over het bouwen van CLI-applicaties in Go met Cobra & Viper behandelt dit patroon in detail.

mytool/
├── main.go
├── command/
│   ├── root.go
│   └── version.go
├── go.mod
└── README.md

REST API-service

Bij het bouwen van REST API’s in Go, scheidt deze structuur verantwoordelijkheden schoon: handlers behandelen HTTP-zaken, services bevatten bedrijfslogica en repositories beheren data-toegang. Voor een uitgebreide gids die benaderingen met de standaardbibliotheek, frameworks, authenticatie, testpatronen en productie-gerichte beste praktijken dekt, zie onze complete gids voor het bouwen van REST API’s in Go. Voor gestructureerd JSON-logging, verzoek- en tracecorrelatie en logvormen die alerting ondersteunen, zie Gestructureerd loggen in Go met slog voor observabiliteit en alerting.

myapi/
├── cmd/
│   └── api/
│       └── main.go
├── internal/
│   ├── config/
│   ├── middleware/
│   ├── user/
│   │   ├── handler.go
│   │   ├── service.go
│   │   └── repository.go
│   └── product/
│       ├── handler.go
│       ├── service.go
│       └── repository.go
├── pkg/
│   └── httputil/
├── go.mod
└── README.md

Monorepo met meerdere services

myproject/
├── cmd/
│   ├── api/
│   ├── worker/
│   └── scheduler/
├── internal/
│   ├── shared/        # Shared internal packages
│   ├── api/          # API-specific code
│   ├── worker/       # Worker-specific code
│   └── scheduler/    # Scheduler-specific code
├── pkg/              # Shared libraries
├── go.work           # Go workspace file
└── README.md

Testen en documentatie

Plaats testbestanden naast de code die ze testen met de _test.go-extensie:

internal/
└── user/
    ├── service.go
    ├── service_test.go
    ├── repository.go
    └── repository_test.go

Deze conventie houdt tests dicht bij de implementatie, waardoor ze makkelijk te vinden en te onderhouden zijn. Voor integratietests die meerdere pakketten raken, maakt u een aparte test/-directory op de projectroot. Voor uitgebreide begeleiding bij het schrijven van effectieve unit tests, inclusief table-driven tests, mocks, coverage-analyse en beste praktijken, zie onze gids voor Go unit-testing structuur en beste praktijken.

Documentatie behoort thuis in:

  • README.md: Projectoverzicht, instructies voor installatie, basisgebruik
  • docs/: Gedetailleerde documentatie, architectuurkeuzes, API-referenties
  • api/: OpenAPI/Swagger-specificaties, protobuf-definities

Voor REST API’s is het genereren en serveren van OpenAPI-documentatie met Swagger essentieel voor API-ontdekking en ontwikkelaarservaring. Onze gids over Swagger toevoegen aan uw Go API behandelt integratie met populaire frameworks en beste praktijken.

Afhankelijkheden beheren met Go Modules

Elk Go-project moet Go Modules gebruiken voor afhankelijkheidsbeheer. Voor een uitgebreide referentie over Go-commando’s en modulebeheer, bekijk onze Go Cheatsheet. Initialiseer met:

go mod init github.com/yourusername/myproject

Dit creëert go.mod (afhankelijkheden en versies) en go.sum (checksums voor verificatie). Houd deze bestanden in versiebeheer voor reproduceerbare builds.

Werk afhankelijkheden regelmatig bij:

go get -u ./...          # Update all dependencies
go mod tidy              # Remove unused dependencies
go mod verify            # Verify checksums

Belangrijkste punten

  1. Begin eenvoudig, evolueer natuurlijk: Over-engineer uw initiële structuur niet. Voeg directories en pakketten toe wanneer complexiteit dat vereist.

  2. Geef de voorkeur aan platte hiërarchieën: Beperk nesting tot één of twee niveaus. De platte pakketstructuur van Go verbetert de leesbaarheid.

  3. Gebruik beschrijvende pakketnamen: Vermijd generieke namen zoals utils. Noem pakketten naar wat ze doen: auth, storage, validator.

  4. Scheid verantwoordelijkheden duidelijk: Houd handlers gericht op HTTP, repositories op data-toegang en bedrijfslogica in servicepakketten.

  5. Gebruik internal/ voor privacy: Gebruik het voor code die niet extern geïmporteerd moet worden. De meeste applicatiecode behoort hier thuis.

  6. Pas architectuurpatronen toe wanneer nodig: Voor complexe systemen bieden Hexagonale Architectuur en DDD duidelijke grenzen en testbaarheid.

  7. Laat Go u leiden: Volg Go-idiomen in plaats van patronen te importeren uit andere talen. Go heeft zijn eigen filosofie over simpliciteit en organisatie.

Andere gerelateerde artikelen

Abonneren

Ontvang nieuwe berichten over systemen, infrastructuur en AI-engineering.