Golang Project Structure

Tips and tricks for writing and structuring Go code

How to Concatenate Slices

Language

  • unknown

by

Slices are one of the most important data structures used in Go. In this post, we will look at how to concatenate slices. We will start by explaining the general concept of concatenation, then we will look at examples of concatenating two or more slices in Go. Finally, we will build functions that can help us to concatenate slices more easily.

What Is Concatenation?

The word catena in Latin means “chain”, so if something is concatenated, it is literally “chained together” or “tied together”. The word concatenation is sometimes seen in literary texts with this meaning.

Many columns of linked metal chains. This is intended as a visual illustration of the concept of concatenation.
Each of the links in these chains is bound together with two others, one above and one below.
You can, therefore, say that the links are concatenated.

For example, the classic English author Thomas Hardy uses the word in his 19th-century novel The Mayor of Casterbridge (available online at Project Gutenberg), when describing the title character’s reaction to his estranged wife’s death:

Henchard, like all his kind, was superstitious, and he could not help thinking that the concatenation of events this evening had produced was the scheme of some sinister intelligence bent on punishing him.

However, in programming and computer science, concatenation specifically refers to the process of creating a single value from two (or more) values of the same underlying type or data structure.

For example, if we combine the strings "Hello, " and "world!" together to create a single string, "Hello, world!", then it can be said that we have concatenated the strings.

The term concatenation is sometimes shortened to concat, especially colloquially or in variable/function names. For example, the Javascript method that adds one string to another is known as concat, as you can see in the following short snippet of JS code:

const slang = "concat";

console.log(slang.concat("enation")); // prints "concatenation" to the console

This post will consider various ways to concatenate slices, which are a form of dynamic array commonly used in Go. Just as when we concatenate strings, we add all of the characters that make up one string onto the end of another string, so when we concatenate slices, we add of the elements in that slice onto the end of another slice.

Of course, we will look at some examples using Go code below, which will focus on the practicalities involved.

Concatenating Two Slices, One to Another

As we previously saw in the post about adding elements to a slice, the builtin append function makes it possible for us to add elements onto the end of an existing slice, automatically growing its capacity if necessary.

We can use this to concatenate two slices, appending the elements of one onto the end of another, as in the example below:

package main

import "fmt"

func main() {
	sliceOne := []int{1, 2, 3, 4}
	sliceTwo := []int{5, 6, 7, 8}

	sliceOne = append(sliceOne, sliceTwo...)

	fmt.Println(sliceOne)
}

We use the ellipsis (three dots) to unpack the elements of sliceTwo, so that each can be considered as a separate argument in the append function. When you run the code above, you will see that sliceOne now contains all eight integers.

We haven’t modified sliceTwo, so it will still contain the four integers that it did originally.

Concatenating Two Slices to a New Slice

We did, however, mutate sliceOne by appending four more elements to it. That may be okay, but there may also be times when you want to preserve both slices but concatenate their elements onto a third slice that is created specifically for the purpose. We do this below:

package main

import "fmt"

func main() {
	sliceOne := []int{1, 2, 3, 4}
	sliceTwo := []int{5, 6, 7, 8}
	sliceThree := make([]int, 0, len(sliceOne)+len(sliceTwo))

	sliceThree = append(sliceThree, sliceOne...)
	sliceThree = append(sliceThree, sliceTwo...)

	fmt.Println(sliceThree)

}

You can see how we initialize sliceThree with a capacity large enough to hold all elements, by adding the length of sliceOne to the length of sliceTwo.

We then simply append the elements of sliceOne and sliceTwo in turn.

When we print out sliceThree, we can see that it contains all eight of the elements. More importantly, we have not had to modify either of the two original slices, meaning that we could have continued using them as they are, if he had other code that relied on them.

Creating a Generic Function to Concatenate Two Slices

Now that we know how to concatenate slices, let’s create a function so that we can call it whenever we need to perform this operation:

package main

import "fmt"

func ConcatenateTwoSlices[T any](sliceOne []T, sliceTwo []T, createSlice bool) []T {
	var sliceThree []T

	if createSlice {
		sliceThree = make([]T, 0, len(sliceOne)+len(sliceTwo))
		sliceThree = append(sliceThree, sliceOne...)
	} else {
		sliceThree = sliceOne
	}

	sliceThree = append(sliceThree, sliceTwo...)

	return sliceThree

}

func main() {
	sliceOne := []int{1, 2, 3, 4}
	sliceTwo := []int{5, 6, 7, 8}
	sliceThree := ConcatenateTwoSlices(sliceOne, sliceTwo, true)

	fmt.Println(sliceThree)

}

The ConcatenateTwoSlice function uses exactly the same logic that we saw in our two previous examples above.

If the createSlice parameter is set to true, a third slice will be created, onto which the first two slices will be concatenated. If, on the other hand, it is set to false, the elements of sliceTwo will simply be be added onto the end of sliceOne.

The function uses generic syntax in order to accept slices of any type. However, all slice arguments must be of the same type (denoted here by T). It is not possible to concatenate a slice of strings onto a slice of integers, for example, because the compiler will not convert them automatically. If you try, you’ll get an error that looks something like this:

type []string of sliceOfStrings does not match inferred type []int for []T

Improving Our Generic Function to Concatenate Many Slices

Finally, let’s create a more general function that can concatenate any number of source slices to a destination slice:

package main

import "fmt"

func ConcatenateSlices[T any](createSlice bool, destination []T, sources ...[]T) []T {
	var result []T

	if createSlice {
		resultCap := len(destination)

		for _, s := range sources {
			resultCap += len(s)
		}

		result = make([]T, 0, resultCap)
		result = append(result, destination...)
	} else {
		result = destination
	}

	for _, s := range sources {
		result = append(result, s...)
	}

	return result
}

func main() {
	sliceOne := []int{1, 2, 3, 4}
	sliceTwo := []int{5, 6, 7, 8}
	sliceThree := []int{9, 10, 11, 12}
	sliceFour := ConcatenateSlices(true, sliceOne, sliceTwo, sliceThree)

	fmt.Println(sliceFour)

}

In the ConcatenateSlices function above, if the createSlice option is set to true, the destination will simply be treated as another source to be added to the result slice. Otherwise, the destination will be mutated by adding all of the elements from each of the source slices to it.

Note that we have moved createSlice to be the first argument, rather than the last, because it isn’t possible to have another argument that follows a variadic argument, as sources now is.

We now have two loops, if createSlice is true, because we use an initial loop to calculate the total length of all our source slices, so that we can initialize the result slice with sufficient capacity.

However, this length-counting loop isn’t strictly necessary, since there’s no need to set an initial capacity. The append function will always grow a slice, ensuring that there’s enough room for new elements to be added. Even so, it is better to do it this way because creating the slice with enough capacity to begin with does mean that multiple memory allocations for the slice won’t have to occur, only a single one, so there may be some slight performance gains.

Leave a Reply

Your email address will not be published. Required fields are marked *