实践是检验真理的唯一标准,所以我们就实际写个泛型函数来验证一下。如下代码给出了一个只有一个类型参数的泛型函数,并且分别用两种类型参数来调用它:
package main
import (
"fmt"
)
func Print[T any](v T) {
fmt.Printf("%T: %[1]v\n", v)
}
type Int int
func main() {
Print(1)
Print(Int(1))
}
首先来看一下运行效果,使用go1.18进行编译,然后运行得到的可执行文件,输出结果如下所示:
$ ./main.exe
int: 1
main.Int: 2
go tool nm main.exe | grep ' main\.'
4c7a28 R main..dict.Print[int]
4c7a30 R main..dict.Print[main.Int]
525060 D main..inittask
48c9e0 T main.Print[go.shape.int_0]
48c980 T main.main
// Print(1)
LEAQ main..dict.Print[int](SB), AX
MOVL $0x1, BX
CALL main.Print[go.shape.int_0](SB)
// Print(Int(2))
LEAQ main..dict.Print[main.Int](SB), AX
MOVL $0x2, BX
CALL main.Print[go.shape.int_0](SB)
出于实现起来更简单以及性能方面的考量,我们没有用一个实例来支持所有可能的类型参数。相反,我们让具有相同gcshape的一组类型参数共享一个实例。
在命名方面,所有的gcshape都会被放置到内置包go.shape中。由于实现方面的原因,我们根据类型参数在列表出现的顺序,为相应的gcshape类型加上序号后缀。因此,当一个底层是string类型的类型参数出现在列表中第一个位置时,就会被命名为go.shape.string_0,出现在第二个位置时就会被命名为go.shape.string_1,以此类推。所有的指针类型都作为*uint8类型来命名,也就是go.shape.*uint8_0以及go.shape.*uint8_1等。
我们把一个泛型函数或方法针对一组shape类型参数的实例化,称为shape实例化。
为了创建所需的字典,我们需要把shape类型参数替换为真正的类型参数,这就要求shape类型参数完全可区分。类型参数列表中可能有多个shape参数有着相同的类型,所以我们才要按照它们的顺序给它们加上编号后缀,这样就确保了不同shape参数间完全可区分。
接下来我们就一边参照Proposal一边实践,来看一下字典中到底包含哪些内容。go1.18的编译器在开发的时候,内置了打印字典相关调试信息的代码,只不过在发布的时候用一个布尔变量把这部分逻辑关闭掉了,我们可以再把它打开。在src/cmd/compile/internal/noder/stencil.go的第32行有个infoPrintMode变量,它的默认值是false,把它改成true,然后执行src下的buildall.bash来重新构建编译器。用构建好的编译器来编译泛型代码,就能够看到生成了哪些字典,以及各个字典的结构。下面,我们实际看一下组成字典的4大部分:
第一部分,具体类型参数
这一区间包含了用来调用泛型函数或方法的类型参数的类型信息,也就是对应的runtime._type的地址。比如,build如下代码:
package main
import (
"fmt"
)
func Print[T any](v T) {
fmt.Printf("%T: %[1]v\n", v)
}
type Int int
func main() {
Print(Int(2))
}
$ go build main.go
# command-line-arguments
0] > InstInfo for Print[go.shape.int_
Typeparam go.shape.int_0
> Done Instinfo
=== Creating dictionary .dict.Print["".Int]
* Int
Main dictionary in main at generic function call: Print - (<node FUNCINST>)(Int(2))
=== Finalizing dictionary .dict.Print["".Int]
=== Finalized dictionary .dict.Print["".Int]
第二部分,派生类型信息
这种情况所描述的,就是泛型函数或方法中基于类型参数创建了新的类型,比如*T、[]T和map[K,V]等,并且我们需要用到这些派生类型的动态类型信息(类型元数据)。比如显式或隐式的转换为空接口类型interface{},或者作为type switch或类型断言的目标类型等。简单起见,我们就来测试一个[]T的示例,对上一段代码稍作修改,只是改动了fmt.Printf的第二个参数:
func Print[T any](v T) {
fmt.Printf("%T: %[1]v\n", []T{v})
}
$ go build main.go
# command-line-arguments
>>> InstInfo for Print[go.shape.int_0]
Typeparam go.shape.int_0
Derived type []go.shape.int_0
>>> Done Instinfo
=== Creating dictionary .dict.Print["".Int]
* Int
- []Int
Main dictionary in main at generic function call: Print - (<node FUNCINST>)(Int(2))
=== Finalizing dictionary .dict.Print["".Int]
=== Finalized dictionary .dict.Print["".Int]
第三部分,子字典区间
所谓子字典sub-dictionaries,也就是当前这个泛型函数或方法又调用其他泛型函数或方法时,这些子调用所需要传递的字典。没错,这也是需要从外层一起生成并传递进来的。我们重写一下上面的例子,加上嵌套的泛型函数调用:
package main
import (
"fmt"
)
func Print[T any](v T) {
fmt.Printf("%T: %[1]v\n", v)
}
func PrintSlice[T any](s []T) {
for i := 0; i < len(s); i++ {
Print(s[i])
}
}
type Int int
func main() {
PrintSlice([]Int{1, 2})
}
$ go build main.go
# command-line-arguments
>>> InstInfo for PrintSlice[go.shape.int_0]
Typeparam go.shape.int_0
Subdictionary at generic function/method call: Print - (<node FUNCINST>)(s[i])
Derived type []go.shape.int_0
Derived type func(go.shape.int_0)
>>> Done Instinfo
=== Creating dictionary .dict.PrintSlice["".Int]
* Int
- []Int
- func(Int)
=== Creating dictionary .dict.Print["".Int]
* Int
>>> InstInfo for Print[go.shape.int_0]
Typeparam go.shape.int_0
>>> Done Instinfo
- Subdict .dict.Print["".Int]
Main dictionary in main at generic function call: PrintSlice - (<node FUNCINST>)([]Int{...})
Sub-dictionary in PrintSlice[go.shape.int_0] at generic function call: Print - (<node FUNCINST>)(s[i])
=== Finalizing dictionary .dict.Print["".Int]
=== Finalized dictionary .dict.Print["".Int]
=== Finalizing dictionary .dict.PrintSlice["".Int]
=== Finalized dictionary .dict.PrintSlice["".Int]
第四部分,itab区间
存在这个区间主要是因为,我们的泛型函数或方法中,可能会存在从类型参数以及其派生类型到一种非空接口类型的转换,或者从一个非空接口到类型参数及其派生类型的类型断言等。这种情况下就需要用到相应itab的地址,这也要从外层准备好并传递给被调用的泛型函数或方法,后者从字典中取出并使用。为了展示这种情况,我们再修改一下上面的例子:
package main
import (
"fmt"
"strconv"
)
func Print[T fmt.Stringer](v T) {
fmt.Printf("%T: %[1]v\n", v.String())
}
func PrintSlice[T fmt.Stringer](s []T) {
for i := 0; i < len(s); i++ {
Print(s[i])
}
}
type Int int
func (n Int) String() string {
return strconv.Itoa(int(n))
}
func main() {
PrintSlice([]Int{1, 2})
}
$ go build main.go
# command-line-arguments
>>> InstInfo for PrintSlice[go.shape.int_0]
Typeparam go.shape.int_0
Subdictionary at generic function/method call: Print - (<node FUNCINST>)(s[i])
Derived type []go.shape.int_0
Derived type func(go.shape.int_0)
>>> Done Instinfo
=== Creating dictionary .dict.PrintSlice["".Int]
* Int
- []Int
- func(Int)
=== Creating dictionary .dict.Print["".Int]
* Int
>>> InstInfo for Print[go.shape.int_0]
Typeparam go.shape.int_0
Optional subdictionary at generic bound call: v.String()
Itab for bound call: v.String
>>> Done Instinfo
- Unused subdict entry
- Subdict .dict.Print["".Int]
Main dictionary in main at generic function call: PrintSlice - (<node FUNCINST>)([]Int{...})
Sub-dictionary in PrintSlice[go.shape.int_0] at generic function call: Print - (<node FUNCINST>)(s[i])
=== Finalizing dictionary .dict.Print["".Int]
+ Itab for (Int,fmt.Stringer)
=== Finalized dictionary .dict.Print["".Int]
=== Finalizing dictionary .dict.PrintSlice["".Int]
=== Finalized dictionary .dict.PrintSlice["".Int]
欢迎扫描下方二维码
或微信搜索:KylinLabs
添加作者微信^~^
接受邀请加入交流群~
文章评论