Getting Started with Go: Understanding "Always Pass by Value"

Getting Started with Go: Understanding "Always Pass by Value"

My general take on Golang is that it's simple but safe. That said, Go is often confusing to newcomers in the way it handles passing variables to functions. In this post, I'm going to break down pass-by-value semantics and run through how they work for Go as compared to Java.

Consider Java...

Not familiar with Java? Skip below!

Consider this Java code for reference:

public class PassByValueExample {
    
    public static void main(String[] args) {
        String testStr = "Hello!";
        System.out.println(testStr); // "Hello!"
        addGoodbye(testStr);
        System.out.println(testStr); // still "Hello!"
    }
    
    private static void addGoodbye(String initialMessage) {
        initialMessage = initialMessage + " Goodbye!";
    }
}

This is a contrived example, but it demonstrates a core facet of Java: Java passes variables to functions by value.

This means essentially that Java makes a copy of the variable before passing it. That said, it's different in the case of objects, for example Lists:

import java.util.ArrayList;

public class PassByValueExample {
    
    public static void main(String[] args) {
        ArrayList<Integer> listTest = new ArrayList<>();
        listTest.add(5);
        System.out.println(listTest.toString()); // [5]
     	addToList(listTest);
        System.out.println(listTest.toString()); // [5, 6] -- changed!
    }
     
    private static void addSixToList(ArrayList<Integer> lTest) {
        lTest.add(6);
    }
}

What's going on here? In the first example, we couldn't change the variable passed to us and in the second we could? That's right! And this is actually consistent with passing by value. In the first case, we modified a copy of the String itself, in the second we modified a copy of the reference to the List. In other words, we were still able to call methods as usual.

The point of this example is that Go works the same way. Every variable is passed by value, except that you have to be more conscious of pointers in the approach there.

Variables in Go

Go, unlike Java, supports pointer variables, but still passes all variable by value rather than by reference. If you're coming from a lower level language like C, this might be a surprise to you.

Consider the same example above, the difference being that we can actually modify string values given the expressiveness of the pointer!

package main
import "fmt"

func main() {
	helloStr := "Hello!"
	fmt.Println(helloStr) // "Hello!"
	addGoodbyeString(helloStr)
	fmt.Println(helloStr) // "Hello!" -- nothing added
    helloStrPtr := &helloStr
    addGoodbyePtrString(helloStrPtr)
	fmt.Println(helloStr) // "Hello! Goodbye"
}

func addGoodbyeString(message string) {
	message += "Goodbye!"
}

func addGoodbyePtrString(messagePtr *string) {
	*messagePtr += " Goodbye!"
}

In the first example above, we do the same thing we were doing in Java. We very simply pass the string to the function. However, when we modify the variable inside the function, it does nothing!

Why? Essentially because Go passes by value and makes a copy of anything you pass to a function. So in addGoodbyeString, we're not actually modifying the initial value at all, just a copy.

In the second example, though, things get interesting. Instead of passing the string itself, we pass a pointer to the string. Just like before, the pointer is still copied by value. So essentially Go creates a copy of the pointer.

The initial pointer, helloStrPtr is still there, but we also have a copy of that pointer, messagePtr that points to the same string value. As a result, we can still access the original value by getting the value:

A copy of helloStrPtr, messagePtr, allows us to access the initial string.

The more you recognize that Go always passes by value, just with the additional expressiveness of supporting pointers, you will be able to create much clearer programs!

Conclusion

Always passing by value yet still supporting pointers is one way that Go falls between the expressiveness of a lower level language like C and the higher level consistency of a language like Java.

Though always copying values can be confusing, it typically ends up creating safer code in the end, and the added option of passing a copy of a pointer rather than a copy of a value can give us more options than a language like Java.