1. Home
  2. Golang
  3. GuideToBecomingGoDeveloper
  4. GoingDeeper
  5. ジェネリクス(Generics) - Golang learning step 2-10

ジェネリクス(Generics) - Golang learning step 2-10

  • 公開日
  • カテゴリ:GoingDeeper
  • タグ:Golang,roadmap.sh,学習メモ
ジェネリクス(Generics) - Golang learning step 2-10

roadmap.sh > Go > Going Deeper > Generics の学習を進めていきます。

※ 学習メモとしての記録ですが、後にこのセクションを学ぶ道しるべとなるよう、ですます調で記載しています。

contents

  1. 開発環境
  2. 参考 URL
  3. Go のジェネリクス(Generics)
  4. ジェネリクスの基本構文
  5. 型制約
    1. インターフェースを使用した型制約
  6. ジェネリクスの利点
  7. ジェネリックな関数の例
  8. ジェネリックな構造体の例
  9. ジェネリクスを使用する際のベストプラクティス
  10. 注意点

開発環境

  • チップ: Apple M2 Pro
  • OS: macOS Sonoma
  • go version: go1.23.2 darwin/arm64

参考 URL

Go のジェネリクス(Generics)

ジェネリクスは、型に依存しないコードを作成するための機能です。Goでは、特定の型に依存しない汎用的な関数やデータ構造を作成できるよう、バージョン 1.18 から正式にジェネリクスがサポートされています。 ジェネリクスを使用することで、型安全性を保ちながら、より柔軟で再利用可能なコードを書くことができます。

ジェネリクスの基本構文

Go のジェネリクスは、type パラメーターを用いて定義します。以下はジェネリックな関数の基本的な構文です。

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

ここでは、Max 関数がジェネリクスを使用して定義されており、T という型パラメーターを受け取ります。この T は、関数が呼ばれる際に具体的な型に置き換えられます。

型制約

ジェネリクスでは、型制約を使って、許容する型の範囲を指定することができます。Go では constraints パッケージが提供されており、よく使われる制約が定義されています。例えば、数値型に制約するには以下のようにします。

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

func Sum[T constraints.Ordered](a, b T) T {
  return a + b
}

constraints.Ordered は、比較可能な数値型や文字列型に制約を課すものです。具体的には、数値型(int や float)および文字列型を指します。これにより、Sum 関数が数値や文字列にのみ適用されることを保証します。

constraints パッケージの主要な型制約一覧

// 主な型制約の例
constraints.Ordered  // 順序付け可能な型(数値型、文字列型)
constraints.Integer  // 整数型
constraints.Float    // 浮動小数点数型
constraints.Complex  // 複素数型

constraints package - golang.org/x/exp/constraints - Go Packages

なお、constraints パッケージを使用するには、以下の手順でパッケージをインストールする必要があります:

  1. まず import 文を追加します。
import "golang.org/x/exp/constraints"
  1. その後、以下のコマンドを実行してモジュールを取得します。
go mod tidy

インターフェースを使用した型制約

型制約のインターフェースを使用することで、特定の型セットに対してのみ動作するジェネリックな関数やデータ構造を定義できます。このパターンは、型安全性を保ちながら、柔軟な実装を可能にします。 以下のコードは、数値型に特化した型制約の例を示しています。

package main

import (
  "fmt"
)

// Number 数値型を表す型制約インターフェース
type Number interface {
  ~int | ~int32 | ~int64 | ~float32 | ~float64
}

// Sum 数値のスライスの合計を計算する関数
func Sum[T Number](numbers []T) T {
  var sum T
  for _, n := range numbers {
    sum += n
  }
  return sum
}

func main() {
  ints := []int{1, 2, 3, 4, 5}
  floats := []float64{1.1, 2.2, 3.3, 4.4, 5.5}

  fmt.Println(Sum(ints))   // 出力: 15
  fmt.Println(Sum(floats)) // 出力: 16.5
}
  1. 型制約インターフェースの定義
    • チルダ(~)演算子は、基本型とその型をベースとしたカスタム型も含めることを示します。例えば、type MyInt int のようなカスタム型もこの制約を満たします
    • パイプ(|)演算子はいずれかの型を使用できることを示します
  2. ジェネリック関数の実装
    • [T Number] は、型パラメータ T が Number インターフェースを満たす必要があることを示します
    • この制約により、数値型以外(例:文字列)での関数呼び出しはコンパイルエラーとなります。例えば、Sum([]string{"a", "b"}) はコンパイルエラーとなります

このジェネリックな関数の利点は、同じ関数で整数型と浮動小数点型の両方を扱えることと、新しい数値型(カスタム型)を追加しても、既存のコードを変更する必要がないことです。このように、柔軟性と再利用性の高い関数を作成できます。

ジェネリクスの利点

  1. コードの再利用
    • 型に依存しないため、複数のデータ型に対して同じ関数や構造体を使用できます。
  2. 可読性の向上
    • 型の変換や冗長なコードを書く必要が減り、よりシンプルで読みやすいコードになります。
  3. コンパイル時の安全性
    • ジェネリクスにより型チェックがコンパイル時に行われ、実行時エラーが減ります

ジェネリックな関数の例

ジェネリックな関数の例として、2 つの異なる型の値の最大値を返す関数 Max を示します。この関数は、任意の型に対応するようにジェネリクスを使用しています。

package main

import (
    "fmt"
    "golang.org/x/exp/constraints"
)

// ジェネリックな Max 関数の定義
func Max[T constraints.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

func main() {
    // int 型で Max 関数を使用
    intA, intB := 3, 5
    fmt.Printf("Max of %d and %d is %d\n", intA, intB, Max(intA, intB))

    // float64 型で Max 関数を使用
    floatA, floatB := 4.5, 2.3
    fmt.Printf("Max of %.1f and %.1f is %.1f\n", floatA, floatB, Max(floatA, floatB))

    // string 型で Max 関数を使用
    strA, strB := "apple", "banana"
    fmt.Printf("Max of %s and %s is %s\n", strA, strB, Max(strA, strB))
}
  1. ジェネリックな Max 関数の定義
    • Max[T constraints.Ordered] でジェネリックな関数を定義しています。
    • Tconstraints.Ordered で制約されており、この制約は「比較可能な型」(整数、浮動小数点数、文字列など数値および文字列型のみに使用可能)を意味します。
    • a > b のように比較演算子を使うため、比較可能な型に制約する必要があります。
  2. Max 関数のロジック
    • 引数 ab を比較し、大きい方の値を返します。
    • a > b が true であれば a を返し、そうでなければ b を返すシンプルなロジックです。
  3. main 関数での使用例
    • Max 関数はジェネリックなので、異なる型の値に対して使用できます。
    • int 型、float64 型、string 型の3種類で Max 関数を呼び出しています。

実行結果:

Max of 3 and 5 is 5
Max of 4.5 and 2.3 is 4.5
Max of apple and banana is banana

ジェネリックな構造体の例

ジェネリクスは構造体にも使えます。以下は、任意の型のスタックを実装する例です。以下で定義する Stack 構造体を使用することで、任意の型 T に対応するスタックを作成できます。

package main

import "fmt"

// ジェネリックなスタック構造体の定義
type Stack[T any] struct {
  items []T // T型の要素を格納するスライス
}

// Push - スタックに要素を追加
func (s *Stack[T]) Push(item T) {
  s.items = append(s.items, item)
}

// Pop - スタックから要素を取り出し、削除
func (s *Stack[T]) Pop() (T, bool) {
  if len(s.items) == 0 {
    var zero T
    return zero, false // スタックが空の場合
  }
  item := s.items[len(s.items)-1]
  s.items = s.items[:len(s.items)-1]
  return item, true
}

func main() {
  // int 型のスタックを作成
  intStack := Stack[int]{}
  intStack.Push(10)
  intStack.Push(20)
  intStack.Push(30)

  fmt.Println("Integer Stack:")
  for {
    item, ok := intStack.Pop()
    if !ok {
      break // スタックが空になった場合
    }
    fmt.Println(item)
  }

  // string 型のスタックを作成
  stringStack := Stack[string]{}
  stringStack.Push("Hello")
  stringStack.Push("World")

  fmt.Println("\nString Stack:")
  for {
    item, ok := stringStack.Pop()
    if !ok {
      break // スタックが空になった場合
    }
    fmt.Println(item)
  }
}
  1. Stack 構造体の定義
    • Stack[T any] は任意の型 T のスタックを表すジェネリックな構造体です。
  2. Push メソッド
    • スタックに要素を追加します。
  3. Pop メソッド
    • スタックから要素を取り出し、削除します。
    • スタックが空の場合、型 T のゼロ値(int 型なら 0、string 型なら空文字)と false を返します。
  4. main 関数での使用
    • intStack として int 型のスタックを作成し、Push メソッドで整数を追加後、Pop メソッドで取り出します。
    • 同様に、stringStack として string 型のスタックを作成し、文字列を操作します。

実行結果:

Integer Stack:
30
20
10

String Stack:
World
Hello

ジェネリクスを使用する際のベストプラクティス

  1. 必要な場合のみ使用
    • コードの複雑性が増すため、本当に必要な場合のみ使用する
  2. 明確な型制約
    • 可能な限り具体的な型制約を使用し、anyの使用は最小限に
  3. 読みやすさの重視
    • 型パラメータには意味のある名前を使用(例えば汎用的な T や、データに応じて型が明確にわかる Element や Key などの具体的な名前を使う)
  4. インターフェースの活用
    • 共通の振る舞いを持つ型には、インターフェースによる型制約を定義

注意点

  1. パフォーマンスへの影響
    • ジェネリクスを使用すると、若干のパフォーマンスオーバーヘッドが発生する可能性がある
  2. コンパイル時間
    • ジェネリックなコードは、コンパイル時間が長くなる可能性がある
      • ジェネリクスを使用することでコンパイラがジェネリックな型に応じた実装が生成されるため
  3. 後方互換性
    • Go 1.18以前のバージョンでは使用できない

ジェネリクスは強力な機能ですが、適切な使用が重要です。必要な場合にのみ使用し、コードの可読性とメンテナンス性を考慮しながら設計することが大切です。

まとめ

  • Go 1.18からの正式サポート開始
  • 型に依存しない汎用的なコードの作成が可能
  • type パラメーターによる基本構文の提供
  • constraints パッケージによる型制約の実現
  • インターフェースを用いた柔軟な型制約の定義
  • 関数やデータ構造での幅広い活用
  • コードの再利用性と可読性の向上
  • コンパイル時の型安全性の確保
  • パフォーマンスやコンパイル時間への影響に要注意
  • 必要な場合のみの適切な使用が推奨


[Next] Step 2-11: ポインタ(Pointers)

[Prev] Step 2-9: スケジューラー(Scheduler)

Author

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級