我们通过一个系列文章跟大家详细展示一个 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
快速开发一个《食谱指南》系统,让你快速上手微服务。
生成 usermanage rpc 服务
- 进入 rpc 服务工作区
$ cd FoodGuides/service/usermanage/rpc
- 创建 proto 文件
$ goctl rpc -o user.proto
- 编辑 proto 文件
syntax = "proto3";
package user;
option go_package="./user";
message LoginRequest {
string email = 1;
string password = 2;
}
message RegisterRequest {
string Username = 1;
string Email = 2;
string Password = 3;
}
message UserinfoRequest {
string Userid = 1;
string Token = 2;
}
message Response {
int64 id = 1;
string email = 2;
string username = 3;
string accessToken = 4;
int64 accessExpire = 5;
int64 refreshAfter = 6;
}
service User {
rpc Login(LoginRequest) returns(Response);
rpc Register(RegisterRequest) returns(Response);
rpc Userinfo(UserinfoRequest) returns(Response);
}
我们定义了三个接口:Login
, Register
和 UserInfo
。
- 运行模板生成命令生成
user-rpc
服务
$ goctl rpc protoc user.proto --go_out=. --go-grpc_out=. --zrpc_out=.
Done.
- 添加下载依赖包
$ go mod tidy
编写 user rpc 服务
修改配置文件
$ vim rpc/etc/user.yaml
Name: user.rpc
ListenOn: 0.0.0.0:9999
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
Mysql:
DataSource: root:123456@tcp(127.0.0.1:9528)/foodguides?charset=utf8mb4&parseTime=True&loc=Local
AccessSecret: ad879037-d3fd-tghj-112d-6bfc35d54b7d
AccessExpire: 86400
Salt: HWVOFkGgPTryzICwd7qnJaZR9KQ2i8xe
添加配置的实例化
$ vim rpc/internal/config/config.go
package config
import "github.com/zeromicro/go-zero/zrpc"
type Config struct {
zrpc.RpcServerConf
Mysql struct {
DataSource string
}
AccessSecret string
AccessExpire int64
Salt string
}
注册服务上下文 user model
的依赖
$ vim rpc/internal/svc/servicecontext.go
package svc
import (
"FoodGuides/service/usermanage/model"
"FoodGuides/service/usermanage/rpc/internal/config"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type ServiceContext struct {
Config config.Config
UserModel model.UserModel
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
UserModel: model.NewUserModel(sqlx.NewMysql(c.Mysql.DataSource)),
}
}
添加用户登录逻辑 Login
将 api/internal/logic/loginlogic.go
文件中的 Login
方法的实现逻辑复制到 rpc
服务下的同名文件同名方法中,再稍作修改。
$ vim rpc/internal/logic/loginlogic.go
package logic
import (
"FoodGuides/common/cryptx"
"FoodGuides/common/jwtx"
"FoodGuides/service/usermanage/model"
"context"
"errors"
"time"
"FoodGuides/service/usermanage/rpc/internal/svc"
"FoodGuides/service/usermanage/rpc/user"
"github.com/zeromicro/go-zero/core/logx"
)
type LoginLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
return &LoginLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
func (l *LoginLogic) Login(in *user.LoginRequest) (*user.Response, error) {
// 查询用户是否存在
res, err := l.svcCtx.UserModel.FindOneByEmail(l.ctx, in.Email)
if err != nil {
if err == model.ErrNotFound {
return nil, errors.New("用户不存在")
}
return nil, err
}
// 判断密码是否正确
password := cryptx.PasswordEncrypt(l.svcCtx.Config.Salt, in.Password)
if password != res.Password {
return nil, errors.New("密码错误")
}
now := time.Now().Unix()
accessExpire := l.svcCtx.Config.AccessExpire
jwtToken, err := jwtx.GetJwtToken(l.svcCtx.Config.AccessSecret, now, accessExpire, res.Id)
if err != nil {
return nil, err
}
return &user.Response{
Id: res.Id,
Email: res.Email,
Username: res.Name,
AccessToken: jwtToken,
AccessExpire: now + accessExpire,
RefreshAfter: now + accessExpire/2,
}, nil
}
添加用户注册逻辑 Register
同样的,将 api/internal/logic/registerlogic.go
文件中的 Register
方法的实现逻辑复制到 rpc
服务下的同名文件同名方法中,再稍作修改。
$ vim rpc/internal/logic/registerlogic.go
package logic
import (
"FoodGuides/common/cryptx"
"FoodGuides/common/jwtx"
"FoodGuides/service/usermanage/model"
"context"
"errors"
"time"
"FoodGuides/service/usermanage/rpc/internal/svc"
"FoodGuides/service/usermanage/rpc/user"
"github.com/zeromicro/go-zero/core/logx"
)
type RegisterLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic {
return &RegisterLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
func (l *RegisterLogic) Register(in *user.RegisterRequest) (*user.Response, error) {
// 判断邮箱是否已经被注册
_, err := l.svcCtx.UserModel.FindOneByEmail(l.ctx, in.Email)
if err == nil {
return nil, errors.New("该邮箱已注册")
}
if err != model.ErrNotFound {
return nil, err
}
newUser := model.User{
Name: in.Username,
Password: cryptx.PasswordEncrypt(l.svcCtx.Config.Salt, in.Password),
Email: in.Email,
}
// 插入一条新的用户数据
res, err := l.svcCtx.UserModel.Insert(l.ctx, &newUser)
if err != nil {
return nil, err
}
newUser.Id, err = res.LastInsertId()
if err != nil {
return nil, err
}
now := time.Now().Unix()
accessExpire := l.svcCtx.Config.AccessExpire
var jwtToken string
jwtToken, err = jwtx.GetJwtToken(
l.svcCtx.Config.AccessSecret,
now,
accessExpire,
newUser.Id,
)
if err != nil {
return nil, err
}
return &user.Response{
Id: newUser.Id,
Email: newUser.Email,
Username: newUser.Name,
AccessToken: jwtToken,
AccessExpire: now + accessExpire,
RefreshAfter: now + accessExpire/2,
}, nil
}
添加用户信息逻辑 UserInfo
同样的,将 api/internal/logic/userinfologic.go
文件中的 UserInfo
方法的实现逻辑复制到 rpc
服务下的同名文件同名方法中,再稍作修改。
$ vim rpc/internal/logic/userinfologic.go
package logic
import (
"FoodGuides/service/usermanage/model"
"context"
"errors"
"strconv"
"FoodGuides/service/usermanage/rpc/internal/svc"
"FoodGuides/service/usermanage/rpc/user"
"github.com/zeromicro/go-zero/core/logx"
)
type UserinfoLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewUserinfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserinfoLogic {
return &UserinfoLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
func (l *UserinfoLogic) Userinfo(in *user.UserinfoRequest) (*user.Response, error) {
uid, _ := strconv.ParseInt(in.Userid, 10, 64)
// 查询用户是否存在
res, err := l.svcCtx.UserModel.FindOne(l.ctx, uid)
if err != nil {
if err == model.ErrNotFound {
return nil, errors.New("用户不存在")
}
return nil, err
}
return &user.Response{
Id: res.Id,
Email: res.Email,
Username: res.Name,
}, nil
}
至此,有关用户管理 rpc
服务的接口逻辑代码都已完成。接下来我们要做的是修改 api
服务中的逻辑,即改为 API Gateway
代码调用 rpc
服务,而不是直接将逻辑代码全放在 api
服务中。
优化 user api 服务
修改 user-api.yaml
配置文件
- 删除
Mysql
和Salt
两个配置项,因为我们在rpc
配置文件中重新配置了,这里就不再需要了 - 添加
user rpc
依赖配置项(来源于 rpc/ect/user.yaml)
$ vim api/etc/user-api.yaml
Name: user-api
Host: 0.0.0.0
Port: 8888
Auth:
AccessSecret: ad879037-d3fd-tghj-112d-6bfc35d54b7d
AccessExpire: 86400
UserRpc:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
修改 config.go
文件
同步 user-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
}
UserRpc zrpc.RpcClientConf
}
注册服务上下文 user rpc
的依赖
- 删除
UserModel
的声明,并添加UserRpc
的声明 - 注册上下文
user rpc
的依赖
$ vim api/internal/svc/servicecontext.go
package svc
import (
"FoodGuides/service/usermanage/api/internal/config"
"FoodGuides/service/usermanage/rpc/userclient"
"github.com/zeromicro/go-zero/zrpc"
)
type ServiceContext struct {
Config config.Config
UserRpc userclient.User
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
UserRpc: userclient.NewUser(zrpc.MustNewClient(c.UserRpc)),
}
}
优化用户登录逻辑
改为调用 user rpc
服务进行登录:
$ vim api/internal/logic/loginlogic.go
package logic
import (
"FoodGuides/service/usermanage/api/internal/svc"
"FoodGuides/service/usermanage/api/internal/types"
"FoodGuides/service/usermanage/rpc/userclient"
"context"
"github.com/zeromicro/go-zero/core/logx"
)
type LoginLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
return &LoginLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *LoginLogic) Login(req *types.LoginRequest) (*types.LoginResponse, error) {
res, err := l.svcCtx.UserRpc.Login(l.ctx, &userclient.LoginRequest{
Email: req.Email,
Password: req.Password,
})
if err != nil {
return nil, err
}
token := types.JwtToken{
AccessToken: res.AccessToken,
AccessExpire: res.AccessExpire,
RefreshAfter: res.RefreshAfter,
}
response := types.UserReply{
Id: res.Id,
Username: res.Username,
Email: res.Email,
JwtToken: token,
}
return &types.LoginResponse{UserReply: response}, nil
}
优化用户注册逻辑
$ vim api/internal/logic/registerlogic.go
package logic
import (
"FoodGuides/service/usermanage/api/internal/svc"
"FoodGuides/service/usermanage/api/internal/types"
"FoodGuides/service/usermanage/rpc/userclient"
"context"
"github.com/zeromicro/go-zero/core/logx"
)
type RegisterLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic {
return &RegisterLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *RegisterLogic) Register(req *types.RegisterRequest) (*types.RegisterResponse, error) {
res, err := l.svcCtx.UserRpc.Register(l.ctx, &userclient.RegisterRequest{
Username: req.Username,
Email: req.Email,
Password: req.Password,
})
if err != nil {
return nil, err
}
token := types.JwtToken{
AccessToken: res.AccessToken,
AccessExpire: res.AccessExpire,
RefreshAfter: res.RefreshAfter,
}
response := types.UserReply{
Id: res.Id,
Username: res.Username,
Email: res.Email,
JwtToken: token,
}
return &types.RegisterResponse{UserReply: response}, nil
}
优化用户信息逻辑
$ vim api/internal/logic/userinfologic.go
package logic
import (
"FoodGuides/service/usermanage/api/internal/svc"
"FoodGuides/service/usermanage/api/internal/types"
"FoodGuides/service/usermanage/rpc/userclient"
"context"
"encoding/json"
"strconv"
"github.com/zeromicro/go-zero/core/logx"
)
type UserInfoLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserInfoLogic {
return &UserInfoLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *UserInfoLogic) UserInfo() (*types.UserInfoResponse, error) {
// 通过 l.ctx.Value("uid") 可获取 jwt 载体中 `uid` 信息
uid, _ := l.ctx.Value("uid").(json.Number).Int64()
user, err := l.svcCtx.UserRpc.Userinfo(l.ctx, &userclient.UserinfoRequest{
Userid: strconv.FormatInt(uid, 10),
})
if err != nil {
return nil, err
}
response := types.UserReply{
Id: user.Id,
Username: user.Username,
Email: user.Email,
}
return &types.UserInfoResponse{UserReply: response}, nil
}
启动 user rpc
服务
$ cd FoodGuides/service/usermanage/rpc
$ go run user.go -f etc/user.yaml
Starting rpc server at 0.0.0.0:9999...
运行上述命令后,如果报如下错误,请本机下载并安装 etcd
服务,点击 etcd.exe
运行 etcd
服务后重新执行上述启动 user rpc
服务的命令。
{"level":"warn","ts":"2023-10-20T15:44:45.125405+0800","logger":"etcd-client","caller":"v3@v3.5.9/retry_interceptor.go:62","msg":"retrying of unary invoker failed","target":"etcd-endpoints://0xc0004668c0/127.0.0.1:2379","attempt":0,"error":"rpc error: code = DeadlineExceeded desc = latest balancer error: last connection error: connection error: desc = \"transport: Error while dialing: dial tcp 127.0.0.1:2379: connectex: No connection could be made because the target machine actively refused it.\""}
{"@timestamp":"2023-10-20T15:44:45.125+08:00","caller":"zrpc/server.go:88","content":"context deadline exceeded","level":"error"}
panic: context deadline exceeded
启动 user api
服务
$ cd FoodGuides/service/usermanage/api
$ go run user.go -f etc/user-api.yaml
Starting server at 0.0.0.0:8888...
测试服务
我们用 Postman
尝试请求 Login
,Register
和 Userinfo
这三个接口,测试服务是否正常。测试方法在对应的文章末尾都有提及,仅供参考: