go-zero 基础 -- 进阶指南

2023-11-14

版本: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
}

在这里插入图片描述

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

在这里插入图片描述

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!"
    }
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

go-zero 基础 -- 进阶指南 的相关文章

  • 使用 JDBC 获取 Oracle 11g 的最后插入 ID

    我是使用 Oracle 的新手 所以我将放弃之前已经回答过的内容这个问题 https stackoverflow com questions 3131064 get id of last inserted record in oracle
  • NoInitialContextException:heroku 战争部署

    我一直在开发一个 J2EE 项目 并且在其中使用连接池 也通过部署在 heroku 上的数据库进行访问 我使用以下代码来设置 Connection 对象 Context initContext new InitialContext Cont
  • “java.io.IOException:连接超时”和“SocketTimeoutException:读取超时”之间有什么区别

    如果我设置一个套接字 SoTimeout 并从中读取 当读取时间超过超时限制时 我会收到 SocketTimeoutException 读取超时 这是我的例子中的堆栈 java net SocketTimeoutException Read
  • 获取文件的锁

    我想在对特定文件开始 threo read 时获取文件上的锁定 以便其他应用程序无法读取已锁定的文件并希望在线程终止时释放锁定文件 您可以获得一个FileLock https docs oracle com javase 8 docs ap
  • Java 7 默认语言环境

    我刚刚安装了 jre7 我很惊讶地发现我的默认区域设置现在是 en US 对于jre6 它是de CH 与jre7有什么不同 默认区域设置不再是操作系统之一吗 顺便说一句 我使用的是Windows7 谢谢你的回答 编辑 我已经看到了语言环境
  • 使用 WebDriver 单击新打开的选项卡中的链接

    有人可以在这种情况下帮助我吗 场景是 有一个网页 我仅在新选项卡中打开所有指定的链接 现在我尝试单击新打开的选项卡中的任何一个链接 在下面尝试过 但它仅单击主 第一个选项卡中的一个链接 而不是在新选项卡中 new Actions drive
  • Oracle Java 教程 - 回答问题时可能出现错误

    我是 Java 新手 正在阅读 Oracle 教程 每个部分之后都有问题和答案 我不明白一个答案中的一句话 见下面的粗体线 来源是https docs oracle com javase tutorial java javaOO QandE
  • 埃拉托色尼筛法 - 实现返回一些非质数值?

    我用 Java 实现了埃拉托斯特尼筛法 通过伪代码 public static void sieveofEratosthenes int n boolean numArray numArray new boolean n for int i
  • 在 Struts 2 中传递 URL 参数而不使用查询字符串

    我想使用类似的 URL host ActionName 123 abc 而不是像这样传递查询字符串 host ActionName parm1 123 parm2 abc 我怎样才能在 Struts 2 中做到这一点 我按照下面的方法做了
  • 您建议使用哪种压缩(GZIP 是最流行的)servlet 过滤器?

    我正在寻找一个用于大容量网络应用程序的 GZIP servlet 过滤器 我不想使用容器特定的选项 要求 能够压缩响应负载 XML Faster 已在大批量应用的生产中得到验证 应适当设置适当内容编码 跨容器移植 可选择解压缩请求 谢谢 我
  • 添加到列表时有没有办法避免循环?

    我想知道这样的代码 List
  • 在 Java 中通过 XSLT 分解 XML

    我需要转换具有嵌套 分层 表单结构的大型 XML 文件
  • 如何停止执行的 Jar 文件

    这感觉像是一个愚蠢的问题 但我似乎无法弄清楚 当我在 Windows 上运行 jar 文件时 它不会出现在任务管理器进程中 我怎样才能终止它 我已经尝试过 TASKKILL 但它对我也不起作用 On Linux ps ef grep jav
  • 无法在 Java/Apache HttpClient 中处理带有垂直/管道栏的 url

    例如 如果我想处理这个网址 post new HttpPost http testurl com lists lprocess action LoadList 401814 1 Java Apache 不允许我这么做 因为它说竖线 是非法的
  • 如何从 Ant 启动聚合 jetty-server JAR?

    背景 免责声明 I have veryJava 经验很少 我们之前在 Ant 构建期间使用了 Jetty 6 的包装版本来处理按需静态内容 JS CSS 图像 HTML 因此我们可以使用 PhantomJS 针对 HTTP 托管环境运行单元
  • 无需登录即可直接从 Alfresco 访问文件/内容

    我的场景是这样的 我有一个使用 ALFRESCO CMS 来显示文件或图像的 Web 应用程序 我正在做的是在 Java servlet 中使用用户名和密码登录 alfresco 并且我可以获得该登录的票证 但我无法使用该票证直接从浏览器访
  • 如何处理 StaleElementReferenceException

    我正在为鼠标悬停工作 我想通过使用 for 循环单击每个链接来测试所有链接的工作条件 在我的程序中 迭代进行一次 而对于下一次迭代 它不起作用并显示 StaleElementReferenceException 如果需要 请修改代码 pub
  • Hadoop NoSuchMethodError apache.commons.cli

    我在用着hadoop 2 7 2我用 IntelliJ 做了一个 MapReduce 工作 在我的工作中 我正在使用apache commons cli 1 3 1我把库放在罐子里 当我在 Hadoop 集群上使用 MapReduceJob
  • ECDH使用Android KeyStore生成私钥

    我正在尝试使用 Android KeyStore Provider 生成的私有文件在 Android 中实现 ECDH public byte ecdh PublicKey otherPubKey throws Exception try
  • 将对象从手机共享到 Android Wear

    我创建了一个应用程序 在此应用程序中 您拥有包含 2 个字符串 姓名和年龄 和一个位图 头像 的对象 所有内容都保存到 sqlite 数据库中 现在我希望可以在我的智能手表上访问这些对象 所以我想实现的是你可以去启动 启动应用程序并向左和向

随机推荐

  • Android 权限大全-转载

    Android 权限大全 转自博客园 博客园链接 Key android permission ACCESS CHECKIN PROPERTIES Title 访问检入属性 Memo 允许对检入服务上传的属性进行读 写访问 普通应用程序不能
  • 初入HTML

    1 HTML语言用来做什么 html语言专门用来描述网页 它属于一种标记语言 它是由一组标签构成 2 HTML元素 一个HTML元素是包含了开始标签和结束标签 当然 还有一些是单标签 例如 p 段落标签 p 双标签 br 换行标签 单标签
  • openlayers地图坐标coordinate转换为屏幕像素坐标pixel

    openlayers地图坐标coordinate转换为屏幕像素坐标pixel 网上查资料试了很多人的方法 需要各种转换但没成功 后来发现openlayers的map对象自带该方法 记录下来 希望帮助到大家 方法说明 获取坐标的像素坐标 这将
  • 随机森林补充缺失值

    导入必要的库 import numpy as np import pandas as pd from sklearn ensemble import RandomForestRegressor 读取数据 data data all1 找出所
  • 3D数学基础——向量与矩阵变换

    向量相乘 1 点乘 两个向量的点乘等于他们的数乘结果乘以两个向量之间家教的余弦值 v k v k cos cos v k v k 通过点乘的结果计算两个非单位向量的夹角 2 叉乘 叉乘只在3d空间中有定义 他需要两个不平行向量作为输入 生成
  • nvm install node没反应_LINUX使用nvm安装node,nrm的使用

    为什么要使用nvm来安装node 我们在开发过程中 特别是协作开发时 通常会对具体的node的版本有限制 我们使用nvm可以轻松解决这个问题 nvm安装node的好处就是可以切换node版本 用起来方便 所以介绍下如何使用nvm安装node
  • 【Vs Code 学习笔记】

    Vs Code 远程连接服务器 详细教程 默认你已经安装好了Vs Code 1 如果没有请参考官网链接 https code visualstudio com 直接安装就可以了 2 打开VsCode 你可以看到如下界面 然后在按照如下操作下
  • Java的Properties属性集、获取项目路径的3种方式(干货满满)

    属性集介绍 集合家族中有个成员java util Properties 它继承于Hashtable Properties是使用键值结构存储数据的 但它最大的特点是具有持久化功能 持久化 内存 gt 硬盘 持久化的过程必须依赖于IO流 对IO
  • MyBatis执行器与新增返回主键问题

    前提 在写需求时碰到一个问题 在新增加一条数据时需要返回主键并进行后续操作 发现当前项目并不能返回主键 正常返回主键代码 1
  • PTA C 7-3 计算职工工资

    给定N个职员的信息 包括姓名 基本工资 浮动工资和支出 要求编写程序顺序输出每位职员的姓名和实发工资 实发工资 基本工资 浮动工资 支出 输入格式 输入在一行中给出正整数N 随后N行 每行给出一位职员的信息 格式为 姓名 基本工资 浮动工资
  • C++继承

    继承的概念 继承 inheritance 机制是面向对象程序设计使代码可以复用的重要的手段 它允许程序员在保持原有类特性的基础上进行扩展 增加功能 这样产生新的类 称为派生类 继承呈现了面向对象程序设计的层次结构 体现了由简单到复杂的认知过
  • maven 报错Failed to execute goal org.apache.maven.pluginsmaven-archetype-plugin3.2

    新手走过各种各样的坑 idea中maven基础配置中总是出现各种各样的错误 在网上找了一些资料 发现并没有找到切入主题的解决方法 走过的坑总是记忆尤新 idea第一次配置maven 提示如下所示错误 仔细检查了一个maven的配置文件 发现
  • 正态分布函数_从微积分角度证明“正态分布密度函数”

    本篇我们来证明一个常见的优美的积分等式 聪明你是否看出如下等式曾在哪里出现过呢 没错如下和正态分布中概率密度函数很像 但我们仅从积分学的角度来分析正面它 证明它灵活的数学技巧 你准备好了吗 因为e x 2是关于x的偶函数 所以我们明显可以想
  • 安装SAS可能遇到的各种问题

    近日 为了提升数据分析的效率 准备开始学习SAS相关内容 结合自身已经掌握的Python 希望在数据分析 挖掘方向走的越来越远 下面 来分享下我安装SAS过程中遇到的各种问题 真是一个一个坑走过来的 系统环境 Windows 10 安装版本
  • 在对话框中实现预览图形文件的功能

    一 使用 acdbDisplayPreviewFromDwg 函数 1 引用说明 此功能获取由指定的图形的预览图像 如果有 pszDwgfilename 将其显示在由HWND参数pPreviewWnd标识的窗口中 图像尺寸最大变化不超过25
  • Anaconda3最新换国内源教程,中科大源或者清华源

    环境 ubuntu16 04 anaconda python 3 7 中科大源 conda config add channels https mirrors ustc edu cn anaconda pkgs main conda con
  • esp32 完整开发指南_【安信可ESP32语音开发板专题①】ESP32-A1S音频开发板之离线语音识别控制LED灯

    本博客学习由 安信可开源团队 潜心编写 做ESP32 A1S离线语音初步入门技术交流分享 如有不完善之处 请留言 本团队及时更改 一 前言 离线语音 顾名思义 在不连网络的状态下 产品能识别语音指令并执行相应的控制输出 安信可基于乐鑫ESP
  • @SpringQueryMap注解 feign的get传参方式

    SpringQueryMap注解 feign的get传参方式 问题 启动服务 传入参数测试 发现feign远程调用的方法入参失败 排查发现是feign接口调用controller方法的时候就没进来参 原因 spring cloud项目使用f
  • armeabi-v7a、arm64-v8a、armeabi、x86、x86_64的区别

    1 armeabi v7a 第七代及以上的ARM处理器 2011年以后生产的大部分Android设备都使用 2 arm64 v8a 第8代 64位ARM处理器 很少设备 三星GalaxyS6是其中之一 3 armeabi 第5代 第6代的A
  • go-zero 基础 -- 进阶指南

    版本 1 4 0 1 目录拆分 1 1 系统结构分析 在上文提到的商城系统中 每个系统在对外 http 提供服务的同时 也会提供数据给其他子系统进行数据访问的接口 rpc 因此每个子系统可以拆分成一个服务 而且对外提供了两种访问该系统的方式