Go 1.26 で追加される modernize tools の一覧と詳細について
Table of Contents
Go 1.26 のリリースが迫る中、どの機能に注目しているでしょうか? 私は go fix コマンドの変化について注目しています。
Go 1.26 のリリースノートには、以下の通り書かれています。
The go fix command, following the pattern of go vet in Go 1.10, now uses the Go analysis framework (golang.org/x/tools/go/analysis). This means the same analyzers that provide diagnostics in go vet can be used to suggest and apply fixes in go fix. The go fix command’s historical fixers, all of which were obsolete, have been removed and replaced by a suite of new analyzers that offer fixes to use newer features of the language and library.
Go 1.10 における go vet のパターンに倣い、 go fix コマンドは今 Go の analysis フレームワーク(golang.org/x/tools/go/analysis) を利用しています。これにより、 go vet で診断を提供しているのと同じアナライザを go fix でも修正の提案や適用に利用できるようになります。 go fix コマンドの歴史的な fixer は全て時代遅れだったため除去され、言語や標準ライブラリの新しい機能を利用するための修正を提供する新しい analyzers に置き換えられました。
では、この 言語や標準ライブラリの新しい機能を利用するための修正 とは何でしょうか? その修正は、https://github.com/golang/go/issues/70815 で提起されている以下の22個です。ただし、これらのうち3個はコメントアウトされているので、2026年1月31日現在機能するのは19個です。
アナライザー一覧
| アナライザー名 | 説明 | 対応Goバージョン | 状態 |
|---|---|---|---|
| Any | interface{} を any に置換 | Go 1.18+ | ✅ 有効 |
| FmtAppendf | []byte(fmt.Sprintf(...)) を fmt.Appendf(nil, ...) に置換 | Go 1.19+ | ✅ 有効 |
| ForVar | ループ変数の不要な再宣言を削除 | Go 1.22+ | ✅ 有効 |
| MapsLoop | マップループを maps パッケージの関数に置換 | Go 1.23+ | ✅ 有効 |
| MinMax | if/else を min/max に置換 | Go 1.21+ | ✅ 有効 |
| NewExpr | &x パターンを new(x) に置換 | Go 1.26+ | ✅ 有効 |
| OmitZero | omitempty を omitzero に置換 | Go 1.24+ | ✅ 有効 |
| plusBuild | 古い //+build コメントを削除 | Go 1.18+ | ✅ 有効 |
| RangeInt | for i := 0; i < n; i++ を for i := range n に置換 | Go 1.22+ | ✅ 有効 |
| ReflectTypeFor | reflect.TypeOf(x) を reflect.TypeFor[T]() に置換 | Go 1.22+ | ✅ 有効 |
| SlicesContains | ループを slices.Contains に置換 | Go 1.21+ | ✅ 有効 |
| SlicesSort | sort.Slice を slices.Sort に置換 | Go 1.21+ | ✅ 有効 |
| stditerators | Len/At スタイルをイテレータに置換 | - | ✅ 有効 |
| stringscut | strings.Index を strings.Cut に置換 | Go 1.18+ | ✅ 有効 |
| StringsCutPrefix | HasPrefix+TrimPrefix を CutPrefix に置換 | Go 1.20+ | ✅ 有効 |
| StringsSeq | strings.Split を strings.SplitSeq に置換 | Go 1.24+ | ✅ 有効 |
| StringsBuilder | += 連結を strings.Builder に置換 | Go 1.10+ | ✅ 有効 |
| TestingContext | context.WithCancel を t.Context() に置換 | Go 1.24+ | ✅ 有効 |
| WaitGroup | wg.Add(1); go func() { defer wg.Done() }() を wg.Go() に置換 | Go 1.25+ | ✅ 有効 |
| AppendClippedAnalyzer | append チェーンを slices.Concat に置換 | Go 1.21+ | ⚠️ 無効 |
| BLoopAnalyzer | ベンチマークループを b.Loop() に置換 | Go 1.24+ | ⚠️ 無効 |
| SlicesDeleteAnalyzer | append ベース削除を slices.Delete に置換 | Go 1.21+ | ⚠️ 無効 |
本記事の目的は、それぞれの Analyzer の実装を確認し、どのような恩恵があるのかを理解することです。
Any
Any の役割は interface{} を any に置換することです。any は Go 1.18 で追加された interface{} のエイリアスです。これは純粋にスタイル的な変更で、コードの可読性を向上させます。
使用例
Before:
func process(data interface{}) error {
// ...
}
var result interface{} = getValue()
After:
func process(data any) error {
// ...
}
var result any = getValue()
実装コード (any.go)
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
import (
"go/ast"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/versions"
)
var AnyAnalyzer = &analysis.Analyzer{
Name: "any",
Doc: analyzerutil.MustExtractDoc(doc, "any"),
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: runAny,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any",
}
// The any pass replaces interface{} with go1.18's 'any'.
func runAny(pass *analysis.Pass) (any, error) {
for curFile := range filesUsingGoVersion(pass, versions.Go1_18) {
for curIface := range curFile.Preorder((*ast.InterfaceType)(nil)) {
iface := curIface.Node().(*ast.InterfaceType)
if iface.Methods.NumFields() == 0 {
// Check that 'any' is not shadowed.
if lookup(pass.TypesInfo, curIface, "any") == builtinAny {
pass.Report(analysis.Diagnostic{
Pos: iface.Pos(),
End: iface.End(),
Message: "interface{} can be replaced by any",
SuggestedFixes: []analysis.SuggestedFix{{
Message: "Replace interface{} by any",
TextEdits: []analysis.TextEdit{
{
Pos: iface.Pos(),
End: iface.End(),
NewText: []byte("any"),
},
},
}},
})
}
}
}
}
return nil, nil
}
FmtAppendf
FmtAppendf は []byte(fmt.Sprintf(...)) を fmt.Appendf(nil, ...) に置換します。これにより、Sprintf による中間的な文字列のアロケーションを避け、コードをより効率的にします。fmt.Sprint や fmt.Sprintln にも同様の置換が適用されます。
使用例
Before:
data := []byte(fmt.Sprintf("Hello, %s!", name))
log := []byte(fmt.Sprintln("Debug:", value))
After:
data := fmt.Appendf(nil, "Hello, %s!", name)
log := fmt.Appendln(nil, "Debug:", value)
メリット: 中間的な文字列アロケーションを避けることで、メモリ効率とパフォーマンスが向上します。
実装コード (fmtappendf.go)
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
import (
"fmt"
"go/ast"
"go/types"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/internal/analysis/analyzerutil"
typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
)
var FmtAppendfAnalyzer = &analysis.Analyzer{
Name: "fmtappendf",
Doc: analyzerutil.MustExtractDoc(doc, "fmtappendf"),
Requires: []*analysis.Analyzer{
inspect.Analyzer,
typeindexanalyzer.Analyzer,
},
Run: fmtappendf,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#fmtappendf",
}
// The fmtappend function replaces []byte(fmt.Sprintf(...)) by
// fmt.Appendf(nil, ...), and similarly for Sprint, Sprintln.
func fmtappendf(pass *analysis.Pass) (any, error) {
index := pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
for _, fn := range []types.Object{
index.Object("fmt", "Sprintf"),
index.Object("fmt", "Sprintln"),
index.Object("fmt", "Sprint"),
} {
for curCall := range index.Calls(fn) {
call := curCall.Node().(*ast.CallExpr)
if ek, idx := curCall.ParentEdge(); ek == edge.CallExpr_Args && idx == 0 {
// Is parent a T(fmt.SprintX(...)) conversion?
conv := curCall.Parent().Node().(*ast.CallExpr)
tv := pass.TypesInfo.Types[conv.Fun]
if tv.IsType() && types.Identical(tv.Type, byteSliceType) &&
analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(curCall), versions.Go1_19) {
// Have: []byte(fmt.SprintX(...))
// Find "Sprint" identifier.
var id *ast.Ident
switch e := ast.Unparen(call.Fun).(type) {
case *ast.SelectorExpr:
id = e.Sel // "fmt.Sprint"
case *ast.Ident:
id = e // "Sprint" after `import . "fmt"`
}
old, new := fn.Name(), strings.Replace(fn.Name(), "Sprint", "Append", 1)
edits := []analysis.TextEdit{
{
// delete "[]byte("
Pos: conv.Pos(),
End: conv.Lparen + 1,
},
{
// remove ")"
Pos: conv.Rparen,
End: conv.Rparen + 1,
},
{
Pos: id.Pos(),
End: id.End(),
NewText: []byte(new),
},
{
Pos: call.Lparen + 1,
NewText: []byte("nil, "),
},
}
if len(conv.Args) == 1 {
arg := conv.Args[0]
// Determine if we have T(fmt.SprintX(...)). If so, delete the non-args
// that come before the right parenthesis. Leaving an
// extra comma here produces invalid code. (See
// golang/go#74709)
if arg.End() < conv.Rparen {
edits = append(edits, analysis.TextEdit{
Pos: arg.End(),
End: conv.Rparen,
})
}
}
pass.Report(analysis.Diagnostic{
Pos: conv.Pos(),
End: conv.End(),
Message: fmt.Sprintf("Replace []byte(fmt.%s...) with fmt.%s", old, new),
SuggestedFixes: []analysis.SuggestedFix{{
Message: fmt.Sprintf("Replace []byte(fmt.%s...) with fmt.%s", old, new),
TextEdits: edits,
}},
})
}
}
}
}
return nil, nil
}
ForVar
ForVar はループ変数の不要な再宣言を削除します。Go 1.22 以前では、各イテレーションで新しい変数を作成するために for _, x := range s { x := x ... } と書くことが一般的でした。Go 1.22 で for ループのセマンティクスが変更され、このパターンは不要になりました。このアナライザーは不要な x := x ステートメントを削除します。この修正は range ループにのみ適用されます。
使用例
Before:
for _, x := range items {
x := x // Go 1.22以前のパターン
process(x)
}
After:
for _, x := range items {
process(x) // Go 1.22以降は不要な再宣言が不要
}
メリット: コードが簡潔になり、Go 1.22の新しいセマンティクスを活用できます。
実装コード (forvar.go)
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
import (
"go/ast"
"go/token"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/versions"
)
var ForVarAnalyzer = &analysis.Analyzer{
Name: "forvar",
Doc: analyzerutil.MustExtractDoc(doc, "forvar"),
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: forvar,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#forvar",
}
// forvar offers to fix unnecessary copying of a for variable
func forvar(pass *analysis.Pass) (any, error) {
for curFile := range filesUsingGoVersion(pass, versions.Go1_22) {
for curLoop := range curFile.Preorder((*ast.RangeStmt)(nil)) {
loop := curLoop.Node().(*ast.RangeStmt)
if loop.Tok != token.DEFINE {
continue
}
isLoopVarRedecl := func(stmt ast.Stmt) bool {
if assign, ok := stmt.(*ast.AssignStmt); ok &&
assign.Tok == token.DEFINE &&
len(assign.Lhs) == len(assign.Rhs) {
for i, lhs := range assign.Lhs {
if !(astutil.EqualSyntax(lhs, assign.Rhs[i]) &&
(astutil.EqualSyntax(lhs, loop.Key) ||
astutil.EqualSyntax(lhs, loop.Value))) {
return false
}
}
return true
}
return false
}
// Have: for k, v := range x { stmts }
//
// Delete the prefix of stmts that are
// of the form k := k; v := v; k, v := k, v; v, k := v, k.
for _, stmt := range loop.Body.List {
if isLoopVarRedecl(stmt) {
// { x := x; ... }
// ------
} else if ifstmt, ok := stmt.(*ast.IfStmt); ok &&
ifstmt.Init != nil &&
len(loop.Body.List) == 1 && // must be sole statement in loop body
isLoopVarRedecl(ifstmt.Init) {
// if x := x; cond {
// ------
stmt = ifstmt.Init
} else {
break // stop at first other statement
}
curStmt, _ := curLoop.FindNode(stmt)
edits := refactor.DeleteStmt(pass.Fset.File(stmt.Pos()), curStmt)
if len(edits) > 0 {
pass.Report(analysis.Diagnostic{
Pos: stmt.Pos(),
End: stmt.End(),
Message: "copying variable is unneeded",
SuggestedFixes: []analysis.SuggestedFix{{
Message: "Remove unneeded redeclaration",
TextEdits: edits,
}},
})
}
}
}
}
return nil, nil
}
MapsLoop
MapsLoop はマップを明示的にループするコードを maps パッケージの関数呼び出しに置き換えます。Go 1.23 で追加された maps パッケージの関数を使用します。
使用例
Before:
// マップのコピー
for k, v := range source {
dest[k] = v
}
// 新しいマップの作成
m := make(map[string]int)
for k, v := range source {
m[k] = v
}
After:
// マップのコピー
maps.Copy(dest, source)
// 新しいマップの作成
m := maps.Clone(source)
注意: maps.Clone への変換は保守的に適用されます。これはソースマップの nil 性を保持するため、元のコードが nil マップを同じように処理していない場合、動作の微妙な変化を引き起こす可能性があります。
メリット: コードが簡潔になり、意図が明確になります。
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
// This file defines modernizers that use the "maps" package.
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typeparams"
"golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/versions"
)
var MapsLoopAnalyzer = &analysis.Analyzer{
Name: "mapsloop",
Doc: analyzerutil.MustExtractDoc(doc, "mapsloop"),
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: mapsloop,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#mapsloop",
}
// The mapsloop pass offers to simplify a loop of map insertions:
//
// for k, v := range x {
// m[k] = v
// }
//
// by a call to go1.23's maps package.
func mapsloop(pass *analysis.Pass) (any, error) {
// Skip the analyzer in packages where its
// fixes would create an import cycle.
if within(pass, "maps", "bytes", "runtime") {
return nil, nil
}
info := pass.TypesInfo
// check is called for each statement of this form:
// for k, v := range x { m[k] = v }
check := func(file *ast.File, curRange inspector.Cursor, assign *ast.AssignStmt, m, x ast.Expr) {
// Is x a map or iter.Seq2?
tx := types.Unalias(info.TypeOf(x))
var xmap bool
switch typeparams.CoreType(tx).(type) {
case *types.Map:
xmap = true
case *types.Signature:
k, v, ok := assignableToIterSeq2(tx)
if !ok {
return
}
xmap = false
tx = types.NewMap(k, v)
default:
return
}
// Choose function.
var funcName string
funcName = cond(xmap, "Copy", "Insert")
// Report diagnostic, and suggest fix.
rng := curRange.Node()
prefix, importEdits := refactor.AddImport(info, file, "maps", "maps", funcName, rng.Pos())
var (
newText []byte
start, end token.Pos
)
// Replace loop with call statement.
start, end = rng.Pos(), rng.End()
newText = fmt.Appendf(nil, "%s%s%s(%s, %s)",
allComments(file, start, end),
prefix,
funcName,
astutil.Format(pass.Fset, m),
astutil.Format(pass.Fset, x))
pass.Report(analysis.Diagnostic{
Pos: assign.Lhs[0].Pos(),
End: assign.Lhs[0].End(),
Message: "Replace m[k]=v loop with maps." + funcName,
SuggestedFixes: []analysis.SuggestedFix{{
Message: "Replace m[k]=v loop with maps." + funcName,
TextEdits: append(importEdits, []analysis.TextEdit{{
Pos: start,
End: end,
NewText: newText,
}}...),
}},
})
}
// Find all range loops around m[k] = v.
for curFile := range filesUsingGoVersion(pass, versions.Go1_23) {
file := curFile.Node().(*ast.File)
for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) {
rng := curRange.Node().(*ast.RangeStmt)
if rng.Tok == token.DEFINE &&
rng.Key != nil &&
rng.Value != nil &&
isAssignBlock(rng.Body) {
// Have: for k, v := range x { lhs = rhs }
assign := rng.Body.List[0].(*ast.AssignStmt)
if index, ok := assign.Lhs[0].(*ast.IndexExpr); ok &&
len(assign.Lhs) == 1 &&
astutil.EqualSyntax(rng.Key, index.Index) &&
astutil.EqualSyntax(rng.Value, assign.Rhs[0]) {
if tmap, ok := typeparams.CoreType(info.TypeOf(index.X)).(*types.Map); ok &&
types.Identical(info.TypeOf(index), info.TypeOf(rng.Value)) &&
types.Identical(tmap.Key(), info.TypeOf(rng.Key)) {
// Have: for k, v := range x { m[k] = v }
check(file, curRange, assign, index.X, rng.X)
}
}
}
}
}
return nil, nil
}
// assignableToIterSeq2 reports whether t is assignable to
// iter.Seq[K, V] and returns K and V if so.
func assignableToIterSeq2(t types.Type) (k, v types.Type, ok bool) {
if is[*types.Named](t) {
if !typesinternal.IsTypeNamed(t, "iter", "Seq2") {
return
}
t = t.Underlying()
}
if t, ok := t.(*types.Signature); ok {
if t.Params().Len() == 1 && t.Results().Len() == 0 {
if yield, ok := t.Params().At(0).Type().(*types.Signature); ok {
if yield.Params().Len() == 2 &&
yield.Results().Len() == 1 &&
types.Identical(yield.Results().At(0).Type(), builtinBool.Type()) {
return yield.Params().At(0).Type(), yield.Params().At(1).Type(), true
}
}
}
}
return
}
MinMax
MinMax は条件付き代入を簡略化し、Go 1.21 で導入された組み込みの min と max 関数の使用を提案します。
使用例
Before:
if a < b {
x = a
} else {
x = b
}
if x > y {
result = x
} else {
result = y
}
After:
x = min(a, b)
result = max(x, y)
注意: このアナライザーは浮動小数点型に対する提案を避けます。NaN 値に対する min と max の動作が元の if/else ステートメントと異なる可能性があるためです。
メリット: コードが簡潔になり、意図が明確になります。
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/analysis/analyzerutil"
typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/typeparams"
"golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
)
var MinMaxAnalyzer = &analysis.Analyzer{
Name: "minmax",
Doc: analyzerutil.MustExtractDoc(doc, "minmax"),
Requires: []*analysis.Analyzer{
inspect.Analyzer,
typeindexanalyzer.Analyzer,
},
Run: minmax,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#minmax",
}
// The minmax pass replaces if/else statements with calls to min or max,
// and removes user-defined min/max functions that are equivalent to built-ins.
func minmax(pass *analysis.Pass) (any, error) {
// Check for user-defined min/max functions that can be removed
checkUserDefinedMinMax(pass)
// check is called for all statements of this form:
// if a < b { lhs = rhs }
check := func(file *ast.File, curIfStmt inspector.Cursor, compare *ast.BinaryExpr) {
var (
ifStmt = curIfStmt.Node().(*ast.IfStmt)
tassign = ifStmt.Body.List[0].(*ast.AssignStmt)
a = compare.X
b = compare.Y
lhs = tassign.Lhs[0]
rhs = tassign.Rhs[0]
sign = isInequality(compare.Op)
)
if fblock, ok := ifStmt.Else.(*ast.BlockStmt); ok && isAssignBlock(fblock) {
fassign := fblock.List[0].(*ast.AssignStmt)
// Have: if a < b { lhs = rhs } else { lhs2 = rhs2 }
lhs2 := fassign.Lhs[0]
rhs2 := fassign.Rhs[0]
// For pattern 1, check that:
// - lhs = lhs2
// - {rhs,rhs2} = {a,b}
if astutil.EqualSyntax(lhs, lhs2) {
if astutil.EqualSyntax(rhs, a) && astutil.EqualSyntax(rhs2, b) {
sign = +sign
} else if astutil.EqualSyntax(rhs2, a) && astutil.EqualSyntax(rhs, b) {
sign = -sign
} else {
return
}
sym := cond(sign < 0, "min", "max")
if !is[*types.Builtin](lookup(pass.TypesInfo, curIfStmt, sym)) {
return // min/max function is shadowed
}
// pattern 1
pass.Report(analysis.Diagnostic{
// Highlight the condition a < b.
Pos: compare.Pos(),
End: compare.End(),
Message: fmt.Sprintf("if/else statement can be modernized using %s", sym),
SuggestedFixes: []analysis.SuggestedFix{{
Message: fmt.Sprintf("Replace if statement with %s", sym),
TextEdits: []analysis.TextEdit{{
// Replace IfStmt with lhs = min(a, b).
Pos: ifStmt.Pos(),
End: ifStmt.End(),
NewText: fmt.Appendf(nil, "%s = %s(%s, %s)",
astutil.Format(pass.Fset, lhs),
sym,
astutil.Format(pass.Fset, a),
astutil.Format(pass.Fset, b),
),
}},
}},
})
}
}
}
// Find all "if a < b { lhs = rhs }" statements.
info := pass.TypesInfo
for curFile := range filesUsingGoVersion(pass, versions.Go1_21) {
astFile := curFile.Node().(*ast.File)
for curIfStmt := range curFile.Preorder((*ast.IfStmt)(nil)) {
ifStmt := curIfStmt.Node().(*ast.IfStmt)
if compare, ok := ifStmt.Cond.(*ast.BinaryExpr); ok &&
ifStmt.Init == nil &&
isInequality(compare.Op) != 0 &&
isAssignBlock(ifStmt.Body) {
// a blank var has no type.
if tLHS := info.TypeOf(ifStmt.Body.List[0].(*ast.AssignStmt).Lhs[0]); tLHS != nil && !maybeNaN(tLHS) {
// Have: if a < b { lhs = rhs }
check(astFile, curIfStmt, compare)
}
}
}
}
return nil, nil
}
// isInequality reports non-zero if tok is one of < <= => >:
// +1 for > and -1 for <.
func isInequality(tok token.Token) int {
switch tok {
case token.LEQ, token.LSS:
return -1
case token.GEQ, token.GTR:
return +1
}
return 0
}
// isAssignBlock reports whether b is a block of the form { lhs = rhs }.
func isAssignBlock(b *ast.BlockStmt) bool {
if len(b.List) != 1 {
return false
}
return isSimpleAssign(b.List[0])
}
// isSimpleAssign reports whether n has the form "lhs = rhs" or "lhs := rhs".
func isSimpleAssign(n ast.Node) bool {
assign, ok := n.(*ast.AssignStmt)
return ok &&
(assign.Tok == token.ASSIGN || assign.Tok == token.DEFINE) &&
len(assign.Lhs) == 1 &&
len(assign.Rhs) == 1
}
// maybeNaN reports whether t is (or may be) a floating-point type.
func maybeNaN(t types.Type) bool {
t = typeparams.CoreType(t)
if t == nil {
return true // fail safe
}
if basic, ok := t.(*types.Basic); ok && basic.Info()&types.IsFloat != 0 {
return true
}
return false
}
NewExpr
NewExpr は Go 1.26 の組み込み new(expr) 関数を使用してコードを簡略化します。
使用例
Before:
// ラッパー関数
func intPtr(x int) *int {
return &x
}
// 使用
value := intPtr(42)
After:
// ラッパー関数(インライン可能)
//go:fix inline
func intPtr(x int) *int {
return new(x)
}
// または直接使用
value := new(42)
このようなラッパー関数は、JSON や protobuf などの Go シリアライゼーションパッケージでよく見られます。ポインタはしばしばオプショナリティを表現するために使用されます。
メリット: new(expr) を使用することで、関数がインライン化可能になり、パフォーマンスが向上する可能性があります。
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
import (
_ "embed"
"fmt"
"go/ast"
"go/token"
"go/types"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/versions"
)
var NewExprAnalyzer = &analysis.Analyzer{
Name: "newexpr",
Doc: analyzerutil.MustExtractDoc(doc, "newexpr"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#newexpr",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
FactTypes: []analysis.Fact{&newLike{}},
}
func run(pass *analysis.Pass) (any, error) {
var (
inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
info = pass.TypesInfo
)
// Detect functions that are new-like, i.e. have the form:
//
// func f(x T) *T { return &x }
//
// meaning that it is equivalent to new(x), if x has type T.
for curFuncDecl := range inspect.Root().Preorder((*ast.FuncDecl)(nil)) {
decl := curFuncDecl.Node().(*ast.FuncDecl)
fn := info.Defs[decl.Name].(*types.Func)
if decl.Body != nil && len(decl.Body.List) == 1 {
if ret, ok := decl.Body.List[0].(*ast.ReturnStmt); ok && len(ret.Results) == 1 {
if unary, ok := ret.Results[0].(*ast.UnaryExpr); ok && unary.Op == token.AND {
if id, ok := unary.X.(*ast.Ident); ok {
if v, ok := info.Uses[id].(*types.Var); ok {
sig := fn.Signature()
if sig.Results().Len() == 1 &&
is[*types.Pointer](sig.Results().At(0).Type()) &&
sig.Params().Len() == 1 &&
sig.Params().At(0) == v {
// Export a fact for each one.
pass.ExportObjectFact(fn, &newLike{})
// Check file version.
file := astutil.EnclosingFile(curFuncDecl)
if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_26) {
continue // new(expr) not available in this file
}
var edits []analysis.TextEdit
// If 'new' is not shadowed, replace func body: &x -> new(x).
curRet, _ := curFuncDecl.FindNode(ret)
if lookup(info, curRet, "new") == builtinNew {
edits = []analysis.TextEdit{
// return &x
// ---- -
// return new(x)
{
Pos: unary.OpPos,
End: unary.OpPos + token.Pos(len("&")),
NewText: []byte("new("),
},
{
Pos: unary.X.End(),
End: unary.X.End(),
NewText: []byte(")"),
},
}
}
// Add a //go:fix inline annotation, if not already present.
if !strings.Contains(decl.Doc.Text(), "go:fix inline") {
edits = append(edits, analysis.TextEdit{
Pos: decl.Pos(),
End: decl.Pos(),
NewText: []byte("//go:fix inline\n"),
})
}
if len(edits) > 0 {
pass.Report(analysis.Diagnostic{
Pos: decl.Name.Pos(),
End: decl.Name.End(),
Message: fmt.Sprintf("%s can be an inlinable wrapper around new(expr)", decl.Name),
SuggestedFixes: []analysis.SuggestedFix{
{
Message: "Make %s an inlinable wrapper around new(expr)",
TextEdits: edits,
},
},
})
}
}
}
}
}
}
}
}
// Report and transform calls, when safe.
for curCall := range inspect.Root().Preorder((*ast.CallExpr)(nil)) {
call := curCall.Node().(*ast.CallExpr)
var fact newLike
if fn, ok := typeutil.Callee(info, call).(*types.Func); ok &&
pass.ImportObjectFact(fn, &fact) {
// Check file version.
file := astutil.EnclosingFile(curCall)
if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_26) {
continue
}
// Check new is not shadowed.
if lookup(info, curCall, "new") != builtinNew {
continue
}
pass.Report(analysis.Diagnostic{
Pos: call.Pos(),
End: call.End(),
Message: fmt.Sprintf("call of %s(x) can be simplified to new(x)", fn.Name()),
SuggestedFixes: []analysis.SuggestedFix{{
Message: fmt.Sprintf("Simplify %s(x) to new(x)", fn.Name()),
TextEdits: []analysis.TextEdit{{
Pos: call.Fun.Pos(),
End: call.Fun.End(),
NewText: []byte("new"),
}},
}},
})
}
}
return nil, nil
}
// A newLike fact records that its associated function is "new-like".
type newLike struct{}
func (*newLike) AFact() {}
func (*newLike) String() string { return "newlike" }
OmitZero
OmitZero は構造体フィールドの omitempty を omitzero に置き換えることを提案します。このアナライザーは、構造体自体であるフィールドに omitempty JSON 構造体タグが使用されている場合を識別します。構造体型のフィールドでは、omitempty タグは json.Marshal と json.Unmarshal の動作に影響しません。アナライザーは2つの提案を提供します: タグを削除するか、Go 1.24 で追加された omitzero に置き換えるかです。omitzero は構造体値がゼロ値の場合にフィールドを正しく省略します。
ただし、他のシリアライゼーションパッケージ(特に kubebuilder)は json:",omitzero" タグに独自の解釈を持っている可能性があるため、このアナライザーは +kubebuilder アノテーションを含むパッケージでは変更を行いません。
omitempty を omitzero に置き換えることは動作の変更です。元のコードは常に構造体フィールドをエンコードしますが、修正されたコードはゼロ値の場合は省略します。
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
import (
"go/ast"
"go/types"
"reflect"
"strconv"
"strings"
"sync"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/versions"
)
var OmitZeroAnalyzer = &analysis.Analyzer{
Name: "omitzero",
Doc: analyzerutil.MustExtractDoc(doc, "omitzero"),
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: omitzero,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#omitzero",
}
// The omitzero pass searches for instances of "omitempty" in a json field tag on a
// struct. Since "omitempty" does not have any effect when applied to a struct field,
// it suggests either deleting "omitempty" or replacing it with "omitzero", which
// correctly excludes structs from a json encoding.
func omitzero(pass *analysis.Pass) (any, error) {
// usesKubebuilder reports whether "+kubebuilder:" appears in
// any comment in the package, since it has its own
// interpretation of what omitzero means; see go.dev/issue/76649.
usesKubebuilder := sync.OnceValue[bool](func() bool {
for _, file := range pass.Files {
for _, comment := range file.Comments {
if strings.Contains(comment.Text(), "+kubebuilder:") {
return true
}
}
}
return false
})
checkField := func(field *ast.Field) {
typ := pass.TypesInfo.TypeOf(field.Type)
_, ok := typ.Underlying().(*types.Struct)
if !ok {
return
}
tag := field.Tag
if tag == nil {
return
}
tagconv, _ := strconv.Unquote(tag.Value)
match := omitemptyRegex.FindStringSubmatchIndex(tagconv)
if match == nil {
return
}
omitEmpty, err := astutil.RangeInStringLiteral(field.Tag, match[2], match[3])
if err != nil {
return
}
var remove analysis.Range = omitEmpty
jsonTag := reflect.StructTag(tagconv).Get("json")
if jsonTag == ",omitempty" {
if match[1]-match[0] == len(tagconv) {
remove = field.Tag
} else {
remove, err = astutil.RangeInStringLiteral(field.Tag, match[0], match[1])
if err != nil {
return
}
}
}
if usesKubebuilder() {
return
}
pass.Report(analysis.Diagnostic{
Pos: field.Tag.Pos(),
End: field.Tag.End(),
Message: "Omitempty has no effect on nested struct fields",
SuggestedFixes: []analysis.SuggestedFix{
{
Message: "Remove redundant omitempty tag",
TextEdits: []analysis.TextEdit{
{
Pos: remove.Pos(),
End: remove.End(),
},
},
},
{
Message: "Replace omitempty with omitzero (behavior change)",
TextEdits: []analysis.TextEdit{
{
Pos: omitEmpty.Pos(),
End: omitEmpty.End(),
NewText: []byte(",omitzero"),
},
},
},
},
})
}
for curFile := range filesUsingGoVersion(pass, versions.Go1_24) {
for curStruct := range curFile.Preorder((*ast.StructType)(nil)) {
for _, curField := range curStruct.Node().(*ast.StructType).Fields.List {
checkField(curField)
}
}
}
return nil, nil
}
plusBuild
plusBuild は古い //+build コメントを削除する修正を提案します。以下の形式の古いビルドタグ:
//+build linux,amd64
を、Go 1.18 スタイルのタグも含むファイルから削除します:
//go:build linux && amd64
(古いタグと新しいタグが一貫しているかどうかはチェックしません。それは vet スイートの ‘buildtag’ アナライザーの仕事です。)
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
import (
"go/ast"
"go/parser"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/goplsexport"
"golang.org/x/tools/internal/versions"
)
var plusBuildAnalyzer = &analysis.Analyzer{
Name: "plusbuild",
Doc: analyzerutil.MustExtractDoc(doc, "plusbuild"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#plusbuild",
Run: plusbuild,
}
func init() {
// Export to gopls until this is a published modernizer.
goplsexport.PlusBuildModernizer = plusBuildAnalyzer
}
func plusbuild(pass *analysis.Pass) (any, error) {
check := func(f *ast.File) {
if !analyzerutil.FileUsesGoVersion(pass, f, versions.Go1_18) {
return
}
for _, g := range f.Comments {
sawGoBuild := false
for _, c := range g.List {
if sawGoBuild && strings.HasPrefix(c.Text, "// +build ") {
pass.Report(analysis.Diagnostic{
Pos: c.Pos(),
End: c.End(),
Message: "+build line is no longer needed",
SuggestedFixes: []analysis.SuggestedFix{{
Message: "Remove obsolete +build line",
TextEdits: []analysis.TextEdit{{
Pos: c.Pos(),
End: c.End(),
}},
}},
})
break
}
if strings.HasPrefix(c.Text, "//go:build ") {
sawGoBuild = true
}
}
}
}
for _, f := range pass.Files {
check(f)
}
for _, name := range pass.IgnoredFiles {
if strings.HasSuffix(name, ".go") {
f, err := parser.ParseFile(pass.Fset, name, nil, parser.ParseComments|parser.SkipObjectResolution)
if err != nil {
continue
}
check(f)
}
}
return nil, nil
}
RangeInt
RangeInt は従来の for ループを整数に対する for-range に置き換えることを提案します。Go 1.22 スタイルのより慣用的な書き方に変換します。
使用例
Before:
for i := 0; i < n; i++ {
process(i)
}
for i := 0; i < len(items); i++ {
fmt.Println(items[i])
}
After:
for i := range n {
process(i)
}
for i := range items {
fmt.Println(items[i])
}
制限: この変換は、(a) ループ変数がループ本体内で変更されない場合、および (b) ループの制限式がループ内で変更されない場合にのみ適用されます。for range はそのオペランドを一度だけ評価するためです。
メリット: より簡潔で読みやすく、Go 1.22の新しい機能を活用できます。
実装コード (rangeint.go)
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysis/analyzerutil"
typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/moreiters"
"golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
)
var RangeIntAnalyzer = &analysis.Analyzer{
Name: "rangeint",
Doc: analyzerutil.MustExtractDoc(doc, "rangeint"),
Requires: []*analysis.Analyzer{
inspect.Analyzer,
typeindexanalyzer.Analyzer,
},
Run: rangeint,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#rangeint",
}
// rangeint offers a fix to replace a 3-clause 'for' loop:
//
// for i := 0; i < limit; i++ {}
//
// by a range loop with an integer operand:
//
// for i := range limit {}
func rangeint(pass *analysis.Pass) (any, error) {
var (
info = pass.TypesInfo
typeindex = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
)
for curFile := range filesUsingGoVersion(pass, versions.Go1_22) {
nextLoop:
for curLoop := range curFile.Preorder((*ast.ForStmt)(nil)) {
loop := curLoop.Node().(*ast.ForStmt)
if init, ok := loop.Init.(*ast.AssignStmt); ok &&
isSimpleAssign(init) &&
is[*ast.Ident](init.Lhs[0]) &&
isZeroIntConst(info, init.Rhs[0]) {
// Have: for i = 0; ... (or i := 0)
index := init.Lhs[0].(*ast.Ident)
if compare, ok := loop.Cond.(*ast.BinaryExpr); ok &&
compare.Op == token.LSS &&
astutil.EqualSyntax(compare.X, init.Lhs[0]) {
// Have: for i = 0; i < limit; ... {}
limit := compare.Y
// If limit is "len(slice)", simplify it to "slice".
if call, ok := limit.(*ast.CallExpr); ok &&
typeutil.Callee(info, call) == builtinLen &&
is[*types.Slice](info.TypeOf(call.Args[0]).Underlying()) {
limit = call.Args[0]
}
// Check the form of limit: must be a constant,
// or a local var that is not assigned or address-taken.
limitOK := false
if info.Types[limit].Value != nil {
limitOK = true // constant
} else if id, ok := limit.(*ast.Ident); ok {
if v, ok := info.Uses[id].(*types.Var); ok &&
!(v.Exported() && typesinternal.IsPackageLevel(v)) {
// limit is a local or unexported global var.
for cur := range typeindex.Uses(v) {
if isScalarLvalue(info, cur) {
// Limit var is assigned or address-taken.
continue nextLoop
}
}
limitOK = true
}
}
if !limitOK {
continue nextLoop
}
validIncrement := false
if inc, ok := loop.Post.(*ast.IncDecStmt); ok &&
inc.Tok == token.INC &&
astutil.EqualSyntax(compare.X, inc.X) {
// Have: i++
validIncrement = true
} else if assign, ok := loop.Post.(*ast.AssignStmt); ok &&
assign.Tok == token.ADD_ASSIGN &&
len(assign.Rhs) == 1 && isIntLiteral(info, assign.Rhs[0], 1) &&
len(assign.Lhs) == 1 && astutil.EqualSyntax(compare.X, assign.Lhs[0]) {
// Have: i += 1
validIncrement = true
}
if validIncrement {
// Have: for i = 0; i < limit; i++ {}
// Find references to i within the loop body.
v := info.ObjectOf(index).(*types.Var)
if typesinternal.IsPackageLevel(v) {
continue nextLoop
}
// If v is a named result, it is implicitly
// used after the loop (go.dev/issue/76880).
if moreiters.Contains(enclosingSignature(curLoop, info).Results().Variables(), v) {
continue nextLoop
}
used := false
for curId := range curLoop.Child(loop.Body).Preorder((*ast.Ident)(nil)) {
id := curId.Node().(*ast.Ident)
if info.Uses[id] == v {
used = true
// Reject if any is an l-value (assigned or address-taken):
// a "for range int" loop does not respect assignments to
// the loop variable.
if isScalarLvalue(info, curId) {
continue nextLoop
}
}
}
// If i is no longer used, delete "i := ".
var edits []analysis.TextEdit
if !used && init.Tok == token.DEFINE {
edits = append(edits, analysis.TextEdit{
Pos: index.Pos(),
End: init.Rhs[0].Pos(),
})
}
pass.Report(analysis.Diagnostic{
Pos: init.Pos(),
End: loop.Post.End(),
Message: "for loop can be modernized using range over int",
SuggestedFixes: []analysis.SuggestedFix{{
Message: fmt.Sprintf("Replace for loop with range %s",
astutil.Format(pass.Fset, limit)),
TextEdits: append(edits, []analysis.TextEdit{
// for i := 0; i < limit; i++ {}
// ----- ---
// -------
// for i := range limit {}
// Delete init.
{
Pos: init.Rhs[0].Pos(),
End: limit.Pos(),
NewText: []byte("range "),
},
// Delete inc.
{
Pos: limit.End(),
End: loop.Post.End(),
},
}...),
}},
})
}
}
}
}
}
return nil, nil
}
ReflectTypeFor
ReflectTypeFor は reflect.TypeOf(x) の使用を Go 1.22 で導入された reflect.TypeFor に置き換える修正を提案します。実行時の型がコンパイル時に既知の場合、例えば:
reflect.TypeOf(uint32(0)) -> reflect.TypeFor[uint32]()
reflect.TypeOf((*ast.File)(nil)) -> reflect.TypeFor[*ast.File]()
また、インターフェース型の実行時型を返すために reflect.TypeOf を使用する以下の構築を簡略化する修正も提供します:
reflect.TypeOf((*io.Reader)(nil)).Elem()
を以下に置き換えます:
reflect.TypeFor[io.Reader]()
実行時型が動的な場合(例: var r io.Reader = ...; reflect.TypeOf(r))や、オペランドに潜在的な副作用がある場合は修正は提案されません。
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
// This file defines modernizers that use the "reflect" package.
import (
"go/ast"
"go/types"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysis/analyzerutil"
typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
)
var ReflectTypeForAnalyzer = &analysis.Analyzer{
Name: "reflecttypefor",
Doc: analyzerutil.MustExtractDoc(doc, "reflecttypefor"),
Requires: []*analysis.Analyzer{
inspect.Analyzer,
typeindexanalyzer.Analyzer,
},
Run: reflecttypefor,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#reflecttypefor",
}
func reflecttypefor(pass *analysis.Pass) (any, error) {
var (
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo
reflectTypeOf = index.Object("reflect", "TypeOf")
)
for curCall := range index.Calls(reflectTypeOf) {
call := curCall.Node().(*ast.CallExpr)
// Have: reflect.TypeOf(expr)
expr := call.Args[0]
if !typesinternal.NoEffects(info, expr) {
continue // don't eliminate operand: may have effects
}
t := info.TypeOf(expr)
var edits []analysis.TextEdit
// Special case for TypeOf((*T)(nil)).Elem(),
// needed when T is an interface type.
if astutil.IsChildOf(curCall, edge.SelectorExpr_X) {
curSel := unparenEnclosing(curCall).Parent()
if astutil.IsChildOf(curSel, edge.CallExpr_Fun) {
call2 := unparenEnclosing(curSel).Parent().Node().(*ast.CallExpr)
obj := typeutil.Callee(info, call2)
if typesinternal.IsMethodNamed(obj, "reflect", "Type", "Elem") {
if ptr, ok := t.(*types.Pointer); ok {
t = ptr.Elem()
edits = []analysis.TextEdit{{
Pos: call.End(),
End: call2.End(),
}}
}
}
}
}
// TypeOf(x) where x has an interface type is a
// dynamic operation; don't transform it to TypeFor.
if types.IsInterface(t) && edits == nil {
continue
}
file := astutil.EnclosingFile(curCall)
if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_22) {
continue
}
tokFile := pass.Fset.File(file.Pos())
qual := typesinternal.FileQualifier(file, pass.Pkg)
tstr := types.TypeString(t, qual)
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
continue
}
if isComplicatedType(t) {
continue
}
oldLen := int(expr.End() - expr.Pos())
newLen := len(tstr)
if newLen >= 16 && newLen > 3*oldLen {
continue
}
curArg0 := curCall.ChildAt(edge.CallExpr_Args, 0)
edits = append(edits, refactor.DeleteUnusedVars(index, info, tokFile, curArg0)...)
pass.Report(analysis.Diagnostic{
Pos: call.Fun.Pos(),
End: call.Fun.End(),
Message: "reflect.TypeOf call can be simplified using TypeFor",
SuggestedFixes: []analysis.SuggestedFix{{
Message: "Replace TypeOf by TypeFor",
TextEdits: append([]analysis.TextEdit{
{
Pos: sel.Sel.Pos(),
End: sel.Sel.End(),
NewText: []byte("TypeFor[" + tstr + "]"),
},
{
Pos: call.Lparen + 1,
End: call.Rparen,
},
}, edits...),
}},
})
}
return nil, nil
}
// isComplicatedType reports whether type t is complicated, e.g. it is or contains an
// unnamed struct, interface, or function signature.
func isComplicatedType(t types.Type) bool {
var check func(typ types.Type) bool
check = func(typ types.Type) bool {
switch t := typ.(type) {
case typesinternal.NamedOrAlias:
for ta := range t.TypeArgs().Types() {
if check(ta) {
return true
}
}
return false
case *types.Struct, *types.Interface, *types.Signature:
return true
case *types.Pointer:
return check(t.Elem())
case *types.Slice:
return check(t.Elem())
case *types.Array:
return check(t.Elem())
case *types.Chan:
return check(t.Elem())
case *types.Map:
return check(t.Key()) || check(t.Elem())
case *types.Basic:
return false
case *types.TypeParam:
return false
default:
return true
}
}
return check(t)
}
SlicesContains
SlicesContains はスライス内の要素の存在をチェックするループを簡略化します。Go 1.21 で追加された slices.Contains または slices.ContainsFunc の呼び出しに置き換えます。
使用例
Before:
found := false
for _, v := range items {
if v == target {
found = true
break
}
}
for i, elem := range list {
if elem > 10 {
return true
}
}
return false
After:
found := slices.Contains(items, target)
return slices.ContainsFunc(list, func(elem int) bool {
return elem > 10
})
注意: ターゲット要素の式に副作用がある場合、この変換により、それらの効果はテストされるスライス要素ごとに一度ではなく、一度だけ発生します。
メリット: コードが簡潔になり、意図が明確になります。
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysis/analyzerutil"
typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typeparams"
"golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
)
var SlicesContainsAnalyzer = &analysis.Analyzer{
Name: "slicescontains",
Doc: analyzerutil.MustExtractDoc(doc, "slicescontains"),
Requires: []*analysis.Analyzer{
inspect.Analyzer,
typeindexanalyzer.Analyzer,
},
Run: slicescontains,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicescontains",
}
// The slicescontains pass identifies loops that can be replaced by a
// call to slices.Contains{,Func}. For example:
//
// for i, elem := range s {
// if elem == needle {
// ...
// break
// }
// }
//
// =>
//
// if slices.Contains(s, needle) { ... }
func slicescontains(pass *analysis.Pass) (any, error) {
// Skip the analyzer in packages where its
// fixes would create an import cycle.
if within(pass, "slices", "runtime") {
return nil, nil
}
var (
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo
)
// check is called for each RangeStmt of this form:
// for i, elem := range s { if cond { ... } }
check := func(file *ast.File, curRange inspector.Cursor) {
rng := curRange.Node().(*ast.RangeStmt)
ifStmt := rng.Body.List[0].(*ast.IfStmt)
// isSliceElem reports whether e denotes the
// current slice element (elem or s[i]).
isSliceElem := func(e ast.Expr) bool {
if rng.Value != nil && astutil.EqualSyntax(e, rng.Value) {
return true // "elem"
}
if x, ok := e.(*ast.IndexExpr); ok &&
astutil.EqualSyntax(x.X, rng.X) &&
astutil.EqualSyntax(x.Index, rng.Key) {
return true // "s[i]"
}
return false
}
// Examine the condition for one of these forms:
//
// - if elem or s[i] == needle { ... } => Contains
// - if predicate(s[i] or elem) { ... } => ContainsFunc
var (
funcName string // "Contains" or "ContainsFunc"
arg2 ast.Expr // second argument to func (needle or predicate)
)
switch cond := ifStmt.Cond.(type) {
case *ast.BinaryExpr:
if cond.Op == token.EQL {
var elem ast.Expr
if isSliceElem(cond.X) {
funcName = "Contains"
elem = cond.X
arg2 = cond.Y // "if elem == needle"
} else if isSliceElem(cond.Y) {
funcName = "Contains"
elem = cond.Y
arg2 = cond.X // "if needle == elem"
}
// Reject if elem and needle have different types.
if elem != nil {
tElem := info.TypeOf(elem)
tNeedle := info.TypeOf(arg2)
if !types.Identical(tElem, tNeedle) {
if !types.AssignableTo(tNeedle, tElem) {
return
}
return
}
}
}
case *ast.CallExpr:
if len(cond.Args) == 1 &&
isSliceElem(cond.Args[0]) &&
typeutil.Callee(info, cond) != nil {
sig, isSignature := info.TypeOf(cond.Fun).(*types.Signature)
if isSignature {
if sig.Variadic() {
return
}
var (
tElem = typeparams.CoreType(info.TypeOf(rng.X)).(*types.Slice).Elem()
tParam = sig.Params().At(0).Type()
)
if !types.Identical(tElem, tParam) {
return
}
}
funcName = "ContainsFunc"
arg2 = cond.Fun // "if predicate(elem)"
}
}
if funcName == "" {
return // not a candidate for Contains{,Func}
}
// Prepare slices.Contains{,Func} call.
prefix, importEdits := refactor.AddImport(info, file, "slices", "slices", funcName, rng.Pos())
contains := fmt.Sprintf("%s%s(%s, %s)",
prefix,
funcName,
astutil.Format(pass.Fset, rng.X),
astutil.Format(pass.Fset, arg2))
report := func(edits []analysis.TextEdit) {
pass.Report(analysis.Diagnostic{
Pos: rng.Pos(),
End: rng.End(),
Message: fmt.Sprintf("Loop can be simplified using slices.%s", funcName),
SuggestedFixes: []analysis.SuggestedFix{{
Message: "Replace loop by call to slices." + funcName,
TextEdits: append(edits, importEdits...),
}},
})
}
// Last statement of body must return/break out of the loop.
curBody, _ := curRange.FindNode(ifStmt.Body)
curLastStmt, _ := curBody.LastChild()
switch lastStmt := curLastStmt.Node().(type) {
case *ast.ReturnStmt:
// Have: for ... range seq { if ... { stmts; return x } }
report([]analysis.TextEdit{
// Replace "for ... { if ... " with "if slices.Contains(...)".
{
Pos: rng.Pos(),
End: ifStmt.Body.Pos(),
NewText: fmt.Appendf(nil, "if %s ", contains),
},
// Delete '}' of range statement and preceding space.
{
Pos: ifStmt.Body.End(),
End: rng.End(),
},
})
return
case *ast.BranchStmt:
if lastStmt.Tok == token.BREAK && lastStmt.Label == nil {
// Have: for ... { if ... { stmts; break } }
report([]analysis.TextEdit{
// Replace "for ... { if ... " with "if slices.Contains(...)".
{
Pos: rng.Pos(),
End: ifStmt.Body.Pos(),
NewText: fmt.Appendf(nil, "if %s ", contains),
},
// Delete break statement and preceding space.
{
Pos: func() token.Pos {
if len(ifStmt.Body.List) > 1 {
beforeBreak, _ := curLastStmt.PrevSibling()
return beforeBreak.Node().End()
}
return lastStmt.Pos()
}(),
End: lastStmt.End(),
},
// Delete '}' of range statement and preceding space.
{
Pos: ifStmt.Body.End(),
End: rng.End(),
},
})
return
}
}
}
for curFile := range filesUsingGoVersion(pass, versions.Go1_21) {
file := curFile.Node().(*ast.File)
for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) {
rng := curRange.Node().(*ast.RangeStmt)
if is[*ast.Ident](rng.Key) &&
rng.Tok == token.DEFINE &&
len(rng.Body.List) == 1 &&
is[*types.Slice](typeparams.CoreType(info.TypeOf(rng.X))) {
if ifStmt, ok := rng.Body.List[0].(*ast.IfStmt); ok &&
ifStmt.Init == nil && ifStmt.Else == nil {
// Have: for i, elem := range s { if cond { ... } }
check(file, curRange)
}
}
}
}
return nil, nil
}
SlicesSort
SlicesSort は基本順序型のスライスのソートを簡略化します。
使用例
Before:
sort.Slice(numbers, func(i, j int) bool {
return numbers[i] < numbers[j]
})
sort.Slice(names, func(i, j int) bool {
return names[i] < names[j]
})
After:
slices.Sort(numbers)
slices.Sort(names)
メリット: コードが大幅に簡潔になり、可読性が向上します。
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
import (
"go/ast"
"go/token"
"go/types"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/internal/analysis/analyzerutil"
typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
)
// (Not to be confused with go/analysis/passes/sortslice.)
var SlicesSortAnalyzer = &analysis.Analyzer{
Name: "slicessort",
Doc: analyzerutil.MustExtractDoc(doc, "slicessort"),
Requires: []*analysis.Analyzer{
inspect.Analyzer,
typeindexanalyzer.Analyzer,
},
Run: slicessort,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicessort",
}
// The slicessort pass replaces sort.Slice(slice, less) with
// slices.Sort(slice) when slice is a []T and less is a FuncLit
// equivalent to cmp.Ordered[T].
func slicessort(pass *analysis.Pass) (any, error) {
// Skip the analyzer in packages where its
// fixes would create an import cycle.
if within(pass, "slices", "sort", "runtime") {
return nil, nil
}
var (
info = pass.TypesInfo
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
sortSlice = index.Object("sort", "Slice")
)
for curCall := range index.Calls(sortSlice) {
call := curCall.Node().(*ast.CallExpr)
if lit, ok := call.Args[1].(*ast.FuncLit); ok && len(lit.Body.List) == 1 {
sig := pass.TypesInfo.Types[lit.Type].Type.(*types.Signature)
// Have: sort.Slice(s, func(i, j int) bool { return ... })
s := call.Args[0]
i := sig.Params().At(0)
j := sig.Params().At(1)
if ret, ok := lit.Body.List[0].(*ast.ReturnStmt); ok {
if compare, ok := ret.Results[0].(*ast.BinaryExpr); ok && compare.Op == token.LSS {
// isIndex reports whether e is s[v].
isIndex := func(e ast.Expr, v *types.Var) bool {
index, ok := e.(*ast.IndexExpr)
return ok &&
astutil.EqualSyntax(index.X, s) &&
is[*ast.Ident](index.Index) &&
pass.TypesInfo.Uses[index.Index.(*ast.Ident)] == v
}
file := astutil.EnclosingFile(curCall)
if isIndex(compare.X, i) && isIndex(compare.Y, j) &&
analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_21) {
// Have: sort.Slice(s, func(i, j int) bool { return s[i] < s[j] })
prefix, importEdits := refactor.AddImport(
info, file, "slices", "slices", "Sort", call.Pos())
pass.Report(analysis.Diagnostic{
Pos: call.Fun.Pos(),
End: call.Fun.End(),
Message: "sort.Slice can be modernized using slices.Sort",
SuggestedFixes: []analysis.SuggestedFix{{
Message: "Replace sort.Slice call by slices.Sort",
TextEdits: append(importEdits, []analysis.TextEdit{
{
Pos: call.Fun.Pos(),
End: call.Fun.End(),
NewText: []byte(prefix + "Sort"),
},
{
Pos: call.Args[0].End(),
End: call.Rparen,
},
}...),
}},
})
}
}
}
}
}
return nil, nil
}
stditerators
stditerators は Len/At スタイルの API の代わりにイテレータを使用する修正を提案します。以下の形式の各ループ:
for i := 0; i < x.Len(); i++ {
use(x.At(i))
}
またはその for elem := range x.Len() 相当を、同じデータ型が提供するイテレータに対する range ループに置き換えます:
for elem := range x.All() {
use(elem)
}
ここで、x は標準ライブラリのさまざまなよく知られた型の1つです。
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysis/analyzerutil"
typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/goplsexport"
"golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/stdlib"
"golang.org/x/tools/internal/typesinternal/typeindex"
)
var stditeratorsAnalyzer = &analysis.Analyzer{
Name: "stditerators",
Doc: analyzerutil.MustExtractDoc(doc, "stditerators"),
Requires: []*analysis.Analyzer{
typeindexanalyzer.Analyzer,
},
Run: stditerators,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stditerators",
}
func init() {
// Export to gopls until this is a published modernizer.
goplsexport.StdIteratorsModernizer = stditeratorsAnalyzer
}
// stditeratorsTable records std types that have legacy T.{Len,At}
// iteration methods as well as a newer T.All method that returns an
// iter.Seq.
var stditeratorsTable = [...]struct {
pkgpath, typename, lenmethod, atmethod, itermethod, elemname string
seqn int // 1 or 2 => "for x" or "for _, x"
}{
{"go/types", "Interface", "NumEmbeddeds", "EmbeddedType", "EmbeddedTypes", "etyp", 1},
{"go/types", "Interface", "NumExplicitMethods", "ExplicitMethod", "ExplicitMethods", "method", 1},
{"go/types", "Interface", "NumMethods", "Method", "Methods", "method", 1},
{"go/types", "MethodSet", "Len", "At", "Methods", "method", 1},
{"go/types", "Named", "NumMethods", "Method", "Methods", "method", 1},
{"go/types", "Scope", "NumChildren", "Child", "Children", "child", 1},
{"go/types", "Struct", "NumFields", "Field", "Fields", "field", 1},
{"go/types", "Tuple", "Len", "At", "Variables", "v", 1},
{"go/types", "TypeList", "Len", "At", "Types", "t", 1},
{"go/types", "TypeParamList", "Len", "At", "TypeParams", "tparam", 1},
{"go/types", "Union", "Len", "Term", "Terms", "term", 1},
{"reflect", "Type", "NumField", "Field", "Fields", "field", 1},
{"reflect", "Type", "NumMethod", "Method", "Methods", "method", 1},
{"reflect", "Type", "NumIn", "In", "Ins", "in", 1},
{"reflect", "Type", "NumOut", "Out", "Outs", "out", 1},
{"reflect", "Value", "NumField", "Field", "Fields", "field", 2},
{"reflect", "Value", "NumMethod", "Method", "Methods", "method", 2},
}
// stditerators suggests fixes to replace loops using Len/At-style
// iterator APIs by a range loop over an iterator.
func stditerators(pass *analysis.Pass) (any, error) {
var (
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo
)
for _, row := range stditeratorsTable {
if within(pass, row.pkgpath) {
continue
}
var (
lenMethod = index.Selection(row.pkgpath, row.typename, row.lenmethod)
atMethod = index.Selection(row.pkgpath, row.typename, row.atmethod)
)
// Process each call of x.Len().
nextCall:
for curLenCall := range index.Calls(lenMethod) {
lenSel, ok := ast.Unparen(curLenCall.Node().(*ast.CallExpr).Fun).(*ast.SelectorExpr)
if !ok {
continue
}
var (
rng analysis.Range
curBody inspector.Cursor
indexVar *types.Var
elemVar *types.Var
elem string
edits []analysis.TextEdit
)
// Analyze enclosing loop.
switch first(curLenCall.ParentEdge()) {
case edge.BinaryExpr_Y:
var (
curCmp = curLenCall.Parent()
cmp = curCmp.Node().(*ast.BinaryExpr)
)
if cmp.Op != token.LSS ||
!astutil.IsChildOf(curCmp, edge.ForStmt_Cond) {
continue
}
if id, ok := cmp.X.(*ast.Ident); ok {
var (
v = info.Uses[id].(*types.Var)
curFor = curCmp.Parent()
loop = curFor.Node().(*ast.ForStmt)
)
if v != isIncrementLoop(info, loop) {
continue
}
rng = astutil.RangeOf(loop.For, loop.Post.End())
indexVar = v
curBody = curFor.ChildAt(edge.ForStmt_Body, -1)
elem, elemVar = chooseName(curBody, lenSel.X, indexVar)
elemPrefix := cond(row.seqn == 2, "_, ", "")
edits = []analysis.TextEdit{
{
Pos: v.Pos(),
End: v.Pos() + token.Pos(len(v.Name())),
NewText: []byte(elemPrefix + elem),
},
{
Pos: loop.Init.(*ast.AssignStmt).Rhs[0].Pos(),
End: cmp.Y.Pos(),
NewText: []byte("range "),
},
{
Pos: lenSel.Sel.Pos(),
End: lenSel.Sel.End(),
NewText: []byte(row.itermethod),
},
{
Pos: curLenCall.Node().End(),
End: loop.Post.End(),
},
}
}
case edge.RangeStmt_X:
var (
curRange = curLenCall.Parent()
loop = curRange.Node().(*ast.RangeStmt)
)
if id, ok := loop.Key.(*ast.Ident); ok &&
loop.Value == nil &&
loop.Tok == token.DEFINE {
rng = astutil.RangeOf(loop.Range, loop.X.End())
indexVar = info.Defs[id].(*types.Var)
curBody = curRange.ChildAt(edge.RangeStmt_Body, -1)
elem, elemVar = chooseName(curBody, lenSel.X, indexVar)
elemPrefix := cond(row.seqn == 2, "_, ", "")
edits = []analysis.TextEdit{
{
Pos: loop.Key.Pos(),
End: loop.Key.End(),
NewText: []byte(elemPrefix + elem),
},
{
Pos: lenSel.Sel.Pos(),
End: lenSel.Sel.End(),
NewText: []byte(row.itermethod),
},
}
}
}
if indexVar == nil {
continue
}
// Check that all uses of var i within loop body are x.At(i).
for curUse := range index.Uses(indexVar) {
if !curBody.Contains(curUse) {
continue
}
if ek, argidx := curUse.ParentEdge(); ek != edge.CallExpr_Args || argidx != 0 {
continue nextCall
}
curAtCall := curUse.Parent()
atCall := curAtCall.Node().(*ast.CallExpr)
if typeutil.Callee(info, atCall) != atMethod {
continue nextCall
}
atSel := ast.Unparen(atCall.Fun).(*ast.SelectorExpr)
if !astutil.EqualSyntax(lenSel.X, atSel.X) {
continue nextCall
}
if obj := lookup(info, curAtCall, elem); obj != nil && obj != elemVar && obj.Pos() > indexVar.Pos() {
continue nextCall
}
edits = append(edits, analysis.TextEdit{
Pos: atCall.Pos(),
End: atCall.End(),
NewText: []byte(elem),
})
}
if v, err := methodGoVersion(row.pkgpath, row.typename, row.itermethod); err != nil {
panic(err)
} else if !analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(curLenCall), v.String()) {
continue nextCall
}
pass.Report(analysis.Diagnostic{
Pos: rng.Pos(),
End: rng.End(),
Message: fmt.Sprintf("%s/%s loop can simplified using %s.%s iteration",
row.lenmethod, row.atmethod, row.typename, row.itermethod),
SuggestedFixes: []analysis.SuggestedFix{{
Message: fmt.Sprintf(
"Replace %s/%s loop with %s.%s iteration",
row.lenmethod, row.atmethod, row.typename, row.itermethod),
TextEdits: edits,
}},
})
}
}
return nil, nil
}
// chooseName returns an appropriate fresh name for the index variable
func chooseName(curBody inspector.Cursor, x ast.Expr, i *types.Var) (string, *types.Var) {
// Implementation details omitted for brevity
return "elem", nil
}
// methodGoVersion reports the version at which the method appeared
func methodGoVersion(pkgpath, recvtype, method string) (stdlib.Version, error) {
for _, sym := range stdlib.PackageSymbols[pkgpath] {
if sym.Kind == stdlib.Method {
_, recv, name := sym.SplitMethod()
if recv == recvtype && name == method {
return sym.Version, nil
}
}
}
return 0, fmt.Errorf("methodGoVersion: %s.%s.%s missing from stdlib manifest", pkgpath, recvtype, method)
}
stringscut
stringscut は strings.Index などの特定の使用パターンを Go 1.18 で追加された strings.Cut に置き換えます。例えば:
idx := strings.Index(s, substr)
if idx >= 0 {
return s[:idx]
}
は以下のように置き換えられます:
before, _, ok := strings.Cut(s, substr)
if ok {
return before
}
また、strings.IndexByte の代わりに Index を使用するバリアントや、strings パッケージの代わりに bytes パッケージを使用するバリアントも処理します。
修正は、定義と使用の間に idx、s、または substr 式の潜在的な変更がない場合にのみ提供されます。
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
import (
"fmt"
"go/ast"
"go/constant"
"go/token"
"go/types"
"iter"
"strconv"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysis/analyzerutil"
typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/goplsexport"
"golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
)
var stringscutAnalyzer = &analysis.Analyzer{
Name: "stringscut",
Doc: analyzerutil.MustExtractDoc(doc, "stringscut"),
Requires: []*analysis.Analyzer{
inspect.Analyzer,
typeindexanalyzer.Analyzer,
},
Run: stringscut,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringscut",
}
func init() {
goplsexport.StringsCutModernizer = stringscutAnalyzer
}
// stringscut offers a fix to replace an occurrence of strings.Index{,Byte} with
// strings.{Cut,Contains}, and similar fixes for functions in the bytes package.
func stringscut(pass *analysis.Pass) (any, error) {
var (
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo
stringsIndex = index.Object("strings", "Index")
stringsIndexByte = index.Object("strings", "IndexByte")
bytesIndex = index.Object("bytes", "Index")
bytesIndexByte = index.Object("bytes", "IndexByte")
)
for _, obj := range []types.Object{
stringsIndex,
stringsIndexByte,
bytesIndex,
bytesIndexByte,
} {
nextcall:
for curCall := range index.Calls(obj) {
if !analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(curCall), versions.Go1_18) {
continue
}
indexCall := curCall.Node().(*ast.CallExpr)
obj := typeutil.Callee(info, indexCall)
if obj == nil {
continue
}
var iIdent *ast.Ident
switch ek, idx := curCall.ParentEdge(); ek {
case edge.ValueSpec_Values:
curName := curCall.Parent().ChildAt(edge.ValueSpec_Names, idx)
iIdent = curName.Node().(*ast.Ident)
case edge.AssignStmt_Rhs:
curLhs := curCall.Parent().ChildAt(edge.AssignStmt_Lhs, idx)
iIdent, _ = curLhs.Node().(*ast.Ident)
}
if iIdent == nil {
continue
}
iObj := info.ObjectOf(iIdent)
if iObj == nil {
continue
}
var (
s = indexCall.Args[0]
substr = indexCall.Args[1]
)
if !indexArgValid(info, index, s, indexCall.Pos()) ||
!indexArgValid(info, index, substr, indexCall.Pos()) {
continue nextcall
}
negative, nonnegative, beforeSlice, afterSlice := checkIdxUses(pass.TypesInfo, index.Uses(iObj), s, substr)
if negative == nil && nonnegative == nil && beforeSlice == nil && afterSlice == nil {
continue
}
isContains := (len(negative) > 0 || len(nonnegative) > 0) && len(beforeSlice) == 0 && len(afterSlice) == 0
scope := iObj.Parent()
var (
okVarName = refactor.FreshName(scope, iIdent.Pos(), "ok")
beforeVarName = refactor.FreshName(scope, iIdent.Pos(), "before")
afterVarName = refactor.FreshName(scope, iIdent.Pos(), "after")
foundVarName = refactor.FreshName(scope, iIdent.Pos(), "found")
)
if len(negative) == 0 && len(nonnegative) == 0 {
okVarName = "_"
}
if len(beforeSlice) == 0 {
beforeVarName = "_"
}
if len(afterSlice) == 0 {
afterVarName = "_"
}
var edits []analysis.TextEdit
replace := func(exprs []ast.Expr, new string) {
for _, expr := range exprs {
edits = append(edits, analysis.TextEdit{
Pos: expr.Pos(),
End: expr.End(),
NewText: []byte(new),
})
}
}
indexCallId := typesinternal.UsedIdent(info, indexCall.Fun)
replacedFunc := "Cut"
if isContains {
replacedFunc = "Contains"
replace(negative, "!"+foundVarName)
replace(nonnegative, foundVarName)
edits = append(edits, analysis.TextEdit{
Pos: iIdent.Pos(),
End: iIdent.End(),
NewText: []byte(foundVarName),
}, analysis.TextEdit{
Pos: indexCallId.Pos(),
End: indexCallId.End(),
NewText: []byte("Contains"),
})
} else {
replace(negative, "!"+okVarName)
replace(nonnegative, okVarName)
replace(beforeSlice, beforeVarName)
replace(afterSlice, afterVarName)
edits = append(edits, analysis.TextEdit{
Pos: iIdent.Pos(),
End: iIdent.End(),
NewText: fmt.Appendf(nil, "%s, %s, %s", beforeVarName, afterVarName, okVarName),
}, analysis.TextEdit{
Pos: indexCallId.Pos(),
End: indexCallId.End(),
NewText: []byte("Cut"),
})
}
pass.Report(analysis.Diagnostic{
Pos: indexCall.Fun.Pos(),
End: indexCall.Fun.End(),
Message: fmt.Sprintf("%s.%s can be simplified using %s.%s",
obj.Pkg().Name(), obj.Name(), obj.Pkg().Name(), replacedFunc),
Category: "stringscut",
SuggestedFixes: []analysis.SuggestedFix{{
Message: fmt.Sprintf("Simplify %s.%s call using %s.%s", obj.Pkg().Name(), obj.Name(), obj.Pkg().Name(), replacedFunc),
TextEdits: edits,
}},
})
}
}
return nil, nil
}
// indexArgValid reports whether expr is a valid strings.Index(_, _) arg
func indexArgValid(info *types.Info, index *typeindex.Index, expr ast.Expr, afterPos token.Pos) bool {
tv := info.Types[expr]
if tv.Value != nil {
return true
}
switch expr := expr.(type) {
case *ast.CallExpr:
return types.Identical(tv.Type, byteSliceType) &&
info.Types[expr.Fun].IsType() &&
indexArgValid(info, index, expr.Args[0], afterPos)
case *ast.Ident:
sObj := info.Uses[expr]
sUses := index.Uses(sObj)
return !hasModifyingUses(info, sUses, afterPos)
default:
return false
}
}
// checkIdxUses inspects the uses of i to make sure they match certain criteria
func checkIdxUses(info *types.Info, uses iter.Seq[inspector.Cursor], s, substr ast.Expr) (negative, nonnegative, beforeSlice, afterSlice []ast.Expr) {
// Implementation details omitted for brevity
return nil, nil, nil, nil
}
// hasModifyingUses reports whether any of the uses involve potential modifications
func hasModifyingUses(info *types.Info, uses iter.Seq[inspector.Cursor], afterPos token.Pos) bool {
for curUse := range uses {
ek, _ := curUse.ParentEdge()
if ek == edge.AssignStmt_Lhs {
if curUse.Node().Pos() <= afterPos {
continue
}
assign := curUse.Parent().Node().(*ast.AssignStmt)
if sameObject(info, assign.Lhs[0], curUse.Node().(*ast.Ident)) {
return true
}
} else if ek == edge.UnaryExpr_X &&
curUse.Parent().Node().(*ast.UnaryExpr).Op == token.AND {
return true
}
}
return false
}
// sameObject reports whether we know that the expressions resolve to the same object
func sameObject(info *types.Info, expr1, expr2 ast.Expr) bool {
if ident1, ok := expr1.(*ast.Ident); ok {
if ident2, ok := expr2.(*ast.Ident); ok {
uses1, ok1 := info.Uses[ident1]
uses2, ok2 := info.Uses[ident2]
return ok1 && ok2 && uses1 == uses2
}
}
return false
}
StringsCutPrefix
StringsCutPrefix は strings.HasPrefix と strings.TrimPrefix を使用する一般的なパターンを簡略化します。Go 1.20 で導入された strings.CutPrefix の単一呼び出しに置き換えます。bytes パッケージの同等の関数も処理します。
例えば、以下の入力:
if strings.HasPrefix(s, prefix) {
use(strings.TrimPrefix(s, prefix))
}
は以下のように修正されます:
if after, ok := strings.CutPrefix(s, prefix); ok {
use(after)
}
同様に、CutSuffix を使用する修正も提供します。
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
import (
"fmt"
"go/ast"
"go/token"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysis/analyzerutil"
typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
)
var StringsCutPrefixAnalyzer = &analysis.Analyzer{
Name: "stringscutprefix",
Doc: analyzerutil.MustExtractDoc(doc, "stringscutprefix"),
Requires: []*analysis.Analyzer{
inspect.Analyzer,
typeindexanalyzer.Analyzer,
},
Run: stringscutprefix,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringscutprefix",
}
// stringscutprefix offers a fix to replace an if statement which
// calls to the 2 patterns below with strings.CutPrefix or strings.CutSuffix.
func stringscutprefix(pass *analysis.Pass) (any, error) {
var (
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo
stringsTrimPrefix = index.Object("strings", "TrimPrefix")
bytesTrimPrefix = index.Object("bytes", "TrimPrefix")
stringsTrimSuffix = index.Object("strings", "TrimSuffix")
bytesTrimSuffix = index.Object("bytes", "TrimSuffix")
)
if !index.Used(stringsTrimPrefix, bytesTrimPrefix, stringsTrimSuffix, bytesTrimSuffix) {
return nil, nil
}
for curFile := range filesUsingGoVersion(pass, versions.Go1_20) {
for curIfStmt := range curFile.Preorder((*ast.IfStmt)(nil)) {
ifStmt := curIfStmt.Node().(*ast.IfStmt)
// pattern1
if call, ok := ifStmt.Cond.(*ast.CallExpr); ok && ifStmt.Init == nil && len(ifStmt.Body.List) > 0 {
obj := typeutil.Callee(info, call)
if !typesinternal.IsFunctionNamed(obj, "strings", "HasPrefix", "HasSuffix") &&
!typesinternal.IsFunctionNamed(obj, "bytes", "HasPrefix", "HasSuffix") {
continue
}
isPrefix := strings.HasSuffix(obj.Name(), "Prefix")
firstStmt := curIfStmt.Child(ifStmt.Body).Child(ifStmt.Body.List[0])
for curCall := range firstStmt.Preorder((*ast.CallExpr)(nil)) {
call1 := curCall.Node().(*ast.CallExpr)
obj1 := typeutil.Callee(info, call1)
if obj1 == nil ||
obj1 != stringsTrimPrefix && obj1 != bytesTrimPrefix &&
obj1 != stringsTrimSuffix && obj1 != bytesTrimSuffix {
continue
}
isPrefix1 := strings.HasSuffix(obj1.Name(), "Prefix")
var cutFuncName, varName, message, fixMessage string
if isPrefix && isPrefix1 {
cutFuncName = "CutPrefix"
varName = "after"
message = "HasPrefix + TrimPrefix can be simplified to CutPrefix"
fixMessage = "Replace HasPrefix/TrimPrefix with CutPrefix"
} else if !isPrefix && !isPrefix1 {
cutFuncName = "CutSuffix"
varName = "before"
message = "HasSuffix + TrimSuffix can be simplified to CutSuffix"
fixMessage = "Replace HasSuffix/TrimSuffix with CutSuffix"
} else {
continue
}
var (
s0 = call.Args[0]
pre0 = call.Args[1]
s = call1.Args[0]
pre = call1.Args[1]
)
if astutil.EqualSyntax(s0, s) && astutil.EqualSyntax(pre0, pre) {
after := refactor.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), varName)
prefix, importEdits := refactor.AddImport(
info,
curFile.Node().(*ast.File),
obj1.Pkg().Name(),
obj1.Pkg().Path(),
cutFuncName,
call.Pos(),
)
okVarName := refactor.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), "ok")
pass.Report(analysis.Diagnostic{
Pos: call.Pos(),
End: call.End(),
Message: message,
SuggestedFixes: []analysis.SuggestedFix{{
Message: fixMessage,
TextEdits: append(importEdits, []analysis.TextEdit{
{
Pos: call.Fun.Pos(),
End: call.Fun.Pos(),
NewText: fmt.Appendf(nil, "%s, %s :=", after, okVarName),
},
{
Pos: call.Fun.Pos(),
End: call.Fun.End(),
NewText: fmt.Appendf(nil, "%s%s", prefix, cutFuncName),
},
{
Pos: call.End(),
End: call.End(),
NewText: fmt.Appendf(nil, "; %s ", okVarName),
},
{
Pos: call1.Pos(),
End: call1.End(),
NewText: []byte(after),
},
}...),
}},
})
break
}
}
}
}
}
return nil, nil
}
StringsSeq
StringsSeq は部分文字列の反復処理の効率を向上させます。以下のコード:
for range strings.Split(...)
を、より効率的な以下に置き換えます:
for range strings.SplitSeq(...)
これは Go 1.24 で追加され、部分文字列のスライスのアロケーションを避けます。strings.Fields や bytes パッケージの同等の関数も処理します。
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysis/analyzerutil"
typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
)
var StringsSeqAnalyzer = &analysis.Analyzer{
Name: "stringsseq",
Doc: analyzerutil.MustExtractDoc(doc, "stringsseq"),
Requires: []*analysis.Analyzer{
inspect.Analyzer,
typeindexanalyzer.Analyzer,
},
Run: stringsseq,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringsseq",
}
// stringsseq offers a fix to replace a call to strings.Split with
// SplitSeq or strings.Fields with FieldsSeq when it is the operand of a range loop
func stringsseq(pass *analysis.Pass) (any, error) {
var (
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo
stringsSplit = index.Object("strings", "Split")
stringsFields = index.Object("strings", "Fields")
bytesSplit = index.Object("bytes", "Split")
bytesFields = index.Object("bytes", "Fields")
)
if !index.Used(stringsSplit, stringsFields, bytesSplit, bytesFields) {
return nil, nil
}
for curFile := range filesUsingGoVersion(pass, versions.Go1_24) {
for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) {
rng := curRange.Node().(*ast.RangeStmt)
// Reject "for i, line := ..." since SplitSeq is not an iter.Seq2.
if id, ok := rng.Key.(*ast.Ident); ok && id.Name != "_" {
continue
}
// Find the call operand of the range statement
call, ok := rng.X.(*ast.CallExpr)
if !ok {
if id, ok := rng.X.(*ast.Ident); ok {
if v, ok := info.Uses[id].(*types.Var); ok {
if ek, idx := curRange.ParentEdge(); ek == edge.BlockStmt_List && idx > 0 {
curPrev, _ := curRange.PrevSibling()
if assign, ok := curPrev.Node().(*ast.AssignStmt); ok &&
assign.Tok == token.DEFINE &&
len(assign.Lhs) == 1 &&
len(assign.Rhs) == 1 &&
info.Defs[assign.Lhs[0].(*ast.Ident)] == v &&
soleUseIs(index, v, id) {
call, _ = assign.Rhs[0].(*ast.CallExpr)
}
}
}
}
}
if call != nil {
var edits []analysis.TextEdit
if rng.Key != nil {
end := rng.Range
if rng.Value != nil {
end = rng.Value.Pos()
}
edits = append(edits, analysis.TextEdit{
Pos: rng.Key.Pos(),
End: end,
})
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
continue
}
switch obj := typeutil.Callee(info, call); obj {
case stringsSplit, stringsFields, bytesSplit, bytesFields:
oldFnName := obj.Name()
seqFnName := fmt.Sprintf("%sSeq", oldFnName)
pass.Report(analysis.Diagnostic{
Pos: sel.Pos(),
End: sel.End(),
Message: fmt.Sprintf("Ranging over %s is more efficient", seqFnName),
SuggestedFixes: []analysis.SuggestedFix{{
Message: fmt.Sprintf("Replace %s with %s", oldFnName, seqFnName),
TextEdits: append(edits, analysis.TextEdit{
Pos: sel.Sel.Pos(),
End: sel.Sel.End(),
NewText: []byte(seqFnName),
}),
}},
})
}
}
}
}
return nil, nil
}
StringsBuilder
StringsBuilder は繰り返される文字列の += 連結操作を Go 1.10 の strings.Builder の呼び出しに置き換えます。
使用例
Before:
var s = "["
for x := range seq {
s += x
s += "."
}
s += "]"
use(s)
After:
var s strings.Builder
s.WriteString("[")
for x := range seq {
s.WriteString(x)
s.WriteString(".")
}
s.WriteString("]")
use(s.String())
メリット: 二次的なメモリアロケーションを避け、パフォーマンスを大幅に向上させます(特にループ内での文字列連結)。
制限: アナライザーは、s へのすべての参照が最後の1つを除いて += 操作であることを要求します。自明なケースについて警告を避けるため、少なくとも1つはループ内に現れる必要があります。変数 s はローカル変数でなければならず、グローバル変数やパラメータであってはなりません。
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
import (
"cmp"
"fmt"
"go/ast"
"go/constant"
"go/token"
"go/types"
"maps"
"slices"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/analysis/analyzerutil"
typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/typesinternal/typeindex"
)
var StringsBuilderAnalyzer = &analysis.Analyzer{
Name: "stringsbuilder",
Doc: analyzerutil.MustExtractDoc(doc, "stringsbuilder"),
Requires: []*analysis.Analyzer{
inspect.Analyzer,
typeindexanalyzer.Analyzer,
},
Run: stringsbuilder,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringbuilder",
}
// stringsbuilder replaces string += string in a loop by strings.Builder.
func stringsbuilder(pass *analysis.Pass) (any, error) {
if within(pass, "strings", "runtime") {
return nil, nil
}
var (
inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
)
// Gather all local string variables that appear on the
// LHS of some string += string assignment.
candidates := make(map[*types.Var]bool)
for curAssign := range inspect.Root().Preorder((*ast.AssignStmt)(nil)) {
assign := curAssign.Node().(*ast.AssignStmt)
if assign.Tok == token.ADD_ASSIGN && is[*ast.Ident](assign.Lhs[0]) {
if v, ok := pass.TypesInfo.Uses[assign.Lhs[0].(*ast.Ident)].(*types.Var); ok &&
!typesinternal.IsPackageLevel(v) &&
types.Identical(v.Type(), builtinString.Type()) {
candidates[v] = true
}
}
}
lexicalOrder := func(x, y *types.Var) int { return cmp.Compare(x.Pos(), y.Pos()) }
var (
lastEditFile *ast.File
lastEditEnd token.Pos
)
nextcand:
for _, v := range slices.SortedFunc(maps.Keys(candidates), lexicalOrder) {
var edits []analysis.TextEdit
def, ok := index.Def(v)
if !ok {
continue
}
file := astutil.EnclosingFile(def)
if file == lastEditFile && v.Pos() < lastEditEnd {
continue
}
ek, _ := def.ParentEdge()
if ek == edge.AssignStmt_Lhs &&
len(def.Parent().Node().(*ast.AssignStmt).Lhs) == 1 {
assign := def.Parent().Node().(*ast.AssignStmt)
switch def.Parent().Parent().Node().(type) {
case *ast.BlockStmt, *ast.CaseClause, *ast.CommClause:
default:
continue
}
prefix, importEdits := refactor.AddImport(
pass.TypesInfo, astutil.EnclosingFile(def), "strings", "strings", "Builder", v.Pos())
edits = append(edits, importEdits...)
if isEmptyString(pass.TypesInfo, assign.Rhs[0]) {
edits = append(edits, analysis.TextEdit{
Pos: assign.Pos(),
End: assign.End(),
NewText: fmt.Appendf(nil, "var %[1]s %[2]sBuilder", v.Name(), prefix),
})
} else {
edits = append(edits, []analysis.TextEdit{
{
Pos: assign.Pos(),
End: assign.Rhs[0].Pos(),
NewText: fmt.Appendf(nil, "var %[1]s %[2]sBuilder; %[1]s.WriteString(", v.Name(), prefix),
},
{
Pos: assign.End(),
End: assign.End(),
NewText: []byte(")"),
},
}...)
}
}
var (
numLoopAssigns int
loopAssign *ast.AssignStmt
seenRvalueUse bool
)
for curUse := range index.Uses(v) {
ek, _ := curUse.ParentEdge()
for ek == edge.ParenExpr_X {
curUse = curUse.Parent()
ek, _ = curUse.ParentEdge()
}
if seenRvalueUse {
continue nextcand
}
intervening := func(types ...ast.Node) bool {
for cur := range curUse.Enclosing(types...) {
if v.Pos() <= cur.Node().Pos() {
return true
}
}
return false
}
if ek == edge.AssignStmt_Lhs {
assign := curUse.Parent().Node().(*ast.AssignStmt)
if assign.Tok != token.ADD_ASSIGN {
continue nextcand
}
if intervening((*ast.ForStmt)(nil), (*ast.RangeStmt)(nil)) {
numLoopAssigns++
if loopAssign == nil {
loopAssign = assign
}
}
edits = append(edits, []analysis.TextEdit{
{
Pos: assign.TokPos,
End: assign.Rhs[0].Pos(),
NewText: []byte(".WriteString("),
},
{
Pos: assign.End(),
End: assign.End(),
NewText: []byte(")"),
},
}...)
} else if ek == edge.UnaryExpr_X &&
curUse.Parent().Node().(*ast.UnaryExpr).Op == token.AND {
continue nextcand
} else {
seenRvalueUse = true
edits = append(edits, analysis.TextEdit{
Pos: curUse.Node().End(),
End: curUse.Node().End(),
NewText: []byte(".String()"),
})
}
}
if !seenRvalueUse {
continue nextcand
}
if numLoopAssigns == 0 {
continue nextcand
}
lastEditFile = file
lastEditEnd = edits[len(edits)-1].End
pass.Report(analysis.Diagnostic{
Pos: loopAssign.Pos(),
End: loopAssign.End(),
Message: "using string += string in a loop is inefficient",
SuggestedFixes: []analysis.SuggestedFix{{
Message: "Replace string += string with strings.Builder",
TextEdits: edits,
}},
})
}
return nil, nil
}
// isEmptyString reports whether e (a string-typed expression) has constant value "".
func isEmptyString(info *types.Info, e ast.Expr) bool {
tv, ok := info.Types[e]
return ok && tv.Value != nil && constant.StringVal(tv.Value) == ""
}
TestingContext
TestingContext はテストでのコンテキスト管理を簡略化します。
使用例
Before:
func TestSomething(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// テストコード
doSomething(ctx)
}
After:
func TestSomething(t *testing.T) {
ctx := t.Context()
// テストコード
doSomething(ctx)
}
メリット: コードが簡潔になり、テストの終了時に自動的にコンテキストがキャンセルされます。
注意: この変更は、cancel 関数が他の目的で使用されていない場合にのみ提案されます。
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"strings"
"unicode"
"unicode/utf8"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysis/analyzerutil"
typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
)
var TestingContextAnalyzer = &analysis.Analyzer{
Name: "testingcontext",
Doc: analyzerutil.MustExtractDoc(doc, "testingcontext"),
Requires: []*analysis.Analyzer{
inspect.Analyzer,
typeindexanalyzer.Analyzer,
},
Run: testingContext,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#testingcontext",
}
// The testingContext pass replaces calls to context.WithCancel from within
// tests to a use of testing.{T,B,F}.Context(), added in Go 1.24.
func testingContext(pass *analysis.Pass) (any, error) {
var (
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo
contextWithCancel = index.Object("context", "WithCancel")
)
calls:
for cur := range index.Calls(contextWithCancel) {
call := cur.Node().(*ast.CallExpr)
arg, ok := call.Args[0].(*ast.CallExpr)
if !ok {
continue
}
if !typesinternal.IsFunctionNamed(typeutil.Callee(info, arg), "context", "Background", "TODO") {
continue
}
parent := cur.Parent()
assign, ok := parent.Node().(*ast.AssignStmt)
if !ok || assign.Tok != token.DEFINE {
continue
}
var lhs []types.Object
for _, expr := range assign.Lhs {
id, ok := expr.(*ast.Ident)
if !ok {
continue calls
}
obj, ok := info.Defs[id]
if !ok {
continue calls
}
lhs = append(lhs, obj)
}
next, ok := parent.NextSibling()
if !ok {
continue
}
defr, ok := next.Node().(*ast.DeferStmt)
if !ok {
continue
}
deferId, ok := defr.Call.Fun.(*ast.Ident)
if !ok || !soleUseIs(index, lhs[1], deferId) {
continue
}
var testObj types.Object
if curFunc, ok := enclosingFunc(cur); ok {
switch n := curFunc.Node().(type) {
case *ast.FuncLit:
if ek, idx := curFunc.ParentEdge(); ek == edge.CallExpr_Args && idx == 1 {
obj := typeutil.Callee(info, curFunc.Parent().Node().(*ast.CallExpr))
if (typesinternal.IsMethodNamed(obj, "testing", "T", "Run") ||
typesinternal.IsMethodNamed(obj, "testing", "B", "Run")) &&
len(n.Type.Params.List[0].Names) == 1 {
testObj = info.Defs[n.Type.Params.List[0].Names[0]]
}
}
case *ast.FuncDecl:
testObj = isTestFn(info, n)
}
}
if testObj != nil && analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(cur), versions.Go1_24) {
if _, obj := lhs[0].Parent().LookupParent(testObj.Name(), lhs[0].Pos()); obj == testObj {
pass.Report(analysis.Diagnostic{
Pos: call.Fun.Pos(),
End: call.Fun.End(),
Message: fmt.Sprintf("context.WithCancel can be modernized using %s.Context", testObj.Name()),
SuggestedFixes: []analysis.SuggestedFix{{
Message: fmt.Sprintf("Replace context.WithCancel with %s.Context", testObj.Name()),
TextEdits: []analysis.TextEdit{{
Pos: assign.Pos(),
End: defr.End(),
NewText: fmt.Appendf(nil, "%s := %s.Context()", lhs[0].Name(), testObj.Name()),
}},
}},
})
}
}
}
return nil, nil
}
// soleUseIs reports whether id is the sole Ident that uses obj.
func soleUseIs(index *typeindex.Index, obj types.Object, id *ast.Ident) bool {
empty := true
for use := range index.Uses(obj) {
empty = false
if use.Node() != id {
return false
}
}
return !empty
}
// isTestFn checks whether fn is a test function (TestX, BenchmarkX, FuzzX)
func isTestFn(info *types.Info, fn *ast.FuncDecl) types.Object {
if fn.Type.Results != nil && len(fn.Type.Results.List) > 0 ||
fn.Type.Params == nil ||
len(fn.Type.Params.List) != 1 ||
len(fn.Type.Params.List[0].Names) != 1 {
return nil
}
prefix := testKind(fn.Name.Name)
if prefix == "" {
return nil
}
if tparams := fn.Type.TypeParams; tparams != nil && len(tparams.List) > 0 {
return nil
}
obj := info.Defs[fn.Type.Params.List[0].Names[0]]
if obj == nil {
return nil
}
var name string
switch prefix {
case "Test":
name = "T"
case "Benchmark":
name = "B"
case "Fuzz":
name = "F"
}
if !typesinternal.IsPointerToNamed(obj.Type(), "testing", name) {
return nil
}
return obj
}
// testKind returns "Test", "Benchmark", or "Fuzz" if name is a valid test function name
func testKind(name string) string {
var prefix string
switch {
case strings.HasPrefix(name, "Test"):
prefix = "Test"
case strings.HasPrefix(name, "Benchmark"):
prefix = "Benchmark"
case strings.HasPrefix(name, "Fuzz"):
prefix = "Fuzz"
}
if prefix == "" {
return ""
}
suffix := name[len(prefix):]
if len(suffix) == 0 {
return prefix
}
r, _ := utf8.DecodeRuneInString(suffix)
if unicode.IsLower(r) {
return ""
}
return prefix
}
WaitGroup
WaitGroup は sync.WaitGroup を使用したゴルーチン管理を簡略化します。
使用例
Before:
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
process(id)
}(i)
}
wg.Wait()
After:
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Go(func() {
process(i)
})
}
wg.Wait()
メリット: コードが簡潔になり、Add(1) と Done() の呼び出しを忘れるリスクがなくなります。
実装コード (waitgroup.go)
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modernize
import (
"bytes"
"fmt"
"go/ast"
"go/printer"
"slices"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysis/analyzerutil"
typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
)
var WaitGroupAnalyzer = &analysis.Analyzer{
Name: "waitgroup",
Doc: analyzerutil.MustExtractDoc(doc, "waitgroup"),
Requires: []*analysis.Analyzer{
inspect.Analyzer,
typeindexanalyzer.Analyzer,
},
Run: waitgroup,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#waitgroup",
}
// The waitgroup pass replaces old more complex code with
// go1.25 added API WaitGroup.Go.
//
// Patterns:
//
// 1. wg.Add(1); go func() { defer wg.Done(); ... }()
// =>
// wg.Go(go func() { ... })
//
// 2. wg.Add(1); go func() { ...; wg.Done() }()
// =>
// wg.Go(go func() { ... })
func waitgroup(pass *analysis.Pass) (any, error) {
var (
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo
syncWaitGroupAdd = index.Selection("sync", "WaitGroup", "Add")
syncWaitGroupDone = index.Selection("sync", "WaitGroup", "Done")
)
if !index.Used(syncWaitGroupDone) {
return nil, nil
}
for curAddCall := range index.Calls(syncWaitGroupAdd) {
// Extract receiver from wg.Add call.
addCall := curAddCall.Node().(*ast.CallExpr)
if !isIntLiteral(info, addCall.Args[0], 1) {
continue // not a call to wg.Add(1)
}
addCallRecv := ast.Unparen(addCall.Fun).(*ast.SelectorExpr).X
// Following statement must be go func() { ... } ().
curAddStmt := curAddCall.Parent()
if !is[*ast.ExprStmt](curAddStmt.Node()) {
continue
}
curNext, ok := curAddCall.Parent().NextSibling()
if !ok {
continue
}
goStmt, ok := curNext.Node().(*ast.GoStmt)
if !ok {
continue
}
lit, ok := goStmt.Call.Fun.(*ast.FuncLit)
if !ok || len(goStmt.Call.Args) != 0 {
continue
}
list := lit.Body.List
if len(list) == 0 {
continue
}
// Body must start with "defer wg.Done()" or end with "wg.Done()".
var doneStmt ast.Stmt
if deferStmt, ok := list[0].(*ast.DeferStmt); ok &&
typeutil.Callee(info, deferStmt.Call) == syncWaitGroupDone &&
astutil.EqualSyntax(ast.Unparen(deferStmt.Call.Fun).(*ast.SelectorExpr).X, addCallRecv) {
doneStmt = deferStmt // "defer wg.Done()"
} else if lastStmt, ok := list[len(list)-1].(*ast.ExprStmt); ok {
if doneCall, ok := lastStmt.X.(*ast.CallExpr); ok &&
typeutil.Callee(info, doneCall) == syncWaitGroupDone &&
astutil.EqualSyntax(ast.Unparen(doneCall.Fun).(*ast.SelectorExpr).X, addCallRecv) {
doneStmt = lastStmt // "wg.Done()"
}
}
if doneStmt == nil {
continue
}
curDoneStmt, ok := curNext.FindNode(doneStmt)
if !ok {
panic("can't find Cursor for 'done' statement")
}
file := astutil.EnclosingFile(curAddCall)
if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_25) {
continue
}
tokFile := pass.Fset.File(file.Pos())
var addCallRecvText bytes.Buffer
err := printer.Fprint(&addCallRecvText, pass.Fset, addCallRecv)
if err != nil {
continue
}
pass.Report(analysis.Diagnostic{
Pos: goStmt.Pos(),
End: lit.Type.End(),
Message: "Goroutine creation can be simplified using WaitGroup.Go",
SuggestedFixes: []analysis.SuggestedFix{{
Message: "Simplify by using WaitGroup.Go",
TextEdits: slices.Concat(
// delete "wg.Add(1)"
refactor.DeleteStmt(tokFile, curAddStmt),
// delete "wg.Done()" or "defer wg.Done()"
refactor.DeleteStmt(tokFile, curDoneStmt),
[]analysis.TextEdit{
// go func()
// ------
// wg.Go(func()
{
Pos: goStmt.Pos(),
End: goStmt.Call.Pos(),
NewText: fmt.Appendf(nil, "%s.Go(", addCallRecvText.String()),
},
// ... }()
// -
// ... } )
{
Pos: goStmt.Call.Lparen,
End: goStmt.Call.Rparen,
},
},
),
}},
})
}
return nil, nil
}
無効化されているアナライザー
以下の3つのアナライザーは現在無効化されていますが、将来的に有効になる可能性があります。
AppendClippedAnalyzer
AppendClippedAnalyzer は slices.Concat を使用して append チェーンを簡略化することを提案します。Go 1.21 で追加されました。
使用例
パターン1: append チェーンの簡略化
Before:
result := append(append(slice1, slice2...), slice3...)
combined := append(append([]int{1, 2}, []int{3, 4}...), []int{5, 6}...)
After:
result := slices.Concat(slice1, slice2, slice3)
combined := slices.Concat([]int{1, 2}, []int{3, 4}, []int{5, 6})
パターン2: スライスのクローン
Before:
// 新しく割り当てられたスライスに追加
cloned := append([]int(nil), original...)
copied := append([]byte(nil), data...)
After:
// より簡潔なクローン操作
cloned := slices.Clone(original)
copied := bytes.Clone(data) // bytes パッケージが既にインポートされている場合
適用条件
この修正は、append チェーンのベースが「クリップされた」スライス(clipped slice)である場合にのみ適用されます。クリップされたスライスとは、長さと容量が等しいスライスのことです。例:
x[:0:0]- 長さ0、容量0のスライス[]T{}- 空のスライスリテラルmake([]T, 0)- 長さ0で作成されたスライス
この制限は、ベーススライスの基盤となる配列に対する意図された副作用を排除することでプログラムの動作を変更することを避けるためです。
無効化されている理由
このアナライザーは現在デフォルトで無効化されています。理由は、変換がすべてのケースでベーススライスの nil 性(nilness)を保持しないためです。
具体的には:
- 元のコード:
append([]int(nil), s...)は、sが nil の場合、nil スライスを返す可能性がある - 変換後:
slices.Clone(s)は、sが nil の場合でも空のスライスを返す
この動作の違いにより、nil チェックに依存するコードが壊れる可能性があります。詳細は go.dev/issue/73557 を参照してください。
将来の見通し
この問題が解決されれば、将来的に有効化される可能性があります。現時点では、手動で適用する場合は、nil 性の扱いに注意が必要です。
BLoopAnalyzer
BLoopAnalyzer は for i := 0; i < b.N; i++ や for range b.N の形式のベンチマークループを、Go 1.24 で追加されたより現代的な for b.Loop() に置き換えることを提案します。
この変更により、ベンチマークコードがより読みやすくなり、手動のタイマー制御の必要性もなくなります。そのため、同じ関数内の先行する b.StartTimer、b.StopTimer、または b.ResetTimer の呼び出しも削除されます。
注意: b.Loop() メソッドは、コンパイラがベンチマークループを最適化して削除することを防ぐように設計されており、特定のケースではアロケーションの増加により実行が遅くなる場合があります。その修正がナノ秒スケールのベンチマークのパフォーマンスを変更する可能性があるため、bloop は go fix アナライザースイートでデフォルトで無効化されています(golang/go#74967 を参照)。
SlicesDeleteAnalyzer
SlicesDeleteAnalyzer は以下のイディオム:
s = append(s[:i], s[j:]...)
を、Go 1.21 で導入されたより明示的な以下に置き換えることを提案します:
s = slices.Delete(s, i, j)
このアナライザーはデフォルトで無効化されています。slices.Delete 関数は、メモリリークを防ぐために、スライスの新しい長さと古い長さの間の要素をゼロにします。これは append ベースのイディオムと比較して動作の微妙な違いです(https://go.dev/issue/73686 を参照)。
使用方法とベストプラクティス
基本的な使用方法
すべての修正を一括で適用するには、以下のコマンドを使用できます:
$ go run golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize@latest -fix ./...
段階的な適用
modernize スイートには多くのアナライザーが含まれています。特に「any」(interface{} を any に置き換える)などの診断は特に多数です。コードレビューの負担を軽減するために、2段階で修正を適用することができます:
# ステップ1: any アナライザーのみ適用
$ modernize -any=true -fix ./...
# ステップ2: その他のアナライザーを適用
$ modernize -any=false -fix ./...
注意事項
- レビュー必須: このツールによって生成された変更は、通常どおりマージ前にレビューする必要があります。
- コメントの消失: 場合によっては、ループが単純な関数呼び出しに置き換えられ、ループ内のコメントが破棄されることがあります。価値のあるコメントを失わないように、人間の判断が必要になります。
- 競合する修正: ツールが競合する修正について警告する場合、すべての修正がクリーンに適用されるまで、複数回実行する必要があるかもしれません。
- 動作変更の可能性: 一部のアナライザー(特に
OmitZero、SlicesDeleteなど)は、コードの動作を変更する可能性があります。変更を適用する前に、テストを実行して動作を確認してください。
CI/CD への統合例
# GitHub Actions の例
- name: Run modernize
run: |
go run golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize@latest ./...
# 修正を適用せず、問題を報告するだけの場合は -fix フラグを削除
まとめ
Go 1.26 の go fix コマンドは、Go の analysis フレームワークを使用して、コードをより現代的な言語機能や標準ライブラリの機能を活用するように自動的に修正する19個のアナライザーを提供します。これらのアナライザーは、コードの可読性、パフォーマンス、保守性を向上させることを目的としています。
各アナライザーは特定のGoバージョン以降で利用可能であり、コードベースを段階的にモダナイズするのに役立ちます。ただし、変更を適用する際は、必ずレビューとテストを実施し、コードの動作が意図通りであることを確認してください。