<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Web on chai2010 的博客</title>
    <link>https://chai2010.cn/tags/web/</link>
    <description>Recent content in Web on chai2010 的博客</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>zh-CN</language>
    <lastBuildDate>Sat, 06 Apr 2013 00:00:00 +0000</lastBuildDate>
    
        <atom:link href="https://chai2010.cn/tags/web/index.xml" rel="self" type="application/rss+xml" />
    
    
    <item>
      <title>用Go语言开发Web程序[翻译]</title>
      <link>https://chai2010.cn/post/golang/goweb2013/</link>
      <pubDate>Sat, 06 Apr 2013 00:00:00 +0000</pubDate>
      
      <guid>https://chai2010.cn/post/golang/goweb2013/</guid>
      
        <description>

&lt;!-- 标题: Go语言编写Web程序 --&gt;

&lt;!-- 翻译: chaishushan{AT}gmail.com --&gt;

&lt;!-- 原文: http://golang.org/doc/articles/wiki/ --&gt;

&lt;ul&gt;
&lt;li&gt;英文: &lt;a href=&#34;http://golang.org/doc/articles/wiki/&#34;&gt;http://golang.org/doc/articles/wiki/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;简介&#34;&gt;简介&lt;/h2&gt;

&lt;p&gt;本教程将讨论：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;创建一个支持加载和保存的数据结构&lt;/li&gt;
&lt;li&gt;使用 net/http 包来构建web应用程序&lt;/li&gt;
&lt;li&gt;使用 html/template 包来处理HTML模板&lt;/li&gt;
&lt;li&gt;使用 regexp 包来验证用户输入&lt;/li&gt;
&lt;li&gt;使用闭包&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;基本知识：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;有一定的编程经验&lt;/li&gt;
&lt;li&gt;了解基本的web技术(HTTP、HTML)&lt;/li&gt;
&lt;li&gt;一些UNIX/DOS命令行知识&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&#34;开始&#34;&gt;开始&lt;/h2&gt;

&lt;p&gt;目前、你需要一个运行FreeBSD、Linux、OS X 或 Windows的机器。 我们将使用 $ 来代表命令提示符。&lt;/p&gt;

&lt;p&gt;安装Go语言环境（参考 安装说明）。&lt;/p&gt;

&lt;p&gt;为本教程新建一个目录，将新建目录添加到GOPATH环境变量，然后命令行切换到新建目录:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ mkdir gowiki
$ cd gowiki
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;创建一个名为wiki.go的源文件，使用你喜欢的编辑器打开，并添加以下代码：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;package main

import (
    &amp;quot;fmt&amp;quot;
    &amp;quot;io/ioutil&amp;quot;
)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;我们从标准库导入了fmt和ioutil包。 后面我们将实现更多的功能，到时候我们会添加更多的包到import声明。&lt;/p&gt;

&lt;h2 id=&#34;数据结构&#34;&gt;数据结构&lt;/h2&gt;

&lt;p&gt;我们现定义数据结构。一个wiki通常有一些列相互关联的页面组成，每个页面有一个标题和一个主体（页面的内容）。 在这里，我们定的Page结构体包含标题和主体两个成员。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;type Page struct {
    Title string
    Body  []byte
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;类型 &lt;code&gt;[]byte&lt;/code&gt; 表示“一个byte切片”。 （参见 Go切片：用法和本质） 我们将Body成员定义为 &lt;code&gt;[]byte&lt;/code&gt; 而不是 &lt;code&gt;string&lt;/code&gt; 类型， 因为我们希望类型和 &lt;code&gt;io&lt;/code&gt; 库很好的配合，在后面会看到。&lt;/p&gt;

&lt;p&gt;Page描述的页面内容只是保存在内存中。但是如何进行持久存储呢？ 我们可以为Page类型创建一个save方法：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func (p *Page) save() error {
    filename := p.Title + &amp;quot;.txt&amp;quot;
    return ioutil.WriteFile(filename, p.Body, 0600)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;方法的签名这样读：“这是一个方法，名字叫save， 方法的接收者p是一个指向Page类型结构体的指针。 方法没有参数，但有一个error类型的返回值。”&lt;/p&gt;

&lt;p&gt;该方法会将Page的Body成员的值保存到一个文本文件。 为了简化，我们使用Title成员的值作为文件的名字。&lt;/p&gt;

&lt;p&gt;save方法返回的error值和WriteFile函数的返回类型 一致（将byte切片写入文件的标准库函数）。程序可以通过save方法返回的 error值判断写文件时是否遇到错误。如果写文件一切正常，Page.save() 将返回nil（对应指针、接口等类型的零值）。&lt;/p&gt;

&lt;p&gt;传递给WriteFile函数的第三个参数0600是一个八进制整数面值， 表示新创建的文件只对当前用户是读写权限。（更多信息请参考Unix手册 open(2)）&lt;/p&gt;

&lt;p&gt;除了保存页面，我们还需要加载页面：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func loadPage(title string) *Page {
    filename := title + &amp;quot;.txt&amp;quot;
    body, _ := ioutil.ReadFile(filename)
    return &amp;amp;Page{Title: title, Body: body}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;函数loadPage从title参数构造文件名，然后读取文件的内容到 新的变量body，最后返回两个值：一个指向由title和body构造的 Page面值并且错误返回值为nil。&lt;/p&gt;

&lt;p&gt;函数可以返回多个值。标准库函数io.ReadFile返回[]byte和error。 在loadPage函数中，错误信息被丢失了；“空白标识符”所代表的下划线（_） 符号用于扔掉错误返回值（本质上没哟分配任何值）。&lt;/p&gt;

&lt;p&gt;但是如果ReadFile遇到错误怎么办？对于这个例子，文件可能还不存在。我们不能忽略 类似的错误。我们修改函数返回*Page和error。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func loadPage(title string) (*Page, error) {
    filename := title + &amp;quot;.txt&amp;quot;
    body, err := ioutil.ReadFile(filename)
    if err != nil {
        return nil, err
    }
    return &amp;amp;Page{Title: title, Body: body}, nil
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个函数的调用者可以检测第二个返回参数；如果是nil表示成功加载页面。否则， error可以被调用者截获（更多信息请参考语言规范）。&lt;/p&gt;

&lt;p&gt;现在我们有了一个简单的数据结构，并且可以保存到文件和从文件加载页面。让我们写一个main 来测试一下：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func main() {
    p1 := &amp;amp;Page{Title: &amp;quot;TestPage&amp;quot;, Body: []byte(&amp;quot;This is a sample Page.&amp;quot;)}
    p1.save()
    p2, _ := loadPage(&amp;quot;TestPage&amp;quot;)
    fmt.Println(string(p2.Body))
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;在编译并运行程序后，会创建一个名为TestPage.txt的文件，内容是p1 包含的页面主体。然后文件的内容被读取到p2，并且打印其Body成员到屏幕。&lt;/p&gt;

&lt;p&gt;可以这样编译和运行程序：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ go build wiki.go
$ ./wiki
This is a sample page.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;（如果是使用Windows系统则不需要“wiki”前面的“./”。）&lt;/p&gt;

&lt;p&gt;点击这里浏览完整代码。&lt;/p&gt;

&lt;h2 id=&#34;了解net-http包-插曲&#34;&gt;了解net/http包（插曲）&lt;/h2&gt;

&lt;p&gt;这里是一个简要Web服务器的完整代码：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;package main

import (
    &amp;quot;fmt&amp;quot;
    &amp;quot;net/http&amp;quot;
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, &amp;quot;Hi there, I love %s!&amp;quot;, r.URL.Path[1:])
}

func main() {
    http.HandleFunc(&amp;quot;/&amp;quot;, handler)
    http.ListenAndServe(&amp;quot;:8080&amp;quot;, nil)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;main函数开始调用&lt;code&gt;http.HandleFunc&lt;/code&gt;，告诉&lt;code&gt;http&lt;/code&gt;包用&lt;code&gt;handler&lt;/code&gt;函数处理所以针对跟目录的访问请求（&amp;rdquo;/&amp;ldquo;）。&lt;/p&gt;

&lt;p&gt;然后调用&lt;code&gt;http.ListenAndServe&lt;/code&gt;，指定监听端口为8080（&amp;rdquo;:8080&amp;rdquo;）。 （目前先忽略第二个参数nil。）这个函数会阻塞直到程序终止。&lt;/p&gt;

&lt;p&gt;函数handler的类型是http.HandlerFunc。它的参数是 一个&lt;code&gt;http.ResponseWriter&lt;/code&gt;和一个&lt;code&gt;http.Request&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;参数&lt;code&gt;http.ResponseWriter&lt;/code&gt;汇总HTTP服务器的响应；向它写入的数据会发送 到HTTP客服端。&lt;/p&gt;

&lt;p&gt;参数http.Request是客户端请求数据对应的数据结构。 &lt;code&gt;r.URL.Path&lt;/code&gt;表示客户端请求的URL地址。后面的&lt;code&gt;[1:]&lt;/code&gt;含义是 “从Path的第一个字符到 末尾创建一个子切片。” 这样可以忽略URL路径中的开始的“/”字符。&lt;/p&gt;

&lt;p&gt;如果你运行程序并访问一些URL地址：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;http://localhost:8080/monkeys
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;程序会返回一个包含以下内容的页面：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Hi there, I love monkeys!
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;基于net-http包提供wiki页面&#34;&gt;基于net/http包提供wiki页面&lt;/h2&gt;

&lt;p&gt;使用前需要导入net/http包：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;import (
    &amp;quot;fmt&amp;quot;
    &amp;quot;net/http&amp;quot;
    &amp;quot;io/ioutil&amp;quot;
)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后我们创建一个viewHandler函数，用于处理浏览wiki页面。它会处理所有以&amp;rdquo;/view/&amp;ldquo;为前缀的URL地址。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;const lenPath = len(&amp;quot;/view/&amp;quot;)
func viewHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[lenPath:]
    p, _ := loadPage(title)
    fmt.Fprintf(w, &amp;quot;&amp;lt;h1&amp;gt;%s&amp;lt;/h1&amp;gt;&amp;lt;div&amp;gt;%s&amp;lt;/div&amp;gt;&amp;quot;, p.Title, p.Body)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;首先，该函数从r.URL.Path中取出要浏览页面的标题。 全局常量lenPath是URL前缀&amp;rdquo;/view/&amp;ldquo;的长度。 Path的切片&lt;code&gt;[lenPath:]&lt;/code&gt;用于忽略前面的6个字符。 这是因为URL地址是以&amp;rdquo;/view/&amp;ldquo;为前缀，它们不是页面标题的组成部分。&lt;/p&gt;

&lt;p&gt;接着加载页面数据，然后格式化为一个简单的HTML页面，写入到&lt;code&gt;http.ResponseWriter&lt;/code&gt;类型的w参数。&lt;/p&gt;

&lt;p&gt;这里又一次使用了&lt;code&gt;_&lt;/code&gt;来忽略loadPage返回的错误&lt;code&gt;error&lt;/code&gt;。 这里只是为了简化代码，它并不是好的编程实践。稍后我们会继续完善这个部分。&lt;/p&gt;

&lt;p&gt;要使用这个函数，我们需要修改main函数中的http初始化代码， 使用&lt;code&gt;viewHandler&lt;/code&gt;函数处理对应/view/地址的请求。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func main() {
    http.HandleFunc(&amp;quot;/view/&amp;quot;, viewHandler)
    http.ListenAndServe(&amp;quot;:8080&amp;quot;, nil)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;点击这里浏览完整代码。&lt;/p&gt;

&lt;p&gt;我们创建一些测试页面（例如test.txt），然后尝试提供一个wiki页面：&lt;/p&gt;

&lt;p&gt;使用编辑器打开test.txt文件，输入“Hello world”内容并保存（忽略双引号）。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ go build wiki.go
$ ./wiki
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果是使用Windows系统则不需要“wiki”前面的“./”。&lt;/p&gt;

&lt;p&gt;启动web服务器后，浏览&lt;a href=&#34;http://localhost:8080/view/test&#34;&gt;http://localhost:8080/view/test&lt;/a&gt; 将显示一个标题为“test”内容为“Hello world”的页面。&lt;/p&gt;

&lt;h2 id=&#34;编辑页面&#34;&gt;编辑页面&lt;/h2&gt;

&lt;p&gt;没有编辑能力的wiki就不是真正的wiki了。我们继续创建了两个函数： 一个editHandler用于显示编辑页面的界面，另一个saveHandler 用于保存编辑后的页面内容。&lt;/p&gt;

&lt;p&gt;我们先将它们加入到&lt;code&gt;main()&lt;/code&gt;函数：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func main() {
    http.HandleFunc(&amp;quot;/view/&amp;quot;, viewHandler)
    http.HandleFunc(&amp;quot;/edit/&amp;quot;, editHandler)
    http.HandleFunc(&amp;quot;/save/&amp;quot;, saveHandler)
    http.ListenAndServe(&amp;quot;:8080&amp;quot;, nil)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;函数editHandler加载页面，然后显示一个HTML编辑页面。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func editHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[lenPath:]
    p, err := loadPage(title)
    if err != nil {
        p = &amp;amp;Page{Title: title}
    }
    fmt.Fprintf(w, &amp;quot;&amp;lt;h1&amp;gt;Editing %s&amp;lt;/h1&amp;gt;&amp;quot;+
        &amp;quot;&amp;lt;form action=\&amp;quot;/save/%s\&amp;quot; method=\&amp;quot;POST\&amp;quot;&amp;gt;&amp;quot;+
        &amp;quot;&amp;lt;textarea name=\&amp;quot;body\&amp;quot;&amp;gt;%s&amp;lt;/textarea&amp;gt;&amp;lt;br&amp;gt;&amp;quot;+
        &amp;quot;&amp;lt;input type=\&amp;quot;submit\&amp;quot; value=\&amp;quot;Save\&amp;quot;&amp;gt;&amp;quot;+
        &amp;quot;&amp;lt;/form&amp;gt;&amp;quot;,
        p.Title, p.Title, p.Body)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个函数只是可工作，但是那些HTML相关的代码比较丑陋。 当然，还有更好的实现方式。&lt;/p&gt;

&lt;h2 id=&#34;使用html-template包&#34;&gt;使用html/template包&lt;/h2&gt;

&lt;p&gt;html/template是标准库中的包。我们使用html/template 包可以将HTML代码分离到一个文件，然后我们可以在不改变底层代码前提下调整和完善编辑页面。&lt;/p&gt;

&lt;p&gt;首先，我们导入html/template包。现在我们已经不再使用fmt包了， 因此需要删除它。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;import (
    &amp;quot;html/template&amp;quot;
    &amp;quot;http&amp;quot;
    &amp;quot;io/ioutil&amp;quot;
    &amp;quot;os&amp;quot;
)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;我们需要为编辑页面创建一个模板文件。新建edit.html文件， 并输入以下内容：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;h1&amp;gt;Editing {{.Title}}&amp;lt;/h1&amp;gt;

&amp;lt;form action=&amp;quot;/save/{{.Title}}&amp;quot; method=&amp;quot;POST&amp;quot;&amp;gt;
&amp;lt;div&amp;gt;&amp;lt;textarea name=&amp;quot;body&amp;quot; rows=&amp;quot;20&amp;quot; cols=&amp;quot;80&amp;quot;&amp;gt;{{printf &amp;quot;%s&amp;quot; .Body}}&amp;lt;/textarea&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;div&amp;gt;&amp;lt;input type=&amp;quot;submit&amp;quot; value=&amp;quot;Save&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;修改editHandler函数，使用模板代替硬编码HTML：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func editHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[lenPath:]
    p, err := loadPage(title)
    if err != nil {
        p = &amp;amp;Page{Title: title}
    }
    t, _ := template.ParseFiles(&amp;quot;edit.html&amp;quot;)
    t.Execute(w, p)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;函数&lt;code&gt;template.ParseFiles&lt;/code&gt;将读取edit.html目标文件， 返回值为&lt;code&gt;*template.Template&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;函数&lt;code&gt;t.Execute&lt;/code&gt;处理模板，将生成的HTML写入到&lt;code&gt;http.ResponseWriter&lt;/code&gt;。 其中以点开头的&lt;code&gt;.Title&lt;/code&gt;和&lt;code&gt;.Body&lt;/code&gt;标识符将被&lt;code&gt;p.Title&lt;/code&gt;和&lt;code&gt;p.Body&lt;/code&gt;替换。&lt;/p&gt;

&lt;p&gt;模板的驱动语句是被双花括弧包括的部分. &lt;code&gt;printf &amp;quot;%s&amp;quot; .Body&lt;/code&gt;表示将&lt;code&gt;.Body&lt;/code&gt;输出位字符串
而不是字节串, 类似&lt;code&gt;fmt.Printf&lt;/code&gt;函数的效果. &lt;code&gt;html/template&lt;/code&gt;可以保证输出有效的HTML字符串,
对于&lt;code&gt;(&amp;gt;)&lt;/code&gt;之类的特殊符号会自动替换为&lt;code&gt;&amp;amp;gt;&lt;/code&gt;等对应编码, 保证不会破坏原先的HTML结构.&lt;/p&gt;

&lt;p&gt;需要注意的是我们移除了&lt;code&gt;fmt.Fprintf&lt;/code&gt;语句, 因此也移除了&lt;code&gt;&amp;quot;fmt&amp;quot;&lt;/code&gt;包的导入语句.&lt;/p&gt;

&lt;p&gt;现在我们已经是基于模板方式的, 可以针对&lt;code&gt;viewHandler&lt;/code&gt;函数创建一个名为view.html的模板文件:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;&amp;lt;h1&amp;gt;{{.Title}}&amp;lt;/h1&amp;gt;

&amp;lt;p&amp;gt;[&amp;lt;a href=&amp;quot;/edit/{{.Title}}&amp;quot;&amp;gt;edit&amp;lt;/a&amp;gt;]&amp;lt;/p&amp;gt;

&amp;lt;div&amp;gt;{{printf &amp;quot;%s&amp;quot; .Body}}&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;也要调整&lt;code&gt;viewHandler&lt;/code&gt;函数:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func viewHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[lenPath:]
    p, _ := loadPage(title)
    t, _ := template.ParseFiles(&amp;quot;view.html&amp;quot;)
    t.Execute(w, p)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;观察可以发现前面是否模板的方式非常相似. 因此我们将模板独立大一个函数:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func viewHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[lenPath:]
    p, _ := loadPage(title)
    renderTemplate(w, &amp;quot;view&amp;quot;, p)
}

func editHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[lenPath:]
    p, err := loadPage(title)
    if err != nil {
        p = &amp;amp;Page{Title: title}
    }
    renderTemplate(w, &amp;quot;edit&amp;quot;, p)
}

func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
    t, _ := template.ParseFiles(tmpl + &amp;quot;.html&amp;quot;)
    t.Execute(w, p)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;现在的处理函数更加清晰简短.&lt;/p&gt;

&lt;h2 id=&#34;处理不存在的页面&#34;&gt;处理不存在的页面&lt;/h2&gt;

&lt;p&gt;如果访问&lt;code&gt;/view/APageThatDoesntExist&lt;/code&gt;会发生什么情况? 程序会崩溃掉.
这是因为程序忽略了&lt;code&gt;loadPage&lt;/code&gt;返回的错误信息. 为了处理页面不存在的情况,
程序会重定向到一个新页面的编辑页面:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func viewHandler(w http.ResponseWriter, r *http.Request) {
    title, err := getTitle(w, r)
    if err != nil {
        return
    }
    p, err := loadPage(title)
    if err != nil {
        http.Redirect(w, r, &amp;quot;/edit/&amp;quot;+title, http.StatusFound)
        return
    }
    renderTemplate(w, &amp;quot;view&amp;quot;, p)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;http.Redirect&lt;/code&gt;函数会添加&lt;code&gt;http.StatusFound (302)&lt;/code&gt;状态, 并且重新定位.&lt;/p&gt;

&lt;h2 id=&#34;保存页面&#34;&gt;保存页面&lt;/h2&gt;

&lt;p&gt;函数&lt;code&gt;saveHandler&lt;/code&gt;用于处理提交的表单.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func saveHandler(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Path[lenPath:]
    body := r.FormValue(&amp;quot;body&amp;quot;)
    p := &amp;amp;Page{Title: title, Body: []byte(body)}
    p.save()
    http.Redirect(w, r, &amp;quot;/view/&amp;quot;+title, http.StatusFound)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;页面的标题(URL提供)和表单的内容将作为一个新页面保存.
调用&lt;code&gt;save()&lt;/code&gt;方法将页面写到文件, 然后重定向到&lt;code&gt;/view/&lt;/code&gt;页面.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FormValue&lt;/code&gt;方法返回的返回值是字符串类型. 我们需要先转换为&lt;code&gt;[]byte&lt;/code&gt;, 然后填充到&lt;code&gt;Page&lt;/code&gt;
结构体. 我们通过&lt;code&gt;[]byte(body)&lt;/code&gt;语句做强制转换.&lt;/p&gt;

&lt;h2 id=&#34;错误处理&#34;&gt;错误处理&lt;/h2&gt;

&lt;p&gt;前面的代码基本都是忽略了错误处理. 这不是好的处理方式, 因为发生错误的话会导致程序崩溃.
好的处理方式是截获错误, 并给用户显示错误相关的信息. 这样即使发生错误, 服务器也
可以正常运行, 用户也可以收到错误提示信息.&lt;/p&gt;

&lt;p&gt;首先, 我先处理&lt;code&gt;renderTemplate&lt;/code&gt;中的错误:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
    t, err := template.ParseFiles(tmpl + &amp;quot;.html&amp;quot;)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    err = t.Execute(w, p)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;http.Error&lt;/code&gt;函数返回一个具体的错误码(这里是属于&amp;rdquo;服务器错误&amp;rdquo;类型)和错误信息.
看来刚才决定将模板处理独立到一个函数是一个正确的决定.&lt;/p&gt;

&lt;p&gt;下面是修复后的&lt;code&gt;saveHandler&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func saveHandler(w http.ResponseWriter, r *http.Request) {
    title, err := getTitle(w, r)
    if err != nil {
        return
    }
    body := r.FormValue(&amp;quot;body&amp;quot;)
    p := &amp;amp;Page{Title: title, Body: []byte(body)}
    err = p.save()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    http.Redirect(w, r, &amp;quot;/view/&amp;quot;+title, http.StatusFound)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;p.save()&lt;/code&gt;时发生的错误信息也将报告给用户.&lt;/p&gt;

&lt;h2 id=&#34;缓存模板&#34;&gt;缓存模板&lt;/h2&gt;

&lt;p&gt;前面的实现有一个性能缺陷: &lt;code&gt;renderTemplate&lt;/code&gt;每次都会调用&lt;code&gt;ParseFiles&lt;/code&gt;函数.
更好的优化思路是只在初始化的使用调用一次&lt;code&gt;ParseFiles&lt;/code&gt;, 将全部要处理的模板
放到一个&lt;code&gt;*Template&lt;/code&gt;中. 然后可以使用&lt;code&gt;ExecuteTemplate&lt;/code&gt;渲染指定的模板.&lt;/p&gt;

&lt;p&gt;首先创建一个名位&lt;code&gt;templates&lt;/code&gt;全局变量, 然后用&lt;code&gt;ParseFiles&lt;/code&gt;进行初始化.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;var templates = template.Must(template.ParseFiles(&amp;quot;edit.html&amp;quot;, &amp;quot;view.html&amp;quot;))
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;template.Must&lt;/code&gt;只是一个简便的包装, 当传递非&lt;code&gt;nil&lt;/code&gt;的错误是抛出&lt;code&gt;panic&lt;/code&gt;异常.
在这里抛出异常是合适的: 如果模板不能正常加载, 简单的处理方式就是退出程序.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ParseFiles&lt;/code&gt;接收任意数量的字符串参数为名字的模板文件, 并将这些文件解析到以基本文件名
的模板. 如果我们需要更多的模板, 可以直接将模板文件名添加到&lt;code&gt;ParseFiles&lt;/code&gt;参数中.&lt;/p&gt;

&lt;p&gt;然后是修改&lt;code&gt;renderTemplate&lt;/code&gt;函数, 调用&lt;code&gt;templates.ExecuteTemplate&lt;/code&gt;渲染指定的模板:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
    err := templates.ExecuteTemplate(w, tmpl+&amp;quot;.html&amp;quot;, p)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;需要注意的是模板名字对于模板文件的名字, 因此这里添加了&amp;rdquo;.html&amp;rdquo;后缀名.&lt;/p&gt;

&lt;h2 id=&#34;验证&#34;&gt;验证&lt;/h2&gt;

&lt;p&gt;你可能以及发现, 这个程序有严重的安全缺陷: 用户可以在服务器上读写任意独立路径.
为了降低这种风险, 我们编写一个函数以正则表达式的方式在验证标题的合法性.&lt;/p&gt;

&lt;p&gt;首先, 要导入&lt;code&gt;&amp;quot;regexp&amp;quot;&lt;/code&gt;包. 然后创建一个全局变量保存用于验证的正则表达式:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;var titleValidator = regexp.MustCompile(&amp;quot;^[a-zA-Z0-9]+$&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;函数&lt;code&gt;regexp.MustCompile&lt;/code&gt;将分析和编译正则表达式, 返回&lt;code&gt;regexp.Regexp&lt;/code&gt;.
&lt;code&gt;MustCompile&lt;/code&gt;和&lt;code&gt;Compile&lt;/code&gt;有些不同, &lt;code&gt;MustCompile&lt;/code&gt;遇到错误时会抛出&lt;code&gt;panic&lt;/code&gt;异常,
而&lt;code&gt;Compile&lt;/code&gt;在遇到错误时通过第二个返回值返回错误.&lt;/p&gt;

&lt;p&gt;现在, 让我们写一个函数&lt;code&gt;getTitle&lt;/code&gt;, 从请求的URL提取标题, 并且测试是否是有效的表达式:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func getTitle(w http.ResponseWriter, r *http.Request) (title string, err error) {
    title = r.URL.Path[lenPath:]
    if !titleValidator.MatchString(title) {
        http.NotFound(w, r)
        err = errors.New(&amp;quot;Invalid Page Title&amp;quot;)
    }
    return
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;如果标题是有效的, 将返回&lt;code&gt;nil&lt;/code&gt;错误值. 如果标题无效, 函数会输出&amp;rdquo;404 Not Found&amp;rdquo;错误.&lt;/p&gt;

&lt;p&gt;让我们将&lt;code&gt;getTitle&lt;/code&gt;应用到每个处理程序：&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func viewHandler(w http.ResponseWriter, r *http.Request) {
    title, err := getTitle(w, r)
    if err != nil {
        return
    }
    p, err := loadPage(title)
    if err != nil {
        http.Redirect(w, r, &amp;quot;/edit/&amp;quot;+title, http.StatusFound)
        return
    }
    renderTemplate(w, &amp;quot;view&amp;quot;, p)
}

func editHandler(w http.ResponseWriter, r *http.Request) {
    title, err := getTitle(w, r)
    if err != nil {
        return
    }
    p, err := loadPage(title)
    if err != nil {
        p = &amp;amp;Page{Title: title}
    }
    renderTemplate(w, &amp;quot;edit&amp;quot;, p)
}

func saveHandler(w http.ResponseWriter, r *http.Request) {
    title, err := getTitle(w, r)
    if err != nil {
        return
    }
    body := r.FormValue(&amp;quot;body&amp;quot;)
    p := &amp;amp;Page{Title: title, Body: []byte(body)}
    err = p.save()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    http.Redirect(w, r, &amp;quot;/view/&amp;quot;+title, http.StatusFound)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;函数字面值和闭包&#34;&gt;函数字面值和闭包&lt;/h2&gt;

&lt;p&gt;每个处理函数为了增加错误错误引入了很多重复的代码. 如果是否可以将每个处理函数的
错误处理包装到一个函数? Go语言的闭包函数提供的强有力的手段, 刚好可以用在这里.&lt;/p&gt;

&lt;p&gt;第一步, 我们重写每个处理函数, 增加一个标题字符串参数:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func viewHandler(w http.ResponseWriter, r *http.Request, title string)
func editHandler(w http.ResponseWriter, r *http.Request, title string)
func saveHandler(w http.ResponseWriter, r *http.Request, title string)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后, 我们顶一个包装函数, 参数类型和前面定义的处理函数类型一致, 最后返回
&lt;code&gt;http.HandlerFunc&lt;/code&gt;(用于适配&lt;code&gt;http.HandleFunc&lt;/code&gt;的参数类型):&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func makeHandler(fn func (http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // Here we will extract the page title from the Request,
        // and call the provided handler &#39;fn&#39;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里返回的函数就是一个闭包, 因为它引用了在它外部定义的局部变量的值.
在这里情况下, 变量&lt;code&gt;fn&lt;/code&gt;(&lt;code&gt;makeHandler&lt;/code&gt;函数的唯一参数)被闭包函数持有.
&lt;code&gt;fn&lt;/code&gt;变量将对应我们的保存, 编辑 和 查看 的处理函数.&lt;/p&gt;

&lt;p&gt;现在我们可以将&lt;code&gt;getTitle&lt;/code&gt;的代码移到这里(还有一些细节的改动):&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;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)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;makeHandler&lt;/code&gt;返回的是一个持有&lt;code&gt;http.ResponseWriter&lt;/code&gt;和&lt;code&gt;http.Request&lt;/code&gt;参数的闭包函数
(其实就是&lt;code&gt;http.HandlerFunc&lt;/code&gt;类型). 闭包函数提取页面的标题, 并通过&lt;code&gt;TitleValidator&lt;/code&gt;验证
标题是否符合正则表达式. 如果是无效的标题, 那么将使用&lt;code&gt;http.NotFound&lt;/code&gt;输出错误的响应.
如果是有效的标题, 那么&lt;code&gt;fn&lt;/code&gt;处理函数将会被调用.&lt;/p&gt;

&lt;p&gt;现在我们可以在&lt;code&gt;main&lt;/code&gt;函数注册的时候使用&lt;code&gt;makeHandler&lt;/code&gt;包装具体的处理函数:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func main() {
    http.HandleFunc(&amp;quot;/view/&amp;quot;, makeHandler(viewHandler))
    http.HandleFunc(&amp;quot;/edit/&amp;quot;, makeHandler(editHandler))
    http.HandleFunc(&amp;quot;/save/&amp;quot;, makeHandler(saveHandler))
    http.ListenAndServe(&amp;quot;:8080&amp;quot;, nil)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Finally we remove the calls to getTitle from the handler functions, making them much simpler:&lt;/p&gt;

&lt;p&gt;最后我们删除处理函数对&lt;code&gt;getTitle&lt;/code&gt;的调用, 处理代码变得更加简单:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
    p, err := loadPage(title)
    if err != nil {
        http.Redirect(w, r, &amp;quot;/edit/&amp;quot;+title, http.StatusFound)
        return
    }
    renderTemplate(w, &amp;quot;view&amp;quot;, p)
}

func editHandler(w http.ResponseWriter, r *http.Request, title string) {
    p, err := loadPage(title)
    if err != nil {
        p = &amp;amp;Page{Title: title}
    }
    renderTemplate(w, &amp;quot;edit&amp;quot;, p)
}

func saveHandler(w http.ResponseWriter, r *http.Request, title string) {
    body := r.FormValue(&amp;quot;body&amp;quot;)
    p := &amp;amp;Page{Title: title, Body: []byte(body)}
    err := p.save()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    http.Redirect(w, r, &amp;quot;/view/&amp;quot;+title, http.StatusFound)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;看看页面效果&#34;&gt;看看页面效果!&lt;/h2&gt;

&lt;p&gt;&lt;a href=&#34;http://golang.org/doc/articles/wiki/final.go&#34;&gt;点击这里查看最终版本的代码&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;重新编译代码, 并且运行:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ go build wiki.go
$ ./wiki
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;浏览 &lt;code&gt;http://localhost:8080/view/ANewPage&lt;/code&gt; 将会看到编辑页面.
你可以输入一些文字, 点击 &amp;lsquo;save&amp;rsquo; 保存, 然后重新定向到新创建的页面.&lt;/p&gt;

&lt;h2 id=&#34;其他任务&#34;&gt;其他任务&lt;/h2&gt;

&lt;p&gt;还可以根据自己的兴趣选择一些简单的扩展任务:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;保存模板到&lt;code&gt;tmpl/&lt;/code&gt;目录, 保存数据到&lt;code&gt;data/&lt;/code&gt;目录.&lt;/li&gt;
&lt;li&gt;增加一个根目录的处理函数, 重定向到&lt;code&gt;/view/FrontPage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Spruce up the page templates by making them valid HTML and adding some CSS rules.&lt;/li&gt;
&lt;li&gt;完善页面模板, 让它们输出有效的HTML, 并且添加一些CSS规则。&lt;/li&gt;
&lt;li&gt;通过将&lt;code&gt;[PageName]&lt;/code&gt;转换位&lt;code&gt;&amp;lt;a href=&amp;quot;/view/PageName&amp;quot;&amp;gt;PageName&amp;lt;/a&amp;gt;&lt;/code&gt;实现页面之间的链接.
(提示: 可以使用&lt;code&gt;regexp.ReplaceAllFunc&lt;/code&gt;实现该功能)&lt;/li&gt;
&lt;/ul&gt;
</description>
      
    </item>
    
  </channel>
</rss>