1. Giriş
Go ilə yenicə yazmağa başlayanda slice-lar “dinamik massiv” kimi görünür: []int var, s[i] etmək olur, döngü ilə keçmək olur, çoxlu element saxlamaq olur. Hər şey yaxşı da olardı, əgər slice-lar bəzən... canlı varlıqlar kimi davranmasaydılar. Bəzən eyni slice iki dəyişən vasitəsilə birdən görünür, bəzən uzunluq və tutum nədənsə fərqli olur, bəzən isə indeksləşmə sərhədləri sizin “hiss etdiyiniz” şeylə üst-üstə düşmür.
Gəlin təxmin etməyi dayandıraq və davranışı öncədən proqnozlaşdırmağa başlayaq. Go proqramçılar başlarında sadə bir model saxlayırlar: slice öz elementləri deyil, kiçik bir təsvir-strukturudur, hansı ki elementlərin harada yerləşdiyini və nə qədərinin əlçatan olduğunu göstərir. Bu modelə slice header deyilir.
Məlumatlara “tutacaq” kimi slice: pointer + len + cap
İndi vacib bir fikir gəlir; xüsusilə yorğunsunuzsa, bunu iki dəfə oxumaq faydalıdır: slice dəyəri []T — “elementlərdən ibarət konteyner” deyil, yaddaşdakı müəyyən elementlər dəstinə baxdığımız pəncərənin təsviridir.
Bu “təsvirin” (slice header) məntiqi olaraq üç hissəsi var:
| Hissə | İnsan dilində necə oxunur | Davranışda nəyə cavabdehdir |
|---|---|---|
|
“haraya baxırıq” | niyə iki slice eyni elementləri görə bilər |
|
“nə qədər element görünür” | indeksləşmə sərhədləri 0..len-1, range-ın nə qədər keçəcəyi |
|
“sonra nə qədər ehtiyat var” | niyə slice-ın tutumu uzunluğundan böyük ola bilər |
Bunu belə təsəvvür etmək olar (şərti olaraq, kompilyatordan real çap kimi yox):
slice header:
+---------+-----+-----+
| pointer | len | cap |
+---------+-----+-----+
Vacibdir: bu mühazirədə “həqiqi göstəricilər” mövzusuna (&, *, unsafe) dərin girməyəcəyik. Hələlik ideyanı başa düşmək kifayətdir: içəridə “məlumata istinad” var.
2. len: görünən uzunluq və indeksləşmə sərhədləri
len(s) yazanda siz slice-dan soruşursunuz: bu pəncərə vasitəsilə hazırda neçə element əlçatandır. Və bu rəqəm qanundur. “təxmini göstərici” yox, “hansısa yerdə ümumiyyətlə nə qədər şey var” yox, məhz: indeks üzrə təhlükəsiz oxumaq/yazmaq üçün nə qədər element var.
Məhz len düzgün indeksləri müəyyən edir:
- ilk indeks: 0
- son indeks: len(s) - 1
- len(s) indeksi — artıq sərhəddən kənardır (və müraciət zamanı panic verəcək)
len ilə indekslər arasındakı əlaqəni gözlə görmək üçün kiçik nümunə:
package main
import "fmt"
func main() {
s := []int{10, 20, 30}
fmt.Println(len(s)) // 3
fmt.Println(s[0]) // 10
fmt.Println(s[2]) // 30
fmt.Println(s[3]) // panic: index out of range
}
Əgər əvvəllər null və ya undefined qaytaran dillərlə işləyibsinizsə, Go sizi tez öyrəşdirəcək: sərhəddən çıxmaq “yumşaq səhv” deyil, proqramın fövqəladə dayanmasıdır (panic). Düzünü desək, tədris tapşırıqlarında bu hətta faydalıdır: səhv dərhal görünür.
4. cap: tutum, ehtiyat və niyə bu “daha çox element” deyil
Tutum (cap) yeni başlayanlarda adətən tam iki hiss yaradır: “bu nə üçündür?” və “niyə len-ə bərabər deyil?”. Hər iki hiss normaldır.
Gəlin düzgün oxunuşda razılaşaq:
cap(s) — bu, eyni yaddaş sahəsində, slice-ın cari başlanğıcından etibarən, başqa yerə köçmədən neçə elementin ola biləcəyidir.
Deməli, cap — slice-ı səmərəli genişləndirməyə kömək edən “ehtiyatdır”. Amma (və bu vacibdir) cap indeks üzrə müraciət hüququ vermir. İndeksləşmə len üzrə yaşayır, cap isə daha çox daxili mexanika və performans haqqındadır.
“len cap-dən kiçikdir” vəziyyətini görmək üçün elə bir slice yaratmaq lazımdır ki, o “ehtiyatdan qısa” olsun. Ən sadə yol make-dir, amma burada onu yalnız len/cap göstərmək üçün istifadə edirik, detallara varmadan.
package main
import "fmt"
func main() {
s := make([]int, 2, 5) // len=2, cap=5
fmt.Println(len(s), cap(s)) // 2 5
fmt.Println(s) // [0 0]
}
Qeyd: yuxarıdakı nümunədə make funksiyasından istifadə etdik. Onu ətraflı növbəti mühazirədə izah edəcəyik, burada isə sadəcə fərqli len və cap olan slice yaratmaq üçün lazımdır.
Yuxarıdakı nümunədə nə aldıq:
- len=2 deməkdir: iki əlçatan element var, ikisi də hələ sıfırdır (int üçün zero value).
- cap=5 deməkdir: eyni yaddaş parçasında 5 elementə qədər yerləşdirmək olar, amma hazırda yalnız 2-si görünür.
Əgər s[2] müraciəti etsəniz, cap=5 olmasına baxmayaraq panic alacaqsınız. Bu məntiqlidir: ehtiyat — əlçatanlıq demək deyil.
package main
import "fmt"
func main() {
s := make([]int, 0, 3) // len=0, cap=3
fmt.Println(len(s), cap(s)) // 0 3
// fmt.Println(s[0]) // panic: len=0, indeksləşdirmək olmaz
fmt.Println(s) // []
}
Bu qısa formulu yadda saxlayın:
- len elementlərə düzgün müraciət üçün cavabdehdir;
- cap isə slice-ın nə qədər “rahat” böyüyəcəyini göstərir.
5. pointer: niyə iki slice eyni elementləri görə bilər
İndi ən “sehrli” hissə: header-dəki pointer. Söz qorxulu səslənir, amma fikir sadədir. Slice daxilində məlumatın ilk elementinin harada yerləşdiyi barədə informasiya var (şərti olaraq: “məlumatların başlanğıc ünvanı”). Buna görə iki fərqli slice dəyəri eyni elementlər dəstinə istinad edə bilər.
Məhz bu davranışı belə izah edir:
package main
import "fmt"
func main() {
s := []int{1, 2, 3}
t := s // elementi yox, yalnız header-i kopyalayırıq
t[0] = 99
fmt.Println(s) // [99 2 3]
fmt.Println(t) // [99 2 3]
}
Slice header modelinə görə baş verənlər belədir:
- t := s yalnız header-i kopyalayır (pointer/len/cap).
- pointer həm t-də, həm də s-də eyni baza massivinə (bəzən backing array deyilir) göstərir.
- ona görə də t[0] dəyişəndə “aşağıdakı” məlumat dəyişir və bu, s vasitəsilə görünür.
Sxemi belə çəkə bilərik. Şərtidir, amma faydalıdır:
s (header) ----+
|
v
[ 1 ][ 2 ][ 3 ] (data)
^
|
t (header) ----+
Bu şəkli mənimsəməyincə slice-lar “proqnozlaşdırılmaz” görünəcək. Mənimsəyəndən sonra isə çox məntiqli olacaqlar.
6. Niyə slice “ucuz” kopyalanır
Vacib məqam: Go-da hər şey dəyər kimi ötürülür. Bu o deməkdir ki, mənimsəmə və ya parametr ötürülməsi zamanı Go dəyişənin dəyərini kopyalayır.
Amma hiylə budur: massiv dəyəri ilə slice dəyəri “çəki” baxımından fərqlidir.
- [N]int massivində dəyər N elementi saxlayır.
- []int slice-ında dəyər üç hissəli header saxlayır (pointer/len/cap).
Ona görə də massivi kopyalamaq bütün elementləri kopyalamaqdır, slice-ı kopyalamaq isə “kiçik təsvir kartı”nı kopyalamaqdır.
Bunu funksiya üzərindən görək. Əvvəlcə elementi dəyişdiyimiz nümunə:
package main
import "fmt"
func setFirst(s []int) {
s[0] = 100
}
func main() {
nums := []int{1, 2, 3}
setFirst(nums)
fmt.Println(nums) // [100 2 3]
}
Niyə dəyişikliklər çöldə görünür? Çünki funksiyaya header-in kopyası gəldi, amma bu header-dəki pointer hələ də eyni məlumatlara göstərir.
Burada bir şeyi diqqətlə demək faydalıdır: funksiya “slice-ın istinadı”nı almayıb (biz hələ göstəriciləri keçmədik). Funksiya dəyərin kopyasını alıb, sadəcə bu dəyər ortaq məlumatlara göstərən kiçik bir təsvir olduğu üçün içində pointer var.
7. Təcrübə: kodda len və cap-i müşahidə edirik
Debug üçün mini alət: slice vəziyyətini çap edirik
Öyrənərkən beyin çox vaxt belə soruşur: “Bunu rəqəmlərlə görə bilərəm?” Bəli. Hətta lazımdır — xüsusilə də fmt.Printf-in diaqnostikada çox kömək etdiyi Go-da.
Tədris tətbiqimizdə istifadə edəcəyimiz kiçik printSliceState funksiyası yazaq. Bu tətbiq sadə olacaq: bir neçə ədəd oxuyuruq (məsələn, xərclər və ya qiymətlər), onları slice-a yığırıq və bəzən diaqnostika çap edirik.
package main
import "fmt"
func printSliceState(name string, s []int) {
fmt.Printf("%s: len=%d cap=%d data=%v\n", name, len(s), cap(s), s)
}
func main() {
a := make([]int, 2, 5)
printSliceState("a", a) // a: len=2 cap=5 data=[0 0]
}
Diqqət edin: biz pointer-i çap etmirik (və bu normaldır). Bu səviyyədə biz len və cap-i birbaşa müşahidə edə bilərik, “pointer”i isə təsirlər vasitəsilə anlaya bilərik: ortaq elementlər, dəyişikliklərin fərqli dəyişənlər vasitəsilə görünməsi və s.
Model tətbiqdə proqnozlaşdırıla bilən kod yazmağa necə kömək edir
Bir proqramda verilənlər toplusu saxlayanda adətən iki ssenari olur.
Birinci ssenari — elementlərin sayını əvvəldən bilirsiniz (məsələn, istifadəçi n daxil edir və siz dəqiq n ədəd oxuyursunuz). Bu halda uzunluğu n olan slice yaratmaq və onu indekslər üzrə doldurmaq rahatdır. Bu ssenaridə len “həqiqətən neçə element var” deməkdir və siz onları bir dəfə doldurursunuz.
package main
import (
"fmt"
)
func main() {
var n int
fmt.Scan(&n)
nums := make([]int, n) // len=n, cap=n
for i := 0; i < len(nums); i++ {
fmt.Scan(&nums[i])
}
fmt.Println(nums) // məsələn: [5 10 15]
}
Burada header modeli ona görə kömək edir ki, qarışmırsınız: len(nums) — “neçə ədəd oxuyuruq” və “neçə indeks əlçatandır” deməkdir. Hər şey dürüstdür.
İkinci ssenari — elementlərin sayını bilmir və onları tədricən yığırsınız. Bu halda adətən başqa yanaşma seçilir (sonuna əlavə etmə ilə). Amma bunu bir az sonra ətraflı izah edəcəyik; indi bizim üçün vacib olan budur ki, məhz cap tədrici böyüməni səmərəli edən parametrdir.
Yeri gəlmişkən, tarixi fakt: element əlavə etmənin rahat mexanizmi yaranmazdan əvvəl proqramçılar tez-tez əl ilə belə yoxlama yazırdılar: if n+1 > cap(list) { ... }, yeni bufer ayırır və məlumatı kopyalayırdılar. Belə kodları (və motivasiyasını) Go-nun təkamülü haqqında hekayələrdə görmək olar.
Zehni yoxlama: istənilən slice-ı necə “oxumaq” olar
Çox faydalı vərdişdir, xüsusilə tapşırıqlarda və müsahibələrdə: slice görəndə özünüzə üç sual verin.
Birinci sual: məlumat haradadır? (həmin pointer, yəni “başqası ilə ortaq baza massivim varmı?”). Bu sual “niyə başqa yerdə dəyişdi?” tipli təəccübü aradan qaldırır.
İkinci sual: neçə element görürəm? (bu len-dir). Bu sual panic: index out of range-dən xilas edir.
Üçüncü sual: ehtiyat var, və bu nəyə lazımdır? (bu cap-dir). Bu sual elementlərin əlavə edilməsinin “həmişə pulsuz” olduğuna kor-koranə inanmaqdan xilas edir və kod yazmağa kömək edir ki, o, RAM-ı qızdıran mikrodalğalı sobaya çevrilməsin.
Əgər len ilə cap arasındakı fərqi daha da “toxunula bilən” hiss etmək istəyirsinizsə, proqramınıza doldurmadan əvvəl və sonra diaqnostik çap əlavə edə bilərsiniz ki, cap-ın len ilə üst-üstə düşməsinin məcburi olmadığını görün:
package main
import "fmt"
func main() {
s := make([]int, 2, 5)
fmt.Println(len(s), cap(s), s) // 2 5 [0 0]
s[0] = 7
s[1] = 8
fmt.Println(len(s), cap(s), s) // 2 5 [7 8]
}
Burada vacib bir şey görünür: elementlərin doldurulması nə len-i, nə də cap-i dəyişmir. Bu məntiqlidir: siz aşağıdakı məlumatları dəyişdirdiyiniz üçün header mütləq dəyişməli deyil.
8. Slice header-i başa düşərkən tipik səhvlər
Səhv №1: t := s-in elementləri kopyaladığını düşünmək.
Bu ən çox rast gəlinən tələlərdən biridir. Başlanğıcda insan “yeni dəyişən” gördüyünə görə “yeni siyahı” gözləyir. Slice vəziyyətində header kopyalanır, məlumatlar isə çox vaxt ortaq qalır. Nəticə olaraq t[0] dəyişməsi s[0]-i də dəyişə bilər. Təəccüblənməmək üçün belə düşünmək faydalıdır: “Mən eyni məlumatlara ikinci tutacaq düzəltdim”.
Səhv №2: cap-ı “element sayı” ilə qarışdırmaq və cap üzrə indeksləşdirmək.
Bəzən tələbə cap=10 görüb qərar verir: “Aha, mənim 10 elementim var”, sonra da s[9] yazır. Amma əgər len=2-dirsə, siz elə bil binanın yalnız ilk iki mərtəbəsinin tikildiyi halda 9-cu mərtəbəyə tullanmağa çalışdınız. Go gözlənilən şəkildə reaksiya verir: panic. Qayda sadədir: yalnız len-ə qədər indeksləşdiririk, cap təkbaşına giriş vermir.
Səhv №3: len-i “artıq doldurduğum qədər” hesab etmək, “mövcud element sayı” kimi yox.
Bu xüsusilə make([]int, n) zamanı görünür: çoxları bunun “tutumlu boş siyahı n” olduğunu düşünür. Əslində bu uzunluğu n olan slice-dır, və orada artıq default dəyərlərdən ibarət n element var. Əgər “hələ boşdur, amma ehtiyat var” istəyirsinizsə, bu başqa ssenaridir. Hələlik ideyanı yadda saxlayın: len — həqiqətən mövcud elementlərdir, hətta onlar sıfır olsa belə.
Səhv №4: pointer sözündən qorxub modeli rədd etmək.
“Göstərici” sözü çoxlarını qorxudur, çünki başqa dillərdə və ya C/C++-da o, ağrı və əziyyətlə (bəzən də gecə kabusları ilə) əlaqəlidir. Amma slice header kontekstində bu söz yalnız bir faktı təsvir edir: “içəridə məlumatların başlanğıcına istinad saxlanılır”. Slice-ları düzgün başa düşmək üçün ünvanlarla əl ilə işləməyi bilməyə ehtiyac yoxdur. Ortaq pointer-in ortaq elementlər demək olduğunu xatırlamaq kifayətdir.
Səhv №5: döngələr üçün len-i yeganə həqiqət mənbəyi kimi istifadə etməmək.
Bəzən “gözlənilən ölçü” üzrə, daxil edilən n üzrə, hansısa sabit üzrə döngü yazırlar, amma unudurlar ki, slice artıq başqa uzunluqda ola bilər. Bu ya sərhəddən çıxmağa, ya da elementlərin buraxılmasına səbəb olur. “Döngü həmişə len(s)-ə qədər gedir” vərdişi kodu darıxdırıcı edir — və məlumdur ki, darıxdırıcı kod daha az sınır (bu, az qala tərifdir).
GO TO FULL VERSION