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 といったりします

2026年6月28日日曜日

Go + gin でエラーハンドリング

Go + gin でエラーハンドリング

概要

前回レスポンスモデルを作成しました

今回はエラーハンドラを作成します
ハンドラや各種関数では Error を返却しそれをまとめてハンドリングするミドルウェアをアプリに登録します

環境

  • macOS 26.5.1
  • golang 1.26.4
    • gin 1.12.0

カスタムエラーの作成

カスタムエラーを作成します
エラーは発生した箇所は基本的にこのエラーは return するようにします

  • vim error/error.go
package error

type AppError struct {
	Code    string
	Message string
	Status  int
}

func (e *AppError) Error() string {
	return e.Message
}

// helper
func New(code, message string, status int) *AppError {
	return &AppError{
		Code:    code,
		Message: message,
		Status:  status,
	}
}

エラーハンドラの作成

各所では c.JSON を使ってエラーを返却せずに return Error します
そしてそこで返されたエラーをこのハンドラがキャッチしここでエラーレスポンスを使って返却します

今回はカスタムエラー以外は 500 エラーにしています

  • vim middleware/error_handler.go
package middleware

import (
	"net/http"

	"gin-sample/error"
	"gin-sample/model"

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

func ErrorHandler() gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if r := recover(); r != nil {
				// panic捕捉
				model.RespondError(c, http.StatusInternalServerError,
					"INTERNAL_ERROR", "unexpected error")
			}
		}()

		c.Next()

		// エラーがなければ何もしない
		if len(c.Errors) == 0 {
			return
		}

		err := c.Errors.Last().Err

		switch e := err.(type) {
		case *error.AppError:
			model.RespondError(c, e.Status, e.Code, e.Message)
		default:
			model.RespondError(c, http.StatusInternalServerError,
				"INTERNAL_ERROR", err.Error())
		}
	}
}

アクションハンドラの修正

アクションハンドラ側でエラーを生成した場合はここでも return するようにします
少しポイントが必要で gin に登録するハンドラメソッドの形式にするために Wrap 関数を作成してあげます

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, c.Request)
		if err != nil {
			return err // ★ここ重要
		}

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

		return nil
	}
}

ハンドラ関数でもエラーを返すようにする

ハンドラ関数でも c.JSON を使わずにエラーを返すようにします

  • vim handler/action.go
package handler

import (
	"net/http"
	"strconv"

	customError "gin-sample/error"
)

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

func SumAction(params map[string]string, method string, r *http.Request) (any, error) {
	aStr := params["a"]
	bStr := params["b"]

	a, err := strconv.Atoi(aStr)
	if err != nil {
		return nil, customError.New("INVALID_PARAM", "a is invalid", 400)
	}

	b, err := strconv.Atoi(bStr)
	if err != nil {
		return nil, customError.New("INVALID_PARAM", "b is invalid", 400)
	}

	sum := a + b

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

動作確認

  • gofmt -w .
  • go run main.go
curl -X POST http://localhost:8080 \
  -H "Content-Type: multipart/form-data; boundary=----abc"
  
{"success":false,"error":{"code":"INTERNAL_ERROR","message":"unknown action: "}}

curl -X POST http://localhost:8080 \
-d "Action=Sum&"

{"success":false,"error":{"code":"INVALID_PARAM","message":"a is invalid"}}

c.Request.ParseForm() の部分はエラーにするのは難しいです

最後に

無闇矢鱈に c.JSON でレスポンスは返却せずにエラーハンドラでまとめましょう
その他の箇所では単純に error を return するだけです

2026年6月27日土曜日

Go + gin でレスポンスモデルを管理する

Go + gin でレスポンスモデルを管理する

概要

前回の続きです
レスポンスをハンドラ内でそのまま返していましたが別モジュールとして管理します
またハンドラも別モジュール化します

環境

  • macOS 26.5.1
  • golang 1.26.4
    • gin 1.12.0

レスポンスモデル

成功時のモデルと失敗時のモデルを管理します
またそのモデルを使ってレスポンス情報を生成するメソッドも作成します
本当はこのメソッドも別モジュールのほうがいいかなと思います

  • vim model/response.go
package model

import (
        "net/http"

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

type APIResponse struct {
        Success bool        `json:"success"`
        Data    interface{} `json:"data,omitempty"`
        Error   *APIError   `json:"error,omitempty"`
}

type APIError struct {
        Code    string `json:"code"`
        Message string `json:"message"`
}

func RespondSuccess(c *gin.Context, data interface{}) {
        c.JSON(http.StatusOK, APIResponse{
                Success: true,
                Data:    data,
        })
}

func RespondError(c *gin.Context, status int, code, message string) {
        c.JSON(status, APIResponse{
                Success: false,
                Error: &APIError{
                        Code:    code,
                        Message: message,
                },
        })
}

ハンドラ

アクションごとに dispatcher に登録するハンドラ関数を別のモジュールに移動します
ここで先程のレスポンスモデルを使います

  • vim handler/action.go
package handler

import (
        "fmt"
        "net/http"
        "strconv"
)

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

func SumAction(params map[string]string, method string, r *http.Request) (any, error) {
        aStr := params["a"]
        bStr := params["b"]

        a, err := strconv.Atoi(aStr)
        if err != nil {
                return nil, fmt.Errorf("invalid parameter 'a'")
        }

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

        sum := a + b

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

main.go の修正

ハンドラ関数がなくなったのでかなりすっきりします

  • vim main.go
package main

import (
        "gin-sample/handler"

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

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

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

        r := gin.Default()

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

        r.Run()
}

動作確認

  • gofmt -w .
  • go run main.go
curl -X POST http://localhost:8080 \                
  -d "Action=Sum&a=10&b=20"
{"action":"Sum","requestId":"dummy-request-id","result":{"a":10,"b":20,"sum":30}}

curl "http://localhost:8080/?Action=Hello&name=Taro"
{"action":"Hello","requestId":"dummy-request-id","result":{"message":"hello Taro"}}

最後に

エラーハンドリングの共通化ができていないのでそのあたりはやったほうがいいです

2026年6月26日金曜日

Go + gin で dispatcher パターン入門

Go + gin で dispatcher パターン入門

概要

簡単なサンプルを紹介します
公式サンプルが基本一番わかりやすいです

環境

  • macOS 26.5.1
  • golang 1.26.4
    • gin 1.12.0

準備

とりあえず動かす

ルータを作成しルーティング情報を追加します
ルーティングが行う処理は関数として渡します

gin にはレスポンスを返すためのメソッド (application/json なら JSON や text/plain なら String などなど) があります

ルーティングを登録したら Run で起動できます

  • vim main.go
package main

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

func main() {
        // ルーター作成(デフォルト設定)
        r := gin.Default()

        // GET / にアクセスしたときの処理
        r.GET("/", func(c *gin.Context) {
                c.JSON(200, gin.H{
                        "message": "Hello, Gin!",
                })
        })

        // GET /ping
        r.GET("/ping", func(c *gin.Context) {
                c.String(200, "pong")
        })

        // サーバー起動(デフォルト: :8080)
        r.Run()
}
  • go run main.go
curl localhost:8080     
{"message":"Hello, Gin!"}

curl localhost:8080/ping
pong

こんな感じで動作します

POST + JSON でボディを受け取る

これも王道な方法です
fastapi に近い感じですが gin の場合は struct を準備しボディ情報を自動で変換してくれます
変換できない場合はエラーを返してくれます

  • vim main.go
package main

import (
        "net/http"

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

// リクエスト用構造体
type User struct {
        Name  string `json:"name" binding:"required"`
        Email string `json:"email" binding:"required,email"`
        Age   int    `json:"age"`
}

func main() {
        r := gin.Default()

        // GET /
        r.GET("/", func(c *gin.Context) {
                c.JSON(http.StatusOK, gin.H{
                        "message": "Hello, Gin!",
                })
        })

        // POST /user
        r.POST("/user", func(c *gin.Context) {
                var user User

                // JSONを構造体にバインド
                if err := c.ShouldBindJSON(&user); err != nil {
                        c.JSON(http.StatusBadRequest, gin.H{
                                "error": err.Error(),
                        })
                        return
                }

                // 成功レスポンス
                c.JSON(http.StatusOK, gin.H{
                        "message": "user created",
                        "user":    user,
                })
        })

        r.Run()
}

正常系と異常系は以下の通りです

curl -X POST http://localhost:8080/user \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Taro",
    "email": "taro@example.com",
    "age": 20
  }'
{"message":"user created","user":{"name":"Taro","email":"taro@example.com","age":20}}
curl -X POST http://localhost:8080/user \
  -H "Content-Type: application/json" \
  -d '{
    "email": "taro@example.com",
    "age": 20
  }'
{"error":"Key: 'User.Name' Error:Field validation for 'Name' failed on the 'required' tag"}

POST + JSON でボディの生情報を扱う

c.Request.Body で取得できます gin ではボディ情報は IO ストリームです
ストリームなので Read するのですが Read した場合次の Read ではデータがないのでエラーになります
つまり一度しか Read できないので注意しましょう

package main

import (
        "encoding/json"

        "io"
        "net/http"

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

func main() {
        r := gin.Default()

        r.POST("/raw", func(c *gin.Context) {
                body, err := io.ReadAll(c.Request.Body)
                if err != nil {
                        c.String(http.StatusBadRequest, "read error")
                        return
                }

                // byte → string
                raw := string(body)

                c.JSON(http.StatusOK, gin.H{
                        "raw": raw,
                })
        })

        r.POST("/raw-to-map", func(c *gin.Context) {
                body, _ := c.GetRawData()

                var data map[string]interface{}

                if err := json.Unmarshal(body, &data); err != nil {
                        c.JSON(http.StatusBadRequest, gin.H{
                                "error": "invalid json",
                        })
                        return
                }

                c.JSON(http.StatusOK, gin.H{
                        "map": data,
                })
        })

        r.Run()
}

動作確認は以下です

curl -X POST http://localhost:8080/raw \
  -H "Content-Type: application/json" \
  -d '{"name":"taro"}'
{"raw":"{\"name\":\"taro\"}"}
curl -X POST http://localhost:8080/raw-to-map \
  -H "Content-Type: application/json" \
  -d '{"name":"taro"}'
{"map":{"name":"taro"}}

ルーティングの処理用の関数(ハンドラ)を切り出す

実際のプロダクションではもっと大規模になるはずなのでハンドラを切り出す必要があります

ポイントとしては切り出した関数は *gin.Context を持つ必要があります

  • vim handler/user.go
package handler

import (
        "net/http"

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

// 依存を持つための構造体
type UserHandler struct {
        AppName string
}

// コンストラクタ
func NewUserHandler(appName string) *UserHandler {
        return &UserHandler{
                AppName: appName,
        }
}

// GET /
func (h *UserHandler) Hello(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
                "message": "Hello from " + h.AppName,
        })
}

// POST /user
func (h *UserHandler) CreateUser(c *gin.Context) {
        var body map[string]interface{}

        if err := c.ShouldBindJSON(&body); err != nil {
                c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
                return
        }

        c.JSON(http.StatusOK, gin.H{
                "app":  h.AppName,
                "user": body,
        })
}
  • vim main.go
package main

import (
        "gin-sample/handler"

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

func main() {
        r := gin.Default()

        // ハンドラ生成(依存注入)
        userHandler := handler.NewUserHandler("MyApp")

        // ルーティング
        r.GET("/", userHandler.Hello)
        r.POST("/user", userHandler.CreateUser)

        r.Run()
}

動作確認は以下です

curl -X POST http://localhost:8080/user \      
  -H "Content-Type: application/json" \
  -d '{               
    "email": "taro@example.com",
    "age": 20
  }'
{"app":"MyApp","user":{"age":20,"email":"taro@example.com"}}
curl -X GET http://localhost:8080/
{"message":"Hello from MyApp"}

dispatcher パターンを使う

特定のクエリストリングで処理を分けたい場合などに使えます
REST などでは基本使いませんが / しか受けないような API でパラメータに応じて処理を分けたい場合などに使えるパターンです

特定のアクションに応じた関数を登録しておくことでアクションごとに指定の関数を実行できます

  • vim handler/dispatcher.go
package handler

import (
	"errors"
	"net/http"
)

// Actionの関数型
type ActionFunc func(params map[string]string, method string, r *http.Request) (any, error)

type Dispatcher struct {
	actions map[string]ActionFunc
}

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

// Action登録
func (d *Dispatcher) Register(name string, fn ActionFunc) {
	d.actions[name] = fn
}

// 実行
func (d *Dispatcher) Execute(name string, params map[string]string, method string, r *http.Request) (any, error) {
	if fn, ok := d.actions[name]; ok {
		return fn(params, method, r)
	}
	return nil, errors.New("unknown action: " + name)
}

ハンドラのメインとなる処理です
dispathcher に登録されたアクションごとの関数を実行する前やあとにする処理を記載することができます

  • handler/action_handler.go
package handler

import (
	"fmt"
	"net/http"

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

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

		// パラメータ統合
		if err := c.Request.ParseForm(); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
			return
		}

		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, c.Request)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{
				"error": err.Error(),
			})
			return
		}

		c.JSON(http.StatusOK, gin.H{
			"requestId": requestID,
			"action":    actionName,
			"result":    result,
		})
	}
}

メインではアクションとそれに紐づく関数の登録を行います
紐づく関数は今回 main.go に記載していますが別の箇所に記載しても OK です

  • vim main.go
package main

import (
        "fmt"
        "net/http"
        "strconv"

        "gin-sample/handler"

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

func main() {
        // Dispatcher作成
        dispatcher := handler.NewDispatcher()

        // Action登録
        dispatcher.Register("Hello", helloAction)
        dispatcher.Register("Sum", sumAction)

        // Gin
        r := gin.Default()

        // ★ここがポイント(引数付きハンドラ)
        r.GET("/", handler.HandleAction(dispatcher))
        r.POST("/", handler.HandleAction(dispatcher))

        r.Run()
}

// --- Action実装 ---

func helloAction(params map[string]string, method string, r *http.Request) (any, error) {
        name := params["name"]
        if name == "" {
                name = "guest"
        }
        return map[string]string{
                "message": "hello " + name,
        }, nil
}

func sumAction(params map[string]string, method string, r *http.Request) (any, error) {
        aStr := params["a"]
        bStr := params["b"]

        // 文字列 → 数値変換
        a, err := strconv.Atoi(aStr)
        if err != nil {
                return nil, fmt.Errorf("invalid parameter 'a'")
        }

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

        sum := a + b

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

動作確認は以下です

curl "http://localhost:8080/?Action=Hello&name=Taro"
{"action":"Hello","requestId":"dummy-request-id","result":{"message":"hello Taro"}}

curl -X POST http://localhost:8080 \
  -d "Action=Sum&a=10&b=20"
{"action":"Sum","requestId":"dummy-request-id","result":{"a":10,"b":20,"sum":30}}

最後に

gin で dispatcher パターンを試してみました
エラーハンドリングやレスポンスモデルの作成などまだありますがリクエストのハンドリングの部分はだいたい試せたかなと思います

参考サイト