这是我在学习 Go 语言过程中,结合 Kitex 和 Gorm 框架搭建一个简单的微服务实现 CRUD 操作的笔记。通过这个简单的小项目,主要是学习到一些go语言的基础语法和一些常用的框架使用方法,同时了解自研组件Kitex的使用,以及go语言常见数据库管理组件Gorm的使用,以及一些微服务设计的基本思路。也是个人实习需要学习的开发框架,由于之前没接触过微服务,通过这系列的学习笔记,能够更好地理解微服务的设计和实现细节,也防止自己忘得太快,留篇笔记记录下来。
一、核心组件介绍
1.1 Kitex —— 字节自研 RPC 框架
Kitex 是字节跳动开源的高性能 Go RPC 框架,属于 CloudWeGo 生态的核心组件。
| 对比维度 |
Java (Spring Cloud) |
Go (字节/CloudWeGo) |
| RPC 框架 |
Dubbo / OpenFeign |
Kitex |
| 契约描述 |
Swagger / Proto |
Thrift IDL |
| 序列化协议 |
JSON / Protobuf |
Thrift Binary(性能更高) |
| 服务注册 |
Eureka / Nacos |
Nacos / ETCD |
Kitex 的核心工作流是 “IDL First”:先写 .thrift 接口描述文件,再用命令行工具自动生成 Go 代码骨架,开发者只需填充业务逻辑。
1.2 GORM —— Go 语言 ORM 框架
GORM 是 Go 社区使用最广泛的 ORM 框架,功能类似 Java 的 JPA/Hibernate。
| 对比维度 |
Java (JPA) |
Go (GORM) |
| 字段映射 |
@Column(name="order_id") |
`gorm:"column:order_id"`(Struct Tag) |
| 指定表名 |
@Table(name="settlement") |
TableName() 方法 |
| 自动建表 |
spring.jpa.ddl-auto=create |
db.AutoMigrate(&Model{}) |
| 链式查询 |
Stream / Criteria API |
db.Where(...).Find(...) |
二、架构设计
本项目模拟的业务场景是商户结算系统(Merchant Settlement),实现对结算单的 CRUD。
1 2 3 4 5 6 7 8 9 10 11
| 客户端 Client ↓ RPC 调用(Kitex / Thrift 协议) Server(main.go 启动,监听 :8888) ↓ handler.go(接收 RPC 请求,处理业务逻辑) ↓ 调用 dal/db/order.go(封装数据库操作) ↓ 依赖 dal/init.go(MySQL 连接初始化) ↓ MySQL 数据库(kitex_settlement)
|
最终项目结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| KitexLearning/ ├── item.thrift ← IDL 契约文件(手写) ├── kitex_gen/ ← Kitex 自动生成(只读) │ └── tiktok/settle/ │ ├── item.go ← 所有 struct 定义 │ └── settlementservice/ │ ├── client.go ← Client 调用代码 │ └── server.go ← Server 注册代码 ├── dal/ │ ├── init.go ← MySQL 连接初始化 │ ├── model/ │ │ └── settlement.go ← GORM 数据库模型 │ └── db/ │ └── order.go ← CRUD 数据库操作封装 ├── handler.go ← RPC 业务逻辑实现 ├── main.go ← 服务启动入口 └── client/ └── main.go ← 测试用 RPC 客户端
|
三、环境准备
3.1 安装 Kitex 工具链
1 2 3 4 5
| go install github.com/cloudwego/thriftgo@latest
go install github.com/cloudwego/kitex/tool/cmd/kitex@latest
|

3.2 初始化项目
1 2 3 4 5
| go mod init kitex-learning
go get gorm.io/gorm gorm.io/driver/mysql
|

四、Step 1:编写 IDL 契约文件
文件路径:item.thrift
Thrift 是一种跨语言的接口描述规范,不是编程语言本身。一份 .thrift 文件可以生成 Go、Java、Python 等多种语言的代码,是微服务之间沟通的通用文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| namespace go tiktok.settle
enum SettlementStatus { PENDING = 0 SETTLED = 1 CANCELLED = 2 }
struct SettlementInfo { 1: i64 order_id 2: i64 user_id 3: i64 merchant_id 4: double amount 5: SettlementStatus status }
struct GetRequest { 1: i64 order_id }
struct CreateRequest { 1: i64 user_id 2: i64 merchant_id 3: double amount }
struct UpdateStatusRequest { 1: i64 order_id 2: SettlementStatus new_status }
struct DeleteRequest { 1: i64 order_id }
struct BaseResponse { 1: i32 code 2: string message 3: optional SettlementInfo data }
service SettlementService { BaseResponse GetOrder(1: GetRequest request) BaseResponse CreateOrder(1: CreateRequest request) BaseResponse UpdateOrder(1: UpdateStatusRequest request) BaseResponse DeleteOrder(1: DeleteRequest request) }
|
注意:结算业务中金额字段 amount 在真实字节生产环境中应使用 i64(以”分”为单位)避免浮点精度丢失,学习阶段用 double 即可。
4.1 生成 Kitex 代码骨架
1
| kitex -module kitex-learning -service settle-service item.thrift
|

执行后自动生成 kitex_gen/ 目录,包含所有 struct 定义和 Server/Client 样板代码,不要手动修改这个目录。
五、Step 2:定义 GORM 数据库模型
文件路径:dal/model/settlement.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package model
type Settlement struct { OrderID int64 `gorm:"primaryKey;column:order_id;comment:结算单ID"` UserID int64 `gorm:"column:user_id;not null;comment:用户ID"` MerchantID int64 `gorm:"column:merchant_id;not null;comment:商户ID"` Amount float64 `gorm:"column:amount;not null;comment:结算金额"` Status int32 `gorm:"column:status;default:0;comment:结算状态 0-待结算 1-已结算 2-已取消"` }
func (Settlement) TableName() string { return "settlement" }
|
DAL 层和 RPC 层解耦:这里 Status 用 int32 而不是 Thrift 的枚举类型,数据库只存数字,语义转换在 Handler 层完成。
六、Step 3:初始化数据库连接
文件路径:dal/init.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package dal
import ( "kitex-learning/KitexLearning/dal/model" "gorm.io/driver/mysql" "gorm.io/gorm" )
var DB *gorm.DB
func Init() { dsn := "root:你的密码@tcp(127.0.0.1:3306)/kitex_settlement?charset=utf8mb4&parseTime=True&loc=Local"
var err error DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { panic("数据库连接失败: " + err.Error()) }
DB.AutoMigrate(&model.Settlement{}) }
|
在 Navicat 中提前创建空库:
1
| CREATE DATABASE kitex_settlement DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
程序启动后 AutoMigrate 会自动建表,无需手动创建字段。

七、Step 4:封装数据库操作层
文件路径:dal/db/order.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| package db
import ( "context" "kitex-learning/KitexLearning/dal" "kitex-learning/KitexLearning/dal/model" )
func CreateOrder(ctx context.Context, order *model.Settlement) error { return dal.DB.WithContext(ctx).Create(order).Error }
func GetOrderByID(ctx context.Context, orderID int64) (*model.Settlement, error) { order := &model.Settlement{} result := dal.DB.WithContext(ctx).Where("order_id = ?", orderID).First(order) return order, result.Error }
func UpdateOrderStatus(ctx context.Context, orderID int64, newStatus int32) error { return dal.DB.WithContext(ctx). Model(&model.Settlement{}). Where("order_id = ?", orderID). Update("status", newStatus).Error }
func DeleteOrder(ctx context.Context, orderID int64) error { return dal.DB.WithContext(ctx). Where("order_id = ?", orderID). Delete(&model.Settlement{}).Error }
|
分层规范:Handler 层不直接写 SQL,所有数据库操作封装在 dal/db/ 层,类比 Java 的 Repository/DAO 层。
八、Step 5:实现 Handler 业务逻辑
文件路径:handler.go
Handler 是整个服务的核心,负责接收 RPC 请求、调用 DAL 层、返回响应。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| package main
import ( "context" "fmt" "kitex-learning/KitexLearning/dal/db" "kitex-learning/KitexLearning/dal/model" settle "kitex-learning/KitexLearning/kitex_gen/tiktok/settle" )
type SettlementServiceImpl struct{}
func (s *SettlementServiceImpl) GetOrder(ctx context.Context, request *settle.GetRequest) (resp *settle.BaseResponse, err error) { resp = &settle.BaseResponse{} order, err := db.GetOrderByID(ctx, request.OrderId) if err != nil { resp.Code = 500 resp.Message = "查询失败: " + err.Error() return resp, nil } resp.Code = 200 resp.Message = "success" resp.Data = &settle.SettlementInfo{ OrderId: order.OrderID, UserId: order.UserID, MerchantId: order.MerchantID, Amount: order.Amount, Status: settle.SettlementStatus(order.Status), } return resp, nil }
func (s *SettlementServiceImpl) CreateOrder(ctx context.Context, request *settle.CreateRequest) (resp *settle.BaseResponse, err error) { resp = &settle.BaseResponse{} order := &model.Settlement{ UserID: request.UserId, MerchantID: request.MerchantId, Amount: request.Amount, Status: 0, } err = db.CreateOrder(ctx, order) if err != nil { resp.Code = 500 resp.Message = "创建失败: " + err.Error() return resp, nil } resp.Code = 200 resp.Message = fmt.Sprintf("创建成功, order_id=%d", order.OrderID) return resp, nil }
func (s *SettlementServiceImpl) UpdateOrder(ctx context.Context, request *settle.UpdateStatusRequest) (resp *settle.BaseResponse, err error) { resp = &settle.BaseResponse{} err = db.UpdateOrderStatus(ctx, request.OrderId, int32(request.NewStatus_)) if err != nil { resp.Code = 500 resp.Message = "更新失败: " + err.Error() return resp, nil } resp.Code = 200 resp.Message = "更新成功" return resp, nil }
func (s *SettlementServiceImpl) DeleteOrder(ctx context.Context, request *settle.DeleteRequest) (resp *settle.BaseResponse, err error) { resp = &settle.BaseResponse{} err = db.DeleteOrder(ctx, request.OrderId) if err != nil { resp.Code = 500 resp.Message = "删除失败: " + err.Error() return resp, nil } resp.Code = 200 resp.Message = "删除成功" return resp, nil }
|
九、Step 6:修改 main.go 串联启动
文件路径:main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package main
import ( "kitex-learning/KitexLearning/dal" settle "kitex-learning/KitexLearning/kitex_gen/tiktok/settle/settlementservice" "log" )
func main() { dal.Init()
svr := settle.NewServer(new(SettlementServiceImpl)) err := svr.Run() if err != nil { log.Println(err.Error()) } }
|
启动服务:

十、Step 7:编写 Client 联调测试
文件路径:client/main.go
Kitex 是 RPC 框架,不能用 Postman 直接测,需要用 Go 写 Client 发起调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| package main
import ( "context" "fmt" "log"
"github.com/cloudwego/kitex/client" settle "kitex-learning/KitexLearning/kitex_gen/tiktok/settle" "kitex-learning/KitexLearning/kitex_gen/tiktok/settle/settlementservice" )
func main() { cli, err := settlementservice.NewClient( "settlement", client.WithHostPorts("127.0.0.1:8888"), ) if err != nil { log.Fatal("创建 client 失败:", err) }
ctx := context.Background()
fmt.Println("========== 测试 CreateOrder ==========") createResp, err := cli.CreateOrder(ctx, &settle.CreateRequest{ UserId: 1001, MerchantId: 2001, Amount: 99.99, }) if err != nil { log.Fatal("CreateOrder RPC 失败:", err) } fmt.Printf("Code: %d, Message: %s\n", createResp.Code, createResp.Message)
var createdOrderID int64 fmt.Sscanf(createResp.Message, "创建成功, order_id=%d", &createdOrderID) fmt.Printf(">>> 新建订单 ID = %d\n", createdOrderID)
fmt.Println("========== 测试 GetOrder ==========") getResp, err := cli.GetOrder(ctx, &settle.GetRequest{OrderId: createdOrderID}) if err != nil { log.Fatal("GetOrder RPC 失败:", err) } fmt.Printf("Code: %d, Message: %s\n", getResp.Code, getResp.Message) if getResp.Data != nil { fmt.Printf("查到订单: OrderId=%d, UserId=%d, Amount=%.2f, Status=%v\n", getResp.Data.OrderId, getResp.Data.UserId, getResp.Data.Amount, getResp.Data.Status) }
fmt.Println("========== 测试 UpdateOrder ==========") updateResp, err := cli.UpdateOrder(ctx, &settle.UpdateStatusRequest{ OrderId: createdOrderID, NewStatus_: settle.SettlementStatus_SETTLED, }) if err != nil { log.Fatal("UpdateOrder RPC 失败:", err) } fmt.Printf("Code: %d, Message: %s\n", updateResp.Code, updateResp.Message)
fmt.Println("========== 测试 DeleteOrder ==========") deleteResp, err := cli.DeleteOrder(ctx, &settle.DeleteRequest{OrderId: createdOrderID}) if err != nil { log.Fatal("DeleteOrder RPC 失败:", err) } fmt.Printf("Code: %d, Message: %s\n", deleteResp.Code, deleteResp.Message) }
|
开两个终端运行:
1 2 3 4 5 6 7
| cd KitexLearning go run .
cd KitexLearning/client go run main.go
|



十一、完整流程回顾
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ① 写 item.thrift → 定义接口契约(IDL First) ↓ ② kitex 命令生成代码 → kitex_gen/ 自动生成,只读 ↓ ③ dal/model/settlement.go → 定义 GORM 数据库映射模型 ↓ ④ dal/init.go → 初始化 MySQL 连接,AutoMigrate 建表 ↓ ⑤ dal/db/order.go → 封装 4 个 CRUD 函数 ↓ ⑥ handler.go → 实现 RPC 业务逻辑,调用 DAL 层 ↓ ⑦ main.go → 调用 dal.Init(),启动 Kitex Server ↓ ⑧ client/main.go → 发起 RPC 调用,验证全链路
|
十二、关键知识点总结
| 知识点 |
说明 |
| IDL First |
先写 Thrift 契约,再生成代码,是字节微服务开发的标准流程 |
| ctx 传递 |
每个函数都传 context.Context,携带 TraceID 和超时信息,字节强制规范 |
| 指针用法 |
Go 中结构体参数、返回值、方法接收者几乎都用指针,避免复制开销 |
| DO → DTO |
DAL 层返回的数据库对象 和 RPC 层的传输对象刻意分开,保证安全隔离 |
| 分层架构 |
Handler 不直接写 SQL,DAL 层不感知 RPC,各层职责单一 |
| AutoMigrate |
GORM 根据 struct 自动建表,Code First,保证代码和数据库结构一致 |
扩展:Hertz是什么?
Hertz 是字节跳动开源的高性能 Go HTTP 框架,属于 CloudWeGo 生态的核心组件。它专注于 HTTP 协议,提供了极简的 API 和高性能的实现,适合构建 RESTful API 和 Web 服务。Hertz 的设计理念是“HTTP First”,与 Kitex 的“IDL First”形成互补,开发者可以根据业务需求选择合适的框架进行开发。之后如果有时间,我也会继续了解,写一篇关于 Hertz 的学习笔记,分享它的核心特性和使用方法。