图源:
概念
在介绍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
方法后进行相应长度的扩展,否则是没法顺利打印数据的,原因和切片的实现原理相关,具体可以阅读。
接口值
接口的值实际上是接口包含的变量的实际类型和值。这么说好像挺绕的,让我们看一个实际例子:
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
。
如果还不好理解的话,我用下边的图进行说明:
可能不太好看,不过我尽力了。
所以,一个接口的值是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
。
文章评论