# Advanced Go Interfaces in 2026: Composition, Type Assertions and Interview Questions > Master Go interface composition, type assertions, and self-referential generics from Go 1.26. Includes practical examples and technical interview questions. - Published: 2026-09-10 - Updated: 2026-09-10 - Author: Anthony Fillion-Maillet - Tags: go, interfaces, type-assertions, generics, go-1.26 - Reading time: 9 min --- Go interfaces define behavior without prescribing implementation, making them central to writing flexible, testable code. Go 1.26 extends this power with self-referential generics, new reflection iterators, and type-safe error handling via `errors.AsType`. This guide covers interface composition, type assertions, embedding patterns, and the questions that come up in technical interviews. > **Go 1.26 Interface Enhancement** > > Generic types can now reference themselves in their type parameter list: `type Adder[A Adder[A]] interface { Add(A) A }`. This F-bounded polymorphism pattern enables interfaces that constrain return types to the implementing type itself. ## Interface Composition and Embedding Patterns Interface embedding combines multiple interfaces into a single contract. The result is a new interface that requires all methods from its embedded interfaces. This pattern avoids duplication and creates clear abstraction boundaries. ```go // interfaces.go type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } type Closer interface { Close() error } // Composed interface embedding three interfaces type ReadWriteCloser interface { Reader Writer Closer } ``` Any type implementing `Read`, `Write`, and `Close` methods automatically satisfies `ReadWriteCloser`. The [Go standard library](https://pkg.go.dev/io) uses this pattern extensively in the `io` package. A common interview question asks candidates to explain why Go prefers small, focused interfaces. The answer lies in composability: `io.Reader` appears in hundreds of functions because it demands exactly one method. Larger interfaces create tighter coupling and reduce reuse. ## Type Assertions: Syntax and Safety Type assertions extract a concrete type from an interface value. The two-value form prevents panics by returning a boolean indicating success. ```go // assertions.go func processValue(v interface{}) { // Two-value assertion: safe, no panic if str, ok := v.(string); ok { fmt.Printf("String value: %s\n", str) return } // Type switch for multiple types switch val := v.(type) { case int: fmt.Printf("Integer: %d\n", val) case float64: fmt.Printf("Float: %.2f\n", val) case []byte: fmt.Printf("Bytes: %x\n", val) default: fmt.Printf("Unknown type: %T\n", val) } } ``` Type switches handle multiple possible types cleanly. Each case binds `val` to the asserted type within its block, eliminating the need for separate assertions. > **Panic Risk** > > Single-value assertions like `str := v.(string)` panic if the assertion fails. Production code should always use the two-value form or a type switch. Interviewers often ask about the performance of type assertions. The runtime performs a single comparison of type descriptors, making assertions inexpensive. The cost increases with interface values that wrap pointers to large structs due to indirection, but the assertion itself remains O(1). ## Self-Referential Generics in Go 1.26 Go 1.26 introduced self-referential type parameters, enabling interfaces where methods must return the implementing type. This pattern, sometimes called [F-bounded polymorphism](https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification), solves a long-standing limitation. ```go // builder.go // Self-referential interface: methods return the same type type Builder[B Builder[B]] interface { WithName(name string) B WithAge(age int) B Build() string } type PersonBuilder struct { name string age int } func (p PersonBuilder) WithName(name string) PersonBuilder { p.name = name return p } func (p PersonBuilder) WithAge(age int) PersonBuilder { p.age = age return p } func (p PersonBuilder) Build() string { return fmt.Sprintf("%s, %d years old", p.name, p.age) } // Generic function using the self-referential constraint func configure[B Builder[B]](b B, name string, age int) string { return b.WithName(name).WithAge(age).Build() } ``` The constraint `Builder[B]` ensures that `WithName` and `WithAge` return `B`, not a generic `Builder`. Without this, the return type would be the interface, losing the concrete type information and breaking method chaining. Math types benefit from this pattern. An `Addable[A Addable[A]]` interface ensures `Add(A) A` returns the same numeric type, preventing accidental mixing of `BigInt` and `Decimal` values. ## errors.AsType: Type-Safe Error Unwrapping Go 1.26 added [`errors.AsType`](https://pkg.go.dev/errors#AsType), replacing the pointer-dance pattern of `errors.As`. The generic version returns the unwrapped error directly. ```go // errors_handling.go type ValidationError struct { Field string Message string } func (e *ValidationError) Error() string { return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message) } func handleError(err error) { // Go 1.26: generic type-safe unwrapping if valErr, ok := errors.AsType[*ValidationError](err); ok { log.Printf("Validation error on field %s\n", valErr.Field) return } // Before Go 1.26: pointer-based unwrapping // var valErr *ValidationError // if errors.As(err, &valErr) { ... } log.Printf("Unexpected error: %v\n", err) } ``` The new API eliminates the separate variable declaration and makes the target type explicit in the function call. Error chains are searched the same way as before; only the interface changed. ## Reflection Iterators for Interface Inspection Go 1.26 added iterator methods to the `reflect` package. `Type.Methods()` and `Value.Methods()` return iterators for ranging over methods, replacing index-based loops. ```go // reflection.go import "reflect" func inspectInterface(v interface{}) { t := reflect.TypeOf(v) // Go 1.26: iterator-based method inspection fmt.Printf("Type %s methods:\n", t.Name()) for method := range t.Methods() { fmt.Printf(" %s: %s\n", method.Name, method.Type) } // For struct fields (also new in 1.26) if t.Kind() == reflect.Struct { for field := range t.Fields() { fmt.Printf(" Field: %s (%s)\n", field.Name, field.Type) } } } ``` The iterator pattern aligns with Go 1.23's range-over-function feature. Code becomes more readable, and there is no performance penalty: iterators yield values lazily. ## Interface Satisfaction at Compile Time Go checks interface satisfaction at compile time when assigning a concrete value to an interface variable. Explicit checks using blank identifier assignments catch errors early in large codebases. ```go // compile_check.go type Storage interface { Save(key string, data []byte) error Load(key string) ([]byte, error) Delete(key string) error } type FileStorage struct { basePath string } // Compile-time check: fails if FileStorage misses a method var _ Storage = (*FileStorage)(nil) func (f *FileStorage) Save(key string, data []byte) error { path := filepath.Join(f.basePath, key) return os.WriteFile(path, data, 0644) } func (f *FileStorage) Load(key string) ([]byte, error) { path := filepath.Join(f.basePath, key) return os.ReadFile(path) } func (f *FileStorage) Delete(key string) error { return os.Remove(filepath.Join(f.basePath, key)) } ``` The line `var _ Storage = (*FileStorage)(nil)` compiles only if `*FileStorage` satisfies `Storage`. This pattern catches missing methods immediately rather than at runtime when a value is assigned. ## The Empty Interface and Type Constraints The empty interface `interface{}` accepts any value, while `any` is its alias since Go 1.18. Generic constraints provide compile-time type safety without runtime assertions. ```go // constraints.go import "golang.org/x/exp/constraints" // Generic function with numeric constraint func Sum[T constraints.Integer | constraints.Float](values []T) T { var total T for _, v := range values { total += v } return total } // Comparable constraint for map keys func Contains[K comparable, V any](m map[K]V, key K) bool { _, exists := m[key] return exists } ``` Preferring generics over `interface{}` eliminates type assertions and catches type mismatches at compile time. The tradeoff is added complexity in function signatures, so generics fit best when a function genuinely operates on multiple types. ## Technical Interview Questions on Go Interfaces Interviewers test interface knowledge at multiple levels. Here are common questions with concise answers. **Q: What happens when you call a method on a nil interface value versus a nil concrete value inside an interface?** A nil interface has no type and no value; calling any method panics. A non-nil interface holding a nil pointer has a type; the method executes with a nil receiver. This behavior allows patterns like `(*bytes.Buffer)(nil).String()` returning an empty string. **Q: How does interface comparison work?** Two interface values are equal if they have the same dynamic type and equal dynamic values. Comparing interfaces with uncomparable types (slices, maps, functions) panics at runtime. **Q: When should a method use a pointer receiver versus a value receiver?** Pointer receivers allow mutation and avoid copying large structs. Value receivers are safe for concurrent use and work with both values and pointers. If any method needs a pointer receiver, all methods on that type should use pointer receivers for consistency, since only `*T` satisfies an interface requiring a pointer-receiver method. ```go // receiver_rules.go type Counter struct { count int } // Pointer receiver: modifies the struct func (c *Counter) Increment() { c.count++ } // Value receiver: read-only operation func (c Counter) Value() int { return c.count } type Incrementer interface { Increment() } func main() { var c Counter // var i Incrementer = c // Compile error: Counter lacks Increment var i Incrementer = &c // OK: *Counter has Increment i.Increment() } ``` **Q: Explain the "accept interfaces, return structs" principle.** Functions accepting interfaces decouple from concrete implementations, enabling testing with mocks. Returning concrete types gives callers full access to the type's methods without type assertions. Returning an interface hides future method additions behind a type assertion. For more Go interview preparation, see the [Go concurrency questions](/technologies/go/interview-questions/concurrency-patterns) module and the [testing module](/technologies/go/interview-questions/testing). > **Interview Tip** > > When asked to design an interface, start with the single-method case. Expand only when multiple methods are always called together. The standard library's `Stringer`, `Reader`, and `Handler` interfaces each define one method. ## What Senior Go Developers Should Know About Interfaces - Interface composition through embedding creates flexible contracts without inheritance hierarchies. Keep interfaces small: one to three methods covers most use cases. - Type assertions and type switches extract concrete types safely. Always use the two-value form or a switch in production code to avoid panics. - Go 1.26's self-referential generics enable builder patterns and math types where methods must return the implementing type. - `errors.AsType` simplifies error unwrapping with a generic, type-safe API. Use it in new code targeting Go 1.26 or later. - Compile-time interface checks with `var _ Interface = (*Type)(nil)` catch missing methods before tests run. - Prefer generics over `interface{}` when type safety at compile time outweighs the complexity in signatures. - The "accept interfaces, return structs" guideline keeps APIs flexible for callers while exposing full functionality. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/go/go-interfaces-advanced-composition-type-assertions-2026