Go: с нуля до своего блога Урок 8 из 50
Массивы и срезы в Go: список, который растёт, append, len и cap
Восьмой урок курса по Go. Массив, из которого сделан срез, и сам срез — список, который растёт: []string, append и почему результат присваивают обратно. Чем массив отличается от среза при присваивании, чем длина от ёмкости и главная ловушка: кусок смотрит в ту же память, что и оригинал.
Зачем это нужно
До сих пор одно имя хранило одно значение. Но статья в блоге не одна, тегов у неё несколько, комментариев тоже. Нужен список, и в Go он называется срезом.
Заодно for получит вторую половину обещанного: range, который мы взяли для строки, теперь пройдёт по списку.
Сразу целиком
Новая папка, go mod init sabaq07, main.go:
package main
import "fmt"
func main() {
var titles []string
titles = append(titles, "Шанырак")
titles = append(titles, "Язык Go")
titles = append(titles, "Степь")
fmt.Println("статей:", len(titles), "· место:", cap(titles))
for i, t := range titles {
fmt.Println(i+1, t)
}
fmt.Println("первые две:", titles[:2])
fmt.Println("последняя:", titles[len(titles)-1])
}
Прежде чем запускать — не заглядывая ниже, скажите вслух, что она напечатает. Потом запустите и сверьте: расхождение и есть то место, где вы ещё не знаете языка.
go run . печатает:
статей: 3 · место: 4
1 Шанырак
2 Язык Go
3 Степь
первые две: [Шанырак Язык Go]
последняя: Степь
Обратите внимание: статей три, а места четыре.
Разбор
Сначала массив: то, из чего сделан срез
Слово «массив» только что прозвучало — договорим до конца, потому что без него срез объяснить нельзя.
Массив в Go — список, длина которого задана раз и навсегда и входит в сам тип. [3]string и [4]string — разные типы, и присвоить один другому нельзя. Поэтому массивы в обычном коде почти не пишут: заранее известное число элементов — редкость.
Одно отличие знать обязательно, потому что из него следует всё остальное. Массив при присваивании копируется, срез — нет:
titles := [3]string{"Шанырак", "Язык Go", "Степь"}
copyOf := titles
copyOf[0] = "Горы"
fmt.Println("массив:", titles[0], "·", copyOf[0])
list := []string{"Шанырак", "Язык Go", "Степь"}
same := list
same[0] = "Горы"
fmt.Println("срез: ", list[0], "·", same[0])
массив: Шанырак · Горы
срез: Горы · Горы
Массив присвоили — получилась вторая, независимая тройка строк, и правка первой её не касается. Срез присвоили — оба имени смотрят на один и тот же массив, поэтому правка через одно имя видна через другое.
Вот что такое срез на самом деле: три числа — адрес массива, длина и ёмкость. Данные лежат в массиве, срез только говорит, как до них добраться. Отсюда и append, который иногда «переезжает»: место в массиве кончилось, заводится массив побольше.
Считать элементы руками необязательно: [...]string{"Шанырак", "Язык Go", "Степь"} — тоже массив, длину посчитает компилятор.
Где массив всё-таки встречается: там, где длина — часть смысла. Хеш SHA-256 — это [32]byte, и другой длины у него быть не может. В таких местах массив сообщает размер прямо в типе.
[]string — срез строк
Квадратные скобки перед типом и есть срез. Внутри пусто: длина заранее не назначается, список растёт по мере надобности.
var titles []string
Такой срез пустой, и это законное состояние: len равен нулю, cap нулю, а само значение — nil. Добавлять в него можно сразу, никакой подготовки не нужно.
append добавляет — и возвращает новый срез
titles = append(titles, "Степь")
Слева и справа одно и то же имя, и это не описка. append не меняет старый срез, а возвращает новый. Если места хватило, он допишет элемент в тот же массив и вернёт срез с новой длиной; если не хватило — заведёт массив побольше, и срез окажется уже по другому адресу. Забудете присвоить обратно — добавление пропадёт, и программа даже не пожалуется.
Образ. Переезд. Вы просите положить ещё одну вещь, а вам отвечают: «поместилось, но пришлось переехать, вот новый адрес». Не записали адрес — ходите по старому и удивляетесь, что там всё по-прежнему.
Это ошибка, которую делают все и один раз. Дальше рука сама пишет x = append(x, …).
Длина и ёмкость
len — сколько элементов лежит. cap — сколько влезет, прежде чем понадобится переезд.
В примере их три и четыре: Go выделил место с запасом, чтобы не переселять список на каждом добавлении. Четвёрку не запоминайте: сколько именно запаса взять, решает реализация, и на другой версии Go число может оказаться иным. Проверять надо len, а cap — только наблюдать.
Образ. Коробка на полке. Длина — сколько книг в ней сейчас. Ёмкость — сколько войдёт, пока не придётся брать коробку побольше и перекладывать всё туда.
Ёмкость знать полезно, но управлять ею вручную почти никогда не нужно: Go наращивает её сам и с запасом — небольшой срез удваивает, дальше прибавляет осторожнее. Точный размер новой ёмкости язык не обещает, и полагаться на него в программе нельзя. Важно другое: тысяча добавлений стоит около десятка переездов, а не тысячи.
Индексы и границы
Первый элемент — titles[0], последний — titles[len(titles)-1]. Счёт с нуля.
Выход за границу не прощается: titles[5] при трёх элементах остановит программу с сообщением index out of range. Это лучше, чем тихо вернуть мусор, — вы узнаете об ошибке в ту же секунду, а не через неделю от читателя.
Кусок среза
titles[:2] // первые два
titles[1:] // со второго до конца
titles[1:3] // со второго по третий
Первое число — откуда, второе — до какого не включая. Пропущенное значит «с начала» или «до конца».
Ловушка: кусок — не копия
Вот то, ради чего этот раздел:
two := titles[:2]
two[0] = "ИЗМЕНЕНО"
fmt.Println(titles[0]) // ИЗМЕНЕНО
Мы правили two, а изменился titles. Кусок среза — не отдельный список, а окно в ту же память.
Образ. Не копия документа, а окно в тот же склад. Через окно видно часть полок, и если что-то переставить через окно, оно переставлено на самом складе.
Нужна настоящая копия — делайте её явно:
two := make([]string, 2)
copy(two, titles[:2])
Пока держите в голове одно: срез, полученный из среза, разделяет с ним память. Большинству это не мешает, но когда однажды «непонятно почему изменилось» — вспомните этот абзац.
range по списку
for i, t := range titles {
То же, что и по строке, но проще: i — порядковый номер с нуля, без всяких байтов, t — сам элемент. Если номер не нужен, на его место ставят подчёркивание: for _, t := range titles.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Почему
appendпишут какx = append(x, v), а не простоappend(x, v)? - Чем длина отличается от ёмкости и зачем нужна вторая?
- Вы взяли
part := titles[:2]и поправилиpart[0]. Что стало сtitles[0]и почему?
Задание
Обязательное. Напишите функцию longer(titles []string, n int) []string, которая возвращает только те заголовки, в которых больше n букв. Считайте буквы, а не байты, — как в прошлом уроке. Вызовите её для списка из "Степь", "Шанырак", "Go", "Көш" при n = 3 и напечатайте результат.
По желанию.
- Функция
total(nums []int) int, складывающая все числа списка. - Добавьте в список десять заголовков в цикле и печатайте
lenиcapпосле каждого. На каком шаге ёмкость меняется? - Сделайте настоящую копию первых двух заголовков через
makeиcopy, поправьте копию и убедитесь, что оригинал цел.
Куда это встанет в блоге
Список статей на главной, теги под заголовком, комментарии под текстом, результаты поиска — всё это срезы. В уроке про шаблоны вы отдадите такой список в HTML и он развернётся в карточки; в уроке про базу — получите его оттуда одним запросом.
Ответы
Показать ответы
- Потому что
appendвозвращает новый срез, а не меняет старый. При нехватке места он переселяет данные, и адрес меняется; без присваивания вы остаётесь со старым значением, а добавленное теряется. Компилятор об этом не предупредит: он видит вызов функции, результат которой не взяли. - Длина — сколько элементов сейчас, ёмкость — сколько влезет до переезда. Вторая нужна затем, чтобы добавление не переселяло список каждый раз: Go берёт место с запасом и наращивает его сам, поэтому тысяча добавлений обходится примерно десятком переселений. Насколько именно вырастет ёмкость, язык не обещает.
titles[0]тоже стал «ИЗМЕНЕНО». Кусок среза не копирует данные, а смотрит в ту же память, поэтому правка через один виден через другой. Копия делается явно, черезmakeиcopy.
Источники
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.