Go 的设计很务实:内建的数据结构不算多,但组合能力很强。实际开发通常以内建类型(arrayslicemapstruct)打底,再按场景配合 container/* 包和自定义结构。

速览

结构Go 里常见实现核心特点常见复杂度(平均)
定长数组[N]T长度写在类型里,值语义索引 O(1)
动态数组[]T最常用,自动扩容末尾追加均摊 O(1),索引 O(1)
哈希表map[K]Vkey 查找快,遍历无序查找/插入/删除 O(1)
结构体struct聚合字段,可组合方法字段访问 O(1)
[]TLIFO,最简单高效push/pop 末尾 O(1)
队列[]Tcontainer/listFIFO入队/出队 O(1)(实现得当)
集合map[T]struct{}用 map 模拟 set增删查 O(1)
container/heap优先队列push/pop O(log n)
链表container/list插入删除灵活已定位节点后插删 O(1)

内建结构

Array:定长数组

var a [3]int = [3]int{10, 20, 30}
fmt.Println(a[1]) // 20

Slice:最常用的动态序列

slice 本质是一个描述符(指向底层数组 + len + cap)。

s := []int{1, 2, 3}
s = append(s, 4)
fmt.Println(len(s), cap(s), s) // 4 ... [1 2 3 4]

t := s[1:3] // [2 3]

Map:哈希表

scores := map[string]int{
	"alice": 90,
	"bob":   82,
}

scores["cathy"] = 95
delete(scores, "bob")

v, ok := scores["alice"]
fmt.Println(v, ok)

Struct:业务建模核心

type User struct {
	ID    int
	Name  string
	Email string
}

u := User{ID: 1, Name: "Calvin", Email: "a@b.com"}
fmt.Println(u.Name)

struct 常与方法、接口配合使用,是 Go 建模的基础。通过组合(embedding)可以实现轻量复用。

常见容器模式

Stack 与 Queue:通常用 Slice 就够了

栈(LIFO):

stack := []int{}
stack = append(stack, 10)
stack = append(stack, 20)

top := stack[len(stack)-1]
stack = stack[:len(stack)-1]
fmt.Println(top) // 20

队列(FIFO,简单写法):

queue := []int{}
queue = append(queue, 1, 2, 3)

head := queue[0]
queue = queue[1:]
fmt.Println(head) // 1

注意:长期大量出队时,简单切片队列可能导致底层数组无法及时释放。高频队列建议实现环形缓冲区,或使用 container/list

Set:用 Map 模拟

Go 没有内建 set,最常见写法如下:

set := map[string]struct{}{}

set["go"] = struct{}{}
set["rust"] = struct{}{}

_, exists := set["go"]
delete(set, "rust")
fmt.Println(exists)

值用 struct{} 可以做到几乎零额外空间占用(相较 bool,常见于性能敏感场景)。

Heap:优先队列

标准库提供 container/heap,实现一组接口即可使用。

type IntHeap []int

func (h IntHeap) Len() int           { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h IntHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }

func (h *IntHeap) Push(x any) { *h = append(*h, x.(int)) }
func (h *IntHeap) Pop() any {
	old := *h
	n := len(old)
	x := old[n-1]
	*h = old[:n-1]
	return x
}

典型场景:Top K、任务调度、Dijkstra、合并多个有序流。

List:双向链表

container/list 是双向链表,适合“已知节点位置时高频插删”。

l := list.New()
e1 := l.PushBack("A")
l.PushBack("B")
l.InsertAfter("C", e1)

for e := l.Front(); e != nil; e = e.Next() {
	fmt.Println(e.Value)
}

如果主要是顺序访问,slice 往往更快(缓存友好),优先考虑 slice

哈希冲突与 Go 1.24 map

Open Hashing vs Closed Hashing

术语容易混淆,先统一:

也就是名字有点“反直觉”:closed hashing 反而是 open addressing

Open Hashing (拉链法 / Separate Chaining)

index:   0        1        2        3
			 [ ] ---> [A]->[B]  [ ] ---> [C]   [D]->[E]->[F]
							(冲突后挂链表)         (冲突后继续挂在链上)

特点:冲突元素在桶外(额外节点)通过指针连接。


Closed Hashing (开放寻址 / Open Addressing)

slots: [A] [ ] [B] [C] [ ] [D] [ ] [ ]
hash(K) -> 2
若 2 被占用:探测 3、4、5... 直到找到空槽

特点:所有元素都放在主表里,通过探测序列解决冲突。

如果 Hugo 环境支持 Mermaid,可用下图展示:

flowchart LR subgraph O[Open Hashing / Separate Chaining] b0[Bucket 0] --> n01[A] n01 --> n02[B] b1[Bucket 1] --> n11[C] b2[Bucket 2] --> n21[D] n21 --> n22[E] n22 --> n23[F] end subgraph C[Closed Hashing / Open Addressing] s0[slot0:A] s1[slot1: ] s2[slot2:B] s3[slot3:C] s4[slot4: ] s5[slot5:D] p[collision at slot2 → probe slot3/4/5] end

为什么 Go 1.24 转向 Swiss Table 风格

Go 1.24 重写了 map 底层实现,核心方向是从“桶 + 溢出桶链”转向“更紧凑的开放寻址探测”。

一句话总结:从“冲突后挂链表(指针追逐)”升级为“冲突后主表探测(连续访问 + 元数据筛选)”,在现代硬件上换取更高吞吐与更稳延迟。

对业务代码的影响

选型建议

小结

Go 的数据结构哲学是“少而精”:

slicemapstruct 用扎实,再掌握 heaplist,基本能覆盖大部分工程和算法题场景。