本文主要内容
- 使用Go进行Web编程的方法
- 设计一个典型的Go Web应用
- 编写一个完整的Go Web应用
- 了解Go Web应用的各个组成部分
本文我们将会构建一个简单的网上论坛Web应用,这个应用同样非常基础,但是却有用得多:它允许用户登录到论坛里面,然后在论坛上发布新帖子,又或者回复其他用户发表的帖子。在阅读完这一文之后,你将进一步地了解到使用Go进行Web应用开发的相关方法。
ChitChat简介
网上论坛无处不在,它们是互联网上最受欢迎的应用之一,与旧式的电子公告栏(BBS)、新闻组(Usenet)和电子邮件一脉相承。雅虎公司和Google公司的群组(Groups)都非常流行,雅虎报告称,他们总共拥有1000万个群组以及1.15亿个群组成员,其中每个群组都拥有一个自己的论坛;而全球最具人气的网上论坛之一——Gaia在线——则拥有2300万注册用户以及接近230亿张帖子,并且这些帖子的数量还在以每天上百万张的速度持续增长。尽管现在出现了诸如Facebook这样的社交网站,但论坛仍然是人们在网上进行交流时最为常用的手段之一。作为例子,图1展示了GoogleGroups的样子。
图1 一个网上论坛示例:GoogleGroups里面的Go编程语言论坛
从本质上来说,网上论坛就相当于一个任何人都可以通过发帖来进行对话的公告板,公告板上面可以包含已注册用户以及未注册的匿名用户。论坛上的对话称为帖子(thread),一个帖子通常包含了作者想要讨论的一个主题,而其他用户则可以通过回复这个帖子来参与对话。比较复杂的论坛一般都会按层级进行划分,在这些论坛里面,可能会有多个讨论特定类型主题的子论坛存在。大多数论坛都会由一个或多个拥有特殊权限的用户进行管理,这些拥有特殊权限的用户被称为版主(moderator)。
在本文中,我们将会开发一个名为ChitChat的简易网上论坛。为了让这个例子保持简单,我们只会为ChitChat实现网上论坛的关键特性:在这个论坛里面,用户可以注册账号,并在登录之后发表新帖子又或者回复已有的帖子;未注册用户可以查看帖子,但是无法发表帖子或是回复帖子。现在,让我们首先来思考一下如何设计ChitChat这个应用。
应用设计
Web应用的一般工作流程是客户端向服务器发送请求,然后服务器对客户端进行响应(如图2所示),ChitChat应用的设计也遵循这一流程。
图2 Web应用的一般工作流程,客户端向服务器发送请求,然后等待接收响应
ChitChat的应用逻辑会被编码到服务器里面。服务器会向客户端提供HTML页面,并通过页面的超链接向客户端表明请求的格式以及被请求的数据,而客户端则会在发送请求时向服务器提供相应的数据,如图3所示。
图3 HTTP请求的URL格式
请求的格式通常是由应用自行决定的,比如,ChitChat的请求使用的是以下格式:
。http://<服务器名><处理器名>?<参数>
服务器名(server name)是ChitChat服务器的名字,而处理器名(handler name)则是被调用的处理器的名字。处理器的名字是按层级进行划分的:位于名字最开头是被调用模块的名字,而之后跟着的则是被调用子模块的名字,以此类推,位于处理器名字最末尾的则是子模块中负责处理请求的处理器。比如,对/thread/read
这个处理器名字来说,thread
是被调用的模块,而read
则是这个模块中负责读取帖子内容的处理器。
该应用的参数(parameter)会以URL查询的形式传递给处理器,而处理器则会根据这些参数对请求进行处理。比如说,假设客户端要向处理器传递帖子的唯一ID,那么它可以将URL的参数部分设置成id=123
,其中123
就是帖子的唯一ID。
如果chitchat
就是ChitChat服务器的名字,那么根据上面介绍的URL格式规则,客户端发送给ChitChat服务器的URL可能会是这样的:http://chitchat/thread/read?id=123。
当请求到达服务器时,多路复用器(multiplexer)会对请求进行检查,并将请求重定向至正确的处理器进行处理。处理器在接收到多路复用器转发的请求之后,会从请求中取出相应的信息,并根据这些信息对请求进行处理。在请求处理完毕之后,处理器会将所得的数据传递给模板引擎,而模板引擎则会根据这些数据生成将要返回给客户端的HTML,整个过程如图4所示。
图4 服务器在典型Web应用中的工作流程
数据模型
绝大多数应用都需要以某种方式与数据打交道。对ChitChat来说,它的数据将被存储到关系式数据库PostgreSQL里面,并通过SQL与之交互。
ChitChat的数据模型非常简单,只包含4种数据结构,它们分别是:
- User——表示论坛的用户信息;
- Session——表示论坛用户当前的登录会话;
- Thread——表示论坛里面的帖子,每一个帖子都记录了多个论坛用户之间的对话;
- Post——表示用户在帖子里面添加的回复。
以上这4种数据结构都会被映射到关系数据库里面,图5展示了这4种数据结构是如何与数据库交互的。
ChitChat论坛允许用户在登录之后发布新帖子或者回复已有的帖子,未登录的用户可以阅读帖子,但是不能发布新帖子或者回复帖子。为了对应用进行简化,ChitChat论坛没有设置版主这一职位,因此用户在发布新帖子或者添加新回复的时候不需要经过审核。
图5 Web应用访问数据存储系统的流程
在了解了ChitChat的设计方案之后,现在可以开始考虑具体的实现代码了。在开始学习ChitChat的实现代码之前,请注意,如果你在阅读本章展示的代码时遇到困难,又或者你是刚开始学习Go语言,那么为了更好地理解本章介绍的内容,你可以考虑先花些时间阅读一本Go语言的编程入门书,比如,由William Kennedy、Brian Ketelsen和Erik St. Martin撰写的《Go语言实战》就是一个很不错的选择。
除此之外,在阅读本章时也请尽量保持耐性:本章只是从宏观的角度展示Go Web应用的样子,并没有对Web应用的细节作过多的解释,而是将这些细节留到之后的章节再进一步说明。在有需要的情况下,本章也会在介绍某种技术的同时,说明在哪一章可以找到这一技术的更多相关信息。
请求的接收与处理
请求的接收和处理是所有Web应用的核心。正如之前所说,Web应用的工作流程如下。
(1)客户端将请求发送到服务器的一个URL上。
(2)服务器的多路复用器将接收到的请求重定向到正确的处理器,然后由该处理器对请求进行处理。
(3)处理器处理请求并执行必要的动作。
(4)处理器调用模板引擎,生成相应的HTML并将其返回给客户端。
让我们先从最基本的根URL(/
)来考虑Web应用是如何处理请求的:当我们在浏览器上输入地址http://localhost
的时候,浏览器访问的就是应用的根URL。在接下来的几个小节里面,我们将会看到ChitChat是如何处理发送至根URL的请求的,以及它又是如何通过动态地生成HTML来对请求进行响应的。
1 多路复用器
因为编译后的二进制Go应用总是以main
函数作为执行的起点,所以我们在对Go应用进行介绍的时候也总是从包含main
函数的主源码文件(main source code file)开始。ChitChat应用的主源码文件为main.go
,代码清单1展示了它的一个简化版本。
代码清单1 main.go
文件中的main
函数,函数中的代码经过了简化
package main
import (
"net/http"
)
func main() {
mux := http.NewServeMux()
files := http.FileServer(http.Dir("/public"))
mux.Handle("/static/", http.StripPrefix("/static/", files))
mux.HandleFunc("/", index)
server := &http.Server{
Addr: "0.0.0.0:8080",
Handler: mux,
}
server.ListenAndServe()
}
main.go``中``
首先创建了一个多路复用器,然后通过一些代码将接收到的请求重定向到处理器。net/http
标准库提供了一个默认的多路复用器,这个多路复用器可以通过调用NewServeMux
函数来创建:
mux := http.NewServeMux()
为了将发送至根URL的请求重定向到处理器,程序使用了HandleFunc
函数:
mux.HandleFunc("/", index)
HandleFunc
函数接受一个URL和一个处理器的名字作为参数,并将针对给定URL的请求转发至指定的处理器进行处理,因此对上述调用来说,当有针对根URL的请求到达时,该请求就会被重定向到名为index
的处理器函数。此外,因为所有处理器都接受一个ResponseWriter
和一个指向Request
结构的指针作为参数,并且所有请求参数都可以通过访问Request
结构得到,所以程序并不需要向处理器显式地传入任何请求参数。
需要注意的是,前面的介绍模糊了处理器以及处理器函数之间的区别:我们刚开始谈论的是处理器,而现在谈论的却是处理器函数。这是有意而为之的——尽管处理器和处理器函数提供的最终结果是一样的,但它们实际上并不相同。本书的第3章将对处理器和处理器函数之间的区别做进一步的说明,但是现在让我们暂时先忘掉这件事,继续研究ChitChat应用的代码实现。
2 服务静态文件
除负责将请求重定向到相应的处理器之外,多路复用器还需要为静态文件提供服务。为了做到这一点,程序使用FileServer
函数创建了一个能够为指定目录中的静态文件服务的处理器,并将这个处理器传递给了多路复用器的Handle
函数。除此之外,程序还使用StripPrefix
函数去移除请求URL中的指定前缀:
files := http.FileServer(http.Dir("/public"))
mux.Handle("/static/", http.StripPrefix("/static/", files))
当服务器接收到一个以/static/
开头的URL请求时,以上两行代码会移除URL中的/static/
字符串,然后在public
目录中查找被请求的文件。比如说,当服务器接收到一个针对文件http://localhost/static/css/bootstrap.min.css
的请求时,它将会在public
目录中查找以下文件:
<application root>/css/bootstrap.min.css
当服务器成功地找到这个文件之后,会把它返回给客户端。
3 创建处理器函数
正如之前的小节所说,ChitChat应用会通过HandleFunc
函数把请求重定向到处理器函数。正如代码清单2所示,处理器函数实际上就是一个接受ResponseWriter
和Request
指针作为参数的Go函数。
代码清单2 main.go
文件中的index
处理器函数
func index(w http.ResponseWriter, r *http.Request) {
files := []string{"templates/layout.html",
"templates/navbar.html",
"templates/index.html",}
templates := template.Must(template.ParseFiles(files...))
threads, err := data.Threads(); if err == nil {
templates.ExecuteTemplate(w, "layout", threads)
}
}
index
函数负责生成HTML并将其写入ResponseWriter
中。因为这个处理器函数会用到html/template
标准库中的Template
结构,所以包含这个函数的文件需 要在文件的开头导入html/template
库。之后的小节将对生成HTML的方法做进一步的介绍。
除了前面提到过的负责处理根URL请求的index
处理器函数,main.go
文件实际上还包含很多其他的处理器函数,如代码清单3所示。
代码清单3 ChitChat应用的main.go
源文件
package main
import (
"net/http"