---
title: "Структуры и методы"
description: "Пока мы хранили данные в отдельных переменных: имя в одной, возраст в другой, баланс в третьей. Но эти значения связаны — они описывают одного пользователя. Если передавать их в…"
canonical: https://graphlms.ru/go-basics/book/structs
course: "Основы Go"
updated: 2026-10-12
lang: ru
publisher: GraphLMS
---

# Структуры и методы

> Источник: https://graphlms.ru/go-basics/book/structs — курс «Основы Go», GraphLMS.

## О чём глава

Пока мы хранили данные в отдельных переменных: имя в одной, возраст в другой,
баланс в третьей. Но эти значения связаны — они описывают одного пользователя.
Если передавать их в функции по отдельности, рано или поздно кто-то перепутает
порядок аргументов. **Структура** (`struct`) собирает несколько полей в один
тип, чтобы про «пользователя» или «счёт» можно было говорить как про целое. В
главе об указателях вы уже видели `Account`; теперь разберём структуры как
следует.

В этой главе: объявление структур и создание значений, **методы**, главный
выбор новичка — получатель по значению или по указателю — и **встраивание**,
способ переиспользовать поведение без классов и наследования.

## Объявление структуры

Структура — это набор именованных полей, у каждого свой тип. Объявляют её через
`type ИмяТипа struct { ... }`:

```go
type Point struct {
    X int
    Y int
}
```

Теперь `Point` — полноценный тип, как `int` или `string`. Поля с одинаковым типом
можно перечислить в одну строку: `X, Y int`. Регистр первой буквы имени
имеет значение: поле с заглавной буквы видно из других пакетов, со строчной —
только внутри своего. Пока весь код в одном файле, разницы вы не заметите;
подробно правило разобрано в [главе о пакетах](/go-basics/book/packages-modules).

## Литералы: создаём значения

Есть два способа записать значение структуры. **По именам полей** — самый
читаемый и устойчивый к изменениям:

```go
p := Point{X: 10, Y: 20}
```

И **позиционный** — поля идут строго в порядке объявления:

```go
p := Point{10, 20}
```

Позиционный короче, но хрупкий: добавите новое поле в структуру — и все такие
литералы перестанут компилироваться или, хуже, молча перепутают значения.
Поэтому в коде почти всегда предпочитают форму по именам. К полям обращаются
через точку: `p.X`, `p.Y`. Если поле не указали в литерале, оно получает
**нулевое значение** своего типа (`0`, `""`, `nil` и т.д.) — отдельной
инициализации не требуется.

```go play
package main

import "fmt"

type Point struct {
	X int
	Y int
}

func main() {
	a := Point{X: 1, Y: 2} // по именам
	b := Point{3, 4}       // позиционно
	var c Point            // всё по нулям

	c.X = 5 // поле можно присвоить после создания

	fmt.Println(a)
	fmt.Println(b)
	fmt.Printf("%+v\n", c)
	fmt.Println(a == Point{X: 1, Y: 2})
}
```

Вывод:

```text
{1 2}
{3 4}
{X:5 Y:0}
true
```

`fmt.Println` печатает структуру в фигурных скобках, значения полей через пробел.
Глагол `%+v` добавляет имена полей — при отладке это удобнее. Обратите внимание
на `c`: мы не задавали `Y`, и он остался нулём.

Последняя строка показывает, что структуры можно сравнивать через `==`: две
структуры равны, если равны все их поля. Это работает, пока все поля
сравниваемые; если в структуре есть срез или карта, `==` не скомпилируется.

### Функция-конструктор

Когда у значения есть правила (имя не пустое, баланс не отрицательный), его
создают не литералом, а функцией с именем `NewТип`. Такой функции в Go нет
специального статуса, это просто соглашение. Вот конструктор для `Account` из
главы об указателях (`errors.New` создаёт ошибку, как в `safeDiv` из главы о
функциях):

```go
func NewAccount(owner string) (*Account, error) {
    if owner == "" {
        return nil, errors.New("пустое имя владельца")
    }
    return &Account{Owner: owner}, nil
}
```

Если же нулевое значение структуры уже пригодно к работе, как `var c Counter`
ниже, конструктор не нужен. В Go стараются проектировать типы именно так.

## Методы

**Метод** — это функция, привязанная к типу. От обычной функции его отличает
**получатель** (по-английски *receiver*) — особый параметр в скобках перед именем
метода. Внутри метода получатель работает как обычная переменная и даёт доступ к
полям.

```go play
package main

import "fmt"

type Rectangle struct {
	Width  int
	Height int
}

// describe — метод типа Rectangle.
// (r Rectangle) — это получатель.
func (r Rectangle) describe() string {
	return fmt.Sprintf("прямоугольник %dx%d, площадь %d",
		r.Width, r.Height, r.Width*r.Height)
}

func main() {
	box := Rectangle{Width: 4, Height: 3}
	fmt.Println(box.describe())
}
```

Вывод:

```
прямоугольник 4x3, площадь 12
```

Метод вызывают через точку: `box.describe()`, и Go подставляет `box` в
получатель `r`. `fmt.Sprintf`, напомним, работает как `Printf`, но не печатает,
а возвращает готовую строку.

Методы можно объявлять не только у структур, а у любого типа, объявленного в
вашем пакете. Например, `type Celsius float64` создаёт новый тип на основе
числа, и ему можно дать метод `func (c Celsius) ToF() float64`. Добавить метод
чужому типу, скажем `int` или `string`, нельзя.

## Получатель по значению или по указателю

На этом месте новички ошибаются чаще всего. Получатель можно объявить
**по значению** (`r Rectangle`) или **по указателю** (`r *Rectangle`). Разница
та же, что у обычных параметров из [главы об указателях](/go-basics/book/pointers):
метод получает либо **копию** значения, либо **адрес** оригинала.

Получатель по значению — это копия. Любые изменения полей внутри метода затронут
только копию и исчезнут после возврата. Смотрите:

```go play
package main

import "fmt"

type Counter struct {
	Value int
}

// получатель по значению — работает с копией
func (c Counter) incBroken() {
	c.Value++ // меняем копию, оригинал не тронут
}

// получатель по указателю — работает с оригиналом
func (c *Counter) inc() {
	c.Value++
}

func main() {
	c := Counter{}

	c.incBroken()
	fmt.Println("после incBroken:", c.Value)

	c.inc()
	c.inc()
	fmt.Println("после двух inc:", c.Value)
}
```

Вывод:

```
после incBroken: 0
после двух inc: 2
```

`incBroken` увеличил счётчик у копии — снаружи ничего не поменялось. `inc` через
указатель меняет ту самую структуру, поэтому два вызова дают `2`.

Обратите внимание: метод с получателем-указателем мы вызвали на обычной
переменной, `c.inc()`, без явного `&c`. Go сам берёт адрес переменной. На
значении, у которого нет адреса, например на результате функции
(`makeCounter().inc()`), такой вызов не скомпилируется.

### Как выбрать

Простое практическое правило:

- **Нужно менять поля** — берите **указатель**: `*T`.
- Структура **большая**, копировать дорого — тоже указатель, ради
  производительности.
- Иначе можно по значению — это проще для мелких типов вроде `Point`.
  Но копия структуры неглубокая: через поля-срезы, карты и указатели метод
  всё ещё может менять общие данные.

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

## Встраивание: композиция вместо наследования

В Go нет классов и наследования. Вместо «класс B наследует класс A» используют
**композицию**: одна структура содержит другую. Обычное вложенное поле выглядит
так:

```go
type Engine struct {
    Power int
}

type Car struct {
    engine Engine // обычное поле с именем
}
```

Здесь к мощности придётся обращаться длинно: `car.engine.Power`. Go предлагает
короче — **встраивание (embedding)**. Если написать тип без имени поля, его поля и
методы **продвигаются** наружу, как будто они принадлежат внешней структуре:

```go play
package main

import "fmt"

type Engine struct {
	Power int
}

// метод типа Engine
func (e Engine) start() string {
	return fmt.Sprintf("двигатель на %d л.с. запущен", e.Power)
}

type Car struct {
	Engine // встроенный тип — без имени поля
	Brand  string
}

func main() {
	c := Car{
		Engine: Engine{Power: 120},
		Brand:  "Lada",
	}

	// поле Power доступно напрямую, без c.Engine.Power
	fmt.Println(c.Brand, "—", c.Power, "л.с.")

	// метод start() продвинулся: вызываем его на Car
	fmt.Println(c.start())
}
```

Вывод:

```
Lada — 120 л.с.
двигатель на 120 л.с. запущен
```

`Car` не наследует `Engine` — он его **содержит**, а Go лишь даёт удобный короткий
доступ. Метод `start()` остаётся методом `Engine`; снаружи кажется, что он у
`Car`. Это и есть «композиция вместо наследования»: вы собираете поведение из
кусочков, а не выстраиваете иерархию классов.

Если внешняя структура объявит собственный метод с тем же именем, что и
встроенный, — победит внешний. Встроенный никуда не денется, к нему всегда можно
обратиться явно: `c.Engine.start()`. Это удобный способ «переопределить»
поведение, не ломая исходный тип.

## Анонимные структуры и теги полей

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

```go
var input struct {
    Text string `json:"text"`
}
```

Это *анонимная структура*: у неё есть поля, но нет имени типа. Строка в обратных
кавычках после поля — *тег*. Сам Go его не читает, а пакеты стандартной
библиотеки читают. Здесь тег говорит пакету `encoding/json`: «в JSON это поле
называется `text`». Именно такую запись вы встретите в курсе «Backend на Go»,
когда сервер будет принимать JSON-запрос. Поле при этом должно начинаться с
заглавной буквы, иначе пакет `json` его не увидит; почему — в главе
[«Пакеты и модули»](/go-basics/book/packages-modules).

## Частые ошибки

- **Метод-мутатор с получателем по значению.** Меняете поле внутри метода, а снаружи
  ничего не происходит — потому что меняли копию. Нужна мутация — нужен `*T`.
- **Позиционный литерал после изменения структуры.** Добавили поле — старые
  `T{a, b}` либо не компилируются, либо тихо съезжают. Пишите по именам полей.
- **Смешанные получатели у одного типа.** Часть методов по значению, часть по
  указателю: читателю приходится помнить, кто меняет оригинал, а кто копию.
  Выбирайте один стиль на тип.
- **Путать встраивание с наследованием.** Продвижение методов — это удобный
  доступ, а не родительский тип. Встроенный тип сам по себе не видит внешний —
  продвижение работает только в одну сторону, наружу.

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

Объявите `Account` с полем `Balance int` и метод `Deposit(amount int)`, изменяющий баланс. Создайте нулевой счёт, пополните его на `20`, затем на `5`. Напечатайте баланс. Замените получатель метода с указателя на значение и объясните изменившийся результат.

### Проверка

С указателем результат `25`, с получателем по значению — `0`:

```go
type Account struct {
    Balance int
}

func (a *Account) Deposit(amount int) {
    a.Balance += amount
}
```

В `main` используйте `var a Account`, `a.Deposit(20)`, `a.Deposit(5)` и `fmt.Println(a.Balance)`. Нулевой баланс уже пригоден для работы; отдельный конструктор здесь не нужен. Получатель по значению изменял бы копию поля `Balance`.

## Что дальше

Следующая глава — [интерфейсы](/go-basics/book/interfaces). Там функция сможет работать с разными структурами через нужный ей набор методов. Перед переходом убедитесь, что можете объяснить результат двух версий Deposit из упражнения.
