Маленькая книга о Go – Глава 3: Карты, массивы и срезы
Ранее мы уже видели несколько простых структур. Настало время познакомиться с массивами, срезами и картами.
Массивы
Если вы уже знакомы с Python, Ruby, Perl, JavaScript или PHP (и т. д.), при программировании вы скорее всего использовали динамические массивы. Это массивы, которые способны изменять свой размер в зависимости от хранимых в них данных. В Go, и как во многих других языках, массивы фиксированы. При объявлении массива необходимо указать его размер, после чего изменить его нельзя:
var scores [10]int
scores[0] = 339
Массив выше может хранить до 10 очков, используя индексы от scores[0]
до scores[9]
. При попытке обращения к индексам, не входящим в этот диапазон, произойдет ошибка на этапе компиляции или выполнении программы.
Мы можем инициализировать массив вместе со значениями:
scores := [4]int{9001, 9333, 212, 33}
Можно использовать len
для получения размера массива. range
используется для итерации по нему:
for index, value := range scores {
}
Массивы эффективны в использовании, но жестко заданы. Часто мы не знаем заранее число используемых элементов. В таких случаях применяются срезы.
Срезы
В Go вы редко, даже почти никогда, не будете использовать массивы напрямую. Вместо них вы будете использовать срезы. Срез – это легковесная структура, которая представляет собой часть массива. Есть несколько способов создать срез, и мы позже рассмотрим их подробнее. Первый способ является слегка измененным способом объявления массива:
scores := []int{1,4,293,4,9}
В отличии от декларирования массива, срез объявлен без указания длины в квадратных скобках. Для того, чтобы понять их различия, давайте рассмотрим другой способ создания среза с использованием make
:
scores := make([]int, 10)
Мы используем make
вместо new
потому, что при создании среза происходит немного больше, чем просто выделение памяти (что делает new
). В частности, мы должны выделить память для массива, а также инициализировать срез. В приведенном выше примере мы создаем срез длиной 10 и вместимостью 10. Длина – это размер среза. Вместимость – это размер лежащего в его основе массива. При использовании make
мы можем указать эти два параметра отдельно:
scores := make([]int, 0, 10)
Эта инструкция создает срез с длиной 0 и вместимостью 10. (Если вы были внимательны, вы могли заметить, что make
и len
были перегружены. Go – это такой язык, в котором, к разочарованию некоторых, используются возможности, недоступные разработчикам).
Для лучшего понимания взаимосвязи длины и вместимости, рассмотрим несколько примеров:
func main() {
scores := make([]int, 0, 10)
scores[5] = 9033
fmt.Println(scores)
}
Наш первый пример не работает. Почему? Потому, что срез имеет длину 0. Да, в его основе лежит массив, содержащий 10 элементов, но нам нужно явно расширить срез для получения доступа к этим элементам. Один из способов расширить срез – это append
:
func main() {
scores := make([]int, 0, 10)
scores = append(scores, 5)
fmt.Println(scores) // выведет [5]
}
Но такой способ изменит смысл оригинального кода. Добавление элемента к срезу длиной 0 является установкой первого значения. По определённым причинам наш нерабочий код требует установки элемента по индексу 5. Чтобы это сделать, мы должны пере-срезать наш срез:
func main() {
scores := make([]int, 0, 10)
scores = scores[0:6]
scores[5] = 9033
fmt.Println(scores)
}
Как сильно мы можем изменить размер среза? До размера его вместимости, в нашем случае это 10.
Вы можете подумать на самом деле это не решает проблему фиксированной длины массивов. Оказывается, что append
это что-то особенное. Если основной массив заполнен, создается больший массив и все значения копируются в него (также работают динамические массивы в PHP, Python, Ruby, JavaScript, …). Поэтому пример выше использует append
, мы должны повторно присвоить значение, которое было возвращено append
переменной scores
: append
может создать новое значение, если в исходном не хватает места.
Если я скажу вам, что Go увеличивает массивы в два раза, вы сможете догадаться, что выведет данный код?
func main() {
scores := make([]int, 0, 5)
c := cap(scores)
fmt.Println(c)
for i := 0; i < 25; i++ {
scores = append(scores, i)
// если вместимость изменена,
// Go увеличивает массив, чтобы приспособиться к новым данным
if cap(scores) != c {
c = cap(scores)
fmt.Println(c)
}
}
}
Изначальная вместимость переменной scores
это 5. Для того, чтобы вместить 20 значений, она должна быть расширена 3 раза до вместимости в 10, 20 и наконец 40.
И как последний пример, рассмотрим:
func main() {
scores := make([]int, 5)
scores = append(scores, 9332)
fmt.Println(scores)
}
Здесь вывод будет [0, 0, 0, 0, 0, 9332]
. Возможно, вы думали что получится [9332, 0, 0, 0, 0]
? Для человека это выглядит логично. Но для компилятора, вы говорите: добавить значение к срезу, который уже содержит 5 значений.
В итоге, есть четыре способа инициализировать срез:
names := []string{"leto", "jessica", "paul"}
checks := make([]bool, 10)
var names []string
scores := make([]int, 0, 20)
Когда какой использовать? Первый не требует особых объяснений. Его можно использовать когда вы заранее знаете значения массива.
Второй полезен когда вам нужно записывать значения по определенным индексам среза. Например:
func extractPowers(saiyans []*Saiyans) []int {
powers := make([]int, len(saiyans))
for index, saiyan := range saiyans {
powers[index] = saiyan.Power
}
return powers
}
Третий случай – это пустой срез. Используется в сочетании с append
, когда число элементов заранее неизвестно.
Последний способ позволяет задать изначальную вместимость; полезен когда у вас есть общее представление о том, сколько элементов вам нужно.
Даже если вы знаете размер, можно использовать append
. Это момент по большей части зависит от ваших предпочтений:
func extractPowers(saiyans []*Saiyans) []int {
powers := make([]int, 0, len(saiyans))
for _, saiyan := range saiyans {
powers = append(powers, saiyan.Power)
}
return powers
}
Срезы в роли оберток массивов представляют собой мощный концепт. Во многих языках существует понятие нарезки массива. И в JavaScript и в Ruby массивы имеют метод slice
. Вы можете получить срез в Ruby используя [START..END]
или в Python с помощью [START:END]
. Однако в этих языках срезы в действительности являются новыми массивами со скопированными в них значениями. Если мы возьмем Ruby, что выведет следующий код?
scores = [1,2,3,4,5]
slice = scores[2..4]
slice[0] = 999
puts scores
Ответ: [1, 2, 3, 4, 5]
. Потому, что slice
совершенно новый массив с копией значений. Теперь рассмотрим эквивалент в Go:
scores := []int{1,2,3,4,5}
slice := scores[2:4]
slice[0] = 999
fmt.Println(scores)
Результат: [1, 2, 999, 4, 5]
.
Это изменяет принцип кодирования. Например несколько функций принимают номер позиции в качестве параметра. В JavaScript, если вам нужен символ в строке (да, срезы работают со строками тоже!) идущий после пятого, вам нужно написать:
haystack = "the spice must flow";
console.log(haystack.indexOf(" ", 5));
В Go мы используем срезы:
strings.Index(haystack[5:], " ")
В примере выше мы видим, что [X:]
– это сокращение, которое означает от X до конца, а [:X]
это короткая запись, означающая от начала до X. В отличие от других языков, Go здесь не поддерживает отрицательные индексы. Если мы хотим получить все значения среза, кроме последнего, нам нужно выполнить:
scores := []int{1,2,3,4,5}
scores = scores[:len(scores)-1]
С помощью этого способа мы можем реализовать эффективный способ удаления значения из несортированного среза:
func main() {
scores := []int{1,2,3,4,5}
scores = removeAtIndex(scores, 2)
fmt.Println(scores)
}
func removeAtIndex(source []int, index int) []int {
lastIndex := len(source) - 1
//меняем последнее значение и значение, которое хотим удалить, местами
source[index], source[lastIndex] = source[lastIndex], source[index]
return source[:lastIndex]
}
Наконец, когда мы уже достаточно знаем о срезах, давайте взглянем ещё на одну часто используемую функцию: copy
. copy
одна из тех функций, которая показывает как срезы влияют на способ кодирования. Обычно метод, который копирует значения из одного массива в другой имеет 5 параметров: source
, sourceStart
, count
, destination
и destinationStart
. При работе со срезами нам нужны только два:
import (
"fmt"
"math/rand"
"sort"
)
func main() {
scores := make([]int, 100)
for i := 0; i < 100; i++ {
scores[i] = int(rand.Int31n(1000))
}
sort.Ints(scores)
worst := make([]int, 5)
copy(worst, scores[:5])
fmt.Println(worst)
}
Немного поиграйте с кодом выше. Попробуйте различные вариации. Посмотрите, что произойдет, если вы измените копирование на copy(worst[2:4], scores[:5])
, или посмотрите, что будет если вы попытаетесь скопировать больше, чем 5
значений в worst
?
Карты
Карты в Go – это то, что в других языках называют хеш-таблицами или словарями. Они работают так, как и ожидается: вы определяете ключ и значение, можете получать, устанавливать и удалять значения.
Карты, как и срезы, создаются с помощью функции make
. Давайте взглянем на пример:
func main() {
lookup := make(map[string]int)
lookup["goku"] = 9001
power, exists := lookup["vegeta"]
// prints 0, false
// 0 это значение по умолчанию для типа integer
fmt.Println(power, exists)
}
Для получения количества ключей используйте len
. Для удаления значения по определенному ключу вызывайте delete
:
// returns 1
total := len(lookup)
// ничего не возвращает, можно указывать несуществующий ключ
delete(lookup, "goku")
Карты увеличиваются динамически. Однако вы можете указать второй аргумент в make
для установки начального значения:
lookup := make(map[string]int, 100)
Если вы имеете какое-то представление о том, сколько ключей вам понадобится в карте, указание начального размера может помочь с производительностью.
Когда вам нужна карта в роли поля структуры, вы указываете её так:
type Saiyan struct {
Name string
Friends map[string]*Saiyan
}
Один из способов инициализации:
goku := &Saiyan{
Name: "Goku",
Friends: make(map[string]*Saiyan),
}
goku.Friends["krillin"] = ... //загрузить или создать Krillin
Существует еще один способ объявления и инициализации значений в Go. Как и make
, этот подход является специфичным для карт и массивов. Вы можете объявить карту как составной литерал:
lookup := map[string]int{
"goku": 9001,
"gohan": 2044,
}
Итерация по карте производится с помощью цикла for
в комбинации с ключевым словом range
:
for key, value := range lookup {
...
}
Итерация по карте происходит не по порядку. Каждая итерация будет возвращать пару ключа и значения в случайном порядке.
Указатели против значений
Мы закончили главу 2 вопросом о том, следует ли присваивать и передавать указатели или значения. Сейчас вернемся к нему говоря уже о массивах и картах. Какой способ стоит использовать?
a := make([]Saiyan, 10)
//или
b := make([]*Saiyan, 10)
Многие разработчики считают, что при передаче или возвращении b
функция будет более эффективной. Тем не менее то, что передается/возвращается является копией среза, который в свою очередь является ссылкой. Таким образом в отношении передачи/возврата самого среза нет никакой разницы.
Разница будет видна когда вы изменяете значение среза или карты. В этом случае логика такая же как и в конце главы 2. Так что решение о том, объявлять ли массив указателей или массив значений, принимается исходя из того, что вы будете делать со значениями, а не с самими картами или массивами.
Перед тем как продолжить
Массивы и карты в Go работают так же, как и в других языках. При использовании динамических массивов существуют небольшие изменения, но append
должен избавить вас от большинства проблем. Если мы хотим выйти за пределы поверхности массивов, мы используем срезы. Срезы – мощные конструкции, оказывающие большое влияние на чистоту вашего кода.
Мы не рассмотрели несколько крайних случаев, но вам скорее всего не прийдется вникать в них так глубоко. Хотя если это и понадобится, надеюсь основы, полученные здесь, помогут вам самостоятельно разобраться в том, что происходит.
Далее: Глава 4 – Организация кода и интерфейсы