现在我们已经完成了 "管理 "任务,让我们开始研究我们的堆栈库。首先,让我们定义一个通用的堆栈类型。
stack-demo/stack/stack.go
package stack // 按照惯例,.../stack-demo/stack中的源文件的包名被选为stack,即最后一个路径段。
type Pusher[E any] interface { // 这个通用的Pusher[E any]接口包括一个通用函数Push(item E)。用er后缀来命名一个单一方法的接口也是一种惯例,例如本例中Pusher代表Push。
Push(item E)
}
type Popper[E any] interface { // 在Go中,与其他一些语言不同的是,使用许多小的接口,而不是使用有许多方法的(更完整的)大接口,是一种习惯。这个通用的Popper接口包括一个方法PopOrError() (E, error)
PopOrError() (E, error)
}
type Stack[E any] interface { // 我们不必定义Stack类型。为了说明问题,我们在这个例子中使用接口嵌入语法,将Stack定义为Pusher和Popper的组合。事实上,在这个简单的例子中,我们甚至不需要明确地定义Pusher和Popper接口。见下文
Pusher[E]
Popper[E]
}
如前所述,堆栈类型包括push和pop方法。但是,这并不是定义。在定义堆栈时,后进先出的属性是必不可少的。但是请注意,没有简单的方法可以使用接口等语言结构来指定这一要求,不仅仅是在Go中,在其他任何编程语言中也是如此。
现在让我们实现一个链表。
stack-demo/stack/node.go
package stack
type node[E any] struct { // 一个名为node的(非导出的)通用结构类型
item E
next *node[E] // 一个指针类型的字段,指向 "下一个节点",如果有的话
}
stack-demo/stack/linked-list.go
package stack
type list[E any] struct { // 一般来说,不需要为链表建立一个单独的类型。人们可以只用节点来表示一个链表
head *node[E]
}
func newList[E any]() *list[E] { // 返回一个类型的实例的函数通常被称为 "构造函数"。它们通常被命名为 "New "或一些以 "New "开头的短语,如NewRocket(例如,对于一个名为Rocket的类型)。
return &list[E]{
head: nil, // 指针类型(例如,本例中的*node[E])的零值是nil。我们用nil来表示 "没有头",或者更广泛地表示 "没有节点"。
}
}
func (l *list[E]) addToHead(n *node[E]) { // 一个(单)链表类型支持两个主要的操作,除此之外。将一个节点添加到列表的头部,以及删除头部节点并重设头部,在本例中分别称为 addToHead 和 removeHead。
n.next = l.head
l.head = n
}
func (l *list[E]) removeHead() (n *node[E]) {
n = l.head
if n == nil { // 当没有头部节点需要移除时,我们只是返回一个nil值。这只是一个API设计。我们本可以使用一个额外的错误接口类型的返回值,比如说,来表示空列表的情况。
return
}
l.head = l.head.next
return
}
现在让我们创建一个 "实现堆栈接口 "的类型,使用我们刚刚创建的linkedList数据结构。
stack-demo/stack/liststack.go
package stack
import (
"errors"
"fmt"
)
type ListStack[E any] struct { // 我们把这个通用类型称为ListStack,一个任意的名字
*list[E]
}
func New[E any]() *ListStack[E] { // 一个构造函数,用于说明问题。对于简单的类型,通常不需要构造函数。我们可以直接使用复合字面的语法来创建一个新的值。
s := ListStack[E]{
list: newList[E](),
}
return &s
}
func (s *ListStack[E]) Push(item E) { // 注意 "范型方法 "的语法。类型参数(如本例中的E)与方法接收器(如本例中*ListStack[E]类型的s)相关。
n := node[E]{
item: item,
}
s.addToHead(&n)
}
func (s *ListStack[E]) PopOrError()
(E, error) { // 与PopOrError方法相同。注意,当堆栈为空时,Pusher的PopOrError方法会返回一个 "错误"。
n := s.removeHead()
if n == nil {
var e E // 需要注意的是,Go并没有为一个类型创建默认值或零值的语法。这个var声明语句用E类型(无论具体类型是什么)的零值初始化了变量e。我们只是在返回错误的时候用这个e变量做一个占位符。
return e, errors.New("Empty list")
}
return n.item, nil
}
请注意,这段代码中没有提到Pusher或Popper接口。Go在这方面是比较独特的。正如前面所解释的,一个类型通过实现相关的方法,隐含地实现了一个接口。
在这个例子中,我们甚至不需要定义显式的Pusher和Popper接口来定义一个实现这些(可能不存在的、隐式的)接口的类型。当我们使用实现接口的类型时,例如作为一个函数参数等,就需要显式的接口定义。举例来说。
func PushToStack[E any](s Pusher[E], items ...E) { // 这里需要一个明确的接口。注意,我们只是用Pusher[E]而不是Stack[E]...
for _, e := range items {
s.Push(e) // 因为push方法是我们实现这个函数PushToStack所需要的全部内容。注意,在这个例子中,类型参数可以被推断出来,我们不必把它写成s.Push[E](e)
}
}
顺便提一下,这个函数也可以这样声明:
func PushToStack[E any, S Pusher[E]](s S, items ...E) {
// ...
}
尽管这两个函数的声明(明显)不同,但它们还是大致相当的。对于基本的接口来说,这通常是正确的。它们可以作为通用的函数类型参数约束,也可以简单地作为函数参数使用,其效果大致相同。这两种用法真的等同吗?有什么理由让你更喜欢一种形式而不是另一种形式吗?如果是的话,在什么情况下?我们将把这个问题留给读者,让他们在学习/编程Go的过程中去思考。(提示:没有 "正确 "的答案。)
另一个练习,如果你愿意,就是为这个ListStack[E]类型写一个单元测试。如果你是Go中单元测试的新手,你可以参考官方文档,Go测试包。