GoSuda

Go 1.27, o cómo aprendí a dejar de preocuparme y amar los métodos genéricos

By iwanhae views ...

Panorama general de Go 1.27

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.

1go install golang.org/dl/go1.27rc3@latest
2go1.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:

 1type List[E any] []E
 2
 3// F es un parámetro de tipo declarado por el propio método
 4func (l List[E]) Apply[F any](f func(E) F) List[F] {
 5	r := make(List[F], len(l))
 6	for i, x := range l {
 7		r[i] = f(x)
 8	}
 9	return r
10}
11
12func main() {
13	l := List[int]{1, 2, 3}
14	fmt.Println(l.Apply(func(i int) string { return fmt.Sprint(i * 10) }))
15	// [10 20 30]
16}

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

1type Bag struct{ items []any }
2
3func (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:

1type Adder interface{ Add(int) int }
2
3type G struct{}
4
5func (G) Add[T any](v T) T { return v }
6
7var _ Adder = G{}
1cannot use G{} (value of struct type G) as Adder value in variable declaration:
2	G does not implement Adder (wrong type for method Add)
3		have Add[T any](T) T
4		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:

1func (r *Rand) N[Int intType](n Int) Int
1r := rand.New(rand.NewPCG(1, 2))
2fmt.Println(r.N(int32(100)))     // 76
3fmt.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 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.

 1type Object struct{ name, color string }
 2type Point3D struct {
 3	Object
 4	x, y, z float64
 5}
 6type Line struct {
 7	Object
 8	p, q Point3D
 9}
10
11// Go 1.27: usar directamente el campo promovido
12line := 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.

1_ = Line{Object.name: "x"}   // invalid field name Object.name in struct literal
2_ = 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.

1obj := Object{"edge", "black"}
2_ = Line{Object: obj, name: "diagonal"}
3// 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.

1type PtrEmbed struct {
2	*Object
3	z int
4}
5
6_ = PtrEmbed{name: "x"}
7// 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:

 1func double[T ~int | ~float64](v T) T { return v * 2 }
 2
 3type S struct{ f func(int) int }
 4type A [1]func(int) int
 5
 6func main() {
 7	s := S{f: double}          // 1.26: necesitaba double[int]
 8	a := A{double}             // 1.26: necesitaba double[int]
 9
10	c := make(chan func(int) int, 1)
11	c <- double                // 1.26: necesitaba double[int]
12
13	var fn func(float64) float64 = double  // esto siempre funcionó
14
15	fmt.Println(s.f(21), a[0](5), (<-c)(7), fn(1.5))
16	// 42 10 14 3
17}

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:

 1type small struct{ a, b, c, d int }
 2
 3var sink any
 4
 5func BenchmarkAlloc(b *testing.B) {
 6	for b.Loop() {
 7		sink = &small{}
 8	}
 9}
10
11func BenchmarkSlice(b *testing.B) {
12	for b.Loop() {
13		sink = make([]byte, 48)
14	}
15}
1# predeterminado (malloc especializado por tamaño activado)
2BenchmarkAlloc-12     3000000     5.358 ns/op
3BenchmarkSlice-12     3000000    13.58 ns/op
4
5# GOEXPERIMENT=nosizespecializedmalloc
6BenchmarkAlloc-12     3000000     8.722 ns/op
7BenchmarkSlice-12     3000000    20.81 ns/op
8

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:

12413202 bytes  (predeterminado)
22361826 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.

 1func leak() {
 2	ch := make(chan int) // nadie enviará nunca. nadie.
 3	go func() {
 4		<-ch
 5	}()
 6}
 7
 8func main() {
 9	for range 3 {
10		leak()
11	}
12	runtime.GC()
13	runtime.GC()
14	pprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
15}
1goroutineleak profile: total 3
23 @ 0x104de3f18 0x104d7f0b0 0x104d7ec34 0x104e374f4 0x104dea024
3#	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.

 1func handle(ctx context.Context) {
 2	var wg sync.WaitGroup
 3	wg.Go(func() {
 4		buf := make([]byte, 4<<10)
 5		os.Stdout.Write(buf[:runtime.Stack(buf, true)])
 6	})
 7	wg.Wait()
 8}
 9
10func main() {
11	labels := pprof.Labels("request", "GET /orders/42", "tenant", "acme")
12	pprof.Do(context.Background(), labels, handle)
13}
1goroutine 3 [running] {request: "GET /orders/42", tenant: acme}:
2main.handle.func1()
3	/tmp/go127/tblabel/main.go:15 +0x40
4sync.(*WaitGroup).Go.func1()
5	...
6goroutine 1 [sync.WaitGroup.Wait] {request: "GET /orders/42", tenant: acme}:
7...

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:

1# go.mod contiene: godebug asynctimerchan=1
2go: error loading go.mod:
3go.mod:5: removed GODEBUG "asynctimerchan" set to old value "1" (https://go.dev/doc/godebug#go-127)
4
5# go.mod contiene: godebug asynctimerchan=0
6(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 por fin se concretaron.

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

PaqueteFunción
encoding/jsonLa API v1 que conoces. Comportamiento 100 % sin cambios. Ahora implementada sobre v2
encoding/json/v2Procesamiento semántico. Valores de Go ↔ JSON
encoding/json/jsontextProcesamiento 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í:

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

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:

1var m map[string]int
2json.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:

1var m map[string]int
2jsonv2.Unmarshal([]byte(`{"a":1,"a":2}`), &m)
3// jsontext: duplicate object member name "a"
4
5var s string
6jsonv2.Unmarshal([]byte("\"\xff\""), &s)
7// 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:

 1type Config struct {
 2	Name    string   `json:"name"`
 3	Tags    []string `json:"tags,omitzero"`
 4	Timeout int      `json:"timeout,omitzero"`
 5}
 6
 7out, _ := jsonv2.Marshal(Config{Name: "api"})
 8// {"name":"api"}
 9
10// claves de mapa ordenadas + indentación
11out, _ = jsonv2.Marshal(map[string]int{"b": 2, "a": 1},
12	jsonv2.Deterministic(true), jsontext.WithIndent("  "))
13// {
14//   "a": 1,
15//   "b": 2
16// }
17
18// rechazar campos desconocidos: antes requería un Decoder y DisallowUnknownFields()
19var c Config
20err := jsonv2.Unmarshal([]byte(`{"name":"api","nope":1}`), &c,
21	jsonv2.RejectUnknownMembers(true))
22// json: cannot unmarshal JSON string into Go main.Config:
23//   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:

1// mantener la semántica de v1, pero rechazar claves duplicadas como lo hace v2
2jsonv2.Unmarshal(data, &v,
3	json.DefaultOptionsV1(),
4	jsontext.AllowDuplicateNames(false))
5// 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 Tokens y Values 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.

 1import "uuid"
 2
 3func main() {
 4	fmt.Println(uuid.NewV4())
 5	// b97aa695-da08-472c-af81-ff088129019f
 6
 7	// v7: los 48 bits superiores son una marca de tiempo, así que estos siempre se ordenan por orden de creación
 8	a, b := uuid.NewV7(), uuid.NewV7()
 9	fmt.Println(a.Compare(b) < 0) // true
10
11	u, err := uuid.Parse("urn:uuid:0198a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b")
12	fmt.Println(u, err)
13	// 0198a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b <nil>
14
15	fmt.Println(uuid.Nil(), uuid.Max())
16	// 00000000-0000-0000-0000-000000000000 ffffffff-ffff-ffff-ffff-ffffffffffff
17}

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.

 1sk, _ := mldsa.GenerateKey(mldsa.MLDSA65())
 2pk := sk.PublicKey()
 3
 4msg := []byte("release the gophers")
 5opts := &mldsa.Options{Context: "gosuda.org/blog"}
 6
 7sig, _ := sk.Sign(rand.Reader, msg, opts)
 8
 9fmt.Println("private key seed:", len(sk.Bytes()), "bytes")  // 32
10fmt.Println("public key:", len(pk.Bytes()), "bytes")        // 1952
11fmt.Println("signature:", len(sig), "bytes")                // 3309
12
13fmt.Println(mldsa.Verify(pk, msg, sig, opts))
14// <nil>
15fmt.Println(mldsa.Verify(pk, msg, sig, &mldsa.Options{Context: "other"}))
16// 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.

 1// dst = a*x + y
 2func axpy(dst, x, y []float32, a float32) {
 3	va := simd.BroadcastFloat32s(a)
 4	w := va.Len()
 5
 6	i := 0
 7	for ; i+w <= len(x); i += w {
 8		vx := simd.LoadFloat32s(x[i:])
 9		vy := simd.LoadFloat32s(y[i:])
10		vx.MulAdd(va, vy).Store(dst[i:])
11	}
12	// tratar el resto con una carga/almacenamiento parcial: sin bucle escalar de limpieza
13	if i < len(x) {
14		vx, _ := simd.LoadFloat32sPart(x[i:])
15		vy, _ := simd.LoadFloat32sPart(y[i:])
16		vx.MulAdd(va, vy).StorePart(dst[i:])
17	}
18}
1$ GOEXPERIMENT=simd go1.27rc3 run ./simddemo
2vector bits: 128 emulated: false
3[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:

1type Hasher[T any] interface {
2	Hash(*Hash, T)
3	Equal(x, y T) bool
4}

¿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:

 1type CaseInsensitive struct{}
 2
 3func (CaseInsensitive) Hash(h *maphash.Hash, s string) {
 4	h.WriteString(strings.ToLower(s))
 5}
 6func (CaseInsensitive) Equal(x, y string) bool {
 7	return strings.ToLower(x) == strings.ToLower(y)
 8}
 9
10var seed = maphash.MakeSeed()
11
12func hashOf[T any](hr maphash.Hasher[T], v T) uint64 {
13	var h maphash.Hash
14	h.SetSeed(seed)   // la misma semilla, o nada de esto significa algo
15	hr.Hash(&h, v)
16	return h.Sum64()
17}
18
19fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
20// true

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

1hashOf(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:

 1func TestRetryWithBackoff(t *testing.T) {
 2	synctest.Test(t, func(t *testing.T) {
 3		var hits int
 4		srv := httptest.NewTestServer(t, http.HandlerFunc(
 5			func(w http.ResponseWriter, r *http.Request) {
 6				hits++
 7				if hits < 3 {
 8					w.WriteHeader(http.StatusServiceUnavailable)
 9					return
10				}
11				io.WriteString(w, "ok")
12			}))
13
14		client := srv.Client()
15		start := time.Now()
16		var body string
17		for attempt := range 5 {
18			resp, err := client.Get(srv.URL)
19			if err != nil {
20				t.Fatal(err)
21			}
22			b, _ := io.ReadAll(resp.Body)
23			resp.Body.Close()
24			if resp.StatusCode == http.StatusOK {
25				body = string(b)
26				break
27			}
28			synctest.Sleep(time.Duration(1<<attempt) * time.Second)
29		}
30
31		if body != "ok" {
32			t.Fatalf("got %q", body)
33		}
34		// el retroceso debe ser exactamente 1s + 2s = 3s
35		if elapsed := time.Since(start); elapsed != 3*time.Second {
36			t.Fatalf("elapsed = %v, want 3s", elapsed)
37		}
38		t.Logf("hits=%d elapsed=%v", hits, time.Since(start))
39	})
40}
1=== RUN   TestRetryWithBackoff
2    x_test.go:48: hits=3 elapsed=3s
3--- 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:

1// ya no es necesario
2defer func() {
3	io.Copy(io.Discard, resp.Body)
4	resp.Body.Close()
5}()

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.

1name, ext, ok := strings.CutLast("archive.tar.gz", ".")
2// 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?":

1x, y := big.NewInt(-7), big.NewInt(2)
2
3new(big.Int).Divide(x, y, new(big.Int), big.Trunc)  // q=-3 r=-1
4new(big.Int).Divide(x, y, new(big.Int), big.Floor)  // q=-4 r=1
5new(big.Int).Divide(x, y, new(big.Int), big.Round)  // q=-4 r=1
6new(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.

1orig, _ := url.Parse("https://gosuda.org/blog?tag=go&tag=1.27")
2clone := orig.Clone()
3clone.Host = "example.com"
4fmt.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:

 1package fixdemo
 2
 3import (
 4	"sync"
 5	"sync/atomic"
 6)
 7
 8type Base struct{ ID int }
 9type User struct {
10	Base
11	Name string
12}
13
14func New() User {
15	return User{Base: Base{ID: 1}, Name: "gopher"}
16}
17
18var counter int64
19
20func Incr() { atomic.AddInt64(&counter, 1) }
21
22func Reverse(s []string) {
23	for i := len(s) - 1; i >= 0; i-- {
24		_ = s[i]
25	}
26}
27
28func Spawn(wg *sync.WaitGroup) {
29	wg.Add(1)
30	go func() {
31		defer wg.Done()
32	}()
33}
1$ go1.27rc3 fix -diff ./fixdemo
 1 import (
 2+	"slices"
 3 	"sync"
 4 	"sync/atomic"
 5 )
 6
 7 func New() User {
 8-	return User{Base: Base{ID: 1}, Name: "gopher"}
 9+	return User{ID: 1, Name: "gopher"}
10 }
11
12-var counter int64
13+var counter atomic.Int64
14
15-func Incr() { atomic.AddInt64(&counter, 1) }
16+func Incr() { counter.Add(1) }
17
18 func Reverse(s []string) {
19-	for i := len(s) - 1; i >= 0; i-- {
20-		_ = s[i]
21+	for _, v := range slices.Backward(s) {
22+		_ = v
23 	}
24 }
25
26 func Spawn(wg *sync.WaitGroup) {
27-	wg.Add(1)
28-	go func() {
29-		defer wg.Done()
30-	}()
31+	wg.Go(func() {
32+	})
33 }

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:

1// go.mod dice: go 1.24
2package sv
3
4import "strings"
5
6func Ext(name string) string {
7	_, ext, _ := strings.CutLast(name, ".")  // CutLast es un símbolo de 1.27
8	return ext
9}
1$ go1.27rc3 test ./...
2# sv
3./x.go:6:23: strings.CutLast requires go1.27 or later (module is go1.24)
4FAIL	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".

1'frame'  '=== RUN   TestFail\n'
2None     '    x_test.go:6: hello\n'
3'error'  '    x_test.go:7: boom\n'
4'frame'  '--- FAIL: TestFail (0.00s)\n'
5'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:

1$ go1.27rc3 doc golang.org/x/sync/errgroup@v0.10.0
2package errgroup // import "golang.org/x/sync/errgroup"
3...

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

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

 1$ go1.27rc3 doc -ex strings | grep Example
 2    func ExampleClone()
 3    func ExampleCompare()
 4    func ExampleContains()
 5    func ExampleCut()
 6    func ExampleCutPrefix()
 7    ...
 8
 9$ go1.27rc3 doc strings.ExampleCut
10package main
11
12import (
13	"fmt"
14	"strings"
15)
16
17func main() {
18	show := func(s, sep string) {
19		before, after, found := strings.Cut(s, sep)
20		fmt.Printf("Cut(%q, %q) = %q, %q, %v\n", s, sep, before, after, found)
21	}
22	show("Gopher", "Go")
23	...
24}
25
26Output:
27Cut("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.

 1// Antes
 2module tidydemo
 3
 4go 1.27
 5
 6require golang.org/x/sync v0.10.0
 7
 8// networking
 9require golang.org/x/net v0.33.0
10
11require (
12	golang.org/x/sys v0.28.0 // indirect
13)
14
15require golang.org/x/text v0.21.0 // indirect
 1// Después: go mod tidy
 2module tidydemo
 3
 4go 1.27
 5
 6require (
 7	// networking
 8	golang.org/x/net v0.33.0
 9	golang.org/x/sync v0.10.0
10)

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