Concatenating several slices with repeated append calls can make ownership depend on spare capacity in the destination. slices.Concat takes a different contract: it returns a new slice containing all input elements in order. That makes the storage boundary explicit when a combined result must stand apart from its inputs.
Added in Go 1.22, slices.Concat also defines the empty case precisely. If the total concatenation has no elements, the result is nil, even when one or more arguments are non-nil empty slices.
Concat creates a separate result
The function accepts any number of slices sharing the same slice type:
func Concat[S ~[]E, E any](slices ...S) SA basic call preserves input order:
package main
import (
"fmt"
"slices"
)
func main() {
left := []string{"api", "worker"}
middle := []string{"cron"}
right := []string{"admin", "metrics"}
all := slices.Concat(left, middle, right)
fmt.Println(all)
}The output is:
[api worker cron admin metrics]The returned slice does not reuse an input slice as its result storage. Changing an element in the result therefore does not change the corresponding element in an input slice.
left := []int{10, 20}
right := []int{30, 40}
combined := slices.Concat(left, right)
combined[0] = 99
fmt.Println(left) // [10 20]
fmt.Println(combined) // [99 20 30 40]This differs from building a result with append(left, right...). An append may reuse left’s backing array when capacity permits, so the returned slice can share storage with left. That behavior is useful in code that intentionally extends an owned buffer, but it gives a different ownership contract.
The empty result is nil
Concat returns nil when the sum of all input lengths is zero.
var a []int
b := []int{}
result := slices.Concat(a, b)
fmt.Println(result == nil) // true
fmt.Println(len(result)) // 0The same rule applies with no arguments:
result := slices.Concat[[]byte]()
fmt.Println(result == nil) // trueThis distinction can matter at boundaries that represent nil and non-nil empty slices differently. Code that requires a non-nil empty slice should normalize the result explicitly rather than relying on an input slice’s nilness.
Named slice types are preserved
The type parameter is constrained with S ~[]E, so a named slice type can pass through the operation without conversion.
type Paths []string
base := Paths{"/health", "/ready"}
extra := Paths{"/metrics"}
routes := slices.Concat(base, extra)
fmt.Printf("%T %v\n", routes, routes)The result has type Paths, not []string. This keeps methods and other type-level distinctions attached to a named slice type.
Arguments in one call still need to satisfy the same inferred slice type. If an application holds values under different named slice types, an explicit conversion can establish the intended common type before concatenation.
Allocation is tied to the complete result
The standard-library implementation computes the total length before appending the input slices into fresh result storage. As a result, Concat can size the destination around the complete concatenation instead of depending on incremental capacity growth from an arbitrary first slice.
That contract is more significant than replacing a short sequence of append calls. Consider a helper that combines caller-owned fragments:
func packet(parts ...[]byte) []byte {
return slices.Concat(parts...)
}Callers can retain and mutate their original slice elements without those writes targeting the result’s backing array. The result is also independent of spare capacity hidden in any individual argument.
Element independence is separate from object independence. For a slice of pointers, maps, slices, or other reference-bearing values, Concat copies the elements themselves. It does not recursively copy data reached through those elements.
type Item struct {
Count int
}
item := &Item{Count: 1}
combined := slices.Concat([]*Item{item})
combined[0].Count = 2
fmt.Println(item.Count) // 2The outer slice storage is separate; both slices still contain the same pointer.
Concat fits ownership boundaries
For a single source slice, slices.Clone states the intent more directly. For an existing destination that is meant to grow, append remains the natural operation. slices.Concat fits the case where several slices form one new value and the result should not depend on capacity or backing storage from any input.
That storage contract also makes refactoring safer. Reordering which fragment appears first cannot silently change whether the result aliases a caller-owned backing array.
The useful boundary is simple: slices.Concat copies a sequence of slice elements into a new outer slice, not the object graph behind those elements. Code that needs deeper isolation still has to define and perform that copy at the element level.