如果有太多的客户端试图同时添加 URLs ,第 2 版仍然存在一个性能问题: 由于锁定机制,我们的 map 可以安全的更新并发访问,但是立即将每一个新记录写入到磁盘,是一个瓶颈。磁盘写入可能会同时发生,并且根据你的操作系统的特性,可能会导致崩溃。即使写入不会冲突,每个客户端在 Put 函数返回之前,必须等待他们的数据写入到磁盘。因此,在 I/O 负载大的系统上,客户端需要等待的时间将超过完成添加请求所必须的时间。

    为了解决这个问题,我们必须将 Put 与 Save 的过程解耦合:可以通过 Go 的并发机制做到这点。我们不再将记录直接保存到磁盘,而是发送它们到一个通道,这是一种缓冲,所以发送函数不需要再等待它。

    写入磁盘的保存过程从这个通道读取,并且是启动在一个叫 saveloop 的协程单独启动的线程上。主程序与 saveloop 是同时执行的,所以没有那么多的阻塞。

    我们通过一个 record 类型的通道替换 URLStore 中的 file 字段: save chan record

    1. type URLStore struct {
    2. urls map[string]string
    3. mu sync.RWMutex
    4. save chan record
    5. }

    一个 channel,就像一个 map 一样必须使用 make 创建;我们将修改我们的工厂 NewURLStore ,在它里面使用 make 去创建 channel ,并给他一个 1000 长度的缓冲区,如: save := make(chan record, saveQueueLength) 。为了弥补我们的性能瓶颈, Put 可以将一个 record 发送到我们的 channel 缓冲区保存,而不是进行函数调用保存每一条记录到磁盘。

    1. func (s *URLStore) Put(url string) string {
    2. for {
    3. key := genKey(s.Count())
    4. if s.Set(key, url) {
    5. s.save <- record{key, url}
    6. return key
    7. }
    8. }
    9. panic("shouldn't get here")
    10. }

    save channel 的另一端,我们必须有一个接收器: 我们的新方法 saveLoop 将运行在一个单独的 goroutine 中; 它接收 record 的值并将他们写入到一个文件。 saveLoop 也是在 NewURLStore () 函数中通过 go 关键字启动,我们现在可以删除不再需要的文件打开的代码。这里修改后的 NewURLStore () :

    1. const saveQueueLength = 1000
    2. func NewURLStore(filename string) *URLStore {
    3. s := &URLStore{
    4. urls: make(map[string]string),
    5. save: make(chan record, saveQueueLength),
    6. }
    7. if err := s.load(filename); err != nil {
    8. log.Println("Error loading URLStore:", err)
    9. }
    10. go s.saveLoop(filename)
    11. return s
    12. }

    这里是 saveLoop 方法的代码:

    1. func (s *URLStore) saveLoop(filename string) {
    2. f, err := os.Open(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
    3. if err != nil {
    4. log.Fatal("URLStore:", err)
    5. }
    6. defer f.Close()
    7. e := gob.NewEncoder(f)
    8. for {
    9. // taking a record from the channel and encoding it
    10. r := <-s.save
    11. if err := e.Encode(r); err != nil {
    12. log.Println("URLStore:", err)
    13. }
    14. }
    15. }

    Records 从一个无限循环中的 save channel 读取并编码到文件。

    14 章 我们深入的学习了协程与通道,但是在这里我们看到了一个有用的示例,它可以更好的管理一个程序的不同部分。还要注意,现在我们仅创建了一次 Encoder 对象, 而不是每次保存,这样也节省了一些内存和处理。

    另外一个改善可以使 goto 变得更加灵活:替换代码中的 filename 、硬编码的或者作为程序中常量的监听地址和主机名,我们可以将它们定义为 flags 。

    这样,当启动程序的时候,如果在命令行中输入这些值,它们将被替换成新的值,如果没有输入,将从 flag 中获取默认值。这个功能来自一个不同的包,所以我们必须: import “flag” (关于此包的详细信息,参见 章节 12.4 )。

    我们首先创建一些全局变量去保存 flag 的值:

    1. var (
    2. listenAddr = flag.String("http", ":8080", "http listen address")
    3. dataFile = flag.String("file", "store.gob", "data store file name")
    4. hostname = flag.String("host", "localhost:8080", "host name and port")
    5. )

    为了处理命令行参数,我们必须添加 flag.Parse() 到 main 函数中,并且在 flags 被解析后实例化 URLStore ,因为参数解析后我们才能知道 dataFile 的值( 在代码中使用的是 *dataFile ,这时因为 flag 是一个指针,必须取消引用去获取值,参见 章节 4.9 ):

    1. var store *URLStore
    2. func main() {
    3. flag.Parse()
    4. store = NewURLStore(*dataFile)
    5. http.HandleFunc("/", Redirect)
    6. http.HandleFunc("/add", Add)
    7. http.ListenAndServe(*listenAddr, nil)
    8. }

    在 Add 处理器中,我们现在必须将 localhost:8080 替换成 *hostname

    1. fmt.Fprintf(w, "http://%s/%s", *hostname, key)

    编译并测试第 3 版本或使用目录中的可执行文件(译者注:一样不用去纠结为什么没有这个文件,直接编译去执行就好了)。

    版本 4 —— 使用 json 做持久存储

    第 4 版 goto_4 的代码(在 章节 19.7 中讨论 )能在 code_examples\chapter_19\goto_v4. 中找到。