1.048596

Golang函数类型和闭包

Golang中万物皆有类型函数也不例外,函数的使用是每一种语言中无法缺省的一部分。函数的重要性一部分源自于现代计算机进程执行模型大部分是基于的,编译器不需要太多的转换即可通过函数实现进栈压栈出栈,函数在Go语言中更是作为第一公民

函数属性

函数特点

返回值比其他语言更强,支持多值返回

多值返回是这种语言一个非常好的地方,灵活使用效果非常的好且舒适。

函数支持可变的参数

可变参数的传递下面会有说到,通常传入的参数会以slice的形式存在,并且能够用类切片的方法使用。

函数在Go语言中也是一种类型,叫做函数类型,具有函数变量

这一点有些像C++中的函数指针的设定,但是使用起来简单地多。

函数并不支持函数的重载

这有些让人诟病,当然也有解决办法那就是使用interface来传递不同的变量,并且使用类型判断来决定不同的执行操作。

实际运用

使用技巧

  • 简写参数

函数参数列表的类型如果有几个连续的相同,那么可以只把这几个相同类型参数的最后一个用类型标注就行了。

  • 命名返回值

返回值的的名字也可以直接像参数一样准备好,这样默认情况下就会把函数内同名的变量的值给返回。在声明返回值会默认初始化为零值

多值返回

通过定义多个返回值就可以实现多值返回,并且接收变量用逗号隔开就能让返回值对号入座了。

// This achieves multi-value returns.
func Mix(x int, y int) (int, int) {
	return x + y, x - y
}

可变参数

在这种语言里面,函数的参数如果有标识就会扩容改参数位的容量,可以不断加入参数而不是一一对应,这样的补丁参数部分的参数传入之后会以切片的形式存在。注意,不定参数必须是函数的最后一个参数。

func Change(y ...int) {
	for i := 0; i < len(y); i++ {
		y[i]++
	}
}

参数传递

函数的传参在Go语言中被精简成了值传递,在使用的层面上弱化了指针的特点,但是实际作用没有发生改变。官方文档已经明确的说明了,函数传参只有值传递一种方式

本质上只有值拷贝,但是对于指针地址的拷贝刚好指向了原地址

引用型结构参数

切片是对底层数组的引用这一点没有问题,但是在传参的时候如果使用了切片会怎样呢。

传参本质

传递参数的本质依旧是值拷贝,但是slice中的值是通过指针的方式和底层数组联系的,所以按值传递的切片只能够修改值,而不能修改容量或者长度,否则底层数组变化就会不再对原数组修改了。

当然,如果传递的是切片的地址则可以修改所有的属性,而且能将效果反馈给原slice

实际应用

  • 数组转切片

想要通过函数修改数组中的值,则可以将数组当作切片传入。

func Change(arr []int) {
    arr[0]++
}
var arr int[3] = {1, 2 ,3}
Change(arr[:])

通道传参和切片传参很相似,他们同样是引用类型,作为参数传递的时候传递的方法可以互相作为参考。

// pass by value
func Change(c chan int) {}
// pass by pointer
func Change(c *chan int) {}

签名

函数既然作为一种类型,描述函数的类型的就是函数签名。一个函数的类型就是函数定义首行去掉函数名、参数名和函数定义部分,剩下的就是函数签名

  • 签名类别

函数在Go语言中可以是一种类型,可以作为函数参数来传递。这时候就需要用函数签名来描述函数参数的类型了,这样对应类型的函数就能作为参数传入。

拥有相同的形参列表和返回值列表的函数类型相同

还有一种方法可以定义函数的类型别名,就是使用type的类型别名方法。

type Handler func(int, int) int
// After this definition, this function type has a name, instead of using a
// function signature to describe.

匿名函数

匿名函数的运用在Golang中十分的广泛,通常启动一个简单的goroutine操作的时候直接使用了匿名函数的嵌套定义来实现。需要注意的是,匿名函数能够嵌套定义是闭包实现的基础。

// The declaration is same as a normal function, but there is no function name.
func main() {
	f := func() {
		fmt.Println("Hello World!")
	}
	f()
}

匿名函数在使用过程中还可以直接执行,那就是直接在定义后面用()来传入参数直接调用。

// It can also write in parameters in time and execute directly.
func(x int, y int) int {
  	return x + y
}(10, 20)

闭包

首先明确什么是闭包,函数+引用环境=闭包,解释开来就是闭包是由函数和相关引用环境组合而成的实体。

用一个闭包函数多次,如果该闭包修改了其引用的外部变量,则每一次该闭包都会对该外部变量产生影响,因为是引用

闭包只是在形式和表现上像函数,但实际上不是函数。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例,也就是说产生的函数具体内容是根据环境而决定的。

调用闭包

闭包的调用主要靠是匿名函数来实现的。通过这样的操作返回对应的函数,这样可以得到根据引用环境而变化的对应的函数。

func Creator(a int) func(i int) int {
	return func(i int) int {
		a = a + i
		return a
	}
}

深度理解

闭包函数的返回值是函数

返回的函数绑定在闭包函数内一个变量上

外部引用

闭包可以引用外部环境中的变量,比如全局变量和局部变量,这体现了闭包和函数的区别。这种引用并不是通过参数来传递的。

func Creater() func() int {
	n := 0
	// Modifying the referenced external variable within the closure function
	// will cause the original variable to change.
	return func() int {
		n++
		return n
	}
}

多线程闭包

多线程编程的简洁高效是Go语言的一大特点,闭包在多线程时就更需要注意处理外部环境的情况,尤其是使用循环来创建goroutine的时候。

  • 错误启动线程

这样启动线程的闭包会因为启动线程速度远低于循环进行的速度而出现意料之外的结果。比如这里的i全部等于5。即使是第一个线程真正启动的时候循环也已经到达了最后一个值了,这时候的闭包的外部环境i=5,所以就会出现问题。

var wg sync.WaitGroup
func main() {
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			fmt.Println(i)
			wg.Done()
		}()
	}
	wg.Wait()
}
  • 正确实例

如果作为参数而不是运行时的环境来传入变量就会在一开始就已经决定了传入的值,那么这些闭包启动就是正常的了。

var wg sync.WaitGroup
for i := 0; i < 5; i++ {
	wg.Add(1)
	go func(i int) {
		fmt.Println(i)
		wg.Done()
	}(i)
}
wg.Wait()