Go函数使用注意事项总结(逃逸分析、匿名函数、闭包函数)

函数声明方式:

func 函数名(形参列表)(返回值列表){
	执行语句
    return 返回值列表
}

栈区:Go语言的栈区一般存储基本数据类型,编译器存在一个逃逸分析

堆区:Go语言的堆区一般存储引用数据类型,编译器存在一个逃逸分析

没有基础的童鞋可能会比较迷惑什么是逃逸分析?首先,你需要知道什么是内存逃逸。

内存逃逸

在C、C++中,常常会忘记分配完内存后忘记释放,从而导致内存泄露,大量的内存泄露对程序来说是致命的。在C语言中,只要不是malloc、全局变量、静态局部变量的都是局部变量,分配在栈区,当函数返回一个局部变量的地址的时候,我们就称作这个变量想要逃逸,即 内存逃逸。

知道了什么是内存逃逸,下面我们简单介绍下什么是逃逸分析。

在Go语言的编译的过程中会进行逃逸分析,即分析这个变量,或者说这块内存是否想要逃逸,如果想要逃逸,则将其分配在堆区;否则分配在栈区。

逃逸分析的好处

  • 使得内存分配的更加合理。说白了就是“找准最适合自己呆的地方”,当你使用malloc/new申请一块内存时,编译器发现你在函数退出后没有再使用过它,就会将其存放在栈区。通样的,如果一个普通变量,经过编译器分析当函数推出后其还有在其他地方被引用,那么就会将其分配在堆区。
  • 减少了GC[垃圾回收]的压力。如果变量都分配到堆上,堆不像栈可以自动清理。它会引起Go频繁地进行垃圾回收,而垃圾回收会占用比较大的系统开销
  • 提高效率。堆和栈相比,分配速度显著低于栈,因为堆分配内存需要通过指针一个一个的去找合适的内存块。

Go逃逸分析的基本原则

一个函数返回一个变量的引用,就会发生逃逸。如果函数return之后,确定变量不再被引用,则将其分配到栈上,否则编译器就会将变量分配到堆上。而且,如果一个局部变量非常大,那么它也应该被分配到堆上而不是栈上。

发生内存逃逸的几种情况

  • 局部变量被返回

  • interface{}动态类型

    很多函数参数为interface{}空接口类型,都会造成逃逸,比如func Println(a ...interface{}) (n int, err error)

    func Printf(format string, a ...interface{}) (n int, err error)

  • 栈空间不足

    比如你给栈空间分配一个超大内存的切片,就会发生逃逸

内存逃逸的弊端

提问:函数传递指针真的比传值效率高吗?

我们知道传递指针可以减少底层值的拷贝,可以提高效率,但是如果拷贝的数据量小,由于指针传递会产生逃逸,可能会使用堆,也可能会增加GC的负担,所以传递指针不一定是高效的。

事物都有优有缺,重要的是在合适的地方去合适的运用,才能将其优势发挥出来

参考文章:
Golang内存分配逃逸分析
Golang 内存分配之逃逸分析

好了,上面只是一个小插曲,有兴趣的童鞋也可以继续深入了解。接下来我们继续了解Go语言的函数。

Go函数支持返回多个值,这一点相比于其他语言很有独特性

func SubAdd(a,b int) (int,int){
	return a + b, a - b  //返回和 差
}

注意:如果返回多个值,在接收时,希望忽略某个返回值,则使用_符号占位忽略

函数使用的注意事项和细节讨论:

  1. 函数返回值列表可以是多个,并且支持对函数返回值命名

    func SubAdd(a,b int) (sum int,sub int){
    	sum ,sub = a + b, a - b
    	return 
    }
    
  2. 函数中的变量是局部的,函数外不生效。

  3. 基本数据类型[int、float、bool等]和数组默认都是值传递。在函数内修改,不会影响到原来的值。

  4. Go函数不支持函数重载。

  5. Go中函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量,之后可以通过该变量对函数进行调用。

  6. 函数既然是一种数据类型,则可以作为形参,并进行调用。

  7. 支持可变参数

    func sum(args... int) sum int{//支持0到多个参数
        
    }
    func sum(n1 int,args... int) sum int{//支持1到多个参数
       
    }
    

    如果一个函数的形参列表中有可变参数,则可变参数需要放在形参列表最后

    //add.go
    func Sum(n1 int,args... int) int{
    	sum := n1
    	for i := 0;i < len(args);i++{
    		sum += args[i]
    	}
    	return sum
    }
    //main.go
    fmt.Println(oper.Sum(0,1,2,3,4,5))
    

init函数

每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被Go运行框架调用,也就是说init会在main函数前被调用。

func init(){
	fmt.Println("now is init...")
}
func main()  {
	fmt.Println("now is main...")
}

now is init…
now is main…

init函数使用细节

  1. 如果一个文件同时包含全局变量定义、init函数和main函数,这执行流程为:全局变量定义->init函数->main函数。

    进一步思考:如果main.go和utils.go都含有变量定义、init函数和main函数,那么执行流程是怎么样的呢?

    在这里插入图片描述

  2. init函数的最主要作用,就是完成一些初始化的工作。

    比如想要在main函数里使用全局变量,则可以先通过init进行初始化

匿名函数

匿名函数即没有名字的函数,如果我们某个函数只是希望使用一次,可以考虑使用匿名函数。当然,匿名函数也可以实现多次调用。

  • 方式1:定义匿名函数时直接调用,只能调用一次。

    func main()  {
    	res := func (n1, n2 int) int{
    		return n1 + n2
    	}(10,3)
    	fmt.Println(res)
    }
    
  • 方式2:将匿名函数赋值给一个变量,再通过该变量来调用匿名函数

    func main(){
    	a := func (n1 int, n2 int) int{
    		return n1 + n2
    	}
    	res := a(12,1)
    	fmt.Println(res)
    	res = a(12,143)
    	fmt.Println(res)
    }
    
  • 方式3:全局匿名函数,如果将匿名函数赋值为一个全局变量,那么这个就时全局匿名函数,可以在程序有效

    var (
    	a = func (n1 int, n2 int) int{
    		return n1 + n2
    	}
    )
    func main(){
    	res := a(12,1)
    	fmt.Println(res)
    	res = a(12,143)
    	fmt.Println(res)
    }
    

闭包

闭包就是一个函数和与其相关的引用环境组合的一个整体

//累加器闭包 返回一个函数 func(int)int
func Addupper() func(int)int{
	//以下内容相当于一个整体(封闭)
	var n int = 10
	return func (x int) int{
		n += x
		return n
	}
}

func main(){
	//定义一个闭包 里面的内容是一个整体
	f := Addupper()
	fmt.Println(f(1))//10+1 = 11
	fmt.Println(f(2))//11+2 = 13
	fmt.Println(f(3))//13+3 = 16
	//重新得到一个闭包
	f = Addupper()
	fmt.Println(f(2))//10+2 = 12
	fmt.Println(f(4))//12+4 = 16
	fmt.Println(f(6))//16+6 = 22
}

返回的是一个匿名函数,但是这个匿名函数引用到函数外的n,因此这个匿名函数就和n形成了一个整体,构成闭包

可以这样理解,闭包是一个类,函数是操作,n是字段。我们要搞清楚闭包的关键,就是要分析出返回的函数它使用到哪些变量,因为函数和它引用到的变量共同构成闭包

再举一个例子:

编写一个函数Makesuffix(suffix string) func(string) string,可以接收一个文件后缀名,并返回一个闭包。调用闭包,可以传入一个文件名,如果文件名没有指定的后缀,则返回文件名+后缀。否则如果有后缀名,则返回源文件。

func Makesuffix(suffix string) func(string) string{
	return func (name string)string{
		if !strings.HasSuffix(name,suffix){
			return name+suffix
		}
		return name
	}
}

func main(){
	//定义一个闭包 里面的内容是一个整体
	f := Makesuffix(".jpg")
	fmt.Println(f("asdjh"))
	fmt.Println(f("ernsasdh.jpg"))
}

defer延时机制

当我们需要创建资源,为了在函数执行完毕后,及时的释放资源,我们有了defer

defer使用注意事项:

  1. 当Go执行到一个defer时,不会立即执行defer后的语句,而是将defer后的语句压入到一个栈中,然后继续执行函数下一个语句。
  2. 当函数执行完毕后,再从defer栈中,依次从栈顶取出语句执行[先入后出哦]
  3. 在defer将语句放入到栈中,也会将相关的值拷贝同时入栈

比如说,当我们创建了资源(比如打开了文件、获取了数据库连接、锁资源等)可以执行defer file.Close() defer connect.Close(),在defer后,可以继续使用创建资源,当函数执行完毕后,系统会依次从defer栈中,取出语句,关闭资源。

func main(){
	n := 10
	defer fmt.Println(n) //创建n的值拷贝,并将语句压入defer栈
	n = 11
	defer fmt.Println(n)
	n = 12
	defer fmt.Println(n)
	n = 13
	fmt.Println(n)
}

13
12
11
10

函数参数传递方式

值类型参数默认就是值传递,引用类型参数默认就是引用传递。

本质上值传递和引用传递,实际上传递的都是变量的副本,只不过值传递是拷贝了值,而引用传递拷贝了地址。

值类型:基本数据类型、数组、结构体

引用类型:指针、切片、map、管道、接口

变量作用域

  1. 函数内部声明/定义的变量都是局部变量,作用域仅限于函数内部
  2. 函数外部声明/定于的变量叫全局变量,作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效
  3. 如果变量在一个代码块中,则作用域就在这个代码块

版权声明:本文为qq_42642142原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>