Avatar ·

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

Всем привет! Я начал изучать Go недавно, и меня постоянно мучает вопрос: когда нужно передавать параметры в функцию по указателю, а когда по значению? И то же самое с возвращаемыми значениями. Вроде бы понимаю, что указатели позволяют избежать копирования данных, но в Go и так всё копируется? Или нет? Я написал небольшой пример, где создаю структуру User и пытаюсь её модифицировать: ```go type User struct { Name string Age int } func updateUser(u User) { u.Name = "New Name" // не работает как ожидалось } func updateUserPtr(u *User) { u.Name = "New Name" // работает } ``` Но вот что меня смущает: вроде бы в Go всё передаётся по значению, но слайсы и мапы ведут себя как ссылки? Или нет? Я читал, что большие структуры лучше передавать по указателю, чтобы не копировать, а маленькие — по значению. Но что считать "большим"? 10 полей? 20? Или это не только про размер? Также не понимаю, когда стоит возвращать указатель из функции, а когда значение. Например, при создании нового объекта: ```go func NewUser(name string, age int) *User { ... } // или func NewUser(name string, age int) User { ... } ``` В некоторых пакетах стандартной библиотеки Go я вижу оба подхода. Можете объяснить на пальцах, когда какой вариант предпочтительнее? Особенно интересует влияние на производительность, на сборщик мусора и на безопасность кода (nil-ы, race conditions и т.д.). Хочется чёткого правила или набора правил, чтобы не гадать каждый раз. Спасибо! <<>>

Основное правило: всё начинается с семантики

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

Когда использовать указатель ( *T ) в параметрах:

  • Мутация объекта — если функция должна изменить поля структуры, передавайте указатель. Иначе изменения останутся внутри функции.
  • Большие структуры — если структура > 50-100 байт (например, содержит много строк или вложенных полей), копирование при каждом вызове становится заметным. Указатель (8 байт) дешевле.
  • Nil-семантика — если параметр может отсутствовать (nil), используйте указатель. Значение не может быть nil.
  • Синхронизация — если объект используется с мьютексами или атомиками, он должен быть указателем, чтобы блокировка работала на одном экземпляре.

Когда использовать значение ( T ) в параметрах:

  • Неизменяемые данные — если функция не меняет объект, передача по значению безопаснее: никаких побочных эффектов.
  • Маленькие типы — числа, булевы, маленькие структуры (до 4-8 полей простых типов). Копирование дешевле разыменования указателя.
  • Примитивные типы — int, float, bool, string (строка — это 16 байт: указатель + длина). Их копирование тривиально.
  • Безопасность — значение гарантирует, что оригинал не изменится. Это упрощает рассуждения о коде.

Когда возвращать указатель:

  • Конструкторы больших объектов — если структура большая, возврат указателя избегает копирования.
  • Nil для ошибок — если функция может не создать объект, возвращайте указатель и проверяйте на nil.
  • Единый экземпляр — если объект должен быть общим (например, синглтон или кеш).

Когда возвращать значение:

  • Маленькие типы — int, string, простые структуры. Копирование дешево.
  • Неизменяемые данные — возврат значения предотвращает случайное изменение извне.
  • Избежание мусора — указатель создаёт нагрузку на GC (сборщик мусора). Значение на стеке — без GC.

Практический пример с производительностью:

// Структура ~200 байт
type LargeStruct struct {
    A [50]int  // 400 байт
    B string   // 16 байт
    C float64  // 8 байт
}

// Передача по значению: копируется 424 байта
func processByValue(s LargeStruct) { ... }

// Передача по указателю: копируется 8 байт
func processByPointer(s *LargeStruct) { ... }

Для такой структуры указатель даёт выигрыш ~50x на копировании. Но для type Point struct { X, Y int } (16 байт) разница незаметна.

Что насчёт слайсов и мап?

Они уже содержат указатель на данные внутри. Передавая слайс по значению, вы копируете заголовок (24 байта: указатель + длина + ёмкость), но данные остаются общими. Поэтому слайс можно менять (через индексы) без указателя, но нельзя изменить его длину/ёмкость. Если нужно изменить сам слайс (append, ресайз) — передавайте *[]T.

Золотое правило для новичков:

  1. Если сомневаетесь — используйте значение (T). Это безопаснее.
  2. Если функция модифицирует объект — указатель.
  3. Если структура > 50 байт или содержит мьютекс — указатель.
  4. Для возврата: если объект "живёт" дольше функции (например, добавляется в глобальный список) — указатель. Иначе — значение.

Профилируйте, если сомневаетесь. Go — прагматичный язык, и указатели здесь не для "экономии памяти любой ценой", а для семантики и контроля над временем жизни объектов.

<<>>

Дополнение про конкурентность и интерфейсы

Есть ещё один важный аспект, который стоит учитывать — работа с интерфейсами и горутинами.

Интерфейсы

Если ваша структура реализует интерфейс, то при передаче по значению интерфейс будет содержать копию. Это может привести к неожиданному поведению:

type Stringer interface {
    String() string
}

type User struct {
    Name string
}

func (u User) String() string { return u.Name }

func main() {
    u := User{"Alice"}
    var s Stringer = u  // копия User
    u.Name = "Bob"
    fmt.Println(s.String()) // "Alice", не "Bob"
}

Если нужно, чтобы интерфейс отслеживал изменения — передавайте указатель: var s Stringer = &u.

Горутины и race conditions

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

  • Если данные только читаются — указатель ок.
  • Если данные пишутся из нескольких горутин — либо синхронизация (мьютекс), либо передавайте копию (значение).

И последнее: в Go принято возвращать ошибку как значение, а не как указатель. Исключение — если ошибка должна быть кастомной и тяжёлой, но это редкий случай.

В целом, доверяйте бенчмаркам и профилировщику, а не догадкам. Go даёт инструменты для измерения — используйте их!