Go 1.27, ou Como Aprendi a Parar de me Preocupar e Amar os Generic Methods

O Go 1.27 chega em agosto de 2026 e, pela primeira vez, a própria linguagem mudou. Não no sentido de "adicionamos uma nova função a slices", mas uma mudança real. Métodos genéricos foram implementados. Um problema com onze anos de existência foi encerrado. O pacote encoding/json foi silenciosamente substituído por um novo motor enquanto o sistema estava em operação.
Todos os exemplos abaixo foram executados no go1.27rc3 (darwin/arm64). Cada bloco de saída é um resultado real, copiado e colado, incluindo as mensagens de erro. Se algo parecer estranho, é porque foi exatamente isso que o compilador relatou.
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 então, temos "A Conversa". Ela funciona assim:
"Deixe-me apenas escrever um método
Mapno meu tipo de slice—"
method must have no type parameters"...tudo bem. Ficará como uma função em nível de pacote."
Os métodos só podiam usar parâmetros de tipo declarados pelo receptor. Seu próprio método não podia introduzir novos parâmetros. Portanto, toda transformação genérica era exilada para o escopo do pacote, onde ficava ao lado de dezessete outros auxiliares de escopo global chamados de alguma variação 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 nem 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 esta tarde, há duas restrições, e elas são mais importantes do que parecem:
- Métodos de interface não podem declarar parâmetros de tipo.
- Métodos genéricos não podem implementar métodos de interface.
A segunda restrição é a que causará problemas:
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
Portanto, métodos genéricos e dispatch dinâmico não se misturam. Isso não é a equipe do Go sendo restritiva — é que um método genérico possui infinitas instanciações, e uma tabela de métodos de interface precisa ser finita e conhecida em tempo de compilação. Não é possível colocar algo infinito em uma tabela finita. O universo determinou que não é possível.
Resumo prático: métodos genéricos são para tipos concretos com APIs utilitárias. Qualquer coisa que precise se esconder atrás de uma interface ainda requer a abordagem antiga.
A biblioteca padrão já tirou proveito disso. O Rand do math/rand/v2 recebeu 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
Anteriormente, apenas o rand.N em nível de pacote era genérico, o que significava usar a fonte global. Agora, seu próprio *Rand com seed obtém a mesma conveniência. Algo pequeno. Algo bom.
1.2. Seletores de campo em literais de struct, ou: O Issue #9859 finalmente descansa
O embedding fornece u.ID. O embedding não fornece User{ID: 1}. Em vez disso, ele fornece User{Base: Base{ID: 1}}, que é a maneira do Go perguntar se você realmente quis dizer aquilo.
O Issue #9859 foi aberto em 2015. Agora foi encerrado. Em algum lugar, um gopher que 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. Anteriormente, você escreveria Line{Object: Object{name: "diagonal"}, ...}.
Agora, as entrelinhas, que verifiquei tentando compilar tudo:
(a) A chave ainda é um identificador simples. Você não pode 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, a funcionalidade não é "chaves funcionam como acesso de campo agora". É especificamente "nomes de campos implicitamente promovidos são permitidos". Um pouco mais restrito do que o título sugere.
(b) Você não pode especificar um campo embutido e algo promovido a partir dele ao mesmo tempo.
1obj := Object{"edge", "black"}
2_ = Line{Object: obj, name: "diagonal"}
3// cannot specify promoted field name and enclosing embedded field Object
Razoável. Você forneceu duas informações conflitantes sobre a mesma memória e o compilador se recusou a adivinhar.
(c) O embedding de ponteiros não está incluído.
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, você precisa de um ponteiro para seguir, e no momento da construção do literal, ainda não existe um. Também justo.
Boas notícias: o go fix reescreverá seus literais antigos para você. Mais sobre isso depois.
1.3. A inferência de tipo de função tornou-se menos arbitrária
A inferência de tipo para funções genéricas costumava funcionar em alguns lugares e não em outros, sem um princípio discernível por trás disso. Atribuir a uma variável? Tudo bem. Colocar a mesma função em um campo de struct? Erro do compilador, por favor escreva double[int] como se estivéssemos em 2022.
O Go 1.27 faz a inferência funcionar em qualquer contexto onde o tipo de destino seja inequivocamente conhecido:
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 // isso sempre funcionou
14
15 fmt.Println(s.f(21), a[0](5), (<-c)(7), fn(1.5))
16 // 42 10 14 3
17}
Um único double, instanciado como func(int) int em três lugares e func(float64) float64 em um quarto, inteiramente a partir do contexto. Se você escreve structs de opções ou tabelas de manipuladores cheias de valores de função, uma grande quantidade de ruído [T] 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 para rotinas de alocação especializadas por tamanho para pequenos objetos. As notas de lançamento reivindicam até 30% de redução nas alocações abaixo de 80 bytes e cerca de 1% no geral para programas com uso intensivo de alocação.
Não acredito em notas de lançamento sem medição, 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 não faz nada além de alocar no Apple Silicon. O que significa: este é o melhor cenário, alcançado sob condições de laboratório por um benchmark projetado para fazer o número parecer bom. Em um programa real, o GC e o trabalho efetivo ofuscarão a maior parte disso. A alegação de ~1% é a honesta.
O custo é o tamanho do binário. Hello World:
12413202 bytes (padrão)
22361826 bytes (nosizespecializedmalloc)
Cerca de 50KB, fixos, independentemente do seu programa. Você pode optar por não usar com GOEXPERIMENT=nosizespecializedmalloc, mas essa saída de emergência está agendada para remoção no Go 1.28, portanto, trate-a como uma solução alternativa 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 de forma geral no 1.27. O GOEXPERIMENT=goroutineleakprofile se foi; simplesmente funciona.
1func leak() {
2 ch := make(chan int) // ninguém enviará. 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 número de linha apontando exatamente onde você cometeu o erro.
O truque por trás disso é genuinamente inteligente: ele reutiliza a análise de alcançabilidade do garbage collector. Se a goroutine G estiver bloqueada em uma primitiva P, e P for inalcançável a partir de qualquer goroutine executável (ou qualquer coisa que essas goroutines possam despertar), então nada poderá tocar P novamente, portanto G nunca acordará. Isso não é uma heurística — é uma prova.
O mesmo design oferece a limitação gratuitamente: se o canal ou mutex for alcançável através de uma variável global, ou através de uma variável local de alguma goroutine que ainda está em execução, o GC ainda pode vê-lo, portanto o runtime não pode concluir nada. Observe que até mesmo o exemplo fictício acima precisa de duas chamadas runtime.GC() para relatar. Ele não detectará tudo.
No entanto, detectará o clássico "esqueci de cancelar o contexto, a goroutine trabalhadora vive para sempre" — que constitui a maioria dos vazamentos na maior parte do tempo.
Se você importar net/http/pprof, ele também estará disponível em /debug/pprof/goroutineleak. Conecte isso ao ambiente de staging, verifique semanalmente e sinta um horror silencioso.
Este trabalho foi contribuído por Vlad Saioc, da Uber, que merece uma bebida de sua escolha.
2.3. Tracebacks agora indicam qual requisição morreu
Para módulos que declaram Go 1.27 ou posterior, as linhas de cabeçalho de traceback agora incluem rótulos (labels) de goroutines 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: rótulos são herdados por goroutines filhas. A goroutine 3 nunca definiu um rótulo. Ela herdou um de seu pai.
Portanto, na próxima vez que a produção sofrer um deadlock e alguém enviar um SIGQUIT ao processo, em vez de 4.000 goroutines que parecem idênticas, você terá 4.000 goroutines marcadas com a requisição e o tenant aos quais pertencem. Três linhas de pprof.Do no seu ponto de entrada de requisição lhe garantem isso.
Como os rótulos podem conter dados que você prefere não despejar no stderr, GODEBUG=tracebacklabels=0 desativa isso, e essa opção de desativação foi explicitamente planejada para permanecer disponível para sempre.
2.4. asynctimerchan desapareceu para sempre
O Go 1.23 tornou os canais de temporizador não bufferizados (síncronos) e ofereceu asynctimerchan=1 como uma forma de retornar ao comportamento antigo. No 1.27, essa configuração foi permanentemente removida. Os canais do pacote time são síncronos, ponto final, sem negociação.
A parte interessante é a política introduzida juntamente com isso. Um GODEBUG removido deixado no seu go.mod não quebra o build automaticamente — ele quebra apenas 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 normalmente)
O que significa que as únicas pessoas que receberão avisos são aquelas que realmente dependiam do comportamento removido. Todos que definiram como o padrão final e esqueceram disso continuarão sem se preocupar. Essa é uma peça atenciosa de arqueologia de API.
3. Biblioteca Padrão
3.1. encoding/json/v2: Eles substituíram o motor durante o voo
Esta é a grande novidade. Mais de dois anos de discussão de proposta finalmente chegaram ao fim.
Agora existem três pacotes, e entender a divisão é a maior parte do trabalho:
| Pacote | Função |
|---|---|
encoding/json | A API v1 que você conhece. Comportamento 100% inalterado. Agora implementada sobre a v2 |
encoding/json/v2 | Processamento semântico. Valores Go ↔ JSON |
encoding/json/jsontext | Processamento sintático. JSON como um fluxo de tokens |
A manchete é 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 da v1 legada foi codificada como uma opção, agrupada em DefaultOptionsV1(), e a API v1 sempre aplica esse pacote. O que significa que isso se comporta da mesma forma no 1.26 e no 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 assume a última. Ela sempre fez isso. Ela ainda faz. Compatibilidade significa compatibilidade com as partes ruins também.
A v2, por sua vez, 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 vence, você obtém bugs de segurança. O CVE-2017-12635 do CouchDB foi exatamente isso: um corpo JSON com duas chaves roles, onde o validador leu uma e a camada de armazenamento leu 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// rejeitar campos desconhecidos — anteriormente 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 você resolvia anteriormente com uma biblioteca de terceiros ou um MarshalJSON escrito à mão agora são sinalizadores.
E a história da migração é a melhor parte. Opções posteriores vencem, então você pode adotar o rigor da v2 um comportamento de cada vez:
1// manter semântica v1, mas rejeitar chaves duplicadas como a v2 faz
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 para parar de aceitar chaves duplicadas. Você altera uma opção. Para qualquer coisa grande, este é o caminho realista.
Desempenho: o marshaling está praticamente em paridade, o unmarshaling é significativamente mais rápido. Se algo der errado, GOEXPERIMENT=nojsonv2 restaura a implementação antiga — e essa opção de desativação também está na lista para ser removida eventualmente, então abra um issue em vez de se acomodar.
jsontext é a camada de baixo nível: Encoder/Decoder percorrendo JSON como Tokens e Values com uma máquina de estados mantendo a integridade. Use-a quando estiver escrevendo um transformador de streaming ou um filtro JSON e não quiser materializar valores Go.
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 superiores 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}
Toda a API é New, NewV4, NewV7, Nil, Max, Parse, MustParse e um tipo: type UUID [16]byte. É isso. Você pode ler toda a documentação do pacote enquanto seu café esfria.
Detalhes que valem a pena saber:
UUIDé[16]byte, então==funciona e é diretamente utilizável como chave de mapa. Mesmo design dogoogle/uuid.NileMaxsão funções, não variáveis. Porque umavar Nil UUIDem nível de pacote é uma arma carregada apontada para o seu pé, e alguém, em algum lugar, acabaria atribuindo algo a ela.- Bits aleatórios vêm de um gerador criptograficamente seguro.
- 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. Ordenado por tempo significa que seu índice B-tree para de fragmentar, algo em que os UUIDs v4 são notoriamente ruins.
Agora você pode excluir uma dependência. Se precisar de v1/v3/v5 ou opções de parsing mais sofisticadas, as bibliotecas de terceiros ainda têm seu lugar.
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
Olhe para esses números. Uma única assinatura tem 3.309 bytes. Ed25519 tem 64. É um aumento de 50x, e a chave pública tem quase 2KB além disso. Coloque algumas dessas em uma cadeia de certificados e seu handshake TLS começará a precisar de sua própria estratégia de MTU.
Este é o custo real da resistência quântica hoje, e é por isso que ninguém está mudando tudo amanhã. Mas está na biblioteca padrão agora, que é onde você quer que esteja antes de precisar.
Options.Context é separação de domínio: assine com a mesma chave para propósitos diferentes, use um contexto diferente para cada, e uma assinatura de um contexto não verificará em outro. O exemplo acima mostra exatamente isso — mesma chave, mesma mensagem, contexto diferente, rejeitado.
PrivateKey implementa crypto.Signer, então se encaixa em interfaces existentes. crypto/x509 lida com chaves e assinaturas ML-DSA, e crypto/tls suporta os esquemas de assinatura MLDSA44/MLDSA65/MLDSA87 no TLS 1.3.
Há também SignDeterministic, que ignora a aleatoriedade — útil para testes e builds reproduzíveis.
3.4. simd: Instruções vetoriais sem assembly
O Go 1.26 introduziu o simd/archsimd específico para arquitetura como um experimento. O Go 1.27 adiciona simd — portátil e agnóstico à largura do vetor. Habilite 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 // lide com a cauda com load/store parcial — sem loop de limpeza 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]
A escolha de design que importa: a largura do vetor nunca é codificada rigidamente. Você pergunta va.Len() em tempo de execução. VectorBitSize() diz a largura real, Emulated() diz se você obteve hardware real ou uma imitação de software educada. Meu Mac relatou 128-bit NEON; uma máquina AVX-512 relata mais; uma máquina sem nada relata emulação e o código ainda funciona.
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 lida com ela sem um loop escalar separado.
Ainda experimental, API ainda instável, não coloque em produção. Mas o fato de que este é código Go e não assembly ou cgo é um grande negócio.
3.5. hash/maphash.Hasher
Uma nova interface descrevendo 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 nativo do Go só aceita chaves comparable. Você não pode usar um slice como chave. Você não pode definir "igual ignorando maiúsculas/minúsculas". Hasher resolve ambos 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 semântica == comum, existe ComparableHasher[T]:
1hashOf(maphash.ComparableHasher[int]{}, 42)
O detalhe: Hasher é uma interface, não uma estrutura de dados. Não existe uma tabela de hash padrão ou filtro de Bloom que a consuma ainda. Este é o trabalho de base para um pacote de contêineres futuro. Hoje você a usaria ao construir sua própria estrutura, ou consumiria uma implementação existente como go/types.Hasher (que permite usar types.Type como chave de mapa, respeitando Identical).
O gerenciamento da seed é com você. Se o hashOf acima criasse uma nova seed a cada chamada, valores idênticos teriam hashes diferentes e o exemplo imprimiria false — que é exatamente o bug que escrevi na minha primeira tentativa. Uma seed por contêiner. (A seed é randomizada para derrotar ataques DoS de inundação de hash, razão pela qual não é apenas uma constante.)
3.6. httptest.NewTestServer + synctest.Sleep: O sucesso inesperado
Se você adotar apenas uma coisa deste lançamento, que seja esta.
testing/synctest graduou no Go 1.25 e nos deu um relógio falso para testar código concorrente. Exceto que, no momento em que seu teste tocava em uma rede real, a ilusão colapsava. 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-with-exponential-backoff:
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 isso duas vezes. Ele simulou três segundos de backoff contra um servidor HTTP real e terminou em 0,00 segundos. E a asserção é elapsed == 3*time.Second — exatamente três segundos, não "pelo menos três segundos, com algum jitter do escalonador". Relógios falsos não têm jitter.
synctest.Sleep existe por um motivo específico: se seu teste dorme pela mesma duração que o código sob teste, quem acorda primeiro é uma incógnita. synctest.Sleep dorme e então espera até que toda outra goroutine na bolha esteja duravelmente bloqueada, para que você observe o sistema após ele ter se estabilizado.
Timeouts, retries, disjuntores, limitadores de taxa — todo teste de cliente HTTP dependente de tempo que você possui pode se tornar rápido e determinístico. Se sua suíte de testes está atualmente mantida com time.Sleep(100 * time.Millisecond) e esperança, esta é sua saída.
3.7. Mudanças no net/http que realmente afetam a produção
Silenciosas, mas aparecerão em suas métricas.
Corpos de resposta HTTP/1 drenam automaticamente no Close. O conteúdo não lido agora é drenado (até um limite conservador) quando você fecha o corpo, para que a conexão possa ser reutilizada. O que significa que você finalmente pode excluir esse encantamento que todos copiaram 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 é um no-op ou uma pequena vitória. Se isso piorar as coisas, você provavelmente está no grupo que as notas de lançamento descrevem educadamente: Transport.MaxIdleConns definido como 0, ou um novo Client por requisição, ignorando completamente o limite de conexão ociosa. Transport.DisableKeepAlives = true mascarará o problema, mas o conselho real das notas de lançamento é que "um olhar mais profundo seria benéfico", que é a forma da equipe Go dizer 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, Server.DisableClientPriority = true.
Server.MaxHeaderValueCount. Limita quantos valores de cabeçalho o servidor aceitará, padrão para DefaultMaxHeaderValueCount. Mais uma porta fechada para "enviar dez mil cabeçalhos e ver o que acontece".
ALPN em conexões fornecidas pelo usuário. Se sua net.Conn implementa ConnectionState() tls.ConnectionState, Transport e Server farão negociação TLS ALPN nela — então dialers personalizados passando por um proxy ainda podem negociar HTTP/2.
3.8. Pequenas coisas que trazem alegria
strings.CutLast / bytes.CutLast. Cut divide no primeiro separador. Não havia variante "last", então todos faziam LastIndex manualmente mais fatiamento, e cerca de 30% de nós cometiam um erro de "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 parsing de host:port — onde você deve encontrar o último dois-pontos, porque endereços IPv6 estão cheios deles.
math/big.Int.Divide. Divisão com um modo de arredondamento explícito, substituindo o cara-ou-coroa "é 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 onde a regra de arredondamento está escrita em uma regulamentação, isto é para você.
url.URL.Clone e url.Values.Clone. Cópias profundas. O antigo *u de cópia rasa compartilhava o ponteiro Userinfo, o que produzia exatamente o tipo de bug que leva um dia para encontrar e cinco segundos para corrigir.
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 métodos de leitura agora retornam io.EOF diretamente em vez de envolvê-lo em net.OpError. err == io.EOF funciona agora, como sempre deveria ter funcionado.
database/sql.ConvertAssign e driver.RowsColumnScanner. Recursos para autores de drivers. O primeiro expõe as conversões de tipo que Rows.Scan realiza; o segundo permite que drivers façam scan diretamente para destinos do usuário, pulando 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, você 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 "golden-file" que fazem hash da saída comprimida, eles vão quebrar, e não será uma regressão. Verifique isso antes de atualizar, não durante a revisão do incidente.
4. Toolchain
4.1. go fix ganhou mais modernizadores
O go fix está se tornando silenciosamente uma ferramenta de modernização de código. O Go 1.27 adiciona atomictypes, embedlit, slicesbackward e unsafefuncs. Use -diff para visualizar:
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 dispararam em um pequeno arquivo:
embedlit— reescreve literais embutidos usando a nova sintaxe da §1.2atomictypes—atomic.AddInt64(&x, 1)torna-seatomic.Int64.Add(1), o que torna o acesso não atômico impossível e corrige silenciosamente bugs de alinhamento de 32 bits que você não sabia que tinhaslicesbackward— loops reversos tornam-seslices.Backwardwaitgroupgo— o ritualAdd/go/Donetorna-sewg.Go(renomeado dowaitgroupdo 1.26 para evitar ambiguidade)
go tool fix help lista todos os 26; go tool fix help <name> explica um. Além disso, fmtappendf foi removido "devido a preocupações estilísticas", que é uma maneira lindamente diplomática de descrever o que quer que tenha acontecido naquele thread de issue.
Executar go fix ./... em uma base de código antiga é uma tarde genuinamente satisfatória. Leia o diff primeiro, obviamente.
4.2. go test executa stdversion por padrão
Esta é a mudança com maior probabilidade de interromper seu dia.
go test agora executa a verificação vet stdversion por padrão, sinalizando símbolos da biblioteca padrão mais novos do que sua diretiva go no 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 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 mata o modo de falha clássico onde 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 carregam 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), saída de t.Error (error) e linhas geradas pelo framework (frame) agora são distinguíveis. Qualquer pessoa que tenha escrito um regex para descobrir quais linhas da saída de teste são "reais" agora pode excluí-lo.
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 de código de exemplo:
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
Código de exemplo e saída esperada, no terminal, sem abrir uma aba do navegador que ainda estará aberta na próxima quinta-feira.
4.5. go mod tidy limpa seus blocos require
Para módulos no go 1.27 ou posterior, go mod tidy mescla blocos require espalhados em no máximo dois (direto e indireto), preservando comentários anexados.
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 limpa principalmente após a resolução de conflitos de merge do Git, que é onde blocos require perdidos nascem. Se sua equipe tem mais de três pessoas adicionando dependências, seus conflitos no go.mod ficarão visivelmente mais silenciosos.
4.6. Outros detalhes
- Suporte a
bzrremovido. Se isso lhe afeta, eu realmente gostaria de ouvir a história. go tool trace -httpvincula-se ao localhost quando recebe apenas uma porta.-http=:6060não escuta mais em todas as interfaces. Use-http=0.0.0.0:6060se era isso que você queria. Isso agora corresponde aogo tool pprofe evita o eventual profiler público acidental.- Arquivos de resposta (
@file) são suportados porcompile,link,asm,cgo,coverepack, em um formato compatível com GCC. Para sistemas de build que excedem os limites de comprimento da linha de comando — alô, Bazel.
5. Compilador, Linker, Ports
Compilador. Nomes de arquivo relativos em diretivas //line agora resolvem em relação ao diretório do arquivo que os contém, correspondendo ao go/scanner. Relevante se você escreve geradores de código.
Nomes de símbolo de literal de função (closure) também são mais simples agora — o mesmo nome independentemente do inlining, e múltiplas instâncias do mesmo literal podem compartilhar código no binário. Nenhuma mudança funcional, exceto: código que compara identidade de função via reflect.Value.Pointer verá "igual" com mais frequência do que antes. Essa comparação nunca foi válida, mas se você a tem, agora é quando ela começa a mentir para você mais alto.
Linker. Novas opções -macos e -macsdk definem as versões de SO e SDK no comando de carga LC_BUILD_VERSION do macOS.
Ports.
- Darwin agora requer macOS 13 Ventura ou posterior, como anunciado no 1.26. Verifique seus runners de CI.
- PowerPC (
GOOS=linux GOARCH=ppc64) mudou para o ABI ELFv2. Requer kernel Linux 3.13+ (RHEL7 backported para 3.10). Cgo, PIE e linkagem externa agora são suportados. Se você usa cgo, mas precisa de um binário estático puramente Go, definaCGO_ENABLED=0.
6. Lista de verificação de atualização
O Go 1.27 leva a compatibilidade a sério, mas verifique estes pontos antes de aumentar a versão:
- Testes de golden-file na saída comprimida.
compress/flatealterou os codificadores; bytes de gzip/zip/png podem diferir. - Testes combinando strings de mensagem 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 falham no build apenas se definidos com seus valores antigos. - Violações de
stdversion.go testdetecta isso agora. Execute cedo se você mantém uma biblioteca com uma diretivagoconservadora. - Runners de CI macOS 12 ou anterior. O suporte acabou.
- Testes que fazem asserção em nomes de símbolos de literais de função e comparações de função
reflect.Value.Pointer. -
Transport.MaxIdleConns = 0ou umClientpor requisição. Combinado com corpos de resposta de drenagem automática, isso pode ficar mais lento.
Reflexões finais
O Go 1.27 é o lançamento onde muita manutenção adiada venceu de uma só vez.
Métodos genéricos foram a peça que faltava no trabalho de genéricos do 1.18. Seletores de campo em literais de struct encerraram um problema de onze anos. encoding/json/v2 foram dois anos de discussão de proposta. E uuid preencheu um buraco que praticamente todo projeto Go estava remendando com a mesma dependência de terceiros há uma década.
Se você quer uma ordem de prioridade para realmente obter valor disso:
httptest.NewTestServer+synctest.Sleep— testes HTTP dependentes de tempo tornam-se rápidos e determinísticos. Melhor relação esforço-recompensa de todo o lançamento.- O perfil de vazamento de goroutines — exponha um endpoint em staging e prepare-se para ser humilhado.
- Rótulos de goroutines em traceback — três linhas de
pprof.Dono seu ponto de entrada de requisição transformam seu próximo dump de stack das 3 da manhã em algo legível. go fix ./...— deixe a ferramenta fazer a modernização.encoding/json/v2— sem pressa. Adote uma opção de cada vez.
Pegue o go1.27rc3 e execute sua suíte de testes contra ele agora. Tudo nessa lista de verificação é significativamente mais agradável de descobrir em uma tarde de terça-feira do que durante um incidente.
Referências