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

相关文章

  • Go语言文件读写操作案例详解

    Go语言文件读写操作案例详解

    这篇文章主要为大家介绍了Go语言文件读写操作案例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • golang 解析word文档操作

    golang 解析word文档操作

    这篇文章主要介绍了golang 解析word文档操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • 利用GO语言实现多人聊天室实例教程

    利用GO语言实现多人聊天室实例教程

    聊天室的实现大家应该都遇到过,这篇文章主要给大家介绍了关于利用GO语言实现多人聊天室的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起看看吧。
    2018-03-03
  • go语言 http模型reactor示例详解

    go语言 http模型reactor示例详解

    这篇文章主要介绍了go语言 http模型reactor,接下来看一段基于reactor的示例,这里运行通过 go run main.go,本文结合示例代码给大家介绍的非常详细,需要的朋友可以参考下
    2023-01-01
  • golang原生实现JWT的示例代码

    golang原生实现JWT的示例代码

    在Go中实现JWT验证,可以通过标准库crypto/hmac、crypto/sha256和encoding/base64来编写自己的JWT,本文就详细的来介绍一下,感兴趣的可以了解下
    2023-05-05
  • 基于Go+OpenCV实现人脸识别功能的详细示例

    基于Go+OpenCV实现人脸识别功能的详细示例

    OpenCV是一个强大的计算机视觉库,提供了丰富的图像处理和计算机视觉算法,本文将向你介绍在Mac上安装OpenCV的步骤,并演示如何使用Go的OpenCV绑定库进行人脸识别,需要的朋友可以参考下
    2023-07-07
  • 使用Go语言封装实现邮件发送功能

    使用Go语言封装实现邮件发送功能

    在现代 Web 开发中,邮件发送功能是一个常见的需求,本文将介绍如何在 Go 语言中封装一个通用的邮件发送包,支持验证码发送和通用邮件发送,需要的可以参考下
    2025-03-03
  • Golang 串口通信的实现示例

    Golang 串口通信的实现示例

    串口通信是一种常见的硬件通信方式,用于在计算机和外部设备之间传输数据,本文主要介绍了Golang 串口通信的实现示例,具有一定的参考价值,感兴趣的可以了解一下
    2024-03-03
  • Go Mongox轻松实现MongoDB的时间字段自动填充

    Go Mongox轻松实现MongoDB的时间字段自动填充

    这篇文章主要为大家详细介绍了Go语言如何使用 mongox 库,在插入和更新数据时自动填充时间字段,从而提升开发效率并减少重复代码,需要的可以参考下
    2025-02-02
  • Go Ticker 周期性定时器用法及实现原理详解

    Go Ticker 周期性定时器用法及实现原理详解

    这篇文章主要为大家介绍了Go Ticker 周期性定时器用法及实现原理详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08

最新评论