Writing Web Applications

Introduction

本教程涵盖:

假设知识:

Getting Started

目前,您需要FreeBSD,Linux,OS X或Windows计算机才能运行Go. 我们将使用$表示命令提示符.

安装Go(请参阅安装说明 ).

在您的GOPATH内为本教程创建一个新目录,并对其进行cd:

$ mkdir gowiki
$ cd gowiki

创建一个名为wiki.go的文件,在您喜欢的编辑器中将其打开,并添加以下几行:

package main

import (
	"fmt"
	"io/ioutil"
)

我们从Go标准库中导入fmtioutil软件包. 稍后,当我们实现其他功能时,我们将向此import声明中添加更多包.

Data Structures

让我们从定义数据结构开始. Wiki由一系列相互连接的页面组成,每个页面都有标题和正文(页面内容). 在这里,我们将Page定义为具有两个字段的结构,分别代表标题和正文.

type Page struct {
    Title string
    Body  []byte
}

类型[]byte表示" byte片". (有关分的更多信息,请参见分片:用法和内部知识 .) Body元素是[]byte而不是string因为这是我们将使用的io库所期望的类型,如下所示.

Page结构描述了页面数据将如何存储在内存中. 但是持久存储呢? 我们可以通过在Page上创建一个save方法来解决这个问题:

func (p *Page) save() error {
    filename := p.Title + ".txt"
    return ioutil.WriteFile(filename, p.Body, 0600)
}

该方法的签名如下:"这是一个名为save的方法,它以一个指向Page的指针作为接收者p .它不带参数,并返回error类型的值."

此方法将保存PageBody到一个文本文件中. 为简单起见,我们将" Title用作文件名.

save方法返回一个error值,因为这是WriteFile (将字节片写入文件的标准库函数)的返回类型. save方法返回错误值,以使应用程序在写入文件时遇到任何问题时可以处理该错误值. 如果一切顺利, Page.save()将返回nil (指针,接口和某些其他类型的零值).

作为第三个参数传递给WriteFile的八进制整数文字0600表示该文件应仅对当前用户具有读写权限. (有关详细信息,请参见Unix手册页open(2) .)

除了保存页面,我们还将要加载页面:

func loadPage(title string) *Page {
    filename := title + ".txt"
    body, _ := ioutil.ReadFile(filename)
    return &Page{Title: title, Body: body}
}

函数loadPage从title参数构造文件名,将文件内容读取到新的变量body ,并返回指向使用正确的title和body值构造的Page文字的指针.

函数可以返回多个值. 标准库函数io.ReadFile返回[]byteerror . 在loadPage ,错误尚未得到处理. 下划线( _ )符号表示的"空白标识符"用于丢弃错误返回值(实质上是将值分配为空).

但是,如果ReadFile遇到错误怎么办? 例如,该文件可能不存在. 我们不应忽略这种错误. 让我们修改函数以返回*Pageerror .

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
}

现在,此函数的调用者可以检查第二个参数. 如果为nil则表示已成功加载Page. 否则,将是调用者可以处理的error (有关详细信息,请参见语言规范 ).

至此,我们已经有了一个简单的数据结构,并具有保存到文件和从文件中加载的能力. 让我们编写一个main函数来测试我们编写的内容:

func main() {
    p1 := &Page{Title: "TestPage", Body: []byte("This is a sample Page.")}
    p1.save()
    p2, _ := loadPage("TestPage")
    fmt.Println(string(p2.Body))
}

编译并执行此代码后,将创建一个名为TestPage.txt的文件,其中包含p1的内容. 然后将文件读入struct p2 ,并将其Body元素打印到屏幕上.

您可以像这样编译和运行程序:

$ go build wiki.go
$ ./wiki
This is a sample Page.

(如果使用的是Windows,则必须输入不带" ./ "的" wiki "才能运行该程序.)

Click here to view the code we've written so far.

Introducing the net/http package (an interlude)

这是一个简单的Web服务器的完整示例:

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程序包使用handler对Web根( "/" )的所有请求.

然后,它调用http.ListenAndServe ,指定它应该在任何接口( ":8080" )上的端口8080上进行侦听. (暂时不必担心它的第二个参数nil .)该函数将一直阻塞,直到程序终止.

ListenAndServe始终返回错误,因为它仅在发生意外错误时才返回. 为了记录该错误,我们将函数调用包装为log.Fatal .

函数handler的类型为http.HandlerFunc . 它以http.ResponseWriterhttp.Request作为其参数.

http.ResponseWriter值组合HTTP服务器的响应; 通过写入,我们将数据发送到HTTP客户端.

http.Request是代表客户端HTTP请求的数据结构. r.URL.Path是请求URL的路径部分. 尾随的[1:]表示"创建从第一个字符到结尾的Path子片段". 这将从路径名中删除前导" /".

如果您运行此程序并访问URL:

http://localhost:8080/monkeys

该程序将显示一个包含以下内容的页面:

Hi there, I love monkeys!

Using net/http to serve wiki pages

要使用net/http包,必须将其导入:

import (
	"fmt"
	"io/ioutil"
	"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)
}

同样,请注意使用_来忽略loadPageerror返回值. 为了简化起见,在此进行此操作,通常被认为是不好的做法. 我们将在稍后处理.

首先,此函数从r.URL.Path (请求URL的路径组件)提取页面标题. 使用[len("/view/"):]Path进行切片,以删除请求路径的前导"/view/"部分. 这是因为该路径将始终以"/view/"开头,这不是页面标题的一部分.

然后,该函数加载页面数据,使用简单的HTML字符串格式化页面,并将其写入whttp.ResponseWriter .

要使用此处理程序,我们重写了main函数以使用viewHandler初始化http来处理/view/路径下的所有请求.

func main() {
    http.HandleFunc("/view/", viewHandler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Click here to view the code we've written so far.

让我们创建一些页面数据(如test.txt ),编译我们的代码,然后尝试提供Wiki页面.

在编辑器中打开test.txt文件,并在其中保存字符串" Hello world"(不带引号).

$ go build wiki.go
$ ./wiki

(如果使用的是Windows,则必须输入不带" ./ "的" wiki "才能运行该程序.)

在运行此Web服务器后,对http://localhost:8080/view/test应显示一个标题为" test"的页面,其中包含单词" Hello world".

Editing Pages

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都很难看. 当然,有更好的方法.

The html/template package

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.Titlep.Body .

模板指令用双花括号括起来. printf "%s" .Body指令是一个函数调用,该函数将.Body作为字符串而不是字节流输出,与对fmt.Printf的调用相同. html/template包有助于确保模板操作仅生成安全且外观正确的HTML. 例如,它会自动转义大于号( > ),并用&gt;替换&gt; ,以确保用户数据不会损坏HTML表单.

由于我们现在正在使用模板,因此让我们为viewHandler创建一个名为view.html的模板:

<h1>{{.Title}}</h1>

<p>[<a href="/edit/{{.Title}}">edit</a>]</p>

<div>{{printf "%s" .Body}}</div>

Modify viewHandler accordingly:

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未实现的保存处理程序的注册,则可以再次构建和测试程序. 单击此处查看我们到目前为止编写的代码.

Handling non-existent pages

如果您访问/view/APageThatDoesntExist怎么/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)
}

http.Redirect函数将HTTP状态代码http.StatusFound (302)和Location标头添加到HTTP响应.

Saving Pages

函数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.

FormValue返回的值是string类型. 我们必须先将该值转换为[]byte然后再将其放入Page结构中. 我们使用[]byte(body)进行转换.

Error handling

我们程序中有几个地方会忽略错误. 这是一种不好的做法,尤其是因为确实发生错误时,程序将具有意外的行为. 更好的解决方案是处理错误并将错误消息返回给用户. 这样,如果出现问题,服务器将完全按照我们想要的方式运行,并可以通知用户.

首先,让我们处理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()期间发生的任何错误都将报告给用户.

Template caching

有在此代码中的低效率: renderTemplate调用ParseFiles每次呈现页面时. 更好的方法是在程序初始化时调用一次ParseFiles ,将所有模板解析为一个*Template . 然后,我们可以使用ExecuteTemplate方法呈现特定的模板.

First we create a global variable named templates, and initialize it with ParseFiles.

var templates = template.Must(template.ParseFiles("edit.html", "view.html"))

函数template.Must是一个方便包装器,当传递非零error值时会惊慌,否则返回未更改的*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)
    }
}

请注意,模板名称是模板文件名,因此我们必须在tmpl参数tmpl附加".html" .

Validation

如您所见,该程序存在严重的安全漏洞:用户可以提供要在服务器上读取/写入的任意路径. 为了减轻这种情况,我们可以编写一个函数来使用正则表达式验证标题.

首先,将"regexp"添加到import列表. 然后,我们可以创建一个全局变量来存储我们的验证表达式:

var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")

函数regexp.MustCompile将解析并编译正则表达式,并返回regexp.Regexp . MustCompileCompile不同之处在于,如果表达式编译失败,它将崩溃,而Compile返回error作为第二个参数.

现在,让我们编写一个使用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未找到"错误,并将错误返回给处理程序. 要创建新错误,我们必须导入errors包.

让我们在每个处理程序中调用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

在每个处理程序中捕获错误条件会引入很多重复的代码. 如果我们可以将每个处理程序包装在执行此验证和错误检查的函数中,该怎么办? 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'
	}
}

返回的函数称为闭包,因为它封装了在其外部定义的值. 在这种情况下,变量fnmakeHandler的单个参数)被闭包包围. 变量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.ResponseWriterhttp.Request (换句话说,就是http.HandlerFunc )的http.HandlerFunc . 闭包从请求路径中提取title ,并使用TitleValidator表达式对其进行验证. 如果title无效,则将使用http.NotFound函数将错误写入ResponseWriter . 如果title有效,则将使用ResponseWriterRequesttitle作为参数调用封闭的处理函数fn .

现在,我们可以在main包中使用makeHandler将处理程序函数包装起来,然后再将它们注册到http包中:

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)
}

Try it out!

Click here to view the final code listing.

重新编译代码,然后运行应用程序:

$ go build wiki.go
$ ./wiki

访问http:// localhost:8080 / view / ANewPage应该向您显示页面编辑表单. 然后,您应该能够输入一些文本,单击"保存",然后重定向到新创建的页面.

Other tasks

以下是您可能想自行解决的一些简单任务:

by  ICOPY.SITE