İçerikler

Go (Golang) Nasıl Çalışır? - Derinlemesine Runtime Analizi

İçerikler

Go (Golang) Nasıl Çalışır?

Go (Golang), Google tarafından geliştirilen, modern yazılım geliştirme ihtiyaçlarını karşılamak için tasarlanmış bir programlama dilidir. Bu makalede, Go’nun derleme sürecinden runtime mekanizmalarına, goroutine’lerden garbage collection’a kadar tüm çalışma modelini detaylı bir şekilde inceleyeceğiz.

Özet

  • Derleme Süreci: Lexer, Parser, Type Checker, SSA, Code Generation
  • Runtime Mekanizmaları: Scheduler (M:P:G), Memory Manager, Garbage Collector
  • Concurrency Model: Goroutine’ler, Channel’lar, Select statement
  • Performance: Native binary, düşük latency, yüksek throughput
  • Production Ready: Case studies, debugging senaryoları, optimization techniques

Not: Bu makale, Go runtime’ın derinlemesine bir incelemesidir. Production uygulamalarında bu bilgileri kullanırken, Go’nun resmi dokümantasyonunu ve best practice’leri de göz önünde bulundurmanız önerilir.


1. Go Programının Yaşam Döngüsü

Bir Go programı yazıp çalıştırdığınızda şu adımlardan geçer:

Adım Adım Açıklama

  1. Kaynak Kod (.go): Go kaynak dosyaları yazılır
  2. Derleme (Compile): go build veya go run komutu ile derleme yapılır
  3. Çalıştırılabilir Dosya (Binary): İşletim sistemine özel binary oluşturulur
  4. Go Runtime Başlatılması: Runtime bileşenleri initialize edilir
  5. main() Fonksiyonunun Çalışması: Program başlar

Go, yorumlanan bir dil değildir. Kodunuz önceden derlenir (ahead-of-time compilation) ve işletim sisteminde doğrudan çalıştırılır. Bu sayede:

  • Hızlı başlangıç: JIT derleme gecikmesi yok
  • Öngörülebilir performans: Runtime derleme overhead’i yok
  • Küçük binary boyutu: Runtime dahil olsa da optimize edilmiş

2. Derleme (Compile) Süreci

Go derleyicisi, modern bir derleme pipeline’ı kullanır:

Derleme Aşamaları

2.1 Lexer & Tokenizer

Kaynak kodu token’lara ayırır:

  • Anahtar kelimeler (func, var, if)
  • Operatörler (+, -, :=)
  • Literaller (string, number)
  • Tanımlayıcılar (değişken, fonksiyon isimleri)

2.2 Parser (AST Generation)

Token’ları Abstract Syntax Tree (AST) yapısına dönüştürür:

1
2
3
4
// Örnek kod
func add(a, b int) int {
    return a + b
}

Bu kod şu AST yapısını oluşturur:

  • Function declaration node
  • Parameter list nodes
  • Return statement node
  • Binary expression node

2.3 Type Checker

Statik tip kontrolü yapar:

  • Tip uyumsuzluklarını tespit eder
  • Interface implementasyonlarını kontrol eder
  • Tip çıkarımı (type inference) yapar

2.4 Escape Analysis

Değişkenlerin stack’te mi yoksa heap’te mi saklanacağına karar verir:

1
2
3
4
func example() *int {
    x := 42  // Escape analysis: x heap'e kaçar
    return &x
}

2.5 SSA (Static Single Assignment)

Kod, SSA formuna dönüştürülür. Bu, optimizasyonlar için kritiktir:

SSA Form Özellikleri:

  • Her değişken sadece bir kez assign edilir
  • Data flow analizi kolaylaşır
  • Optimizasyonlar daha etkili olur

2.6 SSA Optimization Passes

SSA formunda birçok optimization pass çalıştırılır:

1. Dead Code Elimination

Kullanılmayan kod’u kaldırır:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Önce
func example() {
    x := 42
    y := 10
    return x  // y kullanılmıyor
}

// Sonra (optimize edilmiş)
func example() {
    x := 42
    return x  // y kaldırıldı
}

Nasıl Çalışır:

  • Data flow analizi ile kullanılmayan değişkenleri tespit eder
  • Unreachable code’u kaldırır
  • Unused function’ları kaldırır

2. Constant Propagation

Sabit değerleri yayar:

1
2
3
4
5
6
// Önce
const x = 42
y := x + 10  // 52 olarak hesaplanabilir

// Sonra (optimize edilmiş)
y := 52  // Compile-time'da hesaplandı

Nasıl Çalışır:

  • Constant expression’ları compile-time’da hesaplar
  • Constant değerleri kullanım yerlerine yayar
  • Conditional branch’leri optimize eder

3. Common Subexpression Elimination (CSE)

Aynı ifadeyi tekrar hesaplamayı önler:

1
2
3
4
5
6
7
// Önce
x := a + b
y := a + b  // Tekrar hesaplanıyor

// Sonra (optimize edilmiş)
x := a + b
y := x  // Tekrar kullanılıyor

Nasıl Çalışır:

  • Expression’ları hash table’da saklar
  • Aynı expression’ı tekrar bulursa reuse eder
  • Register pressure’ı azaltır

4. Loop Invariant Code Motion

Loop dışına taşınabilen kod’u taşır:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Önce
for i := 0; i < n; i++ {
    x := expensive()  // Her iterasyonda hesaplanıyor
    result[i] = x + i
}

// Sonra (optimize edilmiş)
x := expensive()  // Loop dışına taşındı
for i := 0; i < n; i++ {
    result[i] = x + i
}

Nasıl Çalışır:

  • Loop invariant analizi yapar
  • Loop içinde değişmeyen expression’ları tespit eder
  • Bunları loop dışına taşır

5. Inlining Decisions

Küçük fonksiyonları inline eder:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Önce
func add(a, b int) int {
    return a + b
}

func main() {
    x := add(1, 2)  // Function call overhead
}

// Sonra (optimize edilmiş)
func main() {
    x := 1 + 2  // Inline edildi
}

Inlining Kriterleri:

  • Fonksiyon boyutu (genellikle < 40 satır)
  • Call frequency
  • Function complexity
  • Recursive değil

Inlining Avantajları:

  • Function call overhead’i yok
  • Daha fazla optimization fırsatı
  • Better register allocation

Inlining Dezavantajları:

  • Binary size artabilir
  • Instruction cache pressure

2.7 Code Generation

SSA’dan makine koduna dönüşüm:

  • Register allocation
  • Instruction selection
  • Peephole optimizations

Register Allocation:

  • Live variable analysis
  • Register spilling (gerekirse)
  • Register coalescing

Instruction Selection:

  • Platform-specific instruction’ları seçer
  • Instruction scheduling
  • Pipeline optimization

Derleme Sonucu

Derleme sonunda platforma özel binary oluşur:

Platform Binary Format Örnek
Linux ELF (Executable and Linkable Format) ./myapp
Windows PE (Portable Executable) myapp.exe
macOS Mach-O ./myapp

Not: Go binary’leri çoğu zaman runtime’ı da içinde barındırır. Bu yüzden deployment oldukça kolaydır - sadece binary’yi kopyalayıp çalıştırabilirsiniz.

Cross-Compilation

Go, cross-compilation’ı native olarak destekler:

1
2
3
4
5
# Linux için Windows binary
GOOS=windows GOARCH=amd64 go build

# macOS için ARM64 binary
GOOS=darwin GOARCH=arm64 go build

3. Go Runtime Nedir?

Go Runtime, program çalışırken arka planda aktif olan bir sistemdir. JavaScript’teki V8 Engine ne ise, Go için Go Runtime odur.

Runtime Bileşenleri

3.1 Goroutine Scheduler

  • Goroutine’leri OS thread’lerine dağıtır
  • Work-stealing algoritması kullanır
  • M:P:G modeli ile çalışır

3.2 Memory Manager

  • Stack ve heap yönetimi
  • Memory pool’ları
  • Allocation optimizasyonları

3.3 Garbage Collector

  • Concurrent mark-and-sweep
  • Low-latency tasarım
  • Automatic memory reclamation

3.4 Channel Implementation

  • Channel’ların runtime implementasyonu
  • Select statement mekanizması
  • Blocking/unblocking logic

3.5 System Calls

  • OS ile iletişim
  • Network I/O
  • File I/O

Runtime Başlatılması

Program başladığında runtime şu sırayla initialize edilir. Bu süreç runtime.main() fonksiyonundan önce gerçekleşir:

Bootstrap Sequence Detayları

1. Entry Point (_rt0_amd64)

1
2
3
4
5
// runtime/rt0_linux_amd64.s (assembly)
TEXT _rt0_amd64(SB),NOSPLIT,$-8
    MOVQ    0(SP), DI  // argc
    LEAQ    8(SP), SI  // argv
    JMP     runtime·rt0_go(SB)

2. TLS (Thread Local Storage) Initialization

TLS, her OS thread’in kendi goroutine (g), machine (m) ve processor (p) pointer’larına hızlı erişim sağlar. Bu, scheduler’ın performansı için kritiktir.

3. Runtime Args Parsing

  • GOGC değişkenini okur
  • GOMAXPROCS değerini belirler
  • GODEBUG flag’lerini parse eder
  • Memory limit’leri ayarlar

4. CPU Detection

1
2
3
4
5
// runtime/os_linux.go
func osinit() {
    ncpu = getproccount()  // CPU çekirdek sayısı
    physPageSize = getPageSize()  // Sayfa boyutu
}

5. Memory Allocator Initialization

  • mcache, mcentral, mheap yapıları oluşturulur
  • Size class’lar initialize edilir
  • Memory pool’lar hazırlanır

6. Scheduler Initialization

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// runtime/proc.go
func schedinit() {
    // P'leri oluştur (GOMAXPROCS kadar)
    procs := runtime.GOMAXPROCS(0)
    for i := int32(0); i < procs; i++ {
        newproc()
    }
    
    // İlk M'yi oluştur
    mcommoninit(m0)
}

7. Signal Handling Setup

Go, signal’ları şu amaçlarla kullanır:

  • SIGURG: Async preemption (Go 1.14+)
  • SIGQUIT: Stack trace dump (Ctrl+)
  • SIGSEGV: Segmentation fault handling
  • SIGINT/SIGTERM: Graceful shutdown

8. Network Poller Initialization

1
2
3
4
5
// runtime/netpoll.go
func netpollinit() {
    // epoll (Linux), kqueue (BSD), IOCP (Windows)
    epfd = epollcreate1(_EPOLL_CLOEXEC)
}

Network poller, I/O işlemlerini non-blocking yapmak için kullanılır.

9. Defer Mechanism Defer stack’i ve panic/recover mekanizması hazırlanır.

10. runtime.main() Çağrısı

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// runtime/proc.go
func main() {
    // Tüm init() fonksiyonlarını çalıştır
    doInit(&runtime_inittask)
    doInit(&main_inittask)
    
    // main.main() çağrılır
    fn := main_main
    fn()
    
    // Program bitti
    exit(0)
}

Runtime Başlatma Zaman Çizelgesi

Toplam bootstrap süresi genellikle 1-2 milisaniye civarındadır.


4. Goroutine Nedir?

Goroutine, Go’nun concurrency modelinin temelidir. OS thread’lerinden çok daha hafif ve verimli bir eşzamanlılık birimidir.

Goroutine Oluşturma

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Basit goroutine
go doSomething()

// Anonim fonksiyon ile
go func() {
    fmt.Println("Goroutine çalışıyor")
}()

// Parametreli goroutine
go processData(data)

Goroutine vs Thread Karşılaştırması

Özellik OS Thread Goroutine
Başlangıç Stack ~2 MB ~2 KB
Başlatma Süresi ~1-2 ms ~1-2 µs
Maksimum Sayı Binlerce Milyonlarca
Scheduler OS Kernel Go Runtime
Context Switch Pahalı (kernel mode) Ucuz (user mode)

Goroutine Yaşam Döngüsü

Goroutine Özellikleri

  1. Hafiflik: 2KB başlangıç stack boyutu
  2. Hızlı Başlatma: Mikrosaniyeler içinde başlatılabilir
  3. Dinamik Stack: İhtiyaç duydukça büyür (max 1GB)
  4. Cooperative Scheduling: Goroutine’ler gönüllü olarak CPU’yu bırakır
  5. Work Stealing: Boşta kalan P’ler başka P’lerden iş çalar

Pratik Örnek

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
    "fmt"
    "time"
)

func main() {
    // 10,000 goroutine başlat
    for i := 0; i < 10000; i++ {
        go func(id int) {
            fmt.Printf("Goroutine %d çalışıyor\n", id)
            time.Sleep(1 * time.Second)
        }(i)
    }
    
    time.Sleep(2 * time.Second)
    fmt.Println("Tüm goroutine'ler tamamlandı")
}

Bu örnekte 10,000 goroutine başlatılabilir. Aynı sayıda OS thread başlatmaya çalışsaydınız, sistem kaynaklarınız tükenirdi.


5. Go Scheduler Nasıl Çalışır?

Go scheduler, goroutine’leri OS thread’lerine dağıtan akıllı bir sistemdir. M:P:G modeli kullanır.

M:P:G Modeli

Model Bileşenleri

G (Goroutine)

  • Çalışacak iş birimi
  • Kendi stack’ine sahip
  • Program counter (PC) içerir
  • Channel, mutex gibi wait objeleri içerir

P (Processor)

  • Çalışma kapasitesi (context)
  • Her P, bir local goroutine queue’suna sahip
  • Sayısı genellikle CPU çekirdek sayısına eşittir (GOMAXPROCS)
  • Work-stealing için global queue’ya erişimi vardır

M (Machine)

  • OS thread’i temsil eder
  • P ile ilişkilendirilir
  • Gerçek CPU’da çalışır
  • System call yaparken P’den ayrılabilir

Scheduler Algoritması

Scheduler Özellikleri

  1. Work Stealing: Boşta kalan P’ler, dolu P’lerin queue’larından iş çalar
  2. Preemption: Goroutine’ler 10ms’de bir preempt edilir (Go 1.14+)
  3. System Call Handling: System call yapan goroutine, M’den ayrılır ve yeni M oluşturulur
  4. Network Poller: I/O işlemleri için özel poller thread’i
  5. Spinning Threads: CPU’yu verimli kullanmak için spinning mekanizması

Preemption Mekanizması (Go 1.14+)

Go 1.14’ten önce, goroutine’ler sadece cooperative olarak preempt ediliyordu (runtime.Gosched(), channel operations, function calls). Bu, CPU yoğun goroutine’lerin diğerlerini aç bırakmasına neden olabiliyordu.

Async Preemption (Go 1.14+)

Preemption Tipleri:

  1. Cooperative Preemption (Eski yöntem)

    • runtime.Gosched() çağrısı
    • Channel operations
    • Function call boundaries
    • Stack growth
  2. Async Preemption (Go 1.14+)

    • Sysmon goroutine: Her 10ms’de bir kontrol eder
    • SIGURG signal: Preempt edilecek goroutine’e gönderilir
    • Function prologue: Her fonksiyon başında preempt flag kontrol edilir
    • Stack scanning: Preempt edilecek goroutine’in stack’i taranır
1
2
3
4
5
6
// Preemption kontrolü (her fonksiyon başında)
func functionPrologue() {
    if getg().preempt {
        goschedImpl()  // Preempt edil
    }
}

Preemption Timeline:

🔧 Production Notu:

Async preemption mekanizması özellikle yüksek CPU kullanan servislerde latency spike’larını önlemek için kritiktir. CPU-bound goroutine’lerin diğer goroutine’leri aç bırakmasını engelleyerek, production’da daha öngörülebilir performans sağlar.

Spinning Threads

Spinning, bir P’nin iş beklerken CPU’yu boş bırakmak yerine aktif olarak beklemesidir. Bu, yeni goroutine’ler geldiğinde hızlı tepki vermeyi sağlar.

Spinning Stratejisi:

  • P, local queue boşken ~1ms spinning yapar
  • Bu süre içinde yeni goroutine gelirse hemen çalıştırır
  • Spinning süresi dolunca OS thread sleep’e gider
  • Yeni goroutine geldiğinde thread uyandırılır

Spinning Avantajları:

  • Düşük latency (yeni işler hemen çalışır)
  • CPU verimliliği (idle thread’ler CPU’yu bloke etmez)

Spinning Dezavantajları:

  • CPU kullanımı (spinning sırasında CPU çalışır)
  • Güç tüketimi (laptop’larda önemli)

Network Poller Entegrasyonu

Network poller, I/O işlemlerini non-blocking yapmak için kullanılır. Go, epoll (Linux), kqueue (BSD), IOCP (Windows) gibi platform-specific API’leri kullanır.

Network Poller Yapısı:

1
2
3
4
5
6
7
// runtime/netpoll.go
type pollDesc struct {
    fd      uintptr
    closing bool
    rg      uintptr  // Read goroutine
    wg      uintptr  // Write goroutine
}

Network Poller Thread:

  • Tek bir dedicated OS thread
  • epoll_wait() / kqueue() ile event’leri bekler
  • I/O tamamlandığında ilgili goroutine’i uyandırır

System Call Wrapping

System call yapan goroutine’ler, P’yi serbest bırakmalıdır. Böylece başka goroutine’ler çalışabilir.

entersyscall/exitsyscall Mekanizması:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// runtime/proc.go

// System call'a girerken
func entersyscall() {
    // P'yi serbest bırak (başka M kullanabilsin)
    releasep()
    // M'yi system call'da işaretle
    getg().m.incallsyscall = true
}

// System call'dan çıkarken
func exitsyscall() {
    // P bulmaya çalış
    if oldp := getg().m.oldp; oldp != nil {
        // Eski P'yi al
        acquirep(oldp)
    } else {
        // Yeni P bul
        acquirep(pidleget())
    }
}

System Call Senaryoları:

  1. Blocking System Call (read, write, accept)

    • P serbest bırakılır
    • Yeni M oluşturulabilir (eğer gerekiyorsa)
    • System call tamamlanınca P bulunur
  2. Non-Blocking System Call (hızlı işlemler)

    • P tutulur (kısa süreli)
    • System call hemen tamamlanır
    • P’yi serbest bırakmaya gerek yok

M Oluşturma Stratejisi:

M Limit:

  • Varsayılan: 10,000 M
  • runtime/debug.SetMaxThreads() ile ayarlanabilir
  • Çok fazla M, OS kaynaklarını tüketir

Work Stealing Detayları

Work stealing, boşta kalan P’lerin dolu P’lerden iş çalmasıdır.

Work Stealing Algoritması:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// runtime/proc.go
func findrunnable() *g {
    // 1. Local queue'dan al
    if gp := runqget(_p_); gp != nil {
        return gp
    }
    
    // 2. Global queue'dan al
    if sched.runqsize != 0 {
        return globrunqget(_p_, 0)
    }
    
    // 3. Work stealing
    for i := 0; i < 4; i++ {
        // Rastgele bir P seç
        p2 := allp[fastrand()%len(allp)]
        if p2 != _p_ && !p2.runqempty() {
            // P2'nin local queue'sundan yarısını çal
            n := p2.runq.len / 2
            for j := 0; j < n; j++ {
                gp := p2.runq.pop()
                _p_.runq.put(gp)
            }
            return _p_.runq.get()
        }
    }
    
    // 4. Network poller kontrol
    if netpollinited() {
        if gp := netpoll(0); gp != nil {
            return gp
        }
    }
    
    // 5. Idle
    return nil
}

GOMAXPROCS

1
runtime.GOMAXPROCS(4) // 4 P kullan

Varsayılan olarak CPU çekirdek sayısına eşittir. Artırırsanız:

  • Daha fazla paralelizm
  • Daha fazla context switch overhead
  • Daha fazla bellek kullanımı

GOMAXPROCS Tuning:

1
2
3
4
5
6
7
8
// CPU yoğun işler için
runtime.GOMAXPROCS(runtime.NumCPU())

// I/O yoğun işler için
runtime.GOMAXPROCS(runtime.NumCPU() * 2)

// Düşük latency için
runtime.GOMAXPROCS(runtime.NumCPU())

🔧 Production Notu:

GOMAXPROCS değerini production’da workload tipine göre ayarlamak kritiktir. I/O-bound servislerde CPU sayısının 2-4 katı, CPU-bound servislerde ise CPU sayısı kadar ayarlanmalıdır. Yanlış ayar, context switch overhead’i veya CPU underutilization’a neden olabilir.

Pratik Örnek: Scheduler’ı Gözlemleme

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    fmt.Printf("CPU Çekirdekleri: %d\n", runtime.NumCPU())
    fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
    
    // 100 goroutine başlat
    for i := 0; i < 100; i++ {
        go func(id int) {
            for {
                // CPU yoğun iş
                runtime.Gosched() // CPU'yu gönüllü bırak
            }
        }(i)
    }
    
    time.Sleep(1 * time.Second)
    fmt.Printf("Aktif Goroutine Sayısı: %d\n", runtime.NumGoroutine())
}

Scheduler Trace Analizi

1
2
3
4
5
# Trace dosyası oluştur
GODEBUG=schedtrace=1000 go run main.go

# Çıktı:
# SCHED 1000ms: gomaxprocs=4 idleprocs=0 threads=5 spinningthreads=0 idlethreads=0 runqueue=0 [0 0 0 0]

Trace Çıktısı Açıklaması:

  • gomaxprocs=4: 4 P aktif
  • idleprocs=0: Boşta P yok
  • threads=5: 5 OS thread (4 M + 1 network poller)
  • spinningthreads=0: Spinning thread yok
  • idlethreads=0: Idle thread yok
  • runqueue=0: Global queue’da goroutine yok
  • [0 0 0 0]: Her P’nin local queue’sundaki goroutine sayısı

6. Channel’lar ile İletişim

Go’da goroutine’ler genellikle shared memory yerine channel kullanarak haberleşir. Bu yaklaşım:

“Belleği paylaşarak iletişim kurma, iletişim kurarak belleği paylaş.”

felsefesine dayanır.

Channel Tipleri

Unbuffered Channel

1
2
3
4
5
6
7
ch := make(chan int) // Unbuffered

go func() {
    ch <- 42  // Bloklanır, alıcı gelene kadar bekler
}()

value := <-ch  // Bloklanır, gönderici gelene kadar bekler

Özellikler:

  • Senkron iletişim (rendezvous)
  • Gönderici ve alıcı aynı anda hazır olmalı
  • Blocking operation

Buffered Channel

1
2
3
4
5
6
ch := make(chan int, 3) // 3 kapasiteli buffer

ch <- 1  // Non-blocking (buffer'da yer var)
ch <- 2  // Non-blocking
ch <- 3  // Non-blocking
ch <- 4  // Blocking (buffer dolu)

Özellikler:

  • Asenkron iletişim
  • Buffer dolana kadar non-blocking
  • Buffer doluysa blocking

Channel İşlemleri

Select Statement

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
select {
case msg1 := <-ch1:
    fmt.Println("ch1'den mesaj:", msg1)
case msg2 := <-ch2:
    fmt.Println("ch2'den mesaj:", msg2)
case ch3 <- 42:
    fmt.Println("ch3'e gönderildi")
default:
    fmt.Println("Hiçbiri hazır değil")
}

Select mekanizması:

Channel Kapanması

1
2
3
4
5
6
close(ch)  // Channel'ı kapat

value, ok := <-ch
if !ok {
    // Channel kapatıldı
}

Kapalı channel özellikleri:

  • Receive işlemi hemen zero value döner
  • Send işlemi panic oluşturur
  • Kapalı channel’a tekrar close() panic oluşturur

Channel Patterns

1. Worker Pool Pattern

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func workerPool(jobs <-chan int, results chan<- int) {
    for job := range jobs {
        result := process(job)
        results <- result
    }
}

jobs := make(chan int, 100)
results := make(chan int, 100)

// 10 worker başlat
for w := 0; w < 10; w++ {
    go workerPool(jobs, results)
}

// İşleri gönder
for j := 1; j <= 100; j++ {
    jobs <- j
}
close(jobs)

2. Fan-Out / Fan-In Pattern

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Fan-out: Bir channel'dan birden fazla worker'a dağıt
func fanOut(input <-chan int, outputs []chan int) {
    for val := range input {
        for _, out := range outputs {
            out <- val
        }
    }
}

// Fan-in: Birden fazla channel'dan tek channel'a topla
func fanIn(inputs []<-chan int, output chan<- int) {
    var wg sync.WaitGroup
    for _, in := range inputs {
        wg.Add(1)
        go func(ch <-chan int) {
            defer wg.Done()
            for val := range ch {
                output <- val
            }
        }(in)
    }
    wg.Wait()
    close(output)
}

7. Bellek Yönetimi

Go’da bellek yönetimi otomatiktir, ancak stack ve heap arasındaki farkı anlamak performans için kritiktir.

Stack vs Heap

Özellik Stack Heap
Allocation Hızı Çok Hızlı (pointer aritmetiği) Yavaş (GC ile)
Deallocation Otomatik (fonksiyon bitince) GC tarafından
Boyut Küçük (MB seviyesi) Büyük (GB seviyesi)
Erişim LIFO Rastgele
Thread Safety Her goroutine’in kendi stack’i Paylaşılan

Escape Analysis

Go derleyicisi, bir değişkenin stack’te mi yoksa heap’te mi saklanacağına escape analysis ile karar verir.

Escape Analysis Örnekleri

Stack’te Kalır

1
2
3
4
func stackExample() int {
    x := 42  // Stack'te
    return x
}

🔧 Production Notu:

Escape analysis’ı anlamak production performansı için kritiktir. go build -gcflags=-m komutu ile hangi değişkenlerin heap’e kaçtığını görebilirsiniz. Stack’te kalan değişkenler GC overhead’i olmadan çalışır, bu da özellikle hot path’lerde önemli performans kazancı sağlar.

Heap’e Kaçar

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func heapExample() *int {
    x := 42  // Heap'e kaçar (pointer return)
    return &x
}

func channelExample() {
    ch := make(chan *int)
    x := 42
    ch <- &x  // x heap'e kaçar
}

func closureExample() func() int {
    x := 42  // Heap'e kaçar (closure)
    return func() int {
        return x
    }
}

Bellek Yapısı

Memory Layout Görselleştirmesi

Go programının bellek düzeni (Linux x86-64):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
+-------------------+  <- 0x7fffffffffff (High Address)
|                   |
|   Stack (G1)      |  <- Goroutine 1 stack (2KB-1GB)
|   [Local vars]    |
+-------------------+
|   Stack (G2)      |  <- Goroutine 2 stack
|   [Local vars]    |
+-------------------+
|       ...         |
+-------------------+
|                   |
|       Heap        |  <- Dynamic memory
|  [mcache spans]  |     - Small objects (mcache)
|  [mcentral]      |     - Central pools
|  [mheap arenas]  |     - Large objects
|  [GC metadata]   |     - GC structures
|                   |
+-------------------+
|   Data Segment    |  <- Static data
|  [Global vars]   |     - Global variables
|  [BSS]           |     - Uninitialized data
|  [Constants]     |     - Read-only constants
+-------------------+
|   Text Segment   |  <- Executable code
|  [Binary Code]   |     - Machine instructions
|  [Runtime]       |     - Go runtime code
+-------------------+  <- 0x400000 (Low Address)

Memory Segment Detayları:

Memory Layout Özellikleri:

Segment Yön Boyut Özellik
Stack Aşağı 2KB-1GB Per goroutine, guard pages
Heap Yukarı Dinamik GC tarafından yönetilir
Data - Statik Global variables, constants
Text - Statik Executable code, read-only

Guard Pages:

  • Stack overflow’u tespit etmek için
  • Stack’in sonunda özel sayfalar
  • Erişim → segmentation fault

Stack Büyümesi ve Küçülmesi

Goroutine stack’i dinamik olarak büyür ve küçülür:

Stack büyüme mekanizması:

  1. Stack overflow riski tespit edilir (guard page’e yaklaşma)
  2. Yeni, daha büyük stack oluşturulur (2x büyüklükte)
  3. Eski stack’ten yeni stack’e kopyalama yapılır
  4. Pointer’lar güncellenir (stack copying GC ile)
  5. Eski stack serbest bırakılır

Stack shrinking mekanizması:

Stack Shrinking Koşulları:

  • GC sırasında stack scanning yapılır
  • Stack’in %50’den fazlası kullanılmıyorsa küçültülür
  • Minimum stack boyutu: 2KB
  • Stack shrinking, GC overhead’ini azaltır

Stack Splitting vs Stack Copying

Go, stack büyümesi için iki farklı yaklaşım denedi:

Stack Splitting (Go 1.2 ve Öncesi)

Nasıl Çalışırdı:

  1. Stack büyümesi gerektiğinde, yeni bir stack segment’i allocate edilirdi
  2. Eski stack’teki pointer’lar yeni stack’e işaret edecek şekilde güncellenirdi
  3. Stack, linked list gibi segment’lerden oluşurdu

Sorunlar:

  • Hot split problem: Sık büyüyen stack’lerde performans sorunu
  • Complex pointer updates: Tüm pointer’ları güncellemek zor
  • Cache locality: Segment’ler farklı memory bölgelerinde
  • GC complexity: Stack scanning daha karmaşık

Stack Copying (Go 1.3+)

Nasıl Çalışır:

  1. Yeni, daha büyük stack allocate edilir (2x)
  2. Eski stack’ten yeni stack’e tüm veri kopyalanır
  3. Pointer’lar güncellenir (stack copying GC ile)
  4. Eski stack serbest bırakılır

Avantajlar:

  • Basitlik: Tek bir continuous memory region
  • Performance: Daha iyi cache locality
  • GC simplicity: Stack scanning daha basit
  • Predictability: Daha öngörülebilir performans

Neden Copying Tercih Edildi:

Copying Overhead:

  • Copy cost: ~1-5µs (stack size’a göre)
  • Pointer update: GC tarafından otomatik
  • Frequency: Nadir (stack büyümesi sık değil)

Copying Optimizasyonları:

  • Copy-on-write: Mümkün olduğunda
  • Bulk copy: Memory copy optimizasyonları
  • GC integration: Stack copying GC ile entegre

Memory Allocator Mimarisi: mcache, mcentral, mheap

Go’nun memory allocator’ü üç katmanlı bir yapı kullanır:

mcache (Per-P Cache)

Her P’nin kendi mcache’i vardır. Lock-free allocation sağlar.

1
2
3
4
5
// runtime/mcache.go
type mcache struct {
    alloc [numSpanClasses]*mspan  // Size class'lara göre span'ler
    // ...
}

Özellikler:

  • Lock-free: P’ye özel olduğu için lock gerekmez
  • Hızlı allocation: Local cache’den direkt alır
  • Refill: Cache boşalınca mcentral’dan doldurulur

mcentral (Global Pool)

Tüm P’ler tarafından paylaşılan merkezi pool.

1
2
3
4
5
6
7
// runtime/mcentral.go
type mcentral struct {
    spanclass spanClass
    partial [2]spanSet  // Partial spans
    full    [2]spanSet  // Full spans
    // ...
}

Özellikler:

  • Lock-protected: Concurrent access için lock kullanır
  • Size class bazlı: Her size class için ayrı mcentral
  • Span management: Partial ve full span’leri yönetir

mheap (OS Memory)

OS’ten bellek alan ve span’leri yöneten ana yapı.

1
2
3
4
5
6
7
8
9
// runtime/mheap.go
type mheap struct {
    arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
    central [numSpanClasses]struct {
        mcentral mcentral
        pad      [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
    }
    // ...
}

Özellikler:

  • Arena-based: Büyük bellek blokları (64MB arena)
  • Span allocation: Span’leri arena’lardan ayırır
  • OS interaction: mmap/munmap ile OS ile iletişim

Span Yapısı

Span, bellek yönetiminin temel birimidir. Bir veya daha fazla sayfa (page) içerir.

Span Özellikleri:

  • Size: 8KB ile 512KB arası (sayfa sayısına göre)
  • Size class: Span içindeki object boyutunu belirler
  • State: Free, partial, full
  • Link list: mcentral’da linked list ile yönetilir

Span Lifecycle:

Size Class Mekanizması

Go, 67 farklı size class kullanır:

1
2
3
4
5
6
7
// runtime/sizeclasses.go
// Size class 0: 8 bytes
// Size class 1: 16 bytes
// Size class 2: 24 bytes
// Size class 3: 32 bytes
// ...
// Size class 66: 32768 bytes (32KB)

Size Class Hesaplama:

Size Class Avantajları:

  • Internal fragmentation azaltır: Benzer boyutlu object’ler aynı span’de
  • Hızlı allocation: Free list’ten direkt alınır
  • Cache efficiency: Locality artar

Memory Allocation Akışı

Large Object Allocation

32KB’den büyük object’ler doğrudan mheap’ten allocate edilir:

1
2
3
4
5
// runtime/malloc.go
func largeAlloc(size uintptr, needzero bool, noscan bool) *mspan {
    // Direct allocation from mheap
    // No size class, no mcache
}

Large Object Özellikleri:

  • Direct allocation: mcache/mcentral bypass
  • Zero-copy: Büyük object’ler için optimize
  • GC overhead: Büyük object’ler GC’yi etkiler

Memory Pool

Go, küçük objeler için memory pool kullanır:

Size class’lar:

  • 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, … bytes
  • Her size class için ayrı pool
  • Hızlı allocation/deallocation

Memory Ordering ve Atomic Operations

Go, memory ordering için atomic operations sağlar:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import "sync/atomic"

var counter int64

// Atomic increment
atomic.AddInt64(&counter, 1)

// Atomic load
value := atomic.LoadInt64(&counter)

// Atomic store
atomic.StoreInt64(&counter, 42)

// Compare and swap
swapped := atomic.CompareAndSwapInt64(&counter, old, new)

Memory Ordering Semantics:

Go Atomic Operations:

  • Load: Acquire semantics
  • Store: Release semantics
  • CAS: Acquire-Release semantics
  • Add/Sub: Sequentially consistent

Kullanım Senaryoları:

  • Lock-free data structures
  • Counter’lar
  • Flag’ler
  • Memory allocator internal’ları

Pratik İpuçları

  1. Pointer’ları gereksiz kullanmayın: Stack’te kalması daha hızlı
  2. Büyük struct’ları pointer ile geçirin: Copy overhead’ini azaltır
  3. Escape analysis’ı kontrol edin: go build -gcflags=-m
  4. Memory profiling yapın: go tool pprof

8. Garbage Collector (GC)

Go’nun Garbage Collector’ü, kullanılmayan bellek alanlarını otomatik olarak temizler. Modern, concurrent ve low-latency bir tasarıma sahiptir.

GC Tarihçesi

GC Algoritması: Tri-Color Mark & Sweep

GC Süreci

GC Fazları

1. Mark Phase (Concurrent)

1
2
3
4
5
6
7
// GC root'ları bul
- Global variables
- Stack variables
- Registers

// Tüm reachable objeleri işaretle
// Concurrent olarak çalışır

Mark phase özellikleri:

  • Concurrent: Uygulama çalışmaya devam eder
  • Write Barrier: Mutator yazarken mark bilgisini korur
  • Work-Stealing: Paralel marking için

2. Mark Termination (Stop-the-World)

STW süresi:

  • Go 1.8+: < 1ms (çoğu durumda < 100µs)
  • Go 1.12+: < 100µs (çoğu durumda)
  • Go 1.18+: Daha da optimize edildi

3. Sweep Phase (Concurrent)

1
2
3
// İşaretlenmemiş objeleri temizle
// Concurrent olarak çalışır
// Lazy sweeping: ihtiyaç duyuldukça

GC Trigger Mekanizması

GC şu durumlarda tetiklenir:

GOGC değişkeni:

  • Varsayılan: 100
  • Anlamı: Heap %100 büyüdüğünde GC tetiklenir
  • Örnek: 50MB heap → 100MB olunca GC
1
2
GOGC=200 go run main.go  # Daha az sıklıkla GC
GOGC=50 go run main.go   # Daha sık GC

🔧 Production Notu:

GOGC değerini production’da workload’a göre optimize etmek önemlidir. Yüksek throughput gerektiren servislerde GOGC=200-300, düşük latency gerektiren servislerde GOGC=50-100 değerleri genellikle daha uygun olur. Memory limit (Go 1.19+) ile birlikte kullanıldığında daha iyi kontrol sağlar.

Write Barrier Implementasyonu

Write barrier, GC sırasında mutator’ın (uygulama) yazdığı pointer’ları takip etmek için kullanılır.

Write Barrier Tipleri:

  1. Hybrid Write Barrier (Go 1.8+)
1
2
3
4
5
6
// runtime/barrier.go
func gcWriteBarrier(dst *uintptr, src uintptr) {
    // 1. src'yi gray'e ekle (eğer white ise)
    // 2. dst'yi gray'e ekle (eğer white ise)
    // 3. Write işlemini yap
}

Write Barrier Overhead:

  • Her pointer write’ında çağrılır
  • ~5-10ns overhead per write
  • Compiler tarafından optimize edilir (gerekli yerlerde)

GC Pacing Algoritması

Pacing, GC’nin ne zaman başlayacağını ve ne kadar hızlı çalışacağını belirler.

Pacing Hesaplaması:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// runtime/mgc.go
func gcControllerState.endCycle() {
    // Heap growth rate hesapla
    growth := float64(heapLive) / float64(heapGoal)
    
    // GC CPU budget hesapla
    cpuBudget := 0.25  // %25 CPU GC için
    
    // Mark assist ratio hesapla
    assistRatio := allocationRate / scanRate
}

Pacing Stratejisi:

  • Heap büyüme hızına göre: Hızlı büyüme → Daha sık GC
  • Allocation rate’e göre: Yüksek allocation → Daha fazla mark assist
  • CPU budget’e göre: GC, CPU’nun %25’ini kullanabilir

GC Assists Mekanizması

GC assist, allocation yapan goroutine’lerin GC’ye yardım etmesidir.

GC Assist Hesaplama:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// runtime/mgc.go
func gcAssistAlloc(gp *g) {
    // Debt hesapla
    debt := gp.gcAssistBytes
    
    // Mark work yap
    workDone := gcMarkWork(gp, debt)
    
    // Debt azalt
    gp.gcAssistBytes -= workDone
}

Assist Özellikleri:

  • Proportional: Allocation miktarına göre
  • Fair: Her goroutine eşit katkı sağlar
  • Non-blocking: GC worker’ları bloklamaz

Scavenging (Memory Return to OS)

Scavenging, kullanılmayan bellek alanlarını OS’e geri verir.

Scavenging Stratejisi:

1
2
3
4
5
6
// runtime/mheap.go
func (h *mheap) scavenge() {
    // 5 dakikadan eski free span'leri scavenge et
    // Minimum 1MB free memory varsa
    // OS'e iade et (madvise)
}

Scavenging Özellikleri:

  • Lazy: İhtiyaç duyuldukça yapılır
  • Threshold-based: Minimum free memory gerekir
  • OS-specific: Linux’ta MADV_FREE, Windows’ta VirtualFree

Scavenging Timeline:

GC Phases Timeline

GC Phase Süreleri:

  • Mark Phase: 5-50ms (heap size’a göre)
  • Mark Termination: < 100µs (STW)
  • Sweep Phase: 5-20ms (concurrent)
  • Scavenge: 1-5ms (lazy)

GC Performans Metrikleri

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
    "fmt"
    "runtime"
    "runtime/debug"
    "time"
)

func main() {
    // GC istatistiklerini al
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    
    fmt.Printf("GC Sayısı: %d\n", m.NumGC)
    fmt.Printf("Son GC Süresi: %v\n", time.Duration(m.PauseTotalNs))
    fmt.Printf("Heap Kullanımı: %d KB\n", m.Alloc/1024)
    fmt.Printf("Heap Hedef: %d KB\n", m.NextGC/1024)
    fmt.Printf("GC CPU Kullanımı: %.2f%%\n", m.GCCPUFraction*100)
    
    // GC ayarları
    debug.SetGCPercent(100)  // Varsayılan
    debug.SetMemoryLimit(1024 * 1024 * 1024)  // 1GB limit (Go 1.19+)
}

GC Metrikleri:

  • NumGC: Toplam GC sayısı
  • PauseTotalNs: Toplam pause süresi
  • GCCPUFraction: GC için kullanılan CPU oranı
  • NextGC: Bir sonraki GC trigger threshold’u
  • HeapAlloc: Mevcut heap kullanımı

GC Optimizasyon İpuçları

  1. Object Pool Kullanın: sync.Pool ile tekrar kullanım
  2. GOGC Ayarlayın: Uygulamanıza göre optimize edin
  3. Büyük Allokasyonlardan Kaçının: Küçük, sık allokasyonlar daha iyi
  4. Pointer’ları Azaltın: GC mark overhead’ini azaltır
  5. Memory Profiling: go tool pprof ile analiz yapın

sync.Pool Kullanımı

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var pool = sync.Pool{
    New: func() interface{} {
        return make([]byte, 1024)
    },
}

func process() {
    buf := pool.Get().([]byte)
    defer pool.Put(buf)
    
    // buf kullan
}

Pool avantajları:

  • GC pressure’ı azaltır
  • Allocation overhead’ini azaltır
  • Memory reuse sağlar

🔧 Production Notu:

sync.Pool kullanımı özellikle yüksek throughput gerektiren servislerde kritiktir. Sık allocate edilen ve kısa ömürlü objeler için pool kullanmak GC pressure’ı önemli ölçüde azaltır. Ancak pool’dan alınan objelerin sıfırlanması gerektiğini unutmayın, aksi halde data leak riski vardır.


9. Go vs Diğer Diller

Go vs JavaScript

Özellik Go JavaScript
Çalışma Derlenen (AOT) Yorumlanan (JIT)
Concurrency Goroutine (M:N) Event Loop (1:N)
Thread Model Çoklu thread Tek thread
Runtime Go Runtime V8/SpiderMonkey
Tip Sistemi Statik Dinamik
GC Concurrent Mark-Sweep Generational
Performans Yüksek Orta-Yüksek
Kullanım Backend, Systems Frontend, Backend

Go vs Java

Özellik Go Java
Derleme Native binary Bytecode (JVM)
Runtime Go Runtime JVM
GC Concurrent, basit Generational, kompleks
Concurrency Goroutine (hafif) Thread (ağır)
Tip Sistemi Statik, basit Statik, kompleks
Dependency Tek binary JAR dosyaları
Başlatma Hızlı Yavaş (JVM warmup)

Go vs Rust

Özellik Go Rust
Memory Safety GC ile Ownership ile
Concurrency Goroutine async/await
Performans Yüksek Çok Yüksek
Öğrenme Eğrisi Kolay Zor
GC Var Yok
Null Safety Interface{} ile Option ile

10. Mutex ve Atomic Operations

Go’da concurrency için channel’ların yanı sıra, geleneksel synchronization primitives de mevcuttur.

sync.Mutex

Mutex, critical section’ları korumak için kullanılır.

1
2
3
4
5
6
7
8
var mu sync.Mutex
var counter int

func increment() {
    mu.Lock()
    defer mu.Unlock()
    counter++
}

Mutex Özellikleri:

  • Exclusive lock: Bir goroutine lock alır, diğerleri bekler
  • Reentrant değil: Aynı goroutine tekrar lock alamaz
  • Fair değil: FIFO garantisi yok

sync.RWMutex

RWMutex, okuma ve yazma işlemlerini ayırır.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var rwmu sync.RWMutex
var data map[string]int

func read(key string) int {
    rwmu.RLock()
    defer rwmu.RUnlock()
    return data[key]
}

func write(key string, value int) {
    rwmu.Lock()
    defer rwmu.Unlock()
    data[key] = value
}

RWMutex Özellikleri:

  • Multiple readers: Birden fazla goroutine aynı anda okuyabilir
  • Single writer: Yazma sırasında tüm okuyucular bekler
  • Write priority: Yazma istekleri okuma isteklerinden önceliklidir

Mutex vs RWMutex Performansı:

Atomic Operations

Atomic operations, lock-free programming için kullanılır.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import "sync/atomic"

var counter int64

// Atomic increment
atomic.AddInt64(&counter, 1)

// Atomic load
value := atomic.LoadInt64(&counter)

// Atomic store
atomic.StoreInt64(&counter, 42)

// Compare and swap
old := atomic.LoadInt64(&counter)
new := old + 1
swapped := atomic.CompareAndSwapInt64(&counter, old, new)

Atomic vs Mutex:

Özellik Atomic Mutex
Overhead Düşük (~5ns) Yüksek (~50ns)
Kullanım Basit counter’lar Kompleks data structures
Lock-free Evet Hayır
Deadlock riski Yok Var

Atomic Kullanım Senaryoları:

  • Counter’lar
  • Flag’ler
  • Pointer’lar
  • Lock-free data structures

Mutex vs Channel Karşılaştırması

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Mutex kullanımı
var mu sync.Mutex
var data int

func setValue(v int) {
    mu.Lock()
    data = v
    mu.Unlock()
}

// Channel kullanımı
ch := make(chan int, 1)

func setValue(v int) {
    ch <- v
}

Ne Zaman Mutex, Ne Zaman Channel?

Kural:

  • Mutex: Shared state koruması için
  • Channel: Goroutine’ler arası iletişim için
  • Atomic: Basit counter/flag için

11. Advanced Channel Patterns

Pipeline Pattern

Pipeline, veriyi bir dizi stage’den geçirir.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func pipeline() {
    // Stage 1: Generate
    numbers := make(chan int)
    go func() {
        defer close(numbers)
        for i := 0; i < 10; i++ {
            numbers <- i
        }
    }()
    
    // Stage 2: Square
    squares := make(chan int)
    go func() {
        defer close(squares)
        for n := range numbers {
            squares <- n * n
        }
    }()
    
    // Stage 3: Print
    for s := range squares {
        fmt.Println(s)
    }
}

Pipeline Avantajları:

  • Modüler yapı
  • Paralel işleme
  • Backpressure handling

Cancellation Pattern

Context ile cancellation pattern’i:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func worker(ctx context.Context, jobs <-chan Job) error {
    for {
        select {
        case job, ok := <-jobs:
            if !ok {
                return nil
            }
            if err := process(ctx, job); err != nil {
                return err
            }
        case <-ctx.Done():
            return ctx.Err()
        }
    }
}

func process(ctx context.Context, job Job) error {
    // Alt işlemler de context'i almalı
    return subprocess(ctx, job)
}

Error Handling Pattern

Error channel pattern:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type Result struct {
    Value int
    Error error
}

func processWithError(jobs <-chan Job) <-chan Result {
    results := make(chan Result)
    go func() {
        defer close(results)
        for job := range jobs {
            value, err := doWork(job)
            results <- Result{Value: value, Error: err}
        }
    }()
    return results
}

Timeout Pattern

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func withTimeout(fn func(), timeout time.Duration) error {
    done := make(chan struct{})
    go func() {
        fn()
        close(done)
    }()
    
    select {
    case <-done:
        return nil
    case <-time.After(timeout):
        return errors.New("timeout")
    }
}

12. Anti-Patterns ve Common Mistakes

❌ Goroutine Leak Örnekleri

Leak 1: Unbuffered Channel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func leakyFunction() {
    ch := make(chan int)  // Unbuffered
    
    go func() {
        // Bu goroutine sonsuza kadar bloklanır!
        val := <-ch  // Leak!
    }()
    
    // ch'ye hiçbir şey gönderilmiyor
    // Goroutine leak!
}

Çözüm:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func fixedFunction() {
    ch := make(chan int)
    
    go func() {
        val := <-ch
        fmt.Println(val)
    }()
    
    ch <- 42  // Gönder
    close(ch) // Kapat
}

Leak 2: Range Loop Variable Capture

1
2
3
4
5
6
7
func leakyLoop() {
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println(i)  // ❌ Hep 10 yazdırır!
        }()
    }
}

Çözüm:

1
2
3
4
5
6
7
8
func fixedLoop() {
    for i := 0; i < 10; i++ {
        i := i  // Shadow variable
        go func() {
            fmt.Println(i)  // ✅ Doğru değer
        }()
    }
}

Leak 3: Defer in Goroutine

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func leakyDefer() {
    ch := make(chan int)
    
    go func() {
        defer close(ch)  // ❌ Goroutine bitmeden çalışmaz
        ch <- 42
    }()
    
    // Goroutine bitmeden main biter
}

Çözüm:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func fixedDefer() {
    ch := make(chan int)
    var wg sync.WaitGroup
    
    wg.Add(1)
    go func() {
        defer wg.Done()
        defer close(ch)
        ch <- 42
    }()
    
    wg.Wait()  // Goroutine'in bitmesini bekle
}

❌ Deadlock Senaryoları

Deadlock 1: Mutual Blocking

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func deadlockExample() {
    ch1, ch2 := make(chan int), make(chan int)
    
    go func() {
        ch1 <- 1
        <-ch2  // Bloklanır
    }()
    
    go func() {
        ch2 <- 2
        <-ch1  // Bloklanır
    }()
    
    // Deadlock!
}

Deadlock 2: Lock Ordering

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
var mu1, mu2 sync.Mutex

func deadlockLock() {
    go func() {
        mu1.Lock()
        mu2.Lock()  // Bekler
        // ...
    }()
    
    go func() {
        mu2.Lock()
        mu1.Lock()  // Bekler
        // ...
    }()
    
    // Deadlock!
}

Çözüm: Lock ordering

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Her zaman aynı sırada lock al
func fixedLock() {
    go func() {
        mu1.Lock()
        mu2.Lock()
        // ...
        mu2.Unlock()
        mu1.Unlock()
    }()
    
    go func() {
        mu1.Lock()  // Aynı sıra
        mu2.Lock()
        // ...
        mu2.Unlock()
        mu1.Unlock()
    }()
}

❌ Context Propagation Hataları

1
2
3
4
5
6
7
8
9
// ❌ Yanlış: Context'i geçirmiyor
func handleRequest(req *Request) {
    go process(req)  // Context yok!
}

// ✅ Doğru: Context'i geçir
func handleRequest(ctx context.Context, req *Request) {
    go process(ctx, req)  // Context var
}

✅ Doğru Yaklaşımlar

  1. Channel’ları mutlaka kapatın
  2. Context’i tüm alt işlemlere geçirin
  3. WaitGroup kullanın goroutine’lerin bitmesini beklemek için
  4. Select ile timeout ekleyin
  5. Race detector kullanın: go run -race

🔧 Production Notu:

Goroutine leak’leri ve deadlock’lar production’da en yaygın sorunlardandır. Tüm channel’ları kapatmak, context propagation yapmak ve timeout’lar eklemek kritiktir. Race detector’ı CI/CD pipeline’ınıza ekleyin, ancak production’da çalıştırmayın çünkü ~10x performans overhead’i vardır.


13. Pratik Örnekler ve Best Practices

Örnek 1: Worker Pool Pattern

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package main

import (
    "fmt"
    "sync"
)

type Job struct {
    ID int
}

type Result struct {
    JobID int
    Output string
}

func worker(id int, jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        // İşi işle
        result := Result{
            JobID:  job.ID,
            Output: fmt.Sprintf("Job %d processed by worker %d", job.ID, id),
        }
        results <- result
    }
}

func main() {
    const numWorkers = 5
    const numJobs = 100
    
    jobs := make(chan Job, numJobs)
    results := make(chan Result, numJobs)
    
    var wg sync.WaitGroup
    
    // Worker'ları başlat
    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }
    
    // İşleri gönder
    for j := 1; j <= numJobs; j++ {
        jobs <- Job{ID: j}
    }
    close(jobs)
    
    // Sonuçları topla
    go func() {
        wg.Wait()
        close(results)
    }()
    
    // Sonuçları yazdır
    for result := range results {
        fmt.Println(result.Output)
    }
}

Örnek 2: Rate Limiting

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
    "context"
    "fmt"
    "golang.org/x/time/rate"
    "time"
)

func main() {
    limiter := rate.NewLimiter(rate.Every(time.Second), 5) // 5 req/s
    
    for i := 0; i < 20; i++ {
        if err := limiter.Wait(context.Background()); err != nil {
            panic(err)
        }
        fmt.Printf("Request %d\n", i+1)
    }
}

Örnek 3: Context ile Timeout

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
    "context"
    "fmt"
    "time"
)

func longRunningTask(ctx context.Context) error {
    select {
    case <-time.After(5 * time.Second):
        fmt.Println("Task completed")
        return nil
    case <-ctx.Done():
        fmt.Println("Task cancelled:", ctx.Err())
        return ctx.Err()
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    
    if err := longRunningTask(ctx); err != nil {
        fmt.Println("Error:", err)
    }
}

Best Practices

  1. Channel’ları Kapatmayı Unutmayın: Producer channel’ı kapatmalı
  2. Context Kullanın: Timeout ve cancellation için
  3. sync.Pool Kullanın: Sık allokasyon edilen objeler için
  4. Goroutine Leak’lerinden Kaçının: Channel’ları mutlaka kapatın
  5. Race Condition Kontrolü: go run -race ile test edin
  6. Memory Profiling: Production’da memory kullanımını izleyin
  7. GC Tuning: GOGC değişkenini uygulamanıza göre ayarlayın

14. Debugging ve Profiling (Genişletilmiş)

Race Detector

1
2
go run -race main.go
go test -race ./...

Race condition’ları tespit eder, ancak performans overhead’i vardır (~10x slowdown).

Race Detector Özellikleri:

  • Tüm goroutine’leri izler
  • Memory access’leri loglar
  • Race condition’ları raporlar
  • Sadece development’ta kullanılmalı

Memory Profiling

1
2
3
4
5
6
7
8
import _ "net/http/pprof"

func main() {
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()
    // ... uygulama kodu
}
1
2
3
4
5
6
7
8
# Heap profili al
go tool pprof http://localhost:6060/debug/pprof/heap

# Profil komutları
(pprof) top10          # En çok memory kullanan 10 fonksiyon
(pprof) list function  # Fonksiyon detayları
(pprof) web            # Görsel grafik
(pprof) png            # PNG olarak kaydet

Memory Profiling Metrikleri:

  • alloc_space: Toplam allocation
  • alloc_objects: Toplam object sayısı
  • inuse_space: Mevcut kullanım
  • inuse_objects: Mevcut object sayısı

🔧 Production Notu:

Production’da profiling yaparken net/http/pprof kullanarak runtime’da profil toplayabilirsiniz. Ancak CPU profiling’in overhead’i olduğunu unutmayın. Profil toplama süresini kısa tutun (10-30 saniye) ve sadece gerektiğinde aktif edin. Memory profiling daha az overhead’li olduğu için daha sık kullanılabilir.

CPU Profiling

1
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

CPU Profiling Kullanımı:

1
2
3
(pprof) top10          # En çok CPU kullanan 10 fonksiyon
(pprof) list function  # Fonksiyon detayları
(pprof) web            # Flame graph

Goroutine Profiling

1
go tool pprof http://localhost:6060/debug/pprof/goroutine

Goroutine Profiling:

  • Aktif goroutine sayısı
  • Goroutine stack trace’leri
  • Blocking goroutine’ler

Trace Analysis

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import (
    "os"
    "runtime/trace"
)

func main() {
    f, _ := os.Create("trace.out")
    defer f.Close()
    trace.Start(f)
    defer trace.Stop()
    
    // ... uygulama kodu
}
1
go tool trace trace.out

Trace Analizi:

  • Goroutine timeline
  • GC events
  • Network I/O
  • System calls
  • Scheduler events

Memory Leak Detection

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func detectLeak() {
    var m1, m2 runtime.MemStats
    
    runtime.GC()
    runtime.ReadMemStats(&m1)
    
    // ... işlemler
    
    runtime.GC()
    runtime.ReadMemStats(&m2)
    
    if m2.HeapInuse > m1.HeapInuse*1.1 {
        fmt.Println("Potential memory leak!")
    }
}

GOMAXPROCS Tuning Stratejileri

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// CPU yoğun işler
runtime.GOMAXPROCS(runtime.NumCPU())

// I/O yoğun işler
runtime.GOMAXPROCS(runtime.NumCPU() * 2)

// Düşük latency
runtime.GOMAXPROCS(runtime.NumCPU())

// Yüksek throughput
runtime.GOMAXPROCS(runtime.NumCPU() * 4)

GOMAXPROCS Benchmark:

1
2
3
4
5
6
func benchmarkGOMAXPROCS() {
    for procs := 1; procs <= 8; procs++ {
        runtime.GOMAXPROCS(procs)
        // Benchmark çalıştır
    }
}

CPU Profiling Interpretation

Flame Graph Okuma:

  • Genişlik: CPU kullanımı
  • Yükseklik: Call stack derinliği
  • Renk: Rastgele (farklı fonksiyonlar)

Optimizasyon Hedefleri:

  • En geniş fonksiyonlar
  • Sık çağrılan fonksiyonlar
  • Hot path’ler

Troubleshooting Checklist

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
- [ ] Race detector çalıştırıldı mı?
- [ ] Memory profiling yapıldı mı?
- [ ] CPU profiling yapıldı mı?
- [ ] Goroutine sayısı kontrol edildi mi?
- [ ] GC pause süreleri ölçüldü mü?
- [ ] Memory leak var mı?
- [ ] Deadlock var mı?
- [ ] Context propagation doğru mu?
- [ ] Channel'lar kapatılıyor mu?
- [ ] GOMAXPROCS optimize edildi mi?

Performance Tuning Guide

  1. Baseline Ölçümü

    • CPU kullanımı
    • Memory kullanımı
    • Latency
    • Throughput
  2. Profil Alma

    • CPU profiling
    • Memory profiling
    • Trace analysis
  3. Optimizasyon

    • Hot path’leri optimize et
    • Memory allocation’ları azalt
    • GC pressure’ı azalt
  4. Doğrulama

    • Benchmark çalıştır
    • Profil tekrar al
    • Karşılaştır

15. Production Insights

Graceful Shutdown

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func gracefulShutdown(server *http.Server) {
    // Signal handling
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
    
    <-sigChan
    fmt.Println("Shutting down...")
    
    // Context with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    // Shutdown server
    if err := server.Shutdown(ctx); err != nil {
        log.Fatal("Server shutdown error:", err)
    }
    
    // Connection draining
    // Cleanup resources
    fmt.Println("Server stopped")
}

Circuit Breaker Pattern

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type CircuitBreaker struct {
    maxFailures int
    failures    int
    timeout     time.Duration
    mu          sync.Mutex
}

func (cb *CircuitBreaker) Call(fn func() error) error {
    cb.mu.Lock()
    if cb.failures >= cb.maxFailures {
        cb.mu.Unlock()
        return errors.New("circuit breaker open")
    }
    cb.mu.Unlock()
    
    err := fn()
    cb.mu.Lock()
    if err != nil {
        cb.failures++
    } else {
        cb.failures = 0
    }
    cb.mu.Unlock()
    
    return err
}

Retry Logic

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func retry(ctx context.Context, fn func() error, maxRetries int) error {
    var lastErr error
    for i := 0; i < maxRetries; i++ {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
        }
        
        if err := fn(); err == nil {
            return nil
        }
        
        lastErr = err
        time.Sleep(time.Duration(i+1) * 100 * time.Millisecond)
    }
    return lastErr
}

Telemetry & Observability

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/trace"
)

func instrumentedHandler(w http.ResponseWriter, r *http.Request) {
    ctx, span := otel.Tracer("app").Start(r.Context(), "handler")
    defer span.End()
    
    // ... işlemler
    
    span.SetAttributes(
        attribute.String("method", r.Method),
        attribute.String("path", r.URL.Path),
    )
}

16. Reflection ve Interface

Interface Internal Representation

Go’da interface’ler iki tiptir:

  1. iface: Method’lu interface’ler
  2. eface: Empty interface (interface{})

Interface Memory Layout:

1
2
3
4
5
6
7
8
9
type iface struct {
    tab  *itab
    data unsafe.Pointer
}

type eface struct {
    _type *rtype
    data  unsafe.Pointer
}

Type Assertion Maliyeti

1
2
3
4
5
6
7
8
// Type assertion
val, ok := i.(int)  // ~1-2ns

// Type switch
switch v := i.(type) {
case int:
    // ...
}

Type Assertion Overhead:

  • Direct assertion: ~1-2ns
  • Type switch: ~2-5ns
  • Reflection: ~50-100ns

Interface Method Dispatch

Interface method çağrıları, virtual table lookup kullanır:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
type Reader interface {
    Read([]byte) (int, error)
}

type File struct {
    // ...
}

func (f *File) Read(b []byte) (int, error) {
    // Implementation
}

func useReader(r Reader) {
    r.Read([]byte{})  // Method dispatch
}

Method Dispatch Mekanizması:

itab (Interface Table) Yapısı:

1
2
3
4
5
6
7
type itab struct {
    inter *interfacetype  // Interface type
    _type *_type          // Concrete type
    hash  uint32          // Type hash
    _     [4]byte
    fun   [1]uintptr      // Method pointers
}

Method Dispatch Overhead:

  • Direct call: ~1ns (concrete type)
  • Interface call: ~2-5ns (virtual table lookup)
  • Indirect call overhead: ~1-3ns

Dispatch Optimizasyonları:

  • Devirtualization: Compiler bazen direct call’a optimize eder
  • Inlining: Küçük method’lar inline edilebilir
  • Type specialization: Generic’ler (Go 1.18+) daha hızlı

Reflection Overhead

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import "reflect"

func reflectionExample() {
    v := reflect.ValueOf(42)
    t := reflect.TypeOf(42)
    
    // Reflection operations
    kind := v.Kind()
    name := t.Name()
}

Reflection Kullanım Senaryoları:

  • JSON/XML marshaling
  • ORM frameworks
  • Configuration parsing
  • Testing frameworks

Reflection Overhead:

  • ValueOf: ~50ns
  • TypeOf: ~10ns
  • Method call: ~100ns

17. Performance Benchmarks

Channel vs Mutex Benchmark

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func BenchmarkChannel(b *testing.B) {
    ch := make(chan int, 1)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        ch <- i
        <-ch
    }
}

func BenchmarkMutex(b *testing.B) {
    var mu sync.Mutex
    var val int
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        mu.Lock()
        val = i
        mu.Unlock()
    }
}

Gerçek Benchmark Sonuçları:

1
2
3
BenchmarkChannel-8         50000000     35 ns/op
BenchmarkMutex-8          100000000     18 ns/op
BenchmarkAtomicAdd-8     1000000000      2 ns/op

Sonuçlar:

  • Channel: ~35ns per operation
  • Mutex: ~18ns per operation
  • Atomic: ~2ns per operation

Goroutine vs Thread Creation Benchmark

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func BenchmarkGoroutineCreation(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        go func() {
            // Do nothing
        }()
    }
}

func BenchmarkThreadCreation(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        var wg sync.WaitGroup
        wg.Add(1)
        go func() {
            defer wg.Done()
            runtime.LockOSThread()
        }()
        wg.Wait()
    }
}

Gerçek Benchmark Sonuçları:

1
2
BenchmarkGoroutineCreation-8    5000000    300 ns/op
BenchmarkThreadCreation-8          5000  250000 ns/op

Sonuçlar:

  • Goroutine creation: ~300ns
  • OS Thread creation: ~250,000ns (833x daha yavaş!)

Stack vs Heap Allocation Benchmark

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func BenchmarkStack(b *testing.B) {
    for i := 0; i < b.N; i++ {
        x := 42  // Stack
        _ = x
    }
}

func BenchmarkHeap(b *testing.B) {
    for i := 0; i < b.N; i++ {
        x := new(int)  // Heap
        *x = 42
        _ = x
    }
}

Sonuçlar:

  • Stack: ~0.5ns per allocation
  • Heap: ~50ns per allocation

Buffered vs Unbuffered Channel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func BenchmarkBuffered(b *testing.B) {
    ch := make(chan int, 100)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        ch <- i
        <-ch
    }
}

func BenchmarkUnbuffered(b *testing.B) {
    ch := make(chan int)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        go func() { ch <- i }()
        <-ch
    }
}

Sonuçlar:

  • Buffered: ~30ns per operation
  • Unbuffered: ~200ns per operation (goroutine overhead)

Go Versiyon Karşılaştırması

Özellik Go 1.18 Go 1.19 Go 1.20 Go 1.21 Go 1.22
GC Pause ~100µs ~80µs ~60µs ~50µs ~40µs
Generics
Fuzzing
PGO Preview
Memory Limit
Range Func Preview
Async Preemption

PGO (Profile-Guided Optimization):

  • Go 1.20: Preview
  • Go 1.21+: Production ready
  • Compile-time optimization based on runtime profiles
  • %5-15 performans artışı

Memory Limit (Go 1.19+):

1
debug.SetMemoryLimit(1024 * 1024 * 1024)  // 1GB
  • GC’yi daha agresif tetikler
  • Memory kullanımını sınırlar

18. İleri Seviye Konular

Assembly Optimizations

Go compiler, assembly seviyesinde optimizasyonlar yapar:

1
2
3
4
5
6
7
8
9
// Go kodu
func add(a, b int) int {
    return a + b
}

// Assembly output (amd64)
// MOVQ a+0(FP), AX
// ADDQ b+8(FP), AX
// RET

Compiler Optimizations:

  • Inlining
  • Dead code elimination
  • Constant propagation
  • Loop unrolling
  • Register allocation

cgo Overhead

cgo, C kodu ile entegrasyon sağlar, ancak overhead’i vardır:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/*
#include <stdio.h>
void hello() {
    printf("Hello from C\n");
}
*/
import "C"

func main() {
    C.hello()  // cgo call
}

cgo Overhead:

  • Function call: ~100ns
  • Context switch: Go ↔ C
  • Memory management: C heap

Plugin System

Go plugins, runtime’da dinamik yükleme sağlar:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// plugin.go
package main

func Hello() string {
    return "Hello from plugin"
}

// main.go
p, _ := plugin.Open("plugin.so")
hello, _ := p.Lookup("Hello")
fmt.Println(hello.(func() string)())

Plugin Özellikleri:

  • Runtime loading
  • Symbol resolution
  • Isolation

Build Tags ve Conditional Compilation

1
2
3
4
5
// +build linux

package main

// Linux-specific code

Build Tags Kullanımı:

  • Platform-specific code
  • Feature flags
  • Testing

19. Gerçek Dünya Case Studies

Case Study 1: High-Traffic API Optimizasyonu

Problem:

  • 100K req/s API endpoint
  • Yüksek latency (200ms p95)
  • Yüksek memory kullanımı (4GB)
  • GC pause’ları (50ms)

Analiz:

1
2
3
4
5
6
7
8
# CPU profiling
go tool pprof http://localhost:6060/debug/pprof/profile

# Memory profiling
go tool pprof http://localhost:6060/debug/pprof/heap

# Goroutine profiling
go tool pprof http://localhost:6060/debug/pprof/goroutine

Tespit Edilen Sorunlar:

  1. Goroutine leak: 10,000+ goroutine (channel’lar kapatılmamış)
  2. Excessive heap allocation: Her request’te büyük struct’lar
  3. GC pressure: Çok fazla küçük allocation
  4. GOMAXPROCS: Varsayılan değer (CPU sayısı)

Çözümler:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 1. sync.Pool kullanımı
var requestPool = sync.Pool{
    New: func() interface{} {
        return &Request{}
    },
}

// 2. GOMAXPROCS tuning
runtime.GOMAXPROCS(runtime.NumCPU() * 2)  // I/O yoğun

// 3. GC tuning
debug.SetGCPercent(200)  // Daha az sıklıkla GC

// 4. Channel leak fix
defer close(ch)  // Tüm channel'ları kapat

Sonuçlar:

  • Latency: 200ms → 50ms (4x iyileşme)
  • Memory: 4GB → 1GB (4x azalma)
  • Throughput: 100K → 300K req/s (3x artış)
  • GC Pause: 50ms → 10ms (5x iyileşme)

Case Study 2: Docker’ın Go Kullanımı

Neden Go Seçildi?

  • Native binary: Dağıtım kolaylığı
  • Cross-platform: Linux, Windows, macOS
  • Concurrency: Container management için ideal
  • Performance: C’ye yakın performans

Yapılan Optimizasyonlar:

  1. Memory pooling: Container metadata için
  2. Goroutine management: Container lifecycle için
  3. GC tuning: Production workload’a göre
  4. cgo minimization: C dependency’leri azaltıldı

Karşılaşılan Sorunlar:

  • cgo overhead: C library’ler ile entegrasyon
  • GC latency: Container start/stop sırasında
  • Memory leaks: Container cleanup’da

Çözümler:

  • cgo wrapper: Minimal cgo kullanımı
  • GC tuning: GOGC=200
  • Resource cleanup: Defer pattern’leri

Case Study 3: Kubernetes Scheduler

Scheduler Performansı:

  • Pod scheduling: < 1ms latency
  • Concurrent scheduling: 1000+ pods/s
  • Memory efficiency: < 100MB heap

Memory Optimizasyonları:

  • sync.Pool: Pod object’leri için
  • Object reuse: Allocation overhead’i azaltma
  • GC tuning: Low-latency için optimize

GC Tuning Stratejileri:

1
2
3
// Kubernetes scheduler GC tuning
debug.SetGCPercent(100)  // Varsayılan
debug.SetMemoryLimit(512 * 1024 * 1024)  // 512MB limit

Scheduler Optimizasyonları:

  • Work queue: Priority queue implementation
  • Goroutine pool: Scheduler worker’ları
  • Batch processing: Pod scheduling

🔧 Production Notu:

Kubernetes gibi production sistemlerde Go’nun scheduler’ı kritik öneme sahiptir. Scheduler’ın performansını optimize etmek için goroutine pool’ları, work queue’lar ve batch processing kullanılır. Bu pattern’ler yüksek throughput ve düşük latency gerektiren production sistemlerinde standart yaklaşımdır.


20. Production Debugging Senaryoları

Senaryo 1: Yüksek Memory Kullanımı

Semptomlar:

  • Memory kullanımı sürekli artıyor
  • GC sık çalışıyor
  • Application yavaşlıyor

Debug Adımları:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 1. Heap profili al
curl http://localhost:6060/debug/pprof/heap > heap.prof

# 2. pprof ile analiz et
go tool pprof heap.prof

# 3. En çok memory kullanan fonksiyonları bul
(pprof) top10

# 4. Fonksiyon detaylarını incele
(pprof) list problematicFunction

# 5. Görsel grafik oluştur
(pprof) web

Çözüm Örnekleri:

  • sync.Pool kullanımı
  • Memory leak’leri düzeltme
  • Büyük allocation’ları azaltma

Senaryo 2: Yüksek CPU Kullanımı

Semptomlar:

  • CPU %100 kullanımı
  • Yüksek latency
  • Throughput düşüşü

Debug Adımları:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 1. CPU profili al (30 saniye)
curl http://localhost:6060/debug/pprof/profile?seconds=30 > cpu.prof

# 2. pprof ile analiz et
go tool pprof cpu.prof

# 3. Flame graph oluştur
(pprof) web

# 4. En çok CPU kullanan fonksiyonları bul
(pprof) top10

Flame Graph Yorumlama:

  • Genişlik: CPU kullanım oranı
  • Yükseklik: Call stack derinliği
  • Renk: Farklı fonksiyonlar

Çözüm Örnekleri:

  • Hot path optimizasyonu
  • Algorithm iyileştirmeleri
  • Inefficient loop’ları optimize etme

Senaryo 3: Goroutine Leak

Semptomlar:

  • Goroutine sayısı sürekli artıyor
  • Memory kullanımı artıyor
  • Application yavaşlıyor

Debug Adımları:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 1. Goroutine profili al
curl http://localhost:6060/debug/pprof/goroutine > goroutine.prof

# 2. pprof ile analiz et
go tool pprof goroutine.prof

# 3. Goroutine sayısını kontrol et
(pprof) top

# 4. Stack trace'leri incele
(pprof) list leakyFunction

Tespit:

1
2
# 10,000+ goroutines! Leak detected!
# Çoğu channel'da bloklanmış

Çözüm:

  • Channel’ları kapatma
  • Context cancellation
  • Timeout ekleme

Senaryo 4: Deadlock

Semptomlar:

  • Application donuyor
  • Hiçbir response yok
  • CPU kullanımı düşük

Debug Adımları:

1
2
3
4
5
# 1. SIGQUIT gönder (Ctrl+\)
kill -QUIT <pid>

# 2. Stack trace'i kontrol et
# Tüm goroutine'lerin durumunu gör

Deadlock Tespiti:

  • Tüm goroutine’ler bloklanmış
  • Mutex’ler veya channel’lar bekliyor
  • Circular dependency

Çözüm:

  • Lock ordering düzeltme
  • Timeout ekleme
  • Context cancellation

21. Advanced Optimization Techniques

Memory Arena Pattern

Custom allocator ile GC bypass:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type Arena struct {
    buf []byte
    off int
}

func NewArena(size int) *Arena {
    return &Arena{
        buf: make([]byte, size),
        off: 0,
    }
}

func (a *Arena) Alloc(size int) []byte {
    if a.off+size > len(a.buf) {
        return nil  // Arena dolu
    }
    ptr := a.buf[a.off : a.off+size]
    a.off += size
    return ptr
}

func (a *Arena) Reset() {
    a.off = 0  // Tüm memory'yi serbest bırak
}

Kullanım Senaryoları:

  • Geçici object’ler için
  • Batch processing
  • GC pressure’ı azaltma

Zero-Copy Techniques

1
2
3
4
5
6
7
import "unsafe"

func zeroCopy(data []byte) {
    // unsafe.Pointer ile zero-copy
    ptr := unsafe.Pointer(&data[0])
    // Direct memory access
}

Dikkat:

  • unsafe package kullanımı
  • Memory safety riski
  • Sadece gerekli durumlarda

Inline Assembly

1
2
3
4
5
//go:noescape
//go:linkname runtime_nanotime runtime.nanotime
func runtime_nanotime() int64

// Custom assembly optimizations

Kullanım:

  • Critical path optimizasyonları
  • Platform-specific optimizations
  • Performance-critical code

PGO (Profile-Guided Optimization) - Go 1.21+

1
2
3
4
5
6
# 1. Profil oluştur
go build -pgo=auto

# 2. Production'da profil topla
# 3. Profil ile yeniden derle
go build -pgo=default.pgo

Avantajlar:

  • %5-15 performans artışı
  • Hot path optimizasyonları
  • Better inlining decisions

🔧 Production Notu:

PGO (Profile-Guided Optimization) Go 1.21+ ile production-ready hale geldi. Production workload’larınızdan profil toplayıp bu profil ile yeniden derleme yaparak %5-15 arası performans artışı elde edebilirsiniz. Özellikle hot path’lerde belirgin iyileştirmeler görülür. CI/CD pipeline’ınıza PGO build adımı eklemeyi düşünün.


22. Monitoring & Alerting

Metrics Collection

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    requestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name: "http_request_duration_seconds",
            Help: "HTTP request duration",
        },
        []string{"method", "endpoint"},
    )
    
    goroutineCount = prometheus.NewGauge(
        prometheus.GaugeOpts{
            Name: "go_goroutines",
            Help: "Number of goroutines",
        },
    )
)

func init() {
    prometheus.MustRegister(requestDuration)
    prometheus.MustRegister(goroutineCount)
}

func main() {
    http.Handle("/metrics", promhttp.Handler())
    // ...
}

Key Metrics

Runtime Metrics:

  • go_goroutines: Goroutine sayısı
  • go_memstats_alloc_bytes: Heap allocation
  • go_memstats_gc_duration_seconds: GC süresi
  • go_memstats_gc_cpu_fraction: GC CPU kullanımı

🔧 Production Notu:

Production’da monitoring ve alerting kritiktir. Goroutine sayısı, memory kullanımı ve GC pause süreleri için alert’ler kurun. Prometheus ve Grafana ile dashboard’lar oluşturun. Özellikle goroutine leak’leri ve memory leak’leri erken tespit etmek için sürekli monitoring yapın. Alert threshold’larını workload’unuza göre ayarlayın.

Application Metrics:

  • Request latency (p50, p95, p99)
  • Throughput (req/s)
  • Error rate
  • Memory usage

Alerting Rules

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Prometheus alerting rules
groups:
  - name: go_app
    rules:
      - alert: HighGoroutineCount
        expr: go_goroutines > 10000
        for: 5m
        annotations:
          summary: "High goroutine count detected"
      
      - alert: HighMemoryUsage
        expr: go_memstats_alloc_bytes > 2e9  # 2GB
        for: 5m
        annotations:
          summary: "High memory usage detected"
      
      - alert: HighGCPause
        expr: go_memstats_gc_duration_seconds > 0.1  # 100ms
        for: 5m
        annotations:
          summary: "High GC pause detected"

Observability Stack


23. Go Performance Cheat Sheet

Quick Reference

İşlem Süre Kullanım
Goroutine creation ~300ns Concurrency
Channel send ~35ns Communication
Mutex lock ~18ns State protection
Atomic add ~2ns Simple counters
Stack alloc ~0.5ns Local variables
Heap alloc ~80ns Dynamic memory
Interface call ~2-5ns Polymorphism
Direct call ~1ns Concrete types
Reflection call ~100ns Dynamic dispatch

When to Use What?

Channels:

  • ✅ Goroutine’ler arası iletişim
  • ✅ Event signaling
  • ✅ Pipeline patterns
  • ❌ Shared state protection

Mutex:

  • ✅ Shared state protection
  • ✅ Critical sections
  • ❌ Goroutine communication

Atomic:

  • ✅ Simple counters
  • ✅ Flags
  • ✅ Lock-free structures
  • ❌ Complex operations

Stack vs Heap:

  • ✅ Stack: Local variables, small objects
  • ✅ Heap: Escaped variables, large objects
  • ❌ Stack: Pointer return, closures

Performance Tips

  1. Allocation Optimization:

    • Stack allocation tercih et
    • sync.Pool kullan
    • Büyük allocation’ları azalt
  2. GC Optimization:

    • GOGC ayarla
    • Memory limit kullan (Go 1.19+)
    • Pointer’ları azalt
  3. Concurrency:

    • Goroutine pool kullan
    • Channel buffer size optimize et
    • Context cancellation kullan
  4. Compiler Optimizations:

    • PGO kullan (Go 1.21+)
    • Inlining için küçük fonksiyonlar
    • Dead code elimination

Common Pitfalls Checklist

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
- [ ] Channel leak: Channel'lar kapatılıyor mu?
- [ ] Goroutine leak: Tüm goroutine'ler bitiyor mu?
- [ ] Context propagation: Context tüm alt işlemlere geçiyor mu?
- [ ] Memory leak: sync.Pool kullanılıyor mu?
- [ ] Deadlock: Lock ordering doğru mu?
- [ ] Race condition: Race detector çalıştırıldı mı?
- [ ] GC tuning: GOGC optimize edildi mi?
- [ ] GOMAXPROCS: Doğru değer ayarlandı mı?
- [ ] Profiling: Production'da profiling yapılıyor mu?
- [ ] Monitoring: Metrics toplanıyor mu?

24. Özet ve Sonuç

Go’nun çalışma modeli şu temel prensiplere dayanır:

Go’nun Güçlü Yönleri

  1. Basitlik: Minimal syntax, öğrenmesi kolay
  2. Performans: Native binary, düşük latency
  3. Concurrency: Goroutine modeli ile kolay paralel programlama
  4. Tooling: Mükemmel araç seti (fmt, vet, pprof)
  5. Deployment: Tek binary, kolay dağıtım
  6. GC: Modern, concurrent, low-latency garbage collection

Kullanım Alanları

  • Microservices: Yüksek throughput API’ler
  • CLI Tools: Hızlı, native araçlar
  • System Programming: Sistem seviyesi programlama
  • Network Services: Yüksek performanslı network uygulamaları
  • DevOps Tools: Docker, Kubernetes, Terraform gibi araçlar
  • Cloud Services: Distributed systems

Sonuç

Go, performans + sadelik + concurrency dengesini çok iyi kuran bir dildir. Modern yazılım geliştirme ihtiyaçlarını karşılamak için tasarlanmış, pratik ve verimli bir araçtır. Bu yüzden microservice’ler, API’ler, CLI araçları ve sistem programlama için sıkça tercih edilir.

Go’nun çalışma modelini anlamak, daha verimli ve performanslı uygulamalar yazmanıza yardımcı olacaktır. Runtime mekanizmalarını bilmek, debugging ve optimization süreçlerinde de büyük avantaj sağlar.


25. Kaynaklar ve Referanslar

Go Source Code

Resmi Dokümantasyon

Önemli Blog Yazıları

  • Russ Cox Blog: https://research.swtch.com/

    • “Go Data Structures” serisi
    • “Go Scheduler” yazıları
    • “Go GC” detayları
  • Go Team Blog Yazıları:

    • “Go GC: Prioritizing low latency and simplicity”
    • “Go Scheduler: M, P, G”
    • “Go 1.5 GC improvements”

Go Proposal Dökümanları

Community Best Practices

İlham Kaynakları

  • “How Go Works” - Go runtime deep dives
  • “Go Internals” - Runtime mekanizmaları

Önerilen Okumalar

  1. “The Go Programming Language” - Alan Donovan, Brian Kernighan
  2. “Concurrency in Go” - Katherine Cox-Buday
  3. Go Blog yazıları - Runtime, GC, Scheduler
  4. Go source code - Runtime implementasyonları

Not: Bu makale, Go runtime’ın derinlemesine bir incelemesidir. Production uygulamalarında bu bilgileri kullanırken, Go’nun resmi dokümantasyonunu ve best practice’leri de göz önünde bulundurmanız önerilir.