读懂报错,是这本书希望帮你建立的一项独立能力。这一页把学习与实战中最常撞上的错误按「报错原文 → 原因 → 修法」整理出来,每条都给出最小复现代码。
建议的用法是:先看报错原文匹配到对应条目,再照着「修法」改。下面所有报错文本都在本机 go1.27.0 上实际触发过。
D.1 declared and not used
./main.go:4:2: declared and not used: x
Go 把「声明了局部变量却没用」当编译错误,而不是警告。
package main
func main() {
x := 42
}
修法:要么用掉它,要么删掉,要么用空白标识符 _ = x 显式丢弃(后者通常意味着这段代码该重构)。
D.2 cannot use x (type T) as type U
./main.go:7:17: cannot use n (variable of type int) as string value in variable declaration
Go 不做隐式类型转换,int 不能自动变成 string。
package main
import "fmt"
func main() {
var n int = 3
var s string = n
fmt.Println(s)
}
修法:显式转换用 string(rune(n))(数字转字符)或 strconv.Itoa(n)(数字转十进制字符串)。注意 string(65) 得到的是 "A" 而不是 "65",这是常见误区。
D.3 assignment to entry in nil map
panic: assignment to entry in nil map
map 的零值是 nil,可以读、可以 len、可以 range,但不能写入。
package main
func main() {
var m map[string]int
m["a"] = 1
}
修法:用 make 或字面量初始化:m := make(map[string]int) 或 m := map[string]int{}。
D.4 index out of range
panic: runtime error: index out of range [5] with length 3
下标越界。切片长度为 3,却访问了第 5 个元素。
package main
import "fmt"
func main() {
xs := []int{1, 2, 3}
fmt.Println(xs[5])
}
修法:访问前判断 if i < len(xs),或用 for _, v := range xs 避免手写下标。注意 xs[i:j] 的上界是容量而非长度:只要 0 ≤ i ≤ j ≤ cap(xs) 就不会 panic(j 可以超过 len),超出 cap 才 panic;而 xs[i] 一旦 i ≥ len(xs) 就会 panic。
D.5 切片别名导致的意外修改
[1 99 3]
[1 99 3] [-1 99 3]
这行输出看着奇怪,其实是切片共享底层数组:修改子切片会影响原切片。
package main
import (
"fmt"
"slices"
)
func main() {
xs := []int{1, 2, 3}
sub := xs[1:]
sub[0] = 99
fmt.Println(xs)
ys := slices.Clone(xs)
ys[0] = -1
fmt.Println(xs, ys)
}
修法:需要独立副本时用 slices.Clone(xs) 或 append([]int(nil), xs...)。append 若触发扩容会另起数组,因此「有时共享、有时不共享」,更不能依赖。
D.6 goroutine 泄漏
before: 1 after: 101
没有对应的报错——goroutine 泄漏的可怕之处正是它安静地累积,直到内存或文件描述符耗尽。
package main
import (
"fmt"
"runtime"
"time"
)
func leak() {
ch := make(chan int)
go func() { <-ch }() // 永远等不到,泄漏
}
func main() {
base := runtime.NumGoroutine()
for i := 0; i < 100; i++ {
leak()
}
time.Sleep(50 * time.Millisecond)
fmt.Println("before:", base, "after:", runtime.NumGoroutine())
}
修法:让每个 goroutine 都有明确的退出路径——用 context 取消、关闭 channel 通知、或用带缓冲的 channel 保证发送不被卡住。排查时用 runtime.NumGoroutine() 或 pprof 的 goroutine profile。
D.7 context deadline exceeded
ctx: context deadline exceeded
context 超时被触发,ctx.Err() 返回 context.DeadlineExceeded。
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
defer cancel()
select {
case <-time.After(100 * time.Millisecond):
fmt.Println("done")
case <-ctx.Done():
fmt.Println("ctx:", ctx.Err())
}
}
修法:这通常不是 bug 而是预期行为——要么放宽超时,要么让被调用的操作更快。判断取消原因用 errors.Is(ctx.Err(), context.DeadlineExceeded)。
D.8 module declares its path as …
module declares its path as: example.com/bar
but was required as: example.com/foo
依赖模块 go.mod 里声明的模块路径,与引用它时使用的路径不一致。
go.mod 里写 require example.com/foo v1.0.0,
但被拉取的模块内部声明的是 module example.com/bar
修法:改对 require 的路径,或改被依赖模块的 module 行使两者一致。多版本 / 迁移仓库时最容易出现,复制 go.mod 而忘了改 module 行是常见原因。
D.9 missing go.sum entry
main.go:4:2: missing go.sum entry for module providing package github.com/example/lib (imported by app); to add:
go get app
代码 import 了某个模块,但 go.sum 里没有它的校验和。
package main
import (
_ "github.com/example/lib"
)
func main() {}
修法:按提示跑 go mod tidy(或 go get <module>),它会补齐 go.sum。不要手工编辑 go.sum;也不要轻易删掉它,那会绕过内容校验。
D.10 import cycle not allowed
package cycle
imports cycle/a from main.go
imports cycle/b from a.go
imports cycle/a from b.go: import cycle not allowed
包 A 依赖包 B,包 B 又依赖包 A,形成循环。Go 不允许包级循环依赖。
a/a.go: import "cycle/b"
b/b.go: import "cycle/a"
修法:抽出第三个包放共享类型(如 internal/model),让 A、B 都依赖它;或把其中一个依赖改为通过接口注入,打断编译期的直接引用。
D.11 中文字符串按字节切片的乱码
�
对 UTF-8 字符串按字节下标切片,切断了某个多字节字符,得到非法序列。
package main
import "fmt"
func main() {
s := "你好世界"
fmt.Println(s[0:2])
}
修法:先转成 rune 切片再切:string([]rune(s)[:1]) 得到 "你"。记住 len(s) 是字节数,len([]rune(s)) 才是字符数。
D.12 WARNING: DATA RACE
WARNING: DATA RACE
Read at 0x00c000106058 by goroutine 15:
多个 goroutine 无同步地读写同一变量。注意这只有在开了 -race 时才会报告,普通 go run 可能什么都不说。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
n := 0
for i := 0; i < 10; i++ {
wg.Add(1)
go func() { defer wg.Done(); n++ }()
}
wg.Wait()
fmt.Println(n)
}
修法:用 sync.Mutex 保护临界区,或改用 atomic.Int64。测试与 CI 里都应带上 -race,它是发现并发 bug 最省力的工具。
FAQ:几个常见疑问
| 疑问 | 简答 |
|---|---|
| 编译过了就说明代码对吗 | 不对。类型错误能挡,语义错误挡不住,见序中 Counter.Inc 的例子 |
为什么 string(65) 是 "A" | 它把 65 当 Unicode 码点;要十进制字符串用 strconv.Itoa(65) |
nil 切片能直接 append 吗 | 能,会自动分配;但 nil map 不能写 |
range 出来的地址能取吗 | 1.22 起每轮变量独立,取 &v 不再全指向同一个 |
| 该用值接收者还是指针接收者 | 看方法是否需要修改接收者,同一类型保持一致 |
| 两个 goroutine 读写同一个 map | 会 panic(concurrent map writes),必须加锁 |
忘了 defer cancel() 会怎样 | context 泄漏,关联的定时器与资源不会释放 |
| 模块路径写错了怎么办 | 改 go.mod 的 module 行,再 go mod tidy |
遇到表里没有的报错,先读一遍原文里的文件、行号、类型三个信息——Go 的错误信息通常已经点明了问题所在。
小结
- 编译错误(declared and not used、类型不匹配)说明代码还没到运行阶段,先修这些。
- 运行期 panic(nil map、越界、nil 指针)多为「用了零值却忘了初始化」,初始化能解决大半。
- 切片与 map 是引用语义,副本要显式
Clone,否则改一个会影响另一个。 - 并发问题不会自己报错:goroutine 泄漏安静累积,data race 只有
-race才看得见。 - 模块类错误(path 不匹配、missing go.sum、import cycle)几乎都能靠
go mod tidy与「抽公共包」解决。 - 中文乱码的根源是「按字节操作字符串」,先转
[]rune再处理。
本附录只是高频问题的集合。真正遇到陌生报错时,最可靠的三个工具是:读懂报错原文、go doc 查签名、以及 -race 与 pprof 观察运行时。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。