最佳实践¶
本文档提供使用 Go Web Frame 的推荐模式和最佳实践。
项目结构¶
推荐的目录布局¶
myapp/
├── main.go # 应用入口
├── go.mod # Go 模块文件
├── go.sum
├── config/ # 配置文件目录
│ └── application.yml
├── controller/ # HTTP 处理器 / REST 控制器
│ ├── user_controller.go
│ └── order_controller.go
├── service/ # 业务逻辑层
│ ├── user_service.go
│ └── order_service.go
├── model/ # 数据访问层
│ ├── user.go
│ ├── user_model.go
│ └── order_model.go
├── entity/ # 领域实体
│ ├── user.go
│ └── order.go
├── filter/ # HTTP 过滤器 / 中间件
│ ├── auth_filter.go
│ └── logging_filter.go
├── runner/ # 后台任务
│ └── cleanup_runner.go
├── util/ # 应用工具函数
├── logs/ # 日志文件
└── www/ # 静态文件(可选)
分层分离¶
控制器层(HTTP)¶
控制器只处理 HTTP 相关的关注点:
type UserController struct {
core.IService
userService *UserService
}
func (c *UserController) Init(context *core.Context) error {
c.userService = wf.GetService[*UserService](context)
context.Get("/users", c.List)
context.Get("/users/:id", c.Get)
context.Post("/users", c.Create)
return nil
}
func (c *UserController) List(req *web.Request) (any, error) {
users, err := c.userService.GetAllUsers()
if err != nil {
return nil, err
}
return users, nil
}
func (c *UserController) Get(req *web.Request) (any, error) {
id := req.ParamUint("id")
return c.userService.GetUserById(id)
}
服务层(业务逻辑)¶
服务包含业务逻辑并编排模型:
type UserService struct {
core.IService
userModel *UserModel
emailService *EmailService
}
func (s *UserService) Init(context *core.Context) error {
s.userModel = wf.GetModel[*UserModel](context)
s.emailService = wf.GetService[*EmailService](context)
return nil
}
func (s *UserService) CreateUser(input *CreateUserInput) (*User, error) {
// 业务验证
if !s.isValidEmail(input.Email) {
return nil, errors.New("invalid email format")
}
// 检查唯一性
exists, _ := s.userModel.Query().Where("email = ?", input.Email).Count()
if exists > 0 {
return nil, errors.New("email already registered")
}
// 创建用户
user := &User{
Name: input.Name,
Email: input.Email,
}
if err := s.userModel.Save(user); err != nil {
return nil, err
}
// 发送欢迎邮件(异步)
go s.emailService.SendWelcome(user.Email)
return user, nil
}
模型层(数据访问)¶
模型处理数据库操作:
type UserModel struct {
*model.EntryModel[*User, uint]
}
func (m *UserModel) Init(db *db.DB, ctx *core.Context) error {
m.EntryModel = model.NewEntryModel[*User, uint](db, "t_user")
return m.CreateTable()
}
// 添加自定义查询
func (m *UserModel) FindActiveUsers() ([]*User, error) {
return m.Query().Where("status = ?", 1).Order("create_time desc").All()
}
func (m *UserModel) FindByEmail(email string) (*User, error) {
return m.Query().Where("email = ?", email).One()
}
错误处理¶
定义自定义错误¶
var (
ErrUserNotFound = errors.New("user not found")
ErrInvalidInput = errors.New("invalid input")
ErrUnauthorized = errors.New("unauthorized")
ErrForbidden = errors.New("forbidden")
)
在处理器中使用错误¶
func (c *UserController) GetUser(req *web.Request) (any, error) {
user, err := c.userService.GetUserById(id)
if err != nil {
return nil, err
}
if user == nil {
return nil, ErrUserNotFound
}
return user, nil
}
自定义响应转换器¶
type APIConverter struct{}
func (c *APIConverter) Request(fc web.FilterChain, req *web.Request) {
result, err := fc.Next()
if err != nil {
// 返回标准错误响应
req.Response().AbortWithStatusJSON(500, &web.Message{Code: 500, Msg: err.Error()})
return
}
// 返回标准成功响应
req.Response().AbortWithStatusJSON(200, web.Data(result))
}
通过注册转换器过滤器,可以统一所有 API 的响应格式:
认证与授权¶
使用路由元数据¶
// 定义元数据选项
func Public() web.MetaOption {
return web.WithValue("public", true)
}
func RequireAuth() web.MetaOption {
return web.WithValue("auth", true)
}
func RequireRole(role string) web.MetaOption {
return web.WithValue("role", role)
}
// 注册带元数据的路由
func (c *AdminController) Init(context *core.Context) error {
context.Get("/login", c.Login).WithMeta(Public())
context.Get("/dashboard", c.Dashboard).WithMeta(RequireAuth())
context.Delete("/users/:id", c.DeleteUser).WithMeta(RequireAuth(), RequireRole("admin"))
return nil
}
认证过滤器¶
type AuthFilter struct {
core.IFilter
jwtSecret string
}
func (f *AuthFilter) Init(context *core.Context) error {
f.jwtSecret = context.GetConfig().GetString("jwt.secret")
return nil
}
func (f *AuthFilter) Handle(fc web.FilterChain, req *web.Request) (any, error) {
// 跳过公开路由
if req.HasMeta(Public()) {
return fc.Next()
}
// 检查认证
if req.HasMeta(RequireAuth()) {
token := req.GetHeader("Authorization")
if token == "" {
return nil, ErrUnauthorized
}
claims, err := f.validateToken(token)
if err != nil {
return nil, ErrUnauthorized
}
// 存储用户信息到请求上下文
req.Set("user", claims)
// 检查角色
requiredRole, _ := req.HandlerMeta().Get("role").(string)
if requiredRole != "" && claims.Role != requiredRole {
return nil, ErrForbidden
}
}
return fc.Next()
}
配置管理¶
使用环境变量¶
# application.yml
web:
db:
type: ${DB_TYPE:mysql}
host: ${DB_HOST:localhost}
port: ${DB_PORT:3306}
user: ${DB_USER:root}
password: ${DB_PASSWORD:}
database: ${DB_NAME:myapp}
在代码中访问配置¶
type UserService struct {
core.IService
maxRetries int
timeout time.Duration
}
func (s *UserService) Init(context *core.Context) error {
s.maxRetries = context.GetConfig().GetInt("service.max_retries")
s.timeout = context.GetConfig().GetDuration("service.timeout")
return nil
}
数据库最佳实践¶
使用事务处理多步操作¶
func (s *OrderService) CreateOrder(input *CreateOrderInput) (*Order, error) {
var order *Order
tx := s.ctx.GetTransaction()
err := tx.Exec(func(tx *db.DB) error {
// 使用 GetReNewModel 在事务中创建模型
orderModel := wf.GetReNewModel[*OrderModel](tx, s.ctx)
// 步骤 1:创建订单
order = &Order{UserID: input.UserID, Total: input.Total}
if err := orderModel.Save(order); err != nil {
return err
}
// 步骤 2:创建订单项
itemModel := wf.GetReNewModel[*OrderItemModel](tx, s.ctx)
for _, item := range input.Items {
orderItem := &OrderItem{OrderID: order.Id, ProductID: item.ProductID}
if err := itemModel.Save(orderItem); err != nil {
return err
}
}
return nil
})
return order, err
}
为频繁查询的字段添加索引¶
type User struct {
Id uint `gorm:"primaryKey"`
Email string `gorm:"size:255;uniqueIndex"` // 唯一索引
Status int `gorm:"index"` // 常规索引
CompanyID uint `gorm:"index:idx_company"` // 命名索引
CreatedAt time.Time `gorm:"index:idx_created"` // 用于排序
}
后台任务¶
优雅关闭¶
type EmailRunner struct {
core.IRunner
emailQueue chan *Email
}
func (r *EmailRunner) Init(context *core.Context) error {
r.emailQueue = make(chan *Email, 100)
return nil
}
func (r *EmailRunner) Run() error {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
select {
case <-r.ctx.Done():
// 在退出前处理剩余邮件
r.drainQueue()
return nil
case email := <-r.emailQueue:
if err := r.sendEmail(email); err != nil {
log.Error("failed to send email", zap.Error(err))
}
}
}
}
日志¶
结构化日志¶
func (s *UserService) CreateUser(input *CreateUserInput) (*User, error) {
log.Info("creating user",
zap.String("email", input.Email),
zap.String("name", input.Name),
)
user, err := s.doCreateUser(input)
if err != nil {
log.Error("failed to create user",
zap.String("email", input.Email),
zap.Error(err),
)
return nil, err
}
log.Info("user created",
zap.Uint("id", user.Id),
zap.String("email", user.Email),
)
return user, nil
}
测试¶
单元测试服务¶
func TestUserService_CreateUser(t *testing.T) {
// 设置
mockModel := &MockUserModel{}
service := &UserService{userModel: mockModel}
// 执行
user, err := service.CreateUser(&CreateUserInput{
Name: "Alice",
Email: "alice@example.com",
})
// 断言
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
}
集成测试¶
使用 WebFrame.Test() 方法进行集成测试(不启动 HTTP 服务器):
func TestUserController_GetUser(t *testing.T) {
// 加载测试配置
cfg, err := config.LoadSingleFileConfig("test.yml")
if err != nil {
t.Fatal(err)
}
// 构建应用
builder := wf.NewBuilder(cfg)
builder.Model(&model.UserModel{})
builder.Service(&service.UserService{})
builder.Rest(&controller.UserController{})
app := builder.Build()
// 使用 Test 方法初始化并运行测试
err = app.Test(func(ctx *core.Context) error {
userService := wf.GetService[*service.UserService](ctx)
user, err := userService.GetUserById(1)
if err != nil {
return err
}
assert.Equal(t, "test_user", user.Name)
return nil
})
assert.NoError(t, err)
}
性能技巧¶
- 使用连接池 - 配置合适的池大小
- 为数据库字段添加索引 - 为频繁查询的列添加索引
- 缓存频繁访问的数据 - 使用缓存组件
- 避免 N+1 查询 - 使用 GORM 的 Preload
- 使用后台任务 - 卸载繁重操作
// 好:使用 Preload 避免 N+1
users, err := userModel.Query().
Preload("Profile").
Preload("Orders").
All()
// 坏:N+1 问题
users, _ := userModel.Query().All()
for _, u := range users {
profile, _ := profileModel.FindByUserId(u.Id) // N 次查询!
}