同步本人的掘金账号中的文章:https://juejin.cn/post/7299364292515872805
简单介绍
本篇是在学习Go的过程中,对Go接口的一个总结,文中若有不对之处,欢迎大家评论留言一起讨论,一起学习Go~~~
一、接口简介
Go
中的接口是一组方法签名的集合,且Go接口
的设计是非侵入式,一个具体类型(比如结构体strcut
)实现接口不需要在语法上显示地声明,只需要具体类型的实现方法集是囊括了接口的方法集,就代表该类型实现了接口,即一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。
二、接口的声明及初始化
1、接口的声明
接口的声明格式如下:
type Namer interface {
Method1(param_list) return_type
Method2(param_list) return_type
...
}
声明新接口类型的可以遵从以下特点:
- 接口的命名一般以"er"结尾;
- 接口定义的内部方法声明不需要func引导;
- 在接口定义中,只有方法声明,并没有方法的具体实现;
举个例子:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
2、接口的初始化
Go接口在初始化方面,有两种方式:
(1)实例赋值接口
一个具体类型实现了接口中的所有方法,则该具体类型实现该接口,可以将该具体类型的实例直接赋值给接口类型的变量,直接上代码~
package main
import "fmt"
type Shaper interface {
Area() float64
}
type Circle struct {
Radius float64
}
type Rectangle struct {
Length float64
Wide float64
}
func (circle *Circle) Area() float64 {
return 3.14 * circle.Radius * circle.Radius
}
func (rectangle *Rectangle) Area() float64 {
return rectangle.Length * rectangle.Wide
}
func main() {
circle := &Circle{Radius: 5}
var shape1 Shaper = circle // 将 Circle 类型的实例 circle 赋值给接口类型的变量 shape1
fmt.Println("Area of circle:", shape1.Area()) // Area of circle: 78.5
rectangle := &Rectangle{Length: 5, Wide: 4}
var shape2 Shaper = rectangle // 将 Square 类型的实例 square 赋值给接口类型的变量 shape2
fmt.Println("Area of rectangle:", shape2.Area()) // Area of rectangle: 20
}
在 main()
方法中创建了一个 Circle
的实例。在主程序外边定义了一个接收者类型是 Circle
方法的 Area()
,用来计算圆形的面积,即Circle
的实例实现了接口 Shaper
的所有方法,结构体 Circle
实现了接口 Shaper
。
另外Rectangle
也体现在Go中的多态,根据当前的类型选择正确的方法,或者说:同一种类型在不同的实例上表现出不同的行为。
(2)接口变量赋值接口变量
已经初始化的接口类型变量a
可以直接赋值给另一个接口变量b
的条件为b的方法集是a的方法集的子集,即b
接口变量所拥有的方法,a
接口变量均已实现。
package main
import "fmt"
type Shaper interface {
Area() float64
}
type Shaper2 interface {
Area() float64
Perimeter() float64
}
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
}
func main() {
circle := Circle{Radius: 5}
var b Shaper
var a Shaper2
a = circle
b = a // Shape的方法集是Shape2的方法集的子集
fmt.Println("Area of shaper:", b.Area())
fmt.Println("Area of shaper2:", a.Area())
fmt.Println("Perimeter of shaper2:", a.Perimeter())
}
执行结果
Area of shape: 78.5
Area of shape2: 78.5
Perimeter of shape2: 31.400000000000002
没有初始化的接口变量,其默认值是
nil
package main
import "fmt"
type Shaper interface {
Area() float64
}
func main() {
var shaper Shaper
fmt.Println(shaper) // <nil>
}
测试一个实例是否实现了某个接口的小方法~
方法一:通过使用类型断言,可以判断出一个实例变了是否实现了接口。
package main
import "fmt"
type Shape interface {
Area() float64
}
type Circle struct {
Radius float64
}
func (circle *Circle) Area() float64 {
return 3.14 * circle.Radius * circle.Radius
}
func main() {
circle := &Circle{Radius: 5}
var shape Shape = circle // 将 Circle 类型的实例 circle 赋值给接口类型的变量 shape
fmt.Println("Area of circle:", shape.Area()) // Area of circle: 78.5
if sv, ok := shape.(Shape); ok {
fmt.Printf("shape implements Area(): %f\n", sv.Area()) // note: sv, not v
}
}
上述代码main()
中,使用sv, ok := shape.(Shape)
的类型断言,判断接口变量shape
绑定的实例类型(上述代码中的circle
)是否实现了括号中指定的Shape
接口,ok
表示布尔值来判断实例变量是否实现了接口。类型断言在后续会介绍到~
方法二:全局声明
package main
import "fmt"
type Shape interface {
Area() float64
}
type Circle struct {
Radius float64
}
func (circle *Circle) Area() float64 {
return 3.14 * circle.Radius * circle.Radius
}
var _ Shape = (*Circle)(nil) // 确保 Circle 实现了 Shape 接口.
通过上述var _ Shape = (*Circle)(nil)
的声明及初始化,可以通过编译器来帮助我们判断Circle
是否实现了Shape
接口~
如果上述代码中Circle没有实现Shape接口,则第17行则会报错:
Cannot use '(*Circle)(nil)' (type *Circle) as the type Shape Type does not implement 'Shape' as some methods are missing: Area() float64
上述报错提示*Circle
类型不能被用作 Shape
类型,因为 *Circle
类型并没有实现 Area() float64
方法。
接口的
静态类型
与动态类型
动态类型:接口绑定的具体实例的类型。接口可以绑定不同类型的实例,所以接口的动态类型随着其绑定的不同类型实例而变化。
静态类型:接口被定义时,类型已经被确定,即静态类型。静态类型的本职特征即为接口的方法签名集合。如果两个接口的方法签名集合相同(顺序可不同),则这两个接口在语义上等价,无需强制类型转换即可赋值,反之需要用到接口类型断言。
举个例子:
package main
import "fmt"
// Shape 接口在被定义时,静态类型已确定
type Shape interface {
Area() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14 * c.Radius * c.Radius
}
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func main() {
circle := Circle{Radius: 5}
rectangle := Rectangle{Width: 4, Height: 6}
var shape Shape
shape = circle // 动态类型为Circle
fmt.Printf("Dynamic type of shape: %T\n", shape)
shape = rectangle // 动态类型为Rectangle
fmt.Printf("Dynamic type of shape: %T\n", shape)
}
上述例子中,定义了一个接口 Shape
,它包含了一个方法 Area()
,用于计算形状的面积。
定义了两个具体类型 Circle
和 Rectangle
,它们分别实现了 Shape
接口的 Area()
方法。
在 main
函数中,创建了一个 Circle
类型的实例 circle
和一个 Rectangle
类型的实例 rectangle
。
随即声明了一个接口类型的变量 shape
,然后将 circle
赋值给 shape
。此时,接口 shape
的动态类型是 Circle
。
之后将 rectangle
赋值给 shape
。此时,接口 shape
的动态类型变为 Rectangle
。
最后使用 %T
格式化动态类型,并打印出结果。
输出结果将会是:
Dynamic type of shape: main.Circle
Dynamic type of shape: main.Rectangle
通过这个例子,我们可以看到,接口的动态类型随着其绑定的不同类型的实例而变化。当接口绑定了 Circle
类型的实例时,动态类型就是 Circle
,当接口绑定了 Rectangle
类型的实例时,动态类型就是 Rectangle
。并且Shape
接口在被定义时,其静态类型已确定下来。
三、接口的调用
接口方法的调用与普通函数不同,接口方法调用的最终地址是在运行期间决定的,具体类型变量circle
变量赋值给接口变量后,会使用circle
变量的方法指针初始化接口变量,当调用接口变量的方法时,实际上是间接地调用实例circle的方法。并且直接调用未初始化的接口变量的方法会产生panic,例如:
package main
import "fmt"
type Shaper interface {
Area() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14 * c.Radius * c.Radius
}
func main() {
var shaper Shaper
// 未初始化的接口变量,调用其方法会产生panic
shaper.Area() // panic: runtime error: invalid memory address or nil pointer dereference
// 必须初始化
shaper = Circle{Radius: 5}
fmt.Println(shaper.Area()) // 78.5
}
上述main()
中声明了一个接口变量shaper
,此时该接口变量shaper
未初始化,调用其方法时会产生panic: runtime error: invalid memory address or nil pointer dereference
。
在接口变量shaper
初始化后,调用接口变量的方法,则会间接调用实例circle
的方法。
四、接口的嵌套
在接口声明时,接口定义大括号内可以是方法声明的集合,也可以嵌入另一个接口类型匿名字段,同时二者可以混合。接口嵌入匿名接口字段,简单来说就是一个接口定义里面包括了其他接口,例如:
type Shaper interface {
Area() float64
}
type Shaper2 interface {
Shaper // 嵌入匿名接口字段
Perimeter() float64
}
上述接口Shaper2
与下面的接口声明是等价的~
type Shaper2 interface {
Area() float64
Perimeter() float64
}
五、类型断言与类型查询
1、类型断言
在Golang中,接口类型断言的语法形式为i.(TypeNname)
,其中i必须为接口变量,若i为具体类型变量,则编译器会报 on interface type xxx on left
错误。TypeNname
可以是接口类型名,也可以是具体类型名。
- 如果 TypeNname 是一个具体类型名,则类型断言用于判断接口变量绑定的实例类型是否就是具体类型 TypeNname。
- 如果 TypeName 是一个接口类型名,则类型断言用于判断接口变量绑定的实例类型
是否同时实现了 TypeName 接口。
Golang接口断言中,有两种语法表现,分别是:
第一种语法表现为:
o := i.(TypeName)
第二种语法表现为:
if o, ok := i.(TypeName); ok {
}
举个例子:
package main
import "fmt"
// Shaper 接口在被定义时,静态类型已确定
type Shaper interface {
Area() float64
}
// ShaperPlus 接口在被定义时,静态类型已确定
type ShaperPlus interface {
Shaper
Perimeter() float64
}
type Circle struct {
Radius float64
}
func (c *Circle) Area() float64 {
return 3.14 * c.Radius * c.Radius
}
func main() {
circle := &Circle{Radius: 5}
var i interface{} = circle
// 第一种语法表现[start]
// 代码块一:判断i绑定的实例是否实现了接口类型Shaper
o := i.(Shaper)
o.Area()
fmt.Printf("%T\n", i) // *main.Circle
fmt.Printf("%T\n", o) // *main.Circle
// 代码块二:如下语句会引 panic, 因为 i 没有实现接口ShaperPlus
// p := i.(ShaperPlus)
// p.Perimeter()
// 代码块三:判断i绑定的实例是是否是具体类型Circle
s := i.(*Circle)
fmt.Printf("%f\n", s.Radius)
fmt.Printf("%T\n", i) // *main.Circle
fmt.Printf("%T\n", s) // *main.Circle
// 第一种语法表现[end]
// 第二种语法表现[start]
// 代码块四:判断i绑定的实例是否实现了接口类型Shaper
if o, ok := i.(Shaper); ok {
o.Area()
}
// 代码块五:判断i绑定的实例是否实现了接口类型ShaperPlus
if p, ok := i.(ShaperPlus); ok {
// i没有实现接口ShaperPlus,所以此处不执行
p.Perimeter()
}
// 代码块六:判断i绑定的实例是否实现了接口类型Circle
if s, ok := i.(*Circle); ok {
fmt.Printf("%f\n", s.Radius)
}
// 第二种语法表现[end]
}
代码块一里o := i.(Shaper)
中TypeName
为接口类型名Shaper
,若接口变量i
绑定的实例类型实现了接口Shaper
,则变量o
的类型就是Shaper
,变量o
的值就是绑定接口的实例的副本,即变量i
的副本。
代码块二里p := i.(ShaperPlus)
中TypeName
为接口类型名ShaperPlus
,接口变量i
绑定的实例类型没有实现了接口ShaperPlus
,此时p.Perimeter()
会产生panic。
代码块三里s := i.(*Circle)
中TypeName
为具体类型名Circle
,若接口变量i
绑定的实例类型就是具体类型Circle
,则变量O
的类型就是Circle
,变量O
的值为接口变量i
绑定的实例值的副本。
代码块四、五、六则是第二种语法表现。第二种表现语法中,如果TypeName
是具体类型名,或者是接口类型名两种情况都不满足下,ok为false,此时变量o是TypeName
的零值
。
// 接口变量i没有实现ShaperPlus接口,ok为false
if p, ok := i.(ShaperPlus); !ok {
fmt.Printf("%T\n", p) // nil
}
2、类型查询
接口类型查询的语法形式为:
switch v := i.(type) {
case type1:
xxx
case type2:
xxx
default:
xxx
上述结构中,变量i必须为接口类型的变量,因为具体类型的实例是静态的,声明后不再变化,因此具体类型的变量不存在类型查询。
举个例子:
package main
import "fmt"
// Shaper 接口在被定义时,静态类型已确定
type Shaper interface {
Area() float64
}
// ShaperPlus 接口在被定义时,静态类型已确定
type ShaperPlus interface {
Shaper
Perimeter() float64
}
type Circle struct {
Radius float64
}
func (c *Circle) Area() float64 {
return 3.14 * c.Radius * c.Radius
}
func main() {
var i Shaper = &Circle{Radius: 5}
switch v := i.(type) {
case Shaper:
fmt.Printf("Shaper: %T\n", v)
case ShaperPlus:
fmt.Printf("ShaperPlus: %T\n", v)
case *Circle:
fmt.Printf("*Circle: %T\n", v)
case nil:
fmt.Printf("*Circle: %T\n", v)
default:
fmt.Println("default")
}
}
执行结果为:Shaper: *main.Circle
上述代码中,如果接口变量i
是未初始化的接口变量,则v
的值为nil
。
case 字句后面可以为非接口类型名*Circle
,也可以为接口类型名Shaper
、ShaperPlus
,匹配是按照 case 子句的顺序进行的。
如果 case 后面是一个接口类型名Shaper
,且接口变量绑定的实例类型实现了该接口类型的方法(上述代码中接口变量i
绑定的实例实现了Shaper
接口),则匹配成功,v
的类型是接口类型,其绑定的实例是i
绑定具体类型实例的副本。
如果 case 后面是一个具体类型名*Circle
,且接口变量绑定的实例类型就是该具体类型*Circle
(接口变量i
的声明以及初始化为var i Shaper = &Circle{Radius: 5}
),则匹配成功,此时v
就是该具体类型变量,v
的值就是i
绑定的实例值的副本。
如果 case 后面跟着多个用逗号隔开的类型,例如case *Circle, Shaper:
,此时接口变量绑定的实例类型只需要满足其中一个即可匹配成功。
如果所有的case都不满足,则执行default
语句。
fallthrough
语句不能在 Type Switch
语句中使用。
六、空接口interface{}
空接口的方法集为空,所以任意类型都被认为实现了空接口,任意类型的实例都可以赋值或传递给空接口。
func main() {
var variableInt interface{} = 10
fmt.Println(variableInt) // 10
var variableFloat interface{} = 10.24
fmt.Println(variableFloat) // 10.24
var variableBool interface{} = true
fmt.Println(variableBool) // true
var variableStr interface{} = "string"
fmt.Println(variableStr) // string
}
Go 语言没有泛型,如果一个函数需要接收任意类型的参数, 参数类型可以使用空接口类型。
空接口有两个字段,一个是实例类型,另一个是指向绑定实例的指针,只有两个都为nil
,空接口才为nil
举个例子:
package main
import "fmt"
type Shaper interface {
Area() float64
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14 * c.Radius * c.Radius
}
func main() {
var circle *Circle = nil
var shaper Shaper = circle
fmt.Printf("%p\n", circle) // 0x0
fmt.Printf("%p\n", shaper) // 0x0
fmt.Println(shaper == nil) // false
shaper.Area() // panic
}
上述main()
中,fmt.Printf("%p\n", shaper)
的结果为0x0
,即指向绑定实例circle
的指针不为nil
,因此fmt.Println(shaper == nil)
的结果为false
,印证了空接口只有两个字段实例类型
和指向绑定实例的指针
只有两个都为nil
,空接口才为nil
的理论。
shaper.Area()
的执行结果为panic,是因为Area方法的接收值为Circle
值类型,而circle
为nil
,从而无法获取到指针所指的对象值而导致的panic
。若为接收值指针类型,则可以正常执行。
package main
import "fmt"
type Shaper interface {
Area() float64
}
type Circle struct {
Radius float64
}
func (c *Circle) Area() float64 {
fmt.Printf("%p\n", c) // OxO
fmt.Println(c) // nil
return 1 // 避免报错
}
func main() {
var circle *Circle = nil
var shaper Shaper = circle
shaper.Area()
}
七、Go中的面向对象
Go 没有类,而是松耦合的类型、方法对接口的实现。
OO 语言最重要的三个方面分别是:封装、继承、多态,在 Go 中它们是怎样表现的呢?
封装(数据隐藏):和别其他面向对象语言有 4 个或更多的访问层次相比,Go 把它简化为了 2 层
-
包范围内的:通过标识符首字母小写,只在它所在的包内可见;
-
可导出的:通过标识符首字母大写,对所在包以外也可见;
继承:用组合实现:内嵌一个(或多个)包含想要的行为(字段和方法)的类型;多重继承可以通过内嵌多个类型实现。
多态:用接口实现:某个类型的实例可以赋给它所实现的任意接口类型的变量。类型和接口是松耦合的,并且多重继承可以通过实现多个接口实现。Go 接口不是 Java 接口的变体,而且接口间是不相关的,并且是大规模编程和可适应的演进型设计的关键。
八、总结
本篇总七个角度介绍了Go的接口,Go 接口的设计和使用方式使得其非常灵活,它可以用于实现类似于面向对象编程中的多态和抽象类等特性。
Go 接口是一种类型,它定义了一组方法签名。
接口类型的变量可以存储任何实现了该接口的具体类型的值,实现接口需要通过实现接口签名中的所有方法来完成。
接口之间可以进行嵌套,形成更复杂的接口类型。
空接口 interface{}
可以接受任何类型的值,即用于传递任意类型的值,常用于函数需要接收任意类型的参数,
接口的类型断言可以用于检查接口变量是否实现了某个接口或是某个具体类型,也可以通过类型查询来执行对应的逻辑。
接口变量的值可以是 nil,这时调用它的方法值方法会引发运行时错误,调用指针方法则不会,但需要特别注意,指针指向的是nil,在方法内随意使用可能会引发panic。
在 Go 编程中,接口的使用频率非常高,因此对于接口的理解和使用是非常重要的。