下面的程序是一个用来运行 wiki 的 web 应用,它用了不到 100 行代码实现了一组页面的显示、编辑、和保存。它是一个 Go
网站的 codelab 中的一个 wiki 教程,我认为它是最好的 Go 教程
之一;可以通过 wiki 查看完整的代码,可以更好的理解程序是如何构建的。在这里我们将从上到下对整个程序进行补充说明。这个程序是一个 web 服务器,所以它必须在命令行启动(译者注:不要在 IDE 中启动,否则会找不到路径,必须在命令行启动),比如在 8080 端口。浏览器可以通过像这样的 url 来访问 wiki 页面的内容: localhost:8080/view/page1 。
然后会到和这个名字(page1)相同的文本文件中读取文件的内容展示在页面中;页面中包含了一个可以编辑 wiki 页面的超链接( localhost:8080/edit/page1 )。编辑页面用一个文本框显示内容,用户可以修改文本并通过 Save 按钮保存到文件中;然后会在相同的页面(view/page1)中查看到被修改的内容。如果想要查看的页面不存在(例如: localhost:8080/edit/page999 ),程序会将其跳转到一个编辑页面,这样就可以创建并保存一个新的 wiki 页面。
这个 wiki 页面需要一个标题和文本内容;它在程序中是由下面的结构体组成,内容是一个叫 Body 的字节切片。
type Page struct {
Title string
Body []byte
}
为了在正在运行的程序之外保存我们的 wiki 页面,我们将使用简单的文本文件作为持久性存储。程序、模板和文本文件可以在示例代码的 code_examples/chapter_15/wiki 目录中找到。
示例 15.12—wiki.go:
package main
import (
"net/http"
"io/ioutil"
"log"
"regexp"
"text/template"
)
const lenPath = len("/view/")
var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+$")
var templates = make(map[string]*template.Template)
var err error
type Page struct {
Title string
Body []byte
}
func init() {
for _, tmpl := range []string{"edit", "view"} {
templates[tmpl] = template.Must(template.ParseFiles(tmpl + ".html"))
}
}
func main() {
http.HandleFunc("/view/", makeHandler(viewHandler))
http.HandleFunc("/edit/", makeHandler(editHandler))
http.HandleFunc("/save/", makeHandler(saveHandler))
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err.Error())
}
}
func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.
HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[lenPath:]
if !titleValidator.MatchString(title) {
http.NotFound(w, r)
return
}
fn(w, r, title)
}
}
func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
p, err := load(title)
if err != nil {
// 找不到页面
http.Redirect(w, r, "/edit/" + title, http.StatusFound)
return
}
renderTemplate(w, "view", p)
}
func editHandler(w http.ResponseWriter, r *http.Request, title string) {
p, err := load(title)
if err != nil {
p = &Page{Title: title}
}
renderTemplate(w, "edit", p)
}
func saveHandler(w http.ResponseWriter, r *http.Request, title string) {
body := r.FormValue("body")
p := &Page{Title: title, Body: []byte(body)}
err := p.save()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/view/" + title, http.StatusFound)
}
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
err := templates[tmpl].Execute(w, p)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (p *Page) save() error {
filename := p.Title + ".txt"
// 创建一个只有当前用户拥有读写权限的文件
return ioutil.WriteFile(filename, p.Body, 0600)
}
func load(title string) (*Page, error) {
filename := title + ".txt"
body, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return &Page{Title: title, Body: body}, nil
}
让我们来通读代码:
- 首先我们要导入需要的包,要构建一个 web 服务器就要有 http 包,io/ioutil 包可以很轻松的对文件进行读写,regexp 用来验证标题的输入,template 可以动态创建我们的 html 文件;我们使用操作系统的错误。
- 我们希望阻止黑客输入,因为这样会破坏我们的服务器,所以我们将使用下面的正则表达式来检查用户在浏览器中的输入(wiki 页面的标题):
var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+$")
这个将在 makeHandler 函数中进行控制。 - 我们必须有一个将我们的 Page 结构体中的数据插入到 web 页面中的标题和内容中的机制,是通过 template 包来完成的:
- i )首先在编辑器中创建一个 html 模板文件,例如 view.html :
<h1>{{.Title |html}}</h1>
<p>[<a href="/edit/{{.Title |html}}">edit</a>]</p>
<div>{{printf "%s" .Body |html}}</div>
从数据结构中插入的字段被放在 {{ }}
之间,这里的 {{.Title |html}}
和 {{printf “%s” .Body |html}}
中的数据都是来自 Page 结构体(为了展示原理,示例被尽可能的简化了,当然这里也可以是非常复杂的 html),|html
与 printf "%s
的用法看下面的章节。
- ii )
template.Must(template.ParseFiles(tmpl + ".html"))
函数将模板文件转换成一个 *template.Template (Template 结构体的指针),为了提高效率,我们只在我们的程序中转换一次,放在init()
函数中就可以很方便的实现了。这个模板对象被保存在内存中的一个以 html 文件名称为索引的 map 中。
- ii )
templates = make(map[string]*template.Template)
这种技术被称为 模板缓存 ,并且是非常好的值得推荐的方法。
- 为了让模板和结构体输出到页面,我们必须使用
templates[tmpl].Execute(w, p)
函数。
- 为了让模板和结构体输出到页面,我们必须使用
它会调用一个模板,将 Page 结构体
p 作为一个参数在模板中进行替换,并且写入到 ResponseWriter w 中。这个函数必须去检查是否有错误输出;如果出现错误,我们调用 http.Error 来发送信号。这个代码将会在我们的程序中出现多次,所以我们把它分解成一个单独的函数 renderTemplate 。
- 在我们的 web 服务器的
main()
中启动一个使用 8080 端口的 ListenAndServe;像 15.2 章节 一样,我们先定义一些处理函数,它们的访问地址是在 localhost:8080/ 后面加上 view、edit 或者 save 作为开始部分(译者注:实际访问的时候还要加上充当持久化存储的文本文件的名称,如: localhost:8080/view/page999 )。在大多数的 web 服务器程序中,这一系列的访问路径的处理函数的形式,就类似于 Ruby and Rails、Django 或者 ASP.NET MVC 这种 MVC 框架的路由表。请求的网址与这些路径的匹配,会先去与最长的路径去匹配;如果没有与任何路径匹配,就会和 / 匹配,/ 对应的处理函数将会被调用(如果存在,不存在就是 404)。
这里我们定义了 3 个处理函数,并且因为它们包含了重复的代码,我们拆分出了一个 makeHandler 函数。
这是一个值得研究学习的相当特别的高阶函数;它用一个函数来作为它的第一个参数,并且将这个函数作为一个闭包返回:
func makeHandler(fn func(http.ResponseWriter, *http.Request, string))
http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[lenPath:]
if !titleValidator.MatchString(title) {
http.NotFound(w, r)
return
}
fn(w, r, title)
}
}
- 这个闭包为了构造它的返回值使用了一个闭合函数变量;但是在此之前,它使用了
titleValidator.MatchString(title)
来验证输入的标题。如果标题不是由字母与数字组成,会发出一个 NotFound 的错误信号。
(例如用 localhost:8080/view/page++ 来测试一下);在 main () 中的 makeHandler 的参数 viewhandler、edithandler 和 savehandler 都必须与 fn 的参数是相同类型。 - viewhandler 尝试去读取一个指定标题的文本文件; 这是通过一个
load()
函数来完成的,它重新组合了文件名并通过 ioutil.ReadFile 去读取文件。如果文件被找到,会将它的内容读取到一个本地的字符串类型的 body 变量中。将数据填入指向 Page 结构体的指针中:&Page {Title: title, Body: body}
并且将这个和一个为 nil 的错误一起返回给调用者。然后这个结构体通过 renderTemplate 来和模板合并。
如果出现错误,意味着磁盘中不存在 wiki 页面,将错误返回给viewHandler()
, 它对自动的重定向到这个标题对应的编辑页面。 - edithandler 几乎是一样的: 尝试去读取文件,如果找到,用它去渲染编辑模板页面:如果出现错误,创建一个新的 Page 对象,然后用这个标题去渲染它(译者注:存在就修改,不存在就添加)。
- 通过点击编辑页面的保存按钮将页面的内容保存;这个按钮在以
<form action="/save/{{.Title}}" method="POST" >
开头的 html 表单中。
这意味着当从 localhost/save/{Title} (通过模板替换 Title)网址发送一个请求,会被发送到 web 服务器。对于这样的网址,我们定义了一个处理函数:saveHandler()
。通过 request 中的FormValue()
方法,可以提取名字为 body 的文本域字段的内容,然后通过这个信息构造一个 Page 对象,并尝试通过save()
函数保存。如果失败,会返回一个 http.Error 显示到浏览器中,如果它成功了,浏览器会重定向一个相同名称的展示页面。save()
函数非常简单: 使用ioutil.WriteFile()
函数将 Page 结构体的 Body 字段写入一个叫 filename 的文件中。它使用{{ printf “%s” .Body|html}}
(译者注:不明白作者这句是什么意思,这个是模板中用来显示 Body 内容的输出方法)。