In a Go type assertion, how do `v := x.(T)` and `v, ok := x.(T)` differ?
answer
- two forms, only one of them panics
- the second result is a boolean
- a miss yields the zero value
- interface conversion: is X, not Y
- comma-ok for anything you did not build
basics
~20 sThe one-result form v := x.(T) panics if the interface value x does not hold a T. The comma-ok form v, ok := x.(T) never panics: on a miss it yields T's zero value and sets ok to false.
solid answer
~50 sA type assertion `x.(T)` asks what the interface value `x` is holding right now. In the one-result form, `v := x.(T)`, a miss is a runtime panic: `interface conversion: interface {} is string, not int`. In the comma-ok form, `v, ok := x.(T)`, `v` gets `T`'s zero value and `ok` is false, with no panic, so that is the form to use on anything you did not construct yourself — decoded input, plugin values, a cache of `any`. Two things are still checked at compile time in both forms: `x` must be of interface type, and if `T` is a concrete type it must implement `x`'s interface, otherwise the compiler rejects it as an impossible assertion. If `T` is an interface, the assertion succeeds when the dynamic type's method set contains `T`'s methods. A nil interface value matches nothing, so it fails both forms.
code
go · 8 linesfunc describe(x any) {
if s, ok := x.(string); ok {
fmt.Println("string of length", len(s))
return
}
// one-result form: panics unless the dynamic type is exactly int
fmt.Println("doubled:", x.(int)*2)
}go deeper
Be ready to write both forms from memory and say exactly what each does on a miss: a panic, versus the zero value with a false boolean. Knowing to bind and test in one if statement is expected.
Explain the compile-time rules as well: the operand must be an interface, and a concrete target type must implement it. Be able to say what asserting to an interface type checks, and what a nil interface does.
Show the judgment about which form belongs where. An interviewer expects you to treat the one-result form as an assertion of an invariant you control, and the comma-ok form as the default at any boundary carrying data you did not construct.
Own the boundary policy: which layers are allowed to hold values of type any at all, and where untyped data must be turned into concrete types so that assertions disappear from business code entirely.
## What a type assertion actually does An interface value in Go holds a dynamic type together with a value of that type. A type assertion is the operation that asks about that dynamic type and, when it matches, copies the concrete value back out. It is written `x.(T)` where `x` must be an expression of **interface** type. It is not a conversion. `float64(i)` is a conversion: a compile-time-checked change of representation. `x.(int)` changes nothing about the value; it is a **run-time check** on what the interface currently holds, plus a copy of the concrete value out of the interface. ## The two forms ```go var x any = "hello" s := x.(string) // ok: s == "hello" n := x.(int) // PANIC: interface conversion: interface {} is string, not int n, ok := x.(int) // no panic: n == 0, ok == false ``` - **One result (`v := x.(T)`)** — if the dynamic type is not `T`, the program panics with an `interface conversion` message that names both the type that was there and the type you asked for. That message is deliberately precise, and it is usually enough to fix the bug from a log alone. - **Two results (`v, ok := x.(T)`)** — the assertion cannot fail loudly. On a miss `v` is the zero value of `T` (`0`, `""`, `nil` for a pointer or slice) and `ok` is false. You must actually test `ok`: ignoring it and using `v` silently processes a zero value, which is the quiet cousin of the panic. The usual idiom binds and tests in one statement, keeping `v` scoped to the branch that proved it valid: ```go if s, ok := x.(string); ok { // s is a string only in here } ``` ## When the compiler rejects it before you ever run Two compile-time rules apply to both forms. 1. **The operand must be an interface.** `i := 3; i.(int)` does not compile — there is no dynamic type to inspect. If you find yourself wanting to assert on a concrete value, you already know its type. 2. **A concrete `T` must implement the operand's interface.** If `x` is an `io.Reader` and `T` is a concrete type with no `Read` method, no dynamic type could ever satisfy both, and the compiler reports an impossible type assertion. This rule does **not** apply when `T` is an interface type: any concrete type could in principle implement two unrelated interfaces, so the check is deferred to run time. ## Asserting to an interface type `T` does not have to be concrete. `x.(io.Writer)` succeeds when the dynamic type's method set includes `Write` with the right signature. This is how you move from a narrow interface to a wider one — from an `any` out of a decoded map to something you can call methods on, or from one interface to a richer one — and the comma-ok form is the only sane way to write it, because failure is expected rather than exceptional. ## Nil interface values A nil interface has no dynamic type, so it matches nothing. The comma-ok form returns the zero value with `ok` false; the one-result form panics with `interface conversion: interface is nil, not int`. This bites in map lookups: `m["missing"]` on a `map[string]any` returns a nil interface, not an error, so `m["missing"].(int)` panics on a key that simply was not there. ## Choosing between them Use the one-result form only where a miss is a programming error you want to crash on immediately, and where you can see from the surrounding code that the type is guaranteed — for example straight after building the value yourself in the same function. Use the comma-ok form everywhere the value crossed a boundary: JSON, a registry of `any`, a callback's argument, a value read from a shared map. "It is always an int in practice" is not a guarantee the compiler can hold you to, and the panic will happen in production rather than in your test. When there are several types to handle, do not chain comma-ok assertions; that is precisely what a type switch is for, and it reads better and evaluates the dynamic type once.
- What do the two forms do when x is a nil interface value?Both fail, because a nil interface has no dynamic type to match. The comma-ok form gives `T`'s zero value with `ok` false; the one-result form panics with `interface conversion: interface is nil, not int`. This is the common map-lookup trap: a missing key in a `map[string]any` yields a nil interface, so the one-result assertion panics on absence, not just on a wrong type.
- When does the compiler reject a type assertion outright?Two cases. First, when the operand is not of interface type — you cannot write `i.(int)` on an `int` variable, since there is no dynamic type to inspect. Second, when `T` is a concrete type that does not implement the operand's interface: no dynamic type could satisfy both, so it is reported as an impossible type assertion. If `T` is itself an interface, the check is left to run time.
- Is a type assertion the same thing as a conversion such as float64(i)?No. A conversion is resolved entirely at compile time and changes the value's representation. An assertion is a run-time question about what an interface is currently holding; it either copies the concrete value back out unchanged or fails. That is also why `x.(int)` on an interface holding a `float64` fails rather than rounding — no numeric conversion is implied.
saying these in an interview costs you the question
- Says the comma-ok form also panics on a mismatch
- Thinks x.(T) converts the value like float64(i) does
- Ignores ok and uses the zero value anyway
- Believes a nil interface satisfies every assertion
- Tries to assert on a concrete variable such as an int