VimConf 2018に行ってきた
VimConf 2018に行ってきました。
今年は@mattn_jpさんだけじゃなく、Vim作者のBramさんも発表するという超豪華Keynoteでした。
また、今年は去年以上に国際色高まるカンファレンスだったんじゃないかなと思います。
運営のみなさま、とても大変だったと思いますがすごく楽しい1日を過ごすことができました。
本当にありがとうございます。
以下セッションの一言感想です。
What is the next feature? by Yasuhiro Matsumoto
vim-jpについてのエモい話。
自分もVimにパッチを送る時、vim-jpにはとてもお世話になりました。
それにしても(ある程度形になった)パッチを用意してプレゼンに臨むのは流石です。
Vim: From hjkl to a platform for plugins by Bram Moolenaar
Vimの新機能について作者本人から話が聴けるというすごく貴重な発表でした。
プラグイン管理やVimの表現力がさらに充実していきそうで今後がとても楽しみです。
もちろんVim scriptのパフォーマンス向上も!
Migrating plugins to standard features by daisuzu
自分の発表。
Shougo wareを標準機能で置き換える話をしてきました。
Modes by Tatsuhiro Ujihisa
:Termdebug
を使ったモードの実装の解説。
前にMeguro.vimで:Termdebug
を使っているところを見せてもらいましたが、とても便利そうでした。
A day in the life of (ordinary) Vimmer by OKURA Masafumi
Ordinary Vimmerは毎朝アップデートしますよね。
Learn once, use anywhere.
自分の発表で言いたかったことはこれです。
Modern editor-independent development environment for PHP by USAMI Kenta
普段はEmacsを使っていらっしゃる。
自分でHackできるツールが良いというのは完全に同意です。
Effective Modern Vim scripting by Alisue
Create your own plugin!
すごくわかります。
vital-Whiskyの中にあるRx
がすごく気になりました。
Oni - The GUI-fication of Neovim by Akin
Electron(React + TypeScript)製のneovimクライアントの紹介。
バッファを分割してブラウザを開けるのはとても便利そうでした。
Vim ported to WebAssembly by rhysd
今日1番濃い発表だったんじゃないか...?
Vimの内部処理の解説がとても詳しくてすごく勉強になりました。
それにしても8日でやってしまうのが本当にすごい。
grpc-gatewayでProtocol Buffers over HTTP
grpc-gatewayを使うとgRPCサーバをRESTfulなインターフェースで叩けるようになります。
APIクライアントはswaggerから生成しても良いのですが、Goだとprotocで生成したstructをPOSTする方が依存も少なく、楽なこともあるでしょう。
ということで軽く試してみました。
Protocol Buffersのシリアライズ/デシリアライズ
まず、サーバがProtocol Buffersをやりとりできるようにしてあげないといけません。
そのための機能は全てgrpc-gatewayのruntimeパッケージにあるので、以下のようにして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/status
をspb
*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にすると、Code
やMessage
、Details
が取り出せるので後続の処理で自由に使えます。
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で元に戻してあげる必要があります。
例えばDetails
がerrdetails.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" > ]
になります。
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:] }
これを使うことでパスの最上位とそれ以降がそれぞれhead
、tail
として取得できるので、以下を繰り返しながらハンドリングしていく。
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) } }
例えば次のエンドポイントを作るとしたらこんな感じ?
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) } }
うん、まあ面倒。
ちなみにtodosHandler
やusersHandler
のServeHTTP
以降の処理は、
h.handler(head).ServeHTTP(w, r)
の形式で呼べるようにしても良いし、h.create(w, r)
やh.get(w, r, head)
のようにしてしまっても良い。
※ただしhead
をcontext
に詰めて渡すのはダメ!
1の場合はh.handler
がhttp.HandlerFunc
など、ServeHTTP
を実装した型を返すことになる。
もちろんstruct
を返しても構わないけど、How to correctly use context.Context in Go 1.7 – Jack Lindamood – Mediumにあるような割と複雑なコードになってしまうんじゃないかな。
力尽きたのでとりあえずこの辺で...
Google Codelabs の執筆者用ツールの使い方
今週の GCP 7/2 – google-cloud-jp – Mediumでclaat(Codelabs as a Thing)
が紹介されていました。
これを使うとGoogle DocsやMarkdownから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のようなので以下を参考にフォーマットや例を入手して書いていきます。
生成したコンテンツはpython3 -m http.server
やGitHub Pagesなどで配信することもできますが、本家に載せることも可能です。
手順は Can I publish my codelab on the official Google Codelabs site? にあるので興味がある人は是非やってみてください。
そして日本語版は数が少ないので英語版からの翻訳があると初学者はとても助かります。
リポジトリごとに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
これで他のリポジトリのファイルを開いたら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のテンプレートを準備するの面倒になったのでその部分をやってくれるサーバを作ってみた。
go get github.com/daisuzu/liveimg
してからgoemon liveimg
で立ち上げたら- ブラウザで
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を基に○○する関数 なんかはそんなに違和感なさそう
- 「都合によりどうしてもテストしなければならないが、他に良い方法が無い」
- そういう場合はたしかに
- 「そもそも何が悪いのかわからない」
- ...
複雑なケースになってくるとどうなるかわからないけど、うまいこと設計できればなんとかなりそうな気がするのでどこかで試してみたい。