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#
全部评论

相关推荐

和蔼:在竞争中脱颖而出,厉害! 但是有一个小问题:谁问你了?😡我的意思是,谁在意?我告诉你,根本没人问你,在我们之中0人问了你,我把所有问你的人都请来 party 了,到场人数是0个人,誰问你了?WHO ASKED?谁问汝矣?誰があなたに聞きましたか?누가 물어봤어?我爬上了珠穆朗玛峰也没找到谁问你了,我刚刚潜入了世界上最大的射电望远镜也没开到那个问你的人的盒,在找到谁问你之前我连癌症的解药都发明了出来,我开了最大距离渲染也没找到谁问你了我活在这个被辐射蹂躏了多年的破碎世界的坟墓里目睹全球核战争把人类文明毁灭也没见到谁问你了
点赞 评论 收藏
分享
2 3 评论
分享
牛客网
牛客企业服务