市面上有很多的工具可以通过读取mysql的表,生成 golang 结构体作为 gorm 模型使用了
但是为什么我用了就放弃了?因为只是生成模型还不够!
正常模型定义好之后,通过 gorm 就能愉快的简单的进行 CURD 了,不过在简单和复杂的sql操作中,我们可以更方便、更直观的操作
而这个 gormstruct 工具可以根据表名生成表结构体和数据操作方法
一、 提供的功能
(一)关于模型
- **生成模型 :**生成gorm 模型,包含每个字段的注释,注释统一在右边//对齐,方便全局观察字段的定义情况。
- **生成表的所有字段映射:*使用字段自定义查询时,统一使用映射的字段,在字段的维护上更方便;同时默认查询所有字段,不使用,不使用反射查询所有字段。
- **为所有字段生成
SetXxx()
、GetXxx()
方法:**用户设置/获取模型下指定字段的值
(二)关于dao层
自动判断是否存在软删除字段,存在则默认过滤非软删除记录
- **生成操作表的基本方法:**base_dao.go
- **生成
Save()
方法:**存在则更新,否则插入 - **生成
Create()
方法:**创建数据:允许单条/批量创建,批量创建时传入切片 - **为所有字段生成
WithXxx
方法:**将单个字段的值作为查询条件 - **为所有字段生成
WithXxxs
方法(多一个s):**将单个字段的列表值作为查询条件 - **生成
GetByOption()
方法:**函数选项模式获取单条记录 - **生成
GetListByOption
方法:**函数选项模式获取多条记录:支持分页 - **生成
GetCountByOption()
方法:**函数选项模式获取多条记录:支持统计记录总数 - **生成
GetCustomeResultByOption()
方法:**函数选项模式获取多条记录到自定义结构体(result:务必使用指针变量):支持包含自定义聚合字段(自定义的聚合字段务必添加 gorm:“column:字段的别名;” 标签) - **生成
UpdateByOption()
:**更新数据 - **生成``GetFromXxx()`方法:**简单直接通过单个字段值获取数据(自动判断该字段是否设置唯一索引,否则自动获取单条记录,反之则是多条记录)
- **生成
GetsFromXxx()
方法:**简单直接通过单个字段的列表值获取多条记录 - **生成
FetchByXxx()
方法:**通过索引(唯一索引(主键、唯一索引、唯一复合索引)、非唯一索引(普通索引))作为查询条件获取数据(自动判断索引类型确定获取单条记录还是多条记录)
所有的方法:在项目中都包含相应的测试用例
test/xxx_test.go
如果觉得哪些功能需要改进的欢迎留言,最近还发现一个不错的学习网站推荐给大家 itgogogo.cn , 内容丰富,收集了很多学习资料和面试指南。
二、使用步骤
- 克隆项目到本地(https://github.com/leeprince/gormstruct)
- 重命名
./config/config.bak.yaml
为./config/config.yaml
- 修改
dbinfo
为自己的数据库链接配置
# 数据库设置
dbinfo:
host: 127.0.0.1
port: 3306
username: root
password: root
database: ticket # 该参数可通过命令行设置,命令行设置会覆盖该值。如果未设置则必须在命令中指定
type: 0 # 数据库类型:0:mysql
- 运行指令
go run main.go -t={数据库下的表名}
或者./gormstruct -t={数据库下的表名}
会在./out/model
下生成3个文件
- 模型:表名xxx.go
- dao层:表名_dao.go
- 基础dao层:base.go(只有在第一次该工具时生成)
三、示例
(一)配置项
# 数据库设置
dbinfo:
host: 127.0.0.1
port: 3306
username: root
password: root
database: tmp # 该参数可通过命令行设置,命令行设置会覆盖该值。如果未设置则必须在命令中指定
type: 0 # 数据库类型:0:mysql
# --- 建议保留默认 ---------
# 是否控制台输出数据库表结构文档
gendoc: true
# 自定义数据库字段的数据类型映射 golang 的数据类型
self_type_define:
int(11): int64
int(11) unsigned: uint64
smallint: int16
smallint unsigned: uint16
int: int64
int unsigned: int64
bigint: int64
bigint unsigned: uint64
mediumint: int32
mediumint unsigned: uint32
varchar: string
char: string
date: datatypes.Date
datetime: time.Time
bit(1): "[]uint8"
tinyint: int8
tinyint unsigned: uint8
tinyint(1): int8
tinyint(1) unsigned: uint8
tinyint(2): int8
tinyint(2) unsigned: uint8
tinyint(3): int8
tinyint(3) unsigned: uint8
json: datatypes.JSON
text: string
timestamp: time.Time
double: float64
double unsigned: float64
mediumtext: string
longtext: string
float: float32
float unsigned: float32
tinytext: string
enum: string
time: time.Time
tinyblob: "[]byte"
blob: "[]byte"
mediumblob: "[]byte"
longblob: "[]byte"
integer: int64
# 默认包名
packagename: model
# 输出的文件路径
ouput_dir: ./out/model/
# 是否自动根据存在 id+created_at+updated_at+deleted_at 字段时,统一使用 gorm 的结构体 `gorm.Model` 覆盖
# true: 因为 gorm.model 默认这三个时间字段为time.Time数据类型,所以需要表设置的数据类型为时间类型:date、datetime、timestamp, 并且允许为NULL。否则字段值为非NULL,并且查询记录存在时会报:unsupported Scan, storing driver.Value type []uint8 into type *time.Time。其中自动设置 `deleted_at` IS NULL,除非添加`db.Unscoped()`方法。由于该包已经支持自定义所以,不支持设置为true
# false: 强制为false
is_gorm_model_tag: false
# 设置 "is_gorm_model_tag=true"时主键ID的数据类型
primary_id_type: int64
# 字段允许为Null时是否设置为指针类型
# 设置指针类型的条件:配置允许设置为指针类型&字段允许为null&字段的数据类型为uint|int|float|string
# 设置指针的好处是:通过 gorm 的模型结构体执行更新操作时会更新字段为设置的零值
is_null_to_point: true
# 自定义的行记录删除字段列表
delete_flag_field_list:
- is_delete
- is_deleted
- deleted_at
# --- 建议保留默认-end ---------
(二)生成文件部分示例
1. 表模型
user.go
package model
// 用户表
type Users struct {
ID int64 `gorm:"column:id;primaryKey;type:int(11);autoIncrement" json:"id"` // 主键
Name *string `gorm:"column:name;type:varchar(255);is null;" json:"name"` // 名称
Age int64 `gorm:"column:age;type:int(11);not null;DEFAULT '18'" json:"age"` // 年龄
CardNo string `gorm:"column:card_no;type:varchar(18);not null;DEFAULT ''" json:"card_no"` // 身份证
HeadImg string `gorm:"column:head_img;type:varchar(255);not null;DEFAULT ''" json:"head_img"` // 头像
CreatedAt int64 `gorm:"column:created_at;type:int(11);not null;" json:"created_at"` // 创建时间
UpdatedAt int64 `gorm:"column:updated_at;type:int(11);not null;" json:"updated_at"` // 更新时间
DeletedAt int64 `gorm:"column:deleted_at;type:int(11);not null;DEFAULT '0'" json:"deleted_at"` // 删除时间
}
// 获取结构体对应的表名方法
func (m *Users) TableName() string {
return "users"
}
// 实例化结构体对象
func NewUsers() *Users {
return &Users{}
}
// 获取主键的对应字段
func (m *Users) PrimaryKey() string {
return UsersColumns.ID
}
// 获取主键值
func (m *Users) PrimaryKeyValue() int64 {
return m.ID
}
// 表字段的映射
var UsersColumns = struct {
ID string
Name string
Age string
CardNo string
HeadImg string
CreatedAt string
UpdatedAt string
DeletedAt string
}{
ID: "id",
Name: "name",
Age: "age",
CardNo: "card_no",
HeadImg: "head_img",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
DeletedAt: "deleted_at",
}
// 包含所有表字段的切片
var UsersAllColumns = []string{
UsersColumns.ID, // 主键
UsersColumns.Name, // 名称
UsersColumns.Age, // 年龄
UsersColumns.CardNo, // 身份证
UsersColumns.HeadImg, // 头像
UsersColumns.CreatedAt, // 创建时间
UsersColumns.UpdatedAt, // 更新时间
UsersColumns.DeletedAt, // 删除时间
}
// 设置值:主键
func (m *Users) SetID(v int64) {
m.ID = v
}
// 设置值:名称
func (m *Users) SetName(v *string) {
m.Name = v
}
...
2. 基础dao层
base_dao.go(只有在第一次该工具时生成)
package model
import (
"context"
"gorm.io/gorm"
"time"
)
const (
// 分页器时,默认允许最大的记录数
MaxLimit = 1000
)
// 初始化 gorm 实例的其他字段
type _BaseDAO struct {
*gorm.DB
db *gorm.DB
model interface{}
ctx context.Context
cancel context.CancelFunc
timeout time.Duration
columns []string
isDefaultColumns bool
}
// 设置超时
func (obj *_BaseDAO) SetTimeOut(timeout time.Duration) {
obj.ctx, obj.cancel = context.WithTimeout(context.Background(), timeout)
obj.timeout = timeout
}
// 设置上下文
func (obj *_BaseDAO) SetCtx(c context.Context) {
if c != nil {
obj.ctx = c
}
}
// 获取上下文
func (obj *_BaseDAO) GetCtx() context.Context {
return obj.ctx
}
// 取消上下文
func (obj *_BaseDAO) Cancel(c context.Context) {
obj.cancel()
}
// 获取 DB 实例
func (obj *_BaseDAO) GetDBModel() *gorm.DB {
return obj.DB.Model(&obj.model)
}
// 更新 DB 实例
func (obj *_BaseDAO) UpdateDB(db *gorm.DB) {
obj.DB = db
}
// 重置 gorm 会话
func (obj *_BaseDAO) NewDB() *gorm.DB {
return obj.GetDBModel().Session(&gorm.Session{NewDB: true, Context: obj.ctx})
}
// 开启事务:开启事务之后,必须使用开启事务返回的*gorm.DB, 而不是开启事务时使用*gorm.DB
func (obj *_BaseDAO) BeginTx() {
obj.UpdateDB(obj.GetDBModel().Begin())
}
// 事务回滚
func (obj *_BaseDAO) RollbackTx() {
obj.UpdateDB(obj.GetDBModel().Rollback())
obj.UpdateDB(obj.db)
}
// 事务提交
func (obj *_BaseDAO) CommitTx() {
obj.UpdateDB(obj.GetDBModel().Commit())
obj.UpdateDB(obj.db)
}
// 设置上下文获取 *grom.DB
func (obj *_BaseDAO) withContext() (db *gorm.DB) {
return obj.GetDBModel().WithContext(obj.ctx)
}
// 设置 sql 语句是否默认选择表的所有字段:没有通过WithSelect指定字段时,是否默认选择表的所有字段。更新/统计(count)语句时设置为false。
func (obj *_BaseDAO) setIsDefaultColumns(b bool) {
obj.isDefaultColumns = b
}
// 查询指定字段
func (obj *_BaseDAO) WithSelect(query interface{}, args ...interface{}) Option {
return selectOptionFunc(func(o *options) {
o.selectField = queryArg{
query: query,
arg: args,
}
})
}
// 自定义查询条件
func (obj *_BaseDAO) WithWhere(query interface{}, args ...interface{}) Option {
return queryArgOptionFunc(func(o *options) {
o.queryArg = queryArg{
query: query,
arg: args,
}
})
}
// Or 查询:将所有 Withxxx 的 options.queryMap 作为 Or 的查询条件
func (obj *_BaseDAO) WithOrOption(opts ...Option) Option {
optionOr := initOption(opts...)
return queryOrOptionFunc(func(o *options) {
if len(optionOr.queryMap) > 0 {
o.queryMapOr = make(map[string]interface{}, len(optionOr.queryMap))
for key, value := range optionOr.queryMap {
o.queryMapOr[key] = value
}
}
})
}
// 设置 offset、limit 作为 option 条件支持分页
func (obj *_BaseDAO) WithPage(offset int, limit int) Option {
return pageOptionFunc(func(o *options) {
o.page.offset = offset
o.page.limit = limit
})
}
// 设置 offset、limit 作为 option 条件支持分页
func (obj *_BaseDAO) WithOrderBy(orderBy string) Option {
return orderByOptionFunc(func(o *options) {
o.orderBy = orderBy
})
}
// 分组
func (obj *_BaseDAO) WithGroupBy(groupBy string) Option {
return groupByOptionFunc(func(o *options) {
o.groupBy = groupBy
})
}
// 分组后筛选
func (obj *_BaseDAO) WithHaving(query interface{}, args ...interface{}) Option {
return havingByOptionFunc(func(o *options) {
o.having = queryArg{
query: query,
arg: args,
}
})
}
// 执行 sql 前的准备
func (obj *_BaseDAO) prepare(opts ...Option) (tx *gorm.DB) {
options := initOption(opts...)
tx = obj.withContext().
Scopes(obj.selectField(&options)).
Scopes(obj.queryArg(&options)).
Where(options.queryMap).
Or(options.queryMapOr).
Scopes(obj.paginate(&options)).
Scopes(obj.orderBy(&options)).
Scopes(obj.groupBy(&options)).
Scopes(obj.having(&options))
return
}
// 选择字段
func (obj *_BaseDAO) selectField(opt *options) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if opt.selectField.query != nil && opt.selectField.query != "" {
if opt.selectField.arg != nil && len(opt.selectField.arg) > 0 {
return db.Select(opt.selectField.query, opt.selectField.arg...)
}
return db.Select(opt.selectField.query)
} else if obj.isDefaultColumns {
return db.Select(obj.columns)
}
return db
}
}
// 自定义查询条件
func (obj *_BaseDAO) queryArg(opt *options) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if opt.queryArg.query != nil && opt.queryArg.query != "" {
if opt.queryArg.arg != nil && len(opt.queryArg.arg) > 0 {
return db.Where(opt.queryArg.query, opt.queryArg.arg...)
}
return db.Where(opt.queryArg.query)
}
return db
}
}
// 排序
func (obj *_BaseDAO) orderBy(opt *options) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if opt.orderBy != "" {
return db.Order(opt.orderBy)
}
return db
}
}
// 分组
func (obj *_BaseDAO) groupBy(opt *options) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if opt.groupBy != "" {
return db.Group(opt.groupBy)
}
return db
}
}
// 分组后筛选
func (obj *_BaseDAO) having(opt *options) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if opt.having.query != nil && opt.having.query != "" {
if opt.having.arg != nil && len(opt.having.arg) > 0 {
return db.Having(opt.having.query, opt.having.arg)
}
return db.Having(opt.having.query)
}
return db
}
}
// 分页器
func (obj *_BaseDAO) paginate(opt *options) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if opt.page.limit <= 0 {
return db
}
if opt.page.limit > MaxLimit {
opt.page.limit = MaxLimit
}
return db.Offset(opt.page.offset).Limit(opt.page.limit)
}
}
// 函数选项模式的参数字段
type options struct {
// 指定字段:查询指定字段或者更新指定字段
selectField queryArg
// 自定义查询条件
queryArg queryArg
// 查询条件AND
queryMap map[string]interface{}
// 查询条件OR
queryMapOr map[string]interface{}
// 分页
page paging
// 排序
orderBy string
// 分组
groupBy string
// 分组后的条件过滤条件
having queryArg
}
// query-arg数据结构
type queryArg struct {
query interface{}
arg []interface{}
}
// 分页数据结构
type paging struct {
offset int
limit int
}
// 函数选项模式接口
type Option interface {
apply(*options)
}
// options.selectOptionFunc 实现 Option 接口
type selectOptionFunc func(*options)
func (f selectOptionFunc) apply(o *options) {
f(o)
}
// options.queryArg 实现 Option 接口
type queryArgOptionFunc func(*options)
func (f queryArgOptionFunc) apply(o *options) {
f(o)
}
// options.queryMap 实现 Option 接口
type queryOptionFunc func(*options)
func (f queryOptionFunc) apply(o *options) {
f(o)
}
// options.queryMap 实现 Option 接口
type queryOrOptionFunc func(*options)
func (f queryOrOptionFunc) apply(o *options) {
f(o)
}
// options.page 实现 Option 接口
type pageOptionFunc func(*options)
func (f pageOptionFunc) apply(o *options) {
f(o)
}
// options.update 实现 Option 接口
type orderByOptionFunc func(*options)
func (f orderByOptionFunc) apply(o *options) {
f(o)
}
// options.update 实现 Option 接口
type groupByOptionFunc func(*options)
func (f groupByOptionFunc) apply(o *options) {
f(o)
}
// options.update 实现 Option 接口
type havingByOptionFunc func(*options)
func (f havingByOptionFunc) apply(o *options) {
f(o)
}
// 初始化 options
func initOption(opts ...Option) options {
opt := options{
selectField: queryArg{
query: nil,
arg: nil,
},
queryArg: queryArg{
query: nil,
arg: nil,
},
queryMap: make(map[string]interface{}, len(opts)),
queryMapOr: make(map[string]interface{}, len(opts)),
page: paging{
offset: 0,
limit: 0,
},
orderBy: "",
groupBy: "",
}
for _, o := range opts {
o.apply(&opt)
}
return opt
}
3. 表相应的DAO层
user_dao.go
package model
import (
"context"
"fmt"
"gorm.io/gorm"
)
type UsersDAO struct {
*_BaseDAO
}
// UsersDAO 初始化
func NewUsersDAO(ctx context.Context, db *gorm.DB) *UsersDAO {
if db == nil {
panic(fmt.Errorf("UsersDAO need init by db"))
}
ctx, cancel := context.WithCancel(ctx)
return &UsersDAO{
_BaseDAO: &_BaseDAO{
DB: db.Model(&Users{}),
db: db,
model: Users{},
ctx: ctx,
cancel: cancel,
timeout: -1,
columns: UsersAllColumns,
isDefaultColumns: true,
},
}
}
// GetTableName 获取表名称
func (obj *UsersDAO) GetTableName() string {
users := &Users{}
return users.TableName()
}
// Save 存在则更新,否则插入
func (obj *UsersDAO) Save(users *Users) (rowsAffected int64, err error) {
if users.PrimaryKeyValue() > 0 {
return obj.UpdateByOption(users, obj.WithID(users.ID))
}
return obj.Create(users)
}
// Create 创建数据:允许单条/批量创建,批量创建时传入切片
func (obj *UsersDAO) Create(users interface{}) (rowsAffected int64, err error) {
tx := obj.withContext().Create(users)
return tx.RowsAffected, tx.Error
}
// --- 表中的字段作为 option 条件 ---
// WithID 设置 id(主键) 字段作为 option 条件
func (obj *UsersDAO) WithID(id int64) Option {
return queryOptionFunc(func(o *options) { o.queryMap[UsersColumns.ID] = id })
}
// WithIDs 设置 id(主键) 字段的切片作为 option 条件
func (obj *UsersDAO) WithIDs(ids []int64) Option {
return queryOptionFunc(func(o *options) { o.queryMap[UsersColumns.ID] = ids })
}
...
// WithDeletedAt 设置 deleted_at(删除时间) 字段作为 option 条件
func (obj *UsersDAO) WithDeletedAt(deletedAt int64) Option {
return queryOptionFunc(func(o *options) { o.queryMap[UsersColumns.DeletedAt] = deletedAt })
}
// WithDeletedAts 设置 deleted_at(删除时间) 字段的切片作为 option 条件
func (obj *UsersDAO) WithDeletedAts(deletedAts []int64) Option {
return queryOptionFunc(func(o *options) { o.queryMap[UsersColumns.DeletedAt] = deletedAts })
}
// GetByOption 函数选项模式获取单条记录
func (obj *UsersDAO) GetByOption(opts ...Option) (result *Users, err error) {
opts = append(opts, obj.WithDeletedAt(0))
err = obj.prepare(opts...).Find(&result).Error
return
}
// GetListByOption 函数选项模式获取多条记录:支持分页
func (obj *UsersDAO) GetListByOption(opts ...Option) (results []*Users, err error) {
opts = append(opts, obj.WithDeletedAt(0))
err = obj.prepare(opts...).Find(&results).Error
return
}
// GetCountByOption 函数选项模式获取多条记录:支持统计记录总数
func (obj *UsersDAO) GetCountByOption(opts ...Option) (count int64) {
obj.setIsDefaultColumns(false)
opts = append(opts, obj.WithDeletedAt(0))
obj.prepare(opts...).Count(&count)
return
}
// GetCustomeResultByOption 函数选项模式获取多条记录到自定义结构体(result:务必使用指针变量):支持包含自定义聚合字段(自定义的聚合字段务必添加 gorm:"column:字段的别名;" 标签)
func (obj *UsersDAO) GetCustomeResultByOption(result interface{}, opts ...Option) (err error) {
err = obj.prepare(opts...).Find(result).Error
return
}
// UpdateByOption 更新数据
//
// 参数说明:
// users: 要更新的数据
// opts: 更新的条件
func (obj *UsersDAO) UpdateByOption(users *Users, opts ...Option) (rowsAffected int64, err error) {
obj.setIsDefaultColumns(false)
tx := obj.prepare(opts...).Updates(users)
return tx.RowsAffected, tx.Error
}
// --- 表中的字段作为 option 条件 -END ---
// --- 单个字段作为查询条件 ---
// GetFromID 通过单个 id(主键) 字段值,获取单条记录
func (obj *UsersDAO) GetFromID(id int64) (result *Users, err error) {
result, err = obj.GetByOption(obj.WithID(id))
return
}
// GetsFromID 通过多个 id(主键) 字段值,获取多条记录
func (obj *UsersDAO) GetsFromID(ids []int64) (results []*Users, err error) {
results, err = obj.GetListByOption(obj.WithIDs(ids))
return
}
...
// --- 单个字段作为查询条件 -END ---
// --- 通过索引(唯一索引(主键、唯一索引、唯一复合索引)、非唯一索引(普通索引))作为查询条件 ---
// FetchByPrimaryKey 通过 id 字段值,获取单条记录
func (obj *UsersDAO) FetchByPrimaryKey(id int64) (result *Users, err error) {
result, err = obj.GetByOption(obj.WithID(id))
return
}
// FetchUniqueByCardNo 通过 card_no 字段值,获取单条记录
func (obj *UsersDAO) FetchUniqueByCardNo(cardNo string) (result *Users, err error) {
result, err = obj.GetByOption(obj.WithCardNo(cardNo))
return
}
// FetchUniqueIndexByUnqNameCard 通过 name, card_no 字段值,获取单条记录
func (obj *UsersDAO) FetchUniqueIndexByUnqNameCard(name *string, cardNo string) (result *Users, err error) {
result, err = obj.GetByOption(
obj.WithName(name),
obj.WithCardNo(cardNo))
return
}
// FetchIndexByAge 通过 age 字段值,获取多条记录
func (obj *UsersDAO) FetchIndexByAge(age int64) (results []*Users, err error) {
results, err = obj.GetListByOption(obj.WithAge(age))
return
}
// --- 通过索引(唯一索引(主键、唯一索引、唯一复合索引)、非唯一索引(普通索引))作为查询条件 -END ---
四、注意
零值:0、‘’、false
(一)更新
当通过表模型结构体更新数据时,
- GORM Updates() 方法只会更新非零字段。该工具底层就是用了这个方法,如果您想确保指定字段被更新,你应该使用 Select 更新选定字段、或者允许为零值的字段设置为指针、或使用 map 来完成更新操作
建议:工具会根据是否允许为空,判断允许为空则设置为指针,反之不设置;如果表设计是不允许为空,但是有存在零值,只能手动改为指针
- GORM Save() 方法会保存所有的字段,即使字段是零值
(二)查询
当使用表模型结构体作为条件查询时,GORM 只会查询非零值字段。这意味着如果您的字段值为 0、‘’、false 或其他零值,该字段不会被用于构建查询条件。
但通过该工具生成的DAO层WithXxx
和WithXxxs
作为条件则可以忽略这个,因为底层是用 map
(三)创建时间/创建时间
CreatedAt/UpdatedAt:
- 创建数据时:CreatedAt/UpdatedAt:设置非零值时覆盖,为零值时会自动生成
- 更新数据时:CreatedAt 不变;UpdatedAt 自动更新为当前时间戳
五、总结
如果你是一个Golang开发者,并且在使用 gorm 进行数据库操作,那么gormstruct绝对是你的必备工具,它可以帮助开发者根据表名生成表结构体和数据操作方法,提高开发效率。
如果觉得哪些功能需要改进的欢迎留言,最近还发现一个不错的学习网站推荐给大家 itgogogo.cn , 内容丰富,收集了很多学习资料和面试指南。