deadcode 是 Go 生态中轻量级死代码检测工具,仅报告从 main 函数出发不可达的函数和方法,依赖 RTA 算法构建可达性图,无 main 包则无法启动分析,不处理变量、类型、接口实现或 panic 后语句。

deadcode 是目前 Go 生态中检测死代码最直接、最轻量的工具,但它有明确的适用边界:**只报告从 main 函数出发不可达的函数和方法,不处理变量、类型、接口实现或 panic 后的不可达语句**。如果你期望它像编译器报错一样揪出所有“没用的代码”,会失望。
为什么 deadcode 必须有 main 包?
deadcode 的分析起点是程序入口,它基于 Rapid Type Analysis(RTA)构建可达函数图——从每个 main 函数开始,沿着调用链、接口赋值、方法集推导、反射标记(有限)等路径传播“活跃性”。没有 main,就没有根节点,整个分析就失去依据。
常见错误现象:
-
deadcode: no main packages—— 当前目录下没有含func main()的包,或go.mod未正确识别模块路径 - 只扫描了
./...但漏掉cmd/下真正的主程序目录
解决办法:
- 确保在项目根目录(含
go.mod)运行:deadcode ./cmd/...(而不是./...) - 若为库项目(无
main),deadcode本身不适用;可改用go vet -unused(Go 1.22+)或staticcheck的SA4009规则检查未使用的函数参数、局部变量等
deadcode 不会报哪些“死代码”?
它专注函数/方法级可达性,对以下情况完全沉默:
-
var unusedVar = "hello"(未使用的全局变量) -
type UnusedStruct struct{}(未实例化、未导出、未嵌入的结构体) -
func (s *UnusedStruct) Method() {}(未被任何接口要求或显式调用的方法) -
panic("now")后面的语句(这是编译器层面的不可达,deadcode不介入控制流分析) - 仅用于
var _ Interface = &impl{}的接口实现(只要该接口在别处被使用,deadcode就认为实现是活的)
性能影响很小,但误报率低——它只报确定不可达的函数,不猜。
如何验证某个函数为什么没被标为死代码?
用 -whylive 参数反向追踪调用链,比盲删安全得多:
deadcode -whylive=github.com/yourorg/yourpkg.(*Client).DoRequest ./...
输出类似:
github.com/yourorg/yourpkg/main.main dynamic@L012 → github.com/yourorg/yourpkg/http.NewClient static@L045 → github.com/yourorg/yourpkg.(*Client).DoRequest
这说明该方法通过 main → NewClient → DoRequest 被间接调用。如果某函数没出现在任何 -whylive 输出里,且你确认没通过反射、插件或测试代码调用它,那基本可以删。
注意:-whylive 只对当前命令行指定的包生效,跨模块引用需确保模块路径完整、go.mod 依赖已 go mod tidy。
真正容易被忽略的复杂点
deadcode 对接口实现的判断依赖“是否被某个接口变量赋值过”,但不验证该接口变量是否真被使用。比如:
var _ io.Reader = &MyReader{} // 这行会让 MyReader.Read 被视为“活”的
func (*MyReader) Read(p []byte) (n int, err error) { ... }
如果 _ io.Reader = &MyReader{} 仅存在于测试文件或未被任何包导入的文件中,deadcode 仍可能将其判定为活函数——因为它只做静态可达分析,不运行时执行路径裁剪。这种“伪活跃”需要结合项目结构和实际调用上下文人工判断。











