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バージョン状態
Anyinterface{}any に置換Go 1.18+✅ 有効
FmtAppendf[]byte(fmt.Sprintf(...))fmt.Appendf(nil, ...) に置換Go 1.19+✅ 有効
ForVarループ変数の不要な再宣言を削除Go 1.22+✅ 有効
MapsLoopマップループを maps パッケージの関数に置換Go 1.23+✅ 有効
MinMaxif/else を min/max に置換Go 1.21+✅ 有効
NewExpr&x パターンを new(x) に置換Go 1.26+✅ 有効
OmitZeroomitemptyomitzero に置換Go 1.24+✅ 有効
plusBuild古い //+build コメントを削除Go 1.18+✅ 有効
RangeIntfor i := 0; i < n; i++for i := range n に置換Go 1.22+✅ 有効
ReflectTypeForreflect.TypeOf(x)reflect.TypeFor[T]() に置換Go 1.22+✅ 有効
SlicesContainsループを slices.Contains に置換Go 1.21+✅ 有効
SlicesSortsort.Sliceslices.Sort に置換Go 1.21+✅ 有効
stditeratorsLen/At スタイルをイテレータに置換-✅ 有効
stringscutstrings.Indexstrings.Cut に置換Go 1.18+✅ 有効
StringsCutPrefixHasPrefix+TrimPrefixCutPrefix に置換Go 1.20+✅ 有効
StringsSeqstrings.Splitstrings.SplitSeq に置換Go 1.24+✅ 有効
StringsBuilder+= 連結を strings.Builder に置換Go 1.10+✅ 有効
TestingContextcontext.WithCancelt.Context() に置換Go 1.24+✅ 有効
WaitGroupwg.Add(1); go func() { defer wg.Done() }()wg.Go() に置換Go 1.25+✅ 有効
AppendClippedAnalyzerappend チェーンを slices.Concat に置換Go 1.21+⚠️ 無効
BLoopAnalyzerベンチマークループを b.Loop() に置換Go 1.24+⚠️ 無効
SlicesDeleteAnalyzerappend ベース削除を 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 で導入された組み込みの minmax 関数の使用を提案します。

使用例

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 値に対する minmax の動作が元の 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 は構造体フィールドの omitemptyomitzero に置き換えることを提案します。このアナライザーは、構造体自体であるフィールドに omitempty JSON 構造体タグが使用されている場合を識別します。構造体型のフィールドでは、omitempty タグは json.Marshal と json.Unmarshal の動作に影響しません。アナライザーは2つの提案を提供します: タグを削除するか、Go 1.24 で追加された omitzero に置き換えるかです。omitzero は構造体値がゼロ値の場合にフィールドを正しく省略します。

ただし、他のシリアライゼーションパッケージ(特に kubebuilder)は json:",omitzero" タグに独自の解釈を持っている可能性があるため、このアナライザーは +kubebuilder アノテーションを含むパッケージでは変更を行いません。

omitemptyomitzero に置き換えることは動作の変更です。元のコードは常に構造体フィールドをエンコードしますが、修正されたコードはゼロ値の場合は省略します。

// 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 パッケージを使用するバリアントも処理します。

修正は、定義と使用の間に idxs、または 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.HasPrefixstrings.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.Fieldsbytes パッケージの同等の関数も処理します。

// 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.StartTimerb.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 ./...

注意事項

  1. レビュー必須: このツールによって生成された変更は、通常どおりマージ前にレビューする必要があります。
  2. コメントの消失: 場合によっては、ループが単純な関数呼び出しに置き換えられ、ループ内のコメントが破棄されることがあります。価値のあるコメントを失わないように、人間の判断が必要になります。
  3. 競合する修正: ツールが競合する修正について警告する場合、すべての修正がクリーンに適用されるまで、複数回実行する必要があるかもしれません。
  4. 動作変更の可能性: 一部のアナライザー(特に OmitZeroSlicesDelete など)は、コードの動作を変更する可能性があります。変更を適用する前に、テストを実行して動作を確認してください。

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バージョン以降で利用可能であり、コードベースを段階的にモダナイズするのに役立ちます。ただし、変更を適用する際は、必ずレビューとテストを実施し、コードの動作が意図通りであることを確認してください。

Posted on Jan 31, 2026