什么是 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 没有 this 和 self 的概念,可自定义接收者的名字

  • 任何类型都可以绑定方法(除了本身已经是指针的类型,比如 *int

值接收者和指针接收者

初学 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

同样的现象也出现在用值接收者定义的方法上。因此有如下几个规则:

  1. 实参接收者和形参接收者是同一个类型,比如都为 T*T

    1
    2
    
    point{1, 2}.show() // point
    pptr.scaleBy(2)    // *point
    

    ``

  2. 实参接收者是 T 的变量,形参接收者是 *T 类型,编译器会隐式获取变量的地址

    1
    2
    
    p.scaleBy(2)           // 隐式转换为 &p
    point{1, 2}.scaleBy(2) // 错误,不是变量,而是字面量
    

    ``

  3. 实参接收者是 *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 只能在同一个包内访问。