skip to content

Type Assertion and Boxing Cost

What a type assertion actually costs at runtime and how it differs from a type switch: asserting to a concrete type is close to a pointer comparison, while asserting to another interface needs an itab lookup. Comes up when an interviewer asks whether interface-heavy Go is slow and wants more than a shrug.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

Does Go's comma-ok type assertion v, ok := x.(T) cost more at runtime than the single-value form?

level: juniorimportance: must knowfreq 60%

answer

  1. same check, different failure
  2. one form has an escape hatch
  3. zero value plus false, or a panic
  4. the panic value names both types

basics

~20 s

Neither form is faster. Both run the same dynamic-type check and differ only in failure: the comma-ok form yields T's zero value and false, while the single-value form panics with a message naming the actual and the requested type.

solid answer

~50 s

Both spellings compile to the same check on the interface value's dynamic type, so choosing between them is a correctness decision, not a performance one. `v := x.(T)` panics when the dynamic type is not `T`; the panic value is a `*runtime.TypeAssertionError`, and like any panic it ends the process unless the same goroutine recovers it. `v, ok := x.(T)` hands back `T`'s zero value with `ok == false` instead — no panic, no error value, nothing allocated. I use the single-value form only where the type is an invariant established a few lines earlier in the same function, and the comma-ok form (or a type switch with a default arm) anywhere the set of dynamic types is open. A middleware that receives heterogeneous events as `any` is exactly that case: the panicking form there turns one unexpected event kind into an outage.

code

go · 8 lines
go
var ev any = "tick"

// single-value form:
// panic: interface conversion: interface {} is string, not int
n := ev.(int)

// comma-ok form: n2 == 0, ok == false, no panic
n2, ok := ev.(int)

go deeper

for a junior

Be ready to write both spellings from memory and say what each does on a mismatch: zero value plus false, or a panic. Knowing the failure behaviour is essentially the whole question at this level.

for a middle

Explain that both forms emit the same dynamic-type check, so the choice is about failure handling rather than speed, and that the panic value is a *runtime.TypeAssertionError naming the actual and requested types.

for a senior

Show production judgment: an open set of incoming types means comma-ok or a type switch with a default arm, and a panic raised inside a per-message goroutine ends the process unless that same goroutine recovers it.

for a principal

Own the convention across the codebase: which layers may use the panicking form at all, and whether an unrecognised message kind is dropped, dead-lettered or fatal. A blanket per-message recover hides schema drift you would rather be paged about.

## The two spellings A type assertion takes an interface value and asks about the concrete value stored inside it. Go gives it two spellings: ```go v := x.(T) // single-value form v, ok := x.(T) // comma-ok form ``` Both ask the runtime the same question: is the dynamic type inside `x` exactly `T` (or, when `T` is an interface type, does the dynamic type implement `T`)? Using one spelling rather than the other does not skip, weaken or reorder that check. What differs is the code the compiler emits for the *failure* path. - **Single-value form.** On a mismatch the runtime panics. The panic value is a `*runtime.TypeAssertionError`, which implements `error`, and its message names the static interface type, the dynamic type actually present and the type you asked for — for example `interface conversion: interface {} is string, not int`. If the interface value is nil there is no dynamic type at all, and the panic says the interface is nil. - **Comma-ok form.** On a mismatch `v` gets `T`'s zero value and `ok` is `false`. There is no panic, no error value to inspect and nothing allocated; the branch you write on `ok` is the whole error handling. ## Why neither is "the fast one" The cost of an assertion lives in the check, not in the number of results. For a concrete `T` the check is a comparison of the dynamic type against a type descriptor whose address the linker fixed at build time; the boolean of the comma-ok form is the flag that comparison already produced. The panicking form does not "prepare a panic" on the success path — it branches to a runtime call only when the comparison fails. So micro-optimising by reaching for the single-value form buys nothing measurable, while giving up the ability to handle a mismatch. There is one genuine cost difference in this area, and it is about the *target* type rather than the spelling: asserting to another interface type is a runtime question ("does this dynamic type have all of those methods?") rather than a single comparison. That is orthogonal to comma-ok. ## Choosing between them The useful rule is about who controls the dynamic type. - **You control it → single-value form is fine, and is a form of assertion.** You boxed the value yourself two lines earlier, or a preceding switch already narrowed it. A panic here reports a programmer error loudly; silently substituting a zero value would hide the bug and produce wrong output further downstream. - **Something outside controls it → comma-ok, or a type switch with a `default` arm.** Anything decoded, read from a queue, handed in by a caller you do not compile with, or delivered on a bus as `any` has an open set of dynamic types. A new producer ships a new event kind and your dispatcher meets a type it has never seen; that must be a logged, counted, possibly dead-lettered event, not a panic. ## The panic is worse than it looks in a dispatcher A panic unwinds only its own goroutine and then, if nothing recovers it, takes the whole program with it. `recover` works only when it is called directly by a function deferred by the *panicking* goroutine — a supervisor goroutine cannot catch a panic raised in a worker. A middleware that spawns a goroutine per message therefore needs its own deferred recover inside that goroutine if it wants to survive one bad message, and that recover is itself a decision: it converts a crash into a silently dropped message, which is why counting and logging the unhandled type matters more than the recover. ## What reviewers look for In a pull request the tell is a single-value assertion applied to a value that arrived from outside the package: `payload := msg.Body.(OrderCreated)` on a bus that also carries `OrderCancelled`. Ask for the comma-ok form or a type switch with a default arm, and ask what the default arm does — dropping unknown kinds silently is a different bug from crashing on them, and the team should choose deliberately.

  • What does a deferred recover actually receive when the single-value form fails?
    A value of type `*runtime.TypeAssertionError`, which implements `error` and names the interface type, the dynamic type present and the type requested. It only helps if the deferred function calling `recover` belongs to the goroutine that panicked — a panic in a worker goroutine cannot be caught by the supervisor that started it, so a per-message dispatcher needs the recover inside the goroutine handling that message.
  • When would you accept the panicking form in review?
    When the dynamic type is an invariant the same function just established: you boxed the value yourself, or a preceding type switch already narrowed it. There a panic reports a programmer error loudly, and substituting a zero value would hide it. Anything that came from outside the package — decoded input, a message bus, a plugin — gets the comma-ok form instead.
  • What does the comma-ok form give you for a nil interface value?
    The zero value and false, because a nil interface value carries no dynamic type and matches nothing. That is different from an interface holding a nil `*Event` pointer: it does have a dynamic type, so asserting to `*Event` succeeds with `ok == true` and hands you a nil pointer, which your next dereference will fault on.

saying these in an interview costs you the question

  • Claims comma-ok is slower because it returns two values
  • Thinks the single-value form skips the dynamic-type check
  • Believes a failed assertion returns an error you can inspect
  • Says another goroutine can recover this panic
  • Uses the panicking form on values decoded from outside
open as a page

Why is asserting a Go any value to a concrete type cheaper than asserting it to another interface?

level: middleimportance: should knowfreq 35%

basics

~20 s

A concrete target is one comparison: the value's dynamic type against that type's descriptor, whose address is fixed at build time. An interface target makes the runtime match the whole method set, then memoise the answer.

open as a page

Why does mutating a struct asserted out of a Go any value leave the boxed value unchanged?

level: middleimportance: should knowfreq 40%

basics

~20 s

A type assertion yields a copy. Storing a struct in an interface copies it behind the interface's data word, and asserting it back out copies those bytes into your variable, so every write you make reaches only that copy.

open as a page

A Go type switch with 20 cases sits on a hot path, defended by a 2 ns/op benchmark — what do you challenge?

level: seniorimportance: nice to knowfreq 30%

basics

~10 s

Challenge what the benchmark fed it. One dynamic type hits one arm with a predicted branch; real traffic mixes kinds, arms naming an interface cost a runtime check, and the interface copies go uncounted.

open as a page