goa-4-使用积累 | KaiQ.Gu|KerwinKoo Blog
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 | // A Logger writes key/value pairs to a Handler |
具体调用方法为:
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 | var _ = Resource("bottle", func() { //定义资源,资源名为bottle |
代码说明:
DefaultMedia(BottleMedia)中的变量不一定非得是在View中指定为“default”的Media,而是说要使用哪个Media(Test code里只定义了一个名为default的media)。如果没有这句代码,默认生成的Showmethod里没有media的框架(可以手动添加)。TypeName("BottleMedia")该行代码的作用是将Media重命名。如果没有这一行,Goa会根据MediaType函数的第一个参数生成Media名(即结构体名字),上面的例子中,如果去掉该行,生成的Media名为GoaExampleBottle。View可以定义多个,在生成的代码中,决定使用哪个JSON模板做Media的载体。但View中必须要有一个名为default的定义,当没有指定view时,系统默认暴露default。只要有media,就要有个default的view。关于
Media和View的关系。View是Media的子集,REST API中一个资源可以有很多属性,但并不是每个该资源的HTTP请求都应反馈整个属性list,而是根据不同的请求地址,返回不同需求的资源信息,这种方式可以在Design过程中通过View实现。
上面的design代码生成的次代代码(media_types.go)如下:
1 | // A bottle of wine |
从代码中可以看出,当我们需要给该Media赋值时,需要将资源(BottleMedia型的结构体指针)传值给MarshalBottleMedia。该资源在对应的contexts.go中进行定义。
生成的Action Func代码(bottle.go)如下:
1 | // Show runs the show action. |
可以看到定义的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 | c3 := NewOperandsController(service) |
其中middlewareTest是一个测试middleware,只用于OperandsController使用。
自定义Middleware
自定义Middleware有两种方法,一个是自行实现代码编写,另一个是调用goa.NewMiddleware生成Middleware。
方法一,代码实现:
首先看一个官方middleware RequestID的实现:
1 | // RequestID is a middleware that injects a request ID into the context of each request. |
官方其他的middleware代码格式与RequestID相同。上段代码中最重要的就是最内层匿名函数的实现。
首先定义的*goa.Context参数用于获取HTTP访问的行为内容,然后进行逻辑处理,最后将该参数返回给上层传入的goa.Handler参数。
比如我们自己实现一个打印"middleware test"日志的测试middleware:
1 | func ShowRequestLog() goa.Middleware { |
这样所有启用该middleware的操作都会打印ctx.Debug("middleware test")产生的DEBUG信息。
方法二,通过Goa定义:
Goa提供了一个NewMiddleware方法用于实现定义Middleware,我们只需要实现一个返回error的Handler,将逻辑实现写入Handler的匿名函数中,然后传参给NewMiddleware函数即可。
Handler的定义:
1 | // Handler defines the controller handler signatures. |
因此之前测试middleware的实现也可以这样写:
1 | middlewareTest, _ := goa.NewMiddleware(func(ctx *goa.Context) error { |
其中匿名函数func(ctx *goa.Context) error即为我们实现的Handler定义,middlewareTest即可以传参给之前讲到的Use的参数。
Payload
Talked above, 通过MediaType可以指定生成自定义Media,然而Media的使用,除了自己逻辑代码中会用到以外,还可以通过Payload函数,在HTTP操作过程中使用。
1.进行HTTP POST操作时,把Body中的JSON直接赋值给接收的Resource media。
1 | var _ = Resource("sms", func() { |
这样在POST过程中,指定的POST参数就会自动赋值到接收体的Media中。如测试命令:
curl -XPOST -d '{"cellphone_number":"12121111"}' "http://localhost:8080/sms"说明
在函数
Member中可以指定该参数接收的类型,如String,Integer等,通常这些都已在Media定义时指定过,但这里需要重复指定,如果不在Payload中指定,goa会默认将其指定为“String”型。Required指的是Http请求中必须要有的参数(非nil)。如果
Member函数的参数中包含了Required函数中没有要求必须包含的参数,即该参数可以不用在发起HTTP请求时赋值,在生成的代码中,该参数的定义,会以指定类型的指针(如指定为String,则生成的代码中为*string)为定义类型,同时在JSON适配符中标注omitempty,如果该参数在HTTP请求时没有被赋值,则该参数默认值为nil。
自定义HTTP的返回值
除了200返回值,我们通常需要201,400, 500等返回值。拿201举例,Goa提供的返回值模板中,Created代表201,然而该函数没有返回值参数,只会返回一个201状态。
Goa提供了一个返回值模板重写机制,下面的代码重写201,并附带返回值:
1 | ResponseTemplate("SmsCreated", func(pattern string) { |
上段代码写在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 | var ClientSystemInfo = MediaType("application/client_system_infomation+json", func() { |