xfeng

xfeng

健身 技术 阅读 思考 记录
tg_channel
tg_channel
github
bilibili
tg_channel

An analysis of Golang generics

image

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:

image

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

image

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

image

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.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.