Урок 5. Массивы, срезы и append
Когда значений много — оценки класса, список городов, цены товаров, — заводить на каждое свою переменную неудобно. Для этого есть массивы и срезы. На практике в Go почти всегда работают со срезами, а массивы нужны, чтобы понять, как срезы устроены.
Массив
Массив — ряд значений одного типа фиксированной длины. Длина входит в тип: [3]int и [4]int — разные типы.
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.
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]append(nums, 1)возвращает срез с добавленным элементом. Результат обязательно присваивают обратно:nums = append(nums, …). Можно добавить сразу несколько значений.len(nums)— число элементов, последний элемент —nums[len(nums)-1].nums[1:3]— часть среза с индекса 1 до 3, не включая 3.nums[:2]— первые два,nums[4:]— с четвёртого индекса до конца.
make и пустой срез
Срез заданной длины, заполненный нулями, создаёт make:
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] 5for 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, когда места нет, сам выделяет массив побольше и копирует в него элементы. Если размер известен заранее, место можно зарезервировать:
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 100make([]int, 0, 100) — длина 0, ёмкость 100: сто append пройдут без перевыделения памяти.
Перебор: range
range по срезу выдаёт пару — индекс и значение:
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] не копирует элементы, а смотрит на тот же массив. Изменение через часть видно в исходном срезе:
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 — в справке Срез и массив — разница.
Срез срезов
Таблицу — например, поле игры — хранят как срез срезов. Каждую строку создают отдельно:
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 умеет разбивать строку на срез и собирать обратно:
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]
truestrings.Fields делит по любым пробелам и пропускает лишние, strings.Split — строго по разделителю, поэтому между двумя запятыми получилась пустая строка. slices.Contains проверяет, есть ли значение в срезе. Как отсортировать срез — в справке Как отсортировать срез.
Коротко
- Массив
[3]int— фиксированной длины и копируется целиком; срез[]int— растёт и делит память. appendвозвращает новый срез:s = append(s, x).make([]int, n)— срез изnнулей;make([]int, 0, n)— пустой с запасом места.s[a:b]— элементы сaдоb, не включаяb; часть смотрит на тот же массив.for i, v := range s— индекс и копия значения; ненужное —_.- Независимая копия —
copyилиslices.Clone.