All you need to know about Go functions

Joash Xu
·Nov 3, 2021·

7 min read

Subscribe to my newsletter and never miss my upcoming articles

A function performs a specific task and allows you to group code into a reusable unit. Go functions can take zero or more arguments and can return zero or more return values. There are several ways you can declare a function, but they all follow the following format:

$$ \underset{1}{\textbf{func}}\ \underset{2}{FunctionName}(\underset{3}{p1\ \textbf{type}, p2\ \textbf{type}})\ \underset{4}{\textbf{return_type}} $$

A function statement consists of:

  1. The func keyword
  2. The name of the function. If you start with an upper case letter, it will be visible from other packages. If, on the other hand, you start with a lower case letter, this will not be the case, and it will be private.
  3. A comma-separated list of zero or more parameters and types in parentheses.
  4. Zero or multiple return types. The return type is written between the input parameter's closing parenthesis and the opening brace for the function body.

Go has a return keyword for returning values from a function. If a function returns a value, you must supply a return. If a function returns nothing, a return statement is not needed at the end of the function. The return keyword is only required in a function that returns nothing if you exit from the function before the last line.

Invoking functions is done by typing the function name and passing arguments for each function's parameters.

Example:

// A simple function with no parameters 
// and no return value
func PrintHello() {}

// Invoke the function
PrintHello()


// Function with parameters
func PrintHello2(from string, to string) {}
// Multiple parameters of the same type
func PrintHello2(from, to string) {}

// Call the function
PrintHello2("Me", "You")


// Function that return a value
func TheAnswer() int { 
    return 42 
}

// Call the function
value := TheAnswer()


// Return multiple values
func ReturnMulti() (int, string) { 
    return 42, "Hello!"
}

// Call the function
x, s := ReturnMulti()

Go does not allow unused variables. When a function returns multiple values, but you don't need to use one or more, assign the unused values to the name _. For example, if I am not going to use the string from ReturnMulti function, I can just write the assignment as x, _ := ReturnMulti()

Please note, you can not assign multiple returned values from a function to a single value. If you try, you will get a compile-time error.

Go also allows you to specify names for your return values like this:

func ReturnMulti()(n int, s string) {
    n = 42
    s = "Hello!"

    return n, s
}

Warning: When you use named return values in your functions, you can just write return without specifying the values that are returned like this:

func ReturnMulti()(n int, s string) {
    n = 42
    s = "Hello!"

    // n and s will be returned
    return
}

You might think this is great because, you know, less typing. However, this actually makes it harder to understand data flow. You have to scan back through the function to find the last value assigned to the return parameters to see what is returned. So just save yourself the trouble and make your code clear and readable by avoiding blank returns.

Variadic functions

Go also supports variadic parameters. The variadic parameter must be the last (or only) parameter in the input parameter list. To indicate that the parameter is variadic, you use three dots (...) before the type. The variable that is created for the function is a slice of the specified type.

$$ \textbf{func}\ FunctionName(params\ ...\textbf{type})\ \textbf{return_type} $$

Example:

func sumAll(args ...int) int {
    total := 0

    // The variable is a slice of int
    for _, v := range args {
        total += v
    }

    return total
}

result := sumAll(15, 20, 25, 30)
// result = 90

Function in Go is a first-class citizen

In Go, functions are a first-class citizen. This means that a function can be assigned to variables, passed as a function argument, and returned to another function.

func main() {
    // Assign a function to a name
    add := func(a, b int) int {
        return a + b
    }

    // Use the name to invoke the function
    fmt.Println(add(3, 4))
}

Function in Go is call-by-value

In Go, when you supply a variable for a parameter to a function, Go always makes a copy of the variable's value. Any changes to the variable will not affect the original value. This is true not only for primitive types but also composite types as well.

type person struct {
    age int
    name string
}

func unmodifiable(x int, s string, p person) {
    x = x * 2
    s = "Change!"
    p.name = "Brad"
}

func main() {
    p := person{19, "John"}
    x := 2
    s := "Hello"
    unmodifiable(x, s, p)
    fmt.Println(x, s, p)
    // This will print:
    // 2 Hello {19 John}
}

This copy by value also applies to pointers. If you change the copy of a pointer, you have not changed the original. This makes sense since the memory location was passed to the function via call-by-value.

func unmodifiablePointer(pX *int) {
    newNum := 20
    pX = &newNum
}

func main() {
    num := 15
    pNum := &num

    unmodifiablePointer(pNum)
    fmt.Println(*pNum)
    // This will print
    // 15
}

However, you can change the value of the pointer via dereferencing.

func updatePointer(pX *int) {
    *pX = 20
}

func main() {
    num := 15
    pNum := &num

    updatePointer(pNum)
    fmt.Println(*pNum)
    // This will print
    // 20
}

This is why you can change the value of map and slices in a function, and it will be reflected back to the calling function. Because map and slices are both implemented with pointers. Any changes made to a map parameter are reflected in the variable passed into the function. For a slice, it is a bit complicated. You can modify any element in the slice, but you can't lengthen the slice.

Anonymous functions

Go supports anonymous function. You declare an anonymous function with the keyword func immediately followed by the input parameters, the return values, and the opening brace. You don't have to assign them to a variable, either. You can write them inline and call them immediately.

func main() {
    // Assign an anonymous function to a name
    add := func(a, b int) int {
        return a + b
    }

    for i := 0; i < 10; i++ {
        // Declare anonymous function and invoke it
        // immediately.
        func(j int) {
            fmt.Println(j)
        }(i) 
    }
}

Closures

Go support closures. A closure is a function that is declared inside another function. This inner function can access values declared in the outer function.

func scope() func() int{
    outer_var := 2
    inner := func() int {
        return outer_var
    }

    return inner
}

We use closure in several scenarios

  1. Passing functions as arguments

There is a function in the sort package in the standard library called sort.Slice. It takes in any slice and is a function used to sort the slice passed in. Here we pass a function that compares people's last names. Notice that the function can access the people parameter.

sort.Slice(people, func(i, j int) bool{
    return people[i].LastName < people[j].LastName
})
  1. Returning functions from functions

Closures allow you to take the variables within your function and use those values outside of your function. And if you modify the outer variable inside the inner function, it does not change outer variables. In short, closures don't mutate outer variables; instead, they redefine them.

func outer() (func() int, int) {
    outerVar := 2
    inner := func() int {
        outerVar += 100
        return outerVar
    }
    return inner, outerVar
    // This will return 101, 2 (outerVar does not change!)
}

Defer

When you code, you often need to do some form of cleanup, like closing a file or closing network connections. And this cleanup has to happen, no matter how many exit points a function has. In Go, we use the defer statement.

func main() {
    defer fmt.Println("world")

    fmt.Println("hello")
    // This will output
    // hello
    // world
}

A defer statement defers the execution of a function until the surrounding function returns. The deferred calls' arguments are evaluated immediately, but the function will not be executed until the function returns.

If you have multiple defer statements, they will be pushed onto a stack. When a function returns, the deferred calls will be executed in the last-in-first-out order.

func main() {
    fmt.Println("counting")

    for i := 0; i < 2; i++ {
        defer fmt.Println(i)
    }

    fmt.Println("done")
    // This will print
    // counting
   // done
   // 1
   // 0
}
 
Share this