介绍
本教程有这些内容:
- 创建一个加载和保存数据结构的方法
- 用net/http包构建web程序
- 使用html/template包处理HTML模板
- 使用regexp包验证用户输入
- 使用闭包
前置知识:
- 具有编程经验
- 明白基本的web技术(HTTP,HTML)
- 有一定的UNIX/DOC命令行知识
开始
目前,Go可以运行在FreeBSD,Linux,macOS,或者windows设备上,我们会使用 $ 代表命令行提示符.
安装Go(参考安装介绍)
为本教程创建一个新目录 <注:这是我看到最细致的教程了,细致到有些啰嗦...给力>
$ mkdir gowiki
$ cd gowiki
新建一个名为wiki.go的文件,用你擅长的编辑器打开,然后敲入下面语句
package main import ( "fmt" "io/ioutil" )
我们从Go标准库中导入了fmt和ioutil包,随着,我们实现更多功能,将会在import中引入更多包
数据结构
我们来定义数据结构,一个wiki通常会有一些互相链接的页面,每个页面都会有一个标题和内容.在这里,我们定义了一个结构体名为page,其包含两个字段用来表示标题和内容
type Page struct {
Title string
Body []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,它有一个入参p,是个指向page的指针,如果没有指定参数,那么就会返回error类型错误"
这个方法将把page的内容保存到文本文件中,为了简便,我们将用标题作为文件名
save方法会返回一个错误类型,是因为WriteFile(把byte切片写入文件的标准库函数)的返回类型是这样的,save方法具有返回error值的能力,如果在写入文件时出现任何错误,则让应用程序处理.如果一切都处理完毕,page.save()就会返回nil.
WriteFile函数的第三个参数传入了一个八进制数字0600,这表示,为当前用户创建一个可读可写权限的文件.
除了保存页面,当然我们还需要加载页面:
func loadPage(title string) *Page { filename := title + ".txt" body, _ := ioutil.ReadFile(filename) return &Page{Title: title, Body: body} }
loadPage函数从title参数中获取文件名,并读取文件内容,将其导入body变量,返回指向由正确的标题和正文值构造的页面文字的指针。
函数能返回多个值,标准库函数io.ReadFile,返回[]byte和error,在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 }
现在,此函数的调用者,就能通过检查第二个参数发现错误了,如果此值是nil,表示页面被载入成功,如果不是nil,就代表出现了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, 然后这个文件会被读入到p2中,p2的body元素会被打印到屏幕上
$ go build wiki.go $ ./wiki This is a sample Page.
(windows的cmd工具,是不需要在wiki前部添加"./")
所有代码如下:
// 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,这告诉htp包用来使用根路径("/")处理所有请求
然后调用http.ListenAndServe,指示了监听8080(现在不需要关注第二参数是nil),此函数将一直阻塞,直到程序终止.
LinstenAndServe总是返回错误,因为它指挥在出现异常才会有返回值,为了记录这个错误,我们可以使用log.Fatel记录它.
这个函数的处理类型是 http.HandlerFunc.它以http.ResponseWriter和http.Request作为参数。
http.ResponseWriter值组合http服务器的响应;通过写入,我们将数据发送到HTTP客户机。
http.Request是响应客户端HTTP请求的数据结构,r.URL.Path是请求URL的路径组件,trailing[1:]意思是"为path创建一个子切片,从第一个字符一直到最后",这会吧第一个前导"/"从路径中删除.
http://localhost:8080/monkeys
Hi there, I love monkeys!
使用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的error,这里这样做是为了简单,通常被认为是不好的做法。我们稍后会处理这个问题。
首先,这个函数从r.URL.Path中提取页面标题,使用[len(“/view/”)对路径进行重新切片,以删除请求路径的前导“/view/”组件。这是因为路径总是以“/view/”开头,它不是页面标题的一部分。
然后,该函数加载页面数据,使用简单HTML字符串格式化页面,并将其写入http.ResponseWriter。
为了使用这个处理程序,我们重写了主函数,使用viewHandler初始化http,以处理路径/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页面提供服务。
在text.txt文件中写入 "Hello World"
$ go build wiki.go
$ ./wiki
编辑页面
目前,wiki是不具有编辑能力,现在我们来创建两个新的处理方法:editHandler来显示一个"编辑页",另一个叫saveHandler 保存编辑页的数据.
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,.Titile和.Body的 点符号,表示p.Title和p.Body.
模板指令用双大括号括起来。printf“%s”.Body指令是一个函数调用,它将.Body作为字符串而不是字节流输出,与对fmt.printf的调用相同。html/模板包有助于确保模板操作只生成安全且外观正确的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中注释掉未实现的save处理程序的注册,我们可以再次构建和测试我们的程序。单击此处查看到目前为止我们编写的代码。
处理不存在的页面
如果您访问/查看/ApageHatDoesnTextist会怎么样?您将看到一个包含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状态代码HTTP.StatusFound(302)和位置头添加到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存储在新的page中。然后调用save()方法将数据写入文件,并将客户端重定向到/view/page。
FormValue返回的值的类型为string。我们必须先将该值转换为[]字节,然后才能将其放入页面结构中。我们使用[]字节(body)来执行转换
处理错误
在我们的程序中很多地方,都忽略了错误,这是一种不好的做法,尤其是因为当错误发生时,程序将出现意外行为。更好的解决方案是处理错误并向用户返回错误消息。这样,如果出现问题,服务器将按照我们所希望的方式运行,并通知用户.
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) } }
Error函数发送指定的http响应代码(在本例中为“内部服务器错误”)和错误消息。将其放在单独功能中的决定已经取得了成效。
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()期间发生的任何错误都将报告给用户。
模板缓存
目前的代码效率很低:readerTemplate每次都要调用ParseFiles,最好的方式是,程序初始化时调用一次,并把所有模板解析到*Template,然后我们就能通过执行ExecuteTemplate方法调用指定的模板
首先,我们来创建一个全局变量,命名为templates,并且使用ParseFiles初始化它
var templates = template.Must(template.ParseFiles("edit.html", "view.html"))
函数template.Must是一个很好用的包装器,当传递一个非nil错误值时,它会崩溃,否则将返回未更改的*template.恐慌在这里是恰当的;如果无法加载模板,唯一明智的做法是退出程序。
ParseFiles函数任意数量的string类型的参数,这表示我们的模板文件,并将这些文件解析成以文件名为命名的模板,如何我们需要同时解析很多模板,只需把他们的名字添加到parseFiles的入参中.
随后,我们修改rederTemplate函数,传入合适的文件名以调用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"
验证
正如有看到的,这个程序有很严重的安全漏洞,用户可以读写服务器上的任意路径,为了缓解这种情况,我们可以个函数,使用正则表达式来验证标题
首先,在import列表中添加regexp, 然后我们来创建一个全局变量来保存我们的验证表达式
var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")
函数regexp.MustCompile是用来解析和编译正则表达式,然后返回一个regexp.Regexp. MustComplie和complie不同之处在于,如果表达式编译失败前者就会终止执行,而后者会返回一个错误
现在,我们写一个功能,使用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错误值。如果标题非法,这个函数将输出一个"404 Not Found" 错误给Http连接,并且给这个处理函数返回一个error, 我们使用 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) { // 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验证是否符合正则.如果标题非法,将使用http.NotFound函数将错误写入Response.Writer,如果标题合法,将使用ResponseWriter、Request和title作为参数调用附带的处理程序函数fn
现在,在使用http包注册处理函数之前,我们可以在main中使用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) }
试下
完整代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// 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 应向您提供页面编辑表单。然后,您应该能够输入一些文本,单击“保存”,并被重定向到新创建的页面。
其他作业
下面有一些作业, 你应该能独立应付:
- 在temp/中保存模板,在data/中保存页面
- 为web根路径写一个处理函数 重定向到/view/FrontPage
- 通过使页面模板成为有效的HTML并添加一些CSS规则来美化页面模板。
- 通过将[PageName]的实例转换为
<a href="/view/PageName">PageName</a>
. (可以使用regexp.ReplaceAllFunc执行此操作)