go-zero 基础 -- 进阶指南

版本: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 login
    post /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.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
}

4.2 完善yaml配置

# 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配置介绍

4.3 完善服务依赖

// 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),
    }
}

4.4 填充登录逻辑

// 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
}  

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.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))
}

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 search
    get /search/do (SearchReq) returns (SearchReply)
}

service search-api {
    @handler ping
    get /search/ping
}

api语法介绍

生成代码

添加yaml配置项

# 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冲突

验证 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
}

go-zero 基础 -- 进阶指南_第1张图片

启动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'

go-zero 基础 -- 进阶指南_第2张图片

很明显,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'

go-zero 基础 -- 进阶指南_第3张图片

获取jwt token中携带的信息

go-zero从jwt token解析后会将用户生成token时传入的kv原封不动的放在http.RequestContext中,因此我们可以通过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
}

运行结果:

在这里插入图片描述

6、中间件使用

6.1 中间件分类

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

6.2 中间件使用

这里以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)
}
  • 重新生成api代码
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)
        }
    }
}

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.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),
    }
}
  • 添加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.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
}

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.eyJleHAiOjE2Nzk1Njg5NjIsImlhdCI6MTY3OTUzMjk2MiwidXNlcklkIjoxfQ.hse0b2fEXwkcEG7xBpFrTtDBMbmUuGS_aZc-gU-F638'

go-zero 基础 -- 进阶指南_第4张图片

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 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":"用户名不存在"}

9、模板修改

9.1 场景

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

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

9.2 准备工作

我们提前在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)
}

9.3 修改handler模板

修改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进行初始化

9.4 修改模板前后对比

  • 修改前
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)
    }
}

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

修改前

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

修改后

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

你可能感兴趣的:(Golang,golang,java,分布式)