Маленькая книга о 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 должен избавить вас от большинства проблем. Если мы хотим выйти за пределы поверхности массивов, мы используем срезы. Срезы – мощные конструкции, оказывающие большое влияние на чистоту вашего кода.

Мы не рассмотрели несколько крайних случаев, но вам скорее всего не прийдется вникать в них так глубоко. Хотя если это и понадобится, надеюсь основы, полученные здесь, помогут вам самостоятельно разобраться в том, что происходит.