本文永久链接 – 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://tonybai.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

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

© 2022, bigwhite. 版权所有.

Related posts:

  1. Go泛型语法又出“幺蛾子”:引入type set概念和移除type list中的type关键字
  2. Ian Lance Taylor:Go泛型使用的一般准则
  3. Go泛型真的要来了!最早在Go 1.17版本支持
  4. Go 1.18对泛型的支持策略
  5. Go 1.18中值得关注的几个变化