他の言語で学んだデザインパターン、Goでどう実装すればいいか迷ったことはありませんか?「Singletonパターンを実装したいけど、Goにはクラスがない」「Decoratorパターンを使いたいが、継承がないGoでどうすれば?」
実は、Goはシンプルさが重視される言語のため、従来のGoF(Gang of Four)デザインパターンがそのまま当てはまらないケースが多いのです。しかし、それは決して問題ではありません。
この記事ではパターンをなぞるのではなく、それが「解決しようとしていた課題」を、Goの思想に沿ったシンプルで効果的な方法で解決する 「Goらしい」アプローチを紹介します。パターンの暗記から脱却し、本質的な問題解決能力を身につけましょう!
具体的なパターンに入る前に、Goがデザインパターンとどう向き合うべきかの基本思想をEffective Goから学びましょう。
Goには継承がありませんが構造体の埋め込みにより、機能を組み合わせることで柔軟な設計を実現できます。これにより複雑な継承階層を避けながら、コードの再利用性を高められます。
Goのインターフェースは、「小さく、目的を一つに絞る」ことが推奨されます。io.Reader
やio.Writer
のような単一メソッドのインターフェースが、多くのパターンの土台となります。
複雑な設計よりも、誰が読んでも理解できる、明確でシンプルなコードが最も価値があります。Goの文化では「Clever code is bad code」という考え方が根付いており、巧妙すぎるコードよりも素直で分かりやすいコードが好まれます。
Goの思想が色濃く反映されるパターンを3つ選び、コード例を交えて解説します。
課題: アプリケーション全体で唯一のインスタンスを保証したい。
Goらしい解決策: 標準ライブラリのsync.Once
を使うのが最もシンプルで安全です。
go1package main 2 3import ( 4 "fmt" 5 "sync" 6) 7 8type Database struct { 9 connection string 10} 11 12var ( 13 dbInstance *Database 14 once sync.Once 15) 16 17func GetDatabase() *Database { 18 once.Do(func() { 19 fmt.Println("データベース接続を初期化中...") 20 dbInstance = &Database{ 21 connection: "database://localhost:5432", 22 } 23 }) 24 return dbInstance 25}
once.Do()
は、渡された関数がアプリケーションのライフサイクルを通じて一度しか実行されないことを保証し、安全で読みやすいSingletonを実装できます。sync.Once
はメモリバリア保証により、マルチゴルーチン環境でも安全に動作します。
✅ 適用すべきシチュエーション: 設定ファイル読み込み、ログ出力先の初期化など、アプリケーション全体で共有する重いリソースの初期化
❌ 適用すべきでないシチュエーション: テストでモック差し替えが必要な場合、Webアプリのリクエストスコープごとに依存注入(DI)で渡した方が柔軟な場合
テストでモック差し替えが必要な場合や、Webアプリのリクエストスコープごとに依存注入(DI)で渡した方が柔軟な場合は、Singletonよりも依存注入を検討しましょう。
「Global is convenient, but often harmful」— Effective Go
グローバル状態はテストを困難にし、コードの結合度を高める可能性があります。
課題: 既存のオブジェクトの機能を、動的に追加・変更したい。
Goらしい解決策: http.Handler
のミドルウェアパターンのように、同じインターフェースを満たす関数を重ねていくことで実現します。
go1package main 2 3import ( 4 "log" 5 "net/http" 6 "time" 7) 8 9// ログ出力ミドルウェア(デコレーター) 10func LoggingMiddleware(next http.Handler) http.Handler { 11 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 12 start := time.Now() 13 log.Printf("開始: %s %s", r.Method, r.URL.Path) 14 next.ServeHTTP(w, r) 15 log.Printf("完了: %s %s (%v)", r.Method, r.URL.Path, time.Since(start)) 16 }) 17} 18 19// メインハンドラー 20func HelloHandler(w http.ResponseWriter, r *http.Request) { 21 w.Header().Set("Content-Type", "text/plain; charset=utf-8") // レスポンスヘッダを設定 22 w.Write([]byte("Hello, World!")) 23} 24 25func main() { 26 // ミドルウェアを適用 27 handler := LoggingMiddleware(http.HandlerFunc(HelloHandler)) 28 http.Handle("/hello", handler) 29 log.Fatal(http.ListenAndServe(":8080", nil)) 30}
この例では、LoggingMiddleware
がHelloHandler
を「装飾」しています。継承を使わず、関数の合成だけで柔軟なデコレーターを実現しています。
✅ 適用すべきシチュエーション: HTTPミドルウェア、ログ出力、認証・認可、レート制限など、既存の処理に横断的な機能を追加したい場合
❌ 適用すべきでないシチュエーション: 単純な処理で装飾が不要な場合、パフォーマンスが重要で関数呼び出しのオーバーヘッドを避けたい場合
Web以外でも、io.Reader
やio.Writer
インターフェースを使って同様のパターンを適用できます:
go1package main 2 3import ( 4 "fmt" 5 "io" 6 "strings" 7) 8 9// 大文字変換Decorator 10type UppercaseReader struct { 11 reader io.Reader 12} 13 14func NewUppercaseReader(r io.Reader) *UppercaseReader { 15 return &UppercaseReader{reader: r} 16} 17 18func (u *UppercaseReader) Read(p []byte) (n int, err error) { 19 n, err = u.reader.Read(p) 20 for i := 0; i < n; i++ { 21 if p[i] >= 'a' && p[i] <= 'z' { 22 p[i] = p[i] - 'a' + 'A' 23 } 24 } 25 return n, err 26} 27 28func main() { 29 original := strings.NewReader("hello world") 30 decorated := NewUppercaseReader(original) 31 32 result, _ := io.ReadAll(decorated) 33 fmt.Println(string(result)) // "HELLO WORLD" 34}
このように同じインターフェースを満たすことで、元の機能を拡張できます。
課題: 多くの設定項目を持つ複雑なオブジェクトを、柔軟に生成したい。
Goらしい解決策: オプションを設定する関数を可変長引数でコンストラクタに渡す「Functional Options Pattern」というイディオムが広く採用されています。
go1package main 2 3import ( 4 "time" 5) 6 7type Server struct { 8 host string 9 port int 10 timeout time.Duration 11} 12 13// オプション関数の型定義 14type Option func(*Server) 15 16// 各設定項目のオプション関数 17func WithHost(host string) Option { 18 return func(s *Server) { 19 s.host = host 20 } 21} 22func WithPort(port int) Option { 23 return func(s *Server) { 24 s.port = port 25 } 26} 27func WithTimeout(timeout time.Duration) Option { 28 return func(s *Server) { 29 s.timeout = timeout 30 } 31} 32 33// コンストラクタ 34func NewServer(opts ...Option) *Server { 35 // デフォルト値を設定 36 server := &Server{ 37 host: "localhost", 38 port: 8080, 39 timeout: 30 * time.Second, 40 } 41 // オプションを適用 42 for _, opt := range opts { 43 opt(server) 44 } 45 return server 46} 47 48func main() { 49 // 必要なオプションだけを指定 50 server := NewServer(WithPort(9090)) 51}
このパターンは、引数の順序を気にする必要がなく、将来的にオプションを追加しても既存のコードに影響しないという大きな利点があります。
✅ 適用すべきシチュエーション: 設定項目が多いライブラリのAPI、HTTPクライアント設定、データベース接続設定など、柔軟な設定が必要な場合
❌ 適用すべきでないシチュエーション: 設定項目が少ない(2-3個以下)場合、パフォーマンスが最重要でオーバーヘッドを避けたい場合
Functional Options Patternの実用性を確認するテストとベンチマーク:
go1package main 2 3import ( 4 "testing" 5 "time" 6) 7 8func TestNewServer(t *testing.T) { 9 tests := []struct { 10 name string 11 opts []Option 12 expected Server 13 }{ 14 { 15 name: "デフォルト設定", 16 opts: []Option{}, 17 expected: Server{ 18 host: "localhost", 19 port: 8080, 20 timeout: 30 * time.Second, 21 }, 22 }, 23 { 24 name: "カスタム設定", 25 opts: []Option{WithHost("example.com"), WithPort(9090)}, 26 expected: Server{ 27 host: "example.com", 28 port: 9090, 29 timeout: 30 * time.Second, 30 }, 31 }, 32 } 33 34 for _, tt := range tests { 35 t.Run(tt.name, func(t *testing.T) { 36 server := NewServer(tt.opts...) 37 if *server != tt.expected { 38 t.Errorf("期待値: %+v, 実際: %+v", tt.expected, *server) 39 } 40 }) 41 } 42} 43 44func BenchmarkNewServer(b *testing.B) { 45 opts := []Option{WithHost("example.com"), WithPort(9090), WithTimeout(60 * time.Second)} 46 47 b.ResetTimer() 48 for i := 0; i < b.N; i++ { 49 _ = NewServer(opts...) 50 } 51}
このテストにより、オプションの組み合わせが正しく動作することを確認でき、実務での採用に自信を持てます。
実務でGoデザインパターンを適用する際の判断基準をチェックリスト形式でまとめました:
Goにおけるデザインパターンとは、GoFのカタログを暗記することではありません。「課題」に対してGoのツール(インターフェース、関数、合成)を使って「最もシンプルで明確な解決策」を見つけ出す思考プロセスなのです。
今回紹介した3つのアプローチは、いずれもGoの思想である「シンプルさ」「明確さ」「合成」を体現しています。複雑な継承階層や抽象化よりも、誰が読んでも理解できる素直なコードを書くことが、Goエンジニアとしての真の価値です。
「Goらしい」コードを書くことは、あなたのコードの品質を高めるだけでなく、Goエンジニアとしての市場価値を向上させることに直結します。
私たちGoForceでは、このようなGoの思想を深く理解し、質の高いコードを書けるエンジニアを求めているプロジェクトを多数ご紹介しています。あなたの「Goらしさ」を活かせる現場に興味がある方は、ぜひお気軽にご登録ください!
最適なGo案件を今すぐチェック!