xfeng

xfeng

Sporting | Reading | Technology | Recording
github
bilibili

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 initially did not support generics because the developers of Go believed that generics were important but not necessary, so the initial release of Go did not include 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.

For example:

Animal class

Three objects: dog, cat, chicken

One common event: making sound

Three different behaviors: barking, meowing, clucking

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)

3. Challenges of Generics#

Generics have advantages but also disadvantages. Balancing the benefits and drawbacks 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 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, limiting the underlying types of types that implement this interface.

  • Type arguments must be passed 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, indicating 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 the string type
	// If the compiler can perform type inference, printSlice([]string{"Hello", "World"}) can be used
	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 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 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"
)

// 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 include:

  • Declaring variables of these types.
  • Assigning values of the same type to these variables.
  • Passing these variables as arguments to functions or returning them from functions.
  • Taking the address of these variables.
  • Converting or assigning values of these types to interface{} type variables.
  • Assigning an interface value to variables of these types through type assertion.
  • Using them as a case branch in a type switch block.
  • Defining and using composite types composed of these types, such as slices with element types of these types.
  • Passing these types 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#

Like generic functions, when using generic types, it is necessary 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.