如何在Go中基于GoFiber与GORM编写通用CRUD控制器?
解决GoFiber+GORM多实体重复CRUD代码的方案
问题场景
使用GoFiber搭建Go服务器,配合GORM操作MySQL数据库,现有8个业务实体。每个实体都需要实现Create、Update、List、Delete四个控制器方法,还要手动注册对应路由,最终生成32个逻辑重复、仅实体名称不同的方法,代码冗余度极高,急需优化。
实体示例(Account模型)
package models import "time" type Account struct { ID uint `json:"id" gorm:"primary_key;auto_increment;not_null"` Name string `json:"name"` Company string `json:"company"` GSTIN string `json:"gstin"` AccountNo string `json:"accountNo" gorm:"unique"` IFSC string `json:"ifsc"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` }
重复控制器示例
package controllers import ( "net/http" "your-project/models" "your-project/database" "github.com/gofiber/fiber/v2" ) // GET: List call func GetAccounts(c *fiber.Ctx) error { accounts := new([]models.Account) result := database.DB.Find(accounts) if result.Error != nil { return result.Error } c.SendStatus(http.StatusOK) return c.JSON(accounts) } // POST func CreateAccount(c *fiber.Ctx) error { account := new(models.Account) err := c.BodyParser(account) if err != nil { return err } result := database.DB.Create(account) if result.Error != nil{ return result.Error } return c.SendStatus(http.StatusCreated) }
重复路由示例
app.Post("api/account", controllers.CreateAccount) app.Get("api/accounts", controllers.GetAccounts)
优化方案
1. 抽离基础模型,消除实体重复字段
定义包含所有实体共性字段的BaseModel,让业务实体通过嵌入复用,减少重复代码:
package models import "time" type BaseModel struct { ID uint `json:"id" gorm:"primary_key;auto_increment;not_null"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } // 修改Account实体,嵌入BaseModel type Account struct { BaseModel Name string `json:"name"` Company string `json:"company"` GSTIN string `json:"gstin"` AccountNo string `json:"accountNo" gorm:"unique"` IFSC string `json:"ifsc"` }
其余7个实体按同样方式嵌入BaseModel,统一ID与时间戳字段。
2. 用泛型实现通用CRUD控制器
基于Go 1.18+的泛型特性,编写一套适配所有实体的通用CRUD逻辑:
package controllers import ( "net/http" "your-project/database" "github.com/gofiber/fiber/v2" "gorm.io/gorm" ) // GenericController 泛型CRUD控制器 type GenericController[T any] struct{} // List 通用列表查询 func (gc *GenericController[T]) List(c *fiber.Ctx) error { var items []T result := database.DB.Find(&items) if result.Error != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": result.Error.Error()}) } return c.Status(http.StatusOK).JSON(items) } // Create 通用创建 func (gc *GenericController[T]) Create(c *fiber.Ctx) error { item := new(T) if err := c.BodyParser(item); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } result := database.DB.Create(item) if result.Error != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": result.Error.Error()}) } return c.Status(http.StatusCreated).JSON(item) } // Update 通用更新(按ID) func (gc *GenericController[T]) Update(c *fiber.Ctx) error { id := c.Params("id") item := new(T) // 校验记录是否存在 if err := database.DB.First(item, id).Error; err != nil { if err == gorm.ErrRecordNotFound { return c.Status(http.StatusNotFound).JSON(fiber.Map{"error": "record not found"}) } return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } // 解析请求体更新字段 if err := c.BodyParser(item); err != nil { return c.Status(http.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) } result := database.DB.Save(item) if result.Error != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": result.Error.Error()}) } return c.Status(http.StatusOK).JSON(item) } // Delete 通用删除(按ID) func (gc *GenericController[T]) Delete(c *fiber.Ctx) error { id := c.Params("id") item := new(T) if err := database.DB.Delete(item, id).Error; err != nil { return c.Status(http.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.SendStatus(http.StatusNoContent) }
3. 批量注册通用路由
创建路由注册工具,通过实体映射批量生成CRUD路由,避免手动编写:
package routes import ( "your-project/controllers" "your-project/models" "github.com/gofiber/fiber/v2" ) // EntityRoute 实体路由配置 type EntityRoute struct { SingleName string // 单个实体名称,如"account" PluralName string // 复数名称,如"accounts" } // RegisterGenericRoutes 注册所有实体的CRUD路由 func RegisterGenericRoutes(app *fiber.App) { entities := []EntityRoute{ {SingleName: "account", PluralName: "accounts"}, {SingleName: "user", PluralName: "users"}, // 依次添加其余6个实体 } // 为每个实体实例化泛型控制器并注册路由 for _, ent := range entities { switch ent.SingleName { case "account": ctrl := new(controllers.GenericController[models.Account]) registerCRUDRoutes(app, ent, ctrl) case "user": ctrl := new(controllers.GenericController[models.User]) registerCRUDRoutes(app, ent, ctrl) // 其余实体同理添加case } } } // registerCRUDRoutes 注册单个实体的CRUD路由 func registerCRUDRoutes[T any](app *fiber.App, ent EntityRoute, ctrl *controllers.GenericController[T]) { basePath := "/api/" + ent.SingleName pluralPath := "/api/" + ent.PluralName app.Post(basePath, ctrl.Create) app.Get(pluralPath, ctrl.List) app.Put(basePath+"/:id", ctrl.Update) app.Delete(basePath+"/:id", ctrl.Delete) }
在main函数中调用路由注册:
func main() { app := fiber.New() // 初始化数据库连接... routes.RegisterGenericRoutes(app) app.Listen(":3000") }
额外优化建议
- 在通用List方法中添加分页、过滤逻辑,解析query参数实现分页:
page := c.QueryInt("page", 1); limit := c.QueryInt("limit", 10); database.DB.Limit(limit).Offset((page-1)*limit).Find(&items) - 若特定实体需要自定义逻辑(如Account的Create需校验GSTIN),可单独编写该方法并替换路由中对应的控制器方法。
内容的提问来源于stack exchange,提问作者Anmol Garg
相关产品推荐
相关产品推荐

