golang

Массивы и слайсы в Golang

  • пятница, 30 января 2026 г. в 00:00:11
https://habr.com/ru/articles/990544/

Для начала хотелось бы сказать, что же такое массивы и слайсы.

Массивы

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

Давайте рассмотрим на примере:

package main

func main() {
	/*
		Массив создаётся в таком формате:
		Имя := [количество элементов массива] тип данных {элементы массива, через запятую}
	*/

	arr := [3]int{0, 1, 2}
	fmt.Println(arr[1])
}

На примере выше был создан массив с 3-мя элементами и типом данных int.

После создания массива мы не можем изменять его вместительность, однако можем менять сами элементы.

Каждый элемент массива имеет свой индекс. Для простоты понимания в примере элемент равен своему индексу.
Индексация в Go, как и в других языках программирования, начинается с 0.

Другие способы создания массива

package main

import "fmt"

func main() {
	arr := [3]int{0, 1, 2}
	fmt.Println(arr[1])

	var arr1 [2]int
	arr1[0] = 0 // указываем индекс элемента и значение
	arr1[1] = 1

	arr2 := [...]int{5, 6, 7} // массив автоматически посчитает, сколько элементов в нём находится
	fmt.Println(arr2[1])
}

Что выведет fmt.Println(arr2[1])?

Массивы в Go являются обычными значениями, так что при их вызове они просто копируются. Поэтому при передаче или присваивании массивов создаётся копия их содержимого.

Если мы в одной функции создадим массив и внесём какие-то изменения в другой функции, то оригинальный массив не изменится.

package main

import "fmt"

func main() {
	arr2 := [...]int{5, 6, 7}
	fmt.Println(arr2[1]) // Первый по индексу элемент массива — это 6 (индексация начинается с нуля)

	change(arr2)
	fmt.Println(arr2) // Вывод после вызова функции
}

func change(arr2 [3]int) {
	arr2[0] = 12 // Изменяем значение по индексу. Было 5, станет 12
	fmt.Println(arr2) // Вывод (12 6 7)
}

Вывод в консоль:

6
[12 6 7]
[5 6 7]

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

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


Слайсы

Slice в Go - это структура данных динамического размера, которая «ссылается» на массив.

Слайсы создаются аналогично массивам, но имеют ряд отличий.

В работе со слайсами появляются такие термины, как длина (len) и ёмкость (capacity).

package main

import "fmt"

func main() {
	slice := []int{1, 2, 3}
	fmt.Println("cap:", cap(slice), "len:", len(slice)) // Узнаём вместимость и длину слайса

	slice = append(slice, 4) // Добавляем в конец слайса элемент
	fmt.Println("cap:", cap(slice), "len:", len(slice)) // Слайс копировался в более вместительный слайс, с запасом (обычно в 2 раза больше прошлой capacity)

	slice = append(slice, 5, 6, 7) // Добавим ещё элементы, чтобы посмотреть, что происходит в этот раз
	fmt.Println("cap:", cap(slice), "len:", len(slice))
}

Вывод в консоль:

cap: 3 len: 3
cap: 6 len: 4
cap: 12 len: 7

На примере выше мы создаём слайс и выводим его ёмкость и длину (после создания слайса его ёмкость и длина равны).

После этого мы добавляем в конец слайса новый элемент и смотрим на изменения длины и ёмкости. Ёмкость увеличилась в 2 раза.

Далее мы добавляем ещё 3 элемента, чтобы снова переполнить слайс, и ёмкость нового слайса снова удваивается.

Динамический размер

  • Размер слайса может изменяться в процессе работы программы.

Под капотом слайс состоит из:

  • Указателя на массив (pointer)

  • Длины (len)

  • Ёмкости (cap)

Переаллокация памяти

  • Когда длина слайса превышает его ёмкость, Go выделяет новый массив с увеличенной ёмкостью (обычно в 2 раза больше).

  • Старые данные копируются в новый массив.

Ключевые отличия массива и слайса

Характеристика

Массив

Слайс

Размер

Фиксированный

Динамический

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

Передаётся копией

Передаётся по ссылке

Память

Последовательный блок

Указатель на массив + метаданные

Гибкость

Не изменяется

Можно добавлять/удалять элементы


Вопросы и задачи по массивам и слайсам в Go

Вопросы

  1. Чем отличаются массивы и слайсы в Go?

  2. Почему размер массива нельзя изменить после создания?

  3. Какие параметры есть у слайса и для чего они нужны?

  4. Что происходит при добавлении элементов в слайс с помощью append?

  5. В каком случае при append происходит копирование данных?

  6. Почему изменения элементов слайса внутри функции видны снаружи?

  7. В чём разница между длиной (len) и ёмкостью (cap) слайса?

  8. Как влияет превышение capacity на работу слайса?

  9. Почему массивы при передаче в функцию не изменяются?

  10. В каких случаях стоит использовать массив, а в каких - слайс?


Задачи

1)

// Какой будет результат выполнения программы?
func main() {
	a := []string{"a", "b", "c"}
	b := a[1:2]
	b[0] = "q"

	fmt.Println(a)
}

Объясните:

  • почему изменился исходный слайс,

  • какой элемент был изменён и почему.


2)

// Что выведет код и почему?
func mod(a []int) {
	a = append(a, 125)

	for i := range a {
		a[i] = 5
	}

	fmt.Println(a)
}

func main() {
	sl := []int{1, 2, 3, 4, 5}
	mod(sl)
	fmt.Println(sl)
}

Ответьте:

  • изменился ли исходный слайс,

  • какую роль сыграл append.


3)

// Что выведет код и почему?
func mod(a []int) {
	for i := range a {
		a[i] = 5
	}
	fmt.Println(a)
}

func main() {
	sl := make([]int, 4, 8)
	sl[0] = 1
	sl[1] = 2
	sl[2] = 3
	sl[3] = 5

	mod(sl)
	fmt.Println(sl)
}

Объясните:

  • изменился ли исходный слайс,

  • почему изменения видны за пределами функции.


4)

// Что выведет программа?
func main() {
	a := make([]int, 0, 2)

	a = append(a, 1)
	a = append(a, 2)

	b := append(a, 3)

	fmt.Println(a)
	fmt.Println(b)
}

Подсказка: обратите внимание на значения len и cap.


5)

// Почему слайс не был очищен?
func modify(a []int) {
	a = a[:0]
}

func main() {
	sl := []int{1, 2, 3}
	modify(sl)
	fmt.Println(sl)
}

Объясните, почему длина слайса не изменилась вне функции.


6)

// Какой будет результат?
func main() {
	a := []int{1, 2, 3}
	b := a[:2]
	c := a[1:]

	b[1] = 100
	c[0] = 200

	fmt.Println(a)
}

Объясните:

  • какие элементы были изменены,

  • почему результат именно такой.


Рекомендация

Для лучшего понимания рекомендуется запускать примеры локально и выводить значения len и cap после каждой операции со слайсами.

Ответы на вопросы по массивам и слайсам в Go


1. Чем отличаются массивы и слайсы в Go?

Массив - это фиксированная структура данных, которая копируется при передаче.
Слайс - это динамическая структура, которая ссылается на данные.

Пример с пояснениями

package main

import "fmt"

func changeArray(a [3]int) {
	a[0] = 100
	// Меняем первый элемент массива a
}

func changeSlice(s []int) {
	s[0] = 100
	// Меняем первый элемент слайса s
}

func main() {
	arr := [3]int{1, 2, 3}
	sl := []int{1, 2, 3}

	changeArray(arr)
	changeSlice(sl)

	fmt.Println(arr) // [1 2 3]
	fmt.Println(sl)  // [100 2 3]
}

Почему так произошло?

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


2. Почему размер массива нельзя изменить после создания?

Потому что размер массива — часть его типа.

Пример

var a [3]int
var b [4]int

// a и b — разные типы

Почему так произошло?

В Go тип [3]int и [4]int - это разные типы.
Поэтому размер массива нельзя изменить, можно только создать новый массив другого размера.


3. Какие параметры есть у слайса и для чего они нужны?

У слайса есть:

  • длина (len)

  • ёмкость (cap)

Пример с пояснениями

package main

import "fmt"

func main() {
	sl := make([]int, 2, 5)
	// len = 2 → сейчас доступно 2 элемента
	// cap = 5 → можно добавить ещё 3 элемента без расширения памяти

	fmt.Println(len(sl)) // 2
	fmt.Println(cap(sl)) // 5
}

Почему так произошло?

len показывает, сколько элементов сейчас в слайсе.
cap показывает, сколько элементов можно добавить, прежде чем Go создаст новый слайс.


4. Что происходит при добавлении элементов в слайс с помощью append?

append либо добавляет элемент в существующую память, либо создаёт новый слайс.

Пример

sl := make([]int, 0, 2)

sl = append(sl, 1)
sl = append(sl, 2)
// Пока cap не превышена - используется та же память

sl = append(sl, 3)
// cap превышена → создаётся новый слайс

Почему так произошло?

Go всегда старается использовать существующую память, но если места не хватает — создаёт новую и копирует данные.


5. В каком случае при append происходит копирование данных?

Когда длина слайса становится больше его ёмкости.

Пример

sl := make([]int, 2, 2)
// len = 2, cap = 2

sl = append(sl, 3)
// cap не хватает → копирование

Почему так произошло?

Потому что cap - это физический предел текущего участка памяти.
При его превышении Go вынужден выделить новое место.


6. Почему изменения элементов слайса внутри функции видны снаружи?

Потому что функция работает с теми же данными, а не с копией.

Пример

func modify(s []int) {
	s[0] = 10
	// Меняем первый элемент
}

func main() {
	sl := []int{1, 2, 3}
	modify(sl)
	fmt.Println(sl) // [10 2 3]
}

Почему так произошло?

Слайс передаётся в функцию как структура, которая указывает на данные.
Поэтому изменение элемента влияет на исходный слайс.


7. В чём разница между len и cap?

len - сколько элементов видно сейчас.
cap - сколько элементов можно добавить без расширения памяти.

Пример

sl := make([]int, 1, 3)
// sl = [0]

fmt.Println(len(sl)) // 1
fmt.Println(cap(sl)) // 3

Почему так произошло?

len - управляет логикой работы со слайсом,cap - оптимизацией памяти.


8. Как влияет превышение capacity на работу слайса?

Превышение cap приводит к созданию нового слайса.

Пример

a := make([]int, 0, 1)

a = append(a, 1)
// cap ещё хватает

b := append(a, 2)
// cap превышена → b новый слайс

Почему так произошло?

Go не может записать данные за пределы выделенной памяти,поэтому создаёт новый участок и копирует данные.


9. Почему массивы при передаче в функцию не изменяются?

Потому что массив копируется целиком.

Пример

func change(a [3]int) {
	a[0] = 100
}

func main() {
	arr := [3]int{1, 2, 3}
	change(arr)
	fmt.Println(arr) // [1 2 3]
}

Почему так произошло?

Функция получила копию массива, а оригинальный массив в main остался без изменений.


10. В каких случаях стоит использовать массив, а в каких — слайс?

В большинстве случаев - слайс.

Почему?

  • массивы редко нужны из-за фиксированного размера

  • слайсы удобнее, гибче и используются почти везде

  • массивы чаще встречаются во внутренней реализации или в учебных примерах

Решения и объяснения задач

Задача 1

Условие

// Какой будет результат выполнения программы?
func main() {
	a := []string{"a", "b", "c"}
	b := a[1:2]
	b[0] = "q"

	fmt.Println(a)
}

Решение + пояснения

package main

import "fmt"

func main() {
	a := []string{"a", "b", "c"}
	// a = ["a", "b", "c"]

	b := a[1:2]
	// Берём срез с индексами [1:2] (2 НЕ включается)
	// b берёт элементы начиная с a[1] и до a[2) → только один элемент
	// b = ["b"]

	b[0] = "q"
	// Меняем b[0]
	// НО b[0] — это на самом деле a[1]
	// Поэтому меняется именно a[1]

	/*
		После изменения:

		a = ["a", "q", "c"]
		b = ["q"]
	*/

	fmt.Println(a) // ["a", "q", "c"]
}

Почему так произошло?
Слайс при создании нового среза не копирует данные, а использует те же значения. Поэтому изменение элемента через b меняет и a.
Индексация в новом слайсе начинается с 0, поэтому b[0] соответствует a[1].


Задача 2

Условие

// Что выведет код и почему?
func mod(a []int) {
	a = append(a, 125)
	for i := range a {
		a[i] = 5
	}
	fmt.Println(a)
}

func main() {
	sl := make([]int, 5, 5)
	sl[0] = 1
	sl[1] = 2
	sl[2] = 3
	sl[3] = 4
	sl[4] = 5

	mod(sl)
	fmt.Println(sl)
}

Решение + пояснения

package main

import "fmt"

func mod(a []int) {

	/*
		В функцию приходит слайс "a".
		Это НЕ копия всех чисел, а ссылка на данные.

		Но сейчас важнее другое:
		В main мы создали sl с len=5 и cap=5.
		То есть места "в запасе" НЕТ.
	*/

	a = append(a, 125)
	/*
		Мы добавляем 125.

		Так как cap=5 и len=5 - места больше нет.
		Значит append создаёт новый слайс в новом месте памяти
		и копирует туда старые 5 элементов + добавляет 125.

		Теперь "a" внутри функции - это УЖЕ ДРУГИЕ данные,
		и он НЕ связан со sl из main.
	*/

	for i := range a {
		a[i] = 5
		// Меняем все элементы "a" на 5
	}

	fmt.Println(a)
	// Выведет: [5 5 5 5 5 5]
}

func main() {
	sl := make([]int, 5, 5)
	sl[0] = 1
	sl[1] = 2
	sl[2] = 3
	sl[3] = 4
	sl[4] = 5
	// sl = [1 2 3 4 5]

	mod(sl)

	fmt.Println(sl)
	// sl не изменился, потому что внутри mod после append был создан новый слайс
	// Выведет: [1 2 3 4 5]
}

Ожидаемый вывод:

[5 5 5 5 5 5]
[1 2 3 4 5]

Почему так произошло?
Потому что append при заполненной ёмкости создаёт новый слайс, и изменения идут уже в нём. Исходный sl остаётся прежним.


Задача 3

Условие

// Что выведет код и почему?
func mod(a []int) {
	for i := range a {
		a[i] = 5
	}
	fmt.Println(a)
}

func main() {
	sl := make([]int, 4, 8)
	sl[0] = 1
	sl[1] = 2
	sl[2] = 3
	sl[3] = 5

	mod(sl)
	fmt.Println(sl)
}

Решение + пояснения

package main

import "fmt"

func mod(a []int) {
	for i := range a {
		a[i] = 5
		// Меняем каждый элемент слайса на 5
	}
	fmt.Println(a) // [5 5 5 5]
}

func main() {
	sl := make([]int, 4, 8)
	// len=4 cap=8

	sl[0] = 1
	sl[1] = 2
	sl[2] = 3
	sl[3] = 5
	// sl = [1 2 3 5]

	mod(sl)

	/*
		Так как append не было, мы меняли существующие элементы.
		Слайс "a" в функции и "sl" в main смотрят на одни и те же значения.
	*/

	fmt.Println(sl) // [5 5 5 5]
}

Ожидаемый вывод:

[5 5 5 5]
[5 5 5 5]

Почему так произошло?

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


Задача 4

Условие

// Что выведет программа?
func main() {
	a := make([]int, 0, 2)

	a = append(a, 1)
	a = append(a, 2)

	b := append(a, 3)

	fmt.Println(a)
	fmt.Println(b)
}

Решение + пояснения

package main

import "fmt"

func main() {
	a := make([]int, 0, 2)
	// len=0 cap=2

	a = append(a, 1)
	// a = [1] len=1 cap=2

	a = append(a, 2)
	// a = [1 2] len=2 cap=2 (ёмкость заполнена)

	b := append(a, 3)
	/*
		Добавляем третий элемент, но cap=2 и len=2.
		Значит append создаёт новый слайс "b" в новом месте памяти.
	*/

	/*
		Теперь:
		a = [1 2]
		b = [1 2 3]
	*/

	fmt.Println(a) // [1 2]
	fmt.Println(b) // [1 2 3]
}

Почему так произошло?
Ёмкости не хватило, поэтому append создал новый слайс для b.


Задача 5

Условие

// Почему слайс не был очищен?
func modify(a []int) {
	a = a[:0]
}

func main() {
	sl := []int{1, 2, 3}
	modify(sl)
	fmt.Println(sl)
}

Решение + пояснения

package main

import "fmt"

func modify(a []int) {
	a = a[:0]
	/*
		Мы сделали длину "a" равной 0, но только внутри функции.
		Мы НЕ меняли переменную sl в main.
		Мы просто присвоили новое значение локальной переменной "a".
	*/
}

func main() {
	sl := []int{1, 2, 3}
	// sl = [1 2 3]

	modify(sl)

	fmt.Println(sl) // [1 2 3]
}

Почему так произошло?
Потому что в Go аргументы функции передаются как значения.
Мы изменили локальную переменную a, но переменная sl в main осталась прежней.


Задача 6

Условие

// Какой будет результат?
func main() {
	a := []int{1, 2, 3}
	b := a[:2]
	c := a[1:]

	b[1] = 100
	c[0] = 200

	fmt.Println(a)
}

Решение + пояснения

package main

import "fmt"

func main() {
	a := []int{1, 2, 3}
	// a = [1 2 3]

	b := a[:2]
	// b берёт элементы a[0] и a[1]
	// b = [1 2]

	c := a[1:]
	// c берёт элементы a[1] и a[2]
	// c = [2 3]

	b[1] = 100
	/*
		b[1] - это второй элемент b
		НО он указывает на a[1]
		Значит теперь:
		a = [1 100 3]
	*/

	c[0] = 200
	/*
		c[0] - это первый элемент c
		НО он тоже указывает на a[1]
		Поэтому мы перезаписываем a[1] ещё раз:

		a = [1 200 3]
	*/

	fmt.Println(a) // [1 200 3]
}

Почему так произошло?
Потому что b и c - это разные слайсы, но они ссылаются на одни и те же данные.b[1] иc[0] указывают на один и тот же элемент a[1], поэтому второе присваивание перезаписывает первое.