2026年6月29日月曜日

Go + gin でハンドラ関数を struct にし interface 化もする

Go + gin でハンドラ関数を struct にし interface 化もする

概要

これまでアクション用のハンドラ関数は struct にせずに関数だけを定義しました
テストなども考えると struct にし更に interface を定義することで管理も容易になります

環境

  • macOS 26.5.1
  • golang 1.26.4
    • gin 1.12.0

アクション関数のサービス化と struce/interface 化

アクション関数をサービスに移動します
ユースケースと同じような扱いですが今回はわかりやすくサービスに移動します

またアクション関数は gin や http に依存しないように修正します

そしてそれぞれのアクション関数を struct として定義します
更にアクション関数が持つべき関数などを interface として定義します

  • vim service/action.go
package service

import (
	"fmt"

	"strconv"
)

type Action interface {
	Execute(params map[string]string, method string) (any, error)
}

type HelloAction struct{}

func (a *HelloAction) Execute(params map[string]string, method string) (any, error) {
	name := params["name"]
	if name == "" {
		name = "guest"
	}
	return map[string]string{
		"message": "hello " + name,
	}, nil
}

type SumAction struct{}

func (a *SumAction) Execute(params map[string]string, method string) (any, error) {
	aVal, err := strconv.Atoi(params["a"])
	if err != nil {
		return nil, fmt.Errorf("invalid parameter 'a'")
	}

	bVal, err := strconv.Atoi(params["b"])
	if err != nil {
		return nil, fmt.Errorf("invalid parameter 'b'")
	}

	return map[string]interface{}{
		"a":   aVal,
		"b":   bVal,
		"sum": aVal + bVal,
	}, nil
}

dispatcher の修正

dispatcher は interface を使用するように修正します

  • vim handler/dispatcher.go
package handler

import (
	"errors"

	"gin-sample/service"
)

type Dispatcher struct {
	actions map[string]service.Action
}

func NewDispatcher() *Dispatcher {
	return &Dispatcher{
		actions: make(map[string]service.Action),
	}
}

// アクション登録
func (d *Dispatcher) Register(name string, action service.Action) {
	d.actions[name] = action
}

// 登録されたアクションに紐づくハンドラ関数の実行
func (d *Dispatcher) Execute(name string, params map[string]string, method string) (any, error) {
	if action, ok := d.actions[name]; ok {
		return action.Execute(params, method)
	}
	return nil, errors.New("unknown action: " + name)
}

main.go の修正

dispatcher に登録するハンドラは関数から struct に変更します
登録する際は interface ではなく実装したそれぞれの struct 版のアクションを定義します

渡しているのは Action interfaceを満たす構造体のポインタを渡す必要があります

  • vim main.go
package main

import (
	"gin-sample/handler"
	"gin-sample/middleware"
	"gin-sample/service"

	"github.com/gin-gonic/gin"
)

func main() {
	dispatcher := handler.NewDispatcher()

	// ★ handlerから登録
	dispatcher.Register("Hello", &service.HelloAction{})
	dispatcher.Register("Sum", &service.SumAction{})

	r := gin.Default()
	r.Use(middleware.ErrorHandler())

	r.GET("/", handler.Wrap(handler.HandleAction(dispatcher)))
	r.POST("/", handler.Wrap(handler.HandleAction(dispatcher)))

	r.Run()
}

アクションハンドラ

これは今まで通りです

  • vim handler/action_handler.go
package handler

import (
	customError "gin-sample/error"
	"gin-sample/model"

	"github.com/gin-gonic/gin"
)

type AppHandler func(c *gin.Context) error

func Wrap(h AppHandler) gin.HandlerFunc {
	return func(c *gin.Context) {
		if err := h(c); err != nil {
			c.Error(err) // middlewareに流す
		}
	}
}

func HandleAction(dispatcher *Dispatcher) AppHandler {
	return func(c *gin.Context) error {
		requestID := c.GetHeader("X-Request-Id")
		if requestID == "" {
			requestID = "dummy-request-id"
		}

		// パラメータ統合
		if err := c.Request.ParseForm(); err != nil {
			return customError.New("INVALID_REQUEST", "invalid request", 400)
		}

		params := make(map[string]string)
		for key, values := range c.Request.Form {
			if len(values) > 0 {
				params[key] = values[0]
			}
		}

		actionName := params["Action"]

		result, err := dispatcher.Execute(actionName, params, c.Request.Method)
		if err != nil {
			return err
		}

		// 成功時だけレスポンスを書く
		model.RespondSuccess(c, gin.H{
			"requestId": requestID,
			"action":    actionName,
			"result":    result,
		})

		return nil
	}
}

動作確認

  • gofmt -w . && go run main.go
curl -X POST http://localhost:8080 -d "Action=Sum&a=1&b=2"
{"success":true,"data":{"action":"Sum","requestId":"dummy-request-id","result":{"a":1,"b":2,"sum":3}}}

curl -X GET "http://localhost:8080?Action=Hello"
{"success":true,"data":{"action":"Hello","requestId":"dummy-request-id","result":{"message":"hello guest"}}}

最後に

DI を使うようなケースでは可能な限り struct として定義したほうがテストがしやすいです
更に同じような振る舞いがある場合は interface として定義依存関係を interface 経由にするとより管理がしやすいです

ちなみにこういった interface に依存性を持たせる手法を DIP といったりします

0 件のコメント:

コメントを投稿