go——函数

1.定义

函数是结构化编程的最小单元模式。它将复杂的算法过程分解为若干个较小任务,隐藏相关细节,使程序结构更加清晰,易于维护。
函数被设计成相对独立,通过接收输入参数完成一段算法指令,输出或存储相关结果。

一个函数的声明通常包括关键字func、函数名、分别由圆括号包裹的列表参数和结果列表
以及由花括号包裹的函数体,就像这样:

1
2
3
func divide(dividend int,divisor int)(int,error){
    //函数体  
}

函数可以没有参数列表,也可以没有结果列表,但空参数列表必须保留括号,而结果列表则不用,示例如下:

1
2
3
func divide(){
    //函数体  
}

另外,参数列表中的必须有名称,而结果列表中结果的名称则可有可无。
不过,结果列表中的结果要么都省略名称,要么都要有名称。
带有结果名称的divide函数的声明如下:

1
2
3
func divide(dividend int,divisor int)(result int,err error){
    //函数体  
}

如果函数的结果有名称,那么在函数被调用时,以它们为名的变量就会被隐式声明。
如此一来在函数中就可以直接使用它们了,就像使用参数那样。
给代表结果的变量赋值,就相当于设置函数的返回结果。

在Go中,函数类型是一等类型,这意味着可以把函数当作一个值来传递和使用。
函数值既可以作为其它函数地参数,也可以作为其结果。另外,我们还可以利用函数类型的这一特性来生成闭包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
  
import "fmt"
  
func hello() {
    fmt.Println("hello, world")
}
  
func exec(f func()) { //将函数作为参数
    f()
}
  
func main() {
    f := hello
    exec(f)
}
  
/*
结果:
hello, world
*/

  

函数只能判断其是否为nil,不支持其它比较操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
  
import "fmt"
  
func a() {}
func b() {}
  
func main() {
    fmt.Println(a == nil) //false
    fmt.Println(a == b)   //不支持比较操作
    //invalid operation: a == b (func can only be compared to nil)
    //无效操作:a == b (函数只能去判断其是否为nil)
}

  

从函数返回局部变量指针是安全的,编译器会通过逃逸分析来决定是否在堆上分配内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
  
import "fmt"
  
func test() *int { //*int  返回值时指针类型
    a := 0x100
    return &a
}
  
func main() {
    var a *int = test() //定义一个指针类型的变量
    fmt.Println(a, *a)  //a指针变量  *a反向取值
}
  
/*
结果:
0xc00000a168 256
*/

  

 

2.参数

Go对参数的处理偏向保守,不支持有默认值的可选参数,不支持命名实参。
调用时,必须按签名顺序传递指定类型和数量的实参,就算是“_”命名的参数也不能忽略。

1
2
3
4
5
6
7
8
9
10
11
package main
 
import "fmt"
 
func test(x, y int, s string, _ bool) *int {
    return nil
}
 
func main() {
    test(1, 2, "abc") // not enough arguments in call to test定义了四个变量却只传递了三个
}

  

在Go中应该避免在相同层次定义同名变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
 
import "fmt"
 
func add(x, y int) int {  //形参和实参冲突
    x := 100 //no new variables on left side of :=
    var y int
    return x + y
}
 
func main() {
    result := add(10, 20)
    fmt.Println(result)
}

  

在函数中定义的参数,我们称之为形参,函数被调用时所传递的参数我们称之为实参。
形参类似于函数的局部变量,而实参则是函数的外部对象,可以是常量、变量、表达式或函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
 
import "fmt"
 
func test(x *int) {
    fmt.Println(&x, x)
}
 
func main() {
    a := 0x100
    p := &a
    fmt.Println(&p, p)
    test(p)
}
 
/*
结果:
0xc000076018 0xc00004e080
0xc000076028 0xc00004e080
*/

  

虽然形参和实参都指向一个目标,但是传递指针时依然被复制的。

不管传递的参数是指针、引用类型,还是其它类型参数,默认采用的都是值拷贝传递。
值拷贝传递就是在调用函数时将实际参数复制一份传递到函数中,
这样在函数中如果对参数进行修改,将不会影响到实际参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main
 
import "fmt"
 
func main() {
    var a int = 100
    var b int = 200
 
    fmt.Printf("交换前a的值为%d\n", a)
    fmt.Printf("交换前b的值为%d\n", b)
 
    swap(a, b)
 
    fmt.Printf("交换前a的值为%d\n", a)
    fmt.Printf("交换前b的值为%d\n", b)
 
}
 
func swap(x, y int) int {
    var temp int
 
    temp = x
    x = y
    y = temp
 
    return temp
}
 
/*
交换前a的值为100
交换前b的值为200
交换前a的值为100
交换前b的值为200
*/

  另外一种是引用传递,就是在调用函数时将实际参数的地址传递到函数中,那么在函数中,对参数所进行的修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main
 
import "fmt"
 
func main() {
    var a int = 100
    var b int = 200
 
    fmt.Printf("交换前a的值为%d\n", a)
    fmt.Printf("交换前b的值为%d\n", b)
 
    swap(&a, &b)  //交换的是指针
 
    fmt.Printf("交换前a的值为%d\n", a)
    fmt.Printf("交换前b的值为%d\n", b)
 
}
 
func swap(x *int, y *int) int {
    var temp int
 
    temp = *x
    *x = *y
    *y = temp
 
    return temp
}
/*
交换前a的值为100
交换前b的值为200
交换前a的值为200
交换前b的值为100
*/

  

如果函数的参数过多,建议将其重构为一个复合结构类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main
 
import (
    "fmt"
    "log"
    "time"
)
 
type serverOption struct { //定义结构体
    address string
    port    int
    path    string
    timeout time.Duration
    log     *log.Logger
}
 
func newOption() *serverOption { //以函数的形式返回默认参数
    return &serverOption{
        address: "0.0.0.0",
        port:    8080,
        path:    "/var/test",
        timeout: time.Second * 5,
        log:     nil,
    }
}
 
func server(option *serverOption) { //需要操作的函数
    fmt.Println(option)
}
 
func main() {
    opt := newOption()
    opt.port = 8085 //修改属性值
    server(opt)
}
 
/*
结果:
&{0.0.0.0 8085 /var/test 5000000000 <nil>}
*/

  

在Go中还有一种称之为变参的用法,就是一个标识符传递多个参数的用法。
变参本质上就是一个切片。只能接收一到多个同类型的参数,且必须放在列表尾部。
将切片作为变参时,需进行展开操作。如果是数组,先将其转换为切片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main
 
import "fmt"
 
func test(s string, a ...int) {
    fmt.Println(s, a)
}
 
func main() {
    test("abc", 1, 2, 3, 4, 5)
 
    x := []int{10, 20, 30}  //使用切片作为变参
    test("abc", x...)
 
    y := [3]int{40, 50, 60} //使用数组作为变参
    test("abc", y[:]...)
 
}
/*
结果:
abc [1 2 3 4 5]
abc [10 20 30]
abc [40 50 60]
*/

  

 

3.返回值

  有返回值的函数,必须有明确的return终止语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
 
func test(a int) int { //函数如果有返回值就必须定义返回值的类型
    if a > 0 {
        return 1
    } else if a < 0 {
        return 2 //missing return at end of function
    }
    //逻辑必须完整,这里缺少一个else
}
 
func main() {
    test(5)
}

  

函数体中每个条件分支的最后一般都要有return语句,该语句以return关键字开始,
后跟与函数结果列表相匹配的变量、常量、表达式或值。
无论是什么,它们都会被求值并得到确切的值。
但是,如果函数声明的结果是有名称的,那么return关键字后面就不用追加任何东西了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
 
import (
    "errors"
    "fmt"
)
 
func div(x, y int) (int, error) {
    if y == 0 {
        return 0, errors.New("division by zero")
    }
    return x / y, nil
}
 
func main() {
    fmt.Println(div(10, 3))
}

  

在Go中没有元组类型可以用来接收返回值,也不能使用数组或者切片就收,但是可以用“_”忽略掉不想要的返回值。
多返回值可用作其它函数调用实参,或当作结果直接返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main
 
import (
    "errors"
    "fmt"
)
 
func div(x, y int) (int, error) {
    if y == 0 {
        return 0, errors.New("division by zero")
    }
    return x / y, nil
}
 
func log(x int, err error) {
    fmt.Println(x, err)
}
 
func test() (int, error) {
    return div(5, 0)  //多返回值用作return结果
}
 
func main() {
    log(test())  //多返回值用作实参
}

  

还有一种对返回值常用的操作是在定义函数的时候给返回值命名。
返回值和参数一样,可以当作函数局部变量使用,最后由return隐式返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
 
import "fmt"
 
func div(x, y int) (z int, err error) {
    if y == 0 {
        err = errors.New("division by zero")
        return   //这样就可以直接进行return
    }
    z := x / y, nil
    return
}
 
func main() {
    fmt.Println(div(10, 3))
}

  

需要注意的是,这些特殊的“局部变量”会被不同层级的同名变量遮蔽,这时候就需要使用显式地return返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
 
import "fmt"
 
func add(x, y int) (z int) {
    {
        z := x + y //新定义的同名局部变量,同名遮蔽
        return     //z is shadowed during return,改成return z就可以
    }
    return
}
 
func main() {
    fmt.Println(add(10, 1))
}

  

此外,我们在命名地时候要全部命名,除非返回值能明确表明其含义,就可以省略命名。

1
2
3
4
5
6
7
8
9
10
package main
 
func test() (int, s string) { //cannot use 1 (type int) as type string in return argument
    //要么都命名,要么都不命名
    return 1, "abc"
}
 
func main() {
    test()
}

  

4.匿名函数

匿名函数是指没有定义名字符号地函数。
除没有名字外,匿名函数和普通函数完全相同。
最大的区别是,我们可在函数内部定义匿名函数,形成类似嵌套效果。
匿名函数常见的使用方法有四种:直接使用、保存到变量、作为参数和返回值。

(1)直接执行

1
2
3
4
5
6
7
8
9
package main
 
import "fmt"
 
func main() {
    func(s string) {
        fmt.Println(s)
    }("科比要结婚了") //直接执行
}

  

  (2)复制给变量

1
2
3
4
5
6
7
8
9
10
package main
 
import "fmt"
 
func main() {
    add := func(x, y int) int {  //赋值给变量
        return x + y
    }
    fmt.Println(add(1, 3))  //4
}

  

  (3)作为参数

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
 
import "fmt"
 
func test(f func()) {
    f()
}
 
func main() {
    test(func() { //匿名函数作为参数
        fmt.Println("都走吧")
    })
}

  

  (4)作为返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
 
import "fmt"
 
func test() func(int, int) int {  //func(int, int) int:func(int, int)这是返回值名称,后面的int是返回值的类型
    return func(x, y int) int {  //直接返回一个函数
        return x + y
    }
}
 
func main() {
    add := test()  //得到一个函数
    fmt.Println(add(11, 22))  传参
}

  相比与语句块来说,匿名函数的作用域是被隔离的,不会引发外部污染,更加灵活。

 

 

5.闭包

在python中,你调用了一个函数A,但是函数A直接给你返回一个B函数,那么B函数就是闭包函数,
在B函数中,调用函数A的变量称作自由变量。
其实在Go语言中,闭包也是类似的定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
 
import "fmt"
 
func test(s string) func() {
    return func() {
        fmt.Println(s)
    }
}
 
func main() {
    reslut := test("时光悠悠")
    reslut()
}  

test返回的匿名函数会引用上下文环境变量s。
当该函数在main中执行时,它依然可以正确的读取x的值,这种现象就被称作闭包。
还有一种闭包的说法就是,如果在一个内部函数里,对在外部作用域(但不是全局作用域)的变量进行引用,
那么内部函数就被认为是闭包。
那么闭包是如何实现的了?匿名函数被返回之后,为何还能读取环境的变量值了?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main
 
import "fmt"
 
func test(s string) func() {
    fmt.Println(&s, s)
 
    return func() {
        fmt.Println(&s, s)
    }
}
 
func main() {
    reslut := test("时光悠悠")
    reslut()
}
 
/*
0xc0000421c0 时光悠悠
0xc0000421c0 时光悠悠
*/

  

通过指针我们发现闭包直接引用了原环境变量。
正是因为闭包通过指针引用环境变量,那么可能会导致其生命周期延长。
但这有时候会带来一些问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main
 
import "fmt"
 
func test() []func() {
    var s []func() //定义一个数组
 
    for i := 0; i < 2; i++ {
        s = append(s, func() { //添加元素
            fmt.Println(&i, i)
        })
    }
    return s                   //返回匿名函数列表
}
 
func main() {
    for _, f := range test() {  //迭代执行所有匿名函数
        f()
    }
}
 
/*
0xc00000a168 2
0xc00000a168 2
*/

  

这并不是我们想要看到的结果,为什么了?
for循环复用局部变量i,那么每次添加的匿名函数引用的就是同一个变量。
添加元素的操作仅仅是将匿名函数放入列表而并没有执行,所以就会出现这种状况。
解决办法就是每次使用不同的环境变量或传参复制,让各自闭包环境各不相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main
 
import "fmt"
 
func test() []func() {
    var s []func() //定义一个数组
 
    for i := 0; i < 2; i++ {
        x := i
        s = append(s, func() { //添加元素
            fmt.Println(&x, x)
        })
    }
    return s
}
 
func main() {
    for _, f := range test() {
        f()
    }
}
 
/*
结果:
0xc00000a168 0
0xc00000a180 1
*/

  

多个匿名函数引用同一变量时,任何修改都会影响其它函数的取值,这是我们需要注意的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
 
import "fmt"
 
func test(x int) (func(), func()) { //返回两个匿名函数
    return func() {
            fmt.Println(x)
            x += 10 //修改环境变量
        }, func() {
            fmt.Println(x)
        }
}
 
func main() {
    a, b := test(100)
    a() //100
    b() //110
}

  所以对于闭包应该慎用。

 

6.延迟调用

除了前面介绍的流程控制语句外,Go还有一些特有的流程控制语句,其中一个就是defer。
该语句用于延迟调用指定的函数,它只能出现在函数内部,由defer关键字以及针对某个函数的调用表达式组成。
这里被调用的函数称为延迟函数。

1
2
3
4
func outerFunc() {
    defer fmt.Println("函数执行结束前一刻才会被打印"//延迟执行,调用fmt.PrintLn
    fmt.Println("第一个被打印")    //延迟函数
}

其中,defer关键字后面是针对fmt.Println函数的调用表达式。代码里面也说明了延迟函数的执行时机。
这里的outerFunc称为外围函数,调用outerFunc的那个函数称为调用函数。
下面是具体的规则:
  a.当外围函数中的语句正常执行完毕时,只有其中所有的延迟函数都执行完毕,外围函数才会真正结束执行。
  b.当执行外围函数中的return语句时,只有其中所有的延迟函数都执行完毕后,外围函数才会真正返回。
  c.当外围函数中的代码引发运行异常,只有其中所有的延迟函数都执行完毕后,该运行异常才会被真正扩散至调用函数。
正因为defer语句有这样的特性,所以它成为了执行释放资源或异常处理等收尾任务的首选。
明显的优势有以下两个:
  A.对延迟函数的调用总会在外围函数执行结束前执行。
  B.defer语句在外围函数体中的位置不限,并且数量不限。

不过,使用defer语句还有三点需要注意:
第一点,如果在延迟函数中使用外部变量,就应该通过参数传入。

1
2
3
4
5
6
7
func printNumbers() {
    for i := 0; i < 5; i++ {
        defer func() {
            fmt.Printf("%d", i)
        }()
    }
}

上述代码的执行结果是55555,这正是由于延迟函数的执行实际引起的,for循环执行完毕之后,才会执行延迟函数。
也就是说,当执行延迟函数的时候,i已经等于5了。

1
2
3
4
5
6
7
func printNumbers() {
    for i := 0; i < 5; i++ {
        defer func(n,int) {
            fmt.Printf("%d", n)
        }(i)
    }
}

打印的内容会是43210,至于为什么,请看下面的第二点。

第二点:同一个外围函数内多个延迟函数调用的执行顺序,会与其所属的defer语句的执行顺序完全相反。
同一个外围函数中的每个defer语句执行的时候,针对其延迟函数的调用表达式都会被压入同一个栈。
在该外围函数执行结束的那一刻,Go会从这个栈中依次取出,栈的取值顺序是先进后出。

第三点:延迟函数调用,若有参数传入,那么那些参数的值会在当前defer语句执行时求出。

1
2
3
4
5
6
7
func printNumbers() {
    for i := 0; i < 5; i++ {
        defer func(n,int) {
            fmt.Printf("%d", n)
        }(i*2)
    }
}

此时,执行的结果是86420

最后看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
 
import (
    "fmt"
)
 
func main() {
    x, y := 1, 3
 
    defer func(a int) {
        fmt.Println("defer x, y=", a, y) //y为闭包引用
    }(x) //注册时复制调用参数,所以x为1
 
    x += 100
    y += 200
    fmt.Println(x, y)
}
 
/*
101 203
defer x, y= 1 203
*/

  

 

7.错误处理

(1)error

标准库将error定义为接口类型,以便发现自定义错误类型。

1
2
3
type error interface {
    Error() string  
}

按惯例,error总是最后一个返回参数。标准库提供了相关创建函数,
可以很方便的创建包含简单错误处理文本的error对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main
 
import (
    "errors"
    "fmt"
    "log"
)
 
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() {
    z, err := div(5, 0)
    if err == errDivByZero {
        log.Fatalln(err)
    }
 
    fmt.Println(z)
}
 
/*
2018/11/30 03:15:24 division by zero
exit status 1
*/

  

某些时候,我们需要自定义错误类型,以便容纳更多的上下文状态信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main
 
import (
    "fmt"
    "log"
)
 
type DivError struct {
    x, y int
}
 
func (DivError) Error() string {
    return "division by zero"
}
 
func div(x, y, int) (int, error) {
    if y == 0 {
        return 0, DivError{x, y}
    }
    return x / y, nil
}
 
func main() {
    z, err := div(5, 0)
    if err != nil {
        switch e := err.(type) {
        case DivError:
            fmt.Println(e, e.x, e.y)
        default:
            fmt.Println(e)
        }
        log.Fatalln(err)
    }
    fmt.Println(z)
}

  

(2)panic和recover

panic会立即中断当前函数流程,执行延迟调用。
而在延迟函数中,recover可捕获并返回panic提交的错误对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
 
import (
    "fmt"
    "log"
)
 
func main() {
    defer func() {
        if err := recover(); err != nil {
            log.Fatalln(err)
        }
    }()
 
    panic("I dead")
    fmt.Println("exit!")
}
 
/*
2018/11/30 03:35:18 I dead
exit status 1
*/

  

因为panic参数是空接口类型,因此可以使用任何对象作为错误状态。
无论是否执行recover,所有延迟都会被执行。
但中断性错误会调用堆栈向外传递,要么被外层捕获,要么导致进程崩溃。t/css" /> 从函数返回局部变量指针是安全的,编译器会通过逃逸分析来决定是否在堆上分配内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main
 
import (
    "fmt"
    "log"
)
 
func test() {
    defer fmt.Println("test.1")
    defer fmt.Println("test.2")
 
    panic("I dead")
}
 
func main() {
    defer func() {
        log.Fatalln(recover())
    }()
 
    test()
}
 
/*
test.2
test.1
2018/11/30 03:39:02 I dead
exit status 1
*/

  连续调用panic,仅最后一个会被recover捕获。

posted @   明王不动心  阅读(607)  评论(0编辑  收藏  举报
编辑推荐:
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
阅读排行:
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
历史上的今天:
2017-12-11 第12条:不要在for和while循环后面写else块
2017-12-11 将序列中的元素连起来——程序的思想(顺着电脑的思想)——闯关
2017-12-11 动态删除列表中的元素
2017-12-11 文件操作脚本(一)
2017-12-11 函数(1)
2017-12-11 构造器以及解构器
2017-12-11 文件传输协议FTP
点击右上角即可分享
微信分享提示