4、复合数据类型
数组、slice、map和结构体
如何使用结构体来解码和编码到对应JSON格式的数据,并且通过结合使用模板来生成HTML页面
数组和结构体是聚合类型;它们的值由许多元素或成员字段的值组成。数组是由同构的元素组成(每个数组元素都是完全相同的类型);结构体则是由异构的元素组成的。数组和结构体都是有固定内存大小的数据结构。
slice和map则是动态的数据结构,它们将根据需要动态增长。
4.1 数组
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。
和数组对应的类型是 Slice(切片),它是可以增长和收缩动态序列,slice功能也更灵活。
var a [3]int // array of 3 integers
fmt.Println(a[0]) // print the first element
fmt.Println(a[len(a)-1]) // print the last element, a[2]
// Print the indices and elements.
for i, v := range a {
fmt.Printf("%d %d\n", i, v)
}
// Print the elements only.
for _, v := range a {
fmt.Printf("%d\n", v)
}
初始化:
var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // "0"
如果在数组的长度位置出现的是“…”省略号,则表示数组的长度是根据初始 化值的个数来计算。
q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"
数组的长度是数组类型的一个组成部分,[3]int和[4]int是两种不同的数组类型。数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定
q := [3]int{1, 2, 3}
//报错:
q = [4]int{1, 2, 3, 4} // compile error: cannot assign [4]int to [3]int
指定一个索引和对应值列表的方式初始化
type Currency int
const (
USD Currency = iota// 美元
EUR// 欧元
GBP// 英镑
RMB// 人民币
)
symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"} fmt.Println(RMB, symbol[RMB]) // "3 ¥"
//定义了一个含有100个元素的数组r,最后一个元素被初始化为-1,其它元素都是用0初始化
r := [...]int{99: -1}
数组比较:
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1, 2}
fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int
函数调用中,通过指针来传递数组参数是高效的,允许在函数内部修改数组的值。但是数组依然是僵化的类型,因为数组的类型包含了僵化的长度信息。数组依然很少用作函数参数;相反,我们一 般使用slice来替代数组。
4.2 Slice
Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作 []T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。
一个slice是一个轻量级的数据结构,提供了访问数组子序 列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象
一个slice由三个部分构成:
- 指针:指向第一个slice元素对应的底层数组元素的地址,slice的第一个元素并不一定就是数组的第一个元素
- 长度:对应slice中元素的数目;长度不能超过容量
- 容量一般是从slice的开始位置到底层数据的结尾位置
切片操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用于创建一个新的slice,引用s的从第i个元素开 始到第j-1个元素的子序列。新的slice将只有j-i个元素
多个slice之间可以共享底层数据。
数组定义:
months := [...]string{1: "January", /* ... */, 12: "December"}
Q2 := months[4:7]
summer := months[6:9]
fmt.Println(Q2) // ["April" "May" "June"]
fmt.Println(summer) // ["June" "July" "August"]
两个slice都包含了六月份,测试包含相同月份:
for _, s := range summer {
for _, q := range Q2 {
if s == q {
fmt.Printf("%s appears in both\n", s)
}
}
}
如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了 slice,因为新slice的长度会变大:
fmt.Println(summer[:20]) // panic: out of range
endlessSummer := summer[:5] // extend a slice (within capacity)
fmt.Println(endlessSummer) // "[June July August September October]"
因为slice值包含指向第一个slice元素的指针,因此向函数传递slice将允许在函数内部修改底层数组的元素。复制一个slice只是对底层的数组创建了一个新的slice别名。
reverse函数在原内存空间将[]int类型的slice反转,而且它可以用于任意长度的slice。
// reverse reverses a slice of ints in place.
func reverse(s []int) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}
a := [...]int{0, 1, 2, 3, 4, 5}
reverse(a[:])
fmt.Println(a) // "[5 4 3 2 1 0]"
将slice元素循环向左旋转n个元素的方法是三次调用reverse反转函数
s := []int{0, 1, 2, 3, 4, 5}
// Rotate s left by two positions.
reverse(s[:2])//[1,0,2,3,4,5]
reverse(s[2:])//[1,0,5,4,3,2]
reverse(s)
fmt.Println(s) // "[2 3 4 5 0 1]"
slice类型的变量s初始化语法,没有指明序列的 长度。这会隐式地创建一个合适大小的数组,然后slice的指针指向底层的数组。
slice之间不能比较,自己展开每个元素进行比较:
func equal(x, y []string) bool {
if len(x) != len(y) {
return false
}
for i := range x {
if x[i] != y[i] {
return false
}
}
return true
}
不支持==操作的原因:
- 一个slice的元素是间接引用的,一个slice甚至可以包含自身。
- 一个固定的slice值(译注:指slice本身的值,不 是元素的值)在不同的时刻可能包含不同的元素,因为底层数组的元素可能会被修改。
slice唯一合法的比较操作是和nil比较:
if summer == nil { /* ... */ }
一个nil值的slice并没有底层数组,长度和容量都是0。
var s []int // len(s) == 0, s == nil
s = nil // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil
s = []int{} // len(s) == 0, s != nil
内置的make函数创建一个指定元素类型
make([]T, len)//容量等于长度
make([]T, len, cap) // same as make([]T, cap)[:len]
make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引 用底层匿名的数组变量。
4.2.1 append函数
向slice追加元素(理解slice底层是如何工作)
var runes []rune
for _, r := range "Hello, 世界" {
runes = append(runes, r)
}
fmt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']"
appendInt函数:
func appendInt(x []int, y int) []int {
var z []int
zlen := len(x) + 1
//检测slice底层数组是否有足够的容量来保存新添加的元素
if zlen <= cap(x) {
// There is room to grow. Extend the slice.
z = x[:zlen]
} else {
// There is insufficient space. Allocate a new array.
// Grow by doubling, for amortized linear complexity.
zcap := zlen
//通过在每次扩展数组时直接将长度翻倍从而避免了多次内存分配
if zcap < 2*len(x) {
zcap = 2 * len(x)
}
z = make([]int, zlen, zcap)
copy(z, x) // a built-in function; see text
}
z[len(x)] = y
return z
}
通过在每次 扩展数组时直接将长度翻倍从而避免了多次内存分配:
func main() {
var x, y []int
for i := 0; i < 10; i++ {
y = appendInt(x, i)
fmt.Printf("%d cap=%d\t%v\n", i, cap(y), y)
x=y
}
}
每一次容量的变化都会导致重新分配内存和copy操作:
i=3次的迭代:
i=4次的迭代:
slice实际上是一个类似下面结构体的聚合类型:
type IntSlice struct {
ptr *int
len, cap int
}
内置的append函数则可以追加多个 元素,甚至追加一个slice。
var x []int
x = append(x, 1)
x = append(x, 2, 3)
x = append(x, 4, 5, 6)
x = append(x, x...) // append the slice x
fmt.Println(x) // "[1 2 3 4 5 6 1 2 3 4 5 6]"
4.2.2 Slice内存技巧
nonempty函数将在原有slice内存空间之上返回不包含空字符串的列表:
package main
import "fmt"
// nonempty returns a slice holding only the non-empty strings.
// The underlying array is modified during the call.
func nonempty(strings []string) []string {
i := 0
for _, s := range strings {
if s != "" {
strings[i] = s
i++
}
}
return strings[:i]
}
输入的slice和输出的slice共享一个底层数组,可以避免分配另一个数 组,不过原来的数据将可能会被覆盖
data := []string{"one", "", "three"}
fmt.Printf("%q\n", nonempty(data)) // `["one" "three"]`
fmt.Printf("%q\n", data) // `["one" "three" "three"]`
data = nonempty(data)
nonempty函数使用append函数实现:
func nonempty2(strings []string) []string {
out := strings[:0] // zero-length slice of original
for _, s := range strings {
if s != "" {
out = append(out, s)
}
}
return out
}
一个slice可以用来模拟一个stack。最初给定的空slice对应一个空的stack,然后可以使用 append函数将新的值压入stack:
stack = append(stack, v) // push v
stack的顶部位置对应slice的最后一个元素:
top := stack[len(stack)-1] // top of stack
通过收缩stack可以弹出栈顶的元素
stack = stack[:len(stack)-1] // pop
要删除slice中间的某个元素并保存原有的元素顺序,可以通过内置的copy函数将后面的子 slice向前依次移动一位完成:
func remove(slice []int, i int) []int {
copy(slice[i:], slice[i+1:])
return slice[:len(slice)-1]
}
func main() {
s := []int{5, 6, 7, 8, 9}
fmt.Println(remove(s, 2)) // "[5 6 8 9]"
}
如果删除元素后不用保持原来顺序的话,我们可以简单的用最后一个元素覆盖被删除的元素:
func remove(slice []int, i int) []int {
slice[i] = slice[len(slice)-1]
return slice[:len(slice)-1]
}
func main() {
s := []int{5, 6, 7, 8, 9}
fmt.Println(remove(s, 2)) // "[5 6 9 8]
}
4.3 Map
哈希表是一个无序的key/value对的集合,其中所有的key 都是不同的,然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value。map就是一个哈希表的引用:
map[K]V
map中所有的key都有相同的类型,所有的value也有着相同的类型,但是 key和value之间可以是不同的数据类型。
key必须是支持==比较运算符的数据类型,map可以通过测试key是否相等来判断是否已经存在。
内置的make函数可以创建一个map:
ages := make(map[string]int) // mapping from strings to ints
用map字面值的语法创建map,同时还可以指定一些最初的key/value:
ages := map[string]int{
"alice": 31,
"charlie": 34,
}
ages := make(map[string]int)
ages["alice"] = 31
ages["charlie"] = 34
创建空的map的表达式:
map[string]int{}
Map中的元素通过key对应的下标访问:
ages["alice"] = 32
fmt.Println(ages["alice"]) // "32"
使用内置的delete函数可以删除元素:
delete(ages, "alice") // remove element ages["alice"]
元素不在map中也没有关系;如果一个查找失败将返回 value类型对应的零值。
x += y 和 x++ 等简短赋值语法也可以用在map
但是map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作。原因:map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。
遍历map中全部的key/value对:
for name, age := range ages {
fmt.Printf("%s\t%d\n", name, age)
}
Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。
如果要按顺序遍历key/value对,我们必须显式地对key进行排序,可以使用sort包的Strings函数对字符串slice进行排序。
import "sort"
var names []string
for name := range ages {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
fmt.Printf("%s\t%d\n", name, ages[name])
}
创建了一个空的slice,但是slice的容量刚好可以放下map中全部的key:
names := make([]string, 0, len(ages))
map类型的零值是nil,也就是没有引用任何哈希表。
var ages map[string]int
fmt.Println(ages == nil) // "true"
fmt.Println(len(ages) == 0) // "true"
map上的大部分操作,包括查找、删除、len和range循环都可以安全工作在nil值的map上,它 们的行为和一个空的map类似。但是向一个nil值的map存入元素将导致一个panic异常:
ages["carol"] = 21 // panic: assignment to entry in nil map
在向map存数据前必须先创建map。
如果元素类型是一个数字,你可以需要区分一个已经存在的0,和不存在而返回 零值的0,可以像下面这样测试:
age, ok := ages["bob"]
if !ok { /* "bob" is not a key in this map; age == 0. */ }
//ok报告元素是否真的存在
if age, ok := ages["bob"]; !ok { /* ... */ }
map之间也不能进行相等比较;唯一的例外是和nil进行比较。
判断两个map是 否包含相同的key和value,我们必须通过一个循环实现:
func equal(x, y map[string]int) bool {
if len(x) != len(y) {
return false
}
for k, xv := range x {
if yv, ok := y[k]; !ok || yv != xv {
return false
}
}
return true
}
Go语言中并没有提供一个set类型,但是map中的key也是不相同的,可以用map实现类似set 的功能。
dedup程序读取多行输入,但是只打印第一次出现的行。dedup程序通过map来表示所有的输入行所对应的 set集合,以确保已经在集合存在的行不会被重复打印。
func main() {
seen := make(map[string]bool) // a set of strings
input := bufio.NewScanner(os.Stdin)
for input.Scan() {
line := input.Text()
if !seen[line] {
seen[line] = true
fmt.Println(line)
}
}
if err := input.Err(); err != nil {
fmt.Fprintf(os.Stderr, "dedup: %v\n", err)
os.Exit(1)
}
}
需要一个map或set的key是slice类型,但是map的key必须是可比较的类型,但是 slice并不满足这个条件。
使用map来记录提交相同的字符串列表的次数(处理任何不可比较的key类型):
var m = make(map[string]int)
func k(list []string) string { return fmt.Sprintf("%q", list) }
func Add(list []string) { m[k(list)]++ }
func Count(list []string) int { return m[k(list)] }
Map的value类型也可以是一个聚合类型,比如是一个map或slice。
4.4 结构体
结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。
声明了叫Employee的命名的结构体类型,声明一个Employee类型的变量dilbert:
type Employee struct {
ID int
Name string
Address string
DoB time.Time
Position string
Salary int
ManagerID int
}
var dilbert Employee
dilbert结构体变量的成员可以通过点操作符访问
对成员赋值:
dilbert.Salary -= 5000 // demoted, for writing too few lines of code
对成员取地址,然后通过指针访问:
position := &dilbert.Position
*position = "Senior " + *position // promoted, for outsourcing to Elbonia
点操作符和指向结构体的指针一起工作:
var employeeOfTheMonth *Employee = &dilbert
employeeOfTheMonth.Position += " (proactive team player)"
EmployeeByID函数将根据给定的员工ID返回对应的员工信息结构体的指针。
func EmployeeByID(id int) *Employee { /* ... */ } fmt.Println(EmployeeByID(dilbert.ManagerID).Position) // "Pointy-haired boss"
id := dilbert.ID
EmployeeByID(id).Salary = 0 // fired for... no real reason
如果相邻的成员类型如果相 同的话可以被合并到一行
type Employee struct {
ID int
Name, Address string
DoB time.Time
Position string
Salary int
ManagerID int
}
结构体成员的输入顺序也有重要的意义。
如果结构体成员名字是以大写字母开头的,那么该成员就是导出的;一个结构体可能同时包含导出和未导出的成员。
一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。但是S类型的结构体可以包含 *S 指针类型的成员,这可以让我 们创建递归的数据结构,比如链表和树结构等。
结构体类型的零值是每个成员都是零值。对于 bytes.Buffer类型,结构体初始值就是一个随时可用的空缓存;sync.Mutex的零值也是有效的未锁定状态。
如果结构体没有任何成员的话就是空结构体,写作struct{}。它的大小为0,也不包含任何信息,但是有时候依然是有价值的。
seen := make(map[string]struct{}) // set of strings
// ...
if _, ok := seen[s]; !ok {
seen[s] = struct{}{}
// ...first time seeing s...
}
4.4.1 结构体字面值
结构体值也可以用结构体字面值表示,结构体字面值可以指定每个成员的值。
type Point struct{ X, Y int }
//法一:以结构体成员定义的顺序 为每个结构体成员指定一个字面值
//一般只在定义结构体的包内部使用,或者是在较小的结构体中使用
p := Point{1, 2}
以成员名字和相应的值来初始化:
//法二:
anim := gif.GIF{LoopCount: nframes}
结构体可以作为函数的参数和返回值。
func Scale(p Point, factor int) Point {
return Point{p.X * factor, p.Y * factor}
}
fmt.Println(Scale(Point{1, 2}, 5)) // "{5 10}"
考虑效率,较大的结构体通常会用指针的方式传入和返回:
func Bonus(e *Employee, percent int) int {
return e.Salary * percent / 100
}
如果要在函数内部修改结构体成员的话,用指针传入是必须的;因为在Go语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。
func AwardAnnualRaise(e *Employee) {
e.Salary = e.Salary * 105 / 100
}
创建并初始化一个结构体变量,并返回结构体的地址:
pp := &Point{1, 2}
下面语句等价:
pp := new(Point)
*pp = Point{1, 2}
4.4.2 结构体比较
如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体 将可以使用==或!=运算符进行比较。
type Point struct{ X, Y int }
p := Point{1, 2}
q := Point{2, 1}
fmt.Println(p.X == q.X && p.Y == q.Y) // "false"
fmt.Println(p == q) // "false"
可比较的结构体类型,可以用于map的key类型。
type address struct {
hostname string
port int
}
hits := make(map[address]int)
hits[address{"golang.org", 443}]++
4.4.3 结构体嵌入和匿名成员
结构体嵌入机制让一个命名的结构体包含另一个结构体类型的匿名成员,通过简单的点运算符x.f来访问匿名成员链中嵌套的x.d.e.f成员。
type Circle struct {
X, Y, Radius int
}
type Wheel struct {
X, Y, Radius, Spokes int
}
创建一个wheel变量:
var w Wheel
w.X = 8
w.Y = 8
w.Radius = 5
w.Spokes = 20
将相同的属性独立出来:
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
w.Circle.Center.Y = 8
w.Circle.Radius = 5
w.Spokes = 20
只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针
type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}
匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径:
我们在访问子成员的时候可以忽略任何匿名成员部分。
var w Wheel
w.X = 8
w.Y = 8
w.Radius = 5
w.Spokes = 20
结构体字面值并没有简短表示匿名成员的语法,下面的语句都不能编译通过:
w = Wheel{8, 8, 5, 20} // compile error: unknown fields
w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields
结构体字面值必须遵循形状类型声明时的结构:
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)
}
fmt.Printf("%#v\n", w)
// Output:
// Wheel{Circle:Circle{Point:Point{X:8, Y:8}, Radius:5}, Spokes:20}
w.X = 42
fmt.Printf("%#v\n", w)
// Output:
// Wheel{Circle:Circle{Point:Point{X:42, Y:8}, Radius:5}, Spokes:20}
#:用和Go语言类似的语法打印值
不能同时包含两个类型相同的匿名成员
匿名成员并不要求是结构体类型;其实任何命名的类型都可以作为结构体的匿名成员
匿名类型的方法集
外层的结构体不仅仅是获得了匿名成员类型的所有成员,而 且也获得了该类型导出的全部的方法。这个机制可以用于将一个有简单行为的对象组合成有复杂行为的对象。
4.5 JSON
JavaScript对象表示法(JSON)是一种发送和接收结构化信息的标准协议。Go语言对于这些标准格式的编码和解码都有良好的支持,由标准库中的encoding/json、 encoding/xml、encoding/asn1等包提供支持。
JSON是对JavaScript中各种类型的值——字符串、数字、布尔值和对象——Unicode本文编码。
基本的JSON类型有数字(十进制或科学记数法)、布尔值(true或false)、字符串,其中字符串是以双引号包含的Unicode字符序列,支持和Go语言类似的反斜杠转义特性,不过JSON 使用的是 \Uhhhh 转义数字来表示一个UTF-16编码(译注:UTF-16和UTF-8一样是一种变长 的编码,有些Unicode码点较大的字符需要用4个字节表示;而且UTF-16还有大端和小端的问题),而不是Go语言的rune类型。
一个JSON数组是一个有序的值序列,写在一个方括号中并以逗号分隔;一个JSON数组可以用于编码Go语言的数组和slice。一个JSON对象是一个字符串到值的映射,写成以系列的name:value对形式,用花括号包含并以逗号分隔;JSON的对象类型可以用于编码Go语言的map类型(key类型是字符串) 和结构体。
boolean true
number -273.15
string "She said \"Hello, BF\""
array ["gold", "silver", "bronze"]
object {"year": 1980,
"event": "archery",
"medals": ["gold", "silver", "bronze"]}
考虑一个应用程序,该程序负责收集各种电影评论并提供反馈功能。
type Movie struct {
Title string
Year int `json:"released"`
Color bool `json:"color,omitempty"`
Actors []string
}
var movies = []Movie{
{Title: "Casablanca", Year: 1942, Color: false,
Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
{Title: "Cool Hand Luke", Year: 1967, Color: true,
Actors: []string{"Paul Newman"}},
{Title: "Bullitt", Year: 1968, Color: true,
Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},
// ...
}
将一个Go语言中类似movies的结构体slice转为JSON的过程叫编组(marshaling)。编组通过调用 json.Marshal函数完成:
data, err := json.Marshal(movies)
if err != nil {
log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)
Marshal函数返还一个编码后的字节slice,包含很长的字符串,并且没有空白缩进;我们将它 折行以便于显示:
[{"Title":"Casablanca","released":1942,"Actors":["Humphrey Bogart","Ingr id Bergman"]},{"Title":"Cool Hand Luke","released":1967,"color":true,"Ac tors":["Paul Newman"]},{"Title":"Bullitt","released":1968,"color":true," Actors":["Steve McQueen","Jacqueline Bisset"]}]
json.MarshalIndent函数将产生整齐缩进的输出。该函数有两个额外的字符串参数用于表示每一行输出的前缀和每一个层级的缩进:
data, err := json.MarshalIndent(movies, "", " ")
if err != nil {
log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)
上面的代码将产生这样的输出:
[
{
"Title": "Casablanca",
"released": 1942,
"Actors": [
"Humphrey Bogart",
"Ingrid Bergman"
]
},
{
"Title": "Cool Hand Luke",
"released": 1967,
"color": true,
"Actors": [
"Paul Newman"
]
},
{
"Title": "Bullitt",
"released": 1968,
"color": true,
"Actors": [
"Steve McQueen",
"Jacqueline Bisset"
]
}
]
在编码时,默认使用Go语言结构体的成员名字作为JSON的对象。只有导出的结构体成员才会被编码,这也就是我们为什么选择用大写字母开头的成员名称。
Year名字的成员在编码后变成了released,还有Color成员 编码后变成了小写字母开头的color。这是因为构体成员Tag所导致的。一个构体成员Tag是和在编译阶段关联到该成员的元信息字符串:
Year int `json:"released"`
Color bool `json:"color,omitempty"`
结构体的成员Tag可以是任意的字符串面值,但是通常是一系列用空格分隔的key:"value"键值 对序列;因为值中含义双引号字符,因此成员Tag一般用原生字符串面值的形式书写。
omitempty选项,表示当Go语言结构体成员为空或零值时不生成JSON对象
解码,对应将JSON数据解码为Go语言的数据结构,Go语言中一般叫 unmarshaling,通过json.Unmarshal函数完成。
选择性 地解码JSON中感兴趣的成员。当Unmarshal函数调用返回,slice将被只含有Title信息值填 充,其它JSON成员将被忽略。
var titles []struct{ Title string }
if err := json.Unmarshal(data, &titles); err != nil {
log.Fatalf("JSON unmarshaling failed: %s", err)
}
fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]"
许多web服务都提供JSON接口,通过HTTP接口发送JSON格式请求并返回JSON格式的信 息。
Github的issue查询服务:
定义合适的类型和常量
// Package github provides a Go API for the GitHub issue tracker.
// See https://developer.github.com/v3/search/#search-issues.
package github
import "time"
const IssuesURL = "https://api.github.com/search/issues"
type IssuesSearchResult struct {
TotalCount int `json:"total_count"`
Items []*Issue
}
type Issue struct {
Number int
HTMLURL string `json:"html_url"`
Title string
State string
User *User
CreatedAt time.Time `json:"created_at"`
Body string // in Markdown format
}
type User struct {
Login string
HTMLURL string `json:"html_url"`
}
对应的JSON对象名是小写字母,每个结构体的成员名也是声明为大写字母开头的。
SearchIssues函数发出一个HTTP请求,然后解码返回的JSON格式的结果。因为用户提供的 查询条件可能包含类似 ? 和 & 之类的特殊字符,为了避免对URL造成冲突,我们用 url.QueryEscape来对查询中的特殊字符进行转义操作。
package github
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
)
// SearchIssues queries the GitHub issue tracker.
func SearchIssues(terms []string) (*IssuesSearchResult, error) {
q := url.QueryEscape(strings.Join(terms, " "))
resp, err := http.Get(IssuesURL + "?q=" + q)
if err != nil {
return nil, err
}
// We must close resp.Body on all execution paths.
// (Chapter 5 presents 'defer', which makes this simpler.)
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("search query failed: %s", resp.Status)
}
var result IssuesSearchResult
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
resp.Body.Close()
return nil, err
}
resp.Body.Close()
return &result, nil
}
基于流式的解码器json.Decoder,它可以从一个输入流解码 JSON数据;针对输出流的json.Encoder编码对象。
4.6 文本和HTML模板
复杂的打印格式,需要将格式化代码分离出来以便更安全地修改。
由text/template 和html/template等模板包提供的,它们提供了一个将变量值填充到一个文本或HTML格式的模板的机制。
text/template
一个模板是一个字符串或一个文件,里面包含了一个或多个由双花括号包含的 {{action}} 对象。
大部分的字符串只是按面值打印,但是对于actions部分将触发其它的行为。每个actions 都包含了一个用模板语言书写的表达式,一个action虽然简短但是可以输出复杂的打印值,模 板语言包含通过选择结构体的成员、调用函数或方法、表达式控制流if-else语句和range循环 语句,还有其它实例化模板等诸多特性。
const templ = `{{.TotalCount}} issues:
{{range .Items}}----------------------------------------
Number: {{.Number}}
User: {{.User.Login}}
Title: {{.Title | printf "%.64s"}}
Age: {{.CreatedAt | daysAgo}} days
{{end}}`
对于每一个action,都有一个当前值的概念,对应点操作符,写作“.”。当前值“.”最初被 初始化为调用模板时的参数
{{range .Items}} 和 {{end}} 对应一个循环action,循环每次迭代的当前值对应当前的Items元素的值。
| 操作符表示将前一个表达式的结果作为后一个函数的输入。
func daysAgo(t time.Time) int {
return int(time.Since(t).Hours() / 24)
}
time.Time类型对应的JSON值是一个标准时间格式的字符串。
生成模板的输出:分析模板并转为内部表示,然后基于指定的 输入执行模板。
//1、创建并返回一个模板
//2、自定义函数注册到模板中,并返回模板
//3、分析模板
report, err := template.New("report").
Funcs(template.FuncMap{"daysAgo": daysAgo}).
Parse(templ)
if err != nil {
log.Fatal(err)
}
如果模板解析失败将是一个致命的错误。template.Must 辅助函数可以简化这个致命错误的处理:它接受一个模板和一个error类型的参数,检测error 是否为nil(如果不是nil则发出panic异常),然后返回传入的模板。
一旦模板已经创建、注册了daysAgo函数、并通过分析和检测,我们就可以使用 github.IssuesSearchResult作为输入源、os.Stdout作为输出源来执行模板:
var report = template.Must(template.New("issuelist").
Funcs(template.FuncMap{"daysAgo": daysAgo}).
Parse(templ))
func main() {
result, err := github.SearchIssues(os.Args[1:])
if err != nil {
log.Fatal(err)
}
if err := report.Execute(os.Stdout, result); err != nil {
log.Fatal(err)
}
}
程序输出一个纯文本报告。
html/template模板包
和text/template包相同的API和模板语言,但是增加了一个将字符串自动转义特性,避免输入字符串和HTML、JavaScript、CSS或 URL语法产生冲突的问题,还可以避免一些长期存在的安全问题。
模板以HTML格式输出issue列表:
import "html/template"
var issueList = template.Must(template.New("issuelist").Parse(`
<h1>{{.TotalCount}} issues</h1>
<table>
<tr style='text-align: left'>
<th>#</th>
<th>State</th>
<th>User</th>
<th>Title</th>
</tr>
{{range .Items}}
<tr>
<td><a href='{{.HTMLURL}}'>{{.Number}}</a></td>
<td>{{.State}}</td>
<td><a href='{{.User.HTMLURL}}'>{{.User.Login}}</a></td>
<td><a href='{{.HTMLURL}}'>{{.Title}}</a></td>
</tr>
{{end}}
</table>
`))