How to Concatenate Two or More Slices in Go
Slice concatenation in Go is easily achieved by leveraging the built-in
append()
function. It takes a slice (s1
) as its first argument, and all the
elements from a second slice (s2
) as its second. An updated slice with all
the elements from s1
and s2
is returned which may be assigned to a
different variable. Here’s an example:
package main
import "fmt"
func main() {
s1 := []string{"James", "Wagner", "Christene", "Mike"}
s2 := []string{"Paul", "Haaland", "Patrick"}
s3 := append(s1, s2...)
fmt.Println(s3)
}
[James Wagner Christene Mike Paul Haaland Patrick]
The three dots operator (...
) after the second argument is required because
append()
is a variadic function that accepts an unlimited number of
arguments.
s3 := append(s1, s2...)
The above line is basically a shorthand syntax for manually inputting each
element in s2
into the append()
function as follows:
s3 := append(s1, "Paul", "Haaland", "Patrick")
Side effects of slice concatenation with append()
It is important to point out that the append()
function does not always
create a new underlying array for the returned slice. If the capacity of its
first argument (s1
) is big enough to accommodate the elements from the second
(s2
), the result ()s3
) will share the same underlying array as s1
which
could have unintended side effects in your code.
Here’s a demonstration that should make this side effect clearer:
package main
import "fmt"
func main() {
s1 := make([]int, 2, 5) // s1 has capacity of 5
s1[0], s1[1] = 2, 3
s2 := []int{7, 8}
// a new underlying array is not created since the capacity of s1
// is big enough to accommodate the additional elements from s2
s3 := append(s1, s2...)
fmt.Println(s1, s3)
s3[0] = 5
fmt.Println(s1, s3)
}
[2 3] [2 3 7 8]
[5 3] [5 3 7 8]
Notice how changing the value of an element in s3
causes s1
to change as
well. This happens because they both share the same underlying array and it
could lead to subtle bugs that are hard to diagnose. You can prevent this issue
by ensuring that the slice returned by append()
is always backed by a new
underlying array regardless of the capacity of s1
. Here’s how:
package main
import "fmt"
func main() {
s1 := make([]int, 2, 5)
s1[0], s1[1] = 2, 3
s2 := []int{7, 8}
// s1 is resliced here such that its capacity is now equal to its length
// This guarantees that a new underlying array is always created for s3
s3 := append(s1[:len(s1):len(s1)], s2...)
fmt.Println(s1, s3)
s3[0] = 5
fmt.Println(s1, s3)
}
[2 3] [2 3 7 8]
[2 3] [5 3 7 8]
The technique here is to ensure that the capacity of the first argument to
append()
is the same as its length. This can be done by using a full slice
expression which allows you to
specify the maximum capacity of a reslicing operation.
The syntax of a full slice expression is as follows:
slice[low:high:max]
If low
is omitted, it defaults to 0
:
slice[:high:max]
// is equivalent to
slice[0:high:max]
The maximum capacity of the slice is the max
part. To make it equivalent to
the slice length, it should be equivalent to the length of the slice as shown
below:
s3 := append(s1[:len(s1):len(s1)], s2...)
Creating a generic slice concatenation function
If you’re using Go v1.18 or later, you can create a generic concatSlice()
function that uses append()
internally but guarantees that a new underlying
array is always created for the new slice. Here’s how:
func concatSlice[T any](first []T, second []T) []T {
n := len(first)
return append(first[:n:n], second...)
}
func main() {
s1 := []int{1, 2, 3}
s2 := []int{4, 5, 6}
s3 := concatSlice(s1, s2)
fmt.Println(s3)
}
In the concatSlice()
function the first
slice is resliced such that its
maximum capacity is equivalent to its length so that append()
will create a
new underlying array for the resulting slice.
[1 2 3 4 5 6]
Concatenating multiple slices at once
If you want to merge more than two slices at once, you can create a generic
function that uses the copy()
function as shown below. We aren’t use
append()
here as it can lead to many unnecessary allocations.
func concatMultipleSlices[T any](slices [][]T) []T {
var totalLen int
for _, s := range slices {
totalLen += len(s)
}
result := make([]T, totalLen)
var i int
for _, s := range slices {
i += copy(result[i:], s)
}
return result
}
func main() {
s1 := []int{1, 2, 3}
s2 := []int{4, 5, 6}
s3 := []int{7, 8, 9}
s4 := []int{10, 11, 12}
s5 := concatMultipleSlices([][]int{s1, s2, s3, s4})
fmt.Println(s5)
}
[1 2 3 4 5 6 7 8 9 10 11 12]
In the concatMultipleSlices()
function, the total length for all the slices
are calculated and used to pre-allocate the resulting slice. Secondly, the
elements of each slice are copied into the result
slice and the number of
elements copied is added to the counter (i
) such that the next iteration
starts from where the last one stopped. The resulting slice is subsequently
returned to the caller as observed above.
Conclusion
In this article, we discussed the concatenation of two or more slices in Go, and
considered a side effect of using the append()
function for this purpose
as well as its remedy. If you have any further insights regarding slice
concatenation in Go, feel free to leave a comment below.
Thanks for reading, and happy coding!