通向Golang的捷径【15. 网络, 模板和 web 应用】

Go 语言最适合编写 web 应用, 因为它没有提供 GUI 框架, 所以只能将包含字符串或模板的 HTML, 作为应用的输出窗口.

15.1 tcp 服务器

在本节中, 将使用 TCP 协议和并发协程, 开发一个简单的客户端-服务器应用, 一个 (web) 服务器应用需响应多个客户端的并发请求, 在 Go 语言中, 每个客户端请求都将生成一个并发协程, 并对请求进行处理, 同时还需要 net 包, 其中实现了网络的通讯功能, 并包含了支持 TCP/IP 和 UDP 协议, 域名解析等功能的方法, 以下示例给出了所需的服务器代码,

例 15.1 server.go

在这里插入图片描述
在这里插入图片描述
在 main 函数中, 创建了一个 net.Listener 变量 listener, 它可实现服务器的基本功能, 监听来自于客户端的连接请求, 并接受该请求 (在本机状态下, 基于 ip 地址 127.0.0.1 及端口 50000, 可通过 TCP 协议模拟一个客户端连接请求), Listen() 函数可返回一个 err 变量 (error 类型), 之后将在一个死循环中, 使用 listener.Accept()等待客户端的连接请求, 并能接受该请求, 使用 net.Conn 类型变量 conn 可创建一个客户端连接请求, 建立客户端和服务器的连接后, 将启动一个并发协程 doServerStuff(), 其中可将读入的网络数据, 放入一个 512 字节的缓冲中, 并会在服务器终端上, 显示这些网络数据, 当来自于客户端的所有数据读取完毕, 该并发协程也将终止运行, 同时会为每个客户端, 生成一个独立的并发协程, 而服务器代码的运行必须早于客户端代码, 以下是客户端代码,

例 15.2 client.go

在这里插入图片描述
通过 net.Dial 可产生发送给服务器的客户端连接请求, 在上述示例中, 可在死循环中, 接收键盘命令 (os.Stdin),按下 Q 键, 可使客户端程序终止, 之后传输数据将使用网络连接的写入方法, 发送给服务器.

首先服务器程序必须首先启动, 如果服务器无法监听, 也就不能建立与客户端的网络连接, 如果客户端工作时,服务器还未处于监听状态, 客户端将终止运行, 并给出一个错误消息,
在这里插入图片描述
在控制台界面下, 首先可启动服务器程序 (Windows 为 server.exe,Linux 为./server), 这时将出现一条消息, Starting the server …, 之后可开启多个控制台界面, 可分别启动多个客户端, 并发送一些消息, 这时在服务器控制台中, 将出现以下的传输消息:
在这里插入图片描述
当一个客户端终止运行, 服务器控制台可输出以下消息:
在这里插入图片描述
net.Dial 是网络通讯中比较重要的一个函数, 当调用该函数, 与远端系统建立连接后, 可返回一个 conn 接口类型, 利用 conn 可实现数据的发送和接收, 同时该函数实现了必要的抽象, 因此 IPv4, IPv6, TCP, UDP 都可共享一个通用接口, 也就是 net.Dial 函数都支持上述协议, 以下示例将在 80 端口上, 分别使用 TCP, UDP, IPv6-TCP 协议, 向远端系统发送连接请求.

例 15.3 dial.go

在这里插入图片描述
以下示例将给出 net 包的一些功能, 即打开 soket, 基于 socket 实现的写入和读取.

例 15.4 socket.go

在这里插入图片描述
以下示例对 server.go 进行了一些的改进,

例 15.5 simple_tcp_server.go

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上例给出的改进如下:
• 服务器的地址和端口不能直接写入程序中, 应基于 flag 包, 从命令行中读取, 如果未读取到服务器地址和端口,flag.NArg() 将产生一个告警消息. 同时这两个参数将在 fmt.Sprintf 函数中, 被放入一个格式化字符串中.
• 传入 initServer 函数的服务器地址和端口, 将被 net.ResolveTCPAddr 方法所解析, 同时 initServer 函数还可返回一个 *net.TCPListener
• 在每一个连接出现时, 都将启动一个并发协程, 并调用 connectionHandler 函数, conn.RemoteAddr() 将提供客户端的地址
• 通过 conn.Write 函数, 将发送一个消息给客户端
• 可从客户端中读取 25 个字节数据, 并打印出这些数据, 如果在死循环中, 读取出现错误, 则通过 default
子句, 退出死循环, 并关闭客户端的连接, 如果 OS 发出了一个 EAGAIN(重新操作) 错误, 则会重新读取
• 所有的错误检查将集中在一个 checkError 函数中, 当出现错误时, 该函数发布一个故障, 并给出与运行
环境有关的错误消息

在控制台中, 可使用以下命令, 启动一个服务器程序:
在这里插入图片描述
同时还可在其他的控制台中, 启动多个客户端, 在以下示例中, 服务器连接了两个客户端, 每个客户端都有自己的地址:
在这里插入图片描述
net 包可返回错误 (error 类型), 并能提供常见的错误定义, 而通过 net.Error 接口, 可自定义其他的错误类型(这些类型可包含自己的方法).
在这里插入图片描述
使用一个类型检查, 可测试 net.Error 包含的客户端数据, 之后可分析网络错误的一般原因, 比如 web 爬虫进入睡眠状态, 或是在遇到一个临时错误, 它可进行重新尝试, 又或者选择放弃.
在这里插入图片描述
在这里插入图片描述

15.2 简单的 web 服务器

http 是 tcp 的上层协议, 它描述了 web 服务器与浏览器客户端的通讯, 在 Go 语言中, 包含了 net/http 包, 以下将使用一些示例, 来讨论 http 包的用法, 首先是一个可发送 Hello world! 消息的 web 服务器.

首先需导入 http 包, 再使用 net.Listen(”tcp”,”localhost:50000”) 函数, 实现 web 服务器的监听功能, 之后再使用 http.ListenAndServe(”localhost:8080”, nil) 函数, 实现 http 端口 (8080) 的监听, 如果执行成功, 可返回nil, 如果省略服务器地址 (即 localhost), 将返回一个错误.

使用 http.URL 类型, 可描述一个 web 地址, 该类型的 Path 数据域, 可包含一个 url 字符串, 而使用 http.Request类型可描述一个客户端请求, 在该类型中, 也包含了一个 URL 数据域.

如果请求 req 是一个 html 格式的 POST 命令, 在 html 格式中, 将包含一个名为 var1 的 html 输入数据域, 因此在 Go 代码中, 可使用该名称, 即 req.FormValue(”var1”), 参见 15.4 节, 如果使用另一种编程方式, 可先调用 request.ParseForm(), 再使用 request.Form[”var1”] 可获取 var1 值, 如下:
在这里插入图片描述
如果获取到 var1 值,found 将返回 true, 否则 found 将返回 false.

Form 数据域是一个 map[string][]string 类型,web 服务器可发送一个 http.Response, 同时它将被放入http.ResponseWriter 对象, 该对象可视为 http 服务器的响应器, 对其进行写入, 可发送数据给 http 客户端.

web 服务器必须给出客户端请求的处理, 这类处理将放置在 http.HandleFunc 函数类型中, 在以下示例中, 如果请求 web 的根页面 (http://localhost:8080) 或是其他页面时, 将调用 HelloServer 函数, 该函数的类型为http.HandlerFunc, 在多数情况下, 这类函数将被命名为 Prefhandler(其中加入了前缀 Pref). http.HandleFunc可注册一个处理器函数 (本例为 HelloServer), 它用于处理 web 根页面的请求.

除了 web 根页面之外, 还可请求其他页面, 比如/create,/edit 等, 对于不同的 URL, 可定义不同的处理器函数,而处理器函数中可包含两个形参,ResponseWriter 和请求 req, 而 ResponseWriter 可写入一个字符串 (其中将包含 Hello 和 r.URL),req 中给出的 Path[1:], 可获得一个从第一个字符元素开始, 到数组末尾的路径 slice, 这等同于删除路径名之前的 /字符, 获得的路径 slice 可使用 fmt.Fprintf(), 写入 w, 这类似于 io.WriteString(w,”hello, world!\n”), 因此处理器函数的第一个形参为已请求的 web 路径, 第二个形参为调用函数的引用 (当web 页面被请求时, 将调用该函数).

例 15.6 hello_world_webserver.go

在这里插入图片描述
在这里插入图片描述
可在控制台中, 启动上述的 web 服务器程序, 之后将显示以下信息:
在这里插入图片描述
之后打开浏览器, 输入 URL 地址http://localhost:8080/world 并回车, 这时在浏览器窗口中, 将显示文本 Hello, world, 可尝试输入地址 http://localhost:8080/, 看看 web 服务器可提供什么信息.

fmt.Println 可在服务器控制台中打印信息, 这可作为每个请求处理器的日志信息.

上例的改进

• 前两行代码可替换成一行代码 (未包含错误处理):
在这里插入图片描述
• 使用 fmt.Fprint 或 fmt.Fprintf, 可实现一个格式化字符串 (需传输的消息), 之后字符串可写入 http.
ResponseWriter(由 io.Writer 实现), 如下:
在这里插入图片描述
• 为了实现 http 的安全性, 可使用 http.ListenAndServeTLS() 替换 http.ListenAndServe()
• 替换http.HandleFunc(”/”, HFunc), 处理器函数 HFunc 的原型如下:
在这里插入图片描述
因此也可使用http.Handle(”/”, http.HandlerFunc(HFunc)), 而 HandlerFunc 是一个类型名, 它的原型如
下:
在这里插入图片描述
该类型可封装一个 http 处理器函数, 如果 f 为处理器函数,HandlerFunc(f) 可构成一个调用 f 的处理器
对象, 而 http.Handle 的第二个形参, 可传入一个 T 类型的对象 obj, 比如http.Handle(”/”, obj), 这时 T
将是一个 ServeHTTP 方法, 该方法可实现 http 的 Handler(处理器) 接口,
在这里插入图片描述
在 15.8 节的 web 服务器中, 上述方法将用于 Counter 和 Chan 类型, 因此 http 包将提供 http.Handler,
服务于 http 请求.

15.3 站点轮询和读取 web 页面

以下程序将对数组中包含的所有 URL 进行轮询, 一个简单的 http.Head() 请求, 会发送给所有 URL, 该请求函数的原型为 func Head(url string) (r *Response, err error), 之后将打印出 URL 的响应状态.

例 15.7 poll_url.go

在这里插入图片描述
在这里插入图片描述
在以下示例中, 将使用 http.Get() 获取一个 web 页面的内容, 因此从 Get 返回的响应数据中, 将在 Body 数据域包含 web 页面的数据, 之后可使用 ioutil.ReadAll 进行读取:

例 15.8 http_fetch.go在这里插入图片描述

当访问的 web 页面并不存在,CheckError 将输出一个简单的错误信息:
在这里插入图片描述
在以下示例中, 将获取特定用户的 twitter 状态, 而特定用户的状态信息, 可利用 xml 包的功能, 放入到一个结构中.

例 15.9 twitter_status.go

在这里插入图片描述
在 15.4 节中, 将会用到 http 包的一些函数, 如下:
• http.Redirect(w ResponseWriter, r *Request, url string, code int)
可将浏览器重定向到提供的 url 中 (请求路径的相对路径), 并需提供一个状态码.

• http.NotFound(w ResponseWriter, r *Request)
使用 HTTP 404(未找到页面错误) 作为请求的响应消息.

• http.Error(w ResponseWriter, error string, code int)
使用特殊的错误消息和 HTTP 状态码, 作为请求的响应.

• http.Request
可包含一个字符串 (比如 GET 或 POST), 用于标记 web 页面的不同请求.

所有的 HTTP 状态码都是 Go 语言的常量, 如下:
在这里插入图片描述
在这里插入图片描述
同时还可使用w.Header().Set(”Content-Type”, ”…/…”), 设定 web 页面的头部 (header), 在 web 应用中, 如果需要发送 html 格式的字符串时, 可在发送之前, 使用 w.Header().Set(”Content-Type”,”text/html”) 进行设定.

15.4 编写简单的 web 应用

在以下示例中, 将 8088 端口上启动 web 服务器, 如果请求 url/test1 页面, 将提交给 SimpleServer 处理器, 并会在浏览器中输出 hello world 字符串, 如果请求 url/test2 页面, 将提交给 FormServer 处理器, 即浏览器内部可生成了一个 url 请求, 该请求将由 GET 方法提供, 而服务器给出的响应 (FormServer 的处理结果), 则是包含了一个文本框和一个发送 (submit) 按钮的 html(它将显示在浏览器中), 如果在文本框中输入文本, 并点击发送按钮, 这时将再次发送一个 POST 请求, 而 FormServer 处理器将使用 switch 语句, 区分上述的两种请求, 在POST 请求的处理中, 文本框内容将放入 inp, 即 request.FormValue(”inp”), 并能返回到浏览器的页面中, 基于控制台, 可启动以下的 web 服务器程序, 并可打开一个浏览器, 并输入 url 地址http://localhost:8088/test2,查看 FormServer 处理器的处理结果.

例 15.10 simple_webserver.go

在这里插入图片描述
在这里插入图片描述
当使用字符串实现一个 html 页面时, 应当包含 … 标记, 以使浏览器可显示该 html 页面, 为了保证 html 的安全性, 可在服务器响应写入处理器之前, 在页面中, 添加页面头, 如下:
在这里插入图片描述

15.5 web 应用的健壮性

当 web 应用的一个处理器函数出现故障时,web 服务器可简单终止, 但这并不是最好的处理方式, web 服务器必须是一个健壮的应用程序, 能够抵御一些临时问题.

首先可在每个处理器函数中, 使用 defer/recover 语句, 但它们会引入更多的重复代码, 因此使用 13.5 节给出的错误处理方案, 可使代码更优雅, 所以可在上一节的简单 web 服务器中, 使用上述机制, 为了保证代码的可读性, 将创建一个函数类型 (用于页面处理函数):
在这里插入图片描述
13.5 节给出的 errorHandler 函数, 这里变为 logPanics 函数,
在这里插入图片描述
因此在 logPanics 函数中, 封装了处理器函数的调用,
在这里插入图片描述
这时处理器函数可包含 panic 调用或是一个错误检查函数, 如 13.5 节, 在以下示例中, 包含了上述机制,

例 15.11 robust_webserver.go

在这里插入图片描述
在这里插入图片描述

15.6 web 应用的模板

在以下示例中, 将包含一个与 wiki 类似的 web 应用, 在 100 行的代码中, 将实现 web 页面的查看, 编辑和保存, 它来自于 Go 官网的代码实验室 wiki 教程, 该教程也是最好的 Go 语言教程之一, 同时值得耗费时间, 去查看教程中给出的完整代码, 以便更好地理解程序的意图 ((http://golang.org/doc/codelab/wiki/), 这里将使用自顶而下的顺序, 对以下程序进行更完整的论述, 首先该程序是一个 web 服务器, 它必须在控制台中首先运行, 端口号为 8080, 同时在浏览器中, 可输入 url 地址http://localhost:8080/view/page1, 以查看 wiki 页面.

页面的文本来自于一个文件, 并能显示成一个 web 页面, 其中包含了一个超链接, 可对 wiki 页面 (http://localhost:8080/edit/page1) 进行编辑, 在编辑页面中, 包含了一个文本框, 用户可在此处进行文本的修改, 并能按下发送按钮 Save, 将修改结果保存在文件中, 之后修改完成的页面将显示在浏览器中, 如果出现一个页面不存在的通告 (如http://localhost:8080/edit/page999), 服务器程序可识别该错误, 并能使浏览器重定向到编辑页面,同时还可创建和保存新的 wiki 页面.

在 wiki 页面中, 需要一个标题和内容文本 (即一个名为 Body 的字节 slice), 它可基于以下结构完成构建,
在这里插入图片描述
为了在服务器程序之外, 对 wiki 页面进行维护, 可使用一些简单的文本文件作为存储介质, 以下程序和对应的模板, 以及文本文件, 都能在随书的源码包中找到 (code_examples\chapter 15\wiki).

例 15.12 wiki.go

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
以下将给出上述代码的功能说明:
• 首先导入所需的包, 构建一个 web 服务器, 应导入 http 包, 实现文件的读写, 应导入 io/ioutil 包, regexp
包可验证输入的有效性,template 包可动态创建 html,os 包可提供错误定义.

• 为了规避黑客对服务器的伤害, 在浏览器中, 需要对用户输入的 url 进行检查 (比如 wiki 页面的标题), 这
时可使用一个正则表达式, 该表达式可放入 makeHandler 函数.
在这里插入图片描述
• 必须提供一种机制, 可将自定义的 Page(页面) 结构, 插入到 web 页面的标题和目录中, 使用 template
包, 可实现该结构.
▶ 在编辑器中, 创建 html 模板文件, 比如 view.html
在这里插入图片描述
因此上述结构的数据域可插入到{{}} 之间, 这里是{{.Title |html}} 和 {{printf ”%s” .Body|html}},来自 Page 结构的数据域可插入其中, 当然也可实现相当复杂的 html, 但这里进行了简化, 并给出了基本用法.

▶ template.Must(template.ParseFiles(tmpl + ”.html”)) 函数可将 html 文件, 传递给 *template.Template, 为了效率的原因, 服务器程序会将 html 文件进行一次性解析, 所以 init() 函数是最佳的位置, 这时模板对象将包含在内存的 map 结构中, 并能使用 html 文件名作为索引, 即templates = make(map[string]
*template.Template), 这类技术也称为模板缓存, 也是一种相当实用的技术.

▶ 为了创建模板之外的页面和结构, 必须使用templates[tmpl].Execute(w, p) 函数, 它基于模板, 完成所需的操作, 同时将给出 Page 结构 p 作为模板的参数, 还能将处理结果写入 ResponseWriter w, 该函数返回后, 必须进行错误检查, 如果出现错误, 可调用 http.Error 进行通告, 在应用中将多次调用上述代码, 所以也可进行抽取, 并放入一个独立函数 renderTemplate 中.

• 在 main() 函数中, 将启动 web 服务器, 并给出端口 8080 的监听, 同时还定义了一些处理器函数, 以处理localhost:8080 下层的 view,edit 和 save 子页面的请求, 在大多数的 web 服务器中, 不同的处理器将对应不同的 url 路径, 这类似于 MVC 框架的路由表 (比如 Rudy,Rails,Django 和 ASP.NET 的 MVC 框架),
如果请求路径与预定的 url 路径匹配, 可调用对应的处理器, 如果请求路径未实现匹配, 根路径的处理器
将被调用.

上例中定义了三个处理器, 为了防止局部代码的重复出现, 这里使用了 makeHandler 函数进行封装, 这
是一个相当高级的函数, 值得进一步的探索, 该函数的第一个参数, 即为处理器函数, 并能返回该函数.
在这里插入图片描述
• 给出函数变量 fn, 是为了提供所需的返回值, 但在返回处理器函数之前, 需使用titleValidator.MatchString(title), 对输入的 title 进行验证, 如果标题中未包含字母和数字时, 将通告一个 NotFound 错误.

使用 url 地址 (localhost:8080/view/page++), 可测试 view 处理器,edit 处理器和 save 处理器, 它们都是
makeHandler 参数, 且皆为 fn 类型.

• view 处理器可基于给定的标题, 读取一个文本文件, 通过 load() 函数 (需给出文件名) 可载入文件, 使用
ioutil.ReadFile 可实现文件的读取, 如果文件被找到, 它的内容将被送入 Body 中, 这时可构建一个 Page
结构, 即&Page{Title: title, Body: body}.

如果出现错误, 则表示请求的 wiki 页面未包含在磁盘中,viewHandler() 将返回该错误, 这时将自动重定
向, 并给出 edit 页面的请求.

• edit 处理器的操作基本相似, 尝试载入文件, 如果找到文件, 将使用文件内容, 构建 edit 模板, 之后使用
模板和标题, 可创建一个新的 Page 对象, 这时有可能出现错误.

• 在 edit 页面中, 点击 Save 按钮, 则可进入保存页面, 该按钮放置在 html 表格中, 如下:
在这里插入图片描述
这意味着 url 地址 (http://localhost/save/{Title}, 模板可替换 Title) 的 post 请求, 会发送给 web 服务器, 因此也会定义一个处理器函数 saveHandler(), 来处理这类请求, 而请求的 FormValue() 方法, 可获取名为 body 的文本内容, 使用这些信息, 可构建一个 Page 对象, 之后使用 save() 函数, 存储这些信息,如果出现错误, 将返回一个 http.Error 错误, 并会显示在浏览器中, 当上述操作成功完成, 浏览器将重定向到之前的同名页面中,save() 函数很简单, 也就是将 Page 结构的 Body 数据域, 写入一个文件 (文件名为{{ printf ”%s” .Body|html}}) 中, 而底层写入将由 ioutil.WriteFile() 实现.

15.7 template 包

template 包的文档可参考页面http://golang.org/pkg/template/, 在之前的小节中, 可使用 template 包, 将结构类型中的数据, 合并成一个 html 模板, 这对于构建 web 应用来说, 该功能极具价值, 但模板技术更通用的目的在于, 利用数据驱动模式, 以生成文本花输出, HTML 只是其中的一个特例.

模板将合并结构类型中的数据, 在大多数情况下, 可以是一个结构或是一组结构 (即一个结构 slice), 从结构数据域中获取的数据, 可传递给 templ.Execute(), 以生成一段文本, 在模板的合并操作中, 只能使用可导出的数据元素, 其中可包含数值计算或控制语句, 但必须使用{{和}} 进行封闭, 同时数据元素可以是数值或指针, 又或是隐藏在接口中的间接数据.

15.7.1 插入的数据域 ({{.F ieldN ame}})

为了在模板中, 插入结构的数据域, 需要使用双花括号封闭数据域, 并在数据域名的开头, 添加一个点符号 (.),如果结构数据域为 Name, 它的数值需插入到模板中, 那么在模板中需使用 {{.Name}} 文本, 如果 Name 是map 类型的一个 key(键值), 上述的文本格式也能工作, 使用 template.New 可创建一个新模板, 并能包含一个字符串参数 (它将作为模板名), 以下操作已在 15.5 节中给出了实现, 之后使用 Parse 方法, 可生成一个模板, 它将解析一些模板定义的字符串, 生成一个内部描述, 或者使用 ParseFile(将包含一个文件路径的参数) 解析一个文件, 该文件中包含了模板定义, 如果解析出现问题, 可检查上述函数的第二个返回值是否为空, 如Error != nil,最后将使用 Execute 方法, 将结构类型的数值, 合并到模板中, 该方法的第一个形参为 io.Writer, 它还可返回一个错误码, 在以下示例中, 可通过 os.Stdout, 将输出信息打印在控制台中.

例 15.13 template_field.go

在这里插入图片描述
在上述示例的结构类型中, 包含了一个无法导出的数据域, 使用以下语句, 可尝试合并该数据域,
在这里插入图片描述
这时将出现一个错误,
在这里插入图片描述
如果需要使用简化操作, 可在 Execute() 的第二个参数中, 使用{{.}}, 也就是将结构类型中所有可导出的所需数据域, 都合并到模板中. 如果模板需显示在浏览器中, 首个过滤器即为 html 过滤器, 即{{html .}} 或者是一个数据域 (FieldName) 过滤器,{{ .FieldName |html }}, |html 可告知模板引擎, 基于 html 格式, 将 FieldName数值, 合并到模板, 也就是转义一些特殊的 HTML 字符, 比如将 > 替换成> 字串, 从而避免 HTML 格式对用户数据的影响.

15.7.2 模板的验证

为了检查模板定义的语法是否正确, 可将 Pase 的处理结果, 传递给 Must 函数, 在以下示例中, tOk 模板是正确的, 而 tErr 模板将出现一个错误, 并会引发一个运行时故障.

例 15.14 template_validation.go

在这里插入图片描述
模板语法中会出现错误的情况, 并不常见, 但可使用 defer/recover 机制, 来报告这类错误. 在实际的代码中, 通常会连用以上的三个函数, 如下:
在这里插入图片描述

15.7.3 if-else

Execute 函数可基于输入的静态文本, 输出一个模板, 而文本将封闭在{{ }} 符号中, 在 Go 语言中, 它将被称为管道 (pipeline), 如下:

例 15.15 pipeline1.go

在这里插入图片描述
使用 if-else-end 控制语句, 可对文本管道进行控制, 如果文本管道为空, 如{{if “}} 无须打印的文本. {{end}},如果 if 条件的检查结果为 false, 将不会打印后续的文本, 比如:
在这里插入图片描述

例 15.16 template_ifelse.go

在这里插入图片描述
在这里插入图片描述

15.7.4 点符号 (.) 和 with-end 语句

在 Go 模板中, 可使用点符号 (.), 比如{{.}} 可描述当前文本管道的数值. 而 with 语句可将点符号的描述, 指定为文本管道的数值, 如果文本管道为空, with-end 语句块将被跳过, 如果出现 with 的嵌套, 将首先处理最内层的点符号描述, 如下:

例 15.17 template_with_end.go

在这里插入图片描述

15.7.5 模板变量 $

在变量名中添加$ 前缀, 可在模板的文本管道中, 创建一个本地变量, 而本地变量名中, 可包含字母和数字, 以及下划线, 在以下示例中, 将包含一些本地变量的用法,

例 15.18 template_variables.go

在这里插入图片描述

15.7.6 range-end

rang-end 将包含以下格式:
在这里插入图片描述
range 可实现循环控制, 而文本管道的数值必须是一个数组,slice 或 map, 如果文本管道的长度为零, 点符号不会造成影响, 并会执行 T0 语句, 否则点符号将用于描述数组,slice 和 map 中的连续元素, 同时 T1 语句将执行.
在这里插入图片描述
在 20.7 节中, 给出一个更细致的示例, 可通过以下模板, 从 App 引擎的数据集中获取数据:
在这里插入图片描述
range . 语句将基于一个结构 slice 进行循环, 在每个结构中, 将包含 Author,Content 和 Date 数据域.

15.7.7 预定义的模板函数

可在 Go 代码中, 使用一些预定义的模板函数, 比如与 fmt.Sprintf 类似的 printf 函数:

例 15.19 predefined_functions.go

在这里插入图片描述
在 15.6 节中也使用了相同的函数, 即 {{ printf ”%s” .Body|html}}, 也就是打印出 Body 中包含的字符.

15.8 实用的 web 服务器

为了深入理解 http 包, 以及构建更强大的 web 服务器, 可学习以下示例, 其中将实现不同的服务器功能, 以及通告运行程序的不同状态.

例 15.20 elaborated_webserver.go

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

url 处理器: 浏览器得到的响应

• Logger(登录) 处理器, 对应的 url 为http://localhost:12345/, 当前用户为 root.
如果未找到对应的 html 头, 则会在 w.WriteHeader 中写入 404, 也就是发送 404 错误码给浏览器, 这类技术很常用, 会在 web 处理的代码中, 对错误进行检查, 如下:
在这里插入图片描述
同时在 web 服务器的控制台中, 还会打印登录函数的运行日期和时间, 以及每个请求所包含的 url 地址.

• HelloServer 处理器, 对应的 url 为http://localhost:12345/go/hello
expvar 包可创建一个变量 (int,float 或 string 类型), 并能公开派发这些变量, 也就是通过/debug/vars
的 http 路径 (基于 JSON 格式), 可发送这些变量, 它通常用于服务器的计数器 (心跳),helloRequests 是
一个 int64 类型的变量, 在当前处理器中, 可将 helloRequests 加 1, 并将 hello, world! 消息, 发送给浏览器.

• Counter 处理器, 对应的 url 为http://localhost:12345/counter
当浏览器首次请求http://localhost:12345/counter 页面时,counter = 1, 当浏览器再次刷新 (=GET) 时,
counter = 2.counter 对象 ctr 包含了一个 String() 方法, 并且实现了 Var 接口, 这可实现计数值的公开
派发, ServeHTTP 函数即为 ctr 的处理器.

• FileServer 处理器, 对应的 url 为http://localhost:12345/go/ggg.html
FileServer 可返回一个处理器, 它可服务于需要使用文件数据的 http 请求, 为了使用文件系统, 需给出
http.Dir, 如下:
在这里插入图片描述
• FlagServer 处理器, 对应的 url 为http://localhost:12345/flags
该处理器可使用 flag.VisitAll 函数, 对所有的 html 标记进行循环处理, 同时可打印出现 html 标记的名称, 数值和默认值.

• ArgServer 处理器, 对应的 url 为http://localhost:12345/args
该处理器可基于 os.Args, 循环处理所有的命令行参数, 同时不会打印程序名 (以及包含该程序的路径)

• Channel 处理器, 对应的 url 为http://localhost:12345/chan
当浏览器首次请求http://localhost:12345/chan 页面, 并发通道将发送 #1, 如果浏览器再次刷新, 并发通道将发送 #2.

并发通道的 ServeHTTP 方法, 可使每个新请求从并发通道中, 获取到一个整型值 (可视为一个编号), 因此 web 服务器可从一个并发通道中, 获取到相关的响应, 而这些响应可发送其他函数 (或是客户端), 在以下的代码片段中, 给出了超时 30s 后, 该处理器函数所进行的操作:
在这里插入图片描述
• DateServer 处理器, 对应的 url 为http://localhost:12345/date
因为需要调用/bin/date 命令, 所以该处理器只能工作在 Unix 系统中, 它可返回当前的日期和时间,
在这里插入图片描述
os.Pipe() 能返回成对的关联文件, 可读取 r, 并将获取的字节数据写入到 w, 之后可返回文件句柄和一个错误码,
在这里插入图片描述

15.9 远端程序调用 (RPC)

使用 net/rpc 包, 可实现 Go 程序之间的通讯, 这也是另一种客户端-服务器的应用架构, 它可提供一种方便的基于网络连接的函数调用, 因此不同 Go 程序可运行在不同的 PC 上,rpc 包基于 gob 包 (参见 12.11), 所以在基于网络的方法调用中, 可实现传输的自动加密和解密.

服务器可实现对象的注册, 以使对象可见, 并可视为以对象名 (类型名) 区分的一种服务, 可基于网络, 访问该对象可导出 (共享) 的方法, 或是远程客户端提供的 IO 连接, 而这些都必须是基于网络连接, 可共享的类型方法.

rpc 包使用了 http 和 tcp 协议,gob 包 (用于数据传输), 一个服务器可注册不同类型的多种对象 (即服务), 但是注册同一类型的多个对象, 则是一个错误.

在以下示例中, 将定义一个类型 Args 和对应的方法 Multiply, 并实现了一个自定义包, 该方法有可能返回一个错误.

例 15.21 rpc_objects.go

在这里插入图片描述
服务器可创建一个 Args 类型的对象 calc, 并使用 rpc.Register(object) 注册该对象, 再调用HandleHTTP(), 并在对应的地址上, 使用 net.Listen 启动监听, 同时也可使用对象名进行注册, rpc.RegisterName(”Calculator”, calc).

对于传入的每一种类型, 服务器都将提供一个监听器, 即启动一个并发协程 http.Serve(listener, nil), 为每个输入的 http 连接, 创建一个新的服务线程, 并设定了一个特殊时间间隔 (如 time.Sleep(1000e9)), 以使服务器可处理外部的 http 连接.

例 15.22 rpc_server.go

在这里插入图片描述
客户端已预知对象类型及其方法的定义, 它将调用 rpc.DialHTTP(), 当完成 http 连接时, 将使用client.Call(”Type.Method”, args, &reply), 实现远程方法的调用, 其中的 Type 和 Method, 即为远端服务器所定义的Type(类型) 和 Method(方法),args 为 Type 的初始化对象,reply 是一个本地声明的变量, 它可保存远程方法调用的返回结果.

例 15.23 rpc_client.go

在这里插入图片描述
在这里插入图片描述
实际的操作过程, 首先启动服务器, 之后再启动客户端, 之后将得到以下结果:
在这里插入图片描述
该远程调用是一个同步操作, 所以在获得调用结果后, 将会立即返回, 如果采用异步调用, 可使用以下方式:
在这里插入图片描述
最后一个参数值为 nil, 当远程调用完成后, 将分配一个新的并发通道.

如果使用 root 账号, 启动了一个 Go 服务器, 同时还需要使用其他账号, 运行一些程序时, 可使用 go-runas 包(Brad Fitz 贡献), 它基于 rpc 包实现了上述功能, 可参见页面 https://github.com/bradfitz/go-runas, 第 19 章将给出一个完整的 rpc 应用示例.

15.10 netchan

Go 开发组已经决定对当前版本的 netchan 包, 进行改进和修订, 而当前版本已被移入 old/netchan, 而包目录old 通常用于保存弃用代码, 以下将讨论 netchan 的理念, 以及向后兼容的原因.

基于网络的并发通道, 是一种与 rpc 紧密相关的技术, 第 14 章已经介绍了本地并发通道的用法, 当其运行时,只会存在于内存空间中, 但 netchan 包实现了基于网络的安全的并发通道, 它运行并发通道的两端, 可基于网络连接, 实现不同 PC 之间的通讯, 它可将数据发送给一台 PC 的并发通道, 并在另一台 PC 的同一类型的并发通道中接收该数据, 同时并发通道的名称可实现公开派发, 因此在 PC 的连接信息中, 会包含并发通道名, 当完成并发通道名的导入后, 两台 PC 可采用相同的用法 (与本地用法相同), 来使用并发通道, 由于网络通道并不是一个同步通道, 所以会使用缓冲并发通道.

在发送 PC 中, 可给出以下代码:
在这里插入图片描述
在这里插入图片描述
在接收 PC 中, 可给出以下代码:
在这里插入图片描述

15.11 websocket

Go 开发组已经决定将 websocket 包从标准库中移出, 并将其作为一个外部功能包, 页面为code.google.com/p/go, 这也意味着, 在不久的将来它将会产生巨大的变化, 所以 websocket 导入需采用以下方式:
在这里插入图片描述
websocket 协议相比于 http 协议, 它是基于客户端与服务器之间的底层连接, 但从功能上说, 它与 http 几乎一样, 在例 15.24 中, 给出了一个典型的 websocket 服务器, 它启动后, 将对 websocket 客户端进行监听, 而在例15.25 中, 给出了一个只运行 5s 的 websocket 客户端, 当客户端连接成功后, 服务器将打印 new connection, 当客户端终止运行时, 服务器将打印 => closing connection.

例 15.24 websocket_server.go

在这里插入图片描述

例 15.25 websocket_client.go

在这里插入图片描述

15.12 smtp 的邮件发送

smtp 包可实现简单邮件传输协议, 用于邮件的传输, 它给出了一个 Client 类型, 可用于描述 SMTP 服务器的客户端连接.
• Dial 函数可返回一个连接到 SMTP 服务器的 Client 连接
• Mail(发送邮件) 和 Rcpt(接收) 函数
• buf.WriteTo(wc) 可从一个写入器中返回数据

例 15.26 smtp.go

在这里插入图片描述
在这里插入图片描述
如果需要提供签名, 同时存在多个收件人时,SendMail 则必须给出签名和收件人, 使用 addr 可连接到 SMTP服务器, 可能需切换到 TLS(Transport Layer Security encryption and authentication protocol, 传输层的安全加密和签名协议) 模式下, 因此可能存在签名机制, 之后可使用邮件地址发送邮件, 并给出对应的消息 msg,
在这里插入图片描述

例 15.27 smtp_auth.go

在这里插入图片描述
在这里插入图片描述