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 (Wert ist float64, fmt zeigt aber „3“, nicht „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)
// %c druckt e als Zeichen; mit %v käme der Codepoint 20013
fmt.Printf("a=%v b=%v c=%v d=%v e=%c sm=%v\n", a, b, c, d, e, sm)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 Bytes
fmt.Printf("s1=%s s2=%s s3=%s s4=%s\n", s1, s2, s3, s4)
fmt.Printf("b=%v\n", b)
fmt.Printf("r=%v\n", r)s1=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)
Um die Type Assertion zu verstehen, musst du zuerst wissen, was ein Interface-Wert in Go wirklich ist. Ein Wert vom Typ any (oder error, oder jedem anderen Interface) ist nicht einfach der Wert selbst. Er ist ein Paar aus zwei Teilen: dem dynamischen Typ — also der Information, welcher konkrete Typ gerade darin steckt — und dem eigentlichen Wert. Sobald du etwas in ein Interface legst, klebt Go ein „Typ-Etikett“ daran und verbirgt den konkreten Typ hinter der Interface-Fassade. Ab da kannst du mit der Variable nur noch das tun, was das Interface selbst erlaubt — bei any also praktisch nichts außer es weiterzureichen.
Die Type Assertion x.(T) ist der Weg zurück: Sie nimmt den Interface-Wert und fragt „steckt hier konkret ein T drin?“. Passt das Etikett, bekommst du den ausgepackten Wert vom Typ T heraus und kannst wieder uneingeschränkt damit arbeiten.
var x any = "hallo"
x (Interface-Wert)
+----------------------------+
| dynamischer Typ : string | <- das Etikett
| Wert : "hallo" | <- die eigentlichen Daten
+----------------------------+
x.(string) Etikett == string? ja -> "hallo" (ok = true)
x.(int) Etikett == int? nein -> 0, false (ohne comma-ok: Panic)Abgrenzung zur Conversion
Genau hier liegt der Unterschied zur Conversion aus den oberen Abschnitten — und diese Verwechslung ist der häufigste Stolperstein:
- Eine Conversion
T(x)erzeugt einen neuen Wert eines anderen Typs und wird vom Compiler geprüft.float64(i)rechnet denintin einenfloat64um — das Ergebnis ist ein anderer Speicherinhalt. - Eine Assertion
x.(T)erzeugt nichts Neues. Sie holt den bereits vorhandenen Wert unverändert aus dem Interface heraus und wird erst zur Laufzeit geprüft. Schlägt die Prüfung fehl, merkt der Compiler nichts davon; das Programm panickt erst beim Ausführen (oder liefertok = false).
Merksatz: Conversion rechnet um, Assertion packt aus. Eine Conversion fragst du den Compiler, eine Assertion fragt die Laufzeit.
Die zwei Formen
var x any = "hallo"
// x trägt intern: dynamischer Typ = string, Wert = "hallo"
// Form 1: ohne comma-ok — packt aus, panickt bei Typ-Mismatch
s := x.(string)
fmt.Println(s) // hallo
// Form 2: comma-ok bei Treffer — v hält den Wert, ok ist true
v, ok := x.(string)
fmt.Println(v, ok) // hallo true
// Form 3: comma-ok bei Fehlschlag — KEIN Panic
n, ok := x.(int) // x steckt ein string, kein int
fmt.Println(n, ok) // 0 false (0 = Nullwert von int)hallo
hallo true
0 falseWas bei einem Fehlschlag passiert, hängt allein an der Form, die du wählst:
- Ohne comma-ok (
s := x.(T)): Stimmt der Typ nicht, bricht das Programm sofort mit einem Panic ab —panic: interface conversion: interface {} is string, not int. Das willst du nur, wenn ein Mismatch ein echter Programmierfehler wäre. - Mit comma-ok (
v, ok := x.(T)): Du bekommst nie einen Panic. Bei einem Treffer istoktrue undvhält den ausgepackten Wert. Bei einem Fehlschlag istokfalse undvder Nullwert des Zieltyps — fürintalso0, für einen Pointernil. Genau das erklärt die Ausgabe0 false: Der String steckt nicht alsintdrin, also liefert Go Nullwert plusfalsestatt eines Absturzes.
Assertion gegen ein Interface
T muss kein konkreter Typ sein. Du kannst auch gegen ein anderes Interface asserten und damit fragen „erfüllt der verpackte Typ dieses Interface?“ — du prüfst dann nicht auf einen exakten Typ, sondern auf eine Fähigkeit (eine Methodenmenge):
var x any = someValue
// Hat der verpackte Typ eine String()-Methode?
if s, ok := x.(fmt.Stringer); ok {
fmt.Println(s.String()) // ja — als Stringer behandelbar
}So findet idiomatischer Go-Code heraus, ob ein beliebiger Wert sich etwa selbst als String darstellen kann, ganz ohne seinen konkreten Typ zu kennen.
Die kritische Voraussetzung gilt für alle Formen: 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, denn ein konkreter Typ trägt kein „Etikett“, das man abfragen könnte.
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