文章目录
NoteBook : advanced-go-programming-book.pdf
1.go基础知识
在Go语言中,函数参数都是以复制的方式(不支持以引用的方式)传递(比较特殊的是,Go语言闭包函数
对外部变量是以引用的方式
使用
导入包:
- import(. "fmt” )
这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的fmt.Println(“hello world”)可以省略的写成Println(“hello world”)
别名操作别名操作顾名思义我们可以把包命名成另一个我们用起来容易记忆的名字
-
import( f “fmt” )别名操作的话调用包函数时前缀变成了我们的前缀,即
f.Println(“hello world”) -
_ 操作这个操作经常是让很多人费解的一个操作符,请看下面这个import
import (
“database/sql”
_ “github.com/ziutek/mymysql/godrv”
)
_ 操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数, 只要在包中声明了init函数,引用这个包的时候该init函数就会自动被调用,像包中的常量和变量一样被初始化执行
init不是普通函数,可以定义有多个,所以也不能被其它函数调用
字符串:
type StringHeader struct {
Data uintptr
Len int
}
使用反引号 ` 作为原始字符串符号:
s := Starting part Ending part
- []byte(s)转换模拟实现
func str2bytes(s string) []byte {
p := make([]byte, len(s))
for i := 0; i < len(s); i++ {
c := s[i]
p[i] = c
}
return p
}
- string(bytes)转换模拟实现
func bytes2str(s []byte) (p string) {
data := make([]byte, len(s))
for i, c := range s {
data[i] = c
}
hdr := (*reflect.StringHeader)(unsafe.Pointer(&p))
hdr.Data = uintptr(unsafe.Pointer(&data[0]))
hdr.Len = len(s)
return p
}
- []rune(s)转换模拟实现
func str2runes(s string) []rune{
var p []int32
for len(s)>0 {
r,size:=utf8.DecodeRuneInString(s)
p=append(p,int32(r))
s=s[size:]
}
return []rune(p)
}
- string(runes)转换模拟实现
func runes2string(s []int32) string {
var p []byte
buf := make([]byte, 3)
for _, r := range s {
n := utf8.EncodeRune(buf, r)
p = append(p, buf[:n]...)
}
return string(p)
}
recover函数:
recover仅在延迟函数中有效。
必须要和有异常的栈帧只隔一个栈帧,recover函数才能正常捕获异常。换言之,recover函数捕获的是祖父一级调用函数栈帧的异常(刚好可以跨越一层defer函数)!
如果我们直接在defer语句中调用MyRecover函数又可以正常工作了:
// 正常工作
func MyRecover() interface{} {
return recover()
}
func main() {
// 可以正常捕获异常
defer MyRecover()
panic(1)
}
内建函数:
-
内建函数 new 本质上说跟其他语言中的同名函数功能一样:new(T) 分配了零值填充的 T 类型的内存空间,并且返回其地址,
一个 *T 类型的值
。用 Go 的术语说,它返回了一个指针,指向新分配的类型 T 的零值。 -
内建函数 make(T, args) 与 new(T) 有着不同的功能。它只能创建slice,map 和 channel,并且返回
一个有初始值(非零)的 T 类型,而不是 *T
。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。 -
Go 不是面向对象语言,因此并没有继承。但是有时又会需要从已经实现的类型中“继承”并修改一些方法。在 Go 中可以用
嵌入一个类型
的方式来实现组合: type PrintableMutex struct {Mutex } — PrintableMutex 已经从 Mutex 继承了方法集合
接口:
- 对于接口 I,S 是合法的实现,因为它定义了 I 所需的两个方法。注意,即便是没有明确定义 S 实现了 I,这也是正确的
- var _ io.Writer = (*myWriter)(nil),
编译器自动检测类型是否实现接口
- 例如某类型有 m 个方法,某接口有 n 个方法,则很容易知道这种判定的时间复杂度为 O(mn),Go 会对方法集的函数按照函数名的字典序进行排序,所以实际的时间复杂度为 O(m+n)
并发:
channel作为变量传入的形式: func dup3(in <−chan int) (<−chan int, <−chan int, <−chan int) 解释: 传入一个channel,返回三个channel
Context 可以用来通知后台goroutine的退出,防止泄露;
4.基于sync.Once
的单例模式: 原子操作配合互斥锁可以实现非常高效的单件模式,互斥锁的代价比普通整数的原子读写高很多,在性能敏感的地方可以增加一个数字型的标志位,通过原子检测标志位状态降低互斥锁的使用次数来提高性能
type Once struct {
m Mutex
done uint32
}
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 1 {
return
}
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
sync/atomic
包对基本的数值类型及复杂对象的读写都提供了原子操作(顺序一致性内存模型为前提)的支持。atomic.Value原子对象提供了Load和Store两个原子方法,分别用于加载和保存数据,返回值和参数都是interface{}类型,因此可以用于任意的自定义复杂类型。
并发模型:
不要通过共享内存来通信,而应通过通信来共享内存
根据Go语言内存模型规范,对于从无缓冲Channel进行的接收,发生在对该Channel进行的发送完成之前
对于带缓冲的Channel,对于Channel的第K个接收完成操作发生在第K+C个发送操作完成之前,其中C是Channel的缓存大小
- 发布订阅模型
- 生产者消费者模型
切片操作:
切片的结构定义,reflect.SliceHeader:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
切片操作会导致整个底层的数据被锁定,得不到释放。
- 解决办法: 重新克隆一份得到切片内容
方法的定义:
- 不管方法的接收者是什么类型,该类型的值和指针都可以调用;
常见问题:
- 闭包错误引用同一个变量 — 解决办法 :生成一个局部变量赋值
- 在循环内部使用defer — 解决办法:在局部函数内部执行defer
- 不同goroutine之前不满足顺序一致性内存模型 — 解决办法:显示同步,使用channel
- 死循环会独占cpu导致其他goroutine饿死 — 解决办法:加入runtime.Gosched()调度函数
- goroutine是协作式调度,goroutine本身不会主动放弃cpu
- recover必须在defer函数中运行, recover捕获的是祖父级调用异常,直接调用无效
- goroutine竞争检测
golang在1.1之后引入了竞争检测的概念。我们可以使用go run -race 或者 go build -race 来进行竞争检测。 - 测试类的使用
- 唤醒锁: sync.Cond
p.cond = sync.NewCond(&p.lock)
2. CGO编程
快速入门:
全部是Go语言代码,但是执行的时候是先从Go语言的main函数,到CGO自动生成的C语言版本SayHello桥接函数,最后又回到了Go语言环境的SayHello函数。这个代码包含了CGO编程的精华
// +build go1.10
package main
//void SayHello(_GoString_ s);
import "C"
import (
"fmt"
)
func main() {
C.SayHello("Hello, World\n")
}
//export SayHello
func SayHello(s string) {
fmt.Print(s)
}
TODO…
3. go汇编语言
go tool compile -S pkg.go
TODO …
4. rpc和protobuf
TODO…
5. Go和Web
Go的Web框架大致可以分为这么两类:
- Router框架
- MVC类框架
router原理:
httprouter和众多衍生router使用的数据结构被称为压缩字典树(Radix Tree)。读者可能没有接触过压缩字典树,但对字典树(Trie Tree)应该有所耳闻,压缩字典树:每个节点上不只存储一个字母了,这也是压缩字典树中“压缩”的主要含义。使用压缩字典树可以减少树的层数,同时因为每个节点上数据存储也比通常的字典树要多,所以程序的局部性较好(一个节点的path加载到cache即可进行多个字符的对比),从而对CPU缓存友好。
中间件(middleware):
validator请求校验参数:
ORM和SQL Builder:
对象关系映射(英语:Object Relational Mapping,简称ORM,或O/RM,或O/R mapping),
是一种程序设计技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换。
从效果上说,它其实是创建了一个可在编程语言里使用的“虚拟对象数据库”。
RateLimit服务流量限制:
流量限制的手段有很多,最常见的:漏桶、令牌桶两种:
- 漏桶是指我们有一个一直装满了水的桶,每过固定的一段时间即向外漏一滴水。如果你接到了这滴水,那么你就可以继续服务请求,如果没有接到,那么就需要等待下一滴水。
- 令牌桶则是指匀速向桶中添加令牌,服务请求时需要从桶中获取令牌,令牌的数目可以按照需要消耗的资源进行相应的调整。如果没有令牌,可以选择等待,或者放弃。
这两种方法看起来很像,不过还是有区别的。漏桶流出的速率固定,而令牌桶只要在桶中有令牌,那就可以拿。也就是说令牌桶是允许一定程度的并发的,比如同一个时刻,有100个用户请求,只要令牌桶中有100个令牌,那么这100个请求全都会放过去。令牌桶在桶中没有令牌的情况下也会退化为漏桶模型。
MVC框架:
MVC这个概念最早由Trygve Reenskaug在1978年提出,为了能够对GUI类型的应用进行方便扩展,将程序划分为:
- 控制器(Controller)- 负责转发请求,对请求进行处理。
- 视图(View) - 界面设计人员进行图形界面设计。
- 模型(Model) - 程序员编写程序应有的功能(实现算法等等)、数据库专家进行数据管理和数据库设计(可以实现具体的功能)。
前后分离交互图:
接口和表驱动开发:
接口
带给我们的好处也是不言而喻的:一是依赖反转,这是接口在大多数语言中对软件项目所能产生的影响,在Go的正交接口的设计场景下甚至可以去除依赖;二是由编译器来帮助我们在编译期就能检查到类似“未完全实现接口”这样的错误
表驱动的设计方式
,很多设计模式相关的书籍并没有把它作为一种设计模式来讲,但我认为这依然是一种非常重要的帮助我们来简化代码的手段。在日常的开发工作中可以多多思考,哪些不必要的switch case可以用一个字典和一行代码就可以轻松搞定。
当然,表驱动也不是缺点,因为需要对输入key计算哈希,在性能敏感的场合,需要多加斟酌
func entry() {
var bi BusinessInstance
switch businessType {
case TravelBusiness:
bi = travelorder.New()
case MarketBusiness:
bi = marketorder.New()
default:
return errors.New("not supported business")
}
}
可以修改为表驱动的设计方式:
var businessInstanceMap = map[int]BusinessInstance {
TravelBusiness : travelorder.New(),
MarketBusiness : marketorder.New(),
}
func entry() {
bi := businessInstanceMap[businessType]
}
灰度发布和 A/B test:
互联网系统的灰度发布一般通过两种方式实现:
灰度发布:在大型系统中容错是重要的,能够让系统按百分比,分批次到达最终用户,也是很重要的
实现方式:
-
通过分批次部署实现灰度发布
-
通过业务规则进行灰度发布
可选业务规则:
- 按城市发布
- 按概率发布
- 按百分比发布
- 按白名单发布
- 按业务线发布
- 按UA发布(APP、Web、PC)
- 按分发渠道发布
实现灰度发布系统
灰度系统(函数)
- map来存储映射关系
- 哈希算法: 使用较多的算法是 murmurhash, 性能好,分布均匀
6.分布式系统
基于redis的setnx(“SET if Not eXists”)操作:
基于ZooKeeper:
package main
import (
"time"
"github.com/samuel/go-zookeeper/zk"
)
func main() {
c, _, err := zk.Connect([]string{"127.0.0.1"}, time.Second) //*10)
if err != nil {
panic(err)
}
l := zk.NewLock(c, "/lock", zk.WorldACL(zk.PermAll))
err = l.Lock()
if err != nil {
panic(err)
}
println("lock succ, do your business logic")
time.Sleep(time.Second * 10)
// do some thing
l.Unlock()
println("unlock succ, finish business logic")
}
这种分布式的阻塞锁比较适合分布式任务调度
场景,但不适合高频次持锁时间短
的抢锁场景。按照Google的Chubby论文里的阐述,基于强一致协议
的锁适用于粗粒度
的加锁操作。这里的粗粒度指锁占用时间较长
基于etcd:
package main
import (
"log"
"github.com/zieckey/etcdsync"
)
func main() {
m, err := etcdsync.New("/lock", 10, []string{"http://127.0.0.1:2379"})
if m == nil || err != nil {
log.Printf("etcdsync.New failed")
return
}
err = m.Lock()
if err != nil {
log.Printf("etcdsync.Lock failed")
return
}
log.Printf("etcdsync.Lock OK")
log.Printf("Get the lock. Do something here.")
err = m.Unlock()
if err != nil {
log.Printf("etcdsync.Unlock failed")
} else {
log.Printf("etcdsync.Unlock OK")
}
}
延时任务系统:
一般有两种思路来解决这个问题:
- 实现一套类似crontab的分布式定时任务管理系统。
- 实现一个支持定时发送消息的消息队列。
定时器(timer)
的实现在工业界已经是有解的问题了。常见的就是时间堆和时间轮。
-
时间堆一般用小顶堆实现,小顶堆其实就是一种特殊的二叉树,
-
时间轮来实现定时器时,我们需要定义每一个格子的“刻度”,可以将时间轮想像成一个时钟,中心有秒针顺时针转动。每次转动到一个刻度时,我们就需要去查看该刻度挂载的任务列表是否有已经到期的任务,从结构上来讲,时间轮和哈希表很相似,如果我们把哈希算法定义为:触发时间%时间轮元素大小。那么这就是一个简单的哈希表。在哈希冲突时,采用链表挂载哈希冲突的定时器。
分布式搜索引擎:
elasticsearch
负载均衡:
- 基于洗牌算法的负载均衡: 从数学上得到过证明的还是经典的fisher-yates算法,主要思路为每次随机挑选一个值,放在数组末尾。然后在n-1个元素的数组中再随机挑选一个值,放在数组末尾,以此类推。
var endpoints = []string {
"100.69.62.1:3232",
"100.69.62.32:3232",
"100.69.62.42:3232",
"100.69.62.81:3232",
"100.69.62.11:3232",
"100.69.62.113:3232",
"100.69.62.101:3232",
}
var indexes = []int {0,1,2,3,4,5,6}
func init() {
rand.Seed(time.Now().UnixNano())
}
// 重点在这个 shuffle
func shuffle() {
// b := rand.Perm(n)
for i:=len(indexes); i>0; i-- {
lastIdx := i - 1
idx := rand.Intn(i)
indexes[lastIdx], indexes[idx] = indexes[idx], indexes[lastIdx]
}
}
func request(params map[string]interface{}) error {
var err error
shuffle()
maxRetryTimes := 3
idx := 0
for i := 0; i < maxRetryTimes; i++ {
err = apiRequest(params, indexes[idx])
if err == nil {
break
}
idx++
}
if err != nil {
// logging
return err
}
return nil
}
7. 总结
数组是值传递
在函数调用参数中,数组是值传递,无法通过修改数组类型的参数返回结果。必要时需要使用切片
。
x := [3]int{1, 2, 3} - 定义了一个数组
x := []int{1, 2, 3} - 定义了一个slice
-
map遍历是顺序不固定,map是一种hash表实现,每次遍历的顺序都可能不一样
-
for{ }独占CPU导致其它Goroutine饿死:
Goroutine是协作式抢占调度,Goroutine本身不会主动放弃CPU
,解决的方法是在for循环加入runtime.Gosched()调度函数
或者通过阻塞的方式select{}
避免CPU占用:
fmt.Println(runtime.NumCPU())
runtime.GOMAXPROCS(1)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}()
// for {} // 独自占用了CPU
select {} // 不占用CPU
-
不同Goroutine之间不满足顺序一致性内存模型: 解决的办法是用显式同步 channel
-
闭包错误引用同一个变量: 改进的方法是在每轮迭代中生成一个局部变量,或者是通过函数参数传入
// 每轮迭代中生成一个局部变量
func main() {
for i := 0; i < 5; i++ {
i := i
defer func() {
println(i)
}()
}
}
// 或者是通过函数参数传入:
func main() {
for i := 0; i < 5; i++ {
defer func(i int) {
println(i)
}(i)
}
}
-
在循环内部执行defer语句: defer在函数退出时才能执行,在for执行defer会导致资源延迟释放,解决的方法可以在for中构造一个局部函数,在局部函数内部执行defer。recover捕获的是
祖父级调用时的异常
,直接调用时无效, recover必须在defer函数中运行 -
切片会导致整个底层数组被锁定: 切片会导致整个底层数组被锁定,底层数组无法释放内存。如果底层数组较大会对内存产生很大的压力,解决的方法是将结果克隆一份,这样可以释放底层的数组
newBytesArray := append([]byte{}, data[:1]...)
- 空指针和空接口不等价
// 比如返回了一个错误指针,但是并不是空的error接口:
func returnsError() error {
var p *MyError = nil
// 以上声明同理 var p *MyError, p都是nil, 但是空的接口并不是空的指针nil
// 但是注意通过 var p MyError 的声明,p就不是nil了, 是一个空的struct
if bad() {
p = ErrBad
}
return p // Will always return a non-nil error. 始终是非空的error
}
- 内存地址会变化:Go语言中对象的地址可能发生变化,因此指针不能从其它非指针类型的值生成:
func main() {
var x int = 42
var p uintptr = uintptr(unsafe.Pointer(&x))
runtime.GC()
var px *int = (*int)(unsafe.Pointer(p))
// *px 的值可能与x 不一样, GC后, x的内存地址可能发生变化,如果值更新后,但是p(uintptr)不会同步更新
// 导致由p生成的px指针指向的值与x的更新不一致
println(*px)
}
当内存发送变化的时候,相关的指针会同步更新,但是非指针类型的uintptr不会做同步更新。