Mert Özen Her Satırda Daha İleri
Backend Go ile Backend — #14

Go 1.22'den 1.27'ye: Loop Variable, Routing, range-over-func, Green Tea GC, json/v2 ve Generic Method'lar

Mert Özen 15 Kas 2026 15 dk 4 görüntülenme
Go 1.22'den 1.27'ye: Loop Variable, Routing, range-over-func, Green Tea GC, json/v2 ve Generic Method'lar

Go'da bir sürümün davranışını go.mod'daki go satırı ve GODEBUG ayarları belirler; ama kaldırılan ayarlar bu korumayı bitirir: Go 1.27 asynctimerchan'ı kaldırdığı için go 1.22 diyen bir module'ün timer davranışı sadece toolchain yükseltilerek değişti. 1.22'den 1.27'ye backend için önemli değişiklikler, nil check düzeltmesi, os.Root, uuid, yeni allocator ölçümü, json/v2'ye geçişin istemcilere etkisi ve go fix modernizer'ları.

Sürüm notu: Bu yazıdaki kodlar Go 1.27.4 ile derlenip çalıştırılmıştır; Bookstore module'ünün go satırı 1.26.0'dır ve kod Go 1.26.11 ile de derlenir. Karşılaştırmalar için Go 1.26.11 ve Go 1.24.13 toolchain'leri GOTOOLCHAIN ile ayrıca çalıştırıldı. Sadece Go 1.27'de olan package'ları kullanan lab'lar //go:build go1.27 ile ayrılmıştır. Sürüm bilgileri go.dev'deki release notes'lardan ve Go 1.27.4'ün kaynak kodundan doğrulandı (Kasım 2026).

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) eski bir yardımcı servisi var; go.mod'u go 1.22 diyor ve içinde bir timer'ı yeniden kuran şu kod duruyor:

package main

import (
    "fmt"
    "time"
)

func main() {
    t := time.NewTimer(time.Millisecond)
    time.Sleep(5 * time.Millisecond)
    t.Reset(time.Hour)
    select {
    case <-t.C:
        fmt.Println("stale value from the old timer")
    default:
        fmt.Println("no stale value")
    }
}

Kodda ve go.mod'da tek karakter değiştirmeden, sadece toolchain'i Go 1.26.11'den Go 1.27.4'e yükseltiyorsun. Çıktı aynı kalır mı?

$ GOTOOLCHAIN=go1.26.11 go run .
stale value from the old timer
$ go run .     # Go 1.27.4
no stale value

Değişti. Go 1.23 timer channel'larını senkron hâle getirdi: Reset ya da Stop'tan sonra channel'da eski timer'dan kalan bir değer okunmuyor. Ama bu davranış asynctimerchan adlı bir GODEBUG (runtime'ın ve standart kütüphanenin davranışını değiştiren ayarlar) ayarına bağlıydı ve go satırı 1.23'ten küçük module'lerde eski hâli varsayılandı. Go 1.27 bu ayarı tamamen kaldırdı; artık go satırı ne derse desin herkes yeni davranışı alıyor. Go'nun uyumluluk sözü güçlü ama sonsuz değil. Bu yazıda Go 1.22'den 1.27'ye backend geliştiricisinin bilmesi gereken değişikliklere ve bir yükseltmenin nasıl yapılacağına bakıyoruz: go satırı, GODEBUG, go fix ve 10'da söz verdiğimiz json/v2'ye geçiş.

go Satırı, GODEBUG ve Toolchain

Go'nun uyumluluk kuralı Go 1 sürümünden beri aynı: Go 1 için yazılmış kod sonraki Go 1.x sürümleriyle derlenmeye devam eder. Davranış değiştirmek gerektiğinde (güvenlik, hata düzeltme) Go bunu GODEBUG ayarlarıyla yapıyor ve hangi ayarın varsayılan olacağına module'ün go.mod'undaki go satırına bakarak karar veriyor. 13'teki go version -m çıktısında gördüğümüz satır bunun sonucu:

build   DefaultGODEBUG=tracebacklabels=0,x509sslcertoverrideplatform=0

Bookstore Go 1.27.4 ile build ediliyor ama go satırı 1.26.0. Bu yüzden 1.27'nin iki yeni davranışı bu binary'de kapalı: stack trace başlıklarında pprof label'larının görünmesi ve Windows ile macOS'te SSL_CERT_FILE/SSL_CERT_DIR ortam değişkenlerinin sertifika havuzunu değiştirmesi. Bir kopya üzerinde go mod edit -go=1.27.0 yapıp yeniden build ettiğimde DefaultGODEBUG satırı tamamen kayboldu. Tek bir ayarı go.mod'da godebug satırıyla ya da main package'ında //go:debug yorumuyla değiştirebilirsin.

Kaldırılan ayarlar ise bu mekanizmanın sınırı. Önce Tahmin Et'teki koda //go:debug asynctimerchan=1 ekleyince Go 1.26.11 eski davranışı verdi, Go 1.27.4 build'i durdurdu:

$ go run .
# timerdemo
./main.go:1:1: invalid //go:debug: removed GODEBUG "asynctimerchan" set to old value "1" (https://go.dev/doc/godebug#go-127)

Aynı ayarı go.mod'a godebug asynctimerchan=1 olarak yazınca hata go.mod:5'i gösterdi. Release notes'a göre kaldırılmış bir ayar artık sadece son varsayılan değeriyle yazılabiliyor. Hangi ayarın hangi sürümde eklenip kaldırıldığının listesi go.dev'deki GODEBUG sayfasında.

go satırının bir görevi daha var: o module'ü derlemek için gereken en düşük Go sürümünü söylüyor. Daha eski bir toolchain böyle bir module'le karşılaşırsa GOTOOLCHAIN ayarına göre yeni toolchain'i indirip onunla çalışıyor; bu seride Go 1.26.11 ile derleme kontrolünü de bu şekilde yaptım. Altı sürümün backend açısından önemli değişiklikleri:

SürümTarihBackend için öne çıkanlar
1.22Şubat 2024döngü değişkeni her iterasyonda yeni, range ile tam sayı, method ve wildcard'lı ServeMux, math/rand/v2, sql.Null[T]
1.23Ağustos 2024range-over-func iterator'lar ve iter, unique, senkron timer channel'ları
1.24Şubat 2025generic type alias, tool directive, os.Root, b.Loop, t.Context, Swiss Table map'ler, weak, omitzero
1.25Ağustos 2025container-aware GOMAXPROCS, testing/synctest, WaitGroup.Go, CrossOriginProtection, nil check düzeltmesi, json/v2 ve Green Tea deneyleri
1.26Şubat 2026new(expr), Green Tea GC (garbage collector; kullanılmayan belleği toplayan bileşen) varsayılan, errors.AsType, yeni go fix, slog.NewMultiHandler
1.27Ağustos 2026generic method'lar, encoding/json/v2, uuid, crypto/mldsa, goroutineleak profili, size-specialized allocator, macOS 13+

Go her 6 ayda bir major sürüm çıkarıyor ve bir sürümü, iki yeni major çıkana kadar destekliyor. Kasım 2026'da desteklenenler 1.26 ve 1.27; 1.25, 1.27'nin çıktığı Ağustos'ta destek dışında kaldı. Şubat 2027'de 1.28 gelince 1.26'nın desteği bitecek. .NET'in yıllık sürüm ve LTS (Long Term Support; uzun süre desteklenen sürüm) ile STS (Standard Term Support; daha kısa desteklenen sürüm) modeline göre daha kısa bir döngü bu; pratikte "her altı ayda bir küçük bir yükseltme" demek.

1.22: Döngü Değişkeni, ServeMux ve math/rand/v2

1.22'nin en önemli değişikliği, for döngüsünün değişkenini her iterasyonda yeniden oluşturması. Önceden bütün iterasyonlar tek bir değişkeni paylaşıyordu ve döngüde oluşturulan closure'lar ya da goroutine'ler hepsi son değeri görüyordu. Bu değişiklik de go satırına bağlı; bir dosyanın başına //go:build go1.21 yazmak o dosyanın dil sürümünü 1.21'e indiriyor. labs/14-loopvar aynı fonksiyonu iki dosyada çalıştırıyor:

// titles her kitap için bir closure toplar ve döngüden sonra çağırır.
func titles() []string {
    var fs []func() string
    for _, t := range catalog {
        fs = append(fs, func() string { return t })
    }
    var out []string
    for _, f := range fs {
        out = append(out, f())
    }
    return out
}
$ go run ./labs/14-loopvar
go1.22+: [Dune Emma Middlemarch]
go1.21:  [Middlemarch Middlemarch Middlemarch]

C# 5 aynı değişikliği foreach için 2012'de yapmıştı; Go'da 12'deki tt := tt satırı bu yüzden artık gerekmiyor. go fix'in forvar modernizer'ı bu eski satırları siliyor. 1.22'deki ServeMux pattern'lerini 09'da, sql.Null[T]'yi 11'de kullandık. math/rand/v2 ise global seed'i kaldırdı ve generic rand.N fonksiyonunu getirdi; 1.27'de Rand tipi de aynı adlı bir generic method kazandı.

1.23 ve 1.24: Iterator'lar, Timer'lar, os.Root

1.23'ün iterator'larını 06'da ayrıntılı gördük. Aynı sürümdeki timer değişikliği Önce Tahmin Et'teki davranışın kaynağı; ayrıca Stop edilmemiş timer'lar ve ticker'lar artık referans kalmayınca GC tarafından toplanıyor. unique package'ı aynı değerleri tek bir kopyada tutup karşılaştırmayı pointer karşılaştırmasına indiriyor; çok tekrar eden string'ler (ülke kodu, kategori adı) için.

1.24'te backend için en kullanışlı yenilik os.Root. Kullanıcıdan gelen bir dosya adını filepath.Join(dir, name) ile birleştirmek, name ../ içerdiğinde klasörün dışına çıkmaya izin veriyor; buna path traversal (dosya yolunu manipüle ederek izin verilmeyen dosyalara erişmek) deniyor. os.OpenRoot ile açılan bir kök, kendisinin dışına çıkan her yolu reddediyor:

    root, err := os.OpenRoot(covers)
    if err != nil {
        panic(err)
    }
    defer root.Close()

    for _, name := range []string{"9780441172719.jpg", "../secrets.env"} {
        // os.Open(filepath.Join(covers, name)) ../secrets.env'i sorunsuz açar.
        pf, plainErr := os.Open(filepath.Join(covers, name))
        if plainErr == nil {
            pf.Close()
        }
        f, err := root.Open(name)
        if err == nil {
            f.Close()
        }
        fmt.Printf("%-20s os.Open: %v | root.Open: %v\n", name, errText(plainErr), errText(err))
    }
}
$ go run ./labs/14-osroot
9780441172719.jpg    os.Open: ok | root.Open: ok
../secrets.env       os.Open: ok | root.Open: openat ../secrets.env: path escapes from parent

Kapak resmi servis eden bir endpoint için bu tek satırlık bir değişiklik. Aynı sürümde map'lerin implementasyonu Swiss Table'a geçti (kod değişikliği gerekmiyor), weak package'ı weak pointer'ları getirdi, generic type alias'lar tamamlandı ve 12'de kullandığımız b.Loop ile t.Context geldi. 02'deki tool directive'i ve 10'daki omitzero da 1.24'ten.

1.25 ve 1.26: Container'lar, Testler ve Green Tea

1.25'in container-aware GOMAXPROCS'unu 13'te ölçtük; testing/synctest (08, 12), WaitGroup.Go (08) ve CrossOriginProtection (09) da bu sürümden. Yükseltmede sürpriz yaratabilecek değişiklik ise bir derleyici hatasının düzeltilmesi. Go 1.21'den 1.24'e kadar derleyici bazı nil kontrollerini yanlışlıkla hata kontrolünün sonrasına taşıyordu ve şu kod panic vermeden çalışıyordu:

func coverName() (string, error) {
    f, err := os.Open("covers/9780441172719.jpg")
    name := f.Name() // Go 1.21-1.24: burada panic yok, nil kontrolü aşağıya taşınmıştı
    if err != nil {
        return "", err
    }
    return name, nil
}
$ GOTOOLCHAIN=go1.24.13 go run .
error: open covers/9780441172719.jpg: no such file or directory
$ go run .     # Go 1.27.4
panic: runtime error: invalid memory address or nil pointer dereference

Kod hep yanlıştı: f nil iken f.Name() çağrılıyor. Go 1.25 bunu düzeltti ve artık doğru yerde panic oluyor. Bir servis 1.24'ten yükseltildikten sonra panic vermeye başladıysa, önce hata kontrolünden önce sonucu kullanan satırlara bak; düzeltme hata kontrolünü hemen ilgili satırın altına almak.

1.26'da Green Tea GC varsayılan oldu (13), errors.AsType geldi (05), new artık bir ifade alabiliyor (03'te new(expr)), slog.NewMultiHandler geldi (13) ve go fix baştan yazıldı. go mod init'in bir önceki sürümü yazma davranışı da 1.26'da geldi; 02'de gördüğümüz gibi bu davranış 1.26.1'de geri alındı ve Go 1.27.4'ün go mod init'i go 1.27.4 yazıyor.

1.27: Generic Method'lar, json/v2, uuid ve Yeni Allocator

Generic method'ları 06'da gördük: bir method artık kendi type parameter'ını tanımlayabiliyor, ama interface method'ları hâlâ tanımlayamıyor ve generic bir method bir interface method'unu karşılayamıyor. Aynı yazıda bulduğumuz bir ayrıntıyı tekrar edeyim: fonksiyon tipine atamada genişleyen type inference, generic method'ların aksine go satırına bağlı değil. Go 1.27 toolchain'i bu kodu go 1.26.0 satırıyla da derliyor, Go 1.26.11 ise reddediyor. Kodun eski sürümle de derlenmesi gerekiyorsa CI'da o sürümle de build almak şart.

Standart kütüphaneye UUID (Universally Unique Identifier; çakışma ihtimali pratikte sıfır olan 128 bitlik kimlik) üreten uuid package'ı geldi. Import yolu sadece uuid:

func main() {
    a, b := uuid.NewV7(), uuid.NewV7() // zamana göre sıralı: veritabanı key'i olarak uygun
    fmt.Println(a)
    fmt.Println(b)
    fmt.Println("v7 sorts by creation time:", a.Compare(b) < 0)
    fmt.Println("v4:", uuid.NewV4())

    if _, err := uuid.Parse("ORD-1001"); err != nil {
        fmt.Println("parse:", err)
    }
}
01a1dd17-1756-7800-acad-f6820339f332
01a1dd17-1756-786a-a9e3-f18df7e52c18
v7 sorts by creation time: true
v4: 86a01feb-6fcc-4376-8120-181686d2ef81
parse: invalid uuid

Version 7 UUID'ler ilk 48 bitte zaman damgası taşıdığı için oluşturulma sırasına göre sıralanıyor; veritabanında primary key olarak kullanıldığında yeni satırlar index'in sonuna eklenme eğiliminde. uuid.New() şimdilik NewV4 ile aynı. Bookstore'da şimdiye kadar github.com/google/uuid sadece go-mssqldb'nin dolaylı bağımlılığıydı; yeni kodda standart kütüphaneyi kullanmak bir bağımlılık daha az demek.

Runtime tarafında size-specialized allocator, 80 byte'tan küçük allocation'ları ucuzlatıyor. labs/14-alloc 32 byte'lık bir struct'ı heap'te oluşturuyor; aynı benchmark'ı yeni allocator'ı kapatan GOEXPERIMENT=nosizespecializedmalloc ile de çalıştırdım:

$ go test -run '^$' -bench . -benchmem -cpu 1 -count 3 ./labs/14-alloc
BenchmarkSmallAlloc    116146044     10.41 ns/op    32 B/op    1 allocs/op
BenchmarkSmallAlloc    120671779      9.973 ns/op   32 B/op    1 allocs/op
BenchmarkSmallAlloc    100000000     10.41 ns/op    32 B/op    1 allocs/op
$ GOEXPERIMENT=nosizespecializedmalloc go test -run '^$' -bench . -benchmem -cpu 1 -count 3 ./labs/14-alloc
BenchmarkSmallAlloc     77513540     13.59 ns/op    32 B/op    1 allocs/op
BenchmarkSmallAlloc     97752036     13.45 ns/op    32 B/op    1 allocs/op
BenchmarkSmallAlloc     84756985     13.32 ns/op    32 B/op    1 allocs/op

Bu tek allocation'da fark yaklaşık %23. Release notes allocation'ı yoğun programlarda toplamda %1 civarında bir kazanç bekliyor; bir allocation'ın ucuzlaması programın tamamının o kadar hızlanması demek değil. Bedeli binary boyutu: Bookstore'un Linux binary'si yeni allocator ile 9.216.160, onsuz 9.162.912 byte, yani yaklaşık 52 KB fark (release notes yaklaşık 60 KB diyor). 08'de deneysel olarak kullandığımız goroutineleak profili artık GA (general availability; herkesin kullanımına açık, kararlı sürüm) ve 13'te /debug/pprof/goroutineleak'te gördük. crypto/mldsa, kuantum sonrası dönem için tasarlanmış bir imza algoritmasını (ML-DSA) getiriyor. Go 1.27 macOS'ta en az macOS 13 istiyor; Windows ve Linux/amd64 için bir değişiklik yok.

json/v2'ye Geçiş

10'da Bookstore'un API'sinin v1'de kaldığını ve geçişi 14'te konuşacağımızı söylemiştim. Go 1.27'de v1 artık v2'nin üzerinde çalışıyor; 13'teki CPU profilinde stack'te encoding/json/jsontext'i gördük. Yani geçiş yapmasan da performansın bir kısmını zaten alıyorsun. Asıl soru davranış: v2'nin varsayılanları istemcilere ne yapar? labs/14-jsonv2migrate, 10'daki gövdeleri bugünkü decodeJSON'ın v1 mantığından ve v2 karşılığından geçiriyor:

// decodeV2 aynı işi json/v2 ile yapar. UnmarshalRead girdinin tamamını okur
// ve ilk değerden sonra gelen her şeyi kendisi reddeder.
func decodeV2(body []byte, dst any) error {
    return json.UnmarshalRead(bytes.NewReader(body), dst, json.RejectUnknownMembers(true))
}
body 1
  v1: ok, customerId="ada"
  v2: ok, customerId="ada"
body 2
  v1: ok, customerId="mallory"
  v2: error: cannot unmarshal JSON string into Go main.placeOrderRequest: unknown object member name "CUSTOMERID"
body 3
  v1: ok, customerId="mallory"
  v2: error: jsontext: duplicate object member name "customerId"
body 4
  v1: ok, customerId="ad�a"
  v2: error: jsontext: invalid UTF-8 within "/customerId" after offset 17
body 5
  v1: error: unknown field "coupon"
  v2: error: cannot unmarshal JSON string into Go main.placeOrderRequest: unknown object member name "coupon"
body 6
  v1: error: body must contain a single JSON object
  v2: error: jsontext: invalid character '{' after top-level value after offset 32

Doğru istek (1) iki tarafta da aynı. 2, 3 ve 4'te v1'in kabul ettiği ama kabul etmemesi gereken gövdeleri v2 reddediyor; 10'daki Mallory sorunu v2'de kendiliğinden kapanıyor. 6'da ikinci Decode ile yaptığımız tek değer kontrolüne gerek kalmıyor, çünkü UnmarshalRead girdinin tamamını okuyor. Bedeli 2'deki satır: alan adını farklı büyük/küçük harfle gönderen eski bir istemci artık 400 alıyor. Böyle istemciler varsa v1'in eşleştirmesi bir option ile geri getirilebiliyor ve tekrar eden alan yine reddediliyor:

package main

import (
    "encoding/json/v2"
    "fmt"
    "strings"
)

type req struct {
    CustomerID string `json:"customerId"`
}

func main() {
    for _, body := range []string{`{"CustomerId":"ada"}`, `{"customerId":"ada","CUSTOMERID":"mallory"}`} {
        var dst req
        err := json.UnmarshalRead(strings.NewReader(body), &dst,
            json.RejectUnknownMembers(true),      // DisallowUnknownFields
            json.MatchCaseInsensitiveNames(true), // eski istemciler için v1'in eşleştirmesini koru
        )
        fmt.Printf("%-45s %q %v\n", body, dst.CustomerID, err)
    }
}
{"CustomerId":"ada"}                          "ada" <nil>
{"customerId":"ada","CUSTOMERID":"mallory"}   "ada" jsontext: duplicate object member name "CUSTOMERID"

Hata mesajlarının metni de değişiyor ve Go tip adlarını içeriyor (main.placeOrderRequest); 10'da istemciye ham decoder mesajı göndermenin sorun olduğunu söylemiştik, v2'ye geçişte bu iş artık ertelenemez. Bookstore için plan şu: Go 1.26 desteği gerektiği sürece v1'de kal; 1.28 çıkıp 1.26 destek dışı kalınca go satırını 1.27'ye çıkar, decodeJSON'ı json.UnmarshalRead'e taşı, decoder hatalarını kendi mesajlarına çevir ve 12'deki handler testlerine 2, 3 ve 4 numaralı gövdeleri ekle. Cevap tarafında v2 nil slice'ı [] yazıyor; Bookstore zaten boş slice gönderdiği için orada bir şey değişmiyor.

go fix ile Modernize Etmek

1.26'da baştan yazılan go fix, go vet'in analiz altyapısını kullanan ve kodu yeni dil ve kütüphane özelliklerine taşıyan "modernizer"lar çalıştırıyor. Go 1.27.4'te go tool fix help 26 analyzer listeliyor; 1.27 bunlara atomictypes, embedlit, slicesbackward ve unsafefuncs'u ekledi, waitgroup'u da waitgroupgo olarak yeniden adlandırdı. Değişiklik yapmadan önce ne yapacağını görmek için -diff:

$ go fix -diff ./...
--- /home/user/bookstore/labs/07-goroutines/main.go (old)
+++ /home/user/bookstore/labs/07-goroutines/main.go (new)
@@ -19,11 +19,9 @@
 	release := make(chan struct{})
 	var wg sync.WaitGroup
 	for range n {
-		wg.Add(1)
-		go func() {
-			defer wg.Done()
+		wg.Go(func() {
 			<-release // park until main closes the channel
-		}()
+		})
 	}
 	fmt.Printf("started %d goroutines in %v\n", n, time.Since(start).Round(time.Millisecond))
...
fix: omitzero: ignoring alternative fix "Replace omitempty with omitzero (behavior change)"
...
--- /home/user/bookstore/internal/httpapi/middleware.go (old)
+++ /home/user/bookstore/internal/httpapi/middleware.go (new)
...
@@ -13,8 +14,8 @@
 
 // Chain wraps h so that the first middleware in the list runs first.
 func Chain(h http.Handler, mws ...Middleware) http.Handler {
-	for i := len(mws) - 1; i >= 0; i-- {
-		h = mws[i](h)
+	for _, mw := range slices.Backward(mws) {
+		h = mw(h)
 	}
 	return h
 }

(Çıktıyı kısalttım; ayrıca labs/07-stockcheck'te bir waitgroupgo, labs/05-errors'ta bir errorsastype ve labs/10-jsonv2'de bir omitzero önerisi vardı.) 08'de bahsettiğim waitgroupgo işini yaptı. Modernizer'lar dosyanın Go sürümüne bakıyor: wg.Go 1.25'te geldiği için go 1.26.0 olan Bookstore'da önerildi; aynı kalıbı go 1.24 satırlı küçük bir module'de denediğimde hiçbir öneri çıkmadı, satırı 1.25 yapınca çıktı. omitzero analyzer'ı ise bir davranış değişikliği yaratacak düzeltmeyi kendiliğinden uygulamıyor, sadece etkisiz olan omitempty'yi siliyor. Diff'i bir pull request olarak gözden geçirip uygulamak için -diff'siz go fix ./... yeterli.

Üretimde Dikkat

Yükseltmeyi iki adıma böl. Önce sadece toolchain'i yükselt (CI image'ı, Dockerfile'daki golang: tag'i) ve go satırına dokunma; GODEBUG mekanizması davranışı büyük ölçüde sabit tutar ve sorun çıkarsa sadece toolchain'i geri alırsın. Sonra ayrı bir değişiklikte go satırını yükselt; yeni varsayılanlar ve yeni vet kontrolleri o zaman devreye girer. 13'teki go version -m çıktısındaki DefaultGODEBUG satırı, bir binary'nin hangi eski davranışlarla çalıştığını production'da da gösteriyor.

CI'da iki sürümle build al. Bu serinin kodu her yazıda Go 1.27.4 ve Go 1.26.11 ile derlendi; 06'daki inference değişikliği gibi go satırına bağlı olmayan farkları ancak böyle yakalarsın. go vet de yükseltmeyle değişiyor: Go 1.27'deki, pointer tipindeki bir error'ı %w ile sarmayı yakalayan kontrol sadece dil sürümü 1.27 ve üstü olan dosyalarda çalışıyor.

Release notes'u "Minor changes to the library" bölümüne kadar oku. 1.27'de HTTP/1 Response.Body'nin kapatılırken okunmamış içeriği bir sınıra kadar okuması gibi davranış değişiklikleri orada; bir HTTP client'ı yoğun kullanan servis için bu fark edilir bir değişiklik olabilir.

Sık Düşülen Tuzaklar

Toolchain'i ve go satırını aynı commit'te yükseltmek. Bir şey bozulursa hangisinin bozduğunu ayıramazsın.

GODEBUG'ı kalıcı çözüm sanmak. Eski davranışı geri getiren her ayarın bir ömrü var; asynctimerchan 1.23'te geldi, 1.27'de gitti. Bir GODEBUG satırı eklediysen kaldırılacağı sürümü de not et.

go fix'i gözden geçirmeden uygulamak. Modernizer'lar davranışı korumak için tasarlandı, ama diff yine de bir code review'dan geçmeli; özellikle test kapsamı düşük package'larda.

Yeni sürümü sadece kendi makinende denemek. Dockerfile'daki builder image'ı, CI runner'ı ve geliştiricilerin toolchain'i farklı sürümlerde kalırsa "bende çalışıyor" sorunları başlar. go.mod'a bir toolchain satırı eklemek herkesin en az o sürümü kullanmasını sağlıyor.

Küçük Bir Deneme

★☆☆ Bookstore'un bir kopyasında go mod edit -go=1.27.0 çalıştır, build et ve go version -m çıktısındaki DefaultGODEBUG satırına bak. Sonra go.mod'a godebug tracebacklabels=0 ekleyip tekrar bak.

★★☆ go fix -diff ./...'in önerdiği değişiklikleri uygula, go test -race ./... ve Go 1.26.11 ile build al. slices.Backward'lı Chain 1.26'da derleniyor mu?

★★★ internal/httpapi'nin bir kopyasında decodeJSON'ı encoding/json/v2 ile yeniden yaz (dosyaya //go:build go1.27 ekle ya da kopyanın go satırını 1.27'ye çıkar). Decoder hatalarını *json.SemanticError ve *jsontext.SyntacticError'a göre kendi mesajlarına çevir ve 12'deki handler testlerine bu yazıdaki 2, 3 ve 4 numaralı gövdeleri ekle.

Bu yazı serinin planlanan ilk on dört yazısının sonuncusu, ama seri burada bitmiyor. Şimdiye kadar dili, concurrency'yi, net/http ile bir servisi, veritabanını, testleri ve production'ı gördük; C# geliştiricisinin Go'da ilk gün takıldığı standart kütüphane package'ları ise eksik kaldı. Bir sonraki yazıda string'lerle başlıyoruz: strings ve strconv, fmt verb'leri, byte ile rune farkı ve Türkçe karakterler. Ardından time package'ı ve "2006-01-02" biçimi, slices/maps/sort/cmp, io.Reader/io.Writer ile dosya işlemleri ve son olarak timeout'larıyla, retry'ıyla HTTP client geliyor.