フリーキーズ | 独学プログラミング

Go言語ジェネリクス入門!型パラメータの書き方を初心者向けに解説

リンドくん

リンドくん

たなべ先生、Go言語で「ジェネリクス」って機能があるって聞いたんですけど、これって何なんですか?

たなべ

たなべ

ジェネリクスは型を抽象化する仕組みで、Go 1.18から正式に導入されたんだ。
簡単に言うと、同じような処理を複数の型で使い回せるようになる機能なんだよ。

Go言語を学習している皆さんの中で、「同じような処理なのに、int用とstring用で別々の関数を書くのは面倒だな...」と感じたことはありませんか?

実は、Go言語のバージョン1.18からジェネリクス(Generics)という画期的な機能が追加され、このような悩みを解決できるようになりました。
ジェネリクスを使えば、型を抽象化して
より柔軟で再利用性の高いコード
を書くことができます。

本記事では、Go言語のジェネリクスについて、プログラミング初心者の方でも理解できるよう、基本概念から実践的な使い方まで段階的に解説していきます。

プログラミング学習でお悩みの方へ

HackATAは、エンジニアを目指す方のためのプログラミング学習コーチングサービスです。 経験豊富な現役エンジニアがあなたの学習をサポートします。

✓ 質問し放題

✓ β版公開中(2025年内の特別割引)

HackATAの詳細を見る

ジェネリクスとは何か?基本概念を理解しよう

リンドくん

リンドくん

型を抽象化するって具体的にはどういうことなんですか?

たなべ

たなべ

例えば、配列の中から最大値を見つける関数を考えてみよう。
今まではint用、float用、string用と別々に書く必要があったけど、ジェネリクスを使えば1つの関数で全部対応できるようになるんだ。

ジェネリクスが解決する問題

Go言語でジェネリクスが導入される前は、同じような処理でも型が違うと別々の関数を書く必要がありました。例えば、以下のような状況です。

// int用の最大値を求める関数
func MaxInt(a, b int) int {
    if a > b {
        return a
    }
    return b
}

// float64用の最大値を求める関数
func MaxFloat(a, b float64) float64 {
    if a > b {
        return a
    }
    return b
}

// string用の最大値を求める関数
func MaxString(a, b string) string {
    if a > b {
        return a
    }
    return b
}

このようにコードの重複が発生し、保守性が悪くなってしまいます。

ジェネリクスによる解決

ジェネリクスを使用すると、上記の3つの関数を1つにまとめることができます。

func Max[T comparable](a, b T) T {
    if a > b {
        return a
    }
    return b
}

この1つの関数で、intfloat64stringなど、比較可能な全ての型に対応できるのです。

ジェネリクスの主なメリット

  • コードの重複を削減 - 同じ処理を複数回書く必要がなくなります
  • 型安全性の向上 - コンパイル時に型チェックが行われます
  • 保守性の向上 - 修正が必要な場合も1箇所だけで済みます
  • 再利用性の向上 - 様々な型で同じ関数を使い回せます

型パラメータの基本的な書き方

型パラメータの構文

Go言語のジェネリクスでは、型パラメータを使って型を抽象化します。基本的な構文は以下の通りです。

func 関数名[型パラメータ名 制約](引数) 戻り値の型 {
    // 関数の処理
}

構文の要素説明

  • [型パラメータ名 制約] - 角括弧内で型パラメータを定義
  • 型パラメータ名 - 通常はTUVなどの大文字を使用
  • 制約 - その型パラメータが満たすべき条件を指定

シンプルな例から始めよう

最も基本的な例として、2つの値を受け取って最初の値を返すだけの関数を見てみましょう。

func First[T any](a, b T) T {
    return a
}

この関数の使用例:

func main() {
    // int型で使用
    result1 := First[int](10, 20)
    fmt.Println(result1) // 10

    // string型で使用
    result2 := First[string]("hello", "world")
    fmt.Println(result2) // hello

    // 型推論を使用(型パラメータを省略)
    result3 := First(3.14, 2.71)
    fmt.Println(result3) // 3.14
}

ポイント

  • anyは「どんな型でも受け入れる」という意味の制約です
  • 型パラメータは明示的に指定することも、Go言語の型推論に任せることもできます

複数の型パラメータを使用する場合

複数の型パラメータを定義することも可能です。

func Pair[T any, U any](first T, second U) (T, U) {
    return first, second
}

// 使用例
func main() {
    name, age := Pair[string, int]("太郎", 25)
    fmt.Printf("名前: %s, 年齢: %d\n", name, age)
}

制約(Constraints)の使い方

リンドくん

リンドくん

さっきから出てくる「制約」って何ですか?any以外にもあるんですか?

たなべ

たなべ

制約は型パラメータが満たすべき条件を指定するものなんだ。
anyは「何でもOK」だけど、例えば「比較できる型のみ」や「数値型のみ」といったより具体的な条件を指定できるよ。

組み込みの制約

Go言語には、よく使用される制約がいくつか組み込まれています。

1. any - 任意の型

func Print[T any](value T) {
    fmt.Println(value)
}

2. comparable - 比較可能な型

func Contains[T comparable](slice []T, value T) bool {
    for _, v := range slice {
        if v == value {
            return true
        }
    }
    return false
}

カスタム制約の定義

独自の制約を定義することも可能です。これにより、より細かい型の条件を指定できます。

// 数値型の制約を定義
type Number interface {
    int | int64 | float32 | float64
}

// 数値型のみを受け入れる関数
func Add[T Number](a, b T) T {
    return a + b
}

使用例

func main() {
    result1 := Add[int](10, 20)        // 30
    result2 := Add[float64](3.14, 2.86) // 6.0
    
    // string型は使用不可(コンパイルエラー)
    // result3 := Add[string]("a", "b") // エラー
}

メソッドを含む制約

制約にメソッドを含めることで、特定のメソッドを持つ型のみを受け入れることができます。

type Stringer interface {
    String() string
}

func PrintString[T Stringer](value T) {
    fmt.Println(value.String())
}

よくあるつまずきポイントと解決法

リンドくん

リンドくん

ジェネリクス、便利そうですね!でも何か注意することはありますか?

たなべ

たなべ

確かに便利だけど、使いすぎに注意が必要なんだ。
また、エラーメッセージが複雑になることもあるから、最初はシンプルな例から始めるのがおすすめだよ。

つまずきポイント1 制約の選択ミス

問題: 不適切な制約を選んでしまい、コンパイルエラーが発生する

// 間違った例:数値の比較に any を使用
func Max[T any](a, b T) T {
    if a > b { // エラー:any型では比較できない
        return a
    }
    return b
}

解決法: 適切な制約を使用する

// 正しい例:比較可能な型に制約
func Max[T comparable](a, b T) T {
    if a > b {
        return a
    }
    return b
}

つまずきポイント2 型推論への過度な依存

問題: 型推論に頼りすぎて、意図しない型が推論される

func Process[T any](value T) T {
    // 何らかの処理
    return value
}

func main() {
    // 型が曖昧になる可能性
    result := Process(nil) // T は何型?
}

解決法: 明示的に型を指定する

func main() {
    // 明示的に型を指定
    result := Process[*string](nil)
}

つまずきポイント3 パフォーマンスの誤解

注意点: ジェネリクスはランタイムのパフォーマンスオーバーヘッドはほぼありませんが、コンパイル時間が増加する可能性があります。

ベストプラクティス

  • 本当に必要な場合のみジェネリクスを使用する
  • シンプルなケースでは従来の方法も検討する
  • 型パラメータの数は最小限に抑える

まとめ

リンドくん

リンドくん

ジェネリクス、最初は難しそうに感じましたが、だんだん理解できてきました!

たなべ

たなべ

最初は基本的な使い方から始めて、徐々に複雑な例に挑戦していけばいいんだ。
ジェネリクスをマスターすれば、より柔軟で再利用性の高いGoコードが書けるようになるよ。

Go言語のジェネリクスについて、基本概念から実践的な活用法まで解説してきました。ジェネリクスは一見複雑に見えるかもしれませんが、その本質は「型を抽象化して、再利用性の高いコードを書く仕組み」です。

重要なポイントを再確認しましょう。

  • 型パラメータを使って、複数の型で同じ処理を共有できる
  • 制約により、型パラメータが満たすべき条件を指定できる
  • 適切な制約の選択がエラーの回避につながる
  • シンプルなケースから始めることが習得のコツ

ジェネリクスは、Go言語をより表現力豊かで効率的な言語にしてくれる強力な機能です。
最初は基本的な関数から始めて、徐々にデータ構造やより複雑な処理に応用していけば、必ずマスターできるでしょう。

プログラミング学習において重要なのは、実際に手を動かして試してみることです。
今回紹介したサンプルコードを実際に書いて動かしてみて、ジェネリクスの威力を体感してみてください。

この記事をシェア

関連するコンテンツ