Ders 04 / 12
Şablonlar
Aynı şablon metni iki farklı Go tipinden (bir tamsayı, bir ondalıklı sayı) geçirildiğinde bayt bayt aynı çıktıyı üretir; kaynaktaki tip bilgisi çıkışta tümüyle kayboluyor. Bir alan adı çıktıda yalnızca şablonun kendi metninde yazılmışsa görünür — tip sisteminden gelmez. Metin ve HTML şablonları aynı veriyi kaçırma bakımından farklı bayt dizilerine çevirir. Sınırlayıcı ölçüm: şablon alan adlarını çalışma zamanında arar, olmayan bir alan derlemede değil çalıştırmada belli olur, ve hata çıktının ortasında doğar.
İçindekiler
Önceki dersin son ölçümü bir yapı etiketinin ne işe yaradığını gösterdi: yapı etiketi alan adını sınırın karşı tarafına taşıyan bir tarifti, ve o tarif olmadan geri kurma da yoktu. Bu ders aynı soruyu tersinden soruyor. JSON dizileştirme bir yapıyı bayta çevirip geri getirdi — gidiş ile dönüş arasında bir tarif vardı ve tarif, dört alandan beşinin tipini yeniden kurdu. Şablonlar bu geri dönüşü hiç vaat etmiyor: bir şablonun ürettiği şey yalnızca metin, ve o metnin bir daha hiçbir tarifle okunmayacağı varsayılıyor. Kursun sorusu aynı kalıyor — bu değer sınırı geçerken tip bilgisine ne oluyor? — ama bu kez sınırın karşı tarafı bir program değil, bir ekran ya da bir dosya.
Go’nun standart kitaplığı iki şablon paketi taşır: text/template ve html/template.
İkisi de aynı şablon dilini kullanır — {{.Alan}} bir alanı okur, {{if}} ve {{range}}
dallanma ve yineleme sağlar — ve ikisi de bir Go değerini alıp bir io.Writer’a metin
yazar. Aralarındaki tek fark, birazdan ölçülecek olan kaçırma davranışı. Her ikisi de
paket adıyla, kod bloğunda çağrılıyor; düzyazıda yalnızca “metin şablonu” ve “HTML şablonu”
olarak anılıyorlar.
Execute yönteminin ikinci parametresi bir io.Writerdır — bu ders arabellek (bytes.Buffer)
kullanıyor, ama aynı çağrı bir dosyaya, standart çıktıya ya da bir ağ bağlantısına da
yazabilirdi; Sistem ve Veri Erişimi konusunun ilk dersi bu üç hedefin aynı arayüzü
paylaştığını ayrıca ölçecek.
Şablon paketinin kendisi hedefin ne olduğunu bilmiyor ve bilmesi de gerekmiyor — yalnızca
bir dizi bayt üretip yazıyor. Bu ders tam olarak bu üretilen baytların içinde neyin durup
neyin durmadığını ölçüyor: kaynak veri sınırı geçerken, tip sistemi Go’nun kendi tarafında
kalıyor mu, yoksa şablonun ürettiği metne kadar mı taşınıyor?
Kursun ortak ölçümü on üç sınır geçişini üç sınıfa ayırmıştı: birinde tip korunuyor, beşinde yeniden kuruluyor, yedisinde kayboluyor (03. derste kurulan tam dağılım). Şablon çıktısı bu yedi kayıp geçişten biri — ama kayboluşun biçimi JSON’daki kayıptan farklı, çünkü JSON bir geri dönüş biçimidir (bayta çevrilir, sonra geri okunabilir) ve şablon hiç geri dönmeyi vaat etmez; ürettiği metnin bir daha ayrıştırılacağı bile varsayılmaz.
Şablonun Tek Çıktı Tipi: Metin
AG19. Bir şablonun Execute çağrısının ürettiği şey, kaynak verinin tipi ne olursa
olsun, her zaman bir bayt dizisidir — ve o bayt dizisi bir dizgiye dönüştürüldüğünde
kaynaktaki hiçbir tip bilgisini taşımaz. Bunu göstermenin en doğrudan yolu, aynı şablon
metnini iki farklı Go tipinden geçirip çıktıları karşılaştırmaktır.
// sablonlar.go — sablonun urettigi metnin tipini, alan izini, kacirma farkini ve calisma zamani aramasini olcer
package main
import (
"bytes"
"fmt"
"html/template"
"strings"
textTemplate "text/template"
)
type Urun struct {
Ad string
Adet int
Yorum string
}
type UrunTamsayi struct{ Adet int }
type UrunOndalikli struct{ Adet float64 }
func main() {
urun := Urun{Ad: "vida", Adet: 12, Yorum: "iyi <b>ama</b> pahali & yavas"}
sabitSablon := textTemplate.Must(textTemplate.New("t").Parse("{{.Adet}}"))
var bTamsayidan, bOndaliklidan bytes.Buffer
sabitSablon.Execute(&bTamsayidan, UrunTamsayi{Adet: 12})
sabitSablon.Execute(&bOndaliklidan, UrunOndalikli{Adet: 12})
fmt.Println("-- ayni sablon, iki farkli kaynak tipi --")
fmt.Println("tamsayidan gelen cikti:", bTamsayidan.String())
fmt.Println("ondalikli sayidan gelen cikti:", bOndaliklidan.String())
fmt.Println("iki cikti bayt bayt ayni mi:", bTamsayidan.String() == bOndaliklidan.String())
dar := textTemplate.Must(textTemplate.New("dar").Parse("{{.Ad}} x{{.Adet}}"))
var bDar bytes.Buffer
dar.Execute(&bDar, urun)
fmt.Println()
fmt.Println("-- alan adi ciktida yalniz sablon metninde yazilmissa gorunur --")
fmt.Println("dar sablon ciktisi:", bDar.String())
fmt.Println("ciktida \"Ad\" adi geciyor mu:", strings.Contains(bDar.String(), "Ad"))
fmt.Println("ciktida \"Adet\" adi geciyor mu:", strings.Contains(bDar.String(), "Adet"))
genis := textTemplate.Must(textTemplate.New("genis").Parse("Ad: {{.Ad}}, Adet: {{.Adet}}"))
var bGenis bytes.Buffer
genis.Execute(&bGenis, urun)
fmt.Println("genis sablon ciktisi:", bGenis.String())
fmt.Println("ciktida \"Ad\" adi geciyor mu:", strings.Contains(bGenis.String(), "Ad"))
metinSablon := textTemplate.Must(textTemplate.New("m").Parse("{{.Yorum}}"))
var bMetin bytes.Buffer
metinSablon.Execute(&bMetin, urun)
htmlSablon := template.Must(template.New("h").Parse("{{.Yorum}}"))
var bHtml bytes.Buffer
htmlSablon.Execute(&bHtml, urun)
fmt.Println()
fmt.Println("-- ayni veri, metin ve html sablonundan gecince farkli bayt uretiyor --")
fmt.Printf("metin sablonu: %s\n", bMetin.String())
fmt.Printf("html sablonu: %s\n", bHtml.String())
fmt.Println("iki cikti ayni mi:", bMetin.String() == bHtml.String())
hatali := textTemplate.Must(textTemplate.New("hatali").Parse("on: {{.Ad}}, sonra: {{.OlmayanAlan}}"))
var bHatali bytes.Buffer
hata := hatali.Execute(&bHatali, urun)
fmt.Println()
fmt.Println("-- olmayan alan calisma zamaninda hata veriyor, once yazilan kismi yazidirilmis oluyor --")
fmt.Println("hata olustu mu:", hata != nil)
fmt.Printf("hatadan once yaziya gecen kisim: %q\n", bHatali.String())
}
-- ayni sablon, iki farkli kaynak tipi -- tamsayidan gelen cikti: 12 ondalikli sayidan gelen cikti: 12 iki cikti bayt bayt ayni mi: true -- alan adi ciktida yalniz sablon metninde yazilmissa gorunur -- dar sablon ciktisi: vida x12 ciktida "Ad" adi geciyor mu: false ciktida "Adet" adi geciyor mu: false genis sablon ciktisi: Ad: vida, Adet: 12 ciktida "Ad" adi geciyor mu: true -- ayni veri, metin ve html sablonundan gecince farkli bayt uretiyor -- metin sablonu: iyi <b>ama</b> pahali & yavas html sablonu: iyi <b>ama</b> pahali & yavas iki cikti ayni mi: false -- olmayan alan calisma zamaninda hata veriyor, once yazilan kismi yazidirilmis oluyor -- hata olustu mu: true hatadan once yaziya gecen kisim: "on: vida, sonra: "
AG20. UrunTamsayi.Adet bir int, UrunOndalikli.Adet bir float64 — ikisi de
aynı {{.Adet}} şablonundan geçtiğinde çıktı ikisinde de 12 dizgisidir, bayt bayt eşit.
Şablon çalışırken kaynağın hangi tipten geldiğini biliyordu (Execute bir arayüz değeri
alır ve alanına yansımayla erişir), ama ürettiği metnin içinde bu bilgiden hiçbir iz
kalmıyor. Çıktıyı okuyan bir program 12 dizgisinden geriye doğru “bu bir tamsayı mıydı,
ondalıklı mıydı” sorusunu yanıtlayamaz.
Bu, önceki dersteki tarifsiz sözlük gözlemiyle aynı yüzeye iniyor ama farklı bir yoldan.
Bir JSON değeri tarifsiz bir kaba çözüldüğünde sayı hep float64 oluyordu — kayıp vardı,
ama kayıp belirli bir yöndeydi: her sayı aynı tek tipe düşüyordu, ve bu düşüş
encoding/json‘ın kendi kuralıydı. Şablon çıktısında böyle bir kural bile yok, çünkü
ortada hedef bir tip hiç yok — int de float64 de aynı stringe düşüyor, ama bu
düşüşün “tek bir hedef tipe toplanma” gibi bir yapısı yok; ikisi de sıradan metin
oldu ve metnin kendisi hiçbir tipe ait değil. JSON’un kaybı bir dönüştürücünün
kararıydı; şablonun kaybı, ortada dönüştürülecek bir hedef tipin hiç bulunmamasının
sonucu.
AG21. Dar şablon ({{.Ad}} x{{.Adet}}) ürettiği vida x12 metninde ne "Ad" ne
"Adet" sözcüğü geçiyor — alan adları da tip bilgisi kadar kayboluyor, çünkü şablonun
yazdığı şey alanın değeri, alanın adı değil. Geniş şablon (Ad: {{.Ad}}, Adet: {{.Adet}}) çıktısında "Ad" görünüyor, ama bunun nedeni Go’nun yapı alanının adını
otomatik olarak taşıması değil — şablonu yazan kişi "Ad: " dizgisini kendi eliyle,
düz metin olarak şablonun içine yazdı. Alan adının çıktıda görünmesi, tip sisteminden
gelen bir garanti değil, şablon metnini yazan kişinin bir kararı.
Şablonun çıktısının saf metin olması bir sınırlama olduğu kadar bir esneklik de taşıyor:
hedefin bir HTML belgesi olması zorunlu değil. text/template bir yapılandırma dosyası,
bir e-posta gövdesi, bir günlük satırı, hatta başka bir dilde yazılmış bir kaynak dosyası
üretmek için de kullanılabilir — üretilen şey ne olursa olsun, şablon paketinin gözünde
fark etmiyor, çünkü paket yalnızca bir metni bir başka metne, alan değerlerini yerine
koyarak çeviriyor. Kursun bir sonraki dersi bu son kullanımı, kaynak kod üretimini,
tam olarak bu noktadan devralıyor.
UrunTamsayi ve UrunOndalikli bilinçli olarak tek alanlı, birbirine paralel iki tip
olarak tanımlandı: aralarındaki tek fark Adet alanının tipi, başka hiçbir şey değişmiyor.
Bu, ölçümün gösterdiği şeyin yalnızca “tip bilgisi kayboluyor” olduğunu, başka bir yapısal
farktan (alan sayısı, alan sırası, alan adı) kaynaklanmadığını netleştiriyor — iki tip aynı
şablon eylemine ({{.Adet}}) aynı adla yanıt veriyor ve aradaki tek değişken tip olduğu
için, çıktıdaki eşitlik doğrudan bu tek değişkene bağlanabiliyor.
Metin ile HTML Şablonunun Kaçırma Farkı
AG22. Aynı Urun değeri, aynı {{.Yorum}} şablon metniyle, iki farklı pakete
verildiğinde iki farklı bayt dizisi üretiyor. text/template, Yorum alanındaki
<b> ve & karakterlerine hiç dokunmadan olduğu gibi yazıyor. html/template aynı
karakterleri <, > ve & dizgilerine çeviriyor — bu dönüşüm kaçırma
(M14/K01 Web’in Temelleri ve HTML kursunda kurulan terim). html/template bunu
otomatik yapıyor çünkü çıktısının bir HTML belgesinin içine yerleşeceğini varsayıyor
ve <, >, & karakterlerinin HTML’de özel bir anlamı olduğunu biliyor; text/template
hiçbir varsayım yapmıyor, çünkü çıktısının nereye gideceğini bilmiyor.
M14/K01 ve M14/K02 kursları HTML üretimini ve kaçırma davranışını tarayıcı tarafında,
sunucudan gelen bir belgenin işlenmesi bağlamında kurdu; burada tekrarlanmıyor, tek
fark bu iki paketin aynı standart kitaplığın içinde, sunucu tarafında bir üretim adımı
olarak durması. İki paket arasındaki seçim bir stil tercihi değil: çıktı bir HTML sayfasına
gidecekse html/template kullanılmalı, çünkü kaçırmayı atlayan bir metin şablonu, veri
içinde <script> gibi bir dizi geçtiğinde bunu olduğu gibi yazan bir çıktı üretir.
html/templatein kaçırması bu ölçümde göründüğünden daha ayrıntılı çalışıyor: paket bir
alanın şablonun neresinde kullanıldığını (bir etiket gövdesinde mi, bir öznitelik
değerinde mi, bir bağlantı adresinde mi) ayrıştırma sırasında saptıyor ve her bağlama
uygun kaçırma kuralını uyguluyor — bir öznitelik değerindeki tırnak işareti ile bir etiket
gövdesindeki < karakteri farklı kurallarla kaçırılıyor. Bu ders yalnızca en basit bağlamı
(bir etiket gövdesi) ölçüyor; bağlam farkındalığının kendisi ayrı bir konu ve burada
derinleştirilmiyor. Önemli olan sonuç şu: text/template çıktısını hangi bağlamda
kullanacağını hiç bilmiyor ve hiçbir karakteri değiştirmiyor, html/template ise çıktısının
bir HTML belgesine yerleşeceğini varsayıyor ve buna göre davranıyor. Kaçırılmamış bir
kullanıcı girdisi bir HTML belgesinin içine olduğu gibi yerleştiğinde, tarayıcı onu düz
metin değil yapı olarak okuyabilir — kaçırmanın kendisi, aradan geçen değerin görünümünü
değil, karşı tarafın onu nasıl yorumlayacağını değiştiren bir adım.
Alan Adı Çalışma Zamanında Aranır
AG23. {{.OlmayanAlan}} ifadesi Urun tipinde var olmayan bir alanı istiyor.
Bu şablon derleniyor — çünkü şablon metni Go’nun derleyicisinin hiç görmediği, ayrı bir
dilde yazılmış bir dizgi; derleyici Urun tipinin hangi alanlara sahip olduğunu bilir
ama bir dizginin içindeki {{.OlmayanAlan}} ifadesini hiç okumaz. Alan araması,
Execute çağrıldığında, yansıma üzerinden çalışma zamanında yapılıyor, ve o an
alan bulunamayınca Execute bir hata döndürüyor. Kursun “yazılmayanın karşılığı”
sorusuna (M09/K01’de kurulan eksen, burada tekrar kullanılmıyor) benzer bir soru burada
da geçerli: derleyicinin hiç bakmadığı bir katman, kendi hatasını kendi zamanında üretiyor.
Şablonun kendi hata zamanları da tek değil. template.Must çağrısı, Parsein döndürdüğü
hatayı sarıp panikliyor — bu ders boyunca hiç tetiklenmedi, çünkü her şablonun metni
sözdizimsel olarak geçerliydi ({{ her zaman bir }} ile kapandı, her eylem tanınan bir
eylemdi). Parse hatası, örneğin kapanmamış bir {{ parçası için, programın en başında,
şablon kurulurken çıkar. Execute hatası ise sözdizimi tamamen geçerliyken, yalnızca
istenen alan yokken çıkıyor. İkisi de çalışma zamanında (Go’nun derleyicisi ikisini de
görmüyor), ama biri şablon kurulur kurulmaz, öbürü ancak gerçek veriyle çalıştırıldığında
ortaya çıkıyor — ve bu ders yalnız ikinciyi ölçtü, çünkü ilkini tetikleyecek hiçbir örnek
kullanılmadı.
AG24. hatali şablonu "on: {{.Ad}}, sonra: {{.OlmayanAlan}}" metnini taşıyor ve
iki ayrı eylem içeriyor: önce {{.Ad}} başarıyla değerlendiriliyor ve "on: vida, sonra: " metni bHatali arabelleğine yazılıyor; sonra {{.OlmayanAlan}} başarısız oluyor ve
Execute bir hata dönüyor. Ama arabellek geri alınmıyor — hatadan önce yazılmış olan
kısım orada kalıyor, çünkü Execute metni ilerleyerek yazıyor, bir sonuç biriktirip
en sonda tek seferde yazmıyor. Bu, ölçümün son satırında görünen "on: vida, sonra: "
dizgisinin nereden geldiğini açıklıyor: hata, çıktının ortasında, ikinci alan aranırken
doğuyor, ve o ana kadar üretilmiş her şey io.Writer’a zaten ulaşmış oluyor.
AG25. Bu, kursun “reddedildi” ile “açıkta kaldı” ayrımına yeni bir görünüm ekliyor.
Derleyici alan adı hatasını hiç görmüyor — kaynak her koşulda derleniyor — ve hata ne
derleme zamanında ne de şablon ayrıştırılırken (Parse çağrısında) çıkıyor; yalnızca
Execute çalıştığında, ve o çalıştırma anında yazılmış olan çıktı kalıcı oluyor. Bir
şablonu üretime almadan önce sınamanın tek yolu, onu gerçek verilerle çalıştırmaktır;
alan adlarının doğruluğunu statik olarak denetleyen bir araç bu ikilinin dışında kalıyor.
Bunun pratik sonucu, bir şablonun içindeki nadiren tetiklenen bir dalın (bir {{if}}
koşulunun yalnızca belirli bir veriyle geçtiği bir satırın) aylarca hiç çalıştırılmadan
durabilmesi. Go Temelleri kursunun Go Komut Ailesi dersi, inceleyicinin derleyicinin
gözden kaçırdığı bir kalıbı yakaladığını göstermişti — biçim dizgisi ile argüman tipinin
uyuşmaması. Şablon alan adları için böyle bir ikinci denetleyici yok; şablon dilinin
kendisi Go’nun tip sisteminin dışında, ayrı bir dilde yaşıyor ve derleyicinin de
inceleyicinin de erişemediği bir katmanda çalışıyor. Bu yüzden bir şablonun doğruluğu,
derlemenin verdiği güvenceyle değil, o şablonu gerçek veriyle en az bir kez çalıştırmış
olmanın verdiği güvenceyle ölçülüyor — ve bu ders bu ölçümü tam olarak koşturarak yaptı.
Özet
- Bir şablonun ürettiği şey her zaman metindir; kaynağın tipi ne olursa olsun (
int,float64) aynı şablon aynı dizgiyi üretir ve çıktıdan geriye doğru kaynağın tipine ulaşılamaz. - Alan adları da çıktıda otomatik olarak yer almaz — bir ad çıktıda görünüyorsa, bunun nedeni şablonu yazan kişinin o adı düz metin olarak şablona yazmış olmasıdır.
text/templatevehtml/templateaynı şablon dilini paylaşır, amahtml/templateçıktısını HTML kaçırmasından geçirir; aynı veri iki pakette iki farklı bayt dizisi üretir.- Şablondaki bir alan adı derleme zamanında değil,
Executeçalışırken, yansımayla aranır; olmayan bir alan şablonun derlenmesini değil çalıştırılmasını durdurur. - Hata, çıktının ortasında doğar:
Executehata döndürmeden önce yazdığı kısımio.Writer’da kalıcı olarak durur, geri alınmaz.
Sonraki Adım
Bu derste bir şablonun ürettiği çıktının tip taşımayan, saf metin olduğu ölçüldü — ve bu metnin okunması için karşı tarafta hiçbir tarif kurulmamıştı. Kursun bir önceki dersi (JSON dizileştirme) ile bu ders arasındaki fark tam olarak burada duruyor: biri tarifle geri dönen bir gidiş, öbürü hiç geri dönmeyen bir çıkış. Ama iki dersin de ortak bir noktası var: ikisinde de tarif, varsa, sınırın yalnızca bir tarafında duruyordu — JSON’da alıcıdaki hedef tip, şablonda hiç kimse. Sıradaki ders bu iki ucu birleştiriyor: iki ayrı taraf, aynı tarifi paylaştığında sınırı geçen tip bilgisine ne olur? Sözleşme temelli servis iletişimi bu soruyu, tarifin kendisini iki tarafın kaynağını üretecek biçimde kurarak yanıtlıyor — ve kursun on üç sınır geçişi arasında tip bilgisinin korunduğu tek satırı orada kuruyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.