函数
函数是组织好的、可重复使用的、用来实现单一或相关联功能的代码段,其可以提高应用的模块性和代码的重复利用率。
Go语言支持普通函数、匿名函数和闭包,从设计上对函数进行优化和改进,让函数使用起来更加方便。
Go语言的函数属于”一等公民“,也就是说:
- 函数本身可以作为值进行传递
- 支持匿名函数和闭包
- 函数可以满足接口
5.1 声明函数
5.1.1 普通函数的声明形式
Go语言的函数声明以func标识,后面紧接着函数名、参数列表、返回参数列表和函数体,具体形式如下:
func 函数名(参数列表)(返回参数列表){
函数体
}
- 函数名:由字母、数字、下划线组成。其中,函数名的第一个字母不能是数字。在同一个包内,函数名称不能重名。
提示: 包(package)是Go源码的一种组织方式,一个包可以是一个文件夹,在后续会有详细讲解包的概念。
-
参数列表:一个参数由参数变量和参数类型组成,例如:
func foo(a int, b string)
其中,参数列表中的变量作为函数的局部变量存在。
-
返回参数列表:可以是返回值的类型列表,也可以是类似参数列表中的变量名和类型名的组合。函数在声明有返回值时,必须在函数体中使用return语句提供返回值列表
-
函数体:能够被重复调用的代码片段。
5.1.2 参数类型的简写
在参数列表中,如有多个参数变量,以逗号分隔;如果相邻变量是同类型,则可以将类型省略。例如:
func add(a, b int) int {
return a + b
}
5.1.3 函数返回值
Go语言支持多返回值,多返回值能方便地获得函数执行后的多个返回参数,Go语言经常使用多返回值中的最后一个参数返回函数执行中可能发生的错误。示例代码如下:
conn, err := connectToNetwork()
Go语言既支持安全指针,也支持多返回值,因此在使用函数进行逻辑编写时更方便。
-
同一种类型返回值
如果返回值时同一种类型,则用括号将多个返回值类型括起来,用逗号分隔;使用return返回时,注意值列表的顺序需与函数声明的返回值类型一致。示例代码如下:
func typedTwoValues() (int, float) { return 1, 2.0 }
-
带有变量名的返回值
Go语言支持对返回值进行命名,这样返回值就和参数一样拥有变量名和类型。
命名的返回值变量默认值为类型的默认值,即数值0,字符串为空字符串,布尔为false、指针为nil等。
示例如下:
func namedRetValues() (a, b int) {
a = 1
b = 2
return //当函数使用命名返回值时,可以 return后不填写返回值列表,
}
5.1.4 调用函数
函数在定义后,可以通过调用的方式,让当前代码跳转到被调用的函数中进行执行。调用前的函数局部变量都会被保存起来不会被丢失;被调用的函数结束后,恢复到被调用函数的下一行继续执行代码,之前的局部变量也能继续访问。
5.1.5 示例:将”秒“解析为时间单位
在本例中,将一个数值表示时间的”秒“值,然后使用resolveTime()函数将传入的秒数转换为天、小时和分钟等时间单位。
package main
import "fmt"
const (
SecondsPerMinute = 60 //每分钟的秒数
SecondsPerHour = SecondPerMinuter * 60
SecondsPerDay = SecondPerHour * 24
)
//将传入的秒数解析为3种时间单位
func resolveTime(seconds int) (day, hour, minute int) {
day = seconds / SecondsPerDay
hour = seconds / SecondsPerHour
minute = seconds / SecondsPerMinute
return
}
func main() {
fmt.Println(resolveTime(1000))
//只获取小时和分钟
_, hour, min := resolveTime(18000)
fmt.Println(hour, min)
day, _, _ := resolveTime(90000)
fmt.Println(day)
}
5.1.6 示例:函数中的参数传递效果测试
Go语言中传入和返回参数在调用和返回时都使用值传递,这里需要注意的是指针、切片和map等引用类型对象指向的内容在参数传递中不会发生复制,而是将指针进行复制,类似于创建一次引用。
-
测试数据类型
type Data struct { complax []int instance InnerData ptr *InnerData } type InnerData struct { a int }
-
值传递的测试函数
func passByValue(inFunc Data) Data { fmt.Printf("inFunc value: %+v\n", inFunc) fmt.Printf("inFunc ptr: %p\n", &inFunc) return inFunc }
-
测试流程
in := Data{ complax: []int{1,2,3}, instance: InnerData{5}, ptr: &InnerData{1}, } //传入的结构体 fmt.Printf("in value: %+v\n", in) fmt.Printf("in ptr: %p\n", &in) out := passByValue(in) //返回的结构体 fmt.Printf("out value: %+v\n", out) fmt.Printf("out ptr: %p\n", &out)
从运行结果中发现:
- 所有的Data结构的指针地址发生了变化,无论是将Data的结构传入函数内部,还是通过函数返回值传回的Data都会发生复制行为。
- 所有的Data结构中的成员值都没有发生变化,原样传递,意味着所有参数都是值传递。
- Data结构的ptr成员在传递过程中保持一致,表示指针在函数参数值传递中传递只是指针值,不会复制指针指向的部分。
5.2 函数变量
在Go语言中函数也是一种类型,可以和其他类型一样被保存在变量中。下面的代码定义一个函数变量f,并将一个函数名fire()赋给函数变量f, 这样调用函数变量f,实际调用的就是fire()函数,代码如下:
func fire() {
fmt.Println("fire")
}
func main() {
var f func()
f = fire
f()
}
5.3 匿名函数
Go语言支持匿名函数,即在需要使用函数时,再定义函数,匿名函数没有函数名,只有函数体。匿名函数常用于实现回调函数、闭包等。
5.3.1 定义一个匿名函数
匿名函数的定义格式如下:
func (参数列表) (返回参数列表){
函数体
}
-
在定义时调用匿名函数
func (data int) { fmt.Println("hello", data) }(100)
-
将匿名函数赋值给变量
f := func(data int) { fmt.Println("hello", data) } //使用f()调用 f(100)
5.3.2 匿名函数用作回调函数
代码示例如下:
func visit(list []int, f func(int)) {
for _, v := range list {
f(v)
}
}
func main() {
visit([]int{1,2,3,4}, func(v, int){
fmt.Println(v)
})
}
5.3.3 使用匿名函数实现操作封装
var skillparam = flag.String("skill", "", "skill to perform")
func main() {
flag.Parse()
var skill = map[string]func() { //定义一个从字符串映射到func()的map
"fire": func(){
fmt.Println("chicken fire")
},
"run": func(){
fmt.Println("soldier run")
},
"fly": func(){
fmt.Println("angel fly")
},
}
if f, ok := skill[*skillparam]; ok {
f()
} else {
fmt.Println("skill not found")
}
}
5.4 函数类型实现接口
函数和其他类型一样都属于“一等公民”,其他类型能够实现接口,函数也可以,以下将分别对比结构体和函数实现接口的过程。
5.4.1 结构体实现接口
结构体实现Invoker接口代码如下:
type Struct struct{
}
func (s *Struct) Call(p interface{}) {
fmt.Println("from struct", p)
}
5.4.2 函数实现接口
函数的声明不能直接实现接口,需要将函数定义为类型后,使用类型实现结构体。当类型方法被调用时,还需要调用函数本体。
//函数定义为类型
type FuncCaller func(interface{})
func (f FuncCaller) Call(p interface{}) {
f(p) //调用f()函数本体
}
5.5 闭包
闭包是引用自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放和删除,在闭包中可以继续使用这个自由变量。简单说:
函数+引用环境=闭包
同一个函数与不同环境组合,可以形成不同的实例。
一个函数类型就像结构体一样,可以被实例化。函数本身不存储任何信息,只有与引用环境结合后形成的闭包才具有“记忆性” 。函数是编译期静态的概念,而闭包是运行期。
动态的概念。
5.5.1 在闭包内部修改引用的变量
闭包对它作用域上部变量的引用可以修改,修改引用的变量就会对变量进行实际修改,以下例子来理解:
str := "hello world"
foo := func() {
str = "hello dude" //在匿名函数中访问str
}
foo()