函数声明方式:
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的负担,所以传递指针不一定是高效的。
事物都有优有缺,重要的是在合适的地方去合适的运用,才能将其优势发挥出来
好了,上面只是一个小插曲,有兴趣的童鞋也可以继续深入了解。接下来我们继续了解Go语言的函数。
Go函数支持返回多个值,这一点相比于其他语言很有独特性
func SubAdd(a,b int) (int,int){
return a + b, a - b //返回和 差
}
注意:如果返回多个值,在接收时,希望忽略某个返回值,则使用_符号占位忽略
函数使用的注意事项和细节讨论:
- 函数返回值列表可以是多个,并且支持对函数返回值命名
func SubAdd(a,b int) (sum int,sub int){
sum ,sub = a + b, a - b
return
}
- 函数中的变量是局部的,函数外不生效。
- 基本数据类型[int、float、bool等]和数组默认都是值传递。在函数内修改,不会影响到原来的值。
- Go函数不支持函数重载。
- Go中函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量,之后可以通过该变量对函数进行调用。
- 函数既然是一种数据类型,则可以作为形参,并进行调用。
- 支持可变参数
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函数使用细节:
- 如果一个文件同时包含全局变量定义、init函数和main函数,这执行流程为:全局变量定义->init函数->main函数。
进一步思考:如果main.go和utils.go都含有变量定义、init函数和main函数,那么执行流程是怎么样的呢?
- 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使用注意事项:
- 当Go执行到一个defer时,不会立即执行defer后的语句,而是将defer后的语句压入到一个栈中,然后继续执行函数下一个语句。
- 当函数执行完毕后,再从defer栈中,依次从栈顶取出语句执行[先入后出哦]
- 在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、管道、接口
变量作用域:
- 函数内部声明/定义的变量都是局部变量,作用域仅限于函数内部
- 函数外部声明/定于的变量叫全局变量,作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效
- 如果变量在一个代码块中,则作用域就在这个代码块