Three new books, Go Optimizations 101, Go Details & Tips 101 and Go Generics 101 are published now. It is most cost-effective to buy all of them through this book bundle in the Leanpub book store.

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 a generic type 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 type;
  2. ordinary types.

Before Go 1.18, all values types are ordinary types.

A generic type is a defined 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].

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.

Here, the type parameter 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.


Index↡

The Go 101 project is hosted on Github. Welcome to improve Go 101 articles by submitting corrections for all kinds of mistakes, such as typos, grammar errors, wording inaccuracies, description flaws, code bugs and broken links.

If you would like to learn some Go details and facts every serveral days, please follow Go 101's official Twitter account @go100and1.

The digital versions of this book are available at the following places:
Tapir, the author of Go 101, has been on writing the Go 101 series books and maintaining the go101.org website since 2016 July. New contents will be continually added to the book and the website from time to time. Tapir is also an indie game developer. You can also support Go 101 by playing Tapir's games (made for both Android and iPhone/iPad):
Individual donations via PayPal are also welcome.

Index: