Go 1.24ジェネリクスアーキテクチャ設計:型制約から高度なパターンまで7つのプロダクションパターン

编程语言

interface{}がジェネリクスと出会う時:Goの型安全性の覚醒

午前2時、本番環境でpanic発生。調査の結果:interface{}を使った汎用キャッシュモジュールで、Getの戻り値が誤って型アサーションされ、stringintとしてアサーションされ、実行時にクラッシュ。さらに悪いことに、同様の型アサーションが30以上のファイルに散在し、データ構造を変更するたびにグローバル検索・置換が必要だった。

これは決して稀なケースではない。Go 1.18でジェネリクスが導入される前、interface{}とリフレクションがジェネリックプログラミングの唯一の手段だったが、実行時の型安全性は完全に開発者の自律に依存していた。Go 1.24はジェネリクス能力をさらに強化し、型制約がより柔軟に、コンパイラ最適化がより成熟した。本記事では7つのプロダクション級Goジェネリクスアーキテクチャパターンを解説し、型安全で再利用可能、高性能なGoサービスの構築を支援する。


主要ポイント

  • 型制約はジェネリクスの魂:Go 1.24の型制約構文により、ジェネリクスは単なる「糖衣構文」ではなくなった
  • ジェネリックミドルウェアチェーンinterface{}に別れを告げ、型安全なHTTPミドルウェアを実現
  • ジェネリックRepositoryパターン:1行でCRUDを実現、型安全でリフレクション不要
  • Result型:ジェネリクスでerrorのif-else地獄を置き換え
  • 型安全Builder:コンパイル時にビルドの完全性を保証
  • ジェネリック並行処理パターン:ジェネリックチャネル + ジェネリックワーカープール
  • 高度な型合成:型レベルプログラミングとジェネリック制約の組み合わせ

目次

  1. Goジェネリクス核心概念リファレンス
  2. Pattern 1:型制約とインターフェース合成
  3. Pattern 2:ジェネリックミドルウェアチェーン
  4. Pattern 3:ジェネリックRepositoryパターン
  5. Pattern 4:ジェネリックエラーハンドリングとResult型
  6. Pattern 5:型安全Builderパターン
  7. Pattern 6:ジェネリック並行処理パターン
  8. Pattern 7:高度な型合成と型レベルプログラミング
  9. 5つのよくある落とし穴と解決策
  10. 10のよくあるエラートラブルシューティング
  11. 高度な最適化テクニック
  12. 比較分析:ジェネリクスvsインターフェースvsコード生成
  13. オンラインツール推奨
  14. まとめ

Goジェネリクス核心概念リファレンス

概念 構文 用途
型パラメータ [T any] ジェネリック型の宣言 func Print[T any](v T)
型制約 [T constraints.Integer] 型パラメータの範囲を制限 func Sum[T Integer](s []T) T
インターフェース制約 interface{ Method() } 特定メソッドの実装を要求 type Stringer interface{ String() string }
ユニオン型 int | float64 複数の型を許可 [T int | float64]
近似制約 ~int 基礎型を含む [T ~int]type MyInt int にマッチ
ジェネリック構造体 type Stack[T any] struct{} パラメータ化されたデータ構造 type Node[T any] struct{ Val T }
ジェネリックインターフェース type Handler[T any] interface{} パラメータ化されたインターフェース type Store[T any] interface{ Get(id string) T }
型推論 Print(42) 型パラメータを省略 コンパイラが自動的に T = int を推論

Pattern 1:型制約とインターフェース合成

問題:interface{}の実行時トラップ

func Max(values []interface{}) interface{} {
    if len(values) == 0 {
        return nil
    }
    m := values[0]
    for _, v := range values[1:] {
        switch m.(type) {
        case int:
            if v.(int) > m.(int) {
                m = v
            }
        case float64:
            if v.(float64) > m.(float64) {
                m = v
            }
        default:
            panic("unsupported type")
        }
    }
    return m
}

実行時panic、型アサーションの乱用、拡張不可能——これがinterface{}の三大原罪だ。

解決策:型制約 + インターフェース合成

package generic

import "cmp"

type Ordered interface {
    cmp.Ordered
}

func Max[T Ordered](values []T) T {
    if len(values) == 0 {
        var zero T
        return zero
    }
    m := values[0]
    for _, v := range values[1:] {
        if v > m {
            m = v
        }
    }
    return m
}

応用:カスタム型制約の合成

package constraint

type Number interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~float32 | ~float64
}

type Signed interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

type Unsigned interface {
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}

type Float interface {
    ~float32 | ~float64
}

type Integer interface {
    Signed | Unsigned
}

type Numeric interface {
    Integer | Float
}

func Clamp[T Numeric](value, min, max T) T {
    if value < min {
        return min
    }
    if value > max {
        return max
    }
    return value
}

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

Go 1.24の強化:インターフェース制約におけるメソッドセット合成

package constraint

type Stringer interface {
    String() string
}

type Validator interface {
    Validate() error
}

type Entity interface {
    Stringer
    Validator
    ID() string
}

func ProcessEntity[T Entity](entity T) error {
    fmt.Println("Processing:", entity.String())
    if err := entity.Validate(); err != nil {
        return fmt.Errorf("validation failed for %s: %w", entity.ID(), err)
    }
    return nil
}

アーキテクチャ図:型制約階層

                    any
                     |
            +--------+--------+
            |                 |
         Number            String
            |
     +------+------+
     |             |
  Integer        Float
     |
  +--+--+-----+
  |     |      |
Signed Unsigned Complex

Pattern 2:ジェネリックミドルウェアチェーン

問題:interface{}ミドルウェアの型地獄

func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        user, err := validateToken(token)
        if err != nil {
            http.Error(w, "unauthorized", 401)
            return
        }
        ctx := context.WithValue(r.Context(), "user", user)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func Handler(w http.ResponseWriter, r *http.Request) {
    user := r.Context().Value("user").(*User)
    fmt.Println(user.Name)
}

context.Valueinterface{}を返し、型アサーションがpanicする可能性があり、キーの衝突も起こり得る——Go HTTPミドルウェアの古典的な痛点だ。

解決策:ジェネリックコンテキスト + 型安全ミドルウェア

package middleware

import (
    "context"
    "net/http"
)

type contextKey[T any] struct{}

func WithValue[T any](ctx context.Context, value T) context.Context {
    return context.WithValue(ctx, contextKey[T]{}, value)
}

func GetValue[T any](ctx context.Context) (T, bool) {
    v, ok := ctx.Value(contextKey[T]{}).(T)
    return v, ok
}

type User struct {
    ID   string
    Name string
    Role string
}

func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        user, err := validateToken(token)
        if err != nil {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }
        ctx := WithValue[User](r.Context(), user)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func RequireRole(role string) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            user, ok := GetValue[User](r.Context())
            if !ok {
                http.Error(w, "unauthorized", http.StatusUnauthorized)
                return
            }
            if user.Role != role {
                http.Error(w, "forbidden", http.StatusForbidden)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

応用:ジェネリックミドルウェアチェーンフレームワーク

package middleware

import (
    "net/http"
)

type Context[T any] struct {
    Response http.ResponseWriter
    Request  *http.Request
    Data     T
}

type HandlerFunc[T any] func(ctx *Context[T]) error

type Middleware[T any] func(next HandlerFunc[T]) HandlerFunc[T]

func Chain[T any](handler HandlerFunc[T], middlewares ...Middleware[T]) HandlerFunc[T] {
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}

type APIRequest struct {
    UserID string
    Token  string
    Body   []byte
}

func LoggingMiddleware[T any](next HandlerFunc[T]) HandlerFunc[T] {
    return func(ctx *Context[T]) error {
        fmt.Printf("[%s] %s %s\n", time.Now().Format(time.RFC3339), ctx.Request.Method, ctx.Request.URL.Path)
        return next(ctx)
    }
}

func RecoveryMiddleware[T any](next HandlerFunc[T]) HandlerFunc[T] {
    return func(ctx *Context[T]) error {
        defer func() {
            if r := recover(); r != nil {
                fmt.Printf("panic recovered: %v\n", r)
                http.Error(ctx.Response, "internal server error", http.StatusInternalServerError)
            }
        }()
        return next(ctx)
    }
}

func main() {
    handler := func(ctx *Context[APIRequest]) error {
        user, ok := GetValue[User](ctx.Request.Context())
        if !ok {
            http.Error(ctx.Response, "unauthorized", 401)
            return nil
        }
        fmt.Fprintf(ctx.Response, "Hello, %s!", user.Name)
        return nil
    }

    chained := Chain(handler, RecoveryMiddleware[APIRequest], LoggingMiddleware[APIRequest])

    http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
        ctx := &Context[APIRequest]{
            Response: w,
            Request:  r,
        }
        if err := chained(ctx); err != nil {
            http.Error(w, err.Error(), 500)
        }
    })
}

Pattern 3:ジェネリックRepositoryパターン

問題:CRUDコードの重複

各エンティティにCRUD実装が必要——5つのエンティティなら5セットのほぼ同一なコード、違いは型名だけ。コピペはエラーを生みやすく、一つを修正して他の同期を忘れるのは避けられない。

解決策:ジェネリックRepository

package repository

import (
    "context"
    "database/sql"
    "fmt"
)

type Entity interface {
    GetID() string
    SetID(id string)
}

type Repository[T Entity] struct {
    db        *sql.DB
    tableName string
}

func NewRepository[T Entity](db *sql.DB, tableName string) *Repository[T] {
    return &Repository[T]{
        db:        db,
        tableName: tableName,
    }
}

func (r *Repository[T]) GetByID(ctx context.Context, id string) (*T, error) {
    query := fmt.Sprintf("SELECT * FROM %s WHERE id = ?", r.tableName)
    row := r.db.QueryRowContext(ctx, query, id)

    var entity T
    err := row.Scan(&entity)
    if err != nil {
        if err == sql.ErrNoRows {
            return nil, fmt.Errorf("entity with id %s not found", id)
        }
        return nil, fmt.Errorf("query failed: %w", err)
    }
    return &entity, nil
}

func (r *Repository[T]) Create(ctx context.Context, entity *T) error {
    query := fmt.Sprintf("INSERT INTO %s (id, data) VALUES (?, ?)", r.tableName)
    id := (*entity).GetID()
    _, err := r.db.ExecContext(ctx, query, id, entity)
    if err != nil {
        return fmt.Errorf("create failed: %w", err)
    }
    return nil
}

func (r *Repository[T]) Delete(ctx context.Context, id string) error {
    query := fmt.Sprintf("DELETE FROM %s WHERE id = ?", r.tableName)
    result, err := r.db.ExecContext(ctx, query, id)
    if err != nil {
        return fmt.Errorf("delete failed: %w", err)
    }
    rows, _ := result.RowsAffected()
    if rows == 0 {
        return fmt.Errorf("entity with id %s not found", id)
    }
    return nil
}

func (r *Repository[T]) List(ctx context.Context, offset, limit int) ([]*T, error) {
    query := fmt.Sprintf("SELECT * FROM %s ORDER BY id LIMIT ? OFFSET ?", r.tableName)
    rows, err := r.db.QueryContext(ctx, query, limit, offset)
    if err != nil {
        return nil, fmt.Errorf("list failed: %w", err)
    }
    defer rows.Close()

    var entities []*T
    for rows.Next() {
        var entity T
        if err := rows.Scan(&entity); err != nil {
            return nil, fmt.Errorf("scan failed: %w", err)
        }
        entities = append(entities, &entity)
    }
    return entities, nil
}

応用:ジェネリックRepository + ジェネリックSpecification

package repository

type Specification[T any] func(T) bool

func (r *Repository[T]) FindBySpec(ctx context.Context, spec Specification[T]) ([]*T, error) {
    all, err := r.List(ctx, 0, 10000)
    if err != nil {
        return nil, err
    }
    var result []*T
    for _, e := range all {
        if spec(*e) {
            result = append(result, e)
        }
    }
    return result, nil
}

type User struct {
    ID    string
    Name  string
    Email string
    Age   int
}

func (u User) GetID() string  { return u.ID }
func (u *User) SetID(id string) { u.ID = id }

func main() {
    db, _ := sql.Open("sqlite3", ":memory:")
    userRepo := NewRepository[User](db, "users")

    users, _ := userRepo.FindBySpec(context.Background(), func(u User) bool {
        return u.Age >= 18
    })
    fmt.Printf("Adult users: %d\n", len(users))
}

アーキテクチャ図:ジェネリックRepository階層

┌─────────────────────────────────────────┐
│            Service Layer                │
│  UserService  OrderService  ItemService │
├─────────────────────────────────────────┤
│         Repository[T Entity]            │
│  ┌──────────┬──────────┬──────────┐     │
│  │ GetByID  │  Create  │  Delete  │     │
│  │   List   │  Update  │ FindSpec │     │
│  └──────────┴──────────┴──────────┘     │
├─────────────────────────────────────────┤
│          Database Driver                │
│   MySQL    PostgreSQL    SQLite         │
└─────────────────────────────────────────┘

Pattern 4:ジェネリックエラーハンドリングとResult型

問題:if err != nil地獄

func ProcessOrder(req OrderRequest) (*Order, error) {
    user, err := userService.Get(req.UserID)
    if err != nil {
        return nil, fmt.Errorf("get user: %w", err)
    }
    product, err := productService.Get(req.ProductID)
    if err != nil {
        return nil, fmt.Errorf("get product: %w", err)
    }
    payment, err := paymentService.Charge(user, product.Price)
    if err != nil {
        return nil, fmt.Errorf("charge: %w", err)
    }
    order, err := orderService.Create(user, product, payment)
    if err != nil {
        return nil, fmt.Errorf("create order: %w", err)
    }
    return order, nil
}

4つの操作、4つのif err != nilブロック、コードの半分を占める。Goジェネリクスはこれを優雅に解決できる。

解決策:Result型

package result

import "fmt"

type Result[T any] struct {
    value T
    err   error
}

func Ok[T any](value T) Result[T] {
    return Result[T]{value: value}
}

func Err[T any](err error) Result[T] {
    return Result[T]{err: err}
}

func Errf[T any](format string, args ...any) Result[T] {
    return Err[T](fmt.Errorf(format, args...))
}

func (r Result[T]) IsOk() bool {
    return r.err == nil
}

func (r Result[T]) IsErr() bool {
    return r.err != nil
}

func (r Result[T]) Unwrap() T {
    if r.err != nil {
        panic(fmt.Sprintf("called Unwrap on Err: %v", r.err))
    }
    return r.value
}

func (r Result[T]) UnwrapOr(defaultValue T) T {
    if r.err != nil {
        return defaultValue
    }
    return r.value
}

func (r Result[T]) UnwrapOrElse(fn func() T) T {
    if r.err != nil {
        return fn()
    }
    return r.value
}

func (r Result[T]) Err() error {
    return r.err
}

func (r Result[T]) Value() (T, bool) {
    return r.value, r.err == nil
}

func Map[T any, U any](r Result[T], fn func(T) U) Result[U] {
    if r.err != nil {
        return Err[U](r.err)
    }
    return Ok(fn(r.value))
}

func FlatMap[T any, U any](r Result[T], fn func(T) Result[U]) Result[U] {
    if r.err != nil {
        return Err[U](r.err)
    }
    return fn(r.value)
}

Resultを使ったProcessOrderのリファクタリング

func ProcessOrder(req OrderRequest) Result[*Order] {
    user := userService.Get(req.UserID)
    if user.IsErr() {
        return Errf[*Order]("get user: %w", user.Err())
    }

    product := productService.Get(req.ProductID)
    if product.IsErr() {
        return Errf[*Order]("get product: %w", product.Err())
    }

    payment := paymentService.Charge(user.Unwrap(), product.Unwrap().Price)
    if payment.IsErr() {
        return Errf[*Order]("charge: %w", payment.Err())
    }

    order := orderService.Create(user.Unwrap(), product.Unwrap(), payment.Unwrap())
    if order.IsErr() {
        return Errf[*Order]("create order: %w", order.Err())
    }

    return order
}

応用:チェーンResult操作

func ProcessOrderChain(req OrderRequest) Result[*Order] {
    return FlatMap(
        userService.Get(req.UserID),
        func(user *User) Result[*Order] {
            return FlatMap(
                productService.Get(req.ProductID),
                func(product *Product) Result[*Order] {
                    return FlatMap(
                        paymentService.Charge(user, product.Price),
                        func(payment *Payment) Result[*Order] {
                            return orderService.Create(user, product, payment)
                        },
                    )
                },
            )
        },
    )
}

Result vs error比較

特徴 error Result[T]
型安全性 なし、手動アサーション必要 あり、コンパイル時保証
チェーン操作 非対応 Map/FlatMap
デフォルト値 手動処理必要 UnwrapOr
強制チェック なし、無視可能 IsOk/IsErr
パフォーマンスオーバーヘッド ゼロ 微小(構造体1つ分)
エコシステム互換性 ネイティブ アダプタ層が必要

Pattern 5:型安全Builderパターン

問題:オプションパラメータと設定検証

type ServerConfig struct {
    Host         string
    Port         int
    Timeout      time.Duration
    MaxConns     int
    TLS          bool
    CertFile     string
    KeyFile      string
    ReadTimeout  time.Duration
    WriteTimeout time.Duration
}

func NewServerConfig(host string, port int, timeout time.Duration, maxConns int, tls bool, certFile string, keyFile string, readTimeout time.Duration, writeTimeout time.Duration) *ServerConfig {
    return &ServerConfig{
        Host: host, Port: port, Timeout: timeout,
        MaxConns: maxConns, TLS: tls, CertFile: certFile,
        KeyFile: keyFile, ReadTimeout: readTimeout, WriteTimeout: writeTimeout,
    }
}

9パラメータのコンストラクタ——呼び出し側でどの引数がどれか区別できない。TLSを有効にしても証明書パスを忘れた場合、エラーは実行時まで表面化しない。

解決策:ジェネリックBuilder

package builder

type Builder[T any] struct {
    target *T
    errors []error
}

func NewBuilder[T any]() *Builder[T] {
    return &Builder[T]{
        target: new(T),
    }
}

func (b *Builder[T]) With(fn func(*T)) *Builder[T] {
    fn(b.target)
    return b
}

func (b *Builder[T]) WithValidate(fn func(*T) error) *Builder[T] {
    if err := fn(b.target); err != nil {
        b.errors = append(b.errors, err)
    }
    return b
}

func (b *Builder[T]) Build() (*T, error) {
    if len(b.errors) > 0 {
        return nil, fmt.Errorf("build failed: %v", b.errors)
    }
    return b.target, nil
}

ジェネリックBuilderでServerConfigを構築

func main() {
    config, err := NewBuilder[ServerConfig]().
        With(func(c *ServerConfig) {
            c.Host = "0.0.0.0"
            c.Port = 8080
            c.Timeout = 30 * time.Second
            c.MaxConns = 1000
            c.TLS = true
        }).
        WithValidate(func(c *ServerConfig) error {
            if c.Port < 1 || c.Port > 65535 {
                return fmt.Errorf("invalid port: %d", c.Port)
            }
            if c.TLS && c.CertFile == "" {
                return fmt.Errorf("TLS enabled but no cert file")
            }
            if c.TLS && c.KeyFile == "" {
                return fmt.Errorf("TLS enabled but no key file")
            }
            return nil
        }).
        Build()

    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Config: %+v\n", config)
}

応用:型状態Builder(コンパイル時必須フィールド保証)

package builder

type ServerConfigUnset struct{}
type ServerConfigSet struct {
    Host string
    Port int
}

type TypedBuilder[State any] struct {
    host string
    port int
    timeout time.Duration
    maxConns int
    tls bool
    certFile string
    keyFile string
}

func NewTypedBuilder() *TypedBuilder[ServerConfigUnset] {
    return &TypedBuilder[ServerConfigUnset]{}
}

func (b *TypedBuilder[ServerConfigUnset]) HostPort(host string, port int) *TypedBuilder[ServerConfigSet] {
    b.host = host
    b.port = port
    return (*TypedBuilder[ServerConfigSet])(unsafe.Pointer(b))
}

func (b *TypedBuilder[ServerConfigSet]) Timeout(d time.Duration) *TypedBuilder[ServerConfigSet] {
    b.timeout = d
    return b
}

func (b *TypedBuilder[ServerConfigSet]) TLS(certFile, keyFile string) *TypedBuilder[ServerConfigSet] {
    b.tls = true
    b.certFile = certFile
    b.keyFile = keyFile
    return b
}

func (b *TypedBuilder[ServerConfigSet]) Build() *ServerConfig {
    return &ServerConfig{
        Host: b.host, Port: b.port, Timeout: b.timeout,
        MaxConns: b.maxConns, TLS: b.tls,
        CertFile: b.certFile, KeyFile: b.keyFile,
    }
}

呼び出し側でHostPortを呼ばないとBuildを呼べない——コンパイル時の必須フィールド強制だ。


Pattern 6:ジェネリック並行処理パターン

問題:型安全でない並行処理プリミティブ

func FanIn(channels ...chan interface{}) chan interface{} {
    out := make(chan interface{})
    var wg sync.WaitGroup
    for _, ch := range channels {
        wg.Add(1)
        go func(c chan interface{}) {
            defer wg.Done()
            for v := range c {
                out <- v
            }
        }(ch)
    }
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

chan interface{}——またしても実行時型安全性の古い問題だ。

解決策:ジェネリック並行処理プリミティブ

package concurrent

import "sync"

func FanIn[T any](channels ...<-chan T) <-chan T {
    out := make(chan T)
    var wg sync.WaitGroup
    for _, ch := range channels {
        wg.Add(1)
        go func(c <-chan T) {
            defer wg.Done()
            for v := range c {
                out <- v
            }
        }(ch)
    }
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

func FanOut[T any, R any](input <-chan T, worker func(T) R, count int) <-chan R {
    out := make(chan R)
    var wg sync.WaitGroup
    for i := 0; i < count; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for v := range input {
                out <- worker(v)
            }
        }()
    }
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

func Pipeline[T any, R any](input <-chan T, stage func(T) R) <-chan R {
    out := make(chan R)
    go func() {
        defer close(out)
        for v := range input {
            out <- stage(v)
        }
    }()
    return out
}

ジェネリックワーカープール

package concurrent

import "context"

type Job[T any, R any] struct {
    Input  T
    Result chan<- Result[R]
}

type Pool[T any, R any] struct {
    jobs    chan Job[T, R]
    worker  func(context.Context, T) (R, error)
    count   int
    ctx     context.Context
    cancel  context.CancelFunc
}

func NewPool[T any, R any](ctx context.Context, worker func(context.Context, T) (R, error), count int) *Pool[T, R] {
    ctx, cancel := context.WithCancel(ctx)
    p := &Pool[T, R]{
        jobs:   make(chan Job[T, R], count*2),
        worker: worker,
        count:  count,
        ctx:    ctx,
        cancel: cancel,
    }
    p.start()
    return p
}

func (p *Pool[T, R]) start() {
    for i := 0; i < p.count; i++ {
        go func() {
            for job := range p.jobs {
                select {
                case <-p.ctx.Done():
                    job.Result <- Err[R](p.ctx.Err())
                    return
                default:
                    value, err := p.worker(p.ctx, job.Input)
                    if err != nil {
                        job.Result <- Err[R](err)
                    } else {
                        job.Result <- Ok(value)
                    }
                }
            }
        }()
    }
}

func (p *Pool[T, R]) Submit(input T) Result[R] {
    resultCh := make(chan Result[R], 1)
    select {
    case p.jobs <- Job[T, R]{Input: input, Result: resultCh}:
    case <-p.ctx.Done():
        return Err[R](p.ctx.Err())
    }
    return <-resultCh
}

func (p *Pool[T, R]) Shutdown() {
    p.cancel()
    close(p.jobs)
}

ジェネリックワーカープールの使用

func main() {
    ctx := context.Background()

    pool := NewPool[string, int](ctx, func(ctx context.Context, url string) (int, error) {
        resp, err := http.Get(url)
        if err != nil {
            return 0, err
        }
        defer resp.Body.Close()
        return resp.StatusCode, nil
    }, 10)

    urls := []string{
        "https://example.com",
        "https://httpbin.org/status/200",
        "https://httpbin.org/status/404",
    }

    for _, url := range urls {
        result := pool.Submit(url)
        if result.IsOk() {
            fmt.Printf("%s -> %d\n", url, result.Unwrap())
        } else {
            fmt.Printf("%s -> error: %v\n", url, result.Err())
        }
    }

    pool.Shutdown()
}

Pattern 7:高度な型合成と型レベルプログラミング

問題:複雑な型関係の表現が困難

ドメインモデルに複雑な型依存関係がある場合——「注文はユーザーと商品を含み、商品には価格があり、価格は数値型」——interface{}ではこれらの制約をコンパイル時に表現できない。

解決策:ジェネリック型合成

package domain

type Identifiable interface {
    ~string | ~int | ~int64
}

type Timestamped interface {
    GetCreatedAt() time.Time
    GetUpdatedAt() time.Time
}

type SoftDeletable interface {
    GetDeletedAt() *time.Time
    IsDeleted() bool
}

type BaseEntity[ID Identifiable] struct {
    ID        ID
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt *time.Time
}

func (b BaseEntity[ID]) GetCreatedAt() time.Time { return b.CreatedAt }
func (b BaseEntity[ID]) GetUpdatedAt() time.Time { return b.UpdatedAt }
func (b BaseEntity[ID]) GetDeletedAt() *time.Time { return b.DeletedAt }
func (b BaseEntity[ID]) IsDeleted() bool { return b.DeletedAt != nil }

ジェネリックイベントシステム

package event

type Event interface {
    EventName() string
    OccurredAt() time.Time
}

type Handler[T Event] func(ctx context.Context, event T) error

type Bus struct {
    handlers map[string][]any
    mu       sync.RWMutex
}

func NewBus() *Bus {
    return &Bus{
        handlers: make(map[string][]any),
    }
}

func Subscribe[T Event](bus *Bus, handler Handler[T]) {
    bus.mu.Lock()
    defer bus.mu.Unlock()

    var zero T
    name := zero.EventName()
    bus.handlers[name] = append(bus.handlers[name], handler)
}

func Publish[T Event](bus *Bus, ctx context.Context, event T) error {
    bus.mu.RLock()
    handlers := bus.handlers[event.EventName()]
    bus.mu.RUnlock()

    var g errgroup.Group
    for _, h := range handlers {
        handler := h.(Handler[T])
        g.Go(func() error {
            return handler(ctx, event)
        })
    }
    return g.Wait()
}

type UserCreatedEvent struct {
    UserID    string
    UserName  string
    Timestamp time.Time
}

func (e UserCreatedEvent) EventName() string    { return "user.created" }
func (e UserCreatedEvent) OccurredAt() time.Time { return e.Timestamp }

type OrderPlacedEvent struct {
    OrderID   string
    UserID    string
    Amount    float64
    Timestamp time.Time
}

func (e OrderPlacedEvent) EventName() string    { return "order.placed" }
func (e OrderPlacedEvent) OccurredAt() time.Time { return e.Timestamp }

ジェネリック依存性注入コンテナ

package di

type Provider[T any] func(container *Container) (T, error)

type Container struct {
    providers map[reflect.Type]any
    instances map[reflect.Type]any
    mu        sync.RWMutex
}

func NewContainer() *Container {
    return &Container{
        providers: make(map[reflect.Type]any),
        instances: make(map[reflect.Type]any),
    }
}

func Provide[T any](c *Container, provider Provider[T]) {
    c.mu.Lock()
    defer c.mu.Unlock()

    var zero T
    typ := reflect.TypeOf(zero)
    c.providers[typ] = provider
}

func Resolve[T any](c *Container) (T, error) {
    c.mu.RLock()
    var zero T
    typ := reflect.TypeOf(zero)

    if inst, ok := c.instances[typ]; ok {
        c.mu.RUnlock()
        return inst.(T), nil
    }
    c.mu.RUnlock()

    c.mu.Lock()
    defer c.mu.Unlock()

    provider, ok := c.providers[typ]
    if !ok {
        return zero, fmt.Errorf("no provider for type %T", zero)
    }

    instance, err := provider.(Provider[T])(c)
    if err != nil {
        return zero, fmt.Errorf("provider failed: %w", err)
    }

    c.instances[typ] = instance
    return instance, nil
}

アーキテクチャ図:ジェネリック型合成

┌──────────────────────────────────────────────┐
│                  Domain Layer                │
│  ┌──────────────┐  ┌──────────────────────┐  │
│  │ BaseEntity   │  │ Event                │  │
│  │ [ID Identif.]│  │ [T Event]            │  │
│  └──────┬───────┘  └──────────┬───────────┘  │
│         │                     │              │
│  ┌──────┴───────┐  ┌─────────┴────────────┐  │
│  │ User         │  │ UserCreatedEvent     │  │
│  │ Order        │  │ OrderPlacedEvent     │  │
│  │ Product      │  │ PaymentCompletedEvt  │  │
│  └──────────────┘  └──────────────────────┘  │
├──────────────────────────────────────────────┤
│              Infrastructure Layer             │
│  ┌──────────────┐  ┌──────────────────────┐  │
│  │ Repository   │  │ EventBus             │  │
│  │ [T Entity]   │  │ [T Event]            │  │
│  └──────────────┘  └──────────────────────┘  │
├──────────────────────────────────────────────┤
│                Application Layer             │
│  ┌──────────────┐  ┌──────────────────────┐  │
│  │ DI Container │  │ Middleware Chain      │  │
│  │ [T any]      │  │ [T any]              │  │
│  └──────────────┘  └──────────────────────┘  │
└──────────────────────────────────────────────┘

5つのよくある落とし穴と解決策

落とし穴1:ジェネリックメソッドは型推論をサポートしない

type Stack[T any] struct{ items []T }

func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }

var s Stack[int]
s.Push(42)

func Push[T any](s *Stack[T], v T) { s.items = append(s.items, v) }
Push(&s, 42)

Go 1.24ではメソッド呼び出しに明示的な型パラメータは不要だが、ジェネリック関数では必要な場合がある。

落とし穴2:ジェネリック構造体にメソッド型パラメータを定義できない

type Converter[T any] struct{}

func (c *Converter[T]) Convert[R any](v T) R {
}

コンパイルエラー:Goはメソッド上の追加型パラメータをサポートしていない。解決策:パッケージレベル関数を使用。

func Convert[T any, R any](v T, fn func(T) R) R {
    return fn(v)
}

落とし穴3:ジェネリクスとインターフェースアサーションの非互換性

var _ json.Marshaler = Stack[int]{}

ジェネリック型を統一的にインターフェース実装させることはできない。Stack[int]Stack[string]は異なる型だからだ。解決策:各具象インスタンス化型に対して個別に実装する。

落とし穴4:ジェネリックゼロ値のトラップ

func First[T any](s []T) T {
    if len(s) == 0 {
        var zero T
        return zero
    }
    return s[0]
}

Tのゼロ値はnilかもしれない(Tがポインタ/インターフェース/slice/map/channelの場合)。呼び出し側はチェックが必要。Go 1.24にはnullable制約がない——自分で対処する必要がある。

落とし穴5:ジェネリクスと埋め込みの非互換性

type GenericEmbed[T any] struct {
    T
    Name string
}

Goはジェネリック型パラメータの埋め込みをサポートしていない。解決策:埋め込みの代わりに合成を使用。

type GenericHolder[T any] struct {
    Value T
    Name  string
}

10のよくあるエラートラブルシューティング

エラー 原因 解決策
cannot use generic type without instantiation インスタンス化されていないジェネリック型の使用 型パラメータを指定:Stack[int]
interface contains type constraints 制約外の位置でユニオン型を使用 ユニオン型は型制約内でのみ使用可能
invalid recursive type: T refers to itself ジェネリック型が自身を再帰的に参照 型関係を再設計し、直接再帰を避ける
method must have no type parameters メソッドに追加の型パラメータを定義 パッケージレベルのジェネリック関数を使用
cannot use ~T in interface 通常のインターフェースで近似制約を使用 近似制約は型制約インターフェースでのみ使用可能
T does not implement constraint 型パラメータが制約を満たさない 制約条件を確認し、型が必要なメソッドを実装しているか確認
implicit assignment of unexported field パッケージを跨いでプライベートフィールドにアクセス フィールドをエクスポートするか公開メソッドを提供
type parameter T is not used 宣言されたが未使用の型パラメータ 未使用の型パラメータを削除
panic: called Unwrap on Err Err Resultに対してUnwrapを呼び出した 先にIsOk()でチェックするかUnwrapOrを使用
cannot range over T 型パラメータに対してrangeを試みた []T制約を追加するか具象型を使用

高度な最適化テクニック

テクニック1:ジェネリックコードのMonomorphization最適化

Goコンパイラはジェネリックコードに対して単態化(monomorphization)を行い、各具象型に特化したコードを生成する。値型(int、floatなど)の場合、ボクシング/アンボクシングを回避できるため、パフォーマンスの向上が顕著だ。

func Sum[T Integer](values []T) T {
    var total T
    for _, v := range values {
        total += v
    }
    return total
}

コンパイル後、Sum[int]Sum[int64]は2つの独立したコードパスになり、それぞれの型に最適化される。

テクニック2:Shape最適化によるコード膨張の削減

ポインタ型とインターフェース型の場合、Goは完全な単態化ではなく辞書渡し(dictionary passing)を使用し、コード膨張を削減する:

func Process[T Stringer](v T) string {
    return v.String()
}

Process[*MyType]Process[*YourType]は同じコードを共有し、辞書だけが異なる場合がある。

テクニック3:ジェネリクスとインライン最適化

短いジェネリック関数はインライン化されやすい:

func Map[T any, U any](s []T, fn func(T) U) []U {
    result := make([]U, len(s))
    for i, v := range s {
        result[i] = fn(v)
    }
    return result
}

ジェネリック関数は短く保ち、コンパイラがインライン化できるようにして、関数呼び出しのオーバーヘッドを回避する。

テクニック4:ジェネリックキャッシュパターン

package cache

type Cache[T any] struct {
    data map[string]T
    mu   sync.RWMutex
}

func NewCache[T any]() *Cache[T] {
    return &Cache[T]{
        data: make(map[string]T),
    }
}

func (c *Cache[T]) Get(key string) (T, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    v, ok := c.data[key]
    return v, ok
}

func (c *Cache[T]) Set(key string, value T) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[key] = value
}

ジェネリックキャッシュはinterface{}の型アサーションのオーバーヘッドを回避しつつ、汎用性を維持する。


比較分析:ジェネリクスvsインターフェースvsコード生成

次元 ジェネリクス インターフェース コード生成
型安全性 コンパイル時 実行時 コンパイル時
パフォーマンス 単態化最適化 仮想呼び出しオーバーヘッド ゼロオーバーヘッド
コード量 少ない 中程度 多い(生成コード)
デバッグ難易度 中程度 低い 低い
IDEサポート Go 1.24で成熟 優秀 ジェネレータに依存
学習曲線 緩やか 中程度
適用場面 汎用アルゴリズム、データ構造 ポリモーフィズム、疎結合 バッチCRUD
バイナリサイズ 増加する可能性 小さい 著しく大きい
コンパイル速度 やや遅い 速い 速い(生成ステップは別途)

意思決定ツリー

型安全性が必要?
├── いいえ → インターフェース
└── はい
    ├── 汎用アルゴリズム/データ構造が必要?
    │   └── はい → ジェネリクス
    └── いいえ
        ├── 型の数が固定で少ない?
        │   └── はい → インターフェース + 型アサーション
        └── いいえ → コード生成

オンラインツール推奨

外部リファレンス


まとめ

Go 1.24ジェネリクスアーキテクチャ設計は「実験段階」から「プロダクション対応」へと進化した。7つのコアパターンは型制約から高度な合成まで完全なチェーンをカバーする:

  1. 型制約とインターフェース合成 — ジェネリクスの基盤、実行時アサーションを制約で置き換え
  2. ジェネリックミドルウェアチェーン — 型安全でないcontext.Valueに別れを告げる
  3. ジェネリックRepositoryパターン — 1行で型安全なCRUDを実現
  4. ジェネリックエラーハンドリングとResult型 — チェーン操作でif err != nil地獄を置き換え
  5. 型安全Builderパターン — コンパイル時にビルドの完全性を保証
  6. ジェネリック並行処理パターン — 型安全なFanIn/FanOut/ワーカープール
  7. 高度な型合成と型レベルプログラミング — イベントシステム、DIコンテナのジェネリック実装

Goジェネリクスは銀の弾丸ではないが、型安全性、コード再利用、パフォーマンスの間により良いバランスを提供する。ジェネリクスかインターフェースかコード生成かの選択は、具体的なシナリオに依存する——上記の意思決定ツリーを参照してほしい。

関連記事

ブラウザローカルツールを無料で試す →

#Go#泛型#Generics#类型约束#架构设计#Go 1.24#2026#编程语言