Go细节介绍

go env中GOARCH表示目标处理器架构,GOBIN表示编译器和链接器的安装位置,GOOS表示目标操作系统,GOPATH表示当前工作目录,GOROOT表示Go开发包的安装目录。

数据类型

int为64位,可指定数据宽度 byte = uint8,宽度最小 rune = int32 uintptrdouble,为float64 complex64实数虚数 complex128实数虚数

语言定义

{不能位于单独的行

符号定义

变量定义

  • var id+ [\[size\]*] type [= initVal+]

  • var id+ = [\[size\] type]initVal+ 自行判断type

  • id+ := [\[size\] type]initVal+

数组

id := [size]* type {
    
}

数组初始化还可以类似map的方式:{index: value},可以与顺序赋值混合使用:

数组的size在声明时可以为0,任意一维为0,则整个数组内存占用为0,于是可以有以下操作:

为range次数做法;

为管道同步操作

结构体数组key为结构体域

可以用fmt.Printf函数提供的%T%#v谓词语法来打印数组的类型和详细信息

数组长度len(arr),Go语言实现中非0大小数组的长度不得超过 2GB

字符串

源码:UTF-8编码,rune序列,但也可以通过byte访问

字符串相当于

可使用切片,len

字符串中可以使用"\x[num][num]"表示UTF-8字符,损坏的UTF-8字符被转换为\uFFFD,菱形问号错误符,但是正确字符仍然能够被正常访问到

切片

打印切片%v, %+v, %#v

声明指针在type前加*,空指针为nil

无size时为切片定义,其他定义方式:

  • var id []type 不定义长度

  • var id []type = make([]type, len) 定义长度

  • var id []type = make([]type, len, capacity) 定义长度,容量

初始化右值:

  • []type {num...}

  • arr[start:end]左闭右开,arr也有可能是切片,缺省值时为0len(arr)

  • arr[start:end:capacity]

  • len(slice)获取长度

  • cap(slice)获取容量

数组或切片作函数参数时可省略长度信息,空切片为nil

append(slicel, num...)尾部添加元素,添加其他切片所有元素append(slice, []int{nums}...) copy(slice1, slice2)拷贝切片内容到slice1头部,但是并不改变slice1大小

在中间插入元素x:

在中间插入切片x:

使用切片更方便地删除元素

在判断一个切片是否为空时,一般通过len获取切片的长度来判断,一般很少将切片和nil值做直接的比较

零长切片初始化后方便插入形成新切片

切片操作并不会复制底层的数据,因此只引用一小部分数据时会造成整个数组活跃而不被内存回收,因此可以采用复制操作规避损失append([]byte{}, rawSlice...)

删除操作时也有可能待删元素被引用,正确方式:

[]float64 类型的切片转换为 []int 类型的切片:

IEEE754浮点使得浮点数大小可以按照整型(64位)排序

map

map作为type时格式:map[type]type,声明也可使用

id := make(map[type]type)

加入元素为:

id[key] = value

删除元素:

delete(id, key)

可以进行类型转换,格式:typeName(expr)

对于基础类型不支持隐式类型转换

解包操作符:...,将切片解析为各个元素

常量定义

常量定义将变量定义var改为const

iota:被编译器修改的常量,在每次const出现时归0,内部使用时按行从0累加(即使该行未使用):

结构体定义

type作用类似C的typedef

接口定义

定义相同类型函数后使用:

channel

共享内存(array)

声明:

关闭通道:close(ch)

遍历通道使用range,在通道未能产生新的数时阻塞,通道被关闭时结束遍历操作

  • 无缓存的 Channel 上的发送操作总在对应的接收操作完成前发生,否则死锁;

  • 对于从无缓冲 Channel 进行的接收,发生在对该 Channel 进行的发送完成之前。

  • 对于带缓冲的Channel,对于 Channel 的第 K 个接收完成操作发生在第 K+C 个发送操作完成之前,其中 C 是 Channel 的缓存大小

  • 对于从无缓冲 Channel 进行的接收,发生在对该 Channel 进行的发送完成之前

分支

switch用于一个变量的多分枝判断选择,每次只能有一个条件成立

select不指定变量,case为条件语句,每次可能会有多个语句成立,select将会随机挑选执行,也可能没有语句成立,则会阻塞直到某分支能够执行

循环

格式:

  • for init; condition; post { }

  • for condition { }

  • for { }

  • for key := range array { }

  • for key, value := range array { }

  • for _, value := range array { }

函数

func funcName(varName type? [, varName type]) (?[[returnName]? returnType...])?

函数定义是参数相同类型的可以放在一起,在最后加type,支持多个返回值,多个返回值加()

go显然支持了C的函数指针,因此支持回调函数

可变数量的参数其实是一个切片类型的参数

切片中的底层数组部分是通过隐式指针传递(指针本身依然是传值的,但是指针指向的却是同一份的数据),所以被调用函数是可以通过指针修改掉调用参数切片中的数据,但是len和cap是传值的,无法修改调用者信息。因此通过返回修改后的切片来更新之前的切片。这也是为何内置的 append 必须要返回一个切片的原因。

go在递归调用深度上无限制,Go 语言运行时会根据需要动态地调整函数栈的大小,32 位体系结构为 250MB,64 位体系结构为 1GB)

  • <Go1.4,动态栈采用分段式,采用一个链表来实现动态栈,每个链表的节点内存位置不会发生变化。但是链表实现的动态栈对某些导致跨越链表不同节点的热点调用的性能影响较大,因为相邻的链表节点它们在内存位置一般不是相邻的,增加 CPU 高速缓存命中失败的几率

  • =Go1.4 改用连续的动态栈实现,也就是采用一个类似动态数组的结构来表示栈,问题:当连续栈动态增长时,需要将之前的数据移动到新的内存空间,这会导致之前栈中全部变量的地址发生变化

虽然 Go 语言运行时会自动更新引用了地址变化的栈变量的指针,但最重要的一点是要明白 Go 语言中指针不再是固定不变的了

不用关心 Go 语言中函数栈和堆的问题,编译器和运行时会帮我们搞定;同样不要假设变量在内存中的位置是固定不变的,指针随时可能会变化,特别是在你不期望它变化的时候。

函数定义

函数返回值也可以命名

在函数内也可进行嵌套的函数定义,复制给某个变量

defer语句

defer 语句延迟执行了一个匿名函数,因为这个匿名函数捕获了外部函数的局部变量 v,这种函数我们一般叫闭包。闭包对捕获的外部变量并不是传值方式访问,而是以引用的方式访问。

defer funcDef (param)

panic 能够改变程序的控制流,调用 panic 后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的 defer;

recover 可以中止 panic 造成的程序崩溃。它是一个只能在 defer 中发挥作用的函数,在其他作用域中调用不会发挥作用;

  • panic 只会触发当前 Goroutine 的 defer;

  • recover 只有在 defer 中调用才会生效;

  • panic 允许在 defer 中嵌套多次调用,多次调用 panic 也不会影响 defer 函数的正常执行;

嵌套调用的panic通过链表组织

go panic流程:

  1. 创建新的 runtime._panic 并添加到所在 Goroutine 的 _panic 链表的最前面;

  2. 在循环中不断从当前 Goroutine 的 _defer 中链表获取 runtime._defer 并调用 runtime.reflectcall 运行延迟调用函数;

  3. 调用 runtime.fatalpanic 中止整个程序;

go recover流程:

如果当前 Goroutine 没有调用 panic,那么该函数会直接返回 nil,这也是崩溃恢复在非 defer 中调用会失效的原因。

在正常情况下,它会修改 runtime._panic 的 recovered 字段,runtime.gorecover 函数中并不包含恢复程序的逻辑,程序的恢复是由 runtime.gopanic 函数负责

函数参数

函数可以作为另一个函数的参数,参数名后类型为函数形式

函数闭包

函数返回值定义为匿名函数,返回后可直接使用,且栈帧位置一致,多次调用操作同一函数空间

函数作为结构体方法

结构体可以定义有关方法:

若对结构体域有更改,需要使用指针:

对于给定的类型,每个方法的名字必须是唯一的,同时方法和函数一样也不支持重载。

继承:

通过嵌入匿名的成员,我们不仅可以继承匿名成员的内部成员,而且可以继承匿名成员类型所对应的方法。我们一般会将 Point 看作基类,把 ColoredPoint 看作是它的继承类或子类。

不过这种方式继承的方法并不能实现 C++ 中虚函数的多态特性。所有继承来的方法的接收者参数依然是那个匿名成员本身,而不是当前的变量。编译期进行展开

如果一个类型实现了一个 interface 中所有方法,我们说类型实现了该 interface,interface 变量存储的是实现者的值,这里是类似面向接口的做法,定义接口,用结构去实现接口,被调用函数调用接口,调用函数传入结构,动态绑定,向上转型。

区分 interface 的变量究竟存储哪种类型的值,value, ok := em.(T):em 是 interface 类型的变量,T代表要断言的类型,value 是 interface 变量存储的值,ok 是 bool 类型表示是否为该断言的类型 T。

ok 是 true 表明 i 存储的是 *S 类型的值,false 则不是,类型断言(Type assertions)

如果需要区分多种类型,可以使用 switch 断言:

go 可以把指针进行隐式转换得到 value,但反过来则不行。

range

应用于array, slice, map, channel

go

并发,开启新线程执行函数

Go 语言是基于消息并发模型的集大成者,它将基于 CSP 模型的并发编程内置到了语言中,通过一个 go 关键字就可以轻易地启动一个 Goroutine,与 Erlang 不同的是 Go 语言的 Goroutine 之间是共享内存的。

Goroutine是 Go 语言特有的并发体,是一种轻量级的线程,与系统级线程不同的是启动大小可以很小(2KB/4KB),而后会动态伸缩大小

sync包提供原子操作库

并发helloworld

对于从无缓冲 Channel 进行的接收,发生在对该 Channel 进行的发送完成之前。有缓冲则相反,对于 Channel 的第 K 个接收完成操作发生在第 K+C 个发送操作完成之前

生产者消费者模型

通过channel可简单实现

发布订阅模型(pub/sub)

多个channel,视为主题,发布者发布主题,建立管道和订阅者通信,单个主题是生产者/消费者模型

并发的安全退出

基于 select 实现的管道的超时判断:

通过 select 的 default 分支实现非阻塞的管道发送或接收操作:

通过 select 来阻止 main 函数退出:

通过 select 和 default 分支可以很容易实现一个 Goroutine 的退出控制:

多个Goroutine,通过close(cancel)实现广播,所有从关闭管道接收的操作均会收到一个零值和一个可选的失败标志。但是main此时没有等待各个Goroutine回收清理工作完成即退出,改进:

使用context包:

Web开发

中间件

剥离非业务逻辑

分布式

id生成分发

Snowflake算法

snowflake

sequence_id循环自增

这样的机制可以支持我们在同一台机器上,同一毫秒内产生$2 ^ 12 = 4096$条消息,支持我们每数据中心部署 32 台机器,所有数据中心共 1024 台实例。表示 timestamp 的 41 位,可以支持我们使用 69 年

分布式锁

进程内锁:

Redis setnx分布式trylock

ZooKeeper

类似mutex lock,在lock成功前会一直阻塞,适合分布式任务调度场景,但不适合高频次持锁时间短的抢锁场景。基于强一致协议的锁适用于 粗粒度 的加锁操作。这里的粗粒度指锁占用时间较长。

其原理也是基于临时 Sequence 节点和 watch API,例如我们这里使用的是 /lock 节点。Lock 会在该节点下的节点列表中插入自己的值,只要节点下的子节点发生变化,就会通知所有 watch 该节点的程序。这时候程序会检查当前节点下最小的子节点的 id 是否与自己的一致。如果一致,说明加锁成功了。

etcd

可以实现ZooKeeper相同功能:

etcdsync 的 Lock 流程是:

  1. 先检查 /lock 路径下是否有值,如果有值,说明锁已经被别人抢了

  2. 如果没有值,那么写入自己的值。写入成功返回,说明加锁成功。写入时如果节点被其它节点写入过了,那么会导致加锁失败,这时候到 3

  3. watch /lock 下的事件,此时陷入阻塞

  4. 当 /lock 路径下发生事件时,当前进程被唤醒。检查发生的事件是否是删除事件(说明锁被持有者主动 unlock),或者过期事件(说明锁过期失效)。如果是的话,那么回到 1,走抢锁流程。

延时任务系统

定时器:

时间堆实现,小顶堆,对于定时器来说,如果堆顶元素比当前的时间还要大,那么说明堆内所有元素都比当前时间大。进而说明这个时刻我们还没有必要对时间堆进行任何处理。定时检查的时间复杂度是 O(1)。

当我们发现堆顶的元素小于当前时间时,那么说明可能已经有一批事件已经开始过期了,这时进行正常的弹出和堆调整操作就好。每一次堆调整的时间复杂度都是 O(LgN)。

go内置的定时器用四叉时间堆实现,父节点要求比所有子节点都小,但子节点之间没有大小关系要求

时间轮:

用时间轮来实现定时器时,我们需要定义每一个格子的 “刻度”,可以将时间轮想像成一个时钟,中心有秒针顺时针转动。每次转动到一个刻度时,我们就需要去查看该刻度挂载的任务列表是否有已经到期的任务。

从结构上来讲,时间轮和哈希表很相似,如果我们把哈希算法定义为:触发时间 % 时间轮元素大小。那么这就是一个简单的哈希表。在哈希冲突时,采用链表挂载哈希冲突的定时器。

Go语言常见坑

可变参数是空接口类型?

数组是按值传递,无法通过修改数组类型的参数返回结果。

map是一种hash表实现,每次遍历的顺序都可能不一样。

在局部作用域中,命名的返回值被函数内同名的局部变量屏蔽。

recover必须在defer函数中运行

Goroutine 是协作式抢占调度(Go1.14版本之前),Goroutine本身不会主动放弃CPU

不同Goroutine之间不满足顺序一致性内存模型:

空指针和空接口不等价

内存地址会变化

尽管有内存自动回收机制,但是Goroutine比启动程序先退出时无法回收,导致泄漏

有趣代码

禁止main函数退出:

Assert:

Last updated