Pointer-Typen sind in Go nur die halbe Miete — sie existieren als Konzept, aber ohne zwei Operatoren wären sie wertlos. & holt aus einer Variable ihre Adresse heraus und liefert dir einen typisierten Pointer. * geht den Weg zurück: aus einem Pointer wird der Wert, auf den er zeigt. Beide klingen trivial, sind es aber nicht — denn nicht jeder Ausdruck in Go hat eine Adresse. Die Spec definiert präzise, was addressierbar ist, und diese Regel entscheidet, ob &... legal ist oder als Compile-Fehler abgewiesen wird. Map-Werte, String-Bytes, Funktions-Returns, Konstanten — sie alle haben aus guten Gründen keine. Dazu kommt das Zusammenspiel mit Composite Literals (&Config{...}), die automatische Adress-Bildung bei Methoden-Calls und die automatische Dereferenzierung bei Field-Access. Dieser Artikel arbeitet die Mechanik formal durch und macht die Stolperfallen explizit, an denen Anfänger und Fortgeschrittene gleichermaßen hängenbleiben.

Der &-Operator — die Adresse einer Variable

Der Adress-Operator nimmt einen Ausdruck und gibt einen Pointer auf den Speicher zurück, in dem dieser Ausdruck lebt. Aus der Spec:

EBNF Address operator (Go-Spec)
UnaryExpr   = PrimaryExpr | unary_op UnaryExpr .
unary_op    = "+" | "-" | "!" | "^" | "*" | "&" | "<-" .

Die Prosa der Spec dazu:

For an operand x of type T, the address operation &x generates a pointer of type *T to x. The operand must be addressable, that is, either a variable, pointer indirection, or slice indexing operation; or a field selector of an addressable struct operand; or an array indexing operation of an addressable array. As an exception to the addressability requirement, x may also be a (possibly parenthesized) composite literal.

Drei Aussagen daraus, die du verinnerlichen solltest:

  • Aus T macht & ein *T. Der Typ des Ergebnisses ist immer der Pointer-Typ zum Operanden-Typ. Aus int wird *int, aus Config wird *Config, aus []string wird *[]string.
  • Der Operand muss adressierbar sein. Das ist die zentrale Beschränkung — nicht jeder Ausdruck hat eine Adresse, die du nehmen darfst. Was genau zählt, klärt der nächste Abschnitt.
  • Composite Literals sind eine ausdrückliche Ausnahme. &Config{Host: "x"} ist erlaubt, obwohl der Composite-Literal selbst keine Variable ist. Die Spec erlaubt es explizit und allokiert dabei eine neue Variable.

Addressability — was du adressieren darfst

Die Spec listet abschließend, welche Ausdrücke adressierbar sind. Eine kompakte Übersicht:

AusdruckBeispielAdressierbar?
Variablex (egal ob lokal, Paket-global, Parameter)Ja
Pointer-Dereferenz*pJa
Slice-Indexs[i] (für Slice s)Ja
Array-Index auf adressierbarem Arraya[i] (wenn a selbst adressierbar)Ja
Feld einer adressierbaren Structs.field (wenn s adressierbar)Ja
Composite Literal (Sonderfall)Config{...} in &Config{...}Ja (per Ausnahme)
Map-Indexm[key]Nein
String-Indexs[i] (für String s)Nein
Funktions-Returnf()Nein
Methoden-Returnobj.M()Nein
Konstanteconst x = 5; &xNein
Typ-Literal&"hallo", &42, &trueNein

Der konkrete Lauf-Test:

Go addressable.go
package main

import "fmt"

type Config struct{ Host string }

func makeConfig() Config { return Config{Host: "x"} }

func main() {
    // (1) Variable — addressierbar
    x := 42
    _ = &x

    // (2) Slice-Element — addressierbar
    sl := []int{1, 2, 3}
    _ = &sl[0]

    // (3) Feld einer adressierbaren Struct
    c := Config{Host: "localhost"}
    _ = &c.Host

    // (4) Array-Index auf adressierbarem Array
    var arr [3]int
    _ = &arr[1]

    // (5) Pointer-Dereferenz
    p := &x
    _ = &*p   // legal — *p ist eine Variable

    fmt.Println("alle adressierbar")

    // GEGENBEISPIELE — alle Compile-Fehler:
    // m := map[string]int{"a": 1}
    // _ = &m["a"]            // cannot take the address of m["a"]
    // s := "hallo"
    // _ = &s[0]              // cannot take the address of s[0]
    // _ = &makeConfig()      // cannot take the address of makeConfig()
    // _ = &Config{}.Host     // OK — Composite-Literal ist Sonderfall
    // const k = 5
    // _ = &k                 // cannot take the address of k
}
Output
alle adressierbar

Der Trick: Adressierbarkeit ist strukturell. Eine Struct ist adressierbar, wenn die Variable, die sie hält, adressierbar ist. Ein Array-Index ist adressierbar, wenn das Array selbst adressierbar ist. Bei Slices ist es anders — ein Slice-Index ist immer adressierbar, weil ein Slice intern bereits ein Pointer-Header auf ein Backing-Array ist; das Element lebt auf dem Heap.

Was NICHT adressierbar ist — und warum

Die spannenden Fälle sind die Verbote. Jedes hat einen technischen Grund, der mit der Implementation des Typsystems oder der Runtime zusammenhängt.

Map-Werte. Eine Map ist intern eine Hash-Tabelle mit Buckets. Wenn die Tabelle wächst (Load Factor erreicht), wird sie rehasht — die Werte landen in neuen Buckets, an neuen Adressen. Ein Pointer, den du vor dem Rehash auf einen Map-Wert genommen hättest, würde ins Leere zeigen. Die Spec verbietet &m[key] deshalb kategorisch:

Go map-no-address.go
package main

type User struct{ Name string }

func main() {
    users := map[string]User{"a": {Name: "Alice"}}

    // FEHLER: cannot take the address of users["a"]
    // p := &users["a"]

    // Workaround 1: Wert kopieren, dann Pointer auf die Kopie
    u := users["a"]
    _ = &u

    // Workaround 2: Map auf Pointer-Werte umstellen
    ptrMap := map[string]*User{"a": {Name: "Alice"}}
    p := ptrMap["a"]   // p ist *User — direkt nutzbar
    _ = p
}

String-Bytes. Strings in Go sind immutable — der Compiler darf identische String-Literale zu einer einzigen Speicher-Region zusammenfassen, und einige Strings leben im read-only-Daten-Segment der Binary. Würdest du &s[0] nehmen dürfen, wäre ein Schreiben durch den Pointer ein Verstoß gegen die Immutability. Anders bei []byte: dort ist das Index-Ergebnis adressierbar, weil Slices nun mal modifizierbar sind:

Go string-vs-bytes.go
package main

func main() {
    s := "hallo"
    // FEHLER: cannot take the address of s[0]
    // _ = &s[0]

    b := []byte("hallo")
    p := &b[0]      // OK — Slice-Element ist adressierbar
    *p = 'H'        // mutiert b zu "Hallo"
    _ = p
}

Funktions- und Methoden-Returns. Ein Funktions-Aufruf liefert einen Wert zurück, aber dieser Wert lebt nicht in einer benannten Variable — er liegt in einem temporären Register oder Stack-Slot, der nach dem Call verschwindet. Eine Adresse hätte keinen stabilen Halt:

Go call-no-address.go
package main

func makeConfig() Config { return Config{Host: "x"} }

type Config struct{ Host string }

func main() {
    // FEHLER: cannot take the address of makeConfig()
    // p := &makeConfig()

    // Workaround: in Variable zwischenparken
    c := makeConfig()
    p := &c
    _ = p

    // Häufiger Idiom — Funktion gibt direkt Pointer zurück
    // (siehe Stdlib-Stil: os.Open, http.NewRequest, ...)
}

Die elegante Lösung ist meistens, die Funktion direkt einen Pointer zurückgeben zu lassen — func makeConfig() *Config { return &Config{...} } ist idiomatisch und vermeidet die Zwischen-Variable.

String-Literale, Numbers, Konstanten. Ein Literal hat keinen Speicher, der ihm exklusiv gehört — der Compiler darf 42 an Hunderten von Stellen im Code als denselben Wert behandeln. Konstanten sind per Definition nicht-veränderlich; eine Adresse zu nehmen würde suggerieren, du könntest sie durch *p = ... ändern, was widersinnig wäre.

Go literal-no-address.go
package main

const Pi = 3.14159

func main() {
    // Alle Compile-Fehler:
    // _ = &"hallo"
    // _ = &42
    // _ = &true
    // _ = &Pi

    // Workaround: erst in Variable
    s := "hallo"
    _ = &s
}

Der *-Operator — Dereferenzierung

Der Stern als unärer Operator vor einem Pointer-Ausdruck liefert den Wert, auf den der Pointer zeigt. Aus der Spec:

For an operand x of pointer type *T, the pointer indirection *x denotes the variable of type T pointed to by x. If x is nil, an attempt to evaluate *x will cause a run-time panic.

Beachte die genaue Wortwahl: *x ist eine Variable, nicht nur ein Wert. Das ist der Grund, warum *p adressierbar ist und &*p einen gültigen Pointer ergibt.

Go deref.go
package main

import "fmt"

func main() {
    x := 42
    p := &x       // p hat Typ *int, zeigt auf x
    v := *p       // v hat Typ int, kopiert den Wert: v == 42

    *p = 99       // schreibt durch den Pointer in x
    fmt.Println(x, v)   // 99 42 — v ist eine Kopie, x ist verändert

    // nil-Dereferenzierung — Runtime-Panic
    var nullPtr *int
    // _ = *nullPtr   // panic: runtime error: invalid memory address or nil pointer dereference
    _ = nullPtr
}
Output
99 42

Zwei Punkte, die hier häufig untergehen:

  • Dereferenzierung liefert eine Kopie, außer du schreibst durch sie. v := *p legt einen neuen int an. *p = 99 schreibt direkt durch den Pointer.
  • nil zu dereferenzieren ist ein Runtime-Panic, kein Compile-Fehler. Der Compiler kann statisch nicht wissen, ob ein Pointer zur Laufzeit nil ist — die Prüfung muss zur Laufzeit passieren. Mehr dazu im nil-Pointer-Artikel.

Composite Literals — der &T{...}-Idiom

Die Spec macht für Composite Literals eine ausdrückliche Ausnahme von der Addressability-Regel:

As an exception to the addressability requirement, x may also be a (possibly parenthesized) composite literal. Taking the address of a composite literal generates a pointer to a unique variable initialized with the literal's value.

Diese Klausel ist die Grundlage für den meistgenutzten Pointer-Erzeugungs-Pattern in Go-Code. Statt erst eine Variable anzulegen und dann ihre Adresse zu nehmen, schreibst du beides in einem Schritt:

Go composite-address.go
package main

import "fmt"

type Server struct {
    Host string
    Port int
}

func main() {
    // (1) Klassisch: Variable, dann Adresse
    s1 := Server{Host: "localhost", Port: 8080}
    p1 := &s1

    // (2) Idiomatisch: Adresse direkt vom Composite Literal
    p2 := &Server{Host: "localhost", Port: 8080}

    // (3) Auch verschachtelt erlaubt
    servers := []*Server{
        {Host: "a", Port: 80},   // hier wird &Server{...} implizit angewandt
        {Host: "b", Port: 443},
    }

    fmt.Printf("%+v %+v %+v\n", *p1, *p2, *servers[0])
}
Output
{Host:localhost Port:8080} {Host:localhost Port:8080} {Host:a Port:80}

Was passiert technisch in (2): Der Compiler legt eine anonyme Variable an, initialisiert sie mit den Werten aus dem Composite Literal und gibt einen Pointer darauf zurück. Die Variable hat keinen Namen, aber sie existiert — die Escape-Analyse entscheidet, ob sie auf Stack oder Heap landet. Bei Pointer-Return aus einer Funktion ist das praktisch immer Heap.

Diese Ausnahme erspart einen ganzen Teil der Boilerplate, die du in C oder Java schreiben müsstest. new(Server) zusammen mit Server{} plus Feld-Zuweisungen ersetzt sich durch genau ein Statement: &Server{Host: "x", Port: 80}.

Symmetrie — *&x == x und &*p == p

& und * sind formal Inverse. Wer den einen rückgängig macht, indem er den anderen anwendet, landet beim Ausgangspunkt — semantisch wie auch identitäts-mäßig:

Go symmetry.go
package main

import "fmt"

func main() {
    x := 42

    // *&x liefert wieder x — sowohl Wert als auch Identität
    fmt.Println(*&x == x)   // true
    fmt.Println(*&x)        // 42

    p := &x

    // &*p liefert wieder p — gleiche Adresse, gleicher Pointer
    fmt.Println(&*p == p)   // true
    fmt.Println(&*p)        // gleiche Adresse wie p

    // Modifikation per *&x wirkt auf x
    *&x = 99
    fmt.Println(x)          // 99
}
Output
true
42
true
0xc000018088
99

Die genaue Adresse hängt vom Lauf ab — die Identität (&*p == p) gilt aber immer. In der Praxis schreibt man *&x oder &*p praktisch nie — sie sind Identitäts-Operationen. Aber die Gleichheit zu kennen hilft, beim Lesen komplexer Pointer-Ausdrücke nicht den Faden zu verlieren.

Auto-Adresse bei Method-Calls

Hier wird es subtil. Wenn eine Methode einen Pointer-Receiver hat, müsste man sie eigentlich auf einem Pointer aufrufen. Go erlaubt aber, sie auch auf einem adressierbaren Wert aufzurufen — der Compiler nimmt dann automatisch die Adresse:

Go method-auto-address.go
package main

import "fmt"

type Counter struct{ N int }

// Pointer-Receiver — kann den Counter mutieren
func (c *Counter) Inc() {
    c.N++
}

func main() {
    c := Counter{N: 0}
    c.Inc()         // Kurzform für (&c).Inc() — Auto-Adresse
    c.Inc()
    fmt.Println(c.N)  // 2 — c wurde mutiert

    // Funktioniert auch direkt auf Pointer
    p := &c
    p.Inc()         // direkter Pointer-Call
    fmt.Println(c.N)  // 3
}
Output
2
3

Die wichtige Einschränkung: Auto-Adresse funktioniert nur, wenn der Receiver adressierbar ist. Bei nicht-adressierbaren Werten (Map-Element, Funktions-Return) ist der Call mit Pointer-Methode ein Compile-Fehler:

Go method-not-addressable.go
package main

type Counter struct{ N int }

func (c *Counter) Inc() { c.N++ }

func makeCounter() Counter { return Counter{} }

func main() {
    m := map[string]Counter{"a": {N: 0}}

    // FEHLER: cannot call pointer method Inc on Counter
    // m["a"].Inc()

    // FEHLER: cannot take the address of makeCounter()
    // makeCounter().Inc()

    // Workaround: über Variable
    c := m["a"]
    c.Inc()
    m["a"] = c

    // Oder: Map auf *Counter umstellen
    mp := map[string]*Counter{"a": {N: 0}}
    mp["a"].Inc()   // OK — der Map-Wert IST ein Pointer
}

Die Fehler-Meldung des Compilers ist hier eindeutig — er erkennt die Adressability-Verletzung und nennt den genauen Grund. Wer solche Fehler liest, sollte sofort an die Liste der nicht-adressierbaren Ausdrücke aus Abschnitt 2 denken.

Auto-Dereferenzierung bei Field-Access

Die Gegenrichtung ist genauso komfortabel: Wer auf das Feld eines Pointers zugreift, muss nicht erst (*p).Field schreiben — Go dereferenziert automatisch. Aus der Spec, im Selector-Abschnitt:

For a value x of type T or *T where T is not a pointer or interface type, x.f denotes the field or method at the shallowest depth in T where there is such an f.

Konkret heißt das, sowohl c.N als auch (*p).N funktionieren — und für Methoden-Calls gilt dasselbe:

Go auto-deref.go
package main

import "fmt"

type Counter struct{ N int }

func (c *Counter) Show() {
    fmt.Println("N =", c.N)
}

func main() {
    c := Counter{N: 7}
    p := &c

    // Field-Access mit Auto-Dereferenzierung
    fmt.Println(p.N)        // 7 — Kurzform für (*p).N
    fmt.Println((*p).N)     // 7 — explizit, semantisch identisch

    // Method-Call ebenso
    p.Show()                // ruft (*p).Show() bzw. die Pointer-Method direkt
}
Output
7
7
N = 7

Diese Mechanik gilt nicht für interface-Pointer und nicht für Pointer auf Pointer (**T). Bei **T musst du explizit dereferenzieren: (**pp).Field oder zuerst p := *pp; p.Field.

Auto-Dereferenzierung erstreckt sich auf alle Field-Zugriffe und Method-Calls. Was sie nicht macht: explizite Operationen wie Adress-Bildung oder Vergleich. &p.N ist legal (Adresse des Feldes, durch automatische Dereferenzierung), aber *p == *q musst du explizit schreiben, wenn du die Werte vergleichen willst — p == q vergleicht die Adressen.

Mehrfach-Indirection — **T und höher

Pointer auf Pointer kommen in idiomatischem Go selten vor, sind aber technisch zulässig. Der häufigste Fall: eine Funktion soll eine Pointer-Variable im Aufrufer auf einen anderen Pointer umsetzen — also nicht den Wert hinter dem Pointer ändern, sondern den Pointer selbst:

Go double-pointer.go
package main

import "fmt"

type Node struct {
    Value int
    Next  *Node
}

// pp ist *(*Node) — kann die Pointer-Variable selbst umsetzen
func prepend(pp **Node, value int) {
    *pp = &Node{Value: value, Next: *pp}
}

func main() {
    var head *Node      // nil
    prepend(&head, 1)   // head zeigt jetzt auf {1, nil}
    prepend(&head, 2)   // head zeigt jetzt auf {2, {1, nil}}
    prepend(&head, 3)   // head zeigt jetzt auf {3, {2, {1, nil}}}

    for n := head; n != nil; n = n.Next {
        fmt.Println(n.Value)
    }
}
Output
3
2
1

**Node heißt: „Pointer auf Pointer auf Node". *pp liefert die Pointer-Variable selbst (also *Node), *pp = ... setzt diese Pointer-Variable um. In typischem Go-Code löst man das eher mit einem Receiver-Wrapper-Typ (type List struct{ head *Node } mit einer Prepend-Methode) — das ist lesbarer und idiomatischer. **T siehst du primär in low-level Code, bei C-Interop oder bei Linked-List-Implementierungen aus Lehrbüchern.

Höhere Indirection (***T, ****T) ist legal, aber praktisch nie sinnvoll. Wer sie nutzt, hat fast immer ein Design-Problem.

Häufige Stolperfallen

&m[key] — Map-Werte sind nicht adressierbar.

Maps können beim Wachsen rehasht werden — alte Adressen wären dann ungültig. Der Compiler verbietet die Adress-Bildung deshalb mit cannot take the address of m[key]. Workaround: Wert in eine Variable kopieren, oder Map auf map[K]*V umstellen.

&s[i] bei Strings — verboten, anders als bei []byte.

Strings sind immutable; ein adressierbares Byte wäre eine Lücke in dieser Garantie. Wer einzelne Bytes mutieren will, konvertiert mit b := []byte(s) — Slice-Elemente sind adressierbar. Bei reinem Lesen reicht s[i] ohne Adresse.

&f() — Funktions-Returns nicht adressierbar.

Ein Return-Wert lebt nicht in einer Variable, sondern in einem temporären Slot. Workaround: in eine Variable schreiben (x := f(); p := &x) oder die Funktion direkt einen Pointer zurückgeben lassen. Der zweite Weg ist meistens idiomatischer — siehe os.Open, http.NewRequest.

&"literal" — String-Literale, Konstanten und Number-Literale haben keine Adresse.

Literale sind keine Variablen, sie haben keinen stabilen Speicher. &42, &"hallo", &true, &Pi (mit const Pi = ...) sind alle Compile-Fehler. Wer einen Pointer auf einen konstanten Wert braucht, kopiert ihn vorher in eine Variable.

Auto-Adresse bei Method-Call — nur wenn der Receiver adressierbar ist.

c.Inc() mit Pointer-Receiver-Methode funktioniert nur, wenn c adressierbar ist. m[key].Inc() und f().Inc() sind Compile-Fehler, weil Map-Wert und Funktions-Return nicht adressierbar sind. Die Fehler-Meldung des Compilers nennt den genauen Grund.

m[key].Field = ... — direkt schreiben in Map-Werte ist ein Fehler.

Maps liefern beim Index-Zugriff eine Kopie des Wertes, keine Adresse. m[key].Field = x ist deshalb Compile-Fehler — cannot assign to struct field m[key].Field in map. Korrekt: Wert auspacken, modifizieren, wieder in die Map schreiben — oder die Map auf Pointer-Werte umstellen. Mehr im Map-Artikel.

nil-Pointer dereferenzieren — Runtime-Panic, kein Compile-Fehler.

var p *int; _ = *p paniciert zur Laufzeit mit runtime error: invalid memory address or nil pointer dereference. Der Compiler kann nicht statisch wissen, ob ein Pointer zur Laufzeit nil ist. Vor riskanten Dereferenzierungen explizit prüfen: if p != nil { ... }. Details im nil-Pointer-Artikel.

* im Typ-Kontext vs. im Ausdrucks-Kontext.

Visuell identisch, semantisch verschieden. var p *int macht aus int den Pointer-Typ *int — das ist Typ-Konstruktor. x := *p liest den Wert hinter der Adresse — das ist Dereferenzierungs-Operator. Der Kontext (Deklaration vs. Ausdruck) entscheidet, welche Bedeutung greift. Gleich gilt für & — Adress-Operator im Ausdruck, aber bitwise-AND in Rechenformeln.

& ist auch Bitwise AND — der Kontext unterscheidet.

a & b zwischen zwei int-Werten ist Bitwise AND. &x mit einer einzelnen Variable ist Adress-Operator. Der Compiler entscheidet anhand der Operanden-Anzahl und Typen — kein Konflikt, aber gewöhnungsbedürftig beim Lesen. Bei Verwirrung hilft die Faustregel: unär = Adresse, binär = Bitwise.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Pointer

Zur Übersicht