Go Copy by Value vs Reference
last modified May 16, 2025
In this tutorial, we will explore how Go handles copy semantics, including value types, reference types, and pointers. We will discuss the differences between value types and reference types, how Go copies values, and how to use pointers effectively. We will also examine parameter passing in Go, including how to pass values and pointers to functions.
Value Types vs Pointers in Go
Go has a simple but powerful approach to memory management:
- Value types: All basic types (int, float, bool, string, array, struct) are value types
- Reference types: Slices, maps, channels, functions, and pointers are reference types
- Pointers: Explicit pointers can be used with any type
Characteristic | Value Types | Reference Types |
---|---|---|
Assignment Behavior | Copy by value (full copy) | Copy by reference (shared data) |
Memory Location | Stack (usually) | Heap (usually) |
Examples | int , struct , [3]int |
[]int , map , *int |
Copy by Value Example
Go copies values by default. Assigning one variable to another creates an independent copy for value types. This means that changes to the copied variable do not affect the original variable.
package main import "fmt" type Point struct { X, Y int } func main() { // Primitive types a := 10 b := a // Copy by value b = 20 fmt.Printf("a = %d, b = %d\n", a, b) // Structs are value types p1 := Point{1, 2} p2 := p1 // Copy by value p2.X = 10 fmt.Printf("p1 = %v, p2 = %v\n", p1, p2) // Arrays are value types arr1 := [3]int{1, 2, 3} arr2 := arr1 // Copy by value arr2[0] = 99 fmt.Printf("arr1 = %v, arr2 = %v\n", arr1, arr2) }
In the example, the variable a
is copied to b
. Changing
b
does not affect a
. The same applies to the struct
p1
and the array arr1
. The changes to p2
and arr2
do not affect the original variables p1
and
arr1
.
$ go run main.go a = 10, b = 20 p1 = {1 2}, p2 = {10 2} arr1 = [1 2 3], arr2 = [99 2 3]
Reference Types Example
Slices, maps, and channels are reference types in Go. Assigning them copies the reference, not the underlying data.
package main import "fmt" func main() { // Slices are reference types slice1 := []int{1, 2, 3} slice2 := slice1 // Copies the slice header slice2[0] = 99 fmt.Printf("slice1 = %v, slice2 = %v\n", slice1, slice2) // Maps are reference types map1 := map[string]int{"a": 1, "b": 2} map2 := map1 // Copies the reference map2["a"] = 100 fmt.Printf("map1 = %v, map2 = %v\n", map1, map2) }
In the example, the slice slice1
is copied to slice2
.
So when we change slice2
, it also affects slice1
.
The same applies to the map map1
and map2
. The
changes to map2
also affect map1
.
$ go run main.go slice1 = [99 2 3], slice2 = [99 2 3] map1 = map[a:100 b:2], map2 = map[a:100 b:2]
Pointers in Go
Go provides explicit pointers for when you need reference semantics with any
type. Pointers are variables that store the memory address of another variable.
You can create a pointer using the address-of operator &
and
dereference it using the asterisk operator *
.
package main import "fmt" func main() { // Creating pointers a := 10 ptr := &a // Address-of operator fmt.Printf("a = %d, *ptr = %d\n", a, *ptr) *ptr = 20 // Dereference and modify fmt.Printf("a = %d, *ptr = %d\n", a, *ptr) // Pointer to struct p := Point{1, 2} pPtr := &p pPtr.X = 10 fmt.Printf("p = %v, *pPtr = %v\n", p, *pPtr) }
In the example, we create a pointer ptr
that points to the
variable a
. We can modify the value of a
through
the pointer. The same applies to the struct p
. We create a
pointer pPtr
that points to the struct p
. We can
modify the struct's fields through the pointer.
$ go run main.go a = 10, *ptr = 10 a = 20, *ptr = 20 p = {10 2}, *pPtr = {10 2}
Parameter Passing
Go always passes parameters by value, but you can use pointers for reference semantics. When you pass a pointer to a function, the function can modify the original value. This is useful for large data structures or when you want to avoid copying data.
package main import "fmt" func modifyValue(x int) { x = 20 // Doesn't affect original } func modifyPointer(x *int) { *x = 20 // Modifies original } func modifySlice(s []int) { s[0] = 99 // Affects original (slices are reference types) } func main() { // Value parameter a := 10 modifyValue(a) fmt.Println("a =", a) // 10 // Pointer parameter modifyPointer(&a) fmt.Println("a =", a) // 20 // Slice parameter nums := []int{1, 2, 3} modifySlice(nums) fmt.Println("nums =", nums) // [99 2 3] }
In the example, we define three functions: modifyValue
,
modifyPointer
, and modifySlice
. The first function
takes an integer by value, so it doesn't affect the original variable.
The second function takes a pointer to an integer, allowing it to modify
the original variable. The third function takes a slice, which is a
reference type, so it modifies the original slice.
Copying Data Structures
To create independent copies of reference types, you need to explicitly copy
them. For slices, you can use the built-in copy
function. For
maps, you need to create a new map and copy the key-value pairs manually.
package main import "fmt" func main() { // Copying slices original := []int{1, 2, 3} copy1 := make([]int, len(original)) copy(copy1, original) // Built-in copy function copy1[0] = 99 fmt.Println("original =", original) fmt.Println("copy1 =", copy1) // Copying maps mapOriginal := map[string]int{"a": 1, "b": 2} mapCopy := make(map[string]int) for k, v := range mapOriginal { mapCopy[k] = v } mapCopy["a"] = 100 fmt.Println("mapOriginal =", mapOriginal) fmt.Println("mapCopy =", mapCopy) }
In the example, we create a copy of a slice using the built-in
copy
function. We also create a copy of a map by iterating
over the original map and copying each key-value pair. The changes to
copy1
and mapCopy
do not affect the original
original
and mapOriginal
.
Returning Pointers vs Values
Go's escape analysis determines whether to allocate on stack or heap. Returning a pointer to a local variable is generally not safe, as the variable may be deallocated when the function exits. However, if the variable is allocated on the heap, it is safe to return a pointer.
package main import "fmt" func createValue() Point { return Point{1, 2} // Usually stack allocated } func createPointer() *Point { return &Point{3, 4} // Heap allocated (escapes function) } func main() { val := createValue() ptr := createPointer() fmt.Println("val =", val) fmt.Println("ptr =", *ptr) }
In the example, the function createValue
returns a value,
which is usually stack allocated. The function createPointer
returns a pointer to a struct, which is heap allocated. The Go compiler
performs escape analysis to determine whether the variable should be
allocated on the stack or heap. If a variable escapes the function,
it is allocated on the heap. This is important for performance and
memory management.
Summary and Best Practices
- Go defaults to pass-by-value for all types
- Slices, maps, and channels have reference semantics
- Use pointers when you need to modify the original value
- Use built-in
copy
for slice duplication - For large structs, consider passing pointers for efficiency
- Trust Go's escape analysis for memory allocation decisions
- Be explicit about ownership and mutation in your APIs
Understanding these concepts is essential for writing efficient, correct Go code that properly manages memory and data sharing.
This tutorial provided an overview of Go's copy semantics, including value types, reference types, pointers, and parameter passing. We also discussed how to copy data structures and the implications of returning pointers versus values. By understanding these concepts, you can write efficient and correct Go code that properly manages memory and data sharing.
Source
Effective Go: Pointers vs Values
Author
List all Go tutorials.