Масиви та зрізи¶
У Go є два типи послідовностей, які виглядають схоже, а поводяться зовсім по-різному. Масиви (arrays) мають фіксовану довжину, вшиту в їхній тип, і копіюються за значенням. Зрізи (slices) — це розширювані вікна в масив, і саме до них ви вдаєтеся в ~99% випадків. Розуміння зв'язку між ними — ключ до використання зрізів без сюрпризів.
Масиви: фіксована довжина, частина типу¶
Довжина масиву є частиною його типу. [3]int та [4]int — це два
різні, несумісні типи.
var a [3]int // три int, усі обнулені
fmt.Println(a) // output: [0 0 0]
fmt.Println(len(a)) // output: 3
Літерали, та [...], щоб компілятор порахував довжину сам:
b := [3]int{10, 20, 30}
c := [...]int{1, 2, 3, 4} // довжина виводиться як 4
fmt.Println(b, len(c)) // output: [10 20 30] 4
Масиви — це типи-значення: присвоєння чи передавання масиву копіює всі елементи:
Масиви порівнюються через ==, якщо їхній тип елемента порівнюваний:
На практиці масиви напряму оголошують рідко. Їхній фіксований розмір
надто негнучкий, а копіювання під час передавання людей дивує. Здебільшого
вони виринають як сховище-основа за зрізом або для даних фіксованого
розміру на кшталт геш-дайджеста ([32]byte).
Зрізи: робоча конячка¶
Зріз — це легкий заголовок із трьох слів: вказівник на масив-основу, довжина та місткість (capacity), — який описує неперервну ділянку цього масиву. Сам зріз не містить елементів; він на них вказує.
Нульове значення зрізу — nil: довжина 0, місткість 0, без масиву-основи.
Із nil-зрізу безпечно читати довжину, проходити його через range та
викликати на ньому append.
Створення зрізів¶
Літерал — створює масив-основу і зріз за один крок:
make — виділяє зріз заданої довжини (усе обнулене), за бажанням з
наперед зарезервованою додатковою місткістю:
s := make([]int, 3) // len 3, cap 3 → [0 0 0]
t := make([]int, 0, 10) // len 0, cap 10 — порожній, але з місцем на 10
fmt.Println(len(s), len(t), cap(t)) // output: 3 0 10
Довжина та місткість¶
len — це скільки елементів зріз містить зараз; cap — це скільки він
може вмістити до того, як масив-основу доведеться перевиділяти.
Резервування місткості через make уникає повторних перевиділень, коли
ви приблизно знаєте, наскільки великим стане зріз.
append: нарощування зрізу¶
append повертає (можливо, новий) зріз — результат потрібно
присвоїти назад. Якщо в масиві-основі є вільна місткість, append
пише на місці; якщо ні — виділяє більший масив, копіює елементи туди й
повертає зріз, що вказує на новий масив.
s := []int{1, 2}
s = append(s, 3) // один елемент
s = append(s, 4, 5) // кілька одразу
fmt.Println(s) // output: [1 2 3 4 5]
Розгорнути інший зріз у append через ...:
З погляду Python:
append— це не метод, що змінює об'єкт на місці, якlist.append. Це функція, яка повертає нарощений зріз, бо нарощування може перемістити дані. Забутиs = append(s, ...)— це класична помилка початківця.
Зрізання: s[low:high]¶
s[low:high] створює новий заголовок зрізу, що охоплює індекси від low
до high, не включно. Обидві межі необов'язкові (s[:2], s[1:],
s[:]).
s := []int{0, 1, 2, 3, 4}
fmt.Println(s[1:3]) // output: [1 2]
fmt.Println(s[:2]) // output: [0 1]
fmt.Println(s[3:]) // output: [3 4]
Найважливіше: зрізання не копіює. Новий зріз ділить той самий масив-основу, тож запис через один видно через інший.
s := []int{0, 1, 2, 3, 4}
mid := s[1:3]
mid[0] = 99
fmt.Println(s) // output: [0 99 2 3 4] — s теж змінився
Пастка спільного масиву-основи¶
Оскільки зрізи ділять сховище, append може змінити дані, на які ви не
очікували. Якщо в підзрізу є вільна місткість, додавання до нього
перезаписує сусідні елементи оригіналу:
s := []int{1, 2, 3, 4}
head := s[:2] // len 2, але cap усе ще 4
head = append(head, 99) // пише в s[2] — там є місце
fmt.Println(s) // output: [1 2 99 4]
Щоб примусити незалежну копію, або скопіюйте через copy у свіжий зріз,
або скористайтеся зрізом із трьома індексами s[low:high:max], який
обмежує місткість до max-low, тож наступний append гарантовано
перевиділить пам'ять:
s := []int{1, 2, 3, 4}
head := s[:2:2] // len 2, cap 2 — місткість обмежено
head = append(head, 99) // місткість перевищено → новий масив-основа
fmt.Println(s) // output: [1 2 3 4] — оригінал недоторканий
copy: явне копіювання елементів¶
copy(dst, src) копіює min(len(dst), len(src)) елементів і повертає цю
кількість. Це ідіоматичний спосіб продублювати дані зрізу:
src := []int{1, 2, 3}
dst := make([]int, len(src))
n := copy(dst, src)
dst[0] = 99
fmt.Println(n, src, dst) // output: 3 [1 2 3] [99 2 3]
Ітерування¶
for range дає індекс і копію кожного елемента. Відкиньте значення
через _ або відкиньте обидва й лишіть тільки індекс:
s := []string{"a", "b", "c"}
for i, v := range s {
fmt.Println(i, v)
}
// output:
// 0 a
// 1 b
// 2 c
Оскільки v — це копія, присвоєння їй нічого не робить зі зрізом;
звертайтеся через s[i], щоб змінити елемент.
Видалення елемента¶
Вбудованого remove немає; ідіома — це append з розгортанням, щоб
закрити проміжок (зі збереженням порядку):
s := []int{10, 20, 30, 40}
i := 1
s = append(s[:i], s[i+1:]...)
fmt.Println(s) // output: [10 30 40]
Багатовимірні зрізи¶
Справжнього двовимірного зрізу в Go немає — ви будуєте зріз зрізів, і кожен внутрішній зріз виділяється окремо, тож рядки можуть навіть мати різну довжину:
grid := make([][]int, 2)
for i := range grid {
grid[i] = make([]int, 3)
}
grid[1][2] = 7
fmt.Println(grid) // output: [[0 0 0] [0 0 7]]
Швидка довідка¶
| Операція | Результат |
|---|---|
[3]int{...} |
масив — фіксована довжина, копіюється за значенням |
[]int{...} |
літерал зрізу |
make([]T, n) |
зріз довжини n, обнулений |
make([]T, n, c) |
довжина n, місткість c |
len(s) / cap(s) |
поточна довжина / місткість основи |
s = append(s, x) |
нарощування (присвойте результат!) |
s[low:high] |
підзріз, ділить масив-основу |
s[low:high:max] |
підзріз з обмеженою місткістю |
copy(dst, src) |
копіює елементи, повертає кількість |
append(s[:i], s[i+1:]...) |
видалення індексу i |
Джерела¶
- Array types — go.dev/ref/spec#Array_types
- Slice types — go.dev/ref/spec#Slice_types
- Appending and copying slices — go.dev/ref/spec#Appending_and_copying_slices
- Slice expressions — go.dev/ref/spec#Slice_expressions
- Go blog: slices intro — go.dev/blog/slices-intro
- Go blog: arrays and slices usage — go.dev/blog/slices