Go语言中的功能封装是通过函数进行的,不同结构体之间可以通过接口来进行统一,再结合反射特性就可以开发大型的、复杂的项目。


文章目录

  • 一、函数的定义
  • 二、闭包
  • 补充一下:
  • 三、作用域
  • 四、返回值与变长参数
  • 4.1、返回多个值
  • 4.2、入参为变长参数
  • 4.3、空接口作为变长参数
  • 五、defer关键字


一、函数的定义

Go语言是支持面向对象编程的,但是函数才是Go语言的基本组成元素。
Go语言的函数分为具名函数、匿名函数

//具名函数
func main(a int) int {
	return a * a
}
main(11)

//匿名函数
var nua = func(a int) int {
	return a * a
}
nua(12)

二、闭包

匿名函数可以赋值给一个变量,也可以直接写在另一个函数的内部:

func main() {
	f := double()
	fmt.Println(f())
	fmt.Println(f())
	fmt.Println(f())
	fmt.Println(f())
}

func double() func() int  {
	// 1、int类型的默认初始值是0
	var r int
	// 2、返回一个匿名函数
	return func() int {
		r++
		return r*2
	}
}

这里注意一下,返回值的类型为 func() int ,返回的是一个匿名函数
这里的匿名函数就是闭包函数
这里的输出结果:

2
4
6
8

很奇怪的是,为什么不是 2 2 2 2 呢?这就是闭包函数与普通函数的不同之处。
匿名函数在编译的时候将变量包装进自己的函数内了,从而跨过了作用域额限制,可以让变量r一直存在,这就是闭包。

补充一下:

1、闭包会把函数和所访问的变量打包到一起,不再关心这个变量原来的作用域,闭包本身可以看作是独立对象。
2、闭包函数与普通函数的最大区别就是参数不是值传递,而是引用传递,所以闭包函数可以操作自己函数以外的变量。
3、闭包函数对外部变量的操作才使其不能被释放回收,从而跨过了作用域的限制。

三、作用域

对比一下两组代码:

var funcList []func()
for i:=0; i<3; i++{
	funcList = append(funcList,func(){
		fmt.Println(i)
	})
}
for _,f := range funcList{
	f()
}

运行结果:

3
3
3
var funcList2 []func()
for i:=0;i<3;i++{
	j := i  //<<<=================== 不同之处
	funcList2 = append(funcList2,func(){
		fmt.Println(j)
	})
}
for _,f := range funcList2{
	f()
}

运行结果:

0
1
2

上述两个代码块的结果对比差异比较大,原因是在于匿名函数使用外部变量用的是指针,并非值复制。
在for循环中变量i是一个共享变量,每一次循环都会把原理存储的值加1。三次循环执行完后,i存储的值就是3。
第一个代码块中,函数执行的时候虽然打印了3次,也只是重复打印了三次3。
第二个代码块中,循环体内每次执行的时候额外声明了一个局部变量j,相当于每次都把i不同的值存储下来。

注意:
Go语言里面的局部变量名称和全局变量名称是可以重复的,重复的时候系统会默认把重名变量看作是局部变量。

var funcList3 []func(int)
	for i:=0;i<3;i++{
		funcList3 = append(funcList3,func(i int){
			fmt.Println(i)
		})
	}
	for i,f := range funcList3{
		fmt.Println("===========",i)
		f(i)
	}

运行结果:

=========== 0
0
=========== 1
1
=========== 2
2

闭包函数会将自己用到的变量都保存在内存中,导致变量无法被及时回收,并且可能通过闭包修改父函数使用的变量值。

四、返回值与变长参数

4.1、返回多个值

func swap(a,b int) (int,int) {
	return b,a
}
fmt.Println(swap(1,2))

4.2、入参为变长参数

func sum(a int,others ...int) int {
	for _,v := range others{
		a += v
	}
	return a
}
fmt.Println(sum(1,2,3,4,5,6,7,8,9,10)) // 55

argus := []int{2,3,4,5,6,7,8,9,10}
// 加...把切片里面的值解析出来再传递
fmt.Println(sum(1,argus...)) // 55

4.3、空接口作为变长参数

func print(a ...interface{})  {
	fmt.Println(a...)
}
// 空接口定义可变长参数
args := []interface{}{1234,"abcd"}
print(args) // [1234 abcd]
print(args...) // 1234 abcd  加 ... 把切片里面的值解析出来再传递

Go语言中函数的参数传递只有值传递一种方式,即便是使用指针,也是使用值传递的方式传递了指针的值。

这里要强调一下函数的参数传递方式。Go语言中,函数的参数只能进行值传递,不能进行引用传递。虽然可以使用指针,但是本质上传递的还是指针指向的地址,因为访问的是地址内的值,所以会被误认为是引用传递。

比如,切片会让人觉得函数在处理切片时使用的是引用传递,其实是因为切片里面包含地址,所以可以直接访问。

此外,切片包含的长度和容量也是通过值传递传到函数内的,如果在函数内修改了长度或容量,函数外的切片是接收不到的,所以需要再返回一个切片,也是基于这个原因,append函数才会每次都返回切片。

五、defer关键字

在实际应用中,确保在执行函数的过程中遇到报错时能及时处理一些必要的事情,比如关闭连接等情况可以使用defer关键字来实现这些功能。

defer关键字用于释放资源,会在函数返回之前调用,即便函数崩溃也会在结束前调用defer。

一般用法如下:

f,err := os.Open(fileName)
if err != nil{
	panic(err)
}
defer f.Close()

这样操作后,后面处理的代码即便报错,也会在结束前先执行文件关闭操作。

一个函数内也可以有多个defer,在调用的时候按照栈的方式先进后出,即写在前面的会后调用(由最里层的defer语句先执行)。