Ders 08 / 12
Gömülü Kaynaklar
Bir kaynak dosyanın go:embed yönergesiyle üç ayrı Go tipine (dizgi, bayt dilimi, dosya sistemi) aynı içerikle bağlandığı ve bu içeriğin diskte duran dosyayla birebir eşleştiği ölçülür. Sınırlayıcı ölçüm, bir kez derlenmiş ikilinin kaynak dosya diskten silindikten sonra bile aynı içeriği ürettiğini koşturarak gösteriyor: bağ derleme zamanında kuruluyor, koşum zamanında dosyaya hiç ihtiyaç kalmıyor.
İçindekiler
Önceki ders üç kaynağı (dosya, arabellek, dizgi) aynı okuyucu arayüzünden geçirdi ve hepsinin aynı sonucu — ham bir bayt dilimi — verdiğini gösterdi. O üç kaynağın ortak bir özelliği vardı: programın kendisi hâlâ çalışırken erişilebilir kalıyorlardı — dosya diskte duruyordu, arabellek bellekte. Bu ders farklı bir soru soruyor: bir programın ihtiyaç duyduğu bir veri, koşum sırasında hiç erişilebilir olmasa bile programın elinde bulunabilir mi? Yanıt evet, ve yöntem bir okuma işlemi değil, bir bağlama işlemi: kaynak dosyanın içeriğini, derleme sırasında doğrudan ikili dosyanın içine yerleştirmek.
Terim notu: bu derste geçen “gömme” sözcüğü, Bileşik Tipler konusunda ölçülen yapı gömmeyle (bir tipin başka bir tipin alanları arasına adsız olarak yerleştirilmesi) aynı kelimeyi kullanıyor ama ayrı bir düzenek. Yapı gömme kaynak kodun içinde, iki Go tipi arasında kurulan bir ilişkiydi; bu derste ölçülen gömülü kaynak ise bir dosyanın içeriğini kaynak kodun dışından, derleyicinin kendisi aracılığıyla bir değişkene taşıyor. İkisinin ortak yanı yalnızca isim; biri iki Go tipini birbirine, öteki bir dosyayı bir Go değerine bağlıyor.
Belirlenimcilik notu: bu derste hiçbir dosya yolu basılmıyor; ölçülen tek şey içeriğin uzunluğu, tipi ve iki temsilin birbirine eşit olup olmadığı.
Bu düzeneğin tipik kullanım yerleri, koşum sırasında değişmesi beklenmeyen ama programın kendisiyle birlikte taşınması gereken veriler: bir yapılandırma dosyasının varsayılan değerleri, bir komut satırı aracının yardım metni, bir web sunucusunun statik varlıkları. Hepsinin ortak noktası, içeriğin programın bir parçası olması — ayrı bir kurulum adımıyla yanına kopyalanması gereken bir veri değil.
Kaynak Dosya
Gömme yönergesi (//go:embed) bir var bildiriminin hemen üstüne yazılan özel bir yorum;
derleyici bunu okuyup, belirtilen dosyanın içeriğini o değişkene derleme sırasında yerleştiriyor.
Yönergenin çalışabilmesi için önce gömülecek dosyanın var olması gerekiyor — aşağıdaki kabuk
bloğu, dersin geri kalanında gömülecek tarif.txt dosyasını oluşturuyor ve içeriğini
doğrulamak için geri okuyor.
# tarif-dosyasi.sh — go:embed ile gomulecek kaynak dosyayi olusturur cat > tarif.txt <<'EOF' kaynak paketin icine kod ile birlikte tasinir EOF cat tarif.txt
kaynak paketin icine kod ile birlikte tasinir
Aynı Dosya, Üç Tip
SS4. — Aynı tarif.txt dosyası üç ayrı değişkene gömülüyor: bir dizgiye, bir bayt
dilimine, bir de dosya sistemi türüne (embed.FS). Amaç, yönergenin hedef tipe göre farklı
davrandığını ve üçünün de aynı içeriği taşıdığını göstermek.
// gomulu.go — ayni dosya string, []byte ve embed.FS olarak gomuluyor, icerikleri karsilastiriliyor
package main
import (
"embed"
"fmt"
"os"
)
//go:embed tarif.txt
var tarifMetni string
//go:embed tarif.txt
var tarifBaytlari []byte
//go:embed tarif.txt
var tarifSistemi embed.FS
func main() {
sistemdenOkunan, _ := tarifSistemi.ReadFile("tarif.txt")
diskten, hataDosya := os.ReadFile("tarif.txt")
fmt.Printf("tarifMetni tipi=%T uzunluk=%d\n", tarifMetni, len(tarifMetni))
fmt.Printf("tarifBaytlari tipi=%T uzunluk=%d\n", tarifBaytlari, len(tarifBaytlari))
fmt.Println("metin == bayt donusumu:", tarifMetni == string(tarifBaytlari))
fmt.Println("metin == embed.FS okumasi:", tarifMetni == string(sistemdenOkunan))
fmt.Println("metin == dosya sisteminden okunan:", tarifMetni == string(diskten), hataDosya == nil)
}
tarifMetni tipi=string uzunluk=46 tarifBaytlari tipi=[]uint8 uzunluk=46 metin == bayt donusumu: true metin == embed.FS okumasi: true metin == dosya sisteminden okunan: true true
Üç //go:embed yönergesi birbirinden bağımsız; her biri aynı tarif.txtyi okuyor ama hedef
değişkenin tipine göre farklı bir temsil üretiyor. tarifMetni bir dizgi, tarifBaytlari bir
bayt dilimi, tarifSistemi ise Open ve ReadFile gibi yöntemleri olan küçük bir dosya
sistemi soyutlaması. Üçü de aynı kaynaktan geldiği için içerikleri birebir eşleşiyor — bayt
dönüşümüyle karşılaştırıldığında da, embed.FS üzerinden okunduğunda da, dosyanın kendisinden
os.ReadFile ile canlı okunduğunda da aynı 46 bayt geliyor. Bu son karşılaştırma önceki
dersle bir köprü kuruyor: dosya hâlâ diskteyken, gömülen içerik ile diskteki içerik ayırt
edilemiyor — sanki dosya iki kez okunmuş gibi, ama biri derleme sırasında, biri koşum
sırasında gerçekleşiyor.
Aynı dosyanın üç ayrı temsile bölünmesi rastgele değil; her temsil farklı bir kullanım
biçimine uyuyor. Metni doğrudan bir dizgi olarak kullanacak kod (bir sabit metin, bir şablon
kaynağı) tarifMetniyi tercih ediyor. İçeriği başka bir ikili biçime (bir sıkıştırma
algoritmasına, bir ağ gövdesine) aktaracak kod tarifBaytlariyı tercih ediyor, çünkü []byte
o tür arayüzlerin beklediği tip. Birden fazla dosyayı ya da bir dizin yapısını taşımak
gereken kod ise embed.FSe ihtiyaç duyuyor — bu ihtiyaç bir sonraki bölümde ölçülüyor.
Sınırlayıcı Ölçüm: Kaynak Dosya Koşumda Hiç Aranmaz
Buraya kadarki ölçüm tarif.txt diskteyken çalıştı, bu yüzden “gömülü içerik dosyayla aynı”
iddiası ile “program dosyaya bağımlı” iddiası birbirinden ayrılamıyordu. Aşağıdaki adım bu
ikisini ayırıyor: aynı kaynak önce derlenip bağımsız bir ikili hâline getiriliyor, sonra
kaynak dosya diskten siliniyor, sonra da yalnızca o ikili çalıştırılıyor.
SS5. — Derleme adımı go build ile ayrı bir ikili üretiyor; go run kullanılmıyor,
çünkü go run her çağrıda yeniden derliyor ve kaynak dosya o anda hâlâ diskte olurdu. Burada
amaç, derleme ile çalıştırmayı birbirinden ayırmak.
# derle-ve-sil.sh — once derlenir, sonra kaynak dosya silinir, sonra yalniz ikili calistirilir go build -o govde_programi . rm tarif.txt ./govde_programi
tarifMetni tipi=string uzunluk=46 tarifBaytlari tipi=[]uint8 uzunluk=46 metin == bayt donusumu: true metin == embed.FS okumasi: true metin == dosya sisteminden okunan: false false
Çıktının ilk dört satırı bir önceki koşumla birebir aynı: tarifMetni, tarifBaytlari ve
tarifSistemi hâlâ 46 baytlık içeriği doğru biçimde veriyor — tarif.txt artık diskte
olmamasına rağmen. Değişen tek satır sonuncusu: os.ReadFile("tarif.txt") artık dosyayı
bulamıyor, bu yüzden hem karşılaştırma hem hata denetimi falsea dönüyor. Bu fark, gömme
yönergesi ile canlı dosya okuması arasındaki gerçek ayrımı gösteriyor — ilk üç değişken
derleme sırasında içeriği kendi içine aldı ve o andan sonra kaynağı bir daha hiç
okumadı; dördüncü satır ise her çalıştırmada dosyayı yeniden arıyor.
Bu, konunun ölçü ekseninde şöyle okunuyor: kaynak dosyadan gömülen değişkene giden bağ, programın koşum ânında kurulmuyor — derleme ânında zaten kurulmuş durumda. Bir dosyayı okumaktan (önceki dersin konusu) farkı da tam burada: okuma her çağrıda kaynağa yeniden gidiyor, gömme ise kaynağa yalnızca bir kez, derleme sırasında gidiyor ve sonucu ikilinin içine taşıyor. Bu, bir kısıt olduğu kadar bir güvence de: dağıtılan ikili, yanına ayrı bir veri dosyası taşımak zorunda kalmıyor, ve o dosyanın koşum ânında yanlışlıkla değiştirilmesi ya da silinmesi programın davranışını hiç etkilemiyor.
Bunun bedeli de var, ve bedel tam olarak güvencenin ters yüzü: içerik güncellendiğinde, ikili de yeniden derlenmek zorunda. Önceki dersteki dosya okuma, koşum sırasında değişen bir kaynağı hemen görebiliyordu — dosyayı her seferinde yeniden okumak yeterliydi. Gömülü bir kaynak için bu yol kapalı; içeriği güncellemenin tek yolu kaynak dosyayı değiştirip programı yeniden derlemek. Hangi yaklaşımın doğru olduğu içeriğin kendi doğasına bağlı: sık değişen bir veri için okuma, hiç değişmemesi gereken bir veri için gömme daha uygun bir seçim.
Birden Çok Dosya: Örüntü ile Gömme
Yönergenin hedefi tek bir dosya adı değil, bir örüntü de olabiliyor. string ve []byte
yalnızca tek bir dosyanın içeriğini taşıyabildiğinden, birden çok dosyayı gömmenin tek yolu
embed.FS; sonuç, ikilinin içine gömülmüş küçük bir dizin ağacı oluyor.
SS6. — İki dosyalık küçük bir dizin (kaynaklar/) örüntüyle (kaynaklar/*.txt) tek bir
embed.FS değişkenine gömülüyor; ölçüm, gömülen dosya sayısını ve her birinin uzunluğunu
okuyor.
# kaynaklar-dizini.sh — orunu ile gomulecek iki dosyalik bir dizin olusturur mkdir kaynaklar printf 'birinci dosyanin icerigi\n' > kaynaklar/birinci.txt printf 'ikinci dosyanin icerigi, biraz daha uzun\n' > kaynaklar/ikinci.txt ls kaynaklar
birinci.txt ikinci.txt
// dizin.go — orunu ile gomulen bir dizinin icerigi listeleniyor ve okunuyor
package main
import (
"embed"
"fmt"
"sort"
)
//go:embed kaynaklar/*.txt
var kaynakDizini embed.FS
func main() {
girdiler, _ := kaynakDizini.ReadDir("kaynaklar")
adlar := make([]string, 0, len(girdiler))
for _, g := range girdiler {
adlar = append(adlar, g.Name())
}
sort.Strings(adlar)
fmt.Println("gomulu dosya sayisi:", len(adlar))
for _, ad := range adlar {
icerik, _ := kaynakDizini.ReadFile("kaynaklar/" + ad)
fmt.Printf("%s uzunluk=%d\n", ad, len(icerik))
}
}
gomulu dosya sayisi: 2 birinci.txt uzunluk=25 ikinci.txt uzunluk=41
kaynakDizini.ReadDir("kaynaklar") çağrısı, sanki gerçek bir dizini listeliyormuş gibi
davranıyor — dosya adlarını, dosya olup olmadıklarını, boyutlarını veriyor — ama listelediği
şey diskte değil, ikilinin içinde. Bu, embed.FS tipinin standart kitaplığın dosya sistemi
soyutlamasını (io/fs paketindeki fs.FS arayüzü) karşıladığı için mümkün; bir dosya sistemi
gibi davranan her araç, kaynağın gerçek disk mi yoksa ikilinin içine gömülmüş bir kopya mı
olduğunu bilmeden çalışabiliyor. Bu arayüz ayrımı Ağ ve Veri Biçimleri konusunda kurulan
sunucu tarafı ayrıntılarla burada tekrar kurulmuyor; ölçülen tek şey, gömülü dizinin gerçek
bir dizin gibi listelenip okunabilmesi.
Bu, önceki dersteki io.Reader/io.Writer bileşiminin bir başka örneği: fs.FS da tek
birkaç yöntemlik dar bir arayüz, ve onu bekleyen bir işlev os.DirFS("bir/gercek/yol")
ile de, embed.FS ile de, testler için elle yazılmış bir sahte dosya sistemiyle de
çalışabiliyor. kaynakDizini değişkeninin somut tipi embed.FS, ama onu fs.FS bekleyen
bir işleve verdiğinizde o işlev içeriğin gerçek diskte mi yoksa ikilinin içinde mi
durduğunu hiç bilmiyor — tıpkı bir önceki dersteki oku işlevinin kaynağın dosya mı
arabellek mi olduğunu bilmemesi gibi.
Derleme Zamanı Sınırları
Gömme yönergesinin çalışabilmesi için hedef dosyanın derleme sırasında, paketin kendi
dizininde bulunması gerekiyor. Bu iki kısıtı ayrı ayrı sınamak, önceki derste kurulan
derlenirMi biçimindeki bir yardımcıyla mümkün: kaynağı geçici bir dizine yazıp derlemeyi
deneyen, sonucu derlendi/derlenmedi diye sınıflayan bir işlev.
SS7. — Üç kaynak deneniyor: dosyası gerçekten var olan bir gömme, dosyası hiç var olmayan
bir gömme, ve paketin kendi dizininin dışına (../) çıkmaya çalışan bir gömme. Derleyicinin
hata metni burada da basılmıyor; yalnızca ret sınıfı okunuyor.
// derlenir-mi.go — gomme yonergesinin derleme zamani kisitlari, derlenirMi ile sinaniyor
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
)
func derlenirMi(kaynak string, digerDosyalar map[string]string) bool {
dizin, err := os.MkdirTemp("", "sinama")
if err != nil {
return false
}
defer os.RemoveAll(dizin)
os.WriteFile(filepath.Join(dizin, "go.mod"), []byte("module sinama\n\ngo 1.24\n"), 0o644)
os.WriteFile(filepath.Join(dizin, "kod.go"), []byte(kaynak), 0o644)
for ad, icerik := range digerDosyalar {
os.MkdirAll(filepath.Dir(filepath.Join(dizin, ad)), 0o755)
os.WriteFile(filepath.Join(dizin, ad), []byte(icerik), 0o644)
}
cmd := exec.Command("go", "build", "./...")
cmd.Dir = dizin
cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
return cmd.Run() == nil
}
func kaynakUret(dosyaAdi string) string {
return fmt.Sprintf(`package main
import (
_ "embed"
)
//go:embed %s
var veri string
func main() {}
`, dosyaAdi)
}
func main() {
varOlan := derlenirMi(kaynakUret("var.txt"), map[string]string{"var.txt": "icerik"})
olmayan := derlenirMi(kaynakUret("yok.txt"), nil)
paketDisi := derlenirMi(kaynakUret("../disari.txt"), nil)
fmt.Println("var olan dosya gomuluyor :", varOlan)
fmt.Println("olmayan dosya gomuluyor :", olmayan)
fmt.Println("paket disi dosya gomuluyor :", paketDisi)
}
var olan dosya gomuluyor : true olmayan dosya gomuluyor : false paket disi dosya gomuluyor : false
Üç denemenin ilki derleniyor, öteki ikisi derlenmiyor — ama iki ret de aynı nedenden değil.
Olmayan dosya, yönergenin en temel koşulunu karşılamıyor: gömülecek içerik hiç yok. Paket
dışı dosya ise içerik var olsa bile reddediliyor, çünkü gömme yönergesi yalnızca kendi
paketinin dizini ve altındaki dosyalara erişebiliyor; bir üst dizine çıkan bir yol, dosya
gerçekten orada dursa bile derleme zamanında kabul edilmiyor. Bu, dosya okuma ile gömme
arasındaki bir başka fark: os.ReadFile çağıran kodun erişebildiği her yola gidebilirken,
gömme yönergesi paketin kendi sınırlarının dışına hiç çıkamıyor.
Bu sınırın kendisi keyfi değil: bir paket, kendi dizininin dışına referans veren bir gömme yönergesi taşırsa, o paketi başka bir yerden (başka bir modülden, başka bir makineden) alıp derleyen kişi kaynağın nerede durduğunu bilemez — üst dizin, paketle birlikte taşınan bir şey değil. Paketin kendi dizinine hapsedilmiş bir gömme, paketin kendi kendine yeterli olmasını garanti ediyor: paketi kopyalayan biri, gömülü kaynakların tamamını da otomatik olarak kopyalamış oluyor.
Bir başka kısıt burada koşturulmuyor ama belirtmeye değer: adı nokta ya da alt çizgiyle
başlayan dosyalar (.gizli, _yardimci) yönergenin varsayılan örüntüsüne dahil olmuyor;
onları da gömmek isteyen bir yönergenin başına all: öneki eklenmesi gerekiyor. Bu da aynı
ailenin bir parçası: gömme yönergesi, hangi dosyaların “gerçekten kaynağın parçası” sayılacağı
konusunda kendi varsayılan kurallarını taşıyor.
Özet
//go:embedyönergesi bir dosyanın içeriğini derleme sırasında bir Go değişkenine yerleştiriyor; hedef tipe göre (string,[]byte,embed.FS) farklı bir temsil üretiyor. Sınırdan geçen şey yine bayttır; tipi hedef tipin kendisi yeniden kuruyor.- Aynı dosyaya bağlanan üç değişken birbirine ve diskteki dosyanın canlı okumasına birebir eşit içerik veriyor — dosya hâlâ diskteyken.
- Sınırlayıcı ölçüm: derlenmiş bir ikili, kaynak dosya silindikten sonra da gömülü içeriği
doğru üretiyor — bağ derleme zamanında kuruluyor, koşum zamanında kaynağa bir daha hiç
ihtiyaç duyulmuyor, ve yalnızca canlı bir disk okuması (
os.ReadFile) başarısız oluyor. - Bir örüntü, birden çok dosyayı tek bir
embed.FSdeğişkenine gömebiliyor; sonuçfs.FSarayüzünü karşılayan, gerçek bir dizin gibi listelenip okunabilen bir yapı oluyor. - Gömme yönergesi derleme zamanında iki kısıtla sınırlı: hedef dosya gerçekten var olmalı ve paketin kendi dizininin dışına çıkamaz; ikisi de derlemeyi durduruyor.
- “Gömme” burada Bileşik Tipler konusundaki yapı gömmeden ayrı bir düzenek: biri kod içi bir ilişki, öteki derleyicinin dosyadan değişkene kurduğu bir bağ.
Sonraki Adım
Bu derste ölçülen bağ zamanla ilgiliydi ama zamanın kendisi hiç ölçülmedi — yalnızca “önce derlendi, sonra silindi, sonra çalıştırıldı” sırası önemliydi. Bu sıralama, sınırı geçen bir değerin ne taşıdığı sorusuna konunun ilk iki dersinin verdiği yanıtı tamamlıyor: dosya, arabellek ve dizgi tip bilgisini hiç taşımıyordu; gömülü kaynak ise içeriği taşıyor ama bunu sınırı koşum sırasında değil, derleme sırasında geçerek yapıyor — sınırın kendisi zamanla kayıyor.
Sıradaki ders zamanın kendisini, bir değerin metne dönüp geri geldiğinde neyin taşındığını ve neyin kaybolduğunu soruyor: bir an, bir saat dilimine bağlandığında ne kazanıyor, metne yazılıp geri okunduğunda bu kazanımdan ne kalıyor? Burada gömme yönergesinin verdiği “içerik sabit” güvencesinin bir karşılığı yok — zaman, tanımı gereği ölçüldüğü ânın kendisiyle sınırlı, ve o ânı bir dizgiye yazıp geri okumak, gömülü bir dosyayı okumaktan farklı bir kayıp türü doğuruyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.