GoSuda

Go 1.27, ou Como Aprendi a Parar de Me Preocupar e Passar a Amar os Métodos Genéricos

By iwanhae views ...

Visão geral do Go 1.27

A versão Go 1.27 chega em agosto de 2026, e desta vez a linguagem em si mudou. Não é a mudança do tipo "adicionamos uma nova função a slices", mas uma mudança real. Métodos genéricos foram incorporados. Uma issue com onze anos foi fechada. O encoding/json foi substituído silenciosamente por um novo motor enquanto o avião ainda estava no ar.

Todos os exemplos abaixo foram executados com go1.27rc3 (darwin/arm64). Todos os blocos de saída são reais, copiados diretamente, incluindo as mensagens de erro. Se algo parecer estranho, é porque foi exatamente isso que o compilador disse.

1go install golang.org/dl/go1.27rc3@latest
2go1.27rc3 download

1. A linguagem mudou (sim, de verdade)

1.1. Métodos genéricos

Desde o Go 1.18 temos genéricos e, desde o Go 1.18, temos também A Conversa. Ela segue mais ou menos assim:

"Vou apenas escrever um método Map no meu tipo slice..."

method must have no type parameters

"...tudo bem. Então será uma função de nível de pacote."

Os métodos só podiam usar parâmetros de tipo declarados pelo receptor. Um método próprio não podia introduzir novos parâmetros. Assim, toda transformação genérica era exilada para o escopo do pacote, onde ficava ao lado de outras dezessete funções auxiliares soltas com algum nome derivado de MapSlice.

O Go 1.27 corrige isso:

 1type List[E any] []E
 2
 3// F é um parâmetro de tipo declarado pelo próprio 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}

O receptor sequer precisa ser genérico. Uma struct comum pode ter um método genérico:

1type Bag struct{ items []any }
2
3func (b *Bag) Add[T any](v T) { b.items = append(b.items, v) }

Agora, antes de refatorar toda a sua base de código nesta tarde, há duas restrições, e elas importam mais do que parecem:

  1. Métodos de interfaces não podem declarar parâmetros de tipo.
  2. Métodos genéricos não podem implementar métodos de interfaces.

Esta segunda é a que vai te pegar:

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

Ou seja, métodos genéricos e despacho dinâmico não se misturam. Isso não é mesquinhez da equipe do Go: um método genérico tem infinitas instanciações, e a tabela de métodos de uma interface precisa ser finita e conhecida em tempo de compilação. Não se pode colocar algo infinito em uma tabela finita. O universo disse não.

Leitura prática: métodos genéricos são para tipos concretos com APIs de utilitários. Qualquer coisa que precise se esconder atrás de uma interface ainda precisa da abordagem antiga.

A biblioteca padrão já tirou proveito disso. O Rand de math/rand/v2 ganhou um 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

Antes, apenas a função rand.N de nível de pacote era genérica, o que significava usar a fonte global. Agora, o seu próprio *Rand com seed recebe a mesma comodidade. Pequena mudança. Mudança boa.

1.2. Seletores de campos em literais de struct, ou: a Issue #9859 finalmente é resolvida

A embedding permite escrever u.ID. A embedding não permite escrever User{ID: 1}. Em vez disso, é preciso escrever User{Base: Base{ID: 1}}, que é a maneira que o Go tem de perguntar se você realmente queria isso.

A Issue #9859 foi aberta em 2015. Agora foi fechada. Em algum lugar, um gopher que desde então mudou de carreira duas vezes está recebendo uma notificação do 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: use o campo promovido diretamente
12line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}

name não é um campo de Line. É um campo do Object embutido. Antes, seria preciso escrever Line{Object: Object{name: "diagonal"}, ...}.

Agora, os detalhes, que verifiquei tentando compilar todo o código.

(a) A chave continua sendo um identificador simples. Não é possível escrever um caminho de seletor arbitrário.

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

Portanto, o recurso não é "as chaves agora funcionam como acesso a campos". Ele é especificamente "nomes de campos implicitamente promovidos são permitidos". Um pouco mais restrito do que a manchete sugere.

(b) Não é possível especificar um campo embutido e, ao mesmo tempo, algo promovido a partir dele.

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

Razoável. Você deu duas informações conflitantes sobre a mesma memória, e o compilador se recusou a adivinhar.

(c) Embedding de ponteiro não é bem-vindo.

1type PtrEmbed struct {
2	*Object
3	z int
4}
5
6_ = PtrEmbed{name: "x"}
7// invalid implicit pointer indirection to reach name

Para seguir um ponteiro, é preciso ter um ponteiro a seguir, e no momento da construção do literal ele ainda não existe. Também faz sentido.

Boa notícia: o go fix reescreve seus literais antigos automaticamente. Mais sobre isso adiante.

1.3. A inferência de tipos em funções ficou menos arbitrária

A inferência de tipos para funções genéricas funcionava em alguns lugares e em outros não, sem um princípio claro que explicasse a diferença. Atribuir a uma variável? Funcionava. Colocar exatamente a mesma função em um campo de struct? Erro de compilação, por favor escreva double[int] como em 2022.

O Go 1.27 faz a inferência funcionar em todo contexto em que o tipo alvo é conhecido sem ambiguidade:

 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: precisava de double[int]
 8	a := A{double}             // 1.26: precisava de double[int]
 9
10	c := make(chan func(int) int, 1)
11	c <- double                // 1.26: precisava de double[int]
12
13	var fn func(float64) float64 = double  // isto sempre funcionou
14
15	fmt.Println(s.f(21), a[0](5), (<-c)(7), fn(1.5))
16	// 42 10 14 3
17}

Uma única double, instanciada como func(int) int em três lugares e como func(float64) float64 em um quarto, inteiramente a partir do contexto. Se você escreve structs de opções ou tabelas de handlers cheias de valores de função, uma boa quantidade de ruído [T] está prestes a desaparecer da sua base de código.

2. Runtime: desempenho gratuito e um detector de vazamentos

2.1. Alocação especializada por tamanho

O compilador agora emite chamadas a rotinas de alocação especializadas por tamanho para objetos pequenos. As notas de lançamento afirmam até 30% de redução nas alocações abaixo de 80 bytes, e cerca de 1% no geral para programas com muitas alocações.

Não acredito em notas de lançamento sem medir, então:

 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# padrão (malloc especializado por tamanho ativado)
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% mais rápido em um microbenchmark que só faz alocações em Apple Silicon. Ou seja: este é o melhor cenário, obtido em condições de laboratório por um benchmark projetado para deixar o número bonito. Em um programa real, o GC e o trabalho efetivo vão encobrir a maior parte disso. A afirmação de cerca de 1% é a honesta.

O custo está no tamanho do binário. Hello World:

12413202 bytes  (padrão)
22361826 bytes  (nosizespecializedmalloc)

Cerca de 50KB, fixos, independentemente do seu programa. É possível desativar com GOEXPERIMENT=nosizespecializedmalloc, mas essa saída de emergência está programada para remoção no Go 1.28, então trate-a como um paliativo para relatórios de bugs, e não como um estilo de vida.

2.2. O perfil de vazamento de goroutines agora é real

Experimental no Go 1.26, disponível em geral na 1.27. O GOEXPERIMENT goroutineleakprofile deixou de existir; ele simplesmente funciona.

 1func leak() {
 2	ch := make(chan int) // ninguém vai enviar nada. ninguém.
 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

Três goroutines, permanentemente travadas, com um arquivo e uma linha apontando exatamente onde você as criou.

O truque por trás disso é genuinamente engenhoso: ele reaproveita a análise de alcançabilidade do coletor de lixo. Se a goroutine G está bloqueada no primitivo P, e P não é alcançável a partir de nenhuma goroutine executável (nem de nada que essas goroutines possam acordar), então nada pode tocar em P novamente, e G nunca vai acordar. Isso não é uma heurística, é uma prova.

O mesmo design traz de graça a limitação: se o canal ou o mutex é alcançável por meio de uma variável global, ou por meio de uma variável local de alguma goroutine que ainda está em execução, o GC ainda pode enxergá-lo, e o runtime não consegue concluir nada. Observe que até o exemplo simples acima precisa de duas chamadas a runtime.GC() para gerar o relatório. Ele não pega tudo.

No entanto, ele vai pegar o clássico "esqueci de cancelar o contexto, e a goroutine do worker vive para sempre", que é a maioria dos vazamentos na maior parte do tempo.

Se você importar net/http/pprof, o perfil também fica disponível em /debug/pprof/goroutineleak. Conecte isso ao ambiente de staging, verifique semanalmente e fique discretamente horrorizado.

Este trabalho foi contribuído por Vlad Saioc, da Uber, que merece uma bebida de sua escolha.

2.3. Tracebacks agora informam qual requisição falhou

Para módulos que declaram Go 1.27 ou posterior, as linhas de cabeçalho dos tracebacks agora incluem os rótulos de goroutine do 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...

O detalhe importante: os rótulos são herdados pelas goroutines filhas. A goroutine 3 nunca definiu um rótulo. Ela recebeu um do pai.

Assim, da próxima vez que a produção travar e alguém enviar SIGQUIT para o processo, em vez de 4.000 goroutines que parecem todas idênticas, você verá 4.000 goroutines marcadas com a requisição e o tenant a que pertencem. Três linhas de pprof.Do no ponto de entrada da sua requisição garantem isso.

Como os rótulos podem conter coisas que você preferiria não despejar no stderr, GODEBUG=tracebacklabels=0 desativa o recurso, e essa opção de desativação foi explicitamente planejada para permanecer para sempre.

2.4. asynctimerchan foi removido definitivamente

O Go 1.23 tornou os canais de timer não bufferizados (síncronos) e ofereceu asynctimerchan=1 como forma de voltar ao comportamento antigo. Na 1.27, essa configuração é removida permanentemente. Os canais do pacote time são síncronos, ponto final, sem negociação.

A parte interessante é a política que acompanhou essa mudança. Um GODEBUG removido que permanece no seu go.mod não quebra o build automaticamente; ele só quebra se estiver definido com o valor antigo:

1# go.mod contém: 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 contém: godebug asynctimerchan=0
6(compila sem problemas)

Isso significa que as únicas pessoas que levam bronca são aquelas que realmente dependiam do comportamento removido. Quem definiu o valor que virou o padrão e esqueceu do assunto pode continuar sem pensar nisso. É uma peça atenciosa de arqueologia de API.

3. Biblioteca padrão

3.1. encoding/json/v2: trocaram o motor em pleno voo

Este é o grande destaque. Mais de dois anos de discussão de proposta finalmente foram incorporados.

Agora existem três pacotes, e entender a divisão é a maior parte do trabalho:

PacoteFunção
encoding/jsonA API v1 que você conhece. Comportamento 100% inalterado. Agora implementada sobre a v2
encoding/json/v2Processamento semântico. Valores Go ↔ JSON
encoding/json/jsontextProcessamento sintático. JSON como um fluxo de tokens

A principal novidade é que o encoding/json foi reconstruído sobre uma implementação completamente diferente e se comporta de forma idêntica. Veja como:

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

Cada peculiaridade legada da v1 foi codificada como uma opção, agrupada em DefaultOptionsV1(), e a API v1 sempre aplica esse conjunto. Isso significa que o código abaixo se comporta da mesma forma nas versões 1.26 e 1.27:

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

Sim, a v1 ainda aceita silenciosamente chaves duplicadas e fica com a última. Sempre foi assim. Continua assim. Compatibilidade significa compatibilidade também com as partes ruins.

A v2, por outro lado, tem opiniões:

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

Chaves duplicadas e UTF-8 inválido são rejeitados. Ambos são genuinamente perigosos, não apenas desorganizados: quando dois parsers discordam sobre qual chave duplicada prevalece, surgem falhas de segurança. A CVE-2017-12635 do CouchDB foi exatamente isso: um corpo JSON com duas chaves roles, em que o validador lia uma e a camada de armazenamento lia a outra. Rejeitar é a decisão correta.

As opções são 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// chaves de mapa ordenadas + indentação
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// rejeita campos desconhecidos (antes exigia um Decoder e 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: a maioria das coisas que antes você resolvia com uma biblioteca de terceiros ou com um MarshalJSON escrito à mão agora são flags.

E a história da migração é a melhor parte. As opções posteriores prevalecem, então você pode adotar a rigidez da v2 um comportamento por vez:

1// mantém a semântica da v1, mas rejeita chaves duplicadas como a v2
2jsonv2.Unmarshal(data, &v,
3	json.DefaultOptionsV1(),
4	jsontext.AllowDuplicateNames(false))
5// duplicate object member name

Você não precisa portar uma base de código de 200 mil linhas para a v2 apenas para deixar de aceitar chaves duplicadas. Basta alterar uma opção. Para qualquer projeto grande, esse é o caminho realista.

Desempenho: a serialização fica aproximadamente no mesmo nível, e a desserialização é significativamente mais rápida. Se algo der errado, GOEXPERIMENT=nojsonv2 restaura a implementação antiga. Essa opção de desativação também está programada para ser removida um dia, então abra uma issue em vez de se acomodar.

jsontext é a camada de baixo nível: Encoder/Decoder percorrem JSON como Tokens e Values, com uma máquina de estados que mantém você honesto. Use-o quando estiver escrevendo um transformador de streaming ou um filtro de JSON e não quiser materializar valores Go de forma alguma.

3.2. Um pacote uuid padrão

Finalmente. RFC 9562, na biblioteca padrão, sem necessidade de go get.

 1import "uuid"
 2
 3func main() {
 4	fmt.Println(uuid.NewV4())
 5	// b97aa695-da08-472c-af81-ff088129019f
 6
 7	// v7: os 48 bits mais significativos são um timestamp, então estes sempre ordenam na ordem de criação
 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}

A API inteira é New, NewV4, NewV7, Nil, Max, Parse, MustParse e um tipo: type UUID [16]byte. Só isso. Você pode ler a documentação completa do pacote enquanto o seu café esfria.

Detalhes que vale conhecer:

  • UUID é [16]byte, então == funciona e ele pode ser usado diretamente como chave de mapa. Mesmo design do google/uuid.
  • Nil e Max são funções, não variáveis. Porque uma variável de pacote var Nil UUID é uma arma carregada apontada para o seu pé, e alguém, em algum lugar, acabaria atribuindo a ela.
  • Os bits aleatórios vêm de um gerador criptograficamente seguro.
  • Ele implementa encoding.TextMarshaler/TextUnmarshaler/TextAppender, então se encaixa diretamente em structs JSON.
  • NewV7 é o que você quer para chaves primárias de banco de dados. Ordenação temporal significa que o seu índice B-tree deixa de se fragmentar, algo em que UUIDs v4 são notoriamente ruins.

Agora você pode eliminar uma dependência. Se precisar de v1/v3/v5 ou de opções de parsing mais sofisticadas, bibliotecas de terceiros ainda têm utilidade.

3.3. crypto/mldsa: assinaturas pós-quânticas, e são enormes

O Go 1.24 nos deu crypto/mlkem para troca de chaves pós-quântica. O Go 1.27 traz a outra metade: assinaturas ML-DSA, padronizadas 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

Observe esses números. Uma única assinatura tem 3.309 bytes. Ed25519 tem 64. É um aumento de cerca de 50 vezes, e a chave pública tem quase 2KB além disso. Coloque algumas dessas em uma cadeia de certificados e o seu handshake TLS passará a precisar de uma estratégia própria de MTU.

Esse é o custo real da resistência quântica hoje, e é por isso que ninguém vai migrar tudo amanhã. Mas ela já está na biblioteca padrão, que é onde você quer que ela esteja antes de precisar dela.

Options.Context é separação de domínio: assine com a mesma chave para finalidades diferentes, use um contexto diferente para cada uma, e uma assinatura de um contexto não será verificada em outro. O exemplo acima mostra exatamente isso: mesma chave, mesma mensagem, contexto diferente, rejeitado.

PrivateKey implementa crypto.Signer, então se encaixa nas interfaces existentes. crypto/x509 trata chaves e assinaturas ML-DSA, e crypto/tls oferece suporte aos esquemas de assinatura MLDSA44/MLDSA65/MLDSA87 no TLS 1.3.

Também existe SignDeterministic, que dispensa a aleatoriedade, útil para testes e builds reprodutíveis.

3.4. simd: instruções vetoriais sem assembly

O Go 1.26 introduziu o simd/archsimd, específico de arquitetura, como experimento. O Go 1.27 adiciona simd, portável e agnóstico quanto à largura do vetor. Ative com 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	// trata a cauda com um carregamento/armazenamento parcial, sem loop escalar de limpeza
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]

A escolha de design que importa: a largura do vetor nunca é fixa no código. Você consulta va.Len() em tempo de execução. VectorBitSize() informa a largura real, e Emulated() informa se você obteve hardware real ou uma imitação educada em software. O meu Mac reportou NEON de 128 bits; uma máquina com AVX-512 reporta mais; uma máquina sem nada reporta emulação, e o código continua funcionando.

LoadFloat32sPart/StorePart merecem menção especial. A parte mais irritante do SIMD escrito à mão é sempre a cauda irregular no final do array, e isso resolve o problema sem um loop escalar separado.

Ainda é experimental, a API ainda é instável, não coloque em produção. Mas o fato de isso ser código Go, e não assembly ou cgo, é uma mudança realmente grande.

3.5. hash/maphash.Hasher

Uma nova interface que descreve o contrato entre um tipo de valor e contêineres baseados em hash:

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

Por quê? O mapa embutido do Go só aceita chaves comparable. Você não pode usar um slice como chave. Não pode definir "igual ignorando maiúsculas e minúsculas". O Hasher resolve os dois problemas de uma 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)   // mesma seed, ou nada disso faz sentido
15	hr.Hash(&h, v)
16	return h.Sum64()
17}
18
19fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
20// true

Para a semântica comum de ==, existe ComparableHasher[T]:

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

A pegadinha: Hasher é uma interface, não uma estrutura de dados. Ainda não existe uma tabela hash padrão nem um filtro de Bloom que o consuma. Isso é uma base para um futuro pacote de contêineres. Hoje, você o usaria ao construir sua própria estrutura, ou ao consumir uma implementação existente, como go/types.Hasher (que permite usar types.Type como chave de mapa, respeitando Identical).

O gerenciamento da seed é responsabilidade sua. Se o hashOf acima gerasse uma seed nova a cada chamada, valores idênticos receberiam hashes diferentes e o exemplo imprimiria false, exatamente o bug que cometi na minha primeira tentativa. Uma seed por contêiner. (A seed é aleatorizada para impedir ataques DoS por inundação de hash, e por isso não é apenas uma constante.)

3.6. httptest.NewTestServer + synctest.Sleep: o problema do Sleeper

Se você adotar apenas uma coisa desta versão, que seja esta.

O testing/synctest se tornou estável no Go 1.25 e nos deu um relógio falso para testar código concorrente. Só que, no momento em que o teste tocava em uma rede real, a ilusão desmoronava. O httptest.NewTestServer do Go 1.27 usa uma rede falsa em memória, então a bolha permanece intacta. E synctest.Sleep (= time.Sleep + synctest.Wait) completa o conjunto.

Aqui está um teste de retry com backoff 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		// o backoff deve ser exatamente 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)

Leia de novo. Ele simulou três segundos de backoff contra um servidor HTTP real e terminou em 0,00 segundo. E a asserção é elapsed == 3*time.Second, exatamente três segundos, e não "pelo menos três segundos, com alguma variação do escalonador". Relógios falsos não têm variação.

O synctest.Sleep existe por um motivo específico: se o seu teste dorme pela mesma duração que o código testado, não há como saber qual acorda primeiro. O synctest.Sleep dorme e depois espera até que todas as outras goroutines da bolha estejam bloqueadas de forma durável, então você observa o sistema depois que ele se acomodou.

Timeouts, retries, circuit breakers, rate limiters: cada teste de cliente HTTP dependente de tempo que você mantém pode se tornar rápido e determinístico. Se a sua suíte de testes hoje se sustenta com time.Sleep(100 * time.Millisecond) e esperança, esta é a sua saída.

3.7. Mudanças no net/http que afetam produção de fato

Discretas, mas vão aparecer nas suas métricas.

Os corpos de resposta HTTP/1 são drenados automaticamente no Close. O conteúdo não lido agora é drenado (até um limite conservador) ao fechar o corpo, para que a conexão possa ser reutilizada. Isso significa que você finalmente pode apagar esta fórmula que todo mundo copiou da mesma resposta do Stack Overflow desde 2016:

1// não é mais necessário
2defer func() {
3	io.Copy(io.Discard, resp.Body)
4	resp.Body.Close()
5}()

Para a maioria dos programas, isso não faz diferença ou é uma pequena melhoria. Se piorar as coisas, você provavelmente está no caso que as notas de lançamento descrevem de forma educada: Transport.MaxIdleConns definido como 0, ou um novo Client por requisição, contornando totalmente o limite de conexões ociosas. Transport.DisableKeepAlives = true esconde o problema, mas o conselho real das notas de lançamento é que "uma análise mais profunda provavelmente seria benéfica", o que, na linguagem da equipe do Go, significa você tem problemas maiores.

Prioridade de cliente HTTP/2 (RFC 9218). O servidor agora respeita os sinais de prioridade do cliente. Se você preferia o antigo escalonamento round-robin, use Server.DisableClientPriority = true.

Server.MaxHeaderValueCount. Limita quantos valores de cabeçalho o servidor aceitará, com padrão em DefaultMaxHeaderValueCount. Mais uma porta fechada para o "envie dez mil cabeçalhos e veja o que acontece".

ALPN em conexões fornecidas pelo usuário. Se o seu net.Conn implementar ConnectionState() tls.ConnectionState, o Transport e o Server farão a negociação ALPN de TLS sobre ela, de modo que dialers personalizados que passam por um proxy ainda possam negociar HTTP/2.

3.8. As pequenas novidades que trazem alegria

strings.CutLast / bytes.CutLast. O Cut divide na primeira ocorrência do separador. Não havia uma variante "last", então todo mundo escrevia à mão LastIndex mais fatiamento, e cerca de 30% de nós erramos um off-by-one na primeira tentativa.

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

Ótimo para extensões de arquivo e para analisar host:port, onde é preciso encontrar os últimos dois-pontos, porque endereços IPv6 estão cheios deles.

math/big.Int.Divide. Divisão com modo de arredondamento explícito, substituindo a dúvida "é Quo ou Div que eu quero?":

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

Se você trabalha em um domínio em que a regra de arredondamento está escrita em uma regulamentação, isto é para você.

url.URL.Clone e url.Values.Clone. Cópias profundas. A antiga cópia rasa *u compartilhava o ponteiro Userinfo, o que produzia exatamente o tipo de bug que leva um dia para ser encontrado e cinco segundos para ser corrigido.

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

Os métodos de leitura de net.UnixConn agora retornam io.EOF diretamente, em vez de envolvê-lo em net.OpError. err == io.EOF passa a funcionar, como sempre deveria.

database/sql.ConvertAssign e driver.RowsColumnScanner. Recursos para autores de drivers. O primeiro expõe as conversões de tipo que Rows.Scan executa; o segundo permite que drivers façam scan diretamente em destinos do usuário, evitando uma alocação intermediária.

unicode 15 → 17. Duas versões em um salto. O comportamento de classificação e normalização de strings pode mudar sutilmente. Se você tem testes que dependem disso, vai descobrir.

compress/flate ficou mais rápido, e seus bytes de saída podem diferir do Go 1.26. Isso se propaga para archive/zip, compress/gzip, compress/zlib e image/png. Se você tem testes de arquivos de referência (golden files) que calculam hash da saída comprimida, eles vão quebrar, e não será uma regressão. Verifique isso antes de atualizar, e não durante a análise pós-incidente.

4. Toolchain

4.1. O go fix ganhou mais modernizadores

O go fix está silenciosamente se transformando em uma ferramenta de modernização de código. O Go 1.27 adiciona atomictypes, embedlit, slicesbackward e unsafefuncs. Use -diff para visualizar antes:

 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 }

Quatro modernizadores foram acionados em um arquivo pequeno:

  • embedlit: reescreve literais embutidos usando a nova sintaxe da seção 1.2
  • atomictypes: atomic.AddInt64(&x, 1) vira atomic.Int64.Add(1), o que torna impossível o acesso não atômico e corrige silenciosamente bugs de alinhamento em 32 bits que você nem sabia que tinha
  • slicesbackward: loops reversos viram slices.Backward
  • waitgroupgo: o ritual Add/go/Done vira wg.Go (renomeado de waitgroup, da 1.26, para evitar ambiguidade)

go tool fix help lista todos os 26; go tool fix help <nome> explica um deles. Além disso, fmtappendf foi removido "por questões de estilo", que é uma maneira belamente diplomática de descrever o que aconteceu naquela discussão da issue.

Executar go fix ./... em uma base de código antiga é uma tarde genuinamente satisfatória. Leia o diff primeiro, evidentemente.

4.2. O go test executa o stdversion por padrão

Esta é a mudança com mais chances de interromper o seu dia.

O go test agora executa a verificação stdversion do vet por padrão, sinalizando símbolos da biblioteca padrão mais novos do que a diretiva go do seu go.mod permite:

1// go.mod diz: go 1.24
2package sv
3
4import "strings"
5
6func Ext(name string) string {
7	_, ext, _ := strings.CutLast(name, ".")  // CutLast é um símbolo da 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]

Isso elimina o clássico modo de falha em que tudo funciona na sua máquina (toolchain mais recente) e explode no CI ou no ambiente antigo de um usuário. Se você publica bibliotecas com uma diretiva go conservadora, isso é um presente.

4.3. go test -json ganhou OutputType

As linhas "Action":"output" agora podem trazer um campo opcional "OutputType": "error", "error-continue" ou "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'

A saída de t.Log (sem campo), a saída de t.Error (error) e as linhas geradas pelo framework (frame) agora são distinguíveis. Quem já escreveu uma expressão regular para descobrir quais linhas da saída de teste são "reais" agora pode apagá-la.

4.4. Melhorias no go doc

Sintaxe package@version. Leia a documentação de uma versão específica sem adicioná-la ao seu 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 verificar o que mudou antes de uma atualização.

-ex e impressão do código-fonte dos exemplos:

 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

O código-fonte do exemplo e a saída esperada, no terminal, sem precisar abrir uma aba do navegador que ainda estará aberta na próxima quinta-feira.

4.5. O go mod tidy organiza seus blocos require

Para módulos com go 1.27 ou posterior, o go mod tidy mescla blocos require espalhados em no máximo dois (diretos e indiretos), preservando os comentários associados.

 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// Depois: 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)

Observe que o comentário // networking sobreviveu. Isso ajuda principalmente a limpar depois da resolução de conflitos de merge no Git, que é onde os blocos require soltos nascem. Se a sua equipe tem mais de três pessoas adicionando dependências, os conflitos no seu go.mod vão ficar bem mais silenciosos.

4.6. Detalhes diversos

  • Suporte a bzr removido. Se isso afetar você, eu realmente gostaria de ouvir a história.
  • go tool trace -http vincula-se ao localhost quando recebe apenas uma porta. -http=:6060 não escuta mais em todas as interfaces. Use -http=0.0.0.0:6060 se essa for a intenção. Isso agora fica alinhado com o go tool pprof e evita o eventual profiler público acidental.
  • Arquivos de resposta (@file) são suportados por compile, link, asm, cgo, cover e pack, em um formato compatível com o GCC. Para sistemas de build que estouram os limites de tamanho da linha de comando, olá, Bazel.

5. Compilador, linker, portes

Compilador. Nomes de arquivo relativos em diretivas //line agora são resolvidos em relação ao diretório do arquivo que as contém, em consonância com o go/scanner. Relevante se você escreve geradores de código.

Os nomes de símbolos de função literais (closures) também ficaram mais simples: o mesmo nome independentemente de inlining, e múltiplas instâncias do mesmo literal podem compartilhar código no binário. Sem mudança funcional, exceto: código que compara a identidade de funções via reflect.Value.Pointer vai encontrar "igual" com mais frequência do que antes. Essa comparação nunca foi válida, mas se você a usa, agora ela começará a mentir para você de forma mais alta.

Linker. As novas opções -macos e -macsdk definem as versões do sistema operacional e do SDK no comando de carregamento LC_BUILD_VERSION do macOS.

Portes.

  • O Darwin agora exige macOS 13 Ventura ou posterior, como anunciado na 1.26. Verifique os seus runners de CI.
  • PowerPC (GOOS=linux GOARCH=ppc64) mudou para a ABI ELFv2. Requer kernel Linux 3.13+ (o RHEL7 fez backport para 3.10). Cgo, PIE e linkagem externa agora são suportados. Se você usa cgo, mas precisa de um binário Go puro e estático, defina CGO_ENABLED=0.

6. Checklist de atualização

O Go 1.27 leva a compatibilidade a sério, mas verifique estes pontos antes de atualizar a versão:

  • Testes com arquivos de referência sobre saída comprimida. O compress/flate mudou os codificadores; os bytes de gzip/zip/png podem ser diferentes.
  • Testes que comparam strings de mensagens de erro JSON. O comportamento é idêntico; o texto do erro não é.
  • GODEBUGs removidos no go.mod. asynctimerchan, gotypesalias, tlsrsakex, tls3des, tls10server, tlsunsafeekm, x509keypairleaf: estes só fazem o build falhar se estiverem definidos com seus valores antigos.
  • Violações do stdversion. O go test agora as detecta. Execute-o cedo se você mantém uma biblioteca com uma diretiva go conservadora.
  • Runners de CI com macOS 12 ou anterior. O suporte acabou.
  • Testes que fazem asserções sobre nomes de símbolos de funções literais, e comparações de funções com reflect.Value.Pointer.
  • Transport.MaxIdleConns = 0 ou um Client por requisição. Combinado com a drenagem automática dos corpos de resposta, isso pode ficar mais lento.

Considerações finais

O Go 1.27 é a versão em que muita manutenção adiada chegou de uma só vez.

Métodos genéricos eram a peça que faltava no trabalho de genéricos da 1.18. Seletores de campos em literais de struct fecharam uma issue de onze anos. O encoding/json/v2 foi dois anos de discussão de proposta. E o uuid preencheu uma lacuna que praticamente todo projeto Go vinha remendando com a mesma dependência de terceiros há uma década.

Se você quer uma ordem de prioridade para realmente extrair valor disso:

  1. httptest.NewTestServer + synctest.Sleep: testes HTTP dependentes de tempo tornam-se rápidos e determinísticos. A melhor relação esforço-retorno de toda a versão.
  2. O perfil de vazamento de goroutines: exponha um endpoint em staging e prepare-se para se sentir humilhado.
  3. Rótulos de goroutine nos tracebacks: três linhas de pprof.Do no ponto de entrada das requisições transformam o seu próximo dump de pilha às 3 da manhã em algo legível.
  4. go fix ./...: deixe a ferramenta fazer a modernização.
  5. encoding/json/v2: sem pressa. Adote uma opção por vez.

Pegue o go1.27rc3 e execute sua suíte de testes com ele agora. Tudo nesta lista de verificação é muito mais agradável de descobrir numa terça-feira à tarde do que durante um incidente.

Referências