Ders 14 / 16
Modül Yayımlama
Bir sürüm numarası kaynakta açıkça bildirilir, ama o sürümün taşıdığı uyumluluk sözünü kaynağın kendisi değil bir araç denetler: dışa aktarılan işlevlerin imzası karşılaştırılarak eklenen, değişen ve silinen adlar sayılır. Statik karar aynı kaynağı gerçekten derleyerek doğrulanıyor. Sınırlayıcı ölçüm, sözün imzalar üzerine kurulu olduğunu — aynı imzayı taşıyan iki gerçekleştirimin tamamen farklı bir sonuç verebildiğini ve aracın bunu hiç göremediğini — koşturarak gösteriyor.
Yayın Güncelleme
Bu derste
- Bir sürüm değişikliğinin uyumlu mu uyumsuz mu olduğunu, dışa aktarılan işlevlerin imzalarını karşılaştırarak ölçer
- İmza karşılaştırmasının verdiği kararı, eski bir çağıranı yeni sürümle gerçekten derleterek doğrular
- Uyumluluk sözünün imzalar üzerine kurulu olduğunu, aynı imzayı taşıyan iki farklı davranışla sınırlar
İçindekiler
Önceki derste gömülen değer kaynakta hiç yazılı değildi ve hiçbir araç onu denetlemedi. Bu ders tam tersi bir kaynaktan başlıyor: bir sürüm numarası, kaynağın kendisinde, açıkça bildiriliyor. Ama kaynakta yazılı olan bu sayı tek başına hiçbir şey garanti etmiyor — garanti eden şey, o sayının taşıdığı bir söz ve bu sözü tutup tutmadığını denetleyen ayrı bir araç.
Sürüm Kaynakta, Uyumluluk Araçtan
Bir modülün ardışık sürümleri arasındaki uyumluluk sözü (compatibility promise), yeni bir sürümün eski sürümü kullanan kodu bozmayacağını söylüyor. Bu söz kaynakta bir sayıyla bildiriliyor, ama sayının kendisi bir denetim yapmıyor; kimin doğru sayıyı yazdığını kontrol eden bir mekanizma yok. Denetlenebilen şey, sözün davranışsal karşılığı değil — gerçekleştirimin içinde ne olduğu— imzasal karşılığı: dışa aktarılan bir işlevin adı, parametreleri ve dönüş tipi bir sürümden ötekine değişmiş mi?
Aşağıdaki ölçüm standart kitaplığın kendi ayrıştırma paketlerini kullanarak küçük bir karşılaştırıcı kuruyor — gerçek bir yayımlama aracı değil, bu dersin sorusunu yanıtlamak için modellenmiş bir araç. İki kaynak metni ayrıştırıyor, her ikisinde de dışa aktarılan işlevlerin imzasını çıkarıyor ve iki kümeyi karşılaştırıyor.
Karşılaştırıcının kendisi üç adımdan oluşuyor. Önce her kaynak, dilin kendi ayrıştırıcısıyla bir söz dizim ağacına dönüştürülüyor — bu adım hiçbir tip çözümlemesi yapmıyor, yalnızca kaynağın söz dizimini okuyor. Sonra ağaçtaki her işlev bildirimi süzülüyor: bir yöntemin alıcısı olmayan (yani sıradan bir işlev olan) ve adı büyük harfle başlayan bildirimler tutuluyor, geri kalan her şey — yöntemler, dışa aktarılmayan yardımcı işlevler — atılıyor. Son adımda, kalan her işlevin tipi (parametre listesi ve dönüş listesi) yeniden kaynak biçimine yazdırılıyor ve bu yazdırılmış metin, o işlevin imzası olarak bir sözlükte tutuluyor. Sözlüğün anahtarı işlev adı, değeri imza metni; iki sürümün sözlükleri bu yüzden doğrudan karşılaştırılabiliyor.
// yardimcilar.go — bu dersin paylastigi yardimcilar: imza cikarma, gecici paket kurma, arac cagirma
package main
import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
)
// goster bir bilgiyi, kimin soyledigini ve kanitini tek satirda yazdirir.
func goster(bilgi, soyleyen, kanit string) {
fmt.Printf("%-42s %-7s %s\n", bilgi, soyleyen, kanit)
}
// imzalar, verilen kaynaktaki disa aktarilan serbest islevlerin adini, yazdirilmis tip
// imzasina esler. Standart kitapligin kendi ayristirma paketleriyle kurulan kucuk bir model;
// gercek bir uyumluluk araci degil.
func imzalar(kaynak string) map[string]string {
fset := token.NewFileSet()
dosya, err := parser.ParseFile(fset, "kaynak.go", kaynak, 0)
if err != nil {
return nil
}
sonuc := map[string]string{}
for _, dekl := range dosya.Decls {
fd, tamam := dekl.(*ast.FuncDecl)
if !tamam || fd.Recv != nil {
continue
}
ad := fd.Name.Name
if !ast.IsExported(ad) {
continue
}
var buf bytes.Buffer
printer.Fprint(&buf, fset, fd.Type)
sonuc[ad] = strings.TrimPrefix(buf.String(), "func")
}
return sonuc
}
// karsilastir, iki imza kumesi arasindaki eklenen, degisen ve silinen adlari doner.
func karsilastir(eski, yeni map[string]string) (eklenen, degisen, silinen []string) {
for ad, yeniImza := range yeni {
eskiImza, varDi := eski[ad]
if !varDi {
eklenen = append(eklenen, ad)
} else if eskiImza != yeniImza {
degisen = append(degisen, ad)
}
}
for ad := range eski {
if _, varMi := yeni[ad]; !varMi {
silinen = append(silinen, ad)
}
}
sort.Strings(eklenen)
sort.Strings(degisen)
sort.Strings(silinen)
return
}
// uyumluMu, sadece imzalara bakarak eski surumu kullanan kodun yeni surumle de
// derlenecegini soyler: hicbir ad silinmemis ve hicbir imza degismemis olmali.
func uyumluMu(eski, yeni map[string]string) bool {
_, degisen, silinen := karsilastir(eski, yeni)
return len(degisen) == 0 && len(silinen) == 0
}
// paket, verilen dosyalari yeni bir gecici modulun icine yazar; yol hicbir yerde basilmaz.
func paket(dosyalar map[string]string) string {
dizin, err := os.MkdirTemp("", "yayimlama")
if err != nil {
return ""
}
for ad, icerik := range dosyalar {
os.MkdirAll(filepath.Dir(filepath.Join(dizin, ad)), 0o755)
os.WriteFile(filepath.Join(dizin, ad), []byte(icerik), 0o644)
}
return dizin
}
// derlenebilirMi, verilen dizindeki paketi derlemeyi dener; cikti hicbir zaman basilmaz.
func derlenebilirMi(dizin string) bool {
cmd := exec.Command("go", "build", "-o", os.DevNull, "./...")
cmd.Dir = dizin
cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
return cmd.Run() == nil
}
// olcum1.go — surum kaynakta bildiriliyor, uyumluluk sozunu arac (imza karsilastirmasi) denetliyor
package main
import "fmt"
const surumV1 = "v1.0.0"
const surumV2Uyumlu = "v1.1.0"
const surumV2Uyumsuz = "v2.0.0"
const kaynakV1 = "package ornek\n\nfunc Ikiye(x int) int { return x * 2 }\n"
const kaynakV2Uyumlu = "package ornek\n\nfunc Ikiye(x int) int { return x * 2 }\n\nfunc Uce(x int) int { return x * 3 }\n"
const kaynakV2Uyumsuz = "package ornek\n\nfunc Ikiye(x int, taban int) int { return x * taban }\n"
func main() {
i1 := imzalar(kaynakV1)
i2u := imzalar(kaynakV2Uyumlu)
i2s := imzalar(kaynakV2Uyumsuz)
fmt.Println("-- surum kaynakta, uyumluluk aractan --")
goster("surum "+surumV1+" -> "+surumV2Uyumlu, "arac", fmt.Sprint("uyumlu=", uyumluMu(i1, i2u)))
goster("surum "+surumV1+" -> "+surumV2Uyumsuz, "arac", fmt.Sprint("uyumlu=", uyumluMu(i1, i2s)))
eklenenU, degisenU, _ := karsilastir(i1, i2u)
goster("uyumlu degisiklikte eklenen ad sayisi", "arac", fmt.Sprint(len(eklenenU)))
goster("uyumlu degisiklikte degisen imza sayisi", "arac", fmt.Sprint(len(degisenU)))
_, degisenS, _ := karsilastir(i1, i2s)
goster("uyumsuz degisiklikte degisen imza sayisi", "arac", fmt.Sprint(len(degisenS)))
}
-- surum kaynakta, uyumluluk aractan -- surum v1.0.0 -> v1.1.0 arac uyumlu=true surum v1.0.0 -> v2.0.0 arac uyumlu=false uyumlu degisiklikte eklenen ad sayisi arac 1 uyumlu degisiklikte degisen imza sayisi arac 0 uyumsuz degisiklikte degisen imza sayisi arac 1
DR12. Üç sürüm numarasının üçü de kaynakta yazılı — bu ders boyunca tek kaynak sınıfına
giren bilgi bu. Ama sürümlerin hangi ilişkide olduğu — v1.1.0’ın v1.0.0’ı bozmadığı,
v2.0.0’ın bozduğu — sayıların kendisinden değil, karşılaştırıcının imza kümeleri üzerinde
çalıştırılmasından geliyor. v1.1.0 yalnızca yeni bir ad ekliyor, Ikiyenin imzası aynı
kalıyor — bu yüzden uyumlu=true. v2.0.0 ise Ikiyeye ikinci bir parametre ekliyor; ad
aynı ama imza değişti — bu yüzden uyumlu=false. Sürüm numaralarının kendisi (v1.0.0,
v1.1.0, v2.0.0) bir kural değil bir etiket; kuralı uygulayan taraf sayıları hiç
okumadan, yalnızca imza kümelerini karşılaştırıyor.
M09/K01 Go Temelleri’nin go-packages konusu, bir modülün hangi sürümünün seçildiğini
en düşük sürüm seçimi kuralıyla ölçmüştü; bu ders o seçim mekanizmasını tekrar kurmuyor,
yalnızca seçilen sürümün taşıdığı sözü ölçüyor.
Gerçek Bir Derlemeyle Doğrulama
Yukarıdaki karar tümüyle statik: hiçbir kaynak gerçekten derlenmedi, yalnızca ayrıştırıldı ve
karşılaştırıldı. Bu kararın gerçek bir derlemeyle örtüşüp örtüşmediği ayrı bir soru ve
aşağıdaki ölçüm bunu doğrudan sınıyor — v1.0.0’ı eski biçimiyle çağıran bir kaynağı, hem
v1.1.0 hem v2.0.0 paketiyle birlikte gerçekten derliyor.
// olcum2.go — statik karsilastirmayi gercek bir derlemeyle dogrulama
package main
import "fmt"
const kaynakV2Uyumlu = "package ornek\n\nfunc Ikiye(x int) int { return x * 2 }\n\nfunc Uce(x int) int { return x * 3 }\n"
const kaynakV2Uyumsuz = "package ornek\n\nfunc Ikiye(x int, taban int) int { return x * taban }\n"
// kur, eski cagiranin kaynagini verilen paket surumuyle birlikte gecici bir module yaziyor.
func kur(paketKaynagi string) string {
return paket(map[string]string{
"go.mod": "module deneme\n\ngo 1.24\n",
"ornek/ornek.go": paketKaynagi,
"cagiran.go": "package main\n\nimport \"deneme/ornek\"\n\nfunc main() {\n\t_ = ornek.Ikiye(5)\n}\n",
})
}
func main() {
dizinUyumlu := kur(kaynakV2Uyumlu)
dizinUyumsuz := kur(kaynakV2Uyumsuz)
defer func() { _, _ = dizinUyumlu, dizinUyumsuz }()
fmt.Println("-- statik karar, gercek derlemeyle dogrulaniyor --")
goster("eski cagiran + uyumlu surum derlendi mi", "arac", fmt.Sprint(derlenebilirMi(dizinUyumlu)))
goster("eski cagiran + uyumsuz surum derlendi mi", "arac", fmt.Sprint(derlenebilirMi(dizinUyumsuz)))
}
-- statik karar, gercek derlemeyle dogrulaniyor -- eski cagiran + uyumlu surum derlendi mi arac true eski cagiran + uyumsuz surum derlendi mi arac false
DR13. İki satır da bir önceki ölçümün verdiği kararla birebir örtüşüyor: uyumlu=true
denen sürüm çiftinde eski çağıran gerçekten derleniyor, uyumlu=false denen çiftte gerçekten
derlenmiyor. Bu örtüşme rastlantı değil — imza karşılaştırması zaten derleyicinin kendi
kuralını modelliyor: bir çağrının derlenebilmesi için çağrılan işlevin adı ve parametre
listesi çağrı yerinde beklenenle eşleşmeli. Statik karşılaştırıcı bu kuralı derleyiciyi hiç
çalıştırmadan uyguluyor; gerçek derleme ise aynı kuralı, gerçek bir çağrı üzerinden bir kez
daha, bu kez koşum olmadan ama gerçek bir araç çağrısıyla doğruluyor. Bağlayıcı
seçeneklerinde olduğu gibi burada da iki farklı yol aynı soruya varıyor; farkları, birinin
kaynağı hiç derlemeden okuması, ötekinin gerçekten derlemesi.
Bu iki yolun birbirini doğrulaması pratikte önemli bir tasarım seçimini haklı çıkarıyor: statik karşılaştırma, gerçek bir çağıran kaynağına hiç ihtiyaç duymadan çalışıyor — yalnızca iki sürümün kendi kaynağını okuyor, bir “eski kod” örneğine gerek yok. Gerçek derleme ise tam tersi: belirli bir çağıran olmadan hiçbir şey söyleyemiyor, ama söylediği şey doğrudan o çağıranın kaderiyle ilgili. Bir yayımlama sürecinde ilk yol ucuz ve genel bir ön elemeyi, ikinci yol belirli bir tüketiciye karşı kesin bir kanıtı sağlıyor; bu ders ikisinin aynı iki sonuca vardığını göstererek, ucuz yolun bu dar örnekte pahalı yolun yerini tutabildiğini ölçüyor.
Sınırlayıcı Ölçüm: Söz İmzalar Üzerinedir, Davranış Üzerine Değil
Yukarıdaki iki ölçüm de imza değişikliğine bakıyordu. Bir imza hiç değişmeden de bir
gerçekleştirimin davranışı değişebilir — aynı ad, aynı parametre, aynı dönüş tipi, ama
gövdenin içinde farklı bir hesap. Aşağıdaki ölçüm bunu doğrudan gösteriyor: Ikiye adlı iki
gerçek işlev, birebir aynı imzayı taşıyor ama biri ikiyle, öteki üçle çarpıyor.
// olcum3.go — sinirlayici olcum: uyumluluk sozu imzalar uzerinedir, davranis uzerine degil
package main
import "fmt"
// ikiyeEski, ilk surumun gercek gerceklestirimi.
func ikiyeEski(x int) int { return x * 2 }
// ikiyeYeniDavranis, AYNI imzayi tasiyan ama farkli hesaplayan bir sonraki surum gerceklestirimi.
func ikiyeYeniDavranis(x int) int { return x * 3 }
const kaynakEski = "package ornek\n\nfunc Ikiye(x int) int { return x * 2 }\n"
const kaynakYeniDavranis = "package ornek\n\nfunc Ikiye(x int) int { return x * 3 }\n"
func main() {
fmt.Println("-- sinirlayici olcum: imza ayni, davranis farkli --")
goster("imza karsilastirmasi", "arac", fmt.Sprint("uyumlu=", uyumluMu(imzalar(kaynakEski), imzalar(kaynakYeniDavranis))))
goster("Ikiye(5) eski gerceklestirim", "arac", fmt.Sprint(ikiyeEski(5)))
goster("Ikiye(5) yeni gerceklestirim", "arac", fmt.Sprint(ikiyeYeniDavranis(5)))
goster("iki gerceklestirim ayni sonucu mu veriyor", "arac", fmt.Sprint(ikiyeEski(5) == ikiyeYeniDavranis(5)))
}
-- sinirlayici olcum: imza ayni, davranis farkli -- imza karsilastirmasi arac uyumlu=true Ikiye(5) eski gerceklestirim arac 10 Ikiye(5) yeni gerceklestirim arac 15 iki gerceklestirim ayni sonucu mu veriyor arac false
DR14. Karşılaştırıcı bu değişikliği uyumlu=true olarak sınıflıyor, çünkü Ikiyenin adı,
parametre tipi ve dönüş tipi hiç değişmedi. Ama aynı girdiyle (5) çağrıldığında iki
gerçekleştirim farklı sayı veriyor — 10 ve 15. Bu, uyumluluk sözünün kapsamının
sınırı: söz “eski kodunuz derlenmeye ve çalışmaya devam edecek” diyor, “eski kodunuzun
sonucu aynı kalacak” demiyor. Bir çağıranın kodu her iki sürümle de sorunsuz derlenir ve
çalışır — hiçbir hata, hiçbir panik yok — ama ürettiği sonuç sürüme göre değişir. İmza
karşılaştırıcısı bu farkı hiçbir zaman göremiyor, çünkü baktığı şey isimler ve tipler; bir
gövdenin içinde ne hesaplandığı, bu aracın erişebildiği katmanın tümüyle dışında.
Bu sınırın nedeni keyfi değil: bir gövdenin anlamını iki sürüm arasında karşılaştırmak,
imzasını karşılaştırmaktan bambaşka bir problem. İmza karşılaştırması bu derste birkaç
satırlık bir ayrıştırma ve bir sözlük karşılaştırmasıyla çözüldü; davranış karşılaştırması
ise genel halinde çözülemez — iki işlevin her girdi için aynı sonucu verip vermediğini
sınamak, sonlu bir sürede yanıtlanamayan bir problem sınıfına giriyor. Bu yüzden uyumluluk
sözü pratikte hep imzalar üzerine kuruluyor: ölçülebilir, sonlu zamanda karar verilebilir bir
şey. Davranış uyumluluğunu güvence altına almanın elde kalan tek yolu, bu dersin ilk
bölümünde ölçülen tablo güdümlü testler ve karşılaştırmalı ölçümler — yeni sürümü eski
sürümün beklediği girdilerle koşturup sonuçları karşılaştırmak. Bu, imza karşılaştırıcısının
yapamadığını, gerçek koşumla tamamlıyor; ama artık bu derste ölçülen araç sınıfının dışında,
kursun koşum sınıfının konusu.
Bu, bir yayımlama sürecinde imza uyumluluğunun yeterli değil, gerekli bir koşul olduğunu gösteriyor. İmza uyumsuzsa, eski kod kesinlikle derlenmiyor — bu ders bunu gerçek bir derlemeyle doğruladı. İmza uyumluysa, eski kod kesinlikle derleniyor, ama ürettiği sonucun öncekiyle aynı olacağı hiçbir şekilde garanti edilmiyor. Bir sürüm numarasını artırmak bu yüzden iki ayrı kararı birden taşıyor: imzaların değişip değişmediği (bu ders tarafından ölçülebilir) ve davranışın değişip değişmediği (yalnızca ayrı bir test ve gözlem süreciyle ölçülebilir).
Özet
- Bir sürüm numarası kaynakta açıkça bildiriliyor, ama taşıdığı uyumluluk sözünü kaynağın kendisi değil, dışa aktarılan işlevlerin imzasını karşılaştıran bir araç denetliyor.
- Yalnızca yeni ad eklenen bir değişiklik uyumlu, var olan bir imzayı değiştiren bir değişiklik uyumsuz sayılıyor; karar sürüm numarasının kendisinden değil, imza kümesinin karşılaştırılmasından geliyor.
- Statik imza kararı, eski bir çağıranı yeni sürümle gerçekten derleterek doğrulandı; iki yöntem de aynı iki sonuca vardı.
- Sınırlayıcı ölçüm: birebir aynı imzayı taşıyan iki gerçekleştirim tamamen farklı bir sonuç verebiliyor ve imza karşılaştırıcısı bu farkı hiçbir zaman göremiyor.
- Uyumluluk sözü, bir kaynağın derlenip çalışacağını garanti ediyor, ürettiği sonucun sürümden sürüme aynı kalacağını garanti etmiyor.
Sonraki Adım
Bu derste ölçülen araç, kaynağı yalnızca isimler ve tipler düzeyinde okudu; gövdelerin içinde ne olduğuna hiç bakmadı. Sıradaki ders tam da bu boşluğa iniyor: bir kaynağın içindeki belirli kalıpları arayan, imzayı değil gövdenin kendisini okuyan bir araç. Soru artık “isimler ve tipler değişti mi” değil, “kaynağın içinde daha önce hiç aranmamış bir kalıp var mı”.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.