현재 3권의 신간들인 Go Optimizations 101, Go Details & Tips 101Go Generics 101이 출간되어 있습니다. Leanpub 서점에서 번들을 모두 구입하시는 방법이 비용 대비 효율이 가장 좋습니다.

Go에 대한 많은 정보들과 Go 101 책들의 최신 소식을 얻으시려면 Go 101 트위터 계정인 @Go100and1을 팔로잉 해주세요.

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 프로젝트는 Github 에서 호스팅됩니다. 오타, 문법 오류, 부정확한 표현, 설명 결함, 코드 버그, 끊어진 링크와 같은 모든 종류의 실수에 대한 수정 사항을 제출하여 Go 101을 개선을 돕는 것은 언제나 환영합니다.

주기적으로 Go에 대한 깊이 있는 정보를 얻고 싶다면 Go 101의 공식 트위터 계정인 @go100and1을 팔로우하거나 Go 101 슬랙 채널에j가입해주세요.

이 책의 디지털 버전은 아래와 같은 곳을 통해서 구매할 수 있습니다.
Go 101의 저자인 Tapir는 2016년 7월부터 Go 101 시리즈 책들을 집필하고 go101.org 웹사이트를 유지 관리하고 있습니다. 새로운 콘텐츠는 책과 웹사이트에 수시로 추가될 예정입니다. Tapir는 인디 게임 개발자이기도 합니다. Tapir의 게임을 플레이하여 Go 101을 지원할 수도 있습니다. (안드로이드와 아이폰/아이패드용):
  • Color Infection (★★★★★), 140개 이상의 단계로 이루어진 물리 기반의 캐주얼 퍼즐 게임
  • Rectangle Pushers (★★★★★), 2가지 모드와 104개 이상의 단계로 이루어진 캐주얼 퍼즐 게임
  • Let's Play With Particles, 세가지 미니 게임이 있는 캐주얼 액션 게임
페이팔을 통한 개인 기부도 환영합니다.

색인: