Перейти до змісту

Масиви та зрізи

У 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

Масиви — це типи-значення: присвоєння чи передавання масиву копіює всі елементи:

x := [3]int{1, 2, 3}
y := x          // повна копія
y[0] = 99
fmt.Println(x[0], y[0])   // output: 1 99

Масиви порівнюються через ==, якщо їхній тип елемента порівнюваний:

fmt.Println([2]int{1, 2} == [2]int{1, 2})   // output: true

На практиці масиви напряму оголошують рідко. Їхній фіксований розмір надто негнучкий, а копіювання під час передавання людей дивує. Здебільшого вони виринають як сховище-основа за зрізом або для даних фіксованого розміру на кшталт геш-дайджеста ([32]byte).

Зрізи: робоча конячка

Зріз — це легкий заголовок із трьох слів: вказівник на масив-основу, довжина та місткість (capacity), — який описує неперервну ділянку цього масиву. Сам зріз не містить елементів; він на них вказує.

Нульове значення зрізу — nil: довжина 0, місткість 0, без масиву-основи. Із nil-зрізу безпечно читати довжину, проходити його через range та викликати на ньому append.

var s []int           // nil-зріз — у типі немає [N]
fmt.Println(s == nil, len(s))   // output: true 0

Створення зрізів

Літерал — створює масив-основу і зріз за один крок:

s := []int{1, 2, 3}
fmt.Println(s, len(s))   // output: [1 2 3] 3

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 через ...:

a := []int{1, 2}
b := []int{3, 4}
a = append(a, b...)
fmt.Println(a)            // output: [1 2 3 4]

З погляду 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

Джерела