9 函数
函数
- go函数不支持嵌套、重载和默认参数
- 但支持一下特性:
- 无需要申明原型、不定长度变参、多返回值、命名返回值参数和匿名函数、闭包
- 定义函数使用关键字func,且左大括号不能另起一行
- 函数也可以作为一种类型使用
如何定义函数
func 函数名(参数类型 参数,参数类型 参数)(返回值类型,返回值类型){ 左花边号必须和func同行}
func anet(a int,b string) (int,string) {
}
也可以同时定义多个同样类型的返回值和参数,代码如下所示:
func anet(a ,b,c int) (a,b,c int) { // abc都是int类型的
}
由于a,b,c是我们在返回值那个小括号内定义好了,所以我们在函数内部的代码里面不需要再次定义,代码如下所示:
func anet(a, b, c int) (a, b, c int) {
a, b, c = 1, 2, 3 // 在返回值那个小括号内定义好了,所以不需要定义了
return a, b, c
}
可变长度参数,我们在传入参数的时候有时候只知道这个参数是int型,却不知道有多少个,所以这个时候需要使用可变长度参数,用三个点来替代,这个可变长度参数等同于python里面的**kwargs或者*args,都只能放在参数里面的最后一位
func anet(a ...int) { // ...表示可变长度参数,参数类型全为int型
a, b, c = 1, 2, 3
return a, b, c
}
func main() {
anet1("a", 1, 2, 3, 4, 5)
}
func anet1(b string, a ...int) { // 可变长度参数只能放在最后面使用
fmt.Println(b, a)
}
函数参数
对于函数参数传递方式来说,有两种:
-
值传递
-
引用传递
无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。 -
map、slice、chan、指针、interface默认以引用的方式传递。
-
多返回值的时候,我们并不需要重新定义变量,直接等于号赋值就行了。
我们传入一个数组到函数里面,再在这个函数里面去修改这个数组的值,看会不会对原始数组发生改变:
先看例子:
package main
import (
"fmt"
)
func main() {
s2 := []int{1, 2, 3, 4, 5}
fmt.Println(s2)
anet(s2)
fmt.Println(s2)
}
func anet(s []int) {
s[0] = 7
s[1] = 8
fmt.Println(s)
}
// 打印结果如下
[1 2 3 4 5]
[7 8 3 4 5]
[7 8 3 4 5]
由打印结果可得知,如果函数的参数是一个数组,那么传入到这个函数的数组在函数内部进行了操作,那么其原始数组也会变动,因传入参数的时候是直接传入的一个内存地址进去让函数使用的,所以能够对原始数组进行更改。
那么看看输入普通的数字有没有这样的效果
package main
import (
"fmt"
)
func main() {
a, b := 1, 2
fmt.Println(a, b)
anet(a, b)
fmt.Println(a, b)
}
func anet(s ...int) {
s[0] = 7
s[1] = 8
fmt.Println(s)
}
// day
多返回值
多返回值的时候不需要重新定义了
func returnName(a, b int) (total, sub int) {
total = a + b // 不需要重新定义了,直接使用=赋值即可
sub = a / b
return
}
func main() {
t, s := returnName(10, 34)
fmt.Println(t, s)
}
把函数当做参数传入到另一个函数
把某一个函数作为参数传入到另一个函数里面
// func1
package main
import (
"fmt"
)
type add_func func(int, int) int
func add(a, b int) int {
return a + b
}
func operator(op add_func, a, b int) int {
return op(a, b)
}
func main() {
c := add
fmt.Println(c)
su := c(10, 20)
fmt.Println(su)
ss := operator(add, 12, 30) // 把函数当做参数传入进去
fmt.Println(ss)
}
不定长参数
// func1
package main
import (
"fmt"
)
func add2(arg ...int) int {
s := 0
for _, v := range arg {
s += v
}
return s
}
func add3(a, b int, args ...int) int {
s := a + b
for _, v := range args {
s += v
}
return s
}
func addStr(a string, args ...string) string {
s := a
for _, v := range args {
s += v
}
return s
}
func main() {
fmt.Println(add2(1, 2, 3, 4, 5, 6, 10)) // 多个参数
fmt.Println(add3(2, 3, 111, 333, 444, 555)) // 传入2个或者多个参数
fmt.Println(addStr("asdf", "adfuwe", "123", "siw", "sdc")) // 传入一个或者多个参数
}
匿名函数
没有给这个函数命名的函数叫做匿名函数,代码例子如下:
func main() {
a := func() { // 直接把函数赋值给a,未对这个函数进行命名
fmt.Println("Func A")
}
a()
}
闭包
闭包的作用就是返回一个匿名函数,请看代码
func main() {
f := closure(10)
fmt.Println(f(1))
fmt.Println(f(2))
}
func closure(x int) func(int) int { // 实际上是这么写的func closure(x int) (func(int) int)
//只不过在编译的时候自动给你转换了,实际是说返回值是函数,int型的
return func(y int) int {
fmt.Printf("%p", &x)
return x + y
}
}
defer
- 它的执行方式类似于其他语言中的析构函数(python里面的__del__),在函数执行结束后按照调用顺序的相反顺序逐个执行
- 即使函数发生严重的错误也会执行
- 支持匿名函数的调用
- 常用语资源清理,文件关闭,解锁以及记录时间等操作
- 通过与匿名函数配合可在return之后修改函数计算结果
- 如果函数体内某个变量作为defer时匿名函数的参数,则在定义defer时即已经获得了拷贝,否则则是引用某个变量的地址
- Go没有异常机制,但是有panic/recover模式来处理错误
- Panic可以在任何地方引发,单recover只有在defer调用的函数中有效
- defer 语句的调用时遵照先进后出的原则,即最后一个defer语句就先被执行。只不过,当你需要为defer语句到底哪个先执行这种细节而烦恼的时候,说明你的代码结构可能需要调整一下了。
- 当函数返回时,执行defer语句。因此,可以用来做资源清理
- 多个defer语句,按先进后出的方式执行
- defer语句中的变量,在defer声明时就决定了。
defer代码例子
我们看下这个代码例子
func main() {
for i := 0; i < 3; i++ {
defer func() {
fmt.Println(i)
}()
}
}
// 打印结果:
3
3
3
为什么打印结果都是3呢,因为defer是最后执行的,在for循环结束后才执行,然而在for循环结束后i是等于3了,所以打印结果是3
defer与panic与recover的结合使用
- painc是触发异常的,当一个函数执行过程中调用panic()函数的时候,正常的函数执行流程立即终止,但函数中之前使用defer关键字延迟执行的语句将正常展开执行,之后该函数将返回到调用函数,并导致逐层向上执行panic流程,直至所属的goroutine中所有正在执行的函数终止。错误信息将被报告,包括在调用panic()函数时传入的参数,这个过程称为错误处理流程。
- recover()函数用于终止错误处理流程。一般情况下,recover()应该在一个使用defer关键字的函数中执行以有效截取错误处理流程。如果没有在发生异常的goroutine中明确调用恢复过程(使用recover关键字),会导致该goroutine所属的进程打印异常信息后直接退出
我们利用defer在panic触发异常的时候进行纠正,使程序依旧能够正常运行,代码如下:
// heh1
package main
import (
"fmt"
)
func main() {
A()
B()
C()
}
func A() {
fmt.Println("Func B")
}
func B() {
defer func() { // 必须在panic之前定义这个,否则会报错找不到
if err := recover(); err != nil {
fmt.Println("recover in B")
}
}()
panic("Func B")
}
func C() {
fmt.Println("Func C")
}
运行时选择函数
使用映射和函数引用来制造分支
我们如果需要对某个目录下的压缩包进行解压,压缩包的格式有很多种,有zip,gz,rar,tar,tar.gz等等之类的格式,针对这些后缀名,如果我们使用if条件去判断类型后再去执行代码,显然目前是可行的,代码效果如下:
if 后缀名=="zip" {
unzip 文件名
}else if 后缀名=="rar" {
解压文件
}else if 后缀名=="tar" {
解压文件
}else if 后缀名=="targ.gz" {
解压文件
}
显然针对几种文件类型使用if判断还是可以的,但是一旦文件类型成千上万的时候,你觉得if语句写下来那岂不是N多行了啊,这样的if语句你受到了吗?同样的switch语句也是一样,在面对很多条件的时候同样显得很臃肿,那么该如何使用?
其实我们可以考虑使用map的映射关系来解决,这样的话我们只需要考虑在map添加文件类型与对应的解压方法即可,不需要在写if/switch条件语句来判断了。
代码如下:
package main
import (
"fmt"
)
// 使用映射和函数引用来制造分支
var ff = map[string]func(string) ([]string, error){
".gz": GzipFile, ".zip": ZipFile, ".tar.gz": TarGz}
func GzipFile(file string) ([]string, error) {
fmt.Println(file)
xxx := []string{"a", "b"}
return xxx, nil
}
func TarGz(file string) ([]string, error) {
fmt.Println(file)
xxx := []string{"a", "c"}
return xxx, nil
}
func ZipFile(file string) ([]string, error) {
fmt.Println(file)
xxx := []string{"a", "b"}
return xxx, nil
}
func ss(file string) { // 总调用函数
if function, ok := ff[file]; ok {
function(file)
}
}
func main() {
ss(".tar.gz")
}
解释下:
- 首先我们需要定义一个map,map类型为 key为string,value为func(string) ([]string, error) .
- 由于我们定义好了map,所以我们下面所有对应的func函数必须是func(string) ([]string, error)这样的格式,否则会报错的。
- 定义好文件类型对应解压方法以后,我们就可以写一个总函数来通过map映射关系来调用对应的方法,这里 的总函数就是ss。
- ss函数里面左右各一个OK就可以通过if判断function是否有值了,function有值就可以调用对应的解压方法了。
动态函数创建
在运行时动态地选择函数的另一个场景便是,当我们有两个或者多个的函数实现了相同的功能时,比如使用了不同的算法等,我们不希望在程序编译时绑定到其中任何一个函数,例如允许我们动态地选择他们来做性能测试或者回归测试等。
举个例子:
//
package main
import (
"os"
)
var IsPalindrome func(string) bool //
func init() {
if len(os.Args) > 1 && (os.Args[1] == "-a" || os.Args[1] == "--ascii") {
os.Args = append(os.Args[:1], os.Args[2:]...)
IsPalindrome = func(s string) bool { // simple ascii version
if len(s) <= 1 {
return true
}
if s[0] != s[len(s)-1] {
return false
}
return IsPalindrome(s[1 : len(s)-1])
}
} else {
IsPalindrome = func(s string) bool { // UTF8 version
if len(s) <= 1 {
return true
}
if s[0] != s[len(s)-1] {
return false
}
return IsPalindrome(s[1 : len(s)-1])
}
}
}
func main() {
}
代码解释下:
- 我们根据输入的命令来决定IsPalindrome()的实现方式,如果指定了"-a"或者"--ascii"参数,就将它从os.Args切片移除然后创建一个作用于ASCII码的IsPalindrome的函数
- 如果没有输入ASCII选项,我们就创建一个和之前一样的函数,既能处理ASCII码,也可以UTF8码。
- 程序其他IsPalindrome函数可以正常被调用,但是实际上什么代码会被执行完全取决于我们创建的是哪个版本函数。
高阶函数
所谓高阶函数就是将一个或者多个其他函数作为自己的参数,并在函数里调用他们。可以看看下面最简单的例子:
func SliceIndex(limit int,predicate func(i int) bool) int{
for i:= 0;i < limit;i++{
if predicate(i) {
return i
}
}
return -1
}
上面的函数很简单,返回predicate()为真时的索引值。它只知道一个长度信息和它的第二个参数,也是是对于任意给定索引值返回一个布尔值的函数,表明这个索引是否调用者所期望的。下面看看如何调用这个函数
func main() {
xs := []int{1, 2, 3, 4}
fmt.Println(SliceIndex(len(xs), func(i int) bool { return xs[i] == 3 }))
}