Go言語で薄く作るAPI(go-chi/chi) #1 最低限構築
GoっぽくAPIを作る
Goには、様々なWAFが存在しますが、今ひとつデファクトスタンダードなものが未だにありません。その背景としてあるのは、標準パッケージで十分実装出来る。WAFは開発速度を上げてくれますが、そのWAFの開発そのものが製作者のモチベーションに左右されたり、内部実装が分かりにくくなるなど、そんなこと気にするくらいなら、小さいライブラリを組み合わせて、ある程度書いちゃうという人を結構見かけます。
でも、WAFを使わずにGoらしく実装する時ってどうやって書けばいい感じに出来るんだろうというのは、自分の中で結構気になるトピックでした。様々な実装例などから、自分なりにAPIを組んでみたので、シリーズものにして、実装を解説していきたいと思います。
go-chi/chi
今回使うのは、go-chi/chiというnet/httpのインターフェースに準拠したルーティングライブラリを使います。 採用理由としては、高速であること、ルーティングが見やすいなどと言った点です。
Goには、他にも薄いライブラリ系で有名なものがあるので紹介しておきます。
この記事で書くこと
実行
$ go get github.com/pei0804/go-chi-api-example $ cd $GOPATH/src/github.com/pei0804/go-chi-api-example/01 $ make run go build -o build . && ./build 2017/11/26 00:05:04 Starting app $ make curl-members-id id=01 curl -H 'Auth:admin' http://localhost:8080/api/members/01 { "id": 1, "name": "name_1" }
コンストラクタ
// Server Server type Server struct { router *chi.Mux } // New Server構造体のコンストラクタ func New() *Server { return &Server{ router: chi.NewRouter(), } }
アプリケーション内で、保持したいものや、handlerに渡したいものなどをServerという構造体に作成して、簡単なコンストラクタを作成しています。これらをの構造体を作成せずに、アプリケーションを作成していくと、後でテストコードを書く際に辛いことになるので、基本的にはこういうものは作成した方がいいです。
ルーティング
// Router ルーティング設定 func (s *Server) Router() { h := NewHandler() s.router.Route("/api", func(api chi.Router) { api.Use(Auth("db connection")) // /api/*で必ず通るミドルウェア api.Route("/members", func(members chi.Router) { // /api/members/* でグループ化 members.Get("/{id}", h.Show) // /api/members/1 などで受け取るハンドラ members.Get("/", h.List) // /api/members で受け取るハンドラ }) }) // Authする何かのエンドポイントという想定 s.router.Route("/api/auth", func(auth chi.Router) { auth.Get("/login", h.Login) }) }
ルーティング情報はchiを使うと自動生成することが出来たので、貼っておきます。
/api/*/members/*
- RequestID
- Logger
- Recoverer
- CloseNotify
- Timeout.func1
- RequestLogger.func1
- /api/*
- main.Auth.func1
- /members/*
- /
/api/*/members/*/{id}
- RequestID
- Logger
- Recoverer
- CloseNotify
- Timeout.func1
- RequestLogger.func1
- /api/*
- main.Auth.func1
- /members/*
- /{id}
/api/auth/*/login
- RequestID
- Logger
- Recoverer
- CloseNotify
- Timeout.func1
- RequestLogger.func1
- /api/auth/*
- /login
ルーティングの考えかたやGET
POST
などのメソッドについて分からない場合は、過去にAPI設計についてのワークショップ開催した時の資料あるので、そちらを見てください。
www.slideshare.net
ミドルウェア
ルーティングで登場したミドルウェアについて軽く触れます。
サーバーサイドの開発を何度かしたことがある人なら知っているとは思いますが、認証を通したい、処理を通る前にRequestID
のようなものを発行しておきたいなど、APIをラップする機能の総称です。
引用元:Laravel 5.0 - Middleware (Filter-style) | MattStauffer.com
よくある具体例として、panicのrecoverが分かりやすいです。
// Recoverer is a middleware that recovers from panics, logs the panic (and a // backtrace), and returns a HTTP 500 (Internal Server Error) status if // possible. Recoverer prints a request ID if one is provided. // // Alternatively, look at https://github.com/pressly/lg middleware pkgs. func Recoverer(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { defer func() { // panicがあればrvrに入っているエラー内容をかき出す // 注意点として遅延関数内で実行する必要がある if rvr := recover(); rvr != nil { //https://golang.org/pkg/builtin/#recover logEntry := GetLogEntry(r) if logEntry != nil { logEntry.Panic(rvr, debug.Stack()) } else { fmt.Fprintf(os.Stderr, "Panic: %+v\n", rvr) debug.PrintStack() } // 500エラーを返す http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } }() // ハンドラを実行する next.ServeHTTP(w, r) } // 上記の処理をhandlerFuncに渡す return http.HandlerFunc(fn) }
上記のような処理を本来は全てのエンドポイントに書く必要がありますが、それは大変過ぎますし、また同じようなコードを全箇所に書くのはナンセンスです。そういった問題を解決してくれたのがミドルウェアです。 ちなみに、使い方は以下のような感じです。
// Middleware ミドルウェア func (s *Server) Middleware() { s.router.Use(middleware.RequestID) s.router.Use(middleware.Logger) s.router.Use(middleware.Recoverer) s.router.Use(middleware.CloseNotify) s.router.Use(middleware.Timeout(time.Second * 60)) } // ~~~~ s.router.Route("/api", func(api chi.Router) { api.Use(Auth("db connection")) // /api/*で必ず通るミドルウェア api.Route("/members", func(members chi.Router) { // ~~~ }) }) // ~~~ })
今回作成したものは、データベースのコネクションを渡して、何かデータベースと通信して〜〜ということを想定したようなものを作っています。ミドルウェアに何か引数を渡したい時は、以下のような感じで出来ます。
func Auth(db string) (fn func(http.Handler) http.Handler) { // 引数名を指定してるのでreturnのみでおk fn = func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Auth") // Authというヘッダの値を取得する if token != "admin" { // adminという文字列か見る // エラーレスポンスを返す // この関数については後で書きます respondError(w, http.StatusUnauthorized, fmt.Errorf("利用権限がありません")) return } // 何も無ければ次のハンドラを実行する h.ServeHTTP(w, r) }) } return }
ハンドラ
ここまで何度も登場してきたハンドラというワードですが、GoのAPIではこのハンドラによって様々な処理を可能にしています。分かりやすい記事があったのでリンクを貼っておきます。
// A Handler responds to an HTTP request. // // ServeHTTP should write reply headers and data to the ResponseWriter // and then return. Returning signals that the request is finished; it // is not valid to use the ResponseWriter or read from the // Request.Body after or concurrently with the completion of the // ServeHTTP call. // // Depending on the HTTP client software, HTTP protocol version, and // any intermediaries between the client and the Go server, it may not // be possible to read from the Request.Body after writing to the // ResponseWriter. Cautious handlers should read the Request.Body // first, and then reply. // // Except for reading the body, handlers should not modify the // provided Request. // // If ServeHTTP panics, the server (the caller of ServeHTTP) assumes // that the effect of the panic was isolated to the active request. // It recovers the panic, logs a stack trace to the server error log, // and hangs up the connection. type Handler interface { ServeHTTP(ResponseWriter, *Request) }
ServeHTTPというのが重要で、これに最終的に合わせてキャストすると、http.Handlerが使えます。便利過ぎる! なので、今回は以下のような記述になっています。
http.ListenAndServe(fmt.Sprint(":", *port), s.router)
今回はハンドラをカスタムをしていませんが、ハンドラに渡したい、返してほしいものを変えることが出来ます。以下に分かりやすいサンプルがあったので貼っておきます。 次回の記事では、カスタムハンドラの一例を示す予定です。
type Handler func(w http.ResponseWriter, r *http.Request) error // 共通の処理などを受け口を作るとロギングにも良い // WAFのここらへんの設計は結構面白い func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err := h(w, r); err != nil { // handle returned error here. w.WriteHeader(503) w.Write([]byte("bad")) } } func main() { r := chi.NewRouter() r.Method("GET", "/", Handler(customHandler)) http.ListenAndServe(":3333", r) } func customHandler(w http.ResponseWriter, r *http.Request) error { q := r.URL.Query().Get("err") if q != "" { return errors.New(q) } w.Write([]byte("foo")) return nil }
コマンドラインのフラグからの動作変更
ちょっとした動作変更などは、フラグを使うと便利です。ケースによっては設定ファイルでやるべきこともありますが、簡単なアプリケーションなら、フラグパッケージでサクッとやると便利です。絶対にやるべきでないのは、変える可能生の高いものをハードコーディングしてしまうことです。それをするくらいなら、flagパッケージを使ってください。
var ( port = flag.String("port", "8080", "addr to bind") // -port=9000 などにするとポート変更が出来る env = flag.String("env", "develop", "実行環境 (production, staging, develop)") // 実行環境変えたり gendoc = flag.Bool("gendoc", true, "ドキュメント自動生成") ) flag.Parse() <- これを必ず書く
まとめ
もう少し具体例にしたいので、段々アプリケーションっぽいものにしていきます。