Go Derinlemesine İnceleme — Bellek, Pointer'lar, Value Semantics ve Interface'ler
İçindekiler
- Go’nun Bellek Modeli — Genel Bakış
- Stack ve Heap
- Escape Analysis (Kaçış Analizi)
- Value’lar ve Pointer’lar
- Value Semantics vs Pointer Semantics
- Struct’lar: Pratikte Kopyalama Semantiği
- Referans Benzeri Tipler: Slice, Map, Channel
- Method Receiver’ları: Value vs Pointer
- Interface’ler: Dahili (Internal) Temsil
- Nil Interface Tuzağı
- Interface Sağlama (Satisfaction) ve Method Set’ler
- Garbage Collection Temelleri
- Pratik Öneriler
- Yaygın Hatalar Kılavuzu
1. Go’nun Bellek Modeli — Genel Bakış
Go, garbage-collected (çöp toplamalı), statik tipli ve varsayılan olarak value semantics kullanan bir dildir. Bu tek tasarım kararı — atama, fonksiyon argümanları ve struct alanlarının, siz açıkça bir pointer kullanmadığınız sürece değerleri kopyalaması — Go’da belleğin nasıl davrandığına dair hemen hemen her şeyi şekillendirir.
Temel gerçekler:
- Go’daki her değişkenin bir tipi vardır ve her tipin derleme zamanında bilinen sabit bir boyutu vardır (slice, string, map gibi birkaç “header” tipi hariç; bunların sabit boyutlu bir header’ı vardır ve değişken boyutlu veriye işaret eder).
- Çalışan bir Go programında bellek iki yerden birinde bulunur: stack veya heap.
- Go derleyicisi, bir değişkenin nerede yaşayacağına escape analysis (kaçış analizi) adı verilen bir süreçle karar verir — programcı değil, ve C++‘daki gibi “new = heap, new yoksa = stack” mantığıyla da değil.
- Manuel bellek yönetimi yoktur. Garbage collector (GC), heap belleğini otomatik olarak geri kazanır.
- Go’da pointer aritmetiği yoktur (C/C++‘ın aksine), bu da pointer’ları (unsafe.Pointer hariç) yapısal olarak bellek açısından güvenli kılar.
2. Stack ve Heap
Stack
- Her goroutine’in kendi stack’i vardır (küçük başlar, ~8KB, dinamik olarak büyür/küçülür, 64-bit sistemlerde varsayılan olarak genellikle 1GB’a kadar).
- Stack’e ayırma son derece ucuzdur: sadece bir stack pointer’ını kaydırmaktır.
- Bir fonksiyon return ettiğinde stack belleği otomatik olarak geri kazanılır — GC’nin herhangi bir müdahalesi olmaz.
- Fonksiyonu “kaçmayan” (escape etmeyen) yerel değişkenler stack’te tahsis edilir.
Heap
- Tüm goroutine’ler arasında paylaşılır.
- Heap’e ayırma daha pahalıdır: allocator’ı (runtime’daki
mallocgc) devreye sokar ve garbage collector üzerinde baskı oluşturur. - Fonksiyonu kaçan (ömrü fonksiyon çağrısından daha uzun süren veya derleyicinin aksini kanıtlayamadığı) her şey heap’te tahsis edilir.
Bunun Neden Önemli Olduğu
Her heap tahsisi:
- Stack tahsisinden daha yavaştır.
- GC baskısının bir kaynağıdır (GC bunu taramak, potansiyel olarak iş yapmak zorundadır, vb.)
- Potansiyel bir cache-locality (önbellek yerelliği) sorunudur — heap nesneleri, stack frame’leri gibi sıkıca paketlenmek yerine dağınıktır.
“Mekanik olarak uyumlu” (mechanically sympathetic) Go kodu yazmak genellikle gereksiz heap tahsislerini minimize etmek anlamına gelir — pointer’lardan dogmatik olarak kaçınarak değil, bir pointer’ın ne zaman kaçışa (escape) zorladığını anlayarak.
3. Escape Analysis (Kaçış Analizi)
Escape analysis, bir değişkenin güvenle stack’te yaşayıp yaşayamayacağına, yoksa heap’e taşınması (yani “kaçması”) gerekip gerekmediğine karar vermek için derleyici tarafından yapılan statik bir analiz sürecidir.
Altın Kural
Derleyici, bir değişkenin ömrünün mevcut fonksiyon çağrısının stack frame’i ile sınırlı olduğunu kanıtlayamıyorsa, o değişken heap’e kaçar.
Bir Değişkenin Kaçmasının Yaygın Nedenleri
// 1. Yerel bir değişkene pointer döndürme
func newUser() *User {
u := User{Name: "Ada"} // kaçar: çağıran taraf buna bir pointer tutar
return &u
}
// 2. Fonksiyondan daha uzun ömürlü bir struct/slice/map/channel içine pointer saklama
func store(u *User) {
globalUsers = append(globalUsers, u) // kaçar
}
// 3. Bir değeri interface parametresine geçirme (genellikle boxing/kaçışa zorlar)
func log(v interface{}) {
fmt.Println(v)
}
func doWork() {
x := 42
log(x) // interface değerinin buna referans vermesi gerektiğinden x kaçabilir
}
// 4. Değişkenleri referans yoluyla yakalayan closure'lar
func counter() func() int {
count := 0 // kaçar: döndürülen closure tarafından yakalanır
return func() int {
count++
return count
}
}
// 5. Derleme zamanında bilinmeyen boyut (örn. make ile dinamik uzunluklu slice)
func makeSlice(n int) []int {
s := make([]int, n) // n küçük bir derleme-zamanı sabiti değilse, bu kaçar
return s
}
Escape Analysis’i Kendiniz Nasıl Kontrol Edersiniz
go build -gcflags="-m" ./...
Bu, şu şekilde tanılama çıktıları verir:
./main.go:10:9: &u escapes to heap
./main.go:15:6: moved to heap: u
-m -m ile çalıştırmak, derleyicinin bu kararı neden verdiği hakkında daha fazla ayrıntı sunar.
Önemli Nüans
Bir fonksiyondan pointer döndürmek otomatik olarak kötü performans anlamına gelmez. Aslında, genellikle büyük bir struct’ı value olarak döndürmekten (bu, stack’te tüm yapıyı kopyalar veya çağıranı zaten kaçışa zorlar) daha iyidir. Buradaki kilit fikir şudur: gerçek kural “pointer = heap, value = stack” değil, escape analysis’tir. Bir pointer, çağıran fonksiyonun frame’inden hiç kaçmıyorsa stack’te kalabilir; bir value, daha uzun ömürlü bir şey tarafından yakalanıyorsa heap’te sona erebilir.
4. Value’lar ve Pointer’lar
Pointer Nedir
Bir pointer, başka bir değişkenin bellek adresini tutan bir değişkendir. Go’da:
var x int = 10
var p *int = &x // p, x'in adresini tutar
fmt.Println(*p) // dereference: 10 yazdırır
*p = 20 // x'i pointer üzerinden değiştirir
fmt.Println(x) // 20
&x— “adresini al” operatörü, birT‘den*Türetir.*p— “dereference” operatörü, bir*T‘denTüretir.- Herhangi bir pointer tipinin sıfır değeri
nil‘dir. nilbir pointer’ı dereference etmek, çalışma zamanı panic’ine neden olur (invalid memory address or nil pointer dereference).
new vs &T{} vs make
p1 := new(int) // *int, sıfır değerli bir int'e işaret eder, yani 0
p2 := &MyStruct{} // *MyStruct, sıfır değerli bir struct literal'e işaret eder
s := make([]int, 0, 10) // slice header (pointer DEĞİL!), 10 kapasiteyle ön tahsisli
m := make(map[string]int)
ch := make(chan int)
new(T), birTiçin sıfırlanmış depolama alanı tahsis eder ve*Tdöndürür. Deyimsel (idiomatic) Go’da nadiren kullanılır;&T{}daha yaygın ve daha esnektir (alanları ayarlamanıza izin verir).makeyalnızca slice, map ve channel içindir — bir pointer döndürmez, bu referans benzeri tipin başlatılmış (sıfır olmayan) bir değerini döndürür, çünkü bu tiplerin dahili kurulum gerektirir (örn. bir slice’ın tahsis edilmiş bir alt dizisi (underlying array) olması gerekir).
Pointer’lar C++ Anlamında “Referans” Değildir
Go pointer’ları C++ referanslarından daha kısıtlıdır:
nilolabilirler.- Başka bir yeri işaret edecek şekilde yeniden atanabilirler.
*/&semantiğinin operator overloading’i yoktur — her zaman açıktır (explicit).- Pointer aritmetiği yoktur: bir
*intüzerindep++yasadışıdır.
Pointer’a Pointer
Yasaldır ancak deyimsel kodda nadirdir:
var x int = 5
p := &x
pp := &p
**pp = 10 // x artık 10
5. Value Semantics vs Pointer Semantics
Bu, “Go semantiğinin” kavramsal özüdür.
Value Semantics
Bir fonksiyona bir değer (pointer değil) geçirdiğinizde, onu başka bir değişkene atadığınızda veya bir slice/map içinde sakladığınızda, Go onu kopyalar.
type Point struct{ X, Y int }
func move(p Point) {
p.X += 10 // yalnızca KOPYAYI değiştirir
}
func main() {
pt := Point{1, 2}
move(pt)
fmt.Println(pt) // {1 2} — değişmedi!
}
Pointer Semantics
Bir pointer geçirdiğinizde, fonksiyon çağıranla aynı alttaki veri üzerinde çalışır.
func movePtr(p *Point) {
p.X += 10 // orijinali değiştirir
}
func main() {
pt := Point{1, 2}
movePtr(&pt)
fmt.Println(pt) // {11 2} — değişti!
}
Go Neden Varsayılan Olarak Value Semantics Kullanır
- Öngörülebilirlik: bir değer alan bir fonksiyon, açıkça bir pointer almadığı sürece çağıran durumu değiştiremez (aliasing hataları yok).
- Cache dostu olma: value olarak geçirilen küçük değerler genellikle register’larda veya stack’te kalır, heap tahsisinden ve pointer izlemekten (pointer chasing) kaçınır.
- Eşzamanlılık (concurrency) güvenliği: kopyalar, paylaşılan pointer’ların yaşadığı şekilde data race’e uğrayamaz (bu bir sihirli değnek değildir — aşağıya bakın).
Ödünleşim (Trade-off)
- Value semantics bir kopya maliyetine sahiptir — büyük struct’lar için pahalıdır.
- Pointer semantics bir dolaylılık (indirection) maliyetine sahiptir ve genellikle bir heap kaçışına zorlar, ayrıca aliasing hataları ve eşzamanlı kodda data race potansiyeli ekler.
Evrensel olarak “doğru” bir seçim yoktur — bu, tip ve bağlam bazında verilen bir karardır (bkz. Bölüm 13).
6. Struct’lar: Pratikte Kopyalama Semantiği
Atama Kopyalar
type User struct {
Name string
Age int
}
u1 := User{"Ada", 30}
u2 := u1 // tam kopya
u2.Name = "Grace"
fmt.Println(u1.Name) // "Ada" — dokunulmadı
fmt.Println(u2.Name) // "Grace"
Pointer/Slice/Map Alanı İçeren Struct
Struct’ı kopyalamak alanın kendisini kopyalar, işaret ettiği şeyi değil. Pointer alanları için bu, kopyanın aynı işaret edileni (pointee) paylaştığı anlamına gelir:
type Wrapper struct {
Data *User
}
w1 := Wrapper{Data: &User{"Ada", 30}}
w2 := w1 // pointer değerini kopyalar, User'ı DEĞİL
w2.Data.Name = "Grace"
fmt.Println(w1.Data.Name) // "Grace" — paylaşılıyor!
Bu, klasik bir sığ kopya (shallow copy) tuzağıdır. Aynı durum struct içindeki slice, map ve channel alanları için de geçerlidir — struct’ı kopyalamak slice/map header‘ını kopyalar, ancak alttaki dizi/hash tablosu/buffer paylaşılır.
Array vs Slice — Kritik Bir Ayrım
// Array: value tipi, sabit boyut, tipin kendisinin bir parçası
a1 := [3]int{1, 2, 3}
a2 := a1 // 3 elemanın da TAM KOPYASI
a2[0] = 99
fmt.Println(a1) // [1 2 3] — etkilenmedi
// Slice: referans benzeri tip (header: pointer, len, cap)
s1 := []int{1, 2, 3}
s2 := s1 // yalnızca HEADER'ı kopyalar, aynı alttaki dizi
s2[0] = 99
fmt.Println(s1) // [99 2 3] — etkilendi!
[3]int ve [4]int, Go’da farklı tiplerdir — array uzunluğu tipin bir parçasıdır.
7. Referans Benzeri Tipler: Slice, Map, Channel
Bu tipler teknik olarak value tipleridir (kopyalanabilirler, pointer değildirler), ancak paylaşılan alttaki veriye işaret eden küçük bir header struct olduklarından referans gibi davranırlar.
Slice Header
type sliceHeader struct {
ptr *T
len int
cap int
}
- Bir slice’ı kopyalamak
ptr,len,cap‘i — üç makine kelimesini — kopyalar, alttaki diziyi değil. append, alttaki diziyi yeniden tahsis edebilir veya etmeyebilir (eğercapaşılırsa, yeni ve daha büyük bir dizi tahsis edilir ve kopyalanır — bu yüzdenappendyeni bir slice döndürür ve neredeyse her zaman yeniden atama yapmalısınız:s = append(s, x)).- Dilimleme (
s[1:3]), aynı alttaki diziye işaret eden yeni bir header oluşturur — bir slice üzerinden yapılan değişiklikler diğerinde de görülür (yeniden tahsis eden birappendbağlantıyı kırana kadar).
s := []int{1, 2, 3, 4, 5}
sub := s[1:3]
sub[0] = 99
fmt.Println(s) // [1 99 3 4 5] — paylaşılan alttaki dizi
Map
type hmap struct {
// dahili runtime yapısı — bucket'lar, sayaç, hash seed, vb.
// map'ler her zaman bu yapıya bir pointer olarak geçirilir/kopyalanır
}
- Map’ler davranış olarak referans tipleridir: bir map değişkenini kopyalamak, aynı alttaki hash tablosuna bir pointer’ı kopyalar.
- Map’ler, harici senkronizasyon (
sync.RWMutexveyasync.Map) olmadan eşzamanlı okuma/yazma için güvenli değildir. - Bir map’in sıfır değeri
nil‘dir;nilbir map’ten okumak güvenlidir (sıfır değeri döner), ancaknilbir map’e yazmak panic’e neden olur.
Channel
- Aynı zamanda referans benzeridir: bir channel değişkeni, runtime’daki bir
hchanyapısına pointer’dır. - Bir channel değişkenini kopyalamak, kuyruğu değil referansı kopyalar — her iki kopya da aynı channel’a işaret eder.
String
- String’ler değiştirilemez (immutable) value tipleridir, dahili olarak salt okunur byte’lara işaret eden bir
{ptr *byte, len int}header’ıdır. - Bir string’i kopyalamak header’ı kopyalar (2 kelime) — ucuzdur — ancak alttaki byte’lar paylaşılır ve string’ler değiştirilemediği için paylaşmak güvenlidir.
string↔[]bytedönüşümü her zaman alttaki byte’ları kopyalar (çünkü[]bytedeğiştirilebilirdir ve runtime paylaşılan string byte’larını korumak zorundadır).
8. Method Receiver’ları: Value vs Pointer
type Counter struct{ n int }
func (c Counter) IncByValue() { c.n++ } // bir KOPYA üzerinde çalışır
func (c *Counter) IncByPointer() { c.n++ } // ORİJİNAL üzerinde çalışır
Genel Kurallar
- Method, receiver’ı değiştirmesi (mutate) gerekiyorsa, pointer receiver kullanın. Value receiver’lı bir method receiver’ını değiştirmeye çalışırsa, çağıranın bakış açısından sessiz bir no-op’tur (hiçbir şey olmaz).
- Struct büyükse, her çağrıda kopyalamadan kaçınmak için pointer receiver kullanın.
- Tutarlı olun: bir tip üzerindeki herhangi bir method pointer receiver kullanıyorsa, o tipteki tüm method’ların pointer receiver kullanması deyimseldir (ve genellikle interface sağlama için gereklidir).
- Tip bir
sync.Mutexveya kopyalanamayan başka bir alan içeriyorsa, her zaman pointer receiver kullanın (kilitli bir mutex’i kopyalamak bir hatadır —go vetbunu tespit eder).
Otomatik Adresleme
Go, adreslenebilir (addressable) bir değer üzerinde pointer-receiver bir method çağırdığınızda otomatik olarak adresini alır:
c := Counter{}
c.IncByPointer() // derleyici bunu (&c).IncByPointer() olarak yeniden yazar
Bu yalnızca c adreslenebilirse çalışır (yerel bir değişken, bir alan, bir array elemanı) — map değerlerinde veya literal değerlerde çalışmaz:
m := map[string]Counter{"a": {}}
m["a"].IncByPointer() // DERLEME HATASI: m["a"] üzerinde pointer method çağrılamaz (adreslenebilir değil)
9. Interface’ler: Dahili (Internal) Temsil
Go’da bir interface değeri, dahili olarak iki kelimelik bir yapı olarak temsil edilir:
type iface struct {
tab *itab // tip bilgisi + method tablosuna pointer (boş olmayan interface'ler için)
data unsafe.Pointer // alttaki somut (concrete) değere pointer
}
// boş interface{} (veya `any`) için:
type eface struct {
_type *_type
data unsafe.Pointer
}
dataher zaman bir pointer’dır — birinterface{}içinde birintsaklasanız bile, Go bu int’i bir yere tahsis eder (genellikle heap’te, buna “boxing” denir) ve buna bir pointer artı tip metadata’sına bir pointer saklar.- Bu yüzden bir değeri bir interface’e atamak heap’e kaçışa neden olabilir — interface’in işaret edeceği kararlı bir adrese sahip olması gerekir.
- İki interface değerini karşılaştırmak (
==), hem tipi hem de alttaki değeri karşılaştırır (dinamik değer karşılaştırması için alttaki tipin normal==kurallarını takip ederek). Alttaki tip karşılaştırılabilir değilse (örn. bir slice), karşılaştırma çalışma zamanında panic’e neden olur.
Örnek: Boxing Maliyeti
var i interface{} = 42 // 42 kutulanır (boxed): heap'te tahsis edilmiş int + eface{type: int, data: ptr}
Küçük tamsayılar, yaygın küçük değerler için derleyici optimizasyonları/statik tablolar sayesinde bazen heap tahsisinden kaçınabilir, ancak genel olarak boxing’in bir maliyeti olduğunu varsayın ve mümkün olduğunda sıcak döngülerde (hot loop) interface dönüşümlerinden kaçının.
10. Nil Interface Tuzağı
Bu, Go’nun en meşhur tuzaklarından biridir.
type MyError struct{}
func (e *MyError) Error() string { return "boom" }
func mayFail() error {
var e *MyError = nil
return e // NIL OLMAYAN bir error interface döndürür!
}
func main() {
err := mayFail()
fmt.Println(err == nil) // false!!
}
Bunun Neden Olduğu
Bir interface değeri, yalnızca hem tip pointer’ı hem de veri pointer’ı nil ise nil‘dir. Burada, mayFail() şu şekilde bir error interface’i döndürür:
tab/_type=*MyError(somut, nil olmayan bir tip tanımlayıcısı)data=nil(pointer değeri nil olarak gerçekleşiyor)
Interface’in kendisi nil değildir — “tipli bir nil” (typed nil) tutar. Bu yüzden deyimsel örüntü (idiomatic pattern) şudur:
func mayFail() error {
var e *MyError = nil
if someCondition {
e = &MyError{}
}
if e == nil {
return nil // açık: gerçekten nil bir interface döndür, tipli nil değil
}
return e
}
Kural: gerçekten “bir değer var ve bu bir nil pointer” demek istemediğiniz sürece, interface tipli bir return değeri üzerinden asla somut bir nil pointer döndürmeyin. Şüpheye düştüğünüzde, interface tipi için doğrudan literal nil‘i döndürün.
11. Interface Sağlama (Satisfaction) ve Method Set’ler
Method Set’ler
| Method tanımındaki receiver tipi | T‘nin method set’ine dahil mi | *T‘nin method set’ine dahil mi |
|---|---|---|
func (t T) M() | ✅ Evet | ✅ Evet |
func (t *T) M() | ❌ Hayır | ✅ Evet |
Bu şu anlama gelir:
type Speaker interface{ Speak() string }
type Dog struct{}
func (d *Dog) Speak() string { return "Woof" }
var s Speaker = Dog{} // DERLEME HATASI: Dog, Speaker'ı implemente etmiyor (Speak, pointer receiver'a sahip)
var s Speaker = &Dog{} // OK
Bu Kuralın Nedeni
Speak, receiver’ı değiştiriyorsa (veya tutarlılık/performans için pointer receiver ile tanımlanmışsa), bunu bir T değeri üzerinden method set aracılığıyla çağırmak, derleyicinin muhtemelen adreslenemeyen/geçici bir değerin adresini sessizce alması gerektirmesi anlamına gelir — Go, pointer-receiver method’ları value tipinin method set’inden hariç tutarak bu belirsizliği yasaklar.
Örtük / Yapısal (Structural) Interface Sağlama
Go interface’leri örtük olarak (implicitly) karşılanır — bir implements anahtar kelimesi yoktur. Eşleşen method set’e sahip herhangi bir tip, interface’i otomatik olarak sağlar. Bu şunları mümkün kılar:
- Ayrıştırma (decoupling): paketlerin, bir interface’i sağlamak için o interface’in paketini import etmesine gerek yoktur.
- Geriye dönük interface’ler: kullanım noktasına yakın olarak, olay gerçekleştikten sonra dar bir interface tanımlayabilirsiniz (çok deyimsel bir Go örüntüsü: “interface kabul et, struct döndür”).
Interface Gömme (Embedding)
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type ReadWriter interface {
Reader
Writer
}
Kalıtım yerine kompozisyon — küçük, tek metodlu interface’ler (io.Reader, io.Writer, io.Closer) ihtiyaç duyuldukça daha büyük olanlara kompoze edilir.
12. Garbage Collection Temelleri
Go’nun GC’si, şu özelliklere sahip eşzamanlı, üç renkli mark-and-sweep (işaretle-süpür) bir toplayıcıdır:
- Programınızla eşzamanlı olarak çalışır (çoğunlukla) — kurulum/devretme için çok kısa bir “stop the world” aşaması vardır, ancak işaretlemenin çoğu goroutine’lerinizle eşzamanlı olarak gerçekleşir.
- Nesil (generational) tabanlı değil, sıkıştırma (compacting) yapmaz: Java’nın GC’sinin aksine, nesneleri bellekte taşımaz (bu,
unsafe.Pointerkodu ve interop için önemlidir) veya nesneleri yaşa göre ayırmaz. GOGC(bir sonraki GC öncesi hedef heap büyüme yüzdesi, varsayılan %100) veGOMEMLIMIT(Go 1.19’da eklenen yumuşak bir bellek limiti) ile ayarlanır.- Go’da sıkıştırma olmadığından, heap parçalanması (fragmentation), uzun süre çalışan, tahsis-yoğun servislerde gerçek (genellikle küçük olsa da) bir husustur.
GC Baskısını Ne Tetikler
- Kaçan değişkenlerden gelen heap tahsisleri (bkz. Bölüm 3).
- Değerlerin interface’lere boxing’i (bkz. Bölüm 9).
- String ↔ []byte dönüşümleri.
- Değişkenleri referans yoluyla yakalayan closure’lar.
- Slice/map’lerin kapasitenin ötesinde tekrar tekrar büyütülmesi (yeniden tahsisler).
GC Baskısını Azaltma
- Buffer’ları
sync.Poolile yeniden kullanın. - Bilinen kapasiteyle slice/map’leri önceden tahsis edin (
make([]T, 0, n)). - Sıcak yollarda gereksiz interface boxing’inden kaçının.
- Optimize etmeden önce
pprof(go tool pprof) ve-gcflags="-m"ile profil çıkarın — tahmin etmeyin.
13. Pratik Öneriler
Ne Zaman Pointer Receiver / Pointer Parametre Kullanmalı
- Fonksiyon/method, argümanı değiştirmesi (mutate) gerekiyorsa.
- Değer büyükse (genel kural: ~3-4 makine kelimesinden büyük, yani çok alanlı struct’lar veya array içerenler).
- Tip bir mutex,
sync.WaitGroupveya kopyalanmaması gereken başka bir tip içeriyorsa. nilaracılığıyla “değer yok” durumunu temsil etmeniz gerekiyorsa (opsiyonel alanlar).- Tutarlılık: tipteki diğer method’lar zaten pointer receiver kullanıyorsa.
Ne Zaman Value Kullanmalı
- Tip küçükse (birkaç int, birkaç string,
time.Duration,image.Pointgibi küçük bir struct). - Değişmezlik (immutability) isteniyorsa — bir değer geçirmek, çağrılanın çağıranın durumunu etkileyemeyeceğini garanti eder.
- Tip kavramsal olarak temel bir “value nesnesiyse” (bir koordinat, bir para miktarı, bir süre gibi).
- Eşzamanlılık: değerleri pointer yerine kopyayla paylaşmak, tüm bir data race hatası kategorisini bertaraf eder (yine de slice/map gibi kendisi referans tipi olan alanlara dikkat etmeniz gerekir).
Genel Deyimler (Idioms)
io.Reader,io.Writer,error— bunları fonksiyon imzalarında interface olarak kabul edin; bu “interface kabul et, somut struct döndür” deyimidir.- Constructor’lardan (
func NewFoo() *Foo) interface değil, somut tipler döndürmeyi tercih edin, birden fazla implementasyonunuz olup bunları gizlemeniz gerekmedikçe. - Bir döngü değişkeninin adresini alıp sonradan kullanmak için saklarken, per-iterasyon değişken semantiğini (Go 1.22 itibariyle büyük ölçüde düzeltildi) anlamadan yapmayın — Go < 1.22’de, döngü değişkenleri iterasyonlar arasında yeniden kullanılıyordu, klasik bir hata kaynağıydı.
// Go 1.22 öncesi hata örüntüsü:
var funcs []func()
for _, v := range []int{1, 2, 3} {
funcs = append(funcs, func() { fmt.Println(v) })
}
// eski Go: 3, 3, 3 yazdırır (tüm closure'lar aynı v'yi paylaşır)
// Go 1.22+: 1, 2, 3 yazdırır (her iterasyonun kendi v'si vardır)
14. Yaygın Hatalar Kılavuzu
| Hata | Açıklama | Çözüm |
|---|---|---|
| Interface’te tipli nil | var p *T = nil; var i I = p → i != nil | Açıkça nil literal döndürün veya atamadan önce somut pointer’ı kontrol edin |
| Value receiver üzerinden değişiklik yapmaya çalışmak | Method değişiklikleri kalıcı olmaz | Pointer receiver kullanın |
| Mutex içeren bir struct’ı kopyalamak | sync.Mutex kopyalanır → kilitleme bozulur | Pointer receiver’lar/pointer ile geçiş kullanın; go vet bunu yakalar |
append sonrası slice takma adlandırması (aliasing) | İki slice, append sonrası aynı alttaki diziyi paylaşabilir veya paylaşmayabilir | Herhangi bir append sonrası paylaşımı varsaymayın; gerekirse kasıtlı olarak yeniden dilimleyin |
| Map, eşzamanlı erişim için güvenli değil | Eşzamanlı okuma/yazma panic’e neden olur (“concurrent map read and map write”) | sync.RWMutex veya sync.Map kullanın |
nil bir map’e yazma | Çalışma zamanında panic olur | Önce make(map[K]V) ile başlatın |
| Adreslenemeyen bir değer üzerinde pointer method çağırma | Map değerlerinde/literal’lerde derleme hatası | Önce bir değişkene atayın |
| Sıcak döngüde interface boxing’i | Gizli heap tahsisleri, GC baskısı | Sıcak yollarda gereksiz interface{} dönüşümlerinden kaçının; profil çıkarın |
| Karşılaştırılamaz dinamik tipe sahip interface’leri karşılaştırma | == üzerinde çalışma zamanı panic’i | Alttaki tipi bilin veya reflect.DeepEqual‘ı dikkatlice kullanın |
| Büyük struct’ı tekrar tekrar value olarak geçirme | Her çağrıda kopyalama ek yükü | Büyük struct’lar için pointer receiver/parametre kullanın |
Özet
Go’nun tasarım felsefesi şudur: varsayılan olarak value semantics, açık seçimle pointer semantics, hepsi arka planda escape-analysis odaklı bir stack/heap kararı ve eşzamanlı bir garbage collector tarafından yönetilir. Interface’ler, polimorfizmi mümkün kılan bir dolaylılık katmanı ekler (tip bilgisine ve kutulanmış veriye işaret eden şişman, iki kelimelik bir header), ancak bu, gerçek performans ve doğruluk tuzaklarıyla birlikte gelir — özellikle o meşhur tipli-nil tuzağı. Go’da ustalaşmak, verinin ne zaman kopyalandığını ve ne zaman paylaşıldığını içselleştirmek ve bu seçimi tesadüfen değil, bilinçli olarak yapmak anlamına gelir.