Goの並列処理の動作を理解する
Goやるなら並列処理やるでしょ
Goのイメージ = 並列処理というイメージがある人は多いと思います。Goはケアが難しかった並列処理を他の言語よりも比較的扱いがしやすいようになっていて、わりと手軽に書けたりします。ですが、なんとなくで使っていると思わぬメモリリークの発生などが発生したりします。パフォーマンスアップのつもりが、パフォーマンスダウンになってしまうことも・・・。
私自身もちゃんと理解できていないなと感じることがあったので、今回はざっくり理解していきたいと思います。
今回の記事のソースは以下のRepoになります。
並列処理って何が良いの
何が嬉しいのかって話をまずしますと、以下のような合計で6秒どうしてもかかってしまう処理があったとします。確かに順次処理でやっていくと、各処理は2秒ずつかかってしまいます。ですが、例えばこれが同時に処理できたらどうでしょう?最大でも2秒で終わる処理に変わりますよねっていうのが並列処理です。
package main import ( "fmt" "testing" "time" ) func main() { result := testing.Benchmark(func(b *testing.B) { run() }) fmt.Println(result.T) } func run() { fmt.Println("Start!") process("A") process("B") process("C") fmt.Println("Finish!") } func process(name string) { time.Sleep(2 * time.Second) fmt.Println(name) }
$ go run exp1/main.go Start! A B C Finish! 6.010750402s
よくあるミス
Goはgo
キーワードを使って関数を実行するだけで、goroutine(サブスレッドのようなもの)が生成され、並列に処理が実行されますが、使い方には注意が必要です。最初に意味もわからずよくやってしまいがちなミスの例を示すと。
package main import ( "fmt" "time" ) func main() { fmt.Println("Start!") go process("A") <------ goキーワードで関数実行するとgoroutineが生成される go process("B") go process("C") fmt.Println("Finish!") } func process(name string) { time.Sleep(2 * time.Second) fmt.Println(name) }
$ go run exp2/main.go Start! Finish!
上記のような感じで、一瞬で終わりますが、さっきまで出力されていたA,B,Cの文字列が消えています。何故こうなるかについて説明すると、go
キーワードによって生成されたgoroutineに処理を任せてるので、最初のgo run
で生成されたgoroutineと同期を取っていないので勝手に終わってしまっているからです。これを防ぐには何らかの連絡手段を使って同期を取る必要があります。それがchannel
です。
channelを使って同期させよう
ここからの説明では、聞き覚えのあるワードを使って説明します。
スレッド名 | 実行のされ方 | 具体例 |
---|---|---|
メインスレッド | go run *.go | go run exp1/main.go |
サブスレッド | go hgoe() | go process(“A”) |
channel
は何かと言うと、簡単に言うと専用の電話線のようなものです。これを使うことでメインスレッドとサブスレッドの連絡が取れるようになります。以下にサブスレッドの処理と同期をするためのシンプルな例があります。
package main import ( "fmt" "time" ) func main() { fmt.Println("Start!") // boolの型でchannelを作成する ch := make(chan bool) // goroutineを生成して、サブスレッドで処理する go func() { time.Sleep(2 * time.Second) // chに対してtrueを投げる(送信) ch <- true }() // chに受信があったらisFinに返事する。 // 受信があるまで、処理をブロックし続ける(これで同期が取れる) isFin := <-ch // <-chだけでもブロック出来る // chをクローズする close(ch) // 受信した値をprintする fmt.Println(isFin) fmt.Println("Finish!") }
$ go run exp3/main.go Start! true Finish!
以下のどちらかの記述を使うことでブロックすることが出来るのがポイントとなります。
isFin <- ch
<- ch
channelの動作を使って、並列処理をすると以下のようになります。
package main import ( "fmt" "time" ) func main() { // それぞれとの連絡のためのchを作成する isFin1 := make(chan bool) isFin2 := make(chan bool) isFin3 := make(chan bool) fmt.Println("Start!") go func() { process("A") isFin1 <- true }() go func() { process("B") isFin2 <- true }() go func() { process("C") isFin3 <- true }() // 全部が終わるまでブロックし続ける <-isFin1 <-isFin2 <-isFin3 fmt.Println("Finish!") } func process(name string) { time.Sleep(2 * time.Second) fmt.Println(name) }
$ go run exp4/main.go Start! A B C Finish! 2.003493386s
約2秒で終了しました。これだけで、約4秒短く終わらせることが出来るのもすごいですが、非常に簡単に記述することが出来るのもGoのすごいところです。ですが、いまの記述だと非常にひ弱なコードになっています。
例えば、今までとは違い処理の数が変動する場合どうでしょう?chの数が固定だと破綻します。そういったイレギュラーに対応するには、処理の数とchのブロックをうまく使えば、同期処理が可能ですが、直感的でないので、sync.WaitGroup
を使うことをおすすめします。
sync.WaitGroupでいい感じにする
package main import ( "fmt" "testing" "time" "sync" ) func main() { result := testing.Benchmark(func(b *testing.B) { run("A", "B", "C", "D", "E") }) fmt.Println(result.T) } func run(name ...string) { fmt.Println("Start!") // WaitGroupを作成する wg := new(sync.WaitGroup) // channelを処理の数分だけ作成する isFin := make(chan bool, len(name)) for _, v := range name { // 処理1つに対して、1つ数を増やす(この例の場合は5になる) wg.Add(1) // サブスレッドに処理を任せる go process(v, isFin, wg) } // wg.Doneが5回通るまでブロックし続ける wg.Wait() close(isFin) fmt.Println("Finish!") } func process(name string, isFin chan bool, wg *sync.WaitGroup) { // wgの数を1つ減らす(この関数が終了した時) defer wg.Done() time.Sleep(2 * time.Second) fmt.Println(name) isFin <- true }
$ go run exp5/main.go Start! B D E A C Finish! 2.005301726s
sync.WaitGroup
は、処理は走る回数分だけ、wg.Add(int)
します。基本的には、1処理に対してwg.Add(1)
で問題ないと思います。そして、その処理が終わったら、wg.Done
で終了を知らせます。
最後の方に書いているwg.Wait()
はwg.Add(int)
の数分だけのwg.Done()
が通るまで、処理をブロックし続けます。そして、決められた回数処理が終わるとブロックを終了します。
まとめ
以上で簡単な並列処理なら問題なく実行出来ると思います。ただ、実際に使うとなると、例えば並列処理の数は5つまでにしたいとか、ある処理が終わるまでスタートしてほしくないとか、色々あると思います。次回はそこらへんもやってみたいと思います。
GolangのgoaでAPIをデザインしよう(Model編)
goaはいいぞ!
Golangのgoaの勉強に役立つ情報まとめ - ぺい
goaの情報をもっと見たい方は、上のリンクから確認してください
Model層をいい感じにしてくれるgorma
goaはAPIの入り口と出口の部分をいい感じにしてくれますが、Modelとのやり取りなどのロジックは手で書く必要があります。正直だるくないっすか?だるいですよね。簡単なRDBMSとのやり取り程度なら実は自動でいい感じにしてくれるプラグインがgormaです。
goaには、goagenというものがあり、これに独自で書いたコード生成ロジックを作成して食わせるといい感じにしてくれます。gormaはそのコード生成ロジックのModel部分のものです。
gormaがやってくれること
では、実際にgormaがやってくれる素晴らしい仕事をざっくりではありますけど、紹介します。
CRUDは秒で終わる
以下によくあるエンドポイントの例があります。これくらいなら、gormaに任せたら実装は終了します。
GET /api/v1/bottles bottlesを複数取得する GET /api/v1/bottles/{id} bottlesから1件取得する POST /api/v1/bottles bottlesを1件作成する DELETE /api/v1/bottles/{id} bottlesを1件削除する PUT/api/v1/bottles/{id} bottlesを1件更新する
ちょっとした入れ子構造はいい感じにしてくれる
bottlesというリソースがあったとします。bottlesのひとつひとつには、所有者であるaccountsのリソースに紐付いています。jsonでいい感じに返してくださいという案件があったとします。以下のようなjsonがほしくなります。
gormaでやると秒で終わります。
[ { "account": { "email": "example@gmail.com", "id": 1, "name": "山田 太郎" }, "id": 1, "name": "シャルドネ", "quantity": 4 }, { "account": { "email": "example@gmail.com", "id": 1, "name": "山田 太郎" }, "id": 1, "name": "シャルドネ", "quantity": 4 } ]
カスタマイズ出来る
gormaってよくわかんないやつ嫌だなーと最初は思ってたんですけど、実はこいつはgormというORMのコードを自動生成してくれてるだけなので、gormの使い方を理解していればカスタマイズはいくらでも出来ます。
さっそくgorma開発
gormaの開発には当然ですけど、RDBMSで使うDDLやMySQLなどの環境構築が必要になります。なので、用意しました。最低限必要なのはDockerが動く環境なので、適宜用意してください。
今回使うサンプル
github.com
$ go get github.com/tikasan/goa-simple-sample または $ ghq get github.com/tikasan/goa-simple-sample
上記のいずれかのコマンドで、ローカルに落としてきてください。
環境構築と実行
Docker立ち上げて、DDL読み込ませて、起動してSwaggerで実行してみます。
$ make docker/build $ make docker/start $ make migrate/up $ make rundb $ make swaggerUI
Bottlesリソースに対してのアクションをしてみましょう。
とりあえず、レスポンスが返ってきました。では、どういう動きだったかについて詳しく説明します。
やったこと
ER図
発行したSQL
SELECT * FROM `bottles` WHERE `bottles`.`deleted_at` IS NULL SELECT * FROM `accounts` WHERE `accounts`.`deleted_at` IS NULL AND ((`id` IN ('1','2','2','3')))
goaコード
MediaType
var Account = MediaType("application/vnd.account+json", func() { Description("celler account") Attributes(func() { Attribute("id", Integer, "id", func() { Example(1) }) Attribute("name", String, "名前", func() { Example("山田 太郎") }) Attribute("email", String, "メールアドレス", func() { Example("example@gmail.com") }) Required("id", "name", "email") }) View("default", func() { Attribute("id") Attribute("name") Attribute("email") }) }) var Bottle = MediaType("application/vnd.bottle+json", func() { Description("celler bottles") Attributes(func() { Attribute("id", Integer, "id", func() { Example(1) }) Attribute("name", String, "ボトル名", func() { Example("シャルドネ") }) Attribute("quantity", Integer, "数量", func() { Example(4) }) // accountのMediaTypeを入れ子構造にする Attribute("account", Account) Required("id", "name", "quantity", "account") }) View("default", func() { Attribute("id") Attribute("name") Attribute("quantity") Attribute("account") }) }) var Category = MediaType("application/vnd.category+json", func() { Description("celler account") Attributes(func() { Attribute("id", Integer, "id", func() { Example(1) }) Attribute("name", String, "名前", func() { Example("ワイン") }) Required("id", "name") }) View("default", func() { Attribute("id") Attribute("name") }) })
エンドポイント
var _ = Resource("bottles", func() { BasePath("/bottles") // --------- さっき実行したのはこれ --------- Action("list", func() { Description("複数") Routing( GET("/"), ) Response(OK, CollectionOf(Bottle)) Response(BadRequest, ErrorMedia) }) // --------- さっき実行したのはこれ --------- Action("show", func() { Description("単数") Routing( GET("/:id"), ) Params(func() { Param("id", Integer, "id", func() { Example(1) }) }) Response(OK, Bottle) Response(NotFound) Response(BadRequest, ErrorMedia) }) Action("add", func() { Description("追加") Routing( POST("/"), ) Params(func() { Param("account_id", Integer, "アカウントID", func() { Example(1) }) Param("name", String, "ボトル名", func() { Default("") Example("赤ワインなにか") }) Param("quantity", Integer, "数量", func() { Example(0) }) Required("account_id", "name", "quantity") }) Response(Created) Response(BadRequest, ErrorMedia) }) Action("delete", func() { Description("削除") Routing( DELETE("/:id"), ) Params(func() { Param("id", Integer, "id", func() { Example(1) }) }) Response(OK) Response(NotFound) Response(BadRequest, ErrorMedia) }) Action("update", func() { Description("更新") Routing( PUT("/:id"), ) Params(func() { Param("id", Integer, "id", func() { Example(1) }) Param("name", String, "ボトル名", func() { Default("") Example("赤ワインなにか") }) Param("quantity", Integer, "数量", func() { Default(0) Minimum(0) Example(0) }) }) Response(OK) Response(NotFound) Response(BadRequest, ErrorMedia) }) })
gormaコード
Model
gormaには、命名規則があります。
種類 | 説明 | gormaでの書き方 |
---|---|---|
テーブル名 | テーブル名は複数形 | bottles,categories |
カラム名 | カラム名はスネークケース | user_id |
Model名 | テーブル名が複数形でみModelは単数形 | Bottle,Category |
PrimaryKey | 必ずid統一する (IntegerかUUID) | id |
timestamp | created_at,updated_at,deleted_atで統一 | --- |
特にModel名とテーブル名が同じにならないのは、結構ハマりポイントだったりする。この他にもあるかもしれませんが、基本的にgormの規則に従っています。
実際に組んだコードを以下に示します。
package design import ( "github.com/goadesign/gorma" . "github.com/goadesign/gorma/dsl" ) var _ = StorageGroup("celler", func() { Description("celler Model") // Mysqlを使う Store("MySQL", gorma.MySQL, func() { Description("MySQLのリレーションナルデータベース") // accountsテーブルのModelなら、Account Model("Account", func() { // MediaTypeで作成したAccountにマッピングする RendersTo(Account) Description("celler account") // PrimaryKeyの設定 Field("id", gorma.Integer, func() { PrimaryKey() }) Field("name", gorma.String) Field("email", gorma.String) // timestamp系の定義 Field("created_at", gorma.Timestamp) Field("updated_at", gorma.Timestamp) Field("deleted_at", gorma.NullableTimestamp) // HasMany(複数形名, 単数形名) HasMany("Bottles", "Bottle") }) Model("Bottle", func() { RendersTo(Bottle) Description("celler bottle") Field("id", gorma.Integer, func() { PrimaryKey() }) Field("name", gorma.String) Field("quantity", gorma.Integer) Field("created_at", gorma.Timestamp) Field("updated_at", gorma.Timestamp) Field("deleted_at", gorma.NullableTimestamp) // BelongTo(単数形) BelongsTo("Account") HasMany("BottleCategories", "BottleCategory") }) Model("Category", func() { RendersTo(Category) Description("celler category") Field("id", gorma.Integer, func() { PrimaryKey() }) Field("name", gorma.String) Field("created_at", gorma.Timestamp) Field("updated_at", gorma.Timestamp) Field("deleted_at", gorma.NullableTimestamp) HasMany("BottleCategories", "BottleCategory") }) Model("BottleCategory", func() { Description("celler bottle category") Field("id", gorma.Integer, func() { PrimaryKey() }) Field("created_at", gorma.Timestamp) Field("updated_at", gorma.Timestamp) Field("deleted_at", gorma.NullableTimestamp) BelongsTo("Category") BelongsTo("Bottle") }) }) })
gormaのコード生成は以下のコマンドです。
$ go get github.com/goadesign/gorma <--- gorma入れてない人はこれする $ goagen --design=$(REPO)/design gen --pkg-path=github.com/goadesign/gorma
models/ ├── account.go ├── account_helper.go ├── bottle.go ├── bottle_helper.go ├── bottlecategory.go ├── bottlecategory_helper.go ├── category.go └── category_helper.go
上記のようなファイル群が出てきたかと思います。それぞれのファイルが持っているコードは以下のようになっています。
ファイル名 | 説明 |
---|---|
model名.go | gormに必要な構造体とCRUDの最低限のメソッド |
model名_helper.go | MediaTypeに合わせたRead系メソッド集 |
では、実際にクエリを投げるために、下準備をします。
gormaでは、外部テーブルからの参照系のクエリをデフォルトでは実装されていません。恐らくなんでもかんでも結合すると、クエリの処理速度が遅くなるからだと思われす。models/bottle_helper.go
// ListBottle returns an array of view: default. func (m *BottleDB) ListBottle(ctx context.Context, accountID int) []*app.Bottle { defer goa.MeasureSince([]string{"goa", "db", "bottle", "listbottle"}, time.Now()) var native []*Bottle var objs []*app.Bottle //err := m.Db.Scopes(BottleFilterByAccount(accountID, m.Db)).Table(m.TableName()).Find(&native).Error // Accountテーブルから、情報を取得したいので、Preloadメソッドを追加する err := m.Db.Scopes(BottleFilterByAccount(accountID, m.Db)).Table(m.TableName()).Preload("Account").Find(&native).Error if err != nil { goa.LogError(ctx, "error listing Bottle", "error", err.Error()) return objs } for _, t := range native { objs = append(objs, t.BottleToBottle()) } return objs }
上記のように、メソッドを直接書き換えるのも良いですが、自動生成されたメソッドをあまり書き換えたくないというのもあるので、私はユーザー定義用のファイルを作成して対応したりしてます。
bottle_defined.go model名_defined.go
そして、modelのデザインに変更が発生して、再生成する時は、以下のMakeコマンドを走らせることで、安全にコードを再生するようにしています。
model: @ls models | grep -v '_defined.go' | xargs rm -f @goagen --design=$(REPO)/design gen --pkg-path=github.com/goadesign/gorma
次にコントローラーを定義します。controller/bottles.go
package controller import ( "github.com/goadesign/goa" "github.com/jinzhu/gorm" "github.com/tikasan/goa-simple-sample/app" "github.com/tikasan/goa-simple-sample/models" ) // BottlesController implements the bottles resource. type BottlesController struct { *goa.Controller db *gorm.DB } // NewBottlesController creates a bottles controller. func NewBottlesController(service *goa.Service, db *gorm.DB) *BottlesController { return &BottlesController{ Controller: service.NewController("BottlesController"), db: db, } } // Add runs the add action. func (c *BottlesController) Add(ctx *app.AddBottlesContext) error { // BottlesController_Add: start_implement // Put your logic here b := &models.Bottle{} b.AccountID = ctx.AccountID b.Name = ctx.Name b.Quantity = ctx.Quantity bdb := models.NewBottleDB(c.db) err := bdb.Add(ctx.Context, b) if err != nil { return ctx.BadRequest(goa.ErrBadRequest(err)) } // BottlesController_Add: end_implement return ctx.Created() } // Delete runs the delete action. func (c *BottlesController) Delete(ctx *app.DeleteBottlesContext) error { // BottlesController_Delete: start_implement // Put your logic here bdb := models.NewBottleDB(c.db) err := bdb.Delete(ctx.Context, ctx.ID) if err != nil { return ctx.BadRequest(goa.ErrBadRequest(err)) } // BottlesController_Delete: end_implement return nil } //--------------------------------- // 例で使った部分 //--------------------------------- // List runs the list action. func (c *BottlesController) List(ctx *app.ListBottlesContext) error { // BottlesController_List: start_implement // Put your logic here bdb := models.NewBottleDB(c.db) b := bdb.ListBottle(ctx.Context, 0) // BottlesController_List: end_implement res := app.BottleCollection{} res = b return ctx.OK(res) } // Show runs the show action. func (c *BottlesController) Show(ctx *app.ShowBottlesContext) error { // BottlesController_Show: start_implement // Put your logic here bdb := models.NewBottleDB(c.db) b, err := bdb.OneBottle(ctx.Context, ctx.ID, 0) if err != nil { return ctx.NotFound() } // BottlesController_Show: end_implement res := &app.Bottle{} res = b return ctx.OK(res) } // Update runs the update action. func (c *BottlesController) Update(ctx *app.UpdateBottlesContext) error { // BottlesController_Update: start_implement // Put your logic here b := &models.Bottle{} b.ID = ctx.ID b.Name = ctx.Name b.Quantity = ctx.Quantity bdb := models.NewBottleDB(c.db) err := bdb.Update(ctx.Context, b) if err == gorm.ErrRecordNotFound { return ctx.NotFound() } else if err != nil { return ctx.BadRequest(goa.ErrBadRequest(err)) } // BottlesController_Update: end_implement return nil }
これで、完成しました。自動的にマッピングも完了し、欲しい情報を簡単に取得することが出来ます。
gormaは多少取っ付きにくさはありますが、理解すると、goaでの開発がさらにスピードアップするので、結構おすすめです。
gormaですんなり出来ない多対多問題
gormaの作成してくれたコードだけでは、当然出来ないことがあります。それは多対多のリレーションの場合です。これは元々のgormの癖が強いというこもあり、かなり苦労しました。
この解決方法が全然日本語情報がない上に、しかも、よく見かけるケースなので、また記事にする予定です。 今回はgormaをさくっと使うところまでということで、ここまでにしておきます。
GolangのgoaでAPIをデザインしよう(ハマりポイント編)
goaはいいぞ!
Golangのgoaの勉強に役立つ情報まとめ - ぺい
goaの情報をもっと見たい方は、上のリンクから確認してください
designが読み込めない
症状
goagenでコード生成出来ない。
goagen bootstrap -d ./design main.go:15:2: local import "./design" in non-local package
対策
$GOPATHからの相対で指定する必要があります。
goagen bootstrap -d github.com/tikasan/goa-stater/design
デザインを書き直したけど、動きが変わらない
症状
- デザイン書き直したんだけど、どうもエンドポイントの動きが変わっていない気がする。
- goagen流したのだけど、動き変わらない。
対策
デザインはあくまでデザインなので、goagenでコードを再生成する必要があります。また、goagenコマンドを流したとしても既にあるファイルには上書きしない仕様になっています。なので、一度消すかまたはforceオプションで強制上書きが必要です。
私は以下のようなMakefileでコード生成のし直しをしています。これで間違いなく生成のし直しが行われます。
REPO:=github.com/tikasan/goa-simple-sample <--- ここを自分の環境に合わせる gen: clean generate clean: @rm -rf app @rm -rf client @rm -rf tool @rm -rf swagger @rm -rf schema @rm -rf js @rm -f build generate: @goagen app -d $(REPO)/design @goagen swagger -d $(REPO)/design @goagen client -d $(REPO)/design @goagen js -d $(REPO)/design @goagen schema -d $(REPO)/design @go build -o build
$ make gen
GAE/Goで使えないんだけど!!(contextの問題で)
症状
GAE/Go環境下で実行するとcontextパッケージが使えません!的なエラーが出る。
$ goapp serve ./server 2017/05/07 22:59:44 Can't find package "context" in $GOPATH: cannot find package "context" in any of: /Users/jumpei/go/src/github.com/tikasan/hoge/vendor/context (vendor tree) /Users/jumpei/go_appengine/goroot/src/context (from $GOROOT) /Users/jumpei/go/src/github.com/tikasan/hoge/src/context (from $GOPATH) /Users/jumpei/go/src/context
対策
一部のソース書き換えを行います。
examples/appengine at master · goadesign/examples · GitHub
Golangのgoaの勉強に役立つ情報まとめ
goaはいいぞ!
当ブログgoaまとめ
シリーズ
- GolangのgoaでAPIをデザインしよう(基本編) - ぺい
- GolangのgoaでAPIをデザインしよう(バリデーション編) - ぺい
- GolangのgoaでAPIをデザインしよう(レスポンス編) - ぺい
- GolangのgoaでAPIをデザインしよう(クライアント編) - ぺい
- GolangのgoaでAPIをデザインしよう(エンドポイント編) - ぺい
- GolangのgoaでAPIをデザインしよう(ハマりポイント編) - ぺい
- GolangのgoaでAPIをデザインしよう(Model編) - ぺい
- GolangのgoaでAPIをデザインしよう(Model編②) - ぺい
- GolangのgoaでAPIをデザインしよう(ベース作成編) - ぺい
単発
日本語記事
- tchssk(goa日本人コミッターブログ)
- goadesign カテゴリーの記事一覧 - 押してダメならふて寝しろ
- ホーム — そこはかとなく書くよん。
- Goベースのマイクロサービスフレームワーク"goa"によるサービスAPIの定義,レビュー,実装
- Better than
- goaのDSLのAttributeについて - kawaken's blog
- VSCode用のgoaのコードスニペットを作ってみた - kawaken's blog
- goa v1でサポートされてないint64を使う方法 - azihsoyn's blog
- goaでお手軽google loginを行うミドルウェアを作ってみました。 - Qiita
公式
- goa :: Design-first API Generation
日本語ページ - Reference · goa :: Design-first API Generation
リファレンス - goa · GitHub
Repo - GitHub - goadesign/examples: Examples for goa showing specific capabilities
example - GitHub - goadesign/goa-cellar: goa winecellar example service
サンプルアプリケーション - GitHub - goadesign/gorma-cellar: Same design as goa-cellar, but with the added gorma integration
サンプルアプリケーション(gormaを使った例)
ソース
- GitHub - pei0804/goa-simple-sample
サンプル - GitHub - pei0804/goa-stater
プロジェクトを開始する時に必要な構成
スライド
- goa intro. - SSSSLIDE
- goa Design first API Generation - SSSSLIDE
- goaを使ったAPI開発/api-development-with-goa - SSSSLIDE
- goaでスッキリWebAPI開発 - SSSSLIDE
- Goを好きになる方法@Go合宿 - SSSSLIDE
- Development using goa and golang on Pacificporter inc.
- https://slideck.io/github.com/tchssk/goadesigntokyo/index.md#/
勉強会
GolangのgoaでAPIをデザインしよう(エンドポイント編)
goaはいいぞ!
Golangのgoaの勉強に役立つ情報まとめ - ぺい
goaの情報をもっと見たい方は、上のリンクから確認してください
エンドポイントにも色々ある
同じエンドポイントの指定でも、DSLの書き方で変わります。若干癖があったりするので、今回はそれについてまとめたいと思います。
例:ユーザーのフォロー操作。
同じ名前だけど、HTTPメソッドが違うので、操作の中身が違う。
PUT /users/follow // フォローする DELETE /users/follow // フォローを外す
例:なんかのリスト
/listというリソースに対してエンドポイントで操作を表現している。(途中まで操作同じだけど、若干違う)
GET /list/new // 新しいリスト GET /list/topic // 注目されてるリスト GET /list // リスト(全件)
例:ちょっと特殊ケース
GET /api/v1/method/users/:id/follow/:type
以上のエンドポイントをDSLでどうやって表現するかを紹介します。
goaはコード生成する時に少し長めのコマンドが必要になるので、Makefileを作っておくと良いと思います。
ちなみに、コマンドやフォルダ構成は毎回同じものを使うので、テンプレートを作成しました。よろしければ利用してください。
もし、もっと便利なフォーマットがあればPRをください。
$ go get github.com/tikasan/goa-stater または $ ghq get github.com/tikasan/goa-stater
APIをデザインする
順を追って記事を読んでいればすんなりソースも読めると思います。(つまりコメントがなし)
package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) var _ = Resource("method", func() { BasePath("/method") Action("method", func() { Description("HTTPメソッド") Routing( GET("/get"), POST("/post"), DELETE("/delete"), PUT("/put"), ) Response(OK, MessageMedia) Response(BadRequest, ErrorMedia) }) Action("list", func() { Description("リストを返す") Routing( GET("/list"), GET("/list/new"), GET("/list/topic"), ) Response(OK, CollectionOf(UserMedia)) Response(BadRequest, ErrorMedia) }) Action("follow", func() { Description("フォロー操作") Routing( PUT("/users/follow"), DELETE("/users/follow"), ) Response(OK, MessageMedia) Response(BadRequest, ErrorMedia) }) Action("etc", func() { Routing(GET("/users/:id/follow/:type")) Description("ちょっと特殊ケース") Params(func() { Param("id", Integer, "id") Param("type", Integer, "タイプ", func() { Enum(1, 2, 3) }) }) Response(OK, "plain/text") Response(BadRequest, ErrorMedia) }) })
MediaTypeを定義する
以前記事で書いたコードです。
var UserMedia = MediaType("application/vnd.user+json", func() { Description("example") Attributes(func() { Attribute("id", Integer, "id", func() { Example(1) }) Attribute("name", String, "名前", func() { Example("hoge") }) Attribute("email", String, "メールアドレス", func() { Example("satak47cpc@gmail.com") }) Required("id", "name", "email") }) // 特別な指定がない場合はdefaultのMediaType View("default", func() { Attribute("id") Attribute("name") Attribute("email") }) // tinyという名前の場合は、簡潔なレスポンスフォーマットにすることが出来る View("tiny", func() { Attribute("id") Attribute("name") }) })
コントローラー定義
デザインを元にコードを生成しましょう。コントローラーは、値を返すだけの単純なものを作成します。
$ goagen bootstrap -d github.com/tikasan/goa-stater/design
package controller import ( "fmt" "github.com/goadesign/goa" "github.com/tikasan/goa-stater/app" ) // MethodController implements the method resource. type MethodController struct { *goa.Controller } // NewMethodController creates a method controller. func NewMethodController(service *goa.Service) *MethodController { return &MethodController{Controller: service.NewController("MethodController")} } // Etc runs the etc action. func (c *MethodController) Etc(ctx *app.EtcMethodContext) error { // MethodController_Etc: start_implement // Put your logic here // MethodController_Etc: end_implement return ctx.OK([]byte(fmt.Sprintf("ID: %d, Type %d", ctx.ID, ctx.Type))) } // Follow runs the follow action. func (c *MethodController) Follow(ctx *app.FollowMethodContext) error { // MethodController_Follow: start_implement // Put your logic here var message string // 何かのフォロー操作 if "PUT" == ctx.Request.Method { message = "フォローした" } else if "DELETE" == ctx.Request.Method { message = "フォロー外した" } // MethodController_Follow: end_implement res := &app.Message{} res.Message = message return ctx.OK(res) } // List runs the list action. func (c *MethodController) List(ctx *app.ListMethodContext) error { // MethodController_List: start_implement // Put your logic here var listType string switch ctx.RequestURI { //case client.ListMethodPath(): <---これでもいけるけど・・・って感じ 何かいい方法。 case "/api/v1/method/list": listType = "ただの" case "/api/v1/method/list/new": listType = "新しい" case "/api/v1/method/list/topic": listType = "注目されている" } // MethodController_List: end_implement res := make(app.UserTinyCollection, 2) u1 := &app.UserTiny{ ID: 1, Name: listType + "ユーザー1", } u2 := &app.UserTiny{ ID: 2, Name: listType + "ユーザー2", } res[0] = u1 res[1] = u2 return ctx.OKTiny(res) } // Method runs the method action. func (c *MethodController) Method(ctx *app.MethodMethodContext) error { // MethodController_Method: start_implement // Put your logic here message := ctx.RequestURI // MethodController_Method: end_implement res := &app.Message{} res.Message = message return ctx.OK(res) }
実際に動かしてみよう
$ go run main.go
$ curl -v -X PUT 'http://localhost:8080/api/v1/method/users/follow' * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > PUT /api/v1/method/users/follow HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/vnd.message+json < Date: Sat, 06 May 2017 06:34:09 GMT < Content-Length: 33 < {"message":"フォローした"} * Connection #0 to host localhost left intact
$ curl -v -X DELETE 'http://localhost:8080/api/v1/method/users/follow' * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > DELETE /api/v1/method/users/follow HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/vnd.message+json < Date: Sat, 06 May 2017 06:35:05 GMT < Content-Length: 36 < {"message":"フォロー外した"} * Connection #0 to host localhost left intact
$ curl -v 'http://localhost:8080/api/v1/method/users/1/follow/3' * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > GET /api/v1/method/users/1/follow/3 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: plain/text < Date: Sat, 06 May 2017 06:35:35 GMT < Content-Length: 13 < * Connection #0 to host localhost left intact ID: 1, Type 3%
これで出来上がりです。エンドポイントのURIによって処理を変えるところが少し不細工なんで、誰か良い案あれば教えて欲しいです。
goa紹介で使うソース達は、以下のリポジトリに随時更新していきます。
github.com
GolangのgoaでAPIをデザインしよう(クライアント編)
goaはいいぞ!
Golangのgoaの勉強に役立つ情報まとめ - ぺい
goaの情報をもっと見たい方は、上のリンクから確認してください
クライアント編
バックエンドをいい感じに出来るgoaですけど、欲を言えばクライアント側もいい感じにしてほしい。実はいい感じにしてくれます。とは言ってもちょっとした手助けをしてくれる感じです。
対応しているのは、JavascriptでのAPIへのリクエストです。
$ go get github.com/tikasan/goa-simple-sample または $ ghq get github.com/tikasan/goa-simple-sample
今回はgoaのサンプルを作成しているリポジトリを使って説明したいと思います。 本来は以下のコマンドで実行して、クライアント側のコードを生成しますが、出来上がっているので、生成されたものを見てみましょう。
goagen js -d github.com/tikasan/goa-simple-sample/design
// This module exports functions that give access to the goa simple sample API hosted at localhost:8080. // It uses the axios javascript library for making the actual HTTP requests. define(['axios'] , function (axios) { function merge(obj1, obj2) { var obj3 = {}; for (var attrname in obj1) { obj3[attrname] = obj1[attrname]; } for (var attrname in obj2) { obj3[attrname] = obj2[attrname]; } return obj3; } return function (scheme, host, timeout) { scheme = scheme || 'http'; host = host || 'localhost:8080'; timeout = timeout || 20000; // Client is the object returned by this module. var client = axios; // URL prefix for all API requests. var urlPrefix = scheme + '://' + host; // 複数アクション(:ID) // path is the request path, the format is "/api/v1/actions/:ID" // config is an optional object to be merged into the config built by the function prior to making the request. // The content of the config object is described here: https://github.com/mzabriskie/axios#request-api // This function returns a promise which raises an error if the HTTP response is a 4xx or 5xx. client.IDActions = function (path, config) { cfg = { timeout: timeout, url: urlPrefix + path, method: 'get', responseType: 'json' }; if (config) { cfg = merge(cfg, config); } return client(cfg); } ---------------------------------------------------------------- // Validation // path is the request path, the format is "/api/v1/validation" // ID, defaultType, email, enumType, integerType, reg, stringType are used to build the request query string. // config is an optional object to be merged into the config built by the function prior to making the request. // The content of the config object is described here: https://github.com/mzabriskie/axios#request-api // This function returns a promise which raises an error if the HTTP response is a 4xx or 5xx. client.validationValidation = function (path, ID, defaultType, email, enumType, integerType, reg, stringType, config) { cfg = { timeout: timeout, url: urlPrefix + path, method: 'get', params: { id: id, defaultType: defaultType, email: email, enumType: enumType, integerType: integerType, reg: reg, stringType: stringType }, responseType: 'json' }; if (config) { cfg = merge(cfg, config); } return client(cfg); } return client; }; });
jsでそのまま使えるリクエスト集が出来上がりました。
この機能を知った時は感動しましたが、あまりに楽すぎてダメ人間になりそうと思ったので、今後も使っていこうと思います。(?)
GolangのgoaでAPIをデザインしよう(レスポンス編)
goaはいいぞ!
Golangのgoaの勉強に役立つ情報まとめ - ぺい
goaの情報をもっと見たい方は、上のリンクから確認してください
レスポンス
いきなりですが、以下のエンドポイントを見てください。RESTを知ってる人は、レスポンスが違うことが分かると思います。
GET /users/1 GET /users
開発するチームにもよって変わる可能生はあると思いますが、GET /users/1
は恐らくひとりのユーザー情報だけを返す。そして、GET /users
はひとり以上のユーザー情報を返す。
また、GET /users
はIDや名前だけで良かったり、GET /users/1
は詳細な情報が欲しかったり、同じリソースに対しての操作でも、レスポンスは変える必要があります。(と思っています)
具体的にレスポンスの例を示すと以下のような感じになります。
GET /users
[ { "id": 1, "name": "ユーザー1" }, { "ID": 2, "name": "ユーザー2" } ]
GET /users/1
{ "id": 1, "email": "satak47cpc@gmail.com", "name": "ユーザー1" }
また、場合によってはIDの一覧だけ欲しい時もあったり
{"america":3,"japan":2,"korea":4}
キーに意味を持たせた連想配列が欲しかったり
[1,2,3]
上記のような需要全てに対応することが出来ます。実際に動かしてみましょう。
goaはコード生成する時に少し長めのコマンドが必要になるので、Makefileを作っておくと良いと思います。
ちなみに、コマンドやフォルダ構成は毎回同じものを使うので、テンプレートを作成しました。よろしければ利用してください。
もし、もっと便利なフォーマットがあればPRをください。
$ go get github.com/tikasan/goa-stater または $ ghq get github.com/tikasan/goa-stater
APIデザインをする
package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) var _ = Resource("response", func() { BasePath("/response") Action("list", func() { Description("ユーザー(複数)") Routing( GET("/users"), ) // 複数返す Response(OK, CollectionOf(UserMedia)) Response(BadRequest, ErrorMedia) }) Action("show", func() { Description("ユーザー(単数)") Routing( GET("/users/:id"), ) // 単一 Response(OK, UserMedia) Response(BadRequest, ErrorMedia) }) Action("hash", func() { Description("ユーザー(ハッシュ)") Routing( GET("/users/hash"), ) // 連想配列 Response(OK, HashOf(String, Integer)) Response(BadRequest, ErrorMedia) }) Action("array", func() { Description("ユーザー(配列)") Routing( GET("/users/array"), ) // 配列 Response(OK, ArrayOf(Integer)) Response(BadRequest, ErrorMedia) }) })
MediaTypeを定義する
package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) var UserMedia = MediaType("application/vnd.user+json", func() { Description("example") Attributes(func() { Attribute("id", Integer, "id", func() { Example(1) }) Attribute("name", String, "名前", func() { Example("hoge") }) Attribute("email", String, "メールアドレス", func() { Example("satak47cpc@gmail.com") }) Required("id", "name", "email") }) // 特別な指定がない場合はdefaultのMediaType View("default", func() { Attribute("id") Attribute("name") Attribute("email") }) // tinyという名前の場合は、簡潔なレスポンスフォーマットにすることが出来る View("tiny", func() { Attribute("id") Attribute("name") }) })
Controllerを定義する
goagen bootstrap -d github.com/tikasan/goa-stater/design
特にビジネスロジックなどはなしで、値を返すだけのコードを実装します。
package controller import ( "github.com/goadesign/goa" "github.com/tikasan/goa-stater/app" ) // ResponseController implements the response resource. type ResponseController struct { *goa.Controller } // NewResponseController creates a response controller. func NewResponseController(service *goa.Service) *ResponseController { return &ResponseController{Controller: service.NewController("ResponseController")} } // Array runs the array action. func (c *ResponseController) Array(ctx *app.ArrayResponseContext) error { // ResponseController_Array: start_implement // Put your logic here // ResponseController_Array: end_implement res := make([]int, 3) res[0] = 1 res[1] = 2 res[2] = 3 return ctx.OK(res) } // Hash runs the hash action. func (c *ResponseController) Hash(ctx *app.HashResponseContext) error { // ResponseController_Hash: start_implement // Put your logic here // ResponseController_Hash: end_implement res := make(map[string]int, 3) res["japan"] = 2 res["america"] = 3 res["korea"] = 4 return ctx.OK(res) } // List runs the list action. func (c *ResponseController) List(ctx *app.ListResponseContext) error { // ResponseController_List: start_implement // Put your logic here // ResponseController_List: end_implement res := make(app.UserTinyCollection, 2) u1 := &app.UserTiny{ ID: 1, Name: "ユーザー1", } u2 := &app.UserTiny{ ID: 2, Name: "ユーザー2", } res[0] = u1 res[1] = u2 return ctx.OKTiny(res) } // Show runs the show action. func (c *ResponseController) Show(ctx *app.ShowResponseContext) error { // ResponseController_Show: start_implement // Put your logic here // ResponseController_Show: end_implement res := &app.User{} res.ID = 1 res.Name = "ユーザー1" res.Email = "satak47cpc@gmail.com" return ctx.OK(res) }
準備が整ったので、実行してリクエストを投げてみましょう!
$ go run main.go
$ curl -v 'http://localhost:8080/api/v1/response/users/1' * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > GET /api/v1/response/users/1 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/vnd.user+json < Date: Sat, 06 May 2017 04:33:08 GMT < Content-Length: 63 < {"ID":1,"email":"satak47cpc@gmail.com","name":"ユーザー1"} * Connection #0 to host localhost left intact
$ curl -v 'http://localhost:8080/api/v1/response/users' * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > GET /api/v1/response/users HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/vnd.user+json; type=collection < Date: Sat, 06 May 2017 04:35:02 GMT < Content-Length: 66 < [{"ID":1,"name":"ユーザー1"},{"ID":2,"name":"ユーザー2"}] * Connection #0 to host localhost left intact
いい感じのデータが返ってきました。goaはいいぞ!