Go 编写 Web 应用 
                                                    
                        
                    
                    
  
                    
                    这是一篇官方的 Writing Web Applications 的翻译
如果 golang.org 打不开的话,可以把所有链接中的 golang.org 更换成 golang.google.cn ,这是一个官方的国内镜像,和 golang.org 的内容是一致的。
简介
本教程包含下面内容
- 
通过 load 与 save 方法创建数据结构 
- 
使用 net/http去构建 web 应用
- 
使用 html/template去处理 HTML 模板
- 
使用 regexp包去验证用户的输入
- 
使用闭包 
需要已经掌握下面知识
- 
编程经验 
- 
了解基本的 web 技术(HTTP,HTML) 
- 
会一些简单 UNIX/DOS 命令行 
入门
现在,你可以在 FreeBSD, Linux, OS X, 或 Windows 上运行 Go , 我们使用 $ 代表命令行的提示符, $ 开始的内容是输入的命令。
安装 Go (查看 安装文档 )
在 GOPATH 中为这个项目创建一个新的目录,然后切换到这个目录(cd)
$ cd $GOPATH/src
$ mkdir gowiki
$ cd go wiki创建一个文件 wiki.go , 用你喜欢的编辑器打开它,用添加下面内容
package main
import (
    "fmt"
    "io/ioutil"
)我们从 Go 的标准库中导入了 fmt 和 io/ioutil 包, 稍后,当我们实现其他功能时,将会在 import 声明中添加更多的包。
数据结构
我们从定义数据结构开始,一个 wiki 由许多相关的页面组成,每一个页面都包含一个 title 和 body (页面的内容)。我们定义一个 Page 结构,包含两个代表 title 和 body 的字段。
type Page struct {
    Title string
    Body []byte
}类型 []byte 就是一个 byte 切片(关于切片的更多信息,查看: Slices: usage and internals)。 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 的 Body 到一个文本文件。为了简单,我们使用 Title 来做它的文件名。
save 方法返回了一个 error 类型的值,是因为它是 WriteFile 的返回类型(一个用于将字节切片写入到文件的标准库函数)。 save 方法返回的是一个 error 类型的值,所以在写入文件出现错误的时候应该去处理它。如果没有出现错误,应该返回一个 nil (指针,接口和一些其他类型的零值)
八进制整数 0600, 是传给 WriteFile 的第三个参数,表示只有当前用于拥有文件的读写权限。(通过 Unix man page open(2)查看详细说明 “译者注: Linux 上可以通过这个命令查看: man 2 open ”)
除了保存页面,我们也需要加载页面:
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 返回了一个 []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 ,表示成功加载了一个 Page 。如果不是,它将是一个 error ,并且可以由调用者处理(详细查看 语言规范 )。
现在,我们有了一个简单的数据结构,可以保存并加载一个文件。让我们写一个 main 函数来测试我们写的东西。
func main() {
    p1 := &Page{Title: "TestPage", Body: []byte("This is a simple Page.")}
    p1.save()
    p2, _ := loadPage("TestPage")
    fmt.Println(string(p2.Body))
}编译并执行这个代码,一个包含了 p1 的内容的名字是 TestPage.txt 的文件将被创建。这个文件将被读进结构 p2 中,并将它的 Body 元素打印到屏幕上。
你可以像这样编译并运行这个程序:
$ go build wiki.go 
$ ./wiki 
This is a simple Page.(如果你使用的是 Windows ,你必须输入 wiki ,去掉 ./ 去执行这个程序)
net http 包简介
这是一个简单的 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 始终返回一个 error ,并且它只有发生意外错误时才会返回。为了记录这个错误,我们在它的外面包裹一个叫 log.Fatal 的函数。
handler 是一个 http.HandlerFunc 类型的函数,它需要两个参数,http.ResponseWriter 和 http.Request 。
http.ResponseWriter 的值集合了 HTTP 服务器的响应。通过写入它,我们将数据发送到 HTTP 客户端。
http.Request 是表示客户端 HTTP 请求的数据结构。 r.URL.Path 是一个请求地址组成的路径。它的后面跟随 [1:] 的意思是: “创建一个从第一个字符到结尾的子切片” 。从路径中删除开头的 / 。
如果你运行程序,并访问这个地址:
http://localhost:8080/monkeys程序将会显示包含下面内容的页面:
Hi there, I love monkeys!使用 net http 去服务 wiki 页面
要想使用 net/http 包,它必须被导入:
import (
"fmt"
"io/ioutil"
"net/http"
)
我们来创建一个叫 viewHandler 的 handler, 用户可以通过它去查看 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 的 path 组件 r.URL.Path 中提取页面的标题。再通过切片 [len("/view/"):] 去掉前面路径前面的 /view/ ,这是因为路径总是以 /view/ 开始,它不是页面的一部分。
然后函数去加载页面数据,格式化成一个简单的 HTML 格式的字符串并写入到 http.ResponseWriter w 。
要使用这个 handler ,我们重写我们的 main 函数,去初始化 http 通过 viewHandler 去处理每一个 /view/ 下面的请求。
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 ,去掉 ./ 去执行这个程序)
服务启动后,访问 http://localhost:8080/view/test 将会显示页面,标题是 test ,内容是 Hello world 。
编辑页面
wiki 不是一个不能编辑页面的 wiki 。我们来创建两个新的 handler, 一个叫 editHandler ,用来显示编辑页面的 form 表单,另一个叫 saveHandler ,用来保存 form 表单提交的数据。
首先,我们添加它们到 main() 中:
func main() {
    http.HandleFunc("/view/", viewHandler)
    http.HandleFunc("/edit/", editHandler)
    http.HandleFunc("/save/", saveHandler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}editHandler 函数加载页面(或者,它不存在时创建一个空的 Page 结构), 并且显示一个 HTML form 表单。
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 添加到 import 的列表中。我们也不会再使用 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 被生成,它会自动转译大于号符号 >,替换成 >,确保用户数据不会破坏表单数据。
我们现在已经使用模板了,就再创建一个用于 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)
}注意,我们在两个 handler 中使用了几乎完全一样的模板代码。我们将模板代码放到一个单独的函数中,来去除这个重复:
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
    t, _ := template.ParseFiles(tmpl + ".html")
    t.Execute(w, p)
}修改连个 handler 来使用这个函数:
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 handler,然后就可以重新编辑并测试我们的程序了。
处理不存在的页面
如果你输入 /view/APageThatDoesntExist ,你将看到一个包含 HTML 的页面,这是因为它忽略了 loadPage error 返回值,并继续去尝试填充了一个没有数据的模板去替换,如果请求的页面不存在,它应该重定向到编辑页面,这样就可以创建内容。
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) 和 一个地址到 http 响应。
保存页面
saveHandler 函数将处理编辑页面 form 表单提交的数据,在 mian 函数中取消和此相关的注释,然后实现这个 handler 。
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)
}这个页面的 title (由 URL 提供)和表单中的唯一字段 Body 被保存到一个新的 Page ,然后调用 save() 方法写入数据到一个文件,并且将客户端重定向到 /view/ 页面。
FormValue 的返回值是一个字符串类型,我们在将它填充到 Page 前必选转换成 []byte,使用 []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 状态码(示例中是 Internal Server Error)和错误消息。这个抽离出一个单独的函数是一个多么明智的做法,否则要改多个地方了。
现在,我们来修改 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 的 error 时会产生一个 panic,除此之外,返回的 *Template 不会改变。一个 panic 在这里是合适,如果模板不能被加载只有退出程序才是明智的。
ParseFiles 函数接受任意数量的用来识别我们的模板文件的字符串参数,然后将这些文件解析到 templates,并且通过基础的文件名命名。如果我们要添加更多的模板到我们的程序,我们需要将它们的名字添加到 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)
    }
}注意,模板名称是模板的文件名,所以我们必须在 teml 参数后面添加 .html 。
验证
可能你已经注意到,这个程序有一个严重的安全漏洞:用户可以通过任意路径在服务器上读/写。为了解决这个,我们可以编写一个函数,通过正则表达式来验证 title 。
首先,在 import 列表添加 regexp ,然后我们可以创建一个全局变量去保存我们的正则表达式。
var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")函数 regexp.MustCompile 将解析并编译正则表达式,返回一个 regexp.Regexp 。MustCompile 区别于 Compile 的是,当正则表达式编译失败的时候,它将产生一个 panic, 而 Compile 会通过第二个参数返回一个 error 。
现在,我们编写一个函数来使用 validPath 来验证路径并提取页面的 title 。
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 // title 是第二个子表达式
}如果 title 是有效的,它将和一个值为 nil 的 error 一起返回。如果 title 是无效的,函数将写入一个 404 Not Found 错误到 HTTP 链接,并且返回一个错误到 handler 。要创建一个新的 error ,我们还必须要导入 errors 包。
我们将 getTitle 调用放到每一个 handler 中:
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)
}函数文字和闭包简介
捕获每一个函数中的错误处理条件会产生大量的冗余代码。如果我们将每一个 handler 包装进一个函数去处理验证和错误检查呢? Go 的函数字面提供了一个强大的抽象功能可以帮助我们实现它。
首先,我们重写每一个 handler 函数的定义,接收一个字符串类型的 title 参数:
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 中提取 title
        // 并且调用提供的 handler 'fn'
    }
}这个返回的函数叫闭包,因为它包含了在它外部定义的值。在这种情况,变量 fn (传给 makeHandler 的单个参数) 包裹在闭包中, 变量 fn 将会是我们的 save,edit 或 view handler 中的一个。
现在我们可以从 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 )。这个闭包从请求路径中提取 title ,并且通过 title 验证器 regexp 验证它。 如果标题是无效的,一个 error 将通过 http.NotFound 函数写入到 ResponseWriter 。如果 title 是有效的,它包裹的 handler 函数 fn 将传入 ResponseWriter, Request, 和 title 参数被调用。
现在,在 main 函数中,我们可以在 handler 被注册到 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))
}最后,我们从 handler 中删除对 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)
}试试看
重新编译代码并运行它:
$ go build wiki.go
$ ./wiki访问 http://localhost:8080/view/ANewPage 将显示页面编辑表单, 你应该可以输入一些文本,点击 save,然后会重定向到新创建的页面。
其他任务
下面是希望你可能希望自己解决的任务清单:
- 
将模板保存到 tmpl/目录并且将页面数据保存到data/。
- 
添加一个 handler 将 web 根目录重定向到 /view/FrontPage。
- 
完善页面模板,使它们成为一个有效的 HTML 并添加一些 CSS 规则。 
- 
通过 [PageName] 实例实现一些页面间的链接, <a href="/view/PageName">PageName</a>(提示: 你可以通过regexp.ReplaceAllFunc来实现这个)。
本文章来自: broqiang.com 可以随意转载。
本作品采用《CC 协议》,转载必须注明作者和本文链接
 
           broqiang 的个人博客
 broqiang 的个人博客
         
             
             
             
             
             
             
             
           
           关于 LearnKu
                关于 LearnKu
               
                     
                     
                     粤公网安备 44030502004330号
 粤公网安备 44030502004330号 
 
推荐文章: