Basics

Basic topics to cover
  • Example
  • Simulating Named and Optional Parameters
  • Variadic Input Parameters and Slices
  • Multiple Return Values
  • Multiple Return Values Are Multiple Values
  • Named Return Values
  • Blank Returns - Never Use These!

Example


    func div(numerator int, denominator int) int {
        if denominator == 0 {
            return 0
        }
        return numerator / denominator
    }

    /* in case of multiple parameters of same types - club them together */
    func div(numerator, denominator int) int {

    /* in case of multiple return types */
    func div(numerator, denominator int) (int, float) {
        

Simulating Named and Optional Parameters

  • Go doesn't have named and optional input parameters
  • If you want to emulate named and optional parameters, define a struct that has fields that match the desired parameters, and pass the struct to your function.

    /* Using a struct to simulate named parameters */
    type MyFuncOpts struct {
        FirstName string
        LastName string
        Age int
    }

    func MyFunc(opts MyFuncOpts) error {
        // do something here
    }

    func main() {
        MyFunc(MyFuncOpts {
            LastName: "xyz",
            Age: 50,
        })
        My Func(MyFuncOpts {
            FirstName: "abc",
            LastName: "bcd",
        })
    }
        

Variadic Input Parameters and Slices

  • Go supports variadic parameters.
  • The variadic parameter must be the last (or only) parameter in the input parameter list.
  • You indicate it with three dots (…) before the type.

    func addTo(base int, vals ...int) []int {
        out := make([]int, 0, len(vals))
        for _, v := range vals {
            out = append(out, base+v)
        }
        return out
    }
        

Multiple Return Values

  • Go allows multiple return values.
  • When a Go function returns multiple values, the types of the return values are listed in parentheses, separated by commas.
  • Also, if a function returns multiple values, you must return all of them, separated by commas.
  • Don't put parentheses around the returned values; that's a compile-time error.

    func divAndRemainder(numerator int, denominator int) (int, int, error) {
        if denominator == 0 {
            return 0, 0, errors.New("cannot divide by zero")
        }
        return numerator / denominator, numerator % denominator, nil
    }
        

Multiple Return Values Are Multiple Values

  • Python function returns multiple values in a tuple to a single variable. That's not how Go works. You must assign each value returned from a function. If you try to assign multiple return values to one variable, you get a compile-time error

    q, r, err := divAndRemainder(10, 3) # q = 3, r = 1, err = nil
        

Ignoring Returned Values

  • Go does not allow unused variables. If a function returns multiple values, but you don't need to read one or more of the values, assign the unused values to the name _
  • Go does let you implicitly ignore all of the return values for a function.

    q, _, err := divAndRemainder(10, 3) # q = 3, r = 1, err = nil
        

Named Return Values

  • Go allows you to specify names for your return values.
  • Naming return values means pre-declaring variables that you use within the function to hold the return values
  • You must surround named return values with parentheses, even if there is only a single return value.
  • Named return values are initialized to their zero values when created. This means that we can return them before any explicit use or assignment.
  • If you only want to name some of the return values, you can do so by using _ as the name for any return values you want to remain nameless.
  • Go compiler inserts code that assigns whatever is returned to the return parameters. (see first example below for clarity) - The named return parameters give a way to declare an intent to use variables to hold the return values, but don't require you to use them.
Problems associated with named return variables
  • Shadowing: Just like any other variable, you can shadow a named return value. Be sure that you are assigning to the return value and not to a shadow of it.
  • you don't have to return them.

    func divAndRemainder(numerator, denominator int) (result int, remainder int, err error) {
        result, remainder = 20, 30
        if denominator == 0 {
            return 0, 0, errors.New("cannot divide by zero")
        }
        return numerator / denominator, numerator % denominator, nil
    }
    q, r, err := divAndRemainder(5,2) # q = 2, r = 1, err = nil
    ''' even though result and remainder were not assigned the correct values, Go compiler does it implicitly. '''
        

Blank Returns - Never Use These!

  • If you have named return values, you can just write return without specifying the values that are returned. This returns the last values assigned to the named return values.
  • If your function returns values, never use a blank return. It can make it very confusing to figure out what value is actually returned.
Problems associated with blank returns
  • In case we return without assigning any values to named return values, their zero values are returned.
  • Even though the return is blank, we still need to have a return at the end of the function. Otherwise it's a compile error.

    func divAndRemainder(numerator, denominator int) (result int, remainder int, err error) {
        if denominator == 0 {
            err = errors.New("cannot divide by zero")
            return
        }
        result, remainder = numerator/denominator, numerator%denominator
        return
    }
        

Functions are values

  • The type of a function is built out of the keyword func and the types of the parameters and return values. This combination is called the signature of the function.

    /* declare methods */
    func add(i int, j int) int { return i + j }
    func sub(i int, j int) int { return i - j }
    func mul(i int, j int) int { return i * j }
    func div(i int, j int) int { return i / j }

    /* declare map -> { char -> function } */
    var opMap = map[string]func(int, int) int{
        "+": add,
        "-": sub,
        "*": mul,
        "/": div,
    }

    /* call the functions */
    func calculator(p1 int, op string, p2 int) {
        opFunc, ok := opMap[op]
        if !ok {
            fmt.Println("unsupported operator:", op)
            continue
        }
        fmt.Println(opFunc(p1, p2));
    }
        

Function type declaration

In the above example, instead of function signature, we can use function type.

    /* alternatively use function type declaration */
    type opFuncType func(int,int) int // declaring this signature as a variable "opFuncType"
    var opMap = map[string]opFuncType {
        "+": add,
        "-": sub,
        "*": mul,
        "/": div,
    }
        

Anonymous function

  • Declare an anonymous function with the keyword func immediately followed by the input parameters, the return values, and the opening brace.
  • It is a compile-time error to try to put a function name between func and the input parameters
  • Just like any other function, an anonymous function is called by using parenthesis.

    for i := 0; i < 5; i++ {
        func(j int) {
            fmt.Println("printing", j, "from inside of an anonymous function")
        }(i)
    }
        

Closures

  • The functions declared inside of functions are able to access and modify variables declared in the outer function.
  • limits a function's scope. If a function is only going to be called from one other function, but it's called multiple times, you can use an inner function to “hide” the called function.

Passing Functions as Parameters

  • Since functions are values and you can specify the type of a function using its parameter and return types, you can pass functions as parameters into functions.

    type convert func(int) string

    func quote123(fn convert) string {
        return fmt.Sprintf("%q", fn(123))
    }

    func main() {
        foo := func(x int) string { return "foo" }
        result = quote123(foo)
        fmt.Println(result) // foo
    }
        

Returning Functions from Functions


    func makeMult(base int) func(int) int {
        return func(factor int) int {
            return base * factor
        }
    }
    func main() {
        twoBase := makeMult(2)
        threeBase := makeMult(3)
        for i := 0; i < 3; i++ {
            fmt.Println(twoBase(i), threeBase(i)) # (0,0), (2,3), (4,6)
        }
    }
        

Defer

  • Normally, a function call runs immediately, but defer delays the invocation until the surrounding function exits. - Hence used for cleanup.
  • you can supply a function with input parameters to a defer.
  • you can defer multiple closures in a Go function. They run in last-in-first-out order; the last defer registered runs first.
  • The code within defer closures runs after the return statement.
  • there's a way for a deferred function to examine or modify the return values of its surrounding function and it's the best reason to use named return values.
  • A common pattern in Go is for a function that allocates a resource to also return a closure that cleans up the resource. - and that's where defer comes into picture.

    ''' cleanup using defer '''
    func getFile(name string) (*os.File, func(), error) {
        file, err := os.Open(name)
        if err != nil {
            return nil, nil, err
        }
        return file, func() {
            file.Close()
        }, nil
    }

    func main() {
        f, closer, err := getFile(os.Args[1])
        if err != nil {
            log.Fatal(err)
        }
        defer closer()
    }

    /* Because Go doesn't allow unused variables, returning the closer from the function means that the program will not compile if the function is not called. That reminds the user to use defer. */

    ''' accessing variables of outer function in defer func() - only when named return are used '''
    func DoSomeInserts(ctx context.Context, db *sql.DB, value1, value2 string) (err error) {
        tx, err := db.BeginTx(ctx, nil)
        if err != nil {
            return err
        }
        defer func() {
            if err == nil {
                err = tx.Commit()
            }
            if err != nil {
                tx.Rollback()
            }
        }()
        _, err = tx.ExecContext(ctx, "INSERT INTO FOO (val) values $1", value1)
        if err != nil {
            return err
        }
        // use tx to do more database inserts here
        return nil
    }

        
  • You can supply a function that returns values to a defer, but there's no way to read those values.

Go is Call-By-Value

  • It means that when you supply a variable for a parameter to a function, Go always makes a copy of the value of the variable.
  • except for maps and slices, which can be modified (bcoz they are implemented with pointers). However for a slice, you can modify any element, but can't lengthen the slice.

    /* no modification for int, string and struct */

    func modifyFails(i int, s string, p person) {
        i = i * 2
        s = "Goodbye"
        p.name = "Bob"
    }

    func main() {
        p := person{}
        i := 2
        s := "Hello"
        modifyFails(i, s, p)
        fmt.Println(i, s, p) // i = 2, s = Hello, p = {0 }
    }

    /* modified values for maps and slices */

    func modMap(m map[int]string) {
        m[2] = "hello"
        m[3] = "goodbye"
        delete(m, 1)
    }

    func modSlice(s []int) {
        for k, v := range s {
            s[k] = v * 2
        }
        s = append(s, 10)
    }

    func main() {
        m := map[int]string{
            1: "first",
            2: "second",
        }
        modMap(m)
        fmt.Println(m) // map[2:hello 3:goodbye]

        s := []int{1, 2, 3}
        modSlice(s)
        fmt.Println(s) // [2 4 6]
    }
        
  • Every type in Go is a value type. It's just that sometimes the value is a pointer.