golang物件導向分析

haohongfan發表於2021-04-26

說道物件導向(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的這種傳統的、型別驅動的子類。

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-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()

}

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會以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()
}

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 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)
}

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就擁有了ReaderWriter的方法.

尾聲

至此, 基本說完了Golang的物件導向. 有哪裡我理解的不對的地方, 請給我留言.

參考資料

  1. Effective Go: Embedding
  2. Go面試題
  3. Is Go An Object Oriented Language?
  4. go web程式設計
  5. object-oriented-programming-in-go

相關文章