这个章节的代码,可以在 goto_v1\store.go 中找到]

    当我们的应用在生产环境中运行的时候,它将接收到很多短网址的请求,并且还会收到很多将一个长网址变短的请求。我们的程序会用哪些数据结构存储这些数据?如在 章节 19.2 中的 url 类型 (A)和(B)都是一个字符串,并且它们彼此相关: 我们可以将(B)作为键,通过它去获取(A)的值,它们相互映射。为了将数据存储在内存中,我们需要这样一种结构,几乎所有编程语言都存在的,用不同的名字作下标的 hash 表、字典等。

    Go 有一个这样的内置的 map :一个 map[string]string

    键的类型写在 [ ] 里面,后面跟的是值的类型;可以去 章节 8 学习所有和 map 有关的知识。 在任何一个不平凡的程序中,给特定的类型使用一个名称是很有用的。在 Go 中,我们使用关键字 type 可以做到,所以我们定义一个: type URLStore map[string]string

    它将短网址映射到长网址,并且都是字符串。

    要创建一个该类型命名的变量 m,只需使用:

    1. m := make(URLStore)

    假设我们想要存储 goto/agoogle.com/ 的映射到 m , 我们可以使用这样的语句: m["a"] = "http://google.com/"

    (在键始终保持不变的前提下,我们只需要存储 goto/ 的后缀来作为一个键)。要检索指定的 a 对应的长网址,我们写: url := m["a"]

    然后 url 的值等于 「http://google.com/」

    注意: 使用 := 我们不需要说明 urlstring 类型: 编译器会从右侧的值中推导出类型。

    使它线程安全:我们的 URLStore 变量是一个重要的内存数据存储,在这里: 一旦我们获得一些流量,将会有许多重定向类型的请求。这些实际上只是读的操作:使用短网址作为键读取,并且将长网址作为值返回。但是 Add 类型的请求是不同的,他们改变了我们的 URLStore,添加了新的键值对。当我们的服务一次性获得需要更新类型的请求时,可能会出现以下问题: 添加操作可能被另一个相同类型的请求打断,并且长网址的值可能不会被写入。也可能会将读取的内容一起修改,导致读取到一个错误的结果。我们的 map 不能保证在开始一个更新操作的时候,一个新的更新开始之前,它会完全终止。换句话说: 一个 map 不是线程安全的,它将会并发处理许多的请求,所以我们必须使我们的 URLStore 类型安全的从一个单独的线程去访问。最简单和经典的方法就是为它添加一个锁。在 Go 中,这是一个标准库中 sync 包中的 Mutex 类型,我们必须将它导入到我们的代码中(详细的锁定参见: 章节 9.3 )。

    我们将我们的 URLStore 的类型更改成一个 struct 类型(就这像在 C 或者 Java 中一个字段的集合,我们在 第 10 章 解释了 struct ), 它有两个字段: map 和 一个 sync 包中的 RWMutex

    1. import sync
    2. type URLStore struct {
    3. urls map[string]string // map 是从短网址到长网址
    4. mu sync.RWMutex
    5. }

    一个 RWMutex 有两个锁: 一个用于读取,一个用于写入。多个客户端可以同时获得读取锁,但是只能有一个客户端能够获得写入锁(禁用所有读取器),从而有效的序列化更新,使他们连续的工作。

    我们将在一个 Get 方法中实现重定向我们读类型的请求,并且我们添加一个 Set 方法来处理写的请求。Get 方法看起来就像这样:

    1. func (s *URLStore) Get(key string) string {
    2. s.mu.RLock()
    3. url := s.urls[key]
    4. s.mu.RUnlock()
    5. return url
    6. }

    它传入一个 key (短网址),并将相应的 map 中的值作为 url 返回。这个方法用在一个变量 s 上,它是一个指向我们的 URLStore 的指针(参见: 章节 4.9 )。但是在读取值之前,我们使用 s.mu.RLock() 设置了一个读锁,所以没有更新能够打断读取。读取之后我们解锁,所以等待的更新可以开始执行。如果在 map 中不存在 key 怎么办? 一个字符串类型的零值(空字符串)将被返回。注意面向对象语言中熟悉的 . 符号: 方法 RLock()smu 字段上被调用。

    Set 方法需要同时传入一个 key 和 一个 url ,并且必须使用一个写锁 Lock() 来阻止在同一时间的任何其他的更新。它返回一个布尔类型的 true 或者 false 值来表示 Set 是否成功。

    1. func (s *URLStore) Set(key, url string) bool {
    2. s.mu.Lock()
    3. _, present := s.urls[key]
    4. if present {
    5. s.mu.Unlock()
    6. return false
    7. }
    8. s.urls[key] = url
    9. s.mu.Unlock()
    10. return true
    11. }

    通过 _, present := s.urls[key] 这种形式,我们可以测试看看我们的 map 是否已经包含了这个 key,存在 present 变成 true,否则为 false。这个就是我们在 Go 代码中经常遇到的所谓的 逗号 ok 模式。如果 key 已经存在, Set 返回一个布尔型的值 false ,并且因为我们从方法返回, map 不会更新(所以我们不允许短网址被重用)。如果这个 key 不存在,我们添加它到 map,并且返回 true。左侧的 _ 是一个值的占位符,并且我们表明了不会使用它,因为我们将它分配给了 _ 。请注意,要尽可能快的(更新之后)解锁我们的 URLStore

    使用 defer 来简化代码:

    在这种情况下,因为代码简单,它可以很容易的记住去执行 Unlock 。然而,在非常复杂的代码中,就可能会被忘记了或者放在错误的地方,导致的问题难以追踪。对于这种情况,Go 有一个特殊的关键字 defer (参见: 章节 6.4 ),在这种情况下,它允许在执行完锁定后立即发出解锁信号,但其效果是 Unlock() 只会在函数返回之前才会被执行。

    Get 可以被简化为 (我们已经去除了局部变量 url):

    1. func (s *URLStore) Get(key string) string {
    2. s.mu.RLock()
    3. defer s.mu.RUnlock()
    4. return s.urls[key]
    5. }

    Set 的逻辑也变得非常的清晰(我们不需要再考虑解锁):

    1. func (s *URLStore) Set(key, url string) bool {
    2. s.mu.Lock()
    3. defer s.mu.Unlock()
    4. _, present := s.urls[key]
    5. if present {
    6. return false
    7. }
    8. s.urls[key] = url
    9. return true
    10. }

    URLStore 工厂函数:

    URLStore 结构体包含一个 map 字段,在使用它之前,必须要使用 make 初始化。在 Go 中通过定义个 New 前缀的函数来完成创建一个结构体的实例,该函数返回一个被初始化的类型的实例(在这里,大多数情况下,它是一个指向它的指针):

    1. func NewURLStore() *URLStore {
    2. return &URLStore{ urls: make(map[string]string) }
    3. }

    在返回中,我们制造了一个 URLStore 字面,并且初始化 map ,锁不需要特意初始化;这是在 Go 中制造结构体对象的标准方法。 & 是一个地址运算符,将我们返回的内容转换为指针,因为 NewURLStore 需要返回一个指针 *URLStore 。我们只是调用这个函数来制造一个 URLStore 变量 svar s = NewURLStore()

    使用我们的 URLStore:

    向我们的 map 添加一对新的 短 / 长 URL ,我们需要做的是调用 s 上的 Set 方法,并且由于这是一个布尔值,我们能立即包装它到一个 if 语句中:

    1. if s.Set("a", "http://google.com") {
    2. // success
    3. }

    要取回指定短网址 a 的长网址,我们将调用 s 上的 Get 方法,并将结果放到一个 url 变量中:

    1. if url := s.Get(“a”); url != “” {
    2. // redirect to url
    3. } else {
    4. // key not found
    5. }

    在 Go 中,我们可以使用它的一个特性, if 可以在条件之前,使用一个初始化语句。我们还需要一个 Count() 方法,来为我们计算 map 中的键值对的数量,这是由一个内置的 len 函数实现的。

    1. func (s *URLStore) Count() int {
    2. s.mu.RLock()
    3. defer s.mu.RUnlock()
    4. return len(s.urls)
    5. }

    我们如何计算指定的长网址的短网址?我们使用一个函数 genKey(n int) string {…} 并且它的整形参数我们赋予它 s.Count() 当前的值。

    [确切的算法并不重要,可以在 key.go 中找到示例代码]

    我们现在可以做一个 Put 方法,它需要一个长网址,使用 genKey 生成他的短网址 key ,使用 Set 方法,将长网址使用这个(短网址) key 作为下标储存,并返回它的 key:

    1. func (s *URLStore) Put(url string) string {
    2. for {
    3. key := genKey(s.Count())
    4. if s.Set(key, url) {
    5. return key
    6. }
    7. }
    8. // 不应该到这里
    9. return ""
    10. }

    for 循环一直重试 Set , 直到它成功(表示我们生成了一个尚未存在的短网址)。到现在为止,我们已经定义了我们的数据存储和处理它的函数(可以在 store.go 中找到代码)。但是这本身并没有做任何事,我们还必须去定义一个 web 服务器去提供添加与重定向服务。