Cgo — Go tilining C kodlarini chaqirish mexanizmi bo'lib, undan foydalanish ikki xil tamoyilga asoslangan butunlay boshqa dunyolar o'rtasida eshik ochishga o'xshaydi. Bir tomonda — garbage collector har bir ko'rsatkichni kuzatadi, goroutinelar oz sonli operatsion tizim oqimlari (OS threads) bo'ylab yengil rejalashtiriladi va stack kerak bo'lsa dinamik o'sadi yoki qisqaradi. Ikkinchi tomonda esa — xotira to'liq qo'lda boshqariladi, tizim oqimlari og'ir va qat'iy, funksiya chaqiruvi esa toki yakunlangunga qadar oqimni to'liq bloklaydi. Cgo sizga bu ikki tizim o'rtasida ko'prik o'rnatish imkonini beradi, biroq chegaradan har bir o'tish sezilarli narxga (overhead) ega va bu eshikdan olib o'tilayotgan har bir ma'lumot ikkala tomonning qoidalariga to'liq mos bo'lishi talab etiladi. Ushbu bob cgo'dan foydalanish qachon o'zini oqlashi va qachon undan voz kechish kerakligini tushunishga bag'ishlanadi.
Cgo dasturda "C" psevdo-paketini import qilish orqali faollashtiriladi. Ushbu importdan bevosita oldin include direktivalari va C manba kodini o'z ichiga olgan maxsus izoh bloki joylashtiriladi:
/*
#include <stdio.h>
#include <stdlib.h>
void printMessage(char* msg) {
printf("%s\n", msg);
}
*/
import "C"
import "unsafe"
func main() {
msg := C.CString("Hello from C!")
defer C.free(unsafe.Pointer(msg))
C.printMessage(msg)
}
Build vaqtida cgo vositasi ushbu faylni tahlil qilib, Go va C chaqiruv qoidalari (calling conventions) o'rtasida vositachilik qiluvchi yopishtiruvchi (glue) kodlarni generatsiya qiladi va C qismlarini haqiqiy C kompilyatoriga (gcc yoki clang) topshiradi. Natijada Go qismlari bilan birga bitta binar faylga bog'lanadigan obyekt kodi hosil bo'ladi. Tizimda C toolchain mavjud bo'lsa, CGO_ENABLED=1 sukut bo'yicha yoqilgan bo'ladi; CGO_ENABLED=0 o'rnatish esa bularning barchasini o'chiradi va sof Go (pure Go) tilidagi, to'liq statik bog'langan binarni yig'ishni ta'minlaydi.
C tiplari Go kodida C. nom maydoni orqali chaqiriladi: C.int, C.char, C.size_t va inline izohlarda yoki ulangan header'larda e'lon qilingan barcha funksiyalar. Go va C satrlari/baytlari o'rtasida ma'lumot almashish muqarrar ravishda unsafe.Pointerdan foydalanishni talab qiladi, chunki C tili Go satrlarining header tuzilishini tushunmaydi: C.CString C xotirasida (heap) nol bayt (\0) bilan tugaydigan yangi nusxa ajratadi (uni C.free orqali tozalashga to'liq siz javobgarsiz), C.GoString esa C satrini Go boshqaruvidagi xavfsiz satrga nusxalab beradi.
Cgo haqida har qanday qaror qabul qilishda e'tiborga olinishi shart bo'lgan eng asosiy haqiqat shuki: Cgo chaqiruvi — oddiy funksiya chaqiruvi emas. C funksiyasini chaqirish paytida chaqiruvchi goroutine'ga tegishli OS oqimi Go runtime'idan vaqtinchalik ajratib olinishi kerak, toki u garbage collector va Go scheduleri ko'ra olmaydigan C kodini bajara olsin. Ushbu o'tish jarayoni — Go stack holatini saqlash, tizim (OS) stackiga o'tish, C funksiyasini bajarish va so'ng orqaga qaytish — an'anaviy ravishda o'nlab yoki yuzlab nanosekund vaqt oladi. Bu oddiy Go funksiya chaqiruvidan (ayniqsa u inline qilinganda nanosekunddan kam vaqt olishini hisobga olsak) taxminan 100 barobar qimmatroqdir. Oddiy HTTP so'rovida bir marta chaqiriladigan funksiya uchun bu qo'shimcha xarajat sezilmasligi mumkin, biroq millionlab sikl aylanadigan qizg'in oqimlarda bu ulkan to'siqqa aylanadi. Go 1.26 ushbu bazaviy xarajatni taxminan 30% ga qisqartirdi — bu cgo'ni intensiv chaqiruvchi dasturlar uchun ajoyib yutuq. Ammo bu faqat raqamni biroz kamaytiradi xolos, haqiqatni o'zgartirmaydi: chegarani kesib o'tish hamon oddiy Go chaqiruvidan o'nlab barobar qimmatroqdir.
Vaqt yo'qotilishi yagona xarajat emas. Goroutine C kodini bajarayotgan paytda, u band qilgan OS oqimi (thread) Go scheduleri uchun boshqa goroutinelarni bajarishga yaroqsiz bo'lib qoladi. C funksiyasi Go funksiyalari kabi xavfsiz nuqtalarda boshqaruvni topshirmaydi (yield qilmaydi). Go runtime'i buni qoplash uchun: agar C chaqiruvi uzoq vaqt bloklanib qolsa, f fondagi sysmon monitoringi buni payqab, boshqa goroutinelar navbatda qolib ketmasligi uchun yangi OS oqimini ishga tushiradi. Ammo ko'plab tizim oqimlarini ochish xotira va scheduling xarajatlarini keskin oshirib yuboradi.
Goning garbage collectori faqat Go tomonidan ajratilgan xotirani ko'radi va tozalaydi. U C kutubxonasi malloc yordamida ajratgan xotiradan mutlaqo bexabar. Agar dasturingiz C.mallocni chaqirsa — bevosita yoki bilvosita kutubxona orqali — ushbu xotirani qo'lda boshqarish to'liq sizning zimmangizda bo'ladi. Dasturning har qanday bajarilish yo'li, jumladan barcha xatolik holatlari ham o'z vaqtida C.freeni chaqirishi shart, aks holda tizimda xotira sizib chiqishi (memory leak) yuzaga keladi.
Aks yo'nalishdagi ma'lumot oqimi esa yanada nozik va xavflidir. Go ko'rsatkichini C kodiga uzatish Go runtime va kompilyatori tomonidan qat'iy nazorat qilinadi. Asosiy qoida shunday: Go kodi o'z ko'rsatkichini C'ga faqat chaqiruv davomida ishlatish sharti bilan uzatishi mumkin; C kodi chaqiruv tugaganidan keyin ham ushbu Go pointerini saqlab qolishi taqiqlanadi. Sababi, Go garbage collectori Go xotirasini chaqiruvlar orasida ko'chirishi yoki tozalab yuborishi mumkin va C kutubxonasi bu o'zgarishlardan xabardor bo'la olmaydi. Agar Go obyekti C tomonida uzoqroq yashashi kerak bo'lsa (masalan, C qabul qiluvchi callback konteksti uchun), standart cgo.Handle (runtime/cgo) mexanizmidan foydalanish lozim: u C'ga to'g'ridan-to'g'ri xotira ko'rsatkichini emas, balki Go tomonidan boshqariladigan shaffof butun sonli identifikatorni (handle) uzatadi.
Oddiy matematik siklni C tilida qayta yozish chaqiruv boshiga ketadigan xarajatlarni hisobga olganda deyarli hech qachon o'zini oqlamaydi. Ikkita sonni qo'shuvchi quyidagi misol buni yaqqol ko'rsatadi:
/*
static inline int addC(int a, int b) { return a + b; }
*/
import "C"
func addCgo(a, b int) int {
return int(C.addC(C.int(a), C.int(b)))
}
func addGo(a, b int) int {
return a + b
}
Benchmark tekshiruvida addGo kompilyator inline qilgandan so'ng 1 nanosekunddan kamroq vaqtda bajariladi; addCgo esa chegaradan o'tish xarajati sababli undan 100 barobar sekin ishlaydi. Bundan olinadigan asosiy saboq shuki: cgo faqat C funksiyasi ichida bajariladigan hisob-kitob hajmi chegarani kesib o'tish narxini oqlaydigan darajada katta bo'lgandagina ma'noga ega bo'ladi.
Cgo faqat C chaqiruvi ichidagi ish chegarani kesib o'tish xarajatini taqsimlash uchun yetarlicha katta bo'lgandagina o'zini oqlaydi.
#cgo noescape va nocallbackGo 1.24 da qo'shilgan ikkita #cgo direktivasi kompilyatorga muayyan C funksiyasi haqida qo'shimcha kafolatlar berish va natijada optimizatsiyalarni oshirish imkonini beradi:
/*
#cgo noescape addC
#cgo nocallback addC
static inline int addC(int a, int b) { return a + b; }
*/
import "C"
#cgo noescape cFunctionName direktivasi ushbu C funksiyasiga uzatilgan xotira ko'rsatkichi saqlanib qolmasligini (C tomoni uni chaqiruv tugagandan keyin ishlatish uchun yashirib qo'ymasligini) tasdiqlaydi. Bu orqali Go kompilyatori argumentlarni arzon stack xotirasida qoldirishga ruxsat oladi. #cgo nocallback cFunctionName esa ushbu C funksiyasi o'z ichidan hech qachon Go funksiyalarini qayta chaqirmasligini (callback qilmasligini) kafolatlaydi, bu esa qo'shimcha runtime mexanizmlarini tejaydi.
Ikkala direktiva ham kompilyatorga qat'iy kafolat berishdir, oddiy iltimos emas: agar C funksiyasi aslida ko'rsatkichni saqlab qolsa yoki Goga qaytib chaqirsa, dastur kompilyatsiya xatosi bermaydi, balki to'g'ridan-to'g'ri xotira buzilishi yoki crash holatiga uchraydi. Shuning uchun ularni faqat yaxshilab tekshirilgan hot-path C funksiyalaridagina qo'llash zarur.
Go muqobili mavjud bo'lmagan yirik, tayyor C kutubxonalari bilan ishlash. Masalan, faqat C mijoz kutubxonasiga ega ma'lumotlar bazasi drayverlari (klassik SQLite drayveri), ixtisoslashgan video/audio dekoderlar, apparat vositalari bilan ishlovchi vendor SDK'lari.
Standart kutubxonada mavjud bo'lmagan maxsus tizim chaqiruvlari. Garchi golang.org/x/sys ko'plab tizim chaqiruvlarini sof Goda taqdim etsa-da, ba'zi noyob holatlarda Cgo zaxira vosita sifatida kerak bo'ladi.
Mavjud C kodiga yirik donali (batch) chaqiruvlar. Bitta cgo chaqiruvi orqali har bir kichik bayt uchun alohida emas, balki butun boshli hujjatni tahlil qilish, videokadrlarni kodlash yoki katta massivlarni qayta ishlash.
Cgo'ga murojaat qilishdan oldin quyidagi variantlarni albatta ko'rib chiqing:
Sof Go implementatsiyasi allaqachon mavjud. Masalan, net/http va crypto/tls aksariyat holatlarda C'dagi libcurl va OpenSSL o'rnini to'liq va xavfsiz bosa oladi.
Kimdir kutubxonani allaqachon Goga port qilgan. Masalan, mattn/go-sqlite3 (cgo talab qiladi) o'rniga C kodidan to'g'ridan-to'g'ri Go tiliga kompilyatsiya qilingan modernc.org/sqlite kutubxonasidan foydalanish mumkin — bu cgo'siz toza statik binar hosil qilish imkonini beradi.
Sof Go FFI kutubxonalari. github.com/ebitengine/purego kabi vositalar dinamik C kutubxonalarini (.so, .dylib, .dll) cgo talab qilmasdan, sof Go runtime orqali dinamik yuklash imkonini beradi.
Alohida jarayon sifatida ishga tushirish (IPC). Kamdan-kam chaqiriladigan C dasturlarini os/exec orqali alohida jarayonda ishga tushirib, ma'lumotlarni quvurlar (pipes) yoki soketlar orqali uzatish — cgo xatarlaridan to'liq xalos qiladi.
C kodini Go tilida qayta yozish. Agar algoritm unchalik katta bo'lmasa (masalan, xeshlash, CRC tekshiruvi, ma'lumot formati parseri), uni sof Go tilida qayta yozish ko'pincha cgo chaqiruvlarining ortiqcha yukidan xoli bo'lganligi sababli ancha tezroq ishlaydi.
Agar loyihangizda cgo ishlatish shart bo'lsa, quyidagi tavsiyalarga amal qiling:
Cgo kodini alohida paketga izolyatsiya qiling. Barcha import "C" chaqiruvlarini bitta kichik paket ichida saqlang va tashqariga oddiy Go interfeyslarini taqdim eting. Dasturning qolgan qismi cgo ishtirok etayotganidan bexabar bo'lishi kerak.
Chaqiruvlarni guruhlang (batching). Har bir element uchun alohida C chaqiruvi qilish o'rniga, butun massivni bitta chaqiruvda C'ga uzating.
Imkon bo'lsa CGO_ENABLED=0 uchun zaxira (fallback) yarating. //go:build cgo va //go:build !cgo build teglari orqali cgo bo'lmagan holatlar uchun sof Go implementatsiyasini taqdim eting — bu kross-kompilyatsiya qulayligini saqlab qoladi.
C xatoliklarini Go idiomsiga chegaraning o'zidayoq o'giring. errno yoki C status kodlarini wrapper funksiyaning o'zidayoq tekshirib, standart Go error qiymatiga aylantiring.
Cgo nima sababdan tanlanganini hujjatlashtiring. Paket hujjatida qaysi C kutubxonasi nima sababdan ishlatilgani va uni yig'ish uchun qanday tizim bog'liqliklari kerakligi aniq yozib qo'yilishi keyingi dasturchilarni ortiqcha qiyinchiliklardan asraydi.
Keyingi bobda dasturiy ta'minot unumdorligini oshirish bo'yicha eng muhim mavzuga to'xtalamiz: har qanday optimizatsiyani (xoh u cgo, xoh unsafe yoki algoritmik o'zgarish bo'lsin) taxminlar bilan emas, pprof va benchmarklar orqali qanday to'g'ri o'lchash va tahlil qilish kerakligini ko'rib chiqamiz.