概要
これまでアクション用のハンドラ関数は 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 といったりします