【跟着咱們學Golang】之面向對象

萬物皆對象。學過Java編程的都知道Java是一門面向對象的語言,它擁有封裝、繼承和多態的特性。那可不能夠說,擁有封裝、繼承和多態這一特性的語言就是面向對象的語言呢? 仔細想來,也確實是這樣的,由於封裝、繼承和多態這三個特徵,並非Java語言的特徵,而是面向對象的三大特徵。 總結來看,全部包含封裝、繼承和多態者三大特徵的語言均可以說是面向對象的語言。git

那麼Go語言是不是一門面向對象的語言呢?下面咱們經過舉例的方式針對封裝、繼承和多態這面向對象的三大特徵分別進行解釋。github

封裝

Go中有struct結構體,經過結構體可以實現現實世界中對象的封裝。如將學生封裝成對象,除了學生的基礎信息外,還須要一些學生的基礎行爲。編程

定義結構體的方式以前在基礎結構中進行了簡單的解釋,並無針對結構體的方法進行說明。這裏先說明一下定義結構體的方法。數組

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

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

看到這裏難免有些Java的同窗以爲不太好接受,畢竟在Java中,對象的方法都是寫在class中的,在Go中方法都是寫在結構體外的。微信

因此能夠總結一句,Go中的函數分爲兩類,一種是有附屬於結構體的方法,一種是普通函數。附屬於結構體的函數,在使用的過程當中,須要結合結構體來使用,必須像Java那樣先聲明對象,而後結合對象才能使用。 普通函數僅有是否可被外部包訪問的要求,不須要先聲明結構體,結合結構體來使用,開蓋即食哈。網絡

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

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中添加元素ide

這裏有四個方法,兩個打印名字的方法和兩個學習的方法,區別點在於方法的所屬類型一個是指針類型,另外一個是非指針類型。函數

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

這是爲何呢?

這樣就牽涉到了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'獲取。

關注咱們的「微信公衆號」


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

版權歸做者全部,任何形式轉載請聯繫做者。

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

相關文章
相關標籤/搜索