使用Go开发Web应用程序
# 简介
本教程涵盖以下内容:
- 使用加载和保存方法创建数据结构
- 使用net/http包构建web应用程序
- 使用html/template包处理html模板
- 使用regexp包验证用户输入
- 使用闭包
前置知识:
- 编程经验
- 了解基本的web技术(HTTP、HTML)
- 一些UNIX/DOS命令行知识
# 入门
目前,您需要有一台FreeBSD、Linux、macOS或Windows机器才能运行Go。我们将使用$来表示命令提示符。
安装Go(请参阅安装说明)。
在GOPATH中为本教程创建一个新目录,并进入该目录:
$mkdir gowiki
$cd gowiki
创建一个名为wiki.go的文件,在您喜欢的编辑器中打开它,然后添加以下行:
package main
import (
"fmt"
"io/ioutil"
)
我们从Go标准库导入fmt和ioutil包。稍后,当我们实现附加功能时,我们将向该导入声明添加更多的包。
# 数据结构
让我们从定义数据结构开始。wiki由一系列相互连接的页面组成,每个页面都有一个标题和一个正文(页面内容)。在这里,我们将Page定义为一个结构,其中有两个字段表示标题和正文。
type Page struct {
Title string
Body []byte
}
类型 []byte 的意思是“一个字节切片”。(有关切片的更多信息,请参阅Slices: usage and internals (opens new window)。)Body元素是[]字节,而不是字符串,因为这是我们将使用的io库所期望的类型,如下所示。
Page结构描述了页面数据将如何存储在内存中。但是持久存储呢?我们可以通过在Page上创建一个保存方法来解决这个问题.
func (p *Page) save() error {
filename := p.Title + ".txt"
return ioutil.WriteFile(filename, p.Body, 0600)
}
此方法的签名写道:“这是一个名为save的方法,它以指向Page的指针p作为接收器。它不接受参数,并返回类型为error的值。”
此方法将把页面的正文保存到一个文本文件中。为了简单起见,我们将使用标题作为文件名。
save方法返回一个错误值,因为这是WriteFile(一个向文件写入字节片的标准库函数)的返回类型。save方法返回错误值,以便在写入文件时出现任何错误时由应用程序进行处理。如果一切顺利,Page.save()将返回nil(指针、接口和其他类型的零值)。
作为WriteFile的第三个参数传递的八进制整数文本0600表示,创建该文件时应仅具有当前用户的读写权限。(有关详细信息,请参阅Unix手册页打开(2)。)
除了保存页面外,我们还需要加载页面:
func loadPage(title string) *Page {
filename := title + ".txt"
body, _ := ioutil.ReadFile(filename)
return &Page{Title: title, Body: body}
}
函数loadPage从title参数构造文件名,将文件内容读取到新的变量体中,并返回一个指针,指向用正确的title和body值构造的Page文本。
函数可以返回多个值。标准库函数io.ReadFile返回[]字节和错误。在loadPage中,错误尚未得到处理;由下划线(_)符号表示的“空白标识符”用于丢弃错误返回值(本质上,不将值赋值)。
但是如果ReadFile遇到错误会发生什么呢?例如,该文件可能不存在。我们不应该忽视这些错误。让我们修改函数以返回*Page和error。
func loadPage(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
}
此函数的调用方现在可以检查第二个参数;如果为零,则它已成功加载页面。如果没有,那么这将是一个可以由调用者处理的错误(有关详细信息,请参阅语言规范)。
在这一点上,我们有一个简单的数据结构和保存到文件和从文件加载的能力。让我们编写一个主要函数来测试我们所写的内容:
编译并执行此代码后,将创建一个名为TestPage.txt的文件,其中包含p1的内容。然后,该文件将被读取到结构p2中,并将其Body元素打印到屏幕上。
您可以这样编译和运行程序:
$ go build wiki.go
$ ./wiki
This is a sample Page.
(如果您使用的是Windows,则必须键入不带“./”的“wiki”才能运行程序。)
wiki.Go完整代码:
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build ignore
package main
import (
"fmt"
"io/ioutil"
)
type Page struct {
Title string
Body []byte
}
func (p *Page) save() error {
filename := p.Title + ".txt"
return ioutil.WriteFile(filename, p.Body, 0600)
}
func loadPage(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
}
func main() {
p1 := &Page{Title: "TestPage", Body: []byte("This is a sample Page.")}
p1.save()
p2, _ := loadPage("TestPage")
fmt.Println(string(p2.Body))
}
# net/http软件包介绍(插叙)
以下是一个简单web服务器的完整工作示例:
// +build ignore
package main
import (
"fmt"
"log"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
}
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
main函数以对http.HandleFunc的调用开始,该调用告诉http包使用处理程序处理对web根(“/”)的所有请求。
然后,它调用http.ListenAndServe,指定它应该侦听任何接口上的端口8080(“:8080”)。(现在不要担心它的第二个参数nil。)这个函数将被阻塞,直到程序终止。
ListenAndServe总是返回一个错误,因为它只有在发生意外错误时才会返回。为了记录这个错误,我们用log.Fatal包装函数调用。
函数处理程序的类型为http.HandlerFunc。它以http.ResponseWriter和http.Request作为参数。
http.ResponseWriter值汇编http服务器的响应;通过写入它,我们将数据发送到HTTP客户端。
http.Request是一种表示客户端http请求的数据结构。r.URL.Path是请求URL的路径组件。尾部[1:]的意思是“创建从第一个字符到最后的Path的子片段。”这将删除路径名称中的前导“/”。
如果您运行此程序并访问URL:http://localhost:8080/monkeys (opens new window) 该程序将呈现一个页面,该页面包含:
该程序将呈现一个页面,该页面包含:
# 使用net/http为wiki页面提供服务
要使用net/http包,必须导入该包:
import (
"fmt"
"io/ioutil"
"log"
"net/http"
)
让我们创建一个处理程序viewHandler,它将允许用户查看wiki页面。它将处理前缀为“/view/”的URL。
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, _ := loadPage(title)
fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
}
再次注意,使用_可以忽略loadPage中的错误返回值。这里这样做是为了简单,通常被认为是不好的做法。我们稍后会处理这个问题。
首先,此函数从请求URL的路径组件r.URL.Path中提取页面标题。使用[len(“/view/”):]对Path进行重新切片,以删除请求路径的前导“/view/”组件。这是因为路径总是以“/view/”开头,这不是页面标题的一部分。
然后,该函数加载页面数据,用一个简单的HTML字符串格式化页面,并将其写入http.ResponseWriter w。
为了使用这个处理程序,我们重写了主函数来初始化http,使用viewHandler来处理路径/view/下的任何请求。
func main() {
http.HandleFunc("/view/", viewHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
我们迄今为止编写的代码:
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build ignore
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
)
type Page struct {
Title string
Body []byte
}
func (p *Page) save() error {
filename := p.Title + ".txt"
return ioutil.WriteFile(filename, p.Body, 0600)
}
func loadPage(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
}
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, _ := loadPage(title)
fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
}
func main() {
http.HandleFunc("/view/", viewHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
让我们创建一些页面数据(作为test.txt),编译我们的代码,并尝试提供wiki页面。
在编辑器中打开test.txt文件,并在其中保存字符串“Hello world”(不带引号)。
go build wiki.go
./wiki
(如果您使用的是Windows,则必须键入不带“./”的“wiki”才能运行程序。)
在运行此web服务器的情况下,访问http://localhost:8080/view/test应该显示一个标题为“test”的页面,其中包含单词“Hello (opens new window) world”。
# 编辑页面
wiki不是没有编辑页面功能的wiki。让我们创建两个新的处理程序:一个名为editHandler的处理程序用于显示“编辑页面”表单,另一个名的saveHandler用于保存通过表单输入的数据。
首先,我们将它们添加到main()中:
func main() {
http.HandleFunc("/view/", viewHandler)
http.HandleFunc("/edit/", editHandler)
http.HandleFunc("/save/", saveHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
函数editHandler加载页面(或者,如果不存在,创建一个空的page结构),并显示一个HTML表单。
func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/edit/"):]
p, err := loadPage(title)
if err != nil {
p = &Page{Title: title}
}
fmt.Fprintf(w, "<h1>Editing %s</h1>"+
"<form action=\"/save/%s\" method=\"POST\">"+
"<textarea name=\"body\">%s</textarea><br>"+
"<input type=\"submit\" value=\"Save\">"+
"</form>",
p.Title, p.Title, p.Body)
}
这个函数可以很好地工作,但所有硬编码的HTML都很难看。当然,还有更好的方法。
# html/template包
html/template包是Go标准库的一部分。我们可以使用html/template将html保存在一个单独的文件中,这样我们就可以在不修改底层Go代码的情况下更改编辑页面的布局。
首先,我们必须将html/template添加到导入列表中。我们也不会再使用fmt了,所以我们必须删除它。
import (
"html/template"
"io/ioutil"
"net/http"
)
让我们创建一个包含HTML表单的模板文件。打开一个名为edit.HTML的新文件,并添加以下行:
<h1>Editing {{.Title}}</h1>
<form action="/save/{{.Title}}" method="POST">
<div><textarea name="body" rows="20" cols="80">{{printf "%s" .Body}}</textarea></div>
<div><input type="submit" value="Save"></div>
</form>
修改editHandler以使用模板,而不是硬编码的HTML:
func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/edit/"):]
p, err := loadPage(title)
if err != nil {
p = &Page{Title: title}
}
t, _ := template.ParseFiles("edit.html")
t.Execute(w, p)
}
函数template.ParseFiles将读取edit.html的内容并返回一个*template.template。
方法t.Execute执行模板,将生成的HTML写入http.ResponseWriter。.Title和.Body虚线标识符指的是p.Title和p.Body。
模板指令用双大括号括起来。printf“%s”.Body指令是一个函数调用,它将.Body输出为字符串,而不是字节流,这与对fmt.printf的调用相同。html/template包有助于确保模板操作只生成安全且外观正确的html。例如,它会自动转义任何大于号(>)的符号,将其替换为>;,以确保用户数据不会损坏表单HTML。
既然我们现在正在使用模板,那么让我们为我们的viewHandler创建一个名为view.html的模板:
<h1>{{.Title}}</h1>
<p>[<a href="/edit/{{.Title}}">edit</a>]</p>
<div>{{printf "%s" .Body}}</div>
相应地修改viewHandler:
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, _ := loadPage(title)
t, _ := template.ParseFiles("view.html")
t.Execute(w, p)
}
请注意,我们在两个处理程序中使用了几乎完全相同的模板代码。让我们通过将模板代码移动到它自己的函数来消除这种重复:
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
t, _ := template.ParseFiles(tmpl + ".html")
t.Execute(w, p)
}
并修改处理程序以使用该函数:
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, _ := loadPage(title)
renderTemplate(w, "view", p)
}
func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/edit/"):]
p, err := loadPage(title)
if err != nil {
p = &Page{Title: title}
}
renderTemplate(w, "edit", p)
}
如果我们在main中注释掉未实现的保存处理程序的注册,我们可以再次构建和测试我们的程序。 看我们迄今为止编写的代码。
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build ignore
package main
import (
"html/template"
"io/ioutil"
"log"
"net/http"
)
type Page struct {
Title string
Body []byte
}
func (p *Page) save() error {
filename := p.Title + ".txt"
return ioutil.WriteFile(filename, p.Body, 0600)
}
func loadPage(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
}
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
t, _ := template.ParseFiles(tmpl + ".html")
t.Execute(w, p)
}
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, _ := loadPage(title)
renderTemplate(w, "view", p)
}
func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/edit/"):]
p, err := loadPage(title)
if err != nil {
p = &Page{Title: title}
}
renderTemplate(w, "edit", p)
}
func main() {
http.HandleFunc("/view/", viewHandler)
http.HandleFunc("/edit/", editHandler)
//http.HandleFunc("/save/", saveHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
# 处理不存在的页面
如果您访问/view/APageThatDesntExist会怎样?您将看到一个包含HTML的页面。这是因为它忽略了loadPage的错误返回值,并继续尝试在没有数据的情况下填写模板。相反,如果请求的页面不存在,它应该将客户端重定向到编辑页面,以便创建内容:
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, err := loadPage(title)
if err != nil {
http.Redirect(w, r, "/edit/"+title, http.StatusFound)
return
}
renderTemplate(w, "view", p)
}
http.RRedirect函数将http.StatusFind(302)的http状态代码和Location标头添加到http响应中。
# 保存页面
函数saveHandler将处理位于编辑页面上的表单的提交。在main中取消注释相关行之后,让我们实现处理程序:
func saveHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/save/"):]
body := r.FormValue("body")
p := &Page{Title: title, Body: []byte(body)}
p.save()
http.Redirect(w, r, "/view/"+title, http.StatusFound)
}
页面标题(在URL中提供)和表单的唯一字段Body存储在一个新页面中。然后调用save()方法将数据写入文件,并将客户端重定向到/view/page。
FormValue返回的值的类型为字符串。我们必须将该值转换为 []byte ,然后才能将其放入Page结构中。我们使用 []byte(body)来执行转换。
# 错误处理
在我们的程序中,有几个地方的错误被忽略了。这是一种糟糕的做法,尤其是因为当错误发生时,程序会有意外的行为。更好的解决方案是处理错误并向用户返回错误消息。这样,如果出现问题,服务器将按照我们想要的方式运行,并可以通知用户。
首先,让我们处理renderTemplate中的错误:
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
t, err := template.ParseFiles(tmpl + ".html")
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)
}
}
http.Error函数发送指定的http响应代码(在本例中为“内部服务器错误”)和错误消息。把它放在一个单独的功能中的决定已经取得了成效。
现在让我们修复saveHandler:
func saveHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/save/"):]
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)
}
在p.save()过程中发生的任何错误都将报告给用户。
# 模板缓存
这段代码效率低下:每次渲染页面时,renderTemplate都会调用ParseFiles。更好的方法是在程序初始化时调用ParseFiles一次,将所有模板解析为一个*Template。然后我们可以使用ExecuteTemplate方法来渲染特定的模板。
首先,我们创建一个名为templates的全局变量,并使用ParseFiles对其进行初始化。
var templates = template.Must(template.ParseFiles("edit.html", "view.html"))
函数template.Must是一个方便的包装器,当传递非nil错误值时会死机,否则会原封不动地返回*template。恐慌在这里是恰当的;如果模板无法加载,那么唯一明智的做法就是退出程序。
ParseFiles函数采用任意数量的字符串参数来标识模板文件,并将这些文件解析为以基本文件名命名的模板。如果我们要在程序中添加更多的模板,我们会将它们的名称添加到ParseFiles调用的参数中。
然后,我们修改renderTemplate函数以调用templates.ExecuteTemplate方法,该方法具有相应模板的名称:
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
err := templates.ExecuteTemplate(w, tmpl+".html", p)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
请注意,模板名称是模板文件名,因此我们必须将“.html”附加到tmpl参数。
# 验证
正如您可能已经观察到的,这个程序有一个严重的安全缺陷:用户可以在服务器上提供要读/写的任意路径。为了缓解这种情况,我们可以编写一个函数,用正则表达式验证标题。
首先,将“regexp”添加到导入列表中。然后,我们可以创建一个全局变量来存储验证表达式:
var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")
函数regexp.MustCompile将解析和编译正则表达式,并返回regexp.regexp。MustCompile与compile的不同之处在于,如果表达式编译失败,它将死机,而compile将返回一个错误作为第二个参数。
现在,让我们编写一个函数,使用validPath表达式验证路径并提取页面标题:
func getTitle(w http.ResponseWriter, r *http.Request) (string, error) {
m := validPath.FindStringSubmatch(r.URL.Path)
if m == nil {
http.NotFound(w, r)
return "", errors.New("invalid Page Title")
}
return m[2], nil // The title is the second subexpression.
}
如果标题是有效的,那么它将与一个nil错误值一起返回。如果标题无效,函数将向HTTP连接写入一个“404未找到”错误,并向处理程序返回一个错误。要创建新的错误,我们必须导入错误包。
让我们在每个处理程序中调用getTitle:
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, "/edit/"+title, http.StatusFound)
return
}
renderTemplate(w, "view", 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 = &Page{Title: title}
}
renderTemplate(w, "edit", p)
}
func saveHandler(w http.ResponseWriter, r *http.Request) {
title, err := getTitle(w, r)
if err != nil {
return
}
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)
}
# 介绍函数文字和闭包
在每个处理程序中捕捉错误条件会引入大量重复的代码。如果我们可以将每个处理程序封装在一个函数中进行验证和错误检查,会怎么样?Go的函数文字提供了一种强大的功能抽象方法,可以在这里为我们提供帮助。
首先,我们重写每个处理程序的函数定义,以接受一个标题字符串:
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)
现在,让我们定义一个包装函数,它接受上面类型的函数,并返回http.HandlerFunc类型的函数(适合传递给函数http.HandleFunc):
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 'fn'
}
}
返回的函数被称为闭包,因为它包含了在其外部定义的值。在这种情况下,变量fn(makeHandler的单个参数)被闭包包围。变量fn将是我们的保存、编辑或视图处理程序之一。
现在我们可以从getTitle中获取代码并在这里使用它(经过一些小的修改):
func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
m := validPath.FindStringSubmatch(r.URL.Path)
if m == nil {
http.NotFound(w, r)
return
}
fn(w, r, m[2])
}
}
makeHandler返回的闭包是一个使用http.ResponseWriter和http.Request(换句话说,http.HandlerFunc)的函数。闭包从请求路径中提取标题,并使用validPath regexp进行验证。如果标题无效,将使用http.NotFound函数向ResponseWriter写入一个错误。如果标题有效,则将调用包含的处理程序函数fn,并将ResponseWriter、Request和title作为参数。
现在,我们可以在使用http包注册处理程序函数之前,将处理程序函数主要包装为makeHandler:
func main() {
http.HandleFunc("/view/", makeHandler(viewHandler))
http.HandleFunc("/edit/", makeHandler(editHandler))
http.HandleFunc("/save/", makeHandler(saveHandler))
log.Fatal(http.ListenAndServe(":8080", nil))
}
最后,我们从处理程序函数中删除了对getTitle的调用,使它们更加简单:
func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
p, err := loadPage(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 := loadPage(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)
}
# 试试看
查看最终代码列表:
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build ignore
package main
import (
"html/template"
"io/ioutil"
"log"
"net/http"
"regexp"
)
type Page struct {
Title string
Body []byte
}
func (p *Page) save() error {
filename := p.Title + ".txt"
return ioutil.WriteFile(filename, p.Body, 0600)
}
func loadPage(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
}
func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
p, err := loadPage(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 := loadPage(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)
}
var templates = template.Must(template.ParseFiles("edit.html", "view.html"))
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
err := templates.ExecuteTemplate(w, tmpl+".html", p)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")
func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
m := validPath.FindStringSubmatch(r.URL.Path)
if m == nil {
http.NotFound(w, r)
return
}
fn(w, r, m[2])
}
}
func main() {
http.HandleFunc("/view/", makeHandler(viewHandler))
http.HandleFunc("/edit/", makeHandler(editHandler))
http.HandleFunc("/save/", makeHandler(saveHandler))
log.Fatal(http.ListenAndServe(":8080", nil))
}
重新编译代码,然后运行应用程序:
go build wiki.go
./wiki
浏览 http://localhost:8080/view/ANewPage (opens new window) 应该会向您显示页面编辑表单。然后您应该能够输入一些文本,单击“保存”,然后被重定向到新创建的页面。>
# 其他任务
以下是一些你可能想自己解决的简单任务:
- 将模板存储在tmpl/中,将页面数据存储在data/中。
- 添加一个处理程序,使网站根重定向到/viewFrontPage。
- 通过使页面模板成为有效的HTML并添加一些CSS规则来扩充页面模板。
- 通过将[PageName]的实例转换为
<a href=“/view/PageName”>页面名称</a>
。(提示:您可以使用regexp.ReplaceAllFunc来执行此操作)