2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Goの豆知識 — HandlerFunc アダプター

Posted at

1. はじめに

Go では、アダプタパターン(Adapter Pattern) を使って、通常の関数をインターフェース実装としてラップすることができます。
これにより、関数そのものをインターフェースの呼び出し仕様に直接適合させることが可能です。

このパターンの代表的な例は、標準ライブラリ net/httpHandlerFunc です。
HandlerFunc を使うことで、構造体を定義せずに関数を HTTP ハンドラーとして利用できます。

本記事では、まずパターンの原理を説明し、簡易的な Echo 風ルーターを使って実践例を示します。

アダプタパターンは HTTP に限らず、ミドルウェアチェーン、イベントコールバック、タスク実行、戦略パターンなどさまざまな場面で利用できます。HTTP ルーティングはその中で最も一般的な応用例のひとつです。


2. 背景

  • 歴史的背景
    • Go は 1.0 以降、関数を 第一級オブジェクト(first-class citizen) として扱え、関数型がインターフェースを実装できることをサポートしています
    • 標準ライブラリの net/httpHandlerFunc を最初に導入し、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")
}

img.png

  • ポイント:関数を 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. 参考

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?