Appearance
The Go Programming Language - 接口
很多面向对象的语言都有相似的接口概念,但 Go 语言中接口类型的独特之处在于它是满足隐式实现的。也就是说,我们没有必要对于给定的具体类型定义所有满足的接口类型;简单地拥有一些必需的方法就足够了。这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不会去改变这些类型的定义;当我们使用的类型来自于不受我们控制的包时这种设计尤其有用。
1. 接口约定
以 fmt.Fprintf 函数为例:
Go
func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)Go
package io
// Writer is the interface that wraps the basic Write method.
type Writer interface {
// Write writes len(p) bytes from p to the underlying data stream.
// It returns the number of bytes written from p (0 <= n <= len(p))
// and any error encountered that caused the write to stop early.
// Write must return a non-nil error if it returns n < len(p).
// Write must not modify the slice data, even temporarily.
//
// Implementations must not retain p.
Write(p []byte) (n int, err error)
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
因为 fmt.Fprintf 函数没有对具体操作的值做任何假设而是仅仅通过 io.Writer 接口的约定来保证行为,所以第一个参数可以安全地传入一个任何具体类型的值只需要满足 io.Writer 接口。下面我们新建一个类型来进行验证:
Go
type ByteCounter int
func (c *ByteCounter) Write(p []byte) (int, error) {
*c += ByteCounter(len(p)) // convert int to ByteCounter
return len(p), nil
}1
2
3
4
5
6
2
3
4
5
6
调用:
Go
var c ByteCounter
c.Write([]byte("hello"))
fmt.Println(c) // "5", = len("hello")
c = 0 // reset the counter
var name = "Dolly"
fmt.Fprintf(&c, "hello, %s", name)
fmt.Println(c) // "12", = len("hello, Dolly")1
2
3
4
5
6
7
2
3
4
5
6
7
另外 fmt 包还有很重要的接口类型,如 fmt.Stringer:
Go
package fmt
// The String method is used to print values passed
// as an operand to any format that accepts a string
// or to an unformatted printer such as Print.
type Stringer interface {
String() string
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
2. 接口类型
接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。
io.Writer 类型是用的最广泛的接口之一,因为它提供了所有的类型写入 bytes 的抽象,包括文件类型,内存缓冲区,网络链接,HTTP 客户端,压缩工具,哈希等等。io 包中定义了很多其它有用的接口类型。Reader 可以代表任意可以读取 bytes 的类型,Closer 可以是任意可以关闭的值,例如一个文件或是网络链接。(到现在你可能注意到了很多 Go 语言中单方法接口的命名习惯)。
Go
package io
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}1
2
3
4
5
6
7
2
3
4
5
6
7
在往下看,我们发现有些新的接口类型通过组合已经有的接口来定义。下面是两个例子:
Go
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
上面用到的语法和结构内嵌相似,我们可以用这种方式以一个简写命名另一个接口,而不用声明它所有的方法。这种方式本称为接口内嵌。我们也可以像下面这样,不使用内嵌来声明 io.Writer 接口。
Go
type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}1
2
3
4
2
3
4
或者甚至使用种混合的风格:
Go
type ReadWriter interface {
Read(p []byte) (n int, err error)
Writer
}1
2
3
4
2
3
4
上面 3 种定义方式都是一样的效果。方法的顺序变化也没有影响,唯一重要的就是这个集合里面的方法。
3. 实现接口的条件
一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。
值得注意的是,对于每一个命名过的具体类型 T;它一些方法的接收者是类型 T 本身,另一些可能是一个 *T 的指针。在 T 类型的参数上调用一个 *T 的方法是合法的(只要这个参数是一个变量;编译器隐式的获取了它的地址)。但这仅仅是一个语法糖,T 类型的值不拥有所有 *T 指针的方法。
举个例子,先前 IntSet 类型的 String 方法的接收者是一个指针类型,所以我们不能在一个不能寻址的 IntSet 值上调用这个方法:
Go
type IntSet struct { /* ... */ }
func (*IntSet) String() string
var _ = IntSet{}.String() // compile error: String requires *IntSet receiver1
2
3
2
3
但是我们可以在一个 IntSet 值上调用这个方法:
Go
var s IntSet
var _ = s.String() // OK: s is a variable and &s has a String method1
2
2
由于只有 *IntSet 类型有 String 方法,所以也只有 *IntSet 类型实现了 fmt.Stringer 接口:
Go
var _ fmt.Stringer = &s // OK
var _ fmt.Stringer = s // compile error: IntSet lacks String method1
2
2
关于 interface{} 类型,它没有任何方法,这看上去好像没有用,但实际上 interface{} 被称为空接口类型,是不可或缺的。因为空接口类型对实现它的类型没有要求,所以我们可以将任意一个值赋给空接口类型。后续我们可以通过类型断言来获取 interface{} 中值的方法。
Go
var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one": 1}
any = new(bytes.Buffer)1
2
3
4
5
6
2
3
4
5
6
下面的代码在编译期断言一个 *bytes.Buffer 的值实现了 io.Writer 接口类型:
Go
// *bytes.Buffer must satisfy io.Writer
var w io.Writer = new(bytes.Buffer)1
2
2
我们还可以对其进行简化(推荐写法):
Go
// *bytes.Buffer must satisfy io.Writer
var _ io.Writer = (*bytes.Buffer)(nil)1
2
2
4. flag.Value 接口
参考以下程序:
Go
var period = flag.Duration("period", 1*time.Second, "sleep period")
func main() {
flag.Parse()
fmt.Printf("Sleeping for %v...", *period)
time.Sleep(*period)
fmt.Println()
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
执行:
Bash
$ go build gopl.io/ch7/sleep
$ ./sleep
Sleeping for 1s...1
2
3
2
3
执行,并指定 -period 参数:
Bash
$ ./sleep -period 50ms
Sleeping for 50ms...
$ ./sleep -period 2m30s
Sleeping for 2m30s...
$ ./sleep -period 1.5h
Sleeping for 1h30m0s...
$ ./sleep -period "1 day"
invalid value "1 day" for flag -period: time: invalid duration 1 day1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
将时间周期(
time.Duration)作为命令行参数是非常常用的,所以flag.Duration被默认构建到了flag包中。
如果我们想要使用自己的数据类型定义新的标记符号(flag)也是很容易的,我们只需要定义一个实现 flag.Value 接口的类型:
Go
package flag
// Value is the interface to the value stored in a flag.
type Value interface {
String() string
Set(string) error
}1
2
3
4
5
6
7
2
3
4
5
6
7
以 Celsius 类型为例。我们先新建一个 celsiusFlag 类型,并在 celsiusFlag 类型中内嵌一个 Celsius 类型,再让 celsiusFlag 类型实现 flag.Value 接口(这样就可以避免修改 Celsius 类型的源码),同时因为 Celsius 类型本身已经有 String 方法了,所以我们只需要再实现 Set 方法即可:
Go
// *celsiusFlag satisfies the flag.Value interface.
type celsiusFlag struct{ Celsius }
func (f *celsiusFlag) Set(s string) error {
var unit string
var value float64
fmt.Sscanf(s, "%f%s", &value, &unit) // no error check needed
switch unit {
case "C", "°C":
f.Celsius = Celsius(value)
return nil
case "F", "°F":
f.Celsius = FToC(Fahrenheit(value))
return nil
}
return fmt.Errorf("invalid temperature %q", s)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
最后再定义一个 CelsiusFlag 函数将 celsiusFlag 标记加入应用的命令行标记集合中:
Go
// CelsiusFlag defines a Celsius flag with the specified name,
// default value, and usage, and returns the address of the flag variable.
// The flag argument must have a quantity and a unit, e.g., "100C".
func CelsiusFlag(name string, value Celsius, usage string) *Celsius {
f := celsiusFlag{value}
flag.CommandLine.Var(&f, name, usage)
return &f.Celsius
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
使用:
Go
var temp = tempconv.CelsiusFlag("temp", 20.0, "the temperature")
func main() {
flag.Parse()
fmt.Println(*temp)
}1
2
3
4
5
6
2
3
4
5
6
执行:
Bash
$ go build gopl.io/ch7/tempflag
$ ./tempflag
20°C
$ ./tempflag -temp -18C
-18°C
$ ./tempflag -temp 212°F
100°C
$ ./tempflag -temp 273.15K
invalid value "273.15K" for flag -temp: invalid temperature "273.15K"
Usage of ./tempflag:
-temp value
the temperature (default 20°C)
$ ./tempflag -help
Usage of ./tempflag:
-temp value
the temperature (default 20°C)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
5. 接口值
接口值,由两个部分组成:具体的类型和该类型的值。它们被称为接口的动态类型和动态值。
对于像 Go 语言这种静态类型的语言,类型是编译期的概念。因此在接口值的概念模型中,提供类型信息的值被称为类型描述符,比如类型的名称和方法。在一个接口值中,类型部分代表与之相关类型的描述符。
下面 4 个语句中,变量 w 得到了 3 个不同的值(开始和最后的值是相同的)。
Go
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil1
2
3
4
2
3
4
让我们进一步观察在每一个语句后的 w 变量的值和动态行为。
第一个语句定义了变量
w:Govar w io.Writer对于一个接口的零值就是它的类型和值的部分都是
nil:
你也可以通过使用
w==nil或者w!=nil来判读接口值是否为空。第二个语句将一个
*os.File类型的值赋给变量w:Gow = os.Stdout该语句将这个接口值的动态类型设为
*os.File指针的类型描述符,它的动态值持有os.Stdout的拷贝:
第三个语句给接口值赋了一个
*bytes.Buffer类型的值Gow = new(bytes.Buffer)现在动态类型是
*bytes.Buffer并且动态值是一个指向新分配的缓冲区的指针:
最后,第四个语句将
nil赋给了接口值:Gow = nil这个重置将它所有的部分都设为
nil值,把变量w恢复到和它之前定义时相同的状态。
一个接口值可以持有任意大的动态值。例如,表示时间实例的 time.Time 类型,我们从它上面创建一个接口值:
Go
var x interface{} = time.Now()结果可能和下图相似。从概念上讲,不论接口值多大,动态值总是可以容下它(这只是一个概念上的模型,具体的实现可能会很大的不同)。

接口值可以使用 == 和 != 来进行比较。两个接口值相等,仅当它们都是 nil 值或者它们的动态类型相同并且动态值也根据这个动态类型的 == 操作相等。因为接口值是可比较的,所以它们可以用在 map 的键或者作为 switch 语句的操作数。
然而,如果两个接口值的动态类型相同,但是这个动态类型是不可比较的(比如切片),将它们进行比较就会失败并且 panic:
Go
var x interface{} = []int{1, 2, 3}
fmt.Println(x == x) // panic: comparing uncomparable type []int1
2
2
从上面这点来看,接口类型是非常与众不同的。其它类型要么是安全的可比较类型(如:基本类型和指针)要么是完全不可比较的类型(如:切片、映射类型、和函数)。但是在比较接口值或者包含了接口值的聚合类型时,我们必须要意识到潜在的 panic。同样的风险也存在于使用接口作为 map 的键或者 switch 的操作数。只能比较你非常确定它们的动态值是可比较类型的接口值。
查看接口值的动态类型,在我们处理错误或者调试的过程中是非常有用的。我们可以使用 fmt 包的 %T 进行查看(在 fmt 包的内部,实际上是通过反射来获取接口动态类型的名称的):
Go
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"1
2
3
4
5
6
2
3
4
5
6
5.1. 警告:一个包含 nil 指针的接口不是 nil 接口
一个不包含任何值的 nil 接口值和一个刚好包含 nil 指针的接口值是不同的。
Go
const debug = true
func main() {
var buf *bytes.Buffer
if debug {
buf = new(bytes.Buffer) // enable collection of output
}
f(buf) // NOTE: subtly incorrect!
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 的值为 false 时,在 out.Write 方法调用时,程序会引发 panic(以上第 18 行)。
当 main 函数调用函数 f 时,它给 f 函数的 out 参数赋了一个 *bytes.Buffer 的空指针,所以 out 的动态值是 nil,而它的动态类型是 *bytes.Buffer,也就是 out 变量是一个包含空指针值的非空接口(如下图),因以 out!=nil 的结果依然是 true。

因此 (*bytes.Buffer).Write 方法依然会被调用,只不过当它尝试去获取缓冲区时会发生 panic。
解决方案就是将 main 函数中的变量 buf 的类型改为 io.Writer,因此可以避免一开始就将一个不完全的值赋值给这个接口:
Go
var buf io.Writer
if debug {
buf = new(bytes.Buffer) // enable collection of output
}
f(buf) // OK1
2
3
4
5
2
3
4
5
5.2. 打印出接口的动态类型和值
Go
package main
import (
"fmt"
"unsafe"
)
type iface struct {
itab, data unsafe.Pointer
}
func main() {
var a interface{} = nil
var b interface{} = (*int)(nil)
x := 5
var c interface{} = (*int)(&x)
ia := *(*iface)(unsafe.Pointer(&a))
ib := *(*iface)(unsafe.Pointer(&b))
ic := *(*iface)(unsafe.Pointer(&c))
fmt.Println(ia, ib, ic)
fmt.Println(*(*int)(ic.data))
}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
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
输出:
Text
{<nil> <nil>} {0xeb3b40 <nil>} {0xeb3b40 0xc000016088}
51
2
2
从以上输出的结果我们可以清楚:
a的动态类型和动态值均为<nil>;b的动态类型和c的动态类型一致,都是0xeb3b40(也就是*int);c的动态值为5;
拓展链接:Go iface 和 eface 的区别
5.3. 判断接口的动态值是否为 nil
可以使用 reflect.ValueOf(i any).IsNil() 判断接口的动态值是否为 nil。需要注意的是,其中 i 必须为 chan、func、interface、map、pointer 或 slice 其中之一,否则在调用 (Value).IsNil 方法时将会 panic。例如:
Go
package main
import (
"bytes"
"fmt"
"io"
"reflect"
)
func main() {
var a *bytes.Buffer
fmt.Printf("%T, %v, %v\n", a, a, a == nil)
fmt.Printf("%T, %v, %v\n", a, a, reflect.ValueOf(a).IsNil())
testNilDirect(a)
testNilByReflect(a)
}
func testNilDirect(a io.Writer) {
fmt.Printf("%T, %v, %v\n", a, a, a == nil)
}
func testNilByReflect(a io.Writer) {
fmt.Printf("%T, %v, %v\n", a, a, reflect.ValueOf(a).IsNil())
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
输出:
Text
*bytes.Buffer, <nil>, true
*bytes.Buffer, <nil>, true
*bytes.Buffer, <nil>, false
*bytes.Buffer, <nil>, true1
2
3
4
2
3
4
6. sort.Interface 接口
sort.Interface 接口定义:
Go
package sort
type Interface interface {
Len() int
Less(i, j int) bool // i, j are indices of sequence elements
Swap(i, j int)
}1
2
3
4
5
6
7
2
3
4
5
6
7
接口实现以 StringSlice 为例:
Go
type StringSlice []string
func (p StringSlice) Len() int { return len(p) }
func (p StringSlice) Less(i, j int) bool { return p[i] < p[j] }
func (p StringSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }1
2
3
4
2
3
4
使用方式:
Go
sort.Sort(StringSlice(names))6.1. sort.Reverse 方法
如果需要在现有的排序规则的基础上进行逆序,则直接使用 sort.Reverse 方法即可:
Go
sort.Sort(sort.Reverse(StringSlice(names)))Note
sort.Reverse函数值得进行更近一步的学习因为它使用了组合,这是一个很实用的技巧。
Go
package sort
type reverse struct{ Interface } // that is, sort.Interface
func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) }
func Reverse(data Interface) Interface { return reverse{data} }1
2
3
4
5
6
7
2
3
4
5
6
7
reverse的另外两个方法Len和Swap隐式地由原有内嵌的sort.Interface实例对象提供;- 因为
reverse是一个不公开的类型,所以导出函数Reverse函数返回原sort.Interface实例对象;
6.2. 较为灵活的实现方式
对于某个类型我们有可能需要实现多种排序方式,如果每种排序算法我们都重新去定义一个 sort.Interface 实现,则会显得过于麻烦和笨拙,此时我们可以参考以下这种写法(重点在于第 26 行):
Go
type Track struct {
Title string
Artist string
Album string
Year int
Length time.Duration
}
var tracks = []*Track{
{"Go", "Delilah", "From the Roots Up", 2012, length("3m38s")},
{"Go", "Moby", "Moby", 1992, length("3m37s")},
{"Go Ahead", "Alicia Keys", "As I Am", 2007, length("4m36s")},
{"Ready 2 Go", "Martin Solveig", "Smash", 2011, length("4m24s")},
}
func length(s string) time.Duration {
d, err := time.ParseDuration(s)
if err != nil {
panic(s)
}
return d
}
type customSort struct {
t []*Track
less func(x, y *Track) bool
}
func (x customSort) Len() int
func (x customSort) Less(i, j int) bool { return x.less(x.t[i], x.t[j]) }
func (x customSort) Swap(i, j int) { x.t[i], x.t[j] = x.t[j], x.t[i] }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
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
调用:
Go
sort.Sort(customSort{tracks, func(x, y *Track) bool {
if x.Title != y.Title {
return x.Title < y.Title
}
if x.Year != y.Year {
return x.Year < y.Year
}
if x.Length != y.Length {
return x.Length < y.Length
}
return false
}})1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
6.3. 检查是否已经有序
参考以下示例代码:
Go
values := []int{3, 1, 4, 1}
fmt.Println(sort.IntsAreSorted(values)) // "false"
sort.Ints(values)
fmt.Println(values) // "[1 1 3 4]"
fmt.Println(sort.IntsAreSorted(values)) // "true"
sort.Sort(sort.Reverse(sort.IntSlice(values)))
fmt.Println(values) // "[4 3 1 1]"
fmt.Println(sort.IntsAreSorted(values)) // "false"1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
为了使用方便,
sort包为[]int、[]string和[]float64的正常排序提供了特定版本的函数和类型。
7. http.Handler 接口
在这个小节中,我们会对那些基于 http.Handler 接口的服务器 API 做更进一步的学习:
Go
package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
func ListenAndServe(address string, h Handler) error1
2
3
4
5
6
7
2
3
4
5
6
7
ListenAndServe 函数需要一个例如 localhost:8000 的服务器地址,和一个所有请求都可以分派的 Handler 接口实例。它会一直运行,直到这个服务因为一个错误而失败(或者启动失败),它的返回值一定是一个非空的错误。
下面是一个简单的示例:
Go
func 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, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在本机上通过浏览器访问 localhost:8000 将会得到类似如下的输出:
Text
shoes: $50.00
socks: $5.001
2
2
让我们来改造下 ServeHTTP,使其能够根据不同的 URL 执行不同的业务逻辑:
Go
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/list":
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
case "/price":
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)
default:
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such page: %s\n", req.URL)
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
以上示例中的 17、18 行也可以这样写:
Go
msg := fmt.Sprintf("no such page: %s\n", req.URL)
http.Error(w, msg, http.StatusNotFound) // 4041
2
2
在上面这个示例中,我们通过多个 case 来处理不同路径的请求,很明显这样做显得有点笨。幸运的是 net/http 包提供了一个请求多路器 ServeMux 来简化 URL 和 handlers 的联系。一个 ServeMux 将一批 http.Handler 聚集到一个单一的 http.Handler 中。
Go 语言目前没有一个权威的 web 框架,就像 Ruby 语言有 Rails 和 python 有 Django。这并不是说这样的框架不存在,而是 Go 语言标准库中的构建模块就已经非常灵活以至于这些框架都是不必要的。
接下来,我们用 ServeMux 对上面的例子做下重构:
Go
func 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)
}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
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- 行 3 ~ 6,我们使用
http.*ServeMux对象取代了原先的database对象; - 行 4 ~ 5 以及 11 ~ 26,我们将原先的
switch-case语句拆分成了相应的方法,并使用mux.Handle方法将其注册到 http 服务中;
值得注意的是:
database不再满足http.Handler接口;database.list、database.price的类型值为:Gofunc(w http.ResponseWriter, req *http.Request)语句
http.HandlerFunc(db.list)是一个转换而非一个函数调用,因为http.HandlerFunc是一个类型,它的定义如下:Gopackage http type HandlerFunc func(w ResponseWriter, r *Request) func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }1
2
3
4
5
6
7HandlerFunc显示了在 Go 语言接口机制中一些不同寻常的特点。这是一个有实现了接口http.Handler方法的函数类型。ServeHTTP方法的行为调用了它本身的函数。因此HandlerFunc是一个让函数值满足一个接口的适配器,这里函数和这个接口仅有的方法有相同的函数签名。
ServeMux 还有一个更方便的 HandleFunc 方法,因此以上 3 ~ 4 行代码也可以写成这样:
Go
mux.HandleFunc("/list", db.list)
mux.HandleFunc("/price", db.price)1
2
2
因为在大多数程序中,一个 web 服务器就足够了。另外,在一个应用程序的多个文件中定义 HTTP handler 也是非常常见的,如果它们必须全部都显示的注册到某个 ServeMux 实例上会比较麻烦。所以为了方便,net/http 包提供了一个全局的 ServeMux 实例 DefaultServerMux 和包级别的 http.Handle、http.HandleFunc 函数。现在,我们对 main 函数进行修改,改用 DefaultServeMux 作为服务器的主 handler(我们可以不用显示地将 DefaultServeMux 传递给 ListenAndServe 函数,传 nil 值就可以了):
Go
func main() {
db := database{"shoes": 50, "socks": 5}
http.HandleFunc("/list", db.list)
http.HandleFunc("/price", db.price)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}1
2
3
4
5
6
2
3
4
5
6
8. error 接口
error 实际上就是一个 interface 类型,这个类型有一个返回错误信息的单一方法:
Go
type error interface {
Error() string
}1
2
3
2
3
创建一个 error 最简单的方法就是调用 errors.New 函数,它会根据传入的错误信息返回一个新的 error。整个 errors 包仅只有 4 行:
Go
package errors
func New(text string) error { return &errorString{text} }
type errorString struct { text string }
func (e *errorString) Error() string { return e.text }1
2
3
4
5
6
7
2
3
4
5
6
7
承载 errorString 的类型是一个结构体而非一个字符串,这是为了保护它表示的错误避免被粗心(或有意)的更新。
并且因为是指针类型 *errorString 满足 error 接口而非 errorString 类型,所以每个 New 函数的调用都分配了一个独特的和其他错误不相同的实例。我们也不想要重要的 error 例如 io.EOF 和一个刚好有相同错误消息的 error 比较后相等。
Go
fmt.Println(errors.New("EOF") == errors.New("EOF")) // "false"调用 errors.New 函数是非常稀少的,因为有一个方便的封装函数 fmt.Errorf,它还会处理字符串格式化:
Go
package fmt
import "errors"
func Errorf(format string, args ...interface{}) error {
return errors.New(Sprintf(format, args...))
}1
2
3
4
5
6
7
2
3
4
5
6
7
虽然 *errorString 可能是最简单的错误类型,但远非只有它一个。例如,syscall 包提供了 Go 语言底层系统调用 API。在多个平台上,它定义一个实现 error 接口的数字类型 Errno,并且在 Unix 平台上,Errno 的 Error 方法会从一个字符串表中查找错误消息,如下面展示的这样:
Go
package syscall
type Errno uintptr // operating system error code
var errors = [...]string{
1: "operation not permitted", // EPERM
2: "no such file or directory", // ENOENT
3: "no such process", // ESRCH
// ...
}
func (e Errno) Error() string {
if 0 <= int(e) && int(e) < len(errors) {
return errors[e]
}
return fmt.Sprintf("errno %d", e)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
9. 类型断言
类型断言是一个使用在接口值上的操作。语法上它看起来像 x.(T) 被称为断言类型,这里 x 表示一个接口的类型和 T 表示一个类型,这里有两种可能:
第一种,如果断言的类型
T是一个具体类型类型断言检查
x的动态类型是否和T相同。如果这个检查成功了,类型断言的结果是x的动态值,当然它的类型是T。换句话说,具体类型的类型断言从它的操作对象中获得具体的值。如果检查失败,接下来这个操作会抛出panic。Govar 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.Buffer1
2
3
4第二种,如果相反断言的类型
T是一个接口类型类型断言检查
x的动态类型是否满足T。如果这个检查成功了,即便动态值没有获取到,这个结果仍然是一个有相同类型和值部分的接口值。在下面的第一个类型断言后,
w和rw都持有os.Stdout因此它们每个有一个动态类型*os.File,但是变量w是一个io.Writer类型只对外公开出Write方法,而rw变量同时也会公开Read方法。Govar w io.Writer w = os.Stdout rw := w.(io.ReadWriter) // success: *os.File has both Read and Write w = new(ByteCounter) rw = w.(io.ReadWriter) // panic: *ByteCounter has no Read method1
2
3
4
5
经常地我们对一个接口值的动态类型是不确定的,并且我们更愿意去检验它是否是一些特定的类型:
Go
var w io.Writer = os.Stdout
f, ok := w.(*os.File) // success: ok, f == os.Stdout
b, ok := w.(*bytes.Buffer) // failure: !ok, b == nil1
2
3
2
3
9.1. 通过类型断言区分错误类型
通常我们会使用一个专门的类型来描述结构化的错误,如 os.PathError:
Go
package os
// PathError records an error and the operation and file path that caused it.
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
在文件操作相关的错误类型中,如:文件已经存在(对于创建操作)、找不到文件(对于读取操作)以及权限拒绝等情况我们需要进行不同的逻辑处理。此时我们可以使用 os 包中提供的三个帮助函数来对其进行区分:
Go
package os
func IsExist(err error) bool
func IsNotExist(err error) bool
func IsPermission(err error) bool1
2
3
4
5
2
3
4
5
以 IsNotExist 函数的实现方式为例,首先 IsNotExist 会对 err 对象进行断言,确定 err 是否为 *os.PathError 类型,并确定其内部的错误是否为 syscall.ENOENT、os.ErrNotExist 其中之一:
Go
import (
"errors"
"syscall"
)
var ErrNotExist = errors.New("file does not exist")
// IsNotExist returns a boolean indicating whether the error is known to
// report that a file or directory does not exist. It is satisfied by
// ErrNotExist as well as some syscall errors.
func IsNotExist(err error) bool {
if pe, ok := err.(*PathError); ok {
err = pe.Err
}
return err == syscall.ENOENT || err == ErrNotExist
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
使用示例:
Go
_, err := os.Open("/no/such/file")
fmt.Println(os.IsNotExist(err)) // "true"1
2
2
9.2. 通过类型断言确认接口类型
通常,许多满足 io.Writer 接口的重要类型同时也有 WriteString 方法(如:*bytes.Buffer、*os.File 和 *bufio.Writer),但这并不是确定的。为了进一步确认其是否真的实现了 WriteString 方法,我们可以定义一个只有这个方法的新接口,并且使用类型断言来检测该对象是否满足这个新接口(第 4 ~ 9 行):
Go
// writeString writes s to w.
// If w has a WriteString method, it is invoked instead of w.Write.
func writeString(w io.Writer, s string) (n int, err error) {
type stringWriter interface {
WriteString(string) (n int, err error)
}
if sw, ok := w.(stringWriter); ok {
return sw.WriteString(s) // avoid a copy
}
return w.Write([]byte(s)) // allocate temporary copy
}
func writeHeader(w io.Writer, contentType string) error {
if _, err := writeString(w, "Content-Type: "); err != nil {
return err
}
if _, err := writeString(w, contentType); err != nil {
return err
}
// ...
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
实际上,在标准库已有提供了
io.WriteString函数,这是向一个io.Writer接口写入字符串的推荐方法。
10. 类型 switch
一个类型 switch 同普通的 switch 语句一样,它的运算对象是 x.(type),它使用了关键词字面量 type,并且每个 case 有一到多个类型。一个类型 switch 基于这个接口值的动态类型使一个多路分支有效。nil 的 case 和 if x == nil 匹配,在其它 case 都不匹配的情况下匹配 default case:
Go
switch x.(type) {
case nil: // ...
case int, uint: // ...
case bool: // ...
case string: // ...
default: // ...
}1
2
3
4
5
6
7
2
3
4
5
6
7
类型 switch 语句有一个扩展的形式,它可以将提取的值绑定到一个在每个 case 范围内的新变量:
Go
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))
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在上述这个函数中,在每个单一类型的 case 内部,变量 x 和这个 case 的类型相同。例如,变量 x 在 bool 的 case 中是 bool 类型,在 string 的 case 中是 string 类型。在所有其它的情况中,变量 x 是 switch 运算对象的类型(接口);在这个例子中运算对象是一个 interface{}。当多个 case 需要相同的操作时,比如 int 和 uint 的情况,类型 switch 可以很容易的合并这些情况。