Go Derinlemesine İnceleme — Bellek, Pointer'lar, Value Semantics ve Interface'ler

18 Ağustos 2026 · netologist · 16 dakika, 3240 kelime ·

İçindekiler

  1. Go’nun Bellek Modeli — Genel Bakış
  2. Stack ve Heap
  3. Escape Analysis (Kaçış Analizi)
  4. Value’lar ve Pointer’lar
  5. Value Semantics vs Pointer Semantics
  6. Struct’lar: Pratikte Kopyalama Semantiği
  7. Referans Benzeri Tipler: Slice, Map, Channel
  8. Method Receiver’ları: Value vs Pointer
  9. Interface’ler: Dahili (Internal) Temsil
  10. Nil Interface Tuzağı
  11. Interface Sağlama (Satisfaction) ve Method Set’ler
  12. Garbage Collection Temelleri
  13. Pratik Öneriler
  14. 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:


2. Stack ve Heap

Stack

Heap

Bunun Neden Önemli Olduğu

Her heap tahsisi:

  1. Stack tahsisinden daha yavaştır.
  2. GC baskısının bir kaynağıdır (GC bunu taramak, potansiyel olarak iş yapmak zorundadır, vb.)
  3. 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

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)

Pointer’lar C++ Anlamında “Referans” Değildir

Go pointer’ları C++ referanslarından daha kısıtlı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

  1. Ö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).
  2. 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.
  3. 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)

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
}
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
}

Channel

String


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

  1. 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).
  2. Struct büyükse, her çağrıda kopyalamadan kaçınmak için pointer receiver kullanın.
  3. 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).
  4. Tip bir sync.Mutex veya kopyalanamayan başka bir alan içeriyorsa, her zaman pointer receiver kullanın (kilitli bir mutex’i kopyalamak bir hatadır — go vet bunu 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
}

Ö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:

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 tipiT‘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:

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:

GC Baskısını Ne Tetikler

GC Baskısını Azaltma


13. Pratik Öneriler

Ne Zaman Pointer Receiver / Pointer Parametre Kullanmalı

Ne Zaman Value Kullanmalı

Genel Deyimler (Idioms)

// 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

HataAçıklamaÇözüm
Interface’te tipli nilvar p *T = nil; var i I = pi != nilAçıkça nil literal döndürün veya atamadan önce somut pointer’ı kontrol edin
Value receiver üzerinden değişiklik yapmaya çalışmakMethod değişiklikleri kalıcı olmazPointer receiver kullanın
Mutex içeren bir struct’ı kopyalamaksync.Mutex kopyalanır → kilitleme bozulurPointer 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şmayabilirHerhangi bir append sonrası paylaşımı varsaymayın; gerekirse kasıtlı olarak yeniden dilimleyin
Map, eşzamanlı erişim için güvenli değilEş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ğırmaMap değerlerinde/literal’lerde derleme hatasıÖnce bir değişkene atayın
Sıcak döngüde interface boxing’iGizli 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’iAlttaki tipi bilin veya reflect.DeepEqual‘ı dikkatlice kullanın
Büyük struct’ı tekrar tekrar value olarak geçirmeHer ç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.