Go-фича: Обновленный go fix

Часть серии Принято! В ней простыми словами объясняются новые фичи Go.

Обновлённая команда go fix использует новый набор анализаторов и ту же инфраструктуру, что и go vet.

Версия. 1.26 • Тулинг • Полезно

Что

Команда go fix теперь работает на основе analysis framework — это тот же движок, что использует go vet.

Хотя теперь go fix и go vet используют одну и ту же инфраструктуру, у них разные задачи и разные наборы анализаторов:

  • Vet нужен для поиска проблем в коде. Его анализаторы находят проблемы, но не всегда предлагают исправления, и не все исправления безопасно применять.
  • Fix в основном нужен для того, чтобы обновлять код под новые возможности языка и стандартной библиотеки. Его анализаторы вносят только безопасные исправления, но не обязательно указывают на проблемы в коде.

Полный список анализаторов go fix — в разделе Анализаторы.

Зачем

Главная цель — добавить инструменты модернизации кода из Go language server (gopls) в командную строку. Если в go fix появится набор фиксов modernize, разработчики смогут быстро и безопасно обновлять одной командой весь код после выхода новой версии Go.

Переосмысление go fix также упрощает саму экосистему Go. Теперь go fix и go vet используют одну и ту же базу и расширения. Это делает инструменты более понятными, удобными для поддержки и гибкими для тех, кто хочет подключать собственные анализаторы.

Как

Новая команда go fix:

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.

По умолчанию go fix запускает полный набор анализаторов (см. список ниже). Чтобы выбрать конкретные анализаторы, используйте флаг -NAME для каждого из них, или -NAME=false, чтобы запустить все анализаторы, кроме тех, которые вы отключили.

Например, здесь включен только анализатор forvar:

go fix -forvar .

А здесь включены все анализаторы кроме omitzero:

go fix -omitzero=false .

Сейчас нет способа отключить отдельные анализаторы для конкретных файлов или участков кода.

Флаг -fixtool=prog позволяет выбрать другой инструмент анализа вместо стандартного. Например, чтобы собрать и запустить инструмент "stringintconv", который исправляет преобразования string(int), используйте такие команды:

go install golang.org/x/tools/go/analysis/passes/stringintconv/cmd/stringintconv@latest
go fix -fixtool=$(which stringintconv)

Альтернативные инструменты должны строиться на основе unitchecker, который отвечает за взаимодействие с go fix.

Анализаторы

Вот список исправлений, которые сейчас доступны в go fix, с примерами.

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

any

Заменяет interface{} на any:

// before
func main() {
    var val interface{}
    val = 42
    fmt.Println(val)
}
// after
func main() {
    var val any
    val = 42
    fmt.Println(val)
}

bloop

Заменяет в бенчмарках цикл for-range по b.N на b.Loop, и удаляет ручное управление таймером:

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

Заменяет []byte(fmt.Sprintf) на fmt.Appendf, чтобы избежать промежуточного выделения памяти:

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

Убери лишнее переопределение переменных в цикле:

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

Заменяет создание сетевых адресов с помощью fmt.Sprintf на net.JoinHostPort, потому что пары "хост-порт", собранные через форматирование строк типа %s:%d или %s:%s, не работают с 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

Встраивает вызовы функций согласно директивам go:fix inline:

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

Заменяет явные циклы по картам на вызовы функций пакета maps (Copy, Insert, Clone или Collect в зависимости от ситуации):

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

Заменяет if/else на вызовы встроенных функций min или 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

Заменяет собственные функции вида "указатель на T" на вызов 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

Убирает omitempty у полей со структурным типом, потому что этот тег на них не влияет:

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

Удаляет устаревшие комментарии //+build:

//go:build linux && amd64
// +build linux,amd64

package main

func main() {
    var _ = 42
}
//go:build linux && amd64

package main

func main() {
    var _ = 42
}

rangeint

Заменяет 3-частные циклы for на for-range по числам:

// before
func main() {
    for i := 0; i < 5; i++ {
        fmt.Print(i)
    }
}
// after
func main() {
    for i := range 5 {
        fmt.Print(i)
    }
}

reflecttypefor

Заменяет reflect.TypeOf(x) на 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

Заменяет циклы на slices.Contains или 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

Заменяет sort.Slice на slices.Sort для простых типов:

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

Использует итераторы вместо API в стиле Len/At для некоторых типов стандартной библиотеки:

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

Заменяет повторные вызовы += на 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

Заменяет некоторые вызовы strings.Index и срезы строк на strings.Cut или 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

Заменяет strings.HasPrefix/TrimPrefix на strings.CutPrefix, а strings.HasSuffix/TrimSuffix на 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

Заменяет обход через strings.Split/Fields на 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

Заменяет context.WithCancel на t.Context в тестах:

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

Заменяет wg.Add+wg.Done на 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

★ Подписывайтесь на канал и проходите курсы.