Type Assertions, Switches, Embedding, and Satisfaction Checks
How to extract concrete values from interface variables, branch on dynamic types, compose interfaces from smaller ones, and verify interface implementation at compile time in Go.
Once you understand how to declare and implement interfaces, learning how to work with them effectively in real code is important. An interface variable can hold values of many different concrete types, which is powerful but also raises practical questions: How do you get the original concrete value back out of an interface? How do you handle a situation where an interface could contain one of several types? How do you build larger interfaces from small, reusable pieces? And how can you be absolutely certain a type satisfies an interface before the program ever runs?
This page covers the four mechanisms that answer those questions: type assertions, type switches, interface embedding, and compile-time interface satisfaction checks. Each solves a distinct problem, and together they form the everyday toolkit for writing flexible, safe Go code with interfaces.
Extracting Concrete Values with Type Assertions
An interface value stores two things under the hood: the concrete value and its dynamic type. Sometimes you need to retrieve that concrete value to access fields or methods that exist on the original type but not on the interface. A type assertion is the operation that does that — it says “I believe this interface holds a value of a specific type; give me that value.”
Basic syntax and behavior
A type assertion has the form x.(T), where x is an expression of interface type and T is the concrete type you expect. The result is the value of type T that was stored inside the interface.
var i interface{} = "hello"
s := i.(string)
fmt.Println(s) // hello
If the assertion is wrong — the interface does not hold a value of that type — the program panics at runtime. This is a hard stop, like accessing a nil pointer or indexing out of bounds, and it is intentional: an incorrect type assertion is a programmer error, not a recoverable user mistake.
var i interface{} = 42
s := i.(string) // panic: interface conversion: interface {} is int, not string
Panic on failed assertion:
A single-return-value type assertion (i.(T)) will panic if the dynamic type inside the interface is not T. Never use this form unless you are absolutely certain of the concrete type — for example, inside a code path that already validated the type via a type switch or an ok check.
The safe comma-ok pattern
To test an assertion without risking a panic, use the two-value form:
s, ok := i.(string)
if ok {
fmt.Println("string:", s)
} else {
fmt.Println("not a string")
}
The second return value is a bool that reports whether the assertion succeeded. If it failed, the first return value is the zero value of the target type. This pattern is named after the ok variable used idiomatically, but the variable name is your choice.
Idiomatic Go:
The v, ok := x.(T) pattern is the standard, safe way to extract concrete values from interface variables in production Go code. It reads naturally: "value, okay?" and lets you handle the failure case explicitly.
In practice, you'll often see this combined with a short if statement:
if str, ok := data.(string); ok {
fmt.Println("length:", len(str))
}
A common mistake is performing a type assertion on an interface that is nil. A nil interface has no concrete type, so any assertion will panic or return ok == false even if you assert the correct type. Always check for nil before asserting if the interface might have been left uninitialized.
var x MyInterface // nil interface
if str, ok := x.(string); ok {
fmt.Println(str) // never runs; ok is false
}
The nil interface trap:
A type assertion on a nil interface does not return an error — it simply returns false in the ok form. The danger is when you use the single-value form, which panics. Always use the ok form when the interface might be nil.
When you need type assertions in real code
Type assertions appear whenever you pass data through the empty interface (interface{}) and later need the original type back. This happens in JSON unmarshaling (where interface{} stores arbitrary parsed values), in custom error handling (checking if an error is a specific type to extract details), and in callback systems where a function accepts interface{} for flexibility.
A classic example: a function that accepts error but wants to examine the concrete error type to decide how to respond.
func handleErr(err error) {
if netErr, ok := err.(net.Error); ok {
if netErr.Timeout() {
fmt.Println("request timed out")
return
}
}
fmt.Println("generic error:", err)
}
Here, the error interface does not expose Timeout(), but net.Error does. The assertion lets the function react differently to network-specific errors without changing the error interface itself.
Branching on Dynamic Type with Type Switches
A type assertion checks for one specific type. When an interface could hold any of several types, a series of if-else assertions gets verbose. A type switch is a cleaner, more readable way to branch on the dynamic type inside an interface.
How a type switch differs from an assertion
A type switch uses the syntax switch x.(type) inside a switch statement. The cases list concrete types (or nil), and the code runs for the matching case. The key difference from an assertion is that a type switch compares against multiple types in a single control structure, and it provides a type-safe variable scoped to each case.
func describe(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("int: %d\n", v)
case string:
fmt.Printf("string: %s\n", v)
case bool:
fmt.Printf("bool: %t\n", v)
default:
fmt.Printf("unknown type: %T\n", v)
}
}
Each case binds v to the concrete value of that type. In the int case, v is of type int; in the string case, v is a string. There is no risk of a panic because the switch itself does the type check.
If you don't need the value inside the case, you can omit the variable:
switch i.(type) {
case int:
fmt.Println("it's an integer")
case string:
fmt.Println("it's a string")
}
Type switches only work on interface types:
The expression inside switch x.(type) must be of interface type. You cannot use a type switch on a concrete variable — the compiler will reject it.
The nil case
A type switch can include a case nil: branch, which runs when the interface value itself is nil. This is different from the interface holding a nil pointer; case nil matches only a truly nil interface.
var p *os.File = nil
var w io.Writer = p
switch w.(type) {
case nil:
fmt.Println("nil interface")
default:
fmt.Println("non-nil interface") // this runs, because w holds a nil *os.File, which is not a nil interface
}
This behavior trips up many newcomers: a nil concrete pointer inside a non-nil interface is not the same as a nil interface. A type switch case nil checks the interface itself, not the value inside it.
Nil pointer inside non-nil interface:
Assigning a nil pointer of a concrete type to an interface does not make the interface nil. The interface value still has a dynamic type (the pointer type) set. A method call on that interface will reach the method, and if that method does not handle a nil receiver, it will panic. Always check for nil receivers inside methods.
Real-world use of type switches
Type switches shine in code that needs to handle a variety of input types uniformly. A logging package might accept interface{} and use a type switch to format different types appropriately. A serialization library might walk a parsed JSON structure and use a type switch to distinguish objects, arrays, strings, and numbers.
The standard library's fmt package uses type switches internally. When you call fmt.Println(x), the formatting logic uses a type switch to decide how to represent x as a string, falling back to reflection for custom types.
Composing Interfaces Through Embedding
A single, large interface can be restrictive. It forces every implementation to satisfy the entire set of methods, even when a caller only needs one or two of them. Go encourages small interfaces — often just one method — and then composes larger interfaces by embedding smaller ones inside a new interface declaration.
Embedding syntax and semantics
You embed an interface by listing its name inside another interface's method list, exactly as you would embed a struct inside another struct. The outer interface inherits all the methods of the embedded interface.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}
ReadWriter has two methods: Read and Write. Any type that implements ReadWriter must implement both. You do not repeat the method signatures — embedding reuses them.
Embedding is not inheritance:
Embedding an interface does not create a parent-child relationship; it is a composition of method sets. The outer interface does not "inherit" anything beyond the method requirements. There is no shared state, no constructor chaining — just a larger set of required methods.
You can embed multiple interfaces, and the embedding can be transitive. The standard library's io package is built almost entirely this way:
type Closer interface {
Close() error
}
type ReadCloser interface {
Reader
Closer
}
type WriteCloser interface {
Writer
Closer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
Each interface is small, yet by combining them you can express exactly the contract you need. A function that reads and closes a resource can accept io.ReadCloser without imposing Write.
Why small, composable interfaces matter
When you define a function that accepts an interface, the interface should require only what the function actually uses. A large interface forces every caller to provide a full implementation, even if the function only ever calls one method. Small interfaces keep the contract minimal and make it easy for callers to satisfy.
Embedding lets you build the precise interface for a given context from pre-existing small pieces. A type that already implements Reader and Closer automatically satisfies ReadCloser with no extra code. This is the design principle behind much of Go's standard library.
func copyAndClose(dst Writer, src ReadCloser) error {
// ...
}
Here, copyAndClose declares exactly what it needs: something to write to and something to read from that can be closed. The caller doesn't need to know about any other methods.
A common mistake: conflicting methods
If you embed two interfaces that both declare a method with the same signature, there is no conflict — it is simply the same method requirement. The outer interface requires it once. But if two embedded interfaces declare methods with the same name but different signatures (for example, Foo(int) and Foo(string)), the outer interface is invalid and the compiler will reject it. This is rare in practice because interfaces with identically named methods usually have the same signature by convention.
Method signature clashes:
Go does not allow method overloading, so embedding two interfaces with methods of the same name but different parameter types is a compile-time error. The compiler will say "duplicate method". This is almost never a problem in idiomatic Go because interface methods are typically named uniquely and consistently.
Ensuring a Type Satisfies an Interface at Compile Time
Go's interface satisfaction is implicit — a type implements an interface simply by having the right methods. That is elegant but can be unsettling: if you remove a method from a type, or rename it, the type silently stops satisfying the interface, and you won't know until some code that expects that interface fails to compile (possibly in a different package, far from the change). A compile-time satisfaction check is a one-liner that forces the compiler to verify that a type still implements an interface right at the point of declaration.
The var _ Interface = (*Type)(nil) pattern
The standard idiom looks like this:
type MyReader struct {
// fields
}
func (m *MyReader) Read(p []byte) (n int, err error) {
// implementation
}
// Compile-time check: *MyReader implements io.Reader
var _ io.Reader = (*MyReader)(nil)
The line var _ io.Reader = (*MyReader)(nil) does several things. It declares a variable of type io.Reader, assigns it a nil pointer of type *MyReader, and discards the variable by naming it _. The compiler will check that *MyReader satisfies io.Reader. If it doesn't — for example, if Read were removed or its signature changed — compilation fails immediately with a clear error message near this line.
The (*MyReader)(nil) expression creates a nil pointer of the concrete type and assigns it to the interface variable. This is safe because you never call methods on the variable (it's immediately discarded). All that matters is the compile-time type check.
Instant feedback on interface conformance:
Place a satisfaction check line right after a type declaration to get immediate compiler feedback if the type ever drifts from the interface it's supposed to implement. This is especially valuable in large codebases where the type and its interface might be in different packages.
Checking both pointer and value receivers
Some types implement an interface only via a pointer receiver, or only via a value receiver. The check must match the receiver kind. If *MyReader implements io.Reader but MyReader does not, then var _ io.Reader = MyReader{} would fail. Use the appropriate expression.
If you want to enforce that both the value and the pointer satisfy the interface (or assert that the value does not), you can write two separate checks:
var _ io.Reader = MyReader{} // might fail if Read has a pointer receiver
var _ io.Reader = (*MyReader)(nil) // passes if *MyReader implements io.Reader
In practice, you usually only check the receiver type that is intended to satisfy the interface.
Where this pattern is used in production
The Go standard library uses this pattern extensively. For example, bytes.Buffer includes checks like var _ io.Writer = (*Buffer)(nil) and var _ io.Reader = (*Buffer)(nil) to ensure it always satisfies those core interfaces. HTTP handlers, custom error types, and logging implementations all employ the same idiom.
This pattern also serves as documentation: anyone reading the source sees at a glance which interfaces a type is intended to fulfill. It's a declaration of intent enforced by the compiler.
Summary
The four tools covered here — type assertions, type switches, interface embedding, and satisfaction checks — form a complete workflow for using interfaces beyond simple method dispatch. Type assertions and switches let you recover concrete types safely when you need them; embedding lets you build the exact contract you need from tiny, reusable pieces; and satisfaction checks give you compile-time certainty that your types stay in sync with the interfaces they claim to implement.