go-函数与方法

函数与方法

函数

每个函数声明都包含一个名字、一个形参列表、一个可选的返回列表以及函数体:

1
2
3
func name(parameter-list) (result-list) {
...
}

如果几个形参或者返回值的类型相同,那么类型只需要写一次:

1
2
3
func f(i, j, k int, s, t string) {...}
// 二者声明是一样的效果
func f(i int, j int, k int, s string, t string) {...}

函数类型称作为函数签名。当两个函数拥有相同的形参列表和返回列表时,认为这两个函数类型或签名是相同的。而形参和返回值的名字不会影响到函数类型。

形参变量都是函数的局部变量,初始值有调用者提供的实参传递。函数形参以及命名返回值同属于函数最外层作用域的局部变量

实参是按值传递的,所以函数接收到的是实参的副本;修改函数的形参变量并不会影响到调用者提供的实参。然而,如果提供的实参包含引用类型,比如指针、slice、map、函数或者通道,那么当函数使用形参变量时就有可能会间接地修改实参变量

偶尔会看到有些函数的声明没有函数体,那说明这个函数使用除了Go以外的语言实现。这样的声明定义了该函数的签名:

1
2
package math
func Sin(x float) float64 // 使用汇编语言实现

函数变量

函数变量就是把函数名赋值给一个变量:

1
2
3
4
5
6
7
8
9
10
11
12
func square(n int) int {return n * n}
func negative(n int) int {return -n}
func product(m, n int) int {return m * n}
f := suquare // 函数名赋值给变量
fmt.Println(f(3)) // 9
f = negative
fmt.Println(f(3)) // -3
fmt.Printf("%T\n", f) // "func(int) int"
f = product //编译错误:不能把类型func(int, int) int 赋值给func(int) int

函数类型的零值是nil(空值), 调用一个空的函数变量将报错:

1
2
var f func(int) int
f(3) // 报错:调用空函数

函数变量可以和空值比较:

1
2
3
4
var f func(int) int
if f != nil {
f(3)
}

但函数本身不可比较,所以不可以互相进行比较或者作为键值出现在map中

匿名函数

在func关键字后面没有函数的名字,它就是一个表达式,它的值称为匿名函数

变长函数

在参数列表最后的类型名称之前使用省略号: “…”表示声明一个变长函数,调用这个函数时可以传递该类型任意数目的参数:

1
2
3
4
5
6
7
8
9
10
func sum(varls ...int) int {
total := 0
for _, val := range varls{
total += val
}
return total
}
fmt.Println(sum()) // 0
fmt.Println(sum(3)) // 3
fmt.Println(sum(1, 2, 3, 4)) // 10

上面sum函数返回零个或者多个int参数。在函数体内,vals是一个int类型的slice
上面最后一个调用和下面的调用作用是一样的:

1
2
values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) // 10 , 直接在最后一个参数后面放了一个省略号

defer 延迟函数调用

语法上,一个defer语句就是一个普通的函数或方法调用,在调用之前加上关键字defer。函数和参数表达式会在语句执行时求值,但是无论是正常情况下,执行return语句或函数执行完毕,还是不正常的情况下,比如发生宕机,时间的调用推迟到包含defer语句的函数结束后才执行。defer语句没有限制使用次数;执行的时候以调用defer语句顺序的倒序进行。
defer语句经常适用于成对的操作,比如打开和关闭,连接和断开,加锁和解锁:

1
2
3
4
5
6
7
8
9
package ioutil
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.close()
return ReadAll(f)
}

函数ReadFile包含了defer语句,所以当ReadFile执行完毕后,最后才执行f.close()

恢复

内建函数panic和recover

方法

方法的声明和普通函数的声明类似,只是在函数名字前面多了一个参数。这个参数把这个方法绑定到这个参数对应的类型上:

值接收者的方法

如果使用值接收者声明方法,调用时会使用这个值的一个副本来执行。

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
// 定义了一个user类型
type user struct {
name string
email string
}
// 使用值接收者实现了一个方法
func (u user) notify() {
fmt.Printf("Sending User Email To %s<%s>\n", u.name, u.email)
}
// 使用指针接收者实现一个方法
func (u *user) changeEmail(email string) {
u.email = email
}
func main() {
// user类型的值可以用来调用使用值接收者声明的方法
bill := user{"Bill", "bill@email.com"}
bill.notify()
// 指向user类型值的指针也可以用来调用使用值接收者声明的方法
lisa := &user{"Lisa", "lisa@email.com"}
lisa.notify()
// user类型的值可以用来调用指针接收者声明的方法
bill.changeEmail("bill@newdomain.com")
bill.notify()
// 指向user类型值的指针可以用来调用使用指针接收者声明的方法
lisa.changeEmail("lisa@newdoamin.com")
lisa.notify()
}

附加参数p称为方法的接收者

Go语言中,接收者不适用特殊名(比如this或者self);而是我们自己选择接收者名字,就像其他的参数变量一样。

同一个包下的任何类型都可以声明方法,只要它的类型既不是指针类型也不是接口类型

指针接收者的方法

由于主调函数会赋值每一个实参变量,如果函数需要更新一个变量,或者如果一个实参太大而我们希望避免复制整个实参,因为我们必须使用指针来传递变量的地址。这也同样适用于更新接收者:我们将它绑定到指针类型,比如:*user:

1
2
3
4
// 使用指针接收者实现一个方法
func (u *user) changeEmail(email string) {
u.email = email
}

这个方法的名字是:(user).changeEmail。圆括号是必须的;没有圆括号,表达式会被解析为:(user.changeEmail)

本身是指针的类型是不能声明为指针接收者的方法:

1
2
type P *int
func (P) f( {/*...*/}) // 编译出错:非法的接收者类型

方法变量

同函数变量

1
2
3
4
5
6
7
8
9
10
11
12
13
type Point struct{X, Y float64}
// Point类型的方法, 就是给结果Point绑定了一个Distance方法,该方法又接收了一个Point类型的参数
fucnt (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X), q.Y-p.Y
}
p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance // 方法变量
fmt.Println(distanceFromP(q)) // 5
var origin Point // {0, 0}
fmt.Println(distanceFromP(origin))

小结

  • 值接收者使用值的副本来调用方法,而指针接收者使用实际值来调用方法