> 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/structs-methods-and-interfaces.md).

# Структуры, методы и интерфейсы

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

Предположим, нам нужен геометрический код для вычисления периметра прямоугольника по заданной высоте и ширине. Мы можем написать функцию `Perimeter(width float64, height float64)`, где `float64` предназначен для чисел с плавающей запятой, таких как `123.45`.

Цикл TDD должен быть вам уже достаточно знаком.

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

```go
func TestPerimeter(t *testing.T) {
	got := Perimeter(10.0, 10.0)
	want := 40.0

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

Заметили новый формат вывода? `f` предназначен для нашего `float64`, а `.2` означает печать двух знаков после запятой.

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

`./shapes_test.go:6:9: undefined: Perimeter`

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

```go
func Perimeter(width float64, height float64) float64 {
	return 0
}
```

В результате получаем `shapes_test.go:10: got 0.00 want 40.00`.

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

```go
func Perimeter(width float64, height float64) float64 {
	return 2 * (width + height)
}
```

Пока всё легко. Теперь давайте создадим функцию `Area(width, height float64)`, которая возвращает площадь прямоугольника.

Попробуйте сделать это самостоятельно, следуя циклу TDD.

В итоге у вас должны получиться тесты, похожие на эти:

```go
func TestPerimeter(t *testing.T) {
	got := Perimeter(10.0, 10.0)
	want := 40.0

	if got != want {
		t.Errorf("got %.2f want %.2f", got, want)
	}
}

func TestArea(t *testing.T) {
	got := Area(12.0, 6.0)
	want := 72.0

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

И код, похожий на этот:

```go
func Perimeter(width float64, height float64) float64 {
	return 2 * (width + height)
}

func Area(width float64, height float64) float64 {
	return width * height
}
```

Вы могли слышать, что сравнивать числа с плавающей запятой с помощью `!=`/`==` — плохая идея, из-за того, как они представлены в памяти:

```go
func Example_floatComparison() {
	fmt.Println(0.1+0.2 == 0.3)

	var a, b, c float64 = 0.1, 0.2, 0.3
	fmt.Println(a+b == c)

	// Output:
	// true
	// false
}
```

Второе сравнение равно `false`, потому что `0.1` и `0.2` не могут быть представлены точно как `float64`, поэтому их сложение также не даёт ровно `0.3`. Первое же сравнение равно `true` только потому, что оно записано как литеральное выражение — Go вычисляет их с произвольной точностью, а не `float64`, пока они не будут присвоены чему-либо.

Однако эта неточность не относится к нашему тесту `Area` выше: каждое используемое нами значение (`12.0`, `6.0`, `72.0` и так далее) является целым числом, которое `float64` *может* представить точно, и умножение двух точно представленных чисел, пока результат находится в диапазоне, даёт ещё один точно представимый результат. Точное сравнение становится небезопасным только тогда, когда вы вводите значения или вычисления, которые не могут быть представлены точно, например `0.1` или результаты деления. Если вы обнаружите, что пишете подобные тесты, используйте сравнение с допустимым допуском, например [`cmp.Diff` с `cmpopts.EquateApprox`](https://pkg.go.dev/github.com/google/go-cmp/cmp/cmpopts#EquateApprox) из `go-cmp`.

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

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

Мы могли бы просто дать функциям более конкретные имена, например `RectangleArea`. Более элегантное решение — определить свой собственный *тип* под названием `Rectangle`, который инкапсулирует это понятие для нас.

Мы можем создать простой тип, используя **структуру**. [Структура](https://golang.org/ref/spec#Struct_types) — это просто именованная коллекция полей, где вы можете хранить данные.

Объявите структуру в вашем файле `shapes.go` следующим образом:

```go
type Rectangle struct {
	Width  float64
	Height float64
}
```

Теперь давайте реорганизуем тесты, чтобы использовать `Rectangle` вместо обычных `float64`.

```go
func TestPerimeter(t *testing.T) {
	rectangle := Rectangle{10.0, 10.0}
	got := Perimeter(rectangle)
	want := 40.0

	if got != want {
		t.Errorf("got %.2f want %.2f", got, want)
	}
}

func TestArea(t *testing.T) {
	rectangle := Rectangle{12.0, 6.0}
	got := Area(rectangle)
	want := 72.0

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

Не забудьте запустить тесты, прежде чем пытаться исправить ошибки. Тесты должны показать полезную ошибку, например:

```
./shapes_test.go:7:18: not enough arguments in call to Perimeter
    have (Rectangle)
    want (float64, float64)
```

Вы можете получить доступ к полям структуры с помощью синтаксиса `myStruct.field`.

Измените две функции, чтобы исправить тест.

```go
func Perimeter(rectangle Rectangle) float64 {
	return 2 * (rectangle.Width + rectangle.Height)
}

func Area(rectangle Rectangle) float64 {
	return rectangle.Width * rectangle.Height
}
```

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

Наше следующее требование — написать функцию `Area` для кругов.

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

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

	t.Run("rectangles", func(t *testing.T) {
		rectangle := Rectangle{12, 6}
		got := Area(rectangle)
		want := 72.0

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

	t.Run("circles", func(t *testing.T) {
		circle := Circle{10}
		got := Area(circle)
		want := 314.1592653589793

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

}
```

Как видите, `f` был заменен на `g`, и это имеет вескую причину. Использование `g` выведет более точное десятичное число в сообщении об ошибке ([опции форматирования fmt](https://golang.org/pkg/fmt/)). Например, при использовании радиуса 1.5 в расчете площади круга `f` покажет `7.068583`, тогда как `g` покажет `7.0685834705770345`.

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

`./shapes_test.go:28:13: undefined: Circle`

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

Нам нужно определить наш тип `Circle`.

```go
type Circle struct {
	Radius float64
}
```

Теперь попробуйте снова запустить тесты:

`./shapes_test.go:29:14: cannot use circle (type Circle) as type Rectangle in argument to Area`

Некоторые языки программирования позволяют делать что-то подобное:

```go
func Area(circle Circle) float64       {}
func Area(rectangle Rectangle) float64 {}
```

Но в Go так нельзя:

`./shapes.go:20:32: Area redeclared in this block`

У нас есть два варианта:

* Вы можете объявлять функции с одинаковым именем в разных *пакетах*. Таким образом, мы могли бы создать нашу `Area(Circle)` в новом пакете, но здесь это кажется излишним.
* Вместо этого мы можем определить [*методы*](https://golang.org/ref/spec#Method_declarations) для наших недавно определённых типов.

### Что такое методы?

До сих пор мы писали только *функции*, но использовали некоторые методы. Когда мы вызываем `t.Errorf`, мы вызываем метод `Errorf` на экземпляре нашего `t` (`testing.T`).

Метод — это функция с получателем (receiver). Объявление метода связывает идентификатор, имя метода, с методом и ассоциирует метод с базовым типом получателя.

Методы очень похожи на функции, но они вызываются путём их обращения к экземпляру определённого типа. Если вы можете вызывать функции где угодно, например `Area(rectangle)`, то методы можно вызывать только на "объектах".

Пример поможет, поэтому давайте сначала изменим наши тесты, чтобы они вызывали методы, а затем исправим код.

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

	t.Run("rectangles", func(t *testing.T) {
		rectangle := Rectangle{12, 6}
		got := rectangle.Area()
		want := 72.0

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

	t.Run("circles", func(t *testing.T) {
		circle := Circle{10}
		got := circle.Area()
		want := 314.1592653589793

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

}
```

Если мы попробуем запустить тесты, то получим:

```
./shapes_test.go:19:19: rectangle.Area undefined (type Rectangle has no field or method Area)
./shapes_test.go:29:16: circle.Area undefined (type Circle has no field or method Area)
```

> type Circle has no field or method Area

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

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

Давайте добавим несколько методов к нашим типам:

```go
type Rectangle struct {
	Width  float64
	Height float64
}

func (r Rectangle) Area() float64 {
	return 0
}

type Circle struct {
	Radius float64
}

func (c Circle) Area() float64 {
	return 0
}
```

Синтаксис объявления методов почти такой же, как и функций, потому что они очень похожи. Единственное отличие — это синтаксис получателя метода `func (receiverName ReceiverType) MethodName(args)`.

Когда ваш метод вызывается на переменной этого типа, вы получаете ссылку на её данные через переменную `receiverName`. Во многих других языках программирования это делается неявно, и вы обращаетесь к получателю через `this`.

В Go принято, чтобы переменная-получатель была первой буквой типа.

```
r Rectangle
```

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

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

Теперь давайте добьёмся прохождения тестов для прямоугольника, исправив наш новый метод:

```go
func (r Rectangle) Area() float64 {
	return r.Width * r.Height
}
```

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

Чтобы функция `Area` для круга прошла тест, мы воспользуемся константой `Pi` из пакета `math` (не забудьте импортировать его).

```go
func (c Circle) Area() float64 {
	return math.Pi * c.Radius * c.Radius
}
```

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

В наших тестах есть некоторое дублирование.

Всё, что мы хотим сделать, это взять коллекцию *фигур*, вызвать на них метод `Area()` и затем проверить результат.

Мы хотим иметь возможность написать некую функцию `checkArea`, которой мы можем передавать как `Rectangle`, так и `Circle`, но которая не будет компилироваться, если мы попытаемся передать что-то, что не является фигурой.

В Go мы можем выразить это намерение с помощью **интерфейсов**.

[Интерфейсы](https://golang.org/ref/spec#Interface_types) — это очень мощная концепция в статически типизированных языках, таких как Go, потому что они позволяют создавать функции, которые могут использоваться с различными типами, и писать сильно декомпозированный код, сохраняя при этом типобезопасность.

Давайте внедрим это, рефакторизируя наши тесты.

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

	checkArea := func(t testing.TB, shape Shape, want float64) {
		t.Helper()
		got := shape.Area()
		if got != want {
			t.Errorf("got %g want %g", got, want)
		}
	}

	t.Run("rectangles", func(t *testing.T) {
		rectangle := Rectangle{12, 6}
		checkArea(t, rectangle, 72.0)
	})

	t.Run("circles", func(t *testing.T) {
		circle := Circle{10}
		checkArea(t, circle, 314.1592653589793)
	})

}
```

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

Как что-то становится фигурой? Мы просто сообщаем Go, что такое `Shape`, используя объявление интерфейса:

```go
type Shape interface {
	Area() float64
}
```

Мы создаём новый `type` так же, как мы делали с `Rectangle` и `Circle`, но на этот раз это `interface`, а не `struct`.

Как только вы добавите это в код, тесты пройдут.

### Погодите, что?

Это довольно сильно отличается от интерфейсов в большинстве других языков программирования. Обычно вы должны писать код, чтобы сказать `My type Foo implements interface Bar`.

Но в нашем случае:

* `Rectangle` имеет метод `Area`, который возвращает `float64`, поэтому он удовлетворяет интерфейсу `Shape`.
* `Circle` имеет метод `Area`, который возвращает `float64`, поэтому он удовлетворяет интерфейсу `Shape`.
* `string` не имеет такого метода, поэтому он не удовлетворяет интерфейсу.
* и т.д.

В Go **разрешение интерфейсов неявно**. Если переданный вами тип соответствует тому, что запрашивает интерфейс, он скомпилируется.

### Разделение (связанности)

Обратите внимание, что нашему помощнику не нужно беспокоиться о том, является ли фигура `Rectangle`, `Circle` или `Triangle`. Объявляя интерфейс, помощник *разделена* с конкретными типами и имеет только тот метод, который ему нужен для выполнения своей работы.

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

## Дальнейший рефакторинг

Теперь, когда у вас есть некоторое понимание структур, мы можем представить "табличные тесты".

[Табличные тесты](https://go.dev/wiki/TableDrivenTests) полезны, когда вы хотите создать список тестовых случаев, которые можно проверять одним и тем же способом.

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

	areaTests := []struct {
		shape Shape
		want  float64
	}{
		{Rectangle{12, 6}, 72.0},
		{Circle{10}, 314.1592653589793},
	}

	for _, tt := range areaTests {
		got := tt.shape.Area()
		if got != tt.want {
			t.Errorf("got %g want %g", got, tt.want)
		}
	}

}
```

Единственный новый синтаксис здесь — это создание "анонимной структуры" `areaTests`. Мы объявляем срез структур, используя `[]struct` с двумя полями: `shape` и `want`. Затем мы заполняем срез тестовыми случаями.

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

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

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

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

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

Добавить новый тест для нашей новой фигуры очень просто. Просто добавьте `{Triangle{12, 6}, 36.0},` в наш список.

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

	areaTests := []struct {
		shape Shape
		want  float64
	}{
		{Rectangle{12, 6}, 72.0},
		{Circle{10}, 314.1592653589793},
		{Triangle{12, 6}, 36.0},
	}

	for _, tt := range areaTests {
		got := tt.shape.Area()
		if got != tt.want {
			t.Errorf("got %g want %g", got, tt.want)
		}
	}

}
```

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

Помните, продолжайте пытаться запускать тест и позвольте компилятору направлять вас к решению.

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

`./shapes_test.go:25:4: undefined: Triangle`

Мы ещё не определили `Triangle`:

```go
type Triangle struct {
	Base   float64
	Height float64
}
```

Попробуйте снова:

```
./shapes_test.go:25:8: cannot use Triangle literal (type Triangle) as type Shape in field value:
    Triangle does not implement Shape (missing Area method)
```

Он говорит нам, что мы не можем использовать `Triangle` как фигуру, потому что у неё нет метода `Area()`, поэтому добавьте пустую реализацию, чтобы тест заработал:

```go
func (t Triangle) Area() float64 {
	return 0
}
```

Наконец, код компилируется, и мы получаем нашу ошибку:

`shapes_test.go:31: got 0.00 want 36.00`

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

```go
func (t Triangle) Area() float64 {
	return (t.Base * t.Height) * 0.5
}
```

И наши тесты проходят!

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

Опять же, реализация хороша, но наши тесты можно улучшить.

Когда вы просматриваете это:

```
{Rectangle{12, 6}, 72.0},
{Circle{10}, 314.1592653589793},
{Triangle{12, 6}, 36.0},
```

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

До сих пор вам показывали только синтаксис создания экземпляров структур `MyStruct{val1, val2}`, но вы можете по желанию называть поля.

Давайте посмотрим, как это выглядит:

```
        {shape: Rectangle{Width: 12, Height: 6}, want: 72.0},
        {shape: Circle{Radius: 10}, want: 314.1592653589793},
        {shape: Triangle{Base: 12, Height: 6}, want: 36.0},
```

В [Test-Driven Development by Example](https://g.co/kgs/yCzDLF) Кент Бек рефакторизирует некоторые тесты до определённого момента и утверждает:

> Тест говорит нам яснее, как будто это утверждение истины, **а не последовательность операций**

(акцент в цитате мой)

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

## Убедитесь, что вывод ваших тестов информативен

Помните, ранее, когда мы реализовывали `Triangle` и у нас был падающий тест? Он выводил `shapes_test.go:31: got 0.00 want 36.00`.

Мы знали, что это связано с `Triangle`, потому что мы только что работали с ним. Но что, если ошибка прокралась в систему в одном из 20 случаев в таблице? Как разработчик узнает, какой случай не прошёл? Это не очень удобно для разработчика, ему придётся вручную просматривать случаи, чтобы выяснить, какой из них на самом деле не прошёл.

Мы можем изменить наше сообщение об ошибке на `%#v got %g want %g`. Формат вывода `%#v` напечатает нашу структуру со значениями её полей, так что разработчик сможет сразу увидеть свойства, которые тестируются.

Чтобы ещё больше повысить читабельность наших тестовых случаев, мы можем переименовать поле `want` во что-то более описательное, например `hasArea`.

Ещё один последний совет по табличным тестам — использовать `t.Run` и давать имена тестовым случаям.

Оборачивая каждый случай в `t.Run`, вы получите более чёткий вывод тестов при сбоях, так как он будет печатать имя случая:

```
--- FAIL: TestArea (0.00s)
    --- FAIL: TestArea/Rectangle (0.00s)
        shapes_test.go:33: main.Rectangle{Width:12, Height:6} got 72.00 want 72.10
```

И вы можете запускать конкретные тесты в вашей таблице с помощью `go test -run TestArea/Rectangle`.

Вот наш окончательный код теста, который это реализует:

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

	areaTests := []struct {
		name    string
		shape   Shape
		hasArea float64
	}{
		{name: "Rectangle", shape: Rectangle{Width: 12, Height: 6}, hasArea: 72.0},
		{name: "Circle", shape: Circle{Radius: 10}, hasArea: 314.1592653589793},
		{name: "Triangle", shape: Triangle{Base: 12, Height: 6}, hasArea: 36.0},
	}

	for _, tt := range areaTests {
		// using tt.name from the case to use it as the `t.Run` test name
		t.Run(tt.name, func(t *testing.T) {
			got := tt.shape.Area()
			if got != tt.hasArea {
				t.Errorf("%#v got %g want %g", tt.shape, got, tt.hasArea)
			}
		})

	}

}
```

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

Это была ещё одна практика TDD, итеративное решение наших базовых математических задач и изучение новых возможностей языка, мотивированное нашими тестами.

* Объявление структур для создания собственных типов данных, что позволяет связывать связанные данные вместе и делать намерение вашего кода более ясным.
* Объявление интерфейсов, чтобы вы могли определять функции, которые могут использоваться различными типами ([ad hoc полиморфизм](https://en.wikipedia.org/wiki/Ad_hoc_polymorphism)).
* Добавление методов, чтобы вы могли добавлять функциональность к своим типам данных и реализовывать интерфейсы.
* Табличные тесты, чтобы сделать ваши утверждения более ясными, а ваши наборы тестов более простыми для расширения и поддержки.

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

Интерфейсы — отличный инструмент для скрытия сложности от других частей системы. В нашем случае наш вспомогательный *код* не нуждался в знании точной формы, которую он проверял, только в том, как "спросить" её площадь.

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


---

# 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/structs-methods-and-interfaces.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.
