Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Generics in Go
Go 1.18 introduced generics (type parameters), one of the most requested features in Go’s history. Generics allow you to write functions and types that work with any data type while maintaining full type safety at compile time. Before generics, Go developers faced an uncomfortable choice: duplicate code for each type (error-prone), or useinterface{} and lose type safety (runtime panics). Generics solve this by letting you write the code once with a type placeholder, and the compiler fills in the concrete type at each call site — catching type mismatches at compile time, not in production at 3 AM.
Think of generics like a cookie cutter that works with any dough. The cutter (your generic function) defines the shape of the operation, but the dough (the type) can be anything that fits the constraints. Without generics, you needed a separate cutter for each dough type, or you used a “universal” cutter that had no idea what shape it was making (interface{}).
Why Generics?
Before generics, you had two options for writing reusable code:- Use
interface{}: Lose type safety and require type assertions - Code generation: Maintain multiple copies of similar code
Type Parameters
Type parameters are declared in square brackets before the function parameters.Basic Syntax
Simple Generic Function
Multiple Type Parameters
Type Constraints
Constraints define what operations can be performed on type parameters.The any Constraint
any is an alias for interface{} - allows any type.
The comparable Constraint
comparable allows types that support == and != operators.
The constraints Package
The golang.org/x/exp/constraints package provides common constraints:
Custom Constraints
Define your own constraints using interface syntax:Underlying Type Constraint (~)
The~ operator matches types with the same underlying type. This is crucial for real-world code where teams define domain types like type UserID int64 or type Celsius float64. Without ~, those custom types would not satisfy generic constraints even though they behave identically to their underlying type.
Generic Types
You can also define generic structs, interfaces, and type aliases.Generic Structs
Generic Linked List
Generic Map/Dictionary
Common Generic Patterns
Filter, Map, Reduce
Result Type (Error Handling)
Optional Type
Type Inference
Go can often infer type parameters from function arguments:Best Practices
When to Use Generics
Good use cases:- Collection types (stacks, queues, trees)
- Utility functions (filter, map, reduce)
- Type-safe containers
- Algorithms that work on multiple types
- A single concrete type works fine
- Interfaces provide sufficient abstraction
- It makes the code harder to read
Keep Constraints Simple
Zero Values in Generics
Getting the zero value of a generic type is a pattern you will use constantly. Thevar zero T idiom is the standard approach:
Interview Questions
What are generics and why were they added to Go?
What are generics and why were they added to Go?
- Reduce code duplication
- Eliminate the need for type assertions with
interface{} - Enable type-safe container types
- Improve code reusability without sacrificing performance
What's the difference between `any` and `comparable` constraints?
What's the difference between `any` and `comparable` constraints?
anyis an alias forinterface{}and allows any typecomparablerestricts to types that support==and!=operators- Use
comparablewhen you need to compare values (e.g., map keys, finding elements)
What does the ~ operator do in type constraints?
What does the ~ operator do in type constraints?
~ operator matches types with the same underlying type. For example, ~int matches int and any custom type like type MyInt int. Without ~, only the exact type matches.Can generic methods have their own type parameters?
Can generic methods have their own type parameters?
What are the performance implications of generics?
What are the performance implications of generics?
Summary
| Concept | Description |
|---|---|
| Type Parameters | Declared in [T Constraint] before function params |
any | Allows any type (alias for interface{}) |
comparable | Types supporting == and != |
constraints.Ordered | Types supporting <, >, <=, >= |
~T | Matches types with underlying type T |
| Generic Structs | Structs with type parameters |
| Type Inference | Go often infers type parameters automatically |
Interview Deep-Dive
Go resisted adding generics for over a decade. What was the argument against them, and what changed that made the Go team finally add them in 1.18?
Go resisted adding generics for over a decade. What was the argument against them, and what changed that made the Go team finally add them in 1.18?
- The argument against generics was rooted in Go’s core design philosophy: simplicity. The Go team believed that generics would add significant complexity to the language spec, the compiler, error messages, and learning curve. They pointed to C++ templates (which produce notoriously cryptic error messages), Java generics (with type erasure creating confusing behavior), and argued that Go’s existing tools — interfaces and code generation — handled most use cases well enough.
- What changed was a concrete proposal (by Ian Lance Taylor) that demonstrated generics could be added with minimal complexity. The constraints-based approach using interfaces (rather than C++-style concepts or Java-style bounded type parameters) fit naturally into Go’s existing type system. An interface already defines what operations a type supports, so using interfaces as constraints was a natural extension.
- The practical pressure came from two pain points: the massive duplication in the standard library (separate
sort.Ints,sort.Float64s,sort.Stringsfunctions doing the same thing for different types) and the proliferation ofinterface{}in container libraries, which pushed type checking to runtime. Thesync.Pool, for example, requires type assertions on everyGet()call, which is both verbose and a source of runtime panics. - Go’s generics are deliberately limited compared to other languages. No generic methods (only generic functions and types), no specialization, no higher-kinded types. This keeps the implementation simple and error messages readable.
~ (tilde) operator in type constraints, and why was it necessary?Without ~, a constraint like interface{ int } only matches the exact type int. But in Go, you can define type UserID int — a named type with int as its underlying type. Without ~, a generic function constrained to int would not accept UserID, defeating much of the purpose. The ~int constraint matches any type whose underlying type is int, including UserID, type OrderID int, etc. This is essential for practical generics because Go codebases heavily use named types for domain modeling and type safety. Without ~, you would either need to convert every named type to its underlying type before calling generic functions, or the constraint system would be too restrictive to be useful.When should you use generics versus interfaces in Go? Give me a concrete example where each is the better choice.
When should you use generics versus interfaces in Go? Give me a concrete example where each is the better choice.
- Use interfaces when behavior (methods) is what matters, and the specific type is irrelevant. Use generics when the type itself matters — when you need to preserve type identity through the operation, work with the type’s operators (comparison, arithmetic), or avoid the overhead of interface boxing/unboxing.
- Interfaces are better for: dependency injection (your service accepts a
Repositoryinterface, not a concrete type), polymorphic behavior (multiple types need toRender()differently), and API boundaries where the consumer defines what methods it needs. Example: a logging function that acceptsio.Writer— it does not care whether it is writing to a file, buffer, or network connection. The behavior is what matters. - Generics are better for: data structures (a
Stack[T]that preserves the element type so you do not need type assertions on every pop), utility functions operating on slices/maps (Filter[T],Map[T, U],Contains[T comparable]), and algorithms where the type must support specific operators (Min[T constraints.Ordered]). Without generics, you would either duplicate these for every type or useinterface{}with runtime type assertions. - A concrete example where generics win: a
Cache[K comparable, V any]type. With interfaces, everyGet()returnsinterface{}requiring a type assertion. With generics,Get(key K) (V, bool)returns the actual type, and the compiler catches type mismatches at compile time. - The Go team’s guidance: “write code, not types.” Start with concrete types, move to interfaces when you need polymorphism, and only reach for generics when you find yourself duplicating the same logic for multiple types.
func (c *Cache) Get[V any](key string) V, you define type Cache[V any] struct{...} and then func (c *Cache[V]) Get(key string) V. If you truly need a method that operates on a type unknown to the parent struct, extract it into a package-level generic function that takes the struct as a parameter.Show me how you would implement a type-safe, generic Result type in Go that encapsulates either a value or an error. What are the trade-offs compared to Go's standard (value, error) return pattern?
Show me how you would implement a type-safe, generic Result type in Go that encapsulates either a value or an error. What are the trade-offs compared to Go's standard (value, error) return pattern?
- A generic Result type would be
type Result[T any] struct { value T; err error }with constructorsOk[T](v T) Result[T]andErr[T](err error) Result[T], plus methods likeUnwrap() T(panics on error),UnwrapOr(default T) T, andMap(fn func(T) T) Result[T]for chaining. This is inspired by Rust’sResulttype. - Advantages: it makes error handling composable. You can chain operations with
MapandFlatMapwithout nestedif err != nilblocks. It makes impossible states unrepresentable — a Result is either Ok or Err, never both or neither. It works well for functional-style pipelines. - Trade-offs against Go’s standard pattern: it is not idiomatic. The entire Go ecosystem (standard library, third-party packages, tooling) expects
(value, error)returns. A Result type creates friction with every library call — you need wrapper functions to convert between(T, error)andResult[T]. It hides the error handling — one of Go’s explicit design goals is that error handling is visible at every call site. Linters likeerrcheckcannot analyze Result usage. And debugging is harder because the error’s origin is obscured inside the chain. - My recommendation: use the standard
(value, error)pattern for all API boundaries and most code. A Result type can be useful internally for complex transformation pipelines where the chaining genuinely improves readability, but do not expose it in public APIs. The Go community strongly favors explicit error handling, and fighting that convention creates more problems than it solves.
var zero T; return zero. There is no built-in syntax like T{} or default(T) for generic type parameters. This works because Go guarantees every type has a well-defined zero value. For numeric types it is 0, for strings it is "", for pointers/slices/maps it is nil, and for structs it is all fields zero-valued. This pattern appears frequently in generic container types — when a Pop() on an empty stack needs to return something, it returns var zero T along with a false boolean. The idiomatic pattern is func (s *Stack[T]) Pop() (T, bool) where the bool indicates whether a value was available.