Go 的 HTTP 服务端实现

邹业盛 2022-03-20 15:50 更新
  1. 基本结构
  2. 获取参数
  3. 写头与非 200 响应
  4. 读写 Cookie
  5. 多应用和路径映射
  6. 回调触发的时机
  7. 并发处理
  8. Chunk 传输
  9. JSON 处理
  10. 模板
  11. 项目的代码结构

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 服务框架了:

作为静态语言,几行代码就完成了一个 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)
}

通过 URLQuery() 方法返回的对象获取指定的参数。因为同名参数可以是多个(实践中几乎不会使用重复名字的参数),所以 name 是一个字符串列表,可能是一个空列表。所以在获取具体值之前要进行非空判断,否则会发生运行时错误。

request.URL 是一个单独的 url.URL 对象,结构上类似标准的 URI

URI 的各部分,都可以直接取到。但是注意,这里没有 HostnamePort ,这两个要通过方法获取。

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)
}

如果请求的 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)
}

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)
}

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 下的几个直接的方法,像 ListenAndServeHandleFunc 明显是一些快捷方法。

完整点的结构的话:

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() 会产生一个默认的 DefaultServeMuxServer() 中没有指定 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 的结构,我们只用了 AddrHandler ,但是它里面还有一些比较细节的参数配置,比如读写的时间,头的最大长度等等。

那么 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 进行测试:

zys@shopee:/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()
}

http.Flusher 的转换机制,详细来说:

关于转换的理解,可以看下面简单的小例子。

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 + "|")
}

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)
}

上面例子中的 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)
}

10. 模板

go 的官方模块中自带了模板的实现,是 text/templatehtml/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)
}

上面最后一点,用起来虽然麻烦一些,但是却是一种相对很通用的设计。比如,如果你要在 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

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

评论
©2010-2022 zouyesheng.com All rights reserved. Powered by GitHub , txt2tags , MathJax