Go Temelleri: Tipler, Zero Value, Slice, Map, Pointer ve Kontrol Akışı
Go'da slice; pointer, len ve cap'ten oluşan küçük bir header'dır ve alt slice'lar aynı backing array'i paylaşır, bu yüzden books[:0] üzerinde çalışan bir filtre orijinal kataloğu bozar. Zero value felsefesi, iota ile sabitler, string/byte/rune farkı ve Türkçe karakterde len tuzağı, append'in kapasiteyi büyütmesi, nil map ve rastgele map sırası, pointer ve Go 1.26'nın new(expr)'i, 1.22 döngü değişkeni ve nil slice'ın JSON'da null olması.
Sürüm notu: Bu yazıdaki kodlar Go 1.27.4 ile derlenip çalıştırılmıştır; module'ün go satırı 1.26.0'dır ve kod Go 1.26.11 ile de derlenir. new(15) gibi değer alan new Go 1.26 ile gelmiştir.
Önce Tahmin Et
Bu yazıda sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) kataloğuna fiyat filtresi ekleyeceğiz. Go'da slice'larla çalışmaya yeni başlayan biri, bellek ayırmamak için filtreyi çoğu zaman şöyle yazar: sonucu girdinin kendisi üzerinde biriktirir. Kod derleniyor, go vet bir şey demiyor:
// Lab 03: girdisinin üzerine sessizce yazan yerinde (in-place) filtre.
package main
import "fmt"
type Book struct {
Title string
Price int64
}
func cheaperInPlace(books []Book, limit int64) []Book {
result := books[:0] // girdinin backing array'ini yeniden kullanır
for _, b := range books {
if b.Price <= limit {
result = append(result, b)
}
}
return result
}
func main() {
books := []Book{{"Dune", 34990}, {"The Hobbit", 28950}, {"Middlemarch", 31000}, {"Emma", 19900}}
cheap := cheaperInPlace(books, 30000)
fmt.Println(cheap)
fmt.Println(books)
}
Fiyatlar kuruş cinsinden; 30000, 300 TL demek. İlk satırda 300 TL'nin altındaki iki kitap çıkacak, orası belli. Soru ikinci satırda: filtreden sonra books ne yazdırır? C#'ta bir List<Book>'u method'a verip içinde yeni bir liste oluşturduğunda orijinal liste değişmez. Burada da öyle mi?
go run ./labs/03-filter
[{The Hobbit 28950} {Emma 19900}]
[{The Hobbit 28950} {Emma 19900} {Middlemarch 31000} {Emma 19900}]
Orijinal katalog bozuldu: Dune kayboldu, Emma iki kez görünüyor. books[:0] yeni bir liste değil, aynı bellek bloğuna bakan uzunluğu sıfır bir slice. append o bloğa yazdıkça orijinalin ilk element'lerinin üzerine yazdı. Bunun nedenini görmek için önce Go'nun tiplerine ve zero value fikrine bakacağız, sonra slice'ın içini açacağız. Yazının sonunda filtrenin doğru hâlini Bookstore API'sine ekleyeceğiz.
Değişkenler, Sabitler ve Zero Value
Go'da değişken tanımlamanın iki yolu var. var count int tipi açıkça yazar ve değişkene zero value'sunu verir. price := 349.90 ise tipi sağdaki değerden çıkarır; C#'taki var'ın karşılığı, ama sadece fonksiyon içinde kullanılabilir. Bu yazının küçük deneylerini module'deki labs/ klasörüne koyuyorum; her biri kendi main package'ı olan ayrı bir program:
// Lab 03: değişkenler, sabitler, zero value'lar, string'ler ve rune'lar.
package main
import (
"fmt"
"unicode/utf8"
)
type OrderStatus int
const (
Pending OrderStatus = iota
Paid
Shipped
Cancelled
)
type Book struct {
ISBN string
Title string
Price int64
InStock bool
Tags []string
Discount *int
}
func main() {
var count int
var title string
var b Book
fmt.Printf("count=%d title=%q\n", count, title)
fmt.Printf("%+v\n", b)
fmt.Println(b.Tags == nil, len(b.Tags), b.Discount == nil)
price := 349.90 // := tipi çıkarır: float64
isbn := "9780441172719"
fmt.Printf("%T %T %v %v\n", price, isbn, Paid, Cancelled)
turkish := "Çalıkuşu"
fmt.Println(len(turkish), utf8.RuneCountInString(turkish))
fmt.Println(turkish[0], string(turkish[0]))
for i, r := range turkish[:4] {
fmt.Printf("%d:%c ", i, r)
}
fmt.Println()
b.Discount = new(15) // Go 1.26: new bir ifade alabiliyor
fmt.Println(*b.Discount)
}
go run ./labs/03-basics
count=0 title=""
{ISBN: Title: Price:0 InStock:false Tags:[] Discount:<nil>}
true 0 true
float64 string 1 3
11 8
195 Ã
0:Ç 2:a 3:l
15
Go'da ilk değeri verilmemiş bir değişken yoktur. Her tipin bir zero value'su var: sayılar için 0, string için "", bool için false, pointer, slice, map, channel, fonksiyon ve interface için nil. Struct'ın zero value'su ise bütün field'larının zero value'su. var b Book satırı bir constructor çağırmadan kullanılabilir bir kitap verdi. C#'taki nullable reference types uyarılarıyla uğraşmak yerine Go'da tipleri, zero value'ları anlamlı olacak şekilde tasarlarsın: 02'deki boş catalog.Store struct'ının var store catalog.Store ile kullanıma hazır olması bu yüzden. Standart kütüphanede de aynı yaklaşım var: sync.Mutex ya da bytes.Buffer zero value'larıyla çalışır.
const bloğundaki iota, blok içinde her satırda bir artan bir sayaç. Pending 0, Paid 1, Cancelled 3 oldu. Go'da enum anahtar kelimesi yok; type OrderStatus int ile isimli bir tip açıp sabitleri o tipten tanımlamak C#'taki enum OrderStatus'un karşılığı. Go, OrderStatus(42) gibi tanımlı olmayan bir değeri engellemez (C#'taki enum'lar da engellemez). C#'ta switch expression eksik case için CS8509 uyarısı verir; Go derleyicisinde bunun karşılığı yok. Bu yüzden durum geçişlerini 04'te method'larla koruyacağız.
Go'da tip dönüşümleri her zaman açık. int ile int64 farklı tipler ve birbirine kendiliğinden dönüşmez; int64(count) yazman gerekir. C#'taki gibi int'ten long'a örtük dönüşüm yok. İlk haftalarda bu biraz yorar, ama para hesabında yanlış tipte bir sayının sessizce karışmasını önler.
string, byte ve rune: Türkçe Karakterlerde len Tuzağı
Çıktıdaki 11 8 satırı Türkçe metinle çalışan herkesin bilmesi gereken bir şey söylüyor. Go'da string, değiştirilemeyen bir byte dizisi ve kaynak kod, UTF (Unicode Transformation Format; Unicode karakterlerini byte'lara çeviren kodlama ailesi) ailesinden UTF-8 ile yazılır; UTF-8 bir karakteri 1–4 byte ile kodlar. "Çalıkuşu" 8 harf, ama Ç, ı ve ş UTF-8'de ikişer byte tuttuğu için len 11 döndürdü. Harf saymak için utf8.RuneCountInString kullanılır. rune, bir Unicode code point'i tutan int32'nin diğer adı; C#'taki char'a benziyor, ama C#'ta char UTF-16 birimi, Go'da rune code point.
turkish[0] bir harf değil, bir byte döndürdü: 195, yani Ç'nin ilk byte'ı. string(turkish[0]) bu byte'ı tek başına bir karaktermiş gibi yorumlayınca "Ã" çıktı. range ise string üzerinde rune rune döner ve her rune'un başladığı byte index'ini verir: 0, 2, 3. Ç iki byte olduğu için 1 atlandı. C# Temelleri serisinin "C# String'ler" yazısında Türkçe I sorununu anlatmıştık; Go'da aynı soruna bir de byte ile harf farkı ekleniyor. String işlemlerinin ayrıntısına ileride ayrı bir yazıda döneceğiz.
Array ve Slice: len, cap ve Paylaşılan Backing Array
Go'da iki ayrı dizi tipi var. Array'in uzunluğu tipin parçası: [4]string ile [5]string farklı tipler. Array bir değer tipi; atadığında ya da fonksiyona verdiğinde kopyalanır. Günlük kodda neredeyse hiç array kullanmazsın, slice kullanırsın. Slice üç parçalı küçük bir yapı: bir array'e pointer, uzunluk (len) ve kapasite (cap). Bu yapıya slice header, işaret ettiği array'e backing array denir.
// Lab 03: array'ler, slice'lar, len/cap, append ve paylaşılan backing array'ler.
package main
import (
"fmt"
"slices"
)
func main() {
shelf := [4]string{"Dune", "Emma", "The Hobbit", "Middlemarch"}
copied := shelf
copied[0] = "Changed"
fmt.Println(shelf[0], copied[0])
all := shelf[:] // bütün array'i gören bir slice
firstTwo := all[:2]
fmt.Println(len(firstTwo), cap(firstTwo))
firstTwo[1] = "Persuasion"
fmt.Println(shelf[1])
firstTwo = append(firstTwo, "Ulysses")
fmt.Println(shelf[2], len(firstTwo), cap(firstTwo))
firstTwo = append(firstTwo, "Beloved", "Dracula")
firstTwo[0] = "Frankenstein"
fmt.Println(shelf[0], len(firstTwo), cap(firstTwo))
var titles []string
for i := range 10 {
titles = append(titles, fmt.Sprint(i))
fmt.Print(cap(titles), " ")
}
fmt.Println()
prices := []int{34990, 28950, 31000, 19900}
sorted := slices.Clone(prices)
slices.Sort(sorted)
fmt.Println(prices, sorted, slices.Contains(prices, 19900), slices.Index(prices, 31000))
}
Dune Changed
2 4
Persuasion
Ulysses 3 4
Dune 5 8
2 2 4 4 8 8 8 8 16 16
[34990 28950 31000 19900] [19900 28950 31000 34990] true 2

Satır satır gidelim. copied := shelf array'i kopyaladı, bu yüzden shelf[0] değişmedi. firstTwo := all[:2] ise yeni bir array açmadı: uzunluğu 2, kapasitesi 4 olan ve shelf'e bakan bir slice. firstTwo[1]'e yazınca shelf[1] değişti. İlk append'te kapasitede yer vardı; Go yeni bir array açmak yerine elemanı shelf[2]'nin üzerine yazdı ve "The Hobbit" "Ulysses" oldu. İkinci append uzunluğu 5'e çıkaracaktı, kapasite 4'tü. Bu kez runtime kapasitesi 8 olan yeni bir array açtı, eski element'leri kopyaladı ve firstTwo'yu ona bağladı. Bu yüzden firstTwo[0]'a yazmak shelf[0]'ı artık etkilemedi.
Önce Tahmin Et'teki filtrenin hikâyesi de bu. books[:0], books ile aynı backing array'e bakan ve kapasitesi 4 olan bir slice. append hiç yer açmak zorunda kalmadığı için eşleşen kitapları sırayla 0. ve 1. element'in üzerine yazdı. Orijinal slice'ın header'ı değişmediği için uzunluğu hâlâ 4 ve içinde hem yeni yazılan hem de eski element'ler var.
Kapasite satırı (2 2 4 4 8 8 8 8 16 16) append'in kapasiteyi katlayarak büyüttüğünü gösteriyor. Büyüme formülünün ayrıntısı runtime'ın iç işi ve sürümden sürüme değişebilir; sayılara değil, örüntüye bak. C#'taki List<T> da içeride aynı şekilde büyüyen bir array tutar. Fark şu: List<T> bir reference type ve iç array'ini kimseyle paylaşmaz, Go'da ise slice değer olarak kopyalanır ama kopyalar aynı backing array'i görür. C# Koleksiyonlar serisinin "C# List<T> Rehberi" yazısındaki capacity anlatımı burada da geçerli, paylaşım kısmı Go'ya özgü.
Son satırdaki slices package'ı Go 1.21'den beri standart kütüphanede. slices.Clone yeni bir backing array ile kopya verir, slices.Sort yerinde sıralar, Contains ve Index doğrusal arama yapar. LINQ'e (Language Integrated Query; C#'ta collection'lar üzerinde sorgu yazma özelliği) alışkınsan bu fonksiyonlar ilk aradığın şeyler olacak; zincirleme kullanım yok, her biri bir slice alıp bir sonuç döndürür.
Map: nil Map, Rastgele Sıra ve comma-ok
Map, C#'taki Dictionary<TKey, TValue>'in karşılığı. Davranışları üç noktada farklı. Bunları stok sayılarıyla deneyelim:
// Lab 03: map'ler, iteration sırası, pointer'lar, switch ve defer.
package main
import (
"fmt"
"maps"
"slices"
)
type Book struct {
Title string
Stock int
}
func restock(b *Book, n int) {
b.Stock += n
}
func restockCopy(b Book, n int) {
b.Stock += n
}
func label(stock int) string {
switch {
case stock == 0:
return "out of stock"
case stock < 5:
return "low"
default:
return "ok"
}
}
func main() {
defer fmt.Println("main finished")
stock := map[string]int{"Dune": 12, "Emma": 7, "The Hobbit": 0, "Middlemarch": 4}
for range 3 {
for title := range stock {
fmt.Print(title, " | ")
}
fmt.Println()
}
for _, title := range slices.Sorted(maps.Keys(stock)) {
fmt.Printf("%s=%s ", title, label(stock[title]))
}
fmt.Println()
n, ok := stock["Ulysses"]
fmt.Println(n, ok)
b := Book{Title: "Emma", Stock: 7}
restockCopy(b, 5)
fmt.Println(b.Stock)
restock(&b, 5)
fmt.Println(b.Stock)
var reserved map[string]int
fmt.Println(reserved["Dune"], len(reserved))
//lint:ignore SA5000 bu lab'ın amacı panic'i görmek
reserved["Dune"] = 1
}
go run ./labs/03-maps
The Hobbit | Middlemarch | Dune | Emma |
Dune | Emma | The Hobbit | Middlemarch |
Dune | Emma | The Hobbit | Middlemarch |
Dune=ok Emma=ok Middlemarch=low The Hobbit=out of stock
0 false
7
12
0 0
main finished
panic: assignment to entry in nil map
goroutine 1 [running]:
main.main()
/home/user/bookstore/labs/03-maps/main.go:61 +0x4fc
exit status 2
Birinci fark: aynı map'i üç kez dolaştık ve ilk turda sıra farklı çıktı. Go, map iteration sırasını bilerek garanti etmez ve her döngüde rastgele bir noktadan başlar; programlar sıraya güvenmesin diye. Sıralı çıktı istiyorsan key'leri toplayıp sıralarsın: maps.Keys bir iterator döndürür, slices.Sorted onu sıralı bir slice'a çevirir. Iterator'lara 06'da ayrıntılı bakacağız.
İkinci fark: olmayan bir key'i okumak exception fırlatmaz, zero value döndürür. stock["Ulysses"] 0 verdi; ama stokta 0 adet olan "The Hobbit" de 0 veriyor. İkisini ayırmak için "comma-ok" kalıbı var: n, ok := stock["Ulysses"] ikinci değer olarak key'in map'te olup olmadığını döndürür. C#'taki TryGetValue'nun karşılığı.
Üçüncü fark: nil bir map okunabilir ama yazılamaz. var reserved map[string]int satırı bir map oluşturmadı, sadece zero value'su nil olan bir değişken açtı. Okumak 0 döndürdü, yazmak panic'e yol açtı. Map'i make(map[string]int) ya da map[string]int{} ile oluşturman gerekir. Panic olduğunda bile defer ile kaydettiğimiz "main finished" satırının yazıldığına dikkat et. Bu hatayı derleyici yakalamaz, ama 02'de eklediğimiz staticcheck yakalıyor; lab'daki //lint:ignore satırını silip çalıştırınca:
go tool staticcheck ./...
labs/03-maps/main.go:60:2: assignment to nil map (SA5000)
Pointer: Aritmetik Yok, new(expr) Var
Aynı lab'daki restockCopy ve restock fonksiyonları Go'nun en temel kuralını gösteriyor: her şey değer olarak geçirilir. restockCopy(b, 5) struct'ın kopyasını aldı ve kopyanın stoğunu artırdı, b.Stock 7'de kaldı. restock(&b, 5) ise b'nin adresini verdi; fonksiyon pointer üzerinden asıl değişkeni değiştirdi ve stok 12 oldu. C#'ta struct'lar için ref parametresi neyse Go'da pointer o; C# Temelleri serisinin "Value Type ve Reference Type" yazısındaki kopyalama kurallarının hepsi Go struct'ları için de geçerli. Farkı şu: Go'da class yok, bir tipin değer mi referans gibi mi davranacağına tipi tanımlarken değil, kullanırken pointer alıp almayarak karar verirsin.
Pointer'ı field'a erişirken ayrıca açmana gerek yok: b.Stock hem Book hem *Book üzerinde çalışır. Pointer aritmetiği yok; p++ ile bir sonraki bellek adresine geçemezsin. Bu, C#'taki unsafe kodun Go'da varsayılan olarak hiç olmaması demek. Fonksiyondan yerel bir değişkenin adresini döndürmek de güvenli; derleyici değişkeni gerekiyorsa heap'e taşır.
Go 1.26'dan beri new bir ifade alabiliyor. Önceden opsiyonel bir field'a sabit bir değer vermek için ara değişken gerekiyordu: d := 15; b.Discount = &d. Artık b.Discount = new(15) yazılabiliyor. *int gibi pointer field'lar, JSON'da "değer yok" ile "değer 0" arasındaki farkı taşımak için kullanılır; bunu 10'da göreceğiz.
Kontrol Akışı: Tek Döngü for, switch ve defer
Go'da while, do-while ya da foreach yok; hepsinin işini for görür. for i := 0; i < n; i++ klasik döngü, for cond { } while, for { } sonsuz döngü, for i, v := range items foreach. Go 1.22'den beri for i := range 10 ile bir sayının üzerinde de dönebilirsin.
1.22'deki daha önemli değişiklik döngü değişkeninin anlamı. Eskiden döngü değişkeni bütün döngü boyunca tek bir değişkendi; closure'lar onu yakalayınca hepsi son değeri görüyordu. Şimdi her iterasyon kendi değişkenini alıyor. Bu davranış module'ün go satırına bağlı, bu yüzden aynı kodu iki ayrı go satırıyla çalıştırabiliriz:
package main
import "fmt"
func main() {
titles := []string{"Dune", "Emma", "Ulysses"}
var printers []func()
for _, t := range titles {
printers = append(printers, func() { fmt.Print(t, " ") })
}
for _, p := range printers {
p()
}
fmt.Println()
}
go mod edit -go=1.26.0 && go run .
go mod edit -go=1.21.0 && go run .
Dune Emma Ulysses
Ulysses Ulysses Ulysses
Aynı toolchain, aynı kod, farklı sonuç. C# İleri Seviye serisinin "Lambda ve Closure: Yakalanan Değişken Tuzağı" yazısında C# 5'in foreach için aynı düzeltmeyi 2012'de yaptığını anlatmıştık; Go bunu 2024'te, 1.22 ile yaptı ve eski kodu bozmamak için go satırına bağladı. Bookstore go 1.26.0'da olduğu için yeni davranış geçerli.
switch'te case'ler break istemez; eşleşen case çalışır ve switch biter. Bir sonraki case'e geçmek istersen fallthrough yazarsın, ama pratikte nadiren gerekir. Lab'daki label fonksiyonundaki gibi ifadesiz bir switch, uzun bir if-else if zincirinin okunaklı hâli. defer ise bir fonksiyon çağrısını, içinde bulunduğun fonksiyon dönerken çalışmak üzere sıraya koyar. C#'taki finally ya da using bloğunun işini görür ve panic'te bile çalışır; kaynakları kapatmak için ana araç bu. Birden fazla defer varsa ters sırayla çalışırlar.
Katalogda Fiyat Filtresi
Artık Bookstore'a dönebiliriz. internal/catalog'a fiyat, yazar ve stok ekledim. Para için float64 kullanmıyorum: C#'taki decimal'in Go'da standart karşılığı yok ve float64 0.1 + 0.2 gibi toplamlarda kuruş kaybeder. En sade çözüm, tutarı kuruş cinsinden bir tam sayıda tutmak. Bunun için isimli bir tip açtım:
// Package catalog, Bookstore'un sattığı kitapları tutar.
package catalog
import "slices"
// Money kuruş cinsinden bir tutardır: 34990, 349,90 TRY demek.
type Money int64
// Book, katalogdaki bir kitaptır.
type Book struct {
ISBN string `json:"isbn"`
Title string `json:"title"`
Author string `json:"author"`
Price Money `json:"price"`
Stock int `json:"stock"`
}
// books export edilmiyor: diğer package'lar ona sadece Store üzerinden ulaşır.
var books = []Book{
{ISBN: "9780441172719", Title: "Dune", Author: "Frank Herbert", Price: 34990, Stock: 12},
{ISBN: "9780547928227", Title: "The Hobbit", Author: "J.R.R. Tolkien", Price: 28950, Stock: 0},
{ISBN: "9780141439549", Title: "Middlemarch", Author: "George Eliot", Price: 31000, Stock: 4},
{ISBN: "9780141439587", Title: "Emma", Author: "Jane Austen", Price: 19900, Stock: 7},
}
// Store şimdilik kitapları bellekten verir.
type Store struct{}
// All her kitabın kopyasını döndürür, böylece çağıran katalogu değiştiremez.
func (Store) All() []Book {
return slices.Clone(books)
}
// CheaperThan fiyatı en fazla limit olan kitapları döndürür.
func (Store) CheaperThan(limit Money) []Book {
result := []Book{} // nil değil boş: JSON'da null yerine [] olur
for _, b := range books {
if b.Price <= limit {
result = append(result, b)
}
}
return result
}
İki değişiklik bu yazıda öğrendiklerimizden geliyor. 02'deki All paket seviyesindeki books slice'ını doğrudan döndürüyordu; çağıran store.All()[0].Price = 0 yazsa kataloğun kendisi değişirdi, çünkü dönen slice aynı backing array'e bakıyordu. Şimdi slices.Clone ile kopya dönüyor. CheaperThan da sonucu girdinin üzerine değil, yeni bir slice'a yazıyor.
Handler'da maxPrice query parametresini okuyup sayıya çeviriyoruz. strconv.ParseInt iki değer döndürür: sayı ve hata. C#'taki long.TryParse'ın karşılığı, ama başarısızlığı bool ile değil error ile bildiriyor:
http.HandleFunc("GET /api/books", func(w http.ResponseWriter, r *http.Request) {
list := store.All()
if s := r.URL.Query().Get("maxPrice"); s != "" {
limit, err := strconv.ParseInt(s, 10, 64)
if err != nil {
http.Error(w, "maxPrice must be a whole number of kurus", http.StatusBadRequest)
return
}
list = store.CheaperThan(catalog.Money(limit))
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(list)
})
if s := ...; s != "" kalıbı Go'da sık görülür: değişken sadece if bloğunun scope'unda yaşar. catalog.Money(limit) açık bir tip dönüşümü; int64'ü kendiliğinden Money'ye çevirmez. Üç istekle deneyelim (Date header'larını çıkardım):
curl -i "http://127.0.0.1:52710/api/books?maxPrice=30000"
curl -i "http://127.0.0.1:52710/api/books?maxPrice=abc"
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 185
[{"isbn":"9780547928227","title":"The Hobbit","author":"J.R.R. Tolkien","price":28950,"stock":0},{"isbn":"9780141439587","title":"Emma","author":"Jane Austen","price":19900,"stock":7}]
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Content-Length: 41
maxPrice must be a whole number of kurus
Üçüncü istekte hiçbir kitabın eşleşmediği bir limit verdim. CheaperThan'ın ilk hâlinde sonucu var result []Book ile başlatmıştım. Hiçbir kitap eklenmeyince fonksiyon nil slice döndürdü ve encoding/json bunu boş bir dizi olarak değil null olarak yazdı:
curl -i "http://127.0.0.1:52710/api/books?maxPrice=100"
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 5
null
Go içinde nil slice ile boş slice neredeyse aynı davranır: ikisinin de len'i 0'dır, ikisine de append yapılabilir. Ama JSON'da biri null, diğeri []. Angular istemcisi bu cevabı bir diziymiş gibi kullanmaya çalışırsa hata alır. result := []Book{} ile başlatınca aynı istek [] döndü ve Content-Length 3 oldu.
Üretimde Dikkat
Bir slice'ı package dışına verirken kimin sahibi olduğunu düşün. Go'da readonly collection tipi yok; C#'taki IReadOnlyList<T> gibi bir sözleşme dönemezsin. Dışarı verdiğin slice'ın içi değiştirilebilir. Paylaşılan veriyi dışarı veriyorsan kopyasını ver; bu kopyanın bir bellek maliyeti var, ama paylaşılan state'in sessizce değişmesinden ucuzdur. Aynı kural ters yönde de geçerli: bir slice'ı parametre olarak alıp saklıyorsan, çağıranın onu sonra değiştirebileceğini bil.
JSON'a giden slice'ları boş olarak başlat. API sözleşmen "bu alan her zaman bir dizi" diyorsa, nil slice'lar sessizce null üretir. Go 1.27'deki encoding/json/v2 bu konuda varsayılanı değiştiriyor: nil slice'ı boş dizi olarak yazıyor, eski davranış için FormatNilSliceAsNull seçeneği var. Bunu 10'da konuşacağız.
Map'leri birden fazla goroutine'den aynı anda yazma. Go'nun map'i C#'taki Dictionary gibi thread-safe değil; runtime eşzamanlı yazmayı fark ederse programı "concurrent map writes" hatasıyla durdurur. Bunu 08'de mutex ile çözeceğiz.
Sık Düşülen Tuzaklar
Alt slice'ı bağımsız bir kopya sanmak. books[:2], books[1:] ya da books[:0] yeni bir slice header'ı ama aynı backing array. Kopya istiyorsan slices.Clone kullan.
append'in sonucunu atamamak. append(titles, "Dune") tek başına yazılırsa derleyici hata verir, çünkü sonuç kullanılmıyor. Ama other := append(titles, "Dune") yazıp titles'ı kullanmaya devam etmek mantık hatasıdır: yeni element other'da.
len(s)'yi harf sayısı sanmak. Türkçe karakterler UTF-8'de birden fazla byte tutar. Kullanıcıya gösterilecek bir başlığı 30 karakterde kesecekken byte index'iyle kesersen bir harfi ortadan bölüp geçersiz UTF-8 üretebilirsin.
nil map'e yazmak ve map sırasına güvenmek. Map'i make ile oluştur; sıralı çıktı gerekiyorsa key'leri sırala. Sıraya güvenen bir test bazen geçip bazen kalır.
Para için float64. Kuruş cinsinden tam sayı ya da decimal bir library kullan; 10'da bu seçimi JSON tarafıyla birlikte tartışacağız.
Küçük Bir Deneme
★☆☆ labs/03-filter'daki cheaperInPlace'i, girdiye dokunmayacak şekilde yeniden yaz. İki yol dene: make([]Book, 0, len(books)) ile yeni bir slice açmak ve önce slices.Clone ile kopyalayıp sonra slices.DeleteFunc kullanmak.
★★☆ labs/03-basics'e Türkçe bir kitap başlığını en fazla 5 harfte kesen bir fonksiyon ekle. Önce s[:5] ile dene ve "Çalıkuşu" için çıktıya bak; sonra []rune(s) dönüşümüyle düzelt.
★★★ catalog'a InStock() []Book diye stokta olan kitapları döndüren bir method ekle ve GET /api/books?inStock=true ile handler'a bağla. maxPrice ile birlikte verildiğinde iki filtre de uygulansın. Hiçbir kitap eşleşmediğinde cevabın [] olduğunu kontrol et.
Tiplerin, slice'ların ve map'lerin nasıl davrandığını gördük. Bir sonraki yazıda bunları bir araya getiren struct'lara, method'lara ve interface'lere geçiyoruz: class olmadan sipariş modelini kurmak, value ve pointer receiver farkı, embedding ve Go'nun implicit interface'leri. Bookstore'a internal/orders package'ını ekleyeceğiz.