Goa(2) RESTful microservices - KerwinKoo/KerwinKoo.github.io GitHub Wiki
首先目录文件一定要创建在$GOPATH/src下,假设做一个cellar服务,首先创建目录$GOPATH/src/cellar, 然后在此目录下创建子文件夹design和文件design/design.go。
在文件design/design.go中写入代码:
package design
import (
. "github.com/raphael/goa/design"
. "github.com/raphael/goa/design/dsl"
)
var _ = API("cellar", func() {
Title("The virtual wine cellar")
Description("A basic example of an API implemented with goa")
Scheme("http")
Host("localhost:8080")
})
var _ = Resource("bottle", func() {
BasePath("/bottles")
DefaultMedia(BottleMedia)
Action("show", func() {
Description("Retrieve bottle with given id")
Routing(GET("/:bottleID"))
Params(func() {
Param("bottleID", Integer, "Bottle ID")
})
Response(OK)
Response(NotFound)
})
})
var BottleMedia = MediaType("application/vnd.goa.example.bottle+json", func() {
Description("A bottle of wine")
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")
})
View("default", func() {
Attribute("id")
Attribute("href")
Attribute("name")
})
})-
design包名不限制,什么名字都可。 -
API函数,用于声明API,第一个参数为API名字,第二个参数是匿名函数,用于定义该API的基本属性。 -
Resource用于声明服务资源(resource),因为Goa是RESTful设计理念,source在REST中囊括了API所能访问的所有属性,参数即地址,地址亦参数,http的GET、POST等也属于资源范畴。因此函数Resource是Goa设计对象的主体。在上面的代码示例中,函数Resource用于声明一个名为“bottle”的资源,同样的,在匿名函数中对该资源进行属性、行为声明。 -
在
Resource函数中,Action可以定义该资源所提供的行为,Action函数必须在Resource函数中定义。Action同样是一个行为名称+一个匿名函数,在匿名函数中声明了该行为的描述、HTTP method、参数及返回值等属性。其中,HTTP method(代码中的Routing声明)中的路径需使用通配符(:变量名)。承接参数类型定义使用Params,如果该Action不止一个参数,需要多次调用Params对每个参数进行承接声明。同一个Resource函数中,可以定义多个Action函数。 -
Goa用代码描述设计,用设计生成代码。Goa设计的最终端点是Action,并通过函数Action描述。Goa定义了基本的response模板,这里默认返回OK(200)。 -
当资源访问成功后,可以将访问变量提交给我们最后定义的
Media,Media通过函数MediaType定义,Media的结构组成,则是匿名函数Attributes中定义。
通过goagen将设计生成代码:
goagen bootstrap -d cellar/design
在当前工作目录下会生成如下文件结构:
app
app/contexts.go
app/controllers.go
app/hrefs.go
app/media_types.go
app/user_types.go
main.go
bottle.go
client
client/cellar-cli
client/cellar-cli/main.go
client/cellar-cli/commands.go
client/client.go
client/bottle.go
swagger
swagger/swagger.json
swagger/swagger.go
bottle.go为主结构代码文件。当之后再做设计修改,运行goagen工具后,该文件不会改变,只改变app、client和swagger下的文件。通过goagen命令行参数可以指定app名定向生成代码。
-
app目录中的代码是API的胶水代码(脚手架代码),用于实现HTTP底层功能,包括信息接收管理,信息反馈模板,底层路由分发,以及goa定义的媒介结构体。如果没有特殊要求,这个目录下的代码文件系自动生成,不需要认为修改。 -
client包含一个客户端的简单实现,支持命令行参数,可以进行简单的server request测试。 -
swagger目录中存放swagger规范的API说明文件(swagger.json),通过访问swagger可以查看API说明。 -
主目录src下的
main.go和bottle.go实现简单的server。
-
controllers.go:控制接口。在设计过程中,每一个resource都有一个接口定义。例子中的接口定义如下:
// BottleController is the controller interface for the Bottle actions.
type BottleController interface {
goa.Controller
Show(*ShowBottleContext) error
}因此接口需要一个goa.Controller,需要实现Show(*ShowBottleContext) error函数,后者在bottle.go中实现。
controllers.go还实现了向server mount的功能,将其自身API挂载到server中。
-
contexts.go:内容结构体的实现。 -
hrefs.go:提供资源信息描述功能。 -
media_types.go:实现应答数据结构体类型的定义。 -
user_types.go:定义通过 "Type" 设计的结构体。
仍旧是上面代码的例子。Goa已经实现了一个基本且功能全面的框架,剩下的需要我们实现bottle控制器。回到之前介绍的BottleController接口(定义在controllers.go文件中)和ShowBottleContext结构体(定义在app/contexts.go文件中):
// BottleController is the controller interface for the Bottle actions.
type BottleController interface {
goa.Controller
Show(*ShowBottleContext) error
}// ShowBottleContext provides the bottle show action context.
type ShowBottleContext struct {
*goa.Context
BottleID int
}其中BottleID int是我们在之前design时设定的。
次文件包含另外两个函数的实现:
// NotFound sends a HTTP response with status code 404.
func (ctx *ShowBottleContext) NotFound() error {
return ctx.RespondBytes(404, nil)
}
// OK sends a HTTP response with status code 200.
func (ctx *ShowBottleContext) OK(resp *GoaExampleBottle) error {
r, err := resp.Dump()
if err != nil {
return fmt.Errorf("invalid response: %s", err)
}
ctx.Header().Set("Content-Type", "application/vnd.goa.example.bottle+json; charset=utf-8")
return ctx.Respond(200, r)
}上面两个函数仍是Template,供后续开发或bottle.go文件调用。
利用上面的基础框架,我们需要实现文件bottle.go的具体功能。当前的API访问时仅返回一个200,我们需要根据具体业务来实现API功能函数。
用以下代码替换show方法(注意引用fmt包):
// 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
bottleID := fmt.Sprintf("Bottle #%d", ctx.BottleID)
bottleHref := app.BottleHref(ctx.BottleID)
bottle := app.GoaExampleBottle{
ID: &ctx.BottleID,
Name: &bottleID,
Href: &bottleHref,
}
// Let the generated code produce the HTTP response using the
// media type described in the design (BottleMedia).
return ctx.OK(&bottle)
}上面代码与官方示例代码略有不同,但本质相同。官方代码有赋值错误,将value赋值给了point,因此我做了过程上的微小处理。
API实现后,会在main.go中进行挂载。
注意:main中进行挂载的API不能有路由冲突,比如API资源/foo/:id和/foo/:fooID/bar/:id不能同时存在,需要将第一个修改为/foo/:fooID
在当前目录进行编译并启动server:
go build -o cellar_server
./cellar_server
官方示例中是go build -o cellar,但生成过程中本身已存在cellar目录,所有会出现编译错误。这里改成cellar_server。
编译过程中出现其他问题,移步到我上一篇关于Goa的日志。
运行后,打印启动日志:
INFO[01-21|18:22:11] mount app=API ctrl=Bottle action=Show route="GET /bottles/:bottleID"
INFO[01-21|18:22:11] mount app=API file=swagger/swagger.json route="GET /swagger.json"
INFO[01-21|18:22:11] listen app=API addr=:8080
从日志中看,路由中挂载了两个API,分别为Bottle和swagger,方法均为GET。
可以通过curl工具进行测试:
curl -i localhost:8080/bottles/1234
curl -i localhost:8080/bottles/0
第二个是测试之前我们实现的功能:
if ctx.BottleID == 0 {
// Emulate a missing record with ID 0
return ctx.NotFound()
}swagger测试:
curl -i localhost:8080/swagger.json
Goa同时生成CLI测试客户端:
$ cd client/cellar-cli
$ go build -o cellar-cli
$ ./cellar-cli --dump show bottle /bottles/1234
效果与curl大体相同,多了一个href的显示:
{"href":"/bottles/1234","id":1234,"name":"Bottle #1234"}
通过这个例子,可以体会Goa以设计为基础的HTTP API开发。