Creating Slices

Learn how to create slices using literals, the make function, and slicing existing arrays or slices

A slice in Go is the workhorse for sequences of data. Before you can use one, you have to bring it into existence. There are three direct paths: write a slice literal, call the built-in make function, or derive a slice from an existing array or slice. Each path solves a different starting problem, and knowing when to reach for each one is half the skill of using slices well.

Creating Slices with Literals

A slice literal is the most direct way to create a slice with a known set of initial elements. It looks almost identical to an array literal, but the length is absent from the type.

// slice literal
names := []string{"alice", "bob", "charlie"}
// array literal for comparison
arr := [3]string{"alice", "bob", "charlie"}

The Go compiler sees []string (no number between the brackets) and knows you want a slice. It allocates an underlying array just big enough to hold the three strings, sets the slice’s length and capacity to 3, and populates the elements. The result is a fully usable slice with no extra ceremony.

You can also write a literal that produces an empty slice — a slice with length zero that is not nil — by using the empty braces:

empty := []int{}

This is different from a nil slice (var s []int), which we’ll cover in a moment. An empty slice marshals to an empty JSON array, while a nil slice marshals to null. If you need to distinguish the two, reach for a literal like []int{} when you want len(s) == 0 but s != nil.

Slice literals create a backing array:

Even a short literal like []int causes an array allocation behind the scenes. The slice you get is a lightweight descriptor — pointer, length, capacity — that points to that array.

Common Mistake: Thinking You Must Provide All Elements Up Front

A literal is for when you already know the initial contents. If you don’t, you’re better off using make or a nil slice with append. Writing a literal full of zeros just to fill it later is a sign you should probably be reaching for make instead.

Literal with no elements vs nil:

var s []int gives you nil. s := []int gives you an empty, non-nil slice. Use the former when the slice might never hold data; use the latter when you need an empty value that is not nil, for example to encode an empty JSON array.

Creating Slices with make

When you know how many elements you need but not their values, or you want to pre-allocate capacity for future growth, make is the tool. Its signature is:

make([]T, length, capacity)

length is the number of elements the slice will initially contain — the range you can index right after creation. capacity is the size of the underlying array. If you omit capacity, it defaults to the same value as length.

// 5-element slice of ints, all zero, len=5, cap=5
a := make([]int, 5)
// 0-element slice with room to grow to 5 without reallocation
b := make([]int, 0, 5)

The first form, make([]int, 5), gives you a slice where a[0] through a[4] already exist and are set to the zero value for int. You can read and write those positions immediately. The second form, make([]int, 0, 5), creates a slice of length 0 but with an underlying array of 5 integers already allocated. It’s ready to receive elements via append without triggering a new allocation until the length exceeds 5.

Verify a successful make:

After s := make([]byte, 3, 10), printing len(s) (3), cap(s) (10), and the slice contents ([0 0 0]) confirms that the slice is correctly initialized and ready.

Why make Exists

You could, in theory, always start with a nil slice and let append handle allocation. But append may reallocate multiple times as the slice grows, copying elements each time. By using make with a capacity that approximates the final size, you give the runtime a hint that reduces those reallocations. This is a pragmatic performance concern in hot paths.

How Beginners Should Think About It

Think of make as ordering a container. You tell Go: “I need a box that can hold up to 10 widgets, but right now it has 3 widgets in it (all zeroed out).” The box exists, the slots are ready, and you can start using the first 3 slots immediately or fill more later.

Common Mistake: Indexing Beyond Length After make

make([]int, 0, 5) gives you a slice with length 0 and capacity 5. The underlying array has room for 5 elements, but the slice’s length is 0, so indexing s[0] panics. You must use append to grow the length into the capacity:

s := make([]int, 0, 5)
// s[0] = 7  // panic: index out of range [0] with length 0
s = append(s, 7) // now len=1, cap=5

Length and capacity are separate controls:

Accessing an element at an index greater than or equal to the slice’s length causes a runtime panic, even if capacity is larger. Always check length before indexing.

Creating Slices by Slicing Arrays or Other Slices

A slice doesn’t always own its backing array. You can create a new slice that points to a segment of an existing array or slice using the slice expression:

arr := [5]int{10, 20, 30, 40, 50}
s := arr[1:4]  // s contains [20, 30, 40]

arr[1:4] produces a slice whose underlying array is the same array backing arr. The slice’s length is 4 - 1 = 3, and its capacity is the distance from the start index to the end of the array, in this case 5 - 1 = 4. The new slice sees elements at indices 1,2,3 of the original array through its own indices 0,1,2.

You can slice another slice, too:

original := []string{"a", "b", "c", "d", "e"}
window := original[2:4]  // ["c", "d"]

Because the new slice shares storage with the original, modifying an element through one slice is visible through the other:

window[0] = "Z"
fmt.Println(original) // [a b Z d e]

This sharing is by design. It avoids copying, making operations like extracting a sub‑range essentially free.

Slicing does not copy data:

A slice created by slicing an array or another slice points to the same memory. If you later modify elements, those changes appear in all slices that share that region of the underlying array. If you need independent copies, use copy after creating the slice.

Default Bounds

The low and high indices in a slice expression can be omitted. Omitting the low index defaults to 0; omitting the high index defaults to the length of the operand.

s := []int{1,2,3,4,5}
firstThree := s[:3]   // [1,2,3]
fromTwo     := s[2:]   // [3,4,5]
copyWhole   := s[:]    // [1,2,3,4,5] — same backing array

These shorthands are common when you want the front, back, or entire slice.

Creating a Slice from an Array

Any array can be turned into a slice with a full‑range slice expression. This is the standard way to hand an array off to a function that expects a slice:

arr := [4]float64{3.14, 2.71, 1.61, 0.0}
s := arr[:]
// s is []float64 pointing to arr's storage

After this, arr and s share the same memory. Changes through s are reflected in arr, and vice versa.

Common Mistake: Thinking the Slice Is Independent

After slicing, people often treat the new slice as if it owns its data. If you later append to the original slice and the capacity is still within bounds, the new slice’s elements may be altered. Even worse, if the original slice outlives your “view,” the entire backing array stays in memory because the slice keeps a reference to it, potentially holding onto far more data than you need. Use a full copy when you want to sever that link.

Nil and Empty Slices

When you declare a slice variable without initializing it, you get a nil slice:

var s []int
fmt.Println(s == nil) // true
fmt.Println(len(s))    // 0

A nil slice has no underlying array, but you can still call append on it — append treats a nil slice as a zero‑length slice with zero capacity. Many functions that return slices use nil to mean “no data,” and that’s idiomatic.

An empty slice (created with []int{} or make([]int, 0)) is not nil, but its length is also 0. Both are usable in range loops and with len, but they differ in equality checks and JSON encoding.

Use nil to signal absence:

When you write a function that might return no results, returning nil is the conventional way to say “nothing found.” An empty slice signals that there is a result, but it happens to have no elements. Stick to nil when you want to convey absence.

Summary

The method you choose to create a slice shapes how you think about its lifecycle. Use a literal when the initial values are known at compile time and are part of the logic. Use make when you know the eventual size or want to control allocation granularity. Use slice expressions when you need a window into existing data without copying.

Nil slices let you defer allocation entirely; empty slices let you assert existence while still holding nothing. Each of these creation paths produces a value that works with len, cap, append, and copy — the key difference is what sits behind the descriptor and how much control you keep over the underlying array.