Go 1.27, atau Bagaimana Saya Belajar Berhenti Khawatir dan Mencintai Generic Methods


Go 1.27 akan dirilis pada Agustus 2026, dan untuk kali ini bahasa pemrogramannya sendiri benar-benar berubah. Bukan sekadar "kami menambahkan fungsi baru ke slices", melainkan perubahan yang sesungguhnya. Generic methods sudah hadir. Sebuah isu berumur sebelas tahun akhirnya ditutup. encoding/json diganti secara diam-diam dengan mesin baru ketika pesawat sedang mengudara.
Setiap contoh di bawah ini dijalankan pada go1.27rc3 (darwin/arm64). Setiap blok output adalah output sungguhan, disalin apa adanya, termasuk pesan error. Jika ada yang terlihat janggal, itu karena memang demikianlah yang dikeluarkan oleh compiler.
1go install golang.org/dl/go1.27rc3@latest
2go1.27rc3 download
1. Bahasa Pemrograman Telah Berubah (Ya, Sungguh)
1.1. Generic Methods
Sejak Go 1.18 kita sudah memiliki generics, dan sejak Go 1.18 pula kita memiliki Percakapan Itu. Biasanya alurnya seperti ini:
"Biar saya tulis saja method
Mappada tipe slice saya..."
method must have no type parameters"...baiklah. Kalau begitu, fungsi level paket saja."
Method hanya dapat menggunakan type parameter yang dideklarasikan oleh receiver. Method yang kita tulis sendiri tidak dapat memperkenalkan type parameter baru. Akibatnya, setiap transformasi generik harus diasingkan ke level paket, tempat ia berdampingan dengan tujuh belas helper lepas lainnya yang namanya merupakan variasi dari MapSlice.
Go 1.27 memperbaiki hal ini:
1type List[E any] []E
2
3// F adalah type parameter yang dideklarasikan oleh method itu sendiri
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}
Receiver bahkan tidak perlu bersifat generik. Sebuah struct biasa dapat memiliki method generik:
1type Bag struct{ items []any }
2
3func (b *Bag) Add[T any](v T) { b.items = append(b.items, v) }
Sekarang, sebelum Anda merefaktor seluruh codebase pada sore ini, perhatikan dua batasan, dan keduanya lebih penting daripada kelihatannya:
- Method interface tidak boleh mendeklarasikan type parameter.
- Generic methods tidak dapat mengimplementasikan method interface.
Batasan kedua inilah yang akan merugikan Anda:
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
Jadi, generic methods dan dynamic dispatch tidak dapat dicampur. Ini bukan karena tim Go bersikap pelit, melainkan karena sebuah generic method memiliki jumlah instansiasi yang tak terhingga, sedangkan tabel method interface harus berhingga dan diketahui pada waktu kompilasi. Anda tidak dapat memasukkan sesuatu yang tak terhingga ke dalam tabel yang berhingga. Semesta sudah berkata tidak.
Kesimpulan praktisnya: generic methods diperuntukkan bagi tipe konkret dengan API bergaya utilitas. Sesuatu yang harus disembunyikan di balik interface tetap memerlukan pendekatan lama.
Standard library sudah memanfaatkan fitur ini. Rand pada math/rand/v2 memperoleh sebuah generic method:
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
Sebelumnya hanya fungsi level paket rand.N yang bersifat generik, yang berarti hanya dapat menggunakan sumber global. Sekarang *Rand berseed milik Anda sendiri memperoleh kemudahan yang sama. Perubahan kecil. Perubahan yang menyenangkan.
1.2. Pemilih Field pada Struct Literal, atau: Issue #9859 Akhirnya Terselesaikan
Embedding memberi Anda u.ID. Embedding tidak memberi Anda User{ID: 1}. Sebaliknya, embedding memberi Anda User{Base: Base{ID: 1}}, yang merupakan cara Go menanyakan apakah Anda benar-benar menghendakinya.
Issue #9859 diajukan pada tahun 2015. Kini issue tersebut telah ditutup. Di suatu tempat, seorang gopher yang sejak itu telah dua kali berganti karier sedang menerima notifikasi 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: gunakan field hasil promosi secara langsung
12line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}
name bukanlah field dari Line. Ia adalah field dari Object yang di-embed. Sebelumnya Anda harus menulis Line{Object: Object{name: "diagonal"}, ...}.
Sekarang untuk catatan kaki yang penting, yang saya periksa dengan benar-benar mencoba mengompilasi semuanya.
(a) Kunci tetap berupa identifier biasa. Anda tidak dapat menulis path selector yang sembarang.
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
Jadi, fitur ini bukanlah "kunci kini bekerja seperti akses field". Fitur ini secara spesifik adalah "nama field hasil promosi secara implisit diizinkan". Cakupannya sedikit lebih sempit daripada yang tersirat dari judulnya.
(b) Anda tidak dapat menentukan sebuah field embedded dan sebuah field hasil promosi dari field tersebut secara bersamaan.
1obj := Object{"edge", "black"}
2_ = Line{Object: obj, name: "diagonal"}
3// cannot specify promoted field name and enclosing embedded field Object
Masuk akal. Anda memberi tahu compiler dua hal yang saling bertentangan mengenai memori yang sama, dan compiler menolak untuk menebak.
(c) Embedding pointer tidak dianjurkan.
1type PtrEmbed struct {
2 *Object
3 z int
4}
5
6_ = PtrEmbed{name: "x"}
7// invalid implicit pointer indirection to reach name
Untuk mengikuti sebuah pointer, Anda memerlukan pointer untuk diikuti, dan pada saat konstruksi literal pointer tersebut belum ada. Ini juga masuk akal.
Kabar baiknya: go fix akan menulis ulang literal lama Anda secara otomatis. Lebih lanjut mengenai hal itu nanti.
1.3. Inferensi Tipe Fungsi Menjadi Tidak Semena-mena
Inferensi tipe untuk fungsi generik dahulu berfungsi di sebagian tempat dan tidak di tempat lain, tanpa prinsip yang dapat dikenali di balik perbedaan tersebut. Menugaskan ke sebuah variabel? Berfungsi. Memasukkan fungsi yang sama persis ke dalam field struct? Error compiler, silakan tulis double[int] seolah-olah kita masih di tahun 2022.
Go 1.27 membuat inferensi bekerja di setiap konteks ketika tipe target diketahui secara tidak ambigu:
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: membutuhkan double[int]
8 a := A{double} // 1.26: membutuhkan double[int]
9
10 c := make(chan func(int) int, 1)
11 c <- double // 1.26: membutuhkan double[int]
12
13 var fn func(float64) float64 = double // ini selalu berfungsi
14
15 fmt.Println(s.f(21), a[0](5), (<-c)(7), fn(1.5))
16 // 42 10 14 3
17}
Satu double, diinstansiasi sebagai func(int) int di tiga tempat dan func(float64) float64 di tempat keempat, sepenuhnya dari konteks. Jika Anda menulis struct opsi atau tabel handler yang penuh dengan nilai fungsi, sejumlah noise [T] akan segera hilang dari codebase Anda.
2. Runtime: Performa Gratis dan Detektor Kebocoran
2.1. Alokasi Terspesialisasi Ukuran
Compiler kini mengeluarkan pemanggilan ke rutin alokasi yang terspesialisasi ukuran untuk objek kecil. Release notes mengklaim pengurangan alokasi hingga 30% untuk objek di bawah 80 byte, dan sekitar 1% secara keseluruhan untuk program yang sangat intensif dalam alokasi.
Saya tidak mempercayai release notes tanpa pengukuran, maka:
1type small struct{ a, b, c, d int }
2
3var sink any
4
5func BenchmarkAlloc(b *testing.B) {
6 for b.Loop() {
7 sink = &small{}
8 }
9}
10
11func BenchmarkSlice(b *testing.B) {
12 for b.Loop() {
13 sink = make([]byte, 48)
14 }
15}
1# default (size-specialized malloc aktif)
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
Lebih cepat 30โ35% dalam sebuah microbenchmark yang tidak melakukan apa pun selain alokasi pada Apple Silicon. Dengan kata lain: ini adalah kasus terbaik, dicapai dalam kondisi laboratorium oleh benchmark yang dirancang untuk membuat angka tersebut tampak bagus. Dalam program nyata, GC dan pekerjaan sesungguhnya akan menenggelamkan sebagian besar keuntungan tersebut. Klaim sekitar 1% adalah klaim yang jujur.
Harganya berupa ukuran binary. Hello World:
12413202 bytes (default)
22361826 bytes (nosizespecializedmalloc)
Sekitar 50KB, tetap, tanpa memandang program Anda. Anda dapat menonaktifkannya dengan GOEXPERIMENT=nosizespecializedmalloc, tetapi jalan keluar tersebut dijadwalkan untuk dihapus pada Go 1.28, sehingga perlakukan ia sebagai solusi sementara untuk laporan bug, bukan sebagai gaya hidup.
2.2. Goroutine Leak Profile Kini Benar-benar Tersedia
Bersifat eksperimental pada Go 1.26, dan tersedia secara umum pada 1.27. GOEXPERIMENT goroutineleakprofile telah dihapus; fitur ini langsung berfungsi.
1func leak() {
2 ch := make(chan int) // tidak akan pernah ada yang mengirim. tidak akan pernah.
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
Tiga goroutine, terjebak secara permanen, dengan nama file dan nomor baris yang menunjuk tepat ke lokasi di mana Anda membuatnya.
Trik di balik fitur ini sangat cerdas: ia menggunakan ulang analisis keterjangkauan (reachability) milik garbage collector. Jika goroutine G diblokir pada primitif P, dan P tidak dapat dijangkau dari goroutine mana pun yang dapat dijalankan (atau dari apa pun yang dapat dibangunkan oleh goroutine tersebut), maka tidak ada yang dapat menyentuh P lagi, sehingga G tidak akan pernah terbangun. Ini bukan heuristik, melainkan sebuah pembuktian.
Desain yang sama juga memberikan batasannya secara gratis: jika channel atau mutex dapat dijangkau melalui variabel global, atau melalui variabel lokal dari suatu goroutine yang masih berjalan, GC tetap dapat melihatnya, sehingga runtime tidak dapat menyimpulkan apa pun. Perhatikan bahwa bahkan contoh sederhana di atas memerlukan dua pemanggilan runtime.GC() agar dapat dilaporkan. Fitur ini tidak akan menangkap semuanya.
Namun, fitur ini akan menangkap kasus klasik "lupa membatalkan context, goroutine worker hidup selamanya", yang merupakan sebagian besar kebocoran dalam kebanyakan kasus.
Jika Anda mengimpor net/http/pprof, profil ini juga tersedia di /debug/pprof/goroutineleak. Pasang endpoint tersebut di staging, periksa setiap minggu, dan rasakan kengerian yang diam-diam.
Pekerjaan ini dikontribusikan oleh Vlad Saioc dari Uber, yang layak mendapatkan minuman pilihannya sendiri.
2.3. Traceback Kini Memberi Tahu Anda Request Mana yang Gagal
Untuk modul yang mendeklarasikan Go 1.27 atau yang lebih baru, baris header traceback kini menyertakan goroutine label dari 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...
Detail yang penting: label diwarisi oleh goroutine anak. Goroutine 3 tidak pernah menetapkan label. Ia menerimanya dari induknya.
Jadi, ketika production mengalami deadlock berikutnya dan seseorang mengirim SIGQUIT ke proses tersebut, alih-alih 4.000 goroutine yang semuanya tampak identik, Anda akan mendapatkan 4.000 goroutine yang diberi tag request dan tenant mana yang menjadi miliknya. Tiga baris pprof.Do pada titik masuk request Anda sudah cukup untuk mendapatkan hal tersebut.
Karena label dapat berisi hal-hal yang tidak ingin Anda tampilkan ke stderr, GODEBUG=tracebacklabels=0 akan menonaktifkannya, dan opsi nonaktif ini secara eksplisit dimaksudkan untuk tetap ada selamanya.
2.4. asynctimerchan Telah Dihapus Selamanya
Go 1.23 membuat channel timer menjadi unbuffered (sinkron) dan menyediakan asynctimerchan=1 sebagai cara untuk kembali ke perilaku lama. Pada 1.27, pengaturan tersebut dihapus secara permanen. Channel pada paket time bersifat sinkron, titik, tanpa negosiasi.
Bagian yang menarik adalah kebijakan yang diperkenalkan bersamaan dengan perubahan ini. GODEBUG yang sudah dihapus namun masih tertinggal di go.mod Anda tidak akan secara otomatis merusak build. Ia hanya akan merusak build jika diatur ke nilai lama:
1# go.mod berisi: 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 berisi: godebug asynctimerchan=0
6(build berhasil)
Artinya, satu-satunya pihak yang akan diperingatkan adalah mereka yang benar-benar bergantung pada perilaku yang telah dihapus. Semua orang yang mengatur nilai tersebut ke nilai default akhirnya dan kemudian melupakannya tetap dapat terus tidak memikirkannya. Ini adalah bagian arkeologi API yang bijaksana.
3. Standard Library
3.1. encoding/json/v2: Mesin Diganti di Tengah Penerbangan
Ini adalah perubahan besar. Diskusi proposal selama lebih dari dua tahun akhirnya terealisasi.
Kini terdapat tiga paket, dan memahami pembagiannya adalah sebagian besar dari perjuangan ini:
| Paket | Fungsi |
|---|---|
encoding/json | API v1 yang sudah Anda kenal. Perilaku 100% tidak berubah. Kini diimplementasikan di atas v2 |
encoding/json/v2 | Pemrosesan semantik. Nilai Go โ JSON |
encoding/json/jsontext | Pemrosesan sintaksis. JSON sebagai aliran token |
Inti dari perubahan ini adalah bahwa encoding/json dibangun ulang di atas implementasi yang sepenuhnya berbeda dan berperilaku identik. Caranya sebagai berikut:
1// encoding/json.Unmarshal milik Go 1.27, disingkat
2func Unmarshal(data []byte, v any) error {
3 return jsonv2.Unmarshal(data, v, DefaultOptionsV1())
4}
Setiap keunikan legacy v1 dikodekan sebagai sebuah opsi, dikemas dalam DefaultOptionsV1(), dan API v1 selalu menerapkan paket tersebut. Artinya, kode berikut berperilaku sama di 1.26 dan 1.27:
1var m map[string]int
2json.Unmarshal([]byte(`{"a":1,"a":2}`), &m) // <nil>, map[a:2]
Ya, v1 masih diam-diam menerima key duplikat dan mengambil yang terakhir. Hal itu selalu terjadi dan masih terjadi hingga kini. Kompatibilitas berarti kompatibel juga dengan bagian yang buruk.
Sementara itu, v2 memiliki pendapatnya sendiri:
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
Key duplikat dan UTF-8 yang tidak valid ditolak. Keduanya benar-benar berbahaya, bukan sekadar berantakan: ketika dua parser tidak sepakat mengenai key duplikat mana yang menang, muncullah bug keamanan. CVE-2017-12635 pada CouchDB adalah contoh persis dari hal ini: sebuah body JSON dengan dua key roles, di mana validator membaca yang satu dan lapisan penyimpanan membaca yang lain. Menolak adalah keputusan yang tepat.
Opsi bersifat variadic:
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// key map terurut + indentasi
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// tolak field yang tidak dikenal, sebelumnya memerlukan Decoder dan 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: sebagian besar hal yang sebelumnya Anda selesaikan dengan library pihak ketiga atau MarshalJSON yang ditulis tangan kini tersedia sebagai flag.
Dan bagian terbaik adalah cerita migrasinya. Opsi yang datang belakangan akan menang, sehingga Anda dapat mengadopsi ketatnya v2 satu perilaku demi satu perilaku:
1// pertahankan semantik v1, tetapi tolak key duplikat seperti yang dilakukan v2
2jsonv2.Unmarshal(data, &v,
3 json.DefaultOptionsV1(),
4 jsontext.AllowDuplicateNames(false))
5// duplicate object member name
Anda tidak perlu memindahkan codebase berukuran 200 ribu baris ke v2 hanya untuk berhenti menerima key duplikat. Anda cukup mengubah satu opsi. Untuk codebase yang besar, inilah jalur yang realistis.
Performa: marshaling kurang lebih setara, unmarshaling jauh lebih cepat. Jika terjadi masalah, GOEXPERIMENT=nojsonv2 akan mengembalikan implementasi lama, dan opsi nonaktif tersebut pada akhirnya juga akan dihapus, jadi ajukan issue alih-alih terus bergantung padanya.
jsontext adalah lapisan tingkat rendah: Encoder/Decoder yang menelusuri JSON sebagai Token dan Value dengan state machine yang menjaga Anda tetap jujur. Gunakan ini ketika Anda menulis transformer streaming atau filter JSON dan tidak ingin mewujudkan nilai Go sama sekali.
3.2. Paket uuid Standar
Akhirnya. RFC 9562, berada di standard library, tanpa perlu go get.
1import "uuid"
2
3func main() {
4 fmt.Println(uuid.NewV4())
5 // b97aa695-da08-472c-af81-ff088129019f
6
7 // v7: 48 bit teratas adalah timestamp, sehingga nilai ini selalu terurut berdasarkan waktu pembuatan
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}
Seluruh API terdiri dari New, NewV4, NewV7, Nil, Max, Parse, MustParse, dan satu tipe: type UUID [16]byte. Hanya itu. Anda dapat membaca seluruh dokumentasi paket sementara kopi Anda mendingin.
Detail yang layak diketahui:
UUIDadalah[16]byte, sehingga==dapat digunakan dan ia langsung dapat dipakai sebagai key map. Desainnya sama dengangoogle/uuid.NildanMaxadalah fungsi, bukan variabel. Karena sebuahvar Nil UUIDdi level paket adalah senjata berisi yang diarahkan ke kaki Anda sendiri, dan suatu saat seseorang di suatu tempat pasti akan menugaskan nilai ke variabel tersebut.- Bit acak berasal dari generator yang aman secara kriptografis.
- Ia mengimplementasikan
encoding.TextMarshaler/TextUnmarshaler/TextAppender, sehingga langsung cocok digunakan pada struct JSON. NewV7adalah pilihan yang tepat untuk primary key basis data. Pengurutan berdasarkan waktu berarti indeks B-tree Anda tidak lagi terfragmentasi, sesuatu yang terkenal menjadi kelemahan UUID v4.
Sekarang Anda dapat menghapus sebuah dependensi. Jika Anda membutuhkan v1/v3/v5 atau opsi parsing yang lebih canggih, library pihak ketiga masih memiliki perannya.
3.3. crypto/mldsa: Tanda Tangan Pasca-Kuantum, dan Ukurannya Sangat Besar
Go 1.24 memberi kita crypto/mlkem untuk pertukaran kunci pasca-kuantum. Go 1.27 membawa separuh lainnya: tanda tangan ML-DSA, yang distandarisasi sebagai 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
Perhatikan angka-angka tersebut. Satu tanda tangan berukuran 3.309 byte. Ed25519 berukuran 64. Itu peningkatan 50 kali lipat, dan public key-nya hampir 2KB di atasnya. Jika Anda menyisipkan beberapa tanda tangan ini ke dalam certificate chain, handshake TLS Anda akan memerlukan strategi MTU tersendiri.
Inilah biaya nyata dari ketahanan kuantum saat ini, dan inilah alasan mengapa belum ada yang beralih menggunakan semuanya besok. Namun, fitur ini sudah berada di standard library, dan di sanalah seharusnya ia berada sebelum Anda membutuhkannya.
Options.Context adalah pemisahan domain: tandatangani dengan kunci yang sama untuk tujuan yang berbeda, gunakan context yang berbeda untuk masing-masing, dan tanda tangan dari satu context tidak akan dapat diverifikasi pada context lain. Contoh di atas menunjukkan hal itu dengan tepat: kunci yang sama, pesan yang sama, context yang berbeda, ditolak.
PrivateKey mengimplementasikan crypto.Signer, sehingga dapat langsung dipasang pada interface yang sudah ada. crypto/x509 menangani kunci dan tanda tangan ML-DSA, dan crypto/tls mendukung skema tanda tangan MLDSA44/MLDSA65/MLDSA87 pada TLS 1.3.
Terdapat juga SignDeterministic, yang melewati pembangkitan acak, berguna untuk pengujian dan build yang dapat direproduksi.
3.4. simd: Instruksi Vektor Tanpa Assembly
Go 1.26 memperkenalkan simd/archsimd yang spesifik untuk arsitektur sebagai eksperimen. Go 1.27 menambahkan simd, yang portabel dan agnostik terhadap lebar vektor. Aktifkan dengan 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 // tangani sisa elemen dengan load/store parsial, tanpa loop skalar tambahan
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]
Pilihan desain yang penting: lebar vektor tidak pernah di-hardcode. Anda menanyakan va.Len() saat runtime. VectorBitSize() memberi tahu lebar sebenarnya, dan Emulated() memberi tahu apakah Anda mendapatkan perangkat keras sungguhan atau peniruan perangkat lunak yang sopan. Mac saya melaporkan NEON 128-bit; mesin AVX-512 melaporkan lebih lebar; mesin tanpa dukungan apa pun melaporkan emulasi dan kode tetap berjalan.
LoadFloat32sPart/StorePart layak mendapat perhatian khusus. Bagian paling menjengkelkan dari SIMD yang ditulis tangan selalu adalah sisa elemen di ujung array, dan fitur ini menanganinya tanpa loop skalar terpisah.
Masih bersifat eksperimental, API-nya masih tidak stabil, jangan gunakan di production. Namun, fakta bahwa ini adalah kode Go dan bukan assembly atau cgo merupakan hal yang sangat besar.
3.5. hash/maphash.Hasher
Sebuah interface baru yang mendeskripsikan kontrak antara tipe nilai dan container berbasis hash:
1type Hasher[T any] interface {
2 Hash(*Hash, T)
3 Equal(x, y T) bool
4}
Mengapa? Map bawaan Go hanya menerima key yang comparable. Anda tidak dapat menggunakan slice sebagai key. Anda tidak dapat mendefinisikan "sama dengan mengabaikan huruf besar-kecil". Hasher menyelesaikan kedua masalah tersebut sekaligus:
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) // seed yang sama, atau semua ini tidak bermakna
15 hr.Hash(&h, v)
16 return h.Sum64()
17}
18
19fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
20// true
Untuk semantik == yang biasa, tersedia ComparableHasher[T]:
1hashOf(maphash.ComparableHasher[int]{}, 42)
Jebakannya: Hasher adalah sebuah interface, bukan struktur data. Belum ada tabel hash atau Bloom filter standar yang menggunakannya. Ini merupakan fondasi untuk paket container di masa depan. Saat ini Anda dapat menggunakannya ketika membangun struktur sendiri, atau mengonsumsi implementasi yang sudah ada seperti go/types.Hasher (yang memungkinkan Anda menggunakan types.Type sebagai key map, dengan menghormati Identical).
Pengelolaan seed menjadi tanggung jawab Anda. Jika hashOf di atas membuat seed baru pada setiap pemanggilan, nilai yang identik akan menghasilkan hash yang berbeda dan contoh tersebut akan mencetak false, yaitu bug yang persis saya tulis pada percobaan pertama saya. Satu seed untuk setiap container. (Seed diacak untuk menggagalkan serangan DoS hash-flooding, itulah sebabnya ia bukan sekadar konstanta.)
3.6. httptest.NewTestServer + synctest.Sleep: Pukulan Sleeper
Jika Anda hanya mengadopsi satu hal dari rilis ini, jadikan yang ini.
testing/synctest lulus dari status eksperimental pada Go 1.25 dan memberi kita jam palsu untuk menguji kode konkuren. Namun, begitu test Anda menyentuh jaringan sungguhan, ilusinya runtuh. httptest.NewTestServer pada Go 1.27 menggunakan jaringan palsu dalam memori, sehingga bubble tetap utuh. Dan synctest.Sleep (= time.Sleep + synctest.Wait) melengkapinya.
Berikut ini adalah test retry dengan 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 // backoff seharusnya tepat 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)
Bacalah dua kali. Test ini mensimulasikan tiga detik backoff terhadap server HTTP sungguhan, dan selesai dalam 0,00 detik. Dan assertion-nya adalah elapsed == 3*time.Second, tepat tiga detik, bukan "setidaknya tiga detik, kurang lebih karena jitter scheduler". Jam palsu tidak mengalami jitter.
synctest.Sleep ada karena alasan yang spesifik: jika test Anda tidur selama durasi yang sama dengan kode yang diuji, siapa yang bangun lebih dulu adalah tebakan siapa pun. synctest.Sleep tidur lalu menunggu hingga setiap goroutine lain di dalam bubble benar-benar terblokir secara permanen, sehingga Anda mengamati sistem setelah ia stabil.
Timeout, retry, circuit breaker, rate limiter: setiap test client HTTP yang bergantung pada waktu dan Anda miliki dapat menjadi cepat dan deterministik. Jika test suite Anda saat ini ditopang oleh time.Sleep(100 * time.Millisecond) dan harapan, inilah jalan keluar Anda.
3.7. Perubahan net/http yang Benar-benar Memengaruhi Production
Tenang, tetapi perubahan ini akan terlihat pada metrik Anda.
Body response HTTP/1 dikosongkan otomatis saat Close. Konten yang belum dibaca kini dikosongkan (hingga batas konservatif) ketika Anda menutup body, sehingga koneksi dapat digunakan kembali. Artinya, Anda akhirnya dapat menghapus mantra yang sudah disalin oleh semua orang dari jawaban Stack Overflow yang sama sejak 2016:
1// tidak lagi diperlukan
2defer func() {
3 io.Copy(io.Discard, resp.Body)
4 resp.Body.Close()
5}()
Bagi kebanyakan program, perubahan ini tidak berpengaruh atau memberikan sedikit keuntungan. Jika justru memperburuk keadaan, kemungkinan besar Anda berada dalam kondisi yang dijelaskan secara halus oleh release notes: Transport.MaxIdleConns diatur ke 0, atau Client baru untuk setiap request, sehingga melewati batas koneksi idle sepenuhnya. Transport.DisableKeepAlives = true dapat menutupinya, tetapi saran sebenarnya dari release notes adalah bahwa "pemeriksaan yang lebih mendalam kemungkinan akan bermanfaat", yang dalam bahasa tim Go berarti Anda memiliki masalah yang lebih besar.
Prioritas klien HTTP/2 (RFC 9218). Server kini menghormati sinyal prioritas dari klien. Jika Anda lebih menyukai penjadwalan round-robin lama, gunakan Server.DisableClientPriority = true.
Server.MaxHeaderValueCount. Membatasi berapa banyak nilai header yang akan diterima server, dengan nilai default DefaultMaxHeaderValueCount. Satu pintu lagi ditutup untuk skenario "kirim sepuluh ribu header dan lihat apa yang terjadi".
ALPN pada koneksi yang disediakan pengguna. Jika net.Conn Anda mengimplementasikan ConnectionState() tls.ConnectionState, Transport dan Server akan melakukan negosiasi TLS ALPN padanya, sehingga dialer kustom yang melewati proxy tetap dapat menegosiasikan HTTP/2.
3.8. Hal-hal Kecil yang Menyenangkan
strings.CutLast / bytes.CutLast. Cut memisahkan pada pemisah pertama. Tidak ada varian "terakhir", sehingga semua orang menulis sendiri LastIndex ditambah slicing, dan sekitar 30% dari kita melakukan off-by-one pada percobaan pertama.
1name, ext, ok := strings.CutLast("archive.tar.gz", ".")
2// archive.tar gz true
Sangat berguna untuk ekstensi file dan untuk mengurai host:port, di mana Anda harus menemukan titik dua terakhir, karena alamat IPv6 penuh dengan titik dua.
math/big.Int.Divide. Pembagian dengan mode pembulatan yang eksplisit, menggantikan undian koin "apakah saya menginginkan Quo atau Div?":
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
Jika Anda bekerja di domain di mana aturan pembulatan tertulis dalam sebuah regulasi, fitur ini untuk Anda.
url.URL.Clone dan url.Values.Clone. Salinan dalam (deep copy). Salinan dangkal *u yang lama berbagi pointer Userinfo, yang menghasilkan jenis bug yang membutuhkan waktu sehari untuk ditemukan dan lima detik untuk diperbaiki.
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
Metode read pada net.UnixConn kini mengembalikan io.EOF secara langsung, alih-alih membungkusnya dalam net.OpError. err == io.EOF kini berfungsi, seperti seharusnya sejak awal.
database/sql.ConvertAssign dan driver.RowsColumnScanner. Fitur untuk penulis driver. Yang pertama mengekspos konversi tipe yang dilakukan oleh Rows.Scan; yang kedua memungkinkan driver melakukan scan langsung ke tujuan milik pengguna, melewati alokasi perantara.
unicode 15 โ 17. Dua versi dalam satu lompatan. Perilaku klasifikasi dan normalisasi string dapat berubah secara halus. Jika Anda memiliki test yang bergantung padanya, Anda akan mengetahuinya.
compress/flate menjadi lebih cepat, dan byte output-nya mungkin berbeda dari Go 1.26. Hal ini berdampak pada archive/zip, compress/gzip, compress/zlib, dan image/png. Jika Anda memiliki golden-file test yang melakukan hash terhadap output terkompresi, test tersebut akan gagal, dan itu bukan regresi. Periksa hal ini sebelum Anda melakukan upgrade, bukan saat meninjau insiden.
4. Toolchain
4.1. go fix Mendapatkan Lebih Banyak Modernizer
go fix diam-diam berkembang menjadi alat modernisasi kode. Go 1.27 menambahkan atomictypes, embedlit, slicesbackward, dan unsafefuncs. Gunakan -diff untuk melihat pratinjau:
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 }
Empat modernizer aktif pada satu file kecil:
embedlit: menulis ulang literal embedded menggunakan sintaks baru dari ยง1.2atomictypes:atomic.AddInt64(&x, 1)menjadiatomic.Int64.Add(1), yang membuat akses non-atomik menjadi tidak mungkin dan diam-diam memperbaiki bug alignment 32-bit yang tidak Anda ketahui keberadaannyaslicesbackward: loop mundur menjadislices.Backwardwaitgroupgo: ritualAdd/go/Donemenjadiwg.Go(diganti namanya dariwaitgrouppada 1.26 untuk menghindari ambiguitas)
go tool fix help mencantumkan seluruh 26 modernizer; go tool fix help <name> menjelaskan satu per satu. Selain itu, fmtappendf dihapus "karena alasan gaya", yang merupakan cara yang sangat diplomatis untuk menggambarkan apa pun yang terjadi dalam thread issue tersebut.
Menjalankan go fix ./... pada codebase lama adalah sore hari yang sangat memuaskan. Tentu saja, baca diff terlebih dahulu.
4.2. go test Menjalankan stdversion secara Default
Ini adalah perubahan yang paling mungkin mengganggu hari Anda.
go test kini menjalankan pemeriksaan vet stdversion secara default, dengan menandai simbol standard library yang lebih baru daripada yang diizinkan oleh direktif go pada go.mod Anda:
1// go.mod berisi: go 1.24
2package sv
3
4import "strings"
5
6func Ext(name string) string {
7 _, ext, _ := strings.CutLast(name, ".") // CutLast adalah simbol 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]
Ini menghilangkan mode kegagalan klasik di mana semuanya berfungsi di mesin Anda (toolchain terbaru) tetapi meledak di CI atau di lingkungan lama pengguna. Jika Anda mempublikasikan library dengan direktif go yang konservatif, ini adalah hadiah.
4.3. go test -json Mendapatkan OutputType
Baris "Action":"output" kini membawa field opsional "OutputType": "error", "error-continue", atau "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'
Output t.Log (tanpa field), output t.Error (error), dan baris yang dihasilkan framework (frame) kini dapat dibedakan. Siapa pun yang pernah menulis regex untuk mengetahui baris mana dari output test yang "nyata" kini dapat menghapusnya.
4.4. Peningkatan go doc
Sintaks package@version. Membaca dokumentasi untuk versi tertentu tanpa menambahkannya ke modul Anda:
1$ go1.27rc3 doc golang.org/x/sync/errgroup@v0.10.0
2package errgroup // import "golang.org/x/sync/errgroup"
3...
Berguna untuk memeriksa apa yang berubah sebelum melakukan upgrade.
-ex dan pencetakan source contoh:
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
Source contoh dan output yang diharapkan, di terminal, tanpa membuka tab browser yang masih akan terbuka pada Kamis depan.
4.5. go mod tidy Merapikan Blok require Anda
Untuk modul dengan go 1.27 atau yang lebih baru, go mod tidy menggabungkan blok require yang tersebar menjadi paling banyak dua (direct dan indirect), sambil mempertahankan komentar yang melekat.
1// Sebelum
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// Sesudah: 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)
Perhatikan bahwa komentar // networking tetap dipertahankan. Ini sebagian besar membereskan sisa resolusi konflik merge Git, tempat blok require yang tercecer biasanya lahir. Jika tim Anda beranggotakan lebih dari tiga orang yang menambahkan dependensi, konflik pada go.mod Anda akan menjadi jauh lebih sunyi.
4.6. Lain-lain
- Dukungan
bzrdihapus. Jika ini memengaruhi Anda, saya dengan tulus ingin mendengar ceritanya. go tool trace -httphanya terikat ke localhost ketika hanya diberi port.-http=:6060tidak lagi mendengarkan di semua antarmuka. Gunakan-http=0.0.0.0:6060jika memang itu yang Anda maksud. Ini kini sejalan dengango tool pprof, dan mencegah profiler publik yang tidak disengaja.- Response file (
@file) didukung olehcompile,link,asm,cgo,cover, danpack, dalam format yang kompatibel dengan GCC. Untuk sistem build yang melampaui batas panjang command-line: halo, Bazel.
5. Compiler, Linker, Port
Compiler. Nama file relatif dalam direktif //line kini di-resolve terhadap direktori file yang memuatnya, sesuai dengan go/scanner. Relevan jika Anda menulis code generator.
Nama simbol function literal (closure) juga kini lebih sederhana: nama yang sama terlepas dari inlining, dan beberapa instance dari literal yang sama dapat berbagi kode di dalam binary. Tidak ada perubahan fungsional, kecuali: kode yang membandingkan identitas fungsi melalui reflect.Value.Pointer akan melihat "sama" lebih sering dari sebelumnya. Perbandingan itu memang tidak pernah valid, tetapi jika Anda memilikinya, inilah saatnya ia mulai berbohong dengan lebih lantang.
Linker. Opsi baru -macos dan -macsdk mengatur versi OS dan SDK pada load command LC_BUILD_VERSION macOS.
Port.
- Darwin kini memerlukan macOS 13 Ventura atau yang lebih baru, sebagaimana diumumkan pada 1.26. Periksa runner CI Anda.
- PowerPC (
GOOS=linux GOARCH=ppc64) beralih ke ABI ELFv2. Memerlukan kernel Linux 3.13+ (RHEL7 dengan backport ke 3.10). Cgo, PIE, dan external linking kini didukung. Jika Anda menggunakan cgo tetapi memerlukan binary pure-Go yang statis, aturCGO_ENABLED=0.
6. Daftar Periksa Upgrade
Go 1.27 sangat serius dalam hal kompatibilitas, tetapi periksa hal-hal berikut sebelum Anda menaikkan versi:
- Golden-file test pada output terkompresi.
compress/flatemengubah encoder-nya; byte gzip/zip/png mungkin berbeda. - Test yang mencocokkan string pesan error JSON. Perilakunya identik; teks error-nya tidak.
- GODEBUG yang dihapus di
go.mod.asynctimerchan,gotypesalias,tlsrsakex,tls3des,tls10server,tlsunsafeekm,x509keypairleaf: ini hanya menggagalkan build jika diatur ke nilai lama. - Pelanggaran
stdversion.go testkini mendeteksinya. Jalankan lebih awal jika Anda memelihara library dengan direktifgoyang konservatif. - Runner CI macOS 12 atau yang lebih lama. Dukungan sudah dihentikan.
- Test yang memeriksa nama simbol function literal, dan perbandingan fungsi
reflect.Value.Pointer. -
Transport.MaxIdleConns = 0atauClientuntuk setiap request. Dikombinasikan dengan pengosongan otomatis body response, hal ini mungkin menjadi lebih lambat.
Pemikiran Penutup
Go 1.27 adalah rilis di mana banyak pemeliharaan yang tertunda jatuh tempo sekaligus.
Generic methods adalah bagian yang hilang dari pekerjaan generics 1.18. Pemilih field pada struct literal menutup issue berumur sebelas tahun. encoding/json/v2 merupakan hasil dari dua tahun diskusi proposal. Dan uuid mengisi celah yang hampir setiap proyek Go tambal selama satu dekade dengan dependensi pihak ketiga yang sama.
Jika Anda menginginkan urutan prioritas untuk benar-benar memperoleh manfaat dari rilis ini:
httptest.NewTestServer+synctest.Sleep: test HTTP yang bergantung pada waktu menjadi cepat dan deterministik. Rasio usaha terhadap hasil terbaik di seluruh rilis ini.- Goroutine leak profile: ekspos satu endpoint di staging dan bersiaplah untuk direndahkan.
- Label goroutine pada traceback: tiga baris
pprof.Dopada titik masuk request Anda mengubah dump stack pukul 3 pagi berikutnya menjadi sesuatu yang dapat dibaca. go fix ./...: biarkan alat ini melakukan modernisasi.encoding/json/v2: tidak perlu terburu-buru. Adopsi satu opsi demi satu opsi.
Ambil go1.27rc3 dan jalankan test suite Anda terhadapnya sekarang. Setiap item pada daftar periksa itu jauh lebih menyenangkan untuk ditemukan pada sore hari Selasa daripada saat insiden.
Referensi