skip to content

Interfaces and Implicit Satisfaction

Go's one abstraction mechanism: a set of method signatures that any type satisfies just by having the methods. This group covers how satisfaction is decided, what an interface value actually holds at runtime, and how to design interfaces that stay small — the heart of most Go design discussions.

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

questions

27

In a Go type assertion, how do `v := x.(T)` and `v, ok := x.(T)` differ?

level: juniorimportance: must knowfreq 80%

answer

  1. two forms, only one of them panics
  2. the second result is a boolean
  3. a miss yields the zero value
  4. interface conversion: is X, not Y
  5. comma-ok for anything you did not build

basics

~20 s

The 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 s

A 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 lines
go
func 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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
open as a page

What does calling a method through a Go interface value cost compared with calling it on the concrete type?

level: juniorimportance: must knowfreq 55%

basics

~20 s

An interface method call is indirect: the target comes from the interface value's method table at run time, so the compiler cannot inline it or optimise across it. The lost inlining usually costs far more than the extra jump.

open as a page

What does Go's io.ReadWriteCloser gain by embedding io.Reader, io.Writer and io.Closer?

level: juniorimportance: must knowfreq 64%

basics

~20 s

Embedding folds the other interfaces' method sets into the new one, so io.ReadWriteCloser requires exactly Read, Write and Close and declares nothing extra. Any type with all three methods satisfies it automatically, with no declaration of intent.

open as a page

How does a Go type come to satisfy an interface like io.Writer with no implements keyword?

level: juniorimportance: must knowfreq 72%

basics

~20 s

A Go type satisfies an interface just by having every method the interface lists, with matching names and signatures. Nothing is declared anywhere: the compiler checks the match at the point where the value is assigned to the interface.

open as a page

What two parts does a Go interface value hold, and when is it equal to nil?

level: juniorimportance: must knowfreq 66%

basics

~20 s

A Go interface value holds two halves: a dynamic type and a value of that type. It is nil only when both halves are empty. An interface that has been given a type is non-nil even if the value stored is a nil pointer.

open as a page

In a Go type switch `switch v := x.(type)`, what type does v have in each case?

level: middleimportance: must knowfreq 68%

basics

~20 s

A case naming exactly one type gives v that type. In a case listing several types, and in default, v keeps the static type of x, which is the interface type. Cases are tried in source order.

open as a page

Why does Go idiom say to accept interfaces and return concrete types?

level: middleimportance: must knowfreq 72%

basics

~20 s

An interface parameter lets a function work with any implementation, including a stub in a test. Returning the concrete type keeps every field and method reachable for the caller, who can then narrow to whatever interface the calling code actually needs.

open as a page

Why is a Go error non-nil when the function returned a nil *TransportError pointer?

level: middleimportance: must knowfreq 74%

basics

~20 s

Returning a concrete pointer through an error return converts it into an interface value, which records the dynamic type *TransportError. That type half is filled even though the pointer is nil, so the caller's nil check on the error is always false.

open as a page

After wrapping each net.Conn in a session recorder, an SSH bastion's throughput collapsed. What did the wrapper break?

level: seniorimportance: must knowfreq 52%

basics

~20 s

The wrapper's dynamic type is no longer the concrete connection, so probes for optional interfaces such as io.ReaderFrom now fail and the kernel-assisted copy path is gone. Every byte moves through a user-space buffer instead. Nothing reports an error.

open as a page

Your function takes an io.Writer. How do you detect at run time that it also implements io.ReaderFrom, and what must you do when it does not?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Use the comma-ok type assertion: rf, ok := w.(io.ReaderFrom). If ok is true, call rf.ReadFrom for the fast path; if not, fall back to ordinary Write calls. An optional interface is an optimisation, so the fallback is mandatory.

open as a page

What is `any` in Go, and what can you do with a value whose static type is `any`?

level: middleimportance: should knowfreq 58%

basics

~20 s

any is a predeclared alias for interface{} (Go 1.18), so both spellings are one identical type. It declares no methods, so every type satisfies it, and an any value must be type-asserted before you can use it.

open as a page

Why can storing a concrete value in a Go interface variable allocate on the heap?

level: middleimportance: should knowfreq 48%

basics

~20 s

An interface value holds a pointer to its data, so a value that is not pointer-shaped must be copied somewhere that pointer can point at. If the compiler cannot prove the copy stays in the frame, it goes on the heap.

open as a page

What does the declaration var _ io.Writer = (*Blob)(nil) do in a Go source file?

level: middleimportance: should knowfreq 44%

basics

~20 s

It is a compile-time assertion that *Blob satisfies io.Writer. If a method is missing or a signature drifts, the build fails on that line instead of in some consumer's package. It costs nothing at run time.

open as a page

In Go, why can a package define an interface over a dependency type it cannot modify?

level: middleimportance: should knowfreq 50%

basics

~20 s

Because satisfaction is implicit: the dependency's type never names any interface, so any package may declare an interface listing methods that type already has and use the type as that interface. The only import points from the consumer to the dependency.

open as a page

Which optional interfaces does io.Copy probe before falling back to its buffer loop, and in what order?

level: middleimportance: should knowfreq 48%

basics

~20 s

io.Copy first asks whether the source implements io.WriterTo and calls src.WriteTo(dst); otherwise whether the destination implements io.ReaderFrom and calls dst.ReadFrom(src); only if neither matches does it loop through its own buffer, 32 KiB by default.

open as a page

A webhook handler asserts payload["retry_count"].(int) on JSON decoded into map[string]any and the log shows "interface conversion: interface {} is float64, not int" — why, and what do you change?

level: seniorimportance: should knowfreq 45%

basics

~20 s

encoding/json decodes every JSON number into float64 when the target is map[string]any, so an assertion to int never matches and the one-result form panics. Use the comma-ok form on float64 and answer 400, or decode into a typed struct.

open as a page

A Go thumbnailing service heap-allocates its pixel buffer on every resize and `go build -gcflags=-m` blames an interface method call. How do you fix it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The callee behind an interface is invisible to the compiler, so escape analysis assumes it retains the buffer and heap-allocates the backing array. Take the concrete type in the inner function, keep the interface at the package boundary, and re-measure.

open as a page

An exported 12-method interface in your Go proxy package is used by every downstream service. How do you shrink it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Measure which methods each consumer actually calls, then add small one- and two-method interfaces beside the big one and move parameters onto them. Redeclare the wide interface as an embedding of those, so existing code still compiles, then delete it.

open as a page

Your Go RPC transport reports an error on every successful call, logged as <nil>; how do you diagnose it?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Log the dynamic type alongside the value with %T. If it names a concrete pointer type while %v prints nothing, an interface is holding a nil concrete pointer, so the nil check can never be false. Fix the function that produced the error, not the caller.

open as a page

An interface seam in your Go library's hot path costs an allocation per call, but other teams substitute fakes through it. How do you decide whether to remove it?

level: principalimportance: should knowfreq 24%

basics

~20 s

Decide with numbers and with the consumers in view. Measure what the seam costs end to end, then prefer specialising the internals while keeping the published interface, over removing a seam other teams already build on.

open as a page

As the author of a shared Go library, how do you decide whether to export an interface at all, and how wide?

level: principalimportance: should knowfreq 33%

basics

~20 s

Default to exporting concrete types; export an interface only where several real implementations must exist or your own API takes it as a parameter. Size it by what callers call, since an exported interface can never change without breaking implementers.

open as a page

Should a Go library advertise an optional interface for callers to probe, and what does shipping one commit you to?

level: principalimportance: should knowfreq 30%

basics

~20 s

Ship one only when the capability is optional and callers have a correct fallback. Once callers type-assert for it, its method set is frozen: adding a method breaks implementers, and dropping it degrades callers silently, with no compile error.

open as a page

Why does net/http declare Handler as a one-method interface and also provide HandlerFunc?

level: middleimportance: nice to knowfreq 45%

basics

~20 s

Because http.Handler requires just ServeHTTP, a plain function can implement it: http.HandlerFunc is a named function type with a ServeHTTP method that calls itself. Keeping an interface at one method is what makes such a function adapter possible.

open as a page

Why probe with an anonymous interface literal like interface{ Flush() error } instead of a named interface type?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

An interface literal states the required method set inline, so you can probe for a capability no exported interface names, without importing the package that would. The cost: the signature must match exactly, and nothing checks it.

open as a page

When are two Go interface values equal with ==, and when does that comparison panic?

level: middleimportance: nice to knowfreq 32%

basics

~20 s

Two interface values are equal only when their dynamic types are identical and their values compare equal for that type. Different dynamic types are never equal. If the shared dynamic type is not comparable, such as a slice or map, the comparison panics at runtime.

open as a page

When can the Go compiler turn an interface method call into a direct, inlinable call?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

When it can prove which concrete type the interface holds, usually because the value was built nearby or became visible after inlining. A profile-guided build can also guard a hot call site with a type check and call the dominant type directly.

open as a page

Your Go library changed an exported type's method signature; the library builds green but consumers break. Why?

level: seniorimportance: nice to knowfreq 34%

basics

~20 s

Nothing in the library records that the type satisfied an interface. Go checks satisfaction only where a value is used as that interface, so if the library never does that, the mismatch first appears when a consumer compiles.

open as a page