1. 概念
函数,function,具有特定功能的代码块,称之为函数。通常函数可以接收外部数据称 之为函数参数,函数可以将运算结果返回,称之为函数的返回值。
定义函数的作用,为了将特定功能的代码块重用。体现程序设计上的封装性。
Go 应用程序有一个特殊的函数, main.main() 函数,入口函数,应用程序的初始执行点!
2. 声明语法
使用关键字 func 来定义。
语法如下:
func 函数标识符(形参列表) (返回值类型列表) {
函数体
}
其中,返回值类型列表,根据是否存在返回来确定是否使用。若仅仅存在一个返回值,可以 省略小括号。
也就是说,函数由:func 关键字,标识符(函数名)、参数、返回值类型、函数体构成。 其中:函数 func 关键字,形参列表、返回值类型以及函数体,构成了函数的字面量。
之所以称之为函数字面量,在 Go 语言中,函数也作为一种类型进行管理。而且是一种引用类型。
代码演示:
func F(p1 int, p2 string) string {
return ""
}
注意:函数在定义时,不能嵌套。func {func {}} 是错误的。
3. 调用函数
F() 的方式进行调用。
调用函数时,需要根据函数的需要传递参数给函数,同时根据需求得到函数的返回结果。
演示如下:
//调用函数
result := F(42, "hank")
fmt.Println(result)
4. 函数标识符和匿名函数
满足标识符的要求。
函数标识符用于引用函数执行代码的地址,通过函数名,即可调用该函数。
函数名,不完全算作函数的一个部分,是一个查找机制。
在程序设计时,可以定义没有名字的函数,称之为匿名函数
匿名函数仅仅存在函数的字面量部分,需要将其立即调用或者存储到某个特定的变量中。 演示:
//匿名函数
// 存储起来
f := func (p1 int, p2 string) string {
return "return value"
}
fmt.Printf("%T, %v\n", f, f) // func(int, string) string, f值是 0x49c5d0
fmt.Println(f(42, "")) // return value
// 直接调用
result := func (p1 int, p2 string) string {
return "return value"
}(42, "hank")
fmt.Println(result) // return value
本例中, 输出的 func(int, string) string 称之为函数签名,也可以叫函数类型字面量。
函数签名,可以用来描述函数是否相同。
例如,本例中的具名函数 F() ,和 匿名函数变量 f,就是具有相同的函数签名,可以被视为同一个功能函数,指的是在 语法中可以通用。
5.参数
1) 概述
参数用于在调用函数时,向函数体内传递外部数据。
2)形参和实参
参数在描述时分为:形式参数(形参,paramter)和实际参数(实参,argument)。
形参:函数定义时使用的参数。在函数被调用时,被当做函数内的局部变量来处理。
实参:函数在被调用时,传递的参数。 在调用函数时,是使用实参为形参赋值的过程。
代码演示:
func F(p1 int, p2 string) string {
fmt.Println(p1, p2)
return "return value"
}
result := F(42, "hank") // p1 = 42, p2 = "hank"
其中:
p1, p2 就是形参
42, “hank” 就是实参
调用函数时,使用 42 和 hank 为 p1 和 p2 赋值。
3)参数传递
调用函数时,使用 42 和 hank 为 p1 和 p2 赋值。
由于形参 p1, p2 类型不同,要求传递参数也是不同的。
典型的传参定义:
- 常规类型,非引用类型,int,string,array。修改函数内的形参,不会影响函数外 的实参。
- 引用类型 slice,map。修改函数内的形参,会导致函数外的实参改变。
- 指针类型,
*T
。T类型的指针,通过*
解析地址操作,修改形参数据,会影响到外
部的实参。
func Func1(p1 int, p2 []int, p3 *int) {
p1 += 10
p2[1] += 10
*p3 += 20
fmt.Println(p1, p2, *p3) // 52 [1 12 3] 62
}
a1, a2, a3 := 42, []int{1, 2, 3}, 42
Func1(a1, a2, &a3)
fmt.Println(a1, a2, a3) // 42 [1 12 3] 62
注意结果,内部形参改变,切片和指针导致外部实参也随之改变,而常规类型 int,外部实参没有改变。
4)简洁类型语法
当多个参数的类型一致时候,可以在最后一个该类型参数后声明类型即可。
演示:
func Func2(p1, p2 int, p3 string) int {
return 0
}
与 p1 int, p2 int 一致。此时函数的签名还是:func(int, int, string) int 。没有省略第一个参 数参类型的描述。
5)剩余参数,不定数量参数
函数的最后一个形参,在定义的时候,可以在类型前使用剩余运算符定义,语法:
p ...T
注意:一定是最后一个参数。 剩余参数的作用,当调用函数时,可以传递不定数量该类型的参数。
此时形参接收的数据类型为 []T,T 类型的切片。 演示:
func Func3(op string, ps ...int) int {
fmt.Println(ps)
return 0
}
Func3("plus") // []
Func3("plus", 1) // [1]
Func3("plus", 1, 2) // [1 2]
Func3("plus", 1, 2, 3) // [1 2 3]
不定数量,0 个也可以。
当参数定义为不定数量时,传递实参时,也可以利用 ... 展开运算符,将同类型的数据展开传递,演示:
Func3("plus", []int{1, 2, 3, 4, 5}...)
func Func3(op string, ps ...int) int
此时相当于使用 []int{1, 2, 3, 4, 5} 为 ps 赋值的过程。(ps = []int{1, 2, 3, 4, 5} )
注意:
直接使用切片展开进行剩余参数赋值的操作,是两个变量间的直接赋值。
要求切片的类型和形参的类型完全一致。
思考:
fmt.Println() 的签名是:
func Println(a ...interface{}) (n int, err error)
参数 a,是空接口类型的切片。
问题是: 为什么不能 fmt.Println(slice...)的形式来调用。slice = []int{1, 2, 3, 4, 5}。
fmt.Println(1,2, 3, 4, 5)
x := []int{1, 2 ,3, 4, 5} // 错误的语法
fmt.Println(x...) // 不同于 fmt.Println(1,2, 3, 4, 5)
原因是:
展开运运算 为 剩余参数赋值时,是直接变量间赋值的过程。Println 的形参 a,直接被赋值
x 赋值的过程。
a=x
该操作,就要去 a 和 x 的类型一致。当前不一致:
x = []int
a = []interface{}
[]interface{}空接口类型的切片不是 []int 整型切片类型。
6.返回值
1) 概述
函数的处理结果,为函数的返回值。
语法上,需要声明函数的返回值类型以及在函数体内使用 return 完成数据返回。
2)返回值类型声明
语法如下,有 4 种语法: 无返回值,单值返回,多值返回,命名返回。
代码演示:
// 无返回值
func F4() {
}
// 单返回值
func F5() int {
return 0
}
// 亦可
//func F5() (int) {
// return 0
//}
// 多返回值
func F6() (int, string) {
return 0, ""
}
// 命名返回
func F7() (result1 int) {
return
}
func F8() (result1 int, result2 string) {
return
}
函数的返回值声明,在函数参数列表后,使用括号进行包裹声明,声明的部分可以由返回值变量及类型构成。其中返回值变量可以省略,在函数内处理即可。同时若仅存在一个返回值 类型声明,可以省略括号部分。
注意: 函数签名(函数类型字面量)部分 包含返回值类型声明部分。(不包含返回值变量名部分)
3)return 语句
函数体内的 return 语句,用于完成返回值处理。 两种语法:
- 返回特定数据表达式
- 独立的return,通常同于命名返回值上
演示:
func F6() (int, string) {
return 0, ""
}
func F8() (result1 int, result2 string) {
return
}
若函数声明了返回值,则需要强制使用 return 语句。而且是可检测到一定执行的 return 语句。 例如,下面的语法错误:
func F6() (int, string) {
//return 0, ""
} // missing return at end of function
可检测到的 return,指的是 return 必须可能执行到。在 条件判断语句体内的 return,也会被认为没有 return:
func F9() (int, string) {
cond := true
if cond {
return 0, ""
}
} // missing return at end of function
上面语法的处理应该在最后,再加一个 return:
func F9() (int, string) {
cond := true
if cond {
return 0, ""
}
return 0, ""
}
要求 return 后,没有其他语句了。return 为最后一条语句。
因为 return 表示函数运行结束, 后续的语句没有任何意义。反过来说,即使执行了后续的语句,函数就没有正确的返回。演示:
func F9() (int, string) {
cond := true
if cond {
return 0, ""
}
return 0, ""
fmt.Println("after return")
} // missing return at end of function
4)具名返回(命名返回)
在声明返回值时,同时指定了返回变量。
该变量,相当于已经生命好的函数内的局部变量(类似于形参),在函数结束时,直接使用简单的 return 即可完成返回。
代码演示:
func F8() (result1 int, result2 string) {
return
}
命名返回的优势在于可以在返回值声明的位置,通过变量名确定返回值意义。 例如下面的函数定义,返回为一个长度和一个标题
func F11() (int, string) {
return 0, ""
}
func F12() (length int, title string) {
return
}
5)返回引用
返回值也可以是引用类型。
func F13() *int {
result := 42
return &result
}
result := F13()
fmt.Println(result, *result) // 0xc00000a0c0 42
注意:若返回值为当前函数内变量的引用形式,意味着在函数运行结束后,该被应用的变量 值空间,还会被继续使用。此时函数已经运行结束,函数内所控制的资源变量,已经被释放。
该情况的处理,Go 采用了【栈逃逸】的机制。
栈逃逸: 当需要在函数外使用函数内的变量资源时,为函数内的相应变量分配空间时,不会
使用常规的函数调用栈空间,而是在栈外开辟空间存储,该策略称之为栈逃逸。
7.调用栈
函数,在被调用时,会在内存的函数调用栈,为函数开辟空间,将函数的相关数据进行
存储,例如函数内的局部变量(包括形参,函数内声明的参数,命名返回值变量)。
栈:先进后出的一种结构。
如图所示:
注意:当函数运行完毕,对应函数调用栈,会被释放,内部的资源会全部回收!
8.递归调用
1)概述
调用函数的一种方案,指的是,在函数内部调用函数本身,称之为递归调用。
在函数体的某条语句中,完成了对自身的调用。
是迭代(循环)执行一种方案。
用于解决,一个复杂的问题,可以拆解成规模小且方案一致的简单问题。方案一致,意味着需要使用相同的方法进行解决,使用相同的函数进行解决。当问题的规模足够小时,解决方案显而易见。
因此,将大规模问题,逐步拆解成小规模问题,逐一解决小规模问题,将解决的结果向 上集合,进而将大问题解决。
分为:分,治,合,三个步骤。拆分,治理,合并。
例如:遍历某个目录下的全部(包含子目录)内容。
语法演示(伪代码):
func DirRead(path string) {
for file := readFile(path) {
print(file)
if is_dir(file) {
DirRead(file)
}
}
}
编写递归时,主要考虑:
- 是否可以使用递归编程?是否可以大规模拆解为相同算法的小规模?
- 递归调用的条件(递归点)。
- 递归的出口在哪里?当问题拆分到什么程度,不再需要继续拆分了?
2)递归计算阶乘
(阶乘更好的方案,应该是循环结构,此处为了演示递归) 阶乘:
5! = 5 * 4 * 3 * 2 *1
6! = 6 * 5 * 4 * 3 * 2 *1 = 6 * 5!
N! = N * (N-1) * .... * 2 * 1
总结:
N! = N * (N-1)!
可见,当需要求解阶乘问题时,出现了大规模问题可以拆解成小规模问题的现象: N 的问题,可以拆解长 N-1 的问题。
可以使用递归编程来实现。
递归的条件:
直接计算 N-1 的阶乘,递归调用即可。递归的出口:
当 N==1 时,阶乘为 1。1! == 1, 不需要继续递归了,有出口。编程实现:
func Factorial(n int) int {
if 1 == n { // 递归出口,不需要继续递归了
return 1 // 该规模的问题,可以直接解决。
}
return n * Factorial(n - 1)
}
fmt.Println(Factorial(1)) // 1
fmt.Println(Factorial(2)) // 2
fmt.Println(Factorial(3)) // 6
fmt.Println(Factorial(4)) // 24
fmt.Println(Factorial(5)) // 120
fmt.Println(Factorial(6)) // 720
分析该函数的执行过程。如下:
Factorial(5)
// return 5 * Factorial(5-1)
// return 5 * (return 4 * Factorial(4-1))
// return 5 * (return 4 * (return 3 * Factorial(3-1)))
// return 5 * (return 4 * (return 3 * (return 2 * Factorial(2-1))))
// return 5 * (return 4 * (return 3 * (return 2 * 1)))
// return 5 * (return 4 * (return 3 * 2))
// return 5 * (return 4 * 6)
// return 5 * 24
// 120
可见,最外层的 return 一直在等,内部的调用结束。在函数的调用栈中,会存在很多 Factorial()的栈,如图所示:
3)递归计算斐波那契数列第 N 项的值
(递归编程的演示, 本例还是建议使用循环结构)
斐波那契,前两项已知一般是 1,1,从第三项开始,为前两项的和。
F(3) = F(3-1) + F(3-2)
1, 1, 2, 3, 5, 8, 13, 21 ....
要求定义函数,计算第 N 项斐波那契数的值。
分析:
大规模可以拆解成思路一致的小规模。
递归点在于 N-1 和 N-2 上。
出口:前两项已知,n==1, n==2 不用递归实现。
编程实现:
func Fibonacci(n int) int {
//出口
if 1 == n {
return 1
}
if 2 == n {
return 1
}
// 递归计算
return Fibonacci(n-1) + Fibonacci(n-2)
}
fmt.Println(Fibonacci(1))
fmt.Println(Fibonacci(2))
fmt.Println(Fibonacci(3))
fmt.Println(Fibonacci(4))
fmt.Println(Fibonacci(5))
fmt.Println(Fibonacci(6))
fmt.Println(Fibonacci(7))
fmt.Println(Fibonacci(8))
fmt.Println(Fibonacci(9))
fmt.Println(Fibonacci(10))
fmt.Println(Fibonacci(11))
4)处理树状结构
树状结构,是比较典型的嵌套结构。例如,文件夹的关系,分类的关系,以及 word 的
标题层次 都是典型的树状结构。
典型的树状结构,可以处理无限层次。
树状结构数据的存储,很难是直接的树状结构,通常都是在数据库里的并列的表格结构。 业务逻辑中,将表格数据,维护成树状的结构,典型的程序。例如:遍历文件夹中的全部文件,做产品的无限极分类。都是类似的结构。
演示分类的处理:
输入数据,并列的表格数据.
如下演示: 每个分类有三个字段,标题,ID,上级分类 ID。
type Category struct {
ID int // ID
Title string // 标题
PID int // 上级分类 ID
}
categories := []Category {
Category{1, "电脑", 0},
Category{2, "服装", 0},
Category{3, "手机", 0},
Category{4, "图书", 0},
Category{5, "手机通讯", 3},
Category{6, "智能设备", 3},
Category{7, "手机配件", 3},
Category{8, "手机壳", 7},
Category{9, "数据线", 7},
Category{10, "支架", 7},
Category{11, "type-c", 9},
Category{12, "usb", 9},
Category{13, "light", 9},
Category{14, "教材", 4},
Category{15, "文学", 4},
Category{16, "科技", 4},
Category{17, "中小学", 14},
Category{18, "外语词典", 14},
Category{19, "男装", 2},
}
基于该数据,完成树状结构或嵌套结构的获取。
利用递归编程实现。
- 可以用递归的原因:
上级分类检索下级分类的大规模问题,可以拆解为下级分类再检索 下下级分类的问题。大规模拆解成小规模。 - 实现思路:
利用 PID 检索某个分类下的子分类,当检索到某个子分类后,递归基于该子分类继续 检索后代分类。检索的过程就是遍历全部分类的过程。
演示: 增加一个数据类型,用于记录分类及其层级,层级用来标识当前分类的缩进深度。
func Tree(dataList []Category, id, l int) (tree []CategoryTree) {
// 遍历全部的 category
for _, c := range dataList {
// 基于 PID 判断是否为当所检索 Id 的子分类
if c.PID == id {
// 是 子分类
// 1 记录下来
tree = append(tree, CategoryTree{
Category: c,
Level: l,
})
// 2 递归检索,将所到当前分类的子分类,将其追加到结果之后。
tree = append(tree, Tree(dataList, c.ID, l+1)...)
} }
return
}
此时得到的返回值就是带有层级的分类树。
需要时可以根据层级做缩进展示:
categoryTree := Tree(categories, 0, 0)
for _, c := range categoryTree {
fmt.Print(strings.Repeat(" ", c.Level * 2))
fmt.Println(c.Category.Title)
}
输出样式
电脑
服装
男装
手机
手机通讯
智能设备
手机配件
手机壳
数据线
图书
教材
中小学
外语词典
文学
...... 省略
5)嵌套结构
对于该层级分类结构,另一种典型的数据格式为嵌套格式,在当前分类上记录其后代分类。
结构如下:
//得到的数据模拟
var nested = []CategoryNested{
CategoryNested {
Category: Category{3, "手机", 0},
Children: []CategoryNested{
{
Category: Category{5, "手机通讯", 3},
Children: nil,
},
{
Category: Category{7, "手机配件", 3},
Children: []CategoryNested{
},
},
},
},
}
该结构的生成,也是典型的递归程序,实现如下:
func Nested(dataList []Category, id int) (nested []CategoryNested){
for _, c := range dataList {
if c.PID == id {
// 继续检索其后代分类
nested = append(nested, CategoryNested{
Category: c,
Children: Nested(dataList, c.ID),
})
}
}
return
}
categoryNested := Nested(categories, 0)
fmt.Println(categoryNested)
得到的结果:
[{{1 电脑 0}[]}{{2 服装 0}[{{19 男装 2}[]}]}{{3 手机 0}[{{5 手机 通讯 3}[]}{{6 智能设备 3}[]}{{7 手机配件 3}[{{8 手机壳 7}[]}{{9 数 据线 7} [{{11 type-c 9} []} {{12 usb 9} []
} {{13 light 9} []}]} {{10 支架 7} []}]}]} {{4 图书 0} [{{14 教材 4} [{{17 中 小学 14} []} {{18 外语词典 14} []}]} {{15 文学 4} []} {{16 科技 4} []}]}]
展示时,通常配合多层循环完成展示,例如:
categoryNested := Nested(categories, 0)
//fmt.Println(categoryNested)
for _, c := range categoryNested {
fmt.Println(c.Category.Title)
// 判断是否存在子分类
if len(c.Children) > 0 {
for _, cc := range c.Children {
fmt.Println(cc.Category.Title)
// 继续
if len(cc.Children) > 0 {
for _, ccc := range cc.Children {
fmt.Println(ccc.Category.Title)
}
}
}
}
}
可见,下列展示:
或者也有类似目录那种多级的 缩进结构展示
. 9 延迟调用
常规情况下,函数调用后立即执行。可以利用 defer 关键字,将函数的执行延迟当所在
在函数的最后执行。
演示:
fmt.Println("before F14")
defer F14()
fmt.Println("after F14")
func F14() {
fmt.Println("F14 is running.")
}
//输出:
//before F14
//after F14
//F14 is running.
延迟调用,与函数本身无关,仅仅是调用机制问题。
主要用在关闭一些资源上,例如打开的数据库连接,文件句柄等。
一般来说,资源用完需要关闭。为了防止当操作资源后,忘记关闭资源,通常的做法都 是打开资源后立即使用 defer 将其关闭,不会立即关闭,可以保证函数执行完毕,一定会关闭。将打开与关闭写在一起,演示:
func F15 () {
// 操作文件
// 打开文件
handle, _ := os.Open("./if.go")
// 延迟关闭
defer handle.Close()
// 文件操作
// 操作结束
}
defer 延迟调用,接收的参数为调用时的参数值,而不是运行时的参数值,演示:
func F14(n int) {
fmt.Println(n, " ", "F14 is running.") }
fmt.Println("before F14")
a := 42
defer F14(a)
a = 1024
fmt.Println(a, " ", "after F14")
// 输出
// before F14
// 1024 after F14
// 42 F14 is running.
注意: F14 运行时 n 的值为 42 而不是更新后的 1024. 因为在 defer 是会获取参数的拷贝,传递给函数。
多个 defer 的执行顺序,先注册的 defer 后执行,因为函数会维护一个 defer 栈,栈,先 进后出结构。(后进先出)。
defer F14()
defer F16()
// 输出
// f16 is running.
// F14 is running.
defer 三点:
- 延迟到函数结束时调用
- 接收的参数为调用时的参数拷贝
- 先defer后调用,后defer先调用