---
title: 'Dosya ve Girdi/Çıktı Arayüzleri'
source: 'https://academia.sh/tr/kurslar/go-uygulamalar/dosya-ve-girdi-cikti-arayuzleri'
course: 'Standart Kütüphane ve Ağ Uygulamaları'
language: tr
updated: '2026-08-23T16:55:14+00:00'
license: 'CC BY-SA 4.0'
---

# Dosya ve Girdi/Çıktı Arayüzleri

Dört ayrı kaynak (dosya, arabellek, dizgi ve daha önce ölçülen ağ gövdesi) aynı iki yöntemden — okuma ve yazma — geçirilir ve hiçbirinin sınırı aşarken tip bilgisi taşımadığı gösterilir. Sınırlayıcı ölçüm, küçük bir arabellekle okumanın veriyi ve dosya sonunu ayrı adımlarda döndürdüğünü ve bu ayrımı yönetmenin çağıranın işi olduğunu koşturarak ortaya koyuyor.

Ağ ve Veri Biçimleri konusunun son dersi bir soruyla kapandı: bir çerçeveleme kuralı
mesajın nerede başlayıp nerede bittiğini veriyor, ama o sınırın içindeki baytların **ne**
olduğunu vermiyor. Bu konu aynı soruyu programın dışına, ağa taşımadan soruyor. Bir
dosyadan okunan bayt, bir arabellekte biriken bayt ve az önce ağdan gelen bayt — üçü de
aynı sorunun cevabını arıyor: karşı taraftan gelen bir bayt dizisi, hangi tipe ait olduğunu
kendi başına söylüyor mu?

Yanıt hayır, ve bu konunun ilk dersi bu "hayır"ın nereden geldiğini gösteriyor: Go'nun
standart kitaplığı dosyayı, arabelleği, ağ gövdesini ve dizgiyi tek bir ortak sözleşmeyle
konuşturuyor — okuma ve yazma. Bu sözleşme o kadar dar ki, kaynağın kendisi hakkında hiçbir
şey söylemiyor; yalnızca bayt akıtıyor. Ağ ve Veri Biçimleri konusunda HTTP gövdesinin de bu
sözleşmeye uyduğu ölçülmüştü, burada tekrar kurulmuyor; bu ders üç **yeni** kaynağı — dosya,
arabellek, dizgi — aynı sözleşmeden geçirip aynı sonucu (tip bilgisinin taşınmadığını) ayrı
ayrı gösteriyor.

**Belirlenimcilik notu:** dosya kaynağı `os.MkdirTemp` ile kurulan geçici bir dizinde
duruyor; dizinin kendi yolu hiçbir yerde basılmıyor, yalnızca içeriğiyle ilgili sayılar
kullanılıyor.

## Okuma ve Yazma İki Yöntemdir

Go'nun standart kitaplığında bir kaynağın "okunabilir" sayılması için tek bir koşul var:
`Read(p []byte) (n int, err error)` imzasına sahip bir yöntem taşımak. Arayüzlerin kendisi —
örtük gerçekleştirim modeli, bir tipin "bu arayüzü karşılıyorum" diye ayrıca bir bildirim
yapmadan sadece doğru yöntemleri taşıyarak arayüzü karşılaması — Arayüzler, Hatalar ve
Genellikler kursunda kurulmuştu ve burada tekrar kurulmuyor. Bu ders o modelin standart
kitaplıktaki en yaygın örneğini ölçüyor: `io.Reader` arayüzü tek bir `Read` yöntemi istiyor,
ve dosya, ağ, sıkıştırma, şifreleme ve metin işleme paketlerinin **hepsi** bu tek arayüzün
üstüne kuruluyor. Yazma tarafında da simetrik bir arayüz var: `io.Writer`, tek yöntemi
`Write(p []byte) (n int, err error)`.

Bu iki arayüzün küçüklüğü bir eksiklik değil, tam tersi bir tasarım kararı. Bir kaynağın
"okunabilir" olması için taşıması gereken tek şey bu iki dönüş değeri olduğundan, `os.File`,
`bytes.Buffer`, `strings.Reader` ve bir ağ gövdesi gibi birbiriyle hiçbir ortak soyu paylaşmayan
tipler aynı arayüzü **bağımsız olarak** gerçekleştirebiliyor. Hiçbiri ötekini taklit etmek
zorunda değil; her biri kendi `Read` yöntemini kendi iç verisine göre yazıyor, ve dışarıdan
bakan kod hangi tipin elinde olduğunu hiç bilmeden aynı döngüyü çalıştırabiliyor. Örtük
gerçekleştirim burada özellikle görünür oluyor: `os.File` tipinin tanımında `io.Reader`
sözcüğü hiç geçmiyor, ama `Read` yöntemini doğru imzayla taşıdığı için arayüzü karşılıyor —
bağlantı, bir bildirimle değil, imza eşleşmesiyle kuruluyor.

## Dört Kaynak, Aynı Arayüz

**SS1.** — Aşağıdaki üç kaynağa (dosya, arabellek, dizgi) bilerek aynı dört karakterlik metin
(`"vida"`) yerleştirildi; amaç kaynakların **içeriğini** değil, aracın **davranışını**
karşılaştırmak. Dördüncü kaynak — ağ gövdesi — Ağ ve Veri Biçimleri konusunda aynı sonuçla
(kayboluyor) zaten koşturuldu ve burada tekrar kurulmuyor.

```go
// oku.go — uc ayri kaynagi ayni okuyucu arayuzunden geciren yardimci
package main

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"strings"
)

func oku(r io.Reader) (int, string) {
	b, _ := io.ReadAll(r)
	return len(b), fmt.Sprintf("%T", b)
}

type kaynak struct {
	ad string
	r  io.Reader
}

func main() {
	dizin, _ := os.MkdirTemp("", "ss1")
	defer os.RemoveAll(dizin)
	os.WriteFile(dizin+"/icerik.txt", []byte("vida"), 0o644)
	dosya, _ := os.Open(dizin + "/icerik.txt")
	defer dosya.Close()

	var arabellek bytes.Buffer
	arabellek.WriteString("vida")

	dizgi := strings.NewReader("vida")

	kaynaklar := []kaynak{
		{"dosya", dosya},
		{"arabellek", &arabellek},
		{"dizgi", dizgi},
	}
	for _, k := range kaynaklar {
		uzunluk, tip := oku(k.r)
		fmt.Printf("%-10s uzunluk=%d tip=%s\n", k.ad, uzunluk, tip)
	}
}
```

```
dosya      uzunluk=4 tip=[]uint8
arabellek  uzunluk=4 tip=[]uint8
dizgi      uzunluk=4 tip=[]uint8
```

Üç kaynak, birbirinden tamamen ayrı gerçekleştirmelere sahip: `*os.File` işletim sistemi
çağrılarıyla konuşuyor, `*bytes.Buffer` bellekte büyüyen bir dilimi yönetiyor, `*strings.Reader`
değiştirilemez bir dizginin üzerinde ilerliyor. `oku` işlevi bu farkı hiç görmüyor — parametresi
`io.Reader`, yani yalnızca `Read` yöntemini bekliyor. Üçünün de sonucu birebir aynı: dört
bayt, ve tipi her zaman `[]uint8` (Go'da `byte`, `uint8`'in bir takma adı, `%T` bunu kendi
adıyla gösteriyor). Kaynağın orijinal tipi — dosya mı, arabellek mi, dizgi mi — bu noktadan
sonra hiçbir yerde durmuyor; okuma işleminin ürettiği tek şey ham bir bayt dilimi.

Bu, konunun ölçü ekseninin C payındaki ilk somut örneği: sınırı geçen değer (`Read`'in
döndürdüğü veri) **kayboluyor** sınıfına giriyor, çünkü karşı tarafta onu geri kim gönderdiyse
göndersin sonuç aynı ham tipte kalıyor. Bir dosyadan okunan dört bayt ile bir dizgiden okunan
dört bayt, okuma işleminden sonra ayırt edilemez.

Örtük gerçekleştirimin gücü, yalnızca standart kitaplığın kendi tiplerinde durmuyor. `Read`
imzasını doğru yazan **herhangi bir tip**, `io.Reader` arayüzünü karşılıyor — dosya sistemine
ya da belleğe hiç dokunmayan, elle yazılmış bir tip bile.

```go
// elle-yazilan.go — Read yontemini kendi yazan bir tip de io.Reader arayuzunu karsilar
package main

import (
	"fmt"
	"io"
)

// TekrarOkuyucu, bir bayti istenen sayida tekrarlayan, elle yazilmis bir okuyucu.
type TekrarOkuyucu struct {
	Bayt  byte
	Kalan int
}

func (t *TekrarOkuyucu) Read(p []byte) (int, error) {
	if t.Kalan == 0 {
		return 0, io.EOF
	}
	n := 0
	for n < len(p) && t.Kalan > 0 {
		p[n] = t.Bayt
		t.Kalan--
		n++
	}
	return n, nil
}

func main() {
	var kaynak io.Reader = &TekrarOkuyucu{Bayt: 'x', Kalan: 5}
	veri, _ := io.ReadAll(kaynak)
	fmt.Printf("kendi yazdigimiz okuyucudan gelen: %q tip=%T\n", veri, veri)
}
```

```
kendi yazdigimiz okuyucudan gelen: "xxxxx" tip=[]uint8
```

`TekrarOkuyucu` ne bir dosya açıyor ne bir dizgiyi sarmalıyor; kendi `Kalan` sayacına göre
bayt üretiyor. Yine de `io.ReadAll` onu öteki üç kaynaktan hiç farklı görmüyor, çünkü tek
aradığı şey `Read` yönteminin doğru imzayla var olması. Bu, dört kaynağın ortak noktasının
aslında beşinci, altıncı, herhangi bir sayıda yeni kaynağa da açık olduğunu gösteriyor —
arayüz sabit kalıyor, gerçekleştirenlerin sayısı sınırsız.

## Yazma Tarafı: io.Writer

Yazma yönü de aynı simetriye sahip. `io.Writer`i gerçekleştiren bir hedefe veri yazıldığında,
hedefin dosya mı arabellek mi olduğu yazan tarafın umurunda değil; önemli olan yalnızca kaç
bayt yazıldığı.

**SS2.** — Aynı beş baytlık veri (`"somun"`) hem bir dosyaya hem bir arabelleğe yazılıyor;
ardından ikisi de geri okunup birbirine ve kaynağa eşit olup olmadığı sınanıyor.

```go
// yaz.go — ayni veri iki farkli yazici hedefine yaziliyor
package main

import (
	"bytes"
	"fmt"
	"io"
	"os"
)

func yaz(w io.Writer, veri []byte) (int, string) {
	n, _ := w.Write(veri)
	return n, fmt.Sprintf("%T", w)
}

func main() {
	dizin, _ := os.MkdirTemp("", "ss2")
	defer os.RemoveAll(dizin)
	dosya, _ := os.Create(dizin + "/cikti.txt")

	var arabellek bytes.Buffer

	n1, t1 := yaz(dosya, []byte("somun"))
	dosya.Close()
	n2, t2 := yaz(&arabellek, []byte("somun"))

	fmt.Printf("dosyaya yazilan    n=%d tip=%s\n", n1, t1)
	fmt.Printf("arabellege yazilan n=%d tip=%s\n", n2, t2)

	geriOkunan, _ := os.ReadFile(dizin + "/cikti.txt")
	fmt.Println("dosyadan geri okunan     :", string(geriOkunan))
	fmt.Println("arabellekten geri okunan :", arabellek.String())
	fmt.Println("ikisi ayni mi            :", string(geriOkunan) == arabellek.String())
}
```

```
dosyaya yazilan    n=5 tip=*os.File
arabellege yazilan n=5 tip=*bytes.Buffer
dosyadan geri okunan     : somun
arabellekten geri okunan : somun
ikisi ayni mi            : true
```

`yaz` işlevinin imzası `oku`ya simetrik: parametresi somut bir hedef değil, `io.Writer`
arayüzü. `*os.File` ve `*bytes.Buffer` birbirine hiç benzemeyen iki tip olsa da ikisi de
`Write` yöntemini taşıdığı için aynı işlevden geçebiliyor. Sonuç da simetrik: ikisine de
aynı beş bayt yazılıyor, ikisinden de geri okunan içerik birbirine eşit çıkıyor. Bu satırın
kendisi konunun sorusuna bir yanıt daha ekliyor — yazma yönünde de sınırı geçen veri, hedefin
kendi tipine bakılmaksızın aynı ham bayt dizisi olarak taşınıyor; hedefin `*os.File` ya da
`*bytes.Buffer` olması yazılan içeriği hiç etkilemiyor.

## Bileşim: io.Copy ve bufio Aynı Arayüzün Üstüne Kurulu

Arayüzlerin dar olmasının pratik sonucu, standart kitaplığın kaynak ile hedef arasında
**genel** araçlar yazabilmesi. `io.Copy(dst io.Writer, src io.Reader)` işlevi, parametrelerinin
somut tipini hiç bilmeden bir kaynaktan bir hedefe veri taşıyor — kaynak dosya, arabellek,
dizgi ya da başka herhangi bir `io.Reader` olabilir; hedef de aynı şekilde herhangi bir
`io.Writer`. Aynı fikir `bufio` paketinde de geçerli: `bufio.NewScanner` bir `io.Reader` alıp
onu satır satır (ya da kelime kelime) okuyan bir sarmalayıcıya çeviriyor, sardığı kaynağın
dosya mı dizgi mi olduğunu hiç sormadan.

```go
// birlestir.go — io.Copy ve bufio, somut tipi bilmeden herhangi bir okuyucu/yazici uzerinde calisir
package main

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"os"
	"strings"
)

func main() {
	dizin, _ := os.MkdirTemp("", "ss4")
	defer os.RemoveAll(dizin)
	os.WriteFile(dizin+"/kaynak.txt", []byte("civi ve vida"), 0o644)
	dosya, _ := os.Open(dizin + "/kaynak.txt")
	defer dosya.Close()

	var hedef bytes.Buffer
	n, _ := io.Copy(&hedef, dosya)
	fmt.Println("dosyadan arabellege tasinan bayt:", n)
	fmt.Println("arabellegin icerigi              :", hedef.String())

	satirOkuyucu := bufio.NewScanner(strings.NewReader("ilk satir\nikinci satir"))
	satirSayisi := 0
	for satirOkuyucu.Scan() {
		satirSayisi++
	}
	fmt.Println("bufio ile sayilan satir sayisi:", satirSayisi)
}
```

```
dosyadan arabellege tasinan bayt: 12
arabellegin icerigi              : civi ve vida
bufio ile sayilan satir sayisi: 2
```

`io.Copy` çağrısının imzasında dosyanın adı, arabelleğin türü, hiçbir somut tip geçmiyor —
yalnızca iki arayüz. Bu işlev standart kitaplığın içinde **bir kez** yazılmış ve dosyadan
ağa, ağdan arabelleğe, arabellekten başka bir dosyaya her kombinasyonda yeniden kullanılabilir
durumda; kaynak ile hedefin somut tipi kaç tane olursa olsun `io.Copy`nin kendisi hiç
değişmiyor. `bufio.NewScanner` de aynı ilkeyi izliyor: girdisi bir `io.Reader`, ve kendisi de
bir `io.Reader` sarmalıyor olmasına rağmen daha zengin bir arayüz (satır satır tarama)
sunuyor — küçük arayüzlerin üst üste **bileşebilmesi**, tek bir büyük "dosya okuyucu"
tipi yazmak yerine bu tür küçük, birbirine takılabilen parçalar üretmenin doğrudan sonucu.

## Sınırlayıcı Ölçüm: Kısmi Okuma ve Dosya Sonu

Buraya kadarki iki ölçüm `io.ReadAll` kullandı — kaynak tükenene kadar döngüyü kendi içinde
yürüten bir yardımcı işlev. Ama `Read` yönteminin kendisi bunu garanti etmiyor: tek bir
çağrı, istenen baytın **tamamını** döndürmek zorunda değil, ve verinin bitmesi ile hatanın
gelmesi **aynı çağrıda** olmak zorunda değil. Bu ayrım standart kitaplığın belgesinde açıkça
yazılı, ama koşturmadan görülmesi zor.

**SS3.** — Beş baytlık bir dizgi, kasıtlı olarak iki baytlık bir arabellekle okunuyor; her
adımda kaç bayt geldiği ve hatanın hangi adımda ortaya çıktığı ayrı ayrı basılıyor.

```go
// kismi.go — kucuk arabellekle okuma, veri ve dosya sonu ayri adimlarda geliyor
package main

import (
	"fmt"
	"strings"
)

func main() {
	kaynak := strings.NewReader("govde")
	arabellek := make([]byte, 2)
	toplam := 0
	adim := 0
	for {
		n, hata := kaynak.Read(arabellek)
		adim++
		fmt.Printf("adim=%d okunan=%d parca=%q hata=%v\n", adim, n, arabellek[:n], hata)
		toplam += n
		if hata != nil {
			break
		}
	}
	fmt.Println("toplam okunan bayt:", toplam)
}
```

```
adim=1 okunan=2 parca="go" hata=<nil>
adim=2 okunan=2 parca="vd" hata=<nil>
adim=3 okunan=1 parca="e" hata=<nil>
adim=4 okunan=0 parca="" hata=EOF
toplam okunan bayt: 5
```

Beş baytlık `"govde"` dizgisi, iki baytlık bir arabellekle okunduğunda dört ayrı `Read`
çağrısı gerektiriyor — üçüncü adımda tek bayt kalıyor, ve o adımda hata hâlâ `nil`. Dosya
sonu sinyali (`io.EOF`) yalnızca **dördüncü** çağrıda, hiç veri gelmeden geliyor. Bu, kaynağın
davranışının bir sözleşmesi: `Read` bir seferde `0`'dan büyük bir bayt sayısı **ve** `nil`
olmayan bir hata aynı anda döndürebilir, ya da veriyi ve hatayı ayrı çağrılara bölebilir.
Yalnızca `io.ReadAll` gibi bir yardımcıya güvenen kod bu ayrımı hiç görmüyor, çünkü yardımcı
döngüyü kendi içinde tamamlıyor. Ama `Read`i doğrudan çağıran kod, verinin bitmesi ile hatanın
gelmesini **aynı adımda** varsayarsa üçüncü adımdaki son baytı atlayabilir.

Bu, konunun ölçü ekseninin bu derse özgü sınırı: okuma arayüzü **kayıp** taşımıyor — beş
baytın hepsi doğru sırayla geldi — ama kısmi okuma ile dosya sonunu **ayırma işini** hiç
üstlenmiyor. O ayrım, `Read`i çağıran kodun sorumluluğunda kalıyor; standart kitaplığın
`io.ReadFull` ve `io.ReadAll` gibi yardımcıları bu sorumluluğu üstlenen, ama kaynağın kendisi
tarafından zorunlu kılınmayan araçlar.

`strings.NewReader` gibi bellek içi bir kaynak için bu ayrımı elle yönetmek gereksiz görünebilir
— zaten tüm veri bellekte hazır, `Read`in tek seferde her şeyi vermemesi bir tercih meselesi.
Ama arayüzün sözleşmesi tüm gerçekleştirenler için aynı; bir ağ soketinden ya da bir borudan
okuyan bir `io.Reader`de kısmi okuma bir tercih değil, kaynağın doğasından geliyor — karşı
taraf veriyi parça parça gönderiyorsa, `Read`in tek çağrıda her şeyi döndürmesi zaten mümkün
değil. `io.Reader`in sözleşmesi bu iki durumu (bellek içi kaynak ile parça parça gelen kaynak)
ayırmıyor; ikisi de aynı imzayı taşıyor, ve kısmi okumayı doğru yöneten kod ikisinde de
çalışıyor.

## Özet

- `io.Reader` ve `io.Writer`, dilin en dar arayüzlerinden ikisi; sırasıyla tek bir `Read` ve
  tek bir `Write` yöntemi istiyorlar ve bu yüzden dosya, arabellek ve dizgi gibi birbirine
  hiç benzemeyen tipler aynı sözleşmeyi bağımsız olarak gerçekleştirebiliyor.
- Dosyadan, arabellekten ve dizgiden okunan aynı içerik, okuma işleminden sonra ayırt
  edilemez hâle geliyor: üçü de aynı uzunlukta, aynı `[]uint8` tipinde bir sonuç veriyor —
  sınırı geçen veri tip bilgisini taşımıyor.
- Aynı asimetri yazma yönünde de geçerli: `*os.File` ve `*bytes.Buffer` aynı `yaz` işlevinden
  geçiyor ve ikisine yazılan içerik birbirine birebir eşit çıkıyor.
- `io.Copy` ve `bufio.NewScanner` gibi araçlar somut tipi hiç bilmeden çalışıyor; küçük
  arayüzler bu yüzden dosya, arabellek ve dizgi arasında serbestçe **bileşebiliyor**.
- Kısmi okuma ile dosya sonu ayrı olaylar: küçük bir arabellekle okuma, veriyi birkaç adıma
  bölebiliyor ve dosya sonu sinyali veriden bağımsız, kendi adımında geliyor.
- Bu ayrımı yönetmek çağıranın işi; `io.ReadAll` gibi yardımcılar bu sorumluluğu üstleniyor,
  ama arayüzün kendisi bunu zorunlu kılmıyor.

## Sonraki Adım

Bu derste ölçülen üç kaynağın hepsi, çalışırken erişilebilir bir yerde duruyordu: bir dosya
sistemi yolunda, ya da programın kendi belleğinde. Ama bir programın ihtiyaç duyduğu veri
her zaman koşum sırasında bulunabilir olmayabilir — bir yapılandırma dosyası, bir HTML
şablonu, bir statik varlık, programın **kendi ikili dosyasına** taşınabilir. Sıradaki ders bu
bağı derleme zamanına taşıyan düzeneğe bakıyor: bir kaynak dosya, koşum sırasında hiç
aranmadan, doğrudan derlenmiş programın içinde nasıl duruyor?
