说道面向对象(OOP)编程, 就不得不提到下面几个概念:

  • 抽象
  • 封装
  • 继承
  • 多态

其实有个问题Is Go An Object Oriented Language?, 随便谷歌了一下, 你就发现讨论这个的文章有很多:

  1. reddit
  2. google group

那么问题来了

  1. Golang是OOP吗?
  2. 使用Golang如何实现OOP?

一. 抽象和封装

抽象和封装就放在一块说了. 这个其实挺简单. 看一个例子就行了.

type rect struct {
    width int
    height int
}

func (r *rect) area() int {
    return r.width * r.height
}

func main() {
    r := rect{width: 10, height: 5}
    fmt.Println("area: ", r.area())
}

完整代码

要说明的几个地方:
1、Golang中的struct和其他语言的class是一样的.

2、可见性. 这个遵循Go语法的大小写的特性

3、上面例子中, 称*rectreceiver. 关于receiver 可以有两种方式的写法:

func (r *rect) area() int {
    return r.width * r.height
}
func (r rect) area() int {
    return r.width * r.height
}

这其中有什么区别和联系呢?

简单来说, Receiver可以是值传递, 还是可以是指针, 两者的差别在于, 指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作。

4、当Receiver*rect指针的时候, 使用的是r.width, 而不是(*r).width, 是由于Go自动帮我转了,两种方式都是正确的.

5、任何类型都可以声明成新的类型, 因为任何类型都可以有方法.

type Interger int
func (i Interger) Add(interger Interger) Interger {
	return i + interger
}

6、虽然Interger是从int声明而来, 但是这样用是错误的.

var i Interger = 1
var a int = i //cannot use i (type Interger) as type int in assignment

这是因为Go中没有隐式转换(写C++的同学都会特别讨厌这个, 因为编译器背着我们干的事情太多了). Golang中类型之间的相互赋值都必须显式声明.

上面的例子改成下面的方式就可以了.

var i Interger = 1
var a int = int(i)

二. (Composition)

说道继承,其实在Golang中是没有继承(Extend)这个概念. 因为Golang舍弃掉了像C++, Java的这种传统的、类型驱动的子类。

换句话说, Golang中没有继承, 只有Composition.

Golang中的Compostion有两种形式, 匿名组合(Pseudo is-a)非匿名组合(has-a)

注: 如果不了解OOP的is-ahas-a关系的话, 请自行google.

1. has-a

package main

import (
	"fmt"
)

type Human struct {
	name  string
	age   int
	phone string
}

type Student struct {
	h      Human //非匿名字段
	school string
}

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

func (s *Student) SayHi() {
	fmt.Printf("Hi student, I am %s you can call me on %s", s.h.name, s.h.phone)
}

func main() {
	mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
	fmt.Println(mark.h.name, mark.h.age, mark.h.phone, mark.school)
	mark.h.SayHi()
	mark.SayHi()

}
Mark 25 222-222-YYYY MIT
Hi, I am Mark you can call me on 222-222-YYYY
Hi student, I am Mark you can call me on 222-222-YYYY

完整代码

这种组合方式, 其实对于了解传统OOP的话, 很好理解, 就是把一个struct作为另一个struct的字段.

从上面例子可以, Human完全作为Student的一个字段使用. 所以也就谈不上继承的相关问题了.我们也不去重点讨论.

2. is-a(Pseudo)----Embedding

type Human struct {
	name string
	age int
	phone string
}

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

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

func main() {
	mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
	fmt.Println(mark.name, mark.age, mark.phone, mark.school)
	mark.SayHi()
}
Mark 25 222-222-YYYY MIT
Hi, I am Mark you can call me on 222-222-YYYY

完整代码

这里要说的有几点:

1、字段
现在Student访问Human的字符, 就可以直接访问了, 感觉就是在访问自己的属性一样. 这样就实现了OOP的继承.

fmt.Println("Student age:", mark.age) //输出: Student age: 25

但是, 我们也可以间接访问:

fmt.Println("Student age:", mark.Human.age) //输出: Student age: 25

这有个问题, 如果在Student也有个字段name, 那么当使用mark.name会以Studentname为准.

fmt.Println("Student name:", mark.name) //输出:Student Name: student name

完整代码

2、方法
Student也继承了HumanSayHi()方法

mark.SayHi() // 输出: Hi, I am Mark you can call me on 222-222-YYYY

当然, 我们也可以重写SayHi()方法:

type Human struct {
	name  string
	age   int
	phone string
}

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

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

func (h *Student) SayHi() {
	fmt.Println("Student Sayhi")
}

func main() {
	mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT", "student name"}
	mark.SayHi()
}
Student Sayhi

完整代码

3、为什么称其为Pseudo is-a呢?

因为匿名组合不提供多态的特性. 如下面的代码:

package main

type A struct{
}

type B struct {
	A  //B is-a A
}

func save(A) {
	//do something
}

func main() {
	b := new(B)
	save(*b);
}
cannot use *b (type B) as type A in argument to save

完整代码

还有一个面试题的例子

type People struct{}

func (p *People) ShowA() {
	fmt.Println("showA")
	p.ShowB()
}
func (p *People) ShowB() {
	fmt.Println("showB")
}

type Teacher struct {
	People
}

func (t *Teacher) ShowB() {
	fmt.Println("teacher showB")
}

func main() {
	t := Teacher{}
	t.ShowA()
}

输出结果是什么呢?

ShowA
ShowB

Effective Go Says:

也就是说, Teacher由于组合了People, 所以Teacher也有了ShowA()方法, 但是在ShowA()方法里执行到ShowB时, 这个时候的receiver*People而不是*Teacher, 主要原因还是因为embedding是一个Pseudo is-a, 没有多态的功能.

4、 "多继承"的问题

package main

import "fmt"

type School struct {
	address string
}

func (s *School) Address() {
	fmt.Println("School Address:", s.address)
}

type Home struct {
	address string
}

func (h *Home) Address() {
	fmt.Println("Home Address:", h.address)
}

type Student struct {
	School
	Home
	name string
}

func main() {
	mark := Student{School{"aaa"}, Home{"bbbb"}, "cccc"}
	fmt.Println(mark)
	mark.Address()
	fmt.Println(mark.address)

	mark.Home.Address()
	fmt.Println(mark.Home.address)
}
30: ambiguous selector mark.Address
31: ambiguous selector mark.address

完整代码

由此可以看出, Golang中不管是方法还是属性都不存在类似C++那样的多继承的问题. 要访问Embedding相关的属性和方法, 需要在加那个相应的匿名字段, 如:

mark.Home.Address()

5、Embedding valueEmbedding pointer的区别

package main

import (
	"fmt"
)

type Person struct {
	name string
}

type Student struct {
	*Person
	age int
}

type Teacher struct {
	Person
	age int
}

func main()  {
	s := Student{&Person{"student"}, 10}
	t := Teacher{Person{"teacher"}, 40}
	fmt.Println(s, s.name)
	fmt.Println(t, t.name)
}
{0x1040c108 10} student
{{teacher} 40} teacher

完整代码

I. 两者对于结果来说, 没有啥区别, 只是对传参的时候有影响
II. Embedding value是比较常规的写法
III. Embedding pointer比较有优势一点, 不需要关注指针是什么时间被初始化的.

三. Interface

Golang中Composite不提供多态的功能, 那是否Golang不提供多态呢? 答案肯定是否定. Golang依靠Interface实现多态的功能.

下面是我工程里面一段代码的简化:

package main

import (
	"fmt"
)

type Check interface {
	CheckOss()
}

type CheckAudio struct {
	//something
}

func (c *CheckAudio) CheckOss() {
	fmt.Println("CheckAudio do CheckOss")
}

func main() {
	checkAudio := CheckAudio{}

	var i Check

	i = &checkAudio //想一下这里为啥需要&

	i.CheckOss()
}

完整代码

1、Interface 如何Composite ?

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

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

type ReadWriter interface {
    Reader
    Writer
}

其实很简单, 就是把Reader, Writer嵌入到ReadWriter中, 这样ReadWriter就拥有了ReaderWriter的方法.

尾声

至此, 基本说完了Golang的面向对象. 有哪里我理解的不对的地方, 请给我留言.

参考资料

  1. Effective Go: Embedding
  2. Go面试题
  3. Is Go An Object Oriented Language?
  4. go web编程
  5. object-oriented-programming-in-go
04-26 17:53