自作Webフレームワーク
uconを作った話
わかめ まさひろ
わかめ まさひろ @vvakame
TypeScript
Masahiro Wakame
DefinitelyTyped
appengine/go
photo from golang.org/doc/gopher/
GoogleAppEngine/Go
神 いわゆる GOD
諸君、私はappengine/goが好きだ
• 2008年4月 始まる
• サーバレスアーキテクチャ
• 2011年5月 Go対応始まる
• 2015年7月 Go, GAになる
• 2016年4月 Go 1.6対応
1.9.35→❌
1.9.36を使おう!
Eric Schmidt said.
https://www.youtube.com/watch?v=HgWHeT_OwHc&t=1461
GCP Next 2016 Day 1 Keynote
GAE用ライブラリ作ってます
• testerator github.com/favclip/testrator
• UnitTest高速化
• qbg github.com/favclip/qbg
• Datastore用TypeSafeクエリビルダ
• smg github.com/favclip/smg
• Search API用TypeSafeラッパ
productionで利用中!
GoCon 2015 Summer
• appengine専用じゃないけど
• jwg github.com/favclip/jwg
• genbase github.com/favclip/genbase
• GoCon 2015で話をしました!
• SlideShare goo.gl/45lZDK
gb
• gb
• getgb.io/
• gb keeps the peace of our project🌹
• gb gae
• github.com/PalmStoneGames/gb-gae
Google API Discovery Service
誰か知ってる?
APIs Explorer is 神
https://developers.google.com/apis-explorer/
APIs Explorer
• 誰でも簡単に使える
• 実際のAPIが叩かれる
• 結果を共有しやすい
• コードからUIが生成されている
Cloud Endpoints
• appengine専用の仕組み
• cloud.google.com/endpoints/
• 自前APIでAPIs Explorer使える
• APIの構造がわかる!
• 実例がわかる!(DevTool)
• UIを省く極道管理画面も!
周辺ツールも充実
• クライアントライブラリの自動生成
• golangだとこの辺全部そう
• github.com/google/google-api-go-
client
• TypeScript用型定義の生成
• www.npmjs.com/package/gapidts
but…
Googleの闇の領域
ユーザ GAE闇
path mapping
request format
Version切替後reqをなかった事に
custom domain不可
go-endpoints
• CloudEndpoints用framework
• github.com/GoogleCloudPlatform/
go-endpoints
• Service & Method の組み合わせで定義
• w http.ResponseWriterが取れない
• CloudEndpoints的には不要なので…
評価
• 細かい事を気にしなければかなり良い
• カスタムドメイン不可がやはり辛い
• デバッグ不可能な闇の領域が辛い
• ちょいちょいそこが不安定な気が…
• 拡張性が低い
• 横断的な処理を入れにくい…
僕達が必要なもの
Alt Cloud Endpoints
• APIs Explorer的なものが欲しい!
• サバクラの意思疎通が楽
• デバッグが楽
• コード→仕様が良い
• 仕様→コード は努力が必要(努力やだ
• クライアントコードの生成
• 変わったら壊れてほしい
代替ツールの検討
各ツールの評価
• RAML
• API Blueprint (apiary.io
• JSON Schema v4
• Swagger
• gRPC
✨swagger✨
• Swaggerが一番良さそう!
• 個人の見解です
• Open API Initiative発足
• Swagger仕様をbaseに
• 長いものには巻かれたい
• Qiitaに少し書きました goo.gl/BLS3uH
既存GoなSwagger実装の話
最初からswagger対応
の物を選ぶと楽そう
既存Go実装の比較
• go-swagger
• goswagger.io/
• type safeじゃない
• yvasiyarov/swagger
• github.com/yvasiyarov/swagger
• comment baseでつらい
• type safeじゃない
既存Go実装の比較
• go-restful
• github.com/emicklei/go-restful/
• type safeじゃない
• goa
• goa.design/
• type safeじゃない
• DSLがヤバイ
既存Go実装の比較
• grpc-gateway
• github.com/gengo/grpc-gateway
• gRPCのJSONなreverse proxy
• いつのまにかswagger対応してた
• appengineだと❌
既存Webフレームワーク
重要なポイント
• net/httpに近いほうがわかりやすい
• あまりに独自っぽいのはちょっと…
• go-endpointsからの移行
• しばらくCloudEndpointsと両立したい
• コード上の互換性があると嬉しい…
• swagger-uiが使える
既存フレームワーク調べた
• なるべくnet/httpに近い
• revel→❌
• なるべくCloudEndpointsのまま
• net/http→❌
• goji→❌
• Swagger対応!
• martini→❌
結論
•自分でつくろう
趣味に走ったわけではないです
自分で作る話
前提
• appengine縛りにはしない
• とはいえappengineで使えないと困る
• net/httpに近いAPI
• 柔軟性
• go-endpointsとの互換性
• swaggerはopt-in
やっていく
ucon
https://github.com/favclip/ucon
名付け親
某a2cさん
martiniとか
ginとかに
対抗して
名付け親
某a2cさん
uconと
名付けよう!
In japan, ucon (= turmeric) is
to be effective in hangover.
決めた後
某a2cさん
💩
←ゆるさない!!
仕様紹介
まずgo-endpoints
s := &fooService{}



api, err := endpoints.RegisterService(s, "foo", "v1", "Foo API", true)

if err != nil {

panic(err.Error())

}



info := api.MethodByName("Get").Info()

info.HTTPMethod, info.Path, info.Desc = “GET", "/foo/{id}", “Fooを1件取得する"
…
type IntIDRequest struct {

ID int64 `json:"id,string"`

}

func (s *fooService) Get(r *http.Request, req *IntIDRequest) (*FooJSON, error) {
…
}
まずgo-endpoints
s := &fooService{}



api, err := endpoints.RegisterService(s, "foo", "v1", "Foo API", true)

if err != nil {

panic(err.Error())

}



info := api.MethodByName("Get").Info()

info.HTTPMethod, info.Path, info.Desc = “GET", "/foo/{id}", “Fooを1件取得する"
…
type IntIDRequest struct {

ID int64 `json:"id,string"`

}

func (s *fooService) Get(r *http.Request, req *IntIDRequest) (*FooJSON, error) {
…
}
Handler
Response
Setup
まずgo-endpoints
s := &fooService{}



api, err := endpoints.RegisterService(s, "foo", "v1", "Foo API", true)

if err != nil {

panic(err.Error())

}



info := api.MethodByName("Get").Info()

info.HTTPMethod, info.Path, info.Desc = “GET", "/foo/{id}", “Fooを1件取得する"
…
type IntIDRequest struct {

ID int64 `json:"id,string"`

}

func (s *fooService) Get(r *http.Request, req *IntIDRequest) (*FooJSON, error) {
…
}
Request
Response
闇の領域でもろもろ変換されてる
ucon Features
• net/http との類似性
• Routing
• Method, Path Matching
• Middleware
• Bubble
• Dependency Injection
• Plugin
API likes net/http
ucon.HandleFunc("GET", "/", func(w http.ResponseWriter, r *http.Request) {})
API likes net/http
ucon.HandleFunc("GET", "/", func(w http.ResponseWriter, r *http.Request) {})
Routing
ucon.HandleFunc(“*", “/“, …
ucon.HandleFunc(“OPTIONS", “/“, …
ucon.HandleFunc(“GET", “/“, …
ucon.HandleFunc(“POST", “/“, …
ucon.HandleFunc(“GET", “/api/user“, …
ucon.HandleFunc(“GET", “/api/user/me“, …
ucon.HandleFunc(“GET", “/api/user/{id}“, …
Routing rule
• METHODが一致する
• * 指定も可 厳密一致優先
• Request Pathが一致する
• 複数候補ある場合より長い節一致
• Request GET /api/user/123
• 🌟 GET /api/user/{id}
• ❌ GET /api/user
• 先登録優先
Middleware
• 1 request毎の処理に介入
• JavaでいうServletFilter
• ASP.NET MVCでいうFilter
• Logging, DI, CORS用Header,
error→JSON変換 etc, etc…
Middleware
Middleware
Middleware
Middleware
Handler
ServeHTTP DI
Cache-Control
Cookie
appengine.Context
etc, etc…
CORS Header
Path, Query, Body → JSON
*http.Request
http.ResponseWriter
Middleware
type MiddlewareFunc func(b *Bubble) error
type Bubble struct {

R *http.Request

W http.ResponseWriter

Context context.Context

RequestHandler interface{}



ArgumentTypes []reflect.Type

Arguments []reflect.Value

Returns []reflect.Value

}

func (b *Bubble) Next() error {
…

}
func (b *Bubble) do() error {

hv := reflect.ValueOf(b.handler())

…



b.Returns = hv.Call(b.Arguments)



return nil

}
var httpReqType = reflect.TypeOf((*http.Request)(
var httpRespType = reflect.TypeOf((*http.Response
func HTTPRWDI() MiddlewareFunc {

return func(b *Bubble) error {

for idx, argT := range b.ArgumentTypes {

if argT == httpReqType {

b.Arguments[idx] = reflect.ValueOf(b.R)

continue

}

if argT == httpRespType {

b.Arguments[idx] = reflect.ValueOf(b.W)

continue

}

}



return b.Next()

}

}
built-in middleware
• HTTPRWDI
• *http.Request, http.ResponseWriterのDI
• NetContextDI
• net/contextのContextをDI
built-in middleware
• RequestObjectMapper
• path parameter, query paramter, post
bodyをObjectに変換しDI
• ResponseMapper
• HandlerがreturnしたObjectやerrorを
JSONに変換
Plugin
• プロセス起動時1回だけ動作
• 全Handlerの走査
• Handler→Plugin間の値の伝達機構
• swaggerはplugin
• 全Handlerの情報から処理
• swagger.json出力用Handlerの追加
Plugin
type pluginContainer struct {

base interface{}

}



type HandlersScannerPlugin interface {

HandlersScannerProcess(m *ServeMux, rds []*RouteDefinition) error

}

type RouteDefinition struct {

Method string

PathTemplate *PathTemplate

HandlerContainer HandlerContainer

}
func (m *ServeMux) Prepare() {

for _, plugin := range m.plugins {

used := false

if sc := plugin.HandlersScanner(); sc != nil {

err := sc.HandlersScannerProcess(m, m.router.handlers)

if err != nil {

panic(err)

}

used = true

}

if !used {

panic(fmt.Sprintf("unused plugin: %#v", plugin))

}

}

}
Plugin
type pluginContainer struct {

base interface{}

}



type HandlersScannerPlugin interface {

HandlersScannerProcess(m *ServeMux, rds []*RouteDefinition) error

}

type RouteDefinition struct {

Method string

PathTemplate *PathTemplate

HandlerContainer HandlerContainer

}
func (m *ServeMux) Prepare() {

for _, plugin := range m.plugins {

used := false

if sc := plugin.HandlersScanner(); sc != nil {

err := sc.HandlersScannerProcess(m, m.router.handlers)

if err != nil {

panic(err)

}

used = true

}

if !used {

panic(fmt.Sprintf("unused plugin: %#v", plugin))

}

}

}
swagger plugin usage
swPlugin := swagger.NewPlugin(…)

ucon.Plugin(swPlugin)
s := &fooService{}



tag := swPlugin.AddTag(&swagger.Tag{Name: "Foo", Description: ""})

var info *swagger.HandlerInfo



info = swagger.NewHandlerInfo(s.List)

ucon.Handle("GET", "/api/foo/{id}", info)

info.Description, info.Tags = "Fooを1件取得する", []string{tag.Name}
…
type IntIDRequest struct {

ID int64 `json:"id,string"`

}

func (s *fooService) Get(r *http.Request, req *IntIDRequest) (*FooJSON, error) {
…
}
go-endpoints(再掲
s := &fooService{}



api, err := endpoints.RegisterService(s, "foo", "v1", "Foo API", true)

if err != nil {

panic(err.Error())

}



info := api.MethodByName("Get").Info()

info.HTTPMethod, info.Path, info.Desc = “GET", "/foo/{id}", “Fooを1件取得する"
…
type IntIDRequest struct {

ID int64 `json:"id,string"`

}

func (s *fooService) Get(r *http.Request, req *IntIDRequest) (*FooJSON, error) {
…
}
コード規模の話
• 本体 1329行
• ls | grep .go | grep -v _test.go | xargs wc -l
• swaggerプラグイン 1138行
• find ./swagger -type f | grep .go | grep -v sample | grep -v _test.go | xargs wc -l
利用事例
利用サイト
• favclip
• 技術書典 応募サイト
• 怖くてOSSにでけんかったすまんな…
• Topgate社内では今後使っていくはず…
swagger関連ツール
• swagger-uiの話
• Go用クライアントライブラリの話
• TypeScript用型定義ファイル生成の話
求む!
求む!
• 利用してみてブログ書く
• 利用してみて質問する
• 利用してみて…
自分が使えるようになると
満足するタイプ
We are hiring
We are hiring 1
• 開発:テレビ朝日
• jwg, genbase 他 必要に応じて製造
• http://www.favclip.com/
• appengine/go開発者絶賛募集中!
We are hiring 2
• Topgate社も絶賛募集中です
• appengineできる人
• HTML, CSS, JS得意な人
• その他
雑談
https://github.com/golang/proposal/blob/master/
design/15292-generics.md
👍for Web app

GoCon2016 spring 自作Webフレームワーク uconを作った話