1. Overview#
Generics are a standard feature in many languages and using generics in a reasonable way facilitates development. However, Go did not initially support generics because the developers of Go believed that generics were important but not necessary, so generics were not included in the initial release of Go. This year, in response to calls from the Go community, Go 1.17 released an experimental version of generics, laying the groundwork for the official release of generics in Go 1.18. This article introduces generics in Go and some usage methods.
2. What are Generics#
Before introducing generics, let's first understand polymorphism.
What is polymorphism?
Polymorphism is the different behaviors of different objects for the same event.
For example:
Animal class
There are three objects -> dog, cat, chicken
There is one common event -> make sound
There are three different behaviors -> bark, meow, cluck
Polymorphism can be divided into two categories:
- Ad hoc polymorphism (calls the corresponding version based on the type of the actual argument, supports a very limited number of calls, such as function overloading)
- Parametric polymorphism (generates different versions based on the type of the actual argument, supports any number of calls, this is generics)
What are generics?
In short, it is to turn the element type into a parameter.
3. Challenges of Generics#
Generics have advantages but also disadvantages, and how to balance the advantages and disadvantages of generics is a problem that generics designers have to face.
Challenges of generics:
In simple terms: low coding efficiency (slow development by programmers), low compilation efficiency (slow compilation by compilers), low runtime efficiency (poor user experience)
4. Key Points of Generics in Go 1.17#
-
Functions can introduce additional type parameters using the type keyword: func F(type T)(p T) { ... }.
-
These type parameters can be used in the function body just like regular parameters.
-
Types can also have type parameter lists: type M(type T) []T.
-
Each type parameter can have a constraint: func F(type T Constraint)(p T) { ... }.
-
Use interfaces to describe type constraints.
-
The interface used as a type constraint can have a predeclared type list that restricts the underlying types of the types that implement this interface.
-
Type arguments are required when using generic functions or types.
-
In general, type inference allows users to omit type arguments when calling generic functions.
-
If a type parameter has a type constraint, the type argument must implement the interface.
-
Generic functions only allow operations specified by the type constraint.
5. How to Use Generics#
5.1 How to Output Generics#
package main
import (
"fmt"
)
// Use [] to store additional type parameter lists
//[T any] is the type parameter, which means that this function supports any type T
func printSlice[T any](s []T) {
for _, v := range s {
fmt.Printf("%v ", v)
}
fmt.Print("\n")
}
func main() {
printSlice[int]([]int{1, 2, 3, 4, 5})
printSlice[float64]([]float64{1.01, 2.02, 3.03, 4.04, 5.05})
// When calling printSlice[string]([]string{"Hello", "World"}), it will be inferred as string type
// If the compiler can perform type inference, printSlice([]string{"Hello", "World"})
printSlice([]string{"Hello", "World"})
printSlice[int64]([]int64{5, 4, 3, 2, 1})
}
To use generics, you need to have Go version 1.17 or above
If you run the above code directly, you may get an error
.\main.go:9:6: missing function body
.\main.go:9:16: syntax error: unexpected [, expecting (
To run it smoothly, you need to add the parameter: -gcflags=-G=3
5.2 How to Constrain the Type Range of Generics#
Each type has a type constraint, just like each regular parameter has a type.
In Go 1.17, generic functions can only use operations that can be supported by the types instantiated by the type parameters
(For the following code, the + in the add function should be an operation supported by int, int8, int16, int32, int64,uint, uint8, uint16, uint32, uint64,uintptr,float32,float64, complex64, complex128,string
)
package main
import (
"fmt"
)
// Limit the type range of generics
type Addable interface {
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64, complex64, complex128,
string
}
func add[T Addable] (a, b T) T {
return a + b
}
func main() {
fmt.Println(add(1,2))
fmt.Println(add("hello","world"))
}
For type parameter instances without any constraints, the allowed operations on them are:
- Declare variables of these types.
- Assign values of the same type to these variables.
- Pass these variables as arguments to functions or return them from functions.
- Take the address of these variables.
- Convert or assign values of these types to variables of type interface{}.
- Assign an interface value to variables of these types through type assertion.
- Use them as a case branch in a type switch block.
- Define and use composite types composed of this type, such as slices with this type as the element type.
- Pass this type to some built-in functions, such as new.
5.21 Comparable Constraint#
Not all types can be compared using ==
, Go has a built-in comparable constraint that represents comparability
package main
import (
"fmt"
)
func findFunc[T comparable](a []T, v T) int {
for i, e := range a {
if e == v {
return i
}
}
return -1
}
func main() {
fmt.Println(findFunc([]int{1, 2, 3, 4, 5, 6}, 5))
}
5.3 Slices in Generics#
Just like generic functions, when using generic types, you need to instantiate them (explicitly assign types to type parameters), such as vs:=slice{5,4,2,1}
package main
import (
"fmt"
)
type slice[T any] []T
/*
type any interface {
type int, string
}*/
func printSlice[T any](s []T) {
for _, v := range s {
fmt.Printf("%v ", v)
}
fmt.Print("\n")
}
func main() {
// note1: cannot use generic type slice[T interface{}] without instantiation
// note2: cannot use generic type slice[T any] without instantiation
vs := slice[int]{5, 4, 2, 1}
printSlice(vs)
5.4 Pointers in Generics#
package main
import (
"fmt"
)
func pointerOf[T any](v T) *T {
return &v
}
func main() {
sp := pointerOf("foo")
fmt.Println(*sp)
ip := pointerOf(123)
fmt.Println(*ip)
*ip = 234
fmt.Println(*ip)
}
5.5 Maps in Generics#
package main
import (
"fmt"
)
func mapFunc[T any, M any](a []T, f func(T) M) []M {
n := make([]M, len(a), cap(a))
for i, e := range a {
n[i] = f(e)
}
return n
}
func main() {
vi := []int{1, 2, 3, 4, 5, 6}
vs := mapFunc(vi, func(v int) string {
return "<" + fmt.Sprint(v*v) + ">"
})
fmt.Println(vs)
}
5.6 Queues in Generics#
package main
import (
"fmt"
)
type queue[T any] []T
func (q *queue[T]) enqueue(v T) {
*q = append(*q, v)
}
func (q *queue[T]) dequeue() (T, bool) {
if len(*q) == 0 {
var zero T
return zero, false
}
r := (*q)[0]
*q = (*q)[1:]
return r, true
}
func main() {
q := new(queue[int])
q.enqueue(5)
q.enqueue(6)
fmt.Println(q)
fmt.Println(q.dequeue())
fmt.Println(q.dequeue())
fmt.Println(q.dequeue())
}
6. Conclusion#
This article briefly introduces the usage of generics in Go, and using generics properly can improve development efficiency.