Go feature: Modernized go fix

Part of the Accepted! series: Go features explained in simple terms.

The modernized go fix command uses a fresh set of analyzers and the same infrastructure as go vet.

Ver. 1.26 • Tools • Medium impact

Summary

The go fix is re-implemented using the Go analysis framework — the same one go vet uses.

While go fix and go vet now use the same infrastructure, they have different purposes and use different sets of analyzers:

  • Vet is for reporting problems. Its analyzers describe actual issues, but they don't always suggest fixes, and the fixes aren't always safe to apply.
  • Fix is (mostly) for modernizing the code to use newer language and library features. Its analyzers produce fixes are always safe to apply, but don't necessarily indicate problems with the code.

See the full set of fix's analyzers in the Analyzers section.

Motivation

The main goal is to bring modernization tools from the Go language server (gopls) to the command line. If go fix includes the modernize suite, developers can easily and safely update their entire codebase after a new Go release with just one command.

Re-implementing go fix also makes the Go toolchain simpler. The unified go fix and go vet use the same backend framework and extension mechanism. This makes the tools more consistent, easier to maintain, and more flexible for developers who want to use custom analysis tools.

Description

Implement the new go fix command:

usage: go fix [build flags] [-fixtool prog] [fix flags] [packages]

Fix runs the Go fix tool (cmd/fix) on the named packages
and applies suggested fixes.

It supports these flags:

  -diff
        instead of applying each fix, print the patch as a unified diff

The -fixtool=prog flag selects a different analysis tool with
alternative or additional fixers; see the documentation for go vet's
-vettool flag for details.

By default, go fix runs a full set of analyzers (see the list below). To choose specific analyzers, use the -NAME flag for each one, or use -NAME=false to run all analyzers except the ones you turned off.

For example, here we only enable the forvar analyzer:

go fix -forvar .

And here, we enable all analyzers except omitzero :

go fix -omitzero=false .

Currently, there's no way to suppress specific analyzers for certain files or sections of code.

Analyzers

Here's the list of fixes currently available in go fix, along with examples.

any • bloop • fmtappendf • forvar • hostport • inline • mapsloop • minmax • newexpr • omitzero • plusbuild • rangeint • reflecttypefor • slicescontains • slicessort • stditerators • stringsbuilder • stringscut • stringcutprefix • stringsseq • testingcontext • waitgroup

any

Replace 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

Replace for-range over b.N with b.Loop and remove unnecessary manual timer control:

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

Replace []byte(fmt.Sprintf) with fmt.Appendf to avoid 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

Remove 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

Replace network addresses created with fmt.Sprintf by using net.JoinHostPort instead, because host-port pairs made with Sprintf don't work 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

Inline function calls accoring to the 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

Replace explicit loops over maps with calls to 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

Replace if/else statements 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

Replace 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

Remove omitempty from struct-type fields because this tag doesn't have any 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

Remove 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

Replace 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

Replace reflect.TypeOf(x) with reflect.TypeFor[T]():

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

Replace loops 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

Replace sort.Slice with slices.Sort for 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

Use iterators instead of Len/At-style APIs for certain types in the standard library:

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

Replace repeated += with strings.Builder:

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

Replace 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

Replace strings.HasPrefix/TrimPrefix with strings.CutPrefix and strings.HasSuffix/TrimSuffix with string.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

Replace 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

Replace context.WithCancel with t.Context in tests:

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

Replace wg.Add+wg.Done with wg.Go:

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

𝗣 71859 👥 Alan Donovan, Jonathan Amsterdam

★ Subscribe to keep up with new posts.