Массивы в Go являлись для меня одной из сложных тем, так как я не понимал как они работают. В данной статье рассмотрим как же именно работают слайсы и массивы в Go, а также как именно работает append и copy.
Массивы
Массивы - коллекция элементов одного типа. Длина массива не может изменяться. Вот как мы можем создать массив в Go:
arr := [4]int{3,2,5,4}
Если мы создадим два массива в Go с разными длинами, то два массива будут иметь разные типы, так как длина массива в Go, входит в его тип:
a := [3]int{} b := [2]int{} // (a) [2]int и (b) [3]int - разные типы
Более того, если нам лень писать длину массива, то мы можем сказать компилятору, чтобы он сам подсчитал длину:
a := [...]int{1, 2, 3} // [3]int
Передача по значению
Переменная, которую мы инициализировали со значением массива, содержит именно значения массива, а не ссылку на первый элемент массива (как это сделано в C).
Именно поэтому массив в Go является примитивным типом данных, он может копироваться при передаче в другую переменную. По умолчанию в Go все значения копируются, а не передаются с помощью ссылки. Это значит, что если мы передадим наш массив в функцию, то Go скопирует данный массив и в функции будет находиться уже совершенно другой массив (вернее точная копия исходного массива).
Внизу мы рассмотрим пример, где мы скопируем массив, а затем посмотрим на адрес, по которому хранится значение:
package main import "fmt" func main() { var initArray = [...]int{1, 2, 3} var copyArray = initArray fmt.Printf("Address of initArray: %p\n", &initArray) fmt.Printf("Address of copyArray: %p\n", ©Array) } /* Output: Address of initArray: 0xc00001a018 Address of copyArray: 0xc00001a030 */
Слайсы
Слайсы в Go более гибкие, они позволяют изменять свою длину. По сути слайсы являются надмножеством массивов. Слайсы создают нам массив, которым мы можем пользоваться как обычным массивом и при надобности расширяют его.
Слайсы можно создать двумя способами:
// С помощью make var foo []byte s = make([]byte, 5, 5) // С помощью shorthand syntax bar := []byte{}
Способ с make
Способ с make является более интересным, так как дает нам возможность задать тип, длину и вместимость.
С типом я думаю никаких проблем быть не должно. Тип слайса формируется в виде []тип.
С длиной тоже ничего интересного. В зависимости от введенного количества - массив заполнится нулевыми значениями, например:
package main import "fmt" func main() { var foo = make([]byte, 5) var bar = make([]int, 10) var fee = make([]string, 2) fmt.Println(foo, bar, fee) } /* Output: [0 0 0 0 0] [0 0 0 0 0 0 0 0 0 0] [ ] */
Последний параметр - вместимость играет важную роль в производительности программы, а также является интересным. По сути он говорит о том сколько памяти нужно выделить заранее под наш массив, чтобы при расширении нам не пришлось искать новый участок памяти.
Например, если мы создадим массив с вместимостью в 10 элементов, наполним его 5-ю элементами, а потом добавим один - адрес массива не изменится:
package main import "fmt" func main() { var foo = make([]int, 5, 10) fmt.Printf("Address of foo array [before append]: %p\n", &foo) foo = append(foo, 222) fmt.Printf("Address of foo array [after append]: %p\n", &foo) } /* Output: Address of foo array [before append]: 0xc0000aa018 Address of foo array [after append]: 0xc0000aa018 */
К слову, если мы явно не задали вместимость слайса (то есть использовали конструкцию
make([]int, 5)), то вместимость будет равна длине массива (в данном случае - 5).
Если же мы укажем вместимость массива меньше, чем его длину, то код и вовсе нескомпилируется:
package main import "fmt" func main() { var foo = make([]int, 5, 4) fmt.Printf("Capacity of the array: ", cap(foo)) } /* ./prog.go:6:24: invalid argument: length and capacity swapped */
Что будет если мы переполним вместимость?
Если же мы переполним вместимость слайса, то вместимость умножится на 2:
package main import "fmt" func main() { var foo = make([]int, 10, 10) // Изначальная вместимость - 10 foo = append(foo, 2) // Добавляем элемент fmt.Println("Length of the array:", len(foo)) fmt.Println("Capacity of the array:", cap(foo)) } /* Output: Length of the array: 11 Capacity of the array: 20 */
При этом в памяти произойдет следующее:
Go понимает что нам не хватает памяти и посмотрит есть ли после текущего сегмента памяти еще столько же ячеек;
Если ячейки есть, он не будет передвигать массив и просто зарезервирует больше памяти;
Если ячеек нет, то он скопирует всю информацию из уже использующегося сегмента и найдет вдвое больше свободных ячеек, после нажождения он перенесет туда все данные и отдаст нам адрес сегмента;
Shorthand-syntax
С короткой версией объявления слайса все проще:
package main import "fmt" func main() { foo := []int{1, 2, 3} fmt.Println("Length of the array:", len(foo)) fmt.Println("Capacity of the array:", cap(foo)) } /* Output: Length of the array: 3 Capacity of the array: 3 */
В примере вверху Go создаст массив (под капотом) с длиной в три ячейки и такой же вместимостью.
Срезы на слайсах
Срезом на слайсе является дочерний слайс, который ссылется только на часть слайса:
package main import "fmt" func main() { name := []string{"D", "a", "n", "i", "i", "l"} firstThreeLetters := name[:3] fmt.Println(firstThreeLetters) } /* Output: [D a n] */
Не смотря на то, что слайс и срез - понятия взаимозаменяемые (а если быть точнее, то срез - перевод от англ. slice), мы будем называть слайсами все новосозданные слайсы с помощью make() или shorthand-синтаксиса, а срезами будем называть слайсы проделанные над уже существующим массивом.
Мы также можем делать срезы на массивах, таким образом мы можем делать массивы динамически расширяемыми:
package main import "fmt" func main() { nameArray := [6]string{"D", "a", "n", "i", "i", "l"} nameSlice := nameArray[:] nameSlice = append(nameSlice, "!") fmt.Println(nameSlice) } /* Output: [D a n i i l !] */
Слайс под капотом
Слайс под капотом является структурой, которая содержит ссылку на исходный массив, длину и вместимость:
struct { array *[]T length int capacity int }
Когда мы создаем новый слайс или срезаем массив, то ссылка массива присвивается полю array, с помощью данного указателя слайс сможет обращаться к массиву под капотом. length и capacity хранят длину и вместимость, соответственно.

Поскольку слайс ссылается на часть массива, мы можем срезать часть массива. Срез не копирует элементы массива, он просто ссылается на них. Таким образом при изменении среза, изменится и массив, с которого мы брали срез:
package main import "fmt" func main() { nameArray := [6]string{"D", "a", "n", "i", "i", "l"} nameSlice := nameArray[:3] nameSlice[len(nameSlice) - 1] = "m" fmt.Println(nameSlice) // [D a m] fmt.Println(nameArray) // [D a m i i l] }
Мы также можем сделать так, чтобы срез занял всю длину исходного массива. Так как слайс хранит вместимость исходного массива - мы можем сделать срез снова и указать параметр cap(nameArray):
package main import "fmt" func main() { nameArray := [6]string{"D", "a", "n", "i", "i", "l"} nameSlice := nameArray[:3] nameSlice[len(nameSlice)-1] = "m" fmt.Println(nameSlice) // [D a m] // Делаем новый срез nameSlice = nameSlice[0:cap(nameSlice)] fmt.Println(nameSlice) // [D a m i i l] }
У вас может возникнуть вопрос: почему мы не срезали
cap(nameSlice) - 1, ибо мы указали в конце несуществующий индекс (на один больше, нежели существует в массиве). Все дело в том, что последний элемент при срезе не включается в срез.То есть, первый индекс идет включительно в срез, а последний - не включительно.
Копирование
Как уже можно понять, при срезе с массива или слайса мы не создаем новый слайс. Также, если мы присвоим одной переменной значение слайса другой переменной - они обе будут указывать на один массив:
package main import ( "fmt" ) func main() { nameSlice := []string{"D", "a", "n", "i", "i", "l"} secondNameSlice := nameSlice secondNameSlice[0] = "T" fmt.Println(nameSlice, secondNameSlice) // [T a n i i l] [T a n i i l] }
Мы можем избежать такого поведения с помощью копирования. Для того чтобы скопировать слайс (создать независимую копию) - нам достаточно использовать функцию copy:
package main import ( "fmt" ) func main() { nameSlice := []string{"D", "a", "n", "i", "i", "l"} secondNameSlice := make([]string, len(nameSlice), cap(nameSlice)) copy(secondNameSlice, nameSlice) secondNameSlice[0] = "T" fmt.Println(nameSlice, secondNameSlice) // [D a n i i l] [T a n i i l] }
сopy и append под капотом
Мы можем заметить два различия: при использовании функции append - мы переприсваивали значение переменной:
foo := []int {} foo = append(foo, 1)
В случае с copy мы просто передаем саму переменную (не ссылку, а именно переменную!):
foo := []int {1, 2} bar := []int {} copy(bar, foo)
Вот как работает копирование под капотом:
func copy(to []T, from []T) { for i := range from { to[i] = from[i] } }
Разработчики Go решили не добавлять часть с инициализацией нового слайса внутрь copy.
В случае с дополнением все немного иначе. Тут разработчики Go посчитали, что функция сама должна решать нужно ли инициализировать новый слайс, или можно дополнить данные в уже существующий слайс:
func append(slice []T, data ...T) []T { initialLength := len(slice) finalLength := m + len(data) if finalLength > cap(slice) { // if necessary, reallocate // allocate double what's needed, for future growth. newSlice := make([]byte, (n+1)*2) copy(newSlice, slice) slice = newSlice } slice = slice[0:finalLength] copy(slice[initialLength:finalLength], data) return slice }
Вместо заключения 🌚
Если вам понравилась данная статья - то вы всегда можете перейти в мой блог, там больше схожей информации о веб-разработке.
Если у вас остались вопросы - не стесняйтесь задавать их в комментариях. Хорошего времяпрепровождения! 💁🏻♂
