接口类型是对其他类型行为的概括与抽象
接口即约定
具体类型指定了其所含数据的精确布局,暴露了基于这个精确布局的内部操作;接口是一种抽象类型,它仅仅提供一些方法。接口是:你不知道它是什么,只知道它能做什么。
为了展示,给出下面这个例子
package fmt
func Fprintf(w io.Writer,format string ,args ...interface{}) (int,error)
该函数的第一个参数类型,就是一个接口类型
package io
type Writer interface {
Write(p []byte)(n int,err error)
}
io.Writer
接口定义了一种约定,该约定要求提供的具体类型,包含一个与其签名和行为一致的Write
方法。这个约定保证了fmt.Fprintf
只需要能够调用其参数的Write
方法,就能适合一大堆的类型。比如我们自己造一个
type ByteCounter int
func (c *ByteCounter) Write(p []byte) (int, error) {
*c += ByteCounter(len(p))
return len(p),nil
}
这个类型的Write
方法就是将字节切片的长度做累加,我们可以对他做fmt.Fprintf
var c ByteCounter
var str = "world"
fmt.Fprintf(&c,"hello,%s",str)
fmt.Println(c)
接口类型
接口类型所要声明的,仅仅是方法的集合,也就是说下面两种定义是等价的
type Writer interface {
Write(p []byte)(n int,err error)
}
type Reader interface {
Read(p []byte) (n int,err error)
}
type ReadWriter interface {
Writer
Reader
}
上面是使用接口内嵌
type ReadWriter interface {
Write(p []byte)(n int,err error)
Read(p []byte) (n int,err error)
}
上面是声明一个包含多个方法签名的接口,两者效果是一致的
实现接口
如果一个类型实现了一个接口所要求的所有方法,那么这个类型就实现了这个接口。我们称这是一个具体类型“是(is-a)”接口类型
- 接口的赋值规则:仅当一个表达式实现了一个接口时,这个表达式才可以赋值给接口
- 接口的封装功能:接口变量只能调用接口约定的那些方法。也就是将一个具体变量赋值给接口后,只能调用该接口所拥有的方法,原变量的内部结构和其它方法都不被暴露出来
- 空接口类型必不可少
interface{}
,这是因为我们可以将任意类型的变量赋值给空接口类型
其它高级语言也有接口,不过Go语言的特殊之处是接口实现是“隐性的”,只要你实现了这些方法,就自动判定你实现了这个接口。另一方面,类比C++的话,接口有点像是虚基类
接口值
接口值在 Go 中由两部分构成:动态类型和动态值
- 动态类型: 这是接口值的一部分,表示接口值当前的具体类型,或者说实现了接口的具体类型。如果我们将一个具体的类型(如
*os.File
或*bytes.Buffer
)的值赋给一个接口类型的变量,那么这个接口值的动态类型就会变成这个具体的类型。 - 动态值: 这是接口值的另一部分,表示接口值的当前值。这个值是动态类型的一个实例。动态值可以存储实现了接口的类型的值。例如,如果我们有一个接口类型
Writer
,并且类型*os.File
和*bytes.Buffer
都实现了这个接口,那么一个Writer
接口类型的值可以存储一个*os.File
或*bytes.Buffer
的值。
在编译时我们并无法知道一个接口值是怎么样的,所以在运行时会启用动态分发。去查找动态类型对应实现的接口方法,并使用动态值作为接收者调用
类型断言
x.(T)
- 其中x是一个接口类型表达式
- T是一个类型,称为断言类型
- 若T是具体类型,检查x的动态类型是否是T,检查成功,返回x的动态值
- 若T是接口类型,检查x的动态类型是否实现了T,检查成功,返回类型为T的接口变量,其接口值与x保持一致。该方式通常用于给变量“升变“,增加方法数量
- 断言失败时,假设我们有
var w io.Writer = os.Stdout
- 若是单一返回值,直接崩溃
b := w.(*bytes.Buffer)
- 两个返回值,则后一个返回值是布尔值,指示是否成功
b,ok := w.(*bytes.Buffer)
- 若是单一返回值,直接崩溃
类型分支
类型分支是类型断言的一种应用方式,该风格利用率接口值能容纳各种具体类型的能力,将一个接口变量作为这些类型的union来使用。
类型分支通过特殊的switch语句实现
switch x.(type) {
case nil:
case int,uint:
case bool:
default:
}
或者我们可以通过类型断言将原具体类型变量提取出来
func sqlQuote(x interface{}) string {
switch x := x.(type)
case nil:
return "NULL"
case int,uint:
return fmt.Sprintf("%d",x) //这里x还是interface{}
case bool:
if(x) { //这里x是bool
return "TRUE"
}
return "FALSE"
case string:
return sqlQuteString(x)
default:
panic(fmt.Sprintf("unexpected type %T:%v",x,x))
}
对于单一类型的分支,x的类型就是对应类型;对应多类型的分支,x的类型保持interface{}