go語言20小時從入門到精通(八、 面向物件程式設計)
##8.1 概述 對於面向物件程式設計的支援Go 語言設計得非常簡潔而優雅。因為, Go語言並沒有沿襲傳統面向物件程式設計中的諸多概念,比如繼承(不支援繼承,儘管匿名欄位的記憶體佈局和行為類似繼承,但它並不是繼承)、虛擬函式、建構函式和解構函式、隱藏的this指標等。
儘管Go語言中沒有封裝、繼承、多型這些概念,但同樣通過別的方式實現這些特性: 封裝:通過方法實現 繼承:通過匿名欄位實現 多型:通過介面實現
##8.2 匿名組合 ###8.2.1 匿名欄位 一般情況下,定義結構體的時候是欄位名與其型別一一對應,實際上Go支援只提供型別,而不寫欄位名的方式,也就是匿名欄位,也稱為嵌入欄位。
當匿名欄位也是一個結構體的時候,那麼這個結構體所擁有的全部欄位都被隱式地引入了當前定義的這個結構體。
//人
type Person struct {
name string
sex byte
age int
}
//學生
type Student struct {
Person // 匿名欄位,那麼預設Student就包含了Person的所有欄位
id int
addr string
}
複製程式碼
###8.2.2 初始化
//人
type Person struct {
name string
sex byte
age int
}
//學生
type Student struct {
Person // 匿名欄位,那麼預設Student就包含了Person的所有欄位
id int
addr string
}
func main () {
//順序初始化
s1 := Student{Person{"mike",'m',18},1,"sz"}
//s1 = {Person:{name:mike sex:109 age:18} id:1 addr:sz}
fmt.Printf("s1 = %+v\n",s1)
//s2 := Student{"mike",18,"sz"} //err
//部分成員初始化1
s3 := Student{Person: Person{"lily",'f',19},id: 2}
//s3 = {Person:{name:lily sex:102 age:19} id:2 addr:}
fmt.Printf("s3 = %+v\n" ,s3)
//部分成員初始化2
s4 := Student{Person: Person{name: "tom"},id: 3}
//s4 = {Person:{name:tom sex:0 age:0} id:3 addr:}
fmt.Printf("s4 = %+v\n",s4)
}
複製程式碼
###8.2.3 成員的操作
var s1 Student //變數宣告 //給成員賦值 s1.name = "mike" //等價於 s1.Person.name = "mike" s1.sex = 'm' s1.age = 18 s1.id = 1 s1.addr = "sz" fmt.Println(s1) //{{mike 109 18} 1 sz}
var s2 Student //變數宣告 s2.Person = Person{"lily",'f',19} s2.id = 2 s2.addr = "bj" fmt.Println(s2) //{{lily 102 19} 2 bj}
###8.2.4 同名欄位
//人
type Person struct {
name string
sex byte
age int
}
//學生
type Student struct {
Person // 匿名欄位,那麼預設Student就包含了Person的所有欄位
id int
addr string
name string //和Person中的name同名
}
func main() {
var s Student //變數宣告
//給Student的name,還是給Person賦值?
s.name = "mike"
//{Person:{name: sex:0 age:0} id:0 addr: name:mike}
fmt.Printf("%+v\n",s)
//預設只會給最外層的成員賦值
//給匿名同名成員賦值,需要顯示呼叫
s.Person.name = "yoyo"
//Person:{name:yoyo sex:0 age:0} id:0 addr: name:mike}
fmt.Printf("%+v\n",s)
}
複製程式碼
###8.2.5 其它匿名欄位 ####8.2.5.1 非結構體型別 所有的內建型別和自定義型別都是可以作為匿名欄位的:
type mystr string //自定義型別
type Person struct {
name string
sex byte
age int
}
type Student struct {
Person // 匿名欄位,結構體型別
int // 匿名欄位,內建型別
mystr // 匿名欄位,自定義型別
}
func main() {
//初始化
s1 := Student{Person{"mike","bj"}
//{Person:{name:mike sex:109 age:18} int:1 mystr:bj}
fmt.Printf("%+v\n",s1)
//成員的操作,列印結果:mike,m,bj
fmt.Printf("%s,%c,%d,%s\n",s1.name,s1.sex,s1.age,s1.int,s1.mystr)
}
複製程式碼
####8.2.5.2 結構體指標型別
type Person struct { //人
name string
sex byte
age int
}
type Student struct { //學生
*Person // 匿名欄位,結構體指標型別
id int
addr string
}
func main() {
//初始化
s1 := Student{&Person{"mike","bj"}
//{Person:0xc0420023e0 id:1 addr:bj}
fmt.Printf("%+v\n",s1)
//mike,18
fmt.Printf("%s,%d\n",s1.age)
//宣告變數
var s2 Student
s2.Person = new(Person) //分配空間
s2.name = "yoyo"
s2.sex = 'f'
s2.age = 20
s2.id = 2
s2.addr = "sz"
//yoyo 102 20 2 20
fmt.Println(s2.name,s2.sex,s2.age,s2.id,s2.age)
}
複製程式碼
##8.3 方法 ###8.3.1 概述 在面向物件程式設計中,一個物件其實也就是一個簡單的值或者一個變數,在這個物件中會包含一些函式,這種帶有接收者的函式,我們稱為方法(method)。 本質上,一個方法則是一個和特殊型別關聯的函式。
一個面向物件的程式會用方法來表達其屬性和對應的操作,這樣使用這個物件的使用者就不需要直接去操作物件,而是藉助方法來做這些事情。
在Go語言中,可以給任意自定義型別(包括內建型別,但不包括指標型別)新增相應的方法。
⽅法總是繫結物件例項,並隱式將例項作為第⼀實參 (receiver),方法的語法如下: func (receiver ReceiverType) funcName(parameters) (results) 引數 receiver 可任意命名。如⽅法中未曾使⽤,可省略引數名。 引數 receiver 型別可以是 T 或 *T。基型別 T 不能是接⼝或指標。 不支援過載方法,也就是說,不能定義名字相同但是不同引數的方法。 ###8.3.2 為型別新增方法 ####8.3.2.1 基礎型別作為接收者
type MyInt int //自定義型別,給int改名為MyInt
//在函式定義時,在其名字之前放上一個變數,即是一個方法
func (a MyInt) Add(b MyInt) MyInt { //面向物件
return a + b
}
//傳統方式的定義
func Add(a,b MyInt) MyInt { //面向過程
return a + b
}
func main() {
var a MyInt = 1
var b MyInt = 1
//呼叫func (a MyInt) Add(b MyInt)
fmt.Println("a.Add(b) = ",a.Add(b)) //a.Add(b) = 2
//呼叫func Add(a,b MyInt)
fmt.Println("Add(a,b) = ",Add(a,b)) //Add(a,b) = 2
}
複製程式碼
通過上面的例子可以看出,面向物件只是換了一種語法形式來表達。方法是函式的語法糖,因為receiver其實就是方法所接收的第1個引數。
注意:雖然方法的名字一模一樣,但是如果接收者不一樣,那麼方法就不一樣。
####8.3.2.2 結構體作為接收者 方法裡面可以訪問接收者的欄位,呼叫方法通過點( . )訪問,就像struct裡面訪問欄位一樣:
type Person struct {
name string
sex byte
age int
}
func (p Person) PrintInfo() { //給Person新增方法
fmt.Println(p.name,p.sex,p.age)
}
func main() {
p := Person{"mike",18} //初始化
p.PrintInfo() //呼叫func (p Person) PrintInfo()
}
8.3.3 值語義和引用語義
type Person struct {
name string
sex byte
age int
}
//指標作為接收者,引用語義
func (p *Person) SetInfoPointer() {
//給成員賦值
(*p).name = "yoyo"
p.sex = 'f'
p.age = 22
}
//值作為接收者,值語義
func (p Person) SetInfoValue() {
//給成員賦值
p.name = "yoyo"
p.sex = 'f'
p.age = 22
}
func main() {
//指標作為接收者,引用語義
p1 := Person{"mike",18} //初始化
fmt.Println("函式呼叫前 = ",p1) //函式呼叫前 = {mike 109 18}
(&p1).SetInfoPointer()
fmt.Println("函式呼叫後 = ",p1) //函式呼叫後 = {yoyo 102 22}
fmt.Println("==========================")
p2 := Person{"mike",18} //初始化
//值作為接收者,值語義
fmt.Println("函式呼叫前 = ",p2) //函式呼叫前 = {mike 109 18}
p2.SetInfoValue()
fmt.Println("函式呼叫後 = ",p2) //函式呼叫後 = {mike 109 18}
}
複製程式碼
###8.3.4 方法集 型別的方法集是指可以被該型別的值呼叫的所有方法的集合。
用例項例項 value 和 pointer 呼叫方法(含匿名欄位)不受⽅法集約束,編譯器編總是查詢全部方法,並自動轉換 receiver 實參。
####8.3.4.1 型別 *T 方法集 一個指向自定義型別的值的指標,它的方法集由該型別定義的所有方法組成,無論這些方法接受的是一個值還是一個指標。
如果在指標上呼叫一個接受值的方法,Go語言會聰明地將該指標解引用,並將指標所指的底層值作為方法的接收者。
型別 *T ⽅法集包含全部 receiver T + *T ⽅法:
type Person struct {
name string
sex byte
age int
}
//指標作為接收者,引用語義
func (p *Person) SetInfoPointer() {
(*p).name = "yoyo"
p.sex = 'f'
p.age = 22
}
//值作為接收者,值語義
func (p Person) SetInfoValue() {
p.name = "xxx"
p.sex = 'm'
p.age = 33
}
func main() {
//p 為指標型別
var p *Person = &Person{"mike",18}
p.SetInfoPointer() //func (p) SetInfoPointer()
p.SetInfoValue() //func (*p) SetInfoValue()
(*p).SetInfoValue() //func (*p) SetInfoValue()
}
複製程式碼
####8.3.4.2 型別 T 方法集 一個自定義型別值的方法集則由為該型別定義的接收者型別為值型別的方法組成,但是不包含那些接收者型別為指標的方法。
但這種限制通常並不像這裡所說的那樣,因為如果我們只有一個值,仍然可以呼叫一個接收者為指標型別的方法,這可以藉助於Go語言傳值的地址能力實現。
type Person struct {
name string
sex byte
age int
}
//指標作為接收者,引用語義
func (p *Person) SetInfoPointer() {
(*p).name = "yoyo"
p.sex = 'f'
p.age = 22
}
//值作為接收者,值語義
func (p Person) SetInfoValue() {
p.name = "xxx"
p.sex = 'm'
p.age = 33
}
func main() {
//p 為普通值型別
var p Person = Person{"mike",18}
(&p).SetInfoPointer() //func (&p) SetInfoPointer()
p.SetInfoPointer() //func (&p) SetInfoPointer()
p.SetInfoValue() //func (p) SetInfoValue()
(&p).SetInfoValue() //func (*&p) SetInfoValue()
}
複製程式碼
###8.3.5 匿名欄位 ####8.3.5.1 方法的繼承 如果匿名欄位實現了一個方法,那麼包含這個匿名欄位的struct也能呼叫該方法。
type Person struct {
name string
sex byte
age int
}
//Person定義了方法
func (p *Person) PrintInfo() {
fmt.Printf("%s,p.name,p.age)
}
type Student struct {
Person // 匿名欄位,那麼Student包含了Person的所有欄位
id int
addr string
}
func main() {
p := Person{"mike",18}
p.PrintInfo()
s := Student{Person{"yoyo",20},2,"sz"}
s.PrintInfo()
}
複製程式碼
####8.3.5.2 方法的重寫
type Person struct {
name string
sex byte
age int
}
//Person定義了方法
func (p *Person) PrintInfo() {
fmt.Printf("Person: %s,p.age)
}
type Student struct {
Person // 匿名欄位,那麼Student包含了Person的所有欄位
id int
addr string
}
//Student定義了方法
func (s *Student) PrintInfo() {
fmt.Printf("Student:%s,s.name,s.sex,s.age)
}
func main() {
p := Person{"mike",18}
p.PrintInfo() //Person: mike,18
s := Student{Person{"yoyo","sz"}
s.PrintInfo() //Student:yoyo,f,20
s.Person.PrintInfo() //Person: yoyo,20
}
複製程式碼
###8.3.6 表示式 類似於我們可以對函式進行賦值和傳遞一樣,方法也可以進行賦值和傳遞。
根據呼叫者不同,方法分為兩種表現形式:方法值和方法表示式。兩者都可像普通函式那樣賦值和傳參,區別在於方法值繫結例項,⽽方法表示式則須顯式傳參。
####8.3.6.1 方法值
type Person struct {
name string
sex byte
age int
}
func (p *Person) PrintInfoPointer() {
fmt.Printf("%p,%v\n",p,p)
}
func (p Person) PrintInfoValue() {
fmt.Printf("%p,&p,p)
}
func main() {
p := Person{"mike",18}
p.PrintInfoPointer() //0xc0420023e0,&{mike 109 18}
pFunc1 := p.PrintInfoPointer //方法值,隱式傳遞 receiver
pFunc1() //0xc0420023e0,&{mike 109 18}
pFunc2 := p.PrintInfoValue
pFunc2() //0xc042048420,{mike 109 18}
}
複製程式碼
####8.3.6.2 方法表示式
type Person struct {
name string
sex byte
age int
}
func (p *Person) PrintInfoPointer() {
fmt.Printf("%p,&{mike 109 18}
//方法表示式, 須顯式傳參
//func pFunc1(p *Person))
pFunc1 := (*Person).PrintInfoPointer
pFunc1(&p) //0xc0420023e0,&{mike 109 18}
pFunc2 := Person.PrintInfoValue
pFunc2(p) //0xc042002460,{mike 109 18}
}
複製程式碼
##8.4 介面 ###8.4.1 概述 在Go語言中,介面(interface)是一個自定義型別,介面型別具體描述了一系列方法的集合。
介面型別是一種抽象的型別,它不會暴露出它所代表的物件的內部值的結構和這個物件支援的基礎操作的集合,它們只會展示出它們自己的方法。因此介面型別不能將其例項化。
Go通過介面實現了鴨子型別(duck-typing):“當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子”。我們並不關心物件是什麼型別,到底是不是鴨子,只關心行為。
###8.4.2 介面的使用 ####8.4.2.1 介面定義
type Humaner interface {
SayHi()
}
複製程式碼
接⼝命名習慣以 er 結尾 介面只有方法宣告,沒有實現,沒有資料欄位 介面可以匿名嵌入其它介面,或嵌入到結構中
####8.4.2.2 介面實現 介面是用來定義行為的型別。這些被定義的行為不由介面直接實現,而是通過方法由使用者定義的型別實現,一個實現了這些方法的具體型別是這個介面型別的例項。
如果使用者定義的型別實現了某個介面型別宣告的一組方法,那麼這個使用者定義的型別的值就可以賦給這個介面型別的值。這個賦值會把使用者定義的型別的值存入介面型別的值。
type Humaner interface {
SayHi()
}
type Student struct { //學生
name string
score float64
}
//Student實現SayHi()方法
func (s *Student) SayHi() {
fmt.Printf("Student[%s,%f] say hi!!\n",s.score)
}
type Teacher struct { //老師
name string
group string
}
//Teacher實現SayHi()方法
func (t *Teacher) SayHi() {
fmt.Printf("Teacher[%s,%s] say hi!!\n",t.name,t.group)
}
type MyStr string
//MyStr實現SayHi()方法
func (str MyStr) SayHi() {
fmt.Printf("MyStr[%s] say hi!!\n",str)
}
//普通函式,引數為Humaner型別的變數i
func WhoSayHi(i Humaner) {
i.SayHi()
}
func main() {
s := &Student{"mike",88.88}
t := &Teacher{"yoyo","Go語言"}
var tmp MyStr = "測試"
s.SayHi() //Student[mike,88.880000] say hi!!
t.SayHi() //Teacher[yoyo,Go語言] say hi!!
tmp.SayHi() //MyStr[測試] say hi!!
//多型,呼叫同一介面,不同表現
WhoSayHi(s) //Student[mike,88.880000] say hi!!
WhoSayHi(t) //Teacher[yoyo,Go語言] say hi!!
WhoSayHi(tmp) //MyStr[測試] say hi!!
x := make([]Humaner,3)
//這三個都是不同型別的元素,但是他們實現了interface同一個介面
x[0],x[1],x[2] = s,t,tmp
for _,value := range x {
value.SayHi()
}
/*
Student[mike,88.880000] say hi!!
Teacher[yoyo,Go語言] say hi!!
MyStr[測試] say hi!!
*/
}
複製程式碼
通過上面的程式碼,你會發現介面就是一組抽象方法的集合,它必須由其他非介面型別實現,而不能自我實現。
###8.4.3 介面組合 ####8.4.3.1 介面嵌入 如果一個interface1作為interface2的一個嵌入欄位,那麼interface2隱式的包含了interface1裡面的方法。
type Humaner interface {
SayHi()
}
type Personer interface {
Humaner //這裡想寫了SayHi()一樣
Sing(lyrics string)
}
type Student struct { //學生
name string
score float64
}
//Student實現SayHi()方法
func (s *Student) SayHi() {
fmt.Printf("Student[%s,s.score)
}
//Student實現Sing()方法
func (s *Student) Sing(lyrics string) {
fmt.Printf("Student sing[%s]!!\n",lyrics)
}
func main() {
s := &Student{"mike",88.88}
var i2 Personer
i2 = s
i2.SayHi() //Student[mike,88.880000] say hi!!
i2.Sing("學生哥") //Student sing[學生哥]!!
}
複製程式碼
####8.4.3.2 介面轉換 超集接⼝物件可轉換為⼦集接⼝,反之出錯:
type Humaner interface {
SayHi()
}
type Personer interface {
Humaner //這裡像寫了SayHi()一樣
Sing(lyrics string)
}
type Student struct { //學生
name string
score float64
}
//Student實現SayHi()方法
func (s *Student) SayHi() {
fmt.Printf("Student[%s,lyrics)
}
func main() {
//var i1 Humaner = &Student{"mike",88.88}
//var i2 Personer = i1 //err
//Personer為超集,Humaner為子集
var i1 Personer = &Student{"mike",88.88}
var i2 Humaner = i1
i2.SayHi() //Student[mike,88.880000] say hi!!
}
複製程式碼
###8.4.4 空介面 空介面(interface{})不包含任何的方法,正因為如此,所有的型別都實現了空介面,因此空介面可以儲存任意型別的數值。它有點類似於C語言的void *型別。
var v1 interface{} = 1 // 將int型別賦值給interface{} var v2 interface{} = "abc" // 將string型別賦值給interface{} var v3 interface{} = &v2 // 將*interface{}型別賦值給interface{} var v4 interface{} = struct{ X int }{1} var v5 interface{} = &struct{ X int }{1}
當函式可以接受任意的物件例項時,我們會將其宣告為interface{},最典型的例子是標準庫fmt中PrintXXX系列的函式,例如:
func Printf(fmt string,args ...interface{})
func Println(args ...interface{})
複製程式碼
###8.4.5 型別查詢 我們知道interface的變數裡面可以儲存任意型別的數值(該型別實現了interface)。那麼我們怎麼反向知道這個變數裡面實際儲存了的是哪個型別的物件呢?目前常用的有兩種方法: comma-ok斷言 switch測試
####8.4.5.1 comma-ok斷言 Go語言裡面有一個語法,可以直接判斷是否是該型別的變數: value,ok = element.(T),這裡value就是變數的值,ok是一個bool型別,element是interface變數,T是斷言的型別。
如果element裡面確實儲存了T型別的數值,那麼ok返回true,否則返回false。
示例程式碼:
type Element interface{}
type Person struct {
name string
age int
}
func main() {
list := make([]Element,3)
list[0] = 1 // an int
list[1] = "Hello" // a string
list[2] = Person{"mike",18}
for index,element := range list {
if value,ok := element.(int); ok {
fmt.Printf("list[%d] is an int and its value is %d\n",index,value)
} else if value,ok := element.(string); ok {
fmt.Printf("list[%d] is a string and its value is %s\n",ok := element.(Person); ok {
fmt.Printf("list[%d] is a Person and its value is [%s,%d]\n",value.name,value.age)
} else {
fmt.Printf("list[%d] is of a different type\n",index)
}
}
/* 列印結果:
list[0] is an int and its value is 1
list[1] is a string and its value is Hello
list[2] is a Person and its value is [mike,18]
*/
}
8.4.5.2 switch測試
type Element interface{}
type Person struct {
name string
age int
}
func main() {
list := make([]Element,3)
list[0] = 1 //an int
list[1] = "Hello" //a string
list[2] = Person{"mike",element := range list {
switch value := element.(type) {
case int:
fmt.Printf("list[%d] is an int and its value is %d\n",value)
case string:
fmt.Printf("list[%d] is a string and its value is %s\n",value)
case Person:
fmt.Printf("list[%d] is a Person and its value is [%s,value.age)
default:
fmt.Println("list[%d] is of a different type",index)
}
}
}
複製程式碼