• 关于

    延迟过程调用怎么用

    的搜索结果

回答

分布式事务的解决方案有如下几种: 全局消息基于可靠消息服务的分布式事务TCC最大努力通知方案1:全局事务(DTP模型)全局事务基于DTP模型实现。DTP是由X/Open组织提出的一种分布式事务模型——X/Open Distributed Transaction Processing Reference Model。它规定了要实现分布式事务,需要三种角色: AP:Application 应用系统 它就是我们开发的业务系统,在我们开发的过程中,可以使用资源管理器提供的事务接口来实现分布式事务。 TM:Transaction Manager 事务管理器 分布式事务的实现由事务管理器来完成,它会提供分布式事务的操作接口供我们的业务系统调用。这些接口称为TX接口。事务管理器还管理着所有的资源管理器,通过它们提供的XA接口来同一调度这些资源管理器,以实现分布式事务。DTP只是一套实现分布式事务的规范,并没有定义具体如何实现分布式事务,TM可以采用2PC、3PC、Paxos等协议实现分布式事务。RM:Resource Manager 资源管理器 能够提供数据服务的对象都可以是资源管理器,比如:数据库、消息中间件、缓存等。大部分场景下,数据库即为分布式事务中的资源管理器。资源管理器能够提供单数据库的事务能力,它们通过XA接口,将本数据库的提交、回滚等能力提供给事务管理器调用,以帮助事务管理器实现分布式的事务管理。XA是DTP模型定义的接口,用于向事务管理器提供该资源管理器(该数据库)的提交、回滚等能力。DTP只是一套实现分布式事务的规范,RM具体的实现是由数据库厂商来完成的。有没有基于DTP模型的分布式事务中间件?DTP模型有啥优缺点?方案2:基于可靠消息服务的分布式事务这种实现分布式事务的方式需要通过消息中间件来实现。假设有A和B两个系统,分别可以处理任务A和任务B。此时系统A中存在一个业务流程,需要将任务A和任务B在同一个事务中处理。下面来介绍基于消息中间件来实现这种分布式事务。 title 在系统A处理任务A前,首先向消息中间件发送一条消息消息中间件收到后将该条消息持久化,但并不投递。此时下游系统B仍然不知道该条消息的存在。消息中间件持久化成功后,便向系统A返回一个确认应答;系统A收到确认应答后,则可以开始处理任务A;任务A处理完成后,向消息中间件发送Commit请求。该请求发送完成后,对系统A而言,该事务的处理过程就结束了,此时它可以处理别的任务了。 但commit消息可能会在传输途中丢失,从而消息中间件并不会向系统B投递这条消息,从而系统就会出现不一致性。这个问题由消息中间件的事务回查机制完成,下文会介绍。消息中间件收到Commit指令后,便向系统B投递该消息,从而触发任务B的执行;当任务B执行完成后,系统B向消息中间件返回一个确认应答,告诉消息中间件该消息已经成功消费,此时,这个分布式事务完成。上述过程可以得出如下几个结论: 消息中间件扮演者分布式事务协调者的角色。 系统A完成任务A后,到任务B执行完成之间,会存在一定的时间差。在这个时间差内,整个系统处于数据不一致的状态,但这短暂的不一致性是可以接受的,因为经过短暂的时间后,系统又可以保持数据一致性,满足BASE理论。 上述过程中,如果任务A处理失败,那么需要进入回滚流程,如下图所示: title 若系统A在处理任务A时失败,那么就会向消息中间件发送Rollback请求。和发送Commit请求一样,系统A发完之后便可以认为回滚已经完成,它便可以去做其他的事情。消息中间件收到回滚请求后,直接将该消息丢弃,而不投递给系统B,从而不会触发系统B的任务B。此时系统又处于一致性状态,因为任务A和任务B都没有执行。 上面所介绍的Commit和Rollback都属于理想情况,但在实际系统中,Commit和Rollback指令都有可能在传输途中丢失。那么当出现这种情况的时候,消息中间件是如何保证数据一致性呢?——答案就是超时询问机制。 title 系统A除了实现正常的业务流程外,还需提供一个事务询问的接口,供消息中间件调用。当消息中间件收到一条事务型消息后便开始计时,如果到了超时时间也没收到系统A发来的Commit或Rollback指令的话,就会主动调用系统A提供的事务询问接口询问该系统目前的状态。该接口会返回三种结果: 提交 若获得的状态是“提交”,则将该消息投递给系统B。回滚 若获得的状态是“回滚”,则直接将条消息丢弃。处理中 若获得的状态是“处理中”,则继续等待。消息中间件的超时询问机制能够防止上游系统因在传输过程中丢失Commit/Rollback指令而导致的系统不一致情况,而且能降低上游系统的阻塞时间,上游系统只要发出Commit/Rollback指令后便可以处理其他任务,无需等待确认应答。而Commit/Rollback指令丢失的情况通过超时询问机制来弥补,这样大大降低上游系统的阻塞时间,提升系统的并发度。 下面来说一说消息投递过程的可靠性保证。 当上游系统执行完任务并向消息中间件提交了Commit指令后,便可以处理其他任务了,此时它可以认为事务已经完成,接下来消息中间件一定会保证消息被下游系统成功消费掉!那么这是怎么做到的呢?这由消息中间件的投递流程来保证。 消息中间件向下游系统投递完消息后便进入阻塞等待状态,下游系统便立即进行任务的处理,任务处理完成后便向消息中间件返回应答。消息中间件收到确认应答后便认为该事务处理完毕! 如果消息在投递过程中丢失,或消息的确认应答在返回途中丢失,那么消息中间件在等待确认应答超时之后就会重新投递,直到下游消费者返回消费成功响应为止。当然,一般消息中间件可以设置消息重试的次数和时间间隔,比如:当第一次投递失败后,每隔五分钟重试一次,一共重试3次。如果重试3次之后仍然投递失败,那么这条消息就需要人工干预。 title title 有的同学可能要问:消息投递失败后为什么不回滚消息,而是不断尝试重新投递? 这就涉及到整套分布式事务系统的实现成本问题。 我们知道,当系统A将向消息中间件发送Commit指令后,它便去做别的事情了。如果此时消息投递失败,需要回滚的话,就需要让系统A事先提供回滚接口,这无疑增加了额外的开发成本,业务系统的复杂度也将提高。对于一个业务系统的设计目标是,在保证性能的前提下,最大限度地降低系统复杂度,从而能够降低系统的运维成本。 不知大家是否发现,上游系统A向消息中间件提交Commit/Rollback消息采用的是异步方式,也就是当上游系统提交完消息后便可以去做别的事情,接下来提交、回滚就完全交给消息中间件来完成,并且完全信任消息中间件,认为它一定能正确地完成事务的提交或回滚。然而,消息中间件向下游系统投递消息的过程是同步的。也就是消息中间件将消息投递给下游系统后,它会阻塞等待,等下游系统成功处理完任务返回确认应答后才取消阻塞等待。为什么这两者在设计上是不一致的呢? 首先,上游系统和消息中间件之间采用异步通信是为了提高系统并发度。业务系统直接和用户打交道,用户体验尤为重要,因此这种异步通信方式能够极大程度地降低用户等待时间。此外,异步通信相对于同步通信而言,没有了长时间的阻塞等待,因此系统的并发性也大大增加。但异步通信可能会引起Commit/Rollback指令丢失的问题,这就由消息中间件的超时询问机制来弥补。 那么,消息中间件和下游系统之间为什么要采用同步通信呢? 异步能提升系统性能,但随之会增加系统复杂度;而同步虽然降低系统并发度,但实现成本较低。因此,在对并发度要求不是很高的情况下,或者服务器资源较为充裕的情况下,我们可以选择同步来降低系统的复杂度。 我们知道,消息中间件是一个独立于业务系统的第三方中间件,它不和任何业务系统产生直接的耦合,它也不和用户产生直接的关联,它一般部署在独立的服务器集群上,具有良好的可扩展性,所以不必太过于担心它的性能,如果处理速度无法满足我们的要求,可以增加机器来解决。而且,即使消息中间件处理速度有一定的延迟那也是可以接受的,因为前面所介绍的BASE理论就告诉我们了,我们追求的是最终一致性,而非实时一致性,因此消息中间件产生的时延导致事务短暂的不一致是可以接受的。 方案3:最大努力通知(定期校对)最大努力通知也被称为定期校对,其实在方案二中已经包含,这里再单独介绍,主要是为了知识体系的完整性。这种方案也需要消息中间件的参与,其过程如下: title 上游系统在完成任务后,向消息中间件同步地发送一条消息,确保消息中间件成功持久化这条消息,然后上游系统可以去做别的事情了;消息中间件收到消息后负责将该消息同步投递给相应的下游系统,并触发下游系统的任务执行;当下游系统处理成功后,向消息中间件反馈确认应答,消息中间件便可以将该条消息删除,从而该事务完成。上面是一个理想化的过程,但在实际场景中,往往会出现如下几种意外情况: 消息中间件向下游系统投递消息失败上游系统向消息中间件发送消息失败对于第一种情况,消息中间件具有重试机制,我们可以在消息中间件中设置消息的重试次数和重试时间间隔,对于网络不稳定导致的消息投递失败的情况,往往重试几次后消息便可以成功投递,如果超过了重试的上限仍然投递失败,那么消息中间件不再投递该消息,而是记录在失败消息表中,消息中间件需要提供失败消息的查询接口,下游系统会定期查询失败消息,并将其消费,这就是所谓的“定期校对”。 如果重复投递和定期校对都不能解决问题,往往是因为下游系统出现了严重的错误,此时就需要人工干预。 对于第二种情况,需要在上游系统中建立消息重发机制。可以在上游系统建立一张本地消息表,并将 任务处理过程 和 向本地消息表中插入消息 这两个步骤放在一个本地事务中完成。如果向本地消息表插入消息失败,那么就会触发回滚,之前的任务处理结果就会被取消。如果这量步都执行成功,那么该本地事务就完成了。接下来会有一个专门的消息发送者不断地发送本地消息表中的消息,如果发送失败它会返回重试。当然,也要给消息发送者设置重试的上限,一般而言,达到重试上限仍然发送失败,那就意味着消息中间件出现严重的问题,此时也只有人工干预才能解决问题。 对于不支持事务型消息的消息中间件,如果要实现分布式事务的话,就可以采用这种方式。它能够通过重试机制+定期校对实现分布式事务,但相比于第二种方案,它达到数据一致性的周期较长,而且还需要在上游系统中实现消息重试发布机制,以确保消息成功发布给消息中间件,这无疑增加了业务系统的开发成本,使得业务系统不够纯粹,并且这些额外的业务逻辑无疑会占用业务系统的硬件资源,从而影响性能。 因此,尽量选择支持事务型消息的消息中间件来实现分布式事务,如RocketMQ。 方案4:TCC(两阶段型、补偿型)TCC即为Try Confirm Cancel,它属于补偿型分布式事务。顾名思义,TCC实现分布式事务一共有三个步骤: Try:尝试待执行的业务 这个过程并未执行业务,只是完成所有业务的一致性检查,并预留好执行所需的全部资源Confirm:执行业务 这个过程真正开始执行业务,由于Try阶段已经完成了一致性检查,因此本过程直接执行,而不做任何检查。并且在执行的过程中,会使用到Try阶段预留的业务资源。Cancel:取消执行的业务 若业务执行失败,则进入Cancel阶段,它会释放所有占用的业务资源,并回滚Confirm阶段执行的操作。下面以一个转账的例子来解释下TCC实现分布式事务的过程。 假设用户A用他的账户余额给用户B发一个100元的红包,并且余额系统和红包系统是两个独立的系统。 Try 创建一条转账流水,并将流水的状态设为交易中将用户A的账户中扣除100元(预留业务资源)Try成功之后,便进入Confirm阶段Try过程发生任何异常,均进入Cancel阶段Confirm 向B用户的红包账户中增加100元将流水的状态设为交易已完成Confirm过程发生任何异常,均进入Cancel阶段Confirm过程执行成功,则该事务结束Cancel 将用户A的账户增加100元将流水的状态设为交易失败在传统事务机制中,业务逻辑的执行和事务的处理,是在不同的阶段由不同的部件来完成的:业务逻辑部分访问资源实现数据存储,其处理是由业务系统负责;事务处理部分通过协调资源管理器以实现事务管理,其处理由事务管理器来负责。二者没有太多交互的地方,所以,传统事务管理器的事务处理逻辑,仅需要着眼于事务完成(commit/rollback)阶段,而不必关注业务执行阶段。 TCC全局事务必须基于RM本地事务来实现全局事务TCC服务是由Try/Confirm/Cancel业务构成的, 其Try/Confirm/Cancel业务在执行时,会访问资源管理器(Resource Manager,下文简称RM)来存取数据。这些存取操作,必须要参与RM本地事务,以使其更改的数据要么都commit,要么都rollback。 这一点不难理解,考虑一下如下场景: title 假设图中的服务B没有基于RM本地事务(以RDBS为例,可通过设置auto-commit为true来模拟),那么一旦[B:Try]操作中途执行失败,TCC事务框架后续决定回滚全局事务时,该[B:Cancel]则需要判断[B:Try]中哪些操作已经写到DB、哪些操作还没有写到DB:假设[B:Try]业务有5个写库操作,[B:Cancel]业务则需要逐个判断这5个操作是否生效,并将生效的操作执行反向操作。 不幸的是,由于[B:Cancel]业务也有n(0<=n<=5)个反向的写库操作,此时一旦[B:Cancel]也中途出错,则后续的[B:Cancel]执行任务更加繁重。因为,相比第一次[B:Cancel]操作,后续的[B:Cancel]操作还需要判断先前的[B:Cancel]操作的n(0<=n<=5)个写库中哪几个已经执行、哪几个还没有执行,这就涉及到了幂等性问题。而对幂等性的保障,又很可能还需要涉及额外的写库操作,该写库操作又会因为没有RM本地事务的支持而存在类似问题。。。可想而知,如果不基于RM本地事务,TCC事务框架是无法有效的管理TCC全局事务的。 反之,基于RM本地事务的TCC事务,这种情况则会很容易处理:[B:Try]操作中途执行失败,TCC事务框架将其参与RM本地事务直接rollback即可。后续TCC事务框架决定回滚全局事务时,在知道“[B:Try]操作涉及的RM本地事务已经rollback”的情况下,根本无需执行[B:Cancel]操作。 换句话说,基于RM本地事务实现TCC事务框架时,一个TCC型服务的cancel业务要么执行,要么不执行,不需要考虑部分执行的情况。 TCC事务框架应该提供Confirm/Cancel服务的幂等性保障一般认为,服务的幂等性,是指针对同一个服务的多次(n>1)请求和对它的单次(n=1)请求,二者具有相同的副作用。 在TCC事务模型中,Confirm/Cancel业务可能会被重复调用,其原因很多。比如,全局事务在提交/回滚时会调用各TCC服务的Confirm/Cancel业务逻辑。执行这些Confirm/Cancel业务时,可能会出现如网络中断的故障而使得全局事务不能完成。因此,故障恢复机制后续仍然会重新提交/回滚这些未完成的全局事务,这样就会再次调用参与该全局事务的各TCC服务的Confirm/Cancel业务逻辑。 既然Confirm/Cancel业务可能会被多次调用,就需要保障其幂等性。 那么,应该由TCC事务框架来提供幂等性保障?还是应该由业务系统自行来保障幂等性呢? 个人认为,应该是由TCC事务框架来提供幂等性保障。如果仅仅只是极个别服务存在这个问题的话,那么由业务系统来负责也是可以的;然而,这是一类公共问题,毫无疑问,所有TCC服务的Confirm/Cancel业务存在幂等性问题。TCC服务的公共问题应该由TCC事务框架来解决;而且,考虑一下由业务系统来负责幂等性需要考虑的问题,就会发现,这无疑增大了业务系统的复杂度。

1210119897362579 2019-12-02 00:14:25 0 浏览量 回答数 0

问题

MaxCompute百问集锦(持续更新20171011)

隐林 2019-12-01 20:19:23 38430 浏览量 回答数 18

问题

程序员报错QA大分享(1)

问问小秘 2020-06-18 15:46:14 8 浏览量 回答数 1

阿里云试用中心,为您提供0门槛上云实践机会!

0元试用32+款产品,最高免费12个月!拨打95187-1,咨询专业上云建议!

问题

支付宝的性能测试

云效平台 2019-12-01 21:47:13 5472 浏览量 回答数 1

问题

代码跑得慢甩锅Python,怎样给它提速30%?

茶什i 2020-01-13 18:42:24 177 浏览量 回答数 2

回答

Go 的优势在于能够将简单的和经过验证的想法结合起来,同时避免了其他语言中出现的许多问题。本文概述了 Go 背后的一些设计原则和工程智慧,作者认为,Go 语言具备的所有这些优点,将共同推动其成为接替 Java 并主导下一代大型软件开发平台的最有力的编程语言候选。很多优秀的编程语言只是在个别领域比较强大,如果将所有因素都纳入考虑,没有其他语言能够像 Go 语言一样“全面开花”,在大型软件工程方面,尤为如此。 基于现实经验 Go 是由经验丰富的软件行业老手一手创建的,长期以来,他们对现有语言的各种缺点有过切身体会的痛苦经历。几十年前,Rob Pike 和 Ken Thompson 在 Unix、C 和 Unicode 的发明中起到了重要作用。Robert Griensemer 在为 JavaScript 和 Java 开发 V8 和 HotSpot 虚拟机之后,在编译器和垃圾收集方面拥有数十年的经验。有太多次,他们不得不等待 Google 规模的 C++/Java 代码库进行编译。于是,他们开始着手创建新的编程语言,将他们半个世纪以来的编写代码所学到的一切经验包含进去。 专注于大型工程 小型工程项目几乎可以用任何编程语言来成功构建。当成千上万的开发人员在数十年的持续时间压力下,在包含数千万行代码的大型代码库上进行协作时,就会发生真正令人痛苦的问题。这样会导致一些问题,如下: 较长的编译时间导致中断开发。代码库由几个人 / 团队 / 部门 / 公司所拥有,混合了不同的编程风格。公司雇佣了数千名工程师、架构师、测试人员、运营专家、审计员、实习生等,他们需要了解代码库,但也具备广泛的编码经验。依赖于许多外部库或运行时,其中一些不再以原始形式存在。在代码库的生命周期中,每行代码平均被重写 10 次,被弄得千疮百痍,而且还会发生技术偏差。文档不完整。 Go 注重减轻这些大型工程的难题,有时会以使小型工程变得更麻烦为代价,例如,代码中到处都需要几行额外的代码行。 注重可维护性 Go 强调尽可能多地将工作转给自动化的代码维护工具中。Go 工具链提供了最常用的功能,如格式化代码和导入、查找符号的定义和用法、简单的重构以及代码异味的识别。由于标准化的代码格式和单一的惯用方式,机器生成的代码更改看起来非常接近 Go 中人为生成的更改并使用类似的模式,从而允许人机之间更加无缝地协作。 保持简单明了 初级程序员为简单的问题创建简单的解决方案。高级程序员为复杂的问题创建复杂的解决方案。伟大的程序员找到复杂问题的简单解决方案。 ——Charles Connell 让很多人惊讶的一点是,Go 居然不包含他们喜欢的其他语言的概念。Go 确实是一种非常小巧而简单的语言,只包含正交和经过验证的概念的最小选择。这鼓励开发人员用最少的认知开销来编写尽可能简单的代码,以便许多其他人可以理解并使用它。 使事情清晰明了 良好的代码总是显而易见的,避免了那些小聪明、难以理解的语言特性、诡异的控制流和兜圈子。 许多语言都致力提高编写代码的效率。然而,在其生命周期中,人们阅读代码的时间却远远超过最初编写代码所需的时间(100 倍)。例如,审查、理解、调试、更改、重构或重用代码。在查看代码时,往往只能看到并理解其中的一小部分,通常不会有完整的代码库概述。为了解释这一点,Go 将所有内容都明确出来。 错误处理就是一个例子。让异常在各个点中断代码并在调用链上冒泡会更容易。Go 需要手动处理和返回每个错误。这使得它可以准确地显示代码可以被中断的位置以及如何处理或包装错误。总的来说,这使得错误处理编写起来更加繁琐,但是也更容易理解。 简单易学 Go 是如此的小巧而简单,以至于人们可以在短短几天内就能研究通整个语言及其基本概念。根据我们的经验,培训用不了一个星期(相比于掌握其他语言需要几个月),初学者就能够理解 Go 专家编写的代码,并为之做出贡献。为了方便吸引更多的用户,Go 网站提供了所有必要的教程和深入研究的文章。这些教程在浏览器中运行,允许人们在将 Go 安装到本地计算机上之前就能够学习和使用 Go。 解决之道 Go 强调的是团队之间的合作,而不是个人的自我表达。 在 Go(和 Python)中,所有的语言特性都是相互正交和互补的,通常有一种方法可以做一些事情。如果你想让 10 个 Python 或 Go 程序员来解决同一个问题,你将会得到 10 个相对类似的解决方案。不同的程序员在彼此的代码库中感觉更自在。在查看其他人的代码时,国骂会更少,而且人们的工作可以更好地融合在一起,从而形成了一致的整体,人人都为之感到自豪,并乐于工作。这还避免了大型工程的问题,如: 开发人员认为良好的工作代码很“混乱”,并要求在开始工作之前进行重写,因为他们的思维方式与原作者不同。 不同的团队成员使用不同的语言子集来编写相同代码库的部分内容。 ![image.png](https://ucc.alicdn.com/pic/developer-ecology/e64418f1455d46aaacfdd03fa949f16d.png) 简单、内置的并发性 Go 专为现代多核硬件设计。 目前使用的大多数编程语言(Java、JavaScript、Python、Ruby、C、C++)都是 20 世纪 80 年代到 21 世纪初设计的,当时大多数 CPU 只有一个计算内核。这就是为什么它们本质上是单线程的,并将并行化视为边缘情况的马后炮。通过现成和同步点之类的附加组件来实现,而这些附加组件既麻烦又难以正确使用。第三方库虽然提供了更简单的并发形式,如 Actor 模型,但是总有多个可用选项,结果导致了语言生态系统的碎片化。今天的硬件拥有越来越多的计算内核,软件必须并行化才能高效运行。Go 是在多核处理器时代编写的,并且在语言中内置了简单、高级的 CSP 风格并发性。 面向计算的语言原语 就深层而言,计算机系统接收数据,对其进行处理(通常要经过几个步骤),然后输出结果数据。例如,Web 服务器从客户端接收 HTTP 请求,并将其转换为一系列数据库或后端调用。一旦这些调用返回,它就将接收到的数据转换成 HTML 或 JSON 并将其输出给调用者。Go 的内置语言原语直接支持这种范例: 结构表示数据 读和写代表流式 IO 函数过程数据 goroutines 提供(几乎无限的)并发性 在并行处理步骤之间传输管道数据 因为所有的计算原语都是由语言以直接形式提供的,因此 Go 源代码更直接地表达了服务器执行的操作。 OO — 好的部分 更改基类中的某些内容的副作用 面向对象非常有用。过去几十年来,面向对象的使用富有成效,并让我们了解了它的哪些部分比其他部分能够更好地扩展。Go 在面向对象方面采用了一种全新的方法,并记住了这些知识。它保留了好的部分,如封装、消息传递等。Go 还避免了继承,因为它现在被认为是有害的,并为组合提供了一流的支持。 现代标准库 目前使用的许多编程语言(Java、JavaScript、Python、Ruby)都是在互联网成为当今无处不在的计算平台之前设计的。因此,这些语言的标准库只提供了相对通用的网络支持,而这些网络并没有针对现代互联网进行优化。Go 是十年前创建的,当时互联网已全面发展。Go 的标准库允许在没有第三方库的情况下创建更复杂的网络服务。这就避免了第三方库的常见问题: 碎片化:总是有多个选项实现相同的功能。 膨胀:库常常实现的不仅仅是它们的用途。 依赖地狱:库通常依赖于特定版本的其他库。 未知质量:第三方代码的质量和安全性可能存在问题。 未知支持:第三方库的开发可能随时停止支持。 意外更改:第三方库通常不像标准库那样严格地进行版本控制。 关于这方面更多的信息请参考 Russ Cox 提供的资料 标准化格式 Gofmt 的风格没有人会去喜欢,但人人都会喜欢 gofmt。 ——Rob Pike Gofmt 是一种以标准化方式来格式化 Go 代码的程序。它不是最漂亮的格式化方式,但却是最简单、最不令人生厌的格式化方式。标准化的源代码格式具有惊人的积极影响: 集中讨论重要主题: 它消除了围绕制表符和空格、缩进深度、行长、空行、花括号的位置等一系列争论。 开发人员在彼此的代码库中感觉很自在, 因为其他代码看起来很像他们编写的代码。每个人都喜欢自由地按照自己喜欢的方式进行格式化代码,但如果其他人按照自己喜欢的方式格式化了代码,这么做很招人烦。 自动代码更改并不会打乱手写代码的格式,例如引入了意外的空白更改。 许多其他语言社区现在正在开发类似 gofmt 的东西。当作为第三方解决方案构建时,通常会有几个相互竞争的格式标准。例如,JavaScript 提供了 Prettier 和 StandardJS。这两者都可以用,也可以只使用其中的一个。但许多 JS 项目并没有采用它们,因为这是一个额外的决策。Go 的格式化程序内置于该语言的标准工具链中,因此只有一个标准,每个人都在使用它。 快速编译 ![image.png](https://ucc.alicdn.com/pic/developer-ecology/8a76f3f07f484266af42781d9e7b8692.png) 对于大型代码库来说,它们长时间的编译是促使 Go 诞生的原因。Google 主要使用的是 C++ 和 Java,与 Haskell、Scala 或 Rust 等更复杂的语言相比,它们的编译速度相对较快。尽管如此,当编译大型代码库时,即使是少量的缓慢也会加剧编译的延迟,从而激怒开发人员,并干扰流程。Go 的设计初衷是为了提高编译效率,因此它的编译器速度非常快,几乎没有编译延迟的现象。这给 Go 开发人员提供了与脚本类语言类似的即时反馈,还有静态类型检查的额外好处。 交叉编译 由于语言运行时非常简单,因此它被移植到许多平台,如 macOS、Linux、Windows、BSD、ARM 等。Go 可以开箱即用地为所有这些平台编译二进制文件。这使得从一台机器进行部署变得很容易。 快速执行 Go 的运行速度接近于 C。与 JITed 语言(Java、JavaScript、Python 等)不同,Go 二进制文件不需要启动或预热的时间,因为它们是作为编译和完全优化的本地代码的形式发布的。Go 的垃圾收集器仅引入微秒量级的可忽略的停顿。除了快速的单核性能外,Go 还可以轻松利用所有的 CPU 内核。 内存占用小 像 JVM、Python 或 Node 这样的运行时不仅仅在运行时加载程序代码,每次运行程序时,它们还会加载大型且高度复杂的基础架构,以进行编译和优化程序。如此一来,它们的启动时间就变慢了,并且还占用了大量内存(数百兆字节)。而 Go 进程的开销更小,因为它们已经完全编译和优化,只需运行即可。Go 还以非常节省内存的方式来存储数据。在内存有限且昂贵的云环境中,以及在开发过程中,这一点非常重要。我们希望在一台机器上能够快速启动整个堆栈,同时将内存留给其他软件。 部署规模小 Go 的二进制文件大小非常简洁。Go 应用程序的 Docker 镜像通常比用 Java 或 Node 编写的等效镜像要小 10 倍,这是因为它无需包含编译器、JIT,以及更少的运行时基础架构的原因。这些特点,在部署大型应用程序时很重要。想象一下,如果要将一个简单的应用程序部署到 100 个生产服务器上会怎么样?如果使用 Node/JVM 时,我们的 Docker 注册表就必须提供 100 个 docker 镜像,每个镜像 200MB,那么一共就需要 20GB。要完成这些部署就需要一些时间。想象一下,如果我们想每天部署 100 次的话,如果使用 Go 服务,那么 Docker 注册表只需提供 10 个 docker 镜像,每个镜像只有 20MB,共只需 2GB 即可。大型 Go 应用程序可以更快、更频繁地部署,从而使得重要更新能够更快地部署到生产环境中。 独立部署 Go 应用程序部署为一个包含所有依赖项的单个可执行文件,并无需安装特定版本的 JVM、Node 或 Python 运行时;也不必将库下载到生产服务器上,更无须对运行 Go 二进制文件的机器进行任何更改。甚至也不需要讲 Go 二进制文件包装到 Docker 来共享他们。你需要做的是,只是将 Go 二进制文件放到服务器上,它就会在那里运行,而不用关心服务器运行的是什么。前面所提到的那些,唯一的例外是使用net和os/user包时针对对glibc的动态链接。 供应依赖关系 Go 有意识避免使用第三方库的中央存储库。Go 应用程序直接链接到相应的 Git 存储库,并将所有相关代码下载(供应)到自己的代码库中。这样做有很多好处: 在使用第三方代码之前,我们可以对其进行审查、分析和测试。该代码就和我们自己的代码一样,是我们应用程序的一部分,应该遵循相同的质量、安全性和可靠性标准。 无需永久访问存储依赖项的各个位置。从任何地方(包括私有 Git repos)获取第三方库,你就能永久拥有它们。 经过验收后,编译代码库无需进一步下载依赖项。 若互联网某处的代码存储库突然提供不同的代码,这也并不足为奇。 即使软件包存储库速度变慢,或托管包不复存在,部署也不会因此中断。 兼容性保证 Go 团队承诺现有的程序将会继续适用于新一代语言。这使得将大型项目升级到最新版本的编译器会非常容易,并且可从它们带来的许多性能和安全性改进中获益。同时,由于 Go 二进制文件包含了它们需要的所有依赖项,因此可以在同一服务器上并行运行使用不同版本的 Go 编译器编译的二进制文件,而无需进行复杂的多个版本的运行时设置或虚拟化。 文档 在大型工程中,文档对于使软件可访问性和可维护性非常重要。与其他特性类似,Go 中的文档简单实用: 由于它是嵌入到源代码中的,因此两者可以同时维护。 它不需要特殊的语法,文档只是普通的源代码注释。 可运行单元测试通常是最好的文档形式。因此 Go 要求将它们嵌入到文档中。 所有的文档实用程序都内置在工具链中,因此每个人都使用它们。 Go linter 需要导出元素的文档,以防止“文档债务”的积累。 商业支持的开源 当商业实体在开放式环境下开发时,那么一些最流行的、经过彻底设计的软件就会出现。这种设置结合了商业软件开发的优势——一致性和精细化,使系统更为健壮、可靠、高效,并具有开放式开发的优势,如来自许多行业的广泛支持,多个大型实体和许多用户的支持,以及即使商业支持停止的长期支持。Go 就是这样发展起来的。 缺点 当然,Go 也并非完美无缺,每种技术选择都是有利有弊。在决定选择 Go 之前,有几个方面需要进行考虑考虑。 未成熟 虽然 Go 的标准库在支持许多新概念(如 HTTP 2 Server push 等)方面处于行业领先地位,但与 JVM 生态系统中的第三方库相比,用于外部 API 的第三方 Go 库可能不那么成熟。 即将到来的改进 由于清楚几乎不可能改变现有的语言元素,Go 团队非常谨慎,只在新特性完全开发出来后才添加新特性。在经历了 10 年的有意稳定阶段之后,Go 团队正在谋划对语言进行一系列更大的改进,作为 Go 2.0 之旅的一部分。 无硬实时 虽然 Go 的垃圾收集器只引入了非常短暂的停顿,但支持硬实时需要没有垃圾收集的技术,例如 Rust。 结语 本文详细介绍了 Go 语言的一些优秀的设计准则,虽然有的准则的好处平常看起来没有那么明显。但当代码库和团队规模增长几个数量级时,这些准则可能会使大型工程项目免于许多痛苦。总的来说,正是这些设计准则让 Go 语言成为了除 Java 之外的编程语言里,用于大型软件开发项目的绝佳选择。

有只黑白猫 2020-01-07 14:11:38 0 浏览量 回答数 0

回答

首先“缓存”Cache这个东西是干什么的,我们应该先有些基本的了解。要是不太明白的可以看看网上的解释:http://baike.baidu.com/view/907.htm 简单讲,阿里云OCS提供的功能就是提供对热点数据的高速访问。在使用OCS之前(或者在使用任何一种缓存服务之前),我们都应该明白关于缓存的这么几点: 缓存里的数据不是持久化保存的,也就是说它像是电脑里的内存,而不像硬盘;我们不能指望OCS里的数据一直保存不丢失。如果你真的需要存储持久化的数据,也许你应该出门左转找阿里云OSS(开发存储服务); 缓存里存的应该是“热点”数据。遵循常常出现的“20-80法则”,通常程序应用中都有一定比例的数据常常被请求访问,这就是所谓的热点数据,OCS正是为这种数据设计存在的。假定我们的程序中有100个数据,每次访问这些数据的概率完全是均匀分布的1/100,那么使用缓存的效果就不会太好,因为这其中不存在热点数据。 数据逐出。我们可以决定哪些数据是热点数据被放到缓存当中,但是如果我们的缓存容量不够大,这些热点数据中某些最近较少被用到的数据还是会被“挤出去”,这种行为叫做数据逐出。如果想减少出现这种情况,我们可以购买更高容量的OCS。 -------------------------         在开始使用之前,关于阿里云OCS,我们还需要知道以下这些事: 阿里云OCS仅支持阿里云内网访问,不支持公网访问。也就是说,我们用办公室或者家里的电脑(都属于公网)是无法连上阿里云OCS的。为什么会这样呢?因为缓存服务的根本目标是要提供低延迟的高速访问,而从公网电脑来连接OCS服务器的场景下,公网的网络环境是不可控的,可能出现延迟很高甚至断连接的情况,这使得缓存服务无法保证“高速、低延迟”的基本特性,所以阿里云OCS是不支持公网直接访问的。如果觉得高延迟的情况对于我们的应用也能接受,那么我们应该去选择阿里云其他的产品(比如OSS开放存储服务),而不应该选择OCS缓存服务。 阿里云OCS需要与ECS(阿里云服务器)配合使用,而且只能与本地区节点的ECS连通。这一点与上一条相关。OCS只能从阿里云内网访问,也就是说我们只能从阿里云ECS上才能访问并使用OCS服务。所以我们在官网购买OCS的时候,会看到提示信息说需要至少有一台ECS才能买OCS。另外,阿里云ECS是分地区节点的,比如北京、杭州、青岛等,我们在购买OCS缓存的时候也要选相应的地区节点。北京的ECS只能访问北京的OCS,而不能访问杭州或青岛的OCS。 阿里云OCS是按购买量收费的,而不是按使用量收费。这点需要提醒新同学们注意,在我们购买了OCS缓存之后,计费就已经开始了,即使我们还没有真正使用缓存。也就是说,我们买了1G的OCS缓存后,即使目前使用量为0,系统也会按照1G的标准来计费。所以我们在购买OCS的时候,要选取适合我们业务数据需要的缓存档位。当然了,阿里云OCS也提供在线升降缓存容量的功能。也就是说,如果我们在使用了一段时间之后,发现购买的OCS缓存不够用了(或者缓存使用量太低),我们可以在线的对已有的OCS实例进行升档(或者降档),而OCS缓存服务不会被中断。 阿里云OCS对于存贮的对象大小是有限制的。缓存通常对其内部存储的数据尺寸是有限制的,阿里云OCS也一样。目前OCS支持存储的数据对象的上限是1,000,000Byte。如果要存的值超过这个限制,我们应该考虑把数据压缩,或从逻辑上分成不同键存储的几个值。 ------------------------- 现在我们开始在阿里云官网上购买OCS实例  http://buy.aliyun.com/ocs  首先我们需要已经有了一台阿里云ECS,否则我们无法在这个页面成功购买OCS。购买的第一步,我们先要确定选择买哪个地区的OCS;这个很重要,如上面所说,如果我们的ECS是属于北京,而我们在这里购买了杭州的OCS,那么这两者是无法配合协同工作的。所以,在购买OCS的时候一定要选择应用服务器ECS所在地区的OCS。下一步是要选择OCS缓存容量。我们要购买多大的缓存,这个取决于我们对自身业务应用中热点数据总量大小的判断。如果一时难以准确判断数据量,也不用担心:我们可以先买一个大致容量的OCS(比如1GB),随后在使用过程中,通过OCS控制台提供的监控功能,我们可以了解到目前OCS缓存的使用量等数据,然后可以自主的调整所需的缓存量,购买更大的缓存(比如升到5GB)或者减少已购的缓存量(比如降到512MB),阿里云会根据我们选择的新配置来调整对应的收费。此外在选择缓存容量的时候,要知道不同容量的缓存档位对应着不同的性能配额,具体来说包括两个指标:吞吐量带宽与每秒请求处理数(QPS)。比如以现在的配额标准,1GB的OCS缓存对应5MB/sec的吞吐量带宽和3000次/sec的请求处理峰值。当我们使用OCS的时候,如果数据量传输的带宽超过了5MB/s, 或者每秒的请求数超过了3000次,都会触发性能配额控制机制,导致某些请求无法返回正常结果。在确定了地区和缓存容量之后,我们就可以直接下单购买OCS了。 ------------------------- 在成功购买OCS之后,我们的联系邮箱和手机都会收到OCS创建成功的通知,里面会包括OCS的实例ID和初始密码(关于密码的用处后面会讲到)。我们现在登录OCS控制台, http://ocs.console.aliyun.com/ 就可以看到已经购买到的OCS实例列表。在列表页面上对应OCS实例的后面点击“管理”,就可以进入该OCS实例的详情页,看到更多的详细信息。 ------------------------- 我们现在已经有了一个OCS缓存实例,现在是时候试玩OCS了。要使用OCS就要写一点程序代码,不过不用担心,我们在这里采用“Happy-Path”的方法,从最简单的操作开始,让新上手的菜鸟们能马上就有一个能调用OCS缓存服务的程序。OCS提供缓存服务,它并不要求我们的程序是哪种语言来写的。我们这里先以Java程序为例,写一个最简单的“Hello World”。(其他编程语言的例子,我们随后附上。)第一步,登录你的阿里云ECS服务器,在上面安装Java JDK和你常用的IDE(比如Eclipse)。一定要记得我们之前说过的,只有在阿里云内网的ECS服务器上,才能访问我们的OCS实例。所以,用家里或是公司的电脑执行下面的代码示例是看不到结果的。 Java JDK和Eclipse都很容易从网上找到下载,比如 http://download.eclipse.org/ 或者 http://www.onlinedown.net/soft/32289.htm 第二步,在把Java开发环境准备好了之后,下载第一个代码示例(Sample-Code-1第三步,在Eclipse里面打开刚下载的OcsSample1.java,我们要根据自己的OCS实例信息修改几个地方。        我们每个人买到的OCS实例的ID都是不重复的,其对应的阿里云内网地址也是独一无二的,这些信息都在OCS控制台上显示出来。我们在同自己的OCS实例建立连接的时候,需要根据这些信息修改OcsSample1.java中的对应地方。         public static void main(String[] args) {                                        final String host = "b2fd2f89f49f11e3.m.cnqdalicm9pub001.ocs.aliyuncs.com"; //控制台上的“内网地址”                   final String port ="11211";       //默认端口 11211,不用改                   final String username = "b2fd2f89f49f11e3"; //控制台上的“访问账号”                   final String password = "my_password"; //邮件或短信中提供的“密码”                   …… …… ……       信息修改完毕,我们可以运行自己的程序了。运行main函数,我们会在Eclipse下面的console窗口看到下面这样的结果(请忽略可能出现的红色INFO调试信息): OCS Sample CodeSet操作完成!Get操作: Open Cache Service,  from www.Aliyun.com     OK,搞定!我们已经成功的连接上了阿里云的OCS并且调用缓存服务成功,就这么简单。-------------------------我们已经成功运行了第一个调用阿里云OCS缓存服务的Sample程序OcsSample1.java,现在我们看看这个程序里都做了什么。                                  …… …… ……                            System.out.println("OCS Sample Code");                                                        //向OCS中存一个key为"ocs"的数据,便于后面验证读取数据,                             //这个数据对应的value是字符串 Open Cache Service,  from www.Aliyun.com                            OperationFuture future = cache.set("ocs", 1000," Open Cache Service,  from www.Aliyun.com");                            //向OCS中存若干个数据,随后可以在OCS控制台监控上看到统计信息                            for(int i=0;i<100;i++){                                String key="key-"+i;                                String value="value-"+i;                                 //执行set操作,向缓存中存数据                                cache.set(key, 1000, value);                            }                             System.out.println("Set操作完成!");                             future.get();  //  确保之前(cache.set())操作已经结束                         //执行get操作,从缓存中读数据,读取key为"ocs"的数据                            System.out.println("Get操作:"+cache.get("ocs"));                            …… …… …… 从这些代码中可以看出: 1. 我们在建立与OCS缓存服务器的连接后,先是向缓存中存(set)了一个“key-value”(键值对)形式的数据,这个数据的key是字符串“ocs”,其对应的value也是字符串;2. 接着我们继续向缓存中存(set)了100个其他简单的“key-value”数据。3. 最后我们进行功能验证。根据之前给定的key,从缓存中获取(get)其对应的value:也就是输入字符串“ocs”,缓存给我们返回value对应的字符串。 以上的步骤中,1与3是相对应的,我们只有先向缓存中set了某个数据,后面才能从缓存中get到这个数据。步骤2中程序向缓存set了100个数据,是为了从另一个方面进行验证。我们回到阿里云OCS控制台,打开“实例详情”页,在“实例监控”的部分点击刷新,会看到其中一些监控项的值已经发生了变化(注:监控信息的刷新可能存在数秒的延迟), 其中的“Key的个数”已经变成了101,也就是说我们程序已经成功地向OCS缓存中存放了101个数据。-------------------------在写下一篇技术贴之前,列一些OCS用户在入门时问到的问题,方便其他刚认识OCS的同学:Question:买了1G的OCS,那就相当于这个1G是专门缓存用的,与ECS服务器的内存没关系是吧~Answer:是的,OCS的缓存容量与您ECS的内存容量是没关系的。Question:OCS 外网测试,怎么连接?有没有外网连接地址哦?Answer:OCS是不能从外网访问的。参照上面的文章。Question:我之前那个OCS可以正常使用,但现在换了一个OCS就不行了,怎么回事?Answer:经核实您的主机是属于杭州节点的,而现在这个OCS是青岛节点的,不同地域之间的产品内网不互通。Question:在设置一个value时,如果指定过期时间为0,会永久保留吗?Answer:指定过期时间为0,OCS就认为此数据不根据过期时间发生淘汰;但是,此数据仍有可能基于LRU被其他数据淘汰,或者由内存清理造成丢失 ,因此不能认为这个value会永久保留。 Question:对OCS的访问是否需要负载均衡? Answer:不需要。对访问请求的负载均衡都是在OCS服务器端来进行的,用户直接使用缓存服务即可,不用考虑负载均衡的事情。 Question:OCS是否会主动关闭闲置的连接? 如果会,请问连接闲置多久会被关闭?Answer:OCS不会主动关闭闲置的用户连接。但是用户的环境如果使用了SLB,则需要参考SLB连接关闭时间。Question:如何设置数据在OCS缓存中的过期时间 ?Answer:关于设置缓存数据的过期时间,可以参考Memcached官方说明: https://code.google.com/p/memcached/wiki/NewCommands An expiration time, in seconds. Can be up to 30 days. After 30 days, is treated as a unix timestamp of an exact date. 翻译过来就是:0~2592000表示从当前时刻算起的时间长度(以秒计算,最长2592000即30天);大于2592000表示UNIX时间戳。 此值设置为0表明此数据不会主动过期。------------------------- 回 12楼(村里一把手) 的帖子 谢谢,要让大家用得好才算数。 -------------------------缓存与数据库相结合使用,是常见的一种应用搭配场景。现在我们再看一个例子,是用OCS搭配MySQL数据库使用。Java示例代码在此(这个示例代码中,大部分与前几个例子类似。因为要与数据库结合,所以程序需要依赖一个JDBC的jar包才能运行。支持MySQL的JDBC jar包在此(在程序中添加MySQL数据库的连接信息:     …… …… ……            // JDBC driver name and database URL    static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";    static final String DB_URL = "jdbc:mysql://xxxxxxx.mysql.rds.aliyuncs.com/testdb"; //MySQL数据库URL        //  Database用户名及密码    static final String DB_USER = "xxxxxx";    static final String DB_PASS = "xxxxxx";            我们设想这样一个场景:我们需要从数据库的tableone表中查找区域不属于北京的记录总数,用SQL表示就是:SELECT count(*)  FROM testdb.tableone where region != 'beijing'假定这个表中的数据如下,则这条SQL查询返回的结果就是7:如果这个查询被调用到的频率很高,多个用户反复不断的在数据库中查这个数据,我们就可以把这个查询结果放到OCS缓存中去。看下面的代码片段,我们用for循环模拟用户连续20次在数据库中查询上述SQL语句:              for (int i = 1; i <= 20; i++) {                String sql = "SELECT count(*)  FROM testdb.tableone where region != 'beijing'";                String key ="non-beijing"; //给SQL语句自定义一个key                //在OCS缓存里按key查找               String value =  (String) cache.get(key);                                if (value == null) {                    // 在OCS缓存里没有命中                    // step 1:从My SQL数据库中查询                    //Load MySQL Driver                      Class.forName(JDBC_DRIVER);                     con = DriverManager.getConnection(DB_URL, DB_USER, DB_PASS);                    ps = con.prepareStatement(sql);                    ResultSet result = ps.executeQuery(sql);                    result.next();                                        value=result.getString(1);                    System.out.println("从MySQL中查询数据.  Key= "+key+" Value="+value);                                       // step 2: 把数据库返回的数据作为value存放到OCS缓存中去                    cache.set(key, EXPIRE_TIME, value);                                    } else {                    // 在OCS缓存里命中                    System.out.println("从OCS中读取数据.     Key= "+key+" Value="+value);                }                            }// end of for在这段代码中我们可以看到,我们给这条SQL语句标记了一个key,当有用户要执行这条SQL的时候,我们首先按照key在OCS缓存中查找:如果没有对应的缓存数据,则连接MySQL数据库执行SQL查询,把结果返回给用户,并把这个查询结果存到OCS缓存中去;如果OCS中已经有了对应的缓存数据,则直接把缓存数据返回给用户。运行结果如下: 从MySQL中查询数据.  Key= non-beijing, Value=7从OCS中读取数据.     Key= non-beijing, Value=7从OCS中读取数据.     Key= non-beijing, Value=7从OCS中读取数据.     Key= non-beijing, Value=7…… …… 从结果可以看出,程序第1次是从MySQL数据库当中查询数据,后面的19次都是从OCS缓存中获取key对应的value直接返回。也就是说,OCS降低了程序去连接MySQL数据库执行SQL查询的次数,减轻了对数据库的负载压力。用户对热点数据访问的频率越高,OCS的这种优势就越明显。

唐翰 2019-12-01 23:41:23 0 浏览量 回答数 0

回答

Kafka 是目前主流的分布式消息引擎及流处理平台,经常用做企业的消息总线、实时数据管道,本文挑选了 Kafka 的几个核心话题,帮助大家快速掌握 Kafka,包括: Kafka 体系架构 Kafka 消息发送机制 Kafka 副本机制 Kafka 控制器 Kafka Rebalance 机制 因为涉及内容较多,本文尽量做到深入浅出,全面的介绍 Kafka 原理及核心组件,不怕你不懂 Kafka。 1. Kafka 快速入门 Kafka 是一个分布式消息引擎与流处理平台,经常用做企业的消息总线、实时数据管道,有的还把它当做存储系统来使用。早期 Kafka 的定位是一个高吞吐的分布式消息系统,目前则演变成了一个成熟的分布式消息引擎,以及流处理平台。 1.1 Kafka 体系架构 Kafka 的设计遵循生产者消费者模式,生产者发送消息到 broker 中某一个 topic 的具体分区里,消费者从一个或多个分区中拉取数据进行消费。拓扑图如下: 目前,Kafka 依靠 Zookeeper 做分布式协调服务,负责存储和管理 Kafka 集群中的元数据信息,包括集群中的 broker 信息、topic 信息、topic 的分区与副本信息等。 ** 1.2 Kafka 术语** 这里整理了 Kafka 的一些关键术语: Producer:生产者,消息产生和发送端。 Broker:Kafka 实例,多个 broker 组成一个 Kafka 集群,通常一台机器部署一个 Kafka 实例,一个实例挂了不影响其他实例。 Consumer:消费者,拉取消息进行消费。 一个 topic 可以让若干个消费者进行消费,若干个消费者组成一个 Consumer Group 即消费组,一条消息只能被消费组中一个 Consumer 消费。 Topic:主题,服务端消息的逻辑存储单元。一个 topic 通常包含若干个 Partition 分区。 Partition:topic 的分区,分布式存储在各个 broker 中, 实现发布与订阅的负载均衡。若干个分区可以被若干个 Consumer 同时消费,达到消费者高吞吐量。一个分区拥有多个副本(Replica),这是Kafka在可靠性和可用性方面的设计,后面会重点介绍。 message:消息,或称日志消息,是 Kafka 服务端实际存储的数据,每一条消息都由一个 key、一个 value 以及消息时间戳 timestamp 组成。 offset:偏移量,分区中的消息位置,由 Kafka 自身维护,Consumer 消费时也要保存一份 offset 以维护消费过的消息位置。 1.3 Kafka 作用与特点 Kafka 主要起到削峰填谷(缓冲)、系统解构以及冗余的作用,主要特点有: 高吞吐、低延时:这是 Kafka 显著的特点,Kafka 能够达到百万级的消息吞吐量,延迟可达毫秒级; 持久化存储:Kafka 的消息最终持久化保存在磁盘之上,提供了顺序读写以保证性能,并且通过 Kafka 的副本机制提高了数据可靠性。 分布式可扩展:Kafka 的数据是分布式存储在不同 broker 节点的,以 topic 组织数据并且按 partition 进行分布式存储,整体的扩展性都非常好。 高容错性:集群中任意一个 broker 节点宕机,Kafka 仍能对外提供服务。 2. Kafka 消息发送机制 Kafka 生产端发送消息的机制非常重要,这也是 Kafka 高吞吐的基础,生产端的基本流程如下图所示: 主要有以下方面的设计: 2.1 异步发送 Kafka 自从 0.8.2 版本就引入了新版本 Producer API,新版 Producer 完全是采用异步方式发送消息。生产端构建的 ProducerRecord 先是经过 keySerializer、valueSerializer 序列化后,再是经过 Partition 分区器处理,决定消息落到 topic 具体某个分区中,最后把消息发送到客户端的消息缓冲池 accumulator 中,交由一个叫作 Sender 的线程发送到 broker 端。 这里缓冲池 accumulator 的最大大小由参数 buffer.memory 控制,默认是 32M,当生产消息的速度过快导致 buffer 满了的时候,将阻塞 max.block.ms 时间,超时抛异常,所以 buffer 的大小可以根据实际的业务情况进行适当调整。 2.2 批量发送 发送到缓冲 buffer 中消息将会被分为一个一个的 batch,分批次的发送到 broker 端,批次大小由参数 batch.size 控制,默认16KB。这就意味着正常情况下消息会攒够 16KB 时才会批量发送到 broker 端,所以一般减小 batch 大小有利于降低消息延时,增加 batch 大小有利于提升吞吐量。 那么生成端消息是不是必须要达到一个 batch 大小时,才会批量发送到服务端呢?答案是否定的,Kafka 生产端提供了另一个重要参数 linger.ms,该参数控制了 batch 最大的空闲时间,超过该时间的 batch 也会被发送到 broker 端。 2.3 消息重试 此外,Kafka 生产端支持重试机制,对于某些原因导致消息发送失败的,比如网络抖动,开启重试后 Producer 会尝试再次发送消息。该功能由参数 retries 控制,参数含义代表重试次数,默认值为 0 表示不重试,建议设置大于 0 比如 3。 3. Kafka 副本机制 前面提及了 Kafka 分区副本(Replica)的概念,副本机制也称 Replication 机制是 Kafka 实现高可靠、高可用的基础。Kafka 中有 leader 和 follower 两类副本。 3.1 Kafka 副本作用 Kafka 默认只会给分区设置一个副本,由 broker 端参数 default.replication.factor 控制,默认值为 1,通常我们会修改该默认值,或者命令行创建 topic 时指定 replication-factor 参数,生产建议设置 3 副本。副本作用主要有两方面: 消息冗余存储,提高 Kafka 数据的可靠性; 提高 Kafka 服务的可用性,follower 副本能够在 leader 副本挂掉或者 broker 宕机的时候参与 leader 选举,继续对外提供读写服务。 3.2 关于读写分离 这里要说明的是 Kafka 并不支持读写分区,生产消费端所有的读写请求都是由 leader 副本处理的,follower 副本的主要工作就是从 leader 副本处异步拉取消息,进行消息数据的同步,并不对外提供读写服务。 Kafka 之所以这样设计,主要是为了保证读写一致性,因为副本同步是一个异步的过程,如果当 follower 副本还没完全和 leader 同步时,从 follower 副本读取数据可能会读不到最新的消息。 3.3 ISR 副本集合 Kafka 为了维护分区副本的同步,引入 ISR(In-Sync Replicas)副本集合的概念,ISR 是分区中正在与 leader 副本进行同步的 replica 列表,且必定包含 leader 副本。 ISR 列表是持久化在 Zookeeper 中的,任何在 ISR 列表中的副本都有资格参与 leader 选举。 ISR 列表是动态变化的,并不是所有的分区副本都在 ISR 列表中,哪些副本会被包含在 ISR 列表中呢?副本被包含在 ISR 列表中的条件是由参数 replica.lag.time.max.ms 控制的,参数含义是副本同步落后于 leader 的最大时间间隔,默认10s,意思就是说如果某一 follower 副本中的消息比 leader 延时超过10s,就会被从 ISR 中排除。Kafka 之所以这样设计,主要是为了减少消息丢失,只有与 leader 副本进行实时同步的 follower 副本才有资格参与 leader 选举,这里指相对实时。 3.4 Unclean leader 选举 既然 ISR 是动态变化的,所以 ISR 列表就有为空的时候,ISR 为空说明 leader 副本也“挂掉”了,此时 Kafka 就要重新选举出新的 leader。但 ISR 为空,怎么进行 leader 选举呢? Kafka 把不在 ISR 列表中的存活副本称为“非同步副本”,这些副本中的消息远远落后于 leader,如果选举这种副本作为 leader 的话就可能造成数据丢失。Kafka broker 端提供了一个参数 unclean.leader.election.enable,用于控制是否允许非同步副本参与 leader 选举;如果开启,则当 ISR 为空时就会从这些副本中选举新的 leader,这个过程称为 Unclean leader 选举。 前面也提及了,如果开启 Unclean leader 选举,可能会造成数据丢失,但保证了始终有一个 leader 副本对外提供服务;如果禁用 Unclean leader 选举,就会避免数据丢失,但这时分区就会不可用。这就是典型的 CAP 理论,即一个系统不可能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition Tolerance)中的两个。所以在这个问题上,Kafka 赋予了我们选择 C 或 A 的权利。 我们可以根据实际的业务场景选择是否开启 Unclean leader选举,这里建议关闭 Unclean leader 选举,因为通常数据的一致性要比可用性重要的多。 4. Kafka 控制器 控制器(Controller)是 Kafka 的核心组件,它的主要作用是在 Zookeeper 的帮助下管理和协调整个 Kafka 集群。集群中任意一个 broker 都能充当控制器的角色,但在运行过程中,只能有一个 broker 成为控制器。 这里先介绍下 Zookeeper,因为控制器的产生依赖于 Zookeeper 的 ZNode 模型和 Watcher 机制。Zookeeper 的数据模型是类似 Unix 操作系统的 ZNode Tree 即 ZNode 树,ZNode 是 Zookeeper 中的数据节点,是 Zookeeper 存储数据的最小单元,每个 ZNode 可以保存数据,也可以挂载子节点,根节点是 /。基本的拓扑图如下: Zookeeper 有两类 ZNode 节点,分别是持久性节点和临时节点。持久性节点是指客户端与 Zookeeper 断开会话后,该节点依旧存在,直到执行删除操作才会清除节点。临时节点的生命周期是和客户端的会话绑定在一起,客户端与 Zookeeper 断开会话后,临时节点就会被自动删除。 Watcher 机制是 Zookeeper 非常重要的特性,它可以在 ZNode 节点上绑定监听事件,比如可以监听节点数据变更、节点删除、子节点状态变更等事件,通过这个事件机制,可以基于 ZooKeeper 实现分布式锁、集群管理等功能。 4.1 控制器选举 当集群中的任意 broker 启动时,都会尝试去 Zookeeper 中创建 /controller 节点,第一个成功创建 /controller 节点的 broker 则会被指定为控制器,其他 broker 则会监听该节点的变化。当运行中的控制器突然宕机或意外终止时,其他 broker 能够快速地感知到,然后再次尝试创建 /controller 节点,创建成功的 broker 会成为新的控制器。 4.2 控制器功能 前面我们也说了,控制器主要作用是管理和协调 Kafka 集群,那么 Kafka 控制器都做了哪些事情呢,具体如下: 主题管理:创建、删除 topic,以及增加 topic 分区等操作都是由控制器执行。 分区重分配:执行 Kafka 的 reassign 脚本对 topic 分区重分配的操作,也是由控制器实现。 Preferred leader 选举:这里有一个概念叫 Preferred replica 即优先副本,表示的是分配副本中的第一个副本。Preferred leader 选举就是指 Kafka 在某些情况下出现 leader 负载不均衡时,会选择 preferred 副本作为新 leader 的一种方案。这也是控制器的职责范围。 集群成员管理:控制器能够监控新 broker 的增加,broker 的主动关闭与被动宕机,进而做其他工作。这里也是利用前面所说的 Zookeeper 的 ZNode 模型和 Watcher 机制,控制器会监听 Zookeeper 中 /brokers/ids 下临时节点的变化。 数据服务:控制器上保存了最全的集群元数据信息,其他所有 broker 会定期接收控制器发来的元数据更新请求,从而更新其内存中的缓存数据。 从上面内容我们大概知道,控制器可以说是 Kafka 的心脏,管理和协调着整个 Kafka 集群,因此控制器自身的性能和稳定性就变得至关重要。 社区在这方面做了大量工作,特别是在 0.11 版本中对控制器进行了重构,其中最大的改进把控制器内部多线程的设计改成了单线程加事件队列的方案,消除了多线程的资源消耗和线程安全问题,另外一个改进是把之前同步操作 Zookeeper 改为了异步操作,消除了 Zookeeper 端的性能瓶颈,大大提升了控制器的稳定性。 5. Kafka 消费端 Rebalance 机制 前面介绍消费者术语时,提到了消费组的概念,一个 topic 可以让若干个消费者进行消费,若干个消费者组成一个 Consumer Group 即消费组 ,一条消息只能被消费组中的一个消费者进行消费。我们用下图表示Kafka的消费模型。 5.1 Rebalance 概念 就 Kafka 消费端而言,有一个难以避免的问题就是消费者的重平衡即 Rebalance。Rebalance 是让一个消费组的所有消费者就如何消费订阅 topic 的所有分区达成共识的过程,在 Rebalance 过程中,所有 Consumer 实例都会停止消费,等待 Rebalance 的完成。因为要停止消费等待重平衡完成,因此 Rebalance 会严重影响消费端的 TPS,是应当尽量避免的。 5.2 Rebalance 发生条件 关于何时会发生 Rebalance,总结起来有三种情况: 消费组的消费者成员数量发生变化 消费主题的数量发生变化 消费主题的分区数量发生变化 其中后两种情况一般是计划内的,比如为了提高消息吞吐量增加 topic 分区数,这些情况一般是不可避免的,后面我们会重点讨论如何避免因为组内消费者成员数发生变化导致的 Rebalance。 5.3 Kafka 协调器 在介绍如何避免 Rebalance 问题之前,先来认识下 Kafka 的协调器 Coordinator,和之前 Kafka 控制器类似,Coordinator 也是 Kafka 的核心组件。 主要有两类 Kafka 协调器: 组协调器(Group Coordinator) 消费者协调器(Consumer Coordinator) Kafka 为了更好的实现消费组成员管理、位移管理,以及 Rebalance 等,broker 服务端引入了组协调器(Group Coordinator),消费端引入了消费者协调器(Consumer Coordinator)。每个 broker 启动的时候,都会创建一个 GroupCoordinator 实例,负责消费组注册、消费者成员记录、offset 等元数据操作,这里也可以看出每个 broker 都有自己的 Coordinator 组件。另外,每个 Consumer 实例化时,同时会创建一个 ConsumerCoordinator 实例,负责消费组下各个消费者和服务端组协调器之前的通信。可以用下图表示协调器原理: 客户端的消费者协调器 Consumer Coordinator 和服务端的组协调器 Group Coordinator 会通过心跳不断保持通信。 5.4 如何避免消费组 Rebalance 接下来我们讨论下如何避免组内消费者成员发生变化导致的 Rebalance。组内成员发生变化无非就两种情况,一种是有新的消费者加入,通常是我们为了提高消费速度增加了消费者数量,比如增加了消费线程或者多部署了一份消费程序,这种情况可以认为是正常的;另一种是有消费者退出,这种情况多是和我们消费端代码有关,是我们要重点避免的。 正常情况下,每个消费者都会定期向组协调器 Group Coordinator 发送心跳,表明自己还在存活,如果消费者不能及时的发送心跳,组协调器会认为该消费者已经“死”了,就会导致消费者离组引发 Rebalance 问题。这里涉及两个消费端参数:session.timeout.ms 和 heartbeat.interval.ms,含义分别是组协调器认为消费组存活的期限,和消费者发送心跳的时间间隔,其中 heartbeat.interval.ms 默认值是3s,session.timeout.ms 在 0.10.1 版本之前默认 30s,之后默认 10s。另外,0.10.1 版本还有两个值得注意的地方: 从该版本开始,Kafka 维护了单独的心跳线程,之前版本中 Kafka 是使用业务主线程发送的心跳。 增加了一个重要的参数 max.poll.interval.ms,表示 Consumer 两次调用 poll 方法拉取数据的最大时间间隔,默认值 5min,对于那些忙于业务逻辑处理导致超过 max.poll.interval.ms 时间的消费者将会离开消费组,此时将发生一次 Rebalance。 此外,如果 Consumer 端频繁 FullGC 也可能会导致消费端长时间停顿,从而引发 Rebalance。因此,我们总结如何避免消费组 Rebalance 问题,主要从以下几方面入手: 合理配置 session.timeout.ms 和 heartbeat.interval.ms,建议 0.10.1 之前适当调大 session 超时时间尽量规避 Rebalance。 根据实际业务调整 max.poll.interval.ms,通常建议调大避免 Rebalance,但注意 0.10.1 版本之前没有该参数。 监控消费端的 GC 情况,避免由于频繁 FullGC 导致线程长时间停顿引发 Rebalance。 合理调整以上参数,可以减少生产环境中 Rebalance 发生的几率,提升 Consumer 端的 TPS 和稳定性。 6.总结 本文总结了 Kafka 体系架构、Kafka 消息发送机制、副本机制,Kafka 控制器、消费端 Rebalance 机制等各方面核心原理,通过本文的介绍,相信你已经对 Kafka 的内核知识有了一定的掌握,更多的 Kafka 原理实践后面有时间再介绍。

剑曼红尘 2020-04-16 18:15:45 0 浏览量 回答数 0

回答

1 写出下面代码输出内容。 package main import (    "fmt" ) funcmain() {     defer_call() } funcdefer_call() {     deferfunc() {fmt.Println("打印前")}()     deferfunc() {fmt.Println("打印中")}()     deferfunc() {fmt.Println("打印后")}()     panic("触发异常") } 考点:defer执行顺序 解答: defer 是后进先出。 panic 需要等defer 结束后才会向上传递。 出现panic恐慌时候,会先按照defer的后入先出的顺序执行,最后才会执行panic。 打印后 打印中 打印前 panic: 触发异常 2 以下代码有什么问题,说明原因。 type student struct {     Name string     Age  int } funcpase_student() {     m := make(map[string]*student)     stus := []student{         {Name: "zhou",Age: 24},         {Name: "li",Age: 23},         {Name: "wang",Age: 22},     }    for _,stu := range stus {         m[stu.Name] =&stu     } } 考点:foreach 解答: 这样的写法初学者经常会遇到的,很危险! 与Java的foreach一样,都是使用副本的方式。所以m[stu.Name]=&stu实际上一致指向同一个指针, 最终该指针的值为遍历的最后一个struct的值拷贝。 就像想修改切片元素的属性: for _, stu := rangestus {     stu.Age = stu.Age+10} 也是不可行的。 大家可以试试打印出来: func pase_student() {     m := make(map[string]*student)     stus := []student{         {Name: "zhou",Age: 24},         {Name: "li",Age: 23},         {Name: "wang",Age: 22},     }         // 错误写法     for _,stu := range stus {         m[stu.Name] =&stu     }          fork,v:=range m{               println(k,"=>",v.Name)     }           // 正确     for i:=0;i<len(stus);i++ {        m[stus[i].Name] = &stus[i]     }          fork,v:=range m{                println(k,"=>",v.Name)     } } 3 下面的代码会输出什么,并说明原因 func main() {     runtime.GOMAXPROCS(1)     wg := sync.WaitGroup{}     wg.Add(20)   for i := 0; i < 10; i++ {                  gofunc() {            fmt.Println("A: ", i)            wg.Done()         }()     }             for i:= 0; i < 10; i++ {                    gofunc(i int) {            fmt.Println("B: ", i)            wg.Done()         }(i)     }     wg.Wait() } 考点:go执行的随机性和闭包 解答: 谁也不知道执行后打印的顺序是什么样的,所以只能说是随机数字。 但是A:均为输出10,B:从0~9输出(顺序不定)。 第一个go func中i是外部for的一个变量,地址不变化。遍历完成后,最终i=10。 故go func执行时,i的值始终是10。 第二个go func中i是函数参数,与外部for中的i完全是两个变量。 尾部(i)将发生值拷贝,go func内部指向值拷贝地址。 4 下面代码会输出什么? type People struct{}func (p People)ShowA() {     fmt.Println("showA")     p.ShowB() } func(pPeople)ShowB() {     fmt.Println("showB") } typeTeacher struct {     People } func(t*Teacher)ShowB() {     fmt.Println("teachershowB") } funcmain() {     t := Teacher{}     t.ShowA() } 考点:go的组合继承 解答: 这是Golang的组合模式,可以实现OOP的继承。 被组合的类型People所包含的方法虽然升级成了外部类型Teacher这个组合类型的方法(一定要是匿名字段),但它们的方法(ShowA())调用时接受者并没有发生变化。 此时People类型并不知道自己会被什么类型组合,当然也就无法调用方法时去使用未知的组合者Teacher类型的功能。 showAshowB 5 下面代码会触发异常吗?请详细说明 func main() {     runtime.GOMAXPROCS(1)     int_chan := make(chanint, 1)     string_chan := make(chanstring, 1)     int_chan <- 1     string_chan <- "hello"     select {                case value := <-int_chan:        fmt.Println(value)           casevalue := <-string_chan:                   panic(value)     } } 考点:select随机性 解答: select会随机选择一个可用通用做收发操作。 所以代码是有肯触发异常,也有可能不会。 单个chan如果无缓冲时,将会阻塞。但结合 select可以在多个chan间等待执行。有三点原则: select 中只要有一个case能return,则立刻执行。 当如果同一时间有多个case均能return则伪随机方式抽取任意一个执行。 如果没有一个case能return则可以执行”default”块。 6 下面代码输出什么? funccalc(indexstring, a, bint) int {     ret := a+ b     fmt.Println(index,a, b, ret)     return ret } funcmain() {          a := 1     b := 2     defer calc("1", a,calc("10", a, b))    a = 0     defer calc("2", a,calc("20", a, b))    b = 1 } 考点:defer执行顺序 解答: 这道题类似第1题 需要注意到defer执行顺序和值传递 index:1肯定是最后执行的,但是index:1的第三个参数是一个函数,所以最先被调用 calc("10",1,2)==>10,1,2,3 执行index:2时,与之前一样,需要先调用calc("20",0,2)==>20,0,2,2 执行到b=1时候开始调用,index:2==>calc("2",0,2)==>2,0,2,2最后执行index:1==>calc("1",1,3)==>1,1,3,4 10 1 2 320 0 2 22 0 2 21 1 3 4 7 请写出以下输入内容 funcmain() {            s := make([]int,5)     s = append(s,1, 2, 3)     fmt.Println(s) } 考点:make默认值和append 解答: make初始化是由默认值的哦,此处默认值为0 [00000123] 大家试试改为: s := make([]int, 0) s = append(s, 1, 2, 3) fmt.Println(s)//[1 2 3] 8 下面的代码有什么问题? type UserAges struct {     ages map[string]int     sync.Mutex } func(uaUserAges)Add(name string, age int) {     ua.Lock()          deferua.Unlock()     ua.ages[name] = age } func(uaUserAges)Get(name string)int {           ifage, ok := ua.ages[name]; ok {                  return age     }         return-1 } 考点:map线程安全 解答: 可能会出现 fatal error: concurrent mapreadandmapwrite. 修改一下看看效果 func (ua *UserAges)Get(namestring)int {     ua.Lock()          deferua.Unlock()          ifage, ok := ua.ages[name]; ok {                   return age     }            return-1 } 9.   下面的迭代会有什么问题? func (set *threadSafeSet)Iter()<-chaninterface{} {     ch := make(chaninterface{})                  gofunc() {         set.RLock()                for elem := range set.s {            ch <- elem         }                   close(ch)         set.RUnlock()     }()      return ch } 考点:chan缓存池 解答: 看到这道题,我也在猜想出题者的意图在哪里。 chan?sync.RWMutex?go?chan缓存池?迭代? 所以只能再读一次题目,就从迭代入手看看。 既然是迭代就会要求set.s全部可以遍历一次。但是chan是为缓存的,那就代表这写入一次就会阻塞。 我们把代码恢复为可以运行的方式,看看效果 package main import (          "sync"     "fmt")//下面的迭代会有什么问题?type threadSafeSet struct {     sync.RWMutex     s []interface{} } func(set*threadSafeSet)Iter() <-chaninterface{} {     //ch := make(chan interface{}) // 解除注释看看!     ch := make(chaninterface{},len(set.s))    gofunc() {         set.RLock()        forelem,value := range set.s {            ch <- elem             println("Iter:",elem,value)         }       close(ch)         set.RUnlock()     }()     return ch } funcmain() {     th:=threadSafeSet{         s:[]interface{}{"1","2"},     }     v:=<-th.Iter()     fmt.Sprintf("%s%v","ch",v) } 10 以下代码能编译过去吗?为什么? package main import (   "fmt") typePeople interface {     Speak(string) string } typeStduent struct{} func(stu*Stduent)Speak(think string)(talk string) {     ifthink == "bitch" {         talk = "Youare a good boy"     } else {         talk = "hi"     }     return } funcmain() {     var peoPeople = Stduent{}     think := "bitch"    fmt.Println(peo.Speak(think)) } 考点:golang的方法集 解答: 编译不通过! 做错了!?说明你对golang的方法集还有一些疑问。 一句话:golang的方法集仅仅影响接口实现和方法表达式转化,与通过实例或者指针调用方法无关。 11 以下代码打印出来什么内容,说出为什么。 package main import (   "fmt") typePeople interface {     Show() } typeStudent struct{} func(stuStudent)Show() { } funclive()People {     var stuStudent     return stu } funcmain() {   if live() == nil {         fmt.Println("AAAAAAA")     } else {         fmt.Println("BBBBBBB")     } } 考点:interface内部结构 解答: 很经典的题! 这个考点是很多人忽略的interface内部结构。 go中的接口分为两种一种是空的接口类似这样: varininterface{} 另一种如题目: type People interface {     Show() } 他们的底层结构如下: type eface struct {      //空接口     _type _type        //类型信息     data  unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void)} typeiface struct {      //带有方法的接口     tab  itab          //存储type信息还有结构实现方法的集合     data unsafe.Pointer  //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void)} type_type struct {     size       uintptr //类型大小     ptrdata    uintptr //前缀持有所有指针的内存大小     hash       uint32  //数据hash值     tflag     tflag     align      uint8   //对齐     fieldalign uint8   //嵌入结构体时的对齐     kind       uint8   //kind 有些枚举值kind等于0是无效的     alg       *typeAlg //函数指针数组,类型实现的所有方法     gcdata    *byte   str       nameOff     ptrToThis typeOff }type itab struct {     inter  *interfacetype //接口类型     _type  *_type         //结构类型     link   *itab     bad    int32     inhash int32     fun    [1]uintptr     //可变大小方法集合} 可以看出iface比eface 中间多了一层itab结构。 itab 存储_type信息和[]fun方法集,从上面的结构我们就可得出,因为data指向了nil 并不代表interface 是nil, 所以返回值并不为空,这里的fun(方法集)定义了接口的接收规则,在编译的过程中需要验证是否实现接口 结果: BBBBBBB 12.是否可以编译通过?如果通过,输出什么? func main() {     i := GetValue() switch i.(type) {          caseint:                println("int")            casestring:                println("string")            caseinterface{}:                println("interface")            default:                 println("unknown")     } } funcGetValue()int {    return1 } 解析 考点:type 编译失败,因为type只能使用在interface 13.下面函数有什么问题? func funcMui(x,y int)(sum int,error){     returnx+y,nil } 解析 考点:函数返回值命名 在函数有多个返回值时,只要有一个返回值有指定命名,其他的也必须有命名。 如果返回值有有多个返回值必须加上括号; 如果只有一个返回值并且有命名也需要加上括号; 此处函数第一个返回值有sum名称,第二个未命名,所以错误。 14.是否可以编译通过?如果通过,输出什么? package mainfunc main() {    println(DeferFunc1(1)) println(DeferFunc2(1)) println(DeferFunc3(1)) }func DeferFunc1(i int)(t int) {     t = i   deferfunc() {         t += 3     }() return t } funcDeferFunc2(i int)int {     t := i  deferfunc() {         t += 3     }() return t } funcDeferFunc3(i int)(t int) {   deferfunc() {         t += i     }() return2} 解析 考点:defer和函数返回值 需要明确一点是defer需要在函数结束前执行。 函数返回值名字会在函数起始处被初始化为对应类型的零值并且作用域为整个函数 DeferFunc1有函数返回值t作用域为整个函数,在return之前defer会被执行,所以t会被修改,返回4; DeferFunc2函数中t的作用域为函数,返回1;DeferFunc3返回3 15.是否可以编译通过?如果通过,输出什么? funcmain() {    list := new([]int)     list = append(list,1)     fmt.Println(list) } 解析 考点:new list:=make([]int,0) 16.是否可以编译通过?如果通过,输出什么? package mainimport "fmt"funcmain() {     s1 := []int{1, 2, 3}     s2 := []int{4, 5}     s1 = append(s1,s2)     fmt.Println(s1) } 解析 考点:append append切片时候别漏了'…' 17.是否可以编译通过?如果通过,输出什么? func main() {     sn1 := struct {         age  int         name string     }{age: 11,name: "qq"}     sn2 := struct {         age  int         name string     }{age: 11,name: "qq"}  if sn1== sn2 {         fmt.Println("sn1== sn2")     }     sm1 := struct {         age int         m   map[string]string     }{age: 11, m:map[string]string{"a": "1"}}     sm2 := struct {         age int         m   map[string]string     }{age: 11, m:map[string]string{"a": "1"}}             if sm1 == sm2 {         fmt.Println("sm1== sm2")     } } 解析 考点:结构体比较 进行结构体比较时候,只有相同类型的结构体才可以比较,结构体是否相同不但与属性类型个数有关,还与属性顺序相关。 sn3:= struct {     name string     age  int } {age:11,name:"qq"} sn3与sn1就不是相同的结构体了,不能比较。 还有一点需要注意的是结构体是相同的,但是结构体属性中有不可以比较的类型,如map,slice。 如果该结构属性都是可以比较的,那么就可以使用“==”进行比较操作。 可以使用reflect.DeepEqual进行比较 if reflect.DeepEqual(sn1, sm) {     fmt.Println("sn1==sm") }else {     fmt.Println("sn1!=sm") } 所以编译不通过: invalid operation: sm1 == sm2 18.是否可以编译通过?如果通过,输出什么? func Foo(x interface{}) {    if x== nil {         fmt.Println("emptyinterface")                 return     }     fmt.Println("non-emptyinterface") }        funcmain() {           var x *int = nil     Foo(x) } 解析 考点:interface内部结构 non-emptyinterface 19.是否可以编译通过?如果通过,输出什么? func GetValue(m map[int]string, id int)(string, bool) {              if _,exist := m[id]; exist {                    return"存在数据", true     }            returnnil, false}funcmain() {     intmap:=map[int]string{    1:"a",        2:"bb",        3:"ccc",     }     v,err:=GetValue(intmap,3)     fmt.Println(v,err) } 解析 考点:函数返回值类型 nil 可以用作 interface、function、pointer、map、slice 和 channel 的“空值”。但是如果不特别指定的话,Go 语言不能识别类型,所以会报错。报:cannot use nil as type string in return argument. 20.是否可以编译通过?如果通过,输出什么? const (     x = iota     y     z = "zz"     k     p = iota) funcmain()  {     fmt.Println(x,y,z,k,p) } 解析 考点:iota 结果: 0 1 zz zz 4 21.编译执行下面代码会出现什么? package mainvar(     size :=1024     max_size = size*2) funcmain() {     println(size,max_size) } 解析 考点:变量简短模式 变量简短模式限制: 定义变量同时显式初始化 不能提供数据类型 只能在函数内部使用 结果: syntaxerror: unexpected := 22.下面函数有什么问题? package main const cl = 100 var bl   = 123 funcmain() {     println(&bl,bl)    println(&cl,cl) } 解析 考点:常量 常量不同于变量的在运行期分配内存,常量通常会被编译器在预处理阶段直接展开,作为指令数据使用, cannot take the address of cl 23.编译执行下面代码会出现什么? package main funcmain() {     for i:=0;i<10;i++  {     loop:        println(i)     }    gotoloop } 解析 考点:goto goto不能跳转到其他函数或者内层代码 goto loop jumps intoblock starting at 24.编译执行下面代码会出现什么? package main import"fmt" funcmain() {      typeMyInt1 int      typeMyInt2 = int     var i int =9     var i1MyInt1 = i     var i2MyInt2 = i     fmt.Println(i1,i2) } 解析 考点:**Go 1.9 新特性 Type Alias ** 基于一个类型创建一个新类型,称之为defintion;基于一个类型创建一个别名,称之为alias。 MyInt1为称之为defintion,虽然底层类型为int类型,但是不能直接赋值,需要强转; MyInt2称之为alias,可以直接赋值。 结果: cannot use i (typeint) astype MyInt1 in assignment 25.编译执行下面代码会出现什么? package main import"fmt" typeUser struct { } typeMyUser1 User typeMyUser2 = User func(iMyUser1)m1(){     fmt.Println("MyUser1.m1") } func(iUser)m2(){     fmt.Println("User.m2") } funcmain() {     var i1MyUser1     var i2MyUser2     i1.m1()     i2.m2() } 解析 考点:**Go 1.9 新特性 Type Alias ** 因为MyUser2完全等价于User,所以具有其所有的方法,并且其中一个新增了方法,另外一个也会有。 但是 i1.m2() 是不能执行的,因为MyUser1没有定义该方法。 结果: MyUser1.m1User.m2 26.编译执行下面代码会出现什么? package main import"fmt" type T1 struct { } func(tT1)m1(){     fmt.Println("T1.m1") } type T2= T1 typeMyStruct struct {     T1     T2 } funcmain() {     my:=MyStruct{}     my.m1() } 解析 考点:**Go 1.9 新特性 Type Alias ** 是不能正常编译的,异常: ambiguousselectormy.m1 结果不限于方法,字段也也一样;也不限于type alias,type defintion也是一样的,只要有重复的方法、字段,就会有这种提示,因为不知道该选择哪个。 改为: my.T1.m1() my.T2.m1() type alias的定义,本质上是一样的类型,只是起了一个别名,源类型怎么用,别名类型也怎么用,保留源类型的所有方法、字段等。 27.编译执行下面代码会出现什么? package main import (           "errors"     "fmt") varErrDidNotWork = errors.New("did not work") funcDoTheThing(reallyDoItbool)(errerror) {     ifreallyDoIt {         result, err:= tryTheThing()         if err!= nil || result != "it worked" {            err = ErrDidNotWork         }     }    return err } functryTheThing()(string,error) {     return"",ErrDidNotWork } funcmain() {     fmt.Println(DoTheThing(true))     fmt.Println(DoTheThing(false)) } 解析 考点:变量作用域 因为 if 语句块内的 err 变量会遮罩函数作用域内的 err 变量,结果: 改为: func DoTheThing(reallyDoIt bool)(errerror) {     varresult string     ifreallyDoIt {         result, err =tryTheThing()         if err!= nil || result != "it worked" {            err = ErrDidNotWork         }     }    return err } 28.编译执行下面代码会出现什么? package main functest() []func() {     varfuns []func()     fori:=0;i<2;i++  {         funs = append(funs,func() {                       println(&i,i)         })     }    returnfuns } funcmain(){     funs:=test()            for_,f:=range funs{         f()     } } 解析 考点:闭包延迟求值 for循环复用局部变量i,每一次放入匿名函数的应用都是想一个变量。 结果: 0xc042046000 2 0xc042046000 2 如果想不一样可以改为: func test() []func()  {     varfuns []func()     fori:=0;i<2;i++  {         x:=i         funs = append(funs,func() {            println(&x,x)         })     }    returnfuns } 29.编译执行下面代码会出现什么? package main functest(x int)(func(),func()) {     returnfunc() {        println(x)     x+=10     }, func() {              println(x)     } } funcmain() {     a,b:=test(100)     a()     b() } 解析 考点:闭包引用相同变量* 结果: 100 110 30. 编译执行下面代码会出现什么? package main im port (   "fmt"     "reflect") funcmain1() {     deferfunc() {      iferr:=recover();err!=nil{           fmt.Println(err)        }else {           fmt.Println("fatal")        }     }()     deferfunc() {        panic("deferpanic")     }()     panic("panic") } funcmain() {     deferfunc() {        iferr:=recover();err!=nil{            fmt.Println("++++")            f:=err.(func()string)             fmt.Println(err,f(),reflect.TypeOf(err).Kind().String())         }else {            fmt.Println("fatal")         }     }()     deferfunc() {        panic(func()string {            return "defer panic"         })     }()     panic("panic") } 解析 考点:panic仅有最后一个可以被revover捕获 触发panic("panic")后顺序执行defer,但是defer中还有一个panic,所以覆盖了之前的panic("panic") 原文链接:https://blog.csdn.net/itcastcpp/article/details/80462619

剑曼红尘 2020-03-09 10:46:30 0 浏览量 回答数 0
阿里云大学 云服务器ECS com域名 网站域名whois查询 开发者平台 小程序定制 小程序开发 国内短信套餐包 开发者技术与产品 云数据库 图像识别 开发者问答 阿里云建站 阿里云备案 云市场 万网 阿里云帮助文档 免费套餐 开发者工具 企业信息查询 小程序开发制作 视频内容分析 企业网站制作 视频集锦 代理记账服务 2020阿里巴巴研发效能峰会 企业建站模板 云效成长地图 高端建站