<audio id="audio" title="16 | 秒杀大多数开发问题的两个利器：文档和测试案例" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/86/c9/868e7dd2ae529e45df6f8ebea9f081c9.mp3"></audio>

你好，我是温铭。在学习了 OpenResty 的原理和几个重要概念后，我们终于要开始 API 的学习了。

从我个人的经验来看，学习 OpenResty 的 API 是相对容易的，所以并没有占用本专栏太多的篇幅。你可以会疑惑：API 不是最常用、最重要的部分吗，为什么花的笔墨不多？

其实，这主要是出于两个方面的考虑。

第一，OpenResty 提供了非常详尽的文档。和很多其他的开发语言或者平台相比，OpenResty 除了会提供 API 的参数、返回值定义，还会提供完整的、可运行的代码示例，清楚地告诉你API 是如何处理各种边界条件的。

这种在 API 定义下面紧跟着示例代码和注意事项的做法，就是 OpenResty 文档的一贯风格。这样一来，在看完 API 描述后，你就可以立即在自己的环境下运行示例代码，并修改参数来和文档互相印证，加深记忆和理解。

第二，在文档之外，OpenResty还提供了高覆盖度的测试案例集。刚刚我提到过，OpenResty文档中提供了 API 的代码示例，但终究篇幅有限，多个 API 之间如何配合使用、各种异常情况下的报错和处理等，在文档中并没有呈现。

不过，不用担心，这些内容你大都可以在测试案例集里找到。

对于 OpenResty 的开发者来说，最好的 API 学习资料就是官方文档和测试案例，它们足够专业和友好。在这个前提下，如果我单纯地把文档翻译成中文再放在专栏中来讲，就没有太大意义了。

授人以鱼不如授之以渔，我更希望教给你的是通用的方法和经验。让我们用一个真实的例子来体验下，在 OpenResty 的开发中，如何让文档和测试案例集发挥更大的威力。

## shdict get API

shared dict（共享字典）是基于 NGINX 共享内存区的 Lua 字典对象，它可以跨多个 worker 来存取数据，一般用来存放限流、限速、缓存等数据。shared dict 相关的 API 有 20 多个，是 OpenResty 中最常用也是最重要的一组 API。

我们以最简单的 get 操作为例，你可以点开  [文档链接](https://github.com/openresty/lua-nginx-module/#ngxshareddictget)  做为对照。下面的最小化的代码示例，正是由官方文档改编而来：

```
  http {
      lua_shared_dict dogs 10m;
      server {
          location /demo {
              content_by_lua_block {
                  local dogs = ngx.shared.dogs
         dogs:set(&quot;Jim&quot;, 8)
         local v = dogs:get(&quot;Jim&quot;)
                  ngx.say(v)
              }
          }
      }
  }

```

简单说明一下，在Lua 代码中使用 shared dict 之前，我们需要在 nginx.conf 中用 `lua_shared_dict` 指令增加一块内存空间，它的名字是 dogs，大小为 10M。修改完 nginx.conf后，你还需要重启进程，用浏览器或者 curl 访问才能看到结果。

这步骤看起来是不是有些繁琐呢？让我们用一种更直接的方式改造一下。你可以看到，使用 resty CLI 的这种方式，和在 nginx.conf 中嵌入代码的效果是一致的。

```
$ resty --shdict 'dogs 10m' -e 'local dogs = ngx.shared.dogs
 dogs:set(&quot;Jim&quot;, 8)
 local v = dogs:get(&quot;Jim&quot;)
 ngx.say(v)
 '

```

你现在已经知道 nginx.conf 和 Lua 代码是如何配合的，也成功运行了 shared dict 的 set 和 get 方法。一般来说，大部分开发者也就此止步，不再深究了。

事实上，这里还是有几个值得注意的地方，比如：

1. 哪些阶段不能使用共享内存相关的 API 呢？
1. 我们在示例代码中看到 get 函数只有一个返回值，那什么情况下会有多个返回值呢？
1. get 函数的入参是什么类型？是否有长度限制？

不要小看这几个问题，窥一斑而见全豹，它们可以帮助我们更好的深入 OpenResty。接下来我就带你一一解读。

## 哪些阶段不能使用共享内存相关的 API ？

先来看第一个问题，答案很直接，文档中专门有一个 `context` （即上下文部分），里面列出了在什么环境下可以使用这个 API：

```
context: set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*

```

可以看出， `init` 和 `init_worker` 两个阶段不在其中，也就是说，共享内存的 get API 不能在这两个阶段使用。需要注意的是，每个共享内存的 API 可以使用的阶段并不完全相同，比如 set API 就可以在 `init` 阶段使用。

所以，千万不要想当然，还是那句话，使用时多翻阅文档。当然了，尽信书不如无书，OpenResty 的文档有时候也会出现错漏，这时候你就需要用实际的测试来验证了。

接下来，让我们修改下测试案例集，来确定下 `init` 阶段是否可以运行 shared dict 的 get API。

那该如何找到和共享内存相关的测试案例集呢？事实上，OpenResty 的测试案例都放在 `/t` 目录下，并且命名也是有规律的，即`自增数字-功能名.t`。搜索`shdict`，你可以找到 `043-shdict.t`，而这就是共享内存的测试案例集了，它里面有接近 100 个测试案例，包含各种正常和异常情况的测试。

我们来试着修改下第一个测试案例。

你可以把 content 阶段改为 init 阶段，并精简掉无关代码，看看 get 接口能否运行。这里我需要提醒一点，在现阶段，你不用非得搞明白测试案例是如何编写、组织和运行的，你只要知道它是在测试 get 接口就可以了：

```
=== TEST 1: string key, int value
 --- http_config
     lua_shared_dict dogs 1m;
 --- config
     location = /test {
         init_by_lua '
             local dogs = ngx.shared.dogs
             local val = dogs:get(&quot;foo&quot;)
             ngx.say(val)
         ';
     }
 --- request
 GET /test
 --- response_body
 32
 --- no_error_log
 [error]
 --- ONLY

```

你应该注意到了，在测试案例的最后，我加了 `--ONLY` 标记，这表示忽略其他所有测试案例，只运行这一个测试案例，以提高运行速度。后面在测试部分中，我会专门讲解各种各样的标记，你先记住这里就可以了。

修改完以后，我们用 prove 命令，就可以运行这个测试案例：

```
$ prove t/043-shdict.t

```

然后，你会得到一个报错，这也就印证了文档中描述的阶段限制。

```
nginx: [emerg] &quot;init_by_lua&quot; directive is not allowed here

```

## get 函数何时会有多个返回值？

我们再来看第二个问题，它可以从官方文档中总结出来。文档最开始就是这个接口的`syntax` 语法描述部分：

```
value, flags = ngx.shared.DICT:get(key)

```

正常情况下，

- 第一个参数`value` 返回的是字典中 key 对应的值；但当 key 不存在或者过期时，`value` 的值为 nil。
- 第二个参数 `flags` 就稍微复杂一些了，如果 set 接口设置了 flags，就返回，否则不返回。

一旦 API 调用出错，`value` 返回 nil，`flags` 返回具体的错误信息。

从文档总结的信息我们可以看出，`local v = dogs:get("Jim")` 这种只有一个接收参数的写法并不完善，因为它只覆盖了普通的使用场景，没有接收第二个参数，也没有做异常处理。我们可以把它修改为下面这样：

```
local data, err = dogs:get(&quot;Jim&quot;)
if data == nil and err then
    ngx.say(&quot;get not ok: &quot;, err)
    return
end

```

和第一个问题一样，我们可以到测试案例集里搜索一下，印证下我们对文档的理解：

```
=== TEST 65: get nil key
 --- http_config
     lua_shared_dict dogs 1m;
 --- config
     location = /test {
         content_by_lua '
             local dogs = ngx.shared.dogs
             local ok, err = dogs:get(nil)
             if not ok then
                 ngx.say(&quot;not ok: &quot;, err)
                 return
             end
             ngx.say(&quot;ok&quot;)
         ';
     }
 --- request
 GET /test
 --- response_body
 not ok: nil key
 --- no_error_log
 [error]

```

在这个测试案例中，get 接口的入参为 nil，返回的 err 信息是 `nil key`。这一方面验证了我们对文档的分析是正确的，另一方面，也为第三个问题提供了部分答案——起码，get 的入参不能是 nil。

## get 函数的入参是什么类型？

至于第三个问题， get 的入参可以是什么类型的呢？我们按照老规矩先查看文档，不过很可惜，你会发现，文档里并没有注明 key 的合法类型有哪些。这时该怎么办呢？

别着急，至少我们知道 key 可以是字符串类型，并且不能为 nil。不知道你还记得 Lua 中的数据类型吗？除了字符串和 nil，还有数字、数组、布尔类型和函数。后面两个显然没有作为 key 的必要性，我们只需要验证前两个。不妨先去测试文件中搜索一下，是否有数字作为 key 的案例：

```
=== TEST 4: number keys, string values

```

通过这个测试案例，你可以清楚看到，数字也可以作为 key ，内部会将数字转为字符串。那么数组呢？很遗憾，测试案例并没有覆盖到，我们需要自己动手试一下：

```
$ resty --shdict 'dogs 10m' -e 'local dogs = ngx.shared.dogs
 dogs:get({})
 '

```

不出意料，果然报错了：

```
ERROR: (command line -e):2: bad argument #1 to 'get' (string expected, got table)

```

综上，我们可以得出结论：get API 接受的 key 类型为字符串和数字。

那么入参 key 的长度是否有限制呢？这里其实也有一个对应的测试案例，我们一起来看一下：

```
=== TEST 67: get a too-long key
 --- http_config
     lua_shared_dict dogs 1m;
 --- config
     location = /test {
         content_by_lua '
             local dogs = ngx.shared.dogs
             local ok, err = dogs:get(string.rep(&quot;a&quot;, 65536))
             if not ok then
                 ngx.say(&quot;not ok: &quot;, err)
                 return
             end
             ngx.say(&quot;ok&quot;)
         ';
     }
 --- request
 GET /test
 --- response_body
 not ok: key too long
 --- no_error_log
 [error]

```

很显然，字符串长度为 65536 的时候，就会被提示 key 太长了。你可以试下把长度改为 65535，虽然只少了1个字节，却不会再报错了。这就说明，key 的最大长度正是 65535。

## 写在最后

OpenResty 现在的官方文档只有英文版本，国内工程师在阅读时，难免会因为语言问题，抓不住重点，甚至误解其中的内容。但越是这样，越没有捷径可走，你更应该仔细地把文档从头到尾读完，并在有疑问时，结合测试案例集和自己的尝试，去确定出答案。这才是辅助我们学习 OpenResty 的正确途径。

最后，我想提醒一下，在 OpenResty 的 API 中，凡是返回值中带有错误信息的，都必须有变量来接收并做错误处理，否则前方一定会有坑等你跳进去。比如把出错的连接放入了连接池，或者在 API 调用失败的情况下继续后面的逻辑，总之一定让人叫苦不迭。

那么，你在写 OpenResty 代码的时候，如果遇到问题，一般是通过什么方式来解决的？是文档、邮件列表、QQ 群，还是其他渠道呢？

欢迎留言一起探讨，也欢迎你把这篇文章分享给你的同事、朋友，我们一起交流，一起进步。


