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:
UnaryExpr = PrimaryExpr | unary_op UnaryExpr .
unary_op = "+" | "-" | "!" | "^" | "*" | "&" | "<-" .Die Prosa der Spec dazu:
For an operand
xof typeT, the address operation&xgenerates a pointer of type*Ttox. 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,xmay also be a (possibly parenthesized) composite literal.
Drei Aussagen daraus, die du verinnerlichen solltest:
- Aus
Tmacht&ein*T. Der Typ des Ergebnisses ist immer der Pointer-Typ zum Operanden-Typ. Ausintwird*int, ausConfigwird*Config, aus[]stringwird*[]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:
| Ausdruck | Beispiel | Adressierbar? |
|---|---|---|
| Variable | x (egal ob lokal, Paket-global, Parameter) | Ja |
| Pointer-Dereferenz | *p | Ja |
| Slice-Index | s[i] (für Slice s) | Ja |
| Array-Index auf adressierbarem Array | a[i] (wenn a selbst adressierbar) | Ja |
| Feld einer adressierbaren Struct | s.field (wenn s adressierbar) | Ja |
| Composite Literal (Sonderfall) | Config{...} in &Config{...} | Ja (per Ausnahme) |
| Map-Index | m[key] | Nein |
| String-Index | s[i] (für String s) | Nein |
| Funktions-Return | f() | Nein |
| Methoden-Return | obj.M() | Nein |
| Konstante | const x = 5; &x | Nein |
| Typ-Literal | &"hallo", &42, &true | Nein |
Der konkrete Lauf-Test:
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
}alle adressierbarDer 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:
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:
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:
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.
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
xof pointer type*T, the pointer indirection*xdenotes the variable of typeTpointed to byx. Ifxis nil, an attempt to evaluate*xwill 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.
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
}99 42Zwei Punkte, die hier häufig untergehen:
- Dereferenzierung liefert eine Kopie, außer du schreibst durch sie.
v := *plegt einen neuenintan.*p = 99schreibt direkt durch den Pointer. nilzu 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,
xmay 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:
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])
}{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:
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
}true
42
true
0xc000018088
99Die 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:
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
}2
3Die 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:
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
xof typeTor*TwhereTis not a pointer or interface type,x.fdenotes the field or method at the shallowest depth inTwhere there is such anf.
Konkret heißt das, sowohl c.N als auch (*p).N funktionieren — und für Methoden-Calls gilt dasselbe:
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
}7
7
N = 7Diese 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:
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)
}
}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
- Address operators – Go Language Specification
- Composite literals – Go Specification
- Selectors – Go Specification (Auto-Dereferenzierung)
- Calls – Go Specification (Method-Calls und Receivers)
- Method sets – Go Specification
- Effective Go: Pointers vs. Values
Verwandte Artikel
- Pointer – Übersicht und didaktische Einführung
- Pointer-Grundlagen – Typ, Semantik, Speichermodell
- Pointer vs. Wert – wann welche Semantik
- nil-Pointer – Zero Value, Defensive Programming
- Pointer als Parameter – Mutation, Performance, Idiomatik
- Map – warum Map-Werte nicht adressierbar sind
- String – Immutability und Byte-Zugriff
- Structs – Composite Literals und Field-Selektoren