Volver al blog

Novedades de Go 1.26: guía completa

Guía completa de las novedades de Go 1.26: manejo de errores mejorado con errors.AsType, APIs criptográficas más sencillas, detección de fugas de goroutines, mejoras de rendimiento del GC Green Tea y más. Incluye ejemplos de código ejecutables.

Novedades de Go 1.26: guía completa

Casi todo lo nuevo de Go 1.26 aparece en el código que escribes a diario. Hay menos código repetitivo con los punteros y los tipos de error, una forma nueva de detectar fugas de goroutines y un runtime más rápido que obtienes solo con recompilar.

Índice


Inicialización de punteros más limpia con new(expr)

Si has usado alguna API de Go que marca los campos opcionales con punteros, ya has escrito esto:

example.gogo
port := 8080
config := &Config{Port: &port}  // Can't use &8080 directly!

Go no permite tomar la dirección de un valor literal, así que primero necesitas una variable temporal. Algunos proyectos añaden funciones auxiliares como intPtr(n int) *int, pero eso solo cambia el problema de sitio.

Go 1.26 lo arregla ampliando la función integrada new(). Antes solo aceptaba tipos, así que new(int) te daba un *int que apuntaba a cero. Ahora acepta cualquier expresión:

example.gogo
package main

import "fmt"

type ServerConfig struct {
    Host    string
    Port    *int
    Enabled *bool
}

func main() {
    config := ServerConfig{
        Host:    "localhost",
        Port:    new(8080),
        Enabled: new(true),
    }

    fmt.Printf("Port: %d, Enabled: %t\n", *config.Port, *config.Enabled)
}

Cuando escribes new(8080), Go evalúa la expresión, reserva memoria para su tipo (aquí un int), guarda el valor y devuelve un puntero. Sirve cualquier expresión: new(x + y) para un valor calculado, new(time.Now()) para el resultado de una función o new("default") para una cadena.

Donde más lo vas a usar es con APIs JSON, mensajes Protobuf y cualquier otra struct en la que un campo puntero separa «opcional, con valor» de «no proporcionado».

Comprobación de errores con tipado seguro mediante errors.AsType

Para sacar un tipo de error concreto de un error envuelto, hasta ahora usabas errors.As():

example.gogo
var appErr *AppError
if errors.As(err, &appErr) {
    fmt.Printf("Code: %d\n", appErr.Code)
}

Funciona, pero es torpe. La variable se filtra al ámbito exterior aunque solo la usas dentro del bloque if. Pasas &appErr cuando appErr ya es un puntero, y eso confunde a más de uno. Además, errors.As usa reflexión por debajo.

Go 1.26 añade errors.AsType[T](), una función genérica que hace lo mismo con menos ceremonia:

example.gogo
package main

import (
    "errors"
    "fmt"
)

type AppError struct {
    Code    int
    Message string
}

func (e *AppError) Error() string {
    return fmt.Sprintf("app error %d: %s", e.Code, e.Message)
}

func main() {
    err := &AppError{Code: 404, Message: "not found"}

    if appErr, ok := errors.AsType[*AppError](err); ok {
        fmt.Printf("Code: %d, Message: %s\n", appErr.Code, appErr.Message)
    }

    // Works with wrapped errors too
    wrappedErr := fmt.Errorf("operation failed: %w", err)
    if appErr, ok := errors.AsType[*AppError](wrappedErr); ok {
        fmt.Printf("Unwrapped code: %d\n", appErr.Code)
    }
}

El parámetro de tipo [*AppError] le dice al compilador exactamente qué buscas, y el resultado queda limitado al bloque if. No hay puntero a puntero. Como el tipo se conoce en tiempo de compilación, tampoco hay reflexión, así que se ejecuta más rápido en las rutas críticas de manejo de errores.

Restricciones genéricas autorreferenciales

Go 1.26 añade soporte limitado para restricciones de tipo recursivas. Gracias a eso, algunos patrones se pueden expresar por primera vez, como un tipo que se compara con otros valores del mismo tipo:

example.gogo
package main

import "fmt"

type Comparable[T any] interface {
    CompareTo(other T) int
}

type Integer int

func (i Integer) CompareTo(other Integer) int {
    if i < other {
        return -1
    } else if i > other {
        return 1
    }
    return 0
}

func Max[T Comparable[T]](a, b T) T {
    if a.CompareTo(b) > 0 {
        return a
    }
    return b
}

func main() {
    result := Max(Integer(5), Integer(3))
    fmt.Printf("Max: %d\n", result)
}

La restricción Comparable[T] hace referencia a su propio parámetro de tipo, y Max[T Comparable[T]] exige que T pueda compararse consigo mismo. Antes de Go 1.26, este tipo de restricción genérica autorreferencial no compilaba. Ahora puedes construir APIs fluidas con encadenamiento de métodos, árboles cuyos nodos hacen referencia a su propio tipo y builders con tipado seguro que devuelven el tipo correcto en cada paso.

APIs de criptografía más sencillas

Casi cualquier desarrollador de Go ha escrito esta línea:

example.gogo
key, err := rsa.GenerateKey(rand.Reader, 2048)

El argumento rand.Reader es un resto histórico. Las primeras versiones de Go permitían fuentes aleatorias personalizadas para las pruebas. En la práctica siempre deberías pasar crypto/rand.Reader, y pasar cualquier otra cosa, como math/rand, abre un agujero de seguridad.

Go 1.26 convierte la opción segura en la opción por defecto. Pasa nil y la función usará crypto/rand.Reader internamente:

example.gogo
package main

import (
    "crypto/rand"
    "encoding/hex"
    "fmt"
)

func main() {
    key := make([]byte, 32)
    rand.Read(key)

    fmt.Printf("Generated key: %s\n", hex.EncodeToString(key))
}

Esto cubre la generación de claves en todos los paquetes criptográficos: rsa.GenerateKey(nil, bits), ecdsa.GenerateKey(curve, nil) y ecdh.GenerateKey(curve, nil). El único motivo que queda para pasar tu propio reader es una prueba que necesite una salida determinista.

HPKE para cifrado moderno

El nuevo paquete crypto/hpke implementa el RFC 9180, un estándar moderno de cifrado híbrido de clave pública. RSA por sí solo es lento y solo puede cifrar mensajes pequeños. La solución habitual es cifrar con RSA una clave simétrica aleatoria y después usar esa clave para los datos reales. Funciona, pero hay que implementarlo con cuidado.

HPKE estandariza ese patrón con algoritmos modernos. Piensa en una caja con candado. El remitente fabrica un candado y una llave de un solo uso, cierra el mensaje dentro de la caja y la envía junto con unas instrucciones que solo la clave privada del destinatario puede usar para recrear esa llave.

example.gogo
suite := hpke.NewSuite(
    hpke.DHKEM_X25519,
    hpke.KDF_HKDF_SHA256,
    hpke.AEAD_ChaCha20Poly1305,
)

publicKey, privateKey, _ := suite.GenerateKeyPair(nil)

sender, _ := suite.NewSender(publicKey, nil)
ciphertext, _ := sender.Seal(plaintext, nil)
encapsulated := sender.EncapsulatedKey()

recipient, _ := suite.NewRecipient(privateKey, encapsulated)
decrypted, _ := recipient.Open(ciphertext, nil)

HPKE usa claves de 256 bits en lugar de las claves de 2048 a 4096 bits de RSA, admite mensajes de cualquier tamaño y es mucho más rápido.

Cómo detectar fugas de goroutines

Las fugas de goroutines pasan desapercibidas con facilidad. Una fuga de memoria acaba tumbando el programa. Una fuga de goroutines solo lo vuelve más lento con el tiempo, porque miles de goroutines bloqueadas se van acumulando y retienen memoria y CPU sin hacer nada útil.

Esta es una forma habitual de provocar una:

example.gogo
func handleRequest(ctx context.Context) error {
    results := make(chan Result)

    go func() {
        result := expensiveOperation()
        results <- result  // Blocks forever if context cancels
    }()

    select {
    case <-ctx.Done():
        return ctx.Err()  // Goroutine still blocked on send
    case r := <-results:
        return processResult(r)
    }
}

Si el context se cancela antes de que expensiveOperation() devuelva su resultado, la goroutine se queda bloqueada para siempre intentando enviar por un channel sin búfer que nadie va a leer.

Go 1.26 añade un perfil goroutineleak y una opción go test -goroutineleak que las detectan automáticamente. También puedes ver una fuga contando las goroutines antes y después:

example.gogo
package main

import (
    "fmt"
    "runtime"
    "time"
)

func createLeak() {
    ch := make(chan int)
    go func() {
        <-ch  // Blocks forever - no sender
    }()
}

func main() {
    before := runtime.NumGoroutine()
    createLeak()
    time.Sleep(50 * time.Millisecond)
    after := runtime.NumGoroutine()

    fmt.Printf("Before: %d, After: %d\n", before, after)
    if after > before {
        fmt.Println("Goroutine leak detected!")
    }
}

La solución tiene tres partes: un channel con búfer para que el envío no se bloquee, un select dentro de la goroutine para que pueda salir cuando se cancela el context y una comprobación del context en la función que llama:

example.gogo
package main

import (
    "context"
    "fmt"
    "time"
)

func expensiveWork() string {
    time.Sleep(50 * time.Millisecond)
    return "completed"
}

func safeHandler(ctx context.Context) (string, error) {
    ch := make(chan string, 1)

    go func() {
        result := expensiveWork()
        select {
        case ch <- result:
        case <-ctx.Done():
            return
        }
    }()

    select {
    case <-ctx.Done():
        return "", ctx.Err()
    case result := <-ch:
        return result, nil
    }
}

func main() {
    ctx1 := context.Background()
    result1, err1 := safeHandler(ctx1)
    fmt.Printf("Normal: %s, err: %v\n", result1, err1)

    ctx2, cancel := context.WithCancel(context.Background())
    cancel()
    result2, err2 := safeHandler(ctx2)
    fmt.Printf("Canceled: %s, err: %v\n", result2, err2)
}

Con el channel con búfer, el envío no puede bloquearse aunque nadie reciba. El select le da a la goroutine una salida cuando se cancela el context. Tanto si el trabajo sale bien como si se cancela, la goroutine termina.

Go 1.26 también añade métricas de goroutines a runtime/metrics, así que tus paneles de producción pueden seguir el número total de goroutines, las bloqueadas y las listas para ejecutarse.

Mejoras en la biblioteca estándar

Buffer.Peek

Leer de un bytes.Buffer consume los datos. Si querías mirar antes, por ejemplo para comprobar el tipo de un mensaje antes de elegir un parser o para validar un formato antes de leerlo, tenías que copiar el búfer.

Go 1.26 añade Peek():

example.gogo
package main

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBufferString("Hello, World!")

    peeked := buf.Bytes()[:5]
    fmt.Printf("Peeked: %s\n", peeked)

    fmt.Printf("Full content: %s\n", buf.String())
    fmt.Printf("Length: %d\n", buf.Len())
}

Comparación de prefijos IP

Ordenar prefijos IP obligaba a comparar bytes a mano. Ahora netip.Prefix tiene un método Compare() que se pasa tal cual a slices.SortFunc:

example.gogo
package main

import (
    "fmt"
    "net/netip"
    "slices"
)

func main() {
    prefixes := []netip.Prefix{
        netip.MustParsePrefix("192.168.0.0/16"),
        netip.MustParsePrefix("10.0.0.0/8"),
        netip.MustParsePrefix("172.16.0.0/12"),
    }

    slices.SortFunc(prefixes, func(a, b netip.Prefix) int {
        return a.Compare(b)
    })

    for _, p := range prefixes {
        fmt.Println(p)
    }
}

Causa de cancelación en contextos de señal

Si usas signal.NotifyContext() para un apagado ordenado, ahora puedes saber qué señal provocó la cancelación:

example.gogo
ctx, stop := signal.NotifyContext(
    context.Background(),
    os.Interrupt,
    syscall.SIGTERM,
)
defer stop()

<-ctx.Done()

if cause := context.Cause(ctx); cause != nil {
    switch cause {
    case os.Interrupt:
        log.Println("User interrupted (Ctrl+C)")
    case syscall.SIGTERM:
        log.Println("Graceful shutdown requested")
    }
}

Pruebas y logging

slog.MultiHandler

Los servicios en producción suelen escribir logs en más de un sitio: JSON a un archivo para la agregación de logs, texto a stdout para depurar y errores a un servicio de supervisión. slog.MultiHandler hace ese fan-out por ti, así que no tienes que escribirlo tú:

example.gogo
package main

import (
    "log/slog"
    "os"
)

func main() {
    jsonHandler := slog.NewJSONHandler(os.Stdout, nil)
    logger := slog.New(jsonHandler)
    logger.Info("Server started", "port", 8080)
}

Cada handler filtra según su propio nivel. El archivo puede recibirlo todo con nivel debug mientras la consola solo recibe advertencias y errores.

Directorios de artefactos de pruebas

Las pruebas de integración suelen escribir archivos de salida como capturas de pantalla, volcados SQL e informes generados. Antes acababan desperdigados por /tmp o por cualquier otro directorio, y nadie los limpiaba.

t.ArtifactDir() les da una ubicación estándar. Se limpia cuando la prueba pasa y se conserva cuando falla:

example.gogo
func TestGenerateReport(t *testing.T) {
    artifactDir := t.ArtifactDir()
    reportPath := filepath.Join(artifactDir, "report.txt")
    
    err := os.WriteFile(reportPath, []byte("Test Report\n"), 0644)
    if err != nil {
        t.Fatalf("Failed to write: %v", err)
    }
}

Lo útil es que los archivos se conserven cuando algo falla. Puedes abrir la salida de una prueba fallida sin tener que buscarla.

Mejoras en httptest

Probar código HTTPS que habla con hosts externos obligaba a desactivar la verificación de certificados (mala idea incluso en pruebas) o a montar una configuración de certificados engorrosa. En Go 1.26, httptest.Server.Client() redirige las peticiones a example.com hacia tu servidor de pruebas y se encarga de TLS por ti:

example.gogo
func TestExternalAPIClient(t *testing.T) {
    server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{"status": "ok"}`))
    }))
    defer server.Close()

    client := server.Client()
    resp, err := client.Get("https://example.com/api/status")
    // Request goes to test server, not real example.com
}

Rendimiento: el GC Green Tea

Ninguna de estas mejoras requiere cambios en el código. Recompila con Go 1.26 y tu programa irá más rápido.

El nuevo recolector de basura Green Tea reduce la latencia P99 entre un 30 y un 40 % y la sobrecarga de memoria entre un 10 y un 15 %, y sube el throughput entre un 5 y un 10 %. Los programas que asignan muchos objetos de vida corta son los que más ganan. Las mejoras vienen de planificar mejor el trabajo del GC y de gastar menos en el seguimiento de las asignaciones.

Las llamadas al sistema y las llamadas cgo son alrededor de un 30 % más rápidas porque el runtime eliminó el estado de procesador _Psyscall, que añadía sobrecarga a cada syscall. Asignar objetos pequeños (de 1 a 512 bytes) también es alrededor de un 30 % más rápido, gracias a rutinas de asignación especializadas que usan tablas de salto en lugar de rutas genéricas.

Algunas funciones de la biblioteca estándar también se han vuelto más rápidas. fmt.Errorf lo es alrededor de un 30 % e io.ReadAll un 28 %. En pruebas en producción, un servidor de API HTTP que atendía 10.000 peticiones por segundo vio cómo su latencia P99 bajaba de 45 ms a 32 ms.

Novedades del runtime

Conexiones de red que respetan el context

El paquete net ahora aplica los deadlines del context a la resolución DNS además de a la propia conexión:

example.gogo
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

var dialer net.Dialer
conn, err := dialer.DialContext(ctx, "tcp", "example.com:443")
// Timeout now covers DNS + connect time combined

Protección de datos sensibles

El paquete experimental runtime/secret ayuda a mantener los datos sensibles fuera de los volcados de memoria:

example.gogo
secretKey := secret.New([]byte("sk_live_secret"))
defer secretKey.Destroy()

apiKey := secretKey.Bytes()
makeAuthenticatedRequest(apiKey)

Destroy() borra la memoria, lo que acorta el tiempo durante el que un secreto puede aparecer en un core dump o en un archivo de swap.

Operaciones SIMD

El nuevo paquete simd/archsimd te da las instrucciones vectoriales del hardware en amd64:

example.gogo
func vectorAdd(a, b [4]float32) [4]float32 {
    va := archsimd.Float32x4FromArray(a)
    vb := archsimd.Float32x4FromArray(b)
    return va.Add(vb).ToArray()
}

SIMD procesa varios valores por instrucción, lo que da entre 4 y 16 veces más throughput en trabajo numérico de procesamiento de imagen, códecs de audio y video y computación científica. Depende de la plataforma, así que escribe una alternativa escalar si tu código tiene que ejecutarse en otras arquitecturas.

Cómo migrar tu código

Go 1.26 mejora go fix para que pueda automatizar las migraciones más habituales:

example.bashbash
go fix -diff ./...   # Preview changes
go fix ./...         # Apply all fixes

Reescribe rsa.GenerateKey(rand.Reader, 2048) como rsa.GenerateKey(nil, 2048) y convierte los patrones con errors.As a errors.AsType cuando puede. Revisa el diff, ejecuta tus pruebas y haz commit.

La lista completa de reescrituras está en todos los modernizadores de go fix 1.26.

Qué significa esto para tu código

Los primeros cambios que vas a tocar son new(expr) y errors.AsType, y go fix se encarga de parte de esa migración. Ejecuta go test -goroutineleak sobre tu código concurrente para encontrar fugas antes de que lleguen a producción. Los generadores de claves criptográficas ahora eligen la fuente aleatoria segura cuando pasas nil. Las mejoras de velocidad del GC y del runtime no te piden nada más que recompilar.

Escribe Go como un ingeniero sénior

Lecciones interactivas en tu navegador. Las primeras son gratis.

Prueba una lección gratisO crea una cuenta gratuita