一. 函数定义
1.1 特点
- 无需声明原型
- 支持不定变参
- 支持多返回值
- 支持命名返回参数
- 支持匿名函数和闭包
- 函数也是一种类型,一种函数可以赋值给变量
- 不支持嵌套,一个包不能有两个名字一样的函数
- 不支持重载
- 不支持默认参数
1.2 函数声明
函数声明包含一个函数名,参数列表,返回值列表和函数体。如果没有返回值,则返回值列表可以省略。函数从第一条语句开始执行,直到执行return语句或者函数体的最后一条语句。
函数可以没有参数或者接受多个参数。注意:类型在变量名后。
当两个或者两个以上的函数命名参数是同一类型,则除最后一个类型之外,其它可以省略。
函数可以返回任意数量的返回值。
使用关键字func定义函数,左括号不能另起一行。
func test(s string, x, y int) (int, string) {
n := x + y
return n, fmt.Sprintf(s, n)
}
函数是第一类对象,可作为参数传递。建议将复杂签名定义为函数类型,以便于阅读。
有返回值的函数,必须有明确的终止语句,否则会引发编译错误。
你可能会偶尔遇到没有函数体的函数声明,这表示该函数不是以Go实现的。这样的声明定义了函数标识符。
package math
func Sin(x float64) float //用汇编语言实现
1.3 参数
函数定义时指出,函数定义时有参数,该变量可以称为函数形参。形参就像定义在函数体内的局部变量。
但调用函数时,传递过来的变量就是函数的实参。函数可以通过两种方式来传递参数:
值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响实际参数。
引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数进行修改,将影响实际参数。
在默认情况下,Go语言是值传递,即在调用过程中,不会影响实际参数。
注意:
- 无论是值传递还是引用传递,传递给函数的都是变量的副本。不过,值传递是值的拷贝,引用传递是指针的拷贝(解引用后就是实际的值)。
- 一般来说,地址拷贝更加高效(指针在32位下占4字节,在64位下占8字节)。而值拷贝取决于拷贝对象的大小,对象越大性能越低。
- map,slice,chan,指针,interface默认以引用传递
不定参数传递:
不定参数传值,就是函数的参数不是固定的,后面的类型是固定的。(可变参数)
Golang可变参数的本质上就是一个切片slice。只能有一个,且必须是最后一个。
在参数赋值时,可以不用一个个的赋值,可以直接传递一个切片。注意传递切片时在可变参数后需要加"..."。
func myFunc1(args ...int) {//0个或多个参数
}
func myFunc2(a int, args ...int){//1个或多个参数
}
func myFunc3(a int, b int, args ...int){ //2个或多个参数
}
注意:其中args是一个slice,我们可以通过arg[index]依次访问所有的参数,通过len(args)来判断参数的个数。
任意类型的不定参数:
任意类型的不定参数,就是函数参数个数和每个参数的类型都是不固定的。
用户interface{}传递任意类型数据是Go语言的常用惯例,而且interface{}是类型安全的。
func myFunc(args ...interface{}) {
}
1.4 返回值
特点
- "_"标识符,用来忽略函数的返回值
- Go的返回值可以被命名,并且就像函数开头声明的变量那样使用
- 返回值的名称应当具有一定的意义,可以作为文档使用
- 没有参数的return语句返回各个返回变量的当前值(用在返回值有名称的情况)。这种用法被称作"裸"返回
- 直接返回语句仅应当用在短函数中。在长函数中它们会影响代码的可读性。
- 命名返回值参数可看做与形参类似的局部变量,最后由return隐式返回
- Golang返回值不能用容器对象接收多返回值。只能用多个变量,或者"_"忽略返回值。 即有几个返回值,就需要几个变量接收
- 多返回值可以直接作为其它函数调用的实参
- 命名返回参数可被同名局部变量遮掩,此时需要显示返回
- 命名返回参数允许defer延迟调用通过闭包读取和修改
- 显示return返回前,会先修改命名返回参数
1.5 匿名函数
匿名函数是指不需要定义函数名的一种函数实现方式。
在Go语言中,函数可以像普通变量一样被传递或使用,Go语言支持随时在代码里定义匿名函数。
匿名函数由一个不带函数名的函数声明和函数体组成。匿名函数的优点在于可以直接使用函数内的变量,不必声明。
对比C/C++,函数虽然也可以作为变量或参数,但是函数需要有名字。
Golang匿名函数可以赋值给变量,作为结构体字段,或者在channel里传送。
1.6 闭包
闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。
官方的解释是:所谓闭包,指的是一个拥有许多变量和绑定了这些变量的环境表达式,通常是一个函数,因而这些变量也是表达式的一部分。
维基百科讲,闭包是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,由另外一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有很多实例,不同的引用环境和相同的函数组合可以参数不同的实例。
看着上面的描述,会发现闭包和匿名函数很像。下面来一个例子。
- 定义
函数b嵌套在函数a内部,函数a返回函数b,这样执行完var c=a()后,实际c指向了函数b(),再执行c()会执行函数b打印变量i,第一次为1,第二次为2,第三次为3,以此类推。其实,这段代码就创建了一个闭包。因为函数a()外的变量c引用了函数a()内的函数b()。也就是说:当函数a()的内部函数b()被函数a()外的一个变量引用的时候,就创建了一个闭包。
在上面例子中,由于闭包的存在使得函数a返回后,a中的变量i始终存在,这样每次执行c(),i都是自加1后的值。闭包使得Go的垃圾回收机制GC不会回收a()占用的资源。
- 作用
在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。但是变量的作用域仅限于包含它们的函数,所以在其它程序代码中无法进行访问。变量的生存周期变长,在一次函数调用期间所创建生成的值在下次函数调用时仍然存在(所以上面打印的值为1,2,3)。正因为上面的特点,闭包可以用来完成信息隐藏,并进而应用于需要状态表达的某些编程范型中。
对于上面的例子,如果函数a返回的不是函数b,情况就完全不同了。因为函数a执行完后,函数b没有被返回给函数a的外界,只是被函数a引用了,函数a被函数b引用。所以函数a和函数b互相引用不被外界引用。函数a和b会被垃圾回收机制GC回收。
- 引用环境
上面的例子,c()跟c2()引用的是不同的环境,在调用i++时修改的不是同一个i,因此开始时输出的都是1。函数a()每进入一次,就形成了新的环境,对应的闭包中,函数都是同一个函数,环境确是引用不同的环境。这和c()和c1()的调用顺序无关。
闭包进入一次,引用环境不同。
- 闭包复制的是原对象指针,这就很容易解释延时引用现象
匿名函数变量i和test()函数变量i是同一个。
在汇编层,test实际返回的是FuncVal对象,其中包含匿名函数地址,闭包对象指针。当调用匿名函数时,只需以某个寄存器传递该对象即可。
FuncVal { func_address, closure_var_pointer ... }
- 外部引用函数参数的局部变量
- 返回两个闭包
1.7 递归
递归,就是在运行的过程中调用自己。一个函数调用自己,就叫做递归函数。
构成递归需具备的条件:
- 子问题须与原始问题为同样的事情,且更为简单
- 不能无限制地调用本身,必须有个出口,化简为非递归状况处理
- 数字阶乘
一个正整数的阶乘是所有小于等于该数地正整数地积,并且0的阶乘为1。
package main
import "fmt"
func factorial(x int) int {
if x <= 1 {
return 1
}
return x * factorial(x-1) //自己调用自己
}
func main() {
res := factorial(5)
fmt.Println(res)
}
- 斐波那契数列
1.8 延时调用(defer)
defer特性:
- 关键字defer用于注册延时调用
- 这些调用直到return前才被执行。因此,可以用来做资源清理
- 多个defer语句,按先进后出的方式执行。因为后面的语句会依赖前面的资源,因此如果前面的资源释放了,后面的语句就没法执行了。
- defer语句中的变量,在defer声明前就决定了。
defer用途:
- 关闭文件句柄
- 锁资源释放
- 数据库连接释放
defer触发时机:
- 包裹着defer语句的函数返回时
- 包裹着defer语句的函数执行完时
- 当前goroutine发送Panic时
实例:
defer先进后出:
- 当go遇到一个defer语句时,不会立即执行,而是将defer后面的语句压入到一个栈中,然后继续执行函数下的语句。
- 当函数或方法执行完毕,再从栈中依次从栈顶取出执行(先入后出)。
i是从0到4,但是由于defer先进后出,所以是4到0。
defer遇上闭包:
- 在Go语言中,对外部作用域中变量访问的方式是"引用",捕获的是变量的地址。当外部变量发生改变时,闭包中的变量也会发生改变。
- 多个defer语句注册,按先入后出次序执行。哪怕函数或某个延时调用发生错误,这些调用依旧会被执行。
- 延迟调用参数在注册时求值或复制,可以用指针或闭包"延时读取"
- 滥用defer可能导致性能问题,尤其是在一个大循环里面
defer陷阱:
- defer与闭包:
由于闭包使用的外部变量保存的是地址。修改外部变量可以得到最新的值。
- defer与return
- defer nil函数
值得注意的是run在声明是不会报错,而是在使用是报错。
- 在错误位置使用defer
当http.Get失败时会抛异常。
修改:
获取返回错误:
- 释放相同资源
解决方案:不建立闭包,将变量传进来,这样是值拷贝。
1.9 异常处理
Golang没有结构化异常,使用panic抛出错误,recover捕获错误。
使用场景:Go中可以抛出一个panic异常,然后在defer中通过recover捕获这个异常,然后正常处理。
panic:
- 内置函数
- 假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
- 返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
- 直到goroutine整个退出,并报告错误
recover:
- 内置函数
- 用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为。
- 一般的调用建议:
- 在defer函数中,通过recever来终止一个goroutine的panicking过程,从而恢复正常代码的执行。
- 可以通过panic传递错误
注意:
- 利用recover处理panic指令,defer必须放在panic之前定义,另外recover只有在defer调用函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。
- recover处理异常后,逻辑并不会恢复到panic那个点去,函数跑到defer之后的那个点。
- 多个defer会形成defer栈,后定义的defer语句会被最先调用
由于panic和recover参数类型为interface{},因此可以抛出任何类型的对象
func panic(v interface{}) func recover()interface{}
- 向已关闭的通道发送数据会引发panic
- 延时调用中引发错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获
- 当异常panic被回收recover后,就没了,后面的recover回收不到该异常
- 捕获函数recover只有在延时调用内直接调用才会终止错误,否则捕获不到异常(返回nil),任何未捕获的的错误都会沿调用堆栈向外传递。
也可以不使用匿名函数,而是使用函数,也可以回收异常。
- 如果需要保护代码段,可将代码重构成匿名函数,如此可确保后续代码被执行
- 除了用panic来引发中断性错误外,还可返回error类型错误对象来表示函数调用状态
type error interface{
Error() string
}
标志库errors.New和fmt.Errorf函数用于创建实现error接口的错误对象。通过判断错误对象实例来确定具体错误类型。
panic用来返回error接口对象:
- Go实现类似try catch的异常处理
如何区分使用panic和error两种方式?
导致关键流程出现不可修复性错误的使用panic,其它使用error(返回error)。