First Look of Custom Generics

In the custom generics world, a type may be declared as a generic type, and a function may be declared as generic function. In addition, generic types are defined types, so they may have methods.

The declaration of a generic type, generic function, or method of generic types contains a type parameter list part, which is the main difference from the declaration of an ordinary type, function, or method.

A generic type example

Firstly, let's view an example to show how generic types look like. It might be not a perfect example, but it does show the usefulness of custom generic types.

package main

import "sync"

type Lockable[T any] struct {
	sync.Mutex
	Data T
}

func main() {
	var n Lockable[uint32]
	n.Lock()
	n.Data++
	n.Unlock()
	
	var f Lockable[float64]
	f.Lock()
	f.Data += 1.23
	f.Unlock()
	
	var b Lockable[bool]
	b.Lock()
	b.Data = !b.Data
	b.Unlock()
	
	var bs Lockable[[]byte]
	bs.Lock()
	bs.Data = append(bs.Data, "Go"...)
	bs.Unlock()
}

In the above example, Lockable is a generic type. Comparing to non-generic types, there is an extra part, a type parameter list, in the declaration (specification, more precisely speaking) of a generic type. Here, the type parameter list of the Lockable generic type is [T any].

A type parameter list may contain one and more type parameter declarations which are enclosed in square brackets and separated by commas. Each parameter declaration is composed of a type parameter name and a (type) constraint. For the above example, T is the type parameter name and any is the constraint of T.

Please note that any is a new predeclared identifier introduced in Go 1.18. It is an alias of the blank interface type interface{}. We should have known that all types implements the blank interface type.

(Note, generally, Go 101 books don't say a type alias is a type. They just say a type alias denotes a type. But for convenience, the Go Generics 101 book often says any is a type.)

We could view constraints as types of (type parameter) types. All type constraints are actually interface types. Constraints are the core of custom generics and will be explained in detail in the next chapter.

T denotes a type parameter type. Its scope begins after the name of the declared generic type and ends at the end of the specification of the generic type. Here it is used as the type of the Data field.

Since Go 1.18, value types fall into two categories:

  1. type parameter types
  2. ordinary types

Before Go 1.18, all values types are ordinary types.

A generic type is a defiend type. It must be instantiated to be used as value types. The notation Lockable[uint32] is called an instantiated type (of the generic type Lockable). In the notation, [uint32] is called a type argument list and uint32 is called a type argument, which is passed to the corresponding T type parameter. That means the type of the Data field of the instantiated type Lockable[uint32] is uint32.

A type argument must implement the constraint of its corresponding type parameter. The constraint any is the loosest constraint, any value type could be passed to the T type parameter. The other type arguments used in the above example are: float64, bool and []byte.

Every instantiated type is a named type and an ordinary type. For example, Lockable[uint32] and Lockable[[]byte] are both named types.

The above example shows how custom generics avoid code repetitions for type declarations. Without custom generics, several struct types are needed to be declared, like the following code shows.

package main

import "sync"

type LockableUint32 struct {
	sync.Mutex
	Data uint32
}

type LockableFloat64 struct {
	sync.Mutex
	Data float64
}

type LockableBool struct {
	sync.Mutex
	Data bool
}

type LockableBytes struct {
	sync.Mutex
	Data []byte
}

func main() {
	var n LockableUint32
	n.Lock()
	n.Data++
	n.Unlock()
	
	var f LockableFloat64
	f.Lock()
	f.Data += 1.23
	f.Unlock()
	
	var b LockableBool
	b.Lock()
	b.Data = !b.Data
	b.Unlock()
	
	var bs LockableBytes
	bs.Lock()
	bs.Data = append(bs.Data, "Go"...)
	bs.Unlock()
}

The non-generic code contains many code repetitions, which could be avoided by using the generic type demonstrated above.

An example of a method of a generic type

Some people might not appreciate the implementation of the above generic type. Instead, they prefer to use a different implementation as the following code shows. Comparing with the Lockable implementation in the last section, the new one hides the struct fields from outside package users of the generic type.

package main

import "sync"

type Lockable[T any] struct {
	mu sync.Mutex
	data T
}

func (l *Lockable[T]) Do(f func(*T)) {
	l.mu.Lock()
	defer l.mu.Unlock()
	f(&l.data)
}

func main() {
	var n Lockable[uint32]
	n.Do(func(v *uint32) {
		*v++
	})
	
	var f Lockable[float64]
	f.Do(func(v *float64) {
		*v += 1.23
	})
	
	var b Lockable[bool]
	b.Do(func(v *bool) {
		*v = !*v
	})
	
	var bs Lockable[[]byte]
	bs.Do(func(v *[]byte) {
		*v = append(*v, "Go"...)
	})
}

In the above code, a method Do is declared for the generic base type Lockable. Here, the receiver type is a pointer type, which base type is the generic type Lockable. Different from method declarations for ordinary base types, there is a type parameter list part following the receiver generic type name Lockable in the receiver part. Here, the type parameter list is [T].

The type parameter list in a method declaration for a generic base type is actually a duplication of the type parameter list specified in the generic receiver base type specification. To make code clean, type parameter constraints are (and must be) omitted from the list. That is why here the type parameter list is [T], instead of [T any].

The scope of the type parameter T is the whole method declaration (as of Go 1.18. It might be adjusted a little in later Go versions). Here, T is used in a value parameter type, func(*T).

Please note that, the type parameter names used in a method declaration for a generic base type are not required to be the same as the corresponding ones used in the generic type specification. For example, the above method declaration is equivalent to the following rewritten one.

func (l *Lockable[Foo]) Do(f func(*Foo)) {
	...
}

Though, it is a bad practice not to keep the corresponding type parameter names the same.

BTW, the name of a type parameter may even be the blank identifier _ if it is not used (this is also true for the type parameters in generic type and function declarations). For example,

func (l *Lockable[_]) DoNothing() {
}

A generic function example

Now, let's view an example of how to declare and use generic (non-method) functions.

package main

// NoDiff checks whether or not a collection
// of values are all identical.
func NoDiff[V comparable](vs ...V) bool {
	if len(vs) == 0 {
		return true
	}
	
	v := vs[0]
	for _, x := range vs[1:] {
		if v != x {
			return false
		}
	}
	return true
}

func main() {
	var NoDiffString = NoDiff[string]
	println(NoDiff("Go", "Go", "Go")) // true
	println(NoDiffString("Go", "go")) // false
	
	println(NoDiff(123, 123, 123, 123)) // true
	println(NoDiff[int](123, 123, 789)) // false
	
	type A = [2]int
	println(NoDiff(A{}, A{}, A{}))     // true
	println(NoDiff(A{}, A{}, A{1, 2})) // false
	
	println(NoDiff(new(int)))           // true
	println(NoDiff(new(int), new(int))) // false

	println(NoDiff[bool]())   // true

	// _ = NoDiff() // error: cannot infer V
	
	// error: *** does not implement comparable
	// _ = NoDiff([]int{}, []int{})
	// _ = NoDiff(map[string]int{})
	// _ = NoDiff(any(1), any(1))
}

In the above example, NoDiff is a generic function. Different from non-generic functions, and similar to generic types, there is an extra part, a type parameter list, in the declaration of a generic function. Here, the type parameter list of the NoDiff generic function is [V comparable], in which V is the type parameter name and comparable is the constraint of V.

comparable is new predeclared identifier introduced in Go 1.18. It denotes an interface that is implemented by all comparable types. It will be explained with more details in the next chapter.

The scope of the type parameter name V is the whole function declaration excluding the function name part (as of Go 1.18. It might be adjusted a little in later Go versions). Here, V is used as the variadic (value) parameter type.

The notations NoDiff[string], NoDiff[int] and NoDiff[bool] are called instantiated functions (of the generic function NoDiff). Similar to instantiated types, in the notations, [string], [int] and [bool] are called type argument lists. In the lists, string, int and bool are called type arguments, all of which are passed to the V type parameter.

The whole type argument list may be totally omitted from an instantiated function expression if all the type arguments could be inferred from the value arguments. That is why some calls to the NoDiff generic function have no type argument lists in the above example.

Please note that all of these type arguments implement the comparable interface. Incomparable types, such as []int and map[string]int may not be passed as type arguments of calls to the NoDiff generic function. And please note that, although any is a comparable (value) type, it doesn't implement comparable, so it is also not an eligible type argument. This will be talked about in detail in the next chapter.

The above example shows how generics avoid code repetitions for function declarations. Without custom generics, we need to declare a function for each type argument used in the example. The bodies of these function declarations would be almost the same.

Generic functions could be viewed as simplified forms of methods of generic types

The generic function shown in the above section could be viewed as a simplified form of a method of a generic type, as shown in the following code.

package main

type NoDiff[V comparable] struct{}

func (nd NoDiff[V]) Do(vs ...V) bool {
	... // same as the body of the above generic function
}

func main() {
	var NoDiffString = NoDiff[string]{}.Do
	println(NoDiffString("Go", "go")) // false
	
	println(NoDiff[int]{}.Do(123, 123, 789)) // false
	
	println(NoDiff[*int]{}.Do(new(int))) // true
}

In the above code, NoDiff[string]{}.Do, NoDiff[int]{}.Do and NoDiff[*int]{}.Do are three method values of different instantiated types.

We could view a generic type as a type parameter space, and view all of its methods as some functions sharing the same type parameter space.

To make descriptions simple, sometimes, methods of generic types are also called as generic functions in this book.


目录↡

Go101.org网站内容包括Go编程各种相关知识(比如Go基础、Go优化、Go细节、Go实战、Go测验、Go工具等)。后续将不断有新的内容加入。敬请收藏关注期待。

本丛书微信公众号(联系方式一)名称为"Go 101"。二维码在网站首页。此公众号将时不时地发表一些Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。

《Go语言101》系列丛书项目目前托管在Github上(联系方式二)。欢迎各位在此项目中通过提交bug和PR的方式来改进完善《Go语言101》丛书中的各篇文章。我们可以在项目目录下运行go run .来浏览和确认各种改动。

本书的twitter帐号为@Golang_101(联系方式三)。玩推的Go友可以适当关注。

你或许对本书作者老貘开发的一些App感兴趣。

The English version of this book is here.
赞赏
(《Go语言101》系列丛书由老貘从2016年7月开始编写。目前此系列丛书仍在不断改进和增容中。你的赞赏是本系列丛书和此Go101.org网站不断增容和维护的动力。)

目录: