golang中的defer使用方式及实战技巧

golang是一门简洁、高效、并发友好的编程语言,它提供了许多独特的特性,让程序员可以更容易地编写优雅和健壮的代码。其中一个特性就是defer语句,它可以让我们在函数返回之前执行一些清理或收尾的操作,比如关闭文件、释放资源、解锁互斥量等。defer语句的语法很简单,就是在要延迟执行的函数或方法前加上关键字defer,如下所示:

func main() {
    f, err := os.Open("test.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close() // 延迟关闭文件
}

在这个例子中,我们打开了一个文件,并在函数返回之前延迟关闭它,这样可以确保不会因为忘记关闭文件而造成资源泄露。

一、多个defer的执行顺序

defer语句的执行顺序是按照后进先出(LIFO)的原则,也就是说,最后一个defer语句会最先执行,第一个defer语句会最后执行。例如:

func main() {
    defer fmt.Println("A")
    defer fmt.Println("B")
    defer fmt.Println("C")
}

这个程序的输出是:

C
B
A

二、参数赋值的时间

参数的赋值对于有的同学来说一只都是一个难题,那defer是什么时候确定参数的值的呢?我们先来比较下面两个语句输出的差异:

func main() {
    x := 1
    defer fmt.Println(x)
    x = 2
}

这个程序的输出是:

1
	x := 1
	defer func() {
		fmt.Println(x)
	}()
	x = 2

这个程序的输出是:

2

如果熟悉go异步编程的小伙伴应该不会觉得惊讶。原因defer 调用的函数参数的值在 defer 定义时就确定了, 而 defer 函数内部所使用的变量的值需要在这个函数运行时才确定。所以如果我们想在第二个例子中输出1的话,需要做如下的改造:

func main() {
    x := 1
    defer func(x int) {
        fmt.Println(x) // x = 1
    }(x)
    x = 2
}

三、执行表达式

除了可以延迟执行函数或方法外,defer语句还可以延迟执行任意的表达式,只要它们能被求值为一个函数调用。例如:

func main() {
    defer println("Hello", "World") // 等价于 defer func() { println("Hello", "World") }()
}

这个程序的输出是:

Hello World

defer语句有很多实际的应用场景,下面我们介绍一些常见的使用技巧。

四、不可在循环中使用

defer 会延迟到函数返回前执行,如果函数不返回则不会执行,所以如果在一个循环中执行defer,可能并不会跟达到循环结束即释放资源的的预期,例如如下的函数便是一个错误的示例:

func readFiles(ch <-chan string) error {
    for path := range ch { 
        file, err := os.Open(path) 
        if err != nil {
            return err
        }
        defer file.Close() 
        // Do something with file
    }
    return nil
}

五、实际案例

1.延迟释放资源

这是最常见也最基本的使用技巧,就是在获取到某种资源后,立即使用defer语句来延迟释放它,这样可以避免因为忘记释放资源而导致内存泄露、文件占用、数据库连接过多等问题。例如,在打开文件、数据库连接、网络连接、互斥锁等场景中,都可以使用defer语句来保证资源的正确释放。

// 延迟释放文件资源
func readFile(filename string) ([]byte, error) {
    f, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer f.Close() //延迟释放文件资源

    data, err := ioutil.ReadAll(f)
    if err != nil {
        return nil, err
    }
    return data, nil
}
// 延迟关闭数据库连接
func dbQuery(db *sql.DB, query string) (*sql.Rows, error) {
    conn, err := db.Conn(context.Background())
    if err != nil {
        return nil, err
    }
    defer conn.Close() // 延迟关闭数据库连接

    rows, err := conn.QueryContext(context.Background(), query)
    if err != nil {
        return nil, err
    }
    return rows, nil
}

// 延迟关闭响应体
func httpGet(url string) (*http.Response, error) {
    client := &http.Client{}
    resp, err := client.Get(url)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close() // 延迟关闭响应体

    // do something with resp
    return resp, nil
}

//延迟解锁
func lockMutex(mu *sync.Mutex) {
    mu.Lock() // 加锁
    defer mu.Unlock() // 延迟解锁

    // do something with mu
}

2.延迟处理错误

有时候,我们可能需要在函数返回之前对某些错误进行处理,比如记录日志、发送通知、回滚事务等。这时候,我们可以使用defer语句来延迟处理错误,而不是在每个可能出错的地方都重复写相同的处理逻辑。例如:

func processFile(filename string) error {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer f.Close()

    var result error // 用于保存最终的错误

    defer func() {
        if result != nil {
            log.Println("process file failed:", result) // 延迟记录日志
        }
    }()

    data, err := ioutil.ReadAll(f)
    if err != nil {
        result = err // 保存错误
        return err
    }

    // do something with data

    return nil
}

在这个例子中,我们使用了一个名为result的变量来保存最终的错误,然后在defer语句中判断result是否为空,如果不为空,就记录日志。这样,无论函数是正常返回还是提前返回,都可以保证错误被正确处理。

3.延迟修改返回值

在golang中,函数可以有多个返回值,而且可以给返回值命名。这样的话,我们就可以在defer语句中修改返回值,从而实现一些特殊的效果。例如:

func add(a, b int) (sum int) {
    defer func() {
        sum += a + b // 延迟修改返回值
        fmt.Println("sum =", sum)
    }()
    return a + b
}

func main() {
    fmt.Println(add(1, 2))
}

这个程序的输出是:

sum = 6
6

在这个例子中,我们给返回值命名为sum,并在defer语句中对它进行了修改。这样,函数的最终返回值就是修改后的值。这种技巧可以用来实现一些特殊的功能,比如计算函数的执行时间、跟踪函数的调用栈、实现异常捕获等。例如:

func timeTrack(start time.Time, name string) {
	elapsed := time.Since(start)
	log.Printf("%s took %s", name, elapsed)
}

func slowFunc() {
	time.Sleep(time.Second * 2)
}

func main() {
	defer timeTrack(time.Now(), "slowFunc") // 延迟计算函数执行时间

	slowFunc()
}

这个程序的输出是:

slowFunc took 2.000123456s

4.延迟执行匿名函数

有时候,我们可能需要在函数返回之前执行一些额外的操作,比如打印日志、释放资源、修改状态等。如果这些操作只需要在一个地方执行,那么我们可以直接写在函数的最后;但如果这些操作需要在多个地方执行,那么我们就可以使用defer语句来延迟执行一个匿名函数,从而避免重复代码。例如:

func processUser(id int) error {
	user, err:= getUser(id)
    if err != nil {
        return err
    }

    defer func() {
        log.Println("process user done:", user.Name) // 延迟打印日志
        releaseUser(user) // 延迟释放用户
    }()

    // do something with user

    return nil
}

在这个例子中,我们使用了一个匿名函数来延迟执行两个操作:打印日志和释放用户。这样,无论函数是正常返回还是提前返回,都可以保证这两个操作被执行。

5.延迟执行可变参数函数

有时候,我们可能需要在函数返回之前执行一些可变参数的函数,比如fmt.Println、log.Printf等。这时候,我们可以使用defer语句来延迟执行这些函数,而不需要额外定义一个匿名函数来包裹它们。例如:

func printSum(a, b int) {
    defer fmt.Println("sum =", a + b) // 延迟打印和
}

func logError(err error) {
    defer log.Printf("error: %v", err) // 延迟记录错误
}

在这个例子中,我们直接使用了defer语句来延迟执行fmt.Println和log.Printf,而不需要像下面这样写:

func printSum(a, b int) {
    defer func() {
        fmt.Println("sum =", a + b)
    }()
}

func logError(err error) {
    defer func() {
        log.Printf("error: %v", err)
    }()
}

这样可以让代码更简洁和清晰。

六、最后

用好defer,会大大提高我们的代码可读性和稳健性,但是不合理的使用也可能会出现灾难性的结果,在平时的编码过程中,我们需要特别注意defer的各种特性,这样才能写出优秀的代码。

#go##golang##高并发##defer#
全部评论

相关推荐

牛客101244697号:这个衣服和发型不去投偶像练习生?
点赞 评论 收藏
分享
2 3 评论
分享
牛客网
牛客企业服务