0%

go-zero入门实践三

说明

  • 入门实践一 介绍了单体服务和微服务的简单实例,入门实践二 介绍了单体服务结合mysql,redis,jwt等方便的知识
  • 本篇主要介绍为微服务的具体使用场景

user rpc

  • 把user/rpc 目录内容清空
  • user/rpc/user.proto 内容如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
syntax = "proto3";

package user;

// protoc-gen-go 版本大于1.4.0, proto文件需要加上go_package,否则无法生成
option go_package = "./user";

message IdRequest {
int64 id = 1;
}

message UserResponse {
// 用户id
int64 id = 1;
// 用户名称
string username = 2;
// 用户性别
string gender = 3;
}

service User {
rpc getUser(IdRequest) returns(UserResponse); //对外开发的接口
}
  • 生成rpc目录和代码
1
E:\proj\gowork\study-gozero-demo\mall\user\rpc> goctl rpc protoc user.proto --go_out=. --go-grpc_out=. --zrpc_out=.
  • 配置user/rpc/internal/config/config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package config

import (
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/zrpc"
)

type Config struct {
zrpc.RpcServerConf

// 添加mysql配置
Mysql struct {
DataSource string
}
CacheRedis cache.CacheConf
}

  • 配置/user/rpc/etc/user.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
Name: user.rpc
ListenOn: 0.0.0.0:8081
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
# 加入MySQL连接字符串
Mysql:
DataSource: root:123456@tcp(127.0.0.1:3306)/go_zero?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
CacheRedis:
- Host: 127.0.0.1:6379
Pass: ""
Type: node
  • 添加资源依赖/user/rpc/internal/svc/servicecontext.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package svc
import (
"study-gozero-demo/mall/user/model"
"study-gozero-demo/mall/user/rpc/internal/config"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type ServiceContext struct {
Config config.Config
// 引入model层的增删改查
UserModel model.UserModel
}
func NewServiceContext(c config.Config) *ServiceContext {
// 初始化mysql连接
conn := sqlx.NewMysql(c.Mysql.DataSource)
return &ServiceContext{
Config: c,
// 调用user/model层代码
UserModel: model.NewUserModel(conn, c.CacheRedis),
}

}

如上那些配置其实和api层的配置基本上一样,都是加入mysql,引用mode层的代码

  • 添加rpc逻辑user/rpc/internal/logic/getuserlogic.go
1
2
3
4
5
6
7
8
9
10
11
12
13
func (l *GetUserLogic) GetUser(in *user.IdRequest) (*user.UserResponse, error) {
// todo: add your logic here and delete this line
one, err := l.svcCtx.UserModel.FindOne(l.ctx, in.Id)
if err != nil {
return nil, err
}
return &user.UserResponse{
Id: one.Id,
Username: one.Username.String,
Gender: one.Gender.String,
}, nil
}

order api

配置

  • 关于生成model代码,mysql等配置就省略了,和user api的配置一模一样,注意需要保留之前jwt配置
  • 本地数据库需要加上order表,表中字段为:id,productname,price,unit
  • 编写order/api/oder.api
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
type (
OrderReq {
Id int `json:"id"`
UserId int `json:"userid"`
}

OrderReply {
Id int `json:"id"`
Productname string `json:"productname"`
}

OrderAddReq {
Productname string `json:"productname"`
Price string `json:"price"`
Unit string `json:"unit"`
}

OrderAddRes {
Code int `json:"code"`
Messsage string `json:"message"`
}
)

// 在哪个服务头上加这个验证,就是需要jwt鉴权
@server(
jwt: Auth
)
service order {
@handler orderAdd
post /api/order/OrderAdd (OrderAddReq) returns (OrderAddRes)
}

service order {
@handler getOrder
// get /api/order/get/:id (OrderReq) returns (OrderReply)
get /api/order/get (OrderReq) returns (OrderReply)

}
  • 添加UserRpc配置及yaml配置项,order/api/internal/config/config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package config

import (
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/zrpc"
)

type Config struct {
rest.RestConf
// 添加mysql配置
Mysql struct {
DataSource string
}
// 使用缓存
CacheRedis cache.CacheConf
Auth struct {
AccessSecret string
AccessExpire int64
}
// 引用user rpc
UserRpc zrpc.RpcClientConf
}

  • order/api/etc/order.yaml 加入user rpc配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Name: order
Host: 0.0.0.0
Port: 8889
Mysql:
DataSource: root:123456@tcp(127.0.0.1:3306)/go_zero?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
CacheRedis:
- Host: 127.0.0.1:6379
Pass: ""
Type: node
Auth:
# 生成jwt token的密钥 一般格式为udid
AccessSecret: 0c891c78-9415-ec96-deb8-3b658f9e57f3
# jwt token有效期,单位:秒,现在设置的为1小时
AccessExpire: 3600
# 加入user rpc
UserRpc:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
  • order/api/internal/svc/servicecontext.go 添加依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package svc

import (
"study-gozero-demo/mall/order/api/internal/config"
"study-gozero-demo/mall/order/model"
"study-gozero-demo/mall/user/rpc/userclient"

"github.com/zeromicro/go-zero/core/stores/sqlx"
"github.com/zeromicro/go-zero/zrpc"
)

type ServiceContext struct {
Config config.Config
// 引入model层的增删改查
OrderModel model.OrderModel
// 引用最终生成user服务代码路径
UserRpc userclient.User
}

func NewServiceContext(c config.Config) *ServiceContext {
// 初始化mysql连接
conn := sqlx.NewMysql(c.Mysql.DataSource)
return &ServiceContext{
Config: c,
// 调用order/model层代码
OrderModel: model.NewOrderModel(conn, c.CacheRedis),
// 调用user rpc
UserRpc: userclient.NewUser(zrpc.MustNewClient(c.UserRpc)),
}
}

逻辑编写

  • 编写order\api\internal\logic\orderaddlogic.go 业务代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import (
"context"
"errors"
"fmt"

"study-gozero-demo/mall/order/api/internal/svc"
"study-gozero-demo/mall/order/api/internal/types"
"study-gozero-demo/mall/order/model"

"github.com/jinzhu/copier"
"github.com/zeromicro/go-zero/core/logx"
)
...


func (l *OrderAddLogic) OrderAdd(req *types.OrderAddReq) (resp *types.OrderAddRes, err error) {
// todo: add your logic here and delete this line
var addReq model.Order
errCopy := copier.Copy(&addReq, req)
if errCopy != nil {
return nil, errCopy
}

respd, err2 := l.svcCtx.OrderModel.Insert(l.ctx, &addReq)
fmt.Println("------respd:", respd, "error:", err2)
if err2 != nil {
fmt.Println("insert into error:", err)
return nil, errors.New(err.Error())

}

// 登录成功返回数据
return &types.OrderAddRes{
Code: 1,
Messsage: "success",
}, nil

}
  • 编写order\api\internal\logic\getorderlogic.go 业务代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import (
"context"
"errors"
"fmt"

"study-gozero-demo/mall/order/api/internal/svc"
"study-gozero-demo/mall/order/api/internal/types"
"study-gozero-demo/mall/user/rpc/user"

"github.com/zeromicro/go-zero/core/logx"
)
...

func (l *GetOrderLogic) GetOrder(req *types.OrderReq) (resp *types.OrderReply, err error) {
// todo: add your logic here and delete this line

// 使用user rpc
// 新增代码,注意这里的&user,应用的是user rpc的user/user.pb.go中内容,IdRequest其实就是对应入参(user.proto定义的)

_, err = l.svcCtx.UserRpc.GetUser(l.ctx, &user.IdRequest{
Id: int64(req.UserId),
})
if err != nil {
return nil, errors.New("用户不存在")
}

userInfo, err2 := l.svcCtx.OrderModel.FindOne(l.ctx, int64(req.Id))
fmt.Println("userInfo=", userInfo, "error:", err2)
if userInfo == nil {
fmt.Println("iiiiiiii")
return nil, errors.New("订单不存在")
}
return &types.OrderReply{
Id: req.Id,
ProductName: userInfo.Productname.String,
}, nil

}

启动各个服务

  • 启动user rpc,主要验证用户是否存在
1
2
PS E:\proj\gowork\study-gozero-demo\mall\user\rpc> go run user.go
Starting rpc server at 0.0.0.0:8080...
  • 启动user api,用来登录返回token
1
2
PS E:\proj\gowork\study-gozero-demo\mall\user\api> go run .\user.go
Starting server at 0.0.0.0:8888.
  • 启动order api,对外的接口
1
2
3
PS E:\proj\gowork\study-gozero-demo\mall\order\api> go run .\order.go
Starting server at 0.0.0.0:8889...
{"@timestamp":"2023-07-30T16:03:50.226+08:00","cal

测试

  • 测试GetOrder的接口
    • 如果传递的userid错误,就返回用户不存在错误
    • 如果传递的id错误,就返回的订单d不存在
    • 如果userid和id正确,就返回订单的信息
1
2
3
4
5
6
7
8
9
import requests
import json

data2 = {"userid": 20, "id": 1}
resp = requests.get("http://127.0.0.1:8889/api/order/get", json=data2)
print(resp.text)

{"id":1,"productname":"西瓜"}

  • 测试OrderAdd的接口
    • 如果不带正确token的头部,就返回401
    • 如果传递带正确token的头部,就返回新增成功信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
data1 ={"productname":"西瓜","price": "122", "unit": "个"}
resp = requests.post("http://127.0.0.1:8889/api/order/OrderAdd", json=data1)
print(resp)

<Response [401]>

data ={"username": "admin", "password": "123456"}
resp = requests.post("http://127.0.0.1:8888/user/login", json=data)
data = json.loads(resp.text)
token = data["accessToken"]
header = {"Authorization": token}
data1 ={"productname":"西瓜","price": "122", "unit": "个"}
resp = requests.post("http://127.0.0.1:8889/api/order/OrderAdd", headers=header,json=data1)
print(resp.text)

{"code":1,"message":"success"}

自定义错误

业务错误响应格式

  • 业务处理正常
1
2
3
4
5
6
7
{
"code": 0,
"msg": "successful",
"data": {
....
}
}
  • 业务处理异常
1
2
3
4
{
"code": 10001,
"msg": "参数错误"
}

user api之login

  • 在之前,我们在登录逻辑中处理用户名不存在时,直接返回来一个error。我们来登录并传递一个不存在的用户名看看效果。
1
2
3
4
5
6
7
8
9
curl -X POST \
http://127.0.0.1:8888/user/login \
-H 'content-type: application/json' \
-d '{
"username":"1",
"password":"123456"
}'

用户名不存在

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

自定义错误

  • 首先在common中添加一个baseerror.go文件,并填入代码
1
2
3
mkdir common
cd common
mkdir errorx&&cd errorx
  • 编写自定义代码mall\common\errorx\baseerror.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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自定义错误替换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginReply, err error) {
// todo: add your logic here and delete this line

if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 {
// return nil, errors.New("参数错误")
return nil, errorx.NewDefaultError("参数错误")

}

userInfo, err := l.svcCtx.UserModel.FindOneByUsername(l.ctx, req.Username)
fmt.Println("userInfo=", userInfo, "error:", err)
switch err {
case nil:
case model.ErrNotFound:
// return nil, errors.New("用户名不存在")
return nil, errorx.NewDefaultError("用户名不存在")

default:
return nil, err
}
fmt.Println("pwd:", userInfo.Password.String)
fmt.Println("pwd:", req.Password)

if userInfo.Password.String != req.Password {
// return nil, errors.New("用户密码不正确")
return nil, errorx.NewDefaultError("用户密码不正确")

}

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

// 登录成功返回数据
return &types.LoginReply{
Id: userInfo.Id,
Username: userInfo.Username.String,
AccessToken: jwtToken,
AccessExpire: now + accessExpire,
RefreshAfter: now + accessExpire/2,
}, nil
}
  • 开启自定义错误,配置user/api/user.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
"flag"
"fmt"
"net/http"

"study-gozero-demo/mall/common/errorx"
"study-gozero-demo/mall/user/api/internal/config"
"study-gozero-demo/mall/user/api/internal/handler"
"study-gozero-demo/mall/user/api/internal/svc"

"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/rest/httpx"
)

var configFile = flag.String("f", "etc/user-api.yaml", "the config file")

func main() {
flag.Parse()

var c config.Config
conf.MustLoad(*configFile, &c)

server := rest.MustNewServer(c.RestConf)
defer server.Stop()

ctx := svc.NewServiceContext(c)
handler.RegisterHandlers(server, ctx)
// 自定义错误
httpx.SetErrorHandler(func(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()
}

  • 重新运行user\api\user.go

  • 再次测试

1
2
3
4
5
6
 curl -X POST   http://127.0.0.1:8888/user/login   -H 'content-type: application/json'   -d '{
"username":"1",
"password":"123456"
}'
{"code":1001,"msg":"用户名不存在"}