ZetCode

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:

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.

main.go
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.

main.go
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 *.

main.go
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.

main.go
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.

main.go
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.

main.go
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

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

Go FAQ: Pass by Value

Author

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.