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:

go
func Concat[S ~[]E, E any](slices ...S) S

Consider the following example:

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

go
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]

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:

go
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]

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:

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

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

go
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]

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!