Ein Pointer ist eine Variable, deren Wert die Speicheradresse einer anderen Variable ist. So weit wie in jeder C-stämmigen Sprache. Was Go anders macht: Pointer sind strikt typisiert, es gibt keine Pointer-Arithmetic, der Garbage Collector räumt auf, und der Compiler entscheidet per Escape-Analyse, ob ein Pointer-Ziel auf dem Stack oder dem Heap lebt. Du bekommst die Effizienz von „direktem Zugriff ohne Kopie" zurück, ohne die Sicherheits-Fallen von C zu erben. Dieser Artikel klärt die Definition, die Operatoren & und * im Überblick, das Speicher-Modell und die Stellen, an denen Go-Pointer absichtlich nicht so funktionieren wie ihre C-Kollegen.
Die formale Definition
Die Go-Spec definiert Pointer-Typen knapp:
PointerType = "*" BaseType .
BaseType = Type .In Prosa: Der Typ *T ist „Pointer auf T" — egal welcher Typ T ist. Aus der Spec:
A pointer type denotes the set of all pointers to variables of a given type, called the base type of the pointer. The value of an uninitialized pointer is
nil.
Drei Kernaussagen daraus, die du im Hinterkopf haben solltest:
- Pointer zeigen auf Variablen, nicht auf Werte. Nur eine Variable hat eine eindeutige Adresse. Literale, Rückgaben und nicht-adressierbare Ausdrücke haben keine — mehr dazu im Adress-Operator-Artikel.
- Pointer sind typisiert.
*intund*float64sind unterschiedliche Typen. Du kannst sie nicht implizit ineinander umwandeln. Das ist das Hauptmerkmal, das Go-Pointer von C-Pointer unterscheidet. - Zero Value ist
nil. Eine neu deklarierte Pointer-Variable ohne Initialisierung zeigt auf nichts. Sie zu dereferenzieren erzeugt einen Runtime-Panic.
Das Bild im Speicher
Bevor wir Code schreiben, kurz die Mechanik. Eine Variable belegt einen Speicherbereich; jede Speicherzelle hat eine Adresse. Ein Pointer ist nichts anderes als eine Variable, die so eine Adresse enthält.
Speicher
+---------+----------+
| Adresse | Inhalt |
+---------+----------+
| 0x1000 | 42 | <-- Variable x (int)
| 0x1008 | ... |
| 0x1010 | 0x1000 | <-- Variable p (*int) — enthält Adresse von x
+---------+----------+
x := 42 // legt 42 an irgendeine Adresse
p := &x // p enthält die Adresse von x
*p // liefert den Wert an der Adresse, also 42Die genauen Adressen interessieren dich im Alltag nie — die GC verschiebt Objekte im Heap teilweise auch zur Laufzeit, sodass die konkrete Adresse stabil bleibt, aber ihre Lage im physischen RAM sich ändern kann. Wichtig ist die Semantik: p und x zeigen auf denselben Speicher. Wer *p = 99 schreibt, ändert x.
Auf 64-Bit-Systemen ist ein Pointer 8 Byte groß — egal worauf er zeigt. *int und *[1000000]byte sind beide 8 Byte als Pointer-Variable; der Unterschied ist nur, was beim Dereferenzieren passiert.
Das erste lauffähige Beispiel
package main
import "fmt"
func main() {
x := 42 // gewöhnliche int-Variable
var p *int // Pointer-Variable, momentan nil
fmt.Println("p:", p) // <nil>
p = &x // p zeigt jetzt auf x
fmt.Println("p:", p) // Adresse, etwas wie 0xc000018088
fmt.Println("*p:", *p) // 42 — der Wert hinter dem Pointer
*p = 99 // schreibt durch den Pointer in x
fmt.Println("x:", x) // 99 — x wurde verändert
}p: <nil>
p: 0xc000018088
*p: 42
x: 99Die drei Schlüssel-Symbole — sie kriegen jeweils eigene Detail-Artikel, hier nur kurz:
*T— Typ. „Pointer auf T".*int,*Config,*[]string.&x— Operator. Liefert die Adresse einer Variable. Ausintwird*int.*p— Operator. Dereferenziert einen Pointer. Aus*intwirdint.
Der Sterne-Konflikt — * ist mal Typ-Konstruktor, mal Dereferenzierungs-Operator — ist gewöhnungsbedürftig, aber lesbar, sobald du dich an die Kontext-Regel gewöhnt hast: Im Typ-Kontext (Deklaration, Funktions-Signatur) baut * einen Pointer-Typ. Im Ausdrucks-Kontext (rechts vom =, in einer Rechenformel) dereferenziert er einen Pointer.
Pointer sind streng typisiert
Aus der Spec:
Two pointer types are identical if they have identical base types.
Konsequenz: Ein *int lässt sich nicht ohne Weiteres in ein *int32 oder *float64 umwandeln. Der Compiler verhindert solche Konvertierungen — du müsstest explizit unsafe.Pointer einschalten, was du in Produktiv-Code praktisch nie willst.
package main
func main() {
var i int = 42
var f float64
pi := &i
// FEHLER: cannot use pi (variable of type *int) as type *float64
// var pf *float64 = pi
// Auch keine implizite Umwandlung ähnlich dimensionierter Typen
// var p32 *int32 = pi // FEHLER
_ = pi
_ = f
}Das ist ein bewusster Unterschied zu C, wo void * und Casts üblich sind. Go opfert die Flexibilität für Sicherheit: dein Compiler weiß bei jeder Pointer-Operation, welcher Typ herauskommt, und kann passende Speicher-Layouts und Methoden-Dispatch garantieren.
Eigene Pointer-Typen über Defined Types. Wer wirklich einen „neuen" Pointer-Typ braucht, definiert ihn explizit:
package main
// Ein eigener Typ — distinct von *int
type Handle *int
func use(h Handle) {
if h != nil {
println(*h)
}
}Handle und *int haben denselben Underlying-Typ, sind aber als benannte Typen unterscheidbar. In der Praxis macht man das selten — bei Stdlib-unsafe-Tricks und manchen C-Interop-Wrappern sieht man es.
Was Go-Pointer NICHT können
Drei Mechaniken, die du in C kennst und die Go bewusst weglässt:
| Fähigkeit aus C | In Go |
|---|---|
Pointer-Arithmetic (p + 1, p++) | Nicht erlaubt. Kein Compile, keine Hintertür ohne unsafe. |
Cast zwischen beliebigen Pointer-Typen ((int *)x) | Nicht erlaubt ohne unsafe.Pointer. |
Manuelles malloc/free | Nicht nötig. GC räumt auf, wenn niemand mehr referenziert. |
void * als generischer Container | Es gibt unsafe.Pointer, aber Idiom ist interface{} / any. |
| Dangling Pointers (Use-after-free) | Praktisch unmöglich. Solange ein Pointer existiert, lebt das Ziel. |
Die Spec lässt die Beschränkungen implizit, weil die fehlenden Operationen schlicht nicht in der Grammatik vorkommen. p+1 ergibt bei einem Pointer einen Compile-Fehler:
package main
func main() {
arr := [3]int{10, 20, 30}
p := &arr[0]
// FEHLER: invalid operation: p + 1 (mismatched types *int and untyped int)
// q := p + 1
// Korrekt: Index am Array benutzen
q := &arr[1]
println(*q) // 20
}Wer durch ein Array iterieren will, nutzt range oder einen Slice — nicht Pointer-Arithmetic. Das spart eine ganze Klasse von Bugs (Buffer Overruns, Off-by-one im Pointer-Schritt) und ist nebenbei lesbarer.
Garbage Collection — ohne manuelle Freigabe
In C musst du jedem malloc ein free gegenüberstellen, sonst lekst du Speicher; oder du gibst zu früh frei und produzierst Dangling Pointers. Beides Quellen für Security-Bugs.
Go geht den anderen Weg: solange irgendeine Variable einen Pointer auf das Objekt hält (egal wie indirekt), bleibt das Objekt am Leben. Sobald niemand mehr darauf zeigt, räumt der Garbage Collector irgendwann auf. Es gibt keine free-Funktion, und du darfst dir keine ausdenken.
package main
import "fmt"
type Config struct {
Host string
Port int
}
func newConfig() *Config {
return &Config{Host: "localhost", Port: 8080}
// Das &Config{...} darf den Funktions-Frame überleben —
// der Compiler erkennt das (Escape-Analyse) und allokiert auf dem Heap.
}
func main() {
c := newConfig()
fmt.Println(c.Host, c.Port)
// c hält die einzige Referenz. Wenn main endet,
// wird das Config-Objekt für die GC freigegeben.
}localhost 8080In C wäre return &Config{...} ein Klassiker für Use-after-free, weil der Stack-Frame mit dem Funktions-Return zerstört wird. Der Go-Compiler bemerkt, dass der Pointer den Frame verlässt („escaped"), und legt das Objekt deswegen auf dem Heap an. Die Mechanik kannst du dir mit go build -gcflags="-m" anzeigen lassen — sie steht detailliert im Stack-vs-Heap-Bereich der Pointer-Übersicht.
Pointer-Vergleich
Pointer sind mit == und != vergleichbar — entweder mit nil oder untereinander. Aus der Spec:
Pointer values are comparable. Two pointer values are equal if they point to the same variable or if both have value
nil.
package main
import "fmt"
func main() {
a, b := 7, 7
p1 := &a
p2 := &a
p3 := &b
fmt.Println(p1 == p2) // true — gleiche Variable
fmt.Println(p1 == p3) // false — verschiedene Variablen, gleicher Wert
fmt.Println(p1 == nil) // false
var pn *int
fmt.Println(pn == nil) // true
}true
false
false
trueWichtige Unterscheidung: p1 == p3 vergleicht Adressen, nicht Werte. Auch wenn *p1 == *p3 (beide enthalten 7), sind die Pointer ungleich. Für Wertvergleich musst du dereferenzieren.
<, <=, >, >= sind auf Pointern nicht definiert. Eine Sortierung von Pointer-Werten als Adress-Reihenfolge ist nicht portabel und ergibt sowieso selten Sinn — wer sortieren will, sortiert die referenzierten Werte.
Pointer als Variablen — die zwei Erzeugungs-Wege
Es gibt zwei idiomatische Wege, einen Pointer in Go zu bekommen: per &-Operator auf einer existierenden Variable oder per new(T)-Built-in:
package main
import "fmt"
type Counter struct{ N int }
func main() {
// (1) Adress-Operator auf bestehender Variable
c1 := Counter{N: 0}
p1 := &c1
// (2) Adress-Operator auf Composite Literal (häufigster Idiom)
p2 := &Counter{N: 0}
// (3) new(T) — selten benutzt, weil &T{} ausdrucksstärker ist
p3 := new(Counter)
// p3 zeigt auf eine Counter-Instanz mit Zero Values (N == 0)
fmt.Printf("%+v %+v %+v\n", *p1, *p2, *p3)
}{N:0} {N:0} {N:0}new(T) und &T{} machen praktisch dasselbe — sie allokieren eine T-Instanz mit Zero Values und geben einen Pointer darauf zurück. In der Praxis siehst du fast immer &T{...} mit Composite Literal, weil du dort die Initial-Werte gleich mitgibst. new(T) ist primär in generischem Code nützlich, wenn du nicht weißt, ob T ein Composite-Literal-Syntax hat.
unsafe.Pointer — die ausdrückliche Hintertür
Für die seltenen Fälle, in denen du tatsächlich zwischen unterschiedlichen Pointer-Typen umwandeln musst (typischerweise: C-Interop, Reflection, manuelle Memory-Layout-Tricks), existiert unsafe.Pointer. Sie ist void *-artig, gehört aber bewusst in das Paket unsafe:
package main
import (
"fmt"
"unsafe"
)
func main() {
var x int64 = 0x4142434445464748
// *int64 -> unsafe.Pointer -> *byte
// Liest das erste Byte als Byte-Wert
p := (*byte)(unsafe.Pointer(&x))
fmt.Printf("%x\n", *p)
}48Der Output hängt von der Endianness des Systems ab — auf little-endian Intel/AMD ist das niedrigste Byte zuerst. Das demonstriert genau, warum unsafe.Pointer Plattform-abhängig wird und in normalem Code nichts zu suchen hat.
Drei harte Regeln, falls du unsafe.Pointer doch brauchst (Stdlib-Quelle, runtime, hochoptimierte Libraries):
- Die Konvertierungen sind legal, aber nicht garantiert portabel.
- Pointer-Arithmetic auf
unsafe.Pointergeht nur viauintptr— und ist eine eigene Falle, weiluintptrkein Pointer ist und der GC ihn nicht trackt. - Was im
unsafe-Paket lebt, kann sich zwischen Go-Versionen ändern. Stabil ist es nur in den dokumentierten Mustern.
Im Alltag: weglassen.
Häufige Stolperfallen
Pointer und nicht-adressierbare Werte verwechseln.
Nicht alles hat eine Adresse. &"hallo" ist Fehler — String-Literale sind keine Variablen. &m[key] ist Fehler — Map-Elemente können sich beim Resizing verschieben. &f() ist Fehler — Funktions-Returns sind nicht adressierbar. Die genauen Regeln stehen im Adress-Operator-Artikel.
Zero Value eines Pointers ist nil, Dereferenzierung paniciert.
var p *Config; p.Host paniciert mit runtime error: invalid memory address or nil pointer dereference. Wer einen Pointer als Rückgabewert behandelt, prüft vor Zugriff: if p == nil { return ... }. Mehr im nil-Pointer-Artikel.
*T in der Deklaration ist Typ-Konstruktor, *p im Ausdruck ist Dereferenzierung.
Visuell gleich, semantisch unterschiedlich. var p *int macht p zum Pointer-Typ; x := *p liest den Wert an der Adresse, die p enthält. Der Kontext (links/rechts vom =) entscheidet, was * bedeutet.
Pointer-Vergleich vergleicht Adressen, nicht Werte.
p == q ist true nur, wenn beide auf dieselbe Variable zeigen oder beide nil sind. Auch wenn die referenzierten Werte gleich sind (*p == *q), können die Pointer ungleich sein. Wer Werte vergleichen will, dereferenziert vorher.
Keine Pointer-Arithmetic — auch nicht über Umwege.
p + 1, p++, p[1] (Index auf Pointer) sind Compile-Fehler. Wer durch Speicher iterieren will, nutzt Slices oder Arrays mit Index-Zugriff. Die einzige Ausnahme — unsafe.Pointer mit uintptr-Arithmetic — gehört in Stdlib- und C-Interop-Code, nicht in Anwendungs-Code.
new(T) vs &T{} — fast immer ist &T{} die richtige Wahl.
new(T) gibt *T mit Zero Values zurück. &T{...} macht dasselbe und erlaubt zusätzlich, Felder direkt zu initialisieren. Lesbarer und expressiver. Reserviere new für generische Funktionen, in denen du das Composite-Literal nicht hinschreiben kannst.
Pointer auf Map- oder Channel-Werte: meistens unnötig.
Map und Channel sind selbst schon Referenz-Typen — sie tragen intern einen Header, der auf die eigentlichen Daten zeigt. *map[string]int und *chan int sind in 99 % der Fälle ein Code-Smell. Die Übergabe per Wert ist günstig und reicht.
unsafe.Pointer ist keine Hintertür für Performance-Tricks.
Wer unsafe.Pointer in Anwendungs-Code findet, sollte es als Warnsignal werten. Stdlib nutzt es bewusst und kontrolliert — der Rest der Welt baut sich damit Portabilitäts-, Versions- und Memory-Probleme. Im Zweifel: weglassen.
GC reicht — manuelle Freigabe gibt es nicht.
Es gibt kein free(), kein delete auf Pointer-Objekten. Die GC räumt auf, sobald keine Referenz mehr existiert. Wer dem GC helfen will, setzt selten gebrauchte Pointer manuell auf nil, damit die referenzierten Objekte früher freigegeben werden können — aber das ist eine seltene Optimierung.
Weiterführende Ressourcen
Externe Quellen
- Pointer types – Go Language Specification
- Address operators – Go Specification
- Allocation with
new– Go Specification - Effective Go: Allocation with new
unsafe.Pointer– pkg.go.dev