AlgoMaster Logo

Creating Slices (make, literals, slicing)

Last Updated: May 22, 2026

High Priority
7 min read

A slice in Go can be built in several ways, and the form you pick affects readability, performance, and whether the slice is nil or empty out of the gate. This lesson walks through every way to create a slice, from the simplest literal to slicing an existing array or slice, and shows when each form is the right choice. By the end, the choice between each form should be clear, and why pre-sizing with make matters for performance.

Slice Literals

The shortest way to create a slice with values already in it is a slice literal. The syntax is the element type wrapped in square brackets, followed by the values in braces.

[]string{"apple", "milk", "bread"} tells the compiler two things at once: the element type is string, and the slice starts with these three values. Go allocates a backing array of length 3, stores the values, and gives you a slice header that points at it with len == 3 and cap == 3. The square brackets are empty because a slice has no fixed length, which is what separates []string{...} from the array literal [3]string{...}.

Slice literals work for any element type, including structs, other slices, and maps.

A trailing comma after the last element is required when the closing brace is on its own line. This is a gofmt rule, not a feature. The compiler enforces it so that you can add or reorder lines without touching neighbouring commas during a code review.

Use a slice literal when you already know the contents at the point of creation. Configuration values, fixed lookup tables, and test data all fit this pattern.

The Empty Literal []T{}

You can also write a slice literal with no elements. The result is a slice of length 0 backed by a real (but empty) array.

[]string{} is an empty slice: length 0, capacity 0, but a non-nil pointer to a zero-sized backing array. You can call len, cap, and append on it just like any other slice. The short version is: empty slices and nil slices both have len == 0, but they're not identical when it comes to JSON encoding and direct nil comparisons.

This form is handy when you want to return "no items" from a function and have the caller iterate without a nil check. It also makes the intent obvious at a glance: "a slice that starts empty, ready to grow."

The var s []T Declaration

Declaring a slice variable without initializing it gives you a nil slice. The zero value for any slice type is nil.

var orders []string doesn't allocate a backing array. The slice header exists, but its pointer is nil, and both len and cap are 0. The trick is that append handles a nil slice gracefully: it allocates a backing array for you on the first call and returns the new slice. So even though you started with nil, you can build up the slice without any special-case code.

The one-line summary: prefer var s []T when you're declaring a slice you'll grow with append and don't need to distinguish "no result" from "empty result" in any external interface.

make([]T, len)

When you want a slice of a specific length, every element pre-initialized to the zero value, use make with two arguments.

make([]int, 5) allocates a backing array of 5 int values, sets every slot to 0 (the zero value for int), and returns a slice that views the whole array. Length is 5, capacity is 5. You can index any of the five positions immediately without an append, which you can't do with a nil slice or an empty literal.

Use this form when you know the exact number of slots up front and want to fill them by index.

The strings case is similar, except the zero value is the empty string "".

Use make([]T, n) when:

  • You know the final length up front.
  • You want to assign by index instead of appending.
  • You're translating an algorithm that thinks in terms of fixed-size buffers (matrix rows, dynamic programming tables, fixed-window buffers).

make([]T, len, cap) and Why Pre-Sizing Matters

make accepts an optional third argument: the capacity of the backing array. Length and capacity can differ, which is the whole point of slices.

make([]string, 0, 5) allocates space for 5 strings but starts with the slice viewing 0 of them. As you append, the length grows and the capacity stays at 5. No new backing array is needed until you push past the fifth element.

This is the standard form for "I'm going to build a slice with append, and I have a good estimate of the final size." Growing forces an allocation and a copy of everything you've already added, and pre-sizing the capacity avoids that work entirely.

Here's a side-by-side that makes the cost concrete.

The first slice reallocates four times (capacities 1, 2, 4, then 8). The second never reallocates. For 8 elements that's barely noticeable, but in a loop that builds a slice of a million records, pre-sizing turns dozens of allocations into one.

The second and third arguments to make have distinct meanings:

CallLengthCapacityIndexable Now?
make([]int, 5)55Yes, all 5 positions are zeroed
make([]int, 0, 5)05No, you must append first
make([]int, 3, 5)35Yes, first 3 positions, but append won't reallocate until you cross index 4

The form you want depends on whether you're filling by index (use length-based) or by append (use capacity-based).

Slicing an Existing Array

Every array can be sliced. The slice doesn't copy the array; it points at a window of the existing storage.

The expression products[low:high] produces a slice that starts at index low (inclusive) and ends at index high (exclusive). Either bound can be omitted: products[:3] is the same as products[0:3], products[2:] runs to the end, and products[:] covers the whole array.

The important thing for this lesson is that slicing an array is a way to create a slice. The resulting slice shares storage with the array.

Writing through the slice changed products[1] because the slice and the array share storage. This sharing is a feature, not a bug, but it's why you should think twice before handing a slice of an internal array to outside code: that code can mutate your private data.

If you need an independent copy, allocate a fresh slice and copy into it.

Slicing an Existing Slice

The same low:high syntax works on a slice as well. The result is another slice that views part of the same backing array.

Both pageOne and pageTwo are real, independent slices: they have their own length, but they share the backing array of allItems. The capacity of each sub-slice is the distance from its starting index to the end of the original backing array. So pageOne starts at index 0 with capacity 5 (the whole array), and pageTwo starts at index 2 with capacity 3 (positions 2, 3, 4).

The pattern comes up frequently: paging through results, taking a prefix, trimming the last element, breaking a buffer into chunks.

These two slices don't allocate or copy anything. They reuse the same backing array, which makes them effectively free.

Converting an Array Pointer to a Slice

Sometimes you have a pointer to an array (not the array itself) and you want a slice over it. The cleanest, safe way is to dereference the pointer and slice the result.

(*ptr)[:] reads as "take the array ptr points to and slice the whole thing." The resulting slice shares storage with the original array, just like slicing the array directly would.

Since Go 1.17, you can also slice an array pointer without the explicit dereference. ptr[:] is shorthand for (*ptr)[:] when ptr is a pointer to an array.

Both forms produce the same slice. The shorthand is the current idiomatic form.

This conversion comes up most often when a function returns a pointer to an array, or when you're working with a fixed-size buffer (like a network packet header, or a hash output) and you want to pass a generic []byte to a function. From this chapter's point of view, the only thing to remember is that converting *[N]T to []T is a slice expression away.

Comparing the Forms

All of the constructors do the same fundamental thing (produce a slice header pointing at some backing storage), but each is best for a different situation.

FormLengthCapacityBacking ArrayBest When
[]T{a, b, c}33New, sized to fitYou know the values up front
[]T{}00New, zero-sizedYou want a non-nil empty slice
var s []T00None (nil)You'll append and don't care about nil vs empty
make([]T, n)nnNew, all zero valuesYou'll fill by index
make([]T, 0, n)0nNew, n slots reservedYou'll append and know the final size
make([]T, m, n)mnNew, first m zeroedYou want some initial zeroed slots plus room to grow
arr[i:j]j-ilen(arr)-iShared with arrYou want a view into an existing array
s[i:j]j-icap(s)-iShared with sYou want a view into an existing slice
(*ptr)[:] or ptr[:]lencap of arrayShared with *ptrYou hold a pointer to an array

A few rules of thumb:

  • Use a literal when the values are known at the source. It reads cleanly and you don't have to think about length.
  • Use `make([]T, n)` when you have a fixed number of slots to fill by index.
  • Use `make([]T, 0, n)` when you'll append in a loop and n is known. This is the single biggest performance win for slice construction.
  • Use `var s []T` when the slice might end up empty and you'll grow it with append. Simple and idiomatic.
  • Use slicing when you want a view, not a copy. Remember the storage is shared.