Go sizga beradigan har bir xavfsizlik kafolati — slays indekslarining chegaradan chiqmasligi, nil ko'rsatkichni dereference qilish butun xotirani buzib yuborish o'rniga xavfsiz panic qo'zg'atishi, garbage collector qaysi obyektlarga hali murojaat qilish mumkinligini doim aniq bilishi — bularning barchasi kompilyator va runtime siz boshqarayotgan qiymatlarning tiplari haqida to'liq xabardor bo'lishiga tayanadi. unsafe paketi aynan mana shu qat'iy qoidalarni ongli ravishda chetlab o'tish uchun yaratilgan. Bu oddiy ma'nodagi kutubxona emas: u til spetsifikatsiyasining ajralmas qismi bo'lib, to'g'ridan-to'g'ri kompilyator tomonidan amalga oshiriladi va sizga aynan uchta qudratli vosita beradi: boshqa hollarda qat'iyan taqiqlangan pointer turlari orasida o'zaro konvertatsiya qilish, kompilyator tiplaringiz uchun tanlagan xotira joylashuvini o'rganish hamda ko'rsatkichni butun songa va orqaga xavfsiz o'girish. Go tilida "unsafe dasturlash" deb ataladigan barcha murakkab mexanizmlar aynan mana shu uchta primitiv asosiga quriladi.
Paket nomi shunchaki po'pisa yoki bezak emas. unsafe ishlatadigan kod xotirani buzishi, garbage collector'ni chalg'itishi hamda Go versiyalari, protsessor arxitekturalari yoki dasturning har bir ishga tushishi orasida butunlay farqli natijalar berishi mumkin. Shu bilan birga, tor va o'ta ongli ravishda qo'llanilganda, u profiler orqali isbotlangan qimmatli nusxalash amallarini yo'qotishga yordam beradi. Ushbu bobning vazifasi — kompilyator va runtime ma'lumotlaringiz bilan xotirada aslida qanday ishlashini to'g'ri tasavvur qilishga o'rgatish, shunda siz qachon undan foydalanish o'rinli ekanini, qachon esa oddiy ilova dasturlarida unga deyarli hech qachon ehtiyoj yo'qligini aniq tushunib olasiz.
unsafe nima uchun umuman mavjudGo tilining tiplar tizimi ko'rsatkichlar (pointer) borasida ataylab qat'iy cheklovlar o'rnatgan: siz pointerlar ustida arifmetik amallarni bajara olmaysiz, *int32ni to'g'ridan-to'g'ri *int64 sifatida talqin qila olmaysiz va struct maydonlarining xotiradagi xom joylashuviga o'zboshimchalik bilan aralasha olmaysiz. Aynan shu cheklovlar garbage collector'ga xotirani tozalash vazifasini to'liq o'z zimmasiga olishiga imkon beradi: u dasturingizdagi har bir tirik ko'rsatkich tushunarli tipli havolalar zanjiri orqali yetib borilishi mumkinligiga to'liq ishonadi.
Biroq tizimli dasturlash tili ba'zan bu abstraksiyalarni ataylab buzishga majbur bo'ladi: masalan, standart kutubxonada yuqori optimallashtirilgan ma'lumotlar tuzilmasini yaratish, Go struct'ini tarmoq soketidan yoki xotiraga xaritalangan (mmap) fayldan kelgan bayt buferi ustiga to'g'ridan-to'g'ri proyeksiyalash yoki Go tip tizimi haqida hech qanday tushunchaga ega bo'lmagan C kodlarini chaqirish kerak bo'lganda. unsafe — til aynan mana shunday vaziyatlar uchun ajratgan qochish yo'lidir. U kundalik umumiy foydalanish uchun mo'ljallanmagan: standart kutubxona va ayrim yuqori unumdorlik talab qiluvchi paketlar har safar CGO yoki qo'lda yozilgan assembly'ga murojaat qilmasdan, xotirani nusxalash xarajatlarini chetlab o'tishi uchun undan foydalanadi. Go jamoasi unsafe oddiy ilovalarda deyarli uchramasligi kerakligini alohida ta'kidlaydi; agar siz uni oddiy loyihalarda muntazam import qilayotgan bo'lsangiz, bu sizning yuqori mahoratingiz belgisi emas, balki xavfsiz muqobillarni yetarlicha qidirmayotganingizdan dalolat beruvchi jiddiy signaldir.
unsafe.Pointer: istalgan tipga aylana oladigan universal ko'rsatkichOddiy Go ko'rsatkichi *T doimo o'zi ko'rsatayotgan T tipi haqida to'liq biladi. Kompilyator bu bilimdan to'g'ri mashina kodi yaratishda, garbage collector esa xotirani keyingi pointerlar uchun qanday skanerlashni aniqlashda foydalanadi. unsafe.Pointer esa butunlay boshqacha: u har qanday xotira manziliga ishora qila oladi va til uning uchun qat'iy chegaralangan qoidalar to'plamini belgilaydi. Go spetsifikatsiyasi bu borada favqulodda qat'iydir, chunki ozgina xatolik ham xotira buzilishiga (memory corruption) olib keladi:
*T ko'rsatkichi unsafe.Pointerga, har qanday unsafe.Pointer esa istalgan *Tga o'girilishi mumkin.unsafe.Pointerni uintptrga va aksincha uintptrni unsafe.Pointerga o'girish mumkin — ammo faqat quyida tushuntirilgan juda tor va qat'iy sharoitlarda, chunki uintptr garbage collector uchun ko'rsatkich emas, oddiygina butun sondir.unsafe paketining o'z yordamchi funksiyalari (Slice, SliceData, String, StringData, Go 1.17 va 1.20 da kiritilgan) xavfli pointer arifmetikasisiz xom xotira ustida slays va satrlarni xavfsiz yaratishning rasmiy usullarini taqdim etadi.unsafe.Pointer orqali ikki xil pointer tipi orasidagi konvertatsiya Go nuqtai nazaridan sintaktik jihatdan to'g'ri — kompilyator sizni to'xtatmaydi — ammo ular faqat ikkala tip xotirada aynan bir xil tuzilishga ega bo'lgandagina xatosiz ishlaydi. Eng klassik xato — kichikroq tipni kattaroq tip sifatida talqin qilishdir:
var x int32 = 42
ptr := unsafe.Pointer(&x)
y := (*int64)(ptr)
fmt.Println(*y) // 4 baytli qiymatdan 8 bayt o'qiydi — aniqlanmagan xatti-harakat (undefined behavior)
x o'zgaruvchisi xotirada 4 bayt egallaydi. Uni int64 sifatida o'qish o'sha 4 baytni va undan keyingi tasodifiy 4 baytni ham o'qib yuboradi — u yerda boshqa o'zgaruvchi, oraliq to'ldiruvchi (padding) yoki xaritalanmagan xotira bo'lishi mumkin. Kompilyator buni aniqlay olmaydi, chunki siz unga aniq aytdingiz: "bu manzilni int64 deb hisobla". Tip tizimining himoyasi unsafe.Pointer orqali o'tilgan zahoti o'z kuchini yo'qotadi; shu nuqtadan boshlab to'g'rilik uchun to'liq siz javobgar bo'lasiz.
Xotiradagi tuzilishi bir xil bo'lgan tiplar orasidagi o'girish esa mutlaqo qonuniy va xavfsizdir:
type Celsius float64
type Kelvin float64
func celsiusToKelvinBits(c *Celsius) *Kelvin {
return (*Kelvin)(unsafe.Pointer(c))
}
Bu kod bitlarni nusxalamasdan qayta talqin qiladi: u qiymatni matematik o'girmaydi (20 Celsius avtomatik Kelvinga aylanmaydi), balki bir xil 8 baytni boshqa nomlangan tip orqali ko'rish imkonini beradi xolos. Bitlarni qayta talqin qilish (reinterpret) bilan qiymatni konvertatsiya qilish (conversion) orasidagi farqni tushunib olish juda muhimdir.
Bitlarni qayta talqin qilish va qiymatni konvertatsiya qilish — ikki xil narsa:
unsafe.Pointerfaqat birinchisini bajaradi.
Sizeof, Alignof va Offsetof: kompilyatorning xotira joylashuvi bo'yicha qarorlarini o'rganishXom xotira bilan to'g'ri ishlash uchun Go struct maydonlarini xotirada qanday joylashtirishini bilishingiz kerak. Buning uchun unsafe paketi uchta maxsus funksiyani taqdim etadi:
unsafe.Sizeof(x) qiymat egallaydigan baytlar hajmini qaytaradi (faqat header hajmini hisobga oladi; masalan, string uchun uning pointeri va uzunligi o'lchanadi, satrning o'zi saqlanadigan xotira emas). unsafe.Alignof(x) tip talab qiladigan xotirani tekislash (alignment) chegarasini ko'rsatadi. unsafe.Offsetof(x.f) esa struct ichidagi f maydoni nechanchi baytdan boshlanishini (offset) aniqlaydi.
Struct joylashuvi tekislash (alignment) tufayli muhimdir: zamonaviy protsessorlar va Go ABI qoidalari ma'lum tiplarning o'z hajmiga karrali bo'lgan manzillarda joylashishini talab qiladi. Maydonlar tabiiy ravishda bu chegaralarga tushmaganda, kompilyator ular orasiga oraliq to'ldiruvchi baytlar (padding) kiritadi:
type Example struct {
A int8 // 1 bayt
B int64 // 8 bayt, 8 baytli hizalash kerak
C int8 // 1 bayt
}
fmt.Println(unsafe.Sizeof(Example{})) // 24, 10 emas
A 0-baytni egallaydi, ammo B 1-baytdan boshlana olmaydi: u 8 ga karrali manzildan boshlanishi shart, shuning uchun kompilyator 7 bayt bo'sh joy (padding) qoldiradi. So'ng B 8–15 baytlarni egallaydi. C 16-baytda turadi, ammo butun struct o'lchami eng katta maydon hizalanishiga (8 ga) karrali bo'lishi kerakligi sababli, yana 7 bayt qo'shilib, jami hajm 24 baytga yetadi.
Maydonlarni eng katta hizalanish talabidan eng kichigiga qarab tartiblash orqali bu isrofning oldini olish mumkin:
type Tight struct {
B int64 // 8 bayt
A int8 // 1 bayt
C int8 // 1 bayt
} // Sizeof == 16 — sakkiz bayt tejaldi, hech qanday unsafe talab qilinmadi!
Bu bilim unsafe paketini boshqa hech qachon ishlatmasangiz ham juda foydalidir: millionlab nusxada xotiradan ajratiladigan strukturalar — kesh qatorlari yoki qizg'in map yozuvlari — uchun maydonlar tartibini to'g'ri joylashtirish to'liq xavfsiz va qonuniy optimizatsiya hisoblanadi. go vet -fieldalignment tekshiruvi orqali xotirani tejash mumkin bo'lgan struct'larni avtomatik aniqlashingiz mumkin.
unsafening amaliyotda eng ko'p oqlanadigan o'rni — xotirada bir xil tuzilishga ega bo'lgan tiplar, xususan string va []byte orasida ortiqcha nusxalashlarsiz (zero-copy) ma'lumot almashishdir. Go 1.20 dan boshlab standart kutubxona buni rasmiy funksiyalar orqali taqdim etadi:
func BytesToString(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b))
}
func StringToBytes(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}
unsafe.String mavjud baytlarga to'g'ridan-to'g'ri ishora qiluvchi satr header'ini yaratadi; unsafe.Slice esa aksincha ishlaydi. Ikkalasi ham bir qatorli buyruq, ammo ular yuklaydigan mas'uliyat juda kattadir: Go tili qoidasiga ko'ra string o'zgarmas (immutable) bo'lishi shart va barcha paketlar satr baytlari hech qachon o'zgarmaydi degan qat'iy ishonch bilan ishlaydi. unsafe.String bu kafolatni buzadi. Agar siz satrni shu yo'l bilan hosil qilib, keyin uning tagidagi bayt slaysini o'zgartirsangiz, o'sha satrga tayangan boshqa barcha kodlar (masalan, map kaliti xeshlari yoki boshqa goroutinelar) buziladi. Bunday xatoliklar vaqtga bog'liq bo'lganligi sababli data race detector ularni har doim ham tuta olmasligi mumkin va natijada production muhitida tasodifiy, qayta takrorlanishi qiyin bo'lgan xotira buzilishlari yuzaga keladi.
Standart kutubxonadagi strings.Builder o'zining .String() metodida ichki jihatdan aynan mana shu nusxasiz usuldan xavfsiz foydalanadi. Shu sababli bu kabi funksiyalarni o'zingiz yozishingizga deyarli ehtiyoj qolmaydi. unsafe.String/unsafe.Slice funksiyalariga faqat profiling natijasida xotira ajratish haqiqatan tizim sekinlashuviga sabab bo'layotgani isbotlangan va o'zgarmaslik kafolatini to'liq o'zingiz nazorat qila oladigan tor doiralardagina murojaat qiling.
unsafe.Pointer nima uchun emas: pointer arifmetikasi va uintptrGo tilida unsafe.Pointer ustida to'g'ridan-to'g'ri qo'shish yoki ayirish amallarini bajarib bo'lmaydi (+ operatori uning uchun mavjud emas). Xotiradagi ixtiyoriy ofsetga siljish uchun uni uintptrga o'girish, arifmetik amalni bajarish va bitta yaxlit ifodada qaytadan unsafe.Pointerga aylantirish talab etiladi:
fieldPtr := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.Age))
"Bitta yaxlit ifodada" degan talab shunchaki chiroyli kod yozish qoidasi emas, balki hayotiy zaruratdir. Sababi, uintptr garbage collector nuqtai nazaridan oddiy butun sondir; GC uning ilgari ko'rsatkich bo'lganini yoki qaysidir obyektga ishora qilishini mutlaqo bilmaydi. Agar siz uintptr qiymatini alohida o'zgaruvchida saqlab tursangiz va uni qayta o'girmasingizdan oldin garbage collection sikli ishga tushsa, GC u ko'rsatib turgan xotira blokini "keraksiz" deb hisoblab, tozalab yuborishi yoki boshqa joyga ko'chirishi mumkin. Natijada siz yo'qotilgan xotiraga murojaat qilib qolasiz. Shuning uchun unsafe.Pointer → uintptr → arifmetika → unsafe.Pointer zanjiri hech qanday oraliq o'zgaruvchilarsiz, bitta ifoda ichida bajarilishi shart — shundagina kompilyator obyektni amallar tugaguncha tirik saqlaydi.
Go 1.26 dan boshlab 64-bitli tizimlarda xavfsizlik chorasi sifatida heap asosiy manzili tasodifiy tanlanadi (ASLR). Agar kodingiz bitta ifoda qoidasiga to'g'ri amal qilsa, bu yangilik unga xalaqit bermaydi. Ammo xotira manzillari barqaror turadi deb hisoblagan har qanday noto'g'ri yondashuv endi darhol buziladi.
Shu sababli, slays header'ini (pointer, len, cap) unsafe.Pointer orqali qo'lda buzib o'qish (eski reflect.SliceHeader va reflect.StringHeader) endi butunlay eskirgan (deprecated) va qat'iyan tavsiya etilmaydi. Ularning o'rniga zamonaviy unsafe.Slice va unsafe.String funksiyalaridan foydalanish lozim.
unsafe aslida qachon oqlanadiUshbu paketdan foydalanish o'rinli bo'lgan holatlar kamayish tartibida:
CGO orqali C kutubxonalari bilan ishlash. Go satrini *C.charga va aksincha o'girish muqarrar ravishda unsafe.Pointerni talab qiladi, chunki C tili Go satr va slayslarining ichki tuzilishini bilmaydi.
Standart kutubxonada mavjud bo'lmagan maxsus ma'lumotlar tuzilmalarini yaratish, masalan, lock-free ring buferlar, xotirani boshqaruvchi maxsus allokatorlar yoki xotiradagi binar protokollarga to'liq mos kelishi shart bo'lgan tuzilmalar. Ammo bu o'rinlarda ham avval encoding/binary paketini sinab ko'rish tavsiya etiladi.
Profiling orqali qattiq tekshirilgan va boshidan oxirigacha to'liq o'zingizga tegishli bo'lgan kodlarda nusxasiz konvertatsiyalar.
Boshqa barcha holatlarda — "bu tezroq ishlaydigandek tuyuldi", "chegaralarni tekshirishni chetlab o'tmoqchiman" kabi vajlar noto'g'ri. unsafe kod kompilyator tekshiruvidan mahrum bo'ladi, xotira xavfsizligi kafolatlarini yo'qotadi va Goning keyingi relizlarida kutilmaganda buzilib qolishi mumkin. Agar sizga haqiqatan unsafe kerak bo'lsa, uni alohida, kichik va batafsil izohlangan funksiyaga yashiring, uning xavfsizlik shartlarini doc comment'da yozing va ushbu shartlar buzilganda darhol xatolik beruvchi testlarni ta'minlang.
Go 1.27 dagi go fix vositasi yangi unsafefuncs modernizatorini o'z ichiga olgan bo'lib, xavfsiz standart muqobili paydo bo'lgan eski unsafe andozalarini avtomatik almashtirib beradi. Vaqti-vaqti bilan go fix ./... buyrug'ini yurgizib turish kod bazasini xavfsiz saqlashga yordam beradi.
Eng to'g'ri odat — shoshilmasdan avval tizimni profiler (pprof) orqali tahlil qilish va har qanday unsafe optimizatsiyani shaxsiy taxminlar bilan emas, aniq benchmark natijalari bilan asoslashdir. 57-bob aynan mana shunday o'lchovlarni qanday to'g'ri bajarishni o'rgatadi. Agar asosiy maqsad C tili bilan xotira almashish bo'lsa, buning uchun mo'ljallangan rasmiy vosita — cgo hisoblanadi va biz keyingi bobda aynan unga to'xtalamiz.