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 identifier nil cannot be used to initialize a variable with no explicit type.

Daraus folgt das Schema:

FormResultat
var x int = 5x ist int — Typ explizit
var x = 5x ist int — abgeleitet aus Default-Typ von 5
x := 5x 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 = nilFehler — 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 := 0 where there is no explicit type.

Die Default-Typen sind in der Sprache fest verdrahtet:

Literal-BeispielConstant-KategorieDefault Type
true, falseboolean constantbool
42, 0xFF, 0b1010, 0o755integer constantint
3.14, 1.5e10, .5floating-point constantfloat64
1 + 2i, 0icomplex constantcomplex128
'a', '\n', 'ä'rune constantrune (Alias für int32)
"hallo", `raw`string constantstring

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:

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

Go typed_constant.go
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, c

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

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

Go explicit_type.go
// 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:

Go untyped_assignments.go
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)
}
Output
1
3
4611686018427387904
11

Zwei Regeln stecken dahinter:

  • Untyped Constants konvertieren sich, wenn sie im Ziel-Typ exakt repräsentierbar sind. 3.0 als int ist OK (3 exakt), 3.14 ist nicht OK (Truncation würde Information vernichten). Genauso ist 1 << 62 als int64 OK, aber als int32 nicht.
  • Sobald ein Operand getypt ist, übernimmt der ganze Ausdruck dessen Typ. Mischen geht nicht implizit: int32 + int64 ohne 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:

Go function_inference.go
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)
}
Output
int32 *os.File error *errors.errorString

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

Go interface_inference.go
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
_ = f

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

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

Go generic_explicit.go
m := Max[int](3, 5)        // T explizit gesetzt: int
s := Max[string]("a", "b") // T explizit gesetzt: string
_, _ = m, s

In den allermeisten Fällen kann Go den Typ-Parameter aber aus den Argumenten ableiten — das ist die Function Argument Type Inference:

Go generic_inferred.go
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
}
Output
5
2.5
foo

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

Go constraint_inference.go
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
}
Output
10

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

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

Go untyped_args_generic.go
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
}
Output
6
3
3

(3) Generische Typen müssen explizit instanziert werden. Type Inference gibt es nur für Funktionen, nicht für Typ-Deklarationen:

Go generic_type_explicit.go
type Stack[T any] struct {
    data []T
}

// var s Stack          // FEHLER: cannot use generic type Stack without instantiation
var s Stack[int]        // explizit instanziert
_ = s

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

Go multi_return_inf.go
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)
}
Output
3 2 42 <nil> hallo true 30 true

Drei 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&#123; ... &#125; 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 &lt;&lt; 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

/ Weiter

Zurück zu Variablen & Konstanten

Zur Übersicht