Go - Functions

Functions

The structure of the functions in Go is very simple:

func foo (args type) ret type {
    //...magic
    return ret
}

However, there are some points that got my interest when I was developing in Go and noticed some weird behaviors. One of them is related to the Go typing system, there are some primitive types such as string, int and bool as well as user defined types, for example:

type Bartuka struct {
    name    string
    age     int
    height  int
    weight  int
    address Location
}

type Location struct {
    street  string
    number  int
    zipCode ZipCode
}

type ZipCode int64

When you have a function that accepts a pointer, this function will change your data in place. However, if your function accepts a value then the function will make a copy of the original input and manipulate it.

See the following example:

package main

import (
    "fmt"
)

func fooCopy(array [5]int) [5]int {
    array[0] = 100
    array[1] = 200
    array[2] = 300
    fmt.Println("The fooCopy function is done!! Check your data.")
    return array
}

func fooChange(array *[5]int) *[5]int {
    array[0] = 999
    array[1] = 987
    array[2] = 980
    fmt.Println("The fooChange function is done!! Check your data")
    return array
}

func main() {
    array := [5]int{1, 2, 3, 4, 5}
    fmt.Println("Original data:", array)
    fmt.Println("Call fooCopy")
    fooCopy(array)
    fmt.Println("Original data:", array)
    fmt.Println("Call fooChange")
    fooChange(&array)
    fmt.Println("Original data:", array)

}

This is very expected now that I know about it rsrsrs… However, this is not the only thing that is odd. I was playing with concurrency and the great goroutines. If you pass a large array to a function, you cannot use goroutines right, the compiler will complain about the size of your inputs and that is very comprehensible. Think about it, the goroutines are so lightweight that you could open millions of them, however if each goroutine need 8MB of ram only to get the data, then you are screwed. See the example below:

package main

import (
    "fmt"
    "time"
)

func foo(array *[1e6]int) {
    fmt.Println("len do array", len(array))
}

func main() {
    var array [1e6]int

    for i := 0; i < 10; i++ {
        go foo(&array)
    }

    time.Sleep(20 * time.Second)
}

This example works. However, each array has pretty much 8MB, if you try to run the same version of this code without the pointer receptor in the foo function. You will get the following message from the compiler.

fatal error: newproc: function arguments too large for new goroutine

Methods on types?

As I mentioned before, there are easy ways to build user custom types. The functions can be used to add actions to our datatypes. For example:

import "strings"

func (bart Bartuka) email() string {
    tmp := strings.ToLower(bart.name)
    mail := tmp + "@gmail.com"
    return mail
}

Now you can create an instance of my user Bartuka and call the method on it:

user := Bartuka{
    name: "Wanderson",
    age:  26,
}

fmt.Println(user.email())

Conclusion

The decision about the kind of variables your function will accept is not only related to the possibility to change or not the value in place. You must think about the nature of your custom type. Your type should never touch the original value? If this is the case, you have to take care when you receive the data inside the function if the pointer receptor is mandatory. I still need to verify more about this in my daily works. However, I think Go makes all these interactions very clear to the programmer.

Good to know!


Author | Wanderson Ferreira

Currently a MSc student in Universidade de São Paulo in the area of Deep Learning algorithms at the ICONE Research Group.