Go > Collections > Arrays and Slices > Copying slices

Copying Slices in Go

Learn how to effectively copy slices in Go using the copy function. Understand the difference between shallow and deep copies and their implications for your code. This guide provides practical examples and best practices for handling slice copies to avoid common pitfalls.

Basic Slice Copying using copy

This code demonstrates the fundamental way to copy a slice in Go using the built-in copy function. First, we initialize a sourceSlice with some integer values. Then, we create a destinationSlice using make. The crucial part is that the destinationSlice *must* be initialized with sufficient capacity to hold the copied elements. The copy function then iterates through the sourceSlice, copying elements into the destinationSlice until either the sourceSlice is exhausted or the destinationSlice is full. The function returns the number of elements actually copied.

package main

import "fmt"

func main() {
	// Original slice
	sourceSlice := []int{1, 2, 3, 4, 5}

	// Create a destination slice with the same length as the source
	destinationSlice := make([]int, len(sourceSlice))

	// Copy elements from source to destination
	numCopied := copy(destinationSlice, sourceSlice)

	fmt.Println("Source Slice:", sourceSlice)
	fmt.Println("Destination Slice:", destinationSlice)
	fmt.Println("Number of elements copied:", numCopied)
}

Concepts Behind the Snippet

Slices in Go are descriptors of array segments. They don't own the underlying data. When you assign one slice to another (slice2 := slice1), you're just copying the slice header (pointer to the underlying array, length, and capacity), not the array itself. This creates a shallow copy. The copy function, on the other hand, copies the *elements* from one slice to another. If the elements are primitive types (int, string, bool), this effectively creates a deep copy of those elements. However, if the slice contains pointers or complex types, the copy function still only copies the pointers, resulting in a shallow copy of the referenced data.

Real-Life Use Case

Imagine you're building a data processing pipeline where you need to manipulate a slice of data without affecting the original. For example, you might be filtering or transforming data. Using copy allows you to work on a separate copy, ensuring the integrity of your original data. Another use case is in concurrent programming. When multiple goroutines need to access and modify a slice, copying the slice allows each goroutine to work on its own copy, preventing race conditions and data corruption.

Best Practices

  • Always initialize the destination slice: Ensure the destination slice has enough capacity to hold the copied elements. If it doesn't, the copy function will only copy as many elements as the destination can hold.
  • Understand shallow vs. deep copy: Be aware of whether you need a deep copy of the underlying data or if a shallow copy is sufficient. If you need a deep copy of complex objects, you might need to iterate through the slice and manually copy each object.
  • Error Handling: While copy doesn't return an error, always check the returned value to understand how many elements were actually copied. This is crucial when the destination slice has a smaller capacity than the source.

Interview Tip

Be prepared to explain the difference between assigning a slice to another variable (shallow copy) and using the copy function. Also, be able to discuss the implications of shallow vs. deep copies, particularly when dealing with slices of pointers or complex types. A good way to showcase understanding is to explain how modifications to one shallow copy affect the other.

When to use them

Use copy when you need to create a distinct, independent copy of a slice's elements. This is crucial when you want to modify the copy without affecting the original slice. If you just need another reference to the same underlying data, a simple assignment is sufficient.

Memory Footprint

Using copy increases memory usage because you're creating a new slice and copying the elements. The impact depends on the size of the slice and the type of elements. Slices of primitive types will have a smaller memory footprint than slices of complex objects. Consider the memory implications, especially when dealing with large datasets. Shallow copies are more memory-efficient but come with the risk of unintended side effects.

Alternatives

  • Looping and Appending: You can manually loop through the source slice and append each element to a new slice. This gives you more control, especially if you need to transform the elements during the copy. However, it's generally less efficient than using copy.
  • Deep Copy Libraries: For deep copying slices of complex objects, consider using libraries that provide deep copy functionality. These libraries can handle nested objects and circular references more effectively than manual looping.

Pros

  • Creates an independent copy: Modifications to the copied slice won't affect the original.
  • Simple and concise: The copy function is easy to use and understand.
  • Relatively efficient: It's generally faster than manually looping and appending.

Cons

  • Shallow copy for non-primitive types: Only copies the pointers for slices of pointers or complex objects.
  • Increased memory usage: Requires allocating memory for the new slice.
  • Requires initialized destination slice: The destination slice must be created before calling copy.

Copying Slices of Structs

This example extends the basic copying concept to slices of structs. Because structs are value types in Go, the copy function creates a deep copy of the struct elements themselves. Modifying the Name field in the peopleCopy slice does not affect the original people slice. This is because each Person struct in the copy is a completely independent copy of the corresponding struct in the original.

package main

import "fmt"

// Define a struct
type Person struct {
	Name string
	Age  int
}

func main() {
	// Original slice of structs
	people := []Person{
		{"Alice", 30},
		{"Bob", 25},
	}

	// Create a destination slice
	peopleCopy := make([]Person, len(people))

	// Copy the slice
	copy(peopleCopy, people)

	// Modify the copy
	peopleCopy[0].Name = "Charlie"

	fmt.Println("Original:", people)
	fmt.Println("Copy:", peopleCopy)

	// Check if the original is affected (it shouldn't be)
	if people[0].Name == "Alice" {
		fmt.Println("Original slice is not affected.")
	} else {
		fmt.Println("Original slice is affected.")
	}
}

FAQ

  • What happens if the destination slice has a smaller capacity than the source slice?

    The copy function will only copy as many elements as the destination slice can hold. It returns the number of elements actually copied, which will be less than the length of the source slice.
  • Does copy create a deep copy of the slice?

    The copy function creates a shallow copy of the slice header (pointer, length, capacity) but copies the *elements* themselves. For primitive types (int, string, bool), this effectively creates a deep copy of the data. For slices of pointers or complex types, it copies the pointers, resulting in a shallow copy of the referenced data. To deep copy slices of complex types, you'll need to iterate and manually copy each element.
  • Is it necessary to use the copy function to copy a slice?

    No, you can also achieve the same effect by iterating through the slice and appending each element to a new slice. However, using the built-in copy function is generally more efficient.