GAE/Go で net/http から Echo への乗り換え 〜 ルーティング設定とウェブハンドラーとテストを移植する



# はじめに

* GAE/Go 環境で net/http, gorilla/mux, gorilla/context, negroni などの組み合わせでウェブアプリケーションを作っていたが Echo に乗り換えた。
* ルーティンブ設定やハンドラー、ハンドラーのテストを移植したのでその内容を記録する。

# ルーティング設定の移植

* `mux.NewRouter()` とかやっていたのが `echo.New()` になるだけ。
* 公式のドキュメントではファイルのトップレベルの変数宣言に値をセットする関数内で `http.Handle` に `*echo.Echo` を割り当てるとかやっているが、GAE の公式にあるような `func init()` 内で `http.Handle` とかに設定するので問題ない。
  * 確認してみたら var の値セットと init と main は `var -> init -> main` の順で実行される様子

```go
func init() {
	// echo のハンドラ作成
	e := echo.New()
	// 任意のルーティング設定
	e.GET("/hello", HelloHandler)
	// 全アクセスを echo で処理するように設定
	http.Handle("/", e)
}
```

# ハンドラーの移植

* ハンドラーの型は`func(http.ResponseWriter, *http.Request)` から `func(echo.Context) error` になる。
* エラーを返り値で返すようになるのでまあまあ修正する必要がある。
  * エラーのレスポンス返して return とかやっていたところをこつこつ修正。
  * 正常系でも `return nil` を入れていく。
* JSON のデコードとエンコードを自前で実装していたので `Bind` と `JSON` に切り替える
  * 以下は JSON を受け取って JSON を返す一般的な API サーバーの例

```go
// ハンドラー
func HelloHandler(c echo.Context) error {
	var r HelloRequest
	if err := c.Bind(&r); err != nil {
		return err
	}
	msg := fmt.Sprintf("hello %v", r.Name)
	return c.JSON(http.StatusOK, HelloResponse{msg})
}

// ハンドラーのリクエストデータのクラス
type HelloRequest struct {
	Name string
}

// ハンドラーのレスポンスデータのクラス
type HelloResponse struct {
	Message string
}
```

# ハンドラーのテストの移植

* ハンドラーのテストはほぼ元のまま。
* ハンドラーの実行前に `echo.Context` を作る必要があるのと、ハンドラーの実行結果の error を受け取って判定する必要があるくらい。

```go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	
	"google.golang.org/appengine/aetest"
	
	"github.com/labstack/echo"
)

// テスト
func TestHelloHandler(t *testing.T) {

  // aetest (App Engine Test) のインスタンス作成
  // テスト用の環境が用意される
  opt := aetest.Options{StronglyConsistentDatastore: true}
  instance, err := aetest.NewInstance(&opt)
  if err != nil {
    t.Fatalf("Failed to create aetest instance: %v", err)
  }
  defer instance.Close()

  // aetest インスタンスからリクエストの作成
  req, _ := instance.NewRequest("POST", "/hello", strings.NewReader(`{"name":"world"}`))
  req.Header.Set("Content-Type", "application/json")

  // レスポンスの作成
  res := httptest.NewRecorder()

  // リクエストとレスポンスから echo コンテキストの作成
  c := echo.New().NewContext(req, res)

  // ハンドラーの実行
  if err := HelloHandler(c); err != nil {
    t.Fatalf("error %v", err)
  }

  // レスポンスコードのテスト
  if res.Code != http.StatusOK {
    t.Fatalf("Non-expected status code%v:\n\tbody: %v", "200", res.Code)
  }

  // レスポンスデータのテスト
  var r HelloResponse
  if err := json.NewDecoder(res.Body).Decode(&r); err != nil {
    t.Fatal(err)
  }
  if r.Message == "" {
    t.Errorf("invalid result: %v", r)
  }
}
```

# 雑感

* 小さいライブラリを組み合わせてアプリケーションを作っていくのが Go っぽいかなーと思ってやってきたが、結局小さいフレームワークのようなものを自分で作っただけになったので、それなら最初からフレームワークを使ったほうがいいなーと思って Echo を使い出した。
  * たとえばミドルウェア一つ実装するにもライブラリの選定して名前空間考えて実装して、とか地味に手間。
  * フレームワークを作りたいモードだったら違ったと思うがアプリケーションを作りたい気持ちのほうが大きかったので無駄な手間に感じられた。
* Echo を選んだのは適度に小さくてコンテキストを持っているところが気に入ったから。
  * 他の有名どころだと Revel は大きすぎるし goji だと小さすぎる。gin は今見直すと良さげだけれどもなんか選ばなかった。
  * GAE にも context.Context を返す関数があり、Datastore などのサービスとの接続には利用するが、コンテキストに値をセットして別のところからゲットするような仕組みが自分の知る限りなかったので、結局 gorilla/context を不本意ながら使っていた。
  * なので最初からコンテキストを扱うフレームワークを使ったほうがましという気になった。
  * コンテキストと呼ぶオブジェクトがふたつあり若干気持ち悪いが、役割の違う別のものと割り切って使っていく。
* アプリケーションがまだ小さく特殊なことをやっていないこともあり思ったより移植に手間はかからなかった印象。20アクションくらいで2〜3時間でできた。