Go 语言基础学习

学习自菜鸟教程/编码自Go指南/更全的教程

简介

分类

  • 静态强类型、编译型、并发型

特色

  • 简洁、快速、安全
  • 并行、有趣、开源
  • 内存管理、数组安全、编译迅速

用途

  • 搭载 Web 服务器
  • 存储集群
  • 巨型中央服务器的系统

写代码

一个例子

1
2
3
4
5
6
7
8
package main // 定义包名,main包表示一个可独立执行的程序,每个Go应用都包含一个名为main的包

import "fmt" // 引入包

func main() { // main函数是所有可执行程序必须包含的,此处{不可换行否则编译错误
/* 这是我的第一个简单的程序 */
fmt.Println("Hello, World!")
}
  • 行分割,每个语句是一行,无需使用;分割

变量

与其他语言有相同之处,也有不同之处

  • 基本类型:intboolfloat32float64string(字符串的字节使用 UTF-8 编码标识 Unicode 文本)
  • 衍生类型

    • 指针类型(Pointer)

    • 数组类型

      • 比较
      1
      2
      3
      4
      5
      6
      7
      8
      slice1 := []int{1,2,3}
      slice2 := []int{1,2,3,4}
      slice3 := []int{1,2,3}
      slice4 := []int{1,3,2}

      fmt.Println(slice1 == slice2) // 编译错误,长度不相等,无法比较
      fmt.Println(slice1 == slice3) // true
      fmt.Println(slice1 == slice4) // false
  • 结构化类型(struct)

  • Channel 类型

  • 函数类型

  • 切片类型

  • 接口类型(interface)

  • Map 类型

    • 单变量声明方法
  • var agePtr *int 没有初始化则默认为零值
  • var d = true 赋初值省略变脸类型
  • f := "Runoob" 省略var和变量类型,只能在函数中出现,且只能用于声明
  • 多变量声明

    1
    2
    3
    4
    5
    var x, y int
    var ( // 这种因式分解关键字的写法一般用于声明全局变量
    a int
    b bool
    )
  • 所有声明的变量必须被使用,否则会编译失败,因此需要声明但是不会用到的变量可使用_空白标识符占位

  • 相同类型变量的交换赋值可以使用a, b = b, a

  • 常量声明。其中iota为特殊常量,可以认为是一个可以被编译器修改的常量,其值是常量所在的行index

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const (
    d = 4 // 4
    e // 未赋值重复上一个常量赋值,4
    )
    const (
    a = 2 // 2
    b = iota //1
    c // 2
    d = iota * iota // 3 * 3 = 9
    e // 4 * 4 = 16
    )

运算

  • 常规运算:
    • 算数运算+ - * / % ++ --
    • 关系运算> < == != >= <=
    • 逻辑运算&& || !
    • 位运算& | ^ >> << &^(按位清零)
    • 赋值运算=(以及运算后赋值+=等等)
    • 其他运算& *

条件语句

  • ifif-else
  • switch-case
    • 不用类似break语法进行单case控制,可以使用fallthrough使得执行当前case后强行进入下一个case且不用判断下一个case是否为真
    • 可以使用switch …{ case …: }的写法也可以使用switch { case…: }判定”true case”的写法
  • select-case
    • 每个 case 都必须是一个通信
    • 所有 channel 表达式都会被求值
    • 所有被发送的表达式都会被求值
    • 如果任意某个通信可以进行,它就执行,其他被忽略。
    • 如果有多个 case 都可以运行,Select 会随机公平地选出一个执行。其他不会执行。 否则:
      • 如果有 default 子句,则执行该语句。
      • 如果没有 default 子句,select 将阻塞,直到某个通信可以运行;Go 不会重新对 channel 或值进行求值。
    • 结合for循环和goroutine可以完成并行
  • 不支持 ? : 形式的条件判断

循环语句

  • 表示方法

    • for init; condition; post { }
    • for condition { } //等价于C语言中的while

    • for { }// while true

  • or 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环

    1
    2
    3
    for key, value := range oldMap {
    newMap[key] = value
    }

函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func function_name( [parameter list] ) [return_types] {
// 函数体
} // 函数定义
/* ==================================================== */
func () {
// 函数体
}() // 匿名函数直接调用
/* ==================================================== */
myFunc := func () {
// 函数体
} // 声明函数变量
myFunc() // 调用
/* ==================================================== */
func swap(x, y string) (string, string) {
return y, x
} // 返回多值的函数
/* ==================================================== */
/* 闭包函数 */
func getSequence() func() int {
i:=0
return func() int {
i+=1
return i
}
}
nextNumber := getSequence()
fmt.Println(nextNumber()) // 1
fmt.Println(nextNumber()) // 2
fmt.Println(nextNumber()) // 3

nextNumber1 := getSequence()
fmt.Println(nextNumber1()) // 1
fmt.Println(nextNumber1()) // 2
/* ==================================================== */

变量作用域

  • 函数内定义的变量称为局部变量
  • 函数外定义的变量称为全局变量
  • 函数定义中的变量称为形式参数

指针

  • 声明方法 var ip *int
  • 访问指针值 fmt.Printf("*ip 变量的值: %d**\n**", *ip )
  • 指针不赋初值默认为nil空指针

结构体

  • 声明方法

    1
    2
    3
    4
    5
    6
    type Books struct {
    title string
    author string
    subject string
    book_id int
    }
  • 定义结构体

    1
    2
    3
    4
    5
    var Book1 Books   // 定义空结构体

    fmt.Println(Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407}) // 按顺序定义变量值

    fmt.Println(Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", book_id: 6495407}) // 指定成员值
  • 访问成员

    • Book1.title = "some title"
    • fmt.Println(Book1.title)

切片

切片是对数组的抽象,可以理解为动态数组,可以追加元素,在追加时可能使切片的容量增大

  • 声明方法

    1
    2
    3
    var slice1 = []int{1,2,3} // 不指定数组大小声明的数组会被认为是切片
    slice := make([]int, 2, 3) //声明一个容量为3,当前内含2个元素的切片
    var slice2 = []int // slice2 == nil
  • 初始化方法

    1
    slice2 := slice1[1:] // 通过slice1的引用初始化slice2
  • append 方法

    1
    2
    3
    4
    5
    6
    7
    8
    slice3 := make([]int, 2, 5)
    fmt.Println(slice3) // [0, 0]
    slice3 = append(slice3, 1)
    slice3 = append(slice3, 1)
    slice3 = append(slice3, 1)
    slice3 = append(slice3, 1) // 自动扩容到10
    fmt.Println(slice3) // [0,0,1,1,1,1]
    fmt.Println(cap(slice3)) // 10
  • copy 方法

    1
    2
    3
    4
    5
    slice3 := make([]int, 2, 5)
    slice4 := make([]int, len(slice3), cap(slice3))
    copy(slice4, slice3) // 可以认为是for循环两个slice中len最小的值进行赋值
    slice4[0] = 1233
    fmt.Println(slice3, slice4)// [0,0] [1233,0]
    • copy 方法使用两者长度较小的作为复制目标的长度
    • 方法返回复制目标长度,即二者长度最小值

Range

  • 返回一个有序对key, value用于遍历数组/切片/通道/集合,和pythonenumerate方法类似

  • 例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    nums := []int{2, 3, 4}
    sum := 0
    for _, num := range nums {
    sum += num
    }

    kvs := map[string]string{"a": "apple", "b": "banana"}
    for k, v := range kvs {
    fmt.Printf("%s -> %s\n", k, v)
    }

Map

  • 无序键值对的集合,使用哈希表来实现
  • 声明方式
    • var map1 map[string]sttring // 不赋初值声明
    • map2 := make(map[string]string) // 不赋初值声明
    • map3 := map[string]string{"a": "apple", "b": "banana"} // 赋初值声明
  • 赋值方式与python相同
  • 可以使用delete()函数删除字典中的键值对

强制类型转换

  • 使用类似float32(sum)的方法对var sum int = 2进行强制类型转换

接口

  • 通过定义接口,然后直接实现接口中声明的函数,进行多态实现

  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    type phone interface {
    getname() string
    call()
    }

    type Iphone struct {

    }

    type Nokia struct {

    }

    func (iphone Iphone) getname() string {
    return "I am iphone"
    }

    func (nokia Nokia) getname() string {
    return "I am nokia"
    }

    func (iphone Iphone) call() {
    fmt.Println("call with iphone")
    }


    func (nokia Nokia) call() {
    fmt.Println("call with nokia")
    }

    /* ===============in main() ====================== */
    phone := new(Iphone)
    fmt.Println(phone.getname()) //I am iphone
    phone.call()// call with iphone

    anotherPhone := new(Nokia)
    fmt.Println(anotherPhone.getname()) // I am nokia
    anotherPhone.call() // call with nokia

异常捕获

  • 通过实现error.Error()函数来实现异常捕获

  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    type DivideError struct {
    dividee int
    divider int
    }

    func (divideError *DivideError) Error() string {
    strFormat := `
    Cannot proceed, the divider is zero.
    dividee: %d
    divider: 0
    `
    return fmt.Sprintf(strFormat, divideError.dividee)
    }

    func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
    if varDivider == 0 {
    dData := DivideError{
    dividee: varDividee,
    divider: varDivider,
    }
    errorMsg = dData.Error()
    return
    } else {
    return varDividee / varDivider, ""
    }

    }

    /* ===============in main() ====================== */
    for divider := range []int{0, 1} {
    if result, errorMsg := Divide(100, divider); errorMsg == "" {
    fmt.Println(divider, result)
    } else {
    fmt.Println("errorMsg is: ", errorMsg)
    }
    }
    /* output:
    errorMsg is:
    Cannot proceed, the divider is zero.
    dividee: 100
    divider: 0
    1 100
    */

Go 并发

goroutine
  • Go 使用 goroutine实现并发。goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。

  • 开启goroutine的方法:go fun(x, y, z)

  • Goroutine 会随着主线程的结束而自动销毁
channel
  • 通道(channel)是用来传递数据的一个数据结构

  • 使用方法

    1
    2
    3
    4
    ch := make(chan int) // 声明信道
    ch <- v // 把 v 发送到通道 ch
    v := <-ch // 从 ch 接收数据
    // 并把值赋给 v
  • 通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小 ch := make(chan int, 10)

  • 通道遍历可使用range语法

实战

摘自公众号 Golang来啦

1. 下面代码能否正常结束

1
2
3
4
5
6
func main() {
v := []int{1, 2, 3}
for i := range v {
v = append(v, i)
}
}

对于range的循环次数,在最开始就已经确定,不会因为序列的变化而改变

2. 下面的代码输出是什么

1
2
3
4
5
6
7
8
9
10
11
12
func main() {

var m = [...]int{1, 2, 3}

for i, v := range m {
go func() {
fmt.Println(i, v)
}()
}

time.Sleep(time.Second * 3)
}
  • 由于range 函数的返回会复用iv 而不是重新声明,因此在println 函数输出前可能可以是循环过程中的任何值。为保证唯一,可以使用临时变量或者传参的形式保证传入参数的唯一性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
func change(s ...int) {
s = append(s,3)
}

func main() {
slice := make([]int,5,5)
slice[0] = 1
slice[1] = 2
change(slice...)
fmt.Println(slice)
change(slice[0:2]...)
fmt.Println(slice)
}

/* output:
[1 2 0 0 0]
[1 2 3 0 0]
**/
===========================
func main() {
var a = []int{1, 2, 3, 4, 5}
var r [5]int

for i, v := range a {
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
}
fmt.Println("r = ", r)
fmt.Println("a = ", a)
}

/* output:
r = [1 12 13 4 5]
a = [1 12 13 4 5]
**/
  • 切片的底层是一个结构体,包含切片长度、容量和一个数组指针。对切片进行拷贝[i:j]截取操作时,底层的数组指针不会改变,仍指向同一数组。仅当在append 操作使得len > cap 时才会重新创建新的切片。因此对于副本的所有操作均会应用到原切片
  • 注意区分切片的声明和数组的声明

3. defer 和 recover

defer
  • 注册延迟调用的机制

  • 把函数压入栈中,当defer的上层函数返回(包括正常返回和异常返回)后再将栈内函数弹出执行

  • 由于是在真正返回之前进行弹栈,因此就可以通过诸如如下形式,对返回的命名参数在真正返回之前进行操作

    1
    2
    3
    4
    5
    6
    7
    func fn()(r T) {
    // init t
    defer func() {
    // do something change r to change the return value which is t before
    }
    return t
    }
  • 拆解:将函数的返回命名为声明中的返回查看结果。对于匿名返回的参数,可将返回值赋给一个命名变量用来拆解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 第一个例子:

    func f() (r int) {
    t := 5
    defer func() {
    t = t + 5
    }()
    return t
    }
    // 拆解后:
    func f() (r int) {
    t := 5

    // 1. 赋值指令
    r = t

    // 2. defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
    func() {
    t = t + 5
    }()

    // 3. 空的return指令
    return
    }
  • 综上所述,

    • 对于命名返回参数的函数使用拆解的方法看
    • 对于函数式的方法应该注意是传参调用还是闭包调用,传参使用的是复制的当时的值,闭包则使用的是变量的引用
    • defer延时调用的方法,可以在参数出现时定义其最终关闭的方法,防止后续赋值导致其失效的问题
    • defer调用失败,会返回panic
    recover
  • 如果程序出现致命错误,触发panic ,此时当前正在执行程序会被直接停掉,而不只是协程。为了防止程序直接挂掉,需要在defer中使用recover对程序进行会恢复,防止程序完全挂掉

  • recover 只有在defer 的上下文中才有效
  • 通过recover 捕获panic 稳住主流程,以免影响其他协程的正常运行
  • recover函数和panic可以认为是弹栈和压栈操作,panic压栈interface,对于生效的recover再弹栈inteface

4. 闭包

  • 闭包 = 函数 + 引用环境
  • 匿名函数被称作闭包,只可直接调用或者赋值于某个变量
  • 闭包捕获的变量和常量是引用传递
  • 感性的理解上可以将闭包认为是声明了一个类,然后捕获的变量成为了类的成员变量

5. golang中... 的4处用法

  • 函数中的最后一个参数,形如func(n ...T)表示函数在此参数(可变参数)后可接受若干个本类型的参数
  • 在向可变参数函数传递参数时使用func(nums...)方式传入参数
  • 在不定个数数组声明时,形如[...]string{"Moe", "Larry", "Curly"}方式,使编译器自动判定数组长度
  • 在go命令中使用go test ./...的方式递归通配所有包文件

6. 下面代码输出什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func main() {

var m = map[string]int{
"A": 21,
"B": 22,
"C": 23,
}
counter := 0
for k, v := range m {
if counter == 0 {
delete(m, "A")
}
counter++
fmt.Println(k, v)
}
fmt.Println("counter is ", counter)
}

/* output
A 21
B 22
C 23
counter is 3
或者
B 22
C 23
counter is 2
**/
  • Map 的遍历是无序的,因此若A在第一次循环,则可循环3次,若A不在第一次循环,则循环两次

7. break 的高级用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
/* local variable definition */
a := 10

/* for loop execution */
I:
for {
for {
a -= 1
fmt.Println(a)
if a < 0 {
break I // 使得一次性退出两层循环
}
}
}
}

8. 下面代码输出什么

1
2
3
4
5
6
7
8
9
func main() {
i := 1
s := []string{"A", "B", "C"}
i, s[i-1] = 2, "Z"
fmt.Printf("s: %v \n", s)
}
/* output
s: [Z,B,C]
**/
  • 赋值顺序:先计算等号左边表达式取值,再考虑等号右边的赋值

9.下面代码输出什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ts := [2]X{}
for i, t := range ts[:] {
switch i {
case 0:
t.n = 3
ts[1].n = 9
case 1:
fmt.Println(ts[i].n, " ")
}
}

fmt.Println(ts) // [{0} {9}]


ts := [2]X{}
for i := range ts[:] {
switch i {
case 0:
ts[1].n = 9
case 1:
fmt.Println(ts[i].n, " ")
}
}

fmt.Println(ts) // [{0} {9}]
  • 切片操作生成新的切片,使用range返回参数循环变量均为变量副本,对其操作不会影响原切片
  • 切片操作生成新的切片,但是共享底层数组,因此直接使用下标操作会同时生效

10. 下面代码有什么问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 错误代码
func main() {
f, err := os.Open("file")
defer f.Close()// 此处f可能为nil,导致defer中再次出现panic
if err != nil {
return
}

b, err := ioutil.ReadAll(f)
println(string(b))
}


//修正代码
func main() {
f, err := os.Open("file")
if err != nil {
return
}
defer f.Close()

b, err := ioutil.ReadAll(f)
println(string(b))
}
  • 在函数使用场景下,应注意先判断error只有error != nil时,可认为返回值有效,然后进一步处理

11. 下面代码有什么问题

1
2
3
4
5
6
7
8
9
10
11
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
fmt.Println("1")
wg.Done()
wg.Add(1)
}()
wg.Wait() // panic here
}
// panic: sync: WaitGroup is reused before previous Wait has returned
  • 调用Done()后立刻调用Add(1)导致Wait()函数执行时,发现wg变量仍在被使用,误以为自己错误进入了Wait()函数而panic
  • 在使用sync.WaitGroup时,Add(n)就要配对n个Done()调用,否则会出现死锁

12. 下面代码输出什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Slice []int

func NewSlice() Slice {
return make(Slice, 0)
}
func (s *Slice) Add(elem int) *Slice {
*s = append(*s, elem)
fmt.Print(elem)
return s
}
func main() {
s := NewSlice()
defer s.Add(1).Add(2)
s.Add(3)
}

// output: 132
  • defer 只会执行一级函数,对于循环调用,会提前计算到倒数第二级函数结果,仅在defer中执行最后一次调用

13. 下面代码输出什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func alwaysFalse() bool {
return false
}
func main() {}
switch alwaysFalse()
{
case false:
fmt.Println("false")

case true:
fmt.Println("true")

}
}
  • 首先注意到,第六行大括号独占一行。因此可以参考golang 代码断行规则

  • golang 在编译阶段,会在行尾是一下情况时,在其后插入分号

    • 标识符

    • 整数、浮点数、虚部、码点、字符串字面表示形式

      字面表示形式可以理解为变量在代码中的表现,例如值为15但字面表示形式可以是15、0xF、0b1111等多种字面表示形式

    • break、continue、fallthrough、return

    • ++、–
    • )、}、]
    • 为了允许复杂语句完全显示在一个代码行中,分号可能被插在一个右小括号)或者右花括号}之前
  • 对于上述情况外的其他情况,分号需要自行插入

  • 对于可能隐式插入分号的断句可以使用合法的,规避隐式分号的插入

  • 在上述代码中,alwaysFalse函数只是被执行,但是没有被变量接收,等价于如下代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    func alwaysFalse() bool {
    return false
    }
    func main() {}
    switch _ = alwaysFalse();{
    case false:
    fmt.Println("false")

    case true:
    fmt.Println("true")

    }
    }

    上述代码直接进入case true,若希望实现效果,应该改为如下形式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    func alwaysFalse() bool {
    return false
    }
    func main() {}
    switch tmp := alwaysFalse()
    tmp {
    case false:
    fmt.Println("false")
    case true:
    fmt.Println("true")

    }
    }

    仍然是个很奇怪的写法,使得代码可读性大大降低,非常不推荐

  • 可以使用go fmtgo vet命令进行代码格式化,增加代码规范性,发现可能的逻辑错误

14. 下面代码可以编译通过么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func f(x int) {
switch x {
case 1:
{
goto A
A:
}
case 2:
goto B
B: // 编译失败,tag后缺少语句
case 0:
goto C
C:
}
}
  • 编译器会在7行和14行花括号前加入;使得tag后生成一个由;单独构成的空语句,从而使得AC两个tag合法
  • 可在13行后加入;使得语法编译通过

15. 下面代码输出什么

1
2
3
4
func main() {
fmt.Println(strings.TrimRight("ABBA", "BA")) // ""
fmt.Println(strings.TrimRight("ABDBABBBA", "BA")) // "ABD"
}
  • 输出空字符串,TrimRight会将第二字符串中出现的全部字符从第一字符串右边开始匹配直到匹配不上为止
  • TrimSuffix可以完成去除后缀的效果

16. 下面代码输出什么

1
2
3
4
5
6
7
8
9
var x int
func init() {
x++
}

func main() {
init()
fmt.Println(x)
}
  • 编译失败。init函数不能被调用
  • main()函数不可带参数且不能有返回

17. 下面代码输出什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type User struct {
Name string
}

type Employee User

func (u *User)SetName(name string) {
fmt.Println(name)
u.Name = name
}

func main() {
e := new(Employee)
e.SetName()
}
  • 编译失败,SetName为User实现的函数,Employee是重新定义的类,需要重新实现SetName实例才可调用
  • 修改方法
    1. 重写func (e *Employee)SetName(name string)函数
    2. User作为Employee的匿名变量,隐式调用UserSetName函数
    3. 同时使用1和2修改,导致两个SetName的情况时,调用UserSetName需要显式调用

18. 下面代码输出什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"encoding/json"
"fmt"
)

type AutoGenerated struct {
Age int `json:"age"`
Name string `json:"name"`
Child []int `json:"child"`
}

func main() {
jsonStr1 := `{"age": 14,"name": "potter", "child":[1,2,3]}`
a := AutoGenerated{}
json.Unmarshal([]byte(jsonStr1), &a)
aa := a.Child
fmt.Println(aa)
jsonStr2 := `{"age": 12,"name": "potter", "child":[3,4,5,7,8,9]}`
json.Unmarshal([]byte(jsonStr2), &a)
fmt.Println(aa)
}
  • 答案:[1,2,3][3,4,5]

  • 解析:

    • 知识点1:golang json库Unmarshal数组类型的逻辑。针对slice类型,在反序列化时,json库会首先将数组的len置0,然后将数据逐个append进数组。
    • 知识点2:json库对未初始化slice的结构体字段,会初始化一个大小为4的slice,扩容逻辑为newcap := v.Cap() + v.Cap()/2
    • 题目解读:因此,当第一次unmarshal时,aa底层数组指向容量为4的数组。第二次unmarshal时,a结构体中的Child字段,发生一次扩容,放弃原有底层数组,复制数据后,重新指向一个容量为6的底层数组。因此,aa底层数组会保存有第二次反序列化时的前四个元素,同时a的Child字段slice地址也和aa不同。(此处的扩容逻辑是json库的扩容逻辑,需要注意与slice本身的appen过程<翻倍>扩容逻辑区分)
  • 拓展:json库反序列化过程(翻译自Unmarsha源码注释)

    • 接口:json.Unmarshal([]byte(jsonStr1), &a),必须传入一个指针,否则抛出InvalidUnmarshalError异常

    • Unmarshal与Marshal是互逆的操作,在有必要时,会申请map、slice和指针,并且还有如下附加规则:

      • 对于所有反序列化为指针的json字段,如果json中为null字符则设置为nil指针;否则将结构体中指针指向对应值,如果结构体中对应字段为nil,则重新分配一个新的指针来指向对应值

      • 对于实现了Unmarshaler接口的字段,Unmarshal方法会先调用该字段的UnmarshalJSON方法,即便json字符串中对应的值是null;否则,如果该字段实现了encoding.TextUnmarshaler接口并且输入也是一个带引号的字符串,Unmarshal方法会调用该字段的UnmarshalText方法,并将该值的去引号部分作为参数传入

      • 如果要反序列化为一个结构体,Unmarshal会通过用于marshal的key来匹配(字段名称或字段的tag)优先使用精确匹配,同时也可以接受大小写不敏感的匹配情况。默认情况下,没有对应结构体字段的json key会被忽略,可通过 Decoder.DisallowUnknownFields更改配置

      • 如果json字段对应的结构体字段是个interface类型,那么只会将对应字段值根据如下对应关系进行反序列化:

        | go类型 | json类型 |
        | ———————- | ——– |
        | bool | boolean |
        | float64 | number |
        | string | string |
        | []interface{} | array |
        | map[string]interface{} | object |
        | nil | null |

      • 当反序列化一个数组为slice时,Unmarshal方法会先设置其长度为0,然后再逐个元素append到slice中;有一种特殊情况,即当json字符串为一个空数组时,Unmarshal方法会将原有slice替换为一个空slice

      • 当反序列化json数组到go数组时,如果go数组长度和json数组长度相同,直接赋值为对应go数组;若json数组长度较短,剩余go数组位使用0补齐;若json数组长度较长,则忽视多余部分

      • 反序列化map时,会先建立一个map:如果go结构体的原map是nil,会分配一个新的map;不然会重用已有的map,写入新的k-v,同时保留已有的map键。map的键必须是一下类型之一:字符串、数字、实现了json.Unmarshaler或者实现了encoding.TextUnmashaler接口的结构体

      • 如果json值和其类型不匹配,或者数值溢出,Unmarshal会跳过这个字段,并且会尽可能多的将合法的字段反序列化。如果没有更严重的错误发生的话,Unmarshal会返回UnmarshalTypeError说明此次错误。在任何情况下,在错误发生时,都不能确保其余字段能够正常的反序列化。

      • json的null或未设置的字段会反序列化为nil,并且不会抛出任何异常

      • 如果反序列化带引号的字符串,无效的UTF-8或UTF-16字符不会被视为错误,而是会被替换为Unicode字符U+FFFD

0%