Go 模板编程

179 阅读6分钟

一、Go 模板

模板是我们常用的手段用于动态生成页面,或者用于代码生成器的编写等。比如把数据库的表映射成go语言的struct,这些体力活,用于编写代码生成器是最合适不过的了。

Go 本身带了两个模板包,http/template 和 text/template。两者功能相似,语法一致,可以认为 http/templatetext/template加强版,主要就是增强了安全性。

  • http/template:一般用于渲染页面
  • text/template:常常用于各种代码生成

image.png

使用模板时,尽量避免使用复杂的模板特性。 即将大多数事情在 Go 语言本身解决,而不要利 用模板语言解决。

1.1 模板基本语法

一般步骤

  • 创建一个模板实例:template.New,传入模板名 字,在多个模板组合使用的时候可以按名索引
  • 解析模板(预编译模板):调用 tpl.Parse 方 法,传入的参数是模板的具体内容
  • 传入数据,渲染模板:调用 Execute 方法,传 入的参数作为模板渲染所使用的数据
func TestHelloWorld(t *testing.T) {
   tpl := template.New("hello-world")
   tpl, err := tpl.Parse(`Hello, {{.Name}}`)
   if err != nil {
      t.Fatal(err)
   }
   bs := &bytes.Buffer{}
   err = tpl.Execute(bs, &User{Name: "Tom"})
   if err != nil {
      t.Fatal(err)
   }
   assert.Equal(t, `Hello, Tom`, bs.String())
}
  • 使用 {{ }} 来包裹模板语法
  • 使用 . 来访问数据,. 代表的是当前作用域的当前对象,类似于 Java 的 this、Python 的 self。所以 .Name 代表的是访问传入的 User 的 Name 字

image.png

  • 要点:要知道 . 究竟代表什么。它可以是普通结构体或者指针,也 可以是 map,也可以是切片或者数组
func TestMapData(t *testing.T) {
   tpl := template.New("hello-world")
   tpl, err := tpl.Parse(`Hello, {{.Name}}`)
   if err != nil {
      t.Fatal(err)
   }
   bs := &bytes.Buffer{}
   err = tpl.Execute(bs, map[string]string{"Name": "Jack"})
   if err != nil {
      t.Fatal(err)
   }
   assert.Equal(t, `Hello, Jack`, bs.String())
}


func TestSliceData(t *testing.T) {
   tpl := template.New("hello-world")
   tpl, err := tpl.Parse(`Hello, {{index . 0}}`)
   if err != nil {
      t.Fatal(err)
   }
   bs := &bytes.Buffer{}
   err = tpl.Execute(bs, []string{"John"})
   if err != nil {
      t.Fatal(err)
   }
   assert.Equal(t, `Hello, John`, bs.String())
}

1.1.1 为接口生成实现

背景: 在当下,很多时候我们需要调用别人的 HTTP 接口。但是普遍来说我们操作HTTP 接口会比较繁琐,因为我们需要直接操作 HTTP 协议, 在这种情况下,可以考虑定义一个 Go 接口,并且为 Go 接口生成 HTTP 调用代码。 这也可以看做是最为轻量级的 RPC 的实现

注:服务的的定义需要利用AST技术从接口定义中抽取出来。

const serviceTpl = `package {{ .Package }}

import (
   "bytes"
   "context"
   "encoding/json"
   "io/ioutil"
   "net/http"
)

{{ $service :=.GenName -}}
type {{ $service }} struct {
    Endpoint string
    Path string
   Client http.Client
}
{{range $idx, $method := .Methods}}
func (s *{{$service}}) {{$method.Name}}(ctx context.Context, req *{{$method.ReqTypeName}}) (*{{$method.RespTypeName}}, error) {
   url := s.Endpoint + s.Path + "{{$method.Path}}"
   bs, err := json.Marshal(req)
   if err != nil {
      return nil, err
   }
   body := &bytes.Buffer{}
   body.Write(bs)
   httpReq, err := http.NewRequestWithContext(ctx, "POST", url, body)
   if err != nil {
      return nil, err
   }
   httpResp, err := s.Client.Do(httpReq)
   if err != nil {
      return nil, err
   }
   bs, err = ioutil.ReadAll(httpResp.Body)
   resp := &{{$method.RespTypeName}}{}
   err = json.Unmarshal(bs, resp)
   return resp, err
}
{{end}}
`

1.1.2 变量声明

const serviceTpl = `
{{ $service :=.GenName -}}
type {{ $service }} struct {
    Endpoint string
    Path string
   Client http.Client
}
`
  • 去除空格和换行: -,注意要和别的元素用空格分开
  • 声明变量:如同 Go 语言,但是用 来表示。来表示。xxx := some_value
  • 执行方法调用:形式“调用者.方法 参数1 参数2” 。 注意,方法调用的形式和 Go 语言本身的调用形式差 异很大。

1.1.3 方法调用

type FuncCall struct {
   Slice []string
}

func (f FuncCall) Hello(firstName string, lastName string) string {
   return fmt.Sprintf("Hello, %s·%s", firstName, lastName)
}

func TestFuncCall(t *testing.T) {
   tpl := template.New("hello-world")
   tpl, err := tpl.Parse(`
切片长度: {{len .Slice}}
say hello: {{.Hello "Tom" "Jerry"}}
打印数字: {{printf "%.2f" 1.234}}
`)
   assert.Nil(t, err)
   bs := &bytes.Buffer{}
   err = tpl.Execute(bs,
      &FuncCall{Slice: []string{"Tom", "Jerry"}})
   assert.Nil(t, err)
   assert.Equal(t, `
切片长度: 2
say hello: Hello, Tom·Jerry
打印数字: 1.23
`, bs.String())
}

可以调用内置的方法,如 print 类和 len 之 类的。简单来说就是你的编译器会特殊标 记的那些方法

1.1.4 循环

  • 使用 range 关键字,形式 range idx,idx, elem := 某个切片
  • 在下面代码中,我们迭代了 .Methods

注意:不支持 for...i... 的循环形式,也不支持 for true 这 种形式

const serviceTpl = `
{{range $idx, $method := .Methods}}
func (s *{{$service}}) {{$method.Name}}(ctx context.Context, req *{{$method.ReqTypeName}}) (*{{$method.RespTypeName}}, error) {
   url := s.Endpoint + s.Path + "{{$method.Path}}"
   bs, err := json.Marshal(req)
   if err != nil {
      return nil, err
   }
   body := &bytes.Buffer{}
   body.Write(bs)
   httpReq, err := http.NewRequestWithContext(ctx, "POST", url, body)
   if err != nil {
      return nil, err
   }
   httpResp, err := s.Client.Do(httpReq)
   if err != nil {
      return nil, err
   }
   bs, err = ioutil.ReadAll(httpResp.Body)
   resp := &{{$method.RespTypeName}}{}
   err = json.Unmarshal(bs, resp)
   return resp, err
}
{{end}}
`

间接 for...i 循环

  • 创建一个足够大的切片
  • 对这个切片进行遍历
  • $idx 就是我们需要的
func TestForILoop(t *testing.T) {
   // 用一点小技巧来实现 for i 循环
   tpl := template.New("hello-world")
   tpl, err := tpl.Parse(`
{{ range $idx,$elem := . -}}
下标:{{$idx -}},
{{- end}}
`)
   assert.Nil(t, err)
   bs := &bytes.Buffer{}
   // 假设我们要从 0 迭代到 100,即 [0, 100)   // 这里的切片可以是任意类型,[]bool, []byte 都可以
   // 因为我们本身并不关心里面元素,只是借用一下下标而已
   data := make([]bool, 100)
   err = tpl.Execute(bs, data)
   assert.Nil(t, err)
   assert.Equal(t, `
下标:0,下标:1,下标:2,下标:3,下标:4,下标:5,下标:6,下标:7,下标:8,下标:9,下标:10,下标:11,下标:12,下标:13,下标:14,下标:15,下标:16,下标:17,下标:18,下标:19,下标:20,下标:21,下标:22,下标:23,下标:24,下标:25,下标:26,下标:27,下标:28,下标:29,下标:30,下标:31,下标:32,下标:33,下标:34,下标:35,下标:36,下标:37,下标:38,下标:39,下标:40,下标:41,下标:42,下标:43,下标:44,下标:45,下标:46,下标:47,下标:48,下标:49,下标:50,下标:51,下标:52,下标:53,下标:54,下标:55,下标:56,下标:57,下标:58,下标:59,下标:60,下标:61,下标:62,下标:63,下标:64,下标:65,下标:66,下标:67,下标:68,下标:69,下标:70,下标:71,下标:72,下标:73,下标:74,下标:75,下标:76,下标:77,下标:78,下标:79,下标:80,下标:81,下标:82,下标:83,下标:84,下标:85,下标:86,下标:87,下标:88,下标:89,下标:90,下标:91,下标:92,下标:93,下标:94,下标:95,下标:96,下标:97,下标:98,下标:99,
`, bs.String())
}

1.1.5 条件判断

  • 一样采用 if-else 或者 if-else if 的结构
  • 可以使用 and: and 条件1 条件2
  • 可以使用 or: or 条件1 条件2
  • 可以使用 not: not 条件1
func TestIfElseBlock(t *testing.T)  {
   // 用一点小技巧来实现 for i 循环
   tpl := template.New("hello-world")
   tpl, err := tpl.Parse(`
{{- if and (gt .Age 0) (le .Age 6) }}
儿童 0<age<6
{{ else if and (gt .Age 6) (le .Age 18) }}
少年 6<age<=18
{{ else }}
成人 > 18
{{ end -}}
`)
   assert.Nil(t, err)
   bs := &bytes.Buffer{}
   err = tpl.Execute(bs, map[string]any{"Age": 5})
   assert.Nil(t, err)
   assert.Equal(t, `
儿童 0<age<6
`, bs.String())
}

1.1.6 比较操作符

  • 形式:操作符 参数1 参数

image.png

1.1.7 Pipeline

  • Pipeline 是一个在模板里面很难理解的概 念,它字面意思是管道,类似于 shell 里 面的 pipeline
  • 可以将 pipeline 看做一系列的命令,这些 命令就可以是访问变量,调用方法等
  • 一个 pipeline 的输出可以作为另外一个 pipeline 的输入,用 | 来连接

image.png

1.1.8 面试要点

模板在日常工作中非常好用,面试的时候则主要聚焦在模板的语法上:

  • 模板的基本语法:变量声明、方法调用、循环、条件判断、操作符,以及一个比较常见 的,就是怎么在模板里面实现 for ... i ... 的循环
  • 什么是前缀表达式(+ b c)?也就是模板里面的那种语法,和中缀表达式(b+c)比起来, 它更加贴近计算机的计算原理。所以模板用了前缀表达式,能够简化模板引擎的设计和实现
  • http/template 和 text/template 有什么区别:前者多了对 HTTP 的支持,加强了安全性,例如 特殊字符转义等。http/template 能够满足绝大多数页面渲染的要求
  • 模板中的 pipeline 是什么?一串命令,pipeline 之间可以通过 | 连在一起组成更加复杂的 pipeline。单个命令可以是声明变量,也可以是方法调用