Go append function
last modified October 30, 2026
In this article we show how to append elements to slices in Go. The
append built-in function adds one or more elements at the end of a
slice and returns the updated slice.
The signature of the built-in is:
func append(slice []Type, elems ...Type) []Type
Because the returned slice may point to a different array than the input, the result must be stored, usually back into the variable that held the slice.
All examples in this article were compiled and run with Go 1.27.0.
Basic usage
The append function is variadic: it accepts one or more elements in
a single call. The example starts with an empty slice and grows it in four
steps.
package main
import "fmt"
func main() {
vals := []int{}
vals = append(vals, 1)
vals = append(vals, 2, 3)
vals = append(vals, 4, 5, 6)
vals = append(vals, 7, 8, 9, 10)
fmt.Println(vals)
}
Each call returns a slice with the new length, which is assigned back to
vals. The elements are added in the order in which they are
listed.
$ go run main.go [1 2 3 4 5 6 7 8 9 10]
Appending to a nil slice
A nil slice has length zero and no backing array, yet it is ready for use.
append allocates an array on the first call.
package main
import "fmt"
func main() {
var vals []int
fmt.Println(vals, vals == nil, len(vals))
vals = append(vals, 1, 2, 3)
fmt.Println(vals, vals == nil, len(vals))
}
A nil slice prints as [], exactly like an empty non-nil slice; only
the comparison with nil tells them apart. After the appends the
slice is no longer nil, and the nil check is useful for APIs that distinguish a
missing slice from an empty one.
$ go run main.go [] true 0 [1 2 3] false 3
Appending another slice
To append all elements of one slice to another, pass the source slice followed
by the ... operator. Without it, the call does not compile, because
a slice is not an element of the destination slice type.
package main
import "fmt"
func main() {
vals := []int{1, 2, 3}
res := []int{-1, 0}
res = append(res, vals...)
fmt.Println(res)
}
The ... unpacks the elements of vals, so the call is
equivalent to append(res, 1, 2, 3).
$ go run main.go [-1 0 1 2 3]
Appending bytes and strings
The special case append([]byte, string...) is allowed by the
language specification. It appends the bytes of the string to a byte slice.
package main
import "fmt"
func main() {
bytes := append([]byte("old "), "falcon"...)
fmt.Println(bytes)
fmt.Println(string(bytes))
}
The first line prints the numeric byte values, the second one the text they
represent. For building text, strings.Builder is usually clearer,
but byte slices are the natural choice for binary data.
$ go run main.go [111 108 100 32 102 97 108 99 111 110] old falcon
Length and capacity
append increases the length of the slice. The capacity is the size
of the underlying array; when the length would exceed it, Go allocates a larger
array and copies the existing elements.
package main
import "fmt"
func main() {
vals := []int{}
for i := 1; i <= 5; i++ {
vals = append(vals, i)
fmt.Println(len(vals), cap(vals))
}
}
Go does not document the exact growth factor, and the runtime may change it between releases. The capacities printed here are the ones Go 1.27 produced: the first append allocated room for four elements, and the array is replaced again when the fifth element is added.
$ go run main.go 1 4 2 4 3 4 4 4 5 8
Shared backing array
When there is enough capacity, append writes into the existing
array. A slice produced this way shares its elements with the original slice,
so changes through one header are visible through the other.
package main
import "fmt"
func main() {
a := make([]int, 3, 5)
b := append(a, 4)
b[0] = 99
fmt.Println(a)
fmt.Println(b)
fmt.Println(len(a), cap(a), len(b), cap(b))
}
The new element 4 fits into the spare capacity of a, so
b points to the same array. Writing 99 through b also
changes a[0], even though the length of a is still 3.
$ go run main.go [99 0 0] [99 0 0 4] 3 5 4 5
Reallocation on growth
Appending more elements than the remaining capacity forces a new array. The old slice keeps pointing to the old array, and the two slices become independent.
package main
import "fmt"
func main() {
a := make([]int, 3, 5)
b := append(a, 4)
c := append(b, 5, 6)
c[0] = 7
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
}
The third append needs six slots but only five are available, so c
is moved to a new array. Writing to c[0] no longer affects
a or b.
$ go run main.go [0 0 0] [0 0 0 4] [7 0 0 4 5 6]
Copying a slice with append
Appending to a nil slice of the same type is a compact way to copy a slice.
slices.Clone from the standard library does the same thing and
states the intent more clearly.
package main
import "fmt"
func main() {
a := []int{1, 2, 3}
b := append([]int(nil), a...)
b[0] = 99
fmt.Println(a)
fmt.Println(b)
}
The copy has its own backing array, so modifying b leaves
a unchanged. The append allocates exactly once, since the length
of the source is known.
$ go run main.go [1 2 3] [99 2 3]
Deleting an element
There is no built-in delete for slices. Removing the element at index
i means appending the part before i to the part after
it, which shifts the following elements one position to the left.
package main
import "fmt"
func main() {
a := []int{1, 2, 3, 4, 5}
i := 2
a = append(a[:i], a[i+1:]...)
fmt.Println(a)
fmt.Println(len(a), cap(a))
}
The third element, the value 3, is gone. The length drops to 4 but the capacity stays 5, and the old value remains in the last slot of the array. For slices of pointers or structs with pointers, that slot keeps the removed value reachable for the garbage collector.
$ go run main.go [1 2 4 5] 4 5
Deleting with slices.Delete
The slices.Delete function removes the elements in the half-open
range [i, j) and zeroes the freed slots.
slices.DeleteFunc removes the elements that satisfy a predicate.
package main
import (
"fmt"
"slices"
)
func main() {
a := []int{1, 2, 3, 4, 5}
a = slices.Delete(a, 1, 3)
fmt.Println(a)
a = slices.DeleteFunc(a, func(n int) bool { return n > 4 })
fmt.Println(a)
}
The first call removes indexes 1 and 2. The second one keeps only the values that are not greater than 4. Both functions return a slice that must be assigned back.
$ go run main.go [1 4 5] [1 4]
Pop operation
The pop operation removes the last element and returns it. Keep the element before shrinking the slice, because after reslicing it is no longer reachable through the slice.
package main
import "fmt"
func main() {
a := []int{1, 2, 3, 4, 5}
x := a[len(a)-1]
a = a[:len(a)-1]
fmt.Println(x)
fmt.Println(a)
}
The element 5 is returned, and the slice now has four elements. No allocation happens; only the length in the slice header changes.
$ go run main.go 5 [1 2 3 4]
Prepending an element
Prepending requires a new element at the front, which means copying the existing elements. The usual idiom appends the old slice to a one-element slice.
package main
import "fmt"
func main() {
a := []int{1, 2, 3, 4, 5}
a = append([]int{0}, a...)
fmt.Println(a)
}
The one-element slice has no spare capacity, so the append allocates a new array for the result. Prepending is therefore an O(n) operation.
$ go run main.go [0 1 2 3 4 5]
Inserting an element
An element can be inserted in the middle with two appends: the inner one creates a slice with the new element followed by the tail, and the outer one joins it to the head.
package main
import "fmt"
func main() {
a := []int{1, 2, 4, 5}
i := 2
e := 3
a = append(a[:i], append([]int{e}, a[i:]...)...)
fmt.Println(a)
}
The value 3 is placed at index 2, and the elements 4 and 5 move one position to
the right. The expression is easy to get wrong; slices.Insert
expresses the same operation without the nested calls.
$ go run main.go [1 2 3 4 5]
Inserting with slices.Insert
slices.Insert inserts the given values at index i and
returns the updated slice. It panics when the index is out of range.
package main
import (
"fmt"
"slices"
)
func main() {
a := []int{1, 2, 4, 5}
a = slices.Insert(a, 2, 3)
fmt.Println(a)
}
The result is identical, and the intent is visible at a glance. Several values can be inserted in one call.
$ go run main.go [1 2 3 4 5]
The result must be assigned
append belongs to the built-in functions that are not permitted in
statement context. Calling it without using the result is a compile-time error,
because the new length would be lost.
package main
import "fmt"
func main() {
a := []int{1, 2, 3}
append(a, 4)
fmt.Println(a)
}
The compiler rejects the call before the program can run. This protects against
the common mistake of expecting append to modify the slice in
place.
$ go run main.go # command-line-arguments ./main.go:9:2: append(a, 4) (value of type []int) is not used
Appending structs
append copies its arguments. For a struct, the copy is a value
copy: changing the appended element does not modify the value that was passed
in, and vice versa.
package main
import "fmt"
type Point struct {
X int
Y int
}
func main() {
points := []Point{}
points = append(points, Point{1, 2})
points = append(points, Point{3, 4})
points[1].Y = 40
fmt.Println(points)
}
The fields of the second point are updated through the slice. Since the slice holds the structs themselves, the change is stored in the backing array and appears in the printed output.
$ go run main.go
[{1 2} {3 40}]
Slices of slices and slices.Concat
A slice of slices is appended like any other slice: the element type is
[]int, so each appended value is a slice.
slices.Concat concatenates several slices in one call.
package main
import (
"fmt"
"slices"
)
func main() {
rows := [][]int{{1, 2}}
rows = append(rows, []int{3, 4})
fmt.Println(rows)
all := slices.Concat([]int{1}, []int{2, 3}, nil)
fmt.Println(all)
}
The first append adds a whole row. slices.Concat accepts any
number of slices, including nil ones, and returns a new slice with all their
elements.
$ go run main.go [[1 2] [3 4]] [1 2 3]
Source
In this article we have shown how to use the append built-in
function: adding elements and slices, capacity growth and shared arrays,
copying, deleting, popping, prepending and inserting elements, and the
slices helpers that replace several manual idioms.
Author
List all Go tutorials.