08 февраля 2024

🏃 Самоучитель по Go для начинающих. Часть 7. Массивы и слайсы. Append и сopy. Пакет slices

Энтузиаст-разработчик, автор статей по программированию. Сфера интересов - backend, web 3.0, кибербезопасность.
Рассмотрим реализацию массивов и слайсов в языке Go, разберем функции append и copy, изучим пакет slices и по традиции решим несколько занимательных задач.
🏃 Самоучитель по Go для начинающих. Часть 7. Массивы и слайсы. Append и сopy. Пакет slices

Массивы

Массивы в Go
Массивы в Go

Массивы в Go представляют собой фиксированную по размеру коллекцию данных заданного типа. Выделение памяти для массива происходит в момент его инициализации, а хранится он в ней как последовательность блоков указанного типа.

Рассмотрим несколько способов создания массива в Go:

  • С помощью ключевого слова var и последующего присвоения значений:
        var arr1 [2]int
arr1[0] = 0
arr1[1] = 1

var users [2]string
users[0] = "Петя"
users[1] = "Гоша"
    
  • С указанием длины массива и входящих в него элементов:
        arr2 := [3]int{0, 1, 2}

    
  • С перечислением элементов, но без указания длины, которая будет автоматически подсчитана компилятором:
        arr3 := [...]bool{true, false, true}

    
  • Для создания N-мерного массива нужно указать длину каждого измерения, тип и в скобках {} перечислить элементы:
        arrND := [len1][len2][len3]....[lenN]T{}

    

Например, так создается двумерный массив с целочисленными элементами:

        arr2D := [2][2]int{
	{1, 2},
	{3, 4},
}
fmt.Println(arr2D) // [[1 2] [3 4]]

    

Стоит помнить, что массивы разной длины имеют разные типы, так как размер массива входит в определение типа:

        arr3 := [3]int{1, 2, 3}
arr4 := [3]int{1, 2, 3}
fmt.Println(arr3 == arr4) // true

// ошибка: массивы имеют разную длину
arr1 := [3]int{1, 2, 3}
arr2 := [2]int{1, 2}
fmt.Println(arr1 == arr2) // mismatched types [3]int and [2]int

    

Значения массива можно изменять в цикле:

        arr := [3]string{"alpha", "beta", "gamma"}
for ind, val := range arr {
	fmt.Printf("Элемент с индексом %d: %s\\n", ind, val)
}
// Вывод:
// Элемент с индексом 0: alpha
// Элемент с индексом 1: beta
// Элемент с индексом 2: gamma

    

Аналогичная конструкция, использующая функцию len() для вычисления длины массива:

        for i := 0; i < len(arr); i++ {
	fmt.Printf("Элемент с индексом %d: %s\\n", i, arr[i])
}

    

При работе с массивами нужно всегда следить за их длиной и не допускать обращения к посторонней области памяти. При выходе за границы массива компилятор сообщит об ошибке index out of range:

        // ошибка - index out of range [3] with length 3
arr := [3]string{"alpha", "beta", "gamma"}
for i := 0; i < 4; i++ {
	fmt.Printf("Элемент с индексом %d: %s\\n", i, arr[i])
}

    
👨‍💻 Библиотека Go разработчика
Больше полезных материалов вы найдете на нашем телеграм-канале«Библиотека Go разработчика»
🎓 Библиотека Go для собеса
Подтянуть свои знания по Go вы можете на нашем телеграм-канале «Библиотека Go для собеса»
🧩 Библиотека задач по Go
Интересные задачи по Go для практики можно найти на нашем телеграм-канале«Библиотека задач по Go»

Особенности массивов в Go

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

Чтобы лучше понять эту особенность, обратимся к конкретному примеру. В коде ниже массив result копируется при передаче в функцию changeArray, поэтому все операции увеличения производятся над массивом arr, а result остается без изменения.

        func changeArray(arr [3]int, elem int) {
	for i := 0; i < len(arr); i++ {
		arr[i] += elem
	}
}

func main() {
	result := [3]int{1, 2, 3}
	changeArray(result, 100)
	fmt.Println(result) // 1 2 3
}

    

Для изменения массива result следует передать его по указателю в функцию changeArrayPtr, которая, в отличие от changeArray, в качестве одного из параметров принимает не просто массив, а указатель на него:

        func changeArrayPtr(arr *[3]int, elem int) {
	for i := 0; i < len(arr); i++ {
		arr[i] += elem
	}
}

func main() {
	arr := [3]int{1, 2, 3}
	changeArrayPtr(&arr, 100) // передача массива по указателю
	fmt.Println(arr) // 101 102 103
}

    

Слайсы

Слайсы в Go
Слайсы в Go

Слайс – это расширенная реализация массива, которая поддерживает динамическое изменение размера. В исходном коде Go он представляет собой структуру с тремя полями:

        type slice struct {
	array unsafe.Pointer // ссылка на массив
	len   int // длина
	cap   int // емкость
}

    

Из описания структуры можно понять, что слайс не хранит никаких данных, а всего лишь описывает часть базового массива, на который ссылается.

Длина слайса (len) – это количество элементов, содержащихся в нем.

Емкость слайса (cap) – это количество элементов, которые могут быть сохранены в слайсе до того момента, когда произойдет его перераспределение. При этом емкость должна быть строго больше длины, иначе произойдет ошибка компиляции.

Создать слайс можно несколькими способами:

  • С помощью ключевого слова var создается nil-слайс:
        var s1 []int

    
  • Короткое объявление:
        s2 := []string{"a", "b"}

    
  • С указанием типа данных и размера в функции make. В таком случае емкость по умолчанию будет равна заданной длине:
        s3 := make([]int, 3) // аналог []int{0, 0, 0}

    
  • С указанием типа данных, размера и ёмкости в функции make:
        s4 := make([]int, 3, 5) // аналог new([5]int)[:3]

    
  • Создание слайса из массива:
        arr := [2]bool{true, false}
s5 := arr[:] // срез по массиву

    
  • Создать n-мерный слайс можно с помощью добавления измерений в цикле for:
        slice2D := make([][]int, 3)
for i := range slice2D {
	slice2D[i] = make([]int, 3)
	slice2D[i][i] = 1
}
fmt.Println(slice2D) // [[1 0 0] [0 1 0] [0 0 1]]

    

Срез

Срез представляет собой слайс, который ссылается только на заданную часть базового массива или слайса и создается с помощью указания начального и конечного индекса в квадратных скобках:

        slc := []int{1, 2, 3, 4, 5}
part := slc[0:2] // слайс []int{1, 2}

    

Слайс и срез часто объединяют в одно понятие и подразумевают под ним структуру со ссылкой на массив, длиной и емкостью.

Чтобы избежать путаницы, в данном пособии будем считать слайсом расширенную реализацию массива, созданную одним из ранее рассмотренных способов, а срезом – только часть исходного массива или слайса, полученную с помощью конструкции [:].

Особенности слайсов в Go

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

Продемонстрируем эту особенность на следующем примере:

        func main() {
	first := []int{1, 2, 3}
	second := first // []int{1, 2, 3}
	third := first[0:2] // []int{1, 2}

	second[0] = 10
	third[1] = 20
	fmt.Println(first, second, third)
	// Вывод:
	// [10 20 3] [10 20 3] [10 20]

	second = append(second, 60) // теперь second не ссылается на first
	second[0] = 30
	fmt.Println(first, second, third)
	// Вывод:
	// [10 20 3] [30 20 3 60] [10 20]
}

    

В первом выводе можно заметить, что изменения в слайсах second и third коснулись также слайса first. При добавлении числа 60 в слайс second он переаллоцировался и теперь перестал ссылаться на first, так как хранится по новому адресу. Поэтому изменение его нулевого элемента не коснется слайсов first и third, что видно во втором выводе.

Передача слайса в функцию

Несмотря на то что слайс содержит указатель, он на самом деле является именно значением структуры, содержащей указатель на базовый источник данных.

Рассмотрим пример, наглядно демонстрирующий эту особенность. Передадим слайс nums в функцию без возвращаемого значения doubleNumbers, которая увеличит его элементы двое и попытается изменить длину на 2.

        func doubleNumbers(slc []int) {
	for i := range slc {
		slc[i] *= 2
	}
	slc = slc[0 : len(slc)-2]
}

func main() {
	nums := []int{1, 2, 3, 4}
	doubleNumbers(nums)
	fmt.Println(nums, len(nums), cap(nums)) // [2 4 6 8] 4 4
}

    

Как нетрудно заметить, элементы nums действительно увеличились в два раза, а вот длина осталась неизменной.

Теперь добавим возвращаемое значение в функцию doubleNumbers и присвоим результат её выполнения переменной newNums:

        func doubleNumbers(slc []int) []int {
	for i := range slc {
		slc[i] *= 2
	}
	slc = slc[0 : len(slc)-2]
	return slc
}

func main() {
	nums := []int{1, 2, 3, 4}
	newNums := doubleNumbers(nums)

	fmt.Println(nums, newNums)
	fmt.Println("len nums:", len(nums), "cap nums:", cap(nums))
	fmt.Println("len newNums:", len(newNums), "cap newNums:", cap(newNums))
	// [2 4 6 8] [2 4]
	// len nums: 4, cap nums: 4
	// len newNums: 2, cap newNums: 4
}

    

В данном случае слайс nums не изменяется, но возвращаемое значение содержит новую длину, которая сохранится в newNums.

Длина слайса nums осталась неизменной, так как передалась её копия, а не оригинал. Если необходимо изменить длину, нужно явно указать возвращаемое значение функции и присвоить его переменной при вызове, как это было показано в последнем примере.

Append

Для добавления элементов в слайс используется функция append со следующей сигнатурой:

        func append(s []T, vs ...T) []T

    

Она принимает в качестве параметров слайс и значения, которые необходимо в него добавить. В коде ниже показан пример её применения:

        arr := []int{1, 2, 3}
arr = append(arr, 4)
arr = append(arr, 5, 6)
fmt.Println(arr) // [1 2 3 4 5 6]

arr = append(arr, "string") // ошибка

    

В Go допускается добавление одного слайса в другой. Это делается следующим образом:

        slc1 := []int{10, 11, 12}
slc2 := []int{13, 14, 15}
slc1 = append(slc1, slc2...)
fmt.Println(slc1) // [10 11 12 13 14 15]

slc3 := append([]int(nil), slc2...) // создание копии slc2
slc2 = append(slc2, slc2...) // добавление slc2 в конец slc2
fmt.Println(slc2, slc3) // [13 14 15 13 14 15] [13 14 15]

    

Во время выполнения программы функция append самостоятельно решает, нужно ли создавать новый слайс. Это происходит, например, при превышении имеющейся емкости:

        func main() {
	var slc = make([]int, 5)

	fmt.Printf("old address: %p\\n", slc)            // исходный адрес
	fmt.Println("len:", len(slc), "cap:", cap(slc)) // 5 5

	slc = append(slc, 1)

	fmt.Printf("new address: %p\\n", slc)            // новый адрес
	fmt.Println("len:", len(slc), "cap:", cap(slc)) // 6 10
}

    

В коде выше при вызове append будет превышена емкость слайса slc, что приведет к её увеличению вдвое и изменению адреса slc на новый.

Функция append напрямую влияет на использование памяти в программе, поэтому для избежания лишних аллокаций и незапланированного копирования, необходимо следить за емкостью созданных слайсов.

Copy

Функция Copy в Go
Функция Copy в Go

Копирование слайсов производится с помощью отдельной функции copy со следующей сигнатурой:

        func copy(dst, src []Type) int

    

Она копирует элементы из исходного слайса в целевой и возвращает количество скопированных элементов, которое будет минимальным из len(dst) и len(src).

Рассмотрим несколько классических примеров использования copy:

  • Копирование слайса большего размера в слайс с меньшим:
        slc := []int{1, 2}
n3 := copy(slc, []int{3, 4, 5})
fmt.Println(n3, slc) // 2 [3, 4]

    
  • Копирование слайса меньшего размера в слайс с большим:
        slc := []int{1, 2, 3, 4}
n3 := copy(slc, []int{5, 6})
fmt.Println(n3, slc) // 2 [5, 6, 3, 4]

    
  • Копирование слайса в самого себя:
        slc := []int{1, 2, 3, 4}
n2 := copy(slc, slc[2:])
fmt.Println(n2, slc) // 2 [3 4 3 4]

    
  • Особый случай – копирование байтов строки в слайс байтов.
        s := "example"
bytes := make([]byte, 3)
copy(bytes, s)
fmt.Println(bytes, string(bytes)) // [101 120 97] exa

copy(bytes, s[3:])
fmt.Println(bytes, string(bytes)) // [109 112 108] mpl

    

Пакет slices

В версии 1.21 разработчики Go добавили новый пакет slices, облегчающий работу со слайсами. Он позволяет использовать готовые функции для выполнения базовых операций со слайсами, что в разы упрощает работу. Чтобы почувствовать разницу, обратимся к примеру кода для поиска максимального значения в слайсе до и после введения пакета slices:

        // до пакета slices:
slc := []int{5, 9, 1, 100}
max := slc[0]
for _, val := range slc {
	if val > max {
		max = val
	}
}

// пакет slices:
max := slices.Max(slc)

    

Теперь рассмотрим примеры практического применения некоторых функций из пакета slices:

  • Сортировка слайса – slices.Sort:
        slc := []int{4, 3, 5, 2, 6, 1}
slices.Sort(slc) // [1 2 3 4 5 6]

    
  • Сравнение слайсов – slices.Compare:
        slc1 := []int{1, 2, 7, 3}
slc2 := []int{2, 3, 1, 7}
fmt.Println(slices.Compare(slc1, slc2)) // -1

    
  • Поиск элемента в слайсе – slices.Contains:
        slc := []int{1, 2, 7, 3}
	fmt.Println(slices.Contains(slc, 1))
	fmt.Println(slices.Contains(slc, 4))

    
  • Удаление элементов в диапазоне [i:j] из слайса – slices.Delete:
        slc := []int{1, 2, 7, 3}
slc = slices.Delete(slc, 0, 2)
fmt.Println(slc) // [7, 3]

    
  • Вставка элементов начиная с указанного индекса – slices.Insert:
        letters := []string{"alpha", "delta"}
letters = slices.Insert(letters, 1, "beta", "gamma")
fmt.Println(letters)

    

Посмотреть все функции пакета slices и примеры их использования можно на официальном сайте go.

Задачи

Давайте решим несколько классических задач на массивы и слайсы для оттачивания навыков программирования и закрепления материала статьи. Для их решения достаточно применить изученные в этом уроке конструкции, при этом не рекомендуется использовать сторонние пакеты, таких как slices, math и другие.

Самый хороший дом

Задача «Самый хороший дом»
Задача «Самый хороший дом»

На некоторой улице в произвольном порядке стоят n домов. Каждый дом можно однозначно определить с помощью его номера и индекса расположения относительно начала улицы (она начинается слева). При этом номера домов могут повторяться. Назовем хорошим дом, имеющий наибольший номер и располагающийся наиболее близко к началу улицы. Ваша задача — вывести номер и индекс такого дома.

Входные данные: на первой строке подается количество чисел – натуральное число n (n < 1000), на второй строке через пробел перечисляются целочисленные номера домов в диапазоне от -10^4 до 10^4.

Выходные данные: два числа – номер и индекс хорошего дома.

Решение: задача заключается в нахождении значения и индекса максимального числа массива.

        func main() {
	var n int
	fmt.Scan(&n)
	nums := make([]int, n)
	for i := range nums {
		fmt.Scan(&nums[i])
	}

	var maxValue = -10001
	var maxIndex = -10001
	for i := range nums {
		if nums[i] > maxValue {
			maxValue = nums[i]
			maxIndex = i
		}
	}
	fmt.Println(maxValue, maxIndex)
}

    

Гоша ищет редкие числа

Гоша всерьёз увлекся математикой и поставил себе такую задачу: написать на доске n целых чисел и найти среди них самые редкие, то есть такие, что встречаются только один раз. Ваша задача помочь Гоше проверить свои вычисления, написав код для решения этой интересной задачи.

Входные данные: на первой строке подается количество чисел – натуральное число n (n < 10^6), на второй строке через пробел перечисляются целые числа.

Выходные данные: числа, встречающиеся в последовательности ровно один раз.

Решение:

        func main() {
	var n int
	fmt.Scan(&n)
	nums := make([]int, n)
	for i := range nums {
		fmt.Scan(&nums[i])
	}

	var cnt int // счетчик вхождения значений в массив
	for i := range nums {
		cnt = 0 // каждую итерацию обнуляем значение счетчика
		for j := range nums {
			// если два числа с разными индексами совпали
			if nums[i] == nums[j] && i != j {
				cnt++ // увеличиваем значение счетчика
			}
		}
		// если счетчик равен нулю, это значит, что в 
		// текущей итерации не было совпадающих значений
		if cnt == 0 {
			fmt.Println(nums[i])
		}
	}
}

    

Перестановка соседей

Напишите функцию shiftNeighbour(nums []int) для перестановки соседних элементов слайса. Для нечетного количества элементов последний из них остается без изменения.

Пример работы функции:

        nums1 := []int{1, 2, 3, 4}
shiftNeighbour(nums1)
fmt.Println(nums1) // [2 1 4 3]

nums2 := []int{9, 8, 10}
shiftNeighbour(nums2)
fmt.Println(nums2) // [8 9 10]

    

Решение:

        func shiftNeighbour(nums []int) {
	for i := 1; i < len(nums); i += 2 {
		tmp := nums[i]
		nums[i] = nums[i-1]
		nums[i-1] = tmp
	}
}

    

Циклический сдвиг слайса*

Реализуйте функцию sliceShift(nums []int, shift int) []int для циклического сдвига слайса на shift элементов вправо, если shift > 0, и влево, если shift < 0.

Подсказка: для заполнения нового слайса копируйте в него часть исходного с помощью функции copy().

Решение: для начала приведем сдвиг к корректной форме. Если shift > 0, то достаточно взять остаток от деления сдвига на len(nums). В случае shift < 0 заметим, что сдвиг влево равен сдвигу вправо на len(nums) + shift, и преобразуем его к нужному виду. Далее создадим новый слайс и скопируем туда элементы исходного, учитывая индексы:

        func sliceShift(nums []int, shift int) []int {
	ln := len(nums)
	shift %= ln
	if shift < 0 { // приведение сдвига к корректной форме
		shift = -shift
		shift %= ln
		shift = ln - shift
	}
	res := make([]int, ln)
	copy(res[shift:], nums[:ln-shift])
	copy(res[:shift], nums[ln-shift:])
	return res
}

    

Подведём итоги

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

В следующей статье цикла погрузимся в изучение строк, байтов, рун, разберем их особенности и познакомимся с хеш-таблицами.

***

Содержание самоучителя

  1. Особенности и сфера применения Go, установка, настройка
  2. Ресурсы для изучения Go с нуля
  3. Организация кода. Пакеты, импорты, модули. Ввод-вывод текста.
  4. Переменные. Типы данных и их преобразования. Основные операторы
  5. Условные конструкции if-else и switch-case. Цикл for. Вложенные и бесконечные циклы
  6. Функции и аргументы. Области видимости. Рекурсия. Defer
  7. Массивы и слайсы. Append и сopy. Пакет slices
  8. Строки, руны, байты. Пакет strings. Хеш-таблица (map)
  9. Структуры и методы. Интерфейсы. Указатели. Основы ООП
  10. Наследование, абстракция, полиморфизм, инкапсуляция
  11. Обработка ошибок. Паника. Восстановление. Логирование

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ