Go語言學習之路第7天(面向對象)

一.面向對象

  (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.1 匿名字段

  所謂繼承指的是,咱們可能會在一些類(結構體)中,寫一些重複的成員,咱們能夠將這些重複的成員,單獨的封裝到一個類(結構體)中,做爲這些類的父類(結構體),咱們能夠經過以下圖來理解:   

  根據上面的圖,咱們發現學生類(結構體),講師類(結構體)等都有共同的成員(屬性和方法),這樣就存在重複,因此咱們把這些重複的成員封裝到一個父類(結構體)中,而後讓學生類(結構體)和講師類(結構體)繼承父類(結構體)。

  接下來,咱們能夠先將公共的屬性,封裝到父類(結構體)中實現繼承,關於方法(函數)的繼承後面再講。

 

  (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.2 方法

  經過上邊內容的講解,你們可以體會出面向對象編程中繼承的優點了,接下來會給你們介紹面向對象編程中另外的特性:封裝性,其實關於封裝性,在前面的編程中,你們也已經可以體會到了,就是經過函數來實現封裝性。

  你們仔細回憶一下,當初在講解函數時,重點強調了函數的做用,就是將重複的代碼封裝來,用的時候,直接調用就能夠了,不須要每次都寫一遍,這就是封裝的優點。(超級瑪麗案例)

  在面向對象編程中,也有封裝的特性。面向對象中是經過方法來實現。下面,將詳細的給你們講解一下方法的內容。

 

  (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( )方法中,能夠直接使用,由於咱們使用指針指向了同一個結構體內存。

  在調用的過程當中,也能體會出確實很方便不須要考慮太多指針的問題。

 

1.3 方法繼承

  如今咱們已經實現了爲結構體添加成員(屬性),和方法,而且實現了成員屬性的繼承,那麼方法可否繼承呢?

  具體以下:

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年了。

 

1.4 方法重寫

  在前面的案例中,子類(結構體)能夠繼承父類中的方法,可是,若是父類中的方法與子類的方法是重名方法會怎樣呢?

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

  在改案例中,定義了一個動物類(結構體),而且有一個叫的方法,接下來小狗的類(結構體)繼承動物類,小貓的類繼承動物類,它們都有了叫的方法,可是動物類中的叫的方法沒法知足小貓和小狗的叫的要求,只能重寫。

 

1.5 方法地址和放大表達式

  在前面的案例中,咱們調用結構體(類)中的方法,通常都是經過以下的方式:

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

 

1.6 接口

  在講解具體的接口以前,先看以下問題。

  使用面向對象的方式,設計一個加減的計算器

  代碼以下:

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

 

1.7 空接口

  空接口(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)

由此可知,由空接口接收的值是不能參與運算的,要想能參與運算,須要用到等會兒會提到的類型斷言。

 

1.8 類型斷言

  類型斷言就是判斷一個變量是不是某一數據類型的變量 。類型斷言的語法是: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)
		}
	}

}
相關文章
相關標籤/搜索