
我想分享最近几天的结果,其中包括对一些大型Go项目的git历史的分析,目的是找到可以更正错误的提交,然后对其进行形式化以进行检测,以防它们出现在新代码中。
在本文的第二部分中,我们将考虑go-critic中的一些新诊断方法,这些方法可以查找包含错误可能性很高的代码。
搜寻过去的想法
要找到进行新代码检查的想法,您可以查看其他编程语言的静态分析器,可以手动检查开源代码以尝试在其中找到错误的代码模板,或者可以查看历史记录。
假设您正在检查Go-Files
项目。 启动lint(静态分析器)后,没有任何问题;对源文本进行审核也未带来很好的结果。 我应该急于将Go-Files
放在一边吗? 不完全是
在存在期间, Go-Files
具有许多易于检测的缺陷,但是现在它们已得到修复。 我们需要做的是开始分析git存储库的历史记录。

虫子在附近。
收集我们感兴趣的承诺
问题:
- 可能有很多提交
- 有时在同一修订版中,事情彼此无关
- 有时,对提交的评论与实际更改不符。
- 有些人低估了良好提交消息的使用(我们不要指责)
首先,我们希望减少在没有自动化的情况下必须处理的信息量。 可以通过增加样本大小(将更多的Go数据包下载到对照组)来解决假阴性反应的问题。
您可以用不同的方式对其进行分析,我只是为一组模板设置了git log --grep
,根据其提交消息的内容高估或低估了提交的分数。 如果提交过大,您可以立即将其丢弃,因为它很可能弄清楚发生了什么,将是不平凡的。
我还想补充一点,在更正或发现错误旁边,对补丁的描述通常不是很乐观,有时也不是很贴切:
- "We check length later, but we assumed it was always 1 bytes long. Not always the case. I'm a little depressed that this bug was there" - "Really fixed the bug now" - "WTF, the changes that actually fixed the bug for the accesspattern wasn't actually committed" - "how do i pronounce this damn project" ( traefic) - "damnit travis" - "one f*cking dot" (./.. => ./... .travis.yml)
最无用的“错误修复”是编辑语法错误或其他阻止项目甚至无法通过go build
编译的问题。 并非所有项目都使用CI或pre-commit挂钩 ,因此,有时这样的无效修订最终会在master分支中结束。
历史错误
让我们看一下在git日志的掩盖下发现的一些最有趣的错误。
返回nil而不是实际结果
gin-gonic / gin:修复错误,绑定bool失败时返回err :
if err == nil { field.SetBool(boolVal) } - return nil + return err }
go-pg / pg:AppendParam错误修复 :
return method.AppendValue(b, m.strct.Addr(), 1), true } - return nil, false + return b, false }
快速路径缺乏回报
gonum / gonum:修复了dswap快速路径中的错误 :
for i := 0; i < n; i++ { x[i], y[i] = y[i], x[i] } + return }
在没有wg.Add的情况下运行goroutines(1)
btcsuite / btcd:修复RPC服务器关闭中的几个错误 :
+s.wg.Add(1) go s.walletListenerDuplicator()
无效的逻辑条件
gorgonia / gorgonia:还修复了一些错误 :
-for i := 0; i > start; i++ { +for i := 0; i < start; i++ { retVal[i] = 0 }
src-d / go-git:管道/ idxfile:修复在MemoryIndex中搜索错误 :
-if low < high { +if low > high { break }
go-xorm / xorm:修复总和上的错误 :
-if !strings.Contains(colName, " ") && strings.Contains(colName, "(") { +if !strings.Contains(colName, " ") && !strings.Contains(colName, "(") {
btcsuite / btcd:服务器:修复了在filteradd上断开对等连接的错误 :
-if sp.filter.IsLoaded() { +if !sp.filter.IsLoaded() {
btcsuite / btcd:使用最大操作处理来修复反向测试错误 :
-if pop.opcode.value < OP_16 { +if pop.opcode.value > OP_16 { s.numOps++
通过值更新接收者(this / self)
经典版 在方法内部修改对象的副本,因此调用者将看不到预期的结果。
修复了堆栈/队列/双端队列错误(指针接收器) :
-func (s Stack) Push(x interface{}) { - s = append(s, x) +func (s *Stack) Push(x interface{}) { + *s = append(*s, x) }
更新循环中的对象副本
containous / traefik:将过滤后的任务分配给slice中包含的应用程序 :
-for _, app := range filteredApps { - app.Tasks = fun.Filter(func(task *marathon.Task) bool { +for i, app := range filteredApps { + filteredApps[i].Tasks = fun.Filter(func(task *marathon.Task) bool { return p.taskFilter(*task, app) }, app.Tasks).([]*marathon.Task)
容器/ traefik:添加用于安全包装的单元测试 :
-for _, routine := range p.routines { +for i := range p.routines { p.waitGroup.Add(1) - routine.stop = make(chan bool, 1) + p.routines[i].stop = make(chan bool, 1) Go(func() { - routine.goroutine(routine.stop) + p.routines[i].goroutine(p.routines[i].stop) p.waitGroup.Done() }) }
不使用包装功能的结果。
gorgonia / gorgonia:修复了Grad()中的一个微小的无关紧要的错误 :
if !n.isInput() { - errors.Wrapf(err, ...) - // return + err = errors.Wrapf(err, ...) + return nil, err }
使用make的工作不正确
有时候,年轻的地鼠会“ make([]T, count)
”,然后在循环内append
。 此处正确的选项是“ make([]T, 0, count)
”。
HouzuoGuo / tiedo:修复集合扫描错误 :
-ids := make([]uint64, benchSize) +ids := make([]uint64, 0)
上面的修复程序纠正了原始错误,但是在以下提交之一中已解决了一个缺陷:
-ids := make([]uint64, 0) +ids := make([]uint64, 0, benchSize)
但是某些操作(例如copy
或ScanSlice
可能需要“正确”的长度。
go-xorm / xorm:SumsInt的错误修复返回空片 :
-var res = make([]int64, 0, len(columnNames)) +var res = make([]int64, len(columnNames), len(columnNames)) if session.IsAutoCommit { err = session.DB().QueryRow(sqlStr, args...).ScanSlice(&res) } else {
很快,批评家会帮助您发现此类错误。
其他错误
枚举结果相当麻烦,我将其余的内容删除了。 这部分是可选的,因此您可以将其留待以后或大胆地继续。
我想要更多!
以下片段很有趣,尽管它纠正了错误,但可迭代键是使用用于可迭代值的相同名称来命名的。 新代码看起来不正确,可能会使下一个程序员感到困惑。
gonum / gonum:修复了子功能中的错误 :
-for _, el := range *s1 { +for el := range *s1 { if _, ok := (*s2)[el]; !ok { return false }
命名结果几乎是相同的普通变量,因此它们将根据相同的规则进行初始化。 指针的值将为nil
,如果要使用此对象,则需要显式初始化此指针(例如,通过new
)。
dgraph-io / dgraph:修复server / main.go中的nil指针引用错误 :
func (s *server) Query(ctx context.Context, - req *graph.Request) (resp *graph.Response, err error) { + req *graph.Request) (*graph.Response, error) { + resp := new(graph.Response)
处理错误时,大多数情况下最容易引起误解 。
btcsuite / btcd:区块链/索引器:修复索引器重组中的错误,以赶上 :
-block, err := btcutil.NewBlockFromBytes(blockBytes) +block, err = btcutil.NewBlockFromBytes(blockBytes) if err != nil { return err }
go-xorm / xorm:修复插入错误错误 :
-err = session.Rollback() +err1 := session.Rollback() +if err1 == nil { + return lastId, err +} +err = err1
gin-gonic / gin:修复了Run *函数中debugPrint的换行问题 :
-debugPrint("Listening and serving HTTP on %s", addr) +debugPrint("Listening and serving HTTP on %s\n", addr)
在下面的示例中实现互斥锁使我想到了编写检查的想法,该检查报告了*sync.Mutex
作为结构成员的使用情况。 Dave Cheney建议始终使用互斥量值 ,而不是指向它的指针。
tealeg / xlsx:修复加载电子表格时的错误 :
styleCache map[int]*Style `-` + lock *sync.RWMutex }
反对可疑代码
badCond
badCond
检查将发现可能不正确的逻辑表达式。
可疑逻辑表达式的示例:
- 结果始终是
true
还是false
- 该表达式以几乎与错误几乎无法区分的形式编写
此检查还适用于“ x == nil && x == y
”之类的操作。 一种简单的解决方案是将其重写为“ x == nil && y == nil
”。 该代码的可读性不会受到影响,但是在该代码中,lint不会看到任何可疑的东西。
这是badCond
可以发现的错误修复的badCond
:
-if err1 := f(); err != nil && err == nil { +if err1 := f(); err1 != nil && err == nil { err = err1 }
奖杯:
弱电
weakCond
与weakCond
相似,但是警报的置信度略低。
弱条件是不能充分完全覆盖输入的条件。
弱(不足)条件的一个很好的例子是检查切片是否为nil
,然后使用第一个元素。 这里的条件“ s != nil
”还不够。 正确的条件是“ len(s) != 0
”或其等效值:
func addRequiredHeadersToRedirectedRequests( req *http.Request, via []*http.Request) error { - if via != nil && via[0] != nil { + if len(via) != 0 && via[0] != nil {
奖杯:
dupArg
对于某些函数,传递与多个参数相同的值(或变量)没有多大意义。 通常,这表示复制/粘贴错误。
这种功能的一个例子是copy
。 表达式copy(xs, xs)
没有意义。
-if !bytes.Equal(i2.Value, i2.Value) { +if !bytes.Equal(i1.Value, i2.Value) { return fmt.Errorf("tries differ at key %x", i1.Key) }
奖杯:
dupCase
您可能知道在Go中不能在switch
使用重复的case
值。
以下示例无法编译 :
switch x := 0; x { case 1: fmt.Println("first") case 1: fmt.Println("second") }
编译错误:开关中的情况1重复。
但是,如果我们举一个更有趣的例子 :
one := 1 switch x := 1; x { case one: fmt.Println("first") case one: fmt.Println("second") }
变量允许重复的值。 此外, switch true
为错误的可能性更大:
switch x := 0; true { case x == 1: fmt.Println("first") case x == 1: fmt.Println("second") }
这是修复实际错误的示例:
switch { case m < n: // Upper triangular matrix. // . case m == n: // Diagonal matrix. // . -case m < n: // Lower triangular matrix. +case m > n: // Lower triangular matrix. // . }
奖杯:
dupBranchBody
条件语句(例如if/else
和switch
具有要执行的主体。 当满足条件时,控制权将转移到相关的操作单元。 当其他诊断程序验证这些块的条件不同时, dupBranchBody
检查可执行块本身是否完全不相同。
除非它是type switch
,否则条件语句中是否存在重复主体至少是可疑的。
-if s, err := r.ReceivePack(context.Background(), req); err != nil { - return s, err -} else { - return s, err -} +return r.ReceivePack(context.Background(), req)
有关以下代码的正确性的判断留给读者:
if field.IsMessage() || p.IsGroup(field) {
“ if field.IsString()
”内部的主体和关联的“ else
”内部是相同的。
奖杯:
caseOrder
type switch
内的所有类型type switch
按顺序排序,直到第一个兼容。 如果标记值的类型为*T
,并且实现了接口I
,则您需要在带有“ case I
” 的标签之前放置一个带有“ case *T
” 的标签,否则将仅执行“ case I
”,因为*T
与I
兼容(但是反之亦然)。
case string: res = append(res, NewTargetExpr(v).toExpr().(*expr)) -case Expr: - res = append(res, v.toExpr().(*expr)) case *expr: res = append(res, v) +case Expr: + res = append(res, v.toExpr().(*expr))
奖杯:
offBy1
单位错误非常普遍,甚至有自己的维基百科页面 。
if len(optArgs) > 1 { return nil, ErrTooManyOptArgs } -node = optArgs[1] +node = optArgs[0]
-last := xs[len(xs)] +last := xs[len(xs) - 1]
go-critic
对于发现此类错误的选择有限,但到目前为止,尚未将任何修复程序发送到公共存储库。 以下是在查看故事时发现的一些修复程序:
最后有点恐怖
go vet
对“ x != a || x != b
”等表达式进行了很好的检查。 似乎人们可能并不了解gometalinter
,但是go vet
几乎启动了所有人,对吗?
使用gogrep实用程序,我将一小段相似的表达式组合在一起,这些表达式仍在某些项目的主分支中。
对于最勇敢的猫
我建议考虑此列表并发送请求请求。
cloud.google.com/go/trace/trace_test.go:943:7: expectTraceOption != ((o2&1) != 0) || expectTraceOption != ((o3&1) != 0) github.com/Shopify/sarama/mocks/sync_producer_test.go:36:5: offset != 1 || offset != msg.Offset github.com/Shopify/sarama/mocks/sync_producer_test.go:44:5: offset != 2 || offset != msg.Offset github.com/docker/libnetwork/api/api_test.go:376:5: id1 != i2e(i2).ID || id1 != i2e(i3).ID github.com/docker/libnetwork/api/api_test.go:408:5: "sh" != epList[0].Network || "sh" != epList[1].Network github.com/docker/libnetwork/api/api_test.go:1196:5: ep0.ID() != ep1.ID() || ep0.ID() != ep2.ID() github.com/docker/libnetwork/api/api_test.go:1467:5: ep0.ID() != ep1.ID() || ep0.ID() != ep2.ID() github.com/docker/libnetwork/ipam/allocator_test.go:1261:5: len(indices) != len(allocated) || len(indices) != num github.com/esimov/caire/grayscale_test.go:27:7: r != g || r != b github.com/gogo/protobuf/test/bug_test.go:99:5: protoSize != mSize || protoSize != lenData github.com/gogo/protobuf/test/combos/both/bug_test.go:99:5: protoSize != mSize || protoSize != lenData github.com/gogo/protobuf/test/combos/marshaler/bug_test.go:99:5: protoSize != mSize || protoSize != lenData github.com/gogo/protobuf/test/combos/unmarshaler/bug_test.go:99:5: protoSize != mSize || protoSize != lenData github.com/gonum/floats/floats.go:65:5: len(dst) != len(s) || len(dst) != len(y) github.com/gonum/lapack/testlapack/dhseqr.go:139:7: wr[i] != h.Data[i*h.Stride+i] || wr[i] != h.Data[(i+1)*h.Stride+i+1] github.com/gonum/stat/stat.go:1053:5: len(x) != len(labels) || len(x) != len(weights) github.com/hashicorp/go-sockaddr/ipv4addr_test.go:659:27: sockaddr.IPPort(p) != test.z16_portInt || sockaddr.IPPort(p) != test.z16_portInt github.com/hashicorp/go-sockaddr/ipv6addr_test.go:430:27: sockaddr.IPPort(p) != test.z16_portInt || sockaddr.IPPort(p) != test.z16_portInt github.com/nats-io/gnatsd/server/monitor_test.go:1863:6: v.ID != c.ID || v.ID != r.ID github.com/nbutton23/zxcvbn-go/adjacency/adjcmartix.go:85:7: char != "" || char != " " github.com/openshift/origin/pkg/oc/cli/admin/migrate/migrator_test.go:85:7: expectedInfos != writes || expectedInfos != saves github.com/openshift/origin/test/integration/project_request_test.go:120:62: added != deleted || added != 1 github.com/openshift/origin/test/integration/project_request_test.go:126:64: added != deleted || added != 4 github.com/openshift/origin/test/integration/project_request_test.go:132:62: added != deleted || added != 1 gonum.org/v1/gonum/floats/floats.go:60:5: len(dst) != len(s) || len(dst) != len(y) gonum.org/v1/gonum/lapack/testlapack/dhseqr.go:139:7: wr[i] != h.Data[i*h.Stride+i] || wr[i] != h.Data[(i+1)*h.Stride+i+1] gonum.org/v1/gonum/stat/stat.go:1146:5: len(x) != len(labels) || len(x) != len(weights)
从别人的错误中学习

不,即使在最后一部分, 机器学习也一无所获。
但是这里要写的是有关golangci-lint的 ,该语言将go-critic集成在最新版本之一中。 golangci-lint
是golangci-lint
的类似物,您可以阅读它的优点,例如,在文章“ Go中的静态分析:如何在检查代码时节省时间”。
最近,使用lintpack重写了go-critic
。 可以在Go lintpack:可组合的linter管理器中找到详细信息。
如果您尚未开始主动使用工具来分析程序中的潜在错误(无论是样式错误还是逻辑错误),今天,我建议您再次与同事一起思考一下您喝茶的行为。 你会成功的。
对所有人都好。