go-template 全面讲解

1,907 阅读3分钟

​ 你可以把它理解为Java的jsp,理解为js的ejs,vue的template,等等就是一个模版,所以就是需要了解一些模版的语法,以及渲染机制等,本文基本覆盖的很全面,模版其实核心是理解:1、基本语法,2、自定义func使用和内置func使用,3、变量的使用,4、模版复用等机制。

如果你掌握以上我说的,那么开发一个模版工具,是很轻松的,比如orm通用代码,其他工具等。

以下就是个demo:

func TestTemplateString(t *testing.T) {
	tmpl := "my name is {{.}}"
	parse, _ := template.New("demo").Parse(tmpl)
	_ = parse.Execute(os.Stdout, "anthony")
}
// my name is anthony

所以需要模版渲染的部分都需要加入{{}} , 有些部分操作需要加入{{end}} 作为标识符等。

简单语法学习

{{.}}

可以展示任何数据,各种类型的数据,可以理解为接收类型是interface{}

func TestTemplateString(t *testing.T) {
	tmpl := "my name is {{.}}"
	parse, _ := template.New("demo").Parse(tmpl)
	_ = parse.Execute(os.Stdout, []string{"hao", "dong"})
}

=== RUN   TestTemplateString
my name is [hao dong]--- PASS: TestTemplateString (0.00s)
PASS

{{ .Field }}

func TestTemplateStruct(t *testing.T) {
	tmpl := `my name is "{{.Name}}"`
	parse, _ := template.New("demo").Parse(tmpl)
	_ = parse.Execute(os.Stdout, struct{ Name string }{Name: "anthony"})
}

{{/* a comment */}}

​ 给go文件加入注释

func TestTemplateCommon(t *testing.T) {
	tmpl := `{{/* a comment */}}`
	parse, _ := template.New("demo").Parse(tmpl)
	_ = parse.Execute(os.Stdout, struct{ Name string }{Name: "anthony"})
}
// 什么也不输出

{{- content -}}

去除前后空格,超级方便好用

func TestTemplateTrim(t *testing.T) {
	tmpl := `
		     {{- . -}}             `
	parse, _ := template.New("demo").Parse(tmpl)
	_ = parse.Execute(os.Stdout, "hello")

}
//=== RUN   TestTemplateTrim
//hello--- PASS: TestTemplateTrim (0.00s)

如果不去会是这样子

func TestTemplateTrim(t *testing.T) {
	tmpl := `
		     {{ . }}             `
	parse, _ := template.New("demo").Parse(tmpl)
	_ = parse.Execute(os.Stdout, "hello")
}
//=== RUN   TestTemplateTrim
//
//		     hello             --- PASS: TestTemplateTrim (0.00s)
//PASS

条件语句

{{if condition}} do {{else}} doelse {{end}}

这个语意是如果true,则do,否则do-else,必须最后申明 {{end}} , 跟我的if else 一模一样的

doelse的条件为 : false、0、任意 nil 指针、接口值、数组、切片、字典和空字符串 ""(长度为 0 的字符串)。

func TestTemplateIf(t *testing.T) {
	tmpl := `{{if .flag -}}
			 The flag=true
             {{- else -}}
			 The flag=false
			 {{- end}}
`
	parse, _ := template.New("demo").Parse(tmpl)
	_ = parse.Execute(os.Stdout, _map{
		"flag": true,
	})
	_ = parse.Execute(os.Stdout, _map{
		"flag": false,
	})
}

=== RUN   TestTemplateIf
The flag=true
The flag=false
--- PASS: TestTemplateIf (0.00s)
PASS

循环语句

{{ range content }} T1 {{ end }}

​ 语意很简单,就是遍历内容

func TestRange(t *testing.T) {
	tmpl := `{{range .Array}}
             {{- . -}},
             {{- end}}
`
	parse, _ := template.New("demo").Parse(tmpl)
	_ = parse.Execute(os.Stdout, struct {
		Array []string
	}{Array: []string{"a", "b", "c"}})
}
//=== RUN   TestRange
//a,b,c,
//--- PASS: TestRange (0.00s)

{{ range content }} T1 {{ else }} T0 {{ end }}

如果数组为空,输出else的东西

func TestRangeEmpty(t *testing.T) {
	tmpl := `{{range .Array}}
             {{- . -}},
             {{else -}}
              array null
             {{- end}}
`
	parse, _ := template.New("demo").Parse(tmpl)
	_ = parse.Execute(os.Stdout, struct {
		Array []string
	}{Array: []string{}})
}
//=== RUN   TestRangeEmpty
//array null
//--- PASS: TestRangeEmpty (0.00s)

{{ range $index, $value := pipeline }} T1 {{ end }}

{{ range $key, $value := pipeline }} T1 {{ end }} 也适用,用于遍历

​ 关于 $key,这个属于变量操作,后面会讲到

func TestRange(t *testing.T) {
	tmpl := `{{range $key, $value:=.Array}}
             {{- $key}}:{{$value}},
             {{- end}}
`
	parse, _ := template.New("demo").Parse(tmpl)
	_ = parse.Execute(os.Stdout, struct {
		Array map[string]string
	}{Array: map[string]string{"a": "1", "b": "2", "c": "3"}})
}
//=== RUN   TestRange
//a:1,b:2,c:3,
//--- PASS: TestRange (0.00s)
{{ range pipeline }} T1 {{ end }}

// 这个 else 比较有意思,如果 pipeline 的长度为 0 则输出 else 中的内容
{{ range pipeline }} T1 {{ else }} T0 {{ end }}

// 获取容器的下标
{{ range $index, $value := pipeline }} T1 {{ end }}

FuncMap(很重要)

1、自定义函数

type FuncMap map[string]interface{}

​ 它有一个比较好的功能就是,自定义函数

func TestFuncMap(t *testing.T) {
	tem, _ := template.New("").Funcs(map[string]interface{}{
		"ReplaceAll": func(src string, old, new string) string {
			return strings.ReplaceAll(src, old, new)
		},
	}).Parse(`func replace: {{ReplaceAll .content "a" "A"}}`)
	tem.Execute(os.Stdout, map[string]interface{}{
		"content": "aBC",
	})
}

=== RUN   TestFuncMap
func replace: ABC--- PASS: TestFuncMap (0.00s)
PASS

其中,不能写{{ReplaceAll .content a A}} ,因为go不识别 a 是一个字符串,所以必须加引号

如果理解了这个,其实对于一些内置函数会理解很多,其中对于函数的要求是:

// 写法错误:不允许有两个参数返回值,如果是两个返回值,第二个必须是error
"ReplaceAll": func(src string, old, new string) (string, string) {
  return strings.ReplaceAll(src, old, new), "111"
},

// 写法正确: 如果两个返回类型,第二个必须是error,顺序不能颠倒
"ReplaceAll": func(src string, old, new string) (string, error) {
  return strings.ReplaceAll(src, old, new), nil
},

// 写法正确: 如果是一个返回类型,直接返回就行了
"ReplaceAll": func(src string, old, new string) string {
  return strings.ReplaceAll(src, old, new)
},

// 写法正确:参数可以不传递,但是必须有返回值的(其实可以理解,没有返回值,你渲染啥)
tem, _ := template.New("").Funcs(map[string]interface{}{
  "Echo": func() string {
    return "hello world"
  },
}).Parse(`func echo : {{Echo}}`)

// 写法错误:不允许没有返回参数,直接panic
"ReplaceAll": func(src string, old, new string)  {
   strings.ReplaceAll(src, old, new)
},

​ 其实理解了funcmap,你就理解了内置函数如何玩的,接下来就会说的,不用死记硬背。

2、内置函数

内置函数本质上也是 FuncMap,所以如果掌握了如何使用FuncMap,其实就会这个了。

eq

func TestEQ(t *testing.T) {
	parse, _ := template.New("").Parse(`{{eq .content1 .content2}}`)
	parse.Execute(os.Stdout, _map{
		"content1": "a",
		"content2": "b",
	})
}

=== RUN   TestEQ
false--- PASS: TestEQ (0.00s)
PASS

call

func TestCall(t *testing.T) {
	parse, _ := template.New("").Parse(`{{call .fun .param}}
`)
	parse.Execute(os.Stdout, _map{
		"fun":   func(str string) string { return strings.ToUpper(str) },
		"param": "abc",
	})
}

=== RUN   TestCall
ABC
--- PASS: TestCall (0.00s)
PASS

其实就是这么简单,对于call函数来说,它必须要求返回参数格式是1或2个,其中如果是两个则必须一个是error

func call(fn reflect.Value, args ...reflect.Value) (reflect.Value, error)

func safeCall(fun reflect.Value, args []reflect.Value) (val reflect.Value, err error) {
	defer func() {
		if r := recover(); r != nil {
			if e, ok := r.(error); ok {
				err = e
			} else {
				err = fmt.Errorf("%v", r)
			}
		}
	}()
	ret := fun.Call(args)
  // 结果如果两个,则必须有一个是error
	if len(ret) == 2 && !ret[1].IsNil() {
		return ret[0], ret[1].Interface().(error)
	}
	return ret[0], nil
}

变量

​ 变量通常适用于定义了某个值,申明很简单,和写php一样,变量前面需要加入一个 $ ,注意的是需要渲染的部分全部需要用{{}} 包起来

func TestVariable(t *testing.T) {
	tmpl, err := template.New("test").Parse(`{{$a := "anthony"}} hello {{$a}}`)
	if err != nil {
		panic(err)
	}
	err = tmpl.Execute(os.Stdout, nil)
	if err != nil {
		panic(err)
	}
}

其实跟我的go预发基本一致。

模版嵌套

​ 自定义内嵌的模板{{define "template_name"}} template_content {{end}},其中就是一种模版复用的机制。 那么如何模版引用呢 {{template "template_name" "args"}} ,记得传入两个参数就行,一个模版的名称(记住申明模版名称需要加上""),一个是你的模版需要的参数。

func TestTemplateInternal(t *testing.T) {
	parse, _ := template.New("").Parse(`
		{{define "print"}}my name is {{.}} {{end}}
        {{- template "print" .name}}
	`)
	parse.Execute(os.Stdout, _map{
		"name": "hao dong",
	})
}

=== RUN   TestTemplateInternal
		my name is hao dong 
	--- PASS: TestTemplateInternal (0.00s)
PASS

format 工具

​ 我们知道,go是有个fmt插件,可以帮助format 文件,所以这里go 也提供了format

func TestFormat(t *testing.T) {
	parse, _ := template.New("").Parse(`
		package main
	    import  "fmt"
	
	
	    func main(){
			fmt.Println("{{.}}")
		}
	
	`)
	parse.Execute(os.Stdout, _map{"data":"1111"})
}

// 结果:是不是很乱,但是别慌
=== RUN   TestFormat

		package main
	    import  "fmt"
	
	
	    func main(){
			fmt.Println("map[data:1111]")
		}
	
	--- PASS: TestFormat (0.00s)
PASS

如何使用format呢 ?

func TestFormat(t *testing.T) {
	parse, _ := template.New("").Parse(`
		package main
	    import  "fmt"
	
	
	    func main(){
			fmt.Println("{{.}}")
		}
	
	`)

	buffer := &bytes.Buffer{}
	parse.Execute(buffer, _map{"data":"1111"})
	source, _ := format.Source(buffer.Bytes())
	fmt.Printf("%s",source)
}

=== RUN   TestFormat
package main

import "fmt"

func main() {
	fmt.Println("map[data:1111]")
}
--- PASS: TestFormat (0.00s)
PASS

是不是变得很整齐。

总结

go 原生的提供了模版机制,对于开发者相当友好,尽管第三方包有很多模版工具,但是原生是最好的,因为不需要引入依赖。

模版通常用于生成一些复用的代码,比如 protobuf文件生成, 比如orm框架的model,dao等,都是需要改进的

参考

juejin.cn/post/684490…

juejin.cn/post/684490…