GO语言(十九)编写Web应用程序(上)
介绍
本教程涵盖: 创建具有加载和保存方法的数据结构 使用该net/http包构建 Web 应用程序 使用html/template包处理 HTML 模板 使用regexp包验证用户输入 使用闭包
需要知识: 编程经验 了解基本的网络技术(HTTP、HTML) 一些 UNIX/DOS 命令行知识
入门
目前,您需要有一台 FreeBSD、Linux、macOS 或 Windows 机器来运行 Go。
在你的GOPATH和 cd 中为本教程创建一个新目录: $ mkdir gowiki $ cd gowiki
创建一个名为 的文件wiki.go,在您喜欢的编辑器中打开它,然后添加以下行: package main import ( "fmt" "os" )
我们从 Go 标准库中 导入fmt和os包。稍后,当我们实现附加功能时,我们将在此import声明中添加更多包。
数据结构
让我们从定义数据结构开始。一个 wiki 由一系列相互关联的页面组成,每个页面都有一个标题和一个正文(页面内容)。在这里,我们定义Page为一个结构体,其中包含两个字段,分别代表标题和正文。 type Page struct { Title string Body []byte }
该类型的[]byte意思是byte切片。Body元素是 []byte类型而不是 string类型,因为这是 我们将使用的io库所期望的类型。
该Page结构描述了页面数据将如何存储在内存中。但是持久存储呢?我们可以通过在 上创建一个 save方法来解决这个问题Page: func (p *Page) save() error { filename := p.Title + ".txt" return os.WriteFile(filename, p.Body, 0600) }
这是一个名为save的方法,它的接收者p是一个指向的指针Page。它不接受任何参数,并返回一个error类型的值。
此方法会将Page"s保存Body到文本文件中。为简单起见,我们将使用Title作为文件名。
该save方法返回一个error值,因为这是WriteFile(将字节切片写入文件的标准库函数)的返回类型。该save方法返回错误值,让应用程序在写入文件时出现任何问题时处理它。如果一切顺利,Page.save()将返回 nil(指针、接口和其他一些类型的零值)。
八进制整数文字0600,作为第三个参数传递给 WriteFile,表示创建文件时应仅对当前用户具有读写权限。
除了保存页面,我们还需要加载页面: func loadPage(title string) *Page { filename := title + ".txt" body, _ := os.ReadFile(filename) return &Page{Title: title, Body: body} }
该loadPage函数从 title 参数构造文件名,将文件的内容读入一个新变量body,并返回一个指向Page由正确的标题和正文值构造的文字的指针。
函数可以返回多个值。标准库函数 os.ReadFile返回[]byte和error。在loadPage中,尚未处理错误;下划线 ( ) 符号表示的"空白标识符"_用于丢弃错误返回值(本质上,将值赋值为空)。
但是如果ReadFile遇到错误会发生什么?例如,该文件可能不存在。我们不应该忽视这样的错误。让我们修改函数以返回*Page和error。 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 }
该函数的调用者现在可以检查第二个参数;如果是, 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. 然后将文件读入 struct p2,并将其Body元素打印到屏幕上。
您可以像这样编译和运行程序: $ go build wiki.go $ ./wiki This is a sample Page.
介绍net/http包装
这是一个简单 Web 服务器的完整工作示例: //go: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 的调用开始 ,它告诉http包使用handler处理对 Web 根 "/"的所有请求。
然后它调用http.ListenAndServe,指定它应该在任何接口 ( ":8080") 上侦听端口 8080。(暂时不要担心它的第二个参数nil。)这个函数将一直阻塞,直到程序终止。
ListenAndServe总是返回一个错误,因为它只在发生意外错误时返回。为了记录该错误,我们将函数调用用log.Fatal.
handler函数的类型为http.HandlerFunc。它以 http.ResponseWriter和 anhttp.Request作为参数。
http.ResponseWriter值组合了 HTTP 服务器的响应;通过写入它,我们将数据发送到 HTTP 客户端。
http.Request是表示客户端 HTTP 请求的数据结构。r.URL.Path是请求 URL 的路径组件。 [1:] 意味着"创建从第一个字符到结尾的子切片"。
如果您运行此程序并访问 URL: http://localhost:8080/monkeys
该程序将显示一个页面,其中包含: Hi there, I love monkeys!
使用net/http包创建wiki页面
要使用net/http包,必须将其导入: import ( "fmt" "os" "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, "%s
%s", p.Title, p.Body) }
再次注意使用_忽略error 来自loadPage的返回值。这是为了简单起见,通常被认为是不好的做法。我们稍后会处理这个问题。
首先,此函数从请求 URL 的路径组件r.URL.Path中提取页面标题。Path重新切片以删除请求路径的前导"/view/"。这是因为路径总是以"/view/"开头,它不是页面标题的一部分。
然后该函数加载页面数据,用一串简单的 HTML 格式化页面,并将其w写入http.ResponseWriter.
要使用这个处理程序,我们重写我们的main函数来使用viewHandler处理/view/路径下的任何请求。 func main() { http.HandleFunc("/view/", viewHandler) log.Fatal(http.ListenAndServe(":8080", nil)) }
让我们创建一些页面数据,编译我们的代码,并尝试提供一个 wiki 页面。
在编辑器中打开test.txt文件,并在其中保存字符串"Hello world"(不带引号)。 $ go build wiki.go $ ./wiki
随着这个网络服务器的运行,访问http://localhost:8080/view/test 应该会显示一个标题为"test"的页面,其中包含"Hello world"这个词。