【导读】go 语言如何用接口实现多态?本文做了详细介绍。
除Go以外的其他语言,为了实现一个接口,必须显式从该接口继承:
interface IFoo {
void Bar();
}
// java
class Foo implements IFoo {
// ...
}
// C++
class Foo : public IFoo {
// ...
}
IFoo* foo = new Foo;
在面向对象领域中,我们一般这么介绍接口:
接口定义一个对象的行为。但是仅仅只定义了对象可以做什么,实现细节交给对象本身去确定。
在Go中,接口就是方法签名method signature
的集合,当一个类型实现了接口的所有方法,我们就称该类型实现了这个接口。
定义一个接口可以进行文件读写:
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
}
我们定义一个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
}
接口赋值在Go中包含两种情况:
我们现在构造一个特殊的例子,意在指出将对象实例赋值给接口时可能出现的错误:
/*
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
接口。
在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
}
在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 {
// ...
}
}
}
}
可以把两个接口组合到一起,比如将io.Reader
和io.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
- EOF -
Go 开发大全
参与维护一个非常全面的Go开源技术资源库。日常分享 Go, 云原生、k8s、Docker和微服务方面的技术文章和行业动态。
关注后获取
回复 Go 获取6万star的Go资源库
分享、点赞和在看
支持我们分享更多好文章,谢谢!