Go 语言核心技巧¶
本篇整理了 Go 语言在 LeetCode 刷题中的 “生存指南”,涵盖了从数据结构实现到语言陷阱的全方位技巧。
数据结构实现指南¶
Go 的标准库以“通用”著称,不像 C++ STL 或 Python collections 那样开箱即用,因此熟练掌握以下模板是刷题的基础。
堆 (Heap) / 优先队列¶
Go 需要实现 heap.Interface 接口。为了避免重复写模板,推荐使用 函数闭包 实现通用堆:
栈 (Stack) & 队列 (Queue)¶
Go 使用 slice 模拟栈和队列。
| 结构 | 操作 | 代码 | 复杂度 | 备注 |
|---|---|---|---|---|
| 栈 | 入栈 | st = append(st, v) |
\(O(1)\) | |
| 出栈 | v := st[len(st)-1]; st = st[:len(st)-1] |
\(O(1)\) | 记得判空 | |
| 栈顶 | st[len(st)-1] |
\(O(1)\) | ||
| 队列 | 入队 | q = append(q, v) |
\(O(1)\) | |
| 出队 | v := q[0]; q = q[1:] |
\(O(1)\) | 注意内存泄漏! |
队列的内存陷阱
使用 q = q[1:] 会导致底层数组无法释放前半部分的空间。如果队列生命周期很长,建议在长度过大且有效元素较少时重建切片,或者使用环形数组。
集合 (Set)¶
Go 没有内置 set,通常用 map[T]struct{} 模拟。
为什么用 struct{}?
空结构体 struct{} 在 Go 中 不占用内存空间(size 为 0),比 map[int]bool 更节省内存。
避坑指南¶
Map 的随机性¶
陷阱:Go 的 map 遍历顺序是 完全随机 的!且每次运行都不一样。
后果:如果题目要求按顺序输出分组结果,直接遍历 map 必挂。
解法:先提取 Key 到切片,对 Key 排序,再遍历。
循环变量引用陷阱 (Pre-Go 1.22)¶
陷阱:在 for 循环中获取元素指针或闭包引用。
现状:Go 1.22+ 已经修复了此问题,循环变量每次迭代都会创建新实例。但在老版本环境中(某些笔试平台),仍需注意:
二维切片初始化¶
陷阱:不能像 Python 那样 [[0]*n]*m(这是浅拷贝),Go 必须手动循环。
LeetCode 实用技巧¶
字符串高效处理¶
Go 的 string 是不可变的,[]byte 是可变的。
- 大量拼接:务必使用
strings.Builder(推荐) 或bytes.NewBuffer。 strings.Builder:专门为构建字符串优化,零拷贝转换为 string。bytes.NewBuffer:更通用,适合同时也需要字节流操作的场景。- 修改字符:先转
[]byte,改完再转回string。 - 遍历字符:
for i := 0; i < len(s); i++:按 字节 (byte) 遍历,适合 ASCII。for i, r := range s:按 字符 (rune) 遍历,适合含中文等多字节字符。
常用简易辅助函数¶
LeetCode 环境通常没有 generics 版本的 Max/Min/Abs(Go 1.21 前),建议背诵以下三行:
排序黑科技¶
sort.Slice 极其灵活,配合匿名函数可以对任意结构排序:
二分查找神器¶
标准库 sort.Search(n, f) 是刷题神器,它返回 [0, n) 中 第一个满足 f(i) == true 的下标。
原理¶
Go 的 sort.Search 相当于 C++ 的 std::lower_bound。只要 f(i) 满足单调性(先 false 后 true),它就能稳定找到 true/false 的临界点。
位运算黑科技 (math/bits)¶
Go 提供了超级好用的位运算包 math/bits,无需手写位操作:
| 函数 | 功能 | 对应 C++ |
|---|---|---|
bits.OnesCount(x) |
统计二进制中 1 的个数 | __builtin_popcount |
bits.Len(x) |
二进制长度 (最高位 1 的位置 + 1) | 32 - __builtin_clz(x) |
bits.Reverse(x) |
二进制翻转 | - |
bits.RotateLeft(x, k) |
循环左移 k 位 (k<0 为右移) | - |
随机数 (math/rand)¶
对于需要随机化的算法(如 快速选择、Treap),Go 的 rand 需要注意随机种子:
核心语法与底层陷阱¶
变量声明对比¶
| 声明方式 | 底层 | 优缺点 | 场景 | 备注 |
|---|---|---|---|---|
var s []T |
nil | 不分配内存,可直接 append | 全局/延迟初始化 | 具名返回值 []T 也是此类型 |
s := []T{} |
空数组 | 非 nil,序列化为 [] |
明确需要空列表 | 扩容时会有性能开销 |
s := make([]T, n) |
零值数组 | 性能最优,一次分配 | 已知大小 |
:= 只能用在函数内,且只能声明非递归闭包,如:
切片的暗坑¶
切片不仅仅是一个动态数组,它的底层是一个 struct { ptr, len, cap }。
- 引用传递:切片传参是“值传递”,但传递的是结构体副本(指针指向同一底层数组)。在函数内修改元素会影响原切片,但 append 导致扩容 后,会指向新数组,不再影响原切片。
- 内存泄漏:
s2 := s1[100:101]。若s1是 1GB 的大数组,只要s2还在,这 1GB 内存就不会被回收。 - 解法:使用
copy复制所需数据到新切片。
range 的坑¶
- Go 1.22 的 v 是循环变量复用,导致闭包捕获异常
- v 传递的是元素副本,无法被修改,需要通过索引来修改
- 遍历字符串时默认是 rune,而非 byte,以兼容所有字符集
Map 的关键特性¶
- 无序性:遍历顺序随机。
- 非并发安全:多协程并发读写会直接
fatal error(panic 无法捕获)。需要加锁sync.RWMutex或使用sync.Map。 - 内存不收缩:删除 Key 不会释放内存,只有置
nil才会由 GC 回收。
Defer 的“三大天条”¶
- LIFO 执行:后
defer的先执行(栈顺序)。 - 参数预计算:
defer fmt.Println(i)会在defer声明时立即计算i的值并压栈。若要获取最终值,需使用闭包defer func() { fmt.Println(i) }()。 - 修改返回值:
defer可以读取并修改 具名返回值(Named Return Value)。
Interface 的 Nil 判空¶
记住:Interface 包含 (Type, Value) 两个部分。只有 Type 和 Value 都为 nil,Interface 才等于 nil。
Channel 状态¶
| 操作 | nil channel | closed channel | active channel |
|---|---|---|---|
| close | panic | panic | 成功关闭 (不能重复关) |
| send (ch <-) | 永久阻塞 | panic | 阻塞直到由接收方 |
| recv (<- ch) | 永久阻塞 | 立即返回零值 (v, false) | 阻塞直到有发送方 |
一切循环皆为 for¶
Go 中没有 while 关键字,一切遍历和循环相关的操作皆由 for 实现。
类型转换¶
基础转换¶
| 转换 | 代码 | 备注 |
|---|---|---|
string -> int |
v, _ := strconv.Atoi(s) |
忽略错误仅用于刷题 |
int -> string |
s := strconv.Itoa(v) |
最快 |
byte -> int |
int(c - '0') |
字符转数字 |
int -> byte |
byte(v + '0') |
数字转字符 |
string -> []byte |
[]byte(s) |
发生内存拷贝 |
[]byte -> string |
string(b) |
发生内存拷贝 |
零拷贝转换 (Unsafe)¶
在追求极致性能(如千万级 QPS)时使用,普通刷题 慎用。
指针¶
参数传递机制¶
值传递(Pass by Value):
- C、Java、Go:所有参数都是值传递,包括指针(传递的是指针地址的副本)
- 特点:修改参数不影响原变量,除非传递指针并通过指针修改
对象引用传递(Pass by Object-Reference):
- Python、JavaScript、Ruby:传递的是对象引用的副本
- 特点:可以修改对象内容,但重新赋值不影响原变量
- 注意:这 不是真正的引用传递,而是"共享传递"(Call by Sharing)
引用传递(Pass by Reference):
- C++、C#、PHP、Swift:提供特殊语法,参数成为原变量的"别名"
- 语法:C++/PHP 用
&,Swift 用inout,C# 用ref/out - 特点:修改参数直接影响原变量,包括重新赋值
Go 的指针特性¶
虽然所有变量都会指向内存地址,但只有语言允许显式 取地址(&)、 解引用(*)、操作地址本身 时,才被认为支持指针操作。
Go 和 C 一样,支持 多级指针(如 **T、***T)。

多级指针示例¶
以验证二叉搜索树为例,需要在递归中修改外部变量 prev:
二叉树的直径、最大路径和也和验证二叉搜索树一样,使用了多级指针。
问题:
- 二级指针
**TreeNode语法复杂 - 需要频繁解引用
*prev、(*prev).Val - 容易出错,可读性差
用闭包避免多级指针¶
Go 社区不鼓励使用多级指针,推荐用 闭包 来捕获外部变量:
优势:
- 无需二级指针,代码更简洁
- 无需解引用,可读性更好
- 符合 Go 社区最佳实践
Note
Go、Python、Java、JavaScript 等语言会 隐式自动捕获 外部变量,而 PHP、C++ 则需要 显式声明 被捕获的变量(如 PHP 的 use (&$var),C++ 的 [&var])。