Go语言入门 之 访问并读取页面

在下边这个程序中,数组中的 url 都将被访问:会发送一个简单的 http.Head() 请求查看返回值;它的声明如下:func Head(url string) (r *Response, err error)

返回的响应 Response 其状态码会被打印出来。

示例 15.7 poll_url.go

package main

import (
	"fmt"
	"net/http"
)

var urls = []string{
	"http://www.google.com/",
	"http://golang.org/",
	"http://blog.golang.org/",
}

func main() {
	// Execute an HTTP HEAD request for all url's
	// and returns the HTTP status string or an error string.
	for _, url := range urls {
		resp, err := http.Head(url)
		if err != nil {
			fmt.Println("Error:", url, err)
		}
		fmt.Println(url, ": ", resp.Status)
	}
}

输出为:

http://www.google.com/ : 302 Found
http://golang.org/ : 200 OK
http://blog.golang.org/ : 200 OK

译者注 由于国内的网络环境现状,很有可能见到如下超时错误提示: Error: http://www.google.com/ Head http://www.google.com/: dial tcp 216.58.221.100:80: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.

在下边的程序中我们使用 http.Get() 获取并显示网页内容; Get 返回值中的 Body 属性包含了网页内容,然后我们用 ioutil.ReadAll 把它读出来:

示例 15.8 http_fetch.go

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)

func main() {
	res, err := http.Get("http://www.google.com")
	checkError(err)
	data, err := ioutil.ReadAll(res.Body)
	checkError(err)
	fmt.Printf("Got: %q", string(data))
}

func checkError(err error) {
	if err != nil {
		log.Fatalf("Get : %v", err)
	}
}

当访问不存在的网站时,这里有一个CheckError输出错误的例子:

2011/09/30 11:24:15 Get: Get http://www.google.bex: dial tcp www.google.bex:80:GetHostByName: No such host is known.

译者注 和上一个例子相似,你可以把google.com更换为一个国内可以顺畅访问的网址进行测试

在下边的程序中,我们获取一个 twitter 用户的状态,通过 xml 包将这个状态解析成为一个结构:

示例 15.9 twitter_status.go

package main

import (
	"encoding/xml"
	"fmt"
	"net/http"
)

/*这个结构会保存解析后的返回数据。
他们会形成有层级的XML,可以忽略一些无用的数据*/
type Status struct {
	Text string
}

type User struct {
	XMLName xml.Name
	Status  Status
}

func main() {
	// 发起请求查询推特Goodland用户的状态
	response, _ := http.Get("http://twitter.com/users/Googland.xml")
	// 初始化XML返回值的结构
	user := User{xml.Name{"", "user"}, Status{""}}
	// 将XML解析为我们的结构
	xml.Unmarshal(response.Body, &user)
	fmt.Printf("status: %s", user.Status.Text)
}

输出:

status: Robot cars invade California, on orders from Google: Google has been testing self-driving cars ... http://bit.ly/cbtpUN http://retwt.me/97p<exit code="0" msg="process exited normally"/>

译者注 和上边的示例相似,你可能无法获取到xml数据,另外由于go版本的更新,xml.Unmarshal 函数的第一个参数需是[]byte类型,而无法传入 Body

我们会在 15.4节 中用到 http 包中的其他重要的函数:

  • http.Redirect(w ResponseWriter, r *Request, url string, code int):这个函数会让浏览器重定向到 url(可以是基于请求 url 的相对路径),同时指定状态码。
  • http.NotFound(w ResponseWriter, r *Request):这个函数将返回网页没有找到,HTTP 404错误。
  • http.Error(w ResponseWriter, error string, code int):这个函数返回特定的错误信息和 HTTP 代码。
  • 另一个 http.Request 对象 req 的重要属性:req.Method,这是一个包含 GET 或 POST 字符串,用来描述网页是以何种方式被请求的。

go为所有的HTTP状态码定义了常量,比如:

http.StatusContinue		= 100
http.StatusOK			= 200
http.StatusFound		= 302
http.StatusBadRequest		= 400
http.StatusUnauthorized		= 401
http.StatusForbidden		= 403
http.StatusNotFound		= 404
http.StatusInternalServerError	= 500

你可以使用 w.header().Set("Content-Type", "../..") 设置头信息。

比如在网页应用发送 html 字符串的时候,在输出之前执行 w.Header().Set("Content-Type", "text/html")(通常不是必要的)。

练习 15.4:扩展 http_fetch.go 使之可以从控制台读取url,使用 12.1节 学到的接收控制台输入的方法(http_fetch2.go

练习 15.5:获取 json 格式的推特状态,就像示例 15.9(twitter_status_json.go

下边的程序在端口 8088 上启动了一个网页服务器;SimpleServer 会处理 url /test1 使它在浏览器输出 hello worldFormServer 会处理 url /test2:如果 url 最初由浏览器请求,那么它是一个 GET 请求,返回一个 form 常量,包含了简单的 input 表单,这个表单里有一个文本框和一个提交按钮。当在文本框输入一些东西并点击提交按钮的时候,会发起一个 POST 请求。FormServer 中的代码用到了 switch 来区分两种情况。请求为 POST 类型时,name 属性 为 inp 的文本框的内容可以这样获取:request.FormValue("inp")。然后将其写回浏览器页面中。在控制台启动程序,然后到浏览器中打开 url http://localhost:8088/test2 来测试这个程序:

示例 15.10 simple_webserver.go

package main

import (
	"io"
	"net/http"
)

const form = `
	<html><body>
		<form action="#" method="post" name="bar">
			<input type="text" name="in" />
			<input type="submit" value="submit"/>
		</form>
	</body></html>
`

/* handle a simple get request */
func SimpleServer(w http.ResponseWriter, request *http.Request) {
	io.WriteString(w, "<h1>hello, world</h1>")
}

func FormServer(w http.ResponseWriter, request *http.Request) {
	w.Header().Set("Content-Type", "text/html")
	switch request.Method {
	case "GET":
		/* display the form to the user */
		io.WriteString(w, form)
	case "POST":
		/* handle the form data, note that ParseForm must
		   be called before we can extract form data */
		//request.ParseForm();
		//io.WriteString(w, request.Form["in"][0])
		io.WriteString(w, request.FormValue("in"))
	}
}

func main() {
	http.HandleFunc("/test1", SimpleServer)
	http.HandleFunc("/test2", FormServer)
	if err := http.ListenAndServe(":8088", nil); err != nil {
		panic(err)
	}
}

注:当使用字符串常量表示 html 文本的时候,包含 <html><body>...</body></html> 对于让浏览器将它识别为 html 文档非常重要。

更安全的做法是在处理函数中,在写入返回内容之前将头部的 content-type 设置为text/htmlw.Header().Set("Content-Type", "text/html")

content-type 会让浏览器认为它可以使用函数 http.DetectContentType([]byte(form)) 来处理收到的数据。

练习 15.6 statistics.go

编写一个网页程序,可以让用户输入一连串的数字,然后将它们打印出来,计算出这些数字的均值和中值,就像下边这张截图一样:

 

确保网页应用健壮

当网页应用的处理函数发生 panic,服务器会简单地终止运行。这可不妙:网页服务器必须是足够健壮的程序,能够承受任何可能的突发问题。

首先能想到的是在每个处理函数中使用 defer/recover,不过这样会产生太多的重复代码。13.5节 使用闭包的错误处理模式是更优雅的方案。我们把这种机制应用到前一章的简单网页服务器上。实际上,它可以被简单地应用到任何网页服务器程序中。

为增强代码可读性,我们为页面处理函数创建一个类型:

type HandleFnc func(http.ResponseWriter, *http.Request)

我们的错误处理函数应用了13.5节 的模式,成为 logPanics 函数:

func logPanics(function HandleFnc) HandleFnc {
	return func(writer http.ResponseWriter, request *http.Request) {
		defer func() {
			if x := recover(); x != nil {
				log.Printf("[%v] caught panic: %v", request.RemoteAddr, x)
			}
		}()
		function(writer, request)
	}
}

然后我们用 logPanics 来包装对处理函数的调用:

http.HandleFunc("/test1", logPanics(SimpleServer))
http.HandleFunc("/test2", logPanics(FormServer))

处理函数现在可以恢复 panic 调用,类似13.5节 中的错误检测函数。完整代码如下:

示例 15.11 robust_webserver.go

package main

import (
	"io"
	"log"
	"net/http"
)

const form = `<html><body><form action="#" method="post" name="bar">
		<input type="text" name="in"/>
		<input type="submit" value="Submit"/>
	</form></html></body>`

type HandleFnc func(http.ResponseWriter, *http.Request)

/* handle a simple get request */
func SimpleServer(w http.ResponseWriter, request *http.Request) {
	io.WriteString(w, "<h1>hello, world</h1>")
}

/* handle a form, both the GET which displays the form
   and the POST which processes it.*/
func FormServer(w http.ResponseWriter, request *http.Request) {
	w.Header().Set("Content-Type", "text/html")
	switch request.Method {
	case "GET":
		/* display the form to the user */
		io.WriteString(w, form)
	case "POST":
		/* handle the form data, note that ParseForm must
		   be called before we can extract form data*/
		//request.ParseForm();
		//io.WriteString(w, request.Form["in"][0])
		io.WriteString(w, request.FormValue("in"))
	}
}

func main() {
	http.HandleFunc("/test1", logPanics(SimpleServer))
	http.HandleFunc("/test2", logPanics(FormServer))
	if err := http.ListenAndServe(":8088", nil); err != nil {
		panic(err)
	}
}

func logPanics(function HandleFnc) HandleFnc {
	return func(writer http.ResponseWriter, request *http.Request) {
		defer func() {
			if x := recover(); x != nil {
				log.Printf("[%v] caught panic: %v", request.RemoteAddr, x)
			}
		}()
		function(writer, request)
	}
}

用模板编写网页应用

以下程序是用 100 行以内代码实现可行的 wiki 网页应用,它由一组页面组成,用于阅读、编辑和保存。它是来自 Go 网站 codelab 的 wiki 制作教程,我所知的最好的 Go 教程之一,非常值得进行完整的实验,以见证并理解程序是如何被构建起来的(https://golang.org/doc/articles/wiki/)。这里,我们将以自顶向下的视角,从整体上给出程序的补充说明。程序是网页服务器,它必须从命令行启动,监听某个端口,例如 8080。浏览器可以通过请求 URL 阅读 wiki 页面的内容,例如:http://localhost:8080/view/page1

接着,页面的文本内容从一个文件中读取,并显示在网页中。它包含一个超链接,指向编辑页面(http://localhost:8080/edit/page1)。编辑页面将内容显示在一个文本域中,用户可以更改文本,点击“保存”按钮保存到对应的文件中。然后回到阅读页面显示更改后的内容。如果某个被请求阅读的页面不存在(例如:http://localhost:8080/edit/page999),程序可以作出识别,立即重定向到编辑页面,如此新的 wiki 页面就可以被创建并保存。

wiki 页面需要一个标题和文本内容,它在程序中被建模为如下结构体,Body 字段存放内容,由字节切片组成。

type Page struct {
	Title string
	Body  []byte
}

为了在可执行程序之外维护 wiki 页面内容,我们简单地使用了文本文件作为持久化存储。程序、必要的模板和文本文件可以在 wiki 中找到。

示例 15.12 wiki.go

package main

import (
	"net/http"
	"io/ioutil"
	"log"
	"regexp"
	"text/template"
)

const lenPath = len("/view/")

var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+$")
var templates = make(map[string]*template.Template)
var err error

type Page struct {
	Title string
	Body  []byte
}

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

func main() {
	http.HandleFunc("/view/", makeHandler(viewHandler))
	http.HandleFunc("/edit/", makeHandler(editHandler))
	http.HandleFunc("/save/", makeHandler(saveHandler))
	err := http.ListenAndServe("localhost:8080", nil)
	if err != nil {
		log.Fatal("ListenAndServe: ", err.Error())
	}
}

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

func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
	p, err := load(title)
	if err != nil { // page not found
		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 := load(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)
}

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

func (p *Page) save() error {
	filename := p.Title + ".txt"
	// file created with read-write permissions for the current user only
	return ioutil.WriteFile(filename, p.Body, 0600)
}

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

让我们来通读代码:

  • 首先导入必要的包。由于我们在构建网页服务器,http 当然是必须的。不过还导入了 io/ioutil 来方便地读写文件,regexp 用于验证输入标题,以及 template 来动态创建 html 文档。

  • 为避免黑客构造特殊输入攻击服务器,我们用如下正则表达式检查用户在浏览器上输入的 URL(同时也是 wiki 页面标题):

    var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+$")

    makeHandler 会用它对请求管控。

  • 必须有一种机制把 Page 结构体数据插入到网页的标题和内容中,可以利用 template 包通过如下步骤完成:

    1. 先在文本编辑器中创建 html 模板文件,例如 view.html:
     <h1>{{.Title |html}}</h1>
     <p>[<a href="/edit/{{.Title |html}}">edit</a>]</p>
     <div>{{printf "%s" .Body |html}}</div>

    把要插入的数据结构字段放在 {{ 和 }} 之间,这里是把 Page 结构体数据 {{.Title |html}} 和 {{printf "%s" .Body |html}} 插入页面(当然可以是非常复杂的 html,但这里尽可能地简化了,以突出模板的原理。)({{.Title |html}} 和 {{printf "%s" .Body |html}} 语法说明详见后续章节)。

    1. template.Must(template.ParseFiles(tmpl + ".html")) 把模板文件转换为 *template.Template 类型的对象,为了高效,在程序运行时仅做一次解析,在 init() 函数中处理可以方便地达到目的。所有模板对象都被保持在内存中,存放在以 html 文件名作为索引的 map 中:
     templates = make(map[string]*template.Template)

    此种技术被称为模板缓存,是推荐的最佳实践。

    1. 为了真正从模板和结构体构建出页面,必须使用:
     templates[tmpl].Execute(w, p)

    它基于模板执行,用 Page 结构体对象 p 作为参数对模板进行替换,并写入 ResponseWriter 对象 w。必须检查该方法的 error 返回值,万一有一个或多个错误,我们可以调用 http.Error 来明示。在我们的应用程序中,这段代码会被多次调用,所以把它提取为单独的函数 renderTemplate

  • 在 main() 中网页服务器用 ListenAndServe 启动并监听 8080 端口。但正如 15.2节 那样,需要先为紧接在 URL localhost:8080/ 之后, 以viewedit 或 save 开头的 url 路径定义一些处理函数。在大多数网页服务器应用程序中,这形成了一系列 URL 路径到处理函数的映射,类似于 Ruby 和 Rails,Django 或 ASP.NET MVC 这样的 MVC 框架中的路由表。请求的 URL 与这些路径尝试匹配,较长的路径被优先匹配。如不与任何路径匹配,则调用 / 的处理程序。

    在此定义了 3 个处理函数,由于包含重复的启动代码,我们将其提取到单独的 makeHandler 函数中。这是一个值得研究的特殊高阶函数:其参数是一个函数,返回一个新的闭包函数:

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)
	}
}
  • 闭包封闭了函数变量 fn 来构造其返回值。但在此之前,它先用 titleValidator.MatchString(title) 验证输入标题 title 的有效性。如果标题包含了字母和数字以外的字符,就触发 NotFound 错误(例如:尝试 localhost:8080/view/page++)。viewHandlereditHandler 和 saveHandler 都是传入 main() 中 makeHandler 的参数,类型必须都与 fn 相同。

  • viewHandler 尝试按标题读取文本文件,这是通过调用 load() 函数完成的,它会构建文件名并用 ioutil.ReadFile 读取内容。如果文件存在,其内容会存入字符串中。一个指向 Page 结构体的指针按字面量被创建:&Page{Title: title, Body: body}

    另外,该值和表示没有 error 的 nil 值一起返回给调用者。然后在 renderTemplate 中将该结构体与模板对象整合。

    万一发生错误,也就是说 wiki 页面在磁盘上不存在,错误会被返回给 viewHandler,此时会自动重定向,跳转请求对应标题的编辑页面。

  • editHandler 基本上也差不多:尝试读取文件,如果存在则用“编辑”模板来渲染;万一发生错误,创建一个新的包含指定标题的 Page 对象并渲染。

  • 当在编辑页面点击“保存”按钮时,触发保存页面内容的动作。按钮须放在 html 表单中,它开头是这样的:

    <form action="/save/{{.Title |html}}" method="POST">

    这意味着,当提交表单到类似 http://localhost/save/{Title} 这样的 URL 格式时,一个 POST 请求被发往网页服务器。针对这样的 URL 我们已经定义好了处理函数:saveHandler()。在 request 对象上调用 FormValue() 方法,可以提取名称为 body 的文本域内容,用这些信息构造一个 Page 对象,然后尝试通过调用 save() 方法保存其内容。万一运行失败,执行 http.Error 以将错误显示到浏览器。如果保存成功,重定向浏览器到该页的阅读页面。save() 函数非常简单,利用 ioutil.WriteFile(),写入 Page 结构体的 Body 字段到文件 filename 中,之后会被用于模板替换占位符 {{printf "%s" .Body |html}}