İçeriğe geç
academia.sh

Ders 07 / 12

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.

İçindekiler

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.

// 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.

// 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.Writeri 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.

// 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ı okuya 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.

// 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.Copynin 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.

// 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 Readi 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, Readi ç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, Readin 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.Readerde kısmi okuma bir tercih değil, kaynağın doğasından geliyor — karşı taraf veriyi parça parça gönderiyorsa, Readin tek çağrıda her şeyi döndürmesi zaten mümkün değil. io.Readerin 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?

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat