Voltar ao blog

Novidades do Go 1.26: guia completo

Guia completo das novidades do Go 1.26: tratamento de erros melhorado com errors.AsType, APIs de criptografia mais simples, deteção de fugas de goroutines, ganhos de desempenho com o Green Tea GC e muito mais. Inclui exemplos de código executáveis.

Novidades do Go 1.26: guia completo

Quase tudo no Go 1.26 aparece no código que escreves todos os dias. Há menos código repetitivo (boilerplate) à volta de ponteiros e tipos de erro, uma forma nova de apanhar fugas de goroutines e um runtime mais rápido que ganhas só por recompilar.

Índice


Inicialização de ponteiros mais limpa com new(expr)

Se já usaste uma API em Go que marca os campos opcionais com ponteiros, já escreveste isto:

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

O Go não te deixa obter o endereço de um valor literal, por isso precisas primeiro de uma variável temporária. Algumas bases de código acrescentam funções auxiliares como intPtr(n int) *int, mas isso só muda o problema de sítio.

O Go 1.26 resolve isto ao alargar a função integrada new(). Antes só aceitava tipos, e new(int) dava-te um *int a apontar para zero. Agora aceita qualquer expressão:

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)
}

Quando escreves new(8080), o Go avalia a expressão, reserva memória para o seu tipo (aqui, um int), guarda o valor e devolve um ponteiro. Funciona com qualquer expressão: new(x + y) para um valor calculado, new(time.Now()) para o resultado de uma função ou new("default") para uma string.

Vais usá-lo sobretudo com APIs JSON, mensagens Protobuf e qualquer outra struct em que um campo ponteiro separa «opcional, com valor» de «não fornecido».

Verificação de erros com segurança de tipos via errors.AsType

Para tirar um tipo de erro específico de dentro de um erro encapsulado, tens usado errors.As():

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

Funciona, mas é desajeitado. A variável fica visível no âmbito exterior, embora só a uses dentro do bloco if. Passas &appErr quando appErr já é um ponteiro, o que baralha muita gente. E o errors.As depende de reflexão por baixo.

O Go 1.26 acrescenta errors.AsType[T](), uma função genérica que faz o mesmo com menos cerimónia:

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)
    }
}

O parâmetro de tipo [*AppError] diz ao compilador exatamente o que procuras, e o resultado fica limitado ao bloco if. Não há ponteiro para ponteiro. Como o tipo é conhecido em tempo de compilação, também não há reflexão, por isso corre mais depressa em caminhos críticos de tratamento de erros.

Restrições genéricas autorreferenciais

O Go 1.26 acrescenta suporte limitado a restrições de tipo recursivas. Isso torna alguns padrões exprimíveis pela primeira vez, como um tipo que se compara com outros valores do mesmo 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)
}

A restrição Comparable[T] refere o seu próprio parâmetro de tipo, e Max[T Comparable[T]] exige que T se possa comparar consigo próprio. Antes do Go 1.26, este tipo de restrição genérica autorreferencial não compilava. Agora podes criar APIs fluentes com encadeamento de métodos, árvores cujos nós referem o seu próprio tipo e builders com segurança de tipos que devolvem o tipo certo em cada passo.

APIs de criptografia mais simples

A maioria dos programadores de Go já escreveu esta linha:

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

O argumento rand.Reader é um resto histórico. As primeiras versões do Go permitiam fontes aleatórias personalizadas para testes. Na prática deves passar sempre crypto/rand.Reader, e passar qualquer outra coisa, como math/rand, abre uma falha de segurança.

O Go 1.26 torna a opção segura a predefinida. Passa nil e a função usa 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))
}

Isto cobre a geração de chaves em todos os pacotes de criptografia: rsa.GenerateKey(nil, bits), ecdsa.GenerateKey(curve, nil) e ecdh.GenerateKey(curve, nil). A única razão que resta para passares o teu próprio reader é um teste que precise de resultados deterministas.

HPKE para encriptação moderna

O novo pacote crypto/hpke implementa o RFC 9180, um padrão moderno de encriptação híbrida de chave pública. O RSA sozinho é lento e só consegue encriptar mensagens pequenas. A solução habitual é encriptar uma chave simétrica aleatória com RSA e depois usar essa chave para os dados propriamente ditos. Funciona, mas tens de a implementar com cuidado.

O HPKE padroniza esse padrão com algoritmos modernos. Pensa num cofre. O remetente cria um cadeado e uma chave de uso único, fecha a mensagem no cofre e envia-o juntamente com instruções que só a chave privada do destinatário consegue usar para recriar a chave de uso único.

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)

O HPKE usa chaves de 256 bits em vez dos 2048 a 4096 bits do RSA, lida com mensagens de qualquer tamanho e é muito mais rápido.

Detetar fugas de goroutines

As fugas de goroutines passam facilmente despercebidas. Uma fuga de memória acaba por deitar o programa abaixo. Uma fuga de goroutines só o torna mais lento com o tempo, à medida que milhares de goroutines bloqueadas se acumulam e ficam agarradas a memória e CPU sem fazer nada de útil.

Eis uma forma comum de deixar uma fugir:

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)
    }
}

Se o context for cancelado antes de expensiveOperation() retornar, a goroutine fica bloqueada para sempre a tentar enviar para um channel sem buffer que ninguém vai ler.

O Go 1.26 acrescenta um perfil goroutineleak e uma flag go test -goroutineleak que apanham estes casos automaticamente. Também podes ver uma fuga contando as goroutines antes e depois:

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!")
    }
}

A correção tem três partes: um channel com buffer para que o envio não bloqueie, um select dentro da goroutine para que ela possa sair quando o context é cancelado e uma verificação do context no chamador:

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)
}

Com o channel com buffer, o envio não pode bloquear mesmo que ninguém receba. O select dá à goroutine uma saída quando o context é cancelado. Quer o trabalho corra bem, quer seja cancelado, a goroutine termina.

O Go 1.26 também acrescenta métricas de goroutines ao runtime/metrics, para que os teus dashboards de produção acompanhem o número total de goroutines, as bloqueadas e as executáveis.

Melhorias na biblioteca padrão

Buffer.Peek

Ler de um bytes.Buffer consome os dados. Se querias espreitar primeiro, por exemplo para ver o tipo de mensagem antes de escolher o parser ou para validar um formato antes de o ler, tinhas de copiar o buffer.

O Go 1.26 acrescenta 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())
}

Comparação de prefixos IP

Ordenar prefixos IP obrigava a comparar bytes à mão. Agora netip.Prefix tem um método Compare() que encaixa diretamente em 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 do cancelamento no context de sinais

Se usas signal.NotifyContext() para um encerramento controlado (graceful shutdown), já consegues saber que sinal causou o cancelamento:

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")
    }
}

Testes e logging

slog.MultiHandler

Os serviços em produção escrevem muitas vezes logs em mais do que um sítio: JSON para um ficheiro para agregação de logs, texto para o stdout para depuração, erros para um serviço de monitorização. O slog.MultiHandler faz esse fan-out por ti, sem teres de o escrever:

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 pelo seu próprio nível. O ficheiro pode receber tudo ao nível debug enquanto a consola só recebe avisos e erros.

Diretórios de artefactos de teste

Os testes de integração escrevem muitas vezes ficheiros de saída, como capturas de ecrã, dumps de SQL e relatórios gerados. Esses ficheiros acabavam espalhados por /tmp ou por outro diretório qualquer, e ninguém os limpava.

t.ArtifactDir() dá-lhes uma localização padrão. É limpa quando o teste passa e mantida quando falha:

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)
    }
}

O que torna isto útil é os ficheiros ficarem lá quando o teste falha. Podes abrir a saída de um teste falhado sem andares à procura dela.

Melhorias no httptest

Testar código HTTPS que fala com hosts externos obrigava a desligar a verificação de certificados (má ideia, mesmo em testes) ou a uma configuração de certificados trabalhosa. No Go 1.26, httptest.Server.Client() redireciona os pedidos para example.com para o teu servidor de teste e trata do 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
}

Desempenho: Green Tea GC

Nenhuma destas melhorias exige alterações ao código. Recompila com o Go 1.26 e o teu programa corre mais depressa.

O novo garbage collector Green Tea reduz a latência P99 em 30 a 40 % e o overhead de memória em 10 a 15 %, e aumenta o throughput em 5 a 10 %. Os programas que alocam muitos objetos de vida curta são os que mais ganham. Os ganhos vêm de um melhor escalonamento do trabalho do GC e de menos overhead no rastreio das alocações.

As chamadas ao sistema e as chamadas cgo ficam cerca de 30 % mais rápidas porque o runtime deixou de ter o estado de processador _Psyscall, que acrescentava overhead a cada syscall. Alocar objetos pequenos (1 a 512 bytes) também fica cerca de 30 % mais rápido, graças a rotinas de alocação especializadas que usam tabelas de salto em vez de caminhos genéricos.

Algumas funções da biblioteca padrão também ficaram mais rápidas. fmt.Errorf está cerca de 30 % mais rápida e io.ReadAll está 28 % mais rápida. Em testes em produção, um servidor de API HTTP a processar 10.000 pedidos por segundo viu a latência P99 descer de 45ms para 32ms.

Funcionalidades do runtime

Ligações de rede que respeitam o context

O pacote net aplica agora os prazos do context à resolução de DNS, além da própria ligação:

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

Proteger dados sensíveis

O pacote experimental runtime/secret ajuda a manter os dados sensíveis fora dos dumps de memória:

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

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

Destroy() apaga a memória, o que encurta o tempo durante o qual um segredo pode aparecer num core dump ou num ficheiro de swap.

Operações SIMD

O novo pacote simd/archsimd dá-te as instruções vetoriais do hardware em amd64:

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

O SIMD processa vários valores por instrução, o que dá 4 a 16 vezes mais throughput em trabalho numérico de processamento de imagem, codecs de áudio e vídeo e computação científica. Depende da plataforma, por isso escreve uma alternativa escalar se o teu código tiver de correr noutras arquiteturas.

Migrar o teu código

O Go 1.26 melhora o go fix para que possa automatizar as migrações comuns:

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

Reescreve rsa.GenerateKey(rand.Reader, 2048) como rsa.GenerateKey(nil, 2048) e converte padrões de errors.As em errors.AsType quando consegue. Revê o diff, corre os testes e faz commit.

A lista completa de reescritas está em todos os modernizadores do go fix no Go 1.26.

O que isto significa para o teu código

As mudanças em que vais mexer primeiro são new(expr) e errors.AsType, e o go fix trata de parte dessa migração por ti. Corre go test -goroutineleak no teu código concorrente para encontrar fugas antes de chegarem à produção. Os geradores de chaves de criptografia escolhem agora a fonte aleatória segura quando passas nil. As melhorias de velocidade do GC e do runtime não te pedem nada além de recompilar.

Escreve Go como um engenheiro sénior

Lições interativas no navegador. As primeiras são grátis.

Experimenta uma lição grátisOu cria uma conta gratuita