Kubernetes 新手issue整理

捡几个新手 issue做个记录,也是k8s学习的一部分吧,看了这么多,心里也有底了。新手可以进入这个快速链接

附一、github的社交用语

在issue和pr里常看到一些缩写语,这里记录下来:

Prow 命令 角色限制 描述
/lgtm 任何人均可使用,但只有评阅人和批准人使用此命令的时候才会触发自动化操作 用来表明你已经完成 PR 的评阅并对其所作变更表示满意
/approve 批准人 批准某 PR 可以合并
/assign 评阅人或批准人 指派某人来评阅或批准某 PR
/close 评阅人或批准人 关闭 Issue 或 PR
/hold 任何人 添加 do-not-merge/hold 标签,用来表明 PR 不应被自动合并
/hold cancel 任何人 去掉 do-not-merge/hold 标签
  • PR:Pull Request.
  • AKA: also known as,又名
  • /assign <username> ,接下这个issue,我要承包了。
  • LGTM/SGTM, Looks/Sounds Good To Me, 看起来不错, 没有问题(别人 review 完 PR 之后)
  • WIP, Work In Progress, 开发中(work in progress, do not merge yet)
  • CC, Carbon Copy, 抄送(邮件),相当于 @。
  • PTAL, Please Take A Look, 帮我看下(请别人 review 自己的 PR)
  • RFC, Request For Comments, 请求评论, i.e. I think this is a good idea, lets discuss
  • AFAIK / AFAICT, As Far As I Know / Can Tell, 据我所知
  • IMHO, In My Humble Opinion, 以我的拙见(多用于邮件和网络)
  • FYI, For your information, 供你参考
  • AFK, Away From the Keyboard, 稍后回来
  • ACK, ACKnowledgement, 同意, i.e. agreed/accepted change
  • NACK/NAK, Negative ACKnowledgement, 不同意, i.e. disagree with change and/or concept

附二、github kubernetes 的label

https://kubernetes.io/zh/docs/contribute/review/for-approvers/

在issue的右上角可以看到这些label,每周都有一个特定的文档批准人自愿负责对 PR 进行分类和评阅。

标签 描述
priority/critical-urgent 应马上处理
priority/important-soon 应在 3 个月内处理
priority/important-longterm 应在 6 个月内处理
priority/backlog 可无限期地推迟,可在人手充足时处理
priority/awaiting-more-evidence 占位符,标示 Issue 可能是一个不错的 Issue,避免该 Issue 被忽略或遗忘
help or good first issue 适合对 Kubernetes 或 SIG Docs 经验较少的贡献者来处理。
  • good first issue: 新手友好的!
  • help wanted: 成员致力于提供新的贡献者提供额外援助。
  • kind
    • cleanup: 清理。
    • documentation
    • bug
  • lifecycle
    • stale: 过去 90 天内某 Issue 无人问津.
    • frozen: 即使超过 90 天仍无人处理,也不会进入停滞状态。
    • backlog
    • active
  • priority
  • sig
    • node
    • network
  • needs-triage
  • triage
    • accepted
    • duplicate: 重复的 Issue

附三、github kubernetes sigs


Envoy 创始人 Matt Klein 亲述开源心路历程 - Matt Klein

转自:https://mp.weixin.qq.com/s/EHPKFVedxE_--FAHAhvjXA

Image

本文译自 Envoy 代理的创始人 Matt Klein 在个人博客上发布的文章《5 year of Envoy OSS[1]》。他在 Twitter 因为自己的程序 bug 造成重大事故而离职,后加入 Lyft,在开源 Envoy 之前几乎没有贡献和管理开源项目的经验,这篇文章分享了他个人及 Envoy 开源的心路历程,在投身开源 Envoy 还是为雇主 Lyft 效命,该如何抉择?看完本文,相信对于开源项目的维护者、创业者及投资人都会大有收获。

今天是 Envoy Proxy 开源的 5 周年[2]。毫不夸张地说,在专业方面,过去的 5 年是一个史诗般的过山车,我的情绪介于兴奋、自豪、焦虑、尴尬、无聊、倦怠之间。我想分享一下这个项目的前传和历史,以及我在发展大型开源软件项目的过程中所学到的一些经验教训。

一、前传和历史

前传

除了一些小的弯路,我在技术行业二十年的职业生涯一直专注于底层系统:嵌入式系统,操作系统,虚拟化,文件系统,以及最近的分布式系统网络。我的分布式系统网络之旅始于 2010 年初在亚马逊,我有幸帮助开发了第一批高性能计算(HPC)EC2 实例类型。我学到了大量的底层高性能计算机网络知识,尽管我对分布式系统的概念接触有限。

2012 年,我加入了 Twitter,在经历了几次错误的开始后,我最终加入了边缘网络团队。这是我第一次真正接触到分布式系统应用网络概念。我领导了一个新的 HTTP 边缘代理的开发,称为 Twitter 流式聚合器(TSA),它在 2013 年首次推出,以扩大 Twitter 的 “firehose” API(流式所有推文)的交付。在 2014 年世界杯前夕,我们决定将 TSA 作为一个通用的 HTTP/HTTP2/TLS 边缘代理,在靠近巴西赛事的存在点(POPs)推出。这样做的主要原因是不可能在 POP 的少量主机托管机架上部署现有的基于 JVM 的资源匮乏的边缘代理。项目周期特别紧张,我的团队成功地完成了一届没有事故的世界杯。(我还清楚地记得有一段时间,当软件崩溃时,不管是什么时候,我都会给自己打上一页,修复错误,然后重新进行金丝雀部署,继续测试)。在 Twitter 工作期间,我还接触到了该公司通过 Finagle 库进行服务间网络通信的方式,并取得了巨大成功。

2015 年元旦前后,我在 Twitter 的日子里,因为我写的一个 bug,TSA 系统故障导致数百万 Twitter 的安卓用户被注销,这将是我在 Twitter 工作的尾声。

Image

加入 Lyft 和创建 “Lyft 代理”

我在 2015 年春天离开了 Twitter,部分原因是注销事件的影响,部分原因是对没有得到晋升的挫败感,部分原因是想尝试新的东西。我跟着我的老板从 Twitter 到了 Lyft,还有我在 Twitter 的其他同事。

当我加入 Lyft 时,公司规模相对较小(少于 100 名工程师),并且正在努力从单体架构迁移到微服务架构。我已经多次[3]谈到了 Envoy 的这部分历程,所以我不会再重述,在此简短的总结下,Lyft 遇到了所有典型的微服务迁移问题,主要是源于网络和可观察性。此外,Lyft 已经是 “多面手”(使用多种语言和框架),所以使用基于库的解决方案来解决这些问题似乎不切实际。因此,根据我以前建立 TSA 的经验和观察服务间通信在 Twitter 的工作方式,由于得到在 Lyft 的前 Twitter 同事们的信任,我提议建立一个新的应用网络系统,称为 “Lyft 代理”。

经过一些激烈的讨论,包括新的代理是否应该用 Python 构建(是的,真的),我们就项目的大致轮廓达成一致,并决定使用 C++ 作为实现语言。在当时,C++ 似乎是唯一合理的选择。今天我还会选择 C++ 吗?然而,如今已经不是 2015 年初了。

如果不说 “Envoy” 这个名字的由来,这部分的历史就不完整了。我们正在为这个项目建立最初的开发脚手架的时候,一个有远见的同事(Ryan Lane)说,我们不能把这个新项目叫做 “Lyft 代理”,我们必须选择一个更好的名字。我总是很实际,就去找辞典,查了一下 “代理”,然后决定用 Envoy 作为新名字。

在 Lyft 上线

直到 2015 年夏天,我才开始认真地研究 Envoy 的源代码。那几个月是我职业生涯中最有趣的几个月。我们应该珍惜这段初创时期,因为它不会持续很久。我花了很长时间,争取在合理的时间内(根据我的定义,这种类型的项目需要 3-4 个月的时间)做出能给 Lyft 带来价值的东西。俗话说,Lyft 给了我大量的绳子来吊死自己,而我致力于确保这种吊死不会发生。

当然,我的效率主要归功于刚从压缩的开发时间表和许多错误(主要是我自己的)中走出来,在 Twitter 的 TSA。我知道哪些错误是不能犯的,哪些抽象是需要的,哪些测试有效,哪些无效,等等。

2015 年秋天准备投入生产的 Envoy 的最初版本只包含了该项目今天所包含的功能和复杂性的一小部分。它不支持 TLS,只支持 HTTP/1,并且有极其简单的路由和弹性功能。它所拥有的是你今天所看到的东西的骨架。在这个项目的历史上,很少有重大的重构,主要是因为,正如我之前所说的,我知道将要发生什么,以及为了支持这些功能,需要有哪些抽象。Envoy 从一开始就拥有一流的可观察性输出,以指标和日志的形式。在 2021 年,这种类型的网络可观察性是桌面上的赌注(这在很大程度上要归功于 Envoy 的成功),但在当时却不是这样。

Envoy 最初是作为边缘代理在 Lyft 上线的,位于提供 TLS 终止的 AWS ELB 后面。到 2015 年秋末,Envoy 为 Lyft 的 100% 流量提供服务,该系统产生的边缘仪表盘立即得到了回报(例如,提供 API 调用百分点延迟直方图,每个终端的成功率和请求率等)。

在最初推出后不久,另一位 Twitter 同事(Bill Gallagher)加入了我的项目,我们迅速增加了一些功能,如 TLS 终止、HTTP/2 支持、更多路由和负载平衡功能等。

与此同时,Lyft 基于 Envoy 的“服务网格”也开始成形了。首先,Envoy 被部署在 PHP 单片机旁边,以取代 HAProxy 及其一些固有的运维问题(例如,当时 HAProxy 仍然是单线程的),以帮助 MongoDB 的代理。可以毫不夸张地说,Envoy 的早期开发有很大一部分是针对 MongoDB 的稳定性(负载均衡、速率限制、可观察性等)。

基于 Envoy 的边缘机群和单体之间的直接观察能力的好处是非常明显的。不久之后,我们在一些高 RPS 分解的微服务旁边部署了 Envoy,以帮助排除网络问题。这方面的价值也得到了证明。随着时间的推移,我们超越了对可观察性的关注,增加了帮助系统可靠性的功能,如直接连接和服务发现(跳过内部 ELB)、异常值检测、健康检查、重试、断路等。Lyft 的基于负载的重大事件的数量从每 1-2 周一次慢慢减少。当然,Envoy 不能将所有此类事件的减少归功于此,但它提供的网络抽象确实有很大的帮助。

2016 年初,我们决定推动一个 100% 覆盖的服务网格。最初,我们认为这将是一个艰难的过程,需要自上而下的授权。在实践中,团队报名参加了迁移,因为他们将得到的好处是显而易见的。“胡萝卜”式的迁移几乎总是成功的。而“大棒”式的迁移则很少成功,或者即使成功了,也会在组织内留下眼泪和愤怒。

到 2016 年中期,Envoy 被用于 Lyft 的所有网络通信,包括边缘服务、服务间通信、数据库、外部合作伙伴等。无论从哪个角度来看,该项目都取得了巨大的成功,帮助 Lyft 完成了微服务的迁移,提高了整体的可靠性,并对网络进行了抽象,使大多数工程师不需要了解真实的系统拓扑结构。此后,Bill 离开了这个项目,在 Lyft 从事其他工作,接替他的是 Roman Dzhabarov 和 Constance Caramanolis 加入我的团队。我们的小团队为整个 Lyft 开发和运维 Envoy。

开放源码

到 2016 年夏天,我们开始认真讨论开源 Envoy 的问题。早期的 Lyft 员工对开源和它为公司所做的事情很欣赏。很明显,Envoy 并不是 Lyft 的主要业务,那么为什么不把它放在那里并给予回报呢?我可以坦率地说,我们都带着不同的目标和期望来对待开放源代码的过程,以及对项目获得巨大成功后会发生什么感到非常天真。

在加入 Envoy 之前,我已经使用了相当多的开源软件,但我几乎没有开源贡献的经验,也没有维护者的经验。(虽然我在 Linux 内核中有过一次提交[4]!)开源 Envoy 似乎是一个很好的机会,可以扩展我的技能组合,学习新的东西,可能会促进我的职业生涯,坦率地说,我不希望有一个 TSA v3 在第三家公司出现。对于 Lyft 来说,Envoy 是一个重要的工程项目,领导层认为,开放源代码将使 Lyft 作为一个工程组织具有可信度,并有助于招聘工作。正如我之前所说,我们所有人都对创建成功的开源,更重要的是在它获得成功的情况下培育它所需要的东西感到天真。

但是,我们决定给它一个机会。我们在 2016 年夏天花了很大一部分时间来编写文档(Jose Nino 在这个时候加入了团队,他的第一个任务就是阅读并帮助改进所有的文档),清理存储库,使其“不那么尴尬”,制作网站,发布博文等等。我真的很感谢这段时间里我在 Lyft 的同事,他们不仅支持我们,还帮助我们完成了无数的任务,包括网站设计、logo 等等。即使在这个早期阶段,我们也觉得第一印象很重要,如果我们要在开源领域有所作为,就必须通过高质量的文档、网站等给人留下良好的第一印象。

在此期间,我们还利用我们的行业关系,与 Lyft 的一些“同行公司”(湾区的“独角兽”互联网创业公司)会面,向他们展示我们在 Envoy 方面所做的工作,并获得他们的反馈,我们认为如果我们在正式开源前成功获得一个启动合作伙伴,这将是对项目的一个重大帮助。所有这些会议都非常友好,总的来说,所有与我们会面的公司都对我们所取得的成就印象深刻。但是,事后看来,他们都表示,以他们的小型基础设施团队,不可能马上采用 Envoy。他们祝愿我们在开放源代码方面取得最好的成绩,并说他们以后会回来看看。我们不禁对这些会议的结果感到沮丧,但我们还是向前推进了。

2015 年 8 月,我与谷歌进行了第一次友好的会面。一个 Lyft 的同事(Chris Burnett)在一个 gRPC 聚会上发言,提到了 Envoy,因为它与 Envoy 的 gRPC 桥接[5]支持有关。我不知道的是,谷歌在发现 Envoy 的时候,正准备在 NGINX 的基础上推出 Istio。一次会议引出了另一次会议,然后是更多的会议,在 Envoy 开源之前,大量的谷歌员工已经看到了源代码和文档。(稍后会有更多关于这方面的内容)。

到 9 月初,我们已经准备好了,并将开源日定为 9 月 14 日。总的来说,我是一个(过度?)自信的人,但在我的生活中,有几次我对自己成功的能力有很大的焦虑。我立即想到的是:开始上高中,开始上大学,以及大学毕业后在微软工作。而开源的 Envoy 就是其中之一。我记得我被公众的反应吓坏了。人们会怎么说?反馈会是积极的还是恶毒的?虽然我们在开源时是一个小团队,但我仍然写了 90% 或更多的代码,并且觉得把它放到公共领域是对我自己和我的能力的一种反映。

如期而至,Envoy 在 2016 年 9 月 14 日 成为开源产品。我记得我和妻子一起庆祝,并说了一些话。“如果我们能让其他公司像 Lyft 一样使用 Envoy,我就会很高兴。”

对开放源码发布的反应几乎是普遍的积极。令我们惊讶的是,几乎是立刻,我们开始听到大公司的声音,而不是小公司。在几周内,我们与苹果、微软进行了交谈,与谷歌的对话也不断加快。大公司在现有的解决方案中存在问题,并且有大量的团队准备投入到解决这些问题的工作中。具有讽刺意味的是(至少在 Twitter 的观点中),C++ 在这里是一种帮助,而不是一种阻碍。这些大公司都已经拥有充足的 C/C++ 开发资源,以及他们想要整合的现有库,等等。对他们来说,C++ 是一个卖点。

在这段时间里,毫不奇怪,我们与谷歌的人有最多的互动。最初主要是构建 Istio 的团队,但渐渐地,我们与 Anna Berenberg 花了更多时间,她现在是谷歌的杰出工程师,领导各种网络和负载均衡工作。这种关系将产生“喷气燃料”,在 2017 年初真正启动该项目。

开始起飞

到了 2017 年初,很明显,Envoy 的开发正在加速。谷歌承诺用 Envoy 取代 NGINX,用于 Istio(最终在 2017 年春季推出),对项目的未来来说更重要的是,Anna 的大型团队致力于 GCP 云负载均衡功能,他们开始向使用 Envoy 的各种云负载均衡产品以及内部用例(这在这个时期都是非常秘密的,但现在已经众所周知)。

我将永远记得与谷歌互动的那段时间是我职业生涯中最紧张的时期之一。说实话,那感觉就像一个收购(审讯)过程。我记得长长的会议和电子邮件线程,以证明我们的技术决定,“面试”中,谷歌试图确定我们是否会成为一个好的开源项目合作伙伴,等等。当时我们很痛苦地发现,这次“收购”将使 Envoy 进入一个我们自己永远无法实现的轨道,所以我们尽一切努力使它获得成功,最终也获得了成功。而且,在过去 4 年多的时间里,我们与谷歌的合作确实是一种杰出的伙伴关系。早期的谷歌云工程师最终成为维护者,Harvey Tuch 和 Alyssa Wilk,为项目带来了大量的人才,包括技术上的,以及对开源和社区的支持。我对他们的感激之情溢于言表,没有他们,项目就不会有今天的成就。多年来为该项目做出贡献的其他谷歌工程师(现在有很多),除了普遍是优秀的社区管理者之外,还为该项目增加了大量的工程力量,否则该项目就不会有。我当然对最初的谷歌合作关系有顾虑(技术和理念上的分歧,等等),但我可以诚实地说,这些顾虑都没有成为现实。

除了确保 Istio 和 GCP 团队与谷歌合作的成功之外,我们还花了大量时间与其他公司和维护者合作并加入他们,其中许多人对项目产生了巨大的影响,至今仍作为维护者、贡献者或用户大量参与。如果没有这些早期的社区成员,这个项目就不会有今天,我也非常感谢他们对项目的信任。

同时,随着项目的不断深入,我开始收到大量投资者对 Enovy 的兴趣。有强烈的愿望让我离开 Lyft,围绕这个项目开一家公司。我写过这部分的旅程[6],所以我不会在这里重述,留在 Lyft 我会有大量的时间和精力来处理所有这些互动。正如链接的文章所描述的,我最终决定留在 Lyft,不开公司,以支持 Envoy 的持续成功。

与此同时,我仍然在 Lyft 工作,正如我将在后面进一步讨论的那样,我越来越多地从事两份工作。我的第一份工作是在内部领导网络团队,并在运营上支持 Lyft 的 Envoy。我的第二份工作是作为 Envoy 的公众形象,包括 OSS 领导,代码审查,修复错误,编写可以促进项目的功能,在会议上发言,帮助其他公司采用和部署 Envoy,等等。我开始变得过于分散,并出现了倦怠的迹象。然而,到了 2017 年年中,不可否认的事实是,Envoy 的发展轨迹是大大的“向上和向右”。各大公司、“同行公司”、垂直产品和服务等的采用率继续攀升。

捐赠给 CNCF 且感到倦怠

到 2017 年秋天,有两件事是清楚的。

  1. Envoy 已经超出了 Lyft OSS 设备所能提供的范围。该项目需要法律、公共关系、营销、活动组织等方面的帮助。
  2. 我很快就完全倦怠了,需要找出一条可持续发展的道路。

为了解决第一点,我们最终同意考虑将 Envoy 转交到 CNCF。数月来,CNCF 一直在追求该项目,但似乎从来没有任何令人信服的理由来加入。到 2017 年底,很明显,CNCF 的资源即使不是净收益,也至少对项目是中性的。我们开始了提交程序,并最终在我们最初开放项目资源的几乎整整一年后加入了该基金会[7]。我很感谢 Alexis Richardson 和 Chris Aniszczyk 在这个过程中对项目的指导。

第二点则要复杂得多。从根本上说,我的工作时间超过了我的工作能力,有效地跨越了两个不同的工作。此外,我正在期待我的第一个孩子,预产期在 2018 年初,随着到来的日期越来越近,这让我越来越焦虑。到这个时候,我已经很清楚,我在设定期望和界限方面做得不够好,不知道自己能够为 Lyft 提供什么,同时也没有从行业的角度关注 Envoy 的持续增长。在 Lyft,我越来越放任自流,陷入人际关系的争吵,在为更多的初级团队成员提供指导和领导方面,没有达到我这个级别的期望。

简而言之,我当时正处于崩溃的边缘,最终我选择了 Envoy 而不是 Lyft,这对我的 Lyft 同事造成了伤害。我想,如果我在 2017 年初至年中对我的工作量与 Lyft 的领导层更加透明,我可能会避免一些最糟糕的结果,但不幸的现实是,为开源软件行业的工作提供资源,而这些工作对雇主没有立即明显的作用,这是一个复杂的努力。它可能会更顺利,也可能不会。在任何情况下,虽然我对一些我本可以处理得更好的人际关系问题感到遗憾,但无论好坏,我都不后悔把精力放在 Envoy 上。我优先考虑的是 Envoy,而不是 Lyft,我做了我认为当时必须做的事情,以使它成功。

可持续发展道路

我的第一个孩子在 2018 年 2 月出生,Lyft 极其慷慨的陪产假政策为我提供了休息和放空自己的时间。我从 Lyft 获得了一些空间,并开始更深入地思考我想要什么以及什么对我来说是可持续的。

当我休完陪产假回来后,我与 Lyft 领导层明确表示,我不能再参与 Lyft 的 Envoy 的“日常”运维。相反,由于 2017 年底的一些后果,基础设施团队也希望与我分开一些。由于这个原因,我大幅后退,实际上完全停止了在 Lyft 的基础设施工作,在 2018 年年中至年末的 Lyft Bikes 和 Scooters 初始版本中编写固件网络代码。这是一个了不起的团队努力,在压缩的时间范围内得到了一些东西,我真的很喜欢在几个月内做一些完全不同的事情。

2018 年也是我积极开始琢磨在 Envoy OSS 社区中“取代自己”的那一年。我花了大量的时间(并将继续花大量的时间)来培养维护者、新的贡献者,组织第一次专门的 EnvoyCon,等等。任何领导者都应该有一个目标,那就是确保该组织在有一天该领导者退位时能够继续良好地运作。

到 2018 年底,我的主要职业倦怠风险已经得到解决,我又开始了合理的工作时间,并花了很多时间与我的妻子和儿子在一起,我的时间大致在 Envoy OSS 工作和 Lyft 的一般基础设施领导之间各占一半。明确地说,Envoy 的成功带来的特权使我能够在 Lyft 的工作生活中取得这种平衡。随着时间的推移,随着我的行业地位的提高,我的影响力也在同步增加,这使得我更容易按照自己的意愿设定就业条款。没有多少人有这样的运气,我明白我是多么幸运,能够“突破”倦怠墙的另一边而不必离开我的工作。

Envoy 长大了

自 2019 年以来,因为新冠疫情,我在 Lyft 的基础设施领导和 OSS 领导之间继续保持着我上面描述的五五开的比例。当然也有单调和渴望不同的时候(从历史上看,我是一个习惯性换工作的人,6.5 年是迄今为止我在一件事情上工作的最长时间),但总的来说,我很高兴看到 Envoy 从一个“新秀”变成更多的“少年”。我不再专注于做我所能想到的一切,使 Envoy 获得巨大的成功,因为坦率地说,Envoy 是一个巨大的成功,已经席卷了市场,并改变了用户对应用负载均衡工具的期望。相反,我更关注项目的可持续性。我们是在做长期的工作,这些天我觉得自己更像一个 CEO,看减员人数、优先级、预算编制、安全问题等等。这并不是说这不是有用的工作;它显然是有用的,它只是与早期的工作不同,早期的工作技术性更强,节奏更快。

截止到 2021 年末,我对 Envoy 最引以为豪的事情是,在我看来,这个社区已经可以自我维持了。我们有一群令人难以置信的维护者、贡献者和用户,他们对项目的成功充满热情,并在使 Envoy 成为今天的样子中发挥了作用。这确实是一个团队的努力。

二、经验教训

过去的 5 年是一个史诗般的旅程。虽然我觉得我在技术上学到的东西相对较少,但我在领导力、社区建设和所有其他非技术性的东西方面都得到了成长和学习,这些都是建立一个成功的企业,无论是企业还是一个主要的开源成功故事。以下是我对一些主要学习内容的简短总结。

成功的开源软件就像创办一个企业

也许有争议的是,我认为如果一个人有目标要创建一个非常成功的开源软件项目,他们需要把它想成一个企业。除了核心技术之外,创业还涉及很多因素:

  • 招聘(在开源软件中,这意味着招聘贡献者和维护者)
  • 获取客户(在开源软件中,这被转化为用户)
  • 文档和技术写作
  • 公共关系
  • 市场营销
  • 法律(商标、许可等)
  • 人力资源(在开源软件中,这将转化为解决社区纠纷和制定文化)
  • 资金(在开源软件中,这转化为辅助费用,如 CI、为维护者找到允许他们在项目中部分或全部时间工作的工作,等等)
  • 总的说来,就是领导和方向的确定。资源有限,有很多事情可以做。企业/项目需要专注于最重要的事情,以实现产品的市场适应性。

直观地说,我知道这一点,在最初为 Envoy 进行开源努力时,我积极地追求上述所有的领域,努力使项目从开始发展到今天的规模。上述列表中的每一项都很关键,如果没有所有这些,一个项目是不可能成功的,尤其是在技术领域有很多资金雄厚的公司竞争对手的情况下。

我强烈鼓励那些考虑进行大规模开源工作的人提前在上述领域进行投资,以便在第一天就给人留下最佳印象。此外,新的开源项目应该准备在项目成长并开始看到采用时,在上述领域进行更多的投资。

毫不奇怪,这些天我在 Envoy 上做的编码工作相对较少。我在项目上的时间主要是管理项目的所有非技术方面(上述列表中的所有内容,甚至更多!),并确保事情按计划进行。我所做的大多数编码项目都是“清洁”的幕后项目,对项目有好处,但没有什么乐趣,也不可能激励其他贡献者(当然,我对他们每天的工作没有发言权,我有动力让他们尽可能的开心,这样他们就不会离开)。

终端用户驱动的开源软件是一种结构性优势

这些天来,很多“大的开源软件”,特别是在基础设施领域,是由大公司和风险投资支持的初创公司资助的。我不会绕到关于开源软件的困难经济的讨论,因为我已经写过了[8]。我想说的是, 我坚信终端用户的开源软件比企业和风险投资支持的开源软件有很大的优势:最初的客户几乎肯定会从软件中获得价值,否则软件就不会得到资助。这种与客户一起建立东西的良性循环是非常强大的。它几乎普遍导致了更好的结果:软件更可靠、更专注、功能更少。有很多由最终用户驱动的开源软件的例子,然后取得了巨大的商业成功。鉴于坚实的基础和内在的产品市场适应性,这对我来说并不奇怪。我希望看到比今天更多的最终用户驱动的开源软件,尽管我认识到经济上是困难的。对于那些有机会的人来说,请向这种类型的软件所具有的结构性优势靠拢!

不要跟风,要跟随客户

这也许是“成功的开源软件就像创业”和“最终用户驱动的开源软件是一种结构性优势”的必然结果,但我无法强调坚持不懈地关注客户的实际需求而不是炒作周期所认为的客户需求是多么关键。例如,多年来,人们一直在嘲笑 Envoy 是用 C++ 编写的,这引起了无数的笑话。我喜欢 C++ 吗?不,不是很喜欢。它是否在 2015 年完成了工作,并吸引了最初的一批主要用户?肯定是的。这是一个关注客户和市场的例子,而不是屈服于没有实际“商业”影响的炒作。如果一个人把开源软件当做一个企业,就会立刻明白,以客户和市场为中心是取得巨大成功的唯一途径。在 Envoy,我花了大量的时间为终端用户争论,以确保我们建立的东西能让所有人受益,而不仅仅是一小部分小众用户。

可扩展性是至关重要的

跟着客户走往往会导致客户的要求不能很好地融入项目的架构中。从开源软件的角度来看,失去对项目主要目标的关注会导致功能蔓延、软件无法维护和维护人员负担过重。同时,说“不”也是失去潜在用户的一个保证。

对于 Envoy,我想确保我们至少可以说“是的,但是……”,即提供一个强大的可扩展性模型,让用户可以满足他们的需求,而不需要将每一个改动和功能都推到上游。这种策略已经多次得到回报,它减轻了维护者的负担,让用户能够解决他们自己的问题,更重要的是,将 Envoy 推向了我在最初设计该软件时从未想象过的用例。

可扩展性,特别是对于开源软件的构建模块,是至关重要的。

质量问题

跟随客户的另一个推论是,质量确实很重要。用户希望软件易于操作,相对来说没有错误,关心安全,等等。曾几何时很多人会觉得因为开源软件是“免费的”,所以质量就得不到保证。这在理论上也许是正确的,但实际上,在一个项目对软件质量认真对待之前,用户不会大量地聚集在一个软件上。因为获得用户是一个飞轮,可以获得更多的用户(特别是当从早期采用者转向晚期采用者时),所以确保为整个软件质量编列时间预算就更加关键了。

关于 Envoy,我一直有一个“零碰撞”的理念。任何崩溃都会被调查和修复,无论多么不频繁的错误。这种对稳定性和质量的关注不会被忽视。

社区是扩大规模的唯一途径

这很明显,但我还是要说:社区是扩展开源软件的唯一途径。这是一个由维护者、贡献者和用户组成的社区。此外,社区的基调在项目开始时就已经确定,而且极难改变。人类倾向于遵循规范。一旦规范被确定下来,无论规范是什么,与这些规范不一致的人都会被避开。因此,项目最初的公共基调对于设定其长期的社区轨迹极为关键。

当我们把 Envoy 做成开源软件时,我在 GitHub 上投入了大量的精力与人们一起工作,使用建设性和欢迎性的语言。总的来说,我尽我所能让 Envoy 成为一个受欢迎的地方,让人们愿意来贡献自己的力量,无论是维护、偶尔的贡献,还是用户帮助其他用户。

在 Envoy 所取得的所有不同类型的成功中,到目前为止,给我带来最多个人满足感的部分是,有相当多的人告诉我,他们已经发誓不再使用开源软件,尤其是基础设施开源软件,因为他们觉得大多数项目中的人对彼此感觉都很糟糕。相反的,他们喜欢为 Envoy 做贡献,因为这个社区是如此的尊重和欢迎彼此。这需要大量的努力和纪律,尤其是在项目的早期,才能达到这样的结果,而这已经得到了众多的回报。

不要低估从一开始就确定项目的文化和基调的复合效应。

混合商业和开源软件的利益是非常困难的

已经有很多关于开源软件的困难经济学的文章(包括我自己的文章,我在上面提到的)。我只想说, 试图将商业上的成功和开放源码的成功结合起来是非常困难的,主要是因为这些成功往往是相互矛盾的。

我相信,Envoy 通过其强大的 API 和可扩展性系统穿透了这个矛盾。从本质上讲,Envoy 已经成为一个工具,现在被大量的垂直产品和服务所使用。这就产生了一个社区,该社区充满了选择在一个共同的基底上合作的公司,即使是通过在扩展/API/控制平面/UI/UX层上的创新,推出相互竞争的上层产品。

任何成功的开源项目都会看到大量的商业/投资人的兴趣。如果一个项目的目标是保持一个充满活力的社区,同时又能取得商业上的成功(我认为这对整个项目的成功是必要的,因为钱必须来自某处),那么预先考虑如何将核心层和商业层分开是极其重要的。这样做的实用性和策略会因项目和技术的不同而不同,但我相信专注于强大的 API/扩展性的分割是一个富有成效的策略。

基金会是很棘手的

在现代的开源讨论中,有很多关于基金会的作用的讨论。我不打算对这一话题做大量的评论,但我的主要建议是不要被基金会和它们可能提供的理论利益所干扰。相反,要积极地关注产品的市场适应性,生产高质量的软件,并为用户提供价值。如果这些事情得以实现,其余的事情就会自然而然地发生。

对于非常成功的项目来说,基金会,更确切地说,中立的商标持有地,是非常有用的,所以我肯定会在那个时候考虑加入一个。随着项目的成熟,Envoy 从成为 CNCF 的一部分所获得的价值也在不断增加。CNCF 雇佣了开源软件律师、营销人员、公共关系人员、一流的活动人员等等。这些额外的资源在“经营业务”方面是非常宝贵的。

提前考虑治理问题

Image

开源治理是非常困难的。就其本质而言,开放源代码是无政府的,没有明确的领导结构。没有一个适合所有项目的治理方法,每个项目都必须找到自己的前进方向,可以通过“BDFL”/CEO 类型的模式、指导委员会、类似 Apache PMC 的程序等。所有的治理模式都有优点和缺点,并且有不同的失败模式。

最重要的是,在项目变得庞大和成功之前,先认真思考治理问题。写下一套规则和规范,特别是花时间记录项目的冲突解决过程。

同时也要意识到,根据我在上面关于社区规范如何在早期设定的评论,早期的项目维护者将对整个对话和冲突解决的风格产生巨大的影响,就像公司的早期员工对公司的文化产生巨大的影响一样。

在我的印象中,我们在 Envoy 内部非常幸运,没有发生过任何重大分歧,出现的问题也可以迅速友好地解决。在项目的历史上,我们从来没有需要援引维护者投票程序来解决冲突[9]。在我看来,这是一个巨大的成就,也是对所有维护者的素质和专业性的证明,尤其是考虑到该项目已经变得如此受欢迎,以及围绕它的所有商业利益。

对开源贡献的期望是至关重要的

我在上面提到过这个问题,但我自己的职业倦怠很大程度上是由于我没有很好地与我的雇主就我需要花多少时间来管理 Envoy 的开源增长设定合理的期望。我不会撒谎说,进行这样的对话就能神奇地使雇主为某人腾出大量时间来从事开源工作,特别是那些可能不直接适用于其日常工作的项目。话虽如此,我确信对所有参与的人来说,对开源过程有公开和诚实的期望是非常重要的。以下是在开源项目之前或开始以开源身份工作之前要问的合理问题:

  • 雇员应该问他们的雇主,为什么他们要开放源代码?
  • 雇主应该问他们的员工,为什么他们要开放源代码?(这个问题的答案和前一个问题的答案不同是完全合理的,但应该在公开场合讨论)。
  • 雇员应该问他们的雇主,如果项目成功了,会发生什么?该项目将有哪些资源可用?员工将有多少时间可以在通用的开源软件问题上工作,目的是直接推动项目的发展?

雇主和雇员之间不匹配的期望是未来怨恨和倦怠的根本原因。

代理容易,API 难

对一些人来说,Envoy 提供的底层网络代理机制似乎是这个项目的复杂部分。事实证明,与为 Envoy 发展一个稳定的 API 生态系统所做的工作相比,代理部分(在我看来)相对简单。平衡人类和计算机消费的 API 人体工程学,保持不同版本的稳定性,发展 API 以支持其他客户端,如 gRPC,指定协议语义以使 Envoy 能够与数百(可能是数千)个不同的管理服务器对话,等等,都是非常复杂的。我为团队在这一领域取得的成就感到骄傲(特别要感谢推动这一工作的 Harvey),即使在这一过程中出现了一些错误(比如从 API 的 V2 版本强制迁移到 V3 版本)。

如果一个软件提供了一个 API,而且更重要的是希望这个 API 成为其他系统的关键组件,那么不要低估提供一个稳定和符合人体工程学的 API 的成本和复杂性。反过来说,强大的 API 是一个生态系统飞轮的重要组成部分,会以此产生更多的产品和用户,所以在我看来,这些努力是非常值得的。

不要忽视职业倦怠

如果一个人想成就大事业,我不相信他可以 100% 实现良好的工作生活平衡。现实情况是,任何成功都是由现有的特权/机会、一个好的想法、良好的执行力和大量的运气(包括在正确的时间出现在正确的地点)组成的。所有这些东西都在 Envoy 中发挥作用,我不会假装我没有把自己搞得很累,特别是在 2017 年。我也会重新做一遍 2017 年的工作,因为从我的角度来看,我做了我必须做的事情,使项目获得成功。(有时我想,如果我已经有了孩子,Enovy 是否还会诞生。我不确定它是否会发生,但这是一个更长的谈话主题!)

综上所述,我在 2017 年描述的那种史诗般的推动力只能持续这么久,直到一个人崩溃。我鼓励大家不断反思自己的工作生活平衡,并为自己找出一条可持续发展的道路。每个人的情况都不同,我不能提供任何一个避免职业倦怠的建议,但我认为反思是一个好的开始,也是我自己不得不努力的事情。

三、感谢

在过去 6 年半的时间里,在 Envoy 上工作,其中 5 年是作为开源软件,这是我职业生涯中的亮点。这个项目的成功确实是一个团队的努力,我一个人是不可能完成的,我为我们所有人(维护者、贡献者和用户)共同完成的事情感到非常自豪。在这个项目上工作的维护者和贡献者是我所共事过的最好的工程师群体,他们才华横溢,他们就职在不同公司,位于不同的地理位置,这真是开源的理论潜力在实践中的体现。作为一个团队,我们已经产生了世界性的影响,改变了用户对软件负载均衡系统的期望,同时也建立了一个充满活力和热情的社区。在我最疯狂的梦想中,我从未想过这个项目会成为今天的样子。

对我来说,未来会发生什么就不那么清楚了。正如我上面所说的,我的重点已经转移到了可持续性上。我想确保,如果有一天我离开了,这个项目将保持健康。尽管如此,这一天还没有到来,我期待着在可预见的未来帮助领导项目前进,希望能取得更大的成功和采用。向前迈进!

相关链接:

  1. https://mattklein123.dev/2021/09/14/5-years-envoy-oss/
  2. https://eng.lyft.com/announcing-envoy-c-l7-proxy-and-communication-bus-92520b6c8191?gi=a32d29446574
  3. https://mattklein123.dev/appearances/
  4. https://github.com/torvalds/linux/commit/00370b8f8dd6e3171b8202f9c5187a5f73e99497
  5. https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/other_protocols/grpc#grpc-bridging
  6. https://medium.com/@mattklein123/optimizing-impact-why-i-will-not-start-an-envoy-platform-company-8904286658cb
  7. https://eng.lyft.com/envoy-joins-the-cncf-dc18baefbc22
  8. https://medium.com/@mattklein123/the-broken-economics-of-oss-5a1b31fc0182
  9. https://github.com/envoyproxy/envoy/blob/main/GOVERNANCE.md#conflict-resolution-and-voting

文章来源:Cloud Native Community


kubernetes 开发环境搭建与社区贡献

一、go 环境搭建

1.22.1 升级日志 更新来看,使用了 Golang 1.16.7 的环境。

1.1 备份当前的 go 环境

先确认系统路径PATH 有没有包含 goroot,没有的话加上。一般如果已经存在了都会有的:

image-20210831104549671

image-20210831104135117

可以看到我这里系统路径已经包含了goroot和gopath的路径。

主要思路是把 goroot / gopath 做备份,保留环境变量不变,将它们文件和文件夹重命名加个后缀版本号,再在用软链接 ln -s 指向它们。

image-20210831104344545

1.2 安装 Golang 1.16.7

我是 debian 9 x86 环境。

# 也可以直接访问 https://golang.org/dl/ 手动下载
wget https://golang.org/dl/go1.16.7.linux-amd64.tar.gz   

tar zxvf go1.16.7.linux-amd64.tar.gz
mv go /var/local/go1.16.7    # 我习惯使用/var/local保存自己安装的软件

# 该整的软链接整一下
rm /var/local/go /home/kelu/Workspace/go
ln -s /var/local/go1.16.7 /var/local/go
ls -s /home/kelu/Workspace/gopath/go1.16.7 /home/kelu/Workspace/go

image-20210831104344545

确认安装ok

go version

二、kubernetes 源码编译

官方开发参考文档: https://github.com/kubernetes/community/blob/master/contributors/devel/development.md

2.1 下载源码

直奔 https://github.com/kubernetes/kubernetes/releases/tag/v1.22.1

2.2 其它环境准备

  • 系统配置,准备好 8G内存和50G存储。
  • GNU development tools:
sudo apt update
sudo apt install build-essential
  • docker
  • rsync, 文件同步,一般都装了的。
  • jq, JSON 处理包。apt-get install jq. 官方文档
  • gcloud,如果构建 e2e 测试,需要安装GCP的 CLI
  • go
  • pyyaml,某些测试用,pip install pyyaml 官方文档
  • etcd,这个版本是 3.5.0,新版本改进了安全性、性能、监控和开发人员体验。./hack/install-etcd.sh

2.3 尝试编译

使用 kubernetes自带的 Makefile,使用make即可编译。可以通过查看Makefile文件代码,查看编译执行脚本。

也可以对不同的模块可以进行单独的编译。

2.3.1 round 1

在docker中执行跨平台编译出二进制文件。

./build/run.sh make

image-20210831113539036

发现 docker pull 阶段没法拉镜像,需要添加代理:

systemctl cat docker

# /lib/systemd/system/docker.service

修改此文件: vi /lib/systemd/system/docker.service

[Service]
Environment="HTTP_PROXY=http://proxy.example.com:8080/"
Environment="HTTPS_PROXY=http://proxy.example.com:8080/"
Environment="NO_PROXY=localhost,127.0.0.1,.example.com"

如果是 sock5 代理,把 http:// 改成 socks5:// 即可.

重启docker:

docker pull k8s.gcr.io/v2/build-image/kube-cross/manifests/v1.16.7-1

image-20210831115325630

下载有点久阿,要有亿点耐心。

image-20210831120049440

2.3.2 round 2

继续上路:

./build/run.sh make

image-20210831121901677

编译完成的二进制文件在/_output目录下。

image-20210831125906228

2.3.3 round 3 构建单个/跨平台构建

除了使用上边的 ./build/run.sh make,还可以直接使用make命令进行编译,编译出来的目录稍有不同:

image-20210910144957207

image-20210910145024523

make WHAT=cmd/kubectl
make all     # 编译所有
make cross   # 跨平台编译
make cross KUBE_BUILD_PLATFORMS=windows/amd64 # 特定平台编译
make help # 编译帮助

# 如果需要使用dlv进行远程调试,make需要添加一些参数,使得我们可以dlv attach进来:
make WHAT=cmd/kube-apiserver GOGCFLAGS="-N -l" GOLDFLAGS=""

image-20210831142728820

image-20210831145559472

2.3.4 构建镜像
KUBE_BUILD_PLATFORMS=linux/amd64 KUBE_BUILD_CONFORMANCE=n KUBE_BUILD_HYPERKUBE=n make release-images GOFLAGS=-v GOGCFLAGS="-N -l"
  • UBE_BUILD_CONFORMANCE=nKUBE_BUILD_HYPERKUBE=n 参数配置是否构建 hyperkube-amd64conformance-amd64 镜像,默认是 y 构建,设置为 n 不需要构建。
  • make release-images 表示执行编译并生成镜像 tar 包。

编译的 kubernetes 组件 docker 镜像以 tar 包的形式发布在 kubernetes/_output/release-images/amd64 目录中。

image-20210909181021179

image-20210909181143418

三、kubernetes 开发

3.1 github开发流

https://github.com/kubernetes/community/blob/master/contributors/guide/github-workflow.md

git_workflow.jpg

3.2 开发环境搭建

我使用 vscode 作为 ide

# 注意本地环境要做好国外代理,有时候证书工具下载不下来。
./hack/install-etcd.sh
./hack/local-up-cluster.sh

image-20210923143956075

# 打开新的terminal
cd $GOPATH/src/k8s.io/kubernetes
export KUBECONFIG=/var/run/kubernetes/admin.kubeconfig

# 使用kubectl
cluster/kubectl.sh get cs
cluster/kubectl.sh get ns
cluster/kubectl.sh get nodes
cluster/kubectl.sh run nginx --image=nginx
cluster/kubectl.sh get po -A

image-20210923143911863

这样一个简易的本地环境就起来了。

image-20210924111215107

一共运行了 7 个进程:

  • kube-apiserver
  • kube-controller-manager
  • kube-scheduler
  • sudo kubelet
  • kubelet
  • sudo kube-proxy
  • kube-proxy

image-20210923151337473

下面以调试 apiserver 作为例子:

# kill 掉 apiserver, 注意保存apiserver的启动命令。
ps aux | grep apiserver
kill -9 xxx

# 运行命令备忘:
/var/local/go/src/k8s.io/kubernetes/_output/local/bin/linux/amd64/kube-apiserver --authorization-mode=Node,RBAC  --cloud-provider= --cloud-config=   --v=3 --vmodule= --audit-policy-file=/tmp/kube-audit-policy-file --audit-log-path=/tmp/kube-apiserver-audit.log --authorization-webhook-config-file= --authentication-token-webhook-config-file= --cert-dir=/var/run/kubernetes --egress-selector-config-file=/tmp/kube_egress_selector_configuration.yaml --client-ca-file=/var/run/kubernetes/client-ca.crt --kubelet-client-certificate=/var/run/kubernetes/client-kube-apiserver.crt --kubelet-client-key=/var/run/kubernetes/client-kube-apiserver.key --service-account-key-file=/tmp/kube-serviceaccount.key --service-account-lookup=true --service-account-issuer=https://kubernetes.default.svc --service-account-jwks-uri=https://kubernetes.default.svc/openid/v1/jwks --service-account-signing-key-file=/tmp/kube-serviceaccount.key --enable-admission-plugins=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,Priority,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota --disable-admission-plugins= --admission-control-config-file= --bind-address=0.0.0.0 --secure-port=6443 --tls-cert-file=/var/run/kubernetes/serving-kube-apiserver.crt --tls-private-key-file=/var/run/kubernetes/serving-kube-apiserver.key --storage-backend=etcd3 --storage-media-type=application/vnd.kubernetes.protobuf --etcd-servers=http://127.0.0.1:2379 --service-cluster-ip-range=10.0.0.0/24 --feature-gates=AllAlpha=false --external-hostname=localhost --requestheader-username-headers=X-Remote-User --requestheader-group-headers=X-Remote-Group --requestheader-extra-headers-prefix=X-Remote-Extra- --requestheader-client-ca-file=/var/run/kubernetes/request-header-ca.crt --requestheader-allowed-names=system:auth-proxy --proxy-client-cert-file=/var/run/kubernetes/client-auth-proxy.crt --proxy-client-key-file=/var/run/kubernetes/client-auth-proxy.key --cors-allowed-origins="/127.0.0.1(:[0-9]+)?$,/localhost(:[0-9]+)?$"

编译apiserver:

make WHAT=cmd/kube-apiserver GOGCFLAGS="-N -l" GOLDFLAGS=""

可以看到是刚刚编译的apiserver,时间稍微落后一点:

image-20210923152019828

运行命令:

dlv exec --headless --listen=:2345 --api-version=2 /var/local/go/src/k8s.io/kubernetes/_output/local/bin/linux/amd64/kube-apiserver -- --authorization-mode=Node,RBAC  --cloud-provider= --cloud-config=   --v=3 --vmodule= --audit-policy-file=/tmp/kube-audit-policy-file --audit-log-path=/tmp/kube-apiserver-audit.log --authorization-webhook-config-file= --authentication-token-webhook-config-file= --cert-dir=/var/run/kubernetes --egress-selector-config-file=/tmp/kube_egress_selector_configuration.yaml --client-ca-file=/var/run/kubernetes/client-ca.crt --kubelet-client-certificate=/var/run/kubernetes/client-kube-apiserver.crt --kubelet-client-key=/var/run/kubernetes/client-kube-apiserver.key --service-account-key-file=/tmp/kube-serviceaccount.key --service-account-lookup=true --service-account-issuer=https://kubernetes.default.svc --service-account-jwks-uri=https://kubernetes.default.svc/openid/v1/jwks --service-account-signing-key-file=/tmp/kube-serviceaccount.key --enable-admission-plugins=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,Priority,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,ResourceQuota --disable-admission-plugins= --admission-control-config-file= --bind-address=0.0.0.0 --secure-port=6443 --tls-cert-file=/var/run/kubernetes/serving-kube-apiserver.crt --tls-private-key-file=/var/run/kubernetes/serving-kube-apiserver.key --storage-backend=etcd3 --storage-media-type=application/vnd.kubernetes.protobuf --etcd-servers=http://127.0.0.1:2379 --service-cluster-ip-range=10.0.0.0/24 --feature-gates=AllAlpha=false --external-hostname=localhost --requestheader-username-headers=X-Remote-User --requestheader-group-headers=X-Remote-Group --requestheader-extra-headers-prefix=X-Remote-Extra- --requestheader-client-ca-file=/var/run/kubernetes/request-header-ca.crt --requestheader-allowed-names=system:auth-proxy --proxy-client-cert-file=/var/run/kubernetes/client-auth-proxy.crt --proxy-client-key-file=/var/run/kubernetes/client-auth-proxy.key --cors-allowed-origins="/127.0.0.1(:[0-9]+)?$,/localhost(:[0-9]+)?$"

image-20210923153123713

settings.json 配置如下:

{
    "go.delveConfig": {
        "debugAdapter": "legacy",
    }
}

调试配置如下:

image-20210923153256597

此时可以看到,dlv 开始刷日志了,我们连上了!

image-20210923153436186

尝试暂停debug,看看左侧堆栈:

image-20210923153626580

尝试打个断点,成功!

image-20210923154058644

3.3 golang相关能力与代码规范

四、kubernetes 测试

4.1 验证

verify 很烧 cpu,注意自己的机器性能,请酌情验证,我机器已经死机好几次了,16c16g的机器。

image-20210831160623377

4.2 单元测试

todo

4.3 集成测试

todo

4.4 e2e测试

todo

五、kubernetes 贡献

5.1 签贡献者协议(Contributor License Agreement)

https://github.com/kubernetes/community/blob/master/CLA.md#the-contributor-license-agreement

  • 验证邮箱(该邮箱要跟GitHub账户邮箱一致)
  • 重设密码
  • 电子签署SLA文件

image-20210831164732222

linux基金会个人账号信息:https://identity.linuxfoundation.org/user

5.2 选择一个issue/todo

在 Kubernetes issue 列表,用标签过滤问题列表,例如 “good first issue”,“help wanted”,这些标签表明这个问题对新手友好。

image-20210901115818775

也可以搜索代码库里的TODO。

这里有两位大佬的first blood:

5.3 处理issue流程

  • 先熟读当前issue上下文或者pr,理解问题的背景和要解决的难题。
    • 如果它是一个 bug,就要验证你是否能复现这个 bug。请注意这可能需要投入大量时间.
  • 在 Slack 上联系问题(issue)的创建者来验证你的想法。
    • 一周之内没有收到反馈,直接提交 PR 。
  • 开发,coding
  • 本地单元测试/集成测试(最好做一下)
    • 跑通make verify(可能需要 30-40 分钟)
    • 跑通 make test
    • 跑通 make test-integration
  • pr
    • 至少两周内提交 PR 或者提供状态更新。
  • 在相关的sig Slack 频道找 Kubernetes Github 组织的成员给pr打标签 /ok-to-test。

5.4 贡献者相关链接

参考资料:

六、社区其它内容

6.1 社区行为准则

https://github.com/cncf/foundation/blob/master/code-of-conduct-languages/zh.md

Kubernetes 社区行为准则遵循 CNCF的行为准则,CNCF的行为准则也受 Linux 基金会行为准则约束。

CNCF 不可接受的参与者行为(v1.0):

  • 使用性语言或图像
  • 人身攻击
  • 挑衅、侮辱或贬低性评论
  • 公开或私下骚扰
  • 未经明确许可,发布他人的私人信息,比如地址或电子邮箱
  • 其它不道德或不专业的行为

Linux 基金会活动是旨在用于开源社区内的专业网络和协作的工作会议。它们的存在是为了鼓励思想和表达的开放交流,并需要一个承认每个人和团体的内在价值的环境。

不可接受的行为:

  • 骚扰
    • 基于性别、性别认同和表达、性取向、残疾、外貌、体型、种族、年龄、宗教或任何其他受举办会议或法律保护的内容
    • 使用辱骂性、冒犯性或有辱人格的语言
    • 恐吓、跟踪、骚扰性的摄影或录音
    • 不当的身体接触
    • 性图像、不受欢迎的性挑逗或性要求
  • 参展商
    • 不应在其展位中使用色情图片、活动或其他材料
    • 不得使用色情服装、制服、服装或以其他方式营造色情环境。
    • 演讲者不应在其演讲中使用性语言、图像或任何会构成上述定义的骚扰的语言或图像。

6.2 社区价值观

  • 分布式优于集中
  • 社区胜过产品或公司
  • 流程自动化
  • 包容胜于独占
  • 进化胜于停滞

6.3 社区期望

https://github.com/kubernetes/community/blob/master/contributors/guide/expectations.md

  • 必须 code review
  • Reviewer 与提交者交流要注意社区形象

6.4 社区成员与职责

https://github.com/kubernetes/community/blob/master/community-membership.md

  • Member
  • Reviewer
  • Approver
  • Subproject owner

我常用的 vscode 快捷键

因为我vim是必装的,所以某些vscode的快捷键就不用了。

以下是我常用的快捷键:

  • Ctrl+Shift+P打开命令面板

    虽然要敲一些字符,因为我们不会记录特别多的快捷键,所以这个方式也是很常用的。

    image-20210908160544988

    • > GO: GUT 生成单元测试

    image-20210908160557282

  • Ctrl+P 快速搜索(Mac:cmd+P),输入不同字符,进行不同操作:

    • ?:列出当前可执行的动作;

    • !:显示Errors或Warnings;直接快捷键Ctrl+Shift+M;

    • ::跳转到指定行;直接快捷键Ctrl+G;

    • @:查询本文件的 Symbol;

      image-20210908170533240

    • #:查询整个工程的Symbol;

  • Cmd+T,搜索Function,其实就是上一个快捷键+#

    image-20220815100037037

  • F5 debug

  • 窗口管理(分割编辑窗口):

    • Ctrl+ ~: 快速打开终端命令行
    • Ctrl+\:分割出新的窗口;
    • Ctrl+'数字':切换窗口,如Ctrl+1为第一个窗口;
    • :q:关闭当前窗口(标准模式下vim的快捷键);
    • Ctrl+- : 后退
    • Ctrl + Shift + -:前进
  • 代码折叠:

    • Ctrl+Shift+[:折叠当前区域(代码块);
    • Ctrl+Shift+]:展开当前区域(代码块);
  • 代码格式化

    • On Windows:Shift + Alt + F.
    • On Mac:Shift + Option + F.
    • On Ubuntu:Ctrl + Shift + I.
  • 设置:

    • Ctrl+,:快速打开 vscode 设置;

      image-20210908165436318


go 零散笔记

我从17年开始写go代码,到现在断断续续写了四年有余,其实比较惭愧,目前对go的认识非常浅薄。

究其原因,一个是我使用go的开发只是工作上粘合使用,每年写go代码的时间也不足1个月,基本上是在原有框架上做一些新功能的开发。得益于过去多年在laravel上的经验,看api文档和谷歌能力还是不错的,socket交互、orm、mongodb、k8s client-go等东西上手并不难,看半天基本上也就明白了如何使用。

二个是我工作中心更多是放在k8s这套系统以及偏网络方向上,虽然也是研发,更多是行业和架构层面的。

从今年下半年开始,我的工作重心转到了go的开发上来,而我个人也倾向于使用go作为我未来的主力开发语言。接下来这段时间我会记录更多关于 go 的基础知识。

这篇文章没什么重点,记录一些只言片语吧。

一、学习书籍

当遇到看不懂的内容时,有可能是作者的思考回路和我们的有差别。

不必纠结,跳过去,当看到同样内容不同作者的描述,你可能会豁然开朗。

入门:

入门时要注重理解go的设计理念和语言机制(Language Mechanics),

语言机制包括Go语言的句法、数据结构、解耦。

  • 《Go 程序设计语言》——许式伟译(英文原版翻译的,感觉细看浪费时间,快速浏览/后期针对性溯源就行。)
  • 《Go 语言编程》——许式伟(感觉适合基础入门,以补全理论概念铺垫为主,实战代码可以后期再看)

熟练:

熟练时要理解软件设计,研究并发,Go协程(Goroutine)、数据竞赛、多个channel和不用模式和用模式下的操作

高级:

了解基本单元测试、表测试、自测试等发测试方法,以及常见的标准等,还有各种包(Packages)。

更多选看:

二、Go 概念只言片语

这一部分大多来自 《Go 语言编程》——许式伟

2. 1 一些网站:

2. 2 基础类型:

  • int/unit/string/bool

  • 大整数big.Rat/浮点数fload/复数complex (math包)

  • string (strings包/strconv包/fmt包/utf8包/unicode包/regexp包)

  • 字符类型rune

  • 错误类型error

2. 3 组合类型:

  • 指针

  • 数组

  • slice切片

  • map(哈希表/字典)

  • 通道channel

  • struct 结构体

  • interface接口

    • 指定一组方法,抽象的,不可以实例化。接口的名字,默认以er结尾。接口可以嵌入。

    • 空接口 interface{}, 可以表示任意值,相当于指向任意类型的指针。

2. 4 流程控制:

  • 选择
    • 条件语句 if else
    • 选择语句 switch case/select
      • break continue fallthrough
  • 循环 for 和 range
  • 跳转 goto

2. 5 函数调用:

  • 大小写区分:
    • 小写字母开头的函数只在本包内可见,大写字母开头的函数才能被其他包使用。
    • 这个规则也适用于类型和变量的可见性。
  • 函数可以像普通变量一样被传递或使用

  • 不定参数:

    • func myfunc(args ...int)
    • func Printf(format string, args ...interface{})
  • 多返回值:

    • func (file *File) Read(b []byte) (n int, err Error)
  • 匿名函数/闭包:

    • 闭包内可以引用父方法的变量
    • 父方法为闭包提供绑定的计算环境(作用域)
  • 错误处理:

    • type error interface {
      Error() string
      }
      
    • defer

      • 先进后出的原则,延迟执行,一般用于文件、通道chan关闭、错误处理等。
    • panic/recover
  • init()和main()

image-20210819162920319

  • 动态函数
  • 泛型函数
  • 高阶函数
  • 纯记忆函数

2. 6 类型系统:

image-20210823113029415

2. 7 面向对象:

  • 构造函数,以NewXXX 来命名,表示“构造函数”

    func NewRect(x, y, width, height float64) *Rect {
    	return &Rect{x, y, width, height}
    }
    
  • 有继承,直接在struct引用父struct就ok了(匿名组合)。可以重写覆盖父方法。

  • 方法/变量的可见性,用大小写表示public/private。

  • 接口,隐式声明。

    • 变量类型查询: v1.(type)

2. 8 并发编程:

使用场景:

image-20210823142347960

实现方式:

image-20210823142544454

协程:

image-20210823143544145

goroutine:

  • go Add(1, 1)
    
  • 并发通信

    • 共享数据(c/c++,加锁)
    • 消息通信(go)
  • channel是类型相关的,只能传递一种类型的值,这个类型需要在声明时指定。

    • var chanName chan ElementType // 定义
      c := make(chan int, 1024) // 初始化,带缓冲区
          
      ch <- value // 将一个数据写入(发送)至channel的语法
      value := <-ch // 从channel中读取数据
          
      // 不带缓冲区时,向channel写入数据和读取数据会导致程序阻塞,直到有其他goroutine从这个channel中读取数据/写入数据为止。
          
      close(ch) //关闭channel
      
    • 单向channel:

      // 基于 ch4 ,通过类型转换初始化了两个单向channel:单向读的 ch5 和单向写的 ch6 。
          
      ch4 := make(chan int)
      ch5 := <-chan int(ch4) // ch5就是一个单向的读取channel
      ch6 := chan<- int(ch4) // ch6 是一个单向的写入channel
      
    • demo:

      image-20210823144823672

    • 高阶用法:

      • 底层多核并行:还没支持?

      • 出让时间片:Gosched()

      • 同步锁:sync.Mutex 、 sync.RWMutex

      • 多次只运行一次:once.Do()

      • 代码示例:

        image-20210823162559527

2. 9 网络编程:

  • Socket
  • http
  • rpc
  • json
  • net/http包

2. 10 安全编程:

  • pki
  • hash函数
    • MD5
    • SHA-1
  • https

2.11 代码规范:

  • 任何需要对外暴露的名字必须以大写字母开头、不需要对外暴露的则应该以小写字母开头

  • Go语言明确宣告了拥护骆驼命名法而排斥下划线法

  • 代码块中,左花括号{ 必须跟在同一行

  • 工程结构:

    README
    LICENSE
    bin/
    pkg/
    src/
    

2. 12 工程构建与命令行:

命令行主要完成以下这几类工作:

  • 代码格式化

  • 代码质量分析和修复

  • 单元测试与性能测试

    • 创建以_test结尾的go文件,形如[^.]*_test.go

    • 以 Test 和 Benchmark 为函数名前缀并以 *testing.T 为单一参数的函数。

      func TestAdd1(t *testing.T)
      func BenchmarkAdd1(t *testing.T)
      
  • 工程构建

  • 代码文档的提取和展示

  • 跨平台开发、编译

2. 13 高阶话题

  • 反射(reflection)

  • 多语言

    • cgo
  • 协程goroutine原理

  • 标准库

    go 标准库,导入使用unix风格。导入包的使用惯例,pkg.item

    https://books.studygolang.com/The-Golang-Standard-Library-by-Example/

    输入输出 (Input/Output)
    文本
    数据结构与算法
    日期与时间
    数学计算
    文件系统
    数据持久存储与交换
    数据压缩与归档
    测试
    进程、线程与 goroutine
    网络通信与互联网 (Internet)
    email
    应用构建 与 debug
    运行时特性
    底层库介绍
    同步
    加解密
    

2. 14 其它

方法与函数的区别:

函数是指不属于任何结构体、类型的方法,也就是说函数是没有接收者的;

方法是有接收者的,我们说的方法要么是属于一个结构体的,要么属于一个新定义的类型的。

方法在定义的时候,会在func和方法名之间增加一个参数,这个参数就是接收者,这样我们定义的这个方法就和接收者绑定在了一起,称之为这个接收者的方法。

type person struct {
	name string
}
 
func (p person) String() string{
	return "the person name is "+p.name
}

用户自定义类型,也应该实现Len()和Cap()方法。

Go语言的符号(symbol)一样,以大写字母开头的常量/函数在包外可见。

方法func

常量const(字面量)

变量var,赋予一内存块名字,该内存块保存特定的数据类型。可以匿名(返回值,等号左侧填_)

指针:保存了另一个变量内存地址的变量。

& ,取址操作符。

*,解引用操作符。

如果一个函数/方法返回超过4/5个值,最好使用一个切片/指向结构体的指针来传递,成本较低。

三、Go 实操笔记

这一部分大多来自:《Go 语言学习笔记》——雨痕

  • 运行时runtime、编译时Combile-time。

    • 静态类型语言都需要编译,Go是静态类型语言,不能在运行时改变变量类型。
  • 变量

    • 使用关键字 var 定义变量,自动初始化为零值。
    • 在函数内部,可用更简略的 “:=” 方式定义变量。
    • 可一次定义多个变量。
    • 特殊只写变量 “_“,用于忽略值占位。
  • 常量

    • 必须是编译期可确定的数字、字符串、布尔值。

    • 如不提供类型和初始化值,那么值与上一常量相同。

    • iota是常量计数器,在定义枚举时很有用。

      type AudioOutput int
          
      const (
          OutMute AudioOutput = iota // 0
          OutMono                    // 1
          OutStereo                  // 2
          _
          _
          OutSurround                // 5
      )
      
  • 引用类型

    • 包括 slice、map 和 channel。
    • 内置函数 new 计算类型大小,为其分配零值内存,返回指针。
    • make 会被编译器翻译成具体的创建函数,由其分配内存和初始化成员结构,返回对象而非指针。
  • 字符串

    • 字符串是不可变值类型,内部用指针指向 UTF-8 字节数组。

    • 默认值是空字符串 ““。

    • 用索引号访问某字节,如 s[i]。

    • 不能用索引号获取字节元素指针,&s[i] 非法。

    • 不可变类型,无法修改字节数组。

    • 字节数组尾部不包含 NULL。

    • 使用 “`” 定义不做转义处理的原始字符串,支支持跨行行。

    • 连接跨行字符串时,”+” 必须在上一行末尾,否则编译错误。

    • 支持用两个索引号返回子串。子串依然指向原字节数组,仅修改了指针和长度属性。

      s := "Hello, World!"
      s1 := s[:5] // Hello
      s2 := s[7:] // World!
      s3 := s[1:5] // ello
      
    • rune 是 int32 的别名,几乎在所有方面等同于int32,用于区分字符值和整数值。

    • golang 中的字符有两种,uint8(byte)代表ASCII的一个字符,rune代表一个utf-8字符。

    • 修改字符串,可先将其转换成 []rune 或 []byte,完成后再转换为 string.无论哪种转换,都会重新分配内存,并复制字节数组。有汉字等需要utf8支持的就用rune,没汉字随意。

      func main() {
          s := "abc汉字"
          for i := 0; i < len(s); i++ {
              // byte
              fmt.Printf("%c,", s[i])
          }
          fmt.Println()
          for _, r := range s {
              // rune
              fmt.Printf("%c,", r)
          }
      }
      
  • 指针

    • 默认值 nil,没有 NULL 常量。

    • 操作符 “&” 取变量地址,”*” 透过指针访问⺫目目标对象。

    • 不支持指针运算,不支持 “->” 运算符,直接用 “.” 访问目标成员。

      func main() {
          type data struct{ a int }
          var d = data{1234}
          var p *data
              
          p = &d
          
          fmt.Printf("%p, %v\n", p, p.a)    // 直接用指针访问目标对象成员,无须转换。
      }
      
    • 可以在 unsafe.Pointer 和任意类型指针间进行转换。

      func main() {
          x := 0x12345678
          p := unsafe.Pointer(&x) // *int -> Pointer
          n := (*[4]byte)(p) 		// Pointer -> *[4]byte
              
          for i := 0; i < len(n); i++ {
              fmt.Printf("%X ", n[i])
          }
      }
          
      // 78 56 34 12
      
    • 将 Pointer 转换成 uintptr,可变相实现指针运算。

      func main() {
          d := struct {
              s string
              x int
          }{"abc", 100}
              
          p := uintptr(unsafe.Pointer(&d)) // *struct -> Pointer -> uintptr
          p += unsafe.Offsetof(d.x) // uintptr + offset
              
          p2 := unsafe.Pointer(p) // uintptr -> Pointer
          px := (*int)(p2) // Pointer -> *int
          *px = 200 // d.x = 200
              
          fmt.Printf("%#v\n", d)
      }
          
      // struct { s string; x int }{s:"abc", x:200}
      
  • 自定义类型

    • 可用 type 在全局或函数内定义新类型。 type bigint int64
    • 显示转换。x := 1234 var b bigint = bigint(x)
  • 保留字

    break default func interface select
    case defer go map struct
    chan else goto package switch
    const fallthrough if range type
    continue for import return var
    
  • 位运算

  • 循环

    • for,支持初始化语句。

      s := "abcd"
      for i, n := 0, length(s); i < n; i++ {
          println(i, s[i])
      }
      
    • range,range 会复制对象。

      for k, v := range m {
      	println(k, v)
      }
      
    • switch,可省略break,表达式可以任意类型,不限于常量,需要继续下一支支,使用 fallthrough

    • 省略条件表达式,switch可当 if…else if…else 使用

      switch i := x[2]; {
          // 带初始化语句
          case i > 0:
          println("a")
          case i < 0:
          println("b")
          default:
          println("c")
      }
      
    • break 可用用于 for、switch、select,而 continue 仅能用于 for 循环。

    • 支持在函数内 goto 跳转。标签名区分大小写

  • 函数

    • 不支持 嵌套 (nested)、重载 (overload) 和 默认参数 (default parameter)。

    • 无需声明原型 支持不定长变参 支持多返回值 支持命名返回参数 支持匿名函数和闭包

    • 有返回值的函数,必须有明确的终止语句,否则会引发编译错误。

    • 变参,本质上就是 slice。只能有一个,且必须是最后一个。

    • 使用 slice 对象做变参时,必须展开。

      func main() {
          s := []int{1, 2, 3}
          println(test("sum: %d", s...))
      }
      
    • 多返回值可直接作为其他函数调用实参。

    • 使用用 slice 对象做变参时,必须展开。

    • 命名返回参数可被同名局部变量遮蔽,此时需要显式返回。

    • 匿名函数可赋值给变量,做为结构字段,或者在 channel 里传送。

    • 延迟执行defer

      • 在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行

        func test() {
            defer func() {
                fmt.Println(recover())
            }()
                  
            defer func() {
                panic("defer panic")
            }()
                  
            panic("test panic")
        }
        func main() {
            test()
        }
        
      • 滥用 defer 可能会导致性能问题,尤其是在一个 “大循环” 里。

      • 如果需要保护代码片段,可将代码块重构成匿名函数,如此可确保后续代码被执行。

  • 数组

    • 数组是值类型,赋值和传参会复制整个数组,而不是指针。

    • 内置函数 len 和 cap 都返回数组长度 (元素数量)。

    • 指针数组 [n]*T,数组指针 *[n]T。

    • 值拷贝会造成性能问题,请使用 slice,或数组指针。

      a := [3]int{1, 2} // 未初始化元素值为 0。
      b := [...]int{1, 2, 3, 4} // 通过初始化值确定数组⻓长度。
      c := [5]int{2: 100, 4:200} // 使用用索引号初始化元素。
      
  • Slice

    • 初始化和数组很像,不用声明长度

      s1 := []int{0, 1, 2, 3, 8: 100} // 通过初始化表达式构造,可使用用索引号。
      fmt.Println(s1, len(s1), cap(s1))
          
      s2 := make([]int, 6, 8) // 使用用 make 创建,指定 len 和 cap 值。
      fmt.Println(s2, len(s2), cap(s2))
          
          
      s3 := make([]int, 6) // 省略 cap,相当于 cap = len。
      fmt.Println(s3, len(s3), cap(s3))
      
    • 可用指针直接访问底层数组,退化成普通数组操作。

      s := []int{0, 1, 2, 3}
      p := &s[2] // *int, 获取底层数组元素指针。
      *p += 100
      fmt.Println(s)  // [0 1 102 3]
      
    • 基于已有 slice 创建新 slice 对象,新对象依旧指向原底层数组。

      s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
      s1 := s[2:5]  // [2 3 4]
      s1[2] = 100
          
      s2 := s1[2:6]  // [100 5 6 7]
      s2[3] = 200
          
      fmt.Println(s) // [0 1 2 3 100 5 6 200 8 9]
      
    • append/copy

  • map 哈希表

    • 初始化

      m := make(map[string]int, 1000)
          
      m := map[string]int{
      "a": 1,
      }
          
      if v, ok := m["a"]; ok {
          // 判断 key 是否存在。
          println(v)
      }
          
      println(m["c"]) // 对于不存在的 key,直接返回 \0,不会出错。
      m["b"] = 2 // 新增或修改。
      delete(m, "c") // 删除。如果 key 不存在,不会出错。
      println(len(m)) // 获取键值对数量。cap 无无效。
          
      for k, v := range m { // 迭代,可仅返回 key。随机顺序返回,每次都不相同。
          println(k, v)
      }
      
    • 从 map 中取回的是一个 value 临时复制品,对其成员的修改是没有任何意义的。正确做法是完整替换 value 或使用用指针。

      type user struct{ name string }
      m := map[int]user{
          1: {"user1"},
          // 当 map 因扩张而而重新哈希时,各键值项存储位置都会发生生改变。 因此,map
          // 被设计成 not addressable。 类似 m[1].name 这种期望透过原 value
      } // 指针修改成员的行行为自自然会被禁止止。
          
      m[1].name = "Tom" // Error: cannot assign to m[1].name
          
      u := m[1]
      u.name = "Tom"
      m[1] = u      // 替换 value。
          
      m2 := map[int]*user{
      	1: &user{"user1"},
      }
      m2[1].name = "Jack" // 返回的是指针复制品。透过指针修改原对象是允许的。
      
  • struct

    • 支持指向自身类型的指针成员。

    • 顺序初始化必须包含全部字段,否则会出错。

      type Node struct {
          _ int
          id int
          data *byte
          next *Node
      }
      
    • 匿名字段

      可以像普通字段那样访问匿名字段成员, 编译器从外向内逐级查找所有层次的匿名字段,直到发现目标或出错。

      type User struct {
          name string
      }
      type Manager struct {
          User
          title string
      }
      

      外层同名字段会遮蔽嵌入入字段成员,解决方法是使用用显式字段名。

      不能同时嵌入入某一类型和其指针类型,因为它们名字相同。

  • 面向对象

    • 面向对象三大大特征里里, Go 仅支持封装(匿名字段的内存布局和行为类似继承)。没有class 关键字,没有继承、多态等等。
  • 方法

    • 方法总是绑定对象实例,并隐式将实例作为第一实参 (receiver)。

    • 只能为当前包内命名类型定义方法。 参数 receiver 可任意命名。如方法中未曾使用,可省略参数名。 参数 receiver 类型可以是 T 或 *T。基类型 T 不能是接口或指针。 不支持方法重载,receiver 只是参数签名的组成部分。 可用用实例 value 或 pointer 调用用全部方法,编译器自动转换。

    • 没有构造和析构方法,通常用简单工厂模式返回对象实例。

      type Queue struct {
          elements []interface{}
      }
          
      func NewQueue() *Queue {
          // 创建对象实例。
          return &Queue{make([]interface{}, 10)}
      }
          
      func (*Queue) Push(e interface{}) error {
          // 省略 receiver 参数名。
          panic("not implemented")
      }
          
      // func (Queue) Push(e int) error {
          // panic("not implemented")
          // Error: method redeclared: Queue.Push
              
      // }
      func (self *Queue) length() int {
          // receiver 参数名可以是 self、this 或其他。
          return len(self.elements)
      }
      
    • 不支持多级指针查找方法成员。

    • 通过匿名字段,可获得和继承类似的复用能力。依据编译器查找次序,只需在外层定义同名方法,就可以实现 “override”。

      type User struct {
          id
          int
          name string
      }
      type Manager struct {
          User
          title string
      }
          
      func (self *User) ToString() string {
          return fmt.Sprintf("User: %p, %v", self, self)
      }
      func (self *Manager) ToString() string {
          return fmt.Sprintf("Manager: %p, %v", self, self)
      }
      func main() {
          m := Manager{User{1, "Tom"}, "Administrator"}
          fmt.Println(m.ToString())
          fmt.Println(m.User.ToString())
      }
      
  • Interface

    • 接口命名习惯以 er 结尾,结构体。 接口只有方法签名,没有实现。 接口没有数据字段。 可在接口中嵌入其他接口。

    • 空接口 interface{} 没有任何方法签名,也就意味着任何类型都实现了空接口。其作用类似面向对象语言言中的根对象 object。

      type Stringer interface {
          String() string
      }
      type Printer interface {
          Stringer     // 接口口嵌入入。
          Print()
      }
      type User struct {
          id    int
          name string
      }
          
      func (self *User) String() string {
      	return fmt.Sprintf("user %d, %s", self.id, self.name)
      }
      func (self *User) Print() {
      	fmt.Println(self.String())
      }
          
      func main() {
          var t Printer = &User{1, "Tom"}     // *User 方方法集包含 String、Print。
          t.Print()
      }
      
    • 接口对象由接口表 (interface table) 指针和数据指针组成。 只有 tab 和 data 都为 nil 时,接口才等于 nil。

      var a interface{} = nil // tab = nil, data = nil
      var b interface{} = (*int)(nil) // tab 包含 *int 类型信息, data = nil
          
      type iface struct {
          itab, data uintptr
      }
          
      ia := *(*iface)(unsafe.Pointer(&a))
      ib := *(*iface)(unsafe.Pointer(&b))
          
      fmt.Println(a == nil, ia)
      fmt.Println(b == nil, ib, reflect.ValueOf(b).IsNil())
          
      //
      // true {0 0}
      // false {505728 0} true
      

      这个特性,官方有个关于error的有趣的描述:https://golang.org/doc/faq#nil_error,简单来说就是不要自己定义error,免得判断 nil 时候出问题。这里还有个类似的例子:

      image-20210825130630230

    • 数据指针持有的是目标对象的只读复制品,复制完整对象或指针。

      type User struct {
          id    int
          name string
      }
      func main() {
          u := User{1, "Tom"}
          var i interface{} = u
          u.id = 2
          u.name = "Jack"
          fmt.Printf("%v\n", u)
          fmt.Printf("%v\n", i.(User))
      }
          
      // {2 Jack}
      // {1 Tom}
      
    • 接口转型返回临时对象,只有使用指针才能修改其状态。

      type User struct {
          id    int
          name string
      }
      func main() {
          u := User{1, "Tom"}
          var vi, pi interface{} = u, &u
              
          // vi.(User).name = "Jack"    // Error: cannot assign to vi.(User).name
          pi.(*User).name = "Jack"
              
          fmt.Printf("%v\n", vi.(User))
          fmt.Printf("%v\n", pi.(*User))
      }
      
    • 接口类型判断

      type User struct {
          id int
          name string
      }
          
      func (self *User) String() string {
      	return fmt.Sprintf("%d, %s", self.id, self.name)
      }
      func main() {
          var o interface{} = &User{1, "Tom"}
          if i, ok := o.(fmt.Stringer); ok {       // ok-idiom
              fmt.Println(i)
          }
          u := o.(*User)
          // u := o.(User)     // panic: interface is *main.User, not main.User
          fmt.Println(u)
      }
      

      批量判断:

      func main() {
          var o interface{} = &User{1, "Tom"}
          switch v := o.(type) {
              case nil:
                  // o == nil
                  fmt.Println("nil")
              case fmt.Stringer:
                  // interface
                  fmt.Println(v)
              case func() string:
                  // func
                  fmt.Println(v())
              case *User:
                  // *struct
                  fmt.Printf("%d, %s\n", v.id, v.name)
              default:
                  fmt.Println("unknown")
          }
      }
      
    • 让编译器检查,以确保某个类型实现接口。

      var _ fmt.Stringer = (*Data)(nil)
      
  • 并发goroutine

    • 入口函数 main 就以 goroutine 运行。另有与之配套的 channel 类型,实现 “以通讯来共享内存” 的 CSP 模式。

      go func() {
      	println("Hello, World!")
      }()
      
    • 调度器不能保证多个 goroutine 执行行次序,且进程退出时不会等待它们结束。

    • 默认情况下,进程启动后仅允许一个系统线程服务于 goroutine。可使用环境变量或标准库函数 runtime.GOMAXPROCS 修改,让调度器用多个线程实现多核并行,而不仅仅是并发。

    • 调用 runtime.Goexit 将立即终止当前 goroutine 执行,调度器确保所有已注册 defer延迟调用被执行。

  • channel

    • 简单使用

      var chanName chan ElementType // 定义
      c := make(chan int, 1024) // 初始化,带缓冲区
          
      ch <- value // 将一个数据写入(发送)至channel的语法
      value := <-ch // 从channel中读取数据
          
      // 不带缓冲区时,向channel写入数据和读取数据会导致程序阻塞,直到有其他goroutine从这个channel中读取数据/写入数据为止。
          
      close(ch) //关闭channel
      
    • 默认为同步模式,需要发送和接收配对。否则会被阻塞。

    • 异步方式通过判断缓冲区来决定是否阻塞。如果缓冲区已满,发送被阻塞;缓冲区为空,接收被阻塞。

    • 异步 channel 可减少排队阻塞,具备更高的效率。

      func main() {
          data := make(chan int) // 数据交换队列
          exit := make(chan bool) // 退出通知
          go func() {
              for d := range data {            // 从队列迭代接收数据,直到 close 。
                  fmt.Println(d)
              }
                  
              fmt.Println("recv over.")
              exit <- true        // 发出退出通知。
          }()
              
          data <- 1    // 发送数据。
          data <- 2
          data <- 3
          close(data)    // 关闭队列。
              
              
          fmt.Println("send over.")
          <-exit     // 等待退出通知。
      }
      
    • channel 应该考虑使用指针规避大大对象拷贝,将多个元素打包,减小缓冲区大小等。

    • 除用 range 外,还可用 ok-idiom 模式判断 channel 是否关闭。

      for {
          if d, ok := <-data; ok {
              fmt.Println(d)
          } else {
              break
          }
      }
      
    • 单向 chan, 不能将单向 channel 转换为普通 channel。

      c := make(chan int, 3)
      var send chan<- int = c // send-only
      var recv <-chan int = c // receive-only
          
      send <- 1  发送数据
      // <-send  // Error: receive from send-only type chan<- int
          
      <-recv
      // recv <- 2 // Error: send to receive-only type <-chan int
          
          
      
    • 在循环中使用 select default case 需要小心,避免形成洪水。

  • 工具

    • go build

    • go install

    • go clean

    • go get

    • go tool objdump

    • 跨平台编译

    • 数据竞争 (data race)

    • go test

    • Benchmark

    • PProf

四、hello world

main.go

package main

import "fmt"

func main() {
  fmt.Printf("hello, world\n")
}

初始化go module

go mod init kelu.org/apptest
go mod tidy
go build 

这样会生成一个 apptest 的可执行文件。