Scope beantwortet die Frage: „An welchen Stellen im Code ist ein bestimmter Name sichtbar?" In Go ist die Antwort glücklicherweise einfach — die Sprache kennt nur fünf Scope-Ebenen, und alle folgen lexikalischer Bindung: was du im Code schreibst, gilt im jeweils umschließenden Block. Es gibt kein dynamisches Scoping, keine versteckten Capturing-Regeln wie in JavaScript, kein var-vs-let-Doppel wie in modernem JavaScript. Aber: Wer die Ebenen nicht klar im Kopf hat, läuft in zwei spezifische Fallen — Shadowing (eine innere Variable verdeckt eine äußere) und Export-Sichtbarkeit (Großschreibung entscheidet, ob ein Identifier außerhalb des Pakets benutzbar ist). Dieser Artikel arbeitet alle fünf Scope-Ebenen sauber durch und macht die Stolperfallen explizit.

Die fünf Scope-Ebenen

Go ordnet jeden Identifier (Variable, Konstante, Typ, Funktion, Label, Paketname) genau einem Block zu. Blocks sind ineinander geschachtelt, und ein Identifier ist sichtbar vom Punkt seiner Deklaration bis zum Ende des einschließenden Blocks. Die Ebenen — von außen nach innen:

EbeneWer wohnt hierLebensdauer
Universe BlockPredeclared Identifiers (int, string, true, nil, len, make, panic, ...)Immer und überall
Package BlockAlle Top-Level-Deklarationen einer Source-Datei (var, const, type, func außerhalb von Funktionen)Gesamtes Paket
File BlockImportierte Paketnamen (Scope = nur die .go-Datei, die den import enthält)Diese Datei
Function BlockParameter, Result-Variablen, ReceiverGesamter Funktions-Rumpf
Local BlockAlle Variablen innerhalb von { ... }, inkl. if/for/switch-InitialisierungenBis zur schließenden }

Das ist die gesamte Liste. Es gibt keine modulweiten Geheimsphären, keine Closures mit eigener Scope-Klasse, kein „static" wie in C. Wer das im Hinterkopf hat, weiß bei jedem Identifier sofort, wo er gilt.

Universe Block — die eingebauten Identifier

Der Universe Block ist Gos „Welt": alles, was vorab existiert, lebt hier. Die Spec listet die predeclared Identifier auf — du musst sie nicht importieren, sie sind einfach da:

Go universe.go
// Typen
bool byte complex64 complex128 error float32 float64
int int8 int16 int32 int64
rune string
uint uint8 uint16 uint32 uint64 uintptr
any comparable

// Konstanten
true false iota

// Wert
nil

// Eingebaute Funktionen
append cap clear close complex copy delete imag len
make max min new panic print println real recover

Praktisch heißt das: du kannst eine eigene Variable namens int deklarieren — und damit das eingebaute int für den lokalen Block überdecken. Das ist die ultimative Shadowing-Falle. Lint-Tools wie revive und staticcheck warnen davor:

Go universe_shadow.go
package main

import "fmt"

func main() {
    int := "ich bin eine Variable" // <-- legal, aber schlecht
    fmt.Println(int)

    // var x int  // jetzt nicht mehr möglich — int ist eine string-Variable
    // staticcheck warnt: "should not use built-in type int as name for variable"
}
Output
ich bin eine Variable

Niemals predeclared Identifier überschreiben. Auch nicht len, new, error — das produziert sehr verwirrende Folgefehler.

Package Block und Export-Regel

Alles, was auf Top-Level einer Source-Datei steht (var, const, type, func außerhalb einer Funktion), landet im Package Block — sichtbar in allen Dateien desselben Pakets, unabhängig davon, in welcher .go-Datei es deklariert wurde:

Go config.go
package server

var defaultPort = 8080      // klein -> nur paket-intern
const Version = "1.0.0"     // groß -> auch außerhalb sichtbar

type Config struct {        // groß -> exportiert
    Host string             // groß -> exportiertes Feld
    port int                // klein -> nur paket-intern
}

func New() *Config { return &Config{Host: "localhost"} }  // groß -> exportiert
func validate() bool { return true }                       // klein -> nur paket-intern

Hier kommt die Go-spezifische Mechanik: Groß- oder Kleinschreibung des ersten Buchstabens entscheidet über den Export.

  • Großbuchstabe (Config, Version, Host, New) — Identifier ist exportiert. Andere Pakete können ihn als server.Config, server.Version, server.New() ansprechen.
  • Kleinbuchstabe (defaultPort, port, validate) — Identifier ist package-private. Außerhalb des Pakets nicht zugreifbar — auch nicht über Reflection (außer mit Hacks).

Diese Regel gilt auch innerhalb von Structs: ein klein geschriebenes Feld ist nur im eigenen Paket les- und schreibbar. Das ist Gos einziger Sichtbarkeits-Mechanismus — es gibt kein private/public/protected-Schlüsselwort.

File Block — Importe gelten pro Datei

Ein Detail, das viele übersehen: Importe gelten nicht paket-weit, sondern nur in der Datei, in der sie stehen. Wenn a.go import "fmt" schreibt und b.go ebenfalls fmt braucht, muss b.go den Import wiederholen. Aliase wirken nur in der Datei, in der sie definiert sind:

Go a.go
package myapp

import f "fmt"

func Hello() { f.Println("hi") }
Go b.go
package myapp

import "fmt"   // muss neu deklariert werden

// f.Println("...")  // <-- Fehler, f ist nur in a.go bekannt
func World() { fmt.Println("world") }

Das hat zwei praktische Konsequenzen:

  • goimports und IDEs verwalten Imports pro Datei. Wer einen Import alphabetisch sortiert hat, freut sich, wenn das so bleibt — und es bleibt so, weil jede Datei ihre Liste eigenständig hat.
  • Import-Aliase haben keinen paketweiten Effekt. Das ist Absicht: Niemand soll versehentlich Reibung in fremden Dateien erzeugen, indem er irgendwo einen Alias setzt.

Function Block und lokale Blocks

Innerhalb einer Funktion gibt es einen Function Block, der Parameter, Result-Variablen und Receiver einschließt. Darin schachteln sich weitere lokale Blocks — jeder { ... } ist ein Block, ebenso die impliziten Blocks von if, for, switch und ihren Klauseln:

Go local_scopes.go
package main

import "fmt"

func process(items []int) int { // Function Block — items lebt hier
    total := 0 // Function-Block-lokale Variable

    for _, item := range items { // for-Block — item lokal zu diesem Block
        temp := item * 2 // for-Body-Block — temp lokal zu jedem Iteration-Body
        total += temp

        if item > 10 { // if-Block
            big := item * 100 // if-Body-Block — big nur hier sichtbar
            fmt.Println("big:", big)
        }
        // big ist hier außerhalb nicht mehr sichtbar
    }
    // item, temp, big sind hier alle außerhalb nicht mehr sichtbar
    return total
}

func main() {
    fmt.Println(process([]int{5, 12, 3, 20}))
}
Output
big: 1200
big: 2000
4080

Ein Identifier ist sichtbar ab dem Ende seiner Deklaration bis zum Ende des Blocks. Das „ab dem Ende der Deklaration" ist subtil: in var x = x + 1 referenziert das rechte x nicht das gerade deklarierte, sondern ein eventuell äußeres — der neue Identifier ist erst „aktiv", sobald der gesamte VarSpec abgeschlossen ist.

Shadowing — wenn ein innerer Name den äußeren verdeckt

Shadowing ist Gos häufigste Scope-Falle. Sie passiert, wenn ein innerer Block einen Namen deklariert, den ein äußerer Block ebenfalls trägt:

Go shadowing.go
package main

import "fmt"

var name = "Paket-Variable"

func main() {
    fmt.Println(name) // Paket-Variable

    name := "Funktions-Variable" // shadowt die Paket-Variable
    fmt.Println(name) // Funktions-Variable

    {
        name := "Block-Variable"
        fmt.Println(name) // Block-Variable
    }

    fmt.Println(name) // Funktions-Variable — Block-Variable ist weg
}
Output
Paket-Variable
Funktions-Variable
Block-Variable
Funktions-Variable

Bewusstes Shadowing ist legitim und gelegentlich nützlich (z. B. um eine Variable in einem if-Block temporär zu „klonen"). Unbewusstes Shadowing dagegen ist eine Bug-Quelle — der häufigste Fall ist die err-Variable in geschachtelten if-Initialisierungen, der bereits im var-vs-:=-Artikel gezeigte Bug:

Go err_shadow.go
package main

import (
    "fmt"
    "strconv"
)

func main() {
    var err error
    n := 0

    if s := "42"; s != "" {
        // Achtung: := erzeugt NEUES err im if-Block
        n, err := strconv.Atoi(s)
        fmt.Println("inneres:", n, err)
        _ = n
        _ = err
    }

    // äußeres err ist immer noch nil — das innere ist weg
    fmt.Println("äußeres err:", err, "n:", n)
}
Output
inneres: 42 <nil>
äußeres err: <nil> n: 0

Hier ist sowohl das innere n als auch das innere err neu deklariert — die äußeren bleiben unverändert. Lint dagegen: go vet -vettool=$(which shadow) oder golangci-lint mit shadow-Setting im govet-Block.

Forward References — vorwärts schauen erlaubt

Anders als bei lokalen Variablen darfst du im Package Block Identifier vor ihrer Deklaration referenzieren — die Reihenfolge im File ist egal:

Go forward_refs.go
package main

import "fmt"

// Funktion benutzt Konstante, die weiter unten steht — OK
func main() {
    fmt.Println("Version:", AppVersion)
    fmt.Println(double(3))
}

// Konstante zuerst hier deklariert
const AppVersion = "1.0.0"

// Funktion die main vorher schon aufgerufen hat
func double(x int) int { return x * 2 }
Output
Version: 1.0.0
6

Das gilt strikt nur im Package Block. Innerhalb einer Funktion gilt die normale Reihenfolge — du kannst nicht auf ein n zugreifen, das du erst zehn Zeilen weiter unten deklarierst.

Closures und Scope — eine kurze Vorausschau

Funktionen sind in Go First-Class-Werte und können Closures sein: sie schließen lexikalisch ihre umgebende Scope ein. Das ist ein Scope-Mechanismus — keine Magie:

Go closures.go
package main

import "fmt"

func makeCounter() func() int {
    count := 0 // gehört zum Function Block von makeCounter
    return func() int {
        count++ // closet count ein — lebt weiter, solange die Closure lebt
        return count
    }
}

func main() {
    next := makeCounter()
    fmt.Println(next()) // 1
    fmt.Println(next()) // 2
    fmt.Println(next()) // 3

    // Jeder Aufruf von makeCounter schafft eine NEUE count-Variable
    another := makeCounter()
    fmt.Println(another()) // 1 — eigene count
    fmt.Println(next())    // 4 — alte count
}
Output
1
2
3
1
4

Wichtig zu verinnerlichen: Closures fangen Variablen, nicht Werte. Wenn eine for-Schleife eine Closure erzeugt, die i benutzt, dann zeigen alle Closures auf dieselbe i — bis Go 1.22 ein berüchtigter Anfänger-Stolperer. Ab Go 1.22 wurde das Verhalten geändert: Die for-Loop-Variable bekommt pro Iteration eine neue Bindung. Bei älterem Code (oder mit GOEXPERIMENT=loopvar deaktiviert) gilt die alte Regel — Details im Closures-Artikel.

Häufige Stolperfallen

Shadowing in if/for-Initialisierungen ist der Bug Nummer eins.

if x, err := foo(); err != nil { ... } ist sauber — die innere err ist explizit lokal. Aber x, err := foo() in einem if-Body, wenn außen schon ein x existiert, legt ein NEUES x an. Aktiviere den shadow-Analyzer in CI — er fängt diese Bugs.

Predeclared Identifier überschreiben ist legal, aber katastrophal.

len := "Hallo" ist syntaktisch erlaubt — und macht die eingebaute len-Funktion im gesamten Block unbrauchbar. staticcheck hat dafür die SA4006/SA4021-Warnungen. Niemals Variablen int, string, error, len, new, nil nennen.

Importe gelten pro Datei, nicht paket-weit.

Wenn du eine neue .go-Datei im Paket anlegst, musst du dort eigene Imports schreiben. Aliase und Dot-Imports (. "fmt") wirken nur in der Datei, in der sie stehen. Das ist auch der Grund, warum goimports auf File-Ebene operiert.

Großbuchstabe = exportiert, Kleinbuchstabe = privat — keine Ausnahmen.

Es gibt in Go keine Möglichkeit, einen klein geschriebenen Identifier irgendwie doch zu exportieren. Auch Reflection sieht private Felder nur lesend (mit Hacks). Wer ein Feld exportieren will, muss es großschreiben. Wer Helper-Funktionen verstecken will, schreibt sie klein.

Block-Scope endet streng bei der schließenden Klammer.

Eine Variable, die in einem if-Body deklariert wird, existiert hinter der schließenden } nicht mehr — nicht als nil, nicht als irgendetwas. Wer ihren Wert außerhalb braucht, deklariert sie vor dem if-Statement.

for-Loop-Variablen pre-1.22 waren shared, post-1.22 sind sie per-Iteration.

In Go ≤1.21 gab es eine i-Variable für die ganze Schleife — Closures, die i einfingen, sahen am Ende alle den letzten Wert. Ab Go 1.22 bekommt jede Iteration eine neue Bindung. Wer noch mit alten Codebases oder älteren Go-Versionen arbeitet, kennt das alte Verhalten — und sollte vorsichtshalber explizit kopieren: i := i; go func(){ ... }().

Type-Parameter haben eigenen Scope.

Bei func F[T any](x T) T { ... } ist T sichtbar vom Ende seines Namens bis zum Ende des Funktions-Rumpfes. Innerhalb der Funktion kannst du T wie jeden anderen Typ verwenden — aber außerhalb nicht. Bei Typ-Deklarationen type Stack[T any] struct { ... } gilt der Scope nur innerhalb der TypeSpec.

Der Blank Identifier hat keinen Scope.

_ ist eine Sprach-Konvention für „verwerfen", er bindet nichts. Du kannst beliebig oft _ auf der linken Seite einer Zuweisung haben — sie konflikten nicht miteinander, und du kannst _ auch nicht zurücklesen. Praktisch in Multi-Return-Funktionen (data, _ := os.ReadFile(...)) oder beim erzwingen von Side-Effect-Imports (import _ "database/sql/driver").

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Variablen & Konstanten

Zur Übersicht