在Go中连接两个切片

812

我想要把切片 [1, 2] 和切片 [3, 4] 合并起来。在Go语言中我该如何实现?

我尝试了:

append([]int{1,2}, []int{3,4})

但是得到了:

cannot use []int literal (type []int) as type int in append

然而,文档似乎表明这是可能的,但我错过了什么吗?

slice = append(slice, anotherSlice...)
10个回答

1475
在第二个切片后添加点号:
//                           vvv
append([]int{1,2}, []int{3,4}...)

这就像任何其他的可变参数函数一样。

func foo(is ...int) {
    for i := 0; i < len(is); i++ {
        fmt.Println(is[i])
    }
}

func main() {
    foo([]int{9,8,7,6,5}...)
}

69
append()是一个可变参数函数,而...则允许你从切片中传递多个参数给可变参数函数。 - user1106925
26
当切片很大时,这个方法的表现如何?编译器是否真正将所有元素作为参数传递? - Toad
28
@Toad:它实际上并不会使它们分散。在上面的foo()示例中,is参数保存原始切片的副本,也就是说它具有对相同底层数组、长度和容量的轻量引用的副本。如果foo函数改变了一个成员,更改将在原始数据上被看到。这里有一个演示。因此,唯一真正的开销是当你没有一个切片时,它会创建一个新的切片,比如:foo(1, 2, 3, 4, 5),它将创建一个新的切片,is将持有它。 - user1106925
3
如果我理解正确的话,可变参数函数实际上是像参数数组一样实现的(而不是每个参数都在堆栈上)?由于您传入了切片,因此它实际上是一对一映射的吗? - Toad
1
@Toad:是的,当你在现有的切片上使用...时,它只是传递该切片。当你传递单个参数时,它会将它们收集到一个新的切片中并传递它。我没有第一手的了解确切的机制,但我猜测这样做:foo(1, 2, 3, 4, 5)和这样做:func foo(is ...int) {只是简化为这样做:foo([]int{1, 2, 3, 4, 5})和这样做:func foo(is []int) { - user1106925

108

Appending to and copying slices

The variadic function append appends zero or more values x to s of type S, which must be a slice type, and returns the resulting slice, also of type S. The values x are passed to a parameter of type ...T where T is the element type of S and the respective parameter passing rules apply. As a special case, append also accepts a first argument assignable to type []byte with a second argument of string type followed by .... This form appends the bytes of the string.

append(s S, x ...T) S  // T is the element type of S

s0 := []int{0, 0}
s1 := append(s0, 2)        // append a single element     s1 == []int{0, 0, 2}
s2 := append(s1, 3, 5, 7)  // append multiple elements    s2 == []int{0, 0, 2, 3, 5, 7}
s3 := append(s2, s0...)    // append a slice              s3 == []int{0, 0, 2, 3, 5, 7, 0, 0}

Passing arguments to ... parameters

If f is variadic with final parameter type ...T, then within the function the argument is equivalent to a parameter of type []T. At each call of f, the argument passed to the final parameter is a new slice of type []T whose successive elements are the actual arguments, which all must be assignable to the type T. The length of the slice is therefore the number of arguments bound to the final parameter and may differ for each call site.

你的问题的答案可以在Go编程语言规范中找到,例如s3 := append(s2, s0...)。举个例子,

s := append([]int{1, 2}, []int{3, 4}...)

14
注意:对我来说,一般使用append(slice1, slice2...)似乎非常危险。如果slice1是较大数组的一个切片,那么该数组的值将被slice2覆盖。(这让我感到不安的是,这似乎不是普遍关注的问题?) - Hugo
9
如果你“交出”数组的一部分,那么要知道这个切片“所有者”将能够查看/覆盖超出当前切片长度的数组部分。如果不想让这种情况发生,可以使用一个完整的切片表达式(形式为a[low:high:max]),它还指定了最大容量。例如,切片a[0:2:4]的容量为4,即使支持该切片的数组在此之后有一千个元素,也不能重新切片以包括超过它的元素。 - icza

67

我想强调@icza的答案并稍微简化一下,因为这是一个关键的概念。我假设读者已经熟悉了切片

c := append(a, b...)

这是对问题的有效回答。 但是,如果您需要在稍后的不同上下文中使用'slices' a'和'c',那么这不是连接'slices'的安全方式。

为了说明,让我们不用'slices'的术语来阅读表达式,而是用底层数组的术语:

 

"取(基础)数组'a'并将元素从数组'b'附加到其中。如果数组'a'具有足够的容量,以包括来自'b'的所有元素-基础数组'c'将不是一个新数组,它实际上将是数组'a'。基本上,'slice' 'a'将显示长度为a的元素底层数组'a',而'slice' 'c'将显示长度为c的数组'a'的元素。"

append()不一定会创建新数组!这可能会导致意外的结果。请参见 Go Playground示例

如果要确保为'slice'分配新数组,请始终使用make()函数。例如,以下是一些丑陋但足够高效的选项。

la := len(a)
c := make([]int, la, la + len(b))
_ = copy(c, a)
c = append(c, b...)

la := len(a)
c := make([]int, la + len(b))
_ = copy(c, a)
_ = copy(c[la:], b)

1
感谢您指出这些副作用。与此修改后的情景形成了惊人的对比。https://play.golang.org/p/9FKo5idLBj4 虽然在提供过量容量时,应该仔细考虑这些令人困惑的副作用,以免违背合理的直觉。 - olippuner
谢谢Joo,我花了近两个小时在代码中寻找问题,原因是我没有遵循你所说的关于不安全地连接两个稍后要使用的切片的指南(也许可以在这份文档中包含此警告:https://blog.golang.org/slices)。感谢你提供的代码片段,它看起来非常精美! - Victor
1
这应该是被接受的答案。记住,总是将append的输出保存到与第一个参数相同的变量中,像这样:a := append(a, b...) - Chris

57

不是针对其他答案,但我发现在文档中提供的简要解释比其中的示例更易于理解:

func append

func append(slice []Type, elems ...Type) []Type The append built-in function appends elements to the end of a slice. If it has sufficient capacity, the destination is resliced to accommodate the new elements. If it does not, a new underlying array will be allocated. Append returns the updated slice. It is therefore necessary to store the result of append, often in the variable holding the slice itself:

slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)

As a special case, it is legal to append a string to a byte slice, like this:

slice = append([]byte("hello "), "world"...)

2
谢谢!对我很有价值! - Korjavin Ivan
希望这是最佳答案! - MrR

39

我认为指出并了解以下内容很重要:如果目标切片(您要追加的切片)具有足够的容量,则追加操作将在“原地”进行,通过重新切片目标(重新切片以增加其长度以容纳可追加元素)。

这意味着,如果目标是通过从更大的数组或切片中切分而来,而该数组或切片具有超出所得切片长度之外的其他元素,则这些元素可能会被覆盖。

请看以下示例以说明:

a := [10]int{1, 2}
fmt.Printf("a: %v\n", a)

x, y := a[:2], []int{3, 4}
fmt.Printf("x: %v, y: %v\n", x, y)
fmt.Printf("cap(x): %v\n", cap(x))

x = append(x, y...)
fmt.Printf("x: %v\n", x)

fmt.Printf("a: %v\n", a)

输出结果(在Go Playground上试一试):

a: [1 2 0 0 0 0 0 0 0 0]
x: [1 2], y: [3 4]
cap(x): 10
x: [1 2 3 4]
a: [1 2 3 4 0 0 0 0 0 0]
我们创建了一个长度为10的"backing"数组a,然后通过对这个a数组进行切片来创建x目标切片,使用复合字面量[]int{3, 4}来创建y切片。现在当我们将y追加到x时,结果是期望的[1 2 3 4],但令人惊讶的是,背后的数组a也发生了变化,因为x的容量是10,足以将y追加到其中,所以x会被重新切片,并且也会使用相同的a作为背后的数组,append()函数会将y中的元素复制到那里。
如果您想避免这种情况,可以使用完整的切片表达式(full slice expression),其形式如下:
a[low : high : max]

通过设置 max - low,构建一个切片并控制结果切片的容量。

请参阅修改后的示例(唯一的区别是我们像这样创建 x = a[:2:2]

a := [10]int{1, 2}
fmt.Printf("a: %v\n", a)

x, y := a[:2:2], []int{3, 4}
fmt.Printf("x: %v, y: %v\n", x, y)
fmt.Printf("cap(x): %v\n", cap(x))

x = append(x, y...)
fmt.Printf("x: %v\n", x)

fmt.Printf("a: %v\n", a)

输出结果(在Go Playground上尝试)

a: [1 2 0 0 0 0 0 0 0 0]
x: [1 2], y: [3 4]
cap(x): 2
x: [1 2 3 4]
a: [1 2 0 0 0 0 0 0 0 0]

正如您所看到的,我们得到了相同的x结果,但支撑数组a并没有改变,因为x的容量仅为2(感谢完整切片表达式a[:2:2])。因此,要进行追加操作,需要分配一个新的支撑数组来存储xy的元素,这与a不同。


3
非常感谢,这对我面临的问题很有帮助。 - Aidy
谢谢,非常有用 - 不过,如果支持数组足够短以适应新值,那么所示行为是否仅会发生?例如,在您的示例中,如果y的长度为20,那么a是否会保持不变? - patrick
@patrick 如果没有足够的空间进行追加,append() 会分配一个新的支持数组,将旧内容复制到其中,并在新的支持数组上执行追加操作,同时保留旧数组不变。尝试一下有多难呢?Go Playground - icza

9

append()函数和spread操作符

使用标准golang库中的append方法,可以将两个切片连接起来。这类似于可变参数函数操作。因此我们需要使用...

package main

import (
    "fmt"
)

func main() {
    x := []int{1, 2, 3}
    y := []int{4, 5, 6}
    z := append([]int{}, append(x, y...)...)
    fmt.Println(z)
}

上述代码的输出结果是:[1 2 3 4 5 6]。

5
我不确定为什么你不直接使用z := append(x, y...) - user12817546

7

连接两个切片,

func main() {
    s1 := []int{1, 2, 3}
    s2 := []int{99, 100}
    s1 = append(s1, s2...)

    fmt.Println(s1) // [1 2 3 99 100]
}

向切片追加单个值

func main() {
    s1 :=  []int{1,2,3}
    s1 := append(s1, 4)
    
    fmt.Println(s1) // [1 2 3 4]
}

要将多个值附加到一个切片中

func main() {
    s1 :=  []int{1,2,3}
    s1 = append(s1, 4, 5)
    
    fmt.Println(s1) // [1 2 3 4]
}

5
似乎泛型是一个完美的选择(如果使用1.18版本或更高版本)。
func concat[T any](first []T, second []T) []T {
    n := len(first);
    return append(first[:n:n], second...);
}

2
append已经是“通用”的了,因此人们可能认为这不是类型参数的必要用例,但是使用三索引切片表达式:n:n来减少第一个切片的容量的非显而易见的用法确实是一种改进。 - blackgreen

3

append([]int{1,2}, []int{3,4}...)可以正常工作。将参数传递给 ... 参数。

如果函数f是变参函数,最后一个参数为p,类型为...T,那么在函数f内部,p的类型等价于[]T类型。

如果调用函数f时没有实际参数传给p,则传递给p的值为nil

否则,传递的值是一个新的[]T类型的切片,其底层数组为连续的实际参数,所有这些实参都必须可分配给T。因此,该切片的长度和容量是绑定到p的参数数量,并且可能在每个调用站点上不同。

给出函数和函数调用:

func Greeting(prefix string, who ...string)
Greeting("nobody")
Greeting("hello:", "Joe", "Anna", "Eileen")

0
从Go 1.22(2024年第一季度)开始,你可能会考虑使用新的func Concat[S ~[]E, E any](slices ...S) S泛型函数。
请参见提交 2fd195,该提交修复了问题 56353
// Join slices into a new slice
a := []int{ 1, 2, 3 }
b := []int{ 4, 5, 6 }
c := slices.Concat(nil, a, b) 
// c == int{ 1, 2, 3, 4, 5, 6 }

s := [][]int{{1}, nil, {2}}
c = slices.Concat(s...)
// c == int{1, 2}

这是在"Go 1.22的新API变更" (issue 64343)之后进行的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接