说道面向对象(OOP)编程, 就不得不提到下面几个概念:
- 抽象
- 封装
- 继承
- 多态
其实有个问题Is Go An Object Oriented Language?
, 随便谷歌了一下, 你就发现讨论这个的文章有很多:
那么问题来了
- Golang是OOP吗?
- 使用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、上面例子中, 称*rect
为receiver
. 关于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-a
和has-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
会以Student
的name
为准.
fmt.Println("Student name:", mark.name) //输出:Student Name: student name
2、方法Student
也继承了Human
的SayHi()
方法
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 value
和 Embedding 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
就拥有了Reader
和Writer
的方法.
尾声
至此, 基本说完了Golang的面向对象. 有哪里我理解的不对的地方, 请给我留言.
参考资料