文章目录
Go 中接口是一个使用得非常频繁的特性,好的软件设计往往离不开接口的使用,比如依赖倒置原则(通过抽象出接口,分离了具体实现与实际使用的耦合)。 今天,就让我们来了解一下 Go 中接口的一些基本用法。
概述
Go 中的接口跟我们常见的编程语言(如Java)中的接口不太一样,go 里面实现接口是不需要使用 implements 关键字显式声明的, go 的接口为我们提供了难以置信的一系列的灵活性和抽象性。接口有两个特点:
- 接口本质是一种自定义类型。
- 接口是一种特殊的自定义类型,其中没有数据成员,只有方法(也可以为空)。
go 中的接口定义方式如下:
type Flyable interface {
Fly() string
}
接口是完全抽象的,不能将其实例化。但是我们创建变量的时候可以将其类型声明为接口类型:
var a Flyable
然后,对于接口类型变量,我们可以把任何实现了接口所有方法的类型变量赋值给它,这个过程不需要显式声明。 例如,假如 Bird 实现了 Fly 方法,那么下面的赋值就是合法的:
// Bird 实现了 Flyable 的所有方法
var a Flyable = Bird{}
go 实现接口不需要显式声明。
由此我们引出 go 接口的最重要的特性是:
只要某个类型实现了接口的所有方法,那么我们就说该类型实现了此接口。该类型的值可以赋给该接口的值。
因为 interface{} 没有任何方法,所以任何类型的值都可以赋值给它(类似 Java 中的 Object)
基本使用
Java 中的 interface(接口)
先看看其他语言中的 interface 是怎么使用的。
我们知道,很多编程语言里面都有 interface 这个关键字,表示的是接口,应该也用过,比如 Java 里面的:
// 定义一个 Flyable 接口
interface Flyable {
public void fly();
}
// 定义一个名为 Bird 的类,显式实现了 Flyable 接口
class Bird implements Flyable {
public void fly() {
System.out.println("Bird fly.");
}
}
class Test {
// fly 方法接收一个实现了 Flyable 接口的类
public static void fly(Flyable flyable) {
flyable.fly();
}
public static void main(String[] args) {
Bird b = new Bird();
// b 实现了 Flyable 接口,所以可以作为 fly 的参数
fly(b);
}
}
在这个例子中,我们定义了一个 Flyable 接口,然后定义了一个实现了 Flyable 接口的 Bird 类, 最后,定义了一个测试的类,这个类的 fly 方法接收一个 Flyable 接口类型的参数, 因为 Bird 类实现了 Flyable 接口,所以可以将 b 作为参数传递给 fly 方法。
这个例子就是 Java 中interface的典型用法,如果一个类要实现一个接口,我们必须显式地通过 implements 关键字来声明。 然后使用的时候,对于需要某一接口类型的参数的方法,我们可以传递实现了那个接口的对象进去。
Java 中类实现接口必须显式通过 implements 关键字声明。
go 中的 interface(接口)
go 里面也有 interface 这个关键字,但是 go 与其他语言不太一样。 go 里面结构体与接口之间不需要显式地通过 implements 关键字来声明的,在 go 中,只要一个结构体实现了 interface 的所有方法,我们就可以将这个结构体当做这个 interface 类型,比如下面这个例子:
package main
import "fmt"
// 定义一个 Flyable 接口
type Flyable interface {
Fly() string
}
// Bird 结构体没有显式声明实现了 Flyable 接口(没有 implements 关键字)
// 但是 Bird 定义了 Fly() 方法,
// 所以可以作为下面 fly 函数的参数使用。
type Bird struct {
}
func (b Bird) Fly() string {
return "bird fly."
}
// 只要实现了 Flyable 的所有方法,
// 就可以作为 fly 的参数。
func fly(f Flyable) {
fmt.Println(f.Fly())
}
func main() {
var b = Bird{}
// 在 go 看来,b 实现了 Fly 接口,
// 因为 Bird 里面实现了 Fly 接口的所有方法。
fly(b)
}
Go 中结构体实现接口不用通过implements关键字声明。(实际上,Go 也没有这个关键字)
go interface 的优势
go 接口的这种实现方式,有点类似于动态类型的语言,比如 Python,但是相比 Python,go 在编译期间就可以发现一些明显的错误。
比如像 Python 中下面这种代码,如果传递的 coder 没有 say_hello 方法,这种错误只有运行时才能发现:
def hello_world(coder):
coder.say_hello()
但如果是 go 的话,下面这种写法中,如果传递给 hello_world 没有实现 say 接口,那么编译的时候就会报错,无法通过编译:
type say interface {
say_hello()
}
func hello_world(coder say) {
coder.say_hello()
}
因此,go 的这种接口实现方式有点像动态类型的语言,在一定程度上给了开发者自由,但是也在语言层面帮开发者做了类型检查。
go 中不必像静态类型语言那样,所有地方都明确写出类型,go 的编译器帮我们做了很多工作,让我们在写 go 代码的时候更加的轻松。 interface 也是,我们无需显式实现接口,只要我们的结构体实现了接口的所有类型,那么它就可以当做那个接口类型使用(鸭子类型:duck typing)。
空接口
go 中的 interface{} 表示一个空接口(在比较新版本中也可以使用 any 关键字来代替 interface{}),这个接口没有任何方法。因此可以将任何变量赋值给 interface{} 类型的变量。
这在一些允许不同类型或者不确定类型参数的方法中用得比较广泛,比如 fmt 里面的 println 等方法。
如何使用 interface{} 类型的参数?
这个可能是大部分人所需要关心的地方,因为这可能在日常开发中经常需要用到。
类型断言
当实际开发中,我们接收到一个接口类型参数的时候,我们可能会知道它是几种可能的类型之一了,我们就可以使用类型断言来判断 interface{} 变量是否实现了某一个接口:
func fly(f interface{}) {
// 第一个返回值 v 是 f 转换为接口之前的值,
// ok 为 true 表示 f 是 Flyable 类型
if v, ok := f.(Flyable); ok {
fmt.Println("bird " + v.Fly())
}
// 断言形式:接口.(类型)
if _, ok := f.(Bird); ok {
fmt.Println("bird flying...")
}
}
在实际开发中,我们可以使用 ·xx.(Type)· 这种形式来判断:
interface{}类型的变量是否是某一个类型interface{}类型的变量是否实现了某一个接口
如,f.(Flyable) 就是判断 f 是否实现了 Flyable 接口,f.(Bird) 就是判断f是否是 Bird 类型。
另外一种类型断言方式
可能我们会觉得上面的那种 if 的判断方式有点繁琐,确实如此,但是如果我们不能保证 f 是某一类型的情况下,用上面这种判断方式是比较安全的。
还有另外一种判断方式,用在我们确切地知道 f 具体类型的情况:
func fly2(f interface{}) {
fmt.Println("bird " + f.(Flyable).Fly())
}
在这里,我们断言 f 是 Flyable 类型,然后调用了它的 Fly 方法。
这是一种不安全的调用,如果f实际上没有实现Flyable接口,上面这行代码会引发 panic。 而相比之下,v, ok := f.(Flyable) 这种方式会返回第二个值让我们判断这个断言是否成立。
switch…case 中判断接口类型
除了上面的断言方式,还有另外一种判断 interface{} 类型的方法,那就是使用 switch...case 语句:
func str(f interface{}) string {
// 判断 f 的类型
switch f.(type) {
case int:
// f 是 int 类型
return "int: " + strconv.Itoa(f.(int))
case int64:
// f 是 int64 类型
return "int64: " + strconv.FormatInt(f.(int64), 10)
case Flyable:
return "flyable..."
}
return "???"
}
编译器自动检测类型是否实现接口
上面我们说过了,在 go 里面,类型不用显式地声明实现了某个接口(也不能)。那么问题来了,我们开发的时候, 如果我们就是想让某一个类型实现某个接口的时候,但是漏实现了一个方法的话,IDE 是没有办法知道我们漏了的那个方法的:
type Flyable interface {
Fly() string
}
// 没有实现 Flyable 接口,因为没有 Fly() 方法
type Bird struct {
}
func (b Bird) Eat() string {
return "eat."
}
比如这段代码中,我们本意是要 Bird 也实现 Fly 方法的,但是因为没有显式声明,所以 IDE 没有办法知道我们的意图。 这样一来,在实际运行的时候,那些我们需要 Flyable 的地方,如果我们传了 Bird 实例的话,就会报错了。
一种简单的解决方法
如果我们明确知道 Bird 将来是要当做 Flyable 参数使用的话,我们可以加一行声明:
var _ Flyable = Bird{}
这样一来,因为我们有 Bird 转 Flyable 类型的操作,所以编译器就会去帮我们检查 Bird 是否实现了 Flyable 接口了。 如果Bird没有实现 Flyable 中的所有方法,那么编译的时候会报错,这样一来,这些错误就不用等到实际运行的时候才能发现了。
实际上,很多开源项目都能看到这种写法。看起来定义了一个空变量,但是实际上却可以帮我们进行类型检查。
这种解决方法还有另外一种写法如下:
var _ Flyable = (*Bird)(nil)
类型转换与接口断言
我们知道了,接口断言可以获得一个具体类型(也可以是接口)的变量,同时我们也知道了,在 go 里面也有类型转换这东西, 实际上,接口断言与类型转换都是类型转换,它们的差别只是:
interface{} 只能通过类型断言来转换为某一种具体的类型,而一般的类型转换只是针对普通类型之间的转换。
// 类型转换:f 由 float32 转换为 int
var f float32 = 10.8
i := int(f)
// 接口的类型断言
var f interface{}
v, ok := f.(Flyable)
如果是 interface{},需要使用类型断言转换为某一具体类型。
一个类型可以实现多个接口
上文我们说过了,只要一个类型实现了接口中的所有方法,那么那个类型就可以当作是那个接口来使用:
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type myFile struct {
}
// 实现了 Writer 接口
func (m myFile) Write(p []byte) (n int, err error) {
return 0, nil
}
// 实现了 Closer 接口
func (m myFile) Close() error {
return nil
}
在上面这个例子中,myFile 实现了 Write 和 Close 方法,而这两个方法分别是 Writer 和 Closer 接口中的所有方法。 在这种情况下,myFile 的实例既可以作为 Writer 使用,也可以作为 Closer 使用:
func foo(w Writer) {
w.Write([]byte("foo"))
}
func bar(c Closer) {
c.Close()
}
func test() {
m := myFile{}
// m 可以作为 Writer 接口使用
foo(m)
// m 也可以作为 Closer 接口使用
bar(m)
}
接口与 nil 不相等
有时候我们会发现,明明传了一个 nil 给 interface{} 类型的参数,但在我们判断实参是否与 nil 相等的时候,却发现并不相等,如下面这个例子:
func test(i interface{}) {
fmt.Println(reflect.TypeOf(i))
fmt.Println(i == nil)
}
func main() {
var b *int = nil
test(b) // 会输出:*int false
test(nil) // 会输出:<nil> true
}
这是因为 go 里面的interface{}实际上是包含两部分的,一部分是 type,一部分是 data,如果我们传递的nil是某一个类型的 nil, 那么 interface{} 类型的参数实际上接收到的值会包含对应的类型。 但如果我们传递的nil就是一个普通的 nil,那么 interface{} 类型参数接收到的 type 和data都为 nil, 这个时候再与 nil 比较的时候才是相等的。如下图,两个实参i的type是不一样的。

Go中interface{}判nil的正确姿势
func IsNil(x interface{}) bool {
if x == nil {
return true
}
rv := reflect.ValueOf(x)
return rv.Kind() == reflect.Ptr && rv.IsNil()
}
嵌套的接口
在go中,不仅结构体与结构体之间可以嵌套,接口与接口也可以通过嵌套创造出新的接口。
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// 下面这个接口包含了 Writer 和 Closer 的所有方法
type WriteCloser interface {
Writer
Closer
}
WriteCloser 是一个包含了 Writer 和 Closer 两个接口所有方法的新接口,也就是说,WriteCloser 包含了 Write 和 Close 方法。
这样的好处是,可以将接口拆分为更小的粒度。比如,对于某些只需要Close方法的地方,我们就可以用 Closer 作为参数的类型, 即使参数也实现了Write方法,因为我们并不关心除了 Close 以外的其他方法:
func foo(c Closer) {
// ...
c.Close()
}
而对于上面的 myFile,因为同时实现了 Writer 接口和 Closer 接口,而 WriteCloser 包含了这两个接口, 所以实际上 myFile 可以当作 WriteCloser 或者 Writer 或 Closer 类型使用。
总结
- 接口里面只声明了方法,没有数据成员。
go中实现某个接口不需要显式声明(也不能)。- 只要一个类型实现了接口的所有方法,那么该类型实现了此接口。该类型的值可以赋值给该接口类型。
interface{}/any是空接口,任何类型的值都可以赋值给它。- 通过类型断言我们可以将
interface{}类型转换为具体的类型。 - 我们通过声明接口类型的
_变量来让编译器帮我们检查我们的类型是否实现了某一接口。 - 一个类型可以同时实现多个接口,可以当作多个接口类型来使用。
nil与值为nil的interface{}实际上不想等,需要注意。go中的接口可以嵌套,类似结构体的嵌套。
1万+

被折叠的 条评论
为什么被折叠?



