Массивы, срезы, отображения
Перечислимые типы
Массивы
Пожалуй, массивы в языке Go не имеют ярких особенностей, что отличали бы в этом плане Go от прочих языков в ту или иную сторону. Итак, вспомним, что такое массив. В классическом понимании, массив есть индексированная последовательность однотипных элементов фиксированного размера. Конечно, есть языки, наподобие PHP или JavaScript, где массив - это сущность, которая позволяет с собою более вольное обращение, к языку Go это никак не относится. Объявляется массив вот так:
package main
import (
"fmt"
)
func main() {
const size int = 5
var array [size]int
array = [size]int{0, 1, 2, 3, 4}
for i := 0; i < size; i++ {
fmt.Println(array[i])
}
}
Мы объявили массив размерностью 5, присвоили каждому элементу значения, и, в завершение, вывели значения каждого элемента в цикле for. Как видите, ничего необычного. Как и в языке C, размерность массива задаётся при помощи константы. Нумерация элементов начинается с нуля. Разумеется, вместо константы можно использовать литерал и отказаться от использования отдельной переменной для хранения размерности массива, если это вам необходимо.
package main
import (
"fmt"
)
func main() {
//указание типа в левой части можно опустить.
var array = [5]int{0, 1, 2, 3, 4}
for i := 0; i < len(array); i++ {
fmt.Println(array[i])
}
}
Встроенная функция len возвращает размерность массива. В связи с этим, она часто используется в циклах при переборе элементов. Выход за пределы массива вызывает аварийную ситуацию.
Как и всякая другая переменная, при объявлении внутри функции, массив может быть объявлен кратко, при помощи оператора :=
package main
func main() {
array := [5]int{0, 1, 2, 3, 4}
}
Во множестве случаев, длина массива может быть значительной. Для таких случаев предусмотрена возможность переносов строк при инициализации. Необходимо лишь запомнить, что в таком случае после замыкающего элемента также ставится запятая. Это будет единственной особенностью.
package main
func main() {
array := [15]int{0, 1, 2, 3, 4, 5,
6, 7, 8, 9, 10,
11, 12, 13, 14, //не забудьте поставить запятую!
}
}
По массиву (и по срезу тоже) возможна своеобразная "навигация" путём указания значений в квадратных скобках. Например вот так:
package main
import (
"fmt"
)
func main() {
const size int = 5
var array [size]int
array = [size]int{0, 1, 2, 3, 4}
for _, val := range array[1:3] {
fmt.Println(val)
}
}
//Результат:
//1
//2
Как видите, всё просто. Указываем в квадратных скобках желаемый начальный и конечный индекс (включительно). Если начальный индекс - 0, его можно не указывать, вот так: [:3]. Точно так же поступают и с последним индексом: [1:].
Массивы одного типа могут присваиваться и сравниваться. Под типом в данном случае понимается совокупность: размерность + тип элемента.
package main
import (
"fmt"
)
func main() {
array := [5]int{0, 1, 2, 3, 4}
array1 := [5]int{5, 6, 7, 8, 9}
fmt.Println(array)
fmt.Println(array1)
fmt.Println(array == array1)//сравнение
array1 = array// и присвоение
fmt.Println(array1)
fmt.Println(array == array1)
}
//Результат:
//[0 1 2 3 4]
//[5 6 7 8 9]
//false
//[0 1 2 3 4]
//true
Для упрощения дела, я вывожу здесь значения массивов при помощи функции Println пакета fmt в "сыром" виде, чтобы не загромождать код циклами.
Этим, пожалуй, исчерпывается необходимая информация о массивах.
Срезы
В связи с фиксированным размером, массивы на самом деле используются в языке Go сравнительно нечасто. Скорее используются срезы, которые зачастую трактуют как динамический массив, что допустимо, но фактически не совсем верно. В английском оригинале срезы именуются slices, отчего их часто именуют просто "слайсами". В русскоязычной же технической литературе принят перевод "срез", поэтому мы будем придерживаться именно такой терминологии.
Объявить срез просто:
package main
import (
"fmt"
)
func main() {
var slice []int
fmt.Println(slice)
if slice == nil {
fmt.Println("Value is nil!")
}
fmt.Println(len(slice))
}
//Результат:
//[]
//Value is nil!
//0
Значение nil является нулевым указателем. Оно аналогично понятию NULL в С-подобных языках. Это - зарезервированное слово в языке Go. Как вы уже поняли, приведенное выше объявление создаёт "нулевой" срез. Это, однако, не мешает работать с таким срезом, что мы покажем далее. А сейчас инициализируем срез.
package main
import (
"fmt"
)
func main() {
//указание типа в левой части можно опустить.
var slice = []int{0, 1, 2, 3, 4}
fmt.Println(slice)
fmt.Println(len(slice)) // срез из 5 элементов
}
//Результат:
//[0 1 2 3 4]
//5
Существует встроенная функция make, применяемая для инициализации срезов и отображений. Покажем, как она работает:
package main
import (
"fmt"
)
func main() {
var slice = make([]int, 5)
fmt.Println(slice)
fmt.Println(len(slice))
}
//Результат:
//[0 0 0 0 0]
//5
Не трудно заметить, в данном случае мы создали снова-таки пустой срез, но в данном случае он не равен значению nil, а получил определённую размерность. К тому же каждый из элементов среза был проинициализирован значением типа по умолчанию. Поскольку в нашем случае тип - integer, то соответственно, все элементы были проинициализированы значением 0.
Важно! В отличие от массивов, срез имеет свойство сравнимости лишь со значением nil, но не другим срезом.
package main
import (
"fmt"
)
func main() {
slice1 := []int{1, 2, 3}
slice2 := []int{1, 2, 3}
fmt.Println(slice1==slice2)
}
//Ошибка!
//invalid operation: slice1 == slice2 (slice can only be compared to nil)
Для разрешения проблемы сравнения двух срезов, я написал такую функцию:
//TestSliceEq is a func to compare slices if they are equal or not
func TestSliceEq(a, b []int) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
Рекомендую вам также ею воспользоваться. Начните создавать собственную библиотеку функций. Напомню, в разделе о пакетах мы создали корневой каталог fqdn.org для хранения библиотек нашего кода. В нём мы создали папку common-utils для хранения файлов пакета utils. В пакете находился файл utils.go непосредственно с кодом. Полагаю, самое время добавить в этот файл новую функцию.
Замечу, что данная функция обрабатывает срезы одного типа - []int. Для обработки срезов иного типа необходимо заменить тип параметров функции на соответствующий - []string, []float64, []byte и так далее. Для создания же универсальной функции, нам потребуются более сложные инструменты, и, следовательно, больше знаний. Нам повезло. Так много нового впереди!
Append и copy - встроенные функции
Массивы неповоротливы, как испанские галеоны. Всё что можно сделать с массивом - присвоить один другому при условии однотипности, да сравнить. Срезы же гораздо гибче! Специально применительно к срезам заведены встроенные функции append и copy, призванные причинять всяческую пользу. Разберёмся же в них!
append
Эта встроенная функция позволяет добавить элемент в конец данного среза. Вот так:
package main
import (
"fmt"
)
func main() {
slice := []int{0, 1, 2, 3, 4}
fmt.Println(slice)
slice = append(slice, 5)
fmt.Println(slice)
}
//Результат:
//[0 1 2 3 4]
//[0 1 2 3 4 5]
Но что если нам понадобится объединить подобным образом два среза? Нет проблем! С помощью небольшой синтаксической хитрости вы можете сделать это.
package main
import (
"fmt"
)
func main() {
slice := []int{0, 1, 2, 3, 4}
slice1 := []int{5, 6, 7, 8, 9}
fmt.Println(slice)
slice = append(slice, slice1...)//обратите внимание на многоточие!
fmt.Println(slice)
}
//Результат:
//[0 1 2 3 4]
//[0 1 2 3 4 5 6 7 8 9]
Как это стало возможным? Резонный вопрос. Дело в том, что append - это так называемая вариативная функция. Знак многоточия означает, что количество параметров данного типа не определено. Таким образом, передаваемый в качестве аргумента срез рассматривается функцией как последовательность аргументов данного типа. Но не будем слишком отвлекаться и забегать вперёд. Предлагаю о функциях побеседовать подробнее в соответствующей главе. А сейчас - просто запомните данный кейс.
Интересный момент: срез - аргумент можно задать без создания переменной,"на лету":
package main
import (
"fmt"
)
func main() {
slice := []int{0, 1, 2, 3, 4}
fmt.Println(slice)
slice = append(slice, []int{5, 6, 7, 8, 9}...)
fmt.Println(slice)
}
//Результат:
//[0 1 2 3 4]
//[0 1 2 3 4 5 6 7 8 9]
Непроинициализированный, "пустой" срез, значение которого nil, не помеха тому, чтобы в него успешно "аппендить":
package main
import (
"fmt"
)
func main() {
var slice []int //значение nil
fmt.Println(slice)
slice = append(slice, []int{0, 1, 2, 3, 4}...)
fmt.Println(slice)
}
//Результат:
//[]
//[0 1 2 3 4]
copy
"Говорящее" название этой встроенной функции лишает нас необходимости размышлять о её назначении. Просто возьмём, и скопируем один срез в другой.
package main
import (
"fmt"
)
func main() {
slice1 := []int{0, 1, 2, 3, 4}
slice2 := []int{5, 6, 7, 8, 9}
copy(slice1, slice2)
fmt.Println(slice1)
fmt.Println(slice2)
}
//Результат:
//[5 6 7 8 9]
//[5 6 7 8 9]
Как видите, второй аргумент копируется в первый. При несовпадении размерности срезов, поведение функции будет зависеть от того, который из срезов больше. Если в первом аргументе содержится больше элементов, будут скопированы все элементы второго среза, лишние же останутся нетронутыми:
package main
import (
"fmt"
)
func main() {
slice1 := []int{0, 1, 2, 3, 4, 10, 11, 12}
slice2 := []int{5, 6, 7, 8, 9}
copy(slice1, slice2)
fmt.Println(slice1)
}
//Результат:
//[5 6 7 8 9 10 11 12]
В противоположной ситуации, то есть при меньшей размерности первого среза, избыточные элементы второго среза будут проигнорированы (отброшены):
package main
import (
"fmt"
)
func main() {
slice1 := make([]int, 2)
slice2 := []int{5, 6, 7, 8, 9}
copy(slice1, slice2)// элементы 7, 8, 9 "не помещаются"
fmt.Println(slice1)
}
//Результат:
//[5 6]
Приём "работа на месте"
Вернитесь на пару-тройку глав назад, и вспомните наше недавнее знакомство с таким синтаксическим приёмом, как "присваивание кортежу". Этот приём неоценим в работе с типами данных, представляющими из себя индексированные последовательности элементов. Собственно, ценность заключается в факте возможности так называемой "работы на месте" с элементами, что позволяет писать оптимизированные программы, то есть экономить память. приведу пример, который рекомендую вам перенести в свою библиотеку полезных функций.
func reverse(s []byte) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}
Эта функция делает реверс "на месте" элементов заданного среза. Соберите информацию по теме endianness, и вам сразу станет интересно приведенное выше. Впрочем, мы, скорее всего, ещё коснёмся данного вопроса. Скучно не будет!
Срезы: а что под капотом?
Практически, мы использовали срезы как динамические массивы. Это допустимо, но необходимо понимать, что это лишь удобное упрощение, редукция. Настало время разобраться, что же такое представляет собою срез в языке Go фактически. Суть дела заключается в том, что всякий срез привязан к подлежащему так называемому базовому массиву. Он содержит такие понятия, как указатель, длину и ёмкость. Указатель "глядит" на первый элемент массива, доступный через срез. Благодаря этому, изменение среза, приводит и к изменению базового массива, в том числе и при передаче среза в функцию в качестве аргумента. Длина берётся при помощи уже знакомой нам встроенной функции len. В свою очередь, функция cap даёт нам значение ёмкости, которая есть количество элементов между началом среза и концом базового массива. Таким образом, когда мы создаём срез, "под капотом" на деле создаётся массив, с которого и берётся собственно "срез". С другой стороны, можно сначала явно создать массив, а затем "привязать" к нему срез, или несколько срезов, в том числе, возможно, перекрывающих друг друга. А ещё, при использовании функции make, можно третьим параметром указать емкость базового массива, отличную от создаваемого среза. Попытаемся проиллюстрировать сказанное.
Простое объявление среза. Что делает компилятор на самом деле?
package main
import (
"fmt"
)
func main() {
//Объявляя срез, "за кулисами" на деле создаётся базовый массив
//Объявленный срез указывает на нулевой элемент массива
//и равен его ёмкости
slice := []int{0, 1, 2, 3, 4}
fmt.Println(len(slice))//длина среза
fmt.Println(cap(slice))//ёмкость
}
//Результат:
//5
//5
Явное объявление базового массива с последующим взятием с него срезов:
package main
import (
"fmt"
)
func main() {
//Создаём явно базовый массив
var week = [7]string{"Понедельник", "Вторник", "Среда", "Четверг", "Пятница",
"Суббота", "Воскресенье"}
//Делаем с него срезы
workDays := week[:5]
weekend := week[5:]
fmt.Println(week)
fmt.Println(workDays)
fmt.Println(weekend)
fmt.Println(len(workDays))
fmt.Println(cap(workDays))
fmt.Println(len(weekend))
fmt.Println(cap(weekend))
}
//Результат:
//[Понедельник Вторник Среда Четверг Пятница Суббота Воскресенье]
//[Понедельник Вторник Среда Четверг Пятница]
//[Суббота Воскресенье]
//5
//7
//2
//2
Важная деталь! При взятии среза из элементов базового массива в диапазоне X...Y указывается значение Y+1. Как следует из примера выше, срез workDays содержит элементы с 0 по 4 включительно, однако записывается это как workDays := week[:5]! Будьте внимательны!
И, наконец, продемонстрируем третий параметр функции make:
package main
import (
"fmt"
)
func main() {
slice := make([]int, 5, 10)
fmt.Println(len(slice))//длина среза
fmt.Println(cap(slice))//ёмкость
}
//Результат:
//5
//10
По факту, создаётся базовый массив ёмкостью 10 элементов и с него берётся срез, являющийся указателем на нулевой элемент этого массива, и имеющий длину 5 элементов.
Сделаем выводы. Мы осознали, что срезы - это замечательный, легковесный инструмент для работы с последовательностями однотипных данных, который на практике используется гораздо чаще и, пожалуй, более охотно, чем массивы. Цена же этого - некоторая сложность фактической реализации, которую необходимо ясно себе представлять, чтобы грамотно ориентироваться в материале.
One more thing...
Для вывода на экран содержимого среза, правильнее всего использовать цикл for. При работе с большим количеством однотипных срезов, во избежание загромождения кода циклами и следуя концепции "The DRY Principle: Don't Repeat Yourself", рекомендую добавить в свою библиотеку такую функцию:
//PrintBuf func prints elements of the slice
func PrintBuf(buffer []int) {
for _, val := range buffer {
fmt.Printf("%v ", val)
}
}
Разумеется, для обработки срезов различного типа, функция нуждается в корректировке параметра. Подобную же функцию можно использовать и для "распечатки" массивов. Для этого лишь нужно в квадратных скобках указать размерность массива-параметра.
Для особо любознательных и нетерпеливых читателей сообщу, что для работы с байтовыми массивами существует встроенный пакет bytes. Строки также можно трактовать как разновидность массивов, и для работы с ними существует пакет strings, во многом аналогичный брату-близнецу bytes. Конечно, это будет большим забеганием вперёд.
Отображения
Во всякой науке первейшая вещь - дефиниция. Отображения - как раз тот случай, когда следует об этом поговорить. В английском оригинале эта сущность определяется как maps. Поэтому некоторые не мудрствуя лукаво обзывают это просто "мапами", или "картами", на худой конец. В серьёзных же источниках в основном принят термин "отображения", который мы и будем употреблять, как наиболее академический. Что же по факту представляют собой maps? Отображения - это ассоциативные массивы по типу ключ - значение, где как ключ, так и значение могут быть любого типа (это может быть даже другое отображение). Очень близки к отображениям словари (dictionary) из C#. С математической точки зрения отображения - это истинные множества, так как это есть неупорядоченные совокупности уникальных данных.
Объявим отображение. Отметим, что в отличие от срезов, мы обязаны сразу проинициализировать наше отображение. Это делается при помощи встроенной функции make, либо в результате сокращённой инициализации. В противном случае отображение всё же будет объявлено (со значением nil), однако дальнейшая работа с таким отображением вызовет исключение:
package main
import (
"fmt"
)
func main() {
//Создадим отображение типа [int]string
//то есть ключ имеет тип int, а значение - string
var didgits map[int]string
fmt.Println(didgits)
if didgits == nil {
fmt.Println("NIL")
}
//Добавляем элемент
didgits[0] = "Zero" // вызовет типичное исключение
}
//Результат:
//map[]
//NIL
//panic: assignment to entry in nil map
Вот так правильно:
package main
import (
"fmt"
)
func main() {
var didgits = make(map[int]string)
fmt.Println(didgits)
didgits[0] = "Zero" // теперь исключения нет
//Добавляем элемент
fmt.Println(didgits[0])
}
//Результат:
//map[]
//Zero
Для добавления элемента используется простое присваивание значения по ключу.
package main
import (
"fmt"
)
func main() {
var didgits = make(map[int]string)
//Добавляем элементы
didgits[0] = "Zero"
didgits[1] = "One"
didgits[2] = "Two"
didgits[3] = "Three"
didgits[4] = "Four"
didgits[5] = "Five"
didgits[6] = "Six"
didgits[7] = "Seven"
didgits[8] = "Eight"
didgits[9] = "Nine"
for key, val := range didgits {
fmt.Println("Key: ", key)
fmt.Println("Val: ", val)
}
}
//Результат: (последовательность принципиально не упорядочена!)
//Key: 7
//Val: Seven
//Key: 8
//Val: Eight
//Key: 2
//Val: Two
//Key: 3
//Val: Three
//Key: 5
//Val: Five
//Key: 6
//Val: Six
//Key: 0
//Val: Zero
//Key: 1
//Val: One
//Key: 4
//Val: Four
//Key: 9
//Val: Nine
Отображения позволяется инициализировать сокращённо:
package main
import (
"fmt"
)
func main() {
didgits := map[int]string{
0: "Zero",
1: "One",
2: "Two",
3: "Three",
4: "Four",
5: "Five",
6: "Six",
7: "Seven",
8: "Eight",
9: "Nine",
}
for key, val := range didgits {
fmt.Println("Key: ", key)
fmt.Println("Val: ", val)
}
}
Для удаления элементов отображения используется встроенная функция delete.
package main
import (
"fmt"
)
func main() {
var didgits = make(map[int]string)
fmt.Println(didgits)
didgits[0] = "Zero"
fmt.Println(didgits)
fmt.Println(didgits[0])
delete(didgits, 0) //передаём имя отображения + ключ
fmt.Println(didgits)
fmt.Println(didgits[0])// нет такого элемента
}
//Результат:
//map[]
//map[0:Zero]
//Zero
//map[]
Это немного необычно, но попытка вывести на экран или удалить при помощи функции delete несуществующий элемент, не вызовет никакой ошибки! И всё же, вероятно, прежде чем приступить к каким-либо действиям, вы можете захотеть проверить наличие того или иного элемента в данном отображении. Такая возможность существует. Доступ к элементу отображения по ключу может опционально возвращать не только один элемент - собственно значение, но и булев флаг, означающий принадлежность данного ключа множеству, составляющему это отображение.
package main
import (
"fmt"
)
func main() {
var didgits = make(map[int]string)
didgits[0] = "Zero"
//Если элемент существует, выведем его на экран
if value, ok := didgits[0]; ok{
fmt.Println(value)
}
}
//Результат:
//Zero
В завершение, приведу пример, когда значением отображения является другое отображение:
package main
import (
"fmt"
)
func main() {
//Ключ - int, значение - map[string]string
weekdays := map[int]map[string]string{
1: map[string]string{"name": "Sunday", "type": "weekend"},
2: map[string]string{"name": "Monday", "type": "workDay"},
3: map[string]string{"name": "Wednesday", "type": "workDay"},
4: map[string]string{"name": "Tuesday", "type": "workDay"},
5: map[string]string{"name": "Thursday", "type": "workDay"},
6: map[string]string{"name": "Friday", "type": "workDay"},
7: map[string]string{"name": "Saturday", "type": "weekend"},
}
if value, ok := weekdays[1]; ok {
fmt.Println(value["name"], value["type"])
}
}
//Результат:
//Sunday weekend
Пожалуй, самое последнее... Отображения отлично сериализуются! Благодаря своей гибкости и полиморфности, они отлично зарекомендовали себя при работе с JSON в качестве полей структур. Но об этом - в своё время.
Раунд!
Last updated
Was this helpful?