go-zero 基础 -- 进阶指南
创始人
2025-06-01 17:31:11

版本:1.4.0

1、目录拆分

1.1 系统结构分析

在上文提到的商城系统中,每个系统在对外(http)提供服务的同时,也会提供数据给其他子系统进行数据访问的接口(rpc),因此每个子系统可以拆分成一个服务,而且对外提供了两种访问该系统的方式:api和rpc,因此, 以上系统按照目录结构来拆分有如下结构:

.
├── afterSale
│   ├── api
│   └── rpc
├── cart
│   ├── api
│   └── rpc
├── order
│   ├── api
│   └── rpc
├── pay
│   ├── api
│   └── rpc
├── product
│   ├── api
│   └── rpc
└── user├── api└── rpc

1.2 rpc调用链建议

在设计系统时,尽量做到服务之间调用链是单向的,而非循环调用,例如:order服务调用了user服务,而user服务反过来也会调用order的服务, 当其中一个服务启动故障,就会相互影响,进入死循环,你order认为是user服务故障导致的,而user认为是order服务导致的,如果有大量服务存在相互调用链, 则需要考虑服务拆分是否合理。

1.3 常见服务类型的目录结构

在上述服务中,仅列举了api/rpc服务,除此之外,一个服务下还可能有其他更多服务类型,如rmq(消息处理系统),cron(定时任务系统),script(脚本)等, 因此一个服务下可能包含以下目录结构:

user├── api //  http访问服务,业务需求实现├── cronjob // 定时任务,定时数据更新业务├── rmq // 消息处理系统:mq和dq,处理一些高并发和延时消息业务├── rpc // rpc服务,给其他子系统提供基础数据访问└── script // 脚本,处理一些临时运营需求,临时数据修复

1.4 完整工程目录结构示例

mall // 工程名称
├── common // 通用库
│   ├── randx
│   └── stringx
├── go.mod
├── go.sum
└── service // 服务存放目录├── afterSale│   ├── api│   └── model│   └── rpc├── cart│   ├── api│   └── model│   └── rpc├── order│   ├── api│   └── model│   └── rpc├── pay│   ├── api│   └── model│   └── rpc├── product│   ├── api│   └── model│   └── rpc└── user├── api├── cronjob├── model├── rmq├── rpc└── script

预设数据

INSERT INTO `user` (number,name,password,gender)values ('666','小明','123456','男');

2、model生成

首先,下载好演示工程 后,我们以user的model来进行代码生成演示。

2.1 前言

model是服务访问持久化数据层的桥梁,业务的持久化数据常存在于mysqlmongo等数据库中,我们都知道,对于一个数据库的操作莫过于CURD, 而这些工作也会占用一部分时间来进行开发,我曾经在编写一个业务时写了40个model文件,根据不同业务需求的复杂性,平均每个model文件差不多需要 10分钟,对于40个文件来说,400分钟的工作时间,差不多一天的工作量,而goctl工具可以在10秒钟来完成这400分钟的工作。

2.2 准备工作

进入演示工程book,找到的user.sql文件,将其在你自己的数据库中执行建表。

2.3 代码生成(带缓存)

方式一(ddl)

进入service/user/model目录,执行命令

$ cd service/user/model
$ goctl model mysql ddl -src user.sql -dir . -c
Done.

方式二(datasource)

$ goctl model mysql datasource -url="$datasource" -table="user" -c -dir .
Done.

方式三(intellij 插件)

在Goland中,右键user.sql,依次进入并点击New->Go Zero->Model Code即可生成,或者打开user.sql文件, 进入编辑区,使用快捷键Command+N(for mac OS)或者 alt+insert(for windows),选择Mode Code即可

更多

对于持久化数据,如果需要更灵活的数据库能力,包括事务能力,可以参考 Mysql

如果需要分布式事务的能力,可以参考 分布式事务支持

3、api文件编写

3.1 编写user.api文件

# service/user/api/user.api type (LoginReq {Username string `json:"username"`Password string `json:"password"`}LoginReply {Id           int64 `json:"id"`Name         string `json:"name"`Gender       string `json:"gender"`AccessToken  string `json:"accessToken"`AccessExpire int64 `json:"accessExpire"`RefreshAfter int64 `json:"refreshAfter"`}
)service user-api {@handler loginpost /user/login (LoginReq) returns (LoginReply)
}

3.2 生成api服务

方式一

$ cd book/service/user/api
$ goctl api go -api user.api -dir . 
Done.

方式二

user.api 文件右键,依次点击进入New->Go Zero->Api Code,进入目标目录选择,即api源码的目标存放目录,默认为user.api所在目录,选择好目录后点击OK即可。

方式三

打开user.api,进入编辑区,使用快捷键Command+N(for mac OS)或者 alt+insert(for windows),选择Api Code,同样进入目录选择弹窗,选择好目录后点击OK即可。

4、业务编码

4.1 添加Mysql配置

// service/user/api/internal/config/config.gopackage configimport ("github.com/zeromicro/go-zero/rest""github.com/zeromicro/go-zero/core/stores/cache")type Config struct {rest.RestConfMysql struct{DataSource string}CacheRedis cache.CacheConf
}

4.2 完善yaml配置

# service/user/api/etc/user-api.yamlName: user-api
Host: 0.0.0.0
Port: 8888
Mysql:DataSource: $user:$password@tcp($url)/$db?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
CacheRedis:- Host: $hostPass: $passType: node

$user: mysql数据库user
$password: mysql数据库密码
$url: mysql数据库连接地址
$db: mysql数据库db名称,即user表所在database
$host: redis连接地址 格式:ip:port,如:127.0.0.1:6379
$pass: redis密码
更多配置信息,请参考api配置介绍

4.3 完善服务依赖

// service/user/api/internal/svc/servicecontext.gotype ServiceContext struct {Config    config.ConfigUserModel model.UserModel
}func NewServiceContext(c config.Config) *ServiceContext {conn:=sqlx.NewMysql(c.Mysql.DataSource)return &ServiceContext{Config: c,UserModel: model.NewUserModel(conn,c.CacheRedis),}
}

4.4 填充登录逻辑

// service/user/api/internal/logic/loginlogic.gofunc (l *LoginLogic) Login(req *types.LoginReq) (*types.LoginReply, error) {if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 {return nil, errors.New("参数错误")}userInfo, err := l.svcCtx.UserModel.FindOneByNumber(l.ctx, req.Username)switch err {case nil:case model.ErrNotFound:return nil, errors.New("用户名不存在")default:return nil, err}if userInfo.Password != req.Password {return nil, errors.New("用户密码不正确")}// ---start---now := time.Now().Unix()accessExpire := l.svcCtx.Config.Auth.AccessExpirejwtToken, err := l.getJwtToken(l.svcCtx.Config.Auth.AccessSecret, now, l.svcCtx.Config.Auth.AccessExpire, userInfo.Id)if err != nil {return nil, err}// ---end---return &types.LoginReply{Id:           userInfo.Id,Name:         userInfo.Name,	Gender:       userInfo.Gender,AccessToken:  jwtToken,AccessExpire: now + accessExpire,RefreshAfter: now + accessExpire/2,}, nil
}  

5、jwt鉴权

JSON Web Token(令牌)(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑而独立的方法,用于在各方之间安全地将信息作为JSON对象传输。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用秘钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对对JWT进行签名。

5.1 什么时候应该使用JWT

授权:这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单一登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。

信息交换:JSON Web令牌是在各方之间安全地传输信息的一种好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。

5.2 为什么要使用JSON Web令牌

让我们讨论一下JSON Web Tokens (JWT) 与Simple Web Tokens (SWT)和Security Assertion Markup Language Tokens(SAML)相比的优点。

由于JSON不如XML冗长,因此在编码时JSON的大小也较小,从而使JWT比SAML更为紧凑。这使得JWT是在HTML和HTTP环境中传递的不错的选择。

在安全方面,只能使用HMAC算法由共享机密对SWT进行对称签名。但是,JWT和SAML令牌可以使用X.509证书形式的公用/专用密钥对进行签名。与签署JSON的简单性相比,使用XML Digital Signature签署XML而不引入模糊的安全漏洞是非常困难的。

JSON解析器在大多数编程语言中都很常见,因为它们直接映射到对象。相反,XML没有自然的文档到对象的映射。与SAML断言相比,这使使用JWT更加容易。

关于用法,JWT是在Internet规模上使用的。这突显了在多个平台(尤其是移动平台)上对JSON Web令牌进行客户端处理的简便性。

以上内容全部来自jwt官网介绍

5.2 go-zero中怎么使用jwt

jwt鉴权一般在api层使用,我们这次演示工程中分别在user api登录时生成jwt token,在search api查询图书时验证用户jwt token两步来实现。

user api生成jwt token

接着业务编码章节的内容,我们完善上一节遗留的getJwtToken方法,即生成jwt token逻辑

添加配置定义和yaml配置项

// service/user/api/internal/config/config.go
type Config struct {rest.RestConfMysql struct{DataSource string}CacheRedis cache.CacheConfAuth      struct {AccessSecret stringAccessExpire int64}
}
# service/user/api/etc/user-api.yamlName: user-api
Host: 0.0.0.0
Port: 8888
Mysql:DataSource: $user:$password@tcp($url)/$db?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
CacheRedis:- Host: $hostPass: $passType: node
Auth:AccessSecret: $AccessSecretAccessExpire: $AccessExpire

$AccessSecret:生成jwt token的密钥,最简单的方式可以使用一个uuid值。
$AccessExpire:jwt token有效期,单位:秒
api配置介绍

// service/user/api/internal/logic/loginlogic.gofunc (l *LoginLogic) getJwtToken(secretKey string, iat, seconds, userId int64) (string, error) {claims := make(jwt.MapClaims)claims["exp"] = iat + secondsclaims["iat"] = iatclaims["userId"] = userIdtoken := jwt.New(jwt.SigningMethodHS256)token.Claims = claimsreturn token.SignedString([]byte(secretKey))
}

search api使用jwt token鉴权

编写search.api文件

// service/search/api/search.api
type (SearchReq {// 图书名称Name string `form:"name"`}SearchReply {Name string `json:"name"`Count int `json:"count"`}
)@server(jwt: Auth // 开启jwt鉴权
)
service search-api {@handler searchget /search/do (SearchReq) returns (SearchReply)
}service search-api {@handler pingget /search/ping
}

api语法介绍

生成代码

添加yaml配置项

# service/search/api/etc/search-api.yaml
Name: search-api
Host: 0.0.0.0
Port: 8889
Auth:AccessSecret: $AccessSecretAccessExpire: $AccessExpire

$AccessSecret:这个值必须要和user api中声明的一致。
$AccessExpire: 有效期
这里修改一下端口,避免和user api端口8888冲突

验证 jwt token

  • 启动user api服务,登录
$ cd service/user/api
$ go run user.go -f etc/user-api.yaml
Starting server at 0.0.0.0:8888...
$ curl -i -X POST \http://127.0.0.1:8888/user/login \-H 'Content-Type: application/json' \-d '{"username":"666","password":"123456"
}'

访问结果:

{"id": 1,"name": "小明","gender": "男","accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Nzk0MDI4NzUsImlhdCI6MTY3OTM2Njg3NSwidXNlcklkIjoxfQ.kjAEZ2f5KBX6cS-Zc74ByWWJCsC3lMJWEh507wSxwsA","accessExpire": 1679402875,"refreshAfter": 1679384875
}

在这里插入图片描述

启动search api服务,调用/search/do验证jwt鉴权是否通过

$ go run search.go -f etc/search-api.yaml
Starting server at 0.0.0.0:8889...

我们先不传jwt token,看看结果

$ curl -i -X GET \'http://127.0.0.1:8889/search/do?name=%E8%A5%BF%E6%B8%B8%E8%AE%B0'

在这里插入图片描述

很明显,jwt鉴权失败了,返回401statusCode,接下来我们带一下jwt token(即用户登录返回的accessToken

$ curl -i -X GET \'http://127.0.0.1:8889/search/do?name=%E8%A5%BF%E6%B8%B8%E8%AE%B0' \-H 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Nzk0MDI4NzUsImlhdCI6MTY3OTM2Njg3NSwidXNlcklkIjoxfQ.kjAEZ2f5KBX6cS-Zc74ByWWJCsC3lMJWEh507wSxwsA'

在这里插入图片描述

获取jwt token中携带的信息

go-zero从jwt token解析后会将用户生成token时传入的kv原封不动的放在http.RequestContext中,因此我们可以通过Context就可以拿到你想要的值

// service/search/api/internal/logic/searchlogic.gofunc (l *SearchLogic) Search(req *types.SearchReq) (*types.SearchReply, error) {logx.Infof("userId: %v",l.ctx.Value("userId"))// 这里的key和生成jwt token时传入的key一致return &types.SearchReply{}, nil
}

运行结果:

在这里插入图片描述

6、中间件使用

6.1 中间件分类

在go-zero中,中间件可以分为路由中间件全局中间件,路由中间件是指某一些特定路由需要实现中间件逻辑,其和jwt类似,没有放在jwt:xxx下的路由不会使用中间件功能, 而全局中间件的服务范围则是整个服务。

6.2 中间件使用

这里以search服务为例来演示中间件的使用

路由中间件

  • 重新编写search.api文件,添加middleware声明
service/search/api/search.apitype SearchReq struct {}type SearchReply struct {}@server(jwt: Authmiddleware: Example // 路由中间件声明
)
service search-api {@handler searchget /search/do (SearchReq) returns (SearchReply)
}
  • 重新生成api代码
goctl api go -api search.api -dir . 

生成完后会在internal目录下多一个middleware的目录,这里即中间件文件,后续中间件的实现逻辑也在这里编写。

  • 完善资源依赖ServiceContext
// service/search/api/internal/svc/servicecontext.gotype ServiceContext struct {Config config.ConfigExample rest.Middleware
}func NewServiceContext(c config.Config) *ServiceContext {return &ServiceContext{Config: c,Example: middleware.NewExampleMiddleware().Handle,}
}
  • 编写中间件逻辑

这里仅添加一行日志,内容example middle,如果服务运行输出example middle则代表中间件使用起来了。

package middlewareimport "net/http"type ExampleMiddleware struct {
}func NewExampleMiddleware() *ExampleMiddleware {return &ExampleMiddleware{}
}func (m *ExampleMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {logx.Info("example middle")return func(w http.ResponseWriter, r *http.Request) {// TODO generate middleware implement function, delete after code implementation// Passthrough to next handler if neednext(w, r)}
}
  • 启动服务验证
{"@timestamp":"2023-03-21T11:17:21.479+08:00","caller":"middleware/examplemiddleware.go:16","content":"example middle","level":"info"}

全局中间件

通过rest.Server提供的Use方法即可

func main() {flag.Parse()var c config.Configconf.MustLoad(*configFile, &c)ctx := svc.NewServiceContext(c)server := rest.MustNewServer(c.RestConf)defer server.Stop()// 全局中间件server.Use(func(next http.HandlerFunc) http.HandlerFunc {return func(w http.ResponseWriter, r *http.Request) {logx.Info("global middleware")next(w, r)}})handler.RegisterHandlers(server, ctx)fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)server.Start()
}

在这里插入图片描述

在中间件里调用其它服务

通过闭包的方式把其它服务传递给中间件,示例如下:

// 模拟的其它服务
type AnotherService struct{}func (s *AnotherService) GetToken() string {return stringx.Rand()
}// 常规中间件
func middleware(next http.HandlerFunc) http.HandlerFunc {return func(w http.ResponseWriter, r *http.Request) {w.Header().Add("X-Middleware", "static-middleware")next(w, r)}
}// 调用其它服务的中间件
func middlewareWithAnotherService(s *AnotherService) rest.Middleware {return func(next http.HandlerFunc) http.HandlerFunc {return func(w http.ResponseWriter, r *http.Request) {w.Header().Add("X-Middleware", s.GetToken())next(w, r)}}
}

7、rpc编写与调用

在go-zero,我们使用zrpc来进行服务间的通信,zrpc是基于grpc

7.1 场景

在前面我们完善了对用户进行登录,用户查询图书等接口协议,但是用户在查询图书时没有做任何用户校验,如果当前用户是一个不存在的用户则我们不允许其查阅图书信息, 从上文信息我们可以得知,需要user服务提供一个方法来获取用户信息供search服务使用,因此我们就需要创建一个user rpc服务,并提供一个getUser方法。

7.2 rpc服务编写

  • 编译proto文件
// service/user/rpc/user.proto
syntax = "proto3";package user;option go_package = "./user";message IdReq{int64 id = 1;
}message UserInfoReply{int64 id = 1;string name = 2;string number = 3;string gender = 4;
}service user {rpc getUser(IdReq) returns(UserInfoReply);
}
  • 生成rpc服务代码
$ cd service/user/rpc
$ goctl rpc protoc user.proto --go_out=./types --go-grpc_out=./types --zrpc_out=.

如果安装的 protoc-gen-go 版大于1.4.0, proto文件建议加上go_package

  • 添加配置及完善yaml配置项
// service/user/rpc/internal/config/config.gotype Config struct {zrpc.RpcServerConfMysql struct {DataSource string}CacheRedis cache.CacheConf
}
# service/user/rpc/etc/user.yaml
Name: user.rpc
ListenOn: 127.0.0.1:8080
Etcd:Hosts:- $etcdHostKey: user.rpc
Mysql:DataSource: $user:$password@tcp($url)/$db?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
CacheRedis:- Host: $hostPass: $passType: node  
  • 添加资源依赖
// service/user/rpc/internal/svc/servicecontext.go  type ServiceContext struct {Config    config.ConfigUserModel model.UserModel
}func NewServiceContext(c config.Config) *ServiceContext {conn := sqlx.NewMysql(c.Mysql.DataSource)return &ServiceContext{Config: c,UserModel: model.NewUserModel(conn, c.CacheRedis),}
}
  • 添加rpc逻辑
// ervice/user/rpc/internal/logic/getuserlogic.go
func (l *GetUserLogic) GetUser(in *user.IdReq) (*user.UserInfoReply, error) {one, err := l.svcCtx.UserModel.FindOne(l.ctx, in.Id)if err != nil {return nil, err}return &user.UserInfoReply{Id:     one.Id,Name:   one.Name,Number: one.Number,Gender: one.Gender,}, nil
}

7.3 使用rpc

接下来我们在search服务中调用user rpc

  • 添加UserRpc配置及yaml配置项
// service/search/api/internal/config/config.gotype Config struct {rest.RestConfAuth struct {AccessSecret stringAccessExpire int64}UserRpc zrpc.RpcClientConf
}
// service/search/api/etc/search-api.yamlName: search-api
Host: 0.0.0.0
Port: 8889
Auth:AccessSecret: $AccessSecretAccessExpire: $AccessExpire
UserRpc:Etcd:Hosts:- $etcdHostKey: user.rpc
TIP

etcd中的Key必须要和user rpc服务配置中Key一致

  • 添加依赖
// service/search/api/internal/svc/servicecontext.gotype ServiceContext struct {Config  config.ConfigExample rest.MiddlewareUserRpc userclient.User
}func NewServiceContext(c config.Config) *ServiceContext {return &ServiceContext{Config:  c,Example: middleware.NewExampleMiddleware().Handle,UserRpc: userclient.NewUser(zrpc.MustNewClient(c.UserRpc)),}
}
  • 补充逻辑
// /service/search/api/internal/logic/searchlogic.gofunc (l *SearchLogic) Search(req *types.SearchReq) (*types.SearchReply, error) {userIdNumber := json.Number(fmt.Sprintf("%v", l.ctx.Value("userId")))logx.Infof("userId: %s", userIdNumber)userId, err := userIdNumber.Int64()if err != nil {return nil, err}// 使用user rpc_, err = l.svcCtx.UserRpc.GetUser(l.ctx, &user.IdReq{Id: userId,})if err != nil {return nil, err}return &types.SearchReply{Name:  req.Name,Count: 100,}, nil
}

7.4 启动并验证服务

  • 启动user rpc
$ cd service/user/rpc
$ go run user.go -f etc/user.yaml
  • 启动search api
$ cd service/search/api
$ go run search.go -f etc/search-api.yaml
  • 验证服务
$ curl -i -X GET \'http://127.0.0.1:8889/search/do?name=%E8%A5%BF%E6%B8%B8%E8%AE%B0' \-H 'authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Nzk0MDI4NzUsImlhdCI6MTY3OTM2Njg3NSwidXNlcklkIjoxfQ.kjAEZ2f5KBX6cS-Zc74ByWWJCsC3lMJWEh507wSxwsA'
HTTP/1.1 200 OK
Content
-Type: application/json
Date: Tue, 09 Feb 2021 06:05:52 GMT
Content-Length: 32{"name":"西游记","count":100}

8、错误处理

在平时的业务开发中,我们可以认为http状态码不为2xx系列的,都可以认为是http请求错误, 并伴随响应的错误信息,但这些错误信息都是以plain text形式返回的。除此之外,我在业务中还会定义一些业务性错误,常用做法都是通过 codemsg 两个字段来进行业务处理结果描述,并且希望能够以json响应体来进行响应。

8.1 业务错误响应格式

  • 业务处理正常
{"code": 0,"msg": "successful","data": {....}
}
  • 业务处理异常
{"code": 10001,"msg": "参数错误"
}

8.2 user api之login

在之前,我们在登录逻辑中处理用户名不存在时,直接返回来一个error。我们来登录并传递一个不存在的用户名看看效果。

curl -X POST \http://127.0.0.1:8888/user/login \-H 'content-type: application/json' \-d '{"username":"1","password":"123456"
}'
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Tue, 09 Feb 2021 06:38:42 GMT
Content-Length: 19用户名不存在

接下来我们将其以json格式进行返回

8.3 自定义错误

首先在common中添加一个baseerror.go文件,并填入代码

$ cd common
$ mkdir errorx && cd errorx
$ vim baseerror.go
package errorxconst defaultCode = 1001type CodeError struct {Code int    `json:"code"`Msg  string `json:"msg"`
}type CodeErrorResponse struct {Code int    `json:"code"`Msg  string `json:"msg"`
}func NewCodeError(code int, msg string) error {return &CodeError{Code: code, Msg: msg}
}func NewDefaultError(msg string) error {return NewCodeError(defaultCode, msg)
}func (e *CodeError) Error() string {return e.Msg
}func (e *CodeError) Data() *CodeErrorResponse {return &CodeErrorResponse{Code: e.Code,Msg:  e.Msg,}
}

将登录逻辑中错误用CodeError自定义错误替换

if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 {return nil, errorx.NewDefaultError("参数错误")}userInfo, err := l.svcCtx.UserModel.FindOneByNumber(req.Username)switch err {case nil:case model.ErrNotFound:return nil, errorx.NewDefaultError("用户名不存在")default:return nil, err}if userInfo.Password != req.Password {return nil, errorx.NewDefaultError("用户密码不正确")}now := time.Now().Unix()accessExpire := l.svcCtx.Config.Auth.AccessExpirejwtToken, err := l.getJwtToken(l.svcCtx.Config.Auth.AccessSecret, now, l.svcCtx.Config.Auth.AccessExpire, userInfo.Id)if err != nil {return nil, err}return &types.LoginReply{Id:           userInfo.Id,Name:         userInfo.Name,Gender:       userInfo.Gender,AccessToken:  jwtToken,AccessExpire: now + accessExpire,RefreshAfter: now + accessExpire/2,}, nil

开启自定义错误

// service/user/api/user.gofunc main() {flag.Parse()var c config.Configconf.MustLoad(*configFile, &c)ctx := svc.NewServiceContext(c)server := rest.MustNewServer(c.RestConf)defer server.Stop()handler.RegisterHandlers(server, ctx)// 自定义错误
httpx.SetErrorHandlerCtx(func(ctx context.Context, err error) (int, interface{}) {switch e := err.(type) {case *errorx.CodeError:return http.StatusOK, e.Data()default:return http.StatusInternalServerError, nil}
})fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)server.Start()
}

重启服务验证

$ curl -i -X POST \http://127.0.0.1:8888/user/login \-H 'content-type: application/json' \-d '{"username":"1","password":"123456"
}'
HTTP/1.1 200 OK
Content-Type: application/json
Date: Tue, 09 Feb 2021 06:47:29 GMT
Content-Length: 40{"code":1001,"msg":"用户名不存在"}

9、模板修改

9.1 场景

实现统一格式的body响应,格式如下:

{"code": 0,"msg": "OK","data": {} // ①
}

9.2 准备工作

我们提前在module为greet的工程下的response包中写一个Response方法,目录树类似如下:

greet
├── response
│   └── response.go
└── xxx...
package responseimport ("net/http""github.com/zeromicro/go-zero/rest/httpx"
)type Body struct {Code int         `json:"code"`Msg  string      `json:"msg"`Data interface{} `json:"data,omitempty"`
}func Response(w http.ResponseWriter, resp interface{}, err error) {var body Bodyif err != nil {body.Code = -1body.Msg = err.Error()} else {body.Msg = "OK"body.Data = resp}httpx.OkJson(w, body)
}

9.3 修改handler模板

修改handler模板

//  ~/.goctl/${goctl版本号}/api/handler.tplpackage handlerimport ("net/http""greet/response"// ①{{.ImportPackages}}
)func {{.HandlerName}}(svcCtx *svc.ServiceContext) http.HandlerFunc {return func(w http.ResponseWriter, r *http.Request) {{{if .HasRequest}}var req types.{{.RequestType}}if err := httpx.Parse(r, &req); err != nil {httpx.Error(w, err)return}{{end}}l := logic.New{{.LogicType}}(r.Context(), svcCtx){{if .HasResp}}resp, {{end}}err := l.{{.Call}}({{if .HasRequest}}&req{{end}}){{if .HasResp}}response.Response(w, resp, err){{else}}response.Response(w, nil, err){{end}}//②}
}

1.如果本地没有~/.goctl/${goctl版本号}/api/handler.tpl文件,可以通过模板初始化命令goctl template init进行初始化

9.4 修改模板前后对比

  • 修改前
func GreetHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {return func(w http.ResponseWriter, r *http.Request) {var req types.Requestif err := httpx.Parse(r, &req); err != nil {httpx.Error(w, err)return}l := logic.NewGreetLogic(r.Context(), svcCtx)resp, err := l.Greet(&req)// 以下内容将被自定义模板替换if err != nil {httpx.Error(w, err)} else {httpx.OkJson(w, resp)}}
}
  • 修改后
func GreetHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {return func(w http.ResponseWriter, r *http.Request) {var req types.Requestif err := httpx.Parse(r, &req); err != nil {httpx.Error(w, err)return}l := logic.NewGreetLogic(r.Context(), svcCtx)resp, err := l.Greet(&req)response.Response(w, resp, err)}
}

9.5 修改模板前后响应体对比

修改前

{"message": "Hello go-zero!"
}

修改后

{"code": 0,"msg": "OK","data": {"message": "Hello go-zero!"}
}

相关内容

热门资讯

[第一财经]“ 芒果竞技 辅助... 您好:芒果竞技这款游戏可以开挂,确实是有挂的,需要软件加微信【5537821】,很多玩家在永和备厅这...
实测分享“熊猫互娱透视软件辅助... 您好:熊猫互娱这款游戏可以开挂,确实是有挂的,需要了解加客服微信【6355786】很多玩家在熊猫互娱...
科技通报“新青龙究竟有没有挂”... 您好:新青龙这款游戏可以开挂,确实是有挂的,需要软件加微信【8487422】很多玩家在这款游戏中打牌...
今日实测「乐游棋牌」可以开挂吗... 您好:乐游棋牌这款游戏可以开挂,确实是有挂的,需要了解加客服微信【9183893】很多玩家在这款游戏...
玩家实测“天天微友.是不是有挂... 您好:天天微友.这款游戏可以开挂,确实是有挂的,需要了解加客服微信【3636476】很多玩家在这款游...