【从零开始捡起GO】9. 函数

函数

函数是组织好的、可重复使用的、用于执行指定任务的代码块。Go语言中支持函数、匿名函数和闭包。

在go中,函数也是一种特殊的数据类型func()

定义一个函数

函数的定义方法如下:

func function_name( [parameter list] ) [return_types] {
   函数体
}
  1. func:函数由func关键字开始声明
  2. function_name:函数名称,函数名和参数列表一起构成了函数签名。
  3. parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
  4. return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。另外go中是支持多返回的
  5. 函数体:函数定义的代码集合。

使用函数

这里举一个例子,交换两个变量的值:

func swap(i int, j int) (int, int) {
	return j, i
}

调用函数:

func main() {
	i, j := 1, 2
	fmt.Println(i, j) // 1 2
	i, j = swap(i, j)
	fmt.Println(i, j) // 2 1
}

引用传递和值传递

给函数传递的参数叫形参,形参拥有自己的内存空间,与函数外的变量的值一样,内存地址不一样,函数内外的变量是不会互相影响的。举例:

func main() {
	i, j := 1, 2
	fmt.Printf("init: i = %v, j = %v\r\n", i, j)
	// init: i = 1, j = 2
	func1(i, j)
	fmt.Printf("outside func1: i = %v, j = %v\r\n", i, j)
	// outside func1: i = 1, j = 2
}

func func1(i int, j int) {
	i++
	j++
	fmt.Printf("inside func1: i = %v, j = %v\r\n", i, j)
	// inside func1: i = 2, j = 3
}

如果想要在函数内改变函数外的变量,可以使用引用传递,也就是之前说过的&寻址,*找值

func main() {
	i, j := 1, 2
	fmt.Printf("init: i = %v, j = %v\r\n", i, j)
	// init: i = 1, j = 2
	func1(&i, &j)
	fmt.Printf("outside func1: i = %v, j = %v\r\n", i, j)
	// outside func1: i = 2, j = 3
}

func func1(i *int, j *int) {
	*i++
	*j++
	fmt.Printf("inside func1: i = %v, j = %v\r\n", i, j)
	// inside func1: i = 0xc00000a0a8, j = 0xc00000a0c0
	fmt.Printf("inside func1: *i = %v, *j = %v\r\n", *i, *j)
	// inside func1: *i = 2, *j = 3
}

对于引用传递,基于强类型语言的变量类型强一致性的优势,在引用传递上会比php严谨很多。引用传递应用还是非常广的,但是在php中因为简化了对引用传递的使用方法,操作不当会很容易导致变量污染相关问题。在强类型语言go中应该不会存在这个问题。

匿名函数

匿名函数指没有名字的函数;并没有牵扯到应用其他函数的变量问题。仅仅是没有名字。一般来说匿名函数会赋值给一个func()类型的变量或者作为参数进行调用:

作为变量
func main() {
	i, j := 1, 2
	fmt.Printf("i = %v, j = %v\r\n", i, j)
	// i = 1, j = 2
	swap := func(i int, j int) (int, int) {
		return j, i
	}
	i, j = swap(i, j)
	fmt.Printf("i = %v, j = %v\r\n", i, j)
	// i = 2, j = 1
}
作为函数的参数
func main() {
	callback(func() string {
		return "call me"
	})
}

func callback(f func() string) {
	fmt.Println(f())
	// call me
}

闭包

A函数中嵌套着B函数,B程序中有用到A的变量,当外部函数C调用函数A时,虽然A已经执行完毕,理论上函数执行完毕,它就要被弹出栈,但是由于B要用到A,所以A的变量被保存到内存中不被销毁,我们称函数B是闭包。 闭包可以使函数内的变量延迟销毁,并且可以保证函数内外的变量互相隔离:

func main() {
	clock1 := clock(0)
	fmt.Println(clock1())
	// 1
	fmt.Println(clock1())
	// 2
	fmt.Println(clock1())
	// 3
	
	clock2 := clock(100)
	fmt.Println(clock2())
	// 101
	fmt.Println(clock2())
	// 102

	fmt.Println(clock1())
	// 4
	fmt.Println(clock2())
	//103
}

func clock(start int) func() int {
	return func() int {
		start++
		return start
	}
}

可变参数

与php,java一样,go中是支持可变参数的。当需要向函数内传递的参数数量未知的时候可以使用变量 ...类型来传递多个参数。函数内会用一个数组接收他:

func main() {
	list(1,5,2,7)
}

func list(i ...int) {
	fmt.Println(reflect.TypeOf(i))
	// []int
	for k, v := range i {
		fmt.Println(k,v)
	}
	// 0 1
	// 1 5
	// 2 2
	// 3 7
}

延迟处理defer

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

func main() {
	fmt.Println("main start")
	defer fmt.Println("main d1")
	defer fmt.Println("main d2")
	defer fmt.Println("main d3")
	list()
	fmt.Println("main end")
}

func list() {
	fmt.Println("list start")
	i := 100
	defer fmt.Println("d1",i)
	i++
	defer fmt.Println("d2",i)
	i++
	defer fmt.Println("d3",i)
	fmt.Println("list end")
}

输出

main start
list start
list end
d3 102
d2 101
d1 100
main end
main d3
main d2
main d1
defer注册要延迟执行的函数时该函数所有的参数都需要确定其值

从上面的执行顺序可以看出来defer的执行顺序。另外可以看到,defer在执行之前,其值已经确定了

d3 102 d2 101 d1 100

这里举一个例子

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))
	x = 10
	defer calc("BB", x, calc("B", x, y))
	y = 20
	fmt.Println("------")
}

输出

A 1 2 3
B 10 2 12
------
BB 10 12 22
AA 1 3 4

运行流程是这样的:

  1. 注册defer defer calc("AA", x, calc("A", x, y)) 这里再注册时,需要确定 calc("A", x, y) 的值,所以会执行一次
  2. 注册defer defer calc("BB", x, calc("B", x, y)) 这里再注册时,需要确定 calc("B", x, y) 的值,所以会执行一次
  3. 输出了--------
  4. 开始执行defer,根据defer的执行顺序堆原则,先进后出,先执行 defer calc("BB", x, calc("B", x, y)) 再执行 defer calc("AA", x, calc("A", x, y)),其中defer内部注册时已经确定了 calc("B", x, y)calc("A", x, y) 的值,不会重复执行

return和defer的执行顺序

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

我从其他地方找了一个例子:

func f1() int {
	x := 5
	defer func() {
		x++
	}()
	return x // 5
}

func f2() (x int) {
	defer func() {
		x++
	}()
	return 5 // 6
}

func f3() (y int) {
	x := 5
	defer func() {
		x++
	}()
	return x // 5
}
func f4() (x int) {
	defer func(x int) {
		x++
	}(x)
	return 5 // 5
}
func main() {
	fmt.Println(f1()) // 5
	fmt.Println(f2()) // 6 
	fmt.Println(f3()) // 5
	fmt.Println(f4()) // 5
}

依次看一下上面的例子:

f1
func f1() int {
	x := 5 // 一个函数体内的局部变量x
	fmt.Printf("%v\r\n",&x) // 0xc00000a0a8
	defer func() {
		x++ // 匿名函数没有传参,会使用上层f1()内的局部变量x
		fmt.Printf("%v\r\n",&x) // 0xc00000a0a8
	}()
	return x // 看起来是x,其实是赋值给了返回的无名int类型变量,这里还是5
}

这里return x的流程

  1. 给返回值赋值,也就是无名的int类型,这里的x和返回值是不一样的
  2. 执行defer里的匿名函数,因为x没有座位参数传入,此时会向外找高层的变量x并++
  3. 执行返回无名的int,也就是之前赋值的5,defer 没有对 return 产生任何影响
f2
func f2() (x int) {
	defer func() {
		x++ // 这里依然没有形参,会向上寻找高位的变量x,也就是返回值
	}()
	return 5
}

这里 return 5 的流程

  1. 给返回值赋值,也就是给返回的 x int 赋值,此时 x = 5
  2. 执行 defer 的匿名函数,因为没有形参 x 此时会寻找高层的变量x,也就是刚才给返回值赋值的 x 并++
  3. 执行RET,返回了++后的 x 也就是6
f3
func f3() (y int) {
	x := 5
	fmt.Printf("%v\r\n",&x) // 0xc00000a0e0
	defer func() {
		x++
		fmt.Printf("%v\r\n",&x) // 0xc00000a0e0
	}()
	return x
}

这里 return x 的流程

  1. 给返回值赋值,也就是给返回的 y int 赋值,此时 y = 5
  2. 执行 defer 的匿名函数,因为没有形参 x 此时会寻找高层的变量x,这里需要注意的是, f2() 中的 x 是返回值,而这里的 x 是和 f1() 一样的,高层的变量。
  3. 执行RET,返回了变量 y 也就是5
f4
func f4() (x int) {
	defer func(x int) {
		x++
	}(x)
	return 5
}

这里 return x 的流程

  1. 给返回值赋值,也就是给返回的 x int 赋值,此时 x = 5
  2. 执行 defer 的匿名函数,因为有形参 x ,所以此时匿名函数内的 x 和函数外的 x 就不是一个内存地址了,相互是隔离状态的
  3. 执行RET,返回了变量 x 也就是5
自己举一个例子

这里看了别人的例子,可以根据 f4() 进行进一步修改,加上引用操作来尝试一下

func f() (x int) {
	defer func(y *int) {
		*y++
	}(&x)
	return 5
}
func main() {
	fmt.Println(f()) // 6
}

这里 return x 的流程

  1. 给返回值赋值,也就是给返回的 x int 赋值,此时 x = 5
  2. 执行 defer 的匿名函数,因为有形参 y ,所以此时匿名函数内的 y 和函数外的 x 就不是一个内存地址了,相互是隔离状态的。但是!此时x传递的是返回值 x 的内存地址,也就是说,匿名函数内的 y 是一个指针,其保存的是返回值 x 的内存地址,这里使用 *y++ 来通过地址寻找返回值 x 的值++
  3. 执行RET,返回了变量 x 也就是6

活学活用,希望看到这里的你可以根据这些案例有自己的领悟。

总结

入门思维导图下载(使用xmind打开):golang.xmind

程序幼儿员-龚学鹏
请先登录后发表评论
  • latest comments
  • 总共0条评论