![Go并发编程实战](https://wfqqreader-1252317822.image.myqcloud.com/cover/57/44510057/b_44510057.jpg)
3.1 函数的结构
在编程课程中,有一门课程叫“数据结构和算法”,其中会专门对常见的数据结构进行介绍,比如链表、队列等。有一种观点认为数据结构比算法重要,因为数据结构往往决定了它的功能。换句话说,结构决定了功能。
在Go语言中,为什么函数具备可重复调用的特性,且可根据传入的参数经过一定的逻辑处理后返回结果呢?本质上这是由函数的基本结构决定的。
3.1.1 Go语言函数的基本结构
Go语言的函数基本定义语法:
func 函数名( [参数列表] ) [函数返回值类型] { 函数体 }
Go语言的函数由如下几个部分所构成:
· 关键字func
在Go语言中,任何一个函数的定义都必须用关键字func打头进行声明,func是函数英文名function的简写。关键字func和函数名之间必须用空格隔开。
· 函数名
函数名不能用数字或者$、#等特殊字符打头,函数名首字母的大小写决定了该函数的包外可见性。首字母大写的函数在包外可见,而首字母小写的函数在包外不可见。Go语言中建议用驼峰法命名法对函数进行命名,比如用getName()而不是用get_name()。函数名可以不指定(为空),这种函数被称为匿名函数。函数名作为函数的标识符,可以用来对函数进行调用。
· 参数列表
函数可以看作是具备输入和输出接口的黑盒子。函数的参数列表就是函数的输入。Go语言是强类型语言,每个函数的参数列表必须要给定形参的名称以及形参的数据类型。函数定义中给出的形参列表信息就限定了函数调用的时候可以传入的实参个数、顺序以及类型,不匹配的话会出现错误。参数列表可以为空,可以是一个或者多个值。不同的参数之间用英文逗号隔开。形参只是一个占位符,当调用函数时,会将实际的值(称为实参)传递给函数的形参。
· 返回值类型
函数的作用就是对输入进行逻辑处理,并返回结果,从而实现代码的复用。Go语言的函数在定义的时候如果有返回值,就需要指定返回值的数据类型;如果不指定,则表示函数不返回任何值。Go语言中的函数返回值可以是函数。另外,如果函数返回声明中不仅指定了返回值的数据类型,还给出了返回值的别名,如func sum(a, b int) (c int),那么变量c在函数内部可以直接使用,无须再次声明,并且函数最后可以用return语句结束,函数会自动返回c的值,即相当于执行了return c语句。
· 函数体
函数体是函数定义中在符号{ }之间的代码块。在Go语言中,函数的第一个左括号{必须和函数名在一行,不能另起一行。函数体中可以用return关键字把函数的返回值返回给函数的调用者。函数体中既可以调用其他函数,也可以重新定义函数。
下面是Go语言函数使用的示例程序3-1。
示例程序3-1 求和函数:chapter03\code01\func01.go
![](https://epubservercos.yuewen.com/01CADA/23721679501059506/epubprivate/OEBPS/Images/Figure-P68_50178.jpg?sign=1734460384-cg9lhKYCk898ikflMuLeHRw9D2MZFt4Y-0-8c24b2b5f5aa713168c1b725dff5a69c)
在示例程序3-1中,第03行首先用func关键字声明了一个名为sum的函数,这个sum函数有两个参数,分别是a和b,它们都是整数类型。sum函数返回值的类型也是整数类型。第04行是sum函数的函数体,用return返回形参a和b之和。第07行用短变量定义的方式定义了一个变量c,其值为调用函数sum的返回值。此时,实参2和3会被传递到函数sum的形参a和b中,因此返回值为2+3=5。由于Go语言是一门编译型的语言,因此函数编写的顺序是无关紧要的,例如示例程序3-1中的函数sum可以放于main函数之前,也可以放于main函数之后,顺序不影响调用结果。
注意
Go语言中不支持函数的重载,另外同一个包中不允许定义同名的函数,即使它们的参数不一样也不允许。内置的init函数除外。
3.1.2 函数中的变量存储(堆栈)
在计算机程序运行时,变量的分配涉及两个地方:一个是栈(Stack),另一个是堆(Heap)。栈是一种内存连续的数据结构,按照后进先出的原则进行数据的存取,先进入的数据被压入栈底,最后进入的数据放在栈顶。当从栈中读取数据的时候,会从栈顶开始弹出数据。栈是只能在一端进行插入和删除数据的线性结构。
栈是计算机内存中的一个区域,主要用于存储由函数创建的局部变量。当函数调用完成后,栈中存储的局部变量的内存会被自动清空,操作系统可有效管理栈内存空间,因此内存不会碎片化。由于是内存连续的结构,因此存取数据也比较快。栈的内存大小限制取决于操作系统本身,且无法动态调整变量的内存大小。栈的内存是非常有限的,在栈上创建太多变量可能会增加栈溢出的风险。
堆是主要用来存储全局变量或者大对象的地方。一般来说,所有全局变量都存储在堆内存空间中,它支持动态内存分配。堆中的变量一般会由垃圾回收机制来定期清理,但是如果语言本身没有自动垃圾回收,就需要程序员自行清理内存,否则比较容易造成内存泄漏。堆中的内存结构往往不是连续的,因此读取数据的速度相对于栈来说慢一些。堆内存管理比栈内存管理更加复杂,执行的时间也比栈更长。但是,堆可以进行全局变量操作,且能使用操作系统可以提供的最大内存来存取变量。
为了优化性能,有时我们需要对代码中的变量进行分析,以判断哪些变量分配在栈上、哪些变量分配在堆上。Go语言给开发人员提供了一套工具,可以比较方便地进行逃逸分析(Escape Analysis)。在函数中给变量分配内存后,其指针有可能被返回或者被全局引用,这样就可能会被其他函数或者线程所引用,这种现象称作指针(或者引用)的逃逸。通过逃逸分析,就可以知道变量是在栈中进行分配还是在堆中进行分配。
变量在内存中存储的位置(堆还是栈)对编写高效程序确实有一定的影响。Go编译器中优化算法尽可能将函数中局部变量分配到栈上。如果Go编译器无法确认在函数返回后没有其他对象引用该变量,或者该变量非常大,或者变量的大小无法确定(比如interface{},这个类型会在后续章节进行介绍),那么编译器会在堆上分配该变量,以避免出现悬空指针错误(Dangling Pointer Error,也称为“野指针”错误)。与指针相关的内容会在第6章详细说明。
另外,Go语言可以用编译器参数来对变量进行逃逸分析。其中,参数-gcflags可以将编译参数传递给Go编译器,逃逸分析主要涉及两个参数:
· -m:打印出逃逸分析的优化策略,可以同时用多个,如-m -m。
· -l:禁用函数的内联(inline)功能,这样能更好地观察逃逸情况,减少干扰。
比如用如下命令即可对文件main.go进行变量逃逸分析:
go build -gcflags "-l -m" .\main.go
因此,我们可以用go build -gcflags "-l -m" .\func01.go命令对示例程序3-1中的程序代码进行逃逸分析,执行完成后会显示如下信息(注意,示例代码行号和实际打印的行号不一致):
# command-line-arguments .\func01.go:11:12: main ... argument does not escape .\func01.go:11:13: c escapes to heap
由逃逸分析可知,func01.go文件中的第11行变量c逃逸到堆(Heap)上。在示例代码中是第08行,即fmt.Printf("sum(2, 3)=%d \n", c)语句中的c逃逸了。这是由于fmt.Println函数的参数类型是interface{},在编译阶段Go编译器无法确定具体的类型,因此会分配到堆上,从而发生了逃逸。其实我们可以用Go的内建函数println来代替fmt.Println,这样的话示例程序3-1中的变量c就不会发生逃逸问题了。
注意
示例程序3-1中的空行已经删除,但是实际源代码中的空行未被删除,因此显示的行号和示例代码中的行号并不一致。另外,Go不同版本的逃逸分析可能会有所不同,这主要是由于Go编译器会不断优化逃逸分析算法。
3.1.3 函数返回函数
在Go语言中,函数除了可以返回基本数据类型的值之外,还可以返回一个函数。这种特性和JavaScript比较类似。下面给出一个Go语言函数返回一个函数的示例程序,如示例程序3-2所示。
示例程序3-2 函数返回函数的示例:chapter03\code02\func02.go
![](https://epubservercos.yuewen.com/01CADA/23721679501059506/epubprivate/OEBPS/Images/Figure-P70_50179.jpg?sign=1734460384-bQfL42Z679jdACOoJl8B0arorE7NpHyv-0-4ae31db828a0829fce4a25c57074591b)
在示例程序3-2中,第03行声明了一个sum函数,它接收2个整数类型的参数。注意,这个函数的函数体用return返回了一个接收两个整数类型参数的函数(这个函数实际是一个匿名函数),return的函数签名必须和sum函数返回值的数据类型一致,即func(int, int) int。
第09行可以用sum(2, 3)(4, 5)对定义的函数sum进行调用,sum(2,3)实际上返回了其内部定义的函数,假设为f,则可以用f(4, 5)进行调用,这两个函数进行合并即为sum(2, 3)(4, 5),返回值为2+3+4+5=14。
同样的,我们用如下命令对文件func02.go进行变量逃逸分析:
go build -gcflags "-l -m" .\func02.go
则会显示如下信息(注意示例代码行号和实际打印的行号不一致):
.\func02.go:6:9: func literal escapes to heap .\func02.go:14:12: main ... argument does not escape .\func02.go:14:13: c escapes to heap
另外,我们也可以执行go tool compile -m .\func02.go命令来显示编译器的优化策略信息,具体内容如下所示。
.\func02.go:6:9: can inline sum.func1 .\func02.go:14:12: inlining call to fmt.Printf .\func02.go:6:9: func literal escapes to heap .\func02.go:14:13: c escapes to heap .\func02.go:14:12: main []interface {} literal does not escape .\func02.go:14:12: io.Writer(os.Stdout) escapes to heap <autogenerated>:1: (*File).close .this does not escape <autogenerated>:1: (*File).isdir .this does not escape
编译器实际上将sum函数进行内联,提高了执行效率。在这个例子中,go tool compile -m可以比go build -gcflags "-l -m"显示更多的信息。在实际项目中,我们可以根据自己的情况单独使用或者组合使用。