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.Readerveio.Writer, dilin en dar arayüzlerinden ikisi; sırasıyla tek birReadve tek birWriteyö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ı
[]uint8tipinde bir sonuç veriyor — sınırı geçen veri tip bilgisini taşımıyor. - Aynı asimetri yazma yönünde de geçerli:
*os.Fileve*bytes.Bufferaynıyazişlevinden geçiyor ve ikisine yazılan içerik birbirine birebir eşit çıkıyor. io.Copyvebufio.NewScannergibi 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.ReadAllgibi 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.