Bu kitobdagi boshqa har bir bob bitta xususiyat, paket yoki texnika haqida edi. Bu bob esa vaqt haqida — aniqrog'i, bugun yozayotgan kodingizga uch yil o'tgandan keyin, uning siz hech uchratmagan qirqta chaqiruvchisi paydo bo'lgandan va uni yozilgan paytda jamoada bo'lmagan kishi qo'llab-quvvatlaydigan bo'lgandan keyin nima sodir bo'lishi haqida. Uzoq yashaydigan kod bazasidagi deyarli har bir dizayn qarori pirovard natijada o'sha paytda hech kim kutmagan o'zgarishni qanchalik yaxshi qabul qila olgani bilan baholanadi. Goning o'z tarixi nima ishlashining ayniqsa yaxshi dalili: til o'n yildan ortiq ochiq moslik va'dasini saqlagan va standart kutubxona hech narsani buzmasdan kengayishga mo'ljallangan API'ning eng katta yagona namunasi hisoblanadi. Ushbu bob o'sha odatlarni o'z kodingizga qo'llab olish haqida.
Go 1.0 chiqarilganidan beri loyiha quyidagi majburiyatni olgan: Go 1.x uchun yozilgan dasturlar, x dan katta har qanday y uchun, keyingi Go 1.y relizlarida ham kompilyatsiya bo'lishda va to'g'ri ishlashda davom etadi. Go 1.10 dagi standart kutubxonaga qarshi kompilyatsiya qilingan kod Go 1.21 standart kutubxonasiga qarshi ham muammosiz kompilyatsiya bo'ladi va xuddi shunday ishlaydi. Til sintaksisi faqat o'sadi (1.18 da qo'shilgan generics qat'iy kengayishning yirik misoli — u sababli hech bir mavjud to'g'ri dastur kompilyatsiyadan to'xtamadi), vositalar xatti-harakati esa — go build yoki go vet nimani qabul qilishi — 1.x seriyasi ichida izchil qoladi.
Va'da nimani qamrab olmasligini bilish ham ibratlidir. Yangi API'lar va paketlar erkin qo'shilishi mumkin. Unumdorlik reliz-relizga, berilgan ish yuklamasi uchun har ikki yo'nalishda, ba'zan sezilarli o'zgaradi. Agar mavjud xatti-harakat haqiqiy xato bo'lsa va tuzatish kam xavfli deb baholansa, tuzatilishi mumkin — kamdan-kam hollarda, ataylab va doim eski (noto'g'ri) xatti-harakatga hatto tasodifan tayanayotgan dasturlar soni o'lchanib ko'riladi. API'lar esa hech qachon olib tashlanmasdan deprecated deb belgilanishi mumkin — hujjatlarda, ba'zan staticcheck yoki go vet ogohlantirishlari bilan — chunki olib tashlashning o'zi va'dani buzgan bo'lardi.
O'z kodingiz uchun olib chiqariladigan xulosa "Goning aniq siyosatini ko'chirib olish" emas — ko'p loyihalarga bunchalik rasmiy yondashuv kerak emas — balki uning tagidagi intizomni o'zlashtirishdir: moslik kafolatingiz aslida nimadan iborat ekanini (barqaror ommaviy API? barqaror simli format? barqaror CLI flag to'plami?) yozma ravishda belgilang, so'ng qo'shish va olib tashlashni turli darajadagi to'siqqa ega, turli xil o'zgarish turlari sifatida farqlang.
Bitta ishni bajaradigan tip haqida har bir chaqiruv joyida mulohaza yuritish oson va keyinroq kengaytirish ham oson, chunki uning o'sishi mumkin bo'lgan birgina o'qi bor. Bir nechta mas'uliyatni to'playdigan tip — faylga saqlaydigan va ma'lumotlar bazasidan yuklaydigan Calculator — aloqasiz vazifalarni birbiriga bog'laydi, shuning uchun bitta mas'uliyat tufayli kelib chiqadigan o'zgarish faqat boshqasiga e'tibor bergan chaqiruvchilarni buzish xavfini tug'diradi.
// Tor va kompozitsiyalanadigan: har bir imkoniyat mustaqil sinaladi va almashtiriladi
type Calculator struct{}
func (c Calculator) Add(a, b int) int { return a + b }
func (c Calculator) Subtract(a, b int) int { return a - b }
// Hamma narsa bitta tipdan erishiladi: persistence-qatlam o'zgarishi
// endi arifmetik chaqiruvchilarni ham xavf ostiga qo'yadi
type Calculator struct{}
func (c *Calculator) Add(a, b int) int { /* ... */ }
func (c *Calculator) SaveToFile(path string) error { /* ... */ }
Bu 26-bobdagi "interface qabul qiling, struct qaytaring" odatining bitta funksiya emas, butun paket miqyosida qo'llanishidir: chaqiruvchi aslida muhtoj bo'lgan eng kichik interface'ni belgilasin va konkret implementatsiyalar uning ortida ixtiyoriy ravishda farqlansin.
type Storage interface {
Save(key string, value []byte) error
Load(key string) ([]byte, error)
}
func ProcessData(s Storage, data []byte) error {
return s.Save("key", data)
}
ProcessData Storage fayl, ma'lumotlar bazasi, testlar uchun xotiradagi map yoki ikki yildan keyin qo'shiladigan bulutli object store bilan ta'minlanganidan qat'i nazar o'zgarishsiz ishlaydi. Chunki chaqiruvchilar bog'liq bo'lgan narsa implementatsiya emas, balki interface, va yangi backend qo'shish bu funksiyaga umuman tegmaydi.
Go API'sining o'sishi kerak bo'ladigan eng keng tarqalgan holat — "asl funksiyaga yana bitta parametr kerak". Uni majburiy pozitsion argument sifatida qo'shish har bir mavjud chaqiruv joyini buzadi. Idiomatik muqobil — options tipini kiritish va yangi funksiyani eskisini almashtirishga emas, uning yoniga qo'yishdir:
// v1
func CreateUser(name, email string) (*User, error) {
return CreateUserWithOptions(CreateUserOptions{Name: name, Email: email})
}
// v1.1 — haqiqiy, kengaytiriladigan kirish nuqtasi
type CreateUserOptions struct {
Name string
Email string
Phone string // zero value ("") — yaxshi sukut bo'yicha qiymat
Metadata map[string]string // zero value (nil) — yaxshi sukut bo'yicha qiymat
}
func CreateUserWithOptions(opts CreateUserOptions) (*User, error) {
// ...
}
CreateUser yupqa, doimiy qo'llab-quvvatlanadigan qulaylik o'ramiga aylanadi; yangi maydonlar CreateUserOptionsga zero value'lari tanlangan holda qo'shiladi, shunda ular haqida hech narsa bilmaydigan eski chaqiruvchilar avtomatik ravishda oqilona xatti-harakat oladi. Buni API'ning birinchi versiyasidanoq rejalashtirish foydali, bugun faqat bir-ikki parametringiz bo'lsa ham: options struct'i oldindan deyarli hech narsaga tushmaydi, lekin sizga buzuvchi relizsiz yillar davomida qo'shimcha evolyutsiya imkonini beradi. Teskari intizom ham bor — bu struct'ni birinchi kundan ortiqcha loyihalamaslik: hozir aslida kerak bo'lgan ikkita maydonni jo'nating va uchinchisini keyinroq qo'shish arzon bo'lishiga ishoning, aynan shu shaklni tanlaganingiz uchun. Hech bir chaqiruvchi hali so'ramagan o'n ikkita spekulyativ maydonli (qayta urinish siyosati, circuit breaker sozlamalari, metrika hook'lari) Config struct'i o'z turidagi qarzdir: bu hech kim ishlatmaydigan xatti-harakat uchun hujjatlashtirish, testlash va qo'llab-quvvatlash kerak bo'lgan yukdir.
"Bu paket tartibsizlik, biz uni qayta yozishimiz kerak" degan xulosa qilish vasvasasi kod bazasi yillar davomida organik o'sganida eng kuchli bo'ladi. Biroq aynan shu paytda to'liq qayta yozish eng xavflidir, chunki bu organik o'sish jarayonida hech kim noldan qayta spetsifikatsiya qilish uchun yetarlicha yaxshi eslamaydigan chekka holatlar va xato tuzatishlarning uzun qatorini kodlaydi. Amalda natija beradigan muqobil — bosqichma-bosqich yondashuv: yangi implementatsiyani eskisining yoniga boshqa nom ostida kiriting, yangisi tenglikka erishgach eskisini deprecated deb belgilang va eskisini faqat haqiqiy ko'chirish oynasidan keyin olib tashlang. Bu uch ketma-ket pull request sifatida emas, balki oylar davomida qo'llaniladigan siyosat sifatida amalga oshiriladi, ikkala yo'lni ham sinaydigan testlar bilan tasdiqlangan va ko'pincha feature flag ortida bajariladi, shunda yangi kod yo'li yagona bo'lishidan oldin production'da sinab ko'riladi:
var useNewPipeline = os.Getenv("ENABLE_NEW_PIPELINE") == "true"
func Process(data []byte) error {
if useNewPipeline {
return processNew(data)
}
return processOld(data)
}
Bunday feature flag o'z murakkabligini oqlaydigan holat — yangi yo'lni production trafikning bir qismiga jo'natish, uni kuzatish va muammoli deploy'ni qaytarish o'rniga shunchaki environment o'zgaruvchisini o'zgartirib orqaga qaytarish imkonini berganda. Buni doimiy arxitektura xususiyati sifatida emas, balki vaqtinchalik mexanizm sifatida qo'llang. Ko'chirish tugagach flag va eski yo'lni o'chirishni rejalashtiring; hech kim olib tashlashga jur'at eta olmaydigan if useOldThing tarmoqlari bilan to'lgan kod bazasi asl muammoning ustiga qatlamlangan ikkinchi turdagi texnik qarzdir.
Xuddi shu bosqichma-bosqich yondashuv bitta funksiya ichida ham qo'llaniladi: noaniq bo'lib qolgan yuz qatorli funksiyani bo'sh sahifadan qayta yozishdan ko'ra mustaqil sinash mumkin bo'lgan nomlangan bosqichlarga ajratish xavfsizroqdir.
func processEverything(data []byte) error {
if err := validate(data); err != nil {
return err
}
if err := transform(data); err != nil {
return err
}
return save(data)
}
validate, transform va savening har biri endi mustaqil sinalishi, ular haqida mulohaza yuritilishi va — pirovard natijada — almashtirilishi mumkin. Bu aynan asl monolitni xavfsiz o'zgartirishni qiyinlashtirgan xususiyat edi.
Texnik qarzning har bir bo'lagini olib tashlash inson rejalashtirishini talab qilmaydi. Go 1.26 go fixni — ilgari bir necha tarixiy, kamdan-kam qo'llaniladigan tuzatuvchini o'z ichiga olgan, asosan harakatsiz buyruqni — "modernizer"lar to'plamiga qayta qurdi: kodni gofmt formatlashni yangilagani kabi joriyroq idiomaga yangilaydigan, go vet ishlatadigan bir xil statik tahlil framework'iga qurilgan avtomatlashtirilgan qayta yozishlar. Kod bazasiga qarshi go fix ./...ni ishga tushiring — u standart kutubxona keyinchalik to'g'ridan-to'g'ri nom bergan andozalarni almashtirish kabi ishlarni bajaradi. Kutubxona mualliflari funksiyaga //go:fix inline direktivalarini biriktirib, go fixga "buning chaqiruvchilari yangi narsani to'g'ridan-to'g'ri chaqirish uchun qayta yozilishi kerak" deb ko'rsatma berishi mumkin. Go 1.27 yana bir nechta modernizer qo'shdi, jumladan atomic-tip foydalanish, embedded-literal andozalar va o'shandan beri xavfsizroq standart kutubxona ekvivalentlariga ega bo'lgan unsafe-paket chaqiruv joylari uchun modernizer'lar.
Bu aynan shu bob moslik va'dasi bilan boshlangani bilan bog'liq: Goning eski kodni buzmaslik intizomi tufayli kod bazasi yillar davomida "texnik jihatdan to'g'ri, lekin bugun siz boshqacha yozgan bo'lardingiz" andozalarni to'plashi mumkin. go fix — toolchain'ning shu to'planishga o'z javobi: katta, eski kod bazasi idiomalarini bosqichma-bosqich joriy saqlashning mexanik, kam xavfli usuli, yuqoridagi feature-flagli ko'chirish bilan bir xil ruhda. Faqat bunda vosita qayta yozishni o'zi bajaradi, siz esa yangi implementatsiyani qo'lda yozish o'rniga diff'ni ko'rib chiqasiz. Yillar davomida qo'llab-quvvatlaydigan kod bazasida uni vaqti-vaqti bilan ishga tushirish foydali, go mod tidy yoki gofmt bilan bir xil gigiena kategoriyasida: arzon, mexanik va o'zi alohida loyiha emas, har safar kichik to'planuvchi yutuq. Statik tahlil bobida eslatilgan stdversion vet tekshiruvi buning to'ldiruvchi yarmi: go fix allaqachon kompilyatsiya bo'ladigan kodni modernizatsiya qilsa, stdversion (Go 1.27 dan boshlab go test ostida standart holatda ishlaydi) sizning go.modingiz qo'llab-quvvatlashni e'lon qilgan Go versiyasidan yangiroq standart kutubxona simvolini chaqiradigan kod yozishingizga to'sqinlik qiladi. Bir vosita sizni joriy saqlaydi, boshqasi esa siz qanday "joriy"ga majburiyat olganingiz bo'yicha halollikni ta'minlaydi.
vMAJOR.MINOR.PATCH faqat raqamlar da'vo qilgan narsani chindan anglatgandagina o'z vazifasini bajaradi: patch relizi chaqiruvchi oqilona tayanishi mumkin bo'lgan xatti-harakatni hech qachon o'zgartirmaydi, minor relizi faqat qo'shadi, major relizi esa buzuvchi o'zgarishlar yashashiga ruxsat etilgan yagona joy. Go modullari buni ko'p ekotizimlarda bo'lmagan darajada qat'iy amalga oshiradi: v2 yoki undan keyingi major versiya module path'ning o'ziga /v2 (va h.k.) qo'shimchasini talab qiladi. Ya'ni eski va yangi major versiyalar bir build grafida yonma-yon import qilinishi mumkin, va major versiyaga o'tish go get -uda jimgina sodir bo'ladigan narsa emas, balki chaqiruvchi ataylab qilishi kerak bo'lgan import-yo'li o'zgarishi. Hech qachon ommaviy modul nashr qilmasangiz ham bu mexanik faktni bilish foydali: u Go toolchain "major versiya"ni "bu haqiqatan boshqa, birga mavjud shartnoma" deb qanchalik jiddiy qabul qilishini ko'rsatadi, va module-path majburlashisiz ham o'zingizning ichki buzuvchi o'zgarishlaringiz qanchalik ongli ravishda xabar qilinishi kerakligining yaxshi modeli bo'ladi.
Buzuvchi o'zgarish haqiqatan zarur bo'lganda, keyingi odamga chindan yordam beradigan deprecation xabari nima o'zgargani va nima qilish kerakligi haqida aniq bo'lishi kerak, faqat nimadir o'zgarganligi haqida emas:
// ProcessV2 replaces Process.
//
// BREAKING CHANGES from Process:
// - takes an Options parameter (use DefaultOptions() for prior behavior)
// - returns Result instead of only error
//
// Migration:
// old: err := Process(data)
// new: result, err := ProcessV2(data, DefaultOptions())
func ProcessV2(data []byte, opts Options) (Result, error) {
// ...
}
Ushbu kitob davomida asosiy ip shu bo'ldi: Go o'ziga xos turdagi cheklovni mukofotlaydi — kamroq til xususiyatlari, ataylab tanlangan, izchil qo'llangan. Exception'lar o'rniga aniq error qiymatlari. Chuqur ierarxiyalar o'rniga kichik, strukturaviy interface'lar. O'nlab raqobatchi concurrency primitivlari o'rniga goroutine va channel'lar. Generics kech qo'shildi va hali ham tejamkorlik bilan ishlatiladi, chunki ko'p muammolarga ular kerak emas. Bu cheklovlarning hech biri tasodifiy emas, va hech biri mohiyatan til haqida emas. Bu Go dizaynida ongli ravishda qilingan va uning standart kutubxonasida aks-sado bergan tikishdir: o'qish uchun zerikarli kod — kod bazasining keyingi besh yil bilan to'qnashuvidan omon qoladi — soat 2 da uni debug qilayotgan on-call muhandis, asl muallif bilan tanishmasdan uni kengaytirayotgan yangi xodim, bugun siz faxrlangan har bir aqlli hiylani unutgan va ularning birortasini yozmaganingiz uchun minnatdor bo'ladigan sakkiz oydan keyingi sizning o'zingiz.
O'qish uchun zerikarli kod — kod bazasining keyingi besh yil bilan to'qnashuvidan omon qoladi.
Bu kitob shu dalilni oddiy da'vo bilan emas, misollar bilan isbotlashga urindi: oddiylik jozibaliroq muqobil yonidagi zerikarli, xavfsiz tanlov emas — u reliz-relizga, jamoa a'zosi-jamoa a'zosiga haqiqatan to'planib boradigan tanlovdir. Buni ataylab, bobma-bob va tizimma-tizim shunday yozish — butun hunarning o'zi. Indeks va lug'at siz u bilan haqiqiy narsa qurishga kirishganingizda ma'lumotnoma sifatida xizmat qiladi.