这是我在学习 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
# 安装 Thrift 代码生成工具
go install github.com/cloudwego/thriftgo@latest

# 安装 Kitex 命令行工具
go install github.com/cloudwego/kitex/tool/cmd/kitex@latest

安装 Kitex 工具链

3.2 初始化项目

1
2
3
4
5
# 初始化 Go Module
go mod init kitex-learning

# 引入 GORM 和 MySQL 驱动
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 代码生成成功

执行后自动生成 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

// Settlement 对应数据库中的 settlement 表
// Struct Tag 类比 Java 的 @Column 注解
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-已取消"`
}

// TableName 指定数据库表名,类比 Java 的 @Table(name="settlement")
func (Settlement) TableName() string {
return "settlement"
}

DAL 层和 RPC 层解耦:这里 Statusint32 而不是 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 格式: 用户名:密码@tcp(地址:端口)/数据库名?参数
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())
}

// AutoMigrate:根据 struct 自动创建/更新表结构(Code First)
DB.AutoMigrate(&model.Settlement{})
}

在 Navicat 中提前创建空库:

1
CREATE DATABASE kitex_settlement DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;

程序启动后 AutoMigrate 会自动建表,无需手动创建字段。

GORM 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"
)

// 所有函数都传入 ctx,携带链路追踪信息(TraceID)和超时控制
// 一旦上游 RPC 超时,数据库操作自动取消,不产生"僵尸查询"

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"
// DO → DTO 转换:数据库对象 → RPC 传输对象,字节内部分层核心规范
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
// GORM Create 后会把自动生成的主键回填到 order.OrderID
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())
}
}

启动服务:

1
go run .

Kitex Server 启动成功,监听 8888 端口


十、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() {
// WithHostPorts 直接指定地址,跳过服务发现(本地测试用)
// 真实字节环境会通过 Nacos 自动发现服务地址
cli, err := settlementservice.NewClient(
"settlement",
client.WithHostPorts("127.0.0.1:8888"),
)
if err != nil {
log.Fatal("创建 client 失败:", err)
}

ctx := context.Background()

// 1. CreateOrder
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)

// 从返回消息里解析出真实生成的 order_id
var createdOrderID int64
fmt.Sscanf(createResp.Message, "创建成功, order_id=%d", &createdOrderID)
fmt.Printf(">>> 新建订单 ID = %d\n", createdOrderID)

// 2. GetOrder
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)
}

// 3. UpdateOrder
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)

// 4. DeleteOrder
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
# 终端 1:启动 Server
cd KitexLearning
go run .

# 终端 2:运行 Client
cd KitexLearning/client
go run main.go

Server 端日志

Client 端完整 CRUD 调用成功

Navicat 中保留的结算记录


十一、完整流程回顾

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 的学习笔记,分享它的核心特性和使用方法。