Generics
Go added support for generics (also known as type parameters) in version 1.18, allowing you to write functions and data structures that can work with multiple types while providing compile-time type safety. Generics eliminate the need to rely on interfaces or duplicated code for common patterns. Below is an in-depth look at how generics work in Go, accompanied by examples demonstrating their usage and constraints.
1. Basic Concepts of Generics in Go
Type Parameters
Go allows functions, methods, and types (e.g., structs) to have type parameters in square brackets
[ ]
.Example:
func Foo[T any](param T) { ... }
.
Constraints
A type parameter must satisfy a constraint, which specifies what operations or underlying types are permitted.
Common constraints are found in the
constraints
package, for exampleconstraints.Ordered
(for types supporting<
,>
comparisons).
any
Keywordany
is a predeclared identifier that means “no constraint” (equivalent tointerface{}
).
Compile-Time Type Safety
Generic code is checked at compile time, ensuring that operations on type parameters are valid for the provided constraints.
2. Generic Functions
A generic function in Go has one or more type parameters in square brackets. For example:
Example 1: A Generic Min
Function
Explanation:
The function
Min[T constraints.Ordered]
declares a type parameterT
constrained byconstraints.Ordered
.This means
T
can be any type that supports<
and>
(e.g., all numeric types, strings).The function can be called with
int
,float64
,string
, etc.
Example 2: A Generic Slice Filter Function
Explanation:
Filter[T any]
accepts a slice of typeT
and a function that checks if an element should be kept.We can call
Filter
with slices ofint
andstring
without duplicating code.
3. Generic Types
You can also define structs or other composite types with type parameters, making them reusable for multiple underlying types.
Example 3: A Generic Pair Type
Explanation:
Pair[A, B any]
defines two type parameters,A
andB
, each constrained byany
.This allows storing different types in the same data structure without sacrificing type safety.
Example 4: A Generic Stack
Explanation:
Stack[T any]
is a generic type that stores elements of typeT
.Methods like
Push
andPop
operate on the stack with a known typeT
.This eliminates the need for interface-based stacks or code duplication.
4. Multiple Constraints
Constraints can also combine interfaces or types.
Example 5: A Generic Add
Function with Numeric Constraints
Explanation:
type Numeric
is defined as a union ofconstraints.Integer
andconstraints.Float
.Add[T Numeric]
can be used with either an integer or a floating-point type.
5. Using Type Inference
When calling generic functions, Go can often infer the type parameter from the arguments.
Example 6: Type Inference
Explanation:
We didn’t explicitly write
Multiply[int]
orMultiply[float64]
; the compiler deduced type from the arguments.
6. Generics and Methods
Methods on a generic type can also have type parameters, though typically they share the same parameter as the type.
Example 7: Generic Method on a Generic Type
Explanation:
NumberList[T constraints.Ordered]
restrictsT
to ordered types.The method
Max()
compares elements with>
, which is valid underconstraints.Ordered
.
7. Constraints from Regular Interfaces
You can use any interface as a constraint, not just those from constraints
. The key is that all required methods or operators must be supported at compile time. However, note that normal interfaces don’t let you specify operators like <
or +
; you’d rely on interface methods instead.
Example 8: Custom Interface Constraint
Explanation:
PrintStrings[T Stringer]
can only be used with types that have aString()
method.The
User
type satisfies this requirement.
8. Potential Pitfalls and Limitations
No Runtime Polymorphism
Generics do not replace interfaces when you need dynamic dispatch or runtime polymorphism. Generics are resolved at compile time.
Cannot Use Operators Arbitrarily
You must define constraints if you want to use operators like
<
,+
, or==
. Go doesn’t automatically allow them for any type.
Exporting Generic Types
When you export a generic type or function, ensure the constraints are also publicly accessible if they are custom constraints.
Complexity
Overusing generics can make code more complex. Evaluate if a simpler approach (like just one or two specialized functions) might be more readable.
Summary and Best Practices
Generics in Go let you write reusable, type-safe code without duplicating functions for each type.
Constraints define what operations or interfaces the type parameter must support.
Use generics when you have truly type-agnostic logic (e.g., a data structure, an algorithm that operates on multiple types).
Don’t overuse generics for simple tasks or where interfaces would suffice.
Profile readability: A generic approach should be clearer than multiple specialized functions if your logic is truly the same for all types involved.
By understanding how to declare type parameters, apply constraints, and exploit type inference, you can harness Go’s generics to create versatile and maintainable code. As with any powerful feature, use it judiciously to ensure your code remains simple, efficient, and clear.