Go-面向对象

171 阅读8分钟

在讲解 Go 语言面向对象内容之前,需要说明下 Go 语言的代码是以包结构来组织的,且如果标示符(变量名,函数名,自定义类型等)如果以大写字母开头那么这些标示符是可以导出的,可以在任何导入了定义该标示符的包的包中直接使用。Go 语言中的面向对象和 C++,Java 中的面向对象不同,因为 Go 语言不支持继承,Go 语言只支持组合。

自定义类型

Go 语言的中结构体 struct 与 C++、JAVA 中的类 class 相似,但 Go 放弃了传统面向对象的诸多特性,只保留了组合。

  • type typeName typeSpecification

其中,typeName 可以是一个包或者函数内唯一合法的 Go 标示符。typeSpecification 可以是任何内置的类型,一个接口或者是一个结构体。所谓结构体,它的字段是由其他类型或者接口组成。例如我们通过结构体定义了一下类型:

type ColorPoint struct {
    color.Color     // 匿名字段(嵌入)
    x, y int        // 具名字段(聚合)
}

以上代码我们通过结构体自定义了类型 ColorPoint,结构体中 color.Color 字段是 Color 包的类型 color,这个字段没有名字,所以被称为匿名的,也是嵌入字段。字段 x 和 y 是有变量名的,所以被称为具名字段。假如我们创建了类型 ColorPoint 的一个值 point(通过语法:point := ColorPoint{} 创建),那么这些字段可以通过 point.Color、point.x、point.y 访问。其他面向对象语言中的"类 (class)"、"对象 (object)"、"实例 (instance)"在 Go 语言中我们完全避开使用。相反的我们使用"类型 (type)"和其对应的"值",其中自定义类型的值可以包含方法。

定义了结构体后如何创建并初始化一个对象实例呢?Go 语言支持以下几种方法进行实现:

//先定义一个结构体Man
type Man struct{
    name string
    age int
}
//对象创建与初始化
man := new(Man)
man := &Man{}
man := &Man{"Tom", 18}
man := &Man{name: "Tom", age: 18}

为了更加方便的创建对象,我们一般会使用一个全局函数来完成对象的创建,这和传统的“构造函数”类似。

func NewMan(name string, age int) *Man {
    return &Man{name, age}
}

方法

方法是作用在自定义类型上的一类特殊函数,通常自定义类型的值会被传递给该函数,该值可能是以指针或者复制值的形式传递。定义方法和定义函数几乎相同,只是需要在 func 关键字和方法名之间必须写上接接受者。例如我们给类型 Count 定义了以下方法:

type Count int

func (count *Count) Increment() { *count++ }  // 接受者是一个 `Count` 类型的指针
func (count *Count) Decrement() { *count-- }
func (count Count) IsZero() bool { return count == 0 }

以上代码中,我们在内置类型 int 的基础上定义了自定义类型 Count,然后给该类型添加了 Increment()、Decrement() 和 IsZero() 方法,其中前两者的接受者为 Count 类型的指针,后一个方法接收 Count 类型的值。

类型的方法集是指可以被该类型的值调用的所有方法的集合。

一个指向自定义类型的值的指针,它的方法集由该类型定义的所有方法组成,无论这些方法接受的是一个值还是一个指针。如果在指针上调用一个接受值的方法,Go 语言会聪明地将该指针解引用。

一个自定义类型值的方法集合则由该类型定义的接收者为值类型的方法组成,但是不包括那些接收者类型为指针的方法。

其实这些限制 Go 语言帮我们解决的非常好,结果就是我们可以在值类型上调用接收者为指针的方法。假如我们只有一个值,仍然可以调用一个接收者为指针类型的方法,这是因为 Go 语言会自动获取值的地址传递给该方法,前提是该值是可寻址的。

在以上定义的类型 Count 中,*Count 方法集是 Increment(), Decrement() 和 IsZero(),Count 的值的方法集是 IsZero()。但是因为 Count 类型的是可寻址的,所以我们可以使用 Count 的值调用全部的方法。

另外如果结构体的字段也有方法,我们也可以直接通过结构体访问字段中的方法。下面让我们练习下,创建源文件 struct_t.go,输入以下代码:

package main

import "fmt"

type Count int // 创建自定义类型 Count

func (count *Count) Increment()  { *count++ } // Count类型的方法
func (count *Count) Decrement()  { *count-- }
func (count Count) IsZero() bool { return count == 0 }

type Part struct { // 基于结构体创建自定义类型 Part
    stat  string
    Count // 匿名字段
}

func (part Part) IsZero() bool { // 覆盖了匿名字段Count的IsZero()方法
    return part.Count.IsZero() && part.stat == "" // 调用了匿名字段的方法
}

func (part Part) String() string { // 定义String()方法,自定义了格式化指令%v的输出
    return fmt.Sprintf("<<%s, %d>>", part.stat, part.Count)
}

func main() {
    var i Count = -1
    fmt.Printf("Start "Count" test:\nOrigin value of count: %d\n", i)
    i.Increment()
    fmt.Printf("Value of count after increment: %d\n", i)
    fmt.Printf("Count is zero t/f? : %t\n\n", i.IsZero())
    fmt.Println("Start: "Part" test:")
    part := Part{"232", 0}
    fmt.Printf("Part: %v\n", part)
    fmt.Printf("Part is zero t/f? : %t\n", part.IsZero())
    fmt.Printf("Count in Part is zero t/f?: %t\n", part.Count.IsZero()) // 尽管覆盖了匿名字段的方法,单还是可以访问

}

以上代码中,我们创建了 Count 类型,然后在其基础上又创建了结构体类型 Part。我们为 Count 类型定义了 3 个方法,并在 Part 类型中创建了方法 IsZero() 覆盖了其匿名字段 Count 中 IsZero() 方法。但是我们还是可以二次访问到匿名字段中被覆盖的方法。执行代码,输出如下:

$ go run struct_t.go
Start "Count" test:


Origin value of count: -1
Value of count after increment: 0
Count is zero t/f? : true

Start: "Part" test:
Part: <<232, 0>>
Part is zero t/f? : false
Count in Part is zero t/f?: true

组合

Go 语言虽然抛弃了继承,但是却提供了一个更加方便的组合特性。相对于继承的编译期确定实现,组合的运行态指定实现,更加灵活。下面通过一段代码来了解组合的基本属性以及它与继承的不同之处。

先定义一个结构体 Base, 并为它添加两个方法 Foo() 和 Bar()

type Base struct{
    Name string
}

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

type Seed struct {
    Base
    ...
}

func (s *Seed) Foo() {
    s.Base.Foo()
    s.Bar()
    ...
}

上面代码先定义了一个 Base 类,然后定义了一个 Seed 类。Seed 类“继承”了 Base 类的所有成员属性和方法并重写了 Foo() 方法。同时在重写 Foo() 方法时调用了 Base 类的 Foo() 方法和 Bar() 方法。需要注意的是,若此时 Seed 的对象通过 s.Foo() 调用 Foo()方法时,实际调用的是 Seed 重写过后的 Foo() 方法,而不是基类 Base 的 Foo() 方法,若想调用 Base 类的 Foo() 方法则要使用 s.Base.Foo,而调用没有重写的 Bar() 方法时,使用 s.Bar() 和s.Base.Bar() 效果是一样的。

接口

在 Go 中,接口是一组方法签名。当一个类型为接口中的所有方法提供定义时,它被称为实现该接口。它与 oop 非常相似。接口指定类型应具有的方法,类型决定如何实现这些方法。

接口基础

之所以说 Go 语言的面向对象很灵活,很大一部分原因是由于接口的存在。接口是一个自定义类型,它声明了一个或者多个方法签名,任何实现了这些方法的类型都实现这个接口。infterface{} 类型是声明了空方法集的接口类型。任何一个值都满足 interface{} 类型,也就是说如果一个函数或者方法接收 interface{} 类型的参数,那么任意类型的参数都可以传递给该函数。接口是完全抽象的,不能实例化。接口能存储任何实现了该接口的类型。直接看例子吧,创建源文件 interface_t.go,输入以下代码:

package main

import "fmt"

type Human struct { // 结构体
    name  string
    age   int
    phone string
}

//Human实现SayHi方法
func (h Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

//Human实现Sing方法
func (h Human) Sing(lyrics string) {
    fmt.Println("La la la la...", lyrics)
}

type Student struct {
    Human  //匿名字段
    school string
    loan   float32
}

type Employee struct {
    Human   //匿名字段
    company string
    money   float32
}

// Employee重载Human的SayHi方法
func (e Employee) SayHi() {
    fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
        e.company, e.phone)
}

// Interface Men被Human,Student和Employee实现
// 因为这三个类型都实现了这两个方法
type Men interface {
    SayHi()
    Sing(lyrics string)
}

func main() {
    mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}
    paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}
    sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}
    Tom := Employee{Human{"Tom", 37, "222-444-XXX"}, "Things Ltd.", 5000}

    //定义Men类型的变量i
    var i Men

    //i能存储Student
    i = mike
    fmt.Println("This is Mike, a Student:")
    i.SayHi()
    i.Sing("November rain")

    //i也能存储Employee
    i = Tom
    fmt.Println("This is Tom, an Employee:")
    i.SayHi()
    i.Sing("Born to be wild")

    //定义了slice Men
    fmt.Println("Let's use a slice of Men and see what happens")
    x := make([]Men, 3)
    //这三个都是不同类型的元素,但是他们实现了interface同一个接口
    x[0], x[1], x[2] = paul, sam, mike

    for _, value := range x {
        value.SayHi()
    }
}

以上代码中,接口类型声明的变量能存储任何实现了该接口的类型的值。运行代码,输出如下:

go run interface_t.go
This is Mike, a Student:
Hi, I am Mike you can call me on 222-222-XXX
La la la la... November rain
This is Tom, an Employee:
Hi, I am Tom, I work at Things Ltd.. Call me on 222-444-XXX
La la la la... Born to be wild
Let's use a slice of Men and see what happens
Hi, I am Paul you can call me on 111-222-XXX
Hi, I am Sam, I work at Golang Inc.. Call me on 444-222-XXX
Hi, I am Mike you can call me on 222-222-XXX