1. 程式人生 > >Go語言開發(五)、Go語言面向接口

Go語言開發(五)、Go語言面向接口

table ons 推導 arr ado default 被調用 等等 相關

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 main

import "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 main

import "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 main

import "fmt"

//Person類型
type Person struct{
   name string
   age int
}

//切片類型
type IntSlice []int
type FloatSlice []float32
type 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 has
case bool:
   fmt.Printf("boolean %t\n", t)             // t has type bool
case int:
   fmt.Printf("integer %d\n", t)             // t has type int
case *bool:
   fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *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 main

import (
   "reflect"
   "fmt"
)
type Float float64

type 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 main

import (
   "reflect"
   "fmt"
)
type Float float64

type 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 main

import "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: "[email protected]",
      },
      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 main

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

Go語言開發(五)、Go語言面向接口