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

 更新时间:2023年01月05日 09:38:00   作者:nil  
高阶函数(Hiher-order Function)定义为:满足下列条件之一的函数:接收一个或多个函数作为参数;返回值是一个函数。本文为大家介绍了如何利用高阶函数写出优雅的代码,希望对大家有所帮助

前言

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语言高阶函数的资料请关注脚本之家其它相关文章!

相关文章

  • golang gorm框架数据库的连接操作示例

    golang gorm框架数据库的连接操作示例

    这篇文章主要为大家介绍了golang gorm框架数据库操作示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步早日升职加薪
    2022-04-04
  • 浅析Go中函数的健壮性,panic异常处理和defer机制

    浅析Go中函数的健壮性,panic异常处理和defer机制

    这篇文章主要为大家详细介绍了Go中函数的健壮性,panic异常处理和defer机制的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一下
    2023-10-10
  • Golang对sqlite3数据库进行操作实践记录

    Golang对sqlite3数据库进行操作实践记录

    sqlite是嵌入式关系型数据库引擎,官方描述为自包含的、无服务的、零配置并支持事务的关系型数据库引擎,下面这篇文章主要给大家介绍了关于Golang对sqlite3数据库进行操作的相关资料,需要的朋友可以参考下
    2024-03-03
  • Go调用C++动态库实现车牌识别的示例代码

    Go调用C++动态库实现车牌识别的示例代码

    本文主要介绍了如何利用C++中Opencv、TensorRT等库编译出动态库供Go调用,再写个简单的api对上传的车辆图片进行车牌识别,文中通过代码示例给大家介绍的非常详细,需要的朋友可以参考下
    2023-12-12
  • GO语言中回调函数的使用

    GO语言中回调函数的使用

    本文主要介绍了GO语言中回调函数的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-03-03
  • 浅谈Golang中创建一个简单的服务器的方法

    浅谈Golang中创建一个简单的服务器的方法

    这篇文章主要介绍了浅谈Golang中创建一个简单的服务器的方法,golang中的net/http包对网络的支持非常好,这样会让我们比较容易的建立起一个相对简单的服务器,有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-06-06
  • Go 日志封装实战示例详解

    Go 日志封装实战示例详解

    这篇文章主要为大家介绍了Go 日志封装实战示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-04-04
  • Go语言实现LRU算法的核心思想和实现过程

    Go语言实现LRU算法的核心思想和实现过程

    这篇文章主要介绍了Go语言实现LRU算法的核心思想和实现过程,LRU算法是一种常用的缓存淘汰策略,它的核心思想是如果一个数据在最近一段时间内没有被访问到,那么在将来它被访问的可能性也很小,因此可以将其淘汰,感兴趣想要详细了解可以参考下文
    2023-05-05
  • Golang 探索对Goroutine的控制方法(详解)

    Golang 探索对Goroutine的控制方法(详解)

    下面小编就为大家分享一篇Golang 探索对Goroutine的控制方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2017-12-12
  • golang连接mysql数据库操作使用示例

    golang连接mysql数据库操作使用示例

    这篇文章主要为大家介绍了golang连接mysql数据库操作使用示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步早日升职加薪
    2022-04-04

最新评论