Go言語のジェネリクス完全解説

[![表紙](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967563/0bb67b99-2543-4ffa-8479-f790fabfc052.png)](https://leapcell.io/?lc_t=q_gogeneric)

## ジェネリクスとは何か

ジェネリックプログラミング（generic programming）は、プログラミング言語の一つのスタイルまたはパラダイムです。ジェネリクスを使うことで、プログラマーは強い型付けのプログラミング言語でコードを書く際に、後で指定される型を使用でき、インスタンス化時にこれらの型をパラメータとして指定することができます。

ジェネリクスを使うと、さまざまな型に適用できるコードを書くことができ、各型ごとに同じロジックを繰り返して書く必要がなくなります。これにより、コードの再利用性、柔軟性、型安全性が向上します。

Go 言語においては、ジェネリクスは型パラメータによって実現されます。型パラメータは特殊なパラメータであり、任意の型を表すプレースホルダーです。関数、メソッド、型の定義で使用され、具体的な呼び出し時に実際の型に置き換えられます。

## ジェネリクスがない場合

このような要件を考えてみましょう。2 つの int 型の引数を受け取り、そのうち値が小さい方を返す関数を実装します。要件は非常にシンプルなので、直感的に次のようなコードを書くことができます。

```go
func Min(a, b int) int {
    if a < b {
        return a
    }
    return b
}
```

一見良さそうですが、この関数には制限があります。引数は int 型しか受け取れません。もし要件が拡張され、2 つの float64 型の引数を比較して、小さい方を返す必要が出てきた場合はどうでしょうか。

```go
func Min(a, b int) int {
    if a < b {
        return a
    }
    return b
}
func MinFloat64(a, b float64) float64 {
    if a < b {
        return a
    }
    return b
}
```

皆さんはお気づきかもしれませんが、要件が拡張されるたびに、それに応じてコードも変更し、同じことを繰り返さなければなりません。ジェネリクスはまさにこのような問題を解決します。

```go
import "golang.org/x/exp/constraints"

func Min[T constraints.Ordered](x, y T) T {
    if x < y {
        return x
    }
    return y
}
```

## ジェネリクスの基本構文

```go
// 関数定義
func F[T any](p T){...}
// 型定義
type M[T any] []T

// Constraintは具体的な型制約を表す。例: any, comparable
func F[T Constraint](p T){..}

// “~” は基底型（underlying type）の制約を表す
type E interface {
   ~string
}

// 特定の型のみを指定
type UnionElem interface {
   int | int8 | int32 | int64
}
```

### \~記号

Go 言語のジェネリクスにおいて、「\~」は基底型（underlying type）の制約を表します。

例えば、\~int は基底型が int であるすべての型を受け入れます。これにはカスタム型も含まれます。もし MyInt というカスタム型の基底型が int であれば、この制約は MyInt 型も受け入れます。

```go
type MyInt int

type Ints[T int | int32] []T

func main() {
   a := Ints[int]{1, 2}     // 正しい
   b := Ints[MyInt]{1, 2}   // コンパイルエラー
   println(a)
   println(b)
}
```

`MyInt does not satisfy int | int32 (possibly missing ~ for int in int | int32)compilerInvalidTypeArg`

以下のように修正すれば OK です。

```go
type Ints[T ~int | ~int32] []T
```

### 型制約

- any：任意の型を受け入れる
- comparable：==や!=の操作をサポートする
- ordered：大小比較（> <）をサポートする

その他の型制約については [https://pkg.go.dev/golang.org/x/exp/constraints](https://pkg.go.dev/golang.org/x/exp/constraints) を参照してください。

## いつジェネリクスを使うべきか

### ジェネリクスを使う場面

- 言語が定義するコンテナ型を操作するとき：スライス、マップ、チャネルなどのコンテナ型に対して操作を行う関数を書く場合で、要素型について特定の前提がないとき、型パラメータの利用が有用です。例えば、どんな型のマップでも全てのキーを返す関数など。
- 汎用データ構造：連結リストや二分木などの汎用データ構造について、型パラメータを使うことでより汎用的なデータ構造を作成できたり、型アサーションを回避して効率よくデータを格納し、構築時に完全な型チェックが可能になります。
- 汎用メソッドの実装：異なる型で同じ汎用メソッドを実装し、その実装が全く同じ場合、型パラメータの利用が有用です。例えば、任意のスライスタイプに対して sort.Interface を実装する汎用型など。
- 関数優先で：比較関数のようなものが必要な場合、メソッドよりも関数を優先して使用します。汎用データ型に対しても、できるだけ関数を使い、メソッドが必要な制約は避けるべきです。

```go
// SliceFnはT型スライスに対してsort.Interfaceを実装
type SliceFn[T any] struct {
    s    []T
    less func(T, T) bool
}

func (s SliceFn[T]) Len() int {
    return len(s.s)
}
func (s SliceFn[T]) Swap(i, j int) {
    s.s[i], s.s[j] = s.s[j], s.s[i]
}
func (s SliceFn[T]) Less(i, j int) bool {
    return s.less(s.s[i], s.s[j])
}
```

```go
// SortFnは比較関数を用いてスライスsをその場でソート
func SortFn[T any](s []T, less func(T, T) bool) {
    sort.Sort(SliceFn[T]{s, less})
}
```

### ジェネリクスを使わない場面

- インターフェース型の代用はしない：ある型の値のメソッドだけを呼び出す場合、型パラメータではなくインターフェース型を使うべきです。インターフェース型の関数を型パラメータの関数に変換するべきではありません。
- 型ごとに異なる実装が必要な場合：各型で実装が異なるメソッドが必要な場合は、型パラメータを使うのではなく、インターフェース型と異なる実装を用意すべきです。
- リフレクションの適切な利用：メソッドすら持たない型も含めて多様な型をサポートし、かつ型ごとに異なる操作が必要な場合はリフレクション（反射）を利用します。例として、encoding/json パッケージはリフレクションを利用しています。

### 簡単な指針

同じ内容のコードを何度も書いており、違いは使う型だけという場合、型パラメータの使用を検討しても良いでしょう。言い換えれば、全く同じコードを複数回書きそうだと気付くまでは、型パラメータの使用を避けるべきです。

## 余談

なぜ他言語で一般的な山括弧（<>）ではなく、角括弧（\[]）を選んだのか。

[https://github.com/golang/proposal/blob/master/design/15292/2013-12-type-params.md](https://github.com/golang/proposal/blob/master/design/15292/2013-12-type-params.md)

> Use angle brackets, as in Vector. This has the advantage of being familiar to C++ and Java programmers. Unfortunately, it means that f(true) can be parsed as either a call to function f or a comparison of f\<T (an expression that tests whether f is less than T) with (true). While it may be possible to construct complex resolution rules, the Go syntax avoids that sort of ambiguity for good reason.

> Vector のように山括弧（<>）を使うこともできます。これは C++や Java のプログラマーには馴染みがあるという利点があります。しかし、f(true)のような式が、関数 f の呼び出しなのか、あるいは f\<T（f が T より小さいかを判定する式）と(true)との比較なのか、どちらにも解釈できてしまいます。複雑な解決ルールを構築することも可能かもしれませんが、Go の構文はそのような曖昧さを避けるために設計されています。

---

### [私たちはLeapcell、Goプロジェクトのホスティングの最適解です。](https://leapcell.io/?lc_t=q_gogeneric)

[![Leapcell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967563/5b39e250-81e0-4f4b-a271-d4035c92d23a.png)](https://leapcell.io/?lc_t=q_gogeneric)

[Leapcell](https://leapcell.io/?lc_t=q_gogeneric)は、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです：

**複数言語サポート**

- Node.js、Python、Go、Rustで開発できます。

**無制限のプロジェクトデプロイ**

- 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

**比類のないコスト効率**

- 使用量に応じた支払い、アイドル時間は課金されません。
- 例: $25で6.94Mリクエスト、平均応答時間60ms。

**洗練された開発者体験**

- 直感的なUIで簡単に設定できます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

**簡単なスケーラビリティと高パフォーマンス**

- 高い同時実行性を容易に処理するためのオートスケーリング。
- ゼロ運用オーバーヘッド — 構築に集中できます。

[ドキュメントで詳細を確認！](https://docs.leapcell.io/)

[![Try Leapcell](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967563/a51ec172-c41d-47d2-88d9-cafa58826c3b.png)](https://leapcell.io/?lc_t=q_gogeneric)

Xでフォローする：[@LeapcellHQ](https://x.com/LeapcellHQ)

---

[ブログでこの記事を読む](https://leapcell.io/blog/go-generics-everything-you-need-to-know)