Go kennt zwei Wege, eine Variable anzulegen: var und make. Wer den falschen wählt, handelt sich schnell typische Anfängerfehler ein. Der Klassiker ist eine Map, die beim ersten Schreiben eine Panic auslöst. Knapp dahinter kommt ein Channel, der für immer blockiert. Dieser Artikel zeigt, wann du was verwendest und warum Slices, Maps und Channels die Sonderfälle sind. Jedes Beispiel ist lauffähig.
Inhaltsverzeichnis
- Was ist der Unterschied zwischen var und make in Go?
- Wann solltest du var verwenden?
- Wann solltest du make verwenden?
- Warum löst Schreiben in eine nil-Map eine Panic aus, append auf einen nil-Slice aber nicht?
- Und was ist mit new? (var vs make vs new)
- Eine schnelle Entscheidungsregel
- Häufig gestellte Fragen
- Quellen
Was ist der Unterschied zwischen var und make in Go?
var deklariert eine Variable und setzt sie auf den Nullwert ihres Typs. Das funktioniert für jeden Typ der Sprache. make akzeptiert nur einen Slice, eine Map oder einen Channel, also die drei eingebauten Typen, die eine Einrichtung zur Laufzeit brauchen, und liefert einen Wert zurück, der sofort Daten aufnehmen kann. Der Nullwert dieser drei ist nil, deshalb fehlt mit var die Einrichtung, die make übernimmt.
Konkret allokiert make die Hashtabelle hinter einer Map, richtet bei einem Channel die Sende- und Empfangswarteschlangen ein und gibt einem Slice ein zugrunde liegendes Array. Deklarierst du einen der drei mit var, bekommst du stattdessen nil.
Dieselbe Map, auf beide Arten deklariert:
example.gogopackage main import "fmt" func main() { var counts map[string]int // nil map, no hash table yet made := make(map[string]int) // initialized, ready to write fmt.Println(counts == nil) // true fmt.Println(made == nil) // false made["/health"] = 1 // fine, made is initialized fmt.Println(made["/health"]) // 1 // counts["/health"] = 1 // panic: assignment to entry in nil map }
Viele erwarten, dass eine Allokation einen Pointer zurückgibt. make tut das nicht. Es allokiert und gibt dir dann den Wert direkt.
make([]int, 3) liefert also einen gewöhnlichen []int, keinen *[]int, und du verwendest ihn, ohne irgendetwas zu dereferenzieren. make(map[string]int) liefert eine Map, in die du sofort schreiben kannst. Die eingebaute Funktion, die tatsächlich einen Pointer zurückgibt, ist new. Dazu weiter unten mehr.
var x T | make(T, ...) | |
|---|---|---|
| Funktioniert für | jeden Typ | nur Slices, Maps, Channels |
| Liefert dir | den Nullwert | einen initialisierten, benutzbaren Wert |
| Ergebnis bei Slices | nil-Slice (len 0, cap 0) | Slice ungleich nil mit zugrunde liegendem Array |
| Ergebnis bei Maps | nil-Map (Schreiben löst Panic aus) | beschreibbare Map |
| Ergebnis bei Channels | nil-Channel (blockiert für immer) | benutzbarer Channel |
| Rückgabe | der Wert | der Wert, niemals ein Pointer |
Wann solltest du var verwenden?
Verwende var für jeden Typ, dessen Nullwert schon nützlich ist: Zahlen, Booleans, Strings, Structs, Pointer und Interfaces. Go ist so entworfen, dass der Nullwert ein gültiger Startzustand ist. Deshalb musst du selten vorher etwas initialisieren.
bytes.Buffer ist ein gutes Beispiel. Er hat keinen Konstruktor. Du deklarierst ihn mit var und schreibst los:
example.gogopackage main import ( "bytes" "fmt" ) func main() { var buf bytes.Buffer // zero value is ready to use buf.WriteString("status=") buf.WriteString("ok") fmt.Println(buf.String()) // status=ok }
Mit sync.Mutex ist es genauso. Sein Nullwert ist ein entsperrter Mutex, und einen Konstruktor NewMutex gibt es nicht, weil du keinen brauchst.
var funktioniert auch für einen Slice, solange du ihn nur mit append wachsen lässt. Ein nil-Slice ist ein gültiger leerer Slice mit Länge 0 und Kapazität 0, und append allokiert beim ersten Element ein zugrunde liegendes Array. Per Index darauf zugreifen kannst du allerdings nicht. ids[0] = 3 löst eine Panic mit index out of range aus, weil die Länge noch 0 ist. Wenn du per Index zuweisen musst, nimm make([]int, n), damit der Slice von Anfang an eine echte Länge hat. Zum Sammeln mit append reicht ein nil-Slice:
example.gogopackage main import "fmt" func main() { var ids []int // nil slice, but append-ready for i := 1; i <= 3; i++ { ids = append(ids, i*10) } fmt.Println(ids) // [10 20 30] fmt.Println(ids == nil) // false, append allocated it }
ids := make([]int, 0) bringt dir hier nichts. Der nil-Slice ist kürzer und verhält sich genauso, sobald das erste append gelaufen ist. Wenn du das üben willst, bietet der Kurs Composite Types eine ganze Reihe von Übungen zum Verhalten von Slices und Maps.
Wann solltest du make verwenden?
Verwende make, wenn ein Slice, eine Map oder ein Channel Daten aufnehmen muss, bevor irgendetwas angehängt wird. In der Praxis heißt das: eine Map, in die du gleich Keys schreibst, ein Channel, auf dem Goroutinen senden, oder ein Slice mit vorab festgelegter Größe, dessen zugrunde liegendes Array von Anfang an existiert.
Maps kommen am häufigsten vor, weil es kein Gegenstück zu append gibt, das eine Map für dich allokiert. Wenn du irgendeinen Key zuweisen willst, musst du die Map vorher mit make erzeugen. Requests pro Pfad in einem HTTP-Handler zu zählen ist ein typischer Fall:
example.gogopackage main import "fmt" func main() { hits := make(map[string]int) // must initialize before writing for _, path := range []string{"/api", "/api", "/health"} { hits[path]++ // safe: map is initialized } fmt.Println(hits["/api"]) // 2 fmt.Println(hits["/health"]) // 1 }
Bei Channels ist es ähnlich. Ein Channel, auf dem du sendest, muss mit make erzeugt werden, weil ein nil-Channel für immer blockiert (mehr dazu weiter unten). Ein Worker-Fan-out beginnt mit einem Channel aus make:
example.gogopackage main import "fmt" func main() { jobs := make(chan int, 3) // buffered channel, capacity 3 jobs <- 1 jobs <- 2 close(jobs) for j := range jobs { fmt.Println("processing job", j) } }
Bei Slices gibt es make in zwei Formen. make([]T, n) liefert dir einen Slice der Länge n, gefüllt mit Nullwerten, auf den du direkt per Index zugreifst. make([]T, 0, n) liefert dir einen leeren Slice mit Platz für n Elemente, an den du mit append anhängst, ohne neu zu allokieren. Nimm die erste Form, wenn du jeden Platz per Index füllst, etwa beim Lesen einer festen Anzahl von Bytes:
example.gogopackage main import "fmt" func main() { // length 4, index into it directly buf := make([]byte, 4) copy(buf, []byte("data")) fmt.Println(string(buf)) // data // length 0, capacity 8, append without regrowing sums := make([]int, 0, 8) for i := 0; i < 8; i++ { sums = append(sums, i*i) } fmt.Println(sums) // [0 1 4 9 16 25 36 49] }
Bei der zweiten Form geht es um Performance. Während ein Slice wächst, kopiert append ihn immer wieder in größere Arrays um. Wenn du ungefähr weißt, wie viele Elemente kommen, sparst du dir diese Kopien, indem du die Kapazität vorab festlegst. Am Verhalten des Programms ändert das nichts, nur an seiner Geschwindigkeit.
Warum löst Schreiben in eine nil-Map eine Panic aus, append auf einen nil-Slice aber nicht?
Die beiden wurden unterschiedlich entworfen. append weiß, wie es mit einem nil-Slice umgehen muss. Beim ersten Aufruf allokiert es ein zugrunde liegendes Array und gibt einen echten Slice zurück, also funktioniert Schreiben über append immer. Bei Maps gibt es nichts Vergleichbares. Ein Schreibzugriff geht direkt an eine Hashtabelle, eine nil-Map hat keine, und die Runtime löst eine Panic aus, statt zu raten, was du gemeint hast.
Die meisten laufen in ihrer ersten Woche mit Go in genau diesen Fehler. Hier siehst du, was jeder nil-Typ tut. Den Anfang macht der nil-Slice, aus dem du problemlos lesen und an den du problemlos anhängen kannst:
example.gogopackage main import "fmt" func main() { var s []int // nil slice fmt.Println(len(s), cap(s)) // 0 0 fmt.Println(s == nil) // true s = append(s, 42) // works: append allocates fmt.Println(s) // [42] }
Eine nil-Map verhält sich anders. Lesen liefert den Nullwert des Werttyps, und range über sie tut nichts, aber der erste Schreibzugriff löst eine Panic aus:
example.gogopackage main import "fmt" func main() { var m map[string]int // nil map fmt.Println(m["missing"]) // 0, reading nil map is safe fmt.Println(len(m)) // 0, also safe m["key"] = 1 // panic: assignment to entry in nil map fmt.Println(m) }
Weil Lesen sicher ist und erst Schreiben eine Panic auslöst, übersieht man diesen Bug leicht. Eine nil-Map besteht jeden Test, der nur aus ihr liest, und löst dann eine Panic aus, sobald zum ersten Mal echte Daten ankommen. Die Lösung bleibt dieselbe: Initialisiere die Map vor dem ersten Schreiben mit make oder nimm eine Map entgegen, die der Aufrufer bereits erzeugt hat.
Nil-Channels verhalten sich von den dreien am seltsamsten. Sie lösen keine Panic aus. Ein Sende- oder Empfangsvorgang auf einem nil-Channel kommt einfach nie voran, und in einer einzelnen Goroutine ist das ein Deadlock:
example.gogopackage main func main() { var ch chan int // nil channel ch <- 1 // blocks forever: fatal deadlock }
Das klingt nach einer Falle, ist in einem select aber nützlich. Ein nil-Channel ist nie bereit. Setzt du also eine Channel-Variable auf nil, schaltest du ihren Case ab und kannst so einen Zweig zur Laufzeit deaktivieren. Vorsicht bei close: Einen nil-Channel zu schließen löst eine Panic aus, also schließ nur Channels, die du selbst erzeugt hast. Der Kurs Concurrency Fundamentals behandelt das Muster mit select und nil-Channels ausführlich. Das Paar aus nil-Map und nil-Slice steht auch in unserer Liste der häufigsten Go-Fehler, die du vermeiden solltest, weil es auch erfahrene Entwickler erwischt.
Und was ist mit new? (var vs make vs new)
new ist die dritte eingebaute Funktion zum Allokieren, und sie erledigt eine andere Aufgabe als make. new(T) allokiert genullten Speicher für ein T und gibt ein *T zurück. make kümmert sich nur um Slices, Maps und Channels und gibt den Wert zurück. new funktioniert für jeden Typ und gibt immer einen Pointer zurück.
Meistens rufst du es auf, um einen Pointer auf einen frischen Nullwert zu bekommen, ohne vorher eine benannte Variable zu deklarieren:
example.gogopackage main import "fmt" func main() { p := new(int) // *int pointing at a zeroed int fmt.Println(*p) // 0 *p = 7 fmt.Println(*p) // 7 }
Seit Go 1.26 akzeptiert new auch einen Ausdruck. new(expr) allokiert Speicher, legt dort den Wert des Ausdrucks ab und gibt einen Pointer darauf zurück. Damit werden die alten Hilfsfunktionen im Stil von intPtr überflüssig, die nur existierten, um einen Pointer auf ein Literal zu bekommen:
example.gogopackage main import "fmt" func main() { port := new(8080) // *int pointing at 8080 (Go 1.26) enabled := new(true) // *bool pointing at true fmt.Println(*port) // 8080 fmt.Println(*enabled) // true }
Das ist nützlich für optionale Struct-Felder, bei denen ein nil-Pointer „nicht gesetzt“ bedeutet und ein Pointer ungleich nil heißt, dass jemand einen Wert gewählt hat. Der Kurs Pointers & Memory enthält eine Übung zu Konfigurations-Defaults, die auf diesem Muster aufbaut. Die drei eingebauten Funktionen im Vergleich:
| Built-in | Funktioniert für | Rückgabe | Ist das Ergebnis benutzbar? |
|---|---|---|---|
var x T | jeden Typ | der Nullwert | benutzbar, wenn der Nullwert es ist (nicht für Schreibzugriffe auf Maps) |
make(T, ...) | Slice, Map, Channel | der Wert T | ja, initialisiert |
new(T) | jeden Typ | *T, ein Pointer | zeigt auf ein genulltes T |
new(expr) (Go 1.26) | jeden Ausdruck | Pointer auf diesen Wert | zeigt auf den angegebenen Wert |
Für einen Slice, eine Map oder einen Channel, den du gleich befüllen willst, brauchst du fast immer make. Nimm new, wenn du ausdrücklich einen Pointer brauchst, meistens einen Pointer auf ein Literal für ein optionales Feld.
Eine schnelle Entscheidungsregel
Wenn du unsicher bist, was du nehmen sollst, geh diese Liste von oben nach unten durch und hör bei der ersten passenden Zeile auf:
- Eine Map, in die du Keys schreiben wirst? Verwende
make(map[K]V). Eine nil-Map löst beim ersten Schreiben eine Panic aus. - Ein Channel, auf dem Goroutinen senden werden? Verwende
make(chan T)odermake(chan T, n). Ein nil-Channel blockiert für immer. - Ein Slice, den du vorab dimensionieren willst, für mehr Tempo oder für Zuweisungen per Index? Verwende
make([]T, n)odermake([]T, 0, n). - Ein Slice, den du nur mit
appendwachsen lässt? Verwendevar s []T. Ein nil-Slice funktioniert mitappend. - Ein Pointer auf einen frischen Wert? Verwende
new(T)oder ab Go 1.26new(expr), um auf ein Literal zu zeigen. - Alles andere (int, string, bool, Struct, die meisten Typen der Standardbibliothek)? Verwende
var, weil der Nullwert sofort benutzbar ist.
Wenn du dir nur eine Sache merkst, dann diese: Bevor du einen Key in eine Map schreibst, prüf, woher die Map kommt. Eine Map, die als Parameter hereinkommt, hat vermutlich der Aufrufer initialisiert. Eine Map, die du selbst mit var deklariert hast, ist nil, und der erste Schreibzugriff löst eine Panic aus.
Häufig gestellte Fragen
Kann ich var für eine Map verwenden?
Du kannst eine Map mit var m map[string]int deklarieren. Lesen, range und len sind darauf alle sicher. Einen Key zu schreiben ist es nicht: m["x"] = 1 löst eine Panic mit assignment to entry in nil map aus. Wenn du nur liest, ist var in Ordnung. Wenn du irgendeinen Key schreibst, initialisiere die Map vorher mit make oder nimm eine entgegen, die der Aufrufer bereits angelegt hat.
Ist make([]int, 0) dasselbe wie var s []int?
Beide haben die Länge 0 und verhalten sich gleich, sobald du append aufrufst. Der Unterschied zeigt sich beim Vergleich mit nil. var s []int ist nil, während make([]int, 0) ein leerer Slice ungleich nil ist. Das spielt eine Rolle, wenn dein Code oder ein Test s == nil prüft oder wenn du nach JSON kodierst, wo nil zu null wird und der leere Slice zu []. Für reines Anhängen solltest du var bevorzugen.
Funktioniert make für Structs?
Nein. make akzeptiert nur Slices, Maps und Channels, deshalb führt make(MyStruct) zu einem Compilerfehler. Ein Struct braucht die Einrichtung zur Laufzeit, die make übernimmt, gar nicht. Erzeuge es mit einem Composite Literal wie MyStruct{}, deklariere es mit var s MyStruct, um den Nullwert zu bekommen, oder nutze new(MyStruct), wenn du ein *MyStruct willst.
make vs new: Was nehme ich für einen Pointer?
Verwende new, wenn du einen Pointer willst. new(T) gibt ein *T zurück, das auf einen genullten Wert zeigt, und ab Go 1.26 gibt new(expr) einen Pointer auf den Wert des Ausdrucks zurück. make gibt nie einen Pointer zurück. Es liefert einen initialisierten Slice, eine Map oder einen Channel. Die beiden überschneiden sich nicht: make gibt dir eine benutzbare Collection und new einen Pointer.
Quellen
Primärquellen, die in diesem Artikel zitiert werden (zuletzt geprüft am 3. Juli 2026):
- The Go Programming Language Specification: Making slices, maps and channels, abgerufen am 03.07.2026
- The Go Programming Language Specification: Allocation (new), abgerufen am 03.07.2026
- The Go Programming Language Specification: The zero value, abgerufen am 03.07.2026
- Effective Go: Allocation with make and new, abgerufen am 03.07.2026
- Go 1.26 Release Notes: Änderungen an der Sprache, abgerufen am 03.07.2026
