---
id: 1a31e06c0d969b4b6cbbc590259cf089
author: iwanhae
title: Go 1.27, o cómo aprendí a dejar de preocuparme y amar los métodos genéricos
description: Métodos genéricos, encoding/json/v2, un paquete uuid estándar y un detector de fugas de goroutines que realmente funciona. Un recorrido por Go 1.27 con ejemplos que fueron todos efectivamente ejecutados.
language: es
date: 2026-08-15T02:08:43.129534158Z
path: /blog/posts/go-1-27-or-how-i-learned-to-stop-worrying-and-love-generic-methods-za800e454
category: release
---

![Panorama general de Go 1.27](/assets/images/whats-new-in-go1.27/overview.webp)

Go 1.27 llega en agosto de 2026, y por una vez el lenguaje en sí cambió. No el tipo de cambio "añadimos una nueva función a `slices`", sino un cambio real. Los métodos genéricos ya están disponibles. Una incidencia de once años de antigüedad fue cerrada. `encoding/json` fue reemplazado silenciosamente por un nuevo motor mientras el avión estaba en vuelo.

Cada ejemplo siguiente se ejecutó con `go1.27rc3` (darwin/arm64). Cada bloque de salida es una salida real, copiada y pegada, incluidos los mensajes de error. Si algo parece extraño, es porque eso fue exactamente lo que dijo el compilador.

```bash
go install golang.org/dl/go1.27rc3@latest
go1.27rc3 download
```

## 1. El lenguaje cambió (sí, de verdad)

### 1.1. Métodos genéricos

Desde Go 1.18 contamos con genéricos y, desde Go 1.18, también con La Conversación. Va así:

> "Permíteme escribir simplemente un método `Map` en mi tipo slice…"
>
> `method must have no type parameters`
>
> "…de acuerdo. Entonces será una función a nivel de paquete."

Los métodos solo podían usar parámetros de tipo declarados por el _receptor_. Tu propio método no podía introducir otros nuevos. Así, cada transformación genérica quedaba desterrada al ámbito del paquete, donde convivía con otras diecisiete funciones auxiliares sueltas con nombres como alguna variante de `MapSlice`.

Go 1.27 corrige esto:

```go
type List[E any] []E

// F es un parámetro de tipo declarado por el propio método
func (l List[E]) Apply[F any](f func(E) F) List[F] {
	r := make(List[F], len(l))
	for i, x := range l {
		r[i] = f(x)
	}
	return r
}

func main() {
	l := List[int]{1, 2, 3}
	fmt.Println(l.Apply(func(i int) string { return fmt.Sprint(i * 10) }))
	// [10 20 30]
}
```

El receptor ni siquiera necesita ser genérico. Una estructura corriente puede tener un método genérico:

```go
type Bag struct{ items []any }

func (b *Bag) Add[T any](v T) { b.items = append(b.items, v) }
```

Ahora bien, antes de refactorizar toda tu base de código esta tarde, existen **dos restricciones**, y importan más de lo que aparentan:

1. Los métodos de interfaz no pueden declarar parámetros de tipo.
2. Los métodos genéricos no pueden implementar métodos de interfaz.

La segunda es la que te va a morder:

```go
type Adder interface{ Add(int) int }

type G struct{}

func (G) Add[T any](v T) T { return v }

var _ Adder = G{}
```

```
cannot use G{} (value of struct type G) as Adder value in variable declaration:
	G does not implement Adder (wrong type for method Add)
		have Add[T any](T) T
		want Add(int) int
```

Así que los métodos genéricos y el despacho dinámico no se combinan. Esto no es tacañería del equipo de Go: un método genérico tiene infinitas instanciaciones, y la tabla de métodos de una interfaz debe ser finita y conocida en tiempo de compilación. No se puede poner algo infinito en una tabla finita. El universo dijo que no.

Lectura práctica: los métodos genéricos son para tipos concretos con APIs de tipo utilitario. Todo lo que deba ocultarse tras una interfaz sigue necesitando el enfoque anterior.

La biblioteca estándar ya lo aprovechó. El `Rand` de `math/rand/v2` recibió un método genérico:

```go
func (r *Rand) N[Int intType](n Int) Int
```

```go
r := rand.New(rand.NewPCG(1, 2))
fmt.Println(r.N(int32(100)))     // 76
fmt.Println(r.N(time.Second))    // 616.436223ms
```

Anteriormente, solo la función a nivel de paquete `rand.N` era genérica, lo que implicaba usar la fuente global. Ahora tu propio `*Rand` con semilla recibe la misma comodidad. Algo pequeño. Algo agradable.

### 1.2. Selectores de campo en literales de struct, o: la incidencia #9859 por fin se resuelve

El embebido te da `u.ID`. El embebido **no** te da `User{ID: 1}`. En cambio, te da `User{Base: Base{ID: 1}}`, que es la manera en que Go pregunta si realmente lo querías así.

La [incidencia #9859](https://go.dev/issue/9859) se abrió en 2015. Ahora está cerrada. En algún lugar, un gopher que desde entonces ha cambiado de carrera dos veces está recibiendo una notificación de GitHub.

```go
type Object struct{ name, color string }
type Point3D struct {
	Object
	x, y, z float64
}
type Line struct {
	Object
	p, q Point3D
}

// Go 1.27: usar directamente el campo promovido
line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}
```

`name` no es un campo de `Line`. Es un campo del `Object` embebido. Anteriormente habría que escribir `Line{Object: Object{name: "diagonal"}, ...}`.

Ahora la letra pequeña, que comprobé intentando compilar todo el código.

**(a) La clave sigue siendo un identificador simple.** No puedes escribir una ruta de selector arbitraria.

```go
_ = Line{Object.name: "x"}   // invalid field name Object.name in struct literal
_ = Line{p.x: 1}             // invalid field name p.x in struct literal
```

Por tanto, la característica no es "las claves ahora funcionan como acceso a campos". Es específicamente "se permiten nombres de campos promovidos implícitamente". Un poco más restringida de lo que sugiere el titular.

**(b) No puedes especificar a la vez un campo embebido y algo promovido desde él.**

```go
obj := Object{"edge", "black"}
_ = Line{Object: obj, name: "diagonal"}
// cannot specify promoted field name and enclosing embedded field Object
```

Razonable. Le dijiste dos cosas contradictorias sobre la misma memoria y se negó a adivinar.

**(c) No se admite el embebido de punteros.**

```go
type PtrEmbed struct {
	*Object
	z int
}

_ = PtrEmbed{name: "x"}
// invalid implicit pointer indirection to reach name
```

Para seguir un puntero necesitas un puntero que seguir, y en el momento de construir el literal todavía no existe. También es justo.

La buena noticia: `go fix` reescribirá tus literales antiguos por ti. Más adelante hablaremos de ello.

### 1.3. La inferencia de tipos en funciones de tipo función se volvió menos arbitraria

La inferencia de tipos para funciones genéricas funcionaba en algunos lugares y en otros no, sin un principio discernible que determinara cuál era cuál. ¿Asignación a una variable? Bien. ¿Poner exactamente la misma función en un campo de struct? Error del compilador: escribe `double[int]` como si estuviéramos en 2022.

Go 1.27 hace que la inferencia funcione en **todo contexto donde el tipo destino se conoce de manera inequívoca**:

```go
func double[T ~int | ~float64](v T) T { return v * 2 }

type S struct{ f func(int) int }
type A [1]func(int) int

func main() {
	s := S{f: double}          // 1.26: necesitaba double[int]
	a := A{double}             // 1.26: necesitaba double[int]

	c := make(chan func(int) int, 1)
	c <- double                // 1.26: necesitaba double[int]

	var fn func(float64) float64 = double  // esto siempre funcionó

	fmt.Println(s.f(21), a[0](5), (<-c)(7), fn(1.5))
	// 42 10 14 3
}
```

Una sola `double`, instanciada como `func(int) int` en tres lugares y como `func(float64) float64` en un cuarto, enteramente a partir del contexto. Si escribes structs de opciones o tablas de manejadores llenas de valores función, una buena cantidad de ruido `[T]` está a punto de desaparecer de tu base de código.

## 2. Runtime: rendimiento gratuito y un detector de fugas

### 2.1. Asignación especializada por tamaño

El compilador ahora emite llamadas a rutinas de asignación especializadas por tamaño para objetos pequeños. Las notas de la versión afirman hasta un 30 % menos de tiempo en asignaciones de menos de 80 bytes, y alrededor de un 1 % en general para programas intensivos en asignaciones.

No creo en las notas de la versión sin medir, así que:

```go
type small struct{ a, b, c, d int }

var sink any

func BenchmarkAlloc(b *testing.B) {
	for b.Loop() {
		sink = &small{}
	}
}

func BenchmarkSlice(b *testing.B) {
	for b.Loop() {
		sink = make([]byte, 48)
	}
}
```

```
# predeterminado (malloc especializado por tamaño activado)
BenchmarkAlloc-12     3000000     5.358 ns/op
BenchmarkSlice-12     3000000    13.58 ns/op

# GOEXPERIMENT=nosizespecializedmalloc
BenchmarkAlloc-12     3000000     8.722 ns/op
BenchmarkSlice-12     3000000    20.81 ns/op

```

Un 30–35 % más rápido en un microbenchmark que solo asigna memoria en Apple Silicon. Es decir: este es el mejor caso, logrado en condiciones de laboratorio por un benchmark diseñado para que la cifra luzca bien. En un programa real, el GC y el trabajo efectivo sepultarán gran parte de esa mejora. La afirmación del ~1 % es la honesta.

El coste está en el tamaño del binario. Hello World:

```
2413202 bytes  (predeterminado)
2361826 bytes  (nosizespecializedmalloc)
```

Unos 50 KB fijos, independientemente de tu programa. Puedes desactivarlo con `GOEXPERIMENT=nosizespecializedmalloc`, pero **esa salida de emergencia está programada para eliminarse en Go 1.28**, así que trátala como una solución temporal para reportar errores y no como un estilo de vida.

### 2.2. El perfil de fugas de goroutines ya es real

Experimental en Go 1.26, disponible de forma general en 1.27. El GOEXPERIMENT `goroutineleakprofile` ya no existe; simplemente funciona.

```go
func leak() {
	ch := make(chan int) // nadie enviará nunca. nadie.
	go func() {
		<-ch
	}()
}

func main() {
	for range 3 {
		leak()
	}
	runtime.GC()
	runtime.GC()
	pprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
}
```

```
goroutineleak profile: total 3
3 @ 0x104de3f18 0x104d7f0b0 0x104d7ec34 0x104e374f4 0x104dea024
#	0x104e374f3	main.leak.func1+0x23	/tmp/go127/leak/main.go:12
```

Tres goroutines, permanentemente bloqueadas, con un archivo y un número de línea que apuntan exactamente a dónde lo hiciste.

El truco detrás de esto es genuinamente ingenioso: **reutiliza el análisis de alcanzabilidad del recolector de basura**. Si la goroutine G está bloqueada en la primitiva P, y P no es alcanzable desde ninguna goroutine ejecutable (ni desde nada que esas goroutines pudieran despertar), entonces nada puede volver a tocar P, por lo que G nunca se despertará. Eso no es una heurística: es una demostración.

El mismo diseño te da la limitación gratis: si el canal o el mutex es alcanzable a través de una variable global, o a través de una variable local de alguna goroutine que sigue en ejecución, el GC aún puede verlo, así que el runtime no puede concluir nada. Observa que incluso el ejemplo de juguete anterior necesita dos llamadas a `runtime.GC()` para informar algo. No detectará todo.

Sin embargo, sí detectará la clásica situación de "olvidé cancelar el contexto, la goroutine trabajadora vive para siempre", que es la mayoría de las fugas la mayoría de las veces.

Si importas `net/http/pprof`, también está disponible en `/debug/pprof/goroutineleak`. Conéctalo en staging, revísalo semanalmente y horrorízate en silencio.

Este trabajo fue aportado por Vlad Saioc de Uber, quien merece una bebida de su elección.

### 2.3. Los trazados de pila ahora te dicen qué petición murió

Para los módulos que declaran Go 1.27 o posterior, las líneas de encabezado de los trazados de pila ahora incluyen las etiquetas de goroutine de `runtime/pprof`.

```go
func handle(ctx context.Context) {
	var wg sync.WaitGroup
	wg.Go(func() {
		buf := make([]byte, 4<<10)
		os.Stdout.Write(buf[:runtime.Stack(buf, true)])
	})
	wg.Wait()
}

func main() {
	labels := pprof.Labels("request", "GET /orders/42", "tenant", "acme")
	pprof.Do(context.Background(), labels, handle)
}
```

```
goroutine 3 [running] {request: "GET /orders/42", tenant: acme}:
main.handle.func1()
	/tmp/go127/tblabel/main.go:15 +0x40
sync.(*WaitGroup).Go.func1()
	...
goroutine 1 [sync.WaitGroup.Wait] {request: "GET /orders/42", tenant: acme}:
...
```

El detalle importante: **las etiquetas se heredan por las goroutines hijas.** La goroutine 3 nunca estableció una etiqueta. La recibió de su padre.

Así que la próxima vez que producción se bloquee y alguien envíe SIGQUIT al proceso, en lugar de 4000 goroutines que parecen idénticas, obtendrás 4000 goroutines etiquetadas con la petición y el tenant al que pertenecen. Tres líneas de `pprof.Do` en el punto de entrada de tus peticiones te proporcionan eso.

Como las etiquetas pueden contener cosas que no querrías volcar a stderr, `GODEBUG=tracebacklabels=0` lo desactiva, y esa opción de exclusión está explícitamente pensada para quedarse para siempre.

### 2.4. asynctimerchan desaparece definitivamente

Go 1.23 hizo que los canales de temporizadores fueran sin búfer (síncronos) y ofreció `asynctimerchan=1` como forma de volver al comportamiento anterior. En 1.27, esa configuración se **elimina permanentemente**. Los canales del paquete `time` son síncronos, punto final, sin negociación.

Lo interesante es la política que se introdujo junto con ello. Un GODEBUG eliminado que quedó en tu `go.mod` no rompe la compilación automáticamente: solo la rompe si está configurado con el valor **antiguo**:

```
# go.mod contiene: godebug asynctimerchan=1
go: error loading go.mod:
go.mod:5: removed GODEBUG "asynctimerchan" set to old value "1" (https://go.dev/doc/godebug#go-127)

# go.mod contiene: godebug asynctimerchan=0
(compila sin problemas)
```

Esto significa que solo reciben reproches quienes realmente dependían del comportamiento eliminado. Quienes lo configuraron con el valor que acabó siendo el predeterminado y se olvidaron de ello pueden seguir sin pensar en ello. Es una pieza muy considerada de arqueología de API.

## 3. Biblioteca estándar

### 3.1. encoding/json/v2: reemplazaron el motor en pleno vuelo

Este es el gran cambio. [Más de dos años de discusión de propuesta](https://go.dev/issue/71497) por fin se concretaron.

Ahora hay **tres paquetes**, y entender la división es la mayor parte de la batalla:

| Paquete                  | Función                                                                        |
| ------------------------ | ------------------------------------------------------------------------------ |
| `encoding/json`          | La API v1 que conoces. **Comportamiento 100 % sin cambios.** Ahora implementada sobre v2 |
| `encoding/json/v2`       | Procesamiento semántico. Valores de Go ↔ JSON                                  |
| `encoding/json/jsontext` | Procesamiento sintáctico. JSON como flujo de tokens                            |

El titular es que `encoding/json` fue reconstruido sobre una implementación completamente distinta y **se comporta de manera idéntica**. Así:

```go
// encoding/json.Unmarshal de Go 1.27, resumido
func Unmarshal(data []byte, v any) error {
	return jsonv2.Unmarshal(data, v, DefaultOptionsV1())
}
```

Cada particularidad heredada de v1 se codificó como una opción, agrupada en `DefaultOptionsV1()`, y la API v1 siempre aplica ese conjunto. Lo que significa que esto se comporta igual en 1.26 y en 1.27:

```go
var m map[string]int
json.Unmarshal([]byte(`{"a":1,"a":2}`), &m)  // <nil>, map[a:2]
```

Sí, v1 todavía acepta en silencio claves duplicadas y se queda con la última. Siempre lo hizo. Sigue haciéndolo. La compatibilidad implica compatibilidad también con las partes malas.

**v2, en cambio, tiene opiniones:**

```go
var m map[string]int
jsonv2.Unmarshal([]byte(`{"a":1,"a":2}`), &m)
// jsontext: duplicate object member name "a"

var s string
jsonv2.Unmarshal([]byte("\"\xff\""), &s)
// jsontext: invalid UTF-8 after offset 1
```

Las claves duplicadas y el UTF-8 inválido se rechazan. Ambos son genuinamente peligrosos, no solo desordenados: cuando dos analizadores no coinciden sobre qué clave duplicada gana, aparecen errores de seguridad. La CVE-2017-12635 de CouchDB fue exactamente eso: un cuerpo JSON con dos claves `roles`, donde el validador leyó una y la capa de almacenamiento leyó la otra. Rechazar es la decisión correcta.

Las opciones son variádicas:

```go
type Config struct {
	Name    string   `json:"name"`
	Tags    []string `json:"tags,omitzero"`
	Timeout int      `json:"timeout,omitzero"`
}

out, _ := jsonv2.Marshal(Config{Name: "api"})
// {"name":"api"}

// claves de mapa ordenadas + indentación
out, _ = jsonv2.Marshal(map[string]int{"b": 2, "a": 1},
	jsonv2.Deterministic(true), jsontext.WithIndent("  "))
// {
//   "a": 1,
//   "b": 2
// }

// rechazar campos desconocidos: antes requería un Decoder y DisallowUnknownFields()
var c Config
err := jsonv2.Unmarshal([]byte(`{"name":"api","nope":1}`), &c,
	jsonv2.RejectUnknownMembers(true))
// json: cannot unmarshal JSON string into Go main.Config:
//   unknown object member name "nope"
```

`Deterministic`, `MatchCaseInsensitiveNames`, `StringifyNumbers`, `FormatNilSliceAsNull`, `OmitZeroStructFields`: la mayoría de las cosas que antes resolvías con una biblioteca de terceros o con un `MarshalJSON` escrito a mano ahora son banderas.

Y la historia de la migración es lo mejor. **Las opciones posteriores prevalecen**, así que puedes adoptar la rigurosidad de v2 un comportamiento a la vez:

```go
// mantener la semántica de v1, pero rechazar claves duplicadas como lo hace v2
jsonv2.Unmarshal(data, &v,
	json.DefaultOptionsV1(),
	jsontext.AllowDuplicateNames(false))
// duplicate object member name
```

No tienes que portar una base de código de 200 mil líneas a v2 para dejar de aceptar claves duplicadas. Cambias una opción. Para cualquier proyecto grande, este es el camino realista.

Rendimiento: el marshaling está aproximadamente a la par, **el unmarshaling es significativamente más rápido.** Si algo sale mal, `GOEXPERIMENT=nojsonv2` restaura la implementación antigua, y esa opción de exclusión también está en la lista de eliminación a largo plazo, así que abre una incidencia en lugar de acomodarte.

`jsontext` es la capa de bajo nivel: `Encoder`/`Decoder` recorriendo JSON como `Token`s y `Value`s con una máquina de estados que te mantiene honesto. Recurre a ella cuando escribas un transformador en streaming o un filtro JSON y no quieras materializar valores de Go en absoluto.

### 3.2. Un paquete uuid estándar

Por fin. RFC 9562, en la biblioteca estándar, sin necesidad de `go get`.

```go
import "uuid"

func main() {
	fmt.Println(uuid.NewV4())
	// b97aa695-da08-472c-af81-ff088129019f

	// v7: los 48 bits superiores son una marca de tiempo, así que estos siempre se ordenan por orden de creación
	a, b := uuid.NewV7(), uuid.NewV7()
	fmt.Println(a.Compare(b) < 0) // true

	u, err := uuid.Parse("urn:uuid:0198a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b")
	fmt.Println(u, err)
	// 0198a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b <nil>

	fmt.Println(uuid.Nil(), uuid.Max())
	// 00000000-0000-0000-0000-000000000000 ffffffff-ffff-ffff-ffff-ffffffffffff
}
```

La API completa consta de `New`, `NewV4`, `NewV7`, `Nil`, `Max`, `Parse`, `MustParse` y un tipo: `type UUID [16]byte`. Eso es todo. Puedes leer toda la documentación del paquete mientras se enfría tu café.

Detalles que vale la pena conocer:

- **`UUID` es `[16]byte`**, así que `==` funciona y se puede usar directamente como clave de mapa. Mismo diseño que `google/uuid`.
- **`Nil` y `Max` son funciones, no variables.** Porque una `var Nil UUID` a nivel de paquete es una pistola cargada apuntada a tu pie, y alguien, en algún lugar, terminaría asignándole un valor.
- Los bits aleatorios provienen de un generador **criptográficamente seguro**.
- Implementa `encoding.TextMarshaler`/`TextUnmarshaler`/`TextAppender`, así que encaja directamente en estructuras JSON.
- **`NewV7` es el que quieres para claves primarias de bases de datos.** Al estar ordenado por tiempo, tu índice B-tree deja de fragmentarse, algo para lo que los UUID v4 son notoriamente malos.

Ahora puedes eliminar una dependencia. Si necesitas v1/v3/v5 o opciones de análisis más avanzadas, las bibliotecas de terceros siguen teniendo trabajo que hacer.

### 3.3. crypto/mldsa: firmas post-cuánticas, y son enormes

Go 1.24 nos dio `crypto/mlkem` para el intercambio de claves post-cuántico. Go 1.27 aporta la otra mitad: firmas ML-DSA, estandarizadas como FIPS 204.

```go
sk, _ := mldsa.GenerateKey(mldsa.MLDSA65())
pk := sk.PublicKey()

msg := []byte("release the gophers")
opts := &mldsa.Options{Context: "gosuda.org/blog"}

sig, _ := sk.Sign(rand.Reader, msg, opts)

fmt.Println("private key seed:", len(sk.Bytes()), "bytes")  // 32
fmt.Println("public key:", len(pk.Bytes()), "bytes")        // 1952
fmt.Println("signature:", len(sig), "bytes")                // 3309

fmt.Println(mldsa.Verify(pk, msg, sig, opts))
// <nil>
fmt.Println(mldsa.Verify(pk, msg, sig, &mldsa.Options{Context: "other"}))
// mldsa: invalid signature
```

Fíjate en esas cifras. **Una sola firma ocupa 3309 bytes.** Ed25519 ocupa 64. Eso es un aumento de 50 veces, y la clave pública casi llega a 2 KB adicionales. Mete unas cuantas de estas en una cadena de certificados y tu handshake TLS empezará a necesitar su propia estrategia de MTU.

Este es el coste real de la resistencia cuántica hoy, y explica por qué nadie está cambiando todo mañana. Pero ya está en la biblioteca estándar, que es donde quieres que esté _antes_ de necesitarla.

`Options.Context` es separación de dominios: firma con la misma clave para propósitos distintos, usa un contexto diferente para cada uno, y una firma de un contexto no se verificará en otro. El ejemplo anterior muestra exactamente eso: misma clave, mismo mensaje, contexto distinto, rechazado.

`PrivateKey` implementa `crypto.Signer`, así que encaja en las interfaces existentes. `crypto/x509` maneja claves y firmas ML-DSA, y `crypto/tls` admite los esquemas de firma `MLDSA44`/`MLDSA65`/`MLDSA87` en TLS 1.3.

También existe `SignDeterministic`, que omite la aleatoriedad, útil para pruebas y compilaciones reproducibles.

### 3.4. simd: instrucciones vectoriales sin ensamblador

Go 1.26 introdujo `simd/archsimd`, específico de cada arquitectura, como experimento. Go 1.27 añade `simd`, **portable y agnóstico al ancho de vector**. Se activa con `GOEXPERIMENT=simd`.

```go
// dst = a*x + y
func axpy(dst, x, y []float32, a float32) {
	va := simd.BroadcastFloat32s(a)
	w := va.Len()

	i := 0
	for ; i+w <= len(x); i += w {
		vx := simd.LoadFloat32s(x[i:])
		vy := simd.LoadFloat32s(y[i:])
		vx.MulAdd(va, vy).Store(dst[i:])
	}
	// tratar el resto con una carga/almacenamiento parcial: sin bucle escalar de limpieza
	if i < len(x) {
		vx, _ := simd.LoadFloat32sPart(x[i:])
		vy, _ := simd.LoadFloat32sPart(y[i:])
		vx.MulAdd(va, vy).StorePart(dst[i:])
	}
}
```

```bash
$ GOEXPERIMENT=simd go1.27rc3 run ./simddemo
vector bits: 128 emulated: false
[4 7 10 13 16 19 22 25 28 31]
```

La decisión de diseño que importa: **el ancho del vector nunca está codificado de forma fija.** Consultas `va.Len()` en tiempo de ejecución. `VectorBitSize()` te dice el ancho real, `Emulated()` te indica si tienes hardware real o una amable suplantación por software. Mi Mac reportó NEON de 128 bits; una máquina con AVX-512 reporta más; una máquina sin nada reporta emulación y el código sigue ejecutándose.

`LoadFloat32sPart`/`StorePart` merecen una mención especial. La parte más molesta del SIMD escrito a mano es siempre la cola irregular al final del array, y esto la resuelve sin un bucle escalar separado.

Sigue siendo experimental, la API aún es inestable, no lo pongas en producción. Pero el hecho de que esto sea código Go y no ensamblador ni cgo es un avance genuinamente grande.

### 3.5. hash/maphash.Hasher

Una nueva interfaz que describe el contrato entre un tipo de valor y los contenedores basados en hash:

```go
type Hasher[T any] interface {
	Hash(*Hash, T)
	Equal(x, y T) bool
}
```

¿Por qué? El mapa integrado de Go solo acepta claves `comparable`. No puedes usar un slice como clave. No puedes definir "igual ignorando mayúsculas". `Hasher` resuelve ambas cosas a la vez:

```go
type CaseInsensitive struct{}

func (CaseInsensitive) Hash(h *maphash.Hash, s string) {
	h.WriteString(strings.ToLower(s))
}
func (CaseInsensitive) Equal(x, y string) bool {
	return strings.ToLower(x) == strings.ToLower(y)
}

var seed = maphash.MakeSeed()

func hashOf[T any](hr maphash.Hasher[T], v T) uint64 {
	var h maphash.Hash
	h.SetSeed(seed)   // la misma semilla, o nada de esto significa algo
	hr.Hash(&h, v)
	return h.Sum64()
}

fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
// true
```

Para la semántica ordinaria de `==` existe `ComparableHasher[T]`:

```go
hashOf(maphash.ComparableHasher[int]{}, 42)
```

El inconveniente: **`Hasher` es una interfaz, no una estructura de datos.** Todavía no hay una tabla hash estándar ni un filtro de Bloom que la consuma. Es una base para un futuro paquete de contenedores. Hoy la usarías al construir tu propia estructura, o consumirías una implementación existente como `go/types.Hasher` (que te permite usar `types.Type` como clave de mapa, respetando `Identical`).

La gestión de la semilla depende de ti. Si `hashOf` de arriba generara una semilla nueva en cada llamada, los valores idénticos producirían hashes distintos y el ejemplo imprimiría `false`, que es exactamente el error que cometí en mi primer intento. Una semilla por contenedor. (La semilla es aleatoria para frustrar ataques DoS por inundación de hashes, por eso no es simplemente una constante.)

### 3.6. httptest.NewTestServer + synctest.Sleep: el golpe del dormilón

Si solo adoptas una cosa de esta versión, que sea esta.

`testing/synctest` se graduó en Go 1.25 y nos dio un reloj falso para probar código concurrente. Pero en cuanto tu prueba tocaba una red real, la ilusión se derrumbaba. El `httptest.NewTestServer` de Go 1.27 usa una **red falsa en memoria**, así que la burbuja se mantiene intacta. Y `synctest.Sleep` (= `time.Sleep` + `synctest.Wait`) lo completa.

Aquí tienes una prueba de reintento con retroceso exponencial:

```go
func TestRetryWithBackoff(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		var hits int
		srv := httptest.NewTestServer(t, http.HandlerFunc(
			func(w http.ResponseWriter, r *http.Request) {
				hits++
				if hits < 3 {
					w.WriteHeader(http.StatusServiceUnavailable)
					return
				}
				io.WriteString(w, "ok")
			}))

		client := srv.Client()
		start := time.Now()
		var body string
		for attempt := range 5 {
			resp, err := client.Get(srv.URL)
			if err != nil {
				t.Fatal(err)
			}
			b, _ := io.ReadAll(resp.Body)
			resp.Body.Close()
			if resp.StatusCode == http.StatusOK {
				body = string(b)
				break
			}
			synctest.Sleep(time.Duration(1<<attempt) * time.Second)
		}

		if body != "ok" {
			t.Fatalf("got %q", body)
		}
		// el retroceso debe ser exactamente 1s + 2s = 3s
		if elapsed := time.Since(start); elapsed != 3*time.Second {
			t.Fatalf("elapsed = %v, want 3s", elapsed)
		}
		t.Logf("hits=%d elapsed=%v", hits, time.Since(start))
	})
}
```

```
=== RUN   TestRetryWithBackoff
    x_test.go:48: hits=3 elapsed=3s
--- PASS: TestRetryWithBackoff (0.00s)
```

Léelo dos veces. Simuló tres segundos de retroceso contra un servidor HTTP real y terminó en **0.00 segundos**. Y la aserción es `elapsed == 3*time.Second`: _exactamente_ tres segundos, no "al menos tres segundos, más o menos la variación del planificador". Los relojes falsos no tienen variación.

`synctest.Sleep` existe por una razón concreta: si tu prueba duerme la misma duración que el código bajo prueba, no se sabe cuál despierta primero. `synctest.Sleep` duerme _y luego espera_ hasta que todas las demás goroutines de la burbuja estén bloqueadas de forma duradera, así que observas el sistema después de que se haya estabilizado.

Timeouts, reintentos, circuit breakers, limitadores de tasa: cada prueba de cliente HTTP dependiente del tiempo que tengas puede volverse rápida y determinista. Si tu suite de pruebas se sostiene actualmente con `time.Sleep(100 * time.Millisecond)` y esperanza, esta es tu salida.

### 3.7. Cambios en net/http que realmente afectan a producción

Discretos, pero aparecerán en tus métricas.

**Los cuerpos de respuesta HTTP/1 se vacían automáticamente al cerrarse.** El contenido no leído se descarta (hasta un límite conservador) al cerrar el cuerpo, de modo que la conexión puede reutilizarse. Lo que significa que por fin puedes eliminar este conjuro que todo el mundo ha copiado y pegado de la misma respuesta de Stack Overflow desde 2016:

```go
// ya no es necesario
defer func() {
	io.Copy(io.Discard, resp.Body)
	resp.Body.Close()
}()
```

Para la mayoría de los programas esto no hace nada o supone una pequeña mejora. Si empeora las cosas, probablemente estés en el grupo que las notas de la versión describen con cortesía: `Transport.MaxIdleConns` establecido en 0, o un nuevo `Client` por petición, evitando por completo el límite de conexiones inactivas. `Transport.DisableKeepAlives = true` lo tapará, pero el consejo real de las notas de la versión es que "una revisión más profunda probablemente sería beneficiosa", que en lenguaje del equipo de Go significa _tienes problemas más graves_.

**Prioridad de cliente HTTP/2 (RFC 9218).** El servidor ahora respeta las señales de prioridad del cliente. Si preferías la antigua planificación por turnos, usa `Server.DisableClientPriority = true`.

**`Server.MaxHeaderValueCount`.** Limita cuántos valores de cabecera aceptará el servidor, con `DefaultMaxHeaderValueCount` como valor predeterminado. Una puerta más que se cierra a "envía diez mil cabeceras y observa qué pasa".

**ALPN en conexiones proporcionadas por el usuario.** Si tu `net.Conn` implementa `ConnectionState() tls.ConnectionState`, `Transport` y `Server` realizarán la negociación TLS ALPN sobre ella, de modo que los dialers personalizados que pasan por un proxy aún pueden negociar HTTP/2.

### 3.8. Las pequeñas novedades que alegran el día

**`strings.CutLast` / `bytes.CutLast`.** `Cut` divide en el _primer_ separador. No existía una variante "última", así que todos escribíamos a mano `LastIndex` más slicing, y aproximadamente el 30 % de nosotros tuvo un error de un elemento en el primer intento.

```go
name, ext, ok := strings.CutLast("archive.tar.gz", ".")
// archive.tar gz true
```

Ideal para extensiones de archivo y para analizar `host:port`, donde hay que encontrar los dos puntos _finales_, porque las direcciones IPv6 están llenas de ellos.

**`math/big.Int.Divide`.** División con un modo de redondeo explícito, reemplazando el volado de "¿quiero `Quo` o `Div`?":

```go
x, y := big.NewInt(-7), big.NewInt(2)

new(big.Int).Divide(x, y, new(big.Int), big.Trunc)  // q=-3 r=-1
new(big.Int).Divide(x, y, new(big.Int), big.Floor)  // q=-4 r=1
new(big.Int).Divide(x, y, new(big.Int), big.Round)  // q=-4 r=1
new(big.Int).Divide(x, y, new(big.Int), big.Ceil)   // q=-3 r=-1
```

Si trabajas en un dominio donde la regla de redondeo está escrita en una normativa, esto es para ti.

**`url.URL.Clone` y `url.Values.Clone`.** Copias profundas. La antigua copia superficial `*u` compartía el puntero `Userinfo`, lo que producía exactamente el tipo de error que tarda un día en encontrarse y cinco segundos en corregirse.

```go
orig, _ := url.Parse("https://gosuda.org/blog?tag=go&tag=1.27")
clone := orig.Clone()
clone.Host = "example.com"
fmt.Println(orig.Host, clone.Host)  // gosuda.org example.com
```

Los métodos de lectura de **`net.UnixConn`** ahora devuelven `io.EOF` directamente en lugar de envolverlo en `net.OpError`. `err == io.EOF` funciona ahora, como siempre debió funcionar.

**`database/sql.ConvertAssign` y `driver.RowsColumnScanner`.** Funciones para autores de drivers. La primera expone las conversiones de tipo que realiza `Rows.Scan`; la segunda permite a los drivers escanear directamente en destinos del usuario, evitando una asignación intermedia.

**`unicode` 15 → 17.** Dos versiones de un salto. El comportamiento de clasificación y normalización de cadenas puede cambiar sutilmente. Si tienes pruebas que dependen de ello, lo descubrirás.

**`compress/flate` es más rápido**, y **sus bytes de salida pueden diferir de Go 1.26.** Esto se propaga a `archive/zip`, `compress/gzip`, `compress/zlib` e `image/png`. Si tienes pruebas de archivos de referencia que calculan el hash de la salida comprimida, se romperán, y no será una regresión. Compruébalo _antes_ de actualizar, no durante la revisión del incidente.

## 4. Herramientas

### 4.1. go fix incorpora más modernizadores

`go fix` se está convirtiendo silenciosamente en una herramienta de modernización de código. Go 1.27 añade `atomictypes`, `embedlit`, `slicesbackward` y `unsafefuncs`. Usa `-diff` para previsualizar:

```go
package fixdemo

import (
	"sync"
	"sync/atomic"
)

type Base struct{ ID int }
type User struct {
	Base
	Name string
}

func New() User {
	return User{Base: Base{ID: 1}, Name: "gopher"}
}

var counter int64

func Incr() { atomic.AddInt64(&counter, 1) }

func Reverse(s []string) {
	for i := len(s) - 1; i >= 0; i-- {
		_ = s[i]
	}
}

func Spawn(wg *sync.WaitGroup) {
	wg.Add(1)
	go func() {
		defer wg.Done()
	}()
}
```

```bash
$ go1.27rc3 fix -diff ./fixdemo
```

```diff
 import (
+	"slices"
 	"sync"
 	"sync/atomic"
 )

 func New() User {
-	return User{Base: Base{ID: 1}, Name: "gopher"}
+	return User{ID: 1, Name: "gopher"}
 }

-var counter int64
+var counter atomic.Int64

-func Incr() { atomic.AddInt64(&counter, 1) }
+func Incr() { counter.Add(1) }

 func Reverse(s []string) {
-	for i := len(s) - 1; i >= 0; i-- {
-		_ = s[i]
+	for _, v := range slices.Backward(s) {
+		_ = v
 	}
 }

 func Spawn(wg *sync.WaitGroup) {
-	wg.Add(1)
-	go func() {
-		defer wg.Done()
-	}()
+	wg.Go(func() {
+	})
 }
```

Cuatro modernizadores se activaron en un archivo pequeño:

- `embedlit`: reescribe los literales embebidos usando la nueva sintaxis de §1.2
- `atomictypes`: `atomic.AddInt64(&x, 1)` se convierte en `atomic.Int64.Add(1)`, lo que hace imposible el acceso no atómico y corrige silenciosamente errores de alineación en 32 bits que no sabías que tenías
- `slicesbackward`: los bucles hacia atrás se convierten en `slices.Backward`
- `waitgroupgo`: el ritual `Add`/`go`/`Done` se convierte en `wg.Go` (renombrado desde `waitgroup` de 1.26 para evitar ambigüedades)

`go tool fix help` enumera los 26; `go tool fix help <nombre>` explica uno. Además, `fmtappendf` se eliminó "por razones de estilo", que es una forma deliciosamente diplomática de describir lo que sucedió en ese hilo de incidencia.

Ejecutar `go fix ./...` sobre una base de código antigua es una tarde genuinamente satisfactoria. Lee el diff primero, por supuesto.

### 4.2. go test ejecuta stdversion por defecto

Este es el cambio con más probabilidades de interrumpir tu jornada.

`go test` ahora ejecuta la comprobación de vet `stdversion` por defecto, marcando los símbolos de la biblioteca estándar más nuevos de lo que permite la directiva `go` de tu `go.mod`:

```go
// go.mod dice: go 1.24
package sv

import "strings"

func Ext(name string) string {
	_, ext, _ := strings.CutLast(name, ".")  // CutLast es un símbolo de 1.27
	return ext
}
```

```
$ go1.27rc3 test ./...
# sv
./x.go:6:23: strings.CutLast requires go1.27 or later (module is go1.24)
FAIL	sv [build failed]
```

Esto elimina el clásico modo de fallo en el que todo funciona en tu máquina (el toolchain más reciente) y explota en CI o en el entorno más antiguo de un usuario. Si publicas bibliotecas con una directiva `go` conservadora, esto es un regalo.

### 4.3. go test -json incorporó OutputType

Las líneas `"Action":"output"` ahora incluyen un campo opcional `"OutputType"`: `"error"`, `"error-continue"` o `"frame"`.

```
'frame'  '=== RUN   TestFail\n'
None     '    x_test.go:6: hello\n'
'error'  '    x_test.go:7: boom\n'
'frame'  '--- FAIL: TestFail (0.00s)\n'
'frame'  'FAIL\n'
```

La salida de `t.Log` (sin campo), la de `t.Error` (`error`) y las líneas generadas por el framework (`frame`) ahora son distinguibles. Cualquiera que haya escrito una expresión regular para averiguar qué líneas de la salida de pruebas son "reales" puede eliminarla ahora.

### 4.4. Mejoras en go doc

**Sintaxis `package@version`.** Lee la documentación de una versión específica sin añadirla a tu módulo:

```bash
$ go1.27rc3 doc golang.org/x/sync/errgroup@v0.10.0
package errgroup // import "golang.org/x/sync/errgroup"
...
```

Útil para comprobar qué cambió antes de una actualización.

**`-ex` e impresión del código fuente de los ejemplos:**

```bash
$ go1.27rc3 doc -ex strings | grep Example
    func ExampleClone()
    func ExampleCompare()
    func ExampleContains()
    func ExampleCut()
    func ExampleCutPrefix()
    ...

$ go1.27rc3 doc strings.ExampleCut
package main

import (
	"fmt"
	"strings"
)

func main() {
	show := func(s, sep string) {
		before, after, found := strings.Cut(s, sep)
		fmt.Printf("Cut(%q, %q) = %q, %q, %v\n", s, sep, before, after, found)
	}
	show("Gopher", "Go")
	...
}

Output:
Cut("Gopher", "Go") = "", "pher", true
```

El código fuente del ejemplo _y_ la salida esperada, en la terminal, sin abrir una pestaña del navegador que seguirá abierta el próximo jueves.

### 4.5. go mod tidy limpia tus bloques require

Para módulos con `go 1.27` o posterior, `go mod tidy` fusiona los bloques `require` dispersos en como máximo dos (directos e indirectos), conservando los comentarios adjuntos.

```go
// Antes
module tidydemo

go 1.27

require golang.org/x/sync v0.10.0

// networking
require golang.org/x/net v0.33.0

require (
	golang.org/x/sys v0.28.0 // indirect
)

require golang.org/x/text v0.21.0 // indirect
```

```go
// Después: go mod tidy
module tidydemo

go 1.27

require (
	// networking
	golang.org/x/net v0.33.0
	golang.org/x/sync v0.10.0
)
```

Observa que el comentario `// networking` sobrevivió. Esto sobre todo limpia lo que deja la **resolución de conflictos de fusión de Git**, que es donde nacen los bloques `require` sueltos. Si tu equipo tiene más de tres personas añadiendo dependencias, los conflictos de tu `go.mod` van a volverse notablemente más silenciosos.

### 4.6. Varios

- **Se eliminó el soporte de `bzr`.** Si te afecta, de verdad me gustaría conocer tu historia.
- **`go tool trace -http` se vincula a localhost** cuando se le pasa solo un puerto. `-http=:6060` ya no escucha en todas las interfaces. Usa `-http=0.0.0.0:6060` si realmente lo quieres así. Esto ahora coincide con `go tool pprof` y evita el ocasional perfilador público accidental.
- **Los archivos de respuesta (`@file`)** son compatibles con `compile`, `link`, `asm`, `cgo`, `cover` y `pack`, en un formato compatible con GCC. Para sistemas de compilación que superan los límites de longitud de la línea de comandos: hola, Bazel.

## 5. Compilador, enlazador, plataformas

**Compilador.** Los nombres de archivo relativos en las directivas `//line` ahora se resuelven respecto al directorio del archivo que las contiene, igual que `go/scanner`. Relevante si escribes generadores de código.

Los nombres de símbolo de los literales de función (clausuras) también son más simples ahora: el mismo nombre independientemente de la inlinización, y varias instancias del mismo literal pueden compartir código en el binario. Sin cambio funcional, _salvo_: el código que compara la identidad de funciones mediante `reflect.Value.Pointer` verá "igual" con más frecuencia que antes. Esa comparación nunca fue válida, pero si la tienes, ahora empezará a mentirte con más insistencia.

**Enlazador.** Las nuevas opciones `-macos` y `-macsdk` establecen las versiones del sistema operativo y del SDK en el comando de carga `LC_BUILD_VERSION` de macOS.

**Plataformas.**

- **Darwin ahora requiere macOS 13 Ventura o posterior**, como se anunció en la 1.26. Revisa tus runners de CI.
- **PowerPC (`GOOS=linux GOARCH=ppc64`) cambió al ABI ELFv2.** Requiere el kernel de Linux 3.13 o posterior (RHEL7 lo retroportó a 3.10). Cgo, PIE y el enlazado externo ahora son compatibles. Si usas cgo pero necesitas un binario estático puro en Go, establece `CGO_ENABLED=0`.

## 6. Lista de verificación para la actualización

Go 1.27 se toma la compatibilidad en serio, pero revisa estos puntos antes de subir la versión:

- [ ] **Pruebas con archivos de referencia sobre salida comprimida.** `compress/flate` cambió sus codificadores; los bytes de gzip/zip/png pueden diferir.
- [ ] **Pruebas que comparan cadenas de mensajes de error de JSON.** El comportamiento es idéntico; **el texto del error no lo es.**
- [ ] **GODEBUG eliminados en `go.mod`.** `asynctimerchan`, `gotypesalias`, `tlsrsakex`, `tls3des`, `tls10server`, `tlsunsafeekm`, `x509keypairleaf`: estos solo hacen fallar la compilación si están configurados con sus valores _antiguos_.
- [ ] **Infracciones de `stdversion`.** `go test` las detecta ahora. Ejecútalo pronto si mantienes una biblioteca con una directiva `go` conservadora.
- [ ] **Runners de CI con macOS 12 o anterior.** El soporte ha desaparecido.
- [ ] **Pruebas que verifican los nombres de símbolo de literales de función**, y **comparaciones de funciones con `reflect.Value.Pointer`.**
- [ ] **`Transport.MaxIdleConns = 0`** o un `Client` por petición. Combinado con el vaciado automático de cuerpos de respuesta, esto puede volverse más lento.

## Reflexiones finales

Go 1.27 es la versión en la que se saldó de golpe mucho mantenimiento pendiente.

Los métodos genéricos eran la pieza que faltaba del trabajo de genéricos de 1.18. Los selectores de campo en literales de struct cerraron una incidencia de once años. `encoding/json/v2` fue dos años de discusión de propuesta. Y `uuid` llenó un hueco que prácticamente todos los proyectos de Go habían estado parchando con la misma dependencia de terceros durante una década.

Si quieres un orden de prioridades para sacar valor real de esto:

1. **`httptest.NewTestServer` + `synctest.Sleep`**: las pruebas HTTP dependientes del tiempo se vuelven rápidas y deterministas. La mejor relación esfuerzo-beneficio de toda la versión.
2. **El perfil de fugas de goroutines**: expón un endpoint en staging y prepárate para sentirte humillado.
3. **Etiquetas de goroutine en los trazados de pila**: tres líneas de `pprof.Do` en el punto de entrada de tus peticiones convierten tu próximo volcado de pila de las 3 AM en algo legible.
4. **`go fix ./...`**: deja que la herramienta haga la modernización.
5. **`encoding/json/v2`**: sin prisa. Adóptalo una opción a la vez.

Descarga `go1.27rc3` y ejecuta tu suite de pruebas contra él ahora mismo. Todo lo de esa lista de verificación es significativamente más agradable de descubrir un martes por la tarde que durante un incidente.

**Referencias**

- [Notas de la versión de Go 1.27](https://go.dev/doc/go1.27)
- [La especificación del lenguaje de programación Go](https://go.dev/ref/spec)
- [proposal: encoding/json/v2 (#71497)](https://go.dev/issue/71497)
- [proposal: portable SIMD (#78902)](https://go.dev/issue/78902)
- [proposal: hash/maphash.Hasher (#70471)](https://go.dev/issue/70471)
