1. はじめに
Go では、アダプタパターン(Adapter Pattern) を使って、通常の関数をインターフェース実装としてラップすることができます。
これにより、関数そのものをインターフェースの呼び出し仕様に直接適合させることが可能です。
このパターンの代表的な例は、標準ライブラリ net/http
の HandlerFunc
です。
HandlerFunc
を使うことで、構造体を定義せずに関数を HTTP ハンドラーとして利用できます。
本記事では、まずパターンの原理を説明し、簡易的な Echo 風ルーターを使って実践例を示します。
アダプタパターンは HTTP に限らず、ミドルウェアチェーン、イベントコールバック、タスク実行、戦略パターンなどさまざまな場面で利用できます。HTTP ルーティングはその中で最も一般的な応用例のひとつです。
2. 背景
-
歴史的背景:
- Go は 1.0 以降、関数を 第一級オブジェクト(first-class citizen) として扱え、関数型がインターフェースを実装できることをサポートしています
- 標準ライブラリの
net/http
はHandlerFunc
を最初に導入し、Gin や Echo などのフレームワーク設計のモデルとなりました
-
設計の意図:
- Go は シンプルさ と コンポジション を重視し、継承による拡張を推奨していません
- アダプタパターンは、関数とインターフェースの間に橋をかけることで、関数のシンプルさを保ちつつ、インターフェース駆動のフレームワークとシームレスに統合できます
-
解決する課題:
- 関数や構造体メソッドでも、統一されたインターフェースとして扱える
- フレームワーク設計における中間処理の統一や登録方式、拡張性を容易にする
3. 実際に解決できること
- インターフェースの統一:関数やも構造体ハンドラーでも同じように処理できる
- チェーン構築のサポート:ログ、認証、Context などのミドルウェアを柔軟に組み合わせ可能
- 多様な登録方法の統一:異なる形式の関数やオブジェクトも、同じインターフェースを通じて登録できる
- テストの容易さ:関数が直接インターフェースを実装しているため、単体テストが簡単で、サービス全体を起動する必要がない
これらにより、Go フレームワークでは関数ベースのシンプルさを保ちつつ、インターフェース駆動の柔軟な設計が可能になります。
4. 依存なしでの最小例
package main
import "fmt"
type Handler interface {
ServeHTTP(data string)
}
// HandlerFunc は関数型
type HandlerFunc func(string)
// HandlerFunc にメソッドを追加して Handler インターフェースに適合
func (f HandlerFunc) ServeHTTP(data string) {
f(data)
}
type Router struct {
routes map[string]Handler
}
func NewRouter() *Router {
return &Router{routes: make(map[string]Handler)}
}
func (r *Router) Handle(path string, h Handler) {
r.routes[path] = h
}
// HandleFunc は Handle の構文シュガー(syntactic sugar)、明示的な変換を省略
func (r *Router) HandleFunc(path string, f func(string)) {
r.Handle(path, HandlerFunc(f))
}
// サンプル呼び出し
func (r *Router) Serve(path string, data string) {
if h, ok := r.routes[path]; ok {
h.ServeHTTP(data)
} else {
fmt.Println("404 Not Found:", path)
}
}
func main() {
router := NewRouter()
// 方法1: 明示的に HandlerFunc を登録
router.Handle("/hello", HandlerFunc(func(s string) {
fmt.Println("Hello,", s)
}))
// 方法2: HandleFunc を使うと簡潔に登録できる
router.HandleFunc("/index", func(s string) {
fmt.Println("Index,", s)
})
// 擬似リクエスト
router.Serve("/hello", "ABC")
router.Serve("/index", "EFG")
router.Serve("/notfound", "ZXC")
}
-
ポイント:関数を
HandlerFunc
でラップすることでHandler
インターフェースを実装し、独自ルーターに登録可能です。サードパーティの依存は不要です
5. 実践例:Echo 風ルーター
ミドルウェア
import (
"context"
"fmt"
"net/http"
)
type Middleware func(http.Handler) http.Handler
// Chain では中間ウェアを逆順にラップ、登録順に実行される
func Chain(h http.Handler, m ...Middleware) http.Handler {
for i := len(m) - 1; i >= 0; i-- {
h = m[i](h)
}
return h
}
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("[LOG] %s %s\n", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Auth") != "secret" {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
func ContextMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), "user", "Tom")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Router 実装
type Router struct {
routes map[string]map[string]http.Handler
middleware []Middleware
}
func NewRouter() *Router {
return &Router{routes: make(map[string]map[string]http.Handler)}
}
// ミドルウェア登録
func (r *Router) Use(m Middleware) {
r.middleware = append(r.middleware, m)
}
func (r *Router) Handle(method, path string, handler http.Handler) {
if r.routes[method] == nil {
r.routes[method] = make(map[string]http.Handler)
}
handler = Chain(handler, r.middleware...) // 全体ミドルウェアを適用
r.routes[method][path] = handler
}
// HandleFunc は Handle の構文シュガー(syntactic sugar)
func (r *Router) HandleFunc(method, path string, f func(http.ResponseWriter, *http.Request)) {
r.Handle(method, path, http.HandlerFunc(f))
}
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if methodRoutes, ok := r.routes[req.Method]; ok {
if h, ok := methodRoutes[req.URL.Path]; ok {
h.ServeHTTP(w, req)
return
}
}
http.NotFound(w, req)
}
Handler 例
func HelloHandler(w http.ResponseWriter, r *http.Request) {
user, ok := r.Context().Value("user").(string)
if !ok {
user = "unknown"
}
fmt.Fprintf(w, "Hello, %s!", user)
}
func EchoHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "You hit %s with %s", r.URL.Path, r.Method)
}
サーバー起動
func main() {
router := NewRouter()
router.Use(LoggingMiddleware)
router.Use(AuthMiddleware)
router.Use(ContextMiddleware)
router.HandleFunc("GET", "/hello", HelloHandler)
router.HandleFunc("POST", "/echo", EchoHandler)
// 明示的な登録も可能
router.Handle("POST", "/echo", http.HandlerFunc(EchoHandler))
fmt.Println("Server running on :8080")
http.ListenAndServe(":8080", router)
}
-
ポイント:全体ミドルウェアを自動で適用し、関数を直接
Handler
に適合させることができます。Echo や Gin のようなルーターとミドルウェアチェーンに応用可能です
6. まとめ
-
アダプタパターン は Go で「関数 → インターフェース」の橋渡し役を果たします。
net/http
標準ライブラリが最初に導入し、現在ではほとんどの Go フレームワークで採用されているベストプラクティスです -
メリット:
- シンプルな関数を書きながら、フレームワークのインターフェースとスムーズに組み合わせることができる
- 継承なしで柔軟な拡張、コンポジション設計を実現
- 単体テストが容易
-
応用例:
- Web フレームワーク:関数を統一インターフェースに適合させ、ミドルウェアチェーンを簡単に構築
- RPC や DB ドライバ:ハンドラー、コールバック、プラグイン機構の拡張
- 単体テスト:関数をインターフェースのモックとして利用
-
注意点:
- 過剰に使うとインターフェース階層が増え、理解コストが上がる
- アダプタが多すぎると呼び出しチェーンが長くなり、デバッグが難しくなる
フレームワークレベルでの利用は非常に理にかなっていますが、ビジネスロジック内で頻繁に使うと読みにくくなるため、用途を考えて使い分けることが重要です。
7. 参考
- Go 標準ライブラリ:net/http HandlerFunc
- Echo ソースコード:router.go#L43