Modernizing Go Code: Understanding the Revitalized `go fix` Command
Explore the modernized `go fix` command, now powered by the Go analysis framework. Learn how it safely updates and modernizes your Go codebase using a dedicated set of powerful analyzers.
The go fix command has been modernized, now utilizing a fresh set of analyzers built upon the Go analysis framework—the same robust infrastructure employed by go vet.
While go fix and go vet now share a common foundation, their objectives and the analyzers they use differ significantly:
go vet: Primarily designed for identifying and reporting potential problems in code. Its analyzers highlight actual issues, though they don't always offer explicit fixes, and any suggested fixes aren't guaranteed to be safe for automatic application.go fix: Primarily focused on modernizing code to leverage newer Go language features and standard library improvements. Its analyzers generate fixes that are always safe to apply, even if the original code didn't necessarily indicate a "problem."
For a comprehensive list of available go fix analyzers, refer to the "Analyzers" section below.
Motivation
The primary driver behind this modernization is to bring advanced code modernization capabilities, previously found in the Go language server (gopls), directly to the command line. By integrating a "modernize" suite into go fix, developers can effortlessly and safely update their entire codebase to align with new Go releases using a single command.
This re-implementation also streamlines the Go toolchain. With go fix and go vet sharing the same backend framework and extension mechanism, these tools become more consistent, easier to maintain, and offer greater flexibility for developers wishing to incorporate custom analysis tools.
Usage
The updated go fix command is invoked as follows:
usage: go fix [build flags] [-fixtool prog] [fix flags] [packages]
go fix executes the Go fix tool (cmd/fix) on the specified packages, applying suggested code modernizations. It supports the following flags:
-diff: Instead of directly applying each fix, this flag prints the proposed changes as a unified diff.-fixtool=prog: This flag allows selecting an alternative analysis tool that provides different or additional fixers.
By default, go fix runs a complete set of analyzers. To enable only specific analyzers, use the -NAME flag (e.g., go fix -forvar .). Conversely, to run all analyzers except certain ones, use -NAME=false (e.g., go fix -omitzero=false .).
Currently, there is no direct mechanism to suppress specific analyzers for particular files or code sections.
To use an alternative fix tool with the -fixtool=prog flag, ensure it's built on top of unitchecker, which manages its interaction with go fix. For example, to use the "stringintconv" analyzer:
go install golang.org/x/tools/go/analysis/passes/stringintconv/cmd/stringintconv@latest
go fix -fixtool=$(which stringintconv)
Analyzers
Below is a list of the fixes currently available in go fix, accompanied by before-and-after code examples:
any
Replaces interface{} with any:
// before
func main() {
var val interface{}
val = 42
fmt.Println(val)
}
// after
func main() {
var val any
val = 42
fmt.Println(val)
}
bloop
Replaces for-range over b.N with b.Loop and removes unnecessary manual timer control in benchmarks:
// before
func Benchmark(b *testing.B) {
s := make([]int, 1000)
for i := range s {
s[i] = i
}
b.ResetTimer()
for range b.N {
Calc(s)
}
}
// after
func Benchmark(b *testing.B) {
s := make([]int, 1000)
for i := range s {
s[i] = i
}
for b.Loop() {
Calc(s)
}
}
fmtappendf
Replaces []byte(fmt.Sprintf) with fmt.Appendf to prevent intermediate string allocation:
// before
func format(id int, name string) []byte {
return []byte(fmt.Sprintf("ID: %d, Name: %s", id, name))
}
// after
func format(id int, name string) []byte {
return fmt.Appendf(nil, "ID: %d, Name: %s", id, name)
}
forvar
Removes unnecessary shadowing of loop variables:
// before
func main() {
for x := range 4 {
x := x
go func() {
fmt.Println(x)
}()
}
}
// after
func main() {
for x := range 4 {
go func() {
fmt.Println(x)
}()
}
}
hostport
Replaces network addresses constructed with fmt.Sprintf with net.JoinHostPort, as "%s:%d" or "%s:%s" format strings for host-port pairs can fail with IPv6:
// before
func main() {
host := "::1"
port := 8080
addr := fmt.Sprintf("%s:%d", host, port)
net.Dial("tcp", addr)
}
// after
func main() {
host := "::1"
port := 8080
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
net.Dial("tcp", addr)
}
inline
Inlines function calls based on go:fix inline comment directives:
// before
//go:fix inline
func Square(x float64) float64 {
return math.Pow(float64(x), 2)
}
func main() {
fmt.Println(Square(5))
}
// after
//go:fix inline
func Square(x float64) float64 {
return math.Pow(float64(x), 2)
}
func main() {
fmt.Println(math.Pow(float64(5), 2))
}
mapsloop
Replaces explicit loops over maps with calls to functions from the maps package (Copy, Insert, Clone, or Collect), depending on the context:
// before
func copyMap(src map[string]int) map[string]int {
dest := make(map[string]int, len(src))
for k, v := range src {
dest[k] = v
}
return dest
}
// after
func copyMap(src map[string]int) map[string]int {
dest := make(map[string]int, len(src))
maps.Copy(dest, src)
return dest
}
minmax
Replaces if/else statements used for finding minimum or maximum values with calls to min or max:
// before
func calc(a, b int) int {
var m int
if a > b {
m = a
} else {
m = b
}
return m * (b - a)
}
// after
func calc(a, b int) int {
var m int
m = max(a, b)
return m * (b - a)
}
newexpr
Replaces custom "pointer to" functions with new(expr):
// before
type Pet struct {
Name string
Happy *bool
}
func ptrOf[T any](v T) *T {
return &v
}
func main() {
p := Pet{Name: "Fluffy", Happy: ptrOf(true)}
fmt.Println(p)
}
// after
type Pet struct {
Name string
Happy *bool
}
//go:fix inline
func ptrOf[T any](v T) *T {
return new(v)
}
func main() {
p := Pet{Name: "Fluffy", Happy: new(true)}
fmt.Println(p)
}
omitzero
Removes the omitempty tag from struct-type fields, as it has no effect on them:
// before
type Person struct {
Name string `json:"name"`
Pet Pet `json:"pet,omitempty"`
}
type Pet struct {
Name string
}
// after
type Person struct {
Name string `json:"name"`
Pet Pet `json:"pet"`
}
type Pet struct {
Name string
}
plusbuild
Removes obsolete // +build comments:
//go:build linux && amd64
// +build linux,amd64
package main
func main() {
var _ = 42
}
//go:build linux && amd64
package main
func main() {
var _ = 42
}
rangeint
Replaces traditional 3-clause for loops with for-range over integers:
// before
func main() {
for i := 0; i < 5; i++ {
fmt.Print(i)
}
}
// after
func main() {
for i := range 5 {
fmt.Print(i)
}
}
reflecttypefor
Replaces reflect.TypeOf(x) with reflect.TypeFor[T]() when the type is known at compile time:
// before
func main() {
n := uint64(0)
typ := reflect.TypeOf(n)
fmt.Println("size =", typ.Bits())
}
// after
func main() {
typ := reflect.TypeFor[uint64]()
fmt.Println("size =", typ.Bits())
}
slicescontains
Replaces explicit loops for checking slice containment with slices.Contains or slices.ContainsFunc:
// before
func find(s []int, x int) bool {
for _, v := range s {
if x == v {
return true
}
}
return false
}
// after
func find(s []int, x int) bool {
return slices.Contains(s, x)
}
slicessort
Replaces sort.Slice with slices.Sort for sorting basic types:
// before
func main() {
s := []int{22, 11, 33, 55, 44}
sort.Slice(s, func(i, j int) bool {
return s[i] < s[j]
})
fmt.Println(s)
}
// after
func main() {
s := []int{22, 11, 33, 55, 44}
slices.Sort(s)
fmt.Println(s)
}
stditerators
Uses iterators instead of Len/At-style APIs for certain standard library types:
// before
func main() {
typ := reflect.TypeFor[Person]()
for i := range typ.NumField() {
field := typ.Field(i)
fmt.Println(field.Name, field.Type.String())
}
}
// after
func main() {
typ := reflect.TypeFor[Person]()
for field := range typ.Fields() {
fmt.Println(field.Name, field.Type.String())
}
}
stringsbuilder
Replaces repeated string concatenations (+=) with strings.Builder for efficiency:
// before
func abbr(s []string) string {
res := ""
for _, str := range s {
if len(str) > 0 {
res += string(str[0])
}
}
return res
}
// after
func abbr(s []string) string {
var res strings.Builder
for _, str := range s {
if len(str) > 0 {
res.WriteString(string(str[0]))
}
}
return res.String()
}
stringscut
Replaces some uses of strings.Index and string slicing with strings.Cut or strings.Contains:
// before
func nospace(s string) string {
idx := strings.Index(s, " ")
if idx == -1 {
return s
}
return strings.ReplaceAll(s, " ", "")
}
// after
func nospace(s string) string {
found := strings.Contains(s, " ")
if !found {
return s
}
return strings.ReplaceAll(s, " ", "")
}
stringscutprefix
Replaces strings.HasPrefix/TrimPrefix with strings.CutPrefix, and strings.HasSuffix/TrimSuffix with strings.CutSuffix:
// before
func unindent(s string) string {
if strings.HasPrefix(s, "> ") {
return strings.TrimPrefix(s, "> ")
}
return s
}
// after
func unindent(s string) string {
if after, ok := strings.CutPrefix(s, "> "); ok {
return after
}
return s
}
stringsseq
Replaces ranging over strings.Split/Fields with strings.SplitSeq/FieldsSeq:
// before
func main() {
s := "go is awesome"
for _, word := range strings.Fields(s) {
fmt.Println(len(word))
}
}
// after
func main() {
s := "go is awesome"
for word := range strings.FieldsSeq(s) {
fmt.Println(len(word))
}
}
testingcontext
Replaces context.WithCancel with t.Context in test functions:
// before
func Test(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
if ctx.Err() != nil {
t.Fatal("context should be active")
}
}
// after
func Test(t *testing.T) {
ctx := t.Context()
if ctx.Err() != nil {
t.Fatal("context should be active")
}
}
waitgroup
Replaces wg.Add and wg.Done calls with wg.Go for simpler concurrency management:
// before
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("go!")
}()
wg.Wait()
}
// after
func main() {
var wg sync.WaitGroup
wg.Go(func() {
fmt.Println("go!")
})
wg.Wait()
}