Go organisiert Code nicht in Klassen oder Modulen, sondern in Packages — und zwar exakt einem pro Verzeichnis. Imports sind explizit, Sichtbarkeit hängt allein an der Großschreibung des ersten Buchstabens, und der Compiler erzwingt einige Regeln strikter als andere Sprachen. Dieser Artikel erklärt die Package-Klausel, alle vier Import-Formen, den Unterschied zwischen package main und Library-Paketen, die Rolle von internal/, die Reihenfolge von init()-Funktionen und welche Stdlib-Pakete in fast jedem Projekt auftauchen.
Was ein Package ist
Ein Package in Go ist nicht „eine Datei" und nicht „eine Klasse" — es ist die Sammlung aller .go-Dateien in einem Verzeichnis, die dieselbe package-Klausel an Zeile 1 tragen. Die Sprach-Spezifikation drückt es so aus:
Programs are constructed from packages, whose properties allow efficient management of dependencies.
Das bedeutet konkret:
- Ein Verzeichnis enthält genau ein Package. Mehrere Packages pro Ordner sind nicht erlaubt (Ausnahme:
_test-Suffix für externe Tests). - Alle Dateien teilen denselben Package-Block — sie sehen sich gegenseitig ohne Import. Eine Funktion in
a.gokann eine Variable ausb.godirekt nutzen, sofern beidepackage foodeklarieren. - Der Package-Name muss nicht zwingend gleich dem Ordnernamen sein, ist es aber in der Regel. Konventionell: lowercase, ein Wort, keine Unterstriche.
package greet
func Hello(name string) string {
return "Hallo, " + name
}package greet
// Gleiche Package-Klausel — gleicher Scope.
// Kein Import nötig, um Hello() zu sehen.
func Loud(name string) string {
return Hello(name) + "!"
}package main vs. Library-Packages
Es gibt nur zwei Sorten von Packages — und der Unterschied ist binär.
package main— produziert ein ausführbares Programm. Muss eine Funktionfunc main()enthalten.go builderzeugt daraus eine Binary, deren Name vom Verzeichnis (oder Modul-Pfad) abgeleitet wird.- Alle anderen Namen — produzieren eine Library. Werden importiert und genutzt, aber nicht direkt ausgeführt.
go buildfür einen Library-Ordner kompiliert ihn zwar, erzeugt aber keine Binary.
package main
import "example.com/myapp/greet"
func main() {
println(greet.Hello("Welt"))
}Eine Faustregel aus dem Standard-Layout: ausführbare Programme leben unter cmd/<name>/, Bibliotheks-Code unter internal/ oder im Modul-Root. Mehr dazu im Projektstruktur-Artikel.
Import-Syntax
Go kennt vier Formen des Imports, die du in nahezu jeder Codebase findest.
package main
// 1. Single-Import — kompakt für Einzelfälle, selten in echtem Code.
import "fmt"
// 2. Grouped-Import — der Standard. gofmt sortiert alphabetisch.
import (
"encoding/json"
"net/http"
"time"
"github.com/google/uuid"
)
// 3. Aliased-Import — gibt dem Package einen lokalen Namen.
import f "fmt"
// 4. Dot-Import — bringt Symbole direkt in den File-Scope.
// Praktisch nur in Test-DSLs sinnvoll. Sonst vermeiden.
import . "fmt"
// 5. Blank-Import — lädt das Package nur wegen seiner init()-Side-Effects.
import _ "github.com/lib/pq"In der Praxis siehst du fast ausschließlich die gruppierte Form mit Klammern. goimports (oder gofmt ab Go 1.19) sortiert Imports automatisch und gruppiert Stdlib oben, externe Module darunter — durch eine Leerzeile getrennt.
| Form | Syntax | Wann |
|---|---|---|
| Single | import "fmt" | Quick-Scripts, ein einzelnes Package |
| Grouped | import ( ... ) | Standard. Immer wenn ≥ 2 Imports |
| Aliased | import f "fmt" | Namens-Konflikte oder zu lange Namen |
| Dot | import . "fmt" | Selten. Test-DSLs (gomega, ginkgo) |
| Blank | import _ "..." | Side-Effect-Imports (DB-Treiber) |
Import-Pfade
Ein Import-Pfad ist ein String, der ein Package eindeutig identifiziert. Es gibt drei Quellen:
- Standard-Bibliothek — kurze, slash-getrennte Pfade ohne Domain.
fmt,net/http,encoding/json,crypto/sha256. Der vollständige Index: pkg.go.dev/std. - Externe Module — beginnen mit einer Domain, typischerweise einer Code-Forge.
github.com/google/uuid,golang.org/x/sync/errgroup,gopkg.in/yaml.v3. Werden ingo.modals Dependency aufgeführt. - Lokale Packages im selben Modul — bestehen aus dem Modul-Pfad plus Unterverzeichnis. Wenn
go.modmodule example.com/myappdeklariert, dann ist das Package ininternal/auth/alsexample.com/myapp/internal/authimportierbar.
package main
import (
"log" // stdlib
"net/http" // stdlib
"github.com/google/uuid" // extern
"example.com/myapp/internal/auth" // lokal, gleiches Modul
"example.com/myapp/internal/store" // lokal
)
func main() {
id := uuid.New()
log.Println("starting:", id)
_ = auth.New
_ = store.New
_ = http.ListenAndServe
}Relative Pfade wie import "./helpers" gibt es in Modul-Mode nicht mehr. Jeder Import läuft über einen vollständigen Pfad.
Sichtbarkeit über Großschreibung
Go hat keine Schlüsselwörter public, private oder protected. Stattdessen entscheidet der erste Buchstabe des Identifiers, ob ein Symbol außerhalb seines Packages sichtbar ist.
- Großbuchstabe → exportiert. Von anderen Packages aus nutzbar.
- Kleinbuchstabe → paket-intern. Außerhalb unsichtbar, der Compiler verbietet den Zugriff.
Das gilt für alle top-level Identifier: Funktionen, Typen, Variablen, Konstanten, Methoden, Struct-Felder.
package counter
// Exportiert — von außen nutzbar als counter.Counter.
type Counter struct {
Value int // exportiert
label string // privat — nur innerhalb des Packages sichtbar
}
// Exportiert.
func New(label string) *Counter {
return &Counter{label: label}
}
// Privat — Hilfsfunktion, kein Teil der API.
func reset(c *Counter) {
c.Value = 0
}In einem anderen Package gilt dann:
c := counter.New("requests")
c.Value++ // OK — Value ist exportiert
// c.label = "x" // Compile-Fehler: cannot refer to unexported field
// counter.reset(c) // Compile-Fehler: undefined: counter.resetDie init()-Funktion
Jedes Package darf eine (oder mehrere) init()-Funktionen deklarieren. Sie laufen automatisch beim Programmstart, bevor main() aufgerufen wird, und nehmen weder Argumente noch geben sie Werte zurück.
package config
import "os"
var Region string
func init() {
Region = os.Getenv("AWS_REGION")
if Region == "" {
Region = "eu-central-1"
}
}Die Aufruf-Reihenfolge folgt einem festen Schema:
- Alle Package-Variablen werden in Deklarations-Reihenfolge initialisiert (mit Berücksichtigung von Abhängigkeiten).
- Anschließend laufen alle
init()-Funktionen — pro Datei in Datei-Reihenfolge, pro Package in Import-Reihenfolge der Dependencies. - Erst wenn alle
init()-Funktionen aller importierten Packages durchgelaufen sind, startetmain().
init() eignet sich für: Registrieren von Treibern und Codecs, Einlesen von Build-Konfiguration, Pre-Compilation von Templates oder Regex. Es eignet sich nicht für Logik mit Fehler-Handling — init() kann nichts zurückgeben, ein panic reißt den ganzen Prozess mit.
internal/ — die erzwungene Boundary
Go hat einen einzigen, im Compiler verankerten Sichtbarkeits-Mechanismus auf Verzeichnis-Ebene: das magische Verzeichnis-Segment internal/.
Ein Package unter .../foo/internal/bar darf ausschließlich von Packages importiert werden, deren Pfad mit .../foo/ beginnt. Alles außerhalb dieses Sub-Trees bekommt einen Compile-Fehler.
example.com/myapp/
├── go.mod
├── cmd/api/main.go # darf example.com/myapp/internal/* importieren
├── pkg/public/lib.go # darf example.com/myapp/internal/* importieren
└── internal/
└── auth/auth.go # nur von example.com/myapp/... aus erreichbarSobald jemand außerhalb deines Moduls example.com/myapp/internal/auth importieren will, bricht der Build mit use of internal package not allowed. Das macht internal/ zum bevorzugten Ort für alles, was nicht öffentliche API ist — dazu mehr im Projektstruktur-Artikel.
Stdlib-Highlights
Die Go-Standardbibliothek ist groß genug, dass viele Server-Workloads ohne externe Dependencies auskommen. Diese Pakete tauchen in fast jedem Projekt auf:
| Package | Wofür |
|---|---|
fmt | Formatierte I/O — Printf, Sprintf, Errorf |
errors | Error-Werte — errors.New, errors.Is, errors.As |
os | Prozess-API — Args, Env, Datei-System, Exit-Codes |
io / io/fs | Reader/Writer-Interfaces, Filesystem-Abstraktion |
strings | String-Manipulation — Contains, Split, ReplaceAll |
strconv | Konvertierung String ↔ Zahl — Itoa, Atoi, ParseFloat |
time | Datum, Zeit, Timer, Duration |
net/http | HTTP-Client und -Server — produktionsreif aus der Box |
encoding/json | JSON Marshal/Unmarshal mit Struct-Tags |
log / log/slog | Klassisches und strukturiertes Logging (slog ab 1.21) |
context | Cancellation, Deadlines, Request-Scoped Values |
sync | Mutex, WaitGroup, Once — Low-Level Concurrency |
Wer Go produktiv schreibt, kennt diese Pakete im Schlaf. Eine vollständige Übersicht (Stub) entsteht im Bereich Standard Library.
Externe Pakete hinzufügen
Externe Dependencies werden über Go Modules verwaltet. Der Workflow ist denkbar kurz:
# 1. Dependency holen (lädt herunter, aktualisiert go.mod und go.sum)
go get github.com/google/uuid
# 2. In Quelldatei importieren und nutzen
# → siehe import-Block unten
# 3. Aufräumen — entfernt unbenutzte, ergänzt fehlende Einträge
go mod tidypackage main
import (
"fmt"
"github.com/google/uuid"
)
func main() {
fmt.Println("ID:", uuid.New())
}go mod tidy ist der Befehl, den man am häufigsten tippt — er hält go.mod und go.sum synchron mit den tatsächlich verwendeten Imports. Eine bestimmte Version pinnen geht über go get github.com/google/uuid@v1.6.0.
Häufige Stolperfallen
Circular Imports brechen den Build sofort.
Wenn Package A B importiert und B A importiert, weigert sich der Compiler mit import cycle not allowed. Go hat — anders als manche Sprachen — keine Lazy-Resolution für Imports. Lösung: das gemeinsame Stück in ein drittes Sub-Package extrahieren oder eine Schnittstelle auf der konsumierenden Seite definieren, sodass die Abhängigkeit nur in eine Richtung läuft.
internal/ außerhalb seines Eltern-Trees zu importieren ist ein Compile-Fehler.
Der Compiler erzwingt die Boundary unbestechlich. Wer github.com/owner/repo/internal/foo aus einem fremden Modul importieren will, sieht use of internal package not allowed. Das ist ein Feature, kein Bug — internal/ ist die einzige Möglichkeit, eine echte API-Grenze zu setzen.
Unused Imports und Variablen sind Compile-Fehler.
Go duldet keinen Zombie-Code. Ein nicht verwendeter Import bricht den Build mit imported and not used. Beim Refactoring stört das, im Endergebnis sorgt es für saubere Dateien. Der Editor-Helfer goimports räumt automatisch auf — sowohl beim Hinzufügen als auch beim Entfernen — und gehört in jedes ernstzunehmende Go-Setup.
Dot-Imports verschleiern den Ursprung von Symbolen.
import . "fmt" bringt Println direkt in den File-Scope, ohne fmt.-Präfix. Beim Lesen weiß man dann nicht mehr, woher Println kommt — eigene Funktion? Stdlib? Drittpaket? In Test-DSLs wie ginkgo ist es etabliert, im Produktiv-Code praktisch immer eine schlechte Idee.
Blank-Imports haben einen ganz konkreten Sinn.
import _ "github.com/lib/pq" heißt nicht „ich brauche das nicht" — es heißt „ich brauche nur die init()-Side-Effects, keine Symbole". Klassisches Beispiel: Database-Driver registrieren sich in ihrer init()-Funktion bei database/sql. Ohne Blank-Import wäre der Treiber nie geladen. Wer aus Versehen einen _-Import setzt, weil der Compiler maulte, hat einen Fehler eingebaut, nicht behoben.
Package-Name ≠ Ordnername (aber Konvention sagt: identisch halten).
Go erlaubt, dass ein Ordner userauth ein Package namens auth enthält. Der Import-Pfad bleibt .../userauth, aber im Code referenziert man auth.Login. Das ist verwirrend — und der Grund, warum die Konvention ist: Ordnername und Package-Name gleich halten. Eine Ausnahme: Versions-Suffixe wie v2/ werden nicht ins Package übernommen.
Zwei Packages mit gleichem Namen brauchen einen Alias.
Wer crypto/rand und math/rand im selben File braucht, bekommt einen Konflikt — beide heißen schlicht rand. Lösung ist der Aliased-Import: import cryptorand "crypto/rand" und import mathrand "math/rand". Die Aliase sind nur file-scoped, jede Datei darf eigene wählen.
Go importiert nicht transitiv.
Wenn Package A das Package B nutzt und du nur A importierst, hast du B nicht automatisch im Scope. Jede Datei muss explizit auflisten, was sie selbst direkt referenziert. Das macht Imports zu einem akkuraten Index dessen, was eine Datei tatsächlich verwendet — und ist der Grund, warum goimports so präzise arbeiten kann.
Weiterführende Ressourcen
Externe Quellen
- Packages – Go Language Specification
- Import declarations – Go Specification
- Package names – Effective Go
- Standard library index – pkg.go.dev
- goimports – Tool
Verwandte Artikel
- Was ist Go?
- Projektstruktur und go.mod
- Naming Conventions
- Standard Library (Übersicht-Stub)
- Funktionen