go

函数

Posted by 云起 on 2022-09-18
Estimated Reading Time 14 Minutes
Words 3.2k In Total
Viewed Times

函数

随写

特点

1
2
3
4
5
6
7
8
9
10
• 无需声明原型。
• 支持不定 变参。
• 支持多返回值。
• 支持命名返回参数。
• 支持匿名函数和闭包。
• 函数也是一种类型,一个函数可以赋值给变量。

• 不支持 嵌套 (nested) 一个包不能有两个名字一样的函数。
• 不支持 重载 (overload)
• 不支持 默认参数 (default parameter)

基本使用

函数声明

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

函数参数

值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

1
2
3
func swap(x, y int) int {
... ...
}

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

1
2
3
4
5
6
7
8
func swap(x, y *int) {
var temp int

temp = *x /* 保存 x 的值 */
*x = *y /* 将 y 值赋给 x */
*y = temp /* 将 temp 值赋给 y*/

}

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

注意1:无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。

注意2:map、slice、chan、指针、interface默认以引用的方式传递。

不定参数传值
就是函数的参数不是固定的,后面的类型是固定的。(可变参数)

Golang 可变参数本质上就是 slice。只能有一个,且必须是最后一个。

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

1
2
func myfunc(args ...interface{}) {
}
1
2
3
4
5
6
7
8
func test(s string, n ...int) string {
var x int
for _, i := range n {
x += i
}

return fmt.Sprintf(s, x)
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
println(res)
}

函数返回值

"_"标识符,用来忽略函数的某个返回值

Go 的返回值可以被命名,并且就像在函数体开头声明的变量那样使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func add(a,b int) int{
return a+b
}
func add(a, b int) (c int) {
c = a + b
return
}
func calc(a, b int) (sum int, avg int) {
sum = a + b
avg = (a + b) / 2

return//隐式返回
}
func prin(a int){//裸返回
fmt.Println(a)
}

多返回值

1
2
3
4
5
6
7
8
9
10
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func test() (int, int) {
return 1, 2
}

func add(x, y int) int {
return x + y
}

func sum(n ...int) int {
var x int
for _, i := range n {
x += i
}

return x
}

func main() {
println(add(test()))
println(sum(test()))

命名返回参数

命名返回参数可看做与形参类似的局部变量,最后由 return 隐式返回。

1
2
3
4
func add(x, y int) (z int) {
z = x + y
return
}

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

1
2
3
4
5
6
7
func add(x, y int) (z int) {
{ // 不能在一个级别,引发 "z redeclared in this block" 错误。
var z = x + y
// return // Error: z is shadowed during return
return z // 必须显式返回。
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
func add(x, y int) (z int) {
defer func() {
z += 100
}()

z = x + y
return
}

func main() {
println(add(1, 2))
}

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

1
2
3
4
5
6
7
8
9
10
11
12
func add(x, y int) (z int) {
defer func() {
println(z) // 输出: 203
}()

z = x + y
return z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (return)
}

func main() {
println(add(1, 2)) // 输出: 203
}

匿名函数

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

1
2
3
4
5
6
func main() {
getSqrt := func(a float64) float64 {
return math.Sqrt(a)
}
fmt.Println(getSqrt(4))
}

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

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

1
//例如开启多线程的go func(){}即属于匿名函数

闭包

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

1
2
3
4
5
6
7
8
9
10
11
12
13
func test() func() {
x := 100
fmt.Printf("x (%p) = %d\n", &x, x)

return func() {
fmt.Printf("x (%p) = %d\n", &x, x)
}
}

func main() {
f := test()
f()//f实际指向了func()
}
1
2
x (0xc42007c008) = 100
x (0xc42007c008) = 100

延迟调用

defer

  1. 关键字 defer 用于注册延迟调用。

  2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。

  3. 多个defer语句,按先进后出的方式执行。

  4. defer语句中的变量,在defer声明时就决定了。

用途

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

defer + 闭包

1
2
3
4
5
6
func main() {
var whatever [5]struct{}
for i := range whatever {
defer func() { fmt.Println(i) }()
}
} //4 4 4 4 4

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()
}
1
2
x = 20 y = 120
defer: 10 120

defer f.close

1
2
3
4
5
6
7
8
9
10
11
12
13
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()
}
}
1
2
3
c  closed
c closed
c closed

这个输出并不会像我们预计的输出c b a,而是输出c c c

1
2
3
4
5
6
7
8
9
10
11
12
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)
}
}

或者

1
2
3
4
5
6
7
8
9
10
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()
}
}
1
2
3
c  closed
b closed
a closed

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

defer 陷阱

defer 与 闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)
}
1
2
3
third defer err divided by zero!
second defer err <nil>
first defer err <nil>

如果 defer 后面跟的不是一个 closure 最后执行的时候我们得到的并不是最新的值。

defer 与 return

1
2
3
4
5
6
7
8
9
10
11
12
13
func foo() (i int) {

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

return 2
}

func main() {
foo()
}
1
2

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

defer nil 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func test() {
var run func() = nil
defer run()
fmt.Println("runs")
}

func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
test()
}
1
2
runs
runtime error: invalid memory address or nil pointer dereference

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

在错误的位置使用 defer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()
}
1
panic: runtime error: invalid memory address or nil pointer dereference

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

解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func do() error {
res, err := http.Get("http://xxxxxxxxxx")
if res != nil {
defer res.Body.Close()
}

if err != nil {
return err
}

// ..code...

return nil
}

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

不检查错误

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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()
}

异常处理

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函数中,通过recover来终止一个goroutine的panicking过程,从而恢复正常代码的执行
    b). 可以获取通过panic传递的error

注意:

1.利用recover处理panic指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。
2.recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
3.多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。
1
2
3
4
5
6
7
8
9
func test() {
defer func() {
if err := recover(); err != nil {
println(err.(string)) // 将 interface{} 转型为具体类型。
}
}()

panic("panic error!")
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func test() {
defer func() {
fmt.Println(recover())
}()

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

panic("test panic")
}

func main() {
test()
}
1
defer panic 

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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()
}
1
2
3
defer inner
<nil>
test panic

一个神奇的事情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func test() {
defer func() {
fmt.Println(recover()) //有效
}()

defer func() {
func() {
println("defer inner")
//recover() //无效!
}()
}()

panic("test panic")
}

func main() {
test()
}

输出 test panic defer inner 或 defer inner test panic

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

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


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them. Thank you !