【Golang从零开始的修炼之路】Go接口的那些事~

简单介绍


本篇是在学习Go的过程中,对Go接口的一个总结,文中若有不对之处,欢迎大家评论留言一起讨论,一起学习Go~~~

一、接口简介


Go中的接口 是一组方法签名的集合,且Go接口的设计是非侵入式 ,一个具体类型(比如结构体strcut)实现接口不需要在语法上显示地声明,只需要具体类型的实现方法集是囊括了接口的方法集,就代表该类型实现了接口,即一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。

二、接口的声明及初始化


1、接口的声明

接口的声明格式如下:

go 复制代码
type Namer interface {
    Method1(param_list) return_type
    Method2(param_list) return_type
    ...
}

声明新接口类型的可以遵从以下特点:

  • 接口的命名一般以"er"结尾;
  • 接口定义的内部方法声明不需要func引导;
  • 在接口定义中,只有方法声明,并没有方法的具体实现;

举个例子:

go 复制代码
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

2、接口的初始化

Go接口在初始化方面,有两种方式:

(1)实例赋值接口

一个具体类型实现了接口中的所有方法,则该具体类型实现该接口,可以将该具体类型的实例直接赋值给接口类型的变量,直接上代码~

go 复制代码
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接口变量均已实现。

go 复制代码
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())
}

执行结果

text 复制代码
Area of shape: 78.5
Area of shape2: 78.5
Perimeter of shape2: 31.400000000000002

没有初始化的接口变量,其默认值是nil

go 复制代码
package main

import "fmt"

type Shaper interface {
    Area() float64
}

func main() {
    var shaper Shaper
    fmt.Println(shaper) // <nil>
}

测试一个实例是否实现了某个接口的小方法~

方法一:通过使用类型断言,可以判断出一个实例变了是否实现了接口。

go 复制代码
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表示布尔值来判断实例变量是否实现了接口。类型断言在后续会介绍到~

方法二:全局声明

go 复制代码
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 方法。

接口的静态类型动态类型

动态类型:接口绑定的具体实例的类型。接口可以绑定不同类型的实例,所以接口的动态类型随着其绑定的不同类型实例而变化。
静态类型:接口被定义时,类型已经被确定,即静态类型。静态类型的本职特征即为接口的方法签名集合。如果两个接口的方法签名集合相同(顺序可不同),则这两个接口在语义上等价,无需强制类型转换即可赋值,反之需要用到接口类型断言。

举个例子:

go 复制代码
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(),用于计算形状的面积。

定义了两个具体类型 CircleRectangle,它们分别实现了 Shape 接口的 Area() 方法。

main 函数中,创建了一个 Circle 类型的实例 circle 和一个 Rectangle 类型的实例 rectangle

随即声明了一个接口类型的变量 shape,然后将 circle 赋值给 shape。此时,接口 shape 的动态类型是 Circle

之后将 rectangle 赋值给 shape。此时,接口 shape 的动态类型变为 Rectangle

最后使用 %T 格式化动态类型,并打印出结果。

输出结果将会是:

go 复制代码
Dynamic type of shape: main.Circle
Dynamic type of shape: main.Rectangle

通过这个例子,我们可以看到,接口的动态类型随着其绑定的不同类型的实例而变化。当接口绑定了 Circle 类型的实例时,动态类型就是 Circle,当接口绑定了 Rectangle 类型的实例时,动态类型就是 Rectangle。并且Shape接口在被定义时,其静态类型已确定下来。

三、接口的调用


接口方法的调用与普通函数不同,接口方法调用的最终地址是在运行期间决定的 ,具体类型变量circle变量赋值给接口变量后,会使用circle变量的方法指针初始化接口变量,当调用接口变量的方法时,实际上是间接地调用实例circle的方法 。并且直接调用未初始化的接口变量的方法会产生panic,例如:

go 复制代码
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的方法。

四、接口的嵌套


在接口声明时,接口定义大括号内可以是方法声明的集合,也可以嵌入另一个接口类型匿名字段,同时二者可以混合。接口嵌入匿名接口字段,简单来说就是一个接口定义里面包括了其他接口,例如:

go 复制代码
type Shaper interface {
    Area() float64
}

type Shaper2 interface {
    Shaper // 嵌入匿名接口字段
    Perimeter() float64
}

上述接口Shaper2与下面的接口声明是等价的~

go 复制代码
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接口断言中,有两种语法表现,分别是:

第一种语法表现为:

go 复制代码
o := i.(TypeName)

第二种语法表现为:

go 复制代码
if o, ok := i.(TypeName); ok {
}

举个例子:

go 复制代码
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零值

go 复制代码
// 接口变量i没有实现ShaperPlus接口,ok为false
if p, ok := i.(ShaperPlus); !ok {
    fmt.Printf("%T\n", p) // nil
}

2、类型查询

接口类型查询的语法形式为:

go 复制代码
switch v := i.(type) {
case type1:
    xxx
case type2:
    xxx
default:
    xxx

上述结构中,变量i必须为接口类型的变量,因为具体类型的实例是静态的,声明后不再变化,因此具体类型的变量不存在类型查询。

举个例子:

go 复制代码
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 ,也可以为接口类型名 ShaperShaperPlus,匹配是按照 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{}


空接口的方法集为空,所以任意类型都被认为实现了空接口任意类型的实例都可以赋值或传递给空接口

go 复制代码
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

举个例子:

go 复制代码
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值类型,而circlenil,从而无法获取到指针所指的对象值而导致的panic。若为接收值指针类型,则可以正常执行。

go 复制代码
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 编程中,接口的使用频率非常高,因此对于接口的理解和使用是非常重要的。

相关推荐
不爱说话郭德纲15 小时前
聚焦 Go 语言框架,探索创新实践过程
go·编程语言
0x派大星2 天前
【Golang】——Gin 框架中的 API 请求处理与 JSON 数据绑定
开发语言·后端·golang·go·json·gin
IT书架2 天前
golang高频面试真题
面试·go
郝同学的测开笔记2 天前
云原生探索系列(十四):Go 语言panic、defer以及recover函数
后端·云原生·go
秋落风声3 天前
【滑动窗口入门篇】
java·算法·leetcode·go·哈希表
0x派大星4 天前
【Golang】——Gin 框架中的模板渲染详解
开发语言·后端·golang·go·gin
0x派大星5 天前
【Golang】——Gin 框架中的表单处理与数据绑定
开发语言·后端·golang·go·gin
三里清风_6 天前
如何使用Casbin设计后台权限管理系统
golang·go·casbin
0x派大星6 天前
【Goland】——Gin 框架中间件详解:从基础到实战
开发语言·后端·中间件·golang·go·gin
0x派大星6 天前
【Goland】——Gin 框架简介与安装
后端·golang·go·gin