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:

EBNF PointerType (Go-Spec)
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. *int und *float64 sind 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.

ASCII speicher-modell
        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 42

Die 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

Go basics.go
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
}
Output
p: <nil>
p: 0xc000018088
*p: 42
x: 99

Die drei Schlüssel-Symbole — sie kriegen jeweils eigene Detail-Artikel, hier nur kurz:

  • *TTyp. „Pointer auf T". *int, *Config, *[]string.
  • &xOperator. Liefert die Adresse einer Variable. Aus int wird *int.
  • *pOperator. Dereferenziert einen Pointer. Aus *int wird int.

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.

Go typ-strict.go
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:

Go defined-pointer.go
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 CIn 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/freeNicht nötig. GC räumt auf, wenn niemand mehr referenziert.
void * als generischer ContainerEs 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:

Go no-arithmetic.go
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.

Go no-manual-free.go
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.
}
Output
localhost 8080

In 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.

Go compare.go
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
}
Output
true
false
false
true

Wichtige 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:

Go erzeugen.go
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)
}
Output
{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:

Go unsafe-pointer.go
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)
}
Output
48

Der 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.Pointer geht nur via uintptr — und ist eine eigene Falle, weil uintptr kein 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

/ Weiter

Zurück zu Pointer

Zur Übersicht