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 , but let us take a look at the Golang ecosystem. are available for many languages : Disclaimer The author of the story is the CTO at SourceLevel 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 . So each keyword, constant, variable in our code will produce a token. tokens 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 , , , and (Abstract Syntax Tree) packages. Let's jump straight to a practical example by checking this simple snippet: scanner token parser ast package main func main() { println("Hello, SourceLevel!") } Okay, nothing new here. Now we'll use Golang standard library packages to visualize the generated by the code above: ast 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 which is compound by the following structure: ast.File 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. Code Formatting To format code in Golang, we can use the 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. gofmt Here is a simple snippet from unformatted: Go by Examples 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 earned an extra linebreak but the empty line after function declaration is still there. So we can assume that we shouldn’t transfer the responsibility of keeping your code readable to the : consider it as a helper on accomplishing readable and maintainable code. import main gofmt It’s highly recommended to run before you commit your changes, you can even for that. If you want to overwrite the changes instead of printing them, you should use . gofmt configure a precommit hook gofmt -w Simplify option has a as Simplify command, when running with this option it considers the following: gofmt -s 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 instead. _ _meaningfulName 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. Check unused imports On some occasions, we can find ourselves trying different packages during implementation and just give up on using them. By using package]( ) we can identify which packages are being imported and unreferenced in our code and also add missing ones: [goimports https://pkg.go.dev/golang.org/x/tools/cmd/goimports go install golang.org/x/tools/cmd/goimports@latest Then use it by running with option to specify a path, in our case we’re doing a recursive search in the project: -l go imports -l ./.. ../my-project/vendor/github.com/robfig/cron/doc.go So it identified that is unreferenced in our code and it’s safe to remove it from our code. cron/doc Code Complexity 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. 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 , which are compounded by the following: ABC Metric , , , , , , , , , , and (A) Assignment counts: = *= /= %= += <<= >>= &= ^= ++ -- Function is called (B) Branch counts when: Booleans or logic test ( , , , , , , , and ) (C) Conditionals counts: ? < > <= >= != else case This metric should be used as a “score” to decrease, consider it as just an indicator of your codebase or current file being analyzed. Caution: not To have this indicator in Golang, you can use package]( ): [abcgo 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 based on each found in the file. This metric can help new collaborators identify files that a pair programming session would be required during the onboarding period. Score function Cyclomatic Complexity , on the other hand, besides the complex name, has a simple explanation: it calculates how many paths your code has. Cyclomatic Complexity It is useful to indicate that you may break your implementation in separate abstractions or give some and insights. code smells To analyze our Golang code let use package]( ): [gocyclo 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 function has 2 paths since we’re using conditional there. main if Style and Patterns Checking To verify code style and patterns in your codebase, Golang already came with 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 be used as a replacement. [golint](https://github.com/golang/lint) Staticcheck Golint vs Staticcheck vs revive Before Staticcheck was recommended, we had , which for me sounds more like a community alternative linter. revive 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. allows this only for generated files. golint 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. provides more rules compared to . Revive golint Testing revive linter I think the extra point goes for at the point of creating custom rules or formatters. Wanna try it? revive $ 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 directory since my dependencies are there. vendor 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 ./... 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 Where to start? To start, consider using before each commit or whenever you remember to run, then try . Which linters are you using? gofmt revive : Disclaimer The author of the story is the CTO at . SourceLevel