skip to content

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