2016年3月1日火曜日

Go言語とGoogleAppEngineに触れる / GoogleAppEngine

mainpicture

前回の記事でGo言語の第一印象とチュートリアルに関する気づきをまとめました。

今回はそれに引き続き、Google App Engineに関する気づきや詰まった点をまとめていきます。


Google App Engineに触れる

現在Google App Engineでは、java、python、PHP、Goをサポートしていますが、スピンアップが最も高速に立ち上がるのがGo言語のようです。

そのため、多くの方がGo言語Google App Engineへの利用を前提として使っているのではないでしょうか。

インストールや実行に関してはGoogle App Engine SDKなどを参照していただくことにして、実際に実装してきた中で問題となった箇所やその解決策を記載していきます。


json parse

webサービスの要件にrequest bodyにjsonがくるのでそれをparseして値を適宜処理するというものがありました。

他の言語やプラットフォームだと、body parserやjson parserがあり思っていた通りの動作をしてくれることが多いのですが、Google App Engineにはbody(io.Reader interfaceを持つ)を"encoding/json"json.NewDecoderへ渡し、structへ変換するという流れを取ります。

サンプルコードは以下のような形になります。

import (
    "encoding/json"
    "io"
)

type Data struct {
    Type     int      `json:"type"`
    Value    string   `json:"value"`
}

func ParseJson(r io.Reader) (*Data, error) {
    decoder := json.NewDecoder(r)
    data := Data{Type: -1}
    for {
        if err := decoder.Decode(&data); err == io.EOF {
            break
        } else if err != nil {
            return nil, err
        }
    }
    return data, nil
}

こんな感じになります。

struct`json:"type"`というようなtagをつけることにより、jsonのnameをstruct内のfieldにマッピングします。データの方はstructと一致している必要があります。

問題1. 文字列と数値

jsonではvalueに数値がある場合には”“で囲まないようです。そのため、”“が入力値となっている値をintにマッピングしようとするとエラーを出力します。(個人的にはzero valueという仕組みを持っているのだから0入れておいてよと思いますが。)

これはクライアントアプリとの仕様の問題なので、今回は全てstringで値をもらい、後から全て変換する方針にしました。

問題2. マッピングが失敗する

これの解決が一番時間がかかりました。ある特定のfieldだけ値のマッピングがされないという事象に遭遇したのです。

よく見てみるとtagの中にスペースが入っていることが分かり、これを消せばマッピングがうまく動いてくれました。

json: "type"json:"type"です。


カスタムヘッダー

今回オリジナルのヘッダーをクライアントが送信してくるので、サーバーがそれを取得して値を使うという実装がありました。実装自体はそんなに難しくなく、SDKもきちんとHeaderを取得するためのインターフェースを備えていました。

ソースコードしては以下のような感じです。

const MY_HEADER = "x-my-header"

func GetHeader(r *http.Request) string {
    h := r.Header[MY_HEADER]
    if h == nil {
        return ""
    }
    if len(h) == 0 {
        return ""
    }
    return h[0]
}

問題. カスタムヘッダーが取得できない

実際にテストコードは動き、ローカルサーバーも無事に動作することを確認しました。問題は本番環境にデプロイした後に発生しました。

Headerの取得ができない!!

Headerの内容をログに書き出してみると、Google App Engine内ではHeader Keyがキャメルケースになるようです。(RFCを読んでいないので詳細はわからないのですが)

そのため上記サンプルのx-my-headerX-My-Headerに修正する必要があります。

気をつけましょう。


datastore

問題. datastore console viewが正常に動作しない

ある構成のstructをdatastoreに入れたところ、console viewからデータが確認できなくなってしまいまいした。

これに関しては現在調査中で、実際に値が入っているかどうかはwebサービスにendpointを一つ用意して、そのendpointロジック内でqueryを発行して結果を出力して確認できるようにしましたが、いまいちです。

情報. Ancestor(祖先)は自動生成

datastoreの概念にancestorというentityのグルーピングや親子関係を表現するための仕組みがあります。ソースコードではdatastore.NewKey(c, "datastorename", "", 0, ancestor) のようにkeyを生成する際に第5引数に入れます。

このancestor keyに関しては、もし存在しない場合には自動で生成されるので、先に作っておいたりする必要はありません。


interface{}型

Go言語でのinterfaceの定義の仕方は

type Interfacer interface {
    Method() string
}
func (d *Data) Method() string {
    return "hogehoge"
}

このような形でした。interfaceとはメソッド群を定義しています。言語仕様を読むと、全ての型はinterface{}という空のinterfaceを実装しているとの記載があります。

このinterface{}はjavaでいうObject型のような扱いをすることができ、かなり抽象度を上げた取り扱いをすることができます。
また、以下のような形でinterface型から値を取り出し、 それぞれの型に適した処理を実装することができます。

switch i := x.(type) { // xがinterface{}型
case nil:
    printString("x is nil")
case int:
    printInt(i)  // iはint
case float64:
    printFloat64(i)  // iはfloat64
case func(int) float64:
    printFunction(i)  // iは関数
case bool, string:
    printString("type is bool or string")
default:
    printString("don't know the type")
}

testing

前回の記事でも簡単に取り上げましたが、datastoreのテストをする際に一貫性というキーワードが大切になります。これはdataの性質を表すACIDの一つであり、処理が正しく行われた場合は、その処理は一貫した結果を返すという類のものです。

なので誰かがデータを更新した直後に他の人がデータを読みにいったケースを考えると、一貫性が保たれているシステムの場合は更新したデータを100%読むことができます。

例えば金融機関、人事給与の給与データなどはそういう設計をすべきです。

一貫性が保たれていないシステムの場合は、書き込みの直後に読み取りにいってもデータが変わっていない可能性があります。

例えば、ログデータであったり、チャット内容であったり、DNSレコードの更新であったりは、書き込みしたという事実があれば、読み取りは直後でなくてもよいという性質があります。

さて、Google Cloud Platformのdatastoreは一貫性を保証していません。(厳密言えばqueryにancestorを指定した場合など、強い一貫性を持たせて実行することができる箇所もあります)そのためテストを行う際に問題が生じます。それはテストロジックの最初の方でデータをinputするロジックを追加したのに、テストメソッドの箇所では反映されておらずまともにテストができないということです。

今回これにはまりました。

解決策はaetest.NewInstanceを生成する際にオプションでStronglyConsistentDatastore: trueを追加する必要があります。

確かにこのオプション名は強い一貫性のdatastoreにするという意味になりそうですね。

なのでContextを生成する部分を以下のように書き換えます。

opt := aetest.Options{
    AppID: "test",
    StronglyConsistentDatastore: true,
}
inst, _ := aetest.NewInstance(opt)
defer inst.Close()
req, _ := inst.NewRequest("POST", "/", nil)
c := appengine.NewContext(req)

このcを全てに使い回せば、一貫性を保ったテストを記載することができます。


いかがでしたでしょうか。個人的にはGo言語自体は嫌いではないです。ただし、自由度がかなり制限されていると感じています。誰が書いてもしばらくは割と似たようなソースコードになるのではないでしょうか。

また、rubyやnodeに比べると周囲のライブラリがまだ貧弱でやることは少し多くなるのかもしれないなと思います。

ただ、Google App Enginge上での速度は捨てがたいですけどね。

今日はここまでです。

では良いインプットと良いプログラミングを。

0 件のコメント:

コメントを投稿