详解Go语言如何利用高阶函数写出优雅的代码?

前言

go项目中经常需要查询db,按照以前java开发经验,会根据查询条件写很多方法,如:

  • GetUserByUserID
  • GetUsersByName
  • GetUsersByAge

每一种查询条件写一个方法,这种方式对外是挺好的,对外遵循严格原则,让每个对外的方法接口是明确的。但是对内的话,应该尽可能的通用,做到代码复用,少写代码,让代码看起来更优雅、整洁。

问题

在review代码的时候,针对上面3个方法,一般写法是

func GetUserByUserID(ctx context.Context, userID int64) (*User, error){
    db := GetDB(ctx)
    var user User
    if userID > 0 {
        db = db.Where(`userID = ?`, userID)
    }
    if err := db.Model(&User{}).Find(&user).Err; err != nil {
        return nil, err
    }
    
    return user, nil
}

func GetUsersByName(ctx context.Context, name string) (*User, error){
    db := GetDB(ctx)
    var users []User
    if name != "" {
        db = db.Where(`name like '%%'`, name)
    }
    if err := db.Model(&User{}).Find(&users).Err; err != nil {
        return nil, err
    }
    
    return users, nil
}

func GetUsersByAge(ctx context.Context, age int64) (*User, error){
    db := GetDB(ctx)
    var user User
    if age > 0 {
        db = db.Where(`age = ?`, age)
    }
    if err := db.Model(&User{}).Find(&user).Err; err != nil {
        return nil, err
    }
    
    return user, nil
}

当User表上字段有几十个的时候,上面类似的方法会越来越多,代码没有做到复用。当有Teacher表、Class表等其他表的时候,上面的查询方法又要翻倍。

调用方也会写的很死,参数固定。当要增加一个查询条件的时候,要么改原来的函数,增加一个参数,这样其他调用的地方也都要改;要么新写一个函数,这样函数越来越多,难以维护和阅读。

上面是青铜写法,针对这种情况,下面介绍几种白银、黄金、王者写法

白银

将入参定义成一个结构体

type UserParam struct {
    ID int64
    Name string
    Age int64
}

将入参都放在UserParam结构体中

func GetUserInfo(ctx context.Context, info *UserParam) ([]*User, error) {    
        db := GetDB(ctx)    
        db = db.Model(&User{})
        var infos []*User
        if info.ID > 0 {
            db = db.Where("user_id = ?", info.ID)    
        }
        if info.Name != "" {       
            db = db.Where("user_name = ?", info.Name)    
        }    
        if info.Age > 0 {       
            db = db.Where("age = ?", info.Age)    
        } 
        if err := db.Find(&infos).Err; err != nil {
            return nil, err
        }
        
        return infos, nil
}

这个代码写到这里,相比最开始的方法其实已经好了不少,至少 dao 层的方法从很多个入参变成了一个,调用方的代码也可以根据自己的需要构建参数,不需要很多空占位符。但是存在的问题也比较明显:仍然有很多判空不说,还引入了一个多余的结构体。如果我们就到此结束的话,多少有点遗憾。

另外,如果我们再扩展一下业务场景,我们使用的不是等值查询,而是多值查询或者区间查询,比如查询 status in (a, b),那上面的代码又怎么扩展呢?是不是又要引入一个方法,方法繁琐暂且不说,方法名叫啥都会让我们纠结很久;或许可以尝试把每个参数都从单值扩展成数组,然后赋值的地方从 = 改为 in()的方式,所有参数查询都使用 in 显然对性能不是那么友好。

黄金

更高级的优化方法,是使用高阶函数。

type Option func(*gorm.DB)

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

然后针对每一个需要查询的字段,定义一个高阶函数

func UserID(ID int64) Option {    
    return func(db *gorm.DB) {       
        db.Where("`id` = ?", ID)    
    } 
}

func Name(name int64) Option {    
    return func(db *gorm.DB) {       
        db.Where("`name` like %?%", name)    
    } 
}

func Age(age int64) Option {    
    return func(db *gorm.DB) {       
        db.Where("`age` = ?", age)    
    } 
}

返回值是Option类型。

这样上面3个方法就可以合并成一个方法了

func GetUsersByCondition(ctx context.Context, opts ...Option)([]*User, error) {
    db := GetDB(ctx)
    for i:=range opts {
        opts[i](db)
    }
    var users []User
    if err := db.Model(&User{}).Find(&users).Err; err != nil {
        return nil, err
    }
    return users, nil
}

没有对比就没有伤害,通过和最开始的方法比较,可以看到方法的入参由多个不同类型的参数变成了一组相同类型的函数,因此在处理这些参数的时候,也无需一个一个的判空,而是直接使用一个 for 循环就搞定,相比之前已经简洁了很多。

还可以扩展其他查询条件,比如IN,大于等

func UserIDs(IDs int64) Option {    
    return func(db *gorm.DB) {       
        db.Where("`id` in (?)", IDs)    
    } 
}

func AgeGT(age int64) Option {    
    return func(db *gorm.DB) {       
        db.Where("`age` > ?", age)    
    } 
}

而且这个查询条件最终是转换成Where条件,跟具体的表无关,也就是说这些定义是可以被其他表复用的。

王者

优化到上述方法已经可以了,但是王者一般会继续优化。

上述方法GetUsersByCondition只能查User表,能不能更通用一些,查任意表呢?分享GetUsersByCondition方法,发现如果要做到查任意表,有2个阻碍:

  • 表明是在方法中写死的
  • 返回值定义的是[]*User,不能通用

针对第一个问题,我们可以定义一个Option来实现

func TableName(tableName string) Option {
    return func(db *grom.DB) {
        db.Table(tableName)
    }
}

针对第二个问题,可以将返回参数作为入参,通过引用的方式传进来

func GetRecords(ctx context.Context, in any, opts ...Option) {
    db := GetDB(ctx)
    for i:=range opts {
        opts[i](db)
    }
    
    return db.Find(in).Err
}

// 调用:根据user name 和age 查询users
var users []User
if err := GetRecords(ctx, &users, TableName("user"), Name("张三"), Age(18)); err != nil {
    // TODO
}

总结

这里通过对 grom 查询条件的抽象,大大简化了对 DB 组合查询的写法,提升了代码的简洁。

以上就是详解Go语言如何利用高阶函数写出优雅的代码的详细内容,更多关于Go语言高阶函数的资料请关注其它相关文章!

原文地址:https://juejin.cn/post/7184804445991993403