Type Assertions and Switches
An interface value hides a concrete type inside. A type assertion takes it back out:
var v any = "hello" // any is an alias for interface{}: zero methods, so every type satisfies it
s := v.(string) // assert: the value inside v is a string
fmt.Println(len(s)) // 5
v.(string) says "I claim v holds a string — give it to me as one." If
the claim is right, you get the string. If it's wrong, the program panics.
The comma-ok form
Panicking on a guess is rarely acceptable, so assertions have a safe form that reports success instead:
s, ok := v.(string)
if !ok {
// v held something else; s is the zero value ""
}
With two results, a failed assertion never panics — ok is false and the
first result is the zero value of the asserted type. Use the one-result form
only when a wrong type is a programmer error you want to crash loudly.
Type switches
When you need to handle several possible types, chaining comma-ok assertions gets clumsy. Go has a dedicated control structure, the type switch:
func classify(v any) string {
switch x := v.(type) {
case int:
return fmt.Sprintf("int: %d", x)
case string:
return fmt.Sprintf("string of length %d", len(x))
case bool, nil:
return "boolean or nothing"
default:
return fmt.Sprintf("unexpected type %T", v)
}
}
The magic is v.(type), legal only in a switch. In each single-type case,
the bound variable x has that case's type — inside case int, x is an
int, no further assertion needed. A case may list several types (then x
stays any), case nil matches a nil interface, and default catches
everything else.
Smell or legitimate?
Type assertions are a deliberate escape hatch, and reaching for them should make you pause. If you find yourself switching on concrete types you defined yourself, that's usually a design smell: the branches are behavior that belongs on the types, as a method behind an interface — then the switch disappears into dynamic dispatch.
But there are honest uses, all over the standard library:
- Boundaries where typed data enters as
any: JSON decoding, database scans, values fromcontext.Context. - Optional interfaces:
fmtchecks whether a value implementsStringer;io.Copyasks its writer, "do you also implementReaderFrom?" and takes a fast path if so. - Error inspection:
errors.As, coming two lessons from now, is a type assertion with ergonomics.
Rule of thumb: asserting to discover a capability is idiomatic; asserting
to reconstruct knowledge you threw away by over-using any is the smell.
Try it
Write describe(v any) string using a type switch — including the slice type
[]int, which works in a case just like any other type.
Practice
1. Describe Anything
IntermediateImplement
describe, which reports the dynamic type stored in ananyvalue:func describe(v any) stringReturn exactly one of these strings:
Value's type Return int"int"string"string"bool"bool"[]int"[]int"anything else "unknown"For example,
describe(42)returns"int",describe([]int{1, 2})returns"[]int", anddescribe(3.14)returns"unknown"(afloat64isn't in the table).Use a type switch — this is exactly the job it exists for. Note that a composite type like
[]intis a perfectly validcase, and that types not listed (includingnil,float64, and[]string) must all fall through to"unknown".package main import "fmt" // describe returns the name of the dynamic type stored in v: // "int", "string", "bool", or "[]int" — anything else is "unknown". func describe(v any) string { // TODO: implement using a type switch return "" } func main() { fmt.Println(describe(42)) fmt.Println(describe("gopher")) fmt.Println(describe([]int{1, 2, 3})) fmt.Println(describe(3.14)) }Sign in to write and run your solution in the browser.
Sign in for hints and the solution.