本文摘自《Go web编程》 css
京东购书:https://item.jd.com/12252845.htmlhtml
{:--}本文主要内容jquery
上一章在末尾展现了一个很是简单的Go Web应用,可是由于该应用只是一个Hello World程序,因此它实际上并无什么用处。在本章中,咱们将会构建一个简单的网上论坛Web应用,这个应用一样很是基础,可是却有用得多:它容许用户登陆到论坛里面,而后在论坛上发布新帖子,又或者回复其余用户发表的帖子。git
虽然本章介绍的内容没法让你一会儿就学会如何编写一个很是成熟的Web应用,但这些内容将教会你如何组织和开发一个Web应用。在阅读完这一章以后,你将进一步地了解到使用Go进行Web应用开发的相关方法。github
若是你以为本章介绍的内容难度较大,又或者你以为本章展现的大量代码看起来让人以为胆战心惊,那也没必要过于担忧:本章以后的几章将对本章介绍的内容作进一步的解释,在阅读完本章并继续阅读后续章节时,你将会对本章介绍的内容有更加深刻的了解。web
网上论坛无处不在,它们是互联网上最受欢迎的应用之一,与旧式的电子公告栏(BBS)、新闻组(Usenet)和电子邮件一脉相承。雅虎公司和Google公司的群组(Groups)都很是流行,雅虎报告称,他们总共拥有1000万个群组以及1.15亿个群组成员,其中每一个群组都拥有一个本身的论坛;而全球最具人气的网上论坛之一——Gaia在线——则拥有2300万注册用户以及接近230亿张帖子,而且这些帖子的数量还在以天天上百万张的速度持续增加。尽管如今出现了诸如Facebook这样的社交网站,但论坛仍然是人们在网上进行交流时最为经常使用的手段之一。做为例子,图2-1展现了GoogleGroups的样子。sql
图2-1 一个网上论坛示例:GoogleGroups里面的Go编程语言论坛数据库
从本质上来讲,网上论坛就至关于一个任何人均可以经过发帖来进行对话的公告板,公告板上面能够包含已注册用户以及未注册的匿名用户。论坛上的对话称为<span style=“font-family: Times New Roman,楷体_GB2312”>帖子(thread),一个帖子一般包含了做者想要讨论的一个主题,而其余用户则能够经过回复这个帖子来参与对话。比较复杂的论坛通常都会按层级进行划分,在这些论坛里面,可能会有多个讨论特定类型主题的子论坛存在。大多数论坛都会由一个或多个拥有特殊权限的用户进行管理,这些拥有特殊权限的用户被称为<span style=“font-family: Times New Roman,楷体_GB2312”>版主(moderator)。编程
在本章中,咱们将会开发一个名为ChitChat的简易网上论坛。为了让这个例子保持简单,咱们只会为ChitChat实现网上论坛的关键特性:在这个论坛里面,用户能够注册帐号,并在登陆以后发表新帖子又或者回复已有的帖子;未注册用户能够查看帖子,可是没法发表帖子或是回复帖子。如今,让咱们首先来思考一下如何设计ChitChat这个应用。bootstrap
{关于本章展现的代码!}
跟本书的其余章节不同,由于篇幅的关系,本章并不会展现ChitChat论坛的全部实现代码,但你能够在GitHub页面https://github.com/sausheong/gwp找到这些代码。若是你打算在阅读本章的同时实际了解一下这个应用,那么这些完整的代码应该会对你有所帮助。
正如第1章所说,Web应用的通常工做流程是客户端向服务器发送请求,而后服务器对客户端进行响应(如图2-2所示),ChitChat应用的设计也遵循这一流程。
图2-2 Web应用的通常工做流程,客户端向服务器发送请求,而后等待接收响应
ChitChat的应用逻辑会被编码到服务器里面。服务器会向客户端提供HTML页面,并经过页面的超连接向客户端代表请求的格式以及被请求的数据,而客户端则会在发送请求时向服务器提供相应的数据,如图2-3所示。
图2-3 HTTP请求的URL格式
请求的格式一般是由应用自行决定的,好比,ChitChat的请求使用的是如下格式:http://<服务器名><处理器名>?<参数>
。
<span style=“font-family: Times New Roman,楷体_GB2312”>服务器名(server name)是ChitChat服务器的名字,而<span style=“font-family: Times New Roman,楷体_GB2312”>处理器名(handler name)则是被调用的处理器的名字。处理器的名字是按层级进行划分的:位于名字最开头是被调用模块的名字,而以后跟着的则是被调用子模块的名字,以此类推,位于处理器名字最末尾的则是子模块中负责处理请求的处理器。好比,对/thread/read
这个处理器名字来讲,thread
是被调用的模块,而read
则是这个模块中负责读取帖子内容的处理器。
该应用的<span style=“font-family: Times New Roman,楷体_GB2312”>参数(parameter)会以URL查询的形式传递给处理器,而处理器则会根据这些参数对请求进行处理。好比说,假设客户端要向处理器传递帖子的惟一ID,那么它能够将URL的参数部分设置成id=123
,其中123
就是帖子的惟一ID。
若是chitchat
就是ChitChat服务器的名字,那么根据上面介绍的URL格式规则,客户端发送给ChitChat服务器的URL可能会是这样的:http://chitchat/thread/read?id=123。
当请求到达服务器时,<span style=“font-family: Times New Roman,楷体_GB2312”>多路复用器(multiplexer)会对请求进行检查,并将请求重定向至正确的处理器进行处理。处理器在接收到多路复用器转发的请求以后,会从请求中取出相应的信息,并根据这些信息对请求进行处理。在请求处理完毕以后,处理器会将所得的数据传递给模板引擎,而模板引擎则会根据这些数据生成将要返回给客户端的HTML,整个过程如图2-4所示。
图2-4 服务器在典型Web应用中的工做流程
绝大多数应用都须要以某种方式与数据打交道。对ChitChat来讲,它的数据将被存储到关系式数据库PostgreSQL里面,并经过SQL与之交互。
ChitChat的数据模型很是简单,只包含4种数据结构,它们分别是:
以上这4种数据结构都会被映射到关系数据库里面,图2-5展现了这4种数据结构是如何与数据库交互的。
ChitChat论坛容许用户在登陆以后发布新帖子或者回复已有的帖子,未登陆的用户能够阅读帖子,可是不能发布新帖子或者回复帖子。为了对应用进行简化,ChitChat论坛没有设置版主这一职位,所以用户在发布新帖子或者添加新回复的时候不须要通过审核。
图2-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来对请求进行响应的。
由于编译后的二进制Go应用老是以main
函数做为执行的起点,因此咱们在对Go应用进行介绍的时候也老是从包含main
函数的主源码文件(main source code file)开始。ChitChat应用的主源码文件为main.go
,代码清单2-1展现了它的一个简化版本。
代码清单2-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
结构获得,因此程序并不须要向处理器显式地传入任何请求参数。
须要注意的是,前面的介绍模糊了处理器以及处理器函数之间的区别:咱们刚开始谈论的是处理器,而如今谈论的倒是处理器函数。这是有意而为之的——尽管处理器和处理器函数提供的最终结果是同样的,但它们实际上<span style=“font-family: Times New Roman,楷体_GB2312”>并不相同。本书的第3章将对处理器和处理器函数之间的区别作进一步的说明,可是如今让咱们暂时先忘掉这件事,继续研究ChitChat应用的代码实现。
除负责将请求重定向到相应的处理器以外,多路复用器还须要为静态文件提供服务。为了作到这一点,程序使用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
复制代码
当服务器成功地找到这个文件以后,会把它返回给客户端。
正如以前的小节所说,ChitChat应用会经过HandleFunc
函数把请求重定向处处理器函数。正如代码清单2-2所示,处理器函数实际上就是一个接受ResponseWriter
和Request
指针做为参数的Go函数。
代码清单2-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
文件实际上还包含不少其余的处理器函数,如代码清单2-3所示。
代码清单2-3 ChitChat应用的main.go
源文件
package main
import (
"net/http"
)
func main() {
mux := http.NewServeMux()
files := http.FileServer(http.Dir(config.Static))
mux.Handle("/static/", http.StripPrefix("/static/", files))
mux.HandleFunc("/", index)
mux.HandleFunc("/err", err)
mux.HandleFunc("/login", login)
mux.HandleFunc("/logout", logout)
mux.HandleFunc("/signup", signup)
mux.HandleFunc("/signup_account", signupAccount)
mux.HandleFunc("/authenticate", authenticate)
mux.HandleFunc("/thread/new", newThread)
mux.HandleFunc("/thread/create", createThread)
mux.HandleFunc("/thread/post", postThread)
mux.HandleFunc("/thread/read", readThread)
server := &http.Server{
Addr: "0.0.0.0:8080",
Handler: mux,
}
server.ListenAndServe()
}
复制代码
main
函数中使用的这些处理器函数并无在main.go
文件中定义,它们的定义在其余文件里面,具体请参考ChitChat项目的完整源码。
为了在一个文件里面引用另外一个文件中定义的函数,诸如PHP、Ruby和Python这样的语言要求用户编写代码去包含(include)被引用函数所在的文件,而另外一些语言则要求用户在编译程序时使用特殊的连接(link)命令。
可是对Go语言来讲,用户只须要把位于相同目录下的全部文件都设置成同一个包,那么这些文件就会与包中的其余文件分享彼此的定义。又或者,用户也能够把文件放到其余独立的包里面,而后经过导入(import)这些包来使用它们。好比,ChitChat论坛就把链接数据库的代码放到了独立的包里面,咱们很快就会看到这一点。
跟其余不少Web应用同样,ChitChat既拥有任何人均可以访问的公开页面,也拥有用户在登陆帐号以后才能看见的私人页面。
当一个用户成功登陆之后,服务器必须在后续的请求中标示出这是一个已登陆的用户。为了作到这一点,服务器会在响应的首部中写入一个cookie,而客户端在接收这个cookie以后则会把它存储到浏览器里面。代码清单2-4展现了authenticate
处理器函数的实现代码,这个函数定义在route_auth.go
文件中,它的做用就是对用户的身份进行验证,并在验证成功以后向客户端返回一个cookie。
代码清单2-4
文件中的route_auth.go
authenticate
处理器函数
func authenticate(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
user, _ := data.UserByEmail(r.PostFormValue("email"))
if user.Password == data.Encrypt(r.PostFormValue("password")) {
session := user.CreateSession()
cookie := http.Cookie{
Name: "_cookie",
Value: session.Uuid,
HttpOnly: true,
}
http.SetCookie(w, &cookie)
http.Redirect(w, r, "/", 302)
} else {
http.Redirect(w, r, "/login", 302)
}
}
复制代码
注意,代码清单2-4中的authenticate
函数使用了两个咱们还没有介绍过的函数,一个是data.Encrypt
,而另外一个则是data.UserbyEmail
。由于本节关注的是ChitChat论坛的访问控制机制而不是数据处理方法,因此本节将不会对这两个函数的实现细节进行解释,但这两个函数的名字已经很好地说明了它们各自的做用:data.UserByEmail
函数经过给定的电子邮件地址获取与之对应的User
结构,而data.Encrypt
函数则用于加密给定的字符串。本章稍后将会对data
包做更详细的介绍,可是在此以前,让咱们回到对访问控制机制的讨论上来。
在验证用户身份的时候,程序必须先确保用户是真实存在的,而且提交给处理器的密码在加密以后跟存储在数据库里面的已加密用户密码彻底一致。在核实了用户的身份以后,程序会使用User
结构的CreateSession
方法建立一个Session
结构,该结构的定义以下:
type Session struct {
Id int
Uuid string
Email string
UserId int
CreatedAt time.Time
}
复制代码
Session
结构中的Email
字段用于存储用户的电子邮件地址,而UserId
字段则用于记录用户表中存储用户信息的行的ID。Uuid
字段存储的是一个随机生成的惟一ID,这个ID是实现会话机制的核心,服务器会经过cookie把这个ID存储到浏览器里面,并把Session
结构中记录的各项信息存储到数据库中。
在建立了Session
结构以后,程序又建立了Cookie
结构:
cookie := http.Cookie{
Name: "_cookie",
Value: session.Uuid,
HttpOnly: true,
}
复制代码
cookie的名字是随意设置的,而cookie的值则是将要被存储到浏览器里面的惟一ID。由于程序没有给cookie设置过时时间,因此这个cookie就成了一个会话cookie,它将在浏览器关闭时自动被移除。此外,程序将HttpOnly
字段的值设置成了true
,这意味着这个cookie只能经过HTTP或者HTTPS访问,可是却没法经过JavaScript等非HTTP API进行访问。
在设置好cookie以后,程序使用如下这行代码,将它添加到了响应的首部里面:
http.SetCookie(writer, &cookie)
复制代码
在将cookie存储到浏览器里面以后,程序接下来要作的就是在处理器函数里面检查当前访问的用户是否已经登陆。为此,咱们须要建立一个名为session
的工具(utility)函数,并在各个处理器函数里面复用它。代码清单2-5展现了session
函数的实现代码,跟其余工具函数同样,这个函数也是在util.go
文件里面定义的。再提醒一下,虽然程序把工具函数的定义都放在了util.go
文件里面,可是由于util.go
文件也隶属于main
包,因此这个文件里面定义的全部工具函数均可以直接在整个main
包里面调用,而没必要像data.Encrypt
函数那样须要先引入包而后再调用。
代码清单2-5 util.go
文件中的session
工具函数
func session(w http.ResponseWriter, r *http.Request)(sess data.Session, err
error){
cookie, err := r.Cookie("_cookie")
if err == nil {
sess = data.Session{Uuid: cookie.Value}
if ok, _ := sess.Check(); !ok {
err = errors.New("Invalid session")
}
}
return
}
复制代码
为了从请求中取出cookie,session
函数使用了如下代码:
cookie, err := r.Cookie("_cookie")
复制代码
若是cookie不存在,那么很明显用户并未登陆;相反,若是cookie存在,那么session
函数将继续进行第二项检查——访问数据库并核实会话的惟一ID是否存在。第二项检查是经过data.Session
函数完成的,这个函数会从cookie中取出会话并调用后者的Check
方法:
sess = data.Session{Uuid: cookie.Value}
if ok, _ := sess.Check(); !ok {
err = errors.New("Invalid session")
}
复制代码
在拥有了检查和识别已登陆用户和未登陆用户的能力以后,让咱们来回顾一下以前展现的index
处理器函数,代码清单2-6中被加粗的代码行展现了这个处理器函数是如何使用session
函数的。
{--:}代码清单2-6 index
处理器函数
func index(w http.ResponseWriter, r *http.Request) {
threads, err := data.Threads(); if err == nil {
, err := session(w, r)
public_tmpl_files := []string{"templates/layout.html",
"templates/public.navbar.html",
"templates/index.html"}
private_tmpl_files := []string{"templates/layout.html",
"templates/private.navbar.html",
"templates/index.html"}
var templates *template.Template
if err != nil {
templates = template.Must(template.Parse-
Files(private_tmpl_files...))
} else {
templates = template.Must(template.ParseFiles(public_tmpl_files...))
}
templates.ExecuteTemplate(w, "layout", threads)
}
}复制代码
经过调用session
函数能够取得一个存储了用户信息的Session
结构,不过由于index
函数目前并不须要这些信息,因此它使用<span style=“font-family: Times New Roman,楷体_GB2312”>空白标识符(blank identifier)(_)忽略了这一结构。index
函数真正感兴趣的是err
变量,程序会根据这个变量的值来判断用户是否已经登陆,而后以此来选择是使用public
导航条仍是使用private
导航条。
好的,关于ChitChat应用处理请求的方法就介绍到这里了。本章接下来会继续讨论如何为客户端生成HTML,并完整地叙述以前没有说完的部分。
index
处理器函数里面的大部分代码都是用来为客户端生成HTML的。首先,函数把每一个须要用到的模板文件都放到了Go切片里面(这里展现的是私有页面的模板文件,公开页面的模板文件也是以一样方式进行组织的):
private_tmpl_files := []string{"templates/layout.html",
"templates/private.navbar.html",
"templates/index.html"}
复制代码
跟Mustache和CTemplate等其余模板引擎同样,切片指定的这3个HTML文件都包含了特定的嵌入命令,这些命令被称为<span style=“font-family: Times New Roman,楷体_GB2312”>动做(action),动做在HTML文件里面会被{{
符号和}}
符号包围。
接着,程序会调用ParseFiles
函数对这些模板文件进行语法分析,并建立出相应的模板。为了捕捉语法分析过程当中可能会产生的错误,程序使用了Must
函数去包围ParseFiles
函数的执行结果,这样当ParseFiles
返回错误的时候,Must
函数就会向用户返回相应的错误报告:
templates := template.Must(template.ParseFiles(private_tmpl_files...))
复制代码
好的,关于模板文件的介绍已经足够多了,如今是时候来看看它们的庐山真面目了。
ChitChat论坛的每一个模板文件都定义了一个模板,这种作法并非强制的,用户也能够在一个模板文件里面定义多个模板,但模板文件和模板一一对应的作法能够给开发带来方便,咱们在以后就会看到这一点。代码清单2-7展现了layout.html
模板文件的源代码,源代码中使用了define
动做,这个动做经过文件开头的{{ define "layout" }}
和文件末尾的{{ end }}
,把被包围的文本块定义成了layout
模板的一部分。
代码清单2-7 layout.html
模板文件
{{ define "layout" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=9">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ChitChat</title>
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/font-awesome.min.css" rel="stylesheet">
</head>
<body>
{{ template "navbar" . }}
<div class="container">
{{ template "content" . }}
</div> <!-- /container -->
<script src="/static/js/jquery-2.1.1.min.js"></script>
<script src="/static/js/bootstrap.min.js"></script>
</body>
</html>
{{ end }}
复制代码
除了define
动做以外,layout.html
模板文件里面还包含了两个用于引用其余模板文件的template
动做。跟在被引用模板名字以后的点(.
)表明了传递给被引用模板的数据,好比{{ template "navbar" . }}
语句除了会在语句出现的位置引入navbar
模板以外,还会将传递给layout
模板的数据传递给navbar
模板。
代码清单2-8展现了public.navbar.html
模板文件中的navbar
模板,除了定义模板自身的define
动做以外,这个模板没有包含其余动做(严格来讲,模板也能够不包含任何动做)。
代码清单2-8 public.navbar.html
模板文件
{{ define "navbar" }}
<div class="navbar navbar-default navbar-static-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed"
➥ data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">
<i class="fa fa-comments-o"></i>
ChitChat
</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="/login">Login</a></li>
</ul>
</div>
</div>
</div>
{{ end }}
复制代码
最后,让咱们来看看定义在index.html
模板文件中的content
模板,代码清单2-9展现了这个模板的源代码。注意,尽管以前展现的两个模板都与模板文件拥有相同的名字,但实际上模板和模板文件分别拥有不一样的名字也是可行的。
代码清单2-9 index.html
模板文件
{{ define "content" }}
<p class="lead">
<a href="/thread/new">Start a thread</a> or join one below!
</p>
{{ range . }}
<div class="panel panel-default">
<div class="panel-heading">
<span class="lead"> <i class="fa fa-comment-o"></i> {{ .Topic }}</span>
</div>
<div class="panel-body">
Started by {{ .User.Name }} - {{ .CreatedAtDate }} - {{ .NumReplies }}
posts.
<div class="pull-right">
<a href="/thread/read?id={{.Uuid }}">Read more</a>
</div>
</div>
</div>
{{ end }}
{{ end }}
复制代码
index.html
文件里面的代码很是有趣,特别值得一提的是文件里面包含了几个以点号(.
)开头的动做,好比{{ .User.Name }}
和{{ .CreatedAtDate }}
,这些动做的做用和以前展现过的index
处理器函数有关:
threads, err := data.Threads(); if err == nil {
templates.ExecuteTemplate(writer, "layout", threads)
}
复制代码
在如下这行代码中:
templates.ExecuteTemplate(writer, "layout", threads)
复制代码
程序经过调用ExecuteTemplate
函数,执行(execute)已经通过语法分析的layout
模板。执行模板意味着把模板文件中的内容和来自其余渠道的数据进行合并,而后生成最终的HTML内容,具体过程如图2-6所示。
图2-6 模板引擎经过合并数据和模板来生成HTML
程序之因此对layout
模板而不是navbar
模板或者content
模板进行处理,是由于layout
模板已经引用了其余两个模板,因此执行layout
模板就会致使其余两个模板也被执行,由此产生出预期的HTML。可是,若是程序只执行navbar
模板或者content
模板,那么程序最终只会产生出预期的HTML的一部分。
如今,你应该已经明白了,点号(.
)表明的就是传入到模板里面的数据(实际上还不只如此,接下来的小节会对这方面作进一步的说明)。图2-7展现了程序根据模板生成的ChitChat论坛的样子。
图2-7 ChitChat Web应用示例的主页
由于生成HTML的代码会被重复执行不少次,因此咱们决定对这些代码进行一些整理,并将它们移到代码清单2-10所示的generateHTML
函数里面。
代码清单2-10 generateHTML
函数
func generateHTML(w http.ResponseWriter, data interface{}, fn ...string) {
var files []string
for _, file := range fn {
files = append(files, fmt.Sprintf("templates/%s.html", file))
}
templates := template.Must(template.ParseFiles(files...))
templates.ExecuteTemplate(writer, "layout", data)
}
复制代码
generateHTML
函数接受一个ResponseWriter
、一些数据以及一系列模板文件做为参数,而后对给定的模板文件进行语法分析。data
参数的类型为空接口类型(empty interface type),这意味着该参数能够接受任何类型的值做为输入。刚开始接触Go语言的人可能会以为奇怪——Go不是静态编程语言吗,它为何可以使用没有类型限制的参数?
但实际上,Go程序能够经过接口(interface)机制,巧妙地绕过静态编程语言的限制,并藉此得到接受多种不一样类型输入的能力。Go语言中的接口由一系列方法构成,而且每一个接口就是一种类型。一个空接口就是一个空集合,这意味着任何类型均可以成为一个空接口,也就是说任何类型的值均可以传递给函数做为参数。
generateHTML
函数的最后一个参数以3个点(...
)开头,它表示generateHTML
函数是一个<span style=“font-family: Times New Roman,楷体_GB2312”>可变参数函数(variadic function),这意味着这个函数能够在最后的可变参数中接受零个或任意多个值做为参数。generateHTML
函数对可变参数的支持使咱们能够同时将任意多个模板文件传递给该函数。在Go语言里面,可变参数必须是可变参数函数的最后一个参数。
在实现了generateHTML
函数以后,让咱们回过头来,继续对index
处理器函数进行整理。代码清单2-11展现了通过整理以后的index
处理器函数,如今它看上去更整洁了。
代码清单2-11 index
处理器函数的最终版本
func index(writer http.ResponseWriter, request *http.Request) {
threads, err := data.Threads(); if err == nil {
_, err := session(writer, request)
if err != nil {
generateHTML(writer, threads, "layout", "public.navbar", "index")
} else {
generateHTML(writer, threads, "layout", "private.navbar", "index")
}
}
}
复制代码
在这一节中,咱们学习了不少关于模板的基础知识,以后的第5章将对模板作更详细的介绍。可是在此以前,让咱们先来了解一下ChitChat应用使用的数据源(data source),并藉此了解一下ChitChat应用的数据是如何与模板一同生成最终的HTML的。
在本章以及后续几章中,每当遇到须要访问关系数据库的场景,咱们都会使用PostgreSQL。在开始使用PostgreSQL以前,咱们首先须要学习的是如何安装并运行PostgreSQL,以及如何建立本章所需的数据库。
www.postgresql.org/download为各类不一样版本的Linux和FreeBSD都提供了预编译的二进制安装包,用户只须要下载其中一个安装包,而后根据指示进行安装就能够了。好比说,经过执行如下命令,咱们能够在Ubuntu发行版上安装Postgres:
sudo apt-get install postgresql postgresql-contrib
复制代码
这条命令除了会安装postgres
包以外,还会安装附加的工具包,并在安装完毕以后启动PostgreSQL数据库系统。
在默认状况下,Postgres会建立一个名为postgres
的用户,并将其用于链接服务器。为了操做方便,你也可使用本身的名字建立一个Postgres帐号。要作到这一点,首先须要登入Postgres帐号:
sudo su postgres
复制代码
接着使用createuser
命令建立一个PostgreSQL帐号:
createuser –interactive
复制代码
最后,还须要使用createdb
命令建立以你的帐号名字命名的数据库:
createdb <YOUR ACCOUNT NAME>
复制代码
要在Mac OS X上安装PostgreSQL,最简单的方法是使用PostgresApp.com提供的Postgres应用:你只须要把网站上提供的zip压缩包下载下来,解压它,而后把Postgres.app
文件拖曳到本身的Applications
文件夹里面就能够了。启动Postgres.app
的方法跟启动其余Mac OS X应用的方法彻底同样。Postgres.app
在初次启动的时候会初始化一个新的数据库集群,并为本身建立一个数据库。由于命令行工具psql
也包含在了Postgres.app
里面,因此在设置好正确的路径以后,你就可使用psql
访问数据库了。设置路径的工做能够经过在你的~/.profile
文件或者~/.bashrc
文件中添加如下代码行来完成[1]:
export PATH=$PATH:/Applications/Postgres.app/Contents/Versions/9.4/bin
复制代码
由于Windows系统上的不少PostgreSQL图形安装程序都会把一切安装步骤布置稳当,用户只须要进行相应的设置就能够了,因此在Windows系统上安装PostgreSQL也是很是简单和直观的。其中一个流行的安装程序是由Enterprise DB提供的:www.enterprisedb.com/products- services-training/pgdownload。
除了PostgreSQL数据库自己以外,安装包还会附带诸如pgAdmin等工具,以便用户经过这些工具进行后续的配置。
本章前面在展现ChitChat应用的设计方案时,曾经提到过ChitChat应用包含了4种数据结构。虽然把这4种数据结构放到主源码文件里面也是能够的,但更好的办法是把全部与数据相关的代码都放到另外一个包里面——ChitChat应用的data
包也所以应运而生。
为了建立data
包,咱们首先须要建立一个名为data
的子目录,并建立一个用于保存全部帖子相关代码的thread.go
文件(在以后的小节里面,咱们还会建立一个用于保存全部用户相关代码的user.go
文件)。在此以后,每当程序须要用到data
包的时候(好比处理器须要访问数据库的时候),程序都须要经过import
语句导入这个包:
import (
"github.com/sausheong/gwp/Chapter_2_Go_ChitChat/chitchat/data"
)
复制代码
代码清单2-12展现了定义在thread.go
文件里面的Thread
结构,这个结构存储了与帖子有关的各类信息。
{--:}代码清单2-12 定义在thread.go
文件里面的Thread
结构
package data
import(
"time"
)
type Thread struct {
Id int
Uuid string
Topic string
UserId int
CreatedAt time.Time
}复制代码
正如代码清单2-12中加粗显示的代码行所示,文件的包名如今是data
而再也不是main
了,这个包就是前面小节中咱们曾经见到过的data
包。data
包除了包含与数据库交互的结构和代码,还包含了一些与数据处理密切相关的函数。隶属于其余包的程序在引用data
包中定义的函数、结构或者其余东西时,必须在被引用元素的名字前面显式地加上data
这个包名。好比说,引用Thread
结构就须要使用data.Thread
这个名字,而不能仅仅使用Thread
这个名字。
Thread
结构应该与建立关系数据库表threads
时使用的<span style=“font-family: Times New Roman,楷体_GB2312”>数据定义语言(Data Definition Language,<SPAN STYLE=“FONT-FAMILY: TIMES NEW ROMAN,楷体_GB2312”>DDL)保持一致。由于threads
表目前还没有存在,因此咱们必须建立这个表以及容纳该表的数据库。建立chitchat
数据库的工做能够经过执行如下命令来完成:
createdb chitchat
复制代码
在建立数据库以后,咱们就能够经过代码清单2-13展现的setup.sql
文件为ChitChat论坛建立相应的数据库表了。
代码清单2-13 用于在PostgreSQL里面建立数据库表的setup.sql
文件
create table users (
id serial primary key,
uuid varchar(64) not null unique,
name varchar(255),
email varchar(255) not null unique,
password varchar(255) not null,
created_at timestamp not null
);
create table sessions (
id serial primary key,
uuid varchar(64) not null unique,
email varchar(255),
user_id integer references users(id),
created_at timestamp not null
);
create table threads (
id serial primary key,
uuid varchar(64) not null unique,
topic text,
user_id integer references users(id),
created_at timestamp not null
);
create table posts (
id serial primary key,
uuid varchar(64) not null unique,
body text,
user_id integer references users(id),
thread_id integer references threads(id),
created_at timestamp not null
);
复制代码
运行这个脚本须要用到psql
工具,正如上一节所说,这个工具一般会随着PostgreSQL一同安装,因此你只须要在终端里面执行如下命令就能够了:
psql –f setup.sql –d chitchat
复制代码
若是一切正常,那么以上命令将在chitchat
数据库中建立出相应的表。在拥有了表以后,程序就必须考虑如何与数据库进行链接以及如何对表进行操做了。为此,程序建立了一个名为Db
的全局变量,这个全局变量是一个指针,指向的是表明数据库链接池的sql.DB
,然后续的代码则会使用这个Db
变量来执行数据库查询操做。代码清单2-14展现了Db
变量在data.go
文件中的定义,此外还展现了一个用于在Web应用启动时对Db
变量进行初始化的init
函数。
代码清单2-14
文件中的data.go
Db
全局变量以及init
函数
Var Db *sql.DB
func init() {
var err error
Db, err = sql.Open("postgres", "dbname=chitchat sslmode=disable")
if err != nil {
log.Fatal(err)
}
return
}
复制代码
如今程序已经拥有告终构、表以及一个指向数据库链接池的指针,接下来要考虑的是如何链接(connect)Thread
结构和threads
表。幸运的是,要作到这一点并不困难:跟ChitChat应用的其余部分同样,咱们只须要建立可以在结构和数据库之间互动的函数就能够了。例如,为了从数据库里面取出全部帖子并将其返回给index
处理器函数,咱们可使用thread.go
文件中定义的Threads
函数,代码清单2-15给出了这个函数的定义。
代码清单2-15 threads.go
文件中定义的Threads
函数
func Threads() (threads []Thread, err error){
rows, err := Db.Query("SELECT id, uuid, topic, user_id, created_at FROM threads ORDER BY created_at DESC")
if err != nil {
return
}
for rows.Next() {
th := Thread{}
if err = rows.Scan(&th.Id, &th.Uuid, &th.Topic, &th.UserId,
➥&th.CreatedAt); err != nil {
return
}
threads = append(threads, th)
}
rows.Close()
return
}
复制代码
简单来说,Threads
函数执行了如下工做:
(1)经过数据库链接池与数据库进行链接;
(2)向数据库发送一个SQL查询,这个查询将返回一个或多个行做为结果;
(3)遍历行,为每一个行分别建立一个Thread
结构,首先使用这个结构去存储行中记录的帖子数据,而后将存储了帖子数据的Thread
结构追加到传入的threads
切片里面;
(4)重复执行步骤3,直到查询返回的全部行都被遍历完毕为止。
本书的第6章将对数据库操做的细节作进一步的介绍。
在了解了如何将数据库表存储的帖子数据提取到Thread
结构里面以后,咱们接下来要考虑的就是如何在模板里面展现Thread
结构存储的数据了。在代码清单2-9中展现的index.html模板文件,有这样一段代码:
{{ range . }}
<div class="panel panel-default">
<div class="panel-heading">
<span class="lead"> <i class="fa fa-comment-o"></i> {{ .Topic }}</span>
</div>
<div class="panel-body">
Started by {{ .User.Name }} - {{ .CreatedAtDate }} - {{ .NumReplies }}
posts.
<div class="pull-right">
<a href="/thread/read?id={{.Uuid }}">Read more</a>
</div>
</div>
</div>
{{ end }}
复制代码
正如以前所说,模板动做中的点号(.
)表明传入模板的数据,它们会和模板一块儿生成最终的结果,而{{ range . }}
中的.
号表明的是程序在稍早以前经过Threads
函数取得的threads
变量,也就是一个由Thread
结构组成的切片。
range
动做假设传入的数据要么是一个由结构组成的切片,要么是一个由结构组成的数组,这个动做会遍历传入的每一个结构,而用户则能够经过字段名访问结构里面的字段,好比,动做{{ .Topic }}
访问的是Thread
结构的Topic
字段。注意,在访问字段时必须在字段名的前面加上点号,而且字段名的首字母必须大写。
用户除能够在字段名的前面加上点号来访问结构中的字段之外,还能够经过相同的方法调用一种名为<span style=“font-family: Times New Roman,楷体_GB2312”>方法(method)的特殊函数。好比,在上面展现的代码中,{{ .User.Name }}
、{{ .CreatedAtDate }}
和{{ .NumReplies }}
这些动做的做用就是调用结构中的同名方法,而不是访问结构中的字段。
方法是隶属于特定类型的函数,指针、接口以及包括结构在内的全部具名类型均可以拥有本身的方法。好比说,经过将函数与指向Thread
结构的指针进行绑定,能够建立出一个针对Thread
结构的方法,而传入方法里面的Thread
结构则称为<span style=“font-family: Times New Roman,楷体_GB2312”>接收者(receiver):方法能够访问接收者,也能够修改接收者。
做为例子,代码清单2-16展现了NumReplies
方法的实现代码。
代码清单2-16
文件中的thread.go
NumReplies
方法
func (thread *Thread) NumReplies() (count int) {
rows, err := Db.Query("SELECT count(*) FROM posts where thread_id = $1",
thread.Id)
if err != nil {
return
}
for rows.Next() {
if err = rows.Scan(&count); err != nil {
return
}
}
rows.Close()
return
}
复制代码
NumReplies
方法首先打开一个指向数据库的链接,接着经过执行一条SQL查询来取得帖子的数量,并使用传入方法里面的count
参数来记录这个值。最后,NumReplies
方法返回帖子的数量做为方法的执行结果,而模板引擎则使用这个值去代替模板文件中出现的{{ .NumReplies }}
动做。
经过为User
、Session
、Thread
和Post
这4种数据结构建立相应的函数和方法,ChitChat最终在处理器函数和数据库之间构建起了一个数据层,以此来避免处理器函数直接对数据库进行访问,图2-8展现了这个数据层和数据库以及处理器函数之间的关系。虽然有不少库均可以达到一样的效果,但亲自构建数据层可以帮助咱们学习如何对数据库进行基本的访问,并藉此了解到实现这种访问并不困难,只须要用到一些简单直接的代码,这一点是很是有益的。
图2-8 经过结构模型链接数据库和处理器
在本章的最后,让咱们来看一下ChitChat应用是如何启动服务器并将多路复用器与服务器进行绑定的。执行这一工做的代码是在main.go
文件里面定义的:
server := &http.Server{
Addr: "0.0.0.0:8080",
Handler: mux,
}
server.ListenAndServe()
复制代码
这段代码很是简单,它所作的就是建立一个Server
结构,而后在这个结构上调用ListenAndServe
方法,这样服务器就可以启动了。
如今,咱们能够经过执行如下命令来编译并运行ChitChat应用:
go build
复制代码
这个命令会在当前目录以及$GOPATH/bin
目录中建立一个名为chitchat
的二进制可执行文件,它就是ChitChat应用的服务器。接着,咱们能够经过执行如下命令来启动这个服务器:
./chitchat
复制代码
若是你已经按照以前所说的方法,在数据库里面建立了ChitChat应用所需的数据库表,那么如今你只须要访问http://localhost:8080/并注册一个新帐号,而后就可使用本身的帐号在论坛上发布新帖子了。
在本章的各节中,咱们对一个Go Web应用的不一样组成部分进行了初步的了解和观察。图2-9对整个应用的工做流程进行了介绍,其中包括:
(1)客户端向服务器发送请求;
(2)多路复用器接收到请求,并将其重定向到正确的处理器;
(3)处理器对请求进行处理;
(4)在须要访问数据库的状况下,处理器会使用一个或多个数据结构,这些数据结构都是根据数据库中的数据建模而来的;
(5)当处理器调用与数据结构有关的函数或者方法时,这些数据结构背后的模型会与数据库进行链接,并执行相应的操做;
(6)当请求处理完毕时,处理器会调用模板引擎,有时候还会向模板引擎传递一些经过模型获取到的数据;
(7)模板引擎会对模板文件进行语法分析并建立相应的模板,而这些模板又会与处理器传递的数据一块儿合并生成最终的HTML;
(8)生成的HTML会做为响应的一部分回传至客户端。
图2-9 Web应用工做流程概览
主要的步骤大概就是这些。在接下来的几章中,咱们会更加深刻地学习这一工做流程,并进一步了解该流程涉及的各个组件。
ResponseWriter
<span style=“font-family: Times New Roman,楷体_GB2312”>和Requeest
<span style=“font-family: Times New Roman,楷体_GB2312”>指针做为参数的Go函数。sql
<span style=“font-family: Times New Roman,楷体_GB2312”>包以及相应的SQL语句,用户能够将数据持久地存储在关系数据库中。[1] 在安装Postgres.app
时,你可能须要根据Postgres.app
的版本对路径的版本部分作相应的修改,好比,将其中的9.4
修改成9.5
或者9.6
,诸如此类。——译者注