Golang 学习
环境
- MacOS 安装
1 | brew install go |
- 修改
.zshrc
配置
1 | export PATH=$PATH:$GOROOT/bin |
- 查看环境变量
1 | go version |
语法
基本结构和基本数据类型
可见性规则
- 只有当某个(常量、变量、类型、函数、结构字段等)需要被外部包调用的时候才使用大写字母开头,并遵循
Pascal
命名法,称为导出(像面向对象语言中的public
); - 否则就遵循骆驼命名法,即第一个单词的首字母小写,其余单词的首字母大写,对外不可见。
类型
Go
语言中不存在类型继承
- 基本类型:int、float、bool、string
- 结构化/复合:struct、array、since、map、channel
- 结构化的类型没有真正的值,使用
nil
作为默认值
- 结构化的类型没有真正的值,使用
- 描述类型行为:interface
示例
1 | 单类型定义 |
常量
常量使用关键字 const
定义,用于存储不会改变的数据。
存储在常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
常量的定义格式:const identifier [type] = value,[type]
可以省略,例如:
1 | const Pi = 3.14159 |
变量
声明变量的一般形式是使用 var
关键字:var identifier type
。
需要注意的是,Go
和许多编程语言不同,它在声明变量时将变量的类型放在变量的名称之后。
- 定义指针类型
1 | var a, b *int |
- 声明全局变量
系统会自动赋值它该类型的零值:int
为 0
,float
为 0.0
,bool
为 false
,string
为空字符串,指针为 nil
。记住,所有的内存在 Go
中都是经过初始化的。
1 | var a int |
var b int
和var a *int
区别- 两者不一样,前者是普通整数变量,后者是指向 int 类型的指针变量,存储的是内存地址,不是整数值。
- 结果:a = nil,b = 0
赋值操作符 :=
- 用于声明和赋值
1 | var a int |
整型 int 和 浮点 float
整数:
- int8(-128 -> 127)
- int16(-32768 -> 32767)
- int32(-2,147,483,648 -> 2,147,483,647)
- int64(-9,223,372,036,854,775,808 -> 9,223,372,036,854,775,807)
无符号整数:
- uint8(0 -> 255)
- uint16(0 -> 65,535)
- uint32(0 -> 4,294,967,295)
- uint64(0 -> 18,446,744,073,709,551,615)
浮点型(IEEE-754 标准): float32(+- 1e-45 -> +- 3.4 _ 1e38)
- float64(+- 5 _ 1e-324 -> 107 * 1e308)
- int 型是计算最快的一种类型。
整型的零值为 0,浮点型的零值为 0.0。
位运算
位运算只能用于整数类型的变量,且需当它们拥有等长位模式时。
%b
是用于表示位的格式化标识符。
二元运算符
- 按位与
&
1
2
3
41 & 1 -> 1
1 & 0 -> 0
0 & 1 -> 0
0 & 0 -> 0- 按位或
|
1
2
3
41 | 1 -> 1
1 | 0 -> 1
0 | 1 -> 1
0 | 0 -> 0- 按位异
^
1
2
3
41 ^ 1 -> 0
1 ^ 0 -> 1
0 ^ 1 -> 1
0 ^ 0 -> 0- 按位与
一元运算符
- 按位补足
^
该运算符与异或运算符一同使用,即 m^x,对于无符号 x 使用“全部位设置为 1”,对于有符号 x 时使用 m=-1。例如:
1
^10 = -01 ^ 10 = -11
解释:怎么算??
第一步:01 ^ 10 等价于 0 ^ 1 和 1 ^ 0 执行按位异或运算,等于 11
第二步:-01 代表负数,用二进制补码表示法,负数试讲整数的位取反并加 1 表示的,即: -01 = 10 + 1 = 11
第三步:-01 ^ 10 = 11 ^ 10 = 01
第四步:-11 的补码表示: 00 + 1 = 01
第五步:因此 -01 ^ 10 = -11总结
1
2
301 ^ 10 = 11
-01 ^ 10 = 01
-11 ^ 10 = 11位左移
<<
- 用法:bitP << n。
- bitP 的位向左移动 n 位,右侧空白部分使用 0 填充;如果 n 等于 2,则结果是 2 的相应倍数,即 2 的 n 次方。例如:
1
2
31 << 10 = 2¹⁰ = 1024 = 1KB // 等于 1 KB
1 << 20 = 2²⁰ = 1024 * 1024 = 1MB // 等于 1 MB
1 << 30 = 2³⁰ = 1024 * 1024 * 1024 = 1MB * 1024 = 1GB // 等于 1 GB位右移
- 用法:bitP >> n。
- bitP 的位向右移动 n 位,左侧空白部分使用 0 填充;如果 n 等于 2,则结果是当前值除以 2 的 n 次方。
1
2
3
4
5
6
7
8
9
10
11
12
13
14package main
import "fmt"
func main() {
var x uint8 = 8 // 8 的二进制表示为 00001000
var y int8 = -16 // -16 的二进制补码表示为 11110000
resultX := x >> 2 // 右移 2 位
resultY := y >> 2 // 右移 2 位
fmt.Println(result) // 输出 2, 二进制为 00000010
fmt.Println(result) // 输出 -4, 二进制为 11111100
}
- 按位补足
逻辑运算符
Go
中拥有以下逻辑运算符:==
、!=
、<
、<=
、>
、>=
。算术运算符
常见可用于整数和浮点数的二元运算符有
+
、-
、*
和/
。
运算符优先级
1 | 优先级 运算符 |
strings
和 strconv
包
- 前缀和后缀
1 | strings.HasPrefix(s, prefix string) bool // HasPrefix 判断字符串 s 是否以 prefix 开头 |
- 字符串包含关系
1 | strings.Contains(s, substr string) bool // Contains 判断字符串 s 是否包含 substr |
- 判断子字符串或字符在父字符串中出现的位置(索引)
1 | strings.Index(s, str string) int // Index 返回字符串 str 在字符串 s 中的索引(str 的第一个字符的索引),-1 表示字符串 s 不包含字符串 str |
- 字符串替换
1 | strings.Replace(str, old, new, n) string // Replace 用于将字符串 str 中的前 n 个字符串 old 替换为字符串 new,并返回一个新的字符串,如果 n = -1 则替换所有字符串 old 为字符串 new |
- 统计字符串出现次数
1 | strings.Count(s, str string) int // Count 用于计算字符串 str 在字符串 s 中出现的非重叠次数 |
- 重复字符串
1 | strings.Repeat(s, count int) string // Repeat 用于重复 count 次字符串 s 并返回一个新的字符串 |
- 修改字符串大小写
1 | strings.ToLower(s) string // 将字符串中的 Unicode 字符全部转换为相应的小写字符 |
- 修剪字符串
1 | strings.TrimSpace(s) // 剔除字符串开头和结尾的空白符号 |
- 分割字符串
1 | strings.Fields(s) // 将会利用 1 个或多个空白符号来作为动态长度的分隔符将字符串分割成若干小块,并返回一个 slice,如果字符串只包含空白符号,则返回一个长度为 0 的 slice。 |
- 拼接字符串
1 | strings.Join(sl []string, sep string) string // Join 用于将元素类型为 string 的 slice 使用分割符号来拼接组成一个字符串 |
- 从字符串中读取内容
1 | strings.NewReader(str) // 生成一个 Reader 并读取字符串中的内容,然后返回指向该 Reader 的指针 |
时间和日期
time
包为我们提供了一个数据类型 time.Time
(作为值使用)以及显示和测量时间和日期的功能函数。
- time.Now()
- time.Minute()
- time.Day()
- time.Month()
- time.Year()
指针
在 Go
中,指针是一个存储变量地址的值。理解指针的关键在于它们允许你直接访问和操作内存中的值,而不是仅仅通过变量名访问它们。指针在 Go
语言中用于高效传递大型数据结构、操作共享资源、或修改函数参数的值。
变量的地址:每个变量都存储在计算机的内存中,内存中的每个位置都有一个地址。指针保存的是该内存位置的地址,而不是实际的数据。
指针的类型:指针的类型与它指向的变量类型相关。例如,
*int
是一个指向int
类型变量的指针,*string
是一个指向string
类型变量的指针。获取指针:使用
&
操作符来获取变量的地址(即指针)。p := &x
将x
变量的地址赋给p
,此时p
是一个指向x
的指针。
解引用指针:使用
*
操作符可以访问指针指向的值(即解引用)。v := *p
会获取指针p
所指向的值。
控制结构
Go 完全省略了 if
、switch
和 for
结构中条件语句两侧的括号,相比 Java
、C++
和 C#
中减少了很多视觉混乱的因素,同时也使你的代码更加简洁。
- if-else
- switch
- for
- break 和 continue
- 标签和 goto
- 标签的名称是大小写敏感的,为了提升可读性,一般建议使用全部大写字母
函数
函数能够接收参数供自己使用,也可以返回零个或多个值(我们通常把返回多个值称为返回一组值)。相比与 C
、C++
、Java
和 C#
,多值返回是 Go
的一大特性。
按值传递(
call by value
)、按引用传递(call by reference
)命名的返回值(named return variables)
1
2
3
4
5
6
7
8
9
10
11
12
13package main
import "fmt"
var num int = 10 var numx2, numx3 int
func main() { numx2, numx3 = getX2AndX3(num) PrintValues() numx2, numx3 = getX2AndX3_2(num) PrintValues() }
func PrintValues() { fmt.Printf("num = %d, 2x num = %d, 3x num = %d\n", num, numx2, numx3) }
func getX2AndX3(input int) (int, int) { return 2 _ input, 3 _ input }
func getX2AndX3_2(input int) (x2 int, x3 int) { x2 = 2 _ input x3 = 3 _ input // return x2, x3 return }空白符
空白符用来匹配一些不需要的值,然后丢弃掉。1
2
3
4
5
6
7
8
9
10
11
12
13
14package main
import "fmt"
func main() {
var i1 int
var f1 float32
i1, _, f1 = ThreeValues()
fmt.Printf("The int: %d, the float: %f \n", i1, f1)
}
func ThreeValues() (int, int, float32) {
return 5, 6, 7.5
}改变外部变量
传递指针给函数不但可以节省内存(因为没有复制变量的值),而且赋予了函数直接修改外部变量的能力,所以被修改的变量不再需要使用 return。
1 | package main |
传递变长参数
如果函数的最后一个参数是采用 ...type
的形式,那么这个函数就可以处理一个变长的参数,这个长度可以为 0
,这样的函数称为变参函数。
1 | func myFunc(a, b, arg ...int) {} |
defer 和追踪
关键字 defer
允许我们推迟到函数返回之前(或任意位置执行 return
语句之后)一刻才执行某个语句或函数(为什么要在返回之后才执行这些语句?因为 return
语句同样可以包含一些操作,而不是单纯地返回某个值)。
1 | package main |
输出
1 | In Function1 at the top |
- 关闭文件流
1 | defer file.Close() |
- 解锁一个加锁的资源
1 | mu.Lock() |
- 打印最终报告
1 | printHeader() |
- 关闭数据库连接
1 | defer disconnectFromDB() |
通过内存缓存来提升性能
数组与切片
声明和初始化
元素的数目,也称为长度或者数组大小必须是固定的并且在声明该数组时就给出(编译时需要知道数组长度以便分配内存);数组长度最大为 2Gb
。
每个元素是一个整型值,当声明数组时所有的元素都会被自动初始化为默认值 0
。
1 | // 格式 |
切片
切片是引用,不需要使用额外的内存并且比使用数组更有效率,所以在 GO
中切片比数组更常用。
定义时不需要说明长度,一个切片在未出初始化之前默认是 nil
,长度是 0
。
- len() // 长度
- cap() // 计算容量
- 0 <= len(s) <= cap(s)
1 | var identifier []type // 声明切片的格式 |
例子
1 | var arr1 [6]int |
make() 创建切片
当相关数组还没有定义时,我们可以使用 make()
函数来创建一个切片 同时创建好相关数组:var slice1 []type = make([]type, len)
。
如果想创建一个 slice1
,它不占用整个数组,而只是占用以 len
为个数个项,那么只要:slice1 := make([]type, len, cap)
, 其中 cap
是可选参数。
下面两种方法可以生成相同的切片:
1 | make([]int, 50, 100) |
new() 和 make() 的区别
堆上分配内存,但是它们的行为不同,适用于不同的类型。new
函数分配内存,make
函数初始化。
- new(T) 为每个新的类型
T
分配一片内存,初始化为0
并且返回类型为*T
的内存地址:这种方法 返回一个指向类型为T
,值为0
的地址的指针,它适用于值类型如数组和结构体;它相当于&T{}
。 - make(T) 返回一个类型为
T
的初始值,它只适用于3
种内建的引用类型:切片
、map
和channel
。
bytes 包
定义 Buffer
1 | var buffer bytes.Buffer // |
通过 buffer 串联字符串.
- 通过 buffer 串联字符串
这种实现方式比使用 +=
要更节省内存和 CPU
,尤其是要串联的字符串数目特别多的时候。
1 | var buffer bytes.Buffer |
For-range 结构
range
循环 和for
循环
切片的复制和追加
- 数组不能追加,切片可以追加
Map
- 最好提前标注 map 的容量
map2 := make(map[string]float32, 100)
。 如果再增加新的 key-value 时,map de 大小会自动 +1。 val1, isPresent = map1[key1]
, 通过isPresent
来判断是否存在- map 默认是无序的,不会对 key、value 进行排序
包 package
regexp
- regexp.Match
- regexp.MatchString
锁和
sync
包- 不同线程中使用同一个变量时,无法预知变量被不同线程修改的顺序,通常的做法是只让一个线程对共享变量进行操作,当变量被一个线程改变时(临界区),我们为它上锁,知道线程执行完成并解锁后,其他线程才能访问。
map
类型时不存在锁的机制来实现这个效果(出于对性能的考虑),所以map
类型时非线程安全的,当并行访问恭喜的map
类型数据时,map
数据将会出错。- 通过
sync
包中Mutex
来实现,sync.Mutex
是一个互斥锁,它的作用是守护在临界区入口来确保同一个时间只能有一个线程进入临界区。
精密计算和
big
包- 使用
Go
语言中的float64
类型进行浮点运算,返回结果将精确到 15 位。 - 当对超出
int64
或者unit64
类型这样的大数进行计算时,如果对精度有严格要求,我们可以通过big
包。- big.NewInt(n) // 大的整型数字
- big.NewRat(n, d) // 大的有理数
- 使用
自定义包和可见性
go install 安装自定义包
结构(struct)和方法(method)
- 结构体定义
1 | type identifier struct { |
- 结构体工厂
- 如果
File
是一个结构体类型,那么表达式new(File)
和&File{}
是等价的。
- 如果
1 | // File 结构体类型 |
- 带标签的结构体
- 匿名字段和内嵌结构体
- 方法
- 格式:
func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }
- 函数和方法的区别
- 函数将变量作为参数:
Function1(recv)
- 方法在变量上被调用:
recv.Method1()
- 函数将变量作为参数:
- 内嵌类型的方法和继承
- 当一个匿名类型被内嵌在结构体中时,匿名类型的可见方法也同样被内嵌,这在效果上等同于外层类型 继承 了这些方法:
将父类型放在子类型中来实现亚型
。 - 这个机制提供了一种简单的方式来模拟经典面向对象语言中的子类和继承相关的效果,也类似
Ruby
中的混入(mixin
)。
- 当一个匿名类型被内嵌在结构体中时,匿名类型的可见方法也同样被内嵌,这在效果上等同于外层类型 继承 了这些方法:
- 如何在类型中嵌入功能
- 聚合(组合):包含一个所需功能类型的具名字段
- 内嵌:内嵌(匿名地)所需功能类型
- 格式:
读写数据
读取用户输入
fmt.Scanln、fmt.Scanf
Scanln
函数用于从标准输入中读取文本,直到遇到换行符(即用户按下回车键)。它会把输入的内容按空格分隔,依次存储到传入的变量中。Scanf
函数允许更灵活的格式化输入。它基于指定的格式字符串,解析输入内容,并将其存储到相应的变量中。它类似于printf
的逆操作,使用格式化占位符来解析输入。
fmt.Sscanln、fmtSscanf
Sscanln
函数从给定的字符串中解析数据,类似于 Scanln。它根据空格分隔符将字符串拆分,并将数据存储在传入的变量中,直到遇到换行符或字符串结束。Sscanf
函数从给定的字符串中解析数据,类似于 Scanf。它基于格式化字符串来解析数据,可以控制输入的格式,比如指定字符串、数字等的解析方式。
bufio.Reader
NewReader
是指向bufio.Reader
的指针。inputReader := bufio.NewReader(os.Stdin)
这行代码,将会创建一个读取器,并将其与标准输入绑定ReadString
返回读取到的字符串,如果碰到错误则返回nil
。如果它一直读到文件结束,则返回读取到的字符串和 io.EOF。如果读取过程中没有碰到delim
字符,将返回错误err != nil
。
文件读取
- 读文件
- 读取压缩文件
compress
包 - 写文件
1 | package main |
文件拷贝
1 | // filecopy.go |
从命令行读取参数
os
包中有一个 string 类型的切片变量os.Args
,用来处理一些基本的命令行参数,它在程序启动后读取命令行输入的参数。flag
包有一个扩展功能用来解析命令行选项。但是通常被用来替换基本常量,例如,在某些情况下我们希望在命令行给常量一些不一样的值。- flog.Parse() 扫描参数列表,并设置 flag。
- flag.Narg() 返回参数的个数。
- flag.PrintDefaults() 打印 flag 的使用帮助信息。
用 buffer 读取文件
用切片读写文件
切片提供了 Go
中处理 I/O
缓冲的标准方式。
JSON 数据格式
数据结构要在网络中传输或保存到文件,就必须对其编码和解码;目前存在很多编码格式:JSON
,XML
,gob
,Google
缓冲协议等等。Go
语言支持所有这些编码格式;
术语说明:
- 数据结构 –> 指定格式 =
序列化
或编码
(传输之前) - 指定格式 –> 数据格式 =
反序列化
或解码
(传输之后)
序列号
json.Marshal()
的函数签名是 func Marshal(v interface{}) ([]byte, error)
JSON
与 Go
类型对应如下:
- bool 对应 JSON 的 boolean
- float64 对应 JSON 的 number
- string 对应 JSON 的 string
- nil 对应 JSON 的 null
不是所有的数据都可以编码为 JSON
类型:只有验证通过的数据结构才能被编码:
- JSON 对象只支持字符串类型的 key;要编码一个
Go map
类型,map
必须是map[string]T
(T
是json
包中支持的任何类型) - Channel,复杂类型和函数类型不能被编码
- 不支持循环数据结构;它将引起序列化进入一个无限循环
- 指针可以被编码,实际上是对指针指向的值进行编码(或者指针是 nil)
反序列化
json.UnMarshal()
的函数签名是 func Unmarshal(data []byte, v interface{}) error
把 JSON
解码为数据结构。
解码数据到结构
1 | // 结构 |
程序实际上是分配了一个新的切片。这是一个典型的反序列化引用类型(指针、切片和 map
)的例子。
解码和解码流
json
包提供 Decoder
解码 和 Encoder
编码 类型来支持常用 JSON
数据流读写。NewDecoder
和 NewEncoder
函数分别封装了 io.Reader
和 io.Writer
接口。
1 | func NewDecoder(r io.Reader) *Decoder |
XML 数据格式
如同 json
包一样,也有 Marshal()
和 UnMarshal()
从 XML
中编码和解码数据;但这个更通用,可以从文件中读取和写入(或者任何实现了 io.Reader
和 io.Writer
接口的类型)
和 JSON
的方式一样,XML
数据可以序列化为结构,或者从结构反序列化为 XML
数据。
encoding/xml
包实现了一个简单的 XML
解析器(SAX
),用来解析 XML
数据内容。
包中定义了若干 XML 标签类型:
- StartElement 开始标记
- Chardata 这是从开始标签到结束标签之间的实际文本
- EndElement 结束标记
- Comment
- Directive
- ProcInst
用 Gob 传输数据
Gob
是 Go
自己的以二进制形式序列化和反序列化程序数据的格式;可以在 encoding
包中找到。这种格式的数据简称为 Gob
(即 Go binary
的缩写)。
Gob
文件或流是完全自描述的:里面包含的所有类型都有一个对应的描述,并且总是可以用 Go
解码,而不需要了解文件的内容。
只有可导出的字段会被编码,零值会被忽略。在解码结构体的时候,只有同时匹配名称和可兼容类型的字段才会被解码。当源数据类型增加新字段后,Gob
解码客户端仍然可以以这种方式正常工作:解码客户端会继续识别以前存在的字段。
和 JSON
的使用方式一样,Gob
使用通用的 io.Writer
接口,通过 NewEncoder()
函数创建 Encoder
对象并调用 Encode()
;相反的过程使用通用的 io.Reader
接口,通过 NewDecoder()
函数创建 Decoder
对象并调用 Decode()
。
Go 中的密码学
通过网络传输的数据必须加密,以防止被 hacker(黑客)读取或篡改,并且保证发出的数据和收到的数据检验和一致。
hash
包:实现了adler32
、crc32
、crc64
和fnv
校验;crypto
包:实现了其它的hash
算法,比如md4
、md5
、sha1
等。以及完整地实现了aes
、blowfish
、rc4
、rsa
、xtea
等加密算法。
错误处理与测试
Go
没有像 Java
和 .NET
那样的 try/catch
异常机制:不能执行抛异常操作。但是有一套 defer-panic-and-recover
机制。
Go
通过在函数和方法中返回错误对象作为它们的唯一或最后一个返回值——如果返回 nil
,则没有错误发生——并且主调(calling
)函数总是应该检查收到的错误。
在前面的章节中我们了解了 Go
检查和报告错误条件的惯有方式:
- 产生错误的函数会返回两个变量,一个值和一个错误码;如果后者是
nil
就是成功,非nil
就是发生了错误。 - 为了防止发生错误时正在执行的函数(如果有必要的话甚至会是整个程序)被中止,在调用函数后必须检查错误。
协程(goroutine)与通道(channel)
不要通过共享内存来通信,而通过通信来共享内存。
并发、并行和协程
- 并发一个应用程序是运行在机器上的一个进程;
进程是一个运行在自己内存地址空间里的独立执行体。一个进程由一个或多个操作系统线程组成,这些线程其实是共享同一个内存地址空间的一起工作的执行体。
几乎所有’正式’的程序都是多线程的,以便让用户或计算机不必等待,或者能够同时服务多个请求(如 Web
服务器),或增加性能和吞吐量(例如,通过对不同的数据集并行执行代码)。一个并发程序可以在一个处理器或者内核上使用多个线程来执行任务,但是只有同一个程序在某个时间点同时运行在多核或者多处理器上才是真正的并行。
并行是一种通过使用多处理器以提高速度的能力。所以并发程序可以是并行的,也可以不是。
协程多线程应用难以做到对内存中的数据共享,他们会被多线程操作。
在 Go
的标准库 sync
中有一些工具用来在低级别的代码中实现加锁;然而会导致更高的复杂的和更低的性能。所以不适用于现代多核/多处理器编程:thread-per-connection
模型不够有效。
在 Go
中,应用程序并发处理的部分被称作 goroutines
(协程),它可以进行更有效的并发运算。在协程和操作系统线程之间并无一对一的关系:协程是根据一个或多个线程的可用性,映射(多路复用,执行于)在他们之上的;协程调度器在 Go
运行时很好的完成了这个工作。
协程工作在相同的地址空间中,所以共享内存的方式一定是同步的;这个可以使用 sync
包来实现(不推荐),Go
使用 channels
来同步协程。
当系统调用(比如等待 I/O
)阻塞协程时,其他协程会继续在其他线程上工作。协程是轻量的,比线程更轻。协程对杖进行了分割,从而动态的增加(缩减)内存的使用。杖的管理是自动的(不会出现杖溢出),在协程退出后自动释放,不是由垃圾回收机制
来管理。
并发方式:确定性的(明确定义排序)和非确定性的(加锁/互斥从而未定义排序)。
并发和并行的差异
- 并发
- 强调的是任务调度,在单核
CPU
上实现 - 将程序中的多个任务分成小块,在处理器直接切换执行,在执行一个任务时不一定需要等待任务完成再执行下一个任务,可以在等待的过程中处理其他任务。
- 强调的是任务调度,在单核
- 并行
- 在多核
CPU
上运行多个任务,每个任务在不同的核心上独立运动。 - 要求硬件支持,比如多核或多处理器系统。
- 并行可以显著提高程序性能,因为多个任务同时进行,整体执行时间更短。
- 协程需要使用
GOMAXPROCS
变量
- 在多核
使用 GOMAXPROCS
所有的协程都会共享同一个线程除非将 GOMAXPROCS
设置为一个大于 1
的数。当 GOMAXPROCS
大于 1
时,会有一个线程池管理许多的线程。
有这样一个经验法则,对于 n
个核心的情况设置 GOMAXPROCS
为 n-1
以获得最佳性能,也同样需要遵守这条规则:协程的数量 > 1 + GOMAXPROCS > 1
。
Go 协程(goroutines)和协程(coroutines)
Go
协程意味着并行(或者可以以并行的方式部署),协程一般来说不是这样的Go
协程通过通道来通信;协程通过让出和恢复操作来通信
协程间的通道
Go
协程一般不是独立运行的,每个 goroutine
运行在由于 Go
运行时调度的线程上,多个线程可以共享同一个操作系统线程。
通过 通道
,来负责协程直接的通信,避开所有由共享内存导致的陷阱,保证同步性。数据在通道中传递,任何时间,一个数据只有一个线程协程可以对其访问,不会发生数据竞争。
通常使用这样的格式来声明通道:var identifier chan datatype
,未初始化的通道的值是 nil
。
通道实际上是类型化消息的队列:使数据得以传输。它是先进先出(FIFO
)的结构,所以可以保证发送给他们的元素的顺序。通道也是引用类型,所以我们使用 make() 函数来给它分配内存。
通信操作符 <-
这个操作符直观的标示了数据的传输:信息按照箭头的方向流动。
流向通道(发送)
ch <- int1
表示:用通道ch
发送变量int1
(双目运算符,中缀 = 发送)从通道流出(接收)
int2 = <- ch
表示:变量int2
从通道ch
(一元运算的前缀操作符,前缀 = 接收)接收数据(获取新值);假设 int2 已经声明过了,如果没有的话可以写成:int2 := <- ch
。<- ch
可以单独调用获取通道的(下一个)值,当前值会被丢弃,但是可以用来验证,所以以下代码是合法的:1
2
3if <- ch != 1000{
...
}
通道阻塞
默认情况下,通信是同步且无缓冲的:
对于同一个通道,发送操作(协程或者函数中的),在接收者准备好之前是阻塞的:如果 ch 中的数据无人接收,就无法再给通道传入其他数据:新的输入无法在通道非空的情况下传入。所以发送操作会等待 ch 再次变为可用状态:就是通道值被接收时(可以传入变量)。
对于同一个通道,接收操作是阻塞的(协程或函数中的),直到发送者可用:如果通道中没有数据,接收者就阻塞了。
通过一个(或多个)通道交换数据进行协程同步
通信是一种同步形式:通过通道,两个协程在通信(协程会和)中某刻同步交换数据。无缓冲通道成为了多个协程同步的完美工具。
无缓冲通道会被阻塞。设计无阻塞的程序可以避免这种情况,或者使用带缓冲的通道。
同步通道 - 使用带缓冲的通道
一个无缓冲通道只能包含 1 个元素,有时显得很局限。我们给通道提供了一个缓存,可以在扩展的 make
命令中设置它的容量,如下:
1 | buf := 100 |
buf
是通道可以同时容纳的元素(这里是 string
)个数,在缓冲满载(缓冲被全部使用)之前,给一个带缓冲的通道发送数据是不会阻塞的,而从通道读取数据也不会阻塞,直到缓冲空了。
同步:ch := make(chan type, value)
- value == 0 -> synchronous, unbuffered (阻塞)
- value > 0 -> asynchronous, buffered(非阻塞)取决于 value 元素
协程中用通道输出结果
用带缓冲通道实现一个信号量
信号量是实现互斥锁(排外锁)常见的同步机制,限制对资源的访问,解决读写问题,比如没有实现信号量的 sync 的 Go 包,使用带缓冲的通道可以轻松实现:
- 带缓冲通道的容量和要同步的资源容量相同
- 通道的长度(当前存放的元素个数)与当前资源被使用的数量相同
- 容量减去通道的长度就是未处理的资源个数(标准信号量的整数值)
通道的方向
通道类型可以用注解来表示它只发送或者只接收:
1 | var send_only chan<- int // channel can only receive data |
只接收的通道(<-chan T)无法关闭,因为关闭通道是发送者用来表示不再给通道发送值了,所以对只接收通道是没有意义的。
协程的同步:关闭通道-测试阻塞的通道
只有在当需要告诉接收者不会再提供新的值的时候,才需要关闭通道。只有发送者需要关闭通道,接收者永远不会需要
通过 select 切换协程
从不同的并发执行的协程中获取值可以通过关键字 select
来完成,它和 switch
控制语句非常相似也被称作通信开关;它的行为像是“你准备好了吗”的轮询机制;select
监听进入通道的数据,也可以是用通道发送值的时候。
1 | select { |
一般不加 break
或者 return
,默认自带。
- 如果都阻塞了,会等待直到其中一个可以处理
- 如果多个可以处理,随机选择一个
- 如果没有通道操作可以处理并且写了
default
语句,它就会执行:default
永远是可运行的(这就是准备好了,可以执行)。
通道、超时和计时器(Ticker)
time.Ticker
结构体,这个对象以指定的时间间隔重复的向通道 C 发送时间值:
1 | type Ticker struct { |
时间间隔的单位是 ns(纳秒,int64),在工厂函数 time.NewTicker
中以 Duration
类型的参数传入:func NewTicker(dur) *Ticker
。
在协程周期性的执行一些事情(打印状态日志,输出,计算等等)的时候非常有用。
调用 Stop()
使计时器停止,在 defer
语句中使用。
1 | ticker := time.NewTicker(updateInterval) |
协程和恢复(recover)
1 | func server(workChan <-chan *Work) { |
新旧模型对比:任务和 worker
- 旧模式:使用共享内存进行同步
由各个任务组成的任务池共享内存;为了同步各个 worker
以及避免资源竞争,我们需要对任务池进行加锁保护。
1 | type Pool struct { |
sync.Mutex
:它用来在代码中保护临界区资源:同一时间只有一个 go
协程(goroutine
)可以进入该临界区。如果出现了同一时间多个 go
协程都进入了该临界区,则会产生竞争:Pool
结构就不能保证被正确更新。
1 | func Worker(pool *Pool) { |
- 新模式:使用通道
使用通道进行同步:使用一个通道接受需要处理的任务,一个通道接受处理完成的任务(及其结果)。worker
在协程中启动,其数量 N
应该根据任务数量进行调整。
1 | func main() { |
worker 的逻辑比较简单:从 pending 通道拿任务,处理后将其放到 done 通道中:
1 | func Worker(in, out chan *Task) { |
如何选择锁和通道
使用锁的情景:
- 访问共享数据结构中的缓存信息
- 保存应用程序上下文和状态信息数据
使用通道的情景:
- 与异步操作的结果进行交互
- 分发任务
- 传递数据所有权
惯性生成器
1 | package main |
实现 Futures 模式
所谓 Futures 就是指:有时候在你使用某一个值之前需要先对其进行计算。这种情况下,你就可以在另一个处理器上进行该值的计算,到使用时,该值就已经计算完毕了。
Futures 模式通过闭包和通道可以很容易实现,类似于生成器,不同地方在于 Futures 需要返回一个值。
复用
典型的客户端/服务器(C/S)模式
客户端-服务器应用正是 goroutines
和 channels
的亮点所在。
使用 Go
的服务器通常会在协程中执行向客户端的响应,故而会对每一个客户端请求启动一个协程。一个常用的操作方法是客户端请求自身中包含一个通道,而服务器则向这个通道发送响应。
卸载(Teardown):通过信号通道关闭服务器
1 | package main |
限制同时处理的请求数
通过这种方式,应用程序可以通过使用缓冲通道(通道被用作信号量)使协程同步其对该资源的使用,从而充分利用有限的资源(如内存)。
1 | package main |
链式协程
在多核心上并行计算
并行化大量数据的计算
漏桶算法
对 Go 协程进行基准测试
testing.Benchmark()
网络、模版与网页应用
TCP 服务器
net
包
web 服务器
net/http
包http.ListenAndServe("localhost:8080", nil)
启动服务req.FormValue("var1")
获取请求参数request.ParseForm(), request.Form["var1"]
获取请求参数
1 | package main |
访问并读取页面数据
发送一个简单的 http.Head()
请求查看返回值;它的声明如下:func Head(url string) (r *Response, err error)
学习路径
基础:
进阶:
面试: