【golang学习笔记】接口(interface)


接口本身是调用方和实现方均需要遵守的一种协议,大家按照统一的方法命名参数类型和数量来协调逻辑处理的过程。

Go语言中使用组合来实现对象特性的描述,对象的内部使用结构体内嵌组合对象应该具有的特性,对外通过接口暴露能使用的特性。

Go 语言的接口设计是非侵入式的,接口编写者无须知道接口被哪些类型实现。而接口实现者只需知道实现的是什么样子的接口,但无须指明实现哪一个接口。编译器知道最终编译时使用哪个类型实现哪个接口,或者接口应该由谁来实现

非侵入式设计是Go语言设计师经过多年的大项目经验 总结出来的设计之道。只有让接口和实现者真正解耦,编译速度才能真正提高,项目之间的耦合度也会降低。

声明接口

接口是双方约定的一种合作协议。接口实现者不需要关心接口会被怎样使用,调用者也不需要关心接口的实现细节。接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构。

接口声明的格式

每个接口类型由数个方法组成,格式如下:


type 接口类型名 interface{
    
    
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2}

对各个部分的说明:

  • 接口类型名:使用 type 将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加 er,如有写操作的接口叫 Writer,有字符串功能的接口叫 Stringer,有关闭功能的接口叫 Closer 等。
  • 方法名:当方法名首字母是大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以被忽略,例如:
type writer interface{
    
    
    Write([]byte) error
}

开发中常见的接口及写法

Go语言提供的很多包中都有接口,例如 io 包中提供的 Writer 接口:

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

这个接口可以调用 Write() 方法写入一个字节数组([]byte),返回值告知写入字节数(n int)和可能发生的错误(err error)。

类似的,还有将一个对象以字符串形式展现的接口,只要实现了这个接口的类型,在调用 String() 方法时,都可以获得对象对应的字符串。在 fmt 包中定义如下:

type Stringer interface {
    
    
    String() string
}

Stringer 接口在Go语言中的使用频率非常高,功能类似于java语言里的 ToString 的操作。

Go语言的每个接口中的方法数量不会很多。Go语言希望通过一个接口精准描述它自己的功能,而通过多个接口的嵌入和组合的方式将简单的接口扩展为复杂的接口。本章后面的小节中会介绍如何使用组合来扩充接口

实现接口的条件

如果一个任意类型T的方法集为一个借口类型的方法集的超集,则我们说类型T实现了此借口类型。T可以是一个非接口类型,也可以是一个接口类型。

接口定义后 ,需要实现接口,调用方才能正确编译并使用接口。接口的实现需要遵循两条规则才能让接口可用

条件一:接口的方法与实现接口的类型方法格式一致


package main

import (
   "fmt"
)

// 定义一个数据写入器

type DataWriter interface {
    
    
   WriteData(data interface{
    
    }) error
}

// 定义文件结构,用于实现DataWriter
type file struct {
    
    
}

// 实现DataWriter接口的WriteData方法

func (d *file) WriteData(data interface{
    
    }) error {
    
    

   // 模拟写入数据
   fmt.Println("WriteData:", data)
   return nil
}

func main() {
    
    

   // 实例化file
   f := new(file)

   // 声明一个DataWriter的接口
   var writer DataWriter

   // 将接口赋值f,也就是*file类型
   writer = f

   // 使用DataWriter接口进行数据写入
   err := writer.WriteData("data")
   if err != nil {
    
    
      return
   }
}

输出:

WriteData: data

当类型无法实现接口的时候,编译器会报错,比如下面的例子:

  1. 函数名不一致导致的报错
    比如上面的代码,修改file结构的WriteData()方法名

func (d *file) WriteDataX(data interface{
    
    }) error {
    
    

   // 模拟写入数据
   fmt.Println("WriteData:", data)
   return nil
}

报错:

./main.go:35:11: cannot use f (variable of type *file) as type DataWriter in assignment:
	*file does not implement DataWriter (missing WriteData method)
  1. 实现接口的方法签名不一致导致的报错

将参数类型改为int

代码如下:


func (d *file) WriteData(data int) error {
    
    

编译代码,报错:

cannot use f (variable of type *file) as type DataWriter in assignment:
	*file does not implement DataWriter (wrong type for WriteData method)
		have WriteData(data int) error
		want WriteData(data interface{}) error

这种方式的报错就是由实现者的方法签名与接口的方法签名不一致导致的

条件二:接口中所有方法均被实现

只有实现接口中的所有方法,接口才能正确编译并被使用

在上面的代码中接口中添加一个方法:


// CanWrite can write

CanWrite() bool

此时编译报错:

./main.go:38:11: cannot use f (variable of type *file) as type DataWriter in assignment:
	*file does not implement DataWriter (missing CanWrite method)

需要在 file 中实现 CanWrite() 方法才能正常使用 DataWriter()。

Go语言的接口实现是隐式的,无须让实现接口的类型写出实现了哪些接口。这个设计被称为非侵入式设计。

实现者在编写方法时,无法预测未来哪些方法会变为接口。一旦某个接口创建出来,要求旧的代码来实现这个接口时,就需要修改旧的代码的派生部分,这一般会造成雪崩式的重新编译

传统的派生式接口及类关系构建的模式,让类型间拥有强耦合的父子关系。这种关系一般会以“类派生图”的方式进行。经常可以看到大型软件极为复杂的派生树。随着系统的功能不断增加,这棵“派生树”会变得越来越复杂。对于Go语言来说,非侵入式设计让实现者的所有类型均是平行的、组合的。如何组合则留到使用者编译时再确认。因此,使用GO语言时,不需要同时也不可能有“类派生图”,开发者唯一需要关注的就是“我需要什么?”,以及“我能实现什么?”

类型与接口的关系

类型和接口之间有一对多和多对一的关系。

  • 一个类型可以实现多个接口

一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现

  • 多个类型可以实现相同的接口

一个接口的方法,不一定需要由一个类型实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。
使用者不关系某个接口的方法是通过一个类型完全实现的,还是通过多个结构嵌入到一个结构体中拼凑起来共同实现的。

类型断言

类型断言(Type Assertion)是一个使用在接口值上的操作,用于检查接口类型变量所持有的值是否实现了期望的接口或者具体的类型

在Go语言中类型断言的语法格式如下:

value, ok := x.(T)

其中,x 表示一个接口的类型,T 表示一个具体的类型(也可为接口类型)

该断言表达式会返回 x 的值(也就是 value)和一个布尔值(也就是 ok),可根据该布尔值判断 x 是否为 T 类型:

  • 如果 T 是具体某个类型,类型断言会检查 x 的动态类型是否等于具体类型 T。如果检查成功,类型断言返回的结果是 x 的动态值,其类型是 T。
  • 如果 T 是接口类型,类型断言会检查 x 的动态类型是否满足 T。如果检查成功,x 的动态值不会被提取,返回值是一个类型为 T 的接口值。
  • 无论 T 是什么类型,如果 x 是 nil 接口值,类型断言都会失败

示例:便于扩展输出方式的日志系统

日志可以用于查看和分析应用程序的运行状态。日志一般可以支持输出多种形式,如命名行、文件、网络等

  1. 日志对外接口

定义一个日志写入器接口(LogWrite),要求写入设备必须遵守这个接口协议才能被日志器(Logger)注册。日志器有一个写入器的注册方法(Logger的RegisterWrite()方法)。
还有一个Log()方法,进行日志的输出,这个函数会将日志写入到所有已经注册的日志写入器(LogWrite)中

代码:

package main

//LogWriter 声明日志写入器接口
type LogWriter interface {
    
    
   Write(data interface{
    
    }) error
}

//Logger 日志器
type Logger struct {
    
    
   //这个日志器用到的日志写入器
   writerList []LogWriter
}

// RegisterWriter 日志写入器
func (l *Logger) RegisterWriter(writer LogWriter) {
    
    
   l.writerList = append(l.writerList, writer)
}

// Log 将一个data类型的数据写入日志
func (l *Logger) Log(data interface{
    
    }) {
    
    
   //遍历所有注册的写入器
   for _, writer := range l.writerList {
    
    
      //将日志输出到写入器
      writer.Write(data)
   }
}

// NewLogger 创建日志器实例
func NewLogger() *Logger {
    
    
   return &Logger{
    
    }
}
  1. 文件写入器

文件写入器是众多日志写入器中的一种。文件写入器的功能是根据一个文件名创建日志文件(fileWrite的SetFile方法)。再有日志写入时,将日志写入文件。

package main

import (
   "errors"
   "fmt"
   "os"
)

//声明文件写入器
type fileWriter struct {
    
    
   file *os.File
}

// SetFile 设置文件写入器写入文件名
func (f *fileWriter) SetFile(filename string) (err error)  {
    
    
   //如果一个文件已经打开,关闭前一个文件
   if f.file !=nil {
    
    
      err := f.file.Close()
      if err != nil {
    
    
         return err
      }
   }
   //创建一个文件并保存文件句柄
   f.file,err = os.Create(filename)
   //如果创建的过程中出现错误,则返回错误
   return err
}

//实现logwrite的write方法
func (f *fileWriter) Write(data interface{
    
    }) error  {
    
    
   //日志文件没有创建成功
   if f.file==nil{
    
    
      return errors.New("file not created")
   }
   //将数据序列化为字符串
   str := fmt.Sprintf("%v\n",data)
   //将数据以字节数组写入文件中
   _,err := f.file.Write([]byte(str))
   return err
}

//创建文件写入器实例
func newFileWriter() *fileWriter {
    
    
   return &fileWriter{
    
    }
}

在操作文件时,会出现文件无法创建、无法写入等错误。开发中尽量不要忽略这些错误,应该处理可能发生的所有错误
文件使用完,要注意使用os.file的close()方法进行及时关闭,否则文件再次访问时会因为其属性出现无法读取、无法写入的错误。

  1. 命令行写入
    4 . 使用日志
    。。。。

接口的嵌套组合

接口与接口嵌套形成了新接口,只有接口的所有方法被实现,则这个接口中的所有嵌套接口方法均可以被调用

系统包中的接口嵌套组合

Go语言的 io 包中定义了写入器(Writer)、关闭器(Closer)和写入关闭器(WriteCloser)3 个接口,代码如下:

type Writer interface {
    
    
//定义了写入器(Writer),如这个接口较为常用,常用于 I/O 设备的数据写入
    Write(p []byte) (n int, err error)
}
//定义了关闭器(Closer),如有非托管内存资源的对象,需要用关闭的方法来实现资源释放
type Closer interface {
    
    
    Close() error
}

//定义了写入关闭器(WriteCloser),这个接口由 Writer 和 Closer 两个接口嵌入。也就是说,WriteCloser 同时拥有了 Writer 和 Closer 的特性
type WriteCloser interface {
    
    
    Writer
    Closer
}

在接口和类型见转换

Go语言中使用接口断言(type assertions)将接口转换成另外一个接口,也可以将接口转换为另外的类型

类型断言的格式

类型断言是一个使用在接口值上的操作。语法上它看起来像 i.(T) 被称为断言类型,这里 i 表示一个接口的类型和 T 表示一个类型。一个类型断言检查它操作对象的动态类型是否和断言的类型匹配。

类型断言的基本格式如下:

t := i.(T)
  • i 代表接口变量
  • T 代表转换的目标类型
  • t 代表转换后的变量

如果i没有完成实现T接口的方法,这个语句会触发宕机。触发宕机不是很友好,因此上面的语句还有另一种写法:

t,ok := i.(T)

如果发生接口未实现时,将会把ok置为false ,t置为T类型的0值。
这里的ok可以理解为是:i接口是否实现T类型的结果。

将接口转换为其他接口

实现了某个接口的类型同时实现了另一个接口,此时可以在两个接口间转换。

即将T改为你想要转换的接口接口即可。

实现某个接口的类型同时实现了另外一个接口,此时可以在两个接口间转换。

鸟和猪具有不同的特性,鸟可以飞,猪不能飞,但两种动物都可以行走。如果使用结构体实现鸟和猪,让它们具备自己特性的 Fly() 和 Walk() 方法就让鸟和猪各自实现了飞行动物接口(Flyer)和行走动物接口(Walker)。

将鸟和猪的实例创建后,被保存到 interface{} 类型的 map 中。interface{} 类型表示空接口,意思就是这种接口可以保存为任意类型。对保存有鸟或猪的实例的 interface{} 变量进行断言操作,如果断言对象是断言指定的类型,则返回转换为断言对象类型的接口;如果不是指定的断言类型时,断言的第二个参数将返回 false。例如下面的代码:

var obj interface = new(bird)
f, isFlyer := obj.(Flyer)

代码中,new(bird) 产生 *bird 类型的 bird 实例,这个实例被保存在 interface{} 类型的 obj 变量中。使用 obj.(Flyer) 类型断言,将 obj 转换为 Flyer 接口。f 为转换成功时的 Flyer 接口类型,isFlyer 表示是否转换成功,类型就是 bool。

下面是详细的代码(代码1):

package main

import "fmt"

// 定义飞行动物接口
type Flyer interface {
    
    
    Fly()
}

// 定义行走动物接口
type Walker interface {
    
    
    Walk()
}

// 定义鸟类
type bird struct {
    
    
}

// 实现飞行动物接口
func (b *bird) Fly() {
    
    
    fmt.Println("bird: fly")
}

// 为鸟添加Walk()方法, 实现行走动物接口
func (b *bird) Walk() {
    
    
    fmt.Println("bird: walk")
}

// 定义猪
type pig struct {
    
    
}

// 为猪添加Walk()方法, 实现行走动物接口
func (p *pig) Walk() {
    
    
    fmt.Println("pig: walk")
}

func main() {
    
    

// 创建动物的名字到实例的映射 map,映射对象名字和对象实例,实例是鸟和猪。
    animals := map[string]interface{
    
    }{
    
    
        "bird": new(bird),
        "pig":  new(pig),
    }

    // 遍历映射 开始遍历 map,obj 为 interface{} 接口类型。
    for name, obj := range animals {
    
    

        // 判断对象是否为飞行动物 使用类型断言获得 f,类型为 Flyer 及 isFlyer 的断言成功的判定。
        f, isFlyer := obj.(Flyer)
        // 判断对象是否为行走动物 使用类型断言获得 w,类型为 Walker 及 isWalker 的断言成功的判定。
        w, isWalker := obj.(Walker)

        fmt.Printf("name: %s isFlyer: %v isWalker: %v\n", name, isFlyer, isWalker)

//根据飞行动物和行走动物两者是否断言成功,调用其接口
        // 如果是飞行动物则调用飞行动物接口
        if isFlyer {
    
    
            f.Fly()
        }

        // 如果是行走动物则调用行走动物接口
        if isWalker {
    
    
            w.Walk()
        }
    }
}

代码输出如下:

name: pig isFlyer: false isWalker: true
pig: walk
name: bird isFlyer: true isWalker: true
bird: fly
bird: walk

将接口转换为其他类型

 p1 := new(pig)
//p1实现了接口 所以可以进行转换
 var a Walker = p1
 //转换为 * pig类型
 p2 := a.(*pig)
 
 fmt.Printf("p1=%p p2=%p", p1, p2)

空接口类型(interface{})–能保存所有值的类型

空接口是接口类型的特殊形式,空接口没有任何方法,因此任何类型都无须实现空接口。从实现的角度看,任何值都满足这个接口的需求。因此空接口类型可以保存任何值,也可以从空接口中取出原值。
提示

空接口类型类似于 Java语言中的 Object、C语言中的 void*、在泛型和模板出现前,空接口是一种非常灵活的数据抽象保存和使用的方法。
空接口的内部实现保存了对象的类型和指针。使用空接口保存一个数据的过程会比直接用数据对应类型的变量保存稍慢。因此在开发中,应在需要的地方使用空接口,而不是在所有地方使用空接口。

将值保存到空接口

空接口的赋值如下:

//声明 any 为 interface{} 类型的变量
var any interface{
    
    }
//为 any 赋值一个整型 1
any = 1
fmt.Println(any) //打印 any 的值,提供给 fmt.Println 的类型依然是 interface{}
//为 any 赋值一个字符串 hello。此时 any 内部保存了一个字符串。但类型依然是 interface{}
any = "hello"
fmt.Println(any)

any = false
fmt.Println(any)

代码输出如下:

1
hello
false

从空接口获取值

保存到空接口的值,如果直接取出指定类型的值时,会发生编译错误,代码如下:

// 声明a变量, 类型int, 初始值为1
var a int = 1

// 声明i变量, 类型为interface{}, 初始值为a, 此时i的值变为1
var i interface{
    
    } = a

// 声明b变量, 尝试赋值i
var b int = i

第8行代码编译报错:

cannot use i (type interface {}) as type int in assignment: need type assertion

编译器告诉我们,不能将i变量视为int类型赋值给b。

在代码第 5 行中,将 a 的值赋值给 i 时,虽然 i 在赋值完成后的内部值为 int,但 i 还是一个 interface{} 类型的变量。类似于无论集装箱装的是茶叶还是烟草,集装箱依然是金属做的,不会因为所装物的类型改变而改变。

为了让第 8 行的操作能够完成,编译器提示我们得使用 type assertion,意思就是类型断言。

使用类型断言修改第 8 行代码如下:

var b int = i.(int)

修改后,代码可以编译通过,并且 b 可以获得 i 变量保存的 a 变量的值:

空接口的值比较

空接口在保存不同的值后,可以和其他变量值一样使用 == 进行比较操作。空接口的比较有以下几种特性。

  1. 类型不同的空接口间的比较结果不相同

保存有类型不同的值的空接口进行比较时,Go语言会优先比较值的类型。因此类型不同,比较结果也是不相同的,代码如下:

// a保存整型
var a interface{
    
    } = 100

// b保存字符串
var b interface{
    
    } = "hi"

// 两个空接口不相等
fmt.Println(a == b)

代码输出如下:false
2. 不能比较空接口中的动态值

当接口中保存有动态类型的值时,运行时将触发错误,代码如下:

// c保存包含10的整型切片
var c interface{
    
    } = []int{
    
    10}

// d保存包含20的整型切片
var d interface{
    
    } = []int{
    
    20}

// 这里会发生崩溃
fmt.Println(c == d)

代码运行到第8行时发生崩溃:panic: runtime error: comparing uncomparable type []int
这是一个运行时错误,提示 []int 是不可比较的类型。下表中列举出了类型及比较的几种情况。

类型的可比较性
|类 型 |说 明 |
|:–|
|map |宕机错误,不可比较 |
|切片([]T) |宕机错误,不可比较 |
|通道(channel) |可比较,必须由同一个 make 生成,也就是同一个通道才会是 true,否则为 false |
|数组([容量]T) |可比较,编译期知道两个数组是否一致 |
|结构体 |可比较,可以逐个比较结构体的值 |
|函数 |可比较 |

类型断言的书写格式

switch 实现类型分支时的写法格式如下

switch 接口变量.(type) {
    
    
    case 类型1:
        // 变量是类型1时的处理
    case 类型2:
        // 变量是类型2时的处理default:
        // 变量不是所有case中列举的类型时的处理
}

对各个部分的说明:

  • 接口变量:表示需要判断的接口类型的变量。
  • 类型1、类型2……:表示接口变量可能具有的类型列表,满足时,会指定 case 对应的分支进行处理

猜你喜欢

转载自blog.csdn.net/qq_45795744/article/details/125765659