ぺい

渋谷系アドテクエンジニアの落書き

Goの並列処理の動作を理解する

Goやるなら並列処理やるでしょ

Goのイメージ = 並列処理というイメージがある人は多いと思います。Goはケアが難しかった並列処理を他の言語よりも比較的扱いがしやすいようになっていて、わりと手軽に書けたりします。ですが、なんとなくで使っていると思わぬメモリリークの発生などが発生したりします。パフォーマンスアップのつもりが、パフォーマンスダウンになってしまうことも・・・。
私自身もちゃんと理解できていないなと感じることがあったので、今回はざっくり理解していきたいと思います。

今回の記事のソースは以下のRepoになります。

github.com

並列処理って何が良いの

何が嬉しいのかって話をまずしますと、以下のような合計で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はいいぞ!

f:id:tikasan0804:20170505212036p:plain

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の使い方を理解していればカスタマイズはいくらでも出来ます。

github.com

さっそくgorma開発

gormaの開発には当然ですけど、RDBMSで使うDDLMySQLなどの環境構築が必要になります。なので、用意しました。最低限必要なのは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リソースに対してのアクションをしてみましょう。 f:id:tikasan0804:20170512152749p:plain f:id:tikasan0804:20170512152559p:plain f:id:tikasan0804:20170512152600p:plain

とりあえず、レスポンスが返ってきました。では、どういう動きだったかについて詳しく説明します。

やったこ

ER図
f:id:tikasan0804:20170512153959p:plain

発行した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の癖が強いというこもあり、かなり苦労しました。

f:id:tikasan0804:20170515171457p:plain

この解決方法が全然日本語情報がない上に、しかも、よく見かけるケースなので、また記事にする予定です。 今回はgormaをさくっと使うところまでということで、ここまでにしておきます。

GolangのgoaでAPIをデザインしよう(ハマりポイント編)

goaはいいぞ!

f:id:tikasan0804:20170505212036p:plain

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はいいぞ!

f:id:tikasan0804:20170505212036p:plain

当ブログgoaまとめ

シリーズ

  1. GolangのgoaでAPIをデザインしよう(基本編) - ぺい
  2. GolangのgoaでAPIをデザインしよう(バリデーション編) - ぺい
  3. GolangのgoaでAPIをデザインしよう(レスポンス編) - ぺい
  4. GolangのgoaでAPIをデザインしよう(クライアント編) - ぺい
  5. GolangのgoaでAPIをデザインしよう(エンドポイント編) - ぺい
  6. GolangのgoaでAPIをデザインしよう(ハマりポイント編) - ぺい
  7. GolangのgoaでAPIをデザインしよう(Model編) - ぺい
  8. GolangのgoaでAPIをデザインしよう(Model編②) - ぺい
  9. GolangのgoaでAPIをデザインしよう(ベース作成編) - ぺい

単発

日本語記事

公式

ソース

スライド

勉強会

GolangのgoaでAPIをデザインしよう(エンドポイント編)

goaはいいぞ!

f:id:tikasan0804:20170505212036p:plain

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はいいぞ!

f:id:tikasan0804:20170505212036p:plain

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はいいぞ!

f:id:tikasan0804:20170505212036p:plain

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はいいぞ!