Go 中函数特性简介
对 Go 中的函数特性做一个总结. 懂则看, 不懂则算.
Go 中有 3 种函数: 普通函数, 匿名函数(没有名称的函数), 方法(定义在 struct 上的函数).
Go 编译时不在乎函数的定义位置, 但建议 init()定义在最前面 (如果有的话),main 函数定义在 init() 之后, 然后再根据函数名的字母顺序或者根据调用顺序放置各函数的位置.
函数的参数, 返回值以及它们的类型, 结合起来成为函数的签名(signature).
函数调用的时候, 如果有参数传递给函数, 则先拷贝参数的副本, 再将副本传递给函数.
由于引用类型 (slice,map,interface,channel) 自身就是指针, 所以这些类型的值拷贝给函数参数, 函数内部的参数仍然指向它们的底层数据结构.
函数参数可以没有名称, 例如
- func myfunc(int,int)
- .
Go 中的函数可以作为一种 type 类型, 例如
- type myfunc func(int,int) int
- .
实际上, 在 Go 中, 函数本身就是一种类型, 它的 signature 就是所谓的 type, 例如 func(int,int) int. 所以, 当函数 ab()赋值给一个变量 ref_ab 时 ref_ab := ab, 不能再将其它函数类型的函数 cd()赋值给变量 ref_ab.
Go 中作用域是词法作用域, 意味着函数的定义位置决定了它能看见的变量.
Go 中不允许函数重载(overload), 也就是说不允许函数同名.
Go 中的函数不能嵌套函数, 但可以嵌套匿名函数.
Go 实现了一级函数(first-class functions),Go 中的函数是高阶函数(high-order functions). 这意味着:
函数是一个值, 可以将函数赋值给变量, 使得这个变量也成为函数
函数可以作为参数传递给另一个函数
函数的返回值可以是一个函数
这些特性使得函数变得无比的灵活, 例如回调函数, 闭包等等功能都依赖于这些特性.
Go 中的函数不支持泛型(目前不支持), 但如果需要泛型的情况, 大多数时候都可以通过接口, type switch,reflection 的方式来解决. 但使用这些技术使得代码变得更复杂, 性能更低.
参数和返回值
函数可以有 0 或多个参数, 0 或多个返回值, 参数和返回值都需要指定数据类型, 返回值通过 return 关键字来指定.
return 可以有参数, 也可以没有参数, 这些返回值可以有名称, 也可以没有名称. Go 中的函数可以有多个返回值.
(1). 当返回值有多个时, 这些返回值必须使用括号包围, 逗号分隔
(2).return 关键字中指定了参数时, 返回值可以不用名称. 如果 return 省略参数, 则返回值部分必须带名称
(3). 当返回值有名称时, 必须使用括号包围, 逗号分隔, 即使只有一个返回值
(4). 但即使返回值命名了, return 中也可以强制指定其它返回值的名称, 也就是说 return 的优先级更高
(5). 命名的返回值是预先声明好的, 在函数内部可以直接使用, 无需再次声明. 命名返回值的名称不能和函数参数名称相同, 否则报错提示变量重复定义
(6).return 中可以有表达式, 但不能出现赋值表达式, 这和其它语言可能有所不同. 例如 return a+b 是正确的, 但 return c=a+b 是错误的
例如:
- // 单个返回值
- func func_a() int{
- return a
- }
- // 只要命名了返回值, 必须括号包围
- func func_b() (a int){
- // 变量 a int 已存在, 无需再次声明
- a = 10
- return
- // 等价于: return a
- }
- // 多个返回值, 且在 return 中指定返回的内容
- func func_c() (int,int){
- return a,b
- }
- // 多个返回值
- func func_d() (a,b int){
- return
- // 等价于: return a,b
- }
- // return 覆盖命名返回值
- func func_e() (a,b int){
- return x,y
- }
Go 中经常会使用其中一个返回值作为函数是否执行成功, 是否有错误信息的判断条件. 例如 return value,exists,return value,ok,return value,err 等.
当函数的返回值过多时, 例如有 4 个以上的返回值, 应该将这些返回值收集到容器中, 然后以返回容器的方式去返回. 例如, 同类型的返回值可以放进 slice 中, 不同类型的返回值可以放进 map 中.
但函数有多个返回值时, 如果其中某个或某几个返回值不想使用, 可以通过下划线_这个 blank identifier 来丢弃这些返回值. 例如下面的 func_a 函数两个返回值, 调用该函数时, 丢弃了第二个返回值 b, 只保留了第一个返回值 a 赋值给了变量 a.
- func func_a() (a,b int){
- return
- }
- func main() {
- a,_ := func_a()
- }
按值传参
Go 中是通过传值的方式传参的, 意味着传递给函数的是拷贝后的副本, 所以函数内部访问, 修改的也是这个副本.
例如:
- a,b := 10,20
- min(a,b)
- func min(x,y int) int{
- }
上面调用 min()时, 是将 a 和 b 的值拷贝一份, 然后将拷贝的副本赋值给变量 x,y 的, 所以 min()函数内部, 访问, 修改的一直是 a,b 的副本, 和原始的数据对象 a,b 没有任何关系.
如果想要修改外部数据(即上面的 a,b), 需要传递指针.
例如, 下面两个函数, func_value()是传值函数, func_ptr()是传指针函数, 它们都修改同一个变量的值.
- package main
- import "fmt"
- func main() {
- a := 10
- func_value(a)
- fmt.Println(a) // 输出的值仍然是 10
- b := &a
- func_ptr(b)
- fmt.Println(*b) // 输出修改后的值: 11
- }
- func func_value(x int) int{
- x = x + 1
- return x
- }
- func func_ptr(x *int) int{
- *x = *x + 1
- return *x
- }
map,slice,interface,channel 这些数据类型本身就是指针类型的, 所以就算是拷贝传值也是拷贝的指针, 拷贝后的参数仍然指向底层数据结构, 所以修改它们可能会影响外部数据结构的值.
另外注意, 赋值操作 b = a+1 这种类型的赋值也是拷贝赋值. 换句话说, 现在底层已经有两个数据对象, 一个是 a, 一个是 b. 但 a = a+1 这种类型的赋值虽然本质上是拷贝赋值, 但因为 a 的指针指向特性, 使得结果上看是原地修改数据对象而非生成新数据对象.
变长参数 "..."(variadic)
有时候参数过多, 或者想要让函数处理任意多个的参数, 可以在函数定义语句的参数部分使用 ARGS...TYPE 的方式. 这时会将... 代表的参数全部保存到一个名为 ARGS 的 slice 中, 注意这些参数的数据类型都是 TYPE.
... 在 Go 中称为 variadic, 在使用... 的时候(如传递, 赋值), 可以将它看作是一个 slice, 下面的几个例子可以说明它的用法.
例如: func myfunc(a,b int,args...int) int {}. 除了前两个参数 a 和 b 外, 其它的参数全都保存到名为 args 的 slice 中, 且这些参数全都是 int 类型. 所以, 在函数内部就已经有了一个 args = []int{....}的数据结构.
例如, 下面的例子中, min()函数要从所有参数中找出最小的值. 为了实验效果, 特地将前两个参数 a 和 b 独立到 slice 的外面. min()函数内部同时会输出保存到 args 中的参数值.
- package main
- import "fmt"
- func main() {
- a,b,c,d,e,f := 10,20,30,40,50,60
- fmt.Println(min(a,b,c,d,e,f))
- }
- func min(a,b int,args...int) int{
- // 输出 args 中保存的参数
- // 等价于 args := []int{30,40,50,60}
- for index,value := range args {
- fmt.Printf("%s%d%s %d\n","args[",index,"]:",value)
- }
- // 取出 a,b 中较小者
- min_value := a
- if a>b {
- min_value = b
- }
- // 取出所有参数中最小值
- for _,value := range args{
- if min_value> value {
- min_value = value
- }
- }
- return min_value
- }
但上面代码中调用函数时传递参数的方式显然比较笨重. 如果要传递的参数过多 (要比较的值很多), 可以先将这些参数保存到一个 slice 中, 再传递 slice 给 min() 函数. 传递 slice 给函数的时候, 使用 SLICE... 的方式即可.
- func main() {
- s1 := []int{30,40,50,60,70}
- fmt.Println(min(10,20,s1...))
- }
上面的赋值方式已经能说明能使用 slice 来理解... 的行为. 另外, 下面的例子也能很好的解释:
- func f1(s...string){
- f2(s...)
- f3(s)
- }
- func f2(s...string){}
- func f3(s []string){}
如果各参数的类型不同, 又想定义成变长参数, 该如何? 第一种方式, 可以使用 struct, 第二种方式可以使用接口. 接口暂且不说, 如果使用 struct, 大概如下:
- type args struct {
- arg1 string
- arg2 int
- arg3 type3
- }
然后可以将 args 传递给函数: f(a,b int,args{}), 如果 args 结构中需要初始化, 则 f(a,b int,args{arg1:"hello",arg2:22}).
defer 关键字
defer 关键字可以让函数或语句延迟到函数语句块的最结尾时, 即即将退出函数时执行, 即便函数中途报错结束, 即便已经 panic(), 即便函数已经 return 了, 也都会执行 defer 所推迟的对象.
例如:
- func main() {
- a()
- }
- func a() {
- println("in a")
- defer b()
- println("leaving a")
- // 到了这里才会执行 b()
- }
- func b() {
- println("in b")
- println("leaving b")
- }
上面将输出:
- in a
- leaving a
- in b
- leaving b
即便是函数已经报错, 或函数已经 return 返回, defer 的对象也会在函数退出前的最后一刻执行.
- func a() TYPE{
- ...CODE...
- defer b()
- ...CODE...
- // 函数执行出了错误
- return args
- // 函数 b()都会在这里执行
- }
但注意, 由于 Go 的作用域采用的是词法作用域, defer 的定义位置决定了它推迟对象能看见的变量值, 而不是推迟对象被调用时所能看见的值.
例如:
- package main
- var x = 10
- func main() {
- a()
- }
- func a() {
- println("start a:",x) // 输出 10
- x = 20
- defer b(x)
- x = 30
- println("leaving a:",x) // 输出 30
- // 调用 defer 延迟的对象 b(), 输出 20
- }
- func b(x int) {
- println("start b:",x)
- }
比较下面的 defer:
- package main
- var x = 10
- func main() {
- a()
- }
- func a() int {
- println("start a:", x) // 输出 10
- x = 20
- defer func() {
- println("in defer:", x) // 输出 30
- }()
- x = 30
- println("leaving a:", x) // 输出 30
- return x
- }
上面 defer 推迟的匿名函数输出的值是 30, 它看见的不应该是 20 吗? 先再改成下面的:
- package main
- var x = 10
- func main() {
- a()
- }
- func a() int {
- println("start a:", x) // 输出 10
- x = 20
- defer func(x int) {
- println("in defer:", x) // 输出 20
- }(x)
- x = 30
- println("leaving a:", x) // 输出 30
- return x
- }
这个 defer 推迟的对象中看见的却是 20, 这和第一种 defer b(x)是相同的.
原因在于 defer 推迟的如果是函数, 它直接就在它的定义位置处评估好参数, 变量. 该拷贝传值的的拷贝传值, 该指针相见的指针相见. 所以, 对于第 (1) 和第 (3) 种情况, 在 defer 的定义位置处, 就将 x=20 拷贝给了推迟的函数参数, 所以函数内部操作的一直是 x 的副本. 而第二种情况则是直接指向它所看见的 x=20 那个变量, 则个变量是全局变量, 当执行 x=30 的时候会将其值修改, 到执行 defer 推迟的对象时, 它指向的 x 的值已经是修改过的.
再看下面这个例子, 将 defer 放进一个语句块中, 并在这个语句块中新声明一个同名变量 x:
- func a() int {
- println("start a:", x) // 输出 10
- x = 20
- {
- x := 40
- defer func() {
- println("in defer:", x) // 输出 40
- }()
- }
- x = 30
- println("leaving a:", x) // 输出 30
- return x
- }
上面的 defer 定义在语句块中, 它能看见的 x 是语句块中 x=40, 它的 x 指向的是语句块中的 x. 另一方面, 当语句块结束时, x=40 的 x 会消失, 但由于 defer 的函数中仍有 x 指向 40 这个值, 所以 40 这个值仍被 defer 的函数引用着, 它直到 defer 执行完之后才会被 GC 回收. 所以 defer 的函数在执行的时候, 仍然会输出 40.
如果语句块内有多个 defer, 则 defer 的对象以 LIFO(last in first out)的方式执行, 也就是说, 先定义的 defer 后执行.
- func main() {
- println("start...")
- defer println("1")
- defer println("2")
- defer println("3")
- defer println("4")
- println("end...")
- }
将输出:
- start...
- end...
- 4
- 3
- 2
- 1
defer 有什么用呢? 一般用来做善后操作, 例如清理垃圾, 释放资源, 无论是否报错都执行 defer 对象. 另一方面, defer 可以让这些善后操作的语句和开始语句放在一起, 无论在可读性上还是安全性上都很有改善, 毕竟写完开始语句就可以直接写 defer 语句, 永远也不会忘记关闭, 善后等操作.
例如, 打开文件, 关闭文件的操作写在一起:
- open()
- defer file.Close()
... 操作文件 ...
以下是 defer 的一些常用场景:
打开关闭文件
锁定, 释放锁
建立连接, 释放连接
作为结尾输出结尾信息
清理垃圾(如临时文件)
panic()和 recover()
panic()用于产生错误信息并终止当前的 goroutine, 一般将其看作是退出 panic()所在函数以及退出调用 panic()所在函数的函数. 例如, G()中调用 F(),F()中调用 panic(), 则 F()退出, G()也退出.
注意, defer 关键字推迟的对象是函数最后调用的, 即使出现了 panic 也会调用 defer 推迟的对象.
例如, 下面的代码中, main()中输出一个 start main 之后调用 a(), 它会输出 start a, 然后就 panic 了, panic()会输出 panic: panic in a, 然后报错, 终止程序.
- func main() {
- println("start main")
- a()
- println("end main")
- }
- func a() {
- println("start a")
- panic("panic in a")
- println("end a")
- }
执行结果如下:
- start main
- start a
- panic: panic in a
- goroutine 1 [running]:
- main.a()
- E:/learning/err.go:14 +0x63
- main.main()
- E:/learning/err.go:8 +0x4c
- exit status 2
注意上面的 end a 和 end main 都没有被输出.
可以使用 recover()去捕获 panic()并恢复执行. recover()用于捕捉 panic()错误, 并返回这个错误信息. 但注意, 即使 recover()捕获到了 panic(), 但调用含有 panic()函数的函数 (即上面的 G() 函数)也会退出, 所以如果 recover()定义在 G()中, 则 G()中调用 F()函数之后的代码都不会执行(见下面的通用格式).
以下是比较通用的 panic()和 recover()的格式:
- func main() {
- G()
- // 下面的代码会执行
- ...CODE IN MAIN...
- }
- func G(){
- defer func (){
- if str := recover(); str != nil {
- fmt.Println(str)
- }
- }()
- ...CODE IN G()...
- // F()的调用必须在 defer 关键字之后
- F()
- // 该函数内下面的代码不会执行
- ...CODE IN G()...
- }
- func F() {
- ...CODE1...
- panic("error found")
- // 下面的代码不会执行
- ...CODE IN F()...
- }
可以使用 recover()去捕获 panic()并恢复执行. 但以下代码是错误的:
- func main() {
- println("start main")
- a()
- println("end main")
- }
- func a() {
- println("start a")
- panic("panic in a")
- // 直接放在 panic 后是错误的
- panic_str := recover()
- println(panic_str)
- println("end a")
- }
之所以错误, 是因为 panic()一出现就直接退出函数 a()和 main()了. 要想 recover()真正捕获 panic(), 需要将 recover()放在 defer 的推迟对象中, 且 defer 的定义必须在 panic()发生之前.
例如, 下面是通用格式的示例:
- package main
- import "fmt"
- func main() {
- println("start main")
- b()
- println("end main")
- }
- func a() {
- println("start a")
- panic("panic in a")
- println("end a")
- }
- func b() {
- println("start b")
- defer func() {
- if str := recover(); str != nil {
- fmt.Println(str)
- }
- }()
- a()
- println("end b")
- }
以下是输出结果:
- start main
- start b
- start a
- panic in a
- end main
注意上面的 end b,end a 都没有被输出, 但是 end main 输出了.
panic()是内置的函数 (在包 builtin 中), 在 log 包中也有一个 Panic() 函数, 它调用 Print()输出信息后, 再调用 panic().go doc log Panic 一看便知:
- $ go doc log Panic
- func Panic(v ...interface{})
- Panic is equivalent to Print() followed by a call to panic().
内置函数
在 builtin 包中有一些内置函数, 这些内置函数额外的导入包就能使用.
有以下内置函数:
- $ go doc builtin | grep func
- func close(c chan<- Type)
- func delete(m map[Type]Type1, key Type)
- func panic(v interface{})
- func print(args ...Type)
- func println(args ...Type)
- func recover() interface{}
- func complex(r, i FloatType) ComplexType
- func imag(c ComplexType) FloatType
- func real(c ComplexType) FloatType
- func append(slice []Type, elems ...Type) []Type
- func make(t Type, size ...IntegerType) Type
- func new(Type) *Type
- func cap(v Type) int
- func copy(dst, src []Type) int
- func len(v Type) int
close 用于关闭 channel
delete 用于删除 map 中的元素
copy 用于拷贝 slice
append 用于追加 slice
cap 用于获取 slice 的容量
len 用于获取
slice 的长度
map 的元素个数
array 的元素个数
指向 array 的指针时, 获取 array 的长度
string 的字节数
channel 的 channel buffer 中的未读队列长度
print 和 println: 底层的输出函数, 用来调试用. 在实际程序中, 应该使用 fmt 中的 print 类函数
complex,imag,real: 操作复数(虚数)
panic 和 recover: 处理错误
new 和 make: 分配内存并初始化
new 适用于为值类 (value type) 的数据类型 (如 array,int 等) 和 struct 类型的对象分配内存并初始化, 并返回它们的地址给变量. 如 v := new(int)
make 适用于为内置的引用类的类型 (如 slice,map,channel 等) 分配内存并初始化底层数据结构, 并返回它们的指针给变量, 同时可能会做一些额外的操作
注意, 地址和指针是不同的. 地址就是数据对象在内存中的地址, 指针则是占用一个机器字长 (32 位机器是 4 字节, 64 位机器是 8 字节) 的数据, 这个数据中存储的是它所指向数据对象的地址.
- a -> AAAA
- b -> Pointer -> BBBB
递归函数
函数内部调用函数自身的函数称为递归函数.
使用递归函数最重要的三点:
必须先定义函数的退出条件, 退出条件基本上都使用退出点来定义, 退出点常常也称为递归的基点, 是递归函数的最后一次递归点, 或者说没有东西可递归时就是退出点.
递归函数很可能会产生一大堆的 goroutine(其它编程语言则是出现一大堆的线程, 进程), 也很可能会出现栈空间内存溢出问题. 在其它编程语言可能只能设置最大递归深度或改写递归函数来解决这个问题, 在 Go 中可以使用 channel+goroutine 设计的 "lazy evaluation" 来解决.
递归函数通常可以使用 level 级数的方式进行改写, 使其不再是递归函数, 这样就不会有第 2 点的问题.
例如, 递归最常见的示例, 求一个给定整数的阶乘. 因为阶乘的公式为 n*(n-1)*...*3*2*1, 它在参数为 1 的时候退出函数, 也就是说它的递归基点是 1, 所以对是否为基点进行判断, 然后再写递归表达式.
- package main
- import "fmt"
- func main() {
- fmt.Println(a(5))
- }
- func a(n int) int{
- // 判断退出点
- if n == 1 {
- return 1
- }
- // 递归表达式
- return n * a(n-1)
- }
它的调用过程大概是这样的:
再比如斐波那契数列, 它的计算公式为 f(n)=f(n-1)+f(n-2)且 f(2)=f(1)=1. 它在参数为 1 和 2 的时候退出函数, 所以它的退出点为 1 和 2.
- package main
- import "fmt"
- func main() {
- fmt.Println(f(3))
- }
- func f(n int) int{
- // 退出点判断
- if n == 1 || n == 2 {
- return 1
- }
- // 递归表达式
- return f(n-1)+f(n-2)
- }
如何递归一个目录? 它的递归基点是文件, 只要是文件就返回, 只要是目录就进入. 所以, 伪代码如下:
- func recur(dir FILE) FILE{
- // 退出点判断
- if (dir is a file){
- return dir
- }
- // 当前目录的文件列表
- file_slice := filelist()
- // 遍历所有文件
- for _,file := range file_slice {
- return recur(file)
- }
- }
匿名函数
匿名函数是没有名称的函数. 一般匿名函数嵌套在函数内部, 或者赋值给一个变量, 或者作为一个表达式.
定义的方式:
- // 声明匿名函数
- func(args){
- ...CODE...
- }
- // 声明匿名函数并直接执行
- func(args){
- ...CODE...
- }(parameters)
下面的示例中, 先定义了匿名函数, 将其赋值给了一个变量, 然后在需要的地方再去调用执行它.
- package main
- import "fmt"
- func main() {
- // 匿名函数赋值给变量
- a := func() {
- fmt.Println("hello world")
- }
- // 调用匿名函数
- a()
- fmt.Printf("%T\n", a) // a 的 type 类型: func()
- fmt.Println(a) // 函数的地址
- }
如果给匿名函数的定义语句后面加上(), 表示声明这个匿名函数的同时并执行:
- func main() {
- msg := "Hello World"
- func(m string) {
- fmt.Println(m)
- }(msg)
- }
其中 func(c string)表示匿名函数的参数, func(m string){}(msg)的 msg 表示传递 msg 变量给匿名函数, 并执行.
func type
可以将 func 作为一种 type, 以后可以直接使用这个 type 来定义函数.
- package main
- import "fmt"
- type add func(a,b int) int
- func main() {
- var a add = func(a,b int) int{
- return a+b
- }
- s := a(3,5)
- fmt.Println(s)
- }
来源: https://www.cnblogs.com/f-ck-need-u/p/9876203.html