其实用go也用了一段时间,我是看视频入门的,然后去买书,反正来来回回用了一年多的时间,很多点知道怎么用,相关知识也能答上来,但是始终有种感觉就是这样会不会有问题,这个文档就用来记录我复盘golang的学习过程的。
当然基础容器这些我是了解得够多了,所以主要这个文集停留于用的层面,就是关于go的工程化,毕竟我们的代码不单单是给我们自己看的,工程化代码是很有必要的。所以文集的线索会从官方文档到翻译再加上一些辅助资料和实践出真知的例子来说明相关关键字用法,当然在这个过程中也会发生很多错误,如果你看到了,或者你有什么疑问也可以在文章下面评论,我看到就会及时回复的。
goroutine
第一个文章不得不说一下这个明星关键字goroutine。这个小学生都会写的关键字,我们到底如何用它
- 1.尽可能自己做事而不是交给goroutine
- 2.当你不知道goroutine什么时候会关闭的时候就不要使用goroutine
- 3.管理起goroutine的声明周期,开启一个goroutine请问一下自己
1:goroutine什么时候会关闭
2:怎么去主动关闭goroutine
goroutine的易错点
1.谨防goroutine泄露
When it comes to memory management, Go deals with many of the details for you. The Go compiler
decides where values are located in memory using [escape analysis]
(https://www.ardanlabs.com/blog/2017/05/language-mechanics-on-escape-analysis.html). The runtime
tracks and manages heap allocations through the use of the [garbage collector]
(https://blog.golang.org/ismmkeynote). Though it’s not impossible to create [memory leaks]
(https://en.wikipedia.org/wiki/Memory_leak) in your applications, the chances are greatly reduced.
A common type of memory leak is leaking Goroutines. If you start a Goroutine that you expect to
eventually terminate but it never does then it has leaked. It lives for the lifetime of the application and
any memory allocated for the Goroutine can’t be released. This is part of the reasoning behind the
advice “[Never start a goroutine without knowing how it will stop]
(https://dave.cheney.net/2016/12/22/never-start-a-goroutine-without-knowing-how-it-will-stop)”.
翻译
并发编程允许开发人员使用多种执行路径来解决问题,并且经常用于提高性能。并发并不意味着这些多
个路径并行执行。这意味着这些路径是无序执行,而不是顺序执行。从历史上看,使用标准库或第三方
开发人员提供的库可以简化此类编程。
在Go中,语言和运行时内置了诸如Goroutine和通道之类的并发功能,以减少或消除对库的需求。这产生
了一种幻想,即在Go中编写并发程序很容易。在决定使用并发时,您必须谨慎,因为并发会带来一些独
特的副作用或陷阱(如果使用不正确)。如果不小心,这些陷阱会造成复杂性和令人讨厌的错误。
案例:
31 // leak is a buggy function. It launches a goroutine that
32 // blocks receiving from a channel. Nothing will ever be
33 // sent on that channel and the channel is never closed so
34 // that goroutine will be blocked forever.
35 func leak() {
36 ch := make(chan int)
37
38 go func() {
39 val := <-ch
40 fmt.Println("We received a value:", val)
41 }()
42 }
此时我们的goroutine的状态称为泄露状态,我们无法管控,无法退出,我们要严防这类代码
2.控制goroutine退出
其实这里面有一定逻辑上的悖论,假设有个业务模型是这样的:用户注册成功,发送通知短信,发送通知邮件,这种业务叫做旁路业务,也就是主路业务不需要再次管理这种旁路业务,延不延迟其实都没太大所谓,用户不在乎,对于我们自身来说的话也没有太大的业务逻辑。那么就会出现这样的情况:
func main(){
// 用户信息入库
// 发送短信
go sendMsg("注册成功","1888888888")
//todo other
}
func sendMsg(msg string ,phoneNum int64)error{
fmt.printf("给用户%v,发送了消息:%v\n",msg,phoneNum)
}
其实这是可以完成的,只是说这种代码非常危险,第一是对第三方平台不友好,第二是goroutine无法管控。
- 假设我们的goroutine突然第三方响应变得很慢,每个接口需要1min响应,那么在我们内存中就有大量的goroutine,我们不能及时的取消这些goroutine,就导致很多有泄露的危险
- 一般这种涉及到第三方的,对我们平台都有一定的限流,很多的请求就会直接报错误码,也就是会出现大量请求失败
解决办法:
- 1.使用中间件消息队列,但是我举例的只是很小一个例子,其他不一定适用比如日志追踪,请求日志这类就肯定没必要丢到中间件又换个用户去处理
- 2.使用生产消费模型,启动一个专门的goroutine去处理短信,然后使用一个channel传递,当我们通道塞满了后可以选择性的丢弃一半的buffer(当然丢弃指完全旁路,不想丢弃就加大消费者处理能力,比如多加一个goroutine)
3.管控goroutine执行超时
其实这里我相信实际开发的人员都知道一个坑点,就是在select的时候正在进行一项任务,另外一个case进入信号其实是会被阻塞的,在研发初期我一直以为它会直接取消。那我们应该怎么来正确使用context的超时,或者channel的关闭广播,实际context原理就是使用chan的广播。
划分业务:我们的业务其实细分起来是属于算力型和网络型
- 算力型业务:最大应该也不超过100MS,算力型业务我们不应该执行超过这个标准,否则就应该划分函数了,而且算力型业务我们不能使用关闭它,只有等它完成才行
- 网络型业务:中间件之间的调用,rpc调用都是通过网络模型来进行的,那么我们就可以比较容易的做到级联取消,在rpc调用的首参数就是可以添加超时控制,但是我们操控中间件:redis、mysql 好像没有context参数给我们使用,这是为什么呢?redis连接中实际上建立连接就有超时的参数设置,超时会报错,mysql则第一是使用连接池,不能直接关闭连接,第二是mysql本身有超时控制,需要在mysql端设置。所以我们常见的xorm,gorm都没有查询超时的策略,这种的话只有优化好我们的sql才行
4.野生goroutine 尽量少用
首先我们需要明确一点,在golang中没有类似php的那种框架兜底,也就是不论报什么错误都有一个函数给你兜底,不会导致程序宕机,那么其实这就是很恐怖的,又尤其在recover 不能捕获跨goroutine的错误,那么我们应该怎么来处理这个东西呢。
下面是我们采取的实例,简单的这样进行处理。
当然这种情况也要分的
- 1.当go出去的是不可能报panic的,可以用我们封装的也可以不用,但是我建议还是用
- 2.go出去的东西实际上又非常多逻辑 ,这种需要使用我们封装的
func main() {
Go(func() {
})
}
func Go(f func()){
go func() {
defer func() {
if r:=recover();r!=nil{
fmt.Println(r)
}
}()
f()
}()
}
有疑问加站长微信联系(非本文作者)