> For the complete documentation index, see [llms.txt](https://eda-1.gitbook.io/lgwt/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://eda-1.gitbook.io/lgwt/osnovy-go/arrays-and-slices.md).

# Массивы и срезы

[**Весь код для этой главы вы найдете здесь**](https://github.com/quii/learn-go-with-tests/tree/main/arrays)

Массивы позволяют хранить несколько элементов одного типа в переменной в определённом порядке.

Когда у вас есть массивы, очень часто приходится выполнять итерацию по ним. Поэтому давайте используем [наши новые знания о `for`](/lgwt/osnovy-go/iteration.md), чтобы создать функцию `Sum`. `Sum` будет принимать массив чисел и возвращать их сумму.

Используем наши навыки TDD.

## Сначала напишите тест

Создайте новую папку для работы. Создайте новый файл с именем `sum_test.go` и вставьте следующее:

```go
package main

import "testing"

func TestSum(t *testing.T) {

	numbers := [5]int{1, 2, 3, 4, 5}

	got := Sum(numbers)
	want := 15

	if got != want {
		t.Errorf("got %d want %d given, %v", got, want, numbers)
	}
}
```

Массивы имеют *фиксированную ёмкость (capacity)*, которую вы определяете при объявлении переменной. Мы можем инициализировать массив двумя способами:

* \[N]type{value1, value2, ..., valueN} например, `numbers := [5]int{1, 2, 3, 4, 5}`
* \[...]type{value1, value2, ..., valueN} например, `numbers := [...]int{1, 2, 3, 4, 5}`

Иногда полезно также выводить входные данные функции в сообщении об ошибке. Здесь мы используем заполнитель `%v` для вывода в "стандартном" формате, который хорошо подходит для массивов.

[Подробнее о строках форматирования](https://golang.org/pkg/fmt/)

## Попробуйте запустить тест

Если вы инициализировали `go mod` с помощью `go mod init main`, вы получите ошибку `_testmain.go:13:2: cannot import "main"`. Это связано с тем, что, согласно общепринятой практике, пакет `main` будет содержать только интеграцию других пакетов, а не код, поддающийся модульному тестированию, и, следовательно, Go не позволит вам импортировать пакет с именем `main`.

Чтобы исправить это, вы можете переименовать основной модуль в `go.mod` на любое другое имя.

После устранения вышеуказанной ошибки, если вы запустите `go test`, компилятор выдаст знакомую ошибку `./sum_test.go:10:15: undefined: Sum`. Теперь мы можем приступить к написанию фактического метода для тестирования.

## Напишите минимальный объем кода, чтобы тест запустился, и проверьте вывод ошибочного теста

В `sum.go`:

```go
package main

func Sum(numbers [5]int) int {
	return 0
}
```

Ваш тест теперь должен завершиться неудачно с *чётким сообщением об ошибке*:

`sum_test.go:13: got 0 want 15 given, [1 2 3 4 5]`

## Напишите достаточно кода, чтобы тест прошёл

```go
func Sum(numbers [5]int) int {
	sum := 0
	for i := 0; i < 5; i++ {
		sum += numbers[i]
	}
	return sum
}
```

Чтобы получить значение из массива по определенному индексу, просто используйте синтаксис `array[index]`. В этом случае мы используем `for` для итерации 5 раз, чтобы пройти по массиву и добавить каждый элемент к `sum`.

## Рефакторинг

Давайте используем [`range`](https://gobyexample.com/range), чтобы улучшить наш код.

```go
func Sum(numbers [5]int) int {
	sum := 0
	for _, number := range numbers {
		sum += number
	}
	return sum
}
```

`range` позволяет вам итерировать по массиву. На каждой итерации `range` возвращает два значения — индекс и значение. Мы решили игнорировать значение индекса, используя `_` [пустой идентификатор (blank identifier)](https://golang.org/doc/effective_go.html#blank).

### Массивы и их тип

Интересной особенностью массивов является то, что их размер закодирован в их типе. Если вы попытаетесь передать `[4]int` в функцию, которая ожидает `[5]int`, она не скомпилируется. Это разные типы, так что это то же самое, что пытаться передать `string` в функцию, которая хочет `int`.

Вы можете подумать, что довольно громоздко, что массивы имеют фиксированную длину, и большую часть времени вы, вероятно, не будете их использовать!

В Go есть *срезы* (slices), которые не кодируют размер коллекции и вместо этого могут иметь любой размер.

Следующее требование будет заключаться в суммировании коллекций различного размера.

## Сначала напишите тест

Теперь мы будем использовать [тип среза (slice)](https://golang.org/doc/effective_go.html#slices), который позволяет нам иметь коллекции любого размера. Синтаксис очень похож на массивы, вы просто опускаете размер при их объявлении:

`mySlice := []int{1,2,3}` вместо `myArray := [3]int{1,2,3}`

```go
func TestSum(t *testing.T) {

	t.Run("collection of 5 numbers", func(t *testing.T) {
		numbers := [5]int{1, 2, 3, 4, 5}

		got := Sum(numbers)
		want := 15

		if got != want {
			t.Errorf("got %d want %d given, %v", got, want, numbers)
		}
	})

	t.Run("collection of any size", func(t *testing.T) {
		numbers := []int{1, 2, 3}

		got := Sum(numbers)
		want := 6

		if got != want {
			t.Errorf("got %d want %d given, %v", got, want, numbers)
		}
	})

}
```

## Попробуйте запустить тест

Это не компилируется:

`./sum_test.go:22:13: cannot use numbers (type []int) as type [5]int in argument to Sum`

## Напишите минимальный объем кода, чтобы тест запустился, и проверьте вывод ошибочного теста

Проблема здесь в том, что мы можем либо:

* Нарушить существующий API, изменив аргумент `Sum` на срез вместо массива. Когда мы это сделаем, мы потенциально испортим кому-то день, потому что наш *другой* тест больше не будет компилироваться!
* Создать новую функцию.

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

```go
func Sum(numbers []int) int {
	sum := 0
	for _, number := range numbers {
		sum += number
	}
	return sum
}
```

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

## Напишите достаточно кода, чтобы тест прошел

Оказывается, устранение проблем с компилятором — это всё, что нам нужно было сделать, и тесты прошли!

## Рефакторинг

Мы уже сделали рефакторинг `Sum` — всё, что мы сделали, это заменили массивы срезами, поэтому никаких дополнительных изменений не требуется. Помните, что мы не должны пренебрегать нашим тестовым кодом на стадии рефакторинга — мы можем еще улучшить наши тесты `Sum`.

```go
func TestSum(t *testing.T) {

	t.Run("collection of 5 numbers", func(t *testing.T) {
		numbers := []int{1, 2, 3, 4, 5}

		got := Sum(numbers)
		want := 15

		if got != want {
			t.Errorf("got %d want %d given, %v", got, want, numbers)
		}
	})

	t.Run("collection of any size", func(t *testing.T) {
		numbers := []int{1, 2, 3}

		got := Sum(numbers)
		want := 6

		if got != want {
			t.Errorf("got %d want %d given, %v", got, want, numbers)
		}
	})

}
```

Важно подвергать сомнению ценность ваших тестов. Целью должно быть не максимальное количество тестов, а максимальная *уверенность* в вашей кодовой базе. Слишком много тестов может стать реальной проблемой и просто добавляет больше затрат на поддержку. **Каждый тест имеет свою цену**.

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

Встроенный инструментарий тестирования Go включает [инструмент покрытия кода](https://blog.golang.org/cover). Хотя стремление к 100% покрытию не должно быть вашей конечной целью, инструмент покрытия может помочь выявить области вашего кода, не охваченные тестами. Если вы строго придерживались TDD, вполне вероятно, что у вас и так будет около 100% покрытия.

Попробуйте запустить:

`go test -cover`

Вы должны увидеть:

```bash
PASS
coverage: 100.0% of statements
```

Теперь удалите один из тестов и снова проверьте покрытие.

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

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

Например:

`SumAll([]int{1,2}, []int{0,9})` вернёт `[]int{3, 9}`

или

`SumAll([]int{1,1,1})` вернёт `[]int{3}`

## Сначала напишите тест

```go
func TestSumAll(t *testing.T) {

	got := SumAll([]int{1, 2}, []int{0, 9})
	want := []int{3, 9}

	if got != want {
		t.Errorf("got %v want %v", got, want)
	}
}
```

## Попробуйте запустить тест

`./sum_test.go:23:9: undefined: SumAll`

## Напишите минимальный объем кода, чтобы тест запустился, и проверьте вывод ошибочного теста

Нам нужно определить `SumAll` в соответствии с тем, что требует наш тест.

Go позволяет писать [*вариативные функции*](https://gobyexample.com/variadic-functions), которые могут принимать переменное количество аргументов.

```go
func SumAll(numbersToSum ...[]int) []int {
	return nil
}
```

Это допустимо, но наши тесты все равно не скомпилируются!

`./sum_test.go:26:9: invalid operation: got != want (slice can only be compared to nil)`

Go не позволяет использовать операторы равенства со срезами. Вы *могли бы* написать функцию для итерации по каждому срезу `got` и `want` и проверки их значений, но что, если бы у нас был более удобный способ сделать это?

Начиная с Go 1.21, доступен стандартный пакет [slices](https://pkg.go.dev/slices#pkg-overview), который содержит функцию [slices.Equal](https://pkg.go.dev/slices#Equal) для простого поверхностного сравнения срезов, где вам не нужно беспокоиться о типах, как в вышеуказанном случае. Обратите внимание, что эта функция ожидает, что элементы будут [сравнимыми (comparable)](https://pkg.go.dev/builtin#comparable). Таким образом, ее нельзя применять к срезам с несравнимыми элементами, например, к двумерным срезам.

Давайте применим это на практике!

```go
func TestSumAll(t *testing.T) {

	got := SumAll([]int{1, 2}, []int{0, 9})
	want := []int{3, 9}

	if !slices.Equal(got, want) {
		t.Errorf("got %v want %v", got, want)
	}
}
```

Вы должны получить вывод теста примерно такой: `sum_test.go:30: got [] want [3 9]`

## Напишите достаточно кода, чтобы тест прошел

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

```go
func SumAll(numbersToSum ...[]int) []int {
	lengthOfNumbers := len(numbersToSum)
	sums := make([]int, lengthOfNumbers)

	for i, numbers := range numbersToSum {
		sums[i] = Sum(numbers)
	}

	return sums
}
```

Много нового для изучения!

Появился новый способ создания среза. `make` позволяет создать срез с начальной ёмкостью, равной `len` от `numbersToSum`, которые нам нужно обработать. Длина среза — это количество элементов, которые он содержит `len(mySlice)`, в то время как ёмкость — это количество элементов, которое он может содержать в базовом массиве `cap(mySlice)`, например, `make([]int, 0, 5)` создает срез с длиной 0 и ёмкостью 5.

Вы можете индексировать срезы, как массивы, с помощью `mySlice[N]`, чтобы получить значение, или присвоить ему новое значение с помощью `=`.

Тесты теперь должны пройти.

## Рефакторинг

Как упоминалось, срезы имеют ёмкость. Если у вас есть срез ёмкостью 2 и вы попытаетесь сделать `mySlice[10] = 1`, вы получите *ошибку времени выполнения*.

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

```go
func SumAll(numbersToSum ...[]int) []int {
	var sums []int
	for _, numbers := range numbersToSum {
		sums = append(sums, Sum(numbers))
	}

	return sums
}
```

В этой реализации мы меньше беспокоимся о ёмкости. Мы начинаем с пустого среза `sums` и добавляем (append) к нему результат `Sum` по мере обработки вариативных аргументов.

Наше следующее требование — изменить `SumAll` на `SumAllTails`, где она будет вычислять суммы "хвостов" каждого среза. Хвост коллекции — это все элементы коллекции, кроме первого (ее "головы").

## Сначала напишите тест

```go
func TestSumAllTails(t *testing.T) {
	got := SumAllTails([]int{1, 2}, []int{0, 9})
	want := []int{2, 9}

	if !slices.Equal(got, want) {
		t.Errorf("got %v want %v", got, want)
	}
}
```

## Попробуйте запустить тест

`./sum_test.go:26:9: undefined: SumAllTails`

## Напишите минимальный объем кода, чтобы тест запустился, и проверьте вывод ошибочного теста

Переименуйте функцию в `SumAllTails` и перезапустите тест.

`sum_test.go:30: got [3 9] want [2 9]`

## Напишите достаточно кода, чтобы тест прошел

```go
func SumAllTails(numbersToSum ...[]int) []int {
	var sums []int
	for _, numbers := range numbersToSum {
		tail := numbers[1:]
		sums = append(sums, Sum(tail))
	}

	return sums
}
```

Срезы можно срезать! Синтаксис `slice[low:high]`. Если вы опускаете значение с одной из сторон двоеточия `:`, оно захватывает все до этой стороны. В нашем случае мы говорим "взять от 1 до конца" с помощью `numbers[1:]`. Возможно, вы захотите потратить некоторое время на написание других тестов для срезов и поэкспериментировать с оператором среза, чтобы лучше с ним ознакомиться.

## Рефакторинг

В этот раз особо нечего рефакторить.

Как вы думаете, что произойдет, если вы передадите пустой срез в нашу функцию? Что такое "хвост" пустого среза? Что произойдет, когда вы скажете Go захватить все элементы из `myEmptySlice[1:]`?

## Сначала напишите тест

```go
func TestSumAllTails(t *testing.T) {

	t.Run("make the sums of some slices", func(t *testing.T) {
		got := SumAllTails([]int{1, 2}, []int{0, 9})
		want := []int{2, 9}

		if !slices.Equal(got, want) {
			t.Errorf("got %v want %v", got, want)
		}
	})

	t.Run("safely sum empty slices", func(t *testing.T) {
		got := SumAllTails([]int{}, []int{3, 4, 5})
		want := []int{0, 9}

		if !slices.Equal(got, want) {
			t.Errorf("got %v want %v", got, want)
		}
	})

}
```

## Попробуйте запустить тест

```
panic: runtime error: slice bounds out of range [recovered]
    panic: runtime error: slice bounds out of range
```

О нет! Важно отметить, что хотя тест *скомпилировался*, он *имеет ошибку времени выполнения*.

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

## Напишите достаточно кода, чтобы тест прошел

```go
func SumAllTails(numbersToSum ...[]int) []int {
	var sums []int
	for _, numbers := range numbersToSum {
		if len(numbers) == 0 {
			sums = append(sums, 0)
		} else {
			tail := numbers[1:]
			sums = append(sums, Sum(tail))
		}
	}

	return sums
}
```

## Рефакторинг

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

```go
func TestSumAllTails(t *testing.T) {

	checkSums := func(t *testing.T, got, want []int) {
		t.Helper()
		if !slices.Equal(got, want) {
			t.Errorf("got %v want %v", got, want)
		}
	}

	t.Run("make the sums of tails of", func(t *testing.T) {
		got := SumAllTails([]int{1, 2}, []int{0, 9})
		want := []int{2, 9}
		checkSums(t, got, want)
	})

	t.Run("safely sum empty slices", func(t *testing.T) {
		got := SumAllTails([]int{}, []int{3, 4, 5})
		want := []int{0, 9}
		checkSums(t, got, want)
	})

}
```

Мы могли бы создать новую функцию `checkSums` как обычно, но в этом случае мы показываем новую технику: присвоение функции переменной. Это может выглядеть странно, но это ничем не отличается от присвоения переменной `string` или `int`; функции по сути тоже являются значениями.

Здесь это не показано, но эта техника может быть полезна, когда вы хотите связать функцию с другими локальными переменными в "области видимости" (например, между некоторыми `{}`). Это также позволяет уменьшить "площадь поверхности" вашего API.

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

Удобный побочный эффект этого заключается в том, что это добавляет небольшую типобезопасность в наш код. Если разработчик по ошибке добавит новый тест с `checkSums(t, got, "dave")`, компилятор остановит его.

```bash
$ go test
./sum_test.go:52:21: cannot use "dave" (type string) as type []int in argument to checkSums
```

## Подведение итогов

Мы рассмотрели:

* Массивы
* Срезы
  * Различные способы их создания
  * Как они имеют *фиксированную* ёмкость, но вы можете создавать новые срезы из старых с помощью `append`
  * Как выполнять срезы срезов!
* `len` для получения длины массива или среза
* Инструмент покрытия тестов
* `slices.Equal` и почему он нужен вместо обычных операторов равенства

Мы использовали срезы и массивы с целыми числами, но они работают и с любыми другими типами, включая сами массивы/срезы. Таким образом, вы можете объявить переменную типа `[][]string`, если вам это необходимо.

[Посмотрите статью в блоге Go о срезах](https://blog.golang.org/go-slices-usage-and-internals) для углубленного изучения срезов. Попробуйте написать больше тестов, чтобы закрепить то, что вы узнали из нее.

Еще один удобный способ экспериментировать с Go, помимо написания тестов, — это Go playground. Вы можете попробовать большинство вещей, и вы легко можете поделиться своим кодом, если вам нужно задать вопросы. [Я создал Go playground со срезом, чтобы вы могли с ним поэкспериментировать.](https://play.golang.org/p/ICCWcRGIO68)

[Вот пример](https://play.golang.org/p/bTrRmYfNYCp) того, как нарезка массива и как изменение среза влияет на исходный массив; но "копия" среза не повлияет на исходный массив. [Еще один пример](https://play.golang.org/p/Poth8JS28sc) того, почему хорошей идеей является создание копии среза после нарезки очень большого среза.

***


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://eda-1.gitbook.io/lgwt/osnovy-go/arrays-and-slices.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
