Go語言開發(五)、Go語言面向介面
一、Duck Typing簡介
1、Duck Typing簡介
對於一門強型別的靜態語言來說,要想通過運行時多態來隔離變化,多個實作類別就必須屬於同一類型體系,必須通過繼承的方式與同一抽象類別型建立is-a關係。
而Duck Typing則是一種基於特徵,而不是基於類型的多態方式。Duck Typing仍然關心is-a,只不過is-a關係是以對方是否具備相關的特徵來確定的。
是否滿足is-a關係可以使用所謂的鴨子測試(Duck Test)進行判斷。
"當看到一隻鳥走起來像鴨子、遊泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子。"
Duck Test是基於特徵的哲學,給設計提供了強大的靈活性。動態物件導向語言,如Python,Ruby等都遵從了Duck Test來實現運行時多態。
2、C++對Duck Typing的支援
Duck Typing並不是動態語言的專利。C++作為一門強型別的靜態語言,也對Duck Typing特性有強有力的支援。不過C++對Duck Typing特性支援不是在運行時,而是在編譯時間。
C++通過泛型程式設計實現對Duck Typing的支援。對於一個模板類或模板函數,會要求其執行個體化的類型必須具備某種特徵,如某個函數簽名、某個類型定義、某個成員變數等等。如果特徵不具備,編譯器會報錯。
因此C++模板類、模板函數對要執行個體化的客戶類提出了特徵要求,客戶類型需要實現相應的特徵要求,從而複用模板的實現。
Duck Typing需要執行個體化的類型具備一致的特徵,而模板特化的作用正是為了讓不同類型具有統一的特徵(統一的操作介面),所以模板特化可以作為Duck Typing與執行個體化類型之間的適配器。這種模板特化手段稱為萃取(Traits),其中類型萃取最為常見。
類型萃取首先是一種非侵入性的中介層。否則,這些特徵就必須被執行個體化類型提供,而就意味著,當一個執行個體化類型需要複用多個Duck Typing模板時,就需要迎合多種特徵,從而讓自己經常被修改,並逐漸層得龐大和難以理解。
一個Duck Typing模板,比如一個通用演算法,需要執行個體化類型提供一些特徵時,如果一個類型是類,則是一件很容易的事情,因為你可以在一個類裡定義任何需要的特徵。但如果一個基本類型也想複用此通用演算法,由於基本類型無法靠自己提供演算法所需要的特徵,就必須藉助於類型萃取。
3、Go語言對Duck Typing的支援
Go語言作為一種靜態語言,對Duck Typing的支援通過Structural Typing實現。
Structural Typing是Go語言式的介面,就是不用顯示宣告類型T實現了介面I,只要類型T的公開方法完全滿足介面I的要求,就可以把類型T的對象用在需要介面I的地方。
package mainimport "fmt"type ISayHello interface { sayHello()}//美國人type AmericalPerson struct {}func (person AmericalPerson)sayHello(){ fmt.Println("Hello!")}//中國人type ChinesePerson struct {}func (person ChinesePerson)sayHello(){ fmt.Println("你好!")}func greet(i ISayHello){ i.sayHello()}func main() { ameriacal := AmericalPerson{} chinese := ChinesePerson{} var i ISayHello i = ameriacal i.sayHello() i = chinese i.sayHello()}
二、介面的定義和實現
1、介面的定義
Go語言的介面是一種資料類型,介面把所有的具有共性的方法定義在一起,任何其它類型只要實現了介面定義的方法就是實現了介面。介面的定義文法如下:
/* 定義介面 */type interface_name interface { method_name1 [return_type] method_name2 [return_type] method_name3 [return_type] ... method_namen [return_type]}
2、介面的實現
介面的實現是隱式的,不需要顯示聲明實現了介面,只需要實現介面的方法介面。介面的實現文法如下:
/* 定義結構體 */type struct_name struct { /* variables */}/* 實現介面方法 */func (struct_name_variable struct_name) method_name1() [return_type] { /* 方法實現 */}...func (struct_name_variable struct_name) method_namen() [return_type] { /* 方法實現*/}
3、介面的定義與實現執行個體
package mainimport "fmt"//介面的定義type ISayHello interface { sayHello()}//介面的實現//美國人type AmericalPerson struct {}func (person AmericalPerson)sayHello(){ fmt.Println("Hello!")}//介面的實現//中國人type ChinesePerson struct {}func (person ChinesePerson)sayHello(){ fmt.Println("你好!")}func greet(i ISayHello){ i.sayHello()}func main() { ameriacal := AmericalPerson{} chinese := ChinesePerson{} var i ISayHello i = ameriacal i.sayHello() i = chinese i.sayHello()}
三、介面的實值型別
1、介面的實值型別
一個類型可以實現任意數量的介面,每個類型都實現了一個空介面interface{}。介面是一組方法簽名的集合,本質也是一種類型。
如果聲明了一個介面變數,介面變數能夠儲存任何實現該介面的物件類型。
介面類型的本質就是如果一個資料類型實現了介面自身的方法集,那麼該介面類型變數就能夠引用該資料類型的值。
介面類型變數儲存了兩部分資訊,一個是分配給介面變數的具體值,一個是值的類型的描述器,形式是(value, concrete type),而不是(value, interface type)。
2、空介面
空介面類型interface{}一個方法簽名也不包含,所以所有的資料類型都實現了空介面。
空介面類型可以用於儲存任意資料類型的執行個體。
如果一個函數的參數是空介面類型interface{},表明可以使用任何類型的資料。如果一個函數返回一個空介面類型,表明函數可以返回任何類型的資料。
interface{}可用於向函數傳遞任意類型的變數,但對於函數內部,該變數仍然為interface{}類型(空介面類型),而不是傳入的實參類型。
利用介面類型作為參數可以達到抽象資料類型的目的。
定義一個MaxInterface介面,包含三個方法簽名:
Len() int:必須返回集合資料結構的長度
Get(int i) interface{}:必須返回一個在索引i的資料元素
Bigger(i, j int) bool: 返回位於索引i和j的數值比較結果
滿足MaxInterface介面的資料類型需要實現以上三個方法。
package mainimport "fmt"//Person類型type Person struct{ name string age int}//切片類型type IntSlice []inttype FloatSlice []float32type PersonSlice []Person//介面定義type MaxInterface interface { Len() int Get(i int)interface{} Bigger(i,j int)bool}//Len()方法的實現func (x IntSlice) Len()int{ return len(x)}func (x FloatSlice) Len()int{ return len(x)}func (x PersonSlice) Len()int{ return len(x)}//Get(i int)方法實現func (x IntSlice) Get(i int)interface{}{ return x[i]}func (x FloatSlice) Get(i int)interface{}{ return x[i]}func (x PersonSlice) Get(i int)interface{}{ return x[i]}//Bigger(i,j int)方法實現func (x IntSlice) Bigger(i,j int)bool{ if x[i] > x[j]{ return true }else{ return false }}func (x FloatSlice) Bigger(i,j int)bool{ if x[i] > x[j]{ return true }else { return false }}func (x PersonSlice) Bigger(i,j int)bool{ if x[i].age > x[j].age{ return true }else { return false }}//求最大值函數實現func Max(data MaxInterface) (ok bool, max interface{}){ if data.Len() == 0{ return false,nil } if data.Len() == 1{ return true,data.Get(1) } max = data.Get(0) m := 0 for i:=1;i<data.Len();i++{ if data.Bigger(i,m){ max = data.Get(i) m = i } } return true, max}func main() { intslice := IntSlice{1, 2, 44, 6, 44, 222} floatslice := FloatSlice{1.99, 3.14, 24.8} group := PersonSlice{ Person{name:"Jack", age:24}, Person{name:"Bob", age:23}, Person{name:"Bauer", age:104}, Person{name:"Paul", age:44}, Person{name:"Sam", age:34}, Person{name:"Lice", age:54}, Person{name:"Karl", age:74}, Person{name:"Lee", age:4}, } _,m := Max(intslice) fmt.Println("The biggest integer in islice is :", m) _, m = Max(floatslice) fmt.Println("The biggest float in fslice is :", m) _, m = Max(group) fmt.Println("The oldest person in the group is:", m)}
3、類型斷言
interface{}可用於向函數傳遞任意類型的變數,但對於函數內部,該變數仍然為interface{}類型(空介面類型),而不是傳入的實參類型。
介面類型向普通類型的轉換稱為類型斷言(運行期確定)。
func printArray(arr interface{}){ //arr是空介面,不是數群組類型,報錯 for _,v:=range arr{ fmt.Print(v," ") } fmt.Println()}
可以通過類型斷言將介面類型轉換為切片類型。
func printArray(arr interface{}){ //通過斷言實作類別型轉換 a,_ := arr.([]int) for _,v:=range a{ fmt.Println(v, " ") } fmt.Println()}
在使用類型斷言時,最好判斷斷言是否成功。
b,ok := a.(T)if ok{...}
宣告失敗在編譯階段不會報錯,因此,如果不對斷言結果進行判斷將可能會宣告失敗導致運行錯誤。
不同類型變數的運算必須進行顯式的類型轉換,否者結果可能會溢出,導致出錯。
類型斷言也可以配合switch語句進行判斷。
var t interface{}t = functionOfSomeType()switch t := t.(type) {default: fmt.Printf("unexpected type %T", t) // %T prints whatever type t hascase bool: fmt.Printf("boolean %t\n", t) // t has type boolcase int: fmt.Printf("integer %d\n", t) // t has type intcase *bool: fmt.Printf("pointer to boolean %t\n", *t) // t has type *boolcase *int: fmt.Printf("pointer to integer %d\n", *t) // t has type *int}
四、反射機制
Go語言實現了反射,所謂反射就是能檢查程式在運行時的狀態。
reflect包實現了運行時反射,允許程式操作任意類型的對象。
1、擷取Value、Type對象
將變數轉化成reflect對象(reflect.Type或者reflect.Value)
t := reflect.TypeOf(i) //reflect.Type對象v := reflect.ValueOf(i) //reflect.Value對象
調用reflect.TypeOf(x),x首先儲存在一個空介面上,然後在作為參數傳遞給TypeOf函數; Reflect.TypeOf函數內部會解析空介面,接收類型資訊。
同理,reflect.ValueOf函數內部會接收到一個value資訊。
2、擷取對象或者變數的類型
Value.Type()和Value.Kind()方法都可以擷取對象或者變數的類型,如果是變數的話,擷取到的類型都相同;如果是結構體對象,Value.Type()返回結構體的名稱,Value.Kind()返回“struct”;如果是自訂類型,Value.Type()返回自訂類型名稱,Value.Kind()返回自訂類型的底層儲存類型。因此,Value.Kind()可以用於判斷變數是否是結構體。
Kind()描述的是reflection對象的底層類型,而不是靜態類型。假如一個reflection對像包含了一個使用者自訂的靜態類型,Kind()方法返回的是底層資料類型,而不是自訂靜態類型。
package mainimport ( "reflect" "fmt")type Float float64type Person struct { name string age int}func main() { var x1 int = 8 value1 := reflect.ValueOf(x1) fmt.Println(value1.Type())//int fmt.Println(value1.Kind())//int var x Float = 3.14 value2 := reflect.ValueOf(x) fmt.Println(value2.Type())//Float fmt.Println(value2.Kind())//float64 person := Person{} value3 := reflect.ValueOf(person) fmt.Println(value3.Type())//Person fmt.Println(value3.Kind())//struct}
3、擷取變數的值和給變數賦值
擷取變數的值使用value.Interface()方法,返回一個value的值,類型是interface。給變數賦值需要先判斷變數的類型,可以使用Value.Kind()方法,如果變數的類型是reflect.Int,使用Value.SetInt()方法給變數賦值。
如果要修改reflection對象的值,reflection對象的值必須是可settable的。
Settability(可設定)是reflection Value的一個屬性, 並不是所有的reflection Values都擁有Settability屬性。Settability屬性工作表示reflection對象是否可以修改建立reflection對象的實際值,可設定取決於reflection對象所持有的原始值。
調用reflect.ValueOf(x)時,x作為參數傳遞時,首先拷貝x,reflect.ValueOf函數中的interface值是x的拷貝,而不是x本身。如果想要通過reflection修改x, 必須傳遞一個x指標,擷取reflect.Value指標指向的對象,使用reflect.ValueOf(&x).Elem()。
package mainimport ( "reflect" "fmt")type Float float64type Human struct { name string Age uint8}func main() { var x1 int = 8 value1 := reflect.ValueOf(x1) fmt.Println(value1.Type())//int fmt.Println(value1.Kind())//int var x Float = 3.14 //擷取reflect.Value對象,屬性Settability為false value2 := reflect.ValueOf(x) fmt.Println(value2.Type())//Float fmt.Println(value2.Kind())//float64 if value2.Kind() ==reflect.Float64{ if value2.CanSet(){ value2.SetFloat(3.1415926) } } fmt.Println(value2)//3.14 person := Human{"Bauer",30} //擷取reflect.Value指標指向的對象,屬性Settability為true value3 := reflect.ValueOf(&person).Elem() fmt.Println(value3.Type())//Person fmt.Println(value3.Kind())//struct fmt.Println(value3)//{Bauer 30} field0 := value3.FieldByName("name") if field0.Kind() == reflect.String{ if field0.CanSet(){//私人成員不可設定 field0.SetString("Bob") } } fmt.Println(value3)//{Bauer 30} field1 := value3.FieldByName("Age") if field1.Kind() == reflect.Uint8{ if field1.CanSet(){//公有成員可設定 field1.SetUint(20) } } fmt.Println(value3)//{Bauer 20}}
對於結構體,只有公有的成員變數可以被reflect改變值,私人的變數是無法改變值的。
4、擷取結構體成員變數的tag資訊
由於golang變數大小寫和公有私人許可權相關,開發人員很難按照自己的意願來定義變數名,因此golang提供了tag機制,用於給變數提供一個標籤,標籤可以作為一個別名,來給一些儲存結構來擷取結構體變數名字使用。
type Person struct { name string `Country:"CN"` age uint8}bob := Person{"Bob", 30}v := reflect.ValueOf(bob)vt := v.Type()filed,_ := vt.FieldByName("name")fmt.Println(filed.Tag.Get("Country"))//CN
五、介面的組合
1、結構體嵌入類型
結構體類型可以包含匿名或者嵌入欄位。當嵌入一個類型到結構體中時,嵌入類型的名字充當了嵌入欄位的欄位名。
package mainimport "fmt"type User struct { Name string EMail string}type Admin struct { User Level string}func (user *User)Notify() error{ fmt.Printf("User: Sending a Email to %s<%s>\n", user.Name,user.EMail) return nil}func main() { admin := &Admin{ User: User{ Name: "Bauer", EMail: "bauer@gmail.com", }, Level: "super", } admin.Notify() admin.User.Notify()}
當嵌入一個類型,嵌入類型的方法就變成了外部類型的方法,但是當嵌入類型的方法被調用時,方法的接受者是內部類型(嵌入類型),而非外部類型。
嵌入類型的名字充當著欄位名,同時嵌入類型作為內部類型存在,可以使用以下方式的調用方法:
admin.User.Notify()
上述代碼通過類型名稱來訪問內部類型的欄位和方法。內部類型的欄位和方法也同樣被提升到了外部類型,因此可以使用以下方式調用方法:
admin.Notify()
通過外部類型來調用Notify方法,本質上是內部類型的方法。
2、Go語言的方法提升
Go語言中內部類型方法集提升的規則如下:
A、如果S包含一個匿名欄位T,S和S的方法集都包含接收者為T的方法提升。
當嵌入一個類型,嵌入類型的接收者為實值型別的方法將被提升,可以被外部類型的值和指標調用。
B、對於S類型的方法集包含接收者為T的方法提升
當嵌入一個類型,可以被外部類型的指標調用的方法集只有嵌入類型的接收者為指標類型的方法集,即當外部類型使用指標調用內部類型的方法時,只有接收者為指標類型的內部類型方法集將被提升。
C、如果S包含一個匿名欄位T,S和S的方法集都包含接收者為T或者T 的方法提升
當嵌入一個類型的指標,嵌入類型的接收者為實值型別或指標類型的方法將被提升,可以被外部類型的值或者指標調用。
D、如果S包含一個匿名欄位T,S的方法集不包含接收者為*T的方法提升。
根據Go語言規範裡方法提升中的三條規則推匯出的規則。當嵌入一個類型,嵌入類型的接收者為指標的方法將不能被外部類型的值訪問。
3、介面的組合
GO語言中可以通過介面的組合,建立新的介面,新的介面預設繼承組合的介面的抽象方法。
package mainimport "fmt"type IReader interface { Read(file string) []byte}type IWriter interface { Write(file string, data string)}// 介面組合,預設繼承了IReader和IWriter中的抽象方法type IReadWriter interface { IReader IWriter}type ReadWriter struct {}func (rw *ReadWriter) Read(file string) []byte { fmt.Println(file) return nil}func (rw *ReadWriter) Write(file string, data string) { fmt.Printf("filename:%s, contents:%s",file,data)}func main() { readwriter := new(ReadWriter) var irw IReadWriter = readwriter // ok irw.Read("abc.txt") data := "hello world." irw.Write("abc.txt",data)}