[Go]4.Function

Function in Go

Overview

Functions are declared using the keyword func in go. They have the following characteristics: - Functions can have variable number of parameters. - Functions are regarded as first-class citizen in Go. - Functions have keyword defer to work similar to finally in other programming language. - Parameters in go are all passed by values.

Declaration

The simpleset way to declare a function is:

func add(a int, b int) int {
    var sum int = a + b
    return sum
}

The function head func add(a int, b int) int means that the function's name is add, which have two parameters a and be and they are both of int type, so it can be simplified to func add(a, b int) int. The return type is also int. We can also specify the return variable in the function head, which means we declare a variable in the function head to hold the return value:

func add(a int, b int) (sum int) {
    sum = a + b
    return sum
}

We can also simplify the return statement:

func add(a int, b int) (sum int) {
    sum = a + b
    return
}

If we return some expression when we have declared named return values, funcions will first assign the value to the returning variable then return the returning variable:

func c() (i int) {
    i = 2
    defer func() { fmt.Println(i) }()
    return 1
}
// Output: 2

We can also return multiple values, which is very useful when we want to return errors during the computation:

func div(a, b int) (int, error) {
    var err error
    var result int
    if b == 0 {
        err = errors.new("The dividend can't be zero")
    } else {
        results = a / b
    }
    return result, err
}

We can use ellipsis to indicate there might be variable numbers of parameters of the same type:

func add(params ...int) (sum int) {
    for _, v := range params {
        sum += v
    }
    return
}

func main() {
    add(1, 2)                      // 3
    add(1, 2, 3)                   // 6
    slice := []int{1, 2, 3, 4, 5}  // 15
    add(slice...)                  // Break slice into integers
}

Functions are First-class Citizens

In Go, functions can be used just as values. They can be used as parameter, return values, and be assigned to variables. In this case, we call that functions are first-class citizens in one programming language.

myFunc := func (a, b int) int {
    return a + b
}

Function type can be used as value types:

type compute func(a, b int) int

Use function as a parameter for another function:

func filter(score []int, f func(int) bool) []int {
    reSlice := make([]int, 0)
    for _, v := range score {
        if f(v) {
            reSlice = append(reSlice, v)
        }
    }
    return reSlice
}

Defer in Function

There are some scenarios where it requires us to do a pair of operations to ensure the safety or efficiency of the program: - Open/Close files - Open/Close connections for database - Acquire/Release locks

Even experienced programmers may forget to do the latter operation in some branch of the code flow for the above pair operations, which is not promising. Go introduces defer keyword to overcome that. This will achieve similar effect as try-catch-finally statement block in other programming languages. Statements after defer will be called after the function finishes:

package main
import "fmt"

func main() {
    fmt.Println("begin main")
    defer sayHello()
    fmt.Println("end main")
}

func sayHello() {
    fmt.Println("hello")
}

// Output:
// begin main
// end main
// hello

This is a more convenient syntax compared to nested try-catch block which can be very nasty.

If we have multiple defer statements, they will be put into a stack then be executed at the end. So they will be executed in the reverse order:

func main() {
    fmt.Println("begin main")
    defer fmt.Println("defer test1")
    defer fmt.Println("defer test2")
    defer fmt.Println("defer test3")
}

// Output:
// begin main
// defer test3
// defer test2
// defer test1

The expression/arguments in the defer statement will be evaluated when defer statement is evaluated:

func a() {
    i := 0
    defer fmt.Println(i)
    i++
    return  // return 0 here
}

A variant can be:

func a() {
    i := 0
    defer func() {
        fmt.Println(i)
    }
    i++
    return  
    // return 1 here, because i is not used as a parameter here
    // The deferred function reads i's value after i is increased
}

Deferred functions can read and assign to the returning function's named values:

func c() (i int) {
    defer func() { i++ }()
    return 1 // return 2 here
}

Panic and Recover

These two concepts are related to exception handling too. Instead of error, this mechanism is more like exception in other programming languages which can propagate through the functions calling chain.

Panic can stop the current function executing the rest of the code, and recursively execute statements in defer stack in the current Goroutine.

Panic will only trigger the defer in the current Goroutine:

func main() {
    defer println("in main")
    go func() {
        defer println("in goroutine")
        panic("")
    }()

    time.Sleep(1 * time.Second)
}
// output: 
// in goroutine
// panic:

You can see the defer statement in the outer scope is not executed.

recover is only effective in defer, which means it's effective only if it's executed after the panic, so it must be used with defer.

Nested panic:

func main() {
    defer fmt.Println("in main")
    defer func() {
        defer func() {
            panic("panic again and again")
        }()
        panic("panic again")
    }()

    panic("panic once")
}

$ go run main.go
in main
panic: panic once
    panic: panic again
    panic: panic again and again

goroutine 1 [running]:
...
exit status 2

If used correctly, recover can catch panic and make the function returns normally, withouth breaking the whole program:

package main

import (
    "fmt"
)

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in main", r)
        }
    }()

    panic("Panic in main!")
}

// Output:
// Recovered in main
// Panic in main!

Reference