一、前言
函数( Function)是能完成程序预定义功能的代码逻辑单位,一个Go程序至少由一个或多个函数组成。对于可执行的Go程序必须有main()函数,而且只能有一个。
一般来说,Go程序在编译时,函数在程序中所处的位置并不影响编译结果。但为了代码的可读性,一.般是从main()函数开始,按函数间的逻辑结构编写代码。编写函数的目的主要是为了将冗长的代码划分成较小的功能模块,另外-一个目的是为了能反复调用程序的某些功能,提高程序代码的复用性。
Go函数不支持嵌套,不支持重载,也不支持默认参数。但Go函数支持变参、多返回值,还可以命名返回值参数。另外,Go语言还支持匿名函数和闭包。在C语言中,如果要在函数定义之前使用它,首先要声明一个函数原型,而在Go语言中无须这么做,Go函数不需要声明函数原型就可以直接使用。
二、函数声明
在Go语言中,函数在声明之后就可以使用,无须声明函数原型。Go函数一般由关键字func、函数名、参数列表、返回值、函数体和返回语句组成。当然,有些函数没有参数或返回值。
函数声明的基本格式:
func functionName (参数列表) 返回值 {
functionBody
...
return语句
}
在声明函数时要注意:
- (1)函数名的命名规则和变量名相同,遵循标识符命名规则。
- (2)函数可以有参数或者没有参数,主调用函数通常使用参数向被调用函数传递数据。
- (3)函数体内的所有语句使用一对“{}”括起来,左大括号“{”必须和“func”放在同一行,右大括号“}”必须单独占一行。
- (4)函数会在执行完最后一条语句,或执行return语句后结束,Go语言函数支持多返回值。另外,在终止一个无限循环或goroutine时通常也使用return语句。
三、函数调用
1、调用标准函数
Go提供了大量的包和实用函数供用户使用,这些函数被称为标准丽数。常用的标准包有fmt、math、os、time、bytes等,标准包的信息可以在Go安装目录的pkg下查看,或使用godoc查看。在调用标准函数时首先要导人该函数所在的包,比如调用Println()函数要导入fmt包。
2、调用自定义函数
通常一个可执行的Go程序,首先要构建一个main包,在main包中必须声明一个main函数,然后再声明一些其他自定义函数让main函数调用,在这种情况下,除非是必需的标准包,否则不用导人任何其他包,直接调用自定义函数就行了。
3、调用外部包中函数
有时在一个Go项目中,除了main包外还可能创建了其他包,如果被调丽数是由其他包提供的,此时需导人这个包才能调用相关函数。
4、调用内置函数
除了前面几种函数的调用,Go语言还提供了一些非常有用的内置函数( Built-inFunction),这些函数在调用时无须导入任何包就可以直接使用。内置函数一般都能对不同的数据类型进行操作,比如len()函数能获取数组、字符串、切片的长度。有些内置函数直接作用于系统底层,比如像panic()函数,通常用于系统错误处理。Go 语言的内置函数虽然不多,但都非常有用,。
四、参数传递
在Go语言中,函数参数可以是值类型或引用类型,值类型作为函数参数进行传递时,是一个参数值的拷贝,引用类型作为函数参数传递时,是一个地址拷贝。
说明:
- (1)在声明函数时指定的形参,在未进行函数调用时并不占用存储单元。在进行函数调用时,形参才被分配存储单元,在调用结束后存储单元被释放。
- (2)实参可以是常量、变量或者表达式,不管是哪一种形式都必须有确定的值,在调用时将实参的值赋给形参。
- (3)在声明函数时,必须指定形参的类型,而且实参与形参的类型必须一致。
1、常规传递
当使用普通变量作为函数参数时,在传递参数时只是对变量值的拷贝,即将实参的值复制给变参。当函数对变参进行处理时,并不影响原来实参的值。
2、指针传递
函数的参数不仅可以使用普通变量,还可以使用指针变量。当使用指针变量作为函数的参数时,在进行参数传递时将是一个地址拷贝,即将实参的内存地址复制给变参,这时对变参的修改同时也将会影响到实参的值。
3、数组元素作为函数参数
前面已经介绍了可以使用普通变量作为函数的参数,显然,数组元素也可以作为函数的参数,因为数组就是同一种变量的集合。当使用数组元素作为函数参数时,其使用方法和普通变量相同,即是一个“值拷贝”。
4、数组名作为函数参数
除了数组元素可以作为函数参数外,数组名也可以作为函数的参数。和其他语言不同的是,Go语言在将数组名作为函数参数时,参数传递即是对数组的复制。在形参中对数组元素的修改,都不会影响实参数组元素原来的值。
5、Slice作为函数参数
当希望通过形参对底层数组进行修改时,可以使用Slice作为函数参数。在使用Slice作为函数参数时,进行参数传递将是一个地址拷贝,即将底层数组的内存地址复制给参数Slice。这时,对Slice元素的操作即是对底层数组元素的操作。
6、函数作为参数
在Go语言中,函数也作为一种数据类型,所以函数也可以作为函数的参数来使用。
例如:
该例中函数sum作为函数f6()的形参,而变量f是一个函数类型,作为f6()调用时的实参,程序运行结果为:7。
func main( ) {
var a,b int=3,4
f:= sum
f6(a,b,f)
}
func f6(a, b int, sum func(int, int) int) {
fmt. Println(sum(a, b))
}
func sum(a,b int) int {
return a + b
}
五、返回值
函数被调用时,允许有返回值,甚至是多个返回值。Go语言还允许定义返回值变量,这样在使用return语句进行返回时语句更加简洁。
多返回值
Go语言支持函数可以有多个返回值,这和C语言有明显不同,多返回值使得Go程序设计起来更加灵活,而且功能强大。如果函数定义了多个返回值,除了说明各个返回值的类型以外,还需使用一对括号将它们括起来。
返回值的忽略
在处理多返回值时,对于不想要的值,可以使用空白标识符(Blankidentifier)“_”进行忽略。
命名返回值参数
Go语言还支持命名返回值参数,如果使用命名返回值参数,则return语句可以为空。否则,return语句必须按照顺序返回多个结果。
例如:
func main() {
sum,sub := f3(3,6)
fmt.Println(sum, sub)
}
func f3(a,b int) (sum, sub int) {
sum = a+b
sub = a-b
return
}
该例中函数f3()有两个返回值参数,一个是sum,一个是sub。命名了返回值参数,函数f3()使用一条空return语句,就可以向主调函数返回sum、sub这两个结果。如果未命名返回值参数,函数f3()的返回语句写法是: return a+b,a-b。
六、变参函数
Go语言支持不定长变参,但是要注意不定长变参只能作为函数的最后一个参数,不能放在其他参数的前面。
变参函数的声明格式如下:
func functionName (variableArgumentName ...dataType) 返回值 {
functionBody
.....
}
说明:
- (1)变参类型的定义格式是“...类型”,而且变参必须是函数的最后一个参数。如果函数还有其他参数,则必须放在变参的前面定义,例如func fl(a int,s string, args ...int){}。
- (2)不定长变参其实质就是一个切片,可以使用range进行遍历。
任意类型的变参
前面的例子中变参中的元素都必须是同一种类型,比如同为整型、浮点型、字符串等,但在实际应用中,用户希望向变参函数传递不同类型的参数,比如向fmt.Printf()函数那样。Go语言规定,如果希望传递任意类型的变参,变参类型应指定为空接口interface{}。
例如:
func f1(args ... interface{}) {
.....
}
在Go语言中,空接口interface{}可以指向任何数据对象,所以可以使用interface{}定义任意类型变参。同时,interface{}也是类型安全的。
七、匿名函数
匿名函数(Anonymous function)是指不需要定义函数名的一种函数实现方式。匿名函数最早出现在LISP语言中,目前PHPJavaScript都支持这种函数形式。
在Go语言中,函数可以像变量一-样被传递或使用,这与C语言的函数回调比较类似。不同的是,Go语言支持随时在代码里定义匿名函数。匿名函数由一个不带函数名的函数声明和函数体组成,如下所示:
func (参数列表) 返回值 {
functionBody
...
return语句
}
例如:
func(a,b int) int {
return a + b
}
上例声明了一个匿名函数,该匿名函数有两个整型参数,返回值也为整型。匿名函数在
调用时,可以直接赋值给一个变量,或者直接执行。示例如下:
//匿名函数的声明与调用
package main
import(
"fmt"
)
func main() {
//声明并直接将匿名函数赋值给变量f
f:= func(a,b int) int {
return a+ b
}
//对函数类型变量f进行调用
sum : = f(2,3)
fmt.Println(sum)
//声明并直接执行匿名函数
sum = func(a,b int) int {
return a + b
}(5, 7)
fmt.Println(sum)
}
八、闭包
闭包(Closure),就是内部函数通过某种方式使其可见范围超出了其定义的范围,这就产生了一个在其定义范围内的闭包。在Go语言中,闭包可以作为函数对象或者匿名函数,也就是说Go语言中的闭包同样也会引用到函数外的变量。闭包的实现确保只要闭包还被使用,那么被闭包引用的变量会一直存在。
例如:
func main() {
f:= closures(10)
fmt.Println(f(1))
fmt.Println(f(2))
}
func closures(x int) func(int) int {
return func(y int) int {
return x+ y
}
}
这段代码有三个特点:
- (1)匿名函数嵌套在函数closures内部。
- (2)函数closures返回匿名函数。
- (3)匿名函数引用了自身外部的变量x。
函数closures内的匿名函数就是上面所说的内部函数,这样在执行完f:=closures(10)后,变量f实际上是指向了匿名函数,所以执行f(1)后输出11,执行f(2)后输出12。这段代码其实就创建了一个闭包,因为函数closures的变量f引用了函数closures内的匿名函数。也就是说:当函数closures的内部函数(匿名函数)被函数closures外的一个变量引用的时候,就创建了一个闭包。
通过对函数闭包的原理分析和实例测试,函数闭包主要有以下几个作用:
- (1)函数闭包可以保护函数内的变量安全。比如函数closures的参数x只有内部函数才能访问,而无法通过其他途径访问到。
- (2)函数闭包可以在内存中维持一个变量。比如上例中函数closures的参数x在定义变量f时被初始化为10,然后就一直保持为这个值,后面无论调用多少次f,x在内存中一直存在。
九、函数的递归调用
递归调用必须满足以下两个条件:
- (1)递归函数在每一次调用自己时,必须是(在某种意义上)更接近于最终结果。
- (2)必须有一个终止递归调用的条件控制。
所以,函数的递归调用通常使用if语句来控制,只有当某一条件成立时才继续执行递归调用,否则就不再继续。
十、defer语句
在Go语言中,可以使用关键字defer向函数注册退出调用,即当主调函数退出时,defer后的函数才会被调用。defer语句的作用是不管程序是否出现异常,均在函数退出时自动执行相关代码。
例如:
func main(){
defer fmt.Println( "The first.")
fmt.Println("The second. ")
}
上例首先输出“The second.”,然后才输出“The first.'
1、defer语句实现函数逆序调用
如果程序中有多个defer语句,则按照“先进后出(FILO)”的次序执行,即最后一个defer语句将最先被执行。
2、defer语句支持匿名函数调用
在Go语言中,defer语句还支持匿名函数调用,如果函数有返回值,被延迟执行的匿名函数还会读取函数的返回值,并对返回值赋值。
3、defer语句用于清理工作
在Go程序中,当程序返回或发生异常时,defer语句通常用来做一些函数调用后的清理工作,释放资源变量。
十一、异常恢复机制
Go没有Java中那种try-catch-finally结构化异常处理机制,而是使用panic()函数代替throw/raise引发错误,然后在defer语句中调用recover()函数捕获错误,这就是Go语言的异常恢复机制——panic -and-recover机制。
panic是一个内置函数,可以中断原有的控制流程,进入一个异常流程中。当函数调用panic时,函数的执行将被中断,并且函数中的延迟函数会正常执行,然后函数返回到调用它的地方。在调用的地方,函数的行为就像调用了panic。 这一过程继续向上,直到程序崩溃时的所有goroutine返回。异常可以直接调用panic产生,也可以由运行时错误产生,例如访问越界的数组。
Recover也是一个内置的函数,它可以让进入异常流程中的goroutine 恢复过来。Recover仅在延迟函数中有效。在正常的执行过程中,调用recover会返回nil并且没有其他任何效果。如果当前的goroutine陷人异常,调用recover可以捕获到panic的输人值,并且恢复正常的执行。