对Go中的函数特性做一个总结。懂则看,不懂则算。
func myfunc(int,int)
。 type myfunc func(int,int) int
。
func(int,int) int
。所以,当函数ab()赋值给一个变量 ref_ab
时 ref_ab := ab
,不能再将其它函数类型的函数cd()赋值给变量 ref_ab
。 函数可以有0或多个参数,0或多个返回值,参数和返回值都需要指定数据类型,返回值通过return关键字来指定。
return可以有参数,也可以没有参数,这些返回值可以有名称,也可以没有名称。Go中的函数可以有多个返回值。
return a b
是正确的,但 return c=a b
是错误的 例如:
// 单个返回值 func func_a() int{ return a } // 只要命名了返回值,必须括号包围 func func_b() (a int){ // 变量a int已存在,无需再次声明 a = 10 return // 等价于:return a } // 多个返回值,且在return中指定返回的内容 func func_c() (int,int){ return a,b } // 多个返回值 func func_d() (a,b int){ return // 等价于:return a,b } // return覆盖命名返回值 func func_e() (a,b int){ return x,y }
Go中经常会使用其中一个返回值作为函数是否执行成功、是否有错误信息的判断条件。例如 return value,exists
、 return value,ok
、 return value,err
等。
当函数的返回值过多时,例如有4个以上的返回值,应该将这些返回值收集到容器中,然后以返回容器的方式去返回。例如,同类型的返回值可以放进slice中,不同类型的返回值可以放进map中。
但函数有多个返回值时,如果其中某个或某几个返回值不想使用,可以通过下划线 _
这个blank identifier来丢弃这些返回值。例如下面的 func_a
函数两个返回值,调用该函数时,丢弃了第二个返回值b,只保留了第一个返回值a赋值给了变量 a
。
func func_a() (a,b int){ return } func main() { a,_ := func_a() }
Go中是通过传值的方式传参的,意味着传递给函数的是拷贝后的副本,所以函数内部访问、修改的也是这个副本。
例如:
a,b := 10,20 min(a,b) func min(x,y int) int{}
上面调用min()时,是将a和b的值拷贝一份,然后将拷贝的副本赋值给变量x,y的,所以min()函数内部,访问、修改的一直是a、b的副本,和原始的数据对象a、b没有任何关系。
如果想要修改外部数据(即上面的a、b),需要传递指针。
例如,下面两个函数, func_value()
是传值函数, func_ptr()
是传指针函数,它们都修改同一个变量的值。
package main import "fmt" func main() { a := 10 func_value(a) fmt.Println(a) // 输出的值仍然是10 b := &a func_ptr(b) fmt.Println(*b) // 输出修改后的值:11 } func func_value(x int) int{ x = x 1 return x } func func_ptr(x *int) int{ *x = *x 1 return *x }
map、slice、interface、channel这些数据类型本身就是指针类型的,所以就算是拷贝传值也是拷贝的指针,拷贝后的参数仍然指向底层数据结构,所以修改它们 可能 会影响外部数据结构的值。
另外注意,赋值操作 b = a 1
这种类型的赋值也是拷贝赋值。换句话说,现在底层已经有两个数据对象,一个是a,一个是b。但 a = a 1
这种类型的赋值虽然本质上是拷贝赋值,但因为a的指针指向特性,使得结果上看是原地修改数据对象而非生成新数据对象。
有时候参数过多,或者想要让函数处理任意多个的参数,可以在函数定义语句的参数部分使用 ARGS...TYPE
的方式。这时会将 ...
代表的参数全部保存到一个名为ARGS的slice中,注意这些参数的数据类型都是TYPE。
...
在Go中称为variadic,在使用 ...
的时候(如传递、赋值),可以将它看作是一个slice,下面的几个例子可以说明它的用法。
例如: func myfunc(a,b int,args...int) int {}
。除了前两个参数a和b外,其它的参数全都保存到 名为args的slice中 ,且这些参数全都是int类型。所以,在函数内部就已经有了一个 args = []int{....}
的数据结构。
例如,下面的例子中,min()函数要从所有参数中找出最小的值。为了实验效果,特地将前两个参数a和b独立到slice的外面。min()函数内部同时会输出保存到args中的参数值。
package main import "fmt" func main() { a,b,c,d,e,f := 10,20,30,40,50,60 fmt.Println(min(a,b,c,d,e,f)) } func min(a,b int,args...int) int{ // 输出args中保存的参数 // 等价于 args := []int{30,40,50,60} for index,value := range args { fmt.Printf("%s%d%s %d\n","args[",index,"]:",value) } // 取出a、b中较小者 min_value := a if a>b { min_value = b } // 取出所有参数中最小值 for _,value := range args{ if min_value > value { min_value = value } } return min_value }
但上面代码中调用函数时传递参数的方式显然比较笨重。如果要传递的参数过多(要比较的值很多),可以先将这些参数保存到一个slice中,再传递slice给min()函数。传递slice给函数的时候,使用 SLICE...
的方式即可。
func main() { s1 := []int{30,40,50,60,70} fmt.Println(min(10,20,s1...)) }
上面的赋值方式已经能说明能使用slice来理解 ...
的行为。另外,下面的例子也能很好的解释:
func f1(s...string){ f2(s...) f3(s) } func f2(s...string){} func f3(s []string){}
如果各参数的类型不同,又想定义成变长参数,该如何?第一种方式,可以使用struct,第二种方式可以使用接口。接口暂且不说,如果使用struct,大概如下:
type args struct { arg1 string arg2 int arg3 type3 }
然后可以将args传递给函数: f(a,b int,args{})
,如果args结构中需要初始化,则 f(a,b int,args{arg1:"hello",arg2:22})
。
defer关键字可以让 函数或语句 延迟到函数语句块的最结尾时,即即将退出函数时执行,即便函数中途报错结束、即便已经panic()、即便函数已经return了,也都会执行defer所推迟的对象。
例如:
func main() { a() } func a() { println("in a") defer b() println("leaving a") //到了这里才会执行b() } func b() { println("in b") println("leaving b") }
上面将输出:
in a leaving a in b leaving b
即便是函数已经报错,或函数已经return返回,defer的对象也会在函数退出前的最后一刻执行。
func a() TYPE{ ...CODE... defer b() ...CODE... // 函数执行出了错误 return args // 函数b()都会在这里执行 }
但注意,由于Go的作用域采用的是词法作用域,defer的定义位置决定了它推迟对象能看见的变量值,而不是推迟对象被调用时所能看见的值。
例如:
package main var x = 10 func main() { a() } func a() { println("start a:",x) // 输出10 x = 20 defer b(x) x = 30 println("leaving a:",x) // 输出30 // 调用defer延迟的对象b(),输出20 } func b(x int) { println("start b:",x) }
如果语句块内有多个defer,则defer的对象以LIFO(last in first out)的方式执行,也就是说,先定义的defer后执行。
func main() { println("start...") defer println("1") defer println("2") defer println("3") defer println("4") println("end...") }
将输出:
start... end... 4 3 2 1
defer有什么用呢?一般用来做善后操作,例如清理垃圾、释放资源,无论是否报错都执行defer对象。另一方面,defer可以让这些善后操作的语句和开始语句放在一起,无论在可读性上还是安全性上都很有改善,毕竟写完开始语句就可以直接写defer语句,永远也不会忘记关闭、善后等操作。
例如,打开文件,关闭文件的操作写在一起:
open() defer file.Close() ... 操作文件 ...
以下是defer的一些常用场景:
panic()用于产生错误信息并终止 当前的goroutine ,一般将其看作是退出panic()所在函数以及退出调用panic()所在函数的函数。例如,G()中调用F(),F()中调用panic(),则F()退出,G()也退出。
注意,defer关键字推迟的对象是函数最后调用的,即使出现了panic也会调用defer推迟的对象。
例如,下面的代码中,main()中输出一个 start main
之后调用a(),它会输出 start a
,然后就panic了,panic()会输出 panic: panic in a
,然后报错,终止程序。
func main() { println("start main") a() println("end main") } func a() { println("start a") panic("panic in a") println("end a") }
执行结果如下:
start main start a panic: panic in a goroutine 1 [running]: main.a() E:/learning/err.go:14 0x63 main.main() E:/learning/err.go:8 0x4c exit status 2
注意上面的 end a
和 end main
都没有被输出。
可以使用recover()去捕获panic()并恢复执行。recover()用于捕捉panic()错误,并返回这个错误信息。但注意,即使recover()捕获到了panic(),但调用含有panic()函数的函数(即上面的G()函数)也会退出,所以如果recover()定义在G()中,则G()中调用F()函数之后的代码都不会执行(见下面的通用格式)。
以下是比较通用的panic()和recover()的格式:
func main() { G() // 下面的代码会执行 ...CODE IN MAIN... } func G(){ defer func (){ if str := recover(); str != nil { fmt.Println(str) } }() ...CODE IN G()... // F()的调用必须在defer关键字之后 F() // 该函数内下面的代码不会执行 ...CODE IN G()... } func F() { ...CODE1... panic("error found") // 下面的代码不会执行 ...CODE IN F()... }
可以使用recover()去捕获panic()并恢复执行。但以下代码是错误的:
func main() { println("start main") a() println("end main") } func a() { println("start a") panic("panic in a") // 直接放在panic后是错误的 panic_str := recover() println(panic_str) println("end a") }
之所以错误,是因为panic()一出现就直接退出函数a()和main()了。要想recover()真正捕获panic(),需要将recover()放在defer的推迟对象中,且defer的定义必须在panic()发生之前。
例如,下面是通用格式的示例:
package main import "fmt" func main() { println("start main") b() println("end main") } func a() { println("start a") panic("panic in a") println("end a") } func b() { println("start b") defer func() { if str := recover(); str != nil { fmt.Println(str) } }() a() println("end b") }
以下是输出结果:
start main start b start a panic in a end main
注意上面的 end b
、 end a
都没有被输出,但是 end main
输出了。
panic()是内置的函数(在包builtin中),在 log
包中也有一个Panic()函数,它调用Print()输出信息后,再调用panic()。 go doc log Panic
一看便知:
$ go doc log Panic func Panic(v ...interface{}) Panic is equivalent to Print() followed by a call to panic().
在builtin包中有一些内置函数,这些内置函数额外的导入包就能使用。
有以下内置函数:
$ go doc builtin | grep func func close(c chan<- Type) func delete(m map[Type]Type1, key Type) func panic(v interface{}) func print(args ...Type) func println(args ...Type) func recover() interface{} func complex(r, i FloatType) ComplexType func imag(c ComplexType) FloatType func real(c ComplexType) FloatType func append(slice []Type, elems ...Type) []Type func make(t Type, size ...IntegerType) Type func new(Type) *Type func cap(v Type) int func copy(dst, src []Type) int func len(v Type) int
close
用于关闭channel delete
用于删除map中的元素 copy
用于拷贝slice append
用于追加slice cap
用于获取slice的容量 len
用于获取
print
和 println
:底层的输出函数,用来调试用。在实际程序中,应该使用fmt中的print类函数 complex
、 imag
、 real
:操作复数(虚数) panic
和 recover
:处理错误 new
和 make
:分配内存并初始化
v := new(int)
注意,地址和指针是不同的。地址就是数据对象在内存中的地址,指针则是占用一个机器字长(32位机器是4字节,64位机器是8字节)的数据,这个数据中存储的是它所指向数据对象的地址。
a -> AAAA b -> Pointer -> BBBB
函数内部调用函数自身的函数称为递归函数。
使用递归函数最重要的三点:
例如,递归最常见的示例,求一个给定整数的阶乘。因为阶乘的公式为 n*(n-1)*...*3*2*1
,它在参数为1的时候退出函数,也就是说它的递归基点是1,所以对是否为基点进行判断,然后再写递归表达式。
package main import "fmt" func main() { fmt.Println(a(5)) } func a(n int) int{ // 判断退出点 if n == 1 { return 1 } // 递归表达式 return n * a(n-1) }
它的调用过程大概是这样的:
再比如斐波那契数列,它的计算公式为 f(n)=f(n-1) f(n-2)
且 f(2)=f(1)=1
。它在参数为1和2的时候退出函数,所以它的退出点为1和2。
package main import "fmt" func main() { fmt.Println(f(3)) } func f(n int) int{ // 退出点判断 if n == 1 || n == 2 { return 1 } // 递归表达式 return f(n-1) f(n-2) }
如何递归一个目录?它的递归基点是文件,只要是文件就返回,只要是目录就进入。所以,伪代码如下:
func recur(dir FILE) FILE{ // 退出点判断 if (dir is a file){ return dir } // 当前目录的文件列表 file_slice := filelist() // 遍历所有文件 for _,file := range file_slice { return recur(file) } }
匿名函数是没有名称的函数。一般匿名函数嵌套在函数内部,或者赋值给一个变量,或者作为一个表达式。
定义的方式:
// 声明匿名函数 func(args){ ...CODE... } // 声明匿名函数并直接执行 func(args){ ...CODE... }(parameters)
下面的示例中,先定义了匿名函数,将其赋值给了一个变量,然后在需要的地方再去调用执行它。
package main import "fmt" func main() { // 匿名函数赋值给变量 a := func() { fmt.Println("hello world") } // 调用匿名函数 a() fmt.Printf("%T\n", a) // a的type类型:func() fmt.Println(a) // 函数的地址 }
如果给匿名函数的定义语句后面加上 ()
,表示声明这个匿名函数的同时并执行:
func main() { msg := "Hello World" func(m string) { fmt.Println(m) }(msg) }
其中 func(c string)
表示匿名函数的参数, func(m string){}(msg)
的 msg
表示传递msg变量给匿名函数,并执行。
可以将func作为一种type,以后可以直接使用这个type来定义函数。
package main import "fmt" type add func(a,b int) int func main() { var a add = func(a,b int) int{ return a b } s := a(3,5) fmt.Println(s) }
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!