使用 Go 创建 Web 应用
- 处理请求
- 模板
- 中间件
- 存储数据
- HTTPS,HTTP2
- 测试
- 部署
package main
import "net/http"
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hell world"))
})
http.ListenAndServe("localhost:8080", nil) // 传入 nil,即 DefaultServeMux
}
处理(Handle)请求
- 如何处理(Handle)Web 请求
- http.Handle 函数
- http.HandleFunc 函数
创建 Web Server
http.ListenAndServe(addr string, handler Handler) error
- addr:监听的地址,如果为空字符串,则使用 ":http",即监听 80 端口
- handler:处理请求的 Handler,如果为空,则使用 DefaultServeMux
DefaultServeMux 是一个 multiplexer,即多路复用器,用于将请求分发到不同的处理器(可以看作是路由器)
http.ListenAndServe("localhost:8080", nil)
http.Server 是一个 struct
- Addr 字段表示网络地址
- 如果为 "",则使用 ":http",即监听所有网络接口的 80 端口
- Handler 字段
- 如果为 nil,则使用 DefaultServeMux
- ListenAndServe 方法
// serve := &http.Server{
serve := http.Server{
Addr: "localhost:8080",
Handler: nil,
}
serve.ListenAndServe()
上面两种创建 Web Server 的方式,都只能使用 http。如果要用 https,则需要使用同理的 http.ListenAndServeTLS() 和 server.ListenAndServeTLS() 方法
Handler
Handler 是一个接口
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
自己实现 Handler 接口
type myHandler struct{}
func (m *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello world"))
}
func main() {
mh := myHandler{}
server := http.Server{
Addr: "localhost:8080",
Handler: &mh,
}
server.ListenAndServe()
}
DefaultServeMux
DefaultServeMux 是一个 multiplexer,即多路复用器,用于将请求分发到不同的处理器(可以看作是路由器)
多个 Handler - http.Handle
func Handle(pattern string, handler Handler)
不指定 Server struct 里面的 Handler 字段值(指定为 nil)
可以使用 http.Handle 将某个 Handler 附加到 DefaultServeMux 上
- http 包有一个 Handle 函数
- ServerMux struct 也有一个 Handle 方法
如果调用 http.Handle,实际上调用的是 DefaultServeMux 的 Handle 方法
- DefaultServeMux 就是 ServerMux 的指针变量
type helloHandler struct{}
func (h *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello world"))
}
type aboutHandler struct{}
func (a *aboutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("About!"))
}
func main() {
hello := helloHandler{}
about := aboutHandler{}
server := http.Server{
Addr: "localhost:8080",
Handler: nil, // DefaultServeMux
}
http.Handle("/hello", &hello)
http.Handle("/about", &about)
server.ListenAndServe()
}
Handler 函数 - http.HandleFunc
Handler 函数就是那些行为与 handler 类似的函数:
- Handler 函数的签名与 ServeHTTP 方法的签名一样,接收
- http.ResponseWriter
- 指向 http.Request 的指针
http.HandleFunc 原理
- Go 有一个函数类型 HandlerFunc。可以将某个具有适当签名的函数 f,适配成为一个 Handler,而这个 Handler 就是调用 f 本身
type helloHandler struct{}
func (h *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello world"))
}
type aboutHandler struct{}
func (a *aboutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("About!"))
}
func welcome(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Welcome!"))
}
func main() {
hello := helloHandler{}
about := aboutHandler{}
server := http.Server{
Addr: "localhost:8081",
Handler: nil, // DefaultServeMux
}
http.Handle("/hello", &hello)
http.Handle("/about", &about)
http.HandleFunc("/home", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Home!"))
})
// http.HandleFunc("/welcome", welcome)
http.Handle("/welcome", http.HandlerFunc(welcome))
server.ListenAndServe()
}
http.HandleFunc
- http.HandleFunc(pattern string, handler func(ResponseWriter, *Request))
- type HandlerFunc func(ResponseWriter, *Request)
内置的 handlers
- NotFoundHandler
- RedirectHandler
- StripPrefix
- TimeoutHandler
- FileServer
http.NotFoundHandler
- func NotFoundHandler() Handler
- 返回一个 handler,它给每个请求的响应都是 “404 page not found”
http.RedirectHandler
- func RedirectHandler(url string, code int) Handler
- 返回一个 handler,它把每个请求使用给定的状态码跳转到指定的 URL
- url,要跳转到的 URL
- code,跳转的状态码(3xx),常见的:StatusMovedPermanently,StatusFound,StatusSeeOther,StatusTemporaryRedirect,StatusPermanentRedirect
http.StripPrefix
- func StripPrefix(prefix string, h Handler) Handler
- 返回一个 handler,它从请求的 URL 中去掉指定的前缀,然后再调用另一个 handler
- 如果请求的 URL 与提供的前缀不符,那么 404
- 略像中间件
- prefix,URL 将要被移除的字符串前缀
- h,是一个 handler,在移除字符串前缀之后,这个 handler 将会收到请求
- 修饰了另一个 handler
http.TimeoutHandler
- func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler
- 返回一个 handler,它在指定的时间内处理请求,如果超时,就返回一个错误信息
- 也相当于是一个修饰器
- h,将要被修饰的 handler
- dt,第一个 handler 允许的处理时间
- msg,如果超时,那么就把 msg 返回给请求,表示响应时间过长
http.FileServer
- func FileServer(root FileSystem) Handler
- 返回一个 handler,它会在 root 中寻找文件,并将其提供给请求
type FileSystem interface {
Open(name string) (File, error)
}
- 使用时需要用到操作系统的文件系统,所以还需要委托给
- type Dir string
- func (d Dir) Open(name string) (File, error)
例子
通过 localhost:8081/ 访问 index.html
/* 方法1 */
// http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// http.ServeFile(w, r, "wwwroot" + r.URL.Path)
// })
// http.ListenAndServe(":8081", nil)
/* 方法2 */
http.ListenAndServe(":8081", http.FileServer(http.Dir("wwwroot")))
请求
- HTTP 请求
- Request
- URL
- Header
- Body
HTTP 消息
- HTTP Request 和 HTTP Response(请求和响应)
- 它们具有相同的结构
- 请求(响应)行
- 0 个或多个 Header
- 空行
- 可选的消息体(Body)
- net/http 包提供了用于表示 HTTP 消息的结构
请求 Request
- Request 是个 struct,代表了客户端发送的 HTTP 请求消息
- 重要的字段
- URL
- Header
- Body
- Form、PostForm、MultipartForm
- 也可以通过 Request 的方法访问请求中的 Cookie、URL、User Agent 等信息
- Request 既可以代表发送到服务器的请求,又可代表客户端发出的请求
请求的 URL
- Request 的 URL 字段就代表了请求行(请求信息第一行)里面的部分内容
- URL 字段是指向 url.URL 类型的一个指针,url.URL 是一个 struct:
URL 的通用格式:scheme://[userinfo@]host/path[?query][#fragment]
不以斜杠开头的 URL 被解释为:scheme:opaque[?query][#fragment]
type URL struct {
Scheme string
Opaque string // 编码后的不透明数据
User *Userinfo // 用户名和密码信息
Host string // host 或 host:port
Path string
RawPath string // 编码后的 path,保留了转义符
ForceQuery bool // 是否在 URL 中添加 ? 强制添加查询参数
RawQuery string // 编码后的查询字符串,没有 '?'
Fragment string // 引用的片段(文档位置),没有 '#'
}
URL Query
- RawQuery 会提供实际查询的字符串
- 例如:
http://localhost:8080/?name=abc&age=18
- RawQuery 为:
name=abc&age=18
- RawQuery 为:
URL Fragment
- 如果从浏览器发出的请求,就无法提取出 Fragment 字段的值
- 浏览器在发送请求时会把 fragment 部分去掉
- 但不是所有的请求都是从浏览器发出的(例如从 http 客户端包)
Request Header
- 请求和响应(Request、Response)的 headers 是通过 Header 类型来描述的,它是一个 map,用来表述 HTTP Header 里的 Key-Value 对
- Header map 的 key 是 string 类型,value 是一个字符串切片 []string
- 设置 key 的时候会创建一个空的 []string 作为 value,value 里面第一个元素就是新 header 的值
- 为指定的 key 添加一个新的 header 值,执行 append 操作即可
- r.Header,返回 map
- r.Header["Accept-Encoding"],返回 [gzip, deflate],[]string 类型
- r.Header.Get("Accept-Encoding"),返回 gzip, deflate,string 类型
server := http.Server{
Addr: "localhost:8081",
}
http.HandleFunc("/header", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, r.Header)
fmt.Fprintln(w, r.Header["Accept-Encoding"])
fmt.Fprintln(w, r.Header.Get("Accept-Encoding"))
})
server.ListenAndServe()
Request Body
- 请求和响应的 bodies 都是使用 Body 字段来表示的
- Body 是一个 io.ReadCloser 接口
type ReadCloser interface {
Reader
Closer
}
- Reader 接口定义了一个 Open() 方法
- 参数:[]byte
- 返回:byte 的数量、可选的错误
- Closer 接口定义了一个 Close() 方法
- 返回:可选的错误
- 想要读取请求 Body 的内容,可以调用 Body 的 Read 方法
server := http.Server{
Addr: "localhost:8081",
}
http.HandleFunc("/post", func(w http.ResponseWriter, r *http.Request) {
length := r.ContentLength
body := make([]byte, length)
r.Body.Read(body)
fmt.Fprintln(w, body)
fmt.Fprintln(w, string(body))
})
server.ListenAndServe()
查询参数(Query Parameters)
URL Query
- http://localhost:8080/?name=abc&age=18
- r.URL.RawQuery 为:
name=abc&age=18
(实际查询的原始字符串) - r.URL.Query() 方法返回 map[string][]string
- map 的 key 是 string 类型
- map 的 value 是 []string 类型
- r.URL.RawQuery 为:
// http://localhost:8081/query?id=123&name=张三&id=466&name=李四
server := http.Server{
Addr: "localhost:8081",
}
http.HandleFunc("/query", func(w http.ResponseWriter, r *http.Request) {
url := r.URL
query := url.Query()
id := query["id"]
log.Println(id)
name := query.Get("name")
log.Println(name)
})
server.ListenAndServe()
Form
Request 上的函数允许从 URL 或 / 和 Body 中提取数据,通过如下字段
- Form
- PostForm
- MultipartForm
- FormValue
- PostFormValue
- FormFile
- MultiPartReader
Form 里面的数据是 key-value 对
通常的做法是:
- 先调用 ParseForm 或 ParseMultipartForm 来解析 Request
- 然后相应地访问 Form、PostForm、MultipartForm 字段
<form
action="http://localhost:8080/process"
method="post"
enctype="application/x-www-form-urlencoded"
>
<input type="text" name="name" placeholder="Name" />
<input type="text" name="email" placeholder="Email" />
<button type="submit">Submit</button>
</form>
server := http.Server{
Addr: "localhost:8080",
}
http.HandleFunc("/process", func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
fmt.Fprintln(w, r.Form) // map[email:[2439639832@qq.com] name:[客户1号]]
})
server.ListenAndServe()
PostForm 字段
- 上例中,如果只想得到 name 这个 Key 的 Value,可以使用 r.Form["name"],它返回含有一个元素的 slice:["客户 1 号"]
- 如果表单和 URL 里有同样的 Key,那么它们都会放在一个 slice 里:表单里的值靠前,URL 的值靠后
- 如果只想要表单的 key-value 对,不要 URL 的,可以使用 PostForm 字段
<form
action="http://localhost:8080/process?name=客户2号"
method="post"
enctype="application/x-www-form-urlencoded"
>
<input type="text" name="name" placeholder="Name" />
<input type="text" name="email" placeholder="Email" />
<button type="submit">Submit</button>
</form>
server := http.Server{
Addr: "localhost:8080",
}
http.HandleFunc("/process", func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
fmt.Fprintln(w, r.Form) // map[email:[2439639832@qq.com] name:[客户1号 客户2号]]
fmt.Fprintln(w, r.PostForm) // map[email:[2439639832@qq.com] name:[客户1号]]
})
server.ListenAndServe()
MultipartForm 字段
- PostForm 只支持 application/x-www-form-urlencoded 编码
- 要想得到 multipart/form-data 对,必须使用 MultipartForm 字段
- 要想使用 MultiPartForm 字段,必须先调用 ParseMultipartForm 方法
- 该方法会在必要时调用 ParseForm 方法
- 参数是需要读取数据的长度
- MultipartForm 只包含表单的 key-value 对
- 返回类型是一个 struct,这个 struct 里面有两个 map:
- key 是 string,value 是 []string
- key 是 string,value 是 文件
<form
action="http://localhost:8080/process?name=客户2号"
method="post"
enctype="multipart/form-data"
>
<input type="text" name="name" placeholder="Name" />
<input type="text" name="email" placeholder="Email" />
<button type="submit">Submit</button>
</form>
server := http.Server{
Addr: "localhost:8080",
}
http.HandleFunc("/process", func(w http.ResponseWriter, r *http.Request) {
r.ParseMultipartForm(1024)
fmt.Fprintln(w, r.MultipartForm) // &{map[email:[2439639832@qq.com] name:[客户1号]] map[]}
})
server.ListenAndServe()
FormValue 和 PostFormValue 方法
- FormValue 会返回 Form 字段中指定 key 对应的第一个 value
- 无需调用 ParseForm 或 ParseMultipartForm
- PostFormValue 也一样,但只能读取 PostForm 字段
- FormValue 和 PostFormValue 都会调用 ParseMultipartForm 方法
上传文件
multipart/form-data 最常见的应用场景就是上传文件
- 首先调用 ParseMultiPartForm 方法
- 从 File 字段获得 FileHeader,调用其 Open 方法来获得文件
- 可以使用 io.ReadAll 函数把文件内容读取到 byte 切片里
<form
action="http://localhost:8080/process?name=客户2号"
method="post"
enctype="multipart/form-data"
>
<input type="text" name="name" placeholder="Name" />
<input type="text" name="email" placeholder="Email" />
<input type="file" name="file" />
<button type="submit">Submit</button>
</form>
func process(w http.ResponseWriter, r *http.Request) {
r.ParseMultipartForm(1024)
fileHeader := r.MultipartForm.File["file"][0]
file, err := fileHeader.Open()
if err == nil {
data, err := io.ReadAll(file)
if err == nil {
fmt.Fprintln(w, string(data))
}
}
}
func main() {
server := http.Server{
Addr: "localhost:8080",
}
http.HandleFunc("/process", process)
server.ListenAndServe()
}
FormFile 方法
- 上传文件还有一个简便方法:FormFile
- 无需调用 ParseMultipartForm 方法
- 返回指定 key 对应的第一个 value
- 同时返回 File 和 FileHeader,以及错误信息
- 如果只上传一个文件,那么这种方式会快一些
func process(w http.ResponseWriter, r *http.Request) {
// r.ParseMultipartForm(1024)
// fileHeader := r.MultipartForm.File["file"][0]
// file, err := fileHeader.Open()
file, _, err := r.FormFile("file")
if err == nil {
data, err := io.ReadAll(file)
if err == nil {
fmt.Fprintln(w, string(data))
}
}
}
MultiPartReader()
- func (r *Request) MultiPartReader() (*multipart.Reader, error)
- 如果是 multipart/form-data 或 multipart 混合的 POST 请求
- MultiPartReader 方法会返回一个 MIME multipart reader
- 否则返回一个错误
- 可以使用该函数代替 ParseMultipartForm,来把请求的 body 作为 stream 进行处理
- 不是把表单作为一个对象来处理的,不是一次性获得整个 map
- 逐个检查来自表单的值,然后每次处理一个
POST 请求 - JSON Body
- 不是所有的 POST 请求都来自 Form
- 客户端框架(例如 Angular 等)会议不同的方式对 POST 请求编码:
- jQuery 通常使用 application/x-www-form-urlencoded
- Angular 通常使用 application/json
- ParseForm 方法无法处理 application/json
响应
ResponseWriter
- 从服务器向客户端返回响应需要使用 ResponseWriter
- ResponseWriter 是一个接口,handler 用它来返回响应
- 真正支撑 ResponseWriter 的幕后 struct 是非导出的 http.response
写入到 ResponseWriter
- Write 方法接收一个 byte 切片作为参数,然后把它写入到 HTTP 响应的 body 里
- 如果在 Write 方法被调用时, header 里面没有设定 content-type,那么数据的前 512 字节就会用来被检测 content type
func writeExample(w http.ResponseWriter, r *http.Request) {
str := `<html>
<head><title>Go Web</title></head>
<body><h1>Hello World</h1></body>
</html>
`
w.Write([]byte(str))
}
func main() {
server := http.Server{
Addr: "localhost:8080",
}
http.HandleFunc("/write", writeExample)
server.ListenAndServe()
}
curl -i localhost:8080/write
# HTTP/1.1 200 OK
# Date: Sun, 15 Oct 2023 08:38:27 GMT
# Content-Length: 84
# Content-Type: text/html; charset=utf-8
# <html>
# <head><title>Go Web</title></head>
# <body><h1>Hello World</h1></body>
# </html>
WriteHeader 方法
- WriteHeader 方法接收一个整数类型(HTTP 状态码)作为参数,并把它作为 HTTP 响应的状态码返回
- 如果该方法没有显示调用,那么在第一次调用 Write 方法前,会隐式地调用 WriteHeader(http.StatusOK)
- 所以 WriteHeader 主要用来发送错误类的 HTTP 状态码
- 调用完 WriteHeader 方法之后,仍然可以写入到 ResponseWriter,但无法再修改 header 了
func writeHeaderExample(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(501)
fmt.Fprintln(w, "No such service, try next door")
}
func main() {
server := http.Server{
Addr: "localhost:8080",
}
http.HandleFunc("/writeheader", writeHeaderExample)
server.ListenAndServe()
}
curl -i localhost:8080/writeheader
# HTTP/1.1 501 Not Implemented
# Date: Sun, 15 Oct 2023 12:44:53 GMT
# Content-Length: 31
# Content-Type: text/plain; charset=utf-8
# No such service, try next door
Header 方法
- Header 方法返回 headers 的 map,可以进行修改
- 修改后的 headers 将会体现在返回给客户端的 HTTP 响应里
type Post struct {
User string
Threads []string
}
func headerExample(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "http://google.com")
w.WriteHeader(302)
}
func jsonExample(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
post := &Post{
User: "Sau Sheong",
Threads: []string{"first", "second", "third"},
}
json, _ := json.Marshal(post)
w.Write(json)
}
func main() {
server := http.Server{
Addr: "localhost:8080",
}
http.HandleFunc("/header", headerExample)
http.HandleFunc("/json", jsonExample)
server.ListenAndServe()
}
curl -i localhost:8080/header
# HTTP/1.1 302 Found
# Location: http://google.com
# Date: Sun, 15 Oct 2023 12:50:47 GMT
# Content-Length: 0
curl -i localhost:8080/json
# HTTP/1.1 200 OK
# Content-Type: application/json
# Date: Sun, 15 Oct 2023 12:56:51 GMT
# Content-Length: 58
# {"User":"Sau Sheong","Threads":["first","second","third"]}
内置的 Response
- NotFound 函数,包装一个 404 状态码和一个额外的信息
- ServeFile 函数,从文件系统提供文件,返回给请求者
- ServeContent 函数,它可以把实现了 io.ReadSeeker 接口的任何东西里面的内容返回给请求者
- 还可以处理 Range 请求(范围请求),如果只请求了资源的一部分内容,那么 ServeContent 就可以如此响应。而 ServeFile 或 io.Copy 就不行
- Redirect 函数,告诉客户端重定向到另一个 URL
模板
- Web 模板就是预先设计好的 HTML 页面,它可以被模板引擎反复的使用,来产生 HTML 页面
- Go 的标准库提供了 text/template 和 html/template 两个模板库
- 大多数 Go 的 Web 框架都使用这些库作为默认的模板引擎
模板与模板引擎
模板引擎可以合并模板与上下文数据,产生最终的 HTML
Go 的模板引擎
- 主要使用的是 text/template,HTML 相关的部分使用了 html/template,是个混合体
- 模板可以完全无逻辑,但又具有足够的嵌入特性
- 和大多数模板引擎一样,Go Web 的模板位于无逻辑和嵌入逻辑之间的某个地方
关于模板
- 模板必须是可读的文本格式,扩展名任意。对于 Web 应用通常就是 HTML
- 里面会内嵌一些命令(叫作 action)
- text/template 是通用模板引擎,html/template 是 HTML 模板引擎
- action 位于双层花括号之间:
{{ . }}
- 这里的 . 就是一个 action
- 它可以命令模板引擎将其替换成一个值
使用模板引擎
- 解析模板源(可以是字符串或模板文件),从而创建一个解析好的模板的 struct
- 执行解析好的模板,并传入 ResponseWriter 和数据
- 这会触发模板引擎组合解析好的模板和数据,来产生最终的 HTML,并将它传递给 ResponseWriter
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Document</title>
<style></style>
</head>
<body>
{{ . }}
</body>
</html>
func process(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("tmpl.html")
t.Execute(w, "Hello World")
}
func main() {
server := http.Server{
Addr: "localhost:8080",
}
http.HandleFunc("/process", process)
server.ListenAndServe()
}
解析模板
- ParseFiles
- ParseGlob
- Parse
ParseFiles
- 解析模板文件,并创建一个解析好的模板 struct,后续可以被执行
- ParseFiles 函数是 Template struct 上 ParseFiles 方法的简便调用
- 调用 ParseFiles 后,会创建一个新的模板,模板名字是文件名
- New 函数
- ParseFiles 的参数数量可变,但只返回一个模板
- 当解析多个文件时,第一个文件作为返回的模板(名,内容),其余的作为 map,供后续执行使用
// t, _ := template.ParseFiles("tmpl.html")
t := template.New("tmpl.html")
t, _ = t.ParseFiles("tmpl.html")
ParseGlob
- 使用模式匹配来解析特定的文件
t, _ := template.ParseGlob("*.html")
Parse
- 可以解析字符串模板,其他方式最终都会调用 Parse
Lookup
- 通过模板名来寻找模板,如果没找到就返回 nil
Must
- 可以包裹一个函数,返回到一个模板的指针和一个错误
- 如果错误不为 nil,那么就 panic
执行模板
- Execute
- 参数是 ResponseWriter、数据
- 单模板:很适用
- 模板集:只用第一个模板
- ExecuteTemplate
- 参数是 ResponseWriter、模板名、数据
- 模板集:很适用
func process(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("t1.html")
t.Execute(w, "Hello World")
ts, _ := template.ParseFiles("t2.html", "t3.html")
ts.ExecuteTemplate(w, "t2.html", "Hello World")
}
Action
- Action 就是 Go 模板中嵌入的命令,位于两组花括号之间
- . 就是一个 Action,而且是最重要的一个。它代表了传入模板的数据
- Action 主要可以分为 5 类
- 条件类
- 迭代/遍历类
- 设置类
- 包含类
- 定义类
条件 Action
语法
{{ if arg }}
some content
{{ end }}
{{ if arg }}
some content
{{ else }}
some content
{{ end }}
{{ if arg }}
some content
{{ else if arg }}
some content
{{ else }}
some content
{{ end }}
demo
<body>
{{ if . }} Number is greater than 5 {{ else }} Number is 5 or less {{ end }}
</body>
func main() {
http.HandleFunc("/process", process)
http.ListenAndServe("localhost:8080", nil)
}
var rng = rand.New(rand.NewSource(time.Now().UnixNano()))
func process(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("tmpl.html")
t.Execute(w, rng.Intn(10) > 5)
}
迭代/遍历 Action
语法
{{ range array }}
some content {{ . }}
{{ end }}
- 这类 Action 用来遍历数组、slice、map 或 channel 等数据结构
- “.” 用来表示每次迭代循环中的元素
<ul>
{{range .}}
<li>{{.}}</li>
<!-- 回落机制 -->
{{ else }}
<li>Empty list</li>
{{end}}
</ul>
func main() {
http.HandleFunc("/process", process)
http.ListenAndServe("localhost:8080", nil)
}
func process(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("tmpl.html")
daysOfWeek := []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}
t.Execute(w, daysOfWeek)
}
设置 Action
语法
{{ with arg }}
.
{{ end }}
- 它允许在指定范围内,让“.”来表示其它指定的值(arg)
demo
<body>
<div>The dot is set to {{ . }}</div>
<div>{{ with "world" }} Now the dot is set to {{ . }} {{ end }}</div>
<div>The dot is {{ . }} again</div>
</body>
<!-- 回落机制 -->
<body>
<div>The dot is set to {{ . }}</div>
<div>
{{ with "" }} Now the dot is set to {{ . }} {{ else }} The dot is still {{ .
}} {{ end }}
</div>
<div>The dot is {{ . }} again</div>
</body>
func process(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("tmpl.html")
t.Execute(w, "hello")
}
包含 Action
语法
{{ template "name"}}
// 给被包含的模板传递参数
{{ template "name" arg }}
- 它允许在模板中包含其他的模板
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Document</title>
<style></style>
</head>
<body>
<div>this is t1.html</div>
<div>This is the value of the dot in t1.html - [{{ . }}]</div>
<hr />
{{ template "t2.html" . }}
<hr />
<div>This is t1.html after</div>
</body>
</html>
<div style="background-color: yellow">
This is t2.html <br />
This is the value of the dot in t2.html - [{{ . }}]
</div>
func process(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("tmpl.html", "t2.html")
t.Execute(w, "hello")
}
定义 Action
define action
函数和管道
参数
- 参数就是模板里面用到的值
- 模板里面的参数可以是任何类型的值
- 参数可以是变量、方法(返回单个值或返回一个值和一个错误)或函数
- 参数可以是一个“.”,也就是传入到模板引擎的那个值
在 Action 中设置变量
- 可以在 action 中设置变量,变量以 $ 开头:
- $variable := value
- 一个迭代 action 的例子
{{ range $key, $value := . }}
{{ $key }}: {{ $value }}
{{ end }}
管道
- 管道是按顺序连接到一起的参数、函数和方法
- 和 Unix 的管道类似
- 例如:
{{ p1 | p2 | p3 }}
- p1、p2、p3 要么是参数,要么是函数
- 管道允许把参数的输出发给下一个参数,下一个参数由管道(|)分隔开
<body>
<!-- 展示 12.35 -->
{{ 12.3456 | printf "%.2f" }}
<!-- 等价于 -->
{{ printf "%.2f" 12.3456 }}
</body>
函数
- 参数可以是一个函数
- Go 模板引擎提供了一些基本的内置函数,功能比较有限。例如 fmt.Sprint 的各类变体
- 开发者可以自定义函数
- 可以接收任意数量的输入参数
- 返回:
- 一个值
- 一个值 + 一个错误
内置函数
- define\template\block
- html\js\urlquery。对字符串进行转义,防止安全问题
- 如果是 Web 模板,那么不会需要经常使用这些函数
- index
- print/printf/println
- len
- with
自定义函数
template.Funcs(funcMap FuncMap) *Template
type FuncMap map[string]interface{}
创建自定义函数的步骤:
- 创建一个 FuncMap 类型的变量
- key 是函数名
- value 就是函数
- 把 FuncMap 附加到模板
<body>
<!-- 自定义函数可以在管道中使用,更强大灵活 -->
{{ . | fdate }}
<!-- 自定义函数也可以作为正常函数使用 -->
{{ fdate . }}
</body>
func process(w http.ResponseWriter, r *http.Request) {
// 常见用法:
// template.New("").Funcs(funcMap).Parse(...)
// 调用顺序十分重要
funcMap := template.FuncMap{"fdate": formatDate}
t:= template.New("index.html").Funcs(funcMap)
t.ParseFiles("index.html")
t.Execute(w, time.Now())
}
func formatDate(t time.Time) string {
layout := "2006-01-02"
return t.Format(layout)
}