Go kennt zwei Mechanismen, die in vielen Sprachen unter dem Wort „Cast” zusammengefasst werden — und die in Go strikt getrennt sind: Type Conversion T(v) ist eine Compile-Time-Operation, die einen Wert in einen anderen, kompatiblen Typ überführt. Type Assertion x.(T) ist eine Runtime-Operation, die einer Interface-Variable den darin verpackten konkreten Typ entlockt. Wer beide verwechselt, schreibt Code, der entweder gar nicht kompiliert oder zur Laufzeit panischt. Dieser Artikel zeigt beide Mechanismen Seite an Seite, mit den typischen Stolperfallen — von string(42) bis zur nicht erlaubten Pointer-Conversion.
Der zentrale Unterschied
Die beiden Mechanismen lösen unterschiedliche Probleme und greifen zu unterschiedlichen Zeitpunkten:
| Aspekt | Type Conversion T(v) | Type Assertion x.(T) |
|---|---|---|
| Zeitpunkt | Compile-Time | Runtime |
| Eingabe | beliebiger Wert mit kompatiblem Underlying Type | Interface-Wert (Pflicht) |
| Ergebnis | derselbe Wert in neuem statischem Typ | konkreter Typ aus dem Interface „herausgepackt” |
| Fehlerfall | Compile-Error | Runtime-Panic (oder ok == false) |
| Typischer Use-Case | int → float64, []byte → string | error → *MyError, any → konkreter Typ |
Merksatz: Conversion ändert den Typ eines Werts. Assertion fragt nach dem Typ, der bereits in einem Interface steckt.
Type Conversion: T(v)
Die Syntax ist trivial — ein Typname, gefolgt vom Wert in Klammern:
var i int = 42
var f float64 = float64(i) // int → float64
var u uint = uint(f) // float64 → uint (Trunkierung)
b := []byte("hallo") // string → []byte
s := string(b) // []byte → stringErlaubt ist eine Conversion grob dann, wenn Quell- und Zieltyp denselben Underlying Type haben oder explizit als kompatibel definiert sind (numerisch ↔ numerisch, string ↔ []byte/[]rune etc.). Die Go-Spec listet die exakten Regeln.
Was nicht geht, scheitert hart beim Compilieren — nicht erst zur Laufzeit:
var s string = "5"
var i int = int(s) // ❌ cannot convert s (string) to intFür Strings → Zahlen brauchst du strconv.Atoi, nicht int(...).
Numerische Konvertierungen
Zwischen allen numerischen Typen sind Conversions explizit erlaubt — aber nie implizit. Das ist eine bewusste Designentscheidung von Go:
var i int = 3
var f float64 = 3.9
a := float64(i) // 3.0
b := int(f) // 3 — Trunkierung Richtung Null!
c := int32(i) // 3
d := byte(65) // 65 (byte = uint8, Bereich 0–255)
e := rune(0x4E2D) // 中 (rune = int32, Codepoint)
// Achtung: Überlauf bei zu kleinem Zieltyp
var big int = 300
var sm int8 = int8(big) // -56 (300 - 256 mod-Verhalten)a=3 b=3 c=3 d=65 e=中 sm=-56byte und rune sind nur Alias-Namen für uint8 bzw. int32. Sie tauchen oft in String-Verarbeitung auf.
String-Konvertierungen
Strings haben in Go besondere Conversion-Regeln, die einen typischen Anfänger-Stolperstein bilden:
s1 := string(65) // "A" — Codepoint, NICHT "65"!
s2 := string('世') // "世" — rune-Literal
s3 := string([]byte{72, 105}) // "Hi"
s4 := string([]rune{0x4E2D}) // "中"
b := []byte("hallo") // [104 97 108 108 111]
r := []rune("hé") // [104 233] 2 Runen, nicht 3 Bytess1=A s2=世 s3=Hi s4=中
b=[104 97 108 108 111]
r=[104 233]Memory-Hinweis: []byte(s) und string(b) kopieren in der Regel den Speicher — Strings sind in Go unveränderlich, Slices nicht. Für Hot-Loops mit großen Strings lohnt sich gelegentlich unsafe-Tricks oder strings.Builder, aber das ist eine spätere Optimierung.
Die Go-Spec listet alle vier String-Spezialfälle vollständig auf.
Type Assertion: x.(T)
Die Type Assertion fragt einen Interface-Wert, ob der darin verpackte konkrete Typ T ist:
var x any = "hallo"
// Variante 1: ohne comma-ok — panischt bei Mismatch
s := x.(string)
fmt.Println(s) // hallo
// Variante 2: mit comma-ok — sicher
n, ok := x.(int)
fmt.Println(n, ok) // 0 falsehallo
0 falseDie kritische Voraussetzung: x muss ein Interface-Typ sein (any, error, oder ein eigenes Interface). Auf einen konkreten Typ (int, string, *User) lässt sich x.(T) nicht anwenden — der Compiler blockiert das sofort.
Comma-ok als sicherer Default
In Produktionscode ist die comma-ok-Form fast immer die richtige Wahl. Sie verhindert Panics und macht den Fehlerpfad explizit:
func describe(v any) string {
if s, ok := v.(string); ok {
return "string: " + s
}
if n, ok := v.(int); ok {
return fmt.Sprintf("int: %d", n)
}
return "unbekannt"
}Faustregel: Die Variante ohne ok benutzt du nur dann, wenn ein Mismatch ein Programmierfehler wäre — etwa direkt nach einem Type Switch, der die möglichen Typen schon abgrenzt.
Type Switch
Wenn mehrere Typen möglich sind, ist switch v := x.(type) lesbarer als eine Kette von Assertions:
func describe(v any) string {
switch x := v.(type) {
case nil:
return "nil"
case string:
return "string der Länge " + strconv.Itoa(len(x))
case int, int32, int64:
return fmt.Sprintf("ein Integer: %v", x)
case fmt.Stringer:
return "Stringer: " + x.String()
default:
return fmt.Sprintf("unbekannt: %T", x)
}
}// describe("hi") → string der Länge 2
// describe(42) → ein Integer: 42
// describe(nil) → nilAchtung beim Mehrfach-Case wie case int, int32, int64: Innerhalb dieses Case hat x noch den ursprünglichen Interface-Typ — nicht den konkreten. Erst bei einem einzelnen Typ pro Case ist x automatisch eingegrenzt.
Wann KEINE Conversion möglich ist
Conversions setzen voraus, dass die Underlying Types passen. Defined Types schaffen hier reichlich Stolperfallen:
type Celsius float64
type Fahrenheit float64
var c Celsius = 21.0
var f Fahrenheit
f = c // ❌ Compile-Error: cannot use c as Fahrenheit
f = Fahrenheit(c) // ✅ explizite Conversion ist OK,
// Underlying Type ist beide Mal float64
var raw float64 = c // ❌ ebenfalls Compile-Error
raw = float64(c) // ✅ explizit konvertiertAuch nicht möglich: Conversions zwischen Pointer-Typen mit unterschiedlichem Element-Typ. *int zu *int32 lässt sich nicht einfach via (*int32)(p) erzwingen — das geht nur über das unsafe-Paket und ist meistens ein Designsignal, etwas anders zu lösen.
Praxis: error → MyError mit errors.As
Ein häufiger Anwendungsfall in Go-Code: Du bekommst ein error-Interface zurück und willst prüfen, ob es ein bestimmter konkreter Typ ist. Hier könntest du theoretisch eine Type Assertion machen — idiomatisch ist aber errors.As:
type PathError struct {
Op, Path string
Err error
}
func (e *PathError) Error() string { /* ... */ return e.Op }
// Type Assertion — funktioniert nur bei direktem Match
if pe, ok := err.(*PathError); ok {
log.Printf("Pfadfehler bei %s", pe.Path)
}
// errors.As — durchläuft die Wrap-Chain (Empfehlung)
var pe *PathError
if errors.As(err, &pe) {
log.Printf("Pfadfehler bei %s", pe.Path)
}errors.As ist intern keine reine Type Assertion: es entpackt Unwrap()-Ketten und sucht den passenden Typ darin. Bei Errors, die mit fmt.Errorf("...: %w", inner) umhüllt wurden, findet eine schlichte Assertion den inneren Typ nicht — errors.As schon.
Häufige Stolperfallen
string(42) ergibt nicht "42"
string(42) ist eine gültige Conversion von int zu string — und liefert das Zeichen mit Codepoint 42, also ”*”. Nicht den Text “42”. Für die String-Repräsentation einer Zahl nutzt du strconv.Itoa(42) oder fmt.Sprint(42). Seit Go 1.15 warnt go vet bei string(int)-Conversions ohne explizite rune-Absicht.
Float → Int trunkiert, rundet nicht
int(3.9) ist 3, int(-3.9) ist -3 — abgeschnitten Richtung Null. Für mathematisch korrektes Runden brauchst du math.Round(x) und konvertierst danach: int(math.Round(3.9)) ergibt 4.
Type Assertion ohne comma-ok panischt
v := x.(MyType) ohne ok beendet das Programm bei Mismatch mit panic: interface conversion: …. In Bibliotheks- und Server-Code fast immer ein Bug. Default ist die comma-ok-Variante v, ok := x.(MyType) — die ohne-Variante nur in Code, der nach einem Type Switch garantieren kann, dass der Typ stimmt.
Type Assertion verlangt einen Interface-Wert
x.(T) funktioniert nur, wenn x selbst ein Interface-Typ ist (any, error, dein eigenes Interface). Auf einem konkreten Typ wie int oder string ist die Syntax ein Compile-Error. Wer „auf einen Typ casten” will, sucht eigentlich eine Type Conversion — siehe Sektion 2.
Defined Types brauchen explizite Conversion
type Celsius float64 macht Celsius und float64 zu zwei verschiedenen Typen, auch wenn der Underlying Type identisch ist. Eine Zuweisung var f float64 = c schlägt fehl — du brauchst float64(c). Dieselbe Regel gilt für eigene type ID int-Aliase und ähnliche Konstrukte.
Pointer-Conversion zwischen Typen geht nicht direkt
*int lässt sich nicht zu *int32 konvertieren — selbst wenn die Werte „passen” würden. Der einzige Weg ist unsafe.Pointer, was du nur in sehr spezifischen Low-Level-Szenarien (FFI, Memory-Layout) verwenden solltest. In normalem Anwendungscode ist das fast immer ein Anzeichen, das Design zu hinterfragen.
Type Assertion auf nil-Interface panischt
Ist var x any = nil, dann panischt x.(int) mit interface conversion: interface is nil. Die comma-ok-Variante schützt: v, ok := x.(int) liefert dann 0, false. Besonders wichtig in Code, der mit potenziell nicht initialisierten Interface-Variablen arbeitet.
Type Switch: default und Mehrfach-Cases
switch v := x.(type) erlaubt einen optionalen default-Case und mehrere Typen pro Case mit Komma: case int, int32, int64:. Nur wenn ein Case genau einen Typ enthält, hat v innerhalb dieses Case den konkreten Typ — bei Mehrfach-Cases bleibt v der ursprüngliche Interface-Typ.
Weiterführende Ressourcen
Externe Quellen
- Conversions – Go Specification
- Type assertions – Go Specification
- Type switches – Go Specification
- errors.As – pkg.go.dev