阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

Go接口详谈

82次阅读
没有评论

共计 5205 个字符,预计需要花费 14 分钟才能阅读完成。

导读 接口类型的变量可以保存实现接口的类型的值。该类型的值成为接口的动态值,并且该类型成为接口的动态类型。
1. 接口

在 Go 中使用 interface 关键字声明一个接口:

type Shaper interface {Area() float64 
 Perimeter() float64}

如果我们直接使用 fmt 库进行输出,会得到什么结果呢?

func main() { 
 var s Shaper 
 fmt.Println("value of s is", s) 
 fmt.Printf("type of s is %T\n", s) 
}

输出:

value of s is   
type of s is  

在这里,引出接口的概念。接口有两种类型。接口的静态类型是接口本身,例如上述程序中的 Shape。接口没有静态值,而是指向动态值。

接口类型的变量可以保存实现接口的类型的值。该类型的值成为接口的动态值,并且该类型成为接口的动态类型。

从上面的示例开始,我们可以看到零值和接口的类型为 nil。这是因为,此刻,我们已声明类型 Shaper 的变量 s,但未分配任何值。当我们使用带有接口参数的 fmt 包中的 Println 函数时,它指向接口的动态值,Printf 功能中的 %T 语法是指动态类型的接口。实际上,接口静态类型是 Shaper。

当我们使用一个类型去实现该接口后,会是什么效果。

type Rect struct { 
 width  float64 
 height float64 
} 
 
func (r Rect) Area() float64 {return r.width * r.height} 
 
func (r Rect) Perimeter() float64 {return 2 * (r.width + r.height) 
} 
 
// main 
func main() { 
 var s Shaper 
 fmt.Println("value of s is", s) 
 fmt.Printf("type of s is %T\n", s) 
 s = Rect{5.0, 4.0} 
 r := Rect{5.0, 4.0} 
 fmt.Printf("type of s is %T\n", s) 
 fmt.Printf("value of s is %v\n", s) 
 fmt.Printf("area of rect is %v\n", s.Area()) 
 fmt.Println("s == r is", s == r) 
}

输出:

value of s is   
type of s is  
type of s is main.Rect 
value of s is {5 4} 
area of rect is 20 
s == r is tru 

可以看到此时 s 变成了动态类型,存储的是 main.Rect,值变成了{5,4}。

有时,动态类型的接口也称为具体类型,因为当我们访问接口类型时,它会返回其底层动态值的类型,并且其静态类型保持隐藏。

我们可以在 s 上调用 Area 方法,因为接口 Shaper 定义了 Area 方法,而 s 的具体类型是 Rect,它实现了 Area 方法。该方法将在接口保存的动态值上被调用。

此外,我们可以看到我们可以使用 s 与 r 进行比较,因为这两个变量都保存相同的动态类型 (Rect 类型的结构) 和动态值{5 4}。

我们接着使用圆来实现该接口:

type Circle struct {radius float64} 
 
func (c Circle) Area() float64 {return 3.14 * c.radius * c.radius} 
 
func (c Circle) Perimeter() float64 {return 2 * 3.14 * c.radius} 
// main 
s = Circle{10} 
fmt.Printf("type of s is %T\n", s) 
fmt.Printf("value of s is %v\n", s) 
fmt.Printf("area of rect is %v\n", s.Area())

此时输出:

type of s is main.Circle 
value of s is {10} 
area of rect is 314

这里进一步理解了接口保存的动态类型。从切片角度出发,可以说,接口也以类似的方式工作,即动态保存对底层类型的引用。

当我们删除掉 Perimeter 的实现,可以看到如下报错结果。

./rect.go:34:4: cannot use Rect{...} (type Rect) as type Shaper in assignment: 
Rect does not implement Shaper (missing Perimeter method)

从上面的错误应该是显而易见的,为了成功实现接口,需要实现与完全签名的接口声明的所有方法。

2. 空接口

当一个接口没有任何方法时,它被称为空接口。这由接口 {} 表示。因为空接口没有方法,所以所有类型都隐式地实现了这个接口。

空接口的作用之一在于:函数可以接收多个不同类型参数。

例如:fmt 的 Println 函数。

func Println(a ...interface{}) (n int, err error) 
Println 是一个可变函数,它接受 interface{}类型的参数。

例如:

type MyString string 
 
func explain(i interface{}) {fmt.Printf("type: %T, value: %v\n", i, i) 
} 
// main 
s := MyString("hello") 
explain(s) 
r := Rect{1, 2} 
explain(r)

输出:

type: inter.MyString, value: hello 
type: inter.Rect, value: {1 2}

可以看到空接口的类型与值是动态的。

3. 多个接口

在下面的程序中,我们用 Area 方法创建了 Shape 接口,用 Volume 方法创建了 Object 接口。因为结构类型 Cube 实现了这两个方法,所以它实现了这两个接口。因此,我们可以将结构类型 Cube 的值赋给类型为 Shape 或 Object 的变量。

type IShape interface {Area() float64 
} 
 
type Object interface {Volume() float64 
} 
 
type Cube struct {side float64} 
 
func (c Cube) Area() float64 {return 6 * c.side * c.side} 
 
func (c Cube) Volume() float64 {return c.side * c.side * c.side} 
// main 
c := Cube{3} 
var s IShape = c 
var o Object = c 
fmt.Println("area is", s.Area()) 
fmt.Println("Volume is", o.Volume())

这种调用是没有问题的,调用各自动态类型的方法。

那如果是这样呢?

fmt.Println("area of s of interface type IShape is", s.Volume()) 
fmt.Println("volume of o of interface type Object is", o.Area())

输出:

s.Volume undefined (type Shape has no field or method Volume) 
o.Area undefined (type Object has no field or method Area)

这个程序无法编译,因为 s 的静态类型是 IShape,而 o 的静态类型是 Object。因为 IShape 没有定义 Volume 方法,Object 也没有定义 Area 方法,所以我们得到了上面的错误。

要使其工作,我们需要以某种方式提取这些接口的动态值,这是一个立方体类型的结构体,立方体实现了这些方法。这可以使用类型断言来完成。

4. 类型断言

我们可以通过 i.(Type)确定接口 i 的底层动态值,Go 将检查 i 的动态类型是否与 type 相同,并返回可能的动态值。

var s1 IShape = Cube{3} 
c1 := s1.(Cube) 
fmt.Println("area of s of interface type IShape is", c1.Volume()) 
fmt.Println("volume of o of interface type Object is", c1.Area())

这样便可以正常工作了。

如果 IShape 没有存储 Cube 类型,且 Cube 没有实现 IShape,那么报错:

impossible type assertion: 
Cube does not implement IShape (missing Area method)

如果 IShape 没有存储 Cube 类型,且 Cube 实现 Shape,那么报错:

panic: interface conversion: inter.IShape is nil, not inter.Cub

幸运的是,语法中还有另一个返回值:

value, ok := i.(Type)

在上面的语法中,如果 i 有具体的 type 类型或 type 的动态值,我们可以使用 ok 变量来检查。如果不是,那么 ok 将为假,value 将为 Type 的零值(nil)。

此外,使用类型断言可以检查该接口的动态类型是否实现了其他接口,就像前面的 IShape 的动态类型是 Cube,它实现了 IShape、Object 接口,如下例子:

vaule1, ok1 := s1.(Object) 
value2, ok2 := s1.(Skin) 
fmt.Printf("IShape s 的动态类型值是: %v, 该动态类型是否实现了 Object 接口: %v\n", vaule1, ok1) 
fmt.Printf("IShape s 的动态类型值是: %v, 该动态类型是否实现了 Skin 接口: %v\n", value2, ok2)

输出:

IShape s 的动态类型值是: {3}, 该动态类型是否实现了 Object 接口: true 
IShape s 的动态类型值是: , 该动态类型是否实现了 Skin 接口: false 

类型断言不仅用于检查接口是否具有某个给定类型的具体值,而且还用于将接口类型的给定变量转换为不同的接口类型。

5. 类型 Switch

在前面的空接口中,我们知道将一个空接口作为函数参数,那么该函数可以接受任意类型,那如果我有一个需求是:当传递的数据类型是字符串时,要求全部变为大写,其他类型不进行操作?

针对这样的需求,我们可以采用 Type Switch,即:i.(type)+switch。

func switchProcess(i interface{}) {switch i.(type) { 
 case string: 
  fmt.Println("process string") 
 case int: 
  fmt.Println("process int") 
 default: 
  fmt.Printf("type is %T\n", i) 
 } 
}

输出:

process int 
process string
6. 嵌入接口

在 Go 中,一个接口不能实现或扩展其他接口,但我们可以通过合并两个或多个接口来创建一个新的接口。

例如:

这里使用 Runner 与 Eater 两个接口,组合成了一个新接口 RunEater,该接口为 Embedding interfaces。

type Runner interface {run() string 
} 
type Eater interface {eat() string 
} 
 
type RunEater interface { 
 Runner 
 Eater 
} 
 
type Dog struct {age int} 
 
func (d Dog) run() string {return "run"} 
 
func (d Dog) eat() string {return "eat"} 
 
// main 
d := Dog{10} 
var re RunEater = d 
var r Runner = d 
var e Eater = d 
fmt.Printf("RunnEater dynamic type: %T, value: %v\n", re, re) 
fmt.Printf("Runn dynamic type: %T, value: %v\n", r, r) 
fmt.Printf("Eater dynamic type: %T, value: %v\n", e, e)

输出:

RunnEater dynamic type: inter.Dog, value: {10} 
Runn dynamic type: inter.Dog, value: {10} 
Eater dynamic type: inter.Dog, value: {10}
7. 接口比较

如果基础动态值为 nil,则两个接口总是相等的,这意味着两个 nil 接口总是相等的,因此 == operation 返回 true。

var a, b interface{} 
fmt.Println(a == b) // true

如果这些接口不是 nil,那么它们的动态类型 (具体值的类型) 应该相同,具体值应该相等。

如果接口的动态类型不具有可比性,例如 slice、map、function,或者接口的具体值是包含这些不可比较性值的复杂数据结构,如切片或数组,则 == 或!= 操作将导致运行时 panic。

阿里云 2 核 2G 服务器 3M 带宽 61 元 1 年,有高配

腾讯云新客低至 82 元 / 年,老客户 99 元 / 年

代金券:在阿里云专用满减优惠券

正文完
星哥玩云-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2024-07-25发表,共计5205字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中