Есть ли способ передать слайс в функцию так, чтобы изменения были видны без указателей
Что проверяют
Знаете ли вы, что слайс это структура из указателя на массив, длины и ёмкости и что передаётся она по значению. Следом обычно дают код с append внутри функции и просят предсказать вывод.
Как отвечать
Короткий ответ: изменения элементов видны и так, а изменение длины без указателя передаётся только через возвращаемое значение.
- изменение элементов:
s[0] = 42внутри функции пишет в общий базовый массив, и вызывающий код это увидит; appendв пределах ёмкости: элемент ляжет в тот же массив, но у вызывающего своя копия заголовка со старой длиной, и новый элемент он не увидит. Зато позже его собственныйappendперезапишет эту ячейку, и это источник тонких багов;appendсверх ёмкости: рантайм выделяет новый массив, и дальнейшие изменения внутри функции до вызывающего не доходят вовсе.
Идиоматичные способы без указателя:
- вернуть слайс:
s = addItems(s), так устроен самappendи функции пакетаslicesвродеslices.Insertиslices.Delete; - буфер от вызывающего: функция пишет в переданный слайс и возвращает, сколько записала, как
Read(p []byte) (n int, err error)уio.Reader. Вызывающий дальше работает сbuf[:n]; - заранее выделенная длина:
make([]int, n)и запись по индексу, длина не меняется, иappendне нужен.
func fill(dst []int) int {
return copy(dst, []int{1, 2, 3})
}Параметр *[]T тоже законный, например его передают в json.Unmarshal, но в обычном коде возврат значения читается проще.
Где спотыкаются
Говорят «слайсы передаются по ссылке». В Go всё передаётся по значению, просто в значении слайса лежит указатель на массив.
Уверены, что append внутри функции «иногда работает». Запись в массив действительно происходит, но длина у вызывающего остаётся прежней.
Забывают про общий массив у двух слайсов: b := a[:2] и затем append(b, x) перезапишет a[2]. Ограничить ёмкость помогает полное выражение среза a[:2:2].