概述

goa API 设计语言是 Go 中实现的 DSL,可以描述任意微服务API。虽然主要关注的是基于 REST 的 HTTP API,但该语言足够灵活,可以描述遵循其他方法的 API。使用插件可以扩展核心 DSL 以允许描述微服务的其他方面,例如数据库模型,服务发现集成,故障处理程序等。

设计定义

设计语言的核心是由链接在一起描述定义的函数组成。goa 设计语言根定义是 API 定义,定义它的 DSL 如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import (
. "github.com/goadesign/goa/design"
. "github.com/goadesign/goa/design/apidsl"
)

var _ = API("My API", func() { // "My API" is the name of the API used in docs
Title("Documentation title") // Documentation title
Description("The next big thing") // Longer documentation description
Host("goa.design") // Host used by Swagger and clients
Scheme("https") // HTTP scheme used by Swagger and clients
BasePath("/api") // Base path to all API endpoints
Consumes("application/json") // Media types supported by the API
Produces("application/json") // Media types generated by the API
})

关于“点导入”的旁注通常会出现这个问题:goa API 设计语言是在 Go 中实现的 DSL,而不是 Go(“点导入”的含义就是这个包导入之后在调用这个包的函数时,可以省略前缀的包名)。生成的代码或 goa 中的任何实际 Go 代码都不使用“点导入”。将此技术用于 DSL 可以获得更清晰的代码。它还允许透明地混合来自插件的 DSL,等待。

DSL 大量使用匿名函数来递归地描述各种定义。在上面的示例中,API 函数接受 API 的名称作为第一个参数,并接受匿名函数作为第二个参数。此匿名函数在本文档中也称为DSL,它定义了 API 的其他属性。许多其他 DSL 函数同样使用此模式(名称 + DSL)。

API 函数定义 API 的一般属性:文档中使用的 TitleDescription,服务条款(上面的示例中未显示)文档和客户端中使用的默认主机和协议方案以及所有 API 端点的基本路径(可选地还通过基本路径中定义的通配符捕获相应的基本参数)。

该函数还定义了 API 支持的媒体类型。ConsumesProduces 函数可以为请求(Consumes)和响应(Produces)定义支持媒体类型,也可以指定生成的代码用于解码请求包体和编码响应包体的编码包。

API 函数中还可以定义许多其他属性,从附加元数据(联系信息)到安全定义,CORS 策略和响应模板。有关完整列表,请参阅参考资料。

API 端点

除了根 API 定义之外,goa API 设计语言还可以描述实现端点以及请求和响应的详细信息。Resource 函数定义了一组相关的 API 端点 - 如果 API 是 RESTful,则为资源。使用 Action 函数描述每个实际端点。以下是一个简单的 Operands 资源示例,公开了一个添加操作(API 端点):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var _ = Resource("Operands", func() {        // Defines the Operands resource
Action("add", func() { // Defines the add action
Routing(GET("/add/:left/:right")) // The relative path to the add endpoint
Description("add returns the sum of :left and :right in the response body")
Params(func() { // Defines the request parameters
// found in the URI (wildcards) and querystring
Param("left", Integer, "Left operand") // Defines left parameter as path segment
// captured by :left
Param("right", Integer, "Right operand") // Define right parameter as path segment
// captured by :right
})
Response(OK, "plain/text") // Defines response
})
})

一个 Resource 函数可以定义任意数量的 ActionResource 可定义公共基本路径,公共路径参数和其他的属性可以被 Resource 的参数共享。一个 Action 可以定义多个路由(Routing 函数的参数是可变参数),同一个 API 端点可以通过不同的路由或使用不同的 HTTP 方法访问。

在 DSL 中用于定义 Action 的参数可以指定参数的验证规则,范围从整数和数字的最小值和最大值到通过字符串参数的正则表达式定义的模式:

1
2
3
4
Param("left", Integer, "Left operand", func() {
Minimum(0) // Do not allow for negative values.
})

文件服务器

Files 函数可以在资源上定义文件服务器。文件服务器可以提供对单个静态文件或如果路由以以 * 开头的通配符结束,则在给定文件路径下的所有文件。Files 函数可选择接受子 DSL(匿名函数作为最后一个参数)来定义安全方案。语法与用于定义操作的安全方案的语法相同。

以下演示了一个名叫 public 的资源,里面有两个文件服务器,一个为发送到 /swagger.json 的请求提供文件 public/swagger/swagger.json,另一个是发送到 /js/*filepath的public/js/ 下的所有其他文件,其中 *filepath 的值对应于相对于 /public/js 的文件路径。swagger 端点还定义了一个安全方案,要求客户端在能够检索 swagger 规范之前进行身份验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
var _ = Resource("public", func() {

Origin("*", func() { // CORS policy that applies to all actions and file servers
Methods("GET") // of "public" resource
})

Files("/swagger.json", "public/swagger/swagger.json", func() {
Security("basic_auth") // Security scheme implemented by /swagger.json endpoint
})

Files("/js/*filepath", "public/js/") // Serve all files under the public/js directory
})

数据类型

goa API 设计语言可以描述任意的数据类型,API 可用于定义请求包体和响应媒体类型。Type 函数通过使用 Attribute 函数列出每个字段来描述数据结构,它还可以使用ArrayOf 函数来定义数组或数组。以下为示例:

1
2
3
4
5
6
7
8
9
10
11
12
// Operand describes a single operand with a name and an integer value.
var Operand = Type("Operand", func() {
Attribute("name", String, "Operand name", func() { // Attribute name of type string
Pattern("^x") // with regex validation
})
Attribute("value", Integer, "Operand value") // Attribute value of type integer
Required("value") // only value is required
})

// Series represents an array of operands.
var Series = ArrayOf(Operand)

请注意,与 API 函数一样,Type 函数接受两个参数:名称和描述类型属性的 DSL。Type DSL类型包含三个功能:

  • Description:设置类型描述
  • Attribute:定义单个类型字段
  • Required:列出必填字段:必须始终存在于该类型实例中的字段

Type 函数可用于定义 Action 有效负载(以及其他内容):

1
2
3
4
5
6
7
8
Action("sum", func() {          // Defines the sum action
Routing(POST("/sum")) // The relative path to the add endpoint
Description("sum returns the sum of all the operands in the response body")
Payload(Series) // Defines the action request body shape using the Series
// type defined above.
Response(OK, "plain/text") // Defines a response
})

Attributes

Attribute 在 goa DSL 中扮演着特殊的角色,它们是用于定义数据结构的基础。Attribute DSL 用于描述类型字段,请求参数,请求有效负载,响应头,响应主体等基本上任何需要定义数据结构的地方。定义属性的语法非常灵活,允许根据需要指定尽可能少的或完整的定义:

1
2
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{} ?

另外,可以使用 ArrayOfHashOf 或使用递归 DSL 定义类型字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
var User = Type("user", func() {
Description("A user of the API")
Attribute("name") // Simple string attribute
Attribute("address", func() { // Nested definition, defines a struct in Go
Attribute("number", Integer, "Street number")
Attribute("street", String, "Street name")
Attribute("city", String, "City")
Required("city") // The address must contain at least a city
})
Attribute("friends", ArrayOf("user"))
Attribute("data", HashOf(String, String))
})

请注意使用 “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
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
// Results is the media type that defines the shape of the "add" action response.
var Results = MediaType("vnd.application/goa.results", func() {
Description("The results of an operation")
Attributes(func() { // Defines the media type attributes
Attribute("value", Integer, "Results value") // Operation results attribute
Attribute("requester", User) // Requester attribute
})
Links(func() { // Defines the links embedded in the media type
Link("requester") // One link to the requester, will be rendered
// using the "link" view of User media type.
})
View("default", func() { // Defines the default view
Attribute("value") // Includes the "value" field in the default view
Attribute("links") // And render links
})
View("extended", func() { // Defines the extended view
Attribute("value") // Includes the value field
Attribute("requester") // and the requester field
})
})

// User is the media type used to render user resources.
var User = MediaType("vnd.application/goa.users", func() {
Description("A user of the API")
Attributes(func() {
Attribute("id", UUID, "Unique identifier")
Attribute("href", String, "User API href")
Attribute("email", String, "User email", func() {
Format("email")
})
})
View("default", func() {
Attribute("id")
Attribute("href")
Attribute("email")
})
View("link", func() { // The view used to render links to User media types.
Attribute("href") // Here only the href attribute is rendered in links.
})
})

Defining Responses

Response 函数用于 Action 声明以定义特定的潜在响应,它描述了响应状态代码,如果响应包含正文和标题,则为媒体类型。每个 Response 必须在 Action 范围内具有唯一名称,与大多数其他 DSL 功能一样,名称是第一个参数。以下 DSL 定义了一个名为“NoContent”的响应,该响应使用 HTTP 状态代码 204:

1
2
3
4
Response("NoContent", func() {
Description("This is the response returned in case of success")
Status(204)
})

请注意,此示例以及本节中的所有其他示例不使用响应模板,因此定义响应的所有属性,包括其名称。实际上,在大多数情况下,使用其中一个内置模板定义响应实现上面的响应(不添加描述)可以省略到:

1
Response(NoContent)

响应模板将在下面的部分中详细介绍,但在我们讲解它们之前,我们必须首先了解 Response 的工作原理。

如果 Response 包含正文,则使用 Media 函数指定相应的媒体类型。此函数接受媒体类型标识符或实际媒体类型值作为第一个参数,并可选择接受用于呈现响应正文的媒体类型视图的名称。该视图是可选的,因为相同的 Action 可以根据请求状态返回不同的视图。以下是使用状态代码 200 和结果媒体类型的“OK”响应的 Results 定义示例:

1
2
3
4
5
Response("OK", func() {
Description("This is the success response")
Status(200)
Media(Results)
})

为方便起见,响应的媒体类型可以作为 Response 函数的第二个参数提供(这在使用响应模板时特别有用,如下面相应部分所述)。因此上面的代码等同于如下代码:

1
2
3
4
Response("OK", Results, func() {
Description("This is the success response")
Status(200)
})

假设 Results 的标识符是 application/vnd.example.results,则上述内容相当于:

1
2
3
4
Response("OK", "application/vnd.example.results", func() {
Description("This is the success response")
Status(200)
})

请注意,媒体类型标识符(上例中的 application/vnd.example.results)可能对应于通过 MediaType 函数定义的媒体类型的标识符。当媒体类型标识符与设计中定义的媒体类型不匹配时,生成的代码使用 Go 语言中 []byte 来定义响应主体的类型。

如果父操作始终返回默认视图,则响应可以定义为:

1
2
3
4
5
Response("OK", func() {
Description("This is the success response")
Status(200)
Media(Results, "default")
})

响应包头是使用 Headers 函数定义的,定义每个包头的语法与用于定义属性的语法相同:

1
2
3
4
5
6
7
8
9
10
Response("OK", func() {
Status(200)
Media(Results, "default")
Headers(func() {
Header("Location", String, "Resource location", func() {
Pattern("/results/[0-9]+")
})
Header("ETag") // assumes String type as with Attribute
})
})

使用默认 MediaType

Resource 可以为所有操作定义默认媒体类型。定义默认媒体类型有两个效果:

  • 默认媒体类型用于返回状态代码 200 且未定义媒体类型的所有响应。
  • 在响应包体、操作参数和响应媒体类型上定义的属性与默认媒体类型上定义的属性名称匹配,自动从其继承所有属性(描述,类型,验证等)

请考虑以下资源定义,该定义使用上面定义的 Results 媒体类型作为默认媒体类型,并利用它定义 add 动作的 OK 响应:

1
2
3
4
5
6
7
8
9
10
11
var _ = Resource("Operands", func() {
DefaultMedia(Results)
Action("add", func() {
Routing(GET("/add/:left/:right"))
Params(func() {
Param("left", Integer, "Left operand")
Param("right", Integer, "Right operand")
})
Response(OK) // Uses the resource default media type
})
})

现在假设 Results 媒体类型还返回用于计算总和的初始操作数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var Results = MediaType("vnd.application/goa.results", func() {
Description("The results of an operation")
Attributes(func() {
Attribute("left", Integer, "Left operand")
Attribute("right", Integer, "Right operand")
Attribute("value", Integer, "Results value")
Attribute("requester", User)
})
View("default", func() {
Attribute("left")
Attribute("right")
Attribute("value")
Attribute("requester")
})
})

add 操作定义可以利用这一点来避免必须重复 leftright 参数的类型和注释:

1
2
3
4
5
6
7
8
9
10
11
var _ = Resource("Operands", func() {
DefaultMedia(Results)
Action("add", func() {
Routing(GET("/add/:left/:right"))
Params(func() {
Param("left") // Inherits type and description from default media type
Param("right") // Inherits type and description from default media type
})
Response(OK) // Uses the resource default media type
})
})

响应模版

goa API 设计语言允许在 API 级别定义响应模板,任何操作都可以利用它来定义响应。此类模板可以接受任意数量的字符串参数来定义任何响应属性。使用以下语法定义模板:

1
2
3
4
5
6
7
8
9
10
11
12
var _ = API("My API", func() {
ResponseTemplate("created", func(hrefPattern string) { // Defines the "created" template
Description("Resource created") // that takes one argument "hrefPattern"
Status(201) // using status code 201
Headers(func() {
Header("Location", func() { // and contains the "Location" header
Pattern(hrefPattern) // with a regex validation defined by the
// value of the "hrefPattern" argument.
})
})
})
})

然后在定义响应时通过名称简单地引用模板来使用模板:

1
2
3
4
5
Action("sum", func() {
Routing(POST("/accounts"))
Payload(AccountPayload)
Response("created", "^/results/[0-9]+") // Response uses the "created" response template.
})

goa 框架所有标准 HTTP 状态码提供响应模板,因此不需要为简单情况定义模板。内置模板的名称与相应 HTTP 状态代码的名称匹配。例如:

1
2
3
4
5
6
7
Action("show", func() {
Routing(GET("/:id"))
Response("ok", func() {
Status(200)
Media(AccountMedia)
})
})

等价于:

1
2
3
4
Action("show", func() {
Routing(GET("/:id"))
Response(OK, AccountMedia) // Uses the built-in "OK" response template that defines status 200
})

总结

goa 设计语言还包含很多内容,但是这个概述应该让你了解它的工作原理。goa 设计语言不需要很长时间就能运用自如,从而可以快速迭代和完善设计。可以与其他开发者共享 Swagger 定义以收集反馈进行迭代。一旦完成,goagen 将生成 API 脚手架,从设计中请求上下文和验证代码,从而将其实现。该设计成为一个始终与实施最新的生动文档。