Недавно я наткнулся на несколько примеров использования срезов в качестве аргументов функции, которые приводят к неожиданным результатам. Этот пост посвящен этим примерам.
Первый пример. Срез передается функции в качестве аргумента, где он будет изменен.
https://play.golang.org/p/r5mKX5ErwLC
package main
import "fmt"
func change(abc []int) {
for i := range abc {
abc[i] = 4
}
fmt.Println(abc)
}
func main() {
abc := []int{1, 2, 3}
change(abc)
fmt.Println(abc)
}
Вывод:
[4 4 4]
[4 4 4]
Кто-то может ожидать, что, когда мы передадим срез в функцию, мы получим его копию в функции, а изменения появятся только в функции. Но это не так. Срезы в Go имеют базовый массив и передаются по ссылке, а не по значению. Мы просто передаем ссылку на срез в функцию, затем меняем ее, и она меняет внешний срез.
Второй пример.
https://play.golang.org/p/5ruLrp6ZJJc
package main
import "fmt"
func change(abc []int) {
abc = append(abc, 4)
for i := range abc {
abc[i] = 4
}
fmt.Println(abc)
}
func main() {
abc := []int{1, 2, 3}
change(abc)
fmt.Println(abc)
}
Вывод:
[4 4 4 4]
[1 2 3]
Что изменилось? Чем этот пример отличается от первого? Мы просто добавляем один элемент в срез внутри функции. Но это существенно меняет срез. Создает новый срез. Как? Когда мы создаем срез вне функции, он создает базовый массив размером для 3 элементов. Затем мы передаем ссылку в функцию. Но когда мы хотим добавить еще один элемент, у нас нет места для него в базовом массиве. Затем встроенная функция добавления создает новый массив и новый срез. Когда мы меняем в нем значения, мы не меняем первый срез.
Третий пример.
https://play.golang.org/p/0tKWomkDCk3
package main
import "fmt"
func change(abc []int) {
abc = append(abc, 5)
for i := range abc {
abc[i] = 4
}
fmt.Println(abc)
}
func main() {
abc := []int{1, 2, 3}
change(abc)
abc = append(abc, 4)
fmt.Println(abc)
}
Вывод:
[4 4 4 4]
[1 2 3 4]
Почему, когда мы меняем срез вне функции, он не будет указывать на измененный срез в функции? Ну это просто совершенно разные срезы - и все. Когда мы используем append вне функции, он создает для него новый массив и новый срез.
Пример четвертый.
https://play.golang.org/p/uCDG59fJLFm
package main
import "fmt"
func change(abc []int) {
abc = append(abc, 4)
for i := range abc {
abc[i] = 4
}
fmt.Println(abc)
}
func main() {
abc := []int{1, 2, 3}
abc = append(abc, 4)
change(abc)
fmt.Println(abc)
}
Вывод:
[4 4 4 4 4]
[4 4 4 4]
Почему этот пример отличается от третьего? Когда мы добавляем срез перед передачей его функции, он создает базовый массив с емкостью не только для одного добавленного элемента, но и с начальным числом элементов, кратным 1,5 - для 6 элементов. Мы можем это увидеть:
package main
import "fmt"
func change(abc []int) {
abc = append(abc, 4)
for i := range abc {
abc[i] = 4
}
fmt.Println(abc)
}
func main() {
abc := []int{1, 2, 3}
abc = append(abc, 4)
fmt.Println(cap(abc))
change(abc)
fmt.Println(abc)
}
Вывод:
6
[4 4 4 4 4]
[4 4 4 4]
То есть когда мы добавляем новый элемент внутри функции, у нас достаточно места для него - поэтому append не создаст новый массив и новый срез.
Эти примеры могут привести к неожиданным для кого-то результатам. Как этого избежать?
Просто верните срез из функции. Он вернет ссылку на новый срез, если он был создан.
package main
import "fmt"
func change(abc []int) []int {
abc = append(abc, 4)
for i := range abc {
abc[i] = 4
}
fmt.Println(abc)
return abc
}
func main() {
abc := []int{1, 2, 3}
abc = change(abc)
fmt.Println(abc)
}
Вывод:
[4 4 4 4]
[4 4 4 4]
И напишите модульные тесты. Скорее всего, они обнаружат неожиданное поведение. Если применимо, сначала напишите тесты - используйте Test Driven Development.
Читайте также:
- Пакет strings в Golang, функции Trim
- Пакет strings в Golang, функция Map, Repeat, Replace
- Пакет strings в Golang, функции Split
Комментариев нет:
Отправить комментарий