mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2025-08-04 06:51:43 +00:00
Merge branch 'master' of github.com:gopl-zh/gopl-zh.github.com
This commit is contained in:
@@ -20,7 +20,7 @@ func Sprintf(format string, args ...interface{}) string {
|
||||
}
|
||||
```
|
||||
|
||||
Fprintf的前缀F表示文件(File)也表明格式化输出结果应该被写入第一个参数提供的文件中。在Printf函数中的第一个参数os.Stdout是`*os.File`类型;在Sprintf函数中的第一个参数&buf是一个指向可以写入字节的内存缓冲区,然而它
|
||||
Fprintf的前缀F表示文件(File)也表明格式化输出结果应该被写入第一个参数提供的文件中。在Printf函数中的第一个参数os.Stdout是`*os.File`类型;在Sprintf函数中的第一个参数&buf是一个指向可以写入字节的内存缓冲区,然而它
|
||||
并不是一个文件类型尽管它在某种意义上和文件类型相似。
|
||||
|
||||
即使Fprintf函数中的第一个参数也不是一个文件类型。它是io.Writer类型,这是一个接口类型定义如下:
|
||||
@@ -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
|
||||
@@ -92,4 +92,4 @@ type Stringer interface {
|
||||
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类型的值序列。
|
||||
|
@@ -22,7 +22,7 @@ w = rwc // OK: io.ReadWriteCloser has Write method
|
||||
rwc = w // compile error: io.Writer lacks Close method
|
||||
```
|
||||
|
||||
因为ReadWriter和ReadWriteCloser包含所有Writer的方法,所以任何实现了ReadWriter和ReadWriteCloser的类型必定也实现了Writer接口
|
||||
因为ReadWriter和ReadWriteCloser包含有Writer的方法,所以任何实现了ReadWriter和ReadWriteCloser的类型必定也实现了Writer接口
|
||||
|
||||
在进一步学习前,必须先解释一个类型持有一个方法的表示当中的细节。回想在6.2章中,对于每一个命名过的具体类型T;它的一些方法的接收者是类型T本身然而另一些则是一个`*T`的指针。还记得在T类型的参数上调用一个`*T`的方法是合法的,只要这个参数是一个变量;编译器隐式的获取了它的地址。但这仅仅是一个语法糖:T类型的值不拥有所有`*T`指针的方法,这样它就可能只实现了更少的接口。
|
||||
|
||||
@@ -34,7 +34,7 @@ func (*IntSet) String() string
|
||||
var _ = IntSet{}.String() // compile error: String requires *IntSet receiver
|
||||
```
|
||||
|
||||
但是我们可以在一个IntSet值上调用这个方法:
|
||||
但是我们可以在一个IntSet变量上调用这个方法:
|
||||
|
||||
```go
|
||||
var s IntSet
|
||||
@@ -95,7 +95,7 @@ var _ io.Writer = (*bytes.Buffer)(nil)
|
||||
|
||||
非空的接口类型比如io.Writer经常被指针类型实现,尤其当一个或多个接口方法像Write方法那样隐式的给接收者带来变化的时候。一个结构体的指针是非常常见的承载方法的类型。
|
||||
|
||||
但是并不意味着只有指针类型满足接口类型,甚至连一些有设置方法的接口类型也可能会被Go语言中其它的引用类型实现。我们已经看过slice类型的方法(geometry.Path, §6.1)和map类型的方法(url.Values, §6.2.1),后面还会看到函数类型的方法的例子(http.HandlerFunc, §7.7)。甚至基本的类型也可能会实现一些接口;就如我们在7.4章中看到的time.Duration类型实现了fmt.Stringer接口。
|
||||
但是并不意味着只有指针类型满足接口类型,甚至连一些有设置方法的接口类型也可能会被Go语言中其它的引用类型实现。我们已经看过slice类型的方法(geometry.Path,§6.1)和map类型的方法(url.Values,§6.2.1),后面还会看到函数类型的方法的例子(http.HandlerFunc,§7.7)。甚至基本的类型也可能会实现一些接口;就如我们在7.4章中看到的time.Duration类型实现了fmt.Stringer接口。
|
||||
|
||||
一个具体的类型可能实现了很多不相关的接口。考虑在一个组织出售数字文化产品比如音乐,电影和书籍的程序中可能定义了下列的具体类型:
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
在本章,我们会学到另一个标准的接口类型flag.Value是怎么帮助命令行标记定义新的符号的。思考下面这个会休眠特定时间的程序:
|
||||
|
||||
<u></i>gopl.io/ch7/sleep</i></u>
|
||||
<u><i>gopl.io/ch7/sleep</i></u>
|
||||
```go
|
||||
var period = flag.Duration("period", 1*time.Second, "sleep period")
|
||||
|
||||
@@ -49,7 +49,7 @@ type Value interface {
|
||||
|
||||
String方法格式化标记的值用在命令行帮组消息中;这样每一个flag.Value也是一个fmt.Stringer。Set方法解析它的字符串参数并且更新标记变量的值。实际上,Set方法和String是两个相反的操作,所以最好的办法就是对他们使用相同的注解方式。
|
||||
|
||||
让我们定义一个允许通过摄氏度或者华氏温度变换的形式指定温度的celsiusFlag类型。注意celsiusFlag内嵌了一个Celsius类型(§2.5),因此不用实现本身就已经有String方法了。为了实现flag.Value,我们只需要定义Set方法:
|
||||
让我们定义一个允许通过摄氏度或者华氏温度变换的形式指定温度的celsiusFlag类型。注意celsiusFlag内嵌了一个Celsius类型(§2.5),因此不用实现本身就已经有String方法了。为了实现flag.Value,我们只需要定义Set方法:
|
||||
|
||||
<u><i>gopl.io/ch7/tempconv</i></u>
|
||||
```go
|
||||
@@ -74,7 +74,7 @@ func (f *celsiusFlag) Set(s string) error {
|
||||
|
||||
调用fmt.Sscanf函数从输入s中解析一个浮点数(value)和一个字符串(unit)。虽然通常必须检查Sscanf的错误返回,但是在这个例子中我们不需要因为如果有错误发生,就没有switch case会匹配到。
|
||||
|
||||
下面的CelsiusFlag函数将所有逻辑都封装在一起。它返回一个内嵌在celsiusFlag变量f中的Celsius指针给调用者。Celsius字段是一个会通过Set方法在标记处理的过程中更新的变量。调用Var方法将标记加入应用的命令行标记集合中,有异常复杂命令行接口的全局变量flag.CommandLine.Programs可能有几个这个类型的变量。调用Var方法将一个`*celsiusFlag`参数赋值给一个flag.Value参数,导致编译器去检查`*celsiusFlag`是否有必须的方法。
|
||||
下面的CelsiusFlag函数将所有逻辑都封装在一起。它返回一个内嵌在celsiusFlag变量f中的Celsius指针给调用者。Celsius字段是一个会通过Set方法在标记处理的过程中更新的变量。调用Var方法将标记加入应用的命令行标记集合中,有异常复杂命令行接口的全局变量flag.CommandLine.Programs可能有几个这个类型的变量。调用Var方法将一个`*celsiusFlag`参数赋值给一个flag.Value参数,导致编译器去检查`*celsiusFlag`是否有必须的方法。
|
||||
|
||||
```go
|
||||
// CelsiusFlag defines a Celsius flag with the specified name,
|
||||
|
@@ -39,7 +39,7 @@ if out != nil {
|
||||
|
||||

|
||||
|
||||
动态分配机制依然决定(\*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,因此可以避免一开始就将一个不完整的值赋值给这个接口:
|
||||
|
||||
|
@@ -74,7 +74,7 @@ w = nil
|
||||
|
||||
这个重置将它所有的部分都设为nil值,把变量w恢复到和它之前定义时相同的状态,在图7.1中可以看到。
|
||||
|
||||
一个接口值可以持有任意大的动态值。例如,表示时间实例的time.Time类型,这个类型有几个对外不公开的字段。我们从它上面创建一个接口值,
|
||||
一个接口值可以持有任意大的动态值。例如,表示时间实例的time.Time类型,这个类型有几个对外不公开的字段。我们从它上面创建一个接口值:
|
||||
|
||||
```go
|
||||
var x interface{} = time.Now()
|
||||
|
@@ -1,4 +1,5 @@
|
||||
## 7.6. sort.Interface接口
|
||||
|
||||
排序操作和字符串格式化一样是很多程序经常使用的操作。尽管一个最短的快排程序只要15行就可以搞定,但是一个健壮的实现需要更多的代码,并且我们不希望每次我们需要的时候都重写或者拷贝这些代码。
|
||||
|
||||
幸运的是,sort包内置的提供了根据一些排序函数来对任何序列排序的功能。它的设计非常独到。在很多语言中,排序算法都是和序列数据类型关联,同时排序函数和具体类型元素关联。相比之下,Go语言的sort.Sort函数不会对具体的序列和它的元素做任何假设。相反,它使用了一个接口类型sort.Interface来指定通用的排序算法和可能被排序到的序列类型之间的约定。这个接口的实现由序列的具体表示和它希望排序的元素决定,序列的表示经常是一个切片。
|
||||
@@ -124,7 +125,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
|
||||
@@ -212,10 +213,10 @@ fmt.Println(values) // "[4 3 1 1]"
|
||||
fmt.Println(sort.IntsAreSorted(values)) // "false"
|
||||
```
|
||||
|
||||
为了使用方便,sort包为[]int,[]string和[]float64的正常排序提供了特定版本的函数和类型。对于其他类型,例如[]int64或者[]uint,尽管路径也很简单,还是依赖我们自己实现。
|
||||
为了使用方便,sort包为[]int、[]string和[]float64的正常排序提供了特定版本的函数和类型。对于其他类型,例如[]int64或者[]uint,尽管路径也很简单,还是依赖我们自己实现。
|
||||
|
||||
**练习 7.8:** 很多图形界面提供了一个有状态的多重排序表格插件:主要的排序键是最近一次点击过列头的列,第二个排序键是第二最近点击过列头的列,等等。定义一个sort.Interface的实现用在这样的表格中。比较这个实现方式和重复使用sort.Stable来排序的方式。
|
||||
|
||||
**练习 7.9:** 使用html/template包 (§4.6) 替代printTracks将tracks展示成一个HTML表格。将这个解决方案用在前一个练习中,让每次点击一个列的头部产生一个HTTP请求来排序这个表格。
|
||||
**练习 7.9:** 使用html/template包(§4.6)替代printTracks将tracks展示成一个HTML表格。将这个解决方案用在前一个练习中,让每次点击一个列的头部产生一个HTTP请求来排序这个表格。
|
||||
|
||||
**练习 7.10:** sort.Interface类型也可以适用在其它地方。编写一个IsPalindrome(s sort.Interface) bool函数表明序列s是否是回文序列,换句话说反向排序不会改变这个序列。假设如果!s.Less(i, j) && !s.Less(j, i)则索引i和j上的元素相等。
|
||||
|
@@ -1,6 +1,6 @@
|
||||
## 7.7. http.Handler接口
|
||||
|
||||
在第一章中,我们粗略的了解了怎么用net/http包去实现网络客户端(§1.5)和服务器(§1.7)。在这个小节中,我们会对那些基于http.Handler接口的服务器API做更进一步的学习:
|
||||
在第一章中,我们粗略的了解了怎么用net/http包去实现网络客户端(§1.5)和服务器(§1.7)。在这个小节中,我们会对那些基于http.Handler接口的服务器API做更进一步的学习:
|
||||
|
||||
<u><i>net/http</i></u>
|
||||
```go
|
||||
@@ -44,7 +44,7 @@ $ go build gopl.io/ch7/http1
|
||||
$ ./http1 &
|
||||
```
|
||||
|
||||
然后用1.5节中的获取程序(如果你更喜欢可以使用web浏览器)来连接服务器,我们得到下面的输出:
|
||||
然后用1.5节中的获取程序(如果你更喜欢可以使用web浏览器)来连接服务器,我们得到下面的输出:
|
||||
|
||||
```
|
||||
$ go build gopl.io/ch1/fetch
|
||||
@@ -144,7 +144,7 @@ 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)
|
||||
@@ -195,4 +195,4 @@ func main() {
|
||||
|
||||
**练习 7.11:** 增加额外的handler让客户端可以创建,读取,更新和删除数据库记录。例如,一个形如 `/update?item=socks&price=6` 的请求会更新库存清单里一个货品的价格并且当这个货品不存在或价格无效时返回一个错误值。(注意:这个修改会引入变量同时更新的问题)
|
||||
|
||||
**练习 7.12:** 修改/list的handler让它把输出打印成一个HTML的表格而不是文本。html/template包(§4.6)可能会对你有帮助。
|
||||
**练习 7.12:** 修改/list的handler让它把输出打印成一个HTML的表格而不是文本。html/template包(§4.6)可能会对你有帮助。
|
||||
|
@@ -48,7 +48,7 @@ fmt.Printf("%#v\n", err)
|
||||
// &os.PathError{Op:"open", Path:"/no/such/file", Err:0x2}
|
||||
```
|
||||
|
||||
这就是三个帮助函数是怎么工作的。例如下面展示的IsNotExist,它会报出是否一个错误和syscall.ENOENT(§7.8)或者和有名的错误os.ErrNotExist相等(可以在§5.4.2中找到io.EOF);或者是一个`*PathError`,它内部的错误是syscall.ENOENT和os.ErrNotExist其中之一。
|
||||
这就是三个帮助函数是怎么工作的。例如下面展示的IsNotExist,它会报出是否一个错误和syscall.ENOENT(§7.8)或者和有名的错误os.ErrNotExist相等(可以在§5.4.2中找到io.EOF);或者是一个`*PathError`,它内部的错误是syscall.ENOENT和os.ErrNotExist其中之一。
|
||||
|
||||
```go
|
||||
import (
|
||||
|
@@ -1,6 +1,6 @@
|
||||
## 7.12. 通过类型断言询问行为
|
||||
|
||||
下面这段逻辑和net/http包中web服务器负责写入HTTP头字段(例如:"Content-type:text/html)的部分相似。io.Writer接口类型的变量w代表HTTP响应;写入它的字节最终被发送到某个人的web浏览器上。
|
||||
下面这段逻辑和net/http包中web服务器负责写入HTTP头字段(例如:"Content-type:text/html")的部分相似。io.Writer接口类型的变量w代表HTTP响应;写入它的字节最终被发送到某个人的web浏览器上。
|
||||
|
||||
```go
|
||||
func writeHeader(w io.Writer, contentType string) error {
|
||||
|
@@ -48,15 +48,15 @@ switch语句可以简化if-else链,如果这个if-else链对一连串值做相
|
||||
|
||||
```go
|
||||
switch x.(type) {
|
||||
case nil: // ...
|
||||
case int, uint: // ...
|
||||
case bool: // ...
|
||||
case string: // ...
|
||||
default: // ...
|
||||
case nil: // ...
|
||||
case int, uint: // ...
|
||||
case bool: // ...
|
||||
case string: // ...
|
||||
default: // ...
|
||||
}
|
||||
```
|
||||
|
||||
和(§1.8)中的普通switch语句一样,每一个case会被顺序的进行考虑,并且当一个匹配找到时,这个case中的内容会被执行。当一个或多个case类型是接口时,case的顺序就会变得很重要,因为可能会有两个case同时匹配的情况。default case相对其它case的位置是无所谓的。它不会允许落空发生。
|
||||
和(§1.8)中的普通switch语句一样,每一个case会被顺序的进行考虑,并且当一个匹配找到时,这个case中的内容会被执行。当一个或多个case类型是接口时,case的顺序就会变得很重要,因为可能会有两个case同时匹配的情况。default case相对其它case的位置是无所谓的。它不会允许落空发生。
|
||||
|
||||
注意到在原来的函数中,对于bool和string情况的逻辑需要通过类型断言访问提取的值。因为这个做法很典型,类型分支语句有一个扩展的形式,它可以将提取的值绑定到一个在每个case范围内都有效的新变量。
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
## 7.15. 一些建议
|
||||
|
||||
当设计一个新的包时,新手Go程序员总是先创建一套接口,然后再定义一些满足它们的具体类型。这种方式的结果就是有很多的接口,它们中的每一个仅只有一个实现。不要再这么做了。这种接口是不必要的抽象;它们也有一个运行时损耗。你可以使用导出机制(§6.6)来限制一个类型的方法或一个结构体的字段是否在包外可见。接口只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要。
|
||||
当设计一个新的包时,新手Go程序员总是先创建一套接口,然后再定义一些满足它们的具体类型。这种方式的结果就是有很多的接口,它们中的每一个仅只有一个实现。不要再这么做了。这种接口是不必要的抽象;它们也有一个运行时损耗。你可以使用导出机制(§6.6)来限制一个类型的方法或一个结构体的字段是否在包外可见。接口只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要。
|
||||
|
||||
当一个接口只被一个单一的具体类型实现时有一个例外,就是由于它的依赖,这个具体类型不能和这个接口存在在一个相同的包中。这种情况下,一个接口是解耦这两个包的一个好方式。
|
||||
|
||||
|
@@ -4,4 +4,4 @@
|
||||
|
||||
很多面向对象的语言都有相似的接口概念,但Go语言中接口类型的独特之处在于它是满足隐式实现的。也就是说,我们没有必要对于给定的具体类型定义所有满足的接口类型;简单地拥有一些必需的方法就足够了。这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不会去改变这些类型的定义;当我们使用的类型来自于不受我们控制的包时这种设计尤其有用。
|
||||
|
||||
在本章,我们会开始看到接口类型和值的一些基本技巧。顺着这种方式我们将学习几个来自标准库的重要接口。很多Go程序中都尽可能多的去使用标准库中的接口。最后,我们会在(§7.10)看到类型断言的知识,在(§7.13)看到类型开关的使用并且学到他们是怎样让不同的类型的概括成为可能。
|
||||
在本章,我们会开始看到接口类型和值的一些基本技巧。顺着这种方式我们将学习几个来自标准库的重要接口。很多Go程序中都尽可能多的去使用标准库中的接口。最后,我们会在(§7.10)看到类型断言的知识,在(§7.13)看到类型开关的使用并且学到他们是怎样让不同的类型的概括成为可能。
|
||||
|
Reference in New Issue
Block a user