目录

函数基础篇

函数是组织好的、可重复使用的、用于执行指定任务的代码块。Go语言中支持函数、匿名函数和闭包,并且函数在Go语言中属于“一等公民”。

  • go语言不支持函数嵌套,在声明了函数里,不能再声明有名字的函数了。一般在其他语言中也不这么用,只是会调用函数而已。函数里声明函数,这种用法其实在其他被允许的语言里也不常见。
  • go语言同一个包里,是不能有同名函数的!公开性原则
  • 在同一个src目录下,不同的代码是可以调用相互的函数的。

1. 函数定义

Go语言中定义函数使用func关键字,具体格式如下:

1
2
3
func 函数名(参数)(返回值){
    函数体
}

其中:

  • 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也不能重名(包的概念详见后文)。
  • 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用,分隔。
  • 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用,分隔。
  • 函数体:实现指定功能的代码块。

2. 函数类型

2.1 没有参数,也没有返回值

1
2
3
4
5
6
func f() {
	fmt.Println("加油!")
}
func main() {
	f()  // 加油!
}

2.2 有参数无返回值

1
2
3
4
5
6
func sum(a int, b int) {
	fmt.Println(a + b)
}
func main() {
	sum(1, 2) // 直接输出3
}

2.3 没有参数,有返回值

注意有返回值时函数的写法

1
2
3
4
5
6
7
func f() string {
	return "加油!"
}
func main() {
	s := f()
	fmt.Println(s)  // 输出加油
}

2.4 命名函数返回值

返回值可以命名,也可以不命名, 命名的函数返回值相当于在函数中的一个变量

需要注意的是,这里如果是命名返回值,是可以省略的。

1
2
3
4
5
6
7
func f(a int, b int) (ret int) {
	ret = a + b
	return  
}
func main() {
	fmt.Println(f(1, 2)) //3 
}

2.5 多个返回值

1
2
3
4
5
6
func f() (string, int) {
	return "haha", 1
}
func main() {
	fmt.Println(f()) // haha 1
}

当函数有多个返回值,而我们只要其中一个或者几个时,可以使用匿名变量来接受不需要的

1
2
_, n := f()
fmt.Println(n) // 1

2.6 参数类型简写

多个参数类型相同时,可以使用下面方式简写,省略部分传参的类型

1
2
3
4
5
6
func f(a, b int, m, n string, b1 bool) int {
	return a + b
}
func main() {
	fmt.Println(f(1, 2, "aaa", "bbb", true)) // 3
}

2.7 可变长参数

1
2
3
4
5
6
7
func f(x string, y ...int) {
	fmt.Println(x,y)
}
func main() {
	f("下雨咯")
	f("你好", 1, 2, 3, 4, 5, 6)
}

out

1
2
下雨咯 []
你好 [1 2 3 4 5 6]

可以看到,可变长参数其实是slice

3. 示例1

1
2
3
4
5
6
func f(name string) {
	fmt.Println("你好", name)
}
func main() {
	f("bowen")
}

4. 示例2

1
2
3
4
5
6
7
8
func cal(x, y int) (sum, sub int) {
	sum = x + y
	sub = x - y
	return sum, sub
}
func main() {
	fmt.Println(cal(3, 1)) // 4,2
}

函数在Go语言中用的非常之多,要学会灵活使用函数。

5 函数名也是变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func f1() {
	fmt.Println("你好呀")
}
func f2() int {
	return 10
}
func main() {
	a := f1
	fmt.Printf("%T %v\n", a, a)
	b := f2
	fmt.Printf("%T %v\n", b, b())
	fmt.Println(b())
}

out

1
2
3
func() 0x132dd40
func() int 10 
10

可以看到,函数名其实也是变量,它也是有内存地址的!也是有类型的。比如func() int 就是这个变量的类型。在实际编码中是会用到的。

6. 函数作为参数

这种用法也会用到的,要记住

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 接上面代码
func f3(x func() int) {
	ret := x()
	fmt.Println(ret) 
}
func main() {
	a := f1
	fmt.Printf("%T %v\n", a, a)
	b := f2
	fmt.Printf("%T %v\n", b, b())
	fmt.Println(b())
	f3(f2)  //10 
	f3(b)   //10
}

7. 函数也可以作为函数的返回值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 接上面代码
func f5(a, b int) int {
	return a + b
}
func f4(z func() int) func(x, y int) int {  
	return f5
}
func main() {
	a := f1
	fmt.Printf("%T %v\n", a, a)
	b := f2
	fmt.Printf("%T %v\n", b, b())
	fmt.Println(b())

	f3(f2)
	f3(b)
	n := f4(f2)
	fmt.Println(n)  // 0xd3de60 返回函数的内存地址
}

8. 匿名函数

函数当然还可以作为返回值,但是在Go语言中函数内部不能再像之前那样定义函数了,只能定义匿名函数。匿名函数就是没有函数名的函数,匿名函数的定义格式如下:

1
2
3
func(参数)(返回值){
    函数体
}

匿名函数因为没有函数名,没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func main() {
	// 函数内部没有办法声明带名字的函数
	add := func(a, b int) int {
		sum := a + b
		return sum
	}
	fmt.Println(add(1, 2))
	// 如果只是调用一次的函数,可以简写成立即执行函数
	func(a, b int) {
		fmt.Println(a + b)
	}(1, 2)
}

out

1
2
3
3

匿名函数多用于实现回调函数和闭包。

9. defer

Go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func deferdemo() {
	fmt.Println("start...")
	defer fmt.Println("1")
	defer fmt.Println("2")
	defer fmt.Println("3")
	fmt.Println("end...")
}
func main() {
	deferdemo()
}

out

1
2
3
4
5
start...
end... 
3      
2      
1

由于defer语句延迟调用的特性,所以defer语句能非常方便的处理资源释放问题。比如:资源清理文件关闭解锁及记录时间等。

在Go语言的函数中return语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后,RETURN指令执行前。具体如下图所示:

/go基础/20230424202610.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func f1() int {
	x := 5 // 这个时候已经赋值完成,返回了x
	defer func() {
		x++ //再修改的x不是返回值了,而是x本身,x已经赋值完成返回回去了
		fmt.Println(x)
	}()
	return x  // 5
}
func main() {
	fmt.Println(f1()) //5
}
1
2
3
4
5
6
7
8
9
func f2() (x int) { // 命名返回值
	defer func() {
		x++ // 返回了5,x,修改的x就是返回值了,5++就是6
	}()
	return 5  
}
func main() {
	fmt.Println(f2()) // 6
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func f3() (y int) {
	x := 5
	defer func() {
		x++ // 这题改的是x,不是返回值,返回值是y。
	}()
	return x //返回值=y=x=5
}
func main() {
	fmt.Println(f3()) // 5
}
1
2
3
4
5
6
7
8
9
func f4() (x int) {
	defer func(x int) {
		x++   //改变的是函数中的副本
	}(x)
	return 5   // 返回值 = x = 5
}
func main() {
	fmt.Println(f5()) // 5
}

看上面四个代码片段,可以说稍微有点绕,不好理解,又回去翻了一遍视频才理解。长时间不看确实会生疏。

10. 闭包

闭包的概念还是很绕的,一开始看的时候并不理解,现在勉强理解,据说在实际编程中并不怎么用,后面随着逐步深入再理解吧。

Go 语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明.

闭包的描述 函数体内嵌套另外一个函数,并且返回的是一个函数, 主函数temp 体内的 匿名函数func, 被主函数体外 te引用的时候, 就形成了一个闭包 特性

不同变量引用的调用之间没有关系,公用一个主函数体内 变量i. 闭包函数不会被GC(垃圾回收机制)回收所占用资源,因此会出现外部调用时会接着上次调用的结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// FunOne 定义一个FunOne
// 这就是一个普通的函数,只不过传参是函数,在函数里调了函数
func FunOne(f func()) {
	f()
}

// FunTwo 定义了带参数的FunTwo,这个函数也是普通的有两个参数的函数
func FunTwo(x, y int) {
	test := x + y
	fmt.Printf("函数2的 x+y=%d\n", test)
}

// 怎么做不改变FunOne()的条件下,使用funOne() 调用funTwo()
func FunThree(f func(x, y int), left, right int) func() {
	temp := func() {
		f(left, right)
	}
	return temp
}

// 函数体内嵌另外一个函数,并且返回的是一个函数
func temp() func(int) {
	var i int
	return func(ind int) {
		i = i + ind
		fmt.Println(i)
	}
}
func main() {
	ret := FunThree(FunTwo, 10, 100)
	FunOne(ret) // 这样就在函数One中成功调用了函数Two,函数2的 x+y=110

	// 2. 主函数temp体内的匿名函数func,被主函数体外的te引用的时候就形成了一个闭包
	te := temp()
	// 3. 不同变量引用的调用之间没有关系,公用一个主函数体内的变量i
	// 4.闭包函数不会被GC(垃圾回收机制)回收所占用的资源,因此会出现外部调用时会接着上次调用的结果
	te(5)   // 调用函数之后i=5
	te(-10) // 调用函数之后i=-5
	te(1)   // 调用函数之后i=-4
	te(-2)  // 调用函数之后i=-6
}

这个例子还是不错的,解释的比较明白,我看了李文周老师的视频讲解这一块的时候,基本上被绕晕了。