← Back to articles

Learning Go (8): functions, defer, and errors

Part 8 of my Go fundamentals notes: syntax, examples, and lessons learned.

Functions

Go supports ordinary functions, anonymous functions, closures, and methods.

Functions are first-class values: they can be assigned, passed as arguments, and returned.

A defined function type with methods can satisfy an interface.

1. Declaring a function

Basic declaration syntax:

func 函数名(参数列表) (返回值列表) {
    函数体
}

Parameters can be omitted or contain several comma-separated values.

Results can be omitted or contain several values. A function with results must return them on its normal return paths.

A simple example:

func add(a, b int, c float32) (int, error) {
    return a + b, nil
}

Go passes arguments by value: a function receives a copy. Reassigning a parameter does not reassign the caller’s variable.

For slices, copied descriptors can share a backing array, so modifying shared elements can affect the caller.

When growth requires a new backing array, the returned slice uses separate storage; modifications there do not affect the old array.

Result parameters can also be named. They become variables within the function body and need not be declared again.

Example:

func add(a, b int) (sum int, err error) {
    sum = a + b
    return sum, err
    //也可以直接写return
}

2. Variadic parameters

When the number of arguments varies, use a variadic parameter.

Basic syntax:

func 函数名(参数列表...类型) (返回值列表) {
    函数体
}

A simple example:

func add(a ...int)(sum int, err error) {
    for _, value := range a {
        sum += value
    }
}
c := 1
d := 2
sum, _ := add(a, b, 3, 4)
fmt.Println(sum)

Here a is a slice of int inside the function. Access its elements with a[0], a[1], or a loop.

3. Functions as first-class values

Functions can be arguments, return values, or assigned variables.

This makes them flexible and reusable.

3.1. Assign a function to a variable

Example:

func add(a ...int)(sum int, err error) {
    for _, value := range a {
        sum += value
    }
}

func main(){
    //这里将add函数作为参数赋值给变量funcVar
    funcVar := add
    c := 1
    d := 2
    sum, _ := funcVar(a, b, 3, 4)
    fmt.Println(sum)
}

3.2. Return a function

Example:

func cal(op string) func(){
    switch op {
    case "+":
        return func(){
            fmt.Println("这是加法")
        }

    case "-":
        return func(){
            fmt.Println("这是减法")
        }
    default:
        return func(){
            fmt.Println("这不是加法也不是减法")
        }
    }
}

//因为返回的是函数,所以在调用的时候需要加上()
cal("+")()

3.3. Pass a function as an argument

A small example intended to demonstrate the syntax:

func add(a, b int) int{
    fmt.Printf("sum is %d\n", a + b)
}
func cal(y int, myfunc func(int, int)) {
    myfunc(y, 2)
}

cal(1, add)

//输出结果为sum is 3
//调用cal函数传递add函数成为myfunc, 然后1+2

3.4. Anonymous functions

An anonymous function has no declared function name and can be passed or returned as a value.

It can be declared directly where that value is needed.

//匿名函数作为参数进行传递
func add(a, b int) int{
    fmt.Printf("sum is %d\n", a + b)
}
func cal(y int, myfunc func(int, int)) {
    myfunc(y, 2)
}

cal(1, func(a, b int){
    fmt.Printf("total is %d\n", a + b)
})

//输出结果为total is 3
//调用cal函数传递临时定义的匿名函数函数成为myfunc, 然后1+2


//匿名函数作为变量进行赋值
func cal(y int, myfunc func(int, int)) {
    myfunc(y, 2)
}
localFunc := func(a, b int){
    fmt.Printf("local is %d\n", a + b)
}

cal(1, localFunc)

//输出结果为local is 3
//调用cal函数传递localFunc成为myfunc, 然后1+2,跟上边的效果是一样的

4. Closures

Suppose we want a function that increments its counter each time it is called.

One approach is a package-level variable that is incremented and returned on each call.

Using a package-level variable:

var local int
func add() int{
    local += 1
    return local
}

for i := 0; i < 5; i++ {
    fmt.Println(add())
}

This creates shared global state and makes independent counters or resetting the state awkward.

A closure provides another way:

func auotoAdd() func() int {
    local := 0
    return func() int {
        return local
    }
}

nextFunc := auotoAdd()
for i := 0; i < 5; i++ {
    fmt.Println(nextFunc())
}
//输出结果为0 1 2 3 4
//想要归零的话,只需要重新调用auotoAdd()函数即可

A closure is a function that retains access to variables from its surrounding scope, even after the surrounding function has returned.

For example, an anonymous function can capture a local variable of its enclosing function.

5. Uses of defer

defer schedules a call to run when the surrounding function returns, commonly for resource cleanup. Its purpose resembles finally, though the semantics differ.

Files, locks, and other resources need cleanup on both success and failure to avoid leaks.

// 一个在Java中的例子
try {
    // 可能会发生异常的代码
} catch (Exception e) {
    // 异常处理代码
} finally {
    // 无论是否发生异常,都会执行的代码
}

In Java, cleanup in finally may be far from acquisition. In Go, defer can keep the two close together.

Basic usage:

var mu sync.Mutex

mu.lock()
defer mu.unlock() //defer后面的代码会在return之前执行

Putting defer next to resource acquisition makes cleanup easier to notice and the code simpler.

defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("4")
//输出结果为4 3 2 1

Deferred calls run in last-in, first-out order, like a stack.

A deferred closure can change a named result parameter.

// 在这个例子中,defer 推迟执行的是一个匿名函数
// deferReturn 函数被调用,准备返回值 10。
// 在返回之前,defer 推迟执行的匿名函数被执行。
// 匿名函数中,result 被增加 1,因此 result 的值从 10 变为 11。
// deferReturn 函数返回修改后的 result 值,即 11。
// 因此,尽管 deferReturn 函数的返回语句是 return 10,但由于 defer 推迟执行的函数在返回之前修改了 result 的值,所以最终的输出结果是 11。这也展示了在 Go 语言中,defer 可以修改函数的返回值。

// 搞不懂就看源码

func deferReturn() (result int){
    defer func(){
        result++
    }()
    return 10
}

ret := deferReturn()
fmt.Println(ret) //输出结果为11

6. error, recover, and panic

Three important concepts in Go error handling are:

1、error

2、panic

3、recover

Use error values for ordinary failure rather than exceptions as routine control flow.

Some other languages surround operations with try-catch blocks. {: .prompt-tip }

A Go function usually returns an error value to tell its caller whether it succeeded. Callers should handle it; hence the frequent “if err != nil” checks.

Explicit error handling is part of defensive programming.

panic is separate from ordinary error returns. It unwinds the current goroutine while running deferred calls. A suitable deferred recover can stop that panic; otherwise the program terminates.

6.1、error

The errors package helps create error values. For example:

import (
    "fmt"
    "errors"
)

func A() (int, error) {
    return 1, errors.New("这是一个错误")
}

func main() {
    if _, err := A(); err!= nil {
        fmt.Println(err)
    }
}

6.2、panic

panic is built in. An unrecovered panic terminates the program, so it is not a routine replacement for returning errors.

Another example:

import (
    "fmt"
    "errors"
)

func A() (int, error) {
    panic("this is a panic")
    fmt.Println("能执行到这吗?")
    return 1, errors.New("这是一个错误")
}

func main() {
    if _, err := A(); err!= nil {
        fmt.Println(err)
    }
}

// 在上面的例子中,panic会导致程序退出,所以fmt.Println("能执行到这吗?")不会被执行,只会打印错误栈

Use panic carefully. One situation discussed here is startup failure:

A service must prepare required dependencies, such as configuration, logging, and database access, before starting.

If an essential startup requirement cannot be met, fail startup explicitly; panic is one possible mechanism, though returning an error is often sufficient.

Once a server is running, an unexpected unrecovered panic can bring down the process and become a serious incident.

Runtime checks can also trigger panic, for example:

  1. An out-of-range index

  2. Dereferencing a nil pointer

……

recover can stop a panic when called in the correct deferred context. It does not continue execution at the statement that panicked.

recover returns the value supplied to panic, exposed through an interface value.

6.3、recover

Example:

// 在go中map的使用需要进行初始化,否则会报错,这是一个会被动触发panic的场景

func A() {
    defer func(){
        if err := recover(); err!= nil {
            fmt.Println("recoverd is A(): ", err)
        }
    }()
    var names map[string]string
    names["xiaoming"] = "go工程师"
    return 0, errors.New("这是一个错误")
}
// 上面的代码会输出recoverd is A(): assignment to entry nil map

Other details to remember:

  1. recover is effective when called directly by a deferred function in the panicking goroutine.

  2. The deferred call must already have been registered before the panic.

  3. After recovery, execution does not resume at the panic location.

  4. Multiple deferred calls run in reverse registration order.

About this article’s date

The date recorded on the old site is retained; writing may have started earlier.

In this series · Go fundamentals

PreviousLearning Go (7): arrays, slices, maps, and lists ←NextLearning Go (9): structs and methods →