I want to share the results of the work of the last few days, which included an analysis of the git history of some large Go projects with the aim of finding commits, which corrected errors and then formalized them for detection if they appeared in the new code.
In the second part of the article, we will look at some new diagnostics in the go-critic , which allow us to find code that is highly error-prone.
To find ideas for new code inspections, you can pry into static analyzers for other programming languages, you can manually explore open source code with an attempt to find erroneous code templates in it, or you can look into history.
Suppose you are testing a Go-Files
project. After launching the linter (static analyzer) the problems are not found, the audit of the source texts also did not bring much results. Should I hurry to put Go-Files
aside? Not really.
During the existence of the Go-Files
had some number of easily detected defects, but now they have already been fixed. What we need to do is start analyzing the git repository history.
Bugs somewhere nearby.
Problems:
First of all, we want to reduce the amount of information that will have to be processed without automation. The problem of false-negative responses can be solved by increasing the sample size (download more Go packets to the control group).
You can analyze it in different ways, it was enough for me to " git log --grep
" on a set of templates, with overstatement and understating of the weight (score) of the commit depending on the content of its commit message. If the commit is too huge, you can immediately drop it, because it is very likely to figure out what is happening there will be nontrivial.
I would also like to add that, along with corrections or finding errors, quite often there are not very optimistic and sometimes not quite cultural descriptions of patches:
- "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)
The most useless "bug fixes" are editing syntax errors or other problems that prevent the project from even go build
through the go build
. Not all projects use CI or pre-commit hooks , so such broken revisions sometimes fall into the master branch.
Let's go over some of the most interesting bugs that were found under a blanket of git logs.
gin-gonic / gin: fix bug, return err when failed binding bool :
if err == nil { field.SetBool(boolVal) } - return nil + return err }
go-pg / pg: AppendParam bug fix :
return method.AppendValue(b, m.strct.Addr(), 1), true } - return nil, false + return b, false }
gonum / gonum: fixed bug in dswap fast path :
for i := 0; i < n; i++ { x[i], y[i] = y[i], x[i] } + return }
btcsuite / btcd: Fix several bugs in the RPC server shutdown :
+s.wg.Add(1) go s.walletListenerDuplicator()
gorgonia / gorgonia: fixed a few bugs as well :
-for i := 0; i > start; i++ { +for i := 0; i < start; i++ { retVal[i] = 0 }
src-d / go-git: plumbing / idxfile: fix bug searching in MemoryIndex :
-if low < high { +if low > high { break }
go-xorm / xorm: fix bug on sum :
-if !strings.Contains(colName, " ") && strings.Contains(colName, "(") { +if !strings.Contains(colName, " ") && !strings.Contains(colName, "(") {
btcsuite / btcd: server: Fix bug disconnecting peer on filteradd :
-if sp.filter.IsLoaded() { +if !sp.filter.IsLoaded() {
btcsuite / btcd: Fix reversed test bug with the max operations handling :
-if pop.opcode.value < OP_16 { +if pop.opcode.value > OP_16 { s.numOps++
Classic. Inside the method, a copy of the object is modified, which is why the caller will not see the expected results.
Fixed a stack / queue / deque bug (pointer receiver) :
-func (s Stack) Push(x interface{}) { - s = append(s, x) +func (s *Stack) Push(x interface{}) { + *s = append(*s, x) }
containous / traefik: Assign filtered tasks to apps contained in 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)
containous / traefik: Add unit tests for package safe :
-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: Fixed a tiny nonconsequential bug in Grad () :
if !n.isInput() { - errors.Wrapf(err, ...) - // return + err = errors.Wrapf(err, ...) + return nil, err }
Sometimes young gophers make " make([]T, count)
" followed by append
inside the loop. The correct option here is " make([]T, 0, count)
".
HouzuoGuo / tiedot: fix a collection scan bug :
-ids := make([]uint64, benchSize) +ids := make([]uint64, 0)
The correction above corrects the original error, but has one flaw that was corrected in one of the following commits:
-ids := make([]uint64, 0) +ids := make([]uint64, 0, benchSize)
But some operations, such as copy
or ScanSlice
may require the "correct" length.
go-xorm / xorm: bug fix for SumsInt return empty slice :
-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 {
Soon go-critic will help in finding errors of this class.
The listing turns out to be rather cumbersome, the rest of which I carry under the spoiler. This part is optional, so you can leave it for later or safely move on.
The following fragment is interesting in that although it corrects the error, the iterated key is named with the same name that was used for the iterated value. The new code does not look correct and may confuse the next programmer.
gonum / gonum: Fixed bug in subset functions :
-for _, el := range *s1 { +for el := range *s1 { if _, ok := (*s2)[el]; !ok { return false }
Named results are practically the same ordinary variables, so they are initialized by the same rules. Pointers will have the value nil
and if you need to work with this object, you must explicitly initialize this pointer (for example, using new
).
dgraph-io / dgraph: fixing nil pointer reference bug in server / main.go :
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)
More often and most shadowing bites when dealing with errors.
btcsuite / btcd: blockchain / indexers: fix bug in indexer re-org catch up :
-block, err := btcutil.NewBlockFromBytes(blockBytes) +block, err = btcutil.NewBlockFromBytes(blockBytes) if err != nil { return err }
go-xorm / xorm: fix insert err bug :
-err = session.Rollback() +err1 := session.Rollback() +if err1 == nil { + return lastId, err +} +err = err1
gin-gonic / gin: Fixed newline problem with debugPrint in Run * functions :
-debugPrint("Listening and serving HTTP on %s", addr) +debugPrint("Listening and serving HTTP on %s\n", addr)
The introduction of the mutex in the example below led me to the idea of writing a validation check that reports the use of *sync.Mutex
as a member of the structure. Dave Cheney recommends that you always use the mutex value , not the pointer to it.
tealeg / xlsx: fix bug when loading spreads :
styleCache map[int]*Style `-` + lock *sync.RWMutex }
The badCond
check finds potentially incorrect logical expressions.
Examples of suspicious logical expressions:
true
or false
This check also works on operations like " x == nil && x == y
". One simple solution is to rewrite to " x == nil && y == nil
". The readability of the code does not suffer, but the linter will not see anything suspicious in this code.
Here is an example of fixing a bug that can be found badCond
:
-if err1 := f(); err != nil && err == nil { +if err1 := f(); err1 != nil && err == nil { err = err1 }
Trophies:
weakCond
is similar to badCond
, but the confidence level of warnings is somewhat lower.
A weak condition is a condition that does not sufficiently cover the input data.
A good example of a weak (insufficient) condition is to check a slice for nil
with the subsequent use of the first element. The " s != nil
" conditions are not enough. The correct condition is " len(s) != 0
" or its equivalent:
func addRequiredHeadersToRedirectedRequests( req *http.Request, via []*http.Request) error { - if via != nil && via[0] != nil { + if len(via) != 0 && via[0] != nil {
Trophies:
For some functions, passing the same value (or variable) as several arguments does not make much sense. Quite often this signals a copy / paste error.
An example of such a function is copy
. The expression copy(xs, xs)
does not make sense.
-if !bytes.Equal(i2.Value, i2.Value) { +if !bytes.Equal(i1.Value, i2.Value) { return fmt.Errorf("tries differ at key %x", i1.Key) }
Trophies:
You probably know that in Go you cannot use duplicate case
values inside a switch
.
The example below does not compile :
switch x := 0; x { case 1: fmt.Println("first") case 1: fmt.Println("second") }
Compile error: duplicate case 1 in switch.
But what if you take the example more interesting :
one := 1 switch x := 1; x { case one: fmt.Println("first") case one: fmt.Println("second") }
Through the variable to use duplicate values are allowed. In addition, the switch true
gives even more room for errors:
switch x := 0; true { case x == 1: fmt.Println("first") case x == 1: fmt.Println("second") }
Here is an example of correcting a real error:
switch { case m < n: // Upper triangular matrix. // . case m == n: // Diagonal matrix. // . -case m < n: // Lower triangular matrix. +case m > n: // Lower triangular matrix. // . }
Trophies:
In conditional statements such as if/else
and switch
there are bodies for execution. When the condition is fulfilled, control is transferred to the associated block of operations. While other diagnostics check that the conditions of these blocks are different, dupBranchBody
checks that the executable blocks themselves are not completely identical.
The presence of duplicate bodies within conditional statements, unless it is a type switch
, is at least suspicious.
-if s, err := r.ReceivePack(context.Background(), req); err != nil { - return s, err -} else { - return s, err -} +return r.ReceivePack(context.Background(), req)
The judgments about the degree of correctness of the code below are left to the readers:
if field.IsMessage() || p.IsGroup(field) { // . } else if field.IsString() { if nullable && !proto3 { p.generateNullableField(fieldname, verbose) } else { pP(`if this.`, fieldname, ` != that1.`, fieldname, `{`) } } else { if nullable && !proto3 { p.generateNullableField(fieldname, verbose) } else { pP(`if this.`, fieldname, ` != that1.`, fieldname, `{`) } } }
The bodies inside " if field.IsString()
" and its associated " else
" are identical.
Trophies:
All types inside the type switch
iterated sequentially to the first compatible one. If the tag value is of type *T
, and implements interface I
, then you need to put a label with " case *T
" up to the label with " case I
", otherwise only " case I
" will be executed, since *T
compatible with I
(but not the other way around).
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))
Trophies:
Error on the unit is so popular that it even has its own page on Wikipedia .
if len(optArgs) > 1 { return nil, ErrTooManyOptArgs } -node = optArgs[1] +node = optArgs[0]
-last := xs[len(xs)] +last := xs[len(xs) - 1]
There are limited possibilities to find such errors in the go-critic
, but so far no corrections have been sent to the public repository. Here are some corrections that were found when viewing the story:
go vet
has a good check for expressions like " x != a || x != b
". It would seem that people might not know about the gometalinter
, but almost everyone runs gometalinter
go vet
, right?
Using the gogrep utility , I compiled a small list of similar expressions that are still in the master branches of some projects.
I propose to consider this list and send pull requests.
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)
No, even in the last part there will be nothing about machine learning .
But what it will be written here about is golangci-lint , which integrated go-critic in one of the latest releases. golangci-lint
is an analogue of gometalinter , which you can read about the merits of, for example, in the article "Static analysis in Go: how we save time when checking code . "
The go-critic
recently rewritten using lintpack . For details, go to "Go lintpack: composable linters manager" .
If you have not yet begun to actively use the tools to analyze your programs for potential errors, both stylistic and logical, I recommend today to think again about your behavior while having a cup of tea with your colleagues. You will succeed.
All good.
Source: https://habr.com/ru/post/432848/
All Articles