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 するだけです

0 件のコメント:

コメントを投稿