0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

表紙

ジェネリクスとは何か

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

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

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

ジェネリクスがない場合

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

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

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

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
}

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

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

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

ジェネリクスの基本構文

// 関数定義
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 型も受け入れます。

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 です。

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

型制約

  • any:任意の型を受け入れる
  • comparable:==や!=の操作をサポートする
  • ordered:大小比較(> <)をサポートする

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

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

ジェネリクスを使う場面

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

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プロジェクトのホスティングの最適解です。

Leapcell

Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:

複数言語サポート

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

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

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

比類のないコスト効率

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

洗練された開発者体験

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

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

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

ドキュメントで詳細を確認!

Try Leapcell

Xでフォローする:@LeapcellHQ


ブログでこの記事を読む

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?