Skip to content

《Go Cookbook CN》系列 14:常见队列/栈/集合/链表/堆/图全实现

约 5689 字大约 19 分钟

《Go Cookbook CN》系列Go语言

2026-04-02

本文聚焦Go语言中未直接提供的常见数据结构,系统讲解队列、栈、集合、链表、堆、图的核心概念,并基于Go的切片、映射等基础结构从零实现这些数据结构的核心方法。通过本文学习,你将掌握各数据结构的设计逻辑与落地技巧,解决日常开发中各类数据存储与处理的场景需求。

【本篇核心收获】

  • 掌握队列、栈、集合、链表、堆、图的核心定义与典型应用场景
  • 能独立实现Go版队列(入队/出队/Peek)、栈(入栈/出栈/Peek)的完整逻辑
  • 理解集合的数学运算(并集/交集/差集/子集)并完成Go实现
  • 学会单向链表的增删查、二叉堆的push/pop重排、加权图的节点/边管理
  • 明确Go自定义数据结构的并发安全优化方向

14.0 概述

Go语言的基础数据结构仅包含结构体、数组、切片和映射,日常开发中常用的队列、栈、集合等数据结构未直接提供,但基于Go的基础结构可轻松实现。

本文覆盖的自定义数据结构包括:队列 (Queue)、栈 (Stack)、集合 (Set)、链表 (LinkedList)、堆 (Heap)、图 (Graph),所有实现均基于Go的数组/切片/映射,需注意:本文实现的所有数据结构均非并发安全(基础结构本身非并发安全),若需避免争用条件,可通过sync.RWMutex加锁实现。

术语说明

  • 列表 (list):线性、有序的条目序列,Go的数组/切片均属于列表,条目称为“元素”;
  • 图 (Graph):一组相互连接的条目,条目称为“节点 (node)”,节点间连接称为“边 (edge)”。

14.1 队列 (Queue)

14.1.1 核心概念

队列是先进先出 (FIFO) 的有序列表,新元素从尾部加入(入队),元素从头部移除(出队),典型应用场景包括缓冲区、排队系统。

队列核心方法:

  • 入队 (Enqueue):添加元素到队列尾部;
  • 出队 (Dequeue):移除并返回队列头部元素;
  • 窥视 (Peek):获取头部元素但不移除;
  • 大小 (Size):获取队列元素数量;
  • 是否为空 (IsEmpty):检查队列是否无元素。

队列结构示意如下: 图1:队列结构示意图

14.1.2 Go实现方案

基于切片封装结构体,通过切片的追加/重新切片实现队列核心方法。

1. 定义队列结构体

type Queue struct { // 队列 (Queue) 结构体
    elements []any // 切片 (slice),元素类型为任意类型
}

2. 核心方法实现

入队 (Enqueue)

将元素追加到切片尾部(队列尾部):

func (q *Queue) Enqueue(el any) { // 入队 (Enqueue) 方法
    q.elements = append(q.elements, el)
}
出队 (Dequeue)

获取切片索引0的元素(队列头部),并重新切片移除该元素,空队列返回错误:

func (q *Queue) Dequeue() (el any, err error) {
    if q.IsEmpty() { // 检查队列是否为空
        err = errors.New("empty queue")
        return // 如果队列为空,则返回错误
    }
    el = q.elements[0] // 获取索引为 0 的元素
    q.elements = q.elements[1:] // 重新切片,将索引为 0 的元素从队列中删除
    return
}
窥视 (Peek)

返回队列头部元素但不移除,空队列返回错误:

func (q *Queue) Peek() (el any, err error) {
    if q.IsEmpty() { // 检查队列是否为空
        err = errors.New("empty queue")
        return // 如果队列为空,则返回错误
    }
    el = q.elements[0] // 获取索引为 0 的元素
    return
}
辅助方法(Size/IsEmpty)
func (q *Queue) IsEmpty() bool {
    return q.Size() == 0 // 如果队列大小为 0,则返回 true;否则,返回 false
}

func (q *Queue) Size() int {
    return len(q.elements) // 返回队列中元素数量
}

14.1.3 模块小结

队列基于切片实现,核心是利用切片的追加(入队)和重新切片(出队)操作,需注意空队列的错误处理,确保方法鲁棒性。

14.2 创建栈 (Stack)

14.2.1 核心概念

栈是后进先出 (LIFO) 的有序列表,新元素从顶部加入(入栈),元素从顶部移除(出栈),典型应用场景包括内存管理、递归函数、表达式求值、回溯。

栈核心方法:

  • 入栈 (Push):添加元素到栈顶;
  • 出栈 (Pop):移除并返回栈顶元素;
  • 窥视 (Peek):获取栈顶元素但不移除;
  • 大小 (Size):获取栈元素数量;
  • 是否为空 (IsEmpty):检查栈是否无元素。

栈结构示意如下: 图2:栈结构示意图

14.2.2 Go实现方案

基于切片封装结构体,将切片尾部视为栈顶,通过追加/重新切片实现核心方法。

1. 定义栈结构体

type Stack struct { // 栈 (Stack) 结构体
    elements []any // 切片 (slice),元素类型为任意类型
}

2. 核心方法实现

入栈 (Push)

将元素追加到切片尾部(栈顶):

func (s *Stack) Push(el any) { // 入栈方法
    s.elements = append(s.elements, el) // 将元素追加到切片末尾,将切片末尾视为栈顶
}
出栈 (Pop)

获取切片最后一个元素(栈顶),并重新切片移除该元素,空栈返回错误:

func (s *Stack) Pop() (el any, err error) { // 出栈方法
    if s.IsEmpty() { // 检查栈结构是否为空
        err = errors.New("empty stack") // 如果为空,则返回错误
        return
    }
    el = s.elements[len(s.elements)-1]
    s.elements = s.elements[:len(s.elements)-1] // 移除切片尾部的元素,即元素出栈
    return
}
窥视 (Peek)

返回栈顶元素但不移除,空栈返回错误:

func (s *Stack) Peek() (el any, err error) { // 窥视方法
    if s.IsEmpty() { // 检查栈结构是否为空
        err = errors.New("empty stack")
        return
    }
    el = s.elements[len(s.elements)-1] // 返回切片的最后一个元素,即栈顶的元素
    return
}
辅助方法(Size/IsEmpty)
func (s *Stack) IsEmpty() bool {
    return s.Size() == 0
}

func (s *Stack) Size() int {
    return len(s.elements)
}

14.2.3 模块小结

栈的实现逻辑与队列类似(均基于切片),核心差异在于栈以切片尾部为操作端(LIFO),队列以切片头部为出队端(FIFO),需注意空栈的错误处理。

14.3 创建集合 (Set)

14.3.1 核心概念

集合是无重复元素的无序数据结构,支持数学中的集合运算,典型应用场景包括去重、元素归属判断、集合运算(并集/交集等)。

集合核心方法/运算:

类型方法/运算说明
基础方法添加 (Add)向集合添加新元素
基础方法移除 (Remove)从集合中移除指定元素
基础方法是否为空 (IsEmpty)检查集合是否无元素
基础方法大小 (Size)获取集合元素数量
集合运算具有 (Has)检查元素是否为集合成员
集合运算并集 (Union)多个集合的所有元素(去重)
集合运算交集 (Intersection)多个集合的共有元素
集合运算差集 (Difference)属于集合A但不属于集合B的元素
集合运算子集 (IsSubset)检查集合B的所有元素是否都在集合A中(B是A的子集)

14.3.2 Go实现方案

基于映射(Map)封装结构体,利用映射键的唯一性实现集合去重,键为集合元素,值为布尔类型(无实际意义)。

1. 定义集合结构体及初始化函数

映射使用前需初始化,因此需单独定义创建集合的函数:

type Set struct {
    elements map[any]bool
}

func NewSet() Set { // 创建新集合的函数
    return Set{elements: make(map[any]bool)}
}

2. 基础方法实现

添加元素 (Add)

向映射中添加键(值设为true):

func (s *Set) Add(el any) { // 添加元素到集合的方法
    s.elements[el] = true // 将元素添加到映射中,无需关心映射的值,简单地设为 true 即可
}
移除元素 (Remove)

从映射中删除指定键:

func (s *Set) Remove(el any) { // 从集合中删除一个元素
    delete(s.elements, el) // 实际上就是从内部映射中删除一个条目
}
辅助方法(IsEmpty/Size/List)
func (s *Set) IsEmpty() bool {
    return s.Size() == 0 // 如果内部映射大小为 0,则表示集合为空
}

func (s *Set) Size() int {
    return len(s.elements) // 内部映射的元素数量就是集合的大小
}

func (s *Set) List() (list []any) {
    for k := range s.elements { // 遍历内部映射的键
        list = append(list, k) // 将键追加到切片 list 中即可
    }
    return
}

3. 集合运算实现

成员判断 (Has)

检查映射中是否存在指定键(核心基础运算,其他运算依赖此方法):

func (s Set) Has(el any) (ok bool) {
    _, ok = s.elements[el] // 检查内部映射中是否存在键名 el
    return
}
并集 (Union)

接收多个集合,返回包含所有集合元素的新集合(基于第一个集合的副本,避免修改原集合):

func (s *Set) Copy() (u Set) {
    u = NewSet()
    for k := range s.elements {
        u.Add(k)
    }
    return
}

func Union(sets ...Set) (u Set) {
    u = sets[0].Copy() // 为第一个集合创建副本,将该副本作为种子集合。
    for _, set := range sets[1:len(sets)] { // 遍历其余的集合
        for k := range set.elements { // 遍历其余的集合中的元素,并将得到的元素加入到种子集合中
            u.Add(k)
        }
    }
    return
}
交集 (Intersection)

接收多个集合,返回所有集合的共有元素(基于第一个集合的副本,移除不符合条件的元素):

func Intersect(sets ...Set) (i Set) {
    i = sets[0].Copy() // 为第一个集合创建副本,将该副本作为种子集合。
    for k := range i.elements { // 遍历种子集合中的元素
        for _, set := range sets[1:len(sets)] { // 遍历其余的集合
            if !set.Has(k) { // 如果种子集合中的元素不在其余的某个集合中,则从种子集合中移除该元素
                i.Remove(k)
            }
        }
    }
    return
}
差集 (Difference)

返回“属于当前集合但不属于指定集合”的元素:

func (s Set) Difference(t Set) Set {
    u := s.Copy()
    for k := range t.elements { // 遍历给定集合中的元素
        if u.Has(k) { // 如果给定集合中的元素也在集合 s 中,则从 s 中删除该元素
            u.Remove(k) // 从集合 s 中移除该元素
        }
    }
    return u // 返回最终的集合 s,即差集
}
子集判断 (IsSubset)

检查当前集合是否是指定集合的子集:

func (s Set) IsSubset(t Set) bool {
    for k := range s.elements {
        // 遍历集合 s 中的元素
        if !t.Has(k) {
            // 如果集合 s 中的任一元素不在集合 t 中,则返回 false
            return false // s 不是 t 的子集
        }
    }
    return true // 如果集合 s 中的所有元素都在集合 t 中,则返回 true
}

14.3.3 模块小结

集合基于映射实现,核心利用映射键的唯一性保证无重复元素;集合运算需基于副本操作,避免修改原集合,Has方法是所有集合运算的基础。

14.4 创建链表 (LinkedList)

14.4.1 核心概念

链表是线性元素集合,每个元素(节点)包含数据和指向下一节点的指针,元素无需连续存储,插入/删除效率高(仅需修改指针),随机访问效率低(需遍历)。

本文实现单向链表,核心方法:

  • 添加 (Add):在链表头部添加新元素;
  • 插入 (Insert):在指定元素后插入新元素;
  • 删除 (Delete):删除指定元素;
  • 查找 (Find):查找并返回指定元素;
  • 辅助方法:Size/IsEmpty/List(转换为切片)。

链表结构示意如下: 图3:单向链表结构示意图

14.4.2 Go实现方案

定义两个结构体:Element(节点,包含值和下一节点指针)、LinkedList(链表,包含头节点指针和大小),利用泛型约束节点值支持排序。

1. 定义链表相关结构体

type Element[T constraints.Ordered] struct {
    value T             // 值字段,类型 T,必须满足 constraints.Ordered 约束(支持排序)
    next  *Element[T]   // 指向链表中下一个元素的指针
}

type LinkedList[T constraints.Ordered] struct {
    head *Element[T]    // 指向链表的头部(第一个元素)
    size int            // 链表的大小
}

2. 核心方法实现

添加元素 (Add)

在链表头部添加新节点:

// Add 将新元素添加到链表的头部
// 参数 el 是要添加到链表的新元素
func (l *LinkedList[T]) Add(el *Element[T]) {
    // 如果链表为空(即头部为 nil),则新元素直接成为链表的头部
    if l.head == nil {
        l.head = el
    } else {
        // 如果链表不为空,将新元素的 next 指针指向当前的头部元素
        el.next = l.head
        // 然后,将链表的头部更新为新元素
        l.head = el
    }
    // 链表的 size(大小)增加 1
    l.size++
}
插入元素 (Insert)

在指定标记元素后插入新节点,未找到标记返回错误:

// Insert 在链表中插入一个新元素,该元素被插入到值为 marker 的元素之后。
// 如果找到 marker,则新元素 el 被插入到 marker 后面,并返回 nil。如果没有找到 marker,则返回一个错误。
func (l *LinkedList[T]) Insert(el *Element[T], marker T) error {
    // 从链表 l 头部开始遍历,直到链表的末尾
    for current := l.head; current != nil; current = current.next {
        if current.value == marker { // 检查当前节点的值是否为 marker
            // 如果找到了 marker,设置新元素 el 的下一个节点为当前节点 current 的下一个节点
            el.next = current.next
            // 将当前节点 current 的下一个节点设置为新元素 el,完成插入操作
            current.next = el
            // 增加链表的元素数量
            l.size++
            // 插入成功,返回 nil
            return nil
        }
    }
    // 如果遍历完整个链表都没有找到 marker,返回一个错误
    return errors.New("element not found")
}

插入操作示意如下: 图4:链表插入元素示意图

删除元素 (Delete)

删除指定节点,未找到返回错误(需跟踪前一节点):

// Delete 从链表中删除指定的元素 el。如果找到并删除了元素,则返回 nil;如果元素不在链表中,则返回一个错误。
func (l *LinkedList[T]) Delete(el *Element[T]) error {
    // 初始化 prev 和 current 指针,都指向链表的头部
    var prev *Element[T]
    current := l.head
    for current != nil { // 遍历链表直到 current 为 nil
        if current.value == el.value { // 检查当前节点的值是否与要删除的元素 el 的值相等
            if current == l.head {
                l.head = current.next // 如果当前节点是头部节点,更新头部节点为当前节点的下一个节点
            } else {
                prev.next = current.next // 如果当前节点不是头部节点,将前一个节点的 next 指针指向当前节点的下一个节点
            }
            l.size-- // 减少链表的元素数量
            return nil // 删除成功,返回 nil
        }
        prev = current // 更新 prev 指针为当前节点
        current = current.next // 移动 current 指针到下一个节点
    }
    return errors.New("element not found") // 如果遍历完整个链表都没有找到元素,返回一个错误
}

删除操作示意如下: 图5:链表删除元素示意图

查找元素 (Find)

遍历链表查找指定值的节点,未找到返回错误:

func (l *LinkedList[T]) Find(value T) (el *Element[T], err error) {
    // 从链表 l 头部开始遍历,直到链表的末尾
    for current := l.head; current != nil; current = current.next {
        if current.value == value {
            el = current // 如果当前节点的值是否为要查找的值 value,则返回该节点
            return
        }
    }
    if el == nil {
        // 如果遍历完整个链表都没有找到元素,返回一个错误
        err = errors.New("element not found")
    }
    return
}

3. 辅助方法实现

转换为切片 (List)

将链表节点存入切片(链表与切片相互独立,修改互不影响):

func (l *LinkedList[T]) List() (list []*Element[T]) {
    if l.head == nil { // 如果链表为空,返回一个空切片
        return []*Element[T]{}
    }
    // 遍历链表,将每个元素添加到切片中,并返回该切片
    for current := l.head; current != nil; current = current.next {
        list = append(list, current)
    }
    return
}
辅助方法(Size/IsEmpty)
func (l *LinkedList[T]) IsEmpty() bool {
    return l.size == 0
}

func (l *LinkedList[T]) Size() int {
    return l.size
}

14.4.3 模块小结

单向链表通过两个结构体实现,核心是指针的修改(添加/插入/删除),需注意遍历过程中跟踪前一节点(删除操作),以及空链表/未找到元素的错误处理;Go标准库container包提供双向链表实现,可作为扩展参考。

14.5 创建堆 (Heap)

14.5.1 核心概念

堆是满足堆属性的树状数据结构,分为两种类型:

  • 最小堆:父节点键 ≤ 所有子节点键,根节点为最小元素;
  • 最大堆:父节点键 ≥ 所有子节点键,根节点为最大元素。

堆典型应用为优先队列,核心方法:

  • 入堆 (Push):添加节点到堆,并重排恢复堆属性;
  • 出堆 (Pop):移除并返回堆顶节点,并重排恢复堆属性。

本文实现二叉最大堆(每个节点最多两个子节点),基于切片存储节点,节点索引关系:

  • 左子节点索引 = 2 * 父节点索引 + 1;
  • 右子节点索引 = 2 * 父节点索引 + 2;
  • 父节点索引 = (子节点索引 - 1) / 2(整数除法)。

最大堆结构示意如下: 图6:最大堆结构示意图

最大堆的切片布局示意如下: 图7:最大堆切片布局示意图

14.5.2 Go实现方案

基于切片封装结构体,入堆/出堆后通过递归/循环重排堆结构,保证堆属性。

1. 定义堆结构体及索引工具函数

type Heap[T constraints.Ordered] struct {
    nodes []T // 存储堆中节点的切片
}

func parent(i int) int {
    return (i - 1) / 2
}

func leftChild(i int) int {
    return (2 * i) + 1
}

func rightChild(i int) int {
    return (2 * i) + 2
}

2. 核心方法实现

交换节点(辅助函数)
func (h *Heap[T]) swap(i, j int) {
    h.nodes[i], h.nodes[j] = h.nodes[j], h.nodes[i]
}
入堆 (Push)

将节点添加到切片尾部,然后向上冒泡重排(与父节点比较,最大堆则大于父节点则交换):

func (h *Heap[T]) Push(ele T) {
    h.nodes = append(h.nodes, ele) // 将元素添加到堆的末尾(顶部)
    i := len(h.nodes) - 1

    for i > 0 && h.nodes[i] > h.nodes[parent(i)] { // 比较新节点与其父节点(对于最大堆)
        h.swap(i, parent(i)) // 交换节点,将新节点上移到正确的位置
        i = parent(i)
    }
}
重排堆(辅助递归函数)

出堆后,将最后一个节点移到堆顶,然后向下冒泡重排:

func (h *Heap[T]) rearrange(i int) {
    largest := i
    left, right, size := leftChild(i), rightChild(i), len(h.nodes)
    if left < size && h.nodes[left] > h.nodes[largest] {
        largest = left
    }
    if right < size && h.nodes[right] > h.nodes[largest] {
        largest = right
    }
    if largest != i {
        h.swap(i, largest)
        h.rearrange(largest)
    }
}
出堆 (Pop)

移除并返回堆顶节点(切片索引0),将最后一个节点移到堆顶,然后重排:

func (h *Heap[T]) Pop() (ele T) {
    if len(h.nodes) == 0 {
        return ele
    }

    ele = h.nodes[0]
    h.nodes[0] = h.nodes[len(h.nodes)-1]
    h.nodes = h.nodes[:len(h.nodes)-1]
    if len(h.nodes) > 0 {
        h.rearrange(0)
    }
    return
}

3. 辅助方法实现

func (h *Heap[T]) Size() int {
    return len(h.nodes)
}

func (h *Heap[T]) IsEmpty() bool {
    return h.Size() == 0
}

14.5.3 模块小结

二叉堆基于切片实现,核心是入堆/出堆后的重排逻辑(向上/向下冒泡),保证堆属性;Go标准库container/heap包提供通用堆操作接口,可基于该接口实现自定义堆。

14.6 创建图 (Graph)

14.6.1 核心概念

图是由节点和边组成的非线性数据结构,本文实现加权无向图(边有权重,节点间边双向),核心方法:

  • 添加节点 (AddNode):向图中添加节点;
  • 添加边 (AddEdge):添加连接两个节点的加权边;
  • 删除节点 (RemoveNode):从图中删除节点;
  • 删除边 (RemoveEdge):从图中删除边。

加权图结构示意如下: 图8:加权图结构示意图

14.6.2 Go实现方案

定义三个结构体:Node(节点,含名称)、Edge(边,含目标节点指针和权重)、Graph(图,含节点切片和边映射),边映射的键为节点名,值为该节点关联的边切片。

1. 定义图相关结构体及初始化函数

type Node struct { // 表示图中的节点
    name string // 节点名称
}

type Edge struct { // 表示图中连接节点的边
    node   *Node // 指向节点的指针
    weight int   // 边的权重
}

type Graph struct { // 表示一个加权图
    Nodes []*Node        // 图中包含的节点切片,元素类型为 *Node 指针
    Edges map[string][]*Edge // 用映射表示图中的边,键为节点名,值为切片,元素类型为 *Edge 指针
}

func NewGraph() *Graph { // 定义一个函数,用于创建 Graph 结构体。该函数内部将执行
    // Edges 字段的初始化
    return &Graph{
        Edges: make(map[string][]*Edge), // Edges 字段是映射类型,在使用之前需要先初始化
    }
}

2. 核心方法实现

添加节点 (AddNode)

将节点添加到图的节点切片:

func (g *Graph) AddNode(n *Node) {
    g.Nodes = append(g.Nodes, n) // 将新节点添加到图中的节点切片中
}
添加边 (AddEdge)

为两个节点添加双向加权边(无向图):

func (g *Graph) AddEdge(n1, n2 *Node, weight int) { // 添加边,需要三个参数,即两个节点和一个权重
    g.Edges[n1.name] = append(g.Edges[n1.name], &Edge{n2, weight}) // 将边添加到映射 Edges 中,并与节点 n1 相关联
    g.Edges[n2.name] = append(g.Edges[n2.name], &Edge{n1, weight}) // 将边添加到映射 Edges 中,并与节点 n2 相关联
}

14.6.3 模块小结

加权无向图通过“节点切片+边映射”实现,核心是边的双向关联(无向图特性);需注意边映射的初始化,避免空指针错误。

【本篇核心知识点速记】

  1. 基础实现逻辑:队列/栈基于切片,集合基于映射,链表基于节点指针,堆基于切片+重排,图基于节点切片+边映射;
  2. 队列(FIFO):切片头部为出队端,尾部为入队端,核心方法Enqueue/Dequeue/Peek;
  3. 栈(LIFO):切片尾部为栈顶,核心方法Push/Pop/Peek;
  4. 集合:利用映射键唯一性去重,支持Add/Remove/Has及并集/交集/差集/子集运算;
  5. 单向链表:通过Element(节点)+LinkedList(链表)实现,核心是指针修改(Add/Insert/Delete);
  6. 二叉堆:基于切片存储,入堆向上冒泡、出堆向下冒泡重排,保证堆属性;
  7. 加权无向图:Node(节点)+Edge(边)+Graph(图)实现,边为双向关联,需初始化边映射;
  8. 并发安全:所有实现均非并发安全,可通过sync.RWMutex加锁优化。