go特性
一、闭包
1. 闭包基本使用
```go
func main() {
n := 0
f := func() int {
n += 1
return n
}
fmt.Println(f()) // 别忘记括号,不加括号相当于地址
fmt.Println(f())
}
/*
输出:
1
2
*/
```2. 闭包作为函数返回值
3. 并发中的闭包
14. 并发闭包问题的两种解决方式
二、接口
1. 概述
和其他语言不同的是,在Go语言中接口的实现是隐式的。即我们不用去明确的指出某一个类型实现了某一个接口.只要某一类型的方法中实现了接口中的全部方法签名,就意味着此类型实现了这一接口。
2. 接口的组合
直接embbedding。
3. 类型断言
为什么Go语言在运行时还需要再判断一次呢,这是由于在类型断言方法m = i.(Type)中,如果Type实现了接口i,但是此时接口内部并没有任何动态类型(此时为nil),这时在运行时会直接panic。如果不想panic,就加个ok判断一下,如下。ok为bool类型。
4. 空接口
空接口可以存储结构体、字段串、整数等任何的类型。
在Println源码中,使用switch语句嵌套这一语法获取空接口中的动态类型。并根据动态类型的不同,进行不同的格式化输出。
5. 接口的比较性
两个接口之间可以通过==或!=进行比较。
如果两个接口不为nil,如果他们具有相同的动态类型与动态类型值,则两个接口是相同的。
如果都为nil,比较结果相等,如果只有1个接口为nil,那么比较结果总是false。
如果接口存储的动态类型值是不可比较的,在运行时会报错。
三、Go标准库-锁、信号量、POOL
1. Mutex 互斥锁
互斥锁是用来保证在任一时刻, 只能有一个例程访问某个对象。 Mutex的初始值为解锁的状态。 通常作为其他结构体的匿名字段使用, 并且可以安全的在多个例程中并行使用
成员方法
示例代码
2. RWMutex 读写互斥锁
RWMutex是读写互斥锁。该锁可以被同时多个读取者持有或唯一个写入者持有。RWMutex可以创建为其他结构体的字段;零值为解锁状态。RWMutex类型的锁也和线程无关,可以由不同的线程加读取锁/写入和解读取锁/写入锁。Mutex 可以安全的在多个例程中并行使用。
可以读时, 多个goroutine可以同时读。
写的时候, 其他goroutine不可读也不可写。
成员方法
示例代码
3. WaitGroup 组等待
WaitGroup用于等待一组线程的结束。父线程调用Add方法来设定应等待的线程的数量。每个被等待的线程在结束时应调用Done方法。同时,主线程里可以调用Wait方法阻塞至所有线程结束(计数器归零)。
成员方法
示例代码
4. Cond 条件等待
Cond结构
Cond实现了一个条件变量,一个线程集合地,供线程等待或者宣布某事件的发生。
每个Cond实例都有一个相关的锁(一般是Mutex或RWMutex类型的值),它必须在改变条件时或者调用Wait方法时保持锁定。
Cond可以创建为其他结构体的字段,Cond在开始使用后不能被拷贝。
条件等待通过Wait让例程等待,通过Signal让一个等待的例程继续,通过Broadcase让所有等待的继续。在Wait之前需要手动为c.L上锁, Wait结束了手动解锁。为避免虚假唤醒, 需要将Wait放到一个条件判断的循环中,官方要求写法:
成员方法
示例代码
5. Locker
Locker接口代表一个可以加锁和解锁的对象。 是一个接口。
结构
}
6. Pool 临时对象池
我们通常用golang来构建高并发场景下的应用,但是由于golang内建的GC机制会影响应用的性能,为了减少GC,golang提供了对象重用的机制,也就是sync.Pool对象池。
sync.Pool是可伸缩的,并发安全的。其大小仅受限于内存的大小,可以被看作是一个存放可重用对象的值的容器。 设计的目的是存放已经分配的但是暂时不用的对象,在需要用到的时候直接从pool中取。
暂不了解。
四、Go的异常处理
1. 异常与错误的概念
异常和错误不是一个概念,但二者经常被弄混。错误指的是可能出现问题的地方出现了问题,比如打开一个文件时失败,这种情况在人们的意料之中 ;而异常指的是不应该出现问题的地方出现了问题,比如引用了空指针,这种情况在人们的意料之外。可见,错误是业务过程的一部分,而异常不是 。
go使用error来处理错误,使用panic和recover来触发和终止异常流程。同时引入关键字defer来延迟执行defer后面的函数(异常后也执行)。
2. 异常的处理
如越界,引用空指针等触发panic,就调用defer函数。
然后向上传递panic,一直到遇到recover接收处理。
如果没有recover,会一直传到协程的起点,然后终止包括主协程在内的所有协程。
3. go异常和错误互相转化
错误转异常,如当错误次数过多的时候就抛出异常。
异常转错误,如用recover接收后当做错误处理,程序继续执行。
4. 异常与错误的使用场景
异常使用的场景
空指针引用
下标越界
除数为0
不应该出现的分支,比如default
输入不应该引起函数错误
对于异常,我们可以选择在一个合适的上游去recover,并打印堆栈信息,使得部署后的程序不会终止。
其他使用错误
5. 错误处理
不要滥用错误,比如有些返回场景是需要bool类型的,也返回一个errors.New("假的"),就很不合理。很多没有必要的地方,不用非得返回一个nil。
如果有error返回,应该放在返回列表的最后一位。
错误值要统一定义,不要每个函数想起来什么返回什么。最好每个包里有个专门的错误对象定义文件。如下:
错误逐层传递时,层层都加日志,方便定位。
错误处理使用defer
当出现错误的时候,需要把之前申请的资源都释放掉,一般如下这样写:
使用defer来释放资源
当尝试几次可以避免失败时,不要立即返回错误,如尝试请求某个URL,有时第一次没有响应可以再试一次。
当上层函数不关心错误时,建议不返回error,只打印日志就行。
6. 异常处理
在程序开发阶段,坚持速错。在早期开发以及任何发布阶段之前,最简单的同时也可能是最好的方法是调用panic函数来中断程序的执行以强制发生错误,使得该错误不会被忽略,因而能够被尽快修复。
在程序部署后,应恢复异常避免程序终止。在上层函数应该用recover接收异常。可以通过配置文件或环境变量的方式,来区分开发环境和部署环境是否进行recover。
recover一般的处理方法
打印堆栈的异常调用信息和关键的业务信息,以便这些问题保留可见;
将异常转换为错误,以便调用者让程序恢复到健康状态并继续安全运行。错误需要自己转换。
对于不应该出现的分支,使用异常处理。
函数参数由用户保证不会出错,一出错立刻panic,减少繁琐的错误处理。比如我确定就是字符串,非得传一个Int,那就直接panic,也不进行错误检查。
五、Go语言中反射包的实现原理
1. 概述
反射是建立在类型系统(the type system)上的。Go是静态类型化的。每个变量都有一个静态类型,也就是说,在编译的时候变量的类型就被很精确地确定下来了。
如上,i的类型就是int,而j的类型就是MyInt。这里的变量i和j具有不同的静态类型,虽然它们有相同的底层类型(underlying type),如果不显示的进行强制类型转换它们是不能互相赋值的。
2. 作用
应用场景还不知道,看着好像理解是什么,就是能获得对象的类型,但是也不知道这样有什么用。TODO
六、Go语言并发的设计模式和应用场景
1. 利用阻塞通道自动生成数据。
创建一个阻塞通道,每次想要数据了就从中提取。比如可以生成随机数。
2. 主程序阻塞通道等待,等到协程执行完发送信号,主程序再推出。
3. 指定数据生成器
比如指定0和1随机数生成器
如下,其中select是随机执行case语句的,在go的select设计中,每次select,会随机打乱case顺序。
func main() { generator := rand01() //初始化一个01随机生成器
}
4. 定时器
time.After
定时给通道发消息
5. 当成先进先出的队列使用
6. 菊花链(过滤器)
这个真的妙不可言
如这个生成素数的例子,这段程序来慢慢分析。
xrange()可以忽略,直接指定2也行
先看main()函数。
筛子中最小的数肯定是素数,然后这个素数的倍数肯定不是素数,每次循环就是做这个事,把这个素数的倍数剔除了。
filter(nums, number)就是剔除操作,number为当前的素数,nums为要剔除的列表,然后会返回一个新的列表。
把新列表中的第一个数取出来,作为下一次循环的筛子,这个数肯定是素数。
当筛子大于100的时候,就是获得的第一个大于100的素数,循环结束。
filter()函数
func filter(in chan int, number int) chan int { // 输入一个整数队列,筛出是number倍数的, 不是number的倍数的放入输出队列 // in: 输入队列 out := make(chan int)
}
func main() { const max = 100 // 找出100以内的所有素数 nums := xrange() // 初始化一个整数生成器 number := <-nums // 从生成器中抓一个整数(2), 作为初始化整数
}
七、关于golang锁的正确使用方式
1. 尽量减少锁的持有时间
细化锁的粒度。通过细化锁的粒度来减少锁的持有时间以及避免在持有锁操作的时候做各种耗时的操作。
不要在持有锁的时候做 IO 操作。尽量只通过持有锁来保护 IO 操作需要的资源而不是 IO 操作本身。如下:
2. 善用 defer 来确保在函数内正确释放了锁
但是注意defer就代表整个函数期间都持有锁,可以用匿名函数。
3. 在适当时候使用 RWMutex
当确定操作不会修改保护的资源时,可以使用 RWMutex 来减少锁等待时间(不同的 goroutine 可以同时持有 RLock, 但是 Lock 限制了只能有一个 goroutine 持有 Lock)
4. copy 结构体操作可能导致非预期的死锁
copy 结构体时,如果结构体中有锁的话,记得重新初始化一个锁对象,否则会出现非预期的死锁。
5. build/test 时使用 -race 参数以便运行时检测数据竞争问题
6. 使用 go-deadlock 检测死锁或锁等待问题
go-deadlock是一个库
示例代码:
7. 实现 tryLock 功能
一般 Lock() 如果拿不到锁的话,会一直阻塞在那里,在某些场景下这个功能不是我们所期望的结果,我们可能希望程序在一定时间内无法获取到锁的话就做其他操作或者直接返回失败:比如在一个 http server 中,处理请求时因为锁等待时间太长导致客户端大量超时,引发客户端重连以及服务端 goroutine 数量持续增长(虽然客户端超时了,但是处理请求的 goroutine 还在继续处理已超时的请求并且阻塞在了获取锁的地方,然后客户端重连又加重了这个问题,表现就是处理请求的 goroutine 数量直线上升)。这个时候我们就需要有一个类似 tryLock 的功能,在发现短时间内无法获取到锁的时候直接返回失败的响应,防止问题进一步加重(Fail Fast)。
go语言实现tryLock:https://colobu.com/2017/03/09/implement-TryLock-in-Go/
8. 改为使用 channel
八、其他
1. runtime.GOMAXPROCS()
<1:不修改任何数值。=1:单核心执行。>1:多核并发执行。
指定的是使用的CPU核数,go1.5以后不指定的话默认使用最大核数。
2. go defer的一些注意事项
异常后defer函数也会执行,常用来解锁
返回值和defer的执行顺序问题,一般是先把返回值写入,然后开始执行defer。如果defer修改了返回值,分两种情况。
如果返回值是声明过的,就会修改。func name()(i int)
如果返回值没有声明,在函数中声明并返回的,就不会修改。func name()(int)
3. 信道死锁/阻塞
不指定容量,写入数据就会一直阻塞,直到数据读出。(注意不指定容量和指定容量大小为1,是不一样的,为1说明容量为1,只写入一个数据的时候不阻塞,写入第二个数据的时候如果第一个数据没被读出才阻塞。)
指定容量,当容量满的时候才会阻塞。
九、语言相关
1. map
支持多键索引
delete(map,key)删除元素
sync.Map ,并发map,效率降低,安全性提高,但不支持len()操作,不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除。
2. goto
使用goto统一处理错误
最后更新于
这有帮助吗?