Right, let’s talk about making your code less of a liability. You’ve written it, it compiles, and the tests pass. Great. But is it secure? Or did you just accidentally create a delightful little Rube Goldberg machine for an attacker? This is where static analysis tools come in—they’re the nitpicky, hyper-vigilant friend who reads the terms and conditions so you don’t have to. We’re going to look at the big three in the Go ecosystem: go vet, staticcheck, and gosec. They overlap in places, but each brings its own unique flavor of paranoia to the party.

go vet: The Built-in Sniffer

Think of go vet as your code’s first line of defense. It’s built right into the Go toolchain, so you have zero excuses not to use it. It doesn’t focus exclusively on security; its job is to find code that is “suspicious but correct.” It looks for things like misplaced printf-style verbs, unreachable code, and invalid struct tags.

Why should you care? Because many of these little slips can have security implications. A malformed struct tag might not be validated correctly, leading to data exposure. A wrong printf verb can lead to panics, which is a great way to announce your service’s availability to an attacker via a crash.

Run it constantly. Seriously, alias it to your build process. If you’re not running this, you’re being sloppy.

go vet ./...

And here’s a classic vet catch. This code will compile perfectly happily, but vet will rightly scream at you:

package main

import "fmt"

func main() {
    userInput := "Mario"
    // Vet will warn: "printf: %d format arg has non-int value"
    fmt.Printf("Hello, %d", userInput) 
}

It’s not a direct vulnerability, but a crash is a form of denial-of-service. vet is telling you, “Hey, genius, you’re about to shoot yourself in the foot. Again.”

staticcheck: The Pedantic Professor

If go vet is your nitpicky friend, staticcheck is that friend after six cups of coffee. It’s a phenomenal open-source tool that catches a huge range of issues, from useless code and simplifications to genuine bugs. Its checks are incredibly sophisticated, often understanding the flow of your code to find problems vet would miss.

Its security value is immense. It can find race conditions, catch unused values that might contain sensitive data, identify error handling issues, and flag a whole host of suspicious patterns.

Install it and run it. It’s non-negotiable.

go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck ./...

Look at this beauty. Can you spot the issue?

func generateToken() (token string, err error) {
    b := make([]byte, 16)
    _, err = rand.Read(b)
    if err != nil {
        return "", err
    }
    token = hex.EncodeToString(b)
    return
}

staticcheck will immediately flag this with SA4006: this value of err is never used (staticcheck). Wait, what? We’re using it in the error check! Ah, but look at the return statement: return. In a named return function, that returns token and err. And what is err at that point? It’s nil, because the rand.Read call succeeded. We just returned a nil error, implying success, completely ignoring the fact that we might have failed to encode the hex. This is a silent failure bug that staticcheck catches with terrifying ease.

gosec: The Security Bouncer

Now we get to the main event: gosec (Go Security). This tool is explicitly designed to find the juicy security flaws. It scans for SQL injection, hardcoded credentials, weak crypto, file inclusion vulnerabilities, and a dozen other nightmare scenarios.

It works by scanning the AST (Abstract Syntax Tree) of your code for known dangerous patterns. It’s not perfect—it can have false positives and won’t catch every logic flaw—but it is an absolutely critical part of your arsenal.

go install github.com/securego/gosec/v2/cmd/gosec@latest
gosec ./...

Let’s give it something to be angry about, shall we?

package main

import (
    "database/sql"
    "fmt"
    "os"
)

func main() {
    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        panic(err)
    }
    defer db.Close()

    q := fmt.Sprintf("SELECT * FROM users WHERE id=%s", os.Args[1])
    rows, err := db.Query(q)
    // ... handle results ...
}

Run gosec on this, and it will (rightfully) lose its mind: G101: Potential hardcoded credentials (if you had a password) and, more importantly, G201: SQL string formatting vulnerability. It spotted you concatenating user input (os.Args[1]) directly into a SQL query. Hello, SQL injection. This is exactly the kind of rookie mistake you need a tool to catch for you before it hits production.

Integrating This Into Your Workflow

Running these manually is for chumps. You need to automate this ruthlessly.

  1. Your IDE: Plugins exist to run staticcheck and vet on save. Get them. Let the squiggly red lines haunt your dreams.
  2. Your Pre-commit Hook: Use a tool like pre-commit to run these checks before you’re even allowed to create a commit. It’ll save you the embarrassment of pushing stupid bugs.
  3. Your CI Pipeline: This is the most important place. The build must fail if any of these tools find a critical issue. No exceptions. A gosec failure should be treated with the same severity as a failing test.

The combined output of these tools can be verbose. Tune them. With gosec, you can exclude certain rules (-exclude=G101) for specific files or directories if you have a false positive. But do this carefully, and always document why you’re silencing a warning. These tools are your allies. Listen to them, even when their truth is inconvenient.