Go / Generic үндэс

Generic үндэс

Та int тоонуудын хамгийн их утгыг олох функц бичлээ. Дараа нь float64-д мөн адил функц хэрэгтэй болов. Тэгээд string-д. Яг адилхан логик, зөвхөн төрөл л өөр — кодыг гурав давтах шаардлагатай болсон. Generic нь энэ асуудлыг шийддэг: нэг функц эсвэл бүтцийг олон төрлийн өгөгдөлд ашиглах боломж олгодог. Go 1.18-аас эхлэн generic дэмжигдсэн бөгөөд өнөөдөр Go-ийн стандарт library-д өргөн хэрэглэгддэг.

Type parameter — төрлийн параметр

Generic функцэд [T any] гэсэн type parameter нэмдэг. T нь төрлийн нэр (дурын үсэг байж болно), any нь хязгаарлалт (constraint):

go
package main

import "fmt"

// Generic функц: T дурын төрөл байж болно
func Хамгийн их[T int | float64 | string](а, б T) T {
    if а > б {
        return а
    }
    return б
}

func main() {
    // int-тэй ажиллана
    fmt.Println(Хамгийн их(10, 20))       // 20

    // float64-тэй ажиллана
    fmt.Println(Хамгийн их(3.14, 2.71))   // 3.14

    // string-тэй ажиллана
    fmt.Println(Хамгийн их("болд", "энхээ")) // энхээ
}

T int | float64 | string нь "T нь эдгээр гурван төрлийн аль нэг байж болно" гэсэн утгатай. Go compiler нь функцийг дуудах үед T-ийн утгыг автоматаар тодорхойлдог.

Constraint — хязгаарлалт

Constraint нь type parameter-т ямар төрлүүд зөвшөөрөгдөх вэ гэдгийг тодорхойлдог. golang.org/x/exp/constraints package болон cmp package-д бэлэн constraint-ууд байдаг. Өөрийн constraint интерфэйс болгон тодорхойлж болно:

go
package main

import "fmt"

// Тоон төрлүүдийн constraint
type Тоо interface {
    int | int8 | int16 | int32 | int64 |
        uint | uint8 | uint16 | uint32 | uint64 |
        float32 | float64
}

// Slice-ийн нийлбэр тооцоолох generic функц
func Нийлбэр[T Тоо](утгууд []T) T {
    var нийт T
    for _, v := range утгууд {
        нийт += v
    }
    return нийт
}

// Дундаж утга олох
func Дундаж[T Тоо](утгууд []T) float64 {
    if len(утгууд) == 0 {
        return 0
    }
    нийт := Нийлбэр(утгууд)
    return float64(нийт) / float64(len(утгууд))
}

func main() {
    бүхэл := []int{10, 20, 30, 40, 50}
    fmt.Println("Нийлбэр:", Нийлбэр(бүхэл))       // 150
    fmt.Println("Дундаж:", Дундаж(бүхэл))           // 30

    бутархай := []float64{1.5, 2.5, 3.0}
    fmt.Println("Нийлбэр:", Нийлбэр(бутархай))     // 7
}

Тоо constraint нь бүх тоон төрлүүдийг нэгтгэсэн интерфэйс юм. Ингэснээр Нийлбэр функцийг нэг удаа бичиж бүх тоон төрлүүдэд ашиглах боломжтой болно.

Generic бүтэц — Generic struct

Функцээс гадна бүтэц (struct) болон slice-ийн бүтцийг generic болгож болно. Жишээ нь Stack өгөгдлийн бүтэц:

go
package main

import (
    "fmt"
    "errors"
)

// Generic Stack — дурын төрлийн өгөгдөл хадгалах
type Stack[T any] struct {
    утгууд []T
}

func (s *Stack[T]) Push(утга T) {
    s.утгууд = append(s.утгууд, утга)
}

func (s *Stack[T]) Pop() (T, error) {
    var тэг T
    if len(s.утгууд) == 0 {
        return тэг, errors.New("stack хоосон байна")
    }
    сүүлийн := s.утгууд[len(s.утгууд)-1]
    s.утгууд = s.утгууд[:len(s.утгууд)-1]
    return сүүлийн, nil
}

func (s *Stack[T]) Хэмжээ() int {
    return len(s.утгууд)
}

func main() {
    // int Stack
    бүхэлStack := &Stack[int]{}
    бүхэлStack.Push(1)
    бүхэлStack.Push(2)
    бүхэлStack.Push(3)

    утга, _ := бүхэлStack.Pop()
    fmt.Println("Pop:", утга)        // 3
    fmt.Println("Хэмжээ:", бүхэлStack.Хэмжээ()) // 2

    // string Stack — яг адилхан бүтэц, өөр төрөл
    үгStack := &Stack[string]{}
    үгStack.Push("сайн")
    үгStack.Push("уу")
    үг, _ := үгStack.Pop()
    fmt.Println("Үг:", үг)           // уу
}

Stack[int] болон Stack[string] нь яг адил кодоос, зөвхөн өгөгдлийн төрлөөр ялгардаг. Generic байхгүй байсан бол хоёр тусдаа бүтэц бичих байсан.

Map болон Filter — функциональ хэлбэр

Generic-ийн хамгийн өргөн хэрэглээний нэг бол slice дээр ажилладаг туслах функцүүд юм:

go
// Map: slice-ийн утга бүрийг хувиргана
func Map[T, U any](утгууд []T, fn func(T) U) []U {
    үр_дүн := make([]U, len(утгууд))
    for i, v := range утгууд {
        үр_дүн[i] = fn(v)
    }
    return үр_дүн
}

// Filter: нөхцөл хангасан утгуудыг шүүнэ
func Filter[T any](утгууд []T, fn func(T) bool) []T {
    var үр_дүн []T
    for _, v := range утгууд {
        if fn(v) {
            үр_дүн = append(үр_дүн, v)
        }
    }
    return үр_дүн
}

func main() {
    тоонууд := []int{1, 2, 3, 4, 5, 6}

    // Бүгдийг хоёр дахин нэмэгдүүлэх
    хоёр дахин := Map(тоонууд, func(n int) int { return n * 2 })
    fmt.Println(хоёр дахин) // [2 4 6 8 10 12]

    // Зөвхөн тэгш тоонуудыг авах
    тэгш := Filter(тоонууд, func(n int) bool { return n%2 == 0 })
    fmt.Println(тэгш) // [2 4 6]

    // string slice-д map хэрэглэх
    нэрнүүд := []string{"болд", "энхээ", "дорж"}
    урттай := Map(нэрнүүд, func(нэр string) int { return len(нэр) })
    fmt.Println(урттай) // [4 5 4]
}

Энэ хэв маяг нь Go 1.21-ийн slices package-д бэлнээр орсон — slices.Map, slices.Contains, slices.Sort зэрэг generic функцүүд ашиглахад бэлэн байна.

Дараагийн хичээлд:

Generic-ийн дараа Go-ийн нэг чухал хэрэгсэл болох context package-ийг судална. Context нь хүсэлтийн хугацааг хязгаарлах, цуцлах дохио дамжуулах, request-ийн туршид өгөгдөл хуваалцах боломж олгодог — production програмд зайлшгүй шаардлагатай.