编译 Go 程序时,编译器只会关注那些直接被引用的库,而不是像 Java、C 和 C++那样,要遍历 依赖链中全部依赖的库。git
Go 语言对并发的支持是这门语言最重要的特性之一。goroutine 很像线程,可是它占用的 内存远少于线程,使用它须要的代码更少。通道(channel)是一种内置的数据结构,可让 用户在不一样的 goroutine 之间同步发送具备类型的消息。github
goroutine 是能够与其余 goroutine 并行执行的函数,同时也会与主程序(程序的入口)并行 执行。算法
通道是一种数据结构,可让 goroutine 之间进行安全的数据通讯。通道能够帮用户避免其 他语言里常见的共享内存访问的问题。json
在两个 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 作并发调度。
关键字 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 函数都会安排在 main 函数以前执行。init 函数用在设置包、初始化变量 或者其余要在程序运行前优先完成的引导工做。
运行代码经过 go run 命令。
go vet 该 命令会帮开发人员检测代码的常见错误,例如:
这个工具能够很好地捕获一部分常见错误。每次对代码先执行 go vet 再将其签入源代码库是一个很好的习惯。
Go 代码格式化 经过fmt命令。
go doc 能够查看相关包下的文档,例如go doc http。
浏览文档 开发人员启动本身的文档服务器,只须要在终端会话中输入以下命令: godoc -http=:6060 这个命令通知 godoc 在端口 6060 启动 Web 服务器。若是浏览器已经打开,导航到 http://localhost:6060 能够看到一个页面,包含全部 Go 标准库和你的GOPATH 下的 Go 源代码的文档。
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)
}
复制代码
经过键来索引映射时,即使这个键不存在也总会返回一个值。在这种状况下,返回的是该值对应的类型的零值。
在函数间传递映射并不会制造出该映射的一个副本。实际上,当传递映射给一个函数,并对 这个映射作了修改时,全部对这个映射的引用都会察觉到这个修改。这个特性和切片相似,保证能够用很小的成原本复制映射。
方法能给 用户定义的类型 添加新的行为。方法实际上也是函数,只是在声明时,在关键字 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 的内部类型。
当一个标识符的名字以小写字母开头时,这个标识符就是未公开的,即对包外的代码不可见。 若是一个标识符以大写字母开头,这个标识符就是公开的,即对包外的代码可见。
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 会在同一时间进行数据交换;有缓冲的 通道没有这种保证。
runner 包用于展现如何使用通道来监视程序的执行时间,若是程序运行时间太长,也能够 用 runner 包来终止程序。当开发须要调度后台处理任务的程序的时候,这种模式会颇有用。
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 包的 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包的MarshalIndent函数进行编码。这个函数能够很方便地将 Go 语言的 map 类型的值或者结构类型的值转换为易读格式的 JSON 文档。
data, err := json.MarshalIndent(c, "", " ")
if err != nil {
fmt.Println("ERROR:", err)
return
}
fmt.Println(string(data))
复制代码
go test 命令能够用来执行写好的测试代码,须要作的就是遵照一些规则来写测试。并且,能够将测试无缝地集成到代码工程和持续集成系统里。
Go中的单元测试有如下两种:
注意点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 或者内存效率问题,而这段代码的效率可能会严重影响整个应用程序的性能。