Go语言编程笔记6:接口

Go语言编程笔记6:接口

image-20211108153040805

图源:wallpapercave.com

虽然Go语言没有传统编程语言的类与继承,但通过结构、方法和接口,Go语言依然可以实现OOP式的编程。所以接口对于Go语言来说相当重要,这里我们就讨论一下Go语言中的接口。

概念

在介绍Go语言中的接口之前我要先阐述一下其概念的不同,与传统的编程语言比,Go语言的接口是一种隐性实现。即接口只会定义一组方法,所有实现了该方法的类型都满足该接口。

在概念上,这与Python中的协议更类似,不过后者并不会真实定义在代码中,仅仅是文档中的一种约定。

定义

定义接口很简单:

package myinterface

type Reader interface {
	Read([]byte) (int, error)
}

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

type Closer interface {
	Close() error
}

这里是仿照一般性的I/O接口定义了三个接口,分别表示读、写、关闭这三种功能。

需要注意的是,接口名为Reader而其中包含Read方法,这样的命名方式是Go语言接口定义时的一种常见方式,标准库中很多接口都是这么定义的。

如果是Java,更习惯将接口名定义为Readable

除了上边这种普通定义外,还可以通过在接口中包含已有接口的方式来定义新接口:

package myinterface

type ReadWriter interface {
	Reader
	Writer
}

type ReadWriteCloser interface {
	ReadWriter
	Closer
}

type ReadWriteClosePrinter interface {
	ReadWriteCloser
	Print()
}

这种方式称为“内嵌”。当然也可以将两种方式混合使用,比如上边示例中的ReadWriteClosePrinter接口。

无论是哪种方式定义,接口本质上都是一组方法的集合。

实现

在其它传统编程语言中,要实现一个接口,我们通常要在类定义中指明接口名称,但Go语言并不需要。前边我们说了,Go语言中的接口只是一种“协议”,我们只需要实现接口对应的方法即可:

package stringcontainer

//字符串容器
type StringContainer struct {
	container string //存放的字符串
	byteArray []byte //byte形式的字符串
	index     int    //当前读取到的位置
}

func (sc *StringContainer) SetStr(str string) {
	sc.container = str
	sc.byteArray = []byte(str)
	sc.index = 0
}

//从字符串容器中读取一行数据
//param container 存放读取到的数据
//return length 读取到的字节数
//return err 错误
func (sc *StringContainer) Read(container []byte) (length int, err error) {
	for {
		if sc.index >= len(sc.byteArray) {
			return
		}
		char := sc.byteArray[sc.index]
		container = append(container, char)
		sc.index++
		length++
		if char == '\n' {
			return
		}
	}
}

这里通过一个“字符串容器”来进行说明,StringContainer类型是一个结构体,可以接收一个字符串,并按行输出。这里通过给该类型添加Read方法,让该类型满足我们前边定义的Reader接口。

下面给出测试代码:

package main

import (
	"fmt"
	myinterface "go-notebook/ch6/my_interface"
	sc "go-notebook/ch6/string_container"
	"log"
)

func readAndPrint(reader myinterface.Reader) {
	for {
		line := make([]byte, 0, 20)
		length, err := reader.Read(line)
		line = line[0:length]
		if err != nil {
			log.Fatalln(err)
		}
		if length == 0 {
			return
		}
		fmt.Print(string(line))
	}
}

func main() {
	var scontainer sc.StringContainer
	scontainer.SetStr("Hello!\nHow are you!\nI'm fine.")
	readAndPrint(&scontainer)
	// Hello!
	// How are you!
	// I'm fine.
}

测试代码中的readAndPrint函数接收的参数类型是myinterface.Reader,所以我们这里可以将一个StringContainer类型的变量指针传入。

这里需要注意的是,结构体和结构体指针是两种不同的类型,这点在使用接口时需要额外注意,因为我们在之前定义结构体方法时,是给结构体指针添加的方法:func (sc *StringContainer) Read。所以满足Reader接口的并非StringContainer这个结构体类型,而是*StringContainer结构体指针。因此在传参时readAndPrint(&scontainer)我们传入的是scontainer这个变量的地址。

此外,为了能在readAndPrint函数中顺利地将数据读取到切片中,我们必须使用一个初始化了足够容量的切片,并且在执行了reader.read方法后进行相应长度的扩展,否则是没法顺利打印数据的,原因和切片的实现原理相关,具体可以阅读Go语言编程笔记4:结构体和切片

接口值

接口的值实际上是接口包含的变量的实际类型和值。这么说好像挺绕的,让我们看一个实际例子:

package main

import (
	"fmt"
	myinterface "go-notebook/ch6/my_interface"
)

type myByteArr []byte

func (myByteArr) Read([]byte) (length int, err error) {
	return
}

func main() {
	var mba myByteArr = nil
	if mba == nil {
		fmt.Println("mba == nil")
		// mba == nil
	}
	var reader myinterface.Reader = mba
	if reader == nil {
		fmt.Println("reader == nil")
	}
}

这里定义了一个满足Reader接口的引用类型myByteArr,声明了一个该类型的变量mba并用nil初始化。显然,通过==操作符检测是否为nil的结果是true。但是比较奇怪的是,如果我们将mba赋给一个Reader类型的接口变量后,再通过reader == nil检测时,发现是false

表面上很难理解,其实深入的思考一下,每个变量都是由两部分组成:类型和值。而在通过bool表达式比较时,比较的是值。而对于接口变量,其类型是接口,而值是其包含的真实变量,而真实变量正如我们前边所说,包含类型和值两部分内容。这有点套娃的意思,当然,一个接口变量初始化时候其值是nil

如果还不好理解的话,我用下边的图进行说明:

image-20211121184955523

可能不太好看,不过我尽力了。

所以,一个接口的值是nil和值是另一个nil值的变量这是两种不同的情况。在《Go语言编程》一书中,接口包含的实际变量的类型被称作接口的"动态类型"。

类型断言

所谓的类型断言,其实就是将一个接口类型“向下转型”,如果熟悉传统的编程语言应该理解我说的是什么意思:

package main

import (
	"fmt"
	myinterface "go-notebook/ch6/my_interface"
	stringcontainer "go-notebook/ch6/string_container"
	"log"
)

func dealSC(read myinterface.Reader) {
	var sc *stringcontainer.StringContainer = read.(*stringcontainer.StringContainer)
	sc.SetStr("test is changed")
}

func main() {
	var sc stringcontainer.StringContainer
	sc.SetStr("test")
	dealSC(&sc)
	line := make([]byte, 0, 20)
	length, err := sc.Read(line)
	if err != nil {
		log.Fatalln(err)
	}
	line = line[:length]
	fmt.Println(string(line))
	// test is changed
}

上面的示例中,虽然dealSC函数的形参是Reader类型,但实际我们传递的参数是StringContainer类型的变量,所以在这个例子中我们是可以从形参read中“还原”出StringContainer类型的变量的,而这个还原的方式就是通过类型断言。

在Go语言中,类型断言的方式是x.(y),其中x是一个接口变量,y是接口变量的“动态类型”。如果这种转换没有成功,则程序会中断运行:

type myReader struct {
}

func (mr *myReader) Read(container []byte) (length int, err error) {
	return
}

func dealSC(read myinterface.Reader) {
	sc := read.(*myReader)
	// panic: interface conversion: myinterface.Reader is *stringcontainer.StringContainer, not *main.myReader
	fmt.Println(sc)
}

这种类型断言还支持多返回一个bool类型的错误标识,我们可以利用这个错误标识来判断转型是否成功,并避免直接中断程序:

func dealSC(read myinterface.Reader) {
	sc, ok := read.(*myReader)
	if !ok {
		return
	}
	fmt.Println(sc)
}

总之,Go语言中的类型断言实际上和其它语言(如Java)中的类型强制转换很相似,为我们在运行时将接口的类型“向下转型”提供了一种方式。

类型分支

我们可以利用类型断言来检测接口的实际类型:

func dealSC(read myinterface.Reader) {
	if read == nil {
		fmt.Println("nil")
	} else if sc, ok := read.(*stringcontainer.StringContainer); ok {
		sc.SetStr("test is changed")
	} else if _, ok := read.(*myReader); ok {
		fmt.Println("myReader")
	}
}

但这样使用if...else if显得很臃肿,我们可以用一种更简洁的方式:

func dealSC(read myinterface.Reader) {
	switch read := read.(type) {
	case nil:
		fmt.Println("nil")
	case *stringcontainer.StringContainer:
		read.SetStr("test is changed")
	case *myReader:
		fmt.Println("myReader")
	default:
		fmt.Println("other")
	}
}

这种方式称作“类型分支”。需要注意的是switch中的赋值语句并非必须的,这里是因为分支里需要使用转型后的原始值,并且因为switch是一个独立的作用域的关系,这里可以重复使用外部作用域中已有的变量名read

往期内容

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值