foreword
Ian Lance Taylor , the designer of Go generics, published an article when to use generics on the official blog site, detailing when to use generics and when not to use generics. This is very instructive for us to write Go generic code that conforms to best practices.
Based on the translation of the original text, I have made some optimizations in the expression to facilitate everyone's understanding.
original translation
Ian Lance Taylor
2022.04.14
This blog is a compilation of what I have to say about generics at Google Open Source Day and GopherCon 2021.
Go version 1.18 added a major feature: support for generic programming. This article will not cover what generics are and how to use them, but will focus on explaining when to use generics and when not to use them in Go programming practice.
To be clear, I will provide some general guidelines. This is not a hard and fast rule. You can decide according to your own judgment, but if you are not sure how to use generics, it is recommended to refer to the guidelines introduced in this article.
write the code
There is a general guideline for Go programming: write Go programs by writing code, not by defining types.
When it comes to generics specifically, if you start writing code by defining type parameter constraints, you're probably going in the wrong direction. Start by writing a function, and if you find it better to use type parameters during the writing process, then use type parameters.
When are type parameters useful?
Next let's see when it is more useful for us to write code using type parameters.
Use Go's built-in container types
If the function uses the built-in container types (including slice, map and channel) of the language as function parameters, and the processing logic of the function code for the container does not preset the element type in the container, then using the type parameter may it works.
For example, we want to implement a function, the input parameter of the function is a map, and we want to return a slice composed of all keys of the map. The type of the key can be any key type supported by the map.
// 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 does not make any restrictions on the type of the key in the map, and does not use the value in the map, so this code applies to all map types. This is a good example of using type parameters.
In this scenario, reflection can also be used, but reflection is a rather awkward programming model. Static type checking cannot be done at compile time, and it will slow down the runtime.
Implement common data structures
For generic data structures, type parameters can also be useful. Common data structures are similar to slices and maps, but are not built into the language, such as linked lists or binary trees.
In the absence of generics, if you want to implement a general data structure, there are two options:
- Option 1: Implement a data structure for each element type
- Option 2: Use the interface type
The advantage of generics over scheme 1 is that the code is simpler, and it is more convenient to call other modules. The advantages of generics over scheme 2 are that data storage is more efficient, memory resources are saved, and static type checking can be done at compile time, avoiding the use of type assertions in the code.
The following example is a generic binary tree data structure implemented 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 of the binary tree contains a variable ---189b35b85a6e173a83f92f8de5fd4f39--- of type T
val
. When the binary tree is instantiated, the type argument needs to be passed in. At this time, the type of val
has been determined and will not be stored as the interface type.
It is reasonable to use type parameters in this scenario, because Tree
is a general data structure, including the code implementation in the method, which has nothing to do with the type of T
.
Tree
data structure itself does not need to know on how to compare binary tree node type T
variables val
size, it has a member variable cmp
realize the size comparison of ---0db23ba22fd14d6c4ed6439153ae17c3 val
, cmp
is a function type variable, which is specified when the binary tree is initialized. Therefore, the size comparison of the node value on the binary tree is implemented by a function outside the Tree
. You can see the pair cmp
in line 4 of the find
method. use.
Type parameters are used in functions rather than methods
The Tree
data structure example above illustrates another general guideline: when you need a comparison function like cmp
, prefer a function over a method.
For the above Tree
type, in addition to using the member variable of the function type cmp
to compare the size of val
, there is another scheme that requires the type T
must have a Compare
or Less
method for size comparison. To do this, you need to define a type constraint (type constraint) to qualify the type T
must implement this method.
The result of this is that even if T
is just an ordinary int type, the user must define a own int type, implement the method in the type constraint, and then use this custom int type The type parameter T
is passed as a type argument.
But if we refer to the code implementation of the above Tree
and define a member variable of a function type cmp
to do the size comparison of the T
type, the code implementation is more concise .
In other words, it is much easier to turn methods into functions than to add methods to a type. Therefore, for general data types, it is preferable to use functions instead of writing a type restriction that must have methods.
Different types need to implement public methods
Another useful scenario for type parameters is when different types implement some common methods, and for these methods, the implementation logic of different types is the same.
For example, there is a sort package in the Go standard library, which can sort slices that store different data types, such as Float64s(x)
can sort []float64
, Ints(x)
sort []int
.
At the same time, the sort package can also call sort.Sort()
to sort user-defined data types (such as structures, custom int types, etc.), as long as the type implements sort.Interface
this interface type Len()
, Less()
and Swap()
these three methods are enough.
Next, we can use generics to make some modifications to the sort package, so that we can uniformly call sort.Sort()
for slices that store different data types, instead of calling for []int
Ints(x)
, calling Float64s(x)
[]float64
differential processing, which can simplify the code logic.
The following code implements a generic struct type SliceFn
, which implements sort.Interface
.
// 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 different slice types, the Len
and Swap
methods are implemented the same. Less
method requires slice where the two elements to compare, compare logic implemented in SliceFn
where the member variables less
inside, less
It is a variable of function type, which is passed parameter assignment when the structure is initialized. This is similar to the processing of the binary tree general data structure above Tree
.
We then encapsulate sort.Sort
SortFn
as a generic function according to the generic style, so that for all slice types, we can call SortFn
for sorting.
// 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, cmp})
}
This is very similar to sort.Slice in the standard library, except that the parameters of the less
comparison function are specific values, while the sort.Slice
comparison function less
The argument to the comparison function is the subscript index of the slice.
It is more appropriate to use the type parameter in this scenario, because the implementation logic of the methods of different types SliceFn
is the same, but the types of elements stored in slice
are different.
When not to use type parameters
Now let's talk about scenarios where type parameters are deprecated.
Do not replace interface types with type parameters
We all know that the Go language has an interface type, and the interface supports generic programming in a sense.
For example, the widely used io.Reader
interface provides a generic mechanism for reading data, such as support for reading data from files and random number generators.
If your operation on a variable of some type is to call a method of that type, use the interface type directly instead of the type parameter. io.Reader
Easy to read and efficient from a code perspective, no need to use type parameters.
For example, someone might change the first version based on interface type ReadSome
to the second version based on type parameters.
func ReadSome(r io.Reader) ([]byte, error)
func ReadSome[T io.Reader](r T) ([]byte, error)
Don't make this modification, using the first interface-based version makes the function easier to write and read, and the function executes almost as efficiently.
Note : Although generics can be implemented in different ways, and the implementation of generics may change over time, the implementation of generics in Go 1.18 is in many cases for variables of type interface and types of Variable handling for type parameters is very similar. This means that using a type parameter is usually not faster than using an interface, so don't change the interface type to a type parameter just for program speed, because it probably won't run faster.
Don't use type parameters if methods are implemented differently
When deciding whether to use a type parameter or an interface, consider the logical implementation of the method. As we said earlier, if the implementation of a method is the same for all types, it is with type parameters. Conversely, if the method implementation for each type is different, use the interface type, not type parameters.
For example, the implementation from the file Read
is completely different from the implementation from the random number generator Read
. In this scenario, you can define a io.Reader
interface type, which contains a Read
method. The file and random number generator implement their own Read
method.
Reflection can be used when appropriate
Go has runtime reflection . The reflection mechanism supports generic programming in a sense because it allows you to write code that works for any type. If some operations need to support the following scenarios, you can consider using reflection.
- To operate on types without methods, interface types do not apply.
- The operation logic of each type is different, and generics do not apply.
An example is the implementation of the encoding/json package. We don't want to require every type we encode to implement the MarshalJson
method, so we can't use the interface type. And the logic of encoding different types is different, so we should not use generics.
So for this case, encoding/json is implemented using reflection. The specific implementation details can refer to the source code .
a simple principle
To summarize, when to use generics can be reduced to a simple principle as follows.
If you find that you are writing almost the same code repeatedly, the only difference is the type used in the code, then consider whether it can be implemented using generics.
open source address
Articles and sample code are open sourced on GitHub: Beginner, Intermediate, and Advanced Tutorials in Go .
Official account: coding advanced. Follow the official account to get the latest Go interview questions and technology stacks.
Personal website: Jincheng's Blog .
Zhihu: Wuji
References
- Go Blog on When to Use Generics: https://go.dev/blog/when-generics
- Go Day 2021 on Google Open Source : https://www.youtube.com/watch?v=nr8EpUO9jhw
- GopherCon 2021: https://www.youtube.com/watch?v=Pa_e9EeCdy8
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。