链接
相关链接
- Packages - The Go Programming Language
- http - The Go Programming Language
- 使用Go构建RESTful的JSON API - 酱油蔡的酱油坛 - CSDN博客
- 编写HTTP客户端 · Golang Web
- Go语言版crontab | Go语言中文网博客
- go - Golang - Getting a slice of keys from a map - Stack Overflow
- syncmap - GoDoc
- go - ERROR: need type assertion - Stack Overflow
- The-Golang-Standard-Library-by-Example/02.3.md at master · polaris1119/The-Golang-Standard-Library-by-Example
- golang时间处理 - Go语言中文网 - Golang中文社区
- Go实现比较时间大小Golang脚本之家
- Go 语言 JSON 简介 – Cizixs Writes Here
- 【GoLang笔记】遍历map时的key随机化问题及解决方法 - Go语言中文网 - Golang中文社区
- golang中时间(time)的相关操作 - 快乐编程
- [Go语言]你真的了解fmt.Printlf和fmt.Println的区别?stevewang新浪博客
- strconv - The Go Programming Language
- 类型转换 - golang []interface{} 的数组如何转换为 []string 的数组 - SegmentFault
- go 语言获取文件名、后缀 - woquNOKIA的专栏 - CSDN博客
前言
Go与C++的比较
技术选型
- Python+Redis 脚本语言开发迅速、数据结构丰富、也有多线程等模式 无法处理复杂数据结构、需要做C扩展或者使用很多复杂的实现方式,常驻内存后端程序还是不够稳定,无法处理非常高性能的场合,也不方便优化
- C++ +Redis 高性能、极强的定制性、可以实现任何内存和数据结构、可以达到任何功能和性能的要求和强大的底层操控能力 开发维护成本高、出现问题几率较大、没有专职C++开发人员,需要构建很多基础库、包括异步网络框架,采用多线程还是异步IO、配置文件解析、内存管理、日志等基础库的开发
- Go+Redis 学习曲线短、高性能、高并发,支持强大的的类C的内存和各种数据结构操作可以满足一般场景需求
=
和 :=
- C++中没有
:=
这个符号 - Go语言中:=用来声明一个变量的同时给这个变量赋值 并且只能在函数体内使用
- 主要是为了省略类型声明,系统会自己选择相应的类型识别定义的变量
New和make操作符
- C++中new操作符用于给一个对象 分配内存但是没有清零
- go语言中调用new(T)被求值时 所做的是为T类型的新值分配并且清零一块内存空间,然后将内存空间的地址作为结果返回 所以不用担心乱码问题 可以直接拿来使用
- make用于内建类型(map、slice 和channel)的内存分配。new用于各种类型的内存分配。
- make只能创建slice、map和channel,并且返回一个有初始值(非零)的T类型(引用),而不是*T。
面向对象
- go语言和c++在面向对象方面完全没有可比性,尤其是没有继承、泛型、虚函数、函数重载、构造函数、析构函数等。
入门
Go语言编译过程没有警告信息,争议特性之一
在表达式x + y中,可在+后换行,不能在+前换行(译注:以+结尾的话不会被插入分号分隔符,但是以x结尾的话则会被分号分隔符,从而导致编译错误)
根据代码需要, 自动地添加或删除import声明 go get golang.org/x/tools/cmd/goimports
命令行参数
1 | package main |
查找重复的行
1 | %d 十进制整数 |
1 | package main |
GIF动画
1 | package main |
并发获取多个URL
1 | package main |
Web服务
1 | package main |
Switch
tag switch(tagless switch) —- Go语言里的switch还可以不带操作对象(译注:switch不带操作对象时默认用true值代替,然后将每个case的表达式和true值进行比较)
程序结构
命名
关键字
1 | break default func interface select |
预先定义的名字并不是关键字,你可以在定义中重新使用它们
1 | 内建常量: true false iota nil |
在习惯上,Go语言程序员推荐使用 驼峰式 命名
声明
Go语言主要有四种类型的声明语句:var、const、type和func,分别对应变量、常量、类型和函数实体对象的声明。
变量
Go语言中不存在未初始化的变量。
在包级别声明的变量会在main入口函数执行前完成初始化(§2.6.2),局部变量将在声明语句被执行到的时候完成初始化。
变量的生命周期
- 包一级声明的变量 和整个程序的运行周期是一致的
- 局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。
- 函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。
垃圾回收
- 实现思路是,从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。
- 因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在。
编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个选择并不是由用var还是new声明变量的方式决定的。
1
2
3
4
5
6
7
8
9
10
11
12var global *int
func f() {
var x int
x = 1
global = &x
}
func g() {
y := new(int)
*y = 1
}f函数里的x变量必须在堆上分配,因为它在函数退出后依然可以通过包一级的global变量找到,虽然它是在函数内部定义的;用Go语言的术语说,这个x局部变量从函数f中逃逸了。相反,当g函数返回时,变量
*y
将是不可达的,也就是说可以马上被回收的。因此,*y
并没有从函数g中逃逸,编译器可以选择在栈上分配*y
的存储空间(译注:也可以选择在堆上分配,然后由Go语言的GC回收这个变量的内存空间),虽然这里用的是new方式。
赋值
自增和自减是语句,而不是表达式,因此x = i++之类的表达式是错误的)
元组赋值1
x, y = y, x
类型
1 | type Celsius float64 // 摄氏温度 |
- Celsius和Fahrenheit分别对应不同的温度单位。它们虽然有着相同的底层类型float64,但是它们是不同的数据类型,因此它们不可以被相互比较或混在一个表达式运算。
- 因此需要一个类似Celsius(t)或Fahrenheit(t)形式的显式转型操作才能将float64转为对应的类型。
包和文件
因为汉字不区分大小写,因此汉字开头的名字是没有导出的
Import
- 按照惯例,一个包的名字和包的导入路径的最后一个字段相同,例如
gopl.io/ch2/tempconv
包的名字一般是tempconv。 - import别名
1
2
3
4import (
sort "wuxu.bit/example/alg/sort"
"fmt"
)
golang在引入包的时候还有一些tricks:
import . "fmt"
这样在调用fmt包的导出方法时可以省略fmtimport _ "fmt"
这样引入该包但是不引入该包的导出函数,而是为了使用该导入操作的副作用: 调用包里面的init函数
包的初始化
- 包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化
- 初始化工作是自下而上进行的,main包最后被初始化。以这种方式,可以确保在main函数执行之前,所有依赖的包都已经完成初始化工作了。(包括init函数)
作用域
if,switch条件部分为一个隐式词法域,然后是每个分支的词法域。
基础数据类型
Go语言将数据类型分为四类:基础类型、复合类型、引用类型和接口类型。
本章介绍基础类型,包括:数字、字符串和布尔型。
整型
等价类型
- Unicode字符rune类型是和int32等价的类型,通常用于表示一个Unicode码点。
- byte也是uint8类型的等价类型,byte类型一般用于强调数值是一个原始的数据而不是一个小的整数。
二元运算符,它们按照优先级递减的顺序排列如下,二元运算符有五种优先级。在同一个优先级,使用左优先结合规则,但是使用括号可以明确优先顺序,使用括号也可以用于提升优先级
1 | * / % << >> & &^ |
位操作符
1 | & 位运算 AND |
位操作运算符^作为二元运算符时是按位异或(XOR),当用作一元运算符时表示按位取反;
位操作代码
1 | package main |
对于一个float
,可以利用int(f)
这样的语法进行转化,但是会丢弃小数部分
打印八进制、十进制、十六进制
1 | o := 0666 |
- %之后的[1]副词告诉Printf函数再次使用第一个操作数。
- %后的#副词告诉Printf在用%o、%x或%X输出时生成0、0x或0X前缀。
浮点数
一个float32类型的浮点数可以提供大约6个十进制数的精度,而float64则可以提供约15个十进制数的精度;
复数
Go语言提供了两种精度的复数类型:complex64和complex128,分别对应float32和float64两种浮点数精度。内置的complex函数用于构建复数,内建的real和imag函数分别返回复数的实部和虚部:
1 | var x complex128 = complex(1, 2) // 1+2i |
布尔型
1 | func btoi(b bool) int { |
字符串
内置的len函数可以返回一个字符串中的字节数目(不是rune字符数目)
字符串可以用==和<进行比较;比较通过逐个字节比较完成的,因此比较的结果是字符串自然编码的顺序。
字符串的值是不可变的:
原生的字符串面值\
…`,用\
包含的字符串中没有转义操作
Unicode和UTF-8
- Unicode每个符号都分配一个唯一的Unicode码点,Unicode码点对应Go语言中的rune整数类型(译注:rune是int32等价类型)。
- UTF8编码使用1到4个字节来表示每个Unicode码点,ASCII部分字符只使用1个字节,常用字符部分使用2或3个字节表示。
1 | 0xxxxxxx runes 0-127 (ASCII) |
统计utf8的字符串中rune的个数,使用utf8.RuneCountInString(s)
转换代码
1 | // "program" in Japanese katakana |
在第一个Printf中的% x参数用于在每个十六进制数字前插入一个空格。
bytes、strings、strconv和unicode
- bytes.Buffer提供构建字符串。
- strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。
- unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。每个函数有一个单一的rune类型的参数,然后返回一个布尔值。
常量
- 常量表达式的值在编译期计算,而不是在运行期。每种常量的潜在类型都是基础类型:boolean、string或数字。
- 常量间的所有算术运算、逻辑运算和比较运算的结果也是常量,对常量的类型转换操作或以下函数调用都是返回常量结果:len、cap、real、imag、complex和unsafe.Sizeof
iota 常量生成器
1 | const ( |
复合数据类型
数组、slice、map和结构体
- 数组和结构体是聚合类型;它们的值由许多元素或成员字段的值组成。
- 数组是由同构的元素组成——每个数组元素都是完全相同的类型——结构体则是由异构的元素组成的。
- 数组和结构体都是有固定内存大小的数据结构。
- slice和map则是动态的数据结构,它们将根据需要动态增长。
数组
长度固定
初始化
1 | var a [3]int |
数组比较
- 相同类型(相同长度)的才能比较
- 不同类型比较会编译错误
- 数组中的元素完全一样,才是相等
1 | a := [2]int{1, 2} |
函数传参
- 函数参数变量接收的是一个复制的副本
- 传递大的数组类型将是低效的
- Go语言对待数组的方式和其它很多编程语言不同,其它编程语言可能会隐式地将数组作为引用或指针对象传入被调用的函数。
- 改进:传递数组指针
1
2func zero(ptr *[32]byte) {
}
sha256创建及比较
1 | package main |
sha256,sha384,sha512输出
1 | package main |
Slice
语法
- 一个slice类型一般写作[]T,slice的语法和数组很像,只是没有固定长度而已。
- 初始化时,同样可以使用顺序指定序列或者通过索引指定。
- 不能用
==
、!=
比较2个slice中的数据是否相同。 - 使用高度优化的bytes.Equal函数来判断两个字节型slice是否相等([]byte),但是对于其他类型的slice,我们必须自己展开每个元素进行比较
- make
1
2make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]
底层数据结构
- slice是轻量级的数据结构,slice的底层引用了一个数组对象。
- slice由三个部分构成:指针、长度和容量。
- 长度对应slice中元素的数目;长度不能超过容量
- 容量一般是从slice的开始位置到底层数据的结尾位置
- 内置的len和cap函数分别返回slice的长度和容量。
- 切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了slice,因为新slice的长度会变大
底层实现
- 创建slice时,会隐式地创建一个合适大小的数组,然后slice的指针指向底层的数组。
- 多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。
零值
- 一个零值的slice等于nil。一个nil值的slice并没有底层数组。一个nil值的slice的长度和容量都是0
- 非nil值的slice的长度和容量也是0的,例如[]int{}或make([]int, 3)[3:]
Append
1 | var x []int |
- 内存分配策略类似于C++的Vector
重写reverse函数,使用数组指针代替slice。
1 | package main |
编写一个rotate函数,通过一次循环完成旋转。
1 | package main |
写一个函数在原地完成消除[]string中相邻重复的字符串的操作。
1 | package main |
Map
底层数据结构
- map是一个哈希表的引用
- map中所有的key都有相同的类型,所有的value也有着相同的类型
- map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作:禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。
遍历
for k, v := range map
- 迭代顺序是不确定的
按顺序遍历key/value对方法
1 | import "sort" |
查找
1 | if age, ok := ages["bob"]; !ok { /* ... */ } |
判断2个map是否相等
1 | func equal(x, y map[string]int) bool { |
结构体
语法
- 结构体成员名字是以大写字母开头的,那么该成员就是导出的;
- 一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。但是S类型的结构体可以包含*S指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。
比较
- 如果结构体的全部成员都是可以比较的,那么结构体也是可以用
==
比较的
结构体嵌入和匿名成员
- Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。
匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。
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
29type Point struct {
X, Y int
}
type Circle struct {
Point
Radius int
}
type Wheel struct {
Circle
Spokes int
}
var w Wheel
w.X = 8 // equivalent to w.Circle.Point.X = 8
w.Y = 8 // equivalent to w.Circle.Point.Y = 8
w.Radius = 5 // equivalent to w.Circle.Radius = 5
w.Spokes = 20
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)
}这样就可以直接访问叶子属性而不需要给出完整的路径。同时完整的访问方式同样支持
w.Circle.Point.X
- 匿名成员也有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员,这会导致名字冲突。
JSON
marshaling:结构体slice转为JSON
1 | package main |
- 成员Tag一般用原生字符串面值的形式书写
- json开头键名对应的值用于控制encoding/json包的编码和解码的行为,并且encoding/…下面其它的包也遵循这个约定
- 成员Tag中json对应值的第一部分用于指定JSON对象的名字
- omitempty选项,表示当Go语言结构体成员为空或零值时不生成该JSON对象(这里false为零值)
文本和HTML模板
1 | const templ = `{{.TotalCount}} issues: |
- 一个模板是一个字符串或一个文件,里面包含了一个或多个由双花括号包含的对象。
- 每一个action,都有一个当前值的概念,对应点操作符,写作“.”。
- 模板中和对应一个循环action,因此它们直接的内容可能会被展开多次,循环每次迭代的当前值对应当前的Items元素的值。
- 一个action中,|操作符表示将前一个表达式的结果作为后一个函数的输入,类似于UNIX中管道的概念。
- 对于Age部分,第二个动作是一个叫daysAgo的函数
函数
函数声明
Go语言没有默认参数值
递归
Go语言使用可变栈,栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题。
多返回值
如果一个函数所有的返回值都有显式的变量名,那么该函数的return语句可以省略操作数。这称之为bare return。
1 | func HourMinSec(t time.Time) (hour, minute, second int) |
错误
错误形式只有一种,ok
1 | value, ok := cache.Lookup(key) |
错误形式多种,error
1 | fmt.Println(err) |
错误传递
1 | doc, err := html.Parse(resp.Body) |
- fmt.Errorf函数使用fmt.Sprintf格式化错误信息并返回
- 我们使用该函数添加额外的前缀上下文信息到原始错误信息。当错误最终由main函数处理时,错误信息应提供清晰的从原因到后果的因果链
- 错误信息中应避免大写和换行符。最终的错误信息可能很长
错误等待代码
1 | func WaitForServer(url string) error { |
错误打印
- log包中的所有函数会为没有换行符的字符串增加换行符
通过io.EOF
错误判断文件结束
1 | in := bufio.NewReader(os.Stdin) |
函数值
- 函数被看作第一类值(first-class values)
- 函数类型的零值是nil。调用值为nil的函数值会引起panic错误
- 函数值可以与nil比较,但是函数值之间是不可比较的,也不能用函数值作为map的key
匿名函数
- 使用时注意循环变量的作用域问题
可变参数
1 | func sum(vals...int) int { |
Deferred函数
defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。释放资源的defer应该直接跟在请求资源的语句后。
Panic函数
1 | goroutine 1 [running]: |
Recover捕获异常
如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。
1 | func Parse(input string) (s *Syntax, err error) { |
方法
方法声明
1 | package geometry |
- Path是一个命名的slice类型,而不是Point那样的struct类型,然而我们依然可以为它定义方法。
- Go语言里,我们为一些简单的数值、字符串、slice、map来定义一些附加行为很方便。
- 不可以为指针和interface定义额外的方法
基于指针对象的方法
接收器func (p *Point) ScaleBy(factor float64) {}
通过潜入结构体来扩展类型
方法值和方法表达式
1 | type Rocket struct { /* ... */ } |
1 | p := Point{1, 2} |
另一个方法表达式的例子
1 | type Point struct{ X, Y float64 } |
示例:Bit数组
封装
封装提供了三方面的优点。首先,因为调用方不能直接修改对象的变量值,其只需要关注少量的语句并且只要弄懂少量变量的可能的值即可。
第二,隐藏实现的细节,可以防止调用方依赖那些可能变化的具体实现,这样使设计包的程序员在不破坏对外的api情况下能得到更大的自由。
封装的第三个优点也是最重要的优点,是阻止了外部调用方对对象内部的值任意地进行修改。
接口
接口是合约
io.Writer
1 | package io |
- 这种类型都包含一个Write函数
- Fprintf接受任何满足io.Writer接口的值都可以工作
接口类型
1 | package io |
实现接口的条件
一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。
io.ReadWriter或者io.Reader,io.Writer都是接口类型,更多方法的接口类型表示对实现它的类型要求更加严格。
interface{}被称为空接口类型。空接口类型对实现它的类型没有要求,所以我们可以将任意一个值赋给空接口类型。
interface{}没有任何方法,我们不能对它持有的值做操作。
flag.Value接口
自定义flag.Value接口类型
1 | package main |
flag.Value接口类型定义如下
1 | package flag |
- Set方法解析它的字符串参数并且更新标记变量的值
- celsiusFlag内嵌了一个Celsius类型,因此不用实现本身就已经有String方法了。为了实现flag.Value,我们只需要定义Set方法:
- CelsiusFlag函数将所有逻辑都封装在一起。它返回一个内嵌在celsiusFlag变量f中的Celsius指针给调用者
- 解释为什么帮助信息在它的默认值是20.0没有包含°C的情况下输出了°C。
- 因为调用了String()方法
接口值
空类型
1 | var w io.Writer = os.Stdout |
1 | var x interface{} = time.Now() |
sort.Interface接口
1 | package main |
http.Handler接口
1 | package main |
error接口
errors.New
fmt.Errorf
示例:表达式求值
类型断言
x.(T)被称为断言类型
- 如果断言的类型T是一个具体类型,然后类型断言检查x的动态类型是否和T相同。如果这个检查成功了,类型断言的结果是x的动态值,当然它的类型是T。如果检查失败,接下来这个操作会抛出panic。
- 如果相反地断言的类型T是一个接口类型,然后类型断言检查是否x的动态类型满足T。从一个接口类型转换到另一个接口类型,检查是否满足第二个接口类型的要求。
- 如果断言操作的对象是一个nil接口值,那么不论被断言的类型是什么这个类型断言都会失败
基于类型断言区别错误类型
1 | import ( |
通过类型断言询问行为
类型分支
1 | switch x.(type) { |
示例: 基于标记的XML解码
Goroutines和Channels
顺序通信进程”(communicating sequential processes)或被简称为CSP
Goroutines
当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine。
除了从主函数退出或者直接终止程序之外,没有其它的编程方法能够让一个goroutine来打断另一个的执行。
等待转圈代码
1 | func spinner(delay time.Duration) { |
示例: 并发的Clock服务
TCP服务器程序
1 | package main |
TCP客户端程序
1 | package main |
示例: 并发的Echo服务
Channels
- channels则是goroutines之间的通信机制
- 可以让一个goroutine通过它给另一个goroutine发送值信息
- 每个channel都有一个特殊的类型,也就是channels可发送数据的类型
创建
1
ch := make(chan int) // ch has type 'chan int'
channel也对应一个make创建的底层数据结构的引用
- 两个相同类型的channel可以使用==运算符比较
- 一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine
发送和接收两个操作都使用
<-
运算符1
2
3ch <- x // a send statement
x = <-ch // a receive expression in an assignment statement
<-ch // a receive statement; result is discarded关闭channel,随后对基于该channel的任何发送操作都将导致panic异常
- 试图重复关闭一个channel将导致panic异常
- 试图关闭一个nil值的channel也将导致panic异常
关闭一个channels还会触发一个广播机制
1
close(ch)
对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话将直接返回并产生一个零值的数据。
- 创建带缓存的Channel
1
2
3ch = make(chan int) // unbuffered channel
ch = make(chan int, 0) // unbuffered channel
ch = make(chan int, 3) // buffered channel with capacity 3
不带缓存的Channels
- 一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。
- 基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作。因为这个原因,无缓存Channels有时候也被称为同步Channels。
- 当通过一个无缓存Channels发送数据时,接收者收到数据发生在唤醒发送者goroutine之前(译注:happens before,这是Go语言并发内存模型的一个关键术语!)。
串联的Channels(Pipeline)
- 一个Channel的输出作为下一个Channel的输入。这种串联的Channels就是所谓的管道(pipeline)。
- Channel可以被range
单方向的Channel
- 类型
chan<- int
表示一个只发送int的channel - 类型
<-chan int
表示一个只接收int的channel - 对一个只接收的channel调用close将是一个编译错误
- 任何双向channel向单向channel变量的赋值操作都将导致该隐式转换
- 不能将一个类似chan<- int类型的单向型的channel转换为chan int类型的双向型的channel
带缓存的Channels
- 向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。
- 内置的cap函数获取缓存的容量
cap(ch)
- 内置的len函数获取缓存内有效元素的个数
len(ch)
goroutines泄漏,这是一个BUG。和垃圾变量不同,泄漏的goroutines并不会被自动回收,
并发的循环
注意循环变量失效问题
sync.WaitGroup
1 | // makeThumbnails6 makes thumbnails for each file received from the channel. |
- Add和Done方法的不对称。Add是为计数器加一,必须在worker goroutine开始之前调用,而不是在goroutine中
- Done却没有任何参数;其实它和Add(-1)是等价的。
并发的Web爬虫
基于select的多路复用
select{},会永远地等待下去
多个case同时就绪时,select会随机地选择一个执行
示例: 并发的目录遍历
并发的退出
关闭一个channel来进行广播
1 | var done = make(chan struct{}) |
示例: 聊天服务
1 | package main |
基于共享变量的并发
竞争条件
数据竞争会在两个以上的goroutine并发访问相同的变量且至少其中一个为写操作时发生
不要使用共享数据来通信;使用通信来共享数据
sync.Mutex互斥锁
sync.RWMutex读写锁
“多读单写”锁(multiple readers, single writer lock)
1 | var mu sync.RWMutex |
内存同步
1 | var x, y int |
可能出现x:0 y:0
因为赋值和打印指向不同的变量,编译器可能会断定两条语句的顺序不会影响执行结果,并且会交换两个语句的执行顺序。如果两个goroutine在不同的CPU上执行,每一个核心有自己的缓存,这样一个goroutine的写入对于其它goroutine的Print,在主存同步之前就是不可见的了。
sync.Once初始化
1 | var loadIconsOnce sync.Once |
每一次对Do(loadIcons)的调用都会锁定mutex,并会检查boolean变量。在第一次调用时,boolean变量的值是false,Do会调用loadIcons并会将boolean变量设置为true。随后的调用什么都不会做,但是mutex同步会保证loadIcons对内存(这里其实就是指icons变量啦)产生的效果能够对所有goroutine可见。用这种方式来使用sync.Once的话,我们能够避免在变量被构建完成之前和其它goroutine共享该变量。
竞争条件检测
竞争检查器(the race detector)。
只要在go build,go run或者go test命令后面加上-race的flag,就会使编译器创建一个你的应用的“修改”版或者一个附带了能够记录所有运行期对共享变量访问工具的test,并且会记录下每一个读或者写共享变量的goroutine的身份信息。
示例: 并发的非阻塞缓存
Goroutines和线程
动态栈
- 每一个OS线程都有一个固定大小的内存块(一般会是2MB)来做栈,这个栈会用来存储当前正在被调用或挂起(指在调用其它函数时)的函数的内部变量。
- 一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB。
- 一个goroutine的栈,和操作系统线程一样,会保存其活跃或挂起的函数调用的本地变量
- 但是和OS线程不太一样的是,一个goroutine的栈大小并不是固定的;栈的大小会根据需要动态地伸缩。而goroutine的栈的最大值有1GB,比传统的固定大小的线程栈要大得多,尽管一般情况下,大多goroutine都不需要这么大的栈。
Goroutine调度
- OS线程会被操作系统内核调度。
- 线程切换:每几毫秒,一个硬件计时器会中断处理器,这会调用一个叫作scheduler的内核函数。这个函数会挂起当前执行的线程并将它的寄存器内容保存到内存中,检查线程列表并决定下一次哪个线程可以被运行,并从内存中恢复该线程的寄存器信息,然后恢复执行该线程的现场并开始执行线程。
- Go的运行时包含了其自己的调度器,这个调度器使用了一些技术手段,比如m:n调度,因为其会在n个操作系统线程上多工(调度)m个goroutine。
- Go调度器的工作和内核的调度是相似的,但是这个调度器只关注单独的Go程序中的goroutine。
- 和操作系统的线程调度不同的是,Go调度器并不是用一个硬件定时器,而是被Go语言“建筑”本身进行调度的。例如当一个goroutine调用了time.Sleep,或者被channel调用或者mutex操作阻塞时,调度器会使其进入休眠并开始执行另一个goroutine,直到时机到了再去唤醒第一个goroutine。因为这种调度方式不需要进入内核的上下文,所以重新调度一个goroutine比调度一个线程代价要低得多。
GOMAXPROCS
- Go的调度器使用了一个叫做GOMAXPROCS的变量来决定会有多少个操作系统的线程同时执行Go的代码。其默认的值是运行机器上的CPU的核心数
- 你可以用GOMAXPROCS的环境变量来显式地控制这个参数,或者也可以在运行时用runtime.GOMAXPROCS函数来修改它。
1
2
3
4
5
6
7
8
9
10for {
go fmt.Print(0)
fmt.Print(1)
}
$ GOMAXPROCS=1 go run hacker-cliché.go
111111111111111111110000000000000000000011111...
$ GOMAXPROCS=2 go run hacker-cliché.go
010101010101010101011001100101011010010100110...
Goroutine没有ID号
- 线程都有一个独特的身份(id)
包和工具
包简介
导入路径
包声明
默认包名一般采用导入路径名的最后一段的约定也有三种例外情况。
- 包对应一个可执行程序,也就是main包
- 包所在的目录中可能有一些文件名是以
_test.go
为后缀的Go源文件 - 一些依赖版本号的管理工具会在导入路径后追加版本号信息
gopkg.in/yaml.v2
导入声明
导入重命名,别名
1 | import ( |
- 导入包的重命名只影响当前的源文件
- 导入包重命名是一个有用的特性,它不仅仅只是为了解决名字冲突。如果导入的一个包名很笨重,特别是在一些自动生成的代码中,这时候用一个简短名称会更方便。
包的匿名导入
用途:计算包级变量的初始化表达式和执行导入包的init初始化函数
1 | import _ "image/png" // register PNG decoder |
包和命名
- 当创建一个包,一般要用短小的包名,但也不能太短导致难以理解。标准库中最常用的包有bufio、bytes、flag、fmt、http、io、json、os、sort、sync和time等包。
- 包名一般采用单数的形式。
工具
工作区结构
go env
下载包
go get
git clone
&go build
构建包
go build
-> 构建指定的包和它依赖的包,然后丢弃除了最后的可执行文件之外所有的中间编译结果。go install
->被编译的包保存到$GOPATH/pkg,可执行程序被保存到$GOPATH/bin
包文档
go doc
内部包
- internal包
- 一个internal包只能被和internal目录有同一个父目录的包所导入。
查询包
go list ...
测试
go test
_test.go
在*_test.go
文件中,有三种类型的函数:测试函数、基准测试(benchmark)函数、示例函数。
- 一个测试函数是以Test为函数名前缀的函数,用于测试程序的一些逻辑行为是否正确;
- go test命令会调用这些测试函数并报告测试结果是PASS或FAIL。
- 基准测试函数是以Benchmark为函数名前缀的函数,它们用于衡量一些函数的性能;
- go test命令会多次运行基准测试函数以计算一个平均的执行时间。
- 示例函数是以Example为函数名前缀的函数,提供一个由编译器保证正确性的示例文档。
go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,生成一个临时的main包用于调用相应的测试函数,接着构建并运行、报告测试结果,最后清理测试中生成的临时文件。