4

使用具名返回值巧妙解决泛型函数返回零值的问题

 2 years ago
source link: https://tonybai.com/2022/05/20/solving-problems-in-generic-function-implementation-using-named-return-values/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

使用具名返回值巧妙解决泛型函数返回零值的问题

solving-problems-in-generic-function-implementation-using-named-return-values-1.png

本文永久链接 – https://tonybai.com/2022/05/20/solving-problems-in-generic-function-implementation-using-named-return-values

Go语言泛型语法特性在Go 1.18版本落地后,不出所料,在github上看到大量的基础容器类型数据结构被用泛型重写。这种重写我觉得是很正常、很自然的,并且实现良好的通用数据结构改为泛型其实也不难,有些简单的结构可能分分钟就能搞定。

Go 1.18发布后,我一直没机会写泛型,今天在做DSL语义模型提取时,多处用到Stack结构,于是想到使用泛型简单实现了一个通用的Stack结构。

在Go中,我们可以用一个切片来定义Stack。泛型Stack类型的定义如下:

type Stack[T any] []T

这里的Stack类型就是一个带有类型参数(type parameter)的泛型类型,它的类型参数的约束(constraints)为any,即允许任何类型作为Stack的元素类型。

Stack是最基础的数据结构,一般来说它具有的操作方法包括:

  • Push:压栈;
  • Pop:弹栈;
  • Top:获取栈顶元素;
  • Len:获取栈内元素个数。

对于以切片为底层存储的Stack而言,压栈Push操作就相当于对切片的追加(append)操作:

func (s *Stack[T]) Push(v T) {
    (*s) = append((*s), v)
}

不过,这里有两点要注意:

  • 泛型类型的方法原型中,receiver部分的类型要带上类型参数,比如这里的*Stack[T];
  • 这里务必要用*Stack[T],而不要像下面代码这样用Stack[T],否则append方法改变的仅仅是Stack[T]的拷贝,而不是原Stack[T]类型的实例。
func (s Stack[T]) Push(v T) {
    s = append(s, v)
}

我们再来看看*Stack[T]的弹栈Pop方法:

func (s *Stack[T]) Pop() T {
    if len(*s) == 0 {
        return nil
    }     

    // Get the last element from the stack.
    t := (*s)[len(*s)-1]

    // Remove the last element from the stack.
    *s = (*s)[:len(*s)-1]
    return t
}

这样实现的Pop方法会提示return nil一行有错误:cannot use nil as T value in return statement。Go编译器错误信息提示我们:nil不能作为T类型的值返回

Stack的类型参数的约束为any,即Stack的元素可以是任意类型,即可以是切片、map等复合类型,亦可以是int、string等值类型。如果将nil作为所有这些类型的零值的确不恰当。

那么当Stack为空时,应该如何返回呢?多亏Go原生支持类型零值,我们可以声明一个类型零值并将其作为返回值返回:

func (s *Stack[T]) Pop() T {
    if len(*s) == 0 {
        var zero T
        return zero // 模拟类型零值
    }     

    // Get the last element from the stack.
    t := (*s)[len(*s)-1]

    // Remove the last element from the stack.
    *s = (*s)[:len(*s)-1]
    return t
}

虽然这种方法有效,但你是不是和我有一样的感觉:不够优雅。下面我们就来看一个更为优雅的小技巧:利用函数的具名返回值,看代码:

func (s *Stack[T]) Pop() (t T) {
    if len(*s) == 0 {
        return
    } 

    // Get the last element from the stack.
    t = (*s)[len(*s)-1]

    // Remove the last element from the stack.
    *s = (*s)[:len(*s)-1]
    return
}

我们看到:具名返回值(named return value)一出马,一切都变得自然而然了。当然这也要归功于Go的类型零值特性。

具名返回值日常使用的不多,从使用的频度来看,Go标准库以及多数项目的代码默认选择非具名返回值(unamed return value)。当函数使用defer且在deferred函数中修改外部函数返回值时,应用具名返回值可以让代码显得更清晰一些:

func Foo() (a int) {
    defer func() {
        a = 5
    }

    a = 6
}

其他情况,看项目编码规范一致性要求以及个人喜好了。不过,Go引入泛型后,针对上述的泛型函数返回零值的情况,相信具名返回值将得到更多的“出镜”的机会。

本文中涉及的示例代码在这里可以下载到:https://github.com/bigwhite/experiments/tree/master/generics/stack。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}
img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://51smspush.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 微信公众号:iamtonybai
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544
iamtonybai-wechat-qr.png

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

© 2022, bigwhite. 版权所有.

Related posts:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK