A Go static analyzer that identifies variables with unnecessarily wide scope and suggests moving them to tighter scopes, following Go's idiomatic scoping patterns.
Have you ever scrolled through a long function to find where a variable was last modified, only to find its declaration 200 lines earlier? Wide variable scopes create cognitive overhead, make refactoring harder, and can introduce bugs from stale data.
Go was designed with narrow scoping in mind — from the := operator to init statements in control structures.
scopeguard helps you follow these patterns by automatically detecting opportunities to tighten variable scope, helping
you write more idiomatic Go.
Before:
func TestProcessor(t *testing.T) {
// ...
got, want := spyCC.Charges, charges
if !cmp.Equal(got, want) {
t.Errorf("spyCC.Charges = %v, want %v", got, want)
}
}After:
func TestProcessor(t *testing.T) {
// ...
if got, want := spyCC.Charges, charges; !cmp.Equal(got, want) {
t.Errorf("spyCC.Charges = %v, want %v", got, want)
}
}This pattern is used in Go Style Best Practices.
Before:
func process(data []byte) error {
var config Config
err := json.Unmarshal(data, &config)
if err != nil {
return fmt.Errorf("invalid configuration: %w", err)
}
// ... rest of function
}After:
func process(data []byte) error {
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return fmt.Errorf("invalid configuration: %w", err)
}
// ... rest of function
}- Simplifies refactoring — Minimizes dependencies when extracting code blocks
- Reduces cognitive load — Readers can forget variables once their block ends
- Enables shorter names — Variables with narrow scope can use concise names (as Go style guides recommend)
- Clearer intent — Makes the relationship between variables and control structures explicit
- Prevents reuse errors — Eliminates accidental reuse of a variable from a previous operation
- Less pollution — Avoids cluttering broader scopes with temporary variables
- Idiomatic Go — Follows patterns explicitly encouraged by Effective Go and major style guides
Choose one of the following installation methods:
brew install fillmore-labs/tap/scopeguardgo install fillmore-labs.com/scopeguard@latestInstall eget, then
eget fillmore-labs/scopeguardOpportunities to move variables to initializers of if, for, or switch statements, or to block scopes and case
clauses. It supports both short declarations (:=) and explicit variable declarations.
To ensure correctness, scopeguard excludes variables crossing loop or closure boundaries.
To analyze your entire project, run:
scopeguard ./...scopeguard -fix ./...Note: The -fix flag automates refactoring, but some cases require manual review. Always verify changes before
committing. See the Limitations section for details.
By default, generated files are skipped. To analyze them:
scopeguard -generated ./...You can suppress diagnostics for specific lines using linter comments:
x, err := someFunction() //nolint:scopeguardThis is useful when you've intentionally chosen a wider scope for readability or other reasons.
You can configure the maximum number of lines of a declaration to move:
scopeguard -max-lines 5 ./...Not every suggestion improves readability. Legitimate patterns where a slightly wider scope makes code clearer include early returns that reduce nesting.
Use your judgment. The tool highlights opportunities; you decide what makes your code clearer.
Generally, treat -fix as a suggestion, not a command. You may need to rework your logic for the suggestion to be
correct.
Always review automated changes. Some cases require manual intervention after applying -fix.
scopeguard does not consider implicit dependencies on side effects:
called := false
f := func() string {
called = true
return "test"
}
got, want := f(), "test"
if !called {
t.Error("Expected f to be called")
}
if got != want {
t.Errorf("Expected %q, got %q", want, got)
}... will be replaced by:
// ... previous code
if !called {
t.Error("Expected f to be called")
}
if got, want := f(), "test"; got != want {
t.Errorf("Expected %q, got %q", want, got)
}The call to f() is moved after the check for called, causing the test to fail.
To fix this, either rework your logic not to depend on the side effect so early (e.g. test whether the function has been
called after validating the result), use the result before testing the side effect (_ = got is enough and can also
be used to document your dependency on a side effect), or suppress the diagnostic with //nolint:scopeguard.
Similarly, the fix can break code that modifies variables used in the calculation:
const s = "abcd"
i := 1
got, want := s[i], byte('b')
i++
if got != want {
t.Errorf("Expected %q, got %q", want, got)
}In the example above, moving the declaration of got and want into the if statement changes when s[i] is
evaluated. The fix places it after i is incremented, altering the result and breaking the logic.
When moving a variable declaration, the inferred type may change if the original declaration specified an explicit type. Consider:
var a, b int
a, c := 3.0+1.0, 4.5
fmt.Println(1 / a)
if true {
b = 5.0
fmt.Println(b, c)
}... will be transformed to:
a, c := 3.0+1.0, 4.5
fmt.Println(1 / a)
if true {
var b int
b = 5.0
fmt.Println(b, c)
}Moving the declaration changes a's type from int to float64, causing a different result for 1 / a.
This should be rare in practice. To avoid this, ensure variables that need a specific type are declared as narrowly as
possible or use //nolint:scopeguard at the declaration.
go vet -vettool=$(which scopeguard) ./...Add a file .custom-gcl.yaml to your source with
---
version: v2.7.0
name: golangci-lint
destination: .
plugins:
- module: fillmore-labs.com/scopeguard
import: fillmore-labs.com/scopeguard/gclplugin
version: v0.0.2Then, run golangci-lint custom from your project root. You get a custom golangci-lint executable that can be
configured in .golangci.yaml:
---
version: "2"
linters:
enable:
- scopeguard
settings:
custom:
scopeguard:
type: module
description:
"scopeguard identifies variables with unnecessarily wide scope and suggests moving them to tighter scopes."
original-url: "https://fillmore-labs.com/scopeguard"
settings:
max-lines: 10and can be used like golangci-lint:
./golangci-lint run .See also the golangci-lint module plugin system documentation.
ineffassign— Detect ineffectual assignments.shadow— Check for possible unintended shadowing of variables.ifshort— Deprecated linter that checks if code uses short syntax forifstatements (Archived).- noinlineerr — Linter that prefers wider variable scope (the opposite philosophy).
Since shadow might get deprecated and the issues of scope and shadowing are tightly
related, shadow detection may be integrated into a future version. This could also help with the error of moving
shadowed variables.
This project is licensed under the Apache License 2.0. See the LICENSE file for details.