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

