HTTPS通信原理
HTTPS(Secure Hypertext Transfer Protocol)安全超⽂本传输协议 它是⼀个安全通信通道。
HTTPS是HTTP over SSL/TLS,HTTP是应⽤层协议,TCP是传输层协议,在应⽤层和传输层之间,增加了⼀个安全套接层SSL。
服务器 ⽤RSA⽣成公钥和私钥把公钥放在证书⾥发送给客户端,私钥⾃⼰保存客户端⾸先向⼀个权威的
服务器检查证书的合法性,如果证书合法,客户端产⽣⼀段随机数,这个随机数就作为通信的密钥,我
们称之为对称密钥,⽤公钥加密这段随机数,然后发送到服务器服务器⽤密钥解密获取对称密钥,然
后,双⽅就已对称密钥进⾏加密解密通信了。
Https的作⽤
- 内容加密 建⽴⼀个信息安全通道,来保证数据传输的安全;
- 身份认证 确认⽹站的真实性
- 数据完整性 防⽌内容被第三⽅冒充或者篡改
Https和Http的区别
- https协议需要到CA申请证书。
- http是超⽂本传输协议,信息是明⽂传输;https 则是具有安全性的ssl加密传输协议。
- http和https使⽤的是完全不同的连接⽅式,⽤的端⼝也不⼀样,前者是80,后者是443。
- http的连接很简单,是⽆状态的;HTTPS协议是由SSL+HTTP协议构建的可进⾏加密传输、身份认
证的⽹络协议,⽐http协议安全。
Demo
//1.demo
package main
import (
"fmt"
"log"
"net/http"
)
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, Web!")
}
func main() {
http.HandleFunc("/", hello)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
/*http.HandleFunc 将 hello 函数注册到根路径 / 上, hello 函数我们也叫做处理器。它接收
两个参数:
第⼀个参数为⼀个类型为 http.ResponseWriter 的接⼝,响应就是通过它发送给客户端的。
第⼆个参数是⼀个类型为 http.Request 的结构指针,客户端发送的信息都可以通过这个结构获
取。
http.ListenAndServe 将在 8080 端⼝上监听请求,最后交由 hello 处理。*/
多路复用器
- 客户端发送请求;
- 服务器中的多路复⽤器收到请求;
- 多路复⽤器根据请求的 URL找到注册的处理器,将请求交由处理器处理;
- 处理器执⾏程序逻辑,必要时与数据库进⾏交互,得到处理结果;
- 处理器调⽤模板引擎将指定的模板和上⼀步得到的结果渲染成客户端可识别的数据格式(通常是
HTML); - 最后将数据通过响应返回给客户端;
- 客户端拿到数据,执⾏对应的操作,例如渲染出来呈现给⽤户。
net/http 包内置了⼀个默认的多路复⽤器 DefaultServeMux 。定义如下:
// src/net/http/server.go
// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
net/http 包中很多⽅法都在内部调⽤ DefaultServeMux 的对应⽅法,如 HandleFunc 。我们知道, HandleFunc 是为指定的 URL 注册⼀个处理器(准确来说, hello 是处理器函数,⻅下⽂)。其内部实现如下:
// src/net/http/server.go
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
实际上, http.HandleFunc ⽅法是将处理器注册到 DefaultServeMux 中的。
另外,我们使⽤ ":8080" 和 nil 作为参数调⽤ http.ListenAndServe 时,会创建⼀个默认的服务
器:
// src/net/http/server.go
func ListenAndServe(addr string, handler Handler) {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
这个服务器默认使⽤ DefaultServeMux 来处理器请求:
type serverHandler struct {
srv *Server
}
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
handler.ServeHTTP(rw, req)
}
服务器收到的每个请求会调⽤对应多路复⽤器(即 ServeMux )的 ServeHTTP ⽅法。在 ServeMux 的
ServeHTTP ⽅法中,根据 URL查找我们注册的处理器,然后将请求交由它处理。
虽然默认的多路复⽤器使⽤起来很⽅便,但是在⽣产环境中不建议使⽤。由于 DefaultServeMux 是⼀个全局变量,所有代码,包括第三⽅代码都可以修改它。 有些第三⽅代码会在 DefaultServeMux 注册
⼀些处理器,这可能与我们注册的处理器冲突。
创建多路复⽤器
创建多路复⽤器也⽐较简单,直接调⽤ http.NewServeMux⽅法即可。然后,在新创建的多路复⽤器上注册处理器:
package main
import (
"fmt"
"log"
"net/http"
)
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, Web")
}
func main() {
//创建Mux
mux := http.NewServeMux()
mux.HandleFunc("/", hello)
server := &http.Server{
Addr: ":8080",
Handler: mux, //注册处理器
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
通过指定服务器的参数,我们可以创建定制化的服务器。
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 1 * time.Second,
WriteTimeout: 1 * time.Second,
}
在上⾯代码,创建了⼀个读超时和写超时均为 1s 的服务器。
处理器和处理器函数
服务器收到请求后,会根据其 URL将请求交给相应的处理器处理。处理器实现了 Handler 接⼝的结构, Handler 接⼝定义在 net/http 包中:
// src/net/http/server.go
type Handler interface {
func ServeHTTP(w Response.Writer, r *Request)
}
可以定义⼀个实现该接⼝的结构,注册这个结构类型的对象到多路复⽤器中:
package main
import (
"fmt"
"log"
"net/http"
)
type GreetingHandler struct {
Language string
}
func (h GreetingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s", h.Language)
}
func main() {
mux := http.NewServeMux()
mux.Handle("/chinese", GreetingHandler{Language: "你好"})
mux.Handle("/english", GreetingHandler{Language: "Hello"})
server := &http.Server {
Addr: ":8080",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
解析:
定义⼀个实现 Handler 接⼝的结构 GreetingHandler 。然后,创建该结构的两个对象,分别将它注册
到多路复⽤器的 /hello 和 /world 路径上。注意,这⾥注册使⽤的是 Handle ⽅法,注意
与 HandleFunc ⽅法对⽐。
启动服务器之后,在浏览器的地址栏中输⼊ localhost:8080/chinese ,浏览器中将显示 你好 ,输⼊ localhost:8080/english 将显示 Hello 。
虽然,⾃定义处理器这种⽅式⽐较灵活,强⼤,但是需要定义⼀个新的结构,实现 ServeHTTP ⽅法,还
是⽐较繁琐的。
为了⽅便使⽤,net/http 包提供了以函数的⽅式注册处理器,即使⽤ HandleFunc 注册。函数必须满⾜签名: func (w http.ResponseWriter, r *http.Request) 。 这个函数称为处理器函数。 HandleFunc⽅法内部,会将传⼊的处理器函数转换为 HandlerFunc 类型。
// src/net/http/server.go
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter,
*Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
HandlerFunc 是底层类型为 func (w ResponseWriter, r *Request) 的新类型,它可以⾃定义其⽅
法。由于 HandlerFunc 类型实现了 Handler 接⼝,所以它也是⼀个处理器类型,最终使⽤ Handle 注册。
// src/net/http/server.go
type HandlerFunc func(w *ResponseWriter, r *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
注意,这⼏个接⼝和⽅法名很容易混淆:
- Handler :处理器接⼝,定义在 net/http 包中。实现该接⼝的类型,其对象可以注册到多路复⽤
器中; - Handle :注册处理器的⽅法;
- HandleFunc :注册处理器函数的⽅法;
- HandlerFunc :底层类型为 func (w ResponseWriter, r *Request) 的新类型,实现了
Handler 接⼝。它连接了处理器函数与处理器。
URL匹配规则
⼀般的 Web 服务器有⾮常多的 URL 绑定,不同的 URL 对应不同的处理器。但是服务器是怎么决定使⽤
哪个处理器的呢?例如,我们现在绑定了 3 个 URL, / 和 /hello 和 /hello/world 。
显然,
如果请求的 URL 为 / ,则调⽤ / 对应的处理器。
如果请求的 URL 为 /hello ,则调⽤ /hello 对应的处理器。
如果请求的 URL 为 /hello/world ,则调⽤ /hello/world 对应的处理器。
但是,如果请求的是 /hello/others ,那么使⽤哪⼀个处理器呢? 匹配遵循以下规则:
- ⾸先,精确匹配。即查找是否有 /hello/others 对应的处理器。如果有,则查找结束。如果没
有,执⾏下⼀步; - 将路径中最后⼀个部分去掉,再次查找。即查找 /hello/ 对应的处理器。如果有,则查找结束。
如果没有,继续执⾏这⼀步。即查找 /对应的处理器。
这⾥有⼀个注意点,如果注册的 URL 不是以 / 结尾的,那么它只能精确匹配请求的 URL。反之,即使
请求的 URL 只有前缀与被绑定的 URL 相同, ServeMux 也认为它们是匹配的。
这也是为什么上⾯步骤进⾏到 /hello/ 时,不能匹配 /hello 的原因。因为 /hello 不以 / 结尾,必须
要精确匹配。 如果,我们绑定的 URL 为 /hello/ ,那么当服务器找不到与 /hello/others 完全匹配
的处理器时,就会退⽽求其次,开始寻找能够与 /hello/ 匹配的处理器。
package main
import (
"fmt"
"log"
"net/http"
)
func indexHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This is the index page")
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This is the hello page")
}
func worldHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "This is the world page")
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", indexHandler)
mux.HandleFunc("/hello", helloHandler)
mux.HandleFunc("/hello/world", worldHandler)
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
- 浏览器请求 localhost:8080/hello/ 将返回 "This is the index page" 。注意这⾥不是 hello ,因为绑定的 /hello 需要精确匹配,⽽请求的 /hello/ 不能与之精确匹配。故⽽向上查找到 / ;
- 浏览器请求 localhost:8080/hello/world/ 将返回 "This is the index page" ,查找步骤
为 /hello/world/ (不能与 /hello/world 精确匹配)-> /hello/ (不能与 /hello/ 精确匹
配)-> / ; - 浏览器请求 localhost:8080/hello/other 将返回 "This is the index page" ,查找步骤
为 /hello/others -> /hello/ (不能与 /hello 精确匹配)-> / ; - 如果注册时,将 /hello 改为 /hello/ ,那么请求 localhost:8080/hello/ 和
localhost:8080/hello/world/ 都将返回 "This is the hello page" 。
HTTP请求
处理器函数:
func (w http.ResponseWriter, r *http.Request)
其中, http.Request就是请求的类型。客户端传递的数据都可以通过这个结构来获取。结构 Request
定义在包 net/http 中:
// src/net/http/request.go
type Request struct {
Method string
URL *url.URL
Proto string
ProtoMajor int
ProtoMinor int
Header Header
Body io.ReadCloser
ContentLength int
// 省略⼀些字段...
}
Method
请求中的 Method 字段表示客户端想要调⽤服务器的 HTTP 协议⽅法。其取值有 GET/POST/PUT/DELETE 等。服务器根据请求⽅法的不同会进⾏不同的处理,例如 GET ⽅法只是获取信息(⽤户基本信息,商品信息等), POST ⽅法创建新的资源(注册新⽤户,上架新商品等)。
URL
Go 中的 URL 结构定义在 net/url 包中:
// net/url/url.go
type URL struct {
Scheme string
Opaque string
User *Userinfo
Host string
Path string
RawPath string
RawQuery string
Fragment string
}
func urlHandler(w http.ResponseWriter, r *http.Request) {
URL := r.URL
fmt.Fprintf(w, "Scheme: %s\n", URL.Scheme)
fmt.Fprintf(w, "Host: %s\n", URL.Host)
fmt.Fprintf(w, "Path: %s\n", URL.Path)
fmt.Fprintf(w, "RawPath: %s\n", URL.RawPath)
fmt.Fprintf(w, "RawQuery: %s\n", URL.RawQuery)
fmt.Fprintf(w, "Fragment: %s\n", URL.Fragment)
}
// 注册
mux.HandleFunc("/url", urlHandler)
运⾏服务器,通过浏览器访问 localhost:8080/url/posts?page=1&count=10#main
Scheme:
Host:
Path: /url/posts
RawPath:
RawQuery: page=1&count=10
Fragment:
为什么会出现空字段?注意到源码 Request 结构中 URL 字段上有⼀段注释:
// URL specifies either the URI being requested (for server
// requests) or the URL to access (for client requests).
//
// For server requests, the URL is parsed from the URI
// supplied on the Request-Line as stored in RequestURI. For
// most requests, fields other than Path and RawQuery will be
// empty. (See RFC 7230, Section 5.3)
//
// For client requests, the URL's Host specifies the server to
// connect to, while the Request's Host field optionally
// specifies the Host header value to send in the HTTP
// request.
⼤意是作为服务器收到的请求时, URL 中除了 Path 和 RawQuery ,其它字段⼤多为空。
URL := &net.URL {
Scheme: "http",
Host: "example.com",
Path: "/posts",
RawQuery: "page=1&count=10",
Fragment: "main",
}
fmt.Println(URL.String())
上⾯程序运⾏输出字符串:
Proto
Proto 表示 HTTP 协议版本,如 HTTP/1.1 , ProtoMajor 表示⼤版本, ProtoMinor 表示⼩版本
func protoFunc(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Proto: %s\n", r.Proto)
fmt.Fprintf(w, "ProtoMajor: %d\n", r.ProtoMajor)
fmt.Fprintf(w, "ProtoMinor: %d\n", r.ProtoMinor)
}
mux.HandleFunc("/proto", protoFunc)
启动服务器,浏览器请求 localhost:8080 返回:
Proto: HTTP/1.1
ProtoMajor: 1
ProtoMinor: 1
Header
Header 中存放的客户端发送过来的⾸部信息,键-值对的形式。 Header 类型底层其实是 map[string]
[]string :
// src/net/http/header.go
type Header map[string][]string
每个⾸部的键和值都是字符串,可以设置多个相同的键。注意到 Header 值为 []string 类型,存放相
同的键的多个值。浏览器发起 HTTP请求的时候,会⾃动添加⼀些⾸部。
func headerHandler(w http.ResponseWriter, r *http.Request) {
for key, value := range r.Header {
fmt.Fprintf(w, "%s: %v\n", key, value)
}
}
mux.HandleFunc("/header", headerHandler)
启动服务器,浏览器请求 localhost:8080/header 返回:
Accept-Enreading: [gzip, deflate, br]
Sec-Fetch-Site: [none]
Sec-Fetch-Mode: [navigate]
Connection: [keep-alive]
Upgrade-Insecure-Requests: [1]
User-Agent: [Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/79.0.1904.108 Safari/537.36]
Sec-Fetch-User: [?1]
Accept:
[text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/
*;q=0.8,application/signed-exchange;v=b3]
Accept-Language: [zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7]
常⻅的⾸部有:
- Accept :客户端想要服务器发送的内容类型;
- Accept-Charset :表示客户端能接受的字符编码;
- Content-Length :请求主体的字节⻓度,⼀般在 POST/PUT 请求中较多;
- Content-Type :当包含请求主体的时候,这个⾸部⽤于记录主体内容的类型。在发送 POST 或
PUT 请求时,内容的类型默认为 x-www-form-urlecoded 。但是在上传⽂件时,应该设置类型
为 multipart/form-data 。 - User-Agent :⽤于描述发起请求的客户端信息,如什么浏览器。
Content-Length/Body
Content-Length 表示请求体的字节⻓度,请求体的内容可以从 Body 字段中读取。细⼼的朋友可能发
现了 Body 字段是⼀个 io.ReadCloser 接⼝。在读取之后要关闭它,否则会有资源泄露。可以使⽤ defer 简化代码编写。
func bodyHandler(w http.ResponseWriter, r *http.Request) {
data := make([]byte, r.ContentLength)
r.Body.Read(data) // 忽略错误处理
defer r.Body.Close()
fmt.Fprintln(w, string(data))
}
mux.HandleFunc("/body", bodyHandler)
上⾯代码将客户端传来的请求体内容回传给客户端。还可以使⽤ io/ioutil 包简化读取操作:
data, _ := ioutil.ReadAll(r.Body)
- 使⽤表单
func indexHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `
<html>
<head>
<title>Go Web</title>
</head>
<body>
<form method="post" action="/body">
<label for="username">⽤户名:</label>
<input type="text" id="username" name="username">
<label for="email">邮箱:</label>
<input type="text" id="email" name="email">
<button type="submit">提交</button>
</form>
</body>
</html>
`)
}
mux.HandleFunc("/", indexHandler)
在 HTML 中使⽤ form 来显示⼀个表单。点击提交按钮后,浏览器会发送⼀个 POST 请求到路径 /body上,将⽤户名和邮箱作为请求包体。
启动服务器,进⼊主⻚ localhost:8080/ ,显示表单。填写完成后,点击提交。浏览器向服务器发送POST 请求,URL 为 /body , bodyHandler 处理完成后将包体回传给客户端。
上⾯的数据使⽤了 x-www-form-urlencoded 编码,这是表单的默认编码。
- URL 键值对
URL 的⼀般格式时提到过,URL的后⾯可以跟⼀个可选的查询字符串,以 ? 与路径分隔,形如 key1=value1&key2=value2 。
URL 结构中有⼀个 RawQuery字段。这个字段就是查询字符串。
func queryHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, r.URL.RawQuery)
}
mux.HandleFunc("/query", queryHandler)
如果我们以 localhost:8080/query?name=ls&age=20 请求,查询字符串 name=ls&age=20 会传回客户端。
- Form 字段
使⽤ x-www-form-urlencoded编码的请求体,在处理时⾸先调⽤请求的 ParseForm ⽅法解析,然后从 Form 字段中取数据:
func formHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
fmt.Fprintln(w, r.Form)
}
mux.HandleFunc("/form", formHandler)
Form 字段的类型 url.Values 底层实际上是 map[string][]string 。调⽤ ParseForm ⽅法之后,可以使⽤ url.Values 的⽅法操作数据。
- PostForm 字段
如果⼀个请求,同时有 URL键值对和表单数据,⽽⽤户只想获取表单数据,可以使⽤ PostForm 字段。使⽤ PostForm只会返回表单数据,不包括 URL 键值。
- MultipartForm 字段
如果要处理上传的⽂件,那么就必须使⽤ multipart/form-data 编码。与之前的 Form/PostForm 类似,处理 multipart/form-data编码的请求时,也需要先解析后使⽤。只不过使⽤的⽅法不同,解析使⽤ ParseMultipartForm ,之后从 MultipartForm 字段取值。
<form action="/multipartform?lang=cpp&name=dj" method="post"
enctype="multipart/form-data">
<label>MultipartForm:</label>
<input type="text" name="lang" />
<input type="text" name="age" />
<input type="file" name="uploaded" />
<button type="submit">提交</button>
</form>
func multipartFormHandler(w http.ResponseWriter, r *http.Request) {
r.ParseMultipartForm(1024)
fmt.Fprintln(w, r.MultipartForm)
fileHeader := r.MultipartForm.File["uploaded"][0]
file, err := fileHeader.Open()
if err != nil {
fmt.Println("Open failed: ", err)
return
}
data, err := ioutil.ReadAll(file)
if err == nil {
fmt.Fprintln(w, string(data))
}
}
mux.HandleFunc("/multipartform", multipartFormHandler)
MultipartForm 包含两个 map类型的字段,⼀个表示表单键值对,另⼀个为上传的⽂件信息。
使⽤表单中⽂件控件名获取 MultipartForm.File 得到通过该控件上传的⽂件,可以是多个。得到的
是 multipart.FileHeader 类型,通过该类型可以获取⽂件的各个属性。
需要注意的是,这种⽅式⽤来处理⽂件。为了安全, ParseMultipartForm ⽅法需要传⼀个参数,表示
最⼤使⽤内存,避免上传的⽂件占⽤空间过⼤。
- FormValue/PostFormValue
为了⽅便地获取值, net/http 包提供了 FormValue/PostFormValue ⽅法。它们在需要时会⾃动调⽤ ParseForm/ParseMultipartForm ⽅法。
FormValue ⽅法返回请求的 Form字段中指定键的值。如果同⼀个键对应多个值,那么返回第⼀个。如果需要获取全部值,直接使⽤ Form 字段。下⾯代码将返回 hello 对应的第⼀个值:
fmt.Fprintln(w, r.FormValue("hello"))
PostFormValue ⽅法返回请求的 PostForm 字段中指定键的值。如果同⼀个键对应多个值,那么返回第⼀个。如果需要获取全部值,直接使⽤ PostForm 字段
注意: 当编码被指定为 multipart/form-data 时, FormValue/PostFormValue 将不会返回任何值,
它们读取的是 Form/PostForm 字段,⽽ ParseMultipartForm 将数据写⼊ MultipartForm 字段。
- JSON
⾸先通过⾸部 Content-Type 来获知具体是什么格式;
通过 r.Body 读取字节流;
解码使⽤。
HTTP响应
接下来是如何响应客户端的请求。最简单的⽅式是通过 http.ResponseWriter 发送字符串给客户端。
但是这种⽅式仅限于发送字符串。
- ResponseWriter
func (w http.ResponseWriter, r *http.Request)
这⾥的 ResponseWriter 其实是定义在 net/http 包中的⼀个接⼝:
// src/net/http/
type ReponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
响应客户端请求都是通过该接⼝的 3 个⽅法进⾏的。例如之前 fmt.Fprintln(w, "Hello,
Web") 其实底层调⽤了 Write ⽅法。
收到请求后,多路复⽤器会⾃动创建⼀个 http.response 对象,它实现了 http.ResponseWriter 接⼝,然后将该对象和请求对象作为参数传给处理器。那为什么请求对象使⽤的时结构指针
*http.Request ,⽽响应要使⽤接⼝呢?
实际上,请求对象使⽤指针是为了能在处理逻辑中⽅便地获取请求信息。⽽响应使⽤接⼝来操作,底层
也是对象指针,可以保存修改。
接⼝ ResponseWriter 有 3 个⽅法:
- Write ;
- WriteHeader ;
- Header
Write 方法
由于接⼝ ResponseWriter 拥有⽅法 Write([]byte) (int, error) ,所以实现了 ResponseWriter
接⼝的结构也实现了 io.Writer 接⼝:
// src/io/io.go
type Writer interface {
Write(p []byte) (n int, err error)
}
这也是为什么 http.ResponseWriter 类型的变量 w 能在下⾯代码中使⽤的原因( fmt.Fprintln 的第
⼀个参数接收⼀个 io.Writer 接⼝):
fmt.Fprintln(w, "Hello World")
也可以直接调⽤ Write ⽅法来向响应中写⼊数据
func writeHandler(w http.ResponseWriter, r *http.Request) {
str := `<html>
<head><title>Go Web</title></head>
<body><h1>直接使⽤ Write ⽅法<h1></body>
</html>`
w.Write([]byte(str))
}
mux.HandleFunc("/write", writeHandler)
WriteHeader ⽅法
WriteHeader ⽅法的名字带有⼀点误导性,它并不能⽤于设置响应⾸部。 WriteHeader 接收⼀个整
数,并将这个整数作为 HTTP响应的状态码返回。调⽤这个返回之后,可以继续对 ResponseWriter 进⾏写⼊,但是不能对响应的⾸部进⾏任何修改操作。如果⽤户在调⽤ Write ⽅法之前没有执⾏过
WriteHeader ⽅法,那么程序默认会使⽤ 200 作为响应的状态码。
如果,我们定义了⼀个API,还未定义其实现。那么请求这个 API 时,可以返回⼀个 501 NotImplemented 作为状态码。
func writeHeaderHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(501)
fmt.Fprintln(w, "This API not implemented!!!")
}
mux.HandleFunc("/writeheader", writeHeaderHandler)
注意:其实状态码更推荐http.的方式取golang定义过的字面量替换
Header ⽅法
Header ⽅法其实返回的是⼀个 http.Header 类型,该类型的底层类型为 map[string][]string :
// src/net/http/header.go
type Header map[string][]string
类型 Header 定义了 CRUD⽅法,可以通过这些⽅法操作⾸部。
func headerHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "http://baidu.com")
w.WriteHeader(302)
}
设置⾃定义的内容类型。通过 Header.Set ⽅法设置响应的⾸部 Contet-Type
即可。编写⼀个返回 JSON 数据的处理器:
type User struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Age int `json:"age"`
Hobbies []string `json:"hobbies"`
}
func jsonHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
u := &User {
FirstName: "ls",
LastName: "ls",
Age: 18,
Hobbies: []string{"reading", "learning"},
}
data, _ := json.Marshal(u)
w.Write(data)
}
mux.HandleFunc("/json", jsonHandler)
cookie
Go 中 cookie 使⽤ http.Cookie 结构表示,在 net/http 包中定义:
// src/net/http/cookie.go
type Cookie struct {
Name string
Value string
Path string
Domain string
Expires time.Time
RawExpires string
MaxAge int
Secure bool
HttpOnly bool
SameSite SameSite
Raw string
Unparsed []string
}
- Name/Value :cookie 的键值对,都是字符串类型;
- 没有设置 Expires 字段的 cookie 被称为会话 cookie 或临时 cookie,这种 cookie 在浏览器关闭
时就会⾃动删除。设置了 Expires 字段的 cookie 称为持久 cookie,这种 cookie 会⼀直存在,直
到指定的时间来临或⼿动删除; - HttpOnly 字段设置为 true 时,该 cookie 只能通过 HTTP 访问,不能使⽤其它⽅式操作,如
JavaScript。提⾼安全性;
注意:
Expires 和 MaxAge 都可以⽤于设置 cookie 的过期时间。 Expires 字段设置的是 cookie 在什么
时间点过期,⽽ MaxAge 字段表示 cookie ⾃创建之后能够存活多少秒。虽然 HTTP 1.1 中废弃了
Expires ,推荐使⽤ MaxAge 代替。但是⼏乎所有的浏览器都仍然⽀持 Expires ;⽽且,微软的
IE6/IE7/IE8 都不⽀持 MaxAge 。所以为了更好的可移植性,可以只使⽤ Expires 或同时使⽤这两
个字段。
cookie 需要通过响应的⾸部发送给客户端。浏览器收到 Set-Cookie ⾸部时,会将其中的值解析成
cookie 格式保存在浏览器中。
func setCookie(w http.ResponseWriter, r *http.Request) {
c1 := &http.Cookie {
Name: "name",
Value: "lianshi",
HttpOnly: true,
}
c2 := &http.Cookie {
Name: "age",
Value: 18,
HttpOnly: true,
}
w.Header().Set("Set-Cookie", c1.String())
w.Header().Add("Set-Cookie", c2.String())
}
mux.HandleFunc("/set_cookie", setCookie)
上⾯构造 cookie 的代码中,有⼏点需要注意:
- ⾸部名称为 Set-Cookie ;
- ⾸部的值需要是字符串,所以调⽤了 Cookie 类型的 String ⽅法将其转为字符串再设置;
- 设置第⼀个 cookie 调⽤ Header 类型的 Set ⽅法,添加第⼆个 cookie 时调⽤ Add ⽅
法。 Set 会将同名的键覆盖掉。如果第⼆个也调⽤ Set ⽅法,那么第⼀个 cookie 将会被覆
盖。
net/http 包还提供了 SetCookie ⽅法。⽤法如下:
func setCookie2(w http.ResponseWriter, r *http.Request) {
c1 := &http.Cookie {
Name: "name",
Value: "lianshi",
HttpOnly: true,
}
c2 := &http.Cookie {
Name: "age",
Value: "18",
HttpOnly: true,
}
http.SetCookie(w, c1)
http.SetCookie(w, c2)
}
mux.HandleFunc("/set_cookie2", setCookie2)
在服务端,我们可以从请求的 Header 字段读取 Cookie 属性来获得cookie:
func getCookie(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Host:", r.Host)
fmt.Fprintln(w, "Cookies:", r.Header["Cookie"])
}
mux.HandleFunc("/get_cookie", getCookie)
r.Header["Cookie"] 返回⼀个切⽚,这个切⽚⼜包含了⼀个字符串,⽽这个字符串⼜包含了客户端发送的任意多个 cookie。如果想要取得单个键值对格式的 cookie,就需要解析这个字符串。 为
此, net/http 包在 http.Request上提供了⼀些⽅法使我们更容易地获取 cookie:
func getCookie2(w http.ResponseWriter, r *http.Request) {
name, err := r.Cookie("name")
if err != nil {
fmt.Fprintln(w, "cannot get cookie of name")
}
cookies := r.Cookies()
fmt.Fprintln(w, c1)
fmt.Fprintln(w, cookies)
}
mux.HandleFunc("/get_cookies", getCookies2)
- Cookie ⽅法返回以传⼊参数为键的 cookie,如果该 cookie 不存在,则返回⼀个错误;
- Cookies ⽅法返回客户端传过来的所有 cookie。
func main() {
mux1 := http.NewServeMux()
mux1.HandleFunc("/set_cookie", setCookie)
mux1.HandleFunc("/get_cookie", getCookie)
server1 := &http.Server{
Addr: ":8080",
Handler: mux1,
}
mux2 := http.NewServeMux()
mux2.HandleFunc("/get_cookie", getCookie)
server2 := &http.Server {
Addr: ":8081",
Handler: mux2,
}
wg := sync.WaitGroup{}
wg.Add(2)
go func () {
defer wg.Done()
if err := server1.ListenAndServe(); err != nil {
log.Fatal(err)
}
}()
go func() {
defer wg.Done()
if err := server2.ListenAndServe(); err != nil {
log.Fatal(err)
}
}()
wg.Wait()
}
发送给端⼝ 8081 的请求同样可以获取 cookie。
上⾯代码中,不能直接在主 goroutine 中依次 ListenAndServe 两个服务器。因为 ListenAndServe
只有在出错或关闭时才会返回。在此之前,第⼆个服务器永远得不到机会运⾏。所以,我创建两个
goroutine 各⾃运⾏⼀个服务器,并且使⽤ sync.WaitGroup 来同步。否则,主 goroutine 运⾏结束之后,整个程序就退出了。
扩展
- ⾸先调⽤Http.HandleFunc
按顺序做了⼏件事:
1 调⽤了DefaultServeMux的HandleFunc
2 调⽤了DefaultServeMux的Handle
3 往DefaultServeMux的map[string]muxEntry中增加对应的handler和路由规则
- 其次调⽤http.ListenAndServe(":8080", nil)
按顺序做了⼏件事情:
1 实例化Server
2 调⽤Server的ListenAndServe()
3 调⽤net.Listen("tcp", addr)监听端⼝
4 启动⼀个for循环,在循环体中Accept请求
5 对每个请求实例化⼀个Conn,并且开启⼀个goroutine为这个请求进⾏服务go c.serve()
6 读取每个请求的内容w, err := c.readRequest()
7 判断handler是否为空,如果没有设置handler(这个例⼦就没有设置handler),handler
就设置为DefaultServeMux
8 调⽤handler的ServeHttp
9 在这个例⼦中,下⾯就进⼊到DefaultServeMux.ServeHttp
10 根据request选择handler,并且进⼊到这个handler的ServeHTTP
小demo
package main
import (
"fmt"
"log"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World")
}
type greetingHandler struct {
Name string
}
func (h greetingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s", h.Name)
}
func main() {
mux := http.NewServeMux()
// 注册处理器函数
mux.HandleFunc("/hello", helloHandler)
// 注册处理器
mux.Handle("/greeting/golang", greetingHandler{Name: "Golang"})
server := &http.Server {
Addr: ":8080",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
模板引擎
模板引擎按照功能可以划分为两种类型:
- ⽆逻辑模板引擎:此类模板引擎只进⾏字符串的替换,⽆其它逻辑;
- 嵌⼊逻辑模板引擎:此类模板引擎可以在模板中嵌⼊逻辑,实现流程控制/循环等。
这两类模板引擎都⽐较极端。⽆逻辑模板引擎需要在处理器中额外添加很多逻辑⽤于⽣成替换的⽂本。
⽽嵌⼊逻辑模板引擎则在模板中混⼊了⼤量逻辑,导致维护性较差。常⽤的模板引擎⼀般介于这两者之
间。
在 Go 标准库中, text/template 和 html/template 两个库实现模板功能。
模板内容可以是 UTF-8 编码的任何内容。其中⽤ {{ 和 }} 包围的部分称为动作, {{}} 外的其它⽂本在
输出保持不变。模板需要应⽤到数据,模板中的动作会根据数据⽣成响应的内容来替换。
模板解析之后可以多次执⾏,也可以并⾏执⾏,但是注意使⽤同⼀个 Writer 会导致输出交替出现。
定义模板
使⽤模板引擎⼀般有 3 个步骤:
- 定义模板(直接使⽤字符串字⾯量或⽂件);
- 解析模板(使⽤ text/template 或 html/template 中的⽅法解析);
- 传⼊数据⽣成输出。
package main
import (
"log"
"os"
"text/template"
)
type User struct {
Name string
Age int
}
func stringLiteralTemplate() {
s := "My name is {{ .Name }}. I am {{ .Age }} years old.\n"
t, err := template.New("test").Parse(s)
if err != nil {
log.Fatal("Parse string literal template error:", err)
}
u := User{Name: "lianshi", Age: 18}
err = t.Execute(os.Stdout, u)
if err != nil {
log.Fatal("Execute string literal template error:", err)
}
}
func fileTemplate() {
t, err := template.ParseFiles("test")
if err != nil {
log.Fatal("Parse file template error:", err)
}
u := User{Name: "ls", Age: 18}
err = t.Execute(os.Stdout, u)
if err != nil {
log.Fatal("Execute file template error:", err)
}
}
func main() {
stringLiteralTemplate()
fileTemplate()
}
在可执⾏程序⽬录中新建模板⽂件 test ,并写⼊下⾯的内容:
My name is {{ .Name }}. I am {{ .Age }} years old.
⾸先调⽤ template.New 创建⼀个模板,参数为模板名。
然后调⽤ Template 类型的 Parse ⽅法,解析模板字符串,⽣成模板主体。这个⽅法返回两个值。如果模板语法正确,则返回模板对象本身和⼀个 nil 值。 如果有语法错误,则返回⼀个 error 类型的值作为
第⼆个返回值,这时不应该使⽤第⼀个返回值。
最后,调⽤模板对象的 Execute ⽅法,传⼊参数。 Execute 执⾏模板中的动作,将结果输出
到 os.Stdout ,即标准输出。最终我们看到模板中 {{ .Name }} 被 u 的 Name 字段替换, {{ .Age
}} 被 u 的 Age 字段替换,标准输出中就显示我们填⼊内容的字符串:
上⾯代码中, fileTemplate 函数还演示了如何从⽂件中加载模板。其中 template.ParseFiles ⽅法
会创建⼀个模板,并将⽤户指定的模板⽂件名⽤作这个新模板的名字:
t, err := template.ParseFiles("test")
相当于
t := template.New("test")
t, err := t.ParseFiles("test")
模板动作
Go 模板中的动作就是⼀些嵌⼊在模板⾥⾯的命令。动作⼤体上可以分为以下⼏种类型:
- 点动作
- 条件动作
- 迭代动作
- 设置动作
- 包含动作
点动作
点动作( {{ . }} )。它其实代表是传递给模板
的数据,其他动作或函数基本上都是对这个数据进⾏处理,以此来达到格式化和内容展示的⽬的。
func main() {
s := "The user is {{ . }}."
t, err := template.New("test").Parse(s)
if err != nil {
log.Fatal("Parse error:", err)
}
u := User{Name: "lianshi", Age: 18}
err = t.Execute(os.Stdout, u)
if err != nil {
log.Fatal("Execute error:", err)
}
}
输出
The user is {lianshi 18}.
实际上, {{ . }} 会被替换为传给给模板的数据的字符串表示。这个字符串与以数据为参数调⽤ fmt.Sprint 函数得到的内容相同。我们可以为 User 结构编写⼀个⽅法:
func (u User) String() string {
return fmt.Sprintf("(name:%s age:%d)", u.Name, u.Age)
}
输出
The user is (name:lianshi age:18).
条件动作
Go 标准库中对动作有详细的介绍。 其中 pipeline 表示管道,后⾯会有详细的介绍,现在可以将它理
解为⼀个值。 T1/T2 等形式表示语句块,⾥⾯可以嵌套其它类型的动作。最简单的语句块就是不包含
任何动作的字符串。
条件动作的语法与编程语⾔中的 if 语句语法类似,有⼏种形式:
形式⼀:
{{ if pipeline }} T1 {{ end }
如果管道计算出来的值不为空,执⾏ T1 。否则,不⽣成输出。下⾯都表示空值:
- false 、0、空指针或接⼝
- ⻓度为 0 的数组、切⽚、map或字符串
形式⼆:
{{ if pipeline }} T1 {{ else }} T2 {{ end }}
如果管道计算出来的值不为空,执⾏ T1 。否则,执⾏ T2 。
形式三:
{{ if pipeline1 }} T1 {{ else if pipeline2 }} T2 {{ else }} T3 {{ end }}
如果管道 pipeline1 计算出来的值不为空,则执⾏ T1 。反之如果管道 pipeline2 的值不为空,执
⾏ T2 。如果都为空,执⾏ T3 。
type AgeInfo struct {
Age int
GreaterThan60 bool
GreaterThan40 bool
}
func main() {
t, err := template.ParseFiles("test")
if err != nil {
log.Fatal("Parse error:", err)
}
rand.Seed(time.Now().Unix())
age := rand.Intn(100)
info := AgeInfo {
Age: age,
GreaterThan60: age > 60,
GreaterThan40: age > 40,
}
err = t.Execute(os.Stdout, info)
if err != nil {
log.Fatal("Execute error:", err)
}
}
在可执⾏程序的⽬录下新建模板⽂件 test ,键⼊下⾯的内容:
Your age is: {{ .Age }}
{{ if .GreaterThan60 }}
Old People!
{{ else if .GreaterThan40 }}
Middle Aged!
{{ else }}
Young!
{{ end }}
运⾏程序,会随机⼀个年龄,然后根据年龄区间选择性输出 Old People!/Middle Age!/Young! 其中
⼀个。下⾯是我运⾏两次运⾏的输出
Your age is: 7
Young!
Your age is: 79
Old People!
这个程序有⼀个问题,会有多余的空格!除了动作之外的任何⽂本都会原样保持,包括
空格和换⾏!针对这个问题,有两种解决⽅案。第⼀种⽅案是删除多余的空格和换⾏, test ⽂件修改
为:
Your age is: {{ .Age }}
{{ if .GreaterThan60 }}Old People!{{ else if .GreaterThan40 }}Middle Aged!{{
else }}Young!{{ end }}
显然,这个⽅法会导致模板内容很难阅读,不够理想。为此,Go 提供了针对空⽩符的处理。如果⼀个
动作以 {{- (注意有⼀个空格),那么该动作与它前⾯相邻的⾮空⽂本或动作间的空⽩符将会被全部删
除。类似地,如果⼀个动作以 -}} 结尾,那么该动作与它后⾯相邻的⾮空⽂本或动作间的空⽩符将会被
全部删除。例如:
{{23 -}} < {{- 45}}
将会⽣成输出:
23<45
例子修改如下:
Your age is: {{ .Age }}
{{ if .GreaterThan60 -}}
"Old People!"
{{- else if .GreaterThan40 -}}
"Middle Aged!"
{{- else -}}
"Young!"
{{- end }}
迭代动作
形式⼀:
{{ range pipeline }} T1 {{ end }}
管道的值类型必须是数组、切⽚、map、channel。如果值的⻓度为 0,那么⽆输出。否则, . 被设置
为当前遍历到的元素,然后执⾏ T1 ,即在 T1 中 . 表示遍历的当前元素,⽽⾮传给模板的参数。如果值
是 map 类型,且键是可⽐较的基本类型,元素将会以键的顺序访问。
形式⼆:
{{ range pipeline }} T1 {{ else }} T2 {{ end }}
与前⼀种形式基本⼀样,如果值的⻓度为 0,那么执⾏ T2 。
type Item struct {
Name string
Price int
}
func main() {
t, err := template.ParseFiles("test")
if err != nil {
log.Fatal("Parse error:", err)
}
items := []Item {
{ "iPhone", 699 },
{ "iPad", 799 },
{ "iWatch", 199 },
{ "MacBook", 999 },
}
err = t.Execute(os.Stdout, items)
if err != nil {
log.Fatal("Execute error:", err)
}
}
在可执⾏程序⽬录下新建模板⽂件 test ,键⼊内容:
Apple Products:
{{ range . }}
{{ .Name }}: ¥{{ .Price }}
{{ else }}
No Products!!!
{{ end }}
运⾏程序,得到下⾯的输出:
Apple Products:
iPhone: ¥699
iPad: ¥799
iWatch: ¥199
MacBook: ¥999
在 range 语句循环体内, . 被设置为当前遍历的元素,可以直接使⽤ {{ .Name }} 或 {{ .Price }}
访问产品名称和价格。在程序中,将 nil 传给 Execute ⽅法会得到下⾯的输出:
Apple Products:
No Products!!!
设置动作
设置动作使⽤ with 关键字重定义 . 。在 with 语句内, . 会被定义为指定的值。⼀般⽤在结构嵌套很深时,能起到简化代码的作⽤。
形式⼀:
{{ with pipeline }} T1 {{ end }}
如果管道值不为空,则将 . 设置为 pipeline 的值,然后执⾏ T1 。否则,不⽣成输出。
形式⼆:
{{ with pipeline }} T1 {{ else }} T2 {{ end }}
与前⼀种形式的不同之处在于当管道值为空时,不改变 . 执⾏ T2 。
type User struct {
Name string
Age int
}
type Pet struct {
Name string
Age int
Owner User
}
func main() {
t, err := template.ParseFiles("test")
if err != nil {
log.Fatal("Parse error:", err)
}
p := Pet {
Name: "Orange",
Age: 2,
Owner: User {
Name: "ls",
Age: 18,
},
}
err = t.Execute(os.Stdout, p)
if err != nil {
log.Fatal("Execute error:", err)
}
}
模板⽂件内容
Pet Info:
Name: {{ .Name }}
Age: {{ .Age }}
Owner:
{{ with .Owner }}
Name: {{ .Name }}
Age: {{ .Age }}
{{ end }}
运⾏程序,得到下⾯的输出:
Pet Info:
Name: Orange
Age: 2
Owner:
Name: ls
Age: 18
可⻅,在 with 语句内, . 被替换成了 Owner 字段的值
包含动作
包含动作可以在⼀个模板中嵌⼊另⼀个模板,⽅便模板的复⽤。
形式⼀:
{{ template "name" }}
形式⼆:
{{ template "name" pipeline }}
其中 name 表示嵌⼊的模板名称。第⼀种形式,将使⽤ nil 作为传⼊内嵌模板的参数。第⼆种形式,管
道 pipeline 的值将会作为参数传给内嵌的模板。
package main
import (
"log"
"os"
"text/template"
)
func main() {
t, err := template.ParseFiles("test1", "test2")
if err != nil {
log.Fatal("Parse error:", err)
}
err = t.Execute(os.Stdout, "test data")
if err != nil {
log.Fatal("Execute error:", err)
}
}
ParseFiles ⽅法接收可变参数,可将任意多个⽂件名传给该⽅法
模板 test1 :
This is in test1.
{{ template "test2" }}
{{ template "test2" . }}
模板 test2 :
This is in test2.
Get: {{ . }}.
运⾏程序得到输出:
This is in test1.
This is in test2.
Get: <no value>.
This is in test2.
Get: test data.
前⼀个嵌⼊模板,没有传递参数。后⼀个传⼊ . ,即传给 test1 模板的参数。
其它元素
- 注释
{{ /* 注释 */ }}
- 参数
⼀个参数就是模板中的⼀个值。它的取值有多种: - 布尔值、字符串、字符、整数、浮点数、虚数和复数等字⾯量;
- 结构中的⼀个字段或 map 中的⼀个键。结构的字段名必须是导出的,即⼤写字⺟开头,map 的键
名则不必; - ⼀个函数或⽅法。必须只返回⼀个值,或者只返回⼀个值和⼀个错误。如果返回了⾮空的错误,
则 Execute ⽅法执⾏终⽌,返回该错误给调⽤者;
上⾯⼏种形式可以结合使⽤:
{{ .Field1.Key1.Method1.Field2.Key2.Method2 }
type User struct {
FirstName string
LastName string
}
func (u User) FullName() string {
return u.FirstName + " " + u.LastName
}
func main() {
t, err := template.ParseFiles("test")
if err != nil {
log.Fatal("Parse error:", err)
}
err = t.Execute(os.Stdout, User{FirstName: "ls", LastName: "lianshi"})
if err != nil {
log.Fatal("Execute error:", err)
}
}
模板⽂件 test :
My full name is {{ .FullName }}.
模板执⾏会使⽤ FullName ⽅法的返回值替换 {{ .FullName }} ,输出:
My full name is ls lianshi.
关于参数的⼏个要点:
- 参数可以是任何类型;
- 如果参数为指针,实现会根据需要取其基础类型;
- 如果参数计算得到⼀个函数类型,它不会⾃动调⽤。例如 {{ .Method1 }} ,如果 Method1 ⽅法
返回⼀个函数,那么返回值函数不会调⽤。如果要调⽤它,使⽤内置的 call 函数。
管道
管道的语法与 Linux中的管道类似,即命令的链式序列
{{ p1 | p2 | p3 }}
每个单独的命令(即 p1/p2/p3... )可以是下⾯三种类型:
- 参数,⻅上⾯;
- 可能带有参数的⽅法调⽤;
- 可能带有参数的函数调⽤。
在⼀个链式管道中,每个命令的结果会作为下⼀个命令的最后⼀个参数。最后⼀个命令的结果作为整个
管道的值。
管道必须只返回⼀个值,或者只返回⼀个值和⼀个错误。如果返回了⾮空的错误,那么 Execute ⽅法执
⾏终⽌,并将该错误返回给调⽤者。
type Item struct {
Name string
Price float64
Num int
}
func (item Item) Total() float64 {
return item.Price * float64(item.Num)
}
func main() {
t, err := template.ParseFiles("test")
if err != nil {
log.Fatal("Parse error:", err)
}
item := Item {"iPhone", 699.99, 2 }
err = t.Execute(os.Stdout, item)
if err != nil {
log.Fatal("Execute error:", err)
}
}
模板⽂件 test :
Product: {{ .Name }}
Price: ¥{{ .Price }}
Num: {{ .Num }}
Total: ¥{{ .Total | printf "%.2f" }}
先调⽤ Item.Total ⽅法计算商品总价,然后使⽤ printf 格式化,保留两位⼩数。最终输出:
Product: iPhone
Price: ¥699.99
Num: 2
Total: ¥1399.98
变量
在动作中,可以⽤管道的值定义⼀个变量。
$variable := pipeline
$variable 为变量名,声明变量的动作不⽣成输出。
类似地,变量也可以重新赋值:
$variable = pipeline
在 range 动作中可以定义两个变量:
range element := range pipeline
这样就可以在循环中通过 element 访问索引和元素了。
变量的作⽤域持续到定义它的控制结构的 {{ end }} 动作。如果没有这样的控制结构,则持续到模板结
束。模板调⽤不继承变量。
执行开始时, $ 被设置为传⼊的数据参数,即 . 的值
函数
Go 模板提供了⼤量的预定义函数,如果有特殊需求也可以实现⾃定义函数。模板执⾏时,遇到函数调
⽤,先从模板⾃定义函数表中查找,⽽后查找全局函数表。预定义函数分为以下⼏类:
- 逻辑运算, and/or/not ;
- 调⽤操作, call ;
- 格式化操作, print/printf/println ,与⽤参数直接调⽤ fmt.Sprint/Sprintf/Sprintln 得到的内容相同;
- ⽐较运算, eq/ne/lt/le/gt/ge 。
在上⾯条件动作的示例代码中,我们在代码中计算出⼤⼩关系再传⼊模板,这样⽐较繁琐,可以直接使
⽤⽐较运算简化。
有两点需要注意:
- 由于是函数调⽤,所有的参数都会被求值,没有短路求值; {{if p1 or p2}}
- ⽐较运算只作⽤于基本类型,且没有 Go 语法那么严格,例如可以⽐较有符号和⽆符号整数
⾃定义函数
默认情况下,模板中⽆⾃定义函数,可以使⽤模板的 Funcs ⽅法添加。下⾯我们实现⼀个格式化⽇期的
⾃定义函数:
package main
import (
"log"
"os"
"text/template"
"time"
)
func formatDate(t time.Time) string {
return t.Format("2016-01-02")
}
func main() {
funcMap := template.FuncMap {
"fdate": formatDate,
}
t := template.New("test").Funcs(funcMap)
t, err := t.ParseFiles("test")
if err != nil {
log.Fatal("Parse errr:", err)
}
err = t.Execute(os.Stdout, time.Now())
if err != nil {
log.Fatal("Exeute error:", err)
}
}
模板⽂件 test :
Today is {{ . | fdate }}.
模板的 Func ⽅法接受⼀个 template.FuncMap 类型变量,键为函数名,值为实际定义的函数。 可以⼀次设置多个⾃定义函数。⾃定义函数要求只返回⼀个值,或者返回⼀个值和⼀个错误。 设置之后就可以在模板中使⽤ fdate 了,输出:
Today is 7016-01-07.
这⾥不能使⽤ template.ParseFiles ,因为在解析模板⽂件的时候 fdate 未定义会导致解析失败。必须先创建模板,调⽤ Funcs 设置⾃定义函数,然后再解析模板。
创建模板
模板的创建⽅式:
- 先调⽤ template.New 创建模板,然后使⽤ Parse/ParseFiles 解析模板内容;
- 直接使⽤ template.ParseFiles 创建并解析模板⽂件。
第⼀种⽅式,调⽤ template.New 创建模板时需要传⼊⼀个模板名字,后续调⽤ ParseFiles 可以传⼊⼀个或多个⽂件,这些⽂件中必须有⼀个基础名(即去掉路径部分)与模板名相同。如果没有⽂件名与模板名相同,则 Execute调⽤失败,返回错误。例如:
package main
import (
"log"
"os"
"text/template"
)
func main() {
t := template.New("test")
t, err := t.ParseFiles("test1")
if err != nil {
log.Fatal("Parse error:", err)
}
err = t.Execute(os.Stdout, nil)
if err != nil {
log.Fatal("Execute error:", err)
}
}
上⾯代码先创建模板 test ,然后解析⽂件 test1 。执⾏该程序会出现下⾯的错误:
Execute error:template: test: "test" is an incomplete or empty template
为什么?
// 模板的结构
// src/text/template.go
type common struct {
tmpl map[string]*Template // Map from name to defined templates.
option option
muFuncs sync.RWMutex // protects parseFuncs and execFuncs
parseFuncs FuncMap
execFuncs map[string]reflect.Value
}
type Template struct {
name string
*parse.Tree
*common
leftDelim string
rightDelim string
}
模板结构 Template 中有⼀个字段 common , common 中⼜有⼀个字段 tmpl 保存名字到模板的映射。其
实,最外层的 Template 结构是主模板,我们调⽤ Execute ⽅法时执⾏的就是主模板。 执
⾏ ParseFiles⽅法时,每个⽂件都会⽣成⼀个模板。只有⽂件基础名与模板名相同时,该⽂件的内容
才会解析到主模板中。这也是上⾯的程序执⾏失败的原因——主模板为空。 其它⽂件解析⽣成关联模
板,存储在字段 tmpl中。关联模板可以是在主模板中通过 {{ define }} 动作定义,或者在⾮主模板⽂件中定义。关联模板也可以执⾏,但是需要使⽤ ExecuteTemplate ⽅法,显式传⼊模板名
func main()
t := template.New("test")
t, err := t.ParseFiles("test1")
if err != nil {
log.Fatal("in associatedTemplate Parse error:", err)
}
err = t.ExecuteTemplate(os.Stdout, "test1", nil)
if err != nil {
log.Fatal("in associatedTemplate Execute error:", err)
}
}
第⼆种⽅式将创建和解析两步合并在⼀起了。 template.ParseFiles ⽅法将传⼊的第⼀个⽂件名作为
模板名称,其余的⽂件(如果有的话)解析后存放在 tmpl 中。
t, err := template.ParseFiles("file1", "file2", "file3")
等价于:
t := template.New("file1")
t, err := t.ParseFiles("file1", "file2", "file3")
少了不⼀致的可能性,所以调⽤ Execute ⽅法时不会出现上⾯的错误。
还有⼀种创建⽅式,使⽤ ParseGlob 函数。 ParseGlob 会对匹配给定模式的所有⽂件进⾏语法分析。
func main() {
t, err := template.ParseGlob("tmpl*.glob")
if err != nil {
log.Fatal("in globTemplate parse error:", err)
}
err = t.Execute(os.Stdout, nil)
if err != nil {
log.Fatal(err)
}
for i := 1; i <= 3; i++ {
err = t.ExecuteTemplate(os.Stdout, fmt.Sprintf("tmpl%d.glob", i), nil)
if err != nil {
log.Fatal(err)
}
}
}
ParseGlob 返回的模板以匹配的第⼀个⽂件基础名作为名称。 ParseGlob 解析时会对同⼀个⽬录下的
⽂件进⾏排序,所以第⼀个⽂件总是固定的。
创建三个模板⽂件, tmpl1.glob :
In glob template file1.
tmpl2.glob :
In glob template file2.
tmpl3.glob :
In glob template file3.
最终输出为:
In glob template file1.
In glob template file1.
In glob template file2.
In glob template file3.
注意,如果多个不同路径下的⽂件名相同,那么后解析的会覆盖之前的。
嵌套模板
在⼀个模板⽂件中还可以通过 {{ define }} 动作定义其它的模板,这些模板就是嵌套模板。模板定义必须在模板内容的最顶层,像 Go 程序中的全局变量⼀样。
嵌套模板⼀般⽤于布局(layout)。很多⽂本的结构其实⾮常固定,例如邮件有标题和正⽂,⽹⻚有⾸
部、正⽂和尾部等。我们可以为这些固定结构的每部分定义⼀个模板。
定义模板⽂件 layout.tmpl :
{{ define "layout" }}
This is body.
{{ template "content" . }}
{{ end }}
{{ define "content" }}
This is {{ . }} content.
{{ end }}
上⾯定义了两个模板 layout 和 content , layout 中使⽤了 content 。执⾏这种⽅式定义的模板必须
使⽤ ExecuteTemplate ⽅法:
func main() {
t, err := template.ParseFiles("layout.tmpl")
if err != nil {
log.Fatal("Parse error:", err)
}
err = t.ExecuteTemplate(os.Stdout, "layout", "amazing")
if err != nil {
log.Fatal("Execute error:", err)
}
}
块动作
块动作其实就是定义⼀个默认模板
{{ block "name" arg }}
T1
{{ end }}
其实它就等价于定义⼀个模板,然后⽴即使⽤它:
{{ define "name" }}
T1
{{ end }}
{{ template "name" arg }}
如果后⾯定义了模板 content ,那么使⽤后⾯的定义,否则使⽤默认模板。
将模板修改如下
{{ define "layout" }}
This is body.
{{ block "content" . }}
This is default content.
{{ end }}
{{ end }}
去掉后⾯的 content 模板定义,执⾏ layout 时, content 部分会显示默认值。
HTML模板
text/template 库⽤于⽣成⽂本输出。在 Web 开发中,涉及到很多安全⽅⾯的问题。有些数据是⽤户输⼊的,不能直接替换到模板中,否则可能导致注⼊攻击。 Go 提供了 html/template 库处理这些问
题。 html/template 提供了与 text/template ⼀样的接⼝。 我们通常使⽤ html/template ⽣成
HTML 输出。
- HTML模板
html/template 库的使⽤与 text/template 基本⼀样:
package main
import (
"fmt"
"html/template"
"log"
"net/http"
)
func indexHandler(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("hello.html")
if err != nil {
w.WriteHeader(500)
fmt.Fprint(w, err)
return
}
t.Execute(w, "Hello World")
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", indexHandler)
server := &http.Server {
Addr: ":8080",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
模板⽂件 hello.html :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Go Web</title>
</head>
<body>
{{ . }}
</body>
</html>
模板中的 {{ . }} 会被替换为传⼊的数据"Hello World",程序将模板执⾏后⽣成的⽂本通过
ResponseWriter 传回客户端。
打开浏览器,输⼊ localhost:8080 ,即可看到"Hello World"⻚⾯。
为了编写示例代码的便利,在解析时不进⾏错误处理, html/template 库提供了 Must ⽅法。 它接受
两个参数,⼀个模板对象指针,⼀个错误。如果错误参数不为 nil ,直接 panic,否则返回模板对象指
针。 使⽤ Must ⽅法简化上⾯的处理器:
func indexHandler(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.ParseFiles("hello.html"))
t.Execute(w, "Hello World")
}
html模板也有对应的动作:
条件动作
func conditionHandler(w http.ResponseWriter, r *http.Request) {
age, err := strconv.ParseInt(r.URL.Query().Get("age"), 10, 64)
if err != nil {
fmt.Fprint(w, err)
return
}
t := template.Must(template.ParseFiles("condition.html"))
t.Execute(w, age)
}
mux.HandleFunc("/condition", conditionHandler)
模板⽂件 condition.html 只有 body 部分不同
<p>Your age is: {{ . }}</p>
{{ if gt . 60 }}
<p>Old People!</p>
{{ else if gt . 40 }}
<p>Middle Aged!</p>
{{ else }}
<p>Young!</p>
{{ end }}
模板逻辑很简单,使⽤内置函数 gt 判断传⼊的年龄处于哪个区间,显示对应的⽂本。
编译、运⾏程序,打开浏览器,输⼊ localhost:8080/condition?age=10 。
迭代动作
迭代动作⼀般⽤于⽣成⼀个列表
type Item struct {
Name string
Price int
}
func iterateHandler(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.ParseFiles("iterate.html"))
items := []Item {
{ "iPhone", 5499 },
{ "iPad", 6331 },
{ "iWatch", 1499 },
{ "MacBook", 8250 },
}
t.Execute(w, items)
}
mux.HandleFunc("/iterate", iterateHandler)
模板⽂件 iterate.html :
<h1>Apple Products</h1>
<ul>
{{ range . }}
<li>{{ .Name }}: ¥{{ .Price }}</li>
{{ end }}
</ul>
在 {{ range }} 中, .会被替换为当前遍历的元素值
设置动作
设置动作允许⽤户在指定范围内为 . 设置值。
type User struct {
Name string
Age int
}
type Pet struct {
Name string
Age int
Owner User
}
func setHandler(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.ParseFiles("set.html"))
pet := Pet {
Name: "Orange",
Age: 2,
Owner: User {
Name: "ls",
Age: 18,
},
}
t.Execute(w, pet)
}
mux.HandleFunc("/set", setHandler)
模板⽂件 set.html :
<h1>Pet Info</h1>
<p>Name: {{ .Name }}</p>
<p>Age: {{ .Age }}</p>
<p>Owner:</p>
{{ with .Owner }}
<p>Name: {{ .Name }}</p>
<p>Age: {{ .Age }}</p>
{{ end }}
在 {{ with .Owner }} 和 {{ end }} 之间,可以直接通过 {{ .Name }} 和 {{ .Age }} 访问宠物主
⼈的信息。
包含动作
包含动作允许⽤户在⼀个模板⾥⾯包含另⼀个模板,从⽽构造出嵌套的模板。
func includeHandler(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.ParseFiles("include1.html", "include2.html"))
t.Execute(w, "Hello World!")
}
模板 include1.html :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Go Web</title>
</head>
<body>
<div>This is in template include1.html</div>
<p>The value of dot is {{ . }}</p>
<hr/>
<p>Don't pass argument to include2.html:</p>
{{ template "include2.html" }}
<hr/>
<p>Pass dot to include2.html</p>
{{ template "include2.html" . }}
<hr/>
</body>
</html>
模板 include2.html :
<p>Get dot of value [{{ . }}]</p>
{{ template "include2.html" }} 未传⼊参数给模板 include2.html , {{ template
"include2.html" . }} 将模板 include1.html 的参数传给了 include2.html 。
管道
管道我们可以理解为数据的流向,在数据流向输出的每个阶段进⾏特定的处理。
func pipelineHandler(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.ParseFiles("pipeline.html"))
t.Execute(w, rand.Float64())
}
mux.HandleFunc("/pipeline", pipelineHandler)
模板⽂件 pipeline.html :
<p>{{ . | printf "%.2f" }}</p>
该程序实现的功能⾮常简单,将传⼊的浮点数格式化为只保留⼩数点后两位。 | 是管道符号,前⾯的输
出将作为后⾯的输⼊(如果是函数或⽅法调⽤,前⾯的输出将作为最后⼀个参数)。 实际上, {{ . |
printf "%.2f" }} 的输出 fmt.Sprintf("%.2f", .表示的数据) 的返回字符串相同。
函数
Go 模板库内置了⼀些基础的函数,如果要实现更为复杂的功能,可以⾃定义函数。
func formateDate(t time.Time) string {
return t.Format("2006-01-02")
}
func funcsHandler(w http.ResponseWriter, r *http.Request) {
funcMap := template.FuncMap{ "fdate": formateDate }
t :=
template.Must(template.New("funcs.html").Funcs(funcMap).ParseFiles("funcs.html"
))
t.Execute(w, time.Now())
}
mux.HandleFunc("/funcs", funcsHandler)
模板⽂件 funcs.html :
<div>Today is {{ . | fdate }}</div>
⾃定义函数可以接受任意多个参数,但是只能返回⼀个值,或者返回⼀个值和⼀个错误。 上⾯代码中,
必须先通过 template.New 创建模板,然后调⽤ Funcs 设置⾃定义函数,最后再解析模板⽂件。
因为模板⽂件中使⽤了 fdate ,未设置之前会解析失败。
上下⽂感知
上下⽂感知是 html/template 库的⼀个⾮常有趣的特性。根据需要替换的⽂本在⽂档中所处的位置,
模板在显示这些内容的时候会对其进⾏相应的修改。 上下⽂感知的⼀个常⻅⽤途就是对内容进⾏转义。
如果需要显示的是 HTML 的内容,那么进⾏ HTML 转义。如果显示的是 JavaScript 内容,那么进⾏
JavaScript 转义。 Go 模板引擎还能识别出内容中的 URL 或 CSS,可以对它们实施正确的转义。
func contextAwareHandler(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.ParseFiles("context-aware.html"))
t.Execute(w, `He saied: <i>"She's alone?"</i>`)
}
mux.HandleFunc("/contextAware", contextAwareHandler)
模板⽂件 context-aware.html :
<div>{{ . }}</div>
<div><a href="/{{ . }}">Path</a></div>
<div><a href="/?q={{ . }}">Query</a></div>
<div><a onclick="f('{{ . }}')">JavaScript</a></div>
编译、运⾏程序,使⽤ curl 访问 localhost:8080/contextAware ,得到下⾯的内容:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Go Web</title>
</head>
<body>
<div>He saied: <i>"She's alone?"</i></div>
<div><a href="/He%20saied:%20%3ci%3e%22She%27s%20alone?%22%3c/i%3e">Path</a>
</div>
<div><a href="/?
q=He%20saied%3a%20%3ci%3e%22She%27s%20alone%3f%22%3c%2fi%3e">Query</a></div>
<div><a onclick="f('He saied: \x3ci\x3e\x22She\x27s alone?
\x22\x3c\/i\x3e')">JavaScript</a></div>
</body>
</html>
依次来看,需要呈现的数据是 He saied: "She's alone?" :
- 第⼀个 div 中,直接在⻚⾯中显示,其中 HTML 标签``和单、双引号都被转义了;
- 第⼆个 div 中,数据出现在 URL 的路径中,所有⾮法的路径字符都被转义了,包括空格、尖括
号、单双引号; - 第三个 div 中,数据出现在查询字符串中,除了 URL 路径中⾮法的字符,还有冒号( : )、问号( ? )和斜杠也被转义了;
- 第四个 div 中,数据出现在 OnClick 代码中,单双引号和斜杠都被转义了。
这四种转义⽅式⼜有所不同,第⼀种转义为 HTML 字符实体,第⼆、三种转义为 URL 转义字符( % 后
跟字符编码的⼗六进制表示) ,第四种转义为 Go 中的⼗六进制字符表示。
防御 XSS 攻击
XSS 是⼀种常⻅的攻击形式。在论坛之类的可以接受⽤户输⼊的⽹站,攻击者可以内容中添
加 <script> 标签。如果⽹站未对输⼊的内容进⾏处理, 其他⽤户浏览该⻚⾯时, <script> 标签中
的内容就会被执⾏,泄露⽤户的私密信息或利⽤⽤户的权限做破坏。
func xssHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
t := template.Must(template.ParseFiles("xss-display.html"))
t.Execute(w, r.FormValue("comment"))
} else {
t := template.Must(template.ParseFiles("xss-form.html"))
t.Execute(w, nil)
}
}
mux.HandleFunc("/xss", xssHandler)
模板⽂件 xss-form.html :
<form action="/xss" method="post">
Comment: <input name="comment" type="text">
<hr/>
<button id="submit">Submit</button>
</form>
模板⽂件 xss-display.html :
{{ . }}
处理器中我们根据请求⽅法的不同进⾏不同的处理。GET 请求返回⼀个表单⻚⾯,POST 请求显示⽤户
输⼊的评论信息。
正常的思路会触发我们插⼊的代码,但是 alert 代码并没有执⾏,为什么?
因为 Go 模板有上下⽂感知的功能,它检测到在 HTML ⻚⾯中,所以输⼊数据会被转义。查看⽹⻚源码
可以看到转义后的结果。转义之后的代码就不会执⾏了。
那么如何才能不转义呢? html/template 提供了 HTML 类型,Go 模板不会对该类型的变量进⾏转义。
如果我们把上⾯的处理器修改为:
func xssHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
t := template.Must(template.ParseFiles("xss-display.html"))
t.Execute(w, template.HTML(r.FormValue("comment")))
} else {
t := template.Must(template.ParseFiles("xss-form.html"))
t.Execute(w, nil)
}
}
再运⾏就能看到弹出的警告框。
XSS的原理
xss攻击更多出现在jquery时代,那时候需要拼接html所以导致当例如有输入时候,如果恶意输入js标签内部包含逻辑,则构成xss共计,所以,需要程序员进行转义encode(可以前端或者后端处理);但是在vue和react时代,框架本身做了编码处理,除非v-html或者react类似的api,其他的在框架本身已经做了编码操作。
解决方案:例如<script>alert('hello')</script>,可以前端进行编码转义去除script标签而保留内部的文本,或者服务端处理也行。
有疑问加站长微信联系(非本文作者)