go 语言如何用接口实现多态?本文做了详细介绍。

1. 其他语言的接口

除Go以外的其他语言,为了实现一个接口,必须显式从该接口继承:

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

2. 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. 实现接口

FileIFile
 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
*MyBookGetBookName()ChangeBookName()IBook
// 值传递: 不修改数据成员
 func (mb MyBook) GetBookName() (name string) {
     return mb.Name
 }
 
 // 以下是Go编译器根据上述值传递版本的方法成员自动生成的
 func (mb *MyBook) GetBookName() (name string) {
     return (*mb).Name
 }
MyBookChangeBookName()ChangeBookName()BookNameMyBookIBook

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. 类型查询

interface{}
interface{}
 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.Writerio.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
 }