Ders 05 / 12
Sözleşme Temelli Servis İletişimi
İki bağımsız sürecin kaynağı aynı paylaşılan tariften (alan adı, alan tipi, JSON etiketi listesi) text/template ile üretiliyor ve ayrı ayrı derlenip çalıştırılıyor. Tarif ortakken üç alanın üçü de tipini koruyor — kursun on üç sınır geçişi arasında korunuyor sınıfına giren tek satır. Tarif ayrışınca (alıcı tarafın üreticisi güncel değilse) geçiş sessizce bozuluyor: üç alandan ikisi korunuyor, biri hiç hata vermeden kayboluyor.
İçindekiler
Önceki ders bir şablonun ürettiği metnin hiçbir tarafta okunmayı beklemediğini gösterdi: çıktı yalnızca metindi, ve o metni geri okuyup bir tipe bağlayacak hiçbir taraf yoktu. Bu ders tam tersi bir uçtan başlıyor. İki bağımsız program (iki ayrı süreç, iki ayrı derleme) bir değeri birbirine geçirecek, ve bu kez sınırın iki tarafında da aynı tarif duruyor olacak. Kursun sorusu değişmiyor — bu değer sınırı geçerken tip bilgisine ne oluyor? — ama bu ders, on üç sınır geçişinin korunuyor sınıfına giren tek satırını kuruyor.
Bir tarif burada, iki tarafın da bir alan listesi üzerinde anlaştığı bir açıklamadır: her alanın adı, Go tipi ve JSON etiketi. Önceki derslerde tarif hep sınırın bir tarafında duruyordu — JSON’da alıcının elindeki hedef tip, kod ailesinde inceleyicinin bildiği kural. Bu derste tarif, sınırın hiçbir tarafına ait olmayan, üçüncü bir yerde (bir Go dilimi olarak programın kendi belleğinde) tutuluyor ve aynı tarif iki tarafın da kaynağını üretmek için kullanılıyor. Üretim standart kitaplığın kendi şablon paketiyle yapılıyor; hiçbir üçüncü taraf araç, protokol ya da veri biçimi devreye girmiyor.
İki tarafın kaynağını elle, ayrı ayrı yazmak da mümkündü — iki Kayit tipi, iki dosyada,
aynı alan adlarıyla. Ama bu durumda “aynı” olmaları bir disiplin meselesine dönüşürdü:
biri değişirken öbürü unutulabilir, ve bu unutuluş kaynak kodunun kendisinde görünmezdi.
Tarifi tek bir yerde tutup iki kaynağı ondan üretmek bu riski ortadan kaldırmıyor — birazdan
görüleceği gibi, üreticinin kendisi güncellenmezse aynı sorun geri geliyor — ama riski
tek bir noktaya topluyor: artık iki elle yazılmış kaynağın senkron kalması değil, tek
bir tarifin iki üretime de aynı anda verilmesi yeterli.
Aynı Tariften İki Tarafın Kaynağını Üretmek
AG26. Aşağıdaki yordamlar bir tarifi (AlanTarifi dilimini) alıp, Rol alanına göre
iki farklı kaynak üretebilen tek bir şablonu çalıştırıyor: "gonderen" rolü bir değeri
JSON’a çevirip standart çıktıya yazan bir program üretiyor, "alici" rolü standart girdiden
okuyup aynı yapıya çözen bir program üretiyor. calistir her iki üretilmiş kaynağı da
ayrı bir geçici modülde, go run . ile, ayrı bir süreç olarak çalıştırıyor — bu iki
“taraf” birbirinin kaynağını hiç görmüyor, yalnızca standart girdi/çıktı üzerinden bayt
alışverişi yapıyor.
// sozlesme.go — paylasilan tarifi iki tarafin kaynagini uretecek sekilde calistiran yordamlar
package main
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"text/template"
)
type AlanTarifi struct {
Ad string
Tip string
Etiket string
Ornek string
}
type SablonVerisi struct {
Alanlar []AlanTarifi
Rol string
}
func uret(sablon *template.Template, veri SablonVerisi) string {
var b bytes.Buffer
sablon.Execute(&b, veri)
return b.String()
}
func calistir(kaynak string, girdi []byte) string {
dizin, _ := os.MkdirTemp("", "sozlesme")
defer os.RemoveAll(dizin)
os.WriteFile(filepath.Join(dizin, "go.mod"), []byte("module taraf\n\ngo 1.24\n"), 0o644)
os.WriteFile(filepath.Join(dizin, "main.go"), []byte(kaynak), 0o644)
cmd := exec.Command("go", "run", ".")
cmd.Dir = dizin
cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
if girdi != nil {
cmd.Stdin = bytes.NewReader(girdi)
}
cikti, _ := cmd.Output()
return string(cikti)
}
uret şablonu bir bytes.Buffere yazdırıp dizgi olarak döndürüyor — bir önceki dersin
ölçtüğü tam olarak bu adım, burada bir kaynak kodu üretmek için kullanılıyor. calistir
ise Go Temelleri kursunun Go Komut Ailesi dersinde kurulan desenin aynısı: kaynağı geçici
bir modüle yazıp derleyiciyi (burada go run üzerinden) program içinden çağırmak. Fark
şurada — orada tek bir kaynak biçimlendiriliyor ya da inceleniyordu, burada iki farklı
kaynak, aynı tariften türeyip, birbirinden habersiz iki ayrı süreçte çalıştırılıyor.
Şablonun kendisi ikinci bir blokta duruyor; şablon metni, tarif dilimindeki her alan için
bir yapı alanı ve rolüne göre ya bir kodlama ya bir çözme gövdesi üretiyor. İçe aktarım
listesi de role göre dallanıyor: "fmt" ile "io" yalnızca alici rolünde ekleniyor,
çünkü gonderenin gövdesi ikisini de hiç kullanmıyor. Bu ayrım rastgele değil — kullanılmayan
bir içe aktarımın Go’da derlemeyi durdurduğu, Go Temelleri kursunda kurulmuş bir kuraldı;
şablon bu kuralı bilmiyor ama üretici (yani bu ders) biliyor, ve iki farklı kaynak metni
üretirken her ikisinin de kendi başına derlenebilir kalmasını rolüne göre dallanan bir
{{if}} bloğuyla sağlıyor.
// olcum.go — ortak tarifle ve ayrisan tarifle sozlesme temelli gecisin olcumu
package main
import (
"fmt"
"strings"
"text/template"
)
const tikTik = "`"
var sablonMetni = `package main
import (
"encoding/json"
{{if eq .Rol "alici"}} "fmt"
"io"
{{end}} "os"
)
type Kayit struct {
{{range .Alanlar}} {{.Ad}} {{.Tip}} ` + tikTik + `json:"{{.Etiket}}"` + tikTik + `
{{end}}}
func main() {
{{if eq .Rol "gonderen"}} k := Kayit{
{{range .Alanlar}} {{.Ad}}: {{.Ornek}},
{{end}} }
bayt, _ := json.Marshal(k)
os.Stdout.Write(bayt)
{{else}} bayt, _ := io.ReadAll(os.Stdin)
var k Kayit
json.Unmarshal(bayt, &k)
{{range .Alanlar}} fmt.Printf("%s tip=%T deger=%v\n", "{{.Ad}}", k.{{.Ad}}, k.{{.Ad}})
{{end}}{{end}}}
`
func main() {
sablon := template.Must(template.New("taraf").Parse(sablonMetni))
ortakTarif := []AlanTarifi{
{Ad: "Ad", Tip: "string", Etiket: "ad", Ornek: `"vida"`},
{Ad: "Adet", Tip: "int", Etiket: "adet", Ornek: "12"},
{Ad: "Fiyat", Tip: "float64", Etiket: "fiyat", Ornek: "19.90"},
}
gonderenKaynagi := uret(sablon, SablonVerisi{Alanlar: ortakTarif, Rol: "gonderen"})
fmt.Println("-- uretilen gonderen kaynagi --")
fmt.Println(gonderenKaynagi)
tasinanBayt := calistir(gonderenKaynagi, nil)
aliciKaynagi := uret(sablon, SablonVerisi{Alanlar: ortakTarif, Rol: "alici"})
aliciCiktisi := calistir(aliciKaynagi, []byte(tasinanBayt))
fmt.Println("-- tarif ortak: alici uc alani da okuyor --")
fmt.Print(aliciCiktisi)
korunanOrtak := strings.Count(strings.TrimRight(aliciCiktisi, "\n"), "\n") + 1
eskiTarif := ortakTarif[:2]
eskiAliciKaynagi := uret(sablon, SablonVerisi{Alanlar: eskiTarif, Rol: "alici"})
eskiAliciCiktisi := calistir(eskiAliciKaynagi, []byte(tasinanBayt))
fmt.Println()
fmt.Println("-- tarif ayristi: alici tarafin uretici tarifi guncel degil --")
fmt.Print(eskiAliciCiktisi)
korunanEski := strings.Count(strings.TrimRight(eskiAliciCiktisi, "\n"), "\n") + 1
fmt.Println()
fmt.Printf("tarif ortak: %d/%d alanin tipi koruniyor\n", korunanOrtak, len(ortakTarif))
fmt.Printf("tarif ayristi: %d/%d alanin tipi koruniyor, %d alan sessizce kayboldu\n",
korunanEski, len(ortakTarif), len(ortakTarif)-korunanEski)
}
-- uretilen gonderen kaynagi --
package main
import (
"encoding/json"
"os"
)
type Kayit struct {
Ad string `json:"ad"`
Adet int `json:"adet"`
Fiyat float64 `json:"fiyat"`
}
func main() {
k := Kayit{
Ad: "vida",
Adet: 12,
Fiyat: 19.90,
}
bayt, _ := json.Marshal(k)
os.Stdout.Write(bayt)
}
-- tarif ortak: alici uc alani da okuyor --
Ad tip=string deger=vida
Adet tip=int deger=12
Fiyat tip=float64 deger=19.9
-- tarif ayristi: alici tarafin uretici tarifi guncel degil --
Ad tip=string deger=vida
Adet tip=int deger=12
tarif ortak: 3/3 alanin tipi koruniyor
tarif ayristi: 2/3 alanin tipi koruniyor, 1 alan sessizce kayboldu
AG27. ortakTarif üç öğeli bir Go dilimi ve main fonksiyonu içinde tam olarak bir
kez tanımlanıyor. gonderenKaynagi ve aliciKaynagi ikisi de bu aynı değişkenden
üretiliyor — uret çağrısına ikisinde de ortakTarif geçiyor, başka bir kopya, başka bir
elle yazılmış liste yok. Bu, SINIR ortak tanımının koyduğu kuralı karşılıyor: bir ders
“korunuyor” satırı yazacaksa, tarifin iki tarafa nasıl ulaştığını da göstermek zorunda —
burada gösterilen yol, aynı bellek değerinin iki ayrı üretim çağrısına girdi olması.
Üretilen kaynaklar sonra birbirinden tamamen bağımsız: ayrı geçici dizinlerde, ayrı go.mod
dosyalarıyla, ayrı süreçler olarak derlenip çalıştırılıyor. gonderen tarafının kaynağı
alici tarafının kaynağını hiç görmüyor, ikisi yalnızca tasinanBayt üzerinden, düz bir
bayt dizisiyle konuşuyor.
Ayrı süreç olarak çalıştırma burada bir ayrıntı değil, ölçümün gövdesi. Eğer gonderen ve
alici, aynı Go paketinden içe aktarılan tek bir Kayit tipini paylaşsaydı, tip
korunması Go’nun derleyicisinin garantisi olurdu — iki taraf zaten aynı bellek düzenine
bakıyor olurdu, ve bu ders ölçmek istediği şeyi baştan varsaymış olurdu. Ayrı derlenen,
ayrı çalışan iki ikili arasında yalnızca JSON baytlarıyla konuşmak, sınırı gerçekten bir
sınır yapıyor: iki taraf, tarif ortak olduğu için değil, tarif ortak olduğunu her ikisinin
de bağımsızca ürettiği için aynı tipte anlaşıyor.
Kör nokta. Bu ölçümün “korunuyor” sınıfı, tarifin iki tarafa da gerçekten ulaştığı varsayımı üzerine kurulu — ve bu ders bu ulaşmayı, iki üretim çağrısının aynı Go dilimini okuması olarak modelliyor. Üretimde iki tarafın aynı tarifi paylaşması genellikle bir dağıtım adımı gerektirir (bir tarafın güncellenmiş tarifi öbürüne bir şekilde ulaştırması); bu ders o dağıtım adımının kendisini ölçmüyor, yalnızca dağıtım başarılı olduğunda ne kazanıldığını ve başarısız olduğunda (aşağıdaki bölüm) ne kaybedildiğini ölçüyor.
Tarif Ortakken Üç Alanın Üçü de Tipini Koruyor
AG28. alici kaynağı, ortakTariften üretildiğinde, gonderenin yazdığı JSON’u kendi
Kayit tipine çözüyor ve üç alanın üçünü de doğru tipiyle okuyor: Ad bir string,
Adet bir int, Fiyat bir float64 olarak geri geliyor — kaynak taraftaki tiplerle
birebir aynı. Bu satır kursun on üç sınır geçişinin tam dağılımındaki (1 korunuyor, 5
yeniden kuruluyor, 7 kayboluyor; 03. derste kurulan sayı) tek korunuyor satırı. Önceki
derslerdeki “yeniden kuruluyor” satırları da bir tarif kullanıyordu (yapı etiketi, hedef
tip, biçim dizgisindeki damga), ama o tarif hep sınırın bir tarafındaydı ve karşı taraf
kendi tarifini kendi başına, ayrı bir kaynaktan biliyordu. Burada ikisi de aynı tarifin
ürünü — bu fark, “yeniden kuruluyor” ile “korunuyor” arasındaki bütün mesafe.
AG29. Gönderen ve alıcının kaynak kodları birbirinden farklı: biri json.Marshal
çağırıyor, öbürü json.Unmarshal; biri os.Stdouta yazıyor, öbürü os.Stdinden okuyor.
Ama ikisinin Kayit tipi bayt bayt aynı alan adlarını ve aynı JSON etiketlerini taşıyor,
çünkü ikisi de {{range .Alanlar}} döngüsünü aynı ortakTarif dilimi üzerinde
çalıştırdı. Kodun kendisi aynı değil, ama kodu üreten tarif aynı — ve JSON’un alan adı bağlaması
(bir etiketin dize karşılığı üzerinden kurulur) yalnızca bu ikisinin etiketleri aynı olduğu
sürece çalışıyor. Tarif tek bir kaynaktan geldiği için etiketlerin aynı olması bir tesadüf
değil, üretim biçiminin doğrudan sonucu.
Tarif Ayrışınca: Sessiz Kayıp
AG30. eskiTarif, ortakTarifin ilk iki öğesi — Fiyat alanı yok. Bu, alıcı tarafın
üreticisinin güncellenmediği, gönderen tarafın ise güncel kaldığı bir durumu modelliyor:
iki taraf artık aynı tarifi paylaşmıyor. eskiAliciKaynagi bu eksik tariften üretiliyor ve
kendi Kayit tipinde yalnızca Ad ile Adet alanları var. Bu kaynak derleniyor —
Fiyatin yokluğu Go’nun tip sisteminde hiçbir sorun değil, çünkü eksik alan başka bir
tipin parçası olmayı hiç talep etmiyor — ve çalışıyor, json.Unmarshal çağrısı hiçbir
hata döndürmüyor.
AG31. tasinanBayt üç alanı da taşıyor ({"ad":"vida","adet":12,"fiyat":19.9}), ama
eskiAliciKaynaginin çözdüğü yapı yalnızca ikisini okuyabiliyor. fiyat anahtarı JSON
metninde duruyor, json.Unmarshal onu görüyor, ve hedef yapıda karşılığı olmadığı için
sessizce atlıyor — üçüncü paydaki JSON dersinin “alıcıda karşılığı olmayan alan sessizce
düşüyor” gözlemiyle birebir aynı düzenek, burada tek farkla: kayıp, bir programlama
hatasından değil, iki tarafın tarifinin birbirinden ayrışmasından doğuyor. Ölçümün son
satırı bunu sayıyla veriyor: tarif ortakken 3/3, tarif ayrışınca 2/3 alanın tipi korunuyor,
ve kayıp alan hakkında hiçbir uyarı, hiçbir hata çıkmıyor.
Yönün önemi burada ortaya çıkıyor: eksik olan taraf alıcı. Gönderen tarafın kaynağı
güncel kaldığı, yani Fiyati hâlâ gönderdiği sürece, hatanın hiçbir belirtisi gönderen
tarafta görünmüyor — gonderenKaynagi bu derste hiç değişmedi, calistir(gonderenKaynagi, nil) çağrısı iki senaryoda da aynı baytı üretti. Sorun yalnızca alıcı tarafın ürettiği
kaynakta, ve bu kaynağı üreten tarafın (kendi eskiTarifini kullanan üretici) güncellenmemiş
olmasında. Eğer roller tersine çevrilseydi — gönderen eski tarifle, alıcı güncel tarifle
üretilseydi — sonuç farklı olurdu: alıcının beklediği Fiyat alanı JSON’da hiç bulunmazdı
ve yapı bu alanı kendi sıfır değeriyle (0) doldururdu, çünkü encoding/json eksik bir
anahtarı da tıpkı bilinmeyen bir anahtar gibi sessizce görmezden geliyor. İki yön de aynı
sınıfa (kayboluyor) düşüyor, ama kaybın nerede göründüğü değişiyor: bir yönde alıcının
okuduğu yapıda alan hiç yok, öbür yönde alan var ama hep sıfır.
AG32. Bu, sözleşme temelli iletişimin bütün iddiasını tersinden kanıtlıyor: koruma
tarifin kendisinden gelmiyor, tarifin dağıtımından geliyor. Aynı tarif iki tarafa
aynı anda ulaştığı sürece geçiş korunuyor sınıfında kalıyor; taraflardan biri güncellenip
öbürü kalırsa, geçiş hiçbir hata vermeden yeniden “kayboluyor” sınıfına düşüyor. Tek bir
paylaşılan tarif kaynağı (bu derste ortakTarif değişkeni) korumanın önkoşulu; tarifin
kendisi hiçbir zaman bir garanti değil, yalnızca iki tarafın ne kadar senkron kaldığına
bağlı bir olasılık.
Bu sonucun kursun genel iddiasıyla ilişkisi dolaylı değil, doğrudan: on üç sınır geçişinin beşinde tip “yeniden kuruluyordu” ve her birinde geri kuran taraf, karşı tarafın elinde bulunan bir tarifti — ama o tarif hep karşı tarafın kendi tarifiydi, gönderen tarafın tarifiyle aynı olduğunu doğrulayan hiçbir mekanizma yoktu. Bu ders o iki tarifi tek bir tarife indirgeyerek “yeniden kurma” ile “koruma” arasındaki farkı ortaya koydu: yeniden kurma, karşı tarafın tahmininin doğru çıkmasına dayanıyordu; koruma, tahmine hiç gerek kalmamasına.
Özet
- İki bağımsız sürecin kaynağı, aynı paylaşılan tarif dilimi üzerinden
text/templateile üretilip ayrı ayrı geçici modüllerde derlenip çalıştırıldı; taraflar birbirinin kaynağını görmedi, yalnızca bayt alışverişi yaptı. - Tarif ortakken üç alanın üçü de tipini koruyor:
Ad,Adet,Fiyat— kaynak ve hedef taraftaki tipler birebir eşleşti. Bu, kursun on üç sınır geçişi arasındakorunuyorsınıfına giren tek satır. - Koruma, tarifin
uretçağrısına aynı değerden geçirilmesiyle gösterildi; kanıt olmadan bir “korunuyor” satırı yazılamayacağı kuralı burada tam olarak karşılandı. - Tarif iki tarafta ayrışınca (bir alan eksik üretilince) kaynak yine derleniyor, program yine çalışıyor, ama eksik alan hiçbir hata vermeden düşüyor — 3/3’ten 2/3’e sessiz bir kayıp.
- Sözleşme temelli iletişimin koruması bir garanti değil, iki tarafın tarifinin senkron kalmasına bağlı bir sonuç.
Sonraki Adım
Bu derste ölçülen paylaşım, tarifin ne olduğuyla değil, ne zaman kurulduğuyla ilgiliydi: gönderen tarafın kaynağı üretilip çalıştırıldı, sonra alıcı tarafın kaynağı ayrı bir süreçte başladı ve gönderenin bıraktığı baytı okudu — iki taraf hiç aynı anda çalışmadı, ve aralarında tek bir alışveriş oldu: bir istek gibi görünen bir yazma, bir yanıt gibi görünen bir okuma. Sıradaki ders bu varsayımı kaldırıyor: bağlantı uzun sürdüğünde, tek bir alışveriş yerine art arda birçok geçiş aynı bağlantı üzerinden aktığında, ve taraflardan biri karşı taraftan bir istek gelmeden veri itebildiğinde, sınır ne değişiyor?
Çerçeveleme, mesajın nerede başlayıp bittiğini taşıyan tek bilgi olarak devreye giriyor —
bir akışın neresinin bir mesajın sonu, neresinin bir sonrakinin başı olduğunu söyleyen tek
düzenek. Ama bu ders tam olarak gösterdi ki bir sınırı doğru çizmek, içindeki değerin tipini
bilmekle aynı şey değil: çerçeveleme bir mesajın nerede bittiğini söyler, o mesajın
içinde Ad, Adet, Fiyat gibi alanların durup durmadığını değil. O soruyu yanıtlamak
yine bir tarif gerektirecek — bu dersin ölçtüğü türden bir tarif.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.