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.
Go has a simple but powerful approach to memory management:
| 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 |
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]
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]
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}
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.
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.
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.
copy for slice duplicationUnderstanding 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.
Effective Go: Pointers vs Values
My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.
List all Go tutorials.