一、函数特点
- 无需声明原型。
- 支持不定 变参。
- 支持多返回值。
- 支持命名返回参数。
- 支持匿名函数和闭包。
- 函数也是一种类型,一个函数可以赋值给变量。
- 不支持 嵌套 (nested) 一个包不能有两个名字一样的函数。
- 不支持 重载 (overload)
- 不支持 默认参数 (default parameter)。
二、函数声明
函数声明包含一个函数名,参数列表, 返回值列表和函数体。如果函数没有返回值,则返回列表可以省略。函数从第一条语句开始执行,直到执行return语句或者执行函数的最后一条语句。函数可以没有参数或接受多个参数。
语法
func function_name( [parameter list] ) [return_types] {
函数体
}
栗子
func main() {
a, b := test(1, 2, "sum")
fmt.Println("a,b : ", a, b)
dome()
}
func test(x, y int, s string) (int, string) {
// 类型相同的相邻参数,参数类型可合并。 多返回值必须用括号。
n := x + y
return n, s
}
func dome() {
fmt.Println("dome")
}
注意类型在变量名之后 。
当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他都可以省略。函数可以返回任意数量的返回值。使用关键字 func 定义函数,左大括号依旧不能另起一行。
package main
import "fmt"
func test(fn func() int) int {
return fn()
}
// 定义函数类型。
type FormatFunc func(s string, x, y int) string
func format(fn FormatFunc, s string, x, y int) string {
return fn(s, x, y)
}
func main() {
s1 := test(func() int { return 100 }) // 直接将匿名函数当参数。
s2 := format(func(s string, x, y int) string {
return fmt.Sprintf(s, x, y)
}, "%d, %d", 10, 20)
println(s1)
println(s2)
}
100
10,20
三、参数
函数参数
函数定义时指出,函数定义时有参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。
但当调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数:
值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
func swap(x, y int) int {
// ... ...
}
引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
package main
import (
"fmt"
)
/* 定义相互交换值的函数 */
func swap(x, y *int) {
var temp int
temp = *x /* 保存 x 的值 */
*x = *y /* 将 y 值赋给 x */
*y = temp /* 将 temp 值赋给 y*/
}
func main() {
var a, b int = 1, 2
/* 调用 swap() 函数
&a 指向 a 指针,a 变量的地址
&b 指向 b 指针,b 变量的地址
*/
swap(&a, &b)
fmt.Println(a, b)
}
2 1
在默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。
注意1:无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。
注意2:map、slice、chan、指针、interface默认以引用的方式传递。
不定参数传值 就是函数的参数不是固定的,后面的类型是固定的。(可变参数)
Golang 可变参数本质上就是 slice。只能有一个,且必须是最后一个。
在参数赋值时可以不用用一个一个的赋值,可以直接传递一个数组或者切片,特别注意的是在参数后加上“…”即可
func myfunc(args ...int) { //0个或多个参数
}
func add(a int, args…int) int { //1个或多个参数
}
栗子
package main
import "fmt"
func main() {
fmt.Println(test(1, 2, 3, 4))
}
func test(args ...int) string {
return fmt.Sprintf("args : %d", args)
}
注意:其中args是一个slice,我们可以通过args[index]依次访问所有参数,通过len(arg)来判断传递参数的个数.
任意类型的不定参数: 就是函数的参数和每个参数的类型都不是固定的。
用interface{}传递任意类型数据是Go语言的惯例用法,而且interface{}是类型安全的。
func myfunc(args ...interface{}) {
}
返回值
** "_"标识符,用来忽略函数的某个返回值 **
Go 的返回值可以被命名,并且就像在函数体开头声明的变量那样使用。返回值的名称应当具有一定的意义,可以作为文档使用。没有参数的 return 语句返回各个返回变量的当前值。这种用法被称作“裸”返回。直接返回语句仅应当用在像下面这样的短函数中。在长的函数中它们会影响代码的可读性。
Golang返回值不能用容器对象接收多返回值。只能用多个变量,或 “_” 忽略。
package main
import (
"fmt"
)
func calc(a,b int) (num,avg int) {
num = a + b
avg = (a+b)/2
return
}
func main() {
var a, b int = 1, 2
_,avg := calc(a,b)
fmt.Println(avg)
}
1
四、匿名与闭包
匿名函数
在Go里面,函数可以像普通变量一样被传递或使用,Go语言支持随时在代码里定义匿名函数。
匿名函数由一个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。
n := func(a int) int {
a++
return a
}
fmt.Println(n)
0x187740
碰巧匿名函数与上了切片
fns := [](func(x int) int){ //(func(x int) int) 匿名函数类型
func(x int) int {return x+1}, //0
func(x int) int {return x+2}, //1
}
fmt.Println(fns[1](10))
12
闭包
一个令人熟悉的闭包
<?php
function test() {
return function () {
var_dump("闭包");
};
}
test()();
?>
go闭包操作
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
te := adder()
for i:=0;i<5;i++{
fmt.Println(te(i))
}
}
0
1
3
6
10
作用域问题
func a() func(){
i := 0
b := func() {
i++
fmt.Println(i)
}
return b
}
func main(){
c := a()
c()
c()
c()
}
1
2
3
解释:
当函数a()的内部函数b()被函数a()外的一个变量引用的时候,就创建了一个闭包。 在上面的例子中,由于闭包的存在使得函数a()返回后,a中的i始终存在,这样每次执行c(),i都是自加1后的值。 从上面可以看出闭包的作用就是在a()执行完并返回后,闭包使得Javascript的垃圾回收机制GC不会收回a()所占用的资源,因为a()的内部函数b()的执行需要依赖a()中的变量i。
在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。变量的作用域仅限于包含它们的函数,因此无法从其它程序代码部分进行访问。不过,变量的生存期是可以很长,在一次函数调用期间所创建所生成的值在下次函数调用时仍然存在。正因为这一特点,闭包可以用来完成信息隐藏,并进而应用于需要状态表达的某些编程范型中。 下面来想象另一种情况,如果a()返回的不是函数b(),情况就完全不同了。因为a()执行完后,b()没有被返回给a()的外界,只是被a()所引用,而此时a()也只会被b()引 用,因此函数a()和b()互相引用但又不被外界打扰(被外界引用),函数a和b就会被GC回收。所以直接调用a();是页面并没有信息输出。
下面来说闭包的另一要素引用环境。c()跟c2()引用的是不同的环境,在调用i++时修改的不是同一个i,因此两次的输出都是1。函数a()每进入一次,就形成了一个新的环境,对应的闭包中,函数都是同一个函数,环境却是引用不同的环境。这和c()和c()的调用顺序都是无关的。
五、延迟调用(defer)
介绍defer
defer 特性
- 关键字 defer 用于注册延迟调用。
- 这些调用直到 return 前才被执。因此,可以用来做资源清理。
- 多个defer语句,按先进后出的方式执行。
- defer语句中的变量,在defer声明时就决定了。
defer用途
- 关闭文件句柄
- 锁资源释放
- 数据库连接释放
go 语言的defer功能强大,对于资源管理非常方便,但是如果没用好,也会有陷阱。
defer 是先进后出
这个很自然,后面的语句会依赖前面的资源,因此如果先前面的资源先释放了,后面的语句就没法执行了。
测试
func main() {
def()
}
func def() {
defer func() {
fmt.Println("延迟输出")
}()
fmt.Println("正常输出")
}
正常输出
延迟输出
defer 与 结构体
这个大家用的都很频繁,但是go语言编程举了一个可能一不小心会犯错的例子.
package main
import "fmt"
type Asd struct {
name string
}
func (t *Asd) Aaa() {
fmt.Println(t.name)
}
func main() {
tt := []Asd{
{"a"},
{"b"},
{"c"},
}
for _,val := range tt{
defer val.Aaa()
}
}
c
c
c
这个输出并不会像我们预计的输出c b a,而是输出c c c可是按照前面的go spec中的说明,应该输出c b a才对啊.
那我们换一种方式来调用一下.
package main
import "fmt"
type Asd struct {
name string
}
func (t *Asd) Aaa() {
fmt.Println(t.name)
}
func demo(t Asd) {
t.Aaa()
}
func main() {
tt := []Asd{
{"a"},
{"b"},
{"c"},
}
for _,val := range tt{
defer demo(val)
}
}
c
b
a
这个时候输出的就是c b a当然,如果你不想多写一个函数,也很简单,可以像下面这样,同样会输出c b a 看似多此一举的声明
package main
import "fmt"
type Asd struct {
name string
}
func (t *Asd) Aaa() {
fmt.Println(t.name)
}
func main() {
tt := []Asd{
{"a"},
{"b"},
{"c"},
}
for _,val := range tt{
v := val
defer v.Aaa()
}
}
c
b
a
通过以上例子,结合
Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usualand saved anew but the actual function is not invoked.
这句话。可以得出下面的结论:
多个 defer 注册,按 FILO 次序执行 ( 先进后出 )。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
func ww(x int) {
defer fmt.Println("q")
defer fmt.Println("w")
defer func() {
fmt.Println(100/x)
}()
defer fmt.Println("e")
}
ww(0)
e
w
q
panic: runtime error: integer divide by zero
六、异常处理
Golang 没有结构化异常,使用 panic 抛出错误,recover 捕获错误。
异常的使用场景简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。
panic:
1、内置函数
2、假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
3、返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
4、直到goroutine整个退出,并报告错误
recover:
1、内置函数
2、用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为
3、一般的调用建议
a). 在defer函数中,通过recever来终止一个goroutine的panicking过程,从而恢复正常代码的执行
b). 可以获取通过panic传递的error
注意:
1.利用recover处理panic指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。
2.recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
3.多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。
栗子:
defer func() {
if err := recover(); err != nil {
fmt.Println(err.(string))
}
}()
panic("panic error !")
panic error !
底层recover、panic都是interface{}类型,所以可以跑出任何异常对象
func panic(v interface{})
func recover() interface{}
如果defer也需要抛出异常咋办?
defer func() {
if err := recover(); err != nil {
fmt.Println(err.(string))
}
}()
defer func() {
panic("defer panic error !")
}()
panic("panic error !")
defer panic error !
我们也可以实现一个go类型try
/**
* fun 正常的代码执行的方法
*
* handler 出现异常执行的操作
*/
func Try(fun func(), handler func(interface{})) {
defer func() {
if err := recover(); err != nil {
handler(err)
}
}()
fun()
}
func main() {
Try(func() {
panic("test try panic")
}, func(err interface{}) {
fmt.Println(err)
})
}
下一篇:go设计模式 5-1
上一篇:结构体和面对对象 3