Goデザインパターン:Go開発者が押さえるべき必須パターンと面接対策
Goの設計思想に基づくデザインパターンを体系的に解説。Functional Options、Strategy、Observer、Middlewareなど、面接で問われる実践的パターンとその回答例を紹介します。

Goのデザインパターンは、オブジェクト指向言語のそれとは根本的に異なる。クラスや継承を持たないGoでは、コンポジション、インターフェース、第一級関数を活用して同等の柔軟性を実現する。従来のパターンをそのまま移植するのではなく、Goの型システムに適応させることが、実務でも面接でも求められる。
Goはコンポジションを継承より優先する。すべてのデザインパターンは、クラス階層ではなく、インターフェース、構造体埋め込み、第一級関数を用いて適応させる必要がある。
Functional Optionsパターン:柔軟な設定の実現
Functional Optionsパターンは、Goにおける構造体の初期化で頻出する課題を解決する。メソッドのオーバーロードもデフォルト引数もないGoでは、オプショナルなパラメータの扱いが問題になる。Builderパターンも機能するが冗長になりがちである。Functional Optionsは、後方互換性を保ちつつ拡張可能なクリーンなAPIを提供する。
このパターンはDave Cheneyによって広められ、現在では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では、抽象クラスや継承チェーンを必要とせず、インターフェースベースのポリモーフィズムで直接表現できる。
決済処理システムがこのパターンの好例である。各決済方法が同一のインターフェースを実装し、チェックアウトサービスが実行時にストラテジーを選択する。
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と命名されたファクトリ関数が初期化済みの構造体を返す。ファクトリ関数は、ゼロ値の初期化では保証できない不変条件を強制するために使用される。
面接準備においてこの区別は重要である。ファクトリ関数が必要な場合と、ゼロ値がそのまま有用な場合を正しく判断できる能力が求められる。
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と組み合わせることで、より複雑な設定シナリオにも対応できる。
Goの面接対策はできていますか?
インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。
Observerパターン:チャネルとゴルーチンによる実装
Observerパターンは、状態が変化した際に複数の購読者に通知を送る。JavaやC#ではイベントリスナーとコールバックの登録が必要になるが、Goではチャネルというよりイディオマティックな手段がある。各購読者は専用のチャネルでイベントを受信し、ゴルーチンが並行的にイベントを配信する。
このアプローチは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インターフェースにより、この構成は極めて容易である。
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などの人気ルーターでも同一で、func(http.Handler) http.Handler型のミドルウェアを受け付ける。
構造体埋め込みによるコンポジション
Goは継承をサポートしていない。代わりに、構造体埋め込み(struct embedding)を使って内部型のメソッドを外部型に昇格させ、密結合なしにコードを再利用する。これは継承ではない。埋め込まれた型は埋め込み元の型について何も知らず、仮想ディスパッチも発生しない。
この区別を理解することはGoの面接において極めて重要である。埋め込みと継承を混同する候補者は、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宣言は不要である。これにより「インターフェースを受け取り、構造体を返す」という原則が成立する。つまり、実装側ではなく呼び出し側で狭いインターフェースを定義する。その結果、結合度が低くテスタビリティが高いコードが実現される。
面接官は候補者に、具体的な依存をインターフェースにリファクタリングするよう求めることが多い。テストされるのは、必要最小限のメソッドセットを特定し、実装側ではなく利用側でインターフェースを抽出できるかどうかである。
Q: チャネルベースのObserverとコールバックベースのObserverの違いは何か?
チャネルはパブリッシャーとサブスクライバーを時間的にも空間的にも分離する。パブリッシャーはサブスクライバーの関数への参照を保持せず、チャネルに送信するだけである。サブスクライバーはバッファ付きチャネルを使って自分のペースでイベントを処理できる。select文により、タイムアウト処理、contextによるキャンセル、複数のイベントソースの多重化が可能になる。これらはコールバックでは得られない機能である。
サブスクライバーチャネルを閉じ忘れるとゴルーチンリークが発生する。すべてのSubscribeには、チャネルを閉じてバスから削除する対応するUnsubscribeが必要である。
Goの面接対策はできていますか?
インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。
まとめ
- Functional Optionsパターンは、Builderや設定構造体を、既存の呼び出し側を壊さないクリーンで拡張可能なAPIに置き換える
- GoのStrategyパターンはインターフェースベースのポリモーフィズムに対応する。インターフェースの定義は実装側ではなく利用側で行う
- ファクトリ関数はゼロ値初期化では保証できない不変条件を強制する。ランタイム入力にはエラーを返し、パニックはプログラマのバグにのみ使用する
- チャネルベースのObserverはゴルーチンとselectを活用し、コールバックチェーンなしに並行的で疎結合なイベント配信を実現する
- HTTPミドルウェアチェーンは
func(http.Handler) http.Handlerシグネチャを通じて構成され、標準ライブラリとサードパーティルーター全体で共通している - 構造体埋め込みは委譲を提供するものであり、継承ではない。この区別を理解することが、Goを深く理解する開発者とJavaパターンをそのまま移植する開発者を分ける
- 面接での成功には、Gang of Fourの定義の暗記ではなく、パターンをGoの型システムにイディオマティックに適応させる能力が求められる
今すぐ練習を始めましょう!
面接シミュレーターと技術テストで知識をテストしましょう。
タグ
共有
関連記事

Go 1.26面接対策:Green Tea GC、go fixツール、スタック最適化の徹底解説
Go 1.26の面接対策として、新しいGreen Teaガベージコレクタ、刷新されたgo fixツール、スライスのスタック割り当て最適化など、主要な変更点を詳しく解説します。

Go面接の頻出25問: 開発者向け完全ガイド
頻出25問でGoの面接を制する。ゴルーチン、チャネル、インターフェース、並行処理パターンをコード例とともに解説。

Go技術面接:Goroutine、Channel、並行処理パターン完全ガイド
Go技術面接で頻出のgoroutine、channel、並行処理に関する質問を網羅。本番レベルのコード例と各回答の背景にある設計思想を2026年の面接対策として解説します。