Go in a nutshell
  • Go in a nutshell
  • Введение
  • Родословная или Should I Go?
  • Hello, world!
  • Базовые типы данных
  • Go - установка, сборка
  • Алфавит и пакеты
  • Синтаксис
  • Приведение типов
  • Операторы
  • Константы
  • Управление потоком
  • Массивы, срезы, отображения
  • Именованные типы и структуры
  • Указатели и оператор new
  • Функции
  • Работа с параметрами командной строки
  • Завершение работы программы
  • Методы
Powered by GitBook
On this page
  • Массивы
  • Срезы
  • Append и copy - встроенные функции
  • Приём "работа на месте"
  • Срезы: а что под капотом?
  • Отображения

Was this helpful?

Массивы, срезы, отображения

Перечислимые типы

Массивы

Пожалуй, массивы в языке 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 в качестве полей структур. Но об этом - в своё время.

Раунд!

PreviousУправление потокомNextИменованные типы и структуры

Last updated 3 years ago

Was this helpful?