Go语言圣经 第4章 复合数据类型

155 阅读27分钟

4. 复合数据类型

除了基础数据类型外,Golang还提供了一些复合类型:数组、slice、map和结构体。

4.1. 数组

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,因此在Go语言中很少直接使用数组。

使用var语句声明一个数组很简单:

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]

数组的每个元素可以通过索引下标来访问,索引下标的范围是从0开始到数组长度减1的位置。内置的len函数将返回数组中元素的个数。默认情况下,数组的每个元素都被初始化为元素类型对应的零值,对于数字类型来说就是0。

声明一个数组也可以使用数组字面值语法:

var a [3]int = [3]int{1, 2, 3}
var a2 [3]int = [3]int{0: 1, 1: 2, 2: 3}

在数组的字面值中,如果在数组的长度位置出现的是“...”省略号,则表示数组的长度是根据初始化值的个数来计算。因此,上面q数组的定义可以简化为

var a = [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"

声明一个数组也可以使用new关键字:

var a = new([3]int)
fmt.Println(a) // [0, 0, 0]

数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型。数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。

如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候数组才是相等的。不相等比较运算符!=遵循同样的规则。

传递数组作为函数参数:因为函数参数传递机制导致传递的参数进行复制,所以传递数组变量作为函数参数将是低效的。而且,在函数内部,对数组参数的任何修改都是发生在复制的数组上,并不能直接修改调用时原始的数组变量。

常见问题:Go语言的函数传递机制? 在Go语言中,函数参数传递机制是值传递。调用一个函数时,每个调用参数会复制一份传给函数的参数变量,所以函数的参数变量接收的是调用参数的副本,并不是调用参数的原始变量。 另外,Go语言对待数组的方式和其它很多编程语言不同,Go语言使用值传递,而其它一些编程语言可能会隐式地将数组作为引用或指针对象传入被调用的函数。

为了在函数内部修改传入的数组变量,我们可以显式地传入一个数组指针:函数通过指针对数组的任何修改都可以直接反馈到调用者。

func zero(ptr *[32]byte) {
    for i := range ptr {
        ptr[i] = 0
    }
}

虽然通过指针来传递数组参数是高效的,而且也允许在函数内部修改数组的值,但是数组依然是僵化的类型,因为数组的类型包含了僵化的长度信息。上面的zero函数并不能接收指向[16]byte类型数组的指针,而且也没有任何添加或删除数组元素的方法。

4.2.1. 切片操作

数组支持切片操作:a[low:high[:max]],其中low≤high≤max≤len(a),而且max部分可以省略。

数组切片的结果是一个Slice。举例:

var s = [3]int{1, 2, 3}
fmt.Printf("%T %v %d %d\n", s, s, len(s), cap(s)) // [3]int [1 2 3] 3 3

s2 := s[0:1:3]
fmt.Printf("%T %v %d %d\n", s2, s2, len(s2), cap(s2)) // []int [1] 1 3

因为数组切片的语法和原理几乎一样,可以参见Slice的切片操作。

4.2. 切片Slice

gopl-zh.github.io/ch4/ch4-02.… zhuanlan.zhihu.com/p/61121325

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。

数组和slice之间有着紧密的联系。一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。

// runtime/slice.go
type slice struct {
    array unsafe.Pointer // 元素指针
    len   int // 长度 
    cap   int // 容量
}

一个slice由三个部分构成:

  • array:指针,指向第一个slice元素对应的底层数组元素的地址。需要注意的,slice的第一个元素并不一定就是底层数组的第一个元素。
  • len:长度对应slice中元素的数目。内置的len函数可以返回slice的长度。
  • cap:容量一般是从slice的开始位置到底层数据的结尾位置。长度不能超过容量。内置的cap函数可以返回slice的容量。

使用var语句声明一个slice很简单,slice的零值是nil。

var a []int
fmt.Println(a == nil) // true

和数组很类似,声明一个slice也可以使用字面值语法,但是slice并没有指明序列的长度。这会隐式地创建一个合适大小的数组,然后slice的指针指向底层的数组。就像数组字面值一样,slice的字面值也可以按顺序指定初始化值序列,或者是通过索引和元素值指定。

var s = []int{0, 1, 2, 3, 4, 5} // [0 1 2 3 4 5]
var s2 = []int{0: 0, 5: 5}      // [0 0 0 0 0 5]

另外,内置的make函数也可以声明一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下,容量将等于长度。

make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]

在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引用底层匿名的数组变量。在第一种语句中,slice底层是整个数组。在第二个语句中,slice只引用了底层数组的前len个元素,但是容量将包含整个的数组。额外的元素是留给未来的增长用的。

常见问题:new关键字和make关键字的相同点和不同点? 首先,Go语言中的数据类型分为值类型和引用类型:值类型包括 int、float、string等,该类型变量的空间存储的是该类型的值;引用类型是 slice、map、channel和值类型对应的指针,该类型变量的空间存储的是内存中真正存储数据的首地址(即指针)。 相同点: 1)new和make都可以给变量分配内存,并进行初始化。 不同点: 1)分配对象不同:new用于值类型,包括int、float、string、bool、数组、struct等数据类型;make用于引用类型,slice、map,channel等数据类型。 2)返回类型不同:new返回指向变量的指针,make返回变量本身。 3)初始化结果不同:new分配的空间被清零,即初始化为零值;make 分配空间后,会进行初始化,比如slice的长度和底层数组。

和数组不同的是,slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。标准库提供了高度优化的bytes.Equal函数来判断两个[]byte 切片是否相等,但是对于其他类型的slice,我们必须自己对每个元素进行比较。

例如,比较两个[]string切片:

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唯一合法的比较操作是和nil比较。一个nil值的slice并没有底层数组,而且slice的长度和容量都是0。空的切片(比如[]int{}make([]int, 3)[3:])的长度和容量也是0。

与任意类型的nil值一样,我们可以用[]int(nil)表达式来生成一个对应类型slice的nil值。

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

如果你需要测试一个slice是否是空的,应该使用len(s) == 0来判断,而不是用s == nil。除了和nil相等比较外,一个nil值的slice的行为和其它任意0长度的slice一样。除了文档已经明确说明的地方,所有的Go语言函数应该以相同的方式对待nil值的slice和长度为0的slice。

传递切片作为函数参数:虽然函数参数传递机制导致传递的参数进行复制,但是切片变量本身是引用类型(指针),即使复制也是复制引用,二者引用相同的底层。所以,在函数内部,对切片参数的修改会反应在原来的切片上。

4.2.1. 切片操作

和数组一样,slice也支持切片操作:s[low:high[:max]],其中0 ≤ low≤ high≤ cap(s),而且max部分可以省略。切片操作可以创建一个新的slice,引用s的从第low个元素开始到第high-1个元素的子序列。新的slice将只有high-low个元素。如果low位置的索引被省略的话将使用0代替,如果high位置的索引被省略的话将使用len(s)代替。

举例,month声明了一个包含12个月份的数组:一月份是months[1],十二月份是months[12]。数组的第一个元素从索引0开始,但是月份一般是从1开始的,因此我们声明数组时直接跳过第0个元素。

months := [...]string{1: "January", /* ... */, 12: "December"}
all := months[1:13] // equal to months[1:] or months[:]

Q2 := months[4:7]
summer := months[6:9]
fmt.Println(Q2)     // ["April" "May" "June"]
fmt.Println(summer) // ["June" "July" "August"]

months[1:13]切片操作将引用全部有效的月份,和months[1:]操作等价;months[:]切片操作则是引用整个数组。另外,多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。例如,Q2和summer两个slice都包含了六月份,并且都是引用数组month中的数据。

如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了slice:

fmt.Println(summer[:20]) // panic: out of range

fmt.Println(len(summer), cap(summer)) // 3 3 
endlessSummer := summer[:5] // extend a slice (within capacity)
fmt.Println(endlessSummer)  // "[June July August September October]"

传递切片作为函数参数:因为slice包含指向第一个slice元素的指针,因此向函数传递slice将允许在函数内部修改底层数组的元素。换句话说,因为函数参数传递机制导致传递的参数进行复制,复制一个slice只是对底层的数组创建了一个新的slice别名。

4.2.2. append函数

内置的append函数用于向slice追加元素,并返回一个新的slice。

var runes []rune
for _, r := range "Hello, 世界" {
    runes = append(runes, r)
}
fmt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']"

在循环中使用append函数构建一个由九个rune字符构成的slice,当然对应这个特殊的问题我们可以通过Go语言内置的[]rune("Hello, 世界")转换操作完成。

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]"

append函数对于理解slice底层是如何工作的非常重要。简单来说,每次调用append函数,必须先检测slice底层数组是否有足够的容量来保存新插入的元素。

  • 如果有足够空间,就可以直接把新插入的元素添加到底层数组,扩展slice长度使其能够访问新插入的元素,并返回slice。因此,输入的切片和返回的切片仍然引用相同的底层数组。

    • 有足够空间是指,对于新插入的元素,slice的底层数组末尾有足够的空位置。
  • 如果没有足够的增长空间,则会先分配一个足够大的数组用于保存结果(扩容),先将输入的切片复制到新的数组,然后把新插入的元素添加到新的数组。因此,返回的切片和输入的切片将会引用不同的底层数组。

    • 扩容规律:当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,先计算新的容量为原来的1.25倍,然后对进行一次内存对齐,新 slice 容量稍微变化。

因为append调用可能会导致内存的重新分配,我们不能确认新的slice和原始的slice是否引用的是相同的底层数组空间。因此,通常是将append返回的结果直接赋值给输入的slice变量。另外,Go编译器不允许调用了 append 函数后不使用返回值。因此,以下的代码会编译失败。

var x []int
append(x, 1) // append(x, 1) (value of type []int) is not used

为了加深理解slice和append函数,举一个《Go学习笔记》中的例子:

func main() {
    slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    s1 := slice[2:5] // [2 3 4], len=3, cap=3
    s2 := s1[2:6:7]  // [4 5 6 7], len=4, cap=5

    s2 = append(s2, 100)
    s2 = append(s2, 200)

    s1[2] = 20

    fmt.Println(s1)    // [2 3 20]
    fmt.Println(s2)    // [4 5 6 7 100 200]
    fmt.Println(slice) // [0 1 2 3 20 5 6 7 100 9]
}

因为切片s1、s2、slice引用相同的底层数组,所以对切片的更新操作可能会影响底层数组。当切片一次扩容,比如第二次s2 = append(s2, 200),切片s2将会引用一个新的不同的数组。

4.2.3. Slice内存技巧

一个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 remove2(slice []int, i int) []int {
    slice[i] = slice[len(slice)-1]
    return slice[:len(slice)-1]
}

4.3. 字典map

字典map是一个无序的key/value对的集合,也称为哈希表。字典中所有的key都是不同的,然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value。

在Go语言中,一个map就是一个哈希表的引用,map类型可以写为map[K]V,其中K和V分别对应key和value。map中所有的key都有相同的类型,所有的value也有着相同的类型,但是key和value之间可以是不同的数据类型。其中K对应的key必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在。虽然浮点数类型也是支持相等运算符比较的,但是将浮点数用做key类型则是一个坏的想法。对于V对应的value数据类型则没有任何的限制。

使用var语句声明一个map很简单:

var m map[string]int     // nil map
fmt.Println(m == nil)    // "true" 
fmt.Println(len(m) == 0) // "true" 
m["name"] = 25           // panic: assignment to entry in nil map

var m2 = map[string]int{} // empty map
fmt.Println(len(m2) == 0) // "true" 
m2["name"] = 25
fmt.Println(m2) // map[name:25]

如果声明语句中没有初始化表达式,那么Go语言默认会使用零值nil初始化map(因为map类型的零值是nil)。map上的大部分操作,包括查找、删除、len和range循环都可以安全工作在nil值的map上,它们的行为和一个空的map类似。唯一不同的是向一个nil值的map存入元素将导致一个panic异常。

声明一个map也可以使用字面值语法或make关键字:

ages := map[string]int{
    "alice":   31,
    "charlie": 34,
}

var m = make(map[string]int)

使用map的下标语法访问(或查找)一个key将产生一个value。查找操作总是安全的:如果key在map中是存在的,那么将得到与key对应的value;如果key不存在,那么将得到value对应类型的零值。

fmt.Println(ages["alice"])  // "32"
fmt.Println(ages["alice2"]) // "0"

如果需要知道对应的元素是否真的是在map之中,可以使用访问操作的第二个返回值。在这种场景下,map的下标语法将产生两个值;第二个是一个布尔值,用于报告元素是否真的存在。布尔变量一般命名为ok,特别适合马上用于if条件判断部分。

age, ok := ages["bob"]
if !ok { /* "bob" is not a key in this map; age == 0. */ }

内置的delete函数可以用来删除map中的元素。删除操作也是安全的,即使这些元素不在map中也没有关系。

delete(ages, "alice") // remove element ages["alice"]

注意:map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作。禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。

_ = &ages["bob"] // compile error: cannot take address of map element 

range风格的for循环也可以用来遍历map。注意,map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。在实践中,遍历的顺序是随机的,每一次遍历的顺序都不相同。这是故意的,是为了强制要求程序不会依赖具体的哈希函数实现。

for name, age := range ages {
    fmt.Printf("%s\t%d\n", name, age)
}

和slice一样,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实现类似set的功能。

set := make(map[string]bool)
set["Go"] = true
set["Java"] = true

有时候需要map或set的key是slice类型,但是因为map的key必须是可比较的类型,slice并不满足这个条件。解决方案是定义一个辅助函数k,将slice转为map对应的string类型的key,确保只有x和y相等时k(x) == k(y)才成立。注意,辅助函数k(x)也不一定是字符串类型,它可以使用任何可比较的类型,例如整数、数组或结构体等。

4.3.1 同步map

同步map是sync包中的map:sync.Map,是并发安全的。TODO

4.4. 结构体struct

结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。

举例:声明一个Student类型并声明一个该类型的变量。

type Student struct {
    Id     uint64
    Name   string
    Region string
}

var s Student

结构体变量的成员可以通过点操作符访问:

fmt.Println(s.Id, s.Name) // 0, ""
s.Id = 1
s.Name = "John"
fmt.Println(s.Id, s.Name) // 1, "John"

点操作符也可以和指向结构体的指针一起工作:

var s2 *Student = &s
s2.Name += "son"
fmt.Println(s2.Id, s2.Name) // 1, "Johnson"

结构体成员的顺序也有重要的意义。我们也可以交换不同成员出现的先后顺序,那样的话就是定义了不同的结构体类型。

Go语言规定:如果结构体成员名字是以大写字母开头的,那么该成员就是导出的。一个结构体可能同时包含导出和未导出的成员。

一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。但是S类型的结构体可以包含*S指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。

举例,定义一个二叉树的结点结构体:

type tree struct {
    value       int
    left, right *tree
}

结构体类型的零值是每个成员都是零值。通常会将零值作为最合理的默认值。例如,对于bytes.Buffer类型,结构体初始值就是一个随时可用的空缓存,还有在第9章将会讲到的sync.Mutex的零值也是有效的未锁定状态。

如果结构体没有任何成员的话就是空结构体,写作struct{}。它的大小为0,也不包含任何信息,但是有时候依然是有价值的。比如,用map来模拟set数据结构时,可以用它作为map中的value类型。

seen := make(map[string]struct{}) // set of strings

注意:因为这种用法节约的空间有限,而且语法比较复杂,所以我们通常会避免这样的用法。

4.4.1 结构体字面值

声明一个结构体变量也可以用结构体字面值语法,结构体字面值可以指定每个成员的值。

type Point struct{ X, Y int }

p := Point{1, 2}
p2 := Point{X: 3, Y:4}

结构体字面值语法有两种形式:

  • 第一种是以结构体成员定义的顺序为每个结构体成员指定一个字面值,字面值的数量等于成员的数量。这种形式比较僵化,传递的字面值的顺序和数量必须严格遵守结构体的定义,一旦结构体成员有细微的调整就可能导致代码不能编译。
  • 第二种更加常用,以成员名字和相应的值来初始化,可以包含部分或全部的成员。在这种形式中,如果成员被忽略的话将默认用零值。因为提供了成员的名字,所以成员出现的顺序并不重要。

注意:

  • 两种不同形式的写法不能混合使用。
  • 无法在外部包中初始化结构体中未导出的成员。

结构体可以作为函数的参数和返回值。例如,下面的Scale函数将Point类型的值缩放后返回。如果考虑效率的话,较大的结构体通常会用指针的方式传入和返回。

func Scale(p Point, factor int) Point {
    return Point{p.X * factor, p.Y * factor}
}

func Bonus(e *Employee, percent int) int {
    return e.Salary * percent / 100
}

如果要在函数内部修改结构体成员的话,必须用指针传入:因为在Go语言中,函数参数的传入发生值拷贝,函数参数将不再是函数调用时的原始变量。函数参数返回同样会如此。

因为结构体通常通过指针处理,可以使用字面值和取地址运算符或new关键字创建并初始化一个结构体变量,并返回结构体的地址。

pp := &Point{1, 2} 

pp := new(Point)
*pp = Point{1, 2} 

不过,更推荐直接使用&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。

4.4.3. 结构体嵌入和匿名成员

Go语言提供结构体嵌入机制让一个命名的结构体包含另一个结构体类型的匿名成员。

举例,定义一个一个Circle代表的圆形类需要两个成员:点Point表示圆心和半径。

type Circle struct {
    Point
    Radius int
}

在Circle类型的定义中,我们声明一个了Point类型的成员而没有指定成员的名字:这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。

得益于匿名嵌入的特性,通过点运算符可以直接访问匿名结构体的成员:

var c Circle
c.X = 8 // equivalent to c.Point.X = 8 
c.Y = 8 // equivalent to c.Point.X = 8
c.Radius = 5

注意:匿名成员并不是真的无法访问。匿名成员Point都有自己的名字——就是命名的类型名字,但是这些名字在点操作符中是可选的。出于方便,访问子成员的时候可以忽略任何匿名成员部分。

另外,使用字面值语法声明含有匿名成员的结构体变量时会稍微麻烦:

c := Circle{Point{8, 8}, 5}

c2 := Circle{
    Point:  Point{X: 8, Y: 8},
    Radius: 5,
}

因为匿名成员也有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员,这会导致名字冲突。同时,因为成员的名字是由其类型隐式地决定的,所以匿名成员也有可见性的规则约束。

匿名成员的特性包括:

  • 外层结构体可以通过点运算符语法糖访问匿名成员的成员。匿名成员并不要求是结构体类型;其实任何命名的类型都可以作为结构体的匿名成员。
  • 外层结构体也可以通过点运算符语法访问匿名成员的导出方法。实际上,外层结构体不仅仅是获得了匿名成员类型的所有成员,而且也获得了该类型全部导出的方法。

在Go语言中,在结构体中声明一个匿名成员也被称为组合:组合是Go语言中面向对象编程的核心。Go语言使用“组合”实现了其他面向对象语言中的继承机制:如果一个结构体获得了匿名成员的全部成员和方法,那么就可以认为该结构体继承了匿名成员。

4.5. JSON

JSON(JavaScript Object Notation,JavaScript对象表示法)是一种用于发送和接收结构化信息的标准协议。在类似的协议中,JSON并不是唯一的一个标准协议。 XML、Protocol Buffers都是类似的协议,并且有各自的特色,但是由于简洁性、可读性和流行程度等原因,JSON是应用最广泛的一个。

Go语言对于这些标准格式的编码和解码都有良好的支持,由标准库中的encoding/json、encoding/xml、encoding/asn1等包提供支持(译注:Protocol Buffers的支持由 github.com/golang/prot… 包提供),并且这类包都有着相似的API接口。

JSON是对JavaScript中各种类型的值——字符串、数字、布尔值和对象——Unicode本文编码。它可以用有效可读的方式表示int、string等基础数据类型和数组、slice、结构体和map等聚合数据类型。

基本的JSON类型有数字(十进制或科学记数法)、布尔值(true或false)、字符串,其中字符串是以双引号包含的Unicode字符序列,支持和Go语言类似的反斜杠转义特性,不过JSON使用的是\Uhhhh转义数字来表示一个UTF-16编码,而不是Go语言的rune类。

这些基础类型可以通过JSON的数组和对象类型进行递归组合。一个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"]}

举例,定义一个Movie类型:

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语言中的数据结构编码为JSON对象, 在Go语言中一般叫做marshal。编码操作可以使用json.Marshal函数:该函数返回一个编码后的字节slice,包含很长的字符串,并且没有空白缩进(换行以便于显示)。

例如:

data, err := json.Marshal(movies)
if err != nil {
    log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)

// output
[{"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函数可以产生有缩进的输出。该函数有两个额外的字符串参数用于表示每一行输出的前缀和每一个层级的缩进:

在编码结构体时,Go语言默认使用结构体成员的名字作为JSON的对象(通过reflect反射技术)。注意,只有导出的结构体成员才会被编码,这也是为什么我们选择用大写字母开头的名称的原因。

另外,在编码后,Year成员的名字变成了released,还有Color成员的名字变成了color。这是因为Movie结构体使用了成员Tag:

Year  int  `json:"released"`
Color bool `json:"color,omitempty"`

说明:

  • 结构体成员Tag是用于描述结构成员的元信息字符串,可以在编译阶段关联到对应的成员。

  • 结构体成员Tag可以是任意的字符串面值,但是通常是一系列用空格分隔key:"values"键值对序列,其中values以逗号分隔;因为值中含有双引号字符,因此成员Tag用原生字符串面值的形式书写。

  • 不同的key可以定义不同的行为。比如,json键对应的值用于控制encoding/json包的编码和解码的行为。

    • json:"released":json键对应的值的第一部分用于指定JSON对象的名字,比如Movie结构体中的Year成员对应到JSON中的released对象。
    • json:"color,omitempty":omitempty的作用是如果成员为空或零值,就不生成对应的JSON对象。
    • json:"-":忽略该成员,不生成JSON对象。

编码的逆操作是解码,对应将JSON数据解码为Go语言中的数据结构,在Go语言中一般叫unmarshal。解码操作通过json.Unmarshal函数完成,该函数需要一个字节slice和一个变量地址,用于接收解码后的数据。

例如:

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}]"

JSON已经被广泛用于web中:许多web服务都提供JSON接口,通过HTTP接口发送JSON格式请求并返回JSON格式的信息。

对于流式数据,Go语言提供了基于流式的解码器json.Decoder,可以从一个输入流解码JSON数据;对应的编码器是json.Encoder,可以从一个输出流编码JSON数据。