# Goデザインパターン:Go開発者が押さえるべき必須パターンと面接対策 > Goの設計思想に基づくデザインパターンを体系的に解説。Functional Options、Strategy、Observer、Middlewareなど、面接で問われる実践的パターンとその回答例を紹介します。 - Published: 2026-05-28 - Updated: 2026-05-28 - Author: SharpSkill - Tags: go, design-patterns, interview, best-practices - Reading time: 12 min --- Goのデザインパターンは、オブジェクト指向言語のそれとは根本的に異なる。クラスや継承を持たないGoでは、コンポジション、インターフェース、第一級関数を活用して同等の柔軟性を実現する。従来のパターンをそのまま移植するのではなく、Goの型システムに適応させることが、実務でも面接でも求められる。 > **Go設計の原則** > > Goはコンポジションを継承より優先する。すべてのデザインパターンは、クラス階層ではなく、インターフェース、構造体埋め込み、第一級関数を用いて適応させる必要がある。 ## Functional Optionsパターン:柔軟な設定の実現 Functional Optionsパターンは、Goにおける構造体の初期化で頻出する課題を解決する。メソッドのオーバーロードもデフォルト引数もないGoでは、オプショナルなパラメータの扱いが問題になる。Builderパターンも機能するが冗長になりがちである。Functional Optionsは、後方互換性を保ちつつ拡張可能なクリーンなAPIを提供する。 このパターンは[Dave Cheneyによって広められ](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis)、現在ではGoエコシステム全体で標準的に使用されている。可変長関数引数を使って構造体を設定する仕組みである。 ```go // server.go package server import ( "time" "log/slog" ) // Server holds the HTTP server configuration. type Server struct { host string port int timeout time.Duration maxConns int logger *slog.Logger } // Option defines a functional option for Server. type Option func(*Server) // WithPort sets the server port. func WithPort(port int) Option { return func(s *Server) { s.port = port } } // WithTimeout sets the request timeout. func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } } // WithMaxConns sets the maximum concurrent connections. func WithMaxConns(n int) Option { return func(s *Server) { s.maxConns = n } } // New creates a Server with sensible defaults and applies options. func New(host string, opts ...Option) *Server { srv := &Server{ host: host, port: 8080, // default port timeout: 30 * time.Second, // default timeout maxConns: 100, // default max connections logger: slog.Default(), } for _, opt := range opts { opt(srv) } return srv } ``` 呼び出し側はデフォルト値と異なる部分のみを指定すればよい。新しいオプションを追加しても既存の呼び出し箇所には影響しない。このパターンは`google.golang.org/grpc`や`go.uber.org/zap`などのプロダクションライブラリでも採用されている。 ## Strategyパターン:インターフェースによる多態性 Strategyパターンは、交換可能なアルゴリズムを共通のインターフェースの背後にカプセル化する。Goでは、抽象クラスや継承チェーンを必要とせず、インターフェースベースのポリモーフィズムで直接表現できる。 決済処理システムがこのパターンの好例である。各決済方法が同一のインターフェースを実装し、チェックアウトサービスが実行時にストラテジーを選択する。 ```go // payment.go package payment import "fmt" // Processor defines the strategy interface. type Processor interface { Pay(amount float64) (string, error) } // CreditCard implements Processor for card payments. type CreditCard struct { CardNumber string Expiry string } func (c *CreditCard) Pay(amount float64) (string, error) { // Charge the card via payment gateway return fmt.Sprintf("charged %.2f to card ending %s", amount, c.CardNumber[len(c.CardNumber)-4:]), nil } // BankTransfer implements Processor for wire transfers. type BankTransfer struct { IBAN string } func (b *BankTransfer) Pay(amount float64) (string, error) { return fmt.Sprintf("initiated transfer of %.2f to %s", amount, b.IBAN), nil } // Checkout processes a payment using the given strategy. func Checkout(p Processor, amount float64) error { receipt, err := p.Pay(amount) if err != nil { return fmt.Errorf("payment failed: %w", err) } fmt.Println(receipt) return nil } ``` Goのインターフェースは暗黙的に充足される。`implements`キーワードは不要で、`Pay(float64) (string, error)`メソッドを持つ型であれば`Processor`として使用できる。これにより結合度が低くなり、テストも容易になる。モックの`Processor`を渡すだけで予測可能な結果を返すテストが書ける。 ## ファクトリ関数とコンストラクタパターン Goにはコンストラクタが存在しない。その代わりに`New`または`NewXxx`と命名されたファクトリ関数が初期化済みの構造体を返す。ファクトリ関数は、ゼロ値の初期化では保証できない不変条件を強制するために使用される。 面接準備においてこの区別は重要である。ファクトリ関数が必要な場合と、[ゼロ値がそのまま有用な場合](https://go.dev/blog/organizing-go-code)を正しく判断できる能力が求められる。 ```go // connpool.go package pool import ( "errors" "sync" ) // ConnPool manages a pool of reusable connections. type ConnPool struct { mu sync.Mutex conns []Conn maxSize int } // Conn represents a database connection. type Conn struct { ID int Active bool } // NewConnPool validates parameters and returns an initialized pool. func NewConnPool(maxSize int) (*ConnPool, error) { if maxSize <= 0 { return nil, errors.New("pool: maxSize must be positive") } return &ConnPool{ conns: make([]Conn, 0, maxSize), maxSize: maxSize, }, nil } // Acquire returns a connection from the pool. func (p *ConnPool) Acquire() (*Conn, error) { p.mu.Lock() defer p.mu.Unlock() for i := range p.conns { if !p.conns[i].Active { p.conns[i].Active = true return &p.conns[i], nil } } if len(p.conns) >= p.maxSize { return nil, errors.New("pool: no available connections") } c := Conn{ID: len(p.conns) + 1, Active: true} p.conns = append(p.conns, c) return &p.conns[len(p.conns)-1], nil } ``` ファクトリ関数`NewConnPool`は`maxSize`パラメータを検証し、スライスを事前確保する。構造体を直接初期化(`&ConnPool{}`)すると検証がスキップされ、実行時にパニックを引き起こす可能性がある。このパターンはFunctional Optionsと組み合わせることで、より複雑な設定シナリオにも対応できる。 ## Observerパターン:チャネルとゴルーチンによる実装 Observerパターンは、状態が変化した際に複数の購読者に通知を送る。JavaやC#ではイベントリスナーとコールバックの登録が必要になるが、Goではチャネルというよりイディオマティックな手段がある。各購読者は専用のチャネルでイベントを受信し、ゴルーチンが並行的にイベントを配信する。 このアプローチはGoの[並行処理プリミティブ](/blog/go/go-concurrency-goroutines-channels)を直接活用し、コールバックの複雑な連鎖を回避できる。 ```go // eventbus.go package events import "sync" // Event carries a topic and payload. type Event struct { Topic string Payload any } // Bus manages subscriptions and event dispatch. type Bus struct { mu sync.RWMutex subscribers map[string][]chan Event } // NewBus creates an event bus. func NewBus() *Bus { return &Bus{ subscribers: make(map[string][]chan Event), } } // Subscribe returns a channel that receives events for a topic. func (b *Bus) Subscribe(topic string) <-chan Event { ch := make(chan Event, 16) // buffered to avoid blocking publisher b.mu.Lock() b.subscribers[topic] = append(b.subscribers[topic], ch) b.mu.Unlock() return ch } // Publish sends an event to all subscribers of the topic. func (b *Bus) Publish(topic string, payload any) { b.mu.RLock() defer b.mu.RUnlock() for _, ch := range b.subscribers[topic] { select { case ch <- Event{Topic: topic, Payload: payload}: default: // subscriber too slow, drop event } } } ``` `select`文に`default`ケースを設けることで、応答が遅い購読者がバス全体をブロックするのを防いでいる。プロダクション環境では、contextベースのキャンセル機構と`Unsubscribe`メソッドを追加して実装を完成させる必要がある。 ## Middlewareパターン:HTTPリクエストパイプライン ミドルウェアチェーンはGoのHTTPサーバーの中核をなす。このパターンは`http.Handler`をラップして、ロギング、認証、レート制限などの追加機能を、ハンドラ自体を変更することなく付加する。標準ライブラリの`http.Handler`インターフェースにより、この構成は極めて容易である。 ```go // middleware.go package middleware import ( "log/slog" "net/http" "time" ) // Logging records request duration and status. func Logging(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() wrapped := &statusWriter{ResponseWriter: w, status: 200} next.ServeHTTP(wrapped, r) slog.Info("request", "method", r.Method, "path", r.URL.Path, "status", wrapped.status, "duration", time.Since(start), ) }) } // statusWriter captures the HTTP status code. type statusWriter struct { http.ResponseWriter status int } func (w *statusWriter) WriteHeader(code int) { w.status = code w.ResponseWriter.WriteHeader(code) } // Chain applies middleware in order: first listed = outermost. func Chain(h http.Handler, mw ...func(http.Handler) http.Handler) http.Handler { for i := len(mw) - 1; i >= 0; i-- { h = mw[i](h) } return h } ``` `Chain`関数はミドルウェアを宣言順に適用する。各ミドルウェアが次のハンドラをラップしてパイプラインを形成する。このパターンは[chiなどの人気ルーター](https://github.com/go-chi/chi)でも同一で、`func(http.Handler) http.Handler`型のミドルウェアを受け付ける。 ## 構造体埋め込みによるコンポジション Goは継承をサポートしていない。代わりに、構造体埋め込み(struct embedding)を使って内部型のメソッドを外部型に昇格させ、密結合なしにコードを再利用する。これは継承ではない。埋め込まれた型は埋め込み元の型について何も知らず、仮想ディスパッチも発生しない。 この区別を理解することは[Goの面接](/technologies/go/interview-questions/concurrency-patterns)において極めて重要である。埋め込みと継承を混同する候補者は、Goの型システムへの理解が浅いと判断される。 ```go // models.go package models import "time" // Timestamps provides common audit fields. type Timestamps struct { CreatedAt time.Time UpdatedAt time.Time } // User embeds Timestamps to gain CreatedAt/UpdatedAt. type User struct { Timestamps ID int Email string } // Order also embeds Timestamps. type Order struct { Timestamps ID int UserID int Total float64 } ``` `User`と`Order`はどちらも`CreatedAt`と`UpdatedAt`フィールドに直接アクセスできる。`Timestamps`に定義されたメソッドも同様に昇格される。重要な規則として、埋め込みは委譲を提供するものであり、代替ではない。`User`は`Timestamps`ではなく、`Timestamps`を持っている。 ## Goデザインパターンに関する面接質問 Goの面接におけるデザインパターンの質問は、候補者がクラシカルなパターンをGoの型システムに適応できるかどうかを試すものである。以下は、技術スクリーニングやオンサイト面接で頻出する質問である。 **Q: ファクトリ関数がパニックではなくエラーを返すべき場面はどのような場合か?** ファクトリ関数は、検証がランタイム入力(ユーザー提供の設定、環境変数、外部データ)に依存する場合にエラーを返すべきである。パニックはプログラマのエラー、つまりAPIの契約上禁止されているnilポインタを渡すなど、バグを示す状況に限定される。標準ライブラリもこの慣習に従っている。`os.Open`はエラーを返し、`regexp.MustCompile`はコンパイル時定数のパターンを想定しているためパニックする。 **Q: Functional Optionsパターンはどのようにしてアプリケーションの発展を改善するのか?** 新しい`WithXxx`関数の追加は後方互換性のある変更である。既存の呼び出し側はそのまま動作し、修正の必要がない。これは設定構造体(必須フィールドの追加で全ての呼び出し箇所が壊れる)や位置引数(引数の順序変更がバグを引き起こす)とは対照的である。 **Q: GoのインターフェースがJavaやC#のインターフェースと異なる点は何か?** Goのインターフェースは暗黙的に充足される。型が必要なメソッドを持つだけでインターフェースを実装したことになり、`implements`宣言は不要である。これにより「インターフェースを受け取り、構造体を返す」という原則が成立する。つまり、実装側ではなく呼び出し側で狭いインターフェースを定義する。その結果、[結合度が低くテスタビリティが高い](/technologies/go/interview-questions/testing)コードが実現される。 > **面接のポイント** > > 面接官は候補者に、具体的な依存をインターフェースにリファクタリングするよう求めることが多い。テストされるのは、必要最小限のメソッドセットを特定し、実装側ではなく利用側でインターフェースを抽出できるかどうかである。 **Q: チャネルベースのObserverとコールバックベースのObserverの違いは何か?** チャネルはパブリッシャーとサブスクライバーを時間的にも空間的にも分離する。パブリッシャーはサブスクライバーの関数への参照を保持せず、チャネルに送信するだけである。サブスクライバーはバッファ付きチャネルを使って自分のペースでイベントを処理できる。`select`文により、タイムアウト処理、`context`によるキャンセル、複数のイベントソースの多重化が可能になる。これらはコールバックでは得られない機能である。 > **よくある落とし穴** > > サブスクライバーチャネルを閉じ忘れるとゴルーチンリークが発生する。すべての`Subscribe`には、チャネルを閉じてバスから削除する対応する`Unsubscribe`が必要である。 ## まとめ - Functional Optionsパターンは、Builderや設定構造体を、既存の呼び出し側を壊さないクリーンで拡張可能なAPIに置き換える - GoのStrategyパターンはインターフェースベースのポリモーフィズムに対応する。インターフェースの定義は実装側ではなく利用側で行う - ファクトリ関数はゼロ値初期化では保証できない不変条件を強制する。ランタイム入力にはエラーを返し、パニックはプログラマのバグにのみ使用する - チャネルベースのObserverはゴルーチンとselectを活用し、コールバックチェーンなしに並行的で疎結合なイベント配信を実現する - HTTPミドルウェアチェーンは`func(http.Handler) http.Handler`シグネチャを通じて構成され、標準ライブラリとサードパーティルーター全体で共通している - 構造体埋め込みは委譲を提供するものであり、継承ではない。この区別を理解することが、Goを深く理解する開発者とJavaパターンをそのまま移植する開発者を分ける - 面接での成功には、Gang of Fourの定義の暗記ではなく、パターンをGoの型システムにイディオマティックに適応させる能力が求められる --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ja/blog/go/go-design-patterns-essential-patterns-interview