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.bashbashgo 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
- Slices, maps e iteración
- Strings y formato
- Redes, errores y reflexión
- Concurrencia y tests
- Cómo ejecutar go fix de forma segura
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.gogofunc log(args ...interface{}) { fmt.Println(args...) } var cache map[string]interface{}
Después:
example.gogofunc 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.gogox := f() if x < 0 { x = 0 } if x > 100 { x = 100 }
Después:
example.gogox := 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.gogofor i := 0; i < n; i++ { fmt.Println(i) }
Después:
example.gogofor 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.gogofor _, x := range items { x := x // pre-1.22 capture trick go func() { use(x) }() }
Después:
example.gogofor _, 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.gogofunc strPtr(s string) *string { p := s return &p } cfg.Name = strPtr("LevelUpGo")
Después:
example.gogocfg.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.gogosort.Slice(s, func(i, j int) bool { return s[i] < s[j] })
Después:
example.gogoslices.Sort(s)
slicescontains (Go 1.21): los bucles de búsqueda manuales pasan a ser slices.Contains
Antes:
example.gogofunc has(s []int, x int) bool { for _, v := range s { if v == x { return true } } return false }
Después:
example.gogofunc 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.gogofor i := len(s) - 1; i >= 0; i-- { use(i, s[i]) }
Después:
example.gogofor 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.gogofunc 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.gogofunc 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.gogokeys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } for _, k := range keys { use(k) }
Después:
example.gogofor 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.gogot := reflect.TypeOf((*MyType)(nil)).Elem()
Después:
example.gogot := 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.gogotype User struct { Name string `json:"name,omitempty"` Created time.Time `json:"created,omitempty"` }
Después:
example.gogotype 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.gogob := []byte(fmt.Sprintf("%s=%d", key, val))
Después:
example.gogob := 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.gogoi := strings.Index(s, "=") if i >= 0 { key, val := s[:i], s[i+1:] use(key, val) }
Después:
example.gogoif 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.gogoif strings.HasPrefix(s, "go-") { rest := strings.TrimPrefix(s, "go-") handle(rest) }
Después:
example.gogoif 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.gogofor _, line := range strings.Split(s, "\n") { handle(line) }
Después:
example.gogofor 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.gogos := "" for _, p := range parts { s += p } return s
Después:
example.gogovar 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.gogoaddr := fmt.Sprintf("%s:%d", host, port) net.Dial("tcp", addr)
Después:
example.gogoaddr := 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.gogovar myerr *MyErr if errors.As(err, &myerr) { handle(myerr) }
Después:
example.gogoif 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.gogop := unsafe.Pointer(uintptr(base) + uintptr(off))
Después:
example.gogop := 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.gogovar wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() fmt.Println("go!") }() wg.Wait()
Después:
example.gogovar 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.gogovar count int64 atomic.AddInt64(&count, 1) n := atomic.LoadInt64(&count)
Después:
example.gogovar 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.gogofunc TestThing(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() work(ctx) }
Después:
example.gogofunc 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.gogofunc BenchmarkThing(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { work() } }
Después:
example.gogofunc 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:
- Empieza en una rama limpia, sin otros cambios a medias.
- Ejecuta primero
go fix -diff ./.... El flag imprime un diff unificado por stdout en lugar de escribir archivos. Échale un vistazo. - Ejecuta
go fix ./...para aplicar los cambios. - Ejecuta
git diffsobre las modificaciones reales. Busca cualquier reescritura que te sorprenda, sobre todo en código que se escribió a propósito con un estilo antiguo. - Ejecuta tus tests, tu linter y
go build ./.... - 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.gogox := f() if x < 0 { x = 0 } if x > 100 { x = 100 }
La primera ejecución convierte el límite inferior en max:
example.gogox := 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.gogox := 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 sigo.moddeclara la versión mínima adecuada. Si no ves la reescritura que esperabas, revisa tu directivago. - Para migraciones propias de tu base de código, usa la directiva
//go:fix inline. Marca un wrapper o una constante, ejecutago fixy 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.
