Зарегистрируйтесь, чтобы продолжить обучение

Указатели на структуры Структуры в Go

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

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

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

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

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

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

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

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

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

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

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

  1. * — разыменование (получение значения по адресу). Оператор * извлекает значение, на которое указывает указатель:

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

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

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

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

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

  3. Методы с указателями. Чтобы метод мог изменять структуру, его получатель должен быть указателем (*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 делает это сам.

  4. Нулевой указатель (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, если это не гарантировано логикой программы.


Самостоятельная работа

В этом закрепим работу с указателями: передача в функции, изменение значений, проверка nil, методы с указателем‑получателем.

Обновление статуса заказа

  • Опишите структуру Order с полями: ID int, Customer string, Status string.
  • Реализуйте две функции:
    • SetStatusCopy(o Order, status string) — принимает копию и пытается поменять статус;
    • SetStatusPtr(o *Order, status string) — принимает указатель и меняет статус оригинала.
  • Покажите в main разницу в поведении (после вызова первой статус не меняется, после второй — меняется).
Показать пример ответа
package main

import "fmt"

type Order struct {
    ID       int
    Customer string
    Status   string
}

func SetStatusCopy(o Order, status string) {
    o.Status = status // меняем только копию
}

func SetStatusPtr(o *Order, status string) {
    if o == nil {
        return
    }
    o.Status = status // меняем оригинал
}

func main() {
    o := Order{ID: 101, Customer: "Иван", Status: "new"}
    SetStatusCopy(o, "paid")
    fmt.Println("после SetStatusCopy:", o.Status) // new

    SetStatusPtr(&o, "paid")
    fmt.Println("после SetStatusPtr:", o.Status) // paid
}

Безопасное увеличение счётчика

  • Напишите функцию Inc(p *int) bool, которая увеличивает значение по указателю на 1.
  • Если p == nil, функция ничего не делает и возвращает false.
Показать пример ответа
package main

func Inc(p *int) bool {
    if p == nil {
        return false
    }
    *p++
    return true
}

Методы с указателем‑получателем

  • Опишите структуру Account с полями: ID int, Owner string, приватное поле balance float64.
  • Реализуйте методы:
    • Deposit(amount float64) bool — пополняет счёт, игнорирует неположительные суммы;
    • Balance() float64 — возвращает текущий баланс.
  • В main создайте счёт, выполните несколько операций и выведите итоговый баланс.

Подсказки: в Go действует авторазыменование для структур — можно писать ptr.Field и ptr.Method() без (*ptr).

Показать пример ответа
package main

import "fmt"

type Account struct {
    ID      int
    Owner   string
    balance float64
}

func (a *Account) Deposit(amount float64) bool {
    if amount <= 0 {
        return false
    }
    a.balance += amount
    return true
}

func (a *Account) Balance() float64 { return a.balance }

func main() {
    acc := &Account{ID: 1, Owner: "Мария"}
    _ = acc.Deposit(500)
    _ = acc.Deposit(-10) // игнорируем
    fmt.Println("Баланс:", acc.Balance())
}

Дополнительные материалы

  1. Tour of Go — Pointers
  2. Effective Go — Pointers vs. values
  3. Go Spec — Pointer types

Для полного доступа к курсу нужен базовый план

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

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff