下面的程序是一个用来运行 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 的字节切片。

    1. type Page struct {
    2. Title string
    3. Body []byte
    4. }

    为了在正在运行的程序之外保存我们的 wiki 页面,我们将使用简单的文本文件作为持久性存储。程序、模板和文本文件可以在示例代码的 code_examples/chapter_15/wiki 目录中找到。

    示例 15.12—wiki.go:

    1. package main
    2. import (
    3. "net/http"
    4. "io/ioutil"
    5. "log"
    6. "regexp"
    7. "text/template"
    8. )
    9. const lenPath = len("/view/")
    10. var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+$")
    11. var templates = make(map[string]*template.Template)
    12. var err error
    13. type Page struct {
    14. Title string
    15. Body []byte
    16. }
    17. func init() {
    18. for _, tmpl := range []string{"edit", "view"} {
    19. templates[tmpl] = template.Must(template.ParseFiles(tmpl + ".html"))
    20. }
    21. }
    22. func main() {
    23. http.HandleFunc("/view/", makeHandler(viewHandler))
    24. http.HandleFunc("/edit/", makeHandler(editHandler))
    25. http.HandleFunc("/save/", makeHandler(saveHandler))
    26. err := http.ListenAndServe(":8080", nil)
    27. if err != nil {
    28. log.Fatal("ListenAndServe: ", err.Error())
    29. }
    30. }
    31. func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.
    32. HandlerFunc {
    33. return func(w http.ResponseWriter, r *http.Request) {
    34. title := r.URL.Path[lenPath:]
    35. if !titleValidator.MatchString(title) {
    36. http.NotFound(w, r)
    37. return
    38. }
    39. fn(w, r, title)
    40. }
    41. }
    42. func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
    43. p, err := load(title)
    44. if err != nil {
    45. // 找不到页面
    46. http.Redirect(w, r, "/edit/" + title, http.StatusFound)
    47. return
    48. }
    49. renderTemplate(w, "view", p)
    50. }
    51. func editHandler(w http.ResponseWriter, r *http.Request, title string) {
    52. p, err := load(title)
    53. if err != nil {
    54. p = &Page{Title: title}
    55. }
    56. renderTemplate(w, "edit", p)
    57. }
    58. func saveHandler(w http.ResponseWriter, r *http.Request, title string) {
    59. body := r.FormValue("body")
    60. p := &Page{Title: title, Body: []byte(body)}
    61. err := p.save()
    62. if err != nil {
    63. http.Error(w, err.Error(), http.StatusInternalServerError)
    64. return
    65. }
    66. http.Redirect(w, r, "/view/" + title, http.StatusFound)
    67. }
    68. func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
    69. err := templates[tmpl].Execute(w, p)
    70. if err != nil {
    71. http.Error(w, err.Error(), http.StatusInternalServerError)
    72. }
    73. }
    74. func (p *Page) save() error {
    75. filename := p.Title + ".txt"
    76. // 创建一个只有当前用户拥有读写权限的文件
    77. return ioutil.WriteFile(filename, p.Body, 0600)
    78. }
    79. func load(title string) (*Page, error) {
    80. filename := title + ".txt"
    81. body, err := ioutil.ReadFile(filename)
    82. if err != nil {
    83. return nil, err
    84. }
    85. return &Page{Title: title, Body: body}, nil
    86. }

    让我们来通读代码:

    • 首先我们要导入需要的包,要构建一个 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),|htmlprintf "%s 的用法看下面的章节。

      • ii )template.Must(template.ParseFiles(tmpl + ".html")) 函数将模板文件转换成一个 *template.Template (Template 结构体的指针),为了提高效率,我们只在我们的程序中转换一次,放在 init() 函数中就可以很方便的实现了。这个模板对象被保存在内存中的一个以 html 文件名称为索引的 map 中。
    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 内容的输出方法)。