December Code

Go functions and defer interview questions, with answers

Functions are where Go's design choices show most clearly: several return values instead of exceptions, named results, closures that capture variables, and defer, which schedules cleanup for the moment a function returns. Interviewers like defer in particular, because its rules are short but easy to misapply: when its arguments are evaluated, in which order deferred calls run, and what a deferred function can still change.

The answers below cover those rules with code built and run on Go 1.27, starting with how functions return values and moving through defer, closures and variadic parameters to the question of what Go passes when it passes an argument. Then take the free Go diagnostic — ten questions across every Go topic in the bank — to see which of these you can explain but not yet predict.

The questions, with answers

  1. 1.How do multiple return values work in Go?

    In short: A function lists several result types and returns them together; the caller assigns them in order and discards any it does not need with _.

    A Go function can return any number of values, declared as a parenthesised list of types after the parameters, and a return statement supplies all of them. The caller receives them with a multiple assignment, in the same order, and must take all of them or none: to ignore one, assign it to the blank identifier _. The most common use is returning a result and an error, (T, error), which is how Go reports failure without exceptions, and the comma-ok idiom for map lookups and type assertions follows the same pattern. The dm function below returns a quotient and a remainder at once, which would need an out-parameter or a struct in many other languages.

    func dm(a, b int) (int, int) {
        return a / b, a % b
    }
    
    func main() {
        q, r := dm(17, 5)
        fmt.Println(q, r)
        // 3 2
        _, r2 := dm(9, 4)
        fmt.Println(r2)
        // 1
    }
  2. 2.What are named result parameters in Go?

    In short: Results can be given names in the signature; they start at their zero values, act as local variables, and a bare return returns their current values.

    A signature such as func split(n int) (a, b int) names its results. They are ordinary local variables, initialised to their zero values when the function starts, and a bare return statement returns whatever they hold at that moment. Named results document what each value means, which helps when a function returns two values of the same type, such as a width and a height. The Go style guides recommend bare returns only in short functions, because in a long one a reader has to search for the last assignment to know what is returned.

    func split(n int) (a, b int) {
        a = n / 3
        b = n - a
        return
    }
    
    func main() {
        fmt.Println(split(10))
        // 3 7
    }
  3. 3.What does defer do, and in what order do deferred calls run?

    In short: defer schedules a call to run when the surrounding function returns; several deferred calls run in last-in, first-out order.

    A defer statement pushes a function call onto a list that runs when the surrounding function returns, whether it returns normally, through an early return or because of a panic. That makes it the natural place for cleanup: defer f.Close() right after opening a file, or defer mu.Unlock() right after locking, keeps the release next to the acquisition. Deferred calls run in reverse order of deferral, last in, first out, so resources are released in the opposite order to how they were acquired, as the code shows. defer is tied to the function, not to a block or a loop iteration, so deferring inside a long loop keeps every resource open until the function ends.

    func main() {
        defer fmt.Println("one")
        defer fmt.Println("two")
        fmt.Println("body")
        // body
        // two
        // one
    }
  4. 4.When are a deferred call's arguments evaluated in Go?

    In short: Immediately, when the defer statement runs; only the call itself is delayed, so a deferred closure is needed to see a variable's later value.

    defer evaluates the function value and its arguments at the moment the defer statement executes and stores them; only the call waits until the function returns. So defer fmt.Println("arg", n) captures the n of that moment, and later changes to n do not affect it. A deferred closure is different: it has no arguments to evaluate, and reads n when it finally runs, so it sees the last value. In the code, n is 1 when the first defer runs and 20 by the time the closure executes, and because deferred calls run in reverse order, the closure prints first. The same rule means defer mu.Unlock() is safe, while defer log(time.Since(start)) measures almost nothing.

    n := 1
    defer fmt.Println("arg", n)
    n = 10
    defer func() {
        fmt.Println("closure", n)
    }()
    n = 20
    // closure 20
    // arg 1
  5. 5.What is a closure in Go?

    In short: A closure is a function value that refers to variables from the scope it was created in, and keeps them alive for as long as the function exists.

    Function literals in Go can use variables declared outside them, and they capture the variables themselves, not copies of their values. The captured variables live as long as the function value does, even after the function that declared them has returned, which is how counter below keeps c between calls: each call to next increments the same c. Each call to counter creates a new c, so two counters are independent. Closures are how Go writes callbacks, iterators and middleware. Because capture is by variable, a closure that is called later sees the variable's latest value, which is the usual source of surprises with goroutines and deferred calls.

    func counter() func() int {
        c := 0
        return func() int {
            c++
            return c
        }
    }
    
    func main() {
        next := counter()
        next()
        fmt.Println(next(), next())
        // 2 3
    }
  6. 6.How do variadic functions work in Go?

    In short: A final parameter written ...T accepts any number of T arguments, which the function receives as a slice; a slice can be passed with s... .

    Declaring the last parameter as ns ...int lets callers pass zero or more ints, which the function sees as a []int, so sum() is 0 and sum(1, 2) is 3. To pass an existing slice, expand it with xs..., as fmt.Println does with its ...any parameter. When a slice is passed that way, no copy is made: the function's ns shares the caller's backing array, so a variadic function that writes to its parameter changes the caller's data. Only the last parameter can be variadic, and a call cannot mix individual arguments with an expanded slice.

    func sum(ns ...int) int {
        t := 0
        for _, n := range ns {
            t += n
        }
        return t
    }
    
    func main() {
        fmt.Println(sum(1, 2))
        // 3
        xs := []int{4, 5, 6}
        fmt.Println(sum(xs...))
        // 15
    }
  7. 7.Are Go function arguments passed by value or by reference?

    In short: Always by value: the function gets a copy, but a copy of a slice, map, pointer or channel still refers to the same underlying data.

    Go passes every argument by value, copying it into the parameter. For an int or a struct that is a full copy, so the caller's variable cannot change. A slice, though, is a small header holding a pointer to a backing array, a length and a capacity, and copying the header still points at the same array: writing s[0] inside set changes the caller's first element. Changing the header itself, as append does when it needs a bigger array, only changes the function's copy, so the caller never sees the appended 5. Maps and channels behave like pointers for the same reason. To let a function replace a value, pass a pointer or return the new value.

    func set(s []int) {
        s[0] = 9
        s = append(s, 5)
    }
    
    func main() {
        a := []int{1, 2}
        set(a)
        fmt.Println(a)
        // [9 2]
    }

How the diagnostic asks it

One question from the Go bank, exactly as a sitting would show it. The bank has 3 on functions & defer and 30 across Go.

Functions & Defer · mediumGO-006

What does this Go code print?

x := 1
defer fmt.Println("deferred:", x)
x = 2
fmt.Println("now:", x)
  1. 1now: 2, then deferred: 2
  2. 2deferred: 1, then now: 2
  3. 3It does not compile: x is changed after defer uses it
  4. 4now: 2, then deferred: 1correct

A defer statement evaluates the function value and its arguments at once and saves them; only the call itself waits until the function returns. So the deferred Println captured "deferred:" and the value 1 when the defer line ran, and changing x to 2 afterwards does not affect it. main prints now: 2, then returns, and the deferred call prints deferred: 1. deferred: 2 would need a closure, defer func() { fmt.Println(x) }(), which reads x when it finally runs. Printing deferred first ignores that defer waits for the return. Changing a variable after deferring a call that used it is legal.

Measure it

Reading answers tells you what’s true. A diagnostic tells you what you get wrong.

10 Go questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.

What the readiness test measures · how the score is computed

By Harshit · updated