Go En İyi Pratikleri, Deyimler (Idioms) & Tasarım Desenleri
Principal Seviye Mühendis Bakış Açısıyla Detaylı Rehber
İçindekiler
- Go Felsefesi
- Proje Yapısı & Paket Tasarımı
- İsimlendirme Kuralları
- Hata Yönetimi (Error Handling)
- Interface’ler & Kompozisyon
- Eşzamanlılık (Concurrency)
- Generics
- Context
- Go’da Tasarım Desenleri
- Test Yazımı
- Performans & Bellek Yönetimi
- Yaygın Tuzaklar / Anti-Pattern’ler
- Araçlar & Linting
- Loglama & Gözlemlenebilirlik
- API & Kütüphane Tasarımı
- 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:
- Basitlik, zekilikten (cleverness) önce gelir. Kod, yazılmaktan çok okunur. Go, dili küçük tutmak için kalıtım (inheritance), operatör aşırı yükleme (operator overloading) ve exception gibi özellikleri bilinçli olarak dışarıda bırakır.
- Kalıtım yerine kompozisyon. Sınıf hiyerarşisi yoktur. Davranış, struct’ları embed ederek ve interface’leri tatmin ederek inşa edilir.
- Örtük (implicit) değil açık (explicit). Hatalar değerdir (values) ve açıkça return edilir. Exception gibi gizli kontrol akışı yoktur.
- Eşzamanlılık dilin bir parçasıdır. Goroutine ve channel’lar dile gömülüdür, sonradan eklenen bir kütüphane değildir.
- “Biraz kod tekrarı, küçük bir bağımlılıktan daha iyidir.” Tek bir fonksiyon için büyük bir dependency eklemek yerine küçük, yerel bir tekrarı tercih edin.
- gofmt tüm stil tartışmalarını sonlandırır. Tek bir kanonik formatlama vardır; ona karşı savaşmayın.
Rob Pike’ın içselleştirilmesi gereken özdeyişleri:
- Açık olan, zekice olandan iyidir (Clear is better than clever).
- Reflection asla açık (clear) değildir.
- Hatalar değerdir (Errors are values).
- Hataları sadece kontrol etmeyin, zarifçe (gracefully) yönetin.
- Interface ne kadar büyükse, soyutlama o kadar zayıftır.
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
internal/Go derleyicisi tarafından zorunlu kılınır: modül dışından (veyainternal‘ın üst dizini dışından) hiçbir yer buradan import edemez. Agresif şekilde kullanın — varsayılan olarakinternalseçin, gerçekten dış tüketiciler istediğinizdepkg/‘e taşıyın.cmd/neredeyse hiç mantık içermemeli — sadece flag parse etme, config yükleme, dependency wiring veinternal‘ı çağırma.- Genel bir
pkg/utilsveyapkg/commonçöp kutusundan kaçının. Bu, her şeyin atıldığı bir yer haline gelir ve yapay coupling yaratır. Paketleri ne içerdiklerine göre değil, ne sağladıklarına göre isimlendirin.
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
- Paket isimleri küçük harf, tek kelime, alt çizgisiz,
mixedCapsolmadan yazılır. - Tekrardan (stutter) kaçının:
validator.Validatorkötüdür;validator.New()fonksiyonunun birvalidator.Validatordöndürmesi uygundur ama tip adı paket adını gereksiz yere tekrar etmemeli — örneğinhttp.Client,http.HTTPClientdeğil. - Paket adı çağrı noktasının bir parçasıdır:
log.Info(),logging.LogInfo()‘dan daha okunaklıdır.
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)
domainpaketi interface’leri tanımlamalı (type UserRepository interface {...}) ama asla somut altyapı paketlerini (database/sql,net/http) import etmemeli.- Somut implementasyonlar (
postgres.UserRepository)repository/altında yaşar ve domain interface’ini tatmin eder — bu, Go’da deyimsel (idiomatic) olarak uygulanan Dependency Inversion Principle‘dır: interface’ler üretici tarafından değil, tüketici tarafından tanımlanır.
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ı
| Öğe | Kural | Örnek |
|---|---|---|
| Paket | kısa, küçük harf, alt çizgisiz | net/http, encoding/json |
| Export edilen tanımlayıcılar | MixedCaps | type Client struct{} |
| Export edilmeyen tanımlayıcılar | mixedCaps | func parseHeader() |
| Sabitler (constants) | MixedCaps (SCREAMING_SNAKE yok) | const MaxRetries = 3 |
| Interface’ler (tek metotlu) | Metot adı + -er | io.Reader, io.Writer, fmt.Stringer |
| Kısaltmalar | tutarlı case | URL, ID, HTTP — Url, Id, Http değil |
| Getter’lar | Get öneki yok | user.Name(), user.GetName() değil |
| Sentinel hatalar | Err öneki | var ErrNotFound = errors.New(...) |
| Hata tipleri | Error soneki | type ValidationError struct{} |
- Receiver isimleri kısa (1-2 harf), bir tipin tüm metotlarında tutarlı olmalı, asla
selfveyathiskullanılmamalı:
func (c *Client) Do(req *Request) (*Response, error) { ... }
func (c *Client) Close() error { ... }
- Değişken kapsamı (scope), isim uzunluğunu belirlemeli. Sıkı döngülerde:
i,j,k,v. Paket seviyesinde: açıklayıcı, tam kelimeler. - Tanımlayıcıda paket adını tekrar etmekten kaçının:
bytes.Buffer,bytes.BytesBufferdeğil;strings.NewReplacer,strings.NewStringReplacerdeğil.
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
}
%w, hata zincirini korur, böylece çağıranlarerrors.Is/errors.Askullanabilir.- Her sarma (wrap) işlemi yeni bağlamsal bilgi eklemeli, yığın boyunca aynı ifadeyi tekrarlamamalı.
- Hata metinlerini büyük harfle başlatmayın, noktalama işaretiyle bitirmeyin (
fmt.Errorf("Failed to open.")yanlış stil — doğrusu"failed to open: %w").
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
panic, programcı hataları için ayrılmıştır (asla nil olmaması gereken bir invariant’ın nil dereference’ı, bir mantık hatasından kaynaklanan index out of range) — beklenen başarısızlık yolları için değil.- Bir
panic‘in bir API/kütüphane sınırını sessizce geçmesine asla izin vermeyin. Bir kütüphane dahili olarakpanickullanmak zorundaysa (örn. recursive descent parser’lar), export edilen giriş noktasındarecover()yapıp bunu birerror‘a çevirmelidir. - Sunucularda, tek bir goroutine panic’inin tüm süreci çökertmemesi için üst düzey bir
recover()middleware’i kullanın — ama recover edilen her panic’i rutin bir kontrol akışı mekanizması değil, düzeltilmesi gereken bir bug olarak ele alın.
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)
}
- Interface’ler küçük olmalı, ideal olarak 1-3 metot.
io.Reader,io.Writer,sort.Interfacekanonik örneklerdir. - Interface’ler üreticinin değil, tüketicinin paketine aittir. Onu implement eden pakette “belki lazım olur” diye önceden interface tanımlamayın — bu, Go’nun structural typing’ine uymayan bir C#/Java alışkanlığıdır.
- Constructor’lardan somut tipler döndürün ki çağıranlar export edilen tüm metot/alanlara tam erişim sahibi olsun; ihtiyaç duyduklarında kendileri bunu bir interface’e daraltsın — örneğin testte bir mock ile.
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
- Embedding kalıtım değildir: polimorfizm,
superyoktur ve embed edilen tipin metotları dış tipten haberdar değildir (virtual dispatch yoktur). - Interface’leri decorate etmek için kullanışlıdır — örneğin bir
io.Writer‘ı sararken, interface daha büyükse geri kalanını miras almak için onu embed etmek:
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
}()
- Başlattığınız her goroutine’in durması için iyi tanımlanmış bir yolu olmalı. Sonlanma yolu olmayan bir goroutine, kapatılmamış bir dosya handle’ı gibi bir sızıntıdır.
- Yaşam döngüsünün kimin “sahibi” olduğunu ve hataların oradan nasıl yayılacağını bilmeden asla bir goroutine başlatmayın.
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
- “Bellek paylaşarak iletişim kurmayın; iletişim kurarak bellek paylaşın.”
- Buffer’sız channel’lar bir buluşma noktasıdır (rendezvous point) — tamamlanma sinyali vermek için harikadır.
- Channel’ı gönderen kapatmalı, asla alıcı kapatmamalı. Sahip olmadığınız bir channel’ı kapatmak veya iki kez kapatmak panic’e neden olur.
nilchannel’lar gönderme/almada sonsuza kadar bloklanır — birselectcase’ini dinamik olarak devre dışı bırakmak için kullanışlıdır:
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()
- Koordinasyon ve veri akışı için channel’ları kullanın; paylaşılan durumu korumak için (bir cache, bir sayaç)
sync.Mutexkullanın. Hiçbiri evrensel olarak “daha deyimsel” (idiomatic) değildir — problemin yapısına göre seçin. - Okumalar yazmalardan çok daha fazlaysa
sync.RWMutex. - Contention önemliyse, basit sayaçlar için tam bir mutex yerine
sync/atomic:
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
}
~int, “int veya altında yatan (underlying) tipi int olan herhangi bir adlandırılmış tip” anlamına gelir — bu, generic fonksiyonunuzuntype Age intüzerinde de çalışması için önemlidir.- Standart kütüphanenin
cmp.Ordered‘ı (Go 1.21+) çoğu sıralama kısıtlamasını kapsar — kendi başınıza yazmak yerine bunu tercih edin. slicesvemapspaketleri (Go 1.21+ stdlib) zaten genericSort,Contains,Keys,Valuesvb. sağlıyor — kendinizinkini yazmadan önce stdlib’i kontrol edin.
7.3 Generics NE ZAMAN Kullanılmamalı
- Sadece bir somut kullanım alanı olan bir fonksiyonu generic hale getirmeyin — YAGNI burada da geçerlidir.
- 1-2 metotlu bir interface problemi çözüyorsa, bunu bir tip parametresine tercih edin — generics, davranış soyutlamasından (bunu interface’ler zaten iyi hallediyor) çok, veri yapıları (container’lar, slice/map üzerinde algoritmalar) için parlar.
8. Context
8.1 Kurallar
context.Context, bir fonksiyonun ilk parametresi olmalı vectxolarak adlandırılmalı.- 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ı). - Asla
nilgeçirmeyin — gerçekten kararsızsanızcontext.TODO(), gerçek kökte (main, testler, üst düzey handler)context.Background()kullanın. 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
- Testleri, internal detayların white-box testi için aynı pakette (
package foo) veya public API’nin saf black-box testi içinpackage foo_testiçinde tutun — birçok olgun proje, export edilmemiş detaylara kazara güveniyi yakalamak için bilinçli olarakfoo_testkullanır. testdata/dizini go aracı tarafından yok sayılır — fixture’ları oraya koyun.- Hata satır numaralarının çağırana işaret etmesi için test yardımcı fonksiyonlarında
t.Helper()kullanın. - Kompozisyon için elle defer tabanlı teardown yerine
t.Cleanup()kullanın.
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
- Büyük struct’ları değer (value) olarak geçirmek onları kopyalar — birkaç kelimeden büyük struct’lar için pointer geçirin, ama semantik değişmezlik (immutability) gerektiriyorsa küçük struct’ları (
time.Timeboyutunda gibi) değer olarak geçirin. - Döngüde string birleştirme:
+=değil,strings.Builderkullanı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
- CPU/bellek/goroutine/block/mutex profillemesi için
pprof. go test -cpuprofile=cpu.out -memprofile=mem.out- Her zaman optimize etmeden önce ölçün. Go’nun derleyicisi ve runtime’ı iyidir; erken mikro-optimizasyon genellikle ölçülebilir bir kazanç olmadan okunabilirliği zedeler.
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 / goimports | Kanonik formatlama + import gruplama (kaydederken çalıştırın) |
go vet | Şüpheli yapıları yakalar (printf format uyumsuzlukları, struct tag yazım hataları) |
staticcheck | Fiili standart linter; go vet‘in kaçırdığı bug’ları yakalar |
golangci-lint | staticcheck, errcheck, govet, revive vb.‘yi toplayan meta-linter |
errcheck | Kontrol edilmeyen hata dönüş değerlerini işaretler |
go test -race | Data race detector — eşzamanlı kod için CI’da zorunlu |
go test -cover | Kapsam (coverage) raporlaması |
govulncheck | Bilinen CVE’ler için bağımlılıkları tarar |
go mod tidy | go.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,
)
fmt.Sprintftarzı string’ler yerine yapılandırılmış (key-value) logları tercih edin — makine tarafından parse edilebilir, log toplayıcılarda filtrelenebilir.- İstek kapsamlı (request-scoped) bir logger’ı (trace ID, kullanıcı ID vb. ile) global’ler yerine context aracılığıyla iliştirin.
14.2 Metrikler & Tracing
- Temel süreç metrikleri için
expvar(stdlib); metrikler için fiili standart Prometheus client kütüphanesidir (prometheus/client_golang). - Dağıtık tracing için OpenTelemetry (
go.opentelemetry.io/otel) — servis sınırlarında (HTTP handler’lar, DB çağrıları, giden RPC’ler) enstrüman edin.
15. API & Kütüphane Tasarımı
- Export edilen yüzeyi minimal tutun. Export edilen her şey çağıranlara bir sözdür; dışarıda gerekmeyen her şeyi export etmeyin.
- Semantic versioning modüller için önemlidir. Bir breaking change yeni bir major versiyon yolu gerektirir (
github.com/you/pkg/v2). - Public API’nizde üçüncü parti tipleri açığa çıkarmaktan mümkün olduğunca kaçının — bu, tüketicileri sizin bağımlılık seçimlerinize ve versiyonlarınıza kilitler.
- Context farkındalıklı ve olmayan varyantları tutumlu sağlayın — genellikle sadece
context.Contextisteyin; iki paralel API sürdürmeyin. - Export edilen tanımlayıcıları, tanımlayıcının adıyla başlayan bir doc yorumuyla belgeleyin (zorlanan bir deyim,
golint/revivetarafından kontrol edilir):
// 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
- Her export edilen paketin, büyük paketler için geleneksel olarak bir
doc.godosyasında paket seviyesinde bir doc yorumu olmalı:
// Package ratelimit, birden fazla goroutine arasında eşzamanlı
// kullanım için güvenli token-bucket rate limiter'lar sağlar.
package ratelimit
- Çalıştırılabilir Example fonksiyonları kullanın — derlenirler, opsiyonel olarak test olarak çalıştırılırlar ve doğrudan godoc’ta gösterilirler:
func ExampleClient_Do() {
c := NewClient()
resp, _ := c.Do(context.Background(), req)
fmt.Println(resp.Status)
// Output: 200 OK
}
- README’yi şunlara odaklı tutun: proje ne yapar, kurulum talimatları, minimal bir kullanım örneği ve tam godoc’a bir link — API referans dokümanlarının bir kopyası değil.
Kod İncelemesi için Kapanış Kontrol Listesi
- Hatalar bağlamla sarılmış, hemen kontrol edilmiş, bir kez yönetilmiş
- Net bir sonlanma/sahiplik hikayesi olmayan goroutine yok
- Interface’ler küçük ve tüketici tanımlı
-
context.Contextdoğru şekilde geçiriliyor, struct’larda asla saklanmıyor - Döngü değişkeni yakalama bug’ı yok (veya Go 1.22+ onaylandı)
-
go vet,staticcheck,-racehepsi temiz - Export edilen tanımlayıcılar belgelenmiş
- Erken interface/generic soyutlaması yok
- Önemsiz olmayan mantık için tablo güdümlü testler
- Struct alanları padding’i minimize edecek şekilde sıralanmış (sadece hot struct’lar)