Go sehrga (magic) qat'iy shubha bilan qaraydi. Unda makro tizimi yo'q, annotation processor yo'q, kodingizni orqangizdan qayta yozadigan metadasturlash qatlami yo'q. Shunga qaramay, standart toolchain bu cheklovni aslida buzmasdan chetlab o'tishga imkon beradigan ikkita imkoniyat bilan keladi: go generate — haqiqiy build boshlanishidan oldin oddiy Go manba fayllarini hosil qilish uchun oddiy dasturlarni ishga tushiradi, va embed paketi — kompilyatorga ixtiyoriy fayllarni Go qiymatlari kabi binar ichiga joylashtirishga imkon beradi. Ikkalasi ham tilni egmaydi. Ikkalasi ham inson aks holda qo'lda qiladigan ishni — shablon kod yozish yoki resursni binar yoniga ko'chirib, jo'natishni unutmaslikni — avtomatlashtiradi. Har biriga qachon murojaat qilishni va qachon yaxshini yomon qilib qo'ymaslikni bilish — birinchi marta yozganingizdan keyingi yillarda Goni zerikarli va tushunarli saqlashning bir qismi.
go generate: qo'lda yozadigan shablon kodni avtomatlashtirishgo generate build bosqichi emas. U hech qachon go build yoki go testning bir qismi sifatida avtomatik ishlamaydi va kompilyator uning mavjudligini bilmaydi. Bu shunchaki kelishuv: manba fayllaringizni ma'lum shakldagi izohlarni qidirib ko'rib chiqadigan skaner, va siz go generateni aniq ishga tushirganingizda u har bir izoh nomlagan buyruqni bajaradi. Bu buyruq odatda yangi .go fayl yozadi, uni siz xuddi o'zingiz yozgandek oddiy kompilyatsiya qilasiz.
Izoh chekinishsiz //go:generate bilan boshlanadigan qator bo'lishi va darhol shell uslubidagi buyruq bilan davom etishi kerak:
//go:generate stringer -type=Status
package main
type Status int
const (
Pending Status = iota
Active
Completed
Cancelled
)
go generate ./...ni ishga tushirish joriy katalog ostidagi har bir paketni skanerlaydi, bu direktivani topadi va stringer -type=Statusni shell orqali chaqiradi. stringer — kichik dastur (standart kutubxonaning bir qismi emas): u paketingizni go/types orqali tekshiradi, Status tipidagi har bir nomlangan konstantani aniqlaydi va Statusga String() string metodini beradigan fayl yozadi. Go 1.24 dan oldin stringerni PATHga olish go install golang.org/x/tools/cmd/stringer@latest degani edi — modulingizga bog'lanmagan, "suzib yuradigan" versiya, ehtimol har bir hissa qo'shuvchining mashinasida va CI'da boshqacha. Go 1.24 dan uning uchun yaxshiroq joy — tool direktivasi (7-bob mexanizmni to'liq yoritadi): go get -tool golang.org/x/tools/cmd/stringer aniq versiyani go.modning o'ziga yozadi va go tool stringer -type=Status (yoki qisqacha, direktivaning o'zida //go:generate go tool stringer -type=Status) aynan o'sha mahkamlangan versiyani ishga tushiradi. Alohida o'rnatilgan binar yo'q va generator kimningdir mashinasida CI ishlatganidan yangiroq relizga jimgina siljib ketish xavfi ham yo'q:
// Code generated by "stringer -type=Status"; DO NOT EDIT.
package main
import "strconv"
func _() {
var x [1]struct{}
_ = x[Pending-0]
_ = x[Active-1]
_ = x[Completed-2]
_ = x[Cancelled-3]
}
const _Status_name = "PendingActiveCompletedCancelled"
var _Status_index = [...]uint8{0, 7, 13, 22, 30}
func (i Status) String() string {
if i < 0 || i >= Status(len(_Status_index)-1) {
return "Status(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Status_name[_Status_index[i]:_Status_index[i+1]]
}
Indeks ifodalari bilan bu g'alati func _() ustida to'xtalib o'tish kerak, chunki bu zerikarli, ishonchli kod uchun qilingan kichik aqlli hiyla: u konstantalarni qayta generatsiya qilmasdan qayta tartiblasangiz yoki qayta raqamlasangiz, fayl kompilyatsiyadan o'tmasligi uchun mavjud — x[Pending-0] chegaradan tashqari konstanta indeksga aylanadi — runtime'da jimgina noto'g'ri satrni qaytarish o'rniga. Boshqacha qilib aytganda, generatsiya qilingan kod eskirgan zahoti baland ovozda muvaffaqiyatsiz bo'lishga qattiq urinadi.
Metod joyida bo'lgach, fmt (va %v yoki %s ishlatadigan har qanday narsa) uni avtomatik oladi:
status := Active
fmt.Println(status) // "Active", "1" emas
stringer — kanonik misol, lekin mexanizm umumiy maqsadli. go:generate istalgan narsani ishga tushiradi — mock generator, parser generator, protobuf kompilyatori yoki o'zingiz yozgan text/templatega asoslangan generatorni chaqirishdan bitta direktiva nariga turibsiz:
//go:generate stringer -type=Status
//go:generate mockgen -source=service.go -destination=service_mock.go
//go:generate protoc --go_out=. --go_opt=paths=source_relative user.proto
mockgen (go.uber.org/mockdan, eskiroq golang/mockning qo'llab-quvvatlanadigan vorisi) interface ta'rifini o'qiydi va uni amalga oshiradigan, chaqiruvlarni yozib olib, ular haqida tasdiqlaydigan tip chiqaradi — haqiqiy implementatsiyani ko'tarmasdan interface'ga bog'liq kodni testlash uchun foydali:
//go:generate mockgen -source=user.go -destination=user_mock.go
type UserService interface {
GetUser(id int) (*User, error)
CreateUser(user *User) error
}
mockUserService := NewMockUserService(mockCtrl)
mockUserService.EXPECT().GetUser(1).Return(&User{ID: 1}, nil)
Go plagini bilan protoc .proto sxema fayllarini tipli Go struct'lariga va (de)serializatsiya kodiga aylantiradi. Bu yerdagi generator hatto stringer ma'nosida Godan xabardor emas: u tasodifan Go backend'iga ega bo'lgan tillararo vosita.
O'zingizning generatoringizni text/template va os.Createdan boshqa hech narsasiz ham yozishingiz mumkin:
//go:generate go run gen.go
package main
// gen.go
package main
import (
"os"
"text/template"
)
var tmpl = template.Must(template.New("gen").Parse(`
package {{.Package}}
// Code generated by gen.go; DO NOT EDIT.
const (
{{range .Constants}} {{.Name}} = {{.Value}}
{{end}})
`))
func main() {
data := struct {
Package string
Constants []struct{ Name, Value string }
}{
Package: "main",
Constants: []struct{ Name, Value string }{
{"MaxUsers", "100"},
{"Timeout", "30"},
},
}
f, err := os.Create("generated.go")
if err != nil {
panic(err)
}
defer f.Close()
if err := tmpl.Execute(f, data); err != nil {
panic(err)
}
}
Bu yerda til sizga allaqachon bermagan hech narsa yo'q — kichik main paketi, shablon, fayl yozish — aynan shu asosiy nuqta. Goda kod generatsiyasi kamdan-kam framework talab qiladi; u go/ast, go/types yoki text/templateni hamda chiqishni deterministik va generatorni idempotent qilish intizomini talab qiladi.
Bir nechta konvensiyani erta o'zlashtirish kerak, chunki ular zerikarli qoladigan generatsiya qilingan kodni texnik xizmat ko'rsatish tuzog'iga aylanadigan koddan ajratadi:
Uni aniq belgilang. Fayl tepasiga yaqin so'zma-so'z paydo bo'ladigan // Code generated by ... ; DO NOT EDIT. qatori gofmt, ko'plab muharrirlar va generatsiya qilingan fayllarni standart holatda o'tkazib yuboradigan golangci-lint kabi vositalar tomonidan taniladi. Matnni shu andozaga yaqin qiling, va vositalar faylga to'g'ri munosabatda bo'ladi; chetga chiqsangiz, o'zingizni yo mashina chiqishini lint qilayotgan, yoki yomonrog'i, uni tahrirlayotgan va generatorni keyingi qayta ishga tushirganda tuzatishingiz jimgina bekor qilinadigan holatda topasiz.
Generatsiya qilingan fayllarni commit qilish-qilmaslikni ataylab hal qiling. Ikkala javob ham asosli va jamoalar bu borada kelishmaydi. Commit qilish har kim avval stringer, mockgen yoki protocni o'rnatmasdan go build qila olishini va diff'lar sxema yoki tip o'zgarishi aynan nima hosil qilganini ko'rsatishini anglatadi — review'da tasodifiy xatti-harakat o'zgarishini tutish uchun foydali. Commit qilmaslik esa repozitoriy generatorlaridan sinxronlikdan chiqib ketolmasligini anglatadi, evaziga har bir hissa qo'shuvchi generator toolchain'ini o'rnatgan bo'lishi va build ishlashidan oldin go generateni ishga tushirishi kerak. Kutubxonalar uchun — kodingizni build qiladigan iste'molchilar butun generator toolchain'ingizga muhtoj bo'lmasligi kerak — commit qilish deyarli talab. go generateni ishga tushiradigan va har qanday natijaviy diff'da build'ni muvaffaqiyatsiz qiladigan intizomli CI quvuriga ega ichki ilovalar uchun esa commit qilmaslik ko'pincha tozaroq.
Build tag'larni tejamkorlik bilan, faqat generator kodini to'sish uchun ishlating, generatsiya qilingan chiqishni emas:
//go:build generate
//go:generate stringer -type=Status
package main
go generate -tags generate
Bu andoza direktivani oddiy go generate ./... supurishi paytida ishga tushishdan saqlaydi, generator qimmat bo'lganda yoki tasodifan yuz berishini xohlamaydigan tarmoq chaqiruvini talab qilganda foydali.
Generator nima hosil qilganini testlang, unga shunchaki ishonmang. stringer hosil qilgan String() metodi haqiqiy muvaffaqiyatsizlik rejimiga (chegara tekshiruvi) ega kichik funksiya va u har qanday boshqa funksiya oladigan jadvalga asoslangan testga loyiq:
func TestStatusString(t *testing.T) {
tests := []struct {
status Status
want string
}{
{Pending, "Pending"},
{Active, "Active"},
{Completed, "Completed"},
}
for _, tt := range tests {
if got := tt.status.String(); got != tt.want {
t.Errorf("Status(%d).String() = %q, want %q", tt.status, got, tt.want)
}
}
}
embed: fayllarni binarning ichiga tortishgo:generate "buni qo'lda yozishni xohlamayman" muammosini hal qiladi. Go 1.16 da qo'shilgan embed esa boshqasini hal qiladi: "menda fayllar bor — HTML shablonlar, standart konfig, logotip, ma'lumotlar bazasi migratsiyalari — ular dasturimga ishga tushirish vaqtida kerak, va men binar va u bilan birga yurishi, yo'qolishi yoki sinxronlikdan chiqishi mumkin bo'lgan resurslar katalogi o'rniga bitta binar jo'natishni afzal ko'raman."
Mexanizm go:generatega o'xshab ko'rinadi — sehrli izoh — lekin buni tashqi vosita emas, kompilyatorning o'zi tushunadi:
package main
import (
_ "embed"
"fmt"
)
//go:embed version.txt
var version string
Kompilyatsiya vaqtida version.txt mazmuni (u manba fayl yonida yoki unga nisbatan kichik katalogda mavjud bo'lishi kerak) o'qiladi va version o'zgaruvchisiga, kompilyatsiya vaqti konstantasiga o'xshash initsializator sifatida "pishiriladi". Runtime'da fayl tizimiga kirish yo'q va fayl keyinroq diskdan o'chirilsa, hech narsa noto'g'ri ketmaydi — u allaqachon binar ichida. embedning blank import'iga e'tibor bering: string yoki []bytega qaratilgan //go:embed kodingizda embed paketiga nomi bilan murojaat qilmasa ham, kompilyator direktiva qonuniy ekanini bilishi uchun import baribir kerak; usiz go vet (va go build) direktivani hech narsaga biriktirilmagan izoh sifatida rad etadi.
Bir nechta fayl uchun, yoki fayllar to'plamini oldindan bilmasangiz, embed.FSga embed qiling — io/fs.FSning faqat o'qish uchun, xotiradagi implementatsiyasi:
package main
import (
"embed"
"fmt"
)
//go:embed file1.txt file2.txt
var files embed.FS
func main() {
data1, _ := files.ReadFile("file1.txt")
data2, _ := files.ReadFile("file2.txt")
fmt.Println(string(data1), string(data2))
}
Glob andoza butun katalog daraxtini embed qiladi:
//go:embed templates/*
var templates embed.FS
embed.FS fs.FSni qanoatlantirgani uchun u shu interface atrofida yaratilgan io/fsdagi hamma narsa bilan kompozitsiya qilinadi: fayllarni sanash uchun fs.WalkDir, ularni filtrlash uchun fs.Glob va, ayniqsa, net/http allaqachon xizmat ko'rsatishni biladigan narsaga moslashtirish uchun http.FS:
//go:embed static/*
var staticFiles embed.FS
func main() {
fsys := http.FS(staticFiles)
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(fsys)))
http.ListenAndServe(":8080", nil)
}
Bu bitta direktiva va ikki qatorli andoza deployment xatolarining butun bir kategoriyasini yo'q qiladi: sizning mashinangizda static/ katalogi ish katalogida bo'lib qolgani uchun ishlaydigan, production'da esa ish katalogi boshqa bo'lgani, deploy bosqichi resurslarni ko'chirishni unutgani yoki Dockerfiledagi COPY qatori pastki papkani o'tkazib yuborgani uchun ishlamaydigan server. Agar u build bo'lgan bo'lsa, resurslar uning ichida yashaydi.
Xuddi shu hiyla aynan shu uchun qurilgan ParseFS konstruktoriga ega html/template uchun ham ishlaydi:
//go:embed templates/*.html
var templateFS embed.FS
var tmpl = template.Must(template.ParseFS(templateFS, "templates/*.html"))
func handler(w http.ResponseWriter, r *http.Request) {
tmpl.ExecuteTemplate(w, "index.html", map[string]string{
"Title": "Welcome",
"Name": "Alice",
})
}
va SQL migratsiyalari uchun: bunda katalog yozuvlarini embed-e'lon qilingan tartibda o'qish va ularni ketma-ket qo'llash o'zingiz yozgan migrator uchun keng tarqalgan andoza:
//go:embed migrations/*.sql
var migrations embed.FS
func runMigrations(db *sql.DB) error {
entries, err := migrations.ReadDir("migrations")
if err != nil {
return err
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
sql, err := migrations.ReadFile("migrations/" + entry.Name())
if err != nil {
return err
}
if _, err := db.Exec(string(sql)); err != nil {
return fmt.Errorf("migration %s: %w", entry.Name(), err)
}
}
return nil
}
(Bu loyihaning o'z migratsiyalari, ma'lumot uchun, xom embed qilingan SQL emas, gormigrate orqali ishlaydi — lekin embed qilish andozasi ORM darajasidagi migratsiya kutubxonasiz loyiha xuddi shu narsani ishga tushirishning usulidir.)
Turli build'lar turli resurslarni talab qilganda build tag'lar va embed toza birikadi — masalan, ishlab chiqish konfigi va production konfigi:
//go:build !dev
//go:embed production-config.json
var config embed.FS
//go:build dev
//go:embed dev-config.json
var config embed.FS
go build # production-config.json ni embed qiladi
go build -tags dev # dev-config.json ni embed qiladi
Bu imkoniyatning bitta nozik jihati bor, va u aslida Goning umumiy "binarlarni oqilona saqlang" falsafasining bitta aniq holatga qo'llanishi: embed qilgan hamma narsa kompilyatsiya qilingan artefaktning bir qismiga aylanadi, doimiy ravishda, har bir build uchun. //go:embed large-database.db orqali embed qilingan 100 MB SQLite fayli kross-kompilyatsiya qiladigan har bir platforma uchun har safar 100 MB kattaroq binar hosil qiladi. Kichik, statik, kod bilan birga versiyalanadigan resurslar — standart konfiglar, HTML shablonlar, logotip, kilobaytlarda o'lchanadigan seed ma'lumotlar — aynan embed mo'ljallangan narsa. Katta yoki tez-tez o'zgaradigan ma'lumot esa haqiqiy ma'lumotlar omboriga, object storage'ga yoki runtime'da yuklanadigan faylga tegishli: u yerda uni yangilash yangi binar jo'natishni anglatmaydi.
Bu ikki imkoniyat raqobatlashmaydi, balki to'ldiradi, va haqiqiy dasturlar ko'pincha ikkalasini bitta paketda ishlatadi: biror narsadan (sxema, enum, interface) kod hosil qilish uchun go:generate va umuman kod bo'lmagan narsani (shablon, standart fayl, yordam xabari) binarga tortish uchun embed.
//go:embed help.txt
var helpText string
func main() {
if len(os.Args) > 1 && os.Args[1] == "help" {
fmt.Print(helpText)
return
}
// ...
}
Bu kichik, lekin haqiqiy misol: CLI'ning yordam matni Go yozmaydigan jamoadosh tahrirlashi uchun oson oddiy matn faylida turadi, embed uni binarga joylaydi, shuning uchun tarqatish paytida yo'qotib qo'yiladigan alohida fayl yo'q.
Ikkala vosita ham qo'lda yuritiladigan shablonni va resurs jo'natish logistikasini yo'lingizdan tozalash uchun mavjud bo'lib, buni qilish uchun preprocessor yoki runtime plagin tizimiga tayanishni talab qilmaydi. Bu biz yoritgan toolchain'ning qolgan qismi — go fmt, go vet, go mod — bilan juda mos: ularning har biri aniq bir yumushni avtomatlashtiradi va keyin yo'lingizdan chiqadi. 52-bob, Software'ni build qilish va reliz qilish, bu tugagan joydan davom etadi: manba kodingiz va resurslaringiz joyiga tushgandan keyin, ularni foydalanuvchilaringiz ishlatadigan har bir platforma uchun binarlarga qanday aylantirish, versiya bilan belgilash va har safar o'ylab o'tirmasligingiz kerak bo'lgan jadval bo'yicha jo'natish.