Gopls:分析器

Gopls 包含一个用于可插拔、模块化静态 分析器 的驱动程序,例如 go vet 使用的分析器。

大多数分析器会报告代码中的错误;有些则建议可以直接在编辑器中应用的“快速修复”。每次编辑代码时,gopls 都会重新运行其分析器。分析器诊断有助于您更早地检测到 bug,在运行测试之前,甚至在保存文件之前。

本文档描述了 gopls 中提供的分析器套件,该套件聚合了来自各种来源的分析器

  • 来自 go vet 套件的所有常用 bug 查找分析器(例如 printf;请参阅 go tool vet help 获取完整列表);
  • 一些具有更实质性依赖项的分析器,这些依赖项阻止它们在 go vet 中使用(例如 nilness);
  • 通过建议对常见错误的快速修复来增强编译错误的分析器(例如 fillreturns);以及
  • 少量建议可能的样式改进的分析器(例如 simplifyrange)。

要启用或禁用分析器,请使用 analyses 设置。

此外,gopls 还包含 staticcheck 套件。当 staticcheck 布尔选项未设置时,默认启用其中略超过一半的分析器;此子集已根据精度和效率进行选择。将 staticcheck 设置为 true 以启用完整集合,或设置为 false 以禁用完整集合。

Staticcheck 分析器与所有其他分析器一样,可以使用 analyzers 配置设置显式启用或禁用;此设置优先于 staticcheck 设置,因此,无论您使用 staticcheck 的哪个值(true/false/unset),都可以根据您偏好的分析器集合进行调整。

QF1001:应用德摩根定律

自 2021.1 版本起可用

默认:关闭。通过设置 "analyses": {"QF1001": true} 来启用。

软件包文档:QF1001

QF1002:将未标记的 switch 转换为已标记的 switch

一个比较单个变量与一系列值的未标记 switch 可以替换为已标记的 switch。

之前

switch {
case x == 1 || x == 2, x == 3:
    ...
case x == 4:
    ...
default:
    ...
}

之后

switch x {
case 1, 2, 3:
    ...
case 4:
    ...
default:
    ...
}

自 2021.1 版本起可用

默认:开启。

软件包文档:QF1002

QF1003:将 if/else-if 链转换为已标记的 switch

一系列 if/else-if 检查,比较同一个变量与值,可以替换为已标记的 switch。

之前

if x == 1 || x == 2 {
    ...
} else if x == 3 {
    ...
} else {
    ...
}

之后

switch x {
case 1, 2:
    ...
case 3:
    ...
default:
    ...
}

自 2021.1 版本起可用

默认:开启。

软件包文档:QF1003

QF1004:使用 strings.ReplaceAll 代替 strings.Replace (n == -1)

自 2021.1 版本起可用

默认:开启。

软件包文档:QF1004

QF1005:展开 math.Pow 调用

某些 math.Pow 的用法可以简化为基本乘法。

之前

math.Pow(x, 2)

之后

x * x

自 2021.1 版本起可用

默认:关闭。通过设置 "analyses": {"QF1005": true} 来启用。

软件包文档:QF1005

QF1006:将 if+break 提升到循环条件

之前

for {
    if done {
        break
    }
    ...
}

之后

for !done {
    ...
}

自 2021.1 版本起可用

默认:关闭。通过设置 "analyses": {"QF1006": true} 来启用。

软件包文档:QF1006

QF1007:将条件赋值合并到变量声明

之前

x := false
if someCondition {
    x = true
}

之后

x := someCondition

自 2021.1 版本起可用

默认:关闭。通过设置 "analyses": {"QF1007": true} 来启用。

软件包文档:QF1007

QF1008:从选择器表达式中省略嵌入的字段

自 2021.1 版本起可用

默认:关闭。通过设置 "analyses": {"QF1008": true} 来启用。

软件包文档:QF1008

QF1009:使用 time.Time.Equal 代替 == 运算符

自 2021.1 版本起可用

默认:开启。

软件包文档:QF1009

QF1010:在打印字节切片时将其转换为字符串

自 2021.1 版本起可用

默认:开启。

软件包文档:QF1010

QF1011:省略变量声明中的冗余类型

自 2021.1 版本起可用

默认:关闭。通过设置 "analyses": {"QF1011": true} 来启用。

软件包文档:QF1011

QF1012:使用 fmt.Fprintf(x, …) 代替 x.Write(fmt.Sprintf(…))

自 2022.1 版本起可用

默认:开启。

软件包文档:QF1012

S1000:使用普通的 channel 发送或接收,而不是单 case 的 select

只有单个 case 的 select 语句可以替换为简单的发送或接收。

之前

select {
case x := <-ch:
    fmt.Println(x)
}

之后

x := <-ch
fmt.Println(x)

自 2017.1 版本起可用

默认:开启。

软件包文档:S1000

S1001:用 copy 调用替换 for 循环

使用 copy() 将元素从一个 slice 复制到另一个 slice。对于大小相同的数组,可以使用简单的赋值。

之前

for i, x := range src {
    dst[i] = x
}

之后

copy(dst, src)

自 2017.1 版本起可用

默认:开启。

软件包文档:S1001

S1002:省略与布尔常量的比较

之前

if x == true {}

之后

if x {}

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"S1002": true} 来启用。

软件包文档:S1002

S1003:用 strings.Contains 替换 strings.Index 调用

之前

if strings.Index(x, y) != -1 {}

之后

if strings.Contains(x, y) {}

自 2017.1 版本起可用

默认:开启。

软件包文档:S1003

S1004:用 bytes.Equal 替换 bytes.Compare 调用

之前

if bytes.Compare(x, y) == 0 {}

之后

if bytes.Equal(x, y) {}

自 2017.1 版本起可用

默认:开启。

软件包文档:S1004

S1005:删除空白标识符的不必要使用

在许多情况下,赋值给空白标识符是不必要的。

之前

for _ = range s {}
x, _ = someMap[key]
_ = <-ch

之后

for range s{}
x = someMap[key]
<-ch

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"S1005": true} 来启用。

软件包文档:S1005

S1006:使用 'for { … }' 进行无限循环

对于无限循环,使用 for { … } 是最符合惯用语的选择。

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"S1006": true} 来启用。

软件包文档:S1006

S1007:通过使用原始字符串字面量简化正则表达式

原始字符串字面量使用反引号而不是双引号,并且不支持任何转义序列。这意味着反斜杠可以自由使用,而无需转义。

由于正则表达式有自己的转义序列,原始字符串可以提高其可读性。

之前

regexp.Compile("\\A(\\w+) profile: total \\d+\\n\\z")

之后

regexp.Compile(`\A(\w+) profile: total \d+\n\z`)

自 2017.1 版本起可用

默认:开启。

软件包文档:S1007

S1008:简化返回布尔表达式

之前

if <expr> {
    return true
}
return false

之后

return <expr>

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"S1008": true} 来启用。

软件包文档:S1008

S1009:省略对 slice、map 和 channel 的冗余 nil 检查

len 函数适用于所有 slice、map 和 channel,即使是 nil 的,它们的长度也为零。在检查长度不为零之前,无需检查 nil。

之前

if x != nil && len(x) != 0 {}

之后

if len(x) != 0 {}

自 2017.1 版本起可用

默认:开启。

软件包文档:S1009

S1010:省略默认 slice 索引

切片时,第二个索引默认为值的长度,这使得 s[n:len(s)] 和 s[n:] 等效。

自 2017.1 版本起可用

默认:开启。

软件包文档:S1010

S1011:使用单个 append 来连接两个 slice

之前

for _, e := range y {
    x = append(x, e)
}

for i := range y {
    x = append(x, y[i])
}

for i := range y {
    v := y[i]
    x = append(x, v)
}

之后

x = append(x, y...)
x = append(x, y...)
x = append(x, y...)

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"S1011": true} 来启用。

软件包文档:S1011

S1012:用 time.Since(x) 替换 time.Now().Sub(x) 调用

time.Since 助手函数的效果与使用 time.Now().Sub(x) 相同,但更易于阅读。

之前

time.Now().Sub(x)

之后

time.Since(x)

自 2017.1 版本起可用

默认:开启。

软件包文档:S1012

S1016:使用类型转换而不是手动复制结构字段

具有相同字段的两种结构类型可以相互转换。在旧版本的 Go 中,字段必须具有相同的结构标签。然而,自 Go 1.8 以来,结构标签在转换时被忽略。因此,不必逐个手动复制每个字段。

之前

var x T1
y := T2{
    Field1: x.Field1,
    Field2: x.Field2,
}

之后

var x T1
y := T2(x)

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"S1016": true} 来启用。

软件包文档:S1016

S1017:用 strings.TrimPrefix 替换手动修剪

与其使用 strings.HasPrefix 和手动切片,不如使用 strings.TrimPrefix 函数。如果字符串不以前缀开头,则返回原始字符串。使用 strings.TrimPrefix 可以降低复杂性,并避免常见的 bug,例如偏移量错误。

之前

if strings.HasPrefix(str, prefix) {
    str = str[len(prefix):]
}

之后

str = strings.TrimPrefix(str, prefix)

自 2017.1 版本起可用

默认:开启。

软件包文档:S1017

S1018:使用 'copy' 来滑动元素

copy() 允许使用相同的源和目标 slice,即使范围重叠。这使其成为滑动 slice 中元素的理想选择。

之前

for i := 0; i < n; i++ {
    bs[i] = bs[offset+i]
}

之后

copy(bs[:n], bs[offset:])

自 2017.1 版本起可用

默认:开启。

软件包文档:S1018

S1019:通过省略冗余参数来简化 'make' 调用

'make' 函数的 length 和 capacity 参数有默认值。对于 channel,length 默认为零,对于 slice,capacity 默认为 length。

自 2017.1 版本起可用

默认:开启。

软件包文档:S1019

S1020:省略类型断言中的冗余 nil 检查

之前

if _, ok := i.(T); ok && i != nil {}

之后

if _, ok := i.(T); ok {}

自 2017.1 版本起可用

默认:开启。

软件包文档:S1020

S1021:合并变量声明和赋值

之前

var x uint
x = 1

之后

var x uint = 1

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"S1021": true} 来启用。

软件包文档:S1021

S1023:省略冗余控制流

没有返回值的函数不需要 return 语句作为函数的最后一条语句。

Go 中的 switch 语句不像 C 语言那样具有自动的 fallthrough。因此,不必在 case 块的最后一条语句中使用 break。

自 2017.1 版本起可用

默认:开启。

软件包文档:S1023

S1024:用 time.Until(x) 替换 x.Sub(time.Now())

time.Until 助手函数的效果与使用 x.Sub(time.Now()) 相同,但更易于阅读。

之前

x.Sub(time.Now())

之后

time.Until(x)

自 2017.1 版本起可用

默认:开启。

软件包文档:S1024

S1025:不必要地使用 fmt.Sprintf("%s", x)

在许多情况下,有更简单、更有效的方法可以获得值的字符串表示。当值的底层类型已经是字符串,或者类型具有 String 方法时,应直接使用它们。

鉴于以下共享定义

type T1 string
type T2 int

func (T2) String() string { return "Hello, world" }

var x string
var y T1
var z T2

我们可以简化

fmt.Sprintf("%s", x)
fmt.Sprintf("%s", y)
fmt.Sprintf("%s", z)

转换为

x
string(y)
z.String()

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"S1025": true} 来启用。

软件包文档:S1025

S1028:使用 fmt.Errorf 简化错误构造

之前

errors.New(fmt.Sprintf(...))

之后

fmt.Errorf(...)

自 2017.1 版本起可用

默认:开启。

软件包文档:S1028

S1029:直接 range 字符串

对字符串进行 range 操作会产生字节偏移量和 rune。如果未在偏移量中使用,这在功能上等同于将字符串转换为 rune slice 并对其进行 range 操作。然而,直接对字符串进行 range 操作会更高效,因为它避免了分配一个取决于字符串长度的新 slice。

之前

for _, r := range []rune(s) {}

之后

for _, r := range s {}

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"S1029": true} 来启用。

软件包文档:S1029

S1030:使用 bytes.Buffer.String 或 bytes.Buffer.Bytes

bytes.Buffer 同时具有 String 和 Bytes 方法。使用 string(buf.Bytes()) 或 []byte(buf.String()) 几乎总是不必要的——只需使用另一种方法即可。

唯一的例外是 map 查找。由于编译器优化,m[string(buf.Bytes())] 比 m[buf.String()] 更高效。

自 2017.1 版本起可用

默认:开启。

软件包文档:S1030

S1031:省略循环周围的冗余 nil 检查

您可以使用 range 在 nil slice 和 map 上,循环将根本不会执行。这使得循环周围的额外 nil 检查是不必要的。

之前

if s != nil {
    for _, x := range s {
        ...
    }
}

之后

for _, x := range s {
    ...
}

自 2017.1 版本起可用

默认:开启。

软件包文档:S1031

S1032:使用 sort.Ints(x)、sort.Float64s(x) 和 sort.Strings(x)

sort.Ints、sort.Float64s 和 sort.Strings 函数比 sort.Sort(sort.IntSlice(x))、sort.Sort(sort.Float64Slice(x)) 和 sort.Sort(sort.StringSlice(x)) 更易于阅读。

之前

sort.Sort(sort.StringSlice(x))

之后

sort.Strings(x)

自 2019.1 版本起可用

默认:开启。

软件包文档:S1032

S1033:对 'delete' 的调用周围有不必要的保护

在 nil map 上调用 delete 是一个 no-op。

自 2019.2 版本起可用

默认:开启。

软件包文档:S1033

S1034:使用类型断言的结果来简化 case

自 2019.2 版本起可用

默认:开启。

软件包文档:S1034

S1035:在 net/http.Header 上调用方法时,net/http.CanonicalHeaderKey 调用是多余的

net/http.Header 的方法,即 Add、Del、Get 和 Set,在操作 map 之前已经对给定的 header 名称进行了规范化。

自 2020.1 版本起可用

默认:开启。

软件包文档:S1035

S1036:不必要的 map 访问保护

当访问一个尚不存在的 map 键时,会得到一个零值。通常,零值是一个合适的值,例如在使用 append 或进行整数数学运算时。

以下

if _, ok := m["foo"]; ok {
    m["foo"] = append(m["foo"], "bar")
} else {
    m["foo"] = []string{"bar"}
}

可以简化为

m["foo"] = append(m["foo"], "bar")

if _, ok := m2["k"]; ok {
    m2["k"] += 4
} else {
    m2["k"] = 4
}

可以简化为

m["k"] += 4

自 2020.1 版本起可用

默认:开启。

软件包文档:S1036

S1037:不必要的睡眠方式

使用带有接收来自 time.After 结果的单个 case 的 select 语句是一种非常复杂的睡眠方式,可以通过简单的 time.Sleep 调用更简单地表达。

自 2020.1 版本起可用

默认:开启。

软件包文档:S1037

S1038:打印格式化字符串的方式过于复杂

与其使用 fmt.Print(fmt.Sprintf(…)),不如使用 fmt.Printf(…)。

自 2020.1 版本起可用

默认:开启。

软件包文档:S1038

S1039:不必要的 fmt.Sprint 使用

用单个字符串参数调用 fmt.Sprint 是不必要的,并且等同于直接使用字符串。

自 2020.1 版本起可用

默认:开启。

软件包文档:S1039

S1040:类型断言到当前类型

类型断言 x.(SomeInterface),当 x 的类型已经是 SomeInterface 时,只有当 x 为 nil 时才会失败。通常,这是 x 具有不同类型时的遗留代码,您可以安全地删除类型断言。如果您想检查 x 是否不为 nil,请考虑明确使用实际的 if x == nil 比较,而不是依赖类型断言的 panic。

自 2021.1 版本起可用

默认:开启。

软件包文档:S1040

SA1000:无效的正则表达式

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA1000": true} 来启用。

软件包文档:SA1000

SA1001:无效的模板

自 2017.1 版本起可用

默认:开启。

软件包文档:SA1001

SA1002:time.Parse 中的格式无效

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA1002": true} 来启用。

软件包文档:SA1002

SA1003:encoding/binary 函数不支持的参数

encoding/binary 包只能序列化具有已知大小的类型。这排除了 int 和 uint 类型的使用,因为它们的大小在不同架构上是不同的。此外,它不支持序列化 map、channel、string 或函数。

在 Go 1.8 之前,bool 也不受支持。

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA1003": true} 来启用。

软件包文档:SA1003

SA1004:time.Sleep 中可疑的小无类型常量

time.Sleep 函数接受 time.Duration 作为其唯一参数。Duration 以纳秒为单位。因此,调用 time.Sleep(1) 将睡眠 1 纳秒。这是常见的 bug 来源,因为其他语言中的 sleep 函数通常接受秒或毫秒。

time 包提供了像 time.Second 这样的常量来表示长持续时间。这些可以与算术结合以表示任意持续时间,例如 5 * time.Second 表示 5 秒。

如果您确实打算睡眠很短的时间,请使用 n * time.Nanosecond 来告知 Staticcheck 您确实打算睡眠一定量的纳秒。

自 2017.1 版本起可用

默认:开启。

软件包文档:SA1004

SA1005:exec.Command 的第一个参数无效

os/exec 直接运行程序(在 Unix 系统上使用 fork 和 exec 系统调用的变体)。这不应与在 shell 中运行命令混淆。shell 支持输入重定向、管道和一般脚本等功能。shell 还负责将用户的输入分割成程序名称及其参数。例如,与

ls / /tmp

等效的是

exec.Command("ls", "/", "/tmp")

如果您想在 shell 中运行命令,可以考虑使用以下类似方式——但请注意,并非所有系统,特别是 Windows,都会有 /bin/sh 程序

exec.Command("/bin/sh", "-c", "ls | grep Awesome")

自 2017.1 版本起可用

默认:开启。

软件包文档:SA1005

SA1007:net/url.Parse 中的 URL 无效

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA1007": true} 来启用。

软件包文档:SA1007

SA1008:http.Header map 中的非规范键

http.Header map 中的键是规范的,这意味着它们遵循大小写字母的特定组合。诸如 http.Header.Add 和 http.Header.Del 等方法在操作 map 之前会将输入转换为此规范形式。

直接操作 http.Header map 时,与使用提供的方法不同,应注意坚持使用规范形式,以避免不一致。以下代码片段演示了一个此类不一致之处

h := http.Header{}
h["etag"] = []string{"1234"}
h.Add("etag", "5678")
fmt.Println(h)

// Output:
// map[Etag:[5678] etag:[1234]]

获取键的规范形式的最简单方法是使用 http.CanonicalHeaderKey。

自 2017.1 版本起可用

默认:开启。

软件包文档:SA1008

SA1010:(*regexp.Regexp).FindAll 调用时 n == 0,这将始终返回零结果

如果 n >= 0,则函数最多返回 n 个匹配/子匹配。要返回所有结果,请指定负数。

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA1010": true} 来启用。

软件包文档:SA1010

SA1011:'strings' 包中的各种方法期望有效的 UTF-8,但提供了无效输入

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA1011": true} 来启用。

软件包文档:SA1011

SA1012:将 nil context.Context 传递给函数,请考虑使用 context.TODO 代替

自 2017.1 版本起可用

默认:开启。

软件包文档:SA1012

SA1013:io.Seeker.Seek 被调用时,whence 常量作为第一个参数,但应该是第二个

自 2017.1 版本起可用

默认:开启。

软件包文档:SA1013

SA1014:将非指针值传递给 Unmarshal 或 Decode

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA1014": true} 来启用。

软件包文档:SA1014

SA1015:以可能泄漏的方式使用 time.Tick。考虑使用 time.NewTicker,并且仅在测试、命令和无限函数中使用 time.Tick

在 Go 1.23 之前,time.Ticker 必须关闭才能被垃圾回收。由于 time.Tick 无法关闭底层 ticker,重复使用它会导致内存泄漏。

Go 1.23 修复了此问题,允许在未关闭的情况下收集 ticker。

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA1015": true} 来启用。

软件包文档:SA1015

SA1016:捕获无法捕获的信号

并非所有信号都可以被进程拦截。特别是在类 Unix 系统上,syscall.SIGKILL 和 syscall.SIGSTOP 信号永远不会传递给进程,而是由内核直接处理。因此,尝试处理这些信号是徒劳的。

自 2017.1 版本起可用

默认:开启。

软件包文档:SA1016

SA1017:与 os/signal.Notify 一起使用的 channel 应是缓冲的

os/signal 包在传递信号时使用非阻塞 channel 发送。如果 channel 的接收端未准备好,并且 channel 是无缓冲的或已满,则信号将被丢弃。为避免错过信号,应缓冲 channel 并使用适当的大小。对于仅用于通知单个信号值的 channel,缓冲区大小为 1 就足够了。

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA1017": true} 来启用。

软件包文档:SA1017

SA1018:strings.Replace 调用时 n == 0,这没有任何效果

当 n == 0 时,将替换零个实例。要替换所有实例,请使用负数,或使用 strings.ReplaceAll。

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA1018": true} 来启用。

软件包文档:SA1018

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA1020": true} 来启用。

软件包文档:SA1020

SA1021:使用 bytes.Equal 比较两个 net.IP

net.IP 将 IPv4 或 IPv6 地址存储为字节 slice。然而,IPv4 地址的 slice 长度可以是 4 或 16 字节,使用不同的表示 IPv4 地址的方式。为了正确比较两个 net.IP,应使用 net.IP.Equal 方法,因为它同时考虑了两种表示。

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA1021": true} 来启用。

软件包文档:SA1021

SA1023:修改 io.Writer 实现中的缓冲区

Write 不能修改 slice 数据,即使是临时的。

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA1023": true} 来启用。

软件包文档:SA1023

SA1024:string 的 cutset 包含重复字符

strings.TrimLeft 和 strings.TrimRight 函数接受 cutsets,而不是前缀。cutset 被视为要从字符串中删除的字符集。例如,

strings.TrimLeft("42133word", "1234")

将得到字符串“word”——从字符串左侧删除任何是 1、2、3 或 4 的字符。

为了从另一个字符串中删除一个字符串,请使用 strings.TrimPrefix 代替。

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA1024": true} 来启用。

软件包文档:SA1024

SA1025:无法正确使用 (*time.Timer).Reset 的返回值

自 2019.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA1025": true} 来启用。

软件包文档:SA1025

SA1026:不能 marshaling channel 或 function

自 2019.2 版本起可用

默认:关闭。通过设置 "analyses": {"SA1026": true} 来启用。

软件包文档:SA1026

SA1027:64 位变量的原子访问必须是 64 位对齐的

在 ARM、x86-32 和 32 位 MIPS 上,调用者有责任安排 64 位字进行原子访问的 64 位对齐。变量或已分配的 struct、array 或 slice 中的第一个字可以依赖于 64 位对齐。

您可以使用 structlayout 工具检查 struct 中字段的对齐方式。

自 2019.2 版本起可用

默认:关闭。通过设置 "analyses": {"SA1027": true} 来启用。

软件包文档:SA1027

SA1028:sort.Slice 只能用于 slice

sort.Slice 的第一个参数必须是 slice。

自 2020.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA1028": true} 来启用。

软件包文档:SA1028

SA1029:context.WithValue 调用中的不当键

提供的键必须是可比较的,并且不应是 string 或任何其他内置类型,以避免包之间的冲突。WithValue 的用户应为键定义自己的类型。

为了避免在分配给 interface{} 时分配,context 键通常具有 struct{} 的具体类型。或者,导出的 context 键变量的静态类型应为指针或接口。

自 2020.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA1029": true} 来启用。

软件包文档:SA1029

SA1030:strconv 函数调用中的参数无效

此检查验证 strconv 中各种解析和格式化函数的 format、number base 和 bit size 参数。

自 2021.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA1030": true} 来启用。

软件包文档:SA1030

SA1031:传递给编码器的重叠字节 slice

在形式为 Encode(dst, src) 的编码函数中,dst 和 src 被发现引用了相同的内存。这可能导致 src 字节在读取之前被覆盖,当编码器每字节写入多于一个字节时。

自 2024.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA1031": true} 来启用。

软件包文档:SA1031

SA1032:errors.Is 参数顺序错误

errors.Is 函数的第一个参数是我们拥有的错误,第二个参数是我们正在尝试匹配的错误。例如

if errors.Is(err, io.EOF) { ... }

此检查检测到某些参数被交换的情况。它会标记任何第一个参数引用包级错误变量的调用,例如

if errors.Is(io.EOF, err) { /* this is wrong */ }

自 2024.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA1032": true} 来启用。

软件包文档:SA1032

SA2001:空临界区,是否打算延迟解锁?

下面这种空临界区

mu.Lock()
mu.Unlock()

非常可能是拼写错误,原本的意思是

mu.Lock()
defer mu.Unlock()

请注意,有时空临界区可能有用,作为一种信号机制来等待另一个 goroutine。很多时候,有更简单的方法可以达到相同的效果。当不是这种情况时,代码应有充分的注释以避免混淆。将此类注释与 //lint:ignore 指令结合使用可用于抑制这种罕见的误报。

自 2017.1 版本起可用

默认:开启。

软件包文档:SA2001

SA2002:在 goroutine 中调用 testing.T.FailNow 或 SkipNow,这是不允许的

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA2002": true} 来启用。

软件包文档:SA2002

SA2003:在锁定后立即延迟 Lock,很可能意味着延迟 Unlock

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA2003": true} 来启用。

软件包文档:SA2003

SA3000:TestMain 未调用 os.Exit,隐藏了测试失败

测试可执行文件(以及 'go test')如果任何测试失败,将以非零状态码退出。当指定自己的 TestMain 函数时,您有责任安排此行为,通过调用 os.Exit 并传入正确的代码。正确的代码由 (*testing.M).Run 返回,因此实现 TestMain 的常用方法是使其以 os.Exit(m.Run()) 结束。

自 2017.1 版本起可用

默认:开启。

软件包文档:SA3000

SA3001:在 benchmark 中赋值给 b.N 会扭曲结果

testing 包动态设置 b.N 以提高 benchmark 的可靠性,并将其用于计算以确定单个操作的持续时间。Benchmark 代码不得更改 b.N,因为这会使结果失真。

自 2017.1 版本起可用

默认:开启。

软件包文档:SA3001

SA4000:二元运算符两侧的表达式相同

自 2017.1 版本起可用

默认:开启。

软件包文档:SA4000

SA4001:&*x 被简化为 x,它不会复制 x

自 2017.1 版本起可用

默认:开启。

软件包文档:SA4001

SA4003:与负数比较无符号值是毫无意义的

自 2017.1 版本起可用

默认:开启。

软件包文档:SA4003

SA4004:循环在一次迭代后无条件退出

自 2017.1 版本起可用

默认:开启。

软件包文档:SA4004

SA4005:字段赋值永远不会被观察到。您是想使用指针接收器吗?

自 2021.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA4005": true} 来启用。

软件包文档:SA4005

SA4006:分配给变量的值在被覆盖之前从未被读取。忘记了错误检查还是死代码?

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA4006": true} 来启用。

软件包文档:SA4006

SA4008:循环条件中的变量永远不会改变,您是否递增了错误的变量?

例如

for i := 0; i < 10; j++ { ... }

这可能也发生在循环因为无条件的终止循环的控制流而只能执行一次的情况下。例如,当循环体包含无条件的 break、return 或 panic 时

func f() {
    panic("oops")
}
func g() {
    for i := 0; i < 10; i++ {
        // f unconditionally calls panic, which means "i" is
        // never incremented.
        f()
    }
}

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA4008": true} 来启用。

软件包文档:SA4008

SA4009:函数参数在其首次使用前被覆盖

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA4009": true} 来启用。

软件包文档:SA4009

SA4010:append 的结果永远不会在任何地方被观察到

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA4010": true} 来启用。

软件包文档:SA4010

SA4011:无效果的 break 语句。您是想 break 出外层循环吗?

自 2017.1 版本起可用

默认:开启。

软件包文档:SA4011

SA4012:将值与 NaN 比较,尽管没有值等于 NaN

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA4012": true} 来启用。

软件包文档:SA4012

SA4013:对布尔值进行两次取反(!!b)等同于写 b。这要么是冗余的,要么是拼写错误。

自 2017.1 版本起可用

默认:开启。

软件包文档:SA4013

SA4014:if/else if 链具有重复的条件且无副作用;如果条件第一次没有匹配,第二次也不会匹配

自 2017.1 版本起可用

默认:开启。

软件包文档:SA4014

SA4015:对从整数转换的浮点数调用 math.Ceil 等函数没有任何用处

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA4015": true} 来启用。

软件包文档:SA4015

SA4016:某些按位操作,例如 x ^ 0,没有任何用处

自 2017.1 版本起可用

默认:开启。

软件包文档:SA4016

SA4017:丢弃没有副作用的函数的返回值,使调用无意义

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA4017": true} 来启用。

软件包文档:SA4017

SA4018:变量的自赋值

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA4018": true} 来启用。

软件包文档:SA4018

SA4019:同一文件中的多个相同构建约束

自 2017.1 版本起可用

默认:开启。

软件包文档:SA4019

SA4020:类型 switch 中不可达的 case 子句

在如下类型 switch 中

type T struct{}
func (T) Read(b []byte) (int, error) { return 0, nil }

var v any = T{}

switch v.(type) {
case io.Reader:
    // ...
case T:
    // unreachable
}

第二个 case 子句永远无法到达,因为 T 实现 io.Reader 并且 case 子句按源顺序进行评估。

另一个例子

type T struct{}
func (T) Read(b []byte) (int, error) { return 0, nil }
func (T) Close() error { return nil }

var v any = T{}

switch v.(type) {
case io.Reader:
    // ...
case io.ReadCloser:
    // unreachable
}

尽管 T 有 Close 方法,因此实现了 io.ReadCloser,但 io.Reader 总是先匹配。io.Reader 的方法集是 io.ReadCloser 的子集。因此,不可能匹配第二个 case 而不匹配第一个 case。

结构上等价的接口

前一个示例的一个特殊情况是结构上等价的接口。给定以下声明

type T error
type V error

func doSomething() error {
    err, ok := doAnotherThing()
    if ok {
        return T(err)
    }

    return U(err)
}

以下类型 switch 将具有不可达的 case 子句

switch doSomething().(type) {
case T:
    // ...
case V:
    // unreachable
}

T 总是会先于 V 匹配,因为它们在结构上是等价的,因此 doSomething() 的返回值同时实现了两者。

自 2019.2 版本起可用

默认:开启。

软件包文档:SA4020

SA4022:将变量的地址与 nil 进行比较

像 ‘if &x == nil’ 这样的代码是无意义的,因为取一个变量的地址总是会得到一个非 nil 指针。

自 2020.1 版本起可用

默认:开启。

软件包文档:SA4022

SA4023:不可能将接口值与无类型 nil 进行比较

在底层,接口由两个元素实现:类型 T 和值 V。V 是一个具体值,例如 int、struct 或指针,绝不是接口本身,并且具有类型 T。例如,如果我们将在接口中存储 int 值 3,则生成的接口值将是,示意性地,(T=int, V=3)。值 V 也被称为接口的动态值,因为在程序执行期间,给定的接口变量可能持有不同的值 V(和对应的类型 T)。

只有当 V 和 T 都未设置时,接口值才为 nil,(T=nil, V 未设置)。特别是,nil 接口将始终持有一个 nil 类型。如果我们存储类型为 *int 的 nil 指针在接口值中,则内部类型将是 *int,无论指针的值如何:(T=*int, V=nil)。因此,即使指针值 V 为 nil,这样的接口值也将是非 nil 的。

这种情况可能令人困惑,并且在将 nil 值存储在接口值(例如错误返回值)中时出现

func returnsError() error {
    var p *MyError = nil
    if bad() {
        p = ErrBad
    }
    return p // Will always return a non-nil error.
}

如果一切顺利,函数返回一个 nil p,因此返回值是一个错误接口值,其中包含 (T=*MyError, V=nil)。这意味着如果调用者将返回的错误与 nil 进行比较,即使什么坏事都没发生,看起来也总是像有错误一样。要向调用者返回一个真正的 nil 错误,函数必须返回一个显式的 nil

func returnsError() error {
    if bad() {
        return ErrBad
    }
    return nil
}

对于返回错误的函数,最好始终在签名中使用 error 类型(如上所示),而不是具体的类型(如 *MyError),以帮助确保错误已正确创建。例如,os.Open 返回一个错误,即使(如果非 nil)它总是具体类型为 *os.PathError。

在任何时候使用接口时,都可能出现与此处描述的类似情况。只要记住,如果任何具体值已存储在接口中,该接口将不会为 nil。有关更多信息,请参阅“反射定律”:https://golang.ac.cn/doc/articles/laws_of_reflection.html

此文本摘自 https://golang.ac.cn/doc/faq#nil_error,根据知识共享署名 3.0 许可授权。

自 2020.2 版本起可用

默认:关闭。通过设置 "analyses": {"SA4023": true} 来启用。

软件包文档:SA4023

SA4024:检查内置函数的可能不可能的返回值

len 和 cap 内置函数的返回值不能为负数。

参见 https://golang.ac.cn/pkg/builtin/#lenhttps://golang.ac.cn/pkg/builtin/#cap

示例

if len(slice) < 0 {
    fmt.Println("unreachable code")
}

自 2021.1 版本起可用

默认:开启。

软件包文档:SA4024

SA4025:字面量整数除法结果为零

当除以两个整数常量时,结果也将是整数。因此,2 / 3 这样的除法结果为 0。这对以下所有示例都成立

_ = 2 / 3
const _ = 2 / 3
const _ float64 = 2 / 3
_ = float64(2 / 3)

Staticcheck 会标记此类除法,如果双方都是整数字面量,因为除法截断为零的可能性极小。Staticcheck 不会标记涉及命名常量的整数除法,以避免产生过多的误报。

自 2021.1 版本起可用

默认:开启。

软件包文档:SA4025

SA4026:Go 常量无法表示负零

在 IEEE 754 浮点数运算中,零有一个符号,可以是正的或负的。这在某些数值代码中可能很有用。

然而,Go 常量无法表示负零。这意味着字面量 -0.0 和 0.0 具有相同的理想值(零),并且在运行时都将表示正零。

要显式可靠地创建负零,您可以使用 math.Copysign 函数:math.Copysign(0, -1)。

自 2021.1 版本起可用

默认:开启。

软件包文档:SA4026

SA4027:(*net/url.URL).Query 返回副本,修改它不会改变 URL

(*net/url.URL).Query 解析 net/url.URL.RawQuery 的当前值并将其作为 net/url.Values 类型的 map 返回。除非将 map 编码并分配给 URL 的 RawQuery,否则对此 map 的后续更改不会影响 URL。

因此,以下代码模式是一个昂贵的 no-op:u.Query().Add(key, value)。

自 2021.1 版本起可用

默认:开启。

软件包文档:SA4027

SA4028:x % 1 始终为零

自 2022.1 版本起可用

默认:开启。

软件包文档:SA4028

SA4029:对 slice 进行排序的无效尝试

sort.Float64Slice、sort.IntSlice 和 sort.StringSlice 是类型,而不是函数。执行 x = sort.StringSlice(x) 没有任何作用,更不用说对任何值进行排序了。正确的用法是 sort.Sort(sort.StringSlice(x)) 或 sort.StringSlice(x).Sort(),但有更方便的辅助函数,即 sort.Float64s、sort.Ints 和 sort.Strings。

自 2022.1 版本起可用

默认:开启。

软件包文档:SA4029

SA4030:生成随机数的无效尝试

math/rand 包中接受上限的函数,例如 Intn,在半开区间 [0,n) 中生成随机数。换句话说,生成的数字将 >= 0 且 < n——它们不包括 n。因此,rand.Intn(1) 不会生成 0 或 1,它总是生成 0。

自 2022.1 版本起可用

默认:开启。

软件包文档:SA4030

SA4031:检查永不为 nil 的值是否为 nil

自 2022.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA4031": true} 来启用。

软件包文档:SA4031

SA4032:将 runtime.GOOS 或 runtime.GOARCH 与不可能的值进行比较

自 2024.1 版本起可用

默认:开启。

软件包文档:SA4032

SA5000:赋值给 nil map

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA5000": true} 来启用。

软件包文档:SA5000

SA5001:在检查可能错误之前延迟 Close

自 2017.1 版本起可用

默认:开启。

软件包文档:SA5001

SA5002:空 for 循环('for {}')会产生 spin 并可能阻塞调度器

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA5002": true} 来启用。

软件包文档:SA5002

SA5003:无限循环中的 defer 将永远不会执行

defer 的作用域是围绕的函数,而不是围绕的块。在一个永不返回的函数中,即包含无限循环的函数,defer 将永远不会执行。

自 2017.1 版本起可用

默认:开启。

软件包文档:SA5003

SA5004:'for { select { ...' 带有空 default 分支会产生 spin

自 2017.1 版本起可用

默认:开启。

软件包文档:SA5004

SA5005:finalizer 引用被 finalizer 的对象,阻止垃圾回收

finalizer 是与对象关联的函数,当垃圾回收器准备好收集该对象时运行,即当对象不再被任何东西引用时。

然而,如果 finalizer 引用了该对象,它将始终作为对该对象的最终引用,阻止垃圾回收器收集该对象。finalizer 将永远不会运行,对象也将永远不会被收集,导致内存泄漏。这就是为什么 finalizer 应该使用其第一个参数来操作对象。这样,在将对象传递给 finalizer 之前,引用的数量可以暂时变为零。

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA5005": true} 来启用。

软件包文档:SA5005

SA5007:无限递归调用

递归调用自身的函数需要有退出条件。否则,它将无限递归,直到系统内存耗尽。

此问题可能由简单的 bug 引起,例如忘记添加退出条件。它也可以“故意”发生。某些语言具有尾调用优化,使得某些无限递归调用是安全的。然而,Go 不实现 TCO,因此应该使用循环。

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA5007": true} 来启用。

软件包文档:SA5007

SA5008:无效的结构标签

自 2019.2 版本起可用

默认:开启。

软件包文档:SA5008

SA5010:不可能的类型断言

某些类型断言可以被静态证明是不可能的。当类型断言的两个参数的方法集发生冲突时,例如通过包含具有不同签名的相同方法。

Go 编译器在从接口值断言到具体类型时已应用此检查。如果具体类型缺少接口中的方法,或者函数签名不匹配,则类型断言永远无法成功。

此检查在从一个接口断言到另一个接口时应用相同的逻辑。如果两个接口类型包含相同的方法但具有不同的签名,那么类型断言也永远无法成功。

自 2020.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA5010": true} 来启用。

软件包文档:SA5010

SA5011:可能出现 nil 指针解引用

指针被无条件解引用,同时也在另一个地方进行了 nil 检查。这表明指针可能是 nil,解引用它可能会导致 panic。这通常是由于代码顺序不当或缺少 return 语句造成的。请考虑以下示例

func fn(x *int) {
    fmt.Println(*x)

    // This nil check is equally important for the previous dereference
    if x != nil {
        foo(*x)
    }
}

func TestFoo(t *testing.T) {
    x := compute()
    if x == nil {
        t.Errorf("nil pointer received")
    }

    // t.Errorf does not abort the test, so if x is nil, the next line will panic.
    foo(*x)
}

Staticcheck 尝试推断哪些函数会中止控制流。例如,它知道函数在调用 panic 或 log.Fatal 后不会继续执行。然而,有时这种检测会失败,特别是在有条件的情况下。请考虑以下示例

func Log(msg string, level int) {
    fmt.Println(msg)
    if level == levelFatal {
        os.Exit(1)
    }
}

func Fatal(msg string) {
    Log(msg, levelFatal)
}

func fn(x *int) {
    if x == nil {
        Fatal("unexpected nil pointer")
    }
    fmt.Println(*x)
}

Staticcheck 会标记 x 的解引用,尽管它完全安全。Staticcheck 无法推断出对 Fatal 的调用会退出程序。目前,最简单的解决方法是修改 Fatal 的定义,如下所示

func Fatal(msg string) {
    Log(msg, levelFatal)
    panic("unreachable")
}

我们还硬编码了来自常见日志记录包(如 logrus)的函数。如果我们缺少对流行包的支持,请提交一个 issue。

自 2020.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA5011": true} 来启用。

软件包文档:SA5011

SA5012:将奇数大小的 slice 传递给期望偶数大小的函数

一些接受 slice 作为参数的函数期望 slice 具有偶数个元素。通常,这些函数将 slice 中的元素视为对。例如,strings.NewReplacer 接受成对的旧字符串和新字符串,用奇数个元素调用它将是错误的。

自 2020.2 版本起可用

默认:关闭。通过设置 "analyses": {"SA5012": true} 来启用。

软件包文档:SA5012

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA6000": true} 来启用。

软件包文档:SA6000

SA6001:通过字节 slice 索引 map 时错过了优化机会

Map 键必须是可比较的,这排除了字节 slice 的使用。这通常导致使用字符串键并将字节 slice 转换为字符串。

通常,将字节 slice 转换为字符串需要复制数据并产生分配。然而,编译器会识别 m[string(b)] 并直接使用 b 的数据,而无需复制它,因为它知道在 map 查找期间数据不会改变。这导致了反直觉的情况,即

k := string(b)
println(m[k])
println(m[k])

将比以下方式效率低

println(m[string(b)])
println(m[string(b)])

因为第一种版本需要复制和分配,而第二种则不需要。

有关此优化的历史记录,请查看 Go 存储库中的提交 f5f5a8b6209f84961687d993b93ea0d397f5d5bf。

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA6001": true} 来启用。

软件包文档:SA6001

SA6002:将非指针值存储在 sync.Pool 中会分配内存

sync.Pool 用于避免不必要的分配并减少垃圾回收器的工作量。

当将非指针值传递给接受接口的函数时,需要将该值放在堆上,这意味着额外的分配。Slice 是放入 sync.Pool 的常见项,它们是具有 3 个字段(length、capacity 和指向数组的指针)的结构。为了避免额外的分配,应存储指向 slice 的指针。

有关此问题的讨论,请参阅 https://go-review.googlesource.com/c/go/+/24371 上的注释。

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA6002": true} 来启用。

软件包文档:SA6002

SA6003:在 range 之前将字符串转换为 rune slice

您可能想循环遍历字符串中的 rune。与其将字符串转换为 rune slice 并循环遍历它,不如直接循环遍历字符串本身。即,

for _, r := range s {}

for _, r := range []rune(s) {}

将产生相同的值。然而,第一种方法将更快,并避免不必要的内存分配。

请注意,如果您对索引感兴趣,range 字符串和 range rune slice 会产生不同的索引。前者产生字节偏移量,而后者产生 rune slice 中的索引。

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA6003": true} 来启用。

软件包文档:SA6003

SA6005:使用 strings.ToLower 或 strings.ToUpper 进行低效的字符串比较

将两个字符串转换为相同的大小写并像这样进行比较

if strings.ToLower(s1) == strings.ToLower(s2) {
    ...
}

比使用 strings.EqualFold(s1, s2) 进行比较要昂贵得多。这是由于内存使用以及计算复杂性。

strings.ToLower 需要为新字符串分配内存,并且会完全转换两个字符串,即使它们在第一个字节就不同。另一方面,strings.EqualFold 一次比较一个字符。它不需要创建两个中间字符串,并且可以在找到第一个不匹配的字符后立即返回。

有关此问题的更深入解释,请参阅 https://blog.digitalocean.com/how-to-efficiently-compare-strings-in-go/

自 2019.2 版本起可用

默认:开启。

软件包文档:SA6005

SA6006:使用 io.WriteString 写入 []byte

使用 io.WriteString 写入字节 slice,例如

io.WriteString(w, string(b))

是不必要的且低效的。从 []byte 到 string 的转换必须分配和复制数据,而我们可以简单地使用 w.Write(b) 代替。

自 2024.1 版本起可用

默认:开启。

软件包文档:SA6006

SA9001:range 循环中的 defer 可能不会按预期运行

自 2017.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA9001": true} 来启用。

软件包文档:SA9001

SA9002:使用非八进制的 os.FileMode,看起来像是有意使用八进制。

自 2017.1 版本起可用

默认:开启。

软件包文档:SA9002

SA9003:if 或 else 分支中的空主体

自 2017.1 版本起可用,非默认

默认:关闭。通过设置 "analyses": {"SA9003": true} 来启用。

软件包文档:SA9003

SA9004:只有第一个常量有显式类型

在常量声明中,例如

const (
    First byte = 1
    Second     = 2
)

常量 Second 的类型与常量 First 不同。此构造不应与

const (
    First byte = iota
    Second
)

混淆,其中 First 和 Second 确实具有相同的类型。类型仅在未为常量分配显式值时才传递。

因此,在声明具有显式值的枚举时,不要写

const (
      EnumFirst EnumType = 1
      EnumSecond         = 2
      EnumThird          = 3
)

类型的这种差异会导致各种令人困惑的行为和 bug。

变量声明中的错误类型

这种不正确的枚举最明显的问题表现为编译错误

package pkg

const (
    EnumFirst  uint8 = 1
    EnumSecond       = 2
)

func fn(useFirst bool) {
    x := EnumSecond
    if useFirst {
        x = EnumFirst
    }
}

编译失败,错误为

./const.go:11:5: cannot use EnumFirst (type uint8) as type int in assignment

丢失方法集

更微妙的问题发生在具有方法和可选接口的类型上。考虑以下

package main

import "fmt"

type Enum int

func (e Enum) String() string {
    return "an enum"
}

const (
    EnumFirst  Enum = 1
    EnumSecond      = 2
)

func main() {
    fmt.Println(EnumFirst)
    fmt.Println(EnumSecond)
}

这段代码将输出

an enum
2

因为 EnumSecond 没有显式类型,因此默认为 int。

自 2019.1 版本起可用

默认:开启。

软件包文档:SA9004

SA9005:尝试 marshal 一个没有公共字段或自定义 marshal 的 struct

encoding/json 和 encoding/xml 包仅对 struct 中的导出字段进行操作,不对未导出字段进行操作。尝试 (un)marshal 仅由未导出字段组成的 struct 通常是错误的。

此检查不会标记涉及定义了自定义 marshal 行为(例如通过 MarshalJSON 方法)的类型的调用。它也不会标记空 struct。

自 2019.2 版本起可用

默认:关闭。通过设置 "analyses": {"SA9005": true} 来启用。

软件包文档:SA9005

SA9006:固定大小整数值的可疑位移

将值移位超过其大小将始终清除该值。

例如

v := int8(42)
v >>= 8

将始终结果为 0。

此检查仅标记固定大小整数值的位移操作。也就是说,int、uint 和 uintptr 永远不会被标记,以避免在某些奇异但有效的位操作技巧中出现潜在的误报。

// Clear any value above 32 bits if integers are more than 32 bits.
func f(i int) int {
    v := i >> 32
    v = v << 32
    return i-v
}

自 2020.2 版本起可用

默认:开启。

软件包文档:SA9006

SA9007:删除不应删除的目录

几乎从不正确删除系统目录,如 /tmp 或用户的主目录。然而,错误地这样做可能相当容易,例如,错误地使用 os.TempDir 而不是 ioutil.TempDir,或者忘记在 os.UserHomeDir 的结果后面添加后缀。

编写

d := os.TempDir()
defer os.RemoveAll(d)

在您的单元测试中将对您系统的稳定性产生毁灭性影响。

此检查会标记尝试删除以下目录的尝试

  • os.TempDir
  • os.UserCacheDir
  • os.UserConfigDir
  • os.UserHomeDir

自 2022.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA9007": true} 来启用。

软件包文档:SA9007

SA9008:类型断言的 else 分支可能没有读取正确的值

在 if 语句中声明变量时(如 'if foo := ...; foo {'),相同的变量也将位于 else 分支的作用域中。这意味着在以下示例中

if x, ok := x.(int); ok {
    // ...
} else {
    fmt.Printf("unexpected type %T", x)
}

else 分支中的 x 将引用 x, ok :=; 中的 x,而不是正在进行类型断言的 x。类型断言失败的结果是要断言的类型的零值,因此 else 分支中的 x 始终具有值 0 和类型 int。

自 2022.1 版本起可用

默认:关闭。通过设置 "analyses": {"SA9008": true} 来启用。

软件包文档:SA9008

SA9009:无效的 Go 编译器指令

找到了一个潜在的 Go 编译器指令,但由于它以空格开头而无效。

自 2024.1 版本起可用

默认:开启。

软件包文档:SA9009

ST1000:不正确或缺失的包注释

包必须有一个根据 https://golang.ac.cn/wiki/CodeReviewComments#package-comments 中概述的准则格式化的包注释。

自 2019.1 版本起可用,非默认

默认:关闭。通过设置 "analyses": {"ST1000": true} 来启用。

软件包文档:ST1000

ST1001:不推荐使用点导入

不推荐在外部测试包之外使用点导入。

dot_import_whitelist 选项可用于白名单某些导入。

引用 Go 代码审查注释

import . 形式在测试中可能很有用,由于循环依赖关系,这些测试不能成为被测试包的一部分

package foo_test

import (
    "bar/testutil" // also imports "foo"
    . "foo"
)

在这种情况下,测试文件不能在 foo 包中,因为它使用了 bar/testutil,它导入了 foo。所以我们使用 import . 形式让文件假装属于 foo 包,即使它不是。除了这种情况之外,请不要在您的程序中使用 import .。它会使程序更难阅读,因为不清楚 Quux 这样的名称是当前包中的顶级标识符还是导入包中的。

自 2019.1 版本起可用

选项 dot_import_whitelist

默认:关闭。通过设置 "analyses": {"ST1001": true} 来启用。

软件包文档:ST1001

ST1003:选择的标识符不佳

标识符,例如变量名和包名,遵循一定的规则。

有关详情,请参阅以下链接

自 2019.1 版本起可用,非默认

选项 initialisms

默认:关闭。通过设置 "analyses": {"ST1003": true} 来启用。

软件包文档:ST1003

ST1005:错误字符串格式不正确

错误字符串遵循一组指南,以确保统一性和良好的可组合性。

引用 Go 代码审查注释

错误字符串不应大写(除非以专有名词或缩写开头)或以标点符号结尾,因为它们通常会跟随其他上下文显示。也就是说,使用 fmt.Errorf(“something bad”) 而不是 fmt.Errorf(“Something bad”),这样 log.Printf(“Reading %s: %v”, filename, err) 就可以在消息中间没有多余的大写字母的情况下进行格式化。

自 2019.1 版本起可用

默认:关闭。通过设置 "analyses": {"ST1005": true} 来启用。

软件包文档:ST1005

ST1006:选择的接收者名称不当

引用 Go 代码审查注释

方法接收者的名称应反映其身份;通常,其类型的单字母或双字母缩写就足够了(例如,“c”或“cl”代表“Client”)。不要使用泛称名称,如“me”、“this”或“self”,这些标识符是面向对象语言的典型用法,它们更侧重于方法而不是函数。名称不必像方法参数那样具有描述性,因为它的作用是显而易见的,并且没有文档目的。它可以非常简短,因为它将出现在类型的几乎所有方法的每一行上;熟悉程度允许简短。也要保持一致:如果你在一个方法中将接收者称为“c”,那么在另一个方法中就不要称其为“cl”。

自 2019.1 版本起可用

默认:关闭。通过设置 "analyses": {"ST1006": true} 来启用。

软件包文档:ST1006

ST1008:函数的错误值应为其最后一个返回值

函数的错误值应为其最后一个返回值。

自 2019.1 版本起可用

默认:关闭。通过设置 "analyses": {"ST1008": true} 来启用。

软件包文档:ST1008

ST1011:time.Duration 类型变量的名称选择不当

time.Duration 值表示一段时间,该时间表示为纳秒计数。像 5 * time.Microsecond 这样的表达式会产生 5000 的值。因此,不适合用任何时间单位(如 Msec 或 Milli)来后缀 time.Duration 类型的变量。

自 2019.1 版本起可用

默认:关闭。通过设置 "analyses": {"ST1011": true} 来启用。

软件包文档:ST1011

ST1012:错误变量的名称选择不当

作为 API 的一部分的错误变量应命名为 errFoo 或 ErrFoo。

自 2019.1 版本起可用

默认:关闭。通过设置 "analyses": {"ST1012": true} 来启用。

软件包文档:ST1012

ST1013:应使用常量代替 HTTP 错误代码的魔术数字

HTTP 有大量的状态码。虽然其中一些是众所周知的(200、400、404、500),但大多数都不是。net/http 包为所有属于各种规范的状态码提供了常量。建议使用这些常量而不是硬编码魔术数字,以极大地提高代码的可读性。

自 2019.1 版本起可用

选项 http_status_code_whitelist

默认:关闭。通过设置 "analyses": {"ST1013": true} 来启用。

软件包文档:ST1013

ST1015:switch 的 default case 应为第一个或最后一个 case

自 2019.1 版本起可用

默认:关闭。通过设置 "analyses": {"ST1015": true} 来启用。

软件包文档:ST1015

ST1016:使用一致的方法接收者名称

自 2019.1 版本起可用,非默认

默认:关闭。通过设置 "analyses": {"ST1016": true} 来启用。

软件包文档:ST1016

ST1017:不要使用 Yoda 条件

Yoda 条件是“if 42 == x”之类的条件,其中字面量位于比较的左侧。这些是分配是表达式的语言中常见的惯用法,用于避免“if (x = 42)”之类的错误。在 Go 中,不允许这种错误,我们更喜欢更惯用的“if x == 42”。

自 2019.2 版本起可用

默认:关闭。通过设置 "analyses": {"ST1017": true} 来启用。

软件包文档:ST1017

ST1018:避免在字符串字面量中使用零宽度和控制字符

自 2019.2 版本起可用

默认:关闭。通过设置 "analyses": {"ST1018": true} 来启用。

软件包文档:ST1018

ST1019:多次导入同一个包

Go 允许多次导入同一个包,只要使用不同的导入别名即可。也就是说,下面的代码是有效的

import (
    "fmt"
    fumpt "fmt"
    format "fmt"
    _ "fmt"
)

然而,这种情况很少是故意的。通常,这是由于重构代码时意外添加了重复的导入语句。它也是一项鲜为人知的特性,这可能会导致混淆。

请注意,有时此功能可能会被有意使用(例如,请参阅 https://github.com/golang/go/commit/3409ce39bfd7584523b7a8c150a310cea92d879d)——如果您想在代码库中允许此模式,建议禁用此检查。

自 2020.1 版本起可用

默认:关闭。通过设置 "analyses": {"ST1019": true} 来启用。

软件包文档:ST1019

ST1020:导出函数的文档应以函数名开头

文档注释最好是完整的句子,这样可以支持多种自动呈现方式。第一句应为单句摘要,以声明的名称开头。

如果每个文档注释都以其描述的项目名称开头,则可以使用 go 工具的 doc 子命令并对输出进行 grep。

有关如何编写良好文档的更多信息,请参阅 https://golang.ac.cn/doc/effective_go#commentary

自 2020.1 版本起可用,非默认

默认:关闭。通过设置 "analyses": {"ST1020": true} 来启用。

软件包文档:ST1020

ST1021:导出类型的文档应以类型名开头

文档注释最好是完整的句子,这样可以支持多种自动呈现方式。第一句应为单句摘要,以声明的名称开头。

如果每个文档注释都以其描述的项目名称开头,则可以使用 go 工具的 doc 子命令并对输出进行 grep。

有关如何编写良好文档的更多信息,请参阅 https://golang.ac.cn/doc/effective_go#commentary

自 2020.1 版本起可用,非默认

默认:关闭。通过设置 "analyses": {"ST1021": true} 来启用。

软件包文档:ST1021

ST1022:导出变量或常量的文档应以变量名开头

文档注释最好是完整的句子,这样可以支持多种自动呈现方式。第一句应为单句摘要,以声明的名称开头。

如果每个文档注释都以其描述的项目名称开头,则可以使用 go 工具的 doc 子命令并对输出进行 grep。

有关如何编写良好文档的更多信息,请参阅 https://golang.ac.cn/doc/effective_go#commentary

自 2020.1 版本起可用,非默认

默认:关闭。通过设置 "analyses": {"ST1022": true} 来启用。

软件包文档:ST1022

ST1023:变量声明中的冗余类型

自 2021.1 版本起可用,非默认

默认:关闭。通过设置 "analyses": {"ST1023": true} 来启用。

软件包文档:ST1023

appends:检查 append 后缺少值

此检查器报告调用 append 时没有传递要附加到切片的值。

s := []string{"a", "b", "c"}
_ = append(s)

此类调用始终是空操作,并且经常表明存在潜在的错误。

默认:开启。

软件包文档:appends

asmdecl:报告汇编文件与 Go 声明之间的不匹配

默认:开启。

软件包文档:asmdecl

assign:检查无用的赋值

此检查器报告 x = x 或 a[i] = a[i] 形式的赋值。这些几乎总是无用的,即使它们不是无用的,通常也是一个错误。

默认:开启。

软件包文档:assign

atomic:检查使用 sync/atomic 包的常见错误

atomic 检查器查找形式为

x = atomic.AddUint64(&x, 1)

的赋值语句,这些语句不是原子的。

默认:开启。

软件包文档:atomic

atomicalign:检查对 sync/atomic 函数的非 64 位对齐参数

默认:开启。

软件包文档:atomicalign

bools:检查涉及布尔运算符的常见错误

默认:开启。

软件包文档:bools

buildtag:检查 //go:build 和 // +build 指令

默认:开启。

软件包文档:buildtag

cgocall:检测 cgo 指针传递规则的某些违规行为

检查无效的 cgo 指针传递。这会查找使用 cgo 调用 C 代码的代码,传递的值类型几乎总是与 cgo 指针共享规则不符。具体来说,它会警告尝试将 Go 的 chan、map、func 或 slice 传递给 C,无论是直接传递、通过指针、数组还是结构体传递。

默认:开启。

软件包文档:cgocall

composites:检查未键入的复合字面量

此分析器会报告一个诊断,用于从其他包导入的结构体类型的复合字面量,这些字面量未使用字段键入的语法。此类字面量很脆弱,因为向结构体添加新字段(即使是未导出的)也会导致编译失败。

例如,

err = &net.DNSConfigError{err}

应替换为

err = &net.DNSConfigError{Err: err}

默认:开启。

软件包文档:composites

copylocks:检查被错误地按值复制的锁

无意中复制包含锁(如 sync.Mutex 或 sync.WaitGroup)的值可能会导致两个副本都发生故障。通常,此类值应通过指针引用。

默认:开启。

软件包文档:copylocks

deepequalerrors:检查对错误值调用 reflect.DeepEqual

deepequalerrors 检查器查找形式为

reflect.DeepEqual(err1, err2)

的调用,其中 err1 和 err2 是错误。不鼓励使用 reflect.DeepEqual 来比较错误。

默认:开启。

软件包文档:deepequalerrors

defers:报告 defer 语句中的常见错误

defers 分析器在 defer 语句会导致 time.Since 调用非 defer 时报告诊断,因为经验表明这几乎总是一个错误。

例如

start := time.Now()
...
defer recordLatency(time.Since(start)) // error: call to time.Since is not deferred

正确的代码是

defer func() { recordLatency(time.Since(start)) }()

默认:开启。

软件包文档:defers

deprecated:检查已弃用的标识符的使用情况

deprecated 分析器查找已弃用的符号和包导入。

要了解 Go 对已弃用标识符的文档记录和信号约定,请参阅 https://golang.ac.cn/wiki/Deprecated

默认:开启。

软件包文档:deprecated

directive:检查 Go 工具链指令,如 //go:debug

此分析器检查包目录中所有 Go 源文件中的已知 Go 工具链指令的问题,即使是那些被 //go:build 约束排除的文件,以及所有非 Go 源文件。

对于 //go:debug(请参阅 https://golang.ac.cn/doc/godebug),分析器会检查指令是否仅放置在 Go 源文件中,仅放在包注释上方,并且仅在 package main 或 *_test.go 文件中。

将来可能会支持其他已知指令。

此分析器不检查 //go:build,该检查由 buildtag 分析器处理。

默认:开启。

软件包文档:directive

embed:检查 //go:embed 指令的使用情况

此分析器会检查是否导入了 embed 包(如果存在 //go:embed 指令),并提供一个建议的修复方法来添加缺失的导入(如果缺失)。

此分析器还检查 //go:embed 指令是否出现在单个变量声明之前。

默认:开启。

软件包文档:embed

errorsas:报告将非指针或非错误值传递给 errors.As

errorsas 分析器报告 errors.As 的调用,其中第二个参数的类型不是指向实现 error 的类型的指针。

默认:开启。

软件包文档:errorsas

fillreturns:建议修复由于返回值的数量不正确而导致的错误

此检查器为“错误的返回值数量(需要 %d,得到 %d)”类型的类型错误提供建议的修复。例如

func m() (int, string, *bool, error) {
    return
}

将变为

func m() (int, string, *bool, error) {
    return 0, "", nil, nil
}

此功能与 https://github.com/sqs/goreturns 类似。

默认:开启。

软件包文档:fillreturns

framepointer:报告在保存之前破坏了帧指针的汇编

默认:开启。

软件包文档:framepointer

gofix:应用基于 go:fix 注释指令的修复

gofix 分析器会内联标记为内联的函数和常量。

函数

给定一个标记为内联的函数,如下所示

//go:fix inline
func Square(x int) int { return Pow(x, 2) }

此分析器将建议在此函数调用处(在同一包或其他包中)进行内联。

内联可用于移除对已弃用函数的依赖

// Deprecated: prefer Pow(x, 2).
//go:fix inline
func Square(x int) int { return Pow(x, 2) }

它还可以用于移除对过时包的依赖,例如当导入路径发生更改或提供更高主版本时

package pkg

import pkg2 "pkg/v2"

//go:fix inline
func F() { pkg2.F(nil) }

将调用 pkg.F() 替换为 pkg2.F(nil) 可能不会对程序产生任何影响,因此此机制提供了一种低风险的方式来更新大量的调用。我们建议在可能的情况下,根据新 API 来表达旧 API,以实现自动迁移。

inliner 会小心处理,以避免行为更改,即使是细微的更改,例如参数表达式的求值顺序更改。当它无法安全地消除所有参数变量时,它可能会引入一个“绑定声明”,形式为

var params = args

以正确的顺序求值参数表达式并将它们绑定到参数变量。由于生成的代码转换在风格上可能不是最优的,因此可以通过指定 -gofix.allow_binding_decl=false 标志给分析器驱动程序来禁用此类内联。

(在无法安全地“缩减”调用——即用适当替换的函数 f 的主体替换调用 f(x)——的情况下,inliner 机制能够将 f 替换为函数字面量 func(){…}()。但是,gofix 分析器会无条件地丢弃所有此类“字面化”,同样是出于风格原因。)

常量

给定一个标记为内联的常量,如下所示

//go:fix inline
const Ptr = Pointer

此分析器将建议将 Ptr 的用法替换为 Pointer。

与函数一样,内联可用于替换已弃用的常量和过时包中的常量。

仅当常量定义引用另一个命名常量时,它才能标记为内联。

“//go:fix inline”注释必须出现在单个 const 声明的上方,如上所示;在 const 声明是组的一部分时,如本例所示

const (
   C = 1
   //go:fix inline
   Ptr = Pointer
)

或在组上方,应用于组中的每个常量

//go:fix inline
const (
    Ptr = Pointer
    Val = Value
)

该提案 https://golang.ac.cn/issue/32816 引入了“//go:fix”指令。

您可以使用此(不受官方支持)命令批量应用 gofix 修复

$ go run golang.org/x/tools/internal/gofix/cmd/gofix@latest -test ./...

(不要使用“go get -tool”将 gopls 添加为模块的依赖项;gopls 命令必须从其发布分支构建。)

默认:开启。

软件包文档:gofix

hostport:检查传递给 net.Dial 的地址格式

此分析器会标记使用 fmt.Sprintf 生成网络地址字符串的代码,如下例所示

addr := fmt.Sprintf("%s:%d", host, 12345) // "will not work with IPv6"
...
conn, err := net.Dial("tcp", addr)       // "when passed to dial here"

分析器建议使用正确的方法,即调用 net.JoinHostPort,来修复这个问题

addr := net.JoinHostPort(host, "12345")
...
conn, err := net.Dial("tcp", addr)

对于“%s:%s”格式字符串,也会产生类似的诊断和修复。

默认:开启。

软件包文档:hostport

httpresponse:检查 HTTP 响应使用错误

使用 net/http 包的一个常见错误是在检查确定响应是否有效的错误之前,就 defer 调用关闭 http.Response Body。

resp, err := http.Head(url)
defer resp.Body.Close()
if err != nil {
    log.Fatal(err)
}
// (defer statement belongs here)

此检查器通过报告此类错误的诊断来帮助发现潜在的 nil 解引用错误。

默认:开启。

软件包文档:httpresponse

ifaceassert:检测不可能的接口到接口的类型断言

此检查器会标记类型断言 v.(T) 和相应的类型开关,其中 v 的静态类型 V 是一个接口,它不可能实现目标接口 T。当 V 和 T 包含同名但签名不同的方法时,会发生这种情况。示例

var v interface {
    Read()
}
_ = v.(io.Reader)

v 中的 Read 方法与 io.Reader 中的 Read 方法签名不同,因此此断言不可能成功。

默认:开启。

软件包文档:ifaceassert

infertypeargs:检查调用表达式中不必要的类型参数

如果类型参数可以从函数参数或其他类型参数推断出来,则可以从调用表达式中省略显式类型参数

func f[T any](T) {}

func _() {
    f[string]("foo") // string could be inferred
}

默认:开启。

软件包文档:infertypeargs

loopclosure:检查来自嵌套函数对循环变量的引用

此分析器报告函数字面量引用外部循环的迭代变量的位置,并且循环以某种方式(例如,使用 go 或 defer)调用该函数,使其可能比循环迭代寿命长,并可能观察到变量的错误值。

注意:迭代变量的生命周期可能比循环迭代长,这仅发生在 Go 版本 <=1.21 中。在 Go 1.22 及更高版本中,循环变量的生命周期已更改为为每次循环迭代创建一个新的迭代变量。(请参阅 go.dev/issue/60078。)

在此示例中,所有推迟的函数在循环完成后运行,因此它们都观察到 v 的最终值 [<go1.22]。

for _, v := range list {
    defer func() {
        use(v) // incorrect
    }()
}

一种修复方法是为循环的每次迭代创建一个新变量

for _, v := range list {
    v := v // new var per iteration
    defer func() {
        use(v) // ok
    }()
}

在 Go 1.22 版本之后,前面的两个 for 循环是等效的,并且都是正确的。

下一个示例使用 go 语句并具有类似的问题 [<go1.22]。此外,由于循环与 goroutine 并发更新 v,因此存在数据竞争。

for _, v := range elem {
    go func() {
        use(v)  // incorrect, and a data race
    }()
}

修复与之前相同。该检查器还报告 golang.org/x/sync/errgroup.Group 启动的 goroutine 中的问题。并行测试中常见的一种难以发现的此形式的变体

func Test(t *testing.T) {
    for _, test := range tests {
        t.Run(test.name, func(t *testing.T) {
            t.Parallel()
            use(test) // incorrect, and a data race
        })
    }
}

t.Parallel() 调用导致函数的其余部分与循环并发执行 [<go1.22]。

分析器仅在最后一个语句中报告引用,因为它不够深入,无法理解后续可能使引用无害的语句的效果。(“最后一个语句”是复合语句(如 if、switch 和 select)中递归定义的。)

参见:https://golang.ac.cn/doc/go_faq.html#closures_and_goroutines

默认:开启。

软件包文档:loopclosure

lostcancel:检查 context.WithCancel 返回的 cancel func 是否被调用

context.WithCancel、WithTimeout、WithDeadline 和 WithCancelCause 等变体返回的取消函数必须被调用,否则新的 context 将保持活动状态,直到其父 context 被取消。(background context 永远不会被取消。)

默认:开启。

软件包文档:lostcancel

maprange:检查 range 语句中对 maps.Keys 和 maps.Values 的不必要调用

考虑一个像这样写的循环

for val := range maps.Values(m) {
    fmt.Println(val)
}

这应该写成不调用 maps.Values

for _, val := range m {
    fmt.Println(val)
}

golang.org/x/exp/maps 返回切片而不是迭代器来表示 Keys/Values,但应同样移除不必要的调用

for _, key := range maps.Keys(m) {
    fmt.Println(key)
}

应重写为

for key := range m {
    fmt.Println(key)
}

默认:开启。

软件包文档:maprange

modernize:通过使用现代构造简化代码

此分析器报告了通过使用 Go 及其标准库的更现代特性来简化和澄清现有代码的机会。

每个诊断都提供了一个修复。我们的目的是让这些修复能够安全地批量应用,而不会改变程序的行为。在某些情况下,建议的修复可能不完美,并且可能导致(例如)未使用的导入或未使用的局部变量,从而导致构建失败。但是,这些问题通常很容易修复。我们认为任何行为改变程序的现代化程序都有严重错误,并将努力修复它。

要批量应用所有现代化修复,您可以使用以下命令

$ go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -fix -test ./...

(不要使用“go get -tool”将 gopls 添加为模块的依赖项;gopls 命令必须从其发布分支构建。)

如果工具警告存在冲突的修复,您可能需要多次运行它,直到它干净地应用了所有修复。此命令不是官方支持的接口,并且将来可能会更改。

应像往常一样在合并前审查此工具产生的更改。在某些情况下,循环可能会被简单的函数调用替换,导致循环内的注释被丢弃。可能需要人工判断以避免丢失有价值的注释。

modernize 报告的每个诊断都有一个特定的类别。(类别列在下面。)某些类别的诊断,例如“efaceany”(它会在安全的情况下将“interface{}”替换为“any”),数量特别多。分两步应用修复可能会减轻代码审查的负担,第一步仅包含类别为“efaceany”的修复,第二步包含所有其他修复。这可以通过使用 -category 标志来实现

$ modernize -category=efaceany  -fix -test ./...
$ modernize -category=-efaceany -fix -test ./...

modernize 诊断类别

  • forvar:移除因 go1.22 中循环的新语义而变得不必要的 x := x 变量声明。

  • slicescontains:将“for i, elem := range s { if elem == needle { …; break }}”替换为调用 slices.Contains,该函数已在 go1.21 中添加。

  • minmax:将 if/else 条件赋值替换为调用内置的 min 或 max 函数,这些函数已在 go1.21 中添加。

  • sortslice:将 sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }) 替换为调用 slices.Sort(s),该函数已在 go1.21 中添加。

  • efaceany:将 interface{} 替换为“any”类型,该类型已在 go1.18 中添加。

  • mapsloop:将围绕 m[k]=v map 更新的循环替换为调用 maps 包中的 Collect、Copy、Clone 或 Insert 函数之一,该函数已在 go1.21 中添加。

  • fmtappendf:将 []byte(fmt.Sprintf…) 替换为 fmt.Appendf(nil, …),该函数已在 go1.19 中添加。

  • testingcontext:将测试中的 context.WithCancel 用法替换为 t.Context,该函数已在 go1.24 中添加。

  • omitzero:将 omitempty 替换为结构体上的 omitzero,该选项已在 go1.24 中添加。

  • bloop:将基准测试中的“for i := range b.N”或“for range b.N”替换为“for b.Loop()”,并移除任何先前对 b.StopTimer、b.StartTimer 和 b.ResetTimer 的调用。

    B.Loop 故意规避了编译器优化,例如内联,以便基准测试不会完全被优化掉。但是,目前,它可能会在某些情况下导致基准测试变慢,因为它会增加分配;请参阅 https://golang.ac.cn/issue/73137

  • rangeint:将三子句“for i := 0; i < n; i++”循环替换为“for i := range n”,该语法已在 go1.22 中添加。

  • stringsseq:在“for range strings.Split(…)”中将 Split 替换为 go1.24 中更高效的 SplitSeq,或将 Fields 替换为 FieldSeq。

  • stringscutprefix:将某些 HasPrefix 后跟 TrimPrefix 的用法替换为 CutPrefix,该函数已添加到 strings 包中(go1.20)。

  • waitgroup:将 sync.WaitGroup 的旧复杂用法替换为 go1.25 中更简单的 WaitGroup.Go 方法。

默认:开启。

软件包文档:modernize

nilfunc:检查函数与 nil 之间的无用比较

无用比较类似于 f == nil,而不是 f() == nil。

默认:开启。

软件包文档:nilfunc

nilness:检查冗余或不可能的 nil 比较

nilness 检查器检查包中每个函数的控制流图,并报告 nil 指针解引用、退化 nil 指针和带有 nil 值的 panic。退化比较的形式为 x==nil 或 x!=nil,其中 x 被静态地已知为 nil 或非 nil。这些通常是错误的,尤其是在与错误相关的控制流中。检查带有 nil 值的 panic,因为它们无法通过

if r := recover(); r != nil {

此检查报告以下条件

if f == nil { // impossible condition (f is a function)
}

p := &v
...
if p != nil { // tautological condition
}

if p == nil {
    print(*p) // nil dereference
}

if p == nil {
    panic(p)
}

有时控制流可能非常复杂,导致错误难以发现。在下面的示例中,err.Error 表达式保证会 panic,因为在第一次返回后,err 必须为 nil。中间的循环只是一个干扰。

...
err := g.Wait()
if err != nil {
    return err
}
partialSuccess := false
for _, err := range errs {
    if err == nil {
        partialSuccess = true
        break
    }
}
if partialSuccess {
    reportStatus(StatusMessage{
        Code:   code.ERROR,
        Detail: err.Error(), // "nil dereference in dynamic method call"
    })
    return nil
}

默认:开启。

软件包文档:nilness

nonewvars:“:=”左侧没有新变量”的建议修复

此检查器为“:=”左侧没有新变量”类型的类型错误提供建议的修复。例如

z := 1
z := 2

将变为

z := 1
z = 2

默认:开启。

软件包文档:nonewvars

noresultvalues:意外返回值”的建议修复

此检查器为“不期望返回值”或“返回值的数量过多”类型的类型错误提供建议的修复。例如

func z() { return nil }

将变为

func z() { return }

默认:开启。

软件包文档:noresultvalues

printf:检查 Printf 格式字符串和参数的一致性

此检查适用于格式化函数(如 [fmt.Printf] 和 [fmt.Sprintf])的调用,以及任何检测到的这些函数的包装器(如 [log.Printf])。它报告各种错误,例如格式字符串中的语法错误以及动词与其参数之间的不匹配(数量和类型)。

有关格式运算符及其操作数类型的完整列表,请参阅 fmt 包的文档。

默认:开启。

软件包文档:printf

recursiveiter:检查低效的递归迭代器

当一个返回迭代器(iter.Seq 或 iter.Seq2)的函数在 range 语句的操作数中调用自身时,此分析器会发出报告,因为这效率低下。

在为递归数据类型(如树或链表)实现迭代器(例如 iter.Seq[T])时,很容易递归地对每个子元素的迭代器进行 range 操作。

这是一个二叉树上的简单迭代器的示例

type tree struct {
    value       int
    left, right *tree
}

func (t *tree) All() iter.Seq[int] {
    return func(yield func(int) bool) {
        if t != nil {
            for elem := range t.left.All() { // "inefficient recursive iterator"
                if !yield(elem) {
                    return
                }
            }
            if !yield(t.value) {
                return
            }
            for elem := range t.right.All() { // "inefficient recursive iterator"
                if !yield(elem) {
                    return
                }
            }
        }
    }
}

虽然它正确地枚举了树中的元素,但它隐藏了一个重大的性能问题——实际上是两个。考虑一个具有 N 个节点的平衡树。迭代根节点将导致 All 被调用一次,对树中的每个节点进行调用。当在叶节点上调用 yield(t.value) 时,这会导致一系列嵌套的活动 range-over-func 语句。

第一个性能问题是每个 range-over-func 语句通常需要堆分配一个变量,因此迭代树会分配与树中元素一样多的变量,总共 O(N) 次分配,所有这些都是不必要的。

第二个问题是,对叶子节点的每次 yield 调用都会使每个封闭的 range 循环接收一个值,然后它们会立即将其传递给各自的 yield 函数。这会导致每个元素有 log(N) 次动态 yield 调用,总共 O(N*log N) 次动态调用,而只需要 O(N) 次。

递归迭代器更好的实现策略是首先为您的递归数据类型定义“every”运算符,其中 every(f) 报告 f(x) 是否对数据类型中的每个元素 x 都为 true。对于我们的树,every 函数将是

func (t *tree) every(f func(int) bool) bool {
    return t == nil ||
        t.left.every(f) && f(t.value) && t.right.every(f)
}

然后迭代器可以简单地表示为该函数的平凡包装器

func (t *tree) All() iter.Seq[int] {
    return func(yield func(int) bool) {
        _ = t.every(yield)
    }
}

实际上,tree.All 计算 yield 对每个元素返回 true,如果 yield 返回 false 则短路,然后丢弃最终的布尔结果。

这具有更好的性能特征:它对树的每个元素进行一次动态调用,并且不进行任何堆分配。它也更清晰。

默认:开启。

软件包文档:recursiveiter

shadow:检查变量是否可能被意外屏蔽

此分析器检查被屏蔽的变量。被屏蔽的变量是在内部作用域中声明的变量,其名称和类型与外部作用域中的变量相同,并且内部变量在声明后被提及。

(此定义可以进行细化;该模块会产生太多误报,并且尚未默认启用。)

例如

func BadRead(f *os.File, buf []byte) error {
    var err error
    for {
        n, err := f.Read(buf) // shadows the function variable 'err'
        if err != nil {
            break // causes return of wrong value
        }
        foo(buf)
    }
    return err
}

默认:关闭。通过设置 "analyses": {"shadow": true} 来启用。

软件包文档:shadow

shift:检查等于或超过整数宽度的大小位移

默认:开启。

软件包文档:shift

sigchanyzer:检查非缓冲 os.Signal 通道

此检查器报告形式为

signal.Notify(c <-chan os.Signal, sig ...os.Signal),

其中 c 是一个非缓冲通道,可能会错过信号。

默认:开启。

软件包文档:sigchanyzer

simplifycompositelit:检查复合字面量简化

形式为

[]T{T{}, T{}}

将被简化为

[]T{{}, {}}

这是“gofmt -s”应用的简化之一。

此分析器会忽略生成代码。

默认:开启。

软件包文档:simplifycompositelit

simplifyrange:检查 range 语句简化

形式为

for x, _ = range v {...}

将被简化为

for x = range v {...}

形式为

for _ = range v {...}

将被简化为

for range v {...}

这是“gofmt -s”应用的简化之一。

此分析器会忽略生成代码。

默认:开启。

软件包文档:simplifyrange

simplifyslice:检查切片简化

形式为

s[a:len(s)]

将被简化为

s[a:]

这是“gofmt -s”应用的简化之一。

此分析器会忽略生成代码。

默认:开启。

软件包文档:simplifyslice

slog:检查无效的结构化日志调用

slog 检查器查找对 log/slog 包中的函数的调用,这些函数接受交替的键值对。它报告在键位置的参数既不是字符串也不是 slog.Attr 的调用,以及最后一个键缺少其值的调用。例如,它会报告

slog.Warn("message", 11, "k") // slog.Warn arg "11" should be a string or a slog.Attr

slog.Info("message", "k1", v1, "k2") // call to slog.Info missing a final value

默认:开启。

软件包文档:slog

sortslice:检查 sort.Slice 的参数类型

sort.Slice 需要一个切片类型的参数。检查传递给 sort.Slice 的 interface{} 值实际上是一个切片。

默认:开启。

软件包文档:sortslice

stdmethods:检查知名接口方法的签名

有时一个类型可能旨在满足一个接口,但由于其方法签名中的错误而未能满足。例如,此 WriteTo 方法的结果应为 (int64, error),而不是 error,才能满足 io.WriterTo

type myWriterTo struct{...}
func (myWriterTo) WriteTo(w io.Writer) error { ... }

此检查确保每个方法名与标准库中的几个知名接口方法之一匹配的方法都具有该接口的正确签名。

检查的方法名包括

Format GobEncode GobDecode MarshalJSON MarshalXML
Peek ReadByte ReadFrom ReadRune Scan Seek
UnmarshalJSON UnreadByte UnreadRune WriteByte
WriteTo

默认:开启。

软件包文档:stdmethods

stdversion:报告使用过新的标准库符号

stdversion 分析器报告对标准库中符号的引用,这些符号是在高于文件当前 Go 版本的新 Go 版本中引入的。(请记住,文件的 Go 版本由其 go.mod 文件中的 'go' 指令或文件顶部的 "//go:build go1.X" 构建标签定义。)

分析器不会为引用“过新”类型的“过新”字段或方法报告诊断,因为这可能导致误报,例如,当通过类型别名访问字段或方法时,该类型别名由 Go 版本约束保护。

默认:开启。

软件包文档:stdversion

stringintconv:检查 string(int) 转换

此检查器标记形式为 string(x) 的转换,其中 x 是整数(但不是 byte 或 rune)类型。此类转换不被推荐,因为它们会返回 Unicode 码点 x 的 UTF-8 表示,而不是像预期的那样返回 x 的十进制字符串表示。此外,如果 x 表示一个无效的码点,则无法在静态上拒绝转换。

对于打算使用码点的转换,请考虑将其替换为 string(rune(x))。否则,strconv.Itoa 及其等价物返回以所需基数表示的值的字符串表示。

默认:开启。

软件包文档:stringintconv

structtag:检查结构体字段标签是否符合 reflect.StructTag.Get

还报告与未导出字段一起使用的某些结构体标签(json、xml)。

默认:开启。

软件包文档:structtag

testinggoroutine:报告从测试启动的 goroutine 中调用 (*testing.T).Fatal

会突然终止测试的函数,例如 *testing.T 的 Fatal、Fatalf、FailNow 和 Skip{,f,Now} 方法,必须从测试 goroutine 本身调用。此检查器检测在测试启动的 goroutine 中发生的对这些函数的调用。例如

func TestFoo(t *testing.T) {
    go func() {
        t.Fatal("oops") // error: (*T).Fatal called from non-test goroutine
    }()
}

默认:开启。

软件包文档:testinggoroutine

tests:检查测试和示例的常见错误用法

tests 检查器会遍历 Test、Benchmark、Fuzzing 和 Example 函数,检查名称错误、签名错误以及记录不存在标识符的示例。

有关 Test、Benchmark 和 Example 的约定,请参阅 golang.org/pkg/testing 包的文档。

默认:开启。

软件包文档:tests

timeformat:检查 time.Format 或 time.Parse 与 2006-02-01 的调用

timeformat 检查器查找具有 2006-02-01(yyyy-dd-mm)格式的时间格式。在国际上,“yyyy-dd-mm”不常见于日历日期标准,因此很可能 intended 的是 2006-01-02(yyyy-mm-dd)。

默认:开启。

软件包文档:timeformat

unmarshal:报告将非指针或非接口值传递给 unmarshal

unmarshal 分析器报告 json.Unmarshal 等函数的调用,其中参数类型不是指针或接口。

默认:开启。

软件包文档:unmarshal

unreachable:检查不可达代码

unreachable 分析器查找由于 return 语句、panic 调用、无限循环或类似构造而导致执行永远无法到达的语句。

默认:开启。

软件包文档:unreachable

unsafeptr:检查 uintptr 到 unsafe.Pointer 的无效转换

unsafeptr 分析器报告将整数转换为指针时 unsafe.Pointer 的可能不正确的用法。从 uintptr 到 unsafe.Pointer 的转换无效,如果它暗示内存中有一个 uintptr 类型的字包含指针值,因为该字对堆栈复制和垃圾收集器是不可见的。

默认:开启。

软件包文档:unsafeptr

unusedfunc:检查未使用的函数、方法等

unusedfunc 分析器报告在声明本身之外从未引用的函数和方法。

如果一个函数是未导出的且未被引用(除了在其自身的声明内),则认为该函数未使用。

如果一个方法未导出,未被引用(除了在其自身的声明内),并且其名称与同一包中声明的某个接口类型的方法名称不匹配,则认为该方法未使用。

在某些情况下,该工具可能会报告误报,例如

  • 对于从另一个包使用 go:linkname 机制引用的未导出函数的声明,如果声明的文档注释也没有 go:linkname 注释。

    (此类代码无论如何都强烈不推荐:linkname 注释(如果必须使用的话)应同时用于声明和别名。)

  • 对于“runtime”包中的编译器内置函数,尽管从未引用,但编译器已知它们,并且是通过编译后的目标代码间接调用的。

  • 对于仅从汇编调用的函数。

  • 对于仅从其构建标签未在当前构建配置中选择的文件中调用的函数。

有关这些限制的讨论,请参阅 https://github.com/golang/go/issues/71686

unusedfunc 算法不如 golang.org/x/tools/cmd/deadcode 工具精确,但它的优点在于它在模块化分析框架内运行,可以在 gopls 中实现近乎实时的反馈。

unusedfunc 分析器还报告未使用的类型、变量和常量。枚举(使用 iota 定义的常量)被忽略,因为即使是未使用的值也必须保留才能保持逻辑顺序。

默认:开启。

软件包文档:unusedfunc

unusedparams:检查函数中未使用的参数

unusedparams 分析器检查函数以查看是否有任何未使用的参数。

为确保健全性,它会忽略

  • “地址已获取”函数,即作为值使用的函数,而不是直接调用;它们的签名可能需要符合 func 类型。
  • 导出的函数或方法,因为它们可能在另一个包中被地址获取。
  • 未导出的方法,其名称与同一包中声明的接口方法匹配,因为方法的签名可能需要符合接口类型。
  • 函数体为空或仅包含对 panic 的调用的函数。
  • 未命名参数,或命名为“_”(空白标识符)的参数。

分析器建议将参数名替换为“_”的修复方法,但在这种情况下,可以通过调用“重构:移除未使用的参数”代码操作来获得更深入的修复,该操作将完全消除参数以及所有相应的调用点参数,同时注意保留参数表达式中的任何副作用;请参阅 https://github.com/golang/tools/releases/tag/gopls%2Fv0.14

此分析器会忽略生成代码。

默认:开启。

软件包文档:unusedparams

unusedresult:检查某些函数调用的结果是否未使用

fmt.Errorf 等函数会返回一个结果且没有副作用,因此丢弃结果总是一个错误。其他函数可能会返回一个不能被忽略的错误,或者一个必须被调用的清理操作。此分析器在调用此类函数且结果被忽略时报告。

函数集可以通过标志控制。

默认:开启。

软件包文档:unusedresult

unusedvariable:检查未使用的变量并建议修复

默认:开启。

软件包文档:unusedvariable

unusedwrite:检查未使用的写入

分析器报告从未读取的对结构体字段和数组的写入实例。具体来说,当复制结构体对象或数组时,其元素会由编译器隐式复制,并且对该副本的任何元素写入都不会对原始对象产生任何影响。

例如

type T struct { x int }

func f(input []T) {
    for i, v := range input {  // v is a copy
        v.x = i  // unused write to field x
    }
}

另一个例子是关于非指针接收者

type T struct { x int }

func (t T) f() {  // t is a copy
    t.x = i  // unused write to field x
}

默认:开启。

软件包文档:unusedwrite

waitgroup:检查 sync.WaitGroup 的误用

此分析器检测在新的 goroutine 中误调用 (*sync.WaitGroup).Add 方法,导致 Add 与 Wait 发生竞争

// WRONG
var wg sync.WaitGroup
go func() {
        wg.Add(1) // "WaitGroup.Add called from inside new goroutine"
        defer wg.Done()
        ...
}()
wg.Wait() // (may return prematurely before new goroutine starts)

正确的代码是在启动 goroutine 之前调用 Add

// RIGHT
var wg sync.WaitGroup
wg.Add(1)
go func() {
    defer wg.Done()
    ...
}()
wg.Wait()

默认:开启。

软件包文档:waitgroup

yield:报告 yield 的调用,其中结果被忽略

yield 函数返回 false 后,调用者不应再次调用 yield 函数;通常迭代器应尽快返回。

此示例未能检查 yield 调用的结果,导致此分析器报告诊断

yield(1) // yield may be called again (on L2) after returning false
yield(2)

修正后的代码是这个

if yield(1) { yield(2) }

或者简单地

_ = yield(1) && yield(2)

忽略 yield 的结果并不总是错误。例如,这是一个有效的单元素迭代器

yield(1) // ok to ignore result
return

只有当返回 false 的 yield 调用可能后跟另一个调用时,这才是错误。

默认:开启。

软件包文档:yield


本文档的源代码可以在 golang.org/x/tools/gopls/doc 下找到。