为什么 Go 语言没有泛型

为什么这么设计(Why’s THE Design)是一系列关于计算机领域中程序设计决策的文章,我们在这个系列的每一篇文章中都会提出一个具体的问题并从不同的角度讨论这种设计的优缺点、对具体实现造成的影响。如果你有想要了解的问题,可以在文章下面留言。

Go 是一门语法元素少、设计简单的编程语言,简单的设计往往意味着较弱的表达能力,工程师也需要使用更多时间编写重复的逻辑。Go 语言从发布到今天已经过去了 10 多年,向 Go 语言添加泛型的讨论也从 2010 年一直持续到今天。社区对泛型的讨论非常多,呼声也非常高,下这里列举一些泛型相关的讨论和反馈:

很多人都认为 Go 语言永远不会加入泛型,然而这不是正确的结论,Go 语言很可能会在第二个主要版本中加入泛型4。所以本文要分析的问题是 —— 为什么 Go 语言到目前为止都没有泛型,以及这些原因是否已经被解决,又是如何被解决的。

如果你对 Go 语言的标准库稍微有一些了解,你能找到一些如下所示的函数签名:

package sort 
func Float64s(a []float64) 
func Strings(a []string) 
func Ints(a []int) ...

上述函数都是 sort 包提供的,它们的功能非常相似,底层的实现也使用了近乎相同的逻辑,但是由于传入类型的不同却需要对外提供多个函数。Java 的泛型就解决了这个问题:

这段 Java 代码使用泛型数组作为参数实现了通用的数组排序逻辑,任意类型只要实现了 Comparable 接口,insertionSort 函数就能排序由该对象组成的数组。使用泛型能够减少重复的代码和逻辑,为工程师提供更强的表达能力从而提升效率。

既然泛型能够增强语言的表达能力,提升工程师的效率,那么为什么 Go 语言到目前为止也不支持泛型呢?本文总结了两个原因:

  • 泛型困境使我们必须在开发效率、编译速度和运行速度三者中选择两个;

  • 目前社区中的 Go 语言方案都是有缺陷的,而 Go 团队认为泛型的支持不够紧急;

上述两个原因导致 Go 语言没有在 1.x 版本中加入泛型。

泛型困境

泛型和其他特性一样不是只有好处,为编程语言加入泛型会遇到需要权衡的两难问题。语言的设计者需要在编程效率、编译速度和运行速度三者进行权衡和选择5,编程语言要选择牺牲一个而保留另外两个。

generics-dilemma

图 1 - 泛型困境

我们以 C、C++ 和 Java 为例,介绍它们在设计上的不同考量:

  • C 语言是系统级的编程语言,它没有支持泛型,本身提供的抽象能力非常有限。这样做的结果是牺牲了程序员的开发效率,与 Go 语言目前的做法一样,它们都需要手动实现不同类型的相同逻辑。但是不引入泛型的好处也显而易见 —— 降低了编译器实现的复杂度,也能保证源代码的编译速度;

  • C++ 与 C 语言的选择完全不同,它使用编译期间类型特化实现泛型,提供了非常强大的抽象能力。虽然提高了程序员的开发效率,不再需要手写同一逻辑的相似实现,但是编译器的实现变得非常复杂,泛型展开会生成的大量重复代码也会导致最终的二进制文件膨胀和编译缓慢,我们往往需要链接器来解决代码重复的问题;

  • Java 在 1.5 版本引入了泛型,它的泛型是用类型擦除实现的。Java 的泛型只是在编译期间用于检查类型的正确,为了保证与旧版本 JVM 的兼容,类型擦除会删除泛型的相关信息,导致其在运行时不可用。编译器会插入额外的类型转换指令,与 C 语言和 C++ 在运行前就已经实现或者生成代码相比,Java 类型的装箱和拆箱会降低程序的执行效率6

generics-and-programming-languages

图 2 - 不同语言的决策

当我们面对是否应该支持泛型时,实际上需要考虑的问题是:我们应该牺牲工程师的开发效率、牺牲编译速度和更大的编译产物还是牺牲运行速度。

泛型的引入一定会影响编译速度和运行速度,同时也会增加编译器的复杂度,所以社区在考虑泛型时也非常谨慎。Go 2 的泛型提案在面对这个问题时没有进行选择,让具体实现决定是应该影响编译速度(单独编译不同的类型参数)还是运行时间(使用方法调用在运行时决定具体执行的函数)。

不紧急不完善

Go 语言团队认为加入泛型并不紧急7,更重要的是完善运行时机制,包括 调度器8、垃圾收集器等功能。作者在使用 Go 语言时,对泛型没有特别多的需求,只是在提供一些通用的抽象逻辑时不得不使用 interface{} 作为方法的参数,这不是一种很好的做法,但也是在当前语言限制下为数不多的方法。

社区中的大部分泛型提案都有各自的缺陷,所以不会被 Go 团队采纳,在这里我们为大家列出一部分提案,感兴趣的读者可以访问下面的链接了解更多的内容:

正是因为向 Go 语言中加入泛型并不是团队的首要工作,而过去的提案都有明显的缺陷,所以从 Go 语言发布 10 多年以来一直都没有支持泛型。

2019 年 7 月底,Go 团队发布了 Go 2 泛型设计的草稿 Contracts - Draft Design9,这个设计草稿建议增加参数多态来扩展 Go 语言,有了参数多态,函数能够接收的参数不再仅限于子类型关系(Subtyping),还可以有显式的结构约束(Structural constraint),下面的代码就约束了切片中的类型 T 需要满足 stringer 合约:

该提案从语法(Syntax)、类型约束(Type constraint)、类型推导(Type inference)和实现(Implementation)四个方面提出 Go 语言应该如何支持泛型:

  • 语法 —— 泛型、函数和方法是如何声明和使用的?

  • 类型约束 —— 如何定义类型约束?

  • 类型推导 —— 什么时候函数调用可以忽略类型参数?

  • 实现 —— 使用编译期替换还是运行时替换?

与之前的提案相比,这是 Go 团队目前能给出的最好方案,cmd/compile/internal/syntax: parse/print support for type parameters and contracts10 展示了如何通过修改编译器来支持提案中的语法,然而这也只是一个简单的原型,最终的实现和草案本身都需要经过社区的讨论。

总结

Go 语言从来没有旗帜鲜明地反对向语言中加入泛型这一特性,很多人对于 Go 的这一决策都有误解。到目前为止,Go 语言没有泛型的原因也可以简单总结成两点:

  • 泛型困境是所有编程语言都需要面对的,也是加入泛型之前不得不深思熟虑的;

  • 目前的多数泛型提案都有明显的缺陷,而且在 1.x 版本中,提升语言其他方面性能带来的收益比泛型带来的更多;

Go 2 的泛型草案暂时也没有解决这两个问题。它只是决定了引入泛型来增强语言的表达能力,提高程序员的生产力,但是却绕过了编译速度和运行速度的抉择问题,我们还不清楚最终到底会如何决策;最新的草案与之前的版本相比已经相对完善,但是还有很多的问题需要解决,例如:隐式约束(Implied constraints)、双重实现(Dual implementation)等。

作者相信 Go 社区能够做出相对合理的决策,并解决引入泛型带来的问题。到最后,我们还是来看一些比较开放的相关问题,有兴趣的读者可以仔细思考一下下面的问题:

  • Go 草案中的泛型设计与 Java 或者其他语言有哪些不同?

  • Go 语言中的哪些标准库可以被泛型重写?

如果对文章中的内容有疑问或者想要了解更多软件工程上一些设计决策背后的原因,可以在博客下面留言,作者会及时回复本文相关的疑问并选择其中合适的主题作为后续的内容。

推荐阅读

如果你对 Go 语言的设计原理、实现原理比较感兴趣,可以阅读 Go 语言设计与实现 了解更多的内容。

Go 语言是 Google 在 2009 年 12 月发布的编程语言,目前的 Go 语言在国内外的社区都非常热门,很多著名的开源框架 Kubernetes、etcd 和 prometheus 等都使用 Go 语言进行开发,近年来热门的微服务架构和云原生技术也为 Go 语言社区带来了非常多的活力。

作者目前也使用 Go 语言作为日常开发的主要语言,虽然 Go 语言没有 Lisp 系语言的开发效率和强大表达能力,但是却是一门非常容易使用并且大规模运用的工程语言,这也是作者学习和使用 Go 语言的主要原因。

这本书介绍的主要内容其实就是 Go 语言的设计原理与实现,其中包括 Go 语言的编译过程、关键字、运行时、调度器、内存分配等内容。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Golang 是不支持泛型的,所以你不能使用泛型。但是你可以使用类似于泛型的方法来实现类似的效果。 你可以使用 interface{} 类型来表示任何类型。例如: ``` func Print(a interface{}) { fmt.Println(a) } ``` 这样就可以使用 Print 函数输出任何类型的值了。 你也可以使用反射来实现类似的效果。例如: ``` func Print(a interface{}) { v := reflect.ValueOf(a) fmt.Println(v) } ``` 这样就可以使用 Print 函数输出任何类型的值了。 你也可以使用模板函数来实现类似的效果。例如: ``` func Print(a interface{}) { t := template.Must(template.New("").Parse("{{.}}")) t.Execute(os.Stdout, a) } ``` 这样就可以使用 Print 函数输出任何类型的值了。 希望这对你有帮助。 ### 回答2: 截至2021年1月,Go语言(Golang)尚不具备内建的泛型机制,即没有直接的方法来创建通用的数据结构和算法。然而,Go社区一直在探索如何在缺少泛型的情况下实现通用性。 在没有泛型的情况下,开发者通常采用特定类型的接口(interface{})或类型断言(type assertion)来实现类似泛型的效果。使用接口可以定义通用的函数签名,使函数能够接受多种类型参数。同时,使用类型断言可以在运行时检查参数的实际类型,并进行相应的处理。 此外,Go语言还提供了一些通用的数据结构和算法库,如container包中的List、HashMap等。通过在这些数据结构和算法中使用interface{}类型,并利用类型断言来确保类型的正确性,可以实现相对通用的功能。 Go语言的开发团队也一直在推动泛型的研究和开发。在2022年将发布的Go 1.18版本中,预计将加入泛型支持。该特性将为开发者提供使用更为简洁和安全的方式来创建通用的数据结构和算法。 总之,目前在Go语言中还没有内建的泛型机制,但可以通过接口和类型断言来实现类似的功能。此外,Go语言的开发者正在努力研发并计划在未来的版本中添加泛型支持。 ### 回答3: 目前(截至2021年),Go语言Golang)尚不支持原生泛型,这意味着在编写Go代码时,无法直接声明泛型类型或方法。然而,Go社区一直在积极探索和讨论如何实现泛型,有一些可选方案可以用来模拟泛型的使用。 其中,最常见的一种方法是使用接口类型实现泛型。通过创建一个接口类型并在函数签名或结构体中使用该接口类型作为参数或字段,可以实现对各种类型的参数和字段的通用操作。这种方法虽然可以达到类似泛型的效果,但在类型实参和类型断言方面存在一些限制和复杂性。 另一种方法是使用代码生成工具模拟泛型。通过在编译时使用代码生成工具,可以根据不同的类型参数生成特定的代码,并将其插入到源代码中。这种方式可以实现在编译时生成针对不同类型的特定代码,从而实现类似泛型的效果。但是,这种方法需要使用额外的工具和开发流程,并且会增加代码的复杂性。 除了这些方法外,Go社区还在不断探索其他更为原生的泛型实现方式,例如Go2泛型的设计,该设计目前正在积极开发中。Go2泛型通过添加泛型类型和泛型函数的直接支持,将大大简化使用泛型的过程,并提供更好的类型安全和可读性。尽管Go2泛型还未正式发布,但对于在项目中使用泛型的需求,可以关注最新的Go官方进展和社区讨论。 总的来说,目前在Go语言中,尚不支持原生的泛型,但可以使用接口类型或代码生成工具等方式来模拟泛型的使用。未来,随着Go2泛型的正式发布,将会提供更为便捷和直接的方式来使用泛型。环境和项目需求决定了选择合适的方案。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值