AlgoMaster Logo

Slices Basics

Last Updated: May 22, 2026

High Priority
7 min read

A slice is Go's everyday list type. When you need to hold a bunch of products in a cart, a list of order IDs, or a few customer ratings, a slice is the right choice. Unlike the fixed-size array, a slice grows and shrinks as your program runs, which matches how most real data behaves. This lesson covers what a slice is at a practical level, how to declare one, how to read and write its elements, and how slices compare to arrays for everyday work.

What a Slice Is

A slice is an ordered, indexable, resizable list of values, all of the same type. You can think of it as a list of products on a receipt: each item sits at a numbered position, you can read any position directly, and the list can grow as the cashier scans more items.

The type is written as []string, which reads "slice of string". The brackets are empty because a slice doesn't carry a fixed size in its type. A [3]string is a different type (a 3-element array). A []string is a single type that can hold zero items, three items, or three million items, and the count can change as the program runs.

Three things are true of every slice, and they're the only properties that matter at this stage: every value in it has the same element type, len(s) reports the current number of elements, and elements are numbered from 0 up to len(s)-1.

There is one more property called capacity, plus an internal pointer to a backing array. Those are what make a slice work internally. For now, treat the slice as a growable list and don't worry about what's underneath.

Declaring a Slice

The most direct way to create a slice is with a slice literal, which lists the starting values inside braces.

Every literal has two parts: the type before the braces ([]float64, []int, []string) and the comma-separated values inside. The compiler counts the values for you, so you don't write the length anywhere. Add or remove values and the slice changes size without any other edit.

You can also declare a slice without initial values. Two forms come up most often.

Both slices have length zero, so both behave like empty lists for almost everything you'll do in this chapter. There's a real difference between nil and "empty but not nil", but it only matters in a few situations (JSON output, explicit nil checks). For now, treating them as "two ways to start with no items" is fine.

There's also a third form using make, which pre-allocates room for a known number of elements:

make([]int, 5) builds a slice of length 5, with every position set to the zero value of int. We'll mostly use literals in this lesson.

Indexing and len

Once a slice exists, you read any element by its index. Indexing starts at 0, so the first item lives at [0] and the last lives at [len(s)-1].

len(cart) returns the current number of elements as an int. It's the count, not the highest index, which is why the last item sits at len(cart)-1. This off-by-one is a common source of bugs with slices.

Indexing out of bounds is a runtime error in Go, called a panic. The program prints a stack trace and exits.

The error message tells you the index you tried (5) and the slice's length (2). Go checks every index at runtime, so a bad index doesn't quietly read garbage from memory the way it would in C. The safest habit is to test computed indexes against len(s) with if index < len(cart) before reading or writing.

Iterating with for and range

The plain for loop with an index is the most explicit way to walk a slice.

The condition i < len(prices) is the safe bound. Writing i <= len(prices) would walk one position too far and trigger the index-out-of-range panic.

Most of the time you don't need to write the index by hand. Go's range form gives you both the index and the value in one step:

range prices produces two values on each iteration: the index and a copy of the element at that index. The copy is important. Writing to price inside the loop changes the local copy, not the slice. We'll see this in a moment.

When you don't need the index, replace it with the blank identifier _:

The blank identifier is Go's way of saying "I'm required to receive this value, but I'm not going to use it." It compiles without a warning, while leaving an unused variable named i would not.

When you only need the index, drop the second variable entirely. Writing for i := range prices gives just the index, which is useful when you want to write back into the slice rather than read from it.

Three valid forms then: for i, v := range s for both, for _, v := range s for value only, and for i := range s for index only. Pick the one that names the variables you actually use.

Modifying Elements

Slices are mutable. You change an element by assigning to its indexed position.

This is the same indexing syntax used for reading, just on the left side of the =. The slice still has the same length and the same element type; only the value at that position changes. Trying to assign past the end raises the same index-out-of-range panic that reading past the end does.

To update every element, combine indexing with a for loop. This is where the index-form range is useful, because the value-form gives you a copy:

The loop writes directly into the slice through prices[i]. The value form of range would only update a local copy and leave the slice unchanged.

Adding a new item is a slightly different operation. You can't index past len(s) to "create" a position because that's a panic. Instead, you use the built-in append function:

append returns a (possibly new) slice with the value tacked on at the end, and reassigning to cart updates the variable to point at the result. You must reassign, because append doesn't always modify the original slice in place. For now, the rule "always write cart = append(cart, ...)" is enough.

Arrays vs Slices in Practice

Arrays are declared with a size in the type, like [5]int, and that size is fixed at compile time. Slices look almost the same on the page, but they behave quite differently. The table below summarizes the contrast for the cases you'll hit in everyday code.

AspectArray ([5]int)Slice ([]int)
Size in the typeYes, part of the typeNo, length is dynamic
ResizableNo, fixed foreverYes, grows with append
Zero valueAn array of zerosnil (length 0)
Pass to a functionCopies the whole arrayCopies a small header; data is shared
Compare with ==Allowed (element-wise)Not allowed (compile error)
Common useRare in application codeDefault for "a list of T"

Two of those rows are worth seeing in code. The first is that passing an array to a function copies the whole array, while passing a slice shares the underlying data.

The array call leaves the original cart untouched because clearArray worked on a copy. The slice call wiped every item because both slices, the one in main and the one inside clearSlice, point at the same underlying data. This is why slices are the default for "give a function some data and let it modify it" in Go, and why arrays are rare outside of fixed-size buffers.

The second row to see is comparison. Two arrays of the same type are equal when all their elements are equal, so == works:

Try the same thing with slices and the compiler rejects the file:

The error message tells you the rule: a slice can only be compared to nil. To check whether two slices have the same contents, you walk them element by element, or call slices.Equal from the slices standard library package. The reason == isn't defined for slices is that the language designers didn't want a "compare two slices" operation to silently become O(n). Making it a function makes the cost obvious.

When to Use a Slice

For application code, use a slice. A slice covers cart contents, search results, order histories, customer reviews, and basically every other "list of X" you'll write.

Use an array only when you need a fixed size the type system can enforce (a 16-byte hash, an RGB color as [3]uint8), or when you want the value to be comparable with ==. Everything else is a slice.

The following example puts the basics together to compute a cart total and the most expensive item:

Two slices used together, indexed by the same position, give you a quick way to associate names with prices. It works, and it's a fine starting point. Once you've seen structs, you'll usually store each product as a single value with its own name and price fields, and keep one slice of those.

Here's a Mermaid view of how a slice of product names lays out at this level. Each index from 0 to len(s)-1 points to one element, and the slice variable itself is what your code holds onto.

This picture is enough to get real work done. There's a more complete picture, with a backing array and capacity sitting under the slice.