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()
}
Links & Credits
𝗣 71859 👥 Alan Donovan, Jonathan Amsterdam
★ Subscribe to keep up with new posts.