[go: up one dir, main page]
More Web Proxy on the site http://driver.im/

Go言語で API サーバーを開発する

読了時間15分

czMjYXJ0aWNsZSMzNTA3MiMxNDE3NjkjMzUwNzJfbnhLR1VnYkxHdS5wbmc

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

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

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

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

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

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

開発環境

  • Go: バージョン 1.4.2
  • Vim: テキストエディター
  • vim-go: Go 開発向け IDE 環境 (各種ツールをまとめてインストールしてくれる)
  • goenv: 仮想環境管理ツール
  • fresh: 自動ビルドとサーバー再起動
  • godep: 依存パッケージ解決

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

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

  • var は関数外で変数宣言するときや型を明示するときに使う
  • := は関数内でのみ使える
  • := は if, for, switch ステートメントのローカル変数として使える

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

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

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

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

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

使っているライブラリ

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

  • binding: バリデーションライブラリ
  • configparser: ini ファイルパーサー
  • context: context オブジェクトを扱うライブラリ (リクエストグローバル変数)
  • go-mysql-driver: mysql ドライバー
  • golang-stats-api-handler: サーバーのシステム情報を取得するライブラリ
  • gorp: OR マッパー的なライブラリ
  • logrus: ロギングライブラリ
  • negroni: HTTP ライブラリ
  • redigo: Redis クライアント
  • squirrel: SQL ジェネレーター

補足すると、単体テストに使う 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 にはないとあげられています (内容は一部抜粋) 。

  • 動的なコードの読み込み/eval

    Go は eval を行うには向いてない。理論的にはその都度コードをビルドすればできるだろうけど、Go はそもそもそういった用途に設計されていない。

  • REPL

    eval がないことの結果として Go に REPL はありません。 go-repl のように入力する度にコンパイルするものもあるけれど、その方法では任意の状態をビルドできません。Go は本番環境には良いけど、プロトタイピングには向いていない。

  • numpy

    巨大な行列を扱う線形代数の演算を考えてみましょう。Python には numpy があり、その構文は演算子オーバーロードのおかげで十分良いものです。Go には演算子オーバーロードがないため、線形代数を扱うときにコードが醜くなってしまいます。なぜ演算子オーバーロードがないかと言うと、Java と同様にそれが “不必要な複雑性” だからです。

  • 例外がない

    例外を投げる代わりに panic させる、例外を捕捉する代わりに recover する、finally の代わりに defer を使えば良いとコメントしてくれた方がいます。panic させるのではなくエラー表現を返すというのは、厳密な必要性というよりもライブラリ設計の話です。そして私が使っている多くのライブラリはそのスタイルを採用しています。

  • GUI バインディング

    現時点で Go の Qt バインディングはアルファレベルです。今後変わるかもしれません。そういったバインディングを開発するのがどのぐらい大変かは知りません。並行サーバーにおいて GUI は不要なので Go 開発者やそのコミュニティは GUI プログラミングを気にしないでしょう。

  • これらの全てを組み合わせたもの

    我々の業務では、やや大きな問題を扱っています。ビデオクリップに様々な視覚化を行う多くのプラグインを扱ったり、さらに REPL を使って対話的にプログラミングをサポートしようとしています。Python はある分野では Matlab を置き換えて使えるけれど、Go はそうなりません。

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

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

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

まとめ

Go で実際に開発してみた所感や Python プログラマーからみた Go の印象について紹介しました。

シンプルな API サーバーを Go で開発するのに困ることは、いまのところ特にありません。開発環境、各種バインディングも一通り揃っているし、(英語を読むのに抵抗がなければ) ドキュメントで困ることもありません。開発していて手が止まったときもほとんど Stack Overflow で知りたいことが見つかります。

その他の Go の情報収集サイトや学習に役立った記事なども紹介しておきます。

この辺の情報はRSS 登録しなくても、 カメリオ で Go のテーマを追いかけると最新情報が簡単に集まりますので、お試しください!

最後に白ヤギでは Go エンジニアを始め、デザイナーやメディアディレクターなど様々な職種を募集中ですので、ご興味のある方は下のリンクをご覧ください!

最先端情報吸収研究所 – AIAL

際限ない情報の中から、自分に価値のある情報を効果的に吸収することは、かつてなく大きなチャレンジです。最先端情報研究所はニュースアプリ「カメリオ」、レコメンドエンジン「カメクト」を提供する白ヤギコーポレーションのR&D部門として、データサイエンスの力でこの問題を解決していきます。白ヤギでは現在研究開発メンバーを募集しております。ご興味のある方は是非下記サイトを御覧ください!

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