Herramientas 6 min de lectura 1135 palabras

go fix vuelve: modernizar código Go con un solo comando

EN
go fix vuelve: modernizar código Go con un solo comando

Si llevas unos cuantos años escribiendo Go, tu código tiene capas. Hay un interface{} de cuando aún no existía any, un for i := 0; i < n; i++ de antes del range sobre enteros, algún x := x dentro de un bucle para esquivar el famoso problema de las variables de iteración y más de un if encadenado que hoy sería un min(). Nada de eso está mal. Simplemente, el lenguaje ha evolucionado y el código se ha quedado donde estaba.

Para eso ha vuelto go fix. Alan Donovan lo explicaba en el blog oficial de Go, en Using go fix to modernize Go code: con Go 1.26 el subcomando se ha reescrito por completo y ahora hace algo muy concreto: buscar sitios donde tu código podría usar características modernas del lenguaje o de la librería estándar, y aplicar el cambio por ti.

Un poco de historia

go fix no es nuevo. Existe desde antes de Go 1.0, cuando el lenguaje cambiaba lo bastante como para que hiciera falta una herramienta que reescribiera el código antiguo. Después de la 1.0 y su promesa de compatibilidad se quedó prácticamente sin trabajo, y durante años fue uno de esos comandos que casi nadie ejecutaba.

La versión nueva reutiliza la infraestructura de análisis que ya usan go vet y gopls. Lo que antes eran sugerencias en el editor (esas líneas punteadas que te proponen “simplificar esto”) ahora se pueden aplicar a todo un proyecto de una vez.

Cómo se usa

Lo básico es lo que esperarías:

# Aplicar todas las correcciones al módulo
go fix ./...

# Ver qué cambiaría, sin tocar nada
go fix -diff ./...

# Listar los fixers disponibles y la ayuda de uno concreto
go tool fix help
go tool fix help forvar

También puedes activar o desactivar analizadores concretos:

go fix -any ./...        # sólo el fixer "any"
go fix -any=false ./...  # todos menos "any"

Un consejo que da el propio artículo y que suscribo: ejecútalo con el árbol de git limpio. Así el resultado es un diff que puedes revisar con calma, y si algo no te convence, lo descartas. Los ficheros generados se saltan automáticamente.

Algunos de los modernizadores

La lista completa la da go tool fix help, pero estos son los que más me han llamado la atención:

FixerQué hace
anyCambia interface{} por any
forvarElimina el x := x redundante en bucles (Go 1.22+)
minmaxSustituye if/else por min() y max()
rangeintConvierte bucles de tres cláusulas en for range n (Go 1.22+)
stringscutUsa strings.Cut en lugar de strings.Index y slicing
fmtappendfCambia []byte(fmt.Sprintf(...)) por fmt.Appendf
mapsloopUsa el paquete maps en vez de bucles explícitos
newexprAprovecha new(expr), novedad de Go 1.26
inlineAplica inlining guiado por directivas //go:fix inline

Algunos ejemplos se entienden mejor viéndolos. El de minmax:

// Antes
x := f()
if x < 0 {
    x = 0
}
if x > 100 {
    x = 100
}

// Después
x := min(max(f(), 0), 100)

El de rangeint, cuando el índice ni siquiera se usa:

// Antes
for i := 0; i < n; i++ {
    f()
}

// Después
for range n {
    f()
}

Y stringscut, que es de esos que agradeces porque el código original era fácil de escribir mal:

// Antes
eq := strings.IndexByte(pair, '=')
result[pair[:eq]] = pair[1+eq:]

// Después
before, after, _ := strings.Cut(pair, "=")
result[before] = after

Mi favorito es newexpr. En Go 1.26, new acepta una expresión y no sólo un tipo, así que todas esas funciones auxiliares que acabamos escribiendo en cada proyecto para obtener un puntero a un literal sobran:

// Antes
func newInt(x int) *int { return &x }

cfg := Config{Attempts: newInt(10)}

// Después
cfg := Config{Attempts: new(10)}

Quien haya trabajado con structs de configuración o con APIs generadas donde los campos opcionales son punteros sabe cuántas veces ha escrito (o copiado) esa función.

Lo que conviene saber

La herramienta es prudente, y eso me gusta. Algunos detalles:

  • Respeta la versión de Go del módulo. Sólo aplica un cambio si el fichero exige la versión necesaria, ya sea por la directiva go del go.mod o por una restricción //go:build go1.X. No te va a meter un for range n en un proyecto que todavía compila con Go 1.21.
  • Limpia los imports que quedan sin usar después de aplicar las correcciones.
  • Puede merecer una segunda pasada. Un fix a veces abre la puerta a otro, así que el artículo recomienda ejecutarlo más de una vez; normalmente con dos basta.
  • Los conflictos semánticos no se resuelven solos. Si dos correcciones independientes chocan en significado, puede que el código no compile y te toque arreglarlo a mano. Es raro, pero puede pasar.
  • Cada plataforma se analiza por separado. Si tienes ficheros con build tags por sistema operativo o arquitectura, conviene ejecutarlo con distintos GOOS/GOARCH:
GOOS=linux   GOARCH=amd64 go fix ./...
GOOS=darwin  GOARCH=arm64 go fix ./...
GOOS=windows GOARCH=amd64 go fix ./...

También hay modernizadores que se han quedado fuera a propósito porque cambian el comportamiento de forma sutil. El ejemplo que dan es slices.Clone: devuelve nil para un slice vacío, mientras que el código que reemplazaría devuelve un slice vacío pero no nulo. Para la mayoría de programas da igual, pero para alguno no, y un go fix que rompe cosas en silencio no le sirve a nadie.

Hacia dónde va

La parte final del artículo es la que me parece más interesante a medio plazo. La idea es pasar a un modelo de autoservicio: que los mantenedores de una librería puedan publicar sus propios modernizadores junto al código, y que go fix o gopls los carguen y ejecuten de forma segura. La directiva //go:fix inline ya apunta en esa dirección, porque permite marcar una función obsoleta para que las llamadas se reescriban por su sustituta.

También hablan de generalizar comprobaciones de flujo de control del tipo “no olvides hacer X después de Y”: cerrar un fichero, cancelar un contexto, liberar un mutex. Hoy son comprobaciones concretas y la idea es que cada uno pueda aplicarlas a sus propios tipos mediante anotaciones.

Por qué me parece importante

Lo interesante de go fix no es ningún cambio concreto, porque casi todos los podrías hacer a mano. Lo interesante es que nadie los hace. En cualquier proyecto con unos años encima, actualizar interface{} a any o simplificar bucles nunca es prioritario, y así el código acumula estilos de cinco versiones distintas del lenguaje.

Con un comando que respeta la versión del módulo, genera un diff limpio y no cambia el comportamiento, tener el código al día deja de ser un proyecto que nunca encuentra hueco. Pasa a ser un paso más cada vez que subes la versión de Go en el go.mod. El artículo tiene ya unos meses, pero sigue valiendo la pena leerlo entero.