Linting Go Code
In this article, I will demonstrate how to use lint tools in Golang. A linter is a tool that checks code files using a set of rules that describe problems that cause confusion, produce unexpected results, or reduce the readability of the code. Furthermore, lint can help us find potential bugs in the code, such as assigning undeclared variables, which can cause runtime errors, or getting the value from a global variable, which makes debugging difficult, and so on.
Some go packages which can be used to perform linting such as:
- go vet detects suspicious constructs that the compile may skip, but it only catches a limited number of potential issues such as calling fmt.Printf with the wrong arguments.
- errcheck, An error checker
- golangCI-lint,  It’s a linting tool that provides a facade on top of many useful linters and formatters. Also, it allows running the linters in parallel to improve analysis speed, which is quite handy.
We can also use golint which is maintained by the Go developers. It is intended to enforce the coding conventions described in Effective Go. But for now, this project is frozen and deprecated. You can refer to 'freeze and deprecate golint thread' to read more.
Besides linters, we should also use code formatters to fix code style. Here is a list of some code formatters for you to try:
- gofmt package, which is already present in the installation, so we can run it to automatically indent and format your code. Note that it uses tabs for indentation and blanks for alignment
- goimports, A standard Go imports formatter
Using Golang Vet for linting
go vet
: Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string. Vet uses heuristics that do not guarantee all reports are genuine problems, but it can find errors not caught by the compilers.
Let's look at this below example to understand how Golang Vet works:
package main
import "fmt"
// print out "Anna got 99.5 points in the Maths exam"
func main() {
name := "Anna"
score := 99.5
// %s means the uninterpreted bytes of the string or slice while score is float64
fmt.Printf("%s got %s points in the Maths exam\n", name, score)
}
When we run 'go run main.go', the output will be:
Anna got %!s(float64=99.5) points in the Maths exam
When we run "go vet main.go", the output will be:
# command-line-arguments
.\milestone32.go:10:2: fmt.Printf format %s has arg score of wrong type float64
Using gofmt to for formatting
The gofmt
formatting don’t affect the execution of the code—rather, they improve codebase readability by ensuring that the code is visually consistent. gofmt
focuses on things like indentation, whitespace, comments, and general code succinctness. We have written a dedicate chapter on go formatting.
Example:
To check files for unnecessary parentheses:
gofmt -r '(a) -> a' -l *.go
To remove the parentheses:
gofmt -r '(a) -> a' -w *.go
Consider the below example to see how gofmt
format the code:
package main
import "fmt"
// print out "Anna got 99.5 points in the Maths exam"
func main() {
name := "Anna"
score := 99.5
fmt.Printf(("%s got %s points in the Maths exam\n", name, score))
}
The program will automatically format to this:
package main
import "fmt"
// print out "Anna got 99.5 points in the Maths exam"
func main() {
name := "Anna"
score := 99.5
fmt.Printf("%s got %s points in the Maths exam\n", name, score)
}
Using GolangCI-Lint to lint go program
Golangci-lint is a Go linters tool that runs linters in parallel, reuses the Go build cache, and caches analysis results for significantly improved performance on subsequent runs, is the preferred way to configure linting in Go projects
For convenience and performance reasons, the golangci-lint
project was created to aggregate and run several individual linters in parallel. When you install the program, it will include about 48 linters (at the time of writing), and you can then choose which ones are important for your project.
Set up environment
To install golangci-lint
, run the command below:
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
To see a list of supported linters and which linters are enabled/disabled:
golangci-lint help linters
Perform Test Run
You may encounter errors if you run the enabled linters from the root of your project directory. Each problem is reported with all of the context you need to fix it, including a brief description of the problem as well as the file and line number where it occurred.
golangci-lint run
By passing the path, you can specify which directories and files to analyze.
golangci-lint run dir1 dir2 dir3/main.go
Configuring GolangCI-Lint
The config file has lower priority than command-line options. If the same bool/string/int option is provided on the command-line and in the config file, the option from command-line will be used. Slice options (e.g. list of enabled/disabled linters) are combined from the command-line and config file.
Config File
GolangCI-Lint looks for config files in the following paths from the current working directory:
.golangci.yml
.golangci.yaml
.golangci.toml
.golangci.json
Example of .golangci.yaml
config file:
linters:
# Disable all linters.
# Default: false
disable-all: true
# Enable specific linter
# https://golangci-lint.run/usage/linters/#enabled-by-default
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- containedctx
- govet
- grouper
- ifshort
- importas
- ineffassign
- interfacebloat
- nakedret
- nestif
- wsl
# Enable all available linters.
# Default: false
enable-all: true
# Disable specific linter
# https://golangci-lint.run/usage/linters/#disabled-by-default
disable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- goimports
- golint
- gomnd
- gomoddirectives
- gomodguard
- goprintffuncname
- gosec
- gosimple
- govet
# Enable presets.
# https://golangci-lint.run/usage/linters
presets:
- bugs
- comment
- complexity
# Run only fast linters from enabled linters set (first run won't be fast)
# Default: false
fast: true
Command-Line Options
$ golangci-lint run -h
Usage:
golangci-lint run [flags]
Flags:
--out-format string Format of output: colored-line-number|line-number|json|tab|checkstyle|code-climate|html|junit-xml|github-actions (default "colored-line-number")
--print-issued-lines Print lines of code with issue (default true)
--print-linter-name Print linter name in issue line (default true)
--uniq-by-line Make issues output unique by line (default true)
--sort-results Sort linter results
--path-prefix string Path prefix to add to output
--modules-download-mode string Modules download mode. If not empty, passed as -mod=<mode> to go tools
--issues-exit-code int Exit code when issues were found (default 1)
--go string Targeted Go version
--build-tags strings Build tags
Pass -E/--enable
to enable linter and -D/--disable
to disable:
golangci-lint run --disable-all -E errcheck
Suppressing linting errors
Disabling specific linting issues that arise in a file or package is sometimes necessary. This can be accomplished in two ways: via the nolint
directive and via exclusion rules
in the configuration file.
The nolint
 directive
Here is an example of generating a random number:
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
rand.Seed(time.Now().UnixNano())
fmt.Println(rand.Int())
}
When we run golangci-lint run -E gosec lint.go the output will be:
lint.go:11:14: G404: Use of weak random number generator (math/rand instead of crypto/rand) (gosec)
fmt.Println(rand.Int())
The linter recommends using the Int method from crypto/rand instead because it is more cryptographically secure, but it has a less friendly API and slower performance. You can ignore the error by adding a nolint
directive to the relevant line:
func main() {
rand.Seed(time.Now().UnixNano())
fmt.Println(rand.Int()) //nolint
}
This inline use of nolint disables all linting issues detected for that line. You can disable issues from a specific linter by naming it in the directive (recommended).
func main() {
rand.Seed(time.Now().UnixNano())
fmt.Println(rand.Int()) //nolint:gosec
}
When a nolint
directive is used at the top of a file, it disables all linting issues for that file:
//nolint:govet,errcheck
package main
Exclusion rules
Exclude Issue by Text: Exclude issue by text using command-line option -e or config option issues.exclude. It's helpful when you decided to ignore all issues of this type. Also, you can use issues.exclude-rules
config option for per-path or per-linter configuration.
issues:
exclude:
- "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*printf?|os\\.(Un)?Setenv). is not checked"
- "exported (type|method|function) (.+) should have comment or be unexported"
- "ST1000: at least one file in a package should have a package comment"
Exclude Issues by Path:
Exclude issues in path by run.skip-dirs, run.skip-files or issues.exclude-rules config options. In the following example, all the reports from the linters (linters) that concerns the path (path) are excluded:
issues:
exclude-rules:
- path: '(.+)_test\.go'
linters:
- funlen
- goconst
Summary
The code checking tools mentioned above are mostly the "official" ones (the ones maintained by Golang developers). IDE integrations are a nice benefit of having official tooling. For example, Goland
includes gofmt support, and VSCode
includes an official Go extension that can check your code whenever you save a file.
There are plenty of great options for keeping your code clean and consistent, whether you use these official code checkers or others provided by the community. This article should have given you a better understanding of how to use go vet
, gofmt
to benefit your code.
References
https://pkg.go.dev/cmd/vet
https://github.com/golangci/golangci-lint
https://pkg.go.dev/cmd/gofmt
https://pkg.go.dev/golang.org/x/lint/golint
https://go.dev/doc/effective_go