- Специальный синтаксис работы с указателями
- Проблема без указателей
- Решение с указателями
- Удобство Go: автоматическая разыменовка
- Важный нюанс: nil-указатели
В языке Go структуры часто используют для описания сущностей: пользователя, заказ, товар, сообщение. Но при работе с такими объектами нам важно не только хранить данные, но и управлять тем, как они передаются между функциями и сохраняются в памяти. Здесь нам помогают указатели.
Указатель — это переменная, которая хранит не само значение, а адрес этого значения в памяти. Когда программа работает с указателем, она получает доступ к тому же самому объекту, а не к его копии.
Зачем они нам нужны:
- Большие структуры. Если структура состоит из десятков или сотен полей, ее копирование при каждом вызове функции становится затратным по времени и памяти. Указатель позволяет передавать только адрес, работая с объектом напрямую.
- Изменение данных. Когда функция должна менять объект (например, обновить статус заказа или пересчитать баланс счета), удобнее передавать указатель. Так изменения отразятся на оригинальном объекте, а не на копии.
- Хранение в коллекциях. В срезах и словарях часто сохраняют именно указатели. Это экономит память и позволяет обновлять данные в одном месте, а использовать — в нескольких. Например, при загрузке профилей из базы данных ORM-библиотеки почти всегда возвращают указатели на структуры.
Указатели — это инструмент управления эффективностью и целостностью данных: с ними мы избегаем лишнего копирования и работаем с одним и тем же объектом в разных частях программы.
Специальный синтаксис работы с указателями
В Go указатели устроены проще, чем в C/C++: здесь нет арифметики указателей, нельзя «гулять» по памяти, все строго типизировано. Но есть несколько ключевых операторов, которые нам нужно знать.
&
— взять адрес значения. Оператор&
позволяет получить указатель на существующую переменную:order := Order{ID: 1, Status: "new"} ptr := &order // ptr имеет тип *Order, то есть "указатель на Order"
Теперь
ptr
не содержит сам заказ, а указывает на участок памяти, где он хранится. Такой прием используют, когда хотят:
* передать объект в функцию без копирования, * хранить объект в коллекции и работать с ним по ссылке, * экономить ресурсы при работе с большими структурами.
*
— разыменование (получение значения по адресу). Оператор*
извлекает значение, на которое указывает указатель:fmt.Println((*ptr).Status) // выведет "new"
Важно:
*ptr
— это сама структураOrder
, а(*ptr).Status
— доступ к полю.Авторазыменование для структур. Go избавляет нас от лишнего синтаксиса: для структур можно писать проще. Вместо
(*ptr).Status
разрешено писатьptr.Status
:ptr.Status = "paid" // Go автоматически разыменует указатель fmt.Println(ptr.Status) // "paid"
Таким образом, запись через
*
и без нее абсолютно эквивалентны. В реальных проектах всегда используют короткую формуptr.Field
, потому что она чище и понятнее.Методы с указателями. Чтобы метод мог изменять структуру, его получатель должен быть указателем (*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 делает это сам.Нулевой указатель (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())
}
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.