20 / 73

Qiymat sifatida funksiyalar

Go tili funksional dasturlash tili emas — unda monadalar, qat'iy o'zgarmaslik (immutability) yoki murakkab pattern matching yo'q. Biroq Goda funksiyalar birinchi darajali til elementi (first-class citizen) hisoblanadi: ularga oddiy qiymatlar sifatida qaraladi. Funksiyalarning o'ziga xos tipi bor, ularni o'zgaruvchilarda va struct maydonlarida saqlash, argument sifatida boshqa funksiyalarga uzatish, funksiyalardan qaytarish va anonim tarzda (inline) yaratish mumkin. Bu imkoniyat closure'lar, callback'lar, HTTP middleware zanjirlari va funksional andozalarni (map, filter, reduce) qulay amalga oshirish uchun to'liq yetarlidir.

Funksiya tiplari

Funksiyaning tipi — uning signaturasi, ya'ni parametr tiplari va qaytariladigan tiplar, boshqa hech narsa emas; ya'ni butunlay boshqa nomli va boshqa tanali ikkita funksiya, agar bir xil narsalarni qabul qilib, bir xil narsalarni qaytarsa, bir xil tipga ega:

example.go
func(int, int) int    // ikkita int qabul qiladi, int qaytaradi
func(string) error    // string qabul qiladi, error qaytaradi
func([]int) []int     // int slice'ini qabul qiladi, int slice'ini qaytaradi

Funksiya tipiga type e'loni bilan nom berish API'lardagi signaturalarni, ayniqsa ular bir-ikki parametrdan uzunroq bo'lganda, ancha o'qishli qiladi:

example.go
type BinaryOp func(int, int) int

var add BinaryOp = func(a, b int) int { return a + b }
var multiply BinaryOp = func(a, b int) int { return a * b }

add(3, 5)      // 8
multiply(3, 5) // 15

Funksiya o'zgaruvchisi aniq tipdagi har qanday boshqa o'zgaruvchi kabi tutadi — unga o'z tipiga mos keladigan istalgan funksiya qiymatini qayta o'zlashtirish mumkin, jumladan ish vaqtida implementatsiyalarni almashtirish ham:

example.go
var operation func(int, int) int
operation = func(a, b int) int { return a + b }
operation = func(a, b int) int { return a * b } // uni almashtirish mutlaqo joiz

Generic funksiyalarni funksiya tipli o'zgaruvchilarga o'zlashtirish

Yuqoridagi misollarning hammasi oddiy, generic bo'lmagan funksiyalarni nomlangan funksiya tipiga o'zlashtiradi. Generic funksiya bilan xuddi shu ishni qilish avval faqat ba'zi kontekstlardagina ishlardi, chunki kompilyator hosil bo'lgan aniq tip mos kelishini tekshirishdan oldin generic funksiyaning tip argumentlarini o'zlashtirishdan chiqarib olishi kerak edi — generics'ning dastlabki versiyalari bu chiqarishni faqat ancha tor joylar to'plamidagina bajarardi. Go 1.27 function type inference'ni umumlashtirdi, endi u generic funksiya mos funksiya tipiga o'zlashtirilgan yoki o'tkazilgan har bir kontekstda qo'llaniladi, avval ishlagan bir necha joyda emas:

example.go
func Max[T cmp.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

type IntOp func(int, int) int

var op IntOp = Max[int] // Go 1.27 dan boshlab avval wrapper talab qilgan kontekstlarda ham ishlaydi

Bu o'zgarishdan oldin, generic funksiyaning int'ga ixtisoslashtirilgan shaklini oddiy func(int, int) int o'zgaruvchisida saqlamoqchi bo'lgan kod ko'pincha aniq generic bo'lmagan wrapper closure'ni talab qilardi — var op IntOp = func(a, b int) int { return Max(a, b) } — faqat kompilyatorga shu maxsus joyda taqqoslay oladigan shaklni berish uchun. Bu chora hamon kompilyatsiya bo'ladi, shuning uchun uni eski Go versiyalari uchun yozilgan kodda ko'rasiz, lekin to'g'ridan-to'g'ri o'zlashtirish yoki o'tkazish aks holda tip tekshiruvidan o'ta oladigan har qanday joyda endi kerak emas.

Anonim funksiyalar

Funksiya literali — nomsiz, joyida yozilgan func(...) { ... } — boshqalar kabi qiymat, va uni darhol chaqirish, o'zlashtirish yoki to'g'ridan-to'g'ri boshqa funksiyaning argumentlar ro'yxatiga uzatish mumkin:

example.go
func() {
    fmt.Println("darhol ishlaydi")
}() // oxiridagi () uni aynan e'lon qilingan joyida chaqiradi

greet := func() { fmt.Println("Hello") }
greet()

sort.Slice(people, func(i, j int) bool {
    return people[i].Age < people[j].Age
})

Oxirgi shakl — literal to'g'ridan-to'g'ri chaqiruvga uzatilgan, unga hech qachon nom berilmagan — HTTP handler'lar, sort comparator'lari va qat'iy qiymat emas, "bir bo'lak xatti-harakat" istaydigan har qanday API bilan ishlaydigan Go kodida doimiy takrorlanadigan shakl. Quyida map, filter va reduce haqidagi bo'lim aynan shu andozaga quriladi.

Closure'lar

Closure — u e'lon qilingan scope'dagi o'zgaruvchilarga murojaat qiluvchi funksiya literali, va Go closure'lari bu o'zgaruvchilarni reference orqali ushlaydi — yaratilish paytidagi qiymatning snapshot'ini olish orqali emas, balki haqiqiy o'zgaruvchining o'zini ushlab turish orqali, shuning uchun uning keyingi o'zgarishlari (har ikki tomondan) closure orqali ko'rinadi:

example.go
func makeCounter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

counter := makeCounter()
counter() // 1
counter() // 2
counter() // 3

count o'zgaruvchisi odatda makeCounter qaytganda scope'dan chiqib, qayta tiklanardi — lekin qaytarilgan closure hamon unga murojaat qilgani uchun, Go kompilyatori count funksiya chaqiruvidan uzoqroq yashashi kerakligini aniqlaydi va uni stack o'rniga heap'da ajratadi (bu 22-bobda tilga olingan "escape analysis", va closure'lar qiymat escape qilishining eng keng tarqalgan sabablaridan biri). makeCounterga har bir chaqiruv yangi count va uning atrofida yangi closure yaratadi, shuning uchun mustaqil yaratilgan hisoblagichlar bir-biriga xalaqit bermaydi:

example.go
c1, c2 := makeCounter(), makeCounter()
c1() // 1
c1() // 2
c2() // 1 — c2 ning o'z count'i bor, c1 unga tegmagan

Closure parametrlarni ham ushlashi mumkin, bu umumiy shablondan ixtisoslashtirilgan funksiyalar qurishning asosi — doimiy uchraydigan factory andozasi:

example.go
func makeMultiplier(factor int) func(int) int {
    return func(n int) int { return n * factor }
}

double := makeMultiplier(2)
triple := makeMultiplier(3)
double(5) // 10
triple(5) // 15

Loop o'zgaruvchisi tuzog'i (va nega u endi asosan tarix)

Ko'p yillar davomida Godagi eng keng tarqalgan closure xatosi shunday ko'rinardi:

example.go
var funcs []func()
for i := 0; i < 3; i++ {
    funcs = append(funcs, func() {
        fmt.Println(i)
    })
}
// tarixan: uchala closure ham 0, 1, 2 emas, 3 chiqarardi

Go 1.22 dan oldin for siklining indeks o'zgaruvchisi sikl tanasidan tashqarida bir marta e'lon qilinardi va har iteratsiyada qayta ishlatilardi — shuning uchun sikl ichida yaratilgan har bir closure bir xil o'zgaruvchini ushlab turardi, sikl tugagandan keyin uning oxirgi qiymati sikl tugash sharti qoldirgan qiymat edi. 1.22 dan oldingi tuzatish har bir iteratsiya o'z nusxasini olishi uchun sikl o'zgaruvchisini tana ichida aniq shadow qilish edi:

example.go
for i := 0; i < 3; i++ {
    i := i // shu iteratsiyaga xos, chinakam yangi o'zgaruvchi yaratish
    funcs = append(funcs, func() { fmt.Println(i) })
}

Go 1.22 til semantikasini o'zgartirdi, endi for sikl o'zgaruvchilari sukut bo'yicha har iteratsiyaga xos scope'ga ega — birinchi misoldagi aynan o'sha kod endi to'g'ri 0, 1, 2 chiqaradi, hech qanday chora talab qilmasdan. Eski Go kodi, qo'llanmalari yoki Stack Overflow javoblarini o'qisangiz, i := i idiomasini hamma joyda ko'rasiz, va uni yozishda davom etish zararsiz (bir xil yangi o'zgaruvchini shadow qiluvchi yangi o'zgaruvchi — no-op), lekin u Go 1.22 yoki undan yangi versiyani maqsad qilgan har qanday kod bazasida endi majburiy emas. Birini boshqasidan afzal ko'rishdan oldin go.modingizdagi go direktivasi qaysi xatti-harakatni bildirishini bilish foydali.

Go 1.22 dan boshlab for sikl o'zgaruvchilari har iteratsiyaga xos scope'ga ega — i := i chorasi endi majburiy emas.

Higher-order funksiyalar: Map, Filter, Reduce

Boshqa funksiyani argument sifatida qabul qiladigan yoki qaytaradigan funksiya higher-order deyiladi, va uch shakl nomlashga arzigulik darajada tez-tez uchraydi, garchi Goda ularga boshidan shunga asoslangan tillardagidek o'rnatilgan qo'llab-quvvatlash bo'lmasa ham.

Mapping slice'ning har bir elementini o'zgartiradi:

example.go
func mapInts(numbers []int, fn func(int) int) []int {
    result := make([]int, len(numbers))
    for i, n := range numbers {
        result[i] = fn(n)
    }
    return result
}

Filtering faqat predikatni qanoatlantiradigan elementlarni saqlaydi:

example.go
func filterInts(numbers []int, fn func(int) bool) []int {
    var result []int
    for _, n := range numbers {
        if fn(n) {
            result = append(result, n)
        }
    }
    return result
}

Reducing esa slice'ni bitta to'plangan qiymatga yig'adi:

example.go
func reduceInts(numbers []int, initial int, fn func(int, int) int) int {
    result := initial
    for _, n := range numbers {
        result = fn(result, n)
    }
    return result
}

sum := reduceInts([]int{1, 2, 3, 4, 5}, 0, func(acc, n int) int { return acc + n }) // 15

Generics'dan (Go 1.18) oldin bularning har birini har bir element tipi uchun qo'lda qayta yozish kerak edi — mapInts, mapStrings va hokazo — va bu andoza Go uchun boshidan unga qurilgan tillardagidek markaziy bo'lib qolmaganining katta sababi. Type parameter'lar bilan bitta generic versiya har bir tipni qoplaydi:

example.go
func Map[T, U any](slice []T, fn func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}

func Filter[T any](slice []T, fn func(T) bool) []T {
    var result []T
    for _, v := range slice {
        if fn(v) {
            result = append(result, v)
        }
    }
    return result
}

Shunday bo'lsa ham, idiomatik Go kuchli funksional an'anali tillarga qaraganda Map/Filter/Reducedan ko'ra oddiy for siklini ko'proq afzal ko'radi — generic versiyalar noto'g'ri bo'lgani uchun emas, balki aniq tanali sikl odatda xuddi shunday o'qishli va o'quvchidan boshida qo'shimcha bilvositalik qatlamini ushlab turishni talab qilmagani uchun. 32-bob bu kelishuvni idiomatik generic kod yozishning bir qismi sifatida to'g'ridan-to'g'ri muhokama qiladi; halol xulosa shuki, bu yordamchilar utility paketida bo'lish foydali, lekin ularga refleks bilan emas, ataylab murojaat qilish foydali.

Funksiya qiymatlariga qurilgan andozalar

Callback'lar eng to'g'ridan-to'g'ri qo'llanish — xatti-harakat bo'lagini keyinroq, chaqiruvchi nazorat qilmaydigan paytda uni chaqiradigan kodga topshirish:

example.go
type EventHandler func(event Event)

func onUserLogin(handler EventHandler) {
    handler(Event{Type: "login", User: currentUser})
}

onUserLogin(func(e Event) {
    fmt.Printf("%s logged in\n", e.User.Name)
})

Middleware — bir xil shakldagi boshqa funksiyani o'rab, unga topshirishdan oldin va/yoki keyin xatti-harakat qo'shadigan funksiya — xuddi shu g'oyaning aynan HTTP handler'larga qo'llanishi, va aksariyat Go veb-frameworklari (hamda standart kutubxonaning o'zi net/http) logging va auth kabi kesib o'tuvchi vazifalarni handler'larning o'zini chalkashtirmasdan qanday birlashtirsa, shunday:

example.go
type Handler func(http.ResponseWriter, *http.Request)

func loggingMiddleware(next Handler) Handler {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Printf("%s %s", r.Method, r.URL.Path)
        next(w, r)
    }
}

Konfiguratsiyani yopib oladigan (close over) va ixtisoslashtirilgan funksiyani qaytaradigan factory funksiya — yuqoridagi closure andozasining o'zi, faqat validatsiyaga qo'llangan:

example.go
func makeValidator(min, max int) func(int) error {
    return func(n int) error {
        if n < min || n > max {
            return fmt.Errorf("value %d out of range [%d, %d]", n, min, max)
        }
        return nil
    }
}

validateAge := makeValidator(0, 120)
validateScore := makeValidator(0, 100)

Bir nechta odat

Signaturasi bir necha joyda ishlatilgach, funksiya tipiga type bilan nom bering — type Processor func([]byte) ([]byte, error) funksiya signaturasida hamma joyda takrorlangan inline versiyadan ancha yaxshi o'qiladi. Closure'larni qisqa saqlang; ichiga bir sahifalik mantiq ko'milgan closure xuddi shu mantiq nomlangan funksiyaga chiqarilib, qiymat orqali murojaat qilingan holatdan ko'ra testlash va o'qish qiyinroq. Va "bu higher-order funksiya bo'lishi kerakmi yoki oddiy sikl" savolini har ikki yo'nalishda ham refleks emas, haqiqiy dizayn savoli sifatida qarang — maqsad, Godagi doimgidek, keyingi o'quvchi bir qarashda ishonishi eng oson bo'lgan versiya.

Bir nechta mashq: multiplier closure qaytaradigan funksiya yozing; predikatga mos keladigan elementlarnigina saqlaydigan generic filter yozing; generic reduce yozing; ikkita bir argumentli funksiyani bittaga birlashtiring (compose(f, g)(x) == f(g(x))); va haqiqatan foydali qiyinroq mashq sifatida, har qanday func(T) R ni (bunda T comparable) o'rab, natijalarni kirish bo'yicha keshlaydigan, shunda bir xil argument bilan takroriy chaqiruvlar qayta hisoblashni o'tkazib yuboradigan memoize funksiyasini yozing.

Funksiyalarga qiymat sifatida qarash — Go tilida moslashuvchan dasturlashning muhim poydevoridir. Keyingi bobda biz metodlar — receiver orqali muayyan tipga biriktirilgan funksiyalar va ularda pointer yoki value receiver tanlash qoidalari bilan tanishamiz.