介绍
在上一篇文章中,我向您展示了如何基于底层类型声明用户定义的类型。我通过使用具体类型、空接口和最后的泛型编写同一类型的不同版本的过程来做到这一点。我还提供了有关编译器在零值构造期间推断泛型类型替换的能力如何受到限制的信息,但它可以在初始化构造中进行。
在这篇文章中,我将分享一个示例,说明如何基于具有通用字段的结构声明用户定义类型。我还将讨论使用值或指针类型声明将如何改变语义。这篇文章的代码可以在这个playground链接中找到。
具体例子
如果你现在想在 Go 中编写一个链表,你必须为你想要管理的每种新数据类型编写完整的链表实现。使用新的泛型语法,您现在可以只有一个实现。
清单 1
14 type node[T any] struct {
15 Data T
16 next *node[T]
17 prev *node[T]
18 }
在清单 1 中,声明了一个结构类型,它表示链表的一个节点。每个都node
包含由列表存储和管理的单个数据。标识符T
被定义为泛型类型(稍后确定),这要归功于第 14 行附加到类型名称any
的方括号。在相同的方括号内使用预先声明的标识符(any类型约束)告诉编译器没有约束意味着T
可以成为任何类型。它指出T
可以替代“any”具体类型。
注意:泛型类型声明需要一个约束作为语法的一部分。
T
声明类型后,第15 行的Data
字段现在可以定义为T
类型的字段。第16 行和第 17 行声明的next
和prev
字段需要指向node[T]
相同类型的 指针。这些分别是指向链表中下一个和上一个节点的指针。为了建立这种联系,字段被声明为指向通过使用方括号node
绑定类型T
的指针。
清单 2
20 type list[T any] struct {
21 first *node[T]
22 last *node[T]
23 }
清单 2 显示了第二个名为list
的结构类型,它通过指向列表中的第一个和最后一个节点来表示节点集合。这些字段需要指向node[T]
,就像清单 1 中的next
和prev
字段一样。
在第 20 行,标识符T
被定义为可以替代“any”具体类型的通用类型(稍后确定)。然后在第 21 和 22 行,first
和last
字段被声明为使用方括号语法指向node
某种类型T
的指针。
清单 3
25 func (l *list[T]) add(data T) *node[T] {
26 n := node[T]{
27 Data: data,
28 prev: l.last,
29 }
30 if l.first == nil {
31 l.first = &n
32 l.last = &n
33 return &n
34 }
35 l.last.next = &n
36 l.last = &n
37 return &n
38 }
清单 3 显示了add
为该list
类型命名的方法的实现。不需要正式的泛型类型列表声明(与函数一样),因为方法绑定到list
通过接收器。该add
方法的接收器被声明为一个list[T]
指针类型,并返回被声明为一个指向一个node[T]
的指针。
第 30 行到第 37 行的代码将始终相同,无论列表中存储的是什么类型的数据,因为这只是指针操作。只有第26 行的新构造node
会受到将要管理的数据类型的影响。由于泛型,node
节点的构造可以绑定到T
随后在编译时被替换。
如果没有泛型,则需要复制整个方法,因为第 26 行需要在编译之前硬编码为已知的声明类型。由于需要针对不同数据类型更改的代码量(对于整个列表实现)非常少,因此能够声明node
和list
管理某种类型的数据T
可以降低代码重复和维护的成本。
应用
声明了node
和list
类型后,我现在可以编写一个小型应用程序,该应用程序构造两个列表,在其中添加和显示数据。
清单 4
44 type user struct {
45 name string
46 }
50 func main() {
51
52 // Store values of type user into the list.
53 var lv list[user]
54 n1 := lv.add(user{"bill"})
55 n2 := lv.add(user{"ale"})
56 fmt.Println(n1.Data, n2.Data)
57
58 // Store pointers of type user into the list.
59 var lp list[*user]
60 n3 := lp.add(&user{"bill"})
61 n4 := lp.add(&user{"ale"})
62 fmt.Println(n3.Data, n4.Data)
63 }
Output
{bill} {ale}
&{bill} &{ale}
清单 4 显示了这个小应用程序。在第 44 行,声明了一个user
类型名称,然后在第 53 行,定义了变量lv将构造list[user]
的零值状态以管理类型user
的值。在第 59 行,定义了变量lp是第二个被构造的零值状态,这个列表管理指向类型user
值的指针。这两个列表之间的唯一区别是一个管理类型user
的值,另一个管理类型user
的指针。
由于在第 53 行user
的构造过程中明确指定了类型list
,因此该add
方法依次接受user
第 54 行和第 55 行的类型值。由于在第 59 行user
的构造过程中明确指定了类型指针,因此list
在第 59行add
调用该方法60 和 61 接受类型的指针user
。
您可以在程序的输出中看到,Data
各个列表中节点的字段与构造中使用的数据语义相匹配。
强制指针语义
如果我选择更改代码并将Data
字段声明为类型指针T
并且add
方法也接受类型指针,会发生什么情况T
?
清单 5
14 type node[T any] struct {
15 Data *T <- CHANGED CODE
16 next *node[T]
17 prev *node[T]
18 }
25 func (l *list[T]) add(data *T) *node[T] { <- CHANGED CODE
26 n := node[T]{
27 Data: data,
28 prev: l.last,
29 }
在清单 5 中,我更改了第 15 行和第 25 行的代码,以将Data
字段声明为类型指针T
和add
接受类型指针的方法T
。
当我构建程序时会发生什么?
清单 6
50 func main() {
51
52 // Store values of type user into the list.
53 var lv list[user]
54 n1 := lv.add(user{"bill"}) <- NOW MUST PASS A POINTER OF TYPE USER
55 n2 := lv.add(user{"ale"}) <- NOW MUST PASS A POINTER OF TYPE USER
56 fmt.Println(n1.Data, n2.Data)
57
58 // Store pointers of type user into the list.
59 var lp list[*user]
60 n3 := lp.add(&user{"bill"}) <- NOW MUST PASS A POINTER TO A POINTER OF TYPE USER
61 n4 := lp.add(&user{"ale"}) <- NOW MUST PASS A POINTER TO A POINTER OF TYPE USER
62 fmt.Println(n3.Data, n4.Data)
63 }
Output
type checking failed for main
prog.go2:54:15: cannot use (user literal) (value of type user) as *user value in argument
prog.go2:55:15: cannot use (user literal) (value of type user) as *user value in argument
prog.go2:60:15: cannot use &(user literal) (value of type *user) as **user value in argument
prog.go2:61:15: cannot use &(user literal) (value of type *user) as **user value in argument
清单 6 显示了这个问题。您可以看到编译器希望代码user
在第 54 行和第 55 行传递一个类型指针,并user
在第 60 和 61 行传递一个指向类型指针的指针。
在构造调用上显式传递给编译器的类型信息不再与add
方法要求的输入相匹配。API 不再符合在构造时声明的具体数据。在我看来,这可能会在 API 的工作方式以及底层使用的数据语义方面造成混淆。API 中的这种混淆会导致误用和错误。
结论
阅读这篇文章后,您应该对 Go 中基于结构类型的用户定义类型的泛型语法有更好的理解。您了解了如何声明泛型类型的字段以及如何声明可以指向其他结构类型值的字段。您还看到了如何声明一个可以接受和返回相同泛型类型的值的方法。最后,您将了解在泛型类型的字段和方法声明上强制指针语义如何会造成使用 API 的混乱和误用。
在下一篇文章中,我将探讨当泛型函数需要泛型类型的值来表现行为时,如何声明泛型类型的行为约束。如果您迫不及待,我建议您查看这些博客文章所基于的代码仓库并自己进行实验。如果您有任何问题,请通过电子邮件、Slack 或 Twitter 与我联系。
原文链接:https://www.ardanlabs.com/blog/2020/09/generics-03-struct-types-and-data-semantics.html