Указатели или значения: когда использовать * и & в параметрах и возвращаемых значениях Go?
Основное правило: всё начинается с семантики
В 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.
Золотое правило для новичков:
- Если сомневаетесь — используйте значение (T). Это безопаснее.
- Если функция модифицирует объект — указатель.
- Если структура > 50 байт или содержит мьютекс — указатель.
- Для возврата: если объект "живёт" дольше функции (например, добавляется в глобальный список) — указатель. Иначе — значение.
Профилируйте, если сомневаетесь. 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 даёт инструменты для измерения — используйте их!
Ответов пока нет.
Войдите чтобы оставить ответ