> Имеет ли смысл передавать слайс по указателю в Go (Go)

Уровень: senior · Роль: backend · Категория: Технические вопросы

Компании: Wildberries

Стек: Go

> Пример ответа

Короткий ответ

В подавляющем большинстве случаев - нет. Слайс уже является структурой из указателя на массив, длины и ёмкости, поэтому передача по значению копирует только эти три поля, а не сами данные. Передача по указателю оправдана только тогда, когда функция должна изменить сам слайс: его длину, ёмкость или заменить на новый. В остальных сценариях это добавляет косвенность без выгоды и ухудшает читаемость.

Подробное объяснение

Слайс в Go - это дескриптор, состоящий из трёх слов: указатель на underlying array, length и capacity. При передаче по значению копируются эти три слова - это дёшево (24 байта на 64-битной платформе). Сами элементы массива не копируются, поэтому мутации элементов внутри функции будут видны вызывающему коду независимо от способа передачи.

Ключевое различие: если функция вызывает append и новый массив не требуется, элементы изменятся в обоих случаях. Но если append приводит к перераспределению памяти (когда capacity недостаточно), новый слайс получит новый указатель на массив - и это изменение не будет видно вызывающему при передаче по значению.

Передача по указателю *[]T нужна, когда функция:

  • меняет длину или ёмкость слайса (например, через append с перераспределением);
  • присваивает слайсу новое значение (s = nil, s = make([]T, n));
  • возвращает изменённый слайс через параметр, а не через return.

При этом передача по указателю имеет недостатки:

  • добавляет лишний уровень косвенности - код становится менее очевидным;
  • мешает инлайнингу и может снизить производительность в горячих путях;
  • создаёт риск случайного изменения слайса там, где это не предполагалось.

Идиоматичный Go: если функция должна изменить слайс, лучше вернуть новый слайс из функции, а не принимать указатель. Например, append сам возвращает новый слайс.

На практике

В реальном коде правило простое: передавайте слайс по значению, если функция только читает элементы или изменяет их содержимое. Используйте *[]T только когда функция обязана изменить сам дескриптор слайса - и даже тогда чаще предпочтителен возврат нового значения.

Исключения, когда указатель оправдан:

  • очень большие слайсы, где копирование 24 байт дескриптора несущественно, но указатель может быть семантически важен (редко);
  • API, где слайс должен оставаться nil или быть заменён целиком, и это часть контракта;
  • оптимизация в ultra-hot path, где лишняя копия дескриптора измерена профилировщиком (крайне редко).

На практике в продакшн-коде Go вы почти не встретите *[]T в сигнатурах - это сигнал к рефакторингу.

Пример кода

GO
// Плохо: указатель не нужен, append внутри меняет дескриптор
func appendBad(s *[]int, v int) {
*s = append(*s, v)
}
// Хорошо: возвращаем новый слайс
func appendGood(s []int, v int) []int {
return append(s, v)
}
// Хорошо: мутация элементов видна и без указателя
func fill(s []int, v int) {
for i := range s {
s[i] = v
}
}
// Единственный оправданный случай: замена слайса целиком
func reset(s *[]int) {
*s = nil
}

Как отвечать на собеседовании

Начните с того, что слайс - это структура из трёх слов, и передача по значению копирует только дескриптор. Подчеркните, что элементы не копируются, поэтому мутации элементов видны всегда. Затем объясните разницу: append может перераспределить память, и тогда изменение длины/указателя не дойдёт до вызывающего при передаче по значению. Скажите, что идиоматичный подход - возвращать новый слайс, а не принимать указатель. Приведите пример, где указатель действительно нужен (замена на nil или полный сброс). Упомяните, что *[]T - редкий паттерн в реальном коде, и его наличие часто указывает на проблему дизайна.

Что проверяет интервьюер

Интервьюер оценивает:

  • понимание внутреннего устройства слайса (дескриптор, underlying array);
  • знание поведения append и перераспределения памяти;
  • умение различать мутацию элементов и мутацию самого слайса;
  • следование идиомам Go (возврат значения вместо указателя);
  • способность объяснить trade-off между косвенностью и читаемостью.

Типичные ошибки

  • Утверждение, что слайс - это ссылочный тип, и поэтому указатель не нужен никогда - это неверно, потому что append может изменить дескриптор.
  • Путаница между изменением элементов (видно всегда) и изменением длины/ёмкости (видно только при передаче по указателю или через возврат).
  • Предложение использовать *[]T для "экономии памяти" - это ошибка: копия дескриптора стоит 24 байта, а указатель - 8, разница ничтожна.
  • Игнорирование случая, когда функция присваивает слайсу nil или новый make - здесь по значению не сработает.
  • Рекомендация передавать по указателю "для производительности" без профилирования - в Go это антипаттерн.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью