NilAway
[!警告]
NilAway 目前正在积极开发中:可能会出现误报和破坏性更改。 我们非常感谢任何反馈和贡献!
NilAway 是一个静态分析工具,旨在通过在编译时而非运行时捕获空指针异常,帮助开发人员避免在生产环境中出现空指针崩溃。NilAway 类似于标准的nilness 分析器,但它采用了更复杂和强大的静态分析技术来跟踪包内以及跨包的空值流,并报告错误,为用户提供空值流以便更容易调试。
NilAway 具有三个关键特性,使其脱颖而出:
-
它是全自动的:NilAway 配备了推理引擎,除了标准的 Go 代码外,不需要开发人员提供任何额外信息(如注释)。
-
它速度快:我们设计 NilAway 时考虑了速度和可扩展性,使其适用于大型代码库。在我们的测量中,启用 NilAway 时观察到的构建时间开销不到 5%。我们还在不断应用优化以进一步减少其占用。
-
它是实用的:它不会阻止代码中所有可能的空指针异常,但它能捕获我们在生产中观察到的大多数潜在空指针异常,使 NilAway 能够在实用性和构建时间开销之间保持良好平衡。
:star2: 有关更详细的技术讨论,请查看我们的 Wiki、工程博客 和论文(进行中)。
运行 NilAway
NilAway 使用标准的 go/analysis 实现,使其易于与现有的分析器驱动程序集成(即 golangci-lint、nogo 或 作为独立检查器运行)。
[!重要]
默认情况下,NilAway 分析所有 Go 代码,包括标准库和依赖项。这有助于 NilAway 更好地理解依赖项的代码并减少误报。然而,对于有大量依赖项的大型 Go 项目,这也会带来显著的性能成本(对于支持模块化的驱动程序来说只需一次),并增加依赖项中不可操作错误的数量。我们强烈建议使用 include-pkgs 标志将分析范围缩小到仅限于您的项目代码。这指示 NilAway 跳过分析依赖项(例如第三方库),让您专注于 NilAway 在您的一方代码中报告的潜在空指针异常!
独立检查器
[!重要]
由于 NilAway 进行的分析较为复杂,NilAway 通过 go/analysis 框架的 Fact 机制 缓存其对特定包的发现。因此,强烈建议使用支持模块化分析的驱动程序(即 bazel/nogo 或 golangci-lint,但不是独立检查器,因为它将所有事实存储在内存中)以获得更好的大型项目性能。提供独立检查器更多是为了评估目的,因为它易于上手。
通过运行以下命令从源代码安装二进制文件:
go install go.uber.org/nilaway/cmd/nilaway@latest
然后,运行 linter:
nilaway -include-pkgs="<YOUR_PKG_PREFIX>,<YOUR_PKG_PREFIX_2>" ./...
golangci-lint (>= v1.57.0)
NilAway 在其当前形式下可能会报告误报。这不幸阻碍了它立即合并到 golangci-lint 并作为一个 linter 提供(参见 PR#4045)。因此,您需要将 NilAway 构建为 golangci-lint 的插件,以作为私有 linter 执行。golangci-lint 中有两个插件系统,使用 模块插件系统(自 v1.57.0 版本引入)要容易得多,这是在 golangci-lint 中运行 NilAway 的唯一支持方法。
(1) 如果您还没有在仓库根目录创建 .custom-gcl.yml
文件,请创建它并添加以下内容:
# 这必须是 >= v1.57.0 以支持模块插件系统。
version: v1.57.0
plugins:
- module: "go.uber.org/nilaway"
import: "go.uber.org/nilaway/cmd/gclplugin"
version: latest # 或固定版本以实现可重现构建。
(2) 将 NilAway 添加到 linter 配置文件 .golangci.yaml
中:
linters-settings:
custom:
nilaway:
type: "module"
description: 检测 Go 代码中潜在空指针异常的静态分析工具。
settings:
# 设置必须是"字符串到字符串的映射"以模拟命令行标志:键是标志名,值是特定标志的值。
include-pkgs: "<YOUR_PACKAGE_PREFIXES>"
# NilAway 可以像其他 golangci-lint 分析器一样在配置文件的其他部分中被称为 `nilaway`。
(3) 构建包含 NilAway 的自定义 golangci-lint 二进制文件:
# 注意,用于引导自定义二进制文件的 `golangci-lint` 版本也必须 >= v1.57.0。
$ golangci-lint custom
默认情况下,自定义二进制文件将在 .
目录下构建,名称为 custom-gcl
,这可以在 .custom-gcl.yml
文件中进一步自定义(参见 模块插件系统 的说明)。
[!提示]
缓存自定义二进制文件以避免重新构建,节省资源。如果您使用的是 NilAway 的固定版本,可以使用.custom-gcl.yml
文件的哈希值作为缓存键。如果您使用latest
作为 NilAway 版本,可以将构建日期附加到缓存键上,以强制在特定时间段后使缓存过期。
(4) 运行自定义二进制文件而不是 golangci-lint
:
# 参数与 `golangci-lint` 相同。
$ ./custom-gcl run ./...
Bazel/nogo
使用 bazel/nogo 运行需要稍多的努力。首先按照 rules_go、gazelle 和 nogo 的说明设置您的 Go 项目,使其可以使用 bazel/nogo 构建,不配置或使用默认的 linter 集。然后,
(1) 在您的 tools.go
文件中添加 import _ "go.uber.org/nilaway"
(或您用于配置工具依赖的其他文件,参见 Go 模块文档中的 如何跟踪模块的工具依赖?),以避免 go mod tidy
删除 NilAway 作为工具依赖。
(2) 运行以下命令将NilAway作为工具依赖添加到您的项目中:
# 将NilAway作为依赖项获取,并在go.mod文件中获取其传递依赖项。
$ go get go.uber.org/nilaway@latest
# 这不应该从go.mod文件中删除NilAway作为依赖项。
$ go mod tidy
# 运行gazelle以从go.mod同步依赖项到WORKSPACE文件。
$ bazel run //:gazelle -- update-repos -from_file=go.mod
(3) 将NilAway添加到nogo配置中(通常在顶级BUILD.bazel
文件中):
nogo(
name = "my_nogo",
visibility = ["//visibility:public"], # 必须具有公共可见性
deps = [
+++ "@org_uber_go_nilaway//:go_default_library",
],
config = "config.json",
)
(4) 运行bazel build以查看NilAway的工作情况(任何nogo错误都会停止bazel构建,您可以使用--keep_going
标志要求bazel尽可能多地构建):
$ bazel build --keep_going //...
(5) 查看nogo文档了解如何向nogo驱动程序传递配置JSON,并查看我们的wiki页面了解如何向NilAway传递配置。
代码示例
让我们看几个例子,了解NilAway如何帮助防止空指针引起的panic。
// 示例1:
var p *P
if someCondition {
p = &P{}
}
print(p.f) // nilness在这里不报错,但NilAway会报错。
在这个例子中,局部变量p
只在someCondition
为真时被初始化。在访问字段p.f
时,如果someCondition
为假,可能会发生panic。NilAway能够捕获这个潜在的空指针流,并报告以下错误,显示这个空指针流:
go.uber.org/example.go:12:9: error: 检测到潜在的空指针panic。观察到从源到解引用点的空指针流:
- go.uber.org/example.go:12:9: 未赋值的变量`p`访问了字段`f`
如果我们用空指针检查(if p != nil
)来保护这个解引用,错误就会消失。
NilAway还能捕获跨函数的空指针流。例如,考虑以下代码片段:
// 示例2:
func foo() *int {
return nil
}
func bar() {
print(*foo()) // nilness在这里不报错,但NilAway会报错。
}
在这个例子中,函数foo
返回一个空指针,在bar
中直接解引用,导致每次调用bar
时都会panic。NilAway能够捕获这个潜在的空指针流,并报告以下错误,描述了跨函数边界的空指针流:
go.uber.org/example.go:23:13: error: 检测到潜在的空指针panic。观察到从源到解引用点的空指针流:
- go.uber.org/example.go:20:14: 字面量`nil`从`foo()`在位置0返回
- go.uber.org/example.go:23:13: `foo()`的结果0被解引用
请注意,在上面的例子中,foo
不一定要与bar
位于同一个包中。NilAway能够跟踪跨包的空指针流。此外,NilAway还处理Go特有的语言结构,如接收器、接口、类型断言、类型switch等。
配置
我们通过go/analysis中的标准标志传递机制公开了一组标志。请查看wiki/Configuration以了解可用的标志以及如何使用不同的linter驱动程序传递它们。
支持
我们遵循与Go项目相同的版本支持政策:我们支持并测试Go的最后两个主要版本。
如果您有任何问题、错误报告和功能请求,请随时在GitHub上提出问题。
贡献
我们非常欢迎您为NilAway做出贡献!请注意,一旦您创建拉取请求,您将被要求签署我们的Uber贡献者许可协议。
许可证
本项目版权归2023 Uber Technologies, Inc.所有,并根据Apache 2.0许可证授权。