Go基础—4.函数(func)

TOC

一、函数(func)

go语言中的三种函数:

  • 普通的带有名字的函数
  • 匿名函数或者 lambda 函数
  • 方法

普通函数的声明

函数声明的语法格式:

func 函数名(形式参数列表)(返回值列表){
    函数体代码
    是否有返回参数
}

我们下面写了三个函数,然后进行调用。
【示例】代码如下所示:

package main

import "fmt"

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

func b(x int, y int) int {
	var z int
	z = x + y
	return z
}

func c(x int, y int) (z int) {
	z = x + y
	return
}

func main() {
	fmt.Printf("a函数: %d, b函数: %d, c函数: %d", a(23, 34), b(23, 34), c(23, 34))
}
/* 运行结果如下
a函数: 57, b函数: 57, c函数: 57
*/

上面三种函数的写法不同,但是执行的效果是一样的
正如上面的a、b、c函数一样,如果一组形参或返回值有相同的类型,我们不必为每个形参都写出参数类型,我们可以写成下面这样,写法如下所示:

func a(x, y int) int{/*......*/}
func b(x, y int) int{/*......*/}
func c(x, y int) (z int){/*......*/}

函数的返回值

go语言中支持多个返回值,代码示例如下所示:

func test() (a, b int){
    a = 1
    b = 2
    return a, b
}

func main(){
    value1, value2 := test()
    fmt.Println(value1, value2)
}
/* 运行结果如下
1 2
*/

同一种类型
如果返回值是同一种类型,则用括号将多个返回值类型括起来,用逗号分隔每个返回值的类型。代码示例如下所示:

func test() (int, int){
    return 1, 2
}

带有变量名的返回值
go语言支持对返回值进行命名,这样返回值就和参数一样拥有参数变量名和类型。代码示例如下所示:

func test() (a, b int){
    a = 1
    b = 2
    return
}

函数的调用

下面我们写了函数的多种调用方式,包括了无返回值函数的调用、有返回值函数的调用和把函数作为值保存到变量中的方式。
【示例】代码如下所示:

package main

import "fmt"

func echo() {
    fmt.Println("hello!!!")
}

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

func main() {
	// 无返回值函数的调用
	echo()
	// 有返回值函数的调用
	num := add(2, 3)
	fmt.Println(num)
	// 把函数作为值保存到变量中
	var e func()
	e = echo
	e()
}
/* 运行结果如下
hello!!!
5
hello!!!
*/

匿名函数

定义匿名函数

在自定义时调用

func(data int) {
    fmt.Println("data的值:", data)
}(100)

将匿名函数赋值给变量

// 将匿名函数保存到f中
f := func(data int) {
    fmt.Println("data的值:", data)
}

// 使用f()进行调用匿名函数
f(100)

匿名函数作为回调函数

下面的代码实现对切片的遍历操作,遍历中访问每个元素的操作使用匿名函数来实现,用户传入不同的匿名函数体可以实现对元素不同的遍历操作。
【示例】代码如下所示:

package main

import "fmt"

// 构建一个函数,用匿名函数作为回调函数
func print_list(list []string, f func(string)) {
	for _, value := range list {
		f(value)
	}
}

func main() {
    // 创建一个匿名函数赋值给f,功能是输出传的参数
	f := func(data string) {
		fmt.Printf(data + " ")
	}
	// 创建一个字符串类型的数组
	var test_list []string = []string{"alice", "bob", "jack", "tom"}
	print_list(test_list, f)
}
/* 运行结果如下
alice bob jack tom
*/

使用匿名函数实现操作封装

可以将匿名函数封装作为 map 的键值,通过传递的参数动态调用匿名函数。
【示例】代码如下所示:

package main

import "fmt"

func options(c string) {
    // 封装匿名函数到map中
	var choose = map[string]func(){
		"status": func() {
			fmt.Println("app status info.")
		},
		"run": func() {
			fmt.Println("app is run...")
		},
		"stop": func() {
			fmt.Println("app is stop...")
		},
	}
	// 判断传入的参数,如果在map中存在则调用对应key的函数
	if f, ok := choose[c]; ok {
		f()
	} else {
		fmt.Println("choose not found!!!")
	}
}

func main() {
    // 创建一个数组,保存需要调用的参数值
	var cmd_list []string = []string{"run", "status", "stop", "reload"}
	// 循环依次调用options函数传入不同的参数
	for _, cmd := range cmd_list {
		options(cmd)
	}
}
/* 运行结果如下
app is run...
app status info.
app is stop...
choose not found!!!
*/

变参函数(可变参数类型)

可变参数是指函数传入的参数个数是可变的,意思就是传参的个数不固定,根据你调用函数时,你传入的个数为准。

固定类型的可变参数

固定类型就是传入的参数个数有很多个,但是这些参数的数据类型一致统一。
我们通过编写一个函数循环输出我们传入的参数,代码示例如下所示:

func myf(args ...int) {
    fmt.Printf("传入的参数列表: ")
    for _, arg := range args {
        fmt.Printf("%d ", arg)
    }
}
myf(2, 23, 17, 19)
/* 运行结果如下
传入的参数列表: 2 23 17 19
*/

上面...int的本质来说就是[]int,也就是一个数组切片,但是写成int[]的话传入参数部分就不能是传入多个了就要传入一个数组切片,比如myf(int[]{2, 23, 17, 19}),所以有了...type的语法糖之后,我们就可以传入多个参数形成数组切片,不用自己来处理了传入的类型是数组切片了。

任意类型的可变参数

之前的例子我们将可变参数的参数类型设置为int,如果你希望传入任意类型的参数,可以指定类型为 interface{}。
下面我们把可变参数为 interface{} 类型时,可以传入任何类型的值,我们一一获取每个参数的类型。
【示例】代码如下所示:

package main

import (
	"bytes"
	"fmt"
)

func printValueType(args ...interface{}) string {

	// 字节缓冲作为快速字符串连接
	var b bytes.Buffer

	// 遍历参数
	for _, arg := range args {
		// 将interface{}类型格式化为字符串
		str := fmt.Sprintf("%v", arg)
		// 类型的字符串描述
		var typeString string

		// 对传入的参数进行类型断言
		switch arg.(type) {
		case bool: // 当参数为布尔类型时
			typeString = "bool"
		case string: // 当参数为字符串类型时
			typeString = "string"
		case int: // 当参数为整型类型时
			typeString = "int"
		case int64: // 当参数为整型64位时
			typeString = "int64"
		case float64: // 当参数为浮点型时
			typeString = "float64"
		}

		// 写字符串前缀
		b.WriteString("value: ")
		// 写入值
		b.WriteString(str)
		// 写入,符号
		b.WriteString(",")
		// 写类型前缀
		b.WriteString(" type: ")
		// 写类型字符串
		b.WriteString(typeString)
		// 写入换行符
		b.WriteString("\n")

	}
	return b.String()
}

func main() {
	// 将不同类型的变量通过printValueType()打印出来
	fmt.Println(printValueType(100, "str", true, 234, 3.14159))
}
/* 运行结果如下
value: 100, type: int
value: str, type: string
value: true, type: bool
value: 234, type: int
value: 3.14159, type: float64
*/

在多个变参函数中传递可变参数

如果要将这个含有可变参数的变量传递给下一个可变参数函数,可以在传递时给可变参数变量后面添加...,这样就可以将切片中的元素进行传递,而不是传递可变参数变量本身。
下面例子模拟了一个变参函数中如何传递被传入的可变参数到另外一个变参函数中,并且打印出来传递的所有参数。
【示例】代码如下所示:

package main

import (
	"fmt"
)

// 接受并打印transfer()调用传递的可变参数
func print_args(args_b ...interface{}) {
	fmt.Printf("传入的参数有: ")
	for _, value := range args_b {
		fmt.Printf("%v ", value)
	}
}

// 第一个变参函数,将可变参数传递给print_args()打印出来
func transfer(args_a ...interface{}) {
	print_args(args_a...)
}

func main() {
	// 调用第一个变参函数transfer()
	transfer("apple", 3.14, 26)
}
/* 运行结果如下
传入的参数有: apple 3.14 26
*/

结构体方法定义与调用示例

结构体方法定义是指在 Go 语言中,为结构体类型绑定函数,使该函数成为该结构体的方法。把操作逻辑和数据绑定在一起,实现面向对象编程思想,在调用同种行为方法的过程中根据绑定的结构体不同,实现不同的调用结果
下面实例通过为 cat 和 dog 结构体定义各自的 call() 方法,实现了不同类型结构体拥有各自行为(发出叫声)的功能。代码如下所示:

package main

import "fmt"

type cat struct{}

func (c cat) call() {
  fmt.Println("喵喵喵~")
}

type dog struct{}

func (d dog) call() {
  fmt.Println("汪汪汪~")
}

func main() {
  c := dog{}
  d := cat{}
  c.call()
  d.call()
}
/* 运行结果如下
喵喵喵~
汪汪汪~
*/

defer(延迟执行语句)

defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行。也就是说越后面被defer的语句会越早执行,执行顺序从下到上依次执行。如下所示:

fmt.Println("defer begin")
// 将defer放入延迟调用栈,最上面最后执行
defer fmt.Println(1)
defer fmt.Println(2)
// 最后一个放入, 位于栈顶, 最先调用
defer fmt.Println(3)
fmt.Println("defer end")
/* 运行结果如下
defer begin  
defer end  
3  
2  
1
*/

递归函数

所谓递归函数指的是在函数内部调用函数自身的函数。递归函数可以解决许多数学问题,如计算给定数字阶乘、产生斐波系列等。
【示例】代码如下所示:

package main

import "fmt"

// 斐波那契数列函数,功能是创建输出一个num参数以内的斐波那契数列
func fibonacci(num int) {
	var f func(int) int
	f = func(n int) (res int) {
		if n <= 2 {
			res = 1
		} else {
			res = f(n-1) + f(n-2)
		}
		return
	}
	var result int
	for i := 1; i <= num; i++ {
		result = f(i)
		fmt.Printf("%d ", result)
	}
	fmt.Println()
}

// 数字阶乘函数,传入一个整数参数,得出并打印出该参数的阶乘值
func factorial(num int) {
	var f func(n uint64) (result uint64)
	f = func(n uint64) (result uint64) {
		if n > 0 {
			result = n * f(n-1)
			return result
		}
		return 1
	}
	fmt.Printf("%d 的阶乘是 %d\n", num, f(uint64(num)))
}

func main() {
	// 打印出10个斐波那契数列
	fibonacci(10)
	// 打印出10的阶乘值
	factorial(10)
}
/* 运行结果如下
1 1 2 3 5 8 13 21 34 55 
10 的阶乘是 3628800
*/

因为你需要在匿名函数内部递归调用它自身,所以不能直接写成 result := func(n int) int { ... },那样在函数内部引用 f 会找不到符号。
以下是错误的写法,不能工作的原因是:在匿名函数定义的内部使用了变量 f,但此时 f 还没完成初始化。- f := ... 是一个短变量声明,它在赋值时才声明并初始化变量 f。在右边的 func(…) 这个表达式里面,Go 试图使用 f 进行递归,但此时 f 还不存在(尚未完成初始化),所以会报错。

f := func(n int) (res int) {
	if n <= 2 {
		res = 1
	} else {
		res = f(n-1) + f(n-2)
	}
	return
}