编写Web应用程序(编写web应用程序的过程)

2023-08-23

Go和Java已经十多年了,最初接触Go语言是因为Java大多做应用和网络编程,而Go可以从底层到前端都可以做,同时,Go和Java作为在世界上影响巨大的两门开发语言,在语言特点和应用领域上都存在共通和相似之处,加上最开始是搞C/C++做游戏开发的,对底层的计算原理和架构还比较熟.

一直想系统性的写一些比较有用的文章,供后来者参考,但苦于时间有限一直未能前行,2022年,打算开始写,万事开头难,希望能坚持不懈,完成Go、Java、Python系列文章,系统性的讲解这三门语言,以期为编程爱好者提供实实在在的帮助,少走弯路少踩坑,达到事半功倍的效果。

让我们从Go语言开始吧。

简介

这部分我们主要完成Web应用程序的编写,功能比较简单:浏览网页、编辑网页内容以及创建新的网页并保存到硬盘上。涉及的主要技术如下:

创建具有加载和保存方法的数据结构使用net/http包提供的方法构建Web应用程序使用html/template包提供的方法处理 HTML 模板使用regexp包验证Web输入的内容(validate)。使用闭包

前提条件

编程经验:这里的代码非常简洁,有一定的编程经验有助于了解一些关于函数的知识。编辑工具:任何文本编辑器都可以用来做Go编程,大多数文本编辑器都对Go有很好的支持。这里我们使用的是 VSCode(免费)网络知识:了解基本的网络技术(HTTP、HTML)等命令终端:Go 在 Linux 和 Mac 上的任何终端以及 Windows 中的 PowerShell 或 cmd 上都能很好地工作。

详细的环境准备请参考下面的链接:

哲然源代码:开始使用Go语言6 赞同 · 0 评论文章

本文的开发环境:

编辑器:Visual Studio Code:版本: 1.62.3 (user setup)Go:version go1.17.7 windows/amd64操作系统:Windows 10 家庭中文版

一个简单的Web程序

先举一个例子,下面的代码是一个完整的简单的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包处理所有来自网页root("/" )的请求,都交由handler函数去处理,然后调用http.ListenAndServe启动http服务,监听端口为8080,函数将一直阻塞,直到程序终止。

函数handler以 http.ResponseWriter和 http.Request作为参数,http.ResponseWriter把HTTP 服务器响应的数据发送到 HTTP 客户端,http.Request客户端 HTTP 请求的数据结构,r.URL.Path是请求 URL 从root("/" )开始的路径数组,r.URL.Path[1:]表示从第一个字符到结尾[1:]的子集合”,也就是去掉Path路径中的第一个“/”。

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

http://localhost:8080/monkeys

该程序将显示一个页面,其中r.URL.Path的值为:/monkeys,而r.URL.Path[1:]就是取出monkeys:

Hi there, I love monkeys!

保存/加载功能

一个Web应用程序由一系列相互关联的页面组成,每个页面都有一个标题和一个正文(页面内容)。我们首先定义一个结构体:Page,包含两个字段,分别代表标题和正文:

type Page struct { Title string Body []byte }

Body字段定义为[]byte类型,而不是string类型,因为我们将要使用的io库所期望的类型是[]byte类型,而不是string类型。

page结构体描述了页面的数据在内存的形式,那如何存到硬盘上呢,我们可以写一个方法,把内存中的页面数据保存到硬盘上:

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

上面的代码中save方法的定义表明:方法的接收者是一个指向的page结构体的指针p,方法没有任何参数,其返回类型为error。

此方法会将page的字段Body保存到txt文件中,其文件名为page的Title字段。方法返回一个error值,这是由WriteFile方法的返回类型决定的,如果一切顺利,Page.save()将返回 nil(空值)。

第三个参数八进制整数文字0600,表示创建文件时应仅对当前用户具有读写权限。

有关指针、函数和方法的详细内容,请参考:

哲然源代码:第一部分 基础 ---(3)扩展类型0 赞同 · 0 评论文章

到此,我们已经定义了save方法,我们还需要装入方法,装入保存到硬盘的文件:

func load(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 }

上面的load函数传入一个文件名(title),将文件的内容读入变量body,并返回一个指向page结构体的指针。

注意:Go语言中函数可以返回多个值,标准库函数 io.ReadFile返回[]byte和error,通过检查第二个参数err;如果成功读入了文件,则err的值为nil空值。如果发生错误,err则是错误信息error。

显示页面处理函数

接下来我们要写一个函数,该函数完成浏览器发过来的页面查看请求,然后向浏览器返回页面数据,浏览器收到数据后,显示该页面。

func viewHandler(w http.ResponseWriter, r *http.Request) { title := r.URL.Path[len("/view/"):] p, _ := load(title) fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body) }

上面的代码显示,viewHandler处理函数将处理以"/view"开头的URLs请求,函数首先取到要查看页面对应的文件名(title),然后从硬盘上读入该文件,最后把文件内容输出到浏览器客户端。

上面的代码,直接输出HTML代码,代码比较丑陋,而且阅读起来也不方便,当我们要修改页面内容或布局的时候,就要修改对应的Go代码,非常不方便。

有没有比较好的方法呢?

可以使用html/template 包,这个包是Go标准库的一部分,我们可以讲HTML代码保存到一个单独的文件中,这样我们就可以很方便的更改html页面的布局等,而无需修改Go代码,然后我们使用html/template包来处理这个html文件。

让我们先创建一个模板文件,该模板文件内容为上面的HTML代码,文件名为view.html,该文件保存到代码同一个目录下:

<h1>{{.Title}}</h1> <p>[<a href="/edit/{{.Title}}">编辑</a>]</p> <div>{{printf " %s" .Body}}</div>

接下来我们修改上面的编辑处理函数viewHandler,通过使用模板来代替HTML硬编码:

func viewHandler(w http.ResponseWriter, r *http.Request) { title := r.URL.Path[len("/view/"):] p, _ := load(title) t, _ := template.ParseFiles("view.html") t.Execute(w, p) }

函数template.ParseFiles将读取view.html文件的内容,并返回一个类型为template.Template的指针。

方法t.Execute进行模板解析,将生成的 HTML 写入http.ResponseWriter,上面的文件view.html中的.Title和.Body标识符将会被load函数的返回指针p的字段Title和Body 的值对应替换。

细心的同学可能会发现,在view.html文件中,.Title和.Body使用双大括号括起来了,{{printf "%s" .Body}}语句表明以字符串的形式输出内容。

使用html/template包可以确保模板生成安全且内容正确的HTML代码,例如,html/template会自动转义大于符号(>)为&gt;,以确保页面不会崩溃。

编辑页面处理函数

接下来我们要写一个函数,该函数完成浏览器发过来的页面编辑请求,如果要编辑的页面不存在,函数则创建一个空的page结构体,然后向浏览器返回页面数据,浏览器收到数据后,显示该页面内容(HTML表单)。

让我们先创建一个模板文件,该模板文件内容为上面的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 := load(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,上面的文件edit.html中的.Title和.Body标识符将会被load函数的返回指针p的字段Title和Body 的值对应替换。

保存页面处理函数

接下来我们要写一个函数,该函数完成浏览器发过来的页面保存请求,在编辑页面点击提交按钮之后,页面的标题title和表单的内容,被存储到一个新的page结构体,然后调用save方法将数据写入文件,最后将url重定向到/view/title页面,也即是在浏览器里显示刚保存的页面内容。

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

方法FormValue的返回类型是string,此处必须将其转换为[]byte,然后才能将其放入Page结构中:[]byte(body)用来进行类型转换。

页面模板处理函数

上面的显示处理viewHandler和编辑处理editHandler函数,几乎用了完全相同的模板代码,可以将这部分代码移到一个函数来避免代码重复:

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

错误处理函数

在上面的程序中有几个地方会发生错误,当确实发生错误时,程序会出现意外行为,比较好的办法是处理这些错误并将错误消息返回给用户。

如果出现问题,服务器将完全按照正常的错误方式来处理,并通过网页通知浏览器客户端。

让我们先修改函数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方法,将所有模板加载解析到一个集合中,然后可以调用ExecuteTemplate方法来渲染特定的模板。

首先,我们创建一个名为 的全局变量templates,并用调用ParseFiles对其进行初始化,例:

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

函数template.Must是一个非常方便的包装器,返回 *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"到import列表中,然后我们可以创建一个全局变量来存储我们的验证表达式:

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

函数regexp.MustCompile将解析和编译正则表达式,并返回一个regexp。Regexp. MustCompile不同于函数Compile的地方在于:如果表达式编译失败,它会产生程序异常,而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 // 标题是第二个子表达式. }

如果标题有效,它将与nil 错误值一起返回。如果标题无效,该函数将向 HTTP 连接写入“404 Not Found”错误,并向处理程序返回错误。此处,需要创建新错误,因此必须导入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) }

函数闭包

在每个处理程序中捕获错误条件会引入大量重复代码。如果我们可以将每个处理程序包装在一个执行此验证和错误检查的函数中怎么办?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) { // 这里将从Request中提取页面标题, // 并调用提供的处理程序fn } }

返回的函数称为闭包,因为它包含在其外部定义的值。在这种情况下,变量fn作为makeHandler函数的单个参数,被闭包包围。该变量fn将是saveHandler、editHandler或viewHandler中的其中之一个。

现在我们可以从中获取代码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.ResponseWriterand http.Request(换句话说,一个http.HandlerFunc)的函数,最后返回一个闭包。该闭包函数从请求路径中提取title ,并使用正则validPath表达式对其进行验证。如果 title无效,将向 ResponseWriter使用http.NotFound函数写入错误信息。如果title有效, 闭包函数fn则将使用ResponseWriter、 Request和title作为参数调用处理程序。

现在,我们可以用makeHandler理函数处理对应的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)) }

最后,我们从处理函数中删除对getTitle的调用,将title作为一个参数传入,使函数更简单:

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

运行程序:

重新编译代码,然后运行应用程序(此处假定我们的文件为webapp.go):

$ go build webapp.go $ ./webapp

访问http://localhost:8080/view/ANewPage,应该会显示页面编辑表单。

然后,点击“edit”链接,输入一些文本:

单击“保存”按钮,页面会被重定向到新创建的页面。

代码的目录和文件如下图所示:

最终的代码:

package main import ( "html/template" "log" "net/http" "os" "regexp" ) type Page struct { Title string Body []byte } func (p *Page) save() error { filename := p.Title + ".txt" return os.WriteFile(filename, p.Body, 0600) } 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 } 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)) }

关注微信公众号「哲然源代码」,在手机上阅读所有资料,随时随地都能学习。