Go语言基础 – 函数、匿名函数、闭包、递归与内置函数

  • A+
所属分类:go语言编程 学编程

一、说明

函数是组织好的、可重复使用的、用于执行指定任务的代码块。

Go语言中支持函数、匿名函数和闭包

1.1、Go语言中函数的特点

1.不支持重裁(即一个包里不能有两个名字一样的函数)

2.函数也是一种类型,一个函数可以赋值给变量

3.函数支持多返回值

4.支持匿名函数

1.2、Go函数的传值说明

1.无论是值传递还是引用传递,传递给函数的都是变量的副本

2.值传递是值拷贝,引用是地址拷贝,地址拷贝更高效,值拷贝取决于拷贝对象大小(对象越大,拷贝性能越低)

3.map、slice、chan、指针、interface默认以引用的方式传递,其他的都是值传递

二、函数使用

2.1、函数基本定义与调用

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

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

说明:

1.函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。

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

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

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

下面是一个求和的函数示例:

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

函数的参数和返回值都是可选的,例如我们可以实现一个既不需要参数也没有返回值的函数:

func testDemo() {
    fmt.Println("Hello World !")
}

函数调用
定义了函数之后,我们可以通过函数名()的方式调用函数。 例如我们调用上面定义的两个函数,代码如下:

func main() {
    fmt.Println(add(10, 20))    // 结果为:30
    testDemo()    // 结果为:Hello World !
}

2.2、可变参数

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

注意:可变参数通常要作为函数的最后一个参数。

举个例子:

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

调用上面的函数:

ret1 := intSum2()   // 结果:[]
ret2 := intSum2(10)   // 结果:[10]
fmt.Println(ret1, ret2)   // 结果:0 10

固定参数搭配可变参数使用时,可变参数要放在固定参数的后面,示例代码如下:

func intSum3(x int, y ...int) int {
    fmt.Println(x, y)
    sum := x
    for _, v := range y {
        sum = sum + v
    }
    return sum
}

调用上述函数:

ret5 := intSum3(100)
ret6 := intSum3(100, 10)
ret7 := intSum3(100, 10, 20)
ret8 := intSum3(100, 10, 20, 30)
fmt.Println(ret5, ret6, ret7, ret8) //100 110 130 160

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

2.3、返回值

Go语言中通过return关键字向外输出返回值。

多返回值
Go语言中函数支持多返回值,函数如果有多个返回值时必须用()将所有返回值包裹起来。

举个例子:

func calc(x, y int) (int, int) {
    sum := x + y
    sub := x - y
    return sum, sub
}

返回值命名
函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回。

例如:

func calc(x, y int) (sum, sub int) {
    sum = x + y
    sub = x - y
    return    // 命名返回值后 return后面就不用加内容了
}

2.4、变量作用域

全局变量
全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。 在函数中可以访问到全局变量。

局部变量
局部变量又分为两种: 函数内定义的变量无法在该函数外使用,

如果局部变量和全局变量重名,优先访问局部变量。

2.5、自定义函数参数类型与函数赋值操作

我们可以使用type关键字来定义一个函数类型,具体使用方法如下:

type op_func func(int, int) int      // 定义一个类型(类型名为:op_func)类型为:func(参数1, 参数2) 返回值。有两个参数和一个返回值的类型
func add(a,b int) int {    // 定义add函数(这个函数和刚定义的参数类型的参数和返回值要一直)
    return a + b
}
func operation(op op_func, a, b int) int {      //使用刚自己定义的类型,op接收c变量(实际就是add函数),a,b接收100和200参数 
    return op(a,b)        // 实质是执行add函数,返回值为:参数a+b的结果
}

operation函数调用如下:

c := add     // 将add函数赋值给变量c
ret3 := operation(c, 100, 200)    // 调用operation变量,将变量c和100、200传给operation函数(此时执行operation函数)
fmt.Println(ret3)      // 结果为:300

三、匿名函数

函数当然还可以作为返回值,但是在Go语言中函数内部不能再像之前那样定义函数了,只能定义匿名函数。匿名函数就是没有函数名的函数,匿名函数的定义格式如下:

func(参数)(返回值){
    函数体
}()        // 函数体后面加()可以直接调用匿名函数

实名函数的示例:

// 1.在函数里定义匿名函数
func test1(a, b int) int {
    result := func(a1 int, b1 int) int {
        return a1 + b1
    }
    return result(a, b)        // 匿名函数的两种调用方法:第一种:再函数的大括号后面直接加小括号实现调用。第二种:通过变量加小括号实现调用
}

// 2.在外部直接定义匿名函数
var (
    result := func(a1 int, b1 int) int {
        return a1 + b1
    }(10, 20)    // 加()直接实现调用该匿名函数
)

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

四、闭包

闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境。 首先我们来看一个例子:

func adder() func(int) int {
    var x int    // 第一次调用x值为:0。第二次调用x值为10
    return func(y int) int {    // 第一次调用时y值为10.第二次调用y值为20
        x += y    // 第一次调用x+y=10.第二次调用x+y=30
        return x   //第一次返回结果为:10。第二次返回结果为:30
    }
}
func main() {
    var f = adder()
    fmt.Println(f(10))   //第一次调用,结果为:10
    fmt.Println(f(20))   //第二次调用,在上次调用得到的结果的基础上操作,结果为:30
    fmt.Println(f(30))   // 第三次调用,在第二次调用得到结果的基础上操作,结果为:60
}

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

func adder2(x int) func(int) int {   // 变量赋值给f时x的值为10,第二次调用时x的值为20.
    return func(y int) int {    // 第一次调用y的值为10。第二次调用y的值为:20
        x += y    // 第一次调用:x+y=20。第二次调用x+y=40
        return x
    }
}
func main() {
    var f = adder2(10)
    fmt.Println(f(10))    //20
    fmt.Println(f(20))    //40
    fmt.Println(f(30))    //70

闭包进阶示例2:

func makeSuffixFunc(suffix string) func(string) string {
    return func(name string) string {
        if !strings.HasSuffix(name, suffix) {    // 判断name不是以suffix结尾
            return name + suffix    // 就执行这里
        }
        return name
    }
}

func main() {
    jpgFunc := makeSuffixFunc(".jpg")    // .jpg结尾
    txtFunc := makeSuffixFunc(".txt")   // .txt结尾
    fmt.Println(jpgFunc("test"))    //结果为:test.jpg
    fmt.Println(txtFunc("test"))     //结果为: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))    //结果为:11 9   (第一次:10+1=11、11-2=9)
    fmt.Println(f1(3), f2(4))    //结果为:12 8    (第二次:9+3=12、12-4=8)
    fmt.Println(f1(5), f2(6)) //结果为:13 7       (第三次:8+5=13、13-6=7)
}

五、defer语句

5.1、defer说明

1.当函数执行到最后或返回值时会执行defer语句,因此可以用来做资源清理

2.多个defer语句,按先进后出的方式执行

3.defer语句中的变量在defer声明时就决定了(后面的修改不会影响defer的值)

4.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语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。

5.2、defer的用途

1.关闭文件句柄(防止文件泄露),示例:

    func read() {
        file := open(filename)
        defer file.Close()
    }

2.锁资源释放(如果锁资源忘记释放就会变成死锁)

    func lock() {
        mc.Lock()
        defer mc.Unlock()
        ...
    }

3.数据库连接的释放等

六、递归

函数自己调用自己,但是一定要有个结束的条件,否则递归到一定深度(每个语言的深度不一样)会因为资源问题报错。

示例如下:

func recusive(n int) {
    fmt.Println("Hello", n)
    time.Sleep(time.Second*2)
    if n >= 10 {
        return
    }
    recusive(n + 1)
}

以上递归的调用:

func main() {
    recusive(0)
}

递归实现斐波那契数实现(0和1都是1,剩下的每项是前两项的合)

func fab(n int) int {
    if n <= 1 {
        return 1
    }
    return fab(n-1) + fab(n-2)
}

执行上面的函数:

func main() {
    for i := 0; i < 10; i ++ {
        fmt.Println(fab(i))
    }
}

七、内置函数

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

7.1、内置new函数使用

func newDemo() {
    var i int
    fmt.Println(i)    // 结果为:0

    j :=new(int)
    fmt.Println(j," - ",*j)    // 结果为:0xc00005a088  -  0(new得到的是一个地址)
    *j = 100       // 修改值
    fmt.Println(*j)     // 结果为:100
}

7.2、内置new函数和make函数的区别

1.make用来创建map、slice、channel

2.new用来创建值类型

func newMake() {
    s1 := new([]int)
    fmt.Println("in s1 - ",s1)      // 结果为:in s1 -  &[] (返回的是地址,一个空的slice)

    s2 := make([]int, 10)
    fmt.Println("in s2 - ",s2)      // 结果为:in s2 -  [0 0 0 0 0 0 0 0 0 0](返回的不是地址,就是一个slice)

    *s1 = make([]int,5)             // 通过make分配new的指针的空间
    (*s1)[0] = 100                  // 修改指针s1的值
    fmt.Println(".. s1 - ",s1)      // 结果为:.. s1 -  &[100 0 0 0 0] (分配了5个空间)
    return
}

7.3、内置append函数使用

func appendDemo() {
    var a []int
    a = append(a, 10, 20, 30)   // 将10,20,30追加到a里
    a = append(a, a...)      // a... :表示将a数组展开(即得到数组里的内容)
    fmt.Println(a)    // 结果为:[10 20 30 10 20 30]
}

7.4、使用内置panic和recover函数做错误处理

1.没做处理的报错的示例

func funcA() {
    fmt.Println("func A")
}

func funcB() {
    panic("panic in B")
}

func funcC() {
    fmt.Println("func C")
}
func main() {
    funcA()
    funcB()
    funcC()
}

输出:

func A
panic: panic in B

goroutine 1 [running]:
main.funcB(...)
        .../code/func/main.go:12
main.main()
        .../code/func/main.go:20 +0x98

程序运行期间funcB中引发了panic导致程序崩溃,异常退出了。这个时候我们就可以通过recover将程序恢复回来,继续往后执行。

func funcA() {
    fmt.Println("func A")
}

func funcB() {
    defer func() {
        err := recover()
        //如果程序出出现了panic错误,可以通过recover恢复过来
        if err != nil {
            fmt.Println("recover in B")    // 打印异常
        }
    }()
    panic("panic in B")    // panic抛出异常
}

func funcC() {
    fmt.Println("func C")
}
func main() {
    funcA()
    funcB()
    funcC()
}

注意:
1.recover()必须搭配defer使用。
2.defer一定要在可能引发panic的语句之前定义。

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: