Go тілі: нөлден өз блогыңызға дейін 50-сабақ (барлығы 50)
Әрі қарай қайда: дженериктер және курстан кейінгі жол
Go курсының елуінші, соңғы сабағы. Дженериктер — үшеудің орнына бір функция, ал типке қойылған шартты компилятор тексереді: `string` іске қосылғанға дейін өтпейтіні өлшенді. Олардың `any`-ден несімен жақсы екені де өлшенді: 534 наносекунд пен 1801.
Не үшін керек
Курс бітіп келеді. Блог жұмыс істейді, өз доменінде HTTPS астында тұр, ұқыпты тоқтайды әрі қайта жүктеуден аман өтеді. Екі әңгіме қалды: өтпеген нәрсеміз туралы әрі әрі қарай қайда бару туралы.
Өтпегеніміз — дженериктер, көп типке бір код жазу тәсілі. Бұл курстың соңғы тақырыбы болуы кездейсоқ емес: онсыз ұзақ өмір сүруге болады, ал түсіну үш дерлік бірдей функцияны қолмен жазып шыққанда оңайырақ.
Бірден тұтас
Жаңа қалта, go mod init sabaq47:
package main
import (
"fmt"
"sort"
)
// Number — тип емес, шарт: мұнда плюспен қосылатын әрі сан сияқты ұстанатын
// нәрсенің бәрі жарайды. Тильда «іші осындай өз типтерің де» дегенді білдіреді.
type Number interface {
~int | ~int64 | ~float64
}
// Sum кез келген сандар тілімін қосады. Дженериктерге дейін мұндай функцияны
// үшеу жазатын: int-ке, int64-ке әрі float64-ке — денесі бірдей.
func Sum[T Number](xs []T) T {
var total T
for _, x := range xs {
total += x
}
return total
}
// Keys сөздіктің кілттерін қайтарады. K comparable — өйткені сөздіктің кілті
// тек салыстырылатын нәрсе бола алады; V any — мән бізге бәрібір.
func Keys[K comparable, V any](m map[K]V) []K {
out := make([]K, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}
// Views — горутиналар сабағындағы қарау санағышымыз, тек енді ол немен
// санайтынын білмейді: мақаламен, тегпен әлде оқырманмен.
type Views[K comparable] struct {
counts map[K]int
}
func NewViews[K comparable]() *Views[K] {
return &Views[K]{counts: make(map[K]int)}
}
func (v *Views[K]) Add(key K) { v.counts[key]++ }
func (v *Views[K]) Count(key K) int { return v.counts[key] }
// Балмен қойылған баға — өз типі, бірақ іші int. Number-дегі тильда оған да рұқсат етеді.
type Score int
func main() {
fmt.Println("== үшеудің орнына бір функция")
fmt.Println("бүтін: ", Sum([]int{2, 3, 4}))
fmt.Println("бөлшек: ", Sum([]float64{0.5, 0.25}))
fmt.Println("өз типі: ", Sum([]Score{10, 20}))
fmt.Println()
fmt.Println("== кез келген сөздіктің кілттері")
ages := map[string]int{"Абай": 41, "Мұқағали": 45}
names := Keys(ages)
sort.Strings(names)
fmt.Println("жолдар: ", names)
years := Keys(map[int]string{1845: "Абай", 1931: "Мұқағали"})
sort.Ints(years)
fmt.Println("сандар: ", years)
fmt.Println()
fmt.Println("== немен санайтыны бәрібір санағыш")
byArticle := NewViews[string]()
byArticle.Add("bir-jazba")
byArticle.Add("bir-jazba")
fmt.Println("мақала бойынша:", byArticle.Count("bir-jazba"))
byYear := NewViews[int]()
byYear.Add(2026)
fmt.Println("жыл бойынша: ", byYear.Count(2026))
}
Шығатыны:
== үшеудің орнына бір функция
бүтін: 9
бөлшек: 0.75
өз типі: 30
== кез келген сөздіктің кілттері
жолдар: [Абай Мұқағали]
сандар: [1845 1931]
== немен санайтыны бәрібір санағыш
мақала бойынша: 2
жыл бойынша: 1
Талдау
Тип параметрі — тағы бір аргумент, тек ол тип
Кәдімгі функция мәндерді қабылдайды. Дженерик оған қоса типті қабылдайды — кәдімгі жақшалардың алдындағы тік жақшада:
func Sum[T Number](xs []T) T
T — шақыру орнында белгілі болатын типтің аты. []int-пен шақырдыңыз — T int болды; []float64-пен шақырдыңыз — float64 болды. Компилятор типті өзі қояды, сондықтан мысалда еш жерде Sum[int](...) деп жазылмаған: ол аргументтен көрініп тұр.
Дженериктерсіз бұл — денесі бірдей үш функция: SumInt, SumInt64, SumFloat. Денедегі бірінші қате — үш түзету, әрі біреуін бір күні ұмытады.
Шарт — дженериктегі ең бастысы
Number — тип те емес, ескі мағынадағы интерфейс те емес. Бұл — шарт: T-ға не болуға рұқсат етілгенінің тізімі.
type Number interface {
~int | ~int64 | ~float64
}
Тильда «іші осындай кез келген өз типің де» дегенді білдіреді. Сондықтан мысалда type Score int деп жарияланған Score те қосылады.
Шарт — әшекей емес. Онсыз функцияның денесі жиналмас еді: компилятор T-ға + қолдануға болатынын білмейді. Әрі ол шақыруды да тексереді:
$ go build ./...
./main.go:20:17: string does not satisfy Number (string missing in ~int | ~int64 | ~float64)
Жолдар плюспен қосылады, бірақ сан емес. Қате іске қосылғанға дейін келеді, бірінші сәтсіз кеште емес, — бұл істің бүкіл мәні.
Бұл any-ден несімен жақсы
Дженериктерге дейін әртүрлі нәрсені any арқылы әрі орнында типті талдау арқылы қосатын. Жұмыс істейді, бірақ оның ақысы бар. Мың саннан тұратын тілімде өлшенді:
BenchmarkSumGeneric-8 2238902 533.7 ns/op 0 B/op 0 allocs/op
BenchmarkSumAny-8 663471 1801 ns/op 0 B/op 0 allocs/op
534 наносекунд пен 1801 — үш жарым есе. Айырмашылық жадта емес (екі жағдайда да бөлу жоқ), жұмыста: any нұсқасында әр элементте тип талданады, ал дженерик типті алдын ала біледі әрі сандарды тікелей қосады.
Мұны өзіңіз өлшеген жөн: go test -bench . -benchmem — тесттер сабағындағы сол команда, тек жылдамдық туралы.
Дженериктер қашан керек емес
Жаңадан бастаушының азғыруы — бәрін дженерик қылу. Одан ұстап қалатын үш ереже:
- Бір типке арналған бір функция — себеп емес.
func words(s string) intөзі болып қала береді; мұнда тип параметрі жақша қосады да, басқа ештеңе бермейді. - Интерфейс жиі жақсырақ. Әртүрлі типке әртүрлі мінез керек болса — бұл интерфейс, интерфейстер сабағындағы
blog.Storeсияқты. Дженерик — әртүрлі дерек типтерінің үстіндегі бірдей мінез туралы. - Оқылымдылық қымбатырақ.
func Map[T, U any](xs []T, f func(T) U) []U— әдемі, бірақ мұндайдың үш ішінде тұрған шақыруы кәдімгі циклден нашар оқылады. Go-да цикл — ұят емес.
Жақсы белгі: сіз екінші дерлік бірдей функцияны жазып та қойдыңыз әрі үшіншісін көшіріп жатырсыз. Міне енді.
Стандартты кітапханада олар бар
Жазғың келетіннің жартысын жазудың қажеті жоқ:
slices—slices.Sort,slices.Contains,slices.Index,slices.Max;maps—maps.Keys,maps.Values. Бір нәзіктік: олар тілім емес, тізбек қайтарады, сондықтан тілімді былай алады:keys := slices.Collect(maps.Keys(m)). МысалдағыKeys-іміз — бірден тілім қайтаратын оқу көшірмесі;cmp— салыстыру әріcmp.Or;sync.OnceValue— бір рет санап, дайынын қайтару.
Әрі қарай қайда бару керек
Курс бітті, тіл — жоқ. Жеті бағыт, әрқайсысында ол не үшін керегі туралы бір жол:
slices,maps,cmp— енді оқи алатыныңыз: олар тұтасымен дженериктерде.- SQLite орнына Postgres — сол
database/sql, басқа драйвер әрі нағыз қосылымдар пулы. Блогтағы SQLite таңдау болатын, заң емес. errgroupменsync.WaitGroup— горутина екеу емес, қырық болғанда әрі бәрі бітуге тиіс болғанда.net/http/pprof— бағдарлама уақытты қайда өткізетінін болжамай білу.go test -fuzz— кірісті өзі ойлап табатын әрі сіз ойламағанды табатын тесттер.- Метрикалар мен трассировка —
expvar, Prometheus, OpenTelemetry: қанша сұраныс, қайсысы баяу, қай жерде күтті. - Түймемен жинау — GitHub Actions: әр коммитке тесттер әрі бір командамен деплой.
Оқу реті мынадай: go.dev/doc/effective_go, содан кейін Go by Example, одан соң бөтен код — стандартты кітапханадан бастап, ол оқылымды.
Блогта не салуға болады
Курстағы блог — аяқталған нәрсе емес, іргетас. Әрі қарай не сұранып тұр, күрделілігі өсу ретімен:
- әлеуметтік желіге арналған сурет —
og:image, сілтеме жол емес, сілтеме болып көрінуі үшін; - жобалар — мақала бірден бәріне көрінбейді; бір өріс әрі сұраныстағы бір тексеру;
- пікірлер — форма, кесте, модерация әрі тағы да CSRF;
- бірнеше автор — құқықтар бізде бар, автор беті жетіспейді;
- хаттар — поштаны растау әрі құпиясөзді қалпына келтіру.
Сабақ картасы
Сіз енді не істей аласыз
Елу сабақ бұрын біз төрт жолдан тұратын бағдарлама жаздық. Қазір сіздің артыңызда блог тұр: ол мақалаларды көшірулері бар дерекқорда сақтайды, олардан іздейді, адамдарды құпиясөзбен кіргізеді әрі сессияларды ұстайды, құқықтарды ажыратады, суреттерді қабылдайды, JSON береді, жарыстарға тексерілген тесттермен қапталған, өз доменінде HTTPS-пен жұмыс істейді, ұқыпты тоқтайды әрі сақтық көшірме жасай алады.
Репозиторийдегі отыз төрт қадам — әр сабақтағы сол блог, әрі кез келгенін бір командамен іске қосуға болады. Бірдеңе ұмытылса, оларға оралыңыз: ондағы код мәтіндегімен бірдей.
Өз сөзіңізбен айтыңыз
Қарамай, дауыстап немесе қағазға жауап беріңіз. Жауаптар — сабақтың соңында.
- Тип шақыруда бәрібір қойылатын болса, дженерикке типке қойылған шарт не үшін керек?
- Типті талдайтын
anyнеге тип параметрінен баяу? - Дженерик мүмкін болса да, қашан керек емес?
Тапсырма
Міндетті. Блогыңыздан денесі дерлік бірдей екі функцияны тауып, оларды тип параметрі бар біреуге біріктіріңіз. Ондай болмаса — Filter[T any](xs []T, keep func(T) bool) []T жазып, оны мақалалар тізіміне қолданыңыз.
Қалауыңызша.
- Өз нұсқаңызды
anyнұсқасына қарсы өлшеңіз:go test -bench . -benchmem. - Горутиналар сабағындағы қарау санағышын кез келген нәрсені санайтындай етіп қайта жазыңыз — ішінде мьютексі бар.
- Өз шағын көмекшілеріңізді
slicesпенmaps-қа ауыстырып, қанша жол кеткенін қараңыз.
Блогта бұл қайда тұрады
Еш жерде — әрі бұл дұрыс аяқталу. Уәде етілген блогтың өзегі дайын: ол жұмыс істейді, шығарылған әрі оны көрсетуге болады. Дженериктер оған керек емес, әрі сабақ үшін оларды қосу шыншыл болмас еді.
Жақсарту тізімі бар — ол кез келген жұмыс істеп тұрған жүйеде бар. Сабақтарда аталғаннан: кіруді теруден қорғау, мерзімі өткен сессияларды тазалау әрі «барлық жерден шығу» түймесі, мақалалар тізімін беттерге бөлу, мақаламен бірге суреттерді өшіру, іздеудегі қазақ морфологиясы, тексерудегі дерекқордың дайындығы. Бұл — курстың қарызы емес, кәдімгі пайдалану кезегі.
Әрі қарай блогты курс емес, сіз өзгертесіз.
Жауаптар
Жауаптарды көрсету
- Өйткені шартсыз компилятор
T-мен не істеуге болатынын білмейді: қосу, салыстыру, әдіс шақыру. Шарт — функцияның денесіне берілген рұқсат әрі оны шақырушыға қойылған тексеру. - Өйткені
anyнұсқасында тип әр элементте, жұмыс кезінде талданады. Дженерик типті жинау кезінде біледі — өлшенді: 534 наносекунд пен 1801. - Функция бір типке керек болғанда; типтерге әртүрлі мінез керек болғанда — онда интерфейс; әрі тип параметрінен оқылымдылық ұтқанынан көбірек ұтылғанда.
Пікірлер (0)
Пікір қалдыру үшін кіріңіз →
Әзірге пікір жоқ. Бірінші болыңыз.