Go 语言接口详解(二)

这是『就要学习 Go 语言』系列的第 20 篇分享文章

提醒:文末给大家留了小练习,可以先看文章,再做练习,检验自己的学习成果!

我们接着上一篇,继续讲接口的其他用法。

实现多个接口

一种类型可以实现多个接口,来看下例子:

 1type Shape interface {
2    Area() float32
3}
4
5type Object interface {
6    Perimeter() float32
7}
8
9type Circle struct {
10    radius float32
11}
12
13func (c Circle) Area() float32 {
14    return math.Pi * (c.radius * c.radius)
15}
16
17func (c Circle) Perimeter() float32 {
18    return 2 * math.Pi * c.radius
19}
20
21func main() {
22    c := Circle{3}
23    var s Shape = c
24    var p Object = c
25    fmt.Println("area: ", s.Area())
26    fmt.Println("perimeter: ", p.Perimeter())
27}

输出:

1area:  28.274334
2perimeter:  18.849556

上面的代码,结构体 Circle 分别实现了 Shape 接口和 Object 接口,所以可以将结构体变量 c 赋给变量 s 和 p,此时 s 和 p 具有相同的动态类型和动态值,分别调用各自实现的方法 Area() 和 Perimeter()。
我们修改下程序:

1fmt.Println("area: ", p.Area())
2fmt.Println("perimeter: ", s.Perimeter())

编译会出错:

1p.Area undefined (type Object has no field or method Area)
2s.Perimeter undefined (type Shape has no field or method Perimeter)

为什么?因为 s 的静态类型是 Shape,而 p 的静态类型是 Object。那有什么解决办法吗?有的,我们接着看下一节

类型断言

类型断言可以用来获取接口的底层值,通常的语法:i.(Type),其中 i 是接口,Type 是类型或接口。编译时会自动检测 i 的动态类型与 Type 是否一致。

 1type Shape interface {
2    Area() float32
3}
4
5type Object interface {
6    Perimeter() float32
7}
8
9type Circle struct {
10    radius float32
11}
12
13func (c Circle) Area() float32 {
14    return math.Pi * (c.radius * c.radius)
15}
16
17func (c Circle) Perimeter() float32 {
18    return 2 * math.Pi * c.radius
19}
20
21func main() {
22    var s Shape = Circle{3}
23    c := s.(Circle)
24    fmt.Printf("%T\n",c)
25    fmt.Printf("%v\n",c)
26    fmt.Println("area: ", c.Area())
27    fmt.Println("perimeter: ", c.Perimeter())
28}

输出:

1main.Circle
2{3}
3area:  28.274334
4perimeter:  18.849556

上面的代码,我们可以通过 c 访问接口 s 的底层值,也可以通过 c 分别调用方法 Area() 和 Perimeter(),这就解决了上面遇到的问题。
在语法 i.(Type) 中,如果 Type 没有实现 i 所属的接口,编译的时候会报错;或者 i 的动态值不是 Type,则会报 panic 错误。怎么解决呢?可以使用下面的语法:

1value, ok := i.(Type)

使用上面的语法,Go 会自动检测上面提到的两种情况,我们只需要通过变量 ok 判断结果是否正确即可。如果正确,ok 为 true,否则为 false,value 为 Type 对应的零值。

类型选择

类型选择用于将接口的具体类型与各种 case 语句中指定的多种类型进行匹配比较,有点类似于 switch case 语句,不同的是 case 中指定是类型。
类型选择的语法有点类似于类型断言的语法:i.(type),其中 i 是接口,type 是固定关键字,使用这个可以获得接口的具体类型而不是值,每一个 case 中的类型必须实现了 i 接口。

 1func switchType(i interface{}) {
2    switch i.(type) {
3    case string:
4        fmt.Printf("string and value is %s\n", i.(string))
5    case int:
6        fmt.Printf("int and value is %d\n", i.(int))
7    default:
8        fmt.Printf("Unknown type\n")
9    }
10}
11func main() {
12    switchType("Seekload")
13    switchType(27)
14    switchType(true)
15}

输出:

1string and value is Seekload
2int and value is 27
3Unknown type

上面的代码应该很好理解,i 的类型匹配到哪个 case ,就会执行相应的输出语句。
注意:只有接口类型才可以进行类型选择。其他类型,例如 int、string等是不能的:

1i := 1
2switch i.(type) {
3case int:
4    println("int type")
5default:
6    println("unknown type")
7}

报错:

1cannot type switch on non-interface value i (type int)

接口嵌套

Go 语言中,接口不能去实现别的接口也不能继承,但是可以通过嵌套接口创建新接口。

 1type Math interface {
2    Shape
3    Object
4}
5type Shape interface {
6    Area() float32
7}
8
9type Object interface {
10    Perimeter() float32
11}
12
13type Circle struct {
14    radius float32
15}
16
17func (c Circle) Area() float32 {
18    return math.Pi * (c.radius * c.radius)
19}
20
21func (c Circle) Perimeter() float32 {
22    return 2 * math.Pi * c.radius
23}
24
25func main() {
26
27    c := Circle{3}
28    var m Math = c
29    fmt.Printf("%T\n", m )
30    fmt.Println("area: ", m.Area())
31    fmt.Println("perimeter: ", m.Perimeter())
32}

输出:

1main.Circle
2area:  28.274334
3perimeter:  18.849556

上面的代码,通过嵌套接口 Shape 和 Object,创建了新的接口 Math。任何类型如果实现了接口 Shape 和 Object 定义的方法,则说类型也实现了接口 Math,例如我们创建的结构体 Circle。
主函数里面,定义了接口类型的变量 m,动态类型是结构体 Circle,注意下方法 Area 和  Perimeter 的调用方式,类似与访问嵌套结构体的成员。

使用指针接收者和值接收者实现接口

在前面我们都是通过值接收者去实现接口的,其实还可以通过指针接收者实现接口。实现过程中还是有需要注意的地方,我们来看下:

 1type Shape interface {
2    Area() float32
3}
4
5type Circle struct {
6    radius float32
7}
8
9type Square struct {
10    side float32
11}
12
13func (c Circle) Area() float32 {
14    return math.Pi * (c.radius * c.radius)
15}
16
17func (s *Square) Area() float32 {
18    return s.side * s.side
19}
20
21func main() {
22    var s Shape
23    c1 := Circle{3}
24    s = c1
25    fmt.Printf("%v\n",s.Area())
26
27    c2 := Circle{4}
28    s = &c2
29    fmt.Printf("%v\n",s.Area())
30
31    c3 := Square{3}
32    //s = c3
33    s = &c3
34    fmt.Printf("%v\n",s.Area())
35
36}

输出:

128.274334
250.265484
39

上面的代码,结构体 Circle 通过值接收者实现了接口 Shape。我们在方法那篇文章中已经讨论过了,值接收者的方法可以使用值或者指针调用,所以上面的 c1 和 c2 的调用方式是合法的。

结构体 Square 通过指针接收者实现了接口 Shape。如果将上方注释部分打开的话,编译就会出错:

1cannot use c3 (type Square) as type Shape in assignment:
2Square does not implement Shape (Area method has pointer receiver)

从报错提示信息可以清楚看出,此时我们尝试将值类型 c3 分配给 s,但 c3 并没有实现接口 Shape。这可能会令我们有点惊讶,因为在方法中,我们可以直接通过值类型或者指针类型调用指针接收者方法。
记住一点:对于指针接受者的方法,用一个指针或者一个可取得地址的值来调用都是合法的。但接口存储的具体值是不可寻址的,对于编译器无法自动获取 c3 的地址,于是程序报错。

关于接口的使用方法总结全都在这,希望这两篇文章能够给你带来帮助!

作业:
文章提到的类型断言:i.(Type),其中 i 是接口,Type 可以是类型或接口,如果 Type 是接口的话,表达式是什么意思呢?下面的程序输出什么?欢迎大家留言讨论!

 1type Shape interface {
2    Area() float32
3}
4
5type Object interface {
6    Perimeter() float32
7}
8
9type Circle struct {
10    radius float32
11}
12
13func (c Circle) Area() float32 {
14    return math.Pi * (c.radius * c.radius)
15}
16
17func main() {
18    var s Shape = Circle{3}
19    value1,ok1 := s.(Shape)
20    value2,ok2 := s.(Object)
21
22    fmt.Println(value1,ok1)
23    fmt.Println(value2,ok2)
24}

 

 

四月你好,继续努力

Go 语言接口详解(二)  Go 语言接口详解(二)  Go 语言接口详解(二)

如果我的文章对你有所帮助,点赞、转发都是一种支持!

Go 语言接口详解(二)

 

Go 语言接口详解(二)

 

上一篇:再掀上市潮,Robinhood和Circle能否重燃华尔街热情?


下一篇:6-2 求平面几何形状的面积(圆形和矩形)C++ (5 分)