【go语言学习】函数function
函数是组织好的、可重复使用的、用于执行指定任务的代码块。
Go语言中支持函数、匿名函数和闭包,并且函数在Go语言中属于“一等公民”。
一、函数的声明和调用
1、函数的声明
Go语言中定义函数使用func关键字,具体格式如下:
func funcName(parametername type) (output type) {
//这里是处理逻辑代码
//返回多个值
return valu
}
- func 函数由func开始声明
- funcName 函数名
- () 函数的标志
- parametername type 函数的参数列表,参数列表指定参数的类型、顺序以及个数,参数是可选,可有可无;这里的参数相当于一个占位符,所以也叫形式参数,当函数被调用时,传入的值传递给参数,这个值被称为实际参数
- ouput type 返回值列表,返回值可以由名称和类型组成,也可以只写类型不写名称;返回值可以有一个或多个,当有一个返回值时可以不加
()
,多个返回值时必须加()
- 函数体:实现指定功能的代码块
{}
里面的内容。
2、函数的调用
可以通过funcName(parameter)
的方式调用函数。调用有返回值的函数时,可以不接收其返回值。
package main
import "fmt"
func main() {
a := 10
b := 20
res := sum(a, b)
fmt.Printf("%v + %v = %v", a, b, res)
}
func sum(a, b int) int {
return a + b
}
运行结果
10 + 20 = 30
二、函数的参数
1、参数的使用:
形式参数:定义函数时,用于接收外部传入的数据,叫做形式参数,简称形参。
实际参数:调用函数时,传给形参的实际的数据,叫做实际参数,简称实参。
函数调用:
- A:函数名称必须匹配
- B:实参与形参必须一一对应:顺序,个数,类型
函数的参数中如果相邻变量的类型相同,则可以省略类型,如:
package main
import "fmt"
func main() {
a := 10
b := 20
res := add(a, b)
fmt.Printf("%v + %v = %v", a, b, res)
}
func add(a, b int) sum int {
sum = a + b
return
}
2、可变参数
可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...来标识。
注意:可变参数通常要作为函数的最后一个参数。
func funcName(arg ...int) {}
arg ...int
告诉Go这个函数接受不定数量的参数。注意,这些参数的类型全部是int。在函数体中,变量arg是一个int类型的slice
3、参数的传递
go语言函数的参数也是存在值传递和引用传递
数据类型:
-
按照数据类型来分:
- 基本数据类型
int,float,string,bool - 复合数据类型
arry,slice,map,struct,interface,chan...
- 基本数据类型
-
按照数据的存储特点来分:
- 值类型的数据,操作的是数据本身。
int,float,string,bool,arry,struct - 引用类型的数据,操作的数据的地址。
slice,map,chan
- 值类型的数据,操作的是数据本身。
-
值传递:值类型的数据传递为值传递,传递的是数据的副本。修改数据,对原始数据没有影响。
package main
import "fmt"
func main() {
arr1 := [3]int{1, 2, 3}
fmt.Println("函数调用前,数组的数据:", arr1)
fun(arr1)
fmt.Println("函数调用后,数组的数据:", arr1)
}
func fun(arr2 [3]int) {
fmt.Println("函数中,数组的数据:", arr2)
arr2[0] = 100
fmt.Println("函数中,修改后数据的数据:", arr2)
}
运行结果
函数调用前,数组的数据: [1 2 3]
函数中,数组的数据: [1 2 3]
函数中,修改后数据的数据: [100 2 3]
函数调用后,数组的数据: [1 2 3]
- 引用传递:引用类型的数据传递为引用传递,传递的数据的地址,导致多个变量指向同一块内存。
package main
import "fmt"
func main() {
slice1 := []int{1, 2, 3}
fmt.Println("函数调用前,切片的数据:", slice1)
fun(slice1)
fmt.Println("函数调用后,切片的数据:", slice1)
}
func fun(slice2 []int) {
fmt.Println("函数中,切片的数据:", slice2)
slice2[0] = 100
fmt.Println("函数中,修改后切片的数据:", slice2)
}
运行结果
函数调用前,切片的数据: [1 2 3]
函数中,切片的数据: [1 2 3]
函数中,修改后切片的数据: [100 2 3]
函数调用后,切片的数据: [100 2 3]
- 指针传递:传递的就是数据的内存地址。
三、函数的返回值
函数的返回值:
一个函数的执行结果,返回给函数调用处,执行结果就叫函数的返回值。
return语句:
一个函数的定义上有返回值,那么函数中必须有return语句,将执行结果返回给函数的调用处。
函数的返回结果必须和函数定义的一致,类型、数量、顺序。
- 返回值的命名
函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回。
func add(x, y int) (sum int) {
sum = x + y
return
}
- 多返回值
一个函数可以没有返回值,也可以有一个返回值,也可以有返回多个值。
func calc(x, y int) (sum, sub int) {
sum = x + y
sub = x - y
return
}
- 空白标识符
-
可用来舍弃某些返回值。
func calc(x, y int) (sum, sub int) {
sum = x + y
sub = x - y
return
}
_, sub := calc(10, 20) //舍弃sum
四、函数的作用域
作用域:变量可以使用的范围。
1、全局变量
全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。 所有的函数都可以使用,而且共享这一份数据。
package main
import "fmt"
var a = 10
func main() {
fmt.Println("test调用前,main中访问a:", a)
test()
fmt.Println("test调用后,main中访问a:", a)
}
func test() {
fmt.Println("操作前,test中访问a: ", a)
a = 20
fmt.Println("操作后,test中访问a: ", a)
}
运行结果
test调用前,main中访问a: 10
操作前,test中访问a: 10
操作后,test中访问a: 20
test调用后,main中访问a: 20
2、局部变量
一个函数内部定义的变量,就叫做局部变量
局部变量只能在定义的范围内访问操作
package main
import "fmt"
func main() {
test()
fmt.Println("main中访问a:", a) //undefined: a
}
func test() {
a := 20
fmt.Println("test中访问a: ", a)
}
运行结果
# command-line-arguments
.\main.go:7:35: undefined: a
局部变量和全局变量重名,优先访问局部变量。
package main
import "fmt"
var a = 100
func main() {
test()
}
func test() {
a := 20
fmt.Println("test中访问a: ", a)
}
运行结果
test中访问a: 20
另外,if
,switch
,for
语句中声明的变量也属于局部变量,在代码块外无法访问。
五、函数的本质
函数也是Go语言中的一种数据类型,可以作为另一个函数的参数,也可以作为另一个函数的返回值。
1、函数是一种数据类型
package main
import "fmt"
func main() {
fmt.Printf("%T\n", fun1) //fun1的类型是func(int, int)
fmt.Printf("%T\n", fun2) //fun2的类型是func(int, int) int
}
func fun1(a, b int) {
fmt.Println(a, b)
}
func fun2(c, d int) int {
fmt.Println(c, d)
return 0
}
运行结果
func(int, int)
func(int, int) int
2、定义函数类型的变量
var f fun(int, int) int
上面语句定义了一个变量f,它是一个函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值。
所有参数和返回值符合条件的函数可以赋值给f变量
package main
import "fmt"
func main() {
var f func(int, int) int
f = sum
res := f(20, 10)
fmt.Println("20 + 10 = ", res)
f = sub
res = f(20, 10)
fmt.Println("20 - 10 = ", res)
}
func sum(a, b int) int {
return a + b
}
func sub(a, b int) int {
return a - b
}
运行结果
20 + 10 = 30
20 - 10 = 10
六、匿名函数
匿名函数就是没有函数名的函数
func (参数) (返回值) {
函数体
}
匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数。
package main
import "fmt"
func main() {
// 将匿名函数保存到变量中
sum := func(a, b int) int {
return a + b
}
// 通过变量调用匿名函数
res := sum(10, 20)
fmt.Println("10 + 20 =", res)
// 自执行函数,匿名函数定义完直接加()执行
func(c, d int) {
fmt.Printf("%v + %v = %v\n", c, d, c+d)
}(10, 20)
}
运行结果
10 + 20 = 30
10 + 20 = 30
七、高阶函数
go语言支持函数式编程:
- 将一个函数作为另一个函数的参数
- 将一个函数作为另一个函数的返回值
1、回调函数
一个函数被作为参数传递给另一个函数,那么这个函数就叫做回调函数。
回调函数并不会马上被调用执行,它会在包含它的函数内的某个特定的时间点被“回调”(就像它的名字一样)。
package main
import "fmt"
func main() {
res := calc(10, 20, add)
fmt.Println(res)
}
// add是一个func(int, int)int类型的函数,可以作为参数传递给calc函数
func add(a, b int) int {
return a + b
}
// calc 高阶函数,它有两个int类型的参数和一个func(int, int)int函数类型的参数
// oper 回调函数,它被作为参数传递给calc函数
func calc(a, b int, oper func(int, int) int) int {
res := oper(a, b)
return res
}
运行结果
30
2、函数作为返回值
package main
import "fmt"
func main() {
fun := calc("+")
res := fun(10, 20)
fmt.Println("10 + 20 =", res)
}
func sum(a, b int) int {
return a + b
}
func sub(a, b int) int {
return a - b
}
func calc(s string) func(int, int) int {
switch s {
case "+":
return sum
case "-":
return sub
default:
fmt.Println("你传的是个啥玩意!")
return nil
}
}
运行结果
10 + 20 = 30
3、闭包
一个外层函数,有内层函数,该内层函数会操作外层函数的局部变量(外层函数的参数,或外层函数定义的变量),并且该内层函数作为外层函数的返回值。
这个内层函数和外层函数的局部变量,统称为闭包结构。
局部变量的生命周期会发生改变。正常的局部变量随着函数的调用而创建,随着函数的结束而销毁。
但是闭包结构的外层函数的局部变量并不会随着外层函数的结束而销毁,因为内层函数还要继续使用。
package main
import "fmt"
func main() {
fun := add()
res := fun()
fmt.Println("第一次调用,res=", res)
res = fun()
fmt.Println("第二次调用,res=", res)
res = fun()
fmt.Println("第二次调用,res=", res)
}
func add() func() int {
i := 0
return func() int {
i++
return i
}
}
运行结果
第一次调用,res= 1
第二次调用,res= 2
第二次调用,res= 3
七、defer语句
defer是Go语言中的延迟执行语句,用来添加函数结束时执行的代码,常用于释放某些已分配的资源、关闭数据库连接、断开socket连接、解锁一个加锁的资源。
Go语言机制担保一定会执行defer语句中的代码。
1、延迟函数
- 被延迟的函数,在离开所在的函数或方法时,执行(报错的时候也会执行
- 如果有很多defer语句,遵从“先进后出”的模式
package main
import "fmt"
func main() {
a := 1
b := 2
c := 3
d := 4
//defer a++ //a++ 是一个语句,并非函数或方法,程序报错
defer fmt.Println("defer", a)
defer fmt.Println("defer", b)
defer fmt.Println("defer", c)
defer fmt.Println("defer", d)
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
fmt.Println(d)
}
运行结果
1
2
3
4
defer 4
defer 3
defer 2
defer 1
2、延迟方法
延迟并不仅仅局限于函数。延迟一个方法调用也是完全合法的。
package main
import "fmt"
// Student 学生结构体
type Student struct {
name string
city string
}
func (s Student) hello() {
fmt.Printf("我叫%v, 我来自%v。\n", s.name, s.city)
}
func main() {
s := Student{
name: "jack",
city: "北京市",
}
defer s.hello()
fmt.Print("大家好,")
}
运行结果
大家好,我叫jack, 我来自北京市
3、延迟参数
defer声明时会先计算确定参数的值,defer推迟执行的仅是其函数体。
package main
import "fmt"
func main() {
a := 1
defer fun(a)
a++
fmt.Println("main中的a =", a)
}
func fun(a int) {
fmt.Println("fun中的a =", a)
}
运行结果
main中的a = 2
fun中的a = 1
4、defer与return
- 所有函数在执行 RET 返回指令之前,都会先检查是否存在 defer 语句,若存在则先逆序调用 defer 语句进行收尾工作再退出返回;
- 匿名返回值是在 return 执行时被声明,有名返回值则是在函数声明的同时被声明,因此在 defer 语句中只能访问有名返回值,而不能直接访问匿名返回值;
- return 其实应该包含前后两个步骤:第一步是给返回值赋值(若为有名返回值则直接赋值,若为匿名返回值则先声明再赋值);第二步是调用 RET 返回指令并传入返回值,而 RET 则会检查 defer 是否存在,若存在就先逆序插播 defer 语句,最后 RET 携带返回值退出函数;
- 因此,defer、return、返回值三者的执行顺序应该是:return最先给返回值赋值;接着 defer 开始执行一些收尾工作;最后 RET 指令携带返回值退出函数。
(1)匿名返回值的情况
package main
import "fmt"
func main() {
fmt.Println(fun1())
}
func fun1() int {
var i int
defer func() {
i++
}()
return i
}
运行结果
0
(2)有名返回值的情况
package main
import "fmt"
func main() {
fmt.Println(fun2())
}
func fun2() (i int) {
defer func() {
i++
}()
return i
}
运行结果
1
分析:
-
fun1()int
函数的返回值没有被提前声名,其值来自于其他变量的赋值,而 defer 中修改的也是其他变量(其实该 defer 根本无法直接访问到返回值),因此函数退出时返回值并没有被修改。 -
fun2()(i int)
函数的返回值被提前声名,这使得 defer 可以访问该返回值,因此在 return 赋值返回值 i 之后,defer 调用返回值 i 并进行了修改,最后致使 return 调用 RET 退出函数后的返回值才会是 defer 修改过的值。
经典案例
package main
import "fmt"
func main() {
fmt.Println(f1())
fmt.Println(f2())
fmt.Println(f3())
fmt.Println(f4())
}
func f1() int {
x := 5
defer func() {
x++ // defer 访问的是变量x,访问不到返回值
// fmt.Println("f1函数defer中的x =", x) //6
}()
return x // 返回值 = 5 //返回5
}
func f2() (x int) {
defer func() {
x++ //defer 访问x, 可以访问返回值,在RET之前,将返回值修改为6
// fmt.Println("f2函数defer中的x =", x) //6
}()
return 5 // 返回值(x) = 5 //返回6
}
func f3() (y int) {
x := 5
defer func() {
x++ // defer 访问变量x,将变量x修改为6
// fmt.Println("f3函数defer中的x =", x) //6
}()
return x // 返回值(y) = 5 //返回5
}
func f4() (x int) {
defer func(x int) {
x++ // 这里修改的defer时传入的x(0),将其修改为1
// fmt.Println("f4函数defer中的x =", x) //1
}(x) // defer 语句调用时传入x的值为int类型的默认值0
return 5 // 返回值(x) = 5 //返回5
}
运行结果
5
6
5
5
5、defer的作用域
- defer 只对当前协程有效(main 可以看作是主协程);
- 当任意一条(主)协程发生 panic 时,会执行当前协程中 panic 之前已声明的 defer;
- 在发生 panic 的(主)协程中,如果没有一个 defer 调用 recover()进行恢复,则会在执行完最后一个已声明的 defer 后,引发整个进程崩溃;
- 主动调用 os.Exit(int) 退出进程时,defer 将不再被执行。
package main
import (
"fmt"
// "os"
)
func main() {
fmt.Println("start")
// panic("崩溃了") // defer和之后的语句都不再执行
// os.Exit(1) // defer和之后的语句都不再执行
defer fmt.Println("defer")
// go func() {
// panic("崩溃了")
// }() // defer不被执行
// panic("崩溃了") // defer会执行,但后面的语句不再执行
fmt.Println("over")
// os.Exit(1) // defer不被执行
}
八、内置函数
内置函数 | 介绍 |
---|---|
close | 主要用来关闭channel |
len | 用来求长度,比如string、array、slice、map、channel |
new | 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针 |
make | 用来分配内存,主要用来分配引用类型,比如chan、map、slice |
append | 用来追加元素到数组、slice中 |
panic和recover | 用来做错误处理 |
panic和recover
Go语言中目前是没有异常机制,但是使用panic/recover模式来处理错误。 panic可以在任何地方引发,但recover只有在defer调用的函数中有效。
package main
import (
"fmt"
)
func main() {
fmt.Println("start")
defer func() {
err := recover()
if err != nil {
fmt.Println("recover")
fmt.Println("活了")
}
}()
panic("panic")
fmt.Println("over")
}
运行结果
start
recover
活了