Golang 中的接口與多態

【導讀】go 語言如何用接口實現多態?本文做了詳細介紹。

1. 其他語言的接口

除 Go 以外的其他語言,爲了實現一個接口,必須顯式從該接口繼承:

 interface IFoo {
     void Bar();
 }
 
 // java
 class Foo implements IFoo {
     // ...
 }
 
 // C++
 class Foo : public IFoo {
     // ...
 }
 
 IFoo* foo = new Foo;

2. Go 中的接口

在面向對象領域中,我們一般這麼介紹接口:

接口定義一個對象的行爲。但是僅僅只定義了對象可以做什麼,實現細節交給對象本身去確定。

在 Go 中,接口就是方法簽名method signature的集合,當一個類型實現了接口的所有方法,我們就稱該類型實現了這個接口。

接口使用方法

1. 定義接口

定義一個接口可以進行文件讀寫:

 type IFile interface {
     Read(buf []byte) (n int, err error)
     Write(buf []byte) (n int, err error)
     Seek (off int64, whence int) (pos int64, err error)
     Close() error
 }

2. 實現接口

我們定義一個File類型,只要實現了IFile接口的所有方法就可以認爲實現了該接口:

 type File struct {
 }
 
 func (f *File) Read(buf []byte) (n int, err error) {
     // ...
     return
 }
 
 func (f *File) Write(buf []byte) (n int, err error) {
     // ...
     return
 }
 
 func (f *File) Seek(off int64, whence int) (pos int64, err error) {
     // ...
     return
 }
 
 func (f *File) Close() error {
     // ...
     return nil
 }

3. 接口賦值

接口賦值在 Go 中包含兩種情況:

3.1 將對象實例賦值給接口

我們現在構造一個特殊的例子,意在指出將對象實例賦值給接口時可能出現的錯誤:

 /*
 IBook接口
 */
 type IBook interface {
     // 獲取書籍名稱
     GetBookName() (name string)
     // 修改書籍名稱
     ChangeBookName(newName string)
 }
 
 /*
 定義MyBook類型實現了IBook接口
 */
 type MyBook struct {
     Name string
 }
 
 // 值傳遞: 不修改數據成員
 func (mb MyBook) GetBookName() (name string) {
     return mb.Name
 }
 
 // 指針傳遞: 修改數據成員
 func (mb *MyBook) ChangeBookName(newName string) {
     mb.Name = newName
 }

如果我們要將對象賦值給接口,那麼下述兩種寫法是不同的:

 // 構造MyBook類型的對象實例
 var mb = MyBook{
     Name: "TOMO-CAT",
 }
 
 // 將對象實例賦值給接口
 var book IBook = &mb  // 正確
 var book IBook = mb   // 錯誤

錯誤的語句編譯期間報錯如下:

 ./main.go:10:6: cannot use mb (type MyBook) as type IBook in assignment:
     MyBook does not implement IBook (ChangeBookName method has pointer receiver)
 
 Compilation finished with exit code 2

原因在於 Go 可以根據值傳遞版本的方法成員自動生成指針傳遞版本的方法成員,因此*MyBook類型就既存在GetBookName()方法和ChangeBookName()方法,從而實現了IBook接口。

 // 值傳遞: 不修改數據成員
 func (mb MyBook) GetBookName() (name string) {
     return mb.Name
 }
 
 // 以下是Go編譯器根據上述值傳遞版本的方法成員自動生成的
 func (mb *MyBook) GetBookName() (name string) {
     return (*mb).Name
 }

但是類型MyBook缺少對應的ChangeBookName()方法,原因在於ChangeBookName()方法通過指針傳遞的方式修改了數據成員,即使 Go 語言自動生成了該方法成員的值傳遞版本也不會對實際操作的對象產生影響(即沒法修改對象的BookName)。因此MyBook類型不能複製給IBook接口。

3.2 將一個接口賦值給另一個接口

在 Go 語言中只要兩個接口有相同的方法集合,那麼就可以相互賦值(在 Go 中認爲這兩個接口是等價的)。舉個例子,下述兩個接口定義在不同的包中,但是定義相同的方法集合:

 package foo
 
 type IFoo interface {
     Read(buf []byte) (n int, err error)
     Write(buf []byte) (n int, err error)
 }
 package bar
 
 type IBar interface {
     Read(buf []byte) (n int, err error)
     Write(buf []byte) (n int, err error)
 }

在 Go 語言中這兩個接口是等價的,可以互相賦值:

 func main() {
     var file1 foo.IFoo
     var file2 bar.IBar
     file1 = file2
     file2 = file1
 
     return
 }

另外接口賦值並不要求兩個接口完全等價,如果接口 A 方法集合是接口 B 方法集合的子集,那麼接口 B 可以賦值給接口 A,反之不成立:

 type Writer interface {
     Write(buf []byte) (n int, err error)
 }
 
 func main() {
     var file1 foo.IFoo
     var file3 Writer
 
     file3 = file1  // 正確
     file1 = file3  // 錯誤
 
     return
 }

4. 類型查詢

在 Go 語言中我們可以查詢接口指向對象實例的類型,比如常見的空接口interface{}類型查詢:

由於 Go 語言中任何對象實例都滿足空接口interface{},因此可以認爲空接口是指向任何對象的 Any 類型。

 type Stringer interface {
     String() string
 }
 
 // Println的簡單實現
 func Println(args ...interface{}) {
     for _, arg := range args {
         switch v := arg.(type) {
         // 查詢是否是int, string等內置類型
         case int:
             // ...
         case string:
             // ...
         default:
             // 查詢是否實現了某接口
             if v, ok := arg.(Stringer); ok {
                 val := v.String()
                 // ...
             } else {
                 // ...
             }
         }
     }
 }

5. 接口組合

可以把兩個接口組合到一起,比如將io.Readerio.Writer組合成io.ReadWriter

 type ReadWriter interface {
     Reader
     Writer
 }

接口實現多態

 /*
 IBook接口
 */
 type IBook interface {
     // 獲取書籍名稱
     GetBookName() (name string)
 }
 
 /*
 定義MyBook類型實現了IBook接口
 */
 type MyBook struct {
     Name string
 }
 
 func (mb MyBook) GetBookName() (name string) {
     return mb.Name
 }
 
 type TestBook struct {
     Name string
 }
 
 func (tb TestBook) GetBookName() (name string) {
     return "TestBook"
 }
 
 func main() {
     book1 := MyBook{
         Name: "TOMOCAT",
     }
     book2 := TestBook{}
     bookList := []IBook{book1, book2}
 
     for _, book := range bookList {
         fmt.Println(book.GetBookName())
     }

     return
 }

轉自:

zhuanlan.zhihu.com/p/350902249

Go 開發大全

參與維護一個非常全面的 Go 開源技術資源庫。日常分享 Go, 雲原生、k8s、Docker 和微服務方面的技術文章和行業動態。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/MP7NQb2WVaGL8CLrYyi2Kg