Go Function Arguments Are Always Pass by Value
Introduction
In this post, we will learn the difference and caveats of passing arguments by pass by reference in Go. By the end of this article you should have a better understanding about the topic “pass by reference” with respect to function arguments, and you should also be able to identify this pattern being used in various SDKs and libraries.
Let’s Dive In
Check out the program below. Try to compute the output of the program before proceeding forward.
package main
import "fmt"
func incrementByPointer(a *int) {
var b int
b = *a
b++
a = &b
}
func main() {
value := 1
a := &value
// print initial value
fmt.Println("[initial value of a]", *a)
incrementByPointer(a)
fmt.Println("[incrementByPointer]", *a)
}
Inspecting incrementByPointer
function and analyse what it does step by step.
var b int
, we declare a new variableb
of type int to temporarily store and update the value of incoming argumenta
.b = *a
, we copy the value being referenced bya
to our locally scoped variableb
.b++
, we increment our locally scoped variableb
.a = &b
, this is probably the most interesting part. Remember the function argument is a pointer, a pointer holds a memory address of a certain type. So, in this statement we set the value of our incoming pointer argumenta
to hold the address of our locally scoped variableb
.
The output of the program is going to be as below.
[initial value of a] 1
[incrementByPointer] 1
Shouldn’t our incrementByPointer
reflect output as 2? The Go spec highlights that all the function arguments are pass by value. This means even the references are copy of the actual variables.
However, they would still point to the same address in the memory.
the parameters of the call are passed by value to the function and the called function begins execution. The return parameters of the function are passed by value back to the caller when the function returns. Source
With that in mind let’s confirm that with two approaches. Let’s write two new functions incrementByPointerV2
and incrementByPointerV3
that would follow and validate the spec.
package main
import "fmt"
func incrementByPointer(a *int) {
var b int
b = *a
b++
a = &b
}
func incrementByPointerV2(a *int) *int {
var b int
b = *a
b++
a = &b
return a
}
func incrementByPointerV3(a *int) {
*a = *a + 1
}
func main() {
value := 1
a := &value
// print initial value
fmt.Println("[initial value of a]", *a)
incrementByPointer(a)
fmt.Println("[incrementByPointer]", *a)
a = incrementByPointerV2(a)
fmt.Println("[incrementByPointerV2]", *a)
incrementByPointerV3(a)
fmt.Println("[incrementByPointerV3]", *a)
}
Let’s take a look at our incrementByPointerV2
function and see step by step what it does.
- Step 1-4 are as is as
incrementByPointerV2
. - The final thing that we do here is to return the pass by value pointer function argument
a
to update the pointera
scoped under the main block.
Let’s take a look at our incrementByPointerV3
function and see step by step what it does.
- In this function, we deal with the value that the pointer points to and hence are not required to return the updated pointer to update the value in the invoking scope.
While both approaches yield about the same result at high level. There might be cases when you might want to use one over the other. Let’s see them below.
Approach One - incrementByPointerV2
Use case - query builders.
Pros
- Provides flexibility to the invoking program to store the return result in the same or a different variable.
- Original argument is not modified providing flexibility to branch out different versions from a base pointer.
- Allows method chaining based on the updated value.
Cons
- Updated values need to be captured.
Approach Two - incrementByPointerV3
Use Case - SDK Clients.
Pros
- No overhead of capturing the updated result.
Cons
- Dev needs to self-manage any branching out from the same version of variable.
Conclusion
With the above programs and examples, I hope to have helped you get a better understanding of how passing of references to function arguments works in Go. With two versions of our approach we also observed possible use cases along with their trade-offs.