Особенности передачи слайса в функцию в Go. Это передача по ссылке или по значению?

Ответ

Это классический вопрос с подвохом. Правильный ответ: в Go всё, включая слайсы, передается в функцию по значению.

Однако, чтобы понять последствия, нужно знать, что такое слайс.

Слайс — это не сам массив данных, а легковесная структура-дескриптор, которая содержит три поля:

  1. Указатель на первый элемент базового массива (underlying array).
  2. Длина (len) — количество элементов в слайсе.
  3. Емкость (cap) — количество элементов от начала слайса до конца базового массива.

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

Это приводит к двум важным сценариям:

1. Изменение элементов слайса

Если вы изменяете элемент слайса внутри функции, вы изменяете данные в общем базовом массиве. Эти изменения будут видны снаружи.

func modifyElements(s []int) {
    s[0] = 100 // Изменение видно снаружи
}

func main() {
    originalSlice := []int{1, 2, 3}
    modifyElements(originalSlice)
    fmt.Println(originalSlice) // Вывод: [100 2 3]
}

2. Изменение самого слайса (например, через append)

Здесь ситуация сложнее. Функция append может как изменить существующий массив, так и создать новый.

  • Если append не превышает емкость (cap): Новый элемент добавляется в тот же базовый массив. Однако изменяется только поле len у локальной копии слайса внутри функции. Оригинальный слайс об этом "не знает".
  • Если append превышает емкость (cap): Go создает новый, больший массив, копирует туда старые элементы и добавляет новый. Локальная копия слайса теперь указывает на этот новый массив. Оригинальный слайс остается без изменений.
func tryToAppend(s []int) {
    // Эта операция не будет видна снаружи, т.к. может быть создан новый массив,
    // а s - это локальная копия дескриптора.
    s = append(s, 4)
}

func main() {
    originalSlice := []int{1, 2, 3}
    tryToAppend(originalSlice)
    fmt.Println(originalSlice) // Вывод: [1 2 3]
}

Как правильно?

Чтобы надежно изменять слайс (его длину, емкость или перенаправлять на новый массив), используйте идиоматический подход Go: возвращайте измененный слайс из функции.

func correctlyAppend(s []int) []int {
    s = append(s, 4)
    return s
}

func main() {
    originalSlice := []int{1, 2, 3}
    originalSlice = correctlyAppend(originalSlice) // Переприсваиваем результат
    fmt.Println(originalSlice) // Вывод: [1 2 3 4]
}