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()
}
Links & Credits
𝗣 71859 👥 Alan Donovan, Jonathan Amsterdam
★ Подписывайтесь на канал и проходите курсы.