Go
This page curates a list of example ast-grep rules to check and to rewrite Go code.
Detect problematic defer statements with function calls
Description
This rule detects a common anti-pattern in Go testing code where defer statements contain function calls with parameters that are evaluated immediately instead of when the defer executes.
In Go, defer schedules a function call to be executed when the surrounding function returns. However, the arguments to the deferred function are evaluated immediately when the defer statement is encountered, not when the defer executes.
This is particularly problematic when using assertion libraries in tests. For example:
defer require.NoError(t, failpoint.Disable("some/path"))In this case, failpoint.Disable("some/path") is called immediately when the defer statement is reached, not when the function exits. This means the failpoint is disabled right after being enabled, making the test ineffective.
Pattern
ast-grep \
--lang go \
--pattern '{ defer $A.$B(t, failpoint.$M($$$)) } \
--selector defer_statement'Example
func TestIssue16696(t *testing.T) {
alarmRatio := vardef.MemoryUsageAlarmRatio.Load()
vardef.MemoryUsageAlarmRatio.Store(0.0)
defer vardef.MemoryUsageAlarmRatio.Store(alarmRatio)
require.NoError(t, failpoint.Enable("github.com/pingcap/tidb/pkg/executor/sortexec/testSortedRowContainerSpill", "return(true)"))
defer require.NoError(t,
failpoint.Disable(
"github.com/pingcap/tidb/pkg/executor/sortexec/testSortedRowContainerSpill"
))
require.NoError(t, failpoint.Enable("github.com/pingcap/tidb/pkg/executor/join/testRowContainerSpill", "return(true)"))
defer require.NoError(t,
failpoint.Disable("github.com/pingcap/tidb/pkg/executor/join/testRowContainerSpill"))
}Fix
The correct way to defer a function with parameters is to wrap it in an anonymous function:
defer func() {
require.NoError(t, failpoint.Disable("some/path"))
}()Contributed by
Inspired by YangKeao's tweet about this common pitfall in TiDB codebase.
Find function declarations with names of certain pattern
Description
ast-grep can find function declarations by their names. But not all names can be matched by a meta variable pattern. For instance, you cannot use a meta variable pattern to find function declarations whose names start with a specific prefix, e.g. TestAbs with the prefix Test. Attempting Test$_ will fail because it is not a valid syntax.
Instead, you can use a YAML rule to use the regex atomic rule.
YAML
id: test-functions
language: go
rule:
kind: function_declaration
has:
field: name
regex: Test.*Example
package abs
import "testing"
func TestAbs(t *testing.T) {
got := Abs(-1)
if got != 1 {
t.Errorf("Abs(-1) = %d; want 1", got)
}
}Contributed by
kevinkjt2000 on Discord.
Match Function Call in Golang
Description
One of the common questions of ast-grep is to match function calls in Golang.
A plain pattern like fmt.Println($A) will not work. This is because Golang syntax also allows type conversions, e.g. int(3.14), that look like function calls. Tree-sitter, ast-grep's parser, will prefer parsing func_call(arg) as a type conversion instead of a call expression.
To avoid this ambiguity, ast-grep lets us write a contextual pattern, which is a pattern inside a larger code snippet. We can use context to write a pattern like this: func t() { fmt.Println($A) }. Then, we can use the selector call_expression to match only function calls.
Please also read the deep dive on ambiguous pattern.
YAML
id: match-function-call
language: go
rule:
pattern:
context: 'func t() { fmt.Println($A) }'
selector: call_expressionExample
func main() {
fmt.Println("OK")
}Contributed by
Inspired by QuantumGhost from ast-grep/ast-grep#646
Match package import in Golang
Description
A generic rule template for detecting imports of specific packages in Go source code. This rule can be customized to match any package by modifying the regex pattern, making it useful for security auditing, dependency management, and compliance checking.
This rule identifies Go import statements based on the configured regex pattern, including:
Direct imports: import "package/name"
Versioned imports: import "package/name/v4"
Subpackage imports: import "package/name/subpkg"
Grouped imports within import () blocks
YAML
id: match-package-import
language: go
rule:
kind: import_spec
has:
regex: PACKAGE_PATTERN_HEREExample
JWT Library Detection
package main
import (
"fmt"
"github.com/golang-jwt/jwt" // This matches the AST rule
)
func main() {
token := jwt.New(jwt.SigningMethodHS256) // Create a new token
// Add some claims
token.Claims = jwt.MapClaims{"user": "alice", "role": "admin"}
tokenString, err := token.SignedString([]byte("my-secret")) // Sign the token
if err != nil {
fmt.Printf("Error signing token: %v\n", err)
return
}
fmt.Printf("Generated token: %s\n", tokenString)
}Contributed by
Detect problematic JSON tags with dash prefix
Description
This rule detects a security vulnerability in Go's JSON unmarshaling. When a struct field has a JSON tag that starts with -,, it can be unexpectedly unmarshaled with the - key.
According to the Go documentation, if the field tag is -, the field should be omitted. However, a field with name - can still be unmarshaled using the tag -,.
This creates a security issue where developers think they are preventing a field from being unmarshaled (like IsAdmin in authentication), but attackers can still set that field by providing the - key in JSON input.
type User struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
IsAdmin bool `json:"-,omitempty"` // Intended to prevent marshaling
}
// This still works and sets IsAdmin to true!
json.Unmarshal([]byte(`{"-": true}`), &user)
// Result: main.User{Username:"", Password:"", IsAdmin:true}YAML
id: unmarshal-tag-is-dash
severity: error
message: Struct field can be decoded with the `-` key because the JSON tag
starts with a `-` but is followed by a comma.
rule:
pattern: '`$TAG`'
inside:
kind: field_declaration
constraints:
TAG:
regex: json:"-,.*"Example
package main
type TestStruct1 struct {
A string `json:"id"` // ok
}
type TestStruct2 struct {
B string `json:"-,omitempty"` // wrong
}
type TestStruct3 struct {
C string `json:"-,123"` // wrong
}
type TestStruct4 struct {
D string `json:"-,"` // wrong
}Fix
To properly omit a field from JSON marshaling/unmarshaling, use just - without a comma:
type User struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
IsAdmin bool `json:"-"` // Correctly prevents marshaling/unmarshaling
}Contributed by
Inspired by Trail of Bits blog post and their public Semgrep rule.
Prefer any over interface{} Has Fix
Description
Go 1.18 introduced any as an alias for interface{}. The shorter name makes unconstrained values and type parameters easier to recognize.
Use this rule only when the project targets Go 1.18 or newer, and review code that declares its own identifier named any.
YAML
id: use-any
language: Go
rule:
pattern: interface{}
fix: anyExample
func Decode(v interface{}) error { return nil }Diff
func Decode(v interface{}) error { return nil }
func Decode(v any) error { return nil } Credits
Inspired by JetBrains' Go Modern Guidelines.
Replace delete-all loops with clear Has Fix
Description
The clear built-in expresses removing every entry from a map without a handwritten loop. Concrete-syntax strictness ensures the fix is offered only when the loop body is exactly the matching delete call, without comments that the rewrite could discard.
Use this rule only when the project targets Go 1.21 or newer.
YAML
id: use-clear
language: Go
rule:
pattern:
context: |-
for $KEY := range $MAP {
delete($MAP, $KEY)
}
strictness: cst
fix: clear($MAP)Example
func reset(entries map[string]int) {
for key := range entries {
delete(entries, key)
}
}Diff
func reset(entries map[string]int) {
for key := range entries {
delete(entries, key)
}
clear(entries)
}Credits
Inspired by JetBrains' Go Modern Guidelines.
Append formatted bytes with fmt.Appendf Has Fix
Description
Since Go 1.19, fmt.Appendf can format directly into a byte slice. It avoids creating an intermediate string with fmt.Sprintf and then converting that string to []byte. This rule matches only the exact nested append shape and keeps the buffer to a plain identifier.
It assumes fmt names the standard-library package and does not add the import.
YAML
id: use-fmt-appendf
language: Go
severity: hint
message: Append formatted bytes directly with fmt.Appendf.
rule:
pattern:
context: func f() { $BUFFER = append($BUFFER, []byte(fmt.Sprintf($$$ARGS))...) }
selector: assignment_statement
constraints:
BUFFER:
kind: identifier
fix: $BUFFER = fmt.Appendf($BUFFER, $$$ARGS)Example
import "fmt"
func appendCount(buf []byte, count int) []byte {
buf = append(buf, []byte(fmt.Sprintf("count=%d", count))...)
return buf
}Diff
import "fmt"
func appendCount(buf []byte, count int) []byte {
buf = append(buf, []byte(fmt.Sprintf("count=%d", count))...)
buf = fmt.Appendf(buf, "count=%d", count)
return buf
}Credits
Based on JetBrains' Go Modern Guidelines.
Remove redundant range-variable copies Has Fix
Description
Since Go 1.22, variables declared by a range clause are recreated for each iteration. A direct item := item copy is therefore unnecessary, even when a closure captures item or code takes its address.
This rule deliberately matches only variables declared with := in an enclosing range loop. Use it only when the module targets Go 1.22 or newer.
YAML
id: loopvar-capture
language: Go
rule:
all:
- pattern:
context: func f() { $VAR := $VAR }
selector: short_var_declaration
- inside:
kind: for_statement
has:
any:
- pattern:
context: for $VAR := range $RANGE {}
selector: range_clause
strictness: cst
- pattern:
context: for $VAR, $_ := range $RANGE {}
selector: range_clause
strictness: cst
- pattern:
context: for $_, $VAR := range $RANGE {}
selector: range_clause
strictness: cst
stopBy: end
fix: ""Example
func processAll(items []Item) {
for _, item := range items {
item := item
go process(item)
}
}Diff
func processAll(items []Item) {
for _, item := range items {
item := item
go process(item)
}
}Credits
Based on JetBrains' Go Modern Guidelines.
Find map copy loops that may use maps.Copy
Description
Since Go 1.21, maps.Copy(dst, src) clearly expresses a shallow copy that overwrites matching destination keys. This rule finds loops whose body is exactly dst[k] = v and keeps the destination to a plain identifier.
No automatic fix is offered because ast-grep does not resolve Go types. Assignment permits cases such as copying map[string]string into map[any]any, while maps.Copy requires compatible key and value types. Verify the types before converting the loop, and add import "maps" when needed.
YAML
id: use-maps-copy
language: Go
severity: hint
message: Consider maps.Copy after verifying compatible map types.
rule:
pattern:
context: |
for $KEY, $VALUE := range $SOURCE {
$DESTINATION[$KEY] = $VALUE
}
selector: for_statement
strictness: cst
constraints:
DESTINATION:
kind: identifierExample
func merge(dst, src map[string]int) {
for key, value := range src {
dst[key] = value
}
}Credits
Based on JetBrains' Go Modern Guidelines.
Find loops that can range over an integer
Description
Go 1.22 can range directly over an integer, producing values from zero through n-1. This rule finds the equivalent zero-based loop with a unit increment and an identifier, integer literal, or len bound.
No automatic fix is offered because the traditional loop reevaluates its bound on every iteration, while a range expression evaluates it once. The loop body can also modify the counter. Use this suggestion only when the project targets Go 1.22 or newer and both the bound and counter remain stable.
YAML
id: use-range-over-int
language: Go
severity: hint
message: Consider ranging over the integer after verifying the bound and counter stay stable.
rule:
pattern:
context: |-
for $INDEX := 0; $INDEX < $BOUND; $INDEX++ {
$$$BODY
}
strictness: ast
constraints:
BOUND:
any:
- kind: int_literal
- kind: identifier
- pattern: len($COLLECTION)Example
func visit(items []string) {
for i := 0; i < len(items); i++ {
process(items[i])
}
}Credits
Inspired by JetBrains' Go Modern Guidelines.
Prefer reflect.TypeFor Has Fix
Description
Go 1.22 added reflect.TypeFor[T](), a direct way to obtain the reflect.Type for a type argument. It replaces the less readable nil-pointer expression reflect.TypeOf((*T)(nil)).Elem().
This is a syntactic rule: it assumes reflect names the standard-library package. Use it only when the module targets Go 1.22 or newer.
YAML
id: reflect-type-for
language: Go
rule:
pattern: reflect.TypeOf((*$TYPE)(nil)).Elem()
fix: reflect.TypeFor[$TYPE]()Example
import "reflect"
func typeOf[T any]() reflect.Type {
return reflect.TypeOf((*T)(nil)).Elem()
}Diff
import "reflect"
func typeOf[T any]() reflect.Type {
return reflect.TypeOf((*T)(nil)).Elem()
return reflect.TypeFor[T]()
}Credits
Based on JetBrains' Go Modern Guidelines.
Prefer slices.Clip Has Fix
Description
slices.Clip limits a slice's capacity to its length. It is the readable standard-library equivalent of the full slice expression s[:len(s):len(s)].
This helper requires Go 1.21 or newer. The fix assumes the standard-library slices package is available under its usual name; add the import if needed.
YAML
id: slices-clip
language: Go
rule:
pattern:
context: func f() { $SLICE = $SLICE[:len($SLICE):len($SLICE)] }
selector: assignment_statement
constraints:
SLICE:
kind: identifier
fix: $SLICE = slices.Clip($SLICE)Example
import "slices"
func releaseCapacity(items []string) {
items = items[:len(items):len(items)]
use(items)
}Diff
import "slices"
func releaseCapacity(items []string) {
items = items[:len(items):len(items)]
items = slices.Clip(items)
use(items)
}Credits
Based on JetBrains' Go Modern Guidelines.
Prefer slices.Reverse Has Fix
Description
slices.Reverse clearly expresses an in-place reversal and avoids hand-written index arithmetic. The rule uses CST strictness and identifier constraints so it fixes only the exact two-index swap loop shown below.
This helper requires Go 1.21 or newer. The fix assumes the standard-library slices package is available under its usual name; add the import if needed.
YAML
id: slices-reverse
language: Go
rule:
pattern:
context: |
for $I, $J := 0, len($SLICE)-1; $I < $J; $I, $J = $I+1, $J-1 {
$SLICE[$I], $SLICE[$J] = $SLICE[$J], $SLICE[$I]
}
selector: for_statement
strictness: cst
constraints:
I:
kind: identifier
J:
kind: identifier
SLICE:
kind: identifier
fix: slices.Reverse($SLICE)Example
import "slices"
func reverse(items []string) {
for i, j := 0, len(items)-1; i < j; i, j = i+1, j-1 {
items[i], items[j] = items[j], items[i]
}
}Diff
import "slices"
func reverse(items []string) {
for i, j := 0, len(items)-1; i < j; i, j = i+1, j-1 {
items[i], items[j] = items[j], items[i]
}
slices.Reverse(items)
}Credits
Based on JetBrains' Go Modern Guidelines.
Combine checks and trims with CutPrefix or CutSuffix Has Fix
Description
Since Go 1.20, strings.CutPrefix and strings.CutSuffix return both the remaining string and whether the affix matched. This rule replaces an adjacent check and trim only when they use the same simple value and affix. It also skips bodies containing ok, because the fix introduces that name in the if initializer.
The rule assumes strings is the standard-library package imported under its canonical name and does not add the import.
YAML
id: use-strings-cut-prefix-suffix
language: Go
severity: hint
message: Combine an affix check and trim with strings.CutPrefix or strings.CutSuffix.
rule:
all:
- pattern:
context: |
func f() {
if $_CHECK($$$CHECK_ARGS) {
$REST := $TRIM_CALL
$$$BODY
}
}
selector: if_statement
- any:
- pattern:
context: |
func f() {
if strings.HasPrefix($VALUE, $AFFIX) {
$REST := strings.TrimPrefix($VALUE, $AFFIX)
$$$BODY
}
}
selector: if_statement
- pattern:
context: |
func f() {
if strings.HasSuffix($VALUE, $AFFIX) {
$REST := strings.TrimSuffix($VALUE, $AFFIX)
$$$BODY
}
}
selector: if_statement
- not:
regex: '\bok\b'
constraints:
VALUE:
kind: identifier
AFFIX:
any:
- kind: identifier
- kind: interpreted_string_literal
- kind: raw_string_literal
transform:
CUT_CALL:
replace:
source: $TRIM_CALL
replace: '^strings\.Trim'
by: strings.Cut
fix: |-
if $REST, ok := $CUT_CALL; ok {
$$$BODY
}Example
import "strings"
func inspect(name string) {
if strings.HasPrefix(name, "log:") {
value := strings.TrimPrefix(name, "log:")
use(value)
}
if strings.HasSuffix(name, ".tmp") {
value := strings.TrimSuffix(name, ".tmp")
use(value)
}
}Diff
import "strings"
func inspect(name string) {
if strings.HasPrefix(name, "log:") {
value := strings.TrimPrefix(name, "log:")
if value, ok := strings.CutPrefix(name, "log:"); ok {
use(value)
}
if strings.HasSuffix(name, ".tmp") {
value := strings.TrimSuffix(name, ".tmp")
if value, ok := strings.CutSuffix(name, ".tmp"); ok {
use(value)
}
}Credits
Based on JetBrains' Go Modern Guidelines.
Stream split results with SplitSeq and FieldsSeq Has Fix
Description
Go 1.24 added SplitSeq and FieldsSeq to strings and bytes. When a range loop discards the slice index, these helpers stream each part without first allocating the complete result slice. The rule preserves the loop body and deliberately ignores loops that use the index.
It assumes strings and bytes are the canonical standard-library package names and does not manage imports.
YAML
id: use-split-seq
language: Go
severity: hint
message: Stream split results with a Seq helper.
rule:
pattern:
context: |
func f() {
for _, $ITEM := range $PACKAGE.$FUNCTION($$$ARGS) {
$$$BODY
}
}
selector: for_statement
constraints:
PACKAGE:
regex: '^(strings|bytes)$'
FUNCTION:
regex: '^(Split|Fields)$'
ITEM:
all:
- kind: identifier
- not:
regex: '^_$'
transform:
SEQ_FUNCTION:
replace:
source: $FUNCTION
replace: '$'
by: Seq
fix: |-
for $ITEM := range $PACKAGE.$SEQ_FUNCTION($$$ARGS) {
$$$BODY
}Example
import (
"bytes"
"strings"
)
func visit(text string, data []byte) {
for _, part := range strings.Split(text, ",") {
useString(part)
}
for _, field := range bytes.Fields(data) {
useBytes(field)
}
}Diff
import (
"bytes"
"strings"
)
func visit(text string, data []byte) {
for _, part := range strings.Split(text, ",") {
for part := range strings.SplitSeq(text, ",") {
useString(part)
}
for _, field := range bytes.Fields(data) {
for field := range bytes.FieldsSeq(data) {
useBytes(field)
}
}Credits
Based on JetBrains' Go Modern Guidelines.
Prefer time.Since Has Fix
Description
time.Since(start) states the intent directly and is equivalent to time.Now().Sub(start).
This syntactic rule assumes time is the unaliased standard-library package. It does not add or change imports. time.Since is available in Go 1.0 and newer.
YAML
id: use-time-since
language: Go
rule:
pattern: time.Now().Sub($START)
fix: time.Since($START)Example
import "time"
func elapsed(start time.Time) time.Duration {
return time.Now().Sub(start)
}Diff
import "time"
func elapsed(start time.Time) time.Duration {
return time.Now().Sub(start)
return time.Since(start)
}Credits
Inspired by JetBrains' Go Modern Guidelines.