(1)面向對象與面向過程的區別程序員
面向過程就是分析出解決問題所須要的步驟,而後用函數把這些步驟一步一步實現,使用的時候一個一個依次調用就能夠了;面向對象是把構成問題事務分解成各個對象,創建對象的目的不是爲了完成一個步驟,而是爲了描敘某個事物在整個解決問題的步驟中的行爲。編程
能夠拿生活中的實例來理解面向過程與面向對象,例如五子棋,面向過程的設計思路就是首先分析問題的步驟:一、開始遊戲,二、黑子先走,三、繪製畫面,四、判斷輸贏,五、輪到白子,六、繪製畫面,七、判斷輸贏,八、返回步驟2,九、輸出最後結果。把上面每一個步驟用不一樣的方法來實現。架構
若是是面向對象的設計思想來解決問題。面向對象的設計則是從另外的思路來解決問題。整個五子棋能夠分爲一、黑白雙方,這兩方的行爲是如出一轍的,二、棋盤系統,負責繪製畫面,三、規則系統,負責斷定諸如犯規、輸贏等。第一類對象(玩家對象)負責接受用戶輸入,並告知第二類對象(棋盤對象)棋子佈局的變化,棋盤對象接收到了棋子的變化就要負責在屏幕上面顯示出這種變化,同時利用第三類對象(規則系統)來對棋局進行斷定。編程語言
能夠明顯地看出,面向對象是以功能來劃分問題,而不是步驟。一樣是繪製棋局,這樣的行爲在面向過程的設計中分散在了多個步驟中,極可能出現不一樣的繪製版本,由於一般設計人員會考慮到實際狀況進行各類各樣的簡化。而面向對象的設計中,繪圖只可能在棋盤對象中出現,從而保證了繪圖的統一。
總結下來就兩句話:面向對象就是高度實物抽象化、面向過程就是自頂向下的編程!函數
(2)面向對象的特色佈局
在瞭解其特色以前,我們先談談對象,對象就是現實世界存在的任何事務均可以稱之爲對象,有着本身獨特的個性性能
屬性用來描述具體某個對象的特徵。好比小志身高180M,體重70KG,這裏身高、體重都是屬性。
面向對象的思想就是把一切都當作對象,而對象通常都由屬性+方法組成!測試
屬性屬於對象靜態的一面,用來形容對象的一些特性,方法屬於對象動態的一面,我們舉一個例子,小明會跑,會說話,跑、說話這些行爲就是對象的方法!因此爲動態的一面, 咱們把屬性和方法稱爲這個對象的成員!spa
類:具備同種屬性的對象稱爲類,是個抽象的概念。好比「人」就是一類,期中有一些人名,好比小明、小紅、小玲等等這些都是對象,類就至關於一個模具,他定義了它所包含的全體對象的公共特徵和功能,對象就是類的一個實例化,小明就是人的一個實例化!咱們在作程序的時候,常常要將一個變量實例化,就是這個原理!咱們通常在作程序的時候通常都不用類名的,好比咱們在叫小明的時候,不會喊「人,你幹嗎呢!」而是說的是「小明,你在幹嗎呢!」.net
面向對象有三大特性,分別是封裝性、繼承性和多態性。
(3)面向過程與面向對象的優缺點
用面向過程的方法寫出來的程序是一份蛋炒飯,而用面向對象寫出來的程序是一份蓋澆飯。所謂蓋澆飯,北京叫蓋飯,東北叫燴飯,廣東叫碟頭飯,就是在一碗白米飯上面澆上一份蓋菜,你喜歡什麼菜,你就澆上什麼菜。我以爲這個比喻仍是比較貼切的。
蛋炒飯製做的細節,我不太清楚,由於我沒當過廚師,也不會作飯,但最後的一道工序確定是把米飯和雞蛋混在一塊兒炒勻。蓋澆飯呢,則是把米飯和蓋菜分別作好,你若是要一份紅燒肉蓋飯呢,就給你澆一份紅燒肉;若是要一份青椒土豆蓋澆飯,就給澆一份青椒土豆絲。
蛋炒飯的好處就是入味均勻,吃起來香。若是恰巧你不愛吃雞蛋,只愛吃青菜的話,那麼惟一的辦法就是所有倒掉,從新作一份青菜炒飯了。蓋澆飯就沒這麼多麻煩,你只須要把上面的蓋菜撥掉,更換一份蓋菜就能夠了。蓋澆飯的缺點是入味不均,可能沒有蛋炒飯那麼香。
究竟是蛋炒飯好仍是蓋澆飯好呢?其實這類問題都很難回答,非要比個上下高低的話,就必須設定一個場景,不然只能說是各有所長。若是你們都不是美食家,沒那麼多講究,那麼從飯館角度來說的話,作蓋澆飯顯然比蛋炒飯更有優點,他能夠組合出來任意多的組合,並且不會浪費。
蓋澆飯的好處就是」菜」「飯」分離,從而提升了製做蓋澆飯的靈活性。飯不滿意就換飯,菜不滿意換菜。用軟件工程的專業術語就是」可維護性「比較好,」飯」 和」菜」的耦合度比較低。蛋炒飯將」蛋」「飯」攪和在一塊兒,想換」蛋」「飯」中任何一種都很困難,耦合度很高,以致於」可維護性」比較差。軟件工程追求的目標之一就是可維護性,可維護性主要表如今3個方面:可理解性、可測試性和可修改性。面向對象的好處之一就是顯著的改善了軟件系統的可維護性。
簡單的總結一下!
面向過程:
優勢:性能比面向對象高,由於類調用時須要實例化,開銷比較大,比較消耗資源;好比單片機、嵌入式開發、 Linux/Unix等通常採用面向過程開發,性能是最重要的因素。 缺點:沒有面向對象易維護、易複用、易擴展
面向對象:
優勢:易維護、易複用、易擴展,因爲面向對象有封裝、繼承、多態性的特性,能夠設計出低耦合的系統,使系統 更加靈活、更加易於維護
缺點:性能比面向過程低
(上面這些是我在網上看到一哥們寫的,以爲寫的很不錯就拷貝過來了,他博文地址是:http://www.javashuo.com/article/p-uwxpzpcs-ks.html)
(4)GO語言中的面向對象
前面咱們瞭解了一下,什麼是面向對象,以及類和對象的概念。可是,GO語言中的面向對象在某些概念上和其它的編程語言仍是有差異的。
嚴格意義上說,GO語言中沒有類(class)的概念,可是咱們能夠將結構體比做爲類,由於在結構體中能夠添加屬性(成員),方法(函數)。
//結構體:類,結構體中的成員變量:類屬性 type Student struct { Id int name string age int sex string addr string }
類的實例化產生類對象
func main() { //藉助類,實例化後,生成類對象 stu := Student{1001, "張三", 26, "M", "北京"} fmt.Println(stu) }
Go語言中實現面向對象的封裝,繼承,多態的方式分別爲:方法,匿名字段和接口。
所謂繼承指的是,咱們可能會在一些類(結構體)中,寫一些重複的成員,咱們能夠將這些重複的成員,單獨的封裝到一個類(結構體)中,做爲這些類的父類(結構體),咱們能夠經過以下圖來理解:
根據上面的圖,咱們發現學生類(結構體),講師類(結構體)等都有共同的成員(屬性和方法),這樣就存在重複,因此咱們把這些重複的成員封裝到一個父類(結構體)中,而後讓學生類(結構體)和講師類(結構體)繼承父類(結構體)。
接下來,咱們能夠先將公共的屬性,封裝到父類(結構體)中實現繼承,關於方法(函數)的繼承後面再講。
(1)匿名字段建立與初始化
那麼怎樣實現屬性的繼承呢?
能夠經過匿名字段(也叫匿名組合)來實現,什麼是匿名字段呢?經過以下使用,你們就明白了。
type Person struct { id int name string age int } type Student struct { Person //匿名字段 score float64 }
以上代碼經過匿名字段實現了繼承,將公共的屬性封裝在Person中,在Student中直接包含Person,那麼Student中就有了Person中全部的成員,Person就是匿名字段。注意:Person匿名字段,只有類型,沒有名字。
那麼接下來講咱們就能夠給Student賦值了,具體初始化的方式以下:
func main() { var stu Student = Student{Person{1001, "李四", 27}, 99.5} fmt.Println("stu = ",stu) }
以上代碼中建立了一個結構體變量stu,這個stu咱們能夠理解爲就是Student對象,可是要注意語法格式,如下的寫法是錯誤的:
var stu Student = Student{1001,"李四",27,99.5}
其它初始化方式以下:
自動推導類型:
//自動推導類型 stu := Student{Person{1001, "張三", 27}, 98.5} //%+v:顯示更加詳細 fmt.Printf("stu = %+v\n", stu)
制定初始化成員:
//指定成員初始化,沒有初始化的整型自動賦值爲0,字符串爲空 stu := Student{score: 99.7} fmt.Printf("stu = %+v\n", stu)
接下來還能夠對Person中指定的成員進行初始化:
//指定成員初始化,沒有初始化的整型自動賦值爲0,字符串爲空 stu := Student{Person{name: "李四"}, 97} fmt.Printf("stu = %+v\n", stu)
(2)成員操做
建立完成對象後,能夠根據對象來操做對應成員屬性,是經過"."運算符來完成操做的。具體案例以下:
func main() { //指定成員初始化,沒有初始化的整型自動賦值爲0,字符串爲空 stu := Student{Person{1001, "張三", 25}, 96} stu.score = 98 stu.Person.id = 1002 stu.age = 30 fmt.Printf("stu:%+v\n", stu) }
因爲Student繼承了Person,因此Person具備的成員,Student也有,因此根據Student建立出的對象能夠直接對age成員項進行修改。
因爲在Student中添加了匿名字段Person,因此對象s1,也能夠經過匿名字段Person來獲取age,進行修改。
固然也能夠進行以下修改:
func main() { //指定成員初始化,沒有初始化的整型自動賦值爲0,字符串爲空 stu := Student{Person{1001, "張三", 25}, 96} stu.Person = Person{1002, "李四", 30} fmt.Printf("stu:%+v\n", stu) }
直接給對象stu中的Person成員(匿名字段)賦值。
經過以上案例咱們能夠總結出,根據類(結構體)能夠建立出不少的對象,這些對象的成員(屬性)是同樣的,可是成員(屬性)的值是能夠徹底不同的。
(3)同名字段
如今將Student結構體與Person結構體,進行以下的修改:
type Person struct { id int name string age int } type Student struct { Person //匿名字段 name string //與父類Person中的屬性同名 score float64 }
在Student中也加入了一個成員name,這樣與Person重名了,那麼以下代碼是給Student中name賦值仍是給Person中的name 進行賦值?
func main() { var stu Student stu.name = "李四" fmt.Printf("stu:%+v\n", stu) }
輸出結構以下:
stu: {Person:{id:0 name: age:0} name:李四 score:0}
經過結果發現是對Student中的name進行賦值。在子類屬性中,包含與父類相同的屬性時,建立子類時,不會覆蓋父類。在操做同名字段時,有一個基本的原則:若是可以在本身對象所屬的類(結構體)中找到對應的成員,那麼直接進行操做,若是找不到就去對應的父類(結構體)中查找。這就是所謂的就近原則。
(4) 指針匿名字段
結構體(類)中的匿名字段的類型,也能夠是指針:
例如:
type Person struct { id int name string age int } type Student struct { *Person //指針類型匿名字段 score float64 } func main() { stu := Student{&Person{1001, "李四", 27}, 95} fmt.Println(stu) }
獲得的結果以下:
{0xc00000c080 95}
輸出告終構體的地址。若是要取值,能夠進行以下操做:
func main() { stu := Student{&Person{1001, "李四", 27}, 95} fmt.Println(stu.id, stu.name, stu.age, stu.score) }
在定義對象stu時,完成初始化,而後經過"."的操做完成成員的操做。
可是,注意如下的寫法是錯誤的:
type Person struct { id int name string age int } type Student struct { *Person //指針類型匿名字段 score float64 } func main() { var stu Student stu.id = 1002 stu.name = "李四" stu.age = 27 stu.score = 96 fmt.Printf("stu: %+v\n", stu) }
你們能夠思考一下,以上代碼爲何會出錯?
會出錯,錯誤信息以下:
invalid memory address or nil pointer dereference
翻譯成中文:無效的內存地址或nil指針引用
意思是*Person沒有指向任何的內存地址,那麼其默認值爲nil.
也就是指針類型匿名字段*Person沒有指向任何一個結構體,因此對象s也就沒法操做Person中的成員。
具體的解決辦法以下:
func main() { var stu Student stu.Person = new(Person) //使用new分配空間 stu.id = 1002 stu.name = "李四" stu.age = 27 stu.score = 96 fmt.Println(stu.id, stu.name, stu.age, stu.score) }
new( )的做用是分配空間,new( )函數的參數是一個類型,這裏爲Person結構體類型,返回值爲指針類型,因此賦值給*Person,這樣*Person也就指向告終構體Person的內存。
(5)多重繼承
在上面的案例,Student類(結構體)繼承了Person類(結構體),那麼Person是否能夠在繼承別的類(結構體)呢,或者Student也繼承了別的類(結構體)呢?
能夠,這就是多重繼承。
多重繼承有兩種繼承方式:
1.C——B——A(C繼承B,B繼承A)
具體案例以下:
type Object struct { id int flag bool } type Person struct { *Object name string age int } type Student struct { *Person name string //與Person中的屬性同名 score float64 }
接下來,看一下怎樣對多重繼承中的成員進行操做:
func main() { var stu Student stu.Person = new(Person) stu.Object = new(Object) stu.name = "張三" stu.Person.name = "張老三" stu.Person.Object.id = 1003 fmt.Println(stu.Person.Object.id, stu.Person.name, stu.name) }
(2)C——B同時C——A(C繼承B,同時C繼承A)
具體案例以下:
type Person struct { id int name string age int } type Address struct { addr string } type Student struct { *Person *Address name string //與Person中的屬性同名 score float64 }
接下來,看一下怎樣對多重繼承中的成員進行操做:
func main() { var stu Student stu.Person = new(Person) stu.Address = new(Address) stu.name = "李四" stu.Person.name = "李老四" stu.Address.addr = "北京" fmt.Println(stu.name,stu.Person.name,stu.Address.addr) }
注意:多重繼承,很容易出現同名字段,可使用實名字段解決。 應該在程序中,儘可能避免使用多重繼承。
經過上邊內容的講解,你們可以體會出面向對象編程中繼承的優點了,接下來會給你們介紹面向對象編程中另外的特性:封裝性,其實關於封裝性,在前面的編程中,你們也已經可以體會到了,就是經過函數來實現封裝性。
你們仔細回憶一下,當初在講解函數時,重點強調了函數的做用,就是將重複的代碼封裝來,用的時候,直接調用就能夠了,不須要每次都寫一遍,這就是封裝的優點。(超級瑪麗案例)
在面向對象編程中,也有封裝的特性。面向對象中是經過方法來實現。下面,將詳細的給你們講解一下方法的內容。
(1)基本方法建立
在介紹面向對象時,講過能夠經過屬性和方法(函數)來描述對象。
那什麼是方法呢?
方法,你們能夠理解成就是函數,可是在定義使用方面與前面講解的函數仍是有區別的。
咱們先定義一個傳統的函數:
func Test(a, b int) int { return a + b } func main() { result := Test(1, 2) fmt.Println(result) }
這個函數很是簡單,下面定義一個方法,看一下在語法與傳統的函數有什麼區別:
方法的定義:
//爲int類型定義別名 type Integer int //爲Integer綁定方法Test func (a Integer) Test(b Integer) Integer { return a + b } func main() { //定義一個Integer類型變量 var result Integer = 3 r := result.Test(4) fmt.Println(r) }
type Integer int :表示的意思是給int類型指定了一個別名叫Integer,別名能夠隨便起,只要符合GO語言的命名規則就能夠。
指定別名後,後面能夠用Integer來代替int 來使用。
func (a Integer) Test(b Integer) Integer{
}
表示定義了一個方法,方法的定義與函數的區別:
第一:在關鍵字後面加上( a Integer), 這個在方法中稱之爲接收者,所謂的接受者就是接收傳遞過來的第一個參數,而後複製a,a的類型是Integer,因爲Integer是int的別名,因此a的類型爲int。
第二:在表示參數的類型時,都使用了對應的別名。
經過方法的定義,能夠看出方法其實就是給某個類型綁定的函數。在該案例中,是爲整型綁定的函數,只不過在給整型綁定函數(方法)時,必定要經過type來指定一個別名,由於int類型是系統已經規定好了,沒法直接綁定函數,因此只能經過別名的方式。
第三:調用方式不一樣
var result Interger=3
表示定義一個整型變量result,並賦值爲3.
result.Test(3)
經過result變量,完成方法的調用。由於,Test( )方法,是爲int類型綁定的函數,而result變量爲int類型。因此能夠調用Test( )方法。result變量的值會傳遞給Test( )方法的接受者,也就是參數a,而實參Test(3),會傳遞形參b.
固然,咱們也能夠將Test( )方法,理解成是爲int類型擴展了,追加了的方法。由於系統在int類型時,是沒有該方法的。
在以上案例中,Test( )方法是爲int類型綁定的函數,因此任何一個整型變量,均可以調用該方法。
var sum Integer = 6 r := sum.Test(10) fmt.Println(r)
(2)給結構體添加方法
上面給整型建立了一個方法,那麼直接經過整型變量加上"點",就能夠調用該方法了。
你們想一下,若是給結構體(類)加上了方法,那麼根據結構體(類)建立完成對象後,是否是就能夠經過對象加上"點",就能夠完成方法的調用,這與調用類中定義的屬性的方式是徹底同樣的。這樣就完成了經過方法與屬性來描述一個對象的操做。
給結構體添加方法,語法以下:
type Student struct { id int name string age int score float64 } //將方法綁定在指定的結構體(類)上 func (stu Student) PrintInfo() { fmt.Printf("stu: %+v\n", stu) } func main() { stu := Student{1001, "李四", 26, 97} //定義完對象後,調用方法 stu.PrintInfo() }
給結構體添加方法的方式與前面給int類型添加方法的方式,基本一致。惟一不一樣的是,不須要給結構體指定別名,由於結構體Student就是至關於其全部成員屬性的別名(id,name,score),因此這裏不要在給結構體Student建立別名。
調用方式:根據結構體(類)建立的對象,完成了方法的調用。
PrintInfo( )方法的做用,只是將結構體的成員(屬性)值打印出來,若是要修改其對應的值,應該怎麼作呢?
因爲結構體是值傳遞,因此必須經過指針來修改,因此要將方法的接收者,修改爲對應的指針類型。
具體修改以下:
type Student struct { id int name string age int score float64 } //將方法綁定在指定的結構體(類)上 func (stu Student) PrintInfo() { fmt.Printf("stu: %+v\n", stu) } func (stu *Student) EditInfo(new_id int, new_name string, new_age int, new_score float64) { stu.id = new_id stu.name = new_name stu.age = new_age stu.score = new_score } func main() { stu := &Student{1001, "李四", 26, 97} //定義完對象後,調用方法 stu.EditInfo(1003, "王五", 30, 100) fmt.Printf("stu: %+v\n", *stu) }
在建立方法時,接收者類型爲指針類型,因此在調用方法時,建立一個結構體變量,同時將結構體變量的地址,傳遞給方法的接收者,而後調用EditInfo( )方法,完成要修改的數據傳遞。
在使用方法時,要注意以下幾個問題:
第一:只要接收者類型不同,這個方法就算同名,也是不一樣方法,不會出現重複定義函數的錯誤
type long int func (l long)Test() { } type char byte func (c char)Test() { }
可是,若是接收者類型同樣,可是方法的參數不同,是會出現錯誤的。
type long int func (tmp long) Test() { } func (res long) Test(a,b int) { }
也就是,在GO中沒有方法重載(所謂重載,指的是方法名稱一致,參數類型,個數不一致)。
第二:關於接收者不能爲指針類型。
type long int func (tmp *long) Test() { }
以上定義是正確的
但下面的定義就是錯誤的
type pointer *int //pointer爲接收者類型,它自己不能是指針類型 func (tmp pointer) Test() { }
第三:接收者爲普通變量,非指針,值傳遞
type Student struct { id int name string age int score float64 } //將方法綁定在指定的結構體(類)上 func (stu Student) PrintInfo(id int, name string, age int, score float64) { stu.id = id stu.name = name stu.age = age stu.score = score } func main() { var stu Student stu.PrintInfo(1001, "張三", 26, 99) fmt.Printf("stu: %+v\n", stu) }
結果以下:
stu: {id:0 name: age:0 score:0}
接收者爲指針變量,引用傳遞:
type Student struct { id int name string age int score float64 } //將方法綁定在指定的結構體(類)上 func (stu *Student) EditInfo(id int, name string, age int, score float64) { stu.id = id stu.name = name stu.age = age stu.score = score } func main() { var stu Student (&stu).EditInfo(1001, "張三", 26, 99) fmt.Printf("stu: %+v\n", stu) }
結果以下:
stu: {id:1001 name:張三 age:26 score:99}
(3)指針變量的方法值
在上面的案例中,咱們定義了兩個方法,一個是PrintInfo( ), 該方法的接收者爲普通變量,一個EditInfo( )方法,該方法的接收者爲指針變量,那麼你們思考這麼一個問題:定義一個結構體指針變量,可否調用PrintShow( )方法呢?以下所示:
type Student struct { id int name string age int score float64 } func (stu Student) PrintInfo(id int, name string, age int, score float64) { stu.id = id stu.name = name stu.age = age stu.score = score fmt.Printf("stu: %+v\n", stu) } func (stu *Student) EditInfo(id int, name string, age int, score float64) { stu.id = id stu.name = name stu.age = age stu.score = score } func main() { var stu Student (&stu).PrintInfo(1003, "李四", 30, 98) }
結果以下:
stu: {id:1003 name:李四 age:30 score:98}
經過測試,發現是能夠調用的。
爲何結構體指針變量,能夠調用PrintShow( )方法呢?
緣由是:先將指針stu,轉換成*stu(解引用)再調用。
等價以下代碼:
(*(&stu)).PrintInfo(1003, "李四", 30, 98)
因此,若是結構體變量是一個指針變量,它可以調用哪些方法,這些方法就是一個集合,簡稱方法集。
若是是普通的結構體變量可否調用EditInfo( )方法。
type Student struct { id int name string age int score float64 } func (stu Student) PrintInfo(id int, name string, age int, score float64) { stu.id = id stu.name = name stu.age = age stu.score = score fmt.Printf("stu: %+v\n", stu) } func (stu *Student) EditInfo(id int, name string, age int, score float64) { stu.id = id stu.name = name stu.age = age stu.score = score } func main() { var stu Student stu.EditInfo(1002, "王五", 25, 97) fmt.Printf("stu: %+v\n", stu) }
結果以下:
stu: {id:1002 name:王五 age:25 score:97}
是能夠調用的,緣由是:將普通的結構體類型的變量轉換成(&stu)在調用EditInfo( )方法。
這樣的好處是很是靈活,建立完對應的對象後,能夠隨意調用方法,不須要考慮太多指針的問題。
下面進行面向對象編程的練習
練習1:
定義一個學生類,有六個屬性,分別爲姓名、性別、年齡、語文、數學、英語成績。
有2個方法:
第一個打招呼的方法:介紹本身叫XX,今年幾歲了,是男同窗仍是女同窗。
第二個是計算本身總分數和平均分的方法。{顯示:我叫XX,此次考試總成績爲X分,平均成績爲X分}
1:結構體定義以下:
type Student struct { name string sex string age int ch float64 math float64 eng float64 }
2:爲結構體定義相應的方法,而且在方法中能夠完成對傳遞過來的數據的校驗
func (stu *Student) SayHello(name string, sex string, age int) { stu.name = name stu.sex = sex stu.age = age if stu.age < 0 || stu.age > 120 { stu.age = 0 } if stu.sex != "男" || stu.sex != "女" { stu.sex = "男" } fmt.Printf("我叫%s,今年%d歲了,是%s同窗。\n", stu.name, stu.age, stu.sex) } func (stu *Student) SumAndAvg(ch, math, eng float64) { stu.ch = ch stu.math = math stu.eng = eng var sum float64 sum = stu.math + stu.ch + stu.eng fmt.Printf("我叫%s,此次考試總成績是%.1f,平均成績是%.1f。\n", stu.name, sum, sum/3) }
3:完成方法的調用
func main() { var stu Student stu.SayHello("張三", "男", 25) stu.SumAndAvg(96, 97, 98) fmt.Printf("stu :%+v\n", stu) }
結果以下:
我叫張三,今年25歲了,是男同窗。 我叫張三,此次考試總成績是291.0,平均成績是97.0。 stu :{name:張三 sex:男 age:25 ch:96 math:97 eng:98}
在以上的案例中,SayHello()方法中已經完成了name屬性的賦值,因此在SumAndAvg( )方法中,能夠直接使用,由於咱們使用指針指向了同一個結構體內存。
在調用的過程當中,也能體會出確實很方便,不須要考慮太多指針的問題。
如今咱們已經實現了爲結構體添加成員(屬性),和方法,而且實現了成員屬性的繼承,那麼方法可否繼承呢?
具體以下:
type Person struct { name string sex string age int } //Person類型,實現了一個方法 func (per *Person) PrintInfo() { fmt.Printf("name=%s,sex=%s,age=%d\n", per.name, per.sex, per.age) } //定義一個Student類,繼承Person類 type Student struct { Person id int score float64 } func main() { stu := Student{Person{"張三", "M", 26}, 1001, 99} //子類對象調用父類方法 stu.PrintInfo() }
方法繼承與屬性繼承一致,子類對象能夠直接調用父類方法。
練習:根據如下信息,實現對應的繼承關係
記者:我是記者 個人愛好是偷拍 個人年齡是34 我是一個男狗仔
程序員:我叫孫權 個人年齡是23 我是男生 個人工做年限是3年
思路:1.找出公共的屬性,定義父類(結構體)
type Person struct { name string sex string age int }
2:找出公共的方法,定義在父類(結構體)
func (per *Person)serValue(name string,sex string,age int) { per.name = name per.sex = sex per.age = age }
3:找出獨有的屬性,定義在本身的結構體(類)中。
4:找出獨有的方法,定義在本身的結構體(類)中。
type Reporter struct { Person hobby string } func (rep *Reporter) ReporterSayHello(hobby string) { rep.hobby = hobby fmt.Printf("我叫%s,是一名狗仔,我是%s生,今年%d歲,愛好是%s。\n", rep.name, rep.sex, rep.age, rep.hobby) } type Programmer struct { Person workyear int } func (prog *Programmer) ProgrammerSayHello(workyear int) { prog.workyear = workyear fmt.Printf("我叫%s,是一名程序員,我是%s生,今年%d歲,工做%d年了。\n",prog.name,prog.sex,prog.sex,prog.workyear) }
完成調用:
func main() { var rep Reporter rep.setValue("張三", "男", 34) rep.ReporterSayHello("偷拍") var prog Programmer prog.setValue("孫權", "男", 23) prog.ProgrammerSayHello(3) }
結果以下:
我叫張三,是一名狗仔,我是男生,今年34歲,愛好是偷拍。 我叫孫權,是一名程序員,我是男生,今年23歲,工做3年了。
在前面的案例中,子類(結構體)能夠繼承父類中的方法,可是,若是父類中的方法與子類的方法是重名方法會怎樣呢?
type Person struct { name string age int sex string } func (per *Person) PrintInfo() { fmt.Printf("name=%s,age=%d,sex=%s", per.name, per.age, per.sex) } type Student struct { Person id int score float64 } //子類跟父類定義了相同的方法 func (stu *Student) PrintInfo() { fmt.Printf("stu: %+v\n", *stu) } func main() { stu := Student{Person{"張三", 27, "男"}, 1001, 99} stu.PrintInfo() }
上面子類和父類都定義了PrintInfo方法,子類在調用PrintInfo時,是調用子類的方法仍是父類的方法呢?
結果以下:
stu: {Person:{name:張三 age:27 sex:男} id:1001 score:99}
若是子類(結構體)中的方法名與父類(結構體)中的方法名同名,在調用的時候是先調用子類(結構體)中的方法,這就方法的重寫。
所謂的重寫:就是子類(結構體)中的方法,將父類中的相同名稱的方法的功能從新給改寫了。
若是想調用父類的方法該怎麼作呢?
子類對象.父類名.父類方法 —— 使用父類方法
按上面的例子就是:
stu.Person.PrintInfo()
爲何要重寫父類(結構體)的方法呢?
一般,子類(結構體)繼承父類(結構體)的方法,在調用對象繼承方法的時候,調用和執行的是父類的實現。可是,有時候須要
對子類中的繼承方法有不一樣的實現方式。例如,假設動物存在"叫"的方法,從中繼承有,貓類和狗類兩個子類,可是它們的叫是不同的。
例如如下案例:
type Animal struct { age int } func (p *Animal) Bark() { fmt.Println("叫") } type Dog struct { Animal } func (d *Dog) Bark() { fmt.Println("汪汪叫") } type Cat struct { Animal }
func (c *Cat) Bark() { fmt.Println("喵喵叫") }
func main() {
var dog Dog
dog.Bark()
var cat Cat
cat.Bark()
}
在改案例中,定義了一個動物類(結構體),而且有一個叫的方法,接下來小狗的類(結構體)繼承動物類,小貓的類繼承動物類,它們都有了叫的方法,可是動物類中的叫的方法沒法知足小貓和小狗的叫的要求,只能重寫。
在前面的案例中,咱們調用結構體(類)中的方法,通常都是經過以下的方式:
var dog Dog dog.Bark() var cat Cat cat.Bark()
或者是指針變量,如今,在給你們補充另一種方式。
以下所示:
var dog Dog dFunc := dog.Bark dFunc()
以上調用的方式稱爲方法值。這種方式隱藏了接收者。
還有一種調用的方式是經過方法表達式,以下所示:
type Person struct { name string sex string age int } func (p Person) SetInfoValue() { fmt.Printf("SetInfoValue: %p,%v\n", &p, p) } func (p *Person) SetINfoPointer() { fmt.Printf("SetInfoPointer: %p,%v\n", p, p) } func main() { p := Person{"李四", "男", 27} fmt.Printf("main: %p,%v\n", &p, p) /* f := p.SetInfoValue f() 方法值:隱藏了接受者 */ //方法表達式 f := (Person).SetInfoValue f(p) //顯示把接受者傳遞出去 ======》p.SetInfoValue() f2 := (*Person).SetINfoPointer f2(&p) //顯示把接受者傳遞出去 ======》p.SetInfoPointer() }
在講解具體的接口以前,先看以下問題。
使用面向對象的方式,設計一個加減的計算器
代碼以下:
type ObjectOperate struct { num1 int num2 int } type AddOperate struct { ObjectOperate } func (add *AddOperate) Operate(a, b int) int { add.num1 = a add.num2 = b return add.num1 + add.num2 } type SubOperate struct { ObjectOperate } func (sub *SubOperate) Operate(a, b int) int { sub.num1 = a sub.num2 = b return sub.num1 - sub.num2 } func main() { var sub SubOperate fmt.Println(sub.Operate(7, 2)) }
以上實現很是簡單,可是有個問題,在main( )函數中,當咱們想使用減法操做時,建立減法類的對象,調用其對應的減法的方法。可是,有一天,系統需求發生了變化,要求使用加法,再也不使用減法,那麼須要對main( )函數中的代碼,作大量的修改。將原有的代碼註釋掉,建立加法的類對象,調用其對應的加法的方法。有沒有一種方法,讓main( )函數,只修改不多的代碼就能夠解決該問題呢?有,要用到接下來給你們講解的接口的知識點。
(1)什麼是接口?
接口就是一種規範與標準,在生活中常常見接口,例如:筆記本電腦的USB接口,能夠將任何廠商生產的鼠標與鍵盤,與電腦進行連接。爲何呢?緣由就是,USB接口將規範和標準制定好後,各個生產廠商能夠按照該標準生產鼠標和鍵盤就能夠了。
在程序開發中,接口只是規定了要作哪些事情,幹什麼。具體怎麼作,接口是無論的。這和生活中接口的案例也很類似,例如:USB接口,只是規定了標準,可是不關心具體鼠標與鍵盤是怎樣按照標準生產的。
在企業開發中,若是一個項目比較龐大,那麼就須要一個能理清全部業務的架構師來定義一些主要的接口,這些接口告訴開發人員你須要實現那些功能。
(2)接口的定義
接口定義的語法以下:
//定義接口類型 type Human interface { //接口中的方法,只聲明,不實現;由別的類型(自定義類型)實現 sayhi() }
怎樣具體實現接口中定義的方法呢?
type Student struct { name string score float64 } //Student實現了此方法 func (stu *Student) sayhi() { fmt.Printf("學生%s考了%.1f分。\n", stu.name, stu.score) } type Teacher struct { name string subject string } //Teacher實現了此方法 func (tea *Teacher) sayhi() { fmt.Printf("教師%是教%s的。\n", tea.name, tea.subject) }
具體的調用以下:
func main() { //定義接口類型變量 var h Human //只要實現了此接口方法的類型,那麼這個類型的變量(接受者類型)就能夠給h賦值 stu := Student{"張三", 99} h = &stu //這裏必須賦值地址 h.sayhi() tea := Teacher{"李四", "語文"} h = &tea h.sayhi() }
只要類(結構體)實現對應的接口,那麼根據該類建立的對象,能夠賦值給對應的接口類型。
接口的命名習慣以er結尾。
如今咱們用接口來修改一下開始的計算器程序
type Operater interface { result(a, b int) int } type ObjectOperate struct { num1 int num2 int } type AddOperate struct { ObjectOperate } func (add *AddOperate) result(a, b int) int { add.num1 = a add.num2 = b return add.num1 + add.num2 } type SubOperate struct { ObjectOperate } func (sub *SubOperate) result(a, b int) int { sub.num1 = a sub.num2 = b return sub.num1 - sub.num2 } func main() { var o Operater var sub SubOperate o = &sub res := o.result(10, 2) fmt.Println(res) }
(3)多態
接口有什麼好處呢?實現多態。
所謂多態指的是多種表現形式,以下圖所示:
該拖拉機既能夠掃地又能夠當風扇。功能很是強大。
使用接口實現多態的方式以下:
//定義接口類型 type Humaner interface { //接口中的方法只聲明,不實現,由別的類型(自定義類型)實現 PrintInfo() } type Person struct { name string } type Student struct { Person score float64 } //Student實現了該方法 func (stu *Student) PrintInfo() { fmt.Printf("學生%s考了%.1f分。\n", stu.name, stu.score) } type Teacher struct { Person subject string } //Teacher實現了該方法 func (tea *Teacher) PrintInfo() { fmt.Printf("教師%s是教%s的。\n", tea.name, tea.subject) } //定義一個普通函數,參數類型是接口類型 //只有一個函數,能夠有多種表現,多態 //實現了多態 func WhoSay(h Humaner) { h.PrintInfo() } func main() { stu := Student{Person{"張三"}, 96} tea := Teacher{Person{"李四"}, "數學"} //調用同一個函數,經過傳入不一樣參數,獲得不一樣結果,多態,多種形態 WhoSay(&stu) WhoSay(&tea) }
關於接口的定義,以及使用接口實現多態,你們都比較熟悉了,可是多態有什麼好處呢?如今仍是以開始提出的計算器案例給你們講解一下,在開始咱們已經實現了一個加減功能的計算器,可是有人感受太麻煩了,由於實現加法,就要定義加法操做的類(結構體),實現減法就要定義減法的類(結構體),因此這我的實現了一個比較簡單的加減法的計算器,以下所示:
1.使用面向對象的思想實現一個加減功能的計算器,可能有人感受很是簡單,代碼以下:
type Operation struct { } func (p *Operation) GetResult(num1, num2 float64, operate string) float64 { var result float64 switch operate { case "+": result = num1 + num2 case "-": result = num1 - num2 } return result } func main() { var operation Operation res := operation.GetResult(10, 20, "+") fmt.Println(res) }
咱們定義了一個類(結構體),而後爲該類建立了一個方法,封裝了整個計算器功能,之後要使用直接使用該類(結構體)建立對象就能夠了。這就是面向對象總的封裝性。
也就是說,當你寫完這個計算器後,交給你的同事,你的同事要用,直接建立對象,而後調用GetResult()方法就能夠, 根本不須要關心該方法是怎樣實現的。
2.你們仔細觀察上面的代碼,有什麼問題嗎?
如今讓你在該計算器中,再增長一個功能,例如乘法,應該怎麼辦呢?你可能會說很簡單啊,直接在GetResult( )方法的switch中添加一個case分支就能夠了。
問題是:在這個過程當中,若是你不當心將加法修改爲了減法怎麼辦?或者說,對加法運算的規則作了修改怎麼辦?舉例子說明:
你能夠把該程序方法想象成公司中的薪資管理系統。若是公司決定對薪資的運算規則作修改,因爲全部的運算規則都在Operation類中的GetResult()方法中,因此公司只能將該類的代碼所有給你,你才能進行修改。這時,你一看本身做爲開發人員工資這麼低,心想「TMD,老子累死累活纔給這麼點工資,這下有機會了」。直接在本身工資後面加了3000:num1+num2+3000
因此說,咱們應該將 加減等運算分開,不該該所有糅合在一塊兒,這樣你修改加的時候,不會影響其它的運算規則:
具體實現以下:
type Operation struct { num1 float64 num2 float64 } type GetResulter interface { GetResult() float64 } type AddOperation struct { Operation } func (add *AddOperation) GetResult() float64 { return add.num1 + add.num2 } type SubOperation struct { Operation } func (sub *SubOperation) GetResult() float64 { return sub.num1 - sub.num2 } //多態 func Result(i GetResulter) float64 { return i.GetResult() }
如今已經將各個操做分開了,而且這裏咱們還定義了一個父類(結構體),將公共的成員放在該父類中。若是如今要修改某項運算規則,只需將對應的類和方法發給你,進行修改就能夠了。
這裏的實現雖然將各個運算分開了,可是與咱們第一次實現的仍是有點區別。咱們第一次實現的加減計算器也是將各個運算分開了,可是沒有定義接口。那麼該接口的意義是什麼呢?繼續看下面的問題。
3.如今怎樣調用呢?
這就是一開始給你們提出的問題,若是調用的時候,直接建立加法操做的對象,調用對應的方法,那麼後期要改爲減法呢?須要作大量的修改,因此問題解決的方法以下:
//建立一個類負責對象的建立 type OperationFactory struct { } func (f *OperationFactory) CreatOption(num1, num2 float64, option string) float64 { var result float64 switch option { case "+": add := AddOperation{Operation{num1, num2}} result = Result(&add) case "-": sub := SubOperation{Operation{num1,num2}} result = Result(&sub) } return result }
建立了一個類OperationFactory,在改類中添加了一個方法CreateOption( )負責建立對象,若是輸入的是「+」,建立AddOperation的對象,而後調用Result( )方法,將對象的地址傳遞到該方法中,因此變量i指的就是AddOperation,接下來在調用GetResult( )方法,實際上調用的是AddOperation類實現的GetResult( )方法。
同理若是傳遞過來的是「-」,流程也是同樣的。
因此,經過該程序,你們可以體會出多態帶來的好處。
4.最後調用
func main() { var opfactory OperationFactory res := opfactory.CreatOption(10, 20, "+") fmt.Println(res) }
這時會發現調用,很是簡單,若是如今想計算減法,只要將"+",修改爲"-"就能夠。也就是說,除去了main( )函數與具體運算類的依賴。
固然程序通過這樣設計之後:若是如今修改加法的運算規則,只須要修改AddOperation類中對應的方法,不須要關心其它的類。
若是如今要增長「乘法」 功能,應該怎樣進行修改呢?
第一:定義乘法的類,完成乘法運算。
第二:在OperationFactory類中CreateOption( )方法中添加相應的分支。可是這樣作並不會影響到其它的任何運算。
在使用面向對象思想解決問題時,必定要先分析,定義哪些類,哪些接口,哪些方法。把這些分析定義出來,而後在考慮具體實現。
最後完整代碼以下:
type Operation struct { num1 float64 num2 float64 } type Resulter interface { GetResult() float64 } type AddOperation struct { Operation } func (add *AddOperation) GetResult() float64 { return add.num1 + add.num2 } type SubOperation struct { Operation } func (sub *SubOperation) GetResult() float64 { return sub.num1 - sub.num2 } type MulOperation struct { Operation } func (mul *MulOperation) GetResult() float64 { return mul.num1 * mul.num2 } type DivOperation struct { Operation } func (div *DivOperation) GetResult() float64 { return div.num1 / div.num2 } //實現多態 func Result(i Resulter) float64 { return i.GetResult() } type OperationFactory struct { } func (p *OperationFactory) CreateOption(num1, num2 float64, option string) float64 { var result float64 switch option { case "+": add := AddOperation{Operation{num1, num2}} result = Result(&add) case "-": sub := SubOperation{Operation{num1, num2}} result = Result(&sub) case "*": mul := MulOperation{Operation{num1, num2}} result = Result(&mul) case "/": div := DivOperation{Operation{num1, num2}} result = Result(&div) } return result } func main() { var opfactory OperationFactory res := opfactory.CreateOption(3, 4, "/") fmt.Println(res) }
空接口(interface{})不包含任何的方法,正由於如此,全部的類型都實現了空接口,所以空接口能夠存儲任意類型的數值。
例如:
func main() { //空接口萬能類型,能夠保存任意類型的值 var i interface{} fmt.Printf("i = %v\n", i) fmt.Printf("%T\n", i) i = "abc" fmt.Printf("i = %v\n", i) fmt.Printf("%T\n", i) }
結果以下:
i = <nil> <nil> i = abc string
空接口默認值爲nil,默認數據類型是nil;接受完數據後,類型會變成數據對應的數據類型。
當函數能夠接受任意的對象實例時,咱們會將其聲明爲interface{},最典型的例子是標準庫fmt中PrintXXX系列的函數,例如:
func Printf(format string, a ...interface{})
func Println(a ...interface{})
若是本身定義函數,能夠以下:
func Test(arg ...interface{}) {
}
Test( )函數能夠接收任意個數,任意類型的參數。
如今有一個問題,由空接口接收的值能夠進行運算嗎?咱們看一下代碼:
func main() { //空接口萬能類型,能夠保存任意類型的值 var i interface{} i = 100 fmt.Printf("%T\n", i) fmt.Println(i + 100) }
執行時會報錯:
invalid operation: i + 100 (mismatched types interface {} and int)
由此可知,由空接口接收的值是不能參與運算的,要想能參與運算,須要用到等會兒會提到的類型斷言。
類型斷言就是判斷一個變量是不是某一數據類型的變量 。類型斷言的語法是:value,status := element.(T);這裏value就是變量的值,status是一個bool類型,element是interface變量,T是斷言的數據類型。若是element裏面確實存儲了T類型的數據,則status就爲true,value就會保存對應的值;不然status就爲false,value就爲T類型的默認值。由空接口接收的數據,若是想要參與運算,必定要進行類型斷言。
具體案例以下:
type Student struct { id int name string } func main() { i := make([]interface{}, 3) i[0] = 1 //int i[1] = "hello" //string i[2] = Student{1001, "張三"} //Student for index, data := range i { if value, status := data.(int); status { fmt.Printf("i[%d] 類型爲int,內容爲%d\n", index, value) } else if value, status := data.(string); status { fmt.Printf("i[%d] 類型爲string,內容爲%s\n", index, value) } else if value, status := data.(Student); status { fmt.Printf("i[%d] 類型爲Student,內容爲%+v\n", index, value) } } }
結果以下:
i[0] 類型爲int,內容爲1 i[1] 類型爲string,內容爲hello i[2] 類型爲Student,內容爲{id:1001 name:張三}
用switch語句完成以下:
type Student struct { id int name string } func main() { i := make([]interface{}, 3) i[0] = 1 //int i[1] = "hello" //string i[2] = Student{1001, "張三"} //Student for index, data := range i { switch value := data.(type) { case int: fmt.Printf("i[%d] 類型是int,內容爲%v\n", index, value) case string: fmt.Printf("i[%d] 類型是string,內容爲%v\n", index, value) case Student: fmt.Printf("i[%d] 類型是Student,內容爲%+v\n", index, value) } } }