函数声明:
函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。
func 函数名(形式参数列表)(返回值列表){
函数体
}
形式参数列表描述了函数的参数名以及参数类型,这些参数作为局部变量,其值由参数调用者提供,返回值列表描述了函数返回值的变量名以及类型,如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的。
如果一个函数在声明时,包含返回值列表,那么该函数必须以 return 语句结尾。
下面,我们给出 4 种方法声明拥有 2 个 int 型参数和 1 个 int 型返回值的函数,空白标识符_
可以强调某个参数未被使用。
func add(x int, y int) int {return x + y} func sub(x, y int) (z int) { z = x - y; return} func first(x int, _ int) int { return x } func zero(int, int) int { return 0 } fmt.Printf("%T\n", add) // "func(int, int) int" fmt.Printf("%T\n", sub) // "func(int, int) int" fmt.Printf("%T\n", first) // "func(int, int) int" fmt.Printf("%T\n", zero) // "func(int, int) int"
Go语言支持多返回值,多返回值能方便地获得函数执行后的多个返回参数,Go语言经常使用多返回值中的最后一个返回参数返回函数执行中可能发生的错误,示例代码如下:
conn, err := connectToNetwork()
在这段代码中,connectToNetwork 返回两个参数,conn 表示连接对象,err 返回错误信息。
如果返回值是同一种类型,则用括号将多个返回值类型括起来,用逗号分隔每个返回值的类型。
func typedTwoValues() (int, int) { return 1, 2 }
Go语言支持对返回值进行命名,这样返回值就和参数一样拥有参数变量名和类型。
func namedRetValues() (a, b int) { a = 1 b = 2 return }
提示
同一种类型返回值和命名返回值两种形式只能二选一,混用时将会发生编译错误,例如下面的代码:
func namedRetValues() (a, b int, int)
编译报错提示:
mixed named and unnamed function parameters
意思是:在函数参数中混合使用了命名和非命名参数。
函数在定义后,可以通过调用的方式,让当前代码跳转到被调用的函数中进行执行,调用前的函数局部变量都会被保存起来不会丢失,被调用的函数运行结束后,恢复到调用函数的下一行继续执行代码,之前的局部变量也能继续访问。
函数内的局部变量只能在函数体中使用,函数调用结束后,这些局部变量都会被释放并且失效。
Go语言的函数调用格式如下:
返回值变量列表 = 函数名(参数列表)
下面是对各个部分的说明:
- 函数名:需要调用的函数名。
- 参数列表:参数变量以逗号分隔,尾部无须以分号结尾。
- 返回值变量列表:多个返回值使用逗号分隔。
例如,加法函数调用样式如下:
result := add(1,1)
函数变量:
在Go语言中,函数也是一种类型,可以和其他类型一样保存在变量中,下面的代码定义了一个函数变量 f,并将一个函数名为 fire() 的函数赋给函数变量 f,这样调用函数变量 f 时,实际调用的就是 fire() 函数,代码如下:
package main import ( "fmt" ) func fire() { fmt.Println("fire") } func main() { var f func() f = fire f() }
代码输出结果:
fire
代码说明:
- 定义了一个 fire() 函数。
- 将变量 f 声明为 func() 类型,此时 f 就被俗称为“回调函数”,此时 f 的值为 nil。
- 将 fire() 函数作为值,赋给函数变量 f,此时 f 的值为 fire() 函数。
- 使用函数变量 f 进行函数调用,实际调用的是 fire() 函数。
匿名函数:
Go语言支持匿名函数,即在需要使用函数时再定义函数,匿名函数没有函数名只有函数体,函数可以作为一种类型被赋值给函数类型的变量,匿名函数也往往以变量方式传递,这与C语言的回调函数比较类似,不同的是,Go语言支持随时在代码里定义匿名函数。
匿名函数是指不需要定义函数名的一种函数实现方式,由一个不带函数名的函数声明和函数体组成,下面来具体介绍一下匿名函数的定义及使用。
匿名函数的定义格式如下:
func(参数列表)(返回参数列表){
函数体
}
匿名函数的定义就是没有名字的普通函数定义。
1,调用匿名函数:
匿名函数可以在声明后调用,例如:
func(data int) { fmt.Println("hello", data) }(100)
2,将匿名函数赋值给变量:
匿名函数可以被赋值,例如:
// 将匿名函数体保存到f()中 f := func(data int) { fmt.Println("hello", data) } // 使用f()调用 f(100)
匿名函数的用途非常广泛,它本身就是一种值,可以方便地保存在各种容器中实现回调函数和操作封装。
3,匿名函数用作回调函数:
下面的代码实现对切片的遍历操作,遍历中访问每个元素的操作使用匿名函数来实现,用户传入不同的匿名函数体可以实现对元素不同的遍历操作,代码如下:
package main import ( "fmt" ) // 遍历切片的每个元素, 通过给定函数进行元素访问 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) }) }
代码说明如下:
- 使用 visit() 函数将整个遍历过程进行封装,当要获取遍历期间的切片值时,只需要给 visit() 传入一个回调参数即可。
- 准备一个整型切片 []int{1,2,3,4} 传入 visit() 函数作为遍历的数据。
- 定义了一个匿名函数,作用是将遍历的每个值打印出来。
4,使用匿名函数实现操作封装
下面这段代码将匿名函数作为 map 的键值,通过命令行参数动态调用匿名函数,代码如下:
package main import ( "flag" "fmt" ) var skillParam = flag.String("skill", "", "skill to perform") func main() { flag.Parse() var skill = map[string]func(){ "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") } }
代码说明如下:
- 定义命令行参数 skill,从命令行输入 --skill 可以将
=
后的字符串传入 skillParam 指针变量。 - 解析命令行参数,解析完成后,skillParam 指针变量将指向命令行传入的值。
- 定义一个从字符串映射到 func() 的 map,然后填充这个 map。
- 初始化 map 的键值对,值为匿名函数。
- skillParam 是一个 *string 类型的指针变量,使用 *skillParam 获取到命令行传过来的值,并在 map 中查找对应命令行参数指定的字符串的函数。
- 如果在 map 定义中存在这个参数就调用,否则打印“技能没有找到”。
函数体实现接口:
函数的声明不能直接实现接口,需要将函数定义为类型后,使用类型实现结构体,当类型方法被调用时,还需要调用函数本体。
// 函数定义为类型 type FuncCaller func(interface{}) // 实现Invoker的Call func (f FuncCaller) Call(p interface{}) { // 调用f()函数本体 f(p) }
代码说明如下:
- 将 func(interface{}) 定义为 FuncCaller 类型。
- FuncCaller 的 Call() 方法将实现 Invoker 的 Call() 方法。
- FuncCaller 的 Call() 方法被调用与 func(interface{}) 无关,还需要手动调用函数本体。
上面代码只是定义了函数类型,需要函数本身进行逻辑处理,FuncCaller 无须被实例化,只需要将函数转换为 FuncCaller 类型即可,函数来源可以是命名函数、匿名函数或闭包,参见下面代码:
// 声明接口变量 var invoker Invoker // 将匿名函数转为FuncCaller类型, 再赋值给接口 invoker = FuncCaller(func(v interface{}) { fmt.Println("from function", v) }) // 使用接口调用FuncCaller.Call, 内部会调用函数本体 invoker.Call("hello")
代码说明如下:
- 声明接口变量。
- 将 func(v interface{}){} 匿名函数转换为 FuncCaller 类型(函数签名才能转换),此时 FuncCaller 类型实现了 Invoker 的 Call() 方法,赋值给 invoker 接口是成功的。
- 使用接口方法调用。
代码输出如下:
from function hello
结构体实现接口:
结构体实现 Invoker 接口的代码如下:
// 结构体类型 type Struct struct { } // 实现Invoker的Call func (s *Struct) Call(p interface{}) { fmt.Println("from struct", p) }
代码说明如下:
- 定义结构体,该例子中的结构体无须任何成员,主要展示实现 Invoker 的方法。
- Call() 为结构体的方法,该方法的功能是打印 from struct 和传入的 interface{} 类型的值。
将定义的 Struct 类型实例化,并传入接口中进行调用,代码如下:
// 声明接口变量 var invoker Invoker // 实例化结构体 s := new(Struct) // 将实例化的结构体赋值到接口 invoker = s // 使用接口调用实例化结构体的方法Struct.Call invoker.Call("hello")
代码说明如下:
- 声明 Invoker 类型的变量。
- 使用 new 将结构体实例化,此行也可以写为 s:=&Struct。
- s 类型为 *Struct,已经实现了 Invoker 接口类型,因此赋值给 invoker 时是成功的。
- 通过接口的 Call() 方法,传入 hello,此时将调用 Struct 结构体的 Call() 方法。
接下来,对比下函数实现结构体的差异。
代码输出如下:
from struct hello
可变参数:
本节我们将介绍可变参数的用法。合适地使用可变参数,可以让代码简单易用,尤其是输入输出类函数,比如日志函数等。
可变参数是指函数传入的参数个数是可变的,为了做到这点,首先需要将函数定义为可以接受可变参数的类型:
func myfunc(args ...int) { for _, arg := range args { fmt.Println(arg) } }
形如...type
格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数,它是一个语法糖(syntactic sugar),即这种语法对语言的功能并没有影响,但是更方便程序员使用,通常来说,使用语法糖能够增加程序的可读性,从而减少程序出错的可能。
之前的例子中将可变参数类型约束为 int,如果你希望传任意类型,可以指定类型为 interface{},下面是Go语言标准库中 fmt.Printf() 的函数原型:
func Printf(format string, args ...interface{}) { // ... }
遍历可变参数列表:
可变参数列表的数量不固定,传入的参数是一个切片,如果需要获得每一个参数的具体值时,可以对可变参数变量进行遍历,参见下面代码:
package main import ( "bytes" "fmt" ) // 定义一个函数, 参数数量为0~n, 类型约束为字符串 func joinStrings(slist ...string) string { // 定义一个字节缓冲, 快速地连接字符串 var b bytes.Buffer // 遍历可变参数列表slist, 类型为[]string for _, s := range slist { // 将遍历出的字符串连续写入字节数组 b.WriteString(s) } // 将连接好的字节数组转换为字符串并输出 return b.String() } func main() { // 输入3个字符串, 将它们连成一个字符串 fmt.Println(joinStrings("pig ", "and", " rat")) fmt.Println(joinStrings("hammer", " mom", " and", " hawk")) }
当可变参数为 interface{} 类型时,可以传入任何类型的值,此时,如果需要获得变量的类型,可以通过 switch 获得变量的类型,下面的代码演示将一系列不同类型的值传入 printTypeValue() 函数,该函数将分别为不同的参数打印它们的值和类型的详细描述。
package main import ( "bytes" "fmt" ) func printTypeValue(slist ...interface{}) string { // 字节缓冲作为快速字符串连接 var b bytes.Buffer // 遍历参数 for _, s := range slist { // 将interface{}类型格式化为字符串 str := fmt.Sprintf("%v", s) // 类型的字符串描述 var typeString string // 对s进行类型断言 switch s.(type) { case bool: // 当s为布尔类型时 typeString = "bool" case string: // 当s为字符串类型时 typeString = "string" case int: // 当s为整型类型时 typeString = "int" } // 写字符串前缀 b.WriteString("value: ") // 写入值 b.WriteString(str) // 写类型前缀 b.WriteString(" type: ") // 写类型字符串 b.WriteString(typeString) // 写入换行符 b.WriteString("\n") } return b.String() } func main() { // 将不同类型的变量通过printTypeValue()打印出来 fmt.Println(printTypeValue(100, "str", true)) }
代码说明如下:
- printTypeValue() 输入不同类型的值并输出类型和值描述。
- bytes.Buffer 字节缓冲作为快速字符串连接。
- 遍历 slist 的每一个元素,类型为 interface{}。
- 使用 fmt.Sprintf 配合
%v
动词,可以将 interface{} 格式的任意值转为字符串。 - 声明一个字符串,作为变量的类型名。
- switch s.(type) 可以对 interface{} 类型进行类型断言,也就是判断变量的实际类型。
- s 变量可能的类型,将每种类型的对应类型字符串赋值到 typeString 中。
- 写输出格式的过程。
在多个可变参数函数间传递参数:
可变参数变量是一个包含所有参数的切片,如果要将这个含有可变参数的变量传递给下一个可变参数函数,可以在传递时给可变参数变量后面添加...
,这样就可以将切片中的元素进行传递,而不是传递可变参数变量本身。
可变参数传递例子:
package main import "fmt" // 实际打印的函数 func rawPrint(rawList ...interface{}) { // 遍历可变参数切片 for _, a := range rawList { // 打印参数 fmt.Println(a) } } // 打印函数封装 func print(slist ...interface{}) { // 将slist可变参数切片完整传递给下一个函数 rawPrint(slist...) } func main() { print(1, 2, 3) }
defer语句:
Go语言的 defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。
当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出),下面的代码是将一系列的数值打印语句按顺序延迟处理,如下所示:
package main import ( "fmt" ) func main() { 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
结果分析如下:
- 代码的延迟顺序与最终的执行顺序是反向的。
- 延迟调用是在 defer 所在函数结束时进行,函数结束可以是正常返回时,也可以是发生宕机时。
使用延迟执行语句在函数退出时释放资源:
1,使用延迟并发解锁
在下面的例子中会在函数中并发使用 map,为防止竞态问题,使用 sync.Mutex 进行加锁,参见下面代码:
var ( // 一个演示用的映射 valueByKey = make(map[string]int) // 保证使用映射时的并发安全的互斥锁 valueByKeyGuard sync.Mutex ) // 根据键读取值 func readValue(key string) int { // 对共享资源加锁 valueByKeyGuard.Lock() // 取值 v := valueByKey[key] // 对共享资源解锁 valueByKeyGuard.Unlock() // 返回值 return v }
代码说明如下:
- 第 3 行,实例化一个 map,键是 string 类型,值为 int。
- 第 5 行,map 默认不是并发安全的,准备一个 sync.Mutex 互斥量保护 map 的访问。
- 第 9 行,readValue() 函数给定一个键,从 map 中获得值后返回,该函数会在并发环境中使用,需要保证并发安全。
- 第 11 行,使用互斥量加锁。
- 第 13 行,从 map 中获取值。
- 第 15 行,使用互斥量解锁。
- 第 17 行,返回获取到的 map 值。
使用 defer 语句对上面的语句进行简化,参考下面的代码。
func readValue(key string) int { valueByKeyGuard.Lock() // defer后面的语句不会马上调用, 而是延迟到函数结束时调用 defer valueByKeyGuard.Unlock() return valueByKey[key] }
上面的代码中第 6~8 行是对前面代码的修改和添加的代码,代码说明如下:
- 第 6 行在互斥量加锁后,使用 defer 语句添加解锁,该语句不会马上执行,而是等 readValue() 函数返回时才会被执行。
- 第 8 行,从 map 查询值并返回的过程中,与不使用互斥量的写法一样,对比上面的代码,这种写法更简单。
2,使用延迟释放文件句柄
文件的操作需要经过打开文件、获取和操作文件资源、关闭资源几个过程,如果在操作完毕后不关闭文件资源,进程将一直无法释放文件资源,在下面的例子中将实现根据文件名获取文件大小的函数,函数中需要打开文件、获取文件大小和关闭文件等操作,由于每一步系统操作都需要进行错误处理,而每一步处理都会造成一次可能的退出,因此就需要在退出时释放资源,而我们需要密切关注在函数退出处正确地释放文件资源,参考下面的代码:
// 根据文件名查询其大小 func fileSize(filename string) int64 { // 根据文件名打开文件, 返回文件句柄和错误 f, err := os.Open(filename) // 如果打开时发生错误, 返回文件大小为0 if err != nil { return 0 } // 取文件状态信息 info, err := f.Stat() // 如果获取信息时发生错误, 关闭文件并返回文件大小为0 if err != nil { f.Close() return 0 } // 取文件大小 size := info.Size() // 关闭文件 f.Close() // 返回文件大小 return size }
代码说明如下:
- 第 2 行,定义获取文件大小的函数,返回值是 64 位的文件大小值。
- 第 5 行,使用 os 包提供的函数 Open(),根据给定的文件名打开一个文件,并返回操作文件用的句柄和操作错误。
- 第 8 行,如果打开的过程中发生错误,如文件没找到、文件被占用等,将返回文件大小为 0。
- 第 13 行,此时文件句柄 f 可以正常使用,使用 f 的方法 Stat() 来获取文件的信息,获取信息时,可能也会发生错误。
- 第 16~19 行对错误进行处理,此时文件是正常打开的,为了释放资源,必须要调用 f 的 Close() 方法来关闭文件,否则会发生资源泄露。
- 第 22 行,获取文件大小。
- 第 25 行,关闭文件、释放资源。
- 第 28 行,返回获取到的文件大小。
在上面的例子中,第 25 行是对文件的关闭操作,下面使用 defer 对代码进行简化,代码如下:
func fileSize(filename string) int64 { f, err := os.Open(filename) if err != nil { return 0 } // 延迟调用Close, 此时Close不会被调用 defer f.Close() info, err := f.Stat() if err != nil { // defer机制触发, 调用Close关闭文件 return 0 } size := info.Size() // defer机制触发, 调用Close关闭文件 return size }
代码中加粗部分为对比前面代码而修改的部分,代码说明如下:
- 第 10 行,在文件正常打开后,使用 defer,将 f.Close() 延迟调用,注意,不能将这一句代码放在第 4 行空行处,一旦文件打开错误,f 将为空,在延迟语句触发时,将触发宕机错误。
- 第 16 行和第 22 行,defer 后的语句(f.Close())将会在函数返回前被调用,自动释放资源。
参考文档:
https://studygolang.com/articles/13958