İçeriğe geç
academia.sh

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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat