1、golang函数基本定义与使用
func 函数名 (形参列表) (返回值类型列表) {
函数体
return + 返回值列表
}
其中func用于表明这是一个函数,剩下的东西与其他语言的函数基本一致,在定义与使用的时候注意函数名、参数、返回值书写的位置即可。下面使用一个例子说明:求两个整数的和
func main() {
fmt.Println("10 + 20 = ", getSum(10, 20))
}
func getSum (num1, num2 int) int {
//这里的返回值类型列表如果只有一个返回值,那么() 可以省略
return num1 + num2
}
一些函数的使用细节:
- 函数与函数是并列的关系,因此不能嵌套定义
- 函数名的定义需要遵循标识符命名规范,
即 首字符不能是数字
函数名首字母大写表明该函数可以被本包文件和其他包文件使用
函数名首字母小写表明只能被本包文件使用 其他包文件不能使用 - 形参列表 可以是若干个参数 0,1,2 ····
- 返回值类型列表:可以返回若干个,取决于函数的实现以及应用场景。对于返回值有多个的情况,如果在某一特定场景下,有些返回值不想使用,那么可以选择使用 "_"来告诉编译器这个返回值自动丢弃,不接收
2、golang函数传递参数相关细节
经典案例引入:实现一个函数exchangeNum函数,交换两个数的值
func exchangeNum(n1, n2 int) {
var temp int = n1
n1 = n2
n2 = temp
}
func main() {
var num1 int = 100
var num2 int = 200
fmt.Printf("exchange before num1 is %v, num2 is %v \n", num1, num2)
exchangeNum(num1, num2)
fmt.Printf("exchange after num1 is %v, num2 is %v \n", num1, num2)
}
函数exchangeNum并没有将两个数完成交换。写出这样的代码根本原因是对参数的传递不够了解。下面展开说明。
我们都知道一个程序在运行时会为其分配一块内存空间,这块内存空间在逻辑上被划分为堆区、栈区、代码区等区块。其中函数执行时会在栈区上为该函数分配一个栈帧,该栈帧在函数调用完毕后会被自动销毁。所以这个栈帧的生命周期是跟随函数的。
用此例来讲,main函数运行时,会产生main函数的栈帧,内部有两个变量num1=100, num2=200,调用exchangeNum的时候,会产生该函数的栈帧,然后该函数接收到两个参数n1=100, n2=200,在exchangeNum函数内,确实完成了这两个数值的交换,函数执行完n1, n2的值交换了。但是函数执行完exchangeNum函数栈帧也一同被销毁了,反观main函数的栈帧中num1 和num2的值依旧是没有发生变化。这也就是为什么执行完exchangeNum函数后两个变量的值并没有被交换的原因。
如何成功交换?
func exchangeNum(n1, n2 *int) {
var temp int = *n1
*n1 = *n2
*n2 = temp
}
func main() {
var num1 int = 100
var num2 int = 200
fmt.Printf("exchange before num1 is %v, num2 is %v \n", num1, num2)
exchangeNum(&num1, &num2)
fmt.Printf("exchange after num1 is %v, num2 is %v \n", num1, num2)
}
//方法2
func main() {
var num1 int = 100
var num2 int = 200
num1, num2 = num2, num1
}
注意:
golang函数不支持函数重载
golang中支持可变参数 “…”
func test(args... int) { //test函数可以接受多个int类型的参数
//在函数内部处理可变参数时将其当作切片来处理
for(index := range args) {
//相关处理,如打印
}
}
基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改不会影响到原来的值
以值传递方式的数据类型,如果希望在函数内的改动能都影响到函数外,可以传入变量的地址&,函数内以指针的方式操作变量,从效果上看类似于引用传递
3、Go中函数可以当作数据类型
在Go语言中,函数也是一种数据类型,可以赋值给一个变量,那么该变量就是一个函数类型的变量。通过该变量可以对函数进行调用
既然函数是一种数据类型,因此在go中,函数可以作为形参并且被调用
func exchangeNum(n1, n2 *int) {
var temp int = *n1
*n1 = *n2
*n2 = temp
}
// 定义一个函数,能够接收函数作为形参
func forTest(flag bool, funcExNum func(*int, *int), num1, num2 *int) {
if flag {
funcExNum(num1, num2)
}
}
func main() {
var num1 int = 100
var num2 int = 200
fmt.Printf("exchange before num1 is %v, num2 is %v \n", num1, num2)
myexchange := exchangeNum
fmt.Printf("myexchange 对应的类型是:%T, exchangeNum对应的类型是: %T \n", myexchange, exchangeNum)
//此时使用 myexchange(&num1, &num2)等价于exchangeNum(&num1, &num2)
myexchange(&num1, &num2)
fmt.Printf("exchange after num1 is %v, num2 is %v \n", num1, num2)
//此时使用forTest函数再次进行交换 并测试 函数雷总最为形参的使用
forTest(true, myexchange, &num1, &num2)
fmt.Printf("exchange after forTest: num1 is %v, num2 is %v \n", num1, num2)
}
为了简化数据类型定义,Go语言支持自定义数据类型
基本语法:type 自定义数据类型 数据类型
func main() {
type myInt int
var num1 myInt = 100
var num2 int = 40
//注意:虽然是别名,但是在编译器看来,
//myInt和int并不是同一种数据类型
num2 = int(num1)
}
func exchangeNum(n1, n2 *int) {
var temp int = *n1
*n1 = *n2
*n2 = temp
}
type myFun func(*int, *int)
// 定义一个函数,能够接收函数作为形参
func forTest(flag bool, funcExNum myFun, num1, num2 *int) {
if flag {
funcExNum(num1, num2)
}
}
支持对函数返回值命名
//定义一个函数,返回两数之和 和 差
func dealNums(num1 int, num2 int) (int, int) {
sum := num1 + num2
sub := num1 - num2
return sum, sub
}
这样的写法要求返回值与函数返回值类型列表需要一一对应
还有一种写法,显示地指出返回值
func dealNums(num1 int, num2 int) (sum int, sub int) {
sub := num1 - num2
sum := num1 + num2
return
}
注意:这样写在函数内部代码执行完毕后,会将sub的最终值返回给函数返回值类型列表中的sub,sum也是如此!
4、匿名函数
匿名函数是指不需要定义函数名的一种函数实现方式。
在Go里面,函数可以像普通变量一样被传递或使用,Go语言支持随时在代码里定义匿名函数
匿名函数由一个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,不必声明
匿名函数的定义格式如下:
/*
func(参数列表)(返回参数列表){
函数体
}
*/
package main
import (
"fmt"
"math"
)
func main() {
//这里将一个函数当做一个变量一样的操作
getSqrt := func(a float64) float64 {
return math.Sqrt(a)
}
fmt.Println(getSqrt(4))
}
匿名函数可以在申明后调用
func(data int) {
fmt.Println("hello", data)
}(100) //(100),表示对匿名函数进行调用,传递参数为 100。
匿名函数也可以用作回调函数
package main
import (
"fmt"
)
// 遍历切片的每个元素, 通过给定函数进行元素访问
func visit(list []int, f func(int)) {
for _, v := range list {
f(v)
}
}
func main() {
// 使用匿名函数打印切片内容
visit([]int{1, 2, 3, 4}, func(v int) {
fmt.Println(v)
})
}
返回多个匿名函数
package main
import "fmt"
func FGen(x, y int) (func() int, func(int) int) {
//求和的匿名函数
sum := func() int {
return x + y
}
// (x+y) *z 的匿名函数
avg := func(z int) int {
return (x + y) * z
}
return sum, avg
}
func main() {
f1, f2 := FGen(1, 2)
fmt.Println(f1())
fmt.Println(f2(3))
}
5、闭包
所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
简单来说就是包含在代码块中的自由变量和引用对象在作用域结束时并没有被释放,已经与被执行的代码块绑定到一起
Go语言中的闭包同样也会引用到函数外的变量。闭包的实现确保只要闭包还被使用,那么被闭包引用的变量会一直存在。
闭包=函数+引用环境
示例:
package main
import (
"fmt"
)
func squares() func() int {
var x int
return func() int {
x++
return x * x
}
}
func main() {
f1 := squares()
f2 := squares()
fmt.Println("first call f1:", f1()) //1
fmt.Println("second call f1:", f1())//4
fmt.Println("first call f2:", f2()) //1
fmt.Println("second call f2:", f2())//4
}
6、延迟调用
Go语言的 defer 语句会将其后面跟随的语句进行延迟处理
defer特性:
- 关键字 defer 用于注册延迟调用。
- 这些调用直到 return 前才被执。因此,可以用来做资源清理。
- 多个defer语句,按先进后出的方式执行。
- defer语句中的变量,在defer声明时就决定了。
defer的用途:
- 关闭文件句柄
- 锁资源释放
- 数据库连接释放
defer 的执行时机
- 在Go语言的函数中return语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。
- defer语句执行的时机就在返回值赋值操作后,RET指令执行前。具体如下图所示:
go 语言的defer功能强大,对于资源管理非常方便,但是如果没用好,也会有陷阱
package main
import "fmt"
func main() {
var whatever = [5]int{1,2,3,4,5}
for i := range whatever {
defer fmt.Println(i)
}
//[5,4,3,2,1] 实现逆序输出
}
另一个示例
package main
import (
"log"
"time"
)
func main() {
start := time.Now()
log.Printf("开始时间为:%v", start)
defer log.Printf("时间差:%v", time.Since(start)) // Now()此时已经copy进去了
//不受这3秒睡眠的影响
time.Sleep(3 * time.Second)
log.Printf("函数结束")
}
这个函数本意是想得到main函数执行所耗费的时间差,但是实际上得到的时间远远小于3s,这是不合实际的。原因是在执行defer语句的时候已经将当前时间copy进去了,因此在函数退出时真正执行defer的时候,时间差并不是用此时的时间与start做差,而是用之前的Now()与start做差
- Go 语言中所有的函数调用都是传值的
- 调用 defer 关键字会立刻拷贝函数中引用的外部参数 ,包括start 和time.Since中的Now
- defer的函数在压栈的时候也会保存参数的值,并非在执行时取值。
如何解决??使用defer fun()
package main
import (
"log"
"time"
)
func main() {
start := time.Now()
log.Printf("开始时间为:%v", start)
defer func () {
log.Printf("时间差:%v", time.Since(start))
}()
time.Sleep(3 * time.Second)
log.Printf("函数结束")
}
因为拷贝的是函数指针,函数属于引用传递
小试牛刀
package main
import "fmt"
func main() {
var whatever = [5]int{1,2,3,4,5}
for i,v := range whatever {
//函数正常执行
//由于闭包用到的变量 i 在执行的时候已经变成4,所以输出全都是4
//而func()在执行的时候,v的值为5,因此输出的全都是5
defer func() { fmt.Println(i, v) }()
}
}
package main
import "fmt"
func main() {
var whatever = [5]int{1,2,3,4,5}
for i,v := range whatever {
i := i
v := v
defer func() { fmt.Println(i, v) }()
}
}
为什么会有这样的结果呢?
这是应为 defer语句中的匿名函数会捕获外部的变量。由于i和v是在循环中定义的,它们会在每次迭代中都创建一个新的副本。因此,每个匿名函数都捕获了不同的i和v值,最终的输出将是索引和值的正确组合。
7、异常处理
Go语言中使用 panic 抛出错误,recover 捕获错误。
异常的使用场景简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理
panic:
- 内置函数
- 假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
- 返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
- 直到goroutine整个退出,并报告错误
recover:
- 内置函数
- 用来捕获panic,从而影响应用的行为
golang 的错误处理流程:
当一个函数在执行过程中出现了异常或遇到 panic(),正常语句就会立即终止,然后执行 defer 语句,再报告异常信息,最后退出 goroutine。
如果在 defer 中使用了 recover() 函数,则会捕获错误信息,使该错误信息终止报告。
注意:
- 利用recover处理panic指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。
- recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
- 多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。
package main
func main() {
test()
}
func test() {
defer func() {
if err := recover(); err != nil {
// 将 interface{} 转型为具体类型。
println(err.(string))
}
}()
panic("panic error!")
}
由于 panic、recover 参数类型为 interface{},因此可抛出任何类型对象
延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获
package main
import "fmt"
func test() {
defer func() {
// defer panic 会打印
fmt.Println(recover())
}()
defer func() {
panic("defer panic")
}()
panic("test panic")
}
func main() {
test()
}