Play with Generic Go

  1. golang
  2. programming
  3. opinion

Go 终于在 1.18 喜迎泛型,在它正式 release 之前忍不住玩了一下,然后跟 ahxxm 一起写了一个名字非常普通(当然跟 lodash 没有关系,我学的是 dash.el)的工具库 go-dash

Go 与泛型与我

第一次用 Go 可能要回到 5 年前,刚从一个 Android/iOS Developer 转型,给公司写一个新的后端项目。在用习惯了充满语法糖的 kotlin 和 swift 之后再来使用这样一个“大道至简”的语言确实不太习惯。其中的一点当然就是泛型。关于泛型的讨论在 Go 社区应该从未停止过,这当中还有不少支持者是让我比较难以相信的,这也算是我第一次接触到“语言宗教”这样一个概念吧。不过这就扯远了,我简单说一下我不喜欢的理由吧。

没有泛型必然导致很多“泛用”方法需要针对不同类型重复定义,在没有泛型的时候,大概有这样三种 workaround(AFAIK)。而这三种方法和我不喜欢的主要理由如下:

  • 标准库作弊,比如说 append, len 等。相当于是对于特定数据结构进行了“特殊处理”,如果你有类似的自定义数据结构,却没办法保证一致的处理模式。
  • 使用 interface{} 然后通过反射处理。既享受不了动态语言的好处,却要承担动态语言的坏处。
  • 代码生成。虽然重复,但又没那么重复。生成的代码是代码还是数据?应不应该放在仓库里?
_20220321_131240screenshot.png
Figure 1: the many interfaces (from package go-gorp/gorp)

他们还有一个共同的问题就是会导致代码的可读性或可浏览性变差,虽然没有盲目使用其他语言的“设计模式”来得伤害那么大,但也算是相当难受了。咳咳,阴阳怪气就到这里吧。毕竟后来官方就宣布“已经在做了”。

快进到前段时间突然在 HN 看到了 go-generics-example,发现本来预订要在 Go 2 出现的泛型,竟然在 Go 1.17 已经可以实验性开启,也就是说不需要等到很大概率不会保持向后兼容的 Go 2 了。

Go Dash, Go

泛型成为 a thing 之后,似乎 Golang 团队对如何修改标准库开始犯难了,作为一个长期向后兼容的语言,如何合理的进化标准库确实是一个很难的话题。详情可见这些讨论:

抱着知道写这些代码多半不会有太大用处的心情,我们还是从 clojure.core 以及 clojure.core.async 中精心(随机)挑选了一些 API 之后,就开始模仿(抄袭)了。

Go 1.17 中的实验性泛型功能无法定义 public function, 所以需要用到 gotip 来获取 master 分支的 Go. 比较惊喜的是 gopls 也同步更新着,所以工具支持上没有任何问题。

go get -v golang.org/dl/gotip
gotip download
gotip get golang.org/x/tools/gopls@master golang.org/x/tools@master

发稿日 之前,2022/3/15 Go 1.18 正式 release 了,所以 gotip 不再必要。

Gerrit What

实现这些方法倒是也不难(比较难的是我不确定只写了针对 int 类型的测试算不算完全覆盖),不过在写了一点注释后尝试通过 go doc 生成文档时倒是发现了一个奇妙的 bug,那就是 所有返回 []T 的方法都会被 go doc 忽略 ,对于针对 slice 写的 dash 包几乎可以说是全灭了。

勤奋的 ahxxm 同学即日就阅读了 go doc 源码并提交了 issuepr,不过最后因为某团队 Code Review 效率问题,最后还是被内部解决了。

Play and Learn

Go 其实不算是一个能让人感觉 exciting 的语言,增加泛型这次算是个例外吧。写这个没用的库倒是也能学到一些东西,比如在 pkg.go.dev 可以随意申请添加 package。看到 go doc 生成的 html 之后我对从注释生成文档也没那么讨厌了(治好了我的 javadoc 后遗症)。

不过比起这些我更希望标准库能尽快更新泛型版本,以后堆代码也能开心一点。就目前来看 1.18 里包含的 golang.org/x/exp 只能算聊胜于无吧,并且也不太可能在生产代码中使用。