goa v2.0.0-wip 事始め

1. golangはじめました(趣味で)

3〜4週間前から趣味でgolangを始めてみました。
ということでやっと基礎学習も出来つつあり、色々試し始められたので、たまにブログっていければと思います。
唐突ですが goa というものに興味を持って使ってみたので、その辺のお話を取り上げたいと思います。

また、本投稿の動作確認に使った環境は以下のとおりです。
* macOS High Sierra 10.13.6
* go 1.10.3
* $GOPATH=/Users/daigo/go

2. goaとは

goaとは、APIを作る際 DSLによりデザインファイルを記述すると そこから自動で実装の雛形やドキュメント(swaggerファイル)を生成してくれるマイクロサービス用フレームワークです。
私もそうだったのですが、DSLって聞くと、また新しい記法を覚えなくちゃいけないのかぁ・・・と思いますが、goaのDSLgolangで記述することができるので、敷居はだいぶ低くなっていると思います。まあ、golangによるgoaの記法を覚える必要はありますが(汗
ということで、goaよりも以前からある swagger からの流れと同じく、以下のような問題の解決を図るフレームワークとなります。

  • いきなりAPI書くんじゃなくてきちんと設計しようよ
  • でもドキュメント(設計書)書くのめんどいよね(トラディショナルカンパニーのExcel設計書とかクソだよね)
  • バックエンドAPI開発者とフロントエンド開発者が分離したりしてくると仕様の齟齬は避けたいよね
  • ドキュメント書いてもコードのバージョンアップにきちんと追随したいよね(労力最小限で)

v1とv2

golang本体もそうですが、周辺フレームワークのバージョンアップもまだまだかなり激しいですよね。
goaのバージョンは2018/8/19時点で、「stableバージョン=v1.4.0」「次のメジャーバージョン=v2.0.0-wip」です。
v2はまだwipな状態ですが、github上で以下のように記述されている通り、既に結構作り込まれているのではないかと思います。

goa v2 is currently in beta: it is robust enough to be used in production but there may still be breaking changes before the final release. If you're new to goa then you may want to consider starting with v2.

v1.x系とv2.0ではDSLの記法、ツールコマンドの使い方が変わっているので改めて学習する必要があります。とはいえ、向かっている思想は同じなのでスムーズに移行できるのではないかと思います。(既存コードのMigrationは難しい?その観点では調べてないので?はてな?です)

goaのgihhubは以下(v1.x)で↓↓↓

github.com

その中にv2ブランチが切られています。↓↓↓

github.com

3. goa v2をインストール

goa v2のインストールは以下のコマンドでいけます。

$ go get -u goa.design/goa/...

インストールの確認は「goa version」コマンドで行えます。

$ goa version
goa version v2.0.0-wip

4. goa v2を使ってみる

公式github上のドキュメントトップでも説明されているレベルの「はじめてのgoa v2」的なサンプルを作っていこうと思います。
サンプル仕様はDBもセッションも使わず完全ステートレスなWebAPIとします。

ガチャサービスとして以下の2つのメソッドを実装します。
* ガチャを回す(GETリクエスト、URLパラメータでガチャタイプをIntで指定)
 → GET http://localhost:8080/{type}
 → ガチャ結果はStringで返却
* チートガチャを回す(POSTリクエスト)
 → POST http://localhost:8080/ POSTデータ=secret_key(string)
 → ガチャ結果はStringで返却

4.1. プロジェクトの作成

gachaプロジェクトを作成します。
(ひとまずdepも使わないし、ディレクトリ掘るだけ)

$ cd $GOPATH/src
$ mkdir gacha
$ cd gacha

結局、この段階で ↓ にいる感じです。

$ pwd
/Users/daigo/go/src/helloGoa/gacha

4.2. designファイルの定義

goaではdesignファイルを用意して、そこにgolangAPI仕様を記述します。

$ mkdir design
$ vi design/design.go
// design/design.go

package design

import . "goa.design/goa/http/design"
import . "goa.design/goa/http/dsl"

var _ = API("gacha", func() {
    Title("gacha service")
    Description("サンプルのガチャサービスです。")
    Server("http://localhost:8080")
})

var _ = Service("gachasvc", func() {
    Method("pon", func() {
        Description("ガチャを回します")
        Payload(func() {
            Attribute("type", Int, "gacha type")
        })
        Result(String)
        HTTP(func() {
            GET("/pon/{type}")
        })
    })

    Method("cheat_pon", func() {
        Description("チートガチャを回します")
        Payload(func() {
            Attribute("secret_key", String, "秘密の鍵")
        })
        Result(String)
        HTTP(func() {
            POST("/pon/cheat/")
        })
    })
})

API

APIはトップレベDSLです。
APIのタイトルやホスト名バージョン等の記述を行います。
指定できる仕様の詳細は公式Docにありますが、ここでは省略します。

Service / Method

サービスDSLは、メソッドを定義します。
なんとなく眺めると理解できると思いますが、「pon」と「cheart_pon」の2つのメソッドが定義されています。
descriptionは、そのままメソッドの説明ですね。
Payloadは、メソッドが受け取るペイロード(引数)の定義です。サンプルではそれぞれのメソッドが1つづつ受け取っていますが、複数の場合はAttribute()定義を並べて定義します。
Resultは、メソッドの結果の戻り値の型定義になります。
HTTPは、メソッドをHTTPでホストすること、そしてHTTP Verb(GET/POST等)とルーティングURLを定義しています。
Payload/ResultとHTTPが分離されているところが重要で、「メソッド名/INの引数/OUTの戻り値の定義」と@トランスポートのプロトコル」は分離して考えることが出来ます。例えば、HTTPではなくgRPCでホストする定義に簡単に書き換えることが出来ます(今日段階ではgRPCはgoa v2で対応していない?)。

4.3. コード自動生成

では、goaのDSL定義(design.go)からコードの自動生成を行います。
「goa gen gacha/design」コマンドを実行します。
gacha/designの部分は $GOPATH/src からの相対パスでdesign.goが定義されているパスを指定します。

$ goa gen gacha/design                                                                                                               [~/go/src/gacha]

gen/gachasvc/client.go
gen/gachasvc/endpoints.go
gen/gachasvc/service.go
gen/http/cli/cli.go
gen/http/gachasvc/client/cli.go
gen/http/gachasvc/client/client.go
gen/http/gachasvc/client/encode_decode.go
gen/http/gachasvc/client/paths.go
gen/http/gachasvc/client/types.go
gen/http/gachasvc/server/encode_decode.go
gen/http/gachasvc/server/paths.go
gen/http/gachasvc/server/server.go
gen/http/gachasvc/server/types.go
gen/http/openapi.json
gen/http/openapi.yaml

15ファイルほど自動生成されました。
サービスエンドポイントやHTTPトランスポートレイヤの実装などが自動生成されています。
swaggerファイル(openapi.json / openapi.yaml)も自動生成されています。

openapi.jsonは以下のような定義になっています。

# 自動生成された openapi.json
{
  "swagger": "2.0",
  "info": {
    "title": "gacha service",
    "description": "サンプルのガチャサービスです。",
    "version": ""
  },
  "host": "localhost:8080",
  "paths": {
    "/pon/cheat": {
      "post": {
        "tags": [
          "gachasvc"
        ],
        "summary": "cheat_pon gachasvc",
        "description": "チートガチャを回します",
        "operationId": "gachasvc#cheat_pon",
        "parameters": [
          {
            "name": "CheatPonRequestBody",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/GachasvcCheatPonRequestBody"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK response.",
            "schema": {
              "type": "string"
            }
          }
        },
        "schemes": [
          "http"
        ]
      }
    },
    "/pon/{type}": {
      "get": {
        "tags": [
          "gachasvc"
        ],
        "summary": "pon gachasvc",
        "description": "ガチャを回します",
        "operationId": "gachasvc#pon",
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "description": "ガチャタイプ",
            "required": true,
            "type": "integer"
          }
        ],
        "responses": {
          "200": {
            "description": "OK response.",
            "schema": {
              "type": "string"
            }
          }
        },
        "schemes": [
          "http"
        ]
      }
    }
  },
  "definitions": {
    "GachasvcCheatPonRequestBody": {
      "title": "GachasvcCheatPonRequestBody",
      "type": "object",
      "properties": {
        "secret_key": {
          "type": "string",
          "description": "秘密の鍵",
          "example": "Qui iste laboriosam quis ducimus fugiat qui."
        }
      },
      "example": {
        "secret_key": "Accusamus iste."
      }
    }
  }
}

「goa gen」コマンドではサービス自体のドメインロジックの実装部や、サーバとしてホストするロジックが生成されていません。
これらを実装する出発点となる実装の生成は「goa example gacha/design」コマンドで行うことが出来ます。

$ goa example gacha/design

cmd/gacha_cli/main.go
cmd/gacha_svc/main.go
gachasvc.go

3つのgoファイルが生成されました。

cmd/gacha_cli/main.go

APIメソッドを呼び出すテスト用CLIコードです。

cmd/gacha_svc/main.go

HTTPでサービスをホストする実装コードです。ログ出力等含めた実装になるので、ここからカスタマイズして自分の実装に持っていってもいいと思います。

gachasvc.go

ガチャサービスの実装雛形です。 ここに自分のドメインロジックの実装を行います。

// 自動生成された gachasvc.go

package gacha

import (
    "context"
    gachasvc "gacha/gen/gachasvc"
    "log"
)

// gachasvc service example implementation.
// The example methods log the requests and return zero values.
type gachasvcSvc struct {
    logger *log.Logger
}

// NewGachasvc returns the gachasvc service implementation.
func NewGachasvc(logger *log.Logger) gachasvc.Service {
    return &gachasvcSvc{logger}
}

// ガチャを回します
func (s *gachasvcSvc) Pon(ctx context.Context, p *gachasvc.PonPayload) (res string, err error) {
    s.logger.Print("gachasvc.pon")
    return
}

// チートガチャを回します
func (s *gachasvcSvc) CheatPon(ctx context.Context, p *gachasvc.CheatPonPayload) (res string, err error) {
    s.logger.Print("gachasvc.cheat_pon")
    return
}

上記の Pon() / CheatPoin() の中身を実装すればいいですね。
自動生成されたコードでは、ログ出力と空文字のreturnが実装されています。

4.4. ドメインロジックの実装

では、Pon() / CheatPoin()の2つのメソッドに対する、面白みのない実装は以下のとおりです。
(ベタに読めるように、べた書きしています。)
乱数を発生させて確率で「SS激レア・Sレア・レア」の結果文字列を返却しています。
パラメータは *gachasvc.PonPayload ポインタ型として引数で受け取れるので * p.Type みたいに参照することが出来ます。(ポインタなので * を忘れずに)

// gachasvc.goに実装を追加

// ガチャを回します
func (s *gachasvcSvc) Pon(ctx context.Context, p *gachasvc.PonPayload) (res string, err error) {
    s.logger.Print("gachasvc.pon")

    rand.Seed(time.Now().UnixNano())
    r := rand.Intn(10)

    if *p.Type == 0 { // ノーマルガチャ
        if r == 9 {
            return "SS激レア", nil
        } else if r > 6 {
            return "Sレア", nil
        }
    } else if *p.Type == 1 { // 高確率ガチャ
        if r == 7 {
            return "SS激レア", nil
        } else if r > 3 {
            return "Sレア", nil
        }
    }
    return "レア", nil
}

// チートガチャを回します
func (s *gachasvcSvc) CheatPon(ctx context.Context, p *gachasvc.CheatPonPayload) (res string, err error) {
    s.logger.Print("gachasvc.cheat_pon")

    if *p.SecretKey == "神の手" {
        return "SS激レア", nil
    }

    // 不正チート。垢BAN処理!!!!

    return "ノーマル", nil
}

4.5. ビルド&実行

ではビルドして実行しましょう。
まず、プロジェクトルート($GOPATH/src/gacha)で「go build」コマンドを実行します。

$ cd $GOPATH/src/gacha
$ go build

サーバ(サービス)の実行

次にサーバ(サービス)実装をビルドします。
$GOPATH/src/gacha/cmd/gacha_svcディレクトリで「go build」コマンドを実行します。

$ cd $GOPATH/src/gacha/cmd/gacha_svc
$ go build

lsすると gacha_svc が生成されたのを確認できます。

$ ls -la 
total 19528
drwxr-xr-x  4 daigo  staff      128  8 19 12:58 .
drwxr-xr-x  4 daigo  staff      128  8 19 12:01 ..
-rwxr-xr-x  1 daigo  staff  9990876  8 19 13:10 gacha_svc
-rw-r--r--  1 daigo  staff     4073  8 19 12:01 main.go

サービスを実行します。

$ ./gacha_svc

[gacha] 13:13:09 method "Pon" mounted on GET /pon/{type}
[gacha] 13:13:09 method "CheatPon" mounted on POST /pon/cheat
[gacha] 13:13:09 listening on :8080

OK。8080ポートでリスニングしてくれています。

クライント(CLI)の実行

ブラウザやPostmanなどからサービスを叩くことも出来ますが、goa example がサーバ(サービス)を叩くクライントCLIも自動生成してくれています。
もう1つターミナルを開いて、「$GOPATH/src/gacha/cmd/gacha_cliディレクトリで「go build」コマンドを実行します。

$ cd $GOPATH/src/gacha/cmd/gacha_cli
$ go build

lsすると gacha_cli が生成されたのを確認できます。

$ ls -la 
total 18000
drwxr-xr-x  4 daigo  staff      128  8 19 12:58 .
drwxr-xr-x  4 daigo  staff      128  8 19 12:01 ..
-rwxr-xr-x  1 daigo  staff  9211836  8 19 13:17 gacha_cli
-rw-r--r--  1 daigo  staff     2285  8 19 12:01 main.go

「./gacha_cli --help」で使い方が表示されます。

./gacha_cli is a command line client for the gacha API.

Usage:
    ./gacha_cli [-url URL][-timeout SECONDS][-verbose|-v] SERVICE ENDPOINT [flags]

    -url URL:    specify service URL (http://localhost:8080)
    -timeout:    maximum number of seconds to wait for response (30)
    -verbose|-v: print request and response details (false)

Commands:
    gachasvc (pon|cheat-pon)
    
Additional help:
    ./gacha_cli SERVICE [ENDPOINT] --help

Example:
    ./gacha_cli gachasvc pon --type 2753046654881133159

/pon/{type}への GET リクエストは以下の通り。

$ ./gacha_cli gachasvc pon --type 0

"Sレア"

/pon/cheat/への POST リクエストは以下の通り。

$ ./gacha_cli gachasvc cheat-pon --body '{
     "secret_key": "神の手"         
  }'
 
"SS激レア"

5. まとめ

ということで goa の基本的な利用の流れの紹介でした。
記事中では十分に紹介しきれていませんが、改めてgoaを使うメリットは以下のようなものがあります。

  • なるべく少ないコストで、API設計ドキュメントを作成・メンテする事ができる(goa DSL -> swagger doc)
  • APIメソッドの入力パラメータ仕様に基づいてgoa側(+生成されたコード)がバリデーション+パラメータエンティティオブジェクトへの割当を行ってくれる
  • つまり開発者は、ドメイン領域へのコーディングに集中することができる

ということで、本ブログでは初めてgolang関連を取り上げましたが、私の所属会社ではメインのRubyに続いてgolangが最近元気なようなので、引き続きgolangに(趣味で)取り組んで行こうと思うので引き続きブログっていきたいと思います。