{
"$type": "site.standard.document",
"canonicalUrl": "https://rednafi.com/go/slice-gotchas/",
"description": "Avoid common Go slice mistakes: shared backing arrays, nil vs empty slices, append behavior, and slice copying pitfalls explained.",
"path": "/go/slice-gotchas/",
"publishedAt": "2025-02-06T00:00:00.000Z",
"site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
"tags": [
"Go",
"Data Structures"
],
"textContent": "Just like any other dynamically growable container structure, Go slices come with a few\ngotchas. I don't always remember all the rules I need to be aware of. So this is an attempt\nto list some of the most common mistakes I've made at least once.\n\nSlices are views over arrays\n\nIn Go, a slice is a lightweight wrapper around an array. Instead of storing data itself, it\nkeeps track of three things: a pointer to an underlying array where the data is stored, the\nnumber of elements it currently holds, and the total capacity before it needs more space.\nThe Go runtime defines it like this:\n\nWhen you create a slice from an array or another slice, Go doesn't copy the data - it simply\npoints to a section of the existing array.\n\nThis makes slices efficient. Passing a slice by value doesn't mean copying all its\nelements - only the small slice struct gets copied, while the data stays where it is. But\nthis behavior is also the source of much confusion. The next sections cover some common\npitfalls.\n\nSliced slices share the underlying array\n\nReslicing a slice doesn't copy data. The newly created slices point to the same array. So\nmodifying one slice will affect others.\n\nSolution: To get independent slices, you need to explicitly copy the data. Use make to\ncreate a new slice and copy to transfer the elements.\n\nAppend may reallocate\n\nappend reallocates the underlying array if capacity is insufficient, changing the backing\narray pointer.\n\nWhen passing slices to functions, reallocation inside the function won't update the original\nslice header in the caller _unless_ the slice is returned and reassigned. Modifications\nwithin the capacity _are_ visible.\n\nIf you create a slice with a predefined capacity and start appending elements, everything\nlooks fine until you exceed that capacity. Once that happens, Go reallocates memory and\nmoves the slice to a new backing array.\n\nThe same behavior applies when passing a slice to a function. If the function modifies\nelements within the allocated capacity, those changes persist and are visible from outside\nthe function. But if append triggers a reallocation inside the function, the caller's\nslice remains unchanged.\n\nSolution: If append inside a function reallocates memory, the caller won't see the\nchange. To make it explicit, return the modified slice and reassign it.\n\nAppend returns new slice\n\nappend returns a _new_ slice. If you don't reassign the result back to the original slice\nvariable, the slice remains unchanged after the append operation. We already saw this in\nthe last section but I think it deserves a section of its own.\n\nSolution: Remember to always assign the return value of append back to the slice\nvariable you are working with.\n\nNil and empty slices differ\n\nNil slices have nil array pointers; empty slices have initialized, non-nil pointers and\nzero length. While often interchangeable for emptiness checks, the distinction matters in\ncertain contexts like JSON encoding or API interactions.\n\nSolution: When you need a truly empty slice (e.g., to represent an empty list in JSON),\ninitialize it as an empty slice (e.g., []int{} or make([]int, 0)). For general emptiness\nchecks, len(slice) == 0 works for both nil and empty slices.\n\nSlicing can leak memory\n\nSmall slices created from large arrays can keep the entire large array in memory.\n\nSolution: To avoid memory leaks, copy the data of the small slice into a new,\nindependent slice. This allows the large underlying array to be garbage collected if no\nlonger referenced elsewhere.\n\nRange copies values\n\nfor...range on value types iterates over _copies_. Modifications to the loop variable\ndon't change the original slice.\n\nSolution: If you need to modify slice elements during iteration, use an index-based\nfor loop. This provides direct access to each element via its index.\n\nMake with length initializes\n\nmake([]T, length, capacity) initializes the first length elements with the zero value of\nT. This can be a subtle point if you expect an uninitialized slice of a certain size.\n\nSolution: If you want an empty slice with a specific capacity but _without_ initial zero\nvalues, use make([]T, 0, capacity). Or use the slice literal []T{} syntax if you don't\ncare about the capacity.\n\nIf you need a slice of a certain length initialized with zero values,\nmake([]T, length, capacity) is the correct approach.\n\nOverlapping copy is tricky\n\ncopy(dst, src) with overlapping slices can corrupt data when dst starts inside src.\n\nSolution: To avoid corruption, just don't do it. If you have to, then one way to fix it\nis by using a temporary buffer. Even then it's messy.\n\nCopy truncates silently\n\ncopy also returns the number of elements copied, which is the smaller of len(dst) and\nlen(src). If dst is shorter, data gets truncated.\n\nSolution: On dst, always set the length from the src while copying.\n\nI may have missed, forgotten, or not yet encountered a few other gotchas. If you've run into\nany that aren't listed here, I'd love to hear about them.",
"title": "Go slice gotchas"
}