How to Concatenate Two or More Slices in Go
Starting from Go 1.22, concatenating slices is straightforward with the
introduction of the slice.Concat()
method. This method allows for the
combination of multiple slices into a single new slice. The method signature is
as follows:
func Concat[S ~[]E, E any](slices ...S) S
Consider the following example:
package main
import (
"fmt"
"slices"
)
func main() {
s1 := []string{"James", "Wagner", "Christene", "Mike"}
s2 := []string{"Paul", "Haaland", "Patrick"}
s3 := []string{"Peter", "Mark", "Luke"}
s4 := slices.Concat(s1, s2, s3)
fmt.Println(s4)
}
[James Wagner Christene Mike Paul Haaland Patrick Peter Mark Luke]
Before Go 1.22
Before Go version 1.22, concatenating slices was typically achieved using the
append()
function which allows for the combination of two slices into one, by
taking the first slice as the initial argument and then appending elements from
the second slice using the variadic syntax:
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]
However, this method may not always result in a new underlying array, which can lead to shared underlying arrays and unintended side effects.
This behavior is particularly noticeable when the first slice (s1
) has enough
capacity to include the elements from the second slice (s2
). In such cases,
appending s2
to s1
does not lead to the allocation of a new array; instead,
s3
shares its underlying array with s1
, which can lead to unexpected
mutations in your program.
Consider the following example for clarity:
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]
Modifying s3
inadvertently alters s1
as well, demonstrating the shared array
issue. This problem can be circumvented by ensuring that the append()
operation results in a slice with a new underlying array, regardless of s1
’s
initial capacity.
This is achievable through re-slicing s1
with its capacity set to its length,
thus forcing append()
to allocate a new array for s3
:
package main
import "fmt"
func main() {
s1 := make([]int, 2, 5)
s1[0], s1[1] = 2, 3
s2 := []int{7, 8}
// s1 is re-sliced 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]
By employing a
full slice expression
(slice[low:high:max]), where max
is set to the slice’s length, we ensure that
appending elements results in a slice backed by a distinct array. This method
prevents the shared array issue, allowing for safer modifications to the
resulting slice without affecting the original.
Creating a generic slice concatenation function
If you’re using Go version that supports generics (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)
}
[1 2 3 4 5 6]
In this concatSlice()
function, the capacity of the first slice is tailored to
its length, prompting append()
to allocate a new array for the merged slice.
Concatenating multiple slices at once
If you want to merge more than two slices at once on a Go version earlier than
1.22, you can create a generic function that uses the built-in copy()
function
as shown below. This approach avoids the potential for excessive memory
allocations that append()
might introduce:
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]
The concatMultipleSlices()
function calculates the combined length of all
slices to allocate memory efficiently. It then sequentially copies elements from
each slice into the preallocated result slice, ensuring a seamless merging
process. This approach guarantees minimal memory allocation and efficient
execution, providing a powerful tool for slice manipulation in Go.
Final thoughts
This guide explored slice concatenation in Go, addressing the append()
function’s potential side effects and offering solutions. For additional
insights or thoughts on slice concatenation, feel free to leave a comment below.
Thanks for reading, and happy coding!