Go语言基础 -- 函数编程

1. 函数

  1. 无需声明原型。
  2. 支持不定 变参。
  3. 支持多返回值。
  4. 支持命名返回参数。
  5. 支持匿名函数和闭包。
  6. 函数也是一种类型,一个函数可以赋值给变量。
  7. 不支持 嵌套 (nested) 一个包不能有两个名字一样的函数。
  8. 不支持 重载 (overload)
  9. 不支持 默认参数 (default parameter)。

2. 包

1. 包的基本作用

  1. 在实际的开发中,往往需要在不同的文件中,调用其他文件定义的函数,比如在main.go中,使用utils.go文件中的函数
  2. 两个程序员共同开发一个Go项目,程序员A希望定义函数Cal,程序员B也想定义Cal,如何解决,在不同的包中可以定义同名函数
  3. 包的本质就是创建不同的文件夹,存放程序文件
  4. go的每一个文件都是属于一个包的,也就是说go是以包的形式来管理文件和项目目录结构的

2. 包的作用

  1. 区分相同名字的函数,变量等标识符
  2. 当程序文件很多是,可以很好地管理项目
  3. 控制函数,变量等访问范围,即作用域

3. 包的引用和打包

打包基本语法

package util

引入包的基本语法

import "包的路径"

4. 包的默认导入路径

src 路径下的所有包

5. 包的注意事项

  1. 在给一个文件打包时,该包对应一个文件夹,比如 utils 文件夹对应的包名就是 utils,文件的 package 包名 通常和文件所在的文件夹名一致,一般为小写字母
  2. 当一个文件要使用其他函数或变量时,需要先引入对应的包
    引入方式一: import "包名"
    引入方式二:
    import (
    "包名1"
    "包名2"
    )
  3. package 指令在文件第一行 然后是import 指令
  4. 在import 包时,路径从$GOPATH 的 src下开始,不用带 src,编译时自动从 src下开始引入
  5. 为了让其他包的文件,可以访问到本包的函数,则该函数名的首字母需要大写,类似其他语言的public,这样才能跨包访问
  6. 在访问其他函数,变量时,其语法时 包名.函数名 包名.变量名
  7. 如果包名较长,Go支持给包取别名,取别名后,原来的包名就不能使用了
  8. 在同一个包下,不能有相同的函数名或者相同的全局变量,否则报重复定义的错误
  9. 如果要编译成一个可执行程序文件,则需要将这个包声明为 main,即 package main 这个是语法规范,如果是写一个库,包名随意
    如何将项目编译成一个可执行文件?
    1. cd 到 $GOPATH 的目录下
    2. go build -o bin/my.exe (项目的src目录下开始)go_code/mypro/main.go // -o 可以指定编译的可执行文件的名称
    3. 编译成功后会自动在pkg文件下生成当前项目所使用的库文件(文件名为.a为后缀的二进制文件)

3. 函数结构

1. 执行流程

函数声明包含一个函数名,参数列表, 返回值列表和函数体。如果函数没有返回值,则返回列表可以省略。函数从第一条语句开始执行,直到执行return语句或者执行函数的最后一条语句。

2. 参数

1. 参数说明

函数可以没有参数或接受多个参数。类型在变量名之后 。

当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他都可以省略。

形参:函数定义时的参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。

实参调用函数时,传递到函数内部的的变量就是函数的实参,

2. 参数传递方式

2.1 值传递

// 指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数
func swap(x, y int) int {
       ... ...
  }

2.2 引用传递

// 是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数

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)
}

**在默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数 **

2. 可变参数(args)

1. 固定类型的可变参数

函数的参数不是固定的,后面的类型是固定的

``Golang 可变参数本质上就是 slice。只能有一个,且必须是最后一个。可以通过arg[index]依次访问所有参数,`

package main

import (
	"fmt"
)

func swap(x int, args ...int) { // 可变参数必须写在参数列表最后
	fmt.Println(x, args)     // 1 [2 3 4 5 6]
	fmt.Println(args[0])     // 2
    fmt.Println(len(args))   // 切片的长度为: 5
}

func main() {
	swap(1, 2, 3, 4, 5, 6)
}

在参数赋值时可以不用用一个一个的赋值,可以直接传递一个数组或者切片,特别注意的是在参数后加上即可

package main

import (
	"fmt"
)

func swap(x int, args ...int) {
	fmt.Println(x, args)

}

func main() {
	s1 := []int{3, 4, 5, 6, 7}
	swap(1, s1...)   // slice...  用来打散传递
}

2. 任意类型的可变参数(args)

就是函数的参数和每个参数的类型都不是固定的,用interface{}传递任意类型数据是Go语言的惯例用法,而且interface{}是类型安全的。

package main

import (
	"fmt"
)

func swap(args ...interface{}) { 
	fmt.Println(args) // [1 3 4.2 [5 6] map[小明:16]]
	for _, v := range args {
		fmt.Printf("值为:%v ,参数类型为: %T\n", v, v)
	}
}

func main() {
	s1 := []int{5, 6}
	m1 := map[string]int{
		"小明": 16,
	}
	swap(1, "3", 4.2, s1, m1)
}


// 运行结果
[1 3 4.2 [5 6] map[小明:16]]
值为:1 ,参数类型为: int
值为:3 ,参数类型为: string
值为:4.2 ,参数类型为: float64
值为:[5 6] ,参数类型为: []int
值为:map[小明:16] ,参数类型为: map[string]int

3. ...打散

使用 slice 对象做变参时,必须展开(slice...)

package main

import (
	"fmt"
)

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}
	res := test("sum: %d", s...) // slice... 展开slice
	fmt.Println(res)
}

// 打印结果:
6

3. 返回值

  1. 函数可以返回任意数量的返回值。
  2. 有返回值的函数,必须有明确的终止语句,否则会引发编译错误。
  3. _标识符,用来忽略函数的某个返回值
  4. 返回值可以被命名,并且就像在函数体开头声明的变量那样使用。
  5. 返回值的名称应当具有一定的意义,可以作为文档使用

1. 裸返回(隐式返回)

没有参数的 return 语句返回各个返回变量的当前值。这种用法被称作“裸”返回。

1. 单个参数返回值

package main

import "fmt"

func add(a, b int) (c int) {
	c = a + b
	return
}

func main() {
	var a, b int = 1, 2
	c := add(a, b)
	fmt.Println(c)
}

2. 多个参数返回值

package main

import "fmt"

// 返回多个参数
func add(a, b int) (sum, avg int) {
	sum = a + b
	avg = (a + b) / 2
	return
}

func main() {
	var a, b int = 1, 2
    // 接收多个参数
	sum, avg := add(a, b)
    fmt.Println(sum, avg)
    
    // 有不需要的参数可以用下划线_ 忽略
    sum, _ := add(a, b)
	fmt.Println(sum)
}


// 运行结果
3 1
3

2. 返回值接收

Golang返回值不能用容器对象接收多返回值。只能用多个变量,或 _ 忽略

package main

func test() (int, int) {
    return 1, 2
}

func main() {
    // s := make([]int, 2)
    // s = test()   // 用切片来接收参数会发生错误,Error: multiple-value test() in single-value context

    x, _ := test()
    println(x)
}

// 运行结果
1

3. 返回值当做参数传递

多返回值可直接作为其他函数调用实参

示例一:

package main

import "fmt"

func work() (int, int) {
	return 10, 53
}

func sum(a, b int) {
	fmt.Println(a + b)
}

func main() {
	sum(work())
}

// 打印结果
63

示例二:

package main

import "fmt"

func work() (int, int) {
	return 10, 53
}

func sum(n ...int) {
	var result int
	for _, v := range n {
		result += v
	}
	fmt.Println(result)
}

func main() {
	sum(work())
}

// 打印结果:
63

4. 命名返回参数

命名返回参数可被同名局部变量遮蔽,此时需要显式返回

写法一:

package main

import "fmt"

func add(a, b int) (c int) {
	c = a + b
	return
}

func main() {
	fmt.Println(add(1, 3))
}

// 打印结果:
4

写法二:

package main

import "fmt"

func add(a, b int) (c int) {
	{  	// 花括号为固定格式写法
		var c = a + b
		return c   // 必须显式返回。
	}
}

func main() {
	fmt.Println(add(1, 3))
}

// 打印结果:
4

5. defer 延迟调用

命名返回参数允许 defer 延迟调用通过闭包读取和修改。

package main

import "fmt"

func add(a, b int) (z int) {
    // 延时调用此匿名参数,函数退出前调用
	defer func() {
		z += 100
	}()

	z = a + b
	return
}

func main() {
	fmt.Println(add(1, 2))
}

// 运行结果
103

6. return + 表达式

显式 return 返回前,会先修改命名返回参数。

package main

import "fmt"

func add(a, b int) (z int) {
	defer func() {
		z += 100 //103
	}()
	
	z = a + b  // 3
	return z + 100 //203
}

func main() {
	fmt.Println(add(1, 2))
}

// 运行结果
203

4. 注意事项

  1. 无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。
  2. map、slice、chan、指针、interface默认以引用的方式传递。

4. 函数声明

1. 普通函数声明

func test(x, y int, s string) (int, string) {
    // 类型相同的相邻参数,参数类型可合并。 多返回值必须用括号。
    n := x + y          
    return n, fmt.Sprintf(s, n)
}

2. 匿名函数声明

func test(fn func() int) int {
	return fn()
}

func main() {
	// 匿名函数
	s1 := test(func() int {
		return 100
	})
	fmt.Println(s1)
}

// 运行结果
100

3. 参数传递

函数是第一类对象,可作为参数传递。建议将复杂签名定义为函数类型,以便于阅读

package main

import "fmt"

// 1. 定义函数类型
type FormatFunc func(s string, x, y int) string

// 2. 类型引用
func format(fn FormatFunc, s string, x, y int) string {
	return fn(s, x, y)
}

func main() {
	// 匿名函数当做参数传递
	s2 := format(func(s string, x, y int) string {
		return fmt.Sprintf(s, x, y)
	}, "我的年龄是:%d%d", 1, 8)
	fmt.Println(s2)
}


// 运行结果
我的年龄是:18

4. 第三方函数

你可能会偶尔遇到没有函数体的函数声明,这表示该函数不是以Go实现的。这样的声明定义了函数标识符

package math

// 该函数不是以Go实现的
func Sin(x float64) float 

5. 变量的作用域

  1. 在函数内部 声明 / 定义 的变量称之为 局部变量, 作用域仅限于函数内部,不管首字母是否大写
  2. 在函数外部 声明 / 定义 的变量称之为 全局变量, 作用域在整个包中都有效,如果首字母是大写的,则作用域在整个程序有效
  3. 如果变量是在同一个代码块下,比如 for/if 中,该变量的作用域只在该代码块中

示例一: 在函数内部可以改变全局变量的值

package main

import "fmt"

// 定义全局变量
var name = "tom"

func test01(){
	fmt.Println(name)  // 先查找局部作用域中有没有name, 如果局部作用域没有, 则查找全局作用域
}

func test02(){
	name := "jack"
	fmt.Println(name)   // 先查找局部作用域中有没有name, 如果有, 直接访问, 如果没有,则查找全局作用域
}

func test03(){
	name = "jack02"
	fmt.Println(name)   // 先查找局部作用域中有没有name, 如果有, 直接访问, 如果没有,则查找全局作用域
}

func main() {
	fmt.Println(name)   // 函数内部访问 name
	test01()
	test02()
	test03()     // 修改全局变量的 name
	fmt.Println(name)   // 函数内部访问 name
}

2. 全局变量的类型推导注意点

5. init 函数

每个源文件都可以包含一个init函数,该函数会在main函数执行前,被GO运行框架调用,也就是说会在main函数前被调用

package main

import "fmt"

func init() {
	fmt.Println("init()......")
}

func main() {
	fmt.Println("main()......")
}

注意事项

  1. 如果一个文件同时包含 全局变量定义init 函数main 函数, 则执行流程是 变量定义 --> init 函数 --> main 函数
  2. init 函数 最主要的作用,就是完成一些初始化的工作
  3. 如果 main.goutils.go 都有全局变量定义。init 函数,执行流程是什么?

6. 匿名函数

  1. 匿名函数是指不需要定义函数名的一种函数实现方式。1958年LISP首先采用匿名函数。

  2. 在Go里面,函数可以像普通变量一样被传递或使用,Go语言支持随时在代码里定义匿名函数。

  3. 匿名函数由一个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。

package main

import (
	"fmt"
	"math"
)

func main() {
	
	getSqrt := func(a float64) float64 {
		return math.Sqrt(a)
	}
	fmt.Println(getSqrt(4))
}

上面先定义了一个名为getSqrt 的变量,初始化该变量时和之前的变量初始化有些不同,使用了func,func是定义函数的,可是这个函数和上面说的函数最大不同就是没有函数名,也就是匿名函数。这里将一个函数当做一个变量一样的操作

匿名函数可赋值给变量,做为结构字段,或者在 channel 里传送。

package main

func main() {
    // 定义匿名函数
    fn := func() { println("Hello, World!") }
    fn()

    // 匿名函数切片
    fns := [](func(x int) int){
        func(x int) int { return x + 1 },
        func(x int) int { return x + 2 },
    }
    // 调用函数
    println(fns[0](100))

    // 结构体,属性fn的值是匿名函数
    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, World!" }
    println((<-fc)())
}

7. 递归函数

一个函数在函数体内又调用了本身,这就是递归调用,递归,就是在运行的过程中调用自己。 一个函数调用自己,就叫做递归函数。

1. 基本使用

构成递归需具备的条件:

  1. 子问题须与原始问题为同样的事,且更为简单.
  2. 不能无限制地调用本身,须有个出口,化简为非递归状况处理。


结果

n=2
n=2
n=3


结果

n=2

2. 注意事项

  1. 执行一个函数就创建一个新的受保护的独立空间(新函数栈)
  2. 函数的局部变量时独立的,不会相互影响
  3. 递归必须向退出递归的条件逼近,否则就是无限递归
  4. 当一个函数执行完毕,或者遇到return,就会返回,最受谁调用,就将结果返回给谁,当函数执行完毕,或返回时,该函数被回收机制销毁

3. 练习案例

1. 数字阶乘

一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1。自然数n的阶乘写作n!。1808年,基斯顿·卡曼引进这个表示法。

package main

import "fmt"

func factorial(i int) int {
    if i <= 1 {
        return 1
    }
    return i * factorial(i-1)
}

func main() {
    var i int = 7
    fmt.Printf("Factorial of %d is %d\n", i, factorial(i))
}

// 打印结果
Factorial of 7 is 5040

2. 斐波那契数列(Fibonacci)

这个数列从第3项开始,每一项都等于前两项之和。

package main

import "fmt"

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\n", fibonaci(i))
    }
}

// 打印结果:
0
1
1
2
3
5
8
13
21
34

3. 求函数值,已知 f(1)=3;f(n)=2*f(n-1)+1;请用递归的思想编程求出f(n)的值

func zh(n int) int {
    if n == 1 {
        return 3
    }else{
        return 2 * zh(n - 1)+1
    }
}

4. 猴子吃桃子的问题:有一堆桃子,猴子第一天吃了其中的一半,并再多吃了一个!以后每天猴子都吃其中的一半,然后再多吃一个,当到第十天的时候想再吃(还没吃),发现只有一个桃子了,请问最初有多少个桃子
思路:

  1. 第十天只有一个桃子
  2. 第九天的桃子数量是:(第十天桃子数量 + 1) * 2
  3. 第八天的桃子数量是:(第九天桃子数量 + 1) * 2
  4. 规律: 第n天桃子的数量 = peach(n) = (peach(n + 1) +1) *2
func peach(n int) int {
    if n == 10 {
        return 1
    }  else {
        return (peach(n + 1) +1) * 2  
    }
}

8. 闭包

1. 什么是闭包呢?

闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。

官方的解释是:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

**维基百科讲: ** 闭包(Closure),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

看着上面的描述,会发现闭包和匿名函数似乎有些像。可是可能还是有些云里雾里的。因为跳过闭包的创建过程直接理解闭包的定义是非常困难的。目前在JavaScript、Go、PHP、Scala、Scheme、Common Lisp、Smalltalk、Groovy、Ruby、 Python、Lua、objective c、Swift 以及Java8以上等语言中都能找到对闭包不同程度的支持。通过支持闭包的语法可以发现一个特点,他们都有垃圾回收(GC)机制。 javascript应该是普及度比较高的编程语言了,通过这个来举例应该好理解写。看下面的代码,只要关注script里方法的定义和调用就可以了

<!DOCTYPE html>
<html lang="zh">
<head>
    <title></title>
</head>
<body> 
</body>
</html>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js" type="text/javascript"></script>
<script>
function a(){
    var i=0;
    function b(){
        console.log(++i);
        document.write("<h1>"+i+"</h1>");
    }
    return b;
}

$(function(){
    var c=a();
    c();
    c();
    c();
    //a(); //不会有信息输出
    document.write("<h1>=============</h1>");
    var c2=a();
    c2();
    c2();
});

</script>

这段代码有两个特点:

函数b嵌套在函数a内部 函数a返回函数b 这样在执行完var c=a()后,变量c实际上是指向了函数b(),再执行函数c()后就会显示i的值,第一次为1,第二次为2,第三次为3,以此类推。 其实,这段代码就创建了一个闭包。因为函数a()外的变量c引用了函数a()内的函数b(),就是说:

当函数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()的调用顺序都是无关的。

2. 闭包示例

Go语言是支持闭包的,这里只是简单地讲一下在Go语言中闭包是如何实现的。 下面我来将之前的JavaScript的闭包例子用Go来实现。

package main

import (
	"fmt"
)

func warpper() func() int {
	x := 10
	b := func() int {
		x++
		fmt.Println(x)
		return x
	}
	fmt.Println(x)
	return b
}

func main() {
	c := warpper() // 10
	c()            // 11
	c()            // 12
	c()            // 13
	warpper()      // 10
}

// 运行结果
10
11
12
13
10

这说明Go语言是支持闭包的。

闭包复制的是原对象指针,这就很容易解释延迟引用现象。

package main

import "fmt"

func test() func() {
	x := 100
	fmt.Printf("指针:%p  值:%d\n", &x, x)

	return func() {
		fmt.Printf("指针:%p  值:%d\n", &x, x)
	}
}

func main() {
	f := test()
	f()
}

// 运行结果
指针:0xc0000aa058  值:100
指针:0xc0000aa058  值:100

在汇编层 ,test 实际返回的是 FuncVal 对象,其中包含了匿名函数地址、闭包对象指针。当调 匿名函数时,只需以某个寄存器传递该对象即可。

FuncVal { func_address, closure_var_pointer ... }

3. 外部引用函数参数局部变量

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))
	
    // tep1 和 tmp2 是两个闭包了
	tmp2 := add(100)
	fmt.Println(tmp2(1), tmp2(2))
}
// 运行结果
11 13
101 103

4. 返回2个闭包

package main

import "fmt"

// 返回2个函数类型的返回值
func test01(base int) (func(int) int, func(int) int) {
    // 定义2个函数,并返回
    // 相加
    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 := test01(10)
    // base一直是没有消
    fmt.Println(f1(1), f2(2))
    // 此时base是9
    fmt.Println(f1(3), f2(4))
}

9. 延迟调用(defer)

1. defer特性:

  1. 关键字 defer 用于注册延迟调用。
  2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
  3. 多个defer语句,按先进后出的方式执行。
  4. defer语句中的变量,在defer声明时就决定了。

2. defer用途:

  1. 关闭文件句柄
  2. 锁资源释放
  3. 数据库连接释放

go语言 defer

go 语言的defer功能强大,对于资源管理非常方便,但是如果没用好,也会有陷阱。

3. defer 是先进后出

这个很自然,后面的语句会依赖前面的资源,因此如果先前面的资源先释放了,后面的语句就没法执行了。

package main

import "fmt"

func main() {
    var whatever [5]struct{}

    for i := range whatever {
        defer fmt.Println(i)
    }
}

// 打印结果
4
3
2
1
0

4. defer 碰上闭包

package main

import "fmt"

func main() {
    var whatever [5]struct{}
    for i := range whatever {
        defer func() { fmt.Println(i) }()
    }
}

// 打印结果
4
4
4
4
4

其实go说的很清楚,我们一起来看看go spec如何说的

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.

也就是说函数正常执行,由于闭包用到的变量 i 在执行的时候已经变成4,所以输出全都是4

5. defer f.Close及易错点

这个大家用的都很频繁,但是go语言编程举了一个可能一不小心会犯错的例子.

package main

import "fmt"

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 spec中的说明,应该输出c b a才对啊,那我们换一种方式来调用一下

package main

import "fmt"

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

这个时候输出的就是c b a,当然,如果你不想多写一个函数,也很简单,可以像下面这样,同样会输出c b a,看似多此一举的声明

package main

import "fmt"

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 {
        t2 := t
        defer t2.Close()
    }
}

// 打印结果:
c  closed
b  closed
a  closed

通过以上例子,结合

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后面的语句在执行的时候,函数调用的参数会被保存起来,但是不执行。也就是复制了一份。但是并没有说struct这里的this指针如何处理,通过这个例子可以看出go语言并没有把这个明确写出来的this指针当作参数来看待。

6. defer 执行顺序

多个 defer 注册,按 FILO 次序执行 ( 先进后出 )。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。

package main

func test(x int) {
    defer println("a")
    defer println("b")

    defer func() {
        println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。
    }()

    defer println("c")
}

func main() {
    test(0)
}
// 运行结果
c
b
a
panic: runtime error: integer divide by zero

延迟调用参数在注册时求值或复制,可用指针或闭包 "延迟" 读取。

package main

func test() {
    x, y := 10, 20

    defer func(i int) {
        println("defer:", i, y) // y 闭包引用
    }(x) // x 被复制

    x += 10
    y += 100
    println("x =", x, "y =", y)
}

func main() {
    test()
}

// 运行结果
 x = 20 y = 120
defer: 10 120

滥用 defer 可能会导致性能问题,尤其是在一个 "大循环" 里。

package main

import (
    "fmt"
    "sync"
    "time"
)

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 < 10000; i++ {
            test()
        }
        elapsed := time.Since(t1)
        fmt.Println("test elapsed: ", elapsed)
    }()
    func() {
        t1 := time.Now()

        for i := 0; i < 10000; i++ {
            testdefer()
        }
        elapsed := time.Since(t1)
        fmt.Println("testdefer elapsed: ", elapsed)
    }()

}

// 运行结果:
test elapsed:  223.162µs
testdefer elapsed:  781.304µs

7. defer陷阱

1. defer 与 closure

package main

import (
    "errors"
    "fmt"
)

func foo(a, b int) (i int, err error) {
    defer fmt.Printf("first defer err %v\n", err)
    defer func(err error) { fmt.Printf("second defer err %v\n", err) }(err)
    defer func() { fmt.Printf("third defer err %v\n", 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 后面跟的不是一个 closure 最后执行的时候我们得到的并不是最新的值。

2. defer 与 return

package main

import "fmt"

func foo() (i int) {

    i = 0
    defer func() {
        fmt.Println(i)
    }()

    return 2
}

func main() {
    foo()
}

解释:在有具名返回值的函数中(这里具名返回值为 i),执行 return 2 的时候实际上已经将 i 的值重新赋值为 2。所以defer closure 输出结果为 2 而不是 1。

3. defer nil 函数

package main

import (
	"fmt"
)

func test() {
	var run func() = nil
	defer run()
	fmt.Println("runs")
}

func main() {
	defer func() {
		// 捕获异常
		if err := recover(); err != nil {
			fmt.Println(err)
		}
	}()
	test()
}
// 运行结果
runs
runtime error: i nvalid memory address or nil pointer dereference

解释:名为 test 的函数一直运行至结束,然后 defer 函数会被执行且会因为值为 nil 而产生 panic 异常。然而值得注意的是,run() 的声明是没有问题,因为在test函数运行完成后它才会被调用。

4. 在错误的位置使用 defer

当 http.Get 失败时会抛出异常。

package main

import "net/http"

func do() error {
    res, err := http.Get("http://www.google.com")
    defer res.Body.Close()
    if err != nil {
        return err
    }

    // ..code...

    return nil
}

func main() {
    do()
}

输出结果:

panic: runtime error: invalid memory address or nil pointer dereference

因为在这里我们并没有检查我们的请求是否成功执行,当它失败的时候,我们访问了 Body 中的空变量 res ,因此会抛出异常

**解决方案: **

总是在一次成功的资源分配下面使用 defer ,对于这种情况来说意味着:当且仅当 http.Get 成功执行时才使用 defer

package main

import "net/http"

func do() error {
    res, err := http.Get("http://xxxxxxxxxx")
    if res != nil {
        defer res.Body.Close()
    }

    if err != nil {
        return err
    }

    // ..code...

    return nil
}

func main() {
    do()
}

在上述的代码中,当有错误的时候,err 会被返回,否则当整个函数返回的时候,会关闭 res.Body 。

解释:在这里,你同样需要检查 res 的值是否为 nil ,这是 http.Get 中的一个警告。通常情况下,出错的时候,返回的内容应为空并且错误会被返回,可当你获得的是一个重定向 error 时, res 的值并不会为 nil ,但其又会将错误返回。上面的代码保证了无论如何 Body 都会被关闭,如果你没有打算使用其中的数据,那么你还需要丢弃已经接收的数据。

5. 不检查错误

在这里,f.Close() 可能会返回一个错误,可这个错误会被我们忽略掉

package main

import "os"

func do() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }

    if f != nil {
        defer f.Close()
    }

    // ..code...

    return nil
}

func main() {
    do()
}

改进一下

package main

import "os"

func do() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }

    if f != nil {
        defer func() {
            if err := f.Close(); err != nil {
                // log etc
            }
        }()
    }

    // ..code...

    return nil
}

func main() {
    do()
}

再改进一下

通过命名的返回变量来返回 defer 内的错误。

package main

import "os"

func do() (err error) {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }

    if f != nil {
        defer func() {
            if ferr := f.Close(); ferr != nil {
                err = ferr
            }
        }()
    }

    // ..code...

    return nil
}

func main() {
    do()
}

释放相同的资源

如果你尝试使用相同的变量释放不同的资源,那么这个操作可能无法正常执行。

package main

import (
    "fmt"
    "os"
)

func do() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func() {
            if err := f.Close(); err != nil {
                fmt.Printf("defer close book.txt err %v\n", err)
            }
        }()
    }

    // ..code...

    f, err = os.Open("another-book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func() {
            if err := f.Close(); err != nil {
                fmt.Printf("defer close another-book.txt err %v\n", err)
            }
        }()
    }

    return nil
}

func main() {
    do()
}

输出结果:

defer close book.txt err close ./another-book.txt: file already closed

当延迟函数执行时,只有最后一个变量会被用到,因此,f 变量 会成为最后那个资源 (another-book.txt)。而且两个 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\n", 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\n", err)
            }
        }(f)
    }

    return nil
}

func main() {
    do()
}

10. 异常处理

Golang 没有结构化异常,使用 panic 抛出错误,recover 捕获错误。

异常的使用场景简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。

1. panic()

  1. 内置函数
  2. 假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数内如果存在要执行的defer函数列表,按照defer的逆序执行
  3. 返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
  4. 直到goroutine整个退出,并报告错误

2. recover()

  1. 内置函数
  2. 用来控制一个goroutinepanicking行为,捕获panic,从而影响应用的行为
  3. 一般的调用建议
    1. 在defer函数中,通过recever来终止一个goroutine的panicking过程,从而恢复正常代码的执行
    2. 可以获取通过panic传递的error

3. 案例

  1. 利用recover处理panic指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。
  2. recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
  3. 多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。

正常的,抛出异常和捕获异常

package main

import "fmt"

func test() {
	defer func() {
		if err := recover();err != nil{
			fmt.Println(err.(string))
		}
	}()

	panic("panic error!")
}

func main() {
	test()
}


// 运行结果
panic error!

由于 panic、recover 参数类型为 interface{},因此可抛出任何类型对象

func panic(v interface{})
func recover() interface{}

向已关闭的通道发送数据会引发panic

package main

import "fmt"

func test() {
	defer func() {
		if err := recover();err != nil{
			fmt.Println(err)
		}
	}()

	ch := make(chan int,10)
	close(ch)
	ch <- 10
	fmt.Println("函数结束")
}
func main() {
	test()
}


// 运行结果,不会执行fmt.Println("sss")
send on closed channel

延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。

package main

import "fmt"

func test() {
	defer func() {
		fmt.Println(recover())
	}()

	defer func() {
		panic("第一个错误!")
	}()
	defer func() {
		panic("第二个错误")
	}()
}

func main() {
	test()
}

// 运行结果
第一个错误!

捕获函数 recover 只有在延迟调用内直接调用才会终止错误,否则总是返回 nil。任何未捕获的错误都会沿调用堆栈向外传递。

package main

import "fmt"

func test() {
	defer func() {
		fmt.Println(recover()) // 有效
	}()
	defer recover()              // 无效!
	defer fmt.Println(recover()) // nil
	defer func() {
		func() {
			println("defer inner")
			recover() // 无效!
		}()
	}()

	panic("test panic")
}

func main() {
	test()
}


// 运行结果

defer inner
<nil>
test panic

使用延迟匿名函数或下面这样都是有效的。

package main

import (
    "fmt"
)

func except() {
    fmt.Println(recover())
}

func test() {
    defer except()
    panic("test panic")
}

func main() {
    test()
}

// 运行结果
test panic

如果需要保护代码 段,可将代码块重构成匿名函数,如此可确保后续代码被执 。

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\n", z)
}

func main() {
    test(2, 1)
}

// 运行结果
x / y = 0

除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态。

type error interface {
    Error() string
}

标准库 errors.Newfmt.Errorf 函数用于创建实现 error 接口的错误对象。通过判断错误对象实例来确定具体错误类型

package main

import (
	"errors"
	"fmt"
)

var ErrDivByZero = errors.New("division by zero")

func div(x, y int) (int, error) {
	if y == 0 {
		return 0, ErrDivByZero
	}
	return x / y, nil
}

func main() {
	defer func() {
		fmt.Println(recover())
	}()

	switch z, err := div(10, 0); err {
	case nil:
		fmt.Println(z)
	case ErrDivByZero:
		panic(err)

	}
}

// 运行结果
division by zero

4. 实现 try catch 语句功能

package main

import "fmt"

func Try(fu func(), handler func(err interface{})) {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("err:", err)
			handler(err)
		}
	}()
	fu()
}

func main() {
	Try(func() {
		panic("报错了!!!")
	}, func(err interface{}) {
		fmt.Println("执行!!!")
	})
}

// 运行结果
err: 报错了!!!
执行!!!

5. 如何区别使用 panic 和 error 两种方式?

惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。

6. 自定义error

1. 抛异常和处理异常

抛异常

package main

import "fmt"

// 系统抛
func test01() {
    a := [5]int{0, 1, 2, 3, 4}
    a[1] = 123
    fmt.Println(a)
    //a[10] = 11
    index := 10
    a[index] = 10
    fmt.Println(a)
}

func getCircleArea(radius float32) (area float32) {
    if radius < 0 {
        // 自己抛
        panic("半径不能为负")
    }
    return 3.14 * radius * radius
}

func test02() {
    getCircleArea(-5)
}

//
func test03() {
    // 延时执行匿名函数
    // 延时到何时?(1)程序正常结束   (2)发生异常时
    defer func() {
        // recover() 复活 恢复
        // 会返回程序为什么挂了
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    getCircleArea(-5)
    fmt.Println("这里有没有执行")
}

func test04()  {
    test03()
    fmt.Println("test04")
}

func main() {
    test04()
}

// 运行结果
半径不能为负
test04

2. 返回异常

package main

import (
    "errors"
    "fmt"
)

func getCircleArea(radius float32) (area float32, err error) {
    if radius < 0 {
        // 构建个异常对象
        err = errors.New("半径不能为负")
        return
    }
    area = 3.14 * radius * radius
    return
}

func main() {
    area, err := getCircleArea(-5)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(area)
    }
}

// 运行结果
半径不能为负

2. 自定义error:

package main

import (
    "fmt"
    "os"
    "time"
)

type PathError struct {
    path       string
    op         string
    createTime string
    message    string
}

func (p *PathError) Error() string {
    return fmt.Sprintf("path=%s \nop=%s \ncreateTime=%s \nmessage=%s", p.path,
                       p.op, p.createTime, p.message)
}

func Open(filename string) error {

    file, err := os.Open(filename)
    if err != nil {
        return &PathError{
            path:       filename,
            op:         "read",
            message:    err.Error(),
            createTime: fmt.Sprintf("%v", time.Now()),
        }
    }

    defer file.Close()
    return nil
}

func main() {
    err := Open("/Users/5lmh/Desktop/go/src/test.txt")
    switch v := err.(type) {
        case *PathError:
        fmt.Println("get path error,", v)
        default:

        }

}
// 运行结果

get path error, path=/Users/pprof/Desktop/go/src/test.txt 
op=read 
createTime=2018-04-05 11:25:17.331915 +0800 CST m=+0.000441790 
message=open /Users/pprof/Desktop/go/src/test.txt: no such file or directory
posted @ 2023-03-22 15:43  河图s  阅读(19)  评论(0)    收藏  举报