go学习 4、复合数据类型

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/47692.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Nautilus Chain 即将治理通证 NAUT ,生态发展进程加速

独特且优势明显的 Nautilus Chain 目前&#xff0c;行业内首个模块化底层 Nautilus Chain 已经上线主网&#xff0c;并且即将有超过 70 个应用原生部署在 Nautilus Chain 上。Nautilus Chain 本身是一个以 Layer3 为定位的区块链系统&#xff0c;其通过 Celestia 模块化底层来…

监控和可观察性在 DevOps 中的作用!

在不断发展的DevOps世界中&#xff0c;深入了解系统行为、诊断问题和提高整体性能的能力是首要任务之一。监控和可观察性是促进这一过程的两个关键概念&#xff0c;为系统的健康状况和性能提供有价值的可见性。虽然这些术语经常互换使用&#xff0c;但它们代表了理解和管理复杂…

解决IDEA的git非常缓慢方法

解决IDEA的git非常缓慢方法 xxxx\IDEA2021.1.3\IntelliJ IDEA 2021.1.3\bin

如何使用 Flatpak 在 Linux 上安装 ONLYOFFICE 桌面编辑器?

Flatpak 是一款与 Linux 发行版无关的软件实用工具&#xff0c;可用于在 Linux 上构建和分发桌面端应用。其可帮助您安装第三方 Linux 应用程序&#xff0c;无需安装库或处理依赖。 ONLYOFFICE 桌面版是什么 ONLYOFFICE 编辑器桌面版是一款全面的办公工具&#xff0c;提供了文…

firefox笔记-Centos7离线安装firefox

目前&#xff08;2023-03-22 16:41:35&#xff09;Centos7自带的firefox已经很新了是2020年的。主要原因是有个web项目&#xff0c;用2020年的firefox打不开。 发到互联网上是2023-07-24。 报错是js有问题&#xff0c;估计是搞前端的只做了chrome适应&#xff0c;没做firefox…

618技术揭秘 - 大促弹窗搭投实践 | 京东云技术团队

背景 618 大促来了&#xff0c;对于业务团队来说&#xff0c;最重要的事情莫过于各种大促营销。如会场、直播带货、频道内营销等等。而弹窗作为一个极其重要的强触达营销工具&#xff0c;通常用来渲染大促氛围、引流主会场、以及通过频道活动来提升频道复访等。因此&#xff0…

【框架篇】Spring Boot 日志

Spring Boot 日志 一&#xff0c;日志用途 尽管一个项目在没有日志记录的情况下可能能够正常运行&#xff0c;但是日志记录对于我们来说却是至关重要的&#xff0c;它存在以下功能&#xff1a; 1&#xff0c;故障排查和调试&#xff1a;当项目出现异常或者故障时&#xff0c;…

钉钉返回:访问ip不在白名单之中,请参考FAQ

新版钉钉 在开发管理-服务器出口IP-配置返回错误信息返回给你的requestIp

Mysql sql优化

目录 目的 目标 explain 优化 避免使用select * 用union all代替union 小表驱动大表&#xff08;in与exists&#xff09; 批量操作 多使用limit in中值太多 不使用%前缀模糊查询 不在where子句中进行表达式操作 避免隐式类型转换 联合索引遵守最左前缀法则 inne…

平衡二叉树介绍

一、树的概念 1.1 空树和非空树 空树&#xff1a;结点数为0的树 非空树&#xff1a;有且仅有一个根节点。其中&#xff0c;没有后继的结点叫叶子结点&#xff0c;有后继的结点叫做分支结点。 如下图所示&#xff1a; 1.2树的属性 除了根结点外任何一个结点都有且仅有一个前…

【黑马头条之图片识别文字审核敏感词】

本笔记内容为黑马头条项目的图片识别文字审核敏感词部分 目录 一、需求分析 二、图片文字识别 三、Tess4j案例 四、管理敏感词和图片文字识别集成到文章审核 一、需求分析 产品经理召集开会&#xff0c;文章审核功能已经交付了&#xff0c;文章也能正常发布审核。对于上次…

组件间嵌套与父子组件通信

1.组件的嵌套 比如在App.vue内使用注册的ShowInfo组件,这就是组件嵌套,其中ShowInfo是子组件,App是父组件 ◼ 前面我们是将所有的逻辑放到一个App.vue中&#xff1a;  在之前的案例中&#xff0c;我们只是创建了一个组件App&#xff1b;  如果我们一个应用程序将所有的逻…

Ansible自动化运维学习——综合练习

目录 (一)练习一 1.新建一个role——app 2.创建文件 3.删除之前安装的httpd服务和apache用户 4.准备tasks任务 (1)创建组group.yml (2)创建用户user.yml (3)安装程序yum.yml (4)修改模板httpd.conf.j2 (5)编写templ.yml (6)编写start.yml (7)编写copyfile.yml (8…

TEE GP(Global Platform)技术委员会及中国任务小组

TEE之GP(Global Platform)认证汇总 一、TEE GP技术委员会 二、GP中国任务小组 参考&#xff1a; GlobalPlatform Certification - GlobalPlatform

基于C#的无边框窗体动画效果的完美解决方案 - 开源研究系列文章

最近在整理和编写基于C#的WinForm应用程序&#xff0c;然后碰到一个其他读者也可能碰到的问题&#xff0c;就是C#的Borderless无边框窗体的动画效果问题。 在Visual Studio 2022里&#xff0c;C#的WinForm程序提供了Borderless无边框窗体的样式效果&#xff0c;但是它没提供在无…

用QFramework来重构 祖玛游戏

资料 Unity - 祖玛游戏 GitHub 说明 用QF一个场景就够了&#xff0c;在UIRoot下切换预制体达到面板切换。 但测试中当然要有一个直接跳到测试面板的 测试脚本&#xff0c;保留测试Scene&#xff08;不然初学者也不知道怎么恢复测试Scene&#xff09;&#xff0c;所以全文按S…

经营在线业务的首选客服工具--SS客服

随着网购正在快速取代传统零售业&#xff0c;各行各业的企业都在大力发展电子商务以取悦客户。但是&#xff0c;有这么多可用的电子商务平台&#xff0c;选择一款符合自己发展的平台确实不容易。电子商务平台不仅是企业在线销售产品和服务的地方&#xff0c;也是他们管理日常运…

按键消抖实现

一、使用状态机实现按键消抖 可将按键按下整个过程看做四个状态&#xff1a;按键空闲状态&#xff0c;按下抖动状态&#xff0c;稳定按下状态&#xff0c;释放抖动状态。 代码实现&#xff1a; /** Description: 状态机方式按键消抖(多按键)* Author: Fu Yu* Date: 2023-07-27…

听GPT 讲K8s源代码--pkg(八)

k8s项目中 pkg/kubelet/envvars&#xff0c;pkg/kubelet/events&#xff0c;pkg/kubelet/eviction&#xff0c;pkg/kubelet/images&#xff0c;pkg/kubelet/kubeletconfig这些目录都是 kubelet 组件的不同功能模块所在的代码目录。 pkg/kubelet/envvars 目录中包含了与容器运行…

服务器用友数据库中了locked勒索病毒后怎么解锁数据恢复

随着信息技术的迅速发展&#xff0c;服务器成为现代企业中不可或缺的重要设备。然而&#xff0c;由于网络安全风险的存在&#xff0c;服务器在日常运作中可能遭受各种威胁&#xff0c;包括恶意软件和勒索病毒攻击。近日&#xff0c;我们收到很多企业的求助&#xff0c;企业的用…