Go系列:Go 接口
这篇文章主要讲述 Go 接口。
接口类型是对行为的概括与抽象。通过使用接口,我们可以写出更新灵活和通用的函数,这些函数不用绑定在一个特定的类型实现上。 不像其它编程语言,Go 语言的接口实现方式是隐式实现,换句话说,对于一个具体的类型,无须声明它实现了哪些接口,只要提供接口所必需的方法即可。这种设计让你无须改变已有类型的实现,就可以为这些类型接口创建新的接口,对于那些不能修改包的类型,这一点特别有用。
接口即约定
接口是一种抽象类型,它没有暴露所含数据的布局或者内部结构,当然也没有这些数据的基本操作,它所提供的仅仅是一些方法而已,其形式如下所示:
1
2
3type Writer interface {
Write(p []byte) (n int, err error)
}
接口类型
一个接口类型定义一套方法,如果一个具体类型要实现该接口,那么必须实现接口类型定义中的所有方法。以io包中的接口类型为例。
1
2
3
4
5
6
7
8
9package io
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
另外,还可以通过组合已有接口得到新的接口,如: 1
2
3
4type ReadWriter interface {
Reader
Write
}1
2
3
4type ReadWriter interface {
Read(p []byte) (n int, err error)
Writer
}
接口的实现
如果一个类型实现了一个接口要求的所有方法,那么这个类型实现了这个接口。
1
2
3
4
5
6type ByteCounter int
func (c *ByteCounter) Write(p []byte) (int,error) {
*c += ByteCounter(len(p)) // 转换int 为 ByteCounter 类型
return len(p), nil
}*ByteCounter
实现了 Writer
接口中的 write 方法,可以说 *ByteCounter
实现了
Writer
接口。
空接口类型
接口类型
interface{}
,表示空接口类型,它完全不包含任何方法。在程序中,空接口类型是不可缺少的,它可以用来表示任何类型。
1
2
3
4
5
6var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one",1}
any = new(bytes.Buffer)
接口值
从概念上来讲,一个接口类型的值(简称接口值)其实有两个部分:一个具体类型和该类型的一个值。二者称为接口的动态类型和动态值。
如下四个语句中,变量 w 有三个不同值: 1
2
3
4var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
假定接口类型为 type,值为 value,则有: var w io.Writer
:
等同于: 1
2w.type = nil
w.value = nil
w = os.Stdout
: 等同于: 1
2w.type = *os.File
w.value = fd int=1(stdout) // 指向一个os.File对象
w = new(bytes.Buffer)
: 等同于: 1
2w.type = *byte.Buffer
w.value = &(bytes.Buffer) // 指向一个bytes.Buffer 对象
w = nil
: 等同于: 1
2w.type = nil
w.value = nil
接口值可以用 == 和 !=
操作符来比较。如果两个接口值都是nil或者二者的动态类型安全一致且动态值相等(使用动态类型的
==
操作符来比较),那么两个接口值相等。因为接口值是可以比较的,所以它们可以作为
map 的键,也可以作为 switch 语句的操作数。
需要注意的是,在比较两个接口值时,如果两个接口值的动态类型一致,但对应的动态值是不可比较的(比如slice),那么这个比较会以崩溃的方式失败:
1
2var x interface{} = []int{1,2,3}
fmt.Println(x == x) // 宕机:试图比较不可比较的类型 []int
当处理错误或者调试时,能拿到接口值的动态类型是很有帮助的。可以使用
fmt 包的 %T 来实现这个需求: 1
2
3
4
5
6
7
8var 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) // "*byte.Buffer"
注意:含有空指针的非空接口 nil 的接口值(类型和值都为nil)与仅仅动态值为 nil 的接口值是不一样的,它们二者不是相等关系。
方法转接口
在 net/http 包中实现了 Web客户端和服务器代码,其中 http.Handler
接口定义了服务端处理函数,它传入两个对象,一个是请求对象指针,用于传入请求的数据,另外一个对象是响应输出对象ResponseWriter,用于返回输出结果,如下所示:
1
2
3
4
5
6
7package http
type Handler interface {
ServerHttp(w ResponseWriter, r *Request)
}
func ListenAndServer(address string, h Handler) error
ListenAndServer 函数需要一个服务器地址,比如"localhost:8080",以及一个 Handler 接口的实例(用来接受所有的请求)。这个函数会一直运行,直到服务出错(或者启动时就失败了)时返回一个非空的错误。
假定一个电子商务网站,使用一个数据库来存储商品和价格,如下程序展示,它用一个
map类型(命名为database)来代表仓库,再加一个 ServerHTTP 方法来满足
http.Handler 接口,这个函数遍历整个 map 并且输出其中的元素:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16func main(){
db := database{"shoes": 50,"socks":5}
log.Fatal(http.ListenAndServe("localhost:8000", db))
}
type dollars float32
func (d dollars) String() string { return fmt.Sprintf("$%.2f",d) }
type database map[string]dollars
func (db database) ServeHTTP(w http.Responsewriter,reg *http.Request) {
for item,price := range db {
fmt.Fprintf(w,"%s:%s n",item, price)
}
}
ServeHTTP
函数只有一个功能,输出所有产品的价格,如果要增加其它的endpoint,如/price,
用来显示单个商品的价格,商品可以在请求参数中指定,比如:/price?item=socks,
需要修改代码: 1
2
3
4
5
6
7
8
9
10
11func (db database) ServeHTTP(w http.Responsewriter,reg *http.Request) {
switch req.URL.Path {
case "/list":
//...
case "/price":
//...
default:
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such page: %s\n",reg.URL)
}
}
在下面的代码中,创建了一个 ServeMux,用于将 /list,/price 这样的 URL
和对应的处理程序关联起来,这些处理程序已经拆分到不同的方法中。最后作为主处理程序在
ListenAndServe 调用中使用这个 ServeMux: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26func main() {
db := database{"shoes": 50, "socks": 5}
mux := http.NewServeMux()
mux.Handle("/list", http.HandlerFunc(db.list))
mux.Handle("/price", http.HandlerFunc(db.price))
log.Fatal(http.ListenAndServe("localhost:8000", mux))
}
type database map[string]dollars
func (db database) list(w http.ResponseWriter, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}
func (db database) price(w http.ResponseWriter, req *http.Request) {
item := req.URL.Query().Get("item")
price, ok := db[item]
if !ok {
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such item: %q\n", item)
return
}
fmt.Fprintf(w, "%s\n", price)
}
mux.Handle
是 *ServeMux
中的方法,定义如下:
func (mux *ServeMux) Handle(pattern string, handler Handler)
参数中 hanler 是 Handler 类型的接口,而 db.list 和 db.price
是如下类型的函数:
func(w http.Responsewriter,reg *http.Request)
db.list 和
db.price 是一个函数,它没有对 Handle 所需的方法,所以不能直接传递给
handler 参数。
在这里使用了一个类型转换,将函数转换为一个Handle接口类型,如下所示:
1
http.HandlerFunc(db.list)
HandlerFunc 是一个类型,不是一个函数调用,其定义如下: 1
2
3
4
5
6
7package http
type HandlerFunc func(w ResponseWriter, r *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
## error 接口 在程序中,经常使用到 error
类型,它实际上一个接口,其定义如下: 1
2
3type error interface {
Error() string
}
构造 error 最简单的方法是调用 error.New,
它返回一个包含指定错误信息的新 error 实例,完整的 error
包只有如下4行代码: 1
2
3
4
5
6
7package errors
func New(text string) error { return &errorString{text}}
type errorString struct {text string}
func (e *errorString) Error() string { return e.text }
底层的 errorString 类型是一个结构,而没有直接用字符串,主要是为了避免将来增加内容。满足 error 接口的是 *errorString 指针,而不是原始的 errorString, 主要是为了让每次 New 分配的 error 实例都互不相等。
除了调用 errors.New函数生成 error 实例,也可以使用 fmt.Errorf,
它额外提供了字符串格式化功能,如下所示: 1
2
3
4
5
6
7package fmt
import "errors"
func Errorf(format string, args ...interface{}) error {
return errors.New(Sprintf(format, args...))
}
类型断言
类型断言是一个作用在接口值上的操作,写出来类似于 x.(T), 其中 x 是一个接口类型的表达式,而 T 是一个类型(称为断言类型)。类型断言会检查作为操作数的动态类型是否满足指定的断言类型。在这里,T 有两种场景,它可以是具体类型,也可以是接口类型。
如果断言类型 T 是一个具体类型,那么类型断言会检查 x
的动态类型是否就是 T. 如果检查成功,类型断言的结果就是 x
的动态值,类型当前就是 T.
换句话说,类型断言就是用来从它的操作数中把具体类型取出来的操作。如果检查失败,那么操作崩溃。比如:
1
2
3
4var w io.Writer
w = os.Stdout
f := w.(*os.File) // 成功:f == os.Stdout
c := w.(*bytes.Buffer) // 崩溃:接口持有的是 *os.File,不是 *bytes.Buffer.
如果断言类型 T 是一个接口类型,那么类型断言检查 x 的动态类型是否满足 T. 如果检查成功,动态值并没有提取出来,结果仍然是一个接口值,接口值的类型和值部分也没有变更,只是结果的类型为接口类型T. 换句话说,类型断言是一个接口值表达式,从一个接口类型变为另外一套方法的接口类型(通常方法数量是增多),但保留了接口值中的动态类型和动态值部分。
如下类型断言代码中, w 和 rw 都持有 os.Stdout,
于是所有对应的动态类型都是 *os.File,但 w 作为 io.Writer 仅暴露了文件的
Write 方法,而 rw 还暴露了它的 Read 方法。 1
2
3
4
5
6var w io.Writer
w = os.Stdout
rw := w.(io.ReadWriter) // 成功: *os.File 有 Read 和 Write 方法。
w = new(ByteCounter)
rw = w.(io.ReadWriter) // 崩溃:*ByteCounter 没有 Read 方法。
无论哪种类型作为断言类型,如果操作数是一个空接口值,类型断言都会失败。很少需要从一个接口类型向一个要求更宽松的类型做类型断言,该宽松类型的接口方法比原类型的少,而且是子集。除了在操作数为
nil 的情况,在其它情况下这种操作与赋值一致,如下所示: 1
w = rw.(io.Writer) // 仅当 rw == nil 时失败
1
r = rw // io.ReadWriter 可以赋给 io.Writer
另外,为了避免断言失败时崩溃,可以使用有两个返回值的断言类型,多出一个布尔类型的返回值表示断言是否成功,如下所示:
1
2
3
4var w io.Writer = os.Stdout
f, ok := w.(*os.File) // 成功:ok, f == os.Stdout
b, ok := w.(*bytes.Buffer) // 失败,!ok, b == nil
这种形式可以结合 if 表达式,写出比较紧凑的代码: 1
2
3if f, ok := w.(*os.File); ok {
// ... 使用 f...
}
使用类型断言识别错误或查询接口特性
在程序中,可以使用类型断言来识别错误,或检查类型是否满足某一接口,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13package fmt
func formatOneValue(x interface{}) string {
if err, ok := x.(error); ok { // 识别错误
return err.Error()
}
if str, ok := x.(Stringer); ok { // 判断是否满足某一接口
return str.String()
}
// ... 所有其它类型
}
类型分支
类型断言与switch语句结合,可以进行不同类型分支的判断,如下所示:
1
2
3
4
5
6
7
8
switch x.(type) {
case nil: // ...
case int, uint: // ...
case bool: // ...
case string: // ...
default: // ...
}
或将类型断言的结果赋值给新的变量: 1
switch x := x.(type) { /* ... */}
把新的变量也命名为 x, 也可以命名为其它名字。