Type Assertions
Interface values at runtime
Section titled “Interface values at runtime”When a value is stored in an interface variable, Go keeps two things under the hood: a pointer to the concrete type descriptor and a pointer to the concrete value. A type assertion extracts the concrete value back out.
The single-value form
Section titled “The single-value form”var s Shape = Circle{Radius: 5}c := s.(Circle) // asserts that s holds a Circlefmt.Println(c.Radius)If s does not hold a Circle, this panics at runtime. Use this form only when you are certain of the dynamic type — for example, right after a type check.
The comma-ok form
Section titled “The comma-ok form”The safe variant returns a boolean instead of panicking:
c, ok := s.(Circle)if ok { fmt.Println("it's a circle with radius", c.Radius)} else { fmt.Println("not a circle")}ok is true when the assertion succeeds, false otherwise. c is the zero value of Circle when ok is false. Always prefer the comma-ok form when the dynamic type is uncertain.
The type switch
Section titled “The type switch”When you need to branch on several possible types, a type switch is cleaner than chained if ok blocks:
switch v := x.(type) {case Circle: fmt.Println("circle", v.Radius)case Rect: fmt.Println("rect", v.W, v.H)default: fmt.Printf("unknown: %T\n", v)}Inside each case, v is already the concrete type — no additional assertion needed. The default case fires when none of the listed types match.
A case can also list several types together, case Circle, Rect: — but then v keeps the original interface type, not a concrete one, because the compiler cannot pick a single concrete type for the branch. Only a single-type case narrows v.
Asserting to an interface
Section titled “Asserting to an interface”You can also assert from one interface to another:
var w io.Writer = os.Stdoutif rc, ok := w.(io.ReadWriter); ok { // os.Stdout implements io.ReadWriter _ = rc}This is how standard library code checks for optional capabilities (e.g., whether a Writer also supports Seek).
Asserting on a nil interface
Section titled “Asserting on a nil interface”A nil interface — no type, no value — behaves consistently under both assertion forms, but the two forms disagree loudly:
var s Shape // nil interface — no type, no valuec, ok := s.(Circle)fmt.Println(c, ok) // {0} false — comma-ok is safe on a nil interface
switch v := s.(type) {case nil: fmt.Println("s is nil")case Circle: fmt.Println("circle", v)}
_ = s.(Circle) // single-value form panics on a nil interfaceRunning this on Go 1.26.5:
{0} falses is nilpanic: interface conversion: main.Shape is nil, not main.CircleThe comma-ok form is completely safe — ok is simply false. A type switch can catch the nil case explicitly with case nil:, which runs before any concrete-type case is considered. Only the single-value form panics, and the message names the nil interface specifically: main.Shape is nil, not main.Circle. Compare that to asserting the wrong concrete type on a non-nil interface — same single-value form, different message:
var a Animal = Dog{}_ = a.(Cat)panic: interface conversion: main.Animal is main.Dog, not main.CatBoth are interface conversion panics, but the first names nil where the second names the actual dynamic type — worth recognizing on sight when reading a crash log.
Pointer vs value: s.(*T) vs s.(T)
Section titled “Pointer vs value: s.(*T) vs s.(T)”Which concrete type an interface variable can hold — T or *T — depends on which receiver kind satisfies the interface, and it changes what a type assertion can succeed against. If Area has a pointer receiver, Circle itself never satisfies Shape, so asserting to the value type is not just false at runtime — it does not even compile:
type Shape interface{ Area() float64 }type Circle struct{ Radius float64 }
func (c *Circle) Area() float64 { return 3.14 * c.Radius * c.Radius }
func main() { var s Shape = &Circle{Radius: 2} if _, ok := s.(Circle); ok { // Circle does not implement Shape at all // unreachable }}./main.go:13:14: impossible type assertion: s.(Circle) Circle does not implement Shape (method Area has pointer receiver)The compiler proves this statically because Circle’s method set never includes Area — asserting to a type that could never satisfy the interface is a compile error, not a runtime maybe. Give Area a value receiver instead, and both Circle and *Circle satisfy Shape — but s still only ever holds the concrete type it was assigned, so the value assertion fails at runtime while the pointer assertion succeeds:
func (c Circle) Area() float64 { return 3.14 * c.Radius * c.Radius } // value receiver
var s Shape = &Circle{Radius: 2} // *Circle satisfies Shape too (method set of *T includes value-receiver methods)
if _, ok := s.(Circle); ok { fmt.Println("value assertion matched")} else { fmt.Println("value assertion failed — s holds *Circle, not Circle")}if _, ok := s.(*Circle); ok { fmt.Println("pointer assertion matched")}value assertion failed — s holds *Circle, not Circlepointer assertion matched*T’s larger method set lets *Circle satisfy an interface that Circle alone could also satisfy — but the assertion still checks the exact dynamic type stored, not “any type whose method set would qualify.”
package main
import "fmt"
type Animal interface { Sound() string}
type Dog struct{ Name string }type Cat struct{ Name string }
func (d Dog) Sound() string { return "Woof" }func (c Cat) Sound() string { return "Meow" }
func identify(a Animal) { // comma-ok form — safe even when a is nil if d, ok := a.(Dog); ok { fmt.Printf("Dog named %s says %s\n", d.Name, d.Sound()) return } // type switch handles the nil interface explicitly switch v := a.(type) { case nil: fmt.Println("no animal") case Cat: fmt.Printf("Cat named %s says %s\n", v.Name, v.Sound()) default: fmt.Printf("Unknown animal: %T\n", v) }}
func main() { animals := []Animal{Dog{Name: "Rex"}, Cat{Name: "Luna"}, nil} for _, a := range animals { identify(a) }} Trade-offs
Section titled “Trade-offs”| What you get | Benefit | Cost |
|---|---|---|
Comma-ok assertion v, ok := i.(T) | Safe, never panics | Must check ok every time |
Single-value assertion v := i.(T) | Concise | Panics if the dynamic type does not match |
| Type switch | Handles several types clearly | More verbose than a single assertion |
reflect.TypeOf | Inspects a type at runtime | High overhead — use only when truly needed |
errors.As is an assertion that walks the chain
Section titled “errors.As is an assertion that walks the chain”errors.As looks like a function call, but conceptually it is a type assertion that also unwraps: it repeatedly calls Unwrap() on the error chain, type-asserting each link against the target type, until one matches or the chain ends.
type NotFoundError struct{ Key string }
func (e *NotFoundError) Error() string { return "not found: " + e.Key }
func lookup(key string) error { return &NotFoundError{Key: key}}
func loadConfig() error { if err := lookup("timeout"); err != nil { return fmt.Errorf("load config: %w", err) } return nil}
func main() { err := fmt.Errorf("startup: %w", loadConfig())
var nf *NotFoundError if errors.As(err, &nf) { // walks the %w chain until it finds a *NotFoundError fmt.Println("missing key:", nf.Key) }}missing key: timeouterr here is wrapped twice (startup: then load config: ), but errors.As finds the *NotFoundError two levels down — something a single err.(*NotFoundError) assertion could never do, because it only ever sees the outermost error. The target argument must be a non-nil pointer to a type that implements error (or to an interface type). Passing the error value itself, instead of a pointer to a variable of that type, panics:
err := &NotFoundError{Key: "x"}var nf NotFoundError // not a pointer — wrong targetfmt.Println(errors.As(err, nf))panic: errors: target must be a non-nil pointerWhen a type assertion is a design smell — and when it’s not
Section titled “When a type assertion is a design smell — and when it’s not”Reaching for a type assertion to recover a concrete type usually means an interface was the wrong tool, or too broad — a smell worth questioning before writing if v, ok := x.(ConcreteType); ok. But the standard library also uses assertions deliberately, as an optional capability check rather than a control-flow shortcut. go doc io.Copy states the contract explicitly:
If src implements WriterTo, the copy is implemented by callingsrc.WriteTo(dst). Otherwise, if dst implements ReaderFrom, the copy isimplemented by calling dst.ReadFrom(src).io.Copy accepts the small Reader/Writer interfaces, but internally asserts for io.WriterTo and io.ReaderFrom to take a faster path when the concrete type supports it — callers never need to know this happens.
net/http uses the same pattern for http.Flusher and http.Hijacker, and its own doc comment for Flusher warns about the trap directly: “ResponseWriter wrappers may not [support Flusher]. Handlers should always test for this ability at runtime.” A logging wrapper that embeds http.ResponseWriter demonstrates exactly how the capability gets lost:
type loggingWriter struct { http.ResponseWriter status int}
func (w *loggingWriter) WriteHeader(code int) { w.status = code w.ResponseWriter.WriteHeader(code)}
var _ http.Flusher = &loggingWriter{}./main.go:15:22: cannot use &loggingWriter{} (value of type *loggingWriter) as http.Flusher value in variable declaration: *loggingWriter does not implement http.Flusher (missing method Flush)Embedding http.ResponseWriter only promotes the three methods that interface declares (Header, Write, WriteHeader) — Flush and Hijack are not part of it, so they are never promoted, even though the concrete *http.response underneath supports them. Handing lw to code that expects streaming silently breaks it. The fix is to assert against the embedded field, not the wrapper, and forward explicitly:
if f, ok := lw.ResponseWriter.(http.Flusher); ok { f.Flush()}Common Misconceptions
Section titled “Common Misconceptions”- A type assertion is always safe. Without the comma-ok form, it panics immediately if the type does not match.
- A type switch behaves like an ordinary switch. A type switch tests the interface’s dynamic type, not a value.
- A type assertion is the same as a type conversion. A conversion changes the type representation; an assertion checks whether the interface already holds that concrete type.
💡 Real-world example
encoding/json:Unmarshaluses type assertions to handle[]anyandmap[string]anywhen the JSON structure is not known in advance.
net/http: aResponseWriterwrapper checks whether the underlying writer also implementshttp.Flusherorhttp.Hijackervia a type assertion, to enable streaming or a WebSocket upgrade.
Reading AI-generated code
Section titled “Reading AI-generated code”When generated code type-asserts, check for these before trusting it:
- Is a single-value assertion (
v := x.(T)) used on a value that might not holdT— especially a value coming from a nil interface variable or an untrusted source? That is a live panic waiting to happen. - Does an
errors.Ascall pass the error value itself, or a non-pointer variable, as the second argument? It must be a non-nil pointer to the target type, or it panics. - Does a wrapper struct embed an interface (
http.ResponseWriter,io.Writer, …) and get handed to code that expects an optional capability (Flusher,Hijacker,WriterTo)? Embedding only promotes the embedded interface’s own methods — extra capabilities of the concrete value underneath are silently lost unless the wrapper re-asserts and forwards them. - Is there a type assertion recovering a concrete type from an interface parameter, right after the function accepted that parameter as an interface? That usually means the parameter should have been the concrete type all along, or the interface is too broad.
- Does a type switch have a
case nil:when the input could plausibly be a nil interface? Without it,case ConcreteType:never matches nil, and it silently falls todefault.
🐛 Spot the bug
type Shape interface{ Area() float64 }type Circle struct{ Radius float64 }
func (c Circle) Area() float64 { return 3.14 * c.Radius * c.Radius }
func report(s Shape) { c := s.(Circle) fmt.Println(c.Area())}
func main() { var s Shape report(s)}Show the answer
report uses the single-value form s.(Circle) with no comma-ok check. main calls it with s still at its zero value — a nil interface, not a Circle. The assertion panics immediately: panic: interface conversion: main.Shape is nil, not main.Circle. The fix is the comma-ok form: c, ok := s.(Circle); if !ok { return } (or handle the nil case explicitly in a type switch with case nil:).