Go 1.27, o cómo aprendí a dejar de preocuparme y amar los Generic Methods

Go 1.27 llega en agosto de 2026 y, por una vez, el lenguaje mismo ha cambiado. No un cambio del tipo "añadimos una nueva función a slices", sino un cambio real. Los métodos genéricos ya están aquí. Un problema de hace once años ha sido cerrado. encoding/json fue discretamente reemplazado por un nuevo motor mientras el avión estaba en pleno vuelo.
Cada ejemplo a continuación se ejecutó en go1.27rc3 (darwin/arm64). Cada bloque de salida es una salida real, copiada y pegada, incluyendo los mensajes de error. Si algo parece extraño, es porque eso es lo que dijo realmente 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 hemos tenido genéricos, y desde Go 1.18 hemos tenido "La Conversación". Es así:
"Déjame escribir un método
Mapen mi tipo slice..."
method must have no type parameters"...está bien. 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í que cada transformación genérica fue exiliada al ámbito del paquete, donde se almacenaba junto a otros diecisiete ayudantes flotantes llamados con alguna variación de MapSlice.
Go 1.27 soluciona 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 simple 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 que refactorices toda tu base de código esta tarde, existen dos restricciones, y son más importantes de lo que parecen:
- Los métodos de interfaz no pueden declarar parámetros de tipo.
- Los métodos genéricos no pueden implementar métodos de interfaz.
Esa segunda es la que te afectará:
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
Por lo tanto, los métodos genéricos y el despacho dinámico no se mezclan. Esto no es porque el equipo de Go sea tacaño; es porque un método genérico tiene infinitas instanciaciones, y una tabla de métodos de interfaz debe ser finita y conocida en tiempo de compilación. No puedes 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 API de utilidad. Cualquier cosa que deba ocultarse detrás de una interfaz todavía necesita el enfoque antiguo.
La biblioteca estándar ya sacó provecho. El Rand de math/rand/v2 obtuvo 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 el rand.N a nivel de paquete era genérico, lo que significaba usar la fuente global. Ahora tu propio *Rand con semilla obtiene la misma comodidad. Algo pequeño. Algo agradable.
1.2. Selectores de campos en literales de estructura, o: el problema #9859 finalmente descansa
La incrustación (embedding) te da u.ID. La incrustación no te da User{ID: 1}. En cambio, te da User{Base: Base{ID: 1}}, que es la forma que tiene Go de preguntar si realmente querías decir eso.
El problema #9859 se presentó en 2015. Ahora ha sido cerrado. En algún lugar, un gopher que ha cambiado de carrera dos veces desde entonces 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: usa el campo promovido directamente
12line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}
name no es un campo de Line. Es un campo del Object incrustado. Anteriormente escribirías Line{Object: Object{name: "diagonal"}, ...}.
Ahora la letra pequeña, que comprobé intentando compilar todo.
(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 lo tanto, la característica no es "las claves funcionan como acceso a campos ahora". Es específicamente "los nombres de campos promovidos implícitamente están permitidos". Ligeramente más estrecho de lo que sugiere el titular.
(b) No puedes especificar un campo incrustado y algo promovido fuera de él al mismo tiempo.
1obj := Object{"edge", "black"}
2_ = Line{Object: obj, name: "diagonal"}
3// cannot specify promoted field name and enclosing embedded field Object
Razonable. Le diste dos instrucciones contradictorias sobre la misma memoria y se negó a adivinar.
(c) La incrustación de punteros no está invitada.
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 la construcción del literal aún no hay ninguno. También es justo.
Buenas noticias: go fix reescribirá tus literales antiguos por ti. Más sobre esto más adelante.
1.3. La inferencia de tipos de funciones se volvió menos arbitraria
La inferencia de tipos para funciones genéricas solía funcionar en algunos lugares y en otros no, sin un principio discernible detrás de cuál era cuál. ¿Asignar a una variable? Bien. ¿Poner la misma función en un campo de estructura? Error del compilador, por favor escribe double[int] como si fuera 2022.
Go 1.27 hace que la inferencia funcione en cada contexto donde el tipo de destino es inequívocamente conocido:
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}
Un double, instanciado como func(int) int en tres lugares y func(float64) float64 en un cuarto, completamente a partir del contexto. Si escribes estructuras de opciones o tablas de controladores llenas de valores de función, un montón 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 asignaciones bajo 80 bytes, y alrededor de un 1% en general para programas con muchas 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# default (size-specialized malloc on)
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
30–35% más rápido en un micro-benchmark que no hace más que asignar en Apple Silicon. Es decir: este es el mejor caso, logrado bajo condiciones de laboratorio por un benchmark diseñado para hacer que el número luzca bien. En un programa real, el GC y el trabajo real enterrarán la mayor parte de eso. La afirmación del ~1% es la honesta.
El costo es el tamaño del binario. Hello World:
12413202 bytes (default)
22361826 bytes (nosizespecializedmalloc)
Alrededor de 50KB, fijo, independientemente de tu programa. Puedes optar por no participar con GOEXPERIMENT=nosizespecializedmalloc, pero esa vía de escape está programada para su eliminación en Go 1.28, así que trátala como una solución temporal para informes de errores más que como un estilo de vida.
2.2. El perfil de fugas de goroutines es real ahora
Experimental en Go 1.26, disponible generalmente en 1.27. El goroutineleakprofile de GOEXPERIMENT ha desaparecido; 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 número de línea que señalan exactamente dónde lo hiciste.
El truco detrás de esto es genuinamente inteligente: reutiliza el análisis de alcanzabilidad del recolector de basura. Si la goroutine G está bloqueada en el primitivo P, y P es inalcanzable desde cualquier goroutine ejecutable (o cualquier cosa 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 prueba.
El mismo diseño te da la limitación gratis: si el canal o mutex es alcanzable a través de una variable global, o a través de una variable local de alguna goroutine que todavía se está ejecutando, el GC aún puede verlo, por lo que el runtime no puede concluir nada. Observa que incluso el ejemplo de juguete anterior necesita dos llamadas a runtime.GC() para informar. No atrapará todo.
Sin embargo, atrapará el clásico "olvidé cancelar el contexto, la goroutine de trabajo vive para siempre", que es la mayoría de las fugas la mayor parte del tiempo.
Si importas net/http/pprof, también está en /debug/pprof/goroutineleak. Conéctalo en el entorno de staging, revísalo semanalmente y horrorízate en silencio.
Este trabajo fue aportado por Vlad Saioc de Uber, quien merece la bebida de su elección.
2.3. Los seguimientos ahora te dicen qué solicitud murió
Para los módulos que declaran Go 1.27 o posterior, las líneas de encabezado de seguimiento ahora incluyen 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 son heredadas por las goroutines hijas. La goroutine 3 nunca estableció una etiqueta. Obtuvo una de su padre.
Así que la próxima vez que la producción se bloquee y alguien envíe SIGQUIT al proceso, en lugar de 4,000 goroutines que parecen idénticas, obtendrás 4,000 goroutines etiquetadas con a qué solicitud y a qué inquilino pertenecen. Tres líneas de pprof.Do en tu punto de entrada de solicitud compran eso.
Dado que las etiquetas pueden contener cosas que preferirías no volcar en stderr, GODEBUG=tracebacklabels=0 lo desactiva, y esa opción de exclusión está explícitamente destinada a permanecer para siempre.
2.4. asynctimerchan se ha ido para siempre
Go 1.23 hizo que los canales de temporizador no tuvieran búfer (síncronos) y ofreció asynctimerchan=1 como una forma de volver al comportamiento anterior. En 1.27, esa configuración se elimina permanentemente. Los canales del paquete time son síncronos, punto, sin negociación.
La parte interesante es la política introducida junto con ella. Un GODEBUG eliminado que se deja en tu go.mod no rompe automáticamente la compilación; solo se rompe si se establece en el valor antiguo:
1# go.mod contains: 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 contains: godebug asynctimerchan=0
6(builds fine)
Lo que significa que las únicas personas a las que se les grita son las que realmente confiaban en el comportamiento eliminado. Todos los que lo establecieron en el valor predeterminado final y se olvidaron de él pueden seguir sin pensar en ello. Esa es una pieza reflexiva de arqueología de API.
3. Biblioteca estándar
3.1. encoding/json/v2: reemplazaron el motor en pleno vuelo
Este es el grande. Más de dos años de discusión de propuestas finalmente aterrizaron.
Ahora hay tres paquetes, y entender la división es gran parte de la batalla:
| Paquete | Trabajo |
|---|---|
encoding/json | La API v1 que conoces. Comportamiento 100% sin cambios. Ahora implementado sobre v2 |
encoding/json/v2 | Procesamiento semántico. Valores de Go ↔ JSON |
encoding/json/jsontext | Procesamiento sintáctico. JSON como una secuencia de tokens |
El titular es que encoding/json fue reconstruido sobre una implementación completamente diferente y se comporta de manera idéntica. Aquí está cómo:
1// Go 1.27's encoding/json.Unmarshal, abridged
2func Unmarshal(data []byte, v any) error {
3 return jsonv2.Unmarshal(data, v, DefaultOptionsV1())
4}
Cada peculiaridad heredada de la v1 se codificó como una opción, se agrupó en DefaultOptionsV1(), y la API v1 siempre aplica el paquete. Lo que significa que esto se comporta igual en 1.26 y 1.27:
1var m map[string]int
2json.Unmarshal([]byte(`{"a":1,"a":2}`), &m) // <nil>, map[a:2]
Sí, la v1 todavía acepta silenciosamente claves duplicadas y toma la última. Siempre lo hizo. Todavía lo hace. Compatibilidad significa compatibilidad también con las partes malas.
La v2, mientras tanto, 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 no válido son rechazados. Ambos son genuinamente peligrosos, no solo descuidados: cuando dos analizadores no están de acuerdo sobre qué clave duplicada gana, obtienes errores de seguridad. El CVE-2017-12635 de CouchDB fue exactamente esto: un cuerpo JSON con dos claves roles, donde el validador leía una y la capa de almacenamiento leía 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// sorted map keys + indentation
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// reject unknown fields — previously required a Decoder and 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 un MarshalJSON escrito a mano ahora son indicadores (flags).
Y la historia de la migración es la mejor parte. Las opciones posteriores ganan, por lo que puedes adoptar la rigurosidad de la v2 comportamiento por comportamiento:
1// keep v1 semantics, but reject duplicate keys like v2 does
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 200k líneas a v2 para dejar de aceptar claves duplicadas. Cambias una opción. Para cualquier cosa 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 también está en la lista para ser eliminada eventualmente, así que presenta un problema en lugar de instalarte.
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 de 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, no requiere go get.
1import "uuid"
2
3func main() {
4 fmt.Println(uuid.NewV4())
5 // b97aa695-da08-472c-af81-ff088129019f
6
7 // v7: top 48 bits are a timestamp, so these always sort in creation order
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}
Toda la API es 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:
UUIDes[16]byte, por lo que==funciona y es directamente utilizable como clave de mapa. Mismo diseño quegoogle/uuid.NilyMaxson funciones, no variables. Porque unavar Nil UUIDa nivel de paquete es un arma cargada apuntando a tu pie, y alguien, en algún lugar, eventualmente la asignaría.- Los bits aleatorios provienen de un generador criptográficamente seguro.
- Implementa
encoding.TextMarshaler/TextUnmarshaler/TextAppender, por lo que se integra directamente en estructuras JSON. NewV7es el que quieres para claves primarias de bases de datos. Ordenado por tiempo significa que tu índice B-tree deja de fragmentarse, algo en 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 elegantes, las bibliotecas de terceros todavía tienen trabajo.
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 trae 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
Mira esos números. Una sola firma es de 3,309 bytes. Ed25519 es de 64. Eso es un aumento de 50x, y la clave pública es de casi 2KB además de eso. Mete algunas de estas en una cadena de certificados y tu handshake TLS comienza a necesitar su propia estrategia MTU.
Este es el costo real de la resistencia cuántica hoy, y es por eso que nadie cambiará todo mañana. Pero está en la biblioteca estándar ahora, que es donde quieres que esté antes de que lo necesites.
Options.Context es separación de dominio: firma con la misma clave para diferentes propósitos, 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 diferente, rechazado.
PrivateKey implementa crypto.Signer, por lo que se integra 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 el simd/archsimd específico de la arquitectura como un experimento. Go 1.27 añade simd: portátil y agnóstico al ancho del vector. Habilítalo 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 // maneja el final con carga/almacenamiento parcial — sin bucle de limpieza escalar
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 elección de diseño que importa: el ancho del vector nunca está codificado. Preguntas a va.Len() en tiempo de ejecución. VectorBitSize() te dice el ancho real, Emulated() te dice si obtuviste hardware real o una imitación de software educada. Mi Mac informó NEON de 128 bits; una máquina AVX-512 informa más; una máquina sin nada informa 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 de la matriz, y esto lo maneja sin un bucle escalar separado.
Todavía experimental, API aún inestable, no lo pongas en producción. Pero el hecho de que esto sea código Go y no ensamblador o cgo es un gran problema genuino.
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 incorporado de Go solo acepta claves comparable. No puedes usar un slice como clave. No puedes definir "igual ignorando mayúsculas/minúsculas". Hasher resuelve ambos 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) // misma semilla, o nada de esto significa nada
15 hr.Hash(&h, v)
16 return h.Sum64()
17}
18
19fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
20// true
Para semántica == ordinaria existe ComparableHasher[T]:
1hashOf(maphash.ComparableHasher[int]{}, 42)
La trampa: Hasher es una interfaz, no una estructura de datos. Todavía no existe una tabla hash estándar o un filtro de Bloom que lo consuma. Esto es trabajo preliminar para un futuro paquete de contenedores. Hoy lo usarías cuando construyas tu propia estructura, o consumas 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 anterior hiciera una nueva semilla en cada llamada, los valores idénticos generarían hashes diferentes y el ejemplo imprimiría false, que es exactamente el error que escribí en mi primer intento. Una semilla por contenedor. (La semilla se aleatoriza para derrotar los ataques de DoS por inundación de hash, razón por la cual no es solo una constante.)
3.6. httptest.NewTestServer + synctest.Sleep: el éxito inesperado
Si solo adoptas una cosa de esta versión, haz que sea esta.
testing/synctest se graduó en Go 1.25 y nos dio un reloj falso para probar código concurrente. Excepto que en el momento en que tu prueba tocaba una red real, la ilusión colapsaba. El httptest.NewTestServer de Go 1.27 usa una red falsa en memoria, por lo que la burbuja permanece intacta. Y synctest.Sleep (= time.Sleep + synctest.Wait) lo completa.
Aquí hay 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 // backoff should be exactly 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)
Lee eso dos veces. Simuló tres segundos de retroceso contra un servidor HTTP real, y terminó en 0.00 segundos. Y la afirmación es elapsed == 3*time.Second, exactamente tres segundos, no "al menos tres segundos, más o menos el jitter del programador". Los relojes falsos no tienen jitter.
synctest.Sleep existe por una razón específica: si tu prueba duerme por la misma duración que el código bajo prueba, quién se despierta primero es una incógnita. synctest.Sleep duerme y luego espera hasta que todas las demás goroutines en la burbuja estén bloqueadas de forma duradera, por lo que observas el sistema después de que se haya asentado.
Tiempos de espera, reintentos, disyuntores, limitadores de tasa: cada prueba de cliente HTTP dependiente del tiempo que poseas puede volverse rápida y determinista. Si tu suite de pruebas actualmente se mantiene unida con time.Sleep(100 * time.Millisecond) y esperanza, esta es tu salida.
3.7. Cambios en net/http que realmente afectan la producción
Silenciosos, pero aparecerán en tus métricas.
Los cuerpos de respuesta HTTP/1 se drenan automáticamente al cerrar. El contenido no leído ahora se drena (hasta un límite conservador) cuando cierras el cuerpo, por lo que la conexión puede reutilizarse. Lo que significa que finalmente puedes eliminar este encantamiento que todos han copiado y pegado de la misma respuesta de Stack Overflow desde 2016:
1// no longer necessary
2defer func() {
3 io.Copy(io.Discard, resp.Body)
4 resp.Body.Close()
5}()
Para la mayoría de los programas, esto es una operación nula o una pequeña victoria. Si empeora las cosas, probablemente estés en el grupo que las notas de la versión describen cortésmente: Transport.MaxIdleConns establecido en 0, o un nuevo Client por solicitud, pasando por alto el límite de conexión inactiva por completo. Transport.DisableKeepAlives = true lo encubrirá, pero el consejo real de las notas de la versión es que "una mirada más profunda probablemente sería beneficiosa", que es el lenguaje del equipo de Go para tienes problemas mayores.
Prioridad de cliente HTTP/2 (RFC 9218). El servidor ahora respeta las señales de prioridad del cliente. Si prefieres la programación round-robin antigua, Server.DisableClientPriority = true.
Server.MaxHeaderValueCount. Limita cuántos valores de encabezado aceptará el servidor, por defecto DefaultMaxHeaderValueCount. Una puerta más cerrada a "envía diez mil encabezados y mira 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 en ella, por lo que los marcadores personalizados que pasan a través de un proxy aún pueden negociar HTTP/2.
3.8. Las pequeñas cosas que generan alegría
strings.CutLast / bytes.CutLast. Cut divide en el primer separador. No había variante "last", así que todos escribieron manualmente LastIndex más el corte, y aproximadamente el 30% de nosotros cometimos un error de "off-by-one" en el primer intento.
1name, ext, ok := strings.CutLast("archive.tar.gz", ".")
2// archive.tar gz true
Genial para extensiones de archivo y para analizar host:port, donde debes encontrar los últimos dos puntos, porque las direcciones IPv6 están llenas de ellos.
math/big.Int.Divide. División con un modo de redondeo explícito, reemplazando el lanzamiento de moneda "¿es Quo o Div lo que quiero?":
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 un reglamento, 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 lleva un día encontrar y cinco segundos arreglar.
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
net.UnixConn: los métodos de lectura ahora devuelven io.EOF directamente en lugar de envolverlo en net.OpError. err == io.EOF funciona ahora, como siempre debería haberlo hecho.
database/sql.ConvertAssign y driver.RowsColumnScanner. Características para autores de controladores. El primero expone las conversiones de tipo que realiza Rows.Scan; el segundo permite a los controladores escanear directamente a los destinos del usuario, omitiendo una asignación intermedia.
unicode 15 → 17. Dos versiones en un salto. La clasificación de cadenas y el comportamiento de normalización pueden cambiar sutilmente. Si tienes pruebas que dependen de ello, lo descubrirás.
compress/flate se volvió más rápido — y sus bytes de salida pueden diferir de Go 1.26. Esto se traslada a archive/zip, compress/gzip, compress/zlib e image/png. Si tienes pruebas de archivos dorados que hacen hash a la salida comprimida, se van a romper, y no será una regresión. Verifica esto antes de actualizar, no durante la revisión del incidente.
4. Toolchain
4.1. go fix obtuvo más modernizadores
go fix se está convirtiendo discretamente 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 ejecutaron en un archivo pequeño:
embedlit— reescribe literales incrustados usando la nueva sintaxis de §1.2atomictypes—atomic.AddInt64(&x, 1)se convierte enatomic.Int64.Add(1), lo que hace imposible el acceso no atómico y corrige silenciosamente errores de alineación de 32 bits que no sabías que teníasslicesbackward— los bucles hacia atrás se convierten enslices.Backwardwaitgroupgo— el ritualAdd/go/Donese convierte enwg.Go(renombrado delwaitgroupde 1.26 para evitar ambigüedades)
go tool fix help enumera los 26; go tool fix help <name> explica uno. Además, fmtappendf fue eliminado "debido a preocupaciones estilísticas", que es una forma bellamente diplomática de describir lo que sea que haya sucedido en ese hilo de problemas.
Ejecutar go fix ./... en una base de código antigua es una tarde genuinamente satisfactoria. Lee el diff primero, obviamente.
4.2. go test ejecuta stdversion por defecto
Este es el cambio con más probabilidades de interrumpir tu día.
go test ahora ejecuta la verificación vet stdversion por defecto, marcando símbolos de la biblioteca estándar más nuevos de lo que permite tu directiva go en go.mod:
1// go.mod says: go 1.24
2package sv
3
4import "strings"
5
6func Ext(name string) string {
7 _, ext, _ := strings.CutLast(name, ".") // CutLast is a 1.27 symbol
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 modo de falla clásico donde todo funciona en tu máquina (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 obtuvo OutputType
Las líneas "Action":"output" ahora llevan 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 salida 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 salida de prueba son "reales" ahora puede eliminarla.
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 de fuente de ejemplo:
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
Fuente de ejemplo y 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 en go 1.27 o posterior, go mod tidy fusiona bloques require dispersos en un máximo de dos (directos e indirectos), preservando los comentarios adjuntos.
1// Before
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// After: 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)
Ten en cuenta que el comentario // networking sobrevivió. Esto limpia principalmente después de la resolución de conflictos de fusión de Git, que es donde nacen los bloques require extraviados. Si tu equipo tiene más de tres personas añadiendo dependencias, tus conflictos en go.mod están a punto de volverse notablemente más silenciosos.
4.6. Varios
- Soporte
bzreliminado. Si esto te afecta, genuinamente me gustaría escuchar la historia. go tool trace -httpse vincula a localhost cuando solo se le da un puerto.-http=:6060ya no escucha en todas las interfaces. Usa-http=0.0.0.0:6060si eso es lo que querías. Esto ahora coincide congo tool pprofy evita el perfilador público accidental ocasional.- Archivos de respuesta (
@file) son compatibles concompile,link,asm,cgo,coverypack, 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, puertos
Compilador. Los nombres de archivo relativos en las directivas //line ahora se resuelven con respecto al directorio del archivo que los contiene, coincidiendo con go/scanner. Relevante si escribes generadores de código.
Los nombres de símbolos de literales de función (cierre) también son más simples ahora: el mismo nombre independientemente de la inserción (inlining), y múltiples instancias del mismo literal pueden compartir código en el binario. Sin cambios funcionales, excepto: el código que compara la identidad de la función a través de reflect.Value.Pointer verá "igual" más a menudo que antes. Esa comparación nunca fue válida, pero si la tienes, ahora es cuando empieza a mentirte más fuerte.
Enlazador. Las nuevas opciones -macos y -macsdk establecen las versiones de SO y SDK en el comando de carga LC_BUILD_VERSION de macOS.
Puertos.
- Darwin ahora requiere macOS 13 Ventura o posterior, como se anunció en 1.26. Ve a revisar tus ejecutores de CI.
- PowerPC (
GOOS=linux GOARCH=ppc64) cambió a la ABI ELFv2. Requiere kernel de Linux 3.13+ (RHEL7 retroportado a 3.10). Cgo, PIE y enlace externo ahora son compatibles. Si usas cgo pero necesitas un binario estático puro de Go, estableceCGO_ENABLED=0.
6. Lista de verificación de actualización
Go 1.27 se toma en serio la compatibilidad, pero verifica esto antes de aumentar la versión:
- Pruebas de archivos dorados en salida comprimida.
compress/flatecambió los codificadores; los bytes de gzip/zip/png pueden diferir. - Pruebas que coinciden con cadenas de mensajes de error JSON. El comportamiento es idéntico; el texto del error no lo es.
- GODEBUGs eliminados en
go.mod.asynctimerchan,gotypesalias,tlsrsakex,tls3des,tls10server,tlsunsafeekm,x509keypairleaf: estos fallan la compilación solo si se establecen en sus valores antiguos. - Violaciones de
stdversion.go testcaptura esto ahora. Ejecútalo temprano si mantienes una biblioteca con una directivagoconservadora. - Ejecutores de CI de macOS 12 o anteriores. El soporte ha desaparecido.
- Pruebas que afirman sobre nombres de símbolos de literales de función, y comparaciones de funciones
reflect.Value.Pointer. -
Transport.MaxIdleConns = 0o unClientpor solicitud. Combinado con cuerpos de respuesta de drenaje automático, esto puede volverse más lento.
Reflexiones finales
Go 1.27 es la versión donde mucho mantenimiento diferido venció a la vez.
Los métodos genéricos fueron la pieza que faltaba del trabajo de genéricos de 1.18. Los selectores de campo de literales de estructura cerraron un problema de once años. encoding/json/v2 fueron dos años de discusión de propuestas. Y uuid llenó un agujero que básicamente todos los proyectos de Go habían estado parcheando con la misma dependencia de terceros durante una década.
Si quieres un orden de prioridad para obtener valor real de esto:
httptest.NewTestServer+synctest.Sleep— las pruebas HTTP dependientes del tiempo se vuelven rápidas y deterministas. La mejor relación esfuerzo-recompensa en toda la versión.- El perfil de fugas de goroutine — expón un endpoint en staging y prepárate para ser humillado.
- Etiquetas de goroutine de seguimiento — tres líneas de
pprof.Doen tu punto de entrada de solicitud convierten tu próximo volcado de pila de las 3 AM en algo legible. go fix ./...— deja que la herramienta haga la modernización.encoding/json/v2— sin prisas. Adóptalo una opción a la vez.
Toma go1.27rc3 y ejecuta tu suite de pruebas contra él ahora. Todo en esa lista de verificación es significativamente más placentero de descubrir un martes por la tarde que durante un incidente.
Referencias