Skip to main content

Type Assertion and Type Switch

Type assertion extracts a concrete type from an interface value, while type switch branches on the type. Together they enable safe polymorphism in Go.

Type Assertion​

Assert that an interface value holds a specific type and extract that value.

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 main() {
var a Animal = Dog{Name: "Rex"}

// 1. Simple type assertion β€” panics on failure!
dog := a.(Dog)
fmt.Println(dog.Name, dog.Sound()) // Rex woof

// 2. Safe type assertion β€” ok pattern (recommended)
if dog, ok := a.(Dog); ok {
fmt.Println("Dog:", dog.Name)
} else {
fmt.Println("not a Dog")
}

if cat, ok := a.(Cat); ok {
fmt.Println("Cat:", cat.Name)
} else {
fmt.Println("not a Cat") // printed
}

// 3. Wrong type assertion β€” causes panic
defer func() {
if r := recover(); r != nil {
fmt.Println("panic recovered:", r)
}
}()
_ = a.(Cat) // panic: interface conversion: interface {} is Dog, not Cat
}

Safe Type Assertion Patterns​

Always use the ok pattern in production code.

package main

import "fmt"

type Shape interface {
Area() float64
}

type Circle struct{ Radius float64 }
type Rectangle struct{ Width, Height float64 }
type Triangle struct{ Base, Height float64 }

func (c Circle) Area() float64 { return 3.14159 * c.Radius * c.Radius }
func (r Rectangle) Area() float64 { return r.Width * r.Height }
func (t Triangle) Area() float64 { return 0.5 * t.Base * t.Height }

// Check for optional interface support
type Resizable interface {
Resize(factor float64) Shape
}

func (c Circle) Resize(factor float64) Shape {
return Circle{Radius: c.Radius * factor}
}

func processShape(s Shape) {
fmt.Printf("base area: %.2f\n", s.Area())

// Check if optional capability is supported
if r, ok := s.(Resizable); ok {
bigger := r.Resize(2.0)
fmt.Printf("2x area: %.2f\n", bigger.Area())
} else {
fmt.Println("this shape doesn't support resizing")
}
}

func main() {
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 4, Height: 6},
Triangle{Base: 3, Height: 4},
}

for _, s := range shapes {
fmt.Printf("=== %T ===\n", s)
processShape(s)
}
}

Type Switch​

Use switch v.(type) to branch on multiple types.

package main

import "fmt"

func describe(i any) string {
switch v := i.(type) {
case nil:
return "nil"
case int:
return fmt.Sprintf("integer: %d", v)
case float64:
return fmt.Sprintf("float: %.2f", v)
case bool:
if v {
return "boolean: true"
}
return "boolean: false"
case string:
return fmt.Sprintf("string: %q (len: %d)", v, len(v))
case []int:
return fmt.Sprintf("int slice: %v (len: %d)", v, len(v))
case map[string]any:
return fmt.Sprintf("map: %v", v)
case error:
return fmt.Sprintf("error: %s", v.Error())
default:
return fmt.Sprintf("unknown type: %T = %v", v, v)
}
}

func main() {
values := []any{
nil,
42,
3.14,
true,
false,
"hello",
[]int{1, 2, 3},
map[string]any{"key": "value"},
fmt.Errorf("something went wrong"),
struct{ X, Y int }{1, 2},
}

for _, v := range values {
fmt.Println(describe(v))
}
}

Type Switch with Interface Combinations​

package main

import (
"fmt"
"math"
)

type Shape interface {
Area() float64
}

type Circle struct{ Radius float64 }
type Rectangle struct{ Width, Height float64 }
type Triangle struct{ A, B, C float64 } // side lengths

func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}

func (r Rectangle) Area() float64 {
return r.Width * r.Height
}

func (t Triangle) Area() float64 {
// Heron's formula
s := (t.A + t.B + t.C) / 2
return math.Sqrt(s * (s - t.A) * (s - t.B) * (s - t.C))
}

// Type-specific detail output
func shapeDetail(s Shape) string {
switch v := s.(type) {
case Circle:
return fmt.Sprintf("Circle(r=%.1f, d=%.1f)", v.Radius, v.Radius*2)
case Rectangle:
return fmt.Sprintf("Rectangle(%.1fΓ—%.1f)", v.Width, v.Height)
case Triangle:
return fmt.Sprintf("Triangle(sides: %.1f, %.1f, %.1f)", v.A, v.B, v.C)
default:
return fmt.Sprintf("unknown shape (%T)", v)
}
}

// Handle multiple types in one case
func isPolygon(s Shape) bool {
switch s.(type) {
case Rectangle, Triangle:
return true
default:
return false
}
}

func main() {
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 3, Height: 4},
Triangle{A: 3, B: 4, C: 5},
}

for _, s := range shapes {
fmt.Printf("%s\n area=%.2f, polygon=%v\n",
shapeDetail(s), s.Area(), isPolygon(s))
}
}

Type Assertion in JSON Parsing​

A common real-world pattern: type assertion after JSON parsing.

package main

import (
"encoding/json"
"fmt"
)

func parseJSON(data string) {
var result any
if err := json.Unmarshal([]byte(data), &result); err != nil {
fmt.Println("parse error:", err)
return
}

// JSON parsing results in map[string]any, []any, float64, string, bool, nil
processValue("root", result)
}

func processValue(key string, v any) {
switch val := v.(type) {
case map[string]any:
fmt.Printf("%s: {object}\n", key)
for k, child := range val {
processValue(key+"."+k, child)
}
case []any:
fmt.Printf("%s: [array, len=%d]\n", key, len(val))
for i, item := range val {
processValue(fmt.Sprintf("%s[%d]", key, i), item)
}
case float64:
fmt.Printf("%s: number=%v\n", key, val)
case string:
fmt.Printf("%s: string=%q\n", key, val)
case bool:
fmt.Printf("%s: bool=%v\n", key, val)
case nil:
fmt.Printf("%s: null\n", key)
}
}

func main() {
jsonData := `{
"name": "John Doe",
"age": 30,
"active": true,
"scores": [95.5, 87.0, 92.3],
"address": {
"city": "Seoul",
"zip": "04524"
},
"memo": null
}`

parseJSON(jsonData)
}

errors.As and errors.Is β€” Error Type Assertion​

The standard way to use type assertion with errors in Go.

package main

import (
"errors"
"fmt"
)

// Custom error types
type ValidationError struct {
Field string
Message string
}

func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error [%s]: %s", e.Field, e.Message)
}

type NotFoundError struct {
Resource string
ID int
}

func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s(ID=%d) not found", e.Resource, e.ID)
}

var ErrPermissionDenied = errors.New("permission denied")

func findUser(id int) error {
if id <= 0 {
return &ValidationError{Field: "id", Message: "must be positive"}
}
if id > 100 {
return fmt.Errorf("user lookup failed: %w", &NotFoundError{Resource: "User", ID: id})
}
if id == 13 {
return fmt.Errorf("access blocked: %w", ErrPermissionDenied)
}
return nil
}

func handleError(err error) {
if err == nil {
fmt.Println("success!")
return
}

// errors.As: unwrap to specific type
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Printf("input error β€” field: %s, message: %s\n", ve.Field, ve.Message)
return
}

var nfe *NotFoundError
if errors.As(err, &nfe) {
fmt.Printf("not found β€” %s ID=%d\n", nfe.Resource, nfe.ID)
return
}

// errors.Is: check for specific error value
if errors.Is(err, ErrPermissionDenied) {
fmt.Println("permission error β€” login required")
return
}

fmt.Println("unknown error:", err)
}

func main() {
testCases := []int{-1, 42, 999, 13}
for _, id := range testCases {
fmt.Printf("ID=%d: ", id)
handleError(findUser(id))
}
}

Upcasting and Downcasting Interfaces​

package main

import "fmt"

type Base interface {
BaseMethod() string
}

type Extended interface {
Base
ExtendedMethod() string
}

type MyType struct {
value string
}

func (m MyType) BaseMethod() string { return "base: " + m.value }
func (m MyType) ExtendedMethod() string { return "extended: " + m.value }

func useBase(b Base) {
fmt.Println(b.BaseMethod())

// Downcast: Base β†’ Extended
if ext, ok := b.(Extended); ok {
fmt.Println(ext.ExtendedMethod())
}

// Cast to concrete type
if mt, ok := b.(MyType); ok {
fmt.Println("concrete type:", mt.value)
}
}

func main() {
m := MyType{value: "hello"}

// Upcast: concrete type β†’ interface (automatic)
var b Base = m
var e Extended = m

useBase(b)
fmt.Println("---")
useBase(e) // Extended also implements Base
}

Key Summary

  • v.(T): panics on failure β†’ only use when certain
  • v, ok := i.(T): ok pattern β†’ always use this in production
  • switch v := i.(type): branch on multiple types β†’ ideal for type-based dispatch
  • errors.As/errors.Is: standard way to do type assertion with errors
  • Type assertion only works on interface values (not concrete types)