1.1 函数声明
每个函数声明都包含一个名字,一个形参列表,一个可选的返回列表以及函数体
func fun(x,y float64) float{
return 100.00
}
也可以
func fun(x float64,y float64) float{
return 100.00
}
1.2函数递归
函数可以递归调用,这就意味着函数可以直接或者间接的调用自己
注:许多编程语言都有自己的函数调用栈;大小在64kb到2MB之间。递归的深度受限于固定大小的栈大小。所以进行深度递归时必须注意防止 【栈溢出】。Go语言实现了可变长度的栈,可达到1GB左右的上限,这使得我们可以安全的使用递归而不担心栈溢出问题。
1.3函数多返回值问题
GO语言和C/C++语言不一样的地方就是,GO语言的函数可以有多个返回值,而c/c++只能有一个返回值。
注:裸返回:裸返回是将每个命名返回结果按照顺序返回的快捷方法,所以下边的函数中,每个 return 的语句相当于 :return words,images,err
func Country(url string)(words,images int,err error){
resp, err := http.Get(url)
if(err != nil){
return
}
doc, err := html.Parse(resp,Body)
resp.Body.Close()
if err != nil{
err = fmt.Errorf("Parse html")
return
}
words, images = countryWords(doc)
return
}
1.4 函数错误处理
当一个函数调用返回一个错误时,调用者应当负责检查并且采取适当的处理应对。根据情景,可能有很多的处理方式。
最常见的处理方式:将错误传递下去
resp, err := http.Get(url)
if err != nil{
return nil, err
}
1.5 函数变量
函数在 GO语言中是头等重要的值:就像其他值,函数也有类型,而且它们可以赋值给变量或者传递或者从其他函数中返回。函数变量也可以像其他变量一样调用。
package main
import (
"fmt"
)
func fun() {
fmt.Println("我是函数")
}
func main() {
f := fun
f()
}
输出结果:我是函数
函数类型的零值是 nil(空值) ,调用一个空的函数变量将导致宕机。
package main
import (
//"fmt"
)
var f func(int) int
func main() {
f(3)
}
运行错误:panic:运行时错误:无效内存地址或nil指针引用。
注:函数变量可以和空值相比较,但是它们本身不能比较。所以不能作为 键值 出现在 map 中。
1.6 匿名函数(函数字面量)
命名函数只能在包级别的作用域进行声明,但是我们能够使用【函数字面量】在任何表达式内指定函数变量。函数字面量就像函数声明,但是在 func 关键字后面没有函数名称。它是一个表达式,它的值称作匿名函数。
package main
import (
"fmt"
)
func ret() func() int {
var x int
return func() int {
x++
return x * x
}
}
func main() {
f := ret()
fmt.Println(f())
fmt.Println(f())
fmt.Println(f())
fmt.Println(f())
}
程序输出:1
4
9
16
注:为什么这里的输出是1,4,9,16。因为 ret()函数返回的是一个匿名函数,但是匿名函数使用的 x变量是 ret()函数内部的局部变量。因此匿名函数每调用一次 x++ ,ret()函数内部的 x 就会+1,所以输出的就是1,4,9,16。
1.7 变长函数
变长函数被调用的时候可以有可变的参数个数。最熟悉的函数就是 fmt.Printf() 和它的变种。Printf() 需要在开头提供一个固定的参数,后续便可以接收任意数目的参数。
在参数列表最后的类型名称之前使用 省略号 "..." 表示声明一个变长函数,调用这个函数的时候可以传递该类型的任意数目的函数。
package main
import (
"fmt"
)
func sum(num int, val ...int) int {
total := 0
for _, v := range val {
total += v
}
return total
}
func main() {
fmt.Println(sum(1))
fmt.Println(sum(1))
fmt.Println(sum(2, 3))
fmt.Println(sum(1, 2, 3, 4))
}
程序输出:0
0
3
9
package main
import (
"fmt"
)
func sum(val ...int) int {
total := 0
for _, v := range val {
total += v
}
return total
}
func main() {
fmt.Println(sum(1))
fmt.Println(sum(1))
fmt.Println(sum(2, 3))
fmt.Println(sum(1, 2, 3, 4))
}
程序输出:1
1
5
10
1.8 延迟函数调用(defer)
Go语言中延迟函数defer充当着 cry...catch 的重任,使用起来也非常简便,然而在实际应用中,很多人并没有真正搞明白defer、return和返回值之间的执行顺序,从而掉进坑中,今天我们就来揭开它的神秘面纱!
先来运行下面两段代码:
A. 无名返回值的情况
package main
import (
"fmt"
)
func main() {
fmt.Println("return:", a()) // 打印结果为 return: 0
}
func a() int {
var i int
defer func() {
i++
fmt.Println("defer2:", i) // 打印结果为 defer: 2
}()
defer func() {
i++
fmt.Println("defer1:", i) // 打印结果为 defer: 1
}()
return i
}
程序输出:defer1: 1
defer2: 2
return: 0
B. 有名返回值的情况
package main
import (
"fmt"
)
func main() {
fmt.Println("return:", b()) // 打印结果为 return: 2
}
func b() (i int) {
defer func() {
i++
fmt.Println("defer2:", i) // 打印结果为 defer: 2
}()
defer func() {
i++
fmt.Println("defer1:", i) // 打印结果为 defer: 1
}()
return i // 或者直接 return 效果相同
}
程序输出结果:defer1: 1
defer2: 2
return: 2
先来假设出结论,帮助大家理解原因:
1 多个defer的执行顺序为“后进先出”;
2 defer、return、返回值三者的执行逻辑应该是:return最先执行,return负责将结果写入返回值中;接着defer开始执行一些收尾工作;最后函数携带当前返回值退出。
如何解释两种结果的不同:
上面两段代码的返回结果之所以不同,其实从上面第2条结论很好理解。
a()int 函数的返回值没有被提前声名,其值来自于其他变量的赋值,而defer中修改的也是其他变量,而非返回值本身,因此函数退出时返回值并没有被改变。
b()(i int) 函数的返回值被提前声名,也就意味着defer中是可以调用到真实返回值的,因此defer在return赋值返回值 i 之后,再一次地修改了 i 的值,最终函数退出后的返回值才会是defer修改过的值。
C. 下面我们再来看第三个例子,验证上面的结论:
package main
import (
"fmt"
)
func main() {
fmt.Println("c return:", *(c())) // 打印结果为 c return: 2
}
func c() *int {
var i int
defer func() {
i++
fmt.Println("c defer2:", i) // 打印结果为 c defer: 2
}()
defer func() {
i++
fmt.Println("c defer1:", i) // 打印结果为 c defer: 1
}()
return &i
}
程序输出:defer1: 1
c defer2: 2
c return: 2
虽然 c()*int 的返回值没有被提前声明,但是由于 c()*int 的返回值是指针变量,那么在return将变量 i 的地址赋给返回值后,defer再次修改了 i 在内存中的实际值,因此函数退出时返回值虽然依旧是原来的指针地址,但是其指向的内存实际值已经被成功修改了。
即,我们假设的结论是正确的!
1.9 不允许函数内嵌定义
package main
import "fmt"
func main() {
func swap(x, y string) (string, string) {
return y, x
}
a, b := swap("hello", "world")
fmt.Println(a, b)
}
1.9 宕机
Go语言的类型系统会捕捉许多编译时错误,但是一些其他错误(数组访问越界等等)都需要在运行时检查。当Go语言运行时检测到这些错误,就会发生宕机。