1.函数
-
Go语言中支持函数,匿名和闭包,并且函数在Go语言中属于“一等公民”
-
特点:
• 无需声明原型。 • 支持不定 变参。 • 支持多返回值。 • 支持命名返回参数。 • 支持匿名函数和闭包。 • 函数也是一种类型,一个函数可以赋值给变量。 • 不支持 嵌套 (nested) 一个包不能有两个名字一样的函数。 • 不支持 重载 (overload) • 不支持 默认参数 (default parameter)。
1函数的定义
-
函数声明包含一个函数名,参数列表, 返回值列表和函数体。如果函数没有返回值,则返回列表可以省略。函数从第一条语句开始执行,直到执行return语句或者执行函数的最后一条语句。函数可以没有参数或接受多个参数。(注意类型在变量名之后 )当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他都可以省略。函数可以返回任意数量的返回值。
-
Go语言中定义函数使用func 关键字,具体格式如下:
func 函数名(参数)(返回值){ 函数体 }
-
其中:
- 函数名:由字母,数字,下划线组成,但函数名的第一个字母不能是数字,在同一个包内,函数名称不能重名
- 参数:参数由参数变量和参数变量的类型,多个参数之间使用,分隔
- 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值,必须用()包裹,并用
,
分隔。 - 函数体:实现指定功能的代码块。
-
定义一个简单函数
package main import "fmt" //函数 //定义一个不需要参数也没有返回值的函数:sayHello func sayHello() { fmt.Println("hello xjk") } func main() { //函数调用 sayHello() }
-
函数作为参数传递,可以将复杂签名定义为函数类型,以便于阅读
package main import "fmt" func test(fn func() int) int { return fn() } // 定义函数类型 type FormatFunc func(s string,x,y int) string // format接收fn 的类型为为一个函数类型,通过type定义一个函数类型更直观 func format(fn FormatFunc,s string, x,y int) string { return fn(s,x,y) } func main() { // test 内部为一个匿名函数 s1 := test(func() int { return 100})//直接将匿名函数当参数 // 第一个参数为匿名函数,定义接收类型,返回类型和函数逻辑。 // 第二个参数 "%d,%d" 字符串 // 第三个,第四个 参数为数字 s2 := format(func(s string, x, y int) string { return fmt.Sprintf(s, x, y) }, "%d, %d", 10, 20) println(s1,s2) // 100 10, 20 }
2.函数参数
-
函数定义时指出,函数定义时有参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。
但当调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数:
-
值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
func swap(x,y int) int { ... ... }
-
引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
func swap(x,y *int){ *x,*y = *y,*x } func main() { var a, b int = 1, 2 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个或多个参数 } func add(a int, b int, args…int) int { //2个或多个参数 }
注意:其中args是一个slice,我们可以通过arg[index]依次访问所有参数,通过len(arg)来判断传递参数的个数。
任意类型的不定参数: 就是函数的参数和每个参数的类型都不是固定的。
用interface{}传递任意类型数据是Go语言的惯例用法,而且interface{}是类型安全的。
func myfunc(args ...interface{}) { }
- demo
func test(s string, n...int) string { var x int for _,i := range n{ x += i } return fmt.Sprintf(s,x) } func main() { println(test("sum:%d",1,2,3)) } // sum:6
-
使用 slice 对象做变参时,必须展开。
(slice...)
func test(s string, n...int) string { var x int for _,i := range n{ x += i } return fmt.Sprintf(s,x) } func main () { s := []int{1,2,3} // s... 将slice展开 res := test("sum:%d",s...) println(res) }
3.函数返回值
-
可以用
"_"
标识符,用来忽略函数的某个返回值。Go 的返回值可以被命名,并且就像在函数体开头声明的变量那样使用。返回值的名称应当具有一定的意义,可以作为文档使用。没有参数的 return 语句返回各个返回变量的当前值。这种用法被称作“裸”返回。直接返回语句仅应当用在像下面这样的短函数中。在长的函数中它们会影响代码的可读性。 -
demo
func add(a,b int) (c int) { c = a + b return } // 指定返回的sum和avg类型,所以return 后面可以不接变量 func calc(a,b int) (sum int,avg int) { sum = a + b avg = (a + b) / 2 return } func main() { var a,b int = 1,2 c := add(a,b) sum,avg := calc(a,b) fmt.Println(a,b,c,sum,avg) } // 1 2 3 3 1
-
Golang返回值不能用容器对象接收多返回值。只能用多个变量,或
"_"
忽略。func test() (int, int) { return 1,2 } func main() { x,_ := test() println(x)//1 }
-
多返回值可直接作为其他函数调用实参。
func test()(int,int) { return 1,2 } func add(a,b int) int { return a+b } func sum(n ...int) int{ var x int for _,i := range n{ x += i } return x } func main() { fmt.Println(add(test()))//3 fmt.Println(sum(test()))//3 }
-
命令返回参数可看作与形参类似的局部变量,最后由return隐式返回。
package main func add(x,y int) (z int) { z = x + y return } func main() { println(add(1,2))//3 }
-
命名返回参数可被同名局部变量遮蔽,此时需要显示返回。
func add(x,y int)(z int){ // var z不能再一个级别重复定义,会引发"z redeclared in this block" 错误 var z = x+y // 必须显式返回 return z }
-
命名返回参数允许defer延迟调用通过闭包读取和修改
func add(a,b int) (c int) { defer func() { c += 100 }() c = a + b return } func main() { println(add(1, 2)) //103 }
-
显示return返回前,会先修改命名返回参数。
func add(x, y int) (z int) { defer func() { println("defer:",z) // 输出: 203 }() z = x + y return z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (return) } func main() { println(add(1,2)) } // defer: 203 // 203
4.匿名函数
-
匿名函数是指不需要定义函数名的一种函数实现方式。1958年LISP首先采用匿名函数。
在Go里面,函数可以像普通变量一样被传递或使用,Go语言支持随时在代码里定义匿名函数。
匿名函数由一个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。
package main import ( "fmt" "math" ) func main() { getSqrt := func(a float64) float64 { // 开平方根 return math.Sqrt(a) } fmt.Println(getSqrt(4))//2 }
上面先定义了一个名为getSqrt 的变量,初始化该变量时和之前的变量初始化有些不同,使用了func,func是定义函数的,可是这个函数和上面说的函数最大不同就是没有函数名,也就是匿名函数。这里将一个函数当做一个变量一样的操作。
-
Golang匿名函数可赋值给变量,做为结构字段,或者在 channel 里传送。
package main import ( "fmt" // "math" ) func main() { // 复制变量通过() 调用 fn := func() { fmt.Println("Hello World")} fn() // 切片定义多个函数 fn2 := [](func(x int) int){ func(x int) int {return x+1}, func(x int) int {return x+2}, } // 通过索引调用函数 println(fn2[0](100)) // 结构体内定义函数 d := struct { fn func() string }{ fn: func() string {return "hello world!"}, } // 通过点语法调用 println(d.fn()) // 再channel里传送 fc := make(chan func() string,2) fc <- func() string {return "Hello,Go!"} println((<-fc)()) } // Hello World // 101 // hello world! // Hello,Go
5.闭包与递归
-
由函数及其相关引用环境组合而成的实体(闭包=函数+引用环境)
-
Go语言支持闭包的,这里只是简单地讲一下在Go语言中闭包是如何实现的
package main import "fmt" func a() func() int{ i:= 0 b := func() int { i++ fmt.Println(i) return i } return b } func main() { c := a() c()//1 c()//2 // c = a() 调用a() 不会输出 i 的值 }
-
因闭包复制时原对象指针,这样很容易解释延迟引用现象。
func test() func() { x := 100 fmt.Printf("x (%p) = %d ", &x, x) return func() { fmt.Printf("inner x (%p) = %d ", &x, x) } } func main() { f := test() f() // x (0x1100a090) = 100 // inner x (0x1100a090) = 100 }
在汇编层 ,test 实际返回的是 FuncVal 对象,其中包含了匿名函数地址、闭包对象指针。当调 匿名函数时,只需以某个寄存器传递该对象即可。
FuncVal { func_address, closure_var_pointer ... }
-
外部引用函数参数局部变量。
package main import "fmt" // 外部引用函数参数局部变量 func add(base int) func(int) int { return func(i int) int { base += i return base } } func main() { tmp1 := add(10) fmt.Println(tmp1(1), tmp1(2)) // 此时tmp1和tmp2不是一个实体了 tmp2 := add(100) fmt.Println(tmp2(1), tmp2(2)) }
-
返回2个闭包
func test(base int) (func(int) int, func(int) int ){ add := func(i int) int { base += i return base } sub := func(i int) int { base -= i return base } return add,sub } func main() { f1,f2 := test(1) // base一直是没有消 // f1执行完base为11,f2执行会用base=11在函数进行运算,所以为-4 fmt.Println(f1(10),f2(15))// 11 -4 }
-
递归函数
-
递归,就是在运行的过程中调用自己。 一个函数调用自己,就叫做递归函数。
-
条件:
1.子问题须与原始问题为同样的事,且更为简单。 2.不能无限制地调用本身,须有个出口,化简为非递归状况处理。
-
数字阶乘
- 一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1。自然数n的阶乘写作n!。
package main import "fmt" func factorial(i int) int { if i<=1{ return 1 } return i * factorial(i-1) } func main() { var i int = 10 fmt.Printf("Factorial of %d is %d ",i,factorial(i))// Factorial of 10 is 3628800 }
-
斐波那契数列
func fibonaci(i int) int { if (i ==0) { return 0 } if (i == 1) { return 1 } return fibonaci(i-1) + fibonaci(i-2) } func main() { var i int for i = 0; i < 10; i++ { fmt.Printf("%d ", fibonaci(i)) } } // 0 // 1 // 1 // 2 // 3 // 5 // 8 // 13 // 21 // 34
-
6.延迟调用 defer
-
defer特性:
1.关键字 defer 用于注册延迟调用 2.这些调用直到return 前才被执行,因此可以用来做资源清理。 3.多个defer语句,按先进后出的方式执行。 4.defer语句中的变量,在defer声明时就决定了。
-
defer用途:
1.关闭文件句柄 2.锁资源释放 3.数据库连接释放
-
Go语言中defer语句会将其后面跟随的语句进行延迟处理,在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆顺序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句最先被执行。
package main import "fmt" func main() { var w [5] struct{} for i := range w{ defer fmt.Println(i) } } // 4 // 3 // 2 // 1 // 0
-
defer碰上闭包
package main import "fmt" func main() { var w [5] struct{} for i := range w{ defer func() {fmt.Println(i)}() } } // 4 // 4 // 4 // 4 // 4
函数正常执行,由于闭包用到的变量 i 在执行的时候已经变成4,所以输出全都是4.
-
defer f.Close 存在陷阱
type Test struct { name string } func (t *Test) Close() { fmt.Println(t.name," closed") } func main() { ts := []Test{{"a"},{"b"},{"c"}} for _,t := range ts { defer t.Close() } } // c closed // c closed // c closed
上面代码不如我们预期输出的c b a 而是c c c
-
可是按照前面go sepc中说明应该c b a 才对,那么换一种方式调用
type Test struct { name string } func (t *Test) Close() { fmt.Println(t.name," closed") } func Close(t Test){ t.Close() } func main() { ts := []Test{{"a"},{"b"},{"c"}} for _,t := range ts { defer Close(t) } } // c closed // b closed // a closed
Go语言中并没有把这个struct的this指针做相应处理,没有明确写出来的this指针当作参数看待。而通过定义Close(t Test) 函数讲当前循环struct传入禁区,然后Test结构体内的方法。
-
多个defer注册,按FILO次序(先进后出),最后哪怕函数或某个延迟调用发生错误,也会调用依旧被执行。
func test(x int) { defer println("a") defer println("b") defer func() { println(100/x)// 这里传入0,会报错,异常未被捕获,逐步往外传递,最终终止进程。 }() defer println("c") } func main() { test(0) } // c // b // a // panic: runtime error: integer divide by zero
-
延迟调用参数在注册时求值或复制,可用指针或闭包“延迟”读取。
package main import "fmt" func test() { x,y := 10,20 defer func(i int) { fmt.Println("defer:",i,y)// y在内层函数,引用外层变量 }(x) //此时x为值拷贝 x+=10 y+=100 fmt.Println("x=",x,"y=",y) } func main() { test() } // x= 20 y= 120 // defer: 10 120
-
滥用defer可能会导致性能问题,尤其是在大循环里:
package main import ( "fmt" "sync" "time" ) // 声明 sync.Mutex互斥锁 var lock sync.Mutex func test() { // 加锁 lock.Lock() // 解锁 lock.Unlock() } func testdefer() { lock.Lock() defer lock.Unlock() } func main() { func() { t1 := time.Now() for i:=0;i<100000;i++ { test() } // 计算程序运行时间 elapsed := time.Since(t1) fmt.Println("test elapsed: ",elapsed) }() func() { t1 := time.Now() for i:=0;i<100000;i++ { testdefer() } // 计算程序运行时间 elapsed := time.Since(t1) fmt.Println("testdefer elapsed: ",elapsed) }() } // test elapsed: 1.9929ms // testdefer elapsed: 6.0194ms
-
defer 的陷阱
- defer 与 closure(闭包),如果 defer 后面跟的不是一个 closure 最后执行的时候我们得到的并不是最新的值。
package main import ( "errors" "fmt" ) func foo(a, b int) (i int, err error) { defer fmt.Printf("first defer err %v ", err) //第二个defer值拷贝。 defer func(err error) { fmt.Printf("second defer err %v ", err) }(err) defer func() { fmt.Printf("third defer err %v ", err) }() if b == 0 { err = errors.New("divided by zero!") return } i = a / b return } func main() { foo(2, 0) } // third defer err divided by zero! // second defer err <nil> // first defer err <nil>
-
defer和return
func foo() (i int) { i = 0 defer func() { fmt.Println(i) }() return 2 } func main() { foo()//2 }
在有具名返回值的函数中(这里具名返回值为i),执行return 2的时候实际上已经将i的值重新赋值为2,所以 defer closure输出结构为2,而不是1
-
defer nil 函数
package main import "fmt" //Recover 是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,recover 仅在延迟函数 defer 中有效,在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,如果当前的 goroutine 陷入恐慌,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。 func test() { // 函数指向空地址 var run func() = nil defer fun() fmt.Println("runs...") } func main() { defer func() { // recover捕捉panic输入值 if err:= recover()l err != nil { fmt.Println(err) } }() test() } // runs // runtime error: invalid memory address or nil pointer dereference
解释:名为 test 的函数一直运行至结束,然后 defer 函数会被执行且会因为值为 nil 而产生 panic 异常。然而值得注意的是,run() 的声明是没有问题,因为在test函数运行完成后它才会被调用。
-
错误的位置使用defer
package main import ( "net/http" "fmt" ) func do() error{ //当 http.Get 失败时会抛出异常。 res,err := http.Get("http://www.google.com") defer res.Body.Close() if err != nil { return err } return nil } func main() { do() }
因为在这里我们并没有检查我们的请求是否成功执行,当它失败的时候,我们访问了 Body 中的空变量 res ,因此会抛出异常
-
改进:
- 总是在一次成功的资源分配下面使用 defer ,对于这种情况来说意味着:当且仅当 http.Get 成功执行时才使用 defer
package main import ( "net/http" "fmt" ) func do() error{ //当 http.Get 失败时会抛出异常。 res,err := http.Get("http://www.baidudddsfw45fgsdfsfds.com") // <nil> Get http://www.baidudddsfw45fgsdfsfds.com: dial tcp: lookup www.baidudddsfw45fgsdfsfds.com: no such host fmt.Println(res,err) if res != nil { defer res.Body.Close() } if err != nil { return err } return nil } func main() { do() }
解释:在这里,你同样需要检查 res 的值是否为 nil ,这是 http.Get 中的一个警告。通常情况下,出错的时候,返回的内容应为空并且错误会被返回,可当你获得的是一个重定向 error 时, res 的值并不会为 nil ,但其又会将错误返回。上面的代码保证了无论如何 Body 都会被关闭,如果你没有打算使用其中的数据,那么你还需要丢弃已经接收的数据。
-
defer 文件操作:
package main import "os" func do() (err error) { f,err := os.Open("book.txt") println(f,err) if err != nil { println(err) return err } if f != nil { println("f-->",f) defer func() { if ferr:= f.Close();ferr != nil { err = ferr } }() } // code... return nil } func main() { do() }
f.Close() 可能会返回一个错误,可这个错误会被我们忽略掉,通过命名的返回变量来返回 defer 内的错误。
-
释放相同的资源
package main import ( "fmt" "io" "os" ) func do() error { f, err := os.Open("book.txt") if err != nil { return err } if f != nil { defer func(f io.Closer) { if err := f.Close(); err != nil { fmt.Printf("defer close book.txt err %v ", err) } }(f) } // ..code... f, err = os.Open("another-book.txt") if err != nil { return err } if f != nil { defer func(f io.Closer) { if err := f.Close(); err != nil { fmt.Printf("defer close another-book.txt err %v ", err) } }(f) } return nil } func main() { do() }
如果你尝试使用相同的变量释放不同的资源,那么这个操作可能无法正常执行。当延迟函数执行时,只有最后一个变量会被用到,因此,f 变量 会成为最后那个资源 (another-book.txt)。而且两个 defer 都会将这个资源作为最后的资源来关闭
7.异常处理
-
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 语句会被最先调用。
-
demo
package main func main() { test() } func test() { defer func() { if err:=recover();err != nil { // fmt.Printf("%T ",err) println(err.(string))// 将 interface{} 转型为具体类型。 } }() panic("panic error!") } // panic error!
-
panic,recover 参数类型为interface{} ,因此可抛出任何类型对象。
func panic(v interface{}) func recover() interface{}
-
向已经关闭的通道发送数据会引发panic
func main() { defer func() { // recover捕获异常 if err := recover(); err != nil { // 打印异常 fmt.Println(err)//send on closed channel } }() var ch chan int = make(chan int,10) // 关闭channel close(ch) // 向channel 传值 ch <- 1 }
-
延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。
func test() { // 捕获最后一个错误。 defer func() { fmt.Println(recover())//defer panic }() defer func() { panic("defer panic") }() panic("test panic") }
-
捕获函数 recover 只有在延迟调用内直接调用才会终止错误,否则总是返回 nil。任何未捕获的错误都会沿调用堆栈向外传递。
func test() { defer func() { fmt.Println("-->",recover()) //无效 }() defer recover() //无效! defer fmt.Println(recover()) //无效! defer func() { func() { println("defer inner") recover() //无效! }() }() defer func() { fmt.Println("---->",recover()) //有效 }() panic("test panic") } // ----> test panic // defer inner // <nil> // --> <nil>
-
使用延迟匿名函数或下面这样都是有效的。
package main import ( "fmt" ) func except() { fmt.Println(recover())//test panic } func test() { defer except() panic("test panic") } func main() { test() }
-
如果需要保护代码 段,可将代码块重构成匿名函数,如此可确保后续代码被执
package main import "fmt" func test(x,y int) { var z int func() { // 捕获异常 defer func() { if recover() != nil{ z = 0 } }() // 触发异常 panic("test panic") z = x/y return }() fmt.Printf("x / y = %d ", z)//x / y = 0 } func main() { test(1,2) }
-
除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态。
type error interface { Error() string }
-
标准库 errors.New 和 fmt.Errorf 函数用于创建实现 error 接口的错误对象。通过判断错误对象实例来确定具体错误类型。
package main import ( "fmt" "errors" ) // 定义异常对象 var ErrDivZero = errors.New("division by zero") func div(x,y int) (int, error) { if y == 0{ return 0,ErrDivZero } return x/y,nil } func main() { defer func(){ fmt.Println(recover())//division by zero }() switch z,err := div(10,0);err { case nil: println(z) case ErrDivZero: panic(err) } }
-
Go实现try catch的异常
package main import "fmt" func Try(fun func(),handler func(interface{})) { defer func() { // 执行defer,通过recover捕获异常赋值给err. // 执行第二个参数的函数,将err传递。 if err:= recover();err!=nil{ handler(err) } }() // 执行第一个参数的函数,触发异常 fun() } func main() { // 传入匿名函数: // 1.函数定义触发错误 // 2.函数用于打印异常 Try(func() { panic("test panic") },func(err interface{}){ fmt.Println(err)// test panic }) }
如何区别使用 panic 和 error 两种方式?
惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。