воскресенье, 4 октября 2020 г.

Срезы как аргументы в Golang

Недавно я наткнулся на несколько примеров использования срезов в качестве аргументов функции, которые приводят к неожиданным результатам. Этот пост посвящен этим примерам.

Первый пример. Срез передается функции в качестве аргумента, где он будет изменен.

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.


Читайте также:


Комментариев нет:

Отправить комментарий