Voltar ao blog

Todos os modernizadores do `go fix` no Go 1.26

O Go 1.26 reescreveu o go fix sobre o framework de analisadores. Aqui tens todos os modernizadores que traz: 26 diffs de antes e depois, da linguagem à biblioteca padrão.

Todos os modernizadores do `go fix` no Go 1.26

O Go 1.26 reescreveu o go fix sobre o mesmo framework do go vet. O novo comando reúne todos os modernizadores que a equipa do Go e os autores do x/tools criaram. Cada um é um pequeno analisador que reconhece um padrão antigo e o reescreve em Go atual.

Um só comando executa-os todos:

example.bashbash
go fix ./...

Corre-o numa branch limpa, revê o diff, corre os testes e faz commit. Se quiseres pré-visualizar as alterações sem escrever ficheiros, usa go fix -diff ./....

Abaixo estão todos os modernizadores incluídos no 1.26, agrupados por área. Cada entrada indica a versão do Go que introduziu a API para a qual o código é reescrito, além de um par antes/depois. Foi pensado tanto para ler na diagonal ou pesquisar com ctrl-F como para ler do início ao fim.

Índice


Linguagem e funções integradas

Há aqui sete modernizadores. A maioria cobre funcionalidades da linguagem (any, a nova forma range n, a alteração da variável de ciclo no 1.22, a extensão new(expr)) e ainda algumas limpezas que atravessam várias versões.

any (Go 1.18): substitui interface{} por any

any é apenas um alias, mas lê-se melhor em assinaturas e parâmetros de tipo.

Antes:

example.gogo
func log(args ...interface{}) {
    fmt.Println(args...)
}

var cache map[string]interface{}

Depois:

example.gogo
func log(args ...any) {
    fmt.Println(args...)
}

var cache map[string]any

minmax (Go 1.21): substitui os if/else que limitam valores por min e max

As funções integradas min e max são variádicas e funcionam com qualquer tipo ordenado.

Antes:

example.gogo
x := f()
if x < 0 {
    x = 0
}
if x > 100 {
    x = 100
}

Depois:

example.gogo
x := max(0, min(100, f()))

rangeint (Go 1.22): ciclos for com inteiros passam a for i := range n

Um ciclo de contagem simples, sem nenhum slice ou array por trás, pode fazer range diretamente sobre o inteiro.

Antes:

example.gogo
for i := 0; i < n; i++ {
    fmt.Println(i)
}

Depois:

example.gogo
for i := range n {
    fmt.Println(i)
}

forvar (Go 1.22): elimina o truque de captura x := x

Antes do 1.22, a variável do ciclo era partilhada entre iterações, por isso as closures e as goroutines que a capturavam precisavam de x := x dentro do corpo do ciclo. O 1.22 passou a dar a cada iteração a sua própria variável, o que torna essa redeclaração redundante.

Antes:

example.gogo
for _, x := range items {
    x := x // pre-1.22 capture trick
    go func() { use(x) }()
}

Depois:

example.gogo
for _, x := range items {
    go func() { use(x) }()
}

O analisador só atua quando o go.mod declara go 1.22 ou posterior, já que os módulos mais antigos mantêm a semântica da variável partilhada.

newexpr (Go 1.26): ponteiro para um valor com new(expr)

O 1.26 alarga a função integrada new para aceitar uma expressão, e não apenas um tipo. new(s) devolve um *string que aponta para uma cópia de s. Isto torna desnecessárias as habituais funções auxiliares strPtr/intPtr/boolPtr.

Antes:

example.gogo
func strPtr(s string) *string {
    p := s
    return &p
}

cfg.Name = strPtr("LevelUpGo")

Depois:

example.gogo
cfg.Name = new("LevelUpGo")

Se tens funções auxiliares que ainda não podes apagar (outros chamadores, API exportada), vê o inline mais abaixo para conheceres a diretiva que te permite migrar de forma gradual.

inline (diretiva): expande funções e constantes marcadas com //go:fix inline

Adiciona a diretiva a um wrapper e o go fix reescreve todas as chamadas para usarem a expressão interna. Acho-a útil para renomeações e para retirar wrappers finos. Também a podes usar para aplicar os teus próprios modernizadores numa base de código privada.

Antes:

example.gogo
//go:fix inline
func Square(x int) int { return Pow(x, 2) }

a := Square(5)
b := Square(10)

Depois:

example.gogo
//go:fix inline
func Square(x int) int { return Pow(x, 2) }

a := Pow(5, 2)
b := Pow(10, 2)

A mesma diretiva funciona com constantes, o que a torna útil para migrar entre valores de enum renomeados.

plusbuild (Go 1.17): substitui // +build por //go:build

A sintaxe antiga das build tags está obsoleta desde o 1.17. A nova diretiva usa expressões booleanas normais de Go.

Antes:

example.gogo
// +build linux,amd64

package sys

Depois:

example.gogo
//go:build linux && amd64

package sys

Slices, maps e iteração

Cinco dos sete modernizadores deste grupo passam ciclos escritos à mão para os pacotes slices e maps. Os outros dois tratam de reflect.TypeFor e das tags JSON omitzero.

slicessort (Go 1.21): sort.Slice passa a slices.Sort

No caso comum de ordenar por ordem crescente um slice de um tipo ordenado, slices.Sort é genérico e dispensa por completo a closure de comparação.

Antes:

example.gogo
sort.Slice(s, func(i, j int) bool {
    return s[i] < s[j]
})

Depois:

example.gogo
slices.Sort(s)

slicescontains (Go 1.21): ciclos de pesquisa manuais passam a slices.Contains

Antes:

example.gogo
func has(s []int, x int) bool {
    for _, v := range s {
        if v == x {
            return true
        }
    }
    return false
}

Depois:

example.gogo
func has(s []int, x int) bool {
    return slices.Contains(s, x)
}

slicesbackward (Go 1.23): iteração inversa com slices.Backward

slices.Backward devolve um iterador que produz pares índice/valor do fim do slice até ao início.

Antes:

example.gogo
for i := len(s) - 1; i >= 0; i-- {
    use(i, s[i])
}

Depois:

example.gogo
for i, v := range slices.Backward(s) {
    use(i, v)
}

mapsloop (Go 1.21): ciclos de cópia, clonagem e inserção passam a chamadas a maps

O pacote maps tem uma função para cada um dos ciclos sobre maps mais comuns, por isso o nome da chamada diz a quem lê o que o ciclo fazia.

Antes:

example.gogo
func copyMap(src map[string]int) map[string]int {
    dst := make(map[string]int, len(src))
    for k, v := range src {
        dst[k] = v
    }
    return dst
}

Depois:

example.gogo
func copyMap(src map[string]int) map[string]int {
    dst := make(map[string]int, len(src))
    maps.Copy(dst, src)
    return dst
}

O mesmo modernizador cobre maps.Clone, maps.Equal e o padrão de inserção a partir de pares.

stditerators (Go 1.23): usa as funções iteradoras da biblioteca padrão em ciclos range

maps.Keys, maps.Values, slices.All, slices.Values e companhia devolvem iteradores que podes percorrer diretamente com range, por isso não precisas de construir primeiro um slice intermédio.

Antes:

example.gogo
keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
for _, k := range keys {
    use(k)
}

Depois:

example.gogo
for k := range maps.Keys(m) {
    use(k)
}

reflecttypefor (Go 1.22): elimina o truque de reflect com um ponteiro nil

reflect.TypeFor[T]() é a forma tipada e genérica de obter um reflect.Type para um tipo estático. O velho contorno TypeOf((*T)(nil)).Elem() é anterior aos genéricos.

Antes:

example.gogo
t := reflect.TypeOf((*MyType)(nil)).Elem()

Depois:

example.gogo
t := reflect.TypeFor[MyType]()

omitzero (Go 1.24): troca o frágil omitempty por omitzero

omitempty avalia se o valor é zero segundo as regras do Go, o que significa que um time.Time{} não está «vazio» (é um struct com campos) e, por isso, nunca é omitido. omitzero chama o método IsZero do valor quando este existe, pelo que os valores zero de time.Time desaparecem da saída, como seria de esperar.

O analisador só altera os campos em que omitzero é seguro. Strings, slices e ponteiros ficam com omitempty porque o comportamento deles mudaria.

Antes:

example.gogo
type User struct {
    Name    string    `json:"name,omitempty"`
    Created time.Time `json:"created,omitempty"`
}

Depois:

example.gogo
type User struct {
    Name    string    `json:"name,omitempty"`
    Created time.Time `json:"created,omitzero"`
}

Strings e formatação

Estes cinco substituem padrões antigos de strings e fmt por APIs mais recentes. Alguns poupam uma alocação e outros são simplesmente mais fáceis de ler.

fmtappendf (Go 1.19): []byte(fmt.Sprintf(...)) passa a fmt.Appendf

fmt.Appendf escreve a saída formatada diretamente num []byte que tu forneces (ou nil para uma nova alocação), sem passar pela string intermédia.

Antes:

example.gogo
b := []byte(fmt.Sprintf("%s=%d", key, val))

Depois:

example.gogo
b := fmt.Appendf(nil, "%s=%d", key, val)

stringscut (Go 1.18): strings.Index com slicing manual passa a strings.Cut

strings.Cut devolve o prefixo, o sufixo e um bool que indica se encontrou o separador, tudo numa só chamada. Também trata de forma explícita o caso em que não o encontra.

Antes:

example.gogo
i := strings.Index(s, "=")
if i >= 0 {
    key, val := s[:i], s[i+1:]
    use(key, val)
}

Depois:

example.gogo
if key, val, ok := strings.Cut(s, "="); ok {
    use(key, val)
}

stringscutprefix (Go 1.20): HasPrefix mais TrimPrefix passa a CutPrefix

CutPrefix devolve a string já sem o prefixo e um bool que indica se o encontrou, por isso só verificas o prefixo uma vez.

Antes:

example.gogo
if strings.HasPrefix(s, "go-") {
    rest := strings.TrimPrefix(s, "go-")
    handle(rest)
}

Depois:

example.gogo
if rest, ok := strings.CutPrefix(s, "go-"); ok {
    handle(rest)
}

O mesmo modernizador trata também de CutSuffix.

stringsseq (Go 1.24): Split e Fields em ciclos range passam a SplitSeq e FieldsSeq

SplitSeq e FieldsSeq devolvem iteradores em vez de alocarem um slice. Se só precisas de percorrer as partes uma vez, a forma com iterador evita por completo essa alocação.

Antes:

example.gogo
for _, line := range strings.Split(s, "\n") {
    handle(line)
}

Depois:

example.gogo
for line := range strings.SplitSeq(s, "\n") {
    handle(line)
}

O analisador só reescreve os casos em que o slice é consumido uma vez e descartado. Se guardas o slice ou acedes a ele por índice, a forma original com Split mantém-se.

stringsbuilder (Go 1.10): concatenação quadrática em ciclos passa a strings.Builder

s += p dentro de um ciclo realoca e copia em cada iteração, o que resulta em trabalho O(n²). strings.Builder cresce como um slice.

Antes:

example.gogo
s := ""
for _, p := range parts {
    s += p
}
return s

Depois:

example.gogo
var sb strings.Builder
for _, p := range parts {
    sb.WriteString(p)
}
return sb.String()

Aqui o analisador é conservador. Só atua perante o padrão acumulador óbvio, e não em todos os += dentro de um ciclo. Se o teu ciclo concatena duas ou três constantes pequenas, a forma simples serve perfeitamente.


Redes, erros e reflexão

Este grupo tem três: uma armadilha antiga do net, o novo errors.AsType genérico e as funções auxiliares mais seguras do unsafe.

hostport (Go 1.0): Sprintf para endereços passa a net.JoinHostPort

fmt.Sprintf("%s:%d", host, port) constrói um endereço inválido quando host é um literal IPv6, já que os endereços IPv6 têm os seus próprios dois pontos. net.JoinHostPort coloca os hosts IPv6 entre parênteses retos e é a forma canónica de construir uma string host:port.

Antes:

example.gogo
addr := fmt.Sprintf("%s:%d", host, port)
net.Dial("tcp", addr)

Depois:

example.gogo
addr := net.JoinHostPort(host, strconv.Itoa(port))
net.Dial("tcp", addr)

errorsastype (Go 1.26): errors.As passa ao errors.AsType genérico

O 1.26 acrescenta errors.AsType[T], que devolve o erro tipado e um bool, em vez de te obrigar a declarar uma variável de destino e a passar o seu endereço.

Antes:

example.gogo
var myerr *MyErr
if errors.As(err, &myerr) {
    handle(myerr)
}

Depois:

example.gogo
if myerr, ok := errors.AsType[*MyErr](err); ok {
    handle(myerr)
}

unsafefuncs (Go 1.17): aritmética com unsafe passa a unsafe.Add e unsafe.Slice

O 1.17 acrescentou unsafe.Add e unsafe.Slice para poderes fazer aritmética de ponteiros e construir slices sobre memória arbitrária sem teres de escrever à mão toda a dança com uintptr.

Antes (base é um unsafe.Pointer):

example.gogo
p := unsafe.Pointer(uintptr(base) + uintptr(off))

Depois:

example.gogo
p := unsafe.Add(base, off)

O mesmo modernizador reescreve os padrões manuais com unsafe.Pointer e reflect.SliceHeader para usarem unsafe.Slice.


Concorrência e testes

Os últimos quatro arrumam código que usa sync, atomic e o pacote testing.

waitgroupgo (Go 1.25): Add(1) mais defer Done() passa a wg.Go

O 1.25 acrescentou WaitGroup.Go, que junta o Add, o lançamento da goroutine e o defer Done numa só chamada.

Antes:

example.gogo
var wg sync.WaitGroup
wg.Add(1)
go func() {
    defer wg.Done()
    fmt.Println("go!")
}()
wg.Wait()

Depois:

example.gogo
var wg sync.WaitGroup
wg.Go(func() {
    fmt.Println("go!")
})
wg.Wait()

atomictypes (Go 1.19): atómicos em estilo de função passam a estilo de método

atomic.Int64, atomic.Uint64, atomic.Pointer[T] e companhia guardam o seu próprio valor, por isso deixas de andar a passar ponteiros de um lado para o outro e de errar ao fazer corresponder o tamanho da operação ao tipo da variável.

Antes:

example.gogo
var count int64
atomic.AddInt64(&count, 1)
n := atomic.LoadInt64(&count)

Depois:

example.gogo
var count atomic.Int64
count.Add(1)
n := count.Load()

testingcontext (Go 1.24): WithCancel manual em testes passa a t.Context()

t.Context() devolve um context que é cancelado automaticamente quando o teste termina. Deixas de ter de te lembrar do defer cancel(), e o context propaga-se aos subtestes.

Antes:

example.gogo
func TestThing(t *testing.T) {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    work(ctx)
}

Depois:

example.gogo
func TestThing(t *testing.T) {
    ctx := t.Context()
    work(ctx)
}

bloop (Go 1.24): for i := 0; i < b.N; i++ passa a for b.Loop()

b.Loop() é o novo ciclo de benchmark. Também trata automaticamente de reiniciar e parar o temporizador, o que significa que as chamadas manuais a b.ResetTimer() e b.StopTimer() à volta da preparação desaparecem na maioria dos benchmarks.

Antes:

example.gogo
func BenchmarkThing(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        work()
    }
}

Depois:

example.gogo
func BenchmarkThing(b *testing.B) {
    for b.Loop() {
        work()
    }
}

Como executar o go fix em segurança

O go fix reescreve ficheiros de código-fonte reais. Trata-o como qualquer outra refatoração automática.

Uma ordem de operações segura:

  1. Começa numa branch limpa, sem outras alterações em curso.
  2. Corre primeiro go fix -diff ./.... A flag imprime um diff unificado para o stdout em vez de escrever ficheiros. Dá-lhe uma vista de olhos.
  3. Corre go fix ./... para aplicar as alterações.
  4. Corre git diff sobre as alterações reais. Procura qualquer reescrita que te surpreenda, sobretudo em código que foi escrito de propósito num estilo mais antigo.
  5. Corre os teus testes, o teu linter e go build ./....
  6. Faz commit.

Uma só execução muitas vezes não chega

Aplicar uma correção pode abrir caminho a outra, e os analisadores não perseguem essa cascata dentro de uma só passagem.

O limite da secção do minmax mostra isto. Começa com:

example.gogo
x := f()
if x < 0 {
    x = 0
}
if x > 100 {
    x = 100
}

A primeira execução reescreve o limite inferior com max:

example.gogo
x := max(0, f())
if x > 100 {
    x = 100
}

A segunda execução vê o padrão if/atribuição que resta e termina o trabalho:

example.gogo
x := min(100, max(0, f()))

São precisas duas passagens porque cada modernizador é um analisador independente que olha para o teu código uma vez por cada execução do go fix. Percorre a árvore sintática, encontra os padrões que conhece e emite correções para eles.

No exemplo do limite, a primeira passagem reescreve o bloco < 0. Depois dessa alteração, o bloco > 100 vem a seguir a uma chamada a max(...) em vez de um simples x := f(), o que é uma nova oportunidade para o minmax. Mas o minmax já fez a sua única passagem, por isso só vai ver a nova forma quando voltares a correr o go fix.

O mesmo acontece entre analisadores. Uma reescrita do mapsloop pode abrir caminho a uma do stditerators, porque o analisador de iteradores procurava uma forma range maps.Keys(m) que não existia antes de o ciclo ser reescrito.

A solução é voltar a correr go fix ./... até o diff vir vazio. Duas passagens chegam para a maioria das bases de código, e uma terceira é rara.

Outras notas

  • Os modernizadores dependentes da versão (forvar, rangeint, newexpr, errorsastype) só atuam se o go.mod declarar a versão mínima certa. Se não vês a reescrita que esperavas, verifica a tua diretiva go.
  • Para migrações específicas da tua base de código, usa a diretiva //go:fix inline. Marca um wrapper ou uma constante, corre go fix e todas as chamadas são atualizadas no próprio sítio.
  • Se uma reescrita altera o comportamento de uma forma que o analisador não detetou, isso é um bug do analisador e vale a pena reportá-lo. A equipa do Go faz a triagem desses casos rapidamente.

Onde aprender mais

Para a cobertura completa da versão Go 1.26 (novas APIs de criptografia, Green Tea GC, deteção de fugas de goroutines e o resto), consulta o guia de novidades do Go 1.26.

Nas fontes originais, a documentação de x/tools/go/analysis/passes/modernize cobre cada analisador em detalhe, e o artigo do blog do Go sobre o go fix explica o novo framework.

Escreve Go como um engenheiro sénior

Lições interativas no navegador. As primeiras são grátis.

Experimenta uma lição grátisOu cria uma conta gratuita