让我一起谈谈Go中接口

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

 [[408979]]

本文转载自微信公众号「光城」,作者lightcity。转载本文请联系光城公众号。

1.接口

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

  1. type Shaper interface { 
  2.  Area() float64 
  3.  Perimeter() float64 

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

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

输出:

  1. value of s is  <nil> 
  2. type of s is <nil> 

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

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

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

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

  1. type Rect struct { 
  2.  width  float64 
  3.  height float64 
  4.  
  5. func (r Rect) Area() float64 { 
  6.  return r.width * r.height 
  7.  
  8. func (r Rect) Perimeter() float64 { 
  9.  return 2 * (r.width + r.height) 
  10.  
  11. // main 
  12. func main() { 
  13.  var s Shaper 
  14.  fmt.Println("value of s is ", s) 
  15.  fmt.Printf("type of s is %T\n", s) 
  16.  s = Rect{5.0, 4.0} 
  17.  r := Rect{5.0, 4.0} 
  18.  fmt.Printf("type of s is %T\n", s) 
  19.  fmt.Printf("value of s is %v\n", s) 
  20.  fmt.Printf("area of rect is %v\n", s.Area()) 
  21.  fmt.Println("s == r is", s == r) 

输出:

  1. value of s is  <nil> 
  2. type of s is <nil> 
  3. type of s is main.Rect 
  4. value of s is {5 4} 
  5. area of rect is 20 
  6. s == r is tru 

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

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

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

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

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

  1. type Circle struct { 
  2.  radius float64 
  3.  
  4. func (c Circle) Area() float64 { 
  5.  return 3.14 * c.radius * c.radius 
  6.  
  7. func (c Circle) Perimeter() float64 { 
  8.  return 2 * 3.14 * c.radius 
  9. // main 
  10. s = Circle{10} 
  11. fmt.Printf("type of s is %T\n", s) 
  12. fmt.Printf("value of s is %v\n", s) 
  13. fmt.Printf("area of rect is %v\n", s.Area()) 

此时输出:

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

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

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

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

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

2.空接口

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

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

例如:fmt的Println函数。

  1. func Println(a ...interface{}) (n int, err error) 

Println是一个可变函数,它接受interface{}类型的参数。

例如:

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

输出:

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

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

3.多个接口

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

  1. type IShape interface { 
  2.  Area() float64 
  3.  
  4. type Object interface { 
  5.  Volume() float64 
  6.  
  7. type Cube struct { 
  8.  side float64 
  9.  
  10. func (c Cube) Area() float64 { 
  11.  return 6 * c.side * c.side 
  12.  
  13. func (c Cube) Volume() float64 { 
  14.  return c.side * c.side * c.side 
  15. // main 
  16. c := Cube{3} 
  17. var s IShape = c 
  18. var o Object = c 
  19. fmt.Println("area is", s.Area()) 
  20. fmt.Println("Volume is", o.Volume()) 

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

那如果是这样呢?

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

输出:

  1. s.Volume undefined (type Shape has no field or method Volume) 
  2. 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相同,并返回可能的动态值。

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

这样便可以正常工作了。

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

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

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

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

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

  1. value, ok := i.(Type) 

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

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

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

输出:

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

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

5.类型Switch

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

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

  1. func switchProcess(i interface{}) { 
  2.  switch i.(type) { 
  3.  case string: 
  4.   fmt.Println("process string"
  5.  case int
  6.   fmt.Println("process int"
  7.  default
  8.   fmt.Printf("type is %T\n", i) 
  9.  } 

输出:

  1. process int 
  2. process string 

6.嵌入接口

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

例如:

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

  1. type Runner interface { 
  2.  run() string 
  3. type Eater interface { 
  4.  eat() string 
  5.  
  6. type RunEater interface { 
  7.  Runner 
  8.  Eater 
  9.  
  10. type Dog struct { 
  11.  age int 
  12.  
  13. func (d Dog) run() string { 
  14.  return "run" 
  15.  
  16. func (d Dog) eat() string { 
  17.  return "eat" 
  18.  
  19. // main 
  20. d := Dog{10} 
  21. var re RunEater = d 
  22. var r Runner = d 
  23. var e Eater = d 
  24. fmt.Printf("RunnEater dynamic type: %T, value: %v\n", re, re) 
  25. fmt.Printf("Runn dynamic type: %T, value: %v\n", r, r) 
  26. fmt.Printf("Eater dynamic type: %T, value: %v\n", e, e) 

输出:

  1. RunnEater dynamic type: inter.Dog, value: {10} 
  2. Runn dynamic type: inter.Dog, value: {10} 
  3. Eater dynamic type: inter.Dog, value: {10} 

7.接口比较

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

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

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

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

学习自https://medium.com/rungo/interfaces-in-go-ab1601159b3a

 

责任编辑:武晓燕 来源: 光城
相关推荐

2021-04-12 18:03:39

Nginx架构负载

2021-09-11 19:02:34

Hook使用版本

2021-01-12 05:08:49

DHCP协议模型

2021-12-16 12:01:21

区块链Libra货币

2024-02-26 00:00:00

Go性能工具

2023-03-26 23:47:32

Go内存模型

2022-11-02 08:41:40

2022-07-10 23:15:46

Go语言内存

2009-06-19 15:11:34

DWR和Spring

2021-09-14 17:16:21

Java 同步工具类

2021-07-12 11:35:13

Go协程Goroutine

2023-11-29 09:04:00

前端接口

2015-10-20 16:48:06

AnsibleDocker可扩展设计

2024-06-27 08:54:22

Go模块团队

2024-05-17 08:47:33

数组切片元素

2022-02-22 10:50:19

IDEAGit工具,

2022-12-02 14:20:09

Tetris鸿蒙

2022-11-29 16:35:02

Tetris鸿蒙

2022-12-06 08:12:11

Java关键字

2009-11-20 12:09:40

ExchangeVPNWindowsServ
点赞
收藏

51CTO技术栈公众号