Go En İyi Pratikleri, Deyimler (Idioms) & Tasarım Desenleri

16 Ağustos 2026 · netologist · 24 dakika, 5023 kelime ·

Principal Seviye Mühendis Bakış Açısıyla Detaylı Rehber


İçindekiler

  1. Go Felsefesi
  2. Proje Yapısı & Paket Tasarımı
  3. İsimlendirme Kuralları
  4. Hata Yönetimi (Error Handling)
  5. Interface’ler & Kompozisyon
  6. Eşzamanlılık (Concurrency)
  7. Generics
  8. Context
  9. Go’da Tasarım Desenleri
  10. Test Yazımı
  11. Performans & Bellek Yönetimi
  12. Yaygın Tuzaklar / Anti-Pattern’ler
  13. Araçlar & Linting
  14. Loglama & Gözlemlenebilirlik
  15. API & Kütüphane Tasarımı
  16. Dokümantasyon

1. Go Felsefesi

Go, az sayıda temel ilke etrafında tasarlanmıştır ve syntax ezberlemekten çok bu ilkeleri içselleştirmek önemlidir:

Rob Pike’ın içselleştirilmesi gereken özdeyişleri:


2. Proje Yapısı & Paket Tasarımı

2.1 Standart Yerleşim (topluluk kuralı, resmi değil)

myproject/
├── cmd/
│   └── myapp/
│       └── main.go          # ince giriş noktası, bağımlılıkları bağlar
├── internal/
│   ├── domain/               # temel iş mantığı, dış bağımlılık yok
│   ├── service/               # uygulama/use-case katmanı
│   ├── transport/
│   │   ├── http/
│   │   └── grpc/
│   ├── repository/           # kalıcılık (persistence) adaptörleri
│   └── config/
├── pkg/                        # dışarıya import edilmesi güvenli kod (az kullanın)
├── api/                         # protobuf/openapi şemaları
├── migrations/
├── scripts/
├── go.mod
├── go.sum
└── Makefile

2.2 Paket İsimlendirme

// KÖTÜ
package utils
package common
package helpers

// İYİ — paket adı ne sağladığını anlatır
package validator
package ratelimit
package sqlrepo

2.3 Bağımlılık Yönü

Tam bir framework olmadan bile kabaca hexagonal / clean-architecture ayrımını izleyin:

domain  <-- service <-- transport (HTTP/gRPC handler'lar)
   ^
   |
repository (domain interface'lerini implement eder)

2.4 Import Döngülerinden Kaçının

Go, derleme zamanında döngüsel (cyclic) import’ları yasaklar. A paketinin B’yi, B’nin de A’yı import etmesi gerektiğini fark ederseniz, paylaşılan sözleşmeyi (genelde domain/interface paketi) her ikisinin de bağımlı olduğu üçüncü bir pakete çıkarın.


3. İsimlendirme Kuralları

ÖğeKuralÖrnek
Paketkısa, küçük harf, alt çizgisiznet/http, encoding/json
Export edilen tanımlayıcılarMixedCapstype Client struct{}
Export edilmeyen tanımlayıcılarmixedCapsfunc parseHeader()
Sabitler (constants)MixedCaps (SCREAMING_SNAKE yok)const MaxRetries = 3
Interface’ler (tek metotlu)Metot adı + -erio.Reader, io.Writer, fmt.Stringer
Kısaltmalartutarlı caseURL, ID, HTTPUrl, Id, Http değil
Getter’larGet öneki yokuser.Name(), user.GetName() değil
Sentinel hatalarErr önekivar ErrNotFound = errors.New(...)
Hata tipleriError sonekitype ValidationError struct{}
func (c *Client) Do(req *Request) (*Response, error) { ... }
func (c *Client) Close() error { ... }

4. Hata Yönetimi (Error Handling)

4.1 Hatalar Değerdir (Values)

func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("divide: division by zero")
    }
    return a / b, nil
}

Hataları her zaman hemen kontrol edin. Neden güvenli olduğunu belgelemeden _ ile asla göz ardı etmeyin.

4.2 Bağlam Ekleyerek Sarma (Wrapping) — %w

func LoadConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("load config %q: %w", path, err)
    }
    var cfg Config
    if err := yaml.Unmarshal(data, &cfg); err != nil {
        return nil, fmt.Errorf("parse config %q: %w", path, err)
    }
    return &cfg, nil
}

4.3 Sentinel Hatalar vs Hata Tipleri vs Opak (Opaque) Hatalar

// Sentinel — basit kimlik kontrolleri için
var ErrNotFound = errors.New("resource not found")

if errors.Is(err, ErrNotFound) { ... }

// Özel hata tipi — yapılandırılmış veriye ihtiyaç duyduğunuzda
type ValidationError struct {
    Field string
    Msg   string
}
func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Msg)
}

var ve *ValidationError
if errors.As(err, &ve) {
    log.Printf("bad field: %s", ve.Field)
}

// Opak — çağıran sadece başarı/başarısızlığı bilmeli, tipi değil
func DoSomething() error { ... } // çağıran sadece err != nil kontrolü yapar

Çağıranın ihtiyacını karşılayan en az miktarda hata yüzeyi (surface) açığa çıkarmayı tercih edin — “opak hatalar” geçerli ve genellikle tercih edilen bir varsayılandır.

4.4 Çoklu Hatalar (Go 1.20+)

err := errors.Join(err1, err2, err3)
if errors.Is(err, ErrTimeout) { ... }

4.5 panic/recover

func Parse(input string) (result Node, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("parse: %v", r)
        }
    }()
    return parseInternal(input), nil
}

4.6 Hatayı Hem Loglamayın Hem De Return Etmeyin

Çok yaygın bir anti-pattern:

// KÖTÜ — hata yukarı yayıldıkça tekrar tekrar loglanır
if err != nil {
    log.Println(err)
    return err
}

Bir hatayı bir kez yönetin — ya çağrı yığınının en üstünde logla-ve-yut, ya da sar-ve-döndür (wrap-and-return). Her katmanda ikisini birden yapmak, aynı kök nedenin N kez tekrarlandığı log spam’i üretir.


5. Interface’ler & Kompozisyon

5.1 Interface Kabul Et, Struct Döndür

// İYİ
func NewUserService(repo UserRepository) *UserService { ... }

// Tüketici, ihtiyaç duyduğu minimal interface'i tanımlar
type UserRepository interface {
    FindByID(ctx context.Context, id string) (*User, error)
}

5.2 Structural Typing / Örtük Tatmin (Implicit Satisfaction)

type Stringer interface {
    String() string
}

type Point struct{ X, Y int }
func (p Point) String() string { return fmt.Sprintf("(%d,%d)", p.X, p.Y) }

// Point otomatik olarak Stringer'ı tatmin eder — "implements" anahtar kelimesi gerekmez
var s Stringer = Point{1, 2}

Bir tipin interface’i tatmin ettiğini garanti etmek istediğinizde derleme zamanı kontrolü (implementasyonlarda yaygındır):

var _ io.Writer = (*MyWriter)(nil)

5.3 Kompozisyon için Embedding

type Base struct {
    ID string
}
func (b Base) Describe() string { return "id=" + b.ID }

type User struct {
    Base   // embed edilmiş — Describe()'ı User'a taşır (promote eder)
    Name string
}

u := User{Base: Base{ID: "1"}, Name: "Ada"}
u.Describe() // çalışır — taşınmış (promoted) metot
type countingWriter struct {
    io.Writer
    n int64
}
func (w *countingWriter) Write(p []byte) (int, error) {
    n, err := w.Writer.Write(p)
    w.n += int64(n)
    return n, err
}

5.4 Boş Interface & any

any‘i (Go 1.18’den beri interface{}‘in takma adı) tutumlu kullanın — statik tiplemeyi geçersiz kılar. Tip-bağımsız davranışa tip güvenliğiyle ihtiyaç duyduğunuzda generics’i tercih edin (bkz. §7).


6. Eşzamanlılık (Concurrency)

6.1 Goroutine’ler

go func() {
    // bu goroutine asla sızmasın — her zaman net bir çıkış yolu olsun
}()

6.2 Channel’lar

ch := make(chan int)       // buffer'sız — senkron el değiştirme
ch := make(chan int, 10)   // buffer'lı — gönderen bloklanmadan 10 taneye kadar
var timeout <-chan time.Time
if useTimeout {
    timeout = time.After(5 * time.Second)
}
select {
case v := <-ch:
    ...
case <-timeout: // nil ise bu case asla tetiklenmez
    ...
}

6.3 sync Primitifleri

var mu sync.Mutex
mu.Lock()
defer mu.Unlock()

var once sync.Once
once.Do(func() { initialize() })

var wg sync.WaitGroup
wg.Add(len(tasks))
for _, t := range tasks {
    t := t // döngü değişkenini yakala (bkz. tuzaklar §12)
    go func() {
        defer wg.Done()
        process(t)
    }()
}
wg.Wait()
var counter atomic.Int64
counter.Add(1)

6.4 errgroup — Deyimsel Fan-Out/Fan-In

import "golang.org/x/sync/errgroup"

g, ctx := errgroup.WithContext(ctx)
for _, url := range urls {
    url := url
    g.Go(func() error {
        return fetch(ctx, url)
    })
}
if err := g.Wait(); err != nil {
    return err
}

errgroup, herhangi bir goroutine hata döndürür döndürmez paylaşılan context’i iptal eder ve ilk hatayı toplar — bu, elle yazılan WaitGroup + hata channel’ı boru hattının standart yerine geçenidir.

6.5 Worker Pool’lar

func workerPool(ctx context.Context, jobs <-chan Job, workers int) <-chan Result {
    results := make(chan Result)
    var wg sync.WaitGroup
    wg.Add(workers)
    for i := 0; i < workers; i++ {
        go func() {
            defer wg.Done()
            for job := range jobs {
                select {
                case results <- process(job):
                case <-ctx.Done():
                    return
                }
            }
        }()
    }
    go func() {
        wg.Wait()
        close(results)
    }()
    return results
}

Desen: sınırlı (bounded) eşzamanlılık, context farkındalıklı iptal ve çağıranın results üzerinde güvenle range yapabilmesi için özel bir kapatıcı (closer) goroutine.

6.6 Pipeline Deseni

func generate(ctx context.Context, nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, n := range nums {
            select {
            case out <- n:
            case <-ctx.Done():
                return
            }
        }
    }()
    return out
}

func square(ctx context.Context, in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            select {
            case out <- n * n:
            case <-ctx.Done():
                return
            }
        }
    }()
    return out
}

// kullanım: for v := range square(ctx, generate(ctx, 1, 2, 3)) { ... }

Her aşama: kendi çıkış channel’ının sahibidir, bittiğinde kapatır, iptal için ctx.Done()‘a saygı gösterir.

6.7 Race Detector

CI’da testleri her zaman go test -race ile çalıştırın. Eşzamanlı kod için bu opsiyonel değildir — data race’ler Go’nun bellek modelinde tanımsız davranıştır (undefined behavior) ve race detector, gerçek dünya vakalarının büyük çoğunluğunu yakalar.


7. Generics

Go 1.18’de tanıtıldı. Aksi takdirde tipler arasında mantığı tekrarlayacağınız veya interface{} + reflection/tip assertion’a güveneceğiniz durumlarda kullanın.

7.1 Temel Sözdizimi

func Map[T, U any](s []T, f func(T) U) []U {
    result := make([]U, len(s))
    for i, v := range s {
        result[i] = f(v)
    }
    return result
}

doubled := Map([]int{1, 2, 3}, func(n int) int { return n * 2 })

7.2 Kısıtlamalar (Constraints)

type Number interface {
    ~int | ~int64 | ~float64
}

func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

7.3 Generics NE ZAMAN Kullanılmamalı


8. Context

8.1 Kurallar

  1. context.Context, bir fonksiyonun ilk parametresi olmalı ve ctx olarak adlandırılmalı.
  2. Bir Context‘i asla bir struct alanında saklamayın — çağrı zinciri boyunca açıkça geçirin (belgelenmiş istisnalar son derece nadirdir, örn. bazı http.Request.Context() dahili durumları).
  3. Asla nil geçirmeyin — gerçekten kararsızsanız context.TODO(), gerçek kökte (main, testler, üst düzey handler) context.Background() kullanın.
  4. context.WithValue‘yu tutumlu kullanın — sadece API sınırlarından geçen istek kapsamlı (request-scoped) metadata için (trace ID’ler, auth token’ları), asla opsiyonel parametreler veya dependency injection için değil.
func FetchUser(ctx context.Context, id string) (*User, error) {
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    ...
}

8.2 İptal Yayılımı (Cancellation Propagation)

func longRunningTask(ctx context.Context) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err() // context.Canceled veya context.DeadlineExceeded
        default:
            // bir miktar iş yap
        }
    }
}

8.3 Özel Context Anahtarları

type ctxKey int
const requestIDKey ctxKey = 0

func WithRequestID(ctx context.Context, id string) context.Context {
    return context.WithValue(ctx, requestIDKey, id)
}

func RequestIDFromContext(ctx context.Context) (string, bool) {
    id, ok := ctx.Value(requestIDKey).(string)
    return id, ok
}

Paketler arası çakışmayı önlemek için anahtar için export edilmemiş özel bir tip kullanın (asla ham bir string değil).


9. Go’da Tasarım Desenleri

Go’da sınıf yoktur, bu yüzden klasik GoF desenleri interface’ler, kompozisyon ve birinci sınıf fonksiyonlar aracılığıyla yeniden ifade edilir. Aşağıdakiler, gerçek Go kod tabanlarında sürekli karşınıza çıkanlardır.

9.1 Functional Options (Fonksiyonel Seçenekler)

Constructor overloading / setter’lı builder’ın deyimsel yerine geçenidir:

type Server struct {
    addr    string
    timeout time.Duration
    tls     bool
}

type Option func(*Server)

func WithTimeout(d time.Duration) Option {
    return func(s *Server) { s.timeout = d }
}
func WithTLS() Option {
    return func(s *Server) { s.tls = true }
}

func NewServer(addr string, opts ...Option) *Server {
    s := &Server{addr: addr, timeout: 30 * time.Second} // makul varsayılanlar
    for _, opt := range opts {
        opt(s)
    }
    return s
}

// kullanım
srv := NewServer(":8080", WithTimeout(5*time.Second), WithTLS())

Faydaları: geriye dönük uyumlu API evrimi (bir seçenek eklemek çağıranları bozmaz), kendi kendini belgeleyen çağrı noktaları, makul varsayılanlar.

9.2 Builder Deseni (seçenekler yetmediğinde)

type QueryBuilder struct {
    table string
    wheres []string
}

func NewQuery(table string) *QueryBuilder {
    return &QueryBuilder{table: table}
}
func (q *QueryBuilder) Where(cond string) *QueryBuilder {
    q.wheres = append(q.wheres, cond)
    return q
}
func (q *QueryBuilder) Build() string {
    query := "SELECT * FROM " + q.table
    if len(q.wheres) > 0 {
        query += " WHERE " + strings.Join(q.wheres, " AND ")
    }
    return query
}

sql := NewQuery("users").Where("age > 18").Where("active = true").Build()

9.3 Strategy Deseni

type CompressionStrategy interface {
    Compress([]byte) ([]byte, error)
}

type GzipStrategy struct{}
func (GzipStrategy) Compress(b []byte) ([]byte, error) { /* ... */ return b, nil }

type ZstdStrategy struct{}
func (ZstdStrategy) Compress(b []byte) ([]byte, error) { /* ... */ return b, nil }

type Archiver struct {
    strategy CompressionStrategy
}
func (a *Archiver) Archive(data []byte) ([]byte, error) {
    return a.strategy.Compress(data)
}

Go’da bir strategy genellikle tek metotlu bir interface yerine sadece bir fonksiyon değeridir:

type CompressFunc func([]byte) ([]byte, error)

type Archiver struct {
    compress CompressFunc
}

9.4 Decorator Deseni

type Handler func(http.ResponseWriter, *http.Request)

func WithLogging(next Handler) Handler {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next(w, r)
        log.Printf("%s %s took %v", r.Method, r.URL.Path, time.Since(start))
    }
}

func WithAuth(next Handler) Handler {
    return func(w http.ResponseWriter, r *http.Request) {
        if !isAuthorized(r) {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }
        next(w, r)
    }
}

// kompozisyon
handler := WithLogging(WithAuth(myHandler))

Bu, Go’nun deyimsel middleware desenidir ve HTTP sunucularında (chi, echo, gin hepsi bunun varyantlarını kullanır) her yerde görülür.

9.5 Observer Deseni (Channel’lar Aracılığıyla Pub/Sub)

type EventBus struct {
    mu   sync.RWMutex
    subs map[string][]chan Event
}

func (b *EventBus) Subscribe(topic string) <-chan Event {
    b.mu.Lock()
    defer b.mu.Unlock()
    ch := make(chan Event, 1)
    b.subs[topic] = append(b.subs[topic], ch)
    return ch
}

func (b *EventBus) Publish(topic string, evt Event) {
    b.mu.RLock()
    defer b.mu.RUnlock()
    for _, ch := range b.subs[topic] {
        select {
        case ch <- evt:
        default: // abone yavaşsa düşür — non-blocking publish
        }
    }
}

9.6 Singleton (az kullanın — açık dependency injection’ı tercih edin)

var (
    instance *Config
    once     sync.Once
)

func GetConfig() *Config {
    once.Do(func() {
        instance = loadConfig()
    })
    return instance
}

Singleton’lar testi zorlaştırır (gizli global durum) — mümkün olan her yerde bağımlılıkları constructor’lar aracılığıyla açıkça geçirmeyi tercih edin. Bu deseni gerçekten süreç genelinde (process-wide) olan şeyler için saklayın, örneğin bir metrics registry.

9.7 Factory Deseni

type StorageType string

const (
    StorageS3    StorageType = "s3"
    StorageLocal StorageType = "local"
)

func NewStorage(t StorageType, cfg Config) (Storage, error) {
    switch t {
    case StorageS3:
        return NewS3Storage(cfg)
    case StorageLocal:
        return NewLocalStorage(cfg)
    default:
        return nil, fmt.Errorf("unknown storage type: %s", t)
    }
}

9.8 Adapter Deseni

// üçüncü parti kütüphane bizimkiyle uyumsuz kendi logger interface'ine sahip
type thirdPartyLogger interface {
    LogMessage(level, msg string)
}

type LoggerAdapter struct {
    logger *slog.Logger
}
func (a *LoggerAdapter) LogMessage(level, msg string) {
    a.logger.Log(context.Background(), slog.Level(0), msg, "level", level)
}

9.9 State Deseni (interface’ler + type switch, veya açık bir state machine ile)

type OrderState interface {
    Next(o *Order) OrderState
    Name() string
}

type Pending struct{}
func (Pending) Name() string { return "pending" }
func (Pending) Next(o *Order) OrderState { return Shipped{} }

type Shipped struct{}
func (Shipped) Name() string { return "shipped" }
func (Shipped) Next(o *Order) OrderState { return Delivered{} }

9.10 Pipeline / Chain of Responsibility

§6.6’da zaten gösterildi — Go’nun channel tabanlı pipeline’ları, akan veri (streaming data) için deyimsel chain-of-responsibility desenidir. Senkron middleware tarzı zincirler için Decorator desenine bakın (§9.4).

9.11 Null Object Deseni

type NoopMetrics struct{}
func (NoopMetrics) Inc(name string)          {}
func (NoopMetrics) Observe(name string, v float64) {}

// çağıranların asla nil kontrolü yapmasına gerek kalmaması için varsayılan olarak kullanılır
var DefaultMetrics Metrics = NoopMetrics{}

10. Test Yazımı

10.1 Tablo Güdümlü Testler (Table-Driven Tests)

Go’nun en önemli test deyimi:

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        want     int
    }{
        {"positive", 2, 3, 5},
        {"negative", -1, -1, -2},
        {"zero", 0, 0, 0},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Add(tt.a, tt.b)
            if got != tt.want {
                t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
            }
        })
    }
}

10.2 Alt Testler & Paralellik

func TestSlow(t *testing.T) {
    t.Parallel()
    // ...
}

CI’ı hızlandırmak için bağımsız testlerde t.Parallel() kullanın, ancak paylaşılan mutable fixture’larla dikkatli olun (döngü değişkeni yakalama sorunu tablo testlerindeki tt için de geçerlidir — Go 1.22+ klasik loop-var bug’ını düzeltti, ama 1.22 öncesi kod için her zaman gölgeleyin: tt := tt).

10.3 Interface’ler Aracılığıyla Test Double’ları

type EmailSender interface {
    Send(to, subject, body string) error
}

type mockSender struct {
    sent []string
}
func (m *mockSender) Send(to, subject, body string) error {
    m.sent = append(m.sent, to)
    return nil
}

func TestNotifyUser(t *testing.T) {
    mock := &mockSender{}
    svc := NewNotificationService(mock)
    svc.Notify("[email protected]")
    if len(mock.sent) != 1 {
        t.Fatalf("expected 1 email sent, got %d", len(mock.sent))
    }
}

Küçük, tüketici tanımlı interface’ler (§5.1) tam olarak bunu kolaylaştıran şeydir — çoğu durumda mocking framework’üne gerek yoktur, ama daha büyük yüzeyler için gomock/mockery/testify/mock yardımcı olur.

10.4 Golden Dosyalar

func TestRender(t *testing.T) {
    got := Render(input)
    golden := filepath.Join("testdata", "render.golden")
    if *update {
        os.WriteFile(golden, got, 0644)
    }
    want, _ := os.ReadFile(golden)
    if !bytes.Equal(got, want) {
        t.Errorf("mismatch, run with -update to regenerate")
    }
}

10.5 Fuzz Testing (Go 1.18+)

func FuzzParse(f *testing.F) {
    f.Add("valid input")
    f.Fuzz(func(t *testing.T, s string) {
        _, err := Parse(s)
        if err != nil {
            return // sorun yok, geçersiz girdi
        }
        // parse ediyorsa, yeniden marshal edilmesi round-trip yapmalı
    })
}

10.6 Benchmark Testleri

func BenchmarkConcat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = strings.Join([]string{"a", "b", "c"}, "")
    }
}

Op başına allocation’ları görmek için go test -bench=. -benchmem ile çalıştırın — hot-path optimizasyon kararları için kritiktir.

10.7 Test Organizasyonu


11. Performans & Bellek Yönetimi

11.1 Boyut Biliniyorsa Slice’ları Önceden Ayırın (Preallocate)

// KÖTÜ — slice büyüdükçe tekrar tekrar yeniden ayırma/kopyalama
var result []int
for _, v := range input {
    result = append(result, transform(v))
}

// İYİ
result := make([]int, 0, len(input))
for _, v := range input {
    result = append(result, transform(v))
}

11.2 Gereksiz Allocation’lardan Kaçının

var b strings.Builder
b.Grow(estimatedSize)
for _, s := range parts {
    b.WriteString(s)
}
result := b.String()

11.3 Escape Analysis’i Anlayın

// n heap'e kaçar (escape) çünkü ona bir pointer return ediliyor
func newInt(v int) *int {
    n := v
    return &n
}

Escape analysis kararlarını görmek için go build -gcflags="-m" çalıştırın. Her şeyin manuel ayarlamaya ihtiyacı yoktur, ama hot path’ler için neyin heap allocation’a zorladığını (pointer döndürmek, interface’lerde saklamak, referans yakalayan closure’lar) anlamak önemlidir.

11.4 Yeniden Kullanılabilir Buffer’lar için sync.Pool

var bufPool = sync.Pool{
    New: func() any { return new(bytes.Buffer) },
}

func process() {
    buf := bufPool.Get().(*bytes.Buffer)
    buf.Reset()
    defer bufPool.Put(buf)
    // buf'ı kullan
}

Sadece allocate edilmesi pahalı ve hot, yüksek throughput’lu yollarda kullanılan nesneler için (örn. ağır yük altındaki HTTP handler’lar) buna değer. Önce profilleyin.

11.5 Profilleme

import _ "net/http/pprof"
// sonra: go tool pprof http://localhost:6060/debug/pprof/profile

11.6 Struct Alan Sıralaması (Alignment Padding)

// KÖTÜ — padding nedeniyle 24 byte
type Bad struct {
    A bool    // 1 byte + 7 padding
    B int64   // 8 byte
    C bool    // 1 byte + 7 padding
}

// İYİ — 16 byte, alanlar büyükten küçüğe sıralı
type Good struct {
    B int64
    A bool
    C bool
}

Büyük struct’larda bunu otomatik olarak yakalamak için go vet veya fieldalignment (golang.org/x/tools/go/analysis/passes/fieldalignment‘tan) kullanın.


12. Yaygın Tuzaklar / Anti-Pattern’ler

12.1 Döngü Değişkeni Yakalama (Go 1.22’de düzeltildi, eski kod için hâlâ geçerli)

// KÖTÜ (Go < 1.22) — tüm goroutine'ler aynı son değeri yazdırabilir
for _, v := range items {
    go func() { fmt.Println(v) }()
}

// DÜZELTME (1.22 öncesi)
for _, v := range items {
    v := v
    go func() { fmt.Println(v) }()
}

// veya parametre olarak geçirin
for _, v := range items {
    go func(v int) { fmt.Println(v) }(v)
}

Go 1.22+, döngü semantiğini her iterasyonun kendi değişkenine sahip olacağı şekilde değiştirdi — ama 1.22 öncesi kodu okurken veya go.mod uyumluluğunu ayarlarken bunu hâlâ bilmeniz gerekir.

12.2 nil Interface vs nil Pointer

type MyError struct{}
func (e *MyError) Error() string { return "boom" }

func doWork() error {
    var e *MyError = nil
    if false {
        e = &MyError{}
    }
    return e // nil pointer'ı saran NIL OLMAYAN bir interface döndürür!
}

err := doWork()
fmt.Println(err == nil) // false! klasik tuzak

Çözüm: kasıtlı değilse, interface tipli bir dönüş değeri aracılığıyla tiplenmiş bir nil pointer değil, açıkça nil döndürün.

12.3 := ile Hataları Gölgelemek (Shadowing)

func do() error {
    x, err := step1()
    if err != nil {
        return err
    }
    if x > 0 {
        y, err := step2() // bu blok içinde dış err'ı gölgeler!
        if err != nil {
            return err
        }
        _ = y
    }
    return err // BUG: bu dış err bayat/nil olabilir, gerçek amacı maskeler
}

12.4 Döngülerde defer‘ı Göz Ardı Etmek

// KÖTÜ — dosya handle'ları birikir, sadece fonksiyon return ettiğinde kapanır
for _, path := range paths {
    f, _ := os.Open(path)
    defer f.Close() // hepsi çevreleyen fonksiyonun sonuna ertelenir, döngü iterasyonuna değil
    process(f)
}

// İYİ — her iterasyonda defer'ın tetiklenmesi için bir fonksiyona sarın
for _, path := range paths {
    func() {
        f, _ := os.Open(path)
        defer f.Close()
        process(f)
    }()
}

12.5 Senkronizasyon Olmadan Goroutine’leri Yanlış Kullanmak

// KÖTÜ — main, goroutine çalışmadan önce çıkabilir; ayrıca counter'da data race var
counter := 0
go func() { counter++ }()
fmt.Println(counter)

12.6 Karşılaştırılamayan Alanlara Sahip Struct’ları Karşılaştırmak

type S struct {
    M map[string]int // map'ler karşılaştırılamaz
}
// s1 == s2 // derleme hatası

Bu tür tipler için reflect.DeepEqual veya manuel bir Equal metodu kullanın.

12.7 Slice Aliasing / Append Sürprizleri

a := []int{1, 2, 3, 4, 5}
b := a[1:3]         // a ile aynı backing array'i paylaşır
b = append(b, 99)   // kapasite izin veriyorsa a[3]'ün üzerine yazabilir!

Kapasiteyi sınırlamak ve append’de yeni bir allocation’ı zorlamak için üç indeksli bir slice (a[1:3:3]) kullanın, veya gerçek izolasyona ihtiyacınız varsa copy() kullanın.

12.8 Interface Kirliliği

Birden fazla implementasyonunuz olmadan (veya açıkça öngörmeden), ya da testlerde mock için ihtiyaç duymadan bir interface tanımlamayın. Her interface’in sadece tek bir implementasyonunu export eden bir paket, aşırı soyutlanmıştır (over-abstracted) — bu, Java/C# alışkanlıklarından gelen ve Go’ya iyi hizmet etmeyen yaygın bir aktarımdır.

12.9 Derin Çağrı Yığınlarından Çıplak/Sarılmamış Hatalar Döndürmek

Sarma (wrapping) olmadan, üç katman yukarıdaki çağıranlar "connection refused" gibi bir hatanın nereden kaynaklandığını bilemez. Her katman sınırında her zaman bağlam ekleyin (bkz. §4.2).

12.10 init()‘i Aşırı Kullanmak

init() fonksiyonları import zamanında örtük olarak, öngörülemeyen bir sırayla çalışır (bir paket içinde iyi tanımlı, paketler arasında daha az belirgin), testi zorlaştırır ve kontrol akışını gizler. main‘den çağrılan açık initialization fonksiyonlarını tercih edin.


13. Araçlar & Linting

AraçAmaç
gofmt / goimportsKanonik formatlama + import gruplama (kaydederken çalıştırın)
go vetŞüpheli yapıları yakalar (printf format uyumsuzlukları, struct tag yazım hataları)
staticcheckFiili standart linter; go vet‘in kaçırdığı bug’ları yakalar
golangci-lintstaticcheck, errcheck, govet, revive vb.‘yi toplayan meta-linter
errcheckKontrol edilmeyen hata dönüş değerlerini işaretler
go test -raceData race detector — eşzamanlı kod için CI’da zorunlu
go test -coverKapsam (coverage) raporlaması
govulncheckBilinen CVE’ler için bağımlılıkları tarar
go mod tidygo.mod/go.sum’ı doğru tutar

Önerilen golangci-lint temel konfigürasyonu en azından şunları içermeli: govet, errcheck, staticcheck, unused, ineffassign, gosimple, bodyclose, noctx, gosec.

# .golangci.yml (minimal örnek)
linters:
  enable:
    - errcheck
    - gosimple
    - govet
    - ineffassign
    - staticcheck
    - unused
    - bodyclose
    - noctx
    - gosec

14. Loglama & Gözlemlenebilirlik

14.1 log/slog ile Yapılandırılmış Loglama (stdlib, Go 1.21+)

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("request handled",
    "method", r.Method,
    "path", r.URL.Path,
    "duration_ms", elapsed.Milliseconds(),
    "status", status,
)

14.2 Metrikler & Tracing


15. API & Kütüphane Tasarımı

// Client, uzak servise bir bağlantıyı yönetir ve birden fazla
// goroutine tarafından eşzamanlı kullanım için güvenlidir.
type Client struct { ... }

// Do, req'i gönderir ve ayrıştırılmış yanıtı döndürür. ctx'in
// iptaline ve deadline'larına saygı gösterir.
func (c *Client) Do(ctx context.Context, req *Request) (*Response, error) { ... }

16. Dokümantasyon

// Package ratelimit, birden fazla goroutine arasında eşzamanlı
// kullanım için güvenli token-bucket rate limiter'lar sağlar.
package ratelimit
func ExampleClient_Do() {
    c := NewClient()
    resp, _ := c.Do(context.Background(), req)
    fmt.Println(resp.Status)
    // Output: 200 OK
}

Kod İncelemesi için Kapanış Kontrol Listesi