1. Overview#
Generics are a standard feature in many languages and using them properly facilitates development. However, Go did not initially support generics because the developers believed that generics were important but not necessary. Therefore, when Go was first released, it did not have generics. 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 ability of different objects to respond differently to the same event.
For example:
Animal class
Three objects: dog, cat, chicken
One common event: make sound
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, generics turn element types into parameters.
3. Challenges of Generics#
Generics have advantages but also disadvantages. Balancing the benefits and drawbacks of generics is a problem that designers of generics 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 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 type constraints.
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 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 successfully, 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 any type parameter can support
(In the following code, the + in the add function should be int, int8, int16, int32, int64,uint, uint8, uint16, uint32, uint64,uintptr,float32,float64, complex64, complex128,string
can support)
package main
import (
"fmt"
)
// Constrain 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 interface{} type variables.
- 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, which 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 provides a brief introduction to the usage of generics in Go, and using generics properly can improve development efficiency.