变化
该草案是一份变化文件,这意味着这些提案需要随着时间的推移而改变。本节记录此提案何时发生更改。
2020/08/21 :推进泛型设计草案
介绍
在上一篇文章中,我向您展示了如何使用泛型草案文档中建议的语法在 Go 中编写泛型函数。我通过使用具体类型、空接口和最后的泛型编写同一函数的不同版本的过程来做到这一点。我还提供了有关为什么需要新语法以及编译器如何通过在调用站点推断类型信息来简化泛型函数调用的信息。
在这篇文章中,我将分享一个基于泛型底层类型声明类型的示例,并进一步分解新语法。我将使用与上一篇文章中类似的进展,以不同的方式声明相同的类型。你可以在这个playground链接找到这篇文章的代码。
具体示例
如果您需要定义一个使用整数的向量怎么办?
清单 1.
16 type vectorInt []int
17
18 func (v vectorInt) last() (int, error) {
19 if len(v) == 0 {
20 return 0, errors.New("empty")
21 }
22 return v[len(v)-1], nil
23 }
69 func main() {
70 fmt.Print("vectorInt : ")
71 vInt := vectorInt{10, -1}
72 i, err := vInt.last()
73 if i < 0 {
74 fmt.Print("negative integer: ")
75 }
76 fmt.Printf("value: %d error: %v\n", i, err)
Output:
vectorInt : negative integer: value: -1 error: <nil>
在清单 1 中,在第 16 行定义了一个名为vectorInt
的类型,其基础类型是int
。在第 18 行,使用值接收器定义了一个名为last
的方法,该接收器返回存储在向量中最高索引位置的整数。如果向量为空,则返回整数的零值并返回错误。
在 main 函数的第 71 行,您可以看到如何构造一个vectorInt
用两个整数值初始化的类型的值。然后在第 72 行执行调用last
并返回整数值 -1。在第 73 行,检查返回值是否为负数,然后在第 76 行显示该值。
如果您的应用程序现在需要使用字符串向量怎么办?
清单 2.
25 type vectorString []string
26
27 func (v vectorString) last() (string, error) {
28 if len(v) == 0 {
29 return "", errors.New("empty")
30 }
31 return v[len(v)-1], nil
32 }
69 func main() {
...
78 fmt.Print("vectorString : ")
79 vStr := vectorString{"A", "B", string([]byte{0xff})}
80 s, err := vStr.last()
81 if !utf8.ValidString(s) {
82 fmt.Print("non-valid string: ")
83 }
84 fmt.Printf("value: %q error: %v\n", s, err)
Output:
vectorString : non-valid string: value: "\xff" error: <nil>
在清单 2 中,在第 25 行定义了一个名为vectorString
的类型,其基础类型是string
. 在第 27 行,该last
方法定义了一个值接收器,该接收器返回存储在向量中最高索引位置的字符串。如果向量为空,则返回字符串的零值和错误。
在 main 函数的第 79 行,您可以看到如何构造一个vectorString
用三个字符串值初始化的类型的值。然后在第 80 行执行调用last
并返回无效字符串。在第 81 行,检查返回值以验证字符串是否完全由有效的 UTF-8 编码符文组成,然后在第 84 行上,该值显示为带引号。
如果您的应用程序现在需要使用浮点向量怎么办?在这一点上,你需要复制的整数执行和更换int
用float64
,改变了变量名。
这两个实现之间唯一真正的区别是每个都只适用于不同的具体类型,并且这种区别需要新的实现。积极的事情是您可以对返回的值执行基于特定类型的验证,因为您直接使用已知且特定的具体类型的值。
空接口
有没有办法定义一个可以处理整数、字符串和浮点数的向量类型?
清单 3.
41 type vectorInterface []interface{}
42
43 func (v vectorInterface) last() (interface{}, error) {
44 if len(v) == 0 {
45 return nil, errors.New("empty")
46 }
47 return v[len(v)-1], nil
48 }
清单 3 显示了一个向量类型的实现,它可以处理整数、字符串、浮点数以及任何类型的数据。再一次,这个和其他两种向量类型之间的唯一区别是底层切片使用空接口。
清单 4.
69 func main() {
...
88 fmt.Print("vectorInterface : ")
89 vItf := vectorInterface{10, "A", 20, "B", 3.14}
90 itf, err := vItf.last()
91 switch v := itf.(type) {
92 case int:
93 if v < 0 {
94 fmt.Print("negative integer: ")
95 }
96 case string:
97 if !utf8.ValidString(v) {
98 fmt.Print("non-valid string: ")
99 }
100 default:
101 fmt.Printf("unknown type %T: ", v)
102 }
103 fmt.Printf("value: %v error: %v\n", itf, err)
Output:
vectorInterface : unknow type float64: value: 3.14 error: <nil>
在清单 4 的第 89 行,您可以看到空接口版本如何允许在构造期间存储多种类型的数据。在这个例子中,我混合了整数、字符串和浮点数的存储。这使得向量更灵活,但也更复杂。
查看第 91 到 102 行。如果我想检测返回的最后一个值是否为负整数,我需要执行类型断言。检测返回的最后一个值是否为有效字符串也是如此。我不再保证向量正在处理特定类型的数据。这使得使用向量更加复杂和容易出错。
泛 型
使用泛型,您可以定义一个向量类型,该类型适用于任何类型的值,其中向量的每个构造都仅限于单一类型的数据。
清单 5.
57 type vector[T any] []T
58
59 func (v vector[T]) last() (T, error) {
60 var zero T
61 if len(v) == 0 {
62 return zero, errors.New("empty")
63 }
64 return v[len(v)-1], nil
65 }
清单 5 显示了一个通用向量类型,它将向量的构造限制为单一类型的数据。如上一篇文章所述,方括号用于声明该类型是在编译时确定的泛型类型T。约束使用any
描述了对类型没有约束。
在第 59 行,last
使用类型的值接收器声明该方法,vector[T]
以表示具有vector
某种类型的底层切片的类型值T
。该方法返回相同类型的值T
。
清单 6.
68 func main() {
...
107 fmt.Print("vector[int] : ")
108 vGenInt := vector[int]{10, -1}
109 i, err = vGenInt.last()
110 if i < 0 {
111 fmt.Print("negative integer: ")
112 }
113 fmt.Printf("value: %d error: %v\n", i, err)
114
115 fmt.Print("vector[string] : ")
116 vGenStr := vector[string]{"A", "B", string([]byte{0xff})}
117 s, err = vGenStr.last()
118 if !utf8.ValidString(s) {
119 fmt.Print("non-valid string: ")
120 }
121 fmt.Printf("value: %q error: %v\n", s, err)
Output:
vector[int] : negative integer: value: -1 error: <nil>
vector[string] : non-valid string: value: "\xff" error: <nil>
清单 6 展示了如何使用第 108 行的int
和第 116行的string
基础类型构造vector
类型的值。此代码的一个重要方面是构造调用。
清单 7.
// Zero Value Construction
var vGenInt vector[int]
var vGenStr vector[string]
// Non-Zero Value Construction
vGenInt := vector[int]{10, -1}
vGenStr := vector[string]{"A", "B", string([]byte{0xff})}
清单 7 显示了在构造过程中如何显式提供要使用的类型T
。在将这些泛型类型构造为零值时,编译器不可能推断出类型。然而,在构造过程中初始化的情况下,编译器可以推断类型,但工具目前没有实现这一点。
现在,如果您的应用程序需要一个浮点向量,您可以在构造时声明它,而无需编写任何新代码。
清单 8.
func main() {
fmt.Print("vector[float64] : ")
vGenFlt := vector[float64]{10.45, -1.32}
f, err := vGenFlt.last()
if f < 0.0 {
fmt.Print("negative float: ")
}
fmt.Printf("value: %f error: %v\n", f, err)
清单 8 展示了如何使用浮点向量。所需要的只是float64
在构造时提供类型以表示T
实现中的类型。检查负值时不需要类型断言,因为该向量中的所有数据都被限制为相同类型的float64
。
零值
规范的一个方面侧重于将泛型类型构造为零值状态。
清单 9.
56 type vector[T any] []T
57
58 func (v vector[T]) last() (T, error) {
59 var zero T
60 if len(v) == 0 {
61 return zero, errors.New("empty")
62 }
63 return v[len(v)-1], nil
64 }
在清单 9 中的第 58 行,关注方法声明last
以及该方法如何返回泛型类型的值T
。现在看第 61 行,函数需要返回一个零值。在这种情况下,您需要为类型返回零值T
。
当前的草案提供了两种解决方案来编写此代码。您在清单 9 中看到的第一个解决方案。在第 59 行,一个名为zero
的变量被构造为其类型的零值状态,然后该变量用于第 61 行的返回。
另一种选择是使用内置函数new
并在语句中引用返回的指针return
。
清单 10.
58 func (v vector[T]) last() (T, error) {
59
60 if len(v) == 0 {
61 return *new(T), errors.New("empty")
62 }
63 return v[len(v)-1], nil
64 }
清单 10 显示了return
对第 61 行语句的更改。这个版本last
使用内置函数new
来构造零值并取消引用返回的指针以满足返回类型T
。
注意:你可能会想为什么不使用T{}
来执行零值构造呢?问题是这种语法不适用于所有类型,例如标量类型(int、string、bool)。所以这不是一个选择。
在某些时候,社区需要决定哪个选项应该被视为最佳实践。在此第一个版本之前,可能会提供其他选项。
结论
阅读本文后,您应该对 Go 中基于泛型基础类型的用户定义类型的基本语法有了更好的了解。在声明泛型类型时,您应该看到需要方括号来形成泛型类型列表。在构造泛型类型时,您了解了必须如何显式传递类型信息,尽管草稿确实支持为非零值构造调用推断类型的功能。
在下一篇文章中,我将探讨如何使用泛型来定义具有结构的用户定义类型。我还将通过此示例探讨值和指针语义。如果您等不及了,我建议您查看这些博客文章所基于的代码存储库,并亲自进行实验。如果您有任何疑问,请通过电子邮件,Slack或Twitter与我联系。
原文链接:https://www.ardanlabs.com/blog/2020/08/generics-02-underlying-types.html