概述
goa API 设计语言是 Go 中实现的 DSL,可以描述任意微服务API。虽然主要关注的是基于 REST 的 HTTP API,但该语言足够灵活,可以描述遵循其他方法的 API。使用插件可以扩展核心 DSL 以允许描述微服务的其他方面,例如数据库模型,服务发现集成,故障处理程序等。
设计定义
设计语言的核心是由链接在一起描述定义的函数组成。goa 设计语言根定义是 API 定义,定义它的 DSL 如下所示:
1 | import ( |
关于“点导入”的旁注通常会出现这个问题:goa API 设计语言是在 Go 中实现的 DSL,而不是 Go(“点导入”的含义就是这个包导入之后在调用这个包的函数时,可以省略前缀的包名)。生成的代码或 goa 中的任何实际 Go 代码都不使用“点导入”。将此技术用于 DSL 可以获得更清晰的代码。它还允许透明地混合来自插件的 DSL,等待。
DSL 大量使用匿名函数来递归地描述各种定义。在上面的示例中,API 函数接受 API 的名称作为第一个参数,并接受匿名函数作为第二个参数。此匿名函数在本文档中也称为DSL,它定义了 API 的其他属性。许多其他 DSL 函数同样使用此模式(名称 + DSL)。
API 函数定义 API 的一般属性:文档中使用的 Title
和 Description
,服务条款(上面的示例中未显示)文档和客户端中使用的默认主机和协议方案以及所有 API 端点的基本路径(可选地还通过基本路径中定义的通配符捕获相应的基本参数)。
该函数还定义了 API 支持的媒体类型。Consumes
和 Produces
函数可以为请求(Consumes)和响应(Produces)定义支持媒体类型,也可以指定生成的代码用于解码请求包体和编码响应包体的编码包。
API 函数中还可以定义许多其他属性,从附加元数据(联系信息)到安全定义,CORS 策略和响应模板。有关完整列表,请参阅参考资料。
API 端点
除了根 API 定义之外,goa API 设计语言还可以描述实现端点以及请求和响应的详细信息。Resource 函数定义了一组相关的 API 端点 - 如果 API 是 RESTful,则为资源。使用 Action 函数描述每个实际端点。以下是一个简单的 Operands
资源示例,公开了一个添加操作(API 端点):
1 | var _ = Resource("Operands", func() { // Defines the Operands resource |
一个 Resource
函数可以定义任意数量的 Action
。Resource
可定义公共基本路径,公共路径参数和其他的属性可以被 Resource
的参数共享。一个 Action
可以定义多个路由(Routing
函数的参数是可变参数),同一个 API 端点可以通过不同的路由或使用不同的 HTTP 方法访问。
在 DSL 中用于定义 Action
的参数可以指定参数的验证规则,范围从整数和数字的最小值和最大值到通过字符串参数的正则表达式定义的模式:
1 | Param("left", Integer, "Left operand", func() { |
文件服务器
Files
函数可以在资源上定义文件服务器。文件服务器可以提供对单个静态文件或如果路由以以 *
开头的通配符结束,则在给定文件路径下的所有文件。Files
函数可选择接受子 DSL(匿名函数作为最后一个参数)来定义安全方案。语法与用于定义操作的安全方案的语法相同。
以下演示了一个名叫 public
的资源,里面有两个文件服务器,一个为发送到 /swagger.json
的请求提供文件 public/swagger/swagger.json
,另一个是发送到 /js/*filepath的public/js/
下的所有其他文件,其中 *filepath
的值对应于相对于 /public/js 的文件路径。swagger 端点还定义了一个安全方案,要求客户端在能够检索 swagger 规范之前进行身份验证。
1 | var _ = Resource("public", func() { |
数据类型
goa API 设计语言可以描述任意的数据类型,API 可用于定义请求包体和响应媒体类型。Type
函数通过使用 Attribute
函数列出每个字段来描述数据结构,它还可以使用ArrayOf
函数来定义数组或数组。以下为示例:
1 | // Operand describes a single operand with a name and an integer value. |
请注意,与 API 函数一样,Type 函数接受两个参数:名称和描述类型属性的 DSL。Type
DSL类型包含三个功能:
- Description:设置类型描述
- Attribute:定义单个类型字段
- Required:列出必填字段:必须始终存在于该类型实例中的字段
Type
函数可用于定义 Action
有效负载(以及其他内容):
1 | Action("sum", func() { // Defines the sum action |
Attributes
Attribute
在 goa DSL 中扮演着特殊的角色,它们是用于定义数据结构的基础。Attribute
DSL 用于描述类型字段,请求参数,请求有效负载,响应头,响应主体等基本上任何需要定义数据结构的地方。定义属性的语法非常灵活,允许根据需要指定尽可能少的或完整的定义:
1 | Attribute(<name>, <type>, <description>, <dsl>) |
只需要第一个参数,所有其他参数都是可选的。默认属性类型是 String。属性的可能类型是:
Name | Go equivalent | JSON equivalent |
---|---|---|
Boolean | bool | “true” or “false” |
Integer | int | number |
Number | float | number |
String | string | string |
DateTime | time.Time | RFC3339 string |
UUID | uuid.UUID | RFC4122 string |
Any | interface{} | ? |
另外,可以使用 ArrayOf
或 HashOf
或使用递归 DSL 定义类型字段:
1 | var User = Type("user", func() { |
请注意使用 “user” 类型名称来定义 friends
字段,而不是引用 User
类型变量。其实两种语法(使用“user”类型或“User”变量定义数组)都被接受。使用名称而不是变量引用允许构建递归定义。
示例 Github 存储库包含一个类型目录,其中包含许多示例,用于演示设计类型和生成的代码之间的映射。
Responses
Response Media Types
接下来查看响应,goa 设计语言 MediaType
函数描述了表示响应主体的媒体类型。MediaType
的定义类似于 Type
的定义(媒体类型是一种特殊类型)但 MediaType
有两个独特的属性:
- Views:可以描述相同
MediaType
的不同渲染。通常,API 在列出请求时使用资源的“短”表示,并在返回单个资源的请求中使用更详细的表示。视图通过提供定义这些不同表示的方式来涵盖该用例。MediaType
定义必须定义用于呈现资源的默认视图(恰当地命名为default
)。 - Links:表示应该嵌入在响应中的相关
MediaType
。用于呈现链接的视图是链接,这意味着链接到的媒体类型必须定义链接视图。链接在媒体类型定义中的Links
函数下列出。然后,视图可以使用指定links
属性来呈现所有链接。
以下是 MediaType
定义的示例:
1 | // Results is the media type that defines the shape of the "add" action response. |
Defining Responses
Response
函数用于 Action
声明以定义特定的潜在响应,它描述了响应状态代码,如果响应包含正文和标题,则为媒体类型。每个 Response
必须在 Action
范围内具有唯一名称,与大多数其他 DSL 功能一样,名称是第一个参数。以下 DSL 定义了一个名为“NoContent”的响应,该响应使用 HTTP 状态代码 204:
1 | Response("NoContent", func() { |
请注意,此示例以及本节中的所有其他示例不使用响应模板,因此定义响应的所有属性,包括其名称。实际上,在大多数情况下,使用其中一个内置模板定义响应实现上面的响应(不添加描述)可以省略到:
1 | Response(NoContent) |
响应模板将在下面的部分中详细介绍,但在我们讲解它们之前,我们必须首先了解 Response
的工作原理。
如果 Response
包含正文,则使用 Media
函数指定相应的媒体类型。此函数接受媒体类型标识符或实际媒体类型值作为第一个参数,并可选择接受用于呈现响应正文的媒体类型视图的名称。该视图是可选的,因为相同的 Action
可以根据请求状态返回不同的视图。以下是使用状态代码 200 和结果媒体类型的“OK”响应的 Results
定义示例:
1 | Response("OK", func() { |
为方便起见,响应的媒体类型可以作为 Response
函数的第二个参数提供(这在使用响应模板时特别有用,如下面相应部分所述)。因此上面的代码等同于如下代码:
1 | Response("OK", Results, func() { |
假设 Results
的标识符是 application/vnd.example.results
,则上述内容相当于:
1 | Response("OK", "application/vnd.example.results", func() { |
请注意,媒体类型标识符(上例中的 application/vnd.example.results
)可能对应于通过 MediaType
函数定义的媒体类型的标识符。当媒体类型标识符与设计中定义的媒体类型不匹配时,生成的代码使用 Go 语言中 []byte
来定义响应主体的类型。
如果父操作始终返回默认视图,则响应可以定义为:
1 | Response("OK", func() { |
响应包头是使用 Headers
函数定义的,定义每个包头的语法与用于定义属性的语法相同:
1 | Response("OK", func() { |
使用默认 MediaType
Resource
可以为所有操作定义默认媒体类型。定义默认媒体类型有两个效果:
- 默认媒体类型用于返回状态代码 200 且未定义媒体类型的所有响应。
- 在响应包体、操作参数和响应媒体类型上定义的属性与默认媒体类型上定义的属性名称匹配,自动从其继承所有属性(描述,类型,验证等)
请考虑以下资源定义,该定义使用上面定义的 Results
媒体类型作为默认媒体类型,并利用它定义 add
动作的 OK
响应:
1 | var _ = Resource("Operands", func() { |
现在假设 Results
媒体类型还返回用于计算总和的初始操作数:
1 | var Results = MediaType("vnd.application/goa.results", func() { |
add
操作定义可以利用这一点来避免必须重复 left
、right
参数的类型和注释:
1 | var _ = Resource("Operands", func() { |
响应模版
goa API 设计语言允许在 API 级别定义响应模板,任何操作都可以利用它来定义响应。此类模板可以接受任意数量的字符串参数来定义任何响应属性。使用以下语法定义模板:
1 | var _ = API("My API", func() { |
然后在定义响应时通过名称简单地引用模板来使用模板:
1 | Action("sum", func() { |
goa 框架所有标准 HTTP 状态码提供响应模板,因此不需要为简单情况定义模板。内置模板的名称与相应 HTTP 状态代码的名称匹配。例如:
1 | Action("show", func() { |
等价于:
1 | Action("show", func() { |
总结
goa 设计语言还包含很多内容,但是这个概述应该让你了解它的工作原理。goa 设计语言不需要很长时间就能运用自如,从而可以快速迭代和完善设计。可以与其他开发者共享 Swagger 定义以收集反馈进行迭代。一旦完成,goagen
将生成 API
脚手架,从设计中请求上下文和验证代码,从而将其实现。该设计成为一个始终与实施最新的生动文档。