grpc-gatewayでProtocol Buffers over HTTP

grpc-gatewayを使うとgRPCサーバをRESTfulなインターフェースで叩けるようになります。
APIクライアントはswaggerから生成しても良いのですが、Goだとprotocで生成したstructをPOSTする方が依存も少なく、楽なこともあるでしょう。
ということで軽く試してみました。

Protocol Buffersのシリアライズ/デシリアライズ

まず、サーバがProtocol Buffersをやりとりできるようにしてあげないといけません。
そのための機能は全てgrpc-gatewayruntimeパッケージにあるので、以下のようにしてServeMuxを初期化すればOKです。

mux := runtime.NewServeMux(
    runtime.WithMarshalerOption("application/octet-stream", new(runtime.ProtoMarshaller)),
)

APIクライアント

これでクライアントからapplication/octet-streamでProtocol Buffersを投げることができるようになりました。

func do(url string, in *pb.ExampleRequest) (*pb.ExampleResponse, error) {
    body := new(bytes.Buffer)
    if err := new(runtime.ProtoMarshaller).NewEncoder(body).Encode(in); err != nil {
        return nil, err
    }

    res, err := http.Post(url, "application/octet-stream", body)
    if err != nil {
        return nil, err
    }
    defer res.Body.Close()

    if res.StatusCode >= 400 {
        b, err := ioutil.ReadAll(res.Body)
        if err != nil {
            return nil, errors.New(res.Status)
        }
        return nil, fmt.Errorf("%s: %s", res.Status, b)
    }

    out := new(pb.ExampleResponse)
    if err := new(runtime.ProtoMarshaller).NewDecoder(res.Body).Decode(out); err != nil {
        return nil, err
    }
    return out, err
}

エラーの型を共通化する

このままでも普通に使う分には問題ありませんが、サーバがエラーレスポンスを返した際もBodyをstructにデコードできるとより嬉しいです。
現状だと、サーバ側のエラーレスポンスはデフォルトで以下のunexportedな型になっています。

type errorBody struct {
    Error   string     `protobuf:"bytes,1,name=error" json:"error"`
    // This is to make the error more compatible with users that expect errors to be Status objects:
    // https://github.com/grpc/grpc/blob/master/src/proto/grpc/status/status.proto
    // It should be the exact same message as the Error field.
    Message string     `protobuf:"bytes,1,name=message" json:"message"`
    Code    int32      `protobuf:"varint,2,name=code" json:"code"`
    Details []*any.Any `protobuf:"bytes,3,rep,name=details" json:"details,omitempty"`
}

// Make this also conform to proto.Message for builtin JSONPb Marshaler
func (e *errorBody) Reset()         { *e = errorBody{} }
func (e *errorBody) String() string { return proto.CompactTextString(e) }
func (*errorBody) ProtoMessage()    {}

そのため、クライアント側でデコードするためにはこちらをコピーして使うか、サーバ側で型を変更する必要があります。

エラーの型を変える場合はWithProtoErrorHandlerを使います。

mux := runtime.NewServeMux(
    runtime.WithMarshalerOption("application/octet-stream", new(runtime.ProtoMarshaller)),
    runtime.WithProtoErrorHandler(func(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, w http.ResponseWriter, r *http.Request, err error) {
        w.Header().Set("Content-Type", marshaler.ContentType())

        s, ok := status.FromError(err)
        if !ok {
            s = status.New(codes.Unknown, err.Error())
        }

        buf, merr := marshaler.Marshal(s.Proto())
        if merr != nil {
            w.WriteHeader(http.StatusInternalServerError)
            io.WriteString(w, `{"error": "failed to marshal error message"}`)
            return
        }

        w.WriteHeader(runtime.HTTPStatusFromCode(s.Code()))
        w.Write(buf)
    }),
)

内容はDefaultHTTPErrorとほとんど同じですが、エラーの型をgoogle.golang.org/genproto/googleapis/rpc/status(*spb.Status)にしています。

こうすることで、クライアント側はgoogle.golang.org/genproto/googleapis/rpc/statusspb*1としてimportし、proto.Unmarshal*spb.Statusに戻せるようになります。

if res.StatusCode >= 400 {
    b, err := ioutil.ReadAll(res.Body)
    if err != nil {
        return nil, errors.New(res.Status)
    }

    v := new(spb.Status)
    if err := proto.Unmarshal(b, v); err != nil {
        return nil, fmt.Errorf("%s: %s", res.Status, b)
    }
    return nil, status.ErrorProto(v)
}

返ってきたエラーはstatus.FromError*status.Statusにすると、CodeMessageDetailsが取り出せるので後続の処理で自由に使えます。

if s, ok := status.FromError(err); ok {
    log.Printf("code: %d, message: %s, details: %v", s.Code(), s.Message(), s.Proto().Details)
}

なお、Detailsは型情報が落ちてしまっているため、内容を正しく出力するにはptypes.UnmarshalAnyで元に戻してあげる必要があります。

例えばDetailserrdetails.BadRequestの場合だと以下のようなコードです。

if s, ok := status.FromError(err); ok {
    var details []string
    for _, d := range s.Proto().Details {
        var m errdetails.BadRequest
        if err := ptypes.UnmarshalAny(d, &m); err == nil {
            details = append(details, m.String())
        }
    }
    log.Printf("code: %d, message: %s, details: %v", s.Code(), s.Message(), details)
}

s.Details()だと[field_violations:<field:"data" description:"invalud" > ]
s.Proto().Detailsだと[type_url:"type.googleapis.com/google.rpc.BadRequest" value:"\n\017\n\004data\022\007invalud" ]になってしまうのが、
ちゃんと[field_violations:<field:"data" description:"invalud" > ]になります。

*1:statusだと"google.golang.org/grpc/status"とかぶるため

Goの標準パッケージだけでRESTfulなHandlerを作る

Goのnet/httpパッケージだけではパスパラメータを扱うことができないため、/users/:id のようなエンドポイントを作ろうとしたら自分で処理を書かなければいけない。
適当なフレームワークや3rd-partyのパッケージを使えば簡単ではあるんだけど、時々標準パッケージだけで書きたくなってその度にどうやって書くんだっけ?となるのでblogに書いておく。

ポイントはHow to not use an http-router in goで紹介されているShiftPath

func ShiftPath(p string) (head, tail string) {
    p = path.Clean("/" + p)
    i := strings.Index(p[1:], "/") + 1
    if i <= 0 {
        return p[1:], "/"
    }
    return p[1:i], p[i:]
}

これを使うことでパスの最上位とそれ以降がそれぞれheadtailとして取得できるので、以下を繰り返しながらハンドリングしていく。

func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    var head string

    // それ以降のパスをr.URL.Pathに設定し、次のHandlerに渡す
    head, r.URL.Path = ShiftPath(r.URL.Path)

    // headを使って次のHandlerを決める
    switch head {
    case "users":
        h.users.ServeHTTP(w, r)
    default:
        http.NotFound(w, r)
    }
}

例えば次のエンドポイントを作るとしたらこんな感じ?

  • POST /api/todos
  • GET /api/todos/:id
  • POST /api/users
  • GET /api/users/:id
func ShiftPath(p string) (head, tail string) {
    p = path.Clean("/" + p)
    i := strings.Index(p[1:], "/") + 1
    if i <= 0 {
        return p[1:], "/"
    }
    return p[1:i], p[i:]
}

func NewHandler() http.Handler {
    return &rootHandler{
        api: &apiHandler{
            todos: &todosHandler{},
            users: &usersHandler{},
        },
    }
}

type rootHandler struct {
    api *apiHandler
}

func (h *rootHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    var head string
    head, r.URL.Path = ShiftPath(r.URL.Path)

    switch head {
    case "api":
        h.api.ServeHTTP(w, r)
    default:
        http.NotFound(w, r)
    }
}

type apiHandler struct {
    todos *todosHandler
    users *usersHandler
}

func (h *apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    var head string
    head, r.URL.Path = ShiftPath(r.URL.Path)

    switch head {
    case "todos":
        h.todos.ServeHTTP(w, r)
    case "users":
        h.users.ServeHTTP(w, r)
    default:
        http.NotFound(w, r)
    }
}

type todosHandler struct{}

func (h *todosHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    var head string
    head, r.URL.Path = ShiftPath(r.URL.Path)

    switch r.Method {
    case http.MethodPost:
        // Create Todo
    case http.MethodGet:
        // Get Todo
    default:
        http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
    }
}

type usersHandler struct{}

func (h *usersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    var head string
    head, r.URL.Path = ShiftPath(r.URL.Path)

    switch r.Method {
    case http.MethodPost:
        // Create User
    case http.MethodGet:
        // Get User
    default:
        http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
    }
}

うん、まあ面倒。
ちなみにtodosHandlerusersHandlerServeHTTP以降の処理は、

  1. h.handler(head).ServeHTTP(w, r)の形式で呼べるようにしても良いし、
  2. h.create(w, r)h.get(w, r, head)のようにしてしまっても良い。

※ただしheadcontextに詰めて渡すのはダメ!

1の場合はh.handlerhttp.HandlerFuncなど、ServeHTTPを実装した型を返すことになる。
もちろんstructを返しても構わないけど、How to correctly use context.Context in Go 1.7 – Jack Lindamood – Mediumにあるような割と複雑なコードになってしまうんじゃないかな。

力尽きたのでとりあえずこの辺で...

Google Codelabs の執筆者用ツールの使い方

今週の GCP 7/2 – google-cloud-jp – Mediumclaat(Codelabs as a Thing)が紹介されていました。

github.com

これを使うとGoogle DocsMarkdownからGoogle Codelabsのコンテンツを生成することができます。

Markdownでとりあえず試してみる

1. ツールをインストール
go get -u github.com/googlecodelabs/tools/claat
# またはリリースページからダウンロードする
2. 適当なファイルを作成
  • index.md
# App Engine 入門 (Go)

## 概要

### 学習内容

* Google App Engine での単純な Go サーバーの作成方法。
* サーバーを停止せずにコードを更新する方法。

### 必要な環境

* Go
* Vim、Emacs、Nano などの標準 Linux テキスト エディタの熟知

## セットアップと要件
3. htmlを生成
claat export index.md
4. サーバを起動
claat serve

でOKです。

Google Docsでちゃんと作る

正式なフォーマットはGoogle Docsのようなので以下を参考にフォーマットや例を入手して書いていきます。

f:id:daisuzu:20180711022911p:plain

生成したコンテンツはpython3 -m http.serverGitHub Pagesなどで配信することもできますが、本家に載せることも可能です。

手順は Can I publish my codelab on the official Google Codelabs site? にあるので興味がある人は是非やってみてください。

そして日本語版は数が少ないので英語版からの翻訳があると初学者はとても助かります。

codelabs.developers.google.com

リポジトリごとにGOPATHを切る環境でのvim-go

↓のようなリポジトリごとにその直下をGOPATHにする環境でGoを書く時、

.
├── github.com/daisuzu/bar
│     ├── bin
│     ├── pkg
│     └── src
│         └── bar
│              └── main.go
└── github.com/daisuzu/foo
       ├── bin
       ├── pkg
       └── src
           └── foo
                └── main.go

両方のリポジトリのファイルを同時に開いて行ったり来たりするのを少し楽にするためにvim-goの設定を切り替える関数を作ることにした。

function! SwitchRepo()
  let toplevel = trim(system('git rev-parse --show-toplevel'))
  if toplevel =~# '^fatal'
    return
  endif

  " GOPATHをリポジトリ直下に変更する
  execute 'GoPath ' . toplevel

  " リポジトリ名をgoimportsの'-local'フラグに渡す
  let g:go_fmt_options = { 'goimports': '-local ' . fnamemodify(toplevel, ':t') }
endfunction

*1

これで他のリポジトリのファイルを開いたらcdした後にcall SwitchRepo()すれば:GoDefでちゃんと飛べるし、
:GoImportsした時もサブパッケージを標準パッケージとは異なるグループにしてくれる。

  • Before
import (
    "bar/handler"
    "bar/model"
    "fmt"
)
  • After
import (
    "fmt"

    "bar/handler"
    "bar/model"
)

autocmdを組み合わせればもっと便利になるかもしれないけど、今のところは必要ない。

*1:普段はg:go_fmt_optionsを使っていないので都度上書き

図のライブプレビュー環境を改善した

PlantUMLで図を書く時もblockdiagと同じようにやろうかなと思ったけど、markdown作ったりpandocのテンプレートを準備するの面倒になったのでその部分をやってくれるサーバを作ってみた。

github.com

  1. go get github.com/daisuzu/liveimgしてから
  2. goemon liveimgで立ち上げたら
  3. ブラウザでhttp://localhost:8080/_liveimg/uml.pngを開けばOK
    • /_liveimg/以降はカレントディレクトリにある任意のイメージ

Goで時刻が絡む単体テストをどうするか考える

Goにはtimeパッケージをmockする仕組みが用意されていない。
そのため、time.Nowを以下のようにして使っているコードを見かけることがある。

var Now = func() time.Time { return time.Now() }

たしかにこうすることでNow()が返す値を自由に設定でき、単体テストが書きやすくなる。
ただ、関数を変数に入れるということは「その関数を処理の中で書き換えるため」という意味を持たせることになり、それをテストでしか書き換えないのであれば読み手に無用な混乱を与えてしまいそうで個人的にはあまり好きではない。

その辺りの意識が開発者の間で統一されていればそれで良いのかもしれないが、新しく入った人なんかはそうもいかないはず。
さすがに意図せず書き換えてトラブってしまうことはまず無いだろうが、自分がよく使いそうな用途でこのような使い方を避けられないか考えてみようと思う。

時刻を含むデータが返ってくる処理

例えばタイムスタンプを付与したリソースを作成するような関数。

type Resource struct {
    Data       string
    CreateTime time.Time
}

func NewResource(data string) *Resource {
    return &Resource{
        Data:       data,
        CreateTime: time.Now(),
    }
}

当然NewResource()のテストでreflect.DeepEqualを使うと通らない。

func TestNewResource(t *testing.T) {
    want := &Resource{Data: "test", CreateTime: time.Now()}

    got := NewResource("test")
    if !reflect.DeepEqual(got, want) {
        // got.CreateTimeとwant.CreateTimeが一致しない
        t.Errorf("NewResource() = %v, want %v", got, want)
    }
}

ただ、ここでCreateTimeの値を確認したいかというと必ずしもそうではない。
なので不要なフィールドは除外して比較する、で十分なケースがほとんどなはず。

func resetFields(r *Resource) *Resource {
    // 元の値は変えず、CreateTimeだけゼロ値にしたコピーを作る
    after := *r
    after.CreateTime = time.Time{}
    return &after
}

func TestNewResource_reset(t *testing.T) {
    want := &Resource{Data: "test"}

    got := NewResource("test")
    after := resetFields(got)
    if !reflect.DeepEqual(after, want) {
        t.Errorf("NewResource() = %v, want %v", after, want)
    }
}

毎回resetFields()のような関数を作るのが面倒であればgo-cmpを使うとcmpopts.IgnoreFieldsに除外するフィールドを設定するだけになるので簡単だ。

func TestNewResource_ignore(t *testing.T) {
    want := &Resource{Data: "test"}
    opt := cmpopts.IgnoreFields(Resource{}, "CreateTime")

    got := NewResource("test")
    if !cmp.Equal(got, want, opt) {
        t.Errorf("NewResource() = %v, want %v", got, want)
    }
}

どうしてもtime.Nowが使われていていることを確認したければ、それはテストではなく静的解析をしてチェックしてあげれば良いんじゃないだろうか。

ということでtime.Nowを書き換える必要は無さそう。

時刻を使用した条件判定がある処理

例えば以下のような関数。

// GetResource は指定されたidのResourceを返す。Resourceの取得に失敗、
// もしくは取得したResourceの有効期限が切れていた場合はエラーを返す。
func (s *service) GetResource(id int64) (*Resource, error) {
    res, err := s.store.Get(id)
    if err != nil {
        return nil, err
    }

    if time.Now().After(res.ExpireTime) {
        return nil, ErrExpired
    }

    return res, nil
}

こういったものは時刻使った判定処理を関数やメソッドに切り出し、その部分を単体でテストできるようにしてしまう。

func IsExpired(r *Resource, t time.Time) bool {
    return t.After(r.ExpireTime)
}

// or

func (r *Resource) IsExpired(t time.Time) bool {
    return t.After(r.ExpireTime)
}

正直GetResource()がエラーを返す場合のテストケースを網羅するよりも、呼び出し元がそのエラーハンドリングを正しく実装できているかを確認する方がよっぽど大事だと思っている。
呼び出し元は次のようなREST APIのハンドラだったりするのでserviceはモックなどにしてテストすることになり、そうなるとtime.Nowを書き換える必要は無くなる。

func (h handler) Get(w http.ResponseWriter, r *http.Request) {
    id, err := getIDFromPath(r.URL.Path)
    if err != nil {
        http.NotFound(w, r)
        return
    }

    res, err := h.service.GetResource(id)
    if err != nil {
        if err == ErrExpired {
            http.NotFound(w, r)
        } else {
            http.Error(w, err.Error(), http.StatusInternalServerError)
        }
        return
    }

    json.NewEncoder(w).Encode(res)
}

以前から気にはなっていたけど、そういうパッケージやTesting in Go by example: Part 5にあるようなパターンを見てもあまりピンと来ず、
色々な人に聞いてみても「time.Nowを変数に入れるのは仕方ない」という意見ばかりだったのでちょっと考えてみた。

  • 「引数で渡そうとしても結局はどこかでtime.Nowを呼ばなきゃいけなくなる」
    • time.Nowを書き換える必要がなければ良いのでは?
  • 「引数にtime.Timeを渡したくない」
    • 好みの問題ではあるけど、そこまで不自然じゃなければ良いのでは?
    • 渡されたtを基に○○する関数 なんかはそんなに違和感なさそう
  • 「都合によりどうしてもテストしなければならないが、他に良い方法が無い」
    • そういう場合はたしかに
  • 「そもそも何が悪いのかわからない」
    • ...

複雑なケースになってくるとどうなるかわからないけど、うまいこと設計できればなんとかなりそうな気がするのでどこかで試してみたい。

Meguro.vim #8 に行ってきた

Meguro.vim #8 で久しぶりにvimrcを整理し、不要なプラグインや設定なんかを削ったら300行くらい短くすることができた。
ついでにリファクタリングをして、次のような任意のEXコマンドの結果を新しいバッファに表示するコマンド*1を作ってみた。
(今までは特定のEXコマンドに限定していたものを使っていた)

command! -nargs=1 -complete=command L
      \ <mods> new
      \ | setlocal buftype=nofile bufhidden=hide noswapfile
      \ | call setline(1, split(execute(<q-args>), '\n'))

:scriptnamesとか:buffers用のつもりだったけど、ちょうど同じタイミングで開催された Osaka.vim #12@koturn さんが発表しているのを見て、:registers:marksで使っても良さそうだなと思った。

*1:実際はもう少し色々とやっている