版本:1.4.0
在上文提到的商城系统中,每个系统在对外(http)提供服务的同时,也会提供数据给其他子系统进行数据访问的接口(rpc),因此每个子系统可以拆分成一个服务,而且对外提供了两种访问该系统的方式:api和rpc,因此, 以上系统按照目录结构来拆分有如下结构:
.
├── afterSale
│ ├── api
│ └── rpc
├── cart
│ ├── api
│ └── rpc
├── order
│ ├── api
│ └── rpc
├── pay
│ ├── api
│ └── rpc
├── product
│ ├── api
│ └── rpc
└── user
├── api
└── rpc
在设计系统时,尽量做到服务之间调用链是单向的,而非循环调用,例如:order服务调用了user服务,而user服务反过来也会调用order的服务, 当其中一个服务启动故障,就会相互影响,进入死循环,你order认为是user服务故障导致的,而user认为是order服务导致的,如果有大量服务存在相互调用链, 则需要考虑服务拆分是否合理。
在上述服务中,仅列举了api/rpc服务,除此之外,一个服务下还可能有其他更多服务类型,如rmq(消息处理系统),cron(定时任务系统),script(脚本)等, 因此一个服务下可能包含以下目录结构:
user
├── api // http访问服务,业务需求实现
├── cronjob // 定时任务,定时数据更新业务
├── rmq // 消息处理系统:mq和dq,处理一些高并发和延时消息业务
├── rpc // rpc服务,给其他子系统提供基础数据访问
└── script // 脚本,处理一些临时运营需求,临时数据修复
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','男');
首先,下载好演示工程 后,我们以user的model来进行代码生成演示。
model
是服务访问持久化数据层的桥梁,业务的持久化数据常存在于mysql、mongo等数据库中,我们都知道,对于一个数据库的操作莫过于CURD, 而这些工作也会占用一部分时间来进行开发,我曾经在编写一个业务时写了40个model文件,根据不同业务需求的复杂性,平均每个model文件差不多需要 10分钟,对于40个文件来说,400分钟的工作时间,差不多一天的工作量,而goctl工具可以在10秒钟来完成这400分钟的工作。
进入演示工程book,找到的user.sql
文件,将其在你自己的数据库中执行建表。
进入service/user/model
目录,执行命令
$ cd service/user/model
$ goctl model mysql ddl -src user.sql -dir . -c
Done.
$ goctl model mysql datasource -url="$datasource" -table="user" -c -dir .
Done.
在Goland中,右键user.sql
,依次进入并点击New->Go Zero->Model Code
即可生成,或者打开user.sql文件, 进入编辑区,使用快捷键Command+N
(for mac OS)或者 alt+insert
(for windows),选择Mode Code
即可
对于持久化数据,如果需要更灵活的数据库能力,包括事务能力,可以参考 Mysql
如果需要分布式事务的能力,可以参考 分布式事务支持
# 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 login
post /user/login (LoginReq) returns (LoginReply)
}
$ 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
即可。
// service/user/api/internal/config/config.go
package config
import (
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/core/stores/cache"
)
type Config struct {
rest.RestConf
Mysql struct{
DataSource string
}
CacheRedis cache.CacheConf
}
# service/user/api/etc/user-api.yaml
Name: 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: $host
Pass: $pass
Type: 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配置介绍
// service/user/api/internal/svc/servicecontext.go
type ServiceContext struct {
Config config.Config
UserModel model.UserModel
}
func NewServiceContext(c config.Config) *ServiceContext {
conn:=sqlx.NewMysql(c.Mysql.DataSource)
return &ServiceContext{
Config: c,
UserModel: model.NewUserModel(conn,c.CacheRedis),
}
}
// service/user/api/internal/logic/loginlogic.go
func (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.AccessExpire
jwtToken, 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
}
JSON Web Token(令牌)(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑而独立的方法,用于在各方之间安全地将信息作为JSON对象传输。
由于此信息是经过数字签名的,因此可以被验证和信任
。可以使用秘钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对对JWT进行签名。
授权
:这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单一登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。
信息交换
:JSON Web令牌是在各方之间安全地传输信息的一种好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。
让我们讨论一下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官网介绍
jwt鉴权一般在api层使用,我们这次演示工程中分别在user
api登录时生成jwt token,在search
api查询图书时验证用户jwt token两步来实现。
接着业务编码章节的内容,我们完善上一节遗留的getJwtToken
方法,即生成jwt token逻辑
// service/user/api/internal/config/config.go
type Config struct {
rest.RestConf
Mysql struct{
DataSource string
}
CacheRedis cache.CacheConf
Auth struct {
AccessSecret string
AccessExpire int64
}
}
# service/user/api/etc/user-api.yaml
Name: 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: $host
Pass: $pass
Type: node
Auth:
AccessSecret: $AccessSecret
AccessExpire: $AccessExpire
$AccessSecret:生成jwt token的密钥,最简单的方式可以使用一个uuid值。
$AccessExpire:jwt token有效期,单位:秒
api配置介绍
// service/user/api/internal/logic/loginlogic.go
func (l *LoginLogic) getJwtToken(secretKey string, iat, seconds, userId int64) (string, error) {
claims := make(jwt.MapClaims)
claims["exp"] = iat + seconds
claims["iat"] = iat
claims["userId"] = userId
token := jwt.New(jwt.SigningMethodHS256)
token.Claims = claims
return token.SignedString([]byte(secretKey))
}
// 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 search
get /search/do (SearchReq) returns (SearchReply)
}
service search-api {
@handler ping
get /search/ping
}
api语法介绍
# service/search/api/etc/search-api.yaml
Name: search-api
Host: 0.0.0.0
Port: 8889
Auth:
AccessSecret: $AccessSecret
AccessExpire: $AccessExpire
$AccessSecret:这个值必须要和user api中声明的一致。
$AccessExpire: 有效期
这里修改一下端口,避免和user api端口8888冲突
$ 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鉴权失败了,返回401
的statusCode
,接下来我们带一下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'
go-zero从jwt token解析后会将用户生成token时传入的kv原封不动的放在http.Request
的Context
中,因此我们可以通过Context
就可以拿到你想要的值
// service/search/api/internal/logic/searchlogic.go
func (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
}
运行结果:
在go-zero中,中间件可以分为路由中间件和全局中间件,路由中间件是指某一些特定路由需要实现中间件逻辑,其和jwt类似,没有放在jwt:xxx下的路由不会使用中间件功能, 而全局中间件的服务范围则是整个服务。
这里以search
服务为例来演示中间件的使用
search.api
文件,添加middleware
声明service/search/api/search.api
type SearchReq struct {}
type SearchReply struct {}
@server(
jwt: Auth
middleware: Example // 路由中间件声明
)
service search-api {
@handler search
get /search/do (SearchReq) returns (SearchReply)
}
goctl api go -api search.api -dir .
生成完后会在internal
目录下多一个middleware
的目录,这里即中间件文件,后续中间件的实现逻辑也在这里编写。
ServiceContext
// service/search/api/internal/svc/servicecontext.go
type ServiceContext struct {
Config config.Config
Example rest.Middleware
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
Example: middleware.NewExampleMiddleware().Handle,
}
}
这里仅添加一行日志,内容example middle
,如果服务运行输出example middle
则代表中间件使用起来了。
package middleware
import "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 need
next(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.Config
conf.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)
}
}
}
在go-zero,我们使用zrpc来进行服务间的通信,zrpc是基于grpc
。
在前面我们完善了对用户进行登录,用户查询图书等接口协议,但是用户在查询图书时没有做任何用户校验,如果当前用户是一个不存在的用户则我们不允许其查阅图书信息, 从上文信息我们可以得知,需要user
服务提供一个方法来获取用户信息供search
服务使用,因此我们就需要创建一个user
rpc服务,并提供一个getUser
方法。
// 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);
}
$ 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
// service/user/rpc/internal/config/config.go
type Config struct {
zrpc.RpcServerConf
Mysql struct {
DataSource string
}
CacheRedis cache.CacheConf
}
# service/user/rpc/etc/user.yaml
Name: user.rpc
ListenOn: 127.0.0.1:8080
Etcd:
Hosts:
- $etcdHost
Key: user.rpc
Mysql:
DataSource: $user:$password@tcp($url)/$db?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
CacheRedis:
- Host: $host
Pass: $pass
Type: node
// service/user/rpc/internal/svc/servicecontext.go
type ServiceContext struct {
Config config.Config
UserModel model.UserModel
}
func NewServiceContext(c config.Config) *ServiceContext {
conn := sqlx.NewMysql(c.Mysql.DataSource)
return &ServiceContext{
Config: c,
UserModel: model.NewUserModel(conn, c.CacheRedis),
}
}
// 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
}
接下来我们在search
服务中调用user
rpc
// service/search/api/internal/config/config.go
type Config struct {
rest.RestConf
Auth struct {
AccessSecret string
AccessExpire int64
}
UserRpc zrpc.RpcClientConf
}
// service/search/api/etc/search-api.yaml
Name: search-api
Host: 0.0.0.0
Port: 8889
Auth:
AccessSecret: $AccessSecret
AccessExpire: $AccessExpire
UserRpc:
Etcd:
Hosts:
- $etcdHost
Key: user.rpc
TIP
etcd中的Key必须要和user rpc服务配置中Key一致
// service/search/api/internal/svc/servicecontext.go
type ServiceContext struct {
Config config.Config
Example rest.Middleware
UserRpc 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.go
func (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
}
$ cd service/user/rpc
$ go run user.go -f etc/user.yaml
$ 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.eyJleHAiOjE2Nzk1Njg5NjIsImlhdCI6MTY3OTUzMjk2MiwidXNlcklkIjoxfQ.hse0b2fEXwkcEG7xBpFrTtDBMbmUuGS_aZc-gU-F638'
在平时的业务开发中,我们可以认为http状态码不为2xx
系列的,都可以认为是http请求错误, 并伴随响应的错误信息,但这些错误信息都是以plain text形式返回的。除此之外,我在业务中还会定义一些业务性错误,常用做法都是通过 code
、msg
两个字段来进行业务处理结果描述,并且希望能够以json响应体来进行响应。
{
"code": 0,
"msg": "successful",
"data": {
....
}
}
{
"code": 10001,
"msg": "参数错误"
}
在之前,我们在登录逻辑中处理用户名不存在时,直接返回来一个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格式进行返回
首先在common
中添加一个baseerror.go
文件,并填入代码
$ cd common
$ mkdir errorx && cd errorx
$ vim baseerror.go
package errorx
const defaultCode = 1001
type 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.AccessExpire
jwtToken, 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.go
func main() {
flag.Parse()
var c config.Config
conf.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":"用户名不存在"}
实现统一格式的body
响应,格式如下:
{
"code": 0,
"msg": "OK",
"data": {} // ①
}
我们提前在module为greet
的工程下的response
包中写一个Response
方法,目录树类似如下:
greet
├── response
│ └── response.go
└── xxx...
package response
import (
"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 Body
if err != nil {
body.Code = -1
body.Msg = err.Error()
} else {
body.Msg = "OK"
body.Data = resp
}
httpx.OkJson(w, body)
}
修改handler
模板
// ~/.goctl/${goctl版本号}/api/handler.tpl
package handler
import (
"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
进行初始化
func GreetHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.Request
if 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.Request
if 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)
}
}
修改前
{
"message": "Hello go-zero!"
}
修改后
{
"code": 0,
"msg": "OK",
"data": {
"message": "Hello go-zero!"
}
}