State of Golang linters and the differences between them
Tags: go, linters, golint, staticcheck
Published: 2022-01-18 10:45:27

---
title: State of Golang linters and the differences between them
published: true
date: 2022-01-18 10:45:27 UTC
description: Curious about which linters are available in Golang ecosystem? Here's an introduction of how to get started.
tags: golang, linters, golint, staticcheck
canonical_url: https://sourcelevel.io/blog/state-of-golang-linters-and-the-differences-between-them
cover_image: https://sourcelevel.io/wp-content/uploads/golang_lint.jpeg
---

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**.

## What is a linter?

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](https://sourcelevel.io/linters-unleashing-the-power-of-static-code-analysis), but let us take a look at the Golang ecosystem.

## First things first: how do linters analyze code?

Most linters analyzes the result of two phases:

### Lexer

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.

### Parser

It will take the tokens produced in the previous phase and try to determine whether these statements are semantically correct.

### Golang packages

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:

```go
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:

```go
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 the generated AST:

```go
 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:

```go
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](https://www.youtube.com/watch?v=HxaD_trXwRE).

Using AST is possible to check the formatting, code complexity, bug risk, unused variables, and a lot more.

## Code Formatting

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](https://gobyexample.com/closures) unformatted:

```go
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:

```go
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](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) for that. If you want to overwrite the changes instead of printing them, you should use `gofmt -w`.

### Simplify option

`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:**

```go
	[]T{T{}, T{}}
```

will be simplified to:

```go
	[]T{{}, {}}
```

**A slice expression of the form:**

```go
s[a:len(s)]
```

will be simplified to:

```go
s[a:]
```

**A range of the form:**

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

will be simplified to:

```go
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:**

```go
for _ = range v {...}
```

will be simplified to:

```go
for range v {...}
```

Note that it could be incompatible with earlier versions of Go.

## Check unused imports

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
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
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.

## Code Complexity

Linters can be also used to identify how complex your implementation is, using some methodologies as example, let’s start by exploring ABC Metrics.

### 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](https://en.wikipedia.org/wiki/ABC_Software_Metric), which are compounded by the following:

- **(A) Assignment counts:** `=` , `*=` , `/=`, `%=`, `+=`, `<<=`, `>>=`, `&=`, `^=`, `++`, and `--`
- **(B) Branch counts when:** Function is called
- **(C) Conditionals counts:** Booleans or logic test (`?`, `<`, `>`, `<=`, `>=`, `!=`, `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):

```bash
$ go get -u github.com/droptheplot/abcgo
$ (cd $GOPATH/src/github.com/droptheplot/abcgo && go install)
```

Give the following Golang snippet:

```go
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`:

```bash
$ 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

[Cyclomatic Complexity](https://en.wikipedia.org/wiki/Cyclomatic_complexity) in another 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](https://en.wikipedia.org/wiki/Code_smell) and insights.

To analyze our Golang code let use `[gocyclo` package](https://github.com/fzipp/gocyclo):

```bash
$ 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:

```bash
$ 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 function name, so we can see that the `main` function has 2 paths since we’re using `if` conditional there.

## Style and Patterns Checking

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](https://staticcheck.io/) be used as a replacement.

### **Golint vs Staticcheck vs revive**

Before Staticcheck was recommended, we had [revive](https://revive.run/), which for me sounds more like a community alternative linter.

As revive states how different it is from archived `golint`:

- Allows us to enable or disable rules using a configuration file.
- Allows us to configure the linting rules with a TOML file.
- 2x faster running the same rules as golint.
- Provides functionality for disabling a specific rule or the entire linter for a file or a range of lines.
- **golint** allows this only for generated files.
- Optional type checking. Most rules in golint do not require type checking. If you disable them in the config file, revive will run over 6x faster than golint.
- Provides multiple formatters which let us customize the output.
- Allows us to customize the return code for the entire linter or based on the failure of only some rules.
- **Everyone can extend it easily with custom rules or formatters.**
- **Revive** provides more rules compared to **golint**.

### Testing revive linter

I think the extra point goes for **revive** at the point of creating custom rules or formatters. Wanna try it?

```bash
$ go install github.com/mgechev/revive@latest
```

Then you can run it with the following command:

```bash
$ 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:

```markdown
# 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`:

```markdown
$ revive -exclude vendor/... -config revive.toml -formatter friendly ./...
```

---

## What else?

As I’ve shown, you can use linters for many possibilities, you can also focus on:

- Performance
- Unused code
- Reports
- Outdated packages
- Code without tests (no coverage)
- Magic number detector

Feel free to try new linters that I didn’t mention here, I’d recommend the [archived repository awesome-go-linters](https://github.com/golangci/awesome-go-linters#linters).

## Where to start?

To start, consider using `gofmt` before each commit or whenever you remember to run, then try `revive`. Which linters are you using?