深色模式
Goa 设计 DSL
概述
Goa 最值钱的部分,不是生成出来的代码,而是 design/ 目录里的 DSL。它相当于一份可执行的接口契约:请求长什么样、响应长什么样、错误有哪些、HTTP 怎么映射,都先写在这里,再交给代码生成器去展开。
中小项目写 Goa DSL 时,最容易犯的错有两个:一个是把 DSL 写成“另一个业务层”,另一个是过早追求大而全。更稳的做法,是先把契约写清楚,再按领域拆文件,保持设计层只描述接口,不描述实现。
先把 DSL 当作契约
Goa 的 DSL 适合表达这些东西:
- 资源和服务有哪些
- 每个方法接收什么输入
- 每个方法返回什么输出
- 失败时可能有哪些错误
- HTTP 路径、方法、参数映射关系是什么
它不适合承担这些事情:
- 查询数据库
- 拼接 SQL
- 写权限判断实现
- 写缓存逻辑
- 写事务代码
一句话说,DSL 写的是“外部怎么调用服务”,不是“服务内部怎么实现”。
API、Service、Method 各管什么
在 Goa 里,这三个对象是最常用的骨架:
API:全局信息,比如标题、版本、公共错误、通用安全方案Service:一组相关接口,通常按业务域拆分Method:单个具体能力,对应一个业务动作
例如:
go
package design
import . "goa.design/goa/v3/dsl"
var _ = API("blog", func() {
Title("Blog API")
Description("中小型博客后台接口")
})
var _ = Service("user", func() {
Description("用户服务")
HTTP(func() {
Path("/users")
})
Method("show", func() {
Payload(func() {
Field(1, "id", UInt64, "用户 ID")
Required("id")
})
Result(func() {
Field(1, "id", UInt64)
Field(2, "name", String)
Required("id", "name")
})
HTTP(func() {
GET("/{id}")
})
})
})这里表达的是一条完整的接口契约:有一个 user 服务,里面有一个 show 方法,它接收 id,返回用户信息,并通过 GET /users/{id} 暴露出去。
Payload、Result、Error 分别代表什么
这三个对象最好从一开始就分清:
Payload:请求契约Result:成功响应契约Error:失败响应契约
如果一个字段只属于数据库,不属于接口,就不应该放进 Payload 或 Result。比如软删除标记、内部审计字段、数据库索引辅助字段,这些通常不属于 API 契约。
一个比较稳的写法是这样:
go
Method("create", func() {
Payload(func() {
Field(1, "name", String, "用户名")
Field(2, "email", String, "邮箱")
Required("name", "email")
})
Result(func() {
Field(1, "id", UInt64)
Field(2, "name", String)
Field(3, "email", String)
Required("id", "name", "email")
})
Error("conflict")
Error("internal_error")
HTTP(func() {
POST("/")
Response(StatusCreated)
Response("conflict", StatusConflict)
Response("internal_error", StatusInternalServerError)
})
})这类定义的重点不是“字段写全”,而是“把外部能感知到的行为写准”。
HTTP 映射应该写在哪里
HTTP 细节放在 HTTP(func() {}) 里。常见的有:
GET、POST、PUT、DELETE- 路径参数
- 头字段
- 查询参数
- 状态码映射
例如:
go
Method("list", func() {
Payload(func() {
Field(1, "page", Int)
Field(2, "page_size", Int)
})
Result(ArrayOf(String))
HTTP(func() {
GET("/")
Param("page")
Param("page_size")
})
})这样做的好处是,方法语义和 HTTP 暴露方式都在同一个地方,看接口时不用在项目里来回跳。
中小项目的 design/ 目录怎么拆
中小项目不需要一开始就把 design/ 拆成十几个文件。更实用的方式通常是按领域拆成 2 到 5 个文件:
text
design/
├── api.go
├── user.go
├── auth.go
└── types.go拆分时可以按这几个原则:
api.go放全局信息和公共安全方案- 每个业务域一个文件,比如
user.go、order.go - 可复用类型多了,再单独放
types.go - 不要按 HTTP 方法拆文件,比如
get.go、post.go
对中小项目来说,按业务域拆比分层拆更自然。因为团队日常讨论的对象,往往也是“用户模块”“订单模块”,不是“所有 POST 接口”。
这些坑很常见
设计层掺进实现细节
比如在 DSL 里试图表达表名、数据库索引、GORM 标签,这通常都不合适。DSL 的边界是 API 契约,不是存储实现。
直接把生成类型当数据库模型
Goa 生成的类型服务于接口层,数据库模型服务于存储层。两者在字段、约束和生命周期上都不完全一样,强行共用,后面很容易互相污染。
一开始就把高级特性写满
Views、多传输协议、复杂的安全组合、很细的公共类型抽象,这些都不是起步阶段必须有的。中小项目先把 HTTP 契约写顺,再慢慢加,整体会更稳。
