12 / 73

O'zgaruvchilar va bloklar

O'zgaruvchilarni e'lon qilish usullari, ichma-ich bloklar va := operatoridan foydalanishda xatolar bilan bog'liq nozik tuzoq — deyarli har bir Go dasturchisi hech bo'lmaganda bir marta duch keladigan shadowing (soyalash) muammosi — ushbu bobning asosiy mavzusidir.

Ushbu xatolikni keltirib chiqaradigan kod bir qarashda mutlaqo to'g'ri ko'rinadi va kompilyatordan birorta ham ogohlantirishsiz yig'iladi.

var va := bir-birining o'rnini bosmaydi

var — umumiy maqsadli e'lon qilish shakli bo'lib, u paket darajasida ham, funksiyalar ichida ham ishlaydi. Bu o'zgaruvchiga darhol qiymat bermasdan e'lon qilish imkonini beradigan yagona vositadir. Bunday holda o'zgaruvchi avtomatik ravishda o'z tipining nol qiymatini oladi:

example.go
var name string        // "" — string uchun nol qiymat
var age int = 30
var count = 100        // tipi int deb avtomatik aniqlanadi
var x, y int = 1, 2

Qisqa e'lon operatori := faqat funksiya tanasi ichida ishlaydi, har doim boshlang'ich qiymat talab qiladi va o'zgaruvchi tipini o'sha berilgan qiymatdan xulosa qilib oladi:

example.go
name := "Alice"
age := 30
x, y := 1, 2

Idiomatik Go kodi lokal o'zgaruvchilar uchun asosan := dan foydalanadi — u ixcham va qulay. Biroq var ham bir nechta aniq vaziyatlarda o'z o'rniga ega:

  1. Paket darajasidagi (global) o'zgaruvchilarda: := funksiyadan tashqarida ishlamaydi.
  2. Nol qiymatdan boshlanib, keyinroq to'ldiriladigan o'zgaruvchilarda:
example.go
var buffer []byte  // aniq va ravshan: "bo'sh (nil) dan boshlanadi, pastda to'ldiriladi"
buffer := []byte{} // texnik jihatdan boshqacha (xotirada bo'sh slice) va niyat unchalik ravshan emas
  1. Tipni ochiq ko'rsatish kodni o'qiyotgan odamga yordam beradigan holatlarda:
example.go
var timeout time.Duration = 30 * time.Second

Bloklar va o'zgaruvchilarning ko'rinish doirasi (Scope)

Go tilida nomlarning ko'rinish doirasi leksik bloklarga asoslangan: har qanday nom e'lon qilingan nuqtadan boshlab uni o'rab turgan eng ichki jingalak qavslar { ... } blokining oxirigacha amal qiladi. Ichki bloklar o'zini o'rab turgan tashqi bloklardagi barcha o'zgaruvchilarni ko'ra oladi, ammo tashqi bloklar ichki blokdagi o'zgaruvchilarni ko'ra olmaydi:

example.go
func example() {
    x := 1
    if true {
        y := 2
        fmt.Println(x, y) // to'g'ri — ichki blok tashqi x ni bemalol ko'radi
    }
    fmt.Println(y) // kompilyatsiya xatosi: bu yerda y mavjud emas
}

if, for va switch konstruksiyalari har biri o'zining mustaqil blokini hosil qiladi. Shart oldidagi boshlang'ich qismda e'lon qilingan o'zgaruvchilar faqat shu shart/sikl blokiga tegishli bo'ladi:

example.go
if x := getValue(); x > 0 {
    fmt.Println(x) // to'g'ri, x faqat shu if/else zanjirida mavjud
}
// bu yerda x mavjud emas

for i := 0; i < 10; i++ {
    // i faqat sikl ichida mavjud
}

Paket darajasidagi e'lonlar esa butun paket bo'ylab (shu paketga kiruvchi barcha .go fayllarida) ko'rinadi:

example.go
package main

var packageVar int // shu paketdagi har bir faylda va funksiyada ko'rinadi

func f1() { packageVar = 1 }
func f2() { fmt.Println(packageVar) }

O'zgaruvchining paket tashqarisida ko'rinishi esa bosh harf qoidasiga bog'liq: katta harf bilan boshlangan nomlar eksport qilinadi (ommaviy), kichik harf bilan boshlanganlari esa faqat paket ichida qoladi. Boshqa tillardagi public, private kabi kalit so'zlarga ehtiyoj yo'q.

Soyalash (Shadowing): ichki nom tashqi nomni yashirganda

Soyalash (shadowing) — ichki blok ichidagi := orqali tashqi blokda allaqachon mavjud bo'lgan nom bilan yangi o'zgaruvchi e'lon qilinganda yuz beradi. Yangi o'zgaruvchi mutlaqo boshqa xotira maydoniga ega bo'lib, ichki blok doirasida tashqi o'zgaruvchini "to'sib" qo'yadi:

example.go
func example() {
    x := 1
    fmt.Println(x) // 1

    if true {
        x := 2 // YANGI x yaratildi, tashqi x ni soyalaydi
        fmt.Println(x) // 2
    }

    fmt.Println(x) // 1 — tashqi x ga hech kim tegmadi!
}

Eng muhim farq := va = operatorlari orasidadir:

  • := yangi blokda ishlatilsa, har doim yangi o'zgaruvchi yaratadi va tashqisini soyalaydi.
  • = esa hech qachon soyalamaydi — u mavjud o'zgaruvchining qiymatini yangilaydi:
example.go
func example() {
    x := 1
    if true {
        x = 2 // O'SHA tashqi x ga qiymat yoziladi — soyalash yo'q
    }
    fmt.Println(x) // 2
}

Import qilingan paket nomlarini ham bexosdan soyalab qo'yish mumkin:

example.go
import "fmt"

func example() {
    fmt := "oops" // endi fmt — lokal string o'zgaruvchisi
    fmt.Println("Hello") // kompilyatsiya xatosi: string tipida Println metodi yo'q
}

Dasturchilarni eng ko'p chalg'itadigan xato (Error Shadowing)

Shadowing xatosi eng ko'p uchraydigan va jiddiy oqibatlarga olib keladigan holat — bu sikl yoki blok ichida err o'zgaruvchisini qayta e'lon qilib qo'yishdir:

example.go
func process(data []byte) error {
    var err error

    for _, item := range data {
        result, err := processItem(item) // := operatori yangi err yaratadi va tashqi err ni soyalaydi!
        if err != nil {
            return err // bu tekshiruv ishlaydi, ichki err ni qaytaradi
        }
        _ = result
    }

    return err // XATOLIK: tashqi err hech qachon o'zgarmagan, shuning uchun doimo nil qaytadi!
}

Tezda ko'z yugurtirganda hammasi to'g'ridek ko'rinadi. Biroq result, err := processItem(item) qatorida result yangi o'zgaruvchi bo'lgani sababli := operatori err ni ham yangidan lokal o'zgaruvchi sifatida yaratib yuboradi. Natijada tashqi err ga hech narsa yozilmaydi. Agar sikldan keyin xato qaytarilsa, u har doim nil bo'lib qolaveradi.

Buning eng to'g'ri va idiomatik yechimi — xato yuz bergan zahoti funksiyadan darhol chiqish (early return):

example.go
func process(data []byte) error {
    for _, item := range data {
        _, err := processItem(item)
        if err != nil {
            return err
        }
    }
    return nil
}

Agar sikl tugagach xatoni qaytarish chindan zarur bo'lsa, := o'rniga tashqi o'zgaruvchiga = orqali o'zlashtirish kerak:

example.go
func process(data []byte) error {
    var err error
    for _, item := range data {
        var result Result
        result, err = processItem(item) // Tashqi err ga yoziladi — soyalash yo'q
        if err != nil {
            return err
        }
        _ = result
    }
    return err
}

Standart go vet buni xato deb hisoblamaydi, chunki sintaktik jihatdan ikkala variant ham to'g'ri Go kodi hisoblanadi — ular shunchaki turli ma'noni anglatadi. Shuning uchun ham ushbu noziklikni dasturchining o'zi doimo nazorat qilishi kerak. golangci-lint tarkibidagi shadow analizatori e'tiborga loyiq nomzodlarni ko'rsatishi mumkin (u sukut bo'yicha o'chirilgan, chunki ataylab qilingan va zararsiz soyalashlarni ham ko'plab belgilaydi), lekin haqiqiy yechim — vositaga emas, o'z odatingizga tayanish.

Ko'rinadigan err mavjud bo'lgan blokda x, err := ... yozayotganingizda har doim o'zingizga savol bering: men haqiqatan ham yangi err yaratmoqchimanmi yoki mavjudiga qiymat yozmoqchimanmi?

Ishlatilmagan o'zgaruvchilar

Go tilida e'lon qilingan, ammo funksiya ichida ishlatilmagan lokal o'zgaruvchilar ogohlantirish (warning) emas, balki qat'iy kompilyatsiya xatosi hisoblanadi. Bu keraksiz kodlar va chalkashliklarning oldini oladi. Kerak bo'lmagan qiymatlarni tashlab yuborish uchun Goda bo'sh identifikator (_) mavjud:

example.go
x, y := getValues()
_ = x // x dan foydalanmasligimizni ochiq bildiramiz
fmt.Println(y)

Xulosa va eng yaxshi amaliyotlar

  1. O'zgaruvchilarni funksiyaning eng yuqori qismiga to'plamasdan, ulardan birinchi marta foydalaniladigan joyga imkon qadar yaqin e'lon qiling.
  2. Funksiya ichida odatda := dan, paket darajasida yoki nol qiymat maqsad qilinganda esa var dan foydalaning.
  3. Tashqi o'zgaruvchilar mavjud joyda := ishlatganda shadowing xavfini unutmang.
  4. Xatolarni sikl oxiriga to'plamasdan, yuzaga kelgan zahoti qaytaring (early return). O'zgaruvchilar va ularning bloklardagi amal qilish doirasi bilan tanishib bo'lgach, keyingi bobda dastur oqimini boshqaruvchi shartli konstruksiyalar — if va switch imkoniyatlarini ko'rib chiqamiz.