Структуры в Go

Теория: Указатели на структуры

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

Указатель — это переменная, которая хранит не само значение, а адрес этого значения в памяти. Когда программа работает с указателем, она получает доступ к тому же самому объекту, а не к его копии.

Зачем они нам нужны:

  1. Большие структуры. Если структура состоит из десятков или сотен полей, ее копирование при каждом вызове функции становится затратным по времени и памяти. Указатель позволяет передавать только адрес, работая с объектом напрямую.
  2. Изменение данных. Когда функция должна менять объект (например, обновить статус заказа или пересчитать баланс счета), удобнее передавать указатель. Так изменения отразятся на оригинальном объекте, а не на копии.
  3. Хранение в коллекциях. В срезах и словарях часто сохраняют именно указатели. Это экономит память и позволяет обновлять данные в одном месте, а использовать — в нескольких. Например, при загрузке профилей из базы данных ORM-библиотеки почти всегда возвращают указатели на структуры.

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

Специальный синтаксис работы с указателями

В Go указатели устроены проще, чем в C/C++: здесь нет арифметики указателей, нельзя «гулять» по памяти, все строго типизировано. Но есть несколько ключевых операторов, которые нам нужно знать.

  1. & — взять адрес значения. Оператор & позволяет получить указатель на существующую переменную:

    order := Order{ID: 1, Status: "new"}
    ptr := &order // ptr имеет тип *Order, то есть "указатель на Order"

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

    • передать объект в функцию без копирования,
    • хранить объект в коллекции и работать с ним по ссылке,
    • экономить ресурсы при работе с большими структурами.
  2. * — разыменование (получение значения по адресу). Оператор * извлекает значение, на которое указывает указатель:

    fmt.Println((*ptr).Status) // выведет "new"

    Важно: *ptr — это сама структура Order, а (*ptr).Status — доступ к полю.

  3. Авторазыменование для структур. Go избавляет нас от лишнего синтаксиса: для структур можно писать проще. Вместо (*ptr).Status разрешено писать ptr.Status:

    ptr.Status = "paid" // Go автоматически разыменует указатель
    fmt.Println(ptr.Status) // "paid"

    Таким образом, запись через * и без нее абсолютно эквивалентны. В реальных проектах всегда используют короткую форму ptr.Field, потому что она чище и понятнее.

  4. Методы с указателями. Чтобы метод мог изменять структуру, его получатель должен быть указателем (*Type). Это стандартный паттерн, например, в моделях данных:

    func (o *Order) MarkAsPaid() {
        o.Status = "paid"
    }

    Применение:

    order := Order{ID: 2, Status: "new"}
    order.MarkAsPaid()
    fmt.Println(order.Status) // "paid"

    Здесь order автоматически передается как указатель, даже если мы написали order.MarkAsPaid(), а не (&order).MarkAsPaid(). Go делает это сам.

  5. Нулевой указатель (nil). Указатель может быть пустым, то есть не указывать никуда. По умолчанию любая переменная-указатель инициализируется nil:

    var o *Order
    fmt.Println(o == nil) // true

    Если попробовать обратиться к полю через o.Status, будет паника (runtime error: invalid memory address). Такие ситуации часто используют как признак отсутствия данных: например, функция может вернуть *Order или nil, если заказ не найден.

Краткая шпаргалка:

  • & — взять адрес значения.
  • * — получить значение по адресу.
  • Авторазыменование — обращаться к полям структуры через указатель можно без *.
  • Методы с указателями позволяют менять объект. nil означает, что указатель пуст, работать с ним как с объектом нельзя.

Проблема без указателей

Представим, что мы пишем систему заказов. У нас есть структура Order:

type Order struct {
	ID       int
	Customer string
	Status   string
}

Мы пишем функцию, которая меняет статус заказа на "paid":

func MarkAsPaid(o Order) {
	o.Status = "paid"
}

Пробуем вызвать:

order := Order{ID: 101, Customer: "Иван", Status: "new"}
MarkAsPaid(order)
fmt.Println(order.Status) // что выведет?

Результат — new. Почему? Потому что в функцию передалась копия структуры. Изменения произошли только внутри функции, а оригинал остался без изменений.

Решение с указателями

Исправим функцию:

func MarkAsPaid(o *Order) {
	o.Status = "paid"
}

Теперь вызываем так:

order := Order{ID: 101, Customer: "Иван", Status: "new"}
MarkAsPaid(&order)
fmt.Println(order.Status) // выведет "paid"

Мы передали адрес структуры, а внутри функции меняем данные по этому адресу.

Удобство Go: автоматическая разыменовка

Go делает работу с указателями удобной. Когда у нас есть указатель на структуру, можно обращаться к ее полям напрямую без явного (*o).Status. Компилятор сам понимает, что нужно разыменовать:

order := &Order{ID: 102, Customer: "Мария", Status: "new"}
order.Status = "paid" // это корректно
fmt.Println(order.Status)

Важный нюанс: nil-указатели

Так как указатель может не указывать ни на что, возможна ситуация nil. Это часто используется, чтобы показать, что данных нет.

var order *Order
if order == nil {
  fmt.Println("Заказ отсутствует")
}

Если попробовать обратиться к order.Status, будет паника (runtime error: invalid memory address). Поэтому перед использованием указателей всегда нужно проверять, что они не nil, если это не гарантировано логикой программы.

Рекомендуемые программы

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845