本篇文章以用户注册接口为例,快速上手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 tidykitex框架
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



















