Go语言的面向对象设计的非常简洁。简洁在于Go语言并没有沿袭传统面向对象编程中的诸多概念,比如继承、虚函数、构造函数、析构函数、隐藏this指针等。
1、类型系统
类型系统是指一个语言的类型体系结构。一个典型的类型系统通常包含如下基本内容:
- 基础类型:如
byte
、int
、bool
、float
等 - 复合类型:如数组、结构体、指针等
- 可以指向任意对象的类型
Any类型
- 值语义和引用语义
- 面向对象,即所有具备面向对象特征的类型
- 接口
在Go语言中,可以为任意类型(包括内置类型、但不包括指针类型)添加相应的方法。例如:
1 2 3 4 5 6 |
type Integer int func (a Integer) Less(b Integer) bool { return a < b } |
在这个例子中,我们定义了一个类型Integer
,他和int
没有本质的区别,只是它为内置的int
类型增加了一个新的方法Less()
。
1 2 3 4 5 |
func main() { var a Integer = 1 fmt.Println(a.Less(2)) } |
在Go语言中没有隐藏的this
指针:
- 方法施加的目标(也就是对象)显示传递,没有被隐藏起来
- 方法施加的目标(也就是对象)不需要非得是指针,也不用非得是
this
在php
中,使用写一段差不多意思的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Integer { public $val = 0; public function __construct( $val ) { $this -> val = $val; } public function less(Integer $b) { return $this -> val < $b -> val; } } $a = new Integer(1); $b = new Integer(2); var_dump( $a -> less( $b ) ); |
初学者可能会比较难离理解其背后的机制,一直this
到底从何而来。其实能是php
自动隐藏了this指针
,如果不隐藏的话,代码可能会这个样子:
1 2 3 4 |
public function less(Integer $this, Integer $b) { return $this -> val < $b -> val; } |
Go语言就非常直观的表现出来了,我们重写一下上面的例子:
1 2 3 4 5 6 |
type Integer int func (this Integer) Less(b Integer) bool { return this < b } |
就会发现,原来this
是这么过来的。
如果要求对象必须以指针形式传递,这有时会是个额外的成本,因为对象有时很小(比如4字节),用指针传递并不划算。
只有在你需要修改对象的时候,才必须使用指针。它不是Go语言的约束,而是一种自然的约束。
举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
func (a *Integer) Add(b Integer) { *a += b } func (a Integer) FakeAdd(b Integer) { a += b } // 会增加5 a.Add(5) fmt.Println(a) // 不会增加 a.FakeAdd(5) fmt.Println(a) |
2、值语义和引用语义
值语义和引用语义的差别在于赋值,比如下面的例子:
1 2 3 |
b = a b.Modify() |
如果b
的修改没有影响到a
,那么此类型属于值类型,反之为引用类型。
Go语言中的大多数类型都是值语义,包括:
- 基本类型:
byte
、int
、float32
、float64
、string
等 - 符合类型:
array
、struct
、pointer
等
Go语言中的数组和基本类型没有什么区别,是很纯粹的值类型。
1 2 3 4 5 |
var a = [3]{1, 2, 3} var b = a b[1]++ fmt.Println(a, b) // [1 2 3] [1 3 3] |
如果希望完全复制,需要用到指针
1 2 3 4 5 |
var a = [3]{1, 2, 3} var b = &a b[1]++ fmt.Println(a, *b) // [1 3 3] [1 3 3] |
Go语言中有4个类型比较特别,看起来像引用类型,
- 切片: 指向数组的一个区间
- map: 极其常见的数据结构,提供键值查询能力
- channel: 执行体
goroutine
间的通信设施 - 结构: 对一组满足某个且越的类型的抽象
但是这并不影响我们将Go语言看作值语义。
切片本事上是一个区间,大致可以将[]T
表示为:
1 2 3 4 5 6 |
type slice struct { first *T len int cat int } |
因为数组切片内部是指针,所以可以改变所指向的数组元素并不奇怪。切片类型本身的复制仍然是值语义。
map
本质上是一个字典指针,你可以大致将map[K]V
表示为:
1 2 3 4 5 6 7 8 |
<br />type Map_K_V struct { // ... } type Map[K]V struct { impl *Map_K_V } |
基于指针,我们完全可以创建一个引用类型,如:
1 2 3 4 |
type IntegerRef struct { impl *int } |
channel
和map
类似,本质上是一个指针。将他们设计为引用类型的原因是,完整复制一个channel
或map
并不是常规需求。
同样,接口具备引用语义,是因为维持了两个指针,示意为:
1 2 3 4 5 |
type interface struct { data *void itab *Itab } |
3、结构体
Go语言的结构体和其他语言的类有同等的地位,但是Go语言放弃了包括继承在内的大量面向对象特性,只保留了组合这个最基础的特新。
上面我们说道,所有的Go语言类型(指针除外)都可以有自己的方法。在这个背景下,Go语言的结构体只是很普通的复合类型,平淡无奇。
1 2 3 4 5 6 7 8 9 |
type Rect struct { x, y float64 width, height float64 } func (r *Rect) Area() float64 { return r.width * r.height } |
定义了Rect
类型后,该如何创建初始化对象实例呢?
1 2 3 4 5 |
r1 := new(Rect) r2 := &Rect{} r3 := &Rect{0, 0, 100, 200} r4 := &Rect{width: 100, height: 200} |
Go语言中未进行显示初始化的变量都为被初始化为该类型的零值。
Go语言虽然不支持继承,但是我们可以采用组合的方式,来完成继承所做的事情,我们将其称之为匿名组合:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
type Base struct { Name string } func (base *Base) Bar() { fmt.Println("Bar") } type Foo struct { Base age int } func (foo *Foo) Bar() { foo.Base.Bar() } |
以上代码实现了Foo
继承了Base
,并且Foo
重写Base
的Bar
方法。
在Go语言官方网站提供了Effective Go
中提到一个匿名组合的例子:
1 2 3 4 5 6 7 8 9 10 |
type Job struct { Command string *log.Logger } func (job *Job) Start() { job.Println("Starting now") // ... } |
在适合的赋值后,我们在Job类型的所有成员方法中可以很舒适的借用log.Logger
提供的方法。
1 2 3 4 |
// 调用 j := &Job{"Start", log.New(os.Stderr, "Job: ", log.Ldate)} j.Start() |
对Job的实现者来说,根本就不需要意识到log.Logger
的存在,这既是匿名组合的魅力所在。