# 开发流程
# dao/do/entity 生成
在项目根目录下执行make dao(gf gen dao)生成对应的dao/do/entity文件:
dao # 通过对象方式访问底层数据源,底层基于ORM组件实现
do # 数据转换模型,业务模型到数据模型的转换,由工具维护,用户不能修改。每次生成代码文件将会被覆盖
entity # 数据模型,由工具维护,用户不能修改。每次生成代码文件将会被覆盖
# dao中的internal/dao/internal/user.go
# 用于封装对数据表user的访问。该文件自动生成了一些数据结构和方法,简化对数据表的CRUD操作
# 该文件每次生成都会覆盖,由开发工具自动维护,开发者无需关心
- 代码详解
// UserDao 结构体:封装表名、数据库组、列名和自定义处理器
type UserDao struct {
table string
group string
columns UserColumns
handlers []gdb.ModelHandler
}
// UserColumns结构体:存储字段名常量(id、name、status、age)避免硬编码
type UserColumns struct {
Id string
Name string
Status string
Age string
}
// UserColumns结构体:赋值
var userColumns = UserColumns{
Id: "id",
Name: "name",
Status: "status",
Age: "age",
}
// 个工厂函数,用于创建并初始化 UserDao 实例
// 可以传入 0 个或多个 ModelHandler 函数
// 返回值 *UserDao:返回 UserDao 结构体的指针
func NewUserDao(handlers ...gdb.ModelHandler) *UserDao {
// 结构体初始化
return &UserDao{
group: "default", // 数据库配置组名,使用默认配置
table: "user", // 操作的数据库表名
columns: userColumns, // 列名集合(之前定义的变量)
handlers: handlers, // 自定义处理器切片
}
}
// 根据配置组名获取对应的数据库连接,返回底层数据库对象
func (dao *UserDao) DB() gdb.DB {
return g.DB(dao.group)
}
// 返回表名 "user"
func (dao *UserDao) Table() string {
return dao.table
}
// 返回列名结构体
func (dao *UserDao) Columns() UserColumns {
return dao.columns
}
// 返回数据库配置组名 "default"
func (dao *UserDao) Group() string {
return dao.group
}
// DAO 的核心方法,用于创建带上下文的数据库查询模型
// 参数 ctx context.Context:接收上下文,用于传递请求级别的信息(如超时、追踪ID等)
// 返回值 *gdb.Model:返回 GoFrame 的查询构建器对象
func (dao *UserDao) Ctx(ctx context.Context) *gdb.Model {
// 创建基础模型,得到一个基础的查询构建器
model := dao.DB().Model(dao.table)
// 遍历所有注册的ModelHandler函数,每个handler接收当前model,返回修改后的model
// 典型应用场景:
// 软删除过滤:m.Where("deleted_at", nil)
// 数据权限控制:m.Where("tenant_id", tenantId)
// 自动添加条件:m.Where("status", 1)
for _, handler := range dao.handlers {
model = handler(model)
}
// 设置安全模式和上下文
// 启用安全模式,防止误操作(如不带条件的 UPDATE/DELETE)
// 绑定上下文,支持:请求链路追踪、超时控制、日志记录
return model.Safe().Ctx(ctx)
}
// 事务封装方法,简化了数据库事务的使用,自动化管理:无需手动调用 Begin/Commit/Rollback
// f func(ctx context.Context, tx gdb.TX) error 回调函数:包含需要在事务中执行的业务逻辑
// ctx:上下文(可传递给子操作)
// tx gdb.TX:事务对象,用于执行 SQL
// 返回值:error,决定事务提交还是回滚
func (dao *UserDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}
- 使用示例
// 带处理器(创建时传入)
dao := NewUserDao(
func(m *gdb.Model) *gdb.Model {
return m.Where("status", 1) // 只查询正常状态
},
)
// 基本查询
users := dao.Ctx(ctx).Where("age > ?", 18).All()
// 事务使用
err := userDao.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
// 步骤1:扣除转出账户余额
_, err := tx.Exec("UPDATE user SET balance = balance - 100 WHERE id = ?", fromId)
if err != nil {
return err // 触发回滚
}
// 步骤2:增加转入账户余额
_, err = tx.Exec("UPDATE user SET balance = balance + 100 WHERE id = ?", toId)
if err != nil {
return err // 触发回滚
}
return nil // 所有操作成功,自动提交
})
if err != nil {
log.Fatal("转账失败,已回滚")
}
# dao中的internal/dao/user.go
# 对internal/dao/internal/user.go的进一步封装,用于供其他模块直接调用访问
# 该文件开发者可以随意修改,或者扩展dao的能力
- 代码详解
// 小写 userDao:私有结构体,外部包无法直接创建
type userDao struct {
*internal.UserDao // 通过匿名组合(Embedding)继承内部 DAO 的所有方法
}
// 分组声明的写法,
// User:大写字母开头,是公开的全局变量
// 单例模式:整个应用共享同一个 User 实例
var (
// 初始化包装结构体
User = userDao{
internal.NewUserDao()
}
)
// 单独声明的方式,两种写法在功能上完全等价
// 多个变量:推荐用 var () 分组,更符合 Go 社区惯例
// var User = userDao{internal.NewUserDao()}
- 可以在 userDao 中添加业务特定的查询方法
// 在 user.go 中可以添加自定义方法
func (d *userDao) FindActiveUsers(ctx context.Context) ([]*entity.User, error) {
var users []*entity.User
err := d.Ctx(ctx).Where("status", 1).Scan(&users)
return users, err
}
// 调用
activeUsers, _ := dao.User.FindActiveUsers(ctx)
# api 请求/输出/接口
type CreateReq struct {
g.Meta `path:"/user" method:"post" tags:"User" summary:"Create user"`
Name string `v:"required|length:3,10" dc:"user name"`
Age uint `v:"required|between:18,200" dc:"user age"`
}
type CreateRes struct {
Id int64 `json:"id" dc:"user id"`
}
注意
- 校验规则在调用路由函数之前就已经由GoFrame框架的Server自动执行了
- 如果请求参数校验失败,会立即返回错误,不会进入到路由函数
- 删除接口
type DeleteReq struct {
g.Meta `path:"/user/{id}" method:"delete" tags:"User" summary:"Delete user"`
Id int64 `v:"required" dc:"user id"`
}
type DeleteRes struct{}
- 更新接口
// 定义了一个用户状态类型Status
type Status int
const (
StatusOK Status = 0 // User is OK.
StatusDisabled Status = 1 // User is disabled.
)
type UpdateReq struct {
g.Meta `path:"/user/{id}" method:"put" tags:"User" summary:"Update user"`
Id int64 `v:"required" dc:"user id"`
Name *string `v:"length:3,10" dc:"user name"`
Age *uint `v:"between:18,200" dc:"user age"`
Status *Status `v:"in:0,1" dc:"user status"`
}
type UpdateRes struct{}
注意
- 接口参数我们使用了指针来接收,目的是避免类型默认值对我们修改接口的影响
- 举个例子,假如Status不定义为指针,那么它就会有默认值0的影响
- 那么在处理逻辑中,很难判断到底调用端有没有传递该参数,是否要真正修改数值为0
- 但我们使用指针后,当用户没有传递该参数时,该参数的默认值为nil,处理逻辑便很好做判断
- 查询接口(单个)
type GetOneReq struct {
g.Meta `path:"/user/{id}" method:"get" tags:"User" summary:"Get one user"`
Id int64 `v:"required" dc:"user id"`
}
type GetOneRes struct {
*entity.User `dc:"user"`
}
- 查询接口(列表)
type GetListReq struct {
g.Meta `path:"/user" method:"get" tags:"User" summary:"Get users"`
Age *uint `v:"between:18,200" dc:"user age"`
Status *Status `v:"in:0,1" dc:"user status"`
}
type GetListRes struct {
List []*entity.User `json:"list" dc:"user list"`
}
# controller 代码生成
通过make ctrl命令(或者gf gen ctrl)生成控制器代码,生成的代码主要包含3类文件:
api接口抽象文件
# 用于保证控制器实现的接口完整性,该文件由开发工具维护,开发者无需关心
controller路由对象管理
# 用于管理控制器的初始化,以及一些控制内部使用的数据结构、常量定义
# user.go 是一个空文件,可用于定义一些控制器内部使用的数据结构、常量等内容
# user_new.go 文件是自动生成的路由对象创建文件,这两个文件只会生成一次,随后开发者可以随意修改
controller路由实现代码
# 用于具体的api接口实现的代码文件
# 默认情况下,一个api接口生成一个源码文件。也可以控制按照api文件定义的接口生成到一个源码文件中
为何存在一个空的 go 文件
- 该文件只会生成一次,用户可以在里面填充必要的预定义代码内容
- 例如,该模块 controller 内部使用的变量、常量、数据结构定义,或者包初始化 init 方法等
# logic 业务逻辑实现
// 务逻辑的具体实现类
type sUser struct{}
// 自动注册机制:包被导入时自动执行
// 只有在生成完成接口文件后,您才能在每个业务模块中加上接口的具体实现注入
func init() {
service.RegisterUser(New())
}
// 是一个构造函数,典型的工厂模式,用于创建服务层实例
func New() service.IUser {
return &sUser{}
}
- 创建逻辑实现
func (s *sUser) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) {
id, err := dao.User.Ctx(ctx).Data(do.User{
Name: req.Name,
Age: req.Age,
Status: v1.StatusOK,
}).InsertAndGetId()
if err != nil {
return nil, err
}
res = &v1.CreateRes{
Id: id,
}
return
}
- 删除接口
func (s *sUser) Delete(ctx context.Context, req *v1.DeleteReq) (res *v1.DeleteRes, err error) {
// WherePri方法,该方法会将给定的参数req.Id作为主键进行Where条件限制
_, err = dao.User.Ctx(ctx).WherePri(req.Id).Delete()
return
}
- 更新接口
func (s *sUser) Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error) {
//_, err = dao.User.Ctx(ctx).Data(g.Map{
// "name": req.Name,
// "age": req.Age,
//}).Where("id", req.Id).Update()
//return
_, err = dao.User.Ctx(ctx).Data(do.User{
Name: req.Name,
Age: req.Age,
Status: v1.StatusOK,
}).WherePri(req.Id).Update()
return
}
- 查询接口(单个)
func (s *sUser) GetOne(ctx context.Context, req *v1.GetOneReq) (res *v1.GetOneRes, err error) {
res = &v1.GetOneRes{} // 创建一个空的 GetOneRes 实例,因为 Scan(&res.User) 需要一个有效的指针来填充数据
err = dao.User.Ctx(ctx).WherePri(req.Id).Scan(&res.User)
return
}
注意
- &res.User中的User属性对象其实是没有初始化的,其值为nil
- 如果查询到了数据,Scan方法会对其做初始化并赋值
- 如果查询不到数据,那么Scan方法什么都不会做,其值还是nil
- 查询接口(多个)
func (s *sUser) GetList(ctx context.Context, req *v1.GetListReq) (res *v1.GetListRes, err error) {
res = &v1.GetListRes{}
err = dao.User.Ctx(ctx).Where(do.User{
Age: req.Age,
Status: req.Status,
}).Scan(&res.List)
return
}
# service 服务接口层
gf gen service 通过分析给定的 logic 业务逻辑模块下的代码,自动生成 service 代码,包含三部分:
service # 生成接口文件代码
logic.go # 接口实现注册文件,用于在程序启动时,将接口的具体实现在启动时执行注册
main.go # 最顶部 import _ "demo/internal/logic" 后加空行。若还引入了packed包,放到packed后面
注意
- 由于该命令是根据业务模块生成 service 接口,因此只会解析二级目录下的 go 代码文件,并不会无限递归分析代码文件。以 logic 目录为例,该命令只会解析 logic/xxx/*.go 文件。因此,需要 logic 层代码结构满足一定规范。
- 不同业务模块中定义的结构体名称在生成的 service 接口名称时可能会重复覆盖,因此需要在设计业务模块时保证名称不能冲突。
type (
IUser interface {
Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error)
Delete(ctx context.Context, req *v1.DeleteReq) (res *v1.DeleteRes, err error)
Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error)
GetOne(ctx context.Context, req *v1.GetOneReq) (res *v1.GetOneRes, err error)
GetList(ctx context.Context, req *v1.GetListReq) (res *v1.GetListRes, err error)
}
)
var (
localUser IUser
)
func User() IUser {
if localUser == nil {
panic("implement not found for interface IUser, forgot register?")
}
return localUser
}
func RegisterUser(i IUser) {
localUser = i
}