Go ist eine Sprache mit wenigen, aber strikt durchgehaltenen Naming-Regeln. Manches ist Sprach-Semantik (der erste Buchstabe entscheidet über Sichtbarkeit), anderes ist Konvention, die durch Stdlib, gofmt, go vet und Linter wie staticcheck als de-facto-Standard durchgesetzt wird. Wer Go-Code so schreibt wie die Stdlib, fügt sich automatisch in das Ökosystem ein — und umgekehrt sticht jeder Verstoß sofort heraus. Dieser Artikel sortiert die wichtigen Regeln aus Effective Go, den Code Review Comments und dem Google Go Style Guide und zeigt jeweils, wie idiomatischer Go-Code aussieht.

Die eine Regel, die wirklich zählt: Großbuchstabe = exportiert

Go hat keine public/private-Keywords. Stattdessen entscheidet der erste Buchstabe eines Identifiers über die Sichtbarkeit außerhalb des Pakets:

  • Großbuchstabe → exportiert, von außen importierbar.
  • Kleinbuchstabe → paket-intern, von außen unsichtbar.

Das gilt für jeden Identifier: Typen, Funktionen, Methoden, Variablen, Konstanten und sogar Struct-Felder.

Go user.go
package user

// User ist exportiert — sichtbar als user.User von außen.
type User struct {
    ID    int    // exportiert
    Name  string // exportiert
    email string // unexportiert — nur innerhalb des user-Pakets
}

// New ist exportiert.
func New(name string) *User { return &User{Name: name} }

// validate ist unexportiert — Helfer nur fürs eigene Paket.
func validate(u *User) error { return nil }

Das hat eine angenehme Konsequenz: Beim Lesen sieht man auf einen Blick, was Teil der API eines Pakets ist und was nicht. Man muss nicht in eine Definition springen, um Sichtbarkeit zu prüfen — die Schreibweise verrät es.

MixedCaps — kein snake_case

Mehrwortige Namen werden in Go camelCase beziehungsweise PascalCase geschrieben. Die offizielle Bezeichnung in der Go-FAQ lautet MixedCaps (exportiert) und mixedCaps (unexportiert).

Go mixedcaps.go
// Idiomatisch:
var maxRetryCount int
const DefaultTimeout = 5 * time.Second
func ReadFromFile(path string) ([]byte, error) { /* ... */ }

// Nicht idiomatisch — sofort als „kein Go-Code" erkennbar:
var max_retry_count int
const DEFAULT_TIMEOUT = 5
func read_from_file(path string) ([]byte, error) { /* ... */ }

Es gibt keinen Compiler-Fehler bei snake_case — die Sprache erlaubt es. Aber gofmt formatiert nur das Layout, nicht die Namen, und staticcheck (über die Regel ST1003) markiert jeden Verstoß. In der Praxis siehst du Underscores in Go-Code praktisch nie — außer in Test-Funktions-Namen wie TestUser_Save und in generierten Konstanten wie protobuf-Enums.

Package-Namen — kurz, ein Wort, klein

Package-Namen sind die Visitenkarte eines Pakets, weil sie an jedem Aufruf-Ort als Prefix erscheinen (http.Server, json.Marshal, fmt.Println). Daraus folgen drei Regeln:

  • Kleinbuchstaben, ein einzelnes Wort.
  • Kein Underscore, kein camelCase.
  • Knapp und aussagekräftig — der Importer tippt diesen Namen oft.
GutSchlechtWarum
httphttpUtils„Utils" ist nichtssagend
jsonJSON_parserKein Underscore, kein Caps
fmtformattingKnapp ist besser
userusermanagementLange Composita vermeiden
bufiodata_utilsSprechend, ein Wort, klein

Vermeide außerdem inhaltsleere Sammelnamen wie util, common, helpers, misc, types, interfaces. Sie sagen nichts über den Inhalt aus und werden mit der Zeit zur Mülldeponie für alles, was nirgends sonst hinpasst.

Innerhalb eines Pakets gilt: kein Stuttering. Wenn das Paket user heißt, dann nennst du den Typ User, nicht UserUser oder UserModel. Aufrufer sehen ohnehin user.User — die Verdoppelung würde nur stören. Aus dem gleichen Grund heißt der Server-Typ in net/http schlicht Server (also http.Server), nicht HTTPServer.

Variablen — Scope bestimmt die Länge

Go folgt einer einfachen Heuristik: Je größer der Scope, desto sprechender der Name. Eine Schleifen-Variable lebt zwei Zeilen, eine Paket-globale lebt das ganze Programm — das soll man der Schreibweise ansehen.

Go scope.go
// Klein und kurz — minimaler Scope:
for i, c := range items {
    sum += c.price
}

// Mittlere Funktionen — sprechender:
func processOrder(order *Order) error {
    total := order.calculateTotal()
    return saveTotal(total)
}

// Paket-Level — voll ausgeschrieben:
var defaultRequestTimeout = 30 * time.Second

Konkret: i, j, k für Indizes, r für Reader, w für Writer, c für Count oder Client, err für Errors, ctx für Context — diese Konventionen sind in der Stdlib vollkommen einheitlich. Lange Namen wie lineCount oder sliceIndex für eine drei Zeilen lange Schleife wirken in Go-Code überdimensioniert.

Receiver-Namen — 1–3 Buchstaben, konsistent

Receiver sind die Pendants zu this oder self aus anderen Sprachen. Go macht sie explizit und erlaubt einen frei wählbaren Namen. Konvention:

  • Ein bis drei Buchstaben — meist der Anfangsbuchstabe des Typs.
  • Konsistent über alle Methoden eines Typs hinweg.
  • Niemals me, self, this oder s für alles.
Go receiver.go
type User struct{ Name string }

// Idiomatisch — überall derselbe Receiver-Name:
func (u *User) Save(ctx context.Context) error  { /* ... */ }
func (u *User) Delete(ctx context.Context) error { /* ... */ }
func (u *User) String() string                   { return u.Name }

// Nicht idiomatisch — wechselnder Name verwirrt:
func (user *User) Save(ctx context.Context) error  { /* ... */ }
func (self *User) Delete(ctx context.Context) error { /* ... */ }
func (this *User) String() string                   { return this.Name }

In der Stdlib siehst du das Muster überall: b *Buffer, s *Scanner, f *File, r *Reader, t *Time. Der erste Buchstabe des Typs reicht — und reicht immer, auch wenn er sich mit einem Parameter überschneidet (dann wählt man eben einen zweiten Buchstaben wie cl für Client).

Interface-Namen — -er für eine Methode, Domäne für mehrere

Interfaces in Go folgen einer eigenen Naming-Konvention, je nach Anzahl der Methoden:

  • Eine Methode → Methodenname + -er-Suffix als Agent-Substantiv.
  • Mehrere Methoden → ein Domänen-Begriff.
Go interfaces.go
// Eine-Methoden-Interfaces — idiomatisch mit -er:
type Reader interface  { Read(p []byte) (n int, err error) }
type Writer interface  { Write(p []byte) (n int, err error) }
type Closer interface  { Close() error }
type Stringer interface{ String() string }

// Mehrere Methoden — Domänen-Begriff statt Methoden-Liste:
type Handler interface { // http.Handler
    ServeHTTP(w ResponseWriter, r *Request)
}

type Interface interface { // sort.Interface
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
}

Wenn dein Typ eine Methode mit derselben Bedeutung wie eine bekannte Stdlib-Methode hat (Read, Write, Close, String, Error, Flush), benutze die gleiche Signatur und den gleichen Namen — sonst riskierst du, dass dein Typ versehentlich ein bekanntes Interface erfüllt (oder eben nicht).

Die zweite Empfehlung aus dem Google Style Guide: Interfaces gehören in das Paket, das sie konsumiert, nicht in das Paket, das sie implementiert. Definiere keine Interfaces „auf Vorrat" oder nur fürs Mocking — Go-Interfaces sind so leichtgewichtig, dass sie erst dort entstehen sollten, wo sie tatsächlich gebraucht werden.

Errors — Sentinels, lokale err, Custom-Types

Beim Benennen von Errors haben sich drei Muster eingebürgert:

  • Paket-globale Sentinel-Errors — Prefix Err, danach MixedCaps: ErrNotFound, ErrInvalidInput, io.EOF (historische Ausnahme).
  • Lokale Variable — schlicht err, in jeder Funktion gleich.
  • Custom-Error-Type — Suffix Error: *PathError, *SyntaxError, *ValidationError.
Go errors.go
package store

import "errors"

// Sentinel — exportiert, mit Err-Prefix:
var ErrNotFound = errors.New("store: not found")

// Custom-Error-Type — Suffix Error:
type ValidationError struct {
    Field string
    Msg   string
}

func (e *ValidationError) Error() string {
    return e.Field + ": " + e.Msg
}

// Lokal: immer „err":
func Load(id string) (*Record, error) {
    r, err := db.Find(id)
    if err != nil {
        return nil, err
    }
    return r, nil
}

Eine Stilregel aus den Code Review Comments: Error-Strings selbst werden klein geschrieben und enden nicht mit Satzzeichen. Sie werden meist mit Kontext davor und/oder dahinter zusammengesetzt — fmt.Errorf("config laden: %w", err) produziert dann saubere Ketten wie config laden: open /etc/x: not found.

Booleans — Verb-Präfix, kein Get

Boolesche Werte und Methoden, die einen Boolean zurückgeben, tragen idiomatisch ein Verb-Präfix: is…, has…, can…, should…. Das liest sich an der Aufrufstelle natürlich:

Go booleans.go
if u.IsAdmin() && cache.HasKey("config") {
    // ...
}

if !path.CanResolve() {
    return ErrInvalidPath
}

var isReady bool
var hasNext bool

Und genauso wichtig: kein Get-Präfix für Getter. Effective Go ist dabei sehr deutlich:

It's neither idiomatic nor necessary to put Get into the getter's name.

Hat eine Struct das Feld owner, heißt der Getter Owner() — nicht GetOwner(). Der Setter heißt dann SetOwner(o User). Begründung: Der Großbuchstabe macht ohnehin schon klar, dass es sich um eine exportierte Methode handelt; Get ist redundantes Geräusch aus Sprachen wie Java, die kein Property-System haben.

Acronyms — immer alle Buchstaben gleich groß

Die wahrscheinlich am häufigsten übertretene Regel — und gleichzeitig die am leichtesten erkennbare:

Words in names that are initialisms or acronyms (e.g. "URL" or "NATO") have a consistent case.

Konkret: Ein Akronym wird vollständig groß oder vollständig klein geschrieben — nie gemischt. Daraus folgt:

IdiomatischFalschAkronym
URLUrlURL
parseURLparseUrlURL
HTTPClientHttpClientHTTP
ServeHTTPServeHttpHTTP
userIDuserIdID
XMLHTTPRequestXmlHttpRequestXML + HTTP
JSONEncoderJsonEncoderJSON
IPAddressIpAddressIP

Die Regel gilt auch am Wort-Anfang in unexportierten Namen: urlPony (klein, weil unexportiert) oder URLPony (groß), aber niemals UrlPony. Bei mehreren Akronymen hintereinander folgen sie der gleichen Regel: xmlHTTPRequest oder XMLHTTPRequest.

staticcheck (Regel ST1003) markiert das zuverlässig — wenn dein Editor mit gopls läuft, siehst du Verstöße in Echtzeit unterstrichen.

Konstanten — auch nur MixedCaps

Aus C, Java oder Python ist man SCREAMING_SNAKE_CASE für Konstanten gewohnt. In Go gibt es das nicht. Konstanten folgen exakt denselben Regeln wie Variablen und Funktionen:

  • ExportiertMaxRetries, DefaultTimeout, Pi.
  • UnexportiertmaxBufSize, defaultPort.
Go constants.go
// Idiomatisch:
const (
    MaxRetries     = 3
    DefaultTimeout = 30 * time.Second
    apiVersion     = "v1" // unexportiert
)

// Nicht idiomatisch — wirkt wie aus C übernommen:
const (
    MAX_RETRIES     = 3
    DEFAULT_TIMEOUT = 30
    API_VERSION     = "v1"
)

Der Hintergrund: In Go gibt es keine syntaktische Unterscheidung zwischen Konstanten und Variablen am Aufruf-Ort. MaxRetries und defaultTimeout werden im Code aufgerufen wie alles andere — und folgen deshalb auch der gleichen Schreibweise.

Interessantes

Pike: "Don't use Get prefix for getters."

Effective Go formuliert es trocken: „It's neither idiomatic nor necessary to put Get into the getter's name." In einer Sprache mit klarer Großbuchstaben-Regel für Sichtbarkeit ist Get schlicht redundantes Geräusch — übernommen aus Java/C#, wo Properties syntaktisch nicht existieren. obj.Owner() reicht; obj.GetOwner() wirkt für Go-Augen sofort fremd.

„MixedCaps" ist der offizielle Begriff — nicht „camelCase".

Die Go-FAQ und Effective Go nennen die Konvention konsequent MixedCaps beziehungsweise mixedCaps. „camelCase" ist die geläufige Allgemein-Bezeichnung, taucht in den offiziellen Go-Dokumenten aber praktisch nicht auf. Wer im Go-Kontext „MixedCaps" sagt, klingt wie jemand, der die Quellen gelesen hat.

http.Server heißt nicht HTTPServer — Stuttering vermeiden.

Weil das Paket schon http heißt, ergibt der Typ Server — Aufrufer sehen http.Server. http.HTTPServer würde sich an der Aufruf-Stelle als http.HTTPServer doppeln. Das gleiche Prinzip steckt hinter bufio.Reader (statt BufReader) und ring.New (statt NewRing, weil Ring der einzige exportierte Typ ist).

golint ist tot — staticcheck ist der Nachfolger.

Der historische golint (von Robert Griesemer geschrieben) wurde 2020 von der Go-Maintainer-Gruppe deprecated. Sein faktischer Nachfolger ist staticcheck mit der Regel-Familie ST1003 für Naming-Verstöße. In CI-Pipelines wird staticcheck meist über den Meta-Linter golangci-lint gestartet.

Receiver-Naming: schau in die Stdlib.

Die Receiver-Konvention ist nirgends per Compiler erzwungen, aber in der Stdlib so konsequent durchgehalten, dass sie als gelebte Norm wirkt: b *Buffer, s *Scanner, f *File, r *Reader, t *Time, c *Client. Meist der erste Kleinbuchstabe des Typs — und immer derselbe Buchstabe für alle Methoden eines Typs.

Empty struct als Set-Marker — struct{}.

Eine Map mit Wert-Typ struct{} belegt für den Wert null Bytes — der Compiler optimiert leere Structs vollständig weg. Das idiomatische Set-Pattern in Go: seen := map[string]struct{}{}; seen["x"] = struct{}{}. Kein Speicher-Overhead, semantisch klar als „Marker, kein Wert" lesbar.

Die Initialism-Liste umfasst über 30 Acronyms.

Die Code Review Comments listen explizit Akronyme, die durchgängig groß geschrieben werden müssen: URL, HTML, HTTP, HTTPS, JSON, XML, YAML, ID, IP, TCP, UDP, TLS, SSH, FTP, SQL, API, UID, GID, UUID, ASCII, UTF8, CPU, RAM, RPC, gRPC, OS, IO, DB, DNS, LHS, RHS und weitere. Für Code-Review-Diskussionen lohnt es sich, diese Liste griffbereit zu haben.

Test-, Benchmark- und Example-Funktionen folgen festen Mustern.

Das testing-Package erkennt Test-Funktionen ausschließlich an der Signatur und am Namens-Prefix: TestXxx(t *testing.T), BenchmarkXxx(b *testing.B), FuzzXxx(f *testing.F), ExampleXxx(). Das Xxx muss mit einem Großbuchstaben beginnen — Klein-Anfang ist kein Test mehr und wird ignoriert. Hier ist Naming sogar Sprach-Mechanik, nicht nur Konvention.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Grundlagen

Zur Übersicht