GoLang 学习笔记(二)《Go语言实战》

第1-3章

编译 Go 程序时,编译器只会关注那些直接被引用的库,而不是像 Java、C 和 C++那样,要遍历 依赖链中所有依赖的库。

Go 语言对并发的支持是这门语言最重要的特性之一。goroutine 很像线程,但是它占用的 内存远少于线程,使用它需要的代码更少。通道(channel)是一种内置的数据结构,可以让 用户在不同的 goroutine 之间同步发送具有类型的消息。

goroutine

goroutine 是可以与其他 goroutine 并行执行的函数,同时也会与主程序(程序的入口)并行 执行。

通道

通道是一种数据结构,可以让 goroutine 之间进行安全的数据通信。通道可以帮用户避免其 他语言里常见的共享内存访问的问题。

在两个 goroutine 间传输数据是同步的,一旦传输完成,两个 goroutine都会知道数据已经完成传输。

通道并不提供跨 goroutine 的数据访问保护机制。如果通过通道传输数据的一份副本,那么每个 goroutine 都持有一份副本,各自对自己的副本做修改是安全的。当传输的是指向数据的指针时,如果读和写是由不同的 goroutine 完成的,每个 goroutine 依旧需要额外的同步动作。

类型系统

Go 开发者使用组合(composition)设计模式,只需简单地将一个类型嵌入到另一个类型,就能 复用所有的功能。

Go 语言还具有独特的接口实现机制,允许用户对行为进行建模,而不是对类型进行建模。

init()函数会在main()函数之前调用。

一个包定义一组编译过的代码,包的名字类似命名空间,可以用来间接访问包内声明的标识符。这个特性可以把不同包中定义的同名标识符区别开。

导入的路径前面有一个下划线,例如

_ "github.com/goinaction/code/chapter2/sample/matchers"
复制代码

这个技术是为了让 Go 语言对包做初始化操作,但是并不使用包里的标识符。为了让程序的 可读性更强,Go 编译器不允许声明导入某个包却不使用。下划线让编译器接受这类导入,并且 调用对应包内的所有代码文件里定义的 init 函数。

变量没有定义在任何函数作用域内,所以会被当成包级变量。

在 Go 语言里,标识符要么从包里公开,要么不从包里公开。当代码导入了一个包时,程序 可以直接访问这个包中任意一个公开的标识符。这些标识符以大写字母开头。以小写字母开头的 标识符是不公开的,不能被其他包中的代码直接访问。

在 Go 语言中,所有变量都被初始化为其零值。对于数值类型,零值是 0;对于字符串类型, 零值是空字符串;对于布尔类型,零值是 false;对于指针,零值是 nil。对于引用类型来说, 所引用的底层数据结构会被初始化为对应的零值。但是被声明为其零值的引用类型的变量,会返 回 nil 作为其值。

Go 语言使用关键字 func 声明函数,关键字后面紧跟着函数名、参数以及返回值

切片是一种实现了一个动态数组的引用类型。在 Go 语言里可以用切片来操作一组数据。

简化变量声明运算符(:=)。这个运算符用于声明一个变量,同时给这个变量赋予初始值。编译器使用函数返回值的类型来确定每个变量的类型。简化变量声明运算符只是一种简化记法,让代码可读性更高。这个运算符声明的变量和其他使用关键字 var 声明的变量没有任何区别。

如果需要声明初始值为零值的变量,应该使用var关键字声明变量;如果提供确切的非零值初始化变量或者使用函数返回值创建变量,应该使用简化变量声明运算符。

sync 包的 WaitGroup 跟踪所有启动的 goroutine。实际开发中,非常推荐使用 WaitGroup 来 跟踪 goroutine 的工作是否完成。WaitGroup 是一个计数信号量,我们可以利用它来统计所有的 goroutine 是不是都完成了工作。

关键字 range 可以用于迭代数组、字符串、切片、映射和通道。使用for range迭代切片时,每次迭代会返回两个值。第一个值是迭代的元素在切片里的索引位置,第二个值是元素值的一个副本。

如果要调用的函数返回多个值,而又不需要其中的某个值,就可以使用下划线标识符将其忽略。

一个 goroutine 是一个独立于其他函数运行的函数。使用关键字 go 启动一个 goroutine,并对这个 goroutine 做并发调度。

指针变量可以方便地在函数之间共享数据。使用指针变量可以让函数访问并修改一个变量的状态,而这个变量可以在其他函数甚至是其他goroutine的作用域里声明。指针变量的值是所指向的内存地址,在函数间传递指针变量,是在传递这个地址值。

关键字 defer 会安排随后的函数调用在函数返回时才执行。在使用完文件后,需要主动关 闭文件。使用关键字 defer 来安排调用 Close 方法,可以保证这个函数一定会被调用。哪怕函 数意外崩溃终止,也能保证关键字 defer 安排调用的函数会被执行。关键字 defer 可以缩短打 开文件和关闭文件之间间隔的代码行数,有助提高代码可读性,减少错误。

一个接口的行为最终由在这个接口类型中声明的方法决定。

命名接口的时候,也需要遵守 Go 语言的命名惯例。如果接口类型只包含一个方法,那么这 个类型的名字以 er 结尾。我们的例子里就是这么做的,所以这个接口的名字叫作 Matcher。如 果接口类型内部声明了多个方法,其名字需要与其行为关联。

type Matcher interface {
    Search(feed *Feed, searchTerm string) ([]*Result, error)
}
复制代码

如果要让一个用户定义的类型实现一个接口,这个用户定义的类型要实现接口类型里声明的 所有方法。

type defaultMatcher struct{}
复制代码

上面的代码使用一个空结构声明了一个名叫 defaultMatcher 的结构类型。空结构在创建实例时,不会分配任何内存。这种结构很适合创建没有任何状态的类型。

所有的.go 文件,除了空行和注释,都应该在第一行声明自己所属的包。每个包都在一个单独的目录里。不能把多个包放到同一个目录中,也不能把同一个包的文件分拆到多个不同目录中。这意味着,同一个目录下的所有.go 文件必须声明同一个包名。

标准库中的包会在安装 Go 的位置找到。Go 开发者创建的包会在 GOPATH 环境变量指定的目录里查找。 GOPATH 指定的这些目录就是开发者的个人工作空间。编译器会首先查找Go的安装目录,然后才会按顺序查找 GOPATH 变量里列出的目录。

远程导入

用导入路径编译程序时,go build 命令会使用GOPATH 的设置,在磁盘上搜索这个包。事实上, 这个导入路径代表一个 URL,指向 GitHub 上的代码库。如果路径包含 URL,可以使用 Go 工具链从 DVCS 获取包,并把包的源代码保存在 GOPATH 指向的路径里与 URL 匹配的目录里。这个获取过程 使用 go get 命令完成。go get 将获取任意指定的 URL 的包,或者一个已经导入的包所依赖的其 他包。由于go get 的这种递归特性,这个命令会扫描某个包的源码树,获取能找到的所有依赖包。

命名导入

用于解决包名相同的问题。 命名导入是指,在 import 语句给出的包路径的左侧定义一个名字,将导入的包命名为新名字。 例如,若用户已经使用了标准库里的 fmt 包,现在要导入自己项目里名叫 fmt 的包,可参照如下代码:

import (
    "fmt"
    myfmt "mylib/fmt"
)
复制代码
空白标识符

下划线字符(_)在 Go 语言里称为空白标识符,有很多用法。这个标识符用来抛弃不 想继续使用的值,如给导入的包赋予一个空名字,或者忽略函数返回的你不感兴趣的值。

函数init

每个包可以包含任意多个 init 函数,这些函数都会在程序执行开始的时候被调用。所有被 编译器发现的 init 函数都会安排在 main 函数之前执行。init 函数用在设置包、初始化变量 或者其他要在程序运行前优先完成的引导工作。

运行代码通过 go run 命令。

go vet 该 命令会帮开发人员检测代码的常见错误,例如:

  1. Printf 类函数调用时,类型匹配错误的参数。
  2. 定义常用的方法时,方法签名的错误。
  3. 错误的结构标签。
  4. 没有指定字段名的结构字面量。

这个工具可以很好地捕获一部分常见错误。每次对代码先执行 go vet 再将其签入源代码库是一个很好的习惯。

Go 代码格式化 通过fmt命令。

go doc 可以查看相关包下的文档,例如go doc http。

浏览文档 开发人员启动自己的文档服务器,只需要在终端会话中输入如下命令: godoc -http=:6060 这个命令通知 godoc 在端口 6060 启动 Web 服务器。如果浏览器已经打开,导航到 http://localhost:6060 可以看到一个页面,包含所有 Go 标准库和你的GOPATH 下的 Go 源代码的文档。

  1. 在 Go 语言中包是组织代码的基本单位。
  2. 环境变量 GOPATH 决定了 Go 源代码在磁盘上被保存、编译和安装的位置。
  3. 可以为每个工程设置不同的 GOPATH,以保持源代码和依赖的隔离。
  4. go 工具是在命令行上工作的最好工具。
  5. 开发人员可以使用 go get 来获取别人的包并将其安装到自己的 GOPATH 指定的目录。
  6. 想要为别人创建包很简单,只要把源代码放到公用代码库,并遵守一些简单规则就可以了。
  7. Go 语言在设计时将分享代码作为语言的核心特性和驱动力。
  8. 推荐使用依赖管理工具来管理依赖。
  9. 有很多社区开发的依赖管理工具,如 godep、vender 和 gb。

第 4 章 数组、切片和映射

Go 语言有 3 种数据结构可以让用户管理集合数据:数组、切片和映射,数组是切片和映射的基础数据结构。

数组的内部实现

数组是一个 长度固定 的数据类型,用于存储一段 具有相同的类型 的元素的连续块。数组存储的类型可以是内置类型,如整型或者字符串,也可以是某种结构类型。

数组占用的内存是连续分配的。由于内存连续,CPU 能把正在使用的数据缓存更久的时间,同时访问速度也会很快。

数组的声明和初始化

声明数组时需要指定内部存储的数据的类型,以及需要存储的元素的数量。

var array [5]int // 声明一个包含 5 个元素的整型数组
复制代码

此外,一种快速创建数组并初始化的方式是使用数组字面量。数组字面量允许声明数组里元素的数 量同时指定每个元素的值。

array:= [5]int{1,2,3,4,5}


// 声明一个整型数组
// 用具体值初始化每个元素
// 容量由初始化值的数量决定
array := [...]int{10, 20, 30, 40, 50}

// 声明一个有 5 个元素的数组
// 用具体值初始化索引为 1 和 2 的元素
// 其余元素保持零值
array := [5]int{1: 10, 2: 20}
复制代码
多维数组
// 声明一个二维整型数组,两个维度分别存储 4 个元素和 2 个元素
var array [4][2]int
// 使用数组字面量来声明并初始化一个二维整型数组
array := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
复制代码

数组在函数间进行传递会进行完成复制操作,这样不利于优化内存和性能,比较好的一种做法是使用指针,将数组的地址传入函数,此时只需要分配8字节的内存给指针就可以了。当然要意识到,因为现在传递的是指针,所以如果改变指针指向的值,会改变共享的内存。如你所见,使用切片能更好地处理这类共享问题。

切片的内部实现和基础功能

原理:切片是一种数据结构,这种数据结构便于使用和管理数据集合。切片是围绕动态数组的概念 构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数 append 来实现的。这个函 数可以快速且高效地增长切片。还可以通过对切片再次切片来缩小一个切片的大小。因为切片的 底层内存也是在连续块中分配的,所以切片还能获得索引、迭代以及为垃圾回收优化的好处。

切片有 3 个字段的数据结构,分别是指向底层数组的指针、切片访问的元素的个数(即长度)和切片允许增长到的元素个数(即容量)。

切片的创建和初始化

方法1:make和切片字面量

// 创建一个字符串切片
// 其长度和容量都是 5 个元素
slice := make([]string, 5)

// 创建一个整型切片
// 其长度为 3 个元素,容量为 5 个元素
slice:=make([]int,3,5)

// 创建字符串切片
// 其长度和容量都是 5 个元素
slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}
复制代码

记住,如果在[]运算符里指定了一个值,那么创建的就是数组而不是切片。只有不指定值 的时候,才会创建切片

// 创建有 3 个元素的整型数组
array := [3]int{10, 20, 30}
// 创建长度和容量都是 3 的整型切片
slice := []int{10, 20, 30}
复制代码

对底层数组容量是 k 的切片 slice[i:j]来说

长度: j - i

容量: k - i

切片增长

使用 append,需要一个被操作的切片和一个要追加的值。

注意:函数 append 会智能地处理底层数组的容量增长。在切片的容量小于 1000 个元素时,总是 会成倍地增加容量。一旦元素个数超过 1000,容量的增长因子会设为 1.25,也就是会每次增加 25% 的容量。随着语言的演化,这种增长算法可能会有所改变。

置函数 append 也是一个可变参数的函数。这意味着可以在一次调用传递多个追加的值。 如果使用...运算符,可以将一个切片的所有元素追加到另一个切片里。

// 创建两个切片,并分别用两个整数进行初始化
s1 := []int{1, 2}
s2 := []int{3, 4}
// 将两个切片追加在一起,并显示结果
fmt.Printf("%v\n", append(s1, s2...))
Output:
[1 2 3 4]
复制代码

对于切片,函数 len返回切片的长度,函数 cap 返回切片的容量

在函数间传递切片

在 64 位架构的机器上,一个切片需要 24 字节的内存:指针字段需要 8 字节,长度和容量 字段分别需要 8 字节。由于与切片关联的数据包含在底层数组里,不属于切片本身,所以将切片 复制到任意函数的时候,对底层数组大小都不会有影响。复制时只会复制切片本身,不会涉及底 层数组,在函数间传递 24 字节的数据会非常快速、简单。这也是切片效率高的地方。不需要传递指 针和处理复杂的语法,只需要复制切片,按想要的方式修改数据,然后传递回一份新的切片副本。

映射的内部实现和基础功能

映射是一种数据结构,用于存储一系列无序的键值对。映射里基于键来存储值。映射功能强大的地方是,能够基于键快速检索数据。键就像索引一样,指向与该键关联的值。 映射是无序的。

内部实现
创建和初始化
//创建一个映射,键的类型是 string,值的类型是 int
dict:=make(map[string]int)

// 创建一个映射,键和值的类型都是 string
// 使用两个键值对初始化映射
dict := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}
复制代码

映射的键可以是任何值。这个值的类型可以是内置的类型,也可以是结构类型,只要这个值 可以使用==运算符做比较。切片、函数以及包含切片的结构类型这些类型由于具有引用语义, 不能作为映射的键

从映射取值时有两个选择。第一个选择是,可以同时获得值,以及一个表示这个键是否存在 的标志。

// 获取键 Blue 对应的值
value, exists := colors["Blue"]
// 这个键存在吗?
if exists {
    fmt.Println(value)
}
复制代码

通过键来索引映射时,即便这个键不存在也总会返回一个值。在这种情况下,返回的是该值对应的类型的零值。

在函数间传递映射

在函数间传递映射并不会制造出该映射的一个副本。实际上,当传递映射给一个函数,并对 这个映射做了修改时,所有对这个映射的引用都会察觉到这个修改。这个特性和切片类似,保证可以用很小的成本来复制映射。

本章小结
  1. 数组是构造切片和映射的基石。
  2. Go 语言里切片经常用来处理数据的集合,映射用来处理具有键值对结构的数据。
  3. 内置函数 make 可以创建切片和映射,并指定原始的长度和容量。也可以直接使用切片 和映射字面量,或者使用字面量作为变量的初始值。
  4. 切片有容量限制,不过可以使用内置的 append 函数扩展容量。
  5. 映射的增长没有容量或者任何限制。
  6. 内置函数 len 可以用来获取切片或者映射的长度。
  7. 内置函数 cap 只能用于切片。
  8. 通过组合,可以创建多维数组和多维切片。也可以使用切片或者其他映射作为映射的值。 但是切片不能用作映射的键。
  9. 将切片或者映射传递给函数成本很小,并且不会复制底层的数据结构。

第5章 Go语言的类型系统

方法

方法能给 用户定义的类型 添加新的行为。方法实际上也是函数,只是在声明时,在关键字 func 和方法名之间增加了一个参数。

Go 语言里有两种类型的接收者:值接收者和指针接收者

 // notify 使用值接收者实现了一个方法
func (u user) notify() {
    fmt.Printf("Sending User Email To %s<%s>\n",
    u.name,u.email)
}
复制代码

如果使用值接收者声明方法,调用时会使用这个值的一个副本来执行。

// changeEmail 使用指针接收者实现了一个方法
func (u *user) changeEmail(email string) {
    u.email = email
}
复制代码

值接收者使用值的副本来调用方法,而指针接受者使用实际值来调用方法。

内置类型是由语言提供的一组类型,当把这些类型的值传递给方法或者函数时,应该传递一个对应值的 副本。

引用类型,Go 语言里的引用类型有如下几个:切片、映射、通道、接口和函数类型。通过复制来传递一个引用类型的值的副本,本质上就是在共享底层数据结构。

接口
嵌入类型

Go 语言允许用户扩展或者修改已有类型的行为。这个功能对代码复用很重要,在修改已有 类型以符合新类型的时候也很重要。这个功能是通过嵌入类型(type embedding)完成的。嵌入类 型是将已有的类型直接声明在新的结构类型里。被嵌入的类型被称为新的外部类型的内部类型。

type user3 struct {
	name  string
	email string
}

type admin struct {
	user3
	level string
}
复制代码

上面的代码我们声明了一个名为 user 的结构类型和另一个名为admin 的结构类型。在声明 admin 类型的方法中,我们将 user类型嵌入admin类型里。要嵌入一个类型,只需要声明这个类型的名字就可以了。 一旦我们将 user 类型嵌入 admin,我们就可以说 user 是外部类型 admin 的内部类型。

公开或未公开的标识符

当一个标识符的名字以小写字母开头时,这个标识符就是未公开的,即对包外的代码不可见。 如果一个标识符以大写字母开头,这个标识符就是公开的,即对包外的代码可见。

小结
  1. 使用关键字 struct 或者通过指定已经存在的类型,可以声明用户定义的类型。
  2. 方法提供了一种给用户定义的类型增加行为的方式。
  3. 设计类型时需要确认类型的本质是原始的,还是非原始的。
  4. 接口是声明了一组行为并支持多态的类型。
  5. 嵌入类型提供了扩展类型的能力,而无需使用继承。
  6. 标识符要么是从包里公开的,要么是在包里未公开的。

第6章 并发

Go 语言里的并发指的是能让某个函数独立于其他函数运行的能力。当一个函数创建为 goroutine 时,Go 会将其视为一个独立的工作单元。这个单元会被调度到可用的逻辑处理器上执行。

Go 语言的并发同步模型来自一个叫作通信顺序进程(Communicating Sequential Processes,CSP) 的范型(paradigm)。CSP 是一种消息传递模型,通过在 goroutine 之间传递数据来传递消息,而不是 对数据进行加锁来实现同步访问。用于在 goroutine 之间同步和传递数据的关键数据类型叫作通道 (channel)。

并发(concurrency)不是并行(parallelism)。并行是让不同的代码片段同时在不同的物理处 理器上执行。并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做 了一半就被暂停去做别的事情了。在很多情况下,并发的效果比并行好,因为操作系统和硬件的 总资源一般很少,但能支持系统同时做很多事情。

如果两个或者多个 goroutine在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态,这种情况被称作竞争状态(racecandition)。对一个共享资源的读和写操作必须是原子化的,换句话说,同一时刻只能有一个goroutine对共享资源进行读和写操作。

锁住共享资源

一种修正代码、消除竞争状态的办法是,使用 Go 语言提供的锁机制,来锁住共享资源,从 而保证 goroutine 的同步状态。Go 语言提供了传统的同步 goroutine 的机制,就是对共享资源加锁。如果需要顺序访问一个 整型变量或者一段代码,atomic 和 sync 包里的函数提供了很好的解决方案。

原子函数

原子函数能够以很底层的加锁机制来同步访问整型变量和指针。

例如atmoic 包中的AddInt64,LoadInt64和StoreInt64这几个原子函数。

互斥锁

互斥锁用于在代码上创建一个临界区,保证同一时间只有一个 goroutine 可以 执行这个临界区代码(Mutex)。

mutex.Lock()//加锁
临界区的代码
mutex。Unlock()//解锁
复制代码

通道

当一个资源需要在 goroutine 之间共享时,通道在 goroutine 之间架起了一个管道,并提供了 确保同步交换数据的机制。声明通道时,需要指定将要被共享的数据的类型。可以通过通道共享 内置类型、命名类型、结构类型和引用类型的值或者指针。 在 Go 语言中需要使用内置函数 make 来创建一个通道,例如:

// 无缓冲的整型通道
unbuffered := make(chan int)
// 有缓冲的字符串通道
buffered := make(chan string, 10)
复制代码

向通道发送值或者指针需要用到<-操作符

// 通过通道发送一个字符串
buffered <- "Gopher"

// 从通道接收一个字符串
value := <-buffered
复制代码

注意<- 在通道的位置,向通道发送时在右边,从通道中接收时在左边。

无缓冲的通道

指在接收前没有能力保存任何值的通道。要求发送和接收的goroutine必须同时准备好才能完成发送和接收的操作,否则会造成goroutine阻塞。

有缓冲的通道

有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个值的通道。这种类 型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的 条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲 区容纳被发送的值时,发送动作才会阻塞。这导致有缓冲的通道和无缓冲的通道之间的一个很大 的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的 通道没有这种保证。

小结
  1. 并发是指 goroutine 运行的时候是相互独立的。
  2. 使用关键字 go 创建 goroutine 来运行函数。
  3. oroutine 在逻辑处理器上执行,而逻辑处理器具有独立的系统线程和运行队列。
  4. 竞争状态是指两个或者多个 goroutine 试图访问同一个资源。
  5. 原子函数和互斥锁提供了一种防止出现竞争状态的办法。
  6. 通道提供了一种在两个 goroutine 之间共享数据的简单方法。
  7. 无缓冲的通道保证同时交换数据,而有缓冲的通道不做这种保证。

第7章 并发模式

runner

runner 包用于展示如何使用通道来监视程序的执行时间,如果程序运行时间太长,也可以 用 runner 包来终止程序。当开发需要调度后台处理任务的程序的时候,这种模式会很有用。

小结

  1. 可以使用通道来控制程序的生命周期。
  2. 带 default 分支的 select 语句可以用来尝试向通道发送或者接收数据,而不会阻塞。
  3. 有缓冲的通道可以用来管理一组可复用的资源。
  4. 语言运行时会处理好通道的协作和同步。
  5. 使用无缓冲的通道来创建完成工作的 goroutine 池。
  6. 任何时间都可以用无缓冲的通道来让两个 goroutine 交换数据,在通道操作完成时一定保 证对方接收到了数据。

第8章 标准库

Go 标准库是一组核心包,用来扩展和增强语言的能力。这些包为语言增加了大量不同的类型。

记录日志

一个简单的自定义日志记录器

package main

import (
	"io"
	"io/ioutil"
	"log"
	"os"
)

var (
	Trace   *log.Logger
	Info    *log.Logger
	Warning *log.Logger
	Error   *log.Logger
)

func init() {

	file, err := os.OpenFile("errors.txt",
		os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		log.Fatalln("Failed to open error log file:", err)
	}

	Trace = log.New(ioutil.Discard, "TRACE:", log.Ldate|log.Ltime|log.Lshortfile)

	Info = log.New(os.Stdout, "INFO:", log.Ldate|log.Ltime|log.Lshortfile)

	Warning = log.New(os.Stdout, "WARNING:", log.Ldate|log.Ltime|log.Lshortfile)

	Error = log.New(io.MultiWriter(file, os.Stderr), "ERROR:",     log.Ldate|log.Ltime|log.Lshortfile)

}

func main() {

	Trace.Println("I have something standard to say")
	Info.Println("Special Information")
	Warning.Println("There is something you need to know about")
	Error.Println("Something has failed")

}
复制代码

编码/解码

解码JSON

使用 json 包的 NewDecoder 函数以及 Decode方法进行解码。

解析来自网络的json数据

// 将 JSON 响应解码到结构类型
var gr gResponse
err = json.NewDecoder(resp.Body).Decode(&gr)
if err != nil {
    log.Println("ERROR:", err)
    return
}
fmt.Println(gr)
复制代码

有时,需要处理的 JSON 文档会以 string 的形式存在。在这种情况下,需要将 string 转换 为 byte 切片([]byte),并使用 json 包的 Unmarshal 函数进行反序列化的处理。

// 将 JSON 字符串反序列化到变量
var c Contact
err := json.Unmarshal([]byte(JSON), &c)
if err != nil {
    log.Println("ERROR:", err)
    return
}
复制代码

无法为 JSON 的格式声明一个结构类型,而是需要更加灵活的方式来处理 JSON 文档。 在这种情况下,可以将 JSON 文档解码到一个 map 变量中。

// 将 JSON 字符串反序列化到 map 变量
var c map[string]interface{}
err := json.Unmarshal([]byte(JSON), &c)
if err != nil {
    log.Println("ERROR:", err)
    return
}
fmt.Println("Name:", c["name"])
fmt.Println("Title:", c["title"])
fmt.Println("Contact")
fmt.Println("H:", c["contact"].(map[string]interface{})["home"])
fmt.Println("C:", c["contact"].(map[string]interface{})["cell"])
复制代码
编码JSON

使用json包的MarshalIndent函数进行编码。这个函数可以很方便地将 Go 语言的 map 类型的值或者结构类型的值转换为易读格式的 JSON 文档。

data, err := json.MarshalIndent(c, "", "    ")
if err != nil {
	fmt.Println("ERROR:", err)
	return
}
fmt.Println(string(data))
复制代码

第9章 测试和性能

go test 命令可以用来执行写好的测试代码,需要做的就是遵守一些规则来写测试。而且,可以将测试无缝地集成到代码工程和持续集成系统里。

单元测试

Go中的单元测试有以下两种:

  1. 基础测试(basic test)只使用一组参数和结果来测试一段代码。
  2. 表组测试(table test)也会测试一段代码,但是会使用多组参数和结果进行测试。

注意点1 Go 语言的测试工具只会认为以_test.go 结尾的文件是测试文件。

注意点2 一个测试函数必须是公开的函数,并且以 Test 单词开头。不但函数名字要以 Test 开头,而且函数的签名必须接收一个指向 testing.T 类型的指针,并且不返回任何值。

例如:

func TestDownload(t *testing.T) 

const checkMark = "\u2713" //输出对号
const ballotX = "\u2717"   //输出错号
复制代码

标准库包含一个名为 httptest 的包,它让开发人员可以模仿基于 HTTP 的网络调用。

func mockServer() *httptest.Server {
    f := func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(200)
        w.Header().Set("Content-Type", "application/xml")
        fmt.Fprintln(w, feed)
    }
    return httptest.NewServer(http.HandlerFunc(f))
}
复制代码

基准测试是一种测试代码性能的方法。想要测试解决同一问题的不同方案的性能,以及查看 哪种解决方案的性能更好时,基准测试就会很有用。基准测试也可以用来识别某段代码的 CPU 或者内存效率问题,而这段代码的效率可能会严重影响整个应用程序的性能。

转载于:https://juejin.im/post/5ce21fc1e51d45108c59a47c

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值