第7章,部分字词修订,语序调整。少量错误修订。

This commit is contained in:
zhliner 2017-08-24 22:29:24 +08:00
parent 9a9b9a0594
commit 17919273e1
14 changed files with 87 additions and 87 deletions

View File

@ -2,9 +2,9 @@
目前为止,我们看到的类型都是具体的类型。一个具体的类型可以准确的描述它所代表的值,并且展示出对类型本身的一些操作方式:就像数字类型的算术操作,切片类型的取下标、添加元素和范围获取操作。具体的类型还可以通过它的内置方法提供额外的行为操作。总的来说,当你拿到一个具体的类型时你就知道它的本身是什么和你可以用它来做什么。
在Go语言中还存在着另外一种类型接口类型。接口类型是一种抽象的类型。它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合它们只会展示出它们自己的方法。也就是说当你有看到一个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的方法来做什么。
在Go语言中还存在着另外一种类型接口类型。接口类型是一种抽象的类型。它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合它们只会表现出它们自己的方法。也就是说当你有看到一个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的方法来做什么。
在本书中我们一直使用两个相似的函数来进行字符串的格式化fmt.Printf它会把结果写到标准输出和fmt.Sprintf它会把结果以字符串的形式返回。得益于使用接口我们不必可悲的因为返回结果在使用方式上的一些浅显不同就必需把格式化这个最困难的过程复制一份。实际上这两个函数都使用了另一个函数fmt.Fprintf来进行封装。fmt.Fprintf这个函数对它的计算结果会被怎么使用是完全不知道的。
在本书中我们一直使用两个相似的函数来进行字符串的格式化fmt.Printf它会把结果写到标准输出和fmt.Sprintf它会把结果以字符串的形式返回。得益于使用接口我们不必可悲的因为返回结果在使用方式上的一些浅显不同就必需把格式化这个最困难的过程复制一份。实际上这两个函数都使用了另一个函数fmt.Fprintf来进行封装。fmt.Fprintf这个函数对它的计算结果会被怎么使用是完全不知道的。
``` go
package fmt
@ -23,7 +23,7 @@ func Sprintf(format string, args ...interface{}) string {
Fprintf的前缀F表示文件(File)也表明格式化输出结果应该被写入第一个参数提供的文件中。在Printf函数中的第一个参数os.Stdout是`*os.File`类型在Sprintf函数中的第一个参数&buf是一个指向可以写入字节的内存缓冲区然而它
并不是一个文件类型尽管它在某种意义上和文件类型相似。
即使Fprintf函数中的第一个参数也不是一个文件类型。它是io.Writer类型这是一个接口类型定义如下
即使Fprintf函数中的第一个参数也不是一个文件类型。它是io.Writer类型这是一个接口类型定义如下:
``` go
package io
@ -43,9 +43,9 @@ type Writer interface {
io.Writer类型定义了函数Fprintf和这个函数调用者之间的约定。一方面这个约定需要调用者提供具体类型的值就像`*os.File`和`*bytes.Buffer`这些类型都有一个特定签名和行为的Write的函数。另一方面这个约定保证了Fprintf接受任何满足io.Writer接口的值都可以工作。Fprintf函数可能没有假定写入的是一个文件或是一段内存而是写入一个可以调用Write函数的值。
因为fmt.Fprintf函数没有对具体操作的值做任何假设而是仅仅通过io.Writer接口的约定来保证行为所以第一个参数可以安全地传入一个任何具体类型的值只需要满足io.Writer接口。一个类型可以自由的使用另一个满足相同接口的类型来进行替换被称作可替换性(LSP里氏替换)。这是一个面向对象的特征。
因为fmt.Fprintf函数没有对具体操作的值做任何假设而是仅仅通过io.Writer接口的约定来保证行为所以第一个参数可以安全地传入一个只需要满足io.Writer接口的任意具体类型的值。一个类型可以自由地被另一个满足相同接口的类型替换被称作可替换性(LSP里氏替换)。这是一个面向对象的特征。
让我们通过一个新的类型来进行校验,下面`*ByteCounter`类型里的Write方法仅仅在丢写向它的字节前统计它们的长度。(在这个+=赋值语句中让len(p)的类型和`*c`的类型匹配的转换是必须的。)
让我们通过一个新的类型来进行校验,下面`*ByteCounter`类型里的Write方法仅仅在丢写向它的字节前统计它们的长度。(在这个+=赋值语句中让len(p)的类型和`*c`的类型匹配的转换是必须的。)
<u><i>gopl.io/ch7/bytecounter</i></u>
```go
@ -84,12 +84,12 @@ type Stringer interface {
我们会在7.10节解释fmt包怎么发现哪些值是满足这个接口类型的。
**练习 7.1** 使用来自ByteCounter的思路实现一个针对单词和行数的计数器。你会发现bufio.ScanWords非常的有用。
**练习 7.1** 使用来自ByteCounter的思路实现一个针对单词和行数的计数器。你会发现bufio.ScanWords非常的有用。
**练习 7.2** 写一个带有如下函数签名的函数CountingWriter传入一个io.Writer接口类型返回一个新的Writer类型把原来的Writer封装在里面和一个表示写入新的Writer字节数的int64类型指针
**练习 7.2** 写一个带有如下函数签名的函数CountingWriter传入一个io.Writer接口类型返回一个把原来的Writer封装在里面的新的Writer类型和一个表示新的写入字节数的int64类型指针。
```go
func CountingWriter(w io.Writer) (io.Writer, *int64)
```
**练习 7.3** 为在gopl.io/ch4/treesort (§4.4)的*tree类型实现一个String方法去展示tree类型的值序列。
**练习 7.3** 为在gopl.io/ch4/treesort (§4.4)的*tree类型实现一个String方法去展示tree类型的值序列。

View File

@ -2,7 +2,7 @@
接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。
io.Writer类型是用最广泛的接口之一,因为它提供了所有类型写入bytes的抽象包括文件类型内存缓冲区网络链接HTTP客户端压缩工具哈希等等。io包中定义了很多其它有用的接口类型。Reader可以代表任意可以读取bytes的类型Closer可以是任意可以关闭的值例如一个文件或是网络链接。到现在你可能注意到了很多Go语言中单方法接口的命名习惯
io.Writer类型是用最广泛的接口之一,因为它提供了所有类型写入bytes的抽象包括文件类型内存缓冲区网络链接HTTP客户端压缩工具哈希等等。io包中定义了很多其它有用的接口类型。Reader可以代表任意可以读取bytes的类型Closer可以是任意可以关闭的值例如一个文件或是网络链接。到现在你可能注意到了很多Go语言中单方法接口的命名习惯
```go
package io
@ -14,7 +14,7 @@ type Closer interface {
}
```
往下看,我们发现有些新的接口类型通过组合已有的接口来定义。下面是两个例子:
往下看,我们发现有些新的接口类型通过组合已有的接口来定义。下面是两个例子:
```go
type ReadWriter interface {
@ -27,7 +27,7 @@ type ReadWriteCloser interface {
Closer
}
```
上面用到的语法和结构内嵌相似,我们可以用这种方式以一个简写命名一个接口,而不用声明它所有的方法。这种方式称为接口内嵌。尽管略失简洁我们可以像下面这样不使用内嵌来声明io.Writer接口。
上面用到的语法和结构内嵌相似我们可以用这种方式以一个简写命名一个接口而不用声明它所有的方法。这种方式称为接口内嵌。尽管略失简洁我们可以像下面这样不使用内嵌来声明io.ReadWriter接口。
```go
type ReadWriter interface {
@ -36,7 +36,7 @@ type ReadWriter interface {
}
```
或者甚至使用种混合的风格:
或者甚至使用种混合的风格:
```go
type ReadWriter interface {
@ -45,9 +45,9 @@ type ReadWriter interface {
}
```
上面3种定义方式都是一样的效果。方法顺序变化也没有影响,唯一重要的就是这个集合里面的方法。
上面3种定义方式都是一样的效果。方法顺序变化也没有影响,唯一重要的就是这个集合里面的方法。
**练习 7.4** strings.NewReader函数通过读取一个string参数返回一个满足io.Reader接口类型的值和其它值。实现一个简单版本的NewReader用它来构造一个接收字符串输入的HTML解析器§5.2
**练习 7.4** strings.NewReader函数通过读取一个string参数返回一个满足io.Reader接口类型的值和其它值。实现一个简单版本的NewReader用它来构造一个接收字符串输入的HTML解析器§5.2
**练习 7.5** io包里面的LimitReader函数接收一个io.Reader接口类型的r和字节数n并且返回另一个从r中读取字节但是当读完n个字节后就表示读到文件结束的Reader。实现这个LimitReader函数

View File

@ -24,7 +24,7 @@ rwc = w // compile error: io.Writer lacks Close method
因为ReadWriter和ReadWriteCloser包含所有Writer的方法所以任何实现了ReadWriter和ReadWriteCloser的类型必定也实现了Writer接口
在进一步学习前,必须先解释表示一个类型持有一个方法当中的细节。回想在6.2章中对于每一个命名过的具体类型T它一些方法的接收者是类型T本身然而另一些则是一个`*T`的指针。还记得在T类型的参数上调用一个`*T`的方法是合法的只要这个参数是一个变量编译器隐式的获取了它的地址。但这仅仅是一个语法糖T类型的值不拥有所有`*T`指针的方法,这样它就可能只实现更少的接口。
在进一步学习前,必须先解释一个类型持有一个方法的表示当中的细节。回想在6.2章中对于每一个命名过的具体类型T一些方法的接收者是类型T本身然而另一些则是一个`*T`的指针。还记得在T类型的参数上调用一个`*T`的方法是合法的只要这个参数是一个变量编译器隐式的获取了它的地址。但这仅仅是一个语法糖T类型的值不拥有所有`*T`指针的方法,这样它就可能只实现更少的接口。
举个例子可能会更清晰一点。在第6.5章中IntSet类型的String方法的接收者是一个指针类型所以我们不能在一个不能寻址的IntSet值上调用这个方法
@ -50,7 +50,7 @@ var _ fmt.Stringer = s // compile error: IntSet lacks String method
12.8章包含了一个打印出任意值的所有方法的程序然后可以使用godoc -analysis=type tool(§10.7.4)展示每个类型的方法和具体类型和接口之间的关系
就像信封封装和隐藏信件来一样,接口类型封装和隐藏具体类型和它的值。即使具体类型有其它的方法也只有接口类型暴露出来的方法会被调用到:
就像信封封装和隐藏信件来一样,接口类型封装和隐藏具体类型和它的值。即使具体类型有其它的方法也只有接口类型暴露出来的方法会被调用到:
```go
os.Stdout.Write([]byte("hello")) // OK: *os.File has Write method
@ -62,7 +62,7 @@ w.Write([]byte("hello")) // OK: io.Writer has Write method
w.Close() // compile error: io.Writer lacks Close method
```
一个有更多方法的接口类型比如io.ReadWriter和少一些方法的接口类型,例如io.Reader进行对比更多方法的接口类型会告诉我们更多关于它的值持有的信息并且对实现它的类型要求更加严格。那么关于interface{}类型,它没有任何方法,请讲出哪些具体的类型实现了它?
一个有更多方法的接口类型比如io.ReadWriter和少一些方法的接口类型例如io.Reader进行对比更多方法的接口类型会告诉我们更多关于它的值持有的信息并且对实现它的类型要求更加严格。那么关于interface{}类型,它没有任何方法,请讲出哪些具体的类型实现了它?
这看上去好像没有用但实际上interface{}被称为空接口类型是不可或缺的。因为空接口类型对实现它的类型没有要求,所以我们可以将任意一个值赋给空接口类型。
@ -75,11 +75,11 @@ any = map[string]int{"one": 1}
any = new(bytes.Buffer)
```
尽管不是很明显,从本书最早的例子中我们就已经在使用空接口类型。它允许像fmt.Println或者5.7章中的errorf函数接受任何类型的参数。
尽管不是很明显从本书最早的例子中我们就已经在使用空接口类型。它允许像fmt.Println或者5.7章中的errorf函数接受任何类型的参数。
对于创建的一个interface{}值持有一个booleanfloatstringmappointer或者任意其它的类型我们当然不能直接对它持有的值做操作因为interface{}没有任何方法。我们会在7.10章中学到一种用类型断言来获取interface{}中值的方法。
因为接口实现只依赖于判断两个类型的方法,所以没有必要定义一个具体类型和它实现的接口之间的关系。也就是说,尝试文档化和断言这种关系几乎没有用,所以并没有通过程序强制定义。下面的定义在编译期断言一个`*bytes.Buffer`的值实现了io.Writer接口类型:
因为接口实现只依赖于判断两个类型的方法,所以没有必要定义一个具体类型和它实现的接口之间的关系。也就是说,尝试文档化和断言这种关系几乎没有用,所以并没有通过程序强制定义。下面的定义在编译期断言一个`*bytes.Buffer`的值实现了io.Writer接口类型:
```go
// *bytes.Buffer must satisfy io.Writer
@ -139,7 +139,7 @@ type Video interface {
}
```
这些接口不止是一种有用的方式来分组相关的具体类型和表示他们之间的共同特。我们后面可能会发现其它的分组。举例如果我们发现我们需要以同样的方式处理Audio和Video我们可以定义一个Streamer接口来代表它们之间相同的部分而不必对已经存在的类型做改变。
这些接口不止是一种有用的方式来分组相关的具体类型和表示他们之间的共同特。我们后面可能会发现其它的分组。举例如果我们发现我们需要以同样的方式处理Audio和Video我们可以定义一个Streamer接口来代表它们之间相同的部分而不必对已经存在的类型做改变。
```go
type Streamer interface {

View File

@ -39,9 +39,9 @@ if out != nil {
![](../images/ch7-05.png)
动态分配机制依然决定(\*bytes.Buffer).Write的方法会被调用但是这次的接收者的值是nil。对于一些如\*os.File的类型nil是一个有效的接收者(§6.2.1),但是\*bytes.Buffer类型不在这些类中。这个方法会被调用但是当它尝试去获取缓冲区时会发生panic。
动态分配机制依然决定(\*bytes.Buffer).Write的方法会被调用但是这次的接收者的值是nil。对于一些如\*os.File的类型nil是一个有效的接收者(§6.2.1),但是\*bytes.Buffer类型不在这些类中。这个方法会被调用但是当它尝试去获取缓冲区时会发生panic。
问题在于尽管一个nil的\*bytes.Buffer指针有实现这个接口的方法它也不满足这个接口具体的行为上的要求。特别是这个调用违反了(\*bytes.Buffer).Write方法的接收者非空的隐含先觉条件所以将nil指针赋给这个接口是错误的。解决方案就是将main函数中的变量buf的类型改为io.Writer因此可以避免一开始就将一个不完的值赋值给这个接口:
问题在于尽管一个nil的\*bytes.Buffer指针有实现这个接口的方法它也不满足这个接口具体的行为上的要求。特别是这个调用违反了(\*bytes.Buffer).Write方法的接收者非空的隐含先觉条件所以将nil指针赋给这个接口是错误的。解决方案就是将main函数中的变量buf的类型改为io.Writer因此可以避免一开始就将一个不完的值赋值给这个接口:
```go
var buf io.Writer

View File

@ -20,7 +20,7 @@ var w io.Writer
![](../images/ch7-01.png)
一个接口值基于它的动态类型被描述为空或非空所以这是一个空的接口值。你可以通过使用w==nil或者w!=nil来判接口值是否为空。调用一个空接口值上的任意方法都会产生panic:
一个接口值基于它的动态类型被描述为空或非空所以这是一个空的接口值。你可以通过使用w==nil或者w!=nil来判接口值是否为空。调用一个空接口值上的任意方法都会产生panic:
```go
w.Write([]byte("hello")) // panic: nil pointer dereference
@ -32,7 +32,7 @@ w.Write([]byte("hello")) // panic: nil pointer dereference
w = os.Stdout
```
这个赋值过程调用了一个具体类型到接口类型的隐式转换这和显式的使用io.Writer(os.Stdout)是等价的。这类转换不管是显式的还是隐式的,都会刻画出操作到的类型和值。这个接口值的动态类型被设为`*os.Stdout`指针的类型描述符它的动态值持有os.Stdout的拷贝这是一个代表处理标准输出的os.File类型变量的指针图7.2)。
这个赋值过程调用了一个具体类型到接口类型的隐式转换这和显式的使用io.Writer(os.Stdout)是等价的。这类转换不管是显式的还是隐式的,都会刻画出操作到的类型和值。这个接口值的动态类型被设为`*os.File`指针的类型描述符它的动态值持有os.Stdout的拷贝这是一个代表处理标准输出的os.File类型变量的指针图7.2)。
![](../images/ch7-02.png)
@ -72,7 +72,7 @@ w.Write([]byte("hello")) // writes "hello" to the bytes.Buffers
w = nil
```
这个重置将它所有的部分都设为nil值把变量w恢复到和它之前定义时相同的状态在图7.1中可以看到。
这个重置将它所有的部分都设为nil值把变量w恢复到和它之前定义时相同的状态在图7.1中可以看到。
一个接口值可以持有任意大的动态值。例如表示时间实例的time.Time类型这个类型有几个对外不公开的字段。我们从它上面创建一个接口值,
@ -84,7 +84,7 @@ var x interface{} = time.Now()
![](../images/ch7-04.png)
接口值可以使用==和!来进行比较。两个接口值相等仅当它们都是nil值或者它们的动态类型相同并且动态值也根据这个动态类型的操作相等。因为接口值是可比较的所以它们可以用在map的键或者作为switch语句的操作数。
接口值可以使用==和!来进行比较。两个接口值相等仅当它们都是nil值或者它们的动态类型相同并且动态值也根据这个动态类型的==操作相等。因为接口值是可比较的所以它们可以用在map的键或者作为switch语句的操作数。
然而如果两个接口值的动态类型相同但是这个动态类型是不可比较的比如切片将它们进行比较就会失败并且panic:

View File

@ -34,11 +34,11 @@ sort.Sort(StringSlice(names))
对字符串切片的排序是很常用的需要所以sort包提供了StringSlice类型也提供了Strings函数能让上面这些调用简化成sort.Strings(names)。
这里用到的技术很容易适用到其它排序序列中,例如我们可以忽略大些或者含有特殊的字符。本书使用Go程序对索引词和页码进行排序也用到了这个技术对罗马数字做了额外逻辑处理。对于更复杂的排序我们使用相同的方法但是会用更复杂的数据结构和更复杂地实现sort.Interface的方法。
这里用到的技术很容易适用到其它排序序列中,例如我们可以忽略大小写或者含有的特殊字符。本书使用Go程序对索引词和页码进行排序也用到了这个技术对罗马数字做了额外逻辑处理。对于更复杂的排序我们使用相同的方法但是会用更复杂的数据结构和更复杂地实现sort.Interface的方法。
我们会运行上面的例子来对一个表格中的音乐播放列表进行排序。每个track都是单独的一行每一列都是这个track的属性像艺术家标题和运行时间。想象一个图形用户界面来呈现这个表格并且点击一个属性的顶部会使这个列表按照这个属性进行排序再一次点击相同属性的顶部会进行逆向排序。让我们看下每个点击会发生什么响应。
下面的变量tracks包了一个播放列表。One of the authors apologizes for the other authors musical tastes.每个元素都不是Track本身而是指向它的指针。尽管我们在下面的代码中直接存储Tracks也可以工作sort函数会交换很多对元素所以如果每个元素都是指针会更快而不是全部Track类型指针是一个机器字码长度而Track类型可能是八个或更多。
下面的变量tracks包了一个播放列表。One of the authors apologizes for the other authors musical tastes.每个元素都不是Track本身而是指向它的指针。尽管我们在下面的代码中直接存储Tracks也可以工作sort函数会交换很多对元素所以如果每个元素都是指针而不是Track类型会更快指针是一个机器字码长度而Track类型可能是八个或更多。
<u><i>gopl.io/ch7/sorting</i></u>
```go
@ -66,7 +66,7 @@ func length(s string) time.Duration {
}
```
printTracks函数将播放列表打印成一个表格。一个图形化的展示可能会更好点但是这个小程序使用text/tabwriter包来生成一个列整齐对齐和隔开的表格,像下面展示的这样。注意到`*tabwriter.Writer`是满足io.Writer接口的。它会收集每一片写向它的数据它的Flush方法会格式化整个表格并且将它写向os.Stdout标准输出
printTracks函数将播放列表打印成一个表格。一个图形化的展示可能会更好点但是这个小程序使用text/tabwriter包来生成一个列整齐对齐和隔开的表格像下面展示的这样。注意到`*tabwriter.Writer`是满足io.Writer接口的。它会收集每一片写向它的数据它的Flush方法会格式化整个表格并且将它写向os.Stdout标准输出
```go
func printTracks(tracks []*Track) {
@ -81,7 +81,7 @@ func printTracks(tracks []*Track) {
}
```
为了能按照Artist字段对播放列表进行排序我们会像对StringSlice那样定义一个新的带有必须LenLess和Swap方法的切片类型。
为了能按照Artist字段对播放列表进行排序我们会像对StringSlice那样定义一个新的带有必须LenLess和Swap方法的切片类型。
```go
type byArtist []*Track
@ -124,7 +124,7 @@ Go Delilah From the Roots Up 2012 3m38s
Go Ahead Alicia Keys As I Am 2007 4m36s
```
sort.Reverse函数值得进行更近一步的学习因为它使用了(§6.3)章中的组合这是一个重要的思路。sort包定义了一个不公开的struct类型reverse它嵌入了一个sort.Interface。reverse的Less方法调用了内嵌的sort.Interface值的Less方法但是通过交换索引的方式使排序结果变成逆序。
sort.Reverse函数值得进行更近一步的学习因为它使用了(§6.3)章中的组合这是一个重要的思路。sort包定义了一个不公开的struct类型reverse它嵌入了一个sort.Interface。reverse的Less方法调用了内嵌的sort.Interface值的Less方法但是通过交换索引的方式使排序结果变成逆序。
```go
package sort
@ -136,7 +136,7 @@ func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) }
func Reverse(data Interface) Interface { return reverse{data} }
```
reverse的另外两个方法Len和Swap隐式地由原有内嵌的sort.Interface提供。因为reverse是一个不公开的类型所以导出函数Reverse函数返回一个包含原有sort.Interface值的reverse类型实例。
reverse的另外两个方法Len和Swap隐式地由原有内嵌的sort.Interface提供。因为reverse是一个不公开的类型所以导出函数Reverse返回一个包含原有sort.Interface值的reverse类型实例。
为了可以按照不同的列进行排序我们必须定义一个新的类型例如byYear
@ -166,9 +166,9 @@ type customSort struct {
less func(x, y *Track) bool
}
func (x customSort) Len() int
func (x customSort) Len() int { return len(x.t) }
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] }
func (x customSort) Swap(i, j int) { x.t[i], x.t[j] = x.t[j], x.t[i] }
```
让我们定义一个多层的排序函数它主要的排序键是标题第二个键是年第三个键是运行时间Length。下面是该排序的调用其中这个排序使用了匿名排序函数
@ -199,7 +199,7 @@ Go Ahead Alicia Keys As I Am 2007 4m36s
Ready 2 Go Martin Solveig Smash 2011 4m24s
```
尽管对长度为n的序列排序需要 O(n log n)次比较操作检查一个序列是否已经有序至少需要n1次比较。sort包中的IsSorted函数帮我们做这样的检查。像sort.Sort一样它也使用sort.Interface对这个序列和它的排序函数进行抽象但是它从不会调用Swap方法这段代码示范了IntsAreSorted和Ints函数和IntSlice类型的使用:
尽管对长度为n的序列排序需要 O(n log n)次比较操作检查一个序列是否已经有序至少需要n-1次比较。sort包中的IsSorted函数帮我们做这样的检查。像sort.Sort一样它也使用sort.Interface对这个序列和它的排序函数进行抽象但是它从不会调用Swap方法这段代码示范了IntsAreSorted和Ints函数在IntSlice类型上的使用:
```go
values := []int{3, 1, 4, 1}

View File

@ -15,7 +15,7 @@ func ListenAndServe(address string, h Handler) error
ListenAndServe函数需要一个例如“localhost:8000”的服务器地址和一个所有请求都可以分派的Handler接口实例。它会一直运行直到这个服务因为一个错误而失败或者启动失败它的返回值一定是一个非空的错误。
想象一个电子商务网站,为了销售它的数据库将它物品的价格映射成美元。下面这个程序可能是能想到的最简单的实现了。它将库存清单模型化为一个命名为database的map类型我们给这个类型一个ServeHttp方法这样它可以满足http.Handler接口。这个handler会遍历整个map并输出物品信息。
想象一个电子商务网站,为了销售,将数据库中物品的价格映射成美元。下面这个程序可能是能想到的最简单的实现了。它将库存清单模型化为一个命名为database的map类型我们给这个类型一个ServeHttp方法这样它可以满足http.Handler接口。这个handler会遍历整个map并输出物品信息。
<u><i>gopl.io/ch7/http1</i></u>
```go
@ -53,7 +53,7 @@ shoes: $50.00
socks: $5.00
```
目前为止这个服务器不考虑URL只能为每个请求列出它全部的库存清单。更真实的服务器会定义多个不同的URL每一个都会触发一个不同的行为。让我们使用/list来调用已经存在的这个行为并且增加另一个/price调用表明单个货品的价格像这样/price?item=socks来指定一个请求参数。
目前为止这个服务器不考虑URL只能为每个请求列出它全部的库存清单。更真实的服务器会定义多个不同的URL每一个都会触发一个不同的行为。让我们使用/list来调用已经存在的这个行为并且增加另一个/price调用表明单个货品的价格像这样/price?item=socks来指定一个请求参数。
<u><i>gopl.io/ch7/http2</i></u>
```go
@ -112,7 +112,7 @@ no such page: /help
对于更复杂的应用一些ServeMux可以通过组合来处理更加错综复杂的路由需求。Go语言目前没有一个权威的web框架就像Ruby语言有Rails和python有Django。这并不是说这样的框架不存在而是Go语言标准库中的构建模块就已经非常灵活以至于这些框架都是不必要的。此外尽管在一个项目早期使用框架是非常方便的但是它们带来额外的复杂度会使长期的维护更加困难。
在下面的程序中我们创建一个ServeMux并且使用它将URL和相应处理/list和/price操作的handler联系起来这些操作逻辑都已经被分到不同的方法中。然后我门在调用ListenAndServe函数中使用ServeMux为主要的handler。
在下面的程序中我们创建一个ServeMux并且使用它将URL和相应处理/list和/price操作的handler联系起来这些操作逻辑都已经被分到不同的方法中。然后我门在调用ListenAndServe函数中使用ServeMux为主要的handler。
<u><i>gopl.io/ch7/http3</i></u>
```go
@ -144,13 +144,13 @@ func (db database) price(w http.ResponseWriter, req *http.Request) {
}
```
让我们关注这两个注册到handlers上的调用。第一个db.list是一个方法值 (§6.4),它是下面这个类型的值
让我们关注这两个注册到handlers上的调用。第一个db.list是一个方法值 (§6.4),它是下面这个类型的值
```go
func(w http.ResponseWriter, req *http.Request)
```
也就是说db.list的调用会援引一个接收者是db的database.list方法。所以db.list是一个实现了handler类似行为的函数但是因为它没有方法所以它不满足http.Handler接口并且不能直接传给mux.Handle。
也就是说db.list的调用会援引一个接收者是db的database.list方法。所以db.list是一个实现了handler类似行为的函数但是因为它没有方法(理解:该方法没有它自己的方法)所以它不满足http.Handler接口并且不能直接传给mux.Handle。
语句http.HandlerFunc(db.list)是一个转换而非一个函数调用因为http.HandlerFunc是一个类型。它有如下的定义
@ -165,7 +165,7 @@ func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
}
```
HandlerFunc显示了在Go语言接口机制中一些不同寻常的特点。这是一个实现了接口http.Handler方法的函数类型。ServeHTTP方法的行为调用了它本身的函数。因此HandlerFunc是一个让函数值满足一个接口的适配器这里函数和这个接口仅有的方法有相同的函数签名。实际上这个技巧让一个单一的类型例如database以多种方式满足http.Handler接口一种通过它的list方法一种通过它的price方法等等。
HandlerFunc显示了在Go语言接口机制中一些不同寻常的特点。这是一个实现了接口http.Handler方法的函数类型。ServeHTTP方法的行为调用了它的函数本身。因此HandlerFunc是一个让函数值满足一个接口的适配器这里函数和这个接口仅有的方法有相同的函数签名。实际上这个技巧让一个单一的类型例如database以多种方式满足http.Handler接口一种通过它的list方法一种通过它的price方法等等。
因为handler通过这种方式注册非常普遍ServeMux有一个方便的HandleFunc方法它帮我们简化handler注册代码成这样
@ -175,7 +175,7 @@ mux.HandleFunc("/list", db.list)
mux.HandleFunc("/price", db.price)
```
从上面的代码很容易看出应该怎么构建一个程序它有两个不同的web服务器监听不同的端口的并且定义不同的URL将它们指派到不同的handler。我们只要构建另外一个ServeMux并且调用一次ListenAndServe可能并行的。但是在大多数程序中一个web服务器就足够了。此外在一个应用程序的多个文件中定义HTTP handler也是非常典型的如果它们必须全部都显示的注册到这个应用的ServeMux实例上会比较麻烦。
从上面的代码很容易看出应该怎么构建一个程序由两个不同的web服务器监听不同的端口并且定义不同的URL将它们指派到不同的handler。我们只要构建另外一个ServeMux并且调用一次ListenAndServe可能并行的。但是在大多数程序中一个web服务器就足够了。此外在一个应用程序的多个文件中定义HTTP handler也是非常典型的如果它们必须全部都显式地注册到这个应用的ServeMux实例上会比较麻烦。
所以为了方便net/http包提供了一个全局的ServeMux实例DefaultServerMux和包级别的http.Handle和http.HandleFunc函数。现在为了使用DefaultServeMux作为服务器的主handler我们不需要将它传给ListenAndServe函数nil值就可以工作。
@ -191,8 +191,8 @@ func main() {
}
```
最后一个重要的提示就像我们在1.7节中提到的web服务器在一个新的协程中调用每一个handler所以当handler获取其它协程或者这个handler本身的其它请求也可以访问的变量时一定要使用预防措施比如锁机制。我们后面的两章中讲到并发相关的知识。
最后一个重要的提示就像我们在1.7节中提到的web服务器在一个新的协程中调用每一个handler所以当handler获取其它协程或者这个handler本身的其它请求也可以访问到变量时,一定要使用预防措施,比如锁机制。我们后面的两章中将讲到并发相关的知识。
**练习 7.11** 增加额外的handler让客端可以创建,读取,更新和删除数据库记录。例如,一个形如 `/update?item=socks&price=6` 的请求会更新库存清单里一个货品的价格并且当这个货品不存在或价格无效时返回一个错误值。(注意:这个修改会引入变量同时更新的问题)
**练习 7.11** 增加额外的handler让客端可以创建,读取,更新和删除数据库记录。例如,一个形如 `/update?item=socks&price=6` 的请求会更新库存清单里一个货品的价格并且当这个货品不存在或价格无效时返回一个错误值。(注意:这个修改会引入变量同时更新的问题)
**练习 7.12** 修改/list的handler让它把输出打印成一个HTML的表格而不是文本。html/template包(§4.6)可能会对你有帮助。

View File

@ -50,7 +50,7 @@ type call struct {
type Env map[Var]float64
```
我们也需要每个表式去定义一个Eval方法这个方法会根据给定的environment变量返回表达式的值。因为每个表达式都必须提供这个方法我们将它加入到Expr接口中。这个包只会对外公开ExprEnv和Var类型。调用方不需要获取其它的表达式类型就可以使用这个求值器。
我们也需要每个表式去定义一个Eval方法这个方法会根据给定的environment变量返回表达式的值。因为每个表达式都必须提供这个方法我们将它加入到Expr接口中。这个包只会对外公开ExprEnv和Var类型。调用方不需要获取其它的表达式类型就可以使用这个求值器。
```go
type Expr interface {
@ -71,7 +71,7 @@ func (l literal) Eval(_ Env) float64 {
}
```
unary和binary的Eval方法会递归的计算它的运算对象然后将运算符op作用到它们上。我们不将被零或无穷数除作为一个错误因为它们都会产生一个固定的结果无限。最后call的这个方法会计算对于powsin或者sqrt函数的参数值然后调用对应在math包中的函数。
unary和binary的Eval方法会递归的计算它的运算对象然后将运算符op作用到它们上。我们不将被零或无穷数除作为一个错误因为它们都会产生一个固定的结果——无限。最后call的这个方法会计算对于powsin或者sqrt函数的参数值然后调用对应在math包中的函数。
```go
func (u unary) Eval(env Env) float64 {
@ -111,7 +111,7 @@ func (c call) Eval(env Env) float64 {
}
```
一些方法会失败。例如一个call表达式可能未知的函数或者错误的参数个数。用一个无效的运算符如!或者<去构建一个unary或者binary表达式也是可能会发生的尽管下面提到的Parse函数不会这样做这些错误会让Eval方法panic其它的错误像计算一个没有在environment变量中出现过的Var只会让Eval方法返回一个错误的结果所有的这些错误都可以通过在计算前检查Expr来发现这是我们接下来要讲的Check方法的工作但是让我们先测试Eval方法
一些方法会失败。例如一个call表达式可能未知的函数或者错误的参数个数。用一个无效的运算符如!或者<去构建一个unary或者binary表达式也是可能会发生的尽管下面提到的Parse函数不会这样做这些错误会让Eval方法panic其它的错误像计算一个没有在environment变量中出现过的Var只会让Eval方法返回一个错误的结果所有的这些错误都可以通过在计算前检查Expr来发现这是我们接下来要讲的Check方法的工作但是让我们先测试Eval方法
下面的TestEval函数是对evaluator的一个测试。它使用了我们会在第11章讲解的testing包但是现在知道调用t.Errof会报告一个错误就足够了。这个函数循环遍历一个表格中的输入这个表格中定义了三个表达式和针对每个表达式不同的环境变量。第一个表达式根据给定圆的面积A计算它的半径第二个表达式通过两个变量x和y计算两个立方体的体积之和第三个表达式将华氏温度F转换成摄氏度。
@ -159,7 +159,7 @@ go test(§11.1) 命令会运行一个包的测试用例:
$ go test -v gopl.io/ch7/eval
```
这个-v标识可以让我们看到测试用例打印的输出正常情况下像这个一样成功的测试用例会阻止打印结果的输出。这里是测试用例里fmt.Printf语句的输出
这个-v标识可以让我们看到测试用例打印的输出正常情况下像这样一个成功的测试用例会阻止打印结果的输出。这里是测试用例里fmt.Printf语句的输出
```
sqrt(A / pi)
@ -177,7 +177,7 @@ pow(x, 3) + pow(y, 3)
幸运的是目前为止所有的输入都是适合的格式,但是我们的运气不可能一直都有。甚至在解释型语言中,为了静态错误检查语法是非常常见的;静态错误就是不用运行程序就可以检测出来的错误。通过将静态检查和动态的部分分开,我们可以快速的检查错误并且对于多次检查只执行一次而不是每次表达式计算的时候都进行检查。
让我们往Expr接口中增加另一个方法。Check方法一个表达式语义树检查出静态错误。我们马上会说明它的vars参数。
让我们往Expr接口中增加另一个方法。Check方法一个表达式语义树检查出静态错误。我们马上会说明它的vars参数。
```go
type Expr interface {
@ -248,9 +248,9 @@ log(10) unknown function "log"
sqrt(1, 2) call to sqrt has 2 args, want 1
```
Check方法的参数是一个Var类型的集合这个集合聚集从表达式中找到的变量名。为了保证成功的计算这些变量中的每一个都必须出现在环境变量中。从逻辑上讲这个集合就是调用Check方法返回的结果但是因为这个方法是递归调用的所以对于Check方法填充结果到一个作为参数传入的集合中会更加的方便。调用方在初始调用时必须提供一个空的集合。
Check方法的参数是一个Var类型的集合这个集合聚集从表达式中找到的变量名。为了保证成功的计算这些变量中的每一个都必须出现在环境变量中。从逻辑上讲这个集合就是调用Check方法返回的结果但是因为这个方法是递归调用的所以对于Check方法填充结果到一个作为参数传入的集合中会更加的方便。调用方在初始调用时必须提供一个空的集合。
在第3.2节中,我们绘制了一个在编译才确定的函数f(x,y)。现在我们可以解析检查和计算在字符串中的表达式我们可以构建一个在运行时从客户端接收表达式的web应用并且它会绘制这个函数的表示的曲面。我们可以使用集合vars来检查表达式是否是一个只有两个变量,x和y的函数——实际上是3个因为我们为了方便会提供半径大小r。并且我们会在计算前使用Check方法拒绝有格式问题的表达式这样我们就不会在下面函数的40000个计算过程100x100个栅格每一个有4个角重复这些检查。
在第3.2节中,我们绘制了一个在编译才确定的函数f(x,y)。现在我们可以解析检查和计算在字符串中的表达式我们可以构建一个在运行时从客户端接收表达式的web应用并且它会绘制这个函数的表示的曲面。我们可以使用集合vars来检查表达式是否是一个只有两个变量x和y的函数——实际上是3个因为我们为了方便会提供半径大小r。并且我们会在计算前使用Check方法拒绝有格式问题的表达式这样我们就不会在下面函数的40000个计算过程100x100个栅格每一个有4个角重复这些检查。
这个ParseAndCheck函数混合了解析和检查步骤的过程

View File

@ -11,9 +11,9 @@ f := w.(*os.File) // success: f == os.Stdout
c := w.(*bytes.Buffer) // panic: interface holds *os.File, not *bytes.Buffer
```
第二种如果相反断言的类型T是一个接口类型然后类型断言检查是否x的动态类型满足T。如果这个检查成功了动态值没有获取到这个结果仍然是一个有相同类型和值部分的接口值,但是结果有类型T。换句话说对一个接口类型的类型断言改变了类型的表述方式改变了可以获取的方法集合通常更大但是它保了接口值内部的动态类型和值的部分。
第二种,如果相反断言的类型T是一个接口类型然后类型断言检查是否x的动态类型满足T。如果这个检查成功了动态值没有获取到这个结果仍然是一个有相同动态类型和值部分的接口值,但是结果为类型T。换句话说对一个接口类型的类型断言改变了类型的表述方式改变了可以获取的方法集合通常更大但是它保了接口值内部的动态类型和值的部分。
在下面的第一个类型断言后w和rw都持有os.Stdout因此它们每个有一个动态类型`*os.File`但是变量w是一个io.Writer类型只对外公开出文件的Write方法然而rw变量也只公开它的Read方法。
在下面的第一个类型断言后w和rw都持有os.Stdout,因此它们都有一个动态类型`*os.File`但是变量w是一个io.Writer类型只对外公开了文件的Write方法而rw变量还公开了它的Read方法。
```go
var w io.Writer
@ -23,14 +23,14 @@ w = new(ByteCounter)
rw = w.(io.ReadWriter) // panic: *ByteCounter has no Read method
```
如果断言操作的对象是一个nil接口值那么不论被断言的类型是什么这个类型断言都会失败。我们几乎不需要对一个更少限制性的接口类型更少的方法集合做断言因为它表现的就像赋值操作一样除了对于nil接口值的情况。
如果断言操作的对象是一个nil接口值那么不论被断言的类型是什么这个类型断言都会失败。我们几乎不需要对一个更少限制性的接口类型更少的方法集合做断言因为它表现的就像赋值操作一样除了对于nil接口值的情况。
```go
w = rw // io.ReadWriter is assignable to io.Writer
w = rw.(io.Writer) // fails only if rw == nil
```
经常地我们对一个接口值的动态类型是不确定的并且我们更愿意去检验它是否是一些特定的类型。如果类型断言出现在一个预期有两个结果的赋值操作中例如如下的定义这个操作不会在失败的时候发生panic但是代地返回一个额外的第二个结果,这个结果是一个标识成功的布尔值:
经常地,对一个接口值的动态类型我们是不确定的并且我们更愿意去检验它是否是一些特定的类型。如果类型断言出现在一个预期有两个结果的赋值操作中例如如下的定义这个操作不会在失败的时候发生panic但是代地返回一个额外的第二个结果,这个结果是一个标识成功与否的布尔值:
```go
var w io.Writer = os.Stdout
@ -38,7 +38,7 @@ f, ok := w.(*os.File) // success: ok, f == os.Stdout
b, ok := w.(*bytes.Buffer) // failure: !ok, b == nil
```
第二个结果常规地赋值给一个命名为ok的变量。如果这个操作失败了那么ok就是false值第一个结果等于被断言类型的零值在这个例子中就是一个nil的`*bytes.Buffer`类型。
第二个结果常赋值给一个命名为ok的变量。如果这个操作失败了那么ok就是false值第一个结果等于被断言类型的零值在这个例子中就是一个nil的`*bytes.Buffer`类型。
这个ok结果经常立即用于决定程序下面做什么。if语句的扩展格式让这个变的很简洁
@ -48,7 +48,7 @@ if f, ok := w.(*os.File); ok {
}
```
当类型断言的操作对象是一个变量,你有时会看见原来的变量名重用而不是声明一个新的本地变量,这个重用的变量会覆盖原来的值,如下面这样:
当类型断言的操作对象是一个变量,你有时会看见原来的变量名重用而不是声明一个新的本地变量这个重用的变量原来的值会被覆盖理解其实是声明了一个同名的新的本地变量外层原来的w不会被改变,如下面这样:
```go
if w, ok := w.(*os.File); ok {

View File

@ -1,6 +1,6 @@
## 7.11. 基于类型断言区别错误类型
思考在os包中文件操作返回的错误集合。I/O可以因为任何数量的原因失败但是有三种经常的错误必须进行不同的处理文件已经存在对于创建操作找不到文件对于读取操作和权限拒绝。os包中提供了三个帮助函数来对给定的错误值表示的失败进行分类:
思考在os包中文件操作返回的错误集合。I/O可以因为任何数量的原因失败但是有三种经常的错误必须进行不同的处理文件已经存在对于创建操作找不到文件对于读取操作和权限拒绝。os包中提供了三个帮助函数来对给定的错误值表示的失败进行分类
```go
package os
@ -19,9 +19,9 @@ func IsNotExist(err error) bool {
}
```
但是处理I/O错误的逻辑可能一个和另一个平台非常的不同所以这种方案并不健壮并且对相同的失败可能会报出各种不同的错误消息。在测试的过程中通过检查错误消息的子字符串来保证特定的函数以期望的方式失败是非常有用的但对于线上的代码是不够的。
但是处理I/O错误的逻辑可能一个和另一个平台非常的不同所以这种方案并不健壮并且对相同的失败可能会报出各种不同的错误消息。在测试的过程中,通过检查错误消息的子字符串来保证特定的函数以期望的方式失败是非常有用的,但对于线上的代码是不够的。
一个更可靠的方式是使用一个专门的类型来描述结构化的错误。os包中定义了一个PathError类型来描述在文件路径操作中涉及到的失败像Open或者Delete操作,并且定义了一个叫LinkError的变体来描述涉及到两个文件路径的操作像Symlink和Rename。这下面是os.PathError
一个更可靠的方式是使用一个专门的类型来描述结构化的错误。os包中定义了一个PathError类型来描述在文件路径操作中涉及到的失败像Open或者Delete操作并且定义了一个叫LinkError的变体来描述涉及到两个文件路径的操作像Symlink和Rename。这下面是os.PathError
```go
package os
@ -38,7 +38,7 @@ func (e *PathError) Error() string {
}
```
大多数调用方都不知道PathError并且通过调用错误本身的Error方法来统一处理所有的错误。尽管PathError的Error方法简单地把这些字段连接起来生成错误消息PathError的结构保护了内部的错误组件。调用方需要使用类型断言来检测错误的具体类型以便将一种失败和另一种区分开具体的类型比字符串可以提供更多的细节。
大多数调用方都不知道PathError并且通过调用错误本身的Error方法来统一处理所有的错误。尽管PathError的Error方法简单地把这些字段连接起来生成错误消息PathError的结构保护了内部的错误组件。调用方需要使用类型断言来检测错误的具体类型以便将一种失败和另一种区分开具体的类型可以比字符串提供更多的细节。
```go
_, err := os.Open("/no/such/file")

View File

@ -44,9 +44,9 @@ func writeHeader(w io.Writer, contentType string) error {
}
```
为了避免重复定义我们将这个检查移入到一个实用工具函数writeString中但是它太有用了以致标准库将它作为io.WriteString函数提供。这是向一个io.Writer接口写入字符串的推荐方法。
为了避免重复定义我们将这个检查移入到一个实用工具函数writeString中但是它太有用了以致标准库将它作为io.WriteString函数提供。这是向一个io.Writer接口写入字符串的推荐方法。
这个例子的神奇之处在于没有定义了WriteString方法的标准接口和没有指定它是一个需要行为的标准接口。而且一个具体类型只会通过它的方法决定它是否满足stringWriter接口而不是任何它和这个接口类型表明的关系。它的意思就是上面的技术依赖于一个假设;这个假设就是,如果一个类型满足下面的这个接口然后WriteString(s)方法必须和Write([]byte(s))有相同的效果。
这个例子的神奇之处在于没有定义了WriteString方法的标准接口也没有指定它是一个所需行为的标准接口。一个具体类型只会通过它的方法决定它是否满足stringWriter接口而不是任何它和这个接口类型所表达的关系。它的意思就是上面的技术依赖于一个假设,这个假设就是:如果一个类型满足下面的这个接口然后WriteString(s)方法必须和Write([]byte(s))有相同的效果。
```go
interface {
@ -55,9 +55,9 @@ interface {
}
```
尽管io.WriteString记录了它的假设,但是调用它的函数极少有可能会去记录它们也做了同样的假设。定义一个特定类型的方法隐式地获取了对特定行为的协约。对于Go语言的新手特别是那些来自有强类型语言使用背景的新手可能会发现它缺乏显式的意图令人感到混乱但是在实战的过程中这几乎不是一个问题。除了空接口interface{},接口类型很少意外巧合地被实现。
尽管io.WriteString实施了这个假设,但是调用它的函数极少可能会去实施类似的假设。定义一个特定类型的方法隐式地获取了对特定行为的协约。对于Go语言的新手特别是那些来自有强类型语言使用背景的新手可能会发现它缺乏显式的意图令人感到混乱但是在实战的过程中这几乎不是一个问题。除了空接口interface{}接口类型很少意外巧合地被实现。
上面的writeString函数使用一个类型断言来知一个普遍接口类型的值是否满足一个更加具体的接口类型;并且如果满足,它会使用这个更具体接口的行为。这个技术可以被很好的使用不论这个被询问的接口是一个标准如io.ReadWriter或者用户定义的如stringWriter。
上面的writeString函数使用一个类型断言来知一个普遍接口类型的值是否满足一个更加具体的接口类型;并且如果满足,它会使用这个更具体接口的行为。这个技术可以被很好的使用不论这个被询问的接口是一个标准如io.ReadWriter或者用户定义的如stringWriter接口
这也是fmt.Fprintf函数怎么从其它所有值中区分满足error或者fmt.Stringer接口的值。在fmt.Fprintf内部有一个将单个操作对象转换成一个字符串的步骤像下面这样
@ -75,6 +75,6 @@ func formatOneValue(x interface{}) string {
}
```
如果x满足这两个接口类型中的一个具体满足的接口决定对值的格式化方式。如果都不满足默认的case或多或少会统一地使用反射来处理所有的其它类型我们可以在第12章知道具体是怎么实现的。
如果x满足这两个接口类型中的一个具体满足的接口决定对值的格式化方式。如果都不满足默认的case或多或少会统一地使用反射来处理所有的其它类型我们可以在第12章知道具体是怎么实现的。
再一次的它假设任何有String方法的类型满足fmt.Stringer中约定的行为这个行为会返回一个适合打印的字符串。
再一次的它假设任何有String方法的类型满足fmt.Stringer中约定的行为这个行为会返回一个适合打印的字符串。

View File

@ -1,8 +1,8 @@
## 7.13. 类型开关
## 7.13. 类型分支
接口被以两种不同的方式使用。在第一个方式中以io.Readerio.Writerfmt.Stringersort.Interfacehttp.Handler和error为典型一个接口的方法表达了实现这个接口的具体类型间的相似性但是隐藏了代的细节和这些具体类型本身的操作。重点在于方法上,而不是具体的类型上。
接口被以两种不同的方式使用。在第一个方式中以io.Readerio.Writerfmt.Stringersort.Interfacehttp.Handler和error为典型一个接口的方法表达了实现这个接口的具体类型间的相似性但是隐藏了代的细节和这些具体类型本身的操作。重点在于方法上,而不是具体的类型上。
第二个方式利用一个接口值可以持有各种具体类型值的能力并且将这个接口认为是这些类型的union联合。类型断言用来动态地区别这些类型并且对每一种情况都不一样。在这个方式中,重点在于具体的类型满足这个接口,而不在于接口的方法如果它确实有一些的话并且没有任何的信息隐藏。我们将以这种方式使用的接口描述为discriminated unions可辨识联合
第二个方式利用一个接口值可以持有各种具体类型值的能力,将这个接口认为是这些类型的联合。类型断言用来动态地区别这些类型,使得对每一种情况都不一样。在这个方式中重点在于具体的类型满足这个接口而不在于接口的方法如果它确实有一些的话并且没有任何的信息隐藏。我们将以这种方式使用的接口描述为discriminated unions可辨识联合
如果你熟悉面向对象编程你可能会将这两种方式当作是subtype polymorphism子类型多态和 ad hoc polymorphism非参数多态但是你不需要去记住这些术语。对于本章剩下的部分我们将会呈现一些第二种方式的例子。
@ -19,7 +19,7 @@ func listTracks(db sql.DB, artist string, minYear, maxYear int) {
}
```
Exec方法使用SQL字面量替换在查询字符串中的每个'?'SQL字面量表示相应参数的值它有可能是一个布尔值一个数字一个字符串或者nil空值。用这种方式构造查询可以帮助避免SQL注入攻击这种攻击就是对手可以通过利用输入内容中不正确的引来控制查询语句。在Exec函数内部我们可能会找到像下面这样的一个函数它会将每一个参数值转换成它的SQL字面量符号。
Exec方法使用SQL字面量替换在查询字符串中的每个'?'SQL字面量表示相应参数的值它有可能是一个布尔值一个数字一个字符串或者nil空值。用这种方式构造查询可以帮助避免SQL注入攻击这种攻击就是对手可以通过利用输入内容中不正确的引来控制查询语句。在Exec函数内部我们可能会找到像下面这样的一个函数它会将每一个参数值转换成它的SQL字面量符号。
```go
func sqlQuote(x interface{}) string {
@ -42,9 +42,9 @@ func sqlQuote(x interface{}) string {
}
```
switch语句可以简化if-else链如果这个if-else链对一连串值做相等测试。一个相似的type switch类型开关可以简化类型断言的if-else链。
switch语句可以简化if-else链如果这个if-else链对一连串值做相等测试。一个相似的type switch类型分支可以简化类型断言的if-else链。
它最简单的形式中一个类型开关像普通的switch语句一样它的运算对象是x.(type)它使用了关键词字面量type并且每个case有一到多个类型。一个类型开关基于这个接口值的动态类型使一个多路分支有效。这个nil的case和if x == nil匹配并且这个default的case和如果其它case都不匹配的情况匹配。一个对sqlQuote的类型开关可能会有这些case
最简单的形式中一个类型分支像普通的switch语句一样它的运算对象是x.(type)——它使用了关键词字面量type——并且每个case有一到多个类型。一个类型分支基于这个接口值的动态类型使一个多路分支有效。这个nil的case和if x == nil匹配并且这个default的case和如果其它case都不匹配的情况匹配。一个对sqlQuote的类型分支可能会有这些case
```go
switch x.(type) {
@ -58,15 +58,15 @@ switch x.(type) {
和(§1.8)中的普通switch语句一样每一个case会被顺序的进行考虑并且当一个匹配找到时这个case中的内容会被执行。当一个或多个case类型是接口时case的顺序就会变得很重要因为可能会有两个case同时匹配的情况。default case相对其它case的位置是无所谓的。它不会允许落空发生。
注意到在原来的函数中对于bool和string情况的逻辑需要通过类型断言访问提取的值。因为这个做法很典型类型开关语句有一个扩展的形式它可以将提取的值绑定到一个在每个case范围内的新变量。
注意到在原来的函数中对于bool和string情况的逻辑需要通过类型断言访问提取的值。因为这个做法很典型类型分支语句有一个扩展的形式它可以将提取的值绑定到一个在每个case范围内都有效的新变量。
```go
switch x := x.(type) { /* ... */ }
```
这里我们已经将新的变量也命名为x和类型断言一样重用变量名是很常见的。和一个switch语句相似地一个类型开关隐式的创建了一个语言因此新变量x的定义不会和外面块中的x变量冲突。每一个case也会隐式的创建一个单独的语言块。
这里我们已经将新的变量也命名为x和类型断言一样重用变量名是很常见的。和一个switch语句相似地一个类型分支隐式的创建了一个词法因此新变量x的定义不会和外面块中的x变量冲突。每一个case也会隐式的创建一个单独的词法块。
使用类型开关的扩展形式来重写sqlQuote函数会让这个函数更加的清晰
使用类型分支的扩展形式来重写sqlQuote函数会让这个函数更加的清晰
```go
func sqlQuote(x interface{}) string {
@ -88,6 +88,6 @@ func sqlQuote(x interface{}) string {
}
```
在这个版本的函数中在每个单一类型的case内部变量x和这个case的类型相同。例如变量x在bool的case中是bool类型和string的case中是string类型。在所有其它的情况中变量x是switch运算对象的类型接口在这个例子中运算对象是一个interface{}。当多个case需要相同的操作时比如int和uint的情况类型开关可以很容易的合并这些情况。
在这个版本的函数中在每个单一类型的case内部变量x和这个case的类型相同。例如变量x在bool的case中是bool类型和string的case中是string类型。在所有其它的情况中变量x是switch运算对象的类型接口在这个例子中运算对象是一个interface{}。当多个case需要相同的操作时比如int和uint的情况类型分支可以很容易的合并这些情况。
尽管sqlQuote接受一个任意类型的参数但是这个函数只会在它的参数匹配类型开关中的一个case时运行到结束其它情况的它会panic出“unexpected type”消息。虽然x的类型是interface{}但是我们把它认为是一个intuintboolstring和nil值的discriminated union可识别联合
尽管sqlQuote接受一个任意类型的参数但是这个函数只会在它的参数匹配类型分支中的一个case时运行到结束其它情况的它会panic出“unexpected type”消息。虽然x的类型是interface{}但是我们把它认为是一个intuintboolstring和nil值的discriminated union可识别联合

View File

@ -1,6 +1,6 @@
## 7.14. 示例: 基于标记的XML解码
第4.5章节展示了如何使用encoding/json包中的Marshal和Unmarshal函数来将JSON文档转换成Go语言的数据结构。encoding/xml包提供了一个相似的API。当我们想构造一个文档树的表示时使用encoding/xml包会很方便但是对于很多程序并不是必须的。encoding/xml包也提供了一个更低层的基于标记的API用于XML解码。在基于标记的样式中解析器消费输入产生一个标记流四个主要的标记类型StartElementEndElementCharData和Comment每一个都是encoding/xml包中的具体类型。每一个对(\*xml.Decoder).Token的调用都返回一个标记。
第4.5章节展示了如何使用encoding/json包中的Marshal和Unmarshal函数来将JSON文档转换成Go语言的数据结构。encoding/xml包提供了一个相似的API。当我们想构造一个文档树的表示时使用encoding/xml包会很方便但是对于很多程序并不是必须的。encoding/xml包也提供了一个更低层的基于标记的API用于XML解码。在基于标记的样式中解析器消费输入产生一个标记流四个主要的标记类型StartElementEndElementCharData和Comment每一个都是encoding/xml包中的具体类型。每一个对(\*xml.Decoder).Token的调用都返回一个标记。
这里显示的是和这个API相关的部分
@ -33,9 +33,9 @@ func NewDecoder(io.Reader) *Decoder
func (*Decoder) Token() (Token, error) // returns next Token in sequence
```
这个没有方法的Token接口也是一个可识别联合的例子。传统的接口如io.Reader的目的是隐藏满足它的具体类型的细节这样就可以创造出新的实现在这个实现中每个具体类型都被统一地对待。相反,满足可识别联合的具体类型的集合被设计确定和暴露而不是隐藏。可识别的联合类型几乎没有方法操作它们的函数使用一个类型开关的case集合来进行表述这个case集合中每一个case中有不同的逻辑。
这个没有方法的Token接口也是一个可识别联合的例子。传统的接口如io.Reader的目的是隐藏满足它的具体类型的细节这样就可以创造出新的实现在这个实现中每个具体类型都被统一地对待。相反,满足可识别联合的具体类型的集合被设计为确定和暴露而不是隐藏。可识别联合的类型几乎没有方法操作它们的函数使用一个类型分支的case集合来进行表述这个case集合中每一个case都有不同的逻辑。
下面的xmlselect程序获取和打印在一个XML文档树中确定的元素下找到的文本。使用上面的API它可以在输入上一次完成它的工作而从来不要具体化这个文档树。
下面的xmlselect程序获取和打印在一个XML文档树中确定的元素下找到的文本。使用上面的API它可以在输入上一次完成它的工作而从来不要实例化这个文档树。
<u><i>gopl.io/ch7/xmlselect</i></u>
```go
@ -89,7 +89,7 @@ func containsAll(x, y []string) bool {
}
```
每次main函数中的循环遇到一个StartElement时它把这个元素的名称压到一个栈里并且每次遇到EndElement时它将名称从这个栈中推出。这个API保证了StartElement和EndElement的序列可以被完全的匹配甚至在一个糟糕的文档格式中。注释会被忽略。当xmlselect遇到一个CharData时只有当栈中有序地包含所有通过命令行参数传入的元素名称时它才会输出相应的文本。
main函数中的循环遇到一个StartElement时它把这个元素的名称压到一个栈里并且每次遇到EndElement时它将名称从这个栈中推出。这个API保证了StartElement和EndElement的序列可以被完全的匹配甚至在一个糟糕的文档格式中。注释会被忽略。当xmlselect遇到一个CharData时只有当栈中有序地包含所有通过命令行参数传入的元素名称时它才会输出相应的文本。
下面的命令打印出任意出现在两层div元素下的h2元素的文本。它的输入是XML的说明文档并且它自己就是XML文档格式的。
@ -108,15 +108,15 @@ html body div div h2: B Definitions for Character Normalization
...
```
**练习 7.17** 扩展xmlselect程序以便让元素不仅可以通过名称选择也可以通过它们CSS样式上属性进行选择;例如一个像这样
**练习 7.17** 扩展xmlselect程序以便让元素不仅可以通过名称选择也可以通过它们CSS风格的属性进行选择。例如一个像这样
``` html
<div id="page" class="wide">
```
的元素可以通过匹配id或者class同时还有它的名称来进行选择。
的元素可以通过匹配id或者class同时还有它的名称来进行选择。
**练习 7.18** 使用基于标记的解码API编写一个可以读取任意XML文档和构造这个文档所代表的普通节点树的程序。节点有两种类型CharData节点表示文本字符串和 Element节点表示被命名的元素和它们的属性。每一个元素节点有一个节点的切片。
**练习 7.18** 使用基于标记的解码API编写一个可以读取任意XML文档并构造这个文档所代表的通用节点树的程序。节点有两种类型CharData节点表示文本字符串和 Element节点表示被命名的元素和它们的属性。每一个元素节点有一个节点的切片。
你可能发现下面的定义会对你有帮助。

View File

@ -4,6 +4,6 @@
当一个接口只被一个单一的具体类型实现时有一个例外,就是由于它的依赖,这个具体类型不能和这个接口存在在一个相同的包中。这种情况下,一个接口是解耦这两个包的一个好方式。
因为在Go语言中只有当两个或更多的类型实现一个接口时才使用接口它们必定会从任意特定的实现细节中抽象出来。结果就是有更少和更简单方法经常和io.Writer或 fmt.Stringer一样只有一个的更小的接口。当新的类型出现时,小的接口更容易满足。对于接口设计的一个好的标准就是 ask only for what you need只考虑你需要的东西
因为在Go语言中只有当两个或更多的类型实现一个接口时才使用接口它们必定会从任意特定的实现细节中抽象出来。结果就是有更少和更简单方法的更小的接口经常和io.Writer或 fmt.Stringer一样只有一个。当新的类型出现时小的接口更容易满足。对于接口设计的一个好的标准就是 ask only for what you need只考虑你需要的东西
我们完成了对methods和接口的学习过程。Go语言对面向对象风格的编程支持良好但这并不意味着你只能使用这一风格。不是任何事物都需要被当做一个对象独立的函数有它们自己的用处未封装的数据类型也是这样。观察一下在本书前五章的例子中像input.Scan这样的方法被调用不超过二十次与之相反的是普遍的函数调用如fmt.Printf。
我们完成了对方法和接口的学习过程。Go语言对面向对象风格的编程支持良好但这并不意味着你只能使用这一风格。不是任何事物都需要被当做一个对象独立的函数有它们自己的用处未封装的数据类型也是这样。观察一下在本书前五章的例子中像input.Scan这样的方法被调用不超过二十次与之相反的是普遍调用的函数如fmt.Printf。