ZetCode

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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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

Go builtin append - reference

Go slices package - reference

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Go tutorials.