go语言编写Web程序

标签: go 语言 web | 发表时间:2013-03-15 22:08 | 作者:不锈的阳光
出处:http://www.cnblogs.com/

 

1. 简介

这个例子涉及到的技术:

  • 创建一个数据类型,含有load和save函数
  • 基于http包创建web程序
  • 基于template包的html模板技术
  • 使用regexp包验证用户输入
  • 使用闭包

假设读者有以下知识:

  • 基本的编程经验
  • web程序的基础技术(HTTP, HTML)
  • UNIX 命令行

2. 开始

首先,要有一个Linux, OS X, or FreeBSD系统,可以运行go程序。如果没有的话,可以安装一个虚拟机(如VirtualBox)或者 Virtual Private Server。

安装Go环境: (请参考 Installation Instructions).

创建一个新的目录,并且进入该目录:

$ mkdir ~/gowiki
$ cd ~/gowiki

创建一个wiki.go文件,用你喜欢的编辑器打开,然后添加以下代码:

package main

import (
"fmt"
"io/ioutil"
"os"
)

我们从go的标准库导入fmt, ioutil 和 os包。 以后,当实现其他功能时,我们会根据需要导入更多包。

3. 数据结构

我们先定义一个结构类型,用于保存数据。wiki系统由一组互联的wiki页面组成,每个wiki页面包含内容和标题。我们定义wiki页面为结构page, 如下:

type page struct {
titlestring
body[]byte
}

类型[]byte表示一个byte slice。(参考Effective Go了解slices的更多信息) 成员body之所以定义为[]byte而不是string类型,是因为[]byte可以直接使用io包的功能。

结构体page描述了一个页面在内存中的存储方式。但是,如果要将数据保存到磁盘的话,还需要给page类型增加save方法:

func (p *page) save() os.Error {
filename := p.title + ".txt"
return ioutil.WriteFile(filename, p.body, 0600)
}

类型方法的签名可以这样解读:“save为page类型的方法,方法的调用者为page类型的指针变量p。该成员函数没有参数,返回值为os.Error,表示错误信息。”

该方法会将page结构的body部分保存到文本文件中。为了简单,我们用title作为文本文件的名字。

方法save的返回值类型为os.Error,对应WriteFile(标准库函数,将byte slice写到文件中)的返回值。通过返回os.Error值,可以判断发生错误的类型。如果没有错误,那么返回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根据页面标题从对应文件读取页面的内容,并且构造一个新的 page变量——对应一个页面。

go中函数(以及成员方法)可以返回多个值。标准库中的io.ReadFile在返回[]byte的同时还返回os.Error类型的错误信息。前面的代码中我们用下划线“_”丢弃了错误信息。

但是ReadFile可能会发生错误,例如请求的文件不存在。因此,我们给函数的返回值增加一个错误信息。

func loadPage(title string) (*page, os.Error) {
filename := title + ".txt"
body, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return &page{title: title, body: body}, nil
}

现在调用者可以检测第二个返回值,如果为nil就表示成功装载页面。否则,调用者可以得到一个os.Error对象。(关于错误的更多信息可以参考os package documentation)

现在,我们有了一个简单的数据结构,可以保存到文件中,或者从文件加载。我们创建一个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的值打印到 屏幕。

可以用类似以下命令编译运行程序:

$ 8g wiki.go
$ 8l wiki.8
$ ./8.out
This is a sample page.

(命令8g和8l对应GOARCH=386。如果是amd64系统,可以用6g和6l)

点击这里查看我们当前的代码。

4. 使用http包

下面是一个完整的web server例子:

package main

import (
"fmt"
"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)
http.ListenAndServe(":8080", nil)
}

在main函数中,http.HandleFunc设置所有对根目录请求的处理函数为handler。

然后调用http.ListenAndServe,在8080端口开始监听(第二个参数暂时可以忽略)。然后程序将阻塞,直到退出。

函数handler为http.HandlerFunc类型,它包含http.Conn和http.Request两个类型的参数。

其中http.Conn对应服务器的http连接,我们可以通过它向客户端发送数据。

类型为http.Request的参数对应一个客户端请求。其中r.URL.Path 为请求的地址,它是一个string类型变量。我们用[1:]在Path上创建 一个slice,对应"/"之后的路径名。

启动该程序后,通过浏览器访问以下地址:

http://localhost:8080/monkeys

会看到以下输出内容:

Hi there, I love monkeys!

5. 基于http提供wiki页面

要使用http包,先将其导入:

import (
"fmt"
"http"
"io/ioutil"
"os"
)

然后创建一个用于浏览wiki的函数:

const lenPath = len("/view/")

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

首先,这个函数从r.URL.Path(请求URL的path部分)中解析页面标题。全局常量lenPath保存"/view/"的长度,它是请求路径的前缀部分。Path总是以"/view/"开头,去掉前面的6个字符就可以得到页面标题。

然后加载页面数据,格式化为简单的HTML字符串,写到c中,c是一个http.Conn类型的参数。

注意这里使用下划线“_”忽略loadPage的os.Error返回值。 这不是一种好的做法,此处是为了保持简单。我们将在后面考虑这个问题。

为了使用这个处理函数(handler),我们创建一个main函数。它使用viewHandler初始化http,把所有以/view/开头的请求转发给viewHandler处理。

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

点击这里查看我们当前的代码。

让我们创建一些页面数据(例如as test.txt),编译,运行。

$ echo "Hello world" > test.txt
$ 8g wiki.go
$ 8l wiki.8
$ ./8.out

当服务器运行的时候,访问 http://localhost:8080/view/test将显示一个页面,标题为“test”,内容为“Hello world”。

6. 编辑页面

编辑功能是wiki不可缺少的。现在,我们创建两个新的处理函数(handler):editHandler显示"edit page"表单(form),saveHandler保存表单(form)中的数据。

首先,将他们添加到main()函数中:

func main() {
http.HandleFunc("/view/", viewHandler)
http.HandleFunc("/edit/", editHandler)
http.HandleFunc("/save/", saveHandler)
http.ListenAndServe(":8080", nil)
}

函数editHandler加载页面(或者,如果页面不存在,创建一个空page 结构)并且显示为一个HTML表单(form)。

func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[lenPath:]
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非常丑陋。当然,我们有更好的办法。

7. template包

template包是 GO语言标准库的一个部分。我们使用template将HTML存放在一个单独的文件中,可以更改编辑页面的布局而不用修改相关的GO代码。

首先,我们必须将template添加到导入列表:

import (
"http"
"io/ioutil"
"os"
"template"
)

创建一个包含HTML表单的模板文件。打开一个名为edit.html的新文件,添加下面的行:

<h1>Editing {title}</h1>

<form action="/save/{title}" method="POST">
<div><textarea name="body" rows="20" cols="80">{body|html}</textarea></div>
<div><input type="submit" value="Save"></div>
</form>

修改editHandler,用模板替代硬编码的HTML。

func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[lenPath:]
p, err := loadPage(title)
if err != nil {
p = &page{title: title}
}
t, _ := template.ParseFile("edit.html", nil)
t.Execute(p, w)
}

函数template.ParseFile读取edit.html的内容,返回*template.Template类型的数据。

方法t.Execute用p.title和p.body的值替换模板中所有的{title}和{body},并且把结果写到http.Conn。

注意,在上面的模板中我们使用{body|html}。|html部分请求模板引擎在输出body的值之前,先将它传到html格式化器(formatter),转义HTML字符(比如用>替换>)。 这样做,可以阻止用户数据破坏表单HTML。

既然我们删除了fmt.Sprintf语句,我们可以删除导入列表中的"fmt"。

使用模板技术,我们可以为viewHandler创建一个模板,命名为view.html。

<h1>{title}</h1>

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

<div>{body}</div>

修改viewHandler:

func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[lenPath:]
p, _ := loadPage(title)
t, _ := template.ParseFile("view.html", nil)
t.Execute(p, w)
}

注意,在两个处理函数(handler)中使用了几乎完全相同的模板处理代码,我们可以把模板处理代码写成一个单独的函数,以消除重复。

func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[lenPath:]
p, _ := loadPage(title)
renderTemplate(w, "view", p)
}

func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[lenPath:]
p, err := loadPage(title)
if err != nil {
p = &page{title: title}
}
renderTemplate(w, "edit", p)
}

func renderTemplate(w http.ResponseWriter, tmpl string, p *page) {
t, _ := template.ParseFile(tmpl+".html", nil)
t.Execute(p, w)
}

现在,处理函数(handler)代码更短、更加简单。

8. 处理不存在的页面

当你访问/view/APageThatDoesntExist的时候会发生什么?程序将会崩溃。因为我们忽略了loadPage返回的错误。请求页不存在的时候,应该重定向客户端到编辑页,这样新的页面将会创建。

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

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

9. 储存页面

函数saveHandler处理表单提交。

func saveHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[lenPath:]
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/页面。

FormValue返回值的类型是string,在将它添加到page结构前,我们必须将其转换为[]byte类型。我们使用[]byte(body)执行转换。

10. 错误处理

在我们的程序中,有几个地方的错误被忽略了。这是一种很糟糕的方式,特别是在错误发生后,程序会崩溃。更好的方案是处理错误并返回错误消息给用户。这样做,当错误发生后,服务器可以继续运行,用户也会得到通知。

首先,我们处理renderTemplate中的错误:

func renderTemplate(w http.ResponseWriter, tmpl string, p *page) {
t, err := template.ParseFile(tmpl+".html", nil)
if err != nil {
http.Error(w, err.String(), http.StatusInternalServerError)
return
}
err = t.Execute(p, w)
if err != nil {
http.Error(w, err.String(), http.StatusInternalServerError)
}
}

函数http.Error发送一个特定的HTTP响应码(在这里表示“Internal Server Error”)和错误消息。

现在,让我们修复saveHandler:

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.String(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

p.save()中发生的任何错误都将报告给用户。

11. 模板缓存

代码中有一个低效率的地方:每次显示一个页面,renderTemplate都要调用ParseFile。更好的做法是在程序初始化的时候对每个模板调用ParseFile一次,将结果保存为*Template类型的值,在以后使用。

首先,我们创建一个全局map,命名为templates。templates用于储存*Template类型的值,使用string索引。

然后,我们创建一个init函数,init函数会在程序初始化的时候调用,在main函数之前。函数template.MustParseFile是ParseFile的一个封装,它不返回错误码,而是在错误发生的时候抛出(panic)一个错误。抛出错误(panic)在这里是合适的,如果模板不能加载,程序唯一能做的有意义的事就是退出。

func init() { for _, tmpl := range []string{"edit", "view"} { templates[tmpl] = template.MustParseFile(tmpl+".html", nil) } }

使用带range语句的for循环访问一个常量数组中的每一个元素,这个常量数组中包含了我们想要加载的所有模板的名称。如果我们想要添加更多的模板,只要把模板名称添加的数组中就可以了。

修改renderTemplate函数,在templates中相应的Template上调用Execute方法:

func renderTemplate(w http.ResponseWriter, tmpl string, p *page) {
err := templates[tmpl].Execute(p, w)
if err != nil {
http.Error(w, err.String(), http.StatusInternalServerError)
}
}

12. 验证

你可能已经发现,程序中有一个严重的安全漏洞:用户可以提供任意的路径在服务器上执行读写操作。为了消除这个问题,我们使用正则表达式验证页面的标题。

首先,添加"regexp"到导入列表。然后创建一个全局变量存储我们的验证正则表达式:

函数regexp.MustCompile解析并且编译正则表达式,返回一个regexp.Regexp对象。和template.MustParseFile类似,当表达式编译错误时,MustCompile抛出一个错误,而Compile在它的第二个返回参数中返回一个os.Error。

现在,我们编写一个函数,它从请求URL解析中解析页面标题,并且使用titleValidator进行验证:

func getTitle(w http.ResponseWriter, r *http.Request) (title string, err os.Error) {
title = r.URL.Path[lenPath:]
if !titleValidator.MatchString(title) {
http.NotFound(w, r)
err = os.NewError("Invalid Page Title")
}
return
}

如果标题有效,它返回一个nil错误值。如果无效,它写"404 Not Found"错误到HTTP连接中,并且返回一个错误对象。

修改所有的处理函数,使用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.String(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

13. 函数文本和闭包

处理函数(handler)中捕捉错误是一些类似的重复代码。如果我们想将捕捉错误的代码封装成一个函数,应该怎么做?GO的函数文本提供了强大的抽象能力,可以帮我们做到这点。

首先,我们重写每个处理函数的定义,让它们接受标题字符串:

定义一个封装函数,接受上面定义的函数类型,返回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是我们的处理函数,save、edit、或view。

我们可以把getTitle的代码复制到这里(有一些小的变动):

func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[lenPath:]
if !titleValidator.MatchString(title) {
http.NotFound(w, r)
return
}
fn(w, r, title)
}
}

makeHandler返回的闭包是一个函数,它有两个参数,http.Conn和http.Request(因此,它是http.HandlerFunc)。闭包从请求路径解析title,使用titleValidator验证标题。如果title无效,使用函数http.NotFound将错误写到Conn。如果title有效,封装的处理函数fn将被调用,参数为Conn, Request, 和title。

在main函数中,我们用makeHandler封装所有处理函数:

func main() {
http.HandleFunc("/view/", makeHandler(viewHandler))
http.HandleFunc("/edit/", makeHandler(editHandler))
http.HandleFunc("/save/", makeHandler(saveHandler))
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.String(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

14. 试试!

点击这里查看最终的代码

重新编译代码,运行程序:

$ 8g wiki.go
$ 8l wiki.8
$ ./8.out

访问 http://localhost:8080/view/ANewPage将会出现一个编辑表单。你可以输入一些文版,点击“Save”,重定向到新的页面。

15. 其他任务

这里有一些简单的任务,你可以自己解决:

  • 把模板文件存放在tmpl/目录,页面数据存放在data/目录。
  • 增加一个处理函数(handler),将对根目录的请求重定向到/view/FrontPage。
  • 修饰页面模板,使其成为有效的HTML文件。添加CSS规则。
  • 实现页内链接。将[PageName]修改为<a href="/view/PageName">PageName</a>。(提示:可以使用regexp.ReplaceAllFunc达到这个效果)

本文链接

相关 [go 语言 web] 推荐:

go语言编写Web程序

- - 博客园_首页
创建一个数据类型,含有load和save函数. 基于http包创建web程序. 基于template包的html模板技术. 使用regexp包验证用户输入. web程序的基础技术(HTTP, HTML). 首先,要有一个Linux, OS X, or FreeBSD系统,可以运行go程序. 如果没有的话,可以安装一个虚拟机(如VirtualBox)或者 Virtual Private Server.

采访:关于 Go 语言和《Go Web编程》

- - 开源中国社区最新新闻
最近,在网上出现了一本名为《Go Web编程》的书籍,里面详细地讲述了使用Go语言进行Web编程的各个方面. 很特别的是,这本书是在GitHub上以开源的方式撰写的. 日前,InfoQ采访了这本书的作者谢孟军先生,请他来和大家谈谈Go语言以及他撰写的开源书籍. InfoQ:请您先简单和大家介绍一下自己.

Go 语言初步

- wei - 云风的 BLOG
所谓认真玩,就是拿 Go 写点程序,前后大约两千行吧. 据说 Go 的最佳开发平台是 Mac OS ,我没有. Windows 版还没全部搞定,但是也可以用了. 如果你用 google 搜索,很容易去到一个叫 go-windows 的开源项目上. 如果你用这个,很多库都没有,而且语法也是老的. 我在 Windows 下甚至不能正确链接自己写的多个 package.

谁是最快的Go Web框架

- - 鸟窝
前几天我写了一篇文章: 超全的Go Http路由框架性能比较,利用Julien Schmidt实现的 benchmark测试框架对几乎所有的go web框架的路由功能进行了比较. 我本来以为对Go web框架的性能考察就告以段落了,直到我写了一段简单的代码测试Irsi,用来模拟实际产品中的处理,才发现了Julien Schmidt测试框架的问题.

《学习Go语言》0.4 中文版

- way - python.cn(jobs, news)
鱼哥(https://twitter.com/#!/smallfishxy)上个月勒令我要完成 0.4 版的翻译. 之前公司重组的时候,没顾上看英文版本的更新,结果这老外不声不响的做了如此之多的改动……. 于是只好人工 diff,一条一条的对比 commit 内容. 总算是跟进到了 0.4 这个 tag.

文章: Go语言开发工具LiteIDE

- - InfoQ cn
Go语言最初在2009年11月对外公布,在2011年3月16日发布第一个release,第一个正式版本Go1于2012年3月28日推出. 在Go语言的正式版本推出后,Eclipse、IntelliJ IDEA、vim、emacs、gedit、SublimeText2、Textmate、Textpad、SciTE、Notepad++等IDE和编辑器开始纷纷有了各自的Go语言插件.

为什么放弃Go语言【转载】

- - 编程语言 - ITeye博客
转载地址: http://blog.csdn.net/liigo/article/details/23699459. 有好几次,当我想起来的时候,总是会问自己:我为什么要放弃Go语言. 开门见山地说,我当初放弃Go语言(golang),就是因为两个“不爽”:第一,对Go语言本身不爽;第二,对Go语言社区里的某些人不爽.

Dropbox 开源其 Go 语言库 —— godropbox

- - 开源中国社区最新新闻
Dropbox的成功大部分归功于Python,这个语言可以使我们快速迭代开发. 然而,为了支持日益增长的用户量,我们的基础设施日渐成熟,这时 我们开始寻找一种更为高效的方式来改变系统规模. 大约在一年前,我们作出决定,把对于性能要求很苛刻的后台部分从Python迁移到了Go语言,以提供更 好的并发支持和更快的运行速度.

为什么要使用 Go 语言,Go 语言的优势在哪里?

- - 知乎每日精选
可直接编译成机器码,不依赖其他库,glibc的版本有一定要求,部署就是扔一个文件上去就完成了. 静态类型语言,但是有动态语言的感觉,静态类型的语言就是可以在编译的时候检查出来隐藏的大多数问题,动态语言的感觉就是有很多的包可以使用,写起来的效率很高. 语言层面支持并发,这个就是Go最大的特色,天生的支持并发,我曾经说过一句话,天生的基因和整容是有区别的,大家一样美丽,但是你喜欢整容的还是天生基因的美丽呢.