版本: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 loginpost /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.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
}
# 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配置介绍
// 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),}
}
// 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
}
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.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))
}
// 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语法介绍
# 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冲突
$ 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.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
}
运行结果:
在go-zero中,中间件可以分为路由中间件和全局中间件,路由中间件是指某一些特定路由需要实现中间件逻辑,其和jwt类似,没有放在jwt:xxx下的路由不会使用中间件功能, 而全局中间件的服务范围则是整个服务。
这里以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)
}
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)}}
}
在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.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),}
}
// 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.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
}
$ 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.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}
在平时的业务开发中,我们可以认为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 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":"用户名不存在"}
实现统一格式的body
响应,格式如下:
{"code": 0,"msg": "OK","data": {} // ①
}
我们提前在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)
}
修改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
进行初始化
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)}
}
修改前
{"message": "Hello go-zero!"
}
修改后
{"code": 0,"msg": "OK","data": {"message": "Hello go-zero!"}
}