Mert Özen Her Satırda Daha İleri
Backend Go ile Backend — #04

Go'da Struct, Method ve Interface: Class'sız OOP, Embedding ve Implicit Interface

Mert Özen 15 Kas 2026 15 dk 3 görüntülenme
Go'da Struct, Method ve Interface: Class'sız OOP, Embedding ve Implicit Interface

Go'da class yok: veri struct'ta, davranış method'larda durur; state değiştiren method pointer receiver ister, interface'ler ise implicit'tir ve onu kullanan tarafta küçük tanımlanır. Bookstore'un sipariş modeli (NewOrder, Timestamps embedding'i, MarkPaid), orders.Repository ile memstore ve sqlstore, accept interfaces return structs, Stringer, type switch, method set hatası ve staticcheck'in yakaladığı nil interface tuzağı.

Sürüm notu: Bu yazıdaki kodlar Go 1.27.4 ile derlenip çalıştırılmıştır; module'ün go satırı 1.26.0'dır ve kod Go 1.26.11 ile de derlenir. staticcheck 2026.2.2 kullanılmıştır. sqlstore package'ı derlenir, ama bu yazıda bir veritabanına bağlanıp çalıştırılmamıştır; onu 11'de SQL Server ile çalıştıracağız.

Önce Tahmin Et

Bu yazıda sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) sipariş modelini Go'da kuracağız. Go'da class yok; veri struct'ta durur, davranış struct'a bağlı method'larda. Bir method'u tanımlarken receiver'ı ya değer olarak ya da pointer olarak yazarsın. Aşağıdaki iki method'un tek farkı bu:

// Lab 04: value receiver kopya üzerinde, pointer receiver asıl değer üzerinde çalışır.
package main

import "fmt"

type Order struct {
    ID     string
    Status string
}

func (o Order) PayOnCopy() {
    //lint:ignore SA4005 bu lab tuzağı bilerek gösteriyor
    o.Status = "Paid"
}

func (o *Order) Pay() {
    o.Status = "Paid"
}

func main() {
    o := Order{ID: "ORD-1001", Status: "Pending"}
    o.PayOnCopy()
    fmt.Println(o.Status)
    o.Pay() // Go &o'yu bizim yerimize alır
    fmt.Println(o.Status)
}

C#'ta bir class'ın method'u this'in field'ını değiştirdiğinde değişiklik kalıcıdır. o burada bir struct değişkeni. İki satırda ne yazar? o.Pay() satırı derlenir mi, yoksa (&o).Pay() yazmak mı gerekir?

go run ./labs/04-receivers
Pending
Paid

PayOnCopy'nin receiver'ı bir değer: method çağrıldığında struct kopyalandı, kopyanın durumu değişti ve kopya method bitince kayboldu. Pay'in receiver'ı pointer: asıl değişkene yazdı. o.Pay() da derlendi, çünkü o adresi alınabilen bir değişken ve Go &o'yu kendisi alıyor. Derleyici ilk method için bir şey demedi, ama staticcheck'in bu tuzak için bir kontrolü var. Lab'daki //lint:ignore satırını silip çalıştırınca:

labs/04-receivers/main.go:12:2: ineffective assignment to field Order.Status (SA4005)

Bu yazıda struct'ları, receiver kuralını, inheritance yerine embedding'i ve Go'nun implicit interface'lerini Bookstore'un sipariş modeli üzerinden göreceğiz.

Struct ve Constructor Fonksiyonu

Sipariş modeli internal/orders package'ında; C# tarafındaki Bookstore.Ordering module'ünün karşılığı. Önce tipler:

// Package orders müşteri siparişlerini modeller.
package orders

import (
    "fmt"
    "time"

    "bookstore/internal/catalog"
)

// Status, siparişin yaşam döngüsünde nerede olduğunu söyler.
type Status int

const (
    StatusPending Status = iota
    StatusPaid
    StatusShipped
    StatusCancelled
)

func (s Status) String() string {
    switch s {
    case StatusPending:
        return "Pending"
    case StatusPaid:
        return "Paid"
    case StatusShipped:
        return "Shipped"
    case StatusCancelled:
        return "Cancelled"
    default:
        return fmt.Sprintf("Status(%d)", int(s))
    }
}

// Item siparişin bir satırıdır.
type Item struct {
    ISBN      string
    Title     string
    UnitPrice catalog.Money
    Quantity  int
}

// Total satırın tutarıdır.
func (i Item) Total() catalog.Money {
    return i.UnitPrice * catalog.Money(i.Quantity)
}

// Timestamps, ne zaman değiştiğini izleyen tiplere gömülür.
type Timestamps struct {
    CreatedAt time.Time
    UpdatedAt time.Time
}

// Touch bir değişikliği kaydeder.
func (t *Timestamps) Touch(now time.Time) {
    t.UpdatedAt = now
}

// Order bir müşterinin siparişidir.
type Order struct {
    Timestamps
    ID         string
    CustomerID string
    Items      []Item
    Status     Status
}

// NewOrder satırı olmayan, Pending durumunda bir sipariş döndürür.
func NewOrder(id, customerID string, now time.Time) *Order {
    return &Order{
        Timestamps: Timestamps{CreatedAt: now, UpdatedAt: now},
        ID:         id,
        CustomerID: customerID,
        Status:     StatusPending,
    }
}

// AddItem siparişe bir satır ekler.
func (o *Order) AddItem(item Item, now time.Time) {
    o.Items = append(o.Items, item)
    o.Touch(now)
}

// MarkPaid Pending bir siparişi Paid yapar ve yapıp yapmadığını döndürür.
func (o *Order) MarkPaid(now time.Time) bool {
    if o.Status != StatusPending {
        return false
    }
    o.Status = StatusPaid
    o.Touch(now)
    return true
}

// Total bütün satırların toplamıdır.
func (o *Order) Total() catalog.Money {
    var sum catalog.Money
    for _, it := range o.Items {
        sum += it.Total()
    }
    return sum
}

// String, fmt.Stringer interface'ini sağlar.
func (o *Order) String() string {
    return fmt.Sprintf("order %s (%v): %d items, %v", o.ID, o.Status, len(o.Items), o.Total())
}

Go'da constructor yok; onun yerine NewOrder gibi New ile başlayan sıradan bir fonksiyon yazılır. Her tip için constructor yazmak gerekmez, 03'te gördüğümüz gibi zero value çoğu zaman yeterli. NewOrder'ı yazdım çünkü bir siparişin anlamlı olması için kimliği, müşterisi ve oluşma zamanı olmalı. Ama Go, orders.Order{} yazıp bu fonksiyonu atlamanı engellemez; C#'ta private constructor ile korunan invariant'lar (bir object'in her zaman doğru kalması gereken kuralları) Go'da sadece konvansiyonla korunur. Bunu bilerek tasarla: zero value'su geçersiz olan bir tipin field'larını ya export etme ya da method'larda kontrol et.

NewOrder pointer döndürüyor. Bir siparişin durumu değişecek ve onu bir repository'de saklayacağız; herkesin aynı siparişi görmesi için pointer doğru seçim. Zaman time.Now() ile içeride alınmıyor, parametre olarak geliyor. Bu bir test alışkanlığı: zamanı dışarıdan veren kod, testte sabit bir zamanla çalıştırılabilir. Status sabitleri 03'teki iota kalıbı; String() method'u sayesinde log'larda 1 yerine Paid yazıyor. 03'te söz verdiğim durum geçişi kontrolü de MarkPaid'de: sadece Pending bir sipariş ödenebilir. Şimdilik sonucu bool ile bildiriyor; 05'te bunu anlamlı bir hataya çevireceğiz.

Value mı Pointer mı? Receiver Kuralı

Önce Tahmin Et'te gördüğümüz davranıştan pratik bir kural çıkıyor. Method receiver'ı değiştiriyorsa pointer receiver kullan. Struct büyükse, her çağrıda kopyalanmasın diye yine pointer kullan. sync.Mutex gibi kopyalanmaması gereken bir field içeriyorsa pointer kullan. Küçük, değişmeyen değerler için (Money, Status, Item gibi) value receiver doğal. Bir tipin method'larından biri pointer receiver ise, tutarlılık için hepsini pointer yap; Order'da Total ve String bu yüzden pointer receiver.

Receiver seçimi bir tipin hangi interface'leri sağladığını da etkiler. Pointer receiver'lı bir method sadece pointer'ın method set'ine girer. MarkPaid'i isteyen bir interface'e Order değerini vermeyi denedim:

type Payer interface {
    MarkPaid(now time.Time) bool
}

o := orders.NewOrder("ORD-1001", "ada", time.Now())
var p Payer = *o
cannot use *o (variable of struct type orders.Order) as Payer value in variable declaration: orders.Order does not implement Payer (method MarkPaid has pointer receiver)

Derleyici neden açıkça söylüyor. var p Payer = o yazınca, yani pointer'ı verince derlenir. Bu kural rastgele değil: interface bir değerin kopyasını tutsaydı, MarkPaid kopyanın durumunu değiştirir ve Önce Tahmin Et'teki hatayı bu kez interface üzerinden yapardık.

Embedding: Composition, Inheritance Değil

Order'ın ilk field'ı bir isim almadan yazılmış: Timestamps. Buna embedding denir. Embed edilen struct'ın field'ları ve method'ları dış struct'a promote edilir (Go spec'indeki adıyla promoted field ve method): o.CreatedAt ve o.Touch(now) yazabilirsin, o.Timestamps.CreatedAt yazmana gerek yok. AddItem içindeki o.Touch(now) çağrısı bu sayede çalışıyor. C#'ta bunu bir AuditableEntity base class'ıyla yapardın. Go'da base class yok ve embedding inheritance değil:

var t *orders.Timestamps = o
cannot use o (variable of type *orders.Order) as *orders.Timestamps value in variable declaration

Bir *Order, *Timestamps yerine geçmez; aralarında "is-a" ilişkisi yok. Order bir Timestamps içeriyor ve onun method'larını kısayol olarak sunuyor. İçteki değere ihtiyacın varsa açıkça &o.Timestamps yazarsın. Embed edilen tipin method'ları virtual da değil: Touch içinde t, her zaman Timestamps'tir ve dış struct'tan haberi yoktur. C#'taki virtual dispatch'in Go'daki karşılığı interface'ler. C# ile Nesne Yönelimli Programlama serisinin composition ve SOLID (nesne yönelimli tasarımın beş temel ilkesinin baş harfleri) ilkelerine giriş yazısında anlattığımız "inheritance yerine composition" önerisi Go'da bir öneri değil, dilin tek yolu.

Implicit Interface ve Tüketen Tarafta Tanımlamak

Siparişleri saklamak için bir service ve onun ihtiyaç duyduğu bir interface yazalım:

package orders

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

// Repository, Service'in depolamadan beklediği şeydir. Tüketicisinin yanında
// tanımlanır ve başka yerde, adı hiç anılmadan implement edilir.
type Repository interface {
    Save(ctx context.Context, o *Order) error
    ByID(ctx context.Context, id string) (*Order, error)
}

// Service sipariş oluşturur.
type Service struct {
    repo   Repository
    now    func() time.Time
    nextID int
}

// NewService herhangi bir Repository kabul eder ve somut bir *Service döndürür.
func NewService(repo Repository) *Service {
    return &Service{repo: repo, now: time.Now, nextID: 1000}
}

// Place verilen satırlarla bir sipariş oluşturur ve kaydeder.
func (s *Service) Place(ctx context.Context, customerID string, items []Item) (*Order, error) {
    s.nextID++ // henüz eşzamanlı kullanım için güvenli değil; 08'de düzelecek
    o := NewOrder(fmt.Sprintf("ORD-%d", s.nextID), customerID, s.now())
    for _, it := range items {
        o.AddItem(it, s.now())
    }
    if err := s.repo.Save(ctx, o); err != nil {
        return nil, err
    }
    return o, nil
}

Go'da bir tip bir interface'i : IRepository gibi bir bildirimle implement etmez. Interface'in istediği method'lar tipte varsa, tip o interface'i sağlar; o kadar. Bunun önemli bir sonucu var: interface'i implementasyonun yanında değil, onu kullananın yanında tanımlarsın. C# projelerinde IOrderRepository genellikle ayrı bir abstraction projesinde durur ve implementasyon onu referans eder. Go'da Repository, onu kullanan Service'in package'ında. Implementasyonlar bu interface'in adını hiç anmak zorunda değil.

Bu yaklaşım interface'leri küçük tutar. Service sadece Save ve ByID istiyor; depolama katmanı on method sunsa bile interface iki method'luk. Go standart kütüphanesinin en çok kullanılan interface'leri de böyle: io.Reader tek method, fmt.Stringer tek method, error tek method. SOLID serisinin "Interface Segregation Principle" yazısında anlattığımız role interface fikri Go'da varsayılan davranış. İsimlendirmede de Go'nun bir alışkanlığı var: package adı her çağrıda göründüğü için orders.OrderRepository yerine orders.Repository yazılır, ismi tekrarlamaya gerek yok.

Go topluluğunun bir sözü bu bölümü özetliyor: "accept interfaces, return structs". NewService bir interface kabul ediyor, böylece testte sahte bir repository verebiliriz; ama somut bir *Service döndürüyor, çünkü çağıranın elinde bütün method'ları olan gerçek tip olmalı. ctx context.Context parametresine şimdilik takılma; iptal ve timeout taşıyan bu tipi 08'de anlatacağız. Go'da I/O yapan her method'un ilk parametresi olması bir konvansiyon, C#'taki CancellationToken gibi.

İki Implementasyon: memstore ve sqlstore

İlk implementasyon siparişleri bellekte, bir map'te tutuyor:

// Package memstore siparişleri bellekte tutar.
package memstore

import (
    "context"
    "fmt"

    "bookstore/internal/orders"
)

// Store bellek içi bir sipariş deposudur. Henüz eşzamanlı kullanım için güvenli değil.
type Store struct {
    byID map[string]*orders.Order
}

// *Store'un orders.Repository'yi sağladığının derleme zamanı kontrolü.
var _ orders.Repository = (*Store)(nil)

// New boş bir store döndürür.
func New() *Store {
    return &Store{byID: make(map[string]*orders.Order)}
}

// Save siparişi kaydeder; aynı ID'li siparişin üzerine yazar.
func (s *Store) Save(_ context.Context, o *orders.Order) error {
    s.byID[o.ID] = o
    return nil
}

// ByID verilen ID'li siparişi döndürür.
func (s *Store) ByID(_ context.Context, id string) (*orders.Order, error) {
    o, ok := s.byID[id]
    if !ok {
        return nil, fmt.Errorf("order %s not found", id)
    }
    return o, nil
}

var _ orders.Repository = (*Store)(nil) satırı bir Go deyimi. Hiçbir şey yapmayan bir atama, ama *Store interface'i sağlamıyorsa derleme burada durur. Implicit interface'in tek dezavantajı, bir method'un imzasını yanlış yazdığında hatanın tipi kullandığın yerde çıkması; bu satır hatayı implementasyonun yanına çeker. make ile map'i oluşturmayı unutmadım, 03'teki nil map dersini hatırla. Store'un neden eşzamanlı kullanım için güvenli olmadığını ve nasıl düzeltileceğini 08'de konuşacağız.

İkinci implementasyon SQL Server için. Bu yazıda bir veritabanına bağlanmıyoruz, bu yüzden bu kodu çalıştırmadım; sadece derleniyor ve aynı interface'i sağladığını derleyici doğruluyor. Driver'ı, tabloları ve transaction'ları 11'de ekleyeceğiz:

// Package sqlstore siparişleri database/sql üzerinden SQL Server'da tutar.
// Driver ve gerçek sorgular 11. yazıda geliyor.
package sqlstore

import (
    "context"
    "database/sql"

    "bookstore/internal/orders"
)

// Store, Orders tablosunu okur ve yazar.
type Store struct {
    db *sql.DB
}

var _ orders.Repository = (*Store)(nil)

// New açık bir *sql.DB'yi sarar.
func New(db *sql.DB) *Store {
    return &Store{db: db}
}

// Save sipariş başlığını ekler. Satırlar ve transaction 11. yazıda.
func (s *Store) Save(ctx context.Context, o *orders.Order) error {
    _, err := s.db.ExecContext(ctx,
        `INSERT INTO Orders (Id, CustomerId, Status, CreatedAt) VALUES (@p1, @p2, @p3, @p4)`,
        o.ID, o.CustomerID, int(o.Status), o.CreatedAt)
    return err
}

// ByID sipariş başlığını yükler.
func (s *Store) ByID(ctx context.Context, id string) (*orders.Order, error) {
    o := &orders.Order{}
    err := s.db.QueryRowContext(ctx,
        `SELECT Id, CustomerId, Status, CreatedAt FROM Orders WHERE Id = @p1`, id).
        Scan(&o.ID, &o.CustomerID, &o.Status, &o.CreatedAt)
    if err != nil {
        return nil, err
    }
    return o, nil
}

İki package'ın da orders'ı import ettiğine, orders'ın ise ikisini de import etmediğine dikkat et. Bağımlılık yönü tek: depolama domain'i bilir, domain depolamayı bilmez. 02'deki import cycle tuzağının çözümü olarak söz ettiğim "bir tarafta küçük bir interface tanımlamak" tam olarak bu. Dapper serisinde elle yazılmış SQL ile repository kurmuştuk; sqlstore aynı zihniyetin Go'daki karşılığı olacak.

Stringer, Type Assertion ve Type Switch

Parçaları bir lab programında birleştirdim. Program bir sipariş veriyor, ödüyor, store'dan geri okuyor ve farklı tipteki değerleri bir type switch'ten geçiriyor:

// Lab 04: siparişler, Stringer, type switch ve nil interface tuzağı.
package main

import (
    "context"
    "fmt"
    "time"

    "bookstore/internal/catalog"
    "bookstore/internal/orders"
    "bookstore/internal/orders/memstore"
    "bookstore/internal/orders/sqlstore"
)

func main() {
    ctx := context.Background()
    repo := memstore.New()
    svc := orders.NewService(repo)

    books := catalog.Store{}.All()
    dune, emma := books[0], books[3]
    o, err := svc.Place(ctx, "ada", []orders.Item{
        {ISBN: dune.ISBN, Title: dune.Title, UnitPrice: dune.Price, Quantity: 2},
        {ISBN: emma.ISBN, Title: emma.Title, UnitPrice: emma.Price, Quantity: 1},
    })
    if err != nil {
        fmt.Println("place failed:", err)
        return
    }
    fmt.Println(o)
    fmt.Println(o.MarkPaid(time.Now()), o.MarkPaid(time.Now()), o.Status)

    found, err := repo.ByID(ctx, o.ID)
    fmt.Println(found == o, err)
    _, err = repo.ByID(ctx, "ORD-9999")
    fmt.Println(err)

    for _, v := range []any{o.Total(), o.Status, err, 42, nil} {
        fmt.Println(describe(v))
    }

    var r orders.Repository = repo
    if ms, ok := r.(*memstore.Store); ok {
        fmt.Printf("type assertion: %T\n", ms)
    }

    r = pickRepository(false)
    //lint:ignore SA4023 bu lab tuzağı bilerek gösteriyor
    fmt.Printf("r == nil: %v, dynamic type: %T\n", r == nil, r)
}

func describe(v any) string {
    switch x := v.(type) {
    case nil:
        return "nil"
    case catalog.Money:
        return "money: " + x.String()
    case fmt.Stringer:
        return "stringer: " + x.String()
    case error:
        return "error: " + x.Error()
    default:
        return fmt.Sprintf("other: %T", x)
    }
}

// pickRepository'de bir hata var: interface'in içinde tipli bir nil pointer döndürüyor.
func pickRepository(useSQL bool) orders.Repository {
    var s *sqlstore.Store
    if useSQL {
        s = sqlstore.New(nil)
    }
    return s
}
go run ./labs/04-orders
order ORD-1001 (Pending): 2 items, 898.80 TRY
true false Paid
true <nil>
order ORD-9999 not found
money: 898.80 TRY
stringer: Paid
error: order ORD-9999 not found
other: int
nil
type assertion: *memstore.Store
r == nil: false, dynamic type: *sqlstore.Store

İlk satırda fmt.Println(o) struct'ın field'larını değil, String()'in çıktısını yazdı. fmt package'ı bir değerin fmt.Stringer interface'ini (String() string) sağlayıp sağlamadığına bakıyor; C#'taki ToString() override'ının karşılığı, ama burada da bir override yok, sadece method'un varlığı yetiyor. 898,80 TL; iki Dune ve bir Emma. 03'teki Money tipine de bir String() ekledim, tutarlar bu yüzden kuruş yerine TL olarak yazıldı. İkinci satırda MarkPaid ilk çağrıda true, ikincide false döndü; ödenmiş bir sipariş tekrar ödenemiyor.

any, interface{}'in diğer adı; her tip onu sağlar, C#'taki object gibi. Bir interface değerinin içindeki somut tipe iki yolla ulaşırsın. Type assertion, r.(*memstore.Store), tek bir tipi sorar; comma-ok biçimiyle yazınca tip uymazsa panic yerine false döner. Type switch ise birden fazla tipi sırayla dener; C#'taki switch (v) { case Money m: ... } pattern matching'in karşılığı. Case'lerin sırası önemli: catalog.Money da bir Stringer, fmt.Stringer case'i önce yazılsaydı tutar "money:" yerine "stringer:" diye yazılırdı. err ise String() method'u olmayan bir error olduğu için üçüncü case'e düştü.

nil Interface Tuzağı

Çıktının son satırı Go'nun en bilinen tuzaklarından biri. pickRepository(false) hiçbir store oluşturmadı, s nil kaldı. Ama dönen interface nil değil. Bir interface değeri iki parçadan oluşur: içindeki somut tip ve o tipin değeri. Interface'in nil olması için ikisinin de boş olması gerekir. return s satırı tipi *sqlstore.Store, değeri nil pointer olan bir interface döndürdü; %T tipi açıkça gösteriyor. Bu interface'le r.ByID(...) çağırsaydın, method çalışır ve s.db'ye erişirken nil pointer panic'i alırdın.

Derleyici bu durumu yakalamaz, ama staticcheck yakalar. Lab'daki //lint:ignore satırını silince:

labs/04-orders/main.go:48:49: this comparison is never true (SA4023)
    labs/04-orders/main.go:47:6: the lhs of the comparison is the 1st return value of this function call
    labs/04-orders/main.go:67:6: bookstore/labs/04-orders.pickRepository never returns a nil interface value

Çözüm, "yok" demek istediğin yerde açıkça return nil yazmak: if !useSQL { return nil }. Kural kısa: interface döndüren bir fonksiyonda somut tipli bir değişkeni döndürme, ya gerçek bir değer ya da düz nil döndür. Aynı tuzak hatalarda daha sık görülür, çünkü error de bir interface; 05'te tam olarak bu hatayı yapan bir validation fonksiyonu göreceğiz.

Üretimde Dikkat

Interface'leri ihtiyaç doğunca aç. C# projelerinde her service için bir interface yazmak alışkanlıktır, çünkü DI (Dependency Injection; bağımlılıkların dışarıdan verilmesi) container'ı ve mock framework'leri bunu ister. Go'da tek implementasyonu olan bir tip için interface yazmak çoğu zaman gereksiz bir katman. Interface'i, ikinci bir implementasyon (testteki fake ya da ikinci bir depolama) gerçekten gerektiğinde ve onu kullanan tarafta tanımla.

Paylaşılan pointer'lara dikkat et. memstore siparişin pointer'ını saklıyor ve ByID aynı pointer'ı döndürüyor; lab'daki found == o satırı true yazdı. Bu, store'dan okuyan herkesin aynı siparişi değiştirebileceği anlamına geliyor. Gerçek bir veritabanı her okumada yeni bir kopya verir; bellek içi bir store'u test dışında kullanacaksan kopyasını döndürmeyi düşün.

Zaman ve ID üretimini dışarıdan ver. Service'teki now func() time.Time field'ı bu yüzden var. Production'da time.Now, testte sabit bir saat. ID sayacı ise şimdilik basit bir int; birden fazla request aynı anda sipariş verirse çakışır. 08'de bunu düzelteceğiz.

Sık Düşülen Tuzaklar

Value receiver'lı bir method'da state değiştirmek. Kod derlenir, değişiklik kaybolur. Bir tipin method'larından biri bile pointer receiver ise hepsini pointer yap.

Embedding'i inheritance gibi kullanmak. Embed edilen tipin method'ları dış tipi bilmez ve override edilemez. Polymorphism gerekiyorsa interface kullan.

Interface'i implementasyonun yanında, büyük ve önceden tanımlamak. IOrderRepository'yi on method'la yazıp her yere vermek, Go'da test yazmayı ve yeni bir implementasyon eklemeyi zorlaştırır. Tüketici neye ihtiyaç duyuyorsa interface o kadardır.

Tipli nil'i interface olarak döndürmek. var s *Store; return s nil olmayan bir interface üretir. Özellikle error döndüren fonksiyonlarda bu hata sinsi bir şekilde "hata var" sonucuna yol açar.

Type switch'te geniş case'i öne yazmak. fmt.Stringer ya da error gibi interface case'leri, onları sağlayan somut tiplerin case'lerinden sonra gelmeli.

Küçük Bir Deneme

★☆☆ orders'a MarkShipped(now time.Time) bool ekle: sadece Paid bir sipariş Shipped olabilsin. labs/04-orders'da ödenmemiş bir siparişi kargoya vermeyi dene.

★★☆ pickRepository'yi düzelt ve useSQL false iken gerçekten nil döndürdüğünü lab'da göster. Sonra //lint:ignore satırını sil ve staticcheck'in artık bir şey demediğini kontrol et.

★★★ Order'a bir Discount alanı ekleyecek bir Discounted struct'ı yaz ve Order'a embed et; Discounted'ın da bir Total() method'u olsun. o.Total() hangisini çağırıyor? Discounted'ın Total'ı içeriden Order'ın satırlarına ulaşabiliyor mu? Bu deney embedding'in neden inheritance olmadığını gösteriyor.

Sipariş modeli hazır, ama hata durumlarını şimdilik bool ve düz metinle anlatıyoruz. Bir sonraki yazıda Go'nun error handling yaklaşımına geçiyoruz: error değerleri, ErrOutOfStock gibi sentinel error'lar, ValidationError gibi custom tipler, %w ile wrapping, errors.Is, errors.As ve Go 1.26'daki errors.AsType, son olarak da panic ve recover.