😸

Go言語のジェネリクス入門

に公開

Go言語のジェネリクスは、Go1.18(2022年3月)によってリリースされました。

Goの歴史の中でジェネリクスは比較的新しい機能で、その後のリリースにおいても少しずつ新しい要素が追加されています。そこでこの記事では、できるだけ最新の仕様と用語法にもとづいてジェネリクスの言語仕様について解説していきます。Goの最新リリースに合わせて、不定期的に更新もしていく予定です。

サンプルコードにはGo Playground(Webブラウザ上のGo実行環境)へのリンクをつけてあるので、ぜひ実際に動かしたりコードを少しいじってみてGoジェネリクスに親しんでください。

章目次

タイトル 内容
基本原則とシンプルな例 Goジェネリクスの基本原則と典型的な例を解説します。一番重要です。
unions メソッドの実装以外の性質をジェネリックに扱いたい場合に使える機能を解説します。
~とunderlying type unionsをさらに使いこなすための文法~を解説します。
できそうだが、実際にはできないこと できるかできないかが分かりにくい色々な細部の仕様を、「Goジェネリクスの理想のテーゼ」という考え方で整理しつつ網羅します。

記事の対象読者と更新履歴・更新予定

この記事は、

  • プログラミング自体には慣れているがGoには詳しくないGo初級者
  • Goに慣れているがジェネリクスについて細かい疑問を解決したいGo経験者

の両方を対象としています。Go初級者の方は、最初の章「基本原則とシンプルな例」のサンプルコードをPlaygroundで動かしてみて、細部を変えてみることから始めてみてください。それ以外の章の知識は必要になる頻度が少ないものなので、後回しにしても良いでしょう。

更新履歴

最終更新時のGoバージョン: Go1.25(2025-08-12)

更新予定

以下の点については今後の更新で対応予定です。

  • generic aliasについて記述がない
  • 組み込みのmin, max関数が追加されたことに対して記述が対応していない

関連記事

タイトル 内容
Go言語のジェネリクス入門(1) この記事です。基本的なジェネリクスの使用法とunion, ~について説明します。
Go言語のジェネリクス入門(2) インスタンス化と型推論 この記事の続編です。インスタンス化と型推論、そこで使われるunificationというルーチンについてできるだけ厳密に説明します。

基本原則とシンプルな例

Goジェネリクスの基本原則と3つの基本的な例を説明します。この3つの例によって、Goジェネリクスの基本的な機能は全て理解できます。

Goのジェネリクスの基本原則

Goのジェネリクスの基本事項についてはType Parameters Proposalの冒頭に挙げられています。このうち特に重要なのは次の2つです。この2つを覚えればGoのジェネリクスを十分に使うことができると思います。

  • 「関数」と「型」は「型パラメータ」を持つことができる。
  • 「型パラメータ」の満たすべき性質は「インタフェース型」を「型制約」として使うことで表す。

このことを、3つの基本的な具体例で見ていきましょう。

  • 型パラメータを持つ関数f[T Stringer]
  • 型パラメータを持つ型Stack[T any]
  • 型パラメータを持つ型Stack[T any]

具体例1: 型パラメータを持つ関数f[T Stringer]

まず「型パラメータを持つ関数」の具体例を見てみましょう。

func main() {
	fmt.Println(f([]MyInt{1, 2, 3, 4}))
    // Output:
    // [1 2 3 4]
}

// fは型パラメータを持つ関数
// Tは型パラメータ
// インタフェースStringerは、Tに対する型制約として使われている
func f[T Stringer](xs []T) []string {
	var result []string
	for _, x := range xs {
        // xは型制約StringerによりString()メソッドが使える
		result = append(result, x.String())
	}
	return result
}

type Stringer interface {
	String() string
}

type MyInt int

// MyIntはStringerを実装する
func (i MyInt) String() string {
	return strconv.Itoa(int(i))
}

https://go.dev/play/p/NWxONCa85DL

関数fの宣言時にf[T Stringer]という四角カッコの文法要素がついていますね。これが型パラメータと一緒に導入される新しい文法です。この意味は、

  • 関数fにおいて型パラメータTを宣言する
  • T型はインタフェースStringerを満たす型である、という型制約を設ける

という意味です。このように宣言した型パラメータは関数の他の部分で参照できます。例えば引数の型としてTを使うことができます。よって、f[T Stringer](xs []T)というのは、引数xsとして型Tのスライス型[]Tを受け取る、という意味になります。

具体例2: 型パラメータを持つ型Stack[T any]

関数だけでなく、型も「型パラメータ」を持つことができます。

一例として、データ構造「スタック」を実装してみます。

type Stack[T any] []T

func New[T any]() *Stack[T] {
	v := make(Stack[T], 0)
	return &v
}

func (s *Stack[T]) Push(x T) {
	(*s) = append((*s), x)
}

func (s *Stack[T]) Pop() T {
	v := (*s)[len(*s)-1]
	(*s) = (*s)[:len(*s)-1]
	return v
}

func main() {
	s := New[string]()
	s.Push("hello")
	s.Push("world")
	fmt.Println(s.Pop()) // world
	fmt.Println(s.Pop()) // hello
}

https://go.dev/play/p/jCS7vhCe_XC

まず型定義において、type Stack[T any] []Tとしています。もしstringに限定したスタックであればtype Stack []stringと定義するところです。このstringの部分をパラメータ化するために[T any]を追加したわけです。

ここで、anyは新しく導入される識別子で、空インタフェースinterface{}の別名です。anyを書けるところには代わりにinterface{}を書いても構いませんし、その逆もOKです。Stackの内容になる要素型は何の型であっても良いですから、anyを型制約にするのが適切です。

次にコンストラクタであるNew関数を見てみます。型自体がパラメータ化されているので、コンストラクタも型パラメータを持つ関数としています。

StackはメソッドPushPopを持ちます。型パラメータを持つ型に対してメソッドを宣言するときは、次のような構文を使います。

func(s *Stack[T]) Push(x T)

*とポインタにしてあるのはポインタレシーバにするためで、これは従来通りの文法です。少し覚えにくいのはレシーバの型をStack[T]のようにして型パラメータをつける必要があるところです。このTをメソッド内の別な場所で参照することができます。Pushの場合は引数の型として(x T)と使っていますね。

最後にmainを見てみましょう。

	s := New[string]()

という行がありますね。関数宣言ではなく、関数呼び出しの方に[string]がついています。これは型引数(type argument)で、型パラメータに具体的な型を渡すための構文です。

型引数は通常は省略可能です。それは、関数の引数の型と型パラメータをマッチングさせて型パラメータを型推論できる場合が多いからです。しかし、New関数には引数がないため、具体的な型引数を渡さないと型推論ができず、コンパイルが失敗します。

メソッド宣言において新たな型パラメータは宣言できない

メソッドも関数の一種なので、メソッド宣言時に新たな型パラメータを宣言できるのかという疑問が湧くかもしれません。これはできないことになっています。

例えば次のようなコードが書きたくなりますが、書けません。

// これは書けない
func (s *Stack[T]) ZipWith[S,U any](x *Stack[S], func(T, S) U) *Stack[U] {
    // ...
}

こういうことをしたければメソッドではない関数として定義する必要があります。

// これは書ける
func ZipWith[S,T,U any](x *Stack[T], y *Stack[S], func(T, S) U) *Stack[U] {
    // ...
}

https://go.dev/play/p/-_HxaTjE_Zi

これができないのはいくつかの点で不便です。メソッドチェーンのようなAPIを作れなかったり、型によるコードのグループ化の妨げになることがあるからです。

ただし、この制約は将来解除される可能性が高いと思います。次のプロポーザルで検討が進んでいるからです。ただし、2026年2月現在では未承認のプロポーザルなので、まだどうなるかはわからないことに留意してください。

https://github.com/golang/go/issues/77273

具体例3: 型パラメータを持つ型Set[T comparable]

次に、いわゆるSet型を定義してみましょう。

これはある型の値をただ集めた「集合」として使えるデータ型です。Goにおいては、次のようにmapのキーだけを使う方法で実装すると簡単です。そのキーとして使う型を「ジェネリック」にしたいです。

type Set[T comparable] map[T]struct{}

func New[T comparable](xs ...T) Set[T] {
	s := make(Set[T])
	for _, xs := range xs {
		s.Add(xs)
	}
	return s
}

func (s Set[T]) Add(x T) {
	s[x] = struct{}{}
}

func (s Set[T]) Includes(x T) bool {
	_, ok := s[x]
	return ok
}

func (s Set[T]) Remove(x T) {
	delete(s, x)
}

func main() {
	s := New(1, 2, 3)
	s.Add(5)
	fmt.Println(s.Includes(3)) // true
	s.Remove(3)
	fmt.Println(s.Includes(3)) // false
}

https://go.dev/play/p/ht_akn1eCGy
型定義に注目してください。

type Set[T comparable] map[T]struct{}

ここで、comparableという新しいインタフェース型が型制約に使われています。なぜanyではダメなのでしょうか?

それは、Tmapのkeyとして使いたいからです。mapはkeyの値に重複がないように値を保管していくデータ構造なので、重複しているかどうかを判定できる必要があります。その判定には==及び!=演算子による比較が用いられます。Go言語ではこの2つの演算子により比較できる型と比較できない型があるため、「比較可能なすべての型により満たされるインタフェース」が必要なのです。

しかしそのようなインタフェースをユーザが定義することはできません。そこでGo言語はcomparableというインタフェースを予め定義されたものとして提供することにしました。これを使えば、genericに使えるSet型を簡単に作ることができます。

ジェネリクスがないGoで何ができなかったのか

ここでジェネリクスのモチベーションをより具体的にみるために、ジェネリクスがなかった時代(Go1.17まで)のコードを考えてみます。

インタフェース型のスライスを受け取る関数

まず、Go1.17において次のインタフェースと関数を考えます。

type Stringer interface {
    String() string
}

func f(xs []Stringer) []string {
    var result []string
    for _, x := range xs {
        result = append(result, x.String())
    }
    return result
}

また、次のようにStringerを実装する型を用意します。

type MyInt int

// MyIntはStringerを実装する
func(i MyInt) String() string {
    return strconv.Itoa(int(i))
}

このとき次のように、MyIntのスライスをfに渡すことはできるでしょうか?

xs := []MyInt{0,1,2}
f(xs) // fは[]Stringerを受け付ける

このようなコードは書けません。MyIntStringerを満たすのでMyInt型の値はStringer型の変数に代入可能ですが、[]MyInt型の値は[]Stringer型の変数に代入できないためです。

Go1.17で[]Stringerを一般的に扱う関数を書くには、次のf2のように空インタフェース型interface{}を受け取るようにするしかありませんでした。この関数f2にはどんな型の値でも渡せてしまうので、関数の利用側で間違った値を渡さないように気をつけなければいけません。

// 【注意】 Stringerを実装する型Tのスライス[]Tだけを渡すこと
func f2(xs interface{}) {
    if vs,ok := xs.([]MyInt); ok {
        // vsに関する処理
    }
    // ... 
}

型パラメータによる記述

Stringerインタフェースを実装する型Tのスライス[]Tだけを渡すこと

という条件付けは、型パラメータを使うと次のように記述できます。

// fは型パラメータを持つ関数
// Tは型パラメータ
// インタフェースStringerは、Tに対する型制約として使われている
func f[T Stringer](xs []T) []string {
	var result []string
	for _, x := range xs {
        // xは型制約StringerによりString()メソッドが使える
		result = append(result, x.String())
	}
	return result
}

このfには[]MyInt型の値だけでなく、何のコードの変更もなしにStringerを実装する型Tのスライス[]Tを渡せます。また、そうでない型の値を渡した場合はコンパイルエラーとして検出できます。

まとめ

この章で学んだことをまとめておきましょう:

  • 「関数」と「型」は「型パラメータ」を持つことができる。
  • 「型パラメータ」の満たすべき性質は「インタフェース型」を「型制約」として使うことで表す。
  • 関数や型の後に[T constraint]という文法要素をつけると、「型パラメータTを宣言する。Tはconstraintを満たさなければならない」という意味になる。constraintは型制約と呼ばれ、インタフェース型を用いる。
  • メソッドに追加で型パラメータを宣言することはできない。
  • anyインタフェースは空インタフェースinterface{}の別名である
  • 比較可能、つまり==, !=による等値判定が可能な型により満たされるcomparableが提供される。
  • 型パラメータの重要な使い方の1つは、スライスやマップなどのいわゆるコレクション型の抽象化である。

また、基本的な用語を、少しフォーマルな表現でまとめておきます。

用語 意味
型パラメータ 関数宣言・型宣言の時点では定まっていない型に対するプレースホルダーとして働く型のこと。
ジェネリック関数 型パラメータ付きで宣言された関数のこと。
ジェネリック型 型パラメータ付きで宣言された型のこと。
型制約 型パラメータを宣言するときに必ずセットで指定しなければいけないインタフェース型のこと。型制約によって、(1)その型パラメータに対して実際に渡せる具体的な型の範囲と、(2)型パラメータで表される抽象的な型の値に対してできる操作の範囲が決まる。

unions

Go1.18ではunionsという文法要素を使って、従来のインタフェースでは表現できない型制約を定義することができます。

genericなMax関数とunions

標準パッケージのmath.Max関数はfunc Max(x, y float64) float64というシグネチャを持ち、float64の値しか渡すことができません。

せっかく型パラメータが使えるようになるので、genericなMax関数を作ってみたいと思います。まず初めに次のようなコードを考えました。

func Max(T any) (x, y T) T {
	if x >= y {
		return x
	}
	return y
}

ところが、このコードは動作しません。実行すると次のエラーメッセージが出力されます。Tの型制約はanyなので、演算子>=で比較できるとは限らないからです。

invalid operation: cannot compare x >= y (operator >= not defined on T)

無効な演算: x >= yという比較はできません。(演算子 >= は 型Tで定義されていません)

それでは、適当なインタフェース型を定義して演算子>=で比較できるような型制約にすることはできるでしょうか?

Go1.17までは、できませんでした。なぜなら、Go1.17までのインタフェース型とは「メソッドセット」すなわちメソッドの集合(集まり)を定義するものであって、「ある演算子が使える」というようなメソッド以外の型の性質を表すことはできないからです。

そこでGo言語は、「インタフェース型」として次のようなものも定義できるように機能を拡張することにしました。

type Number interface {
    int | int32 | int64 | float32 | float64
}

このNumberというインタフェースは、int, int32, int64, float32, float64という5種類の型によって 「満たされ」ます。かつ、これ以外の型によっては満たされません。
この文法要素int | int32 | int64 | float32 | float64のことをunionsunion elementと呼びます。

大切なことは、Numberを実装する全ての型は、演算子>=をサポートしていることです。これにより、次のような関数を書くことができます。

type Number interface {
    int | int32 | int64 | float32 | float64
}

func Max[T Number] (x, y T) T {
	if x >= y {
		return x
	}
	return y
}

まとめ

  • Goの型パラメータは型制約をインタフェース型によって表現するが、型の性質には「メソッドを持つ」以外の性質もある。その性質の一部はunionsを利用した新しいインタフェース型によって表現できる。
  • <, >, <=, >=による順序付可能性はunionsを使って順序づけられる型のみを列挙することで表現できる。
  • ==, !=による比較可能性はcomparableインタフェースで表現する(再掲)。

~とunderlying type

モチベーション

前章ではunionsを使ったインタフェース定義により、複数の数値型に適用できるgenericなMax関数を作れることを見ました。

type Number interface {
    int | int32 | int64 | float32 | float64
}

func Max[T Number] (x, y T) T {
	if x >= y {
		return x
	}
	return y
}

では、次のように定義したNewIntNewNewIntに対してMax関数を使用できるでしょうか?

type NewInt int

type NewNewInt NewInt

「できない」というのが答えです。int, NewInt, NewNewIntはそれぞれ相異なる型であり、したがってNewIntNewNewIntNumberインタフェースを実装しないからです。

~をつかってunderlying typeをマッチングする

NewIntNewNewIntも数値型であることに変わりはなく、>=などの演算子で比較することができるのですから、このような型を許すインタフェースを作りたいです。

もちろん、NewIntを直接unionsに加えればNewIntNumberを実装させることはできます:

// NewIntとNewNewIntがNumberを実装するようになった
type Number interface { 
    int | int32 | int64 | float32 | float64 | NewInt | NewNewInt
}

しかし、「intを元にして型定義で作られる新しい型」は無限にあるので、それら全てがNumberを実装するようにしたいです。そのための文法として、Go言語は~を導入しました。

type Number interface { 
    ~int | ~int32 | ~int64 | ~float32 | ~float64
}

このように定義すると、「int, int32, int64, float32, float64のうちいずれかをunderlying typeとする型」すべてがNumberを実装するようになります。

これにより、次のようなコードが書けるようになります。

type Number interface { 
    ~int | ~int32 | ~int64 | ~float32 | ~float64
}

func Max[T Number] (x, y T) T {
	if x >= y {
		return x
	}
	return y
}

var x y NewInt = 1, 2

max := Max(x, y) // max == NewInt(2)

underlying type

Go言語の全ての型は、それに対応する"underlying type"という型を持っています。

1つの型に対して、対応するunderlying typeは必ず1つだけ存在します。underlying typeを持たない型や、underlying typeを2つ以上持つ型は存在しません。

具体例

まず具体例を見てみます。

type NewInt int // NewIntのunderlying typeはint

type NewNewInt NewInt // NewNewIntのunderlying typeもint

// intのunderlying typeはint

type IntSlice []int // IntSliceのunderlying typeは[]int

// []intのunderlying typeは[]int

大まかにいうと、type A Bという形の型定義を左から右に遡ってゆき、それ以上遡れないところにある型がunderlying typeです。

厳密な定義(Go 1.17)

https://go.dev/ref/spec##Types によると、

Each type T has an underlying type: If T is one of the predeclared boolean, numeric, or string types, or a type literal, the corresponding underlying type is T itself. Otherwise, T's underlying type is the underlying type of the type to which T refers in its type declaration.

とあります。つまり、

  • Tが事前宣言されたboolean, 数値, 文字列型や型リテラルのとき、Tのunderlying typeはT自身である
  • それ以外の場合、(Ttype T Xのように定義された型なので)Tのunderlying typeはXのunderlying typeである

のように再帰的な定義になっています。

より丁寧な解説を見たいかたは、DQNEOさんによる次の発表を見るのが良いと思います。

cmpパッケージ

<, >で順序づけできる型をunionsで列挙できることは分かりましたが、実際に全ての型を書こうとすると面倒だなと思われた方もいると思います。

そこで、<, >で順序づけできる型によって満たされる型制約は標準パッケージcmpcmp.Orderedとして提供されています。

実装は次のようになっています。

// Ordered is a constraint that permits any ordered type: any type
// that supports the operators < <= >= >.
// If future releases of Go add new ordered types,
// this constraint will be modified to include them.
//
// Note that floating-point types may contain NaN ("not-a-number") values.
// An operator such as == or < will always report false when
// comparing a NaN value with any other value, NaN or not.
// See the [Compare] function for a consistent way to compare NaN values.
type Ordered interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 |
		~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
		~float32 | ~float64 |
		~string
}

これを使って、一般的なMax関数を定義できます。

https://go.dev/play/p/-WB97e8w2NC

package main

import (
	"cmp"
	"fmt"
)

type NewInt int

var x, y NewInt = 3, 2

func main() {
	fmt.Println(Max(x, y)) // 3
}

func Max[T cmp.Ordered](x, y T) T {
	if y > x {
		return y
	}
	return x
}

unionsの要素としてどんな型でも書いていいのか

細かい話になりますが、unionsの要素として使える型については、少し制約があります。

unionsが複数要素からなるとき、その要素になれるのは

  • 非インタフェース型
  • メソッド定義を含まないインタフェース型

です。つまり、fmt.StringerのようにString() stringというメソッド定義を含んでいる型は複数要素からなるunionsの要素とすることができません。

まとめ

  • ~をつかうと型定義によって作りうる無限の型にインタフェースを実装させることができる
  • ~TTをunderlying typeに持つすべての型を表す

できそうだが、実際にはできないこと

Goジェネリクスで「できそうなのにできないこと」がいくつかあります。重要な例は 「メソッドにおいて独自の型パラメータを追加できない」 という制限です。これについては最初の章ですでに説明しました。

この章では、その他の「できそうでできないこと」を説明しましょう。ただ単純に列挙しても良いのですが、より整理して理解するために、 Goのジェネリクスで「できそうなこと」とはそもそも何なのかを考えてみます。

そのために、Goのジェネリクスがどういうものだったかおさらいします。ジェネリックな関数の場合で考えると、型制約として渡されたインターフェースで宣言されているメソッドは、関数のbody(実装)のなかであたかも型パラメータ型のメソッドであるかのように使って良いのでした。これを示すため、この記事の最初のサンプルコードを再掲します:

// fは型パラメータを持つ関数
// Tは型パラメータ
// インタフェースStringerは、Tに対する型制約として使われている
func f[T Stringer](xs []T) []string {
	var result []string
	for _, x := range xs {
        // xは型制約StringerによりString()メソッドが使える
		result = append(result, x.String())
	}
	return result
}

type Stringer interface {
	String() string
}

type MyInt int

// MyIntはStringerを満たす
func (i MyInt) String() string {
	return strconv.Itoa(int(i))
}

言い換えると、 「型制約を満たすすべての型についてString()が使えるならば、型パラメータTに対してもString()が使える」 というのがGoのジェネリック関数だと言っても良さそうです。

もしも、この文を一般化した 「型制約を満たすすべての型について操作Xが可能ならば、型パラメータTに対しても操作Xが可能である」 というテーゼが成り立つなら非常に分かりやすく、ある意味で理想的です。Goのジェネリクスに対して「できそうなこと」だとプログラマーが期待することの多くは、このテーゼが成り立って欲しいという期待に基づいていると思います。そこでこの記事では、これをジェネリクスの 「理想のテーゼ」 と呼ぶことにします。

しかし、2026年2月(Go1.25)現在、この「理想のテーゼ」は必ずしも成り立ちません。"操作X"に様々なものを当てはめて、それをみていきましょう。

次のセクションから、型パラメータTは型制約Constaraintを満たすものとします。そして、Tの値について考えられるさまざまな操作Xを、

  • 「理想のテーゼ」が成り立つ操作 = できそうで、実際にできること
  • 「理想のテーゼ」が成り立たない操作 = できそうで、実際にはできないこと

の2つに分類します。

できそうで、実際にできること

次のそれぞれの「操作X」については、 「型制約を満たすすべての型について操作Xが可能ならば、型パラメータTに対しても操作Xが可能である」 というテーゼが成り立ちます。

  • nilの代入
  • 型リテラルの値の代入
  • Representability(表現可能性)
  • 算術演算
  • 比較演算(==, !=)と順序演算(<=など)
  • チャネル受信演算
  • 型変換
  • clear関数の適用
  • len,cap関数の適用
  • unsafe.Pointeruintptrの間の型変換

nilの代入

型制約Constraintを満たすすべての型についてnilを代入できるならば、型パラメータTの変数にもnilを代入可能です。

よって、次のコードはコンパイルできます。

type Constraint interface {
	[]int | ~[]string | *bool
}

func f[T Constraint]() {
	var _ T = nil
}

https://go.dev/play/p/cH5ktnw3Yck

型リテラルの値の代入

型制約Constraintを満たすすべての型について型リテラルで表される型Vの値が代入可能ならば、型パラメータTの変数にもVの値を代入可能です。

よって、次のコードはコンパイルできます。

type Constraint interface {
	DefinedIntSliceA | DefinedIntSliceB
}

type DefinedIntSliceA []int
type DefinedIntSliceB []int

func f[T Constraint]() {
	var _ T = []int{}
}

https://go.dev/play/p/R21YGC2O8nV

型制約Constraintを満たすすべての型について、その値が型リテラルで表される型Vの変数に代入可能ならば、型パラメータTの値はVの変数に代入可能です。

よって、次のコードはコンパイルできます。

type Constraint interface {
	DefinedIntSliceA | DefinedIntSliceB
}

type DefinedIntSliceA []int
type DefinedIntSliceB []int

func f[T Constraint]() {
	var t T
	var _ []int = t
}

https://go.dev/play/p/8ifyI02FINX

Representability(表現可能性)

型制約Constraintを満たすすべての型によって、ある型なし定数cが表現可能ならば、その型なし定数cは型パラメータTによって表現可能です。

よって、次のコードはコンパイルできます。

type Constraint interface {
	complex128 | float64
}

func f[T Constraint]() {
	const c = 1.1
	var _ T = c // 表現可能なので代入可能である
}

https://go.dev/play/p/FJO4JhKl09x

算術演算

型制約Constraintを満たすすべての型についてある算術演算が可能ならば、型パラメータTの値に対してもその算術演算が可能です。

よって、次のコードはコンパイルできます。

type Constraint interface {
	int | float32 | float64
}

func f[T Constraint](t1, t2 T) T {
	return t1 * t2
}

https://go.dev/play/p/BDeKlBse44u

比較演算(==, !=)と順序演算(<=など)

型制約Constraintを満たすすべての型が比較可能ならば、型パラメータTの値もその比較可能です。

よって、次のコードはコンパイルできます。

package main

func f[T comparable](t1, t2 T) bool { return t1 == t2 }

func main() {
	var x1, x2 any
	f(x1, x2)
}

https://go.dev/play/p/wagDyQk6xRp

型制約Constraintを満たすすべての型が<などで順序づけできるならば、Tの値に対しても順序づけできます。

よって、次のコードはコンパイルできます。

type Constraint interface {
	int | string | float32
}

func f[T Constraint](t1, t2 T) bool { return t1 < t2 }

https://go.dev/play/p/JqPmRpRYgkN

チャネル受信演算

型制約Constraintを満たすすべての型が、型Sの値を受信できるチャネル型ならば、型パラメータTの値からも型Sの値を受信できます。

よって、次のコードはコンパイルできます。

// 型Sはこの場合intに相当する
type MyChanInt <-chan int

type Constraint interface {
	chan int | <-chan int | MyChanInt
}

func f[T Constraint](t T) int {
	return <-t
}

https://go.dev/play/p/YKXhTLD6Uwy

型変換

型変換については3つのパターンを説明します。

型制約Constraintを満たすすべての型Vについて、型Vから別な型Wへの型変換ができるならば、型パラメータTからWへの型変換が可能です。

よって、次のコードはコンパイルできます。

type Constraint interface {
	int | int32 | int64 // 全てfloat64への型変換が可能
}

func f[T Constraint](t T) {
	var _ = float64(t)
}

https://go.dev/play/p/CGVytLgeERL

型制約Constraintを満たすすべての型Vについて、別な型WからVへの型変換ができるならば、Wから型パラメータTへの型変換が可能です。

よって、次のコードはコンパイルできます。

type Constraint interface {
	int | int32 | int64 // 全てfloat64からの型変換が可能
}

func f[T Constraint]() {
	var v float64 = 1.1
	var _ = T(v)
}

https://go.dev/play/p/EghdRMpCycD

型制約Constraint1を満たす全ての型Vと、型制約Constraint2を満たす全ての型Wについて、VからWへの型変換が可能ならば、それぞれ対応する型パラメータT1からT2への型変換が可能です。

よって、次のコードはコンパイルできます。

type Constraint1 interface {
	int | int32 | int64 // 全てfloat32, float64への型変換が可能
}

type Constraint2 interface {
	float32 | float64
}

func f[T1 Constraint1, T2 Constraint2](t1 T1) T2 {
	return T2(t1)
}

https://go.dev/play/p/u01fTfEmoJc

clear関数の適用

型制約Constraintを満たす全ての型について、clear関数の適用が可能ならば、型パラメータTの値に対してもclear関数の適用が可能です。

よって、次のコードはコンパイルできます。

type Constraint interface {
	[]int | map[int]int | []string
}

func f[T Constraint](t T) {
	clear(t)
}

https://go.dev/play/p/pzsv02pBSaH

len,cap関数の適用

型制約Constraintを満たす全ての型について、len,cap関数の適用が可能ならば、型パラメータTの値に対してもこれらの関数の適用が可能です。

よって、次のコードはコンパイルできます。

type MyInt int

type Constraint interface {
	map[int]bool | map[MyInt]bool | []MyInt
}

func f[T Constraint](m T) int {
	return len(m)
}

https://go.dev/play/p/ZO8mpMzLukQ

unsafe.Pointeruintptrの間の型変換

  • 型制約Constraintを満たす全ての型について、unsafe.Pointerへの型変換ができるならば、型パラメータTの値もunsafe.Pointerへの型変換ができます。
  • 型制約Constraintを満たす全ての型について、uintptrへの型変換ができるならば、型パラメータTの値もuintptrへの型変換ができます。

よって、次のコードはコンパイルできます。

import "unsafe"

type MyUintPtr uintptr

type Constraint interface {
	uintptr | MyUintPtr
}

func f[T Constraint](ptr T) unsafe.Pointer {
	return unsafe.Pointer(ptr)
}

https://go.dev/play/p/_INs7vJ5TKb

できそうだが、実際にはできないこと

次のそれぞれの「操作X」については、 「型制約を満たすすべての型について操作Xが可能ならば、型パラメータTに対しても操作Xが可能である」 というテーゼが 必ずしも成り立ちません。

  • フィールドの読み取り
  • 定数宣言
  • コンポジットリテラルの使用
  • インデックス式の使用
  • スライス式の使用
  • 関数呼び出し(型パラメータ型自体が関数型というケース)
  • チャネルへの送信
  • range句を使ったfor文
  • append関数による要素の追加
  • channelのclose関数
  • 複素数に関する操作
  • mapのdelete関数による特定エントリーの削除
  • make関数による作成

フィールドの読み取り

型制約Constraintを満たすすべての型について、その値xのフィールドのセレクタ式x.Fが有効だとしても、型パラメータTの値tについてt.Fは有効ではありません。

よって、次のコードはコンパイルできません。

type AB struct {
	A int
	B int
}
type ABC struct {
	AB
	C int
}

type Constraint interface {
	AB | ABC
}

func f[T Constraint](t T) int {
	return t.A
}

https://go.dev/play/p/IUcO6kAVYu3

定数宣言

型制約Constraintを満たすすべての型について、その型を持つ定数を定数式expで宣言できるとしても、型パラメータTの型を持つ定数を宣言することはできません。

定数宣言の型として型パラメータTを使うこと自体ができないためです。

よって、次のコードはコンパイルできません。

type Constraint interface {
	complex128 | float64
}

func f[T Constraint]() {
	const exp = 1.1
	const _ T = exp
}

https://go.dev/play/p/HKUPvDpkLmm

コンポジットリテラルの使用

型制約Constraintを満たすすべての型について、その型のコンポジットリテラルが使えるとしても、型パラメータTのコンポジットリテラルが使えるとは限りません。

追加条件として、Tを満たすすべての型が、同一のunderlying typeを持つ必要があります。

よって、次のコードはコンパイルできません。

type Constraint interface {
	[]int | [1]int
}

func f[T Constraint]() {
	var _ = T{}
}

https://go.dev/play/p/Ogs7lmQL3Cj

インデックス式の使用

型制約Constraintを満たすすべての型について、その型の式からインデックス式が作れるとしても、型パラメータTの式にインデックス式が使えるとは限りません。

追加条件として、Tを満たすすべての型が、同一の要素型を持つ必要があります。

よって、次のコードはコンパイルできません。

type Constraint interface {
	[1]int | [1]string // どちらもインデックス式が作れるが、要素型がintとstringで異なる
}

func f[T Constraint]() {
	var t T
	_ = t[0] // このようなインデックス式は無効
}

https://go.dev/play/p/G1JrWC1UQKm

スライス式の使用

型制約Constraintを満たすすべての型について、その型の式からスライス式が作れるとしても、型パラメータTの式にスライス式が使えるとは限りません。

追加条件として、Tを満たすすべての型が同一のunderlying typeを持つ必要があります。ただし、string型と[]byte型はこのルールの適用上は同一視して良いことになっています。

よって、次のコードはコンパイルできません。

type Constraint interface {
	[10]int | [11]int // どちらもインデックス式が作れるが、underlying typeが異なる
}

func f[T Constraint]() {
	var t T
	_ = t[:] // このようなスライス式は無効
}

https://go.dev/play/p/y0ZsHgjBtre

関数呼び出し(型パラメータ型自体が関数型というケース)

型パラメータFの型制約Constraintを満たすすべての型について、その型が関数型であり、特定の引数(a)に対して関数呼び出しが可能だとしても、型パラメータFの値である関数についてその呼び出しが可能だとは限りません。

追加条件として、Constraintを満たすすべての型が同一のunderlying typeを持つ必要があります。

よって、次のコードはコンパイルできません。

type MyIntPointer *int

type Constraint interface {
	func() *int | func() MyIntPointer
}

func f[F Constraint]() {
	var f F
	var _ *int = f() // 無効: MyIntPointerは*intに代入可能であるにもかかわらず。
}

https://go.dev/play/p/gUfpOnaSmKj

チャネルへの送信

型制約Constraintを満たすすべての型について、その型がチャネル型であり、ある値をその型のチャネルに送信可能だとしても、型パラメータTの値であるチャネルにその値を送信可能であるとは限りません。

追加条件として、Constraintを満たすすべての型について、その要素型が同一でなければいけません。

よって、次のコードはコンパイルできません。

type MyInt int
type MyChanInt chan<- MyInt

type Constraint interface {
	chan int | chan<- int | MyChanInt // 要素型がintとMyIntで一致しない
}

func f[T Constraint](t T) {
	t <- 1 // 無効
}

https://go.dev/play/p/J2wFa_fZ39n

range句を使ったfor文

型制約Constraintを満たすすべての型について、その型の値vを使ってfor _,e := range v {...}というfor文によって型Eの値eを取り出せるしても、型パラメータTの値に対して同様の操作ができるとは限りません。

追加条件として、Constraintを満たす全ての型のunderlying typeが同一である必要があります。

よって、次のコードはコンパイルできません。

type Constraint interface {
	string | []byte // E = byteとすればどちらもfor文でbyteを取り出せる型である
}

func f[T Constraint](t T) {
	for _, v := range t {
	}
}

https://go.dev/play/p/og_cj81k-NM

append関数による要素の追加

型制約Constraintを満たすすべての型について、append関数である値を追加することができたとしても、型パラメータTについて同じことができるとは限りません。

追加条件として、Constraintを満たす全ての型のunderlying typeが同一である必要があります。

よって、次のコードはコンパイルできません。

type MyInt int

type Constraint interface {
	[]int | []MyInt
}

func f[T Constraint](t T) {
	append(t, 1)
}

https://go.dev/play/p/UxrSm8UIz_o

channelのclose関数

型制約Constraintを満たすすべての型について、その値をclose関数に渡すことができるとしても、型パラメータTの値をclose関数に渡せるとは限りません。

追加条件として、Constraintを満たす全ての型の要素型が同一である必要があります。

よって、次のコードはコンパイルできません。

type Constraint interface {
	chan int | chan string
}

func f[T Constraint](ch T) {
	close(ch)
}

https://go.dev/play/p/5huphqnb64r

複素数に関する操作

型制約Constraintを満たすすべての型について、その値をreal,imag,complexのそれぞれの関数に渡せるとしても、型パラメータTの値をこれらの関数に渡すことはできません。

これらの関数はそもそも型パラメータ型を受け取らないようになっているからです。

よって、次のコードはコンパイルできません。

type Constraint interface {
	float32 | float64
}

func f[T Constraint](v T) {
	_ = complex(v, v)
}

https://go.dev/play/p/7PMcp7Q91oM

mapのdelete関数による特定エントリーの削除

型制約Constraintを満たすすべての型について、その値mとあるキー値kについてdelete(m,k)によるエントリー削除ができるとしても、型パラメータTの値mに対してdelete(m,k)ができるとは限りません。

追加条件として、Constarintを満たす全ての型についてキーの型が同一である必要があります。

よって、次のコードはコンパイルできません。

type MyInt int

type Constraint interface {
	map[int]bool | map[MyInt]bool
}

func f[T Constraint](m T) {
	delete(m, 1)
}

https://go.dev/play/p/__j2DhnYrUn

make関数による作成

型制約Constraintを満たすすべての型について、その値をmake関数で作れるとしても、型パラメータTについてその値をmake関数で作れるとは限りません。

追加条件として、次のいずれかに当てはまる必要があるからです。

  • Constraintを満たす全ての型のunderlying typeが同一のスライス型またはmap型である
  • Constraintを満たす全ての型がチャネル型であり、その要素の型が同一で、方向が矛盾しない

よって、次のコードはコンパイルできません。

type Constraint interface {
	MyChan | chan<- int
}

type MyChan <-chan int

func f[T Constraint]() {
	_ = make(T)
}

https://go.dev/play/p/QTggKJPwmlW

最後に

この記事をかくにあたり#gospecreadingから得られた理解が本質的でした。いつもありがとうございます。

GitHubで編集を提案

Discussion