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)
}
output
[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)
}
output
[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)
}
output
[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.

output
[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)
}
output
[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!