高阶函数编程技巧

函数是 Go 语言的一等公民,如何利用好其高级用法特性,是一件值得思考和实践的事情

背景

在日常业务开发中,对于一些表的不同字段做筛选查询,是基础的功能。而且大部分可能是在根据不同条件去查询。就像这样

1
2
3
4
5
6
type XXXRepo interface {
GetXXXByIdOrName(ctx context.Context, id int, name string) (o []admin.XXX, err error)
GetXXXInfoList(ctx context.Context, req *GetXXXRequest) (total int64, o []admin.XXX, err error)
GetXXXInfo(ctx context.Context, columnId, gradeId int) (o []admin.XXX, err error)
GetXXXByIdList(ctx context.Context, idList []int) (o []admin.XXX, err error)
}

这也还只是少许的一些条件,如果一张表有十多个字段配合查询呢 ? dao层也会有非常多的冗余代码,可能也就改变了一下入参而已。

假设有一张订单表,简化结构如下

1
2
3
4
5
6
7
8
CREATE TABLE `order` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`order_id` bigint NOT NULL COMMENT '订单id',
`shop_id` varchar NOT NULL COMMENT '店铺id',
`product_id` int NOT NULL DEFAULT '0' COMMENT '商品id',
`status` int NOT NULL DEFAULT '0' COMMENT '状态',
PRIMARY KEY (`id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

举例说明用以下这些字段的不同组合来查询

1
order_id, shop_id, produce_id,status

会在dao层来编写类似于这样的代码

根据orderId来查询

1
2
3
4
5
6
7
8
func GetOrderInfoByOrderId(ctx context.Context, orderId int64) ([]*resource.Order) {
db := GetDB(ctx)
db = db.Table(resource.Order{}.TableName())
var infos []*resource.Order
db = db.Where("order_id = ?", orderId)
db.Find(&infos)
return infos
}

根据shopId来查询

1
2
3
4
5
6
7
8
func GetOrderInfoByShopId(ctx context.Context, shopId int64) ([]*resource.Order) {
db := GetDB(ctx)
db = db.Table(resource.Order{}.TableName())
var infos []*resource.Order
db = db.Where("shop_id = ?", shopId)
db.Find(&infos)
return infos
}

可以看到,两个方法的代码极度相似,除了入参和命名不一样,如果再需要按照 produce_id 或者 status 查询,那需要再写几个类似的方法,导致相似的方法非常多。当然很容易想到,如果参数是传多个,传多个不就好了,可能就是这样的写法

1
2
3
4
5
6
7
8
func GetOrderInfo(ctx context.Context,orderId, shopId int64) ([]*resource.Order) {
db := GetDB(ctx)
db = db.Table(resource.Order{}.TableName())
var infos []*resource.Order
db = db.Where("shop_id = ? and order_id = ?", shopId,orderId)
db.Find(&infos)
return infos
}

如果什么时候业务有变化,需要改条件。也许就会变为这样

1
2
3
4
5
6
7
8
9
10
11
12
13
func GetOrderInfo(ctx context.Context,orderId, shopId int64) ([]*resource.Order) {
db := GetDB(ctx)
db = db.Table(resource.Order{}.TableName())
var infos []*resource.Order
if orderId != 0 {
db = db.Where("order_id = ?",orderId)
}
if shopId != 0 {
db = db.Where("shop_id = ?",shopId)
}
db.Find(&infos)
return infos
}

调用方的代码大概是这样的

1
2
3
4
5
// 根据shopId 查询
infos := GetOrderInfo(ctx, 0, 1)

// 根据orderId 查询
infos := GetOrderInfo(ctx, 1, 0)

相当于其他不关心的查询字段用对应类型默认的零值来替换了。

当然也可以用结构体来作为一个参数

1
2
3
4
5
6
func GetOrderInfo(ctx context.Context,order Order) ([]*resource.Order) {
db := GetDB(ctx)
db = db.Table(resource.Order{}.TableName())
db.Where(&order).find(&infos)
return infos
}

但是估计有的人遇到过这样的坑,那就是如果当字段是int,int64等,有0时,不清楚到底是传入了0,还是没有传值,是区分不了的。因为go语言默认的类型零值。
如果是类型的0值也想作为参数来查询,则默认是忽略的,可以参考

gorm 官方有这样一句话

1
NOTE When querying with struct, GORM will only query with non-zero fields, that means if your field’s value is 0, '', false or other zero values, it won’t be used to build query conditions, for example:

针对这种情况可以选择转化为map或者像以下这种方式来判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func GetOrderInfoInfo(ctx context.Context, o Order) ([]*resource.Order) {
db := GetDB(ctx)
db = db.Table(resource.Order{}.TableName())
var infos []*resource.Order
if o.orderId > 0 {
db = db.Where("order_id = ?", o.orderId)
}
if o.shopId != "" {
db = db.Where("shop_id = ?", o.shop_id)
}
// 后面就先省略了
if xxxx
db.Find(&infos)
return infos
}

这里还只是简短几个字段,如果是十几个字段来组合查询,则要写非常多if判断。

基于以上这种所有情况,有必要来优化一下

可以利用函数式编程来优化

定义如下

1
type Option func(*gorm.DB)

定义 Option 是一个函数,这个函数的入参类型是*gorm.DB,返回值为空。

然后针对 表中需要筛选查询的字段定义一个函数,赋值

1
2
3
4
5
6
7
8
9
10
11
12
func OrderID(orderID int64) Option {
return func(db *gorm.DB) {
db.Where("`order_id` = ?", userID)
}
}

func ShopID(shopID int64) Option {
return func(db *gorm.DB) {
db.Where("`shop_id` = ?", shopID)
}
}

所以需要为可能得字段来创建不同的函数,返回一个Option函数,该函数是把入参赋值给【db *gorm.DB】对象

所以基于以上,要改写dao层就很方便了。

1
2
3
4
5
6
7
8
9
10
func GetOrderInfo(ctx context.Context, options ...func(option *gorm.DB)) ([]*resource.OrderInfo) {
db := GetDB(ctx)
db = db.Table(resource.OrderInfo{}.TableName())
for _, option := range options {
option(db)
}
var infos []*resource.OrderInfo
db.Find(&infos)
return infos
}

这样底层的逻辑就不用写很多if判断了,用 for循环来代替

调用者知道自己需要根据什么参数来查询,则就用上面写好的参数函数来作为入参

1
2
3
4
5
// orderID 查询
infos := GetOrderInfo(ctx, OrderID(orderID))

// orderID,shopID 组合查询
infos := GetOrderInfo(ctx, OrderID(orderID), ShopID(shopID))

当然还根据其他 in 等条件查询,再写一个函数即可

经过优化之后,简化了逻辑。相当于配置类的Option就生成了,代码优雅了不少。这里只提到了查询,更新也是类似的道理,删除和写入就没太大必要这样了。

参考
Self-referential functions and the design of options

Using functional options instead of method chaining in Go

分享到 评论