Go中函数特性简介

对Go中的函数特性做一个总结。懂则看,不懂则算。

  1. Go中有3种函数:普通函数、匿名函数(没有名称的函数)、方法(定义在struct上的函数)。
  2. Go编译时不在乎函数的定义位置,但建议init()定义在最前面(如果有的话),main函数定义在init()之后,然后再根据函数名的字母顺序或者根据调用顺序放置各函数的位置。
  3. 函数的参数、返回值以及它们的类型,结合起来成为函数的签名(signature)。
  4. 函数调用的时候,如果有参数传递给函数,则先拷贝参数的副本,再将副本传递给函数。
    • 由于引用类型(slice、map、interface、channel)自身就是指针,所以这些类型的值拷贝给函数参数,函数内部的参数仍然指向它们的底层数据结构。
  5. 函数参数可以没有名称,例如func myfunc(int,int)
  6. Go中的函数可以作为一种type类型,例如type myfunc func(int,int) int
    • 实际上,在Go中,函数本身就是一种类型,它的signature就是所谓的type,例如func(int,int) int。所以,当函数ab()赋值给一个变量ref_abref_ab := ab,不能再将其它函数类型的函数cd()赋值给变量ref_ab
  7. Go中作用域是词法作用域,意味着函数的定义位置决定了它能看见的变量。
  8. Go中不允许函数重载(overload),也就是说不允许函数同名。
  9. Go中的函数不能嵌套函数,但可以嵌套匿名函数。
  10. Go实现了一级函数(first-class functions),Go中的函数是高阶函数(high-order functions)。这意味着:
    • 函数是一个值,可以将函数赋值给变量,使得这个变量也成为函数
    • 函数可以作为参数传递给另一个函数
    • 函数的返回值可以是一个函数
    • 这些特性使得函数变得无比的灵活,例如回调函数、闭包等等功能都依赖于这些特性。
  11. 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,existsreturn value,okreturn 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,则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 aend 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 bend 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中的未读队列长度
  • printprintln:底层的输出函数,用来调试用。在实际程序中,应该使用fmt中的print类函数
  • compleximagreal:操作复数(虚数)
  • panicrecover:处理错误
  • newmake:分配内存并初始化

    • new适用于为值类(value type)的数据类型(如array,int等)和struct类型的对象分配内存并初始化,并返回它们的地址给变量。如v := new(int)
    • make适用于为内置的引用类的类型(如slice、map、channel等)分配内存并初始化底层数据结构,并返回它们的指针给变量,同时可能会做一些额外的操作

注意,地址和指针是不同的。地址就是数据对象在内存中的地址,指针则是占用一个机器字长(32位机器是4字节,64位机器是8字节)的数据,这个数据中存储的是它所指向数据对象的地址。

a -> AAAA
b -> Pointer -> BBBB

递归函数

函数内部调用函数自身的函数称为递归函数。

使用递归函数最重要的三点:

  1. 必须先定义函数的退出条件,退出条件基本上都使用退出点来定义,退出点常常也称为递归的基点,是递归函数的最后一次递归点,或者说没有东西可递归时就是退出点。
  2. 递归函数很可能会产生一大堆的goroutine(其它编程语言则是出现一大堆的线程、进程),也很可能会出现栈空间内存溢出问题。在其它编程语言可能只能设置最大递归深度或改写递归函数来解决这个问题,在Go中可以使用channel+goroutine设计的”lazy evaluation”来解决。
  3. 递归函数通常可以使用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)
}

版权声明:本文为f-ck-need-u原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/f-ck-need-u/p/9876203.html