Pass by Value and Reference in Go

Introduction

Many programming languages support passing an argument by value and/or by reference. In this article, we’re going to learn how Go’s function handles passed arguments.

What Is Pass-By-Value?

In Go, when a parameter is passed to a function by value, it means the parameter is copied into another location of your memory, and when accessing or modifying the variable within your function, only the copy is accessed/modified and the original value is never modified. All primitive/basic types (int (all its variant), float (all its variant), boolean, string, array, and struct) in Go are passed by value. Passing by value is how your values are passed on to functions most of the time. Let us look at some examples below:

# int

func modifyInt(n int) int {
	return n + 5
}

age := 30
fmt.Println("Before function call: ", age)              // 30
fmt.Println("Function call:", modifyInt(age))           // 35
fmt.Println("After function call: ", age)               // 30
# float

func modifyFloat(n float64) float64 {
	return n + 5.0
}

cash := 10.50
fmt.Println("Before function call: ", cash)             // 10.5
fmt.Println("Function call:", modifyFloat(cash))        // 15.5
fmt.Println("After function call: ", cash)              // 10.5
# bool

func modifyBool(n bool) bool {
	return !n
}

old := false
fmt.Println("Before function call: ", old)              // false
fmt.Println("Function call:",  modifyBool(old))         // true
fmt.Println("After function call: ", old)               // false
# string

func modifyString(n string) string {
	return "My favourite language is: " + n
}

message := "Go"
fmt.Println("Before function call: ", message)         // Go
fmt.Println("Function call:", modifyString(message))   // My favourite language is: Go
fmt.Println("After function call: ", message)          // Go
# array

func modifyArray(coffee [3]string) [3]string {
	coffee[2] = "germany"
	return coffee
}

country := [3]string{"nigeria", "egypt", "sweden"}
fmt.Println("Before function call: ", country)         // [nigeria egypt sweden]
fmt.Println("Function call:", modifyArray(country))    // [nigeria egypt germany]
fmt.Println("After function call: ", country)          // [nigeria egypt sweden]
# struct

func modifyStruct(p profile) profile {
	p.Age = 85
	p.Name = "Balqees"
	p.Salary = 500.45
	p.TechInterest = true
	return p
}

myProfile := profile{
	Age:          15,
	Name:         "Adeshina",
	Salary:       300,
	TechInterest: false,
}
fmt.Println("Before function call: ", myProfile)       // {15 Adeshina 300 false}
fmt.Println("Function call:", modifyStruct(myProfile)) // {85 Balqees 500.45 true}
fmt.Println("After function call: ", myProfile)        // {15 Adeshina 300 false}

If you run through the examples above, we can confirm that the values of variables passed to the functions remain the same before and after the functions calls. In a nutshell, the variables were passed by value. Now, let’s explore the other way Go function treats parameters; Pass-By-Reference.

What Is Pass-By-Reference?

There is an understanding/debate of whether Go composite types are passed to function by reference. To be specific, Go does not support “Pass-By-Reference” semantic in any way. The reason is simple; Go does not support a reference variable as you have in other programming languages like C++. Conceptually, in a case of Map, when you create a type of Map and then pass it to a function, should the function modify the argument, the effect will affect the original variable as well. Well, this may appear as if the Map variable is passed by reference, but that is not correct. When you create a variable of type Map using the “make()”, under the hood, it calls on makemap() which returns *hmap (that is a pointer). So, passing the variable to a function is a Pass-By-Pointer, not a Pass-By-Reference. The same concept applies to Channel. Although, Slice is relatively different in how its data structure is (a struct with three types; pointer to the underlying array, length of the slice, and capacity for the slice). But, it is also treated as a Pass-by-Pointer.

Before we move on to some examples of how Go function treats the Composite types (Slice, and Map), Channel, Pointer, and function, let us take a look at this snippet of code that confirms Go composite types are not Passed-By-Reference:

package main

import "fmt"

func myMap(v map[int]int) {
        v = make(map[int]int) // make() declares and initializes v to 0
}

func myInt(v []int) {
        v = make([]int) // make() declares and initializes v to 0
}

func main() {
        var v map[int]int  //v is declared but NOT initialized, which means its value is nil
        myMap(v)
        fmt.Println(v == nil) // true
		
		    var i []int  // i is declared but NOT initialized, which means its value is nil
        myInt(i)
        fmt.Println(i == nil) // true
}

Looking at the example above, we could tell that even after declaring a variable “V”, then calling myF() on it in order to initialize it to 0. Eventually, when we test for its value after the call, it results in “true”. This means myF() did not treat “v” as Pass-By-Reference (because Go does not support that semantic). The same result we would get if we try Slice and Channel.

You can read more on this topic in this GREAT Dave Cheney’s blog.

Below are the example of passing the Composite types and other types (asides from Primitive types discussed above) in Go:

# slice

func modifySlice(coffee []string) []string {
	coffee[1] = "turkish_coffee"
	return coffee
}

coffeeBox := []string{"egyptian_coffee", "kenyan_coffee", "brazilian_coffee"}
fmt.Println("Before function call: ", coffeeBox)         // [egyptian_coffee kenyan_coffee brazilian_coffee]
fmt.Println("Function call:", modifySlice(coffeeBox))    // [egyptian_coffee turkish_coffee brazilian_coffee]
fmt.Println("After function call: ", coffeeBox)          // [egyptian_coffee turkish_coffee brazilian_coffee]
# map

func modifyMap(expenses map[string]int) map[string]int {
	expenses["food"] = 4500
	return expenses
}

expenses := make(map[string]int, 0)
expenses["transport"] = 30
expenses["food"] = 300
expenses["rent"] = 100

fmt.Println("Before function call: ", expenses)         //  map[food:300 rent:100 transport:30]
fmt.Println("Function call:", modifyMap(expenses))      //  map[food:4500 rent:100 transport:30]
fmt.Println("After function call: ", expenses)          //  map[food:4500 rent:100 transport:30]
# pointer

func ModifyBasicTypes(name *string, age *int, cash *float64, techInterest *bool, countries *[3]string, myProfile *profile) {
	*name = "Golang"
	*age = 90
	*cash = 50.45
	*techInterest = !(*techInterest)
	*countries = [3]string{"sudanese", "belgium", "zambia"}
	*myProfile = profile{
		Age:          100,
		Name:         "GOOGLE",
		Salary:       40.54,
		TechInterest: true,
	}
}

myProfile=  profile{
	Age:          0,
	Name:         "",
	Salary:       0,
	TechInterest: false,
}
fmt.Println("Before function call: ", message, age, cash, old, country, myProfile)     // {0  0 false [nigeria egypt swed] {0  0 false}}
ModifyBasicTypes(&message, &age, &cash, &old, &country, &myProfile)
fmt.Println("After function call: ", message, age, cash, old, country, myProfile)      // {90 Golang 50.45 true [nigerian colombian sudanese] {50 Hassan 45.45 false}}

# channel
func modifyChannel(s chan string) {
	s <- "INJECTING A NEW MESSAGE"
}

status := make(chan string)  // P.S: "status" has an empty value at the moment 
go modifyChannel(status)    
fmt.Println("After function call: ", <- status) // INJECTING A NEW MESSAGE

Running through the examples above, we can see the effect of passing parameters to functions. In the slice, for example, we can confirm that the value of the variable coffeeBox was modified when passed to the function modifySlice. It’s the same with map, pointer, function, and channel. If you find yourself in need of modifying the value of a basic type (int, float, bool, etc), simply pass the variable’s memory address to the function (in other words, treat the parameter as a pointer). The pointer section clearly exemplifies this scenario.

Summary:

Go supports the Pass-By-Value sematic; when arguments are passed by value, the function receives a copy of each argument - modifications to the copy do not affect the caller. On the other hand, It does not support Pass-By-Reference. But, it does support Pass-by-Pointer which can be used to modify the underlying argument’s value.

In this short blog, we have explored the two ways of how Go treats parameters passed to its function. It is very important to be aware of this concept so as to avoid false/wrong expectations. Till the next blog, keep Go-ing :)