Go基础-函数

2022/12/14 go

在编程中,函数是指一段可以直接被另一段程序或代码引用的、可重复使用的、用来实现单一或相关联功能的代码段。目的是为了提高应用的模块性和代码的重复利用率。

相比较其他语言,Go 语言 在设计上对函数进行了优化和改进,使其使用起来更加便利。

Go 语言中函数有着以下特性:

  • 函数本身可以作为值进行传递;
  • 支持普通函数、匿名函数和闭包(closure);
  • 函数可以满足接口;

# 函数定义

在 Go 语言中,定义一个函数需要声明参数函数名返回值等。

# 定义一个普通函数

定义函数需要以 func 标识开头,后面紧接着函数名、参数列表、返回参数列表以及函数体

func 函数名(参数列表) (返回参数列表) {
  函数体
}
1
2
3
  • 函数名:函数名由字母、数字、下划线组成;函数名不能以数字开头;同一包内,函数名不能重名。
  • 参数列表:参数列表中声明的每一个参数由变量名和参数类型组成,可省略不写,不写代表不需要传参
  • 返回参数列表:可以是返回一个值类型,也可以是一个组合,即返回多个参数。另外,当函数中声明了有返回值时,函数体中必须 使用 return 语句提供返回值。一个返回值的时候()可省略,可省略不写,不写代表不需要返回值

一个简单的加法函数:

func sum(a int, b int)int {
    return a+b
}
1
2
3

# 参数列表简写

当参数列表中定义了多个参数,且参数类型相同时,代码如下:

func sum(a int, b int) int {
}
1
2

上面代码中,变量 a、b 的类型均为整型 int,可以采用以下简写方式:

func sum(a , b int) int {
}
1
2

统一定义一个 int 类型即可。

# 函数返回值

返回值支持返回多个,常用场景下,多返回值的最后一个参数会返回函数执行中可能发生的错误,示例代码如下:

conn, err := connectToDatabase()
1

上面这段代码中,函数 connectToDatabase() 用来获取数据库连接,conn 表示数据库连接,err 用来接收获取过程中可能发生的错误。

# 同一类型的返回值

如果函数返回值是统一类型,则用括号将多个返回值括起来,以逗号隔开,示例代码如下:

package main

import "fmt"

func say(name, content string) (string, string) {
	return name, content
}

func main()  {
	name, content := say("hello", "Go")

	fmt.Println(name, content)
}
1
2
3
4
5
6
7
8
9
10
11
12
13

注意:使用 return 语句返回多个值时,值的顺序需要与函数声明的返回值一致。

代码输出:

hello Go
1

# 带有变量名的返回值

Go 语言支持对返回值进行命名,命名后代码的可读性更佳。

命名的返回值默认值为该类型的默认值,例如,若返回值为整型 int,则默认值为 0;若为字符串string,则默认值为空字符串;布尔为 false; 指针为 nil 等。

下面是代码示例:

func initValue() (a int, b int) {//声明返回变量名
	a = 1
	b = 2
	return //不填写返回值
}
1
2
3
4
5

上面这段代码中,函数声明中将返回值变量命名为 ab,然后在函数体中分别对 ab 进行赋值, 然后使用 return 语句进行返回。

注意: 当函数使用命名进行返回时,可以在 return 语句中不填返回值列表,当然填写也是可以的,上面的代码与下面的代码执行效果相同:

func initValue() (a int, b int) {//声明返回变量名
	a = 1
	return a, 2 //填写返回值
}
1
2
3
4

接收的时候变量名不一定要求要跟返回变量名一样

# 调用函数

函数的调用格式如下:

//带返回值的
返回值列表 = 函数名(参数列表) 
//不带返回值
函数名(参数列表) 
1
2
3
4
  • 函数名:需要被调用的函数名;
  • 参数列表: 传入的参数列表,以逗号隔开;
  • 返回值列表:多个返回值以逗号隔开;
package main

import "fmt"

func initValue() (a int, b int) {//声明返回变量名
	a = 1
	return a, 2 //填写返回值
}

func main() {
	// 调用 initValue 函数
	a, b := initValue()
	fmt.Println(a, b)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

代码输出:

1 2
1

函数内定义的局部变量只能作用在函数体中,函数执行结束后,这些变量都会被释放掉,无法再次访问。

# 函数变量(将函数保存到变量中)

在 Go 语言 中,函数也是一种类型,可以和其他类型(如intfloatstring 等)一样被保存到变量中,类型为func(参数列表) (返回参数列表)

以下为几种情况的示例代码:

package main

import "fmt"

//无参数无返回值
func say1() {
	fmt.Println("hello Go")
}

//有参数,无返回值
func say2(a string) {
	fmt.Println(a)
}

//无参数,有返回值
func say3() string {
	return "hello GO"
}

//有参数有返回值
func say4(a string) string {
	return a
}

func main() {
	//无参数无返回值
	var f func()
	f = say1
	f()
	//有参数,无返回值
	var f2 func(string2 string)
	f2 = say2
	f2("haha")
	//无参数,有返回值
	var f3 func() string
	f3 = say3
	s := f3()
	fmt.Println(s)
	//有参数有返回值
	var f4 func(string) string
	f4 = say4
	s2 := f4("bbbb")
	fmt.Println(s2)
}

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
35
36
37
38
39
40
41
42
43
44
45

代码输出:

hello Go
haha
hello GO
bbbb
1
2
3
4

函数变量也可以使用短变量声明和初始化进行简化写法,可使代码更为简洁美观:

package main

import "fmt"

//无参数无返回值
func say1() {
	fmt.Println("hello Go")
}

//有参数,无返回值
func say2(a string) {
	fmt.Println(a)
}

//无参数,有返回值
func say3() string {
	return "hello GO"
}

//有参数有返回值
func say4(a string) string {
	return a
}

func main() {
	//无参数无返回值
	f := say1
	f()
	//有参数,无返回值
	f2 := say2
	f2("haha")
	//无参数,有返回值
	f3 := say3
	s := f3()
	fmt.Println(s)
	//有参数有返回值
	f4 := say4
	s2 := f4("bbbb")
	fmt.Println(s2)
}

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
35
36
37
38
39
40
41

代码输出:

hello Go
haha
hello GO
bbbb
1
2
3
4

# 匿名函数_回调函数

匿名函数 是没有名字的函数,只有函数体匿名函数经常以变量 的形式被传递。

大部分场景下,匿名函数经常被使用于实现函数回调、闭包等。

# 定义一个匿名函数

要定义一个匿名函数,其实格式跟普通函数差不多,只是没有函数名

func(参数列表) (返回参数列表) {
  函数体
}
1
2
3

# 使用方式

# 定义后立即调用匿名函数

定义完匿名函数后,在函数后面跟上传入参数,就可以立即调用了

package main

import "fmt"

func main() {
	func(name string) {
		fmt.Printf("hello, %s", name)
	}("Go")
}
1
2
3
4
5
6
7
8
9

代码输出:

hello Go
1

# 将匿名函数赋值给变量

匿名函数可以赋值给变量,格式跟函数变量一样:

package main

import "fmt"

func main() {
	f := func(name string) {
		fmt.Printf("hello, %s", name)
	}
	f("Go")
}

1
2
3
4
5
6
7
8
9
10
11

代码输出:

hello Go
1

# 匿名函数用作回调函数

回调函数可以当作函数的一个参数传参,在函数本身做完对应的逻辑后,扩展后续的操作。比如两个数相加后,你可以将其输出或者做其他操作等,提升代码的扩展性

两数相加并打印的例子:

package main

import "fmt"

func sumPrint(a int, b int, f func(int)) {
	sum := a + b
	f(sum)
}

func main() {
	//两数相加,回调函数将其输出
	sumPrint(1, 1, func(i int) {
		fmt.Println(i)
	})
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

代码输出:

2
1

# 函数实现接口(将函数当做接口来调用)

其他基本类型能够实现接口,函数 同样可以实现接口。

# 如何定义一个接口

定义格式如下:

type 接口名字 interface {
	方法名(interface{})
}
1
2
3

下面是一个动物的接口:

// 定义一个名为 Animal 的接口
type Animal interface {
    // 需要实现一个说话 say() 方法
	say(interface{})
}
1
2
3
4
5

这个接口需要实现 say() 方法,调用时需传入一个 interface{} 类型的变量,这种类型表示你可以传入任意类型的值。

# 实现方法

# 结构体实现接口

package main

import "fmt"

type Animal interface {
	say(interface{})
}

// 定义一个结构体类型:狗
type Dog struct {
}

// 实现接口 animal 定义的 say 方法,方法中打印一句话
func (s *Dog) say(p interface{}) {
	fmt.Println("狗说: ", p)
}

func main() {
	// 定义接口
	var animal Animal
	// 实例化狗结构体
	dog := new(Dog)
	// 将实例化的结构体赋给接口
	animal = dog
	// 使用接口调用实例化结构体的方法 Say()
	animal.say("汪汪")
}

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

代码输出:

狗说:  汪汪
1

# 函数实现接口

函数要想实现接口,需要先将自己定义为类型,然后实现接口方法,同时需要再方法中调用函数本体。函数实现接口无需实例化,只需要将函数转换为 函数类型即可。

示例代码如下:

package main

import "fmt"

type Animal interface {
	say(interface{})
}

// 将函数定义为类型
type Cat func(interface{})

// 实现接口 Dog 的 Say 方法
func (f Cat) say(p interface{}) {
	// 调用 f() 函数本体
	f(p)
}

func main() {
	// 定义接口
	var animal Animal
	// 将匿名函数转换为 Cat 类型,此时 Cat 类型实现了 say 方法,赋值给接口是成功的
	animal = Cat(func(p interface{}) {
		fmt.Println("猫说: ", p)
	})
	// 使用接口调用 say 方法
	animal.say("喵喵")
}

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

代码输出:

猫说:  喵喵
1

# 闭包函数

“官方”的解释是:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

维基百科讲,闭包(Closure),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

简单来说就是引用了外部变量 的匿名函数

# 定义闭包

闭包是由函数及其相关引用环境组合而成的实体。

函数 + 引用外部变量 = 闭包
1

下面代码演示了如何在 Go 语言中定义闭包:

package main

import "fmt"

func main() {
	// 定义一个字符串
	str := "hello World"

	// 创建一个匿名函数
	function := func() {
		// 给字符串 str 赋予一个新的值,注意: 匿名函数引用了外部变量,这种情况形成了闭包
		str = "hello Go"
		// 打印
		fmt.Println(str)
	}

	// 执行闭包
	function()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

代码输出:

hello Go
1

闭包复制的是原对象指针,这就很容易解释延迟引用现象。

package main

import "fmt"

func test() func() {
    x := 100
    fmt.Printf("x (%p) = %d\n", &x, x)

    return func() {
        fmt.Printf("x (%p) = %d\n", &x, x)
    }
}

func main() {
    f := test()
    f()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

代码输出:

x (0xc0000180a8) = 100
x (0xc0000180a8) = 100
1
2

在汇编层 ,test 实际返回的是 FuncVal 对象,其中包含了匿名函数地址闭包对象指针。当调 匿名函数时,只需以某个寄存器传递该对象即可。

FuncVal { func_address, closure_var_pointer ... }
1

# 闭包的记忆效应

闭包在引用外部变量后具有记忆效应,闭包中可以修改变量,变量会随着闭包的生命周期一直存在,此时,闭包如同变量一样拥有了记忆效应。

示例代码如下:

package main

import "fmt"

// 定义一个累加函数,返回类型为 func() int, 入参为整数类型,每次调用函数对该值进行累加
func add(value int) func() int {
	// 返回一个闭包
	return func() int {
		// 累加
		value++
		// 返回累加值
		return value
	}
}

func main() {
	// 创建一个累加器,初始值为 1
	accumulator := add(1)

	// 累加1并打印
	fmt.Println(accumulator())
	// 再来一次
	fmt.Println(accumulator())

	// 创建另一个累加器,初始值为 10
	accumulator2 := add(10)

	// 累加1并打印
	fmt.Println(accumulator2())
}

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

代码输出:

2
3
11
1
2
3

通过输出可以看出闭包的记忆效应,每次调用 accumulator() 后,都会对 value 进行累加操作。

# 返回2个闭包

package main

import "fmt"

// 返回2个函数类型的返回值
func test01(base int) (func(int) int, func(int) int) {
    // 定义2个函数,并返回
    // 相加
    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 := test01(10)
    // base一直是没有消
    fmt.Println(f1(1), f2(2))
    // 此时base是9
    fmt.Println(f1(3), f2(4))
}
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

代码输出:

11 9
12 8
1
2

# 通过闭包实现一个生成器

可以通过闭包的记忆效应来实现设计模式中工厂模式的生成器。

下面的代码示例展示了创建游戏玩家生成器的过程:

package main

import "fmt"

// 定义一个玩家生成器,它的返回类型为 func() (string, int),输入名称,返回新的玩家数据
func genPlayer(name string) func() (string, int) {
	// 定义玩家血量
	hp := 1000
	// 返回闭包
	return func() (string, int) {
		// 引用了外部的 hp 变量, 形成了闭包
		return name, hp
	}
}

func main() {
	// 创建一个玩家生成器
	generator := genPlayer("新手1号")
	// 返回新创建玩家的姓名, 血量
	name, hp := generator()
	// 打印
	fmt.Println(name, hp)

}

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

代码输出:

新手11000
1

从上面代码看出,闭包具有面向对象语言的特性 —— 封装性,变量 hp 无法从外部直接访问和修改。

# 递归函数

递归,就是在运行的过程中调用自己,一个函数调用自己,就叫做递归函数

构成递归需具备的条件:

  1. 子问题须与原始问题为同样的事,且更为简单。
  2. 不能无限制地调用本身,须有个出口,化简为非递归状况处理。

# 数字阶乘

一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1。自然数n的阶乘写作n!。1808年,基斯顿·卡曼引进这个表示法。

package main

import "fmt"

func factorial(i int) int {
    if i <= 1 {
        return 1
    }
    return i * factorial(i-1)
}

func main() {
    var i int = 7
    fmt.Printf("Factorial of %d is %d\n", i, factorial(i))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

代码输出:

Factorial of 7 is 5040
1

# 斐波那契数列(Fibonacci)

这个数列从第3项开始,每一项都等于前两项之和。

package main

import "fmt"

func fibonaci(i int) int {
    if i == 0 {
        return 0
    }
    if i == 1 {
        return 1
    }
    return fibonaci(i-1) + fibonaci(i-2)
}

func main() {
    var i int
    for i = 0; i < 10; i++ {
        fmt.Printf("%d\n", fibonaci(i))
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

代码输出:

0
1
1
2
3
5
8
13
21
34
1
2
3
4
5
6
7
8
9
10
package main

import (
    "fmt"
    "io"
    "os"
)

func do() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func(f io.Closer) {
            if err := f.Close(); err != nil {
                fmt.Printf("defer close book.txt err %v\n", err)
            }
        }(f)
    }

    // ..code...

    f, err = os.Open("another-book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func(f io.Closer) {
            if err := f.Close(); err != nil {
                fmt.Printf("defer close another-book.txt err %v\n", err)
            }
        }(f)
    }

    return nil
}

func main() {
    do()
} 
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
35
36
37
38
39
40
41