2025-06-15
Go言語を使っていて、こんな経験はありませんか?「int用のスライス操作関数を書いたけど、string用にも同じような関数が必要になった」「interface{}
を使ったら型安全性が失われて、実行時エラーに悩まされた」。
go1// ジェネリクス導入前:型ごとに関数を定義する必要があった 2 3func FindInt(slice []int, target int) int { 4 for i, v := range slice { 5 if v == target { 6 return i 7 } 8 } 9 return -1 10} 11 12func FindString(slice []string, target string) int { 13 for i, v := range slice { 14 if v == target { 15 return i 16 } 17 } 18 return -1 19} 20 21// または interface{} を使うと型安全性が失われる 22 23func Find(slice []interface{}, target interface{}) int { 24 for i, v := range slice { 25 if v == target { // 実行時エラーのリスク 26 return i 27 } 28 } 29 return -1 30}
Go 1.18で導入されたジェネリクス(型パラメータ) は、まさにこれらの問題を解決する「待望の機能」でした。
go1// ジェネリクス版:一つの関数で複数の型に対応、かつ型安全 2 3func Find[T comparable](slice []T, target T) int { 4 for i, v := range slice { 5 if v == target { 6 return i 7 } 8 } 9 return -1 10} 11 12// 使用例 13 14intIndex := Find([]int{1, 2, 3}, 2) // 1 15strIndex := Find([]string{"a", "b"}, "b") // 1
この呼び出しでintIndex
は1
を、strIndex
も1
を返します(それぞれ要素が見つかったインデックス)。
この記事では、ジェネリクスの基本構文から実践的なユースケース、そして「使いどころ」の勘所まで、具体的なコード例と共に解説していきます。
Goジェネリクスは主に「型パラメータ」「制約」「ジェネリックな関数/型」の3要素で構成されます。シンプルなコードと共に見ていきましょう。 ※これらの概念は公式のGo Tourでも実際に試すことができます
関数や型の定義に[T any]
のような型パラメータリストを追加します。T
が、この関数/型の中で使われる任意の「型」を表す変数(型パラメータ)です。
型パラメータが満たすべき条件を定義するものです。
any
とcomparable
:
any
は最も緩い制約で、どんな型でも許容します(interface{}
のエイリアスです)。comparable
は==
や!=
で比較可能な型(数値、文字列、ポインタ、structなど)に限定する、Goに組み込まれた制約です。カスタム制約: インターフェースを用いて、より具体的な制約を自分で定義できます。
go1// 複数の数値型を許容する制約 2// (package main、import文は省略) 3 4type Number interface { 5 int | int64 | float32 | float64 6}
💡 Tip: Go 1.20 以降は golang.org/x/exp/constraints から constraints.Ordered が go/constraints に移り、整数・浮動小数をまとめて扱えます。自作の Number を使うか既存制約を使うかは好みと用途で選びましょう。
これらを使って、汎用的な関数やデータ構造を作ります。
go1// ジェネリック関数: Number制約を満たす型Tのスライスの合計を計算 2 3func Sum[T Number](vals []T) T { 4 var sum T 5 for _, v := range vals { 6 sum += v 7 } 8 return sum 9} 10 11// 使用例 12 13total := Sum([]float64{1.5, 2.5}) // total == 4.0 14 15// ジェネリック型: 任意の型Tを保持できるスタック 16 17type Stack[T any] struct { 18 vals []T 19} 20 21func (s *Stack[T]) Push(val T) { 22 s.vals = append(s.vals, val) 23} 24 25// 使用例 26 27stack := Stack[string]{} 28stack.Push("hello") // 文字列をプッシュ
実務で役立つ具体的な応用例を紹介します。
Set(集合)やキュー、ツリーといった汎用的なデータ構造も、ジェネリクスを使えば型ごとに実装する必要がなくなり、コードの再利用性が劇的に向上します。
go1// 任意の比較可能な型のSetを実装 2 3type Set[T comparable] struct { 4 m map[T]struct{} 5} 6 7func NewSet[T comparable]() *Set[T] { 8 return &Set[T]{m: make(map[T]struct{})} 9} 10 11func (s *Set[T]) Add(v T) { 12 s.m[v] = struct{}{} 13} 14 15func (s *Set[T]) Contains(v T) bool { 16 _, ok := s.m[v] 17 return ok 18}
Map
(各要素を変換)やFilter
(条件に合う要素を抽出)といった、スライスに対する高階関数をジェネリクスで簡単に実装でき、定型的なループ処理を共通化できます。
go1// スライスの各要素を、指定された関数で変換する 2 3func Map[T, U any](slice []T, f func(T) U) []U { 4 result := make([]U, len(slice)) 5 for i, v := range slice { 6 result[i] = f(v) 7 } 8 return result 9} 10 11// スライスの要素を、条件を満たすものだけ抽出する 12 13func Filter[T any](slice []T, f func(T) bool) []T { 14 var result []T 15 for _, v := range slice { 16 if f(v) { 17 result = append(result, v) 18 } 19 } 20 return result 21} 22 23// 使用例 24 25nums := []int{1, 2, 3, 4} 26doubled := Map(nums, func(i int) int { return i * 2 }) // [2, 4, 6, 8] 27evens := Filter(nums, func(i int) bool { return i%2 == 0 }) // [2, 4]
実行すると以下のように動作します:
doubled
には各要素が2倍された[2, 4, 6, 8]
が格納されますevens
には偶数のみが抽出された[2, 4]
が格納されますオブジェクトの初期化でよく使われる「関数オプションパターン」を例に、ジェネリクスが必ずしも必要ではないケースを見てみましょう。
go1import "time" 2 3// サーバー設定の例 4 5type ServerConfig struct { 6 Host string 7 Port int 8 Timeout time.Duration 9} 10 11// 従来の関数オプションパターン(ジェネリクスなし) 12 13type Option func(*ServerConfig) 14 15func WithHost(host string) Option { 16 return func(config *ServerConfig) { 17 config.Host = host 18 } 19} 20 21func WithPort(port int) Option { 22 return func(config *ServerConfig) { 23 config.Port = port 24 } 25} 26 27func NewServerConfig(opts ...Option) *ServerConfig { 28 config := &ServerConfig{ 29 Host: "localhost", 30 Port: 8080, 31 Timeout: 30 * time.Second, 32 } 33 34 for _, opt := range opts { 35 opt(config) 36 } 37 return config 38} 39 40// 使用例 41 42server := NewServerConfig( 43 WithHost("0.0.0.0"), 44 WithPort(9090), 45)
重要なポイント: この例では、従来の手法で十分であり、無理にジェネリクス化する必要はありません。ジェネリクスは「型の違いを抽象化」するものであり、「振る舞いの違いを抽象化」するインターフェースとは使い分けが重要です。実際にはinterface{}
で十分な例として、適切な技術選択の判断基準を示しています。
技術的な成熟度を示すために、ジェネリクスを適切に使い分けることが重要です。
インターフェースで十分な場合: 異なる型が「同じメソッドを持つ」ことで共通化できるなら、従来通りインターフェースを使うのがGoらしい解決策です。
可読性の低下: 型制約が複雑になりすぎ、関数のシグネチャが難解になる場合は、ジェネリクスを使わないシンプルな実装の方が良い場合もあります。
性能クリティカルなホットパス: メソッドインスタンス化コストが無視できない場合は専用実装の方が速い
ジェネリクスはコードの重複を減らし、型安全性を高める強力なツールです。しかし、その真価は「いつ使うべきか、いつ使うべきでないか」を正しく判断することにあります。
パターン | 用途 | 制約 |
---|---|---|
Find[T comparable] | スライス検索 | comparable |
Sum[T Number] | 数値計算 | カスタム制約 |
Set[T comparable] | データ構造 | comparable |
Map[T, U any] | 関数型操作 | any |
※関数オプションパターンなど、インターフェースで十分な場合はジェネリクス不要です。
このようなモダンな言語機能を深く理解し、適切に使いこなすスキルは、フリーランスエンジニアの市場価値を大きく左右します。私たちGoForceは、あなたの高度な技術力を正当に評価するプロジェクトへ繋ぐお手伝いをします。ぜひ一度、ご相談ください。
最適なGo案件を今すぐチェック!