Skip to content

老陈是一个普通的文艺二逼青年

For The Dream

Golang 入门(六) —— 面向对象编程

Written by chen

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的存在,这既是匿名组合的魅力所在。

Go语言编程 · golang

Copyright © 2022 老陈是一个普通的文艺二逼青年. 沪ICP备13044041号-1