Golang is full of tools to help us on developing securer, reliable, and useful apps. And there is a category that I would like to talk about: Static Analysis through Linters.
Linter is a tool that analyzes source code without the need to compile/run your app or install any dependencies. It will perform many checks in the static code (the code that you write) of your app.
It is useful to help software developers ensure coding styles, identify tech debt, small issues, bugs, and suspicious constructs. Helping you and your team in the entire development flow.
Linters are available for many languages, but let us take a look at the Golang ecosystem.
Disclaimer: The author of the story is the CTO at SourceLevel
Most linters analyzes the result of two phases:
Also known as tokenizing/scanning is the phase in which we convert the source code statements into tokens. So each keyword, constant, variable in our code will produce a token.
It will take the tokens produced in the previous phase and try to determine whether these statements are semantically correct.
In Golang we have scanner
, token
, parser
, and ast
(Abstract Syntax Tree) packages. Let's jump straight to a practical example by checking this simple snippet:
package main
func main() {
println("Hello, SourceLevel!")
}
Okay, nothing new here. Now we'll use Golang standard library packages to visualize the ast
generated by the code above:
import (
"go/ast"
"go/parser"
"go/token"
)
func main() {
// src is the input for which we want to print the AST.
src := `our-hello-world-code`
// Create the AST by parsing src.
fset := token.NewFileSet() // positions are relative to fset
f, err := parser.ParseFile(fset, "", src, 0)
if err != nil {
panic(err)
}
// Print the AST.
ast.Print(fset, f)
}
Now let's run this code and look at the generated AST:
0 *ast.File {
1 . Package: 2:1
2 . Name: *ast.Ident {
3 . . NamePos: 2:9
4 . . Name: "main"
5 . }
6 . Decls: []ast.Decl (len = 1) {
7 . . 0: *ast.FuncDecl {
8 . . . Name: *ast.Ident {
9 . . . . // Name content
16 . . . }
17 . . . Type: *ast.FuncType {
18 . . . . // Type content
23 . . . }
24 . . . Body: *ast.BlockStmt {
25 . . . . // Body content
47 . . . }
48 . . }
49 . }
50 . Scope: *ast.Scope {
51 . . Objects: map[string]*ast.Object (len = 1) {
52 . . . "main": *(obj @ 11)
53 . . }
54 . }
55 . Unresolved: []*ast.Ident (len = 1) {
56 . . 0: *(obj @ 29)
57 . }
58 }
As you can see, the AST describes the previous block in a struct called ast.File
which is compound by the following structure:
type File struct {
Doc *CommentGroup // associated documentation; or nil
Package token.Pos // position of "package" keyword
Name *Ident // package name
Decls []Decl // top-level declarations; or nil
Scope *Scope // package scope (this file only)
Imports []*ImportSpec // imports in this file
Unresolved []*Ident // unresolved identifiers in this file
Comments []*CommentGroup // list of all comments in the source file
}
To understand more about lexical scanning and how this struct is filled, I would recommend Rob Pike talk.
Using AST is possible to check the formatting, code complexity, bug risk, unused variables, and a lot more.
To format code in Golang, we can use the gofmt
package, which is already present in the installation, so you can run it to automatically indent and format your code. Note that it uses tabs for indentation and blanks for alignment.
Here is a simple snippet from Go by Examples unformatted:
package main
import "fmt"
func intSeq() func() int {
i := 0
return func() int {
i++
return i
}
}
func main() {
nextInt := intSeq()
fmt.Println(nextInt())
fmt.Println(nextInt())
fmt.Println(nextInt())
newInts := intSeq()
fmt.Println(newInts())
}
Then it will be formatted this way:
package main
import "fmt"
func intSeq() func() int {
i := 0
return func() int {
i++
return i
}
}
func main() {
nextInt := intSeq()
fmt.Println(nextInt())
fmt.Println(nextInt())
fmt.Println(nextInt())
newInts := intSeq()
fmt.Println(newInts())
}
So we can observe that import
earned an extra linebreak but the empty line after main
function declaration is still there. So we can assume that we shouldn’t transfer the responsibility of keeping your code readable to the gofmt
: consider it as a helper on accomplishing readable and maintainable code.
It’s highly recommended to run gofmt
before you commit your changes, you can even configure a precommit hook for that. If you want to overwrite the changes instead of printing them, you should use gofmt -w
.
gofmt
has a -s
as Simplify command, when running with this option it considers the following:
An array, slice, or map composite literal of the form:
[]T{T{}, T{}}
will be simplified to:
[]T{{}, {}}
A slice expression of the form:
s[a:len(s)]
will be simplified to:
s[a:]
A range of the form:
for x, _ = range v {...}
will be simplified to:
for x = range v {...}
Note that for this example, if you think that variable is important for other collaborators, maybe instead of just dropping it with _
I would recommend using _meaningfulName
instead.
A range of the form:
for _ = range v {...}
will be simplified to:
for range v {...}
Note that it could be incompatible with earlier versions of Go.
On some occasions, we can find ourselves trying different packages during implementation and just give up on using them. By using [goimports
package](https://pkg.go.dev/golang.org/x/tools/cmd/goimports) we can identify which packages are being imported and unreferenced in our code and also add missing ones:
go install golang.org/x/tools/cmd/goimports@latest
Then use it by running with -l
option to specify a path, in our case we’re doing a recursive search in the project:
go imports -l ./..
../my-project/vendor/github.com/robfig/cron/doc.go
So it identified that cron/doc
is unreferenced in our code and it’s safe to remove it from our code.
Linters can be also used to identify how complex your implementation is, using some methodologies as an example, let’s start by exploring ABC Metrics.
It’s common nowadays to refer to how large a codebase is by referring to the LoC (Lines of Code) it contains. To have an alternate metric to LoC, Jerry Fitzpatrick proposed a concept called ABC Metric, which are compounded by the following:
=
, *=
, /=
, %=
, +=
, <<=
, >>=
, &=
, ^=
, ++
, and --
?
, <
, >
, <=
, >=
, !=
, else
, and case
)Caution: This metric should not be used as a “score” to decrease, consider it as just an indicator of your codebase or current file being analyzed.
To have this indicator in Golang, you can use [abcgo
package](https://github.com/droptheplot/abcgo):
$ go get -u github.com/droptheplot/abcgo
$ (cd $GOPATH/src/github.com/droptheplot/abcgo && go install)
Give the following Golang snippet:
package main
import (
"fmt"
"os"
"my_app/persistence"
service "my_app/services"
flag "github.com/ogier/pflag"
)
// flags
var (
filepath string
)
func main() {
flag.Parse()
if flag.NFlag() == 0 {
printUsage()
}
persistence.Prepare()
service.Compare(filepath)
}
func init() {
flag.StringVarP(&filepath, "filepath", "f", "", "Load CSV to lookup for data")
}
func printUsage() {
fmt.Printf("Usage: %s [options]\n", os.Args[0])
fmt.Println("Options:")
flag.PrintDefaults()
os.Exit(1)
}
Then let’s analyze this example using abcgo
:
$ abcgo -path main.go
Source Func Score A B C
/tmp/main.go:18 main 5 0 5 1
/tmp/main.go:29 init 1 0 1 0
/tmp/main.go:33 printUsage 4 0 4 0
As you can see, it will print the Score based on each function
found in the file. This metric can help new collaborators identify files that a pair programming session would be required during the onboarding period.
Cyclomatic Complexity, on the other hand, besides the complex name, has a simple explanation: it calculates how many paths your code has.
It is useful to indicate that you may break your implementation in separate abstractions or give some code smells and insights.
To analyze our Golang code let use [gocyclo
package](https://github.com/fzipp/gocyclo):
$ go install github.com/fzipp/gocyclo/cmd/gocyclo@latest
Then let’s check the same piece of code that we’ve analyzed in the ABC Metrics section:
$ gocyclo main.go
2 main main main.go:18:1
1 main printUsage main.go:33:1
1 main init main.go:29:1
It also breaks the output based on the function name, so we can see that the main
function has 2 paths since we’re using if
conditional there.
To verify code style and patterns in your codebase, Golang already came with [golint](https://github.com/golang/lint)
installed. Which was a linter that offer no customization but it was performing recommended checks from the Golang development team. It was archived in mid-2021 and it is being recommended Staticcheck be used as a replacement.
Before Staticcheck was recommended, we had revive, which for me sounds more like a community alternative linter.
As revive states how different it is from archived golint
:
I think the extra point goes for revive at the point of creating custom rules or formatters. Wanna try it?
$ go install github.com/mgechev/revive@latest
Then you can run it with the following command:
$ revive -exclude vendor/... -formatter friendly ./...
I often exclude my vendor
directory since my dependencies are there.
If you want to customize the checks to be used, you can supply a configuration file:
# Ignores files with "GENERATED" header, similar to golint
ignoreGeneratedHeader = true
# Sets the default severity to "warning"
severity = "warning"
# Sets the default failure confidence. The semantics behind this property
# is that revive ignores all failures with a confidence level below 0.8.
confidence = 0.8
# Sets the error code for failures with severity "error"
errorCode = 0
# Sets the error code for failures with severity "warning"
warningCode = 0
# Configuration of the `cyclomatic` rule. Here we specify that
# the rule should fail if it detects code with higher complexity than 10.
[rule.cyclomatic]
arguments = [10]
# Sets the severity of the `package-comments` rule to "error".
[rule.package-comments]
severity = "error"
Then you should pass it on running revive
:
$ revive -exclude vendor/... -config revive.toml -formatter friendly ./...
As I’ve shown, you can use linters for many possibilities, you can also focus on:
Feel free to try new linters that I didn’t mention here, I’d recommend the archived repository awesome-go-linters.
To start, consider using gofmt
before each commit or whenever you remember to run, then try revive
. Which linters are you using?
Disclaimer: The author of the story is the CTO at SourceLevel.