Golang函数详解

283 阅读11分钟

一、概述

函数 是基于功能或逻辑进行封装的可复用的代码结构。将一段功能复杂、很长的一段代码封装成多个代码片段(即函数),有助于提高代码可读性和可维护性。由于 Go 语言是编译型语言,所以函数编写的顺序是无关紧要的。

特点

  • 无需声明原型
  • 支持不定变参
  • 支持多返回值
  • 支持命名返回参数
  • 支持匿名函数和闭包
  • 函数也是一种类型,一个函数可以赋值给变量
  • 不支持函数嵌套 (nested) ,但可以嵌套匿名函数。
  • 不支持重载 (overload) ,一个包不能有两个名字一样的函数。
  • 不支持默认参数 (default parameter)

二、函数的声明

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

func 函数名(参数名 类型,参数名 类型)(返回值1类型,返回值2类型){
	函数体
	return 返回值1,返回值2
}

说明

  • 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名不能重名。

  • 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用,分隔。

  • 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用,分隔。

  • 函数体:实现指定功能的代码块。

示例

//定义一个函数,求两数之和
//函数返回一个无名变量,返回值列表的括号省略
func sum(x int,y int) int{
	return x + y
}

// 参数的类型一致,只在最后一个参数后添加该类型
func sub(x , y int) int {
	return x - y
}

//调用该函数打印出Hello GO
//函数的参数和返回值都是可选的,下方函数既不需要参数也没有返回值
func hello(){
	fmt.Println("Hello GO")
}
func main() {
    //调用函数
	hello()
	s := sum(10,20)
	b := sub(20,10)
	fmt.Println(s)
	fmt.Println(b)
}

说明

  • 形式参数列表:函数的参数名以及参数类型,这些参数作为局部变量,其值由参数调用者提供,函数中的参数列表和返回值并非是必须的。

  • 返回值列表:函数返回值的变量名以及类型,如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的。

  • 如果有连续若干个参数的类型一致,那么只需在最后一个参数后添加该类型。

  • 定义了函数之后,可以通过函数名()的方式调用函数。

注意:调用有返回值的函数时,可以不接收其返回值。

三、可变参数

3.1 多个类型一致的参数

可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...来标识。

示例

func sum2and(x ...int) int{
	fmt.Println(x)	//x是一个切片
	sum := 0
	for _,v := range x{
		sum = sum + v
	}
	return sum
}

func main() {
	ret1 := sum2and()
	ret2 := sum2and(10)
	ret3 := sum2and(10, 20)
	ret4 := sum2and(10, 20, 30)
	fmt.Println(ret1, ret2, ret3, ret4) //0 10 30 60
}

运行结果

[]
[10]
[10 20]
[10 20 30]
0 10 30 60

固定参数搭配可变参数使用时,可变参数要放在固定参数的后面

示例代码

func sum3and(x int, y ...int) int {
	fmt.Println(x, y) //y是一个切片
	sum := x
	for _, v := range y {
		sum = sum + v
	}
	return sum
}
func main() {
	ret5 := sum3and(100)
	ret6 := sum3and(100, 10)
	ret7 := sum3and(100, 10, 20)
	ret8 := sum3and(100, 10, 20, 30)
	fmt.Println(ret5, ret6, ret7, ret8) //100 110 130 160
}

运行结果

100 []
100 [10]
100 [10 20]
100 [10 20 30]
100 110 130 160

本质上,函数的可变参数是通过切片来实现的。

注意:如果该函数下有其他类型的参数,这些其他参数必须放在参数列表的前面,切片必须放在最后。

3.2 多个类型不一致的参数

如果传多个参数的类型都不一样,可以指定类型为 ...interface{} ,然后再遍历。

func printType(args ...interface{}) {
	for _, arg := range args {
		switch arg.(type) {
		case int:
			fmt.Println(arg, "type is int.")
		case string:
			fmt.Println(arg, "type is string.")
		case float64:
			fmt.Println(arg, "type is float64.")
		case bool:
			fmt.Println(arg, "type is boole.")
		default:
			fmt.Println(arg, "is an unknown type.")
		}
	}
}
func main() {
	printType(10, 3.14, "李林超博客",true)
}

运行结果

10 type is int.
2.16 type is float64.
李林超博客 type is string.
true type is boole.

四、返回值

4.1 定义

函数可以有0或多个返回值,返回值需要指定数据类型,返回值通过 return关键字来指定。

  1. return可以有参数,也可以没有参数,这些返回值可以有名称,也可以没有名称。go中的函数可以有多个返回值。
  2. return关键字中指定了参数时,返回值可以不用名称。如果return省略参数,则返回值部分必须带名称。
  3. 当返回值有名称时,必须使用括号包围,逗号分隔,即使只有一个返回值。
  4. 但即使返回值命名了,return中也可以强制指定其它返回值的名称,也就是说return的优先级更高
  5. 命名的返回值是预先声明好的,在函数内部可以直接使用,无需再次声明。命名返回值的名称不能和函数参数名称相同,否则报错提示变量重复定义
  6. return中可以有表达式,但不能出现赋值表达式,这和其它语言可能有所不同。例如return a+b是正确的,但return c=a+b是错误的。

4.2 实例

(1)没有返回值

func hello() {
	fmt.Printf("Hello GO")
}

(2)有一个返回值

func sum(x int, y int) (ret int) {
	ret = x + y
	return ret
}

(3)多个返回值,且在return中指定返回的内容

func person() (name string, age int) {
	name = "Leefs"
	age = 20
	return name, age
}

(4)多个返回值,返回值名称没有被使用

func person2and() (name string, age int) {
	name = "Leefs"
	age = 20
	return // 等价于return name, age
}

(5)return覆盖命名返回值,返回值名称没有被使用

func person3and() (name string, age int) {
	n := "Leefs"
	a := 20
	return n, a
}

Go中经常会使用其中一个返回值作为函数是否执行成功、是否有错误信息的判断条件。例如 return value,existsreturn value,okreturn value,err等。

当函数有多个返回值时,如果其中某个或某几个返回值不想使用,可以通过下划线 _来丢弃这些返回值。

五、参数传递

5.1 值传递

func changeA(a int) {
	a = 200
	fmt.Printf("a1: %v\n", a)	//a1: 200
}

func main() {
	a := 100
	changeA(a)
	fmt.Printf("a: %v\n", a)	//a: 100
}

从运行结果可以看到,调用函数changeA后,a的值并没有被改变,说明参数传递是拷贝了一个副本,也就是拷贝了一份新的内容进行运算。

5.2 引用传递

引用传递本质上也是值传递,只不过这份值是一个指针(地址)。 所以我们在函数内对这份值的修改,其实不是改这个值,而是去修改这个值所指向的数据,从而会影响到函数外部的值的。

func changeA(a *int) {
	*a = 200
	fmt.Printf("a1: %v\n", *a)	//a1: 200
}

func main() {
	a := 100
	changeA(&a)
	fmt.Printf("a: %v\n", a)	//a: 200
}

传指针使得多个函数能操作同一个对象。

传指针比较轻量级(8bytes),只是传内存地址,可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次 copy 上面就会花费相对较多的系统开销(内存和时间)。所以当需要传递大的结构体的时候,用指针是一个明智的选择。

mapsliceinterfacechannel这些数据类型本身就是指针 类型的,所以就算是拷贝传值也是拷贝的指针,拷贝后的参数仍然指向底层数据结构,所以修改它们可能 会影响外部数据结构的值

func changeSlice(a []int) {
	a[0] = 100
}

func main() {
	a := []int{1, 2}
	changeSlice(a)
	fmt.Printf("a: %v\n", a)	//a: [100 2]
}

从运行结果发现,调用函数后,slice内容被改变了。

六、高阶函数

高阶函数分为函数作为参数和函数作为返回值两部分。

6.1 函数作为参数

func sayHello(name string) {
	fmt.Printf("Hello,%s", name)
}

func f1(name string, f func(string)) {
	f(name)
}

func main() {
	f1("Leefs", sayHello)
}

运行结果

Hello,Leefs

6.2 函数作为返回值

func add(x, y int) int {
	return x + y
}

func sub(x, y int) int {
	return x - y
}

func cal(s string) func(int, int) int {
	switch s {
	case "+":
		return add
	case "-":
		return sub
	default:
		return nil
	}
}

func main() {
	add := cal("+")
	r := add(1, 2)
	fmt.Printf("r: %v\n", r)

	fmt.Println("-----------")

	sub := cal("-")
	r = sub(100, 50)
	fmt.Printf("r: %v\n", r)
}

运行结果

r: 3
-----------
r: 50

七、匿名函数

Go语言函数不能嵌套,但是在函数内部可以定义匿名函数,实现一下简单功能调用。

匿名函数就是没有函数名的函数,匿名函数的定义格式如下:

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

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

func main() {
	// 将匿名函数保存到变量
	add := func(x, y int) {
		fmt.Println(x + y)
	}
	add(10, 20) // 通过变量调用匿名函数

	//自执行函数:匿名函数定义完加()直接执行
	func(x, y int) {
		fmt.Println(x + y)
	}(10, 20)
}

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

八、闭包

Go语言中闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量,因此,简单的说:

函数 + 引用环境 = 闭包

同一个函数与不同引用环境组合,可以形成不同的实例,如下图所示。

12.Golang函数02.jpg

一个函数类型就像结构体一样,可以被实例化,函数本身不存储任何信息,只有与引用环境结合后形成的闭包才具有“记忆性”,函数是编译期静态的概念,而闭包是运行期动态的概念。

示例

// 返回一个函数
func add() func(int) int {
	var x int
	return func(y int) int {
		x += y
		return x
	}
}

func main() {
	var f = add()
	fmt.Println(f(10))
	fmt.Println(f(20))
	fmt.Println(f(30))
	fmt.Println("-----------")
	f1 := add()
	fmt.Println(f1(40))
	fmt.Println(f1(50))
}

运行结果

10
30
60
-----------
40
90

变量 f是一个函数并且它引用了其外部作用域中的 x变量,此时 f就是一个闭包。 在 f的生命周期内,变量 x也一直有效。

闭包进阶示例1:

func add(x int) func(int) int {
	return func(y int) int {
		x += y
		return x
	}
}
func main() {
	var f = add(10)
	fmt.Println(f(10))
	fmt.Println(f(20))
	fmt.Println(f(30))

	fmt.Println("----------")

	f1 := add(20)
	fmt.Println(f1(40))
	fmt.Println(f1(50))
}

运行结果

20
40
70
----------
60
110

闭包进阶示例2:

func makeSuffixFunc(suffix string) func(string) string {
	return func(name string) string {
		if !strings.HasSuffix(name, suffix) {
			return name + suffix
		}
		return name
	}
}

func main() {
	jpgFunc := makeSuffixFunc(".jpg")
	txtFunc := makeSuffixFunc(".txt")
	fmt.Println(jpgFunc("test")) 
	fmt.Println(txtFunc("test")) 
}

运行结果

test.jpg
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() {
	f1, f2 := calc(10)
	fmt.Println(f1(1), f2(2)) 
	fmt.Println(f1(3), f2(4)) 
	fmt.Println(f1(5), f2(6)) 
}

运行结果

11 9
12 8
13 7

闭包其实并不复杂,只要牢记 闭包=函数+引用环境

九、defer语句

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

示例

func main() {
	fmt.Println("start")
	defer fmt.Println(1)
	defer fmt.Println(2)
	defer fmt.Println(3)
	fmt.Println("end")
}

运行结果

start
end
3
2
1

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

defer执行时机

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

具体如下图所示:

12.Golang函数03.png

十、内置函数介绍

内置函数介绍
close主要用来关闭channel
len用来求长度,比如string、array、slice、map、channel
new用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针
make用来分配内存,主要用来分配引用类型,比如chan、map、slice
append用来追加元素到数组、slice中
panic和recover用来做错误处理

附参考文章链接

www.liwenzhou.com/posts/Go/09…

www.go-edu.cn/2022/05/16/…