Files
the-way-to-go_ZH_CN/eBook/19.5.md
2019-08-04 00:23:02 -07:00

147 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 版本 2 - 添加持久化存储
第 2 个版本的代码 *goto_v2* 见 [goto_v2](examples/chapter_19/goto_v2)。
# 19.5 持久化存储gob
(本节代码见 [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))。
通过 `gob` 包的 `NewEncoder``NewDecoder` 函数,可以指定数据要写入或读取的位置。返回的 `Encoder``Decoder` 对象提供了 `Encode``Decode` 方法,用于对文件写入和从中读取 Go 数据结构。提示:`Encoder` 实现了 `Writer` 接口,同样 `Decoder` 实现了 `Reader` 接口。我们在 `URLStore` 上增加一个新的 `file` 字段(`*os.File` 类型),它是用于读写已打开文件的句柄。
```go
type URLStore struct {
urls map[string]string
mu sync.RWMutex
file *os.File
}
```
我们把这个文件命名为 store.gob当初始化 `URLStore` 时将其作为参数传入:
```go
var store = NewURLStore("store.gob")
```
接着,调整 `NewURLStore` 函数:
```go
func NewURLStore(filename string) *URLStore {
s := &URLStore{urls: make(map[string]string)}
f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Fatal("URLStore:", err)
}
s.file = f
return s
}
```
现在,更新后的 `NewURLStore` 函数接受一个文件名参数,它会打开该文件(见 [12 章](12.0.md)),将返回的 `*os.File` 作为 `file` 字段的值存储在 `URLStore` 变量 `store` 中,即这里的本地变量 `s`
`OpenFile` 的调用可能会失败(例如文件可能被删除或改名)。它会返回一个错误 err注意 Go 是如何处理这种情况的:
```go
f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Fatal("URLStore:", err)
}
```
当 err 不为 `nil`,表示确实发生了错误,那么输出一条消息并停止程序执行。这是处理错误的一种方式,大多数情况下错误应该返回给调用函数,但这种检测错误的模式在 Go 代码中也很普遍。在 `}` 之后可以确定文件被成功打开了。
打开该文件时启用了写入标志,更精确地说是“追加模式”。每当一对新的短/长 URL 在程序中创建后,我们通过 `gob` 把它存储到文件 "store.gob" 中。
为达到目的,定义一个新的结构体类型 `record`
```go
type record struct {
Key, URL string
}
```
以及新的 `save` 方法,将给定的键和 URL 组成 `record` ,以 `gob` 编码的形式写入磁盘。
```go
func (s *URLStore) save(key, url string) error {
e := gob.NewEncoder(s.file)
return e.Encode(record{key, url})
}
```
goto 程序启动时,磁盘上存储的数据必须读取到 `URLStore` 的 map 中。为此,我们编写 `load` 方法:
```go
func (s *URLStore) load() error {
if _, err := s.file.Seek(0, 0); err != nil {
return err
}
d := gob.NewDecoder(s.file)
var err error
for err == nil {
var r record
if err = d.Decode(&r); err == nil {
s.Set(r.Key, r.URL)
}
}
if err == io.EOF {
return nil
}
return err
}
```
这个新的 `load` 方法会寻址(`Seek`)到文件的起始位置,读取并解码(`Decode`)每一条记录(`record`),然后用 `Set` 方法将数据存储到 map 中。再次注意无处不在的错误处理模式。文件的解码由一个无限循环完成,只要没有错误就会一直继续:
```go
for err == nil {
}
```
如果得到了一个错误,可能是刚解码了最后一条记录,于是产生了 `io.EOF`EndOfFile 错误。若并非此种错误,表示产生了解码错误,用 `return err` 来返回它。对该方法的调用必须加入到 `NewURLStore` 中:
```go
func NewURLStore(filename string) *URLStore {
s := &URLStore{urls: make(map[string]string)}
f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Fatal("Error opening URLStore:", err)
}
s.file = f
if err := s.load(); err != nil {
log.Println("Error loading data in URLStore:", err)
}
return s
}
```
同时在 `Put` 方法中,当新的 URL 对加入到 map 中,也应该立即将它们保存到数据文件中:
```go
func (s *URLStore) Put(url string) string {
for {
key := genKey(s.Count())
if s.Set(key, url) {
if err := s.save(key, url); err != nil {
log.Println("Error saving to URLStore:", err)
}
return key
}
}
panic("shouldnt get here")
}
```
编译并测试这第二个版本的程序,或直接使用现有的可执行程序,验证关闭服务器(在终端窗口可以按 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.
结束进程并重启后,就能正常工作了。或者,可以在 goto 启动前先创建空的 store.gob 文件。
**备注:** 当第二次启动 goto 时,可能会产生错误:
Error loading URLStore: extra data in buffer
这是由于 `gob` 是基于流的协议,它不支持重新开始。在版本 4 中,会用 json 作为存储协议来补救此问题。
## 链接
- [目录](directory.md)
- 上一节:[用户界面web 服务端](19.4.md)
- 下一节:[用协程优化性能](19.6.md)