Когда Golang append () создает новый срез?

По словам builtin api docs, append () будет перераспределять и копировать в новый блок массива, когда емкость исходного среза недостаточно велика.

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

package main

import (
    "fmt"
)

func AddOption(c chan []bool, combo []bool, length int) {
    if length == 0 {
        fmt.Println(combo, "!")
        c <- combo
        return
    }
    var newCombo []bool
    for _, ch := range []bool{true, false} {
        newCombo = append(combo, ch)
        AddOption(c, newCombo, length-1)
    }
}

func main() {
    c := make(chan []bool)
    go func(c chan []bool) {
        defer close(c)
        AddOption(c, []bool{}, 4)
    }(c)
    for combination := range c {
        fmt.Println(combination)
    }
}

здесь это ссылка playground на этот код. В выводе:

[true true true true] !
[true true true false] !
[true true true false]
[true true true false]
[true true false true] !
[true true false false] !
[true true false false]
[true true false false]
[true false true true] !
[true false true false] !
[true false true false]
[true false true false]
[true false false true] !
[true false false false] !
[true false false false]
[true false false false]
[false true true true] !
[false true true false] !
[false true true false]
[false true true false]
[false true false true] !
[false true false false] !
[false true false false]
[false true false false]
[false false true true] !
[false false true false] !
[false false true false]
[false false true false]
[false false false true] !
[false false false false] !
[false false false false]
[false false false false]

строки, заканчивающиеся восклицательным знаком, - это строки, отправленные в канал из AddOption. Те, кто снаружи, - это то, что появляется с другой стороны (т. е. в main()). Ясно, что срезы, отправленные по каналу, изменяются после их отправки.

поскольку AddOption возвращается сразу после отправки среза, модификация должна исходить из блока кода

var newCombo []bool
for _, ch := range []bool{true, false} {
    newCombo = append(combo, ch)
    AddOption(c, newCombo, length-1)
}

но, согласно документам, append () должен возвращать новый срез (cap(combo) недостаточно велик). Согласно ответ, дескриптор среза, отправленный в AddOption, должен быть копией; это не так? Насколько я могу судить, либо значение, отправленное в качестве второго аргумента AddOption (), является либо указателем на дескриптор среза, либо append() не возвращает новый срез.

3 ответов


, когда append() создает новый срез, он не создает срез, который только на один больше, чем срез раньше. Он фактически создает срез, который уже на пару элементов больше предыдущего. Взгляните на этот код:

package main

import "fmt"

func main() {
    var sl []bool

    for i := 0; i < 100; i++ {
        sl = append(sl, true)
        fmt.Println(cap(sl))
    }
}

площадка

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


вы путаете срез, тип данных, с фактическим представлением. фрагмент дескриптора состоит из пары ints, один для len и один для cap, и указатель на базовые данные.

так, что append возвращает действительно новый срез и то, что передается в добавить параметр действительно копия дескриптора кусочек. Но поскольку дескриптор имеет указатель на данные, значение указателя (адрес базовых данных) является тот же.

редактировать: вот фрагмент кода, иллюстрирующий мою точку зрения:

package main

import "fmt"

func main() {
    s := make([]int, 0, 5)
    s = append(s, []int{1, 2, 3, 4}...)

    a := append(s, 5)
    fmt.Println(a)

    b := append(s, 6)
    fmt.Println(b)
    fmt.Println(a)
}

если вы запустить вы получаете:

[1 2 3 4 5]
[1 2 3 4 6]
[1 2 3 4 6]

потому что с s все еще имеет емкость, оба a и b поделитесь теми же данными ptr. Если вы измените емкость на 4, он печатает:

[1 2 3 4 5]
[1 2 3 4 6]
[1 2 3 4 5]

Ref:http://criticalindirection.com/2016/02/17/slice-with-a-pinch-of-salt/

по ссылке:

Go использует более бережливый и ленивый подход при этом. Оно держит изменение одного и того же базового массива до тех пор, пока емкость среза достигнутый.

это сильно отличается от поведения фрагментов на других языках:

большинство языков, таких как Python, создают еще одну копию основной массив, когда любой из срезов, указывающих на него, делает запись.

выход пример говорилось, объясняет поведение.

Slice a len=7 cap=7 [0 0 0 0 0 0 0]
Slice b refers to the 2, 3, 4 indices in slice a. Hence, the capacity is 5 (= 7-2).
b := a[2:5]
Slice b len=3 cap=5 [0 0 0]

Modifying slice b, also modifies a, since they are pointing to the same underlying array.
b[0] = 9
Slice a len=7 cap=7 [0 0 9 0 0 0 0]
Slice b len=3 cap=5 [9 0 0]

Appending 1 to slice b. Overwrites a.
Slice a len=7 cap=7 [0 0 9 0 0 1 0]
Slice b len=4 cap=5 [9 0 0 1]

Appending 2 to slice b. Overwrites a.
Slice a len=7 cap=7 [0 0 9 0 0 1 2]
Slice b len=5 cap=5 [9 0 0 1 2]

Appending 3 to slice b. Here, a new copy is made as the capacity is overloaded.
Slice a len=7 cap=7 [0 0 9 0 0 1 2]
Slice b len=6 cap=12 [9 0 0 1 2 3]

Verifying slices a and b point to different underlying arrays after the capacity-overload in the previous step.
b[1] = 8
Slice a len=7 cap=7 [0 0 9 0 0 1 2]
Slice b len=6 cap=12 [9 8 0 1 2 3]

здесь, на последнем этапе проверки, кажется немного жутким, что любой модификация b больше не вызывает модификацию базового массив, на который указывает a. Логическим ожиданием было бы следующее: Когда b хиты предела, a и b оба указывают на одно и то же вновь выделенное основной массив, вместо того, чтобы продолжать указывать на более старый.

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