用Go实现异步的Web开发

不知道大家还记得不记得大约一年前,我的一个白日梦《关于Web编程异步模型的白日梦》,然后这个白日梦我又连续做了好几天《Web编程异步模型的PHP原生实现》、《Web编程异步模型的 Gearman 实现(残)》。

当时怎么也没相通,还死皮白赖的粘在PHP的异步实现上不肯放手。好吧,实现是繁琐的,应用是成功的,代码是容易写的,环境是要搭建的……

昨晚睡觉前突然觉得自己应该真正用Go实现一下异步的Web,哪怕是个例子也好啊。于是,边吃饭,边敲了一票代码搞了一个很简单的demo,分享给大家吧。在这里下载完整的代码:webdemo

包含以下文件:

  • async.go——异步Web
  • data.go——模拟数据
  • Makefile——不说了,你懂……
  • page.go——页面
  • sync.go——同步Web
  • timer.go——记录执行时间的日志
  • webdemo.go——主文件

编译并运行

<br />
make run<br />

通过浏览器分别访问同步方式的http://127.0.0.1:8888/sync,和异步方式的http://127.0.0.1:8888/async。在控制台会输出请求处理的时长,实际上即便不看统计时长,两者之间的速度差异很直观的能体会出来。

<br />
[email protected]:~/Desktop/webdemo$ make run<br />
8g -o _go_.8 timer.go data.go page.go async.go sync.go<br />
rm -f _obj/webdemo.a<br />
gopack grc _obj/webdemo.a _go_.8<br />
cp _obj/webdemo.a &quot;/home/mikespook/bin/go/pkg/linux_386/webdemo.a&quot;<br />
8g webdemo.go<br />
8l -o webdemo webdemo.8<br />
./webdemo<br />
2011/03/25 15:06:35 async:	228623000<br />
2011/03/25 15:06:57 sync:	20024992000<br />

核心思路很简单,我在模拟数据请求的代码里用了 time.Sleep(),让每次数据请求都延迟 0.2 秒(一个优化得不太好的,很大的数据库表的一次很烂的查询所用时间):

<br />
// data.go<br />
func GetContents(key string) string {<br />
    time.Sleep(SLEEP) // 延迟<br />
    return fmt.Sprintf(&quot;%s. The quick brown fox jumps over the lazy dog.&quot;, key)<br />
}<br />

对于同步数据请求来说,必须等上一次的数据请求完毕才能发起下一次数据请求(原始的PHP即是如此),那么如果100个0.2秒的数据请求,则最终耗时一定大于 20 秒。
下面是请求100次数据的处理:

<br />
// webdemo.go 同步数据请求<br />
func syncHandler(w http.ResponseWriter, r *http.Request) {<br />
    timer := webdemo.NewTimer(&quot;sync&quot;)<br />
    defer timer.End()<br />
    page := webdemo.NewSyncPage()<br />
    for i := 0; i &lt; 100; i ++ {<br />
        key := strconv.Itoa(i)<br />
        page.SetContents(key)<br />
    }<br />
    page.Render(w)<br />
}<br />

下面是获取数据到页面,并渲染页面输出到http.ResonseWriter的方法:
<br />
// sync.go<br />
func (page *SyncPage) SetContents(key string) {<br />
    page.contents[key] = GetContents(key)<br />
}</p>
<p>func (page *SyncPage) Render(w http.ResponseWriter) {<br />
    lines := &quot;&quot;<br />
    for i := 0; i &lt; len(page.contents); i++ {<br />
        key := strconv.Itoa(i)<br />
        lines += fmt.Sprintf(TEMPLATE_LINE, page.contents[key])<br />
    }<br />
    block := fmt.Sprintf(TEMPLATE_BLOCK, lines)<br />
    fmt.Fprintf(w, TEMPLATE_PAGE, block)<br />
}<br />

对于异步来说,总时间长度略大于单条数据请求时间长度。下面是异步请求的 handler 代码,其实跟同步并无区别。

<br />
// webdemo.go 异步Handler<br />
func asyncHandler(w http.ResponseWriter, r *http.Request) {<br />
    timer := webdemo.NewTimer(&quot;async&quot;)<br />
    defer timer.End()<br />
    page := webdemo.NewAsyncPage()<br />
    page.CountOut = 100<br />
    for i:= 0; i &lt; page.CountOut; i++ {<br />
        key := strconv.Itoa(i)<br />
        page.SetContents(key)<br />
    }<br />
    page.Render(w)<br />
}<br />

为了实现异步的数据获取,在 async.go 的代码中,使用 go 语言强大的 channel 来实现。所以在 AsyncPage 结构中定义了一个 contents 是 chan。
<br />
// async.go<br />
// 页面内容<br />
type AsyncContents struct {<br />
    key, value string<br />
}<br />
// 页面<br />
type AsyncPage struct {<br />
    contents chan AsyncContents<br />
    timeout chan bool<br />
    CountOut int<br />
    page Page<br />
}<br />

在异步页面的 SetContents 方法中,用 go 关键字建立一个 goroutines 向 chan 中输入内容。同时建立另一个 goroutines 作为 timeout(本例中不会出现 timeout 的情况,不过实际环境中这是必要的)。
<br />
// async.go<br />
func (page *AsyncPage) SetContents(key string) {<br />
    // 异步的数据获取<br />
    go func() {<br />
        page.contents &lt;- AsyncContents{key, GetContents(key)}<br />
    }()<br />
    // 设置针对页面的超时<br />
    go func() {<br />
        time.Sleep(TIMEOUT)<br />
        page.timeout &lt;- true<br />
    }()<br />
}<br />

页面的渲染也跟同步方式不同,通过 select 将数据从chan 中取出,并渲染到模板。
<br />
// async.go<br />
func (page *AsyncPage) Render(w http.ResponseWriter) {</p>
<p>    lines := &quot;&quot;<br />
    LOOP: for i := 0; i &lt; page.CountOut; i++{<br />
        select {<br />
            case line := &lt;-page.contents:<br />
                lines = fmt.Sprintf(&quot;%s&quot; + TEMPLATE_LINE, lines, line.value)<br />
                // 每获取一个数据,就去掉一个超时<br />
                go func() {&lt;-page.timeout}()<br />
            case &lt;-page.timeout:<br />
                lines = fmt.Sprintf(&quot;%s&quot; + TEMPLATE_LINE, lines, &quot;Time Out&quot;)<br />
                break LOOP<br />
        }<br />
    }<br />
    block := fmt.Sprintf(TEMPLATE_BLOCK, lines)<br />
    fmt.Fprintf(w, TEMPLATE_PAGE, block)<br />
}<br />

为了演示异步的结构,我并没有使用模板来渲染页面。不过用模板来渲染也差不多:异步的获取模板上叫做%VARn的变量,然后将数据集合渲染至模板上……

当数据的获取有依赖关系的时候情况会比较复杂,不过如果把 web 页面当作一个树(DOM Tree?)的话,让异步数据从叶节点开始获取,逐层上推是个不错的办法。

6 thoughts on “用Go实现异步的Web开发”

Leave a Reply

Your email address will not be published. Required fields are marked *