Yükleniyor...

Go Best Practices: Modern Backend Geliştirme Rehberi

Yazar: Burak Balkı | Kategori: Backend Development | Okuma Süresi: 8 dk

Bu rehberde, Go (Golang) backend geliştirme süreçlerinde uygulanması gereken best practices, proje yapısı, hata yönetimi, concurrency ve performans optimizas...

## Go (Golang) Nedir ve Neden Best Practices Önemlidir? **Go (Golang)**, Google tarafından geliştirilen, statik tipli, derlenen ve yüksek performanslı bir programlama dilidir. Modern backend sistemlerinde Go'nun tercih edilme sebebi, sadeliği ve eşzamanlılık (concurrency) yetenekleridir. Ancak, bu sadeliği korumak ve sürdürülebilir bir kod tabanı oluşturmak için **Go best practices** (en iyi uygulamalar) standartlarına uymak kritik öneme sahiptir. Standartlara uymak, teknik borcu azaltır, ekip içi iş birliğini artırır ve uygulamanın çalışma zamanı performansını optimize eder. Bu rehberde, Go projelerinizde uygulamanız gereken endüstri standartlarını teknik detaylarıyla ele alacağız. ## Proje Yapılandırması ve Standart Klasör Düzeni Go ekosisteminde resmi bir zorunluluk olmasa da, topluluk tarafından kabul görmüş bir klasör yapısı bulunmaktadır. Bu yapı, projenin ölçeklenebilirliğini sağlar. ```text /project-root /cmd # Uygulama giriş noktaları (main.go) /internal # Dışarıya kapalı, sadece proje içi kullanılan kodlar /pkg # Dış projeler tarafından içe aktarılabilecek kütüphaneler /api # API tanımları (OpenAPI, Proto) /configs # Konfigürasyon dosyaları /scripts # Build ve deploy scriptleri /web # Frontend varlıkları (varsa) ``` > **Not:** `internal` klasörü Go derleyicisi tarafından özel olarak korunur. Bu klasördeki paketler, sadece kök dizindeki diğer paketler tarafından import edilebilir. ## Değişken Tanımlama ve Veri Tiplerinde En İyi Uygulamalar Go'da değişken tanımlarken gereksiz karmaşadan kaçınılmalıdır. Kısa değişken tanımlama operatörü (`:=`), fonksiyon içi tanımlamalarda tercih edilir. ```go // Kötü: Gereksiz uzunluk var userCount int = 10 // İyi: Kısa ve öz userCount := 10 // Sabit tanımlamaları const ( MaxRetries = 3 Timeout = 30 * time.Second ) ``` Değişken isimlendirmelerinde **camelCase** kullanılmalı, kısaltmalar (ID, HTTP, JSON) büyük harfle yazılmalıdır (`userID`, `httpServer`). ## Etkili Hata Yönetimi (Error Handling) Stratejileri Go'da hatalar birer değerdir (values). `try-catch` blokları yerine hataların kontrol edilmesi (check) ve sarmalanması (wrap) gerekir. ```go func fetchData(id string) (*Data, error) { res, err := db.Query(id) if err != nil { // Hatayı sarmalayarak bağlam ekleyin return nil, fmt.Errorf("fetching data for id %s: %w", id, err) } return res, nil } ``` **Hata Kontrolü Tablosu:** | Yöntem | Kullanım Amacı | | :--- | :--- | | `errors.New()` | Basit metin tabanlı hatalar | | `fmt.Errorf("...%w", err)` | Hatayı sarmalamak (Wrapping) | | `errors.Is(err, target)` | Hatanın belirli bir tipte olup olmadığını kontrol etmek | | `errors.As(err, &target)` | Hatayı spesifik bir tipe cast etmek | ## Concurrency (Eşzamanlılık) ve Goroutine Yönetimi Go'nun en güçlü yanı olan Goroutine'ler, yanlış kullanıldığında bellek sızıntılarına (memory leak) yol açabilir. Asla bir Goroutine'in ne zaman biteceğini bilmeden onu başlatmayın. ```go func processTasks(tasks []string) { var wg sync.WaitGroup for _, task := range tasks { wg.Add(1) go func(t string) { defer wg.Done() executeTask(t) }(task) // Değişkeni parametre olarak geçmek önemlidir } wg.Wait() } ``` ### Channel Kullanımı ve Select Bloğu Channel'lar veri iletimi için kullanılır. `select` bloğu ile timeout mekanizması eklemek, sistemin kilitlenmesini önler. ```go select { case res := <-resultChan: fmt.Println("Sonuç:", res) case <-time.After(2 * time.Second): fmt.Println("İşlem zaman aşımına uğradı") } ``` ## Interface Tasarımı ve Bağımlılık Enjeksiyonu Interface'ler küçük tutulmalıdır (Single Method Interface). "Accept interfaces, return structs" (Interface kabul et, struct döndür) kuralı Go'nun temel felsefesidir. ```go // Küçük interface tasarımı type Reader interface { Read(p []byte) (n int, err error) } // Dependency Injection örneği type UserService struct { repo UserRepository } func NewUserService(r UserRepository) *UserService { return &UserService{repo: r} } ``` ## Paket Yönetimi ve Go Modules Kullanımı Modern Go projelerinde `go mod` kullanımı zorunludur. Bağımlılıkların versiyonlanması ve izole edilmesi için `go.mod` ve `go.sum` dosyaları titizlikle yönetilmelidir. ```bash # Yeni modül başlatma go mod init github.com/user/project # Bağımlılıkları temizleme ve güncelleme go mod tidy ``` ## Performans Optimizasyonu ve Memory Management Go'da performans için bellek tahsisini (allocation) minimize etmek gerekir. `make` kullanırken kapasiteyi (capacity) önceden belirlemek performansı artırır. ```go // Kötü: Her append işleminde yeniden allocation yapılabilir nums := []int{} // İyi: Kapasite biliniyorsa önceden tanımlanmalı nums := make([]int, 0, 100) ``` ### Pointer vs Value Semantics - Küçük yapılar (structs) için değer (value) kopyalama daha hızlı olabilir. - Büyük yapılar veya durumu (state) değişmesi gereken nesneler için pointer kullanılmalıdır. ## Test Yazımı ve Unit Testing Standartları Go, yerleşik bir test framework'ü ile gelir. Testler `_test.go` uzantılı dosyalarda yazılmalıdır. **Table-driven tests** deseni, farklı senaryoları test etmek için en temiz yoldur. ```go func TestAdd(t *testing.T) { tests := []struct { name string a, b int expected int }{ {"pozitif sayılar", 1, 2, 3}, {"negatif sayılar", -1, -1, -2}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := Add(tt.a, tt.b) if result != tt.expected { t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected) } }) } } ``` ## Sık Yapılan Hatalar ve Kaçınılması Gerekenler 1. **Panic Kullanımı:** Uygulama ayağa kalkarken (init) kritik bir hata yoksa `panic` kullanılmamalıdır. Bunun yerine `error` dönülmelidir. 2. **Shadowing:** Değişkenlerin iç bloklarda aynı isimle tekrar tanımlanarak üst bloktaki değişkeni gölgelemesi. 3. **Global State:** Global değişkenler test edilebilirliği azaltır ve race condition riskini artırır. ## Kod Kalitesi ve Linting Araçları Kod kalitesini otomatize etmek için `golangci-lint` aracı kullanılmalıdır. Bu araç, statik analiz yaparak potansiyel hataları ve stil bozukluklarını raporlar. ```bash # Lint çalıştırma golangci-lint run ``` ## Sık Sorulan Sorular (FAQ) **1. Go'da neden try-catch yok?** Go, hataların akış kontrolünün bir parçası olduğunu savunur. Bu, geliştiricinin her olası hata durumunu açıkça ele almasını sağlayarak daha güvenli kod yazılmasını teşvik eder. **2. Goroutine sızıntısı nasıl önlenir?** Her Goroutine'in bir sonlanma sinyali (context veya channel) olmalıdır. `context.Context` kullanarak timeout veya iptal durumlarında Goroutine'lerin temizlenmesi sağlanır. **3. Interface'ler ne zaman kullanılmalı?** Bir davranışı birden fazla tip uygulayacaksa veya bağımlılıkları mock'lamak (test etmek) istiyorsanız interface kullanmalısınız. **4. 'make' ve 'new' arasındaki fark nedir?** `make` sadece slice, map ve channel oluşturmak için kullanılır ve initialize edilmiş bir değer döner. `new` ise bellekte yer ayırır ve o tipin sıfır değerine (zero value) işaret eden bir pointer döner. **5. Go projelerinde hangi loglama kütüphanesi kullanılmalı?** Standart `log` paketi basit işler için yeterlidir ancak yapısal loglama (structured logging) için Go 1.21 ile gelen `slog` veya üçüncü parti `zap` kütüphanesi önerilir. ## Özet ve Sonuç Go ile başarılı bir backend projesi geliştirmenin anahtarı sadelik ve standartlara bağlılıktır. Etkili hata yönetimi, doğru eşzamanlılık modelleri ve modüler bir yapı ile yüksek performanslı sistemler inşa edebilirsiniz. Unutmayın, Go'da kodun okunabilirliği, akıllıca yapılmış karmaşık çözümlerden her zaman daha değerlidir.