03 / 73

Go kodi qanday tashkil qilinadi

Har bir til bir xil asosiy tashkiliy savolga javob berishi kerak: kompilyator qaysi fayllar bir-biriga tegishli ekanini qanday biladi va kodning bir bo'lagi boshqasiga qanday murojaat qiladi? Goning javobi uchta ichma-ich joylashgan tushunchadan quriladi — paketlar, modullar va repozitoriylar. Ularning o'zaro bog'liqligini tushunib olsangiz, keyinchalik ancha chalkashlikdan qutulasiz, chunki Go bilan kundalik ishdagi hayratlanarli darajada ko'p noqulaylik (import xatolari, ko'rinuvchanlik savollari, "nega buni bu yerdan ishlata olmayapman") aynan shu uch qatlam haqidagi noaniq tasavvurdan kelib chiqadi.

Paketlar: Go aslida kompilyatsiya qiladigan birlik

Paket — Goning kompilyatsiya va nom fazosi (namespacing) uchun asosiy birligi. Har bir .go fayl (alohida paket sifatida nomlanishi mumkin bo'lgan test fayllaridan tashqari) eng birinchi qatorida qaysi paketga tegishli ekanini e'lon qiladi, va bitta papkadagi har bir fayl bir xil paketga tegishli bo'lishi shart — Go bir nechta turli paketlarning bitta papkada aralashib yotishiga yo'l qo'ymaydi.

Bu ko'pchilik tillarga qaraganda qattiqroq qoida: boshqa tillarda bir papkada bir-biriga aloqasi yo'q bir nechta klass turishi mumkin. Goda esa papkaning o'zi — paket.

example.go
// fayl: math/calculator.go
package math

// Add a va b ning yig'indisini qaytaradi.
func Add(a, b int) int {
	return a + b
}

// subtract eksport qilinmagan: uni faqat shu paket ichidagi kod chaqira oladi.
func subtract(a, b int) int {
	return a - b
}

Paket nomlari odatiy tarzda qisqa, kichik harfli va pastki chiziqsiz bo'ladi — http, json, strconv, lekin HTTPClient yoki json_utils emas. Nom odatda import yo'lining oxirgi bo'lagiga mos keladi, garchi bu kompilyator majburlaydigan qoida emas, shunchaki kelishuv bo'lsa ham.

Goda ko'rinuvchanlik butunlay bosh harf bilan yozilishiga qarab hal qilinadi — bu public/private kalit so'zlari bor tillardan kelganlarni hayratga soladi. Bosh harfli Add eksport qilingan: u math ni import qilgan har qanday paketga ko'rinadi. Kichik harfli subtract eksport qilinmagan: uni faqat math paketining ichidagi kod, shu jumladan o'sha paketning o'z test fayllari ishlata oladi. Bu qoida funksiyalar, tiplar, struct maydonlari hamda paket darajasidagi o'zgaruvchi va konstantalar uchun bir xilda amal qiladi.

Qisman ko'rinuvchanlik yo'q, "protected" yo'q, do'st klasslar yo'q — faqat eksport qilingan yoki qilinmagan, va buni bitta harfning registri hal qiladi.

Modullar: paketlarni versiyalar bilan birlashtiradigan narsa

Modul — birga kuzatiladigan, birga versiyalanadigan va daraxt ildizidagi bitta go.mod fayli bilan tavsiflanadigan bir yoki bir nechta paket. go.mod fayli qisqa, ammo haqiqiy ish bajaradi:

matn
module github.com/username/myproject

go 1.23

require (
	github.com/some/dependency v1.2.3
)

module qatori modulning o'z import yo'lini e'lon qiladi — bu boshqa kod undan paketlarni import qilish uchun ishlatadigan satr. Kuchli kelishuvga ko'ra u modulning haqiqiy repozitoriy manzili bilan mos keladi (github.com/username/myproject), va aynan shu narsa go get ga alohida paket reyestriga chiqarishni talab qilmasdan, kodni to'g'ridan-to'g'ri versiya nazorati tizimidan olib kelish imkonini beradi.

Goning butun bog'liqliklar modeli manba kodini joylashtirish va paketga nom berish bitta narsa ekaniga tayanadi.

go qatori modul talab qiladigan minimal til versiyasini yozib qo'yadi, require bloki esa to'g'ridan-to'g'ri bog'liqliklarni va modul qaysi versiyalar bilan yig'ilib sinalganini sanab o'tadi.

Bitta modulga tegishli papkalar daraxti odatda shunday ko'rinadi:

papkalar
myproject/
├── go.mod
├── main.go
├── calculator/
│   └── calc.go
└── utils/
    └── helpers.go

Bu yerdagi .go fayllari bor har bir ichki papka o'zicha alohida paket (ildizdagi main paketi bilan bir qatorda calculator va utils), ammo ularning barchasi yagona myproject modulining bir qismi bo'lib, birga versiyalanadi va birga yuklab olinadi.

Repozitoriylar: odatda bitta modul, ba'zan ko'proq

Repozitoriy — aksariyat hollarda Git repozitoriysi — ko'pincha aynan bitta modulni o'z ichiga oladi, va agar sizda boshqacha qilish uchun aniq sabab bo'lmasa, standart holda shu tartibni tanlang. Repozitoriyni bir nechta mustaqil versiyalanadigan modullarga ajratish ("ko'p modulli repozitoriy") — turli reliz jadvaliga muhtoj, chindan ham mustaqil komponentlari bor yirik loyihalar uchun o'rinli yondashuv, ammo u haqiqiy noqulaylik keltiradi: endi bitta repozitoriy ichida modullararo o'zgarish kiritish ishlab chiqish paytida versiyalarni sinchkovlik bilan ko'tarishni va replace direktivalarini talab qiladi.

Ajratmaslikning og'rig'i bir nechta go.mod faylini boshqarish og'rig'idan aniq oshib ketmaguncha, har bir repozitoriyga bitta modul tamoyiliga amal qiling.

Joylashuv kelishuvi: cmd, internal, pkg

Go ba'zi frameworklar kabi papkalar tuzilishini majburlamaydi, ammo ekotizim bo'ylab amalda shakllangan kelishuv bor, va u shu qadar kuchliki, sababsiz undan chetga chiqsangiz, tajribali Go o'quvchilari to'xtab qoladi:

papkalar
project/
├── go.mod
├── go.sum
├── cmd/
│   ├── server/
│   │   └── main.go
│   └── worker/
│       └── main.go
├── internal/
│   └── auth/
│       └── user.go
├── pkg/
│   └── client/
│       └── client.go
└── testdata/
    └── sample.json

cmd/ modulingiz ishlab chiqaradigan har bir bajariladigan dastur uchun bitta ichki papkani saqlaydi, ularning har birida o'z package main va main() funksiyasi bo'ladi. Bu loyihaga bittadan ko'p ikkilik fayl kerak bo'lgan paytda ahamiyat kasb etadi — masalan, server va unga hamroh CLI — chunki har biriga o'z kirish nuqtasi kerak, lekin ular mantiqning asosiy qismini internal paketlar orqali birga ishlatadi.

internal/ shunchaki kelishuv emas, uni kompilyatorning o'zi majburlaydi. internal deb nomlangan papka ostidagi har qanday paketni faqat o'sha internal papkasigacha (uni ham qo'shib) bir xil yo'l prefiksiga ega kod import qila oladi — amalda bu sizning o'z modulingiz ichidagi boshqa kod degani.

Bu — tashqi iste'molchilar bog'lanib qolishini istamagan ichki tafsilotlar atrofiga qattiq chegara chizish uchun Go beradigan mexanizm, va u nom berish kelishuvlariga yoki odamlar e'tiborsiz qoldirishi mumkin bo'lgan (va oxir-oqibat qoldiradigan) hujjat ogohlantirishlariga tayanmaydi.

pkg/ (yoki umuman o'rab turuvchi papkasiz, shunchaki yuqori darajadagi paketlar) — boshqa modullar import qilishini xohlagan kod uchun. pkg/ papkasidan umuman foydalanish kerakmi degan savol Go hamjamiyatida haqiqiy uslub bahsi hisoblanadi — standart kutubxonaning o'zi undan foydalanmaydi — ammo u ilova repozitoriylarida shu qadar ko'p uchraydiki, siz uni doimo ko'rasiz.

testdata/ asboblar to'plamida alohida holat sifatida qaraladi: go buyrug'i build paytida uni e'tiborsiz qoldiradi, lekin test kodi undagi fayllarni bemalol o'qiy oladi. Shu sababli u fixture, etalon (golden) fayllar va namunaviy kirish ma'lumotlari uchun odatiy joy hisoblanadi.

main paketi va build aslida qanday hal qilinadi

main paketi — bajariladigan dasturning kirish nuqtasi joylashgan joy, va u boshqa har qanday paketdan ikki jihati bilan farq qiladi: uni hech narsa import qila olmaydi (bajariladigan dasturni "import qilish"ning ma'noli usuli yo'q) va umuman ikkilik fayl hosil qiladigan narsa aynan uni build qilishdir — boshqa har qanday paketni build qilish shunchaki build keshini kompilyatsiya qilingan obyekt kodi bilan to'ldiradi, ishchi papkangizga esa hech narsa chiqmaydi.

example.go
package main

import (
	"fmt"

	"github.com/username/myproject/pkg/client"
)

func main() {
	c := client.New()
	fmt.Println(c.Fetch())
}

go build ni ishga tushirganingizda kompilyator duch keladigan har bir import uchun oldindan aytib bo'ladigan tartibda ish ko'radi: avval standart kutubxona (fmt, net/http va shu kabilar — ular asboblar to'plamining o'zi bilan keladi va hech qachon tarmoqqa murojaat qilmaydi), so'ng go.mod da sanab o'tilgan narsalar uchun modullar keshi, keyin, agar siz go.work ish maydoni ichida bo'lsangiz, o'sha ish maydoni aniq nomlagan lokal modullar, va nihoyat, agar build -mod=vendor bilan chaqirilgan bo'lsa, vendor/ papkasi. Bitta paket ichida initsializatsiya qat'iy va aniq belgilangan tartibda kechadi: avval paket darajasidagi o'zgaruvchilar bog'liqlik tartibida initsializatsiya qilinadi (boshqasiga bog'liq o'zgaruvchi undan keyin initsializatsiya qilinadi, manba kodidagi tartibdan qat'i nazar), so'ng init() funksiyalari o'z fayllari tartibida ishga tushadi (fayl nomi bo'yicha alifbo tartibida, har bir fayl ichida esa yuqoridan pastga), va faqat bularning barchasi tugagandan keyingina main() ishga tushadi.

Bu eshitilganidan ko'ra muhimroq: bir necha faylga sochilgan, har biri umumiy holatni o'zgartiradigan init() funksiyalari bor paket — olti oydan keyin hech kim tushuna olmaydigan Go dasturi yozishning eng ishonchli usullaridan biri, shuning uchun tajribali Go jamoalari init() dan kam foydalanadi va main dan chaqiriladigan aniq sozlashni afzal ko'radi.

Build cheklovlari: turli maqsadlar uchun turli kodni kompilyatsiya qilish

Ba'zan sizga faqat muayyan operatsion tizim yoki arxitektura uchun kompilyatsiya qilinadigan kod kerak bo'ladi — masalan, faqat Linux'da ma'no kasb etadigan syscall o'rami. Go buni ikki usulda qo'llab-quvvatlaydi. Fayl nomi qo'shimchasi kelishuvi — soddaroq va ko'proq uchraydigani: thing_linux.go deb nomlangan fayl faqat GOOS=linux uchun mo'ljallangan build'larga qo'shiladi, thing_windows.go faqat Windows uchun, thing_amd64.go esa faqat amd64 arxitekturasi uchun; bu qo'shimchalarni birlashtirish mumkin (thing_linux_amd64.go). Operatsion tizim yoki arxitekturadan aniqroq shart kerak bo'lsa, faylning eng yuqorisidagi build-teg izohi sizga ixtiyoriy mantiqiy shartlar beradi:

example.go
//go:build linux && amd64

package main

//go:build izohidan keyin va package e'lonidan oldin bo'sh qator turishi shart — asboblar to'plami bunga qattiq rioya qiladi, va bu "nega mening build tegim ishlamayapti" degan chalkashlikning keng tarqalgan manbasi.

Hammasini birgalikda ko'rish

Haqiqiy hayotdagi kichik xizmat bu g'oyalarning har birini bir joyga bog'laydi:

papkalar
todoapp/
├── go.mod
├── go.sum
├── cmd/
│   └── todo/
│       └── main.go          // package main
├── internal/
│   ├── db/
│   │   └── connection.go    // package db — bu moduldan tashqarida import qilib bo'lmaydi
│   └── models/
│       └── task.go          // package models
└── pkg/
    └── api/
        ├── server.go        // package api — boshqa modullar import qila oladi
        └── handlers.go

cmd/todo ikkilik fayli internal/db va internal/models ga bog'liq — bular faqat shu modul ko'ra oladigan ichki xususiy kod — va o'zining HTTP API interfeysini pkg/api orqali taqdim etadi, uni boshqa tashqi modullar mijoz kutubxonasi sifatida import qila oladi.

"Faqat bizning modul ichida ishlatiladigan kod" bilan "tashqi ochiq interface sifatida taqdim etiladigan kod" o'rtasidagi bu qat'iy chegara — Goning paket tizimi hech qanday ortiqcha murakkabliksiz joriy qiladigan eng qimmatli arxitektura qoidalaridan biridir.

Shu tuzilmani yodda tutgan holda — paketlar kod va ko'rinuvchanlik birligi sifatida, modullar bog'liqlik va versiyalash birligi sifatida, cmd/internal/pkg kelishuvi esa ularni diskda joylashtirishning qabul qilingan usuli sifatida — siz bularning barchasi ustida ishlaydigan vositalar bilan tanishishga tayyorsiz. Keyingi bobda har kuni o'nlab marta ishlatiladigan asosiy buyruqlar ko'rib chiqiladi: go run, go build, go test va go doc.