Go 的 HTTP 服务端实现
1. 基本结构
go 官方提供了 net/http
来做 HTTP 协议的客户端及服务端处理。这里只说服务端部分。
虽然 go 的社区中有很多的 web 应用层框架,但是因为 net/http
的封装是比较高级的,所以社区建议也是以它为准。个人的理解,不管上层框架做了什么事,大概还是会依据相同的 API 设计。
另外, net/http
是同步的实现,先随大流吧。
package main import ( "net/http" ) func HelloHandler(response http.ResponseWriter, request *http.Request) { response.Write([]byte("Hello")) } func main () { http.HandleFunc("/", HelloHandler) println("Server is starting on 8888 ...") http.ListenAndServe(":8888", nil) }
基本结构上, net/http
已经是像一个 Web 服务框架了:
HandleFunc
完成路径的映射。HelloHandler
处理逻辑,参数中一个“响应”,一个“请求”。ListenAndServe
完成端口监听。
作为静态语言,几行代码就完成了一个 Web 服务端的实现,还是很能体会到“时代进步”的。
2. 获取参数
http.Request
是一个面向数据的比较原始的对象,不是面向 HTTP 抽象概念的封装(你需要了解 HTTP 的原始报文,才知道这句话说的什么)。
它没有提供像 GetParams()
之类的方法,只提供了 URL 对象,所以,你需要自己知道,获取 GET 参数得从 URL 中解析。
2.1. GET 和 URL 对象
package main import ( "net/http" "fmt" ) func HelloHandler(response http.ResponseWriter, request *http.Request) { var n string var name []string = request.URL.Query()["name"] if len(name) > 0 { n = name[0] } response.Write([]byte("Hello " + n)) } func main () { http.HandleFunc("/", HelloHandler) fmt.Println("Server is starting on 8888 ...") http.ListenAndServe(":8888", nil) }
通过 URL
的 Query()
方法返回的对象获取指定的参数。因为同名参数可以是多个(实践中几乎不会使用重复名字的参数),所以 name
是一个字符串列表,可能是一个空列表。所以在获取具体值之前要进行非空判断,否则会发生运行时错误。
request.URL
是一个单独的 url.URL
对象,结构上类似标准的 URI 。
[scheme:][//[[email protected]]host][/]path[?query][#fragment]
- 或者
scheme:opaque[?query][#fragment]
URI 的各部分,都可以直接取到。但是注意,这里没有 Hostname
和 Port
,这两个要通过方法获取。
query
部分的解析,由 Query()
方法单独处理,返回的是 Values
结构,实际上就是一个 Map 。
这个 Values
提供了一个 Get()
方法,可以返回指定的 key
的第一个值,并且是 decode 之后的值。
package main import "net/url" import "fmt" func main() { u, err := url.Parse("https://zys.me?a=123&b=%E9%82%B9&b=1") if err != nil { panic(err) } fmt.Println(u.Query().Get("b")) fmt.Println(u.Query().Has("c")) }
Get()
方法才应该是获取 GET
参数的正解:
func HelloHandler(response http.ResponseWriter, request *http.Request) { var n string = request.URL.Query().Get("name") response.Write([]byte("Hello " + n)) }
即使不存在 name
参数,也可以得到一个空字符串,不会报错。
GET 的参数也可以从获取 POST 参数的 Form
中一起获取到。
2.2. 获取头
使用 request.Header
获取请求的头, Header
是一个 map ,但是它的 Get()
方法对头的名字做了兼容性处理。所以不需要担心大小写,横杠问题。
func HelloHandler(response http.ResponseWriter, request *http.Request) { var t string = request.Header.Get("Content-Type") fmt.Println(t) response.Write([]byte("Hello " + t)) }
这里写 Content-Type
或者 Content-type
都可以正常工作的。可以简单理解成,横杠写对,无视大小写就可以了。
2.3. POST
POST 参数的获取,先要调用一下 ParseForm()
,然后可以从 Form
这个 map 中获取。
package main import ( "net/http" "fmt" ) func HelloHandler(response http.ResponseWriter, request *http.Request) { request.ParseForm() fmt.Println(request.Form) var n string = request.Form.Get("name") response.Write([]byte("Hello " + n)) } func main () { http.HandleFunc("/", HelloHandler) fmt.Println("Server is starting on 8888 ...") http.ListenAndServe(":8888", nil) }
request.Form
会包括GET
和POST
的参数。- 同名的参数会放在一个列表当中,
POST
的在前,GET
的在后。(Get()
会优先取到POST
参数)
如果请求的 Content-Type 不是 application/x-www-form-urlencoded
的话:
客户端请求:
# -*- coding: utf-8 -*- import requests res = requests.post('http://localhost:8888', data="123456", headers={"Content-Type": "plain/text"}) print(res.text)
服务端处理:
package main import ( "net/http" "fmt" "io/ioutil" ) func HelloHandler(response http.ResponseWriter, request *http.Request) { var content_type string = request.Header.Get("Content-Type") fmt.Println("Content-Type", content_type) var body []byte body, err := ioutil.ReadAll(request.Body) if err != nil { fmt.Println("Error", err) return } response.Write([]byte("Hello ")) response.Write(body) response.Write([]byte(" ")) response.Write([]byte(string(body))) } func main () { http.HandleFunc("/", HelloHandler) fmt.Println("Server is starting on 8888 ...") http.ListenAndServe(":8888", nil) }
- 通过
request.Body
获取原始内容。 request.Body
是一个 io.ReadCloser 接口实现。request.Body
虽然有Close()
,但是不需要手工关闭。
2.4. 获取文件
文件的处理跟 POST 参数差不多,使用 ParseMultiparseForm()
代替 ParseForm()
:
客户端请求:
# -*- coding: utf-8 -*- import requests f = open('/home/zys/temp/a.svg', 'rb') res = requests.post('http://localhost:8888', files={"file": f}, data={"name": "123"}) print(res.text)
服务端处理:
package main import ( "net/http" "fmt" "io/ioutil" ) func HelloHandler(response http.ResponseWriter, request *http.Request) { fmt.Println(request.Method) var err error = request.ParseMultipartForm(1024 * 1024 * 5) if err != nil { fmt.Println(err) return } file, file_header, err := request.FormFile("file") if err != nil { fmt.Println(err) return } fmt.Println("file_name", file_header.Filename) fmt.Println("file_size", file_header.Size) fmt.Println(file_header.Header) body, err := ioutil.ReadAll(file) if err != nil { fmt.Println(err) return } fmt.Println(string(body)) var name string = request.Form.Get("name") response.Write([]byte("Hello " + name)) } func main () { http.HandleFunc("/", HelloHandler) fmt.Println("Server is starting on 8888 ...") http.ListenAndServe(":8888", nil) }
- 先调用
request.ParseMultiparseForm(max)
,需要指定缓冲区的最大字节长度。它也会做ParseForm()
的事。 - 使用
request.FormFile()
获取指定参数的文件内容。 file_header
有文件基本信息,文件名,大小。- 文件内容是通过
file
,作为一个文件对象处理的。
3. 写头与非 200 响应
response.Header()
方法可以获取响应中的头对象, Set()
方法完成值的设置:
func HelloHandler(response http.ResponseWriter, request *http.Request) { var header http.Header = response.Header() header.Set("Content-Type", "text/ttt") response.Write([]byte("Hello")) }
状态码使用 response.WriteHeader()
方法处理(奇怪的名字):
func HelloHandler(response http.ResponseWriter, request *http.Request) { response.WriteHeader(503) response.Write([]byte("Hello")) }
response.WriteHeader()
的调用必须在第一个 response.Write()
前,否则 response.Write()
会写入 200 的状态码,之后再调用 response.WriteHeader()
也没有作用了。
4. 读写 Cookie
4.1. Cookie 结构
http.Cookie 提供了 Cookie 的结构支持:
type Cookie struct { Name string Value string Path string // optional Domain string // optional Expires time.Time // optional RawExpires string // for reading cookies only // MaxAge=0 means no 'Max-Age' attribute specified. // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' // MaxAge>0 means Max-Age attribute present and given in seconds MaxAge int Secure bool HttpOnly bool SameSite SameSite Raw string Unparsed []string // Raw text of unparsed attribute-value pairs }
同时,它还有一个 String()
方法,可以直接用于单条 cookie 头的设置:
func HelloHandler(response http.ResponseWriter, request *http.Request) { var cookie *http.Cookie = &http.Cookie{ Name: "first", Value: "abcc", } response.Header().Add("Set-Cookie", cookie.String()) response.Write([]byte("Hello")) }
4.2. 写 Cookie
除了直接使用 response.Header().Add()
,把 cookie 作为普通头处理之外, http 也提供了一个 SetCookie()
静态函数:
func HelloHandler(response http.ResponseWriter, request *http.Request) { var cookie *http.Cookie = &http.Cookie{ Name: "first", Value: "abcc", } http.SetCookie(response, cookie) response.Write([]byte("Hello")) }
4.3. 读 Cookie
Cookie 的获取,可以直接读取头自己解析,也可以通过 request
中的 Cookies()
和 Cookie(name)
方法处理。
Cookies()
返回的是一个 http.Cookie
对象的列表。 Cookie(name)
就是一个 http.Cookie
对象。
func HelloHandler(response http.ResponseWriter, request *http.Request) { fmt.Println("all", request.Cookies()[0]) var cookie *http.Cookie cookie, err := request.Cookie("soup") if err != nil { fmt.Println(err) return } fmt.Println("name", cookie.Name) fmt.Println("value", cookie.Value) response.Write([]byte("Hello")) }
5. 多应用和路径映射
在最前面,我们看到的代码示例是:
package main import ( "net/http" ) func HelloHandler(response http.ResponseWriter, request *http.Request) { response.Write([]byte("Hello")) } func main () { http.HandleFunc("/", HelloHandler) println("Server is starting on 8888 ...") http.ListenAndServe(":8888", nil)
http
下的几个直接的方法,像 ListenAndServe
和 HandleFunc
明显是一些快捷方法。
完整点的结构的话:
http.Server
是定义一个服务。Multiplexer
是管理一套路径映射。 go 中没有 Application 的概念。Handler
是一个接口。
5.1. Server 与多应用
http.Server
可以定义一个 HTTP 服务,并且使用已有的路径映射。
package main import ( "net/http" "fmt" ) func HelloHandler(response http.ResponseWriter, request *http.Request) { fmt.Println("all", request.Cookies()[0]) var cookie *http.Cookie cookie, err := request.Cookie("soup") if err != nil { fmt.Println(err) return } fmt.Println("name", cookie.Name) fmt.Println("value", cookie.Value) response.Write([]byte("Hello")) } func main () { http.HandleFunc("/", HelloHandler) fmt.Println("Server is starting on 8888 ...") go func(){ var srv = &http.Server{ Addr: "127.0.0.1:8889", } srv.ListenAndServe() }() http.ListenAndServe(":8888", nil) }
在一个 goroutine 有另一外监听到 8889 的服务,同时在“主进程”中,原来的 8888 也是活动的,这样程序就不会直接结束。此时,两个端口都可以正常响应。
http.HandleFunc()
会产生一个默认的 DefaultServeMux
, Server()
中没有指定 Handler
的话,就会使用它。
通过 goroutine 配合 Server
,启动多个服务是简单的,go 中的 Server
另外一些有价值的 API ,是“优雅关闭”的内置支持。
Close()
方法会直接断掉连接,但是 Shutdown()
会等到当前连接已经处理完,才停止服务。这期间,会拒绝进的连接进入。
5.2. 信号处理
可以配合信号完成一个多服务的停止实现, go 中的信号处理直接就用的通道了:
package main import ( "net/http" "fmt" "strconv" "os" "os/signal" "syscall" "context" "time" ) func HelloHandler(response http.ResponseWriter, request *http.Request) { time.Sleep(10 * time.Second) response.Write([]byte("Hello" + strconv.FormatInt(time.Now().Unix(), 10))) } func main () { http.HandleFunc("/", HelloHandler) var port_list = [...]int{8890, 8891, 8892, 8893} var srv_list = [len(port_list)]*http.Server{} for i, port := range port_list { var srv = &http.Server{ Addr: ":" + strconv.Itoa(port), } srv_list[i] = srv go func(){ srv.ListenAndServe() }() } //http.ListenAndServe(":8888", nil) var s_chan = make(chan os.Signal, 1) signal.Notify(s_chan, syscall.SIGINT) var root = context.Background() for { var s = <-s_chan switch s { case syscall.SIGINT: fmt.Println("exit...") for _, srv := range srv_list { srv.Shutdown(root) } os.Exit(0) } } }
运行这个服务,先访问 8890 ,然后按 Ctrl-C ,再访问 8891 。
可以看到, 8890 没有中断,但是 8891 已经拒绝连接了。要等到 8890 的请求正常响应之后,整个服务才会退出。
5.3. Multiplexer 和 Handler
Multiplexer
是一组路径映射,关联路径和对应的 Handler
。
Handler
是一套接口,不是一个函数。前面使用 HandleFunc()
是一个简便方法。
Multiplexer
的实例配置好之后,就可以把实例放到 Server 中启动。
package main import ( "net/http" "fmt" "strconv" "time" ) type handler struct { text string } func (h *handler) ServeHTTP(response http.ResponseWriter, request *http.Request){ response.Write([]byte(h.text + strconv.FormatInt(time.Now().Unix(), 10))) } func main () { var mux = http.NewServeMux() mux.Handle("/", &handler{"HAHAHA"}) var srv = &http.Server{ Addr: ":8888", Handler: mux, } fmt.Println("8888...") srv.ListenAndServe() }
Server 中的 Handler
参数,就是需要一个 Multiplexer
实例。
而另一方法,Handler
接口,只需要一个 ServeHTTP()
方法,所以 HandleFunc()
更直接方便。
5.4. Middleware
不考虑错误处理,中间件可以分为“前置”和“后置”。不管是接收 Handler
实例返回新实例,还是接收函数,返回新函数,都是比较好处理的。
不过 go 中没有 Class ,也没有继承( struct
那套是什么鬼),所以函数式的方式我觉得更自然一些吧。
package main import ( "net/http" "fmt" "strconv" "time" ) func hello(response http.ResponseWriter, request *http.Request) { response.Write([]byte("Hello" + strconv.FormatInt(time.Now().Unix(), 10))) } func pre_log(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { return func(response http.ResponseWriter, request *http.Request){ fmt.Println("PRE_LOG") handler(response, request) } } func main () { var mux = http.NewServeMux() mux.HandleFunc("/", pre_log(hello)) var srv = &http.Server{ Addr: ":8888", Handler: mux, } fmt.Println("8888...") srv.ListenAndServe() }
高阶函数,跟 Python 的“装饰器”机制一样,不过 go 中就没有方便的语法糖可用了。
这样只是“理论上可行”,实际项目中,估计还是从 mux.HandleFunc
这一层动手,封装出的形式会更好看一些。
6. 回调触发的时机
Server
的结构,我们只用了 Addr
和 Handler
,但是它里面还有一些比较细节的参数配置,比如读写的时间,头的最大长度等等。
那么 Header 和 Body 既然是分开的,对于 HandleFunc
注册的函数,它是在 Header 完就调用了呢,还是要等 Body 完才调用?
package main import ( "net/http" "fmt" "strconv" "time" ) func hello(response http.ResponseWriter, request *http.Request) { fmt.Println("here") response.Write([]byte("Hello" + strconv.FormatInt(time.Now().Unix(), 10))) fmt.Println("finish") } func main () { var mux = http.NewServeMux() mux.HandleFunc("/", hello) var srv = &http.Server{ Addr: ":8888", Handler: mux, } fmt.Println("8888...") srv.ListenAndServe() }
对于上面的代码,通过 telnet 进行测试:
[email protected]:/home/zys >>> telnet localhost 8888 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. POST / HTTP/1.1 Host: localhost Content-Length: 3 123 HTTP/1.1 200 OK Date: Mon, 29 Nov 2021 18:43:08 GMT Content-Length: 15 Content-Type: text/plain; charset=utf-8 Hello1638211377
可以看出,头接收完之后,函数就已经执行完了,不会等 Body 部分。
如果把 hello
改一下:
func hello(response http.ResponseWriter, request *http.Request) { fmt.Println("here") request.ParseForm() var name string = request.Form.Get("name") response.Write([]byte(name + "Hello" + strconv.FormatInt(time.Now().Unix(), 10))) fmt.Println("finish") }
在请求的 Content-Type
头是可以处理的情况之下,比如是 application/x-www-form-urlencoded
,那么 request.ParseForm()
这行会阻塞,直到 Body 接收完毕,再继承执行。
如果要直接读 request.Body
,也会阻塞:
func hello(response http.ResponseWriter, request *http.Request) { fmt.Println("here") var body []byte body, err := ioutil.ReadAll(request.Body) if err != nil { fmt.Println("Error", err) return } response.Write([]byte(string(body) + "Hello" + strconv.FormatInt(time.Now().Unix(), 10))) fmt.Println("finish") }
7. 并发处理
一个问题,在不启 goroutine 的情况下, net/http
自己的 HandleFunc
有并发处理能力吗?
package main import ( "net/http" "fmt" "strconv" "time" ) func hello(response http.ResponseWriter, request *http.Request) { fmt.Println("start") time.Sleep(10 * time.Second) response.Write([]byte("Hello" + strconv.FormatInt(time.Now().Unix(), 10))) fmt.Println("finish") } func main () { var mux = http.NewServeMux() mux.HandleFunc("/", hello) var srv = &http.Server{ Addr: ":8888", Handler: mux, } fmt.Println("8888...") srv.ListenAndServe() }
上面的服务启动之后,直接使用 curl 来验证,不要使用浏览器,因为浏览器本身对同一个地址有请求限制。
结果是, HandleFunc
本身是有并发处理能力的。即,在第一个请求没有返回之前(看到了 start
没有看到 finish
),可以同时接收并处理后续请求(能看到更多的 start
)。
好消息是, net/http
这一套直接拿到项目中使用,已经差不多了。坏消息是,如果自己要重新实现一套异步的 http 服务端实现, HandleFunc
这套东西都要自己重新做。
8. Chunk 传输
Chunk 传输 指响应头中没有 Content-Length ,即对返回内容长度不定的一种处理方式,有专门的头:
Transfer-Encoding: chunked
从 TCP 的角度,就是连接不断,服务端不断给连接写入内容,在 API 设计上,一般需要显式的 finish ,否则这个响应返回,从逻辑上来说就认为是一直没有结束的。所以,如果一个 Handler 的 API 被设计成 return Response
的格式,就没办法直观地处理 chunk 这种情况。
net/http
的 API ,本来就是 Write()
,其实可以很容易处理 chunk 的情况,但在默认情况下,它的行为仍然还是 Handler 返回( return
)之后,才会在响应中加上头,并把多次 Write()
的内容一并返回。
要实现 chunk 传输,需要使用 http.Flusher
这个接口的 Flush()
方法:
package main import ( "net/http" "fmt" "strconv" "time" ) func hello(response http.ResponseWriter, request *http.Request) { fmt.Println("start") flusher, ok := response.(http.Flusher) if !ok { panic("ERROR") } response.Write([]byte("\nHello " + strconv.FormatInt(time.Now().Unix(), 10))) flusher.Flush() time.Sleep(2 * time.Second) response.Write([]byte("\nHello " + strconv.FormatInt(time.Now().Unix(), 10))) flusher.Flush() time.Sleep(2 * time.Second) response.Write([]byte("\nHello " + strconv.FormatInt(time.Now().Unix(), 10))) flusher.Flush() time.Sleep(2 * time.Second) response.Write([]byte("\nHello " + strconv.FormatInt(time.Now().Unix(), 10))) fmt.Println("finish") } func main () { var mux = http.NewServeMux() mux.HandleFunc("/", hello) var srv = &http.Server{ Addr: ":8888", Handler: mux, } fmt.Println("8888...") srv.ListenAndServe() }
- 仍然没有显式的 finish ,还是以函数结束为响应结束。现实当中使用的话,大概是需要自己使用 goroutine 做一些阻塞性操作。
response
本来是http.ResponseWriter
,但是可以显式的转成http.Flusher
。并且转换之后才能使用Flusher()
。- 通过 telnet 可以清楚看到 chunk 传输的过程,浏览器不一定看得到。
http.Flusher
的转换机制,详细来说:
response
这个结构,本身已经实现了多个接口(有多套方法)。- 我们自己的
hello
这个函数,它的参数签名指定了http.ResponseWriter
这个接口,所以response
结构,就被作为http.ResponseWriter
接口使用了。 - 对于接口,在运行时,可以强制转换成另外的接口,但是不一定能成功转换,所以需要判断转换返回的第二个参数,
ok
,如果成功是true
,失败是false
。因为response
结构已经实现了http.Flusher
接口,所以我们可以强制转换之后使用Flush()
方法。
关于转换的理解,可以看下面简单的小例子。
package main type Foo interface { A() } type Bar interface { B() } type Other interface { C() } type Obj struct {} func (self Obj) A() { println("A") } func (self Obj) B() { println("B") } func test(a Foo) { a.A() //a.B() b, ok := a.(Bar) println(ok) b.B() _, ok2 := a.(Other) println(ok2) } func main() { var obj = &Obj{} obj.A() obj.B() test(obj) }
这里还有一个小点,对于 func test(a Foo)
中的参数,以接口声明约束时,a
是直接用结构,还是用指针,都可以。
即, var obj Obj = Obj{}
和 var obj *obj = &Obj{}
,都可以。
9. JSON 处理
静态语言处理 json 是比较麻烦的, decode 时还可以通过方法跳过具体的对象构造(比如 simpleJson , https://pkg.go.dev/github.com/bitly/go-simplejson), encode 就没好办法了。加上 go 的规则中,公开的方法或者属性还必须首字母大写,对于 encode 的 json 字符串,如果对于成员名有小写的要求,就还要在结构中额外配置 tag 。
9.1. encode
encoding/json
中的 Marshal()
方法,可以直接把结构体,或者成员是结构体的列表,转成 json 字符串。
整数,字符串,布尔值,空值,列表,对象:
package main import ( "fmt" "encoding/json" ) func main() { var s string var js []byte var err error js, err = json.Marshal("1") if err != nil { panic(err) } s = string(js) fmt.Println("|" + s + "|") js, err = json.Marshal(0) if err != nil { panic(err) } s = string(js) fmt.Println("|" + s + "|") js, err = json.Marshal(true) if err != nil { panic(err) } s = string(js) fmt.Println("|" + s + "|") js, err = json.Marshal(nil) if err != nil { panic(err) } s = string(js) fmt.Println("|" + s + "|") js, err = json.Marshal([]string{"1", "2"}) if err != nil { panic(err) } s = string(js) fmt.Println("|" + s + "|") js, err = json.Marshal([]int{1, 2}) if err != nil { panic(err) } s = string(js) fmt.Println("|" + s + "|") js, err = json.Marshal(map[string]string{"a": "123", "b": "453"}) if err != nil { panic(err) } s = string(js) fmt.Println("|" + s + "|") js, err = json.Marshal(map[string]interface{}{"a": "123", "b": "453", "c": 123}) if err != nil { panic(err) } s = string(js) fmt.Println("|" + s + "|") js, err = json.Marshal([]interface{}{1, "2", true, nil, []string{"2", "3"}}) if err != nil { panic(err) } s = string(js) fmt.Println("|" + s + "|") }
基本数据类型直接拼还是很容易的,也可以直接嵌套。
结构体。
package main import ( "fmt" "encoding/json" ) type Hello struct { Name string `json:"name"` Value int World World `json:"www"` } type World struct { Name string } func main() { var s string var js []byte var err error var hello = &Hello{ Name: "first", Value: 123, World: World{ Name: "second", }, } js, err = json.Marshal(hello) if err != nil { panic(err) } s = string(js) fmt.Println("|" + s + "|") }
- 结构体可以直接嵌套。
- 属性默认必须是大写字母开头。
- 如果要小写字母作属性值,需要使用 tag ,格式是
`json:"xxx"`
,中间不能有多余的空格,也不少了双引号。
9.2. decode
json 中的类型,非嵌套的,都是基本类型。嵌套的,都按 interface{}
处理。取值时,再把 interface{}
转成具体类型的。
package main import ( "fmt" "encoding/json" ) func main() { var js []byte var result interface{} var err error js = []byte(`1`) json.Unmarshal(js, &result) var i float64 i = result.(float64) fmt.Println(i) js = []byte(`"1"`) json.Unmarshal(js, &result) var s string s = result.(string) fmt.Println(s) js = []byte(`null`) json.Unmarshal(js, &result) var f interface{} f = result fmt.Println(f) js = []byte(`true`) json.Unmarshal(js, &result) var b bool b = result.(bool) fmt.Println(b) js = []byte(`[true, 123, "sss", null, [1, "2"]]`) err = json.Unmarshal(js, &result) fmt.Println(err) var any []interface{} any = result.([]interface{}) for i, obj := range any { fmt.Println(i, obj) } js = []byte(`{"a": "123", "b": 345, "c": [{"x": 123}]}`) err = json.Unmarshal(js, &result) fmt.Println(err) var any2 map[string]interface{} any2 = result.(map[string]interface{}) for i, obj := range any2 { fmt.Println(i, obj) } var c []interface{} = any2["c"].([]interface{}) fmt.Println(c[0]) var x map[string]interface{} = c[0].(map[string]interface{}) fmt.Println(x["x"]) var last float64 = x["x"].(float64) fmt.Println(last) }
nil
就是interface{}
- json 中的整数,在 go 中按
float64
处理的。(记得 js 中本来就不区分整数和浮点)
上面例子中的 result
,可以直接是一个结构体:
package main import ( "fmt" "encoding/json" ) type Hello struct { Name string `json:"abc"` Value int Mark bool `json:mark` } func main() { var js = []byte(`{"abc": "xxx", "value": 123, "mark": true}`) var result Hello err := json.Unmarshal(js, &result) fmt.Println(err) fmt.Println(result.Name) fmt.Println(result.Value) fmt.Println(result.Mark) }
- 对于指定了结构体的 decode ,首字大写可以自动适配。
- 写了 tag 的, tag 优先级最高。没有对应 tag 的属性,会再尝试
Name
,name
。
10. 模板
go 的官方模块中自带了模板的实现,是 text/template
及 html/template
, 这两个都有相同的接口, html
多了安全方面的处理。
10.1. 基本使用
package main import ( "fmt" "text/template" ) type Hello struct { Name string Result string } func (self *Hello) Write(p []byte) (int, error) { self.Result += string(p) return len(p), nil } func main() { var hello *Hello = &Hello{Name: "hello"} tpl, err := template.New("test").Parse("{{.Name}} xx here") if err != nil { panic(err) } err = tpl.Execute(hello, hello) if err != nil { panic(err) } fmt.Println(tpl.Name()) fmt.Println(hello.Result) }
template
通过New
创建,再调用Parse
返回同一个对象实例。Execute()
需要两个参数,第一个是实现了Write()
接口的对象,第二个就是模板参数的“根对象”。Execute()
并不会返回结果,因为Execute()
的过程会多次调用Write()
,结果放在哪里由接口实现决定。
上面最后一点,用起来虽然麻烦一些,但是却是一种相对很通用的设计。比如,如果你要在 Web 应用中引入这套模板,那么 Write()
就可以实现成直接往连接写入响应内容。
10.2. 当前对象
当前对象,是指 {{ . }}
。
package main import ( "text/template" "os" ) func main() { var hello = map[string]string{"foo": "hello"} tpl, err := template.New("test").Parse("{{.foo}} xx here") if err != nil { panic(err) } err = tpl.Execute(os.Stdout, hello) if err != nil { panic(err) } }
最普通的,就是 Execute()
传入的参数。
在 range
迭代中,也是迭代的当前对象:
func main() { var hello = [...]int{1,2,3} tpl, err := template.New("test").Parse(` {{ range . }} {{ . }} here {{ end }} `) if err != nil { panic(err) } err = tpl.Execute(os.Stdout, hello) if err != nil { panic(err) } }
上面会输出:
1 here 2 here 3 here
{{ range . }}
这里的.
,就是{1,2,3}
列表。{{ . }} here
就是迭代时的列表成员了。
10.3. 前后空行
这里一个,有时会比较实用的功能。
上面的例子:
func main() { var hello = [...]int{1,2,3} tpl, err := template.New("test").Parse(` {{ range . }} {{ . }} here {{ end }} `) if err != nil { panic(err) } err = tpl.Execute(os.Stdout, hello) if err != nil { panic(err) } }
输出是:
1 here 2 here 3 here
可以看到,前后和中间,都多出空行。因为在原模板中: {{ range . }}
和 {{ end }}
都是独占一行的。同时, {{ . }}
的前面也有多个空格及一个换行。
把模板先写成一行比较容易了解换行的位置:
tpl, err := template.New("test").Parse(`{{ range . }}{{ . }} here{{ end }}`)
我们想不要多余的空行与空格的话,容易想到:
tpl, err := template.New("test").Parse(` {{- range . }} {{- . }} here {{ end }}`)
{{- ... }}
可以换左侧的换行和空格去掉,同理,{{ ... -}}}
可以把右侧的换行和空格去掉。
上面那样写,我们最后得到的结果是:
1 here 2 here 3 here
2 here
前面的空格,是 {{ end }}
左侧到 here
为止的内容,这里就没有办法在保留换行的情况下,使用 {{- ...}}
去掉了。如果写成:
tpl, err := template.New("test").Parse(` {{- range . }} {{- . }} here {{- end }}`)
结果会变成:
1 here2 here3 here
所以,要换行,不要空格,就只能手动调整模板:
tpl, err := template.New("test").Parse(` {{- range . }} {{- . }} here {{ end }}`)
10.4. 命令块
以前用过的模板,都会有像“引用”,“继承”之类的机制,可以在模板内部去做 import ,或者“重写块”。 go 的这个模板没有这些, Parse()
的时候,就是一个整体的字符串,但是里面,可以有多个片段,单独给片段命名之后,就可以重复引用了,有点像函数:
{{ define "first" }} {{ . }} {{ end }} {{ template "first" 123 }} {{ template "first" 456 }} {{ template "first" 492 }}
这个模板就是先自己定义了一个 first
片段,然后通过 template
命令做了三次引用,引用时传递了数字作为参数。
如果是从文件 Parse
,也可以一次性写多个文件:
tpl, err := template.ParseFiles("demo.go","a.html", "b.html")
这里,对于定义就可以整合到一起处理。
注意, ParseFiles
是运行时读取文件并解析的,不是编译时。
10.5. 函数
{{ call }}
命令可以调用指定的传入的函数:
tpl, err := template.New("test").Parse(`{{ call . 1 2 }}`)
虽然可以传入函数,但是模板中的使用限制还是非常大。不能嵌套调用,也不能对结果赋值。
10.6. 迭代
{{ range }}
和 {{ end }}
可以完成迭代。
func main() { type item map[string]string var array = [...]item{item{"a": "123"}, item{"a": "345"}, item{"a": "9993"}} tpl, err := template.New("test").Parse(` {{ range . }} {{ .a }} {{ end }} `) if err != nil { panic(err) } err = tpl.Execute(os.Stdout, array) if err != nil { panic(err) } }
10.7. 条件判断
{{ if }}
{{ else }}
{{ end }}
是一套,它们可以嵌套。
但是,这里没有逻辑判断的能力,只能使用一些内置的函数:
func main() { type item map[string]int var array = [...]item{item{"a": 123}, item{"a": 345}, item{"a": 9993}} tpl, err := template.New("test").Parse(` {{ range . }} {{if eq .a 123 }} ok {{ else }} error {{ end }} {{ end }} `) if err != nil { panic(err) } err = tpl.Execute(os.Stdout, array) if err != nil { panic(err) } }
if
那里写成 {{ if .a == 123 }}
是不支持。
or
and
not
这三个逻辑关系,也是函数行为,前置,不能中置:
{{ range . }} {{if or (eq .a 123) (eq .a 345) }} ok {{ else }} error {{ end }} {{ end }}
好在这里至少可以用括号了。
11. 项目的代码结构
TODO