本篇文章以用户注册接口为例,快速上手Hertz+Kitex
以用户注册接口来演示hertz结合kitex实现网关+微服务架构的最简易版本
项目结构
api- gateway:网关实现,这里采用hertz框架
idl:接口定义用来生成kitex代码
kitex_gen:thrift协议自动生成的代码
rpc_service:微服务的服务逻辑
hertz框架
hertz框架其实和gin作用差不多,为字节跳动开源的go语言web网络框架
框架细节部分 大家感兴趣可以参考官方文档路由 | CloudWeGo
快速引入
服务逻辑
用户请求注册接口->网关层服务拿到进行处理路由,分发到具体的微服务
核心处理路由函数handler.go
package user
import (
"context"
"log"
"time"
"video_douyin/kitex_gen/user" // Kitex生成的用户服务数据结构
"video_douyin/kitex_gen/user/userservice" // Kitex生成的用户服务客户端
"github.com/cloudwego/hertz/pkg/app" // Hertz HTTP上下文处理
"github.com/cloudwego/kitex/client" // Kitex客户端核心
"github.com/cloudwego/kitex/client/callopt" // Kitex调用选项
)
// 全局用户服务客户端实例
var userClient userservice.Client
// 初始化Kitex客户端连接
func init() {
c, err := userservice.NewClient(
"douyin.video.user", // 服务名
client.WithHostPorts("0.0.0.0:8888"), // 服务地址
)
if err != nil {
panic(err) // 连接失败终止程序
}
userClient = c
}
// Handler 处理用户注册HTTP请求
func Handler(ctx context.Context, c *app.RequestContext) {
req := user.NewRegisterRequest() // 创建注册请求结构体
log.Printf("Phone: %s", req.Phone) // 记录请求参数
// 调用用户服务RPC接口,超时3秒
resp, err := userClient.Register(
context.Background(),
req,
callopt.WithRPCTimeout(3*time.Second),
)
if err != nil {
log.Fatal(err) // RPC调用失败记录日志
}
c.String(200, resp.String()) // 返回RPC响应内容
}
这里服务依赖的kitex目前先不看,只用知道这里做了如下动作
通过hertz服务拿到了请求参数
往微服务进行传递参数
main函数
package main
import (
"log"
"api_gateway/user"
"github.com/cloudwego/hertz/pkg/app/server"
)
func main() {
// 创建路由分组(符合 RESTful 风格)
// 第一级分组 /douyin 作为 API
douyin := hz.Group("/douyin")
// 第二级分组 /user 用于用户相关操作
userGroup := douyin.Group("/user")
// 注册用户注册接口,POST 方法对应创建操作
// 处理函数指向 user 包的 Handler 方法
userGroup.POST("/register/", user.Handler)
// 启动 HTTP 服务,若失败则记录错误日志
// Run() 会阻塞直到服务终止:ml-citation{ref="6" data="citationList"}
if err := hz.Run(); err != nil {
log.Fatal(err)
}
}
可以看到这里和gin的操作差不多 gin的笔记:gin框架学习笔记_gin学习-CSDN博客
这里的主要逻辑:路由组注册
导入依赖
go mod tidy
kitex框架
kitex框架可以类比于grpc框架,grpc框架笔记:gRPC学习笔记记录以及整合gin开发-CSDN博客
也可以类比成dubbo和Java的SpringCloud
快速引入
本篇文章采用的协议为thrift
kitex工具下载
这个工具主要用来根据idl定义来自动生成代码,提高开发效率
go install github.com/cloudwego/kitex/tool/cmd/kitex@latest
idl定义
namespace go user
// 用户注册请求
struct RegisterRequest {
1: required string phone; // 手机号
2: required string verifyCode; // 验证码
}
// 用户注册响应
struct RegisterResponse {
1: required bool success; // 是否成功
2: optional string message; // 错误信息
3: optional i64 userId; // 用户ID
4: optional string username; // 自动生成的用户名
}
// 用户登录请求
struct LoginRequest {
1: required string phone; // 手机号
2: required string verifyCode; // 验证码
}
// 用户登录响应
struct LoginResponse {
1: required bool success; // 是否成功
2: optional string message; // 错误信息
3: optional i64 userId; // 用户ID
4: optional string token; // 登录令牌
5: optional UserInfo userInfo; // 用户信息
}
// 用户基本信息
struct UserInfo {
1: required i64 userId; // 用户ID
2: required string username; // 用户名
3: optional string avatar; // 头像URL
4: optional string signature; // 个性签名
5: optional i32 followCount; // 关注数
6: optional i32 followerCount; // 粉丝数
7: optional bool isFollow; // 当前用户是否关注了该用户
}
// 获取用户信息请求
struct GetUserInfoRequest {
1: required i64 userId; // 要查询的用户ID
2: optional i64 currentUserId; // 当前登录的用户ID
}
// 获取用户信息响应
struct GetUserInfoResponse {
1: required bool success; // 是否成功
2: optional string message; // 错误信息
3: optional UserInfo userInfo; // 用户信息
}
// 发送验证码请求
struct SendVerifyCodeRequest {
1: required string phone; // 手机号
2: required i32 codeType; // 验证码类型: 1-注册,2-登录,3-重置密码
}
// 发送验证码响应
struct SendVerifyCodeResponse {
1: required bool success; // 是否成功
2: optional string message; // 错误信息
}
// 用户服务接口定义
service UserService {
// 用户注册
RegisterResponse Register(1: RegisterRequest req);
// 用户登录
LoginResponse Login(1: LoginRequest req);
// 获取用户信息
GetUserInfoResponse GetUserInfo(1: GetUserInfoRequest req);
// 发送验证码
SendVerifyCodeResponse SendVerifyCode(1: SendVerifyCodeRequest req);
}
这里定义了很多接口,包括用户请求参数结构定义,请求返回参数定义
自动生成代码
执行kitex指令
kitex -module video_douyin idl/user.thrift
返回
代码成功生成
user.go: 根据 IDL 生成的编解码文件,由 IDL 编译器生成
k-consts.go、k-user.go:kitex 专用的一些拓展内容
userservice:kitex 封装代码主要在这里
main.go入口函数定义
package main
import (
"log"
user "video_douyin/kitex_gen/user/userservice"
"video_douyin/pkg/db"
)
func main() {
// 初始化MySQL
if err := db.InitMySQL("root:901project@tcp(127.0.0.1:3306)/kanyuServer?charset=utf8mb4&parseTime=True&loc=Local"); err != nil {
log.Fatalf("MySQL初始化失败: %v", err)
}
// 初始化Redis
if err := db.InitRedis("127.0.0.1:6379", 0); err != nil {
log.Fatalf("Redis初始化失败: %v", err)
}
// 创建UserServiceImpl实例并设置db和redis
impl := &UserServiceImpl{
db: db.DB,
redis: db.Redis,
}
// 创建server
svr := user.NewServer(impl)
// 运行服务
if err := svr.Run(); err != nil {
log.Fatalf("服务运行失败: %v", err)
}
}
这里处理的作用:
1,初始化mysql
2,初始化redis
3,新建rpc服务实例注入db和redis
4,新建server
5,运行服务
依赖配置初始化db和redis
package db
import (
"context"
"fmt"
"sync"
"video_douyin/dal/model"
"github.com/go-redis/redis/v8"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
var (
DB *gorm.DB
Redis *redis.Client
redisOnce sync.Once
)
func InitMySQL(dsn string) error {
var err error
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
// 禁用表名复数化
NamingStrategy: schema.NamingStrategy{
SingularTable: true,
},
})
if err != nil {
return err
}
// 自动迁移
if err := DB.AutoMigrate(&model.User{}); err != nil {
return err
}
return nil
}
func InitRedis(addr string, db int) error {
var initErr error
redisOnce.Do(func() {
Redis = redis.NewClient(&redis.Options{
Addr: addr,
DB: db,
})
if _, err := Redis.Ping(context.Background()).Result(); err != nil {
initErr = fmt.Errorf("redis连接失败: %w", err)
}
})
return initErr
}
model定义
这里实现了注册接口,所以定义user_db表
package model
import (
"gorm.io/gorm"
)
type User struct {
gorm.Model
Phone string `gorm:"unique"`
Password string
Username string
Avatar string
}
// TableName 实现接口返回自定义表名
func (User) TableName() string {
return "user_db" // 指定实际表名
}
接口核心服务逻辑
package main
import (
"context"
"crypto/rand"
"errors"
"fmt"
"log"
"math/big"
"time"
"video_douyin/dal/model"
user "video_douyin/kitex_gen/user"
"github.com/go-redis/redis/v8"
"gorm.io/gorm"
)
// UserServiceImpl implements the last service interface defined in the IDL.
type UserServiceImpl struct {
db *gorm.DB
redis *redis.Client
}
const (
verifyCodePrefix = "verify_code:"
tokenPrefix = "user_token:"
codeExpiration = 5 * time.Minute
tokenExpiration = 24 * time.Hour
)
// Register implements the UserServiceImpl interface
func (s *UserServiceImpl) Register(ctx context.Context, req *user.RegisterRequest) (resp *user.RegisterResponse, err error) {
resp = user.NewRegisterResponse()
if resp == nil {
return nil, errors.New("failed to initialize response")
}
// 1. Validate the request parameters
log.Printf("进入rpc: %v\n", req.GetPhone())
if req.GetPhone() == "" || req.GetVerifyCode() == "" {
msg := "参数为空错误" // 改为包级变量或堆分配
resp.SetMessage(&msg)
resp.SetSuccess(false)
if resp.Message != nil {
log.Printf("错误详情: %s\n", *resp.Message)
}
return
}
// 2. 验证码校验
storedCode, err := s.redis.Get(ctx, verifyCodePrefix+req.GetPhone()).Result()
if err != nil || storedCode != req.GetVerifyCode() {
msg := "验证码错误或已过期"
resp.SetMessage(&msg)
resp.SetSuccess(false)
if resp.Message != nil {
log.Printf("错误详情: %s\n", *resp.Message)
}
return resp, nil
}
// 3. 检查用户是否存在 存在则进入注册网页
var existingUser model.User
if err := s.db.Where("phone = ?", req.GetPhone()).First(&existingUser).Error; err == nil {
errorMsg := "用户已存在"
resp.SetMessage(&errorMsg)
resp.SetSuccess(false)
if resp.Message != nil {
log.Printf("错误详情: %s\n", *resp.Message)
}
return resp, nil
}
// 4. 创建用户
newUser := model.User{
Phone: req.GetPhone(),
Username: fmt.Sprintf("用户%s", req.GetPhone()[:4]),
}
if err := s.db.Create(&newUser).Error; err != nil {
errorMsg := "注册失败"
resp.SetMessage(&errorMsg)
resp.SetSuccess(false)
if resp.Message != nil {
log.Printf("错误详情: %s\n", *resp.Message)
}
return resp, nil
}
// 5. 生成token
token, err := generateToken()
if err != nil {
errorMsg := "系统错误"
resp.SetMessage(&errorMsg)
resp.SetSuccess(false)
if resp.Message != nil {
log.Printf("错误详情: %s\n", *resp.Message)
}
return resp, nil
}
// 6. 存储token
if err := s.redis.Set(ctx, tokenPrefix+token, newUser.ID, tokenExpiration).Err(); err != nil {
errorMsg := "系统错误"
resp.SetMessage(&errorMsg)
resp.SetSuccess(false)
if resp.Message != nil {
log.Printf("错误详情: %s\n", *resp.Message)
}
return resp, nil
}
successMsg := "注册成功"
resp.SetMessage(&successMsg)
resp.SetSuccess(true)
// resp.SetToken(token)// 需改下idl
return resp, nil
}
// Login implements the UserServiceImpl interface.
func (s *UserServiceImpl) Login(ctx context.Context, req *user.LoginRequest) (resp *user.LoginResponse, err error) {
resp = user.NewLoginResponse()
// 1. 校验参数异常
if req.GetPhone() == "" || req.GetVerifyCode() == "" {
errorMsg := "Invalid request parameters" // 声明字符串变量
resp.SetMessage(&errorMsg) // 传递指针
resp.SetSuccess(true)
}
//2,验证码校验
// 2. 验证码校验
storedCode, err := s.redis.Get(ctx, verifyCodePrefix+req.GetPhone()).Result()
if err != nil || storedCode != req.GetVerifyCode() {
errorMsg := "验证码错误或已过期"
resp.SetMessage(&errorMsg)
resp.SetSuccess(false)
return resp, nil
}
// 3. 查询用户是否存在
var existingUser model.User
if err := s.db.Where("phone = ?", req.GetPhone()).First(&existingUser).Error; err != nil {
errorMsg := "用户不存在"
resp.SetMessage(&errorMsg)
resp.SetSuccess(false)
return resp, nil
}
// 4. 生成token
token, err := generateToken()
if err != nil {
errorMsg := "系统错误"
resp.SetMessage(&errorMsg)
resp.SetSuccess(false)
return resp, nil
}
// 5. 存储token
if err := s.redis.Set(ctx, tokenPrefix+token, existingUser.ID, tokenExpiration).Err(); err != nil {
errorMsg := "系统错误"
resp.SetMessage(&errorMsg)
resp.SetSuccess(false)
return resp, nil
}
successMsg := "登录成功"
resp.SetMessage(&successMsg)
resp.SetSuccess(true)
// resp.SetToken(token) 需加上返回给前端
return resp, nil
}
// GetUserInfo implements the UserServiceImpl interface.
func (s *UserServiceImpl) GetUserInfo(ctx context.Context, req *user.GetUserInfoRequest) (resp *user.GetUserInfoResponse, err error) {
resp = user.NewGetUserInfoResponse()
// 1. 参数校验
if req.GetUserId() == 0 {
errorMsg := "Invalid user id"
resp.SetMessage(&errorMsg)
resp.SetSuccess(false)
return resp, nil
}
// 2. 查询用户信息
var userModel model.User
if err := s.db.Where("id = ?", req.GetUserId()).First(&userModel).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
errorMsg := "User not found"
resp.SetMessage(&errorMsg)
resp.SetSuccess(false)
return resp, nil
}
errorMsg := "Database error"
resp.SetMessage(&errorMsg)
resp.SetSuccess(false)
return resp, nil
}
// 3. 构造UserInfo结构体
userInfo := &user.UserInfo{
UserId: userModel.ID,
Username: userModel.Username,
Avatar: &userModel.Avatar,
}
// 4. 设置响应
resp.SetSuccess(true)
resp.SetUserInfo(userInfo)
return resp, nil
}
// SendVerifyCode implements the UserServiceImpl interface.
func (s *UserServiceImpl) SendVerifyCode(ctx context.Context, req *user.SendVerifyCodeRequest) (resp *user.SendVerifyCodeResponse, err error) {
resp = user.NewSendVerifyCodeResponse()
phone := req.GetPhone()
// 1. 校验参数异常
if phone == "" {
errorMsg := "Invalid request parameters" // 声明字符串变量
resp.SetMessage(&errorMsg) // 传递指针
resp.SetSuccess(false)
}
//2,查询db看用户手机号是否存在
CheckPhoneExists(s.db, phone)
//3, 生成验证码
code, _ := generateCaptcha()
log.Printf("code: %s", code) // 记录请求参数
//4. 保存验证码到redis
if err := s.saveCodeToRedis(phone, code); err != nil {
errorMsg := "Failed to save verification code"
resp.SetMessage(&errorMsg)
resp.SetSuccess(false)
return resp, nil
}
//5. 发送验证码
successMsg := "success" // 声明字符串变量
resp.SetMessage(&successMsg) // 传递指针
resp.SetSuccess(true)
return resp, nil
}
func generateCaptcha() (string, error) {
// 生成一个0到999999之间的随机数(6位数)
n, err := rand.Int(rand.Reader, big.NewInt(1000000)) // 1000000是10^6,即最大值+1
if err != nil {
return "", err
}
// 将随机数转换为字符串,并确保长度为6位(不足时前面补0)
captcha := fmt.Sprintf("%06d", n) // %06d确保至少6位数字,不足时前面补0
return captcha, nil
}
// 生成token
func generateToken() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return fmt.Sprintf("%x", b), nil
}
// CheckPhoneExists 检查手机号是否已存在
func CheckPhoneExists(db *gorm.DB, phone string) (bool, error) {
var user = &model.User{}
result := db.Where("phone = ?", phone).First(user)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
return false, nil // 手机号不存在
}
return false, result.Error // 查询出错
}
return true, nil // 手机号已存在
}
// 存入redis
func (s *UserServiceImpl) saveCodeToRedis(phone, code string) error {
key := verifyCodePrefix + phone
return s.redis.Set(context.Background(), key, code, codeExpiration).Err()
}
导入依赖
go mod tidy
服务启动与测试
rpc服务启动
http服务启动
请求
返回
看db存储
源码
GitHub - enjoykanyu/video_feed: 仿抖音后端项目 springcloud+springboot+mysql+redis+rabiitmq
觉得不错的话可以帮点个star呗,感谢
参考
Hertz | CloudWeGo
Kitex | CloudWeGo