一、概述
函数 是基于功能或逻辑进行封装的可复用的代码结构。将一段功能复杂、很长的一段代码封装成多个代码片段(即函数),有助于提高代码可读性和可维护性。由于 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关键字来指定。
return可以有参数,也可以没有参数,这些返回值可以有名称,也可以没有名称。go中的函数可以有多个返回值。return关键字中指定了参数时,返回值可以不用名称。如果return省略参数,则返回值部分必须带名称。- 当返回值有名称时,必须使用括号包围,逗号分隔,即使只有一个返回值。
- 但即使返回值命名了,
return中也可以强制指定其它返回值的名称,也就是说return的优先级更高 - 命名的返回值是预先声明好的,在函数内部可以直接使用,无需再次声明。命名返回值的名称不能和函数参数名称相同,否则报错提示变量重复定义
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,exists、return value,ok、return 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 上面就会花费相对较多的系统开销(内存和时间)。所以当需要传递大的结构体的时候,用指针是一个明智的选择。
map、slice、interface、channel这些数据类型本身就是指针 类型的,所以就算是拷贝传值也是拷贝的指针,拷贝后的参数仍然指向底层数据结构,所以修改它们可能 会影响外部数据结构的值
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语言中闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量,因此,简单的说:
函数 + 引用环境 = 闭包
同一个函数与不同引用环境组合,可以形成不同的实例,如下图所示。
一个函数类型就像结构体一样,可以被实例化,函数本身不存储任何信息,只有与引用环境结合后形成的闭包才具有“记忆性”,函数是编译期静态的概念,而闭包是运行期动态的概念。
示例
// 返回一个函数
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指令执行前。
具体如下图所示:
十、内置函数介绍
| 内置函数 | 介绍 |
|---|---|
| close | 主要用来关闭channel |
| len | 用来求长度,比如string、array、slice、map、channel |
| new | 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针 |
| make | 用来分配内存,主要用来分配引用类型,比如chan、map、slice |
| append | 用来追加元素到数组、slice中 |
| panic和recover | 用来做错误处理 |
附参考文章链接