附录 D.常见错误与 FAQ

本附录把 Go 学习与实战中最常见的错误按「报错原文 → 原因 → 修法」整理:declared and not used、类型不匹配、nil map 写入、切片别名、goroutine 泄漏、context deadline exceeded、module path 不匹配、missing go.sum entry、import cycle 与中文乱码,每条都给出最小复现代码与修法。

读懂报错,是这本书希望帮你建立的一项独立能力。这一页把学习与实战中最常撞上的错误按「报错原文 → 原因 → 修法」整理出来,每条都给出最小复现代码。

建议的用法是:先看报错原文匹配到对应条目,再照着「修法」改。下面所有报错文本都在本机 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 观察运行时。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「golang」更多文章

  1. 《Go 语言编程实战》目录
  2. 《Go 语言编程实战》18.3 上线、观测与迭代
  3. 《Go 语言编程实战》18.2 故障演练