之前介绍的Go中的类型都是具体类型(concrete type),本篇文章介绍Go中的一种抽象类型(abstract type)——接口(interface)。
1. 接口类型
Go的接口不同于其他OOP语言中的接口,Go中的接口概念上非常的简单——Go中的接口定义了一系列的方法,只要某种具体的类型拥有这些方法,我们就说这种类型满足(satisfy)这个接口或者说这个类型是接口的一个实例(instance)。原话是这样:
A type satisies an interface if it possesses all the methods the interface requires.
看下面的例子:
package io
// 定义一个Reader接口类型(有一个Read方法)
type Reader interface {
Read(p []byte) (n int, err error)
}
// 定义一个Writer接口类型(有一个Write方法)
type Writer interface {
Write(p []byte] (n int, err error)
}
// 定义一个Closer接口类型(有一个Close方法)
type Closer interface {
Close() error
}
// 定义一个ReadWriter接口类型(有两个方法)
type ReadWriter interface {
Reader
Writer
}
// 定义一个ReadWriteCloser接口类型(有三个方法)
type ReadWriteCloser interface {
Reader
Writer
Closer
}
var w io.Writer
w = os.Stdout // OK,os.Stdout是*os.File类型,该类型有Write方法
w = new(bytes.Buffer) // OK,*bytes.Buffer有Writer方法
w = time.Second // Compile error:time.Duration lack Write method
var roc io.ReadWriteCloser
rwc = os.Stdout // OK,*os.File有Read、Write、Close方法
rwc = new(bytes.Buffer) // Compile error:*bytes.Buffer没有Close方法
w = rwc // OK: io.ReadWriteCloser有Write方法
rwc = w // Compile error:io.Writer lacks Close方法
我们再来看一个特殊的接口:空接口(empty interface):interface{}
。空接口没有任何方法,所以根据接口的定义和要求我们得出任何类型都都是满足空接口的。换句话说,我们可以把任何类型的值赋给空接口(有点像很多OOP中最顶层的类型,比如Java中的Object类)。
var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one", 1}
any = new(bytes.Buffer)
空接口的这个特性在函数传参使用的非常广泛。
2. Interface Values
接口值(Interface Values)由两部分组成:动态类型(dynamic type)和动态值(dynamic value)(以下简称类型和值)。接口的零值(nil
)指类型和值都为nil。两个接口值相等的条件是都为nil或者他们的类型与值都相同。
我们看几个例子:
例子1:
var w io.Writer // io.Writer是一个接口类型
我们声明了一个io.Writer类型的变量w,此时w的值为被默认初始化为接口的零值,即类型和值都是nil。
例子2:
w = os.Stdout
这里我们将类型为os.File类型的os.Stdout赋给了w,此时w的类型为os.File,值为os.Stdout。
例子3:
w = new(bytes.Buffer)
这里我们w的类型为*bytes.Buffer,值为[]byte
例子4:
w = nil
这个赋值语句将w的类型和值都变成了nil,即接口的零值。
Interface Values在实际使用中有一个非常容易犯错的地方:
An Interface Containing a Nil Pointers is Non-Nil.
其实就是我们要注意区分值和类型都为nil的接口和值为nil但类型不为nil的情况。看个例子
const debug = true
func main() {
var buf *bytes.Buffer
if debug {
buf = new(bytes.Buffer)
}
f(buf) // error...
if debug {
// ...use buf ...
}
}
// If out is non-nil, output will be written to it
func f(out io.Writer) {
// ... do something ...
if out != nil {
out.Write([]byte("done!\n"))
}
}
上面这个例子是如果打开debug(debug置为true),我们就增加一些日志输出。但是有一个bug,就是当我们将debug置为false的时候,不会执行new语句,这样变量buf(指向bytes.Buffer接口的指针)的类型是bytes.Buffer,值是nil。所以buf!=nil
,但因为值为nil,所以后面调用Write方法的时候会panic。
3. Type Assertions
类型断言操作只只适用于Interface Values,语法格式为:
x.(T)
x为Interface Values或可以产生Interface Value的表达式。T为类型(称为断言类型“asserted type
”),这里有两种情况:
如果T是某种具体的类型,那么类型断言就检查x的类型和T是不是一致,如果一致,则断言的结果值是x的动态值,类型是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
如果T是接口类型,则类型断言检查x的动态类型是不是满足(satisfies)T。如果满足,则x依旧是接口类型,但是其类型变为T的类型(即拥有了更多的方法)。如果不满足,则程序panic。
var w io.Writer w = os.Stdout rw := w.(io.ReadWriter) // success:*os.File has both Read and file w = new(ByteCounter) // ByteCounter has only Write Method rw = w.(io.ReadWriter) // panic: *ByteCounter has no Read Method
很显然,如果类型不一致,程序就panic在很多场景都是不可以接受的。所以类型断言提供了“comma, ok”机制:
var w io.Writer = os.Stdout
f, ok := w.(*os.File) // success: f=os.Stdout, ok = true
b, ok := w.(*bytes.Buffer) // fail: f=nil, ok = false
成功的时候,第二个返回值(bool类型)为true,失败的话为false。
4. Type Switch
其实Type Switch和Type Assertions比较类似,比如我们拿到一个interface{}类型的值,如果我们想知道它具体是哪一种类型,那我们就可以写很多if-else分支,利用Type Assertions里面介绍的"comma, ok"机制去一种一种试,直到ok为true为止。但这样写起来太麻烦了,所以就有了Type Switch,它可以让我们使用switch的方式去实现上面的功能。语法格式如下:
switch x.(type) { // type是关键字
case T1:
// do something
case T2:
// do something
...
case Tn:
// do something
default:
// do something
}
看个例子:
package main
import (
"fmt"
)
func exp(x interface{}) string {
switch x := x.(type) {
case nil:
return "NULL"
case int, uint:
return fmt.Sprintf("%d", x)
case bool:
if x {
return "True"
}
return "False"
case string:
return "String"
default:
return fmt.Sprintf("unexpected type %T: %v", x, x)
}
}
func main() {
var a interface{}
fmt.Println(exp(a))
a = 5
fmt.Println(exp(a))
a = true
fmt.Println(exp(a))
a = "I am string"
fmt.Println(exp(a))
a = 3.14
fmt.Println(exp(a))
}
输出为:
NULL
5
True
String
unexpected type float64: 3.14
[Finished in 0.3s]
关于Type Switch有几个注意点:
- 因为一个值可能符合多个类型,所以需要注意case语句的顺序,default语句的位置无所谓。
- 不允许使用
fallthrough
Interface对于OOP编程是比较重要的,但是概念上还是比较简单的,本文主要是介绍了一些基本的概念,更多更深入的了解可以查看Golang官方的文档和博客。
PS:这篇博客10.8号写了一点后,直到今天又才接着写完,拖延症太可怕o(╯□╰)o...
评论已关闭