Go 语言没有类的概念,但是你可以为某个类型定义 方法( method )。
方法 是一个带 接收者参数 的特殊函数。接收者参数位于 func 关键字与方法名之间,以括号包围。
下面这个例子中, Abs 方法有一个 Vertex 类型的接收者参数 v :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package main
import (
"fmt"
"math"
)
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
}
|
再次强调:方法只是一个带有接收者参数的函数而已 。
你可以重写 Abs ,将其实现成一个普通函数,功能上并没有任何区别:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package main
import (
"fmt"
"math"
)
type Vertex struct {
X, Y float64
}
func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
fmt.Println(Abs(v))
}
|
非结构体方法
不仅 结构体 可以定义方法,其他任何自定义类型都可以。
这就是一个例子,为数值类型 MyFloat 定义方法 Abs :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package main
import (
"fmt"
"math"
)
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
func main() {
f := MyFloat(-math.Sqrt2)
fmt.Println(f.Abs())
}
|
方法和对应类型定义必须在同一个 包 定义。
指针接受者
方法接收者可以定义成 指针 。
这样一来,对于类型 T 来说,接收者参数的类型就是 *T 。需要注意的是, T 本身不能是指针,比如 *int 。
例子中, Scale 方法就定义在 *Vertex 上:
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"
"math"
)
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
v.Scale(10)
fmt.Println(v.Abs())
}
|
接收者参数定义成指针的好处是,方法代码可以修改指针指向的值。 由于方法经常需要修改对应的值,因此指针接收者参数相对来说更常用。
读者可以自行修改程序,将 *
号从 Scale 方法移除,并观察程序行为。 不出意外,你将看到程序输出 5 。换句话讲,目标值并没有被修改。 这是为啥呢?
如果定义 值接收者( value receiver ), Scale 方法相当于在原 Vertex 值的一个拷贝上操作(同样适用于其他参数)。因此,为了修改 Vertex 值,接收者参数必须定义成指针。
传值与传引用
接下来,我们将 Abs 和 Scale 方法重写成普通函数。
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"
"math"
)
type Vertex struct {
X, Y float64
}
func Abs(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func Scale(v *Vertex, f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
Scale(&v, 10)
fmt.Println(Abs(v))
}
|
同样,将 *
号从 Scale 函数移除会怎样? 不出意外,结果是类似的。
这其实是编程里最经典的 传值 、 传引用 问题, 传指针相当于传引用 。
间接传指针
对比上面两个程序,你可能已经注意到了——带指针参数的函数只能传指针:
1
2
3
|
var v Vertex
ScaleFunc(v, 5) // Compile error!
ScaleFunc(&v, 5) // OK
|
然而,对于方法,不管接收者是一个值还是指针,均可调用:
1
2
3
4
5
|
var v Vertex
v.Scale(5) // OK
p := &v
p.Scale(10) // OK
|
对于语句 v.Scale(5)
,尽管 v 是一个值而不是指针,还是自动调用了带指针接收者参数的方法。这是因为,Scale 方法需要指针接收者参数, Go 按照惯例将 v.Scale(5)
解释成: (&v).Scale(5)
。这就是 间接传指针 ,或者叫做 隐式传指针 。
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
|
package main
import "fmt"
type Vertex struct {
X, Y float64
}
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func ScaleFunc(v *Vertex, f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
v.Scale(2)
ScaleFunc(&v, 10)
p := &Vertex{4, 3}
p.Scale(3)
ScaleFunc(p, 8)
fmt.Println(v, p)
}
|
间接传值
对普通 函数 来说,值参数只能传对应类型的值,传指针则导致编译错误:
1
2
3
|
var v Vertex
fmt.Println(AbsFunc(v)) // OK
fmt.Println(AbsFunc(&v)) // Compile error!
|
相反,就算方法定义了值接收者参数,用指针调用也是可以的:
1
2
3
4
5
|
var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK
|
在这,方法调用语句 p.Abs()
则被解释成: (*p).Abs()
。 这就是 间接传值 ,或者叫做 隐式传值 。
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"
"math"
)
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func AbsFunc(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
fmt.Println(v.Abs())
fmt.Println(AbsFunc(v))
p := &Vertex{4, 3}
fmt.Println(p.Abs())
fmt.Println(AbsFunc(p))
}
|
传值还是传指针
那么,接收者参数到底是实现成值还是指针呢? 如何选择?
使用指针接收者参数主要有两方面考虑:
首先,只有这种方式能够对指向的值进行修改。
其次,从性能方面考虑,使用指针可以避免在每次调用方法时拷贝值。 这种方式相对来说更高效,特别是当接收者 结构体 很大很复杂时。
在这个例子, Scale 方法和 Abs 方法接收者参数类型均为 *Vertex ,尽管 Abs 方法并不修改其接收者:
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
|
package main
import (
"fmt"
"math"
)
type Vertex struct {
X, Y float64
}
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := Vertex{3, 4}
fmt.Println("Before scaling: %+v, Abs: %v\n", v, v.Abs())
v.Scale(5)
fmt.Println("After scaling: %+v, Abs: %v\n", v, v.Abs())
}
|
通常,不管为何种类型编写方法,均需要定义 值接收者 或者 指针接收者 ,但不能混用。
【小菜学Go语言】系列文章首发于公众号【小菜学编程】,敬请关注: