goa-4-使用积累 | KaiQ.Gu|KerwinKoo Blog

JerryXia 发表于 , 阅读 (0)

Goa日志

Goa默认日志

Goa默认日志是基于[log15 package](https://godoc.org/gopkg.in/inconshreveable/log15.v2)。

Goa默认的日志格式固定,如测试代码的mount日志:

INFO[01-25|09:56:34] mount      app=API ctrl=Operands action=Add route="GET /add/:left/:right"

依次为:

  • 日志类型
  • 日志时间
  • 行为名称
  • 日志内容

其中日志内容是以key=value的形式存在。

Logger接口的实现写在包gopkg.in/inconshreveable/log15.v2,Goa将其重命名为log:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// A Logger writes key/value pairs to a Handler
type Logger interface {
// New returns a new Logger that has this logger's context plus the given context
New(ctx ...interface{}) Logger

// SetHandler updates the logger to write records to the specified handler.
SetHandler(h Handler)

// Log a message at the given level with context key/value pairs
Debug(msg string, ctx ...interface{})
Info(msg string, ctx ...interface{})
Warn(msg string, ctx ...interface{})
Error(msg string, ctx ...interface{})
Crit(msg string, ctx ...interface{})
}

具体调用方法为:

1
service.Info("mount", "ctrl", "Operands", "action", "Add", "route", "GET /add/:left/:right")

第一个是日志的名字,其他必须是成对出现,每对为对应的key=value。

注意

  • Goa不支持日志存储,目前输出对象只有Stdout,因为对日志存储的需求因人而异。为不影响访问速度,建议自省实现异步的日志存储。

Goa Media

使用Goa的Media之前,首先得明白其存在的意义。

当终端访问我们的HTTP API时,应有一个双方约定好的信息格式载体,我们以常用的JSON为例。Response body信息为JSON,同时server端反馈的信息也应该是JSON。在Go代码里,JSON的本质是一个type为string的key-value的map,代码表述为:

map[string]interface{}

其中interface接受各种类型。

当我们需要实现这么一个map时,最规范也是最常用的办法,就是将此段JSON的模板结构体直接赋值转换为map。

因此,go生成API-JSON返回信息的基本流程为:

  • 1.定义结构体&定义该结构体对应的JSON模板
  • 2.为结构体变量赋值
  • 3.将结构体变量映射到JSON模板中,生成JSON型map
  • 4.将此map(已成为JSON)返回给终端

除了第2步需要人为的写代码赋值外,Goa的MediaType均可以在Design过程中指定关键字来实现。

基于官方的Test code举个栗子:

design.go中关于API resource:bottle的testcode解读:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var _ = Resource("bottle", func() { //定义资源,资源名为bottle
BasePath("/bottles")
DefaultMedia(BottleMedia) //指定generated code中默认使用的Media
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")
TypeName("BottleMedia") //给Media重命名
Attributes(func() {
Attribute("id", Integer, "Unique bottle ID") //参数分别为JSON-key,type,描述
Attribute("href", String, "API href for making requests on the bottle")
Attribute("name", String, "Name of wine")
})

View("default", func() { //定义好Media后,必须要指定一个default的View
Attribute("id") //在这个View中暴露出的属性
Attribute("href")
Attribute("name")
})
})

代码说明:

  • DefaultMedia(BottleMedia)中的变量不一定非得是在View中指定为“default”的Media,而是说要使用哪个Media(Test code里只定义了一个名为default的media)。如果没有这句代码,默认生成的Show method里没有media的框架(可以手动添加)。

  • TypeName("BottleMedia")该行代码的作用是将Media重命名。如果没有这一行,Goa会根据MediaType函数的第一个参数生成Media名(即结构体名字),上面的例子中,如果去掉该行,生成的Media名为GoaExampleBottle

  • View可以定义多个,在生成的代码中,决定使用哪个JSON模板做Media的载体。但View中必须要有一个名为default的定义,当没有指定view时,系统默认暴露default。只要有media,就要有个default的view。

  • 关于MediaView的关系。View是Media的子集,REST API中一个资源可以有很多属性,但并不是每个该资源的HTTP请求都应反馈整个属性list,而是根据不同的请求地址,返回不同需求的资源信息,这种方式可以在Design过程中通过View实现。

上面的design代码生成的次代代码(media_types.go)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// A bottle of wine
// Identifier: application/vnd.goa.example.bottle+json
type BottleMedia struct {
// API href for making requests on the bottle
Href *string `json:"href,omitempty"`
// Unique bottle ID
ID *int `json:"id,omitempty"`
// Name of wine
Name *string `json:"name,omitempty"`
}

// Dump produces raw data from an instance of BottleMedia running all the
// validations. See LoadBottleMedia for the definition of raw data.
func (mt *BottleMedia) Dump() (res map[string]interface{}, err error) {
res, err = MarshalBottleMedia(mt, err)
return
}

// MarshalBottleMedia validates and renders an instance of BottleMedia into a interface{}
// using view "default".
func MarshalBottleMedia(source *BottleMedia, inErr error) (target map[string]interface{}, err error) {
err = inErr
tmp23 := map[string]interface{}{
"href": source.Href,
"id": source.ID,
"name": source.Name,
}
target = tmp23
return
}

从代码中可以看出,当我们需要给该Media赋值时,需要将资源(BottleMedia型的结构体指针)传值给MarshalBottleMedia。该资源在对应的contexts.go中进行定义。

生成的Action Func代码(bottle.go)如下:

1
2
3
4
5
// Show runs the show action.
func (c *BottleController) Show(ctx *app.ShowBottleContext) error {
res := &app.BottleMedia{}
return ctx.OK(res)
}

可以看到定义的res变量即为我们要的BottleMedia型的结构体指针,而函数MarshalBottleMedia进行多重封装后在return ctx.OK(res)这一句中被调用,因此上面讲的第二步,即是我们需要做的修改,根据业务,将信息写入变量res,然后发出。

Middleware

官方Middleware

Goa官方目前支持的Middleware有:

  • LogRequest,用于记录Http request 和 response。
  • Recover,用于恢复及记录server内部错误。
  • Timeout,超时后回复cancelation signal
  • RequireHeader, 验证require的handler和参数是否一致。
  • CORS,提供一个简单的CORS的API描述。

Goa所有的中间件可以加载在Service中,作用于全局;也可以只加载在某些或某个Controller中,只对针对的业务有效。加载方法均为Use函数。

全局使用middleware,在生成的代码(main.go)中有所体现:

1
service.Use(middleware.RequestID())

Controller限定使用middleware,可以在newctrl后调用Use

1
2
3
c3 := NewOperandsController(service)
c3.Use(middlewareTest)
app.MountOperandsController(service, c3)

其中middlewareTest是一个测试middleware,只用于OperandsController使用。

自定义Middleware

自定义Middleware有两种方法,一个是自行实现代码编写,另一个是调用goa.NewMiddleware生成Middleware。

方法一,代码实现:

首先看一个官方middleware RequestID的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// RequestID is a middleware that injects a request ID into the context of each request.
// Retrieve it using ctx.Value(ReqIDKey). If the incoming request has a RequestIDHeader header then
// that value is used else a random value is generated.
func RequestID() goa.Middleware {
return func(h goa.Handler) goa.Handler {
return func(ctx *goa.Context) error {
id := ctx.Request().Header.Get(RequestIDHeader)
if id == "" {
id = fmt.Sprintf("%s-%d", reqPrefix, atomic.AddInt64(&reqID, 1))
}
ctx.SetValue(ReqIDKey, id)

return h(ctx)
}
}
}

官方其他的middleware代码格式与RequestID相同。上段代码中最重要的就是最内层匿名函数的实现。

首先定义的*goa.Context参数用于获取HTTP访问的行为内容,然后进行逻辑处理,最后将该参数返回给上层传入的goa.Handler参数。

比如我们自己实现一个打印"middleware test"日志的测试middleware:

1
2
3
4
5
6
7
8
9
func ShowRequestLog() goa.Middleware {
return func(h goa.Handler) goa.Handler {
return func(ctx *goa.Context) error {
ctx.Debug("middleware test")
err := h(ctx)
return err
}
}
}

这样所有启用该middleware的操作都会打印ctx.Debug("middleware test")产生的DEBUG信息。

方法二,通过Goa定义:

Goa提供了一个NewMiddleware方法用于实现定义Middleware,我们只需要实现一个返回error的Handler,将逻辑实现写入Handler的匿名函数中,然后传参给NewMiddleware函数即可。

Handler的定义:

1
2
3
4
5
6
7
8
9
// Handler defines the controller handler signatures.
// Controller handlers accept a context and return an error.
// The context provides typed access to the request and response state. It implements
// the golang.org/x/net/context package Context interface so that handlers may define
// deadlines and cancelation signals - see the Timeout middleware as an example.
// If a controller handler returns an error then the application error handler is invoked
// with the request context and the error. The error handler is responsible for writing the
// HTTP response. See DefaultErrorHandler and TerseErrorHandler.
Handler func(*Context) error

因此之前测试middleware的实现也可以这样写:

1
2
3
4
middlewareTest, _ := goa.NewMiddleware(func(ctx *goa.Context) error {
ctx.Debug("middleware test")
return nil
})

其中匿名函数func(ctx *goa.Context) error即为我们实现的Handler定义,middlewareTest即可以传参给之前讲到的Use的参数。

Payload

Talked above, 通过MediaType可以指定生成自定义Media,然而Media的使用,除了自己逻辑代码中会用到以外,还可以通过Payload函数,在HTTP操作过程中使用。

1.进行HTTP POST操作时,把Body中的JSON直接赋值给接收的Resource media。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var _ = Resource("sms", func() {
Description("2.3 send message check")
BasePath("/sms")
DefaultMedia(SmsMedia) //指定默认接收体Media
Action("SendCheckNumber", func() {
Description("Send a checking number")
Routing(POST("/"))
Payload(func() {
Member("cellphone_number") //定义受体结构体
Required("cellphone_number") //定义需要被HTTP请求端赋值的参数
})
Response(Created)
})
})

这样在POST过程中,指定的POST参数就会自动赋值到接收体的Media中。如测试命令:

curl  -XPOST -d '{"cellphone_number":"12121111"}' "http://localhost:8080/sms"

说明

  • 在函数Member中可以指定该参数接收的类型,如StringInteger等,通常这些都已在Media定义时指定过,但这里需要重复指定,如果不在Payload中指定,goa会默认将其指定为“String”型。

  • Required指的是Http请求中必须要有的参数(非nil)。

  • 如果Member函数的参数中包含了Required函数中没有要求必须包含的参数,即该参数可以不用在发起HTTP请求时赋值,在生成的代码中,该参数的定义,会以指定类型的指针(如指定为String,则生成的代码中为*string)为定义类型,同时在JSON适配符中标注omitempty,如果该参数在HTTP请求时没有被赋值,则该参数默认值为nil

自定义HTTP的返回值

除了200返回值,我们通常需要201400500等返回值。拿201举例,Goa提供的返回值模板中,Created代表201,然而该函数没有返回值参数,只会返回一个201状态。

Goa提供了一个返回值模板重写机制,下面的代码重写201,并附带返回值:

1
2
3
4
5
6
ResponseTemplate("SmsCreated", func(pattern string) {
Description("Resource created")
Media(SmsMedia) //指定返回时的载体Media
Status(201) //指定返回值
... //指定返回的其他部分,如Header等
})

上段代码写在API函数定义体内,定义该模板后,返回时调用函数(ctx *SendCheckNumberSmsContext) SmsCreated(resp *SmsMedia) error即可返回201的同时返回指定内容resp

说明,指定了Media,则生成的函数SmsCreated就会包含一个该Media的结构体参数作为response的消息体。这里涉及到一个返回消息内容的问题。以JSON格式为例,JSON命名规范是Linux命名规范,以小写字母加_为主,Golang则是首字母大写,且第一个字母大小写意义不同。因此这里指定的Media的结构体参数,在Golang中是以Golang规范命名,在JSON中则是JSON格式,这个形式的实现,是要求在MediaType函数定义Media时指定的,所有的Attribute都应以JSON命名规范命名,生成的Golang结构体则自动转换(去_并自动首字母大写)。

Links

不论在Request还是Response中,信息载体的定义中往往会出现结构体嵌套的情况。这种情况在Goa中表现的尤为复杂,因为Goa除了要定义常用的defaultView外,有时还会定义其他用途的Media View,嵌套的部分如果不做指定处理,会出现二义性问题。

Goa通过实现Links机制来控制结构体在定义过程中的嵌套问题。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
var ClientSystemInfo = MediaType("application/client_system_infomation+json", func() {
Description("describe the information of client system")
TypeName("ClientSystemInfoMedia")
Attributes(func() {
Attribute("os_name", String, "name of client OS")
Attribute("os_version", String, "version of client OS")
})

View("default", func() {
Attribute("os_name")
Attribute("os_version")
View("osName", func() {
Attribute("os_name")
})
})

var MessagePushSDK = MediaType("application/message_push_sdk+json", func() {
Description("the SDK of message pusher")
TypeName("MessagePushSDKMedia")
Attributes(func() {
Attribute("vendor_name", String, "vendor name")
Attribute("version", String, "SDK version")
})

View("default", func() {
Attribute("vendor_name")
Attribute("version")
})
})

var AccountInformation = MediaType("application/new_account+json", func() {
Description("create new acount media")
TypeName("AccountInformation")
Attributes(func() {
Attribute("cellphone_number", String, "user cellphone number")
Attribute("client_system", ClientSystemInfo, "system information of the client")
Attribute("message_push_sdk", MessagePushSDK, "information of the message pusher SDK")
Links(func() {
Link("client_system", "osName") //指定定义的“client_system”是"osName"的View
Link("message_push_sdk", "default")
})

Attribute("user_id", String, "create a user id for response")
})

View("default", func() {
Attribute("cellphone_number")
Attribute("password")
//去掉屏蔽,则默认使用Media定义中的“default” View
// Attribute("client_system")
// Attribute("message_push_sdk")