【跟著我們學Golang】之物件導向

搜雲庫技術團隊發表於2019-05-13

萬物皆物件。學過Java程式設計的都知道Java是一門物件導向的語言,它擁有封裝、繼承和多型的特性。那可不可以說,擁有封裝、繼承和多型這一特性的語言就是物件導向的語言呢? 仔細想來,也確實是這樣的,因為封裝、繼承和多型這三個特徵,並不是Java語言的特徵,而是物件導向的三大特徵。 總結來看,所有包含封裝、繼承和多型者三大特徵的語言都可以說是物件導向的語言。

那麼Go語言是否是一門物件導向的語言呢?下面我們通過舉例的方式針對封裝、繼承和多型這物件導向的三大特徵分別進行解釋。

封裝

Go中有struct結構體,通過結構體能夠實現現實世界中物件的封裝。如將學生封裝成物件,除了學生的基礎資訊外,還需要一些學生的基礎行為。

定義結構體的方式之前在基礎結構中進行了簡單的解釋,並沒有針對結構體的方法進行說明。這裡先說明一下定義結構體的方法。

func(alias type) func_name(parameter1 type, parameter2 type2)(ret1 type3, ret2 type4){
    ...
}
複製程式碼

定義結構體的方法的語法與函式的語法類似,區別於普通函式,方法的定義在func後有一個括號(alias type),指定方法的附屬結構體,以方便通過結構體來進行方法的使用。

看到這裡不免有些Java的同學覺得不太好接受,畢竟在Java中,物件的方法都是寫在class中的,在Go中方法都是寫在結構體外的。

所以可以總結一句,Go中的函式分為兩類,一種是有附屬於結構體的方法,一種是普通函式。附屬於結構體的函式,在使用的過程中,需要結合結構體來使用,必須像Java那樣先宣告物件,然後結合物件才能使用。 普通函式僅有是否可被外部包訪問的要求,不需要先宣告結構體,結合結構體來使用,開蓋即食哈。

方法的結構體在指定時,alias別名可以隨意設定,但是所屬型別不能,(此處有坑)下面看一個例子

package main

import "fmt"

type Student struct {
	Name    string
	Learned []string
}

func (s Student) learnEnglish() {
	s.Learned = append(s.Learned, "i'm fine, thank you")
}

func (s *Student) learnMath() {
	s.Learned = append(s.Learned, "1 + 1 = 2")
}

func (s *Student) whoAmI() {
	fmt.Println("your name is : ", s.Name)
}

func (s Student) whoAmII() {
	fmt.Println("your name is : ", s.Name)
}

func main() {
	s := Student{Name: "jack"}

	s.whoAmI()
	s.whoAmII()

	s.learnEnglish() //學英語
	s.learnMath()    //學數學

	fmt.Println(s.Name, "學過: ")
	for _, learned := range s.Learned {
		fmt.Printf("\t %s \n", learned)
	}
}

/*
執行結果:
your name is :  jack
your name is :  jack
jack 學過:
	 1 + 1 = 2

---
沒有學過英語???
*/

複製程式碼

append為Go自帶函式,向陣列和slice中新增元素

這裡有四個方法,兩個列印名字的方法和兩個學習的方法,區別點在於方法的所屬型別一個是指標型別,另一個是非指標型別。

執行結果顯示,列印名字的方法都正確輸出了名字,但是學習英語和數學後,卻顯示只學過數學,沒學過英語,這豈不是讓我等學生的老師很頭疼?

這是為什麼呢?

這樣就牽涉到了Go中的值拷貝和地址拷貝了。我們們先簡單看一下值拷貝和地址拷貝。

值拷貝&地址拷貝

在Java中同樣有值拷貝和地址拷貝的說法,學過Java的自然對Go的這點特性會比較容易理解。

在Go中雖然是都是值拷貝,但是在拷貝的過程中,拷貝的可能是變數的地址,或者是變數的值,不同的內容得到的結果當然是不一樣的。

在函式定義引數時,如果引數型別是指標型別,則函式內修改了引數的內容,函式外同樣會察覺到改引數的變化,這就是因為在呼叫該函式的時候,傳遞給該函式的值是一個地址,發生的是地址的拷貝,而這個地址指向的引數與函式外的變數是同一個,函式內修改了該地址的內容,相應的,函式外也會發生變化。這個還是通過例子比較好理解。

我們們繼續讓Jack學習不同的知識,在上一個程式碼中繼續新增兩個函式。


func learnChinese(s *Student) {
	s.Learned = append(s.Learned, "鋤禾日當午,汗滴禾下土")
}

func learnPingPang(s Student) {
	s.Learned = append(s.Learned, "ping pang")
}

func main() {
	s := Student{Name: "jack"} //初始化姓名

	s.whoAmI()
	s.whoAmII()

	learnPingPang(s) //學習乒乓球
	learnChinese(&s) //學習中文
	s.learnEnglish() //學英語
	s.learnMath()    //學數學

	fmt.Println(s.Name, "學過: ")
	for _, learned := range s.Learned {
		fmt.Printf("\t %s \n", learned)
	}
}

/*
執行結果:
your name is :  jack
your name is :  jack
jack 學過:
	 鋤禾日當午,汗滴禾下土
	 1 + 1 = 2

---
沒有學過英語???
沒有學過乒乓???
*/
複製程式碼

例子中新增了兩個函式learnChinese(s *Student)和learnPingPang(s Student)兩個函式,分別接收帶指標和不帶指標的引數,下面執行的結果卻顯示Jack只學習了中文沒學習乒乓,這也說明了learnPingPang(s Student)這個函式接收的引數發生了值拷貝,傳遞給該函式的值就是Student物件,而且是生成了一個新的Student物件,所以函式內發生的變化在函式外並不能感知。這個在平時的開發中還是需要特別的注意的。

看到這裡應該就能理解為什麼Jack沒有學過英語了。(s Student) learnEnglish()這個函式中定義的所屬型別是非指標型別,在使用時發生值拷貝,會生成新的Student物件,從而函式內部發生的變化並不會在函式外部有所感知。原來學英語的並不是Jack本人啊。

瞭解瞭如何定義方法之後就可以對封裝有一個比較清晰的認識了,Go中的結構體定義物件和結構體方法定義物件的行為,可以滿足封裝要求了,也算是符合了封裝的條件。下面來一個完整的封裝例子

package main

import "fmt"

type Class struct {
	Name string
}

type School struct {
	Name string
}

type Student struct {
	Name       string
	Age        int
	Height     float64
	Weight     float64
	SchoolInfo School
	ClassInfo  Class
	Learned    []string
}

func (s Student) learnEnglish() {
	// append為Go自帶函式,向陣列和slice中新增元素
	s.Learned = append(s.Learned, "i'm fine, thank you")
}

func (s *Student) learnMath() {
	s.Learned = append(s.Learned, "1 + 1 = 2")
}

func (s *Student) whoAmI() {
	fmt.Println("your name is : ", s.Name, " and your className is : ", s.ClassInfo.Name, " and your schoolName is : ", s.SchoolInfo.Name)
}

func (s Student) whoAmII() {
	fmt.Println("your name is : ", s.Name, " and your className is : ", s.ClassInfo.Name, " and your schoolName is : ", s.SchoolInfo.Name)
}

func learnChinese(s *Student) {
	s.Learned = append(s.Learned, "鋤禾日當午,汗滴禾下土")
}

func learnPingPang(s Student) {
	s.Learned = append(s.Learned, "ping pang")
}

func main() {
	/*
		定義物件時可以使用key:value的形式進行賦值,也可以使用value直接賦值,但是兩中方式不能同時使用

		使用key:value時,不需要注意順序,可以直接賦值
		使用value時,需要注意順序,按照預設欄位順序進行賦值

		️注意::如果最後一個欄位與右大括號不在一行,需要在最後一個欄位的賦值後加上逗號
	*/
	s := Student{
		Age:        18,
		Weight:     70,
		Height:     180,
		SchoolInfo: School{"北大附中"},
		Name:       "jack",
		ClassInfo:  Class{"高二·8班"},
	} //初始化student物件

	fmt.Println("學校: ", s.SchoolInfo.Name)
	fmt.Println("班級: ", s.ClassInfo.Name)
	fmt.Println("姓名: ", s.Name)
	fmt.Println("年齡: ", s.Age, "歲")
	fmt.Println("身高: ", s.Height, "cm")
	fmt.Println("體重: ", s.Weight, "kg")

	s.whoAmI()
	s.whoAmII()

	learnPingPang(s) //學習乒乓球
	learnChinese(&s) //學習中文
	s.learnEnglish() //學英語
	s.learnMath()    //學數學

	fmt.Println(s.Name, "學過: ")
	for _, learned := range s.Learned {
		fmt.Printf("\t %s \n", learned)
	}
}

/*
執行結果:
學校:  北大附中
班級:  高二·8班
姓名:  jack
年齡:  18 歲
身高:  180 cm
體重:  70 kg
your name is :  jack  and your className is :  高二·8班  and your schoolName is :  北大附中
your name is :  jack  and your className is :  高二·8班  and your schoolName is :  北大附中
jack 學過:
	 鋤禾日當午,汗滴禾下土
	 1 + 1 = 2

---
沒有學過英語
沒有學過乒乓
*/

複製程式碼

這裡的Jack既有班級資訊又有學校資訊,既能學中文又能學英文。也算是把學生這個物件封裝好了。

繼承

Java中,繼承是說父子類之間的關係,子類繼承父類,子類就擁有父類的部分功能。這個繼承通過extend關鍵字就可以實現。在Go中,沒有這個關鍵字,但是也可以做到相同的效果。使用的方式就是結構體的巢狀。我們繼續使用學生這個例子進行講解,現在將學生中的部分資訊抽出到People這個結構體中。

package main

import "fmt"

type Class struct {
	Name string
}

type School struct {
	Name string
}

type People struct {
	Name   string
	Age    int
	Height float64
	Weight float64
}

func (p *People) SayHey() {
	fmt.Println("愛老虎油")
}

func (p *People) Run() {
	fmt.Println(p.Name, "is running...")
}

func (p *People) Eat() {
	fmt.Println(p.Name, "is eating...")
}

func (p *People) Drink() {
	fmt.Println(p.Name, "is drinking...")
}

type Student struct {
	People     //內嵌people
	Name       string
	SchoolInfo School
	ClassInfo  Class
	Learned    []string
}

func (s *Student) SayHey() {
	fmt.Println("i love you")
}

func (s Student) learnEnglish() {
	// append為Go自帶函式,向陣列和slice中新增元素
	s.Learned = append(s.Learned, "i'm fine, thank you")
}

func (s *Student) learnMath() {
	s.Learned = append(s.Learned, "1 + 1 = 2")
}

func (s *Student) whoAmI() {
	fmt.Println("your name is : ", s.Name, " and your className is : ", s.ClassInfo.Name, " and your schoolName is : ", s.SchoolInfo.Name)
}

func (s Student) whoAmII() {
	fmt.Println("your name is : ", s.Name, " and your className is : ", s.ClassInfo.Name, " and your schoolName is : ", s.SchoolInfo.Name)
}

func learnChinese(s *Student) {
	s.Learned = append(s.Learned, "鋤禾日當午,汗滴禾下土")
}

func learnPingPang(s Student) {
	s.Learned = append(s.Learned, "ping pang")
}

func main() {
	s := Student{
		People: People{
			Name:   "jack", //小名
			Age:    18,
			Weight: 70,
			Height: 180,
		},
		Name:       "jack·li", //大名
		SchoolInfo: School{"北大附中"},
		ClassInfo:  Class{"高二·8班"},
	} //初始化student物件

	fmt.Println("學校: ", s.SchoolInfo.Name)
	fmt.Println("班級: ", s.ClassInfo.Name)
	fmt.Println("姓名: ", s.Name) //列印時會列印大名
	fmt.Println("年齡: ", s.Age, "歲")
	fmt.Println("身高: ", s.Height, "cm")
	fmt.Println("體重: ", s.Weight, "kg")

	s.whoAmI()
	s.whoAmII()

	learnPingPang(s) //學習乒乓球
	learnChinese(&s) //學習中文
	s.learnEnglish() //學英語
	s.learnMath()    //學數學

	fmt.Println(s.Name, "學過: ")         //列印時會列印大名
	for _, learned := range s.Learned { //列印學過的知識
		fmt.Printf("\t %s \n", learned)
	}
	s.Eat()   //直接使用內嵌型別的方法
	s.Drink() //直接使用內嵌型別的方法
	s.Run()   //直接使用內嵌型別的方法

	s.SayHey()                        //使用 Student 的sayHey
	fmt.Println("俺叫:", s.People.Name) //使用內嵌People的name列印小名
	s.People.SayHey()                 //使用 內嵌People的SayHey
}

/*
執行結果:
學校:  北大附中
班級:  高二·8班
姓名:  jack·li
年齡:  18 歲
身高:  180 cm
體重:  70 kg
your name is :  jack·li  and your className is :  高二·8班  and your schoolName is :  北大附中
your name is :  jack·li  and your className is :  高二·8班  and your schoolName is :  北大附中
jack·li 學過: 
	 鋤禾日當午,汗滴禾下土 
	 1 + 1 = 2 
jack is eating...
jack is drinking...
jack is running...
i love you
俺叫: jack
愛老虎油
*/

複製程式碼

在這個例子中,Student內嵌了People,在定義Student物件時People結構體的欄位單獨定義在People物件中。但是在使用時,可以直接像s.Eat()s.Run()s.Height這樣直接呼叫,也可以使用s.People.SayHey()s.People.Name這樣間接的呼叫。這就是巢狀的使用方法。

使用巢狀結構體的方式定義物件之後,就可以直接使用內嵌型別的欄位以及方法,但是在使用時遇到相同的欄位(Student的Name和People的Name)則直接使用欄位時,使用的就是結構體的欄位,而不是內嵌型別的欄位,或者遇到相同的方法(Student的SayHey()和People的SayHey())則直接使用時,使用的就是結構體的方法,而不是內嵌型別的方法。如果要使用內嵌型別的欄位或方法,可以在使用時指明內嵌結構體。這個有點像Java中的覆蓋。所以有時在使用時需要注意要使用的是那個具體的欄位,避免出錯。

曲線救國也算是救國,Go通過內嵌結構體的形式,變相的實現了物件導向的繼承,但是感覺總是比Java中的繼承要差些什麼。或許差的是繼承的那些條條框框吧。

多型

相同型別的物件表現出不一樣的行為特徵叫做多型。這個在Go中同樣可以實現。通過interface就可以。

上節講到interface是基礎型別,這裡我們們繼續講解interface作為介面的用法。

interface作為介面時,可以定義一系列的函式供其他結構體實現,但是隻能定義函式,不能定義欄位等。它的語法如下

type name interface {
    func1([請求引數集]) [返回引數集]
}
複製程式碼

Go中的介面在實現時可沒有Java中的implement關鍵字,在實現介面的時候只需要實現介面中定義的全部的方法就可以認為是實現了這個介面,所以說Go的介面實現是一種隱式的實現,並不是直觀上的實現。這點也是類似Java中的介面的,但是介面實現的這種關係並不是那麼嚴格,如果通過ide在開發的過程中,能看到很多定義的方法實現了自己不知道的介面,不過放心,這是一種正常的現象,只要在使用的過程中稍加註意即可。

讓我們們繼續優化上面的例子來理解interface介面,還是看下面的例子

package main

import "fmt"

type Class struct {
	Name string
}

type School struct {
	Name string
}

type Animal interface {
	Eat()
	Drink()
	Run()
}

//實現了Animal的三個方法,可認為*People實現了Animal介面
type People struct {
	Name   string
	Age    int
	Height float64
	Weight float64
}

func (p *People) SayHey() {
	fmt.Println("愛老虎油")
}

//實現Animal介面的Run方法
func (p *People) Run() {
	fmt.Println(p.Name, "is running...")
}

//實現Animal介面的Eat方法
func (p *People) Eat() {
	fmt.Println(p.Name, "is eating...")
}

//實現Animal介面的Drink方法
func (p *People) Drink() {
	fmt.Println(p.Name, "is drinking...")
}

//實現了Animal的三個方法,可認為*Student實現了Animal介面
type Student struct {
	People //內嵌people
	Name       string
	SchoolInfo School
	ClassInfo  Class
	Learned    []string
}

//實現Animal介面的Run方法
func (s *Student) Run() {
	fmt.Println(s.Name, "is running around campus")
}

//實現Animal介面的Eat方法
func (s *Student) Eat() {
	fmt.Println(s.Name, "is eating in the school cafeteria")
}

//實現Animal介面的Drink方法
func (s *Student) Drink() {
	fmt.Println(s.Name, "is drinking in the school cafeteria")
}

func (s *Student) SayHey() {
	fmt.Println("i love you")
}

func (s Student) learnEnglish() {
	// append為Go自帶函式,向陣列和slice中新增元素
	s.Learned = append(s.Learned, "i'm fine, thank you")
}

func (s *Student) learnMath() {
	s.Learned = append(s.Learned, "1 + 1 = 2")
}

func (s *Student) whoAmI() {
	fmt.Println("your name is : ", s.Name, " and your className is : ", s.ClassInfo.Name, " and your schoolName is : ", s.SchoolInfo.Name)
}

func (s Student) whoAmII() {
	fmt.Println("your name is : ", s.Name, " and your className is : ", s.ClassInfo.Name, " and your schoolName is : ", s.SchoolInfo.Name)
}

func learnChinese(s *Student) {
	s.Learned = append(s.Learned, "鋤禾日當午,汗滴禾下土")
}

func learnPingPang(s Student) {
	s.Learned = append(s.Learned, "ping pang")
}

func main() {
	s := Student{
		People: People{
			Name:   "jack", //小名
			Age:    18,
			Weight: 70,
			Height: 180,
		},
		Name:       "jack·li", //大名
		SchoolInfo: School{"北大附中"},
		ClassInfo:  Class{"高二·8班"},
	} //初始化student物件

	fmt.Println("學校: ", s.SchoolInfo.Name)
	fmt.Println("班級: ", s.ClassInfo.Name)
	fmt.Println("姓名: ", s.Name) //列印時會列印大名
	fmt.Println("年齡: ", s.Age, "歲")
	fmt.Println("身高: ", s.Height, "cm")
	fmt.Println("體重: ", s.Weight, "kg")

	s.whoAmI()
	s.whoAmII()

	learnPingPang(s) //學習乒乓球
	learnChinese(&s) //學習中文
	s.learnEnglish() //學英語
	s.learnMath()    //學數學

	fmt.Println(s.Name, "學過: ") //列印時會列印大名
	for _, learned := range s.Learned { //列印學過的知識
		fmt.Printf("\t %s \n", learned)
	}
	s.People.Eat()   //直接使用內嵌型別的方法
	s.People.Drink() //直接使用內嵌型別的方法
	s.People.Run()   //直接使用內嵌型別的方法

	s.SayHey()                        //使用 Student 的sayHey
	fmt.Println("俺叫:", s.People.Name) //使用內嵌People的name列印小名
	s.People.SayHey()                 //使用 內嵌People的SayHey

	var xiaoming, xiaohua Animal //大家都是動物,尷尬
	//Student的指標型別實現了Animal介面,可以使用&Student來給Animal賦值
	xiaoming = &s //jack的中文名叫xiaoming
	//People的指標型別實現了Animal介面,可以使用&People來給Animal賦值
	xiaohua = &People{Name: "xiaohua", Age: 5, Height: 100, Weight: 50} //xiaohua還小,每到上學的年級,不是學生
	xiaoming.Run()                                                      //xiaoming在跑步
	xiaohua.Run()                                                       //xiaohua在跑步

	xiaoming.Eat() //xiaoming在吃東西
	xiaohua.Eat()  //xiaohua在吃東西

	xiaoming.Drink() //xiaoming在吃東西
	xiaohua.Drink()  //xiaohua在吃東西

}

/*
執行結果:
學校:  北大附中
班級:  高二·8班
姓名:  jack·li
年齡:  18 歲
身高:  180 cm
體重:  70 kg
your name is :  jack  and your className is :  高二·8班  and your schoolName is :  北大附中
your name is :  jack  and your className is :  高二·8班  and your schoolName is :  北大附中
jack 學過:
	 鋤禾日當午,汗滴禾下土
	 1 + 1 = 2
jack·li is eating in the school cafeteria
jack·li is drinking in the school cafeteria
jack·li is running around campus
i love you
俺叫: jack
愛老虎油
jack·li is running around campus
xiaohua is running...
jack·li is eating in the school cafeteria
xiaohua is eating...
jack·li is drinking in the school cafeteria
xiaohua is drinking...

*/

複製程式碼

將People的三個方法抽象成介面Anmial,讓People和Student兩個結構都實現Animal的三個方法。宣告xiaohua和xiaoming兩個物件為Animal型別,給xiaohua宣告一個還沒上學People物件,給xiaoming宣告一個已經上學的Student物件,最終得到了不一樣的結果。

這裡可能會有疑問,問什麼將jack賦值給xiaoming時,給xiaoming的是&s指標地址。這要從函式的實現說起。因為函式的實現指定的是指標形式的型別,在賦值時需要賦予指標型別的值才不會發生值拷貝,而且可以在使用的過程中修改物件中的值。但是在使用時可以不加指標直接使用,比如s.SayHey()就可以直接使用,不用轉換為指標型別。

總結

Go通過interface也實現了物件導向中多型的特徵。現在總結來看,Go能夠直接實現封裝和多型,變相的實現繼承的概念,這個在網路上被人稱為是不完全的物件導向或者是弱物件導向,不過對於物件導向的開發,這已經夠用了。

原始碼可以通過'github.com/souyunkutech/gosample'獲取。

關注我們的「微信公眾號」

【跟著我們學Golang】之物件導向


首發微信公眾號:Go技術棧,ID:GoStack

版權歸作者所有,任何形式轉載請聯絡作者。

作者:搜雲庫技術團隊 出處:gostack.souyunku.com/2019/05/13/…

相關文章