Глиф Учебники

Урок 5. Массивы, срезы и append

Курс «Go» · урок 5 из 10 · 4 минуты чтения

Когда значений много — оценки класса, список городов, цены товаров, — заводить на каждое свою переменную неудобно. Для этого есть массивы и срезы. На практике в Go почти всегда работают со срезами, а массивы нужны, чтобы понять, как срезы устроены.

Массив

Массив — ряд значений одного типа фиксированной длины. Длина входит в тип: [3]int и [4]int — разные типы.

Go
package main

import "fmt"

func main() {
    var a [3]int
    a[0] = 10
    b := [...]string{"Минск", "Гомель", "Брест"}
    fmt.Println(a, len(a))
    fmt.Println(b[1], len(b))
}
Вывод
[10 0 0] 3
Гомель 3

Номера элементов — индексы — начинаются с нуля. Незаданные элементы получают нулевые значения. [...] просит компилятор посчитать длину по числу значений.

Массив копируется целиком при присваивании и при передаче в функцию, а его длину нельзя изменить. Поэтому чаще нужен срез.

Срез

Срез (slice) — «окно» на массив: он хранит ссылку на массив, длину и ёмкость. Срез может расти, и при передаче в функцию данные не копируются. Тип среза записывается без длины: []int.

Go
package main

import "fmt"

func main() {
    nums := []int{5, 3, 8}
    nums = append(nums, 1)
    nums = append(nums, 7, 2)
    fmt.Println(nums, len(nums))
    fmt.Println(nums[0], nums[len(nums)-1])
    fmt.Println(nums[1:3], nums[:2], nums[4:])
}
Вывод
[5 3 8 1 7 2] 6
5 2
[3 8] [5 3] [7 2]
индекс 0 1 2 3 4 5 nums[1:3] 5 3 8 1 7 2 [3 8] nums[:2] 5 3 8 1 7 2 [5 3] nums[4:] 5 3 8 1 7 2 [7 2] конец части — в неё не входит
Индексы среза nums идут с нуля: часть с 1 до 3 даёт 3 и 8, часть до 2 — 5 и 3, часть с 4 до конца — 7 и 2, а правая граница в часть не входит.

make и пустой срез

Срез заданной длины, заполненный нулями, создаёт make:

Go
package main

import "fmt"

func main() {
    squares := make([]int, 5)
    for i := range squares {
        squares[i] = i * i
    }
    fmt.Println(squares)

    var evens []int
    for i := 1; i <= 10; i++ {
        if i%2 == 0 {
            evens = append(evens, i)
        }
    }
    fmt.Println(evens, len(evens))
}
Вывод
[0 1 4 9 16]
[2 4 6 8 10] 5

for i := range squares перебирает индексы среза. Срез evens объявлен без значения — это nil-срез: длина 0, элементов нет, но append работает с ним как с пустым. Так срез обычно и собирают: начинают с пустого и добавляют.

По индексу можно обращаться только к существующим элементам. evens[0] = 2 для пустого среза — паника (аварийная остановка программы) index out of range [0] with length 0: сначала элемент нужно добавить через append или создать срез нужной длины через make. Подробно — в справке index out of range.

Длина и ёмкость

У среза есть ёмкость (cap) — сколько элементов поместится в массив под ним без нового выделения памяти. append, когда места нет, сам выделяет массив побольше и копирует в него элементы. Если размер известен заранее, место можно зарезервировать:

Go
package main

import "fmt"

func main() {
    s := make([]int, 0, 100)
    fmt.Println(len(s), cap(s))
    s = append(s, 1, 2, 3)
    fmt.Println(len(s), cap(s))
}
Вывод
0 100
3 100

make([]int, 0, 100) — длина 0, ёмкость 100: сто append пройдут без перевыделения памяти.

nums := []int{5, 3, 8} 5 3 8 len 3 cap 3 массив ровно на 3 числа nums = append(nums, 1) 5 3 8 места нет — старый массив больше не нужен копия 5 3 8 1 len 4 cap 6 nums = append(nums, 7, 2) 5 3 8 1 7 2 len 6 cap 6 место есть — тот же массив
У среза nums из трёх чисел ёмкость 3: для append(nums, 1) места нет, и создаётся массив побольше с копией элементов, а следующий append(nums, 7, 2) пишет в свободное место того же массива.

Перебор: range

range по срезу выдаёт пару — индекс и значение:

Go
package main

import "fmt"

func main() {
    cities := []string{"Минск", "Гродно", "Витебск"}
    for i, city := range cities {
        fmt.Println(i+1, city)
    }

    total := 0
    for _, n := range []int{4, 5, 6} {
        total += n
    }
    fmt.Println("сумма:", total)
}
Вывод
1 Минск
2 Гродно
3 Витебск
сумма: 15

Если индекс не нужен, вместо него пишут _: неиспользуемая переменная i была бы ошибкой компиляции. Значение в range — копия элемента: city = "…" внутри цикла срез не изменит. Чтобы изменить элемент, пишут cities[i] = ….

Части среза делят память

Часть среза s[a:b] не копирует элементы, а смотрит на тот же массив. Изменение через часть видно в исходном срезе:

Go
package main

import "fmt"

func main() {
    base := []int{1, 2, 3, 4, 5}
    part := base[1:3]
    part[0] = 100
    fmt.Println(base, part)

    clone := make([]int, len(base))
    copy(clone, base)
    clone[0] = -1
    fmt.Println(base, clone)
}
Вывод
[1 100 3 4 5] [100 3]
[1 100 3 4 5] [-1 100 3 4 5]

Независимую копию дают copy(куда, откуда) или короче — slices.Clone(base) из пакета slices. Подробно о разнице между массивом и срезом и о ловушках с append — в справке Срез и массив — разница.

base len 5 cap 5 part len 2 cap 4 1 0 100 1 3 2 4 3 5 4 part := base[1:3] part[0] = 100 один массив на двоих — base тоже видит 100 clone len 5 cap 5 -1 0 100 1 3 2 4 3 5 4 copy(clone, base) clone[0] = -1 свой массив — base не меняется
Срез part смотрит на тот же массив, что и base, начиная с индекса 1, поэтому запись 100 через part видна и в base; copy даёт clone свой массив, и запись -1 в clone не меняет base.

Срез срезов

Таблицу — например, поле игры — хранят как срез срезов. Каждую строку создают отдельно:

Go
package main

import "fmt"

func main() {
    grid := make([][]int, 3)
    for i := range grid {
        grid[i] = make([]int, 4)
    }
    grid[1][2] = 7
    fmt.Println(grid)
    fmt.Println(len(grid), len(grid[0]))
}
Вывод
[[0 0 0 0] [0 0 7 0] [0 0 0 0]]
3 4

Срезы строк

Пакет strings умеет разбивать строку на срез и собирать обратно:

Go
package main

import (
    "fmt"
    "slices"
    "strings"
)

func main() {
    words := strings.Fields("  раз   два три ")
    fmt.Println(len(words), words)
    fmt.Println(strings.Join(words, "-"))
    fmt.Println(strings.Split("a,b,,c", ","))
    fmt.Println(slices.Contains(words, "два"))
}
Вывод
3 [раз два три]
раз-два-три
[a b  c]
true

strings.Fields делит по любым пробелам и пропускает лишние, strings.Split — строго по разделителю, поэтому между двумя запятыми получилась пустая строка. slices.Contains проверяет, есть ли значение в срезе. Как отсортировать срез — в справке Как отсортировать срез.

Коротко

Проверьте себя

1. Что выведет fmt.Println(s[1:3]) для s := []int{10, 20, 30, 40}?
2. Почему пишут s = append(s, x), а не просто append(s, x)?
3. Что выведет программа?
Go
package main

import "fmt"

func main() {
    a := []int{1, 2, 3}
    b := a[:2]
    b[0] = 9
    fmt.Println(a[0])
}
4. Чем make([]int, 5) отличается от make([]int, 0, 5)?