goaはいいぞ！

Golangのgoaの勉強に役立つ情報まとめ - ぺい

goaの情報をもっと見たい方は、上のリンクから確認してください

goでAPIを作成する場合は、必ずといっていいくらいgoaでやっている私ですが、日本ではあまり使っている人が居ません。恐らくこの理由は日本語の情報があまり無いことやシンプルなサンプル集がないからかもしれない・・・と思いまして、軽量なサンプルを定期的に紹介していくことにしました。

API設計フェーズ

goaは最初に設計を行ってから、その設計書を元に実装を行っていきます。設計はDSLと呼ばれるもので、最初はこの定義に慣れないですが、読み方さえ分かれば容易にAPIを設計出来ます。

何が嬉しいの？

例えば、 GET /users/:ID というエンドポイントがあるとします。どのようなロジックを組む必要があるでしょう？

例 curl http://localhost/users/1 でリクエストする

/users/1 の1の部分を取得する :IDから取得したものが文字なのか数字なのかチェックする 取得した:IDを使って、データベースなりにSELECT句などで検索する 該当するものがなかったら、404ステータスを返す 正しく取得できたら、200ステータスとレコードを返す。

goaだとどうなるか？

/users/1 の1の部分を取得する :IDから取得したものが文字なのか数字なのかチェックする 取得した:IDを使って、データベースなりにSELECT句などで検索する 該当するものがなかったら、404ステータスを返す 正しく取得できたら、200ステータスとレコードを返す。

1,2に当たる値チェックなどが一切必要なくなります。上記のように少ないパラメータならそこまでの労力ではありませんが、正規表現が必要だったり、複数だったりすると・・・面倒ですよね？

つまり、goaはビズネスロジックオンリーの開発が出来るようになるので、人が組むコードの部分が非常にシンプルになります。

しかも、swaggerのドキュメントが自動生成されるので、ドキュメント更新地獄から解放されます。

フォルダ構成

以下のようなファイル構成を構築してください。

※designフォルダ内のファイルは実際は一つでもokですが、分ける方が私は好きです。

. ├── LICENSE ├── Makefile ├── README.md ├── design │ ├── api_definition.go │ ├── media_types.go │ └── resources.go └── public └── swagger

goaはコード生成する時に少し長めのコマンドが必要になるので、Makefileを作っておくと良いと思います。 ちなみに、コマンドやフォルダ構成は毎回同じものを使うので、テンプレートを作成しました。よろしければ利用してください。

もし、もっと便利なフォーマットがあればPRをください。

go get github.com/tikasan/goa-stater または ghq get github.com/tikasan/goa-stater

デザインする

準備が整ったので、さっそくデザインをしていきたいと思います。

今回はとりあえず動かそうということで、サンプルソースをひたすら貼ります。

細かい説明は順を追って記事を作成して、紹介致します。

APIの実際の動作ではなく、どういったAPIか？やグローバルな設定などを行います。ここに紹介しているもの以外にも関数は存在します。

package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) var _ = API( "goa simple sample" , func () { Title( "tikasan/goa-simple-sample" ) Description( "goaのサンプルです" ) Contact( func () { Name( "pei" ) Email( "satak47cpc@gmail.com" ) URL( "https://github.com/tikasan/goa-simple-sample/issues" ) }) License( func () { Name( "MIT" ) URL( "https://github.com/tikasan/eventory/blob/master/LICENSE" ) }) Docs( func () { Description( "wiki" ) URL( "https://github.com/tikasan/goa-simple-sample/wiki" ) }) Host( "localhost:8080" ) Scheme( "http" , "https" ) BasePath( "/api/v1" ) Origin( "http://localhost:8080/swagger" , func () { Expose( "X-Time" ) Methods( "GET" , "POST" , "PUT" , "DELETE" ) MaxAge( 600 ) Credentials() }) })

MediaType(media_types.go)

レスポンスデータの形式を定義します。（詳しいことは別記事を紹介予定）

package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) var IntegerMedia = MediaType( "application/vnd.integer+json" , func () { Description( "example" ) Attributes( func () { Attribute( "id" , Integer, "id" , func () { Example( 1 ) }) Required( "id" ) }) View( "default" , func () { Attribute( "id" ) }) })

リソースへの操作(resources.go)

リソースへの操作を定義します。（詳しいことは別記事を紹介予定）

package design import ( . "github.com/goadesign/goa/design" . "github.com/goadesign/goa/design/apidsl" ) var _ = Resource( "actions" , func () { BasePath( "/actions" ) Action( "ping" , func () { Description( "サーバーとの導通確認" ) Routing( GET( "/ping" ), ) Response(OK, MessageType) Response(BadRequest, ErrorMedia) }) Action( "hello" , func () { Description( "挨拶する" ) Routing( GET( "/hello" ), ) Params( func () { Param( "name" , String, "名前" , func () { Default( "" ) }) Required( "name" ) }) Response(OK, MessageType) Response(BadRequest, ErrorMedia) }) Action( "ID" , func () { Description( "複数アクション（:id）" ) Routing( GET( "/:id" ), ) Params( func () { Param( "id" , Integer, "id" ) }) Response(OK, IntegerType) Response(NotFound) Response(BadRequest, ErrorMedia) }) }) var _ = Resource( "swagger" , func () { Origin( "*" , func () { Methods( "GET" ) }) Files( "/swagger.json" , "swagger/swagger.json" ) Files( "/swagger/*filepath" , "public/swagger/" ) })

コードを生成する

インストール方法やコマンドの説明はikawahaさんの記事がわかりやすいので、紹介しておきます。

ikawaha.hateblo.jp

今回は github.com/tikasan/goa-stater のrepoをgo getした想定で説明します。

goagen bootstrap -d github.com/tikasan/goa-stater/design

コマンドを実行すると以下のようなログが流れます。 実行出来ない場合は、designパッケージ（フォルダ）の指定が間違っています。

app/contexts.go app/controllers.go ..... ..... swagger swagger/swagger.json swagger/swagger.yaml

出来上がる構成は以下のような感じです。

実際に作業をするファイルは一部です。 app,client,tool,swagger周りは自動で生成されたもので、変更することはないです。

. ├── LICENSE ├── Makefile ├── README.md ├── actions.go <--------- actionsリソース ├── app ├── client ├── design │ ├── api_definition.go │ ├── media_types.go │ ├── resources.go │ └── security.go ├── main.go <------------ Middlewareとかそこらへんやる ├── public │ └── swagger ├── security.go ├── swagger ├── swagger.go └── tool

このままだと少しごちゃごちゃしているので、私はcontrollerというパッケージにまとめたりしています。

. ├── LICENSE ├── Makefile ├── README.md ├── actions.go <--------- controllerフォルダへ移動させる ├── app ├── client ├── controller ├── design │ ├── api_definition.go │ ├── media_types.go │ ├── resources.go │ └── security.go ├── main.go ├── public │ └── swagger ├── security.go ├── swagger ├── swagger.go <------------ controllerフォルダへ移動させる └── tool

main.goを若干記述を変える必要があるので、調整だけしてください。

package main import ( "github.com/goadesign/goa" "github.com/goadesign/goa/middleware" "github.com/tikasan/goa-stater/app" "github.com/tikasan/goa-stater/controller" <-------importする ) func main() { service := goa.New( "goa simple sample" ) service.Use(middleware.RequestID()) service.Use(middleware.LogRequest( true )) service.Use(middleware.ErrorHandler(service, true )) service.Use(middleware.Recover()) c := controller.NewActionsController(service) <-----controllerパッケージからメソッドを呼ぶ app.MountActionsController(service, c) if err := service.ListenAndServe( ":8080" ); err != nil { service.LogError( "startup" , "err" , err) } }

これで準備完了しました。ここまでの作業は正直テンプレートなので、慣れればほとんど時間がかかりません。

ビジネスロジックを組んでみよう

package controller import ( "github.com/goadesign/goa" "github.com/tikasan/goa-stater/app" ) type ActionsController struct { *goa.Controller } func NewActionsController(service *goa.Service) *ActionsController { return &ActionsController{Controller: service.NewController( "ActionsController" )} } func (c *ActionsController) ID(ctx *app.IDActionsContext) error { if ctx.ID == 0 { return ctx.NotFound() } res := &app.Integer{} res.ID = ctx.ID return ctx.OK(res) } func (c *ActionsController) Hello(ctx *app.HelloActionsContext) error { name := ctx.Name res := &app.Message{} res.Message = "Hello " + name return ctx.OK(res) } func (c *ActionsController) Ping(ctx *app.PingActionsContext) error { message := "pong" res := &app.Message{} res.Message = message return ctx.OK(res) }

以上でAPIのビジネスロジックの実装完了です。簡単でしょう？

では、実行してみましょう。

go run main.go

エラーが起きた場合は、importミスやポートが空いてないや何かしらのミスが考えられます。

curl -v http://localhost:8080/api/v1/actions/1 * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > GET /api/v1/actions/1 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/vnd.integer+json < Date: Fri, 05 May 2017 11:49:59 GMT < Content-Length: 9 < {"id":1} * Connection #0 to host localhost left intact

上記のような感じでレスポンスが返ってきました。

試しに間違ったリクエストを投げてみましょう。

curl -v http://localhost:8080/api/v1/actions/hoge * Trying ::1... * Connected to localhost (::1) port 8080 (#0) > GET /api/v1/actions/hoge HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 400 Bad Request < Content-Type: application/vnd.goa.error < Date: Fri, 05 May 2017 12:01:54 GMT < Content-Length: 188 < {"id":"Iwzpk08B","code":"invalid_request","status":400,"detail":"invalid value \"hoge\" for parameter \"id\", must be a integer","meta":{"expected":"integer","param":"ID","value":"hoge"}} * Connection #0 to host localhost left intact

エラーのフォーマットはカスタム出来ますが、いまはデフォルトで定義されているフォーマットで返ってきます。特にこだわりがなければこれで完了します。

SwaggerUIを使ってみよう

swaggerのドキュメントも出来上がっているので、 github.com/tikasan/goa-stater を使っている場合は、以下のコマンドでAPIのドキュメントの参照とテストが出来ます！！！

open http://localhost:8080/swagger/index.html

初めの方に説明した。ビズネスロジックだけを書けば実装が完了していることが実感出来たと思います。他にもヘッダーのチェックやmodel定義が簡単に出来るgormaなど色々あるので、どんどん紹介していきたいと思います。