本节的代码能在 goto_v2 目录中的 store.go 和 main.go 中找到]

    当 goto 进程(运行在 8080 端口的 web 服务器)结束时,这个迟早并一定会发生, map 保存在内存中的 URLs 将会丢失。要保存我们 map 中的数据,我需要将它保存到一个磁盘文件中。我们将修改 URLStore ,用于将它的数据写入到一个文件,并且在 goto 启动的时候恢复这个数据。 为了实现它,我们将使用 Go 的 encoding/gob 包:这是一个序列化与反序列化包,它将数据结构转换成 bytes 数组(或者更准确的说是一个切片),反之依然(参见: 章节 12.11 )。

    使用 gob 包的 NewEncoderNewDecoder 函数,你来决定向它写入数据或者从它读取数据。由 Encoder 与 Decoder 所产生的对象提供了 Encode 和 Decode 的方法,用于向文件中写入和读取 Go 数据结构。顺便说一下: Encoder 也能实现 Writer 接口,Decoder 也同样可以实现 Reader 接口。我们将向 URLStore 添加一个新的 file 字段( *os.File 类型 ), 它将是一个可以用于写入和读取的打开文件的句柄 。

    1. type URLStore struct {
    2. urls map[string]string
    3. mu sync.RWMutex
    4. file *os.File
    5. }

    当我们实例化 URLStore 的时候,我们将调用这个文件 store.gob ,并将它的名称作为参数: var store = NewURLStore("store.gob")

    现在我们必须调整我们的 NewURLStore 函数:

    1. func NewURLStore(filename string) *URLStore {
    2. s := &URLStore{urls: make(map[string]string)}
    3. f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
    4. if err != nil {
    5. log.Fatal("URLStore:", err)
    6. }
    7. s.file = f
    8. return s
    9. }

    NewURLStore 函数现在得到一个 filename 参数,打开文件( 参见 12 章 ),并且在我们的 URLStore 的变量 store 的 file 字段中保存 *os.File 的值,这里被称为 s

    调用 OpenFile 可能会失败(例如,我们的磁盘文件可能被删除或重命名)

    它能返回一个错误 err,注意 Go 是如何处理的:

    1. f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
    2. if err != nil {
    3. log.Fatal("URLStore:", err)
    4. }

    当 err 不是 nil ,这意味着真的存在一个错误,我们停止程序,记录一条消息。这是处理的一种方式,大多数情况下,错误会返回给调用函数,但是这种测试错误的方式在 Go 中无处不在。在 } 之后,我们确信文件已经打开了。

    我们用可写的方式打开文件,更确切的说是在追加模式下。每次在我们的程序中创建一对新的 (短、长) URL ,我们将通过 gob 将它保存在 store.gob 文件中。

    为此,我们定义一个新的结构体类型 record :

    1. type record struct {
    2. Key, URL string
    3. }

    以及一个新的 save 方法,它将给定的 key 和 url 作为一个 gob 编码的 record 写入到磁盘。

    1. func (s *URLStore) save(key, url string) error {
    2. e := gob.NewEncoder(s.file)
    3. return e.Encode(record{key, url})
    4. }

    在 goto 启动的时候,我们磁盘上的数据存储必须读取到 URLStore 中,为此,我们有一个 load 方法

    1. func (s *URLStore) load() error {
    2. if _, err := s.file.Seek(0, 0); err != nil {
    3. return err
    4. }
    5. d := gob.NewDecoder(s.file)
    6. var err error
    7. for err == nil {
    8. var r record
    9. if err = d.Decode(&r); err == nil {
    10. s.Set(r.Key, r.URL)
    11. }
    12. }
    13. if err == io.EOF {
    14. return nil
    15. }
    16. return err
    17. }

    新的 load 方法将从文件的开头寻找、读取并解码每一条记录,然后使用 Set 方法保存数据到 map 中。再次注意无处不在的错误处理。这个文件的解码是一个无限循环,只要没有错误就会一直继续下去:

    1. for err == nil {
    2. ...
    3. }

    如果我们收到一个错误,它可能是因为我们刚好解码到最后一条记录,然后遇到一个 io.EOF (文件结束) 错误;如果不是这种情况,则是我们在解码时发生错误,要将 err 返回。这个方法必须添加到 NewURLStore :

    1. func NewURLStore(filename string) *URLStore {
    2. s := &URLStore{urls: make(map[string]string)}
    3. f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
    4. if err != nil {
    5. log.Fatal("Error opening URLStore:", err)
    6. }
    7. s.file = f
    8. if err := s.load(); err != nil {
    9. log.Println("Error loading data in URLStore:", err)
    10. }
    11. return s
    12. }

    同样的,在 Put 函数中,当我们向我们的 map 添加一对新的 url 时,它也应该立即被保存到数据文件:

    1. func (s *URLStore) Put(url string) string {
    2. for {
    3. key := genKey(s.Count())
    4. if s.Set(key, url) {
    5. if err := s.save(key, url); err != nil {
    6. log.Println("Error saving to URLStore:", err)
    7. }
    8. return key
    9. }
    10. }
    11. panic("shouldn't get here")
    12. }

    编译并测试第二个版本,或者简单的使用已经存在的可执行文件(译者注:别纠结为什么没有这个,就自己编译吧),并在关闭了 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 的时候,你可能会收到这个错误:

    1. Error loading URLStore: extra data in buffer

    这是因为 gob 是一个基于流的协议,不支持重启。 在第 4 版中,我们将使用 json 作为存储协议,来弥补这种情况。

    版本 3—- 添加协程

    第 3 版 goto_v3 的代码(在 章节 19.6 中讨论)能在 code_examples\ chapter_19\goto_v3 目录中找到。