Go ist eine statisch getypte Sprache — jede Variable hat zur Compile-Zeit einen festen Typ. Trotzdem musst du diesen Typ nur selten ausschreiben: x := 5 und var name = "Anna" reichen, der Compiler erschließt den Rest. Diese Bequemlichkeit heißt Type Inference, und sie folgt überraschend einfachen Regeln. Anders als Sprachen mit Hindley-Milner-Inferenz (Haskell, OCaml) blickt Go nicht über das gesamte Programm, sondern leitet lokal ab — aus der rechten Seite einer Zuweisung, aus einem Funktions-Argument, aus einem Return-Wert. Der spannendste Mechanismus dahinter sind die untyped constants mit ihren Default-Typen. Wer sie versteht, weiß auch, warum var f float64 = 1 legal ist und var i int = 3.14 nicht — und wer sie nicht versteht, läuft in subtile Typ-Verwechslungen, die Go-Anfänger reihenweise erwischen. Dieser Artikel arbeitet beide Ebenen sauber durch: erst die klassische Variablen-Inferenz mit untyped-Constants, dann die seit Go 1.18 hinzugekommene Type-Inference bei Generics.
Wie Type Inference in Go grundsätzlich funktioniert
Type Inference greift in Go an zwei Stellen: bei Variablen-Deklarationen ohne expliziten Typ und bei Generics-Aufrufen ohne explizite Type-Parameter. In beiden Fällen gilt dasselbe Prinzip: Der Compiler schaut auf die rechte Seite des Ausdrucks und übernimmt deren Typ. Es gibt keine globale Analyse, kein Backward-Reasoning aus späteren Verwendungen — die Information muss lokal verfügbar sein.
Die Spec formuliert die Variablen-Regel knapp:
If a type is present, each variable is given that type. Otherwise, each variable is given the type of the corresponding initialization value in the assignment. If that value is an untyped constant, it is first implicitly converted to its default type; if it is an untyped boolean value, it is first implicitly converted to type
bool. The predeclared identifiernilcannot be used to initialize a variable with no explicit type.
Daraus folgt das Schema:
| Form | Resultat |
|---|---|
var x int = 5 | x ist int — Typ explizit |
var x = 5 | x ist int — abgeleitet aus Default-Typ von 5 |
x := 5 | x ist int — Short Variable Declaration, gleiches Verfahren |
var x = math.Sin(0.5) | x ist float64 — Return-Typ der Funktion |
var a, b = 1, "hi" | a ist int, b ist string — pro Variable separat |
var x = nil | Fehler — nil hat keinen ableitbaren Typ |
Pro Identifier links wird der Typ aus der zugehörigen Expression rechts gezogen — eigenständig, nicht „der gemeinsame Typ aller Ausdrücke". Bei var a, b = 1, "hi" bekommt a seinen eigenen int, b seinen eigenen string. Mehr ist an der Basis-Regel nicht dran.
Untyped vs. Typed Constants — der Kern des Mechanismus
Hier liegt das Konzept, das viele zunächst übersehen. Literale wie 42, 3.14, "hi", true, 'a' haben in Go keinen festen Typ — sie sind untyped. Erst wenn sie in einen Kontext geraten, der einen konkreten Typ verlangt (etwa eine Zuweisung an eine getypte Variable oder ein Funktions-Argument), werden sie zu einem konkreten Typ „verfestigt". Aus der Spec:
An untyped constant has a default type which is the type to which the constant is implicitly converted in contexts where a typed value is required, for instance, in a short variable declaration such as
i := 0where there is no explicit type.
Die Default-Typen sind in der Sprache fest verdrahtet:
| Literal-Beispiel | Constant-Kategorie | Default Type |
|---|---|---|
true, false | boolean constant | bool |
42, 0xFF, 0b1010, 0o755 | integer constant | int |
3.14, 1.5e10, .5 | floating-point constant | float64 |
1 + 2i, 0i | complex constant | complex128 |
'a', '\n', 'ä' | rune constant | rune (Alias für int32) |
"hallo", `raw` | string constant | string |
Das ist die einzige Tabelle, die du wirklich auswendig wissen solltest — alle weiteren Stolperer im Artikel folgen daraus. Wichtig: rune ist int32, nicht byte. Ein Character-Literal 'a' hat damit Default-Typ int32, was Anfänger regelmäßig überrascht.
Was bedeutet „untyped" praktisch? Solange eine Konstante untyped bleibt, kann sie sich an den Kontext anpassen:
package main
import "fmt"
const x = 5 // untyped integer constant — KEIN int!
func main() {
var a int = x // x verfestigt sich zu int
var b int64 = x // x verfestigt sich zu int64
var c float64 = x // x verfestigt sich zu float64
var d byte = x // x verfestigt sich zu byte (uint8)
fmt.Printf("a=%d (%T), b=%d (%T), c=%g (%T), d=%d (%T)\n",
a, a, b, b, c, c, d, d)
}a=5 (int), b=5 (int64), c=5 (float64), d=5 (uint8)Dieselbe Konstante x wird in vier verschiedenen Zeilen zu vier verschiedenen Typen — weil sie untyped ist. Sobald du sie typst, ist die Flexibilität weg:
const y int = 5 // y ist getypt: int
var a int = y // OK
// var b int64 = y // FEHLER: cannot use y (untyped int constant 5) as int64 value
var c int64 = int64(y) // expliziter Cast nötig
_, _ = a, cMerksatz: Eine untyped Constant ist wie ein Wertstoff, der erst beim Verbau seine Form annimmt. Eine typed Constant ist bereits fertig gegossen — sie passt nur noch in eine Form ihres eigenen Typs.
Inferenz in der Praxis — x := 5 ergibt int, nicht int64
Genau dieser Default-Typ-Mechanismus greift, wenn du := oder ein typloses var benutzt: Da kein Ziel-Typ vorgegeben ist, fällt die untyped Constant auf ihren Default Type zurück. Daraus folgt die Standard-Falle für Newcomer:
package main
import "fmt"
func main() {
x := 5
y := 3.14
z := 'A'
s := "Hallo"
b := true
fmt.Printf("x = %d (%T)\n", x, x)
fmt.Printf("y = %g (%T)\n", y, y)
fmt.Printf("z = %d (%T)\n", z, z) // rune ist int32 — %d zeigt Codepoint
fmt.Printf("s = %q (%T)\n", s, s)
fmt.Printf("b = %v (%T)\n", b, b)
}x = 5 (int)
y = 3.14 (float64)
z = 65 (int32)
s = "Hallo" (string)
b = true (bool)z ist int32, nicht etwa byte — 'A' ist eine rune-Constant. Und x ist int, nicht int64 — selbst wenn dein Anwendungsfall klar int64 braucht (große Counter, Timestamps, Datenbank-Spalten). Wer einen abweichenden Typ will, hat zwei saubere Wege:
// Variante A: var mit explizitem Typ
var counter int64 = 0
var pixel byte = 255
var ratio float32 = 1.5
// Variante B: := mit Cast
counter2 := int64(0)
pixel2 := byte(255)
ratio2 := float32(1.5)Beide Formen sind idiomatisch. Variante A ist marginal lesbarer bei Zero-Initialisierung, Variante B knapper bei einem konkreten Wert. Code-Reviews schauen vor allem darauf, dass die Absicht ablesbar ist — nicht implizit hofft, dass der Default-Typ schon passt.
Untyped Constants in Ausdrücken — was geht und was nicht
Die Untyped-Mechanik schlägt nicht nur bei der Deklaration zu, sondern bei jeder Zuweisung. Daraus folgen ein paar zunächst paradox wirkende Regeln:
package main
import "fmt"
func main() {
// (1) untyped integer in float-Slot — OK
var f float64 = 1
fmt.Println(f) // 1
// (2) untyped float, der int-repräsentierbar ist — OK
var i int = 3.0
fmt.Println(i) // 3
// (3) untyped float mit Nachkomma in int-Slot — FEHLER
// var bad int = 3.14
// constant 3.14 truncated to integer
// (4) untyped Ausdruck bleibt untyped, solange nur untyped Operanden
const big = 1 << 62 // untyped — passt nicht in int32, schon in int64
var x int64 = big
fmt.Println(x) // 4611686018427387904
// (5) sobald ein typed Operand mitspielt, wird der Ausdruck typed
var n int32 = 10
// var m int64 = n + 1 // FEHLER: n ist int32, Ausdruck wird int32
var m int64 = int64(n) + 1
fmt.Println(m)
}1
3
4611686018427387904
11Zwei Regeln stecken dahinter:
- Untyped Constants konvertieren sich, wenn sie im Ziel-Typ exakt repräsentierbar sind.
3.0als int ist OK (3exakt),3.14ist nicht OK (Truncation würde Information vernichten). Genauso ist1 << 62als int64 OK, aber als int32 nicht. - Sobald ein Operand getypt ist, übernimmt der ganze Ausdruck dessen Typ. Mischen geht nicht implizit:
int32 + int64ohne Cast ist ein Compile-Fehler.
Das ist Gos kompromissloser Verzicht auf implicit numeric promotion. C wandelt int + double selbstständig zu double — Go verlangt einen expliziten Cast. Der Preis: ein bisschen mehr Tipparbeit. Der Gewinn: keine versteckten Präzisions-Verluste, keine Rundungs-Überraschungen.
Inferenz aus Funktions-Returns
Bei Aufrufen einer Funktion bestimmt der Return-Typ den Variablen-Typ. Hier passieren keine Default-Type-Überraschungen, weil Return-Typen immer schon getypt sind:
package main
import (
"fmt"
"os"
"strconv"
)
func smallCounter() int32 { return 0 }
func main() {
// x bekommt int32 — nicht int!
x := smallCounter()
// f bekommt *os.File, nicht etwa io.Reader oder io.ReadCloser
f, _ := os.Open("/etc/hostname")
defer f.Close()
// n ist int, err ist error
n, err := strconv.Atoi("42")
fmt.Printf("%T %T %T %T\n", x, f, n, err)
}int32 *os.File error *errors.errorStringBeachte den zweiten Fall: os.Open gibt *os.File zurück. Wer eine io.Reader-Variable haben will (um sie etwa generisch an einen anderen Reader-Konsumenten zu übergeben), kann das mit := nicht erreichen — := übernimmt den konkreten Typ. Wer das Interface will, muss var benutzen:
import (
"io"
"os"
)
// Ergibt *os.File:
f, _ := os.Open("/tmp/data")
// Ergibt io.Reader (Interface):
var r io.Reader
r, _ = os.Open("/tmp/data")
_ = r
_ = fDas Interface-Promotion-Problem taucht in den Pitfalls am Ende noch einmal auf — es ist Gos „Typ ist genau das, was rechts steht"-Regel in einer Form, die einen erst beim Refactoring beißt.
Type Inference bei Generics — Go 1.18 und neuer
Seit Go 1.18 gibt es Type Parameters (Generics). Eine generische Funktion wird mit ihren Typ-Parametern in eckigen Klammern deklariert:
import "cmp"
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}Beim Aufruf muss T festgelegt sein. Theoretisch wäre die explizite Form nötig:
m := Max[int](3, 5) // T explizit gesetzt: int
s := Max[string]("a", "b") // T explizit gesetzt: string
_, _ = m, sIn den allermeisten Fällen kann Go den Typ-Parameter aber aus den Argumenten ableiten — das ist die Function Argument Type Inference:
package main
import (
"cmp"
"fmt"
)
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
// T wird aus den Argument-Typen abgeleitet
fmt.Println(Max(3, 5)) // T = int
fmt.Println(Max(1.5, 2.5)) // T = float64
fmt.Println(Max("foo", "bar")) // T = string
}5
2.5
fooDer Compiler löst hier intern eine Typ-Gleichung: für jeden Parameter T setzt er den Argument-Typ ein und prüft, ob alle resultierenden Werte konsistent sind. Max(3, 5) ergibt T = int, weil beide Argumente vom (Default-)Typ int sind.
Constraint Type Inference ist die zweite Mechanik — sie kommt zum Tragen, wenn eine Constraint die Struktur eines Typs vorgibt:
package main
import "fmt"
// S muss ein Slice-Typ mit Element-Typ E sein
func First[S ~[]E, E any](s S) E {
return s[0]
}
type MyList []int
func main() {
xs := MyList{10, 20, 30}
// Aus dem Argument-Typ MyList wird S = MyList abgeleitet,
// und aus der Constraint S ~[]E folgt E = int.
fmt.Println(First(xs)) // 10
}10Hier hilft die Constraint: aus S ~[]E und der Erkenntnis, dass S = MyList = []int ist, schließt der Compiler E = int. Genau dieser Mechanismus macht slices.Sort, maps.Keys, slices.BinarySearch und Konsorten so bequem bedienbar — du übergibst nur den Slice, der Element-Typ wird automatisch erschlossen.
Wann Generics-Inferenz scheitert
Inferenz ist kompetent, aber kein Allheilmittel. Drei Fall-Klassen brechen die Ableitung und verlangen explizite Type-Argumente:
(1) Return-Type-only Generics. Wenn ein Typ-Parameter nur im Return-Typ vorkommt, hat der Compiler keine Argument-Quelle für ihn:
package main
import "fmt"
// T kommt nur im Return-Typ vor — keine Inferenz möglich
func Zero[T any]() T {
var z T
return z
}
func main() {
// x := Zero() // FEHLER: cannot infer T
x := Zero[int]() // explizit angeben
y := Zero[string]() // dito
fmt.Printf("%v %T | %q %T\n", x, x, y, y)
}0 int | "" string(2) Untyped Constants als einziges Argument. Wenn alle Argumente untyped Constants sind, gilt Gos „typed Argumente vor untyped Constants"-Regel — gibt es keine typed Argumente, fällt der Compiler auf die Default-Typen zurück. Seit Go 1.21 ist das in den meisten Mischfällen zuverlässig; zuvor brauchte es öfter explizite Typen:
package main
import "fmt"
func Sum[T int | float64](xs ...T) T {
var s T
for _, v := range xs {
s += v
}
return s
}
func main() {
// Nur untyped int-Literals — Default-Typ int wird genommen
fmt.Println(Sum(1, 2, 3)) // 6, T = int
// Mix aus int- und float-Literal: Go 1.21+ erweitert auf float64
fmt.Println(Sum(1, 2.0)) // 3, T = float64
// Eindeutig durch typed Argument
var x float64 = 1
fmt.Println(Sum(x, 2)) // 3, T = float64
}6
3
3(3) Generische Typen müssen explizit instanziert werden. Type Inference gibt es nur für Funktionen, nicht für Typ-Deklarationen:
type Stack[T any] struct {
data []T
}
// var s Stack // FEHLER: cannot use generic type Stack without instantiation
var s Stack[int] // explizit instanziert
_ = sDie Faustregel: Type Inference greift nur für Funktions-Aufrufe. Bei generischen Typen schreibst du den Typ-Parameter immer aus.
Inferenz und mehrere Return-Werte
Bei Multi-Return-Funktionen läuft Inferenz pro Variable. Jede linke Variable bekommt ihren eigenen Typ aus der zugehörigen Position des Return-Tuples:
package main
import (
"fmt"
"strconv"
)
func divmod(a, b int) (int, int) { return a / b, a % b }
func main() {
q, r := divmod(17, 5) // q ist int, r ist int
n, err := strconv.Atoi("42") // n ist int, err ist error
// Type Assertion mit comma-ok
var i any = "hallo"
s, ok := i.(string) // s ist string, ok ist bool
// Map-Lookup mit comma-ok
m := map[string]int{"alice": 30}
age, found := m["alice"] // age ist int, found ist bool
fmt.Println(q, r, n, err, s, ok, age, found)
}3 2 42 <nil> hallo true 30 trueDrei spezielle Multi-Return-Idiome — Type Assertion, Map-Lookup, Channel-Receive — geben über die zweite Position einen bool zurück. Inferenz behandelt das wie jeden anderen Funktions-Return: jeder Slot eigenständig.
Häufige Stolperfallen
x := 5 ergibt int, nicht int64 — auch wenn du int64 brauchst.
Untyped Integer Constants fallen auf den Default-Typ int zurück. Wer für Counter, Timestamps oder Datenbank-Felder int64 will, schreibt var x int64 = 5 oder x := int64(5). Bei := ohne Cast bekommst du int — und merkst es spätestens beim ersten cannot use x (int) as int64 value.
'a' ist rune (int32), nicht byte.
Character-Literals sind in Go untyped rune-Constants mit Default-Typ int32. Wer einen byte (ASCII oder einzelnes UTF-8-Byte) will, muss explizit casten: b := byte('a'). Ein häufiger Folge-Bug: beim Iterieren über einen String mit for i, c := range s ist c eine rune, nicht ein byte — wer das mit s[i] (das byte liefert) verwechselt, holt sich bei Umlauten Müll.
var f float64 = 1 ist legal, var i int = 3.14 ist es nicht.
Untyped Constants konvertieren sich nur, wenn sie im Ziel-Typ exakt repräsentierbar sind. 1 als float64 ist exakt (1.0), also OK. 3.14 als int würde Information abschneiden, also Compile-Fehler. Bei var x float32 = 1.1 greift die Spec-Ausnahme: Float-Literals sind in float32 und float64 repräsentierbar (ggf. mit Rundung) — der Compiler akzeptiert.
var n = nil ist verboten — nil hat keinen ableitbaren Typ.
nil kann ein nil-Slice, nil-Map, nil-Pointer, nil-Interface, nil-Channel oder nil-Function sein. Ohne Kontext weiß der Compiler nicht, welches. Lösungen: var p *int = nil, var s []byte (Zero Value ist nil-Slice), oder direkt mit make/new initialisieren.
Generics ohne Argument-Bezug erfordern explizite Type-Parameter.
Wenn ein Type-Parameter nur im Return vorkommt — wie bei func Zero[T any]() T — gibt es nichts, aus dem inferiert werden könnte. Aufruf nur als Zero[int](). Gleiches Problem bei func Decode[T any](data []byte) T — Aufruf zwingend mit Decode[MyStruct](raw).
Generische Typen müssen immer instanziert werden — Inferenz nur für Funktionen.
Bei type Stack[T any] struct{ ... } schreibst du immer Stack[int], niemals nur Stack. Inferenz gibt es ausschließlich beim Aufruf generischer Funktionen. Bei Typ-Konstruktion (Variablen, Felder, Funktions-Parameter) ist der Typ-Parameter Pflicht.
r := os.Stdin macht r zu *os.File, nicht zu io.Reader.
Inferenz übernimmt immer den konkreten Typ der rechten Seite. Wer eine Interface-Variable will (etwa um sie an Funktionen zu übergeben, die nur das Interface verlangen), nimmt var r io.Reader = os.Stdin. Das ist beim Refactoring relevant: wenn eine Funktion plötzlich nicht mehr *os.File, sondern *bytes.Buffer zurückgibt, bricht :=-Code an der Stelle still — die Variable wechselt den Typ mit.
Untyped Constant-Ausdrücke bleiben untyped, solange kein typed Operand mitspielt.
const big = 1 << 62 ist untyped — der Wert wäre für int32 zu groß, aber var x int64 = big funktioniert, weil die Verfestigung erst bei der Zuweisung passiert. Sobald irgendwo ein typed Wert in den Ausdruck rutscht, wird der ganze Ausdruck typisiert — und überschreitet er dann den Wertebereich des resultierenden Typs, gibt es einen Compile-Fehler.
Bei Multi-Return wird jede Variable einzeln inferiert.
a, b := f() setzt a auf den Typ des ersten Returns, b auf den des zweiten — unabhängig voneinander. Die Redeclaration-Regel von := greift dabei pro Position: wenn a schon existiert und denselben Typ hat, wird sie zugewiesen statt neu deklariert, solange mindestens ein anderer Name links neu ist.
Weiterführende Ressourcen
Externe Quellen
- Constants – Go Language Specification
- Variable declarations – Go Specification
- Type inference – Go Specification
- Type parameter declarations – Go Specification
- An Introduction to Generics – Go Blog
- Type inference in Go – Go Blog
Verwandte Artikel
- var vs. := – Deklarationsformen und Redeclaration
- Zero Values – was eine deklarierte Variable enthält
- const und iota – Konstanten und ihre Typen
- Scoping-Regeln – wo Identifier sichtbar sind
- int – Ganzzahlen und ihre Größen
- float – Fließkommazahlen und Genauigkeit
- rune – Unicode-Codepoints und Char-Literals