Writing Web Applications
编写网络应用程序
Introduction 介绍
Covered in this tutorial:
在本教程中涵盖的内容:
- Creating a data structure with load and save methods
创建一个具有加载和保存方法的数据结构 - Using the
net/http
package to build web applications
使用net/http
包构建 Web 应用程序 - Using the
html/template
package to process HTML templates
使用html/template
包来处理 HTML 模板 - Using the
regexp
package to validate user input
使用regexp
包验证用户输入 - Using closures 使用闭包
Assumed knowledge: 假定知识:
- Programming experience 编程经验
- Understanding of basic web technologies (HTTP, HTML)
理解基本的网络技术(HTTP,HTML) - Some UNIX/DOS command-line knowledge
一些 UNIX/DOS 命令行知识
Getting Started 开始使用
At present, you need to have a FreeBSD, Linux, macOS, or Windows machine to run Go.
We will use $
to represent the command prompt.
目前,您需要拥有一个 FreeBSD、Linux、macOS 或 Windows 机器来运行 Go。我们将使用 $
来表示命令提示符。
Install Go (see the Installation Instructions).
安装 Go(请参阅安装说明)。
Make a new directory for this tutorial inside your GOPATH
and cd to it:
在 GOPATH
中为本教程创建一个新目录,并切换到该目录:
$ mkdir gowiki $ cd gowiki
Create a file named wiki.go
, open it in your favorite editor, and
add the following lines:
创建一个名为 wiki.go
的文件,在您喜欢的编辑器中打开它,并添加以下行:
package main import ( "fmt" "os" )
We import the fmt
and os
packages from the Go
standard library. Later, as we implement additional functionality, we will
add more packages to this import
declaration.
我们从 Go 标准库中导入 fmt
和 os
包。随后,当我们实现额外功能时,我们将向这个 import
声明中添加更多包。
Data Structures 数据结构
Let's start by defining the data structures. A wiki consists of a series of
interconnected pages, each of which has a title and a body (the page content).
Here, we define Page
as a struct with two fields representing
the title and body.
让我们从定义数据结构开始。维基由一系列相互连接的页面组成,每个页面都有一个标题和一个正文(页面内容)。在这里,我们将 Page
定义为一个具有代表标题和正文的两个字段的结构。
type Page struct { Title string Body []byte }
The type []byte
means "a byte
slice".
(See Slices: usage and
internals for more on slices.)
The Body
element is a []byte
rather than
string
because that is the type expected by the io
libraries we will use, as you'll see below.
类型 []byte
表示“ byte
切片”。(有关切片的用法和内部信息,请参阅切片:用法和内部信息。) Body
元素是 []byte
而不是 string
,因为这是我们将要使用的 io
库期望的类型,如下所示。
The Page
struct describes how page data will be stored in memory.
But what about persistent storage? We can address that by creating a
save
method on Page
:
Page
结构描述了页面数据将如何存储在内存中。但是持久性存储呢?我们可以通过在 Page
上创建一个 save
方法来解决这个问题:
func (p *Page) save() error { filename := p.Title + ".txt" return os.WriteFile(filename, p.Body, 0600) }
This method's signature reads: "This is a method named save
that
takes as its receiver p
, a pointer to Page
. It takes
no parameters, and returns a value of type error
."
该方法的签名如下:“这是一个名为 save
的方法,其接收者为 p
,一个指向 Page
的指针。它不接受任何参数,并返回类型为 error
的值。”
This method will save the Page
's Body
to a text
file. For simplicity, we will use the Title
as the file name.
该方法将 Page
的 Body
保存到文本文件中。为简单起见,我们将使用 Title
作为文件名。
The save
method returns an error
value because
that is the return type of WriteFile
(a standard library function
that writes a byte slice to a file). The save
method returns the
error value, to let the application handle it should anything go wrong while
writing the file. If all goes well, Page.save()
will return
nil
(the zero-value for pointers, interfaces, and some other
types).
save
方法返回一个 error
值,因为这是 WriteFile
的返回类型(一个标准库函数,用于将字节切片写入文件)。 save
方法返回错误值,以便让应用程序在写文件时出现问题时处理它。如果一切顺利, Page.save()
将返回 nil
(指针、接口和其他一些类型的零值)。
The octal integer literal 0600
, passed as the third parameter to
WriteFile
, indicates that the file should be created with
read-write permissions for the current user only. (See the Unix man page
open(2)
for details.)
八进制整数字面值 0600
,作为传递给 WriteFile
的第三个参数,表示文件应该仅为当前用户创建并具有读写权限。(有关详细信息,请参阅 Unix man 页面 open(2)
。)
In addition to saving pages, we will want to load pages, too:
除了保存页面,我们还想要加载页面:
func loadPage(title string) *Page { filename := title + ".txt" body, _ := os.ReadFile(filename) return &Page{Title: title, Body: body} }
The function loadPage
constructs the file name from the title
parameter, reads the file's contents into a new variable body
, and
returns a pointer to a Page
literal constructed with the proper
title and body values.
函数 loadPage
从标题参数构造文件名,将文件内容读入新变量 body
,并返回指向使用正确标题和正文值构造的 Page
文字的指针。
Functions can return multiple values. The standard library function
os.ReadFile
returns []byte
and error
.
In loadPage
, error isn't being handled yet; the "blank identifier"
represented by the underscore (_
) symbol is used to throw away the
error return value (in essence, assigning the value to nothing).
函数可以返回多个值。标准库函数 os.ReadFile
返回 []byte
和 error
。在 loadPage
中,错误尚未被处理;下划线( _
)符号代表的“空白标识符”被用来丢弃错误返回值(实质上将该值赋予空)。
But what happens if ReadFile
encounters an error? For example,
the file might not exist. We should not ignore such errors. Let's modify the
function to return *Page
and error
.
但是如果 ReadFile
遇到错误会发生什么呢?例如,文件可能不存在。我们不应忽略这样的错误。让我们修改函数以返回 *Page
和 error
。
func loadPage(title string) (*Page, error) { filename := title + ".txt" body, err := os.ReadFile(filename) if err != nil { return nil, err } return &Page{Title: title, Body: body}, nil }
Callers of this function can now check the second parameter; if it is
nil
then it has successfully loaded a Page. If not, it will be an
error
that can be handled by the caller (see the
language specification for details).
调用此函数的用户现在可以检查第二个参数;如果它是 nil
,那么它已成功加载页面。如果不是,它将是一个 error
,可以由调用者处理(有关详细信息,请参阅语言规范)。
At this point we have a simple data structure and the ability to save to and
load from a file. Let's write a main
function to test what we've
written:
到目前为止,我们有一个简单的数据结构和保存到文件以及从文件加载的能力。让我们编写一个 main
函数来测试我们所写的内容:
func main() { p1 := &Page{Title: "TestPage", Body: []byte("This is a sample Page.")} p1.save() p2, _ := loadPage("TestPage") fmt.Println(string(p2.Body)) }
After compiling and executing this code, a file named TestPage.txt
would be created, containing the contents of p1
. The file would
then be read into the struct p2
, and its Body
element
printed to the screen.
编译和执行此代码后,将创建一个名为 TestPage.txt
的文件,其中包含 p1
的内容。然后将该文件读入结构 p2
,并将其 Body
元素打印到屏幕上。
You can compile and run the program like this:
您可以这样编译和运行程序:
$ go build wiki.go $ ./wiki This is a sample Page.
(If you're using Windows you must type "wiki
" without the
"./
" to run the program.)
(如果您使用 Windows,必须输入“ wiki
”而不带“ ./
”来运行程序。)
Click here to view the code we've written so far.
点击这里查看我们迄今为止编写的代码。
Introducing the net/http
package (an interlude)
介绍 net/http
包(插曲)
Here's a full working example of a simple web server:
这是一个简单 Web 服务器的完整工作示例:
//go: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))
}
The main
function begins with a call to
http.HandleFunc
, which tells the http
package to
handle all requests to the web root ("/"
) with
handler
.
main
函数从调用 http.HandleFunc
开始,该函数告诉 http
包处理所有对 Web 根目录( "/"
)的请求,使用 handler
。
It then calls http.ListenAndServe
, specifying that it should
listen on port 8080 on any interface (":8080"
). (Don't
worry about its second parameter, nil
, for now.)
This function will block until the program is terminated.
然后调用 http.ListenAndServe
,指定它应该在任何接口( ":8080"
)上监听端口 8080。(现在不用担心它的第二个参数 nil
。)此函数将阻塞,直到程序终止。
ListenAndServe
always returns an error, since it only returns when an
unexpected error occurs.
In order to log that error we wrap the function call with log.Fatal
.
ListenAndServe
总是返回错误,因为它只在发生意外错误时返回。为了记录该错误,我们用 log.Fatal
包装函数调用。
The function handler
is of the type http.HandlerFunc
.
It takes an http.ResponseWriter
and an http.Request
as
its arguments.
函数 handler
的类型为 http.HandlerFunc
。它以 http.ResponseWriter
和 http.Request
作为参数。
An http.ResponseWriter
value assembles the HTTP server's response; by writing
to it, we send data to the HTTP client.
一个 http.ResponseWriter
值组装 HTTP 服务器的响应;通过向其写入数据,我们将数据发送给 HTTP 客户端。
An http.Request
is a data structure that represents the client
HTTP request. r.URL.Path
is the path component
of the request URL. The trailing [1:]
means
"create a sub-slice of Path
from the 1st character to the end."
This drops the leading "/" from the path name.
一个 http.Request
是表示客户端 HTTP 请求的数据结构。 r.URL.Path
是请求 URL 的路径组件。尾部的 [1:]
表示“从第一个字符到结尾创建 Path
的子切片”。这会删除路径名称中的前导“/”。
If you run this program and access the URL:
如果您运行此程序并访问该 URL:
http://localhost:8080/monkeys
the program would present a page containing:
该程序将呈现一个包含:
Hi there, I love monkeys!
Using net/http
to serve wiki pages
使用 net/http
来提供维基页面
To use the net/http
package, it must be imported:
要使用 net/http
包,必须导入:
import ( "fmt" "os" "log" "net/http" )
Let's create a handler, viewHandler
that will allow users to
view a wiki page. It will handle URLs prefixed with "/view/".
让我们创建一个处理程序, viewHandler
,允许用户查看维基页面。它将处理以“/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) }
Again, note the use of _
to ignore the error
return value from loadPage
. This is done here for simplicity
and generally considered bad practice. We will attend to this later.
再次注意使用 _
来忽略 loadPage
返回值。这样做是为了简单起见,通常被认为是不良实践。我们稍后会处理这个问题。
First, this function extracts the page title from r.URL.Path
,
the path component of the request URL.
The Path
is re-sliced with [len("/view/"):]
to drop
the leading "/view/"
component of the request path.
This is because the path will invariably begin with "/view/"
,
which is not part of the page's title.
首先,此函数从请求 URL 的路径组件 r.URL.Path
中提取页面标题。 Path
使用 [len("/view/"):]
重新切片,以删除请求路径的前导 "/view/"
组件。这是因为路径将不可避免地以 "/view/"
开头,而 "/view/"
不是页面标题的一部分。
The function then loads the page data, formats the page with a string of simple
HTML, and writes it to w
, the http.ResponseWriter
.
该函数然后加载页面数据,使用一串简单的 HTML 格式化页面,并将其写入 w
, http.ResponseWriter
。
To use this handler, we rewrite our main
function to
initialize http
using the viewHandler
to handle
any requests under the path /view/
.
为了使用这个处理程序,我们重写我们的 main
函数来初始化 http
,使用 viewHandler
来处理路径 /view/
下的任何请求。
func main() { http.HandleFunc("/view/", viewHandler) log.Fatal(http.ListenAndServe(":8080", nil)) }
Click here to view the code we've written so far.
点击这里查看我们迄今为止编写的代码。
Let's create some page data (as test.txt
), compile our code, and
try serving a wiki page.
让我们创建一些页面数据(如 test.txt
),编译我们的代码,然后尝试提供维基页面。
Open test.txt
file in your editor, and save the string "Hello world" (without quotes)
in it.
在您的编辑器中打开 test.txt
文件,并将字符串“Hello world”(不带引号)保存在其中。
$ go build wiki.go $ ./wiki
(If you're using Windows you must type "wiki
" without the
"./
" to run the program.)
(如果您使用 Windows,必须输入“ wiki
”而不带“ ./
”来运行程序。)
With this web server running, a visit to http://localhost:8080/view/test
should show a page titled "test" containing the words "Hello world".
使用此 Web 服务器运行,访问 http://localhost:8080/view/test
应该显示一个名为“测试”的页面,其中包含“Hello world”一词。
Editing Pages 编辑页面
A wiki is not a wiki without the ability to edit pages. Let's create two new
handlers: one named editHandler
to display an 'edit page' form,
and the other named saveHandler
to save the data entered via the
form.
一个维基如果没有编辑页面的能力,那就不是维基。让我们创建两个新的处理程序:一个命名为 editHandler
用于显示“编辑页面”表单,另一个命名为 saveHandler
用于保存通过表单输入的数据。
First, we add them to main()
:
首先,我们将它们添加到 main()
:
func main() { http.HandleFunc("/view/", viewHandler) http.HandleFunc("/edit/", editHandler) http.HandleFunc("/save/", saveHandler) log.Fatal(http.ListenAndServe(":8080", nil)) }
The function editHandler
loads the page
(or, if it doesn't exist, create an empty Page
struct),
and displays an HTML form.
函数 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) }
This function will work fine, but all that hard-coded HTML is ugly.
Of course, there is a better way.
这个功能会正常工作,但所有那些硬编码的 HTML 看起来很丑。当然,有更好的方法。
The html/template
package html/template
包
The html/template
package is part of the Go standard library.
We can use html/template
to keep the HTML in a separate file,
allowing us to change the layout of our edit page without modifying the
underlying Go code.
html/template
包是 Go 标准库的一部分。我们可以使用 html/template
将 HTML 保存在单独的文件中,这样我们就可以在不修改基础 Go 代码的情况下更改编辑页面的布局。
First, we must add html/template
to the list of imports. We
also won't be using fmt
anymore, so we have to remove that.
首先,我们必须将 html/template
添加到导入列表中。我们也不再使用 fmt
,所以我们必须将其移除。
import ( "html/template" "os" "net/http" )
Let's create a template file containing the HTML form.
Open a new file named edit.html
, and add the following lines:
让我们创建一个包含 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>
Modify editHandler
to use the template, instead of the hard-coded
HTML:
将 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) }
The function template.ParseFiles
will read the contents of
edit.html
and return a *template.Template
.
函数 template.ParseFiles
将读取 edit.html
的内容并返回 *template.Template
。
The method t.Execute
executes the template, writing the
generated HTML to the http.ResponseWriter
.
The .Title
and .Body
dotted identifiers refer to
p.Title
and p.Body
.
方法 t.Execute
执行模板,将生成的 HTML 写入 http.ResponseWriter
。 .Title
和 .Body
点标识符指的是 p.Title
和 p.Body
。
Template directives are enclosed in double curly braces.
The printf "%s" .Body
instruction is a function call
that outputs .Body
as a string instead of a stream of bytes,
the same as a call to fmt.Printf
.
The html/template
package helps guarantee that only safe and
correct-looking HTML is generated by template actions. For instance, it
automatically escapes any greater than sign (>
), replacing it
with >
, to make sure user data does not corrupt the form
HTML.
模板指令用双花括号括起来。 printf "%s" .Body
指令是一个函数调用,将 .Body
输出为字符串,而不是字节流,与调用 fmt.Printf
相同。 html/template
包有助于确保模板操作生成的 HTML 是安全且正确的外观。例如,它会自动转义任何大于号( >
),将其替换为 >
,以确保用户数据不会破坏表单 HTML。
Since we're working with templates now, let's create a template for our
viewHandler
called view.html
:
由于我们现在正在使用模板,让我们为我们的 viewHandler
创建一个名为 view.html
的模板:
<h1>{{.Title}}</h1> <p>[<a href="/edit/{{.Title}}">edit</a>]</p> <div>{{printf "%s" .Body}}</div>
Modify viewHandler
accordingly:
相应地修改 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) }
Notice that we've used almost exactly the same templating code in both
handlers. Let's remove this duplication by moving the templating code
to its own function:
请注意,我们在两个处理程序中几乎完全使用了相同的模板代码。让我们通过将模板代码移动到自己的函数中来消除这种重复:
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) { t, _ := template.ParseFiles(tmpl + ".html") t.Execute(w, p) }
And modify the handlers to use that function:
并修改处理程序以使用该函数:
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) }
If we comment out the registration of our unimplemented save handler in
main
, we can once again build and test our program.
Click here to view the code we've written so far.
如果我们注释掉在 main
中注册未实现的保存处理程序,我们可以再次构建和测试我们的程序。单击此处查看我们迄今为止编写的代码。
Handling non-existent pages
处理不存在的页面
What if you visit
/view/APageThatDoesntExist
? You'll see a page containing
HTML. This is because it ignores the error return value from
loadPage
and continues to try and fill out the template
with no data. Instead, if the requested Page doesn't exist, it should
redirect the client to the edit Page so the content may be created:
如果您访问 /view/APageThatDoesntExist
会发生什么?您将看到一个包含 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) }
The http.Redirect
function adds an HTTP status code of
http.StatusFound
(302) and a Location
header to the HTTP response.
http.Redirect
函数将 HTTP 响应添加一个状态码 http.StatusFound
(302)和一个 Location
头。
Saving Pages 保存页面
The function saveHandler
will handle the submission of forms
located on the edit pages. After uncommenting the related line in
main
, let's implement the handler:
函数 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) }
The page title (provided in the URL) and the form's only field,
Body
, are stored in a new Page
.
The save()
method is then called to write the data to a file,
and the client is redirected to the /view/
page.
页面标题(在 URL 中提供)和表单的唯一字段, Body
,存储在新的 Page
中。然后调用 save()
方法将数据写入文件,并将客户端重定向到 /view/
页面。
The value returned by FormValue
is of type string
.
We must convert that value to []byte
before it will fit into
the Page
struct. We use []byte(body)
to perform
the conversion.
FormValue
返回的值是 string
类型的。在将该值放入 Page
结构之前,我们必须将其转换为 []byte
。我们使用 []byte(body)
来执行转换。
Error handling 错误处理
There are several places in our program where errors are being ignored. This
is bad practice, not least because when an error does occur the program will
have unintended behavior. A better solution is to handle the errors and return
an error message to the user. That way if something does go wrong, the server
will function exactly how we want and the user can be notified.
我们的程序中有几个地方忽略了错误。这是一个不好的做法,尤其是当发生错误时,程序会产生意外行为。更好的解决方案是处理错误并向用户返回错误消息。这样,如果出现问题,服务器将按照我们的期望正常运行,并通知用户。
First, let's handle the errors in renderTemplate
:
首先,让我们处理 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) } }
The http.Error
function sends a specified HTTP response code
(in this case "Internal Server Error") and error message.
Already the decision to put this in a separate function is paying off.
http.Error
函数发送指定的 HTTP 响应代码(在本例中为“内部服务器错误”)和错误消息。已经决定将此放入单独的函数中是值得的。
Now let's fix up saveHandler
:
现在让我们修复 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) }
Any errors that occur during p.save()
will be reported
to the user.
任何在 p.save()
期间发生的错误都将报告给用户。
Template caching 模板缓存
There is an inefficiency in this code: renderTemplate
calls
ParseFiles
every time a page is rendered.
A better approach would be to call ParseFiles
once at program
initialization, parsing all templates into a single *Template
.
Then we can use the
ExecuteTemplate
method to render a specific template.
这段代码存在效率低下的问题:每次渲染页面时都会调用 ParseFiles
。更好的方法是在程序初始化时调用 ParseFiles
一次,将所有模板解析为单个 *Template
。然后我们可以使用 ExecuteTemplate
方法来渲染特定模板。
First we create a global variable named templates
, and initialize
it with ParseFiles
.
首先,我们创建一个名为 templates
的全局变量,并用 ParseFiles
初始化它。
var templates = template.Must(template.ParseFiles("edit.html", "view.html"))
The function template.Must
is a convenience wrapper that panics
when passed a non-nil error
value, and otherwise returns the
*Template
unaltered. A panic is appropriate here; if the templates
can't be loaded the only sensible thing to do is exit the program.
函数 template.Must
是一个方便的包装器,当传递一个非 nil 的 error
值时会引发 panic,否则返回未更改的 *Template
。在这里引发 panic 是合适的;如果模板无法加载,唯一明智的做法就是退出程序。
The ParseFiles
function takes any number of string arguments that
identify our template files, and parses those files into templates that are
named after the base file name. If we were to add more templates to our
program, we would add their names to the ParseFiles
call's
arguments.
ParseFiles
函数接受任意数量的字符串参数,这些参数标识我们的模板文件,并将这些文件解析为以基本文件名命名的模板。如果我们要向程序添加更多模板,我们将把它们的名称添加到 ParseFiles
调用的参数中。
We then modify the renderTemplate
function to call the
templates.ExecuteTemplate
method with the name of the appropriate
template:
我们然后修改 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) } }
Note that the template name is the template file name, so we must
append ".html"
to the tmpl
argument.
请注意,模板名称是模板文件名,因此我们必须将 tmpl
参数附加到 ".html"
。
Validation 验证
As you may have observed, this program has a serious security flaw: a user
can supply an arbitrary path to be read/written on the server. To mitigate
this, we can write a function to validate the title with a regular expression.
正如您可能已经观察到的那样,该程序存在严重的安全漏洞:用户可以提供一个任意路径来在服务器上进行读取/写入。为了减轻这种情况,我们可以编写一个函数来使用正则表达式验证标题。
First, add "regexp"
to the import
list.
Then we can create a global variable to store our validation
expression:
首先,将 "regexp"
添加到 import
列表中。然后,我们可以创建一个全局变量来存储我们的验证表达式:
var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")
The function regexp.MustCompile
will parse and compile the
regular expression, and return a regexp.Regexp
.
MustCompile
is distinct from Compile
in that it will
panic if the expression compilation fails, while Compile
returns
an error
as a second parameter.
函数 regexp.MustCompile
将解析和编译正则表达式,并返回 regexp.Regexp
。 MustCompile
与 Compile
不同之处在于,如果表达式编译失败,它会引发恐慌,而 Compile
会将 error
作为第二个参数返回。
Now, let's write a function that uses the validPath
expression to validate path and extract the page title:
现在,让我们编写一个函数,该函数使用 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.
}
If the title is valid, it will be returned along with a nil
error value. If the title is invalid, the function will write a
"404 Not Found" error to the HTTP connection, and return an error to the
handler. To create a new error, we have to import the errors
package.
如果标题有效,则将其与 nil
错误值一起返回。如果标题无效,则函数将在 HTTP 连接中写入“404 Not Found”错误,并将错误返回给处理程序。要创建新错误,我们必须导入 errors
包。
Let's put a call to getTitle
in each of the handlers:
让我们在每个处理程序中都给 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) }
Introducing Function Literals and Closures
引入函数字面量和闭包
Catching the error condition in each handler introduces a lot of repeated code.
What if we could wrap each of the handlers in a function that does this
validation and error checking? Go's
function
literals provide a powerful means of abstracting functionality
that can help us here.
在每个处理程序中捕获错误条件会引入大量重复的代码。如果我们能够将每个处理程序包装在一个执行此验证和错误检查的函数中会怎样?Go 的函数文字提供了一种强大的抽象功能的方式,可以帮助我们在这里。
First, we re-write the function definition of each of the handlers to accept
a title string:
首先,我们重新编写每个处理程序的函数定义,以接受一个标题字符串:
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)
Now let's define a wrapper function that takes a function of the above
type, and returns a function of type http.HandlerFunc
(suitable to be passed to the function http.HandleFunc
):
现在让我们定义一个包装函数,该函数接受上述类型的函数,并返回一个类型为 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' } }
The returned function is called a closure because it encloses values defined
outside of it. In this case, the variable fn
(the single argument
to makeHandler
) is enclosed by the closure. The variable
fn
will be one of our save, edit, or view handlers.
返回的函数被称为闭包,因为它封装了在其外部定义的值。在这种情况下,变量 fn
(传递给 makeHandler
的单个参数)被闭包封装。变量 fn
将是我们的保存、编辑或查看处理程序之一。
Now we can take the code from getTitle
and use it here
(with some minor modifications):
现在我们可以从 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]) } }
The closure returned by makeHandler
is a function that takes
an http.ResponseWriter
and http.Request
(in other
words, an http.HandlerFunc
).
The closure extracts the title
from the request path, and
validates it with the validPath
regexp. If the
title
is invalid, an error will be written to the
ResponseWriter
using the http.NotFound
function.
If the title
is valid, the enclosed handler function
fn
will be called with the ResponseWriter
,
Request
, and title
as arguments.
makeHandler
返回的闭包是一个接受 http.ResponseWriter
和 http.Request
(换句话说,一个 http.HandlerFunc
)的函数。 闭包从请求路径中提取 title
,并使用 validPath
正则表达式进行验证。 如果 title
无效,则将使用 http.NotFound
函数将错误写入 ResponseWriter
。 如果 title
有效,则将使用 ResponseWriter
, Request
和 title
作为参数调用封闭的处理程序函数 fn
。
Now we can wrap the handler functions with makeHandler
in
main
, before they are registered with the http
package:
现在我们可以在将处理程序函数与 main
包装在 http
之前,将它们注册到 http
包中:
func main() { http.HandleFunc("/view/", makeHandler(viewHandler)) http.HandleFunc("/edit/", makeHandler(editHandler)) http.HandleFunc("/save/", makeHandler(saveHandler)) log.Fatal(http.ListenAndServe(":8080", nil)) }
Finally we remove the calls to getTitle
from the handler functions,
making them much simpler:
最后,我们从处理程序函数中删除对 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) }
Try it out! 试一试!
Click here to view the final code listing.
点击这里查看最终的代码清单。
Recompile the code, and run the app:
重新编译代码,然后运行应用程序:
$ go build wiki.go $ ./wiki
Visiting http://localhost:8080/view/ANewPage
should present you with the page edit form. You should then be able to
enter some text, click 'Save', and be redirected to the newly created page.
访问 http://localhost:8080/view/ANewPage 应该呈现给您页面编辑表单。然后您应该能够输入一些文本,点击“保存”,然后被重定向到新创建的页面。
Other tasks 其他任务
Here are some simple tasks you might want to tackle on your own:
这里有一些简单的任务,您可能想要自己解决:
- Store templates in
tmpl/
and page data indata/
.
将模板存储在tmpl/
中,将页面数据存储在data/
中。 - Add a handler to make the web root redirect to
/view/FrontPage
.
将处理程序添加到使 Web 根目录重定向到/view/FrontPage
。 - Spruce up the page templates by making them valid HTML and adding some
CSS rules.
通过使页面模板成为有效的 HTML 并添加一些 CSS 规则来装饰页面模板。 - Implement inter-page linking by converting instances of
[PageName]
to
通过将[PageName]
的实例转换为实现页面间链接
<a href="/view/PageName">PageName</a>
. (hint: you could useregexp.ReplaceAllFunc
to do this)
<a href="/view/PageName">PageName</a>
. (提示:您可以使用regexp.ReplaceAllFunc
来做到这一点)