Zurück zum Blog

var vs make in Go: Wann du was verwendest (mit Beispielen)

var liefert dir den Nullwert, und make initialisiert Slices, Maps und Channels. Wann du in Go was verwendest und warum Schreiben in eine nil-Map eine Panic auslöst, append auf einen nil-Slice aber nicht.

var vs make in Go: Wann du was verwendest (mit Beispielen)

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?

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.gogo
package 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 Tmake(T, ...)
Funktioniert fürjeden Typnur Slices, Maps, Channels
Liefert dirden Nullwerteinen initialisierten, benutzbaren Wert
Ergebnis bei Slicesnil-Slice (len 0, cap 0)Slice ungleich nil mit zugrunde liegendem Array
Ergebnis bei Mapsnil-Map (Schreiben löst Panic aus)beschreibbare Map
Ergebnis bei Channelsnil-Channel (blockiert für immer)benutzbarer Channel
Rückgabeder Wertder 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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-inFunktioniert fürRückgabeIst das Ergebnis benutzbar?
var x Tjeden Typder Nullwertbenutzbar, wenn der Nullwert es ist (nicht für Schreibzugriffe auf Maps)
make(T, ...)Slice, Map, Channelder Wert Tja, initialisiert
new(T)jeden Typ*T, ein Pointerzeigt auf ein genulltes T
new(expr) (Go 1.26)jeden AusdruckPointer auf diesen Wertzeigt 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:

  1. Eine Map, in die du Keys schreiben wirst? Verwende make(map[K]V). Eine nil-Map löst beim ersten Schreiben eine Panic aus.
  2. Ein Channel, auf dem Goroutinen senden werden? Verwende make(chan T) oder make(chan T, n). Ein nil-Channel blockiert für immer.
  3. Ein Slice, den du vorab dimensionieren willst, für mehr Tempo oder für Zuweisungen per Index? Verwende make([]T, n) oder make([]T, 0, n).
  4. Ein Slice, den du nur mit append wachsen lässt? Verwende var s []T. Ein nil-Slice funktioniert mit append.
  5. Ein Pointer auf einen frischen Wert? Verwende new(T) oder ab Go 1.26 new(expr), um auf ein Literal zu zeigen.
  6. 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):

Schreib Go wie ein Senior Engineer

Interaktive Lektionen im Browser. Die ersten sind kostenlos.

Kostenlose Lektion testenOder kostenloses Konto erstellen