8.5 copy(): Moving Data Between Slices
Now, let’s talk about copy(), the workhorse function for moving data between slices when a simple assignment just won’t cut it. You use copy() for one simple reason: you want two separate, independent slices with the same underlying data. An assignment like slice2 := slice1 doesn’t do that; it just creates a new header pointing to the exact same array. Change an element in slice2, and boom, you’ve changed it in slice1 too. It’s a recipe for spooky action at a distance, and we don’t like that.
copy() is your escape hatch from that shared-data nightmare. Its signature is straightforward:
func copy(dst, src []Type) int
It takes a destination slice and a source slice (which must be of the same element type) and returns the number of elements it actually copied. This return value is crucial, and ignoring it is a classic rookie mistake. We’ll get to that.
How copy() Actually Works (It’s Not Magic)
Think of copy() as a very efficient, compiled for loop. It doesn’t care about the capacities of your slices; it only cares about their lengths. It walks from index 0 to min(len(src), len(dst)) and assigns dst[i] = src[i]. That’s it. It’s a blind, mechanical copy. This leads to its most important behavior: it will only copy as many elements as the smaller of the two slices can hold.
Let’s see this in action. This is the happy path, where the destination is big enough.
src := []string{"Aragorn", "Gimli", "Legolas", "Boromir"}
dst := make([]string, len(src)) // Critical: dst is the same length as src
elementsCopied := copy(dst, src)
fmt.Printf("Copied %d elements\n", elementsCopied) // Copied 4 elements
fmt.Printf("src: %v\n", src) // src: [Aragorn Gimli Legolas Boromir]
fmt.Printf("dst: %v\n", dst) // dst: [Aragorn Gimli Legolas Boromir]
// Now let's prove they are independent
dst[0] = "Gandalf"
fmt.Printf("src[0] is still: %s\n", src[0]) // src[0] is still: Aragorn
fmt.Printf("dst[0] is now: %s\n", dst[0]) // dst[0] is now: Gandalf
Perfect. We’ve successfully forked our data. But what if we mess up the destination size?
The Pitfall: Undersized Destination Slices
This is where things get real. If you make your destination slice too small, copy() will not panic, will not warn you, and will not resize the destination. It will silently copy only what fits and return. This is a fantastic way to introduce subtle, annoying bugs.
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3) // Oops! Only made room for 3 elements.
elementsCopied := copy(dst, src)
fmt.Printf("Copied only %d elements\n", elementsCopied) // Copied only 3 elements
fmt.Printf("src: %v\n", src) // src: [1 2 3 4 5]
fmt.Printf("dst: %v\n", dst) // dst: [1 2 3]
Your data has been silently truncated. The designers made a choice here: it’s better to copy a partial slice than to cause a runtime panic. I get the logic, but it means you must check the return value if there’s any doubt about the sizes. If elementsCopied != len(src), you know you’ve lost data.
The Power Move: Slicing to a Full Capacity
You can use copy() brilliantly with the three-index slice we just discussed. Remember, a slice created with a[low:high:max] has a length of high-low but a capacity of max-low. This lets you create a perfect-sized destination slice for a copy operation.
// Let's say we have a slice from some other function, and we want a complete copy.
sourceSlice := []int{10, 20, 30, 40, 50}
// Instead of making a new slice of len(sourceSlice), we can do this:
perfectSizedDst := make([]int, len(sourceSlice))
copy(perfectSizedDst, sourceSlice)
// But we can also use the third index to force the length and capacity to be identical.
// This is a bit more explicit about the intent: "I want a copy exactly this big."
dstWithForcedCap := sourceSlice[:len(sourceSlice):len(sourceSlice)]
copy(dstWithForcedCap, sourceSlice)
The first method with make is more common and clearer, but the second shows you understand how the length and capacity interact. Both are correct.
Best Practices and Why They Matter
Always Check the Return Value: Unless you are 110% certain that
len(dst) >= len(src), store the return value and verify it. A simpleif n != len(src) { // handle the error }can save you hours of debugging.Pre-allocate the Destination Correctly: The most common pattern is
dst := make([]T, len(src))followed bycopy(dst, src). This guarantees the copy will be complete. It’s simple, readable, and effective.You Can Copy to a Non-Empty Slice:
copy()will happily overwrite existing elements in the destination. It starts at index 0 of both slices. If you want to append a copy of one slice to another, you’d useappend(dst, src...), but rememberappendmight allocate a new array.copyis for overwriting.It Works with Overlapping Slices: The Go spec guarantees that
copyworks correctly even if the source and destination slices overlap. So you can do things likecopy(mySlice[1:], mySlice)to shift elements down, which is incredibly useful. It’s implemented to handle this case without corrupting data.
In essence, copy() is a sharp, simple tool. It does one job and does it well, but it expects you to understand the dimensions of your slices. It won’t hold your hand, so you have to be smarter than the function. Which, frankly, isn’t a high bar—it’s just moving bits around. The intelligence has to come from you.