zoukankan      html  css  js  c++  java
  • 拒绝滥用golang defer机制

    原文链接 : http://www.bugclosed.com/post/17

    defer机制

    go语言中的defer提供了在函数返回前执行操作的机制,在需要资源回收的场景非常方便易用(比如文件关闭,socket链接资源十分,数据库回话关闭回收等),在定义资源的地方就可以设置好资源的操作,代码放在一起,减小忘记引起内存泄漏的可能。
    defer机制虽然好用,但却不是免费的,首先性能会比直接函数调用差很多;其次,defer机制中返回值求值也是一个容易出错的地方。

    一个简单的性能对比测试

    通过一个对锁机制的defer操作来比较性能差异。

    package main
    
    import (
    	"sync"
    	"testing"
    )
    
    var (
    	lock = new(sync.Mutex)
    )
    
    func lockTest() {
    	lock.Lock()
    	lock.Unlock()
    }
    func lockDeferTest() {
    	lock.Lock()
    	defer lock.Unlock()
    }
    func BenchmarkTest(b *testing.B) {
    	for i := 0; i < b.N; i++ {
    		lockTest()
    	}
    }
    func BenchmarkTestDefer(b *testing.B) {
    	for i := 0; i < b.N; i++ {
    		lockDeferTest()
    	}
    }
    

    运行命令go test -v -test.bench, 性能对比测试结果如下:

    BenchmarkTest-8        	100000000	        18.5 ns/op
    BenchmarkTestDefer-8   	20000000	        56.4 ns/op
    
    

    从测试结果可以看出,Defer版本的lock操作时间消耗几乎是函数直接调用的3倍以上。

    defer执行顺序和返回值求值

    看一个简单的测试:

    package main
    
    import (
    	"fmt"
    )
    
    func test_unnamed()(int) {
    	var i int
    	defer func() {
    		i++
    		fmt.Println("defer a:", i) 
    	}()
    	defer func() {
    		i++
    		fmt.Println("defer b :", i) 
    	}()
    	return i
    }
    func test_named()(i int) {
    	defer func() {
    		i++
    		fmt.Println("defer c:", i) 
    	}()
    	defer func() {
    		i++
    		fmt.Println("defer d :", i) 
    	}()
    	return i
    }
    
    func main() {
    	fmt.Println("return:", test_unnamed()) 
    	fmt.Println("return:", test_named()) 
    }
    

    执行结果是:

    defer b : 1
    defer a: 2
    return: 0
    defer d : 1
    defer c: 2
    return: 2
    

    关于同时有多个defer时的执行顺序,可以看做是go编译器为每个函数维护了一个先进后出的堆栈。每次遇到defer语句就讲执行体封装后压入堆栈中,等到函数返回时,从堆栈中依次出栈执行。所以 “defer b”语句在后,却先调用。

    关于函数求值问题,可以将test_unnamed函数返回和defer的执行和求值理解为3个步骤:

    1. 运行到“return i“语句时,取值当前i值,赋值给test_unnamed返回值,得到函数test的返回值0(因为test_unnamed中只定义了i,并未操作,i保留成初始默认值)。
    2. 按照先进后出的方式,一次调用defer语句执行。
    3. 执行真正的test_unnamed 函数返回 ”return“。

    以上是分析了匿名返回值的情况,具名返回值test_named的情况稍有不同,return 返回了2,而不是0,因为defer函数中对返回值变量i做了修改。

    由此可见,使用多个defer和defer函数中还需要处理返回值的情况下极容易出问题,使用时需要小心谨慎。

    defer释放锁

    通过defer释放锁(sync.Mutex)是很常见的场景,示例如下:

    def GetMapData(key uint32) uint32{
    	lock.Lock()
    	defer lock.Unlock()
    	if v, ok := mapData[key]; ok{
    		return v
    	}
    	return 0
    }
    

    在这样简单的场景下,通过defer直接释放锁,在后续的代码逻辑基本可以忘记锁的存在而写代码。但是这种模式就存在一个锁粒度的问题--整个函数都被锁住了。

    如果lock后面还有很多复杂或者阻塞的逻辑(写日志,访问数据库,从ch读取数据等),会导致锁的持有时间过大,影响系统的处理性能;此时可以精细控制逻辑函数的分拆,让锁尽量只控制共享资源,抛弃defer自行控制unlock,以免锁粒度过大。

    总结

    defer是一个很强大的机制,尤其是在资源释放的场景特别适用。但是使用时要注意,defer是有不小的性能损耗,且过度使用后也会导致逻辑变复杂。

  • 相关阅读:
    C#--带参SQL语句数通用数据访问类
    VS 2017产品密匙
    关于编码中的字符和字节问题
    关于C++中的cin用法
    C++基础(一、基本语法,Hello World)
    Oracle查看用户所在的表空间
    静态变量、枚举、以及静态代码块的使用场景
    Java 枚举(enum) 详解7种常见的用法
    第一章 对象和封装
    摘抄Java反射
  • 原文地址:https://www.cnblogs.com/bugmaking/p/9083365.html
Copyright © 2011-2022 走看看