Go言語で API サーバーを開発する (original) (raw)

czMjYXJ0aWNsZSMzNTA3MiMxNDE3NjkjMzUwNzJfbnhLR1VnYkxHdS5wbmc

こんにちは!白ヤギの開発者、森本です。

白ヤギではいま API サーバーを Go 言語で開発しています。

皆さんも Go の話題をよく見聞きするようになっていると思います。今回は白ヤギの業務でどんな風に Go を使って開発しているかの一端を紹介します。

余談ですが、先日、大学の先生とお話ししたときにこんな話を伺いました。その先生は学生にプログラミングを教えているそうですが、何割かの学生は及第点に届かないそうです。しかし、そういった学生がプログラミングの素養がないかというとそういう訳ではなく、プログラミングを学ぶ上でその学生にとって何が理解を促すのかが違うだけなのだと仰っていました。教える側として全ての学生が習得できるプログラミング教育というのを見つけられていないのが悔しいといった話をされていました。

何かを学ぶというのを一般論では語るのは難しいということかもしれません。そのため、私はこういうやり方で学びましたといった記事がたくさん増えることは、初学者にとって有益だと思います。Go を学習中の方はどんどんブログを書くと良いと思います。

私も Go 歴2ヶ月程度の若輩ですが、こんな感じで Go を学習していますというのも少し綴ろうと思います。

開発環境

Go に限らず、静的型付け言語で開発するときは開発環境を調べて最初に作り込むのが良いと思います。静的型付け言語と聞くと、それだけで開発が遅くなるといった先入観をもつ人もいます。型やコンパイルが必要だからといって開発の生産性が大きく落ちるとは一概には言えません。

唐突ですが、Go の変数宣言は var を使う方法と := を使う方法の2種類があります。Type inference からサンプルコードを引用します。

var i int j := i // j is an int

i := 42 // int f := 3.142 // float64 g := 0.867 + 0.5i // complex128

同じことをする方法が複数あるのは使い分けがあるからだと推測されます。私が調べた限りでは以下のような使い分けになっているようです。

詳細は 変数の宣言 を参照してください。

さて Go の変数宣言は右辺から型推論を行ってくれます。新規にコードを書く場合、試行錯誤しながらコードを書くため、一度書いたコードを後で書き換えるという作業は頻繁に発生します。一通り書いた後になってもっと良い方法を思いついたとか、コードが汚いからリファクタリングしようとか、定義していた型を変えたいというのはよくあることなので、型推論は新規開発における生産性を考えたときに重要な機能です (Go の型推論は変数宣言のときのみの限定的なものだという意見もあったりします) 。

開発の生産性を考える上で、型推論もそうですし、静的解析の恩恵により、コードをコンパイルしなくても、コードの型チェックやエラーチェックを IDE がサポートしてくれます。例えば、vim-go では、関数の情報を取得したり、定義元にジャンプしたり、コード補完してくれたりといった一連の機能が幅広く提供されています。

スクリプト言語のように書いたコードをすぐ実行できるというのもプログラミングの楽しさですが、実行しなくても動くという安心感をもってコードを書き進められるというのもまた別のプログラミングの楽しさです。

Go は開発を支援するツール群が標準でサポートされていることでも定評があり、vim-go もそれらのツール群を内部的に使っています。私は先に vim-go を見つけて使い始めましたが、その後に以下の記事も拝見しました。こちらの記事によると、IDE 環境を構築するツールの管理ポリシーが異なるようです。

使っているライブラリ

いま開発中のプロジェクトで使っている主要なライブラリです。

補足すると、単体テストに使う DB に go-sqlite3 も使っています。

Go はまだまだ若い開発コミュニティなので、このフレームワークやライブラリが定番といったものは少ないです。群雄割拠といえば良いのか、そういう状態でライブラリをどのように選定すればいいでしょうか。私は awesome-go にまとめられているものの中からいくつか選択し、その機能や人気度 (github のスター)、メンテナンスが継続されているかなどを考慮して選択しています。

ブログなどを読んでそこで使っているライブラリももちろん参考にしますが、そういったものもほぼ確実に awesome-go に載っています。awesome-go でいくつかライブラリを絞ってググって評判などを検索するといった方法もいいでしょう。

個々のライブラリを詳細には説明しませんが、使ってみての個人的な所感をいくつか書きます。

ロギングライブラリ

Go 標準のロギングライブラリとして log ライブラリがあります。しかし、log ライブラリには leveled logging (ログレベルを分けてログ出力する機能) がありません。当初は log ライブラリを使っていましたが、デバッグ用途で一時的にログレベルを変えてトレースしたいといったことはシンプルな API サーバーでもあるため、leveled logging を提供しているライブラリの中から logrus を選択しました。

logrus は leveled logging に加え、以下のような structured logging というフィールド名と一緒に値をログ出力する仕組みも提供しています。

log.WithFields(log.Fields{ "event": event, "topic": topic, "key": key, }).Fatal("Failed to send event")

time="2015-03-26T01:27:38-04:00" level=fatal msg="Failed to send event" event=... topic=... key=...

ほとんど気に入って使っているのですが、1つだけ不満なところがあります。logrus にはログ出力時にソースファイル名や行数を出力する機能がありません。logrus の github 上では issue や pull request で提案されたりもしていますが、なかなか解決されないようです。

ちなみに標準の log ライブラリでは以下のようにフラグをセットするとログ出力されます。

log.SetFlags(log.LstdFlags | log.Lshortfile) // filename only: d.go:23

// or

log.SetFlags(log.LstdFlags | log.Llongfile) // full path: /a/b/c/d.go:23

Web フレームワークと Context オブジェクト

シンプルな API サーバーだったのでなるべく軽量なフレームワークが良いだろうという理由から negroni を採用しました。negroni のドキュメントには、これはフレームワークではなく、Go 標準の net/http パッケージを直接扱うようなライブラリだと説明されています。

negroni の作者は Martini というフレームワークの作者でもあり、Martini が Go っぽくないとか、魔法が多過ぎといった声に反応して作ったものが negroni だそうです。

説明するよりコードをみた方が早いので README からサンプルコードを引用します。

package main

import ( "github.com/codegangsta/negroni" "net/http" "fmt" )

func main() { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "Welcome to the home page!") })

n := negroni.Classic() n.UseHandler(mux) n.Run(":3000") }

たったこれだけです。

いくつか API を実装していくうちに全てまたは一部の API に対して横断的な処理を実装したくなります。例えば、キャッシュ周りの処理とか、レスポンスを json に変換するといった処理などです。

試しに以下のような Python のデコレーターのようなものも実装してみましたが、これは汚いのでやめました。

func acceptPostOnly(f http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { httperror.MethodNotAllowed(w) return } f(w, r) } }

var myAPI = acceptPostOnly(func(res http.ResponseWriter, req *http.Request) { ... }

そういった用途には negroni のミドルウェアを実装するのが良さそうです。

func MyMiddleware(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { // do some stuff before next(rw, r) // do some stuff after }

n := negroni.New() n.Use(negroni.HandlerFunc(MyMiddleware))

そしてミドルウェアを実装しているうちに、リクエストグローバルな変数を扱う context オブジェクトが欲しくなりました。

以前、雑誌の良い設計に関する特集記事で、開発とは抽象化やレイヤーを積み重ねていって最後に拡張言語層 (DSL) が残る、開発を進めていくと自然とフレームワークが出来上がっていくものだといった内容が書かれているのを読んだことがあります。

あとになって見返すと、それなら最初から Context オブジェクトをサポートしているフレームワークを採用しても良かったなと、いまは思います。それでも Go のコードはシンプルに書けるので negroni の見通しの良さを気に入って、独自フレームワークを実装していくというのも好みの問題かもしれません。

テスト

現プロジェクトでは、リリース前に単体、結合、負荷テストが一通り全て揃っています。

単体テスト

Go 標準の testing を使っています。テストコードを書き始める前に確認しておくこととして Go には assertがありません。モダンな言語で assert がないのは珍しい方だと思います。

FAQ によると、assert を使うことがプログラマーにとって、適切なエラーハンドリングとエラーレポートを書くということへの思考停止を招いているのではないかというお話です。問いかけとしてはおもしろいですし、エラー制御を正しく設計するということの重要性にも賛成です。

とはいえ、私の結論から述べると、単体テストを書く上で assert がないのはしんどいです。逆に言えば、単体テストのような、ほぼ定型的なエラーレポートをたくさん書くような状況において assert は便利だというのを再認識しました。

一般論として独自 assert 関数を定義するのは保守 (学習) コストが上がるため、良いプラクティスとはみなされていません。Go の assert を提供しないことの背景はおもしろいですが、現実には assert がない → でも独自 assert 関数は作りたくない → なら DRY の原則に反するけど、自分でエラーレポート書く!みたいな苦行になってしまいました。

そういった経緯から、私はサードパーティが提供している assert ライブラリを使えば良いように考えています。他に良い方法があったら教えてください。

テーブル駆動テスト

TableDrivenTests では、テーブル駆動テストと表現されていますが、一般的にはデータ駆動テストと呼ばれているものです。この wiki では、fmt パッケージのテストコードからサンプルとして紹介されています。

var flagtests = []struct { in string out string }{ {"%a", "[%a]"}, {"%-a", "[%-a]"}, {"%+a", "[%+a]"}, // ... }

func TestFlagParser(t *testing.T) { var flagprinter flagPrinter for _, tt := range flagtests { s := Sprintf(tt.in, &flagprinter) if s != tt.out { t.Errorf("Sprintf(%q, &flagprinter) => %q, want %q", tt.in, s, tt.out) } } }

テストコードを書けばすぐに気付きますが、注意事項として T.ErrorT.Fatal の違いを説明します。T.Error はエラーレポートを出力して実行を継続します。そのため、このテストは複数あるテストデータのうちどれかのデータでテストが失敗したとしても継続して他のデータのテストも実行されます。一方 T.Fatal を使うと、エラーレポートを出力してそこでテストを終了してしまいます。そのため、テーブル駆動テストで T.Error を使わないと、データ駆動テストの有効性を生かしきれません。

テーブル駆動テスト (データ駆動テスト) の概念は、Go に限らずテストを書く上での便利な手法なのでいろいろなテストケースに応用すると良いでしょう。

http ハンドラーのテスト

{“message”: “pong”} といった json データを返す Ping API があるとします。その Ping API のテストコードを書いてみます。

package handlers_test

import ( "net/http" "net/http/httptest" "net/url" "testing"

"myapp/handlers"
"myapp/handlers/handlerstest"

)

func TestPingAPI(t *testing.T) { res := httptest.NewRecorder()

req := new(http.Request)
req.URL = &url.URL{Path: "/api/ping"}
req.Method = "GET"
req.Header = make(http.Header)
req.Header.Set("Content-Type", "application/json")

handlers.PingAPI(res, req)

data, err := handlerstest.GetJsonData(res.Body)
if err != nil {
    t.Fatalf("Getting json data error %v", err)
}

if data["message"] != "pong" {
    t.Fatalf(`Wrong message; expected "pong", got %q`, data["Message"])
}

}

単体テストを書き始めるときのお手本として Go の標準ライブラリのテストをみてみましょう。

自分が実装したい内容のテストが、ある標準ライブラリでも同種のテストをしているはずだと予想できたら、そのテストを探して読んでみるところから始めるのがお勧めです。

例えば、http ハンドラーのテストを書く場合、net/http のテストを読んでみると、レスポンスオブジェクトをどうやって生成し、リクエストオブジェクトはどんな風に扱えば良いかといったものが分かります。

res := httptest.NewRecorder()

req := new(http.Request) req.URL = &url.URL{Path: "/api/ping"} req.Method = "GET" req.Header = make(http.Header) req.Header.Set("Content-Type", "application/json")

ここでは net/http/httptest というテスト向けのユーティリティパッケージがあることにも気付きます。もちろん自分たちのアプリから httptest パッケージもそのまま使えます。

さらにテスト用のユーティリティパッケージもアプリ内に作ってしまっても良いんだといったことも伺えます。なら自分たちのアプリ特有のものは、ここでは handlerstest パッケージにまとめましょうとなります。

data, err := handlerstest.GetJsonData(res.Body)

Go ではパッケージとテストが同じディレクトリに存在しているのでテストコードを探すのがとても簡単です。実装コードとテストコードの置き場所が近いというのはテストを実装するときにお手本にしやすいというのに気付きました。

結合テスト

pytest を使っています。

pytest を知らない方のために、pytest は Python コミュニティでよく使われているテストランナー・テストライブラリの1つです。簡単な単体テストから複雑な機能テストまで幅広く利用できます。ちょっと癖はありますが、テスト関数への DI (Dependency Injection) やモック (monkeypatch) の仕組みも標準でサポートしていて強力です。pytest プラグイン も豊富なので用途に応じて組み合わせられます。

自分でプラグインを作るのも簡単です。例えば、API サーバーの結合テストを実装していて、テストが失敗したときにそのリクエストを送る cURL コマンドをテストレポートとして出力できれば、API サーバーがどのような言語で開発されていても便利かなと思って pytest-curl-report プラグインを作りました。以下のようなテストレポートを生成します。

============================= test session starts ============================== platform darwin -- Python 2.7.9 -- py-1.4.27 -- pytest-2.6.4 plugins: curl-report, httpbin, cache, capturelog, cov, flakes, pep8 collected 1 items

test.py F

=================================== FAILURES =================================== ______________________________ test_requests_get _______________________________

def test_requests_get():
    r = requests.get('http://httpbin.org/get')
  assert False

E assert False

test.py:7: AssertionError -------------------------- How to reproduce with curl -------------------------- curl -X GET -H "Connection: keep-alive" -H "Accept-Encoding: gzip, deflate" -H "Accept: /" -H "User-Agent: python-requests/2.7.0 CPython/2.7.9 Darwin/14.3.0" "http://httpbin.org/get"

また pytest (2.2.4) ドキュメント に翻訳されたドキュメントもありますが、このドキュメントはもう古いので pytest の全体像をざっくり目を通すには構いませんが、詳細を知りたい方は英語の最新ドキュメントを参照してください。

負荷テスト

locust を使っています。

私は負荷テストを書いたことがなくて、書く前はかなり身構えたものがあったのですが、locust を使う分には何ら気にする必要はありませんでした。私が locust について調べた中では以下の記事が他の負荷テストツールとの比較も書かれていて参考になりました。

locust は普通に Python のスクリプトを書く感覚で負荷テストのシナリオを実装できます。良く言えば Python、悪く言えば Python です。負荷テストを書くために Python を書かないといけないのが Python プログラマー以外にはどうみえるのか気になるところです。私からみると、あれこれ DSL 覚えるよりも Python 覚える方が簡単で応用範囲が広いのではないかと思います。

負荷テストを実施していて1つはまったのは、クライアント側のファイルディスクリプタの最大数を上げておかないと、以下のエラーでテストが失敗します。

CatchResponseError('HTTP status code error: 0',)

locust のドキュメント にもそう書かれているのですが、サーバー側の問題だと考えてしばらくサーバー設定を見直したりしてはまりました。

Python と Go

白ヤギでは Python もよく使っています。私も Python が最も慣れ親しんだ言語です。

昨年の Go Conference で聞いた Go is more Pythonic than Python. (Why my Go program is slow?) という言葉が私の中ではとても印象に残っています。Go and the Zen of Python から触発された言葉のようです。私自身 Go を書き始めてまだ浅いものの、Go のコードを読み書きして感じる心地良さは Python のそれに似ていると思うことがよくあります。そのため、Python プログラマーが Go に移行しやすいのも理解できます。

Go があれば Python はいらないのではないか説をたまに見聞きします。現時点での私の答えは Go も Python も両方使うでしょうといったものです。

現プロジェクトでも結合テストや負荷テストは Python で書きました。それらも Go で書くかといったら躊躇してしまいます。それは Python で書いた方がずっと楽だろうと思うからです。テストというのは、テストデータを扱ったり、外部システムとの連携があったり、アプリの仕様変更に柔軟に対応したり、そういった煩雑な要件がある割にしっかりきっちり作る類のものでもありません。

あれ?冒頭では Go でも生産性は落ちないと言っていたのに矛盾すると思う人もいるかもしれません。それは「しっかりきっちり作る」ものを Go で開発しても Python で開発しても、双方のメリット・デメリットを考慮して全体的にみるとそんなに差はないということであって、そうでないものはまだ Python に分があると私は思います。

もちろん Python には何年にも渡って開発されてきた実用的なライブラリが豊富というのも大きな要因ではありますが、それ以上に泥臭いことを手軽にやるのはスクリプト言語 (動的型付け言語) の方がまだ簡単だというのが実情でしょう。

そんなことを考えていたときにたまたま Things from Python I’d miss in Go という記事を読みました。約1年前の記事なので現在の状況と少し違っているかもしれません。冒頭からいくつかの文章を引用します。

Rob Pike 氏は言った。「C++ プログラマーが Go に移行するだろうと思っていたけれど、どうやら Python プログラマーがそうなるようだ。」 (中略) Go にあって Python にないものは何だろう?パフォーマンスと静的型付け。もしそれが必要なら、Go が現れる何年も前に私は Python から Java へ移行していただろう。いや、たぶん違う。だって Java は Go よりも随分と冗長だから、とりわけ Java 8 より前はそう。とはいえ、私は確かに Java を検討していたし、おそらく Go は今後もさらに成功を収めるだろうけれども、、、。特徴的なのを除いて、私が必要としていて Go に不足しているもの (その多くは Java も同様だ) のリストをあげてみます。

この記事の中では以下のものが Python にあって Go にはないとあげられています (内容は一部抜粋) 。

この著者の結論として、Go は並行サーバーに向いていると締め括っています。Go は C や C++ よりプログラミングしたいと思うものの、Python よりもそうかと言うと、一般論としては No であり、この記事の著者はそう思わないそうです。

私の所感は、Python は成熟した言語であり、大きな変化に対して保守的にならざるを得ないでしょう。一方 Go はモダンな言語で開発コミュニティが成長中で応用分野も模索中、なにか新しい可能性があるんじゃないかというわくわく感があります。Go Conference 2015 summer で 750 人以上の人が参加登録していたりすることからもその人気ぶりが伺えます。

Python プログラマーは Go に馴染みやすいので適材適所で両方使っていくと良いように思います。

Date:2015-05-25 Posted in:バックエンドの技術 Text by:Tetsuya