跳到主要内容

Go 里用 slice 实现栈和队列时,别忽略这几个细节

· 阅读需 3 分钟
一介布衣
全栈开发者 / 技术写作者

用 slice 实现栈和队列几乎是 Go 入门练习里的必做题。因为语法不复杂,append 也足够顺手,所以很多人会觉得这类题没什么难度。但真正把它写进稍微像样一点的小程序里时,你会发现问题并不在“会不会写”,而在“有没有考虑清楚容量变化、出队后残留引用、空结构操作这些细节”。

栈的写法简单,但返回语义要先约定

栈最常见的两个操作就是 PushPop。很多教程只演示怎么把最后一个元素拿出来,却没有说明空栈时应该怎么表现。是返回零值?返回额外布尔值?还是直接报错?这件事如果不先想清楚,后面调用方就只能靠猜。

我更倾向于让练习代码也保持业务代码的习惯:接口语义先清楚,再写内部实现。哪怕只是一个小栈,返回值定义明确,后面组合起来也更自然。

队列最大的问题,往往不是功能,而是“头部越来越大”

用 slice 当队列时,最直观的做法是每次取第一个元素,然后把切片向后挪一位。这种写法逻辑没有错,但如果长期运行、数据量也不小,就要开始关注底层数组还被前面那部分元素引用着的问题。

简单说,表面上你已经把头元素“出队”了,但底层空间未必马上释放。对于练习题这不是大事,可一旦把这种写法直接搬进长时间运行的程序里,就可能让你对内存占用产生误判。

删除元素后,适当清空引用是个好习惯

如果切片里存的是指针、结构体指针或者较大的对象引用,在出栈、出队时把原位置显式置空,通常会更稳妥一些。这不是因为 Go 不会做垃圾回收,而是因为你主动消除残留引用,能让代码意图更清楚,也更容易避免长链路上的意外持有。

这类习惯在练习阶段养成,后面做缓存、任务队列、对象池时会很有帮助。

先封装小方法,再写题目逻辑

我自己练数据结构题时,不太喜欢把 append、截断、空判断全部散落在主流程里。更顺手的方式是先封装一个很小的 StackQueue 类型,把操作集中起来。哪怕只是练习,也能顺便训练接口拆分。

type IntStack struct {
items []int
}

func (s *IntStack) Push(v int) {
s.items = append(s.items, v)
}

func (s *IntStack) Pop() (int, bool) {
if len(s.items) == 0 {
return 0, false
}
last := len(s.items) - 1
v := s.items[last]
s.items = s.items[:last]
return v, true
}

真正的价值不在于这个类型多高级,而在于以后无论你拿它去做括号匹配、表达式求值还是 BFS、DFS 的练习,都不会每次从头重写同样的边界处理。

数据结构练习,练的是手感,也是工程意识

栈和队列本身并不难,但它们特别适合训练“一个操作的副作用是什么”“空输入怎么处理”“底层空间会不会被长期占着”这种工程感。很多算法题解只追求结果正确,可实际写服务和工具时,稳定性往往就输在这些细节上。

所以别把这种题当成只是面试热身。把它们写稳,对 Go 的 slice 心智模型也会更扎实。