分享
  1. 首页
  2. 文章

GoWeb

强某某 · · 1870 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

HTTPS通信原理

HTTPS(Secure Hypertext Transfer Protocol)安全超文本传输协议 它是一个安全通信通道。

HTTPS是HTTP over SSL/TLS,HTTP是应用层协议,TCP是传输层协议,在应用层和传输层之间,增加了一个安全套接层SSL。

1.png

服务器 用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 处理。*/

多路复用器

2.jpg
  • 客户端发送请求;
  • 服务器中的多路复用器收到请求;
  • 多路复用器根据请求的 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())

上面程序运行输出字符串:

http://example.com/posts?page=1&count=10#main

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 index,element := range pipeline

这样就可以在循环中通过 index 和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: &lt;i&gt;&#34;She&#39;s alone?&#34;&lt;/i&gt;</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标签而保留内部的文本,或者服务端处理也行。


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:强某某

查看原文:GoWeb

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
1870 次点击
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏