- Go语言中支持函数、匿名函数和闭包,并且函数在Go语言中属于“一等公民”。
一、函数基础
1、常见的定义
格式:
func 函数名(参数)(返回值){
函数体
}
- 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。
- 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用,分隔。
- 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用,分隔。
- 函数体:实现指定功能的代码块。
示例:
// 1、标准的函数定义语法
func sum(a int, b int) (ret int) {
ret = a + b // 因为声明的返回值名称,因此可以直接用=号
return // 声明了返回值名称ret ,return后可以不加参数,默认返回ret
}
// 2、返回值可以命名,也可以不用命名(命名返回值,就相当于在函数中声明了一个变量,可以在函数内直接使用,否则需要:=的方式来声明)
func sum1(a int, b int) int {
return a + b
}
// 3、函数可以没有返回值
func sum2(a int, b int) {
fmt.Println(a + b)
}
// 4、没有参数没有返回值
func sum3() {
fmt.Println("sum3")
}
// 5、没有参数但是有返回值
func sum4() int {
return 2
}
2、函数的调用
- 定义了函数之后,我们可以通过 函数名() 的方式调用函数。 例如我们调用上面定义的两个函数
- 注意,调用有返回值的函数时,可以不接收其返回值。
func main() {
sayHello()
ret := intSum(10, 20)
fmt.Println(ret)
}
3、函数的参数
3.1、参数类型的简写
- 当参数中连续两个以上参数的类型一致时,我们可以将前面的参数的类型省略
// 1、参数类型的简写,当参数中连续两个以上参数的类型一致时,我们可以将前面的参数的类型省略
func sum6(x, y int, a, b, c string) int {
fmt.Println(a, b, c)
return x + y
}
3.2、可变长参数
- 可变长参数(变量名 …类型)这种方式传入,可变长参数返回的是slice切片类型数据
- 可变长参数可以传入多个参数,也可以不传参
- 但是注意:可变长参数必须放在函数参数的最后一个
// 2、可变长参数(变量名 ...类型)这种方式传入,可变长参数返回的是slice切片类型数据
func f1(a string, b ...int) {
fmt.Println(a, b)
}
func main() {
f1("小明", 1, 2, 3, 4, 5)
}
4、函数返回值
4.1、返回值可以命名,也可以不用命名
// 1、返回值可以命名,也可以不用命名(命名返回值,就相当于在函数中声明了一个变量,可以在函数内直接使用,否则需要:=的方式来声明)
func sum1(a int, b int) int {
return a + b
}
4.2、函数可以没有返回值
- Go语言中通过 return 关键字向外输出返回值。
- Go语言中函数支持多返回值,函数如果有多个返回值时必须用()将所有返回值包裹起来。
// 2、函数可以没有返回值
func sum2(a int, b int) {
fmt.Println(a + b)
}
4.3、多返回值
- 多个返回值时,要么都不命名,要么就都命名
func calc(x, y int) (int, int) {
sum := x + y
sub := x - y
return sum, sub
}
func calc(x, y int) (sum int, sub int) {
sum = x + y
sub = x - y
return
}
4.4、返回值补充
- 当我们的一个函数返回值类型为slice时,nil可以看做是一个有效的slice,没必要显示返回一个长度为0的切片。
func someFunc(x string) []int {
if x == "" {
return nil // 没必要返回[]int{}
}
}
补充: Go语言中不支持默认参数
二、函数进阶
1、变量作用域
1.1、全局变量
- 全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。 在函数中可以访问到全局变量。
var s = "函数外的全局变量" // 在函数外用var关键字定义的变量,包内全局有效
func Hunwen() {
fmt.Println(s)
}
func main() {
Hunwen()
}
1.2、局部变量
- 局部变量又分为两种:
- 1、函数内定义的变量无法在该函数外使用
- 2、函数内if条件判断、for循环、switch等语句内定义的变量在语句外部也无法调用
func f1() {
var s1 = "abc"
fmt.Println(s1)
}
func main() {
fmt.Println(s1) // 在函数f1中定义的变量在函数外无法被调用
}
- 如果局部变量和全局变量重名,优先访问局部变量。
var s1 = "函数外的全局变量" // 在函数外用var关键字定义的变量,包内全局有效
func f1() {
var s1 = "abc"
fmt.Println(s1) // abc
}
- if条件判断、for循环、switch语句上定义变量。
func f1(a, b int) {
fmt.Println(a, b)
if a > b {
Z := 10
fmt.Println(Z) // 在函数内的if、for、switch等语句中定义的变量,在语句外无法调用
}
fmt.Println(z) // 在这个位置无法使用变量z
}
func main() {
f1(2, 3)
}
2、高阶函数
- 高阶函数分为函数作为参数和函数作为返回值两部分。
- 函数其实也可以看作是一种类型
示例
func A() {
fmt.Println("abc")
}
func B() int {
return 2
}
func C(x, y int) int {
return x + y
}
func main() {
a := A
fmt.Printf("%T\n", a) // a 是 func() 类型
b := B
fmt.Printf("%T\n", b) // b 是 func()int 类型
c := C
fmt.Printf("%T\n", c) // c 是 func(int, int) int 类型
}
- 函数也可以作为参数传入
func B() int {
return 2
}
// 函数也可以作为参数传入
func D(f func() int) {
ret := f()
fmt.Println(ret)
}
func main() {
D(B) // 返回结果2,将函数B作为参数传入,传入的函数B必须满足函数D的参数类型
}
- 函数可以作为返回值
func f1() int {
return 2
}
func f2(a func() int) func(int, int) int { // 将func(int, int) int 这个类型函数作为返回值
ret := func(x, y int) int {
return x + y + a()
}
return ret
}
func main() {
f3 := f2(f1) // f3 接收 f2 返回的函数
fmt.Println(f3(1, 2)) // 输出结果为5
}
3、匿名函数和闭包
3.1、匿名函数
- 没有命名的函数
示例:
// func直接定义一个匿名函数,可以用变量接收
var f1 = func(a, b int) {
fmt.Println(a + b)
}
func main() {
f1(1, 2)
}
- 一般情况下,我们是不会在全局定义匿名函数的,一般在命名函数内使用
- 在命名的函数内,无法再声明一个命名的函数,但是匿名函数可以
示例:
func demo(a,b int){
// 1、可以在函数内定义匿名函数,用变量接收后可以多次调用
f1 := func (x,y int) {
fmt.Println( x + y)
}
f1(1,2)
// 2、如果只是使用一次,可以简写成立即执行函数。即在函数定义后加上()调用
func (x string) {
fmt.Println(x)
}("abc")
}
3.2 闭包
3.2.1、闭包的定义
- 闭包指的是一个函数和与其相关的引用环境组合而成的实体,简单来说,“闭包=函数+引用环境”
- 闭包是一个函数,这个函数包含了它外部作用域的变量,“闭包=函数+外部变量的引用”
示例1:
func adder() func(int) int {
var x int
return func(y int) int {
x += y
return x
}
}
func main() {
var ret = adder() // 此时的ret其实就是adder返回的匿名函数,这个其实就是个闭包
ret1 := ret(100) // 相当于将100传参进adder内的匿名函数
fmt.Println(ret1) // 100,打印的结果其实就是x
}
3.2.2、闭包的进阶
- 如下演示:
- 变量f是一个函数并且它引用了其外部作用域中的x变量,此时f就是一个闭包。 在f的生命周期内,变量x也一直有效。
- 内部匿名函数中的参数优先在函数体内部寻找,没有则一层层往外找
进阶示例1:
func adder(x int) func(int) int {
return func(y int) int {
x += y
return x
}
}
func main() {
var ret = adder(100) // 将100传入adder函数,此时的ret其实就是adder返回的匿名函数且内部变量x的值为adder传入的100
ret1 := ret(200) // 相当于将200传参进adder内的匿名函数
fmt.Println(ret1) // 300,打印的结果其实就是x
}
闭包进阶示例2:
func makeSuffixFunc(suffix string) func(string) string {
return func(name string) string {
if !strings.HasSuffix(name, suffix) { // strings.HasSuffix()方法用作字符串后缀判断,如果参数1是以参数2为后缀的,那么就返回true
return name + suffix
}
return name
}
}
func main() {
jpgFunc := makeSuffixFunc(".jpg")("test")
txtFunc := makeSuffixFunc(".txt")
fmt.Println(jpgFunc) //test.jpg
fmt.Println(txtFunc("test")) //test.txt
}
闭包进阶示例3:
func calc(base int) (func(int) int, func(int) int) {
add := func(i int) int {
base += i
return base
}
sub := func(i int) int {
base -= i
return base
}
return add, sub
}
func main() {
add, sub := calc(10)
fmt.Println(add(1), sub(2)) //11 9
}
3.2.3、应用场景演示
应用场景演示如下:小明写了函数f1,提供出一个接口。小红写了函数f2,需要用f1去调用,但是如下示例中,由于f1接收参数与f2的类型不一致,所以无法接收,针对这种场景,就可以使用闭包
// 小明写了一个接口函数f1
func f1(f func()) {
fmt.Println("this is f1")
f()
}
// 小红写了功能函数f2,需要被f1调用,但是此时是无法直接作为参数传入的
func f2(x, y int) {
fmt.Println("this is f2")
fmt.Println(x + y)
}
// 可以定义一个函数f3,对函数f2进行包装
func f3(f func(int, int), x, y int) func() {
tmp := func() {
fmt.Println("this is f3")
f(x, y) // 这里的x和y在匿名函数内部没找到,就会去外层找,通过f3参数传入
}
return tmp // 返回的是 func() 类型函数,满足了 f1 的参数类型
}
func main() {
f3(f2, 1, 2) // 这样return的函数满足了f1的参数类型,且f3内部实质上只是调用了f2
f1(f3(f2, 1, 2))
}
变量f是一个函数并且它引用了其外部作用域中的x变量,此时f就是一个闭包。 在f的生命周期内,变量x也一直有效。 闭包进阶示例1:
5、defer语句
5.1、了解defer语句
- Go语言中的 defer 语句会将其后面跟随的语句进行延迟处理。
- 在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行 (即先被defer的语句最后被执行 且 最后被defer的语句,最先被执行)
- 注意:由于defer语句延迟调用的特性,所以defer语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。
示例:
func f1(a, b, c int) {
fmt.Println("start")
defer fmt.Println(a) // 最前面的defer,最后执行
defer fmt.Println(b)
defer fmt.Println(c) // 最下面的defer,最先输出
fmt.Println("end") // 非defer的语句一定会在defer语句前执行
}
func main() {
f1(1, 2, 3)
}
输出结果
start
end
3
2
1
5.2、defer执行时机
- 在Go语言的函数中return语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后,RET指令执行前。
流程图如下:
- 在没有defer语句的函数中,函数 return 返回值有两个步骤,例如
return x
- 1、先将变量x的值赋值给返回值
- 2、执行底层的RET执行,进行返回
- 在有defer语句的函数中,defer语句会在 return 的中间步骤运行
- 1、先将 变量x 的值赋值给 返回值
- 2、执行defer语句
- 3、执行RET指令
5.3、defer经典案例
- 看代码,写结果
func f1() int {
x := 5
defer func() {
x++
}()
return x // 返回值时5
}
func f2() (x int) {
defer func() {
x++
}()
return 5 // 返回的结果是6。先x=5,然后执行defer语句x++,最后return 6。
}
func f3() (y int) {
x := 5
defer func() {
x++
}()
return x // 返回5。首先返回值 = y = x = 5,然后x++,但是返回值y依旧是5了
}
func f4() (x int) {
defer func(x int) {
x++
}(x)
return 5 // 返回的是5,defer声明的参数x,和传入的x不是同一个作用域
}
// 传入一个x的指针到匿名函数
func f5() (x int) {
defer func(x *int) {
fmt.Println(x)
*x++
}(&x)
return 5
}
func main() {
fmt.Println(f1())
fmt.Println(f2())
fmt.Println(f3())
fmt.Println(f4())
fmt.Println(f5())
}
5.4、defer的又一特性
- defer语句后面跟着的函数参数是按照正常的执行顺序传入的,只是函数执行是遵守defer的规则
常见的关于defer的面试题
func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}
func main() {
x := 1
y := 2
defer calc("AA", x, calc("A", x, y)) // defer calc("AA", 1, 3)
x = 10
defer calc("BB", x, calc("B", x, y)) // defer calc("BB", 10, 12)
y = 20
}
返回结果
A 1 2 3
B 10 2 12
BB 10 12 22
AA 1 3 4
三、内置函数初了解
1、内置函数介绍
内置函数 | 介绍 |
---|---|
close | 主要用来关闭channel |
len | 用来求长度,比如string、array、slice、map、channel |
new | 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针 |
make | 用来分配内存,主要用来分配引用类型,比如chan、map、slice |
append | 用来追加元素到数组、slice中 |
panic 和 recover | 用来做错误处理 |
2、panic/recover
- Go语言中目前(Go1.12)是没有异常机制,但是使用panic/recover模式来处理错误。
- panic可以在任何地方引发,但recover只有在defer调用的函数中有效。
- 应用场景:例如程序引发了一个错误,导致程序奔溃,但是程序仍在运行,资源仍旧被占用,panic和recover就是为了处理这种情况
- 注意点:
- recover()必须搭配defer使用。
- defer一定要在可能引发panic的语句之前定义。
- recover()在特定的场景下可以使用,正常情况下,该panic的地方,还是应该panic,不要跳过
示例:利用 panic 来触发异常,直接退出程序
func funcA() {
fmt.Println("func A")
}
func funcB() {
// 假设这里进行数据库连接
defer func() {
fmt.Println("释放数据库连接")
}()
panic("出现严重错误!!") // 连接失败,程序崩溃退出
fmt.Println("func b")
}
func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}
输出结果:运行了函数A,然后运行函数B时,触发异常后退出,函数C就不执行了
func A
释放数据库连接
panic: 出现严重错误!!
goroutine 1 [running]:
main.funcB(...)
E:/GoProject/src/gitee.com/LTP/main.go:12
main.main()
E:/GoProject/src/gitee.com/LTP/main.go:20 +0x66
exit status 2
- 程序运行期间funcB中引发了panic导致程序崩溃,异常退出了。这个时候我们就可以通过recover将程序恢复回来,继续往后执行(即 recover 的作用就是尝试恢复下程序)。
示例:
func funcA() {
fmt.Println("func A")
}
func funcB() {
// 假设函数检测刚连接数据库出错
defer func() {
err := recover() // 这个recover()方法,可以拿到panic报出的错误,定义的recover()办法后,程序就会继续向下执行
fmt.Println(err) // "出现严重错误!!", 打印的结果就是panic报出的异常
//如果程序出出现了panic错误,可以通过recover恢复过来
if err != nil {
fmt.Println("recover in B") // 这里定义尝试解决的办法代码,尝试让程序恢复
}
fmt.Println("释放数据库连接")
}()
panic("出现严重错误!!") // 程序崩溃退出
fmt.Println("func b")
}
func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}
执行结果:
func A
出现严重错误!!
recover in B
释放数据库连接
func C
小试牛刀
/*
你有50枚金币,需要分配给以下几个人:Matthew,Sarah,Augustus,Heidi,Emilie,Peter,Giana,Adriano,Aaron,Elizabeth。
分配规则如下:
a. 名字中每包含1个'e'或'E'分1枚金币
b. 名字中每包含1个'i'或'I'分2枚金币
c. 名字中每包含1个'o'或'O'分3枚金币
d: 名字中每包含1个'u'或'U'分4枚金币
写一个程序,计算每个用户分到多少金币,以及最后剩余多少金币?
程序结构如下,请实现 ‘dispatchCoin’ 函数
*/
var (
coins = 50
users = []string{
"Matthew", "Sarah", "Augustus", "Heidi", "Emilie", "Peter", "Giana", "Adriano", "Aaron", "Elizabeth",
}
distribution = make(map[string]int, len(users))
)
func main() {
left := dispatchCoin()
fmt.Println("剩下:", left)
}
实现方式:
func dispatchCoin() (count int) {
count = 50
for i := 0; i < len(users); i++ {
for key := range scoringFormula {
for _, v := range users[i] {
if key == v {
count -= int(scoringFormula[key])
distribution[users[i]] += int(scoringFormula[key])
}
}
}
}
fmt.Println(distribution)
return count
}
func main() {
left := dispatchCoin()
fmt.Println("剩下:", left)
}
// map[Aaron:3 Adriano:5 Augustus:12 Elizabeth:4 Emilie:6 Giana:2 Heidi:5 Matthew:1 Peter:2]
// 剩下: 10