• Go语言学习笔记—golang函数


    视频来源:B站《golang入门到项目实战 [2022最新Go语言教程,没有废话,纯干货!]》

    文章为自己整理的学习笔记,侵权即删,谢谢支持!


    一 golang 函数简介

    函数是go语言中的一级公民,我们把所有的功能单元都定义在函数中,可以重复使用。

    函数包含函数的名称、参数列表和返回值类型,这些构成了函数的签名(signature)。

    1.1 函数特性

    1. go语言中有3种函数:普通函数、匿名函数(没有名称的函数)、方法(定义在struct上的函数)。
    2. go语言中不允许函数重载(overload),也就是说不允许函数同名。
    3. go语言中的函数不能嵌套函数,但可以嵌套匿名函数。
    4. 函数是一个值,可以将函数赋值给变量,使得这个变量也成为函数。
    5. 函数可以作为参数传递给另一个函数。
    6. 函数的返回值可以是一个函数。
    7. 函数调用的时候,如果有参数传递给函数,则先拷贝参数的副本,再将副本传递给函数。
    8. 函数参数可以没有名称。

    1.2 函数的定义和调用

    函数在使用之前必须先定义,可以调用函数来完成某个任务。函数可以重复调用,从而达到代码重用。

    1.2.1 语法

    func function_name( [parameter list] ) [return_types]
    {
       函数体
    }
    
    • 1
    • 2
    • 3
    • 4
    • func:函数由 func 开始声明
    • function_name:函数名称,函数名和参数列表一起构成了函数签名。
    • [parameter list]:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
    • return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。
    • 函数体:函数定义的代码集合。

    1.2.2 函数定义实例

    ① 定义一个求和函数

    func sum(a int, b int) (ret int) {
        ret = a + b
        return ret
    }
    
    • 1
    • 2
    • 3
    • 4

    ② 定义一个比较两个数大小的函数

    func compare(a int, b int) (max int) {
        if a > b {
            max = a
        } else {
            max = b
        }
        return max
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    1.2.3 go语言函数调用

    当我们要完成某个任务时,可以调用函数来完成。调用函数要传递参数,如何有返回值可以获得返回值。

    例如:对1.2.2两例的函数进行调用

    func main() {
        s := sum(1, 2)
        fmt.Printf("s: %v\n", s)
     
        max := compare(1, 2)
        fmt.Printf("max: %v\n", max)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    运行结果

    s: 3
    max: 2
    
    • 1
    • 2

    二 golang函数的返回值

    函数可以有0或多个返回值,返回值需要指定数据类型,返回值通过return关键字来指定。

    2.1 return的特性

    1. return可以有参数,也可以没有参数,这些返回值可以有名称,也可以没有名称。go中的函数可以有多个返回值。
    2. return关键字中指定了参数时,返回值可以不用名称。如果return省略参数,则返回值部分必须带名称
    3. 当返回值有名称时,必须使用括号包围,逗号分隔,即使只有一个返回值
    4. 但即使返回值命名了,return中也可以强制指定其它返回值的名称,也就是说return的优先级更高
    5. 命名的返回值是预先声明好的,在函数内部可以直接使用,无需再次声明。命名返回值的名称不能和函数参数名称相同,否则报错提示变量重复定义
    6. return中可以有表达式,但不能出现赋值表达式,这和其它语言可能有所不同。例如return a+b是正确的,但return c=a+b是错误的。

    2.2 实例

    ① 没有返回值

    package main
    
    import "fmt"
    
    func f1() {
    	fmt.Printf("我没有返回值")
    }
    
    func main() {
    	f1()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    运行结果:

    我没有返回值
    
    • 1

    ② 有一个返回值

    package main
    
    import "fmt"
    
    func sum(a int, b int) (ret int) {
    	ret = a + b
    	return ret
    }
    
    func main() {
    	s := sum(1, 2)
    	fmt.Printf("s: %v\n", s)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    运行结果:

    s: 3
    
    • 1

    ③ 有多个返回值,且在return中指定返回的内容

    package main
    
    import "fmt"
    
    func f2() (name string, age int) {
    	name = "Psych"
    	age = 18
    	return name, age
    }
    
    func main() {
    	name, age := f2()
    	fmt.Printf("name: %v\n", name)
    	fmt.Printf("age: %v\n", age)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    运行结果:

    name: Psych
    age: 18
    
    • 1
    • 2

    ④ 多个返回值,返回值名称没有被使用

    package main
    
    import "fmt"
    
    func f3() (name string, age int) {
    	name = "Psych"
    	age = 18
    	return // 等价于return name, age
    }
    
    func main() {
    	name, age := f3()
    	fmt.Printf("name: %v\n", name)
    	fmt.Printf("age: %v\n", age)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    运行结果:

    name: Psych
    age: 18
    
    • 1
    • 2

    ⑤ return覆盖命名返回值,返回值名称没有被使用

    package main
    
    import "fmt"
    
    func f4() (name string, age int) {
    	n := "Psych"	// 重新声明
    	a := 18
    	return n, a
    }
    
    func main() {
    	name, age := f4()
    	fmt.Printf("name: %v\n", name)
    	fmt.Printf("age: %v\n", age)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    运行结果:

    name: Psych
    age: 18
    
    • 1
    • 2

    2.3 注意事项

    1. Go中经常会使用其中一个返回值作为函数是否执行成功、是否有错误信息的判断条件。例如return value,existsreturn value,okreturn value,err等。

    2. 当函数的返回值过多时,例如有4个以上的返回值,应该将这些返回值收集到容器中,然后以返回容器的方式去返回。例如,同类型的返回值可以放进slice中,不同类型的返回值可以放进map中。

    3. 但函数有多个返回值时,如果其中某个或某几个返回值不想使用,可以通过下划线_来丢弃这些返回值。例如下面的f1函数两个返回值,调用该函数时,丢弃了第二个返回值b,只保留了第一个返回值a赋值给了变量a。

      package main
       
      import "fmt"
       
      func f1() (int, int) {
          return 1, 2
      }
      func main() {
          _, x := f1()
          fmt.Printf("x: %v\n", x)
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11

      运行结果:

      x: 2
      
      • 1

    三 golang函数的参数

    3.1 基本概念

    1. Go语言函数可以有0或多个参数,参数必须指定数据类型
    2. 声明函数时的参数列表叫做形参,调用时传递的参数叫做实参
    3. Go语言是通过传值的方式传参的,意味着传递给函数的是拷贝后的副本,所以函数内部访问、修改的也是这个副本。
    4. Go语言可以使用变长参数,有时候并不能确定参数的个数,可以使用变长参数,可以在函数定义语句的参数部分使用ARGS...TYPE的方式。这时会将...代表的参数全部保存到一个名为ARGS的slice中,注意这些参数的数据类型都是TYPE

    3.2 形参和实参

    声明函数时的参数列表叫做形参,调用时传递的参数叫做实参

    func f1(a int, b int) int {
        // 其中a和b为形参
        if a > b {
            return a
        } else {
            return b
        }
    }
     
    func main() {
        r := f1(1, 2)
        // 其中1和2为实参
        fmt.Printf("r: %v\n", r)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    运行结果:

    r: 2
    
    • 1

    3.3 函数参数传递方式

    函数值类型参数默认就是值传递,而引用类型参数默认就是引用传递。

    • 值类型:基本数据类型 int 系列, float 系列, bool, string 、数组和结构体 struct
    • 引用类型:指针、slice 切片、map、管道 chaninterface 等都是引用类型

    不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是,值传递的是值的拷贝,引用传递的是地址的拷贝,一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定拷贝的数据大小,数据越大,效率越低。

    • 值传递特点:变量直接存储值,内存通常在栈中分配
    • 引用传递特点:变量存储的是一个地址,这个地址对应的空间才真正存储数据,内存通常在堆上分配,当任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由GC来回收
    • 如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用 。

    3.4 参数按值传递

    Go语言函数是通过传值的方式传参的,意味着传递给函数的是拷贝后的副本,所以函数内部访问、修改的也是这个副本。

    package main
     
    import "fmt"
     
    func f1(a int) {
        a = 200
        fmt.Printf("a1: %v\n", a)
    }
     
    func main() {
        a := 100
        f1(a)
        fmt.Printf("a: %v\n", a)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    运行结果:

    a1: 200
    a: 100
    
    • 1
    • 2

    从运行结果可以看到,调用函数f1后,a的值并没有被改变,说明参数传递是拷贝了一个副本,也就是拷贝了一份新的内容进行运算。

    mapsliceinterfacechannel这些数据类型本身就是指针类型的,所以就算是拷贝传值也是拷贝的指针,拷贝后的参数仍然指向底层数据结构,所以修改它们可能会影响外部数据结构的值。

    package main
     
    import "fmt"
     
    func f1(a []int) {
        a[0] = 100
    }
     
    func main() {
        a := []int{1, 2}
        f1(a)
        fmt.Printf("a: %v\n", a)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    运行结果:

    a: [100 2]
    
    • 1

    从运行结果发现,调用函数后,slice内容被改变了。

    3.5 变长参数

    Go语言可以使用变长参数,有时候并不能确定参数的个数,可以使用变长参数,可以在函数定义语句的参数部分使用ARGS...TYPE的方式。这时会将...代表的参数全部保存到一个名为ARGS的slice中,注意这些参数的数据类型都是TYPE

    package main
     
    import "fmt"
     
    func f1(args ...int) {
        for _, v := range args {
            fmt.Printf("v: %v\n", v)
        }
    }
    func f2(name string, ok bool, args ...int) {
        fmt.Printf("name: %v\n", name)
        fmt.Printf("ok: %v\n", ok)
        for _, v := range args {
            fmt.Printf("v: %v\n", v)
        }
    }
    func main() {
        f1(1, 2, 3)
        fmt.Println("------------")
        f1(1, 2, 3, 4, 5, 6)
        fmt.Println("------------")
        f2("Psych", true, 1, 2, 3)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    运行结果:

    v: 1
    v: 2
    v: 3
    ------------
    v: 1
    v: 2
    v: 3
    v: 4
    v: 5
    v: 6
    ------------
    name: Psych
    ok: true
    v: 1
    v: 2
    v: 3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    四 golang函数类型与函数变量

    4.1 基本概念

    可以使用type关键字来定义一个函数类型,语法格式如下:

    type fun func(int, int) int
    
    • 1

    上面语句定义了一个fun函数类型,它也是一种数据类型

    这种函数接收两个int类型的参数,并且返回一个int类型的返回值。

    在 Go 中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用

    4.2 应用实例

    下面我们定义两个这样结构的两个函数,一个求和,一个比较大小:

    func sum(a int, b int) int {
        return a + b
    }
     
    func max(a int, b int) int {
        if a > b {
            return a
        } else {
            return b
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    下面定义一个fun函数类型,把sum和max赋值给它

    package main
    
    import "fmt"
    
    type fun func(int, int) int
    
    func sum(a int, b int) int {
    	return a + b
    }
    
    func max(a int, b int) int {
    	if a > b {
    		return a
    	} else {
    		return b
    	}
    }
    
    func main() {
    	var f fun
    	f = sum
    	fmt.Printf("f的数据类型是:%T\nsum的数据类型是:%T\n", f, sum)
    	s := f(1, 2)
    	fmt.Printf("s: %v\n", s)
    	f = max
    	m := f(3, 4)
    	fmt.Printf("m: %v\n", m)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    运行结果:

    f的数据类型是:main.fun
    sum的数据类型是:func(int, int) int
    s: 3
    m: 4
    
    • 1
    • 2
    • 3
    • 4

    由此我们可以看出函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用。

    因此函数既然是一种数据类型,因此在 Go 中,函数可以作为形参,并且调用

    package main
    
    import "fmt"
    
    func sum(a int, b int) int {
    	return a + b
    }
    
    func myFun(funvar func(int, int) int, num1 int, num2 int) int {
    	return funvar(num1, num2)
    }
    
    func main() {
    	res := myFun(sum, 20, 30)
    	fmt.Printf("res: %v\n", res)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    运行结果:

    res: 50
    
    • 1

    五 golang高阶函数

    由4.2了解到了函数既然是一种数据类型,因此在 Go 中,函数可以作为形参,并且调用。

    其实go语言的函数,可以作为函数的参数,传递给另外一个函数,也可作为另外一个函数的返回值返回。

    5.1 函数作为参数

    package main
     
    import "fmt"
     
    func sayHello(name string) {
        fmt.Printf("Hello,%s", name)
    }
     
    func f1(name string, f func(string)) {
        f(name)
    }
     
    func main() {
        f1("golang", sayHello)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    运行结果:

    Hello,golang
    
    • 1

    5.2 函数作为返回值

    package main
     
    import "fmt"
     
    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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    运行结果:

    r: 3
    -----------
    r: 50
    
    • 1
    • 2
    • 3

    六 匿名函数

    6.1 基本概念

    go语言函数不能嵌套,但是在函数内部可以定义匿名函数,实现一下简单功能调用。

    所谓匿名函数就是,没有名称的函数。

    语法格式如下:

    func (参数列表)(返回值)
    
    • 1

    当然可以既没有参数,可以没有返回值,

    匿名函数也可以实现多次调用。

    6.2 匿名函数的使用方式

    ① 方式一:在定义匿名函数时就直接调用

    这种方式匿名函数只能调用一次

    package main
     
    import "fmt"
     
    func main() {
        func(a int, b int) {
            max := 0
            if a > b {
                max = a
            } else {
                max = b
            }
            fmt.Printf("max: %v\n", max)
        }(1, 2)	// 直接调用,自动执行
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    运行结果:

    max: 2
    
    • 1

    ② 方式二:将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数

    package main
     
    import "fmt"
     
    func main() {
        max := func(a int, b int) int {
            if a > b {
                return a
            } else {
                return b
            }
        }
     
        i := max(1, 2)
        fmt.Printf("i: %v\n", i)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    运行结果:

    i: 2
    
    • 1

    ③ 方式三:全局匿名函数

    如果将匿名函数赋给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在程序有效。

    package main
    
    import "fmt"
    
    var Fun1 = func(n1 int, n2 int) int {
    	// 此时Fun1是一个全局匿名函数
    	return n1 * n2
    }
    
    func main() {
    	res := Fun1(4, 9)
    	// 全局匿名函数的使用
    	fmt.Printf("res: %v\n", res)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    运行结果:

    res: 36
    
    • 1

    七 golang闭包

    7.1 基础知识

    闭包可以理解成定义在一个函数内部的函数。

    在本质上,闭包是将函数内部和函数外部连接起来的桥梁,或者说是函数和其引用环境的组合体。

    闭包指的是一个函数和与其相关的引用环境组合而成的实体。

    简单来说,闭包=函数+引用环境

    7.2 实例演示

    package main
    
    import "fmt"
    
    func add() func(int) int {
    	var n int = 10
    	return func(x int) int {
    		n = n + x
    		return n
    	}
    }
    
    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))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    代码解释说明:

    1. add()是一个函数,返回的数据类型是fun(int)int

    2. 闭包说明:

      	var n int = 10
      	return func(x int) int {
      		n = n + x
      		return n
      	}
      
      • 1
      • 2
      • 3
      • 4
      • 5

      返回的是一个匿名函数,但是这个匿名函数引用到函数外的n,因此这个匿名函数就和n形成一个整体,构成闭包

    3. 可以这样理解:闭包是类, 函数是操作,n 是字段。函数和它使用到 n 构成闭包。

    4. 当我们反复的调用 f 或者 f1 函数时,因为 n 是初始化一次,因此每调用一次就进行累计。

    5. 要搞清楚闭包的关键,就是要分析出返回的函数使用(引用)到哪些变量,因为函数和它引用到的变量共同构成闭包。

    6. f 的生命周期内,变量 n 一直有效。

    7.3 进阶实例

    package main
     
    import (
        "fmt"
    )
     
    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))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    运行结果:

    11 9
    12 8
    13 7
    
    • 1
    • 2
    • 3

    7.4 闭包最佳应用场景

    请编写一个程序,具体要求如下

    1. 编写一个函数 makeSuffix(suffix string) 可以接收一个文件后缀名(比如.jpg),并返回一个闭包
    2. 调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.jpg) ,则返回 文件名.jpg , 如果已经有.jpg 后缀,则返回原文件名。
    3. 要求使用闭包的方式完成
    4. strings.HasSuffix , 该函数可以判断某个字符串是否有指定的后缀。
    package main
    
    import (
    	"fmt"
    	"strings"
    )
    
    func makeSuffixFunc(suffix string) func(string) string {
    	return func(name string) string {
    		// 若 name 没有指定后缀则加上,否则返回原来的名字
    		if !strings.HasSuffix(name, suffix) {
    			return name + suffix
    		}
    		return name
    	}
    }
    
    func main() {
    	f := makeSuffixFunc(".jpg")
    	fmt.Printf("文件名处理后: %v\n", f("winter"))
    	fmt.Printf("文件名处理后: %v\n", f("bird.jpg"))
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    代码解释说明:

    1. 返回的匿名函数和 makeSuffix (suffix string)suffix 变量 组合成一个闭包,因为 返回的函数引用到 suffix 这个变量
    2. 我们体会一下闭包的好处:如果使用传统的方法,也可以轻松实现这个功能,但是传统方法需要每次都传入 后缀名,比如 .jpg ,而闭包因为可以保留上次引用的某个值,所以我们传入一次就可以反复使用。

    八 golang递归

    8.1 基础知识

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

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

    • 递归就是自己调用自己。
    • 必须先定义函数的退出条件,没有退出条件,递归将成为死循环。
    • go语言递归函数很可能会产生一大堆的goroutine,也很可能会出现栈空间内存溢出问题。

    8.2 实例演示

    ① 阶乘

    package main
     
    import "fmt"
     
    func a(n int) int {
        // 返回条件
        if n == 1 {
            return 1
        } else {
            // 自己调用自己
            return n * a(n-1)
        }
    }
     
    func main() {
        n := 5
        // 5! = 5x4x3x2x1
        r := a(n)
        fmt.Printf("r: %v\n", r)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    运行结果:

    r: 120
    
    • 1

    ② 斐波那契数列

    斐波拉契数列的计算公式为f(n)=f(n-1)+f(n-2)f(2)=f(1)=1

    package main
     
    import "fmt"
     
    func f(n int) int {
        // 退出点判断
        if n == 1 || n == 2 {
            return 1
        }
        // 递归表达式
        return f(n-1) + f(n-2)
    }
     
    func main() {
        r := f(5)
        fmt.Printf("r: %v\n", r)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    运行结果:

    r: 5
    
    • 1

    8.3 重要原则

    1. 执行一个递归时,就创建一个新的受保护的独立空间(新函数栈)
    2. 递归的局部变量是独立的,不会相互影响
    3. 递归必须向退出递归的条件逼近,否则就是无限递归(死循环)
    4. 当一个递归执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当函数执行完毕或者返回时,该递归本身也会被系统销毁

    8.4 练习

    有一堆桃子,猴子第一天吃了其中的一半,并再多吃了一个!以后每天猴子都吃其中的一半,然后再多吃一个。当到第十天时,想再吃时(还没吃),发现只有 1 个桃子了。问题:最初共多少个桃子?

    思路分析:

    1. 第 10 天只有一个桃子
    2. 第 9 天有几个桃子 = (第 10 天桃子数量 + 1) * 2
    3. 规律: 第 n 天的桃子数据 peach(n) = (peach(n+1) + 1) * 2
    package main
    
    import (
    	"fmt"
    )
    
    func peach(n int) int {
    	if n > 10 || n < 1 {
    		fmt.Println("天数错误")
    		return 0 // 返回0表示没有得到正确数量
    	}
    
    	if n == 10 {
    		return 1
    	} else {
    		return (peach(n+1) + 1) * 2
    	}
    }
    
    func main() {
    	a := peach(1)
    	fmt.Printf("第一天最初的桃子数量是: %v\n", a)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    运行结果:

    第一天最初的桃子数量是: 1534
    
    • 1

    九 golang defer语句

    9.1 基础知识

    go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行

    defer的特性:

    1. 关键字 defer 用于注册延迟调用。
    2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
    3. 多个defer语句,按先进后出的方式执行。
    4. defer语句中的变量,在defer声明时就决定了。
    5. 遵守栈stack,后进先出。

    9.2 为什么需要 defer

    1. 关闭文件句柄
    2. 锁资源释放
    3. 数据库连接释放

    9.3 实例演示

    例如查看执行顺序

    package main
     
    import "fmt"
     
    func main() {
        fmt.Println("start")
        defer fmt.Println("step1")
        defer fmt.Println("step2")
        defer fmt.Println("step3")
        fmt.Println("end")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    运行结果:

    start
    end
    step3
    step2
    step1
    
    • 1
    • 2
    • 3
    • 4
    • 5

    9.4 注意事项

    1. 当 go 执行到一个 defer 时,不会立即执行 defer 后的语句,而是将 defer 后的语句压入到一个栈中[暂时称该栈为 defer 栈], 然后继续执行函数下一个语句。

    2. 当函数执行完毕后,在从 defer 栈中,依次从栈顶取出语句执行(注:遵守栈 先入后出的机制)

    3. 在 defer 将语句放入到栈时,也会将相关的值拷贝同时入栈。请看一段代码:

      package main
      
      import (
      	"fmt"
      )
      
      func sum(n1 int, n2 int) int {
      	//当执行到defer时,暂时不执行,会将defer后面的语句先压入到一个独立的栈
      	//当函数执行完毕时,再从defer的栈按照先入后出的原则出栈并执行
      	defer fmt.Println("ok1 n1 = ", n1)	// n1=10
      	defer fmt.Println("ok2 n2 = ", n2)	// n2=20
      	n1++	//n1=11
      	n2++	//n2=21
      	res := n1 + n2	//res=32
      	fmt.Printf("ok3 res= %v\n", res)	
      	return res
      }
      
      func main() {
      	res := sum(10, 20)
      	fmt.Printf("res= %v\n", res)	//res=32
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22

      运行结果:

      ok3 res= 32
      ok2 n2 =  20
      ok1 n1 =  10
      res= 32
      
      • 1
      • 2
      • 3
      • 4

    9.5 实践应用场景

    defer 最主要的价值是在,当函数执行完毕后,可以及时的释放函数创建的资源。

    看下模拟代码:

    func test(){
        //关闭文件资源
        file = openfile(文件名)
        defer file.close()
        // 其他代码
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    func test(){
        //释放数据库资源
        connect = openDatabse()
        defer connect.close()
        // 其他代码
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    说明:

    1. 在 golang 编程中的通常做法是,创建资源后,比如(打开了文件,获取了数据库的链接,或者是锁资源), 可以执行 defer file.Close()或者 defer connect.Close()
    2. 在 defer 后,可以继续使用创建资源.
    3. 当函数完毕后,系统会依次从 defer 栈中,取出语句,关闭资源.
    4. 这种机制,非常简洁,程序员不用再为在什么时机关闭资源而烦心。

    十 init函数

    10.1 基础知识

    golang有一个特殊的函数init函数,先于main函数执行,实现包级别的一些初始化操作。

    主要特点:

    • init函数先于main函数自动执行,不能被其他函数调用
    • init函数没有输入参数、返回值
    • 每个包可以有多个init函数
    • 包的每个源文件也可以有多个init函数,这点比较特殊
    • 同一个包的init执行顺序,golang没有明确定义,编程时要注意程序不要依赖这个执行顺序
    • 不同包的init函数按照包导入的依赖关系决定执行顺序
    • golang代码初始化顺序:变量初始化->init()->main()

    10.2 实例演示

    package main
     
    import "fmt"
     
    var i int = initVar()
     
    func init() {
        fmt.Println("init2")
    }
     
    func init() {
        fmt.Println("init...")
    }
     
    func initVar() int {
        fmt.Println("initVar...")
        return 100
    }
     
    func main() {
        fmt.Println("main....")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    运行结果:

    initVar...
    init2
    init...
    main....
    
    • 1
    • 2
    • 3
    • 4
  • 相关阅读:
    Spring | Spring的“数据库开发“ (Srping JDBC)
    【轻NAS】Windows搭建可道云私有云盘,并内网穿透公网访问
    HVV(护网)蓝队视角的技战法分析
    【校招VIP】前端JS语言考点之选择器和优先级
    java计算机毕业设计springboot+vue中国古诗词网站(源码+系统+mysql数据库+Lw文档)
    解决在Vue项目中无法在Internet Explorer中打开的问题
    SPL-在SpringBoot中的集成(三)
    python-爬虫(可直接使用)
    视频模板SDK,为企业带来无限创意与效率
    大橙子vfed 5.0去授权完美破解主题模版源码 | 苹果CMS
  • 原文地址:https://blog.csdn.net/qq_39280718/article/details/125417987