Указатели против значений: когда использовать * и & в параметрах и возвращаемых значениях 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. Практический чеклист
Перед тем как написать *, спросите себя:
- Мне нужно изменить оригинал? → Да → указатель
- Структура > 100 байт? → Да → указатель
- Может быть nil? → Да → указатель
- Это слайс/мап/канал/интерфейс? → Да → значение
- Это 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, он хорошо оптимизирует копирование маленьких структур.