Modernizing Go Code: Understanding the Revitalized `go fix` Command

go-development

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()
}