Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for State of Golang linters and the differences between them
SourceLevel profile imageWeverton Timoteo
Weverton Timoteo forSourceLevel

Posted on • Edited on • Originally published atsourcelevel.io

     

State of Golang linters and the differences between them

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.

Lintersare available for many languages, 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 intotokens. 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 havescanner,token,parser, andast (Abstract Syntax Tree) packages. Let's jump straight to a practical example by checking this simple snippet:

packagemainfuncmain(){println("Hello, SourceLevel!")}
Enter fullscreen modeExit fullscreen mode

Okay, nothing new here. Now we'll use Golang standard library packages to visualize theast generated by the code above:

import("go/ast""go/parser""go/token")funcmain(){// 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 fsetf,err:=parser.ParseFile(fset,"",src,0)iferr!=nil{panic(err)}// Print the AST.ast.Print(fset,f)}
Enter fullscreen modeExit fullscreen mode

Now let's run this code and look the generated AST:

0*ast.File{1.Package:2:12.Name:*ast.Ident{3..NamePos:2:94..Name:"main"5.}6.Decls:[]ast.Decl(len=1){7..0:*ast.FuncDecl{8...Name:*ast.Ident{9....// Name content16...}17...Type:*ast.FuncType{18....// Type content23...}24...Body:*ast.BlockStmt{25....// Body content47...}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}
Enter fullscreen modeExit fullscreen mode

As you can see, the AST describes the previous block in a struct calledast.Filewhich is compound by the following structure:

typeFilestruct{Doc*CommentGroup// associated documentation; or nilPackagetoken.Pos// position of "package" keywordName*Ident// package nameDecls[]Decl// top-level declarations; or nilScope*Scope// package scope (this file only)Imports[]*ImportSpec// imports in this fileUnresolved[]*Ident// unresolved identifiers in this fileComments[]*CommentGroup// list of all comments in the source file}
Enter fullscreen modeExit fullscreen mode

To understand more about lexical scanning and how this struct is filled, I would recommendRob Pike talk.

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 thegofmt 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 fromGo by Examples unformatted:

packagemainimport"fmt"funcintSeq()func()int{i:=0returnfunc()int{i++returni}}funcmain(){nextInt:=intSeq()fmt.Println(nextInt())fmt.Println(nextInt())fmt.Println(nextInt())newInts:=intSeq()fmt.Println(newInts())}
Enter fullscreen modeExit fullscreen mode

Then it will be formatted this way:

packagemainimport"fmt"funcintSeq()func()int{i:=0returnfunc()int{i++returni}}funcmain(){nextInt:=intSeq()fmt.Println(nextInt())fmt.Println(nextInt())fmt.Println(nextInt())newInts:=intSeq()fmt.Println(newInts())}
Enter fullscreen modeExit fullscreen mode

So we can observe thatimport earned an extra linebreak but the empty line aftermain function declaration is still there. So we can assume that we shouldn’t transfer the responsibility of keeping your code readable to thegofmt: consider it as a helper on accomplishing readable and maintainable code.

It’s highly recommended to rungofmt before you commit your changes, you can evenconfigure a precommit hook for that. If you want to overwrite the changes instead of printing them, you should usegofmt -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:

[]T{T{},T{}}
Enter fullscreen modeExit fullscreen mode

will be simplified to:

[]T{{},{}}
Enter fullscreen modeExit fullscreen mode

A slice expression of the form:

s[a:len(s)]
Enter fullscreen modeExit fullscreen mode

will be simplified to:

s[a:]
Enter fullscreen modeExit fullscreen mode

A range of the form:

forx,_=rangev{...}
Enter fullscreen modeExit fullscreen mode

will be simplified to:

forx=rangev{...}
Enter fullscreen modeExit fullscreen mode

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_=rangev{...}
Enter fullscreen modeExit fullscreen mode

will be simplified to:

forrangev{...}
Enter fullscreen modeExit fullscreen mode

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:

goinstallgolang.org/x/tools/cmd/goimports@latest
Enter fullscreen modeExit fullscreen mode

Then use it by running with-l option to specify a path, in our case we’re doing a recursive search in the project:

goimports-l./..../my-project/vendor/github.com/robfig/cron/doc.go
Enter fullscreen modeExit fullscreen mode

So it identified thatcron/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 calledABC 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, andcase)

Caution: This metric shouldnot 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&& goinstall)
Enter fullscreen modeExit fullscreen mode

Give the following Golang snippet:

packagemainimport("fmt""os""my_app/persistence"service"my_app/services"flag"github.com/ogier/pflag")// flagsvar(filepathstring)funcmain(){flag.Parse()ifflag.NFlag()==0{printUsage()}persistence.Prepare()service.Compare(filepath)}funcinit(){flag.StringVarP(&filepath,"filepath","f","","Load CSV to lookup for data")}funcprintUsage(){fmt.Printf("Usage: %s [options]\n",os.Args[0])fmt.Println("Options:")flag.PrintDefaults()os.Exit(1)}
Enter fullscreen modeExit fullscreen mode

Then let’s analyze this example usingabcgo:

$abcgo-path main.goSource            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
Enter fullscreen modeExit fullscreen mode

As you can see, it will print theScore based on eachfunction 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 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 somecode smells and insights.

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

$goinstallgithub.com/fzipp/gocyclo/cmd/gocyclo@latest
Enter fullscreen modeExit fullscreen mode

Then let’s check the same piece of code that we’ve analyzed in the ABC Metrics section:

$gocyclo main.go2 main main main.go:18:11 main printUsage main.go:33:11 main init main.go:29:1
Enter fullscreen modeExit fullscreen mode

It also breaks the output based on function name, so we can see that themain function has 2 paths since we’re usingif 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 recommendedStaticcheck be used as a replacement.

Golint vs Staticcheck vs revive

Before Staticcheck was recommended, we hadrevive, which for me sounds more like a community alternative linter.

As revive states how different it is from archivedgolint:

  • 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 forrevive at the point of creating custom rules or formatters. Wanna try it?

$goinstallgithub.com/mgechev/revive@latest
Enter fullscreen modeExit fullscreen mode

Then you can run it with the following command:

$revive-exclude vendor/...-formatter friendly ./...
Enter fullscreen modeExit fullscreen mode

I often exclude myvendor 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 golintignoreGeneratedHeader = 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"
Enter fullscreen modeExit fullscreen mode

Then you should pass it on runningrevive:

$ revive -exclude vendor/... -config revive.toml -formatter friendly ./...
Enter fullscreen modeExit fullscreen mode

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 thearchived repository awesome-go-linters.

Where to start?

To start, consider usinggofmt before each commit or whenever you remember to run, then tryrevive. Which linters are you using?

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

DORA and Pull Request metrics for data-driven teams

Accelerate deliveries and mature engineering practices

More fromSourceLevel

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp