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
copyfor 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.