Go ジェネリクス完全ガイド 2026年版:型パラメータ、制約、面接対策

2026年の技術面接で頻出するGoジェネリクスの質問を完全解説。型パラメータ、any vs comparable、チルダ演算子、カスタム制約、スレッドセーフなジェネリックキャッシュ実装まで網羅します。

Go ジェネリクス完全ガイド 2026年版:型パラメータ、制約、面接対策

Go 1.18は2022年3月にジェネリクスを導入し、その後Go 1.21、1.22、そしてそれ以降のバージョンで機能は成熟してきました。本記事では、2026年の技術面接で頻出するGoジェネリクスの質問について、基本的な型パラメータから高度な制約パターンまで解説します。

Goジェネリクスとは

Goジェネリクスは、コンパイル時の型安全性を保ちながら、あらゆるデータ型で動作する関数や型を記述することを可能にします。実行時に型アサーションを使用するインターフェースとは異なり、ジェネリクスはコンパイル時に型を解決するため、パフォーマンスの向上と早期のエラー検出を実現します。

Goにおける型パラメータの理解

型パラメータはGoジェネリクスの基盤を形成します。型パラメータは、ジェネリック関数や型が使用される際に指定される型のプレースホルダーです。

generic.gogo
// 型パラメータTを持つ基本的なジェネリック関数
func PrintSlice[T any](items []T) {
    for _, item := range items {
        fmt.Println(item)
    }
}

// 使用例 - 型推論がTを自動的に決定
func main() {
    PrintSlice([]int{1, 2, 3})        // Tはint
    PrintSlice([]string{"a", "b"})     // Tはstring
}

角括弧構文 [T any] は、any で制約された型パラメータ T を宣言します。Goのコンパイラは引数から具体的な型を推論するため、ほとんどの場合で明示的な型指定は不要です。

面接頻出質問:anycomparable の違いとは

面接では、組み込み制約である anycomparable についてよく質問されます。これらの違いを理解することで、ジェネリクスの確かな知識を示すことができます。

constraints.gogo
// any: すべての型を受け入れる(interface{}のエイリアス)
func Process[T any](value T) T {
    return value
}

// comparable: == と != をサポートする型のみ
func Contains[T comparable](slice []T, target T) bool {
    for _, v := range slice {
        if v == target {  // この比較にはcomparableが必要
            return true
        }
    }
    return false
}

// これはコンパイルされる
Contains([]int{1, 2, 3}, 2)

// これは失敗:スライスはcomparableではない
// Contains([][]int{{1}, {2}}, []int{1})

comparable 制約は、等価演算子をサポートする型に型パラメータを制限します。スライス、マップ、関数は、言語レベルで等価性が定義されていないため除外されます。

インターフェースによるカスタム型制約の作成

Goはインターフェースを使用してカスタム制約を定義し、ジェネリック関数が受け入れられる型を拡張します。Go仕様書では、制約インターフェース内での型要素の動作方法が定義されています。

number.gogo
// 型共用体を使用したカスタム制約
type Number interface {
    int | int8 | int16 | int32 | int64 |
    uint | uint8 | uint16 | uint32 | uint64 |
    float32 | float64
}

// 数値型に制約されたジェネリック関数
func Sum[T Number](values []T) T {
    var total T
    for _, v := range values {
        total += v  // すべてのNumber型が+をサポートするため動作
    }
    return total
}

func main() {
    fmt.Println(Sum([]int{1, 2, 3}))         // 6
    fmt.Println(Sum([]float64{1.5, 2.5}))    // 4.0
}

| を使用した型共用体は、制約を満たす正確な型を指定します。このアプローチは、実行時の型アサーションを避けながら、any よりも細かい制御を提供します。

面接質問:制約における ~ チルダ演算子を説明してください

制約におけるチルダ演算子 ~ は、ある型とその基底型を持つすべての型にマッチします。この質問は、Goの型システムの理解度を試すものです。

underlying.gogo
// ~intはintと基底型がintのすべての型にマッチ
type Signed interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

// 基底型がintのカスタム型
type UserID int
type Temperature int64

func Abs[T Signed](value T) T {
    if value < 0 {
        return -value
    }
    return value
}

func main() {
    var id UserID = -42
    var temp Temperature = -10
    
    fmt.Println(Abs(id))    // 42 - ~intがUserIDにマッチするため動作
    fmt.Println(Abs(temp))  // 10 - ~int64がTemperatureにマッチするため動作
}

~ がない場合、制約 int は正確に int 型のみにマッチし、UserID のようなカスタム型にはマッチしません。チルダは派生型を含めるよう制約を拡張し、ジェネリックコードをより柔軟にします。

Goの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

ジェネリック型:構造体とメソッド

ジェネリック型は関数を超えて構造体やメソッドにも拡張されます。このパターンはデータ構造の実装で頻繁に使用されます。

stack.gogo
// ジェネリックStack型
type Stack[T any] struct {
    items []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  // 型Tのゼロ値
        return zero, false
    }
    index := len(s.items) - 1
    item := s.items[index]
    s.items = s.items[:index]
    return item, true
}

func main() {
    intStack := Stack[int]{}
    intStack.Push(10)
    intStack.Push(20)
    
    val, ok := intStack.Pop()  // val=20, ok=true
}

ジェネリック型のメソッドは角括弧で型パラメータを繰り返す必要がありますが、新しい型パラメータを導入することはできません。レシーバー (s *Stack[T]) は、メソッドを Stack の特定のインスタンス化にバインドします。

面接質問:なぜメソッドは独自の型パラメータを持てないのですか

Goはメソッドに追加の型パラメータを持つことを禁止しています。これは、JavaやC#から来た開発者を驚かせる設計上の決定です。Goジェネリクス提案では、型システムを扱いやすく保つためにこの制限が存在すると説明されています。

go
// これは有効なGoコードではない
type Container[T any] struct {
    value T
}

// エラー:メソッドは型パラメータを持てない
// func (c *Container[T]) Transform[U any](fn func(T) U) U {
//     return fn(c.value)
// }

// 有効な代替案:スタンドアロン関数を使用
func Transform[T, U any](c *Container[T], fn func(T) U) U {
    return fn(c.value)
}

回避策として、メソッドの代わりに複数の型パラメータを持つトップレベル関数を使用します。この設計により、メソッドディスパッチがシンプルに保たれ、レシーバー型とメソッド型パラメータ間の複雑な相互作用を回避できます。

型推論と制約推論

Goの型推論は、ジェネリック関数を呼び出す際の冗長性を軽減します。明示的な型引数が必要な場合を理解することで、よりクリーンなコードを書くことができます。

inference.gogo
func Map[T, R any](input []T, transform func(T) R) []R {
    result := make([]R, len(input))
    for i, v := range input {
        result[i] = transform(v)
    }
    return result
}

func main() {
    numbers := []int{1, 2, 3}
    
    // 型推論:T=int、R=stringは引数から推論される
    strings := Map(numbers, func(n int) string {
        return fmt.Sprintf("%d", n)
    })
    
    // 複雑なケースでは明示的な型が必要な場合がある
    // Map[int, string](numbers, converter)
}

型推論は関数引数から型パラメータへと働きます。コンパイラが引数だけから型を推論できない場合、角括弧内の明示的な型引数で曖昧さを解消します。

制約パッケージ:標準ライブラリの cmpslices

Go 1.21では、Ordered 制約と比較関数を持つ cmp パッケージが追加されました。slices パッケージは、標準ライブラリにおける慣用的なジェネリックコードを示しています。

stdlib.gogo
import (
    "cmp"
    "slices"
)

func main() {
    numbers := []int{3, 1, 4, 1, 5, 9}
    
    // slices.Sortは内部でcmp.Ordered制約を使用
    slices.Sort(numbers)  // [1, 1, 3, 4, 5, 9]
    
    // ソート済みスライスでの二分探索
    index, found := slices.BinarySearch(numbers, 4)
    
    // cmp.Compareは-1、0、または1を返す
    result := cmp.Compare(3, 5)  // -1
    
    // cmp.Orは最初のゼロでない値を返す
    value := cmp.Or(0, 0, 42, 100)  // 42
}

標準ライブラリドキュメントは、これらのパッケージが順序付き型に対する型安全な操作のためにジェネリクスをどのように活用しているかを示しています。これらのパッケージに精通していることは、理論的な理解を超えた実践的なジェネリクスの知識を示します。

面接問題:ジェネリックキャッシュの実装

一般的な面接課題では、ジェネリックでスレッドセーフなキャッシュの実装を求められます。これはジェネリクス、syncパッケージを使った並行性、およびAPI設計をテストします。

cache.gogo
import (
    "sync"
    "time"
)

type Cache[K comparable, V any] struct {
    mu    sync.RWMutex
    items map[K]cacheItem[V]
}

type cacheItem[V any] struct {
    value      V
    expiration time.Time
}

func NewCache[K comparable, V any]() *Cache[K, V] {
    return &Cache[K, V]{
        items: make(map[K]cacheItem[V]),
    }
}

func (c *Cache[K, V]) Set(key K, value V, ttl time.Duration) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.items[key] = cacheItem[V]{
        value:      value,
        expiration: time.Now().Add(ttl),
    }
}

func (c *Cache[K, V]) Get(key K) (V, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    
    item, exists := c.items[key]
    if !exists || time.Now().After(item.expiration) {
        var zero V
        return zero, false
    }
    return item.value, true
}

この実装では、キーに comparable(マップの要件)、値に any を使用しています。別個の cacheItem 構造体は、ジェネリック型が互いにネストできることを示しています。スレッドセーフ性は sync.RWMutex から得られ、これはGoの並行性面接でカバーされるパターンです。

ゼロ値と型制約

ジェネリックコードでゼロ値を処理するには、Goがパラメータ化された型の変数をどのように初期化するかを理解する必要があります。

zero.gogo
// スライスが空の場合にゼロ値を返す
func First[T any](slice []T) T {
    if len(slice) == 0 {
        var zero T  // ゼロ値:intは0、stringは""、ポインタはnil
        return zero
    }
    return slice[0]
}

// 代替案:曖昧さを避けるためにポインタを返す
func FirstOrNil[T any](slice []T) *T {
    if len(slice) == 0 {
        return nil
    }
    return &slice[0]
}

var zero T パターンは、任意の型 T のゼロ値を作成します。ゼロが有効な値である型(整数の 0 など)では、ポインタを返すバリアントが「見つからない」と「ゼロを見つけた」を区別します。

上級:複数の制約の組み合わせ

複雑なジェネリック関数では、型が複数の制約を満たす必要がある場合があります。Goはインターフェースの埋め込みによってこれを処理します。

combined.gogo
// 順序付けと文字列変換の両方を必要とする制約
type Stringable interface {
    String() string
}

type OrderedStringable interface {
    cmp.Ordered
    Stringable
}

// 代替案:インラインで複数の制約を持つ型パラメータを使用
func PrintSorted[T interface{ cmp.Ordered; fmt.Stringer }](items []T) {
    slices.Sort(items)
    for _, item := range items {
        fmt.Println(item.String())
    }
}

インターフェースの埋め込みは制約を組み合わせ、型がすべての埋め込まれたインターフェースを実装することを要求します。インライン構文 interface{ A; B } は、名前付き制約型を宣言せずに同じ機能を提供します。

まとめ

  • [T any] を使用した型パラメータは、実行時リフレクションなしで再利用可能で型安全なコードを書くことを可能にします
  • comparable==!= をサポートする型に制限し、マップキーに必要です
  • ~ チルダは特定の基底型を持つ型にマッチし、制約の柔軟性を拡張します
  • カスタム制約は型共用体(int | string)を使用して許可される正確な型を指定します
  • メソッドは独自の型パラメータを持てません。代替としてトップレベル関数を使用します
  • 標準ライブラリパッケージ cmpslices は慣用的なジェネリックパターンを示します
  • var zero T によるゼロ値は、ジェネリック関数で空またはデータがない場合を処理します
  • スレッドセーフなジェネリックデータ構造は、ジェネリクスとsyncプリミティブを組み合わせます

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

共有

関連記事