goa
マイクロサービス開発を支援するフレームワーク
github
https://github.com/goadesign/goa
日本語doc
https://goa.design/ja/
install
依存管理はdep
で行いました
$ brew install dep
$ dep
dep is a tool for managing dependencies for Go projects
Usage: dep <command>
Commands:
init Initialize a new project with manifest and lock files
status Report the status of the project's dependencies
ensure Ensure a dependency is safely vendored in the project
prune Prune the vendor tree of unused packages
Examples:
dep init set up a new project
dep ensure install the project's dependencies
dep ensure -update update the locked versions of all dependencies
dep ensure github.com/pkg/errors add a dependency to the project
Use "dep help [command]" for more information about a command.
# 設定ファイル作成
$ dep init
goaの追加
goファイルがないと下記のように怒られるので気をつけてください
私はひとまずmain.goを作成することで解決しました
$ dep ensure -add github.com/goadesign/goa/goagen
all dirs lacked any go code
使ってみる
チュートリアルの作成物としては
ワインボトルを扱うセラーサービスをAPIとして提供する
goaデザイン言語を書く
design/design.goの作成
package design
import (
. "github.com/goadesign/goa/design" // Use . imports to enable the DSL
. "github.com/goadesign/goa/design/apidsl"
)
// API defines the microservice endpoint and
// other global properties. There should be one
// and exactly one API definition appearing in the design.
var _ = API("cellar", func() {
Title("The virtual wine cellar")
Description("A simple goa service")
Scheme("http")
Host("localhost:8080")
})
// Resources group related API endpoints
// together. They map to REST resources for REST services.
var _ = Resource("bottle", func() {
BasePath("/bottles")
DefaultMedia(BottleMedia)
// Actions define a single API endpoint together
// with its path, parameters (both path
// parameters and querystring values) and payload
// (shape of the request body).
Action("show", func() {
Description("Get bottle by id")
Routing(GET("/:bottleID"))
Params(func() {
Param("bottleID", Integer, "Bottle ID")
})
// Responses define the shape and status code of HTTP responses.
Response(OK)
Response(NotFound)
})
})
// BottleMedia defines the media type used to render bottles.
var BottleMedia = MediaType("application/vnd.goa.example.bottle+json", func() {
Description("A bottle of wine")
// Attributes define the media type shape.
Attributes(func() {
Attribute("id", Integer, "Unique bottle ID")
Attribute("href", String, "API href for making requests on the bottle")
Attribute("name", String, "Name of wine")
Required("id", "href", "name")
})
// View defines a rendering of the media type.
// Media types may have multiple views and must
// have a "default" view.
View("default", func() {
Attribute("id")
Attribute("href")
Attribute("name")
})
})
-
MediaTypeの第一引数には任意の値を入れる
-
デフォではjsonになる
-
default viewの設定は必須
-
Attributesの中にはMediaType内で使われる可能性のあるものを定義する
-
グローバルに置いているMediaTypeは__OK__レスポンスのときに参照される
codeを生成する
depで入れた場合は、goagenをbuildする必要がある
$ cd vendor/github.com/goadesign/goa/goagen
$ go build # goagenが作られる
$ cd -
code生成
$ ./vendor/github.com/goadesign/goa/goagen/goagen bootstrap -d path/design
app
app/contexts.go
app/controllers.go
app/hrefs.go
app/media_types.go
app/user_types.go
app/test
app/test/bottle_testing.go
main.go
bottle.go
tool/cellar-cli
tool/cellar-cli/main.go
tool/cli
tool/cli/commands.go
client
client/client.go
client/bottle.go
client/user_types.go
client/media_types.go
swagger
swagger/swagger.json
swagger/swagger.yaml
__path/__は、$GOPATH/src/__path__だと考えてください
作成されたものについて
main.go
はapiのエンドポイント
bottle.go
はbottleリソースに対応するコントローラ
上記は、既に存在する場合には再生成されない
コントローラであるbottle.goにロジックを実装すれば完成する
package main
import (
"github.com/goadesign/goa"
"project/goa-tutorial/app"
)
// BottleController implements the bottle resource.
type BottleController struct {
*goa.Controller
}
// NewBottleController creates a bottle controller.
func NewBottleController(service *goa.Service) *BottleController {
return &BottleController{Controller: service.NewController("BottleController")}
}
// Show runs the show action.
func (c *BottleController) Show(ctx *app.ShowBottleContext) error {
//ここにロジックを実装する
res := &app.GoaExampleBottle{}
return ctx.OK(res)
}
BottleControllerはmain.goのapp.MountBottleController()でmountされている
package main
import (
"github.com/goadesign/goa"
"github.com/goadesign/goa/middleware"
"project/goa-tutorial/app"
)
func main() {
// Create service
service := goa.New("cellar")
// Mount middleware
service.Use(middleware.RequestID())
service.Use(middleware.LogRequest(true))
service.Use(middleware.ErrorHandler(service, true))
service.Use(middleware.Recover())
// Mount "bottle" controller
c := NewBottleController(service)
app.MountBottleController(service, c)
// Start service
if err := service.ListenAndServe(":8080"); err != nil {
service.LogError("startup", "err", err)
}
}
app.MountBottleController()にルーティングが記載されている
package app
import (
"context"
"github.com/goadesign/goa"
"net/http"
)
// initService sets up the service encoders, decoders and mux.
func initService(service *goa.Service) {
// Setup encoders and decoders
service.Encoder.Register(goa.NewJSONEncoder, "application/json")
service.Encoder.Register(goa.NewGobEncoder, "application/gob", "application/x-gob")
service.Encoder.Register(goa.NewXMLEncoder, "application/xml")
service.Decoder.Register(goa.NewJSONDecoder, "application/json")
service.Decoder.Register(goa.NewGobDecoder, "application/gob", "application/x-gob")
service.Decoder.Register(goa.NewXMLDecoder, "application/xml")
// Setup default encoder and decoder
service.Encoder.Register(goa.NewJSONEncoder, "*/*")
service.Decoder.Register(goa.NewJSONDecoder, "*/*")
}
// BottleController is the controller interface for the Bottle actions.
type BottleController interface {
goa.Muxer
Show(*ShowBottleContext) error
}
// MountBottleController "mounts" a Bottle resource controller on the given service.
func MountBottleController(service *goa.Service, ctrl BottleController) {
initService(service)
var h goa.Handler
h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
// Check if there was an error loading the request
if err := goa.ContextError(ctx); err != nil {
return err
}
// Build the context
rctx, err := NewShowBottleContext(ctx, req, service)
if err != nil {
return err
}
return ctrl.Show(rctx)
}
service.Mux.Handle("GET", "/bottles/:bottleID", ctrl.MuxHandler("Show", h, nil))
service.LogInfo("mount", "ctrl", "Bottle", "action", "Show", "route", "GET /bottles/:bottleID")
}
BottleControllerのShowメソッドが受け取るcontextの実装は下記
// Show runs the show action.
func (c *BottleController) Show(ctx *app.ShowBottleContext) error {
//ここにロジックを実装する
res := &app.GoaExampleBottle{}
return ctx.OK(res)
}
package app
import (
"context"
"github.com/goadesign/goa"
"net/http"
"strconv"
)
// ShowBottleContext provides the bottle show action context.
type ShowBottleContext struct {
context.Context
*goa.ResponseData
*goa.RequestData
BottleID int
}
// NewShowBottleContext parses the incoming request URL and body, performs validations and creates the
// context used by the bottle controller show action.
func NewShowBottleContext(ctx context.Context, r *http.Request, service *goa.Service) (*ShowBottleContext, error) {
var err error
resp := goa.ContextResponse(ctx)
resp.Service = service
req := goa.ContextRequest(ctx)
req.Request = r
rctx := ShowBottleContext{Context: ctx, ResponseData: resp, RequestData: req}
paramBottleID := req.Params["bottleID"]
if len(paramBottleID) > 0 {
rawBottleID := paramBottleID[0]
if bottleID, err2 := strconv.Atoi(rawBottleID); err2 == nil {
rctx.BottleID = bottleID
} else {
err = goa.MergeErrors(err, goa.InvalidParamTypeError("bottleID", rawBottleID, "integer"))
}
}
return &rctx, err
}
// OK sends a HTTP response with status code 200.
func (ctx *ShowBottleContext) OK(r *GoaExampleBottle) error {
ctx.ResponseData.Header().Set("Content-Type", "application/vnd.goa.example.bottle+json")
return ctx.ResponseData.Service.Send(ctx.Context, 200, r)
}
// NotFound sends a HTTP response with status code 404.
func (ctx *ShowBottleContext) NotFound() error {
ctx.ResponseData.WriteHeader(404)
return nil
}
OKとNotFoundのようにcontextがレスポンス結果の振る舞いを持つようになっている
コントローラを仮に実装して動かしてみる
// Show implements the "show" action of the "bottles" controller.
func (c *BottleController) Show(ctx *app.ShowBottleContext) error {
if ctx.BottleID == 0 {
// Emulate a missing record with ID 0
return ctx.NotFound()
}
// Build the resource using the generated data structure
bottle := app.GoaExampleBottle{
ID: ctx.BottleID,
Name: fmt.Sprintf("Bottle #%d", ctx.BottleID),
Href: app.BottleHref(ctx.BottleID),
}
// Let the generated code produce the HTTP response using the
// media type described in the design (BottleMedia).
return ctx.OK(&bottle)
}
ビルドして実行
$ go build -o cellar
$ ./celler
2017/08/20 00:10:26 [INFO] mount ctrl=Bottle action=Show route=GET /bottles/:bottleID
2017/08/20 00:10:26 [INFO] listen transport=http addr=:8080
・
・
・
リクエストのテスト
$ curl -i localhost:8080/bottles/1
HTTP/1.1 200 OK
Content-Type: application/vnd.goa.example.bottle+json
Date: Sat, 19 Aug 2017 16:55:47 GMT
Content-Length: 48
{"href":"/bottles/1","id":1,"name":"Bottle #1"}
不正なID(非整数)のバリデーションテスト(goagenによって生成されている)
$ curl -i localhost:8080/bottles/n
HTTP/1.1 400 Bad Request
Content-Type: application/vnd.goa.error
Date: Sat, 19 Aug 2017 16:58:41 GMT
Content-Length: 194
{"id":"n2OmVVJH","code":"invalid_request","status":400,"detail":"invalid value \"n\" for parameter \"bottleID\", must be a integer","meta":{"expected":"integer","param":"bottleID","value":"n"}}
該当コード
func NewShowBottleContext(ctx context.Context, r *http.Request, service *goa.Service) (*ShowBottleContext, error) {
var err error
resp := goa.ContextResponse(ctx)
resp.Service = service
req := goa.ContextRequest(ctx)
req.Request = r
rctx := ShowBottleContext{Context: ctx, ResponseData: resp, RequestData: req}
paramBottleID := req.Params["bottleID"]
if len(paramBottleID) > 0 {
rawBottleID := paramBottleID[0]
if bottleID, err2 := strconv.Atoi(rawBottleID); err2 == nil {
rctx.BottleID = bottleID
} else {
err = goa.MergeErrors(err, goa.InvalidParamTypeError("bottleID", rawBottleID, "integer"))
}
}
return &rctx, err
}
curlの代わりに生成されているCLIツールを使う
サーバーは起動しておいてください
$ cd tool/cellar-cli
$ go build -o cellar-cli
$ ./cellar-cli
$ ./cellar-cli show bottle /bottles/1
2017/08/20 02:05:55 [INFO] started id=6Q+3ari3 GET=http://localhost:8080/bottles/1
2017/08/20 02:05:55 [INFO] completed id=6Q+3ari3 status=200 time=5.34273ms
{"href":"/bottles/1","id":1,"name":"Bottle #1"}
cliツールは該当APIのhelpにもアクセスできます
$ ./cellar-cli show bottle --help
Usage:
cellar-cli show bottle ["/bottles/BOTTLEID"] [flags]
Flags:
--bottleID int Bottle ID
-h, --help help for bottle
--pp Pretty print response body
Global Flags:
--dump Dump HTTP request and response.
-H, --host string API hostname (default "localhost:8080")
-s, --scheme string Set the requests scheme
-t, --timeout duration Set the request timeout (default 20s)
感想
goa wayとでもいうのでしょうか
コードが自動生成されて、それを使うっていうのは現状抵抗ありますが
開発が早くなってドキュメントも残るのであれば慣れていきたいなーと思いました
もう少しAPIを増やして使いづらい点や良い点を見つけていければと
frameworkはいつ切り捨てられるかわからないので
なるべく薄くてコミッターが多いものを使っていきたいです