Go不是一门纯函数式的编程语言,但函数在Go中的第一公民,表现在:
函数式一种类型,函数类型变量可以像其他类型变量一样使用,可以作为其他函数的参数或返回值,也可以直接调用执行。
函数支持多值返回。
支持闭包。
函数支持可变参数 。
Go是通过编译成本地代码且基于 堆栈 式执行的,Go的错误处理和函数也有千丝万缕的联系。
2.1 基本概念
2.1.1 函数定义
函数首字母的大小写决定该函数在其他包的可见性:大写时其他包可见,小写时只有相同的包可以访问。
func funcName(param-list)(result-list) {
function-body
}
1) 函数可以没有输入参数,也可以没有返回值
2)多个相邻的相同类型参数可以使用简写。 例如:
func add(a, b int) int {
return a + b
}
3)支持有名的返回值,参数名就相当于函数体内最外层的局部变量,命名返回值变量会被初始化为类型零值,最后的return可以不带参数名直接返回。
func add(a, b int) (sum int) {
sum = a + b
return // return sum的简写模式
// sum := a + b // 如果是 sum :=a+b,则相当于新声明一个sum变量命名返回变量sum覆盖
// return sum // 最后需要显式地调用return sum
}
4)不支持默认值参数。
5)不支持函数重载。
6)不支持函数嵌套,严格地说是不支持命名函数的嵌套定义 ,但支持嵌套匿名函数。例如:
func add(a, b int) (sum int) {
anonymous := func(x, y int) int {
return x + y
}
return anonymous(a, b)
}
2.1.2 多返回值
Go函数支持多值返回,定义多值返回的返回参数列表时,要使用()包括
func swap(a, b int) (int, int) {
return b, a
}
如果多返回值有错误类型,一般将错误类型作为最后一个返回值。
2.1.3 实参到形参的传递
Go函数实参到形参的传递永远是值拷贝。
2.1.4 不定参数
1) 所有的不定参数类型必须是相同的。
2)不定参数必须是函数的最后一个参数。
3)不定参数名在函数体内相当于切片,对切片的操作同样适合对不定参数的操作。例如:
func sum(arr ...int) (sum int) {
for _, v := range arr { // 此时 arr 就相当于切片,可以使用range访问
sum += v
}
}
4)切片可以作为参数传递给不定参数,切片名后要加上“...”。例如:
func sum(arr ...int) (sum int) {
for _, v := range arr {
sum += v
}
return
}
func main() {
slice := []int {1, 2, 3, 4}
array := [...]int {1, 2, 3, 4}
// 数组不可以作为实参传递给不定参数的函数
sum(slice...)
}
5)形参为不定参数的函数和形参为切片的函数类型不相同
func suma(arr ...int) (sum int) {
for v := range arr {
sum += v
}
return
}
func sumb(arr []int) (sum int) {
for v := range arr {
sum += v
}
return
}
// suma 和 sumb 的类型并不一样
fmt.Printf("%T\n", suma) // func(...int) int
fmt.Printf("%T\n", sumb) // func([]int) int
2.2 函数签名和匿名函数
2.2.1 函数签名
函数类型又叫函数签名。
func add(a, b int) int {
return a + b
}
func main() {
fmt.Printf("%T\n", add) // func(int, int) int
}
两个函数类型相同的条件是:拥有相同形参列表和返回值列表,以下两个函数类型完全一样:
func add(a, b int) int { return a + b}
func sub(x int, y int) (c int) { c = x - y; reutrn c}
可以使用type定义函数类型,函数类型变量可以作为函数的参数或者返回值
type Op func(int, int) int // 定义一个函数类型,输入的是两个int类型,返回值是一个int类型
func add(a, b int) int {
return a + b
}
func sub(a, b int) int {
reutrn a - b
}
func do(f Op, a, b i nt) int { // 定义一个函数,第一个参数是函数类型Op
return f(a, b) // 函数类型变量可以直接用来进行函数调用
}
func main() {
a := do(add, 1, 2) // 函数名add可以当作相同函数类型形参,不需要强制转换类型
fmt.Println(a) // 3
s := do(sub, 1, 2)
fmt.Println(s) // -1
}
函数类型和map、slice、chan一样,实际函数类型变量和函数名都可以当作指针变量,该指针指向函数代码的开始位置。
通常说函数类型变量是一中引用类型,未初始化的函数类型的变量默认值是 nil
有名函数的函数名可以看做函数类型的常量,可以直接使用函数没那个调用,也可以直接赋值给函数类型变量,后续通过该变量来调用。例如:
func add(a , b int) int {
return a + b
}
func main() {
add(1, 2) //直接调用
f := add // 赋值给变量,然后通过变量来调用
f(1, 2)
}
2.2.2 匿名函数
var sum = func(a, b int) int { // 匿名函数被直接赋值函数变量
return a + b
}
func doinput(f func(int, int) int, a, b int) int {
return f(a, b)
}
//匿名函数作为返回值
func wrap(op string) func(int, int) int {
switch op {
case "add":
return func(a, b int) int {
return a + b
}
case "sub":
return func(a, b int) int {
return a + b
}
default:
return nil
}
}
func main() {
// 匿名函数直接被调用
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
sum(1, 2)
//匿名函数作为实参
doinput(func(x, y int) int {
return x + y
}, 1, 2)
opFunc := wrap("add") //这里是把一个函数赋值给了 opFunc 这个变量
re := opFunc(2, 3) // 这里是调用 上面函数 返回 的函数
}
2.3 defer
注册多个延迟调用,先进后出(FILO),常用于回收和释放
func main() {
//先进后出
defer func() {
println(fister)
}()
defer func() {
println("second")
}
println("function body")
}
defer后面必须是函数或者方法的调用,不能是语句。
defer函数的实参在注册时通过值拷贝传递进去。下面代码,实参a的值在defer注册时通过值拷贝传递进入,后续a++并不会影响defer语句最后的输出结果.
func f() int {
a := 0
defer func(i int) {
println("defer i=", i)
}(a)
a++
return a
}
// defer打印结果
defer i=0
defer语句必须先注册 后才能执行,如果defer位于returtn之后,则defer因为没有注册,不会执行。
func main() {
defer func() {
println("first")
}()
a := 0
println(a)
return
defer func() {
println("second")
}()
}
结果 :
0
first
主动调用os.Exit(int)退出进程时,defer将不再被执行(即使defer已经提前注册)
func main() {
defer func() {
println("defer")
}()
println("func body")
os.Exit(1)
}
结果
func body
exit status 1
defer的好处是可以在一定程度上避免资源泄漏,特别是在有很多return语句,有多个资源需要关闭的场景,很容易漏掉资源的关闭操作,例如:
func CopyFile(dst, src string) (w int64, err error) {
src, err := os.Open(src)
if err != nil {
return
}
dst, err := os.Create(dst)
if err != nil {
// src很容易被忘记关闭
src.Close()
return
}
w, err = i.Copy(dst, src)
dst.Close()
src.Close()
return
}
使用defer改写后,在打开资源无报错后直接调用defer关闭资源,一旦养成这样的编程习惯,则很难会忘记资源的释放。例如:
func CopyFile(dst, src string) (w int64, err error) {
src, err := os.Open(src)
if err !=nil {
return
}
defer src.Close()
dst, err := os.Create(dst)
if err != nil {
return
}
defer dst.Close()
w, err = io.Copy(dst, src)
return
}
defer语句的位置不当,有可能导致panic,一般defer语句放在错误检查语句之后。
defer也有明显的副作用:defer会推迟资源的释放,defer尽量不要放到循环语句里面,将大函数内部的defer语句单独拆分一个小函数是一个和好的方式。
另外,defer相当于普通的函数调用需要间接的数据结构的支持,相对普通函数调用有一定的性能损耗。
defer 中最好不要对有名返回值参数进行操作,否则会发生奇怪的结果。
2.4 闭包
2.4.1 概念
闭包是由函数及其引用环境组合而成的实体,一般通过在匿名函数中引用外部函数的局部变量或包全局变量构成。
闭包 = 函数 + 引用环境
闭包对闭包外的环境引入是直接引用,编译器检查到闭包,会将闭包引用的外部变量分配到堆上。
func fa(a int) func(i int) int {
return func(i int) int {
println(&a, a)
a = a + i
return a
}
}
func main() {
f := fa(1) // f 引用的外部的闭包环境包括本次函数调用的形参a的值 1
g := fa(1) // g 引用的外部的闭包环境包括本次函数调用的形参a的值 1
// 此时 f、g 引用的必报环境中的a的值并不是同一个,而是两次函数调用产生的副本
println(f(1))
//多次调用 f 引用的是同一个副本 a
println(f(1))
// g 中 a 的值仍然是 1
println(g(1))
println(g(1))
}
f 和 g 引用的是不同的 a
如果一个函数调用返回的闭包引用修改了全局变量,则每次调用都会影响全局变量。
使用闭包是为了减少全局变量,所以闭包引用全局变量不是好的编程方式。例如:
var (
a = 0
)
func fa() func(i int) int {
return func(i int) int {
println(&a, a)
a = a + 1
return a
}
}
func main() {
f := fa() // f 引用的外部的必报环境包括全局变量 a
g := fa() // g 引用的外部的必报环境包括全局变量 a
// 此时, f 、 g 引用的必报环境中的a的值是同一个。
}
2.4.2 闭包的价值
闭包最初的目的是减少全局变量,但是一般不建议使用闭包
对象是附有行为的数据,而闭包是附有数据的行为。
2.5 panic 和 recover
2.5.1 基本概念
引发panic的两种情况,一是主动调用 panic 函数, 另一张是程序产生运行时错误,并由运行时检测并抛出。
recover()用来捕获panic,阻止panic继续向上传递。recover()和defer一起使用,但是recover()只有在defer后面的函数体内被直接调用才能捕获panic终止异常,否则返回nil,异常继续向外传递.
// 这个会捕获失败
defer recover()
//这个会捕获失败
defer fmt.Println(recover())
// 这个嵌套两层也会捕获失败
defer func() {
func() {
println("defer inner")
recover() //无效
}()
}()
//如下场景会捕获成功
defer func() {
println("defer inner")
recover()
}()
func except() {
recover()
}
func test() {
defer except()
panic("test panic")
}
2.5.2 使用场景
1)程序遇到无法正常执行下去的错误,主动调用panic
2)调试程序,通过主动调用panic实现快速退出,panic打印出的堆栈能够更快的定位错误。
2.6 错误处理
2.6.1 error
2.6.2 错误和异常
2.7 底层实现
2.7.1 函数调用规约
2.7.2 汇编基础
2.7.3 多值返回分析
GOOS=linux GOARCH=amd64 go tool complie -l -N -S swap.go > swap.s 2>&1
从汇编的代码得知:
1)函数的调用者负责环境准备,包括为参数和返回值开辟栈空间。
2)寄存器的保存和恢复也由调用方负责。
3)函数调用后回收栈空间,恢复BP也由主调函数负责。
函数的多值返回 实质上是在栈上开辟多个地址分别存放返回值,这个并没有什么特别的地方,如果返回值是存放在堆上,则多了一个复制的动作。
2.7.4 闭包底层实现
func a(i int) func() { //函数返回引用了外部变量i的闭包
return func() {
print(i)
}
}
func main() {
f := a(1)
f()
}