golang中interface的Q&A
阿新 • • 發佈:2022-03-07
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