1. 程式人生 > 其它 >golang中interface的Q&A

golang中interface的Q&A

interface Q&A

Go介面與C++介面有何異同?

1. 介面定義了一種規範,描述了類的行為和功能,而不做具體實現
2. C++定義的介面稱為侵入式,而go中的介面為非侵入式,不需要顯示宣告,只需要實現介面定義的函式,編譯器會自動識別
案例
type Animal interface {
	Run()
	Say()
}

type Dog struct {}
func (d *Dog) Run() {
	fmt.Println("dog run")
}
func (d *Dog) Say() {
	fmt.Println("dog say")
}
func (d *Dog) Sing() {
	fmt.Println("dog sing")
}

func main() {
	var a Animal
	a = &Dog{}
	a.Run()
	a.Say()
	v := a.(*Dog)
	fmt.Println(v, reflect.TypeOf(v))  // &{} *main.Dog
}
3. go通過itab中的fun欄位來實現介面變數呼叫實體型別的函式

go語言與鴨子型別的關係

1. 鴨子型別是動態程式語言的一種物件推斷策略,它更關注物件能如何被使用,而不是物件的型別本身
2. go語言作為靜態型別語言,通過介面的方式完美的支援了鴨子型別
3. 在靜態語言java、c++中,必須顯示的宣告實現了某個介面,之後,才能用在任何需要這個介面的地方
def hello_world(coder):
    coder.say_hello()
4. 如果你在程式中呼叫say_hello()方法,卻傳入了一個根本沒有實現該方法的型別,那麼在編譯階段就不會通過
    這也是靜態型別語言比動態型別語言更安全的原因
5. go語言作為一門現代靜態語言,是有後發優勢的,它引入了動態語言的便利,同時又擁有靜態語言的型別檢查
    go不要求型別顯示的宣告實現了某個介面,只要實現了介面中的所有方法即可
6. go作為一門靜態語言,通過介面實現了鴨子型別,實際上是go編譯器在其中做了隱匿的轉換工作

iface和eface的區別是什麼?

1. iface和eface都是go中描述介面的底層結構體,區別在於iface描述的介面包含方法,而eface則是不包含任何方法的空介面
2. 從原始碼層面看一下
type iface struct {
    tab  *itab
    data unsafe.Pointer
}
type itab struct {
    inter  *interfacetype
    _type  *_type
    link   *itab
    hash   uint32 // copy of _type.hash. Used for type switches.
    bad    bool   // type does not implement interface
    inhash bool   // has this itab been added to hash?
    unused [2]byte
    fun    [1]uintptr // variable sized
}
3. iface內部維護兩個指標, tab指向itab實體,它表示介面的型別以及賦值給這個介面的實體型別
    data則指向介面具體的值,一般而言是一個指向堆記憶體的指標
4. 在來看一下itab結構體,_type欄位描述了實體的型別,包括記憶體對齊、大小等;inner欄位描述了介面的型別,
    fun欄位放置和介面方法對應的具體資料型別的方法地址,實現介面呼叫方法的動態分配
    一般在每次給介面賦值發生轉換時會更新此表,或者直接拿快取的itab
5. itab裡面只會列出例項的型別和介面中的方法,實體的其它方法不會在這裡列出,為什麼fun陣列的大小為1
    實際上這裡儲存的是第一個函式方法的指標,如果有更多的方法,在它之後繼續儲存,通過增加地址就可以獲得這些函式指標
6. 再來看看interfacetype型別
type interfacetype struct {
    typ     _type
    pkgpath name
    mhdr    []imethod
}
可以看到,它包裝了 _type 型別,_type 實際上是描述 Go 語言中各種資料型別的結構體。
我們注意到,這裡還包含一個 mhdr 欄位,表示介面所定義的函式列表, pkgpath 記錄定義了介面的包名。
7. 接下來看一下eface的原始碼
type eface struct {
    _type *_type
    data  unsafe.Pointer
}
_type表示空介面所承載的具體的實體型別,data描述了具體的值
8. go語言各種資料型別都是在_type結構體的基礎上,增加一些額外的欄位來進行管理的
    這些資料型別的結構體定義,是實現反射的基礎

值接收者和指標接受者的區別?

方法
1. 方法能給使用者自定義的型別新增新的行為, 它和函式的區別在於方法有一個接收者,
    給一個函式新增一個接收者,那麼它就變成了方法,接收者可以是值接收者,也可以是指標接受者
2. 在呼叫方法的時候,值型別既可以呼叫值接收者的方法,也可以呼叫指標接受者的方法
    指標型別既可以呼叫指標接受者的方法,也可以呼叫值接收者的方法
3. 總結一句話,不管方法的接收者是什麼型別, 該型別的值和指標都可以呼叫,不必嚴格符合接收者的型別
案例:
type Person struct {
	age int
}
func (p Person) howOld() int {
	return p.age
}
func (p *Person) growUp() int {
	return p.age + 1
}

func main() {
	// 值型別
	p := Person{age: 10}
	fmt.Println(p.howOld())
	fmt.Println(p.growUp())

	// 指標型別
	ptr := &Person{age: 100}
	fmt.Println(ptr.howOld())
	fmt.Println(ptr.growUp())
}
4. 呼叫了growUp函式後,不管呼叫者是值型別還是指標型別都可以呼叫成功,age值都會被改變了
5. 實際上當型別和方法的接收者型別不同是,編譯器在背後會做轉換的
-              值接收者                             指標接收者
值型別呼叫者     方法會使用呼叫者的副本,類似於"傳值"     使用值的指標來呼叫方法,比如p.growUp2()會轉換為:(&p).growUp2(), 方法會使用呼叫者的指標的副本
指標型別呼叫者    指標被解為值,比如(*ptr).growUp1()    實際上也是傳值,方法的操作會影響到呼叫者,類似於指標傳參,拷貝了一份指標
                同時方法會使用呼叫者的副本,類似於傳值
值接收者和指標接收者
1. 前面說過不管接收者是值型別還是指標型別,都可以通過值型別或指標型別進行呼叫
    這裡面實際上通過語法糖起作用的,
2. 先說結論:實現了接收者是值型別的方法,會自動實現一個接收者是指標型別的方法
    而實現了接收者是指標型別的方法,不會自動實現對應的接收者是值型別的方法
案例:
type coder interface {
	code()
	debug()
}
type Gopher struct {
	language string
}
func (g Gopher) code() {
	fmt.Printf("I am coding %s\n", g.language)
}
// 非常重要:實現了接收者是值型別的方法會自動實現接收者是指標型別的方法
// 實現了接收者是指標型別的方法,不會自動實現接收者是值型別的方法
func (g *Gopher) debug() {
	fmt.Printf("I am debuging %s\n", g.language)
}

func main() {
	// 實現了接收者是值型別的方法,會自動實現接收者是指標型別的方法
	// 上面雖然只實現了值型別的code()方法,但是通過指標型別去呼叫也可以
	var c coder = &Gopher{language: "GO"}
	c.code()
	c.debug()
	
	// goland直接提示錯誤
	// 無法將 'Gopher{language: "python"}' (型別 Gopher) 用作型別 coder型別未實現 'coder',
	// 因為 'debug' 方法有指標接收器
	// 原因:實現接收者是指標型別的方法,不會自動實現接收者是值型別的方法
	var cc coder = Gopher{language: "python"}
	cc.code()
	cc.debug()
}
* 重點:實現了接收者是值型別的方法,會自動實現接收者是指標型別的方法
* 實現了接收者是指標型別的方法,不會自動實現接收者是值型別的方法
3. 上面的說法有一個簡單解釋,接收者是指標型別的方法,很可能在方法中對接收者的屬性進行更改操作
    從而影響呼叫者,而對於接收者是值型別的方法,即使在方法內部修改接收者的屬性,也不會影響呼叫者
4. 所以當實現了接收者是值型別的方法時,會自動生成接收者是指標型別的方法,因為兩者的方法內部都不會影響呼叫者
    但是當實現了接收者是指標型別的方法時,如果此時自動生成一個接收者是值型別的方法時,
    原本期望對接收者的改變通過指標實現,現在無法實現,因為值型別會產生一個拷貝,不會真正應用呼叫者
5. 最後只要記住一句話:如果實現了接收者是值型別的方法時,會隱含的實現接收者是指標型別的方法
兩者分別在何時使用
1. 如果方法的接收者是值型別,無論呼叫者是值型別還是指標型別,方法內部修改的只是副本,不會影響呼叫者
2. 如果方法的接收者是指標型別,必須通過指標型別進行呼叫,不能通過值型別進行呼叫,而且方法內部修改後,呼叫者也會受到影響
3. 使用指標型別作為方法接收者的理由:
    1. 方法內部能夠修改接收者指向的值
    2. 避免每次呼叫時複製該值,在值的型別為大型結構體時,這樣做更加的高效
4. 是使用值接收者還是指標接受者,不應該由方法內部是否修改了呼叫者(接收者)而決定,而是應該基於該型別的本質
5. 如果型別具備原始的本質,也就是說它的成員是由go語言內建的原始型別,如整型、字串,那就定義值接收者型別的方法
    像內建的引用型別如:slice、map、interface、channel這些型別比較特殊,宣告她們的時候實際上是建立了一個header
    對於她們也是直接定義值接收者型別的方法,這樣,呼叫函式是,是直接copy了這些型別的header,
    而header本身就是為複製而設計的
6. 如果型別具備非原始的本質,不能被安全的賦值,這種型別應該總是被共享,那就定義指標型別的方法

如果用interface實現多型

1. go語言並沒有設計整合、多重繼承等概念,但是它通過介面優雅的實現了面向物件的特性
2. 多型是一種執行期的行為,有一下幾個特點
    * 一種型別具有多種型別的能力
    * 允許不同物件對同一訊息做出靈活的反應
    * 以一種通用的方式對待每個使用的物件
    * 非動態語言必須通過繼承或介面的方式來實現
案例:
func whatJob(p Person) {
	p.job()
}
func growUp(p Person) {
	p.growUp()
}

type Person interface {
	job()
	growUp()
}

type Student struct {
	age int
}
// 由下面兩句哈總結:Student{}沒有實現Person介面,而&Student{}實現了Person介面
func (s Student) job() {  // 接收者實現了值型別的方法會自動實現接收者是指標型別的方法
	fmt.Println("i am a student")
}
func (s *Student) growUp() {  // 接收者實現了指標型別的方法,不會自動實現接收者是值型別的方法
	s.age += 1
}

type Programmer struct {
	age int
}
func (p Programmer) job() {
	fmt.Println("I am a programmer")
}
func (p Programmer) growUp() {
	p.age += 1
}

func main() {
	s := Student{age: 10}
	whatJob(&s)
	growUp(&s)
	fmt.Println(s) // {11} 方法內部修改了呼叫者的值,因為方法實現的是接收者指標型別

	p := Programmer{age: 18}  // 方法內部修改未影響呼叫者的值,因為方法實現的是接收者值型別
	whatJob(p)
	growUp(&p)  // 此處即使傳遞指標型別,編譯器內部也會將其改成值型別去呼叫接收者值型別的方法即: (*(&p)).growUp()
	fmt.Println(p)

}
3. 上面定義了兩個結構體,Student、Programmer,同時*Student和Programmer實現了Person介面中定義的兩個函式
    注意*Student實現了Person介面,而Student型別沒有
4. main函式裡建立了Student和Programmer物件,在將她們分別傳入whatJob和growUp函式,
    函式中直接呼叫介面函式,實際執行的時候是看最終傳入的實體型別是什麼,呼叫的是實體型別實現的函式
    於是不同物件針對同一訊息就由多種表現,多型就實現了
5. 在深入一點來說的話,在函式whatJob或者growUp中,介面person綁定了實體型別*Student或Programmer,
    根據前面的分析iface原始碼,這裡會直接呼叫iface結構體中的tab指標指向的itab結構體中的fun陣列中儲存的函式,
    而因為fun數組裡儲存的是實體型別實現的函式,所以當函式傳入不同實體型別是,呼叫的是不同實體的不同函式實現
    從而實現多型

介面的動態型別和動態值

1. 從原始碼裡可以看到iface包含兩個欄位,tab指標指向itab結構體(包含型別資訊),data指標指向具體的資料
    它們分別被稱為動態型別和動態值,而介面值包括動態型別和動態值
2. 引申1,介面型別和nil比較,介面值的零值是指動態型別和動態值都為nil, 
    當僅且當動態型別和動態值這兩部分都為nil的時候,這個介面值才會被認為:介面值 == nil
案例:
type Coder interface {
	code()
}
type Gopher struct {
	name string
}
func (g Gopher) code() {
	fmt.Printf("%s is coding\n", g.name)
}

func main() {
	var c Coder
	// 此時介面變數的動態型別和動態值都為nil,所以此時 介面值 == nil
	fmt.Printf("c: %T, %v\n", c, c)  // c: nil, nil
	fmt.Println(c == nil)  // true

	var g *Gopher
	fmt.Println(g == nil)  // true

	c = g
	// 將Gopher的指標型別賦值給介面變數c,此時介面值的動態型別不是nil了,而是*Gopher,雖然它的動態值是nil,
	// 注意:雖然方法只實現了接收者值型別,但是會自動生成接收者指標型別的方法
	fmt.Printf("c: %T, %v\n", c, c)  // c: *main.Gopher, <nil>
	fmt.Println(c == nil)  // false

}
3. 開始c的動態型別和動態值都為nil, g的值也為nil, 當把g賦值給c的時候,c的動態型別為 *main.Gopher,
    動態值為nil, 此時 c 就不在等於nil了
4. 引申2,再來看個例子,看下輸出
// 定義一個結構體,同時實現了Error函式,也就實現了error介面
type MyError struct {}
func (m MyError) Error() string {
	return "MyError"
}

func Process() error {
	var err *MyError  // 宣告指標型別,未進行初始化, 所以就是一個空指標
	return err
}

func main() {
	err := Process()
	// err介面值:動態型別是:*MyError, 動態值是nil
	fmt.Println(err)  // nil
	fmt.Println(err == nil)  // false

	// 打印出介面的動態型別和動態值
	fmt.Printf("%T:%v\n", err, err)
}
5. 這裡先定義了一個MyError結構體,實現了Error()函式,同時也就實現了error介面
    然後定義了一個Process()函式,返回一個error介面型別,這塊隱含了型別轉換
    雖然返回的值是nil, 但是它的型別是*MyError, 最後和nil比較的時候,結果為false
案例:(與上面的有一點不同)
// 定義一個結構體,同時實現了Error函式,也就實現了error介面
type MyError struct {}
func (m MyError) Error() string {
	return "MyError"
}

func Process() error {
	var err *MyError = &MyError{}  // 宣告指標型別,並進行記憶體初始化
	return err
}

func main() {
	err := Process()
	// err介面值:動態型別是:*MyError, 動態值是MyError
	fmt.Println(err)  // MyError
	fmt.Println(reflect.TypeOf(err), reflect.ValueOf(err))  // *main.MyError MyError
	fmt.Println(err == nil)  // false

	// 打印出介面的動態型別和動態值
	fmt.Printf("%T:%v\n", err, err)  // *main.MyError MyError
}
6. 獲取介面變數的動態型別和動態值的兩種辦法:
    1. fmt.Println(reflect.TypeOf(err), reflect.ValueOf(err))  // *main.MyError MyError
    2. fmt.Printf("%T:%v\n", err, err)

介面轉換的原理

1. 當判定一種型別是否滿足某個介面時,go使用型別的方法集合介面的方法集進行匹配,
    如果型別的方法集完全包含了介面的方法集,那麼就認為該型別實現了該介面
2. 例如某型別有m個方法,某介面有n個方法,則可以很容易的知道時間複雜度為O(mn),
    go會對方法集的函式按照函式名字典序進行排序,所以實際時間複雜度為O(m+n)
3. 這裡我們來探索一下將一個介面轉換給另外一個介面背後的原理,當然能轉換的原因,必然是型別是相容的
type coder interface {
	code()
	run()
}
type runner interface {
	run()
}
type Gopher struct {
	language string
}
func (g Gopher) code() {
	return
}
func (g Gopher) run() {
	return
}

func main() {
	// 如何將一個介面轉換給另外一個介面
	// 介面變數可以一直改變,但是介面型別中的動態型別和動態值是不會改變的
	var c coder
	c = Gopher{language: "go"}

	var r runner
	r = c

	fmt.Println(r, c)  // {go} {go}
	fmt.Println(r.(Gopher).language, c.(Gopher).language)  // go go
}
4. 介面變數賦值給另外一個介面變數時,最核心的就是去看itab結構體中的_type實體型別是否完全實現了interfaceType介面型別中的所有方法
5. 具體型別轉空介面時,_type欄位直接複製原型別的_type, 呼叫mallocgc 獲取一塊新記憶體,把值複製進去,data指向這塊新記憶體
6. 具體型別轉非空介面時,入參tab是在編譯階段生成好的,新介面tab欄位直接指向入參tab指向的itab,
    呼叫mallocgc 獲得一塊新記憶體,把值複製進去,data指向這塊新記憶體
7. 介面轉介面時,itab呼叫getitab獲取,只用生成一次,之後直接從hash表中獲取
* 重點:無論介面如何轉換,介面值的動態型別是不會改變的

型別轉換和斷言的區別?

1. go語言中不允許隱式型別轉換,也就是說 = 兩邊不允許出現型別不一樣的變數
2. 型別轉換和型別斷言本質上都是把一個型別轉換成另外一個型別,不同的是型別斷言是對介面變數就行的操作
型別轉換
1. 對於型別轉換而言,轉換前後的兩個型別要相互相容才行
案例
func main() {
	var i int = 9
	var f float64
	f = float64(i)
	fmt.Printf("%T:%v\n", f, f)

	f = 10.8
	a := int(f)
	fmt.Printf("%T:%v\n", a, a)
}
2. 上面int 和 float64之間相互轉換時成功的,因為它們的型別是相互相容的
斷言
1. 因為空介面 interface{} 沒有定義任何函式,因此go中所有型別都實現了空介面
    當一個函式的形參是interface{}時,那麼在函式中,我們就需要對形參進行斷言,得到它的真實型別
2. 斷言的語法
    1. 安全型別斷言:目標型別值, 布林引數 := 表示式.(目標型別值)
    2. 非安全型別斷言:目標型別值 := 表示式.(目標型別值)
3. 型別轉換和型別斷言有相似之處,不同之處在於型別斷言只針對於介面變數
案例:
type Student struct {
	Name string
	Age int
}

func main() {
	// new()函式用來分配記憶體空間,返回值是*Student並賦值給了一個空介面變數
	var i interface{} = new(Student)
	s := i.(Student)  // 直接panic: interface conversion: interface {} is *main.Student, not main.Student
	fmt.Println(s)  
}
採用安全斷言
func main() {
	// new()函式用來分配記憶體空間,返回值是*Student並賦值給了一個空介面變數
	var i interface{} = new(Student)
	s, ok := i.(*Student)  // 直接panic: interface conversion: interface {} is *main.Student, not main.Student
	if ok{
		fmt.Println(s)
	}
}
4. 斷言其實還有另外一種形式,就是利用switch語句,每一個case都會被順序的考徐,所以case的順序很重要,
    因為很有可能會有多個case匹配的情況
案例
type Student struct {
	Name string
	Age int
}

func main() {
	//var i interface{} = new(Student)
	//var i interface{} = (*Student)(nil)
	var i interface{}

	fmt.Printf("%p:%v\n", &i, i)

	judge(i)

}

func judge(i interface{}) {
	fmt.Printf("%p:%v\n", &i, i)

	switch v := i.(type) {
	case nil:
		fmt.Printf("%p:%v\n", &v, v)
		fmt.Printf("nil type [%T] [%v]\n", v, v)
	case Student:
		fmt.Printf("%p:%v\n", &v, v)
		fmt.Printf("Student type [%T] [%v]\n", v, v)
	case *Student:
		fmt.Printf("%p:%v\n", &v, v)
		fmt.Printf("*Student type [%T] [%v]\n", v, v)
	default:
		fmt.Printf("%p:%v\n", &v, v)
		fmt.Printf("unknow type [%T] [%v]\n", v, v)
	}
}
5. 引申1,fmt.Println()函式的引數時interface型別,對於內建型別,函式內會用窮舉法得出它的真實型別,
    然後轉換為字串列印,對於自定義型別,首先判斷該型別是否實現了String()方法,如果實現了,則直接列印String()方法的輸出結果
    否則會通過反射來遍歷物件的成員進行列印
案例:
type Student struct {
	Name string
	Age  int
}

func main() {
	var s = Student{
		Name: "qcrao",
		Age:  18,
	}
	fmt.Println(s)
}
6. 實現自定義型別的String()方法
type Student struct {
	Name string
	Age  int
}
func (s Student) String() string {
	return fmt.Sprintf("{name: %s, age: %d}", s.Name, s.Age)
}

func main() {
	var s = Student{
		Name: "qcrao",
		Age:  18,
	}
	fmt.Println(s)  // {name: qcrao, age: 18}
	fmt.Println(&s)  // {name: qcrao, age: 18}
}
上面的兩個列印是一致的,原因是實現了方法的值接收者會自動實現方法的指標接受者,所以列印一致,再看看下面的例子
type Student struct {
	Name string
	Age  int
}
func (s *Student) String() string {
	return fmt.Sprintf("{name: %s, age: %d}", s.Name, s.Age)
}

func main() {
	var s = Student{
		Name: "qcrao",
		Age:  18,
	}
	fmt.Println(s)  // {qcrao 18}
	fmt.Println(&s)  // {name: qcrao, age: 18}
}

兩個列印不一致,原因:實現了方法的指標接收者不會自動實現方法的值接收者。

* 核心知識點:型別T只有接收者是T的方法,而型別*T擁有接收者是T和接收者是*T的兩個方法
  語法上T能直接呼叫接收者是*T的方法,僅僅是go的語法糖
	// 語法上Student能直接呼叫*Student的方法,僅僅是因為go的語法糖實現的
	fmt.Println(s.String())

編譯器自動檢查型別是否實現某介面

案例:
type myWriter struct {}
func (m myWriter) Write(p []byte) (n int, err error) {
	return 0, nil
}

func main() {
	// 編譯器自動檢查型別是否實現介面
	// 編譯器會由此檢查 *MyWriter型別是否實現了io.Writer介面
	var a io.Writer = (*myWriter)(nil)
	fmt.Printf("%T:%v\n", a, a)  // *main.myWriter:<nil>

	// 編譯器會由此檢查 *MyWriter型別是否實現了io.Writer介面
	var b io.Writer = new(myWriter)
	fmt.Printf("%T:%v\n", b, b)  // *main.myWriter:&{}

	// 編譯器會由此檢查 myWriter型別是否實現了io.Writer介面
	var c io.Writer = myWriter{}
	fmt.Printf("%T:%v\n", c, c)  // main.myWriter:{}

}
1. 實際上,上述的賦值語句會發生隱式的型別轉換,在轉換的過程中,編譯器會自動檢查
    等號右邊的型別是否實現了等號左邊所規定的函式
* 總結一下:可通過在程式碼中新增如下的程式碼,來檢查型別是否實現了介面
var _ io.Writer = (*myWriter)(nil)  // 動態型別非nil,動態值是nil
var _ io.Writer = myWriter{}  // 動態型別和動態值都非nil
或者下面這種方法  
var _ io.Writer = new(myWriter)  // 動態型別和動態值都非nil