Golangで超小規模のAPIを実装する際にフレームワークを使うべきかどうかを考える

Published: 2021年5月8日 by tomsato

概要

今回の記事について

仕事で新しくGolangを触ることになりました、社内で使うツールを作ることになったのですがそのツールのFE部分をNode.js(仮)、API部分をGolangで実装することになりました

筆者は主にPHP(Slim framework)、Scala(Play framework)を触ったことがあり、いやいやフレームワークはとりあえずでも使った方が拡張性もあるし良いのでは?とは思っている派だったのですが、今回は社内用のツール、しかもDBを叩いて表示を行うぐらいの簡単なWebアプリケーションを想定しているため、それぐらいだったらフレームワークは使わず標準パッケージ(net/http)でも良いのでは?という意見をチームメンバーから頂いたことで今回の記事を書くことになりました

したがって「超小規模のGolang REST APIを作成する時でもフレームワークを使うべきかどうか」という観点で調べていくことにします

考え方の道筋

まずはフレームワークを使うとしたらどのフレームワークを使うかを考え、フレームワークを使わずにAPIを実装した場合と、使った場合でどのような違いがあるかを見ていきたいと思います

あくまでも判断材料の1つにすることを目的とします

サンプルで利用するフレームワークについて

フレームワークの紹介・比較サイトは色々あり、それぞれでさまざまなフレームワークが紹介されていますが、今回は「Echo framework」を使おうと思います

選定した理由としては以下になります

  • フレームワークの紹介・比較サイトでほぼほぼEchoが登場している
  • 国内での事例が多く、何かあった時は参考にできることが多そう
  • 小規模アプリケーション向けということが多く記載されており、詰まるところはシンプルで使いやすいのではと判断した

Echoでどのようなことができるかは公式サイトを見てもらえればと思います

Echo - High performance, extensible, minimalist Go web framework

今回作成するサンプルアプリケーションについて

要件

  • 2つのAPIを用意する
    • 何かしらのAPIを叩いて取得した結果をログに出力する
    • DBを叩いて取得した結果をログに出力する
  • DBのパスワード等は環境変数から受け取れる
  • レスポンスはJson
  • GETパラメーターで受け取ったものを1部処理に利用する
  • エラー処理
    • GETパラメーターのバリデーションチェックを行う
    • 指定したHTTPメソッド以外のリクエストがきた場合はエラーとする

レスポンスの例

{
  code: 2000,
  message: "OK"
}

net/httpを利用してサンプルAPIを実装する

ソースコード

1ファイルで記述したのでGistに記載しました

https://gist.github.com/tomsato/64c22ebf60927fb17a5ec2bb5e595c13

実行方法

$ DB_USERNAME=username DB_PASSWORD=password go run sample-go-api.go

この状態で以下のページにアクセスすることでレスポンスを取得できる

  • http://localhost:8080/person?user=hoge
  • http://localhost:8080/request?key=huge

※ ローカルにMySQLを用意している必要があり

次から実装の説明をしていきます

エントリポイントの登録

http.HandleFunc でエントリポイントの登録をします

第2引数にハンドラー関数を記述することになりますが、handlerに共通の変数(今回は環境変数)を渡したかったので、3つの引数をもつfunctionを引数にとり、func(ResponseWriter, *Request)を返すようにしています

func main() {
	http.HandleFunc("/request", makeHandler(requestHandler))
	http.HandleFunc("/person", makeHandler(personHandler))
	http.ListenAndServe(":8080", nil)
}

func makeHandler(fn func(http.ResponseWriter, *http.Request, appEnvironment)) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		fn(w, r, getAppEnvironment())
	}
}

環境変数の取得、Json変換

環境変数を取得する関数を用意して、取得した結果を構造体に格納し、各ハンドラーに渡す

// 環境変数の定義
type appEnvironment struct {
	username string
	password string
}

func getAppEnvironment() appEnvironment {
	if len(os.Getenv("DB_USERNAME")) == 0 || len(os.Getenv("DB_PASSWORD")) == 0 {
		panic("appEnvironment not setting.")
	}

	return appEnvironment{
		username: os.Getenv("DB_USERNAME"),
		password: os.Getenv("DB_PASSWORD"),
	}
}

構造体からJsonレスポンス用に []byte と都度変換することになるが、変換時にエラーチェックすることになり、都度記述するのは冗長なので関数化

func createResponsJson(a *apiRequestResponse) []byte {
	json, err := json.Marshal(*a)
	if err != nil {
		panic("json can not marshal.")
	}
	return json
}

APIを叩いてレスポンスを返す

methodのバリデーションチェックや、GETパラメーターのバリデーションチェックもこの中で行う

http.Get にてHTTPリクエストを行い、取得した結果をログに出力している

func requestHandler(w http.ResponseWriter, r *http.Request, env appEnvironment) {
	// headerセット
	w.Header().Set("Content-Type", "application/json; charset=utf-8")

	// methodバリデーション
	if r.Method != http.MethodGet {
		w.WriteHeader(http.StatusMethodNotAllowed)
		w.Write(createResponsJson(&apiRequestResponse{Code: 2001, Message: "Method not allowed."}))
		return
	}

	// クエリパラメータ取得&バリデーションチェック
	paramKey := r.URL.Query().Get("key")
	if len(paramKey) == 0 {
		w.WriteHeader(http.StatusBadRequest)
		w.Write(createResponsJson(&apiRequestResponse{Code: 2002, Message: "key paramater not found."}))
		return
	}

	// HTTPリクエストを行い結果をログに出力
	resp, _ := http.Get("http://httpbin.org/get?key=" + paramKey)
	if resp == nil {
		w.WriteHeader(http.StatusInternalServerError)
		w.Write(createResponsJson(&apiRequestResponse{Code: 2003, Message: "HTTP Request failed."}))
		return
	}
	defer resp.Body.Close()
	byteArray, _ := ioutil.ReadAll(resp.Body)
	fmt.Printf("log: %s", byteArray)

	// レスポンスデータの生成
	w.Write(createResponsJson(&apiRequestResponse{Code: 2000, Message: "OK"}))
}

DBを叩いてレスポンスを返す

この中でもmethodのバリデーションチェックや、GETパラメーターのバリデーションチェックも行っている

またDBから値を取得しログに出力を行っている

func personHandler(w http.ResponseWriter, r *http.Request, env appEnvironment) {
	// headerセット
	w.Header().Set("Content-Type", "application/json; charset=utf-8")

	// methodバリデーション
	if r.Method != http.MethodGet {
		w.WriteHeader(http.StatusMethodNotAllowed)
		w.Write(createResponsJson(&apiRequestResponse{Code: 2001, Message: "Method not allowed."}))
		return
	}

	// クエリパラメータ取得&バリデーションチェック
	paramUser := r.URL.Query().Get("user")
	if len(paramUser) == 0 {
		w.WriteHeader(http.StatusBadRequest)
		w.Write(createResponsJson(&apiRequestResponse{Code: 2002, Message: "user paramater not found."}))
		return
	}

	// DBからデータを取得して結果をログに出力
	db, err := sql.Open("mysql", "root:"+env.password+"@/"+env.username)
	if err != nil {
		panic(err.Error())
	}
	defer db.Close()
	rows, err := db.Query("SELECT * FROM person")
	if err != nil {
		panic(err.Error())
	}
	defer rows.Close()
	for rows.Next() {
		var person person
		err := rows.Scan(&person.id, &person.name)
		if err != nil {
			panic(err.Error())
		}
		fmt.Println(person.id, person.name)
	}
	err = rows.Err()
	if err != nil {
		panic(err.Error())
	}

	// レスポンスデータの生成
	w.Write(createResponsJson(&apiRequestResponse{Code: 2000, Message: "OK"}))
}

net/httpについて思ったところ

課題に思ったところ

  • エラーハンドリングは都度実装する必要があり面倒
    • GET Methodだけ対応しているなどのHTTPメソッドのチェック処理の記述が都度必要
    • /hogeなど存在しないエントリポイントを叩いた場合には404 page not foundというレスポンスが返ってくるためJsonでレスポンスを返したい場合は実装が必要
  • レスポンス用の構造体から[]byteへの変換が面倒
  • 各ハンドラーに共通の変数(今回は環境変数)を渡し方法として1つ関数(makeHandler)を挟むことになるため面倒

良いと思ったところ

  • http.HandleFunc にてエントリポイントが登録できエントリポイント毎に関数分けできるのでわかりやすい
  • GETパラメーターの取得も簡単にできた

net/http だけで意外と簡単に色々できることがわかった

Echoフレームワークを利用してサンプルAPIを実装する

ソースコード

net/http 版のAPIとほぼ同等の機能を実装したサンプルAPIをEchoフレームワークを用いて実装していきます

1ファイルで記述したのでGistに記載しました

https://gist.github.com/tomsato/c946d7a428e6d2bb7596f240978ecd29

実行方法

$ DB_USERNAME=username DB_PASSWORD=password go run sample-go-echo-api.go

この状態で以下のページにアクセスすることでレスポンスを取得できる

  • http://localhost:8080/person?user=hoge
  • http://localhost:8080/request?key=huge

※ ローカルにMySQLを用意している必要があり

次から実装の説明をしていきます

エントリポイントの登録

echo.New() にてEchoのインスタンスを作成し、e.GETなどでエントリポイントを作成していく

net/http の時と同様にhandlerに共通の変数(環境変数)を渡すためにmakeHandlerを用意している

func main() {
	e := echo.New()
	e.GET("/request", makeHandler(requestHandler))
	e.GET("/person", makeHandler(personHandler))
	e.HTTPErrorHandler = customHTTPErrorHandler
	e.Logger.Fatal(e.Start(":8080"))
}

func makeHandler(fn func(c echo.Context, env appEnvironment) error) echo.HandlerFunc {
	return func(c echo.Context) error {
		return fn(c, getAppEnvironment())
	}
}

環境変数の取得、Json変換

環境変数の取得はnet/httpサンプルの時と同じ

Json変換は後ほど紹介するc.JSONに構造体をそのまま渡せるようになったので不要になった

APIを叩いてレスポンスを返す

レスポンスJson用の構造体をc.JSONに記載するだけ、なのでContent-Typeの指定はいらなくなる

func requestHandler(c echo.Context, env appEnvironment) error {
	// クエリパラメータ取得&バリデーションチェック
	paramKey := c.QueryParam("key")
	if len(paramKey) == 0 {
		return c.JSON(http.StatusOK, apiRequestResponse{Code: 2002, Message: "key paramater not found."})
	}

	// HTTPリクエストを行い結果をログに出力
	resp, _ := http.Get("http://httpbin.org/get?key=" + paramKey)
	if resp == nil {
		return c.JSON(http.StatusOK, apiRequestResponse{Code: 2003, Message: "HTTP Request failed."})
	}
	defer resp.Body.Close()
	byteArray, _ := ioutil.ReadAll(resp.Body)
	fmt.Printf("log: %s", byteArray)

	return c.JSON(http.StatusOK, apiRequestResponse{Code: 2000, Message: "OK"})
}

e.GET でエントリポイントを登録しているためMethodバリデーションは不要で、Echoで用意しているデフォルトのエラーハンドリングが適用される

従ってエラーレスポンスをカスタマイズしたい場合は以下のように記述する

func customHTTPErrorHandler(err error, c echo.Context) {
	code := http.StatusInternalServerError
	if he, ok := err.(*echo.HTTPError); ok {
		code = he.Code
	}

	if code == http.StatusMethodNotAllowed {
		// 405エラーの場合にレスポンスをカスタマイズする
		c.JSON(code, apiRequestResponse{Code: 2001, Message: "Method not allowed."})
	} else {
		// 405以外のエラーが発生した場合は以下の内容でレスポンスする
		c.JSON(code, apiRequestResponse{Code: 5000, Message: "Internal server error."})
	}
}

DBを叩いてレスポンスを返す

func personHandler(c echo.Context, env appEnvironment) error {
	// クエリパラメータ取得&バリデーションチェック
	paramUser := c.QueryParam("user")
	if len(paramUser) == 0 {
		return c.JSON(http.StatusOK, apiRequestResponse{Code: 2002, Message: "user paramater not found."})
	}

	// DBからデータを取得して結果をログに出力
	db, err := sql.Open("mysql", "root:"+env.password+"@/"+env.username)
	if err != nil {
		panic(err.Error())
	}
	defer db.Close()
	rows, err := db.Query("SELECT * FROM person")
	if err != nil {
		panic(err.Error())
	}
	defer rows.Close()
	for rows.Next() {
		var person person
		err := rows.Scan(&person.id, &person.name)
		if err != nil {
			panic(err.Error())
		}
		fmt.Println(person.id, person.name)
	}
	err = rows.Err()
	if err != nil {
		panic(err.Error())
	}

	return c.JSON(http.StatusOK, apiRequestResponse{Code: 2000, Message: "OK"})
}

Echoフレームワークについて思ったところ

課題に思ったところ

フレームワークを使うことに対して

  • カスタムエラーハンドリングはどうやるんだっけ、パラメータはどう取得するんだっけ、など疑問が生じた場合は都度調べる必要があるため、やはりフレームワークとしての学習コストは発生する
  • フレームワークに脆弱性が発生した場合は都度アップデートが必要なので保守コストが発生する

Echoに対して

  • 公式サイトが英語

良いと思ったところ

  • デフォルトでエラーハンドリングされ、カスタマイズするにも簡単
  • レスポンス用に変換する必要がなくなった
  • 導入も簡単で、使い方もシンプルでnet/httpと同じ感覚で利用できた
  • リクエストデータを構造体にバインドさせることができそう
  • 認証機能やテンプレート機能など色々な機能がありそうなので、今後機能拡張する場合や新規にWebページ表示用のアプリケーションを作成したい場合にもEchoを使えそう

まとめ

今回はAPIを叩いてレスポンスを返す、DBを叩いてレスポンスを返すなどシンプルなことしかしていないためフレームワークを使う優位性についてはそこまで感じることはできなかったように感じます

またGolangの標準パッケージ(net/http)が強力なため、今回のようなAPIについてはそれで十分に感じ、そもそもGolangの良さはシンプルさとも言われているためわざわざフレームワークを使う必要はないかもしれません

一方でやはり便利なところはあり、様々な機能があることから今後幅広くEchoを使っていく予定があるならば率先して利用するのはありなのかと思います

コメントを書く

※ Emailは公開されません

※ コメントは承認されると下記に表示されます

コメント一覧

最近の投稿

社内ツールなどの超小規模なAPIをGolangで実装する際にフレームワークを使うべきかを、実際にnet/httpを使った実装とフレームワークを使った実装を比較することでどれだけ優位性があるかを見ていきたいと思います。今回はフレームワークにはシンプルで使いやすそうなEchoを使うことにします。

vue-pdfを使ってNuxt.jsで作成しているアプリケーションに pdfスライドを表示させるサンプルを作成しました README.md通りに実装してもうまくいかないところがあったのでそのあたり含めてまとめます

Vue.js / Nuxt.jsにおけるログインの実装方法をまとめる Auth0やNuxt.jsのAuth Moduleとmiddlewareについて調べつつサンプルを作成することで理解を深める

コンポーネント設計について考える Atomic DesignやPresentational Component, Container Componentについてまとめつつ 自分だったらVue.js / Nuxt.jsでどういうコンポーネント設計にするかについてまとめます

Netlify Formsを使ってブログサイトにコメント機能を追加する方法を調べたので紹介 Netlify FormsはNetlifyに標準機能として用意されているフォーム機能 サーバレスなので別途コメント用にサーバを用意する必要がなくHTMLを埋め込むだけで準備できる

カテゴリ一覧

タグ一覧