学习go语言编程之面向对象

  1. 类型系统
    1. 为类型添加方法
    2. 值语义和引用语义
    3. 结构体
  2. 初始化
  3. 匿名组合
  4. 可见性
  5. 接口
    1. 非侵入式接口
    2. 接口赋值
    3. 接口查询
    4. 类型查询
    5. 接口组合
    6. Any类型

类型系统

类型系统是指一个语言的类型体系结构,一个典型的类型系统通常包含如下基本内容:

  • 基础类型,如:byte、int、bool、float等
  • 复合类型,如:数组、结构体、指针等
  • 可以指向任意对象的类型(Any类型)
  • 值语义和引用语义
  • 面向对象,即:所有具备面向对象特征(比如成员方法)的类型
  • 接口

为类型添加方法

在Golang中,可以给任意类型(包括内置类型,但不包括指针类型)添加相应的方法。

type Integer int

func (a Integer) Less(b Integer) bool {
	return a < b
}

如上示例,定义了一个新的类型Integer,它和int没有本质不同,只是它为内置的int类型增加了个新方法Less()

这样实现了Integer后,就可以让整型像一个普通的类一样使用:

func main() {
	var a Integer = 1
	if a.Less(2) {
		fmt.Println(a, "less 2")
	}
}

Golang中的面向对象最为直观,也无需支付额外的成本。如果要求对象必须以指针传递,这有时会是个额外成本,因为对象有时很小(比如4个字节),用指针传递并不划算。
只有在需要修改对象的时候才必须使用指针。

// 需要修改对象的时候才必须使用指针
func (a *Integer) Add(b Integer) {
	*a += b
}

值语义和引用语义

值语义和引用语义的差别在于赋值,如下示例:

b = a
b.Modify()

如果b的修改不会影响a的值,那么此类型就是值类型;如果会影响a的值,那么此类型就是引用类型。

Golang中的大多数类型都是值语义,包括:

  • 基本类型,如:byte、int、bool、float32、float64和string等
  • 复合类型,如:数组、结构体、指针等

Golang中的数组与基本类型没有区别,是很纯粹的值类型。

var a = [3]int{1, 2, 3}
var b = a
b[1]++
fmt.Println(a)
fmt.Println(b)

输出:

[1 2 3]
[1 3 3]

b[1]++的结果并没有影响到a[1]的值,这表明b=a赋值语句是数据内容的完整复制。

要想表达引用,需要使用指针:

var a = [3]int{1, 2, 3}
var b = &a
b[1]++
fmt.Println(a)
fmt.Println(*b)

输出:

[1 3 3]
[1 3 3]

b[1]++的结果影响到了a[1]的值,这表明b=&a赋值语句是数组内容的引用。变量b的类型不是[3]int,而是*[3]int

Golang中有4个类型比较特别,看起来像是引用类型,包括:

  • 数组切片:指向数组(array)的一个区间
  • map:极其常见的数据结构,提供键值查询能力
  • channel:执行体(goroutine)间的通信设施
  • 接口(interface):对一组满足某个契约的类型的抽象

结构体

Golang放弃了包括继承在内的大量面向对象特性,只保留了组合这个最基础的特性。
前面已经说过,所有的Golang类型(指针类型除外)都可以有自己的方法。在这个背景下,Golang的结构体只是很普通的复合类型。

// 定义一个矩形类型
type Rect struct {
	x, y          float64
	width, height float64
}

定义一个成员方法来计算矩形的面积:

// 定义一个成员方法来计算面积
func (r *Rect) Area() float64 {
	return r.width * r.height
}

初始化

创建并初始化类型的对象实例有多种方式:

// 创建初始化自定义类型对象实例
rect1 := new(Rect)
rect2 := &Rect{}
rect3 := &Rect{0, 0, 100, 200}
rect4 := &Rect{width: 100, height: 200}

在Golang中,未进行显示初始化的变量都会被初始化为该类型的零值(如bool类型的零值为false,int类型的零值为0,string类型的零值为空字符串)。
在Golang中,没有构造函数的概念,对象的创建通常交给一个全局的创建函数来完成,以NewXXX类命名,表示构造函数。

// 创建类型对象的全局函数
func NewRect(x, y, width, height float64) *Rect {
	return &Rect{x, y, width, height}
}

匿名组合

确切地说,Golang也提供了继承,但是采用了组合的文法,所以称其为匿名组合。

type Base struct {
	Name string
}

func (base *Base) Foo() {
    fmt.Println("This is foo in Base")
}
func (base *Base) Bar() {}

type Foo struct {
	Base
}

func (foo *Foo) Bar() {
	foo.Base.Bar()
}

如上示例代码定义了一个Base类(实现了Foo()和Bar()两个成员方法),然后定义了一个Foo类,该类从Base类“继承”并改写了Bar()方法(该方法实现时先调用了基类的Bar()方法)。
在“派生类”Foo中没有改写“基类”的Base的成员方法时,相应的方法就被“继承”,例如调用foo.Foo()和调用foo.Base.Foo()效果一致。

func main() {
    foo := new(Foo)
	foo.Foo()      // 输出:This is foo in Base
	foo.Base.Foo() // 输出:This is foo in Base
}

与其他语言不同,Golang很清晰地告诉了类的内存布局是怎样的。
此外,在Golang中还可以随心所欲地修改内存布局。

type Foo struct {
	// ...
	Base
}

这段代码从语义上来说,与其他例子并无不同,但是内存布局发生了变化:“基类”Base的数据放在了“派生类”Foo的最后。

另外,在Golang中还可以以指针的方式从一个类型“派生”:

type Foo struct {
	*Base
}

这段代码依然有“派生”的效果,只是在创建Foo实例的时候,需要外部提供一个Base类实例的指针。

可见性

Golang没有提供类似privateprotectedpublic这样表示可见性的关键字,要使某个符号对其他包可见,需要将该符号定义为大写字母开头。

type Rect struct {
	X, Y          float64
	Width, Height float64
}

这样Rect类型的全部成员就被导出了,可以被其他引用了Rect所在包的代码访问到。

成员方法的可见性也遵循同样的规则,如:

func (r *Rect) area() float64 {
	return r.Width * r.Height
}

这样Rect类型的成员方法area()就只能在该类型所在包内使用。

接口

非侵入式接口

在Golang中,一个类只需要实现了接口要求的所有函数,就可以说这个类实现了该接口。

如下定义一个File类,并实现了ReadWriteSeekClose方法。

// 定义一个File类
type File struct {
}

func (f *File) Read(buf []byte) (n int, err error) {
	return 0, nil
}
func (f *File) Write(buf []byte) (n int, err error) {
	return 0, nil
}
func (f *File) Seek(off int64, whence int) (pos int64, err error) {
	return 0, nil
}
func (f *File) Close() error {
	return nil
}

假设有如下接口:

type IFile interface {
	Read(buf []byte) (n int, err error)
	Write(buf []byte) (n int, err error)
	Seek(off int64, whence int) (pos int64, err error)
	Close() error
}

type IReader interface {
	Read(buf []byte) (n int, err error)
}

type IWriter interface {
	Write(buf []byte) (n int, err error)
}

type ICloser interface {
	Close() error
}

尽管File类并没有从这些接口继承,甚至可以不知道这些接口的存在,但是File类实现了这些接口(实现了接口中的所有函数),所以如下赋值是正确的:

var file1 IFile = new(File)
var file2 IReader = new(File)
var file3 IWriter = new(File)
var file4 ICloser = new(File)

接口赋值

接口赋值在Golang中分为两种情况:

  • 将对象实例赋值给接口
  • 将一个接口赋值给另一个接口

将某种类型的实例赋值给接口,这要求该对象实例实现了接口要求的所有方法。

// Integer是一个新的类型
type Integer int

func (a Integer) Less(b Integer) bool {
	return a < b
}

func (a *Integer) Add(b Integer) {
	*a += b
}

// LessAdder是一个接口类型
type LessAdder interface {
	Less(b Integer) bool
	Add(b Integer)
}

显然,Integer类实现了LessAdder接口的所有方法,所以可以将Integer类实例赋值给LessAdder接口类型。

var a Integer = 1
var b LessAdder = &a

将一个接口赋值给另一个接口,要求两个接口拥有相同的方法列表(方法次序可以不同)。

type ReadWriter interface {
	Read(buf []byte) (n int, err error)
	Write(buf []byte) (n int, err error)
}

type IStream interface {
	Write(buf []byte) (n int, err error)
	Read(buf []byte) (n int, err error)
}

如上,两个接口类型ReadWriterIStream拥有相同的方法列表,在Golang中这两个接口并无差别。
如下赋值是完全正确的:

var file1 ReadWriter = new(File) // 创建一个ReadWriter类型
var file2 IStream = file1        // 将ReadWriter类型赋值给IStream类型
var file3 ReadWriter = file2     // 将IStream类型赋值给ReadWriter类型

接口赋值并不要求两个接口必须等价:如果接口A的方法列表是接口B的方法列表的子集,那么接口B可以赋值给接口A,反过来并不成立。

type Writer interface {
	Write(buf []byte) (n int, err error)
}

var file1 ReadWriter = new(File)
var file2 Writer = file1     // 将“大”接口赋值给小接口可以
var file3 ReadWriter = file2 // 编译报错,不能将“小”接口赋值给大接口

接口查询

接口查询的语法如下:

// 判断file1对象(Writer类型)是否实现了IStream接口,如果实现了则执行特定的代码
var file1 Writer = new(File)
if file2, ok := file1.(IStream); ok {
    fmt.Println("file1 implement interface IStream, file2: ", file2)
}

还可以询问接口指向的对象是否是某个类型:

// 判断file1对象(Writer类型)是否属于File类型
var file1 Writer = new(File)
if file3, ok := file1.(*File); ok {
    fmt.Println("file1 is File type, file3: ", file3)
}

类型查询

在Golang中可以直接了当地询问接口只想的对象实例的类型:

var v1 interface{} = new(File)
switch v := v1.(type) {
case *File:
    fmt.Println(v, "is File type")
default:
    fmt.Println(v, "is Unknown type")
}

接口组合

// io包中的ReadWriter接口将基本的Read和Write方法组合起来
type ReadWriter interface { 
    Reader 
    Writer 
}

ReadWriter接口组合了ReaderWriter两个接口,它完全等同于如下写法:

type ReadWriter interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
}

因为这两种写法的表意完全相同:ReadWriter接口既能做Reader接口的所有事情,又能做Writer接口的所有事情。

可以认为接口组合是类型匿名组合的一个特定场景,只不过接口只包含方法,而不包含任何成员变量。

Any类型

Golang中的任何对象都满足空接口interface{},所以interface{}看起来像是可以指向任何对象的Any类型,如下:

var v1 interface{} = 1                    // 将int类型赋值给interface{}
var v2 interface{} = "abc"                // 将string类型赋值给interface{}
var v3 interface{} = &v2                  // 将*interface{}类型赋值给interface{}
var v4 interface{} = struct{ x int }{1}   // 将结构体类型赋值给interface{}
var v5 interface{} = &struct{ x int }{1}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,在下面评论区告诉我^_^^_^