目次

    Go 1.18+ ジェネリクス徹底解説:基本構文から実践的なユースケースまで
    Goジェネリクス型パラメータ制約型安全性Go 1.18

    2025-06-15

    Go 1.18+ ジェネリクス徹底解説:基本構文から実践的なユースケースまで

    導入:Goジェネリクスは、何を変えたのか?

    Go言語を使っていて、こんな経験はありませんか?「int用のスライス操作関数を書いたけど、string用にも同じような関数が必要になった」「interface{}を使ったら型安全性が失われて、実行時エラーに悩まされた」。

    go
    1// ジェネリクス導入前:型ごとに関数を定義する必要があった
    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で導入されたジェネリクス(型パラメータ) は、まさにこれらの問題を解決する「待望の機能」でした。

    go
    1// ジェネリクス版:一つの関数で複数の型に対応、かつ型安全
    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

    この呼び出しでintIndex1を、strIndex1を返します(それぞれ要素が見つかったインデックス)。

    この記事では、ジェネリクスの基本構文から実践的なユースケース、そして「使いどころ」の勘所まで、具体的なコード例と共に解説していきます。


    ジェネリクスの基本を理解する

    Goジェネリクスは主に「型パラメータ」「制約」「ジェネリックな関数/型」の3要素で構成されます。シンプルなコードと共に見ていきましょう。 ※これらの概念は公式のGo Tourでも実際に試すことができます

    1. 型パラメータ(Type Parameters)

    関数や型の定義に[T any]のような型パラメータリストを追加します。Tが、この関数/型の中で使われる任意の「型」を表す変数(型パラメータ)です。

    2. 制約(Constraints)

    型パラメータが満たすべき条件を定義するものです。

    anycomparable

    • anyは最も緩い制約で、どんな型でも許容します(interface{}のエイリアスです)。
    • comparable==!=で比較可能な型(数値、文字列、ポインタ、structなど)に限定する、Goに組み込まれた制約です。

    カスタム制約: インターフェースを用いて、より具体的な制約を自分で定義できます。

    go
    1// 複数の数値型を許容する制約
    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 を使うか既存制約を使うかは好みと用途で選びましょう。

    3. ジェネリックな「関数」と「型」

    これらを使って、汎用的な関数やデータ構造を作ります。

    go
    1// ジェネリック関数: 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") // 文字列をプッシュ

    実践!ジェネリクスのパワフルなユースケース3選

    実務で役立つ具体的な応用例を紹介します。

    1. データ構造の実装を劇的に簡潔に

    Set(集合)やキュー、ツリーといった汎用的なデータ構造も、ジェネリクスを使えば型ごとに実装する必要がなくなり、コードの再利用性が劇的に向上します。

    go
    1// 任意の比較可能な型の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}

    2. スライス操作の共通化

    Map(各要素を変換)やFilter(条件に合う要素を抽出)といった、スライスに対する高階関数をジェネリクスで簡単に実装でき、定型的なループ処理を共通化できます。

    go
    1// スライスの各要素を、指定された関数で変換する
    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]が格納されます

    3. 関数オプションパターン:従来版で十分な例

    オブジェクトの初期化でよく使われる「関数オプションパターン」を例に、ジェネリクスが必ずしも必要ではないケースを見てみましょう。

    go
    1import "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{}で十分な例として、適切な技術選択の判断基準を示しています。


    ジェネリクスの「使いどころ」:何でもジェネリクスはアンチパターン

    技術的な成熟度を示すために、ジェネリクスを適切に使い分けることが重要です。

    推奨されるケース

    • スライス、マップ、チャネルなど、特定のデータ構造を操作する関数
    • 汎用的なデータ構造(コンテナ型)の実装(Stack, Queue, Set, Treeなど)
    • 異なる型で、振る舞い(アルゴリズム)が全く同じ場合

    避けるべき、または慎重になるべきケース

    • インターフェースで十分な場合: 異なる型が「同じメソッドを持つ」ことで共通化できるなら、従来通りインターフェースを使うのがGoらしい解決策です。

    • 可読性の低下: 型制約が複雑になりすぎ、関数のシグネチャが難解になる場合は、ジェネリクスを使わないシンプルな実装の方が良い場合もあります。

    • 性能クリティカルなホットパス: メソッドインスタンス化コストが無視できない場合は専用実装の方が速い


    まとめ:ジェネリクスを使いこなし、ワンランク上のGoエンジニアへ

    ジェネリクスはコードの重複を減らし、型安全性を高める強力なツールです。しかし、その真価は「いつ使うべきか、いつ使うべきでないか」を正しく判断することにあります。

    主要なジェネリクス活用パターン

    パターン用途制約
    Find[T comparable]スライス検索comparable
    Sum[T Number]数値計算カスタム制約
    Set[T comparable]データ構造comparable
    Map[T, U any]関数型操作any

    ※関数オプションパターンなど、インターフェースで十分な場合はジェネリクス不要です。

    このようなモダンな言語機能を深く理解し、適切に使いこなすスキルは、フリーランスエンジニアの市場価値を大きく左右します。私たちGoForceは、あなたの高度な技術力を正当に評価するプロジェクトへ繋ぐお手伝いをします。ぜひ一度、ご相談ください。

    参考文献

    会員登録はこちら

    最適なGo案件を今すぐチェック!

    会員登録

    生年月日 *

    /

    /

    Go経験年数 *

    /

    利用規約プライバシーポリシーに同意してお申し込みください。