Zurück zum Blog

Das Go-Keyword var: Nullwerte, := und Variablen auf Package-Ebene

So funktioniert das Go-Keyword var: Deklarationsformen, Nullwerte, var oder :=, Shadowing, Variablen auf Package-Ebene und ihre Initialisierungsreihenfolge, Sentinel-Errors und Interface-Prüfungen zur Compile-Zeit.

Das Go-Keyword var: Nullwerte, := und Variablen auf Package-Ebene

Das Keyword var deklariert eine Variable. Du gibst ihr einen Namen und einen Typ, einen Anfangswert oder beides, etwa var port int, var host = "localhost" oder var ratio float64 = 0.25. Eine Variable ohne Wert beginnt mit dem Nullwert ihres Typs. Das ist 0 für Zahlen, "" für Strings, false für boolesche Werte und nil für Pointer, Slices, Maps, Channels, Funktionen und Interfaces. Innerhalb von Funktionen verwendet der meiste Go-Code stattdessen die Kurzform name := value, aber auf Package-Ebene ist var die einzige Möglichkeit, eine Variable zu deklarieren (Go spec).

Kurzfassung

  • var name Type deklariert eine Variable mit ihrem Nullwert. var name = value leitet den Typ aus dem Wert ab. var name Type = value macht beides. Ein Block var ( ... ) gruppiert mehrere Deklarationen.
  • Jede Variable in Go ist initialisiert. Es gibt keinen uninitialisierten Speicher, und Typen wie sync.Mutex, sync.WaitGroup und bytes.Buffer sind mit ihrem Nullwert sofort einsatzbereit.
  • := deklariert und weist in einem Schritt zu, aber nur innerhalb von Funktionen. Auf Package-Ebene brauchst du var.
  • := braucht auf der linken Seite mindestens einen neuen Namen. Namen, die im selben Scope schon existieren, werden wiederverwendet, und Namen aus einem äußeren Scope werden überdeckt (Shadowing).
  • Eine äußere Variable mit := innerhalb eines if-Blocks zu überdecken ist ein häufiger Bug. Der Compiler akzeptiert es, und der Analyzer shadow findet es.
  • Eine ungenutzte lokale Variable ist ein Compilerfehler, declared and not used. Ungenutzte Variablen auf Package-Ebene kompilieren.
  • Variablen auf Package-Ebene werden in der Reihenfolge ihrer Abhängigkeiten initialisiert, bevor irgendeine init()-Funktion läuft. Sentinel-Errors und kompilierte reguläre Ausdrücke sind typische Variablen auf Package-Ebene.
  • var _ http.Handler = (*Server)(nil) lässt den Compiler prüfen, ob ein Typ ein Interface implementiert.

Wie deklariert man eine Variable in Go?

Es gibt drei Formen und einen gruppierten Block, der sie beliebig mischen kann:

example.gogo
package main

import (
	"fmt"
	"time"
)

var (
	serviceName        = "billing-api"
	maxRetries   int64 = 3
	requestCount int
)

func main() {
	var port int             // type only: starts at 0
	var host = "localhost"   // value only: the type is inferred as string
	var ratio float64 = 0.25 // both
	var readTimeout, writeTimeout time.Duration
	fmt.Println(serviceName, maxRetries, requestCount, port, host, ratio, readTimeout, writeTimeout)
}

Das gibt billing-api 3 0 0 localhost 0.25 0s 0s aus.

Mit Typ und ohne Wert bekommt die Variable den Nullwert dieses Typs. Mit Wert und ohne Typ übernimmt die Variable den Typ des Werts. Bei einer untypisierten Konstante wie "localhost" oder 0.25 ist das der Standardtyp: string, int, float64, rune, bool oder complex128. Mit beidem wird der Wert in den deklarierten Typ konvertiert, und so bekommst du aus einem einfachen Zahlenliteral einen int64 oder einen float32. var maxRetries int64 = 3 ist ein int64. var maxRetries = 3 ist ein int.

Eine Deklaration kann mehrere Namen auflisten. var readTimeout, writeTimeout time.Duration deklariert zwei Variablen desselben Typs, beide 0s. Der gruppierte Block var ( ... ) ist auf Package-Ebene üblich, wo er zusammengehörige Einstellungen beieinander hält. Anders als ein const-Block wiederholt er nicht den Ausdruck der vorherigen Zeile. Jeder Name ohne Wert bekommt also den Nullwert.

Wie jeder andere Name in Go wird eine Variable auf Package-Ebene exportiert, wenn ihr Name mit einem Großbuchstaben beginnt. http.DefaultClient und os.Args sind exportierte Variablen auf Package-Ebene. Das Go-Keyword package erklärt, wie exportierte Namen über Package-Grenzen hinweg funktionieren.

Was ist der Nullwert in Go?

Der Nullwert ist der Wert, den eine Variable hat, wenn du sie ohne Initialisierung deklarierst. Go kennt keinen uninitialisierten Speicher, also hat jede Variable ab dem Moment, in dem sie existiert, einen definierten Wert:

TypNullwert
boolfalse
int, float64 und die anderen numerischen Typen0
string"" (der leere String)
Pointer, Slices, Maps, Channels, Funktionen, Interfacesnil
Arraysjedes Element auf seinem Nullwert
Structsjedes Feld auf seinem Nullwert

Dieselbe Regel gilt für Speicher aus new, für Struct-Felder, die in einem Composite Literal fehlen, und für die Elemente eines Slice, der mit make erzeugt wurde. Ein Struct User, das aus JSON ohne Feld email dekodiert wird, hat am Ende Email == "" und keinen zufälligen Speicherinhalt.

Warum sind Nullwerte in Go nützlich?

Gut entworfene Typen funktionieren mit ihrem Nullwert, ohne Konstruktor, und die Standardbibliothek verlässt sich darauf. Ein sync.Mutex mit Nullwert ist ein nicht gesperrter Mutex, eine sync.WaitGroup ist eine Gruppe mit dem Zähler null und ein bytes.Buffer ist ein leerer Puffer, in den du sofort schreiben kannst. Ein Struct aus solchen Feldern ist einsatzbereit, sobald es deklariert ist:

example.gogo
package main

import (
	"bytes"
	"fmt"
	"sync"
)

type RequestCounter struct {
	mu    sync.Mutex
	total int
}

func (c *RequestCounter) Inc() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.total++
}

func main() {
	var counter RequestCounter // ready to use, no constructor needed
	var wg sync.WaitGroup
	for range 10 {
		wg.Go(counter.Inc)
	}
	wg.Wait()

	var buf bytes.Buffer
	fmt.Fprintf(&buf, "handled %d requests", counter.total)
	fmt.Println(buf.String())
}

Das gibt handled 10 requests aus. Keine der drei Variablen brauchte eine New-Funktion. Effective Go empfiehlt, eigene Typen genauso zu entwerfen, damit var x T reicht, um ein T zu verwenden.

Welche Nullwerte kann man nicht sofort verwenden?

Maps und in geringerem Maß Pointer und Channels. Ein nil-Slice funktioniert mit append, len und range, also ist var tags []string gefolgt von tags = append(tags, "go") in Ordnung. Aus einer nil-Map kannst du lesen, aber nicht in sie schreiben. Ein Schreibzugriff löst eine Panic mit assignment to entry in nil map aus. Das Dereferenzieren eines nil-Pointers löst eine Panic aus, und das Senden auf einem nil-Channel blockiert für immer. var oder make in Go geht diese Fälle durch und zeigt, wann du stattdessen zu make greifst.

Was ist der Unterschied zwischen var und := in Go?

:= ist die kurze Variablendeklaration. retries := 3 bedeutet dasselbe wie var retries = 3: Es deklariert eine neue Variable und leitet ihren Typ aus dem Wert ab. Sie spart das Keyword und den Typ und ist innerhalb von Funktionen die übliche Wahl, besonders für Rückgabewerte eines Funktionsaufrufs:

example.gogo
resp, err := http.Get(healthURL)

Der Unterschied liegt darin, wo die beiden erlaubt sind. := funktioniert nur innerhalb eines Funktionsrumpfs. Auf Package-Ebene muss jede Deklaration mit einem Keyword beginnen, deshalb scheitert das hier:

example.gogo
package main

port := 8080

func main() {}
example.texttext
./main.go:3:1: syntax error: non-declaration statement outside function body

var port = 8080 auf Package-Ebene kompiliert.

Innerhalb von Funktionen kompilieren beide Formen, und die Wahl folgt ein paar Konventionen:

  1. Verwende :=, wenn du einen Wert zuweisen willst, etwa cfg := loadConfig() oder n, err := w.Write(body).
  2. Verwende var, wenn du bewusst den Nullwert willst, etwa var buf bytes.Buffer, var wg sync.WaitGroup oder var users []User für einen Slice, den du mit append füllst. var zeigt dem Leser, dass der Nullwert der Ausgangspunkt ist.
  3. Verwende var mit Typ, wenn der Standardtyp eines Literals nicht passt. var limit int64 = 1 << 40 oder var threshold float32 = 0.8 liest sich besser als limit := int64(1 << 40).
  4. Verwende var, wenn die Variable erst später in verschiedenen Zweigen zugewiesen wird, etwa var status string gefolgt von einem if/else, das sie setzt.

Kann man eine Variable in Go mit := erneut deklarieren?

Teilweise. := braucht auf der linken Seite mindestens eine neue Variable, und jeder Name, der im selben Scope schon deklariert ist, wird zugewiesen statt neu deklariert. Deshalb kann eine Funktion ein einziges err über mehrere Aufrufe hinweg wiederverwenden:

example.gogo
package main

import "os"

func main() {
	f, err := os.Open("config.yaml")
	if err != nil {
		return
	}
	defer f.Close()

	info, err := f.Stat() // ok: info is new, err is reused
	_ = info
	f, err := os.Open("secrets.yaml")
	_ = f
}
example.texttext
./main.go:14:9: no new variables on left side of :=

info, err := f.Stat() kompiliert, weil info neu ist. Beim letzten := existieren f und err beide schon, es gibt also nichts zu deklarieren. Schreib f, err = os.Open("secrets.yaml") mit einem einfachen =, um ihnen etwas zuzuweisen. Für var gibt es keine solche Ausnahme. Zweimal var err error im selben Scope scheitert mit err redeclared in this block.

Was ist Variable Shadowing in Go?

Shadowing entsteht, wenn eine Deklaration in einem inneren Scope denselben Namen verwendet wie eine Variable in einem äußeren Scope. Die innere Variable verdeckt die äußere, bis der innere Scope endet. Jeder if-, for-, switch- und { }-Block öffnet einen neuen Scope, und := deklariert immer im aktuellen. Zusammen machen diese beiden Regeln den folgenden Bug leicht:

example.gogo
package main

import (
	"fmt"
	"strconv"
)

func listenAddr(rawPort string) (string, error) {
	port := 8080
	if rawPort != "" {
		port, err := strconv.Atoi(rawPort)
		if err != nil {
			return "", fmt.Errorf("invalid PORT %q: %w", rawPort, err)
		}
		fmt.Println("using PORT", port)
	}
	return fmt.Sprintf(":%d", port), nil
}

func main() {
	addr, err := listenAddr("9090")
	fmt.Println(addr, err)
}

Das gibt using PORT 9090 und danach :8080 <nil> aus. Innerhalb des if-Blocks ist err neu, also ist := erlaubt, und es deklariert dabei auch ein neues port. Der geparste Wert landet im inneren port, das an der schließenden Klammer verschwindet. Die Funktion gibt das äußere port zurück, das immer noch 8080 ist. Das Go-Keyword if erklärt die Scope-Regeln von if-Anweisungen, die das möglich machen.

Die Lösung ist, der äußeren Variable etwas zuzuweisen, statt eine neue zu deklarieren. Deklarier zuerst err und verwende =:

example.gogo
if rawPort != "" {
	var err error
	port, err = strconv.Atoi(rawPort)
	if err != nil {
		return "", fmt.Errorf("invalid PORT %q: %w", rawPort, err)
	}
}

Die Standardprüfungen von go vet melden Shadowing nicht, weil viel Shadowing gewollt ist, etwa err in einem verschachtelten if. Der Analyzer shadow vom Go-Team meldet es:

example.texttext
go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
go vet -vettool=$(which shadow) ./...
example.texttext
./main.go:11:3: declaration of "port" shadows declaration at line 9

golangci-lint kann dieselbe Prüfung über den Linter govet mit aktiviertem shadow ausführen.

Warum meldet Go „declared and not used“?

Der Compiler lehnt jede lokale Variable ab, die nie gelesen wird. Im Listing oben ist die Print-Zeile der einzige Grund, warum das innere port kompiliert. Ohne sie meldet der Compiler declared and not used: port und zeigt damit auf den Bug. Einen Wert zuzuweisen zählt nicht als Verwendung:

example.gogo
package main

import "net/http"

func main() {
	resp, err := http.Get("https://example.com")
	if err != nil {
		return
	}
	retries := 0
	retries = 3
}
example.texttext
./main.go:6:2: declared and not used: resp
./main.go:10:2: declared and not used: retries

Die Regel gilt nur für lokale Variablen. Eine ungenutzte Variable auf Package-Ebene, ein ungenutzter Funktionsparameter und ein ungenutzter benannter Rückgabewert kompilieren alle. Wenn du einen Wert behalten musst, den du noch nicht liest, weise ihn mit _ = resp dem Blank Identifier zu, oder lass ihn mit _, err := http.Get(url) ganz weg.

Wie funktionieren Variablen auf Package-Ebene in Go?

Eine Variable, die außerhalb jeder Funktion deklariert ist, gehört zum Package. Sie ist in jeder Datei des Packages sichtbar, existiert während der gesamten Laufzeit des Programms und wird initialisiert, bevor main startet. Variablen auf Package-Ebene werden nach ihren Abhängigkeiten initialisiert, nicht in der Reihenfolge, in der sie im Quellcode stehen:

example.gogo
package main

import (
	"errors"
	"fmt"
)

var (
	usersURL = apiBase + "/users"
	apiBase  = apiHost + "/v2"
	apiHost  = "https://api.internal"
)

var ErrNotFound = errors.New("user not found")

func init() {
	fmt.Println("init sees", usersURL)
}

func main() {
	err := fmt.Errorf("load user 42: %w", ErrNotFound)
	fmt.Println(usersURL, errors.Is(err, ErrNotFound))
}

Das gibt init sees https://api.internal/v2/users und danach https://api.internal/v2/users true aus. usersURL wird zuerst deklariert, hängt aber von apiBase ab, das wiederum von apiHost abhängt. Der Compiler initialisiert sie deshalb in umgekehrter Reihenfolge. Variablen, die nicht voneinander abhängen, werden in der Reihenfolge ihrer Deklaration initialisiert. Zwei Variablen, die voneinander abhängen, sind ein Fehler, initialization cycle for a.

Sobald jede Variable auf Package-Ebene ihren Wert hat, laufen die init()-Funktionen, in der Reihenfolge, in der sie im Quellcode stehen, Datei für Datei. Ein Package kann mehrere init()-Funktionen haben, sogar in einer Datei. Importierte Packages werden vorher vollständig initialisiert. Wenn dein init() läuft, ist also jedes Package, das du importierst, bereit. Das Go-Keyword import erklärt, wie init() mit Blank Imports zusammenspielt.

Warum deklariert man Sentinel-Errors mit var?

Weil errors.New ein Funktionsaufruf ist und sein Ergebnis keine Konstante sein kann. Ein Sentinel-Error ist eine Variable auf Package-Ebene, mit der Aufrufer per errors.Is vergleichen:

example.gogo
var (
	ErrNotFound   = errors.New("user not found")
	ErrEmailTaken = errors.New("email already registered")
)

errors.Is vergleicht Error-Werte, und jeder Aufruf von errors.New liefert einen eigenen Wert, auch bei gleichem Text. Deshalb muss der Error einmal auf Package-Ebene erzeugt und gemeinsam genutzt werden. Beispiele aus der Standardbibliothek sind io.EOF, sql.ErrNoRows und http.ErrServerClosed.

Dasselbe gilt für andere Werte, die aufwendig zu bauen sind und sich nach dem Start nie ändern. var slugPattern = regexp.MustCompile("^[a-z0-9-]+$") kompiliert den regulären Ausdruck einmal beim Programmstart. Ist das Muster ungültig, löst es außerdem schon beim Start eine Panic aus, statt erst beim ersten Request zu scheitern.

Sollte man in Go globale Variablen verwenden?

Für Werte, die nach dem Start feststehen, wie Sentinel-Errors, kompilierte Muster und Lookup-Tabellen, ja. Für veränderlichen Zustand meist nicht. Ein var db *sql.DB oder var cfg Config auf Package-Ebene wird von jeder Goroutine und jedem Test im Package geteilt. Gleichzeitige Schreibzugriffe brauchen einen Mutex, Tests können nicht mit unterschiedlichen Werten parallel laufen, und nichts in der Signatur einer Funktion zeigt, dass sie von der globalen Variable abhängt.

Die übliche Alternative ist, Abhängigkeiten in ein Struct zu packen und dieses weiterzugeben:

example.gogo
type Server struct {
	db     *sql.DB
	logger *slog.Logger
}

func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) {
	// uses s.db and s.logger instead of package-level variables
}

Jeder Test kann dann seinen eigenen Server mit eigener Datenbank und eigenem Logger bauen.

Was bedeutet var _ = in Go?

Es deklariert eine Variable mit dem Blank Identifier _ als Namen. Der Wert wird ausgewertet und dann verworfen. Am häufigsten dient das als Prüfung zur Compile-Zeit, ob ein Typ ein Interface implementiert:

example.gogo
type Server struct{}

func (s *Server) ServeHTTp(w http.ResponseWriter, r *http.Request) {}

var _ http.Handler = (*Server)(nil)

Der Methodenname oben enthält einen Tippfehler, und die Deklaration macht daraus einen Build-Fehler:

example.texttext
./main.go:9:22: cannot use (*Server)(nil) (value of type *Server) as http.Handler value in variable declaration: *Server does not implement http.Handler (missing method ServeHTTP)
		have ServeHTTp(http.ResponseWriter, *http.Request)
		want ServeHTTP(http.ResponseWriter, *http.Request)

Ohne die Prüfung würde der Tippfehler erst dort auffallen, wo ein *Server als http.Handler übergeben wird, und das kann in einem anderen Package sein oder nirgends. (*Server)(nil) ist ein nil-Pointer, der in *Server konvertiert wird, die Prüfung kostet also keine Allokation. Verwende stattdessen Server{}, wenn die Methoden Value Receiver haben.

Der Blank Identifier funktioniert auch in Deklarationen mit mehreren Werten. var _, offset = time.Now().Zone() behält nur das zweite Ergebnis.

Wie deklariert man in Go mehrere Variablen auf einmal?

Liste die Namen durch Kommas getrennt auf und gib eine passende Liste von Werten an oder einen Funktionsaufruf, der ebenso viele Werte zurückgibt:

example.gogo
package main

import (
	"fmt"
	"strings"
)

var host, port = "localhost", 8080

func main() {
	var user, domain, found = strings.Cut("[email protected]", "@")
	fmt.Println(host, port, user, domain, found)
}

Das gibt localhost 8080 ops levelupgo.dev true aus. Ohne Typ bekommt jede Variable den Typ ihres eigenen Werts, also ist host ein string und port ein int. Mit Typ, etwa var minPort, maxPort int = 1024, 65535, teilen sich alle diesen Typ.

Zuweisungen funktionieren genauso. a, b = b, a vertauscht zwei Variablen, weil Go jeden Wert auf der rechten Seite auswertet, bevor es irgendeinen davon zuweist.

Seit Go 1.22 sind Schleifenvariablen, die in einer for-Anweisung mit := deklariert werden, in jeder Iteration neue Variablen. Eine Closure oder Goroutine, die job in for _, job := range jobs erfasst, sieht den Job dieser Iteration und nicht den letzten. Das Go-Keyword for zeigt den Goroutine-Bug, den das behoben hat, und wie du prüfst, welches Verhalten ein Modul bekommt.

Wann verwendet man in Go var und wann const?

Verwende const für Werte, die du schon beim Schreiben des Codes kennst und die sich nie ändern, etwa Timeouts, Limits und Header-Namen. Verwende var für alles andere: Werte, die zur Laufzeit berechnet werden, Werte aus der Konfiguration und jeden Slice, jede Map, jedes Struct und jeden Pointer, da diese keine Konstanten sein können. Das Go-Keyword const erklärt untypisierte Konstanten, iota und Enums.

Wo LevelUpGo ins Spiel kommt

LevelUpGo bringt dir Go mit Übungen bei, die echten Go-Code im Browser ausführen. Go Basics führt var, := und Nullwerte in den ersten Lektionen ein, zusammen mit Konstanten und Typen. Go Language Deep Dives hat eine Lektion zu Scope und Shadowing, in der du überdeckte Variablen findest und korrigierst. Der Training Ground bietet kurze, eigenständige Übungen zum Üben außerhalb eines Kurses. Die anderen 24 reservierten Wörter findest du in Go Keywords: Alle 25 erklärt.

Häufig gestellte Fragen

Ist var ein Keyword in Go?

Ja. var ist eines der 25 reservierten Keywords von Go, du kannst es also nicht als Variablen-, Funktions- oder Typnamen verwenden. Es leitet eine Variablendeklaration ein, entweder ein einzelnes var name Type = value oder einen gruppierten Block var ( ... ).

Was ist der Unterschied zwischen var und := in Go?

:= ist eine Kurzform von var, die den Typ immer ableitet und nur innerhalb von Funktionen funktioniert. x := 5 und var x = 5 deklarieren beide einen int mit dem Wert 5. Auf Package-Ebene ist var Pflicht, und es ist die klarere Wahl, wenn du den Nullwert willst oder einen anderen Typ als den Standardtyp des Literals. := kann außerdem neue und bestehende Namen mischen, solange mindestens ein Name auf der linken Seite neu ist.

Was ist der Nullwert einer Variable in Go?

Das ist der Wert, den eine Variable hat, wenn sie ohne Initialisierer deklariert wird: 0 für Zahlen, "" für Strings, false für boolesche Werte und nil für Pointer, Slices, Maps, Channels, Funktionen und Interfaces. Bei Arrays und Structs bekommt jedes Element oder Feld seinen Nullwert. Go kennt keine uninitialisierten Variablen.

Kann man := in Go außerhalb einer Funktion verwenden?

Nein. Auf Package-Ebene muss jede Deklaration mit einem Keyword wie var, const, type oder func beginnen, und port := 8080 scheitert mit syntax error: non-declaration statement outside function body. Schreib stattdessen var port = 8080.

Warum gibt Go den Fehler „declared and not used“ aus?

Weil eine ungenutzte lokale Variable oft auf einen Bug hinweist, etwa eine überdeckte Variable oder eine vergessene Fehlerprüfung. Deshalb lehnt der Go-Compiler sie ab. Einen Wert zuzuweisen zählt nicht als Verwendung. Lies die Variable, entferne sie oder weise sie mit _ = x dem Blank Identifier zu. Ungenutzte Variablen auf Package-Ebene und ungenutzte Funktionsparameter sind erlaubt.

Quellen

Schreib Go wie ein Senior Engineer

Interaktive Lektionen im Browser. Die ersten sind kostenlos.

Kostenlose Lektion testenOder kostenloses Konto erstellen