hello云胜

技术与生活

0%

函数

函数是go语言中的一等公民。

在go中一个函数的声明可以出现在它的调用之前,也可以出现在它的调用之后。

当然,在java中我们一般称呼函数为方法。在函数定义语法上,go和java还是有很多区别点的。

普通函数

一个简单例子

1
2
3
4
5
6
func SquareSumAndDiff(a int64, b int64) (c int64, d int64) {
x, y := a+b, a-b
c = x * x
d = y * y
return // 等价于 return c, d
}

这一个简单的例子就有很多可说的。

  1. go的参数声明都是名字在前,类型在后。函数名首字母大小写规则遵循导出规则。

  2. 多个参数,如果类型一样,可以简写(a, b int64)。另外go支持变长参数,在类型前加...

  3. go支持多返回值。这个确实方便。习惯了之后,会觉得java的单返回值写起来有点麻烦。

  4. 如果只有一个返回值,并且返回值没有名字,那么可以不写括号()。但是入参的括号永远不能省略。

  5. 仔细看例子,返回值是可以有名字的。这个名字相当于进行了变量申明,在函数中可以直接使用。

    在这种返回值有名字的情况下,return c, d可以简写为return。如果返回值是匿名的,那么必须显示return c,d

    1
    2
    3
    4
    5
    6
    func SquareSumAndDiff2(a int64, b int64) (int64, int64) {
    x, y := a+b, a-b
    c := x * x
    d := y * y
    return c, d
    }

关于函数参数

go函数传参采用的是值传递的方式。就是将实际参数在内存中一个bit一个bit的拷贝到形参里,比如传整型,数组或者string类型,即使这个数组很大,也是copy整个原始数组传进去。

但是像切片,map,struct等类型,他们的内存存储的是具体内容的引用(或者叫指针)。这样传参时copy的也是引用。

关于多返回值

返回值是可以有名字的,称为具名返回值。

Go 标准库以及大多数项目代码中的函数,都是使用普通的非具名返回值形式

匿名函数

首先要明确的一个事情是:匿名函数的定义不是一种函数声明。一个标准的函数只能声明在包级。不能在一个函数中再声明新的函数。

但是可以在一个函数中定义一个匿名函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

func main() {
// 这个匿名函数没有输入参数,但有两个返回结果。
x, y := func() (int, int) {
return 3, 4
}() // 一对小括号表示立即调用此函数。不需传递实参。

// 下面这些匿名函数没有返回结果。
func(a, b int) {
println("a*a + b*b =", a*a + b*b) // a*a + b*b = 25
}(x, y) // 立即调用并传递两个实参。

func(x int) {
// 形参x遮挡了外层声明的变量x。
println("x*x + y*y =", x*x + y*y) // x*x + y*y = 32
}(y) // 将实参y传递给形参x。

func() {
println("x*x + y*y =", x*x + y*y) // x*x + y*y = 25
}() // 不需传递实参。
}

闭包

上面例子中的最后一个匿名函数处于变量xy的作用域内,所以在在它的函数体内可以直接使用这两个变量。 这样的函数称为闭包(closure)。

事实上,Go中的所有的自定义函数(包括普通函数和匿名函数)都可以被视为闭包。

换句话说,所谓的闭包,不过是一个函数可以使用函数外定义的变量。

一个很常见的闭包的例子就是一个函数的返回值是函数

1
2
3
4
5
6
7
func incr() func() int {
var x int // 默认值是0
return func() int {
x++
return x
}
}

函数incr()的返回值是一个函数。

1
2
3
4
5
6
func TestClosure(t *testing.T) {
f := incr()
t.Log(f()) // 1
t.Log(f()) // 2
t.Log(f()) // 3
}

可以看到,多次调用闭包函数。x都增加了。也就是说x并没有随着函数的结束而销毁。因为x已经被变量f持有,只要f还在其作用域内,x也就一直存在下来了。通过x的增加,也可说明f对x的持有是保存其地址,所以x始终是同一个x。

1
2
3
t.Log(incr()())   // 1
t.Log(incr()()) // 1
t.Log(incr()()) // 1

这样写,每次都是一个新的x。所以一直是1

init函数

init函数的作用是在main执行前进行一些初始化工作。

每个代码包下,可以有多个init函数。init函数和main函数一样,没有入参,没有返回值。

1
2
3
func init() {
fmt.Println("init===")
}

init函数是在包加载的时候执行。

多个init按照声明的顺序串行执行。

一个代码包依赖于应一个代码包,那么一定是被依赖的代码包的init先执行。

在加载一个代码包的时候,此代码包中声明的所有包级变量都将在此包中的任何一个init函数执行之前初始化完毕。

defer函数

go的函数内部可以使用defer关键字,产生类似java的try-catch-finally的finally效果。

但是,go的defer函数可以写多个。最后的执行顺序和声明顺序使相反的。可想而知,是用栈结构来存的多个defer函数。

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
defer fmt.Println("333333")
defer fmt.Println("222222")
fmt.Println("111111")
}

panic恢复

go没有java的异常抛出机制。go产生一个运行时异常称为发生了panic(恐慌)。如果这个panic没有处理,将导致整个应用的崩溃。

消除panic,可以在defer中调用recover函数,这是go内置的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
defer func() {
fmt.Println("正常退出")
}()
fmt.Println("嗨!")
defer func() {
v := recover()
fmt.Println("恐慌被恢复了:", v)
}()
panic("拜拜!") // 产生一个恐慌
fmt.Println("执行不到这里")
}
1
2
3
嗨!
恐慌被恢复了: 拜拜!
正常退出

无论在哪个 Goroutine 中发生未被恢复的 panic,整个程序都将崩溃退出。

一等公民

我们说函数在go语言中是一等公民。怎么理解这句话?

首先什么叫一等公民?这个业界还没有公认的准确定义,引用wiki 发明人的解释

如果一门编程语言对某种语言元素的创建和使用没有限制,我们可以像对待值(value) 一样对待这种语法元素,那么我们就称这种语法元素是这门编程语言的“一等公民”。 拥有“一等公民”待遇的语法元素可以存储在变量中,可以作为参数传递给函数,可以 在函数内部创建并可以作为返回值从函数返回。

也就是说一门语言的一等公民元素,其创建和使用非常灵活,可以用在各个地方。那我们对比下go的函数

  • 存储在变量里

    类似前面匿名函数的例子

    1
    2
    3
    4
    5
    6
    func TestFuncAsParam(t *testing.T) {
    var f = func() string {
    return "hello"
    }
    t.Log(f())
    }
  • 函数作为返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    func TestFuncAsReturn(t *testing.T) {
    var f1 = func() func() string {
    return func() string{
    return "a func"
    }
    }
    var f2 = f1()
    t.Logf("%T", f1())
    t.Log(f2())
    }

    匿名函数的返回值还是一个函数,保存到变量f1

  • 作为参数传到方法里

    1
    2
    3
    4
    5
    6
    7
    func TestTime(t *testing.T) {
    time.AfterFunc(time.Second * 2, func() {
    println("after 2 second...")
    })
    time.Sleep(time.Second * 3)
    t.Log("end")
    }

    比如time包的AfterFunc函数就可以接收一个函数作为参数

  • 函数有自己的类型,函数类型

​ func关键字+函数的参数列表+返回值列表就是一个函数类型。

​ 其中函数的参数列表+返回值列表也成为函数签名

​ 不用管参数或者返回值的名字。无所谓。

​ 函数签名一样的两个函数就是同一个函数类型

​ 因为函数有类型,也就是说函数可以进行显式的类型转换

函数拥有以上众多特性,可以称为go语言的一等公民