go-zero 实战进阶 - 食材管理 rpc 服务

283 阅读4分钟

我们通过一个系列文章跟大家详细展示一个 go-zero 微服务实例,整个系列分十三篇文章,目录结构如下:

  1. go-zero 实战 - 服务划分与项目创建
  2. go-zero 实战 - User API Gateway
  3. go-zero 实战 - User Login
  4. go-zero 实战 - User Register
  5. go-zero 实战 - User Userinfo
  6. go-zero 实战 - Food API Gateway
  7. go-zero 实战 - Food Search
  8. go-zero 实战 - Food AddFood
  9. go-zero 实战 - Food DeleteFood
  10. go-zero 实战 - Food Foodlist
  11. go-zero 实战进阶 - rpc 服务
  12. go-zero 实战进阶 - 用户管理 rpc 服务
  13. go-zero 实战进阶 - 食材管理 rpc 服务

期望通过本系列文章带你在本地利用 go-zero 快速开发一个《食谱指南》系统,让你快速上手微服务。

生成 foodmanage rpc 服务

  • 进入 rpc 服务工作区
$ cd FoodGuides/service/foodmanage/rpc
  • 创建 proto 文件
$ goctl rpc -o food.proto
  • 编辑 proto 文件
syntax = "proto3";

package food;
option go_package="./food";

message SearchRequest {
  string searchKey = 1;
}

message AddFoodRequest {
  string userid = 1;
  string foodId = 2;
}

message DeleteFoodRequest {
  string userid = 1;
  string foodId = 2;
}

message FoodListRequest {
  string userid = 1;
}

message FoodInfoResponse {
  string protein = 1;
  string fat = 2;
  string carbohydrate = 3;
  string calorie = 4;
  string minerals = 5;
  string calcium = 6;
  string phosphorus = 7;
  string iron = 8;
  string purine = 9;
  string id = 10;
  string name = 11;
}

message StatusResponse {
  int32 success = 1;
}

message FoodListResponse {
  repeated FoodInfoResponse data = 1;
}

service Food {
  rpc Search(SearchRequest) returns(FoodInfoResponse);
  rpc AddFood(AddFoodRequest) returns(StatusResponse);
  rpc DeleteFood(DeleteFoodRequest) returns(StatusResponse);
  rpc FoodList(FoodListRequest) returns(FoodListResponse);
}

我们定义了四个接口:SearchAddFoodDeleteFoodFoodList

  • 运行模板生成命令生成 food-rpc 服务
$ goctl rpc protoc food.proto --go_out=. --go-grpc_out=. --zrpc_out=.
Done.
  • 添加下载依赖包
$ go mod tidy

编写 food rpc 服务

修改配置文件

$ vim rpc/etc/food.yaml

Name: food.rpc
ListenOn: 0.0.0.0:9998

Etcd:
  Hosts:
  - 127.0.0.1:2379
  Key: food.rpc

Mysql:
  DataSource: root:123456@tcp(127.0.0.1:9528)/foodguides?charset=utf8mb4&parseTime=True&loc=Local

添加配置的实例化

$ vim rpc/internal/config/config.go 

package config

import "github.com/zeromicro/go-zero/zrpc"

type Config struct {
    zrpc.RpcServerConf

    Mysql struct {
       DataSource string
    }
}

注册服务上下文 food model 的依赖

$ vim rpc/internal/svc/servicecontext.go

package svc

import (
    "FoodGuides/service/foodmanage/model"
    "FoodGuides/service/foodmanage/rpc/internal/config"
    "github.com/zeromicro/go-zero/core/stores/sqlx"
)

type ServiceContext struct {
    Config        config.Config
    FoodModel     model.FoodModel
    UserFoodModel model.UserFoodModel
}

func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
       Config:        c,
       FoodModel:     model.NewFoodModel(sqlx.NewMysql(c.Mysql.DataSource)),
       UserFoodModel: model.NewUserFoodModel(sqlx.NewMysql(c.Mysql.DataSource)),
    }
}

添加食材搜索逻辑 Search

api/internal/logic/searchlogic.go 文件中的 Search 方法的实现逻辑复制到 rpc 服务下的同名文件同名方法中,再稍作修改。

$ vim rpc/internal/logic/searchlogic.go

package logic

import (
    "context"
    "strconv"

    "FoodGuides/service/foodmanage/rpc/food"
    "FoodGuides/service/foodmanage/rpc/internal/svc"

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

type SearchLogic struct {
    ctx    context.Context
    svcCtx *svc.ServiceContext
    logx.Logger
}

func NewSearchLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SearchLogic {
    return &SearchLogic{
       ctx:    ctx,
       svcCtx: svcCtx,
       Logger: logx.WithContext(ctx),
    }
}

func (l *SearchLogic) Search(in *food.SearchRequest) (*food.FoodInfoResponse, error) {
    res, err := l.svcCtx.FoodModel.FindOneByName(l.ctx, in.SearchKey)
    if err != nil {
       return nil, err
    }

    return &food.FoodInfoResponse{
       Id:           strconv.FormatInt(res.Id, 10),
       Name:         res.Name,
       Protein:      res.Protein,
       Fat:          res.Fat,
       Carbohydrate: res.Carbohydrate,
       Calorie:      res.Calorie,
       Minerals:     res.Minerals,
       Calcium:      res.Calcium,
       Phosphorus:   res.Phosphorus,
       Iron:         res.Iron,
       Purine:       res.Purine,
    }, nil
}

添加新增食材逻辑 AddFood

同样的,将 api/internal/logic/addfoodlogic.go 文件中的 AddFood 方法的实现逻辑复制到 rpc 服务下的同名文件同名方法中,再稍作修改。

$ vim rpc/internal/logic/addfoodlogic.go

package logic

import (
    "FoodGuides/service/foodmanage/model"
    "context"
    "database/sql"
    "errors"
    "fmt"
    "strconv"

    "FoodGuides/service/foodmanage/rpc/food"
    "FoodGuides/service/foodmanage/rpc/internal/svc"

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

type AddFoodLogic struct {
    ctx    context.Context
    svcCtx *svc.ServiceContext
    logx.Logger
}

func NewAddFoodLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AddFoodLogic {
    return &AddFoodLogic{
       ctx:    ctx,
       svcCtx: svcCtx,
       Logger: logx.WithContext(ctx),
    }
}

func (l *AddFoodLogic) AddFood(in *food.AddFoodRequest) (*food.StatusResponse, error) {
    uid, _ := strconv.ParseInt(in.Userid, 10, 64)
    foodId, _ := strconv.ParseInt(in.FoodId, 10, 64)

    _, err := l.svcCtx.FoodModel.FindOne(l.ctx, foodId)

    if err != nil {
       if err == model.ErrNotFound {
          return nil, errors.New(fmt.Sprintf("不存在 ID 为 %d 的食材", foodId))
       }
       return nil, err
    }

    data := model.UserFood{
       Userid: sql.NullInt64{
          Int64: uid,
          Valid: true,
       },
       Foodid: sql.NullInt64{
          Int64: foodId,
          Valid: true,
       },
    }

    _, err = l.svcCtx.UserFoodModel.Insert(l.ctx, &data)
    if err != nil {
       return nil, err
    }

    return &food.StatusResponse{
       Success: 1,
    }, nil
}

添加删除食材逻辑 DeleteFood

同样的,将 api/internal/logic/deletefoodlogic.go 文件中的 DeleteFood 方法的实现逻辑复制到 rpc 服务下的同名文件同名方法中,再稍作修改。

$ vim rpc/internal/logic/deletefoodlogic.go

package logic

import (
    "context"
    "database/sql"
    "errors"
    "fmt"
    "strconv"

    "FoodGuides/service/foodmanage/rpc/food"
    "FoodGuides/service/foodmanage/rpc/internal/svc"

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

type DeleteFoodLogic struct {
    ctx    context.Context
    svcCtx *svc.ServiceContext
    logx.Logger
}

func NewDeleteFoodLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteFoodLogic {
    return &DeleteFoodLogic{
       ctx:    ctx,
       svcCtx: svcCtx,
       Logger: logx.WithContext(ctx),
    }
}

func (l *DeleteFoodLogic) DeleteFood(in *food.DeleteFoodRequest) (*food.StatusResponse, error) {
    uid, _ := strconv.ParseInt(in.Userid, 10, 64)
    foodId, _ := strconv.ParseInt(in.FoodId, 10, 64)
    userFood, _ := l.svcCtx.UserFoodModel.FindOneByUserid(l.ctx, sql.NullInt64{
       Int64: uid,
       Valid: true,
    })

    if userFood == nil {
       return nil, errors.New(fmt.Sprintf("该用户名下没有关联的食物,用户 ID:%d", uid))
    }

    if userFood.Foodid.Int64 != foodId {
       return nil, errors.New(fmt.Sprintf("该用户名下没有此关联的食物,用户 ID:%d, 食物 ID: %d", uid, foodId))
    }

    err := l.svcCtx.UserFoodModel.Delete(l.ctx, userFood.Id)

    if err != nil {
       return nil, err
    }

    return &food.StatusResponse{
       Success: 1,
    }, nil
}

添加我的食谱逻辑 FoodList

同样的,将 api/internal/logic/foodlistlogic.go 文件中的 FoodList 方法的实现逻辑复制到 rpc 服务下的同名文件同名方法中,再稍作修改。

$ vim rpc/internal/logic/foodlistlogic.go

package logic

import (
    "context"
    "strconv"

    "FoodGuides/service/foodmanage/rpc/food"
    "FoodGuides/service/foodmanage/rpc/internal/svc"

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

type FoodListLogic struct {
    ctx    context.Context
    svcCtx *svc.ServiceContext
    logx.Logger
}

func NewFoodListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FoodListLogic {
    return &FoodListLogic{
       ctx:    ctx,
       svcCtx: svcCtx,
       Logger: logx.WithContext(ctx),
    }
}

func (l *FoodListLogic) FoodList(in *food.FoodListRequest) (*food.FoodListResponse, error) {
    uid, _ := strconv.ParseInt(in.Userid, 10, 64)
    userFoods, err := l.svcCtx.UserFoodModel.FindManyByUserid(l.ctx, uid)
    if err != nil {
       return nil, err
    }

    var foodIds []string
    for _, f := range userFoods {
       foodIds = append(foodIds, strconv.FormatInt(f.Foodid.Int64, 10))
    }

    foods, err := l.svcCtx.FoodModel.FindMany(l.ctx, foodIds)
    if err != nil {
       return nil, err
    }

    var foodList []*food.FoodInfoResponse
    for _, f := range foods {
       item := food.FoodInfoResponse{
          Id:           strconv.FormatInt(f.Id, 10),
          Name:         f.Name,
          Protein:      f.Protein,
          Fat:          f.Fat,
          Carbohydrate: f.Carbohydrate,
          Calorie:      f.Calorie,
          Minerals:     f.Minerals,
          Calcium:      f.Calcium,
          Phosphorus:   f.Phosphorus,
          Iron:         f.Iron,
          Purine:       f.Purine,
       }
       foodList = append(foodList, &item)
    }

    return &food.FoodListResponse{
       Data: foodList,
    }, nil
}

至此,有关食材管理 rpc 服务的接口逻辑代码都已完成。接下来我们要做的是修改 api 服务中的逻辑,即改为 API Gateway 代码调用 rpc 服务。

优化 food api 服务

修改 food-api.yaml 配置文件

$ vim api/etc/food-api.yaml

Name: food-api
Host: 0.0.0.0
Port: 8889

Auth:
  AccessSecret: ad879037-d3fd-tghj-112d-6bfc35d54b7d
  AccessExpire: 86400
  
FoodRpc:
  Etcd:
    Hosts:
      - 127.0.0.1:2379
    Key: food.rpc

修改 config.go 文件

同步 food-api.yaml 配置文件的修改内容

$ vim api/internal/config/config.go

package config

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

type Config struct {
    rest.RestConf

    Auth struct {
       AccessSecret string
       AccessExpire int64
    }

    FoodRpc zrpc.RpcClientConf
}

注册服务上下文 food rpc 的依赖

$ vim api/internal/svc/servicecontext.go

package svc

import (
    "FoodGuides/service/foodmanage/api/internal/config"
    "FoodGuides/service/foodmanage/rpc/foodclient"
    "github.com/zeromicro/go-zero/zrpc"
)

type ServiceContext struct {
    Config  config.Config
    FoodRpc foodclient.Food
}

func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
       Config:        c,
       FoodRpc: foodclient.NewFood(zrpc.MustNewClient(c.FoodRpc)),
    }
}

优化食材搜索逻辑

改为调用 food rpc 服务进行搜索:

$ vim api/internal/logic/searchlogic.go

package logic

import (
    "FoodGuides/service/foodmanage/api/internal/svc"
    "FoodGuides/service/foodmanage/api/internal/types"
    "FoodGuides/service/foodmanage/rpc/foodclient"
    "context"

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

type SearchLogic struct {
    logx.Logger
    ctx    context.Context
    svcCtx *svc.ServiceContext
}

func NewSearchLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SearchLogic {
    return &SearchLogic{
       Logger: logx.WithContext(ctx),
       ctx:    ctx,
       svcCtx: svcCtx,
    }
}

func (l *SearchLogic) Search(req *types.SearchRequest) (*types.SearchResponse, error) {
    food, err := l.svcCtx.FoodRpc.Search(l.ctx, &foodclient.SearchRequest{
       SearchKey: req.Key,
    })
    if err != nil {
       return nil, err
    }

    foodReply := types.FoodReply{
       Id:           food.Id,
       Name:         food.Name,
       Protein:      food.Protein,
       Fat:          food.Fat,
       Carbohydrate: food.Carbohydrate,
       Calorie:      food.Calorie,
       Minerals:     food.Minerals,
       Calcium:      food.Calcium,
       Phosphorus:   food.Phosphorus,
       Iron:         food.Iron,
       Purine:       food.Purine,
    }
    
    return &types.SearchResponse{FoodReply: foodReply}, nil
}

优化新增食材逻辑

$ vim api/internal/logic/addfoodlogic.go

package logic

import (
    "FoodGuides/service/foodmanage/rpc/foodclient"
    "context"
    "encoding/json"
    "strconv"

    "FoodGuides/service/foodmanage/api/internal/svc"
    "FoodGuides/service/foodmanage/api/internal/types"

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

type AddFoodLogic struct {
    logx.Logger
    ctx    context.Context
    svcCtx *svc.ServiceContext
}

func NewAddFoodLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AddFoodLogic {
    return &AddFoodLogic{
       Logger: logx.WithContext(ctx),
       ctx:    ctx,
       svcCtx: svcCtx,
    }
}

func (l *AddFoodLogic) AddFood(req *types.AddFoodRequest) (*types.AddFoodResponse, error) {
    // 获取 jwt 载体中 `uid` 信息,
    uid, _ := l.ctx.Value("uid").(json.Number).Int64()

    _, err := l.svcCtx.FoodRpc.AddFood(l.ctx, &foodclient.AddFoodRequest{
       Userid: strconv.FormatInt(uid, 10),
       FoodId: req.FoodId,
    })
    if err != nil {
       return nil, err
    }

    return &types.AddFoodResponse{}, nil
}

优化删除食材逻辑

$ vim api/internal/logic/deletefoodlogic.go

package logic

import (
    "FoodGuides/service/foodmanage/api/internal/svc"
    "FoodGuides/service/foodmanage/api/internal/types"
    "FoodGuides/service/foodmanage/rpc/foodclient"
    "context"
    "encoding/json"
    "strconv"

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

type DeleteFoodLogic struct {
    logx.Logger
    ctx    context.Context
    svcCtx *svc.ServiceContext
}

func NewDeleteFoodLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteFoodLogic {
    return &DeleteFoodLogic{
       Logger: logx.WithContext(ctx),
       ctx:    ctx,
       svcCtx: svcCtx,
    }
}

func (l *DeleteFoodLogic) DeleteFood(req *types.DeleteFoodRequest) (*types.DeleteFoodResponse, error) {
    // 获取 jwt 载体中 `uid` 信息,
    uid, _ := l.ctx.Value("uid").(json.Number).Int64()
    
    _, err := l.svcCtx.FoodRpc.DeleteFood(l.ctx, &foodclient.DeleteFoodRequest{
       Userid: strconv.FormatInt(uid, 10),
       FoodId: req.FoodId,
    })
    if err != nil {
       return nil, err
    }

    return &types.DeleteFoodResponse{}, nil
}

优化我的食谱逻辑

$ vim api/internal/logic/foodlistlogic.go

package logic

import (
    "FoodGuides/service/foodmanage/rpc/food"
    "context"
    "encoding/json"
    "strconv"

    "FoodGuides/service/foodmanage/api/internal/svc"
    "FoodGuides/service/foodmanage/api/internal/types"

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

type FoodListLogic struct {
    logx.Logger
    ctx    context.Context
    svcCtx *svc.ServiceContext
}

func NewFoodListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FoodListLogic {
    return &FoodListLogic{
       Logger: logx.WithContext(ctx),
       ctx:    ctx,
       svcCtx: svcCtx,
    }
}

func (l *FoodListLogic) FoodList() (*types.FoodListResponse, error) {
    // 获取 jwt 载体中 `uid` 信息,
    uid, _ := l.ctx.Value("uid").(json.Number).Int64()
    res, err := l.svcCtx.FoodRpc.FoodList(l.ctx, &food.FoodListRequest{
       Userid: strconv.FormatInt(uid, 10),
    })
    if err != nil {
       return nil, err
    }

    var foodReplays []types.FoodReply
    for _, f := range res.Data {
       foodReply := types.FoodReply{
          Id:           f.Id,
          Name:         f.Name,
          Protein:      f.Protein,
          Fat:          f.Fat,
          Carbohydrate: f.Carbohydrate,
          Calorie:      f.Calorie,
          Minerals:     f.Minerals,
          Calcium:      f.Calcium,
          Phosphorus:   f.Phosphorus,
          Iron:         f.Iron,
          Purine:       f.Purine,
       }
       foodReplays = append(foodReplays, foodReply)
    }

    return &types.FoodListResponse{List: foodReplays}, nil
}

启动 food rpc 服务

$ cd FoodGuides/service/foodmanage/rpc
$ go run food.go -f etc/food.yaml
Starting rpc server at 0.0.0.0:9998...

启动 food api 服务

$ cd FoodGuides/service/foodmanage/api
$ go run food.go -f etc/food-api.yaml
Starting server at 0.0.0.0:8889...

测试接口

我们用 Postman 尝试分别请求上述四个接口,测试服务是否正常。测试方法在对应的文章末尾都有提及,仅供参考:

至此,rpc 服务就全部实现完毕,小伙伴们可以对比 api 服务api + rpc 服务 两者在实现上的差异性及优势。

后记

本人也是最近才学习 Go 语言,从而了解到 go-zero 框架,如文中描述与官方不符的,请以官方文档 为准。该教程是借鉴 go-zero项目实战 教程编写而成,在此对作者 @Ningxi 表示感谢。

最后,附上项目 Github 地址: FoodGuides-Go ,仅供参考。

上一篇《go-zero 实战进阶 - 用户管理 rpc 服务》