Volver al blog

Todos los modernizadores de `go fix` en Go 1.26

Go 1.26 reescribió go fix sobre el framework de analizadores. Aquí tienes todos los modernizadores que incluye: 26 diffs de antes y después que abarcan el lenguaje y la biblioteca estándar.

Todos los modernizadores de `go fix` en Go 1.26

Go 1.26 reescribió go fix sobre el mismo framework que usa go vet. El nuevo comando incluye todos los modernizadores que han creado el equipo de Go y los autores de x/tools. Cada uno es un pequeño analizador que reconoce un patrón antiguo y lo reescribe en Go actual.

Un solo comando los ejecuta todos:

example.bashbash
go fix ./...

Ejecútalo en una rama limpia, revisa el diff, pasa los tests y haz commit. Si quieres ver una vista previa sin escribir archivos, usa go fix -diff ./....

A continuación están todos los modernizadores que trae 1.26, agrupados por área. Cada entrada indica la versión de Go que introdujo la API a la que reescribe, junto con un ejemplo de antes y después. Está pensado tanto para ojearlo o buscar con ctrl-F como para leerlo entero.

Índice


Lenguaje y funciones integradas

Aquí hay siete modernizadores. La mayoría cubren funcionalidades del lenguaje (any, la nueva forma range n, el cambio en la variable de bucle de 1.22, la extensión de new(expr)) y un par de limpiezas que abarcan varias versiones.

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

any es solo un alias, pero se lee mejor en firmas y parámetros de tipo.

Antes:

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

var cache map[string]interface{}

Después:

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

var cache map[string]any

minmax (Go 1.21): sustituye los if/else que acotan valores por min y max

Las funciones integradas min y max son variádicas y funcionan con cualquier tipo ordenado.

Antes:

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

Después:

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

rangeint (Go 1.22): bucles for de enteros con for i := range n

Un bucle de conteo simple, sin un slice ni un array detrás, puede hacer range directamente sobre el entero.

Antes:

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

Después:

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

forvar (Go 1.22): elimina el truco de captura x := x

Antes de 1.22, la variable del bucle se compartía entre iteraciones, así que los closures y las goroutines que la capturaban necesitaban x := x dentro del cuerpo del bucle. Desde 1.22 cada iteración tiene su propia variable, por lo que esa redeclaración sobra.

Antes:

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

Después:

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

El analizador solo actúa cuando go.mod declara go 1.22 o una versión posterior, ya que los módulos más antiguos siguen teniendo la semántica de variable compartida.

newexpr (Go 1.26): puntero a un valor con new(expr)

1.26 amplía la función integrada new para que acepte una expresión, no solo un tipo. new(s) devuelve un *string que apunta a una copia de s. Con eso sobran los típicos helpers strPtr/intPtr/boolPtr.

Antes:

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

cfg.Name = strPtr("LevelUpGo")

Después:

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

Si tienes helpers que todavía no puedes borrar (otros llamadores, API exportada), consulta inline más abajo para ver la directiva que te permite migrar de forma gradual.

inline (directiva): expande funciones y constantes marcadas con //go:fix inline

Añade la directiva a un wrapper y go fix reescribe cada llamada para que use la expresión interna. A mí me resulta útil para renombrar y para retirar wrappers finos. También puedes usarla para aplicar tus propios modernizadores en una 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)

Después:

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

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

La misma directiva funciona con constantes, lo que la hace útil para migrar entre valores de enum renombrados.

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

La sintaxis antigua de build tags está obsoleta desde 1.17. La nueva directiva usa expresiones booleanas normales de Go.

Antes:

example.gogo
// +build linux,amd64

package sys

Después:

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

package sys

Slices, maps e iteración

Cinco de los siete modernizadores de este grupo llevan los bucles escritos a mano a los paquetes slices y maps. Los otros dos se ocupan de reflect.TypeFor y de las etiquetas JSON omitzero.

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

Para el caso habitual de ordenar de forma ascendente un slice de un tipo ordenado, slices.Sort es genérico y evita por completo el closure de comparación.

Antes:

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

Después:

example.gogo
slices.Sort(s)

slicescontains (Go 1.21): los bucles de búsqueda manuales pasan a ser slices.Contains

Antes:

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

Después:

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

slicesbackward (Go 1.23): iteración inversa con slices.Backward

slices.Backward devuelve un iterador que produce pares de índice y valor desde el final del slice hasta el principio.

Antes:

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

Después:

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

mapsloop (Go 1.21): los bucles de copia, clonado e inserción pasan a ser llamadas a maps

El paquete maps tiene una función para cada uno de los bucles sobre maps habituales, así que el nombre de la llamada le dice al lector qué hacía el bucle.

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
}

Después:

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

El mismo modernizador cubre maps.Clone, maps.Equal y el patrón de inserción a partir de pares.

stditerators (Go 1.23): usa las funciones iteradoras de la biblioteca estándar en bucles range

maps.Keys, maps.Values, slices.All, slices.Values y compañía devuelven iteradores que puedes recorrer directamente con range, así que no necesitas construir antes un slice intermedio.

Antes:

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

Después:

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

reflecttypefor (Go 1.22): elimina el truco de reflect con un puntero nil

reflect.TypeFor[T]() es la forma tipada y genérica de obtener un reflect.Type para un tipo estático. El viejo apaño TypeOf((*T)(nil)).Elem() es anterior a los genéricos.

Antes:

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

Después:

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

omitzero (Go 1.24): cambia el frágil omitempty por omitzero

omitempty se fija en si el valor es cero según Go, lo que significa que un time.Time{} no está «vacío» (es un struct con campos), así que nunca se omite. omitzero llama al método IsZero del valor cuando existe, de modo que los valores cero de time.Time desaparecen de la salida, como cabría esperar.

El analizador solo cambia los campos en los que omitzero es seguro. Los strings, los slices y los punteros se quedan con omitempty porque su comportamiento cambiaría.

Antes:

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

Después:

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

Strings y formato

Estos cinco sustituyen patrones antiguos de strings y fmt por APIs más nuevas. Algunos se ahorran una reserva de memoria y otros simplemente se leen mejor.

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

fmt.Appendf escribe la salida formateada directamente en un []byte que tú le pasas (o nil para una reserva nueva), sin pasar por el string intermedio.

Antes:

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

Después:

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

stringscut (Go 1.18): strings.Index con recorte manual pasa a ser strings.Cut

strings.Cut devuelve el prefijo, el sufijo y un bool que indica si encontró el separador, todo en una sola llamada. Además, trata de forma explícita el caso en que no lo encuentra.

Antes:

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

Después:

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

stringscutprefix (Go 1.20): HasPrefix más TrimPrefix pasa a ser CutPrefix

CutPrefix devuelve el string recortado y un bool que indica si encontró el prefijo, así que solo compruebas el prefijo una vez.

Antes:

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

Después:

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

El mismo modernizador se encarga también de CutSuffix.

stringsseq (Go 1.24): Split y Fields en bucles range pasan a ser SplitSeq y FieldsSeq

SplitSeq y FieldsSeq devuelven iteradores en lugar de reservar un slice. Si solo necesitas recorrer las partes una vez, la forma con iterador se ahorra esa reserva por completo.

Antes:

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

Después:

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

El analizador solo reescribe los casos en los que el slice se consume una vez y se descarta. Si guardas el slice o accedes a él por índice, se mantiene la forma original con Split.

stringsbuilder (Go 1.10): la concatenación cuadrática en bucles pasa a ser strings.Builder

s += p dentro de un bucle reserva memoria y copia en cada iteración, lo que acaba siendo un trabajo O(n²). strings.Builder crece como un slice.

Antes:

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

Después:

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

Aquí el analizador es conservador. Solo actúa ante el patrón acumulador evidente, no con cada += dentro de un bucle. Si tu bucle concatena dos o tres constantes pequeñas, la forma simple está bien.


Redes, errores y reflexión

Este grupo tiene tres: una vieja trampa de net, el nuevo errors.AsType genérico y los helpers más seguros de unsafe.

hostport (Go 1.0): Sprintf para direcciones pasa a ser net.JoinHostPort

fmt.Sprintf("%s:%d", host, port) construye una dirección incorrecta cuando host es un literal IPv6, porque las direcciones IPv6 ya contienen sus propios dos puntos. net.JoinHostPort pone los hosts IPv6 entre corchetes y es la forma canónica de construir un string host:port.

Antes:

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

Después:

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

errorsastype (Go 1.26): errors.As pasa a ser el genérico errors.AsType

1.26 añade errors.AsType[T], que devuelve el error tipado y un bool en lugar de pedirte que declares una variable de destino y pases su dirección.

Antes:

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

Después:

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

unsafefuncs (Go 1.17): la aritmética con unsafe pasa a ser unsafe.Add y unsafe.Slice

1.17 añadió unsafe.Add y unsafe.Slice para que puedas hacer aritmética de punteros y construir slices sobre memoria arbitraria sin tener que escribir a mano todo el baile con uintptr.

Antes (base es un unsafe.Pointer):

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

Después:

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

El mismo modernizador reescribe los patrones manuales de unsafe.Pointer con reflect.SliceHeader para que usen unsafe.Slice.


Concurrencia y tests

Los cuatro últimos limpian el código que usa sync, atomic y el paquete testing.

waitgroupgo (Go 1.25): Add(1) más defer Done() pasa a ser wg.Go

1.25 añadió WaitGroup.Go, que combina el Add, el lanzamiento de la goroutine y el defer Done en una sola llamada.

Antes:

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

Después:

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

atomictypes (Go 1.19): los atómicos de estilo función pasan a ser de estilo método

atomic.Int64, atomic.Uint64, atomic.Pointer[T] y compañía guardan su propio valor, así que dejas de ir pasando punteros de un lado a otro y de equivocarte al emparejar el tamaño de la operación con el tipo de la variable.

Antes:

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

Después:

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

testingcontext (Go 1.24): el WithCancel manual en tests pasa a ser t.Context()

t.Context() devuelve un context que se cancela automáticamente cuando termina el test. No tienes que acordarte de defer cancel(), y el context se propaga a los subtests.

Antes:

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

Después:

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

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

b.Loop() es el nuevo bucle de benchmark. Además gestiona automáticamente el reinicio y la parada del temporizador, así que en la mayoría de los benchmarks desaparecen las llamadas manuales a b.ResetTimer() y b.StopTimer() alrededor de la preparación.

Antes:

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

Después:

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

Cómo ejecutar go fix de forma segura

go fix reescribe archivos de código fuente reales. Trátalo como cualquier otra refactorización automática.

Un orden de pasos seguro:

  1. Empieza en una rama limpia, sin otros cambios a medias.
  2. Ejecuta primero go fix -diff ./.... El flag imprime un diff unificado por stdout en lugar de escribir archivos. Échale un vistazo.
  3. Ejecuta go fix ./... para aplicar los cambios.
  4. Ejecuta git diff sobre las modificaciones reales. Busca cualquier reescritura que te sorprenda, sobre todo en código que se escribió a propósito con un estilo antiguo.
  5. Ejecuta tus tests, tu linter y go build ./....
  6. Haz commit.

Una sola ejecución a menudo no basta

Aplicar una corrección puede abrir la puerta a otra, y los analizadores no persiguen esa cascada dentro de una misma pasada.

El código que acota un valor de la sección de minmax lo muestra. Parte de esto:

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

La primera ejecución convierte el límite inferior en max:

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

La segunda ejecución ve el patrón de if con asignación que queda y termina el trabajo:

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

Hacen falta dos pasadas porque cada modernizador es un analizador independiente que mira tu código una vez por cada ejecución de go fix. Recorre el árbol sintáctico, encuentra los patrones que conoce y emite las correcciones correspondientes.

En el ejemplo del acotamiento, la primera pasada reescribe el bloque < 0. Después de ese cambio, el bloque > 100 va detrás de una llamada a max(...) en lugar de un simple x := f(), y eso es una nueva oportunidad para minmax. Pero minmax ya ha hecho su única pasada, así que no verá la nueva forma hasta que vuelvas a ejecutar go fix.

Lo mismo ocurre entre analizadores. Una reescritura de mapsloop puede abrir una oportunidad para stditerators, porque el analizador de iteradores buscaba una forma range maps.Keys(m) que no existía antes de que se reescribiera el bucle.

La solución es volver a ejecutar go fix ./... hasta que el diff salga vacío. En la mayoría de las bases de código bastan dos pasadas, y rara vez hace falta una tercera.

Otras notas

  • Los modernizadores que dependen de la versión (forvar, rangeint, newexpr, errorsastype) solo actúan si go.mod declara la versión mínima adecuada. Si no ves la reescritura que esperabas, revisa tu directiva go.
  • Para migraciones propias de tu base de código, usa la directiva //go:fix inline. Marca un wrapper o una constante, ejecuta go fix y todas las llamadas se actualizan en su sitio.
  • Si una reescritura cambia el comportamiento de una forma que el analizador no detectó, es un bug del analizador y vale la pena reportarlo. El equipo de Go revisa esos casos rápido.

Dónde aprender más

Para ver todas las novedades de Go 1.26 (nuevas APIs de criptografía, el GC Green Tea, la detección de fugas de goroutines y el resto), consulta la guía de novedades de Go 1.26.

En las fuentes originales, la documentación de x/tools/go/analysis/passes/modernize describe cada analizador en detalle, y el artículo del blog de Go sobre go fix explica el nuevo framework.

Escribe Go como un ingeniero sénior

Lecciones interactivas en tu navegador. Las primeras son gratis.

Prueba una lección gratisO crea una cuenta gratuita