第十九章修改 (#832)

Co-authored-by: Joe Chen <jc@unknwon.io>
This commit is contained in:
Haigang Zhou
2022-05-19 20:00:29 +08:00
committed by GitHub
parent fa1cfcc67f
commit 800466becd
9 changed files with 82 additions and 71 deletions

View File

@@ -1,12 +1,12 @@
# 19.1 简介
由于 web 无处不在,本章我们将开发一个完整的程序:`goto`,它是一个 web 缩短网址应用程序。示例来自 Andrew Gerrand 的讲座(见参考资料 22。我们将把项目分成 3 个阶段,每一个都会比之前阶段包含更多的功能,并逐渐展示更多 Go 语言中的特性。我们会大量使用在 [15章](15.0.md) 所学的网页应用程序的知识。
由于 web 无处不在,本章我们将开发一个完整的程序:`goto`,它是一个 web 缩短网址应用程序。示例来自 Andrew Gerrand 的讲座(见参考资料 22。我们将把项目分成 3 个阶段,每一个都会比之前阶段包含更多的功能,并逐渐展示更多 Go 语言中的特性。我们会大量使用在 [15 ](15.0.md)所学的网页应用程序的知识。
**版本 1** 利用映射和结构体,与 `sync` 包的 `Mutex` 一起使用,以及一个结构体工厂。
**版本 2** 数据以 `gob` 格式写入文件以实现持久化。
**版本 3** 利用协程和通道重写应用(见 [14章](14.0.md))。
**版本 3** 利用协程和通道重写应用(见 [14 ](14.0.md))。
**版本 4** 如果我们要使用 json 格式的文件该如何修改?

View File

@@ -1,8 +1,8 @@
# 19.2 短网址项目简介
你肯定知道有些浏览器中的地址(称为 URL非常长且/或复杂,在网上有一些将他们转换成简短 URL 来使用的服务。我们的项目与此类似:它是具有 2 个功能的 *web 服务*web service
你肯定知道有些浏览器中的地址(称为 URL非常长且/或复杂,在网上有一些将他们转换成简短 URL 来使用的服务。我们的项目与此类似:它是具有 2 个功能的 *web 服务* (web service)
## 添加Add
## 添加 (Add)
给定一个较长的 URL会将其转换成较短的版本例如
```
@@ -11,7 +11,7 @@ http://maps.google.com/maps?f=q&source=s_q&hl=en&geocode=&q=tokyo&sll=37.0625,-9
- (A) 转变为:`http://goto/UrcGq`
- (B) 并保存这对数据
## 重定向Redirect
## 重定向 (Redirect)
短网址被请求时,会把用户重定向到原始的长 URL。因此如果你在浏览器输入网址 (B),会被重定向到页面 (A)。

View File

@@ -6,9 +6,9 @@
(本节代码见 [goto_v1/store.go](examples/chapter_19/goto_v1/store.go)。)
当程序运行在生产环境时,会收到很多短网址的请求,同时会有一些将长 URL 转换成短 URL 的请求。我们的程序要以什么样的结构存储这些数据呢?[19.2 节](19.2.md) 中 (A) 和 (B) 两种 URL 都是字符串,此外,它们相互关联:给定键 (B) 能获取到值 (A),他们互相*映射*map。要将数据存储在内存中,我们需要这种结构,它们几乎存在于所有的编程语言中,只是名称有所不同,例如“哈希表”或“字典”等。
当程序运行在生产环境时,会收到很多短网址的请求,同时会有一些将长 URL 转换成短 URL 的请求。我们的程序要以什么样的结构存储这些数据呢?[19.2 节](19.2.md)中 (A) 和 (B) 两种 URL 都是字符串,此外,它们相互关联:给定键 (B) 能获取到值 (A),他们互相*映射* (map)。要将数据存储在内存中,我们需要这种结构,它们几乎存在于所有的编程语言中,只是名称有所不同,例如“哈希表”或“字典”等。
Go 语言就有这种内建的映射map`map[string]string`
Go 语言就有这种内建的映射 (map)`map[string]string`
键的类型写在 `[``]` 之间,紧接着是值的类型。有关映射的所有知识详见 [8 章](08.0.md)。为特定类型指定一个别名在严谨的程序中非常实用。Go 语言中通过关键字 `type` 来定义,因此有定义:
```go
@@ -16,7 +16,7 @@ type URLStore map[string]string
```
它从短 URL 映射到长 URL两者都是字符串。
要创建那种类型的变量,并命名为 m,使用:
要创建那种类型的变量,并命名为 `m`,使用:
```go
m := make(URLStore)
```
@@ -37,7 +37,7 @@ url := m["a"]
## 使程序线程安全
这里,变量 `URLStore` 是中心化的内存存储。当收到网络流量时,会有很多 `Redirect` 服务的请求。这些请求其实只涉及读操作:以给定的短 URL 作为键,返回对应的长 URL 的值。然而,对 `Add` 服务的请求则大不相同,它们会更改 `URLStore`,添加新的键值对。当在瞬间收到大量更新请求时,可能会产生如下问题:添加操作可能被另一个同类请求打断,写入的长 URL 值可能会丢失;另外,读取和更改同时进行,导致可能读到脏数据。代码中的 map 并不保证当开始更新数据时会彻底阻止另一个更新操作的启动。也就是说map 不是线程安全的goto 会并发地为很多请求提供服务。因此必须使 `URLStore` 是线程安全的,以便可以从不同的线程访问它。最简单和经典的方法是为其增加一个锁,它是 Go 标准库 `sync` 包中的 `Mutex` 类型,必须导入到我们的代码中(关于锁详见 [9.3 节](09.3.md))。
这里,变量 `URLStore` 是中心化的内存存储。当收到网络流量时,会有很多 `Redirect` 服务的请求。这些请求其实只涉及读操作:以给定的短 URL 作为键,返回对应的长 URL 的值。然而,对 `Add` 服务的请求则大不相同,它们会更改 `URLStore`,添加新的键值对。当在瞬间收到大量更新请求时,可能会产生如下问题:添加操作可能被另一个同类请求打断,写入的长 URL 值可能会丢失;另外,读取和更改同时进行,导致可能读到脏数据。代码中的 `map` 并不保证当开始更新数据时,会彻底阻止另一个更新操作的启动。也就是说,`map` 不是线程安全的goto 会并发地为很多请求提供服务。因此必须使 `URLStore` 是线程安全的,以便可以从不同的线程访问它。最简单和经典的方法是为其增加一个锁,它是 Go 标准库 `sync` 包中的 `Mutex` 类型,必须导入到我们的代码中(关于锁详见 [9.3 节](09.3.md))。
现在,我们把 `URLStore` 类型的定义更改为一个结构体(就是字段的集合,类似 C 或 Java [10 章](10.0.md) 介绍了结构体),它含有两个字段:`map``sync` 包的 `RWMutex`
```go
@@ -50,7 +50,7 @@ type URLStore struct {
`RWMutex` 有两种锁:分别对应读和写。多个客户端可以同时设置读锁,但只有一个客户端可以设置写锁(以排除所有的读锁),有效地串行化变更,使他们按顺序生效。
我们将在 `Get` 函数中实现 `Redirect` 服务的读请求,在 `Set` 函数中实现 `Add` 服务的写请求。`Get` 函数类似下面这样:
我们将在 `Get()` 函数中实现 `Redirect` 服务的读请求,在 `Set` 函数中实现 `Add` 服务的写请求。`Get()` 函数类似下面这样:
```go
func (s *URLStore) Get(key string) string {
s.mu.RLock()
@@ -60,9 +60,10 @@ func (s *URLStore) Get(key string) string {
}
```
函数按照键(短 URL返回对应映射后的 URL。它所处理的变量是指针类型见 [4.9 节](04.9.md)),指向 `URLStore`。但在读取值之前,先用 `s.mu.RLock()` 放置一个读锁,这样就不会有更新操作妨碍读取。数据读取后撤销锁定,以便挂起的更新操作可以开始。如果键不存在于 map 中会怎样?会返回字符串的零值(空字符串)。注意点号`.`类似面向对象的语言:在 `s``mu` 字段上调用方法 `RLock()`
函数按照键(短 URL返回对应映射后的 URL。它所处理的变量是指针类型见 [4.9 节](04.9.md)),指向 `URLStore`。但在读取值之前,先用 `s.mu.RLock()` 放置一个读锁,这样就不会有更新操作妨碍读取。数据读取后撤销锁定,以便挂起的更新操作可以开始。如果键不存在于 map 中会怎样?会返回字符串的零值(空字符串)。注意点号 (`.`) 类似面向对象的语言:在 `s``mu` 字段上调用方法 `RLock()`
`Set()` 函数同时需要 URL 的键值对,且必须放置写锁 `Lock()` 来排除同一时刻任何其他更新操作。函数返回布尔值 `true``false` 来表示 `Set()` 操作是否成功:
`Set` 函数同时需要 URL 的键值对,且必须放置写锁 `Lock()` 来排除同一时刻任何其他更新操作。函数返回布尔值 `true``false` 来表示 `Set` 操作是否成功:
```go
func (s *URLStore) Set(key, url string) bool {
s.mu.Lock()
@@ -77,13 +78,14 @@ func (s *URLStore) Set(key, url string) bool {
}
```
形式 `_, present := s.urls[key]` 可以测试 map 中是否已经包含该键,包含则 `present``true`,否则为 `false`。这种形式称为“逗号 ok 模式”,在 Go 代码中会频繁出现。如果键已存在,`Set` 函数直接返回布尔值 `false`map 不会被更新(这样可以保证短 URL 不会重复)。如果键不存在,把它加入 map 中并返回 `true`。左侧 `_` 是一个值的占位符,赋值给 `_` 来表明我们不会使用它。注意在更新后尽早调用 `Unlock()` 来释放对 `URLStore` 的锁定。
形式 `_, present := s.urls[key]` 可以测试 `map` 中是否已经包含该键,包含则 `present``true`,否则为 `false`。这种形式称为“逗号 ok 模式”,在 Go 代码中会频繁出现。如果键已存在,`Set()` 函数直接返回布尔值 `false``map` 不会被更新(这样可以保证短 URL 不会重复)。如果键不存在,把它加入 `map` 中并返回 `true`。左侧 `_` 是一个值的占位符,赋值给 `_` 来表明我们不会使用它。注意在更新后尽早调用 `Unlock()` 来释放对 `URLStore` 的锁定。
## 使用 defer 简化代码
目前代码还比较简单,容易记得操作完成后调用 `Unlock()` 解锁。然而在代码更复杂时很容易忘记解锁,或者放置在错误的位置,往往导致问题很难追踪。对于这种情况 Go 提供了一个特殊关键字 `defer`(见 [6.4 节](06.4.md))。在本例中,可以在 `Lock` 之后立即示意 `Unlock`,不过其效果是 `Unlock()` 只会在函数返回之前被调用。
目前代码还比较简单,容易记得操作完成后调用 `Unlock()` 解锁。然而在代码更复杂时很容易忘记解锁,或者放置在错误的位置,往往导致问题很难追踪。对于这种情况 Go 提供了一个特殊关键字 `defer`(见 [6.4 节](06.4.md))。在本例中,可以在 `Lock()` 之后立即示意 `Unlock()`,不过其效果是 `Unlock()` 只会在函数返回之前被调用。
`Get()` 可以简化成以下代码(我们消除了本地变量 `url`
`Get` 可以简化成以下代码(我们消除了本地变量 `url`
```go
func (s *URLStore) Get(key string) string {
s.mu.RLock()
@@ -92,7 +94,8 @@ func (s *URLStore) Get(key string) string {
}
```
`Set` 的逻辑在某种程度上也变得清晰了(我们不用再考虑解锁的事了):
`Set()` 的逻辑在某种程度上也变得清晰了(我们不用再考虑解锁的事了):
```go
func (s *URLStore) Set(key, url string) bool {
s.mu.Lock()
@@ -108,28 +111,29 @@ func (s *URLStore) Set(key, url string) bool {
## URLStore 工厂函数
`URLStore` 结构体中包含 map 类型的字段,使用前必须先用 `make` 初始化。在 Go 中创建一个结构体实例,一般是通过定义一个前缀为 `New`,能返回该类型已初始化实例的函数(通常是指向实例的指针)。
`URLStore()` 结构体中包含 `map` 类型的字段,使用前必须先用 `make()` 初始化。在 Go 中创建一个结构体实例,一般是通过定义一个前缀为 `New`,能返回该类型已初始化实例的函数(通常是指向实例的指针)。
```go
func NewURLStore() *URLStore {
return &URLStore{ urls: make(map[string]string) }
}
```
`return` 语句中,创建了 `URLStore` 字面量实例,其中包含初始化了的 map 映射。锁无需特别指明初始化,这是 Go 创建结构体实例的惯例。`&` 是取址运算符,它将我们要返回的内容变成指针,因为 `NewURLStore` 返回类型是 `*URLStore`。然后调用该函数来创建 `URLStore` 变量:
`return` 语句中,创建了 `URLStore` 字面量实例,其中包含初始化了的 `map` 映射。锁无需特别指明初始化,这是 Go 创建结构体实例的惯例。`&` 是取址运算符,它将我们要返回的内容变成指针,因为 `NewURLStore()` 返回类型是 `*URLStore`。然后调用该函数来创建 `URLStore` 变量:
```go
var store = NewURLStore()
```
## 使用 URLStore
要新增一对短/长 URL 到 map 中,我们只需调用 s 上的 `Set` 方法,由于返回布尔值,可以把它包裹在 `if` 语句中:
要新增一对短/长 URL 到 `map` 中,我们只需调用 `s` 上的 `Set()` 方法,由于返回布尔值,可以把它包裹在 `if` 语句中:
```go
if s.Set("a", "http://google.com") {
// 成功
}
```
要获取给定短 URL 对应的长 URL调用 s 上的 `Get` 方法,将返回值放入变量 `url`
要获取给定短 URL 对应的长 URL调用 `s` 上的 `Get()` 方法,将返回值放入变量 `url`
```go
if url := s.Get("a"); url != "" {
// 重定向到 url
@@ -138,7 +142,7 @@ if url := s.Get("a"); url != "" {
}
```
这里我们利用 Go 语言 `if` 语句的特性,可以在起始部分、条件判断前放置初始化语句。另外还需要一个 `Count` 方法以获取 map 中键值对的数量,可以使用内建的 `len` 函数:
这里我们利用 Go 语言 `if` 语句的特性,可以在起始部分、条件判断前放置初始化语句。另外还需要一个 `Count()` 方法以获取 `map` 中键值对的数量,可以使用内建的 `len()` 函数:
```go
func (s *URLStore) Count() int {
s.mu.RLock()
@@ -149,7 +153,7 @@ func (s *URLStore) Count() int {
如何根据给定的长 URL 计算出短 URL 呢?为此我们创建一个函数 `genKey(n int) string {…}`,将 `s.Count()` 的当前值作为其整型参数传入。(具体算法并不重要,示例代码可以在 [key.go](examples/chapter_19/goto_v1/key.go) 找到。)
现在,我们可以创建一个 `Put` 方法,接收一个长 URL`genKey` 生成其短 URL 键,调用 `Set` 方法在此键下存储长 URL 数据,然后返回这个键:
现在,我们可以创建一个 `Put()` 方法,接收一个长 URL`genKey()` 生成其短 URL 键,调用 `Set()` 方法在此键下存储长 URL 数据,然后返回这个键:
```go
func (s *URLStore) Put(url string) string {
for {
@@ -163,7 +167,7 @@ func (s *URLStore) Put(url string) string {
}
```
`for` 循环会一直尝试调用 `Set` 直到成功为止(意味着生成了一个尚未存在的短网址)。现在我们定义好了数据存储,以及配套的可工作的函数(见代码 [store.go](examples/chapter_19/goto_v1/store.go))。但这本身并不能完成任务,我们还需要开发 web 服务器以交付 `Add``Redirect` 服务。
`for` 循环会一直尝试调用 `Set()` 直到成功为止(意味着生成了一个尚未存在的短网址)。现在我们定义好了数据存储,以及配套的可工作的函数(见代码 [store.go](examples/chapter_19/goto_v1/store.go))。但这本身并不能完成任务,我们还需要开发 web 服务器以交付 `Add``Redirect` 服务。
## 链接

View File

@@ -34,7 +34,7 @@ func main() {
`Add` 函数必须做的事有:
1. 读取长 URL`r.FormValue("url")` 从 HTML 表单提交的 HTTP 请求中读取 URL
2. 使用 store 上的 `Put` 方法存储长 URL
2. 使用 store 上的 `Put()` 方法存储长 URL
3. 将对应的短 URL 发送给用户
每个需求都转化为一行代码:
@@ -46,9 +46,9 @@ func Add(w http.ResponseWriter, r *http.Request) {
}
```
这里 `fmt` 包的 `Fprintf` 函数用来替换字符串中的关键字 `%s`,然后将结果作为响应发送回客户端。注意 `Fprintf` 把数据写到了 `ResponseWriter` 中,其实 `Fprintf` 可以将数据写到任何实现了 `io.Writer` 的数据结构,即该结构实现了 `Write` 方法。Go 中 `io.Writer` 称为接口,可见 `Fprintf` 利用接口变得十分通用可以对很多不同的类型写入数据。Go 中接口的使用十分普遍,它使代码更通用(见 [11 章](11.0.md))。
这里 `fmt` 包的 `Fprintf()` 函数用来替换字符串中的关键字 `%s`,然后将结果作为响应发送回客户端。注意 `Fprintf()` 把数据写到了 `ResponseWriter` 中,其实 `Fprintf()` 可以将数据写到任何实现了 `io.Writer` 的数据结构,即该结构实现了 `Write()` 方法。Go 中 `io.Writer` 称为接口,可见 `Fprintf()` 利用接口变得十分通用可以对很多不同的类型写入数据。Go 中接口的使用十分普遍,它使代码更通用(见 [11 章](11.0.md))。
还需要一个表单,仍然可以用 `Fprintf` 来输出,这次将常量写入 `w`。让我们来修改 `Add`,当未指定 URL 时显示 HTML 表单:
还需要一个表单,仍然可以用 `Fprintf()` 来输出,这次将常量写入 `w`。让我们来修改 `Add()`,当未指定 URL 时显示 HTML 表单:
```go
func Add(w http.ResponseWriter, r *http.Request) {
url := r.FormValue("url")
@@ -68,9 +68,10 @@ URL: <input type="text" name="url">
`
```
在那种情况下,发送字符串常量 `AddForm` 到客户端,它是 html 表单,包含一个 `url` 输入域和一个提交按钮,点击后发送 POST 请求到 `/add`。这样 `Add` 处理函数被再次调用,此时 `url` 的值来自文本域。(` `` ` 用来创建原始字符串,否则按惯例 `""` 将成为字符串边界。)
在那种情况下,发送字符串常量 `AddForm` 到客户端,它是 html 表单,包含一个 `url` 输入域和一个提交按钮,点击后发送 POST 请求到 `/add`。这样 `Add()` 处理函数被再次调用,此时 `url` 的值来自文本域。(` `` ` 用来创建原始字符串,否则按惯例 `""` 将成为字符串边界。)
`Redirect()` 函数在 HTTP 请求路径中找到键(短 URL 的键是请求路径去除首字符,在 Go 中可以写为 `[1:]`。例如请求 "/abc",键就是 "abc"),用 `Get()` 函数从 `store` 检索到对应的长 URL对用户发送 HTTP 重定向。如果没找到 URL发送 404 "Not Found" 错误取而代之:
`Redirect` 函数在 HTTP 请求路径中找到键(短 URL 的键是请求路径去除首字符,在 Go 中可以写为 `[1:]`。例如请求 "/abc",键就是 "abc"),用 `Get` 函数从 `store` 检索到对应的长 URL对用户发送 HTTP 重定向。如果没找到 URL发送 404 "Not Found" 错误取而代之:
```go
func Redirect(w http.ResponseWriter, r *http.Request) {
key := r.URL.Path[1:]
@@ -83,7 +84,7 @@ func Redirect(w http.ResponseWriter, r *http.Request) {
}
```
`http.NotFound``http.Redirect` 是发送通用 HTTP 响应的工具函数。)
`http.NotFound()``http.Redirect()` 是发送通用 HTTP 响应的工具函数。)
我们已经完整地遍历了 [goto_v1](examples/chapter_19/goto_v1) 的代码。
@@ -103,7 +104,7 @@ func Redirect(w http.ResponseWriter, r *http.Request) {
打开浏览器并请求 url`http://localhost:8080/add`
这会激活 `Add` 处理函数。请求还未包含 url 变量,所以响应会输出 html 表单询问输入:
这会激活 `Add()` 处理函数。请求还未包含 url 变量,所以响应会输出 html 表单询问输入:
![](images/19.4_fig19.2.png?raw=true)
@@ -112,7 +113,7 @@ localhost:8080/2`。
![](images/19.4_fig19.3.jpg?raw=true)
复制该 URL 并在浏览器地址栏粘贴以发出请求,现在轮到 `Redirect` 处理函数上场了,对应长 URL 的页面被显示了出来。
复制该 URL 并在浏览器地址栏粘贴以发出请求,现在轮到 `Redirect()` 处理函数上场了,对应长 URL 的页面被显示了出来。
![](images/19.4_fig19.4.jpg?raw=true)

View File

@@ -6,9 +6,9 @@
(本节代码见 [goto_v2/store.go](examples/chapter_19/goto_v2/store.go) 和 [goto_v2/main.go](examples/chapter_19/goto_v2/main.go)。)
当 goto 进程(监听在 8080 端口的 web 服务器)终止,这迟早会发生,内存 map 中缩短的 URL 就会丢失。要保留这些数据,就得将其保存到磁盘文件中。我们将修改 `URLStore`,使它可以保存数据到文件,且在 goto 启动时还原这些数据。为此我们使用 Go 标准库的 `encoding/gob` 包:它用于序列化和反序列化,将数据结构转换为字节数组(确切地说是切片),反之亦然(见 [12.11 节](12.11.md))。
当 goto 进程(监听在 8080 端口的 web 服务器)终止,这迟早会发生,内存 `map` 中缩短的 URL 就会丢失。要保留这些数据,就得将其保存到磁盘文件中。我们将修改 `URLStore()`,使它可以保存数据到文件,且在 goto 启动时还原这些数据。为此我们使用 Go 标准库的 `encoding/gob` 包:它用于序列化和反序列化,将数据结构转换为字节数组(确切地说是切片),反之亦然(见 [12.11 节](12.11.md))。
通过 `gob` 包的 `NewEncoder``NewDecoder` 函数,可以指定数据要写入或读取的位置。返回的 `Encoder``Decoder` 对象提供了 `Encode``Decode` 方法,用于对文件写入和从中读取 Go 数据结构。提示:`Encoder` 实现了 `Writer` 接口,同样 `Decoder` 实现了 `Reader` 接口。我们在 `URLStore` 上增加一个新的 `file` 字段(`*os.File` 类型),它是用于读写已打开文件的句柄。
通过 `gob` 包的 `NewEncoder()``NewDecoder()` 函数,可以指定数据要写入或读取的位置。返回的 `Encoder``Decoder` 对象提供了 `Encode``Decode` 方法,用于对文件写入和从中读取 Go 数据结构。提示:`Encoder` 实现了 `Writer` 接口,同样 `Decoder` 实现了 `Reader` 接口。我们在 `URLStore` 上增加一个新的 `file` 字段(`*os.File` 类型),它是用于读写已打开文件的句柄。
```go
@@ -24,7 +24,7 @@ type URLStore struct {
var store = NewURLStore("store.gob")
```
接着,调整 `NewURLStore` 函数:
接着,调整 `NewURLStore()` 函数:
```go
func NewURLStore(filename string) *URLStore {
s := &URLStore{urls: make(map[string]string)}
@@ -37,9 +37,9 @@ func NewURLStore(filename string) *URLStore {
}
```
现在,更新后的 `NewURLStore` 函数接受一个文件名参数,它会打开该文件(见 [12 章](12.0.md)),将返回的 `*os.File` 作为 `file` 字段的值存储在 `URLStore` 变量 `store` 中,即这里的本地变量 `s`
现在,更新后的 `NewURLStore()` 函数接受一个文件名参数,它会打开该文件(见 [12 章](12.0.md)),将返回的 `*os.File` 作为 `file` 字段的值存储在 `URLStore` 变量 `store` 中,即这里的本地变量 `s`
`OpenFile` 的调用可能会失败(例如文件可能被删除或改名)。它会返回一个错误 err注意 Go 是如何处理这种情况的:
`OpenFile()` 的调用可能会失败(例如文件可能被删除或改名)。它会返回一个错误 `err`,注意 Go 是如何处理这种情况的:
```go
f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
@@ -47,7 +47,7 @@ if err != nil {
}
```
当 err 不为 `nil`,表示确实发生了错误,那么输出一条消息并停止程序执行。这是处理错误的一种方式,大多数情况下错误应该返回给调用函数,但这种检测错误的模式在 Go 代码中也很普遍。在 `}` 之后可以确定文件被成功打开了。
`err` 不为 `nil`,表示确实发生了错误,那么输出一条消息并停止程序执行。这是处理错误的一种方式,大多数情况下错误应该返回给调用函数,但这种检测错误的模式在 Go 代码中也很普遍。在 `}` 之后可以确定文件被成功打开了。
打开该文件时启用了写入标志,更精确地说是“追加模式”。每当一对新的短/长 URL 在程序中创建后,我们通过 `gob` 把它存储到文件 "store.gob" 中。
@@ -58,7 +58,7 @@ type record struct {
}
```
以及新的 `save` 方法,将给定的键和 URL 组成 `record` ,以 `gob` 编码的形式写入磁盘。
以及新的 `save()` 方法,将给定的键和 URL 组成 `record` ,以 `gob` 编码的形式写入磁盘。
```go
func (s *URLStore) save(key, url string) error {
e := gob.NewEncoder(s.file)
@@ -66,7 +66,7 @@ func (s *URLStore) save(key, url string) error {
}
```
goto 程序启动时,磁盘上存储的数据必须读取到 `URLStore` 的 map 中。为此,我们编写 `load` 方法:
goto 程序启动时,磁盘上存储的数据必须读取到 `URLStore``map` 中。为此,我们编写 `load` 方法:
```go
func (s *URLStore) load() error {
if _, err := s.file.Seek(0, 0); err != nil {
@@ -87,14 +87,14 @@ func (s *URLStore) load() error {
}
```
这个新的 `load` 方法会寻址`Seek`到文件的起始位置,读取并解码`Decode`每一条记录`record`,然后用 `Set` 方法将数据存储到 map 中。再次注意无处不在的错误处理模式。文件的解码由一个无限循环完成,只要没有错误就会一直继续:
这个新的 `load()` 方法会寻址 (`Seek`) 到文件的起始位置,读取并解码 (`Decode`) 每一条记录 (`record`),然后用 `Set` 方法将数据存储到 `map` 中。再次注意无处不在的错误处理模式。文件的解码由一个无限循环完成,只要没有错误就会一直继续:
```go
for err == nil {
}
```
如果得到了一个错误,可能是刚解码了最后一条记录,于是产生了 `io.EOF`EndOfFile 错误。若并非此种错误,表示产生了解码错误,用 `return err` 来返回它。对该方法的调用必须加入到 `NewURLStore` 中:
如果得到了一个错误,可能是刚解码了最后一条记录,于是产生了 `io.EOF` (EndOfFile) 错误。若并非此种错误,表示产生了解码错误,用 `return err` 来返回它。对该方法的调用必须加入到 `NewURLStore()` 中:
```go
func NewURLStore(filename string) *URLStore {
s := &URLStore{urls: make(map[string]string)}
@@ -110,7 +110,7 @@ func NewURLStore(filename string) *URLStore {
}
```
同时在 `Put` 方法中,当新的 URL 对加入到 map 中,也应该立即将它们保存到数据文件中:
同时在 `Put()` 方法中,当新的 URL 对加入到 `map` 中,也应该立即将它们保存到数据文件中:
```go
func (s *URLStore) Put(url string) string {
for {
@@ -126,7 +126,7 @@ func (s *URLStore) Put(url string) string {
}
```
编译并测试这第二个版本的程序,或直接使用现有的可执行程序,验证关闭服务器(在终端窗口可以按 CTRL/C并重启后短 URL 仍然有效。goto 程序第一次启动时,文件 store.gob 还不存在,因此当载入数据时会得到错误:
编译并测试这第二个版本的程序,或直接使用现有的可执行程序,验证关闭服务器(在终端窗口可以按 CTRL+C并重启后短 URL 仍然有效。goto 程序第一次启动时,文件 store.gob 还不存在,因此当载入数据时会得到错误:
2011/09/11 11:08:11 Error loading URLStore: open store.gob: The system cannot find the file specified.

View File

@@ -4,11 +4,11 @@
# 19.6 用协程优化性能
如果有太多客户端同时尝试添加 URL第 2 个版本依旧存在性能问题。得益于锁机制,我们的 map 可以在并发访问环境下安全地更新,但每条新产生的记录都要立即写入磁盘,这种机制成为了瓶颈。写入操作可能同时发生,根据不同操作系统的特性,可能会产生数据损坏。就算不产生写入冲突,每个客户端在 `Put` 函数返回前,必须等待数据写入磁盘。因此,在一个 I/O 负载很高的系统中,客户端为了完成 `Add` 请求,将等待更长的不必要的时间。
如果有太多客户端同时尝试添加 URL第 2 个版本依旧存在性能问题。得益于锁机制,我们的 `map` 可以在并发访问环境下安全地更新,但每条新产生的记录都要立即写入磁盘,这种机制成为了瓶颈。写入操作可能同时发生,根据不同操作系统的特性,可能会产生数据损坏。就算不产生写入冲突,每个客户端在 `Put()` 函数返回前,必须等待数据写入磁盘。因此,在一个 I/O 负载很高的系统中,客户端为了完成 `Add()` 请求,将等待更长的不必要的时间。
为缓解该问题,必须对 `Put` 和存储进程*解耦*:我们将使用 Go 的并发机制。我们不再将记录直接写入磁盘,而是发送到一个*通道*中,它是某种形式的缓冲区,因而发送函数不必等待它完成。
为缓解该问题,必须对 `Put()` 和存储进程*解耦*:我们将使用 Go 的并发机制。我们不再将记录直接写入磁盘,而是发送到一个*通道*中,它是某种形式的缓冲区,因而发送函数不必等待它完成。
保存进程会从该通道读取数据并写入磁盘。它是以 `saveLoop` 协程启动的独立线程。现在 `main``saveLoop` 并行地执行,不会再发生阻塞。
保存进程会从该通道读取数据并写入磁盘。它是以 `saveLoop()` 协程启动的独立线程。现在 `main()``saveLoop()` 并行地执行,不会再发生阻塞。
`URLStore``file` 字段替换为 `record` 类型的通道:`save chan record`
```go
@@ -19,7 +19,7 @@ type URLStore struct {
}
```
通道和 map 一样,必须用 `make` 创建。我们会以此修改 `NewURLStore` 工厂函数,并给定缓冲区大小为 1000例如`save := make(chan record, saveQueueLength)`。为解决性能问题,`Put` 可以发送记录 record 到带缓冲的 `save` 通道:
通道和 `map` 一样,必须用 `make()` 创建。我们会以此修改 `NewURLStore()` 工厂函数,并给定缓冲区大小为 1000例如`save := make(chan record, saveQueueLength)`。为解决性能问题,`Put` 可以发送记录 `record` 到带缓冲的 `save` 通道:
```go
func (s *URLStore) Put(url string) string {
for {
@@ -33,7 +33,8 @@ func (s *URLStore) Put(url string) string {
}
```
`save` 通道的另一端必须有一个接收者:新的 `saveLoop` 方法在独立的协程中运行,它接收 record 值并将它们写入到文件。`saveLoop` 是在 `NewURLStore()` 函数中用 `go` 关键字启动的。现在,可以移除不必要的打开文件的代码。以下是修改后的 `NewURLStore()`
`save` 通道的另一端必须有一个接收者:新的 `saveLoop()` 方法在独立的协程中运行,它接收 `record` 值并将它们写入到文件。`saveLoop()` 是在 `NewURLStore()` 函数中用 `go` 关键字启动的。现在,可以移除不必要的打开文件的代码。以下是修改后的 `NewURLStore()`
```go
const saveQueueLength = 1000
func NewURLStore(filename string) *URLStore {
@@ -49,7 +50,7 @@ func NewURLStore(filename string) *URLStore {
}
```
以下是 `saveLoop` 方法的代码:
以下是 `saveLoop()` 方法的代码:
```go
func (s *URLStore) saveLoop(filename string) {
f, err := os.Open(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
@@ -72,7 +73,7 @@ func (s *URLStore) saveLoop(filename string) {
我们在 [14 章](14.0.md) 深入学习了协程和通道,但在这里我们见到了实用的案例,更好地管理程序的不同部分。注意现在 `Encoder` 对象只被创建一次,而不是每次保存时都创建,这也可以节省了一些内存和运算处理。
还有一个改进可以使 goto 更灵活:我们可以将文件名、监听地址和主机名定义为标志flag,来代替在程序中硬编码或定义常量。这样当程序启动时,可以在命令行中指定它们的新值,如果没有指定,将采用 flag 的默认值。该功能来自另一个包,所以需要 `import "flag"`(这个包的更详细信息见 [12.4 节](12.4.md))。
还有一个改进可以使 goto 更灵活:我们可以将文件名、监听地址和主机名定义为标志 (flag),来代替在程序中硬编码或定义常量。这样当程序启动时,可以在命令行中指定它们的新值,如果没有指定,将采用 flag 的默认值。该功能来自另一个包,所以需要 `import "flag"`(这个包的更详细信息见 [12.4 节](12.4.md))。
先创建一些全局变量来保存 flag 的值:
```go
@@ -83,7 +84,7 @@ var (
)
```
为了处理命令行参数,必须把 `flag.Parse()` 添加到 `main` 函数中,在 flag 解析后才能实例化 `URLStore`,一旦得知了 `dataFile` 的值(在代码中使用了 `*dataFile`,因为 flag 是指针类型必须解除引用来获取值,见 [4.9 节](04.9.md)
为了处理命令行参数,必须把 `flag.Parse()` 添加到 `main()` 函数中,在 flag 解析后才能实例化 `URLStore`,一旦得知了 `dataFile` 的值(在代码中使用了 `*dataFile`,因为 flag 是指针类型必须解除引用来获取值,见 [4.9 节](04.9.md)
```go
var store *URLStore
func main() {
@@ -95,7 +96,7 @@ func main() {
}
```
现在 `Add` 处理函数中须用 `*hostname` 替换 `localhost:8080`
现在 `Add()` 处理函数中须用 `*hostname` 替换 `localhost:8080`
```go
fmt.Fprintf(w, "http://%s/%s", *hostname, key)
```

View File

@@ -8,14 +8,14 @@
Error loading URLStore: extra data in buffer
这是由于 gob 是基于流的协议,它不支持重新开始。为补救该问题,这里我们使用 json 作为存储协议(见 [12.9 节](12.9.md)),它以纯文本形式存储数据,因此也可以被非 Go 语言编写的进程读取。同时也显示了更换一种不同的持久化协议是多么简单,因为与存储打交道的代码被清晰地隔离在 2 个方法中,即 `load``saveLoop`
这是由于 gob 是基于流的协议,它不支持重新开始。为补救该问题,这里我们使用 json 作为存储协议(见 [12.9 节](12.9.md)),它以纯文本形式存储数据,因此也可以被非 Go 语言编写的进程读取。同时也显示了更换一种不同的持久化协议是多么简单,因为与存储打交道的代码被清晰地隔离在 2 个方法中,即 `load()``saveLoop()`
从创建新的空文件 store.json 开始,更改 main.go 中声明文件名变量的那一行:
```go
var dataFile = flag.String("file", "store.json", "data store file name")
```
在 store.go 中导入 `json` 取代 `gob`。然后在 `saveLoop` 中唯一需要被修改的行:
在 store.go 中导入 `json` 取代 `gob`。然后在 `saveLoop()` 中唯一需要被修改的行:
```go
e := gob.NewEncoder(f)
```

View File

@@ -1,14 +1,14 @@
# 版本 5 - 分布式程序
第 5 个版本的代码 *goto_v5*19.8 节和 19.9 节讨论)见 [goto_v5](examples/chapter_19/goto_v5)。该版本仍然基于 `gob` 存储,但很容易调整为使用 json正如版本 4 演示的那样。
第 5 个版本的代码 *goto_v5*[19.8 节](19.8.md)和 [19.9 节](19.9.md) 讨论)见 [goto_v5](examples/chapter_19/goto_v5)。该版本仍然基于 `gob` 存储,但很容易调整为使用 json正如版本 4 演示的那样。
# 19.8 多服务器处理架构
目前为止 goto 以单线程运行,但即使用协程,在一台机器上运行的单一进程,也只能为一定数量的并发请求提供服务。一个缩短网址服务,相对于 `Add`(用 `Put()` 写入),通常 `Redirect` 服务(用 `Get()` 读取)要多得多。因此我们应该可以创建任意数量的只读的从slave服务器,提供服务并缓存 `Get` 方法调用的结果,将 `Put` 请求转发给主master服务器,类似如下架构:
目前为止 goto 以单线程运行,但即使用协程,在一台机器上运行的单一进程,也只能为一定数量的并发请求提供服务。一个缩短网址服务,相对于 `Add()`(用 `Put()` 写入),通常 `Redirect()` 服务(用 `Get()` 读取)要多得多。因此我们应该可以创建任意数量的只读的从 (slave) 服务器,提供服务并缓存 `Get()` 方法调用的结果,将 `Put()` 请求转发给主 (master) 服务器,类似如下架构:
![图 19.5 跨越主从计算机的分布式负载](images/19.8_fig19.5.jpg?raw=true)
对于 slave 进程,要在网络上运行 goto 应用的一个 master 节点实例它们必须能相互通信。Go 的 `rpc` 包为跨越网络发起函数调用提供了便捷的途径。这里将把 `URLStore` 变为 RPC 服务([15.9 节](15.9.md) 详细讨论了 rpc 包。slave 进程将应对 `Get` 请求以交付长 URL。当一个长 URL 要被转换为缩短版本(使用 `Put` 方法)时,它们通过 rpc 连接把任务委托给 master 进程,因此只有 master 节点会写入数据文件。
对于 slave 进程,要在网络上运行 goto 应用的一个 master 节点实例它们必须能相互通信。Go 的 `rpc` 包为跨越网络发起函数调用提供了便捷的途径。这里将把 `URLStore` 变为 RPC 服务([15.9 节](15.9.md) 详细讨论了 `rpc`。slave 进程将应对 `Get()` 请求以交付长 URL。当一个长 URL 要被转换为缩短版本(使用 `Put()` 方法)时,它们通过 rpc 连接把任务委托给 master 进程,因此只有 master 节点会写入数据文件。
截至目前 `URLStore` 上基本的 `Get()``Put()` 方法具有如下签名:
```go
@@ -16,18 +16,19 @@ func (s *URLStore) Get(key string) string
func (s *URLStore) Put(url string) string
```
而 RPC 调用仅能使用如下形式的方法(t 是 T 类型的值):
而 RPC 调用仅能使用如下形式的方法(`t``T` 类型的值):
```go
func (t T) Name(args *ArgType, reply *ReplyType) error
```
要使 `URLStore` 成为 RPC 服务,需要修改 `Put``Get` 方法使它们符合上述函数签名。以下是修改后的签名:
要使 `URLStore` 成为 RPC 服务,需要修改 `Put()``Get()` 方法使它们符合上述函数签名。以下是修改后的签名:
```go
func (s *URLStore) Get(key, url *string) error
func (s *URLStore) Put(url, key *string) error
```
`Get()` 代码变更为:
```go
func (s *URLStore) Get(key, url *string) error {
s.mu.RLock()
@@ -76,7 +77,7 @@ func (s *URLStore) Set(key, url *string) error {
s.Set(&r.Key, &r.URL)
```
还必须修改 HTTP 处理函数以适应 `URLStore` 上的更改。`Redirect` 处理函数现在返回 `URLStore` 给出错误的字符串形式:
还必须修改 HTTP 处理函数以适应 `URLStore` 上的更改。`Redirect()` 处理函数现在返回 `URLStore` 给出错误的字符串形式:
```go
func Redirect(w http.ResponseWriter, r *http.Request) {
key := r.URL.Path[1:]
@@ -89,7 +90,8 @@ func Redirect(w http.ResponseWriter, r *http.Request) {
}
```
`Add` 处理函数也以基本相同的方式修改:
`Add()` 处理函数也以基本相同的方式修改:
```go
func Add(w http.ResponseWriter, r *http.Request) {
url := r.FormValue("url")
@@ -106,12 +108,12 @@ func Add(w http.ResponseWriter, r *http.Request) {
}
```
要使应用程序更灵活,正如之前章节所为,可以添加一个命令行标志flag来决定是否在 `main()` 函数中启用 RPC 服务器:
要使应用程序更灵活,正如之前章节所为,可以添加一个命令行标志 (flag) 来决定是否在 `main()` 函数中启用 RPC 服务器:
```go
var rpcEnabled = flag.Bool("rpc", false, "enable RPC server")
```
要使 RPC 工作,还要用 `rpc` 包来注册 `URLStore`,并用 `HandleHTTP` 创建基于 HTTP 的 RPC 处理器:
要使 RPC 工作,还要用 `rpc` 包来注册 `URLStore`,并用 `HandleHTTP()` 创建基于 HTTP 的 RPC 处理器:
```go
func main() {
flag.Parse()

View File

@@ -7,7 +7,7 @@ type ProxyStore struct {
}
```
一个 RPC 客户端必须使用 `DialHTTP()` 方法连接到服务器,所以我们把这句加入 `NewProxyStore` 函数,它用于创建 `ProxyStore` 对象。
一个 RPC 客户端必须使用 `DialHTTP()` 方法连接到服务器,所以我们把这句加入 `NewProxyStore()` 函数,它用于创建 `ProxyStore` 对象。
```go
func NewProxyStore(addr string) *ProxyStore {
client, err := rpc.DialHTTP("tcp", addr)
@@ -18,7 +18,8 @@ func NewProxyStore(addr string) *ProxyStore {
}
```
`ProxyStore``Get``Put` 方法,它们利用 RPC 客户端的 `Call` 方法,将请求直接传递给服务器:
`ProxyStore``Get()``Put()` 方法,它们利用 RPC 客户端的 `Call()` 方法,将请求直接传递给服务器:
```go
func (s *ProxyStore) Get(key, url *string) error {
return s.client.Call("Store.Get", key, url)
@@ -31,7 +32,7 @@ func (s *ProxyStore) Put(url, key *string) error {
## 带缓存的 ProxyStore
可是,如果 slave 进程只是简单地代理所有的工作到 master 节点,不会得到任何增益!我们打算用 slave 节点来应对 `Get` 请求。要做到这点,它们必须有 `URLStore` 中 map 的一份副本(缓存)。因此我们对 `ProxyStore` 的定义进行扩展,将 `URLStore` 包含在其中:
可是,如果 slave 进程只是简单地代理所有的工作到 master 节点,不会得到任何增益!我们打算用 slave 节点来应对 `Get()` 请求。要做到这点,它们必须有 `URLStore``map` 的一份副本(缓存)。因此我们对 `ProxyStore` 的定义进行扩展,将 `URLStore` 包含在其中:
```go
type ProxyStore struct {
urls *URLStore
@@ -39,7 +40,8 @@ type ProxyStore struct {
}
```
`NewProxyStore` 也必须做修改:
`NewProxyStore()` 也必须做修改:
```go
func NewProxyStore(addr string) *ProxyStore {
client, err := rpc.DialHTTP("tcp", addr)
@@ -50,7 +52,7 @@ func NewProxyStore(addr string) *ProxyStore {
}
```
还必须修改 `NewURLStore` 以便给出空文件名时,不会尝试从磁盘写入或读取文件:
还必须修改 `NewURLStore()` 以便给出空文件名时,不会尝试从磁盘写入或读取文件:
```go
func NewURLStore(filename string) *URLStore {
s := &URLStore{urls: make(map[string]string)}
@@ -65,7 +67,8 @@ func NewURLStore(filename string) *URLStore {
}
```
`ProxyStore``Get` 方法需要扩展:**它应该首先检查缓存中是否有对应的键**。如果有,`Get` 返回已缓存的结果。否则,应该发起 RPC 调用,然后用返回结果更新其本地缓存:
`ProxyStore``Get()` 方法需要扩展:**它应该首先检查缓存中是否有对应的键**。如果有,`Get()` 返回已缓存的结果。否则,应该发起 RPC 调用,然后用返回结果更新其本地缓存:
```go
func (s *ProxyStore) Get(key, url *string) error {
if err := s.urls.Get(key, url); err == nil { // url found in local map
@@ -80,7 +83,7 @@ func (s *ProxyStore) Get(key, url *string) error {
}
```
同样地,`Put` 方法仅当成功完成了远程 RPC `Put` 调用,才更新本地缓存:
同样地,`Put()` 方法仅当成功完成了远程 RPC `Put()` 调用,才更新本地缓存:
```go
func (s *ProxyStore) Put(url, key *string) error {
if err := s.client.Call("Store.Put", url, key); err != nil {
@@ -93,7 +96,7 @@ func (s *ProxyStore) Put(url, key *string) error {
## 汇总
slave 节点使用 `ProxyStore`,只有 master 使用 `URLStore`。有鉴于创造它们的方式,它们看上去十分一致:两者都实现了相同签名的 `Get``Put` 方法,因此我们可以指定一个 `Store` 接口来概括它们的行为:
slave 节点使用 `ProxyStore`,只有 master 使用 `URLStore`。有鉴于创造它们的方式,它们看上去十分一致:两者都实现了相同签名的 `Get()``Put()` 方法,因此我们可以指定一个 `Store` 接口来概括它们的行为:
```go
type Store interface {
Put(url, key *string) error