目前为止, goto 作为单个进程运行,即使使用协程,在一台机器上运行的单个进程也只能提供这么多并发请求。一个 URL 缩短服务通常更多的是重定向(使用 Get() 读取),而不是添加(使用 Put 写入)。因此,我们可以创建任意数量的只读从服务器用于缓存 Get 请求,并将 Puts 传递给主服务器,就像下面这个示例图:

多个从服务器进程要运行一个网络中另一台计算上的 goto 应用的主实例,它们必须能够互相通信。Go 的 rpc 包提供了一个通过网络连接进行函数调用的便利的方法,使 URLStore 成为一个 RPC 服务(我们已经在 章节 15.9 中详细的讨论过),这些从服务器进程将处理 Get 请求去提供长 urls 。当一个新的长 url 需要转换成一个短 url (使用 Put() 方法)的时候,它们通过 rpc 连接将任务委托给主服务器进程;所以必须只有主服务器可以写入数据。
到目前为止, URLStore 的 Get() 与 Put() 方法都有签名:
func (s *URLStore) Get(key string) stringfunc (s *URLStore) Put(url string) string
RPC 只能通过这种形式(t 是 T 类型的值)的方法工作:
func (t T) Name(args *ArgType, reply *ReplyType) error
为了使 URLStore 成为一个 RPC 服务,我们需要去修改 Put 与 Get 方法,以便它们匹配这个函数的签名。这是结果:
func (s *URLStore) Get(key, url *string) errorfunc (s *URLStore) Put(url, key *string) error
Get () 代码变成:
func (s *URLStore) Get(key, url *string) error {s.mu.RLock()defer s.mu.RUnlock()if u, ok := s.urls[*key]; ok {*url = ureturn nil}return errors.New("key not found")}
现在,因为 key 和 url 是指针,我们必须在它们前面添加一个 * 来获取它们的值,就像 *key ;u 是一个值,我们可以将它分配给指针,这样: *url = u
Put () 的代码也是一样:
func (s *URLStore) Put(url, key *string) error {for {*key = genKey(s.Count())if err := s.Set(key, url); err == nil {break}}if s.save != nil {s.save <- record{*key, *url}}return nil}
因为 Put() 调用 Set() ,后者也必须去适配 key 和 url 现在是指针的情况,并且它必须返回一个错误而不是布尔值:
func (s *URLStore) Set(key, url *string) error {s.mu.Lock()defer s.mu.Unlock()if _, present := s.urls[*key]; present {return errors.New("key already exists")}s.urls[*key] = *urlreturn nil}
因为同样的原因,当我们从 load() 调用 Set() 的时候,这个调用也必须被适配:
s.Set(&r.Key, &r.URL)
我们还必须得修改 HTTP 处理程序,用来适配 URLStore 的修改。Redirect 处理器现在返回的是由 URLStore 提供的错误字符串:
func Redirect(w http.ResponseWriter, r *http.Request) {key := r.URL.Path[1:]var url stringif err := store.Get(&key, &url); err != nil {http.Error(w, err.Error(), http.StatusInternalServerError)return}http.Redirect(w, r, url, http.StatusFound)}
Add 处理器的变化大致相同:
func Add(w http.ResponseWriter, r *http.Request) {url := r.FormValue("url")if url == "" {fmt.Fprint(w, AddForm)return}var key stringif err := store.Put(&url, &key); err != nil {http.Error(w, err.Error(), http.StatusInternalServerError)return}fmt.Fprintf(w, "http://%s/%s", *hostname, key)}
为了使我们的程序更灵活,就我们在上一章中所做的,我们可以添加一个命令行参数 flag ,用来在 main() 中启动 RPC 服务。
var rpcEnabled = flag.Bool("rpc", false, "enable RPC server")
为了使 rpc 工作,我们必须通过 rpc 包去注册 URLStore ,并通过 HandleHTTP 去设置 RPC-over-HTTP 处理器,就像这样:
func main() {flag.Parse()store = NewURLStore(*dataFile)if *rpcEnabled { // flag has been setrpc.RegisterName("Store", store)rpc.HandleHTTP()}... (像以前一样设置 http)}
