Go学习:接口

接口

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

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

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

7.1 声明接口

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

7.1.1 接口的声明形式

每个接口由数个方法组成。接口的形式代码如下:

type 接口类型名 interface{
	方法名1(参数列表) 返回值列表
	方法名2(参数列表) 返回值列表
    ...
}
  • 接口类型名:使用 type 将接口定义为 自定义的类型名。 Go 语言 在命名 时,一般会在单词后面添 er ,如写操作的接口 Writer, 有字符串功能的接口Stringer 等。

  • 方法名: 当方法名首字母是大写,且这个类型名首字母也是大写这个方法可以被接口所在的包( package )之外的代码访问

  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以被忽略,例如:

    type Writer interface{
    	Write([]byte) error
    }
    

7.2 实现接口的条件

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

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

在类型中添加与接口签名一致的方法就可以实现该方法。签名包括方法中的名称、参数列表、返回参数列表。也就是说任意一项与接口要实现的方法不一致,那么接口的这个方法就不会实现。

这里使用 file 结构体实现Data Writer 接口的 WriteData 法,方法内部只是打印日志,表示有数据写入,详细实现过程请参考代码下面:

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

type file struct {
}

//实现DataWrite接口的writeData方法
func (d *file) WriteData(data interface{}) error {
	fmt.Println("WriteData:", data)
	return nil
}
func main() {
	//实例化file
	f := new(file)
	var writer DataWriter = f //声明一个DataWriter的接口
	writer.WriteData("data")  //使用DataWriter接口进行数据写入
}

本例的调用关系如下图所示:
接口调用关系图

当类型无法实现接口时,编译器会报错,下面列出常见的几种接口无法实现的错误。

  1. 函数名不一致导致的错误

    代码 7.2.1 上尝试修改部分代码, 编译错误,通过编译器的报错理解如何实现接口的方法。首先,修改 file结构的 WriteData()方法名 ,将这个签名修改为:

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

    编译代码, 报错:

    *cannot use f (type file ) as type DataWriter in assignment :

    *file does not implement DataWriter (missing WriteData method)

    错误含义 :不能将f变量(类型*file)视为 DataWriter进行赋值。原因 :*file 类型未实现DataWriter (丢失 WriteData 方法)。

  2. 实现接口的方法签名不一致导致的错误

    将修改的代码恢复后,再尝试修 WriteData()方法,把 data 参数类型从interface{}修改为int类型,代码如下:

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

    这次未实现 DataWriter 的理由变为(错误的 WriteData()方法类型〉发现WriteData(int)error ,期望 WriteData(interface {})error

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

当一个接口中多个方法时,只有这些方法都被实现了,接口才能正确编译并使用

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

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

7.3 理解类型与接口的关系

类型与接口间有一对多和多对一的关系,下面将列举出这些常见的概念,以方便理解接口与类型在复杂环境下的实现关系。

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

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

把Socket能够写入数据和需要关闭的特性使用接口描述,请参考下面代码:

type Socket struct{}

func (s *Socket) Write (p []byte) (n int, e error) {
    return 0, nil
}
func (s *Socket) Close() error {
    return nil
}

Socket结构的Write()方法实现io.Writer接口:

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

同时,Socket结构实现了io.Closer接口:

type Closer interface{
	Close() error
}

代码中使用 Socket 结构实现的 Writer 接口和 Closer 接口代码如下:

// 使用io.Writer的代码,并不知道Socket和io.Closer的存在
func usingWriter(writer io.Writer){
 	writer.Write(nil)
}
//
func usingCloser(closer io.Closer){
 	closer.Close()
}
func main() {
    s := new(Socket)
    usingWriter(s)
    usingCloser(s)
}

usingWriter() 和 usingCloser() 完全独立 ,互相不知道对方存在,也不知道自己使用的接口是 Socket实现的。

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

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

Service 接口定义了两个方法 :一个是开启服务的方法( Start() ), 一个是输出日志的方法( Log() ) 。使用 GameService 构体来实 Service, GameService 自己的结构只能实现Start() 方法, Service 接口中的 Log()方法己经被一个能输出日志的日志器 Logger 实现了,无须再进行 GameService 封装,或者重新实现一遍。 所以,选择将 Logger 嵌入GameService 最大程度地避免代码冗余,简化代码结构 。详细实现过程如下:

// 一个服务需要满足能开启和写日志的功能
type Service interface{
    Start()
    Log(string)
}
//日志器
type Logger struct{}
//实现Service的Log()方法
func (g *Logger) Log(l string) {
}
type GameService struct{
    Logger
}
//实现Service的Start()方法
function (g *GameService) Start() {    
}

func main() {
    var s Service = new(GameService) //实例化GameService并赋予Service
    // s就可以调用Start()和Log()方法
    s.Start()
    s.Log("hello")
}

7.4 接口的嵌套

在Go语言中,不仅结构体之间可以嵌套,接口间也可以嵌套创造出新的接口。只要新接口的所有方法被是实现,则这个接口的所有嵌套接口的方法均可以被调用。

  1. 系统包中的接口嵌套组合

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

    type Writer interface{
    	Write(p []byte) (n int, e error)
    }
    type Closer interface{
    	Close() error
    }
    type WriteCloser interface{	//由Writer和Closer嵌入,同时拥有两种特性
    	Writer
    	Closer
    }
    
  2. 在代码中使用接口嵌套组合

    在代码中使用io.Writer、io.Closer和io.WriteCloser这3个接口时,只需要按照接口实现的规则实现io.Writer接口和io.Closer接口即可。而io.WriteCloser接口在使用时,编译器会根据接口的实现这确认它们是否同时实现了io.Writer和io.Closer接口,详细实现代码如下:

    //声明一个设备结构
    type Device struct{
    }
    //实现io.Writer的Write方法
    func (d *Device) Write(p []byte) (n int, e error) {
    	return 0, nil
    }
    //实现io.Closer的Close方法
    func (d *Device) Close() error {
    	return nil
    }
    func main() {
        var wc io.WriteCloser = new(Device) //声明写入关闭器,并赋予Device的实例
        wc.Write(nil)	//写入数据
        wc.Close()		//关闭设备
        //声明写入器,并赋予Device的新实例
        var writeOnly io.Writer = new(Device)
        writeOnly.Write(nil)
    }
    

7.5 在接口在类型间转换

Go语言中使用**接口断言(type assertions)**将接口转换成另外一个接口,也可将接口转换为另外的类型。转换在开发中非常常见,使用也非常频繁。

7.5.1 类型断言格式

基本格式如下:

t := i.(T)

  • i表示接口变量
  • T表示转换的目标类型
  • t代表转换后的变量

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

t,ok := i.(T)

这种写法,如果发生接口为实现时,将会把ok置位false,t置为T类型的0值。正常实现时,ok为true。

7.5.2 将接口转换为其他接口

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

鸟和猪具有不同的特性,鸟可以飞,猪不能飞,但两种动物都可以行走。如果使用结构体实现鸟和猪,让它们具备自己特性的 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 。详细代码请参考如下:

type Flyer interface {
	Fly()
}

type Walker interface {
	Walk()
}

type bird struct {
}
type pig struct {
}

func (b *bird) Fly() {
	fmt.Println("bird: fly")
}

func (b *bird) Walk() {
	fmt.Println("bird: walk")
}

func (p *pig) Walk() {
	fmt.Println("pig: walk")
}

func main() {
	animals := map[string]interface{}{ //map, 映射对象名字和对象实例,实例是鸟和猪。
		"bird": new(bird),
		"pig":  new(pig),
	}

	for name, obj := range animals { //遍历映射, obj为interface{}接口类型
		f, isFlyer := obj.(Flyer)   //判断对象是否为飞行动物
		w, isWalker := obj.(Walker) //判断对象是否为行走动物
		fmt.Printf("name: %s is Flyer: %v is Waler: %v\n", name, isFlyer, isWalker)

		//如果是飞行动物调用动物接口
		if isFlyer {
			f.Fly()
		}

		if isWalker {
			w.Walk()
		}
	}
}

7.5.3 将接口转换为其他类型

在代码7.5.2中,可以实现将接口转换为普通的指针类型。例如将Walker接口转换为*pig类型,请参考下面代码:

p1 := new(pig)
var a Walker = p1
p2 := a.(*pig)

fmt.Printf("p1=%p p2=%p", p1, p2)

接口在转换为其他类型时,接口内保存的实例对应的类型指针, 必须是要转换的对应类型指针。

7.6 空接口类型(interface{})

空接口在接口类型的特殊形式,空接口没有任何方法,因此任何类型都无须实现空接口。空类型可以保存任何值,也可以从可接口中取出原值。

7.6.1 将值保存到空接口

空接口的赋值如下:

var any interface{}
any = 1
fmt.Println(any)

any = "hello"
fmt.Println(any)

any = false
fmt.Println(any)

7.6.2 从空接口获取值

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

var a int = 1
var i interface{} = a
var b int = i.(int)

7.6.3 从空接口的值比较

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

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

    代码如下:

    var a interface{} = 100
    var b interface{} = "hi"
    fmt.Println( a == b)
    //代码输出:false
    
  2. 不能比较空接口中的动态值

    以下列举出了类型及比较的几种情况

    类型说明
    map宕机错误,不可比较
    切片宕机错误,不可比较
    通道可比较,必须由同一个make生成,也就是同一个通道才会是true,否则为false
    数组可比较,编译器知道两个数组是否一致
    结构体可比较,逐个比较结构体的值
    函数可比较

7.7 类型分支—批量判断空接口中变量的类型

Go语言的switch不仅可以向其他语言一样实现数值、字符串的判断,还有一种特殊的用途—判读一个接口内保存或实现的类型。

7.7.1 类型断言的书写格式

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

switch 接口变量.(type) {
	case 类型1:
	//处理1
	case 类型2:
	//处理2 
	default:
	//不是以上case的处理
}

7.7.2 使用类型分支判断基本类型

下面的例子将一个interface{}类型的参数传给printType()函数,通过switch判断v的类型,然后打印相应类型的提示,代码如下:

func printType(v interface{}) {
	switch v.(type) {
	case int:
		fmt.Println(v, "is int")
	case string:
		fmt.Println(v, "is string")
	case bool:
		fmt.Println(v, "is bool")
	}
}

func main() {
	printType(1024)
	printType("aa")
	printType(true)
}

7.7.3 使用类型分支判断接口类型

多个接口进行类型断言时,可以使用类型分支简化判断过程。

例如,电子支付能够刷脸支付,而现金支付容易被偷等。使用类型分支可以方便地判断一种支付方法具备哪些特性,详细代码请参考如下:

type Alipay struct{}	//电子支付
// 为Alipay添加CanUseFaceID方法,表示支持刷脸
func (a *Alipay) CanUseFaceID() {
}

type Cash struct{}	//现金支付
// 为Cash添加Stolen方法,表示现金支付易被偷
func (c *Cash) Stolen() { 
}

type ContainUseFaceID interface{
    CanUseFaceID()
}
type ContainStolen interface{
    Stolen()
}
func print(paymethod interface{}) {
    switch paymethod.(type){
    case ContainUseFaceID:
        fmt.Printf("%T can use face id\n", paymethod)    
    case ContainStolen:
        fmt.Printf("%T may be stolen\n", paymethod)  
    }
}
func main() {
    //使用电子支付判断
    print(new(Alipay))
   	//使用现金判断
    print(new(Cash))
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值