基本语法
-
syntax:用于标记 api 语言的版本,不同的版本可能语法结构有所不同。示例:textsyntax = "v1" -
info:是 api 语言的meta信息,其仅对当前 api 文件进行描述,暂不参与代码生成。示例:textinfo( title : "图书管理系统" desc: "api语法学习" author: "哈哈哈" email: "12345@qq.com" date: "2023年12月10日" version: "1.0") -
import:在当前 api 文件中引入其他 api 文件的语法块,其支持相对/绝对路径。但是被引入的 api 文件中不允许出现 service 语法块,否则会报错。示例(demo.api):text// foo.api文件和bar.api文件中不能出现service语法块import ( "foo.api" "../relative/bar.api")type UserInfoReq { Id int64 `path:"id"`}type UserInfoResp { Base // Base 为foo.api中的公共结构体 UserInfo // UserInfo 为bar.api中的公共结构体}service user { @handler userInfo get /user/info/:id (UserInfoReq) returns (UserInfoResp)} -
type:由 golang 的 type 演变而来,当然也保留着一些 golang type 的特性,并且保留了 golang 内置数据类型:bool,int,int8,int16,int32,int64,uint,uint8,uint16,uint32,uint64,uintptr,float32,float64,complex64,complex128,string,byte,rune。除此之外,还支持枚举类型,自定义类型,类型别名,数组(切片),哈希,类型嵌套等。但是不支持package设计,如time.Time。示例:api// 别名类型 [1]type Int inttype Integer = int// 空结构体type Foo {}// 单个结构体type Bar { Foo int `json:"foo"` Bar bool `json:"bar"` Baz []string `json:"baz"` Qux map[string]string `json:"qux"`}type Baz { Bar `json:"baz"` // 结构体内嵌 [2] Qux { Foo string `json:"foo"` Bar bool `json:"bar"` } `json:"baz"`}// 空结构体组type ()// 结构体组type ( Int int Integer = int Bar { Foo int `json:"foo"` Bar bool `json:"bar"` Baz []string `json:"baz"` Qux map[string]string `json:"qux"` }) -
@server:对一个服务语句的 meta 信息进行描述,其对应特性包含但不限于:jwt 开关、中间件、路由分组、路由前缀、超时控制。 -
@doc:对单个路由的 meta 信息描述,一般为 key-value 值,可以传递给 goctl 及其插件来进行扩展生成。 -
@handler:对单个路由的 handler 信息控制,主要用于生成 golang http.HandleFunc 的实现转换方法。 -
路由语句:路由语句是对单此 HTTP 请求的具体描述,包括请求方法,请求路径,请求体,响应体信息。text// 没有请求体和响应体的写法get /ping// 只有请求体的写法get /foo (foo)// 只有响应体的写法post /foo returns (foo)// 有请求体和响应体的写法post /foo (foo) returns (bar)
统一 api 前缀
在 HTTP 服务开发中,路由前缀需求是非常常见的,比如通过路由来区分版本,或者通过路由来区分不同的服务,这些都是非常常见的需求。以下是路由前缀的典型场景:
-
版本控制: 如果 API 有多个版本,可以使用路由前缀来区分它们,例如
/v1和/v2。这样,就可以在不同的路由处理器中实现不同版本的 API 逻辑,而不会造成混淆。text@server( prefix: /v1)service user-api { @handler Login post /user/login (LoginRequest) returns (LoginResponse)}@server( prefix: /v2)service user-api { @handler Login post /user/login (LoginRequest) returns (LoginResponse)} -
服务分割: 当有不同的服务或功能模块时,路由前缀可以按模块组织路由,例如
/auth用于认证相关的路由,/orders用于订单处理路由。text@server( prefix: /auth)service user-api { @handler Register post /register (RegisterRequest) returns (RegisterResponse) @handler Login post /login (LoginRequest) returns (LoginResponse)}@server( prefix: /orders)service user-api { @handler CreateOrders post /create (CreateOrdersRequest) returns (CreateOrdersResponse) @handler UpdateOrders post /update (UpdateOrdersRequest) returns (UpdateOrdersResponse)}
使用路由前缀后,生成的路由代码(handler/routes.go)如下:
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { server.AddRoutes( []rest.Route{ { Method: http.MethodPost, Path: "/register", Handler: RegisterHandler(serverCtx), }, { Method: http.MethodPost, Path: "/login", Handler: LoginHandler(serverCtx), }, }, rest.WithPrefix("/auth"), ) server.AddRoutes( []rest.Route{ { Method: http.MethodPost, Path: "/create", Handler: CreateOrdersHandler(serverCtx), }, { Method: http.MethodPost, Path: "/update", Handler: UpdateOrdersHandler(serverCtx), }, }, rest.WithPrefix("/orders"), )}可以看到,其实声明的 prefix 其实在生成代码后通过 rest.WithPrefix 来声明了路由前缀,这样就可以通过路由前缀来区分不同的服务了。
路由分组
路由分组是通过在 api 文件中使用 @server 注解下的group来实现的。每个 @server 可以定义一个路由前缀,然后在该前缀下可以定义一个路由分组。这样,所有相关的路由都会被组织在一起,形成一个路由分组。并且每一个分组都有自己的文件夹。
@server ( prefix: /v1 group: user)service user-api { @handler UserLogin post /user/login (UserLoginReq) returns (UserLoginResp) @handler UserInfo post /user/info (UserInfoReq) returns (UserInfoResp)}@server ( prefix: /v1 group: admin)service user-api { @handler AdminAdd get /admin/add (AdminAddReq) returns (AdminAddResp) @handler AdminUpdate get /admin/update (AdminUpdateReq) returns (AdminUpdateResp)}生成的路由文件(handler/routes.go)如下所示:
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { server.AddRoutes( []rest.Route{ { Method: http.MethodPost, Path: "/user/login", Handler: user.LoginHandler(serverCtx), }, { Method: http.MethodPost, Path: "/user/info", Handler: user.InfoHandler(serverCtx), }, }, rest.WithPrefix("/v1"), ) server.AddRoutes( []rest.Route{ { Method: http.MethodGet, Path: "/admin/add", Handler: admin.AddHandler(serverCtx), }, { Method: http.MethodGet, Path: "/admin/update", Handler: admin.UpdateHandler(serverCtx), }, }, rest.WithPrefix("/v1"), )}从路由文件可以看出分组并不会改变 URL 路由,仅仅只是为了把多个路由和逻辑代码进行分类而已。
参数获取
go-zero 通过在 tag 中来声明参数接收规则,但是不支持多 tag 来接收参数,即一个字段只能有一个 tag。目前 go-zero 支持的参数接收规则如下:
1. json
-
说明:用于处理 JSON 格式的请求体和响应体,对于 POST 和 PUT 请求,用于接收 JSON 格式的请求体;对于响应,会将数据序列化为 json 格式并响应。
-
示例:
gotype MyRequest struct { Name string `json:"name"`}
2. path
-
说明:用于从 URL 路径中提取路由参数。
-
示例:
go// URL: /user/:idtype MyRequest struct { Id string `path:"id"`}
3. form
-
说明:用于解析 POST 请求的
Content-Type为application/x-www-form-urlencoded或者multipart/form-data编码的表单参数。也可以用于解析 GET 请求的 URL 中的 querystring 参数。 -
示例:
gotype LoginForm struct { Username string `form:"username"` Password string `form:"password"`}
4. header
-
说明:用于将 HTTP 请求头映射到结构体字段。对于需要根据请求头做逻辑处理的场景非常有用(例如,读取认证信息、内容类型等)。
-
示例:
gotype AuthRequest struct { ContentLength int `header:"Content-Length"`}
参数校验规则
可以通过在 tag 中来声明参数接收规则,除此之外,还支持参数的校验,参数校验的规则仅对 请求体 有效,参数校验的规则写在 tag value 中,目前 go-zero 支持的参数校验规则如下:
| 接收规则 | 说明 | 示例 |
|---|---|---|
| optional | 当前字段是可选参数,允许为零值(zero value) | json:"foo,optional" |
| options | 当前参数仅可接收的枚举值 | json:"gender,options=[foo,bar]" |
| default | 当前参数默认值 | json:"gender,default=male" |
| range | 当前参数数值有效范围,仅对数值有效,写法规则详情见下 | json:"age,range=[0:120]" |
Range 表达式值规则:
- 左开右闭区间:(min:max],表示大于 min 小于等于 max,当 min 缺省时,min 代表数值 0,当 max 缺省时,max 代表无穷大,min 和 max 不能同时缺省
- 左闭右开区间:[min:max),表示大于等于 min 小于 max,当 max 缺省时,max 代表数值 0,当 min 缺省时,min 代表无穷大,min 和 max 不能同时缺省
- 闭区间:[min:max],表示大于等于 min 小于等于 max,当 min 缺省时,min 代表数值 0,当 max 缺省时,max 代表无穷大,min 和 max 不能同时缺省
- 开区间:(min:max),表示大于 min 小于 max,当 min 缺省时,min 代表数值 0,当 max 缺省时,max 代表无穷大,min 和 max 不能同时缺省
中间件
声明中间件
可以通过 api 语言来声明中间件, 在 api 语言中,使用 middleware 关键字来声明中间件,中间件的声明格式如下:
@server( middleware: DemoMiddleware)然后使用goctl api go --api user.api --dir .命令来自动生成声明中间件的代码。使用该命令后 internal 目录下会多出来一个 middleware 目录用来存放中间件。生成的中间件代码如下:
type DemoMiddleware struct {}func NewDemoMiddleware() *DemoMiddleware { return &DemoMiddleware{}}func (m *DemoMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // TODO generate middleware implement function, delete after code implementation // Passthrough to next handler if need next(w, r) }}中间件的代码是一个结构体,结构体中有一个 Handle 方法,这个方法是中间件的核心方法。该方法的介绍如下:
- 参数:http.HandlerFunc
- 返回值:http.HandlerFunc
- 使用说明:对请求进行处理,比如鉴权,日志记录等等,然后将请求传递给下一个中间件或者 handler。
逻辑代码编写
由于只是一个 demo 练习,所以中间件的任务是设置 Cookie 并统计中间件运行所花费的时间,然后将该时间通过上下文传递到下一个路由处理函数中。中间件代码如下:
func (m *DemoMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // 开始计算时间 nowTime := time.Now() // 中间件的业务逻辑部分: 设置cookie返回给前端 fmt.Print("开始调用中间件") http.SetCookie(w, &http.Cookie{ Name: "session_id", Value: "123456", }) // 计算花费的时间,并将花费的时间传递给下一个路由(通过在上下文当中设置) sub := time.Since(nowTime).Microseconds() ctx := context.WithValue(r.Context(), "cost", sub) // 创建一个新的请求,带有新的上下文 newReq := r.WithContext(ctx) next.ServeHTTP(w, newReq) }}路由处理函数的逻辑代码如下:
func (l *UserLoginLogic) UserLogin(req *types.LoginRequest) (resp *types.LoginResponse, err error) { // 接收上下文中的cost value, _ := l.ctx.Value("cost").(int64) // 接收请求参数 username := req.Username password := req.Password if username != "" && password != "" { costStr := strconv.FormatInt(value, 10) return &types.LoginResponse{ Code: http.StatusOK, Message: "登录成功," + "中间件花费的时间:[" + costStr + "]微秒", }, err } else { return nil, err }}依赖注入
在 go-zero中,是通过ServiceContext来管理和注入依赖项。所以在中间件的逻辑代码编写完成后,还需要配置依赖。配置如下:
type ServiceContext struct { Config config.Config DemoMiddleware rest.Middleware}func NewServiceContext(c config.Config) *ServiceContext { return &ServiceContext{ Config: c, DemoMiddleware: middleware.NewDemoMiddleware().Handle, }}也就是在 ServiceContext 中添加 DemoMiddleware 依赖配置,这个 DemoMiddleware 变量名并不是随便取的,而是使用命令goctl api go --api user.api --dir .后,在自动生成路由文件(router.go)中[]rest.Middleware{serverCtx.DemoMiddleware}可以知道是哪一个配置项。
测试
运行后的测试结果如下:


jwt 验证
api 文件定义
service user-api { @handler userLogin post /login (LoginRequest) returns (LoginResponse)}@server ( jwt: Auth // 开启 jwt 认证)service user-api { @handler userInfo get /userInfo (UserInfoRequest) returns (UserInfoResponse)}如上所示,通过在 @server 中来通过 jwt 关键字声明了开启 jwt 认证,且该 jwt 认证仅对其对应的路由有用,如上的 jwt 仅对 /userInfo 生效,对 /login 是不生效的。
jwt 配置编写
在 user-api.yaml 文件中添加如下内容:
Auth: AccessSecret: secretKey AccessExpire: 3600同样,需要映射到结构体中。所以在 config.go 文件中添加如下配置:
type Config struct { rest.RestConf // JWT 认证需要配置密钥和过期时间 Auth struct { AccessSecret string AccessExpire int64 }}使用 goctl 命令生成的路由文件中,在路由文件中可以看到如下内容:
server.AddRoutes( []rest.Route{ { Method: http.MethodGet, Path: "/userInfo", Handler: userInfoHandler(serverCtx), }, }, rest.WithJwt(serverCtx.Config.Auth.AccessSecret),)rest.WithJwt(serverCtx.Config.Auth.AccessSecret)表明/userInfo 路由需要进行 jwt 认证才能执行。
逻辑代码编写
-
登录功能的逻辑代码如下:
gofunc (l *UserLoginLogic) getJwtToken(secretKey, username string, iat, seconds int64) (string, error) { claims := make(jwt.MapClaims) claims["exp"] = iat + seconds claims["iat"] = iat claims["username"] = username token := jwt.New(jwt.SigningMethodHS256) token.Claims = claims return token.SignedString([]byte(secretKey))}func (l *UserLoginLogic) UserLogin(req *types.LoginRequest) (resp *types.LoginResponse, err error) { // 接收上下文中的cost value, _ := l.ctx.Value("cost").(int64) // iat表示token的颁发时间 iat := time.Now().Unix() // seconds表示token的有效期 seconds := l.svcCtx.Config.Auth.AccessExpire secretKey := l.svcCtx.Config.Auth.AccessSecret // 接收请求参数 username := req.Username password := req.Password if username != "" && password != "" { token, err := l.getJwtToken(secretKey, username, iat, seconds) if err != nil { logx.Infof("生成Token错误:%v\n", err.Error()) } costStr := strconv.FormatInt(value, 10) return &types.LoginResponse{ Code: http.StatusOK, Message: "登录成功," + "中间件花费的时间:[" + costStr + "]微秒", Token: token, }, err } else { return nil, err }}getJwtToken 方法用来生成 token 字符串。其中
AccessExpire和AccessSecret是从依赖中的配置中去获取。 -
返回用户信息的逻辑代码如下:
gofunc (l *UserInfoLogic) UserInfo(req *types.UserInfoRequest) (resp *types.UserInfoResponse, err error) { id := req.Id if id != "" { username, _ := l.ctx.Value("username").(string) return &types.UserInfoResponse{ Username: username, Age: 18, Other: map[string]interface{}{ "address": "江西", "hobby": []string{"篮球", "足球", "双色球"}, }, }, err } else { return nil, err }}
测试
测试结果如下:

