A Closer Look At Functions in Go

Michael Sauter
Share

Functions are one of the basic building blocks of any programming language. However, the feature set and role they play differ quite a lot from language to language. In this article, we’ll have a look at what Go has to offer when it comes to functions. We are going to discover that functions are first-class citizens in Go and provide the developer a very rich feature set – with some interesting details that are not apparent at first. Let’s get started by covering the basics.

Arguments

A function in Go can take any number of typed arguments. All arguments are required, as there is no concept of optional arguments. Important to know as well is that arguments are not passed by reference, but copied into the function (with a few notable exceptions which we’ll get to shortly). This means that if the argument is modified inside the function, it won’t have any effect on the original value. The only way to circumvent this is to pass a pointer to the function. If you’re coming from dynamic languages like PHP, Ruby or JavaScript, then the concept of pointers might not be familiar to you. However, you would have used pointers implicitly as these languages pass (some) arguments by reference. The basic concept of a pointer is easy: when you declare a variable in your program, it lives at some location in memory. Pointers simply contain the address of that location (they “point” to the variable). That means that through a pointer, you can get the actual value and modify it.

Now, back to functions. As mentioned earlier, most arguments are copied into functions. If a pointer is copied, it still points to the same address in memory, so you can modify the value. A simple example:

[golang]
var counter int
func increment(i int) {
i = i + 1
}
counter = 0
increment(counter)
fmt.Printf("%v", counter) // Prints 0
[/golang]

vs.

[golang]
var counter int
func increment(i *int) {
*i = *i + 1
}
counter = 0
increment(&counter)
fmt.Printf("%v", counter) // Prints 1
[/golang]

& creates a pointer, and * dereferences it (returns the value the pointer points to).

Remember I mentioned that almost everything is passed by value? It is important to know that maps, slices and channels are different, which can be quite confusing at first. The Golang FAQ has a detailed explanation of the reasoning behind this design decision. Another thing that receivers or arguments which have an interface type are not prepended by a *, but they still might be pointers!

Return values

In Go, functions can also have return values. If none is specified, the function returns nothing (as opposed to other languages where you might get nil or undefined which is actually something). In fact, trying to use the result of a function that has no return value is a compile error.

A more interesting characteristic is that Go allows for multiple return values. This is something most popular web programming languages lack, and it turns out to be very handy. Inside Go’s standard library it is commonly used for error handling. Consider the Writer interface:

[golang]
type Writer interface {
Write(p []byte) (n int, err error)
}
[/golang]

If Write is successful, n will be the number of bytes written and err will be nil. In the case writing failed, n will be 0 and err will hold the occured error. Consumer code can use this in the following fashion:

[golang]
if n, err := file.Write(p); err != nil {
fmt.Println("Could not write.")
}
[/golang]

The Writer interface shows another interesting bit about return values: they can be named in advance. This removes the need to initialize the variables inside the function. These named variables get automatically initialized to their zero-value and can be returned at any time in the function by a simple return statement.

Receivers

Up to here, we have discussed regular functions. If you’re familiar with object oriented languages, you will know that they have a concept similar to a function called a method, which is basically a function, but acts in the context of an object.

Go provides this functionality through receivers. However, there is a slight difference because anything can be a receiver. It could be a struct, but it can be just an integer as well. Also, the receiver might be a value or a pointer.

Take for example this method Name defined on a value of type User (which is a struct):

[golang]
type User struct {
firstname string
lastname string
}
func (user User) Name() string {
return user.firstname + " " + user.lastname
}
peter := User{firstname: "Peter", lastname: "Pan"}
fmt.Println(peter.Name()) // prints "Peter Pan"
[/golang]

Here, whenever Name() is called, the User struct is copied. Often, it is better to use a pointer receiver. Also, it is necessary to do so if you want to modify the receiver, for example like this:

[golang]
func (user *User) SwapName() {
firstname := user.firstname
user.firstname = user.lastname
user.lastname = firstname
}
paul := &User{firstname: "Paul", lastname: "Panther"}
paul.SwapName()
fmt.Println(paul.Name()) // prints "Panther Paul"
[/golang]

If this method were defined on the receiver user User, the name would only be swapped inside the function, and fmt.Println would have printed Paul Panther.

What would happen if we tried to call use peter.SwapName? peter is of type User (a value), the receiver however is of type *User (a pointer). Does this work? Interestingly, it does. Go is smart enough to create and pass a pointer to the User value, and then the function operates on that pointer, modifying the peter variable.

The dreaded null pointer

Another question you might have asked yourself while reading about receivers is what would happen if the receiver were nil? This is a common problem in many languages and I’m sure you have tried to call a method on an object that was nil at some point in your life as a programmer, and the result was not good at all. You then went forth and added safeguards to your code everywhere those null pointers might occur and ended up with very ugly code …

Go to the rescue! Let’s say we have a FirstUser() method which returns our first user. However, we don’t have any users yet, so the initial implementation will be:

[golang]
func FirstUser() *User {
return nil
}
[/golang]

Now, if we want to get the name of the first user in our code, we would write something like this:

[golang]
name := FirstUser().Name()
[/golang]

With code like this and because FirstUser() returns nil for now, Go will panic. The easiest way to fix this is to add a safeguard as mentioned above, but it’s not looking very pretty:

[golang]
user := FirstUser()
if user != nil {
name:= user.Name()
}
[/golang]

In Go we can deal with this in a better way: inside the functions. We can do this because in Go, nils can be typed:

[golang]
func (u *User) Name() string {
if u == nil {
return ""
}
return user.firstname + " " + user.lastname
}
[/golang]

In contrast to other languages, where you can’t call any method on nil objects, Go lets you call the function on the pointer receiver. So if there might be a scenario in which the receiver is nil, make sure to handle it inside the function so you don’t need the safeguard outside, making consumer code a lot less cluttered.

Closures

One final characteristic of Go functions is that they can be anonymous and used as closures, very similar to what you might know from JavaScript.

For example:

[golang]
func counter() func() int {
c := 0
return func() int {
c += 1
return c
}
}
[/golang]

The counter function has a return type of func() int, which means it returns a function with a return type of int. If we call the counter function and store its return value in a variable, we can call that variable (which holds a function value) several times and the counter will be increased (as the inner function has access to the same c variable):

[golang]
count := counter()
fmt.Println(count()) // Prints 1
fmt.Println(count()) // Prints 2
fmt.Println(count()) // Prints 3
[/golang]

Conclusion

Functions are a major building block of the Go language. They have some unusual features such as multiple return values and pass-by-copy which have a strong influence on the way programs are written. The implementation of “methods” through receivers is quite flexible and allows for more than just typical methods, such as nice handling of nils. However, Go functions also require the programmer to have a thorough understanding of values vs. pointers, and when to use which.