什么是 Go 的方法
Go 不同于其他 OOP 编程语言,而是采用 method 来实现 OOP。
如果有一个对象,假如是一个结构体,那么可以这样为其增加方法:
1
2
3
4
5
6
7
8
|
type point struct {
x, y int
}
func (p point) scaleBy(factor int) {
p.x *= factor
p.y *= factor
}
|
其中,附加的参数成为接收者,调用某个对象的方法就相当于向它发送消息,该对象自然也就是一个接收者。
不同于其他编程语言:
值接收者和指针接收者
初学 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
26
|
type point struct {
x, y int
}
// 值接收者将无法改变参数的值
//func (p point) scaleBy(factor int) {
// p.x *= factor
// p.y *= factor
//}
// 指针接收者将可以改变参数的值
func (p *point) scaleBy(factor int) {
p.x *= factor
p.y *= factor
}
func (p point) show() {
fmt.Printf("point.x: %v, point.y: %v\n", p.x, p.y)
}
func main() {
foo := point{1, 2}
foo.show()
foo.scaleBy(2)
foo.show()
}
|
惯例:如果其中一个方法使用了指针接收者,则其他方法也应该使用指针接收者。
编译器的隐式转换
一般 Go 的编译器隐式动作不多,但在值接收者和指针接收者的方法上倒是例外。理论上,如果定义了指针接收者的方法,如:
1
2
3
4
|
func (p *point) scaleBy(factor int) {
p.x *= factor
p.y *= factor
}
|
那么,这样才是合理的调用:
1
2
|
foo := point{1, 2}
(&foo).scaleBy(2)
|
即调用者是一个指针类型变量。但实际上:
1
2
|
foo := point{1, 2}
foo.scaleBy(2)
|
也是正确的,因为编译器会将其转换成 &foo
。
同样的现象也出现在用值接收者定义的方法上。因此有如下几个规则:
-
实参接收者和形参接收者是同一个类型,比如都为 T
和 *T
:
1
2
|
point{1, 2}.show() // point
pptr.scaleBy(2) // *point
|
``
-
实参接收者是 T
的变量,形参接收者是 *T
类型,编译器会隐式获取变量的地址:
1
2
|
p.scaleBy(2) // 隐式转换为 &p
point{1, 2}.scaleBy(2) // 错误,不是变量,而是字面量
|
``
-
实参接收者是 *T
类型而实参接收者是 T
类型,编译器会隐式解引用接收者,获得实际的值:
1
|
pptr.show() // 隐式转换为 (*pptr)
|
``
用组合来实现 OOP
很多编程语言会用继承的方式来实现 OOP,即定义一个基类,然后再在基类的基础上(继承)扩展新的属性,通常这样会把整个对象关系变得异常复杂。
Go 采用 组合 来简化这一切。如:
1
2
3
4
5
6
7
8
|
type point struct {
x, y int
}
type colorPoint struct {
point
color string
}
|
colorPoint
将拥有 point
的方法。我们还可以这样用:
1
2
3
4
5
6
7
8
9
10
11
12
|
var cache = struct {
sync.Mutex
mapping map[string]string
}{
mapping: make(map[string]string),
}
func Lookup(key string) string {
cache.Lock()
defer cache.Unlock()
return cache.mapping[key]
}
|
方法变量和方法表达式
把一个对象的方法赋予一个变量,此时该变量称为方法变量,如:
1
2
3
4
5
|
p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance // 方法变量
fmt.Println(distanceFromP(q)) // 5
|
此时这个函数是与某个对象绑定在一起的。
与其相关的是方法表达式,其通常写为 T.f
或者 *T.f
。如下所示:
1
2
3
4
5
6
7
8
9
10
11
|
p := Point{1, 2}
q := Point{4, 6}
distance := Point.Distance // 方法表达式
fmt.Println(distance(p, q)) // 5
fmt.Printf("%T\n", distance) // func(Point, Point) float64
scale := (*Point).ScaleBy
scale(&p, 2)
fmt.Println(p) // {2 4}
fmt.Printf("%T\n", scale). // func(*Point, float64)
|
封装
在 Go 中,要封装一个对象,必须使用结构体,且 Go 封装的单元是包而不是类型。
如这个对象:
1
2
3
|
type IntSet struct {
words []uint64
}
|
则 IntSet
可对外暴露,但是 words
只能在同一个包内访问。