当前位置: 首页 > news >正文

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 []intzlen := 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)] = yreturn z 
}

通过在每次 扩展数组时直接将长度翻倍从而避免了多次内存分配:

func main() {var x, y []intfor 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      *intlen, 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 := 0for _, s := range strings {if s != "" {strings[i] = si++ }}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 originalfor _, 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 stringsinput := bufio.NewScanner(os.Stdin)for input.Scan() {line := input.Text()if !seen[line] {seen[line] = truefmt.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        intName      stringAddress   stringDoB       time.TimePosition  stringSalary    intManagerID 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            intName, Address stringDoB           time.TimePosition      stringSalary        intManagerID     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 stringport 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 PointRadius int 
}
type Wheel struct {Circle CircleSpokes int 
}

结构体类型变的清晰了,但是这种修改同时也导致了访问每个成员变得繁琐:

var w Wheel
w.Circle.Center.X = 8
w.Circle.Center.Y = 8
w.Circle.Radius = 5
w.Spokes = 20

只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针

type Circle struct {PointRadius int 
}
type Wheel struct {CircleSpokes 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  stringYear   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 githubimport "time"const IssuesURL = "https://api.github.com/search/issues"type IssuesSearchResult struct {TotalCount int `json:"total_count"`Items          []*Issue
}type Issue struct {Number intHTMLURL string `json:"html_url"`Title stringState	stringUser	*UserCreatedAt time.Time `json:"created_at"`Body      string    // in Markdown format
}type User struct {Login   stringHTMLURL 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 IssuesSearchResultif 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>
`))

相关文章:

go学习 4、复合数据类型

4、复合数据类型 数组、slice、map和结构体 如何使用结构体来解码和编码到对应JSON格式的数据&#xff0c;并且通过结合使用模板来生成HTML页面 数组和结构体是聚合类型;它们的值由许多元素或成员字段的值组成。数组是由同构的元素组成&#xff08;每个数组元素都是完全相同的…...

Rust: Vec类型的into_boxed_slice()方法

比如&#xff0c;我们经常看到Vec类型&#xff0c;但取转其裸指针&#xff0c;经常会看到into_boxed_slice()方法&#xff0c;这是为何&#xff1f; use std::{fmt, slice};#[derive(Clone, Copy)] struct RawBuffer {ptr: *mut u8,len: usize, }impl From<Vec<u8>&g…...

Python - Opencv + pyzbar实时摄像头识别二维码

直接上代码&#xff1a; import cv2 from pyzbar.pyzbar import decodecap cv2.VideoCapture(0) # 打开摄像头while True: # 循环读取摄像头帧ret, frame cap.read()# 在循环中&#xff0c;将每一帧作为图像输入&#xff0c;使用pyzbar的decode()函数识别二维码barcodes …...

网络安全(黑客)就业分析指导

一、针对网络安全市场分析 市场需求量高&#xff1b;则是发展相对成熟入门比较容易。所需要的技术水平国家政策环境 对于国家与企业的地位愈发重要&#xff0c;没有网络安全就没有国家安全 更有为国效力的正义黑客—红客联盟 可见其重视程度。 需要掌握的知识点偏多 外围打点…...

MySQL 主从复制的认识 2023.07.23

一、理解MySQL主从复制原理 1、概念&#xff1a;主从复制是用来建立一个和 主数据库完全一样的数据库环境称为从数据库&#xff1b;主数据库一般是准实时的业务数据库。 2、作用&#xff1a;灾备、数据分布、负载平衡、读写分离、提高并发能力 3、原理图 4、具体步骤 (1) M…...

elasticsearch查询操作(API方式)

说明&#xff1a;elasticsearch查询操作除了使用DSL语句的方式&#xff08;参考&#xff1a;http://t.csdn.cn/k7IGL&#xff09;&#xff0c;也可以使用API的方式。 准备 使用前需先导入依赖 <!--RestHighLevelClient依赖--><dependency><groupId>org.ela…...

Java版企业工程项目管理系统源码+java版本+项目模块功能清单+spring cloud +spring boot

工程项目各模块及其功能点清单 一、系统管理 1、数据字典&#xff1a;实现对数据字典标签的增删改查操作 2、编码管理&#xff1a;实现对系统编码的增删改查操作 3、用户管理&#xff1a;管理和查看用户角色 4、菜单管理&#xff1a;实现对系统菜单的增删改查操…...

理解Android中不同的Context

作者&#xff1a;两日的blog Context是什么&#xff0c;有什么用 在Android开发中&#xff0c;Context是一个抽象类&#xff0c;它是Android应用程序环境的一部分。它提供了访问应用程序资源和执行各种操作的接口。可以说&#xff0c;Context是Android应用程序与系统环境进行交…...

linux判断端口是否占用(好用)

netstat 一般的话使用 netstat -tunlp | grep xxx参数作用-t指明显示TCP端口-u指明显示UDP端口-l仅显示监听套接字(所谓套接字就是使应用程序能够读写与收发通讯协议(protocol)与资料的程序)-p显示进程标识符和程序名称&#xff0c;每一个套接字/端口都属于一个程序。-n不进行…...

springboot 自定义注解 ,实现接口限流(计数器限流)【强行喂饭版】

思路&#xff1a;通过AOP拦截注解标记的方法&#xff0c;在Redis中维护一个计数器来记录接口访问的频率&#xff0c; 并根据限流策略来判断是否允许继续处理请求。 另一篇&#xff1a;springboot 自定义注解 &#xff0c;aop切面Around&#xff1b; 为接口实现日志插入【强行喂…...

istio安装部署总结

istio安装部署总结 大纲 istio基础概念版本选择安装istio核心主件卸载istiokiali安装 istio基础概念 https://istio.io/latest/zh/docs/ 中文文档 istio是一个服务治理平台&#xff0c;治理服务间的访问&#xff0c;&#xff08;例如流量控制&#xff0c;安全策略&#xf…...

Linux操作系统~必考面试题⑨

1、rpm 命令 Linux rpm 命令用于管理套件。 rpm(redhat package manager) 原本是 Red Hat Linux 发行版专门用来管理Linux 各项套件的程序&#xff0c;由于它遵循 GPL 规则且功能强大方便&#xff0c;因而广受欢迎。逐渐受到其他发行版的采用。 RPM 套件管理方式的出现&…...

国标GB28181协议视频平台EasyCVR修改录像计划等待时间较长的原因排查与解决

音视频流媒体视频平台EasyCVR拓展性强&#xff0c;视频能力丰富&#xff0c;具体可实现视频监控直播、视频轮播、视频录像、云存储、回放与检索、智能告警、服务器集群、语音对讲、云台控制、电子地图、H.265自动转码H.264、平台级联等。为了便于用户二次开发、调用与集成&…...

线性代数(主题篇):第三章:向量组 、第四章:方程组

文章目录 第3章 n维向量1.概念(1)n维单位列向量 2.向量、向量组的的线性关系(线性相关性)(1)线性表示 &#xff1a;AXβ(2)线性相关、线性无关&#xff1a; AX0①线性相关②线性无关③线性相关性7大定理 3.极大线性无关组、等价向量组、向量组的秩1.极大线性无关组2.等价向量组…...

大数据课程C4——ZooKeeper结构运行机制

文章作者邮箱&#xff1a;yugongshiyesina.cn 地址&#xff1a;广东惠州 ▲ 本章节目的 ⚪ 了解Zookeeper的特点和节点信息&#xff1b; ⚪ 掌握Zookeeper的完全分布式安装 ⚪ 掌握Zookeeper的选举机制、ZAB协议、AVRO&#xff1b; 一、Zookeeper-简介 1. 特点…...

解决伪类元素‘after‘或者‘before‘遮挡父元素,导致鼠标移入或点击等事件不生效的问题

第一种调整css的index值 如果对显示没有影响的话&#xff0c;可以这么做 第二种设置css属性&#xff1a;pointer-event&#xff1a;none 原理是&#xff1a; 对一个元素设置 pointer-events: none&#xff0c;能让浏览器在处理鼠标操作时&#xff0c;忽视掉这个元素的存在&a…...

电动汽车市场的减速,正在让小鹏汽车付出代价

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 总结&#xff1a; &#xff08;1&#xff09;由于价格压力上升、竞争加剧和需求减弱&#xff0c;小鹏汽车的交付量出现了明显下滑&#xff0c;6月份的交付量已经同比下降了43%。 &#xff08;2&#xff09;小鹏汽车对2023年…...

Yarn上Streaming流自动调节资源设计

Streaming流自动调节资源 自动资源调节简单来说就是根据数据的输入速率和数据的消费速率来判断是否应该调节资源。如果输入速率大于消费速率&#xff0c;并且在输入速率还在攀升&#xff0c;则将该Job停止并调高Job的资源等级然后重启。如果消费速率大于输入速率&#xff0c;并…...

微信小程序的个人博客--【小程序花园】

微信目录集链接在此&#xff1a; 详细解析黑马微信小程序视频–【思维导图知识范围】难度★✰✰✰✰ 不会导入/打开小程序的看这里&#xff1a;参考 让别人的小程序长成自己的样子-更换window上下颜色–【浅入深出系列001】 文章目录 本系列校训啥是个人博客项目里的理论知识…...

智慧园区楼宇合集 | 图扑数字孪生管控系统

智慧园区是指将物联网、大数据、人工智能等技术应用于传统建筑和基础设施&#xff0c;以实现对园区的全面监控、管理和服务的一种建筑形态。通过将园区内设备、设施和系统联网&#xff0c;实现数据的传输、共享和响应&#xff0c;提高园区的管理效率和运营效益&#xff0c;为居…...

Flask RESTful 示例

目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题&#xff1a; 下面创建一个简单的Flask RESTful API示例。首先&#xff0c;我们需要创建环境&#xff0c;安装必要的依赖&#xff0c;然后…...

Admin.Net中的消息通信SignalR解释

定义集线器接口 IOnlineUserHub public interface IOnlineUserHub {/// 在线用户列表Task OnlineUserList(OnlineUserList context);/// 强制下线Task ForceOffline(object context);/// 发布站内消息Task PublicNotice(SysNotice context);/// 接收消息Task ReceiveMessage(…...

Java如何权衡是使用无序的数组还是有序的数组

在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...

大数据零基础学习day1之环境准备和大数据初步理解

学习大数据会使用到多台Linux服务器。 一、环境准备 1、VMware 基于VMware构建Linux虚拟机 是大数据从业者或者IT从业者的必备技能之一也是成本低廉的方案 所以VMware虚拟机方案是必须要学习的。 &#xff08;1&#xff09;设置网关 打开VMware虚拟机&#xff0c;点击编辑…...

AI,如何重构理解、匹配与决策?

AI 时代&#xff0c;我们如何理解消费&#xff1f; 作者&#xff5c;王彬 封面&#xff5c;Unplash 人们通过信息理解世界。 曾几何时&#xff0c;PC 与移动互联网重塑了人们的购物路径&#xff1a;信息变得唾手可得&#xff0c;商品决策变得高度依赖内容。 但 AI 时代的来…...

MySQL 部分重点知识篇

一、数据库对象 1. 主键 定义 &#xff1a;主键是用于唯一标识表中每一行记录的字段或字段组合。它具有唯一性和非空性特点。 作用 &#xff1a;确保数据的完整性&#xff0c;便于数据的查询和管理。 示例 &#xff1a;在学生信息表中&#xff0c;学号可以作为主键&#xff…...

嵌入式常见 CPU 架构

架构类型架构厂商芯片厂商典型芯片特点与应用场景PICRISC (8/16 位)MicrochipMicrochipPIC16F877A、PIC18F4550简化指令集&#xff0c;单周期执行&#xff1b;低功耗、CIP 独立外设&#xff1b;用于家电、小电机控制、安防面板等嵌入式场景8051CISC (8 位)Intel&#xff08;原始…...

MySQL 主从同步异常处理

阅读原文&#xff1a;https://www.xiaozaoshu.top/articles/mysql-m-s-update-pk MySQL 做双主&#xff0c;遇到的这个错误&#xff1a; Could not execute Update_rows event on table ... Error_code: 1032是 MySQL 主从复制时的经典错误之一&#xff0c;通常表示&#xff…...

git: early EOF

macOS报错&#xff1a; Initialized empty Git repository in /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/.git/ remote: Enumerating objects: 2691797, done. remote: Counting objects: 100% (1760/1760), done. remote: Compressing objects: 100% (636/636…...

十九、【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建

【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建 前言准备工作第一部分:回顾 Django 内置的 `User` 模型第二部分:设计并创建 `Role` 和 `UserProfile` 模型第三部分:创建 Serializers第四部分:创建 ViewSets第五部分:注册 API 路由第六部分:后端初步测…...