Menu

Golang Projektstruktur: cmd, internal und Paketlayout

So strukturierst du ein Go-Projekt: flach anfangen, in Pakete aufteilen, wenn es einen Grund gibt, cmd/ für mehrere Binaries und internal/ für Code, den niemand sonst importieren darf, Pakete gut benennen und Tests neben den Code legen.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Mit einer Datei anfangen

Ein Go-Programm kann komplett in einer main.go leben, und kleine Tools sollten das auch. Dieses vollständige Programm parst Eingaben, erledigt seine Arbeit und gibt einen Bericht aus, alles in einem Paket:

Wächst es, teilst du es in weitere Dateien auf, im selben Verzeichnis und im selben Paket (parse.go, report.go, main.go). Dateien in einem Paket teilen sich jeden Bezeichner, also muss nichts exportiert oder importiert werden. Für ein Projekt mit ein paar tausend Zeilen ist das oft schon die ganze Struktur, die es braucht.

Denk daran, dass ein package main aus mehreren Dateien als Paket ausgeführt werden muss: go run ., nicht go run main.go. Sonst werden die Dateien einzeln kompiliert, und du bekommst undefined-Fehler für Namen, die in den anderen Dateien definiert sind.

Es gibt kein vorgeschriebenes Layout

Go verlangt keine Verzeichnisstruktur außer „ein Paket pro Verzeichnis“. Die offizielle Empfehlung ist die Seite Organizing a Go module auf go.dev, und sie beschreibt ein paar Formen, keine Regeln.

Das GitHub-Repository golang-standards/project-layout wird viel kopiert und oft für einen Standard gehalten. Es ist eine Community-Sammlung von Konventionen aus großen Projekten, und Russ Cox, damals Tech Lead von Go, hat dort ein Issue eröffnet, das klarstellt, dass es kein Go-Standard ist. Die meisten seiner Verzeichnisse (pkg/, api/, build/, deployments/) ergeben nur für große Codebasen Sinn. Das ganze Gerüst in ein neues Projekt zu kopieren erzeugt leere Ordner und tiefe Importpfade ohne jeden Nutzen.

Die Regeln, die das Tool tatsächlich durchsetzt, sind kurz:

  • Ein Verzeichnis ist ein Paket. Alle .go-Dateien darin (außer _test.go-Dateien mit dem Suffix _test) teilen sich einen Paketnamen.
  • Der Importpfad ist der Modulpfad plus der Verzeichnispfad.
  • Ein Verzeichnis namens internal schränkt ein, wer importieren darf, was darin liegt.
  • Verzeichnisse namens testdata und solche, die mit . oder _ beginnen, ignoriert das Go-Tool.

Die verbreiteten Formen

Ein einzelner Befehl

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

Flach, alles in package main. go install github.com/you/todo@latest funktioniert, und das Binary heißt todo.

Eine Bibliothek

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

Das Paket liegt an der Wurzel des Moduls, also importieren Nutzer github.com/you/slug und rufen slug.Make(...) auf. Alles, was du nicht als öffentliche API pflegen willst, kommt unter internal/.

Ein Service oder mehrere Binaries

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 enthält ein Verzeichnis pro Binary, und der Verzeichnisname wird zum Namen des Binarys (go build ./cmd/shop-api erzeugt shop-api). Jedes main bleibt schlank: Konfiguration lesen, Abhängigkeiten aufbauen, den Server starten. Der eigentliche Code lebt in Paketen unter internal/, wo beide Binaries ihn nutzen können und kein anderes Modul.

Auf dieses Layout laufen die meisten Go-Services hinaus. Greif dazu, wenn du ein zweites Binary oder einen echten Grund zum Aufteilen der Pakete hast, nicht am ersten Tag.

internal/ wird vom Befehl go durchgesetzt

Ein Paket unter internal/ kann nur von Code in dem Baum importiert werden, der beim Elternverzeichnis von internal beginnt. Aus einem anderen Modul scheitert der Build:

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

Damit ist internal/ das Werkzeug, um deine API-Oberfläche klein zu halten. Code dort kann sich frei ändern, weil du weißt, dass jeder Aufrufer in deinem eigenen Repository liegt. Bei einer Anwendung ist es vernünftig, fast alles nach internal/ zu legen. Bei einer Bibliothek trennt es das, was du stabil zu halten versprichst, von dem, was du nicht versprichst.

internal funktioniert in jeder Tiefe: shop/internal/order ist für ganz shop/ sichtbar, während shop/internal/order/internal/pricing nur innerhalb von shop/internal/order/ sichtbar ist.

Pakete benennen

Der Paketname ist Teil jeder Aufrufstelle, also zählt er mehr als der Verzeichnisbaum.

  • Kurz, kleingeschrieben, ein Wort: order, store, httpapi. Keine Unterstriche und kein mixedCaps.
  • Benenne es nach dem, was es bereitstellt, nicht nach dem, was es enthält. util, common, helpers, misc und models sagen nichts und werden zu Abladeplätzen; die Stilrichtlinien des Go-Teams raten ausdrücklich davon ab. Leg einen Helfer in das Paket, das ihn nutzt, oder in ein Paket, das nach seinem Zweck benannt ist (slug, retry).
  • Vermeide Stottern. Aufrufer schreiben order.Order und order.New, also nenne nichts order.OrderService oder order.NewOrder. Die Standardbibliothek hält das konsequent ein: http.Server, nicht http.HTTPServer.
  • Verzeichnisname und Paketname sollten übereinstimmen, abgesehen von package main in Befehlsverzeichnissen. Unterscheiden sie sich, müssen Leser eine Datei öffnen, um zu erfahren, welchen Bezeichner der Import einführt.

Teil Pakete nach Verantwortung auf, nicht nach Art. Eine Aufteilung in models/, controllers/, services/ (verbreitet in anderen Ökosystemen) zwingt jedes Feature, drei Pakete anzufassen, und erzeugt gern Import-Zyklen, die Go verbietet. Pakete, die um ein Konzept der Domäne herum organisiert sind (order, payment, user), halten jeweils ihre Typen und ihre Logik zusammen.

Wohin Tests gehören

Tests leben neben dem Code im selben Verzeichnis, in Dateien namens *_test.go. Einen tests/-Baum gibt es nicht.

  • package order in order_test.go: ein interner Test, der nicht exportierte Bezeichner benutzen kann.
  • package order_test im selben Verzeichnis: ein externer Test, der nur die exportierte API sieht, so wie ein Aufrufer. Nützlich für Beispiele und um Import-Zyklen in Tests zu vermeiden.
  • Fixture-Dateien gehören in ein Verzeichnis testdata/ neben den Tests. Das Go-Tool ignoriert es, und Tests laufen mit dem Paketverzeichnis als Arbeitsverzeichnis, also funktionieren relative Pfade wie testdata/big.json.

Weitere Dateien an der Wurzel

Datei oder VerzeichnisZweck
go.mod, go.sumModuldefinition und Checksummen der Abhängigkeiten, immer an der Wurzel
README.md, LICENSEwie in jedem Projekt
Makefile oder Taskfile.ymloptionale Build-Abkürzungen
Dockerfilebei Services meist an der Wurzel
migrations/, web/, docs/Nicht-Go-Dateien, benannt nach ihrem Inhalt
tools.goälterer Weg, Tool-Abhängigkeiten festzulegen; Go 1.24 ersetzt ihn durch tool-Zeilen in go.mod (go get -tool)
go.workein Workspace, um mehrere Module lokal gemeinsam zu entwickeln; bei Projekten mit einem Modul meist nicht committet

Häufige Fehler

  • Ein großes Template für ein kleines Projekt kopieren. Fang flach an und füg Verzeichnisse hinzu, wenn ein zweites Binary oder eine echte Grenze auftaucht.
  • Pakete namens utils oder common. Benenne Pakete nach dem, was sie tun.
  • Ein Paket pro Datei oder pro Typ. Go-Pakete sind als größere Einheiten gedacht als Java-Klassen. Ein Paket mit zehn Dateien ist normal.
  • Import-Zyklen. Zwei Pakete können sich nicht gegenseitig importieren. Meist heißt das, dass sie zusammengehören, dass ein gemeinsamer Typ in ein tiefer liegendes Paket wandern sollte oder dass eine Seite von einem kleinen Interface statt vom anderen Paket abhängen sollte.
  • pkg/ aus Reflex. Es fügt jedem Importpfad ein Segment hinzu, ohne Bedeutung hinzuzufügen.
  • Tests in einem separaten Verzeichnis. Dort sehen sie keinen nicht exportierten Code, und das Tooling erwartet sie dort nicht.

Häufig gestellte Fragen

Gibt es ein offizielles Projektlayout für Go?

Kein verbindliches. Das Go-Team veröffentlicht unter go.dev/doc/modules/layout eine Empfehlung, die ein paar verbreitete Formen beschreibt (ein einzelnes Paket, ein Befehl, mehrere Befehle mit internal/). Das beliebte Repository golang-standards/project-layout ist ein Community-Projekt, kein Go-Standard, und das Go-Team hat das öffentlich gesagt.

Was ist das Verzeichnis internal in Go?

Ein Paket, dessen Importpfad ein Element internal enthält, kann nur von Code importiert werden, der unterhalb des Elternverzeichnisses dieses internal liegt. example.com/app/internal/store lässt sich überall unter example.com/app/ importieren, und der Befehl go lehnt Imports aus jedem anderen Modul mit use of internal package ... not allowed ab.

Sollte ich in Go ein Verzeichnis pkg verwenden?

Es ist optional und fügt ein Pfadsegment hinzu, ohne Bedeutung hinzuzufügen. Manche großen Projekte trennen mit pkg/ öffentlichen Bibliothekscode vom Rest, aber die Go-Standardbibliothek und die meisten modernen Projekte tun das nicht. Leg importierbare Pakete an die Wurzel des Moduls oder in benannte Verzeichnisse und alles Private nach internal/.

Wohin gehören Testdateien in einem Go-Projekt?

In dasselbe Verzeichnis wie der Code, den sie testen, mit dem Namen *_test.go. Go hat keinen separaten Testbaum. Fixture-Dateien gehören in ein Verzeichnis testdata neben den Tests, das das Go-Tool bei der Suche nach Paketen ignoriert.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S