前言
我们都知道OOP有两个方式,组合和继承来组织类,分别表示has-a关系和is-a关系,能够实现代码复用、提升扩展性和灵活性。
前面一篇讲方法method阐述了go通过结构体struct嵌套实现对象的扩展(相对于Java类的组合,has a关系),outer结构体会自动生成包装方法委托给inner嵌套结构的方法,这样实现了方法代码的复用。这样有了组合has-a关系,go里面的is-a关系是怎么实现的呢?下面我们来看看接口interface~
1.接口类型
类似于Java中的接口,接口类型描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。实现接口则不同于Java中需要显式地指明具体类implements实现了某个接口,go中的实现是隐式的,只要一个具体类型实现了某个接口的所有方法就足够了。
以我们最常用的fmt包的打印部分为例,Printf和Sprintf都包含格式化输出的部分,只是最终输出的目的地不一样,一个是标准输出还有一个是字符串,所以fmt包的实现将通用的格式化输出的部分抽取到Fprintf,然后将通用逻辑委托给它。Fprintf的前缀F表示文件(File),输出结果应该被写入第一个参数,第一个参数io.Writer是是用得最广泛的接口之一,因为它提供了所有类型的写入bytes的抽象,包括文件类型,内存缓冲区,网络链接等等。Reader则相反,代表任意可以读取bytes的类型。
func Printf(format string, args ...interface{}) (int, error) {
return Fprintf(os.Stdout, format, args...) // 通用逻辑
}
func Sprintf(format string, args ...interface{}) string {
var buf bytes.Buffer
Fprintf(&buf, format, args...) // 通用逻辑
return buf.String()
}
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {}
// 接口
type Writer interface {
Write(p []byte) (n int, err error)
}
type Stringer interface {
String() string
}
1.2 接口组合(嵌套)
新的接口类型通过组合已有的接口来定义,这样可以很简短地包括所有方法,也可以与声明方法混合使用
type ReadWriter interface {
Reader
Writer
}
type ReadWriter interface {
Read(p []byte) (n int, err error)
Writer
}
1.3 实现接口
具体类型只要实现了接口的所有方法,就是实现了接口,该具体类型的实例就可以赋值给该接口的引用。
var rwc io.ReadWriteCloser
rwc = os.Stdout // OK: *os.File 有 Read, Write, Close 方法
var w io.Writer
w = rwc // OK: io.ReadWriteCloser有Write 赋值给超类型 向上隐式转换
rwc = w // 编译报错: io.Writer没有Close方法 赋值给子类型 向下error
回想方法method讲到类型的方法有两种形式的接收者(t T) 或者 (t *T),那两种方式都可以吗?还有就是包级函数(没有接收者)可以吗?
两种类型都可以,但是它们是作为两种类型分别实现了相关方法。如何选择呢?大多数是指针类型*T,特别是像Write方法那样隐式的给接收者带来变化的时候。
type IntSet struct { /* ... */ }
func (*IntSet) String() string
var s IntSet
var _ fmt.Stringer = &s // OK *IntSet类型实现了Stringer接口
var _ fmt.Stringer = s // 编译报错: IntSet没有实现Stringer接口
用接口类型的变量引用具体类型的实例,可以打到封装和隐藏的作用,隐藏了除了接口以外的方法和字段。
关于interface{}类型,它没有任何方法,请讲出哪些具体的类型实现了它?interface{}被称为空接口类型是不可或缺的。因为空接口类型对实现它的类型没有要求,类似于Java的Object,作为一个超类出现,所以我们可以将任意一个值赋给空接口类型。区别在于Java中的基础类型赋值为Object需要隐式包装类处理,而Go不需要。
var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one": 1}
any = new(bytes.Buffer)
但是实例赋值给interface{}变量,就不能直接对其值做任何操作,因为空接口没有任何方法,可以通过类型断言获取其方法。
var _ io.Writer = (*bytes.Buffer)(nil) // 只是告诉 右边的类型实现了左边的接口
1.4 接口抽象
在设计对象和类型时,将类型的通用行为抽取出来,放入接口;
如果多个接口之间还有通用方法,可以继续抽象父接口,然后通过接口组合的方式,
最终形成类似Java中的继承树。
2.接口值
接口值,包括一个具体的类型和那个类型的值,即动态类型和动态值(类似java的多态,有着更细致的子类和子类的实例)。一个接口值类似于Java的一个对象,对象头中包含类型指针,对象体包含各个字段。
如下例
var x interface{} = time.Now()
x变量在内存中包含一个type对应于java对象头中的类型指针,value是实例字段部分,Time是个结构体,包含3个字段如下:。
调用x的方式时,其实时(x.value).(type.method)(),动态类型type决定方法,value决定方法的接收者。
其中type标识的是具体类型,而不是接口类型,如下将os.Stout *os.File赋值给io.Writer接口变量,如图7.2所属type字段是具体类型*os.File。
var w io.Writer
w == nil
w.Write([]byte("hello")) // panic: nil pointer dereference 类似于Java的NPE
w = os.Stdout
- 接口值为空时,不能调用起任何方法,记得判断。
- 接口调用是动态分配(多态,重写),获取实际type的对应方法地址间接调用,recevier就是value存储实例。
接口值可以使用==和!=来进行比较,不像Java需要用equals方法比较。但是如果动态类型是不可比较的(比如Slice),将它们进行比较就会失败并且panic!!所以通常需要获取接口值的动态类型,通常使用fmt包的%T动作如下,fmt是通过反射获取接口动态类型的名称。
var w io.Writer
fmt.Printf("%T\n", w) // "<nil>"
w = os.Stdout
fmt.Printf("%T\n", w) // "*os.File"
w = new(bytes.Buffer)
fmt.Printf("%T\n", w) // "*bytes.Buffer"
- 接口值非空,value是nil时调用方法NPE
func main() {
var buf *bytes.Buffer // 改为 var buf io.Writer 则接口值为nil,check能拦住
f(buf) // NOTE: subtly incorrect!
}
func f(out io.Writer) { //隐式转换,buf从*bytes.Buffer转换成Writer,接口值不为nil,但接口值的value字段为nil
if out != nil {
out.Write([]byte("done!\n"))
}
}
3.类库常用接口
接口 | 功能 | |
---|---|---|
flag.Value | 实现了可以从命令行参数接收-name value解析特定值 | |
sort.Interface | 类似Java的Comparator将比较逻辑传递个特定排序算法 | |
4.类型断言
类型断言是一个作用在在接口值(第二节)上的操作,语法上x.(T)被称为断言类型,x指接口值、T表示一个类型,作用就是判断接口值x(对象)是否是T类型。有两种可能,
- (1)当T是具体类型时,就是判断x的type字段是否是T;如果判断成功,返回动态类型T的对象,否则抛出异常panic。
var w io.Writer
w = os.Stdout
f := w.(*os.File) // success: f == os.Stdout
c := w.(*bytes.Buffer) // panic: interface holds *os.File, not *bytes.Buffer
f, ok := w.(*os.File) // 成功flag success: ok, f == os.Stdout
- (2)T是接口类型,判断x的type类型是否实现了T;判断成功,返回T类型新的接口值。
nil接口值,不论被断言类型T是什么,都会失败。
类型断言主要就是用于检验它是否是一些特定的类型,向下转型(父类型转换成子类型),类似于Java的强制转换Son son = (Son) father。
4.1 应用场景
4.1.1 识别错误类型
在错误处理的时候经常要判断是哪一种类型的错误,然后才好做不同的处理。以下是os包中文件相关的报错,如何判断是文件不存在的呢?
_, err := os.Open("/no/such/file")
fmt.Println(os.IsNotExist(err)) // "true"
var ErrNotExist = errors.New("file does not exist")
// 返回是否文件不存在
func IsNotExist(err error) bool {
if pe, ok := err.(*PathError); ok { // 类型断言
err = pe.Err // 获取其包装内的error值
}
return err == syscall.ENOENT || err == ErrNotExist
}
4.1.2 writeString优化
func WriteString(w Writer, s string) (n int, err error) {
if sw, ok := w.(StringWriter); ok { // 判断是否可以直接写String
return sw.WriteString(s) // 免于转换
}
return w.Write([]byte(s)) // 字符串转切片,申请内存并拷贝,耗时耗空
}
5.类型分支
通常一个接口值,不同的类型有着不同的处理逻辑,需要判断其类型,go提供了一种类型分支,
如下,switch一个接口值的类型上,在每个单一类型的case内部,变量x和这个case的类型相同。
switch x := x.(type) { /* ... */ } // x.(type)语法只能用在类型分支中,判断类型
func sqlQuote(x interface{}) string {
switch x := x.(type) {
case nil:
return "NULL"
case int, uint:
return fmt.Sprintf("%d", x) // x has type interface{} here.
case bool:
if x {
return "TRUE"
}
return "FALSE"
case string:
return sqlQuoteString(x) // (not shown)
default:
panic(fmt.Sprintf("unexpected type %T: %v", x, x))
}
}
在类型分支中,x虽是interface{},其实它是可识别联合(一种隐含约束),满足这个联合是包含nil、int、unit、bool和string类型的集合。
满足可识别联合的具体类型的集合被设计为确定和暴露,而不是隐藏。可识别联合的类型几乎没有方法,操作它们的函数使用一个类型分支的case集合来进行表述,这个case集合中每一个case都有不同的逻辑。
设计Tips
- 不为了面向接口而为仅一个实现设计接口,避免不必要抽象,仅多种类型有相同行为时需抽象出接口。
- 为了扩展,(跨包扩展)可以通过接口,即接口export,但是实现type不导出,两个包有不同的实现,
- ask only for what you need,向下转型时最小接口原则。