---
title: 'Veritabanı Erişimi'
source: 'https://academia.sh/tr/kurslar/go-uygulamalar/veritabani-erisimi'
course: 'Standart Kütüphane ve Ağ Uygulamaları'
language: tr
updated: '2026-08-23T16:55:13+00:00'
license: 'CC BY-SA 4.0'
---

# Veritabanı Erişimi

Bir kukla sürücüyle beş sütunluk bir sorgu koşturulur: sürücünün dar tip kümesinden gelen değerlerin üçü, hedef değişkenin kendi Scan yöntemiyle özel bir Go tipine geri kurulur, ikisi zaten sürücünün doğal tipiyle örtüşür. Sınırlayıcı ölçüm, taramanın hedefin tipine baktığını ve uyumsuz bir hedefin çalışma zamanında hata verdiğini, bağlantı havuzu ölçümü ise kapatılmayan bir satır kümesinin havuzu tıkadığını koşturarak gösteriyor.

Önceki ders bir günlük satırından çıkarılan bir alanın tipinin metin olarak kaldığını,
geri kurmanın okuyan tarafın kendi tarifine bağlı olduğunu gösterdi. Bu konunun son dersi
aynı soruyu bir veritabanı sürücüsünün elinden geçen bir satır üzerinde soruyor — ama bu kez
tarif metinde değil, kodun kendisinde: çağıranın `Scan` çağrısına verdiği hedef değişkenin
tipinde duruyor.

Veritabanı satırı, konu boyunca ölçülen sınırların en katmanlısı. Dosya ve arabellek dersinde
sınır tek bir arayüzdü (`Read`/`Write`); burada üç katman var — sürücünün kendi dar tip
kümesi, `database/sql`in bu kümeyi taşıyan genel mekanizması, ve çağıranın `Scan`e verdiği
hedef tip. Her katman kendi payına düşen dönüşümü yapıyor, ve bu ders üç katmanın da nerede
durduğunu ayrı ayrı gösteriyor.

**Gerçek kaynak kullanılmaz.** Bu derste hiçbir gerçek veritabanı çalıştırılmıyor; standart
kitaplığın `database/sql/driver` paketindeki sürücü arayüzü, bu dersin kendi yazdığı bir
**kukla** ile gerçekleştiriliyor. Kukla sürücü sabit, elde önceden hazırlanmış iki satırlık
bir veri döndürüyor; hiçbir ağ bağlantısı, dosya ya da dış süreç yok.

## Kukla Sürücü

Go'nun standart kitaplığı `database/sql` paketiyle **genel** bir veritabanı arayüzü sunuyor;
gerçek sürücüler (belirli bir veritabanı ürününe özgü bağlantı mantığı) her zaman üçüncü
taraf paketler olarak dağıtılıyor. Bu ders o ayrımı ciddiye alıyor: `database/sql/driver`
paketindeki arayüzleri (`Driver`, `Conn`, `Rows`) kendi yazdığı basit bir tiple
gerçekleştiriyor, hiçbir üçüncü taraf sürücü içe aktarmıyor.

```go
// kukla.go — database/sql/driver arayuzu kendi kuklasiyla gerceklestiriliyor, gercek veritabani yok
package main

import (
	"database/sql"
	"database/sql/driver"
	"fmt"
	"io"
	"time"
)

// KullaniciID, satirin ilk sutununu tasiyan tip: surucu bunu int64 olarak tasir.
type KullaniciID int64

func (k *KullaniciID) Scan(kaynak any) error {
	v, ok := kaynak.(int64)
	if !ok {
		return fmt.Errorf("KullaniciID.Scan: beklenmeyen kaynak tipi %T", kaynak)
	}
	*k = KullaniciID(v)
	return nil
}

// Tutar, kurus cinsinden bir tutari tasir; surucu bunu da int64 olarak tasir.
type Tutar int64

func (t *Tutar) Scan(kaynak any) error {
	v, ok := kaynak.(int64)
	if !ok {
		return fmt.Errorf("Tutar.Scan: beklenmeyen kaynak tipi %T", kaynak)
	}
	*t = Tutar(v)
	return nil
}

// ZamanDamgasi, surucunun dizgi olarak tasidigi bir zamani time.Time'a cevirir.
type ZamanDamgasi struct{ time.Time }

func (z *ZamanDamgasi) Scan(kaynak any) error {
	s, ok := kaynak.(string)
	if !ok {
		return fmt.Errorf("ZamanDamgasi.Scan: beklenmeyen kaynak tipi %T", kaynak)
	}
	ayristirilan, err := time.Parse(time.RFC3339, s)
	if err != nil {
		return err
	}
	z.Time = ayristirilan
	return nil
}

var satirVerisi = [][]driver.Value{
	{int64(1), "kalem", int64(1250), true, "2026-08-23T10:00:00Z"},
	{int64(2), "defter", int64(3990), false, "2026-08-20T09:30:00Z"},
}

type kuklaSurucu struct{}

func (kuklaSurucu) Open(ad string) (driver.Conn, error) { return &kuklaBaglanti{}, nil }

type kuklaBaglanti struct{}

func (kuklaBaglanti) Prepare(sorgu string) (driver.Stmt, error) {
	return nil, fmt.Errorf("kukla surucu: hazirlanmis ifade desteklenmiyor")
}
func (kuklaBaglanti) Close() error { return nil }
func (kuklaBaglanti) Begin() (driver.Tx, error) {
	return nil, fmt.Errorf("kukla surucu: islem desteklenmiyor")
}
func (kuklaBaglanti) Query(sorgu string, args []driver.Value) (driver.Rows, error) {
	return &kuklaSatirlar{veri: satirVerisi, konum: 0}, nil
}

type kuklaSatirlar struct {
	veri  [][]driver.Value
	konum int
}

func (kuklaSatirlar) Columns() []string {
	return []string{"id", "ad", "fiyat_kurus", "aktif", "eklenme"}
}
func (kuklaSatirlar) Close() error { return nil }
func (r *kuklaSatirlar) Next(dest []driver.Value) error {
	if r.konum >= len(r.veri) {
		return io.EOF
	}
	copy(dest, r.veri[r.konum])
	r.konum++
	return nil
}

func init() {
	sql.Register("kukla", kuklaSurucu{})
}
```

Bu dosya `func main` içermediği için diske yazılıyor ama çalıştırılmıyor; aşağıdaki ölçümler
onu kullanıyor. `kuklaSatirlar.Next` yöntemi, sürücünün gerçekte yaptığı şeyi taklit ediyor:
her satırı `driver.Value` diliminde döndürüyor, ve `driver.Value` **kasıtlı olarak dar** bir
tip: yalnızca `int64`, `float64`, `bool`, `[]byte`, `string`, `time.Time` ve `nil` bu tipe
girebiliyor. Bu ders için önemli olan sonuç: bir sütunun kaynaktaki "gerçek" anlamı ne
olursa olsun (bir kullanıcı kimliği, bir para tutarı, bir zaman damgası), sürücü sınırını
geçerken bu yedi tipten birine **indirgeniyor**.

Bu darlık kasıtlı bir tasarım kararı. `database/sql` paketi genel bir sözleşme sunuyor —
her sürücü, hangi veritabanı ürününü konuşursa konuşsun, satırdan gelen değeri bu yedi tipten
birine dönüştürmek zorunda. Çağıran kod, hangi sürücüyle çalıştığını bilmeden aynı `Query` ve
`Scan` çağrılarını kullanabiliyor, çünkü sürücüler arasındaki fark tam olarak bu dar arayüzün
arkasında kalıyor. Bu ders, gerçek bir sürücünün yaptığı şeyi en aza indirgenmiş bir biçimde
tekrarlıyor: iki satırlık sabit bir veri, bağlantı açma girişiminde hiçbir gerçek işlem
yapmayan bir `Open`, ve yalnızca elindeki diziyi sırayla döndüren bir `Next`.

## Beş Sütun, Sürücünün Dar Tip Kümesi

**SS17.** — Beş sütunluk bir sorgu koşturuluyor; her sütun kendi Go tipine (`KullaniciID`,
`string`, `Tutar`, `bool`, `ZamanDamgasi`) taranıyor ve sonucun tipi `%T` ile okunuyor.

```go
// olcum.go — bes sutun kendi tipine taraniyor, indirgenen ve indirgenmeyen sutunlar ayriliyor
package main

import (
	"database/sql"
	"fmt"
)

func main() {
	db, _ := sql.Open("kukla", "bellek-ici")
	defer db.Close()

	satirlar, _ := db.Query("select id, ad, fiyat_kurus, aktif, eklenme from urunler")
	defer satirlar.Close()

	for satirlar.Next() {
		var id KullaniciID
		var ad string
		var fiyat Tutar
		var aktif bool
		var eklenme ZamanDamgasi
		if err := satirlar.Scan(&id, &ad, &fiyat, &aktif, &eklenme); err != nil {
			fmt.Println("tarama hatasi:", err)
			continue
		}
		fmt.Printf("id=%d(%T) ad=%s(%T) fiyat=%d(%T) aktif=%t(%T) eklenme=%s(%T)\n",
			id, id, ad, ad, fiyat, fiyat, aktif, aktif, eklenme.Format("2006-01-02"), eklenme)
	}
}
```

```
id=1(main.KullaniciID) ad=kalem(string) fiyat=1250(main.Tutar) aktif=true(bool) eklenme=2026-08-23(main.ZamanDamgasi)
id=2(main.KullaniciID) ad=defter(string) fiyat=3990(main.Tutar) aktif=false(bool) eklenme=2026-08-20(main.ZamanDamgasi)
```

Beş sütunun üçü — `id`, `fiyat_kurus`, `eklenme` — sürücüden geldiğinde sırasıyla `int64`,
`int64`, `string`; hiçbiri hedef tipin adını (`KullaniciID`, `Tutar`, `ZamanDamgasi`) taşımıyor.
Bu üç sütunun her biri, hedef değişkenin kendi `Scan` yöntemi tarafından **geri kuruluyor** —
`KullaniciID.Scan` ham `int64`'ü alıp kendi tipine dönüştürüyor, `ZamanDamgasi.Scan` ham dizgiyi
alıp `time.Parse` ile bir `time.Time`e çeviriyor. Öteki iki sütun — `ad`, `aktif` — sürücüden
zaten hedef tipiyle aynı biçimde geliyor (`string`, `bool`); onlar için özel bir `Scan`
yöntemine gerek yok, doğrudan atama yeterli. Sonuç: **beş sütunun üçü indirgeniyor ve taramayla
geri kuruluyor, ikisi zaten sürücünün doğal tipiyle örtüşüyor.**

Bu üç `Scan` yöntemi, konunun ölçü ekseninin en net "yeniden kuruluyor" örneği: sürücü hiçbir
yerde "bu bir kullanıcı kimliği" ya da "bu bir zaman" demiyor, yalnızca ham bir sayı ya da
dizgi veriyor. Geri kurmayı sağlayan tarif, sürücüde değil, çağıranın `Scan`e verdiği hedef
değişkenin **kendi tipinde ve kendi yönteminde** duruyor.

`ad` ve `aktif` sütunlarının hiçbir özel `Scan` yöntemine ihtiyaç duymaması bir rastlantı
değil: standart kitaplığın kendi dönüştürme mantığı, hedef değişkenin tipi sürücüden gelen
`driver.Value` tipiyle zaten örtüştüğünde, hiçbir ek adım gerektirmeden doğrudan atama
yapıyor. Özel bir `Scan` yöntemi yalnızca hedef tip sürücünün tanıdığı yedi tipin **dışında**
olduğunda devreye giriyor — `KullaniciID`, `Tutar` ve `ZamanDamgasi` bu yüzden kendi
yöntemlerini taşıyor, `string` ve `bool` taşımıyor. Bu, sürücünün dar tip kümesinin sınırının
tam olarak nerede çizildiğini gösteriyor: sınır sürücüde sabit, ama sınırın ötesine geçen her
hedef tip kendi geçiş kuralını kendisi yazmak zorunda.

## Sınırlayıcı Ölçüm: Tarama Hedefin Tipine Bakar

Yukarıdaki `Scan` çağrısı, sütun sırasının hedef değişken sırasıyla eşleştiği varsayımına
dayanıyor. Bu eşleşme bozulduğunda — bir sütunun hedefi, o sütunun taşıdığı `driver.Value`
tipine uymadığında — sonuç bir derleme hatası değil, bir **çalışma zamanı** hatası.

**SS18.** — İkinci sütun (`ad`, bir `string`) yanlışlıkla `KullaniciID` tipinde bir hedefe
taranıyor; sorgunun kendisi hiç değişmiyor, yalnızca `Scan` çağrısındaki değişken sırası
karışıyor.

```go
// uyumsuz-hedef.go — sorgu ayni, yalniz Scan hedefi karisiyor; hata calisma zamaninda geliyor
package main

import (
	"database/sql"
	"fmt"
)

func main() {
	db, _ := sql.Open("kukla", "bellek-ici")
	defer db.Close()

	satirlar, _ := db.Query("select id, ad, fiyat_kurus, aktif, eklenme from urunler")
	defer satirlar.Close()

	satirlar.Next()
	var id KullaniciID
	var yanlisHedef KullaniciID // ikinci sutun (ad, bir dizgi) buraya taranmaya calisiliyor
	var fiyat Tutar
	var aktif bool
	var eklenme ZamanDamgasi
	hata := satirlar.Scan(&id, &yanlisHedef, &fiyat, &aktif, &eklenme)
	fmt.Println("uyumsuz hedefle tarama hata verdi mi:", hata != nil)
}
```

```
uyumsuz hedefle tarama hata verdi mi: true
```

Sorgu metni (`"select id, ad, ..."`) burada hiçbir şey söylemiyor — sütun adları doğru,
sorgu kukla sürücüden başarıyla dönüyor. Hata, yalnızca `Scan` çağrısı çalıştığında ortaya
çıkıyor: `yanlisHedef` bir `KullaniciID`, ama sürücüden gelen ikinci sütunun değeri bir
`string` (`"kalem"`), ve `KullaniciID.Scan` bunu bir `int64`e dönüştüremediği için hata
döndürüyor. Bu, konunun ölçü ekseninin bu derse özgü sınırı: tarama **hedefin** tipine
bakıyor, **kaynağın** tipine değil — sütunun kendisi hangi tipte olduğunu önceden ilan
etmiyor, yalnızca hedefe uyup uymadığı çalışma zamanında sınanıyor. Bir sorgu yanlış yazılmış
gibi görünmüyor; yalnızca `Scan` argümanlarının sırası kaymış, ve bu kaymayı derleyici hiç
yakalayamıyor çünkü `Scan`in imzası `...any` — kaç ve hangi tipte argüman verildiği derleme
zamanında denetlenmiyor.

Bu, önceki dersteki günlük alanı okumasıyla aynı biçimde ortaya çıkan bir kusur. Orada da
yanlış bir tarifle (`basarili` alanını `int` diye okumak) hata çalışma zamanında geliyordu,
sorgu ya da günlük satırı yazılırken değil. Aradaki fark, sorgunun `select` listesi ile
`Scan`in argüman listesi arasındaki bağın **konuma** dayanması: iki liste arasında sütun
adına bakan hiçbir denetim yok, yalnızca sıra var — ve bu sıranın doğru olup olmadığı, ancak
program fiilen çalıştırıldığında sınanıyor.

## Bağlantı Havuzu ve Kapatma Sorumluluğu

`sql.DB` bir tek bağlantı değil, bir **bağlantı havuzu**; havuzun büyüklüğü
`SetMaxOpenConns` ile sınırlanabiliyor. Bir sorgunun sonucu (`*sql.Rows`) kapatılana kadar,
o sorgunun kullandığı bağlantı havuza geri dönmüyor — kapatmayı unutmak, havuzu tıkıyor.

**SS19.** — Havuz tek bağlantıyla sınırlanıyor. İlk sorgunun sonucu **kasıtlı olarak**
kapatılmadan, aynı havuzdan ikinci bir sorgu (kısa bir zaman aşımıyla) deneniyor; sonra ilk
sonuç kapatılıp üçüncü bir sorgu deneniyor.

```go
// havuz.go — kapatilmayan bir satir kumesi tek baglantili havuzu tikiyor
package main

import (
	"context"
	"database/sql"
	"fmt"
)

func main() {
	db, _ := sql.Open("kukla", "bellek-ici")
	defer db.Close()
	db.SetMaxOpenConns(1)

	birinci, _ := db.Query("select id, ad, fiyat_kurus, aktif, eklenme from urunler")
	birinci.Next() // baglantiyi acik tutuyor, henuz kapatilmadi

	fmt.Println("birinci sorgudan sonra acik baglanti sayisi:", db.Stats().OpenConnections)

	baglamKisa, iptal := context.WithTimeout(context.Background(), 0)
	defer iptal()
	_, hataIkinci := db.QueryContext(baglamKisa, "select id, ad, fiyat_kurus, aktif, eklenme from urunler")
	fmt.Println("havuz doluyken ikinci sorgu basarisiz mi:", hataIkinci != nil)

	birinci.Close() // baglantiyi havuza geri birakir

	ucuncu, hataUcuncu := db.Query("select id, ad, fiyat_kurus, aktif, eklenme from urunler")
	fmt.Println("kapatmadan sonra sorgu basarili mi:", hataUcuncu == nil)
	if hataUcuncu == nil {
		defer ucuncu.Close()
	}
}
```

```
birinci sorgudan sonra acik baglanti sayisi: 1
havuz doluyken ikinci sorgu basarisiz mi: true
kapatmadan sonra sorgu basarili mi: true
```

Havuz tek bağlantıya sınırlandığında, ilk sorgunun sonucu kapatılmadan durduğu sürece o tek
bağlantı **meşgul** kalıyor. İkinci sorgu, süresi anında dolan bir bağlamla (`WithTimeout`
sıfır süreyle) deneniyor — gerçek bir süre ölçülmüyor, yalnızca "havuzda boş bağlantı yok"
durumunun hemen bir hata olarak dönmesi sağlanıyor. `birinci.Close()` çağrıldığında bağlantı
havuza geri dönüyor ve üçüncü sorgu sorunsuz çalışıyor. Bu, "kapatma sorumluluğu" ifadesinin
somut karşılığı: bir `*sql.Rows` değeri kapatılmadığı sürece, altındaki bağlantıyı **elinde
tutmaya devam ediyor** — havuzun büyüklüğü sınırlıysa, bu unutma diğer sorguları
tıkayabiliyor. Sürücünün kendisi (kukla ya da gerçek) burada rol oynamıyor; havuzun yönetimi
tamamen `database/sql` paketinin kendi sorumluluğunda.

Bu ölçüm havuzu bilerek tek bağlantıya indirdi ki tıkanma bir tek sorguyla görünür olsun;
gerçek boyuttaki bir havuzda aynı unutma birden fazla sorgu birikene kadar fark edilmeyebiliyor.
Kod içinde `defer satirlar.Close()` yazmak — `Query` çağrısından hemen sonra, satırların
kullanılmasından önce — bu unutmayı önlemenin standart yolu; bu ders boyunca kullanılan kalıp
da bu. Kapatmanın kendisi ölçülebilir bir sonuç üretmiyor, yalnızca bir sonraki sorgunun
başarılı olup olmamasını belirliyor — tam olarak bu ölçümün gösterdiği şey.

## Özet

- Sürücünün taşıdığı tip kümesi (`driver.Value`) yalnızca yedi tiple sınırlı; bir sütunun
  "gerçek" anlamı ne olursa olsun, sürücü sınırını geçerken bu kümeye indirgeniyor.
- Beş sütunun üçü (`id`, `fiyat_kurus`, `eklenme`) hedef tipin kendi `Scan` yöntemiyle geri
  kuruluyor; ikisi (`ad`, `aktif`) zaten sürücünün doğal tipiyle örtüştüğü için doğrudan
  atanıyor.
- Sınırlayıcı ölçüm: tarama hedefin tipine bakıyor, kaynağın tipine değil — uyumsuz bir hedef
  sorgu yazılırken değil, `Scan` çağrıldığında, çalışma zamanında hata veriyor.
- `sql.DB` bir bağlantı havuzu yönetiyor; bir sorgu sonucu kapatılmadığı sürece altındaki
  bağlantıyı elinde tutmaya devam ediyor.
- Kapatılmayan bir satır kümesi, sınırlı büyüklükteki bir havuzda sonraki sorguları
  tıkayabiliyor; kapatmak bağlantıyı havuza geri bırakıyor.

## Sonraki Adım

Bu derste tarama, hedefin tipine bakarak sürücünün dar tip kümesinden bir Go değerini geri
kurdu — tarif kodun kendisindeydi, `Scan`e verilen değişkenin tipinde. Konunun beş dersi
boyunca aynı soru beş ayrı yerde soruldu: dosya ve arabellekte, ikili dosyaya gömülen bir
kaynakta, metne dönen bir zaman değerinde, alan adlarını taşıyan bir günlük kaydında, ve
şimdi bir veritabanı satırında. Beşinde de sınırı geçen değer kendi başına yeterli değildi;
geri kurmayı sağlayan şey her seferinde karşı tarafın elindeki bir tarifti.

Tarifin nerede durduğu her derste değişti. Gömülü kaynakta tarif derleme zamanındaydı — hedef
değişkenin kendi tipi. Zaman değerinde tarif çağıranın bildiği bir düzendi. Günlük kaydında
tarif okuyan kodun elle yazdığı bir dönüşümdü. Bu derste tarif, hedef tipin kendi `Scan`
yöntemiydi — bir sözleşme olarak, standart kitaplığın kendisine bağlanmış. Ortak nokta hep
aynı: sınırın kendisi hiçbir zaman tip bilgisini taşımadı, taşıyan şey her defasında sınırın
bu tarafında duran bir bilgiydi.

Sıradaki ders bu konunun kapanışı ve aynı soruyu programın **girişine** taşıyor: bir komut
satırı argümanı da, tıpkı bir günlük satırı ya da bir veritabanı sütunu gibi, dizgi olarak
geliyor. Programın kendi ayrıştırıcısı bu dizgiden hangi tipleri, ne kadarını geri kurabiliyor?
