Avatar ·

Указатели против значений: когда использовать * и & в параметрах и возвращаемых значениях Go?

Я только начал изучать Go и постоянно путаюсь, когда передавать указатель, а когда — значение. Вроде бы прочитал кучу статей, но на практике всё равно делаю не так. Например, у меня есть структура User и функция, которая её обновляет: ```go type User struct { Name string Age int Score float64 } func UpdateScore(u User, newScore float64) { u.Score = newScore } ``` Когда я вызываю `UpdateScore(myUser, 95.5)`, то после вызова `myUser.Score` остаётся прежним. Я понял, что это потому что Go копирует структуру. Тогда я меняю на `*User`: ```go func UpdateScore(u *User, newScore float64) { u.Score = newScore } ``` Теперь работает. Но когда я пишу функцию, которая возвращает нового пользователя, я вижу два варианта: ```go func NewUser(name string, age int) User { ... } func NewUserPtr(name string, age int) *User { ... } ``` Какой из них правильный? И ещё: как быть с большими структурами? Везде писать указатели? А что насчёт базовых типов (int, string)? Я слышал, что их никогда не нужно передавать по указателю, но почему? И как быть с интерфейсами? Помогите разобраться с этим зоопарком указателей и значений в Go. <<>>

Основное правило: "когда сомневаешься — передавай значение"

В Go это не просто совет — это философия языка. В отличие от C++ или Java, где указатели/ссылки используются почти всегда, в Go принято минимизировать использование указателей. Давайте разберём все случаи системно.

1. Базовые типы (int, string, bool, float, complex)

Всегда передавайте по значению. Исключение — только если вам нужно изменить оригинал (редкий случай) или если это критично для производительности в цикле на миллиард итераций (но сначала измерьте!).

// ПЛОХО
func double(x *int) {
    *x *= 2
}

// ХОРОШО
func double(x int) int {
    return x * 2
}

Почему? Базовые типы маленькие (8-16 байт), копирование дешёвое. Указатель — это тоже 8 байт, плюс дополнительный уровень косвенности.

2. Структуры

Здесь три критерия:

  • Мутабельность — если функция меняет структуру, нужен указатель
  • Размер — если структура > 100-200 байт, рассмотрите указатель
  • Семантика nil — если структура может отсутствовать, используйте указатель
// Мутация — только указатель
func (u *User) Promote() {
    u.Score *= 1.1
}

// Создание — обычно значение (если не нужно nil)
func NewUser(name string) User {
    return User{Name: name}
}

// Если может быть nil — указатель
func FindUser(id int) *User {
    if id == 0 {
        return nil
    }
    return &User{ID: id}
}

3. Слайсы, мапы, каналы, интерфейсы

Всегда передавайте по значению. Эти типы сами по себе являются ссылками (содержат указатель на внутренние данные).

// Правильно: слайс уже ссылка
func ProcessItems(items []string) {
    items[0] = "changed" // изменит оригинал!
}

// НЕ НУЖНО делать так:
func ProcessItemsWrong(items *[]string) {} // избыточно

Исключение: если вам нужно изменить сам слайс (добавить/удалить элементы), то да, нужен указатель на слайс.

4. Методы: receiver (получатель)

Здесь правило простое:

  • Value receiver — если метод не меняет структуру (геттер, вычисление)
  • Pointer receiver — если метод меняет структуру ИЛИ структура большая
type BigData struct {
    data [1000]int
}

// Value receiver — копирует 8 КБ! Плохо
func (b BigData) Sum() int { ... }

// Pointer receiver — копирует только указатель
func (b *BigData) Sum() int { ... }

5. Возвращаемые значения

Правило "когда сомневаешься — значение" работает и здесь. Указатель возвращайте только если:

  • Функция может вернуть nil (объект не найден)
  • Вы хотите, чтобы вызывающий мог изменить объект
  • Производительность критична (большая структура)
// Хорошо: значение
func ParseConfig(path string) (Config, error) { ... }

// Хорошо: указатель (может быть nil)
func GetUser(id int) (*User, error) { ... }

6. Практический чеклист

Перед тем как написать *, спросите себя:

  1. Мне нужно изменить оригинал? → Да → указатель
  2. Структура > 100 байт? → Да → указатель
  3. Может быть nil? → Да → указатель
  4. Это слайс/мап/канал/интерфейс? → Да → значение
  5. Это int/string/bool? → Да → значение

Если все ответы "нет" — смело используйте значение.

<<>>

Альтернативный взгляд: производительность и escape-анализ

Есть один важный нюанс, который часто упускают: указатели могут заставить Go выделять память в куче (heap) вместо стека (stack). Это называется "escape to heap".

Когда вы возвращаете указатель на локальную переменную:

func NewUser() *User {
    u := User{}
    return &u // u "убегает" в кучу
}

Переменная u не может жить на стеке, потому что указатель на неё уйдёт за пределы функции. Это вызывает дополнительную нагрузку на сборщик мусора.

А если вернуть значение:

func NewUser() User {
    u := User{}
    return u // u остаётся на стеке
}

Всё остаётся на стеке — быстро и без GC.

Когда указатель всё же выгоднее? Когда структура настолько большая (например, [1024]int = 8 КБ), что копирование на стеке дороже, чем работа с кучей. Но это редкий случай — обычно структуры меньше 1 КБ.

Совет: Используйте бенчмарки (testing.B) и флаг -gcflags=-m, чтобы видеть, что убегает в кучу. Не оптимизируйте на глаз — сначала измерьте.

Ещё один случай: если вы передаёте структуру в горутину, указатель может привести к race condition (гонке данных). Значение — безопаснее, так как каждый поток получает свою копию.

Итог: указатели — это не "более производительный" вариант автоматически. Часто наоборот. Доверяйте компилятору Go, он хорошо оптимизирует копирование маленьких структур.