golangは、スクリプト言語のような雰囲気でコードが記載でき、それなりの処理速度も出るので気に入っています。APIサーバの開発をする際によく利用しているので、よく悩まむのがエラーについてです。REST API形式で実装した際に正常応答時には、jsonを返却し、異常応答時には、エラー応答だけというパターンもあるのかもしれませんが、比較的大きなプロジェクトになってくると一般的なHTTPのエラーコードだけではなく、サービス独自のエラーコードを返却したいことが多くあります。今回は、echoフレームワークを利用して、サービス独自のエラーコードを返却する方法についてまとめたいと思います。

echoフレームワークとは

golangでは、標準ライブラリの”net/http”というライブラリがあり、小さなものであれば、十分かと思うのですが、中規模〜大規模で開発する場合には、ある程度のフレームワークがあると便利です。色々なフレームワークがあるのですが、golang自体がシンプルさを売りしているので、なるべくシンプルなフレームワークが良いなと思い、echoフレームワークを利用しています。

REST APIのエラー応答について

REST APIの説明はしませんが、REST APIで実装する際の正常応答は、JSON形式で返却することが多いです。ネットワーク異常とによりサーバサイドが応答を返すことができない場合は除くとして、異常応答時にもJSON形式で返せるほうがよいです。また、API設計をする上で、HTTPのエラーとは別に、サービスとしてのエラーコードを渡すことで、利用者側がエラー内容を把握出来るようになっていると利用者側にとっても魅力的です。

golangのerrorとは

golangにおけるエラーは、下記に示すError()を実装すれば満たせます。

// cf. https://golang.org/pkg/builtin/#error
type error interface {
  Error() string
}

echoフレームワークのエラーハンドラコードについて

echo構造体の定義は、下記のようになっています。HTTPErrorHandlerというハンドラがありますね。

type (
	// Echo is the top-level framework instance.
	Echo struct {
		common
		StdLogger        *stdLog.Logger
		colorer          *color.Color
		premiddleware    []MiddlewareFunc
		middleware       []MiddlewareFunc
		maxParam         *int
		router           *Router
		routers          map[string]*Router
		notFoundHandler  HandlerFunc
		pool             sync.Pool
		Server           *http.Server
		TLSServer        *http.Server
		Listener         net.Listener
		TLSListener      net.Listener
		AutoTLSManager   autocert.Manager
		DisableHTTP2     bool
		Debug            bool
		HideBanner       bool
		HidePort         bool
		HTTPErrorHandler HTTPErrorHandler
		Binder           Binder
		Validator        Validator
		Renderer         Renderer
		Logger           Logger
		IPExtractor      IPExtractor
	}

実際にecho.New()すると下記のコードが呼ばれます。注目すべき点は、DefaultHTTPErrorHandlerという処理が設定されている点です。

// New creates an instance of Echo.
func New() (e *Echo) {
	e = &Echo{
		Server:    new(http.Server),
		TLSServer: new(http.Server),
		AutoTLSManager: autocert.Manager{
			Prompt: autocert.AcceptTOS,
		},
		Logger:   log.New("echo"),
		colorer:  color.New(),
		maxParam: new(int),
	}
	e.Server.Handler = e
	e.TLSServer.Handler = e
	e.HTTPErrorHandler = e.DefaultHTTPErrorHandler
	e.Binder = &DefaultBinder{}
	e.Logger.SetLevel(log.ERROR)
	e.StdLogger = stdLog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0)
	e.pool.New = func() interface{} {
		return e.NewContext(nil, nil)
	}
	e.router = NewRouter(e)
	e.routers = map[string]*Router{}
	return
}

DefaultHTTPErrorHandlerの定義を見てみます。

// DefaultHTTPErrorHandler is the default HTTP error handler. It sends a JSON response
// with status code.
func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) {
	he, ok := err.(*HTTPError)
	if ok {
		if he.Internal != nil {
			if herr, ok := he.Internal.(*HTTPError); ok {
				he = herr
			}
		}
	} else {
		he = &HTTPError{
			Code:    http.StatusInternalServerError,
			Message: http.StatusText(http.StatusInternalServerError),
		}
	}

	// Issue #1426
	code := he.Code
	message := he.Message
	if e.Debug {
		message = err.Error()
	} else if m, ok := message.(string); ok {
		message = Map{"message": m}
	}

	// Send response
	if !c.Response().Committed {
		if c.Request().Method == http.MethodHead { // Issue #608
			err = c.NoContent(he.Code)
		} else {
			err = c.JSON(code, message)
		}
		if err != nil {
			e.Logger.Error(err)
		}
	}
}

上のコードを見ると(err error, c Context)を受け取ることが出来れば問題なさです。

カスタムエラーを扱えるようにする

エラー型を定義する

errorインターフェスを満たせばOKなので、今回は、下記のようにします。

type httpError struct {
        code    int
        Key     string `json:"error"`
        Message string `json:"message"`
}

// errorインタフェースをError()を実装
func (e *httpError) Error() string {
        return e.Key + ": " + e.Message
}

エラー応答ハンドラを作成する

func httpErrorHandler(err error, c echo.Context) {
        var (
                code = http.StatusInternalServerError
                key  = "ServerError"
                msg  string
        )

        if he, ok := err.(*httpError); ok {
                code = he.code
                key = he.Key
                msg = he.Message
        } else {
                msg = http.StatusText(code)
        }

        if !c.Response().Committed {
                if c.Request().Method == echo.HEAD {
                        err := c.NoContent(code)
                        if err != nil {
                                c.Logger().Error(err)
                        }
                } else {
                        err := c.JSON(code, newHTTPError(code, key, msg))
                        if err != nil {
                                c.Logger().Error(err)
                        }
                }
        }
}

出来上がったコード

全体としては、下記のコードのようになりました。

まとめ

今回は、echoフレームワークの内部をみながらカスタムエラーハンドラを実装してみました。golangは、シンプルなインタフェースを組みあせて実装されることが非常に多いので、そのあたりが見える部分でもあったかと思います。