本节的代码能在 goto_v2 目录中的 store.go 和 main.go 中找到]
当 goto 进程(运行在 8080 端口的 web 服务器)结束时,这个迟早并一定会发生, map 保存在内存中的 URLs 将会丢失。要保存我们 map 中的数据,我需要将它保存到一个磁盘文件中。我们将修改 URLStore ,用于将它的数据写入到一个文件,并且在 goto 启动的时候恢复这个数据。 为了实现它,我们将使用 Go 的 encoding/gob 包:这是一个序列化与反序列化包,它将数据结构转换成 bytes 数组(或者更准确的说是一个切片),反之依然(参见: 章节 12.11 )。
使用 gob 包的 NewEncoder
与 NewDecoder
函数,你来决定向它写入数据或者从它读取数据。由 Encoder 与 Decoder 所产生的对象提供了 Encode 和 Decode 的方法,用于向文件中写入和读取 Go 数据结构。顺便说一下: Encoder 也能实现 Writer 接口,Decoder 也同样可以实现 Reader 接口。我们将向 URLStore 添加一个新的 file
字段( *os.File
类型 ), 它将是一个可以用于写入和读取的打开文件的句柄 。
type URLStore struct {
urls map[string]string
mu sync.RWMutex
file *os.File
}
当我们实例化 URLStore 的时候,我们将调用这个文件 store.gob
,并将它的名称作为参数: var store = NewURLStore("store.gob")
现在我们必须调整我们的 NewURLStore 函数:
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 函数现在得到一个 filename 参数,打开文件( 参见 12 章 ),并且在我们的 URLStore 的变量 store 的 file 字段中保存 *os.File
的值,这里被称为 s
。
调用 OpenFile 可能会失败(例如,我们的磁盘文件可能被删除或重命名)
它能返回一个错误 err,注意 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 :
type record struct {
Key, URL string
}
以及一个新的 save 方法,它将给定的 key 和 url 作为一个 gob 编码的 record 写入到磁盘。
func (s *URLStore) save(key, url string) error {
e := gob.NewEncoder(s.file)
return e.Encode(record{key, url})
}
在 goto 启动的时候,我们磁盘上的数据存储必须读取到 URLStore 中,为此,我们有一个 load 方法
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 方法将从文件的开头寻找、读取并解码每一条记录,然后使用 Set 方法保存数据到 map 中。再次注意无处不在的错误处理。这个文件的解码是一个无限循环,只要没有错误就会一直继续下去:
for err == nil {
...
}
如果我们收到一个错误,它可能是因为我们刚好解码到最后一条记录,然后遇到一个 io.EOF (文件结束) 错误;如果不是这种情况,则是我们在解码时发生错误,要将 err 返回。这个方法必须添加到 NewURLStore :
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 函数中,当我们向我们的 map 添加一对新的 url 时,它也应该立即被保存到数据文件:
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("shouldn't get here")
}
编译并测试第二个版本,或者简单的使用已经存在的可执行文件(译者注:别纠结为什么没有这个,就自己编译吧),并在关闭了 web 服务器之后仍然可以知道所有的短 url (你可以通过在终端窗口执行 CTRL + C 停止这个进程)。
第一次启动 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
文件。
备注: 当第 2 次启动 goto 的时候,你可能会收到这个错误:
Error loading URLStore: extra data in buffer
这是因为 gob 是一个基于流的协议,不支持重启。 在第 4 版中,我们将使用 json 作为存储协议,来弥补这种情况。
版本 3—- 添加协程
第 3 版 goto_v3 的代码(在 章节 19.6 中讨论)能在 code_examples\ chapter_19\goto_v3 目录中找到。