GAE/Goでweb framework的なものを使う

GAE/Goでweb framework的なものを使う

August 17, 2020,
tags: golang gae chi


このエントリーをはてなブックマークに追加

Goで使うweb framworkを探していたらこんな意見を頂いた。

Goはnet/httpのパッケージは良くできているのでいらないかもしれない。
ただ、以下の点は気になった。

  • HTTPメソッドの指定
  • Middleware的な処理

HTTPメソッドの指定

http.HandleFunc だとHTTPのメソッド指定ができない。
これはちょっと個人的に使いづらさを感じてしまう。

Middleware的な処理

WebAPIを作る前提で考えているので、bodyParser的なものとか、X-Forwarded-ForなIPの対応とか、そういった処理は自前で用意したりライブラリ用意するのは嫌だ。

そんな感じchiを使ってみる

上記2つを満たすにはchiでいいかなと思い使ってみた。
前回の久しぶりのGAE/go 続きのコードで検証しているので同じように動作するものをchi で書いてみた。

// main.go

package main

import (
    "context"
    "log"
    "net/http"
    "os"

    "cloud.google.com/go/datastore"
)

var datastoreClient *datastore.Client

func main() {
    ctx := context.Background()
    var err error

    datastoreClient, err = datastore.NewClient(ctx, "aaaaa")
    if err != nil {
        log.Fatal(err)
    }

    port := os.Getenv("PORT")
    if port == "" {
        port = "8000"
        log.Printf("Defaulting to port %s", port)
    }

    log.Printf("Listening on port %s", port)

    if err := http.ListenAndServe(":"+port, Router()); err != nil {
        log.Fatal(err)
    }

}
// router.go
package main

import (
    "log"
    "os"
    "time"

    "github.com/go-chi/chi"
    "github.com/go-chi/chi/middleware"
    "github.com/go-chi/render"
)

func Router() chi.Router {
    r := chi.NewRouter()

    middleware.DefaultLogger = middleware.RequestLogger(
        &middleware.DefaultLogFormatter{
            Logger: newLogger(),
        },
    )

    r.Use(middleware.Logger)
    r.Use(middleware.RealIP)
    r.Use(middleware.Recoverer)

    r.Use(middleware.Timeout(60 * time.Second))

    r.Use(render.SetContentType(render.ContentTypeJSON))

    r.Get("/", Index)

    return r
}

func newLogger() *log.Logger {
    return log.New(os.Stdout, "chi-log: ", log.Lshortfile)
}
// handler.go

package main

import (
    "context"
    "fmt"
    "net/http"
    "time"

    "cloud.google.com/go/datastore"
    "github.com/go-chi/render"
)

func Index(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    // Get a list of the most recent visits.
    visits, err := queryVisits(ctx, 10)
    if err != nil {
        msg := fmt.Sprintf("Could not save visit: %v", err)
        http.Error(w, msg, http.StatusInternalServerError)
        return
    }

    // Record this visit.
    if err := recordVisit(ctx, time.Now(), r.RemoteAddr); err != nil {
        msg := fmt.Sprintf("Could not save visit: %v", err)
        http.Error(w, msg, http.StatusInternalServerError)
        return
    }

    render.Status(r, http.StatusOK)

    list := []render.Renderer{}
    for _, visit := range visits {
        response := &visitResponse{visit: visit}
        list = append(list, response)
    }

    render.RenderList(w, r, list)
}

type visit struct {
    Timestamp time.Time `json:"time"`
    UserIP    string    `json:"ip"`
}

type visitResponse struct {
    *visit
}

func (vr *visitResponse) Render(w http.ResponseWriter, r *http.Request) error {
    return nil
}

func recordVisit(ctx context.Context, now time.Time, userIP string) error {
    v := &visit{
        Timestamp: now,
        UserIP:    userIP,
    }

    k := datastore.IncompleteKey("Visit", nil)

    _, err := datastoreClient.Put(ctx, k, v)
    return err
}

func queryVisits(ctx context.Context, limit int64) ([]*visit, error) {
    q := datastore.NewQuery("Visit").Order("-Timestamp").Limit(10)

    visits := make([]*visit, 0)
    _, err := datastoreClient.GetAll(ctx, q, &visits)
    return visits, err
}

実装したコードをはこんな感じ。
Routing、Middleware周りに関しては、chiを使っている。
Jsonのレンダリングには renderを使用した。
renderはレスポンス毎に構造体の定義が必要なので、ちょっとどうかなとかも思うが、どうなんだろう?
この辺はいい感じする方法を知りたい。

最後に

chiは薄いフレームワークなので、ちょうどいいかなと。
というか、webフレームワークというよりルーティングフレームワークっていいってもいいかも。
とりあえずchi使ってこう。

comments powered by Disqus