复合数据类型

一、数组

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。声明方式:var a [3]int

  • 数组的每个元素可以通过索引下标来访问,索引下标的范围是从0开始到数组长度减1的位置
  • 默认情况下,数组的每个元素都被初始化为元素类型对应的零值,对于数字类型来说就是0
  • 在数组字面值中,如果在数组的长度位置出现的是“…”省略号,则表示数组的长度是根据初始化值的个数来计算
  • 数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定,定义后长度不可变。

1、初始化数组

var a [3]int32           // 定义一个数组,不初始化值,则值默认为0

var a2 =[2]int{3,4}

b :=[4]{1,2,3,4}     

c :=[...]int{1,2}     //数组的长度是根据初始化值的个数来计算

d :=[...]string{2:"abc",5:"efg"} //指定一个索引和对应值列表的方式初始化:初始化索引的顺序是无关紧要的,而且没用到的索引可以省略,和前面提到的规则一样,未指定初始值的元素将用零值/“”初始化

var a1 [2]int={3,4} //错误定义,编译时提示语法错误:syntax error

2、数组赋值

q := [3]int{1, 2, 3}
q[1]=5             //指定数组某个元素赋值
fmt.Println("q[1]值=",q[1]) // 返回 q[1]值=5

q =[3]int{9,10} //给整个数组赋值
fmt.Println("q值=",q) // 返回 q值= [9 10 0]

//类型/长度不相等,赋值失败
q = [4]int{1, 2, 3, 4} // cannot use [4]int{…} (value of type [4]int) as [3]int value in assignment
q =[3]int[16]{1,2,3} //cannot use [3]int16{…} (value of type [3]int16) as [3]int32 value in assignment

3、数组比较

q := [3]int{1, 2, 3}
q1 := [3]int{1, 3, 2} 
q2 :=[...]int{1,2,3}
fmt.Println(q == q1) // 返回 false
fmt.Println(q !=q1) // 返回 false
fmt.Println(q1 == q2) // 返回 true

s :=[...]string{"a","b","c"}
s1 :=[...]string{"a","b","c"}
fmt.Println(s == s1) // 返回 true

p :=[2]int8{1,2}
p1 :=[2]int16{1,2}
fmt.Println(p == p1) // 返回 invalid operation: p == p1 (mismatched types [2]int8 and [2]int16)

t :=[2]int{1,2}
t1 :=[3]int{1,2}
fmt.Println(t == t1) // 返回 invalid operation: t == t1 (mismatched types [2]int and [3]int)

4、数组遍历

name1 := [...]string{"zhangsan","lisi","wangwu","刘麻子"}
for k,v :=range name1{
        fmt.Printf("索引= %v \t  value= %v\n",k,v)
    }

/*
索引= 0           value= zhangsan
索引= 1           value= lisi
索引= 2           value= wangwu
索引= 3           value= 刘麻子
*/

5、数组排序

数组,不能直接排序,需转成切片再排序

//升序排列

sort.Ints(qqqqq)
fmt.Println(qqqqq)
//方式一、先将q转为切片再进行排序,不能直接对数组排序
q := [4]int{1, 8, 3,5}
sort.SliceStable(q[:], func(i, j int) bool {return q[i]>q[j]})
fmt.Println(q)


// 方式二
qqqqq:=[...]int{1,3,9,2,6}

var s []int
for _,v:=range qqqqq{
	s = append(s, v)
}
sort.Ints(s)
fmt.Println(s)

二、Slice切片

Slice(切片)简单理解就是:没有固定长度的数组。

//直接创建
c :=[]int{4,5,6}

//基于已有的数组或切片
a :=[5]int{3,4,5,6,7} //a是数组
c1 :=a[1:3] //c1是切片

//使用make来创建一个切片,使用默认值初始化
d :=make([]int,5,7)

注意: 使用make 创建slice时:第一个参数代表类型[]不要写长度,第二个参数为slice的len ,第三个参数为slice的容量需要 >= len

1、获取元素

  • arr[start:end]: 包括start到end-1(包括end-1)之间的所有元素
  • arr[start:]: 包括start到slice最后一个元素(包括最后一个元素)之间的所有元素
  • arr[:end]: 包括0到end-1(包括end-1)之间的所有元素
  • arr[:]: 包括整个数组的所有元素
s :=[]int{1,3,8,6,2,1}
fmt.Println(s[1:4]) //[3 8 6]
fmt.Println(s[1:]) //[3 8 6 2 1]
fmt.Println(s[:4]) //[1 3 8 6]

2、增加元素

  • 使用append(slice []Type, elems …Type) 增加slice 元素,第一个参数是目标slice,第二个参数为需要增加的元素值;
  • 当达到底层的最大容量,切片会进行扩容,扩容的策略是翻倍扩容。
  • 超出len(s)则是意味着扩展了slice,因为新slice的长度会变大
s :=[]int{1,3,8,6,2,1}
fmt.Println(len(s)) //6
fmt.Println(cap(s)) //6
s= append(s, 10)
//增加元素后,达到底层的最大容量,则扩容
fmt.Println(s) //[1 3 8 6 2 1 10]
fmt.Println(len(s)) //7
fmt.Println(cap(s)) //12

说明:当原slice容量(oldcap)小于256的时候,新slice(newcap)容量为原来的2倍;原slice容量超过256,新slice容量newcap = oldcap+(oldcap+3*256)/4。想了解更多内容,可以参考官方仓库

3、切片修改

s :=[]int{1,3,8,6,2,1}
s[1]=5
fmt.Println(s) //[1 5 8 6 2 1]

4、切片追加切片

s :=[]int{1,3,8,6,2,1}
s1 :=[]int{11,18,19}
//这个...定义就是把slice2展开,放到slice1后面
s=append(s,s1...)
fmt.Println(s) //[1 3 8 6 2 1 11 18 19]

三、Map

在Go语言中,一个map就是一个哈希表的引用,是一个无序的key/value对的集合

var m = map[string]int{}
m["xiaoli"] = 18
m["zhangsan"] = 28
fmt.Println(m) //map[xiaoli:18 zhangsan:28]

map初始化
var demoMap = map[string]int
此时还不能直接使用,因为并未正在分配内存,必须使用make或者直接初始化时才能正常使用,如
第一种初始化:var demoMap = map[string]int{}
第二种初始化:var demoMap = make(map[string]int, 2)

1、map基本操作

var m = map[string]int{}
//赋值
m["xiaoli"] = 18 //map[xiaoli:18]
m["zhangsan"] = 28 //map[xiaoli:18 zhangsan:28]
//更新
m["xiaoli"] = 38 //map[xiaoli:38 zhangsan:28]
//删除
delete(m,"xiaoli") //map[zhangsan:28]

// 遍历
for key,value:=range m{
	fmt.Printf("key:%v,value:%v",key,value)
}

//map长度
l:=len(m) //1

2、map切片操作

var sliceMap = make([]map[string] int,2)
sliceMap=append(sliceMap,m)
fmt.Println(sliceMap) //[map[] map[] map[xiaoli:18 zhangsan:28]]

//map合并
for k, v := range otherMap {
	demoMap[k] = v
}
fmt.Println(demoMap)

注意:切片操作必须定义为map类型的切片

3、map排序

Go语言的map是无序的,多次遍历map的结果可能是不同的

result := map[int]uint32{}
keys := []int{}
// 插入各个数据
result[24] = 240
result[17] = 170
result[9] = 90
result[11] = 110
result[55] = 550
	
for key := range result {
	keys = append(keys, key)
}

key升序

// key排序生序,从小到大
sort.Sort(sort.IntSlice(keys))
for _,key :=range keys{
	fmt.Println(key,result[key])
}
fmt.Println(result)

key降序

// key排序降序,从大到小
sort.Sort(sort.Reverse(sort.IntSlice(keys)))
for _,key :=range keys{
	fmt.Println(key,result[key])
}
fmt.Println(result)

value有序
用struct存放key和value,实现sort接口,就可以调用sort.Sort进行排序了

	result := map[string]int32{}
	result["zhangsan"] = 18
	result["lisi"] = 38
	result["wangwu"] = 28


	type person struct {
		Name string
		Age int32
	}

	var lstPerson []person
	for k,v :=range result{
		lstPerson=append(lstPerson,person{k,v})
	}

	// 升序 return lstPerson[i].Age < lstPerson[j].Age
	// 降序 return lstPerson[i].Age > lstPerson[j].Age
	sort.SliceStable(lstPerson, func(i, j int) bool {
		return lstPerson[i].Age < lstPerson[j].Age
	})

	fmt.Println(lstPerson)

四、结构体

结构体是将零个或者多个任意类型的命名变量组合在一起的聚合数据类型。

//声明结构体类型
type Employee struct {
    ID        int
    Name      string
    Address   string
    ManagerID int
}

//普通声明结构体变量
var e Employee =Employee{1,"张三","广州市",1}//使用 结构体字面值表示需要记住成员变量类型和顺序,结构体有变动会编译失败
e1 :=Employee{2,"李四","广州市1",2}
e2 :=Employee{ID:3,Name:"李四"}//以成员名字和相应的值来初始化不用考虑顺序,可以包含部分或全部的成员,如果成员被忽略的话将默认用零值

//指针变量
var e3 *Employee=&Employee{4,"王武","佛山",4}
fmt.Println("e3: ",e3)
e4 := &Employee{5,"liuhua","佛山1",5}

//结构体成员 *tree指针类型的成员
type tree struct {
    value       int
    left, right *tree
}

1、结构体访问与操作

type Employee struct {
	ID      int
	Name    string
	Address string
}

var e3 Employee = Employee{1, "xiaoli", "hahah"}
fmt.Println(e3.Address) //hahah

//赋值成员取地址,然后通过指针访问
name := &e3.Name
*name = "hello " + *name // promoted, for outsourcing to Elbonia
fmt.Println("e3的name对成员取地址,然后通过指针访问:",e3.Name)		//e6的name对成员取地址,然后通过指针访问: hello xiaoli

//点操作符也可以和指向结构体的指针一起使用
var e7 *Employee=&e3
(*e7).ManagerID += 10
fmt.Println("e6的name对成员取地址,然后通过指针访问:",e6.ManagerID)  //e6的name对成员取地址,然后通过指针访问: 30

2、作函数参数

传值 形式

func main() {
 	var e4 =Employee{6,"函数","上海",5}
	PrintValue(e4)
	fmt.Println("main函数中的e4:",e4)
}

func PrintValue(ev Employee)  {
	ev.ManagerID =50
	fmt.Println("值传函数ev: ",ev)

}
/*
值传函数ev:  {6 函数 上海 50}
main函数中的e4: {6 函数 上海 5}
*/

注意:传参过程中,实参会将自己的值拷贝一份给形参。因此结构体“传值”操作几乎不会在实际开发中被使用到。

传引用 形式

func main() {
  var s=Employee{7,"lili","北京",18}
  PrintPointer(&s)
  fmt.Println("main函数中的s:",s)
}
func PrintPointer(ep *Employee) {
	ep.ID=250
	fmt.Println("地址传值ep: ",ep)
}

/*
地址传值ep:  &{250 lili 北京 18}
main函数中的s: {250 lili 北京 18}
*/

注意:近乎100%的使用都采用“传址”的方式,将结构体的引用传递给所需函数。

3、结构体嵌入和匿名成员

type Point struct {
    X, Y int
}

type Circle struct {
    Center Point //定义一个结构体类型的成员变量
    Radius int
}

type Wheel struct {
    Circle Circle //定义一个结构体类型的成员变量
    Spokes int
}


var w Wheel
w.Circle.Center.X = 8 //直接通过Wheel变量来访问Point结构体的X成员变量
w.Circle.Center.Y = 8
w.Circle.Radius = 5
w.Spokes = 20
type Point struct {
    X, Y int
}

type Circle struct {
    Point				//声明结构体类型的匿名成员
    Radius int
}

type Wheel struct {
    Circle
    Spokes int
}

//下面两种实例化
w = Wheel{Circle{Point{8, 8}, 5}, 20}

w = Wheel{
    Circle: Circle{
        Point:  Point{X: 8, Y: 8},
        Radius: 5,
    },
    Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}


//使用
var w Wheel
w.X = 8            // 相当于 w.Circle.Point.X=8
w.Y = 8            // 相当于  w.Circle.Point.Y = 8
w.Radius = 5       // 相当于  w.Circle.Radius = 5
w.Spokes = 20

4、结构体遍历

	in := reflect.ValueOf(Student{"乔峰", 29})
	count := in.NumField()
	fmt.Println("aaaaaaaaaaa")
	for i := 0; i < count; i++ {
		f := in.Field(i) //字段value值
		switch f.Kind() {
		case reflect.Ptr:
			fmt.Println(f.Pointer())
		case reflect.String:
			fmt.Println(f.String())
		case reflect.Int:
			fmt.Println(f.Int())
		}
	}

五、JSON

JSON 是一种轻量级的数据交换格式,易于人阅读和编写,可以在多种语言之间进行数据交换。同时也易于机器解析和生成。Go 标准库中的 encoding/json 包中提供了对 JSON 的支持。

序列化:把对象转化为可传输的字节序列过程称为序列化。
反序列化:把字节序列还原为对象的过程称为反序列化。

1、序列化

import (
	"encoding/json"
	"fmt"
)

type Movie struct {
	Title string
	Year int
	Actors []string
}

func main() {
	m:=Movie{"hello world",2023,[]string{"xiaoli","xiaowang"}}
	fmt.Println(m)
	
	// 结构体转JSON
	// json.Marshal 将结构体序列化为json对象,返回一个编码后的字节slice
	bytes, err := json.Marshal(m)
	if err!=nil {
		fmt.Println("fail",err)
	}else {
		fmt.Println(bytes) //返回的是字节 [123 34 84 105 116 108 101 34 58 34 ...】
		fmt.Println(string(bytes)) //返回的是字节转化成字符串 {"Title":"hello world","Year":2023,"Actors":["xiaoli","xiaowang"]}
	}
	//json.MarshalIndent 格式化输出,格式更整齐
	indent, err := json.MarshalIndent(m, "", " ")
	if err!=nil{
		fmt.Println("fail",err)
	}else {
		fmt.Printf("%s",indent)
	}
	
	// map转JSON
	m2 :=make(map[string]interface{})
	m2["Title"] = "hello"
	m2["Year"] =2023

	marshal, err := json.Marshal(m2)
	if err!=nil{
		fmt.Println("err",err)
	}
	fmt.Println(string(marshal))
}

● JSON object key 只支持 string
● 只有 struct 中支持导出的成员才能被序列化,即首字母大写
● Channel、complex、function 等 type 无法进行序列化
● 数据中如果存在循环引用,则不能进行序列化,会进入无限循环
● Pointer 序列化之后是其指向的值或者是 nil

2、反序列化

JSON数据解码为Go语言的数据结构,Go语言中通过json.Unmarshal函数完成。如果要进行反序列化,我们首先需要创建一个可以接受序列化数据的 Go struct

import (
	"encoding/json"
	"fmt"
)

type Movie struct {
	Title  string
	Year   int
	Actors []string
}

func main() {
	var jsonData = []byte(`{
 	"Title": "hello world",
 	"Year": 2023,
 	"Actors": [
	  "xiaoli",
 	 "xiaowang"
 	]
	}`)

	var m Movie
	err:=json.Unmarshal(jsonData,&m)
	if err!=nil{
		fmt.Println("fail",err)
	}
	fmt.Println(m)

	// 选择性解码,只解码某个字段
	var title struct{Title string}
	err = json.Unmarshal(jsonData, &title)
	if err!=nil{
		fmt.Println("fail",err)
	}
	fmt.Println(title)
}

//空切片编码
type Person struct {
    Address []string
}

func main() {
    var p1 []string //声明,但未初始化,底层没有分配内存空间,与nil比较返回true
    p2 := make([]string, 0) //与nil比较返回false
    p3 := []string{}//与nil比较返回false
    json1, _ := json.Marshal(Person{p1}) //{"Address":null}
    json2, _ := json.Marshal(Person{p2}) //{"Address":[]}
    json3, _ := json.Marshal(Person{p3}) //{"Address":[]}
}

说明:由于json.Unmarshal()方法接收的是字节切片,所以首先需要把JSON字符串转换成字节切片data:=[]byte(s)

3、结构体tag

Struct tag 可以决定 Marshal 和 Unmarshal 函数如何序列化和反序列化数据。
1)指定字段名

type Movie struct {
    Title  string
    Year   int  `json:"released"`
    Color  bool `json:"color,omitempty"`
    Actors []string
}

说明:Go语言用Year表示发布年份,而json中用"released"
2)忽略空值字段

type Person struct {
    Name string `json:"name,omitempty"` // 指定json序列化/反序列化时使用小写name,且字段为空值时忽略该字段
    Age int `json:"age"`
}

3)忽略某个字段

type Person struct {
    Name string `json:"name"`
    Age int `json:"-"` // 指定json序列化/反序列化时忽略此字段
    sex string  //不支持导出
}

4)处理字符串中格式的数字

type Person struct {
    Name string `json:"name,omitempty"` // 指定json序列化/反序列化时使用小写name,且字段为空值时忽略该字段
    Age int `json:"age,string"`
}

4、解析数据流

上面都是使用的UnMarshall解析的JSON数据,如果JSON数据的载体是打开的文件或者HTTP请求体这种数据流(他们都是io.Reader的实现),我们不必把JSON数据读取出来后再去调用encode/json包的UnMarshall方法,包提供的Decode方法可以完成读取数据流并解析JSON数据最后填充变量的操作

json.NewDecoder(resp.Body).Decode(&result)
body, err := io.ReadAll(resp.Body)
json.Unmarshal(body, &result)

5、扩展库

easyjson:

//安装
go get -u github.com/mailru/easyjson/...
//使用
./easyjson -all <结构定义>.go

使用示例:

// 定义结构体employee.go
type BasicInfo struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}
type JobInfo struct {
    Skills []string `json:"skills"`
}
type Employee struct {
    BasicInfo BasicInfo `json:"basic_info"`
    JobInfo   JobInfo   `json:"job_info"`
}

//生成代码,安装easyjson包后在命令行执行:
./easyjson -all employee.go

六、文本和HTML模版

text/template和html/template提供了将变量的值替换到文本的机制。通常用在一些printf应付不了的、需要复杂的格式化的场合。

1、创建模板

模板是一个字符串或一个文件,里面包含了一个或多个由双花括号包含的{{action}}对象。

//定义模板
tepl := "Hi, my name is {{.}}\n" //中间的.表示传入模板的数据,根据传入的数据不同渲染不同的内容

// 创建模版-文本
tmpl, err := template.New("test").Parse(tepl)
// 创建模版-文件
tpl, err := template.ParseFiles(filename)

//Parse方法将传入的字符串解析为模板
tmpl, err := template.New("test").Parse(tepl)

//Execute方法将解析好的模板应用到data上,并将输出写入wr
err = tmpl.Execute(os.Stdout, "Gopher")

说明:{{ 和 }} 包裹的内容统称为 action,分为两种类型:数据求值和控制结构

2、执行模板

// tpl.Execute(io.Writer, data)
//Execute方法将解析好的模板应用到data上,并将输出写入wr
err = tmpl.Execute(os.Stdout, p)
// tpl.ExecuteTemplate(io.Writer, name, data)和上面的简单模板类似,只不过传入了一个模板的名字,指定要渲染的模板(因为tpl可以包含多个模板)
err = tpl.ExecuteTemplate(os.Stdout, fn1, "abc")

3、模板变量

.字符

{{ . }}  //.表示当前对象

如果数据是复杂类型的数据,可以通过{{ .FieldName }}来访问它的字段。
如果字段还是复杂类型,可以链式访问 {{ .Struct.StructTwo.Field }}。

变量
传给模板的数据可以存在模板中的变量中,在整个模板中都能访问。
语法:KaTeX parse error: Expected '}', got 'EOF' at end of input: …而没有产生任何输出。 例如{{name := .Name}},我们使用KaTeX parse error: Expected '}', got 'EOF' at end of input: …,保存传入的数据,然后使用{{name}}来访问变量。

p := Person{Name: "test", Age: 20}
tepl := `{{$name := .Name}} hi, I'm {{$name}}`
tmpl, _ := template.New("test").Parse(tepl)
tmpl.Execute(os.Stdout, p)  //hi, I'm test

4、Actions

模板中双花括标记号叫做Action。模板中所有的除Action的字符串都按照原样打印,Action则可以输出复杂的打印值:打印变量的值、调用函数或方法、表达控制流程if-else语句和range循环语句等。

//注释 
{{/* comment */}}

// 裁剪 content 前后的空格
{{- content -}}
// 裁剪 content 前面的空格
{{- content }}
// 裁剪 content 后面的空格
{{ content -}}

//文本输出
{{ pipeline }}

//条件语句
{{if pipeline}} T1 {{end}}
{{if pipeline}} T1 {{else}} T0 {{end}}
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}

//循环语句 
//pipeline的值必须是数组、切片、字典或者通道, 即可迭代类型的值
{{range pipeline}} T1 {{end}}
//如果pipeline的值的长度为0,不会有任何输出
{{range pipeline}} T1 {{else}} T0 {{end}}
//如果pipeline的值的长度为0,则会执行T0

//管道
//Go的模板语法中支持使用管道符号|链接多个命令,用|将其左边的输出作为其右边的输入。用法和unix下的管道类似
{{ .Name | printf "%.3s" }}
{{. | sayHi}}

5、模板函数

自定义函数

func sayHi(name string) string {
	return "Hi, " + name
}
func introduceAge(age uint) string {
	return "I'm " + strconv.Itoa(int(age)) + " years old"
}

func TestTemplateFunc(t *testing.T) {
	tepl := `{{sayHi .Name}}, {{age .Age}}`
	p := Person{Name: "test", Age: 20}
    //注册sayHi、introduceAge,分别命名为sayHi、age
    //func Must(t *Template, err error) *Template
	tmpl := template.Must(template.New("test").
		Funcs(template.FuncMap{"sayHi": sayHi, "age": introduceAge}).
		Parse(tepl))
	tmpl.Execute(os.Stdout, p)
    //输出Hi, test, I'm 20 years old
}

比较函数
{ { function arg1 arg2 }}, 例如{{if gt .Age 18}} 表示 if age>18

● eq: arg1 == arg2
● ne: arg1 != arg2
● lt: arg1 < arg2
● le: arg1 <= arg2
● gt: arg1 > arg2
● ge: arg1 >= arg2

eq函数比较特殊,可以拿多个参数和第一个参数进行比较。{ { eq arg1 arg2 arg3 arg4}}逻辑是arg1==arg2 || arg1==arg3 || arg1==arg4。

预定义函数

and
    函数返回它的第一个empty参数或者最后一个参数;
    就是说"and x y"等价于"if x then y else x";所有参数都会执行;
or
    返回第一个非empty参数或者最后一个参数;
    亦即"or x y"等价于"if x then x else y";所有参数都会执行;
not
    返回它的单个参数的布尔值的否定
len
    返回它的参数的整数类型长度
index
    执行结果为第一个参数以剩下的参数为索引/键指向的值;
    如"index x 1 2 3"返回x[1][2][3]的值;每个被索引的主体必须是数组、切片或者字典。
print
    即fmt.Sprint
printf
    即fmt.Sprintf
println
    即fmt.Sprintln
html
    返回其参数文本表示的HTML逸码等价表示。
urlquery
    返回其参数文本表示的可嵌入URL查询的逸码等价表示。
js
    返回其参数文本表示的JavaScript逸码等价表示。
call
    执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数;
    如"call .X.Y 1 2"等价于go语言里的dot.X.Y(1, 2);
    其中Y是函数类型的字段或者字典的值,或者其他类似情况;
    call的第一个参数的执行结果必须是函数类型的值(和预定义函数如print明显不同);
    该函数类型值必须有12个返回值,如果有2个则后一个必须是error接口类型;
    如果有2个返回值的方法返回的errornil,模板执行会中断并返回给调用模板执行者该错误;

6、HTML

html/template使用和text/template包相同的API和模板语言。但是增加了一个将字符串自动转义特性,可以避免一些注入攻击的安全问题。
例如"

A header!

“中的尖括号会被编码为<h1>A header!</h1>。
template.HTML可以告诉Go要处理的字符串是安全的,不需要编码。template.HTML(”

A Safe header

")会输出

A Safe header

,注意这个方法处理用户的输入的时候比较危险。

const templ = `<p>A: {{.A}}</p><p>B: {{.B}}</p>`
    tmpl := template.Must(template.New("escape").Parse(templ))
    var data struct {
        A string        // untrusted plain text
        B template.HTML // trusted HTML
    }
    data.A = "<b>Hello!</b>"
    data.B = "<b>Hello!</b>"
    if err := tmpl.Execute(os.Stdout, data); err != nil {
        log.Fatal(err)
    }
//输出:<p>A: &lt;b&gt;Hello!&lt;/b&gt;</p><p>B: <b>Hello!</b></p>
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

克里斯蒂亚诺·罗纳尔达

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值