go generic
go 泛型学习,距离泛型的发布已经有很长一段时间了,之前大概看了下官方文档,说有些东西可能将来都会改变,不保证向前兼容,就没有具体了解,1.18也发了几个版本了,感觉再不努力又要被小伙伴们卷完了。
困扰
没有泛型的时候带给我们的困扰(虽没有亲身体会过,但是感觉很麻烦)
Case 1
// Sum returns the sum of the provided arguments. |
要计算 n 数之和,如上述例子,如果这时要计算 int32, float64 类型元素的和呢?
通常情况下,我们需要进行 asserting type,或者为每种类型写不同的函数 sumInt
sumInt32
sumInt64
等,在源码中就有类似的代码。
Case 2
func main() { |
This leads to cluttered, hard-to-read code where the only purpose variables serve is for deriving pointers.
对每个变量进行取址操作,我之前就写过这样的代码 ,为了取址不得不声明一个变量。
针对这种情况,可以实现不同的函数进行取址操作。
// PtrInt returns *i. |
看起来可能比上边的优雅了一些,但是还不够,我们看下泛型。
// Ptr returns *value. |
一方面是代码量减少了很多,另一方看是看起来更整洁。
generic 语法
使用 泛型 重写这段代码
// Sum returns the sum of the provided arguments. |
泛型版本:
func Sum[T int](args ...T) T { |
[]
是用来定义泛型的,常用的模式为 [<ID> <CONSTRAINT>]
<ID>
是用来表示泛型的符号;<CONSTRAINT>
约束,表明可使用的具体类型,如上,只能使用 int;
但是这时候我们想支持 int64 类型的 n 数和应该怎么办?
Constraint
func Sum[T int|int64](args ...T) T { |
使用 |
运算符,这时,T 就可以满足 int 或者 int64,至于为什么使用 |
运算符不做过多考究。
但是这时还有一个小问题,如果说要支持更多的类型应该怎么做?难道要 int|int32|int64
? go 也给我们提供了相应的语法。
any
constraint
// sum_any.go |
去执行试下,会意外的发现使用 any
其实行不通,原因是啥呢?
./sum_any.go:11:3: invalid operation: operator + not defined on sum (variable of type T constrained by any)
+
运算符并不是对所有的类型都生效。
既然这样,我们是不是可以声明一个东西,限定只能输数字类型的参数呢?
Composite constraints
复合类型
// Numeric expresses a type constraint satisfied by any numeric type. |
下面这种情况如何处理?
// id is a new type definition for an int64 |
我们给 int64 起了个别名,这时候编译就会报错了,那我们应该怎样去识别别名呢?
Tilde ~
波浪号就是干这个事情的,举个例子,~int
表示:
- 内置的 int 类型
type Integer int
类型,起别名的类型
type Numeric interface { |
Type inference
Type inference is a convenience feature
The Go compiler tries really hard to infer the intended types, but it does not always work when you think it should
If you are not sure why something written generically is not working, try providing the types explicitly
Explicit types
显示指定类型
func main() { |
Multiple generic types
到这里为止,接触到的都是接收单一参数的函数,这里学习下怎么接收多个泛型参数。
// PrintIDAndSum prints the provided ID and sum of the given values to stdout. |
注意
在调用这个函数时仍需要显示指定 sum
的类型,PrintIDAndSum("xx", Sum[int32], 1, 2, 3)
,如果没有指定类型,就会报错:./mulgen.go:12:22: cannot use generic function Sum without instantiation
Declaring a new instance of T
with var
这里跟在上述 sum 中声明变量没什么区别。
Declaring a new instance of T
with new
new 之后的变量会被分配地址,不再是 nil。
Structs
将泛型和结构体结合使用,不难看出语法上和函数定义大差不差。
type Ledger[T ~string, K Numeric] struct { |
为泛型结构体添加方法,正因为结构体中有泛型,所以在声明方法时也要导入相应的符号,约束不需要,因为是为了类型推断使用。
func (l Ledger[T, K]) PrintIDAndSum() { |
简单示例:
// generic struct |
Structural Constraints
这个特性还存在一些问题,泛型的结构体暂时禁止访问字段,其实感觉挺一般的,既然更版本了为啥不做好、做完善呢?
…
匿名结构体
如果说要接收任意包含这三个字段的结构体(即 Ledger 的实例),需要像下面这样实现,所有的结构体都实现了匿名结构体。
func SomeFunc[ |
Structural constraints must match the struct exactly, and this means even if all of the fields in the constraint are present, the presence of additional fields in the provided value means the type does not satisfy the constraint.
这里可以暂时不考虑,后续待官方完善了再学也不迟,毕竟现在工作中用不到这些东西..
Interface constraints
接口约束之前已经见过了, Numeric
限制了类型只能是 int int32 int64
type Numeric interface { |
对于结构体来说,想通过接口调用方法,需要在上述那样的基础上再添加上方法。
// Ledgerish expresses a constraint that may be satisfied by types that have |
Careful Constructs
Internals
Type erasure
在了解 Go 中泛型是怎么做到运行时安全前,先了解一下类型擦除。
Wiki 上的定义是:the load-time process by which explicit type annotations are removed from a program before it is executed at run-time
翻译过来就是,程序在运行时执行前,进程显示的将类型注解从程序中移除掉。(自己翻译的)
伪代码演示如下:
var ints = List<Int32>{1, 2, 3} |
Type erasure 执行后
var ints = List{1, 2, 3} |
这两个的唯一区别就是将类型擦除掉了,这个特性在很多流行的编程语言中都有。
JAVA
java 中 type erasure 的体现:java-type-erasure
.Net
.Net 中 type erasure 的体现:.Net-type-erasure
Golang
Go 中 type erasure 的体现:Golang-type-erasure ,这里介绍到,Go中没有反省模板,list 中保留的仍是具体类型,不像 JAVA 中翻译成 object。
Runtime-safety
关于泛型这里我一直有个疑问,比如我们定义了一个泛型数组,使用 Go 代码声明如下:
type List[T any] []T |
假设我们在初始化的时候使用 int,但是后续执行 append 操作的时候加一个 string 能不能行得通?显然在 Go 中并不行。那么在其他语言中呢?
JAVA
java-runtime-safety 从这里不难看出,java 中擦出了初始化泛型的信息,很难保持在 runtime 时安全。
.NET
.NET-runtime-safety 这里保持了泛型的相关信息,所以在运行时是安全的。
Golang
Golang-runtime-safety 通过前面的内容我们知道,Go 中没有进行 type erasure,所以也可以做到 runtime 时安全。
Runtime-instantiation
The ability to instantiate new types using generics at runtime,在运行时使用泛型初始化的能力。
我觉得,如果要支持运行时初始化的能力,需要具备动态推断类型的能力,在运行时检测泛型是哪种类型。
JAVA
java-runtime-instantiation 不支持,Java中的泛型纯粹是编译时特性。
.NET
.NET-runtime-instantiation 支持.. 这么一看.. .NET 还是牛逼..我有个大学同学人家都是 JAVA,Python,这兄弟学了两年 .NET 哈哈哈哈,后来由于比较冷门,也放弃了哈哈。
Golang
golang-runtime-instantiation 不支持,Go 中的泛型同 java 一样都是编译时的特性,在编译时就确定好了类型。
总结
Compile-time type safety | Type erasure | Runtime type safety | Runtime instantiation | |
Java | ✓ | ✓ | ||
.NET | ✓ | ✓ | ✓ | |
Go | ✓ | ✓ |
至此,泛型的学习就告一段落了,学到了语法,如何使用,已经在某些简单的场景下使用泛型带来的便利性。也或多或少的了解到了其他语言的泛型特性。
参考链接:
本文标题:go generic
文章作者:bqyang
发布时间:2022-04-01
最后更新:2023-04-13
原始链接:https://bqyang.top/2022/language/golang/go-generic/
版权声明:The author owns the copyright, please indicate the source reproduced.