Type Assertions & Type Switches Intermediate¶
Introduction¶
Type assertions and type switches are Go's mechanisms for runtime type checking on interface values. Since interfaces in Go are satisfied implicitly, you sometimes need to discover or verify the concrete type behind an interface at runtime. Type assertions extract a concrete type from an interface value, while type switches branch on the dynamic type — similar to a regular switch but matching types instead of values. These are essential when working with any (empty interface), custom interface hierarchies, and standard library interfaces like io.Reader.
Syntax & Usage¶
Type Assertion: x.(T)¶
A type assertion extracts the concrete value of type T from an interface value x.
var i interface{} = "hello"
// Type assertion -- panics if wrong type
s := i.(string)
fmt.Println(s) // "hello"
// This would panic at runtime:
// n := i.(int) // panic: interface conversion: interface {} is string, not int
The Comma-Ok Pattern (Safe Assertion)¶
var i interface{} = "hello"
// Safe -- never panics
s, ok := i.(string)
if ok {
fmt.Println("string:", s)
} else {
fmt.Println("not a string")
}
// Common idiom: check and use in one step
if s, ok := i.(string); ok {
fmt.Printf("Got string of length %d\n", len(s))
}
// When assertion fails: ok=false, value is zero value of T
n, ok := i.(int) // ok=false, n=0
Always Use Comma-Ok for Untrusted Values
A bare type assertion x.(T) panics if the interface doesn't hold type T. Always use the comma-ok form when the type isn't guaranteed.
Type Assertion on Named Interfaces¶
Type assertions aren't limited to interface{}/any. You can assert from any interface type to a more specific type.
type Reader interface {
Read(p []byte) (n int, err error)
}
type ReadCloser interface {
Reader
Close() error
}
func process(r Reader) {
// Check if this Reader also implements Close
if rc, ok := r.(ReadCloser); ok {
defer rc.Close()
}
// Check if this Reader implements WriteTo (for efficient copying)
if wt, ok := r.(io.WriterTo); ok {
wt.WriteTo(os.Stdout)
return
}
// Fall back to regular Read
buf := make([]byte, 1024)
r.Read(buf)
}
Type Switch¶
A type switch branches based on the dynamic type of an interface value.
func describe(i interface{}) string {
switch v := i.(type) {
case int:
return fmt.Sprintf("integer: %d", v)
case string:
return fmt.Sprintf("string: %q (len=%d)", v, len(v))
case bool:
return fmt.Sprintf("boolean: %t", v)
case []int:
return fmt.Sprintf("int slice: %v (len=%d)", v, len(v))
case nil:
return "nil"
default:
return fmt.Sprintf("unknown type: %T", v)
}
}
fmt.Println(describe(42)) // "integer: 42"
fmt.Println(describe("hello")) // "string: \"hello\" (len=5)"
fmt.Println(describe(nil)) // "nil"
Multiple Types in a Case¶
switch v := i.(type) {
case int, int8, int16, int32, int64:
// v is interface{} here, NOT the concrete type
// because multiple types could match
fmt.Printf("some integer: %v\n", v)
case string, []byte:
fmt.Printf("string-like: %v\n", v)
}
Multi-Type Case Variable
When a case lists multiple types, the variable v remains the interface type, not a concrete type. You lose type-specific operations. Prefer separate cases when you need to use type-specific methods.
Asserting Interface Satisfaction¶
Type assertions can check if a value implements a specific interface.
type Stringer interface {
String() string
}
type Logger interface {
Log(msg string)
}
func printIfStringable(v interface{}) {
if s, ok := v.(Stringer); ok {
fmt.Println(s.String())
} else {
fmt.Printf("%v\n", v)
}
}
// Type switch on interfaces
func categorize(v interface{}) {
switch v.(type) {
case Stringer:
fmt.Println("implements Stringer")
case Logger:
fmt.Println("implements Logger")
case error:
fmt.Println("is an error")
default:
fmt.Println("no known interface")
}
}
Extracting Specific Behavior (Common Pattern)¶
This pattern is used extensively in the standard library to optionally use enhanced capabilities.
type Flusher interface {
Flush() error
}
type Sizer interface {
Size() int64
}
func writeData(w io.Writer, data []byte) error {
_, err := w.Write(data)
if err != nil {
return err
}
// Optionally flush if the writer supports it
if f, ok := w.(Flusher); ok {
return f.Flush()
}
return nil
}
// Real example: net/http checks if ResponseWriter implements http.Flusher
func streamHandler(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming not supported", http.StatusInternalServerError)
return
}
for i := 0; i < 10; i++ {
fmt.Fprintf(w, "data: %d\n\n", i)
flusher.Flush() // send chunk immediately
time.Sleep(1 * time.Second)
}
}
Interface Conversion vs Type Assertion¶
type Animal interface {
Speak() string
}
type Dog struct{ Name string }
func (d Dog) Speak() string { return "Woof!" }
func (d Dog) Fetch() string { return "Got it!" }
var a Animal = Dog{Name: "Rex"}
// Type assertion -- extract concrete type Dog
dog := a.(Dog)
fmt.Println(dog.Fetch()) // "Got it!" -- access Dog-specific methods
// Interface conversion -- this is compile-time, not runtime
var s fmt.Stringer = dog // Dog must implement Stringer at compile time
| Operation | Syntax | When | Fails |
|---|---|---|---|
| Type assertion | x.(ConcreteType) |
Runtime | Panic or ok=false |
| Interface assertion | x.(InterfaceName) |
Runtime | Panic or ok=false |
| Interface conversion | var i Interface = value |
Compile time | Compile error |
Error Type Checking with Type Assertions¶
type NotFoundError struct {
ID int
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("resource %d not found", e.ID)
}
func handleError(err error) {
// Type assertion approach
if nfe, ok := err.(*NotFoundError); ok {
log.Printf("not found: ID=%d", nfe.ID)
return
}
// Preferred in modern Go: errors.As (handles wrapping)
var nfe *NotFoundError
if errors.As(err, &nfe) {
log.Printf("not found: ID=%d", nfe.ID)
return
}
log.Printf("unexpected error: %v", err)
}
errors.As vs Type Assertion
For error handling, prefer errors.As over type assertions. errors.As unwraps error chains created with fmt.Errorf("...: %w", err), while a direct type assertion only checks the outermost error.
Quick Reference¶
| Concept | Syntax | Notes |
|---|---|---|
| Type assertion | v := x.(T) |
Panics if x doesn't hold T |
| Safe assertion | v, ok := x.(T) |
ok=false if wrong type, no panic |
| Type switch | switch v := x.(type) { case T: ... } |
Branch on dynamic type |
| Multi-type case | case T1, T2: |
v remains interface type |
| Nil case | case nil: |
Matches nil interface value |
| Default case | default: |
No type matched |
| Interface check | v, ok := x.(InterfaceName) |
Check interface satisfaction |
%T verb |
fmt.Sprintf("%T", x) |
Print dynamic type name |
Best Practices¶
- Always use comma-ok for type assertions on values of uncertain type — bare assertions panic
- Prefer type switches over chains of
if/elsetype assertions — cleaner and easier to extend - Use
errors.Asinstead of type assertions for error types — handles wrapped errors - Keep type switches short — if you have many cases, consider redesigning with interfaces
- Assert to interfaces, not concrete types when possible — check for behavior, not identity
- Use type assertions to extract optional behavior — the
io.Writer→Flusherpattern is idiomatic - Avoid
interface{}when a specific interface will do — type assertions are a sign of lost type information
Common Pitfalls¶
Bare Assertion Panic
var i interface{} = "hello"
n := i.(int) // PANIC: interface conversion: interface {} is string, not int
n, ok := i.(int) unless you're absolutely certain of the type.
Asserting on Nil Interface
Nil interface values fail all type assertions. Check for nil first or use comma-ok.Pointer vs Value Type Confusion
type MyError struct{ Msg string }
func (e *MyError) Error() string { return e.Msg }
var err error = &MyError{"oops"}
// WRONG -- MyError doesn't implement error, *MyError does
_, ok := err.(MyError) // ok=false
// RIGHT
_, ok := err.(*MyError) // ok=true
*T, assert to *T, not T.
Type Switch Doesn't Fall Through
switch v := x.(type) {
case int:
fmt.Println("int")
// does NOT fall through to the next case
case string:
fmt.Println("string")
}
fallthrough keyword is not allowed in type switches.
Shadowing in Type Switch
switch v := x.(type) {
case int:
v++ // v is int here
case string:
v += "!" // v is string here -- same variable name, different type!
}
// v is not accessible here (scoped to switch)
v is re-bound with a different type in each case — this is intentional but can be confusing.
Performance Considerations¶
| Scenario | Performance |
|---|---|
| Type assertion (concrete type) | Very fast — single pointer comparison of type metadata |
| Type assertion (interface) | Slightly slower — must check method set |
| Comma-ok assertion | Same cost as bare assertion (no exception overhead) |
| Type switch (few cases) | Compiled as a sequence of type checks — fast |
| Type switch (many cases) | Linear scan — consider redesigning with polymorphism |
reflect.TypeOf |
Much slower — avoid in hot paths, prefer type assertions |
fmt.Sprintf("%T", x) |
Uses reflection internally — slow, use for debugging only |
No Exception Overhead
Unlike exception-based languages, the comma-ok pattern has zero overhead compared to the panicking form. The "ok" boolean is a normal return value, not a caught exception.
Interview Tips¶
Interview Tip
"What's the difference between a type assertion and a type switch?" A type assertion (x.(T)) tests for a single specific type and extracts the value. A type switch (switch x.(type)) branches on the dynamic type among multiple possibilities. Use assertions for a known expected type; use switches when multiple types are possible.
Interview Tip
"When does a type assertion panic?" A bare assertion x.(T) panics when the interface value doesn't hold type T. The comma-ok form v, ok := x.(T) never panics — it returns ok=false and the zero value of T instead. Always use comma-ok for safety.
Interview Tip
"How are type assertions related to interfaces?" Type assertions are the runtime complement to Go's compile-time interface system. They let you "recover" type information that was erased when a value was stored in an interface. You can assert to a concrete type (get the exact value back) or to another interface (check for additional capabilities).
Interview Tip
"What's the idiomatic way to check optional capabilities?" Assert to an interface that defines the optional capability. For example, net/http checks if a ResponseWriter also implements http.Flusher or http.Hijacker. This pattern lets types opt into additional behavior without changing the base interface.
Key Takeaways¶
- Type assertions extract concrete types from interface values:
v := x.(T) - Always use comma-ok (
v, ok := x.(T)) unless the type is guaranteed — bare assertions panic - Type switches (
switch x.(type)) are cleaner than chains of type assertions - Multi-type cases (
case int, int64:) keep the variable as the interface type - Assert to interfaces to check for behavior, not concrete types — more flexible
- Type assertions are fast — they compare type metadata pointers, not values
- For errors, prefer
errors.Asover type assertions — it handles wrapped error chains fallthroughis not allowed in type switches- If you need many type assertion cases, reconsider your design — interfaces should provide polymorphism