19 / 73

Funksiyalar chuqur

Xatti-harakat Goda funksiyalarda yashaydi. Boshidan "metod yoki funksiya" bahsi qilish shart emas, chunki til darajasida metod aslida qo'shimcha, yashirin birinchi parametrga (receiver) ega funksiyadan boshqa narsa emas — ularni keyinroq tiplarga biriktira boshlaganingizda ham, ushbu bobdagi funksiyalarni e'lon qilish, chaqirish va ulardan qiymat qaytarish haqidagi hamma narsa bir xil amal qiladi. Go funksiyalarini o'ziga xos qiladigan narsa g'ayrioddiy sintaksis emas; bu bir nechta ataylab qilingan tanlov — bir nechta qaytariladigan qiymatlar (multiple return values), nomlangan qaytariladigan qiymatlar (named returns) va variadic parametrlar — va ular idiomatik Go kodi qanday yozilishini, ayniqsa xatolarni qayta ishlash atrofida, belgilab beradi.

Funksiyalarni e'lon qilish va chaqirish

Funksiya e'loni parametrlarini va ularning tiplarini nomlaydi, so'ng qaytariladigan tip keladi:

example.go
func add(a int, b int) int {
    return a + b
}

Ketma-ket parametrlar bir xil tipga ega bo'lsa, Go takrorlanuvchi tip nomini tashlab ketishga ruxsat beradi:

example.go
func add(a, b int) int {
    return a + b
}

Funksiyalar parametrsiz bo'lishi, hech narsa qaytarmasligi yoki ikkalasi ham bo'lishi mumkin:

example.go
func greet() string {
    return "Hello, World!"
}

func printMessage(msg string) {
    fmt.Println(msg)
}

Funksiyani chaqirish boshqa hamma joydagi kabi ko'rinadi:

example.go
result := add(3, 5) // 8

Bularning birortasi o'z-o'zicha g'ayrioddiy emas. To'xtab o'tish foydali bo'lgani — bu oddiy qurilish bloklari qanchalik uzoqqa borishi: Goda parametrlarning standart qiymatlari yo'q, ixtiyoriy argumentlar yo'q va overloading yo'q — nom aniq bitta signaturali aniq bitta funksiyaga ishora qiladi. Bu haqiqiy cheklov (siz NewServerni overload qilish o'rniga NewServerWithTimeout yozasiz), lekin shu bilan birga chaqiruv joyini o'qish sizdan qaysi overload hal qilinganini o'ylashni hech qachon talab qilmasligini anglatadi. Ko'rganingiz — chaqirilayotgan narsa.

Bir nechta qaytariladigan qiymatlar

Bu — funksiyani darhol Goga xos deb belgilaydigan imkoniyat. Natija va muvaffaqiyatsizlik ko'rsatkichini bitta "yoki" tipiga o'rash yoki xato yo'lida exception tashlash o'rniga, Go funksiyalari shunchaki bittadan ortiq qiymat qaytaradi:

example.go
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

result, err := divide(10, 2)
if err != nil {
    log.Fatal(err)
}
fmt.Println(result) // 5

(result, error) shakli standart kutubxonada va idiomatik Goda shu qadar keng tarqalganki, u yozilmagan protokol vazifasini bajaradi: nil xato qolgan qaytariladigan qiymatlar mazmunli ekanini anglatadi, nil bo'lmagan xato esa odatda ularga ishonmaslik kerakligini. strconv.Atoi, os.Open, json.Marshal — andoza takrorlanadi, chunki u Goning if err != nil boshqaruv oqimi bilan yaxshi birikadi; biz bunga keyinroq butun bir bob bag'ishlaymiz.

(result, error) shakli — Goda yozilmagan protokol: nil xato qolgan qiymatlar mazmunli ekanini bildiradi.

Bir nechta qaytariladigan qiymat faqat qiymat-va-xato juftligi bilan cheklanmaydi; har qanday funksiya har qanday tipdagi bir necha qiymat qaytara oladi:

example.go
func minMax(numbers []int) (min, max int) {
    min, max = numbers[0], numbers[0]
    for _, n := range numbers[1:] {
        if n < min {
            min = n
        }
        if n > max {
            max = n
        }
    }
    return min, max
}

Go ba'zi tillardagidek funksiya natijasini sezilmasdan e'tiborsiz qoldirishga ruxsat bermaydi — ikki qiymatli funksiyani bir qiymatli kontekstda chaqirish kompilyatsiya xatosi hisoblanadi. Agar qiymatlardan biri kerak bo'lmasa, uni bo'sh identifikator (_) bilan ochiq tashlab yuborish mumkin:

example.go
result, _ := divide(10, 2) // xatoni ataylab e'tiborsiz qoldirish

Bu kod kompilyatsiya bo'ladi, lekin bunga faqat o'ta zarur hollardagina qo'l urish kerak. Xatoni tekshirmasdan e'tiborsiz qoldirish (yutib yuborish) jiddiy nosozliklarni yashirib qo'yishi mumkin.

Nomlangan qaytariladigan qiymatlar (Named Return Values)

Go funksiyaning qaytarish signaturasidagi qiymatlarga nom berishga imkon beradi. Nomlangan qaytariladigan qiymatlar o'z tipining nol qiymatiga ega bo'lgan oddiy lokal o'zgaruvchilar sifatida yaratiladi va ularni argumentsiz bo'sh return (naked return) operatori bilan qaytarish mumkin:

example.go
func divide(a, b float64) (result float64, err error) {
    if b == 0 {
        err = fmt.Errorf("division by zero")
        return // (0, err) qaytaradi
    }
    result = a / b
    return // (result, nil) qaytaradi
}

Qaytariladigan qiymatlarga nom berishning eng asosiy maqsadi — kodni hujjatlashtirish va tushunarli qilishdir. Bir xil tipdagi bir nechta qiymat qaytarganda (masalan, (min, max int)), nomlar qaysi biri nima ekanini darhol anglatadi. Shuningdek, nomlangan qaytariladigan qiymatlar defer orqali qaytayotgan xatolikni ushlab olish yoki o'zgartirish uchun juda qulay:

example.go
func readConfig(path string) (cfg Config, err error) {
    f, err := os.Open(path)
    if err != nil {
        return
    }
    defer f.Close()

    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("readConfig: recovered from panic: %v", r)
        }
    }()

    return parseConfig(f)
}

Biroq nomlangan qiymatlardan foydalanishda muhim bir noziklik bor: nomlangan qaytariladigan o'zgaruvchini funksiya ichida := operatori orqali yangidan e'lon qilib, tasodifan soyalab (shadowing) qo'yish mumkin:

example.go
func broken() (result int) {
    result := 42 // bu result nomli YANGI lokal o'zgaruvchini e'lon qiladi (shadowing)
    return       // NOMLANGAN result ni qaytaradi, u esa hamon 0 bo'lib qolgan!
}

Shuning uchun, nomlangan qaytariladigan qiymatlarga funksiya ichida := bilan emas, balki = bilan qiymat bering. Katta va uzun funksiyalarda esa argumentsiz bo'sh return o'rniga nima qaytarilayotganini aniq ko'rsatuvchi return value, err shaklidan foydalanish tavsiya etiladi.

Nomlangan qaytariladigan o'zgaruvchiga funksiya ichida := bilan qiymat bermang — aks holda yangi lokal o'zgaruvchi yaratilib, asosiysi soyalanib qoladi.

Variadic funksiyalar

... bilan tugaydigan parametr shu tipdagi istalgancha argumentni qabul qiladi va ularni funksiya tanasi ichida slice'ga yig'adi:

example.go
func sum(numbers ...int) int {
    total := 0
    for _, n := range numbers {
        total += n
    }
    return total
}

sum(1, 2, 3, 4, 5) // 15
sum()               // 0 — nol argument yaroqli

sum ichida numbers oddiy []int; variadic sintaksisi sof chaqiruv qulayligi. Bu simmetriya ikkala yo'nalishda ishlaydi — agar sizda allaqachon slice bo'lsa, uni variadic chaqiruvga xuddi shu ... bilan yoyishingiz mumkin:

example.go
values := []int{1, 2, 3, 4, 5}
sum(values...) // slice'ni alohida argumentlar sifatida yoyadi

Slice'ni oxiridagi ...siz uzatish kompilyatsiya xatosi, ish vaqtidagi kutilmagan hodisa emas (sum(values) kompilyatsiya bo'lmaydi, chunki []int variadic int parametriga o'zlashtirilmaydi) — bu Goning statik tiplashtirishi aks holda faqat kodni ishga tushirganingizda chiqadigan xatoni ushlaydigan joylardan biri. Variadic parametr signaturada oxirgisi bo'lishi kerak, lekin oddiy parametrlar undan oldin kela oladi:

example.go
func join(sep string, items ...string) string {
    return strings.Join(items, sep)
}

join(", ", "a", "b", "c") // "a, b, c"

fmt.Printf va o'rnatilgan append — siz ularni variadic deb o'ylamasdan doim ishlatadigan variadic funksiyalar — append(slice, 1, 2, 3) aynan sum(1, 2, 3) bilan bir xil mexanizm.

Ichkariga singdirish foydali bo'lgan bitta nozik jihat: variadic funksiya oladigan slice o'ziga yoyilgan narsa bilan asosiy xotirani ulashadi (agar slice yoyilgan bo'lsa) yoki yangidan ajratiladi (agar alohida argumentlar uzatilgan bo'lsa). Funksiya ichida shu slice elementini o'zgartirish, agar slice yoyilgan bo'lsa, chaqiruvchiga ko'rinishi mumkin, lekin funksiya ichida qayta kesish yoki append qilish hech qachon ko'rinmaydi, chunki bu amallar funksiyaga lokal yangi slice sarlavhasini hosil qiladi:

example.go
func zeroFirst(numbers ...int) {
    if len(numbers) > 0 {
        numbers[0] = 0 // chaqiruvchiga faqat slice yoyilgan bo'lsa ko'rinadi
    }
}

nums := []int{1, 2, 3}
zeroFirst(nums...)
fmt.Println(nums) // [0 2 3] — asosiy massiv ulashilgan edi

Funksiya variadic ulashish orqali o'zgartirishga tayanmasdan, albatta yangi qiymat hosil qilishini xohlasangiz, joyida o'zgartirishga tayanish o'rniga natijani aniq qaytaring.

Qiymat qaytaradigan funksiyalar

Goda funksiyalar tipga ega qiymatlar, ularning tipi to'liq signaturasi bilan aniqlanadi, va bu haqiqat idiomatik Goni composable qiladigan narsaning ko'p qismiga asos bo'ladi — xatti-harakatni sort.Slicega uzatish, HTTP middleware'ni ulash, kichik callback'ga asoslangan API'larni qurish:

example.go
var op func(int, int) int
op = func(a, b int) int { return a + b }
result := op(3, 5) // 8

func apply(numbers []int, fn func(int) int) []int {
    out := make([]int, len(numbers))
    for i, n := range numbers {
        out[i] = fn(n)
    }
    return out
}

doubled := apply([]int{1, 2, 3}, func(n int) int { return n * 2 }) // [2 4 6]

Buni — closure'lar, higher-order funksiyalar va ular imkon beradigan andozalarni — keyingi bobda, bu yerda yoritilgan funksiyalarni e'lon qilish va chaqirish mexanikasi ikkinchi tabiatga aylangach, chuqurroq ko'rib chiqamiz.

Bardoshli funksiyalarni yozish

Bir nechta kelishuv ishlashi qulay bo'lib qoladigan Go funksiyalarini qulay bo'lmay qoladiganlaridan ajratib turadi. Funksiyaning vazifasini bir jumlada tasvirlab bo'ladigan darajada tor saqlang; bunga endi erishib bo'lmasa, odatda funksiya ikkita ish qilayotganining va ikkita funksiyaga bo'linishi kerakligining belgisi. Funksiyalarni ular nima qilishiga qarab nomlang — calc emas, calculateTotalPrice — chunki nomlar chaqiruvchi chaqiruv joyida ko'radigan asosiy hujjat. Eksport qilinadigan funksiyalar uchun doc comment'ni funksiya nomidan boshlanadigan jumla sifatida yozing (// ParseInteger parses a string and returns an integer, or an error if the string isn't a valid integer.) — bu go doc va godoc.org foydali hujjat ko'rsatish uchun tayanadigan kelishuv, shunchaki uslub go'zalligi emas. Va chaqiruvchi oqilona qayta ishlashni xohlashi mumkin bo'lgan har qanday narsa uchun panic qilishdan ko'ra xato qaytarishni afzal ko'ring — panicni haqiqiy tiklab bo'lmaydigan dasturlash xatolari uchun saqlang, bu farqni xatolar va panic'lar haqidagi bob keskinroq chizadi.

Aynan shu imkoniyatlar bilan bog'liq bir nechta xato nomlashga arzigulik darajada keng tarqalgan. Slice'ni variadic chaqiruvga yoymay qoldirish (sum(nums...) o'rniga sum(nums)) kompilyator tomonidan ushlanadi, lekin tuzatish birinchi marta xatoni ko'rayotgan odamga har doim ham aniq emas. Nomlangan qaytariladigan qiymatni yuqorida ko'rsatilganidek := bilan shadow qilish jimgina kompilyatsiya bo'ladi va jimgina buziladi — tilning shu qismidagi haqiqiy tuzoqqa eng yaqin narsa. Va nomlangan qaytariladigan qiymatli funksiyadagi argumentsiz returnni o'z-o'zini tushuntiruvchi deb hisoblash uzun funksiyada teskari ta'sir qilishi mumkin, u yerda o'quvchi aslida nima qaytarilayotganini bilish uchun signaturaga qaytishi kerak; shubhada bo'lsangiz, return value, errni aniq yozing.

20-bob funksiyalarni qiymat sifatida ishlatish mavzusini to'g'ridan-to'g'ri davom ettiradi: atrofdagi holatni ushlab oluvchi closure'lar, map, filter va reduce kabi higher-order funksiyalar, hamda funksiyalarni oddiy qiymatlar sifatida qarashdan tabiiy ravishda chiqadigan middleware va strategy andozalari.