我们通过一个系列文章跟大家详细展示一个 go-zero
微服务实例,整个系列分十三篇文章,目录结构如下:
- go-zero 实战 - 服务划分与项目创建
- go-zero 实战 - User API Gateway
- go-zero 实战 - User Login
- go-zero 实战 - User Register
- go-zero 实战 - User Userinfo
- go-zero 实战 - Food API Gateway
- go-zero 实战 - Food Search
- go-zero 实战 - Food AddFood
- go-zero 实战 - Food DeleteFood
- go-zero 实战 - Food Foodlist
- go-zero 实战进阶 - rpc 服务
- go-zero 实战进阶 - 用户管理 rpc 服务
- 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);
}
我们定义了四个接口:Search
,AddFood
,DeleteFood
和 FoodList
。
- 运行模板生成命令生成
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
尝试分别请求上述四个接口,测试服务是否正常。测试方法在对应的文章末尾都有提及,仅供参考:
- go-zero 实战 - Food Search
- go-zero 实战 - Food AddFood
- go-zero 实战 - Food DeleteFood
- go-zero 实战 - Food Foodlist
至此,rpc
服务就全部实现完毕,小伙伴们可以对比 api 服务
和 api + rpc 服务
两者在实现上的差异性及优势。
后记
本人也是最近才学习 Go
语言,从而了解到 go-zero
框架,如文中描述与官方不符的,请以官方文档 为准。该教程是借鉴 go-zero项目实战 教程编写而成,在此对作者 @Ningxi 表示感谢。
最后,附上项目 Github 地址: FoodGuides-Go ,仅供参考。