首頁  >  文章  >  後端開發  >  深析golang中interface接口

深析golang中interface接口

藏色散人
藏色散人轉載
2021-01-28 17:36:451950瀏覽

下面由golang教程欄位給大家深析golang中interface接口,希望對需要的朋友有所幫助!

一介面介紹

如果說gorountine和channel是支撐起Go語言的並發模型的基石,讓Go語言在如今集群化與多核心化的時代成為一道亮麗的風景,那麼介面就是Go語言整個類型系列的基石,讓Go語言在基礎程式設計哲學的探索上達到前所未有的高度。 Go語言在程式設計哲學上是變革派,而不是改良派。這不是因為Go語言有gorountine和channel,而更重要的是因為Go語言的型別系統,更是因為Go語言的介面。 Go語言的程式設計哲學因為有介面而趨於完美。 C ,Java 使用"侵入式"接口,主要表現在實現類別需要明確聲明自己實現了某個接口。這種強制性的介面繼承方式是物件導向程式設計思想發展過程中一個遭受相當多質疑的特性。 Go語言採用的是「非侵入式介面",Go語言的介面有其獨到之處:只要類型T的公開方法完全滿足介面I的要求,就可以把類型T的物件用在需要介面I的地方,所謂類型T的公開方法完全滿足介面I的要求,也即是類型T實作了介面I所規定的一組成員。這種做法的學名叫做Structural Typing,有人也把它看作是一種靜態的Duck Typing。

要這個值實作了介面的方法。

type Reader interface { 
 Read(p []byte) (n int, err os.Error) 
} 
 
// Writer 是包裹了基础 Write 方法的接口。 
type Writer interface { 
 Write(p []byte) (n int, err os.Error) 
} 
 
var r io.Reader 
r = os.Stdin 
r = bufio.NewReader(r) 
r = new(bytes.Buffer)

有一個事情是一定要明確的,不論r 保存了什麼值,r 的型別總是io.Reader ,Go 是靜態型別,而r 的靜態型別是io .Reader。介面類型的一個極端重要的例子是空介面interface{},它表示空的方法集合,由於任何值都有零個或多個方法,所以任何值都可以滿足它。也有人說 Go 的介面是動態型別的,不過這是一種誤解。它們是靜態類型的:接口類型的變數總是有著相同的靜態類型,這個值總是滿足空接口,只是儲存在接口變數中的值運行時可能會改變。對於所有這些都必須嚴謹的對待,因為反射和介面密切相關。

二  介面類型記憶體佈局

#在型別中有一個重要的類別就是介面類型,表達了固定的一個方法集合。一個介面變數可以儲存任意實際值(非介面),只要這個值實作了介面的方法。 interface在記憶體上實際由兩個成員組成,如下圖,tab指向虛表,data則指向實際引用的資料。虛擬表描繪了實際的類型資訊及該介面所需的方法集。

type Stringer interface { 
 String() string 
} 
 
type Binary uint64 
 
func (i Binary) String() string { 
 return strconv.FormatUint(i.Get(), 2) 
} 
 
func (i Binary) Get() uint64 { 
 return uint64(i) 
} 
 
func main() { 
 var b Binary = 32 
 s := Stringer(b) 
 fmt.Print(s.String()) 
}

  

觀察itable的結構,首先是描述type資訊的一些元數據,然後是滿足Stringger介面的函數指標列表(注意,這裡不是實際類型Binary的函數指標集合哦)。因此我們如果透過介面進行函數調用,實際的操作其實就是s.tab->fun[0](s.data) 。是不是跟C 的虛表很像?但是他們有本質上的差別。先看C ,它為每個類別創建了一個方法集即虛表,當子類別重寫父類別的虛擬函數時,就將表中的對應函數指標改為子類別自己實現的函數,如果沒有則指向父類的實現,當面臨多重繼承時,C 物件結構裡就會存在多個虛表指針,每個虛表指針指向該方法集的不同部分。我們再來看golang的實作方式,和C 一樣,golang也為每種類型建立了一個方法集,不同的是介面的虛表是在執行時專門產生的,而c 的虛表是在編譯時產生的(但是c 虛函數表表現出的多態是在運行時決定的).例如,當例子中當首次遇見s := Stringer(b)這樣的語句時,golang會產生Stringer接口對應於Binary類型的虛擬表,並將其快取。那為什麼go不採用c 的方式來實現呢?這根c 和golang的物件記憶體佈局是有關係的。

首先c++的动态多态是以继承为基础的,在对象构造初始化的时首先会初始化父类,其次是子类,也就是说一个对象的内存布局是虚表,父类部分,子类部分(编译器不同可能会有差异),当一个父类指针指向子类时,会发生内存的截断,截断子类部分(内存地址偏移),但是此时子类的虚表中的函数指针实际上还是指向了自己的实现,所以此时的指针才会调用到子类的虚函数,如果不是虚函数,因为内存已经截断没有子类的非虚函数信息了,所以只能调用父类的了,这种继承关系让c++的虚表的初始化非常清晰,在一个对象初始化时先调用父类的构造此时虚表跟父类是一样的,接下来初始化子类,此时编译器就会去识别子类有没有覆盖父类的虚函数,如果有则虚表中相应的函数指针改成自己的虚函数实现指针。

那么go有什么不同呢,首先我们很清楚go是没有严格意义上的继承的,go的接口不存在继承关系,只要实现了接口定义的方法都可以成为接口类型,这给go的虚表初始化带来很大的麻烦,到底有多少类型实现了这个接口,一个类型到底实现了多少接口这让编译器很confused。举个例子,某个类型有m个方法,某接口有n个方法,则很容易知道这种判定的时间复杂度为O(mXn),不过可以使用预先排序的方式进行优化,实际的时间复杂度为O(m+n)这样看来其实还行那为什么要在运行时生成虚表呢,这不是会拖慢程序的运行速度吗,注意我们这里是某个类型,某个接口,是1对1的关系,如果有n个类型,n个接口呢,编译器难道要把之间所有的关系都理清吗?退一步说就算编译器任劳任怨把这事干了,可是你在写过程中你本来就不想实现那个接口,而你无意中给这个类型实现的方法中包含了某些接口的方法,你根本不需要这个接口(况且go的接口机制会导致很多这种无意义的接口实现),你欺负编译器就行了,这也太欺负人了吧。如果我们放到运行时呢,我们只要在需要接口的去分析一下类型是否实现了接口的所有方法就行了很简单的一件事。

三 空接口

接口类型的一个极端重要的例子是空接口:interface{} ,它表示空的方法集合,由于任何值都有零个或者多个方法,所以任何值都可以满足它。 注意,[]T不能直接赋值给[]interface{}

//t := []int{1, 2, 3, 4} wrong 
//var s []interface{} = t 
t := []int{1, 2, 3, 4} //right 
s := make([]interface{}, len(t)) 
for i, v := range t { 
 s[i] = v 
}
str, ok := value.(string) 
if ok { 
 fmt.Printf("string value is: %q\n", str) 
} else { 
 fmt.Printf("value is not a string\n") 
}

在Go语言中,我们可以使用type switch语句查询接口变量的真实数据类型,语法如下:

type Stringer interface { 
  String() string 
} 
 
var value interface{} // Value provided by caller. 
switch str := value.(type) { 
case string: 
  return str //type of str is string 
case Stringer: //type of str is Stringer 
  return str.String() 
}

也可以使用“comma, ok”的习惯用法来安全地测试值是否为一个字符串:

str, ok := value.(string) 
if ok { 
  fmt.Printf("string value is: %q\n", str) 
} else { 
  fmt.Printf("value is not a string\n") 
}

四 接口赋值

package main 
 
import ( 
"fmt" 
) 
 
type LesssAdder interface { 
  Less(b Integer) bool 
  Add(b Integer) 
} 
 
type Integer int 
 
func (a Integer) Less(b Integer) bool { 
  return a < b 
} 
 
func (a *Integer) Add(b Integer) { 
  *a += b 
} 
 
func main() { 
 
  var a Integer = 1 
  var b LesssAdder = &a 
  fmt.Println(b) 
 
  //var c LesssAdder = a 
  //Error:Integer does not implement LesssAdder  
  //(Add method has pointer receiver) 
}

go语言可以根据下面的函数:

func (a Integer) Less(b Integer) bool

自动生成一个新的Less()方法

func (a *Integer) Less(b Integer) bool

这样,类型*Integer就既存在Less()方法,也存在Add()方法,满足LessAdder接口。 而根据

func (a *Integer) Add(b Integer)

这个函数无法生成以下成员方法:

func(a Integer) Add(b Integer) { 
  (&a).Add(b) 
}

因为(&a).Add()改变的只是函数参数a,对外部实际要操作的对象并无影响(值传递),这不符合用户的预期。所以Go语言不会自动为其生成该函数。因此类型Integer只存在Less()方法,缺少Add()方法,不满足LessAddr接口。(可以这样去理解:指针类型的对象函数是可读可写的,非指针类型的对象函数是只读的)将一个接口赋值给另外一个接口 在Go语言中,只要两个接口拥有相同的方法列表(次序不同不要紧),那么它们就等同的,可以相互赋值。 如果A接口的方法列表时接口B的方法列表的子集,那么接口B可以赋值给接口A,但是反过来是不行的,无法通过编译。

五 接口查询

接口查询是否成功,要在运行期才能够确定。他不像接口的赋值,编译器只需要通过静态类型检查即可判断赋值是否可行。

var file1 Writer = ...
if file5,ok := file1.(two.IStream);ok {
...
}

这个if语句检查file1接口指向的对象实例是否实现了two.IStream接口,如果实现了,则执行特定的代码。

在Go语言中,你可以询问它指向的对象是否是某个类型,比如,

var file1 Writer = ...
if file6,ok := file1.(*File);ok {
...
}

这个if语句判断file1接口指向的对象实例是否是*File类型,如果是则执行特定的代码。

slice := make([]int, 0)
slice = append(slice, 1, 2, 3)

var I interface{} = slice


if res, ok := I.([]int);ok {
  fmt.Println(res) //[1 2 3]
}

这个if语句判断接口I所指向的对象是否是[]int类型,如果是的话输出切片中的元素。

func Sort(array interface{}, traveser Traveser) error {

  if array == nil {
    return errors.New("nil pointer")
  }
  var length int //数组的长度
  switch array.(type) {
  case []int:
    length = len(array.([]int))
  case []string:
    length = len(array.([]string))
  case []float32:
    length = len(array.([]float32))

  default:
    return errors.New("error type")
  }

  if length == 0 {
    return errors.New("len is zero.")
  }

  traveser(array)

  return nil
}

通过使用.(type)方法可以利用switch来判断接口存储的类型。

小結: 查詢介面所指向的物件是否為某個類型的這種用法可以認為是介面查詢的一個特例。介面是對一組類型的公共特性的抽象,所以查詢介面與查詢具體類型區別好比是下面這兩句問話的區別:

你是醫生麼?

是。

你是莫莫莫


第一句問話查詢的是一個群體,是查詢介面;而第二個問句已經到了具體的個體,是查詢具體類型。

除此之外利用反射也可以進行類型查詢,會在反射中做詳細介紹。

更多golang相關技術文章,請造訪go語言欄位!

以上是深析golang中interface接口的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:jb51.net。如有侵權,請聯絡admin@php.cn刪除