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は、シンプルなインタフェースを組みあせて実装されることが非常に多いので、そのあたりが見える部分でもあったかと思います。