search.png
关于我
menu.png
go 面向对象

概述

go的面向对象和其它的面向语言并不太相同。首先,go是没有类的概念的,在go中数据是数据,方法是方法,二者比较独立。其次go对继承的实现更像是“组合”。go的接口是鸭子类型:只要你像个鸭子,那么我就认为你是个鸭子,而不需要显式的实现接口,像java的implements XXX在go是不存在的。
面向对象的三个特征:继承、封装、多态 go都可以实现。(多态需要基于接口实现)

基础语法

type User struct {
    name string
}

func (u User) introduceMe() {
    fmt.Println("hello, my name is " + u.name)
}


u := User{"momo"}
u.introduceMe()

比较结构体会进行值比较。比较指针则是地址比较,因此u1=u2但是 &u1 != &u2

u1 := User{"momo"}
u2 := User{"momo"}
fmt.Println(u1 == u2)
fmt.Println(&u1 == &u2)

go 的方法是独立的。因此可以通过这种方式调用:


var f func() 
f = u.introduceMe
f()

f2 := (User).introduceMe
f2(u)

这种写法和java的方法引用有些相似。但是go没有java那样的历史包袱,不需要通过接口方法来适配,写法更加的优雅方便

继承

通过将目标类匿名引入结构体即可实现继承:


// Admin 匿名引入实现继承
type Admin struct {
    User
}

fmt.Println(admin.name)
admin.introduceMe()
fmt.Println(admin.User.name)
admin.User.introduceMe()

go实现了语法糖,可以直接调用内部匿名结构体的方法、属性,通过这种方式我们可以实现继承。

封装

在go语言的结构体中,大写字母开头的属性是包外可用的,小写字母开头的属性是只有包内可用的。相比java,没有protected的概念。java的私有变量,大部分情况还需要写getter、setter方法去暴露出来,这其实造成了很多的代码冗余,其实大可不必。

go的包内声明的变量、结构体、类型、函数等都是一样的规则,大写字母开头则对外可见,反之对外不可见。

接口和多态

// Animal 动物接口
type Animal interface {
    // Eat 吃食物
    Eat(food string)
    // Run 跑
    Run()
}

type Chicken struct {
}

func (c Chicken) Eat(food string) {
    fmt.Println("鸡吃 " + food)
}

func (c Chicken) Run() {
    fmt.Println("鸡在跑")
}

type Dog struct {
}

func (d Dog) Eat(food string) {
    fmt.Println("狗吃 " + food)
}

func (d Dog) Run() {
    fmt.Println("狗在跑")
}

var animal Animal
animal = Chicken{}
animal.Eat("鸡饲料")
animal.Run()

dog := Dog{}
animal = dog
animal.Eat("骨头")
animal.Run()

上例实现了动物接口,动物都会吃、跑,只要实现了吃和跑,我们就认为其是动物,因此鸡是动物,狗也是动物,因为它们都会吃和跑。

打印结果:

鸡吃 鸡饲料
鸡在跑
狗吃 骨头
狗在跑

go如何多继承?

在java中,一个对象只能继承自一个类,但是可以实现多个接口。
在go中可以通过引入多个匿名结构体以实现多继承,至于实现多个接口,只需要把这些接口的方法都实现即可。
下例分别实现了多继承和实现多个接口,鸟实现了飞和叫的接口。鸭子继承了鸟和卵生动物。因此鸭子也拥有了飞、叫、下蛋的行为。

type Flyer interface {
    Fly()
}

type Speaker interface {
    Speak()
}

type Bird struct {
}

func (b Bird) Fly() {
    fmt.Println("飞啊飞...")
}

func (b Bird) Speak() {
    fmt.Println("鸟叫")
}

// Waterfowl 水禽
type Waterfowl interface {
    // Swim 水禽都会游泳
    Swim()
}

// Oviparous 卵生动物
type Oviparous struct {
    
}

func (o Oviparous) LayEggs()  {
    fmt.Println("下蛋了")
}

// Duck 鸭子继承了鸟和卵生动物
type Duck struct {
    Bird
    Oviparous
}

重载

go允许覆盖匿名结构体的方法以实现重载,例如上例中,鸭子的叫声应该是“嘎嘎嘎”:


func (d Duck) Speak()  {
    fmt.Println("鸭子叫:嘎嘎嘎")
}

对鸭子增加的Speak方法会覆盖鸟的Speak方法。

duck := new(Duck)
duck.Fly()
duck.LayEggs()
duck.Speak()

输出:

飞啊飞...
下蛋了
鸭子叫:嘎嘎嘎

抽象类

java中抽象类是实现了部分方法,但是部分方法没有实现的虚类,无法对齐进行实例化,但是可以进行继承。go 没有抽象类,但是可以部分继承某个结构的方法,再补充额外的方法以实现接口:

// Animal 动物接口
type Animal interface {
    // Eat 吃食物
    Eat(food string)
    // Run 跑
    Run()
}

type Eater struct {

}

func (Eater) Eat(food string) {
    fmt.Println("吃" + food)
}

type Fox struct {
    Eater
}

func (f Fox) Run() {
    fmt.Println("狐狸跑啊跑")
}

狐狸继承了Eater吃的方法,又实现了Run跑的方法,因此狐狸是动物,可以将其赋值给动物接口类型的变量:

fox := Fox{}
var animal Animal
animal = fox
animal.Run()
animal.Eat("野兔")

结果:

狐狸跑啊跑
吃...东西:野兔

接口嵌套

嵌套得到的接口的使用与普通接口一样,只要实现包含的所有方法即可实现接口。

// Sayer 接口
type Sayer interface {
    say()
}

// Mover 接口
type Mover interface {
    move()
}

// Human 接口嵌套
type Human interface {
    Sayer
    Mover
}


空接口

在go中,interface{}代表空接口,任何类型都可以看作空接口。类似于java中的Object,是所有类的父类。

func testEmptyInterface(t interface{}) {
    switch t.(type) {
    case int:
        fmt.Println("is int")
    case float64:
        fmt.Println("is float64")
    case string:
        fmt.Println("is string")
    default:
        fmt.Println("unknown")
    }
}

// 空接口
testEmptyInterface(1)
testEmptyInterface(1.23232323)
testEmptyInterface("adsadsad")
testEmptyInterface(false)

结果:

is int
is float64
is string
unknown


需要注意的

一个对象,无论其是指针还是实例,都可以引用指针方法和实例方法:

type Student struct{}

func (stu *Student) Speak(name string) (talk string) {
    talk = "您好," + name
    return
}

func (stu Student) Study() {
    fmt.Println("我在学习")
}

下边分别用指针和实例访问方法都是可以的:

stu1 := new(Student)
stu1.Speak("momo")
stu1.Study()

stu2 := Student{}
stu2.Speak("momo")
stu2.Study()

但是对象如果是通过指针实现的接口,例如Student通过指针实现的这个接口:


type People interface {
    Speak(string) string
}

// student 的speak只作用于指针,因此下例编译不通过
var peo People = Student{}
fmt.Println(peo.Speak("momo"))

将会语法报错:Cannot use 'Student{}' (type Student) as the type People Type does not implement 'People' as the 'Speak' method has a pointer receiver
而使用指针则是可以的:

var peo People = &Student{}
fmt.Println(peo.Speak("momo"))

面向对象的一大滥用就是过分抽象,千万不要为了抽象而抽象。过度抽象会导致理解代码变得困难。反而适得其反。当可复用的逻辑不多时就没有必要使用接口去做抽象。

版权声明

知识共享许可协议 本文章由作者“衡于墨”创作,转载请注明出处,未经允许禁止用于商业用途

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
发布时间:2021年03月10日 21:27:33

评论区#

还没有评论哦,期待您的评论!

关闭特效