說道物件導向(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的這種傳統的、型別驅動的子類。
Go Effictive says:
Go does not provide the typical, type-driven notion of subclassing, but it does have the ability to “borrow” pieces of an implementation by embedding types within a struct or interface.
換句話說, 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()
}
Output
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()
}
Output
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()
}
Output
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);
}
Output
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()
}
輸出結果是什麼呢?
Output
ShowA
ShowB
Effective Go Says:
There's an important way in which embedding differs from subclassing. When we embed a type, the methods of that type become methods of the outer type, but when they are invoked the receiver of the method is the inner type, not the outer one
也就是說, 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)
}
Output
{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的物件導向. 有哪裡我理解的不對的地方, 請給我留言.
參考資料