原文:http://tianyalong.icu/content.html?id=3
1.1 Go语言创世纪
Go语言最初由Google公司的Robert Griesemer、Ken Thompson和Rob Pike三个大牛于2007年开始设计发明,设计新语言的最初的洪荒之力来自于对超级复杂的C++11特性的吹捧报告的鄙视
,最终的目标是设计网络和多核时代的C语言。
1.4 函数、方法、接口
函数
有具名函数
和匿名函数
之分:具名函数
:包级函数一般是具名函数,具名函数是匿名函数的一种特例。匿名函数
:匿名函数引用
了外部作用域中的变量时就成了闭包函数
方法
是绑定到一个具体类型
的特殊函数,Go语言中的方法是依托于类型的,必须在编译时静态绑定
。接口
定义了方法
的集合,这些方法依托于运行时的接口对象,因此接口对应的方法是在运行时动态绑定
的.包初始化流程
,先执行main
包下的main
函数,但如果导入了其他包
,先初始化其他包的常量
、再初始化其他包的变量
,再执行其他包的init
函数,最后初始化main包中的常量、变量,再执行init函数。(注意
:main.main函数执行之前,所有代码都在同一个
goroutine中运行,所以如果使用go关键字创建新的goroutine,新的goroutine只有在进入main.main之后才会被执行)- 传入参数和返回参数可以有多个,函数调用都是
值传递
。可变数量
的参数必须是最后
出现的参数,可变数量的参数其实是一个切片类型
的参数。
当可变参数是一个空接口类型
时,调用者是否解包
可变参数会导致不同的结果:
func main(){
var a = []interface{}{123, "abc"}
Print(a...) //123 abc
Print(a) //[123 abc]
}
func Print(a ...interface{}) {
fmt.Println(a...)
}
- 可以给函数的
返回值
命名,如果返回值命名了,可以通过名字来修改返回值,也可以通过defer
语句在return
语句之后修改返回值。闭包在捕获外界变量时不是值传递的方式访问,而是以引用
的方式访问。
func Inc() (v int) {
defer func(){ v++ }()
return 42
}
最终返回43
会出现如下隐患:
func main() {
for i := 0; i < 3; i++ {
defer func(){ println(i) }()
}
}
output:
3
3
3
有两种方式可以解决上述问题:
//第一种
func main() {
for i := 0; i < 3; i++ {
defer func(i int){ println(i) }()
}
}
//第二种
func main() {
for i := 0; i < 3; i++ {
i := i
defer func(){ println(i) }()
}
}
- 每个goroutine刚启动时只会分配很小的栈(
4或8KB
,具体依赖实现),根据需要动态调整
栈的大小,栈最大可以达到GB级(依赖具体实现,在目前的实现中,32位
体系结构为250MB
,64位
体系结
构为1GB
),程序员不需要关注内存分配
在栈中还是分配在堆中,由程序自己决定。 - 在Go1.4以前,Go的动态栈采用的是
分段式
的动态栈,通俗地说就是采用一个链表来实现动态栈,每个链表的节点内存位置不会发生变化
,但由于内存地址不连续对读取性能影响比较大。Go1.4之后改用连续
的动态栈实现,也就是采用一个类似动态数组的结构来表示栈,就存在扩容到一定程度之后需要将整个栈迁移
寻找合适的位置,所以内存地址是不固定的
。 - Go语言中,通过在结构体
内置匿名的成员
来实现继承
,通过嵌入匿名的成员,我们不仅可以继承匿名成员的内部成员,而且可以继承
匿名成员类型所对应的方 法
。:
type Cache struct {
m map[string]string
sync.Mutex
}
func (p *Cache) Lookup(key string) string {
p.Lock()
defer p.Unlock()
return p.m[key]
}
Cache
结构体类型通过嵌入一个匿名的 sync.Mutex
来继承它的 Lock
和 Unlock
方法. 但是在调用 p.Lock()
和 p.Unlock()
时,p
并不是Lock
和Unlock
方法的真正接收者, 而是会将它们展开
为 p.Mutex.Lock()
和 p.Mutex.Unlock()
调用. 这种展开是编译期
完成的, 并没有运行时代价
.
1. fmt.Printf是完全基于接口的,真正执行是依赖于fmt.Fprintf函数,C语言只能打印基础类型,而go灵活的接口特性,可以打印任意对象。
1. error是内置的错误接口
,任何实现error的类型都可以作为error的返回值。
1. 类型和该类型的方法,必须在同一个包中
。
1. 种通过嵌入匿名接口
或嵌入匿名指针
对象来实现继承的做法其实是一种纯虚继承
,我们继承的只是接口指定的规范,真正的实现在运行的时候才被注入。
1.5 面向并发的内存模型
为什么go要自己维护一个runtime?
- 一是对于很多只需要很小的栈空间的线程来说是一个巨大的浪费
- 二是对于少数需要巨大栈空间的线程来说又面临栈溢出的风险。
解决方法就是一个Goroutine会以一个很小的栈启动,当遇到深度递归导致当前栈空间不足时,Goroutine会根据需要动态地伸缩栈的大小。因为启动的代价很小,所以我们可以轻易地启动成千上万个Goroutine。
- Goroutine采用的是
半抢占式
的协作调度
,只有在当前Goroutine发生阻塞
时才会导致调度;同时发生在用户态,调度器会根据具体函数只保存必要
的寄存器,切换的代价要比系统线程低得多。运行时有一个runtime.GOMAXPROCS
变量,用于控制当前运行正常非阻塞Goroutine的系统线程数目。
原子操作
所谓的原子操作就是并发编程中“最小的且不可并行化”的操作
1. atomic.Value
原子对象提供了 Load
和 Store
两个原子方法,分别用于加载和保存数据,返回值和参数都是 interface{}
类型,因此可以用于任意
的自定义复杂类型。
2. 通过channel或者sync.Mutex可以防止指令重排序。
3. 从已经关闭
的无缓冲
channel中获取值会返回零值。
1. 对于从无缓冲
Channel进行的接收
,发生在对该Channel进行的发送
完成之前。
1. 对于有缓冲
的channel,有值时能接收到,没有值会阻塞,从关闭的有缓冲channel中读取到零值,如果值已经达到了最大缓存数,写入操作会阻塞。
常见的并发模型
- sync.Mutex
func main() {
var mu sync.Mutex
mu.Lock()
go func(){
fmt.Println("你好, 世界")
mu.Unlock()
}()
mu.Lock()
}
- channel
func main() {
done := make(chan int, 1) // 带缓存的管道
go func(){
fmt.Println("你好, 世界")
done <- 1
}()
<-done
}
- sync.WaitGroup
func main() {
var wg sync.WaitGroup
// 开N个后台打印线程
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
fmt.Println("你好, 世界")
wg.Done()
}()
}
// 等待N个后台线程完成
wg.Wait()
}
生产者消费者模型
package main
import (
"fmt"
"time"
)
func Producer(factor int, out chan<- int) {
for i := 0; ; i++ {
out <- i * factor
}
}
// 消费者
func Consumer(in <-chan int) {
for v := range in {
fmt.Println(v)
}
}
func main() {
ch := make(chan int, 64) // 成果队列
go Producer(3, ch) // 生成 3 的倍数的序列
go Producer(5, ch) // 生成 5 的倍数的序列
go Consumer(ch) // 消费 生成的队列
// 运行一定时间后退出
time.Sleep(5 * time.Second)
}
发布订阅模型
简单概括:一个发布者包含多个接受者及其过滤消息方法,每次发布会向所有的订阅者发送消息。消费者如果想订阅,就将自己的channel以及过滤方法添加到发布者的map中去。
package pubsub
import (
"fmt"
"strings"
"sync"
"time"
)
type (
subscriber chan interface{} // 订阅者为一个管道
topicFunc func(v interface{}) bool // 主题为一个过滤器
)
// 发布者对象
type Publisher struct {
m sync.RWMutex // 读写锁
buffer int // 订阅队列的缓存大小
timeout time.Duration // 发布超时时间
subscribers map[subscriber]topicFunc // 订阅者信息
}
// 构建一个发布者对象, 可以设置发布超时时间和缓存队列的长度
func NewPublisher(publishTimeout time.Duration, buffer int) *Publisher {
return &Publisher{
buffer: buffer,
timeout: publishTimeout,
subscribers: make(map[subscriber]topicFunc),
}
}
// 添加一个新的订阅者,订阅全部主题
func (p *Publisher) Subscribe() chan interface{} {
return p.SubscribeTopic(nil)
}
// 添加一个新的订阅者,订阅过滤器筛选后的主题
func (p *Publisher) SubscribeTopic(topic topicFunc) chan interface{} {
ch := make(chan interface{}, p.buffer)
p.m.Lock()
p.subscribers[ch] = topic
p.m.Unlock()
return ch
}
// 退出订阅
func (p *Publisher) Evict(sub chan interface{}) {
p.m.Lock()
defer p.m.Unlock()
delete(p.subscribers, sub)
close(sub)
}
// 发布一个主题
func (p *Publisher) Publish(v interface{}) {
p.m.RLock()
defer p.m.RUnlock()
var wg sync.WaitGroup
for sub, topic := range p.subscribers {
wg.Add(1)
go p.sendTopic(sub, topic, v, &wg)
}
wg.Wait()
}
// 关闭发布者对象,同时关闭所有的订阅者管道。
func (p *Publisher) Close() {
p.m.Lock()
defer p.m.Unlock()
for sub := range p.subscribers {
delete(p.subscribers, sub)
close(sub)
}
}
// 发送主题,可以容忍一定的超时
func (p *Publisher) sendTopic(sub subscriber, topic topicFunc, v interface{}, wg *sync.WaitGroup) {
defer wg.Done()
if topic != nil && !topic(v) {
return
}
select {
case sub <- v:
case <-time.After(p.timeout):
}
}
func main() {
p := NewPublisher(100*time.Millisecond, 10)
defer p.Close()
all := p.Subscribe()
golang := p.SubscribeTopic(func(v interface{}) bool {
if s, ok := v.(string); ok {
return strings.Contains(s, "golang")
}
return false
})
p.Publish("hello, world!")
p.Publish("hello, golang!")
go func() {
for msg := range all {
fmt.Println("all:", msg)
}
}()
go func() {
for msg := range golang {
fmt.Println("golang:", msg)
}
}()
// 运行一定时间后退出
time.Sleep(3 * time.Second)
}
控制最大并发数
在Go语言自带的godoc程序实现中有一个 vfs
的包对应虚拟的文件系统,在 vfs
包下面有一个 gatefs
的子包, gatefs
子包的目的就是为了控制访问该虚拟文件系统的最大并发数。
实际是对channel操作做了一层封装。
type gatefs struct {
fs vfs.FileSystem
gate
}
func (g gate) enter() { g <- true }
func (g gate) leave() { <-g }
func (fs gatefs) Lstat(p string) (os.FileInfo, error) {
fs.enter()
defer fs.leave()
return fs.fs.Lstat(p)
}
赢者为王
例如从多个浏览器去搜索一个关键字,只要有一个先返回结果了,其他就可以终止了,因为其他浏览器(IE)可能到他倒闭都搜不出来。
func main() {
ch := make(chan string, 32)
go func() {
ch <- searchByBing("golang")
}()
go func() {
ch <- searchByGoogle("golang")
}()
go func() {
ch <- searchByBaidu("golang")
}()
fmt.Println(<-ch)
}
通过适当
开启一些冗余
的线程,尝试用不同途径去解决同样的问题,最终以赢者为王的方式提升了程序
的相应性能。
素数筛
一个channel负责不断地产生2到无穷大的整数,之后每次第一个数一定是质数,用该质数创建一个channel过滤器,不能除尽该质数的才能进入下一个管道。输出质数的管道一直在变化。
// 返回生成自然数序列的管道: 2, 3, 4, ...
func GenerateNatural() chan int {
ch := make(chan int)
go func() {
for i := 2; ; i++ {
ch <- i
}
}()
return ch
}
// 管道过滤器: 删除能被素数整除的数
func PrimeFilter(in <-chan int, prime int) chan int {
out := make(chan int)
go func() {
for {
if i := <-in; i%prime != 0 {
out <- i
}
}
}()
return out
}
func main() {
ch := GenerateNatural() // 自然数序列: 2, 3, 4, ...
for i := 0; i < 100; i++ {
prime := <-ch // 新出现的素数
fmt.Printf("%v: %v\n", i+1, prime)
ch = PrimeFilter(ch, prime) // 基于新素数构造的过滤器
}
}
并发的安全退出
当 select
有多个分支时,会随机
选择一个可用的管道分支,如果没有可用的管道分支则选择 default
分支,否则会一直保存阻塞
状态。
package test
func main() {
//基于 select 实现的管道的超时判断:
select {
case v := <-in:
fmt.Println(v)
case <-time.After(time.Second):
return
}
//通过 select 的 default 分支实现非阻塞的管道发送或接收操作:
select {
case v := <-in:
fmt.Println(v)
default:
// 没有数据
}
//通过 select 来阻止 main 函数退出:
select {}
}
通过WaitGroup和for+select实现安全的退出
func worker(wg *sync.WaitGroup, cannel chan bool) {
defer wg.Done()
for {
select {
default:
fmt.Println("hello")
case <-cannel:
return
}
}
}
func main() {
cancel := make(chan bool)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go worker(&wg, cancel)
}
time.Sleep(time.Second)
close(cancel)
wg.Wait()
}
context包
context
包,用来简化对于处理单个请求的多个Goroutine之间与请求域的数据
、超时
和退出
等操作。
func worker(ctx context.Context, wg *sync.WaitGroup) error {
defer wg.Done()
for {
select {
default:
fmt.Println("hello")
case <-ctx.Done():
return ctx.Err()
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go worker(ctx, &wg)
}
time.Sleep(time.Second)
cancel()
wg.Wait()
}
1.7 错误和异常
- map的读取
if v, ok := m["key"]; ok {
return v
}
- 打开连接后,如果这个连接最后需要关闭,使用
defer close
紧跟其后,保障在出现异常之后也能正常的进行关闭。 - 使用
recover
可以捕获异常,一般用于对一些panic
的报错,不希望他直接终止掉程序,而是转换成普通的err
继续运行。recover
只能在defer
中使用。 - 一般用
fmt.Errorf
对错误进行包装返回给上层用户。这样就对丢失底层的错误数据,而之后关于错误的一段描述。 - 通过自定义错误,可以获取到错误的调用信息、包装信息。
type Error interface {
Caller() []CallerInfo
Wraped() []error
Code() int
error
private()
}
type CallerInfo struct {
FuncName string
FileName string
FileLine int
}
func New(msg string) error
func NewWithCode(code int, msg string) error
func Wrap(err error, msg string) error
func WrapWithCode(code int, err error, msg string) error
func FromJson(json string) (Error, error)
func ToJson(err error) string
New
用于构建新的错误类型,和标准库中 errors.New
功能类似,但是增加了出错时的函数调用栈信息。 FromJson
用于从JSON字符串编码的错误中恢复错误对象。 NewWithCode
则是构造一个带错误码的错误,同时也包含出错时的函数调用栈信息。 Wrap
和 WrapWithCode
则是错误二次包装函数,用于将底层的错误包装为新的错误,但是保留的原始的底层错误信息。这里返回的错误对象都可以直接调用 json.Marshal
将错误编码为JSON字符串。
1. 必须要和有异常的栈帧只隔一个栈帧
, recover
函数才能正常捕获异常。换言之, recover
函数捕获的是祖父一级调用函数栈帧的异常。
func MyRecover() interface{} {
return recover()
}
func main() {
defer func() {
//无法捕获
if p := myrecover(); p != nil {
fmt.Println("...")
}
}()
panic(1)
}
func main() {
// 可以正常捕获异常
defer MyRecover()
panic(1)
}
func main() {
// 无法捕获异常
defer recover()
panic(1)
}
func main(){
//可以捕获到异常
defer func() {
if p := recover(); p != nil {
fmt.Println("...")
}
}
panic(1)
}