Go: When Should You Use Generics? When Shouldn’t You?

Go: When Should You Use Generics? When Shouldn’t You?
Estimated Reading Time: 7 minutes
- Go generics are best utilized to reduce boilerplate when the only difference between repeated code segments is the specific data type being processed.
- Prioritize **Go’s interfaces** or **reflection** when method implementations vary significantly across types, or when dealing with types that lack common methods.
- When designing generic data structures requiring comparisons, it’s more flexible to pass a **comparison function** (`func(T, T) int`) rather than imposing a constraint that types must implement a specific comparison method.
- Adopt an **iterative development approach**: begin by writing concrete functions with specific types, and introduce generics later once a clear pattern of type-based repetition becomes apparent.
- It’s important to note that the Go 1.18 generics implementation generally does **not offer performance benefits** over using interfaces; therefore, don’t migrate solely for expected speed improvements.
- Introduction
- Write Code: The Go Way
- When Go Generics Prove Invaluable
- When to Opt for Alternatives: Generics Are Not Always the Answer
- A Simple Guideline for Go Generics
- Actionable Steps for Mastering Go Generics:
- A Short Real-World Example: Generalizing Map Operations
- Conclusion: The Guiding Principle for Go Generics
- Ready to Elevate Your Go Code?
- Frequently Asked Questions
Go 1.18 ushered in a significant new capability for developers: generics. This powerful addition offers unprecedented flexibility, but like any robust tool, knowing when and when not to wield it is crucial for writing clean, efficient, and maintainable Go code. This article, based on insights from a leading expert, delves into the practical guidelines for making informed decisions about generics in your projects.
Introduction
This is the blog post version of my talks at Google Open Source Live:
https://youtu.be/nr8EpUO3jhw?si=jlWTapr5NM6isLgt&embedable=true
and GopherCon 2021:
https://youtu.be/Pae9EeCdy8?si=M-87Eis2bN1qmJ&embedable=true
The Go 1.18 release adds a major new language feature: support for generic programming. In this article I’m not going to describe what generics are nor how to use them. This article is about when to use generics in Go code, and when not to use them.
To be clear, I’ll provide general guidelines, not hard and fast rules. Use your own judgement. But if you aren’t sure, I recommend using the guidelines discussed here.
Write Code: The Go Way
Let’s start with a general guideline for programming Go: write Go programs by writing code, not by defining types. When it comes to generics, if you start writing your program by defining type parameter constraints, you are probably on the wrong path. Start by writing functions. It’s easy to add type parameters later when it’s clear that they will be useful.
When Go Generics Prove Invaluable
Generics aren’t for every situation, but in certain contexts, they can significantly reduce boilerplate, improve type safety, and enhance code reusability. Here’s where type parameters truly shine:
When Using Language-Defined Container Types
One case is when writing functions that operate on the special container types that are defined by the language: slices, maps, and channels. If a function has parameters with those types, and the function code doesn’t make any particular assumptions about the element types, then it may be useful to use a type parameter.
For example, here is a function that returns a slice of all the keys in a map of any type:
// MapKeys returns a slice of all the keys in m.
// The keys are not returned in any particular order.
func MapKeys[Key comparable, Val any](m map[Key]Val) []Key { s := make([]Key, 0, len(m)) for k := range m { s = append(s, k) } return s
}
This code doesn’t assume anything about the map key type, and it doesn’t use the map value type at all. It works for any map type. That makes it a good candidate for using type parameters. The alternative to type parameters for this kind of function is typically to use reflection, but that is a more awkward programming model, is not statically typechecked at build time, and is often slower at run time.
General Purpose Data Structures
Another case where type parameters can be useful is for general purpose data structures. A general purpose data structure is something like a slice or map, but one that is not built into the language, such as a linked list, or a binary tree.
Today, programs that need such data structures typically do one of two things: write them with a specific element type, or use an interface type. Replacing a specific element type with a type parameter can produce a more general data structure that can be used in other parts of the program, or by other programs. Replacing an interface type with a type parameter can permit data to be stored more efficiently, saving memory resources; it can also permit the code to avoid type assertions, and to be fully type checked at build time.
For example, here is part of what a binary tree data structure might look like using type parameters:
// Tree is a binary tree.
type Tree[T any] struct { cmp func(T, T) int root *node[T]
} // A node in a Tree.
type node[T any] struct { left, right *node[T] val T
} // find returns a pointer to the node containing val,
// or, if val is not present, a pointer to where it
// would be placed if added.
func (bt *Tree[T]) find(val T) **node[T] { pl := &bt.root for *pl != nil { switch cmp := bt.cmp(val, (*pl).val); { case cmp < 0: pl = &(*pl).left case cmp > 0: pl = &(*pl).right default: return pl } } return pl
} // Insert inserts val into bt if not already there,
// and reports whether it was inserted.
func (bt *Tree[T]) Insert(val T) bool { pl := bt.find(val) if *pl != nil { return false } *pl = &node[T]{val: val} return true
}
Each node in the tree contains a value of the type parameter T. When the tree is instantiated with a particular type argument, values of that type will be stored directly in the nodes. They will not be stored as interface types.
This is a reasonable use of type parameters because the Tree
data structure, including the code in the methods, is largely independent of the element type T. The Tree
data structure does need to know how to compare values of the element type T; it uses a passed-in comparison function for that. You can see this on the fourth line of the find method, in the call to bt.cmp
. Other than that, the type parameter doesn’t matter at all.
For type parameters, prefer functions to methods
The Tree
example illustrates another general guideline: when you need something like a comparison function, prefer a function to a method. We could have defined the Tree
type such that the element type is required to have a Compare
or Less
method. This would be done by writing a constraint that requires the method, meaning that any type argument used to instantiate the Tree
type would need to have that method.
A consequence would be that anybody who wants to use Tree
with a simple data type like int
would have to define their own integer type and write their own comparison method. If we define Tree
to take a comparison function, as in the code shown above, then it is easy to pass in the desired function. It’s just as easy to write that comparison function as it is to write a method.
If the Tree
element type happens to already have a Compare
method, then we can simply use a method expression like ElementType.Compare
as the comparison function.
To put it another way, it is much simpler to turn a method into a function than it is to add a method to a type. So for general purpose data types, prefer a function rather than writing a constraint that requires a method.
Implementing a Common Method
Another case where type parameters can be useful is when different types need to implement some common method, and the implementations for the different types all look the same.
For example, consider the standard library’s sort.Interface
. It requires that a type implement three methods: Len
, Swap
, and Less
.
Here is an example of a generic type SliceFn
that implements sort.Interface
for any slice type:
// SliceFn implements sort.Interface for a slice of T.
type SliceFn[T any] struct { s []T less func(T, T) bool
} func (s SliceFn[T]) Len() int { return len(s.s)
}
func (s SliceFn[T]) Swap(i, j int) { s.s[i], s.s[j] = s.s[j], s.s[i]
}
func (s SliceFn[T]) Less(i, j int) bool { return s.less(s.s[i], s.s[j])
}
For any slice type, the Len
and Swap
methods are exactly the same. The Less
method requires a comparison, which is the Fn
part of the name SliceFn
. As with the earlier Tree
example, we will pass in a function when we create a SliceFn
.
Here is how to use SliceFn
to sort any slice using a comparison function:
// SortFn sorts s in place using a comparison function.
func SortFn[T any](s []T, less func(T, T) bool) { sort.Sort(SliceFn[T]{s, less})
}
This is similar to the standard library function sort.Slice
, but the comparison function is written using values rather than slice indexes.
Using type parameters for this kind of code is appropriate because the methods look exactly the same for all slice types.
(I should mention that Go 1.19–not 1.18–will most likely include a generic function to sort a slice using a comparison function, and that generic function will most likely not use sort.Interface
. See proposal #47619. But the general point is still true even if this specific example will most likely not be useful: it’s reasonable to use type parameters when you need to implement methods that look the same for all the relevant types.)
When to Opt for Alternatives: Generics Are Not Always the Answer
While powerful, generics aren’t a panacea. There are situations where Go’s existing features — interfaces and reflection — remain the superior choice.
Don’t Replace Interface Types with Type Parameters
As we all know, Go has interface types. Interface types permit a kind of generic programming.
For example, the widely used io.Reader
interface provides a generic mechanism for reading data from any value that contains information (for example, a file) or that produces information (for example, a random number generator). If all you need to do with a value of some type is call a method on that value, use an interface type, not a type parameter. io.Reader
is easy to read, efficient, and effective. There is no need to use a type parameter to read data from a value by calling the Read
method.
For example, it might be tempting to change the first function signature here, which uses just an interface type, into the second version, which uses a type parameter.
func ReadSome(r io.Reader) ([]byte, error) func ReadSome[T io.Reader](r T) ([]byte, error)
Don’t make that kind of change. Omitting the type parameter makes the function easier to write, easier to read, and the execution time will likely be the same.
It’s worth emphasizing the last point. While it’s possible to implement generics in several different ways, and implementations will change and improve over time, the implementation used in Go 1.18 will in many cases treat values whose type is a type parameter much like values whose type is an interface type. What this means is that using a type parameter will generally not be faster than using an interface type. So don’t change from interface types to type parameters just for speed, because it probably won’t run any faster.
Don’t Use Type Parameters If Method Implementations Differ
When deciding whether to use a type parameter or an interface type, consider the implementation of the methods. Earlier we said that if the implementation of a method is the same for all types, use a type parameter. Inversely, if the implementation is different for each type, then use an interface type and write different method implementations, don’t use a type parameter.
For example, the implementation of Read
from a file is nothing like the implementation of Read
from a random number generator. That means that we should write two different Read
methods, and use an interface type like io.Reader
.
Use Reflection Where Appropriate
Go has run time reflection. Reflection permits a kind of generic programming, in that it permits you to write code that works with any type.
If some operation has to support even types that don’t have methods (so that interface types don’t help), and if the operation is different for each type (so that type parameters aren’t appropriate), use reflection.
An example of this is the encoding/json
package. We don’t want to require that every type that we encode have a MarshalJSON
method, so we can’t use interface types. But encoding an interface type is nothing like encoding a struct type, so we shouldn’t use type parameters. Instead, the package uses reflection. The code is not simple, but it works. For details, see the source code.
A Simple Guideline for Go Generics
In closing, this discussion of when to use generics can be reduced to one simple guideline.
If You Find Yourself Repeating Code…
If you find yourself writing the exact same code multiple times, where the only difference between the copies is that the code uses different types, consider whether you can use a type parameter.
Another way to say this is that you should avoid type parameters until you notice that you are about to write the exact same code multiple times.
Ian Lance Taylor
This article is available on The Go Blog under a CC BY 4.0 DEED license.
Photo by Michael Dziedzic on Unsplash
The introduction of generics in Go is a powerful evolutionary step for the language. However, like any new feature, its effective application requires understanding its strengths and weaknesses relative to existing idioms.
Actionable Steps for Mastering Go Generics:
- Write Concrete Code First: Don’t start by defining generic type parameters and constraints. Begin by implementing your functions or data structures with specific types. If you later identify a pattern of repetition where only the types differ, then introduce generics.
- Prefer Functions for Generic Comparisons: When building generic data structures that require comparisons (like a `Tree` needing to sort elements), opt to pass a comparison function (`func(T, T) int`) as an argument rather than constraining `T` to have a specific comparison method. This approach offers greater flexibility for users of your generic type.
- Use Interfaces for Simple Method Calls: If your primary interaction with a type involves calling one or more of its methods, and the method implementations across different concrete types are distinct, stick with Go’s established interface patterns. Generics don’t offer performance benefits over interfaces in such cases and can unnecessarily complicate the code.
A Short Real-World Example: Generalizing Map Operations
Consider the provided MapKeys
function. Before generics, to get keys from a map[string]int
and a map[int]float64
, you’d write two distinct functions, or resort to reflection. With generics, a single, type-safe function handles both:
package main import "fmt" // MapKeys returns a slice of all the keys in m.
// The keys are not returned in any particular order.
func MapKeys[Key comparable, Val any](m map[Key]Val) []Key { s := make([]Key, 0, len(m)) for k := range m { s = append(s, k) } return s
} func main() { stringIntMap := map[string]int{"apple": 1, "banana": 2, "cherry": 3} fmt.Println("String keys:", MapKeys(stringIntMap)) // Output: String keys: [apple banana cherry] (order varies) intFloatMap := map[int]float64{10: 1.1, 20: 2.2, 30: 3.3} fmt.Println("Int keys:", MapKeys(intFloatMap)) // Output: Int keys: [10 20 30] (order varies) // And it works seamlessly for other comparable key types too! boolStringMap := map[bool]string{true: "Yes", false: "No"} fmt.Println("Bool keys:", MapKeys(boolStringMap)) // Output: Bool keys: [true false] (order varies)
}
This example clearly demonstrates how generics eliminate redundant code while preserving static type checking, a significant win for developer productivity and code robustness.
Conclusion: The Guiding Principle for Go Generics
Ultimately, the decision of when to use generics in Go boils down to a single, powerful principle: identify repetition. If you’re writing the same logic, but for different types, generics are likely your ally. If your operations fundamentally differ between types, or if simple method calls suffice, Go’s robust interface system or even reflection remain excellent choices.
Embrace generics where they simplify and generalize, but never at the expense of clarity or when existing Go paradigms offer a more idiomatic solution. With these guidelines, you’re well-equipped to leverage Go’s generics effectively and write more powerful, maintainable, and type-safe code.
Ready to Elevate Your Go Code?
Start experimenting with generics in your next Go project, keeping these best practices in mind. Share your insights and generic implementations with the Go community, and let’s continue to build incredible software together!
Frequently Asked Questions
Q: What’s the main situation where generics are recommended?
A: Generics are highly recommended when you find yourself writing the exact same code multiple times, and the only variation is the specific data type being operated on. This helps reduce boilerplate and improves code reusability for things like container types, general data structures, or common method implementations.
Q: Should I use generics to replace all my interface types?
A: No. If your primary interaction with a type involves calling one or more of its methods, and especially if those method implementations differ between concrete types (like io.Reader
), interfaces remain the superior and more idiomatic choice in Go. Generics won’t offer a performance advantage in these scenarios and can unnecessarily complicate your code.
Q: Do generics in Go 1.18 provide significant performance improvements over interfaces?
A: Generally, no. In many cases, the Go 1.18 implementation treats values whose type is a type parameter similarly to values whose type is an interface type. Therefore, you should not switch from interfaces to generics solely for expected speed improvements.
Q: How do I decide between generics, interfaces, and reflection?
A: Use generics for identical logic applied across different types (e.g., `MapKeys`). Use interfaces when you need to call common methods on values whose concrete types have *different* implementations of those methods (e.g., `io.Reader`). Use reflection when operations must support types without methods or when implementations are vastly different for each type (e.g., `encoding/json`).
Q: What’s the “Go Way” to approach using generics in new code?
A: The “Go Way” suggests starting by writing concrete functions with specific types. Only introduce type parameters later, when it becomes evident that you are repeating the same logic across different types and generics would offer a clear benefit in reducing duplication and improving reusability. Avoid defining type parameters and constraints prematurely.