谁知道最近比较火的歌那个特别火的那个MYMK是哪出的,怎么才能买到,跪求链接。

有哪些好看又有创意的手机壳? - 知乎<strong class="NumberBoard-itemValue" title="被浏览<strong class="NumberBoard-itemValue" title="1,245分享邀请回答19925 条评论分享收藏感谢收起48445 条评论分享收藏感谢收起&ul&&li&一个能用的 Parser Combinator, 当时我写了个用来应付编译原理作业. 可以参考 Monadic parsing in Haskell 和 Hutton 的 Programming in Haskell 相关章节, 稍微入门 Haskell 就可以写了, 对理解 Monad, Applicative 还有以后用其他完备的 Parser Combinator 挺有帮助, 虽然实现上差距相当大&/li&&li&基于上面的 Parser Combinator 写各种 Parser: S-Exp, JSON, HTTP, 某个二进制协议(例如 Memcache?) 有错误信息/恢复/位置, 做 benchmark, 体会各种 PC 的差别/拓展(parsec, attoparserc, readp)和自己的渣渣&/li&&li&R5RS 编译到 MIPS
模拟器, 这里也可以利用上面写的PC, 还有可以练习 Haskell 各种 (syntax) tree manipulation 的技法, 写编译器算是 Haskell 看家项目, 所以提高点难度&/li&&li&系统编程方面, 可以实现 GNU Coreutils 里的各种工具, 可以用各种解析命令行参数的库, 还有各种 Stream 抽象的库, 不爽的话继续 benchmark,你会发现为什么 GNU 那帮人这么变态, 参考:
&a href=&//link.zhihu.com/?target=https%3A//github.com/alexander-b/coreutilhs& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&alexander-b/coreutilhs&/a& &a href=&//link.zhihu.com/?target=https%3A//wiki.haskell.org/Simple_Unix_tools& class=& external& target=&_blank& rel=&nofollow noreferrer&&&span class=&invisible&&https://&/span&&span class=&visible&&wiki.haskell.org/Simple&/span&&span class=&invisible&&_Unix_tools&/span&&span class=&ellipsis&&&/span&&/a&&/li&&li&一个简单的 Web Framework, 可以理解Reader Monad,
Monad Transformer, Haskell 应用的 Monad Stack 之类的吧, 可以参考 &a href=&//link.zhihu.com/?target=https%3A//github.com/cbaatz/build-a-haskell-web-framework& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&cbaatz/build-a-haskell-web-framework&/a&&/li&&li&把火车头书(Java Concurrency in Practice) 跟 java.util.concurrent 用 Haskell 过一遍, 体会 Haskell 的并发优势 (逃&/li&&li&&----------------------------------- 更新的分割线 -------------------------------&&/li&&li&上面开玩笑的, 接下来熟练下 Haskell 里各种并发的基础设施 (LWT, Channel, MVar, STM), 结合上面写 HTTP Parser, Web Framework, Stream库还有并发的经验架一个 HTTP Server, 继续 benchmark 跟 Nginx 杠的那种 benchmark, 做 Profile. 搞明白 Haskell 究竟快在哪里或者慢在哪里&/li&&/ul&
一个能用的 Parser Combinator, 当时我写了个用来应付编译原理作业. 可以参考 Monadic parsing in Haskell 和 Hutton 的 Programming in Haskell 相关章节, 稍微入门 Haskell 就可以写了, 对理解 Monad, Applicative 还有以后用其他完备的 Parser Combinato…
&figure&&img src=&https://pic4.zhimg.com/v2-4e34dece690_b.jpg& data-rawwidth=&500& data-rawheight=&333& class=&origin_image zh-lightbox-thumb& width=&500& data-original=&https://pic4.zhimg.com/v2-4e34dece690_r.jpg&&&/figure&&h2&0. 缘起&/h2&大约在三年前,我曾经写过一篇 &a href=&http://link.zhihu.com/?target=http%3A//blog.jobbole.com/56574/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&最佳日志实践&/a&,还被码农周刊选为那年的 &a href=&http://link.zhihu.com/?target=http%3A//oiolong.github.io/linux//manong-2014-collection.html& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&最受欢迎技术干货&/a& 之一。当时我任职于网易杭州研究院的存储平台组,主要做网易对象存储(NOS)的开发和部分运维工作。由于网易云音乐,易信等几个重要产品陆续上线,业务压力剧增,我们的系统在前前后后大约半年的时间里,出现了大大小小各种事故。通过不断总结事故原因、不断地优化代码、进化部署架构,才使整个系统逐渐稳定下来。那个时候组里人常常开玩笑说,我们采用的是TDD的开发模式,只是这个TDD不是测试驱动开发(Test Driven Development),而是悲剧驱动开发(Tragedy Driven Development)。&p&最佳日志实践的第一版便是在那个时候完成的,里面包含了我们在开发和运维过程中的一些好的实践。最初起名&b&“最佳日志实践”&/b&实在有些标题党,不过由于起名字是一件比写代码更困难的事儿,我就继续沿用这个名字吧。有几个原因让我一直想要对那篇文章进行整理和扩充:&/p&&ol&&li&那篇文章里的一些内容太细节,涉及到了网易对象存储中的业务逻辑,对读者不够友好;&/li&&li&那篇文章里一些内容基于Java语言来讨论,实际上之后我有很多的精力都在基于Go语言做开发,因此现在更想要讨论一些与语言无关的方面;&/li&&li&最近几年又写了若干个系统,对于日志这件事情又有了一些心得和体会,也想拿来跟大家分享;&/li&&/ol&&p&开始正文吧...&/p&&br&&h2&1. 什么是日志&/h2&&p&日志用来记录用户操作、系统运行状态等,是一个系统的重要组成部分。然而,由于日志通常不属于系统的核心功能,所以常常不被团队成员所重视。对于一些简单的小程序,可能并不需要在如何记录日志的问题上花费太多精力。但是对于作为基础平台为很多产品提供服务的后端程序,就必须要考虑如何依靠良好的日志来保证系统可靠的运行了。&/p&&p&好的日志可以帮助系统的开发和运维人员:&/p&&ol&&li&了解线上系统的运行状态&/li&&li&快速准确定位线上问题&/li&&li&发现系统瓶颈&/li&&li&预警系统潜在风险&/li&&li&挖掘产品最大价值&/li&&li&……&/li&&/ol&&p&不好的日志导致:&/p&&ol&&li&对系统的运行状态一知半解,甚至一无所知&/li&&li&系统出现问题无法定位,或者需要花费巨大的时间和精力&/li&&li&无法发现系统瓶颈,不知优化从何做起&/li&&li&无法基于日志对系统运行过程中的错误和潜在风险进行监控和报警&/li&&li&对挖掘用户行为和提升产品价值毫无帮助&/li&&li&……&/li&&/ol&&h2&2. 日志的分类&/h2&&p&日志从功能来说,可分为诊断日志、统计日志、审计日志。&/p&&br&&p&诊断日志, 典型的有:&/p&&ul&&li&请求入口和出口&/li&&li&外部服务调用和返回&/li&&li&资源消耗操作: 如读写文件等&/li&&li&容错行为: 如云硬盘的副本修复操作&/li&&li&程序异常: 如数据库无法连接&/li&&li&后台操作:定期执行删除的线程&/li&&li&启动、关闭、配置加载&/li&&/ul&&p&统计日志:&/p&&ul&&li&用户访问统计:用户IP、上传下载的数据量,请求耗时等&/li&&li&计费日志(如记录用户使用的网络资源或磁盘占用,格式较为严格,便于统计)&/li&&/ul&&p&审计日志:&/p&&ul&&li&管理操作&/li&&/ul&&p&对于简单的系统,可以将所有的日志输出到同一个日志文件中,并通过不同的关键字进行区分。而对于复杂的系统,将不同需求的日志输出到不同的日志文件中是必要的,通过对不同类型的文件采用不同的日志格式(例如对于计费日志可以直接输出为Json格式),可以方便接入其他的子系统。&/p&&h2&3. 日志中记录什么&/h2&&p&理想的日志中应该记录&b&不多不少&/b&的信息。&/p&&p&所谓不多,是指不要在日志中记录无用的信息。实践中常见到的无用的日志有:1)能够放在一条日志里的东西,放在多条日志中输出;2)预期会发生且能够被正常处理的异常,打印出一堆无用的堆栈;3)开发人员在开发过程中为了调试方便而加入的“临时”日志&/p&&p&所谓不少,是指对于日志的使用者,能够从日志中得到所有需要的信息。在实践中经常发生日志不够的情况,例如:1)请求出错时不能通过日志直接来定位问题,而需要开发人员再临时增加日志并要求请求的发送者重新发送同样的请求才能定位问题;2)无法确定服务中的后台任务是否按照期望执行;3)无法确定服务的内存数据结构的状态;4)无法确定服务的异常处理逻辑(如重试)是否正确执行;5)无法确定服务启动时配置是否正确加载;6)等等等等&/p&&p&输出日志时要考虑日志的使用者,例如如果日志主要由系统的运维人员来看,那就不能输出:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&[INFO] RequestID:bdd2611184, ErrorCode:1426
&/code&&/pre&&/div&&p&至少应该是:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&[INFO] RequestID:bdd2611184, ErrorCode:1426, Message: callback request (to http://example.com/callback) failed due to socket timeout
&/code&&/pre&&/div&这样运维人员一眼就能清楚问题的原因,而不需要再通过开发来查看ErrorCode对应的具体错误。&p&整理一下通常情况下会遗漏的日志:&/p&&ol&&li&系统的配置参数:系统在启动过程中通常会首先读启动参数,可以在系统启动后将这些参数输出到日志中,方便确认系统是按照期望的参数启动的;&/li&&li&后台定期执行的任务:如定期更新缓存的任务,可以记录任务开始时间,任务结束时间,更新了多少条缓存配置等等,这样可以掌握定期执行的任务的状态;&/li&&li&异常处理逻辑:如对于分布式存储系统来说,当系统在一个存储节点上读数据失败时,需要去另一个数据节点上进行重试,可以将读数据失败这件事情记录下来,之后可以通过对日志的分析确认是否某些节点的磁盘可能存在故障。再比如,如果系统需要请求一个外部资源,可以将请求这个外部资源偶尔失败又重试成功这件事情记录下来,具体来说:&br&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&[INFO] RequestID:bdd2611184, auth request (to http://auth1.example.com/v2) timeout ... 1 try
[INFO] RequestID:bdd2611184, auth request (to http://auth1.example.com/v2) timeout ... 2 try
[INFO] RequestID:bdd2611184, auth request (to http://auth1.example.com/v2) success
&/code&&/pre&&/div&要好于&br&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&[INFO] RequestID:bdd2611184, auth request (to http://auth1.example.com/v2) success
&/code&&/pre&&/div&因为前者可以让我们预判被依赖的服务器服务质量有风险,也许需要进行扩容;&/li&&li&日志中需要记录关键参数,出错时的关键原因等。例如:&br&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&[INFO] RequestID:bdd2611184, auth failed
[INFO] RequestID:bdd2611185, content digest does not match
[INFO] RequestID:bdd2611186, request ip not in whitelist
&/code&&/pre&&/div&就不如:&br&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&[INFO] RequestID:bdd2611184, auth failed due to token expiration
[INFO] RequestID:bdd2611185, content digest does not match, expect 7b3f050bfa060b86bac953, actual ff302d048
[INFO] RequestID:bdd2611186, request ip(=202.17.34.1) not in whitelist
&/code&&/pre&&/div&&/li&&/ol&&h2&4. 关于日志级别&/h2&&p&我们通常使用的日志库,将日志基本分为以下几类(从低到高):&br&&strong&TRACE &/strong&– The TRACE Level designates finer-grained informational events than the DEBUG&br&&strong&DEBUG&/strong& – The DEBUG Level designates fine-grained informational events that are most useful to debug an application.&br&&strong&INFO &/strong&– The INFO level designates informational messages that highlight the progress of the application at coarse-grained level.&br&&strong&WARN &/strong&– The WARN level designates potentially harmful situations.&br&&strong&ERROR &/strong&– The ERROR level designates error events that might still allow the application to continue running.&br&&strong&FATAL &/strong&– The FATAL level designates very severe error events that will presumably lead the application to abort.&/p&&p&开发人员对于何种日志输出为何种级别通常有自己的理解,那在实践中,是否所有的日志级别都有必要存在,哪些操作需要记入日志,哪种错误应该记为WARN级别,而哪种错误又为ERROR级别呢?关于该问题,可以参考StackOverflow上的一个&a href=&http://link.zhihu.com/?target=http%3A//stackoverflow.com/questions/2031163/when-to-use-log-level-warn-vs-error& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&讨论&/a&。&/p&&p&此处对贴子中的一些观点,加上我们在平时运维过程中遇到的相关问题进行归纳:&/p&&ul&&li&一个项目各个日志级别的定义应该是清楚明确的,需要团队的每个开发人员共同遵守;&/li&&li&即使是TRACE或者DEBUG级别的日志,也应该有一定的规范,要保证除了开发人员自己以外,包括测试人员和运维人员都可以方便地通过日志定位问题;&/li&&li&对于日志级别的分类,有以下参考:&br&&strong&FATAL&/strong& — 表示需要立即被处理的系统级错误。当该错误发生时,表示服务已经出现了某种程度的不可用,系统管理员需要立即介入。这属于最严重的日志级别,因此该日志级别必须慎用,如果这种级别的日志经常出现,则该日志也失去了意义。通常情况下,一个进程的生命周期中应该只记录一次FATAL级别的日志,即该进程遇到无法恢复的错误而退出时。当然,如果某个系统的子系统遇到了不可恢复的错误,那该子系统的调用方也可以记入FATAL级别日志,以便通过日志报警提醒系统管理员修复;&br&&strong&ERROR&/strong& — 该级别的错误也需要马上被处理,但是紧急程度要低于FATAL级别。当ERROR错误发生时,已经影响了用户的正常访问。从该意义上来说,实际上ERROR错误和FATAL错误对用户的影响是相当的。FATAL相当于服务已经挂了,而ERROR相当于好死不如赖活着,然而活着却无法提供正常的服务,只能不断地打印ERROR日志。特别需要注意的是,ERROR和FATAL都属于服务器自己的异常,是需要马上得到人工介入并处理的。而对于用户自己操作不当,如请求参数错误等等,是绝对不应该记为ERROR日志的;&br&&strong&WARN&/strong& — 该日志表示系统可能出现问题,也可能没有,这种情况如网络的波动等。对于那些目前还不是错误,然而不及时处理也会变为错误的情况,也可以记为WARN日志,例如一个存储系统的磁盘使用量超过阀值,或者系统中某个用户的存储配额快用完等等。对于WARN级别的日志,虽然不需要系统管理员马上处理,也是需要及时查看并处理的。因此此种级别的日志也不应太多,能不打WARN级别的日志,就尽量不要打;&br&&strong&INFO&/strong& — 该种日志记录系统的正常运行状态,例如某个子系统的初始化,某个请求的成功执行等等。通过查看INFO级别的日志,可以很快地对系统中出现的 WARN,ERROR,FATAL错误进行定位。INFO日志不宜过多,通常情况下,INFO级别的日志应该不大于TRACE日志的10%;&br&&strong&DEBUG&/strong& or &strong&TRACE&/strong& — 这两种日志具体的规范应该由项目组自己定义,该级别日志的主要作用是对系统每一步的运行状态进行精确的记录。通过该种日志,可以查看某一个操作每一步的执 行过程,可以准确定位是何种操作,何种参数,何种顺序导致了某种错误的发生。可以保证在不重现错误的情况下,也可以通过DEBUG(或TRACE)级别的日志对问题进行诊断。需要注意的是,DEBUG日志也需要规范日志格式,应该保证除了记录日志的开发人员自己外,其他的如运维,测试人员等也可以通过 DEBUG(或TRACE)日志来定位问题;&/li&&/ul&&h2&5. 不断优化日志&/h2&&p&有一点可以肯定,好的日志就像好的文章一样,绝不是一遍就可以写好的,而需要在实际的运维过程中,结合线上问题的定位,不断地进行优化。最关键的一点是,团队要重视日志优化这件事情,不要让日志的质量持续降低(当项目变大时,项目的代码也存在一样的问题,越写越乱)。&/p&&p&此处有以下几个比较好的实践:&/p&&ul&&li&在定位问题的过程中完善日志,如果定位问题花费了很长时间,那就说明系统日志还存在问题,需要进一步完善和优化;&/li&&li&需要思考是否可以通过优化日志,来提前预判该问题是否可能发生(如某种资源耗尽而导致的错误,可以对资源的使用情况进行记录)&/li&&li&定义好整个团队记录日志的规范,保证每个开发记录的日志格式统一;特别需要说明的是,对于DEBUG/TRACE级别的日志,也需要定义好清晰的格式,而不是由开发人员自由发挥;&/li&&li&整个团队(包括开发,运维和测试)定期对记录的日志内容进行Review;&/li&&li&开发做运维,通过在查问题的过程来优化日志记录的方式;&/li&&li&运维或测试在日志中发现的问题,都需要及时向开发人员反映;&/li&&/ul&&h2&6. 关于RequestID&/h2&&p&&b&RequestID的作用&/b&&/p&&p&一个系统通常通过RequestID来对请求进行唯一的标记,目的是可以通过RequestID将一个请求在系统中的执行过程串联起来。该RequestID通常会随着响应返回给调用者,如果调用出现问题,调用者也可以通过提供RequestID帮助服务提供者定位问题。&/p&&p&&b&RequestID的生成:&/b&&/p&&p&需要根据实际的使用场景来选择:&/p&&ul&&li&对于简单的系统,可以简单采用一个随机数即可,例如&br&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&RequestID = md5(time.Now() + random.Int())
&/code&&/pre&&/div&这样简单的方式在一定的时间内是不用担心会冲突的&/li&&li&对于复杂的系统,需要在RequestID中编码更多的内容,例如:可以将处理请求的服务器IP,接收到请求的时间等信息编码到RequestID中,这样通过RequestID可以快速的了解请求属于哪台机器,然后进一步定位:&br&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&./decode.sh 4b2c009a0a9f42b8ca96
Thu Nov 21 11:06:12 CST 2013
10.120.202.150
&/code&&/pre&&/div&&/li&&li&对于一些特别的系统,RequestID也可以进行针对性的调整,例如在我实现的一个直播服务里,RequestID由两部分组成,第一部分是一个随机字符串(通过MD5生成),第二部分是一个不断在自增的整数:&br&&figure&&img src=&https://pic1.zhimg.com/v2-94fe6cdd4acb86be74fe4_b.png& data-rawwidth=&1534& data-rawheight=&286& class=&origin_image zh-lightbox-thumb& width=&1534& data-original=&https://pic1.zhimg.com/v2-94fe6cdd4acb86be74fe4_r.jpg&&&/figure&&br&对于直播系统,这样做的好处是通过RequestID的第一部分,可以快速搜索到一路直播流所有的日志,而第二部分自增的整数可以帮助快速定位一段时间的日志。&br&&/li&&/ul&&p&&b&RequestID串联起来的日志系统:&br&&/b&&/p&&p&通常一个服务由若干个子系统组成,拿网易对象存储举例,它包含了前端负载均衡节点、存储逻辑服务器、元数据集群、分布式存储集群、图片处理集群、音视频处理集群、缓存集群等。通常一个请求需要由若干个子系统,甚至所有的子系统的协同处理。这时,如果某个请求出错,再要定位到具体的出错原因就比较复杂了,因为常常需要到数十台机器上去定位日志。&/p&&p&当时的思路在负载均衡节点接收到请求后,就为请求生成一个全局唯一的RequestID,该请求所经过所有子系统系统,均基于该RequestID记录日志,这样通过将所有的日志收集起来,就可以通过这一个RequestID来得到完整的系统处理日志了。&/p&&p&然而这并不是一件容易做的事情:所有的系统间调用都需要进行改造,所有的日志输出的地方都要统一格式,而我们采用的有些开源组件实际上很难支持这种做法。&/p&&p&不过,有了这样的认识,我们组在开发新的底层分布式文件系统时,接口传入的第一个参数就是RequestID了。&/p&&h2&7. 动态日志输出&/h2&&p&上文已经讨论过,DEBUG日志和INFO日志的一个重要的区别是,INFO日志用于记录常规的系统运行状态,请求的基本的输入和输出等,对于定位一般的问题已经足够了。而DEBUG日志则详细的记录了一个请求的处理过程,甚至是每一个函数的输入和输出结果,遇到一些隐藏比较深的问题时,必须要依赖DEBUG日志。&/p&&p&然而,由于DEBUG级别的日志数量比INFO级别的数量多很多(通常差一个数量级),如果长期在线上服务器开启DEBUG级别的日志输出,日志量太大。再比如,有时候仅仅是由于某一个用户的访问模式比较特殊导致了问题,如果将整个服务(特别是一个服务部署了很多台节点时)都临时调整为DEBUG级别日志输出,也非常不方便。&/p&&p&下面介绍一种我采用的方式:&/p&&p&我们的系统采用如下的业务架构(简化版):&/p&&figure&&img src=&https://pic1.zhimg.com/v2-219de91c72a69f4ff748c_b.png& data-rawwidth=&500& data-rawheight=&148& class=&origin_image zh-lightbox-thumb& width=&500& data-original=&https://pic1.zhimg.com/v2-219de91c72a69f4ff748c_r.jpg&&&/figure&&br&&p&在业务处理层的Proxy中,实现如下逻辑:当接收到的HTTP请求的QueryString中包含&DEBUG=ON&参数时,就将所有的DEBUG级别的日志也输出:&/p&&figure&&img src=&https://pic1.zhimg.com/v2-460d1a355f70d2092540_b.png& data-rawwidth=&1314& data-rawheight=&458& class=&origin_image zh-lightbox-thumb& width=&1314& data-original=&https://pic1.zhimg.com/v2-460d1a355f70d2092540_r.jpg&&&/figure&&p&在负载均衡层的Openresty中,实现如下接口:管理员可以配置将哪个用户的哪个桶的哪个对象的哪种操作(注:这是对象存储中的几个概念)输出为DEBUG日志,Openresty会对每个请求进行过滤,当发现请求和配置的DEBUG日志输出条件相匹配时,则在请求的QueryString中新增&DEBUG=ON&参数。&/p&&p&通过这种方式,管理员可以随时配置哪些请求需要输出为DEBUG级别的日志,可以大大提高线上定位问题的效率。&/p&&h2&8. 慢操作日志&/h2&&p&服务在接收到一个请求的时候,记录请求的接收时间(T1),在请求处理完成待发送的时候,会记录请求发送时间(T2),通常一个请求的日志都记为INFO级别,然而当出现请求处理时间(T2-T1)超过一定时间(如10s)时,可以将该日志提升为WARN级别。通过该方法,可以预先发现系统可能存在的一些问题。&/p&&p&同样的慢操作日志还可以用来记录系统一些外部依赖的处理时间,如一个服务可能依赖外部认证服务器来进行认证授权。通过记录每次认证请求的时间并将超出预期时间的请求日志采用WARN级别输出,可以尽早发现认证服务器是不是需要扩容等问题。&/p&&p&慢日志的时间阈值应该是可以动态调整的,这样在进行系统优化时,可以将该报警时间阈值逐渐调小,不断地对系统进行优化。&br&&/p&&h2&9. 日志监控&/h2&&p&通过对日志中的关键字进行监控,可以及时发现系统故障并报警,这对于保证服务的SLA至关重要。&/p&&p&服务的监控和报警是一个很大的话题,此处只说日志监控报警需要注意的一些问题:&/p&&ol&&li&能不报警的就不报警,只有需要运维马上处理的错误才需要发送报警。这样做的原因是避免长期的报警骚扰让运维人员对报警不再敏感,最后真的报警来了时,变成了狼来了的传说;&/li&&li&明确报警关键字,例如用ERROR作为报警的关键字,而不是各种各样的复杂规则。这样做的原因是日志监控本质上是不断的进行字符串匹配操作,如果规则太多太复杂,就可能对线上服务产生影响;&/li&&li&对于一些预警操作,例如某个服务需要重试多次才能成功,或者某个用户的配额快用完等等,可以通过每天一封报警邮件的方式来反馈;&/li&&li&每一次系统出现故障,都需要及时检查日志报警是否灵敏,日志报警的描述是否准确等,不断优化日志报警;&/li&&/ol&&br&&h2&10. 其他的注意事项&/h2&&p&&b&上线后日志观察&/b&&/p&&p&每一次上线完成后,除了对系统进行完整的回归测试外,还需要对日志进行观察,特别是当上线新功能以后,可以通过日志确认新功能是否工作正常。&/p&&p&&b&日志输出到不同的文件&/b&&/p&&p&在性能测试时遇到的另一个问题是,当并发量很大时,可能会有一些请求处理失败(如0.5%),为了对这些错误进行分析,需要去查这些错误请求的日志。而由于这种情况下日志量巨大,使得对错误日志的分析变得困难。&/p&&p&这种情况下可以将所有的错误日志同时输出到一个单独的文件之中。&/p&&p&&b&日志文件的大小&/b&&/p&&p&日志文件不宜过大,过大的日志文件对于日志监控,问题定位等都会带来不便。因此需要进行日志文件的切分,日志文件应该按天来分割,还是按照小时来分割,应该根据日志量来决定,原则就是方便开发或运维人员能快速查找日志。&/p&&p&为了防止日志文件将整个磁盘空间占满,需要定期对日志文件进行删除。例如,在收到磁盘报警时,可以将两个月以前的日志文件删除。此处比较好的实践是:&/p&&ul&&li&将所有日志文件收集起来,这样即使在记录日志的机器上删除,也可以通过收集的日志对之前的问题进行定位;&/li&&li&每天通过定时任务来删除过期日志,如每天在凌晨4点删除60天前的日志&/li&&/ul&&h2&11. 总结&/h2&&p&对文中提出的所有建议总结如下:&/p&&ul&&li&充分认识到日志对于一个可靠的后端系统的关键作用&br&&/li&&li&整个团队(包括运维人员)需要对日志级别有明确的规定,什么日志输出为什么级别,什么级别的错误出现要如何处理等&/li&&li&需要定期对日志内容进行优化更新,目的就是通过日志快速准确地定位问题&/li&&li&要明确不同日志的用途,对日志内容进行分类&/li&&li&绝不要打印没有用的日志,防止无用日志淹没重要信息&/li&&li&日志信息要准确全面,努力做到仅凭日志就可以定位问题&/li&&li&日志的优化是一件需要持续不断投入精力的事,需要不断从错误中学习&/li&&li&根据不同的目的生成RequestID,必要时在RequestID中尽量编码更多的信息&/li&&li&将一个请求的整个处理流程和唯一的RequestID关联起来&/li&&li&支持动态日志输出,方便线上问题定位&/li&&li&新上线服务器后一定要对日志进行观察,特别地,开发人员可以通过观察日志来确认新功能是否工作正常&/li&&li&通过日志级别的提升来发现潜在问题&/li&&li&对日志进行监控报警,比客户先发现系统问题&/li&&li&通过日志中的关键字来确定系统的运行状态&/li&&li&日志格式要统一规范&/li&&li&将错误日志输出到一个单独的文件中进行分析&/li&&li&要把日志的大小,如何切分,如何删除等作为规范建立起来&/li&&/ul&
0. 缘起大约在三年前,我曾经写过一篇 ,还被码农周刊选为那年的
之一。当时我任职于网易杭州研究院的存储平台组,主要做网易对象存储(NOS)的开发和部分运维工作。由于网易云音乐,易信等几个重要产品陆续上线,业务压力剧…
&figure&&img src=&https://pic3.zhimg.com/v2-cafd2007dae0c701bb28_b.jpg& data-rawwidth=&1000& data-rawheight=&563& class=&origin_image zh-lightbox-thumb& width=&1000& data-original=&https://pic3.zhimg.com/v2-cafd2007dae0c701bb28_r.jpg&&&/figure&&h2&说明&/h2&&p&goraft是Raft协议的Golang版本的实现,项目地址为:&a href=&https://link.zhihu.com/?target=https%3A//github.com/goraft/raft& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&goraft/raft&/a&。整个代码质量较高,值得仔细品味。因此,整理了该博文探究下其内部实现。&/p&&h2&数据结构&/h2&&p&goraft主要抽象了server、peer和log三个结构,分别代表服务节点、Follower节点和日志。&/p&&p&&b&server&/b&&/p&&p&Raft作为一种多节点状态一致性维护协议,运行过程中必然涉及到多个物理节点,server就是用来抽象其中的每个节点,维护节点的状态信息。其结构如下:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&type server struct {
*eventDispatcher
transporter Transporter
interface{}
currentTerm uint64
map[string]*Peer
sync.RWMutex
syncedPeer map[string]bool
electionTimeout
time.Duration
heartbeatInterval time.Duration
snapshot *Snapshot
// PendingSnapshot is an unfinished snapshot.
// After the pendingSnapshot is saved to disk,
// it will be set to snapshot and also will be
// set to nil.
pendingSnapshot *Snapshot
stateMachine
StateMachine
maxLogEntriesPerRequest uint64
connectionString string
routineGroup sync.WaitGroup
&/code&&/pre&&/div&&ul&&li&state:每个节点总是处于以下状态的一种:follower、candidate、leader&/li&&li&currentTerm:Raft协议关键概念,每个term内都会产生一个新的leader&/li&&li&peers:raft中每个节点需要了解其他节点信息,尤其是leader节点&/li&&li&syncedPeer:对于leader来说,该成员记录了日志已经被sync到了哪些follower&/li&&li&c:当前节点的命令通道,所有的命令都通过该channel来传递&/li&&li&pendingSnapshot:暂时未知&/li&&/ul&&p&&b&peer&/b& &br&&br&peer描述的是集群中其他节点的信息,结构如下:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&// A peer is a reference to another server involved in the consensus protocol.
type Peer struct {
string `json:&name&`
ConnectionString
string `json:&connectionString&`
prevLogIndex
heartbeatInterval time.Duration
lastActivity
sync.RWMutex
&/code&&/pre&&/div&&ul&&li&server:peer中的某些方法会依赖server的状态,如peer内的appendEntries方法需要获取server的currentTerm&/li&&li&Name:peer的名称&/li&&li&ConnectionString:peer的ip地址,形式为”ip:port”&/li&&li&prevLogIndex:这个很关键,记录了该peer的当前日志index,接下来leader将该index之后的日志继续发往该peer&/li&&li&lastActivity:记录peer的上次活跃时间&/li&&/ul&&p&&b&log&/b&&/p&&p&log是Raft协议的核心,Raft使用日志来存储客户发起的命令,并通过日志内容的同步来维护多节点上状态的一致性。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&// A log is a collection of log entries that are persisted to durable storage.
type Log struct {
func(*LogEntry, Command) (interface{}, error)
[]*LogEntry
commitIndex uint64
sync.RWMutex
startIndex
initialized bool
&/code&&/pre&&/div&&ul&&li&ApplyFunc:日志被应用至状态机的方法,这个应该由使用raft的客户决定&/li&&li&file:日志文件句柄&/li&&li&path:日志文件路径&/li&&li&entries:内存日志项缓存&/li&&li&commitIndex:日志提交点,小于该提交点的日志均已经被应用至状态机&/li&&li&startIndex/startTerm:日志中起始日志项的index和term&/li&&/ul&&p&&b&log entry&/b&&/p&&p&log entry是客户发起的command存储在日志文件中的内容&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&type LogEntry struct {
*uint64 `protobuf:&varint,1,req& json:&Index,omitempty&`
*uint64 `protobuf:&varint,2,req& json:&Term,omitempty&`
CommandName
*string `protobuf:&bytes,3,req& json:&CommandName,omitempty&`
`protobuf:&bytes,4,opt& json:&Command,omitempty&`
XXX_unrecognized []byte
`json:&-&`
// A log entry stores a single item in the log.
type LogEntry struct {
*protobuf.LogEntry
Position int64 // position in the log file
&/code&&/pre&&/div&&ul&&li&LogEntry是日志项在内存中的描述结构,其最终存储在日志文件是经过protocol buffer编码以后的信息&/li&&li&Position代表日志项存储在日志文件内的偏移&/li&&li&编码后的日志项包含Index、Term,原始Command的名称以及Command具体内容&/li&&/ul&&h2&关键流程&/h2&&p&&b&客户端请求&/b&&/p&&p&客户端使用go-raft的时候,先初始化环境,这里不仔细描述,接下来看客户如何发起一个请求:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&command := &raft.DefaultJoinCommand{}
if _, err := s.raftServer.Do(command); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
&/code&&/pre&&/div&&p&客户命令执行的入口是Do:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&func (s *server) Do(command Command) (interface{}, error) {
return s.send(command)
// Sends an event to the event loop to be processed. The function will wait until the event is actually processed before returning.
func (s *server) send(value interface{}) (interface{}, error) {
if !s.Running() {
return nil, StopError
event := &ev{target: value, c: make(chan error, 1)}
case s.c &- event:
case &-s.stopped:
return nil, StopError
case &-s.stopped:
return nil, StopError
case err := &-event.c:
return event.returnValue, err
&/code&&/pre&&/div&&p&send的处理流程很简单,首先将命令写入到server的命令channel,然后等待命令处理完成。&/p&&p&而server作为leader启动完成时会进入一个leaderLoop来处理所有用户的命令:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&func (s *server) leaderLoop() {
logIndex, _ := s.log.lastInfo()
// Begin to collect response from followers
for s.State() == Leader {
case &-s.stopped:
case e := &-s.c:
switch req := e.target.(type) {
// 代表客户端命令
case Command:
s.processCommand(req, e)
&/code&&/pre&&/div&&p&processCommand处理如下:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&// Processes a command.
func (s *server) processCommand(command Command, e *ev) {
s.debugln(&server.command.process&)
// Create an entry for the command in the log.
entry, err := s.log.createEntry(s.currentTerm, command, e)
if err != nil {
s.debugln(&server.command.log.entry.error:&, err)
e.c &- err
if err := s.log.appendEntry(entry); err != nil {
s.debugln(&server.command.log.error:&, err)
e.c &- err
s.syncedPeer[s.Name()] = true
if len(s.peers) == 0 {
commitIndex := s.log.currentIndex()
s.log.setCommitIndex(commitIndex)
s.debugln(&commit index &, commitIndex)
&/code&&/pre&&/div&&p&这里的逻辑比较简单,创建日志项并将日志项append至日志文件,如果过程中由任何错误,就将这个错误写入e.c:e.c &- err,这样等待在该channel的客户端就会收到通知,立即返回。&/p&&p&如果没有错误,这时候客户端还是处于等待状态的,这是因为虽然该Command被leader节点成功处理了,但是该Command的日志还没有被同步至大多数Follow节点,因此该Command也就无法被提交,所以发起该Command的客户端依然等在那,Command被提交,这在后面的日志同步过程中会有所体现。&/p&&p&&b&日志同步&/b&&/p&&p&go-raft的leader向Follower同步日志是在heartbeat中完成的:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&// Listens to the heartbeat timeout and flushes an AppendEntries RPC.
func (p *Peer) heartbeat(c chan bool) {
stopChan := p.stopChan
ticker := time.Tick(p.heartbeatInterval)
case flush := &-stopChan:
if flush {
// before we can safely remove a node
// we must flush the remove command to the node first
case &-ticker:
start := time.Now()
duration := time.Now().Sub(start)
p.server.DispatchEvent(newEvent(HeartbeatEventType, duration, nil))
func (p *Peer) flush() {
debugln(&peer.heartbeat.flush: &, p.Name)
prevLogIndex := p.getPrevLogIndex()
term := p.server.currentTerm
entries, prevLogTerm := p.server.log.getEntriesAfter(prevLogIndex, p.server.maxLogEntriesPerRequest)
if entries != nil {
p.sendAppendEntriesRequest(newAppendEntriesRequest(term, prevLogIndex, prevLogTerm, p.server.log.CommitIndex(), p.server.name, entries))
p.sendSnapshotRequest(newSnapshotRequest(p.server.name, p.server.snapshot))
&/code&&/pre&&/div&&p&核心的逻辑是将leader上的日志通过构造一个AppendEntriesRequest发送给从节点,当然只同步那些Follower上还没有的日志,即prevLogIndex以后的log entry。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&// Sends an AppendEntries request to the peer through the transport.
func (p *Peer) sendAppendEntriesRequest(req *AppendEntriesRequest) {
resp := p.server.Transporter().SendAppendEntriesRequest(p.server, p, req)
if resp == nil {
p.server.DispatchEvent(newEvent(HeartbeatIntervalEventType, p, nil))
p.setLastActivity(time.Now())
// If successful then update the previous log index.
if resp.Success() {
resp.peer = p.Name
// Send response to server for processing.
p.server.sendAsync(resp)
&/code&&/pre&&/div&&p&这里会将Follower的心跳的响应继续发送给server。server会在leaderLoop中处理该类消息:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&func (s *server) leaderLoop() {
logIndex, _ := s.log.lastInfo()
// Begin to collect response from followers
for s.State() == Leader {
case e := &-s.c:
switch req := e.target.(type) {
case Command:
s.processCommand(req, e)
case *AppendEntriesRequest:
e.returnValue, _ = s.processAppendEntriesRequest(req)
case *AppendEntriesResponse:
s.processAppendEntriesResponse(req)
case *RequestVoteRequest:
e.returnValue, _ = s.processRequestVoteRequest(req)
// Callback to event.
e.c &- err
s.syncedPeer = nil
&/code&&/pre&&/div&&p&处理Follower的响应在函数processAppendEntriesResponse中:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&func (s *server) processAppendEntriesResponse(resp *AppendEntriesResponse) {
// If we find a higher term then change to a follower and exit.
if resp.Term() & s.Term() {
s.updateCurrentTerm(resp.Term(), &&)
// panic response if it's not successful.
if !resp.Success() {
// if one peer successfully append a log from the leader term,
// we add it to the synced list
if resp.append == true {
s.syncedPeer[resp.peer] = true
if len(s.syncedPeer) & s.QuorumSize() {
// Determine the committed index that a majority has.
var indices []uint64
indices = append(indices, s.log.currentIndex())
for _, peer := range s.peers {
indices = append(indices, peer.getPrevLogIndex())
sort.Sort(sort.Reverse(uint64Slice(indices)))
commitIndex := indices[s.QuorumSize()-1]
committedIndex := s.log.commitIndex
if commitIndex & committedIndex {
s.log.sync()
s.log.setCommitIndex(commitIndex)
&/code&&/pre&&/div&&p&这里会判断如果多数的Follower都已经同步日志了,那么就可以检查所有的Follower此时的日志点,并根据log index排序,leader会算出这些Follower的提交点,然后提交,调用setCommitIndex。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&// Updates the commit index and writes entries after that index to the stable storage.
func (l *Log) setCommitIndex(index uint64) error {
l.mutex.Lock()
defer l.mutex.Unlock()
// this is not error any more after limited the number of sending entries
// commit up to what we already have
if index & l.startIndex+uint64(len(l.entries)) {
index = l.startIndex + uint64(len(l.entries))
if index & l.commitIndex {
return nil
for i := l.commitIndex + 1; i &= i++ {
entryIndex := i - 1 - l.startIndex
entry := l.entries[entryIndex]
l.commitIndex = entry.Index()
// Decode the command.
command, err := newCommand(entry.CommandName(), entry.Command())
if err != nil {
return err
returnValue, err := l.ApplyFunc(entry, command)
if entry.event != nil {
entry.event.returnValue = returnValue
entry.event.c &- err
_, isJoinCommand := command.(JoinCommand)
if isJoinCommand {
return nil
return nil
&/code&&/pre&&/div&&p&这里的提交主要是设置好commitIndex,并且将日志项中的Command应用到状态机。最后,判断这个LogEntry是不是由客户直接发起的,如果是,那么还需要将状态机的处理结果通过event.c返回给客户端,这样,客户端就可以返回了,请回顾上面的客户端请求。&/p&&p&&b&选主&/b&&/p&&p&在Raft协议运行过程中,Leader节点会周期性的给Follower发送心跳,心跳的作用有二:一方面,Follower通过心跳确认Leader此时还是活着的;第二,Leader通过心跳将自身的日志同步发送给Follower。&/p&&p&但是,如果Follower在超过一定时间后没有收到Leader的心跳信息,就认定Leader可能离线,于是,该Follower就会变成Candidate,发起一次选主,通知其他节点开始为我投票。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&func (s *server) followerLoop() {
since := time.Now()
electionTimeout := s.ElectionTimeout()
timeoutChan := afterBetween(s.ElectionTimeout(), s.ElectionTimeout()*2)
for s.State() == Follower {
var err error
update := false
// 超过一定时间未收到请求
case &-timeoutChan:
if s.promotable() {
// 状态变为Candidate
s.setState(Candidate)
update = true
// The main event loop for the server
func (s *server) loop() {
defer s.debugln(&server.loop.end&)
state := s.State()
for state != Stopped {
switch state {
case Follower:
s.followerLoop()
// 状态变为Candidate后,进入candidateLoop
case Candidate:
s.candidateLoop()
case Leader:
s.leaderLoop()
case Snapshotting:
s.snapshotLoop()
state = s.State()
&/code&&/pre&&/div&&p&当节点状态由Follower变为Candidate后,就会进入candidateLoop来触发一次选主过程。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&func (s *server) candidateLoop() {
for s.State() == Candidate {
if doVote {
s.currentTerm++
s.votedFor = s.name
// 向所有其他节点发起Vote请求
respChan = make(chan *RequestVoteResponse, len(s.peers))
for _, peer := range s.peers {
s.routineGroup.Add(1)
go func(peer *Peer) {
defer s.routineGroup.Done()
peer.sendVoteRequest(newRequestVoteRequest(s.currentTerm, s.name, lastLogIndex, lastLogTerm), respChan)
// 自己给自己投一票
votesGranted = 1
timeoutChan = afterBetween(s.ElectionTimeout(), s.ElectionTimeout()*2)
doVote = false
// 如果多数节点同意我作为Leader,设置新状态
if votesGranted == s.QuorumSize() {
s.setState(Leader)
// 等待其他节点的选主请求的响应
case &-s.stopped:
s.setState(Stopped)
case resp := &-respChan:
if success := s.processVoteResponse(resp); success {
votesGranted++
case &-timeoutChan:
// 如果再一次超时了,重新发起选主请求
doVote = true
&/code&&/pre&&/div&&p&别看上面的代码很多,但是其中逻辑非常清楚。就不作过多说明了。&/p&&p&上面描述了一个Follower节点变为Candidate后,如何发起一次选主,接下来看看一个节点在收到其他节点发起的选主请求后的处理,在函数processRequestVoteRequest():&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&// Processes a &request vote& request.
func (s *server) processRequestVoteRequest(req *RequestVoteRequest) (*RequestVoteResponse, bool)
if req.Term & s.Term() {
return newRequestVoteResponse(s.currentTerm, false), false
if req.Term & s.Term() {
s.updateCurrentTerm(req.Term, &&)
} else if s.votedFor != && && s.votedFor != req.CandidateName {
return newRequestVoteResponse(s.currentTerm, false), false
lastIndex, lastTerm := s.log.lastInfo()
if lastIndex & req.LastLogIndex || lastTerm & req.LastLogTerm {
return newRequestVoteResponse(s.currentTerm, false), false
s.votedFor = req.CandidateName
return newRequestVoteResponse(s.currentTerm, true), true
&/code&&/pre&&/div&&p&接受一个远程节点的选主请求需要满足以下条件:&/p&&ul&&li&远程节点的term必须要大于等于当前节点的term;&/li&&li&远程节点的log必须比当前节点的更新;&/li&&li&当前节点的term和远程节点的选主请求的term如果一样且当前节点未给任何其他节点投出自己的选票。&/li&&/ul&&p&整个流程其实也是蛮简单的。&/p&&p&&b&节点变更&/b&&/p&&p&在Raft协议中,节点的变更也是作为一个客户的命令通过一致性协议统一管理:也就是说,节点变更命令被写入Leader的日志,然后再由Leader同步到Follower,最后如果多数Follower成功写入该日志,主节点提交该日志。&/p&&p&在Go-Raft中,存在两种节点变更命令:DefaultJoinCommand和DefaultLeaveCommand,对于这两种命令的处理关键在于这两个命令的Apply方法,如下:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&func (c *DefaultJoinCommand) Apply(server Server) (interface{}, error) {
err := server.AddPeer(c.Name, c.ConnectionString)
return []byte(&join&), err
func (c *DefaultLeaveCommand) Apply(server Server) (interface{}, error) {
err := server.RemovePeer(c.Name)
return []byte(&leave&), err
&/code&&/pre&&/div&&p&增加节点最终的提交方法是AddPeer:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&func (s *server) AddPeer(name string, connectiongString string) error {
if s.peers[name] != nil {
return nil
if s.name != name {
peer := newPeer(s, name, connectiongString, s.heartbeatInterval)
// 如果是主上新增一个peer,那还需要启动后台协程发送
if s.State() == Leader {
peer.startHeartbeat()
s.peers[peer.Name] = peer
s.DispatchEvent(newEvent(AddPeerEventType, name, nil))
// Write the configuration to file.
s.writeConf()
return nil
// Removes a peer from the server.
func (s *server) RemovePeer(name string) error {
// Skip the Peer if it has the same name as the Server
if name != s.Name() {
// Return error if peer doesn't exist.
peer := s.peers[name]
if peer == nil {
return fmt.Errorf(&raft: Peer not found: %s&, name)
// 如果是Leader,停止给移除节点的心跳协程
if s.State() == Leader {
s.routineGroup.Add(1)
go func() {
defer s.routineGroup.Done()
peer.stopHeartbeat(true)
delete(s.peers, name)
s.DispatchEvent(newEvent(RemovePeerEventType, name, nil))
// Write the configuration to file.
s.writeConf()
return nil
&/code&&/pre&&/div&&p&&b&Snapshot&/b&&/p&&p&根据Raft论文描述,随着系统运行,存储命令的日志文件会一直增长,为了避免这种情况,论文中引入了Snapshot。Snapshot的出发点很简单:淘汰掉那些无用的日志项,那么问题就来了:&/p&&ul&&li&哪些日志项是无用的,可以丢弃?&/li&&li&如何丢弃无用日志项?&/li&&/ul&&p&接下来我们各个击破:&/p&&ul&&li&如果某个日志项中存储的用户命令(Command)已经被提交到状态机中,那么它就被视为无用的,可以被清理;&/li&&li&因为日志的提交是按照index顺序执行的,因此,只要知道当前副本的提交点(commit index),那么在此之前的所有日志项必然也已经被提交了,因此,这个提交点之前(包括该提交点)的日志都可以被删除。实现上,只要将提交点之后的日志写入新的日志文件,再删除老的日志文件,就大功告成了;&/li&&li&最后需要注意的一点是:在回收日志文件之前,必须要对当前的系统状态机进行保存,否则,状态机数据丢失以后,又删了日志,状态真的就无法恢复了。&/li&&/ul&&p&goraft的Snapshot是由应用主动触发的,调用其内部函数TakeSnapshot:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&func (s *server) TakeSnapshot() error {
lastIndex, lastTerm := s.log.commitInfo()
path := s.SnapshotPath(lastIndex, lastTerm)
s.pendingSnapshot = &Snapshot{lastIndex, lastTerm, nil, nil, path}
// 首先应用保存状态机当前状态
state, err := s.stateMachine.Save()
if err != nil {
return err
// 准备Snapshot状态:包括当前日志的index,当前peer等
peers := make([]*Peer, 0, len(s.peers)+1)
for _, peer := range s.peers {
peers = append(peers, peer.clone())
s.pendingSnapshot.Peers = peers
s.pendingSnapshot.State = state
s.saveSnapshot()
// 最后,回收日志项:s.log.compact()
if lastIndex-s.log.startIndex & NumberOfLogEntriesAfterSnapshot {
compactIndex := lastIndex - NumberOfLogEntriesAfterSnapshot
compactTerm := s.log.getEntry(compactIndex).Term()
s.log.compact(compactIndex, compactTerm)
return nil
&/code&&/pre&&/div&&p&关于compact()函数就不作仔细描述了,有兴趣的朋友可以自行阅读,非常简单的。&/p&
说明goraft是Raft协议的Golang版本的实现,项目地址为:。整个代码质量较高,值得仔细品味。因此,整理了该博文探究下其内部实现。数据结构goraft主要抽象了server、peer和log三个结构,分别代表服务节点、Follower节点和日志。serverRaft作为一…
&p&对于初学者来说,可能看机器学习的mooc更过瘾吧!除了大名鼎鼎andrew ng的ml,林轩田的机器学习基础和技法,我分享我正在看的其它两门课:&/p&&p&&b&1.华盛顿大学的machine learning specialization&/b&&/p&&p&课程地址:&a href=&//link.zhihu.com/?target=https%3A//www.coursera.org/specializations/machine-learning& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Machine Learning | Coursera&/a&&/p&&p&共四门课程,优势是讲解非常清晰,事无巨细,作业不难但是很多,绝对是对于看不了PRML,处在绝望边缘的机器小白打基础的好课。&/p&&p&&b&2.优达学城的无人驾驶第一学期&/b&&/p&&p&课程地址:&a href=&//link.zhihu.com/?target=https%3A//www.udacity.com/drive& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Self-Driving Car Engineer&/a&&/p&&p&主要讲深度学习在无人驾驶上的应用,对于想入门DL但望着CNN/RNN不知所措的小白绝对是最好的入门课,实践项目有用CNN来实现自动驾驶,用HOG+SVM实现车辆识别等等。&/p&&p&放三张图,分别是这个课程里三个作业,第一个是车辆识别,第二个是车道跟踪,第三个是最终项目在模拟器里自动驾驶(都是gif,但图太大了,先上传静态图啦)&/p&&figure&&img src=&https://pic4.zhimg.com/v2-0d5ee1a53e44e21da4c9ed2a9b2b54c3_b.png& data-rawwidth=&418& data-rawheight=&226& class=&content_image& width=&418&&&/figure&&br&&figure&&img src=&https://pic3.zhimg.com/v2-6b027d0e4cafda576561fa_b.png& data-rawwidth=&420& data-rawheight=&226& class=&content_image& width=&420&&&/figure&&br&&figure&&img src=&https://pic3.zhimg.com/v2-7a00f69b3b126acb3146_b.png& data-rawwidth=&419& data-rawheight=&211& class=&content_image& width=&419&&&/figure&&br&&p&&b&顺便安利一下我正在学习的华盛顿大学的machine learning笔记,希望能给大家一些帮助。&/b&&/p&&a href=&https://zhuanlan.zhihu.com/p/& class=&internal&&《Machine Learning》课程笔记索引贴 - 知乎专栏&/a&
对于初学者来说,可能看机器学习的mooc更过瘾吧!除了大名鼎鼎andrew ng的ml,林轩田的机器学习基础和技法,我分享我正在看的其它两门课:1.华盛顿大学的machine learning specialization课程地址:共四门课程,优势是讲解非常…
&figure&&img src=&https://pic1.zhimg.com/v2-26f318a05ad8fb986dbb0c9e8db56fe5_b.jpg& data-rawwidth=&425& data-rawheight=&182& class=&origin_image zh-lightbox-thumb& width=&425& data-original=&https://pic1.zhimg.com/v2-26f318a05ad8fb986dbb0c9e8db56fe5_r.jpg&&&/figure&channel是go语言的一大特色,使用原子函数还是使用互斥锁都不如使用channel来的简单,go语言中的channel可以作为函数参数传递和返回值返回,通过发送和接受数据在goroutine之间同步(在学习和使用go语言的时候,我们应该牢记,go语言中所有的结构都是值拷贝的)&p&
本文不对channel使用作讲解,直接上&b&酸(dai)菜(ma)&/b&:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&type hchan struct {
//队列数据总的数据数量
dataqsiz uint
//环形队列的数据大小
unsafe.Pointer //指向dataqsiz元素类型大小的数组
elemsize uint16
elemtype *_type // 元素类型
// 发送数据时的游标
// 接收数据时的游标
// 接收而阻塞的等待队列
// 发送而阻塞的等待队列
lock mutex
// 保护hchan所有字段的锁
&/code&&/pre&&/div&hchan 是chan的结构体,在hchan结构中qcount和elemsize指定队列的容量和使用量,dataqsiz队列的大小,整个hchan结构体只记录了队列大小相关的值,带有缓冲区的chan需要make的时候指定,我们简单的看一下chan的make方法是如何分配缓冲区的&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&func makechan(t *chantype, size int64) *hchan {
elem := t.elem
var c *hchan
if elem.kind&kindNoPointers != 0 || size == 0 {
c = (*hchan)(mallocgc(hchanSize+uintptr(size)*elem.size, nil, true))
if size & 0 && elem.size != 0 {
c.buf = add(unsafe.Pointer(c), hchanSize)
c.buf = unsafe.Pointer(c)
c = new(hchan)
c.buf = newarray(elem, int(size))
&/code&&/pre&&/div&&p&makechan 将hchan初始化0值之后并判断size如果是有缓冲区的chan则紧挨着hchan结构体中分配size大小的 “_type” 类型的数组。&br&&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&type waitq struct {
first *sudog
type sudog struct {
selectdone *uint32
unsafe.Pointer
acquiretime int64
releasetime int64
*sudog // g.waiting list
*hchan // channel
&/code&&/pre&&/div&&p&g和elem分别存储goroutine的数据&br&&/p&&blockquote&&b&发送channel&/b&&/blockquote&&p&
向channel中写数据时在runtime包中对应的是,以下方法:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&func chansend(t *chantype, c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
if raceenabled {
raceReadObjectPC(t.elem, ep, callerpc, funcPC(chansend))
if msanenabled {
msanread(ep, t.elem.size)
if c == nil {
if !block {
return false
gopark(nil, nil, &chan send (nil chan)&, traceEvGoStop, 2)
throw(&unreachable&)
if debugChan {
print(&chansend: chan=&, c, &\n&)
if raceenabled {
racereadpc(unsafe.Pointer(c), callerpc, funcPC(chansend))
if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
(c.dataqsiz & 0 && c.qcount == c.dataqsiz)) {
return false
var t0 int64
if blockprofilerate & 0 {
t0 = cputicks()
lock(&c.lock)
if c.closed != 0 {
unlock(&c.lock)
panic(plainError(&send on closed channel&))
if sg := c.recvq.dequeue(); sg != nil {
send(c, sg, ep, func() { unlock(&c.lock) })
return true
if c.qcount & c.dataqsiz {
qp := chanbuf(c, c.sendx)
if raceenabled {
raceacquire(qp)
racerelease(qp)
typedmemmove(c.elemtype, qp, ep)
if c.sendx == c.dataqsiz {
c.sendx = 0
c.qcount++
unlock(&c.lock)
return true
if !block {
unlock(&c.lock)
return false
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
mysg.elem = ep
mysg.waitlink = nil
mysg.g = gp
mysg.selectdone = nil
mysg.c = c
gp.waiting = mysg
gp.param = nil
c.sendq.enqueue(mysg)
goparkunlock(&c.lock, &chan send&, traceEvGoBlockSend, 3)
if mysg != gp.waiting {
throw(&G waiting list is corrupted&)
gp.waiting = nil
if gp.param == nil {
if c.closed == 0 {
throw(&chansend: spurious wakeup&)
panic(plainError(&send on closed channel&))
gp.param = nil
if mysg.releasetime & 0 {
blockevent(mysg.releasetime-t0, 2)
mysg.c = nil
releaseSudog(mysg)
return true
&/code&&/pre&&/div&&p&发送数据时先判断channel类型,如果有缓冲区,判断channel是否还有空间,然后从等待channel中获取等待channel中的接受者,如果取到接收者,则将对象直接传递给接受者,然后将接受者所在的go放入P所在的可运行G队列,发送过程完成,如果未取到接收者,则将发送者enqueue到发送channel,发送者进入阻塞状态,有缓冲的channel需要先判断channel缓冲是否还有空间,如果缓冲空间已满,则将发送者enqueue到发送channel,发送者进入阻塞状态如果缓冲空间未满,则将元素copy到缓冲中,这时发送者就不会进入阻塞状态,最后尝试唤醒等待队列中的一个接受者。&/p&&br&&br&&blockquote&&b&接收channel &/b&&/blockquote&&p&
向channel中接收数据时在runtime包中对应的是,以下方法:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&func chanrecv(t *chantype, c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// raceenabled: don't need to check ep, as it is always on the stack
// or is new memory allocated by reflect.
if debugChan {
print(&chanrecv: chan=&, c, &\n&)
if c == nil {
if !block {
gopark(nil, nil, &chan receive (nil chan)&, traceEvGoStop, 2)
throw(&unreachable&)
// Fast path: check for failed non-blocking operation without acquiring the lock.
// After observing that the channel is not ready for receiving, we observe that the
// channel is not closed. Each of these observations is a single word-sized read
// (first c.sendq.first or c.qcount, and second c.closed).
// Because a channel cannot be reopened, the later observation of the channel
// being not closed implies that it was also not closed at the moment of the
// first observation. We behave as if we observed the channel at that moment
// and report that the receive cannot proceed.
// The order of operations is important here: reversing the operations can lead to
// incorrect behavior when racing with a close.
if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
c.dataqsiz & 0 && atomic.Loaduint(&c.qcount) == 0) &&
atomic.Load(&c.closed) == 0 {
var t0 int64
if blockprofilerate & 0 {
t0 = cputicks()
lock(&c.lock)
if c.closed != 0 && c.qcount == 0 {
if raceenabled {
raceacquire(unsafe.Pointer(c))
unlock(&c.lock)
if ep != nil {
typedmemclr(c.elemtype, ep)
return true, false
if sg := c.sendq.dequeue(); sg != nil {
// Found a waiting sender. If buffer is size 0, receive value
// directly from sender. Otherwise, receive from head of queue
// and add sender's value to the tail of the queue (both map to
// the same buffer slot because the queue is full).
recv(c, sg, ep, func() { unlock(&c.lock) })
return true, true
if c.qcount & 0 {
// Receive directly from queue
qp := chanbuf(c, c.recvx)
if raceenabled {
raceacquire(qp)
racerelease(qp)
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
typedmemclr(c.elemtype, qp)
if c.recvx == c.dataqsiz {
c.recvx = 0
c.qcount--
unlock(&c.lock)
return true, true
if !block {
unlock(&c.lock)
return false, false
// no sender available: block on this channel.
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
// No stack splits between assigning elem and enqueuing mysg
// on gp.waiting where copystack can find it.
mysg.elem = ep
mysg.waitlink = nil
gp.waiting = mysg
mysg.g = gp
mysg.selectdone = nil
mysg.c = c
gp.param = nil
c.recvq.enqueue(mysg)
goparkunlock(&c.lock, &chan receive&, traceEvGoBlockRecv, 3)
// someone woke us up
if mysg != gp.waiting {
throw(&G waiting list is corrupted&)
gp.waiting = nil
if mysg.releasetime & 0 {
blockevent(mysg.releasetime-t0, 2)
closed := gp.param == nil
gp.param = nil
mysg.c = nil
releaseSudog(mysg)
return true, !closed
&/code&&/pre&&/div&&p&接收channel与发送类似首先也是判断channel的类型,然后如果是有缓冲的channel就判断缓冲中是否有元素,接着从channel中获取接受者,如果取到,则直接从接收者获取元素,并唤醒发送者,本次接收过程完成,如果没有取到接收者,阻塞当前的goroutine并等待发送者唤醒,如果是拥有缓冲的channel需要先判断缓冲中是否有元素,缓冲为空时,阻塞当前goroutine并等待发送者唤醒,缓冲如果不为空,则取出缓冲中的第一个元素,然后尝试唤醒channel中的一个发送者(这篇文章暂属临时版本,有些话需要斟酌,不久会更新。。。)&br&&/p&&figure&&img src=&https://pic4.zhimg.com/v2-d93dec6c47b_b.png& data-rawwidth=&172& data-rawheight=&100& class=&content_image& width=&172&&&/figure&&p&&b&接下来我会发表select的结构先说个预告。。。&/b&&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&select {
case c &- v:
//select的case和default 编译器最终会编译成if else
if selectnbsend(c, v) {
&/code&&/pre&&/div&&p&&b&未来几天我会完成select的具体实现。。。&/b&&/p&
channel是go语言的一大特色,使用原子函数还是使用互斥锁都不如使用channel来的简单,go语言中的channel可以作为函数参数传递和返回值返回,通过发送和接受数据在goroutine之间同步(在学习和使用go语言的时候,我们应该牢记,go语言中所有的结构都是值拷贝…
&p&&b&&a href=&//link.zhihu.com/?target=https%3A//gcc.godbolt.org/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Compiler Explorer - C++&/a& &/b&&/p&&p&这个可以方便对照C++代码编译出来对应的汇编指令&/p&&br&&br&&p&&b&&a href=&//link.zhihu.com/?target=https%3A//wandbox.org/& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&[Wandbox]三へ( へ?? ?)へ ????&/a&
&/b&&/p&&p&这个支持的编译器和版本比较多&/p&
这个可以方便对照C++代码编译出来对应的汇编指令
这个支持的编译器和版本比较多
&figure&&img src=&https://pic2.zhimg.com/v2-5c63e99478e9dcf78fe2_b.jpg& data-rawwidth=&840& data-rawheight=&630& class=&origin_image zh-lightbox-thumb& width=&840& data-original=&https://pic2.zhimg.com/v2-5c63e99478e9dcf78fe2_r.jpg&&&/figure&&p&Go接口的设计和实现是Go整个类型系统的一大特点。接口组合和嵌入、duck typing等实现了优雅的代码复用、解耦、模块化的特性,而且接口是方法动态分派、反射的实现基础(当然更基础的是编译期为运行时提供的类型信息)。理解了接口的实现之后,就不难理解&著名&的nil返回值问题以及反射、type switch、type assertion等原理。本文主要基于Go1.8.1的源码介绍接口的内部实现及其使用相关的问题。&/p&&p&&br&&/p&&h2&&b&1. 接口的实现&/b&&/h2&&p&(1) 下面是接口在runtime中的实现,注意其中包含了接口本身和实际数据类型的类型信息:&/p&&div class=&highlight&&&pre&&code class=&language-go&&&span&&/span&&span class=&c1&&// src/runtime/runtime2.go&/span&
&span class=&kd&&type&/span& &span class=&nx&&iface&/span& &span class=&kd&&struct&/span& &span class=&p&&{&/span&
&span class=&c1&&// 包含接口的静态类型信息、数据的动态类型信息、函数表&/span&
&span class=&nx&&tab&/span&
&span class=&o&&*&/span&&span class=&nx&&itab&/span&
&span class=&c1&&// 指向具体数据的内存地址比如slice、map等,或者在接口&/span&
&span class=&c1&&// 转换时直接存放小数据(一个指针的长度)&/span&
&span class=&nx&&data&/span& &span class=&nx&&unsafe&/span&&span class=&p&&.&/span&&span class=&nx&&Pointer&/span&
&span class=&p&&}&/span&
&span class=&kd&&type&/span& &span class=&nx&&itab&/span& &span class=&kd&&struct&/span& &span class=&p&&{&/span&
&span class=&c1&&// 接口的类型信息&/span&
&span class=&nx&&inter&/span&
&span class=&o&&*&/span&&span class=&nx&&interfacetype&/span&
&span class=&c1&&// 具体数据的类型信息&/span&
&span class=&nx&&_type&/span&
&span class=&o&&*&/span&&span class=&nx&&_type&/span&
&span class=&nx&&link&/span&
&span class=&o&&*&/span&&span class=&nx&&itab&/span&
&span class=&nx&&hash&/span&
&span class=&kt&&uint32&/span&
&span class=&nx&&bad&/span&
&span class=&kt&&bool&/span&
&span class=&nx&&inhash&/span& &span class=&kt&&bool&/span&
&span class=&nx&&unused&/span& &span class=&p&&[&/span&&span class=&mi&&2&/span&&span class=&p&&]&/span&&span class=&kt&&byte&/span&
&span class=&c1&&// 函数地址表,这里放置和接口方法对应的具体数据类型的方法地址&/span&
&span class=&c1&&// 实现接口调用方法的动态分派,一般在每次给接口赋值发生转换时&/span&
&span class=&c1&&// 会更新此表,或者直接拿缓存的itab&/span&
&span class=&nx&&fun&/span&
&span class=&p&&[&/span&&span class=&mi&&1&/span&&span class=&p&&]&/span&&span class=&kt&&uintptr&/span& &span class=&c1&&// variable sized&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&&p&(2) 另外,需要注意与接口相关的两点优化,会影响到反射等的实现:&/p&&ul&&li&空接口(interface{})的itab优化&br&&/li&&/ul&&p&&br&&/p&&p&当将某个类型赋值给空接口时,由于空接口没有方法,所以空接口eface的tab会直接指向数据的具体类型。在Go的reflect包中,reflect.TypeOf和reflect.ValueOf的参数都是空接口,因此所有参数都会先转换为空接口类型。这样反射就实现了对所有参数类型获取实际数据类型的统一。这在后面反射的基本实现中会分析到。&/p&&ul&&li&发生“接口转换”时data字段相关的优化&br&&/li&&/ul&&p&&br&&/p&&p&当被转换为接口的数据的类型长度不超过一个指针的长度时(比如pointer、map、func、chan、[1]int等类型),接口转换时会将数据直接拷贝存放到接口的data字段中(DirectIface),而不再额外分配内存并拷贝。另外,从go1.8+的源码来看除了DirectIface的优化以外,还对长度较小(不超过64字节,未初始化数据内存的array,空字符串等)的零值做了优化,也不会重新分配内存,而是直接指向一个包级全局数组变量zeroVal的首地址。注意这里的优化发生在接口转换时生成的临时接口上,而不是被赋值的接口左值上。&/p&&p&&br&&/p&&p&(3) 再者,在Go中只有值传递(包括接口类型),与具体的类型实现无关,但是某些类型具有引用的属性。典型的9种非基础类型中:&br&&/p&&ul&&li&array传递会拷贝整块数据内存,传递长度为len(arr) * Sizeof(elem)&br&&/li&&li&string、slice、interface传递的是其runtime的实现,所以长度是固定的,分别为16、24、16字节(amd64)&br&&/li&&li&map、func、chan、pointer传递的是指针,所以长度固定为8字节(amd64)&br&&/li&&li&struct传递的是所有字段的内存拷贝,所以长度是所有字段的长度和&br&&/li&&li&详细的测试可以参考[这段程序](&a href=&https://link.zhihu.com/?target=https%3A//github.com/feilengcui008/pieces/blob/master/go/basics/pass_by_value_main.go& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&pass_by_value_main.go&/a&)&/li&&/ul&&p&&br&&/p&&h2&&b&2. runtime中接口的转换操作&/b&&/h2&&p&接口相关的操作主要在于对其内部字段itab的操作,因为接口转换最重要的是类型信息。这里简单分析几个runtime中相关的函数。主要实现在`src/runtime/iface.go`中。值得注意的是,接口的类型转换在编译期会生成一个函数调用的语法树节点(OCALL),调用runtime提供的相应接口转换函数完成接口的类型设置,所以接口的转换是在运行时发生的,其具体类型的方法地址表也是在运行时填写的,这一点和C++的虚函数表不太一样。另外,由于在运行时转换会产生开销,所以对转换的itab做了缓存。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&type MyReader struct {
func (r MyReader) Read(b []byte) (n int, err error) {
// 接口的相关转换编译成对相关runtime函数的调用,比如convI2I/assertI2I等
var i io.Reader = MyReader{}
realReader := i.(MyReader)
var ei interface{} = interface{}(realReader)
&/code&&/pre&&/div&&p&下面以convI2I为例来说明,编译时生成OCALL语法树节点的过程。&br&&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&// src/cmd/compile/internal/gc/walk.go
func convFuncName(from, to *types.Type) string {
tkind := to.Tie()
switch from.Tie() {
// 将接口转换为另一接口,返回需要在runtime中调用的函数名
switch tkind {
return &convI2I&
// src/cmd/compile/internal/gc/walk.go
// 这里只给出节点操作类型为OCONVIFACE(即inerface转换)的处理逻辑
func walkexpr(n *Node, init *Nodes) *Node {
case OCONVIFACE:
n.Left = walkexpr(n.Left, init)
/* 这里省略了很多特殊的处理逻辑,比如空接口相关的优化 */
// 到这里开始进入一般的接口转换
// 查找需要调用的runtime的函数,在Runtimepkg中查找
fn := syslook(convFuncName(n.Left.Type, n.Type))
fn = substArgTypes(fn, n.Left.Type, n.Type)
dowidth(fn.Type)
// 生成函数调用节点
n = nod(OCALL, fn, nil)
n.List.Set(ll)
n = typecheck(n, Erv)
n = walkexpr(n, init)
&/code&&/pre&&/div&&p&一旦itab的函数表设置后,后面的接口的方法调用只需要一次间接调用的开销,不需要反复查找方法的地址。关于接口的实现,Russ Cox写过一篇很好的&a href=&https://link.zhihu.com/?target=http%3A//research.swtch.com/2009/12/go-data-structures-interfaces.html& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&文章&/a&。&/p&&p&&br&&/p&&p&下面分析runtime中接口相关的几个主要函数:&/p&&ul&&li&getitab&/li&&/ul&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&// 根据接口类型和实际数据类型生成itab
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
// 先从缓存中找
h := itabhash(inter, typ)
// look twice - once without lock, once with.
// common case will be no lock contention.
var m *itab
var locked int
for locked = 0; locked & 2; locked++ {
if locked != 0 {
lock(&ifaceLock)
for m = (*itab)(atomic.Loadp(unsafe.Pointer(&hash[h]))); m != m = m.link {
if m.inter == inter && m._type == typ {
if m.bad {
if !canfail {
// 检查并绑定方法地址表
additab(m, locked != 0, false)
if locked != 0 {
unlock(&ifaceLock)
// 缓存中没找到则分配itab的内存: itab结构本身内存 + 末尾存方法地址表的可变长度
m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
m.inter = inter
// 设置接口类型信息
m._type = typ
// 设置实际数据类型信息
additab(m, true, canfail) // 设置itab函数调用表
unlock(&ifaceLock)
if m.bad {
return nil
&/code&&/pre&&/div&&ul&&li&additab&/li&&/ul&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&// 检查具体类型是否实现了接口规定的方法,并使用具体类型的方法
// 地址填充方法表。
func additab(m *itab, locked, canfail bool) {
inter := m.inter
typ := m._type
x := typ.uncommon()
ni := len(inter.mhdr) // 接口方法数量
nt := int(x.mcount)
// 实际数据类型方法数量
xmhdr := (*[1 && 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
for k := 0; k & k++ {
// 对每个接口方法的地址
i := &inter.mhdr[k]
// 使用接口的类型信息获取实际类型, 函数名字,包名字
itype := inter.typ.typeOff(i.ityp)
name := inter.typ.nameOff(i.name)
iname := name.name()
ipkg := name.pkgPath()
if ipkg == && {
ipkg = inter.pkgpath.name()
// 对每个具体类型的方法
t := &xmhdr[j]
tname := typ.nameOff(t.name)
// 具体类型的方法类型和接口方法的类型相同,并且名字相同,则匹配成功
if typ.typeOff(t.mtyp) == itype && tname.name() == iname {
pkgPath := tname.pkgPath()
if pkgPath == && {
pkgPath = typ.nameOff(x.pkgpath).name()
if tname.isExported() || pkgPath == ipkg {
if m != nil {
// 具体类型的某个方法地址
ifn := typ.textOff(t.ifn)
// 填充itab的func表地址
*(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn
goto nextimethod
// didn't find method
// 不匹配panic
if !canfail {
if locked {
unlock(&ifaceLock)
panic(&TypeAssertionError{&&, typ.string(), inter.typ.string(), iname})
// 或者设置失败标识
m.bad = true
nextimethod:
if !locked {
throw(&invalid itab locking&)
h := itabhash(inter, typ)
m.link = hash[h]
m.inhash = true
// 存到itab的hash表缓存
atomicstorep(unsafe.Pointer(&hash[h]), unsafe.Pointer(m))
&/code&&/pre&&/div&&p&&br&&/p&&ul&&li&convI2I&/li&&/ul&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&// 将已有的接口,转换为新的接口类型,失败panic
// var rc io.ReadCloser
// var r io.Reader
// rc = io.ReadCloser(r)
func convI2I(inter *interfacetype, i iface) (r iface) {
tab := i.tab
if tab == nil {
// 接口类型相同直接赋值即可
if tab.inter == inter {
r.tab = tab
r.data = i.data
// 否则重新生成itab
r.tab = getitab(inter, tab._type, false)
// 注意这里没有分配内存拷贝数据
r.data = i.data
&/code&&/pre&&/div&&ul&&li&convT2I&br&&/li&&/ul&&p&&br&&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&// 使用itab并拷贝数据,得到iface
func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
t := tab._type
if raceenabled {
raceReadObjectPC(t, elem, getcallerpc(unsafe.Pointer(&tab)), funcPC(convT2I))
if msanenabled {
msanread(elem, t.size)
// 注意这里发生了内存分配和数据拷贝
x := mallocgc(t.size, t, true)
// memmove内部的拷贝对大块内存做了优化
typedmemmove(t, x, elem)
i.tab = tab
i.data = x
&/code&&/pre&&/div&&p&从上面convX2I我们可以看到,在接口类型之间转换时,并没有分配内存和拷贝数据,但是将非接口类型转换为接口类型时,却发生了内存分配和数据拷贝。这里的原因是Go接口的数据不能被改变,所以接口之间的转换可以使用同一块内存,但是其他情况为了避免外部改变导致接口内数据改变,所以会进行内存分配和数据拷贝。另外,这也是反射非指针变量时无法直接改变变量数据的原因,因为反射会先将变量转换为空接口类型。可以参考&a href=&https://link.zhihu.com/?target=https%3A//groups.google.com/forum/%23%21topic/golang-nuts/e5ddPzR7eKI& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&go-nuts&/a&。这里我们用一个简单的程序测试一下。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&package main
import &fmt&
type Data struct {
func main() {
d := Data{10}
fmt.Printf(&address of d: %p\n&, &d)
// assign not interface type variable to interface variable
// d will be copied
var i1 interface{} = d
// assign interface type variable to interface variable
// the data of i1 will directly assigned to i2.data and will not be copied
var i2 interface{} = i1
fmt.Println(d)
fmt.Println(i1)
fmt.Println(i2)
// 关掉优化和inline
go build -gcflags &-N -l& interface.go
// 可以看到接口变量i1和i2的数据地址是相同的,但是d和i1的数据地址不相同
(gdb) info locals
i2 = {_type = 0x492e00, data = 0xc}
i1 = {_type = 0x492e00, data = 0xc}
&/code&&/pre&&/div&&p&&br&&/p&&h2&&b&3. type assertion与type switch&/b&&/h2&&p&理解了接口的实现,不难猜测type assertion和type switch的实现逻辑,我们只需要取出接口的动态类型(数据类型)与目标类型做比较即可,而目标类型的信息在编译期是可以确定下来的。可以参考&a href=&https://link.zhihu.com/?target=https%3A//golang.org/doc/effective_go.html%23interface_conversions& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Effective Go&/a&中的简单例子。&/p&&p&&br&&/p&&h2&&b&4. nil接口的问题&/b&&/h2&&p&具体的代码可参考&a href=&https://link.zhihu.com/?target=https%3A//github.com/feilengcui008/pieces/blob/master/go/basics/traps_main.go%23L94& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&nil接口返回值测试&/a&。理解了接口的底层实现,这个问题其实也比较好理解了。需要说明的是nil在Go中既指空值,也指空类型。这里的空值并非零值,空值是指未初始化,比如slice没有分配底层的内存。只有chan、interface、func、slice、map、pointer可直接与nil比较和用nil赋值。对于非接口类型来说,对其赋值nil的语义是将其数据变为未初始化的状态,而给接口类型来说,还会将接口的类型信息字段itab置nil。所以:&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&type MyReader interface {
var r MyReader
// (nil, nil)
var n *int = nil
var r1 MyReader = n // (*int, nil)
var r2 MyReader // (nil, nil)
var inter interface{} = r2 // (nil, nil)
&/code&&/pre&&/div&&p&&br&&/p&&h2&&b&5. 接口与反射&/b&&/h2&&p&反射实现的一个基本前提是编译期为运行时提供足够的类型信息,一般来说都会使用一个基本类型(比如Go中的interface、Java中的Object)来存放具体类型的信息,以便在运行时使用。C++到目前为止也没有比较成熟的反射库,大部分原因就是没有比较好的方法提供运行时所需的类型信息,typeid等运行时信息远远不够。Go的反射的实现就是基于interface的。这里简单分析两个常用方法`reflect.TypeOf, reflect.ValueOf`的实现。&/p&&div class=&highlight&&&pre&&code class=&language-text&&&span&&/span&// src/reflect/value.go
// 注意: 从前面的分析可知当转换为空接口的时候,itab指针会直接
// 指向数据的实际类型,所以反射的入口函数参数类型是interface{},
// 转换后,emptyInterface的rtype字段会直接指向数据类型,所以
// 整个反射才能直接得到数据类型,不然itab指向内存的前面部分包含
// 的是接口的静态类型信息
type emptyInterface struct {
word unsafe.Pointer
// src/reflect/type.go
func Ty

我要回帖

更多关于 最近网络上很火的歌曲 的文章

 

随机推荐