LoginSignup
8
6

More than 5 years have passed since last update.

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

Posted at

はじめに

  • 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 の順で実行される様子
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 のデコードとエンコードを自前で実装していたので BindJSON に切り替える
    • 以下は JSON を受け取って JSON を返す一般的な API サーバーの例
// ハンドラー
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 を受け取って判定する必要があるくらい。
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時間でできた。
8
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6