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
internalschränkt ein, wer importieren darf, was darin liegt. - Verzeichnisse namens
testdataund 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,miscundmodelssagen 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.Orderundorder.New, also nenne nichtsorder.OrderServiceoderorder.NewOrder. Die Standardbibliothek hält das konsequent ein:http.Server, nichthttp.HTTPServer. - Verzeichnisname und Paketname sollten übereinstimmen, abgesehen von
package mainin 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 orderinorder_test.go: ein interner Test, der nicht exportierte Bezeichner benutzen kann.package order_testim 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 wietestdata/big.json.
Weitere Dateien an der Wurzel
| Datei oder Verzeichnis | Zweck |
|---|---|
go.mod, go.sum | Moduldefinition und Checksummen der Abhängigkeiten, immer an der Wurzel |
README.md, LICENSE | wie in jedem Projekt |
Makefile oder Taskfile.yml | optionale Build-Abkürzungen |
Dockerfile | bei 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.work | ein 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
utilsodercommon. 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.