Skip to content

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:

go
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

shell
ast-grep \
  --lang go \
  --pattern '{ defer $A.$B(t, failpoint.$M($$$)) } \
  --selector defer_statement'

Example

go
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:

go
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

yaml
id: test-functions
language: go
rule:
  kind: function_declaration
  has:
    field: name
    regex: Test.*

Example

go
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

yaml
id: match-function-call
language: go
rule:
  pattern:
    context: 'func t() { fmt.Println($A) }'
    selector: call_expression

Example

go
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

yaml
id: match-package-import
language: go
rule:
  kind: import_spec
  has:
    regex: PACKAGE_PATTERN_HERE

Example

JWT Library Detection

go
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

Sudesh Gutta

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.

go
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

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

go
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:

go
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

yaml
id: use-any
language: Go
rule:
  pattern: interface{}
fix: any

Example

go
func Decode(v interface{}) error { return nil }

Diff

go
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

yaml
id: use-clear
language: Go
rule:
  pattern:
    context: |-
      for $KEY := range $MAP {
        delete($MAP, $KEY)
      }
    strictness: cst
fix: clear($MAP)

Example

go
func reset(entries map[string]int) {
  for key := range entries {
    delete(entries, key)
  }
}

Diff

go
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

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

go
import "fmt"

func appendCount(buf []byte, count int) []byte {
	buf = append(buf, []byte(fmt.Sprintf("count=%d", count))...)
	return buf
}

Diff

go
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

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

go
func processAll(items []Item) {
	for _, item := range items {
		item := item
		go process(item)
	}
}

Diff

go
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

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: identifier

Example

go
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

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

go
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

yaml
id: reflect-type-for
language: Go
rule:
  pattern: reflect.TypeOf((*$TYPE)(nil)).Elem()
fix: reflect.TypeFor[$TYPE]()

Example

go
import "reflect"

func typeOf[T any]() reflect.Type {
	return reflect.TypeOf((*T)(nil)).Elem()
}

Diff

go
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

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

go
import "slices"

func releaseCapacity(items []string) {
	items = items[:len(items):len(items)]
	use(items)
}

Diff

go
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

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

go
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

go
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

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

go
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

go
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

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

go
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

go
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

yaml
id: use-time-since
language: Go
rule:
  pattern: time.Now().Sub($START)
fix: time.Since($START)

Example

go
import "time"

func elapsed(start time.Time) time.Duration {
  return time.Now().Sub(start)
}

Diff

go
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.

Made with ❤️ with Rust