16.2.2 堆栈库

现在我们已经完成了 "管理 "任务,让我们开始研究我们的堆栈库。首先,让我们定义一个通用的堆栈类型。

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测试包

最后更新于