• type elementとtype termでジェネリクスの型制約を実現できる
    • type elementで定義されている型を変数やフィールド、戻り値、引数の型に指定するとコンパイル時のエラーとなるので注意
var zzz Integer = 123 // cannot use type Integer outside a type constraint: interface contains type constraints
  • type termはデフォルトでは完全に一致しなければいけない
type Integer interface {
	int | int8 | int16 | int32 | int64 |
		uint | uint8 | uint16 | uint32 | uint64 | uintptr
}
 
func divAndRemainder[T Integer](num, denom T) (T, T, error) {
	if denom == 0 {
		return 0, 0, errors.New("0で割ることはできません")
	}
	return num / denom, num % denom, nil
}
 
func main() {
	type MyInt int // intはIntegerに含まれてはいるがMyIntは含まれていない
	var myA MyInt = 10
	var myB MyInt = 20
	fmt.Println(divAndRemainder(myA, myB)) //コンパイル時エラー MyInt does not satisfy Integer (possibly missing ~ for int in Integer)
}
  • ~をつけることで基底型を許容することができる
type Integer interface {
	~int | int8 | int16 | int32 | int64 |
		uint | uint8 | uint16 | uint32 | uint64 | uintptr
}
 
// 省略
 
func main() {
	type MyInt int
	var myA MyInt = 10
	var myB MyInt = 20
	fmt.Println(divAndRemainder(myA, myB)) // MyIntの基底型はintなので許容 0 10 <nil>
}
  • type termは事前宣言された型以外にスライスやマップ、配列、チャネル、構造体、関数も指定できる

8.5.1 型推論とジェネリクス

  • Goでは演算子:=を使うときに型推論をしてくれているが、ジェネリック関数を呼び出す際にも呼び出しを単純にするために型推論をしてくれる
  • 状況によって型推論できない場合はエラーになるので型注釈が必要になる場合もある
package main
 
import (
	"fmt"
	"reflect"
)
 
type Integer interface {
	int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64
}
 
func Convert[T1, T2 Integer](in T1) T2 {
	return T2(in)
}
 
func main() {
	var a int = 10
	b := Convert(a)                   // 戻す型を推論できないのでエラー in call to Convert, cannot infer T2 (declared at ./prog.go:12:18)
	
	b := Convert[int, int64](a)       // 型注釈でエラー回避
 	fmt.Println(b, reflect.TypeOf(b)) // 10 int64
}

8.5.2 型要素による定数の制約

  • 下記のようにジェネリック関数でtype elementを使用することで定数に制約をかけることもできる
package main
 
import (
	"fmt"
)
 
type Integer interface { 
	~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
} 
 
// コンパイル通る
func plusOneHundred[T Integer](in T) T {
	return in + 100 // + 100は全ての整数型で表現可能
}
 
// コンパイル通らない
func plusOneHundred[T Integer](in T) T {
	return in + 1_000 // + 1_000はint8など一部の整数型で表現不可
}
 
func main() {
	var a int = 10
	b := plusOneHundred(a)
 
	fmt.Printf("%T: %v\n", b, b)
}

8.5.3 ジェネリック関数とジェネリックなデータ構造の利用

  • 8章で学んだことを使用して、どんな具象型にも対応できる二本木構造を作る
    • まず「2つの値を比較してその順序を返す」ジェネリック関数を定義する
type OrderableFunc[T any] func(t1, t2 T) int
  • 次にTree構造体と新しいTreeを生成するファクトリの定義をする
type Tree[T any] struct {
	f    OrderableFunc[T]
	root *Node[T]
}
 
type Node[T any] struct {
	val         T
	left, right *Node[T]
}
 
func NewTree[T any](f OrderableFunc[T]) *Tree[T] {
	return &Tree[T]{
		f: f,
	}
}
  • TreeのメソッドはNodeのメソッドをラップする
func (t *Tree[T]) Add(v T) {
	t.root = t.root.Add(t.f, v)
}
 
func (t *Tree[T]) Contains(v T) bool {
	return t.root.Contains(t.f, v)
}
  • Nodeのメソッドで要素の順序付けに使う関数を受け取る
func (n *Node[T]) Add(f OrderableFunc[T], v T) *Node[T] {
	if n == nil {
		return &Node[T]{val: v}
	}
	switch r := f(v, n.val); {
	case r <= -1:
		n.left = n.left.Add(f, v)
	case r >= 1:
		n.right = n.right.Add(f, v)
	}
	return n
}
 
func (n *Node[T]) Contains(f OrderableFunc[T], v T) bool {
	if n == nil {
		return false
	}
	switch r := f(v, n.val); {
	case r <= -1:
		return n.left.Contains(f, v)
	case r >= 1:
		return n.right.Contains(f, v)
	}
	return true
}
 
  • 組み込み関数を使用する場合
func main() {
	t1 := NewTree(cmp.Compare[int])
	t1.Add(10)
	t1.Add(30)
	t1.Add(15)
	fmt.Println(t1.Contains(15)) // true
	fmt.Println(t1.Contains(40)) // false
}
  • 構造体を使用するパターン1 構造体を受け取る関数を定義
func main() {
	t2 := NewTree(OrderPeople)
	t2.Add(Person{"Bob", 30})
	t2.Add(Person{"Maria", 35})
	t2.Add(Person{"Bob", 50})
	fmt.Println(t2.Contains(Person{"Bob", 30})) // true
	fmt.Println(t2.Contains(Person{"Fred", 25})) // false
} 
 
type Person struct {
	Name string
	Age  int
}
 
func OrderPeople(p1, p2 Person) int {
	out := cmp.Compare(p1.Name, p2.Name)
	if out == 0 {
		out = cmp.Compare(p1.Age, p2.Age)
	}
	return out
} 
  • 構造体を使用するパターン2 メソッドを定義
func main() {
	t3 := NewTree(Person.Order)
	t3.Add(Person{"Bob", 30})
	t3.Add(Person{"Maria", 35})
	t3.Add(Person{"Bob", 50})
	fmt.Println(t3.Contains(Person{"Bob", 30})) // true
	fmt.Println(t3.Contains(Person{"Fred", 25})) // false
} 
 
type Person struct {
	Name string
	Age  int
}	
 
func (p Person) Order(other Person) int {
	out := cmp.Compare(p.Name, other.Name)
	if out == 0 {
		out = cmp.Compare(p.Age, other.Age)
	}
	return out
}