diff --git a/.github/ISSUE_TEMPLATE/choose-article-tpl.md b/.github/ISSUE_TEMPLATE/choose-article-tpl.md new file mode 100644 index 000000000..fb8d19ca8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/choose-article-tpl.md @@ -0,0 +1,27 @@ +--- +name: 提交新选题 +about: 规范选题提交的格式 +title: 日期+英文文章标题 +labels: 待认领 +assignees: '' + +--- + +**类似下面的信息** + +标题:How to compile code in the browser with WebAssembly + +原文链接:https://johnstarich.medium.com/how-to-compile-code-in-the-browser-with-webassembly-b59ffd452c2b + +译者自己根据原文翻译,译文保存为 markdown 格式,并附上尾部的签名。 + +``` +--- +via: https://johnstarich.medium.com/how-to-compile-code-in-the-browser-with-webassembly-b59ffd452c2b + +作者:[John Starich](https://johnstarich.medium.com/) +译者:[译者ID](https://github.com/译者ID) +校对:[校对者ID](https://github.com/校对者ID) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 +``` diff --git a/.gitignore b/.gitignore index ddcaae478..338f5b6e0 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ .glide/ .DS_Store rebasefork.sh + +.idea diff --git a/.travis.yml b/.travis.yml index 19ec39e1e..3d08dcbac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,5 +12,5 @@ jobs: include: - stage: markdownlint script: - - gem install mdl + - gem install mdl --version 0.9.0 - mdl . diff --git a/README.md b/README.md index 790317d05..ded6365b2 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ GCTT (Go Chinese Translation Team) 是 Go 中文网(https://studygolang.com) 请首先加入翻译组的 QQ 群,群号是:423373670,加群时请说明是“志愿者”。加入后记得修改您的群名片为您的 GitHub 的 ID。**同时强烈建议注册 Go 中文网账号并和 GitHub 账号绑定,这样无论是翻译提醒和后期译文发布,都会和你的中文网账号关联。** +关注微信公众号:Go语言中文网 + +![](wechat.png) + ## 如何开始 加入的成员,请先阅读 [WIKI 如何开始](https://github.com/studygolang/GCTT/wiki)。 @@ -53,6 +57,7 @@ GCTT (Go Chinese Translation Team) 是 Go 中文网(https://studygolang.com) * 2019/09/11 提升 DingdingZhou 为核心成员。 * 2019/09/16 提升 TomatoAres 为核心成员。 * 2019/09/29 提升 Watermelo 为核心成员。 +* 2019/11/24 提升 lxbwolf 为核心成员。 ## Inspire By LCTT diff --git a/published/news/https:/thenewstack.io/facebooks-golang-object-relational-mapper-moves-to-the-linux-foundation.md b/published/news/https:/thenewstack.io/facebooks-golang-object-relational-mapper-moves-to-the-linux-foundation.md new file mode 100644 index 000000000..3b0717b3f --- /dev/null +++ b/published/news/https:/thenewstack.io/facebooks-golang-object-relational-mapper-moves-to-the-linux-foundation.md @@ -0,0 +1,35 @@ +首发于:https://studygolang.com/articles/35233 + +# Facebook的 Go ORM:ent 移动到了 Linux 基金会 + +![](https://cdn.thenewstack.io/media/2021/09/7e01e106-go-ent.png) + +"[Ent](https://entgo.io/)" 是最初由 Facebook 创建并于 2019 年开源的 Go 实体框架,现已加入 [Linux基金会](https://training.linuxfoundation.org/training/course-catalog/?utm_content=inline-mention)。 Ent 帮助开发人员处理复杂的后端应用程序,在这些应用程序中他们可能需要处理大量实体类型以及它们之间的关系。 + +[Ariel Mashraki](https://il.linkedin.com/in/ariel-mashraki-435a1250),Ent 的创建者和主要维护者说,当他在在致力于为世界带来互联网连接的 [Facebook Connectivity](https://www.facebook.com/connectivity) 团队中,该团队需要一个对象关系映射 (ORM) 工具,该工具可以处理映射他们正在处理的网络拓扑,但找不到合适的工具,于是他们创造了 Ent。经过几年的开源并看到社区越来越多的采用和参与,Facebook 决定将 Ent 项目转移到 Linux 基金会的管理之下,在那里它将为未来找到一个供应商中立的家。 + +通过转向 Linux 基金会,Mashraki 说这让他可以离开 Facebook 共同创立数据图初创公司 [Ariga](https://ariga.io/),同时仍在 Ent 工作,该项目正在寻求更多希望参与的其他公司的参与。 + +传统的 ORM 将面向对象编程中的对象映射到数据库。 Mashraki 解释说,Ent 提供了这种基本的 ORM 功能,同时还对一些附加功能进行了分层,这就是使其成为“实体框架”的原因。根据一篇[博客文章](https://www.linuxfoundation.org/press-release/ent-joins-the-linux-foundation/),Ent“使用图概念对应用程序的模式进行建模,并采用先进的代码生成技术来创建类型安全、高效的代码,与其他方法相比,这大大简化了对数据库的处理。” + +将其分解, Mashraki 进一步解释说,Ent 的创建考虑了三个特定的设计原则。首先,它使用图概念,例如节点和边,对数据进行建模和查询,这意味着数据库可以是关系型的,也可以是基于图的。其次,Ent 的代码生成引擎会分析应用程序架构并生成类型安全的显式 API,供开发人员与数据库(例如 MySQL、PostgreSQL 或 AWS Neptune)交互。最后,Ent 在 Go 代码中表达了与实体相关的所有逻辑(包括授权规则和副作用),这提供了 Ent 的内置支持,可以直接从模式定义中自动生成 GraphQL、REST 或 gRPC 服务器。 Mashraki 说,所有这些不仅有助于处理这些大型数据集,而且还提供了更好的开发人员体验。 + +“这意味着,对于开发人员在其 Ent 架构中定义的每个实体,显式、类型-为开发人员生成安全代码以有效地与其数据交互。类型安全代码提供了卓越的开发体验,因为 IDE 非常了解 API 并且可以提供非常准确的代码完成建议,此外,通过这种方法,在编译过程中可以捕获许多类别的错误,这意味着更快的反馈循环和更高质量的软件,” Mashraki 在一封电子邮件中写道。 “此外,网络拓扑在图概念中更容易建模和查询。尝试维护以关系术语遍历数百种实体的代码和查询太容易出错且速度缓慢,而 Ent 正是从这种痛苦中直接创建的。” + +Ent 都用 Go 编写并用 Go 生成代码, Mashraki 表示该框架在云原生社区找到了一个天然的家,许多云原生计算基金会 (CNCF) 项目都在使用该框架。虽然 Go 是一个最初的选择,但 Mashraki 说他们也在考虑在不久的将来添加其他语言,例如 TypeScript,因为它在前端开发人员中很受欢迎。 + +至于 Go,Mashrak 谈到即将[添加的泛型](https://go.dev/blog/generics-proposal)此举是“将减少生成代码的数量,并使创建通用扩展而不使用代码生成成为可能。我们已经在试验它。” + +展望未来,Mashraki 说 Ent 项目目前有两个主要计划正在进行中。首先是一个迁移 API,它“旨在与 Kubernetes 和 Terraform 等云原生技术无缝集成”。接下来,Ent 将获得一个新的查询引擎,该引擎将允许在同一个 Ent 架构上定义多种存储类型,例如,这将允许开发人员使用相同的 Ent 客户端查询 SQL、blob 和文档数据库。 + +“我们邀请更多公司加入这项努力并成为其中的一部分,” Mashraki 补充道。 + +--- + +via: https://thenewstack.io/facebooks-golang-object-relational-mapper-moves-to-the-linux-foundation/ + +作者:[Mike Melanson](https://thenewstack.io/author/mike-melanson/) +译者:[lavaicer](https://github.com/lavaicer) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/news/survey2020-results.md b/published/news/survey2020-results.md new file mode 100644 index 000000000..4bb05b576 --- /dev/null +++ b/published/news/survey2020-results.md @@ -0,0 +1,227 @@ +首发于:https://studygolang.com/articles/33990 + +# Go 官方 2020 年开发者调查报告 + +2021 年 3 月 9 日,在 Go 官方博客发布了 Go 开发者 2020 年调查报告。一起来看看该报告的内容吧。 + +> 2020 年,一共有 9648 人参与投票,大约相当于 2019 年的投票人数。 + +说明:你可能会注意到有些问题的样本量比其他问题小 (“n =”)。这是因为有些问题是向所有人展示的,而另一些只是向随机的一部分受访者展示。 + +## 01 报告摘要 + +- Go 的使用场景和企业都在扩大,76% 的受访者工作中使用 Go;66% 的人说 Go 对他们公司的成功至关重要; +- 92% 的受访者对 Go 感到满意; +- 在使用不到 3 个月的时间里,大多数受访者感觉使用 Go 非常高效(生产力很高),占比达 81%; +- 大家倾向升级到 Go 最新版本,在前 5 个月中达到了 76%; +- 使用 pkg.go.dev 用户更容易找到想要的包(91% vs 82%); +- Go 模块的采用率几乎达到了普遍水平,满意度为 77%,但受访者还强调需要改进文档; +- Go 继续大量用于 API,CLI,Web,DevOps 和数据处理; +- 代表性不足的群体在社区中往往会受到较少的欢迎; + +## 02 受访者群体 + +人口统计学问题有助于我们区分哪些年度差异可能源于调查对象的变化,哪些是情绪或行为的变化。因为我们的人口统计数据与去年相似,我们有理由相信,其他的年度变化主要不是由于人口统计学的变化。 + +例如,从 2019 年到 2020 年,组织规模、开发人员经验和行业的分布基本保持不变。 + +![Bar chart of organization size for 2019 to 2020 where the majority have fewer than 1000 employees](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/orgsize.svg) + +![Bar chart of years of professional experience for 2019 to 2020 with the majority having 3 to 10 years of experience](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/devex_yoy.svg) + +![Bar chart of organization industries for 2019 to 2020 with the majority in Technology](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/industry_yoy.svg) + +近一半(48%)的受访者使用 Go 不到两年。在 2020 年,我们收到的回应是少于一年。(GCTT 注:可见 Go 还是很年轻,这两年增长也迅速,很多新人进入了) + +![Bar chart of years of experience using Go](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/goex_yoy.svg) + +大多数人说他们在工作中(76%)和工作之外(62%)使用 Go。在工作中使用 Go 的受访者的百分比逐年呈上升趋势。 + +![Bar chart where Go is being used at work or outside of work](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/where_yoy.svg) + +今年我们提出了一个关于主要工作职责的新问题。我们发现,70% 的受访者的主要职责是开发软件和应用程序,但有相当一部分(10%)是设计 IT 系统和架构。 + +![Primary job responsibilities](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/job_responsibility.svg) + +与往年一样,我们发现大多数受访者并不是 Go 开源项目的经常贡献者,75% 的受访者表示他们“不经常”或“从来没有”贡献过。(GCTT 注:看来开源不是那么容易的,还是使用者居多) + +![How often respondents contribute to open source projects written in Go from 2017 to 2020 where results remain about the same each year and only 7% contribute daily](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/foss_yoy.svg) + +## 03 开发人员工具和实践 + +与前几年一样,绝大多数的调查对象报告说他们在使用 Linux (63%)和 macOS (55%)系统。随着时间的推移,主要在 Linux 上开发的受访者的比例似乎略有下降。(GCTT 注:Windows 对开发还是不够友好,不过近一年 Windows 做了很多改变,拭目以待!) + +![Primary operating system from 2017 to 2020](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/os_yoy.svg) + +第一次,首选的编辑器似乎已经稳定: VS Code 仍然是最受欢迎的编辑器(41%) ,GoLand 是强有力的次优(35%)。它们俩加起来占比超过 76% ,其他编辑器并没有像前几年那样继续减少。(GCTT 注:VS Code 确实很棒,插件也已经归属 Go 官方,而 GoLand 同样很棒,但毕竟收费的。其他的没减少,大概率是某个编辑器的忠实粉丝吧!) + +![Editor preferences from 2017 to 2020](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/editor_pref_yoy.svg) + +今年我们询问受访者,假设他们有 100 个 “gophercoin”(一个虚构的货币) ,他们会花多少钱来优先改进他们编辑器的什么功能。代码完成(Code completion)得到的 “gophercoin” 最多。一半的受访者给出了前四项特性(代码完成、导航代码、编辑器性能和重构),它们 10 个或更多的 gophercoin。 + +![Bar char of average number of GopherCoins spent per respondent](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/editor_improvements_means.svg) + +大多数受访者(63%)花费 10-30% 的时间进行重构,这表明这是一项常见的任务,我们希望研究改进它的方法。这也解释了为什么重构支持是最受资助的编辑器改进之一。 + +![Bar chart of time spent refactoring](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/refactor_time.svg) + +去年我们询问了特定的开发人员,发现几乎 90% 的受访者使用文本日志进行调试,因此今年我们增加了一个后续问题来找出原因。结果显示,43% 的人使用它是因为它允许他们在不同的语言中使用相同的调试策略,42% 的人更喜欢使用文本日志而不是其他调试技术。然而,27% 的人不知道如何开始使用 Go 的调试工具,24% 的人从来没有尝试过使用 Go 的调试工具,因此有机会改进调试工具的可发现性、可用性和文档性。此外,由于四分之一的受访者从未尝试过使用调试工具,痛处可能被低估了。(GCTT 注:PHPer 喜欢文本日志调试,哈哈哈哈,你懂的) + +![img](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/why_printf.svg) + +## 04 对 Go 的看法 + +今年,我们第一次询问了总体满意度。92% 的受访者表示,在过去一年中,他们对 Go 的使用非常满意或略感满意。 + +![Bar chart of overall satisfaction on a 5 points scale from very dissatisfied to very satisfied](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/csat.svg) + +这是我们第三年提出“你推荐... ...”[网络推广分数](https://en.wikipedia.org/wiki/Net_Promoter)(NPS)的问题。今年我们的 NPS 结果是 61(68% 的“推动者”减去 6% 的“诋毁者”) ,与 2019 年和 2018 年统计数据相同。(GCTT 注:也就是说 68% 的人会推荐 Go 语言,而 6% 的人说 Go 不好之类的) + +![Stacked bar chart of promoters, passives, and detractors](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/nps.svg) + +与前几年一样,91% 的受访者表示他们更愿意在下一个新项目中使用 Go。89% 的人认为 Go 在他们的团队中表现良好。今年,我们看到越来越多的受访者认为 Go 对他们公司的成功至关重要,从 2019 年的 59% 上升到 2020 年的 66% 。在 5000 人以上的组织工作的受访者不太可能同意(63%) ,而在较小的组织工作的受访者更可能同意(73%)。(GCTT 注:看来小公司更认为 Go 对他们的成功很关键,比如国内的七牛?!) + +![Bar chart of agreement with statements I would prefer to use Go for my next project, Go is working well for me team, 89%, and Go is critical to my company's success](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/attitudes_yoy.svg) + +与去年一样,我们要求受访者根据满意度和重要性对 Go 开发的具体方向进行评级。使用云服务、调试和使用模块(去年被强调为改进的领域)的满意度有所提高,而大多数重要性得分保持不变。我们还介绍了一些新的主题: API 和 Web 框架。我们发现 Web 框架的满意度低于其他领域(64%)。对于大多数当前用户来说,它并不是那么重要(只有 28% 的受访者认为它非常重要) ,但是对于潜在的 Go 开发者来说,它可能是一个缺失的关键特性。 + +![Bar chart of satisfaction with aspects of Go from 2019 to 2020, showing highest satisfaction with build speed, reliability and using concurrency and lowest with Web frameworks](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/feature_sat_yoy.svg) + +81% 的受访者说他们觉得使用 Go 非常有效率。大型组织的受访者比小型组织的受访者更有可能感到极其富有成效。(GCTT 注:看来确实是一门面向大型工程的语言) + +![Stacked bar chart of perceived productivity on 5 point scale from not all to extremely productive ](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/prod.svg) + +我们听说 Go 很容易变得高效。我们询问了那些觉得自己至少有一点生产力的受访者,他们花了多长时间才变得有生产力。93% 的人说他们花了不到一年的时间,大多数人在 3 个月内就感觉到有效率。(GCTT 注:这一定程度上还是说明 Go 简单,容易快速进行开发,提高效率) + +![Bar chart of length of time before feeling productive](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/prod_time.svg) + +虽然与去年大致相同,但随着时间的推移,同意”我感到在 Go 社区受欢迎”这一说法的受访者百分比似乎有所下降,或者至少不像其他领域那样保持同样的上升趋势。 + +我们还发现,认为 Go Team 理解自己需求的受访者比例(63%)逐年显著上升。(GCTT 注:这是说 Go Team 开发的新特性,大部分都是社区需要的) + +![Bar chart showing agreement with statements I feel welcome in the Go community, I am confident in the Go leadership, I feel welcome to contribute, The Go project leadership understands my needs, and The process of contributing to the Go project is clear to me](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/attitudes_community_yoy.svg) + +我们就如何使 Go 社区更受欢迎提出了一个公开问题,最常见的建议(21%)涉及学习资源和文档的不同形式或改进/增加。 + +![Bar chart of recommendations for improving the welcomeness of the Go community](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/more_welcoming.svg) + +## 05 用 Go 干什么 + +构建 API/RPC 服务(74%)和 cli (65%)仍然是 Go 最常见的用途。与去年相比,我们没有看到任何重大变化,当时我们在选项排序中引入了随机化。(在 2019 年之前,名单开头的选项被不成比例地选中。)我们还根据组织规模对这一问题进行了分析,发现受访者在大型企业或者小型组织中使用 Go 的情况类似,尽管大型组织使用返回 HTML 的 Go for Web 服务的可能性有所降低。(GCTT 注:竟然有 8% 的人用 Go 写桌面 GUI 应用?厉害了) + +![Bar chart of Go use cases from 2019 to 2020 including API or RPC services, CLIs, frameworks, Web services, automation, agents and daemons, data processing, GUIs, games and mobile apps](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/app_yoy.svg) + +今年,我们更好地区分了调查者在业余时间使用 Go 和在工作中使用 Go 开发的不同软件。虽然返回 HTML 的 Web 服务是第四个最常见的用例,但这是由于与工作无关的使用。与返回 HTML 的 Web 服务相比,更多的受访者使用 Go 进行自动化/脚本、代理和守护进程以及工作数据处理。很大一部分最不常用的应用(桌面/GUI 应用、游戏和移动应用)是在工作之外编写的。(GCTT 注:看来 GUI 之类的,还是个人爱好的尝试) + +![Stacked bar charts of proportion of use case is at work, outside of work, or both ](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/app_context.svg) + +另一个新问题是询问受访者对每个用例的满意程度。CLI 满意度最高,85% 的受访者说他们非常、中等或稍微满意使用 Go for cli。一般对 Go 的使用往往有较高的满意度分数,但满意度和受欢迎程度并不完全一致。例如,代理和守护进程的满意度排名第二,但在使用率上排名第六。 + +![Bar chart of satisfaction with each use case](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/app_sat_bin.svg) + +其他的后续问题探讨了不同的用例,例如,开发 CLI 的用户一般使用哪个平台,Linux (93%)和 macOS (59%) 具有很高的代表性并不奇怪,因为 Linux 和 macOS 的开发人员使用频率很高,而且 Linux 云的使用频率也很高) ,但是即使是 Windows 也有近三分之一 CLI 开发人员使用。 + +![Bar chart of platforms being targeted for CLIs](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/cli_platforms.svg) + +对 Go 在数据处理的深入研究表明, Kafka 是唯一被广泛采用的引擎,但大多数受访者表示他们使用的是一个定制的数据处理引擎。 + +![Bar chart of data processing engines used by those who use Go for data processing](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/dpe.svg) + +我们还询问了受访者使用 Go 的其他更大领域。最常见的领域是网络开发(68%) ,其他常见领域包括数据库(46%) ,DevOps (42%)、网络编程(41%)和系统编程(40%)。 + +![Bar chart of the kind of work where Go is being used](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/domain_yoy.svg) + +与去年类似,我们发现 76% 的受访者表示将当前的 Go 版本用于生产用途,但今年我们改进了我们的时间表,发现 60% 的人在新版本发布前或两个月内开始试用新版本。这突出了平台即服务提供商(PaaS)快速支持新的稳定版 Go 的重要性。 + +![Bar chart of how soon respondents begin evaluating a new Go release](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/update_time.svg) + +## 06 Module(模块) + +今年我们发现几乎所有人都采用了 Go 模块,并且只使用模块进行包管理的受访者的比例显著增加。96% 的受访者表示他们正在使用模块管理包,高于去年的 89% 。87% 的受访者表示,他们只使用模块管理包,而去年这一比例为 71% 。同时,其他软件包管理工具的使用也在减少。(GCTT 注:这调查感觉意义不大,这是必然的,官方大力推广,可不用嘛) + +![Bar chart of methods used for Go package management](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/modules_adoption_yoy.svg) + +与去年相比,用户对模块的满意度也有所提高。77% 的受访者表示,他们对模块非常、中等或稍微满意,而 2019 年这一比例为 68% 。(GCTT 注:看来不满意的人也不少) + +![Stacked bar chart of satisfaction with using modules on a 7 point scale from very dissatisfied to very satisfied](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/modules_sat_yoy.svg) + +## 07 官方文档 + +大多数受访者表示,他们对官方文档感到头疼。62% 的受访者难以找到足够的信息来实现他们应用程序的一个特性,超过三分之一的人难以开始做他们以前从未做过的事情。(GCTT 注:看来问题不小) + +![Bar chart of struggles using official Go documentation](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/doc_struggles.svg) + +官方文档中问题最多的领域是使用模块和 CLI 开发,20% 的受访者认为模块文档稍微有点帮助或者根本没有帮助,16% 的受访者认为有关 CLI 开发的文档有帮助。(GCTT 注:所以现在官网上增加了一个模块相关的教程) + +![Stacked bar charts on helpfulness of specific areas of documentation including using modules, CLI tool development, error handling, Web service development, data access, concurrency and file input/output, rated on a 5 point scale from not at all to very helpful](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/doc_helpfulness.svg) + +## 08 云上 Go + +在设计时 Go 就考虑到了现代的分布式计算服务,我们希望继续提高开发者使用 Go 构建云服务的体验。 + +- 全球最大的三家云服务提供商(亚马逊网络服务、谷歌云平台和微软 Azure)的受访者使用率持续上升,而大多数其他云服务提供商的受访者比例每年都在下降。特别是 Azure,从 7% 上升到了 12%。(GCTT 注:阿里云等国内云用的少,多半是国人参与这个调查的不多吧) +- 作为最常见的部署目标,对自有或公司拥有的服务器的 On-prem 部署继续减少; + +![Bar chart of cloud providers used to deploy Go programs where AWS is the most common at 44%](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/cloud_yoy.svg) + +部署到 AWS 和 Azure 的受访者发现,部署到管理的 Kubernetes 平台的受访者增加了,目前分别为 40% 和 54% 。发现将 Go 程序部署到 VMs 的用户比例显著下降,容器使用率从 18% 增长到 25% 。与此同时,GCP (已经有很高比例的受访者报告使用管理的 Kubernetes)部署到 serverless 云的比例从 10% 增长到 17% 。 + +![Bar charts of proportion of services being used with each provider](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/cloud_services_yoy.svg) + +总体而言,大多数受访者对三大主要云供应商都使用 Go 感到满意,而且这些数据与去年相比没有统计上的变化。受访者对 AWS 和 GCP 的 Go 开发的满意程度相当(82%)。Azure 的满意度得分较低(58% 的满意度) ,大家在备注中表示需要对 Azure 的 Go SDK 和 Go 支持 Azure 功能进行改进。 + +![Stacked bar chart of satisfaction with using Go with AWS, GCP and Azure](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/cloud_csat.svg) + +## 09 痛苦的地方 + +受访者表示无法使用 Go 的主要原因是,他们仍然在用另一种语言进行项目(54%) ,在一个更喜欢使用另一种语言的团队工作(34%) ,Go 本身缺乏某些关键特性(26%)。(GCTT 注:看来项目领导很重要) + +今年,我们引入了一个新的选项:“I already use Go everywhere I would like to”,这样受访者就可以不受限制,只要我喜欢 Go,总有可以让我使用的场景。这显著降低了其他选项的选择率,但没有改变它们的相对顺序。我们还引入了“ Go 缺少关键框架”的选项。 + +如果我们只看那些选择不使用 Go 的原因的受访者,我们可以更好地了解每年的趋势。随着时间的推移,「用另一种语言从事现有项目」、「项目/团队/领导对另一种语言的偏好」正在减少。 + +![Bar charts of reasons preventing respondents from using Go more](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/goblockers_yoy_sans_na.svg) + +26% 的受访者认为 Go 缺乏他们需要的语言特性,其中 88% 的人选择了泛型作为一个关键的缺失功能。其他关键的缺失特性是更好的错误处理(58%)、nil 安全(44%)、函数式编程特性(42%)和更强/扩展类型系统(41%)。(GCTT 注:可见泛型呼声是最高的) + +需要明确的是,这些数字来自于那些表示如果不缺少一个或多个关键特性他们将能够更多使用 Go 的受访者,而不是整个调查受访者群体。换个角度来看,18% 的受访者因为缺乏泛型而不能使用 Go。 + +![Bar chart of missing critical features](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/missing_features.svg) + +受访者在使用 Go 时报告的最大挑战仍然是 Go 缺乏泛型(18%) ,而「模块/包管理」和「学习曲线/最佳实践/文档」方面的问题各占 13% 。 + +![Bar chart of biggest challenges respondents face when using Go](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/biggest_challenge.svg) + +## 10 Go 社区 + +今年我们询问了受访者查询 Go 相关问题的 5 大资源。去年我们只要求排名前三,所以结果不能直接比较,然而,StackOverflow 仍然是最受欢迎的资源,占 65% 。阅读源代码(57%)仍然是另一个受欢迎的资源,而对 godoc. org (39%)的依赖已经显著减少。包发现网站 pkg.go.dev 是今年榜单中的新成员,是 32% 的受访者的首选资源。使用 pkg.go.dev 的受访者更有可能同意他们能够快速找到他们需要的 Go 软件包/库:pkg.go.dev 用户占 91% ,而其他用户占 82% 。(GCTT 注:开篇已经总结,也就是说,通过 pkg.go.dev 更容易找到想要的包) + +![Bar chart of top 5 resources respondents use to answer Go-related questions](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/resources.svg) + +多年来,不参加 Go 相关活动的受访者比例呈上升趋势。由于 2019 冠状病毒疾病的缘故,今年我们修改了围绕 Go 活动的问题,发现超过四分之一的受访者比往年花更多的时间在 Go 频道上,14% 的人参加了虚拟 Go 会议,是去年的两倍。64% 参加虚拟活动的人说这是他们第一次参加虚拟活动。(GCTT 注:这里说的 virtual Go Meetup 应该指线上吧) + +![Bar chart of respondents participation in online channels and events](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/events.svg) + +我们发现 12% 的受访者认同传统上代表性不足的群体(例如,种族,性别认同,等等) ,与 2019 年相同,2% 的受访者认同女性,少于 2019 年(3%)。认同代表性不足群体的受访者比不认同代表性不足群体的受访者对“我在 Go 社区感到受欢迎”这句话的不同意率更高(10% 对 4%)。这些问题使我们能够衡量社区的多样性,并突出外联和增长的机会。 + +![Bar chart of underrepresented groups](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/underrep.svg) + +![Bar chart of those who identify as women](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/underrep_groups_women.svg) + +![Bar chart of welcomeness of underrepresented groups](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/welcome_underrep.svg) + +今年,我们增加了一个关于辅助技术使用(assistive technology usage)的问题,发现 8% 的受访者正在使用某种形式的辅助技术。最常用的辅助技术是对比度或颜色设置(2%)。这是一个很好的提醒,我们有需要辅助功能的用户,帮助我们在由 Go 团队管理的网站上做出一些设计决策。 + +![Bar chart of assistive technology usage](https://raw.githubusercontent.com/studygolang/gctt-images/master/2020-go-survey/at.svg) + +团队重视多样性和包容性,不仅仅是因为这是正确的事情,而是因为不同的声音可以照亮我们的盲点,最终使所有用户受益。我们询问敏感信息的方式,包括性别和传统上未被充分代表的群体,已经根据数据隐私条例发生了变化,我们希望这些问题,特别是围绕性别多样性,在未来更具包容性。 + +## 11 总结 + +感谢您 Review 我们的调查结果:我们 2020 开发者调查报告!理解开发人员的经验和挑战有助于我们衡量我们的进展和指导 Go 的未来。再次感谢所有参与这项调查的人,没有你们,我们不可能完成这项工作。我们希望明年见到你! + +> 原文链接: +> +> 国内访问链接: +> +> 编译:GCTT(polarisxu),并非完全直译 diff --git a/published/tech/20130705-how-packages-work-in-go.md b/published/tech/20130705-how-packages-work-in-go.md index c0ace7deb..d547af60f 100644 --- a/published/tech/20130705-how-packages-work-in-go.md +++ b/published/tech/20130705-how-packages-work-in-go.md @@ -10,15 +10,15 @@ http://golang.org/doc/code.html 当我开始用 Go 编程时,这是我最开始读的资料之一。可能因为之前一直在 Visual Studio 中工作,代码被解决方案和项目打包的很好,这个文档中的内容对当时的我来说,完全没法读懂。基于文件系统的目录来工作曾让我认为这是个疯狂的想法。但现在我喜欢上这种简单的方式了,不过可能需要花上一段时间你才会发觉这个方案的合理之处。 -“如何编写 Go 代码”从工作空间的概念讲起。把这个理解为你的项目的根目录。如果你使用Visual Studio,那么它应该是解决方案或者项目文件所在的地方。然后在你的工作空间里面,你需要创建一个名为src的子目录。这个目录是必须的,这样 Go 的工具才能正确运行。在 src 目录里你可以按照个人喜好自由的组织你的代码。但是你需要了解 Go 团队为包和源代码制定的约定,不然你可能要重构你的代码行。 +“如何编写 Go 代码”从工作空间的概念讲起。把这个理解为你的项目的根目录。如果你使用 Visual Studio,那么它应该是解决方案或者项目文件所在的地方。然后在你的工作空间里面,你需要创建一个名为 src 的子目录。这个目录是必须的,这样 Go 的工具才能正确运行。在 src 目录里你可以按照个人喜好自由的组织你的代码。但是你需要了解 Go 团队为包和源代码制定的约定,不然你可能要重构你的代码行。 在我的机器上,我创建了一个工作空间叫 Test ,在其下建立了必要的 src 子目录。这是创建项目的第一步。 ![image](https://raw.githubusercontent.com/studygolang/gctt-images/master/package-work/Screen+Shot+2013-07-28+at+10.03.44+AM.png) -然后在 LiteIDE 中打开Test目录(也就是我的工作空间),然后创建如下的子目录以及空的 Go 源文件。 +然后在 LiteIDE 中打开 Test 目录(也就是我的工作空间),然后创建如下的子目录以及空的 Go 源文件。 -首先,为我们创建的应用建立一个子目录。 main 函数所在的文件夹名称就是编译后的可执行文件的名称。在我们这个项目中,main.go 包含了main函数,并且位于myprogram目录下。这意味着我们的可执行文件名就叫myprogram。 +首先,为我们创建的应用建立一个子目录。 main 函数所在的文件夹名称就是编译后的可执行文件的名称。在我们这个项目中,main.go 包含了 main 函数,并且位于 myprogram 目录下。这意味着我们的可执行文件名就叫 myprogram。 其它 src 目录下的子目录包含了项目中的包。按照约定目录的名称就是这个目录下源文件所属的包的名称,在我这个项目中新的包命名为 samplepkg 和 subpkg ,源文件的名称可以自由命名。 @@ -54,7 +54,7 @@ Go 的设计者在命名他们的包和源文件时已经做了一些事情。 最后,打开 doc.go,format.go,print.go 和 scan.go,它们都在 fmt 包中被声明。 -让我们看看sample。go的代码: +让我们看看 sample。go 的代码: ```go package samplepkg @@ -174,11 +174,11 @@ install 命令会在工作空间中创建 bin 和 pkg 文件夹。注意最终 编译好的包放在 pkg 文件夹下,这个目录下创建了一个目标架构的文件夹,并且把源码目录下的目录结构都复制一份在此文件夹下。 -这些编译好的包都存在,于是go工具可以避免不必要的重新编译。 +这些编译好的包都存在,于是 go 工具可以避免不必要的重新编译。 ![image](https://raw.githubusercontent.com/studygolang/gctt-images/master/package-work/Screen+Shot+2013-07-28+at+10.24.16+AM.png) -在“如何编写Go代码”文章中最后部分讲的问题是,Go 工具在以后编译代码时会忽略所有的 .a 文件。没有源文件你没法编译你的应用。我还没有找到任何文档解释这些 .a 文件如何直接参与 Go 程序构建的。如果有人知道还请不吝赐教。 +在“如何编写 Go 代码”文章中最后部分讲的问题是,Go 工具在以后编译代码时会忽略所有的 .a 文件。没有源文件你没法编译你的应用。我还没有找到任何文档解释这些 .a 文件如何直接参与 Go 程序构建的。如果有人知道还请不吝赐教。 最后,我们最好遵循 Go 设计者制定的这些约定,读 Go 的源码是最好的了解这些约定的方法。有很多人为开源社区写代码,如果我们都遵循相同的约定,我们可以提高代码的兼容性和可读性。当有疑问时 在 /usr/local/go/src/pkg 中挖掘答案吧。 diff --git a/published/tech/20130805-ieee-754-in-go.md b/published/tech/20130805-ieee-754-in-go.md index 5ac62d5f8..cab357c85 100644 --- a/published/tech/20130805-ieee-754-in-go.md +++ b/published/tech/20130805-ieee-754-in-go.md @@ -4,7 +4,7 @@ 在六月份时,Gustavo Niemeyer 在他的博客 [Labix.org](http://blog.labix.org/) 上提出了下面这个问题: -*假设 uf 是一个无符号的64位整型,但包含的内容却是符合 IEEE-754 标准的二进制浮点数,你怎么去区分uf是表示整型还是浮点型呢?* +*假设 uf 是一个无符号的 64 位整型,但包含的内容却是符合 IEEE-754 标准的二进制浮点数,你怎么去区分 uf 是表示整型还是浮点型呢?* 由于我对这方面的了解并不是很多,所以无法快速得出这个问题的答案,而且我也无法用语言来向你解释,只能通过写一个相关的程序来进行示范。幸运的是 Gustavo 发布了答案的思路,我认为非常有意思,然后我将思路进行了分解,这样能更好地理解最初的那个问题。为了更通俗易懂,后面的示例我将使用 32 位的数字。 @@ -39,7 +39,7 @@ IEEE-754 标准不是以 10 进制的方式,而是以 2 进制来储存科学 | :----- | :--------- | :--------- | :--- | :--: | :--: | :--: | | 0.085 | 1.36e-4 | 1.36 * 2-4 | 1.36 | 2 | -4 | .36 | -我们需要用 2 的若干次方除以 10 进制的数字(0.085)来获得一个以 1 开头的分数,“以1开头的分数”是什么意思呢?在示例中我们需要一个看上去像是系数的值 1 + .36。IEEE-754 标准中需要系数以 "1." 开头,这让我们必须储存尾数部分并且需要额外的比特位来表示精度。 +我们需要用 2 的若干次方除以 10 进制的数字(0.085)来获得一个以 1 开头的分数,“以 1 开头的分数”是什么意思呢?在示例中我们需要一个看上去像是系数的值 1 + .36。IEEE-754 标准中需要系数以 "1." 开头,这让我们必须储存尾数部分并且需要额外的比特位来表示精度。 下面我们将采用一种暴力的方法,你会看到最终会得到代表着 0.085 并且以 1 开头的分数: @@ -49,7 +49,7 @@ IEEE-754 标准不是以 10 进制的方式,而是以 2 进制来储存科学 > > 0.085 / 2-3 = 0.68 > -> 0.085 / 2-4 = 1.36   ** 我们找到了以1开头的分数 +> 0.085 / 2-4 = 1.36   ** 我们找到了以 1 开头的分数 -4 这个指数让我们获得了所需的以 1 开头的分数,现在我们已经具备了将 10 进制数字 0.085 储存为 IEEE-754 格式的所有条件。 @@ -62,15 +62,15 @@ IEEE-754 标准不是以 10 进制的方式,而是以 2 进制来储存科学 这些比特位可以分为三个部分,先是一个用来标记符号的比特位,然后是表示指数和分数部分的比特位。我们会将尾数作为二进制分数形式储存在分数比特位中。 -当我们把 0.085 存储为单精度(32位数字)时,IEEE-754的位模式看上去就像这样: +当我们把 0.085 存储为单精度(32 位数字)时,IEEE-754 的位模式看上去就像这样: | 符号位 | 指数位 (123) | 分数位(.36) | | :----: | :----------: | :--------------------------: | | 0 | 0111 1011 | 010 1110 0001 0100 0111 1011 | -最左边的符号位决定了这个数的正负,如果把符号位设置为1,那么这个数就是负数。 +最左边的符号位决定了这个数的正负,如果把符号位设置为 1,那么这个数就是负数。 -接下来的 8 位代表着指数。在我们的示例中,十进制数字 0.085 被转换成以2为底的科学计数法格式 1.36 * 2-4,因此指数为 -4。为了能够表示负数,指数位会有一个偏移值,当数字是 32 位时这个偏移值为 127。我们需要找到一个数,这个数减去偏移值能得到 -4,在我们的示例中这个数为 123。如果你注意一下指数的位模式就会发现这个二进制表示的是数字 123。 +接下来的 8 位代表着指数。在我们的示例中,十进制数字 0.085 被转换成以 2 为底的科学计数法格式 1.36 * 2-4,因此指数为 -4。为了能够表示负数,指数位会有一个偏移值,当数字是 32 位时这个偏移值为 127。我们需要找到一个数,这个数减去偏移值能得到 -4,在我们的示例中这个数为 123。如果你注意一下指数的位模式就会发现这个二进制表示的是数字 123。 剩下的 23 位为分数位,为了得到出分数位的位模式,我们需要对二进制的分数位进行计算和求和,直到求出尾数或者最为接近尾数的值,因为我们假定整数部分一直为 “1.”,所以只要储存尾数部分。 @@ -78,9 +78,9 @@ IEEE-754 标准不是以 10 进制的方式,而是以 2 进制来储存科学 | 二进制 | 分数 | 小数 | 幂 | | :----: | :--: | :---: | :--: | -| 0.1 | 1⁄2 | 0.5 | 2-1 | -| 0.01 | 1⁄4 | 0.25 | 2-2 | -| 0.001 | 1⁄8 | 0.125 | 2-3 | +| 0.1 | 1 ⁄ 2 | 0.5 | 2-1 | +| 0.01 | 1 ⁄ 4 | 0.25 | 2-2 | +| 0.001 | 1 ⁄ 8 | 0.125 | 2-3 | 我们需要设置正确的分数位来累加得到尾数,或者是足够接近尾数的值,这也是为什么有时我们会丢失一些精度。 @@ -88,23 +88,23 @@ IEEE-754 标准不是以 10 进制的方式,而是以 2 进制来储存科学 | 位 | 值 | 分数 | 小数 | 总计 | | :--: | :-----: | :-------: | :--------------: | :--------------: | -| 2 | 4 | 1⁄4 | 0.25 | 0.25 | -| 4 | 16 | 1⁄16 | 0.0625 | 0.3125 | -| 5 | 32 | 1⁄32 | 0.03125 | 0.34375 | -| 6 | 64 | 1⁄64 | 0.015625 | 0.359375 | -| 11 | 2048 | 1⁄2048 | 0.00048828125 | 0.35986328125 | -| 13 | 8192 | 1⁄8192 | 0.0001220703125 | 0.3599853515625 | -| 17 | 131072 | 1⁄131072 | 0.00000762939453 | 0.35999298095703 | -| 18 | 262144 | 1⁄262144 | 0.00000381469727 | 0.3599967956543 | -| 19 | 524288 | 1⁄524288 | 0.00000190734863 | 0.35999870300293 | -| 20 | 1048576 | 1⁄1048576 | 0.00000095367432 | 0.35999965667725 | -| 22 | 4194304 | 1⁄4194304 | 0.00000023841858 | 0.35999989509583 | -| 23 | 8388608 | 1⁄8388608 | 0.00000011920929 | 0.36000001430512 | - -你会看到当这12个比特位排列好之后,我们就得到了0.36这个值,以及后面还带有一些额外的分数。让我们总结一下现在所知道的IEEE-754格式: - -1. 任何10进制的数字都会被储存为基于科学计数法的格式。 -2. 基于2进制的科学计数法必须遵循以1开头的分数格式。 +| 2 | 4 | 1 ⁄ 4 | 0.25 | 0.25 | +| 4 | 16 | 1 ⁄ 16 | 0.0625 | 0.3125 | +| 5 | 32 | 1 ⁄ 32 | 0.03125 | 0.34375 | +| 6 | 64 | 1 ⁄ 64 | 0.015625 | 0.359375 | +| 11 | 2048 | 1 ⁄ 2048 | 0.00048828125 | 0.35986328125 | +| 13 | 8192 | 1 ⁄ 8192 | 0.0001220703125 | 0.3599853515625 | +| 17 | 131072 | 1 ⁄ 131072 | 0.00000762939453 | 0.35999298095703 | +| 18 | 262144 | 1 ⁄ 262144 | 0.00000381469727 | 0.3599967956543 | +| 19 | 524288 | 1 ⁄ 524288 | 0.00000190734863 | 0.35999870300293 | +| 20 | 1048576 | 1 ⁄ 1048576 | 0.00000095367432 | 0.35999965667725 | +| 22 | 4194304 | 1 ⁄ 4194304 | 0.00000023841858 | 0.35999989509583 | +| 23 | 8388608 | 1 ⁄ 8388608 | 0.00000011920929 | 0.36000001430512 | + +你会看到当这 12 个比特位排列好之后,我们就得到了 0.36 这个值,以及后面还带有一些额外的分数。让我们总结一下现在所知道的 IEEE-754 格式: + +1. 任何 10 进制的数字都会被储存为基于科学计数法的格式。 +2. 基于 2 进制的科学计数法必须遵循以 1 开头的分数格式。 3. 整个格式被分为截然不同的三部分。 4. 符号位决定了数字的正负。 5. 指数位表示一个减去偏移量的值。 @@ -248,7 +248,7 @@ INTEGER intTest := coefficient & ((1 << uint32(-exponent)) - 1) ``` -系数的计算需要向尾数部分增加1, 因此我们有了基于2进制的系数值。 +系数的计算需要向尾数部分增加 1, 因此我们有了基于 2 进制的系数值。 当我们查看系数计算的第一部分时,会看到下面的位模式: @@ -262,7 +262,7 @@ bits & ((1 << 23) - 1): 00000000011001010000011011000000 第一部分的系数计算中从 IEEE-754 位模式中移除了符号位和指数位。 -第二部分的计算中会把 “1 +” 加入到位模式中。(注:看图就会明白,就是分数位最高位的前一位进行了或操作变为1) +第二部分的计算中会把 “1 +” 加入到位模式中。(注:看图就会明白,就是分数位最高位的前一位进行了或操作变为 1) ``` coefficient := (bits & ((1 << 23) - 1)) | (1 << 23) diff --git a/published/tech/20130808-using-time-timezones-and-location-in-go.md b/published/tech/20130808-using-time-timezones-and-location-in-go.md index d63e6ee7f..05830259c 100644 --- a/published/tech/20130808-using-time-timezones-and-location-in-go.md +++ b/published/tech/20130808-using-time-timezones-and-location-in-go.md @@ -38,13 +38,13 @@ https://maps.googleapis.com/maps/api/timezone/json?location=38.85682,-92.991714& } ``` -它限制一天只能访问2500次。对于我的潮汐站初始加载,我知道我将达到这个限制,而且我不想等几天再加载所有数据。所有我的商业伙伴从 GeoNames 发现了这个 timezone API。 +它限制一天只能访问 2500 次。对于我的潮汐站初始加载,我知道我将达到这个限制,而且我不想等几天再加载所有数据。所有我的商业伙伴从 GeoNames 发现了这个 timezone API。 如果您打开这个网页您就能读到这个 GeoNames's API 文档: http://www.geonames.org/export/web-services.html#timezone -这个 API 需要一个免费帐号,它相当快就可以设置好。一旦您激活您的帐号,为了使用这个 API您需要找到帐号页去激活您的用户名。 +这个 API 需要一个免费帐号,它相当快就可以设置好。一旦您激活您的帐号,为了使用这个 API 您需要找到帐号页去激活您的用户名。 这是一个简单的 GeoNames API 调用和响应: @@ -68,7 +68,7 @@ http://api.geonames.org/timezoneJSON?lat=47.01&lng=10.2&username=demo 这个 API 返回的信息多一些。而且没有访问限制但是响应时间不能保证。目前我访问了几千次没有遇到问题。 -至此我们有两个不同的 web 请求能帮我们获得 timezone 信息。让我们看看怎么使用 Go 去使用 Google web 请求并获得一个返回对象用在我们的程序中。 +至此我们有两个不同的 Web 请求能帮我们获得 timezone 信息。让我们看看怎么使用 Go 去使用 Google Web 请求并获得一个返回对象用在我们的程序中。 首先,我们需要定义一个新的类型来包含从 API 返回的信息。 @@ -151,7 +151,7 @@ func RetrieveGoogleTimezone(latitude float64, longitude float64) (googleTimezone } ``` -这个 web 请求和错误处理是相当的模式化,所以让我们只简单的谈论下 Unmarshal 调用。 +这个 Web 请求和错误处理是相当的模式化,所以让我们只简单的谈论下 Unmarshal 调用。 ```go rawDocument, err = ioutil.ReadAll(resp.Body) @@ -159,7 +159,7 @@ rawDocument, err = ioutil.ReadAll(resp.Body) err = json.Unmarshal(rawDocument, googleTimezone) ``` -当这个 web 调用返回时,我们获取到响应数据并把它存储在一个字节数组中。然后我们调用这个 json Unmarshal 函数,传递字节数组和一个引用到我们返回的指针类型变量。这个 Unmarshal 调用能创建一个 GoogleTimezone类型对象,从返回的 JSON 文档提取并拷贝数据,然后设置这个值到我们的指针变量。它相当聪明,如果任务字段不能映射就被忽略。如果有异常发 Unmarshal 调用会返回一个错误。 +当这个 Web 调用返回时,我们获取到响应数据并把它存储在一个字节数组中。然后我们调用这个 JSON Unmarshal 函数,传递字节数组和一个引用到我们返回的指针类型变量。这个 Unmarshal 调用能创建一个 GoogleTimezone 类型对象,从返回的 JSON 文档提取并拷贝数据,然后设置这个值到我们的指针变量。它相当聪明,如果任务字段不能映射就被忽略。如果有异常发 Unmarshal 调用会返回一个错误。 所以这很好,我们能得到 timezone 数据并把它解封为一个只有三行代码的对象。现在唯一的问题是我们如何使用 timezoneid 来设置我们的位置? @@ -245,15 +245,15 @@ Otherwise, the name is taken to be a location name corresponding to a file in th The time zone database needed by LoadLocation may not be present on all systems, especially non-Unix systems. LoadLocation looks in the directory or uncompressed zip file named by the ZONEINFO environment variable, if any, then looks in known installation locations on Unix systems, and finally looks in $GOROOT/lib/time/zoneinfo.zip. ``` -如果您读最后一段,您将看到 LoadLocation 函数正读取数据库文件获取信息。我没有下载任何数据库,也没设置名为 ZONEINFO 的环境变量。唯一的答案是在 GOROOT 下的 zoneinfo.zip文件。让我们看下: +如果您读最后一段,您将看到 LoadLocation 函数正读取数据库文件获取信息。我没有下载任何数据库,也没设置名为 ZONEINFO 的环境变量。唯一的答案是在 GOROOT 下的 zoneinfo.zip 文件。让我们看下: ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/using-time-timezones-and-location-in-go/Screen+Shot+2013-08-08+at+8.06.04+PM.png) 果然有个 zoneinfo.zip 文件在 Go 的安装位置下的 lib/time 目录下。非常酷!!! -您有它了。现在您知道如何使用 time.LoadLocation函数来帮助确保您的时间值始终在正确的时区。如果您有经纬度,则可以使用任一 API 获取该时区 ID。 +您有它了。现在您知道如何使用 time.LoadLocation 函数来帮助确保您的时间值始终在正确的时区。如果您有经纬度,则可以使用任一 API 获取该时区 ID。 -如果您想要这两个API 都被调的代码可重用副本的话,我已经在 Github 的 GoingGo 库中添加了一个名为 timezone 的新包。以下是整个工作示例程序: +如果您想要这两个 API 都被调的代码可重用副本的话,我已经在 Github 的 GoingGo 库中添加了一个名为 timezone 的新包。以下是整个工作示例程序: ```go package main diff --git a/published/tech/20130820-Collections-Of-Unknown-Length-in-Go.md b/published/tech/20130820-Collections-Of-Unknown-Length-in-Go.md index d49502852..4437016e8 100644 --- a/published/tech/20130820-Collections-Of-Unknown-Length-in-Go.md +++ b/published/tech/20130820-Collections-Of-Unknown-Length-in-Go.md @@ -12,13 +12,13 @@ ![slice-copy](https://raw.githubusercontent.com/studygolang/gctt-images/master/Collections-Of-Unknown-Length-in-Go/slice-copy.png) -我一直在想 go 是如何创建大量新的切片值和底层数组做大量内存分配,并且不断进行复制值。然后垃圾回收器会因为所有这些小变量被创建和销毁而过度工作。 +我一直在想 Go 是如何创建大量新的切片值和底层数组做大量内存分配,并且不断进行复制值。然后垃圾回收器会因为所有这些小变量被创建和销毁而过度工作。 我无法想象需要做数千次这种操作。其实有更好的方法或更效率的方式我没有意识到。 在研究并提出了很多问题之后,我得出的结论是,在大多数实际情况下,使用切片比使用链表更好。这就是为什么语言设计者花时间使切片尽可能高效工作,并且没有引入集合类型的原因。 -我们可以连续几天讨论各种边界情况和性能问题,但 go 希望我们使用切片。因此切片应该是我们的首选,除非代码告诉我们存在问题。掌握切片就像学国际象棋游戏,易于学习但需要一辈子才能成为大师。因为底层数组可以共享,所以在使用中需要注意一些问题。 +我们可以连续几天讨论各种边界情况和性能问题,但 Go 希望我们使用切片。因此切片应该是我们的首选,除非代码告诉我们存在问题。掌握切片就像学国际象棋游戏,易于学习但需要一辈子才能成为大师。因为底层数组可以共享,所以在使用中需要注意一些问题。 在继续阅读之前,你最好看一下我的另一篇文章 [Understanding Slices in Go Programming](http://www.goinggo.net/2013/08/understanding-slices-in-go-programming.html)。 @@ -93,7 +93,7 @@ var red []Record var blue []Record ``` -一个空切片长度和容量都是0,并且不存在底层数组。我们可以使用内置的 `append` 函数向切片中增加数据。 +一个空切片长度和容量都是 0,并且不存在底层数组。我们可以使用内置的 `append` 函数向切片中增加数据。 ```go red = append(red, record) @@ -105,9 +105,9 @@ blue = append(blue, record) Kevin Gillette 在我的小组讨论中进行了说明: (https://groups.google.com/forum/#!topic/golang-nuts/nXYuMX55b6c) -在 go 语音规范中规定,前几千个元素在容量增长的时候每次都将容量翻倍,然后以~1.25的速率进行容量增长。 +在 Go 语音规范中规定,前几千个元素在容量增长的时候每次都将容量翻倍,然后以 ~1.25 的速率进行容量增长。 -我不是学者,但我看到使用波浪号(~)相当多。有些人也许不知道这是什么意思,这里表示大约。因此,`append` 函数会增加底层数组的容量并为未来的增长预留空间。最终 `append` 函数将大约以1.25或25%的系数进行容量增长。 +我不是学者,但我看到使用波浪号(~)相当多。有些人也许不知道这是什么意思,这里表示大约。因此,`append` 函数会增加底层数组的容量并为未来的增长预留空间。最终 `append` 函数将大约以 1.25 或 25%的系数进行容量增长。 让我们证明 `append` 函数增长容量并高效运行: @@ -155,7 +155,7 @@ Index[256] Len[257] Cap[512] - Ran Out Of Room, Double Capacity Index[512] Len[513] Cap[1024] - Ran Out Of Room, Double Capacity Index[1024] Len[1025] Cap[1280] - Ran Out Of Room, Grow by a factor of 1.25 ``` -如果我们观察容量值,我们可以看到 Kevin 是绝对正确的。容量正如他所说的那样在增长。在前1千的元素中,容量增加了一倍。然后容量以1.25或25%的系数增长。这意味着以这种方式使用切片将满足我们在大多数情况下所需的性能,并且内存不会成为问题。 +如果我们观察容量值,我们可以看到 Kevin 是绝对正确的。容量正如他所说的那样在增长。在前 1 千的元素中,容量增加了一倍。然后容量以 1.25 或 25%的系数增长。这意味着以这种方式使用切片将满足我们在大多数情况下所需的性能,并且内存不会成为问题。 最初我认为会为每次调用 `append` 时都会创建一个新的切片值,但事实并非如此。当我们调用 `append` 时,在栈中复制了 `red` 副本。然后当 `append` 返回时,会再进行一次复制操作,但使用的我们已有的内存。 @@ -173,7 +173,7 @@ http://dominik.honnef.co/go-tip/ http://dominik.honnef.co/go-tip/2013-08-23/#slicing -你可以用切片做很多的事情,甚至可以写一整本关于这个主题的书。就像我之前说的那样,切片就像国际象棋一样,易于学习但需要一辈子才能成为大师。如果您来自其他语言,如 C# 和 Java,那么请拥抱切片并使用它。这正是 go 中正确的方式。 +你可以用切片做很多的事情,甚至可以写一整本关于这个主题的书。就像我之前说的那样,切片就像国际象棋一样,易于学习但需要一辈子才能成为大师。如果您来自其他语言,如 C# 和 Java,那么请拥抱切片并使用它。这正是 Go 中正确的方式。 --- diff --git a/published/tech/20130820-Using-C-Dynamic-Libraries-In-Go-Programs.md b/published/tech/20130820-Using-C-Dynamic-Libraries-In-Go-Programs.md index f246ed0d5..c8dc66ef0 100644 --- a/published/tech/20130820-Using-C-Dynamic-Libraries-In-Go-Programs.md +++ b/published/tech/20130820-Using-C-Dynamic-Libraries-In-Go-Programs.md @@ -8,7 +8,7 @@ 我钟爱这台电脑,回想起曾经使用 BASIC 在上面日日夜夜开发游戏,它非常便携,把键盘折叠起来就可以提着走,哈哈。 -额,我好像偏题了,还是回到Go上面来。我发现一种使用 VT100 控制符来显示简单屏幕的方法,并且在上面开始写一些业务逻辑。 +额,我好像偏题了,还是回到 Go 上面来。我发现一种使用 VT100 控制符来显示简单屏幕的方法,并且在上面开始写一些业务逻辑。 但随后就遇到了一些艰难的问题,我要用倒叙的方式来描述一下,比如当不按回车键时,我就没办法从标准输入中获取数据,啊啊啊啊啊,为了寻找解决方案,我整个周末都在阅读资料,甚至找到两个相关的 Go 语言库,但是并没有起到什么作用。后来我意识到,如果要实现这个效果,那么要使用 C 语言来编写功能函数,链接成动态库后再由 Go 调用。 @@ -102,7 +102,7 @@ build: gcc -c test.c ``` -接下来,gcc 会把 test.o 和 libncurses.dylib 进行链接处理,链接后会生成 test 可执行文件。命令中的 l(小写的 L)参数是让 gcc 去链接 libncurses.dylib 文件,-r(小写 R)参数指定了 gcc 去哪个路径下获取这个库文件,-o(小写的 O)参数是指定 gcc 导出可执行文件的名字,最后让gcc在链接操作中包含 test.o。 +接下来,gcc 会把 test.o 和 libncurses.dylib 进行链接处理,链接后会生成 test 可执行文件。命令中的 l(小写的 L)参数是让 gcc 去链接 libncurses.dylib 文件,-r(小写 R)参数指定了 gcc 去哪个路径下获取这个库文件,-o(小写的 O)参数是指定 gcc 导出可执行文件的名字,最后让 gcc 在链接操作中包含 test.o。 ```makefile gcc -lncurses -r/usr/lib -o test test.o @@ -122,13 +122,13 @@ http://www.adp-gmbh.ch/cpp/gcc/create_lib.html http://stackoverflow.com/questions/3532589/how-to-build-a-dylib-from-several-o-in-mac-os-x-using-gcc -让我们在Go中实现这一切吧,先来建立一个新的工程: +让我们在 Go 中实现这一切吧,先来建立一个新的工程: ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/Using-C-Dynamic-Libraries-In-Go-Programs/Screen%2BShot%2B2013-08-20%2Bat%2B6.48.52%2BPM.png) 我建立了一个名叫 Keyboard 的文件夹,里面有两个子文件夹,分别叫 DyLib 和 TestApp。 -在 DyLib 文件夹中我们放入C的动态库源码和 makefile 文件,在 TestApp 中只有一个 main.go 文件,到时就使用这个文件来测试 Go 和 C 语言的动态库交互。 +在 DyLib 文件夹中我们放入 C 的动态库源码和 makefile 文件,在 TestApp 中只有一个 main.go 文件,到时就使用这个文件来测试 Go 和 C 语言的动态库交互。 这是为动态库准备的 C 头文件,和之前 test 头文件中的内容一样: @@ -165,7 +165,7 @@ void CloseKeyboard() { } ``` -接下来是为创建动态库准备的makefile文件: +接下来是为创建动态库准备的 makefile 文件: makefile diff --git a/published/tech/20130914-Pool-Go-Routines-To-Process-Task-Oriented-Work.md b/published/tech/20130914-Pool-Go-Routines-To-Process-Task-Oriented-Work.md index e25cc370b..d0cc2feaf 100644 --- a/published/tech/20130914-Pool-Go-Routines-To-Process-Task-Oriented-Work.md +++ b/published/tech/20130914-Pool-Go-Routines-To-Process-Task-Oriented-Work.md @@ -12,17 +12,17 @@ ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/pool-go/1.png) -如上图所示,主业务例程提交了100个任务到工作池中。工作池将它们都排入队列,当一个 Goroutine 空闲,工作池从任务队列中取出一个任务分配到此 Goroutine 上,此任务将会得到执行。执行完毕后此 Goroutine 将会再次空闲并等待处理其他任务。Goroutines 的数量和队列的容量是可配置的,这意味着工作池可以用于程序的性能调节。 +如上图所示,主业务例程提交了 100 个任务到工作池中。工作池将它们都排入队列,当一个 Goroutine 空闲,工作池从任务队列中取出一个任务分配到此 Goroutine 上,此任务将会得到执行。执行完毕后此 Goroutine 将会再次空闲并等待处理其他任务。Goroutines 的数量和队列的容量是可配置的,这意味着工作池可以用于程序的性能调节。 Go 语言使用 Goroutine 替代了线程。Go 运行环境管理了一个内部的线程池并且在这个池内调度 Goroutines。线程池是最小化 Go 运行环境的负载和最大化程序性能的关键手段。当我们创建了一个新的 Goroutine 时,Go 运行环境将在内部线程池中管理和调度这个 Goroutine。这个原理就和操作系统在空闲的 CPU 核心上调度线程一样。通过 Goroutine 我们可以获得同调度线程池一样的效果,甚至可能更好。 对于处理基于任务的操作我有一个简单的原则:少即是多。我总是想要知道对于特定操作,最好的结果需要的 Goroutines 的最小值是多少。最好的结果不仅仅是全部的任务需要花费多长时间来完成,同样还包括处理这些任务对程序、系统和平台所产生的影响。你必须同时考虑到短期影响和长期影响。 -在系统或程序负载较轻的情况下,我们很容易就能获取到非常快的处理速度。但是某天系统负荷的轻微增加就会导致之前的配置不起作用,而我们并没有意识到正是我们在严重伤害和我们交互的系统。我们可能把数据库或者网络服务器用的太狠了,最终造成了系统的宕机。突发的100个并发任务可以运行正常,但是持续一个小时的并发可能就是致命的。 +在系统或程序负载较轻的情况下,我们很容易就能获取到非常快的处理速度。但是某天系统负荷的轻微增加就会导致之前的配置不起作用,而我们并没有意识到正是我们在严重伤害和我们交互的系统。我们可能把数据库或者网络服务器用的太狠了,最终造成了系统的宕机。突发的 100 个并发任务可以运行正常,但是持续一个小时的并发可能就是致命的。 工作池并不是可以解决全世界运算问题的魔力仙女,它却可以用在你的程序中处理基于任务的操作。它可以根据你的系统表现提供配置选项和控制功能。随着系统变化,你也有足够的灵活度来改变。 -现在让我们举个例子来证明在处理基于任务的操作方面工作池要比盲目的产生 Goroutines 更有效率。我们的测试程序运行某一个任务,它会获取一个 MongoDB 的连接,在数据库上执行查询命令并返回数据。一般的业务中都会有类似的功能。这个测试程序将会提交100个任务到工作池中,运行5次后统计平均运行时间。 +现在让我们举个例子来证明在处理基于任务的操作方面工作池要比盲目的产生 Goroutines 更有效率。我们的测试程序运行某一个任务,它会获取一个 MongoDB 的连接,在数据库上执行查询命令并返回数据。一般的业务中都会有类似的功能。这个测试程序将会提交 100 个任务到工作池中,运行 5 次后统计平均运行时间。 打开终端,运行如下的命令来下载代码: @@ -38,7 +38,7 @@ cd $HOME/example/bin ./workpooltest 100 off ``` -第一个参数告诉程序创建100个 Goroutines 的工作池,第二个参数告诉程序关闭详细的日志输出。 +第一个参数告诉程序创建 100 个 Goroutines 的工作池,第二个参数告诉程序关闭详细的日志输出。 在我的 Macbook 上,运行上面这个命令的结果是: @@ -210,10 +210,10 @@ func New(numberOfRoutines int, queueCapacity int32) *WorkPool { for workRoutine := 0; workRoutine < numberOfRoutines; workRoutine++ { workPool.shutdownWaitGroup.Add(1) - go workPool.workRoutine(workRoutine) + Go workPool.workRoutine(workRoutine) } - go workPool.queueRoutine() + Go workPool.queueRoutine() return &workPool } ``` @@ -224,9 +224,9 @@ func New(numberOfRoutines int, queueCapacity int32) *WorkPool { http://golang.org/doc/effective_go.html#channels -当 channel 初始化完毕后,我们就可以去创建 Goroutines 了。首先我们对每个 Goroutine 的 WaitGroup 加1来关闭它们。接着创建 Goroutines。最后开启 QueueRoutine 来接收工作。 +当 channel 初始化完毕后,我们就可以去创建 Goroutines 了。首先我们对每个 Goroutine 的 WaitGroup 加 1 来关闭它们。接着创建 Goroutines。最后开启 QueueRoutine 来接收工作。 -要学习关闭Goroutines的代码和WaitGroup是如何工作的,请阅读此链接: +要学习关闭 Goroutines 的代码和 WaitGroup 是如何工作的,请阅读此链接: http://dave.cheney.net/2013/04/30/curious-channels @@ -247,7 +247,7 @@ func (wp *WorkPool) Shutdown(goRoutine string) { } ``` -Shutdown函数首先关闭 QueueRoutine,这样就不会接收更多的请求。接着关闭 ShutdownWorkChannel,并等待每个 Goroutine 去对 WaitGroup 计数器做减操作。一旦最后一个 Goroutine 调用了 Done 函数,等待函数 Wait 将会返回,工作池将会被关闭。 +Shutdown 函数首先关闭 QueueRoutine,这样就不会接收更多的请求。接着关闭 ShutdownWorkChannel,并等待每个 Goroutine 去对 WaitGroup 计数器做减操作。一旦最后一个 Goroutine 调用了 Done 函数,等待函数 Wait 将会返回,工作池将会被关闭。 现在让我们看看 PostWork 和 QueueRoutine 函数: diff --git a/published/tech/20130914-Timer-Routines-And-Graceful-Shutdowns-In-Go.md b/published/tech/20130914-Timer-Routines-And-Graceful-Shutdowns-In-Go.md index 279408552..a5e2fa736 100644 --- a/published/tech/20130914-Timer-Routines-And-Graceful-Shutdowns-In-Go.md +++ b/published/tech/20130914-Timer-Routines-And-Graceful-Shutdowns-In-Go.md @@ -2,7 +2,7 @@ # Go 语言中的 Timer Routines 与优雅退出 -在我的 Outcast(译注:作者自己做的一款天气预告 App) 数据服务器中,有几个数据检索任务要用到不同的 Go routine 来运行, 每个 routine 在设定的时间间隔内唤醒。 其中最复杂的工作是下载雷达图像。 它复杂的原因在于:美国有 155 个雷达站,它们每 120 秒拍摄一张新照片, 我们要把所有的雷达图像拼接在一起形成一张大的拼接图。(译注:有点像我们用手机拍摄全景图片时,把多张边缘有重叠的图片拼接成一张大图片) 当 go routine 被唤醒去下载新图像时,它必须尽快为所有 155 个站点都执行这个操作。 如果不够及时的话,得到拼接图将不同步,每个雷达站重叠的边界部分会对不齐。 +在我的 Outcast(译注:作者自己做的一款天气预告 App) 数据服务器中,有几个数据检索任务要用到不同的 Go routine 来运行, 每个 routine 在设定的时间间隔内唤醒。 其中最复杂的工作是下载雷达图像。 它复杂的原因在于:美国有 155 个雷达站,它们每 120 秒拍摄一张新照片, 我们要把所有的雷达图像拼接在一起形成一张大的拼接图。(译注:有点像我们用手机拍摄全景图片时,把多张边缘有重叠的图片拼接成一张大图片) 当 Go routine 被唤醒去下载新图像时,它必须尽快为所有 155 个站点都执行这个操作。 如果不够及时的话,得到拼接图将不同步,每个雷达站重叠的边界部分会对不齐。 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/Timer-Routines-And-Graceful-Shutdowns-In-Go/radar-img-1.png) @@ -14,9 +14,9 @@ 蓝色是雷达的噪点,我们会把它给过滤掉,所以我们剩下绿色、红色和黄色的色块来表示真正的天气状况。上面的图片是在下午 4:46 下载并处理好的,你可以看到他们很接近,能够很好的拼接在一起。 -我们的代码的第一个实现中,使用了单个 go routine,每10分钟唤醒一次,每次这个 go routine 唤醒,它都需要 3 到 4 分钟时间下载、处理、保存并把 155 个雷达站的数据写入的到 mongo 里面去。虽然我会把每个地区的图片 尽可能地拼接起来,但是这些图片存在的延迟差异实在是太大了。每个雷达站都存在一两分钟的延迟,所有的雷达站的延迟叠加起来,使这个问题凸显出来。 +我们的代码的第一个实现中,使用了单个 Go routine,每 10 分钟唤醒一次,每次这个 Go routine 唤醒,它都需要 3 到 4 分钟时间下载、处理、保存并把 155 个雷达站的数据写入的到 mongo 里面去。虽然我会把每个地区的图片 尽可能地拼接起来,但是这些图片存在的延迟差异实在是太大了。每个雷达站都存在一两分钟的延迟,所有的雷达站的延迟叠加起来,使这个问题凸显出来。 -对于所有工作,我都会尽可能地使用单个 go routine 来实现,因为这样能让事情保持简单。但在这个情况下,单一 go routine 并不能凑效。我必须同时处理多个雷达站的数据,来减少延迟造成的差异。在我添加了一个工作池来处理同时多个雷达站的数据后,我能够在一分钟之内把 155 个雷达站的数据都处理好了。目前为止,我还没收到客户端开发团队的抱怨。 +对于所有工作,我都会尽可能地使用单个 Go routine 来实现,因为这样能让事情保持简单。但在这个情况下,单一 Go routine 并不能凑效。我必须同时处理多个雷达站的数据,来减少延迟造成的差异。在我添加了一个工作池来处理同时多个雷达站的数据后,我能够在一分钟之内把 155 个雷达站的数据都处理好了。目前为止,我还没收到客户端开发团队的抱怨。 在篇文章里面,我们主要关注定时 routine 和退出的代码。在下一个文章,我会告诉你怎么去为你的项目添加一个工作池。 @@ -32,7 +32,7 @@ cd example/bin Outcast 数据服务器是个单应用程序,它设计为长期运行的服务程序,这种类型的程序很少会需要退出。让你的程序能在需要的时候优雅地退出是很重要的。当我在开发这种类型的程序时,我总是要从开始就确保,我可以通过某些信号通知应用程序退出,并且不会让它挂起。一个程序,最糟糕的事情莫过于需要你强制杀死进程才能退出了。 -示例程序创建了一个单一的 go routine 并且指定这个 routine 每 15 秒唤醒一次. 当 routine 唤醒的时候,它会进行一个大概耗时 10 秒的操作。当工作完成以后,它再计算需要睡多少秒,来确保这个 routine 能够保持每 15 秒唤醒一次。 +示例程序创建了一个单一的 Go routine 并且指定这个 routine 每 15 秒唤醒一次. 当 routine 唤醒的时候,它会进行一个大概耗时 10 秒的操作。当工作完成以后,它再计算需要睡多少秒,来确保这个 routine 能够保持每 15 秒唤醒一次。 让我们试试运行这个程序并且在它运行的时候把它退出掉。然后我们就可以开始学习它是怎么实现的。我们可以在程序运行的任何时候,按回车键来退出这个程序。 @@ -106,7 +106,7 @@ func (wm *WorkManager) WorkTimer() { 为了更加简洁易读,我把注释和输出日志的代码去掉了。这是一个经典的作业队列 channel, 并且这个解决方案非常的优雅。比起用 C# 实现的同样的东西,优雅多了。 -`WorkTimer()` 函数作为一个 go routine 运行: +`WorkTimer()` 函数作为一个 Go routine 运行: ```go func Startup() { @@ -115,7 +115,7 @@ func Startup() { ShutdownChannel: make(chan string), } - go wm.WorkTimer() + Go wm.WorkTimer() } ``` @@ -180,7 +180,7 @@ func (wm *_WorkManager) PerformTheWork() { 这就是我的定时 routine 和优雅退出程序的代码模式,你也可以把这个模式应用在你的程序中。如果你从 GoingGo 的代码仓库下载了整个示例的话,你可以看到实战的代码和一些小工具。 -阅读下面的文章可以学习到怎么实现一个能够处理多个 go routine 的工作池,正如我上述的处理雷达图像的那个工作池一样: +阅读下面的文章可以学习到怎么实现一个能够处理多个 Go routine 的工作池,正如我上述的处理雷达图像的那个工作池一样: https://studygolang.com/articles/14481 diff --git a/published/tech/20130923-Iterating-Over-Slices-In-Go.md b/published/tech/20130923-Iterating-Over-Slices-In-Go.md index 0bf082101..8f1f36701 100644 --- a/published/tech/20130923-Iterating-Over-Slices-In-Go.md +++ b/published/tech/20130923-Iterating-Over-Slices-In-Go.md @@ -16,7 +16,7 @@ 能够使用指针有优势,但也可以让你陷入困境。使用指针可以减轻内存约束并尽可能的提高性能。但它会创造同步问题,例如对值和资源的共享访问。找到每个用例最适合的解决方案。对于你的 Go 程序,我建议在安全和实用的时候使用指针。Go 是一种命令式编程语言,所以利用好它的这些优势。 -在 Go 中,一切都是按值传递的,记住这一点非常重要。我们可以通过值传递对象的地址,或者通过值传递对象的副本。当我们在 Go 中使用指针时,它有时会令人混淆,因为 Go 处理我们的所有引用。不要误会我的意思,Go做到这一点非常棒,但有时候你可以忘记变量的实际值。 +在 Go 中,一切都是按值传递的,记住这一点非常重要。我们可以通过值传递对象的地址,或者通过值传递对象的副本。当我们在 Go 中使用指针时,它有时会令人混淆,因为 Go 处理我们的所有引用。不要误会我的意思,Go 做到这一点非常棒,但有时候你可以忘记变量的实际值。 在每个程序的某个时刻,我需要迭代一个切片来执行一些工作。在 Go 中,我们使用 for 循环结构来迭代切片。在开始时,我在迭代切片时犯了一些非常严重的错误,因为我误解了 range 关键字是如何工作的。我将向您展示一个令人讨厌的错误,我创建了一个让我困惑的迭代切片的功能。现在对我来说很明显为什么代码执行结果不对,但当时并不知道原因。 @@ -76,7 +76,7 @@ Name: Sammy Age: 10 Addr: 0x2101bc060 ``` -那么为什么狗的值在循环内是不同的,为什么同一个地址出现两次呢?这一切都与 Go 的值传递的事实有关。在这个代码示例中,我们实际上在内存中创建了每个 Dog 的2个额外副本。 +那么为什么狗的值在循环内是不同的,为什么同一个地址出现两次呢?这一切都与 Go 的值传递的事实有关。在这个代码示例中,我们实际上在内存中创建了每个 Dog 的 2 个额外副本。 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/iterating-over-slices-in-go/iterating-over-slices-in-go.png) @@ -191,7 +191,7 @@ for _, dog := range dogs { 无论我们是否使用指针, Go 都会处理对 Dog 值的访问。这很棒,但有时会导致一些混乱。至少这对我来说开始的时候是这样的。 -我不能告诉你何时应该使用指针或何时应该使用副本。但请记住, Go 将按价值传递一切。这包括函数参数,返回值以及在切片、 map 或 channel上迭代时。 +我不能告诉你何时应该使用指针或何时应该使用副本。但请记住, Go 将按价值传递一切。这包括函数参数,返回值以及在切片、 map 或 channel 上迭代时。 是的,你也可以遍历一个 channel 。看看我在 Ewen Cheslack-Postava 撰写的博客文章中改编的示例代码: diff --git a/published/tech/20130926-detecting-race-conditions-with-go.md b/published/tech/20130926-detecting-race-conditions-with-go.md index 2405eda14..2ba3ebfe5 100644 --- a/published/tech/20130926-detecting-race-conditions-with-go.md +++ b/published/tech/20130926-detecting-race-conditions-with-go.md @@ -26,7 +26,7 @@ func main() { for routine := 1; routine <= 2; routine++ { Wait.Add(1) - go Routine(routine) + Go Routine(routine) } Wait.Wait() @@ -59,13 +59,13 @@ go build -race ``` ================== WARNING: DATA RACE -Read by goroutine 5: +Read by Goroutine 5: main.Routine() /Users/bill/Spaces/Test/src/test/main.go:29 +0x44 gosched0() /usr/local/go/src/pkg/runtime/proc.c:1218 +0x9f -Previous write by goroutine 4: +Previous write by Goroutine 4: main.Routine() /Users/bill/Spaces/Test/src/test/main.go:33 +0x65 gosched0() @@ -93,7 +93,7 @@ Found 1 data race(s) 警告报告告诉我们问题发生的准确位置: ``` -Read by goroutine 5: +Read by Goroutine 5: main.Routine() /Users/bill/Spaces/Test/src/test/main.go:29 +0x44 gosched0() @@ -101,7 +101,7 @@ Read by goroutine 5: value := Counter -Previous write by goroutine 4: +Previous write by Goroutine 4: main.Routine() /Users/bill/Spaces/Test/src/test/main.go:33 +0x65 gosched0() @@ -115,7 +115,7 @@ Goroutine 5 (running) created at: runtime.main() /usr/local/go/src/pkg/runtime/proc.c:182 +0x91 - go Routine(routine) + Go Routine(routine) ``` 你能发现竞争检测器指出两行读和写全局变量 Counter 的代码。同时也指出生成协程的代码。 @@ -139,7 +139,7 @@ func main() { for routine := 1; routine <= 2; routine++ { Wait.Add(1) - go Routine(routine) + Go Routine(routine) } Wait.Wait() @@ -192,7 +192,7 @@ Counter = value 在每一次循环的迭代过程中,全局变量 Counter 的值都被暂存到本地变量 value,本地的副本自增后,最终写回全局变量 Counter。如果这三行代码在没有中断的情况下,没有立即运行,那么程序就会出现问题。上面的图片展示了全局变量 Counter 的读取和上下文切换是如何导致问题的。 -在这幅图中,在被协程 1 增加的变量被写回全局变量 Counter 之前,协程 2 被唤醒并读取全局变量 Counter。实质上,这两个协程对全局Counter变量执行完全相同的读写操作,因此最终的结果才是 2。 +在这幅图中,在被协程 1 增加的变量被写回全局变量 Counter 之前,协程 2 被唤醒并读取全局变量 Counter。实质上,这两个协程对全局 Counter 变量执行完全相同的读写操作,因此最终的结果才是 2。 为了解决这个问题,你也许认为我们只需要将增加全局变量 Counter 的三行代码改写减少到一行即可。 @@ -213,7 +213,7 @@ func main() { for routine := 1; routine <= 2; routine++ { Wait.Add(1) - go Routine(routine) + Go Routine(routine) } Wait.Wait() @@ -250,13 +250,13 @@ go build -race ``` ================== WARNING: DATA RACE -Write by goroutine 5: +Write by Goroutine 5: main.Routine() /Users/bill/Spaces/Test/src/test/main.go:30 +0x44 gosched0() /usr/local/go/src/pkg/runtime/proc.c:1218 +0x9f -Previous write by goroutine 4: +Previous write by Goroutine 4: main.Routine() /Users/bill/Spaces/Test/src/test/main.go:30 +0x44 gosched0() @@ -283,7 +283,7 @@ Found 1 data race(s) 然而,在这三十行代码的程序中,我们仍然检测到一个竞争条件。 ``` -Write by goroutine 5: +Write by Goroutine 5: main.Routine() /Users/bill/Spaces/Test/src/test/main.go:30 +0x44 gosched0() @@ -291,7 +291,7 @@ Write by goroutine 5: Counter = Counter + 1 -Previous write by goroutine 4: +Previous write by Goroutine 4: main.Routine() /Users/bill/Spaces/Test/src/test/main.go:30 +0x44 gosched0() @@ -305,10 +305,10 @@ Goroutine 5 (running) created at: runtime.main() /usr/local/go/src/pkg/runtime/proc.c:182 +0x91 - go Routine(routine) + Go Routine(routine) ``` -使用一行代码进行增加操作的程序正确地运行了。但为什么代码仍然有一个竞态条件? 不要被我们用于递增 Counter 变量的一行Go代码所欺骗。让我们看看这一行代码生成的汇编代码: +使用一行代码进行增加操作的程序正确地运行了。但为什么代码仍然有一个竞态条件? 不要被我们用于递增 Counter 变量的一行 Go 代码所欺骗。让我们看看这一行代码生成的汇编代码: ``` 0064 (./main.go:30) MOVQ Counter+0(SB),BX ; Copy the value of Counter to BX @@ -340,7 +340,7 @@ func main() { for routine := 1; routine <= 2; routine++ { Wait.Add(1) - go Routine(routine) + Go Routine(routine) } Wait.Wait() diff --git a/published/tech/20131017-My-Channel-Select-Bug.md b/published/tech/20131017-My-Channel-Select-Bug.md index 0a19177ce..7598f469d 100644 --- a/published/tech/20131017-My-Channel-Select-Bug.md +++ b/published/tech/20131017-My-Channel-Select-Bug.md @@ -30,7 +30,7 @@ func main() { case <-func() chan struct{} { complete := make(chan struct{}) - go LaunchProcessor(complete) + Go LaunchProcessor(complete) return complete }(): return @@ -70,7 +70,7 @@ func LaunchProcessor(complete chan struct{}) { ```go case <-func() chan struct{} { complete := make(chan struct{}) - go LaunchProcessor(complete) + Go LaunchProcessor(complete) return complete }(): ``` @@ -114,7 +114,7 @@ func main() { signal.Notify(sigChan, os.Interrupt) complete := make(chan struct{}) - go LaunchProcessor(complete) + Go LaunchProcessor(complete) for { @@ -235,7 +235,7 @@ func main() { signal.Notify(sigChan, os.Interrupt) complete := make(chan struct{}) - go LaunchProcessor(complete) + Go LaunchProcessor(complete) for { diff --git a/published/tech/20131105-Using-The-Log-Package-In-Go.md b/published/tech/20131105-Using-The-Log-Package-In-Go.md index 1f63e03dc..e14b18ca6 100644 --- a/published/tech/20131105-Using-The-Log-Package-In-Go.md +++ b/published/tech/20131105-Using-The-Log-Package-In-Go.md @@ -130,7 +130,7 @@ Info.Println("Special Information") 对于 Info (译注:消息记录器),os.Stdout 传入到 init 函数给了 infoHandle 。这意味着当您使用 Info 写消息时,消息将通过标准输出显示在终端窗口中。 -最后,看下 Error 的代码: +最后, 看下 Error 的代码: ```go var Error *log.Logger @@ -235,13 +235,13 @@ func main() { } ``` -现在终端窗口上不会显示任何内容。您可以使用任何支持io.Writer 接口的目标。 +现在终端窗口上不会显示任何内容。您可以使用任何支持 io.Writer 接口的目标。 基于这个例子,我为我的所有程序编写了一个新的日志包: go get github.com/goinggo/tracelog -我希望在开始编写Go程序时我就知道 log 和 loggers 。期待将来能够看到我写的更多日志包。 +我希望在开始编写 Go 程序时我就知道 log 和 loggers 。期待将来能够看到我写的更多日志包。 --- diff --git a/published/tech/20140823-Testing-Web-Apps-in-Go.md b/published/tech/20140823-Testing-Web-Apps-in-Go.md index 947c1252e..390365b6d 100644 --- a/published/tech/20140823-Testing-Web-Apps-in-Go.md +++ b/published/tech/20140823-Testing-Web-Apps-in-Go.md @@ -159,7 +159,7 @@ function TestHome(t *testing.T) { 更多相关的详细用法,可以[点击](https://github.com/markberger/carton/blob/master/api/auth_test.go)查看我的小项目。 -如果你有更好的方法来利用标准库进行 web 应用测试,你可以随时进行留言或给我发 email。 +如果你有更好的方法来利用标准库进行 Web 应用测试,你可以随时进行留言或给我发 email。 --- diff --git a/published/tech/20150223-Scheduler-Tracing-In-Go.md b/published/tech/20150223-Scheduler-Tracing-In-Go.md index 0022436ea..c12161da0 100644 --- a/published/tech/20150223-Scheduler-Tracing-In-Go.md +++ b/published/tech/20150223-Scheduler-Tracing-In-Go.md @@ -58,9 +58,9 @@ func work(wg *sync.WaitGroup) { 清单 1 中的例子是为了演示运行时调试器给我们的调试信息。在第 12 秒 for 循环进行 10 次 goroutines。然后主函数在第 16 行的时候等待所有 goroutines 执行完成。在第 22 行 work 函数里面先 sleep 一秒然后 counter 变量 ++ 执行一百亿次。当 for 循环执行完成后调用 Done 方法最后 return。 -在设置 GODEBUG 之前,先用 go build 编译代码。这个变量由运行时获取,因此运行 Go 命令也将产生跟踪输出。如果 GODEBUG 结合 go run 使用,那么你将看到运行之前的跟踪调试信息。 +在设置 GODEBUG 之前,先用 Go build 编译代码。这个变量由运行时获取,因此运行 Go 命令也将产生跟踪输出。如果 GODEBUG 结合 Go run 使用,那么你将看到运行之前的跟踪调试信息。 -现在我们使用 go build 编译上面的例子,这样我们就可以携带 GODEBUG 选项运行例子了: +现在我们使用 Go build 编译上面的例子,这样我们就可以携带 GODEBUG 选项运行例子了: ``` go build example.go @@ -104,16 +104,16 @@ idleprocs=0 : 空闲的处理器个数。这里空闲个数为 0,有一个 runqueue=0 : 在全局运行队列中等待的 goroutinue 数量。所有可运行的 goroutinue 都被移到了本地的运行队列中。 -[9] : 本地运行队列中等待的 goroutine 数量。当前有 9 个 goroutine 在本地运行队列等待。 +[9] : 本地运行队列中等待的 Goroutine 数量。当前有 9 个 Goroutine 在本地运行队列等待。 ``` -在运行时的摘要信息里面给了我们很多非常有用的信息。我们从运行的一秒的标记里面可以看到跟踪的信息。我们可以看到一个 gorountine 如何运行,其它 9 个 goroutine 都在 local run queue 中等待。 +在运行时的摘要信息里面给了我们很多非常有用的信息。我们从运行的一秒的标记里面可以看到跟踪的信息。我们可以看到一个 gorountine 如何运行,其它 9 个 Goroutine 都在 local run queue 中等待。 ### 图 1 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/20150223-Scheduler-Tracing-In-Go/diagram1.png) -从图一中可以看到处理器用字母 "P" 代表,线程使用字母 "M" 代码,goroutines 使用字母 "G" 代表。我们可以看到当 runqueue 的值为 0 时,全局的 run queue 是空的。处理器将会把 gorountine 运行在 idleprocs 为 0 的上面运行。我们运行的其他九个 goroutine 仍然在等待。 +从图一中可以看到处理器用字母 "P" 代表,线程使用字母 "M" 代码,goroutines 使用字母 "G" 代表。我们可以看到当 runqueue 的值为 0 时,全局的 run queue 是空的。处理器将会把 gorountine 运行在 idleprocs 为 0 的上面运行。我们运行的其他九个 Goroutine 仍然在等待。 那如果有多个处理器的时候,那我们该如何跟踪呢?那我们再运行一次程序并添加 GOMAXPROCS 选项,看看会输出什么跟踪信息: @@ -174,7 +174,7 @@ runqueue=0 : All runnable goroutines have been moved to a local run queue. ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/20150223-Scheduler-Tracing-In-Go/diagram2.png) -我们看一下第二秒跟踪信息在图 2 中的信息,我们可以看到一个 goroutine 在每个处理器中是如何运行的。并且我们可以看到 8 个 goroutine 在 local run queues 中等待,每个 local run queues 各四个。在跟踪信息的第六秒发生了改变: +我们看一下第二秒跟踪信息在图 2 中的信息,我们可以看到一个 Goroutine 在每个处理器中是如何运行的。并且我们可以看到 8 个 Goroutine 在 local run queues 中等待,每个 local run queues 各四个。在跟踪信息的第六秒发生了改变: ``` SCHED 6024ms: gomaxprocs=2 idleprocs=0 threads=4 spinningthreads=0 @@ -189,13 +189,13 @@ runqueue=2 : 2 goroutines returned and are waiting to be terminated. ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/20150223-Scheduler-Tracing-In-Go/diagram3.png) -当到第六秒的时候发生了变化。从图 3 中有两个 goroutine 完成工作之后被移到了 global run queue 里面,并且我们仍然有两个 goroutine 在运行。每个 processor 各运行一个,在每个 local run queue 里面各有三个在等待。 +当到第六秒的时候发生了变化。从图 3 中有两个 Goroutine 完成工作之后被移到了 global run queue 里面,并且我们仍然有两个 Goroutine 在运行。每个 processor 各运行一个,在每个 local run queue 里面各有三个在等待。 注释: -在很多情况下,goroutine 运行完成之后并不会被移到全局的 run queue 中。这个例子创建的条件比较特殊。因为这个例子的 for 循环运行了 10 秒多的时间但是没有任何的函数调用。10 秒是调度的次数在调度器里面。在执行 10 秒后,调度器尝试先去取 goroutine。但是这些 goroutine 不能被占用,因为它们没有调用任何的函数。在这种情况下,一旦 goroutine 调用 wg.Done,这个 goroutine 将立即被占用,然后移到全局的 run queue 中。 +在很多情况下,goroutine 运行完成之后并不会被移到全局的 run queue 中。这个例子创建的条件比较特殊。因为这个例子的 for 循环运行了 10 秒多的时间但是没有任何的函数调用。10 秒是调度的次数在调度器里面。在执行 10 秒后,调度器尝试先去取 goroutine。但是这些 Goroutine 不能被占用,因为它们没有调用任何的函数。在这种情况下,一旦 Goroutine 调用 wg.Done,这个 Goroutine 将立即被占用,然后移到全局的 run queue 中。 -当到第 17 秒的时候,我们可以看到最后两个 goroutine 都在运行了: +当到第 17 秒的时候,我们可以看到最后两个 Goroutine 都在运行了: ``` SCHED 17084ms: Gomaxprocs=2 idleprocs=0 threads=4 spinningthreads=0 @@ -225,11 +225,11 @@ runqueue=0 : All the goroutines that were in the queue have been terminated. ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/20150223-Scheduler-Tracing-In-Go/diagram5.png) -至此,所有的 goroutine 都执行完了并且已经结束。 +至此,所有的 Goroutine 都执行完了并且已经结束。 ## 详细的跟踪信息 -概要的跟踪信息是非常有用的,但是有的时候你需要更详细的信息。如果需要更详细的每个处理器,线程的或者 goroutine 的跟踪信息我们可以添加 scheddetail 这个选项。我们再一次运行程序,设置 GODEBUG 选项获取更详细的跟踪信息: +概要的跟踪信息是非常有用的,但是有的时候你需要更详细的信息。如果需要更详细的每个处理器,线程的或者 Goroutine 的跟踪信息我们可以添加 scheddetail 这个选项。我们再一次运行程序,设置 GODEBUG 选项获取更详细的跟踪信息: ``` GOMAXPROCS=2 GODEBUG=schedtrace=1000,scheddetail=1 ./example @@ -263,10 +263,10 @@ G10: status=2(sleep) m=2 lockedm=-1 G11: status=1(sleep) m=-1 lockedm=-1 G12: status=1(sleep) m=-1 lockedm=-1 G13: status=1(sleep) m=-1 lockedm=-1 -G17: status=4(timer goroutine (idle)) m=-1 lockedm=-1 +G17: status=4(timer Goroutine (idle)) m=-1 lockedm=-1 ``` -概要部分基本相同,但是有了关于处理器,线程以及 goroutine 更详细的信息。我们看看关于处理器的信息: +概要部分基本相同,但是有了关于处理器,线程以及 Goroutine 更详细的信息。我们看看关于处理器的信息: ``` P0: status=1 schedtick=10 syscalltick=0 m=3 runqsize=3 gfreecnt=0 @@ -301,10 +301,10 @@ spinning=0 blocked=0 lockedg=-1 这里展示了线程 M3 是如何绑定在处理器 P0 上的。这个信息在 P 和 M 的跟踪信息里面都有。 -G 代码一个 goroutine。在第四秒的时候我们可以看到有 14 个 goroutine 存在,有 17 个 goroutine 被创建。我们之所以知道总共的 goroutine 的个数是因为最后在 G 列表里面绑定的数字: +G 代码一个 goroutine。在第四秒的时候我们可以看到有 14 个 Goroutine 存在,有 17 个 Goroutine 被创建。我们之所以知道总共的 Goroutine 的个数是因为最后在 G 列表里面绑定的数字: ``` -G17: status=4(timer goroutine (idle)) m=-1 lockedm=-1 +G17: status=4(timer Goroutine (idle)) m=-1 lockedm=-1 ``` 如果成行继续创建 goroutine,我们就可以看到这个数字将呈线性的增长。如果这个程序是拦截 Web 请求的例子,那么我们可以用这个数字来确认请求的拦截次数。只有当拦截请求期间不再创建任何的 goroutine,这个才会被关闭。 @@ -317,7 +317,7 @@ G1: status=4(semacquire) m=-1 lockedm=-1 30 wg.Done() ``` -我们可以看到在 main 方法中 goroutine 的状态为 4,状态被锁定在 semacquire 状态,这个状态表示等待调用。 +我们可以看到在 main 方法中 Goroutine 的状态为 4,状态被锁定在 semacquire 状态,这个状态表示等待调用。 为了更好的理解剩下的跟踪信息,先来了解一下状态代码的意思。下面是状态值列表,这些声明在 runtime 包的头文件里面的: @@ -329,12 +329,12 @@ Grunning, // 2 running Gsyscall, // 3 performing a syscall Gwaiting, // 4 waiting for the runtime Gmoribund_unused, // 5 currently unused, but hardcoded in gdb scripts -Gdead, // 6 goroutine is dead +Gdead, // 6 Goroutine is dead Genqueue, // 7 only the Gscanenqueue is used Gcopystack, // 8 in this state when newstack is moving the stack ``` -对照他们的状态我们能更好的理解我们创建的 10 个 goroutine 都在做什么。 +对照他们的状态我们能更好的理解我们创建的 10 个 Goroutine 都在做什么。 ``` // goroutines running in a processor. (idleprocs=0) @@ -356,7 +356,7 @@ G6: status=1(stack growth) m=-1 lockedm=-1 G9: status=1(stack growth) m=-1 lockedm=-1 ``` -基于对 scheduler 的简单了解以及对我们例子程序的了解,我们对程序如何被 scheduled,每个处理器的状态是什么,线程以及 goroutine 等信息都有了全面的了解。 +基于对 scheduler 的简单了解以及对我们例子程序的了解,我们对程序如何被 scheduled,每个处理器的状态是什么,线程以及 Goroutine 等信息都有了全面的了解。 ## 总结: diff --git a/published/tech/20150301-monkey-patching-in-go.md b/published/tech/20150301-monkey-patching-in-go.md index ec31c1116..4b06839d6 100644 --- a/published/tech/20150301-monkey-patching-in-go.md +++ b/published/tech/20150301-monkey-patching-in-go.md @@ -20,7 +20,7 @@ func main() { > example1.go 由 GitHub 托管 [查看源文件](https://gist.github.com/bouk/17262666fae75dd24a25/raw/712ae5ef5b1becf4f782d96ca0be0d67ccdcf061/example1.go) -上述代码应该用 go build -gcflags=-l 来编译,以避免内联。在本文中我假设你的电脑架构是 64 位,并且你使用的是一个基于unix 的操作系统比如 Mac OSX 或者某个 Linux 系统。 +上述代码应该用 Go build -gcflags=-l 来编译,以避免内联。在本文中我假设你的电脑架构是 64 位,并且你使用的是一个基于 unix 的操作系统比如 Mac OSX 或者某个 Linux 系统。 当代码编译后,我们用 [Hopper](http://hopperapp.com/) 来查看,可以看到如上代码会产生如下汇编代码: @@ -30,7 +30,7 @@ func main() { 我们的代码从 main.main 过程开始,从 0x2010 到 0x2026 的指令构建堆栈。你可以在[这儿](http://dave.cheney.net/2013/06/02/why-is-a-goroutines-stack-infinite)获得更多的相关知识,本文后续的篇幅里,我将忽略这部分代码。 -0x202a 行是调用 0x2000 行的 main.a 函数,这个函数只是简单的将 0x1 压入堆栈然后就返回了。0x202f 到 0x2037这几行 将这个值传递给 runtime.printint. +0x202a 行是调用 0x2000 行的 main.a 函数,这个函数只是简单的将 0x1 压入堆栈然后就返回了。0x202f 到 0x2037 这几行 将这个值传递给 runtime.printint. 足够简单!现在让我们看看在 Go 语言中 函数的值是怎么实现的。 @@ -54,7 +54,7 @@ func main() { ``` > funcval.go 由 GitHub 托管 [查看源文件](https://gist.github.com/bouk/c921c3627ddbaae05356/raw/4c18dbaa7cfeb06b74007b65649d85f65384841a/funcval.go) -我在第11行 将 a 赋值给 f,这意味者,执行 f() 就会调用 a。然后我使用 Go 中的 [unsafe](http://golang.org/pkg/unsafe/) 包来直接读出 f 中存储的值。如果你是有 C 语言的开发背景 ,你可以会觉得 f 就是一个简单的函数指针,并且这段代码会输出 0x2000 (我们在上面看到的 main.a 的地址)。当我在我的机器上运行时,我得到的是 0x102c38, 这个地址甚至与我们的代码都不挨着!当反编译时,这就是上面第11行所对应的: +我在第 11 行 将 a 赋值给 f,这意味者,执行 f() 就会调用 a。然后我使用 Go 中的 [unsafe](http://golang.org/pkg/unsafe/) 包来直接读出 f 中存储的值。如果你是有 C 语言的开发背景 ,你可以会觉得 f 就是一个简单的函数指针,并且这段代码会输出 0x2000 (我们在上面看到的 main.a 的地址)。当我在我的机器上运行时,我得到的是 0x102c38, 这个地址甚至与我们的代码都不挨着!当反编译时,这就是上面第 11 行所对应的: ![image](https://raw.githubusercontent.com/studygolang/gctt-images/master/monkey-patch/hopper-2.png) @@ -78,7 +78,7 @@ func main() { fmt.Printf("0x%x\n", **(**uintptr)(unsafe.Pointer(&f))) } ``` -> funcval2.go 由GitHub托管 [查看源文件](https://gist.github.com/bouk/c470c4d80ae80d7b30af/raw/d8bd9cd2b80cad288993d5e8f67b115440c6c2a3/funcval2.go) +> funcval2.go 由 GitHub 托管 [查看源文件](https://gist.github.com/bouk/c470c4d80ae80d7b30af/raw/d8bd9cd2b80cad288993d5e8f67b115440c6c2a3/funcval2.go) 现在输出的正是预期中的 0x2000。我们可以在[这里](https://github.com/golang/go/blob/e9d9d0befc634f6e9f906b5ef7476fbd7ebd25e3/src/runtime/runtime2.go#L75-L78)找到一点为什么代码要这样写的线索。在 Go 语言中函数值可以包含额外的信息,闭包和绑定实例方法借此实现的。 @@ -118,7 +118,7 @@ func main() { print(a()) } ``` -> replace.go 由GitHub托管 [查看源文件](https://gist.github.com/bouk/713f3df2115e1b5e554d/raw/65335f4e7d9d0e11a5f72e78d617ec51249c577b/replace.go) +> replace.go 由 GitHub 托管 [查看源文件](https://gist.github.com/bouk/713f3df2115e1b5e554d/raw/65335f4e7d9d0e11a5f72e78d617ec51249c577b/replace.go) 现在我们该怎么实现这种替换?我们需要修改函数 a 跳到 b 的代码,而不是执行它自己的函数体。本质上,我们需要这么替换,把 b 的函数值加载到 rdx 然后跳转到 rdx 所指向的地址。 @@ -145,7 +145,7 @@ func assembleJump(f func() int) []byte { ``` > assemble_jump.go 由 GitHub 托管 [查看源文件](https://gist.github.com/bouk/4ed563abdcd06fc45fa0/raw/fa9c65c2d5828592e846e28136871ee0bd13e5a9/assemble_jump.go) -现在万事俱备,我们已经准备好将 a 的函数体替换为从 a 跳转到 b了!下述代码尝试直接将机器码拷贝到函数体中。 +现在万事俱备,我们已经准备好将 a 的函数体替换为从 a 跳转到 b 了!下述代码尝试直接将机器码拷贝到函数体中。 ```go package main diff --git a/published/tech/20151216-Goroutine-IDs.md b/published/tech/20151216-Goroutine-IDs.md index d25f2814a..644ec1249 100644 --- a/published/tech/20151216-Goroutine-IDs.md +++ b/published/tech/20151216-Goroutine-IDs.md @@ -6,7 +6,7 @@ 当然存在。 -Go 运行时一定有某种方法来跟踪 goroutine ID。 +Go 运行时一定有某种方法来跟踪 Goroutine ID。 ## 那我该使用它们吗? @@ -20,7 +20,7 @@ Go 运行时一定有某种方法来跟踪 goroutine ID。 已有的来自 Go Team 成员的包,被评价为“[用此包者,将入地狱。](https://godoc.org/github.com/davecheney/junk/id)” -也有一些包基于 goroutine id 来建立 goroutine 本地存储,如: +也有一些包基于 Goroutine id 来建立 Goroutine 本地存储,如: - [github.com/jtolds/gls](https://github.com/jtolds/gls) - [github.com/tylerb/gls](https://github.com/tylerb/gls) @@ -29,7 +29,7 @@ Go 运行时一定有某种方法来跟踪 goroutine ID。 ## 最简代码 -如果读到这里,你仍“执迷不悟”,那么下面就将展示如何获取当前的 goroutine id : +如果读到这里,你仍“执迷不悟”,那么下面就将展示如何获取当前的 Goroutine id : ### Go 源码中的骇客(Hacky)代码 @@ -61,13 +61,13 @@ func getGID() uint64 { #### 工作原理解释 -通过解析调试信息来获取 goroutine id 是可行的. `http/2` 库就使用调试性的代码来对连接进行追踪查看。但仅仅是将 goroutine id 用于调试而已。 +通过解析调试信息来获取 Goroutine id 是可行的. `http/2` 库就使用调试性的代码来对连接进行追踪查看。但仅仅是将 Goroutine id 用于调试而已。 -调试信息可以通过调用 [`runtime.Stack(buf []byte, all bool) int`](https://golang.org/pkg/runtime/#Stack) 来获取,它会以文本形式打印堆栈信息到缓冲区中。堆栈信息的第一行会是如下文本: “goroutine #### […” 。这里的 #### 就是真实的 goroutine id。剩余代码不过是进行一些文本操作来提取和解析堆栈信息中的数字。 +调试信息可以通过调用 [`runtime.Stack(buf []byte, all bool) int`](https://golang.org/pkg/runtime/#Stack) 来获取,它会以文本形式打印堆栈信息到缓冲区中。堆栈信息的第一行会是如下文本: “goroutine #### […” 。这里的 #### 就是真实的 Goroutine id。剩余代码不过是进行一些文本操作来提取和解析堆栈信息中的数字。 ### CGo 版本对应的合法代码 -C 版本的代码来自 [github.com/davecheney/junk/id](https://github.com/davecheney/junk/tree/master/id)。代码中直接获取了当前 goroutine 的 goid 属性并返回它的值。 +C 版本的代码来自 [github.com/davecheney/junk/id](https://github.com/davecheney/junk/tree/master/id)。代码中直接获取了当前 Goroutine 的 goid 属性并返回它的值。 文件 `id.c` @@ -89,7 +89,7 @@ func Id() int64 ## 我该怎么做? -远离 goroutine id 吧,并忘记它们的存在。从 Go 语言设计的角度来看,使用它们是危险的。因为几乎所有使用的目的都是去做一些和 goroutine-local 相关的事情。而这违反了 Go 语言编程的 “[Share Memory By Communicating](https://blog.golang.org/share-memory-by-communicating)” 原则。 +远离 Goroutine id 吧,并忘记它们的存在。从 Go 语言设计的角度来看,使用它们是危险的。因为几乎所有使用的目的都是去做一些和 goroutine-local 相关的事情。而这违反了 Go 语言编程的 “[Share Memory By Communicating](https://blog.golang.org/share-memory-by-communicating)” 原则。 --- diff --git a/published/tech/20160223-How-Go-solves-so-many-problems-for-web-developers.md b/published/tech/20160223-How-Go-solves-so-many-problems-for-web-developers.md index 0d7a65f0c..f4f3e2b73 100644 --- a/published/tech/20160223-How-Go-solves-so-many-problems-for-web-developers.md +++ b/published/tech/20160223-How-Go-solves-so-many-problems-for-web-developers.md @@ -20,7 +20,7 @@ 我之前曾经涉足 C 语言,而 Go 感觉和 C 很像,但是 Go 提供的标准库非常强大且易于使用,所以我对 Go 语法的精炼扼要感到震惊。 -在深入研究之后,我决定研究 Go 是如何解决 PHP 编写 Web 应用 / API等出现的一些问题。 +在深入研究之后,我决定研究 Go 是如何解决 PHP 编写 Web 应用 / API 等出现的一些问题。 如何去解决 Web Sockets?Go 有几个很出色的库文件。下面是一个 Gin 框架使用 Gorilla websockets 库的例子... @@ -100,11 +100,11 @@ func main() { } ``` -我将之前一个上传图片到 s3 的耗时任务放到 goroutine 中去实现接近即时的上传效果,没有第三方服务,完全本地。对于大多数开发人员来说不那么令人印象深刻,但是对于 PHP 背景的开发人员来说,我对 Go 的易用性和性能提升感到震惊。 +我将之前一个上传图片到 s3 的耗时任务放到 Goroutine 中去实现接近即时的上传效果,没有第三方服务,完全本地。对于大多数开发人员来说不那么令人印象深刻,但是对于 PHP 背景的开发人员来说,我对 Go 的易用性和性能提升感到震惊。 ## 测试 -单元测试在 PHP 或 Javascript 中可能会有点痛苦。有无数不同的测试框架,但没有一个能够像 go built 命令去如此简单自然的进行测试。 +单元测试在 PHP 或 Javascript 中可能会有点痛苦。有无数不同的测试框架,但没有一个能够像 Go built 命令去如此简单自然的进行测试。 main.go diff --git a/published/tech/20160731-Assignability-in-Go.md b/published/tech/20160731-Assignability-in-Go.md index 65d7ce1a4..57e3df32d 100644 --- a/published/tech/20160731-Assignability-in-Go.md +++ b/published/tech/20160731-Assignability-in-Go.md @@ -125,7 +125,7 @@ d = untyped via: https://medium.com/golangspec/assignability-in-go-27805bcd5874 -作者:[Michał Łowicki](https://twitter.com/mlowicki) +作者:[Micha ł Ł owicki](https://twitter.com/mlowicki) 译者:[Miancai Li](https://github.com/gogeof) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20160806-Identical-types-in-Go.md b/published/tech/20160806-Identical-types-in-Go.md index f659bdd03..fc0571ba2 100755 --- a/published/tech/20160806-Identical-types-in-Go.md +++ b/published/tech/20160806-Identical-types-in-Go.md @@ -106,7 +106,7 @@ T1 和 T1 是等效的。由于使用了两个独立的类型声明,T1 和 T2 via: https://medium.com/golangspec/identical-types-in-go-9cb89b91fe25 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[leemeans](https://github.com/leemeans) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20160812-Scopes-in-Go.md b/published/tech/20160812-Scopes-in-Go.md index d34684c1e..d475640dc 100644 --- a/published/tech/20160812-Scopes-in-Go.md +++ b/published/tech/20160812-Scopes-in-Go.md @@ -282,14 +282,14 @@ outer ## 参考资料 -- [《Go语言规范》](https://golang.org/ref/spec#Declarations_and_scope) +- [《Go 语言规范》](https://golang.org/ref/spec#Declarations_and_scope) - [《Go 语言中的代码块》](https://studygolang.com/articles/12632) --- via: https://medium.com/golangspec/scopes-in-go-a6042bb4298c -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[MoodWu](https://github.com/MoodWu) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20160926-simple-statement-notion-in-go.md b/published/tech/20160926-simple-statement-notion-in-go.md index 199dfc10f..3302985e5 100644 --- a/published/tech/20160926-simple-statement-notion-in-go.md +++ b/published/tech/20160926-simple-statement-notion-in-go.md @@ -25,7 +25,7 @@ SimpleStmt = EmptyStmt | ExpressionStmt | SendStmt | IncDecStmt | Assignment | S ## 2. 自增或自减语句 ```go -x++ // 语义上等价于赋值x + = 1 +x++ // 语义上等价于赋值 x + = 1 x-- // x -= 1 ``` @@ -47,11 +47,11 @@ foo = bar++ + 10 某些表达式可以放入语句中。 -> **块是由大括号括起来的一系列语句,例如,表达式语句在如下情况下完全有效。** +> **块是由大括号括起来的一系列语句,例如,表达式语句在如下  情况下完全有效。** 允许的选项如下: -- 函数调用(除了一些内置函数,例如 append,cap,complex,imag,len,make,new,real,[unsafe](https://golang.org/pkg/unsafe/).Alignof,unsafe.Offsetof 和unsafe.SizeOf) +- 函数调用(除了一些内置函数,例如 append,cap,complex,imag,len,make,new,real,[unsafe](https://golang.org/pkg/unsafe/).Alignof,unsafe.Offsetof 和 unsafe.SizeOf) - 方法调用 - 接收操作符 @@ -66,10 +66,10 @@ func f(n int) { func main() { f(1) - s := S{"Michał"} + s := S{"Micha ł"} s.m() c := make(chan int) - go func() { + Go func() { c <- 1 }() <-c @@ -80,7 +80,7 @@ func main() { ## 5. 赋值语句 -大家应该都熟悉一些最基本的赋值形式。首先必须声明变量,右边的表达式必须可以[赋值](https://studygolang.com/articles/12381)给一个变量: +大家应该都熟悉一些最基本的赋值形式。首先必须  声明变量,右边的表达式必须可以[赋值](https://studygolang.com/articles/12381)给一个变量: ```go var population int64 @@ -90,7 +90,7 @@ var city, country string city, country = "New York", "USA" ``` -当把一个以上的值分配给变量列表时,有两种形式。第一种形式,一个表达式返回多个值,例如函数调用: +当把一个以上的值分配给变量列表时,有两种形式。 第一种形式,一个表达式返回多个值,例如函数调用: ```go f := func() (int64, string) { @@ -145,7 +145,7 @@ a := [...]int{1,2,3} one, two := f(), g() ``` -现在我们应该很清楚是什么构成了一组有效的简单语句。但是它们用在什么地方呢? +现在我们应该很清楚是  什么构成了一组有效的简单语句。但是  它们用在什么地方呢? ## if 语句 @@ -170,7 +170,7 @@ for i := 0; i < 10; i += 1 { } ``` -当然了,也没有东西阻止程序员在这个地方使用其他的简单语句。 +当然了,也没有  东西阻止程序员在这个地方使用其他的简单语句。 ## switch 语句 @@ -225,8 +225,8 @@ T1 main.T1 10 via: https://medium.com/golangspec/simple-statement-notion-in-go-b8afddfc7916 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[yousanflics](https://github.com/yousanflics) 校对:[polaris1119](https://github.com/polaris1119) -本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go中文网](https://studygolang.com/) 荣誉推出 +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20160929-initialization-dependencies-in-go.md b/published/tech/20160929-initialization-dependencies-in-go.md index 1a4dbd639..5288e3677 100644 --- a/published/tech/20160929-initialization-dependencies-in-go.md +++ b/published/tech/20160929-initialization-dependencies-in-go.md @@ -4,7 +4,7 @@ 让我们直接从两个 Go 语言小程序开始: -**程序1** +**程序 1** ```go package main @@ -19,7 +19,7 @@ func main() { } ``` -**程序2** +**程序 2** ```go package main @@ -35,14 +35,14 @@ func main() { ``` 如果这两段代码会输出相同的结果,那么它们就不是好的素材,很幸运的是,它们的结果是不同的: -**程序1** +**程序 1** ``` 2 1 ``` -**程序2** +**程序 2** 这个程序无法编译通过,甚至会在第 7 行报一个编译时错误 "undefined: b"。 @@ -71,7 +71,7 @@ h 1 2 3 ``` -前面提到的 "正常" 意味着它在自己的函数中完成初始化。当这些初始化代码像程序1中那样被放到包的顶层声明中时,会变得越来越有趣: +前面提到的 "正常" 意味着它在自己的函数中完成初始化。当这些初始化代码像程序 1 中那样被放到包的  顶层声明  中时, 会变得越来越有趣: ```go package main @@ -94,7 +94,7 @@ func main() { 这些变量声明的顺序如下: -* b 是第一个,因为它不依赖其他未初始化的变量 +* b 是第一个,因为它不依赖  其他未初始化的变量 * c 是第二个,在 `f` 函数需要的变量 b 被初始化之后,紧接着被初始化 * a 是在 c 被初始化之后的第三轮初始化循环中被处理 @@ -169,7 +169,7 @@ func f() int { via: https://medium.com/golangspec/initialization-dependencies-in-go-51ae7b53f24c -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[rxcai](https://github.com/rxcai) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20161004-init-function-in-go.md b/published/tech/20161004-init-function-in-go.md index 2e3bc6b6d..321a4681d 100644 --- a/published/tech/20161004-init-function-in-go.md +++ b/published/tech/20161004-init-function-in-go.md @@ -16,7 +16,7 @@ init 函数在包级别被定义,主要用于: ## 包的初始化 -要想使用导入的包首先需要初始化它,这是由golang的运行系统完成的,主要包括(顺序很重要): +要想使用导入的包首先需要初始化它,这是由 golang 的运行系统完成的,主要包括(顺序很重要): 1. 初始化导入的包(递归的定义) 2. 在包级别为声明的变量计算并分配初始值 @@ -214,7 +214,7 @@ import _ "image/png" via: https://medium.com/golangspec/init-functions-in-go-eac191b3860a -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[flexiwind](https://github.com/flexiwind) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20161010-Go-Testable-Examples-under-the-hood.md b/published/tech/20161010-Go-Testable-Examples-under-the-hood.md index 0c98bfea6..8df910e2f 100644 --- a/published/tech/20161010-Go-Testable-Examples-under-the-hood.md +++ b/published/tech/20161010-Go-Testable-Examples-under-the-hood.md @@ -341,7 +341,7 @@ for _, group := range comments { via: https://medium.com/golangspec/gos-testable-examples-under-the-hood-4a4db8db447f -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[lovechuck](https://github.com/lovechuck) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20161013-Panicking-like-a-Gopher.md b/published/tech/20161013-Panicking-like-a-Gopher.md index 903236486..d269b9866 100644 --- a/published/tech/20161013-Panicking-like-a-Gopher.md +++ b/published/tech/20161013-Panicking-like-a-Gopher.md @@ -1,6 +1,6 @@ 首发于:https://studygolang.com/articles/16572 -# 用 gopher 的方式使用 panic +# 用 Gopher 的方式使用 panic Go 运行时(即成功编译后,操作系统启动该该进程)发生的错误会以 panics 的形式反馈。*panic* 可以通过这两种形式触发 : @@ -15,7 +15,7 @@ func main(){ ``` ``` -> go install github.com/mlowicki/lab && ./bin/lab +> Go install github.com/mlowicki/lab && ./bin/lab panic: foo goroutine 1 [running]: @@ -44,7 +44,7 @@ func main() { ``` ``` -> go install github.com/mlowicki/lab && ./bin/lab +> Go install github.com/mlowicki/lab && ./bin/lab panic: runtime error: integer divide by zero [signal 0x8 code=0x7 addr=0x2062 pc=0x2062] @@ -65,7 +65,7 @@ main.main() 用 Go 语言实现的程序在执行时或多或少都会有一些 *goroutine*。在关于 *go statement* 的规范详述中 *goroutine* 定义如下: -> 一个 "Go" 语句在同一个地址空间内作为一个独立的并发线程控制或者 goroutine ,开始执行函数调用。 +> 一个 "Go" 语句在同一个地址空间内作为一个独立的并发线程控制或者 Goroutine ,开始执行函数调用。 Go 是一门并发语言,这是因为它(原生)提供了并发编程的特性,比如并发运行的语句 ([Go 语句](https://golang.org/ref/spec#Go_statements))或能够在一些并发事物中轻松交流的机制([channels](https://golang.org/ref/spec#Channel_types))。 不过并发是什么意思呢?和无处不在的并行又有什么关系呢? @@ -109,7 +109,7 @@ func main() { ``` ``` -> go install github.com/mlowicki/lab && ./bin/lab +> Go install github.com/mlowicki/lab && ./bin/lab 1 2 ``` @@ -141,7 +141,7 @@ func main() { ``` -> go install github.com/mlowicki/lab && ./bin/lab +> Go install github.com/mlowicki/lab && ./bin/lab 1 2 Inside deferred function @@ -164,7 +164,7 @@ func main() { ``` ``` -> go install github.com/mlowicki/lab && ./bin/lab +> Go install github.com/mlowicki/lab && ./bin/lab 1 2 Inside deferred function @@ -193,7 +193,7 @@ func main() { ``` ``` -> go install github.com/mlowicki/lab && ./bin/lab +> Go install github.com/mlowicki/lab && ./bin/lab 1 2 Inside deferred function @@ -229,7 +229,7 @@ func main() { ``` ``` -> go install github.com/mlowicki/lab && ./bin/lab +> Go install github.com/mlowicki/lab && ./bin/lab Inside f, n=1 ``` @@ -254,7 +254,7 @@ func main() { ``` ``` -> go install github.com/mlowicki/lab && ./bin/lab +> Go install github.com/mlowicki/lab && ./bin/lab 3 2 1 @@ -298,7 +298,7 @@ func main() { ``` ``` -> go install github.com/mlowicki/lab && ./bin/lab +> Go install github.com/mlowicki/lab && ./bin/lab Before defer statement After defer statement panic: runtime error: invalid memory address or nil pointer dereference @@ -338,7 +338,7 @@ func main() { ``` ``` -> go install github.com/mlowicki/lab && ./bin/lab +> Go install github.com/mlowicki/lab && ./bin/lab f() == 0 g() == 1 ``` @@ -347,7 +347,7 @@ g() == 1 ## Panicking -当任意函数 f 发生 panic 时,我们在上面例子中已经看到,在 f 中调用延迟函数的函数将以后进先出的顺序调用。之后将有什么发生呢?之后对于 f 的调用者,这种过程将被重复——它的延迟的函数将被触发。如此反复直到 f 的 goroutine 中的最上面的那个函数。最后,最上面的那个函数的延迟的函数被调用,并且程序终止。就像是一个冒泡直到顶端的调用链: +当任意函数 f 发生 panic 时,我们在上面例子中已经看到,在 f 中调用延迟函数的函数将以后进先出的顺序调用。之后将有什么发生呢?之后对于 f 的调用者,这种过程将被重复——它的延迟的函数将被触发。如此反复直到 f 的 Goroutine 中的最上面的那个函数。最后,最上面的那个函数的延迟的函数被调用,并且程序终止。就像是一个冒泡直到顶端的调用链: ```go package main @@ -378,13 +378,13 @@ func h() { func main() { ch := make(chan int) - go f(ch) + Go f(ch) <-ch } ``` ``` -> go install github.com/mlowicki/lab && ./bin/lab +> Go install github.com/mlowicki/lab && ./bin/lab Deferred by h Deferred by g Deferred by f @@ -439,13 +439,13 @@ func h() { func main() { ch := make(chan int) - go f(ch) + Go f(ch) <-ch } ``` ``` -> go install github.com/mlowicki/lab && ./bin/lab +> Go install github.com/mlowicki/lab && ./bin/lab Deferred by h Deferred by g Deferred by f @@ -472,7 +472,7 @@ created by main.main 不论如何,事实证明结果会是,在调用直到调用链的顶部的推迟的函数,这个过程将会执行。虽然会有,第二个 panic 这样的新结果,像之前输出的那样,也会显示出来。 ``` -> go install github.com/mlowicki/lab && ./bin/lab +> Go install github.com/mlowicki/lab && ./bin/lab Deferred by h Deferred by g Deferred by f @@ -546,7 +546,7 @@ func main() { ``` ``` -> go install github.com/mlowicki/lab && ./bin/lab +> Go install github.com/mlowicki/lab && ./bin/lab Start f Start g Start h @@ -577,7 +577,7 @@ Deferred in f via: https://medium.com/golangspec/panicking-like-a-gopher-367a9ce04bb8 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[yixiaoer](https://github.com/yixiaoer) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20161015-debugging-code-generation-in-go.md b/published/tech/20161015-debugging-code-generation-in-go.md index 583ae9068..301071920 100644 --- a/published/tech/20161015-debugging-code-generation-in-go.md +++ b/published/tech/20161015-debugging-code-generation-in-go.md @@ -24,7 +24,7 @@ func main() { go build 是一个对用户来说囊括了一大堆东西的命令。但是,如果你需要的话,它也提供了关于它是做什么的更详细的信息。`-x` 是一个能让 Go build 输出调用了什么的标记。如果你想看看工具链的组件是什么,它们在一个什么样的序列里以及使用了什么标记的话,使用 `-x`。 ``` -$ go build -x +$ Go build -x WORK=/var/folders/00/1b8h8000h01000cxqpysvccm005d21/T/go-build190726544 mkdir -p $WORK/hello/_obj/ mkdir -p $WORK/hello/_obj/exe/ @@ -44,7 +44,7 @@ mv $WORK/hello/_obj/exe/a.out hello 在这里你将看到 main.main 的输出。 ``` -$ go build -gcflags="-S" +$ Go build -gcflags="-S" # hello "".main t=1 size=179 args=0x0 locals=0x60 0x0000 00000 (/Users/jbd/src/hello/hello.go:5) TEXT "".main(SB), $96-0 @@ -95,7 +95,7 @@ $ go build -gcflags="-S" 正如我提到的,`-S` 仅仅作用于中间汇编。真实机器上的表示在最终的工件中可用。你可以使用反汇编器去检查里面有什么。对二进制或库使用 `go tool objdump` 。你可能还想使用 `-s` 来关注符号名。在这个例子里,我将对 main.main 进行转存。这里是为 `darwin/amd64` 生成的真实汇编。 ``` -$ go tool objdump -s main.main hello +$ Go tool objdump -s main.main hello TEXT main.main(SB) /Users/jbd/src/hello/hello.go hello.go:5 0x2040 65488b0c25a0080000 GS MOVQ GS:0x8a0, CX hello.go:5 0x2049 483b6110 CMPQ 0x10(CX), SP @@ -115,7 +115,7 @@ TEXT main.main(SB) /Users/jbd/src/hello/hello.go 有时,你需要的全部只是检查符号表而不是理解代码段或数据段。类似通用的 nm 工具,Go 分发了一个让你能列出一个工件中带注记和大小的符号表的 nm 工具。如果你想看看 Go 的一个二进制或库内部是什么,导出了什么,这是个很便利的工具。 ``` -$ go tool nm hello +$ Go tool nm hello ... f4760 B __cgo_init f4768 B __cgo_notify_runtime_init_done @@ -135,7 +135,7 @@ ad2e0 R _shifts ## 优化 -和新的 SSA 后端的贡献一起,团队贡献了一个可视化所有 SSA pass 的工具。将环境变量 GOSSAFUNC 的值设置为一个函数名称然后运行 go build 命令。将会产生一个 ssa.html 文件,显示了编译器为了优化你的代码所经过的每一步。 +和新的 SSA 后端的贡献一起,团队贡献了一个可视化所有 SSA pass 的工具。将环境变量 GOSSAFUNC 的值设置为一个函数名称然后运行 Go build 命令。将会产生一个 ssa.html 文件,显示了编译器为了优化你的代码所经过的每一步。 ``` $ GOSSAFUNC=main Go build && open ssa.html @@ -148,7 +148,7 @@ $ GOSSAFUNC=main Go build && open ssa.html Go 编译器还可以标注内联和逃逸分析。如果你将 `-m=2` 标志传给编译器,它将输出关于这两个方面的优化和标注。这里我们看到 `net/context` 包相关的内联操作和逃逸分析。 ``` -$ go build -gcflags="-m" golang.org/x/net/context +$ Go build -gcflags="-m" golang.org/x/net/context # golang.org/x/net/context ../golang.org/x/net/context/context.go:140: can inline Background as: func() Context { return background } ../golang.org/x/net/context/context.go:149: can inline TODO as: func() Context { return todo } @@ -205,7 +205,7 @@ $ go build -gcflags="-m" golang.org/x/net/context 值得一提的是你经常需要禁用优化来得到一个关于发生了什么的更简单的视图,因为优化可能会修改操作序列,增加代码,删除代码或是对代码进行变换。开启了优化,将一行 Go 代码与优化后的输出对应起来将更难,进行性能测试也会更难,因为优化可能带来不止一处变化。可以通过 `-N` 来禁用优化,通过 `-l` 来禁用内联。 ``` -$ go build -gcflags="-l -N" +$ Go build -gcflags="-l -N" ``` 一旦优化被禁用,你调试就不会被代码变化影响,进行性能测试也不会受不止一处变化的影响。 @@ -215,7 +215,7 @@ $ go build -gcflags="-l -N" 如果你在 lexer 上工作,编译器提供了一个标志在检查源码时调试 lexer。 ``` -$ go build -gcflags="-x" +$ Go build -gcflags="-x" # hello lex: PACKAGE lex: ident main diff --git a/published/tech/20161017-Variadic-functions-in-Go.md b/published/tech/20161017-Variadic-functions-in-Go.md index 0b284c5cf..cee88afbe 100644 --- a/published/tech/20161017-Variadic-functions-in-Go.md +++ b/published/tech/20161017-Variadic-functions-in-Go.md @@ -62,7 +62,7 @@ func main() { 编译运行代码后输出: ``` -> go install github.com/mlowicki/lab && ./bin/lab +> Go install github.com/mlowicki/lab && ./bin/lab value: []string{"one", "two", "three"} type: []string length: 3 @@ -101,7 +101,7 @@ func main() { ``` ``` -> go install github.com/mlowicki/lab && ./bin/lab +> Go install github.com/mlowicki/lab && ./bin/lab # github.com/mlowicki/lab src/github.com/mlowicki/lab/lab.go:11: cannot use numbers (type []int) as type int in argument to f ``` @@ -128,7 +128,7 @@ func main() { via: https://medium.com/golangspec/variadic-functions-in-go-13c33182b851 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[nicedevcn](https://github.com/nicedevcn) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20161024-Conversions-in-Go.md b/published/tech/20161024-Conversions-in-Go.md index 5102e8a71..7d8863ddd 100644 --- a/published/tech/20161024-Conversions-in-Go.md +++ b/published/tech/20161024-Conversions-in-Go.md @@ -16,7 +16,7 @@ func main() { ``` ``` -> go build +> Go build # github.com/mlowicki/lab ./lab.go:6: cannot convert "2" to type int ./lab.go:6: invalid operation: 1 + "2" (mismatched types int and string) @@ -319,7 +319,7 @@ A Strings 基本上是字节的切片: ```go -text := "abł" +text := "ab ł" for i := 0; i < len(text); i++ { fmt.Println(text[i]) } @@ -340,7 +340,7 @@ for i := 0; i < len(text); i++ { *range* 循环有助于迭代 Unicode 定义的码位( 码位在 Golang 中被称为 *rune* ) ```go -text := "abł" +text := "ab ł" for _, s := range text { fmt.Printf("%q %#v\n", s, s) } @@ -356,15 +356,15 @@ for _, s := range text { > 想了解更多类似 *%q* 和 *%v* 这样的占位符,可以看 [fmt](https://golang.org/pkg/fmt/) 包的文档 -更多的讨论可在 [《Golang的字符串,字节,rune 和字符》](https://blog.golang.org/strings)。在这个快速解释之后,在字符串和字节切片之间的转换应该不会再难以理解。 +更多的讨论可在 [《Golang 的字符串,字节,rune 和字符》](https://blog.golang.org/strings)。在这个快速解释之后,在字符串和字节切片之间的转换应该不会再难以理解。 ### string ↔ slice of bytes ```go -bytes := []byte("abł") +bytes := []byte("ab ł") text := string(bytes) fmt.Printf("%#v\n", bytes) // []byte{0x61, 0x62, 0xc5, 0x82} -fmt.Printf("%#v\n", text) // "abł" +fmt.Printf("%#v\n", text) // "ab ł" ``` 切片由被转换 string 的 utf8 编码字节组成。 @@ -372,10 +372,10 @@ fmt.Printf("%#v\n", text) // "abł" ### string ↔ slice of runes ```go -runes := []rune("abł") +runes := []rune("ab ł") fmt.Printf("%#v\n", runes) // []int32{97, 98, 322} fmt.Printf("%+q\n", runes) // ['a' 'b' '\u0142'] -fmt.Printf("%#v\n", string(runes)) // "abł" +fmt.Printf("%#v\n", string(runes)) // "ab ł" ``` 从被转换 string 中创建的切片是由 Unicode 编码的码位( rune )组成。 @@ -395,7 +395,7 @@ fmt.Printf("%#v\n", string(runes)) // "abł" via: https://medium.com/golangspec/conversions-in-go-4301e8d84067 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[magichan](https://github.com/magichan) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20161103-Methods-in-Go-part-ii.md b/published/tech/20161103-Methods-in-Go-part-ii.md index 6039b71e6..7702dfae4 100644 --- a/published/tech/20161103-Methods-in-Go-part-ii.md +++ b/published/tech/20161103-Methods-in-Go-part-ii.md @@ -68,7 +68,7 @@ func (t *T) N() { t.name = "changed" } func main() { - t := T{name: "Michał"} + t := T{name: "Micha ł"} (*T).M(&t) fmt.Println(t.name) (*T).N(&t) @@ -79,7 +79,7 @@ func main() { 输出: ```go -Michał +Micha ł changed ``` @@ -103,7 +103,7 @@ type I interface { } func main() { - t1 := T{name: "Michał"} + t1 := T{name: "Micha ł"} t2 := T{name: "Tom"} m := I.M m(t1) @@ -115,7 +115,7 @@ func main() { 输出: ``` -Michał +Micha ł Tom ``` @@ -131,7 +131,7 @@ type T struct { func (t *T) M(string) {} func (t T) N(float64) {} func main() { - t := T{name: "Michał"} + t := T{name: "Micha ł"} m := t.M n := t.N m("foo") @@ -161,13 +161,13 @@ type U struct { } func main() { - u := U{T{name: "Michał"}} + u := U{T{name: "Micha ł"}} fmt.Println(u.M()) } ``` -上面的 Go 程序输出 `Michał` 是完全正确的。说嵌入到结构类型中属性的方法属于该类型的方法集是有确切原因的: +上面的 Go 程序输出 `Micha ł` 是完全正确的。说嵌入到结构类型中属性的方法属于该类型的方法集是有确切原因的: ### #1 @@ -202,7 +202,7 @@ type U struct { } func main() { - u := U{T{name: "Michał"}} + u := U{T{name: "Micha ł"}} PrintMethodSet(u) PrintMethodSet(&u) } @@ -242,7 +242,7 @@ type U struct { } func main() { - u := U{&T{name: "Michał"}} + u := U{&T{name: "Micha ł"}} PrintMethodSet(u) PrintMethodSet(&u) } @@ -263,7 +263,7 @@ Method: N via: https://medium.com/golangspec/methods-in-go-part-ii-2b4cc42c5cb6 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[Tyrodw](https://github.com/tyrodw) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20161112-synchronized-goroutines-part-1.md b/published/tech/20161112-synchronized-goroutines-part-1.md index 7856699c5..d9a0cb788 100644 --- a/published/tech/20161112-synchronized-goroutines-part-1.md +++ b/published/tech/20161112-synchronized-goroutines-part-1.md @@ -1,6 +1,6 @@ 首发于:https://studygolang.com/articles/14118 -# goroutine 的同步(第一部分) +# Goroutine 的同步(第一部分) ![image](https://raw.githubusercontent.com/studygolang/gctt-images/master/sync-goroutine/part1.jpeg) @@ -18,11 +18,11 @@ func main() { var v int var wg sync.WaitGroup wg.Add(2) - go func() { + Go func() { v = 1 wg.Done() }() - go func() { + Go func() { fmt.Println(v) wg.Done() }() @@ -30,22 +30,22 @@ func main() { } ``` -两个 goroutine 都对共享变量 *v* 进行操作。其中一个赋新值(写操作)而另一个打印变量的值(读操作)。 +两个 Goroutine 都对共享变量 *v* 进行操作。其中一个赋新值(写操作)而另一个打印变量的值(读操作)。 -> *sync 包中的 [WaitGroup](https://golang.org/pkg/sync/#WaitGroup) 被用来等待两个非 main 的 goroutine 结束。否则,我们甚至都不能确保其中任意一个 goroutine 有被启动。* +> *sync 包中的 [WaitGroup](https://golang.org/pkg/sync/#WaitGroup) 被用来等待两个非 main 的 Goroutine 结束。否则,我们甚至都不能确保其中任意一个 Goroutine 有被启动。* -由于不同的 goroutine 是相互独立的任务,它们进行的操作之间没有任何隐含的顺序。在上面的例子中,我们不清楚会打印出 `0` 还是 `1`。如果在 `fmt.Println` 被触发时,另一个 goroutine 已经执行了赋值语句 `v = 1`,那么输出会是 `1`。然而,在程序真正被执行之前一切都是未知的。换句话说,赋值语句和调用 `fmt.Println` 是无序的 —— 它们是并发的。 +由于不同的 Goroutine 是相互独立的任务,它们进行的操作之间没有任何隐含的顺序。在上面的例子中,我们不清楚会打印出 `0` 还是 `1`。如果在 `fmt.Println` 被触发时,另一个 Goroutine 已经执行了赋值语句 `v = 1`,那么输出会是 `1`。然而,在程序真正被执行之前一切都是未知的。换句话说,赋值语句和调用 `fmt.Println` 是无序的 —— 它们是并发的。 如果我们无法通过查看源码断定程序的行为,这是不好的。Go 的规范引入了内存操作(读和写)的偏序(partial order)关系(*先行发生原则* *happen before*)。这个顺序使我们能够推断程序的行为。另外,这门语言中的一些机制允许程序员强制实行操作的顺序。 -在单个 goroutine 中,所有操作的顺序都与它们在源码中的位置一致。 +在单个 Goroutine 中,所有操作的顺序都与它们在源码中的位置一致。 ```go wg.Add(2) wg.Wait() ``` -上面例子中的函数调用是有序的,因为它们在同一个 goroutine 中 —— `wg.Add(2)` 先于 `wg.Wait()` 被执行。 +上面例子中的函数调用是有序的,因为它们在同一个 Goroutine 中 —— `wg.Add(2)` 先于 `wg.Wait()` 被执行。 ## 1. 信道(Channel) @@ -73,7 +73,7 @@ wg.Wait() 给 *v* 赋值 → 发送到 *ch* → 从 *ch* 接收 → 打印 *v* -第一个箭头和第三个箭头都是由同一个 goroutine 中的顺序确定的。使用 channel 进行通信带来了第二个箭头。最终,分散在两个 goroutine 中的操作是有序的。 +第一个箭头和第三个箭头都是由同一个 Goroutine 中的顺序确定的。使用 channel 进行通信带来了第二个箭头。最终,分散在两个 Goroutine 中的操作是有序的。 ## 2. sync 包 @@ -107,7 +107,7 @@ wg.Wait() ## 资源 - [Go 的内存模型 —— Go 编程语言](https://golang.org/ref/mem) ->The Go memory model specifies the conditions under which reads of a variable in one goroutine can be guaranteed to… +>The Go memory model specifies the conditions under which reads of a variable in one Goroutine can be guaranteed to…
*golang.org* - [像个 Gopher 一样使用 *panic*](https://medium.com/golangspec/panicking-like-a-gopher-367a9ce04bb8) @@ -122,7 +122,7 @@ wg.Wait() *[Synchronization](https://medium.com/tag/synchronization?source=post)* *[Goroutines](https://medium.com/tag/goroutines?source=post)* -**喜欢读吗?给 Michał Łowicki 一些掌声吧。** +**喜欢读吗?给 Micha ł Ł owicki 一些掌声吧。** 简单鼓励下还是大喝采,根据你对这篇文章的喜欢程度鼓掌吧。 @@ -130,7 +130,7 @@ wg.Wait() via: https://medium.com/golangspec/synchronized-goroutines-part-i-4fbcdd64a4ec -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[krystollia](https://github.com/krystollia) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20161115-Anatomy-of-a-function-call-in-Go.md b/published/tech/20161115-Anatomy-of-a-function-call-in-Go.md index 2731c2074..596103a2e 100644 --- a/published/tech/20161115-Anatomy-of-a-function-call-in-Go.md +++ b/published/tech/20161115-Anatomy-of-a-function-call-in-Go.md @@ -1,6 +1,6 @@ -# 剖析 go 语言的函数调用 +# 剖析 Go 语言的函数调用 -让我们来看几个 go 函数调用的简单例子。通过研究 go 编译器为这些函数生成的汇编代码,我们来看看函数调用是如何工作的。这个课题对于一篇小小的文章来讲有点费劲,但是别担心,汇编语言是非常简单的,连 CPU 都能理解它。 +让我们来看几个 Go 函数调用的简单例子。通过研究 Go 编译器为这些函数生成的汇编代码,我们来看看函数调用是如何工作的。这个课题对于一篇小小的文章来讲有点费劲,但是别担心,汇编语言是非常简单的,连 CPU 都能理解它。 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/anatomy-of-a-function/1_CKK4XrLm3ylzsQzNbOaroQ.png) *作者:Rob Baines https://github.com/telecoda/inktober-2016* @@ -13,9 +13,9 @@ func add(a, b int) int { } ``` -通过 `go build -gcflags '-N -l'`,我们禁用了编译优化,以使生成的汇编代码更加容易读懂。然后我们就可以用 go 工具 `objdump -s main.add func` (func是我们用的包名,也是 go build 生成的可执行文件的名称),将这个函数对应的汇编代码导出来。 +通过 `go build -gcflags '-N -l'`,我们禁用了编译优化,以使生成的汇编代码更加容易读懂。然后我们就可以用 Go 工具 `objdump -s main.add func` (func 是我们用的包名,也是 Go build 生成的可执行文件的名称),将这个函数对应的汇编代码导出来。 -如果你以前从来没有接触过汇编语言,那么恭喜,现在它对你来说是个新的东西。我在 mac 电脑上做的试验,所以汇编代码是英特尔 64位 的。 +如果你以前从来没有接触过汇编语言,那么恭喜,现在它对你来说是个新的东西。我在 Mac 电脑上做的试验,所以汇编代码是英特尔 64 位 的。 ``` main.go:20 0x22c0 48c744241800000000 MOVQ $0x0, 0x18(SP) @@ -37,8 +37,8 @@ main.go:21 0x22db c3 RET - MOVQ, ADDQ 以及 RET 是指令。它们告诉 CPU 要做什么操作。跟在指令后面的是参数,告诉 CPU 要对谁进行操作。 - SP, AX 及 CX 是 CPU 的寄存器,是 CPU 存储工作用到的变量的地方。除了这几个,CPU 还会用到其它的一些寄存器。 -- SP 是个特殊的寄存器,它用于存储当前的栈指针。栈是用于存储局部变量、函数的参数及函数返回地址的内存区域。每个 goroutine 对应一个栈。当一个函数调用另一个函数,被调用函数再继续调用别的函数,每个函数都会在栈上得到一个内存区域。函数调用时,SP 的值会减去被调用函数所需栈空间大小,这样就得到了一块供被调用函数使用的内存区域。 -- 0x8(SP) 指向比 SP 所指内存位置往后8个字节的位置。 +- SP 是个特殊的寄存器,它用于存储当前的栈指针。栈是用于存储局部变量、函数的参数及函数返回地址的内存区域。每个 Goroutine 对应一个栈。当一个函数调用另一个函数,被调用函数再继续调用别的函数,每个函数都会在栈上得到一个内存区域。函数调用时,SP 的值会减去被调用函数所需栈空间大小,这样就得到了一块供被调用函数使用的内存区域。 +- 0x8(SP) 指向比 SP 所指内存位置往后 8 个字节的位置。 所以,几个要素包括:内存位置、CPU 寄存器、在内存和寄存器之间移动数据的指令,以及对寄存器的操作。这些差不多就是 CPU 所做的全部。 @@ -55,7 +55,7 @@ main.go:21 0x22db c3 RET [这里我假定 `a` 就是第一个参数,`b` 就是第二个。我不确定这是正确的。我们可能需要更多的试验才能找到正确答案,不过这篇文章已经够长了。] -那么有点玄妙的第一行是做什么的呢?`MOVQ $0x0, 0x18(SP)` 将 0 存入内存地址 SP+0x18,注意到 SP+0x18 正是存储返回值的地址。我们可以猜测这是因为 go 对于未初始化变量会赋值为 0。即便不是必要,编译器也会这么做,因为我们禁用了编译优化。 +那么有点玄妙的第一行是做什么的呢?`MOVQ $0x0, 0x18(SP)` 将 0 存入内存地址 SP+0x18,注意到 SP+0x18 正是存储返回值的地址。我们可以猜测这是因为 Go 对于未初始化变量会赋值为 0。即便不是必要,编译器也会这么做,因为我们禁用了编译优化。 来看看我们学到了什么。 @@ -104,7 +104,7 @@ func add3(a int) int { - `MOVQ BP, 0x8(SP)` 将寄存器 BP 中的值存储在 SP+8 的位置,`LEAQ 0x8(SP), BP` 将 SP+8 所对应的地址存储在 BP 中。这帮助我们建立了栈空间(栈帧) 的链。这有点玄妙,但恐怕这篇文章不会对此做解释了。 - 这一段的最后是 `MOVQ $0x0, 0x20(SP)`。这和我们刚讨论的上一个函数很类似,是将返回值初始化为 0。 -汇编的下一行对应于源码的 `b := 3`。这个命令 `MOVQ $0x3, 0(SP)` 将 3 放入内存 SP+0 处。这个解决了我们的疑问。当我们把 SP 的值减去 0x10=16,我们空出了能容纳 2 个 8字节 变量的空间:局部变量 `b` 存储于 SP+0,而 BP 的值存储于 SP+0x8。 +汇编的下一行对应于源码的 `b := 3`。这个命令 `MOVQ $0x3, 0(SP)` 将 3 放入内存 SP+0 处。这个解决了我们的疑问。当我们把 SP 的值减去 0x10=16,我们空出了能容纳 2 个 8 字节 变量的空间:局部变量 `b` 存储于 SP+0,而 BP 的值存储于 SP+0x8。 后面的 6 行对应于 `return a + b`。这包括从内存加载 `a` 和 `b`, 将它们相加,以及返回计算结果。让我们按顺序来看每一行。 diff --git a/published/tech/20161115-Synchronized-goroutines-part-2.md b/published/tech/20161115-Synchronized-goroutines-part-2.md index 711f27d7a..7d065d93f 100644 --- a/published/tech/20161115-Synchronized-goroutines-part-2.md +++ b/published/tech/20161115-Synchronized-goroutines-part-2.md @@ -1,13 +1,13 @@ 首发于:https://studygolang.com/articles/14478 -# goroutine 的同步(第二部分) +# Goroutine 的同步(第二部分) > Channel 通信 第一部分介绍了发送与接收操作之间最直观的顺序关系: > *向一个 Channel 中发送数据先于接收数据。* -于是,我们能够控制分布于两个 goroutine 中的操作的顺序。 +于是,我们能够控制分布于两个 Goroutine 中的操作的顺序。 ```go var v int @@ -82,7 +82,7 @@ go func() { }() ``` -现在第二个 goroutine 会往 channel 中发送数据,它需要等待第一个 goroutine 中的赋值操作 `v = 1` 完成。发送操作在对应的接收操作后才能完成。 +现在第二个 Goroutine 会往 channel 中发送数据,它需要等待第一个 Goroutine 中的赋值操作 `v = 1` 完成。发送操作在对应的接收操作后才能完成。 `v = 1` → `<-ch` → `ch <- 1` 结束 → `fmt.Println(v)` @@ -133,7 +133,7 @@ fmt.Println(<-ch) 对于有缓存的 channel,目前为止提到的所有规则都是成立的,除了说接收发生在发送结束之前这一条。原因很简单,(在缓存未满时),无需准备好的接受者,发送操作就可以结束。 ->*对于容量为 c 的 channel,第 k 个接收发生在第 (k+c) 个发送完成之前。* +>*对于容量为 c 的 channel,第 k 个接收发生在第 (k + c) 个发送完成之前。* 假定缓存容量被设置为 3。前 3 个向 channel 发送数据的操作即使没有相应的接收语句也可以返回。但是为了第 4 个发送操作完成,必须有至少一个接收操作完成。 @@ -169,11 +169,11 @@ wg.Wait() > Suppose that Go program starts two goroutines: > medium.com -- [go 的内存模型 —— go 编程语言](https://golang.org/ref/mem) ->The Go memory model specifies the conditions under which reads of a variable in one goroutine can be guaranteed to… +- [go 的内存模型 —— Go 编程语言](https://golang.org/ref/mem) +>The Go memory model specifies the conditions under which reads of a variable in one Goroutine can be guaranteed to… > golang.org -- [go 语言规范 —— go 编程语言](https://golang.org/ref/spec) +- [go 语言规范 —— Go 编程语言](https://golang.org/ref/spec) >Go is a general-purpose language designed with systems programming in mind. It is strongly typed and garbage-collected… > golang.org @@ -185,7 +185,7 @@ wg.Wait() *[Channels](https://medium.com/tag/channel?source=post)* *[Synchronization](https://medium.com/tag/synchronization?source=post)* -**喜欢读吗?给 Michał Łowicki 一些掌声吧。** +**喜欢读吗?给 Micha ł Ł owicki 一些掌声吧。** 简单鼓励下还是大喝采,根据你对这篇文章的喜欢程度鼓掌吧。 @@ -193,7 +193,7 @@ wg.Wait() via: https://medium.com/golangspec/synchronized-goroutines-part-ii-b1130c815c9d -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[krystollia](https://github.com/krystollia) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20161118-Detect-Locks-Passed-by-Value-in-Go.md b/published/tech/20161118-Detect-Locks-Passed-by-Value-in-Go.md index 5b7b6e9bc..8b25d6332 100644 --- a/published/tech/20161118-Detect-Locks-Passed-by-Value-in-Go.md +++ b/published/tech/20161118-Detect-Locks-Passed-by-Value-in-Go.md @@ -18,7 +18,7 @@ func f() { ``` ``` -> go tool vet vet.go +> Go tool vet vet.go vet.go:8: unreachable code vet.go:6: missing argument for Printf("%d"): format reads arg 1, have only 0 args ``` @@ -69,7 +69,7 @@ main.(*T).Lock(0x4201162a8) 运行上述程序得到了糟糕的结果,根本原因是把 receiver 按值传递给 Unlock 方法,所以 `t.lock.Unlock()` 实际上是由 lock 的副本调用的。我们很容易忽视这点,特别在更大型的程序中。Go 编译器不会检测这方面,因为这可能是程序员有意为之。该 vet 工具登场啦... ``` -> go tool vet vet.go +> Go tool vet vet.go vet.go:13: Unlock passes lock by value: main.T ``` @@ -91,7 +91,7 @@ func main() { ``` ``` -> go tool vet lab.go +> Go tool vet lab.go lab.go:9: fun passes lock by value: main.T contains sync.WaitGroup contains sync.noCopy lab.go:13: function call copies lock value: main.T contains sync.WaitGroup contains sync.noCopy ``` @@ -108,7 +108,7 @@ vet 工具的源文件放在 `/src/cmd/vet` 路径下。vet 的每个选项都 via: https://medium.com/golangspec/detect-locks-passed-by-value-in-go-efb4ac9a3f2b -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[mbyd916](https://github.com/mbyd916) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20161206-Synchronized-goroutines-part-III.md b/published/tech/20161206-Synchronized-goroutines-part-III.md index 09d5d5be0..8a8f41443 100644 --- a/published/tech/20161206-Synchronized-goroutines-part-III.md +++ b/published/tech/20161206-Synchronized-goroutines-part-III.md @@ -149,7 +149,7 @@ func getCapitalCity(country string) string { *[Synchronization](https://medium.com/tag/synchronization?source=post)* *[Software Development](https://medium.com/tag/software-development?source=post)* -**喜欢读吗?给 Michał Łowicki 一些掌声吧。** +**喜欢读吗?给 Micha ł Ł owicki 一些掌声吧。** 简单鼓励下还是大喝采,根据你对这篇文章的喜欢程度鼓掌吧。 @@ -157,7 +157,7 @@ func getCapitalCity(country string) string { via: https://medium.com/golangspec/synchronized-goroutines-part-iii-c60bcfeefd2a -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[krystollia](https://github.com/krystollia) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20161218-modern-garbage-collection.md b/published/tech/20161218-modern-garbage-collection.md index 01562c596..17d08dcf6 100644 --- a/published/tech/20161218-modern-garbage-collection.md +++ b/published/tech/20161218-modern-garbage-collection.md @@ -10,11 +10,11 @@ [这是新版本(Go 1.5)垃圾收集器的首次公告](https://blog.golang.org/go15gc): -> Go 正在构建一个垃圾收集器(GC),不止适用于 2015 年,甚至适用于 2025 年,甚至更长时间 ... go 1.5 及以后的版本,重点解决 gc 中 stop-the-world 问题不再是安全可靠语言的障碍。未来应用程序可以毫不费力地随硬件一起扩展,随着硬件变得越来越强大,GC 不再是成为更好、更具可扩展性的软件的障碍。适用于未来十年甚至更久。 +> Go 正在构建一个垃圾收集器(GC),不止适用于 2015 年,甚至适用于 2025 年,甚至更长时间 ... Go 1.5 及以后的版本,重点解决 gc 中 stop-the-world 问题不再是安全可靠语言的障碍。未来应用程序可以毫不费力地随硬件一起扩展,随着硬件变得越来越强大,GC 不再是成为更好、更具可扩展性的软件的障碍。适用于未来十年甚至更久。 Go 官方团队声称不仅解决了 GC 暂停的问题,而且还让整个事情变得无脑: - + > 在更高的层次上,解决性能问题的一种方法是添加 GC 旋钮(knob),每个性能问题添加一个。然后,程序员可以旋转旋钮(knob)以搜索适合其应用的设置。不利的一面是,每年使用一个或两个新旋钮(knob),十年之后,你最终会得到 GC 旋钮(knob)特纳就业法案(Turner Employment Act 应该的意思是:需要详细的文档描述说明各个 knob 的详细信息)。Go 不会走那条路。相反,我们只提供了一个叫做 GOGC 的旋钮(knob)。 @@ -65,7 +65,7 @@ Stop-the-world(STW)标记/清除是本科计算机科学课程中最常用 -问题是简单的 STW 标记/清除伸缩性非常差。随着 cpu 核数增加、堆分配速率更大,此算法将无法正常运行。但是,有时你确实有一个小堆,并且来自上述简单 GC 的暂停时间也足够好!在这种情况下,也许您仍然希望使用这种方法将内存开销保持在较低水平。 +问题是简单的 STW 标记/清除伸缩性非常差。随着 CPU 核数增加、堆分配速率更大,此算法将无法正常运行。但是,有时你确实有一个小堆,并且来自上述简单 GC 的暂停时间也足够好!在这种情况下,也许您仍然希望使用这种方法将内存开销保持在较低水平。 另一方面,也许你在拥有数十个内核的机器上使用数百 GB 的堆。也许您的服务器正在进行金融市场交易或运行搜索引擎,因此低停顿时间对您来说非常重要。在这些情况下,您可能愿意使用一种算法,该算法为实现在后台以低暂停时间进行垃圾回收,会降低程序速度。 @@ -90,7 +90,7 @@ Stop-the-world(STW)标记/清除是本科计算机科学课程中最常用 他们还介绍了一些缺点: -* **兼容性**:实现分代收集器需要能够在内存中移动内容,并在程序在某些情况下写入指针时执行额外的工作。这意味着 GC 必须与编译器紧密集成。C ++没有分代 GC 。 +* **兼容性**:实现分代收集器需要能够在内存中移动内容,并在程序在某些情况下写入指针时执行额外的工作。这意味着 GC 必须与编译器紧密集成。C ++ 没有分代 GC 。 * * **堆开销**:这些收集器通过在各种“空间”之间来回复制分配来工作。因为必须有空间可以复制到,所以这些收集器会产生一些堆开销。此外,它们需要维护各种指针映射(记住的集合),这进一步增加了开销。 * **暂停分布**:虽然许多 GC 暂停现在非常快,但仍有一些需要在整个堆上进行完整的 标记/清除。 @@ -113,7 +113,7 @@ Stop-the-world(STW)标记/清除是本科计算机科学课程中最常用 * **GC 吞吐量**:清除堆所需的时间与堆的大小成比例。简单来说,程序使用的内存越多,释放内存的速度就越慢,相对处理有效工作时间,计算机用于收集垃圾时间也就越多。如果您的程序根本不并行化,但是您可以无限制地继续向 GC 添加核心数,是唯一不好的地方。 * **紧凑**:由于没有压缩,程序最终会将堆碎片化。我将在下面详细讨论堆碎片。在缓存中整齐排列也不会带来收益。 -* **程序吞吐量**:由于在每一轮循环中 gc 都需要做大量的工作,其将从 应用程序本身偷取 cpu 时间,并使其变慢 +* **程序吞吐量**:由于在每一轮循环中 gc 都需要做大量的工作,其将从 应用程序本身偷取 CPU 时间,并使其变慢 * **暂停分布**:任何与程序并发运行的垃圾收集器都可能遇到 Java 世界所称的并发模式故障:程序创建垃圾的速度比 GC 线程清理垃圾的速度还快。在这种情况下,运行时别无选择,只能完全停止程序,等待 GC 循环完成。因此,当 Go 声明 GC 暂停时间很短时,这种声明只适用于 GC 有足够的 CPU 时间和内存空间超过主程序(分配内存的速度快过主程序)时。另外,go 编译器不支持稳定停下线程的特性,也就是说,暂停时间的长短很大程度上取决于你的程序什么时候运行,例如:在单一线程上解码一个巨大的 base64 会使暂停时间升高。 * **堆开销**:因为通过 标记/清除 收集堆非常慢,所以需要大量空闲空间来确保不会出现“并发模式故障”。Go 默认的堆开销为 100%,它将使程序所需的内存增加一倍。 diff --git a/published/tech/20161223-Automatic-semicolon-insertion-in-Go.md b/published/tech/20161223-Automatic-semicolon-insertion-in-Go.md index c86590d3e..33ba918db 100644 --- a/published/tech/20161223-Automatic-semicolon-insertion-in-Go.md +++ b/published/tech/20161223-Automatic-semicolon-insertion-in-Go.md @@ -68,7 +68,7 @@ f() f()(g()) ``` -第一个片段没有打印任何东西,但是第二个给出内部函数调用。这是因为前面提到的第4条规则:因为最后的记号都是圆括号,所以两行后面都加了分号。 +第一个片段没有打印任何东西,但是第二个给出内部函数调用。这是因为前面提到的第 4 条规则:因为最后的记号都是圆括号,所以两行后面都加了分号。 ```go f(); @@ -143,7 +143,7 @@ golangspec 已有 300 多关注者。这并不是它的目标,但有越来越 via: https://medium.com/golangspec/automatic-semicolon-insertion-in-go-1990338f2649 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[themoonbear](https://github.com/themoonbear) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20161229-Comparison-operators-in-Go.md b/published/tech/20161229-Comparison-operators-in-Go.md index 7a879d46d..e71c65d2e 100644 --- a/published/tech/20161229-Comparison-operators-in-Go.md +++ b/published/tech/20161229-Comparison-operators-in-Go.md @@ -222,13 +222,13 @@ t = b fmt.Println(t) ``` -产生一个错误,不能使用 b (bool类型)分配给 T 类型。关于常量(有类型和无类型)更详尽的介绍在官方[博客](https://blog.golang.org/constants)上。 +产生一个错误,不能使用 b (bool 类型)分配给 T 类型。关于常量(有类型和无类型)更详尽的介绍在官方[博客](https://blog.golang.org/constants)上。 --- via: https://medium.com/golangspec/comparison-operators-in-go-910d9d788ec0 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[themoonbear](https://github.com/themoonbear) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20170110-Slice-expressions-in-Go.md b/published/tech/20170110-Slice-expressions-in-Go.md index 5844d2cd0..d81ae6539 100644 --- a/published/tech/20170110-Slice-expressions-in-Go.md +++ b/published/tech/20170110-Slice-expressions-in-Go.md @@ -38,7 +38,7 @@ numbers := [5]int{1, 2, 3, 4, 5} fmt.Println((&numbers)[1:3]) // [2, 3] ``` -Slice 的索引 low 和 high 可以省略,low 的默认值是0,high 的默认值为 slice 的长度: +Slice 的索引 low 和 high 可以省略,low 的默认值是 0,high 的默认值为 slice 的长度: ```go fmt.Println("foo"[:2]) // "fo" @@ -83,7 +83,7 @@ main.main() ## 完整表达式 -这种方法可以控制结果 slice 的容量,但是只能用于 array 和指向 array 或 slice 的指针( string 不支持),在简略表达式中结果 slice 的容量是从索引low开始的最大可能容量( slice 的简略表达式): +这种方法可以控制结果 slice 的容量,但是只能用于 array 和指向 array 或 slice 的指针( string 不支持),在简略表达式中结果 slice 的容量是从索引 low 开始的最大可能容量( slice 的简略表达式): ```go numbers := [10]int{0,1,2,3,4,5,6,7,8,9} @@ -184,7 +184,7 @@ fmt.Println(cap(s2)) via: https://medium.com/golangspec/slice-expressions-in-go-963368c20765 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[bizky]](https://github.com/bizky) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20170114-style-packages.md b/published/tech/20170114-style-packages.md index 1b7df38b2..0f076ee98 100644 --- a/published/tech/20170114-style-packages.md +++ b/published/tech/20170114-style-packages.md @@ -1,4 +1,6 @@ -# Go语言中包的风格指南 +首发于:https://studygolang.com/articles/11823 + +# Go 语言中包的风格指南 Go 语言也有自己的命名与代码组织规则。漂亮的代码,布局清晰、易读易懂,就像是设计严谨的 API 一样。拿到代码,用户首先看到和接触的就是布局、命名还有包的结构。 @@ -151,7 +153,7 @@ import ( 例如, - $ go get cloud.google.com/go/datastore + $ Go get cloud.google.com/go/datastore 在后台去查看来自 `https://code.googlesource.com/gocloud` 的源码,把它加到你的工作区当中去,这个工作区是定义在 $GOPATH/src/cloud.google.com/go/datastore 下面的。 diff --git a/published/tech/20170131-Selectors-in-Go.md b/published/tech/20170131-Selectors-in-Go.md index d0024be07..df4e10a5a 100644 --- a/published/tech/20170131-Selectors-in-Go.md +++ b/published/tech/20170131-Selectors-in-Go.md @@ -2,7 +2,7 @@ # Go 语言中的选择器 -在 Go 语言中,表达式 `foo.bar` 可能表示两件事。如果 *foo* 是一个包名,那么表达式就是一个所谓的`限定标识符`,用来引用包 *foo* 中的导出的标识符。由于它只用来处理导出的标识符,*bar* 必须以大写字母开头(译注:如果首字母大写,则可以被其他的包访问;如果首字母小写,则只能在本包中使用): +在 Go 语言中,表达式 `foo.bar` 可能表示两件事。如果 *foo* 是一个包名,那么表达式就是一个所谓的 ` 限定标识符 `,用来引用包 *foo* 中的导出的标识符。由于它只用来处理导出的标识符,*bar* 必须以大写字母开头(译注:如果首字母大写,则可以被其他的包访问;如果首字母小写,则只能在本包中使用): ```go package foo @@ -57,11 +57,11 @@ func (t T) IsStillYoung() bool { return t.age <= 18 } func main() { - t := T{30, E{"Michał"}} + t := T{30, E{"Micha ł"}} fmt.Println(t.IsStillYoung()) // false fmt.Println(t.age) // 30 - t.SayHi() // Hi Michał! - fmt.Println(t.name) // Michał + t.SayHi() // Hi Micha ł! + fmt.Println(t.name) // Micha ł } ``` @@ -99,14 +99,14 @@ func main() { ``` * *c* 的深度是 `v.c`,其值为 0。这是因为字段是在 *C* 中声明的 -* `v.b` 中 *b* 的深度是 1。这是因为它的字段定义在类型 *B* 中,其(类型B)又嵌入在 *C* 中 +* `v.b` 中 *b* 的深度是 1。这是因为它的字段定义在类型 *B* 中,其(类型 B)又嵌入在 *C* 中 * `v.a` 中 *a* 的深度是 2。这是因为需要遍历两个匿名字段(*B* 和 *A*)才能访问它 ## 有效选择器 go 语言中有关哪些选择器有效,哪些无效有着明确规则。让我们来深入了解他们。 -### 唯一性+最浅深度 +### 唯一性 + 最浅深度 当 *T* 不是指针或者接口类型,第一条规则适用于类型 `T` 与 `*T`。选择器 *foo.bar* 表示字段和方法在定义了 *bar* 的类型 *T* 中的最浅深度。在这样的深度,恰好可以定义一个(唯一的)这样的字段或者方法([源代码](https://play.golang.org/p/mGtRxnrAQR)): @@ -243,8 +243,8 @@ func main() { via:https://medium.com/golangspec/selectors-in-go-c53a016702cf -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[cureking](https://github.com/cureking) 校对:[polaris1119](https://github.com/polaris1119) -本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go中文网](https://studygolang.com/) 荣誉推出 +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20170213-interface-in-go-part-1.md b/published/tech/20170213-interface-in-go-part-1.md index e6a97a187..d8cb0bb1f 100644 --- a/published/tech/20170213-interface-in-go-part-1.md +++ b/published/tech/20170213-interface-in-go-part-1.md @@ -1,10 +1,10 @@ 首发于:https://studygolang.com/articles/14580 -# Go语言接口(第一部分) +# Go 语言接口(第一部分) ![avatar](https://raw.githubusercontent.com/studygolang/gctt-images/master/interface-in-go/part1-1.jpg) -接口提升了代码的弹性与拓展性,同时它也是 go 语言实现多态的一种方式。接口允许通过一些必要的行为来实现,而不再要求设置特定类型。而这个行为就是通过一些方法设置来定义的: +接口提升了代码的弹性与拓展性,同时它也是 Go 语言实现多态的一种方式。接口允许通过一些必要的行为来实现,而不再要求设置特定类型。而这个行为就是通过一些方法设置来定义的: ```go type I interface { @@ -45,13 +45,13 @@ func Hello(i I) { fmt.Printf("Hi, my name is %s\n", i.M()) } func main() { - Hello(T{name: "Michał"}) // "Hi, my name is Michał" + Hello(T{name: "Micha ł"}) // "Hi, my name is Micha ł" } ``` 在 function Hello 中,方法调用了 `i.M()`。 这个过程概括一下就是,只要来自不同 type 的方法是通过 type 来实现 interface I,就可以被调用。 -go 语言的突出特点就是其 `interface` 是隐式实现的。程序员不需要指定 type T 实现了 interface I。这个工作由 go 的编译器完成(不需要派一个人去做机器的工作)。这种行为中的实现方式之所以很赞,是因为定义 interface 这件事情是由已经写好的 type 自动实现的(不需要为之做任何改变)。 +go 语言的突出特点就是其 `interface` 是隐式实现的。程序员不需要指定 type T 实现了 interface I。这个工作由 Go 的编译器完成(不需要派一个人去做机器的工作)。这种行为中的实现方式之所以很赞,是因为定义 interface 这件事情是由已经写好的 type 自动实现的(不需要为之做任何改变)。 之所以 interface 可以提供弹性,是因为任意一个 type 可以实现多个 interface ([代码](https://play.golang.org/p/cN6KrJab-l)): @@ -95,7 +95,7 @@ func main() { --- -在 go 中,我们有两个与 interface 相关的概念: +在 Go 中,我们有两个与 interface 相关的概念: 1. 接口-通过[关键字](https://golang.org/ref/spec#Keywords) `interface`,实现此类接口所需要的一组方法; 2. 接口类型-接口类型的变量,可以保存一些实现于特定接口的值。 @@ -296,9 +296,9 @@ i is not nil * 动态类型 * 动态值 -动态类型在之前(“静态类型VS动态类型”部分)已经讨论过了。动态值是指定的实际值。 +动态类型在之前(“静态类型 VS 动态类型”部分)已经讨论过了。动态值是指定的实际值。 -在赋值 `var i I = t` 后的讨论段中,i 的动态值是 nil,但动态类型为\**T*在这个复制后,函数调用 `fmt.Printf("%T\n", i)`将会打印 `*main.T`。`当且仅当动态值与动态类型都为 nil 时,接口类型值为 nil。`结果就是即使接口类型值包含一个 nil 指针,这样的接口值也不是 nil。已知的错误就是返回未初始化,从函数返回接口类型为非接口类型值([源代码](https://play.golang.org/p/4-M35Nc2JZ)): +在赋值 `var i I = t` 后的讨论段中,i 的动态值是 nil,但动态类型为\**T*在这个复制后,函数调用 `fmt.Printf("%T\n", i)` 将会打印 `*main.T`。`当且仅当动态值与动态类型都为 nil 时,接口类型值为 nil。` 结果就是即使接口类型值包含一个 nil 指针,这样的接口值也不是 nil。已知的错误就是返回未初始化,从函数返回接口类型为非接口类型值([源代码](https://play.golang.org/p/4-M35Nc2JZ)): ```go type I interface {} @@ -348,7 +348,7 @@ func main() { ## 满足一个接口 -每个实现了接口所有方法的类型都自动满足这个接口。我们不需要在这些类型中使用任何其他关键字(如 Java中的 implements)来表示该类型实现了接口。它是由 go 语言的编译器自动实现的,而这儿正是该语言的强大之处([源代码](https://play.golang.org/p/U4r6i2X5xb)): +每个实现了接口所有方法的类型都自动满足这个接口。我们不需要在这些类型中使用任何其他关键字(如 Java 中的 implements)来表示该类型实现了接口。它是由 Go 语言的编译器自动实现的,而这儿正是该语言的强大之处([源代码](https://play.golang.org/p/U4r6i2X5xb)): ```go import ( @@ -391,7 +391,7 @@ func main() { via: https://medium.com/golangspec/interfaces-in-go-part-i-4ae53a97479c -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[cureking](https://github.com/cureking) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20170221-Interfaces-in-Go-part-2.md b/published/tech/20170221-Interfaces-in-Go-part-2.md index 2b558af3d..8638de8a7 100644 --- a/published/tech/20170221-Interfaces-in-Go-part-2.md +++ b/published/tech/20170221-Interfaces-in-Go-part-2.md @@ -634,7 +634,7 @@ case T2: via: https://medium.com/golangspec/interfaces-in-go-part-ii-d5057ffdb0a6 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[csshawn](https://github.com/csshawn) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20170228-Interfaces-in-Go-part-III.md b/published/tech/20170228-Interfaces-in-Go-part-III.md index e505a2be6..f51097fb5 100644 --- a/published/tech/20170228-Interfaces-in-Go-part-III.md +++ b/published/tech/20170228-Interfaces-in-Go-part-III.md @@ -221,7 +221,7 @@ Write(p []byte) (n int, err error) *[Education](https://medium.com/tag/education?source=post)* *[Polymorphism](https://medium.com/tag/polymorphism?source=post)* -**喜欢读吗?给 Michał Łowicki 一些掌声吧。** +**喜欢读吗?给 Micha ł Ł owicki 一些掌声吧。** 简单鼓励下还是大喝采,根据你对这篇文章的喜欢程度鼓掌吧。 @@ -229,7 +229,7 @@ Write(p []byte) (n int, err error) via: https://medium.com/golangspec/interfaces-in-go-part-iii-61f5e7c52fb5 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[xmge](https://github.com/xmge) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20170522-Debugging-Go-core-dumps.md b/published/tech/20170522-Debugging-Go-core-dumps.md index 4c14c4070..a35f50623 100644 --- a/published/tech/20170522-Debugging-Go-core-dumps.md +++ b/published/tech/20170522-Debugging-Go-core-dumps.md @@ -8,7 +8,7 @@ 在本文中,我会用一个非常简单的 hello world 网页应用服务举例,实际情况,我们的程序会更加复杂。对核心转储文件的分析意义在于可以帮助我们查看程序当时的运行情况,并可能让我们有机会重现当时的程序问题。 -**注意**: 接下来的操作都是在Linux系统终端中执行,我不确定其它类Unix系统是否可以工作正常,macOS 和 Windows 应该都不支持。 +**注意**: 接下来的操作都是在 Linux 系统终端中执行,我不确定其它类 Unix 系统是否可以工作正常,macOS 和 Windows 应该都不支持。 在开始之前,你需要确定已经打开了操作系统对核心转储文件的支持。 `ulimit` 的默认值为 0 意思是说核心转储文件最大容量只能是零。我通常在开发机上设置为 `unlimited` 命令如下: @@ -16,7 +16,7 @@ 然后,确定你的机器上已经安装了 [delve](https://github.com/derekparker/delve) 。 -这是一个 `main.go` 文件,包含一个HTTP启动服务和一个处理函数。 +这是一个 `main.go` 文件,包含一个 HTTP 启动服务和一个处理函数。 ``` go $ cat main.go @@ -37,7 +37,7 @@ func main() { ``` 我们把它编译成二进制文件。 - $ go build . + $ Go build . 我们假设下,将来这个服务可能会出现问题,但是你不知道会出现什么样的问题。你可能已经用了很多方法测试程序但仍然找不到程序异常退出的原因。 diff --git a/published/tech/20170912 Go Project Layout.md b/published/tech/20170912 Go Project Layout.md new file mode 100644 index 000000000..204b285e1 --- /dev/null +++ b/published/tech/20170912 Go Project Layout.md @@ -0,0 +1,48 @@ +首发于:https://studygolang.com/articles/24686 + +# Go 项目的布局 +Kyle C. Quest +2017 年 9 月 12 日 · 5 min 阅读 + +读过了 [`Tour of Go`](https:/tour.studygolang.com),在 [https://play.studygolang.com/](https://play.studygolang.com/) 上把玩过,然后你感觉你准备好写一些代码了。很棒!但是,你不确定该如何组织你的项目。可以将代码放在你想放的任意地方吗?有没有组织代码的标准方式?如果想有多个应用程序的二进制文件呢?“go getable” 是指什么?你可能会问自己这些问题。 + +首先,你必须了解 Go 的工作空间。 [`How to Write Go Code`](https://golang.org/doc/code.html) 是个很好的起点。缺省地,Go 将所有代码保管在同一个工作空间,并期望所有代码都在同一个工作空间。这个地方由环境变量 `GOPATH` 来标识。对你来说这意味着什么?意味着你必须**将代码放在默认的工作空间**或者必须修改 `GOPATH` 环境变量,指向你自己的代码位置。不管哪种方式,项目的真正源代码都需要放在 `src` 子目录下(即 **`$GOPATH/src/your_project`** 或 `$GOPATH/src/github.com/your_github_username/your_project`)。技术上讲,如果你无需导入外部包且使用相对路径导入自己的代码,你的工程不一定非要放在工作空间里,但不推荐这样做。不过玩具项目或概念验证(Poc)项目这么做是可以的。Go 1.1 确实引入了模块的概念,允许你将项目代码放在 `GOPATH` 之外,且不受上述的导入限制,但直到现在这还是一个实验性的功能。 + +你已经将你的项目目录放在正确的地方。接下来呢? + +对于你是唯一开发者的概念验证(Poc)项目或特别小的项目,将项目代码都写在根目录下的 `main.go` 里就够了。如果知道你的项目将会变得足够大或者它会上生产环境,而且其他人会贡献代码,那你就应该考虑至少采用这里罗列的项目布局样式中的一些。 + +有一些项目布局样式在 Go 生态系统中脱颖而出。`cmd` 和 `pkg` 目录是最常见的两个样式。你应当采用这些样式,除非你的项目特别小。 + +**`cmd`** 布局样式在你需要有多个应用程序二进制文件时十分有用。每个二进制文件拥有一个子目录(即 **`your_project/cmd/your_app`**)。这个样式帮助保持你的项目下的包(project/package) ‘go gettable’。什么意思?这意味着你可以使用 `go get` 命令拉取(并安装)你的项目,项目的应用程序以及库(比如,`go get github.com/your_github_username/your_project/cmd/appxg`)。你不必非要拆分应用程序文件,通过设置正确的 `go build` 标记你可以构建每个应用程序,但是由于不知道该构建哪个应用程序, `go get` 就无法正常工作了。官方的 [Go tools](https://github.com/golang/tools/tree/master/cmd) 是 `cmd` 布局样式的一个例子。很多知名的项目也使用了同样的样式:[Kubernetes](https://github.com/kubernetes/kubernetes/tree/master/cmd), [Docker](https://github.com/moby/moby/tree/master/cmd), [Prometheus](https://github.com/prometheus/prometheus/tree/master/cmd), [Influxdb](https://github.com/influxdata/influxdb/tree/master/cmd)。 + +**`pkg`** 布局样式也十分受欢迎。对新手 Go 开发者来讲这是最容易混淆的一个包结构概念,因为 Go 的工作空间就有一个同名的目录但那个目录有不同的用途(用来存储 Go 编译器构建的包的 object 文件)。`pkg` 目录是放置公共库的地方。它们可以被你的应用内部使用。也可供外部项目使用。这是你和你代码的外部使用者之间的非正式协定。其它项目会导入这些库并期望它们正常工作,所以在把东西放到这里前请三思。很多知名的项目使用了这个样式:[Kubernetes](https://github.com/kubernetes/kubernetes/tree/master/pkg), [Docker](https://github.com/moby/moby/tree/master/pkg), [Grafana](https://github.com/grafana/grafana/tree/master/pkg), [Influxdb](https://github.com/influxdata/influxdb/tree/master/pkg), [Etcd](https://github.com/coreos/etcd/tree/master/pkg). + +`pkg` 目录下的某些库并不总是为了公共使用。为什么呢?因为很多现有的 Go 项目诞生在能隐藏内部包之前。一些项目将内部库放在 `pkg` 目录下,以便保持与其它部分代码结构的一致。另外一些项目将内部库放置在 `pkg` 目录之外另外的目录里。[Go 1.4](https://golang.org/doc/go1.4) 引入了使用 `internal` 隐藏内部库的能力。什么意思呢?如果你将代码放在 ‘internal’目录,外部项目则无法导入那些代码。即使是项目内部的其它代码,如果不在 `internal` 目录的父目录里,也无法访问这些内部代码。这个功能使用还不广泛因为它相对较新;但是作为一个额外(在 Go 用大小写区分函数可见性的规则之外)的控制层它有极大价值。很多知名的项目使用了这个样式:[Dep](https://github.com/golang/dep/tree/master/internal), [Docker](https://github.com/moby/moby/tree/master/internal), [Nsq](https://github.com/nsqio/nsq/tree/master/internal), [Go Ethereal](https://github.com/ethereum/go-ethereum/tree/master/internal), [Contour](https://github.com/heptio/contour/tree/master/internal)。 + +**`internal`** 目录是放置私有包的地方。你可以选择性地添加额外的结构来分离内部共享的库(比如,**`your_project/internal/pkg/your_private_lib`**)以及不希望别人导入的应用程序代码(比如, **`your_project/internal/app/your_app`**)。当你将全部私有代码都放在 ‘internal’ 目录,`cmd` 目录下的应用程序就可以被约束成一些小文件,其只需定义对应于应用程序二进制文件的 ‘main’ 函数。其余代码都从 `internal` 或 `pkg` 目录导入(Heptio 中的 [ark](https://github.com/heptio/ark/blob/master/cmd/ark/main.go),以及 Grafana 中的 [loki](https://github.com/grafana/loki/blob/master/cmd/loki/main.go),是这个 ` 微型 main 函数 ` 包样式的好例子)。 + +如果你 fork 并修改了外部项目的一块该如何?有些项目将这些代码放在 `pkg` 目录下,但更好的做法是将它放在顶层目录下的 **`third_party`** 目录,以便将你自己的代码和你从别人那里借用的代码区分开来。 + +你在项目里导入的外部包呢?它们去哪里?你有几个选项。你可以将它们放在项目以外。使用 `go get` 安装的包将保存在你的 Go 工作空间。大部分情况下可以正常工作,但视具体包而定,它可能会变得脆弱和不可预测,因为别人在构建你的项目时他们可能会拿到这个包的一个不向后兼容的版本。解决办法是 ‘vendoring’。使用 ‘vendoring’ 你通过将依赖与项目一起提交来将它们固定。 [Go 1.6](https://golang.org/doc/go1.6) 导入了一种标准的方式来 ‘vendor’ 外部包(在 Go 1.5 中是实验性功能)。将外部包放在 `vendor` 目录。这与 `third_party` 目录有何区别呢?如果你导入了外部代码且原样使用它就放在 `vendor` 目录。如果你使用的是修改版的外部包就放在 `third_party` 目录。 + +如果你想学习更多关于其他 Go 项目使用的项目结构请阅读 [‘Analysis of the Top 1000 Go Repositories’](http://blog.sgmansfield.com/2016/01/an-analysis-of-the-top-1000-go-repositories/)。它有点陈旧,不过依然有用。 + +一个真正的项目也会有另外的目录。你可以使用这个布局模版作为你的 Go 项目的起点:**[https://github.com/golang-standards/project-layout](https://github.com/golang-standards/project-layout)**。它涵盖了这篇博客里描述的 Go 项目布局样式并包括很多你需要的支持目录。 + +现在是时候写些代码了!如果你还没安装 Go 请查看这个 [quick setup guide for Mac OS X](https://medium.com/golang-learn/quick-go-setup-guide-on-mac-os-x-956b327222b8) (其他平台的安装也是类似的)。如果还没浏览过请你浏览 [‘Tour of Go’](https://tour.golang.org/) ,然后读一下 [’50 Shades of Go’](http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/) 去了解 Go 中最常见的坑,这会在你开始写代码和调试代码时节省很多时间。 + +* [Golang](https://medium.com/tag/golang) +* [Go](https://medium.com/tag/go) +* [Standards](https://medium.com/tag/standards) +* [Project Structure](https://medium.com/tag/project-structure) + +--- + +via: https://medium.com/golang-learn/go-project-layout-e5213cdcfaa2 + +作者:[Kyle C. Quest](https://medium.com/@CloudImmunity) +译者:[krystollia](https://github.com/krystollia) +校对:[DingdingZhou](https://github.com/DingdingZhou) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20171010-learn-go-constants-a-visual-guide.md b/published/tech/20171010-learn-go-constants-a-visual-guide.md index 2cab52dcb..881e8a808 100644 --- a/published/tech/20171010-learn-go-constants-a-visual-guide.md +++ b/published/tech/20171010-learn-go-constants-a-visual-guide.md @@ -22,7 +22,7 @@ ![typed_constants.image](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-const-guide/1*4zXKp5xjt-a9ivu9b0vNMw.png) -类型→Boolean,rune,numerics,或者 string +类型→ Boolean,rune,numerics,或者 string 值→编译期时在声明中分配值 @@ -77,7 +77,6 @@ ## 高精度计算 -If you stay in the untyped constants realm, there is no-speed-limit! But, when you use them in a variable, the speed-limit applies. 如果常量只停留在非类型化常量领域,那么它没有速度的限制!但是,当将常量赋值给变量进行使用时,速度就有限制了。 ![image](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-const-guide/1*YhDCUL1FGF-BbU-yTkxAAA.png) diff --git a/published/tech/20171012-HTTP-File-Upload-and-Download-with-Go.md b/published/tech/20171012-HTTP-File-Upload-and-Download-with-Go.md index fb450b50b..f4b350d9a 100644 --- a/published/tech/20171012-HTTP-File-Upload-and-Download-with-Go.md +++ b/published/tech/20171012-HTTP-File-Upload-and-Download-with-Go.md @@ -2,15 +2,15 @@ # 使用 Go 语言完成 HTTP 文件上传与下载 -最近我使用 Go 语言完成了一个正式的 web 应用,有一些方面的问题在使用 Go 开发 web 应用过程中比较重要。过去,我将 web 开发作为一项职业并且把使用不同的语言和范式开发 web 应用作为一项爱好,因此对于 web 开发领域有一些心得体会。 +最近我使用 Go 语言完成了一个正式的 Web 应用,有一些方面的问题在使用 Go 开发 Web 应用过程中比较重要。过去,我将 Web 开发作为一项职业并且把使用不同的语言和范式开发 Web 应用作为一项爱好,因此对于 Web 开发领域有一些心得体会。 -总的来说,我喜欢使用 Go 语言进行 web 开发,尽管开始一段时间需要去适应它。Go 语言有一些坑,但是正如本篇文章中所要讨论的文件上传与下载,Go 语言的标准库与内置函数,使得开发是种愉快的体验。 +总的来说,我喜欢使用 Go 语言进行 Web 开发,尽管开始一段时间需要去适应它。Go 语言有一些坑,但是正如本篇文章中所要讨论的文件上传与下载,Go 语言的标准库与内置函数,使得开发是种愉快的体验。 在接下来的几篇文章中,我将重点讨论我在 Go 中编写生产级 Web 应用程序时遇到的一些问题,特别是关于身份验证/授权的问题。 -这篇文章将展示HTTP文件上传和下载的基本示例。我们将一个有 `type` 文本框和一个 `uploadFile` 上传框的 HTML 表单作为客户端。 +这篇文章将展示 HTTP 文件上传和下载的基本示例。我们将一个有 `type` 文本框和一个 `uploadFile` 上传框的 HTML 表单作为客户端。 -让我们来看下 Go 语言中是如何解决这种在 web 开发中随处可见的问题的。 +让我们来看下 Go 语言中是如何解决这种在 Web 开发中随处可见的问题的。 ## 代码示例 @@ -91,7 +91,7 @@ func uploadFileHandler() http.HandlerFunc { } ``` -在实际应用程序中,我们可能会使用文件元数据做一些事情,例如将其保存到数据库或将其推送到外部服务——以任何方式,我们将解析和操作元数据。这里我们创建一个随机的新名字(这在实践中可能是一个UUID)并将新文件名记录下来。 +在实际应用程序中,我们可能会使用文件元数据做一些事情,例如将其保存到数据库或将其推送到外部服务——以任何方式,我们将解析和操作元数据。这里我们创建一个随机的新名字(这在实践中可能是一个 UUID)并将新文件名记录下来。 ```go fileName := randToken(12) @@ -128,9 +128,9 @@ func uploadFileHandler() http.HandlerFunc { ## 结论 -这是又一个证明了 Go 如何允许用户为 web 编写简单而强大的软件,而不必像处理其他语言和生态系统中固有的无数抽象层。 +这是又一个证明了 Go 如何允许用户为 Web 编写简单而强大的软件,而不必像处理其他语言和生态系统中固有的无数抽象层。 -在接下来的篇幅中,我将展示一些在我第一次使用 Go 语言编写正式的 web 应用中其他细节,敬请期待。;) +在接下来的篇幅中,我将展示一些在我第一次使用 Go 语言编写正式的 Web 应用中其他细节,敬请期待。;) // 根据 reddit 用户 `lstokeworth` 的反馈对部分代码进行了修改。谢谢:) diff --git a/published/tech/20171029-http-s-proxy-in-golang-in-less-than-100-lines-of-code.md b/published/tech/20171029-http-s-proxy-in-golang-in-less-than-100-lines-of-code.md index 7e1773063..1b285c3bd 100644 --- a/published/tech/20171029-http-s-proxy-in-golang-in-less-than-100-lines-of-code.md +++ b/published/tech/20171029-http-s-proxy-in-golang-in-less-than-100-lines-of-code.md @@ -77,8 +77,8 @@ func handleTunneling(w http.ResponseWriter, r *http.Request) { if err != nil { http.Error(w, err.Error(), http.StatusServiceUnavailable) } - go transfer(dest_conn, client_conn) - go transfer(client_conn, dest_conn) + Go transfer(dest_conn, client_conn) + Go transfer(client_conn, dest_conn) } func transfer(destination io.WriteCloser, source io.ReadCloser) { defer destination.Close() @@ -182,7 +182,7 @@ go transfer(dest_conn, client_conn) go transfer(client_conn, dest_conn) ``` -两个 goroutine 中数据朝两个方向复制:从客户端到目的服务器及其反方向。 +两个 Goroutine 中数据朝两个方向复制:从客户端到目的服务器及其反方向。 ## 测试 @@ -208,7 +208,7 @@ go transfer(client_conn, dest_conn) via: https://medium.com/@mlowicki/http-s-proxy-in-golang-in-less-than-100-lines-of-code-6a51c2f2c38c -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[dongkui0712](https://github.com/dongkui0712) 校对:[rxcai](https://github.com/rxcai) diff --git a/published/tech/201711-Golang-Pros-and-Cons-for-DevOps/20170530-Golang-Pros-and-Cons-for-DevOps-1-of-6.md b/published/tech/201711-Golang-Pros-and-Cons-for-DevOps/20170530-Golang-Pros-and-Cons-for-DevOps-1-of-6.md index c6e3716c7..c1a1623f9 100644 --- a/published/tech/201711-Golang-Pros-and-Cons-for-DevOps/20170530-Golang-Pros-and-Cons-for-DevOps-1-of-6.md +++ b/published/tech/201711-Golang-Pros-and-Cons-for-DevOps/20170530-Golang-Pros-and-Cons-for-DevOps-1-of-6.md @@ -21,7 +21,7 @@ 如果这是你首次阅读有关 Go 的文章,并且你已经知道怎样用类 C 的语言进行编程,你应该去参考[Go 语言之旅](https://tour.golang.org/welcome/1),它将大约花费你一个小时,而且介绍相当有深度。接下来的内容并不是介绍学习如何用 Go 进行编程,而是我们在用 Go 语言开发智能代理系统过程中有过的抱怨和发现的可取之处。 -准备好听听用 Go 进行编程的真正样子吗?我要开始说了(原谅我,这是一个不高明的双关)(译注:原文是 `Here it goes`,goes 对应 go 语言)。 +准备好听听用 Go 进行编程的真正样子吗?我要开始说了(原谅我,这是一个不高明的双关)(译注:原文是 `Here it goes`,goes 对应 Go 语言)。 ## Go 语言的好处 1: Goroutines — 轻量,内核级线程 @@ -31,7 +31,7 @@ Goroutine 等同于 Go。Goroutine 是执行线程,并且它是轻量的、内 ### 怎样执行一个 Goroutine -启动一个 goroutine 线程超级简单。首先,你将在我们的看门狗(Watchdog)产品中看到一些伪代码: +启动一个 Goroutine 线程超级简单。首先,你将在我们的看门狗(Watchdog)产品中看到一些伪代码: ```go import "time" @@ -54,7 +54,7 @@ func main() { ``` 在上面的代码中,CPU、硬盘、网络、进程和身份每 5 秒检查一次。如果上述任何一个挂掉的话,所有的监控就会终止。每一个监控用时越长,检查频率就越低,因为我们在计算完成之后需要休眠 5 秒。 -为了解决这些问题,一种解决方案(一种不完整的方案,但是完美展示 goroutine 的价值所在)是使用 goroutine 来调用每一个监控函数。仅仅在你想要以线程方式运行的函数调用前加上 `go` 关键字。 +为了解决这些问题,一种解决方案(一种不完整的方案,但是完美展示 Goroutine 的价值所在)是使用 Goroutine 来调用每一个监控函数。仅仅在你想要以线程方式运行的函数调用前加上 `go` 关键字。 ```go func main() { @@ -70,13 +70,13 @@ func main() { ``` 现在如果它们之中任何一个挂掉的话,仅仅被阻塞的调用会被终止,不会阻塞其它的监控调用函数。并且因为产生一个线程很容易也很快,现在我们事实上更靠近了每隔 5 秒钟检查这些系统。 -当然,上述这个解决方案也有其他问题,比如在一个 goroutine 的 panic 可能会破坏其他 goroutine,睡眠时间有少量的偏差,代码并不像看到的那样模块化等等。但是生成内核级线程不是很容易吗? +当然,上述这个解决方案也有其他问题,比如在一个 Goroutine 的 panic 可能会破坏其他 goroutine,睡眠时间有少量的偏差,代码并不像看到的那样模块化等等。但是生成内核级线程不是很容易吗? 正如你所看到的,Java 程序需要 12 行代码,而 Go 却只需要 2 个单词。我们想说这意味着你的 Go 源代码将变得简洁和紧凑,但是当我们在这篇文章后面介绍 panics 和 errors 时,你会发现, 不幸的是, 情况并非如此。(事实上 Go 语言是一种比较臃肿的语言,现在信不信由你)。 ### 同步包(和 Channels)加上编排 -我们使用 Go 的同步包和 channels 是为了 goroutine 的编排,发送信号和关闭。你将会发现对 sync.Mutex 和 sync.WaitGroup 的引用以及不断重复的名为 shutdownChannel 的结构体变量充斥在我们的代码中。 +我们使用 Go 的同步包和 channels 是为了 Goroutine 的编排,发送信号和关闭。你将会发现对 sync.Mutex 和 sync.WaitGroup 的引用以及不断重复的名为 shutdownChannel 的结构体变量充斥在我们的代码中。 在 [Go manual](https://golang.org/pkg/sync/) 中关于 sync.Mutex 和 sync.WaitGroup 有一个重要的提示: @@ -95,7 +95,7 @@ func main() { m := &sync.Mutex{} } ``` -然而对于这些结构体的警告恰恰在页面的顶部,很容易忽视掉,这将会导致你将要构建的应用出现奇怪的副作用。 +然而对于这些结构体的警告恰恰在页面的顶部,很容易  忽视掉,这将会导致你将要构建的应用出现奇怪的副作用。 从前一节关于监控的那个例子来看,这个例子是我们如何使用一个 WaitGroup 来确保在任何时候每个系统中不会有多于一个的监控: @@ -153,9 +153,9 @@ goroutine 不仅容易启动,而且总的来说它们也容易协调、关闭 ### Goroutines 的自动清理 -goroutine 持有堆变量和栈变量的引用(避免垃圾收集),但是并不需要一直持有这些引用。它们 (goroutines) 一直运行直到函数完成,然后关闭并且自动释放所有资源。在这过程中,一个需要注意的点是:如果主线程已经退出,那么启动的 goroutine 会被忽略。 +goroutine 持有堆变量和栈变量的引用(避免垃圾收集),但是并不需要一直持有这些引用。它们 (goroutines) 一直运行直到函数完成,然后关闭并且自动释放所有资源。在这过程中,一个需要注意的点是:如果主线程已经退出,那么启动的 Goroutine 会被忽略。 -首先,这里举一个有关启动 goroutine 并被忽略的真实例子。在我们的应用中,我们在子进程中启动一个模块,并且使用 IPC 来配置更新,设置更新和心跳检测。父进程和每个模块进程(子进程)必须不断地从 IPC channel 中读取数据,并且再向别的地方发送数据。这是一种我们启动并且忽略的线程,因为在关机的时候,我们不关心是否已经接收了全部的信息流。请持保留态度的看这段代码,虽然它来自我们的代码,但是为了简单起见,移除了一些重量级的代码: +首先,这里举一个有关启动 Goroutine 并被忽略的真实例子。在我们的应用中,我们在子进程中启动一个模块,并且使用 IPC 来配置更新,设置更新和心跳检测。父进程和每个模块进程(子进程)必须不断地从 IPC channel 中读取数据,并且再向别的地方发送数据。这是一种我们启动并且忽略的线程,因为在关机的时候,我们不关心是否已经接收了全部的信息流。请持保留态度的看这段代码,虽然它来自我们的代码,但是为了简单起见,移除了一些重量级的代码: ```go package ipc @@ -234,7 +234,7 @@ make(chan int, numSlots) 你可以通过这个 channel 传送任何信息。你可以让它们同步工作,异步工作,或者让多个读端来监听这些 channels,并做具体的一些工作。 -不像 queue,一个 channel 可以被用来广播一个消息。在我们的代码中,最经常用来广播的消息是关闭。当到了关闭时刻,我们向所有后台 goroutines ->发送广播:到清理空间的时间了。使用一个 channel 向多个监听者发送单一消息仅仅只有一种方式:那就是你必须关闭这个 channel。下面是我们代码的简化版本: +不像 queue,一个 channel 可以被用来广播一个消息。在我们的代码中,最经常用来广播的消息是关闭。当到了关闭时刻,我们向所有后台 goroutines -> 发送广播:到清理空间的时间了。使用一个 channel 向多个监听者发送单一消息仅仅只有一种方式:那就是你必须关闭这个 channel。下面是我们代码的简化版本: ```go package main @@ -290,7 +290,7 @@ panic 和 error,它们是 Go 语言中最糟糕的东西,而且会是一个 根据 [Go 官方博客的一篇文章](https://blog.golang.org/defer-panic-and-recover), -> Panic 是一个内建函数,用于终止普通的控制流,并使程序崩溃。当函数 F 引发 panic 时,F 的执行终止,通常函数 F 中的任一 deferred 函数会被执行,并且 F 会返回给它的调用者。对于调用者来说,F 表现得像是一个调用 panic 的函数。这个进程继续收回栈帧(栈是向下增长,函数返回时退栈)直到当前 goroutine 中所有的函数返回,这时程序就崩溃了。Panics 可以通过直接调用来引发。它们也可以由运行时错误引发,如数组访问越界。 +> Panic 是一个内建函数,用于终止普通的控制流,并使程序崩溃。当函数 F 引发 panic 时,F 的执行终止,通常函数 F 中的任一 deferred 函数会被执行,并且 F 会返回给它的调用者。对于调用者来说,F 表现得像是一个调用 panic 的函数。这个进程继续收回栈帧(栈是向下增长,函数返回时退栈)直到当前 Goroutine 中所有的函数返回,这时程序就崩溃了。Panics 可以通过直接调用来引发。它们也可以由运行时错误引发,如数组访问越界。 换句话说,当你遇到一个控制流问题时,panics 会终止你的程序。 @@ -426,7 +426,7 @@ if err := this.cleanupModule(moduleSettings); err != nil { 除了 error,你还有 panic(需要应对)。如果你有引发 panic 的事物,它和在哪个函数之内发生 (panic) 没有关系.如果你有一个 panic,它将和 try/catch 做同样的事情,除了你现在需要重复代码外,你还是需要处理同样的 try/catch! -我们的代理软件中的解决方案是使用一个带有重试功能的 wrapper 函数和为 panic 和 error 而做的良好记录。然后我们在主线程和遍及代码产生的每一个 goroutine 中严格的调用它。这些代码不能在其他的任何地方运行,因为他缺失其它类库,而且它是阉割版的代码,但是它应该给你关于如何共同管理 panic 和 error 的一些启发。 +我们的代理软件中的解决方案是使用一个带有重试功能的 wrapper 函数和为 panic 和 error 而做的良好记录。然后我们在主线程和遍及代码产生的每一个 Goroutine 中严格的调用它。这些代码不能在其他的任何地方运行,因为他缺失其它类库,而且它是阉割版的代码,但是它应该给你关于如何共同管理 panic 和 error 的一些启发。 ```go package safefunc @@ -502,7 +502,7 @@ retryLoop: } } ``` -当因为 Go 有这些错误处理让你觉得代码很安全时,一个 panic 错误在运行时发生了。它不做任何检查,把各 goroutine 的所有的错误都放到堆栈里,直到最终使程序崩溃。 +当因为 Go 有这些错误处理让你觉得代码很安全时,一个 panic 错误在运行时发生了。它不做任何检查,把各 Goroutine 的所有的错误都放到堆栈里,直到最终使程序崩溃。 ### 并非每人都讨厌 Go 中的 error 和 panic diff --git a/published/tech/201711-Golang-Pros-and-Cons-for-DevOps/20170612-Golang-Pros-and-Cons-for-DevOps-2-of-6.md b/published/tech/201711-Golang-Pros-and-Cons-for-DevOps/20170612-Golang-Pros-and-Cons-for-DevOps-2-of-6.md index 48e18ebc5..af4510d72 100644 --- a/published/tech/201711-Golang-Pros-and-Cons-for-DevOps/20170612-Golang-Pros-and-Cons-for-DevOps-2-of-6.md +++ b/published/tech/201711-Golang-Pros-and-Cons-for-DevOps/20170612-Golang-Pros-and-Cons-for-DevOps-2-of-6.md @@ -62,7 +62,7 @@ func main() { 你可以对比一下你最喜欢的语言的数据库,以 Java 为例,JDBC 在 java.sql 包中有 ResultSet、Connection、Driver、Statement 以及另外十八个接口,所有这些接口都必须实现。 -我们再来看看 MySQL、SQLServer、Oracle 和 Postgres库。我没有编写或检查它们是否使用 java.sql 中定义的任何具体类或枚举。不过,这些供应商库不需要 JDBC 包来使用上面提到的接口。 +我们再来看看 MySQL、SQLServer、Oracle 和 Postgres 库。我没有编写或检查它们是否使用 java.sql 中定义的任何具体类或枚举。不过,这些供应商库不需要 JDBC 包来使用上面提到的接口。 我们在 Blue Matador 建立的监控代理有很多包和模块。每个模块可以主动或被动启用。现在,它被分为 Lumberjack,我们的集中式日志管理产品,以及 Watchdog,我们的系统的监控工具。 @@ -102,4 +102,4 @@ via: https://blog.bluematador.com/posts/golang-pros-cons-for-devops-part-2/ 译者:[Mr.NoFat](https://github.com/UnFat) 校对:[polaris1119](https://github.com/polaris1119) -本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go中文网](https://studygolang.com/) 荣誉推出 +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/201711-Golang-Pros-and-Cons-for-DevOps/20170710-Golang-Pros-and-Cons-for-DevOps-Part-3-of-6.md b/published/tech/201711-Golang-Pros-and-Cons-for-DevOps/20170710-Golang-Pros-and-Cons-for-DevOps-Part-3-of-6.md index 6323d90e3..70b2e8156 100644 --- a/published/tech/201711-Golang-Pros-and-Cons-for-DevOps/20170710-Golang-Pros-and-Cons-for-DevOps-Part-3-of-6.md +++ b/published/tech/201711-Golang-Pros-and-Cons-for-DevOps/20170710-Golang-Pros-and-Cons-for-DevOps-Part-3-of-6.md @@ -29,13 +29,13 @@ ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go_devops/golang-language-runtimes-2.png) -图中显而易见的是,C++ 是最快的语言 - 完全不出意料。实际上,即使 Java 都把 Golang 击败了,但这出于两个很好的理由:(1)Java 虚拟机(JVM)从1995年就开始开发了,比 Golang 多了 17 年。以及(2) 比起Golang ,JVM 在测试中花了 2 到 30 倍的内存使用量 - 这意味着总体上 Golang 的垃圾回收员比JVM的工作得更勤劳。 +图中显而易见的是,C++ 是最快的语言 - 完全不出意料。实际上,即使 Java 都把 Golang 击败了,但这出于两个很好的理由:(1)Java 虚拟机(JVM)从 1995 年就开始开发了,比 Golang 多了 17 年。以及(2) 比起 Golang ,JVM 在测试中花了 2 到 30 倍的内存使用量 - 这意味着总体上 Golang 的垃圾回收员比 JVM 的工作得更勤劳。 ### 编译速度 我们之前的 agent 是用 Python 写的(并不是一个编译语言),但这并不意味着我们不熟悉 Go 的编译时优势。 -从 Golang 的一开始,较短的编译时间一直是一个严苛的要求。 Go 是 Google 的 Ken Thompson 和 Rob Pike 创造的。Google,有超过20亿行代码,毫无疑问对于编译所会浪费的时间极其严肃。 +从 Golang 的一开始,较短的编译时间一直是一个严苛的要求。 Go 是 Google 的 Ken Thompson 和 Rob Pike 创造的。Google,有超过 20 亿行代码,毫无疑问对于编译所会浪费的时间极其严肃。 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go_devops/compiling.png) @@ -62,12 +62,12 @@ Blue Matador Agent 有 29 个包,116824 行代码。它还有 3 个目标操 尽管如此,以下是我认为 Go 的代码维护效率更好的原因: - 没有内存管理。在 C/C++ 有很多代码是只为了内存管理而存在。你为了管理内存不得不做了一堆古怪的事。这在 Go 里面,得益于垃圾回收机制,完全不是问题。 -- 稳定可靠的核心库。除了[错误返回系统](https://blog.bluematador.com/blog/posts/golang-pros-cons-for-devops-part-1-goroutines-panics-errors/)之外,我们的代码十分精简。这是因为Go的核心库有我们需要的所有东西; 从 HTTP 请求和 JSON 编码/解码到进程 fork 和 IPC 管道。 +- 稳定可靠的核心库。除了[错误返回系统](https://blog.bluematador.com/blog/posts/golang-pros-cons-for-devops-part-1-goroutines-panics-errors/)之外,我们的代码十分精简。这是因为 Go 的核心库有我们需要的所有东西; 从 HTTP 请求和 JSON 编码/解码到进程 fork 和 IPC 管道。 - 没有泛型。对,我即将要说缺少泛型是这个语言的一个严重缺点。但是如果没有泛型的话,变量类型全都会是明确且已知的。当你读一个类文件的时候,你会明确知道预期结果。这让更改代码更容易并且更快。 ### 关于速度的额外阅读资料 -备注:我在我写完这个帖子之后才发现这个。我推荐你阅读这篇文章,如果你对于 Go 的速度想知道更多:[5 件使得Go很快的事](https://dave.cheney.net/2014/06/07/five-things-that-make-go-fast) +备注:我在我写完这个帖子之后才发现这个。我推荐你阅读这篇文章,如果你对于 Go 的速度想知道更多:[5 件使得 Go 很快的事](https://dave.cheney.net/2014/06/07/five-things-that-make-go-fast) ## Golang 之弊:缺乏泛型 diff --git a/published/tech/201711-Golang-Pros-and-Cons-for-DevOps/20171220-Golang-Pros-and-Cons-for-DevOps-4-of-6.md b/published/tech/201711-Golang-Pros-and-Cons-for-DevOps/20171220-Golang-Pros-and-Cons-for-DevOps-4-of-6.md index 239cb5cd1..83ad756d3 100644 --- a/published/tech/201711-Golang-Pros-and-Cons-for-DevOps/20171220-Golang-Pros-and-Cons-for-DevOps-4-of-6.md +++ b/published/tech/201711-Golang-Pros-and-Cons-for-DevOps/20171220-Golang-Pros-and-Cons-for-DevOps-4-of-6.md @@ -4,7 +4,7 @@ ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go_devops/golang-pros-cons-4-time-package-method-overloading.png) -万众期待的 Golang 之于 DevOps 开发的利与弊 系列终于回归了!在这篇文章,我们讨论下 Golang 中的 time 包,以及 go 语言中为什么不使用方法重载。 +万众期待的 Golang 之于 DevOps 开发的利与弊 系列终于回归了!在这篇文章,我们讨论下 Golang 中的 time 包,以及 Go 语言中为什么不使用方法重载。 如果你没有读 [最近一篇](https://studygolang.com/articles/12614) 关于 “接口实现和公有/私有命名方式”(原文描述写错了,这一链接对应的应该是 “速度 vs. 缺少泛型”),请一定仔细阅读下,你也可以 [订阅我们的博客更新](http://eepurl.com/cOHJ3f),以后有系列文章发布的时候你就能收到通知。 @@ -21,7 +21,7 @@ ### 1. Time 包基本内容 -你可能认为每一种语言都有一个标准的,易用的处理 time 操作的内置库,其实不是这样的。NPM 有超过 [8000 多个 time 相关的包](https://www.npmjs.com/search?q=time&page=1&ranking=quality),因为 javascript 的 Date 包没法用。Java8 最终使用 java.time.Instant 和 java.time.chrono 包缓解了这个问题,但仍在编写 [教程](https://www.tutorialspoint.com/java8/java8_datetime_api.htm),研究各种用 Java 操作 time 的类和方法。相反,Golang 的 [time 包](https://golang.org/pkg/time/) 用一句话就能总结:只需引用一个包,你想要的都能实现。 +你可能认为每一种语言都有一个标准的,易用的处理 time 操作的内置库,其实不是这样的。NPM 有超过 [8000 多个 time 相关的包](https://www.npmjs.com/search?q=time&page=1&ranking=quality),因为 JavaScript 的 Date 包没法用。Java8 最终使用 java.time.Instant 和 java.time.chrono 包缓解了这个问题,但仍在编写 [教程](https://www.tutorialspoint.com/java8/java8_datetime_api.htm),研究各种用 Java 操作 time 的类和方法。相反,Golang 的 [time 包](https://golang.org/pkg/time/) 用一句话就能总结:只需引用一个包,你想要的都能实现。 获取当前时间: `time.Now()` @@ -111,4 +111,4 @@ via: https://blog.bluematador.com/golang-pros-cons-part-4-time-package-method-ov 译者:[ArisAries](https://github.com/ArisAries) 校对:[polaris1119](https://github.com/polaris1119) -本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go中文网](https://studygolang.com/) 荣誉推出 +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/201711-Golang-Pros-and-Cons-for-DevOps/20180115-Golang-Pros-and-Cons-for-DevOps-Part-5-of-6.md b/published/tech/201711-Golang-Pros-and-Cons-for-DevOps/20180115-Golang-Pros-and-Cons-for-DevOps-Part-5-of-6.md index 8b3b8602b..9d986cfd8 100644 --- a/published/tech/201711-Golang-Pros-and-Cons-for-DevOps/20180115-Golang-Pros-and-Cons-for-DevOps-Part-5-of-6.md +++ b/published/tech/201711-Golang-Pros-and-Cons-for-DevOps/20180115-Golang-Pros-and-Cons-for-DevOps-Part-5-of-6.md @@ -4,7 +4,7 @@ 在这系列的第五篇文章,我们将讨论 Go 项目的跨平台编译. -在阅读这篇文章之前,请确保你已经阅读了[上一篇](https://studygolang.com/articles/12615)关于“Time包以及重载”的文章,或者订阅我们的博客更新提醒来获取此六部曲后续文章的音讯。 +在阅读这篇文章之前,请确保你已经阅读了[上一篇](https://studygolang.com/articles/12615)关于“Time 包以及重载”的文章,或者订阅我们的博客更新提醒来获取此六部曲后续文章的音讯。 - [Golang 之于 DevOps 开发的利与弊(六部曲之一):Goroutines, Channels, Panics, 和 Errors](https://studygolang.com/articles/11983) - [Golang 之于 DevOps 开发的利与弊(六部曲之二):接口实现的自动化和公有/私有实现](https://studygolang.com/articles/12608) @@ -15,7 +15,7 @@ ## Golang 之利: 在 Linux 下编译 Windows 程序 -对于我这类主要使用 Linux 的人来说,我对于偶尔不得不去应付 Windows 下的问题感到十分的痛苦。这句话在我写我们的 [Smart Agent™](https://www.bluematador.com/smart-agent) 的时候显得格外正确,它能同时跑在 Linux 和 Windows 上,并且会为了我们的日志管理及监控软件去深入探究这两个系统的底层相关问题。 +对于我这类主要使用 Linux 的人来说,我对于偶尔不得不去应付 Windows 下的问题感到十分的痛苦。这句话在我写我们的 [Smart Agent ™](https://www.bluematador.com/smart-agent) 的时候显得格外正确,它能同时跑在 Linux 和 Windows 上,并且会为了我们的日志管理及监控软件去深入探究这两个系统的底层相关问题。 因为我们的 agent 是用 Golang 写的,在 Linux 环境下把代码编译成能在 Windows 跑的程序是十分轻松的。大部分的工作是由两个运行 `go build` 命令时的传入参数:GOARCH 和 GOOS 所完成的. @@ -24,9 +24,9 @@ 你可以运行 `go tool dist list` 去查看这两个参数所有的组合,在 Go 1.8 下一共有 38 种组合。以下的例子展示了如何在 AMD64 和 Intel i386 架构下编译适用于 Linux 和 Windows 的程序,而且你可以轻松看到如何创建一个 `Makefile` 来轻易地为各种不同的系统构建程序。 ```bash -GOOS=linux GOARCH=amd64 go build -o bin/myapp_linux_amd64 myapp -GOOS=windows GOARCH=amd64 go build -o bin/myapp_windows_amd64 myapp -GOOS=linux GOARCH=386 go build -o bin/myapp_linux_386 myapp +GOOS=linux GOARCH=amd64 Go build -o bin/myapp_linux_amd64 myapp +GOOS=windows GOARCH=amd64 Go build -o bin/myapp_windows_amd64 myapp +GOOS=linux GOARCH=386 Go build -o bin/myapp_linux_386 myapp ``` ## Cgo @@ -45,9 +45,9 @@ apt-get install -y gcc-mingw-w64 随之改变的编译指令如下: ```bash -CGO_ENABLED=1 GOOS=linux GOARCH=amd64 CC=gcc go build -o bin/myapp_linux_amd64 myapp -CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CXX=x86_64-w64-mingw32-g++ CC=x86_64-w64-mingw32-gcc go build -o bin/myapp_windows_amd64 myapp -CGO_ENABLED=1 GOOS=linux GOARCH=386 CC=gcc go build -o bin/myapp_linux_386 myapp +CGO_ENABLED=1 GOOS=linux GOARCH=amd64 CC=gcc Go build -o bin/myapp_linux_amd64 myapp +CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CXX=x86_64-w64-mingw32-g++ CC=x86_64-w64-mingw32-gcc Go build -o bin/myapp_windows_amd64 myapp +CGO_ENABLED=1 GOOS=linux GOARCH=386 CC=gcc Go build -o bin/myapp_linux_386 myapp ``` ## Golang 之弊: 关于 Windows 部分的官方文档 diff --git a/published/tech/20171102-Why-Go-is-skyrocketing-in-popularity_ZH-CN.md b/published/tech/20171102-Why-Go-is-skyrocketing-in-popularity_ZH-CN.md index a2d74e963..53fbb5f6b 100644 --- a/published/tech/20171102-Why-Go-is-skyrocketing-in-popularity_ZH-CN.md +++ b/published/tech/20171102-Why-Go-is-skyrocketing-in-popularity_ZH-CN.md @@ -18,9 +18,9 @@ 在 2017 年 9 月的 TIOBE 的 GO 语言指数,可以清楚地看到 2016 年以来受欢迎程度令人难以置信的跳跃,更做为一年中评分上升最高的编程语言,被冠名为 TIOBE 的编程语言 2016 名人堂冠军。 目前它在月度排行榜上排名第 17 位,一年前排名第 19 位,两年前排名第 65 位。 ![tiobe_index_for_go.png](https://opensource.com/sites/default/files/u128651/tiobe_index_for_go.png) -TIOBE的 GO 语言指数 [TIOBE](https://www.tiobe.com/tiobe-index/go/) +TIOBE 的 GO 语言指数 [TIOBE](https://www.tiobe.com/tiobe-index/go/) -“2017年 Stack Overflow 调查”也显示了 Go 的受欢迎程度的提升。 Stack Overflow 对 64,000 名开发人员的综合调查试图通过询问“最受欢迎,最令人生厌,最期待的语言”来获得开发者的偏好。 这个清单是由较新的语言,例如 Mozilla 的 Rust、Smalltalk +“2017 年 Stack Overflow 调查”也显示了 Go 的受欢迎程度的提升。 Stack Overflow 对 64,000 名开发人员的综合调查试图通过询问“最受欢迎,最令人生厌,最期待的语言”来获得开发者的偏好。 这个清单是由较新的语言,例如 Mozilla 的 Rust、Smalltalk Typescript、苹果的 Swift 和 Google 的 Go 等构成。 然而,连续三年以来,Rust、Swift 和 Go 都能成为排名前五的“最受喜爱”的编程语言。 ![stackoverflow_most_loved.png](https://opensource.com/sites/default/files/u128651/stackoverflow_most_loved.png) diff --git a/published/tech/20171105-Parallelizing-Golang-File-IO.md b/published/tech/20171105-Parallelizing-Golang-File-IO.md index 771c85211..417d77493 100644 --- a/published/tech/20171105-Parallelizing-Golang-File-IO.md +++ b/published/tech/20171105-Parallelizing-Golang-File-IO.md @@ -110,11 +110,11 @@ if f.IsDir() { } ``` -哎呀,不好了!现在,它只是列出一些顶级文件。这个程序生成了很多 goroutine,但是随着 main 函数的结束,程序并不会等待 goroutine 完成。我们需要让程序等待所有的 goroutine 结束。 +哎呀,不好了!现在,它只是列出一些顶级文件。这个程序生成了很多 goroutine,但是随着 main 函数的结束,程序并不会等待 Goroutine 完成。我们需要让程序等待所有的 Goroutine 结束。 ## WaitGroup -为此,我们将使用一个 `sync.WaitGroup`。基本上,它会跟踪组中的 goroutine 数目,保持阻塞状态直到没有更多的 goroutine。 +为此,我们将使用一个 `sync.WaitGroup`。基本上,它会跟踪组中的 Goroutine 数目,保持阻塞状态直到没有更多的 goroutine。 首先,创建我们的 `WaitGroup`: @@ -122,7 +122,7 @@ if f.IsDir() { var wg sync.WaitGroup ``` -然后,我们会通过给这个 WaitGroup 加一,利用 goroutine 来启动递归函数.当 `lsFiles()` 结束,我们的 `main` 函数将会在 `wg` 为空之前都保持阻塞状态。 +然后,我们会通过给这个 WaitGroup 加一,利用 Goroutine 来启动递归函数.当 `lsFiles()` 结束,我们的 `main` 函数将会在 `wg` 为空之前都保持阻塞状态。 ```go wg.Add(1) @@ -130,7 +130,7 @@ lsFiles(dir) wg.Wait() ``` -现在,为我们产生的每一个 goroutine 往 WaitGroup 加一: +现在,为我们产生的每一个 Goroutine 往 WaitGroup 加一: ```go if f.IsDir() { @@ -151,7 +151,7 @@ defer wg.Done() 现在是棘手的部分。根据你的 CPU 以及 CPU 的内核数,你可能会也可能不会遇到这个问题。如果 Go 调度器有足够的内核可用,那么它可以充分加载 goroutine([参考这里](https://stackoverflow.com/questions/8509152/max-number-of-goroutines))。但是,多数的操作系统都会限制每个进程打开文件的数目。对于 unix 系统,这个限制是内核 `ulimits`。而在我的 Mac 上,该限制是 10,240 个文件,但是因为我只有 2 个内核,所以我不会受此影响。 -在一台最近生产的有更多内核的计算机上,Go 调度器可能会同时创建超过 10,240 个 goroutine。每个 goroutine 都会打开文件,因此你会获得这样的错误: +在一台最近生产的有更多内核的计算机上,Go 调度器可能会同时创建超过 10,240 个 goroutine。每个 Goroutine 都会打开文件,因此你会获得这样的错误: `too many open files` diff --git a/published/tech/20171109-Understanding-Go-panic-output.md b/published/tech/20171109-Understanding-Go-panic-output.md index 463cc204d..94bd90e9e 100644 --- a/published/tech/20171109-Understanding-Go-panic-output.md +++ b/published/tech/20171109-Understanding-Go-panic-output.md @@ -47,7 +47,7 @@ panic 错误输出的第二行给出有关触发这个 panic 的 UNIX 信号的 `addr` 映射到 `siginfo.si_addr`,其值是 `0x30`,这并不是一个有效的内存地址。 -`pc` 是程序计数器,我们可以使用它来找出程序崩溃的地方,但是我们通常没必要这么做,因为一个 goroutine 跟踪有如下信息。 +`pc` 是程序计数器,我们可以使用它来找出程序崩溃的地方,但是我们通常没必要这么做,因为一个 Goroutine 跟踪有如下信息。 ``` goroutine 58 [running]: @@ -61,7 +61,7 @@ created by main.runServer 在这个深层次的栈帧中,第一个导致 panic 发生的(文件)会先列出。在这个例子中,是 `resp.go` 文件的 108 行。 -在这个 goroutine 回溯信息里,吸引我眼球的东西是函数 `UpdateResponse` 和 `PrefetchLoop` 的参数, 因为该数字与函数签名不匹配。 +在这个 Goroutine 回溯信息里,吸引我眼球的东西是函数 `UpdateResponse` 和 `PrefetchLoop` 的参数, 因为该数字与函数签名不匹配。 ```go func UpdateResponse(c Client, id string, version int, resp *Response, data []byte) error @@ -124,7 +124,7 @@ if resp.StatusCode != http.StatusOK { } ``` -如果 `Wrapf()` 的第一个参数传入为 `nil`, 则它的返回值为 `nil`。当这个 HTTP 状态码不是 `http.StatusOK`,这个函数将错误的返回 `nil,nil,nil`,因为一个非 200 的状态码不是一个错误,因此 `err` 的值为 `nil`。将 `errors.Wrapf()` 调用换成`errors.Errorf()` 可以修复这个 bug。 +如果 `Wrapf()` 的第一个参数传入为 `nil`, 则它的返回值为 `nil`。当这个 HTTP 状态码不是 `http.StatusOK`,这个函数将错误的返回 `nil,nil,nil`,因为一个非 200 的状态码不是一个错误,因此 `err` 的值为 `nil`。将 `errors.Wrapf()` 调用换成 `errors.Errorf()` 可以修复这个 bug。 理解并且结合上下文语境中看 panic 输出可以更容易的追踪到错误!希望这些信息日后对你有用。 diff --git a/published/tech/20171110-In-depth-introduction-to-bufio-Scanner-in-Golang.md b/published/tech/20171110-In-depth-introduction-to-bufio-Scanner-in-Golang.md index 605471c6c..61c2e992a 100644 --- a/published/tech/20171110-In-depth-introduction-to-bufio-Scanner-in-Golang.md +++ b/published/tech/20171110-In-depth-introduction-to-bufio-Scanner-in-Golang.md @@ -420,7 +420,7 @@ panic: bufio.Scan: 100 empty tokens without progressing via: https://medium.com/golangspec/in-depth-introduction-to-bufio-scanner-in-golang-55483bb689b4 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[yujiahaol68](https://github.com/yujiahaol68) 校对:[rxcai](https://github.com/rxcai),[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20171113-error-handling-in-go.md b/published/tech/20171113-error-handling-in-go.md index 876667de6..779e87519 100644 --- a/published/tech/20171113-error-handling-in-go.md +++ b/published/tech/20171113-error-handling-in-go.md @@ -59,7 +59,7 @@ fmt.Printf("%d\n", s) > `func (sf *Sonyflake) NextID() (uint64, error)` > NextID 能够连续生成 ID 从开始时间到 174 年左右。当超过这个限制的时候,NextID 会返回一个错误。 -我非常确信在看这篇文章的人不会活过174年。在这种情况下,你真的需要处理那个特定的错误么?这里真的需要返回一个错误么? +我非常确信在看这篇文章的人不会活过 174 年。在这种情况下,你真的需要处理那个特定的错误么?这里真的需要返回一个错误么? 我认为这是一个设计缺陷,我们可以使用 Go 的另一个灵活性来更好地处理:`panic`。参见一篇很棒的文章 [go by example](https://gobyexample.com/panic): @@ -217,7 +217,7 @@ var urls = []string{ "http://www.somestupidname.com/", } for _, url := range urls { - // Launch a goroutine to fetch the URL. + // Launch a Goroutine to fetch the URL. url := url // https://golang.org/doc/faq#closures_and_goroutines g.Go(func() error { // Fetch the URL. diff --git a/published/tech/20171117-Comment-Your-Code.md b/published/tech/20171117-Comment-Your-Code.md index e69899bdd..4230a24c9 100644 --- a/published/tech/20171117-Comment-Your-Code.md +++ b/published/tech/20171117-Comment-Your-Code.md @@ -1,13 +1,13 @@ 已发布:https://studygolang.com/articles/11909 # 注释你的代码 -2017年11月17日 作者:[Nate Finch](https://npf.io) +2017 年 11 月 17 日 作者:[Nate Finch](https://npf.io) 每隔一段时间,网上总会突然出现一些令人讨厌的帖子,其观点是:不应该为代码写注释,它存在的唯一原因是因为代码本身不足够好。对于这些论点,我完全不能苟同。 ## 烂代码 -他们的观点也不完全是错误的。没有人能说自己的代码足够好。代码本身也会慢慢变坏。你知道什么时候代码腐烂得最厉害吗?当你六个月没有碰这些代码的时候!当回过头再读的时候,你会非常好奇:“这个作者到底是怎么想的?”(于是,使用 git blame 来查看历史记录,没想到代码竟然是自己写的,因为这是你的代码。) +他们的观点也不完全是错误的。没有人能说自己的代码足够好。代码本身也会慢慢变坏。你知道什么时候代码腐烂得最厉害吗?当你六个月没有碰这些代码的时候!当回过头再读的时候,你会非常好奇:“这个作者到底是怎么想的?”(于是,使用 Git blame 来查看历史记录,没想到代码竟然是自己写的,因为这是你的代码。) 反对注释者的论点是:需要注释的唯一原因是你的代码不够“清晰”。如果代码重构、命名和组织地更好,那就不需要这些注释了。 diff --git a/published/tech/20171117-Golang-tutorial-series/1-introduction-and-installation.md b/published/tech/20171117-Golang-tutorial-series/1-introduction-and-installation.md index b027783a0..0ac461b2c 100644 --- a/published/tech/20171117-Golang-tutorial-series/1-introduction-and-installation.md +++ b/published/tech/20171117-Golang-tutorial-series/1-introduction-and-installation.md @@ -39,7 +39,7 @@ Golang 支持三个平台:Mac,Windows 和 Linux(译注:不只是这三 请添加 `/usr/local/go/bin` 到 `PATH` 环境变量中。Go 就已经成功安装在 `Linux` 上了。 -在本系列下一部分 *Golang系列教程第2部分: Hello World* 中,我们将会建立 Go 的工作区,编写我们第一个Go程序 :) +在本系列下一部分 *Golang 系列教程第 2 部分: Hello World* 中,我们将会建立 Go 的工作区,编写我们第一个 Go 程序 :) 请提供给我们宝贵的反馈和意见。感谢您的阅读 :) diff --git a/published/tech/20171117-Golang-tutorial-series/10-Switch-Statement.md b/published/tech/20171117-Golang-tutorial-series/10-Switch-Statement.md index 2a5cf18ff..dd1542de1 100644 --- a/published/tech/20171117-Golang-tutorial-series/10-Switch-Statement.md +++ b/published/tech/20171117-Golang-tutorial-series/10-Switch-Statement.md @@ -36,7 +36,7 @@ func main() { 在上述程序中,`switch finger` 将 `finger` 的值与每个 `case` 语句进行比较。通过从上到下对每一个值进行对比,并执行与选项值匹配的第一个逻辑。在上述样例中, `finger` 值为 4,因此打印的结果是 `Ring` 。 -在选项列表中,`case` 不允许出现重复项。如果您尝试运行下面的程序,编译器会报这样的错误: `main.go:18:2:在tmp / sandbox887814166 / main.go:16:7` +在选项列表中,`case` 不允许出现重复项。如果您尝试运行下面的程序,编译器会报这样的错误: `main.go:18:2:在 tmp / sandbox887814166 / main.go:16:7` ```go package main @@ -98,7 +98,7 @@ func main() { 在上述程序中 `finger` 的值是 8,它不符合其中任何情况,因此会打印 `incorrect finger number`。default 不一定只能出现在 switch 语句的最后,它可以放在 switch 语句的任何地方。 -您可能也注意到我们稍微改变了 `finger` 变量的声明方式。`finger` 声明在了 switch 语句内。在表达式求值之前,switch 可以选择先执行一个语句。在这行 `switch finger:= 8; finger` 中, 先声明了`finger` 变量,随即在表达式中使用了它。在这里,`finger` 变量的作用域仅限于这个 switch 内。 +您可能也注意到我们稍微改变了 `finger` 变量的声明方式。`finger` 声明在了 switch 语句内。在表达式求值之前,switch 可以选择先执行一个语句。在这行 `switch finger:= 8; finger` 中, 先声明了 `finger` 变量,随即在表达式中使用了它。在这里,`finger` 变量的作用域仅限于这个 switch 内。 ## 多表达式判断 @@ -141,9 +141,9 @@ func main() { num := 75 switch { // 表达式被省略了 case num >= 0 && num <= 50: - fmt.Println("num is greater than 0 and less than 50") + fmt.Println("num is greater than 0 and Less than 50") case num >= 51 && num <= 100: - fmt.Println("num is greater than 51 and less than 100") + fmt.Println("num is greater than 51 and Less than 100") case num >= 101: fmt.Println("num is greater than 100") } @@ -153,13 +153,13 @@ func main() { [在线运行程序](https://play.golang.org/p/mMJ8EryKbN) -在上述代码中,switch 中缺少表达式,因此默认它为 true,true 值会和每一个 case 的求值结果进行匹配。`case num >= 51 && <= 100:` 为 true,所以程序输出 `num is greater than 51 and less than 100`。这种类型的 switch 语句可以替代多个 `if else` 子句。 +在上述代码中,switch 中缺少表达式,因此默认它为 true,true 值会和每一个 case 的求值结果进行匹配。`case num >= 51 && <= 100:` 为 true,所以程序输出 `num is greater than 51 and Less than 100`。这种类型的 switch 语句可以替代多个 `if else` 子句。 ## Fallthrough 语句 在 Go 中,每执行完一个 case 后,会从 switch 语句中跳出来,不再做后续 case 的判断和执行。使用 `fallthrough` 语句可以在已经执行完成的 case 之后,把控制权转移到下一个 case 的执行代码中。 -让我们写一个程序来理解 fallthrough。我们的程序将检查输入的数字是否小于 50、100 或 200。例如我们输入 75,程序将输出`75 is lesser than 100` 和 `75 is lesser than 200`。我们用 fallthrough 来实现了这个功能。 +让我们写一个程序来理解 fallthrough。我们的程序将检查输入的数字是否小于 50、100 或 200。例如我们输入 75,程序将输出 `75 is lesser than 100` 和 `75 is lesser than 200`。我们用 fallthrough 来实现了这个功能。 ```go package main diff --git a/published/tech/20171117-Golang-tutorial-series/12-variadic-functions.md b/published/tech/20171117-Golang-tutorial-series/12-variadic-functions.md index 7b3943f87..d472f9f87 100644 --- a/published/tech/20171117-Golang-tutorial-series/12-variadic-functions.md +++ b/published/tech/20171117-Golang-tutorial-series/12-variadic-functions.md @@ -131,7 +131,7 @@ func find(num int, nums ...int) find(89, []int{nums}) ``` -这里之所以会失败是因为 `nums` 是一个 `[]int`类型 而不是 `int`类型。 +这里之所以会失败是因为 `nums` 是一个 `[]int` 类型 而不是 `int` 类型。 那么有没有办法给可变参数函数传入切片参数呢?答案是肯定的。 diff --git a/published/tech/20171117-Golang-tutorial-series/14-Strings.md b/published/tech/20171117-Golang-tutorial-series/14-Strings.md index 67626d2e2..f8c71ed31 100644 --- a/published/tech/20171117-Golang-tutorial-series/14-Strings.md +++ b/published/tech/20171117-Golang-tutorial-series/14-Strings.md @@ -52,7 +52,7 @@ func main() { ``` [在线运行程序](https://play.golang.org/p/XbJO2b0ZDW) -上面程序的第 8 行,**`len(s)` 返回字符串中字节的数量**,然后我们用了一个 for 循环以 16 进制的形式打印这些字节。`%x` 格式限定符用于指定 16 进制编码。上面的程序输出 `48 65 6c 6c 6f 20 57 6f 72 6c 64`。这些打印出来的字符是 "Hello World" 以 [Unicode UTF-8 编码](https://mothereff.in/utf-8#Hello%20World)的结果。为了更好的理解 go 中的字符串,需要对 Unicode 和 UTF-8 有基础的理解。我推荐阅读一下 [https://naveenr.net/unicode-character-set-and-utf-8-utf-16-utf-32-encoding/](https://naveenr.net/unicode-character-set-and-utf-8-utf-16-utf-32-encoding/) 来理解一下什么是 Unicode 和 UTF-8。 +上面程序的第 8 行,**`len(s)` 返回字符串中字节的数量**,然后我们用了一个 for 循环以 16 进制的形式打印这些字节。`%x` 格式限定符用于指定 16 进制编码。上面的程序输出 `48 65 6c 6c 6f 20 57 6f 72 6c 64`。这些打印出来的字符是 "Hello World" 以 [Unicode UTF-8 编码](https://mothereff.in/utf-8#Hello%20World)的结果。为了更好的理解 Go 中的字符串,需要对 Unicode 和 UTF-8 有基础的理解。我推荐阅读一下 [https://naveenr.net/unicode-character-set-and-utf-8-utf-16-utf-32-encoding/](https://naveenr.net/unicode-character-set-and-utf-8-utf-16-utf-32-encoding/) 来理解一下什么是 Unicode 和 UTF-8。 让我们稍微修改一下上面的程序,让它打印字符串的每一个字符。 @@ -119,7 +119,7 @@ func main() { fmt.Printf("\n") printChars(name) fmt.Printf("\n") - name = "Señor" + name = "Se ñ or" printBytes(name) fmt.Printf("\n") printChars(name) @@ -136,7 +136,7 @@ H e l l o W o r l d S e à ± o r ``` -在上面程序的第 28 行,我们尝试输出 **Señor** 的字符,但却输出了错误的 **S e à ± o r**。 为什么程序分割 `Hello World` 时表现完美,但分割 `Señor` 就出现了错误呢?这是因为 `ñ` 的 Unicode 代码点(Code Point)是 `U+00F1`。它的 [UTF-8 编码](https://mothereff.in/utf-8#%C3%B1)占用了 c3 和 b1 两个字节。它的 UTF-8 编码占用了两个字节 c3 和 b1。而我们打印字符时,却假定每个字符的编码只会占用一个字节,这是错误的。在 UTF-8 编码中,一个代码点可能会占用超过一个字节的空间。那么我们该怎么办呢?rune 能帮我们解决这个难题。 +在上面程序的第 28 行,我们尝试输出 **Se ñ or** 的字符,但却输出了错误的 **S e à ± o r**。 为什么程序分割 `Hello World` 时表现完美,但分割 `Se ñ or` 就出现了错误呢?这是因为 ` ñ ` 的 Unicode 代码点(Code Point)是 `U+00F1`。它的 [UTF-8 编码](https://mothereff.in/utf-8#%C3%B1)占用了 c3 和 b1 两个字节。它的 UTF-8 编码占用了两个字节 c3 和 b1。而我们打印字符时,却假定每个字符的编码只会占用一个字节,这是错误的。在 UTF-8 编码中,一个代码点可能会占用超过一个字节的空间。那么我们该怎么办呢?rune 能帮我们解决这个难题。 ## rune @@ -168,7 +168,7 @@ func main() { fmt.Printf("\n") printChars(name) fmt.Printf("\n\n") - name = "Señor" + name = "Se ñ or" printBytes(name) fmt.Printf("\n") printChars(name) @@ -205,13 +205,13 @@ func printCharsAndBytes(s string) { } func main() { - name := "Señor" + name := "Se ñ or" printCharsAndBytes(name) } ``` [在线运行程序](https://play.golang.org/p/BPpQ0dZr8W) -在上面程序中的第8行,使用 `for range` 循环遍历了字符串。循环返回的是是当前 rune 的字节位置。程序的输出结果为: +在上面程序中的第 8 行,使用 `for range` 循环遍历了字符串。循环返回的是是当前 rune 的字节位置。程序的输出结果为: ``` S starts at byte 0 @@ -220,7 +220,7 @@ e starts at byte 1 o starts at byte 4 r starts at byte 5 ``` -从上面的输出中可以清晰的看到 `ñ` 占了两个字节:)。 +从上面的输出中可以清晰的看到 ` ñ ` 占了两个字节:)。 ## 用字节切片构造字符串 @@ -239,7 +239,7 @@ func main() { ``` [在线运行程序](https://play.golang.org/p/Vr9pf8X8xO) -上面的程序中 `byteSlice` 包含字符串 `Café` 用 UTF-8 编码后的 16 进制字节。程序输出结果是 `Café`。 +上面的程序中 `byteSlice` 包含字符串 `Caf é` 用 UTF-8 编码后的 16 进制字节。程序输出结果是 `Caf é`。 如果我们把 16 进制换成对应的 10 进制值会怎么样呢?上面的程序还能工作吗?让我们来试一试: @@ -258,7 +258,7 @@ func main() { ``` [在线运行程序](https://play.golang.org/p/jgsRowW6XN) -上面程序的输出结果也是`Café` +上面程序的输出结果也是 `Caf é` ## 用 rune 切片构造字符串 @@ -277,7 +277,7 @@ func main() { ``` [在线运行程序](https://play.golang.org/p/m8wTMOpYJP) -在上面的程序中 `runeSlice` 包含字符串 `Señor`的 16 进制的 Unicode 代码点。这个程序将会输出`Señor`。 +在上面的程序中 `runeSlice` 包含字符串 `Se ñ or` 的 16 进制的 Unicode 代码点。这个程序将会输出 `Se ñ or`。 ## 字符串的长度 @@ -295,7 +295,7 @@ func length(s string) { fmt.Printf("length of %s is %d\n", s, utf8.RuneCountInString(s)) } func main() { - word1 := "Señor" + word1 := "Se ñ or" length(word1) word2 := "Pets" length(word2) @@ -306,7 +306,7 @@ func main() { 上面程序的输出结果是: ``` -length of Señor is 5 +length of Se ñ or is 5 length of Pets is 4 ``` @@ -356,7 +356,7 @@ func main() { 在上面程序的第 7 行,`mutate` 函数接收一个 rune 切片参数,它将切片的第一个元素修改为 `'a'`,然后将 rune 切片转化为字符串,并返回该字符串。程序的第 13 行调用了该函数。我们把 `h` 转化为一个 rune 切片,并传递给了 `mutate`。这个程序输出 `aello`。 -我已经在 github 上创建了一个程序,里面包含所有我们讨论过的内容。你可以在这[下载](https://github.com/golangbot/stringsexplained)它。 +我已经在 GitHub 上创建了一个程序,里面包含所有我们讨论过的内容。你可以在这[下载](https://github.com/golangbot/stringsexplained)它。 关于字符串的介绍到此为止。祝你愉快。 diff --git a/published/tech/20171117-Golang-tutorial-series/17-Methods.md b/published/tech/20171117-Golang-tutorial-series/17-Methods.md index adc2a06f0..d6e860aa7 100644 --- a/published/tech/20171117-Golang-tutorial-series/17-Methods.md +++ b/published/tech/20171117-Golang-tutorial-series/17-Methods.md @@ -1,6 +1,6 @@ 已发布:https://studygolang.com/articles/12264 -# 第17部分:方法 +# 第 17 部分:方法 欢迎来到 [Golang 系列教程](https://studygolang.com/subject/2) 的第 17 个教程。 @@ -99,7 +99,7 @@ func main() { 既然我们可以使用函数写出相同的程序,那么为什么我们需要方法?这有着几个原因,让我们一个个的看看。 -- [ Go 不是纯粹的面向对象编程语言](https://golang.org/doc/faq#Is_Go_an_object-oriented_language),而且Go不支持类。因此,基于类型的方法是一种实现和类相似行为的途径。 +- [ Go 不是纯粹的面向对象编程语言](https://golang.org/doc/faq#Is_Go_an_object-oriented_language),而且 Go 不支持类。因此,基于类型的方法是一种实现和类相似行为的途径。 - 相同的名字的方法可以定义在不同的类型上,而相同名字的函数是不被允许的。假设我们有一个 `Square` 和 `Circle` 结构体。可以在 `Square` 和 `Circle` 上分别定义一个 `Area` 方法。见下面的程序。 @@ -209,7 +209,7 @@ Employee age before change: 50 Employee age after change: 51 ``` -在上面程序的第 36 行,我们使用 `(&e).changeAge(51)` 来调用 `changeAge` 方法。由于 `changeAge` 方法有一个指针接收器,所以我们使用 `(&e)` 来调用这个方法。其实没有这个必要,Go语言让我们可以直接使用 `e.changeAge(51)`。`e.changeAge(51)` 会自动被Go语言解释为 `(&e).changeAge(51)`。 +在上面程序的第 36 行,我们使用 `(&e).changeAge(51)` 来调用 `changeAge` 方法。由于 `changeAge` 方法有一个指针接收器,所以我们使用 `(&e)` 来调用这个方法。其实没有这个必要,Go 语言让我们可以直接使用 `e.changeAge(51)`。`e.changeAge(51)` 会自动被 Go 语言解释为 `(&e).changeAge(51)`。 下面的[程序](https://play.golang.org/p/nnXBsR3Uc8)重写了,使用 `e.changeAge(51)` 来代替 `(&e).changeAge(51)`,它输出相同的结果。 @@ -314,7 +314,7 @@ Full address: Los Angeles, California ## 在方法中使用值接收器 与 在函数中使用值参数 -这个话题很多Go语言新手都弄不明白。我会尽量讲清楚。 +这个话题很多 Go 语言新手都弄不明白。我会尽量讲清楚。 当一个函数有一个值参数,它只能接受一个值参数。 @@ -369,7 +369,7 @@ func main() { 在第 28 行,我们创建了一个指向 `r` 的指针 `p`。如果我们试图把这个指针传递到只能接受一个值参数的函数 area,编译器将会报错。所以我把代码的第 33 行注释了。如果你把这行的代码注释去掉,编译器将会抛出错误 `compilation error, cannot use p (type *rectangle) as type rectangle in argument to area.`。这将会按预期抛出错误。 -现在到了棘手的部分了,在第35行的代码 `p.area()` 使用指针接收器 `p` 调用了只接受一个值接收器的方法 `area`。这是完全有效的。原因是当 `area` 有一个值接收器时,为了方便Go语言把 `p.area()` 解释为 `(*p).area()`。 +现在到了棘手的部分了,在第 35 行的代码 `p.area()` 使用指针接收器 `p` 调用了只接受一个值接收器的方法 `area`。这是完全有效的。原因是当 `area` 有一个值接收器时,为了方便 Go 语言把 `p.area()` 解释为 `(*p).area()`。 该程序将会输出: @@ -430,7 +430,7 @@ func main() { 在被注释掉的第 33 行,我们尝试通过传入值参数 `r` 调用函数 `perimeter`。这是不被允许的,因为函数的指针参数不接受值参数。如果你把这行的代码注释去掉并把程序运行起来,编译器将会抛出错误 `main.go:33: cannot use r (type rectangle) as type *rectangle in argument to perimeter.`。 -在第 35 行,我们通过值接收器 `r` 来调用有指针接收器的方法 `perimeter`。这是被允许的,为了方便Go语言把代码 `r.perimeter()` 解释为 `(&r).perimeter()`。该程序输出: +在第 35 行,我们通过值接收器 `r` 来调用有指针接收器的方法 `perimeter`。这是被允许的,为了方便 Go 语言把代码 `r.perimeter()` 解释为 `(&r).perimeter()`。该程序输出: ``` perimeter function output: 30 @@ -480,13 +480,13 @@ func main() { [在线运行程序](https://play.golang.org/p/sTe7i1qAng) -在上面程序的第5行,我们为 `int` 创建了一个类型别名 `myInt`。在第7行,我们定义了一个以 `myInt` 为接收器的的方法 `add`。 +在上面程序的第 5 行,我们为 `int` 创建了一个类型别名 `myInt`。在第 7 行,我们定义了一个以 `myInt` 为接收器的的方法 `add`。 该程序将会打印出 `Sum is 15`。 我已经创建了一个程序,包含了我们迄今为止所讨论的所有概念,详见[github](https://github.com/golangbot/methods)。 -这就是Go中的方法。祝你有美好的一天。 +这就是 Go 中的方法。祝你有美好的一天。 **上一教程 - [结构体](https://studygolang.com/articles/12263)** diff --git a/published/tech/20171117-Golang-tutorial-series/18-Interfaces-I.md b/published/tech/20171117-Golang-tutorial-series/18-Interfaces-I.md index 44c51583f..8462bed0a 100644 --- a/published/tech/20171117-Golang-tutorial-series/18-Interfaces-I.md +++ b/published/tech/20171117-Golang-tutorial-series/18-Interfaces-I.md @@ -130,7 +130,7 @@ func main() { 第 36 行声明的 `totalExpense` 方法体现出了接口的妙用。该方法接收一个 `SalaryCalculator` 接口的切片(`[]SalaryCalculator`)作为参数。在第 49 行,我们向 `totalExpense` 方法传递了一个包含 `Permanent` 和 `Contact` 类型的切片。在第 39 行中,通过调用不同类型对应的 `CalculateSalary` 方法,`totalExpense` 可以计算得到支出。 -这样做最大的优点是:`totalExpense` 可以扩展新的员工类型,而不需要修改任何代码。假如公司增加了一种新的员工类型 `Freelancer`,它有着不同的薪资结构。`Freelancer`只需传递到 `totalExpense` 的切片参数中,无需 `totalExpense` 方法本身进行修改。只要 `Freelancer` 也实现了 `SalaryCalculator` 接口,`totalExpense` 就能够实现其功能。 +这样做最大的优点是:`totalExpense` 可以扩展新的员工类型,而不需要修改任何代码。假如公司增加了一种新的员工类型 `Freelancer`,它有着不同的薪资结构。`Freelancer` 只需传递到 `totalExpense` 的切片参数中,无需 `totalExpense` 方法本身进行修改。只要 `Freelancer` 也实现了 `SalaryCalculator` 接口,`totalExpense` 就能够实现其功能。 该程序输出 `Total Expense Per Month $14050`。 diff --git a/published/tech/20171117-Golang-tutorial-series/2-Hello-World.md b/published/tech/20171117-Golang-tutorial-series/2-Hello-World.md index dfe3dfbc1..64c8dae86 100644 --- a/published/tech/20171117-Golang-tutorial-series/2-Hello-World.md +++ b/published/tech/20171117-Golang-tutorial-series/2-Hello-World.md @@ -69,7 +69,7 @@ go hello helloworld.go ``` -3. 第 3 种运行程序的好方法是使用 go playground。尽管它有自身的限制,但该方法对于运行简单的程序非常方便。我已经在 playground 上创建了一个 hello world 程序。[点击这里](https://play.golang.org/p/VtXafkQHYe) 在线运行程序。 +3. 第 3 种运行程序的好方法是使用 Go playground。尽管它有自身的限制,但该方法对于运行简单的程序非常方便。我已经在 playground 上创建了一个 hello world 程序。[点击这里](https://play.golang.org/p/VtXafkQHYe) 在线运行程序。 你可以使用 [go playground](https://play.golang.org) 与其他人分享你的源代码。 ### 简述 hello world 程序 @@ -87,7 +87,7 @@ func main() { //3 ``` 现在简单介绍每一行大概都做了些什么,在以后的教程中还会深入探讨每个部分。 -**package main - 每一个 Go 文件都应该在开头进行 `package name` 的声明**(译注:只有可执行程序的包名应当为 main)。包(Packages)用于代码的封装与重用,这里的包名称是`main`。 +**package main - 每一个 Go 文件都应该在开头进行 `package name` 的声明**(译注:只有可执行程序的包名应当为 main)。包(Packages)用于代码的封装与重用,这里的包名称是 `main`。 **import "fmt"** - 我们引入了 fmt 包,用于在 main 函数里面打印文本到标准输出。 diff --git a/published/tech/20171117-Golang-tutorial-series/20-Introduction-to-Concurrency.md b/published/tech/20171117-Golang-tutorial-series/20-Introduction-to-Concurrency.md index ace25f535..96782011e 100644 --- a/published/tech/20171117-Golang-tutorial-series/20-Introduction-to-Concurrency.md +++ b/published/tech/20171117-Golang-tutorial-series/20-Introduction-to-Concurrency.md @@ -22,7 +22,7 @@ 通过现实中的例子,我们已经明白了什么是并发,以及并发与并行的区别。作为一名极客,我们接下来从技术的角度来考察并发和并行。:) -假如我们正在编写一个 web 浏览器。这个 web 浏览器有各种组件。其中两个分别是 web 页面的渲染区和从网上下载文件的下载器。假设我们已经构建好了浏览器代码,各个组件也都可以相互独立地运行(通过像 Java 里的线程,或者通过即将介绍的 Go 语言中的 [Go 协程](#)来实现)。当浏览器在单核处理器中运行时,处理器会在浏览器的两个组件间进行上下文切换。它可能在一段时间内下载文件,转而又对用户请求的 web 页面进行渲染。这就是并发。并发的进程从不同的时间点开始,分别交替运行。在这里,就是在不同的时间点开始进行下载和渲染,并相互交替运行的。 +假如我们正在编写一个 Web 浏览器。这个 Web 浏览器有各种组件。其中两个分别是 Web 页面的渲染区和从网上下载文件的下载器。假设我们已经构建好了浏览器代码,各个组件也都可以相互独立地运行(通过像 Java 里的线程,或者通过即将介绍的 Go 语言中的 [Go 协程](https://studygolang.com/articles/12342)来实现)。当浏览器在单核处理器中运行时,处理器会在浏览器的两个组件间进行上下文切换。它可能在一段时间内下载文件,转而又对用户请求的 Web 页面进行渲染。这就是并发。并发的进程从不同的时间点开始,分别交替运行。在这里,就是在不同的时间点开始进行下载和渲染,并相互交替运行的。 如果该浏览器在一个多核处理器上运行,此时下载文件的组件和渲染 HTML 的组件可能会在不同的核上同时运行。这称之为并行。 @@ -32,7 +32,7 @@ ## Go 对并发的支持 -Go 编程语言原生支持并发。Go 使用 [Go 协程](#)(Goroutine) 和信道(Channel)来处理并发。在接下来的教程里,我们还会详细介绍它们。 +Go 编程语言原生支持并发。Go 使用 [Go 协程](https://studygolang.com/articles/12342)(Goroutine) 和信道(Channel)来处理并发。在接下来的教程里,我们还会详细介绍它们。 并发的介绍到此结束。请留下你的反馈和评论。祝你愉快。 diff --git a/published/tech/20171117-Golang-tutorial-series/22-Channels.md b/published/tech/20171117-Golang-tutorial-series/22-Channels.md index ebe687fec..b7ac5fc4c 100644 --- a/published/tech/20171117-Golang-tutorial-series/22-Channels.md +++ b/published/tech/20171117-Golang-tutorial-series/22-Channels.md @@ -1,3 +1,5 @@ +首发于:https://studygolang.com/articles/12402 + # 第 22 篇:信道(channel) 欢迎来到 [Golang 系列教程](https://studygolang.com/subject/2)的第 22 篇。 @@ -145,14 +147,14 @@ import ( ) func hello(done chan bool) { - fmt.Println("hello go routine is going to sleep") + fmt.Println("hello Go routine is going to sleep") time.Sleep(4 * time.Second) - fmt.Println("hello go routine awake and going to write to done") + fmt.Println("hello Go routine awake and going to write to done") done <- true } func main() { done := make(chan bool) - fmt.Println("Main going to call hello go goroutine") + fmt.Println("Main going to call hello Go goroutine") go hello(done) <-done fmt.Println("Main received data") @@ -162,7 +164,7 @@ func main() { 在上面程序里,我们向 `hello` 函数里添加了 4 秒的休眠(第 10 行)。 -程序首先会打印 `Main going to call hello go goroutine`。接着会开启 `hello` 协程,打印 `hello go routine is going to sleep`。打印完之后,`hello` 协程会休眠 4 秒钟,而在这期间,主协程会在 `<-done` 这一行发生阻塞,等待来自信道 `done` 的数据。4 秒钟之后,打印 `hello go routine awake and going to write to done`,接着再打印 `Main received data`。 +程序首先会打印 `Main going to call hello Go goroutine`。接着会开启 `hello` 协程,打印 `hello Go routine is going to sleep`。打印完之后,`hello` 协程会休眠 4 秒钟,而在这期间,主协程会在 `<-done` 这一行发生阻塞,等待来自信道 `done` 的数据。4 秒钟之后,打印 `hello Go routine awake and going to write to done`,接着再打印 `Main received data`。 ## 信道的另一个示例 diff --git a/published/tech/20171117-Golang-tutorial-series/23-Buffered-Channels-and-Worker-Pools.md b/published/tech/20171117-Golang-tutorial-series/23-Buffered-Channels-and-Worker-Pools.md index ee88ab123..57cdff302 100644 --- a/published/tech/20171117-Golang-tutorial-series/23-Buffered-Channels-and-Worker-Pools.md +++ b/published/tech/20171117-Golang-tutorial-series/23-Buffered-Channels-and-Worker-Pools.md @@ -79,7 +79,7 @@ func main() { ``` [在线运行程序](https://play.golang.org/p/bKe5GdgMK9) -在上面的程序中,第 16 行在 Go 主协程中创建了容量为 2 的缓冲信道 `ch`,而第 17 行把 `ch` 传递给了 `write` 协程。接下来 Go 主协程休眠了两秒。在这期间,`write` 协程在并发地运行。`write` 协程有一个 for 循环,依次向信道 `ch` 写入 0~4。而缓冲信道的容量为 2,因此 `write` 协程里立即会向 `ch` 写入 0 和 1,接下来发生阻塞,直到 `ch` 内的值被读取。因此,该程序立即打印出下面两行: +在上面的程序中,第 16 行在 Go 主协程中创建了容量为 2 的缓冲信道 `ch`,而第 17 行把 `ch` 传递给了 `write` 协程。接下来 Go 主协程休眠了两秒。在这期间,`write` 协程在并发地运行。`write` 协程有一个 for 循环,依次向信道 `ch` 写入 0 ~ 4。而缓冲信道的容量为 2,因此 `write` 协程里立即会向 `ch` 写入 0 和 1,接下来发生阻塞,直到 `ch` 内的值被读取。因此,该程序立即打印出下面两行: ``` successfully wrote 0 to ch @@ -206,7 +206,7 @@ func main() { go process(i, &wg) } wg.Wait() - fmt.Println("All go routines finished executing") + fmt.Println("All Go routines finished executing") } ``` [在线运行程序](https://play.golang.org/p/CZNtu8ktQh) @@ -226,7 +226,7 @@ started Goroutine 1 Goroutine 0 ended Goroutine 2 ended Goroutine 1 ended -All go routines finished executing +All Go routines finished executing ``` 由于 Go 协程的执行顺序不一定,因此你的输出可能和我不一样。:) diff --git a/published/tech/20171117-Golang-tutorial-series/24-Select.md b/published/tech/20171117-Golang-tutorial-series/24-Select.md index f9a2a7a2b..4b48e7a34 100644 --- a/published/tech/20171117-Golang-tutorial-series/24-Select.md +++ b/published/tech/20171117-Golang-tutorial-series/24-Select.md @@ -27,8 +27,8 @@ func server2(ch chan string) { func main() { output1 := make(chan string) output2 := make(chan string) - go server1(output1) - go server2(output2) + Go server1(output1) + Go server2(output2) select { case s1 := <-output1: fmt.Println(s1) @@ -43,7 +43,7 @@ func main() { 而 `main` 函数在第 20 行和第 21 行,分别调用了 `server1` 和 `server2` 两个 Go 协程。 -在第 22 行,程序运行到了 `select` 语句。`select` 会一直发生阻塞,除非其中有 case 准备就绪。在上述程序里,`server1` 协程会在 6 秒之后写入 `output1` 信道,而`server2` 协程在 3 秒之后就写入了 `output2` 信道。因此 `select` 语句会阻塞 3 秒钟,等着 `server2` 向 `output2` 信道写入数据。3 秒钟过后,程序会输出: +在第 22 行,程序运行到了 `select` 语句。`select` 会一直发生阻塞,除非其中有 case 准备就绪。在上述程序里,`server1` 协程会在 6 秒之后写入 `output1` 信道,而 `server2` 协程在 3 秒之后就写入了 `output2` 信道。因此 `select` 语句会阻塞 3 秒钟,等着 `server2` 向 `output2` 信道写入数据。3 秒钟过后,程序会输出: ``` from server2 @@ -74,7 +74,7 @@ func process(ch chan string) { func main() { ch := make(chan string) - go process(ch) + Go process(ch) for { time.Sleep(1000 * time.Millisecond) select { @@ -204,8 +204,8 @@ func server2(ch chan string) { func main() { output1 := make(chan string) output2 := make(chan string) - go server1(output1) - go server2(output2) + Go server1(output1) + Go server2(output2) time.Sleep(1 * time.Second) select { case s1 := <-output1: diff --git a/published/tech/20171117-Golang-tutorial-series/25-Mutex.md b/published/tech/20171117-Golang-tutorial-series/25-Mutex.md index 5eedb7daf..be5a397f5 100644 --- a/published/tech/20171117-Golang-tutorial-series/25-Mutex.md +++ b/published/tech/20171117-Golang-tutorial-series/25-Mutex.md @@ -30,7 +30,7 @@ x = x + 1 ![one-scenario](https://raw.githubusercontent.com/studygolang/gctt-images/master/golang-series/cs5.png) -我们假设 `x` 的初始值为 0。而协程 1 获取 `x` 的初始值,并计算 `x + 1`。而在协程 1 将计算值赋值给 `x` 之前,系统上下文切换到了协程 2。于是,协程 2 获取了 `x` 的初始值(依然为 0),并计算 `x + 1`。接着系统上下文又切换回了协程 1。现在,协程 1 将计算值 1 赋值给 `x`,因此 `x` 等于 1。然后,协程 2 继续开始执行,把计算值(依然是 1)复制给了 `x`,因此在所有协程执行完毕之后,`x` 都等于 1。 +我们假设 `x` 的初始值为 0。而协程 1 获取 `x` 的初始值,并计算 `x + 1`。而在协程 1 将计算值赋值给 `x` 之前,系统上下文切换到了协程 2。于是,协程 2 获取了 `x` 的初始值(依然为 0), 并计算 `x + 1`。接着系统上下文又切换回了协程 1。现在,协程 1 将计算值 1 赋值给 `x`,因此 `x` 等于 1。然后,协程 2 继续开始执行,把计算值(依然是 1)复制给了 `x`,因此在所有协程执行完毕之后,`x` 都等于 1。 现在我们考虑另外一种可能发生的情况。 @@ -76,7 +76,7 @@ func main() { var w sync.WaitGroup for i := 0; i < 1000; i++ { w.Add(1) - go increment(&w) + Go increment(&w) } w.Wait() fmt.Println("final value of x", x) @@ -111,7 +111,7 @@ func main() { var m sync.Mutex for i := 0; i < 1000; i++ { w.Add(1) - go increment(&w, &m) + Go increment(&w, &m) } w.Wait() fmt.Println("final value of x", x) @@ -120,7 +120,7 @@ func main() { [在 playground 中运行](https://play.golang.org/p/VX9dwGhR62) -[Mutex](https://golang.org/pkg/sync/#Mutex) 是一个结构体类型,我们在第 15 行创建了 `Mutex` 类型的变量 `m`,其值为零值。在上述程序里,我们修改了 `increment` 函数,将增加 `x` 的代码(`x = x + 1`)放置在 `m.Lock()` 和 `m.Unlock()`之间。现在这段代码不存在竞态条件了,因为任何时刻都只允许一个协程执行这段代码。 +[Mutex](https://golang.org/pkg/sync/#Mutex) 是一个结构体类型,我们在第 15 行创建了 `Mutex` 类型的变量 `m`,其值为零值。在上述程序里,我们修改了 `increment` 函数,将增加 `x` 的代码(`x = x + 1`)放置在 `m.Lock()` 和 `m.Unlock()` 之间。现在这段代码不存在竞态条件了,因为任何时刻都只允许一个协程执行这段代码。 于是如果运行该程序,会输出: @@ -152,7 +152,7 @@ func main() { ch := make(chan bool, 1) for i := 0; i < 1000; i++ { w.Add(1) - go increment(&w, ch) + Go increment(&w, ch) } w.Wait() fmt.Println("final value of x", x) diff --git a/published/tech/20171117-Golang-tutorial-series/26-Structs-Instead-of-Classes.md b/published/tech/20171117-Golang-tutorial-series/26-Structs-Instead-of-Classes.md index 852ba90ae..c310810b0 100644 --- a/published/tech/20171117-Golang-tutorial-series/26-Structs-Instead-of-Classes.md +++ b/published/tech/20171117-Golang-tutorial-series/26-Structs-Instead-of-Classes.md @@ -80,7 +80,7 @@ func main() { 我们在第 3 行引用了 `employee` 包。在 `main()`(第 12 行),我们调用了 `Employee` 的 `LeavesRemaining()` 方法。 -由于有自定义包,这个程序不能在 go playground 上运行。你可以在你的本地运行,在 `workspacepath/bin/oop` 下输入命令 `go install opp`,程序会打印输出: +由于有自定义包,这个程序不能在 Go playground 上运行。你可以在你的本地运行,在 `workspacepath/bin/oop` 下输入命令 `go install opp`,程序会打印输出: ```bash Sam Adolf has 10 leaves remaining diff --git a/published/tech/20171117-Golang-tutorial-series/29-Defer.md b/published/tech/20171117-Golang-tutorial-series/29-Defer.md index f5e91e239..2331f87f2 100644 --- a/published/tech/20171117-Golang-tutorial-series/29-Defer.md +++ b/published/tech/20171117-Golang-tutorial-series/29-Defer.md @@ -124,7 +124,7 @@ value of a before deferred function call 10 value of a in deferred function 5 ``` -从上面的输出,我们可以看出,在调用了 `defer` 语句后,虽然我们将 `a` 修改为 10,但调用延迟函数 `printA(a)`后,仍然打印的是 5。 +从上面的输出,我们可以看出,在调用了 `defer` 语句后,虽然我们将 `a` 修改为 10,但调用延迟函数 `printA(a)` 后,仍然打印的是 5。 ## defer 栈 @@ -204,7 +204,7 @@ func main() { go v.area(&wg) } wg.Wait() - fmt.Println("All go routines finished executing") + fmt.Println("All Go routines finished executing") } ``` @@ -256,7 +256,7 @@ func main() { go v.area(&wg) } wg.Wait() - fmt.Println("All go routines finished executing") + fmt.Println("All Go routines finished executing") } ``` @@ -268,7 +268,7 @@ func main() { rect {8 9}'s area 72 rect {-67 89}'s length should be greater than zero rect {5 -67}'s width should be greater than zero -All go routines finished executing +All Go routines finished executing ``` 在上面的程序中,使用 `defer` 还有一个好处。假设我们使用 `if` 条件语句,又给 `area` 方法添加了一条返回路径(Return Path)。如果没有使用 `defer` 来调用 `wg.Done()`,我们就得很小心了,确保在这条新添的返回路径里调用了 `wg.Done()`。由于现在我们延迟调用了 `wg.Done()`,因此无需再为这条新的返回路径添加 `wg.Done()` 了。 diff --git a/published/tech/20171117-Golang-tutorial-series/3-variables.md b/published/tech/20171117-Golang-tutorial-series/3-variables.md index c7fec7e04..eb2b2132b 100644 --- a/published/tech/20171117-Golang-tutorial-series/3-variables.md +++ b/published/tech/20171117-Golang-tutorial-series/3-variables.md @@ -171,7 +171,7 @@ func main() { ``` [在线运行程序](https://play.golang.org/p/7pkp74h_9L) -这里我们声明了 **string 类型的 name、int 类型的 age 和 height**(我们将会在下一教程中讨论 golang 所支持的变量类型)。运行上面的程序会产生输出 `my name is naveen , age is 29 and height is 0`。 +这里我们声明了 **string 类型的 name、int 类型的 age 和 height**(我们将会在下一教程中讨论 Golang 所支持的变量类型)。运行上面的程序会产生输出 `my name is naveen , age is 29 and height is 0`。 ## 简短声明 @@ -216,11 +216,11 @@ package main import "fmt" func main() { - a, b := 20, 30 // 声明变量a和b + a, b := 20, 30 // 声明变量 a 和 b fmt.Println("a is", a, "b is", b) - b, c := 40, 50 // b已经声明,但c尚未声明 + b, c := 40, 50 // b 已经声明,但 c 尚未声明 fmt.Println("b is", b, "c is", c) - b, c = 80, 90 // 给已经声明的变量b和c赋新值 + b, c = 80, 90 // 给已经声明的变量 b 和 c 赋新值 fmt.Println("changed b is", b, "c is", c) } ``` @@ -240,7 +240,7 @@ package main import "fmt" func main() { - a, b := 20, 30 // 声明a和b + a, b := 20, 30 // 声明 a 和 b fmt.Println("a is", a, "b is", b) a, b := 40, 50 // 错误,没有尚未声明的变量 } @@ -278,8 +278,8 @@ minimum value is 145.8 package main func main() { - age := 29 // age是int类型 - age = "naveen" // 错误,尝试赋值一个字符串给int类型变量 + age := 29 // age 是 int 类型 + age = "naveen" // 错误,尝试赋值一个字符串给 int 类型变量 } ``` [在线运行程序](https://play.golang.org/p/K5rz4gxjPj) diff --git a/published/tech/20171117-Golang-tutorial-series/30-error-handling.md b/published/tech/20171117-Golang-tutorial-series/30-error-handling.md index 60ae5e65f..a85d44300 100644 --- a/published/tech/20171117-Golang-tutorial-series/30-error-handling.md +++ b/published/tech/20171117-Golang-tutorial-series/30-error-handling.md @@ -289,7 +289,7 @@ matched files [] **上一教程 - [Defer](https://studygolang.com/articles/12719)** -**下一教程 - 自定义错误(暂未发布,敬请期待)** +**下一教程 - [自定义错误](https://studygolang.com/articles/12784)** --- diff --git a/published/tech/20171117-Golang-tutorial-series/31-custom-errors.md b/published/tech/20171117-Golang-tutorial-series/31-custom-errors.md index 510f8ad37..830e3a9d6 100644 --- a/published/tech/20171117-Golang-tutorial-series/31-custom-errors.md +++ b/published/tech/20171117-Golang-tutorial-series/31-custom-errors.md @@ -54,7 +54,7 @@ import ( func circleArea(radius float64) (float64, error) { if radius < 0 { - return 0, errors.New("Area calculation failed, radius is less than zero") + return 0, errors.New("Area calculation failed, radius is Less than zero") } return math.Pi * radius * radius, nil } @@ -79,7 +79,7 @@ func main() { 在我们的程序中,半径小于零,因此打印出: ``` -Area calculation failed, radius is less than zero +Area calculation failed, radius is Less than zero ``` ## 使用 Errorf 给错误添加更多信息 @@ -98,7 +98,7 @@ import ( func circleArea(radius float64) (float64, error) { if radius < 0 { - return 0, fmt.Errorf("Area calculation failed, radius %0.2f is less than zero", radius) + return 0, fmt.Errorf("Area calculation failed, radius %0.2f is Less than zero", radius) } return math.Pi * radius * radius, nil } @@ -119,12 +119,12 @@ func main() { 在上面的程序中,我们使用 `Errorf`(第 10 行)打印了发生错误的半径。程序运行后会输出: ``` -Area calculation failed, radius -20.00 is less than zero +Area calculation failed, radius -20.00 is Less than zero ``` ## 使用结构体类型和字段提供错误的更多信息 -错误还可以用实现了 `error` [接口](https://studygolang.com/articles/12266)的结构体来表示。这种方式可以更加灵活地处理错误。在上面例子中,如果我们希望访问引发错误的半径,现在唯一的方法就是解析错误的描述信息 `Area calculation failed, radius -20.00 is less than zero`。这样做不太好,因为一旦描述信息发生变化,程序就会出错。 +错误还可以用实现了 `error` [接口](https://studygolang.com/articles/12266)的结构体来表示。这种方式可以更加灵活地处理错误。在上面例子中,如果我们希望访问引发错误的半径,现在唯一的方法就是解析错误的描述信息 `Area calculation failed, radius -20.00 is Less than zero`。这样做不太好,因为一旦描述信息发生变化,程序就会出错。 我们会使用标准库里采用的方法,在上一教程中“断言底层结构体类型,使用结构体字段获取更多信息”这一节,我们讲解了这一方法,可以使用结构体字段来访问引发错误的半径。我们会创建一个实现 `error` 接口的结构体类型,并使用它的字段来提供关于错误的更多信息。 @@ -180,7 +180,7 @@ func main() { area, err := circleArea(radius) if err != nil { if err, ok := err.(*areaError); ok { - fmt.Printf("Radius %0.2f is less than zero", err.radius) + fmt.Printf("Radius %0.2f is Less than zero", err.radius) return } fmt.Println(err) @@ -205,7 +205,7 @@ func main() { 该程序会输出: ``` -Radius -20.00 is less than zero +Radius -20.00 is Less than zero ``` 下面我们来使用上一教程提到的[第二种方法](https://studygolang.com/articles/12724),使用自定义错误类型的方法来提供错误的更多信息。 @@ -250,13 +250,13 @@ func (e *areaError) widthNegative() bool { func rectArea(length, width float64) (float64, error) { err := "" if length < 0 { - err += "length is less than zero" + err += "length is Less than zero" } if width < 0 { if err == "" { - err = "width is less than zero" + err = "width is Less than zero" } else { - err += ", width is less than zero" + err += ", width is Less than zero" } } if err != "" { @@ -277,11 +277,11 @@ func main() { if err != nil { if err, ok := err.(*areaError); ok { if err.lengthNegative() { - fmt.Printf("error: length %0.2f is less than zero\n", err.length) + fmt.Printf("error: length %0.2f is Less than zero\n", err.length) } if err.widthNegative() { - fmt.Printf("error: width %0.2f is less than zero\n", err.width) + fmt.Printf("error: width %0.2f is Less than zero\n", err.width) } return @@ -325,13 +325,13 @@ func (e *areaError) widthNegative() bool { func rectArea(length, width float64) (float64, error) { err := "" if length < 0 { - err += "length is less than zero" + err += "length is Less than zero" } if width < 0 { if err == "" { - err = "width is less than zero" + err = "width is Less than zero" } else { - err += ", width is less than zero" + err += ", width is Less than zero" } } if err != "" { @@ -346,11 +346,11 @@ func main() { if err != nil { if err, ok := err.(*areaError); ok { if err.lengthNegative() { - fmt.Printf("error: length %0.2f is less than zero\n", err.length) + fmt.Printf("error: length %0.2f is Less than zero\n", err.length) } if err.widthNegative() { - fmt.Printf("error: width %0.2f is less than zero\n", err.width) + fmt.Printf("error: width %0.2f is Less than zero\n", err.width) } return @@ -367,8 +367,8 @@ func main() { 该程序会打印输出: ``` -error: length -5.00 is less than zero -error: width -9.00 is less than zero +error: length -5.00 is Less than zero +error: width -9.00 is Less than zero ``` 在上一教程[错误处理](https://studygolang.com/articles/12724)中,我们介绍了三种提供更多错误信息的方法,现在我们已经看了其中两个示例。 @@ -388,7 +388,7 @@ error: width -9.00 is less than zero **上一教程 - [错误处理](https://studygolang.com/articles/12724)** -**下一教程 - panic 和 recover(暂未发布,敬请期待)** +**下一教程 - [panic 和 recover](https://studygolang.com/articles/12785)** --- diff --git a/published/tech/20171117-Golang-tutorial-series/32-panic-and-recover.md b/published/tech/20171117-Golang-tutorial-series/32-panic-and-recover.md index 09cbb3622..276cfd9af 100644 --- a/published/tech/20171117-Golang-tutorial-series/32-panic-and-recover.md +++ b/published/tech/20171117-Golang-tutorial-series/32-panic-and-recover.md @@ -23,7 +23,7 @@ panic 有两个合理的用例。 1. **发生了一个不能恢复的错误,此时程序不能继续运行**。 - 一个例子就是 web 服务器无法绑定所要求的端口。在这种情况下,就应该使用 panic,因为如果不能绑定端口,啥也做不了。 + 一个例子就是 Web 服务器无法绑定所要求的端口。在这种情况下,就应该使用 panic,因为如果不能绑定端口,啥也做不了。 2. **发生了一个编程上的错误**。 假如我们有一个接收指针参数的方法,而其他人使用 `nil` 作为参数调用了它。在这种情况下,我们可以使用 panic,因为这是一个编程错误:用 `nil` 参数调用了一个只能接收合法指针的方法。 @@ -268,7 +268,7 @@ func recovery() { func a() { defer recovery() fmt.Println("Inside A") - go b() + Go b() time.Sleep(1 * time.Second) } @@ -483,7 +483,7 @@ normally returned from main **上一教程 - [自定义错误](https://studygolang.com/articles/12784)** -**下一教程 - 函数是一等公民(暂未发布,敬请期待)** +**下一教程 - [函数是一等公民](https://studygolang.com/articles/12789)** --- diff --git a/published/tech/20171117-Golang-tutorial-series/33-first-class-functions.md b/published/tech/20171117-Golang-tutorial-series/33-first-class-functions.md index 83683ac87..218e389a9 100644 --- a/published/tech/20171117-Golang-tutorial-series/33-first-class-functions.md +++ b/published/tech/20171117-Golang-tutorial-series/33-first-class-functions.md @@ -421,6 +421,8 @@ func main() { **上一教程 - [panic 和 recover](https://studygolang.com/articles/12785)** +**下一教程 - [反射](https://studygolang.com/articles/13178)** + --- via: https://golangbot.com/first-class-functions/ diff --git a/published/tech/20171117-Golang-tutorial-series/34-reflection.md b/published/tech/20171117-Golang-tutorial-series/34-reflection.md index 1a80a5a68..490bd96a5 100644 --- a/published/tech/20171117-Golang-tutorial-series/34-reflection.md +++ b/published/tech/20171117-Golang-tutorial-series/34-reflection.md @@ -1,6 +1,6 @@ 已发布:https://studygolang.com/articles/13178 -# 反射 +# 第 34 篇:反射 ![reflection](https://raw.githubusercontent.com/studygolang/gctt-images/master/golang-series/reflection-golang-3.png) @@ -475,6 +475,8 @@ insert into order(ordId, customerId) values(456, 56) **上一教程 - [函数是一等公民](https://studygolang.com/articles/12789)** +**下一教程 - [读取文件](https://studygolang.com/articles/14669)** + --- via: https://golangbot.com/reflection/ diff --git a/published/tech/20171117-Golang-tutorial-series/20180808-Part-35-Reading-Files.md b/published/tech/20171117-Golang-tutorial-series/35-Reading-Files.md similarity index 98% rename from published/tech/20171117-Golang-tutorial-series/20180808-Part-35-Reading-Files.md rename to published/tech/20171117-Golang-tutorial-series/35-Reading-Files.md index 676a58bad..52d441db0 100644 --- a/published/tech/20171117-Golang-tutorial-series/20180808-Part-35-Reading-Files.md +++ b/published/tech/20171117-Golang-tutorial-series/35-Reading-Files.md @@ -1,6 +1,6 @@ 首发于:https://studygolang.com/articles/14669 -# 读取文件 +# 第 35 篇:读取文件 ![reading files](https://raw.githubusercontent.com/studygolang/gctt-images/master/golang-series/golang-read-files.png) @@ -60,7 +60,7 @@ func main() { ```bash $ cd /home/naveen/go/src/filehandling/ -$ go install filehandling +$ Go install filehandling $ workspacepath/bin/filehandling ``` @@ -68,7 +68,7 @@ $ workspacepath/bin/filehandling ```bash > cd C:\Users\naveen.r\go\src\filehandling -> go install filehandling +> Go install filehandling > workspacepath\bin\filehandling.exe ``` @@ -122,7 +122,7 @@ func main() { ```bash $ cd $HOME -$ go install filehandling +$ Go install filehandling $ workspacepath/bin/filehandling ``` @@ -343,7 +343,7 @@ func main() { 如果我们使用下面命令来运行程序: ```bash -$ go install filehandling +$ Go install filehandling $ wrkspacepath/bin/filehandling -fpath=/path-of-file/test.txt ``` @@ -430,7 +430,7 @@ func main() { 如果我使用下面命令来运行程序: ```bash -$ go install filehandling +$ Go install filehandling $ workspacepath/bin/filehandling -fpath=/path-of-file/test.txt ``` @@ -446,6 +446,8 @@ We have reached the end of the file. **上一教程** - [反射](https://studygolang.com/articles/13178) +**下一教程** - [写入文件](https://studygolang.com/articles/19475) + --- via: https://golangbot.com/read-files/ diff --git a/published/tech/20171117-Golang-tutorial-series/36-Write-File.md b/published/tech/20171117-Golang-tutorial-series/36-Write-File.md index 8320f76e6..ea0cf061d 100644 --- a/published/tech/20171117-Golang-tutorial-series/36-Write-File.md +++ b/published/tech/20171117-Golang-tutorial-series/36-Write-File.md @@ -212,8 +212,8 @@ File handling is easy. 我们将写一个程序,该程序创建 100 个 goroutinues。每个 goroutinue 将并发产生一个随机数,届时将有 100 个随机数产生。这些随机数将被写入到文件里面。我们将用下面的方法解决这个问题 . 1. 创建一个 channel 用来读和写这个随机数。 -2. 创建 100 个生产者 goroutine。每个 goroutine 将产生随机数并将随机数写入到 channel 里。 -3. 创建一个消费者 goroutine 用来从 channel 读取随机数并将它写入文件。这样的话我们就只有一个 goroutinue 向文件中写数据,从而避免竞争条件。 +2. 创建 100 个生产者 goroutine。每个 Goroutine 将产生随机数并将随机数写入到 channel 里。 +3. 创建一个消费者 Goroutine 用来从 channel 读取随机数并将它写入文件。这样的话我们就只有一个 goroutinue 向文件中写数据,从而避免竞争条件。 4. 一旦完成则关闭文件。 我们开始写产生随机数的 `produce` 函数: @@ -322,9 +322,9 @@ func main() { } ``` -`main` 函数在第 41 行创建写入和读取数据的 channel,在第 42 行创建 `done` 这个 channel,此 channel 用于消费者 goroutinue 完成任务之后通知 `main` 函数。第 43 行创建 Waitgroup 的实例 `wg`,用于等待所有生产随机数的 goroutine 完成任务。 +`main` 函数在第 41 行创建写入和读取数据的 channel,在第 42 行创建 `done` 这个 channel,此 channel 用于消费者 goroutinue 完成任务之后通知 `main` 函数。第 43 行创建 Waitgroup 的实例 `wg`,用于等待所有生产随机数的 Goroutine 完成任务。 -在第 44 行使用 `for` 循环创建 100 个 goroutines。在第 49 行调用 waitgroup 的 `wait()` 方法等待所有的 goroutines 完成随机数的生成。然后关闭 channel。当 channel 关闭时,消费者 `consume` goroutine 已经将所有的随机数写入文件,在第 37 行 将 `true` 写入 `done` 这个 channel 中,这个时候 `main` 函数解除阻塞并且打印 `File written successfully`。 +在第 44 行使用 `for` 循环创建 100 个 goroutines。在第 49 行调用 waitgroup 的 `wait()` 方法等待所有的 goroutines 完成随机数的生成。然后关闭 channel。当 channel 关闭时,消费者 `consume` Goroutine 已经将所有的随机数写入文件,在第 37 行 将 `true` 写入 `done` 这个 channel 中,这个时候 `main` 函数解除阻塞并且打印 `File written successfully`。 现在你可以用任何的文本编辑器打开文件 `concurrent`,可以看到 100 个随机数已经写入 :) diff --git a/published/tech/20171117-Golang-tutorial-series/4-Types.md b/published/tech/20171117-Golang-tutorial-series/4-Types.md index 81f4fa03e..c7aef5266 100644 --- a/published/tech/20171117-Golang-tutorial-series/4-Types.md +++ b/published/tech/20171117-Golang-tutorial-series/4-Types.md @@ -55,23 +55,23 @@ d: true **int8**:表示 8 位有符号整型 **大小**:8 位 -**范围**:-128~127 +**范围**:-128 ~ 127 **int16**:表示 16 位有符号整型 **大小**:16 位 -**范围**:-32768~32767 +**范围**:-32768 ~ 32767 **int32**:表示 32 位有符号整型 **大小**:32 位 -**范围**:-2147483648~2147483647 +**范围**:-2147483648 ~ 2147483647 **int64**:表示 64 位有符号整型 **大小**:64 位 -**范围**:-9223372036854775808~9223372036854775807 +**范围**:-9223372036854775808 ~ 9223372036854775807 **int**:根据不同的底层平台(Underlying Platform),表示 32 或 64 位整型。除非对整型的大小有特定的需求,否则你通常应该使用 *int* 表示整型。 **大小**:在 32 位系统下是 32 位,而在 64 位系统下是 64 位。 -**范围**:在 32 位系统下是 -2147483648~2147483647,而在 64 位系统是 -9223372036854775808~9223372036854775807。 +**范围**:在 32 位系统下是 -2147483648 ~ 2147483647,而在 64 位系统是 -9223372036854775808 ~ 9223372036854775807。 ```go package main @@ -123,23 +123,23 @@ type of b is int, size of b is 4 **uint8**:表示 8 位无符号整型 **大小**:8 位 -**范围**:0~255 +**范围**:0 ~ 255 **uint16**:表示 16 位无符号整型 **大小**:16 位 -**范围**:0~65535 +**范围**:0 ~ 65535 **uint32**:表示 32 位无符号整型 **大小**:32 位 -**范围**:0~4294967295 +**范围**:0 ~ 4294967295 **uint64**:表示 64 位无符号整型 **大小**:64 位 -**范围**:0~18446744073709551615 +**范围**:0 ~ 18446744073709551615 **uint**:根据不同的底层平台,表示 32 或 64 位无符号整型。 **大小**:在 32 位系统下是 32 位,而在 64 位系统下是 64 位。 -**范围**:在 32 位系统下是 0~4294967295,而在 64 位系统是 0~18446744073709551615。 +**范围**:在 32 位系统下是 0 ~ 4294967295,而在 64 位系统是 0 ~ 18446744073709551615。 ## 浮点型 @@ -214,7 +214,7 @@ func main() { ``` [在线运行程序](https://play.golang.org/p/kEz1uKCdKs) -在上面的程序里,c1 和 c2 是两个复数。c1的实部为 5,虚部为 7。c2 的实部为8,虚部为 27。c1 和 c2 的和赋值给 `cadd` ,而 c1 和 c2 的乘积赋值给 `cmul`。该程序将输出: +在上面的程序里,c1 和 c2 是两个复数。c1 的实部为 5,虚部为 7。c2 的实部为 8,虚部为 27。c1 和 c2 的和赋值给 `cadd` ,而 c1 和 c2 的乘积赋值给 `cmul`。该程序将输出: ``` sum: (13+34i) diff --git a/published/tech/20171117-Golang-tutorial-series/5-constants.md b/published/tech/20171117-Golang-tutorial-series/5-constants.md index c457e5b0b..3674d5904 100644 --- a/published/tech/20171117-Golang-tutorial-series/5-constants.md +++ b/published/tech/20171117-Golang-tutorial-series/5-constants.md @@ -107,7 +107,7 @@ func main() { 在上面的代码中,我们首先创建一个变量 `defaultName` 并分配一个常量 `Sam` 。**常量 `Sam` 的默认类型是 `string` ,所以在赋值后 `defaultName` 是 `string` 类型的。** -下一行,我们将创建一个新类型 `myString`,它是 `string` 的别名。 +下一行,我们将创建一个新类型 `myString`,它的底层类型是 `string`(译注:原文说是别名,是不对的)。 然后我们创建一个 `myString` 的变量 `customName` 并且给他赋值一个常量 `Sam` 。因为常量 `Sam` 是无类型的,它可以分配给任何字符串变量。因此这个赋值是允许的,`customName` 的类型是 `myString`。 diff --git a/published/tech/20171117-Golang-tutorial-series/6-functions.md b/published/tech/20171117-Golang-tutorial-series/6-functions.md index 5c160c631..2f3bcb66c 100644 --- a/published/tech/20171117-Golang-tutorial-series/6-functions.md +++ b/published/tech/20171117-Golang-tutorial-series/6-functions.md @@ -18,7 +18,7 @@ func functionname(parametername type) returntype { } ``` -函数的声明以关键词 `func` 开始,后面紧跟自定义的函数名 `functionname (函数名)`。函数的参数列表定义在 `(` 和 `)` 之间,返回值的类型则定义在之后的 `returntype (返回值类型)`处。声明一个参数的语法采用 **参数名** **参数类型** 的方式,任意多个参数采用类似 `(parameter1 type, parameter2 type) 即(参数1 参数1的类型,参数2 参数2的类型)`的形式指定。之后包含在 `{` 和 `}` 之间的代码,就是函数体。 +函数的声明以关键词 `func` 开始,后面紧跟自定义的函数名 `functionname (函数名)`。函数的参数列表定义在 `(` 和 `)` 之间,返回值的类型则定义在之后的 `returntype (返回值类型)` 处。声明一个参数的语法采用 **参数名** **参数类型** 的方式,任意多个参数采用类似 `(parameter1 type, parameter2 type) 即(参数 1 参数 1 的类型,参数 2 参数 2 的类型)` 的形式指定。之后包含在 `{` 和 `}` 之间的代码,就是函数体。 函数中的参数列表和返回值并非是必须的,所以下面这个函数的声明也是有效的 diff --git a/published/tech/20171117-Golang-tutorial-series/7-packages.md b/published/tech/20171117-Golang-tutorial-series/7-packages.md index 8e30e7127..5c7945024 100644 --- a/published/tech/20171117-Golang-tutorial-series/7-packages.md +++ b/published/tech/20171117-Golang-tutorial-series/7-packages.md @@ -202,15 +202,15 @@ import ( var rectLen, rectWidth float64 = 6, 7 /* -*2. init 函数会检查长和宽是否大于0 +*2. init 函数会检查长和宽是否大于 0 */ func init() { println("main package initialized") if rectLen < 0 { - log.Fatal("length is less than zero") + log.Fatal("length is Less than zero") } if rectWidth < 0 { - log.Fatal("width is less than zero") + log.Fatal("width is Less than zero") } } @@ -253,10 +253,10 @@ diagonal of the rectangle 9.22 ``` rectangle package initialized main package initialized -2017/04/04 00:28:20 length is less than zero +2017/04/04 00:28:20 length is Less than zero ``` -像往常一样, 会首先初始化 rectangle 包,然后是 main 包中的包级别的变量 rectLen 和 rectWidth。rectLen 为负数,因此当运行 init 函数时,程序在打印 `length is less than zero` 后终止。 +像往常一样, 会首先初始化 rectangle 包,然后是 main 包中的包级别的变量 rectLen 和 rectWidth。rectLen 为负数,因此当运行 init 函数时,程序在打印 `length is Less than zero` 后终止。 本代码可以在 [github](https://github.com/golangbot/geometry) 下载。 diff --git a/published/tech/20171117-Golang-tutorial-series/8-if-statement.md b/published/tech/20171117-Golang-tutorial-series/8-if-statement.md index 603d8c5cc..0f35530f6 100644 --- a/published/tech/20171117-Golang-tutorial-series/8-if-statement.md +++ b/published/tech/20171117-Golang-tutorial-series/8-if-statement.md @@ -88,7 +88,7 @@ import ( func main() { num := 99 if num <= 50 { - fmt.Println("number is less than or equal to 50") + fmt.Println("number is Less than or equal to 50") } else if num >= 51 && num <= 100 { fmt.Println("number is between 51 and 100") } else { @@ -136,7 +136,7 @@ main.go:12:5: syntax error: unexpected else, expecting } 出错的原因是 Go 语言的分号是自动插入。你可以在这里阅读分号插入规则 [https://golang.org/ref/spec#Semicolons](https://golang.org/ref/spec#Semicolons)。 -在 Go 语言规则中,它指定在 `}` 之后插入一个分号,如果这是该行的最终标记。因此,在if语句后面的 `}` 会自动插入一个分号。 +在 Go 语言规则中,它指定在 `}` 之后插入一个分号,如果这是该行的最终标记。因此,在 if 语句后面的 `}` 会自动插入一个分号。 实际上我们的程序变成了 @@ -188,4 +188,5 @@ via: https://golangbot.com/if-statement/ 作者:[Nick Coghlan](https://golangbot.com/about/) 译者:[Dingo1991](https://github.com/Dingo1991) 校对:[rxcai](https://github.com/rxcai) + 本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20171117-Golang-tutorial-series/9-Loops.md b/published/tech/20171117-Golang-tutorial-series/9-Loops.md index f6bd5918c..55d459322 100644 --- a/published/tech/20171117-Golang-tutorial-series/9-Loops.md +++ b/published/tech/20171117-Golang-tutorial-series/9-Loops.md @@ -191,7 +191,7 @@ for { } ``` -下一个程序就会一直打印`Hello World`不会停止。 +下一个程序就会一直打印 `Hello World` 不会停止。 ```go package main diff --git a/published/tech/20171120-Go-Slice-vs-Maps.md b/published/tech/20171120-Go-Slice-vs-Maps.md index 27981e9f4..5bb2087c7 100644 --- a/published/tech/20171120-Go-Slice-vs-Maps.md +++ b/published/tech/20171120-Go-Slice-vs-Maps.md @@ -106,9 +106,9 @@ Go 中的 Map 和其他语言类似(内部实现可能会有所不同)。Go 有关测试的细节: -系统详情 | go操作系统:darwin | Go-1.9.2 +系统详情 | go 操作系统:darwin | Go-1.9.2 ---|---|--- - MAC-OSX | go架构:amd64 | + MAC-OSX | go 架构:amd64 | 源代码: @@ -218,7 +218,7 @@ func Benchmark_TimeRangeSliceInt(b *testing.B) { b.StartTimer() - b.N = 2000000 // 只是为了避免数百万次fmt.Println(以防你在 slicemap.go 包中进行 fmt.Println) + b.N = 2000000 // 只是为了避免数百万次 fmt.Println(以防你在 slicemap.go 包中进行 fmt.Println) for i := 0; i < b.N; i++ { RangeSliceInt(input, 100009) // 对于最坏情况,检查最后一个元素 @@ -255,7 +255,7 @@ func Benchmark_TimeMapLookupInt(b *testing.B) { b.StartTimer() - b.N = 2000000 // 只是为了避免数百万次fmt.Println(以防你在 slicemap.go 包中进行 fmt.Println) + b.N = 2000000 // 只是为了避免数百万次 fmt.Println(以防你在 slicemap.go 包中进行 fmt.Println) for k := 0; k < b.N; k++ { MapLookupInt(input, 100000) @@ -263,7 +263,7 @@ func Benchmark_TimeMapLookupInt(b *testing.B) { /* 运行命令: - go test -bench=Benchmark_TimeMapLookup + Go test -bench=Benchmark_TimeMapLookup */ } @@ -277,7 +277,7 @@ func Benchmark_TimeSliceRangeInt(b *testing.B) { b.StartTimer() - b.N = 2000000 // 只是为了避免数百万次fmt.Println(以防你在 slicemap.go 包中进行 fmt.Println) + b.N = 2000000 // 只是为了避免数百万次 fmt.Println(以防你在 slicemap.go 包中进行 fmt.Println) for k := 0; k < b.N; k++ { RangeSliceIntPrint(input) @@ -295,7 +295,7 @@ func Benchmark_TimeMapRangeInt(b *testing.B) { b.StartTimer() - b.N = 2000 // 只是为了避免数百万次fmt.Println(以防你在 slicemap.go 包中进行 fmt.Println) + b.N = 2000 // 只是为了避免数百万次 fmt.Println(以防你在 slicemap.go 包中进行 fmt.Println) for k := 0; k < b.N; k++ { MapRangeInt(input) @@ -315,7 +315,7 @@ func Benchmark_TimeRangeSliceString(b *testing.B) { b.StartTimer() - b.N = 2000000 // 只是为了避免数百万次fmt.Println(以防你在 slicemap.go 包中进行 fmt.Println) + b.N = 2000000 // 只是为了避免数百万次 fmt.Println(以防你在 slicemap.go 包中进行 fmt.Println) for i := 0; i < b.N; i++ { RangeSliceString(input, "100009") // 对于最坏情况,检查最后一个元素 @@ -332,7 +332,7 @@ func Benchmark_TimeDirectSliceString(b *testing.B) { b.StartTimer() - b.N = 2000000 // 只是为了避免数百万次fmt.Println(以防你在 slicemap.go 包中进行 fmt.Println) + b.N = 2000000 // 只是为了避免数百万次 fmt.Println(以防你在 slicemap.go 包中进行 fmt.Println) for i := 0; i < b.N; i++ { DirectSliceString(input, 99999) // 直接检查索引值。o(1) } @@ -350,7 +350,7 @@ func Benchmark_TimeMapLookupString(b *testing.B) { b.StartTimer() - b.N = 2000000 // 只是为了避免数百万次fmt.Println(以防你在 slicemap.go 包中进行 fmt.Println) + b.N = 2000000 // 只是为了避免数百万次 fmt.Println(以防你在 slicemap.go 包中进行 fmt.Println) for k := 0; k < b.N; k++ { MapLookupString(input, "100000") @@ -360,7 +360,7 @@ func Benchmark_TimeMapLookupString(b *testing.B) { 运行: - go test -bench=Benchmark_TimeMapLookupString + Go test -bench=Benchmark_TimeMapLookupString */ } diff --git a/published/tech/20171124-Go-Defer-Simplified-with-Practical-Visuals.md b/published/tech/20171124-Go-Defer-Simplified-with-Practical-Visuals.md index bf2477ee8..4054344e1 100644 --- a/published/tech/20171124-Go-Defer-Simplified-with-Practical-Visuals.md +++ b/published/tech/20171124-Go-Defer-Simplified-with-Practical-Visuals.md @@ -6,7 +6,7 @@ ## 什么是 defer ? -通过使用 `defer` 修饰一个函数,使其在外部函数 ["返回后"](https://medium.com/@inanc/yeah-semantically-after-is-the-right-word-fad1d5181891) 才被执行,即便外部的函数返回的是 [panic 异常](https://golang.org/ref/spec#Handling_panics),这类函数被称作 `延迟调用函数`。 +通过使用 `defer` 修饰一个函数,使其在外部函数 ["返回后"](https://medium.com/@inanc/yeah-semantically-after-is-the-right-word-fad1d5181891) 才被执行,即便外部的函数返回的是 [panic 异常](https://golang.org/ref/spec#Handling_panics),这类函数被称作 ` 延迟调用函数 `。 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/golang-defer-simplified/what_is_defer.png) @@ -36,7 +36,7 @@ _打印: “first” 然后 “later”_ [在线运行代码](https://play.golang.org/p/Q4P6v_kIAx) -这个延迟函数关闭了已经打开的文件句柄,不论 `NewFromFile` 函数是否返回了错误。 +这个延迟函数关闭了已经打开的文件句柄, 不论 `NewFromFile` 函数是否返回了错误。 ### 从 panic 中恢复 @@ -108,7 +108,7 @@ _分析可视化数字(在左边): 1, 2, 3 ._ ### 延迟调用多个函数 -如果有多个延迟函数,它们会被存储在一个`栈`中,因此,最后被 `defer` 修饰的函数会在函数体返回之后先执行。 +如果有多个延迟函数,它们会被存储在一个 ` 栈 ` 中,因此,最后被 `defer` 修饰的函数会在函数体返回之后先执行。 *注意:同时使用多个 `defer` 表达式可能会降低代码的可读性* diff --git a/published/tech/20171124-Introduction-to-bufio-package-in-Golang.md b/published/tech/20171124-Introduction-to-bufio-package-in-Golang.md index 4e662bcec..78f3cd4a3 100644 --- a/published/tech/20171124-Introduction-to-bufio-package-in-Golang.md +++ b/published/tech/20171124-Introduction-to-bufio-package-in-Golang.md @@ -1,14 +1,18 @@ +首发于:https://studygolang.com/articles/11824 + # Go 语言 bufio 包的介绍 + [原文链接](https://medium.com/golangspec/introduction-to-bufio-package-in-golang-ad7d1877f762) [bufio](https://golang.org/pkg/bufio/) 用来帮助处理 [I/O 缓存](https://www.quora.com/In-C-what-does-buffering-I-O-or-buffered-I-O-mean/answer/Robert-Love-1)。 我们将通过一些示例来熟悉其为我们提供的:Reader, Writer and Scanner 等一系列功能 ## bufio.Writer + 多次进行小量的写操作会影响程序性能。每一次写操作最终都会体现为系统层调用,频繁进行该操作将有可能对 CPU 造成伤害。而且很多硬件设备更适合处理块对齐的数据,例如硬盘。为了减少进行多次写操作所需的开支,golang 提供了 [bufio.Writer](https://golang.org/pkg/bufio/#Writer)。数据将不再直接写入目的地(实现了 [io.Writer](https://golang.org/pkg/io/#Writer) 接口),而是先写入缓存,当缓存写满后再统一写入目的地: ``` producer --> buffer --> io.Writer ``` -下面具体看一下在9次写入操作中(每次写入一个字符)具有4个字符空间的缓存是如何工作的: +下面具体看一下在 9 次写入操作中(每次写入一个字符)具有 4 个字符空间的缓存是如何工作的: ``` producer buffer destination (io.Writer) a -----> a @@ -24,6 +28,7 @@ producer buffer destination (io.Writer) `----->` 箭头代表写入操作 [`bufio.Writer`](https://golang.org/pkg/bufio/#Writer) 底层使用 `[]byte` 进行缓存 + ```go type Writer int func (*Writer) Write(p []byte) (n int, err error) { @@ -57,14 +62,17 @@ Buffered I/O 3 1 ``` -没有被缓存的 `I/O`:意味着每一次写操作都将直接写入目的地。我们进行4次写操作,每次写操作都映射为对 `Write` 的调用,调用时传入的参数为一个长度为1的 `byte` 切片。 + +没有被缓存的 `I/O`:意味着每一次写操作都将直接写入目的地。我们进行 4 次写操作,每次写操作都映射为对 `Write` 的调用,调用时传入的参数为一个长度为 1 的 `byte` 切片。 使用了缓存的 `I/O`:我们使用三个字节长度的缓存来存储数据,当缓存满时进行一次 `flush` 操作(将缓存中的数据进行处理)。前三次写入写满了缓存。第四次写入时检测到缓存没有剩余空间,所以将缓存中的积累的数据写出。字母 `d` 被存储了,但在此之前 `Flush` 被调用以腾出空间。当缓存被写到末尾时,缓存中未被处理的数据需要被处理。`bufio.Writer` 仅在缓存充满或者显式调用 `Flush` 方法时处理(发送)数据。 > `bufio.Writer` 默认使用 4096 长度字节的缓存,可以使用 [`NewWriterSize`](https://golang.org/pkg/bufio/#NewWriterSize) 方法来设定该值 ## 实现 + 实现十分简单: + ```go type Writer struct { err error @@ -73,7 +81,9 @@ type Writer struct { wr io.Writer } ``` + 字段 `buf` 用来存储数据,当缓存满或者 `Flush` 被调用时,消费者(`wr`)可以从缓存中获取到数据。如果写入过程中发生了 I/O error,此 error 将会被赋给 `err` 字段, error 发生之后,writer 将停止操作(writer is no-op): + ```go type Writer int func (*Writer) Write(p []byte) (n int, err error) { @@ -93,9 +103,11 @@ func main() { Write: "abc" boom! ``` + 这里我们可以看到 `Flush` 没有第二次调用消费者的 `write` 方法。如果发生了 error, 使用了缓存的 writer 不会尝试再次执行写操作。 字段 `n` 标识缓存内部当前操作的位置。`Buffered` 方法返回 `n` 的值: + ```go type Writer int func (*Writer) Write(p []byte) (n int, err error) { @@ -120,9 +132,10 @@ func main() { 3 1 ``` -`n` 从 0 开始,当有数据被添加到缓存中时,该数据的长度值将会被加和到 `n`中(操作位置向后移动)。当`bw.Write([] byte{'d'})`被调用时,flush会被触发,`n` 会被重设为0。 +`n` 从 0 开始,当有数据被添加到缓存中时,该数据的长度值将会被加和到 `n` 中(操作位置向后移动)。当 `bw.Write([] byte{'d'})` 被调用时,flush 会被触发,`n` 会被重设为 0。 ## Large writes + ```go type Writer int func (*Writer) Write(p []byte) (n int, err error) { @@ -135,10 +148,13 @@ func main() { bw.Write([]byte("abcd")) } ``` + 由于使用了 `bufio`,程序打印了 `"abcd"`。如果 `Writer` 检测到 `Write` 方法被调用时传入的数据长度大于缓存的长度(示例中是三个字节)。其将直接调用 writer(目的对象)的 `Write` 方法。当数据量足够大时,其会自动跳过内部缓存代理。 ## 重置 + 缓存是 `bufio` 的核心部分。通过使用 `Reset` 方法,`Writer` 可以用于不同的目的对象。重复使用 `Writer` 缓存减少了内存的分配。而且减少了额外的垃圾回收工作: + ```go type Writer1 int func (*Writer1) Write(p []byte) (n int, err error) { @@ -163,7 +179,9 @@ func main() { writer#1: "ab" writer#2: "ef" ``` -这段代码中有一个 bug。在调用 `Reset` 方法之前,我们应该使用 `Flush` flush缓存。 由于 [`Reset`](https://github.com/golang/go/blob/7b8a7f8272fd1941a199af1adb334bd9996e8909/src/bufio/bufio.go#L559) 只是简单的丢弃未被处理的数据,所以已经被写入的数据 `cd` 丢失了: + +这段代码中有一个 bug。在调用 `Reset` 方法之前,我们应该使用 `Flush` flush 缓存。 由于 [`Reset`](https://github.com/golang/go/blob/7b8a7f8272fd1941a199af1adb334bd9996e8909/src/bufio/bufio.go#L559) 只是简单的丢弃未被处理的数据,所以已经被写入的数据 `cd` 丢失了: + ```go func (b *Writer) Reset(w io.Writer) { b.err = nil @@ -173,7 +191,9 @@ func (b *Writer) Reset(w io.Writer) { ``` ## 缓存剩余空间 + 为了检测缓存中还剩余多少空间, 我们可以使用方法 `Available`: + ```go w := new(Writer) bw := bufio.NewWriterSize(w, 2) @@ -190,8 +210,10 @@ fmt.Println(bw.Available()) 1 ``` -## 写`{Byte,Rune,String}`的方法 +## 写 `{Byte,Rune,String}` 的方法 + 为了方便, 我们有三个用来写普通类型的实用方法: + ```go w := new(Writer) bw := bufio.NewWriterSize(w, 10) @@ -209,15 +231,18 @@ fmt.Println(bw.Buffered()) ``` ## ReadFrom + io 包中定义了 [`io.ReaderFrom`](https://golang.org/pkg/io/#ReaderFrom) 接口。 该接口通常被 writer 实现,用于从指定的 reader 中读取所有数据(直到 EOF)并对读到的数据进行底层处理: + ```go type ReaderFrom interface { - ReadFrom(r Reader) (n int64, err error) + ReadFrom(r Reader) (n int64, err error) } ``` ->比如 [`io.Copy`](https://golang.org/pkg/io/#Copy) 使用了 `io.ReaderFrom` 接口 +> 比如 [`io.Copy`](https://golang.org/pkg/io/#Copy) 使用了 `io.ReaderFrom` 接口 `bufio.Writer` 实现了此接口:因此我们可以通过调用 `ReadFrom` 方法来处理从 `io.Reader` 获取到的所有数据: + ```go type Writer int func (*Writer) Write(p []byte) (n int, err error) { @@ -239,14 +264,15 @@ func main() { "thr" "ee" ``` ->使用 `ReadFrom` 方法的同时,调用 `Flush` 方法也很重要 +> 使用 `ReadFrom` 方法的同时,调用 `Flush` 方法也很重要 ## bufio.Reader + 通过它,我们可以从底层的 `io.Reader` 中更大批量的读取数据。这会使读取操作变少。如果数据读取时的块数量是固定合适的,底层媒体设备将会有更好的表现,也因此会提高程序的性能: ``` io.Reader --> buffer --> consumer ``` -假设消费者想要从硬盘上读取10个字符(每次读取一个字符)。在底层实现上,这将会触发10次读取操作。如果硬盘按每个数据块四个字节来读取数据,那么 `bufio.Reader` 将会起到帮助作用。底层引擎将会缓存整个数据块,然后提供一个可以挨个读取字节的 API 给消费者: +假设消费者想要从硬盘上读取 10 个字符(每次读取一个字符)。在底层实现上,这将会触发 10 次读取操作。如果硬盘按每个数据块四个字节来读取数据,那么 `bufio.Reader` 将会起到帮助作用。底层引擎将会缓存整个数据块,然后提供一个可以挨个读取字节的 API 给消费者: ``` abcd -----> abcd -----> a abcd -----> b @@ -259,16 +285,18 @@ efgh -----> efgh -----> e ijkl -----> ijkl -----> i ijkl -----> j ``` -`----->` 代表读取操作
-这个方法仅需要从硬盘读取三次,而不是10次。 +`----->` 代表读取操作
+这个方法仅需要从硬盘读取三次,而不是 10 次。 ## Peek + `Peek` 方法可以帮助我们查看缓存的前 n 个字节而不会真的『吃掉』它: - 如果缓存不满,而且缓存中缓存的数据少于 `n` 个字节,其将会尝试从 `io.Reader` 中读取 - 如果请求的数据量大于缓存的容量,将会返回 `bufio.ErrBufferFull` - 如果 `n` 大于流的大小,将会返回 EOF 让我们来看看它是如何工作的: + ```go s1 := strings.NewReader(strings.Repeat("a", 20)) r := bufio.NewReaderSize(s1, 16) @@ -291,9 +319,10 @@ if err != nil { bufio: buffer full EOF ``` ->被 `bufio.Reader` 使用的最小的缓存容器是 16。 +> 被 `bufio.Reader` 使用的最小的缓存容器是 16。 返回的切片和被 `bufio.Reader` 使用的内部缓存底层使用相同的数组。因此引擎底层在执行任何读取操作之后内部返回的切片将会变成无效的。这是由于其将有可能被其他的缓存数据覆盖: + ```go s1 := strings.NewReader(strings.Repeat("a", 16) + strings.Repeat("b", 16)) r := bufio.NewReaderSize(s1, 16) @@ -307,7 +336,9 @@ fmt.Printf("%q\n", b) ``` ## Reset + 就像 `bufio.Writer` 那样,缓存也可以用相似的方式被复用。 + ```go s1 := strings.NewReader("abcd") r := bufio.NewReader(s1) @@ -330,7 +361,9 @@ fmt.Printf("%q\n", b) 通过使用 `Reset`,我们可以避免冗余的内存分配和不必要的垃圾回收工作。 ## Discard + 这个方法将会丢弃 `n` 个字节的,返回时也不会返回被丢弃的 `n` 个字节。如果 `bufio.Reader` 缓存了超过或者等于 `n` 个字节的数据。那么其将不必从 `io.Reader` 中读取任何数据。其只是简单的从缓存中略去前 `n` 个字节: + ```go type R struct{} func (r *R) Read(p []byte) (n int, err error) { @@ -353,6 +386,7 @@ Read "ijkl" ``` 调用 `Discard` 方法将不会从 reader `r` 中读取数据。另一种情况,缓存中数据量小于 `n`,那么 `bufio.Reader` 将会读取需要数量的数据来确保被丢弃的数据量不会少于 `n`: + ```go type R struct{} func (r *R) Read(p []byte) (n int, err error) { @@ -380,16 +414,19 @@ Discard 由于调用了 `Discard` 方法,所以读取方法被调用了两次。 ## Read + `Read` 方法是 `bufio.Reader` 的核心。它和 [`io.Reader`](https://golang.org/pkg/io/#Reader) 的唯一方法具有相同的签名。因此 `bufio.Reader` 实现了这个普遍存在的接口: + ```go type Reader interface { - Read(p []byte) (n int, err error) + Read(p []byte) (n int, err error) } ``` `bufio.Reader` 的 `Read` 方法从底层的 `io.Reader` 中一次读取最大的数量: 1. 如果内部缓存具有至少一个字节的数据,那么无论传入的切片的大小(`len(p)`)是多少,`Read` 方法都将仅仅从内部缓存中获取数据,不会从底层的 reader 中读取任何数据: + ```go func (r *R) Read(p []byte) (n int, err error) { fmt.Println("Read") @@ -414,15 +451,18 @@ func main() { Read read = "cd", n = 2 ``` -我们的 `io.Reader` 实例无线返回「abcd」(不会返回 `io.EOF`)。 第二次调用 `Read`并传入长度为4的切片,但是内部缓存在第一次从 `io.Reader` 中读取数据之后已经具有数据「cd」,所以 `bufio.Reader` 返回缓存中的数据数据,而不和底层 reader 进行通信。 +我们的 `io.Reader` 实例无线返回「abcd」(不会返回 `io.EOF`)。 第二次调用 `Read` 并传入长度为 4 的切片,但是内部缓存在第一次从 `io.Reader` 中读取数据之后已经具有数据「cd」,所以 `bufio.Reader` 返回缓存中的数据数据,而不和底层 reader 进行通信。 2. 如果内部缓存是空的,那么将会执行一次从底层 io.Reader 的读取操作。 从前面的例子中我们可以清晰的看到如果我们开启了一个空的缓存,然后调用: + ```go n, err := br.Read(buf) ``` + 将会触发读取操作来填充缓存。 3. 如果内部缓存是空的,但是传入的切片长度大于缓存长度,那么 `bufio.Reader` 将会跳过缓存,直接读取传入切片长度的数据到切片中: + ```go type R struct{} func (r *R) Read(p []byte) (n int, err error) { @@ -445,10 +485,13 @@ Read read = "aaaaaaaaaaaaaaaaa", n = 17 buffered = 0 ``` + 从 `bufio.Reader` 读取之后,内部缓存中没有任何数据(`buffered = 0`) ## {Read, Unread}Byte + 这些方法都实现了从缓存中读取单个字节或者将最后一个读取的字节返回到缓存: + ```go r := strings.NewReader("abcd") br := bufio.NewReader(r) @@ -477,14 +520,19 @@ buffered = 3 ``` ## {Read, Unread}Rune + 这两个方法的功能和前面方法的功能差不多, 但是用来处理 Unicode 字符(UTF-8 encoded)。 ## ReadSlice + 函数返回在第一次出现传入字节前的字节: + ```go func (b *Reader) ReadSlice(delim byte) (line []byte, err error) ``` + 示例: + ```go s := strings.NewReader("abcdef|ghij") r := bufio.NewReader(s) @@ -495,13 +543,16 @@ if err != nil { fmt.Printf("Token: %q\n", token) Token: "abcdef|" ``` ->重要:返回的切面指向内部缓冲区, 因此它可能在下一次读取操作期间被覆盖 +> 重要:返回的切面指向内部缓冲区, 因此它可能在下一次读取操作期间被覆盖 如果找不到分隔符,而且已经读到末尾(EOF),将会返回 `io.EOF` error。 让我们将上面程序中的一行修改为如下代码: + ```go s := strings.NewReader("abcdefghij") ``` + 如果数据以 `panic: EOF` 结尾。 当分隔符找不到而且没有更多的数据可以放入缓冲区时函数将返回 [`io.ErrBufferFull`](https://golang.org/pkg/bufio/#pkg-variables): + ```go s := strings.NewReader(strings.Repeat("a", 16) + "|") r := bufio.NewReaderSize(s, 16) @@ -514,10 +565,13 @@ fmt.Printf("Token: %q\n", token) 这一小段代码会出现错误:`panic: bufio: buffer full`。 ## ReadBytes + ```go func (b *Reader) ReadBytes(delim byte) ([]byte, error) ``` + 返回出现第一次分隔符前的所有数据组成的字节切片。 它和 `ReadSlice` 具有相同的签名,但是 `ReadSlice` 是一个低级别的函数,`ReadBytes` 的实现使用了 `ReadSlice`。 那么两者之间有什么不同呢? 在分隔符找不到的情况下,`ReadBytes` 可以多次调用 `ReadSlice`,而且可以累积返回的数据。 这意味着 `ReadBytes` 将不再受到 缓存大小的限制: + ```go s := strings.NewReader(strings.Repeat("a", 40) + "|") r := bufio.NewReaderSize(s, 16) @@ -528,10 +582,13 @@ if err != nil { fmt.Printf("Token: %q\n", token) Token: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa|" ``` + 另外该函数返回一个新的字节切片,所以没有数据会被将来的读取操作覆盖的风险。 ## ReadString + 它是我们上面讨论的 `ReadBytes` 的简单封装: + ```go func (b *Reader) ReadString(delim byte) (string, error) { bytes, err := b.ReadBytes(delim) @@ -540,10 +597,13 @@ func (b *Reader) ReadString(delim byte) (string, error) { ``` ## ReadLine + ```go ReadLine() (line []byte, isPrefix bool, err error) ``` + 内部使用 `ReadSlice` (`ReadSlice('\n')`)实现,同时从返回的切片中移除掉换行符(`\n` 或者 `\r\n`)。 此方法的签名不同于 `ReadBytes` 或者 `ReadSlice`,因为它包含 `isPrefix` 标志。 由于内部缓存无法存储更多的数据,当找不到分隔符时该标志为 true: + ```go s := strings.NewReader(strings.Repeat("a", 20) + "\n" + "b") r := bufio.NewReaderSize(s, 16) @@ -571,7 +631,9 @@ Token: "aaaa", prefix: false Token: "b", prefix: false panic: EOF ``` + 如果最后一次返回的切片以换行符结尾,此方法将不会给出任何信息: + ```go s := strings.NewReader("abc") r := bufio.NewReaderSize(s, 16) @@ -592,13 +654,17 @@ Token: "abc", prefix: false ``` ## WriteTo + `bufio.Reader` 实现了 `io.WriterTo` 接口: + ```go type WriterTo interface { WriteTo(w Writer) (n int64, err error) } ``` + 此方法允许我们传入一个实现了 `io.Writer` 的消费者。 从生产者读取的所有数据都将会被送到消费者。 下面通过练习来看看它是如何工作的: + ```go type R struct { n int @@ -635,12 +701,15 @@ Written bytes: 40 ``` ## bufio.Scanner -[go语言中对bufio.Scanner的深层分析](https://medium.com/golangspec/in-depth-introduction-to-bufio-scanner-in-golang-55483bb689b4) + +[go 语言中对 bufio.Scanner 的深层分析](https://medium.com/golangspec/in-depth-introduction-to-bufio-scanner-in-golang-55483bb689b4) ## ReadBytes('\n'), ReadString('\n'), ReadLine 还是 Scanner? + 就像前面说的那样,`ReadString('\n')` 只是对于 `ReadBytes(`\n`)` 的简单封装。 所以让我们来讨论一下另外三者之间的不同之处吧。 1. ReadBytes 不会自动处理 `\r\n` 序列: + ```go s := strings.NewReader("a\r\nb") r := bufio.NewReader(s) @@ -677,6 +746,7 @@ Token (Scanner): "b" *ReadBytes* 会将分隔符一起返回,所以需要额外的一些工作来重新处理数据(除非返回分隔符是有用的)。 2. *ReadLine* 不会处理超出内部缓存的行: + ```go s := strings.NewReader(strings.Repeat("a", 20) + "\n") r := bufio.NewReaderSize(s, 16) @@ -695,6 +765,7 @@ Token (ReadBytes): "aaaaaaaaaaaaaaaaaaaa\n" Token (Scanner): "aaaaaaaaaaaaaaaaaaaa" ``` 为了取回流中剩余的数据,*ReadLine* 需要被调用两次。 被 Scanner 处理的最大 token 长度为 64*1024。 如果传入更长的 token,scanner 将无法工作。 当 *ReadLine* 被多次调用时可以处理任何长度的 token。 由于函数返回是否在缓存数据中找到分隔符的标志,但是这需要调用者进行处理。 *ReadBytes* 则没有任何限制: + ```go s := strings.NewReader(strings.Repeat("a", 64*1024) + "\n") r := bufio.NewReader(s) @@ -787,18 +858,18 @@ efgh 由于 reader 和 writer 都具有方法 *Buffered*,所以若想获取缓存数据的量,`rw.Buffered()` 将无法工作,编译器会报错:`ambiguous selector rw.Buffered`。 但是类似 `rw.Reader.Buffered()` 的方式是可以的。 ## bufio + standard library -*bufio* 包被广泛使用在 I/O出现的标准库中,例如: +*bufio* 包被广泛使用在 I/O 出现的标准库中,例如: * archive/zip * compress/* * encoding/* * image/* -* 类似于 net/http 的TCP连接包装。 它还结合一些类似于 sync.Pool 的缓存框架来减少垃圾回收的压力 +* 类似于 net/http 的 TCP 连接包装。 它还结合一些类似于 sync.Pool 的缓存框架来减少垃圾回收的压力 --- via: https://medium.com/golangspec/introduction-to-bufio-package-in-golang-ad7d1877f762 -作者:[Michał Łowicki](https://medium.com/@mlowicki?source=post_header_lockup) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki?source=post_header_lockup) 译者:[jliu666](https://github.com/jliu666) 校对:[rxcai](https://github.com/rxcai) diff --git a/published/tech/20171204-Go-is-not-very-simple-folks.md b/published/tech/20171204-Go-is-not-very-simple-folks.md index a3add7e8a..d8c2f57dc 100644 --- a/published/tech/20171204-Go-is-not-very-simple-folks.md +++ b/published/tech/20171204-Go-is-not-very-simple-folks.md @@ -138,7 +138,7 @@ func main() { 这个主题和错误处理比起来,可能是一个更大的蠕虫。 -和 errors 一样,我只想考虑一下这里的复杂性或者简单性。Go 社区的许多人似乎认为,泛型的本质上是复杂的(=坏,嗯嗯嗯咳),有这样或那样的巨大开销。这在某种程度上是事实,但我不认为它像有些人描述的那么糟糕。似乎那些人已经经历了 `C++` 模板的痛苦,从那以后,无论何时提及泛型,都会遭受 PTSD(创伤后应激障碍) 的攻击。 +和 errors 一样,我只想考虑一下这里的复杂性或者简单性。Go 社区的许多人似乎认为,泛型的本质上是复杂的(= 坏,嗯嗯嗯咳),有这样或那样的巨大开销。这在某种程度上是事实,但我不认为它像有些人描述的那么糟糕。似乎那些人已经经历了 `C++` 模板的痛苦,从那以后,无论何时提及泛型,都会遭受 PTSD(创伤后应激障碍) 的攻击。 看到这里的人,**泛型不是一个怪物**。它们当然绝对不应该像 `C++` 那样复杂(或者其他一些奇怪的语言)。我的意思是,甚至前端的人都用泛型工作了一段时间(TypeScript, Flow, …),如果他们不害怕泛型,其他程序员应该是没有理由害怕:)(对不起,前端开发者,只是开个玩笑。) @@ -186,7 +186,7 @@ myfoo := heap.Pop(&someheap) // myfoo has the correct type 所以,读者们,为什么所有这些都离开了你呢?是 Go 复杂还是其他什么原因? -当然不是,绝对不像 `C++` 或 `Haskell` 那样复杂。相比之下,Go 的确很简单。另一方面,比较 Go 和其他常见语言(如`Java` ,`JavaScript` ,`Python` 等)的复杂性时,情况就不太清楚了,正如我希望的那样。 (此外,这是一个很难,没有明确定义的任务。) +当然不是,绝对不像 `C++` 或 `Haskell` 那样复杂。相比之下,Go 的确很简单。另一方面,比较 Go 和其他常见语言(如 `Java` ,`JavaScript` ,`Python` 等)的复杂性时,情况就不太清楚了,正如我希望的那样。 (此外,这是一个很难,没有明确定义的任务。) 我可以提供类似的例子。在某些方面,Go 可能比这些语言更简单,有些则不是...大致上我会说它和其他常用语言的平均差不多。我也不认为简单,无论是感觉上还是实际使用中,最终的体验很重要。 diff --git a/published/tech/20171205-a-tale-of-two-rand.md b/published/tech/20171205-a-tale-of-two-rand.md index 64dcee612..0f246e715 100644 --- a/published/tech/20171205-a-tale-of-two-rand.md +++ b/published/tech/20171205-a-tale-of-two-rand.md @@ -26,7 +26,7 @@ 让我们深入研究下 `math/rand` 包。我们通过一个 `rand.Source` 来实例化 `rand.Rand` 类型。但是像绝大多数 Go 惯用法一样,这个 `Source` 是一个接口。我的第六感来了,或许这就是个机会? -`rand.Source` 最主要的工作由 `Int63() int64` 函数完成,它返回一个非负 `int64` 整数(也就是说,最高位是0)。进一步改进的 `rand.Source64` 仅仅返回一个 `uint64` 类型,并没有对最高位有任何限制。 +`rand.Source` 最主要的工作由 `Int63() int64` 函数完成,它返回一个非负 `int64` 整数(也就是说,最高位是 0)。进一步改进的 `rand.Source64` 仅仅返回一个 `uint64` 类型,并没有对最高位有任何限制。 你们说,我们使用源自 `crypto/rand` 包的功能来尝试创建一个 `rand.Source64` 对象如何?(你可以参考在 [Go Playground](https://play.golang.org/p/_3w6vWTwwE) 上的代码。) @@ -42,7 +42,7 @@ type mySrc struct{} func (s *mySrc) Seed(seed int64) { /*no-op*/ } ``` -因为 `Uint64()` 函数返回值取值范围**最广(widest)**,需要 64 位的随机数,因此我们首先实现它。我们使用 `encoding/binary` 包从`crypto/rand` 包的 `io.Reader` 接口中读取 8 个字节的数据,并直接转换成 `uint64`。 +因为 `Uint64()` 函数返回值取值范围**最广(widest)**,需要 64 位的随机数,因此我们首先实现它。我们使用 `encoding/binary` 包从 `crypto/rand` 包的 `io.Reader` 接口中读取 8 个字节的数据,并直接转换成 `uint64`。 ```go func (s *mySrc) Uint64() (value uint64) { @@ -141,7 +141,7 @@ func BenchmarkCryptoRead(b *testing.B) { BenchmarkCryptoRead-4 2000000 735 ns/op ``` -我不知道如何做才能进一步提高性能。而且,或许对于你的使用场景来说,花费大约1毫秒来获取非特定随机数不是一个问题。这个需要你自己去评估了。 +我不知道如何做才能进一步提高性能。而且,或许对于你的使用场景来说,花费大约 1 毫秒来获取非特定随机数不是一个问题。这个需要你自己去评估了。 ## 另外一种思路? diff --git a/published/tech/20171209-How-to-Create-a-RESTful-API-With-Only-The-Golang-Standard-Library.md b/published/tech/20171209-How-to-Create-a-RESTful-API-With-Only-The-Golang-Standard-Library.md index d52eb278a..780f75464 100644 --- a/published/tech/20171209-How-to-Create-a-RESTful-API-With-Only-The-Golang-Standard-Library.md +++ b/published/tech/20171209-How-to-Create-a-RESTful-API-With-Only-The-Golang-Standard-Library.md @@ -102,7 +102,7 @@ func (r *Redis) Set(key, value string, expiration time.Duration) error { } ``` -最后,一个函数返回了 redis 客户端。 +最后,一个函数返回了 Redis 客户端。 ```go func Connect(addr, password string, db int) *redis.Client { @@ -208,13 +208,13 @@ func (jc *UserController) Register(w http.ResponseWriter, r *http.Request) { oneMonth := time.Duration(60*60*24*30) * time.Second err = jc.Cache.Set(fmt.Sprintf("token_%s", token), strconv.Itoa(id), oneMonth) if err != nil { - log.Fatalf("Add token to redis Error: %s", err) + log.Fatalf("Add token to Redis Error: %s", err) http.Error(w, "", http.StatusInternalServerError) return } ``` -最后一步,将令牌返回给用户,并设置内容类型为 json 格式。 +最后一步,将令牌返回给用户,并设置内容类型为 JSON 格式。 ```go p := map[string]string{ @@ -266,7 +266,7 @@ func CreateUser(db *sql.DB, email, name, password string) (int, error) { ## 结论 -这个控制器只是我创建的 API 的一小段代码,用来展示仅用 Go 语言标准库来创建 API 是多么简单的一件事情。Go 语言是一门伟大的语言,可以用来创建 API 和微服务,并且执行效率胜过了如 javascript 和 PHP 之类的其他语言。所以,使用 Go 语言是一件非常简单的事情。虽然只使用标准库来实现了这些,但是我相信使用一些外部的库,如 [Gorilla Mux](https://github.com/gorilla/mux) 和 [go-validator](https://github.com/go-validator/validator) ,会使得开发更加简便,而且使得代码更加清晰和可维护。 +这个控制器只是我创建的 API 的一小段代码,用来展示仅用 Go 语言标准库来创建 API 是多么简单的一件事情。Go 语言是一门伟大的语言,可以用来创建 API 和微服务,并且执行效率胜过了如 JavaScript 和 PHP 之类的其他语言。所以,使用 Go 语言是一件非常简单的事情。虽然只使用标准库来实现了这些,但是我相信使用一些外部的库,如 [Gorilla Mux](https://github.com/gorilla/mux) 和 [go-validator](https://github.com/go-validator/validator) ,会使得开发更加简便,而且使得代码更加清晰和可维护。 --- diff --git a/published/tech/20171212-How-to-chain-HTTP-Handlers-in-Go.md b/published/tech/20171212-How-to-chain-HTTP-Handlers-in-Go.md index b3e4d6d70..6799b1807 100644 --- a/published/tech/20171212-How-to-chain-HTTP-Handlers-in-Go.md +++ b/published/tech/20171212-How-to-chain-HTTP-Handlers-in-Go.md @@ -4,9 +4,9 @@ 你好,今天我想分享一下,在 `Go` 语言中串联 HTTP 处理器。 -在使用 Go 之前, 我使用 Nodejs + [ExpressJS](http://expressjs.com/en/4x/api.html) 去编写 HTTP 服务器应用。 这个框架提供了很简单的方法去使用中间件和串联很多路由节点,因此,不必指定完整的路由路径来为其添加处理程序。 +在使用  Go 之前, 我使用 Nodejs + [ExpressJS](http://expressjs.com/en/4x/api.html) 去编写 HTTP 服务器应用。 这个框架提供了很简单的方法去使用中间件和串联很多路由节点,因此,不必指定完整的路由路径来为其添加处理程序。 -![图1](https://raw.githubusercontent.com/studygolang/gctt-images/master/chain-http-hanlders/1.png) +![图 1](https://raw.githubusercontent.com/studygolang/gctt-images/master/chain-http-hanlders/1.png) 这个想法是通过分割你的路由和处理每一个部分,串联到处理器,每个处理程序只负责一部分。它理解起来非常简单且非常容易使用和维护,所以首先我尝试在 Go 中做一些类似的事情。 @@ -40,11 +40,11 @@ mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { }) ``` -我们可以看到,在这个例子中为 `/api/` 路由自定义了一个处理器并且定义了一个处理方法给根路由。因此任何以 `/api/*` 开头的路由都将使用 apiHandler 处理器方法。 但是如果我们需要串联一个 usersHandler 到 apiHandler,不通过任何的头脑风暴和编码,我们无法做到这点。 +我们可以看到,在这个例子中  为 `/api/` 路由自定义了一个处理器并且定义了一个处理方法给根路由。因此任何以 `/api/*` 开头的路由都将使用 apiHandler 处理器方法。 但是如果我们需要串联一个 usersHandler 到 apiHandler,不通过任何的头脑风暴和编码,我们无法做到这点。 为此我写了一个小库 - [gosplitter](https://github.com/goncharovnikita/gosplitter),它只提供一个公共方法 `Match(url string, mux *http.ServeMux, http.Handler|http.HandlerFunc|interface{})` - 他匹配给定的路由部分和处理器、处理方法或你给定的任何结构! -让我们来看一个例子: +让我们来看一个  例子: ```go /** @@ -73,7 +73,7 @@ func (c *ColorsHandler) Start() { gosplitter.Match("/black", c.mux, c.HandleBlack()) } /** - * 简单的HTTP处理器方法 + * 简单的 HTTP 处理器方法 */ func (a *APIV1Handler) HandlePing() func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { @@ -94,12 +94,12 @@ func main() { } /** - * 绑定api处理器到根目录 + * 绑定 api 处理器到根目录 */ gosplitter.Match("/api/v1", mux, apiV1) /** - * 开始api的处理 + * 开始 api 的处理 */ apiV1.Start() } @@ -109,7 +109,7 @@ func main() { ```go /** - * 定义处理器类型 + * 定义  处理器类型 */ type APIV1Handler struct { mux *http.ServeMux @@ -124,7 +124,7 @@ type ColorsHandler struct { ```go /** - * Start - 绑定api处理器到根目录 + * Start - 绑定 api 处理器到根目录 */ func (a *APIV1Handler) Start() { var colorsHandler = ColorsHandler{ @@ -143,7 +143,7 @@ func (c *ColorsHandler) Start() { ```go /** - * 简单的HTTP处理器方法 + * 简单的 HTTP 处理器方法 */ func (a *APIV1Handler) HandlePing() func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { @@ -158,7 +158,7 @@ func (c *ColorsHandler) HandleBlack() func(w http.ResponseWriter, r *http.Reques } ``` -添加 `HandlePing` 和 `HandleBlack` 到我们的 `APIV1Handler`,它响应了 `pong` 和 `#000000` +添加 `HandlePing` 和 `HandleBlack` 到我们的 `APIV1Handler`, 它响应了 `pong` 和 `#000000` ```go func main() { @@ -168,11 +168,11 @@ func main() { } /** - * 绑定API处理器到根路由 + * 绑定 API 处理器到根路由 */ gosplitter.Match("/api/v1", mux, apiV1) /** - * 启动API处理器 + * 启动 API 处理器 */ apiV1.Start() } @@ -182,7 +182,7 @@ func main() { 所以在所有这些简单的操作之后我们拥有了两个工作中的路由: `/api/v1/ping` 和 `/api/v1/colors/black`,会响应 `pong` 和 `#000000`。 -使用起来不是很容易么?我认为是这样, 现在在我的项目中使用这个库来方便的进行路由分割和串联处理器 :) +使用起来不是很容易么?我认为是这样,  现在在我的项目中使用这个库来方便的进行路由分割和  串联处理器 :) diff --git a/published/tech/20171215-unsafe-Pointer-and-system-calls.md b/published/tech/20171215-unsafe-Pointer-and-system-calls.md index cdb98cea1..31b9b873f 100644 --- a/published/tech/20171215-unsafe-Pointer-and-system-calls.md +++ b/published/tech/20171215-unsafe-Pointer-and-system-calls.md @@ -2,18 +2,18 @@ # unsafe.Pointer 和系统调用 -按照 Go 语言官方文档所说, unsafe 是关注 Go 程序操作类型安全的包。 + 按照 Go 语言官方文档所说, unsafe 是关注 Go 程序操作类型安全的包。 -像包名暗示的一样,使用它要格外小心; unsafe 可以特别危险,但它也可以特别有效。例如,当处理系统调用时,Go 的结构体必须和 C 的结构体拥有相同的内存结构,这时你可能除了使用 unsafe 以外,别无选择。 + 像包名暗示的一样,使用它要格外小心; unsafe 可以特别危险,但它也可以特别有效。例如,当处理系统调用时,Go 的结构体必须和 C 的结构体拥有相同的内存结构,这时你可能除了使用 unsafe 以外,别无  选择。 -unsafe.Pointer 可以让你无视 Go 的类型系统,完成任何类型与内建的 uintptr 类型之间的转化。根据文档,unsafe.Pointer 可以实现四种其他类型不能的操作: +unsafe.Pointer 可以让你无视 Go 的类型系统,完成  任何类型与内建的 uintptr 类型  之间的转化。根据文档,unsafe.Pointer 可以  实现四种其他类型不能的操作: -* 任何类型的指针都可以转化为一个 unsafe.Pointer +* 任何类型的指针都可以转化为一个  unsafe.Pointer * 一个 unsafe.Pointer 可以转化成任何类型的指针 * 一个 uintptr 可以转化成一个 unsafe.Pointer * 一个 unsafe.Pointer 可以转化成一个 uintptr -这里主要关注两种只能借助 unsafe 包才能完成的操作:使用 unsafe.Pointer 实现两种类型间转换和使用 unsafe.Pointer 处理系统调用。 +这里主要  关注两种  只能借助 unsafe 包才能完成的操作:使用 unsafe.Pointer 实现两种类型间转换和使用 unsafe.Pointer  处理系统调用。 ## 使用 unsafe.Pointer 做类型转换 @@ -23,7 +23,7 @@ 文档描述: -> 如果T2与T1一样大,并且两者有相同的内存结构;那么就允许把一个类型的数据,重新定义成另一个类型的数据 +> 如果 T2 与 T1 一样大,并且两者有相同的内存结构;那么就允许把一个类型的数据,重新定义成另一个类型的数据 经典的例子,是文档中的一次使用,用来实现 math.Float64bits: @@ -40,38 +40,38 @@ func Float64bits(f float64) uint64 { * (*uint64)(unsafe.Pointer(&f)) 将 unsafe.Pointer 类型转化成了 *uint64。 * *(*uint64)(unsafe.Pointer(&f)) 引用这个 *uint64 类型指针,转化为一个 uint64 类型的值。 -第一个例子是下面过程的一个简洁表达: +第一个例子  是下面过程的一个简洁表达: ```go func Float64bits(floatVal float64) uint64 { - // 获取一个指向存储这个float64类型值的指针。 + // 获取一个指向存储这个 float64 类型值的指针。 floatPtr := &floatVal - // 转化*float64类型到unsafe.Pointer类型。 + // 转化*float64 类型到 unsafe.Pointer 类型。 unsafePtr := unsafe.Pointer(floatPtr) - // 转化unsafe.Pointer类型到*uint64类型. + // 转化 unsafe.Pointer 类型到*uint64 类型. uintPtr := (*uint64)(unsafePtr) - // 解引用成一个uint64值 + //  解引用成一个 uint64 值 uintVal := *uintPtr return uintVal } ``` 这是一个非常有用的操作,有些时候也是一个必要操作。 -现在你已经理解了 unsafe.Pointer 是如何使用的,那么让我们再看一个真实的项目例子 +现在你已经理解了 unsafe.Pointer  是如何使用的,那么让我们  再看一个真实的项目例子 ### 现实列子:taskstats 我最近正在研究 [Linux 的 taskstats 接口](https://www.kernel.org/doc/Documentation/accounting/taskstats.txt),我想了一个办法在 Go 中取到了内核的 C 的 taskstats 结构。然后发送一个 CL 把这个结构加到 x/sys/unix 中,我意识到这个结构实际上是如此的[庞大和复杂](https://godoc.org/golang.org/x/sys/unix#Taskstats)。 -为了使用这个结构,我需要从一个 byte 类型的切片中精确的分析每一个字段。更复杂的是,每一个 integer 类型在本地有序的存储,所以这些整数可能根据你的cpu在内存中以不同的格式存储。 +为了使用这个结构,我需要  从一个 byte 类型的切片中  精确的分析每一个字段。 更复杂的是,每一个 integer 类型在本地有序的存储,所以这些整数可能根据你的 cpu 在内存中以不同的格式存储。 -这个情况就非常适合使用简洁的 unsafe.Pointer 转换,下面是我的写法: +这个情况就非常适合  使用简洁的 unsafe.Pointer 转换,下面是我的写法: ```go -// 通过这个包证实包含一个 unix.Taskstats 结构的byte类型的切片是预计的大小,我们不能盲目的将这个byte类型的切片放入一个错误尺寸的结构中。 +//  通过这个包证实包含一个 unix.Taskstats 结构的 byte 类型的切片是预计的大小,我们不能盲目  的将这个 byte 类型的切片放入一个错误  尺寸的结构中。 const sizeofTaskstats = int(unsafe.Sizeof(unix.Taskstats{})) if want, got := sizeofTaskstats, len(buf); want != got { @@ -84,15 +84,15 @@ stats := *(*unix.Taskstats)(unsafe.Pointer(&buf[0])) 首先,我通过参数传来的结构体实例,使用 unsafe.Sizeof,确定了该结构在内存中占有的准确的大小。 -接下来,我确认需要转换的byte类型的切片大小和 unix.Taskstats 结构大小一样,这样我就可以只读取我想要的数据块,而不是随意读取内存。 +接下来,我确认需要转换的 byte 类型的切片大小和 unix.Taskstats 结构大小一样,这样我就可以只读取我想要的数据块,而不是随意读取内存。 最后,我使用 unsafe.Pointer 向 unix.Taskstats 结构转换。 -但是,我为什么必须指定切片索引的0位置呢? +但是,我为什么必须指定切片索引的 0 位置呢? -如果你了解[切片的内部结构](https://blog.golang.org/go-slices-usage-and-internals),你将知道一个切片实际上是一个头和一个指向底层数组的指针。当使用 unsafe.Pointer 来转换切片数据时,必须指定数组第一个元素的内存地址,而不是切片本身的首地址。 +如果你了解[切片的内部结构](https://blog.golang.org/go-slices-usage-and-internals),你将知道一个切片实际上是一个头和一个指向底层数组的指针。当使用 unsafe.Pointer 来转换切片数据时,必须指定数组  第一个元素的内存地址,而不是切片本身的首地址。 -使用 unsafe 使得转换非常的简洁、简单。因为整型数据根据我们的CPU以相同的字节顺序存储,使用 unsafe.Pointer 转化意味着整型值是我们预期的。 +使用 unsafe 使得转换非常的简洁、简单。因为整型数据根据我们的 CPU 以相同的字节顺序  存储,使用 unsafe.Pointer 转化意味着整型值是我们预期的。 你可以去看看我 [taskstats](https://github.com/mdlayher/taskstats) 包中的代码。 @@ -100,18 +100,18 @@ stats := *(*unix.Taskstats)(unsafe.Pointer(&buf[0])) ### 操作方式 -当处理系统调用时,有些时候需要传入一个指向某块内存的指针给内核,以允许它执行某些任务。这是 unsafe.Pointer 在Go中另一个重要的使用场景。当需要处理系统调用时,就必须使用 unsafe.Pointer ,因为为了使用 syscall.Syscall 家族函数,它可以被转化成 uintptr 类型。 +当处理系统调用时,有些时候需要传入一个  指向某块内存的指针给内核,以允许它执行某些任务。这是 unsafe.Pointer  在 Go 中另一个重要的使用场景。当需要处理系统调用时,就必须使用 unsafe.Pointer ,因为为了使用 syscall.Syscall 家族函数,它可以被转化成 uintptr 类型。 -对于许多不同的操作系统,都拥有大量的系统调用。但是在这个例子中,我们将重点关注 ioctl 。ioctl,在UNIX类系统中,经常被用来操作那些无法直接映射到典型的文件系统操作,例如读和写的文件描述符。事实上,由于 ioctl 系统调用十分灵活,它并不在Go的 syscall 或者 x/sys/unix 包中。 +对于许多不同的操作系统,都拥有大量的系统调用。但是在这个例子中 ,我们将重点关注 ioctl 。ioctl,在 UNIX 类系统中,经常被用来操作那些无法直接映射到典型的文件系统操作,例如读和写的文件描述符。事实上,由于 ioctl 系统调用十分灵活,它并不在 Go 的 syscall 或者 x/sys/unix 包中。 让我看看另一个真实的例子。 ### 现实例子:ioctl/vsock -在过去的几年里,Linux增加了一个新的 socket 家族,AF_VSOCK,它可以使管理中心和它的虚拟机之间双向,多对一的通信。 -这些套接字使用一个上下文ID进行通信。通过发送一个带有特殊请求号的 ioctl 到 /dev/vsock 驱动,可以取到这个上下文ID。 +在过去的几年里,Linux 增加了一个新的 socket 家族,AF_VSOCK,它可以使管理中心和它的虚拟机之间  双向,多对一的通信。 + 这些套接字使用一个上下文 ID 进行通信。通过发送一个带有  特殊请求号的 ioctl 到 /dev/vsock 驱动,可以  取到这个上下文 ID。 -下面是 ioctl 函数的定义: +下面是 ioctl  函数的定义: ```go func Ioctl(fd uintptr, request int, argp unsafe.Pointer) error { @@ -119,7 +119,7 @@ func Ioctl(fd uintptr, request int, argp unsafe.Pointer) error { unix.SYS_IOCTL, fd, uintptr(request), - // 在这个调用表达式中,从 unsafe.Pointer 到 uintptr 的转换是必须做的。详情可以查看 unsafe 包的文档 + // 在这个调用表达式中,从 unsafe.Pointer 到 uintptr 的转换是必须做的。详情  可以查看 unsafe 包的文档 uintptr(argp), ) if errno != 0 { @@ -130,18 +130,18 @@ func Ioctl(fd uintptr, request int, argp unsafe.Pointer) error { } ``` -像代码注释所写一样,在这种场景下使用 unsafe.Pointer 有一个很重要的说明: +像  代码注释所写一样,在这种场景下使用 unsafe.Pointer 有一个很重要的说明: -> 在 syscall 包中的系统调用函数通过它们的 uintptr 类型参数直接操作系统,然后根据调用的详细情况,将它们中的一些转化为指针。换句话说,系统调用的执行,是其中某些参数从 uintptr 类型到指针类型的隐式转换。 -> 如果一个指针参数必须转换成 uintptr 才能使用,那么这种转换必须出现在表达式内部。 +> 在 syscall 包中的系统调用函数通过它们的 uintptr 类型  参数直接操作系统,然后根据调用的详细情况,将它们中的一些转化为指针。换句话说,系统调用的执行,是其中某些参数从 uintptr 类型到指针类型的  隐式  转换。 +> 如果一个指针参数必须转换成 uintptr 才能使用,那么这种转换必须出现在表达式  内部。 -但是为什么会这样?这是编译器识别的特殊模式,本质上是指示垃圾收集器在函数调用完成之前,不能将被指针引用的内存再次安排。 +但是为什么会这样? 这是编译器识别的特殊模式,本质上是指示垃圾收集器在函数调用完成之前,不能将被指针  引用的内存再次安排。 -你可以通过阅读文档来获得更多的技术细节,但是你在Go中处理系统调用时必须记住这个规则。事实上,在写这篇文章时,我意识到我的代码违反了这一规则,现在已经被修复了。  +你可以通过阅读文档来获得更多的技术细节,但是你在 Go 中处理系统调用时必须记住这个规则。事实上,在写这篇文章时,我意识到我的代码违反了这一规则,现在已经被修复了。  -意识到这一点,我们可以看到这个函数是如何使用的。 + 意识到这一点,我们可以看到这个函数是如何使用的。 -在 VM 套接字的例子里,我们想传递一个 *uint32 到内核,以便它可以把我们当时的上下文ID赋值到这块内存地址中。 +在 VM 套接字的例子里,我们想传递一个 *uint32 到内核,以便它可以把我们当时的上下文 ID 赋值到这块内存地址中。 ```go f, err := fs.Open("/dev/vsock") @@ -150,38 +150,38 @@ if err != nil { } defer f.Close() -// 存储上下文ID +// 存储上下文 ID var cid uint32 -// 从这台机器的 /dev/vsock 中获取上下文ID +//  从这台机器的 /dev/vsock 中获取上下文 ID err = Ioctl(f.Fd(), unix.IOCTL_VM_SOCKETS_GET_LOCAL_CID, unsafe.Pointer(&cid)) if err != nil { return 0, err } -// 返回当前的上下文ID给调用者 +// 返回当前的上下文 ID 给调用者 return cid, nil ``` -这只是在系统调用时使用 unsafe.Pointer 的一个例子。你可以使用这么模式发送、接收任何数据,或者是用一些特殊方式配置一个内核接口。有很多可能的情况! +这只是在系统调用时使用 unsafe.Pointer 的一个例子。你可以使用这么模式发送 、接收任何数据,或者是用  一些特殊方式配置一个内核接口。有很多可能的  情况! 你可以去看看我 [vsock](https://github.com/mdlayher/vsock) 包中的代码。 ## 结尾 -虽然使用 unsafe 包可能存在风险,但当使用恰当时,它可以是一个非常强大、有用的工具。 +虽然使用 unsafe 包可能存在风险,但  当使用恰当时,它可以是一个非常强大、有用的工具。 -既然你在读这篇文章,我建议你在你的程序使用它之前,去[读一下 unsafe 包的官方文档](https://golang.org/pkg/unsafe/)。 +既然你在读这篇文章,我建议你在你的程序使用它之前,去[读一下 unsafe 包的  官方文档](https://golang.org/pkg/unsafe/)。 如果你有任何问题,请随时联系我!在 [Gophers Slack](https://gophers.slack.com/), [GitHub](https://github.com/mdlayher) and [Twitter](https://twitter.com/mdlayher)上我的名字是 mdlayher。 -非常感谢 [Hazel Virdó](https://twitter.com/HazelVirdo) 对这篇文章的建议和修改! +非常感谢 [Hazel Vird ó](https://twitter.com/HazelVirdo) 对这篇文章的建议和修改! ## 链接 * [unsafe 包](https://golang.org/pkg/unsafe/) * [字节顺序](https://en.wikipedia.org/wiki/Endianness) * [taskstats 包](https://github.com/mdlayher/taskstats) -* [Go中Slice的使用和内部实现](https://blog.golang.org/go-slices-usage-and-internals) +* [Go 中 Slice 的使用和内部实现](https://blog.golang.org/go-slices-usage-and-internals) * [ioctl](https://en.wikipedia.org/wiki/Ioctl) * [vsock 包](https://github.com/mdlayher/vsock) diff --git a/published/tech/20171221-Introduction-to-Reflection.md b/published/tech/20171221-Introduction-to-Reflection.md index 06dac9701..03e22f2ad 100644 --- a/published/tech/20171221-Introduction-to-Reflection.md +++ b/published/tech/20171221-Introduction-to-Reflection.md @@ -4,7 +4,7 @@ 反射是指一门编程语言可以在运行时( runtime )检查其数据结构的能力。利用 Go 语言的反射机制,可以获取结构体的公有字段以及私有字段的标签名,甚至一些其他比较敏感的信息。 -众所周知Go标准库中有一些包利用反射机制来实现它们的功能。我们经常会以 [encoding/json](https://golang.org/pkg/encoding/json/) 包为例,该包常用来把 JSON 文档解析为结构体,同时也可以把结构体编码为JSON格式的字符串。 +众所周知 Go 标准库中有一些包利用反射机制来实现它们的功能。我们经常会以 [encoding/json](https://golang.org/pkg/encoding/json/) 包为例,该包常用来把 JSON 文档解析为结构体,同时也可以把结构体编码为 JSON 格式的字符串。 本文中我想给大家介绍一个略微有点不一样的例子,该例子是我最近在做的一个聊天项目的消息体,该消息体使用结构体来表示: @@ -130,4 +130,4 @@ via:https://scene-si.org/2017/12/21/introduction-to-reflection/ 译者:[yzhfd](https://github.com/yzhfd) 校对:[polaris1119](https://github.com/polaris1119) -本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go中文网](https://studygolang.com/) 荣誉推出 +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20171225-5-advanced-testing-techniques.md b/published/tech/20171225-5-advanced-testing-techniques.md index 20dd665ca..f022ed697 100644 --- a/published/tech/20171225-5-advanced-testing-techniques.md +++ b/published/tech/20171225-5-advanced-testing-techniques.md @@ -66,7 +66,7 @@ Go 提供了非常易于使用的并发原语,这也导致了它被过度的 * 什么时候数据发送完成。 * 在接受数据的时候是否会发生错误。 -* 如果需要在包中清理使用过的channel的时候该怎么做。 +* 如果需要在包中清理使用过的 channel 的时候该怎么做。 * 如何将 API 封装成一个接口,使我们不用直接去调用它。 请看下面这个在队列中读取数据的例子。这个库导出了一个 `channel` 用来让用户读取他的数据。 @@ -120,7 +120,7 @@ func TestConsumer(t testing.T, q queueIface) { } ``` -现在我们不知道如何来向这个 `channel` 中插入数据,来模拟使用时这个代码库的真实运行情况,如果这个库提供了一个同步的API接口,那么我们可以并发的调用它,这样测试就会非常的简单。 +现在我们不知道如何来向这个 `channel` 中插入数据,来模拟使用时这个代码库的真实运行情况,如果这个库提供了一个同步的 API 接口,那么我们可以并发的调用它,这样测试就会非常的简单。 ```go func TestConsumer(t testing.T, q queueIface) { @@ -132,7 +132,7 @@ func TestConsumer(t testing.T, q queueIface) { } ``` -当你有疑问的时候,一定要记住在用户的包中使用 goroutine 是很简单的事情,但是你的包一旦导出了就很难被移除。所以一定别忘了在 package 的文档中注明这个包是不是多 goroutine 并发安全的。 +当你有疑问的时候,一定要记住在用户的包中使用 Goroutine 是很简单的事情,但是你的包一旦导出了就很难被移除。所以一定别忘了在 package 的文档中注明这个包是不是多 Goroutine 并发安全的。 有时,我们不可避免的需要导出一个 `channel`。为了减少这样带来的问题,你可以通过导出只读的 `channel(<-chan)` 或者只写的 `channel(chan<-)` 来替代直接导出一个 `channel`。 @@ -154,7 +154,7 @@ func TestServe(t *testing.T) { t.Fatal(err) } defer l.Close() - go s.Serve(l) + Go s.Serve(l) res, err := http.Get("http://" + l.Addr().String() + "/?sloths=arecool") if err != nil { @@ -188,11 +188,11 @@ func TestServeMemory(t *testing.T) { ##  使用单独的 `_test` 测试包 -大多数情况下,测试都是写在和 package 相同的包中并以 `pkg_test.go` 命名。而一个单独的测试包就是将测试代码和正式代码分割在不同的包中。一般单独的测试包都以包名+`_test` 命名(例如:`foo` 包的测试包为 `foo_test`)。在测试包中你可以把需要测试的包和其他测试依赖的包一起导入进去。这种方式能让测试更加的灵活。当遇到包中循环引用的情况,我们推荐这种变通的方式。他能防止你对代码中易变部分进行测试。并且能让开发者站在包的使用者的角度上来使用自己开发的包。如果你开发的包很难被使用,那么他也肯定很难被测试。 +大多数情况下,测试都是写在和 package 相同的包中并以 `pkg_test.go` 命名。而一个单独的测试包就是将测试代码和正式代码分割在不同的包中。一般单独的测试包都以包名 +`_test` 命名(例如:`foo` 包的测试包为 `foo_test`)。在测试包中你可以把需要测试的包和其他测试依赖的包一起导入进去。这种方式能让测试更加的灵活。当遇到包中循环引用的情况,我们推荐这种变通的方式。他能防止你对代码中易变部分进行测试。并且能让开发者站在包的使用者的角度上来使用自己开发的包。如果你开发的包很难被使用,那么他也肯定很难被测试。 这种测试方法通过限制易变的私有变量来避免容易发生改变的测试。如果你的代码不能通过这种测试,那么在使用的过程中肯定也会有问题。 -这种测试方法也有助于避免循环的引用。大多数包都会依赖你在测试中所要用到的包,所以很容易发生循环依赖的情况。而这种单独的测试包在原包,和被依赖包的层次之外,就不会出现循环依赖的问题。一个例子就是 `net/url` 包中实现了一个URL的解析器,这个解析器被 `net/http` 包所使用。但是当对 `net/url` 包进行测试的时候,就需要导入 `net/http` 包,因此 `net/url_test` 包产生了。 +这种测试方法也有助于避免循环的引用。大多数包都会依赖你在测试中所要用到的包,所以很容易发生循环依赖的情况。而这种单独的测试包在原包,和被依赖包的层次之外,就不会出现循环依赖的问题。一个例子就是 `net/url` 包中实现了一个 URL 的解析器,这个解析器被 `net/http` 包所使用。但是当对 `net/url` 包进行测试的时候,就需要导入 `net/http` 包,因此 `net/url_test` 包产生了。 现在当你使用一个单独的测试包的时候,包中的一些结构体或者函数由于包的可见性的原因在单独的测试包中不能被访问到。大部分人在基于时间的测试的时候都会遇到这种问题。针对这种问题,我们可以在包中 `xx_test.go` 文件中将他们导出,这样我们就可以正常的使用了。 diff --git a/published/tech/20171226-Writing-a-JIT-compiler-in-Golang.md b/published/tech/20171226-Writing-a-JIT-compiler-in-Golang.md index 7e0680ba4..5cf5f04fc 100644 --- a/published/tech/20171226-Writing-a-JIT-compiler-in-Golang.md +++ b/published/tech/20171226-Writing-a-JIT-compiler-in-Golang.md @@ -98,7 +98,7 @@ printFunction := []uint16{ 因此,我使用填充指令 cc 指令(无操作)将数据部分的开始推送到 slice 中的下一个条目。我还更新了 lea 指向 4 个字节的位置以反映这一变化。 -注意:您可以在此`[链接](https://filippo.io/linux-syscall-table/)找到各种系统调用的系统调用号码。 +注意:您可以在此 `[链接](https://filippo.io/linux-syscall-table/)找到各种系统调用的系统调用号码。 ## 转换切片函数 @@ -147,7 +147,7 @@ for i := range printFunction { 标志 syscall.PROT_EXEC 确保新分配的内存地址是可执行的。将此数据结构转换为函数将使其运行平稳。 -以下是完整的代码,尝试在x64机器上运行。 +以下是完整的代码,尝试在 x64 机器上运行。 ```go package main diff --git a/published/tech/20171230-reading-files-in-go-an-overview.md b/published/tech/20171230-reading-files-in-go-an-overview.md index 6c747a510..38fe00f59 100644 --- a/published/tech/20171230-reading-files-in-go-an-overview.md +++ b/published/tech/20171230-reading-files-in-go-an-overview.md @@ -12,7 +12,7 @@ * 大多数情况下,我会经常会交替使用“数组 `array`”和“切片 `slice`”来指代切片,但它们是不一样的。这些[博客](https://blog.golang.org/go-slices-usage-and-internals)[文章](https://blog.golang.org/slices)是了解差异的两个很好的资源。 * 我把所有的实例上传到[kgrz/reading-files-in-go](https://github.com/kgrz/reading-files-in-go)。 -在 go 中像大多数低级语言和一些动态语言(例如Node)中一样,读取文件时返回一个字节流。不自动将读取内容转换为字符串有一个好处,可以避免因为昂贵的字符串分配给 GC 带来的压力。 +在 Go 中像大多数低级语言和一些动态语言(例如 Node)中一样,读取文件时返回一个字节流。不自动将读取内容转换为字符串有一个好处,可以避免因为昂贵的字符串分配给 GC 带来的压力。 为了让这篇文章有一个简单的概念模型,我会使用 `string(arrayOfBytes)` 将字节数组转换成字符串。不过一般来说不建议在生产环境使用这种方式。 @@ -60,7 +60,7 @@ fmt.Println("bytestream to string: ", string(buffer)) 在大多数情况下,一次性读取整个一个文件是没有问题的。有时我们希望使用更节省内存的方法。比如说,按照一定大小来读取一个文件块,并处理这个文件块,然后重复直到读取完整个文件。 -下面的示例使用100字节大小的缓冲区。 +下面的示例使用 100 字节大小的缓冲区。 ```go const BufferSize = 100 @@ -133,7 +133,7 @@ filesize := int(fileinfo.Size()) // Number of goroutines we need to spawn. concurrency := filesize / BufferSize -// check for any left over bytes. Add one more goroutine if required. +// check for any left over bytes. Add one more Goroutine if required. if remainder := filesize % BufferSize; remainder != 0 { concurrency++ } @@ -142,7 +142,7 @@ var wg sync.WaitGroup wg.Add(concurrency) for i := 0; i < concurrency; i++ { - go func(chunksizes []chunk, i int) { + Go func(chunksizes []chunk, i int) { defer wg.Done() chunk := chunksizes[i] @@ -171,7 +171,7 @@ wg.Wait() 这比以前任何方法都要复杂: -1. 我尝试创建一个特定的 `goroutine`,这取决于文件大小和缓冲区大小(在我们的例子中是100)。 +1. 我尝试创建一个特定的 `goroutine`,这取决于文件大小和缓冲区大小(在我们的例子中是 100)。 2. 我们需要一种方法来确保我们“等待”所有的 `goroutine` 运行完成。在这个例子中,我是使用 `WaitGroup`。 3. 我们在 `goroutine` 运行完成时发送一个结束信号,而不是使用无限循环等待运行结束。我们使用 `defer` 调用 `wg.Done()`,当 `goroutine` 运行到 `return` 时,`wg.Done` 会被调用。 @@ -262,7 +262,7 @@ for scanner.Scan() { } fmt.Println("word list:") -for _, word := range words { +for _, Word := range words { fmt.Println(word) } ``` @@ -310,7 +310,7 @@ fmt.Println("word list:") // a constant value. Or the scanner loop might've terminated due to an // error prematurely. In this case the "pos" contains the index of the last // successful update. -for _, word := range words[:pos] { +for _, Word := range words[:pos] { fmt.Println(word) } ``` @@ -337,7 +337,7 @@ for scanner.Scan() { } fmt.Println("word list:") -for _, word := range words { +for _, Word := range words { fmt.Println(word) } ``` @@ -395,7 +395,7 @@ for scanner.Scan() { } ``` -## Ruby风格 +## Ruby 风格 我们已经按照方便性和效率的顺序看到了多种方法来读取文件。但是,如果你只想把文件读入缓冲区呢? `ioutil` 是标准库中的一个包,其中的函数能够使用一行代码完成一些功能。 @@ -464,7 +464,7 @@ handle := handleFn(file) handle(err) ``` -这样做,我错过了一个关键的细节:当没有发生错误并且程序运行完成时,我没有关闭文件句柄。如果程序运行多次而没有发生任何错误,则会导致文件描述符泄漏。这是由[u/shovelpost](https://www.reddit.com/r/golang/comments/7n2bee/various_ways_to_read_a_file_in_go/drzg32k/)在reddit上指出的。 +这样做,我错过了一个关键的细节:当没有发生错误并且程序运行完成时,我没有关闭文件句柄。如果程序运行多次而没有发生任何错误,则会导致文件描述符泄漏。这是由[u/shovelpost](https://www.reddit.com/r/golang/comments/7n2bee/various_ways_to_read_a_file_in_go/drzg32k/)在 reddit 上指出的。 我本意是避免使用 `defer`,因为 `log.Fatal` 在内部调用了不运行延迟函数的 `os.Exit`,所以我选择显式关闭文件,但忽略了成功运行的情况。 diff --git a/published/tech/20171230-reading-files-in-go.md b/published/tech/20171230-reading-files-in-go.md index 619ccb98e..42f11c1df 100644 --- a/published/tech/20171230-reading-files-in-go.md +++ b/published/tech/20171230-reading-files-in-go.md @@ -8,16 +8,16 @@ --- -当我开始学习 Go 的时候,我很难熟练得运用各种操作文件的 API。在我尝试写一个多核心的计数器([kgrz/kwc](https://github.com/kgrz/kwc))时让我感到了困惑 - 操作同一个文件的不同方法。 + 当我开始学习 Go 的时候,我很难熟练得  运用各种操作文件的 API。在我尝试写一个多核心的计数器([kgrz/kwc](https://github.com/kgrz/kwc))时让我感到了困惑 - 操作同一个文件的不同方法。 -在今年的 [Advent of Code](http://adventofcode.com/2017/) 中遇到了一些需要多种读取输入源的方式的问题。最终我把每种方法都至少使用了一次,因此现在我对这些技术有了一个清晰的认识。我会在这篇文章中将这些记录下来。我会按照我遇到这些技术的顺序列出来,而不是按照从易到难的顺序。 +在今年的 [Advent of Code](http://adventofcode.com/2017/) 中遇到了一些需要多种读取输入源的方式的问题。最终我把每种方法都至少使用了一次,因此现在我对这些  技术有了一个清晰的认识。我会在这篇文章中将这些记录下来。我会按照我遇到这些技术的顺序列出来,而不是按照从易到难的顺序。 * 按字节读取 * 将整个文件读入内存中 * 分批读取文件 - * 并行分批读取文件 + *  并行分批读取文件 * 扫描 - * 按单词扫描 + * 按单词  扫描 * 将一个长字符串分割成多个单词 * 扫描用逗号分割的字符串 * Ruby 风格 @@ -30,17 +30,17 @@ * 所有的代码都包裹在 `main()` 代码块内 * 大部分情况下我会使用 "array" 和 "slice" 来指代 slices,但它们的含义是不同的。[这](https://blog.golang.org/go-slices-usage-and-internals)[两](https://blog.golang.org/slices)篇文章很好得解释了两者的不同之处。 -* 我会把所有的示例代码上传到 [kgrz/reading-files-in-go](https://github.com/kgrz/reading-files-in-go)。 +* 我会把所有的示例代码  上传到 [kgrz/reading-files-in-go](https://github.com/kgrz/reading-files-in-go)。 -在 Go 中 - 对于这个问题,大部分的低级语言和一些类似于 Node 的动态语言 - 会返回字节流。之所以不自动返回字符串是因为可以避免昂贵的会增加垃圾回收器的压力的字符串分配操作。 +在 Go 中 - 对于  这个问题,大部分的低级语言和一些类似于 Node 的动态语言 - 会返回字节流。之所以不自动返回字符串是因为可以避免昂贵的会增加垃圾回收器的压力的字符串分配操作。 -为了让这篇文章更加通俗易懂,我会使用 `string(arrayOfBytes)` 来将 `字节` 数组转化为字符串,但不建议在生产模式中使用这种方式。 +为了让这篇文章更加通俗易懂,我会使用 `string(arrayOfBytes)` 来将 ` 字节 ` 数组转化为字符串,但不建议在生产模式中使用这种方式。 ## 按字节读取 *将整个文件读入内存中* -标准库里提供了众多的函数和工具来读取文件数据。我们先从 `os` 包中提供的基本例子入手。这意味着两个先决条件: +标准库里提供了众多的函数和工具来读取文件数据。我们先从 `os` 包中  提供的基本例子入手。这意味着两个  先决条件: 1. 该文件需要放入内存 2. 我们需要预先知道文件大小以便实例化一个足够装下该文件的缓冲区 @@ -109,8 +109,8 @@ for { 与读取整个文件的区别在于: -1. 当读取到 EOF 标记时就停止读取,因此我们增加了一个特殊的断言 `err == io.EOF`。如果你刚开始接触 Go,你可能会对 errors 的约定感到困惑,那么阅读 Rob Pike 的这篇文章可能会对你有所帮助:[Errors are values](https://blog.golang.org/errors-are-values) -2. 我们定义了缓冲区的大小,这样我们可以控制任意的缓冲区大小。由于操作系统的这种工作方式([caching a file that’s being read](http://www.tldp.org/LDP/sag/html/buffer-cache.html)),如果设置得当可以提高性能。 +1. 当读取到 EOF 标记时就  停止读取,因此我们增加了一个特殊的断言 `err == io.EOF`。如果你刚开始接触 Go,你可能会对 errors 的约定感到困惑, 那么阅读 Rob Pike 的这篇文章可能会对你有所帮助:[Errors are values](https://blog.golang.org/errors-are-values) +2. 我们定义了缓冲区的大小,这样我们可以控制任意的缓冲区大小。由于操作系统的这种工作方式([caching a file that’s being read](http://www.tldp.org/LDP/sag/html/buffer-cache.html)),如果  设置得当  可以提高性能。 3. 如果文件的大小不是缓冲区大小的整数倍,那么最后一次迭代只会读取剩余的字节到缓冲区中,因此我们会调用 `buffer[:bytesread]`。在正常情况下,`bytesread` 和缓冲区大小相同。 这种情况和以下的 Ruby 代码非常相似: @@ -126,13 +126,13 @@ while readstring = f.read(bufsize) end ``` -在循环中的每一次迭代,内部的文件指针都会被更新。当下一次读取开始时,数据将从文件指针的偏移量处开始,直到读取了缓冲区大小的内容。这个指针不是编程语言中的概念,而是操作系统中的概念。在 linux 中,这个指针是指创建的文件描述符的属性。所有的 read/Read 函数调用(在 Ruby/Go 中)都被内部转化为系统调用并发送给内核,然后由内核管理所有的这些指针。 +在循环中的每一次迭代,内部的文件指针都会被更新。当下一次读取开始时,数据  将从文件指针的偏移量处开始,直到读取了缓冲区大小的  内容。这个指针不是编程语言中的概念,而是操作系统中的概念。在 Linux 中,这个指针是指创建的文件描述符的属性。所有的 read/Read 函数调用(在 Ruby/Go 中)都被内部  转化为系统调用并发送给内核,然后由内核管理所有的这些指针。 ## 并行分批读取文件 -那怎么样才能加速分批读取文件呢?其中一种方法是用多个 go routine。相对于连续分批读取文件,我们需要知道每个 goroutine 的偏移量。值得注意的是,当剩余的数据小于缓冲区时,`ReadAt` 的表现和 `Read` 有[轻微的不同](https://golang.org/pkg/io/#ReaderAt)。 +那怎么样才能加速分批读取文件呢?其中一种方法是用多个 Go routine。相对于连续分批读取文件,我们需要知道每个 Goroutine 的偏移量。值得注意的是,当剩余的数据小于缓冲区时,`ReadAt` 的表现和 `Read` 有 [轻微的不同](https://golang.org/pkg/io/#ReaderAt)。 -另外,我在这里并没有设置 goroutine 数量的上限,而是由缓冲区的大小自行决定。但在实际的应用中通常都会设定 goroutine 的数量上限。 +另外,我在这里并没有设置 Goroutine 数量的上限,而是由缓冲区的大小自行决定。但在实际的  应用中通常都会设定 Goroutine 的数量上限。 ```go const BufferSize = 100 @@ -150,7 +150,7 @@ if err != nil { } filesize := int(fileinfo.Size()) -// 我们需要使用的 goroutine 数量 +// 我们需要使用的 Goroutine 数量 concurrency := filesize / BufferSize // 如果有多余的字节,增加一个额外的 goroutine @@ -188,13 +188,13 @@ wg.Wait() 这比之前的方法都需要考虑得更多: -1. 我尝试创建特定数量的 Go-routines, 这个数量取决于文件大小以及缓冲区大小(在我们的例子中是 100k)。 +1. 我尝试  创建特定数量的 Go-routines, 这个数量取决于文件大小以及缓冲区大小(在我们的例子中是 100k)。 2. 我们需要一种方法能确定等所有的 goroutines 都结束。在这个例子中,我们使用 wait group。 -3. 我们在每个 goroutine 结束时发送信号,而不是使用 `break` 从 for 循环中跳出。由于我们在 `defer` 中调用 `wg.Done()`,每次从 goroutine 中”返回“时都会调用该函数。 +3. 我们在每个 Goroutine 结束时发送信号,而不是使用 `break` 从 for 循环中跳出。由于我们在 `defer` 中调用 `wg.Done()`,每次从 Goroutine 中 ”返回“时都会调用该函数。 注意:每次都应该检查返回的字节数,并刷新(reslice)输出缓冲区。 -## 扫描 +##  扫描 你可以在各种场景下使用 `Read()` 方法来读取文件,但有时候你需要一些更加方便的方法。就像在 Ruby 中经常使用的类似于 `each_line`,`each_char`,`each_codepoint` 等 IO 函数。我们可以使用 `Scanner` 类型以及 `bufio` 包中的相关函数来达到类似的效果。 @@ -224,7 +224,7 @@ if read { ``` 在 Github 中查看源文件 [scanner-example.go](https://github.com/kgrz/reading-files-in-go/blob/master/scanner-example.go) -因此,若想按行读取整个文件,可以这么做: +因此,若想  按行读取整个文件,可以这么做: ```go file, err := os.Open("filetoread.txt") @@ -251,7 +251,7 @@ for _, line := range lines { ``` 在 Github 中查看源文件 [scanner.go](https://github.com/kgrz/reading-files-in-go/blob/master/scanner.go) -## 按单词扫描 +## 按  单词扫描 `bufio` 包包含了一些基本的预定义的分割函数: @@ -280,12 +280,12 @@ for scanner.Scan() { } fmt.Println("word list:") -for _, word := range words { +for _, Word := range words { fmt.Println(word) } ``` -`ScanBytes` 分割函数会返回和之前所说的 `Read()` 示例一样的结果。两者的主要区别在于在扫描器中每次我们需要添加到字节/字符串数组时存在的动态分配问题。我们可以用预先定义缓冲区大小并在达到大小限制后才增加其长度的技术来规避这种问题。示例如下: +`ScanBytes` 分割函数会返回  和之前所说的 `Read()` 示例一样的结果。两者的主要区别在于在扫描器中每次我们需要添加到字节/字符串数组时存在的动态分配问题。我们可以用预先定义缓冲区大小并在达到大小限制后才增加其长度的技术来规避这种问题。示例  如下: ```go file, err := os.Open("filetoread.txt") @@ -324,17 +324,17 @@ fmt.Println("word list:") // 由于我们会按固定大小扩充缓冲区,缓冲区容量可能比实际的单词数量大, 因此我们只有在 "pos" // 有效时才进行迭代。否则扫描器可能会因为遇到错误而提前终止。在这个例子中,"pos" 包含了 // 最后一次更新的索引。 -for _, word := range words[:pos] { +for _, Word := range words[:pos] { fmt.Println(word) } ``` 在 Github 中查看源文件 [scanner-word-list-grow.go](https://github.com/kgrz/reading-files-in-go/blob/master/scanner-word-list-grow.go) -最终我们可以实现更少的 “扩增” 操作,但同时根据 `bufferSize` 我们可能会在末尾存在一些空的插槽,这算是一种折中的方法。 +最终我们可以实现更少的 “扩增” 操作,但同时  根据 `bufferSize` 我们可能会在末尾存在一些空的插槽,这算是一种  折中的方法。 ## 将一个长字符串分割成多个单词 -`bufio.Scanner` 有一个参数,这个参数是实现了 `io.Reader` 接口的类型,这意味着该类型可以是任何拥有 `Read` 方法的类型。在标准库中 `strings.NewReader` 函数是一个返回 “reader” 类型的字符串实用方法。 +`bufio.Scanner` 有一个参数,这个参数是实现了 `io.Reader` 接口的类型,这意味着该  类型可以是任何拥有 `Read` 方法的类型。在标准库中 `strings.NewReader` 函数是一个返回 “reader” 类型的字符串实用方法。 我们可以把两者结合起来使用: ```go @@ -352,14 +352,14 @@ for scanner.Scan() { } fmt.Println("word list:") -for _, word := range words { +for _, Word := range words { fmt.Println(word) } ``` ## 读取逗号分隔的字符串 -用基本的 `file.Read()` 或者 `Scanner` 类型去解析 CSV 文件/字符串显得过于笨重,因为在 `bufio.ScanWords` 函数中“单词”是指被 unicode 空格分隔的符号(runes)。读取单个符号(runes),并持续跟踪缓冲区大小以及位置(就像 lexing/parsing 所做的)需要做太多的工作和操作。 +用基本的 `file.Read()` 或者 `Scanner` 类型去  解析 CSV 文件/字符串显得过于笨重, 因为在 `bufio.ScanWords` 函数中“单词”是指被 unicode 空格分隔的符号(runes)。读取单个符号(runes),并持续跟踪缓冲区大小以及位置(就像 lexing/parsing 所做的)需要做太多的工作和操作。 当然,这是可以避免的。我们可以定义一个新的分割函数,这个函数读取字符知道遇到逗号,然后调用 `Text()` 或者 `Bytes()` 返回该数据块。`bufio.SplitFunc` 的函数签名如下所示: @@ -370,10 +370,10 @@ for _, word := range words { 1. `data` 是输入的字节字符串 2. `atEOF` 是传递给函数的结束符标志 3. `advance` 使用它,我们可以指定处理当前读取长度的位置数。此值用于在扫描循环完成后更新游标位置 -4. `token` 是指扫描操作的实际数据 +4. `token` 是  指扫描操作的实际数据 5. `err` 你可能想返回发现的问题 -简单起见,我将演示读取一个字符串而不是一个文件。一个使用上述签名的简单的 CSV 读取器如下所示: + 简单起见,我将演示读取一个字符串而不是一个文件。一个使用上述签名的简单的 CSV 读取器如下所示: ```go csvstring := "name, age, occupation" @@ -411,7 +411,7 @@ for scanner.Scan() { ## Ruby 风格 -我们已经按照便利程度和功能一次增加的顺序列举了许多读取文件的方法。如果我们仅仅是想将一个文件读入缓冲区呢?标准库中的 `ioutil` 包包含了一些更简便的函数。 +我们已经按照便利程度和功能一次增加的顺序列举了许多读取文件的方法。如果我们仅仅是想将一个文件读入缓冲区呢?标准库中的 `ioutil` 包包含了一些  更简便的函数。 ## 读取整个文件 @@ -429,7 +429,7 @@ fmt.Println("String read: ", string(bytes)) ## 读取这个文件夹下的所有文件 -无需多言, 如果你有很大的文件,*不要*运行这个脚本 :D + 无需多言, 如果你有很大的文件,*不要*运行这个脚本 :D ```go filelist, err := ioutil.ReadDir(".") @@ -451,17 +451,17 @@ for _, fileinfo := range filelist { ## 更多的帮助方法 -在标准库中还有很多读取文件的函数(确切得说,读取器)。为了防止这篇本已冗长的文章变得更加冗长,我列举了一些我发现的函数: +在标准库中还有  很多  读取文件的函数(确切得说,读取器)。为了防止这篇本已  冗长的文章变得更加冗长,我  列举了一些我发现的函数: 1. `ioutil.ReadAll()` -> 使用一个类似 io 的对象,返回字节数组 2. `io.ReadFull()` 3. `io.ReadAtLeast()` -4. `io.MultiReader` -> 一个非常有用的合并多个类 io 对象的基本工具(primitive)。你可以把多个文件当成是一个连续的数据块来处理,而无需处理 +4. `io.MultiReader` -> 一个非常有用的合并多个类 io 对象的  基本工具(primitive)。你可以把多个文件当成是一个连续的数据块来处理,而无需处理 在上一个文件结束后切换至另一个文件对象的复杂操作。 ## 更新 -我尝试强调 “读取” 函数,我选择使用 error 函数来打印以及关闭文件: +我尝试强调 “读取” 函数,我选择使用 error 函数来打印以及关闭  文件: ```go func handleFn(file *os.File) func(error) { @@ -479,9 +479,9 @@ handle := handleFn(file) handle(err) ``` -这样操作,我忽略了一个重要的细节:如果没有错误发生且程序运行结束,那文件就不会被关闭。如果程序多次运行且没有发生错误,则会导致文件描述符泄露。这个问题已经在 [on reddit by u/shovelpost](https://www.reddit.com/r/golang/comments/7n2bee/various_ways_to_read_a_file_in_go/drzg32k/) 中指出。 +这样操作,我忽略了一个重要的细节:如果没有错误发生且  程序运行结束,那文件就  不会被关闭。如果程序多次运行且没有发生错误,则会导致文件描述符泄露。这个问题已经在 [on reddit by u/shovelpost](https://www.reddit.com/r/golang/comments/7n2bee/various_ways_to_read_a_file_in_go/drzg32k/) 中  指出。 -我之所以不想用 `defer` 是因为 `log.Fatal` 内部会调用 `os.Exit` 函数,而该函数不会运行 deferred 函数,所以我选择了手动关闭文件,然而却忽略了正常运行的情况。 +我之所以不想用 `defer` 是因为 `log.Fatal` 内部会调用 `os.Exit` 函数,而该函数  不会运行 deferred 函数,所以我选择了手动关闭文件,然而却忽略了正常运行的情况。 我已经在更新了的例子中使用了 `defer`,并用 `return` 取代了 `os.Exit()`。 diff --git a/published/tech/2018-Profiling-Go-Applications-with-Flamegraphs.md b/published/tech/2018-Profiling-Go-Applications-with-Flamegraphs.md index c6ea4d501..9c6d8b3d8 100644 --- a/published/tech/2018-Profiling-Go-Applications-with-Flamegraphs.md +++ b/published/tech/2018-Profiling-Go-Applications-with-Flamegraphs.md @@ -6,7 +6,7 @@ 应用的性能问题生来就是无法预料的 —— 而且他们总是在最坏的时间露头。让情况更糟的是,很多性能分析工具都是冷冰冰的,复杂难懂的,用起来彻头彻尾令人困惑的 —— 来自于 `valgrind` 和 `gdp` 这样最受推崇的性能分析工具的用户体验。 -`Flamegraphs` 是由 linux 性能分析大师 Brendan Gegg 创造的一个工具,在一般的 linux 性能追踪 dump 之上生成一个 SVG 可视化层 ,给定位和解决性能问题这个复杂的过程带来了一些“温暖”。在这篇文章中,我们会一步一步地用 `flamegraphs` 对一个简单的 golang 写的 web 应用进行性能分析。 +`Flamegraphs` 是由 Linux 性能分析大师 Brendan Gegg 创造的一个工具,在一般的 Linux 性能追踪 dump 之上生成一个 SVG 可视化层 ,给定位和解决性能问题这个复杂的过程带来了一些“温暖”。在这篇文章中,我们会一步一步地用 `flamegraphs` 对一个简单的 Golang 写的 Web 应用进行性能分析。 ## 开始之前的一些题外话 @@ -14,7 +14,7 @@ ## 示例程序 -我们将用一个小的 HTTP 服务器来演示,这个服务器通过 `GET /ping` 暴露了一个 healthcheck 的 API. 为了可视化,我们同时包含了一个小的 [statsd](https://www.datadoghq.com/blog/statsd/) 客户端用来记录服务器处理的每个请求的延迟。为了保持简单,我们的代码仅仅用到 go 的标准库,不过即使你习惯使用 `gorilla/mux` 或者别的流行的库,这些代码对你来说也不会太陌生。 +我们将用一个小的 HTTP 服务器来演示,这个服务器通过 `GET /ping` 暴露了一个 healthcheck 的 API. 为了可视化,我们同时包含了一个小的 [statsd](https://www.datadoghq.com/blog/statsd/) 客户端用来记录服务器处理的每个请求的延迟。为了保持简单,我们的代码仅仅用到 Go 的标准库,不过即使你习惯使用 `gorilla/mux` 或者别的流行的库,这些代码对你来说也不会太陌生。 ```go import ( @@ -26,7 +26,7 @@ import ( "time" ) -// SimpleClient is a thin statsd client. +// SimpleClient is a Thin statsd client. type SimpleClient struct { c net.PacketConn ra *net.UDPAddr @@ -109,7 +109,7 @@ func pingHandler(s *SimpleClient) http.HandlerFunc{ ## 安装性能分析工具 -go 的标准库内置了诊断性能问题的工具,有一套丰富而完整的工具可以嵌入 go 简单高效的运行时。如果你的应用使用的是默认的 `http.DefaultServeMux` ,那么集成 `pprof` 无需额外的代码,只需在你的 `import` 头部加入以下语句。 +go 的标准库内置了诊断性能问题的工具,有一套丰富而完整的工具可以嵌入 Go 简单高效的运行时。如果你的应用使用的是默认的 `http.DefaultServeMux` ,那么集成 `pprof` 无需额外的代码,只需在你的 `import` 头部加入以下语句。 ```go import ( @@ -121,7 +121,7 @@ import ( ## 生成 Flamegraph -`flamegraph` 工具的工作是接收你系统的已有的一个堆栈跟踪文件,进行解析,生成一个 SVG 可视化。为了得到其中一个神秘的堆栈跟踪文件,我们可以使用随 go 一起安装的 [pprof](https://github.com/google/pprof) 工具。为了将东西整合在一起,免受安装和配置更多软件之苦,我们将使用 [uber/go-torch](https://github.com/uber/go-torch) 这个出色的库 —— 它为整个过程提供了非常方便的集装箱式的工作流程。 +`flamegraph` 工具的工作是接收你系统的已有的一个堆栈跟踪文件,进行解析,生成一个 SVG 可视化。为了得到其中一个神秘的堆栈跟踪文件,我们可以使用随 Go 一起安装的 [pprof](https://github.com/google/pprof) 工具。为了将东西整合在一起,免受安装和配置更多软件之苦,我们将使用 [uber/go-torch](https://github.com/uber/go-torch) 这个出色的库 —— 它为整个过程提供了非常方便的集装箱式的工作流程。 Flamegraph 可以生成自各种各样的配置文件,每一个都针对不同的性能指标。你可以使用同样的工具包和方法论来寻找 CPU 的性能瓶颈、内存泄漏,甚至是死锁的进程。 @@ -136,7 +136,7 @@ docker run uber/go-torch -u http://:8080/debug/pprof -p -t=30 > torch.s 如果你的应用服务器运行在本地,或者是在 staging 环境,复现最初让系统出现性能问题的场景可能很困难。作为模拟生产环境的工作负载的一种途径,我们将使用一个叫做 [vegeta](https://github.com/tsenart/vegeta) 的负载测试小工具来模拟出与线上每台服务器处理的请求相当的吞吐量。 -`vegeta` 有一个极其强大的可配置API,支持不同类型的负载测试及基准测试场景。在我们的简单服务器的案例里,我们可以通过以下单行指令来产生足够的流量,来让事情变得有趣。 +`vegeta` 有一个极其强大的可配置 API,支持不同类型的负载测试及基准测试场景。在我们的简单服务器的案例里,我们可以通过以下单行指令来产生足够的流量,来让事情变得有趣。 ``` # send 250rps for 60 seconds @@ -153,17 +153,17 @@ open -a `Google Chrome` torch.svg ## 读懂 Flamegraph -flamegraph 中的每一个水平区段代表一个栈帧,决定它的宽度的是采样过程中你的程序被观察到在对这个帧进行求值的相对(%)时间。这些区段在垂直方向上根据在调用栈中的位置被组织成一个个的 "flame",也就是说在图的 y轴方向上位于上方的函数是被位于下方的函数调用的 —— 自然地,在上方的函数比下方的函数占用了更小片的 CPU 时间。如果你想深入视图中的某一部分,很简单,只需要点击某一帧,这样位于这帧下方的帧都会消失,而且界面会自己调整尺寸。 +flamegraph 中的每一个水平区段代表一个栈帧,决定它的宽度的是采样过程中你的程序被观察到在对这个帧进行求值的相对(%)时间。这些区段在垂直方向上根据在调用栈中的位置被组织成一个个的 "flame",也就是说在图的 y 轴方向上位于上方的函数是被位于下方的函数调用的 —— 自然地,在上方的函数比下方的函数占用了更小片的 CPU 时间。如果你想深入视图中的某一部分,很简单,只需要点击某一帧,这样位于这帧下方的帧都会消失,而且界面会自己调整尺寸。 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/flamegraphs/2.png) *注:栈帧的颜色是无意义的,完全随机——色调和色度的区分是为了让图更易读。* -通过直接查看或是点击几个帧来缩小范围——有没有性能问题及问题是什么应该会即刻变得显而易见。记住 [80/20 规律](https://en.wikipedia.org/wiki/Pareto_principle) ,你的大部分性能问题都会出现在做了比它该做的多得多的少部分代码上——别把你的时间花在flamegraph图表中那些薄小的穗上。 +通过直接查看或是点击几个帧来缩小范围——有没有性能问题及问题是什么应该会即刻变得显而易见。记住 [80/20 规律](https://en.wikipedia.org/wiki/Pareto_principle) ,你的大部分性能问题都会出现在做了比它该做的多得多的少部分代码上——别把你的时间花在 flamegraph 图表中那些薄小的穗上。 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/flamegraphs/3.png) -举例来说,在我们的程序中,我们可以深入到那些较大片的区段,看到我们花了10%(!) 的时间在将结果通过网络 socket 刷到我们的统计服务器!幸运的是,修复这个很简单——通过在我们的代码中添加一个小的buffer,我们可以解决这个问题,然后产生一个新的,更纤细的图。 +举例来说,在我们的程序中,我们可以深入到那些较大片的区段,看到我们花了 10%(!) 的时间在将结果通过网络 socket 刷到我们的统计服务器!幸运的是,修复这个很简单——通过在我们的代码中添加一个小的 buffer,我们可以解决这个问题,然后产生一个新的,更纤细的图。 ### Code Change @@ -189,7 +189,7 @@ func (sc *SimpleClient) send(s string) error { ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/flamegraphs/4.png) -就是它了!Flamegraph是个窥测你的应用性能的简单且强大的工具。试着为你的应用产生一个 flamegraph——你的发现可能会给你带来惊喜:) +就是它了!Flamegraph 是个窥测你的应用性能的简单且强大的工具。试着为你的应用产生一个 flamegraph——你的发现可能会给你带来惊喜:) ## 扩展阅读 @@ -198,7 +198,7 @@ func (sc *SimpleClient) send(s string) error { - [Flamegraphs - Brendan Gegg](http://www.brendangregg.com/flamegraphs.html) - [The Flame Graph - ACMQ](https://queue.acm.org/detail.cfm?id=2927301) - [The Mature Optimization Handbook](https://www.facebook.com/notes/facebook-engineering/the-mature-optimization-handbook/10151784131623920/) -- [Profiling and Optimizing go Applications](https://www.youtube.com/watch?v=N3PWzBeLX2M) +- [Profiling and Optimizing Go Applications](https://www.youtube.com/watch?v=N3PWzBeLX2M) --- diff --git a/published/tech/20180102-Wrapping-a-microservice-boilerplate-in-golang.md b/published/tech/20180102-Wrapping-a-microservice-boilerplate-in-golang.md index 5c638a159..b799976b6 100644 --- a/published/tech/20180102-Wrapping-a-microservice-boilerplate-in-golang.md +++ b/published/tech/20180102-Wrapping-a-microservice-boilerplate-in-golang.md @@ -16,7 +16,7 @@ go get github.com/spf13/viper ``` -Viper 包是一个 Go 语言下的全方位配置解决方案包含了`十二要素应用程序`(译者注:一种应用开发理论)。它被设计成和应用程序一起运行,几乎能处理所有的配置需求及格式。它支持如下类型: +Viper 包是一个 Go 语言下的全方位配置解决方案包含了 ` 十二要素应用程序 `(译者注:一种应用开发理论)。它被设计成和应用程序一起运行,几乎能处理所有的配置需求及格式。它支持如下类型: - 默认设置 - 读取 JSON, TOML, YAML, HCL, 和 Java 的 properties 配置文件 @@ -76,7 +76,7 @@ go get github.com/garyburd/redigo/redis - 一个类似 Print 一样的接口支持所有的 Redis 命令。 - 管道,包含了管道事务。 -- 支持发布/订阅(Publish和Subscribe)。 +- 支持发布/订阅(Publish 和 Subscribe)。 - 支持连接池。 - Script 辅助类型能对 EVALSHA 方便的进行使用。 - 辅助函数专门来处理命令的应答。 diff --git a/published/tech/20180104-Create-Golang-API-documentation-with-SwaggerUI.md b/published/tech/20180104-Create-Golang-API-documentation-with-SwaggerUI.md index 98b735f50..6fc208635 100644 --- a/published/tech/20180104-Create-Golang-API-documentation-with-SwaggerUI.md +++ b/published/tech/20180104-Create-Golang-API-documentation-with-SwaggerUI.md @@ -10,13 +10,13 @@ 大约两年前,我曾经在开发一个 RESTful 风格的企业应用的后台时,第一次知道 [SwaggerUI](https://swagger.io/swagger-ui/) 。 SwaggerUI 的创造者 SmartBear 将其产品描述为: -> "Swagger UI 允许任何人(无论是你的开发团队还是最终用户)在没有任何实现逻辑的情况下对 API 资源进行可视化和交互。它(API文档)通过 Swagger 定义自动生成,可视化文档使得后端实现和客户端消费变得更加容易。" +> "Swagger UI 允许任何人(无论是你的开发团队还是最终用户)在没有任何实现逻辑的情况下对 API 资源进行可视化和交互。它(API 文档)通过 Swagger 定义自动生成,可视化文档使得后端实现和客户端消费变得更加容易。" 简而言之,通过提供 Swagger(OpenAPI)定义,您可以获得与 API 进行交互的界面,而不必关心编程语言本身。你可以将 Swagger(OpenAPI) 视为 REST 的 WSDL 。 作为参考,Swagger Codegen 可以从这个定义中,用几十种编程语言来生成客户端和服务器代码。 -回到那个时候,我使用的是 Java 和 SpringBoot ,觉得 Swagger 简单易用。你仅需创建一次 bean ,并添加一两个注解到端点上,再添加一个标题和一个项目描述。此外,我习惯将所有请求从 “/” 重定向到 “/swagger-ui” 以便在我打开 `host:port` 时自动跳转到 SwaggerUI 。在运行应用程序的时候, SwaggerUI 在同一个端口依然可用。(例如,您的应用程序运行在`[host]:[port]`, SwaggerUI 将在`[host]:[port]/swagger-ui`上访问到)。 +回到那个时候,我使用的是 Java 和 SpringBoot ,觉得 Swagger 简单易用。你仅需创建一次 bean ,并添加一两个注解到端点上,再添加一个标题和一个项目描述。此外,我习惯将所有请求从 “/” 重定向到 “/swagger-ui” 以便在我打开 `host:port` 时自动跳转到 SwaggerUI 。在运行应用程序的时候, SwaggerUI 在同一个端口依然可用。(例如,您的应用程序运行在 `[host]:[port]`, SwaggerUI 将在 `[host]:[port]/swagger-ui` 上访问到)。 快一年半之后,我想在我们的 Go 项目中实现 SwaggerUI 。问题是 —— 感觉太复杂了。当我在网络上搜索时,我看到不仅仅是我,其他许多用户也遇到了同样的麻烦。 @@ -28,7 +28,7 @@ ## 安装 Go-Swagger -在开始之前,您需要在本地机器上安装 go swagger 。这不是一个强制性的步骤,但会使得更容易的使用 swagger 工作。安装它可以让你在本地测试你的注释,否则,你只能依靠你的 CI 工具。 +在开始之前,您需要在本地机器上安装 Go swagger 。这不是一个强制性的步骤,但会使得更容易的使用 swagger 工作。安装它可以让你在本地测试你的注释,否则,你只能依靠你的 CI 工具。 最简单的安装方式是通过运行 Homebrew / Linuxbrew : @@ -47,7 +47,7 @@ brew install go-swagger 如果你的 API 仅提供在 HTTP 或 HTTPS 上,且只生成 JSON ,您应在此处添加它 - 允许你从每个路由中删除该注释。 -安全也被添加在 swagger:meta 中,在 SwaggerUI 上添加一个授权按钮。为了实现JWT,我使用安全类型承载进行命名并将其定义为: +安全也被添加在 swagger:meta 中,在 SwaggerUI 上添加一个授权按钮。为了实现 JWT,我使用安全类型承载进行命名并将其定义为: ```go // Security: @@ -63,7 +63,7 @@ brew install go-swagger ## Swagger:route [[docs]](https://goswagger.io/generate/spec/route.html) -有两种方式两个注释你的路由,swagger:operation 和swagger:route 。两者看起来都很相似,那么主要区别是什么? +有两种方式两个注释你的路由,swagger:operation 和 swagger:route 。两者看起来都很相似,那么主要区别是什么? 把 swagger:route 看作简单 API 的短注释,它适用于没有输入参数(路径/查询参数)的 API 。那些(带有参数)的例子是 /repos/{owner} , /user/{id} 或者 /users/search?name=ribice @@ -83,14 +83,14 @@ brew install go-swagger ``` 1. **swagger:route** - 注解 -2. **POST** - HTTP方法 +2. **POST** - HTTP 方法 3. /**repo** - 匹配路径,端点 4. **repos** - 路由所在的空间分割标签,例如,“repos users” 5. **createRepoReq** - 用于此端点的请求(详细的稍后会解释) -6. **Creates a new repository …** - 摘要(标题)。对于swager:route注释,在第一个句号(.)前面的是标题。如果没有句号,就会没有标题并且这些文字会被用于描述。 -7. **If repository name exists …** - 描述。对于swager:route类型注释,在第一个句号(.)后面的是描述。 +6. **Creates a new repository …** - 摘要(标题)。对于 swager:route 注释,在第一个句号(.)前面的是标题。如果没有句号,就会没有标题并且这些文字会被用于描述。 +7. **If repository name exists …** - 描述。对于 swager:route 类型注释,在第一个句号(.)后面的是描述。 8. **responses**: - 这个端点的响应 -9. **200: repoResp** - 一个(成功的)响应HTTP状态 200,包含 repoResp(用 swagger:response 注释的模型) +9. **200: repoResp** - 一个(成功的)响应 HTTP 状态 200,包含 repoResp(用 swagger:response 注释的模型) 10. **400: badReq, 409: conflict, 500: internal** - 此端点的错误响应(错误请求,冲突和内部错误, 定义在 cmd/api/swagger/model.go 下) 如此注释您的端点将产生以下内容: @@ -137,7 +137,7 @@ brew install go-swagger ## Swagger:operation [docs](https://goswagger.io/generate/spec/operation.html) -使用 Swagger:operation 可以让你使用所有[OpenAPI规范](https://swagger.io/specification/),你可以描述你的复杂的端点。如果你对细节感兴趣,你可以阅读规范文档。 +使用 Swagger:operation 可以让你使用所有[OpenAPI 规范](https://swagger.io/specification/),你可以描述你的复杂的端点。如果你对细节感兴趣,你可以阅读规范文档。 简单来说 - swagger:operation 包含如下内容: @@ -163,11 +163,11 @@ brew install go-swagger 2. **GET** - HTTP 方法 3. /**repo/{author}** - 匹配路径,端点 4. **repos** - 路由所在的空间分割标签,例如,“repos users” -5. **repoList** - 用于此端点的请求。这个不存在(没有定义),但参数是强制性的,所以你可以用任何东西来替换repoList(noReq,emptyReq等) -6. --- - 这个部分下面是YAML格式的swagger规范。确保您的缩进是一致的和正确的,否则将无法正确解析。注意,如果你在YAML中定义了标签,摘要,描述或操作标签,将覆盖上述常规swagger语法中的摘要,描述,标记或操作标签。 +5. **repoList** - 用于此端点的请求。这个不存在(没有定义),但参数是强制性的,所以你可以用任何东西来替换 repoList(noReq,emptyReq 等) +6. --- - 这个部分下面是 YAML 格式的 swagger 规范。确保您的缩进是一致的和正确的,否则将无法正确解析。注意,如果你在 YAML 中定义了标签,摘要,描述或操作标签,将覆盖上述常规 swagger 语法中的摘要,描述,标记或操作标签。 7. **summary**: - 标题 8. **description**: - 描述 -9. **parameters**: - URL参数(在这个例子中是{author})。字符串格式,强制性的(Swagger不会让你调用端点而不输入),位于路径(/{author})中。另一种选择是参数内嵌的请求 (?name="") +9. **parameters**: - URL 参数(在这个例子中是{author})。字符串格式,强制性的(Swagger 不会让你调用端点而不输入),位于路径(/{author})中。另一种选择是参数内嵌的请求 (?name="") 定义你的路由后,你需要定义你的请求和响应。从示例中,你可以看到,我创建了一个新的包,命名为 swagger 。这不是强制性的,它把所有样板代码放在一个名为 swagger 的包中。但缺点是你必须导出你的所有 HTTP 请求和响应。 @@ -179,9 +179,9 @@ _ "github.com/ribice/golang-swaggerui-example/cmd/swagger" ## Swagger:parameters [[docs]](https://goswagger.io/generate/spec/params.html) -根据您的应用程序模型,您的 HTTP 请求可能会有所不同(简单,复杂,封装等)。要生成 Swagger 规范,您需要为每个不同的请求创建一个结构,甚至包含仅包含数字(例如id)或字符串(名称)的简单请求。 +根据您的应用程序模型,您的 HTTP 请求可能会有所不同(简单,复杂,封装等)。要生成 Swagger 规范,您需要为每个不同的请求创建一个结构,甚至包含仅包含数字(例如 id)或字符串(名称)的简单请求。 -一旦你有这样的结构(例如一个包含一个字符串和一个布尔值的结构),在你的Swagger包中定义如下: +一旦你有这样的结构(例如一个包含一个字符串和一个布尔值的结构),在你的 Swagger 包中定义如下: ```go // Request containing string @@ -195,7 +195,7 @@ type swaggerCreateRepoReq struct { - 第 1 行包含一个在 SwaggerUI 上可见的注释 - 第 2 行包含 swagger:parameters 注释,以及请求的名称(operationID)。此名称用作路由注释的最后一个参数,以定义请求。 - 第 4 行包含这个参数的位置(in:body,in:query 等) -- 第 5 行是实际的内嵌结构。正如前面所提到的,你不需要一个独立的 swagger 批注包(你可以把swagger:parameters注释放在 api.CreateRepoReq 上),但是一旦你开始创建响应注释和验证,那么在 swagger 相关批注一个单独的包会更清晰。 +- 第 5 行是实际的内嵌结构。正如前面所提到的,你不需要一个独立的 swagger 批注包(你可以把 swagger:parameters 注释放在 api.CreateRepoReq 上),但是一旦你开始创建响应注释和验证,那么在 swagger 相关批注一个单独的包会更清晰。 ![swagger-parameters](https://raw.githubusercontent.com/studygolang/gctt-images/master/swagger-golang/swagger-golang4.jpg) @@ -214,7 +214,7 @@ type swaggerCreateRepoReq struct { ![swagger-patameters-ui](https://raw.githubusercontent.com/studygolang/gctt-images/master/swagger-golang/swagger-golang5.jpg) - Swagger 有很多验证注释提供给 swagger:parameters和 swagger:response ,在注释标题旁边的文档中有详细的描述和使用方法。 + Swagger 有很多验证注释提供给 swagger:parameters 和 swagger:response ,在注释标题旁边的文档中有详细的描述和使用方法。 ## Swagger:response [[docs]](https://goswagger.io/generate/spec/response.html) @@ -238,7 +238,7 @@ type swaggerCreateRepoReq struct { } ``` -要使用常规响应,像上面错误响应那样的,我通常在 swagger 包内部创建 model.go(或swagger.go)并在里面定义它们。在示例中,下面的响应用于 OK 响应(不返回任何数据): +要使用常规响应,像上面错误响应那样的,我通常在 swagger 包内部创建 model.go(或 swagger.go)并在里面定义它们。在示例中,下面的响应用于 OK 响应(不返回任何数据): ```go // Success response @@ -302,7 +302,7 @@ type swaggReposResp struct { 总之,这将足以生成您的 API 文档。您也应该向文档添加验证,但遵循本指南将帮助您开始。由于这主要是由我自己的经验组成,并且在某种程度上参考了 Gitea 的[源代码](https://github.com/go-gitea/gitea),我将会听取关于如何改进这部分并相应更新的反馈。 -如果您有一些问题或疑问,我建议您查看[如何生成FAQ](https://goswagger.io/faq/faq_spec.html)。 +如果您有一些问题或疑问,我建议您查看[如何生成 FAQ](https://goswagger.io/faq/faq_spec.html)。 ## 本地运行 SwaggerUI @@ -334,7 +334,7 @@ swagger generate spec -o ./swagger.json --scan-models && swagger serve -F=swagge 例如,我们的应用程序正在 Google App Engine 上运行。Swagger Spec 由我们的 CI 工具生成,并在 /docs 路径上提供。 -我们将 SwaggerUI 作为 Docker 服务部署在 GKE(Google Container/Kubernates Engine)上,它从 /docs 路径中获取swagger.json。 +我们将 SwaggerUI 作为 Docker 服务部署在 GKE(Google Container/Kubernates Engine)上,它从 /docs 路径中获取 swagger.json。 我们的 CI(Wercker)脚本的一部分: @@ -352,7 +352,7 @@ build: code: | go get -u github.com/go-swagger/go-swagger/cmd/swagger swagger generate spec -o ./swagger.json --scan-models - CGO_ENABLED=0 go build -a -ldflags '-s' -installsuffix cgo -o app . + CGO_ENABLED=0 Go build -a -ldflags '-s' -installsuffix cgo -o app . cp app *.template Dockerfile swagger.json "$WERCKER_OUTPUT_DIR" ``` @@ -375,7 +375,7 @@ ENV API_URL "https://api.orga.com/swagger" ## 总结 -SwaggerUI 是一个功能强大的 API 文档工具,可以让您轻松而漂亮地记录您的 API。在 go-swagger 项目的帮助下,您可以轻松地生成 SwaggerUI 所需的swagger规范文件(swagger.json)。 +SwaggerUI 是一个功能强大的 API 文档工具,可以让您轻松而漂亮地记录您的 API。在 go-swagger 项目的帮助下,您可以轻松地生成 SwaggerUI 所需的 swagger 规范文件(swagger.json)。 总之,我描述了为实现这一目标所采取的步骤。可能有更好的方法,我会确保根据收到的反馈更新这篇文章。 diff --git a/published/tech/20180104-Fail-fast-and-furiously.md b/published/tech/20180104-Fail-fast-and-furiously.md index ea84c976d..dd02e8f5d 100644 --- a/published/tech/20180104-Fail-fast-and-furiously.md +++ b/published/tech/20180104-Fail-fast-and-furiously.md @@ -34,7 +34,7 @@ type MySumParams struct { } ``` -(2) 编写*校验器*来检验参数是否符合它的类型在语义上的预期。例如,如果 MySumParams 只接受正数,那么它应该定义一个 `Validate()` 函数,当接收到的参数不是正数,返回一个有意义的错误信息。在 go 的代码库中,对于这些检验器,我们有一个指定的函数签名:`func Validate() error`。 +(2) 编写*校验器*来检验参数是否符合它的类型在语义上的预期。例如,如果 MySumParams 只接受正数,那么它应该定义一个 `Validate()` 函数,当接收到的参数不是正数,返回一个有意义的错误信息。在 Go 的代码库中,对于这些检验器,我们有一个指定的函数签名:`func Validate() error`。 *认真地考虑*为每一个 Thrift RPC 或 gRPC 类型定义一个校验器。通过保证客户端和服务器不会出现不一致的预期,这增加了很大的安全性。而这些不一致的预期,往往在服务有修改或者更新时容易出现(例如,我们之后可能将 `mySum` 修改为允许参数为负)。 diff --git a/published/tech/20180104-Let-Make-an-NTP-Client-in-Go.md b/published/tech/20180104-Let-Make-an-NTP-Client-in-Go.md index 2109a23cb..64b44c36d 100644 --- a/published/tech/20180104-Let-Make-an-NTP-Client-in-Go.md +++ b/published/tech/20180104-Let-Make-an-NTP-Client-in-Go.md @@ -58,7 +58,7 @@ if err := conn.SetDeadline(time.Now().Add(15 * time.Second)); err != nil { ## 从服务端获取时间 -在发送请求包给服务端前,第一个字节是用来设置通信的配置,我们这里用 0x1B(或者二进制 00011011),代表客户端模式为 3,NTP版本为 3,润年为 0,如下所示: +在发送请求包给服务端前,第一个字节是用来设置通信的配置,我们这里用 0x1B(或者二进制 00011011),代表客户端模式为 3,NTP 版本为 3,润年为 0,如下所示: ```go // configure request settings by specifying the first byte as diff --git a/published/tech/20180105-Basic-Role-Based-HTTP-Authorization-in-Go-with-Casbin.md b/published/tech/20180105-Basic-Role-Based-HTTP-Authorization-in-Go-with-Casbin.md index 2cc944f5c..af309e8e4 100644 --- a/published/tech/20180105-Basic-Role-Based-HTTP-Authorization-in-Go-with-Casbin.md +++ b/published/tech/20180105-Basic-Role-Based-HTTP-Authorization-in-Go-with-Casbin.md @@ -2,11 +2,11 @@ # 在 Go 语言中使用 casbin 实现基于角色的 HTTP 权限控制 -身份认证和授权对 web 应用的安全至关重要。最近,我用 Go 完成了我的第一个正式的 web 应用,这篇文章是在这个过程中我所学到的部分内容。 +身份认证和授权对 Web 应用的安全至关重要。最近,我用 Go 完成了我的第一个正式的 Web 应用,这篇文章是在这个过程中我所学到的部分内容。 -本文中,我们的关注点在于如何在 web 应用中使用开源的 casbin 库进行 HTTP 权限控制。同时,在示例代码中我们使用了 scs 库进行 session 管理。 +本文中,我们的关注点在于如何在 Web 应用中使用开源的 casbin 库进行 HTTP 权限控制。同时,在示例代码中我们使用了 scs 库进行 session 管理。 -下面的例子十分基础,希望它尽可能的展示了如何在 Go web 应用中实现权限控制。为了更侧重于展示 casbin 的使用,我们尽量简化业务逻辑(例如:不需密码的登陆操作)。我们一起来看一下! +下面的例子十分基础,希望它尽可能的展示了如何在 Go Web 应用中实现权限控制。为了更侧重于展示 casbin 的使用,我们尽量简化业务逻辑(例如:不需密码的登陆操作)。我们一起来看一下! 注意:请不要在生产环境中使用所示的用例代码,该例子侧重于描述清晰,而不是安全性。 @@ -54,7 +54,7 @@ m = r.sub == p.sub && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*") 在安全性方面,我通常会选择最简单的解决方案,因为当系统开始变复杂和难以维护时,错误就开始发生。 -在这个例子中,策略文件就是一个简单的 csv 文件,描述了哪些角色可以访问哪些路径等。 +在这个例子中,策略文件就是一个简单的 CSV 文件,描述了哪些角色可以访问哪些路径等。 policy.csv 文件格式如下: @@ -244,9 +244,9 @@ func Authorizer(e *casbin.Enforcer, users model.Users) func(next http.Handler) h ## 结论 -我已经在一个中型 web 应用生产环境中使用了 casbin,并且对它的可维护性和稳定性感到十分满意。可以看看它的文档,casbin 是一个非常强大的鉴权工具,以声明的方式提供了大量的访问控制模型。 +我已经在一个中型 Web 应用生产环境中使用了 casbin,并且对它的可维护性和稳定性感到十分满意。可以看看它的文档,casbin 是一个非常强大的鉴权工具,以声明的方式提供了大量的访问控制模型。 -本文旨在展示 casbin 和 scs 的强大之处,并且展示 go web 应用的简洁清晰之处。 +本文旨在展示 casbin 和 scs 的强大之处,并且展示 Go web 应用的简洁清晰之处。 资源: diff --git a/published/tech/20180105-The-Doors-Go-Has-Opened.md b/published/tech/20180105-The-Doors-Go-Has-Opened.md index c286af364..0a9a479b8 100644 --- a/published/tech/20180105-The-Doors-Go-Has-Opened.md +++ b/published/tech/20180105-The-Doors-Go-Has-Opened.md @@ -22,13 +22,13 @@ Cloud Native 将不可能抛弃 Go ,[Cloud Native Computing Foundation](https: ## 工具链 -大家都在 [Go playground](https://play.golang.org/) (译者注:一个Golang的在线编辑网站) 上开始尝试 Go 语言。你只需要打开一个网站,写一些代码,然后运行。无需安装,在哪都能开始写代码,这是一个不错的体验。 +大家都在 [Go playground](https://play.golang.org/) (译者注:一个 Golang 的在线编辑网站) 上开始尝试 Go 语言。你只需要打开一个网站,写一些代码,然后运行。无需安装,在哪都能开始写代码,这是一个不错的体验。 > *无论你在做什么,你只要关注你做的。Go 已经为你找到了解决问题的工具* 然后你去下载一个 toolchain (译者注:工具链,一般指的就是编译工具)—— 一个二进制 `go` 文件。你可以通过运行 `go build` 命令来获一个生产级别的软件。无需学习 GCC toolchain ,C 语言,Linux ,共享对象,JVM 或其它相关技术。 -不管你在开发什么,你只需专注开发的业务,而不是你需要哪些工具。Go已经为你解决了相应的工具了。 +不管你在开发什么,你只需专注开发的业务,而不是你需要哪些工具。Go 已经为你解决了相应的工具了。 ## 一个二进制文件 @@ -60,7 +60,7 @@ Cloud Native 将不可能抛弃 Go ,[Cloud Native Computing Foundation](https: Go 是一个无锁的强大的分布式系统,因为从根本上让并发操作更简单了。 -这就是为什么我们能看到这样一个更有弹性,更快速,并且高效利用CPU的软件。用 Go ,事实上你可以开发你在研究资料中找到的东西。 +这就是为什么我们能看到这样一个更有弹性,更快速,并且高效利用 CPU 的软件。用 Go ,事实上你可以开发你在研究资料中找到的东西。 ## 垃圾回收 @@ -113,7 +113,7 @@ Go 标准库是最好的商业库之一。它不大但是却覆盖了 80% 的常 我希望 Go 能继续成为其它领域的标准——前端服务(替代 Rails / Node .js),CLIs (替换许多脚本语言),也许还能替换 GUIs 和 移动 APP 。 -正值 Go [8 周年](https://blog.golang.org/8years) ,它快速地崛起了。但下一个 8 年它的趋势是否会扩大10倍。 +正值 Go [8 周年](https://blog.golang.org/8years) ,它快速地崛起了。但下一个 8 年它的趋势是否会扩大 10 倍。 还是那句话,Go 将成为软件工程中几个大型领域的标准编程语言。 diff --git a/published/tech/20180116-Import-declarations-in-Go.md b/published/tech/20180116-Import-declarations-in-Go.md index 3604e6596..64b97ec9c 100644 --- a/published/tech/20180116-Import-declarations-in-Go.md +++ b/published/tech/20180116-Import-declarations-in-Go.md @@ -35,7 +35,7 @@ import ( 这四个导入格式都有各自不同的行为,在这篇文章中我们将分析这些差异。 -> 导入包只能引用导入包中的导出标识符。 导出标识符是以Unicode大写字母开头的 +> 导入包只能引用导入包中的导出标识符。 导出标识符是以 Unicode 大写字母开头的 > - [https://golang.org/ref/spec#Exported_identifiers](https://golang.org/ref/spec#Exported_identifiers)。 ## 基础 @@ -137,7 +137,7 @@ func hi() { 上述代码无法被成功编译: ``` -> go build +> Go build // github.com/mlowicki/a ./foo.go:6:2: undefined: fmt ``` @@ -267,7 +267,7 @@ func main() { ``` ``` -> go run main.go +> Go run main.go // command-line-arguments ./main.go:6:2: V redeclared during import "github.com/mlowicki/c" previous declaration during import "github.com/mlowicki/b" @@ -276,7 +276,7 @@ func main() { ### 使用空标识符 -如果导入了包但是不使用,Golang的编译器将无法编译通过。 +如果导入了包但是不使用,Golang 的编译器将无法编译通过。 ```go package main @@ -286,7 +286,7 @@ import "fmt" func main() {} ``` -使用点导入,其中所有导出的标识符都直接添加到导入文件块中,在编译时也会出现失败。唯一的绕过方式是使用空白标识符。需要知道init函数是什么,以便理解为什么我们需要导入空白标识符。参考之前init的介绍文章 [https://medium.com/golangspec/init-functions-in-go-eac191b3860a](https://medium.com/golangspec/init-functions-in-go-eac191b3860a) 我鼓励从上到下阅读这篇文章,但本质上,像如下的导入方式: +使用点导入,其中所有导出的标识符都直接添加到导入文件块中,在编译时也会出现失败。唯一的绕过方式是使用空白标识符。需要知道 init 函数是什么,以便理解为什么我们需要导入空白标识符。参考之前 init 的介绍文章 [https://medium.com/golangspec/init-functions-in-go-eac191b3860a](https://medium.com/golangspec/init-functions-in-go-eac191b3860a) 我鼓励从上到下阅读这篇文章,但本质上,像如下的导入方式: ```go import _ "math" @@ -319,7 +319,7 @@ var B = a.A 尝试构建这两个包中的任何一个都会导致错误: ``` -> go build +> Go build can't load package: import cycle not allowed package github.com/mlowicki/a imports github.com/mlowicki/b @@ -353,8 +353,8 @@ func main() { via:https://medium.com/golangspec/import-declarations-in-go-8de0fd3ae8ff -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[iloghyr](https://github.com/iloghyr) 校对:[无闻](https://github.com/Unknwon) -本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go中文网](https://studygolang.com/) 荣誉推出 +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20180117-deep-dive-into-the-concurrency.md b/published/tech/20180117-deep-dive-into-the-concurrency.md index f430ff77b..c6be5a4fa 100644 --- a/published/tech/20180117-deep-dive-into-the-concurrency.md +++ b/published/tech/20180117-deep-dive-into-the-concurrency.md @@ -4,7 +4,7 @@ ## 介绍 -在过去的几个月里,我在几个项目上使用过 Go,尽管我还算不上专家,但是还是有几件事我要感谢 Go:首先,它有一个清晰而简单的语法,我不止一次注意到 Github 开发人员的风格非常接近于旧 C 程序中使用的风格,从理论上讲,Go 似乎吸收了世界上所有语言最好的特性:它有着高级语言的力量,明确的规则使得更简单,即使这些特性有时有一点点的约束力--就是可以给代码强加一个坚实的逻辑。这是命令式的简单,由大小以位为单位的原始类型组成。但是没有像把字符串当成字符数组那样操作的乏味。然而,我认为这两个非常有用和有趣的功能是 goroutine 和 channels。 +在过去的几个月里,我在几个项目上使用过 Go,尽管我还算不上专家,但是还是有几件事我要感谢 Go:首先,它有一个清晰而简单的语法,我不止一次注意到 Github 开发人员的风格非常接近于旧 C 程序中使用的风格,从理论上讲,Go 似乎吸收了世界上所有语言最好的特性:它有着高级语言的力量,明确的规则使得更简单,即使这些特性有时有一点点的约束力--就是可以给代码强加一个坚实的逻辑。这是命令式的简单,由大小以位为单位的原始类型组成。但是没有像把字符串当成字符数组那样操作的乏味。然而,我认为这两个非常有用和有趣的功能是 Goroutine 和 channels。 ![GoPIC](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-python/1.jpg) ## 前言 @@ -13,7 +13,7 @@ ### Goroutine -假设我们调用一个函数 f(s):这样的写法就是通常的调用方式,同步运行。如果要在 goroutine 中调用这个函数,使用 go f(s) 即可。这个新 goroutine 将和调用它的 goroutine 并发执行。但是... 什么是 goroutine 呢?这是一个独立执行的函数,由 go 语句启动。它有自己的调用堆栈,这个堆栈可以根据需要增长和缩减,而且非常节省空间。拥有数千甚至数十万个 goroutine 是实际存在的,但它不是线程。事实上,在一个有数千个 goroutine 的程序中可能只有一个线程。相反,goroutines 会根据需要动态复用到线程上,以保持所有的 goroutine 运行。如果你把它当成一种便宜的线程,也不会差太多。 +假设我们调用一个函数 f(s):这样的写法就是通常的调用方式,同步运行。如果要在 Goroutine 中调用这个函数,使用 Go f(s) 即可。这个新 Goroutine 将和调用它的 Goroutine 并发执行。但是... 什么是 Goroutine 呢?这是一个独立执行的函数,由 Go 语句启动。它有自己的调用堆栈,这个堆栈可以根据需要增长和缩减,而且非常节省空间。拥有数千甚至数十万个 Goroutine 是实际存在的,但它不是线程。事实上,在一个有数千个 Goroutine 的程序中可能只有一个线程。相反,goroutines 会根据需要动态复用到线程上,以保持所有的 Goroutine 运行。如果你把它当成一种便宜的线程,也不会差太多。 ```go package main @@ -34,11 +34,11 @@ func main() { f("direct") // To invoke this function in a goroutine, use - // `go f(s)`. This new goroutine will execute + // `go f(s)`. This new Goroutine will execute // concurrently with the calling one. go f("goroutine") - // You can also start a goroutine for an anonymous + // You can also start a Goroutine for an anonymous // function call. go func(msg string) { fmt.Println(msg) @@ -56,9 +56,9 @@ func main() { 更多细节 [3](#3) -正如我所说的,coroutine 背后的想法是复用独立执行的函数--coroutines--在一组线程上。当一个 coroutine 阻塞的时候,比如通过调用一个阻塞的系统调用, run-time 会自动地将同一个操作系统线程上的其他 coroutines 移动到一个不同的,可运行的线程上,这样它们就不会被阻塞。这些 coroutines 被称为 goroutines,非常便宜。它们的堆栈内存很少,只有几千字节。此外,为了使堆栈变小,Go 的 run-time 使用可调整大小的有界堆栈。新建的 goroutine 有几千字节,这个大小几乎总是足够的。当空间不够时,run-time 会自动增长(缩小)用于存储堆栈的内存,从而允许许多 goroutines 生存在适量的内存中。每个函数调用的 CPU 开销平均需要大约三个廉价的指令,所以在相同的地址空间中创建数十万个 goroutine 是很实际的。如果 goroutines 只是线程,那么系统资源将会用得更少。 +正如我所说的,coroutine 背后的想法是复用独立执行的函数--coroutines--在一组线程上。当一个 coroutine 阻塞的时候,比如通过调用一个阻塞的系统调用, run-time 会自动地将同一个操作系统线程上的其他 coroutines 移动到一个不同的,可运行的线程上,这样它们就不会被阻塞。这些 coroutines 被称为 goroutines,非常便宜。它们的堆栈内存很少,只有几千字节。此外,为了使堆栈变小,Go 的 run-time 使用可调整大小的有界堆栈。新建的 Goroutine 有几千字节,这个大小几乎总是足够的。当空间不够时,run-time 会自动增长(缩小)用于存储堆栈的内存,从而允许许多 goroutines 生存在适量的内存中。每个函数调用的 CPU 开销平均需要大约三个廉价的指令,所以在相同的地址空间中创建数十万个 Goroutine 是很实际的。如果 goroutines 只是线程,那么系统资源将会用得更少。 -好吧,真的很酷,但... 为什么?为什么我们要编写并发程序?要更快地完成我们的工作(即使编写正确的并发程序可能花费的时间比在并行环境中运行任务的时间长 XD)典型的线程情况包括分配一些共享内存并将其位置存储在 p 中的主线程。主线程启动 n 个工作线程,将指针 p 传递给他们,工作线程可以使用 p 来处理 p 指向的数据。但是如果线程开始更新相同的内存地址呢?我是说,这是计算机科学中最难的一个。好吧,让我们从简考虑:从操作系统的角度来看,一些原子系统调用让你锁定对共享内存区域的访问(我是指信号量,消息队列,锁等)。从语言角度来看,通常有一组原语,调用所需的系统调用,并让你将访问权限同步到共享内存区域(我是指像多处理,多线程,池等的包)。下面,我们来谈谈 Go 的一个工具,它可以帮助您处理 goroutine 之间的并发通信:channels。 +好吧,真的很酷,但... 为什么?为什么我们要编写并发程序?要更快地完成我们的工作(即使编写正确的并发程序可能花费的时间比在并行环境中运行任务的时间长 XD)典型的线程情况包括分配一些共享内存并将其位置存储在 p 中的主线程。主线程启动 n 个工作线程,将指针 p 传递给他们,工作线程可以使用 p 来处理 p 指向的数据。但是如果线程开始更新相同的内存地址呢?我是说,这是计算机科学中最难的一个。好吧,让我们从简考虑:从操作系统的角度来看,一些原子系统调用让你锁定对共享内存区域的访问(我是指信号量,消息队列,锁等)。从语言角度来看,通常有一组原语,调用所需的系统调用,并让你将访问权限同步到共享内存区域(我是指像多处理,多线程,池等的包)。下面,我们来谈谈 Go 的一个工具,它可以帮助您处理 Goroutine 之间的并发通信:channels。 ### Channels @@ -109,7 +109,7 @@ chan<- float64 // can only be used to send float64s ### 总结 -总而言之,你可以在 goroutine 中调用一个函数,甚至是匿名函数, 然后把结果放在一个 channel 中,默认情况下,发送和接收阻塞,直到另一端准备好。所有这些特性都允许 goroutine 在没有显式锁定或条件变量的情况下进行同步。好吧,但是... 他们表现地怎么样呢? +总而言之,你可以在 Goroutine 中调用一个函数,甚至是匿名函数, 然后把结果放在一个 channel 中,默认情况下,发送和接收阻塞,直到另一端准备好。所有这些特性都允许 Goroutine 在没有显式锁定或条件变量的情况下进行同步。好吧,但是... 他们表现地怎么样呢? ## Go vs Python @@ -173,7 +173,7 @@ func msort_merge(l []int, r []int) []int { ### 并发的 Go 版本 -我们来谈谈并发版本。我们可以拆分数组,并从主例程调用子例程,但是我们如何控制并发执行 go-routine 或工作数的最大数量?那么,限制 Go 中的并发的一种方法 [5](#5) 是使用缓冲通道(信号量)。正如我所说的,当你创建一个具有固定维度的通道或缓冲,如果缓冲区未满(发送)或不为空(接收),通信成功而不会阻塞,所以你根据你想拥有的并发单元的数量,实现一个信号量来轻松地阻止执行。真的很酷,但是... 有一个问题:一个 channel 是一个 channel,即使有缓冲,频道上的基本发送和接收也被阻止。幸运的是,Go 非常棒,让你创建明确的非阻塞通道, 使用 select 语句 [6](#6) :因此,您可以使用 select with default 子句来实现无阻塞的发送,接收,甚至是非阻塞的多路选择。还有一些其他的声明来解释,在我的前缀最大数量的并发 goroutine 版本的合并排序: +我们来谈谈并发版本。我们可以拆分数组,并从主例程调用子例程,但是我们如何控制并发执行 go-routine 或工作数的最大数量?那么,限制 Go 中的并发的一种方法 [5](#5) 是使用缓冲通道(信号量)。正如我所说的,当你创建一个具有固定维度的通道或缓冲,如果缓冲区未满(发送)或不为空(接收),通信成功而不会阻塞,所以你根据你想拥有的并发单元的数量,实现一个信号量来轻松地阻止执行。真的很酷,但是... 有一个问题:一个 channel 是一个 channel,即使有缓冲,频道上的基本发送和接收也被阻止。幸运的是,Go 非常棒,让你创建明确的非阻塞通道, 使用 select 语句 [6](#6) :因此,您可以使用 select with default 子句来实现无阻塞的发送,接收,甚至是非阻塞的多路选择。还有一些其他的声明来解释,在我的前缀最大数量的并发 Goroutine 版本的合并排序: ```go // Returns the result of a merge sort - the sort part - over the passed list @@ -187,7 +187,7 @@ func merge_sort_multi(s []int, sem chan struct{}) []int { // split length n := len(s) / 2 - // create a wait group to wait for both goroutine call before final merge step + // create a wait group to wait for both Goroutine call before final merge step wg := sync.WaitGroup{} wg.Add(2) @@ -201,7 +201,7 @@ func merge_sort_multi(s []int, sem chan struct{}) []int { // check if you can acquire a slot case sem <- struct{}{}: - // call another goroutine worker over the first half + // call another Goroutine worker over the first half go func() { l = merge_sort_multi(s[:n], sem) @@ -229,7 +229,7 @@ func merge_sort_multi(s []int, sem chan struct{}) []int { wg.Done() } - // wait for go subroutine + // wait for Go subroutine wg.Wait() // return @@ -238,7 +238,7 @@ func merge_sort_multi(s []int, sem chan struct{}) []int { } ``` -正如你所看到的,在我的默认选择操作中,我编写了一个调用单 routined 版本的合并排序。但是,代码中还有一个有趣的工具:它是由 sync 包提供的 WaitGroup 对象。从官方文档 [7](#7) 来看 ,WaitGroup 等待一系列 goroutines 完成。main goroutine 调用 Add 来设置要等待的 goroutines 的数量。然后,每个 goroutine 程序运行并完成后调用 Done。同时,Wait 可以用来阻塞,直到所有的 goroutines 都完成了。 +正如你所看到的,在我的默认选择操作中,我编写了一个调用单 routined 版本的合并排序。但是,代码中还有一个有趣的工具:它是由 sync 包提供的 WaitGroup 对象。从官方文档 [7](#7) 来看 ,WaitGroup 等待一系列 goroutines 完成。main Goroutine 调用 Add 来设置要等待的 goroutines 的数量。然后,每个 Goroutine 程序运行并完成后调用 Done。同时,Wait 可以用来阻塞,直到所有的 goroutines 都完成了。 ### Python 合并排序 @@ -358,7 +358,7 @@ def merge_sort_parallel_fastest(array, concurrentRoutine, threaded): # mapping each partition to one worker, using the standard merge sort data = pool.map(msort_sort, data) - # go ahead until the number of partition are reduced to one (workers end respective ordering job) + # Go ahead until the number of partition are reduced to one (workers end respective ordering job) while len(data) > 1: # extra partition if there's a odd number of worker diff --git a/published/tech/20180121-a-recap-of-request-handling.md b/published/tech/20180121-a-recap-of-request-handling.md index d0cea0628..fe4159317 100644 --- a/published/tech/20180121-a-recap-of-request-handling.md +++ b/published/tech/20180121-a-recap-of-request-handling.md @@ -49,7 +49,7 @@ func main() { 继续运行应用程序: ``` -$ go run main.go +$ Go run main.go Listening... ``` diff --git a/published/tech/20180202-Using-named-return-variables-to-capture-panics-in-Go.md b/published/tech/20180202-Using-named-return-variables-to-capture-panics-in-Go.md index 9a4a6d62f..dae6c5d6b 100644 --- a/published/tech/20180202-Using-named-return-variables-to-capture-panics-in-Go.md +++ b/published/tech/20180202-Using-named-return-variables-to-capture-panics-in-Go.md @@ -4,8 +4,8 @@ 这将是一个简短的帖子,灵感来源于 Sean Kelly 十一月份的推特。 -> 我发现了一个在 golang 中使用指定的返回值的原因并且现在我感到潸然泪下。 ->                    — Sean Kelly (@StabbyCutyou) 2017年11月15日 +> 我发现了一个在 Golang 中使用指定的返回值的原因并且现在我感到潸然泪下。 +>                    — Sean Kelly (@StabbyCutyou) 2017 年 11 月 15 日 其目标是为了记录并说明一种有必要使用到命名返回变量的情况,所以说让我们进入正题。 @@ -38,7 +38,7 @@ func doStuff() error { 在 Go Playground 上执行它 - https://play.golang.org/p/wzkjKGqFPL -之后你 go run 了你的代码然而...这是什么?你的 error 是 `nil` 值,甚至是在代码发生异常的时候。这并不是我们想要的! +之后你 Go run 了你的代码然而...这是什么?你的 error 是 `nil` 值,甚至是在代码发生异常的时候。这并不是我们想要的! ## 为什么会发生这种情况? diff --git a/published/tech/20180206-go-choose-a-framework.md b/published/tech/20180206-go-choose-a-framework.md index 68f70ca2b..9d9befecc 100644 --- a/published/tech/20180206-go-choose-a-framework.md +++ b/published/tech/20180206-go-choose-a-framework.md @@ -10,7 +10,7 @@ ## 标准库或 stdlib -Go 语言标准库的质量很高。你应该尽可能的使用它。如果你正在编写 API 服务,你需要熟悉 `net/http` 包,而且你最终无论使用哪个框架都是基于这个包的。将第三方包导入标准库是需要认真考虑的,当他们解决的是一个非常专注的问题时,这是不适合进入标准库的。举个例子,生成 UUID 的包,或 JWT 包。一些包(包括 web 框架)都是基于标准库构建的。一个很好的具体例子,[jmoiron/sqlx](https://jmoiron.github.io/sqlx/) 包就是基于 `sql/database` 包构建的。 +Go 语言标准库的质量很高。你应该尽可能的使用它。如果你正在编写 API 服务,你需要熟悉 `net/http` 包,而且你最终无论使用哪个框架都是基于这个包的。将第三方包导入标准库是需要认真考虑的,当他们解决的是一个非常专注的问题时,这是不适合进入标准库的。举个例子,生成 UUID 的包,或 JWT 包。一些包(包括 Web 框架)都是基于标准库构建的。一个很好的具体例子,[jmoiron/sqlx](https://jmoiron.github.io/sqlx/) 包就是基于 `sql/database` 包构建的。 ## 包管理器 @@ -30,11 +30,11 @@ Go 代码放在 GitHub,Bitbucket 和其他存储库,甚至可以自行托管 Go 有一个较小的生态系统,但是有很多基于 Go 的项目都被广泛的采用,最近 GitHub 上的 [go-chi/chi](https://github.com/go-chi/chi) 有 2500 星星和非常好的评论(和 sqlx 类似,chi 项目是基于底层的 `net/http` 包构建的)。我们在 [ErrorHub](https://errorhub.io/) 上使用它,我建议你使用它。 -有许多 web 框架可用,但是如上所述,你应该首先使用 stdlib,这样你可以在继续前进时明白你真正需要的。使用 web 框架本身是完全没有必要的,但是当你有新的需求时,你可以做出更明智地选择从哪里迁移。 +有许多 Web 框架可用,但是如上所述,你应该首先使用 stdlib,这样你可以在继续前进时明白你真正需要的。使用 Web 框架本身是完全没有必要的,但是当你有新的需求时,你可以做出更明智地选择从哪里迁移。 ## 从其他语言迁移到 Go -Go 和其他语言之间的不同之处在于语言细节。从 Python 迁移到 Ruby 或从 PHP 迁移到 Javascript 时,你会发现同样的差异。Go 也不例外。你可能会发现[(例如切片是如何工作的)](https://scene-si.org/2017/08/06/the-thing-about-slices/)起初有点混乱,但从任何语言迁移到任何其他语言时都会遇到这些问题。让我们再看看 ruby 的一个例子 [predicate methods](http://ruby-for-beginners.rubymonstas.org/objects/predicates.html)。 +Go 和其他语言之间的不同之处在于语言细节。从 Python 迁移到 Ruby 或从 PHP 迁移到 Javascript 时,你会发现同样的差异。Go 也不例外。你可能会发现[(例如切片是如何工作的)](https://scene-si.org/2017/08/06/the-thing-about-slices/)起初有点混乱,但从任何语言迁移到任何其他语言时都会遇到这些问题。让我们再看看 Ruby 的一个例子 [predicate methods](http://ruby-for-beginners.rubymonstas.org/objects/predicates.html)。 Go 的入门门槛真的很低。我在 15 年前使用 PHP,迁移到 Go 是相对比较简单的。让你理解 Node 的异步操作是很困难的,包括 Promise 和 yield。如果我能推荐两篇阅读材料,那么你应该阅读一下 [the interview with Ryan Dahl, the creator of Node](https://www.mappingthejourney.com/single-post/2017/08/31/episode-8-interview-with-ryan-dahl-creator-of-nodejs/),[Bob Nystroms critique of asynchronous functions](http://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/) 也是必读的。 @@ -50,11 +50,11 @@ Go 是一门出色的语言,可以提供后端逻辑,例如和数据库交 你被 React 所困恼吗?将其替换为 VueJS 而不用丢弃任何 Go 代码。在其他语言中,你必须严格遵守这个原则来分割应用程序,因为通常情况下,你不是在编写服务器,而只是生成将在浏览器中运行产生输出的脚本。当然,使用 Go 可以以相同的方式使用 `html/template`,但是选择使用前端框架实现前端,将会给你带来好处:专注于该框架的开发人员。不是每个人都喜欢 Go。 -你不会用 bash 写一个 web 服务器,对不对? +你不会用 bash 写一个 Web 服务器,对不对? ## 你为什么要用 Go? -对我来说主要的卖点就是标准库,语言和文档的质量非常高。Ruby,Node,PHP 和其他以 web 开发为中心的语言通常都是单线程的,如果可能的话,超出这个范围的通常都是使用一个附加组件,而不是一等公民。他们的内存管理很差(尽管,至少 PHP 在过去的 15 年里有了很大的改进),也许最重要的是它们都属于脚本语言范畴。编译的代码总是会比通过解释器运行的任何代码都快。 +对我来说主要的卖点就是标准库,语言和文档的质量非常高。Ruby,Node,PHP 和其他以 Web 开发为中心的语言通常都是单线程的,如果可能的话,超出这个范围的通常都是使用一个附加组件,而不是一等公民。他们的内存管理很差(尽管,至少 PHP 在过去的 15 年里有了很大的改进),也许最重要的是它们都属于脚本语言范畴。编译的代码总是会比通过解释器运行的任何代码都快。 人们总是重新发明轮子,不仅仅是因为他们可以,而且还因为他们可以以某种方式改善它。这可以以很小的增量完成,例如优化一个生成特定输出的特定函数,或者可以以更大的增量完成,例如创建一门将并发性作为一等公民的编程语言。 @@ -62,7 +62,7 @@ Go 是一门出色的语言,可以提供后端逻辑,例如和数据库交 ## 笔记 -这篇文章反映了 Go 确实有包管理器,但是到目前为止还没有官方的工具,也没有和 Go 的工具链一起捆绑发布。前面的文章误导了这一点,它暗示了 Go 根本没有包管理器。从技术上讲,所有其他包管理器(至少 npm 和 composer)都是是附加组件。 +这篇文章反映了 Go 确实有包管理器,但是到目前为止还没有官方的工具,也没有和 Go 的工具链一起捆绑发布。前面的文章误导了这一点,它暗示了 Go 根本没有包管理器。从技术上讲,所有其他包管理器(至少 NPM 和 composer)都是是附加组件。 ## 我很荣幸你能够阅读本文... diff --git a/published/tech/20180207-sql-as-an-api.md b/published/tech/20180207-sql-as-an-api.md index 013d37678..876cf3d32 100644 --- a/published/tech/20180207-sql-as-an-api.md +++ b/published/tech/20180207-sql-as-an-api.md @@ -2,7 +2,7 @@ # 以 SQL 作为 API -如果你不是在石头下住着,那么你也应该听过最近兴起一种新的对“函数作为服务”的理解。在开源社区,Alex Ellis 的 [OpenFaas](https://github.com/openfaas/faas) 项目受到了很高的关注,并且 [亚马逊Lambda宣布对Go语言的支持](https://aws.amazon.com/blogs/compute/announcing-go-support-for-aws-lambda/)。这些系统允许你按需扩容,并且通过 API 调用的方式来调用你的 CLI 程序。 +如果你不是在石头下住着,那么你也应该听过最近兴起一种新的对“函数作为服务”的理解。在开源社区,Alex Ellis 的 [OpenFaas](https://github.com/openfaas/faas) 项目受到了很高的关注,并且 [亚马逊 Lambda 宣布对 Go 语言的支持](https://aws.amazon.com/blogs/compute/announcing-go-support-for-aws-lambda/)。这些系统允许你按需扩容,并且通过 API 调用的方式来调用你的 CLI 程序。 ## Lambda/Faas 背后的动机 @@ -50,9 +50,9 @@ http.HandleFunc("/api/", phpHandler()) ## CGI 的解决办法 -如果这几年我有学到点东西的话,那一定是大多数的服务是数据驱动的。这意味着,一定有某种数据库保存着至关重要的数据。根据一个 [Quora上的回答](https://www.quora.com/Which-database-system-s-does-Twitter-use) , Twitter 使用了至少8种不同类型的数据库,从 MySQL,Cassandra 和 Redis,到其他更复杂的数据库。 +如果这几年我有学到点东西的话,那一定是大多数的服务是数据驱动的。这意味着,一定有某种数据库保存着至关重要的数据。根据一个 [Quora 上的回答](https://www.quora.com/Which-database-system-s-does-Twitter-use) , Twitter 使用了至少 8 种不同类型的数据库,从 MySQL,Cassandra 和 Redis,到其他更复杂的数据库。 -实际上,我的大部分工作大概是这样的,从数据库中读取员工数据,然后把它变成 json 格式并且通过 REST 调用提供出去,这些查询通常不能仅仅用一条 SQL 语句来实现,当然在很多情况下也是可以的。那么,不如我们写一些不会有 `os.Exec` 调用成本的 SQL 脚本来实现一些功能,而不是用 CGI 程序来实现它们? +实际上,我的大部分工作大概是这样的,从数据库中读取员工数据,然后把它变成 JSON 格式并且通过 REST 调用提供出去,这些查询通常不能仅仅用一条 SQL 语句来实现,当然在很多情况下也是可以的。那么,不如我们写一些不会有 `os.Exec` 调用成本的 SQL 脚本来实现一些功能,而不是用 CGI 程序来实现它们? 挑战接受了。 @@ -63,7 +63,7 @@ http.HandleFunc("/api/", phpHandler()) 最近我为 Twitch,Slack,Yotube,Discord 写了一些聊天机器人,看起来很快我要写一个 Teleganm 的版本了。其实他们的目的是对各种通道进行相似的连接,记录消息,加总一些统计信息并且对一些命令或者问题进行反馈。对于用 Vue.js 写成的前端网站,我们需要通过 API 来向他们传递一些数据。虽然不是所有的 API 都能用 SQL 来实现,但还是有很大一部分是可以的。比如: 1. 列出所有的通道 -2. 通过通道ID列出通道 +2. 通过通道 ID 列出通道 这两个调用相对来说很相似并且容易实现,我特别创建了两个文件,来提供这些信息: @@ -92,7 +92,7 @@ select * from channels where id=:id ``` 在 SQL 查询中使用 URL 参数 -在 go 语言中获得请求的参数,只需要在请求对象中的 `*url.URL` 结构体上调用 Query() 函数即可。这个函数返回 `url.Values` 对象,这是一个 `map[string][]string` 类型的别名。 +在 Go 语言中获得请求的参数,只需要在请求对象中的 `*url.URL` 结构体上调用 Query() 函数即可。这个函数返回 `url.Values` 对象,这是一个 `map[string][]string` 类型的别名。 我们需要转换这个对象并传递到 sqlx 的语句中。我们需要创建一个 `map[string]interface{}` 。因为我们需要调用的 sqlx 函数在查询时接受这种格式的参数([sqlx.NamedStmt.Queryx](https://godoc.org/github.com/jmoiron/sqlx#NamedStmt.Queryx))。让我们转换它们并且发起查询: ```go @@ -185,7 +185,7 @@ SQL 数据库并不是蠢笨的野兽,实际上它们非常强大,强大的 如果你真的有一些简单的查询,通过它们就能获得你需要的结果的话,那么你可以从这种以 SQL 作为 API 的方法中大大受益,除了节约系统开销外,你还可以提供给那些不熟悉 Go 语言但是熟悉 SQL 的程序员们一个增加系统功能的方法。 随着要求一个 API 返回特定结果的需求的逐渐增加,我们需要一种脚本语言,它能达到甚至超越 [PL/SQL](https://en.wikipedia.org/wiki/PL/SQL) 的种种限制,并且在不同关系型数据库中有不同的实现。 -或者,你在你的应用中一直挂接一个 JavaScript 的虚机就如同 [dop251/goja ](https://github.com/dop251/goja) 这样,然后让你们团队的前台程序员来一次他/她可能永远不会忘记的尝试。现在也有用纯粹 go 语言实现的 LUA 虚机,如果你想要某种比整个 ES5.1 小的“运行时”。 +或者,你在你的应用中一直挂接一个 JavaScript 的虚机就如同 [dop251/goja ](https://github.com/dop251/goja) 这样,然后让你们团队的前台程序员来一次他/她可能永远不会忘记的尝试。现在也有用纯粹 Go 语言实现的 LUA 虚机,如果你想要某种比整个 ES5.1 小的“运行时”。 ### 如果我能在这里遇到你... diff --git a/published/tech/20180219-Tags-in-Golang.md b/published/tech/20180219-Tags-in-Golang.md index 952e6f194..40400673b 100644 --- a/published/tech/20180219-Tags-in-Golang.md +++ b/published/tech/20180219-Tags-in-Golang.md @@ -253,7 +253,7 @@ tags.go:4: struct field tag `one two three` not compatible with reflect.StructTa via: https://medium.com/golangspec/tags-in-golang-3e5db0b8ef3e -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[gogeof](https://github.com/gogeof) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20180306-Generics-for-Functional-Patterns.md b/published/tech/20180306-Generics-for-Functional-Patterns.md index 49b5737b8..2f8a0374e 100644 --- a/published/tech/20180306-Generics-for-Functional-Patterns.md +++ b/published/tech/20180306-Generics-for-Functional-Patterns.md @@ -30,7 +30,7 @@ incremented := ints.Map(func(i int) int { return i + 1 }) 上面的代码看上去很理想,但我们必须做出不少重大改变(例如,像自动类型转换)才能在 Go 语言中实现这个。 -并且,尽管上述很理想,但事实上我们不得不添加另一个特例以使特定类型的 `slices` 可以执行新的 `Map` 方法,这与我的理想背道而驰。正如Russ Cox在 [GopherCon 2017主题演讲](https://www.youtube.com/watch?v=0Zbh_vmAKvk) 中所提到的,我宁愿将注意力放在与泛型有关的体验报告上。这就是这篇文章的内容。 +并且,尽管上述很理想,但事实上我们不得不添加另一个特例以使特定类型的 `slices` 可以执行新的 `Map` 方法,这与我的理想背道而驰。正如 Russ Cox 在 [GopherCon 2017 主题演讲](https://www.youtube.com/watch?v=0Zbh_vmAKvk) 中所提到的,我宁愿将注意力放在与泛型有关的体验报告上。这就是这篇文章的内容。 ## 一个现实点的例子 @@ -42,9 +42,9 @@ incremented := ints.Map(func(i int) int { return i + 1 }) incremented := WrapSlice(ints).Map(func(i int) int { return i + 1}) ``` -这个新的容器在Go中部分实现了类型语义。 +这个新的容器在 Go 中部分实现了类型语义。 -在今天的Go中,除了 `[]int` 之外,没有办法让单个 Wrap 函数在任何其他类型上工作,因为我们没有泛型编程机制。 +在今天的 Go 中,除了 `[]int` 之外,没有办法让单个 Wrap 函数在任何其他类型上工作,因为我们没有泛型编程机制。 你可以得到最接近的方法是手写或者生成代码,使得 Wrap 可以适用于你想要的所有类型。 @@ -54,7 +54,7 @@ incremented := WrapSlice(ints).Map(func(i int) int { return i + 1}) 尽管听起来如此,但是因为我坚信代码生成是一个工具,需要为它选择正确的用武之地。然而,在这里,代码生成绝对用错地方了。 -它有一个大问题,举个例子。假设您想要生成处理 `[] ints`,`[]string` 和自定义类型(MyCustomType)的代码。在今天的Go中,语言引擎将无法为您的所有类型提供相同的Wrap功能。与之相反,生成器可以生成这个API的代码: +它有一个大问题,举个例子。假设您想要生成处理 `[] ints`,`[]string` 和自定义类型(MyCustomType)的代码。在今天的 Go 中,语言引擎将无法为您的所有类型提供相同的 Wrap 功能。与之相反,生成器可以生成这个 API 的代码: ```go myStrings := []string{...} @@ -71,17 +71,17 @@ WrapMyCustomTypeSlice(myCustomTypes).Map(func(m MyCustomType) MyCustomType { }) ``` -所以我们每种类型都有一个 Wrap 函数。我们为所有类型提供兼容性,但是我们仍然没有API来针对我们所有类型编写泛型代码。 +所以我们每种类型都有一个 Wrap 函数。我们为所有类型提供兼容性,但是我们仍然没有 API 来针对我们所有类型编写泛型代码。 ## 下一步 很明显,我正在写关于在 Go 中添加泛型的知识,但“泛型”可能意味着很多东西。我在这里举例说明了我想要一个泛型 API,它有点类似函数式编程模式。您可以将此 API 外推到其他函数式编程模式。 -首先,我希望能够在 `[]T` 或者 `map[T]U` (T和U是任意类型)上调用 Map,并且能够将这些值转换为其他 slices 或 maps ([]A and map[B]C)。和我上一篇文章一样,我不打算在Go中发明泛型的语法,我只是想展示我想要的样子。我可以写一篇后续文章来提出一种语法。 +首先,我希望能够在 `[]T` 或者 `map[T]U` (T 和 U 是任意类型)上调用 Map,并且能够将这些值转换为其他 slices 或 maps ([]A and map[B]C)。和我上一篇文章一样,我不打算在 Go 中发明泛型的语法,我只是想展示我想要的样子。我可以写一篇后续文章来提出一种语法。 ## WrapSlice -我展示了我想要 WrapSlice 和 Map 看起来像上面那样,但这是一个简单的例子。Map 的强大功能是可以将切片从一种类型转换为另一种(即T1 => T2)。除了传递给Map的函数签名(注意参数和返回值是不同的类型)之外,该函数看起来与上例相同: +我展示了我想要 WrapSlice 和 Map 看起来像上面那样,但这是一个简单的例子。Map 的强大功能是可以将切片从一种类型转换为另一种(即 T1 => T2)。除了传递给 Map 的函数签名(注意参数和返回值是不同的类型)之外,该函数看起来与上例相同: ```go ints := []int{1, 2, 3, 4} @@ -100,7 +100,7 @@ bites := WrapSlice(strs).Map(func(s string) []byte { // } ``` -这里我们已经将 `[]int` 转换为 `[]string`,然后将 `[]string`转换为 `[]byte` +这里我们已经将 `[]int` 转换为 `[]string`,然后将 `[]string` 转换为 `[]byte` ## WrapMap diff --git a/published/tech/20180307-runtime-overhead-of-using-defer-in-go.md b/published/tech/20180307-runtime-overhead-of-using-defer-in-go.md index 8e1f44530..e7f572cda 100644 --- a/published/tech/20180307-runtime-overhead-of-using-defer-in-go.md +++ b/published/tech/20180307-runtime-overhead-of-using-defer-in-go.md @@ -46,7 +46,7 @@ func BenchmarkDeferNo(b *testing.B) { 在一个 8 核的谷歌云主机上运行基准测试: ``` -⇒ go test -v -bench BenchmarkDefer -benchmem +⇒ Go test -v -bench BenchmarkDefer -benchmem goos: linux goarch: amd64 pkg: cmd diff --git a/published/tech/20180312-synchronization_queues_in_golang.md b/published/tech/20180312-synchronization_queues_in_golang.md index 02aaea1c8..1c0efc4c6 100644 --- a/published/tech/20180312-synchronization_queues_in_golang.md +++ b/published/tech/20180312-synchronization_queues_in_golang.md @@ -13,10 +13,10 @@ ```go func main() { for i := 0; i < 10; i++ { - go programmer() + Go programmer() } for i := 0; i < 5; i++ { - go tester() + Go tester() } select {} // 漫长的工作日... } @@ -60,7 +60,7 @@ func pingPong() { 这个程序的输出类似这样: ```bash -> go run pingpong.go +> Go run pingpong.go Tester starts Programmer starts Programmer starts @@ -127,10 +127,10 @@ func programmer(q *queue.Queue) { func main() { q := queue.New() for i := 0; i < 10; i++ { - go programmer(q) + Go programmer(q) } for i := 0; i < 5; i++ { - go tester(q) + Go tester(q) } select {} } @@ -199,7 +199,7 @@ func (q *Queue) EndP() { 程序员和测试员通过非缓冲的 channel `<-q.queueP` 或者 `<-q.queueT` 来等待对手。 -从这些 channel 接收数据时,如果此时没有可配对的对手,那么当前的 goroutine 会被阻塞。 +从这些 channel 接收数据时,如果此时没有可配对的对手,那么当前的 Goroutine 会被阻塞。 我们来分析一下给测试员调用的 `StartT` 函数: @@ -219,7 +219,7 @@ func (q* Queue) StartT() { 如果 `numP` 大于 0(表示当前至少有一个程序员在等待加入游戏),那么正在等待中的程序员的数量就会减一,并且有一个正在等待中的程序员批准加入游戏(`q.queueP <- 1`)。有趣的是在这个过程中 mutex 不会被释放掉,这时它的职能就是作为一个允许进入乒乓球桌的令牌。 -如果当前没有正在等待的程序员,那么 `numT`(等待中的测试员的数量)将会加一,并且当前的 goroutine 会被阻塞在 `q.queueT`。 +如果当前没有正在等待的程序员,那么 `numT`(等待中的测试员的数量)将会加一,并且当前的 Goroutine 会被阻塞在 `q.queueT`。 `StartP` 函数基本上是一样的,只是它是给程序员调用的。 @@ -264,7 +264,7 @@ func New() *Queue { queueP: make(chan int), queueT: make(chan int), } - go func() { + Go func() { for { select { case n := <-q.msg: @@ -307,7 +307,7 @@ func (q *Queue) EndP() { } ``` -我们会有个专门的中央协调器在一个独立的 goroutine 里面运行,它负责协调整个过程。协调器通过 `msg` channel 获取所有想要玩乒乓球的和刚玩完乒乓球的员工的信息。收到消息时,调度器的状态将会更新: +我们会有个专门的中央协调器在一个独立的 Goroutine 里面运行,它负责协调整个过程。协调器通过 `msg` channel 获取所有想要玩乒乓球的和刚玩完乒乓球的员工的信息。收到消息时,调度器的状态将会更新: - 等待中的程序员或者测试员的数量会增加。 - 正在游戏的员工的信息会被更新。 @@ -318,7 +318,7 @@ func (q *Queue) EndP() { if q.waitP > 0 && q.waitT > 0 && !q.playP && !q.playT { ``` -如果相应的状态都已经更新了的话,那么一个代表程序员的 goroutine 和一个代表测试员的 goroutine 将会被唤醒。 +如果相应的状态都已经更新了的话,那么一个代表程序员的 Goroutine 和一个代表测试员的 Goroutine 将会被唤醒。 我们在这个方案中没有使用 mutex,而是使用了一个独立的 goroutine,它通过 channel 与外部世界通讯,这让我们的程序成为一个更”地道“(符合 Go 语言风格)的 Go 语言程序。 @@ -328,7 +328,7 @@ if q.waitP > 0 && q.waitT > 0 && !q.playP && !q.playT { ## 参考资料 -- “The Little Book of Semaphores” by Allen B. Downey(译注:[PDF版地址](http://greenteapress.com/semaphores/LittleBookOfSemaphores.pdf)) +- “The Little Book of Semaphores” by Allen B. Downey(译注:[PDF 版地址](http://greenteapress.com/semaphores/LittleBookOfSemaphores.pdf)) - https://medium.com/golangspec/reusable-barriers-in-golang-156db1f75d0b (译文: https://studygolang.com/articles/12718) - https://blog.golang.org/share-memory-by-communicating @@ -336,7 +336,7 @@ if q.waitP > 0 && q.waitT > 0 && !q.playP && !q.playT { via: https://medium.com/golangspec/synchronization-queues-in-golang-554f8e3a31a4 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[Alex-liutao](https://github.com/Alex-liutao) 校对:[Unknwon](https://github.com/Unknwon) diff --git a/published/tech/20180314-Go-Is-Amazing.md b/published/tech/20180314-Go-Is-Amazing.md index fa508a72d..f861f7045 100644 --- a/published/tech/20180314-Go-Is-Amazing.md +++ b/published/tech/20180314-Go-Is-Amazing.md @@ -2,13 +2,13 @@ # Go 实在是令人惊叹,但是我想说说我不喜欢它的地方 -通过我的上一篇文章以及最近几个月期间对于Go编程语言的间接推广,我与许多开始对这门语言感兴趣的人们进行了交流,所以现在我打算转而去写一些我对这门语言的不满,依据我目前积累的经验来提供一种更加全面的看法,借此可以让一部分人意识到Go语言终究并不是他们项目的最佳选择。 +通过我的上一篇文章以及最近几个月期间对于 Go 编程语言的间接推广,我与许多开始对这门语言感兴趣的人们进行了交流,所以现在我打算转而去写一些我对这门语言的不满,依据我目前积累的经验来提供一种更加全面的看法,借此可以让一部分人意识到 Go 语言终究并不是他们项目的最佳选择。 -**备注1** +**备注 1** 需要重点指出的是,文章里的部分观点(如果不是全部的话)是基于我个人的主观想法并且跟我的编程习惯有关,它们没有必要也不应该被描述成“最佳解法”。还有就是,我现在仍旧是一个 Go 语言的菜鸟,我接下来要说的一些东西可能是不准确或者错误的,对于有误的地方请务必纠正我,这样我才能学到新东西。:D -**备注2** +**备注 2** 在开始前我需要声明的是:我热爱这门语言并且我已经解释了为什么我觉得对于许多应用来说这是一个更佳的选择,但是我对于 Go 和 Rust 那个更好或者 Go 和其他任何语言哪一个更好这种问题不感兴趣……选择你认为最佳的方案去完成你要做的事情:如果你认为 Rust 更好就尝试使用它,如果你认为是你传送到处理器的字节码引起了数据总线的错误,就去尝试纠错,两种情况都是,尽管去编程,而不是浪费生命在盲目追逐所谓的流行语言上。 @@ -63,14 +63,14 @@ import "github.com/bettercap/bettercap" 或者是像下面这样: ``` -# go get github.com/bettercap/bettercap +# Go get github.com/bettercap/bettercap ``` -简单来说,在 Go 最简单的安装方式中,你很可能会用到(不使用 vendor 目录并且也不覆盖 $GOPATH 变量的情况下)所有(事实上并不是,但是为了把问题简化我们可以这样假设)在这个安装目录或者你设置的 $GOPATH 变量目录下的东西,在我这里这个目录是 /home/evilsocket/gocode(是的,[确实是这样](https://github.com/evilsocket/dotfiles/blob/master/data/go.zshrc#L2))。每当我使用 go get 命令获取或者通过导包后使用 go get 命令[自动下载所需的包](https://github.com/bettercap/bettercap/blob/master/Makefile#L28)时,它在我的电脑上基本是下面这个样子: +简单来说,在 Go 最简单的安装方式中,你很可能会用到(不使用 vendor 目录并且也不覆盖 $GOPATH 变量的情况下)所有(事实上并不是,但是为了把问题简化我们可以这样假设)在这个安装目录或者你设置的 $GOPATH 变量目录下的东西,在我这里这个目录是 /home/evilsocket/gocode(是的,[确实是这样](https://github.com/evilsocket/dotfiles/blob/master/data/go.zshrc#L2))。每当我使用 Go get 命令获取或者通过导包后使用 Go get 命令[自动下载所需的包](https://github.com/bettercap/bettercap/blob/master/Makefile#L28)时,它在我的电脑上基本是下面这个样子: ``` # mkdir -p $GOHOME/src -# git clone https://github.com/bettercap/bettercap.git $GOHOME/src/github.com/bettercap/bettercap +# Git clone https://github.com/bettercap/bettercap.git $GOHOME/src/github.com/bettercap/bettercap ``` 如你所见,Go 事实上直接使用了 Git 仓库来管理这些包,应用或者任何与 Go 有关的东西……从某方面来说确实很方便,但是这会引起一个很大的问题:只要你不使用其他工具或者基于这个问题做一些难看的规避方案,那么你每次在一个新系统上编译你的软件时,只要缺失了某个依赖包,这个依赖包所在仓库的主分支就会被克隆下来。这意味着,**尽管你应用的代码完全没有修改,但是你每次在新电脑上编译时都很可能会产生代码差异**(只要你任何一个依赖包在主分支上有改动)。 @@ -94,14 +94,14 @@ import "github.com/bettercap/bettercap" 2. 基本上所有你在 Python 里可以用 dir 命令做到的所有事情 3. 构建我想到的 802.11 协议的漏洞检查工具(fuzzer) -由此看出,(Go里面的)反射跟别的语言比起来确实有点有限了……我不清楚你会怎么想,但是这确实让我有点烦…… +由此看出,(Go 里面的)反射跟别的语言比起来确实有点有限了……我不清楚你会怎么想,但是这确实让我有点烦…… ## 泛型?没有 大部分从面向对象编程的语言(转向 Go 开发时)会抱怨 Go 里缺少泛型,就我个人而言这并不算是一个大问题,因为我自己并不是很热衷于不计代价的面相对象编程。相反,我认为 Go 的对象模型(确切的说并不能算是对象模型)很简洁,我认为这种设计跟泛型会引起的复杂性相冲突了。 **备注** -我并不是想说“泛型==面相对象编程(OOP)”,但是大部分开发者希望(Go 语言支持)泛型是因为他们用 Go 来替代 C++ 并且希望有类似的模板,或者 Java 泛型……我们确实可以讨论从其它具有泛型或者类似东西的功能语言转型的一小部分(开发者),但是就我个人经验来说这部分人并不影响统计。 +我并不是想说“泛型 == 面相对象编程(OOP)”,但是大部分开发者希望(Go 语言支持)泛型是因为他们用 Go 来替代 C++ 并且希望有类似的模板,或者 Java 泛型……我们确实可以讨论从其它具有泛型或者类似东西的功能语言转型的一小部分(开发者),但是就我个人经验来说这部分人并不影响统计。 从另一个方面来看,这种(看起来跟直接使用 C 语言里的功能和结构体很相似的)简化对象模型,会让其他一些事情变得没有其他语言来说那么简单和直接。 @@ -153,13 +153,13 @@ func (d Derived) interfaceMethod() { 无需多言,如果你想要(或者需要)支持多架构跨平台(为什么你不应该认为 Go 最大的优点之一——如我们所说的——恰恰正是这个?),这会让你的编译复杂度大增,进而让你的 Go 项目在交叉编译时至少会和一个 C/C++ 项目一样复杂(讽刺的是,有时甚至会更复杂)。 -在[我的一些项目](https://github.com/evilsocket/arc)的某个时刻,我将项目里的所有 sqlite 数据库都替换成了 JSON 文件,这让我摆脱了本地依赖从而构建了一个 100%(基于)Go (编写的)应用。这样依赖交叉编译又重新变得简单了(如果你不能避免使用本地依赖,那么这是你不得不解决的难题……对此我感到十分抱歉 :/)。 +在[我的一些项目](https://github.com/evilsocket/arc)的某个时刻,我将项目里的所有 SQLite 数据库都替换成了 JSON 文件,这让我摆脱了本地依赖从而构建了一个 100%(基于)Go (编写的)应用。这样依赖交叉编译又重新变得简单了(如果你不能避免使用本地依赖,那么这是你不得不解决的难题……对此我感到十分抱歉 :/)。 -如果现在你“聪明的内心“正在尖叫着说“全部使用静态编译!”(静态编译库文件来让它们至少被打包进二进制文件里),不要这样做。如果你用一个特定版本的 glibc(c运行库)来对所有代码进行静态编译,那么编译出来的二进制文件在使用其他版本 glibc 的系统上是无法运行的。 +如果现在你“聪明的内心“正在尖叫着说“全部使用静态编译!”(静态编译库文件来让它们至少被打包进二进制文件里),不要这样做。如果你用一个特定版本的 glibc(c 运行库)来对所有代码进行静态编译,那么编译出来的二进制文件在使用其他版本 glibc 的系统上是无法运行的。 如果你“更聪明的内心“正在尖叫着说“使用 docker 来区分编译版本!”,请找出一个方法来正确的配置所有的平台和所有的(cpu)架构后发邮件告诉我这个方法 :) -如果你的“对 go 语言有点了解的内心”正打算建议一些外部的 glibc 替代品,请参照上一条的需求(如何区分所有配置):D +如果你的“对 Go 语言有点了解的内心”正打算建议一些外部的 glibc 替代品,请参照上一条的需求(如何区分所有配置):D ## ASLR? 没有!(嘲讽脸) diff --git a/published/tech/20180320-How-to-write-Go-middleware.md b/published/tech/20180320-How-to-write-Go-middleware.md index ef64f3e18..a48362b07 100644 --- a/published/tech/20180320-How-to-write-Go-middleware.md +++ b/published/tech/20180320-How-to-write-Go-middleware.md @@ -1,8 +1,8 @@ 已发布:https://studygolang.com/articles/12931 -# 如何写 go 中间件 +# 如何写 Go 中间件 -编写 go 中间件看起来挺简单的,但是有些情况下我们可能会遇到一些麻烦。 +编写 Go 中间件看起来挺简单的,但是有些情况下我们可能会遇到一些麻烦。 让我们来看一些例子。 @@ -43,7 +43,7 @@ func TrailingSlashRedirect(h http.Handler) http.Handler { 假设我们要向请求添加一个头部信息,或者修改它。 `http.Handler` 文档说明如下: - >除了读取主体之外,处理程序不应修改所提供的请求。 + > 除了读取主体之外,处理程序不应修改所提供的请求。 Go 标注库在[传递 `http.Request` 对象到响应链之前会先拷贝 `http.Request`](https://golang.org/src/net/http/server.go#L1981),我们也应该这样做。 假设我们要为每个请求设置一个 `Request-Id` 头部信息,用于内部跟踪。 @@ -150,8 +150,8 @@ func Server(h http.Handler, servername string) http.Handler { ## 其他 `ResponseWriter` 接口 -ResponseWriter接口只需要实现三个方法。 -但实际上,它也可以响应其他接口,例如 `http.Pusher`。此外,你的中间件可能会意外禁用HTTP/2支持,这是不好的。 +ResponseWriter 接口只需要实现三个方法。 +但实际上,它也可以响应其他接口,例如 `http.Pusher`。此外,你的中间件可能会意外禁用 HTTP/2 支持,这是不好的。 ```go // Push implements the http.Pusher interface. diff --git a/published/tech/20180321-How-to-Efficiently-Compare-Strings-in-Go.md b/published/tech/20180321-How-to-Efficiently-Compare-Strings-in-Go.md index c63f96a48..5e2dda5cf 100644 --- a/published/tech/20180321-How-to-Efficiently-Compare-Strings-in-Go.md +++ b/published/tech/20180321-How-to-Efficiently-Compare-Strings-in-Go.md @@ -8,8 +8,8 @@ strings.ToLower(name) == strings.ToLower(othername) ``` -这是一种很直接的写法。把字符串转换成小写,然后在比较。要理解为什么这是一个差的解决方案,你需要知道字符串是如何表示的,以及 `ToLower` 是如何工作的。但是首先,让我们讨论一下字符串比较中主要的使用场景,当使用 `==` 操作符时,我们得到最快和最优化的解决方案。通常 APIs 或类似的软件通常都会考虑这些使用场景。我们使用 `ToLower` 称之为 eature-complete。[^注1]。 -[^注1] This is when we drop in ToLower and call it feature-complete. +这是一种很直接的写法。把字符串转换成小写,然后在比较。要理解为什么这是一个差的解决方案,你需要知道字符串是如何表示的,以及 `ToLower` 是如何工作的。但是首先,让我们讨论一下字符串比较中主要的使用场景,当使用 `==` 操作符时,我们得到最快和最优化的解决方案。通常 APIs 或类似的软件通常都会考虑这些使用场景。我们使用 `ToLower` 称之为 eature-complete。[^ 注 1]。 +[^ 注 1] This is when we drop in ToLower and call it feature-complete. 在 Go 中,字符串是一系列*不可变*的 runes。Rune 是 Go 的一个术语,代表一个码点(Code Point)。你可以在 [Go blog](https://blog.golang.org/strings) 获取更多关于 Strings, bytes, runes 和 characters 的信息。 `ToLower` 是一个标准库函数循环处理字符串中的每个 rune 转换成小写,然后返回新的字符串。所以上面的代码在比较之前遍历了整个字符串。这就和字符串的长度十分相关了。下面的伪代码大概的展示了上面代码片段的复杂度。 diff --git a/published/tech/20180322-Taking-the-Gopher-to-its-extremes.md b/published/tech/20180322-Taking-the-Gopher-to-its-extremes.md index 0e72ddece..6b013c200 100644 --- a/published/tech/20180322-Taking-the-Gopher-to-its-extremes.md +++ b/published/tech/20180322-Taking-the-Gopher-to-its-extremes.md @@ -41,7 +41,7 @@ func isPalindrome (str string) { } ``` -就像你看到的,我们最终用几行代码就写了一个大家一看就懂的函数,在大多数情况下,非常有名的函数都可以实现 go 语言的版本,函数在 golang 中是第一公民,所以我们可以做一些类似下面的实现: +就像你看到的,我们最终用几行代码就写了一个大家一看就懂的函数,在大多数情况下,非常有名的函数都可以实现 Go 语言的版本,函数在 Golang 中是第一公民,所以我们可以做一些类似下面的实现: ```go isPalindrome := func (str string) { @@ -51,9 +51,9 @@ isPalindrome := func (str string) { isPalindrome("radar") ``` -这只是 golang 功能中很小的一部分,还有很多 go 语言实现中有大量优化的地方的例子,例如 go 中的 map,reduce,和 filter,一个好的实践方法是优化它们,并使用它们。 +这只是 Golang 功能中很小的一部分,还有很多 Go 语言实现中有大量优化的地方的例子,例如 Go 中的 map,reduce,和 filter,一个好的实践方法是优化它们,并使用它们。 -例如,数据处理在 golang 中的速度要比 python 快得多,所以,(虽然)我(都)可以创建一个数据管道来清理和组织我的数据,但如果我愿意用 golang 实现的话,将更快。 +例如,数据处理在 Golang 中的速度要比 python 快得多,所以,(虽然)我(都)可以创建一个数据管道来清理和组织我的数据,但如果我愿意用 Golang 实现的话,将更快。 假设我有一个数值列表,我想要用一种优雅的方式,通过一组函数来处理这个列表中的数值: diff --git a/published/tech/20180326-Go-Data-Structures-Interface.md b/published/tech/20180326-Go-Data-Structures-Interface.md index f7630670b..fdc82a983 100644 --- a/published/tech/20180326-Go-Data-Structures-Interface.md +++ b/published/tech/20180326-Go-Data-Structures-Interface.md @@ -79,7 +79,7 @@ func (i Binary) Get() uint64 { 这些例子表明,即使在编译时检查所有隐式转换,显式的 interface-to-interface 的转换也可以在运行时通过查询方法集实现。[《Effective Go》](http://golang.org/doc/effective_go.htm) 中有更多关于如何使用接口的详细信息和示例。 -## Interface的值 +## Interface 的值 带有方法的语言通常会有两种选择:准备一个所有方法的静态调用表(C++ 和 java),或者在每次调用时进行方法查找(Smalltalk,python 以及 javascript)。 @@ -99,7 +99,7 @@ interface 值中的第二个指针指向了实际的数据,也就是 b 的一 要检查接口值是否是特定类型,Go 编译器生成了表达式 `s.tab->type` 来获取类型指针并检查是否是所需的类型。如果类型匹配,则可以通过取消引用来复制 `s.data`。 -在调用 s.String() 时,Go 编译器生成了一份代码,相当于 C 语言中的 `s.tab->fun[0](s.data)`:它会从 itable 中选择合适的函数指针,将 interface 值的数据字段作为它的第一个参数。如果你运行 `8g -S x.go` ( 译者注:8g 是老版本中的一个工具,在 go 1.5 后可以使用 `go tool compile -S x.go` 来代替),你可以看到这段代码。需要注意的是,itable 中的函数的参数只能传入 32-bit 数据字段指针,而不能传入 64-bit 的值。一般来说,在调用接口的时候,代码是不会知道这个指针的意义,也不知道它所指向的数据有多少。相反,在接口的 itable 中的函数,也都期望接收到一个 32-bit 的指针。因此在这个实例中,函数的指针应该是 `(*Binary).String` 而不是 `Binary.String`。 +在调用 s.String() 时,Go 编译器生成了一份代码,相当于 C 语言中的 `s.tab->fun[0](s.data)`:它会从 itable 中选择合适的函数指针,将 interface 值的数据字段作为它的第一个参数。如果你运行 `8g -S x.go` ( 译者注:8g 是老版本中的一个工具,在 Go 1.5 后可以使用 `go tool compile -S x.go` 来代替),你可以看到这段代码。需要注意的是,itable 中的函数的参数只能传入 32-bit 数据字段指针,而不能传入 64-bit 的值。一般来说,在调用接口的时候,代码是不会知道这个指针的意义,也不知道它所指向的数据有多少。相反,在接口的 itable 中的函数,也都期望接收到一个 32-bit 的指针。因此在这个实例中,函数的指针应该是 `(*Binary).String` 而不是 `Binary.String`。 这个例子是一个只有一个方法的 interface。一个具有更多方法的 interface 将在 itable 底部有更多条记录。 @@ -109,9 +109,9 @@ interface 值中的第二个指针指向了实际的数据,也就是 b 的一 Go 的动态类型转换使编译器不可能对所有的 interface 到具体类型的 itables 进行预先计算,但是其实大部分的 itable 也是不需要的(比如程序中只需要计算 Stringer-Binary 的 itable,但是不需要计算 Stringer-string, Stringer-uint64 等对应的 itable)。 -因此,在 go 语言中,编译器为每个具体类型生成一个类型描述,包含了由该类型实现的方法的列表。类似地,编译器也为每个接口类型生成类型描述,同样也包含了该接口类型的实现方法列表。在运行时, 编译器在具体类型的方法表中查找 interface 类型的方法表中列出的每个方法来计算 itable。当生成了 itable 后,会将其保存在cache中,所以每个 itable 只需要生成一次。 +因此,在 Go 语言中,编译器为每个具体类型生成一个类型描述,包含了由该类型实现的方法的列表。类似地,编译器也为每个接口类型生成类型描述,同样也包含了该接口类型的实现方法列表。在运行时, 编译器在具体类型的方法表中查找 interface 类型的方法表中列出的每个方法来计算 itable。当生成了 itable 后,会将其保存在 cache 中,所以每个 itable 只需要生成一次。 -在本文的例子中, Stringer 的方法表中只有一个方法,而 Binary 的方法表中有两个方法。假设 interface 类型 ni个方法,具体类型有 nt 个方法,那么通常来说,检索的复杂度为 `O(ni * nt)`。但是 go 采用了一种更好的方法,通过对两个表的函数进行排序,并且对其进行同步遍历,检索的复杂度可以降为 `O(ni + nt)`。 +在本文的例子中, Stringer 的方法表中只有一个方法,而 Binary 的方法表中有两个方法。假设 interface 类型 ni 个方法,具体类型有 nt 个方法,那么通常来说,检索的复杂度为 `O(ni * nt)`。但是 Go 采用了一种更好的方法,通过对两个表的函数进行排序,并且对其进行同步遍历,检索的复杂度可以降为 `O(ni + nt)`。 ## 内存优化 @@ -149,7 +149,7 @@ for i := 0; i < 100; i++ { 在第 2 行的赋值的时候,程序会计算 itable;因此,在第 4 行执行的 `s.String()` 只需要执行几次内存查找和一次间接调用即可。 -与此相反,在像Smalltalk(或JavaScript、Python)这样的动态语言中,每次执行到第4行时程序都会进行方法查找,在一次次的循环中重复不必要的工作。前面提到的缓存可能会让起稍微快一些,但是它仍然不如一个间接调用指令。 +与此相反,在像 Smalltalk(或 JavaScript、Python)这样的动态语言中,每次执行到第 4 行时程序都会进行方法查找,在一次次的循环中重复不必要的工作。前面提到的缓存可能会让起稍微快一些,但是它仍然不如一个间接调用指令。 当然,这是一篇博客文章,我没有任何数字来支持这个讨论,但是像 Go 语言这样减少内存竞争可以很好的提高性能。另外,本文主要是介绍体系结构,而不是实现的细节,在实现的过程中可能会使用一些常量的优化。 diff --git a/published/tech/20180326-sync.RWMutex--Solving-readers-writers-problems.md b/published/tech/20180326-sync.RWMutex--Solving-readers-writers-problems.md index d13ebcdbd..433ae1757 100644 --- a/published/tech/20180326-sync.RWMutex--Solving-readers-writers-problems.md +++ b/published/tech/20180326-sync.RWMutex--Solving-readers-writers-problems.md @@ -54,7 +54,7 @@ func main() { var rs, ws int rsCh := make(chan int) wsCh := make(chan int) - go func() { + Go func() { for { select { case n := <-rsCh: @@ -69,11 +69,11 @@ func main() { wg := sync.WaitGroup{} for i := 0; i < 10; i++ { wg.Add(1) - go reader(rsCh, &m, &wg) + Go reader(rsCh, &m, &wg) } for i := 0; i < 3; i++ { wg.Add(1) - go writer(wsCh, &m, &wg) + Go writer(wsCh, &m, &wg) } wg.Wait() } @@ -111,7 +111,7 @@ R W ``` -当在临界区里面的 go routine 数量发生变化时,程序就会换行。这些输出可以体现 `RWMutex` “要么允许多个读者访问,要么允许一个写者访问”的特性。 +当在临界区里面的 Go routine 数量发生变化时,程序就会换行。这些输出可以体现 `RWMutex` “要么允许多个读者访问,要么允许一个写者访问”的特性。 还有一个重点是,一旦有一个写者调用 `Lock()` 后 ,后续试图访问临界区的读者将会被阻塞,如果当前已经有读者正在临界区内,则写者会等待这些读者离开临界区后,再锁定临界区。这个特性体现在程序的输出上,就是每次 W 出现的前几行, R 的数量都是逐个减少的。 @@ -160,7 +160,7 @@ return *addr 其中 `addr` 是 `*int32` 类型的而 `delta` 是 `int32` 类型的。由于这是个原子性的操作,所以增加 `delta` 不会有干扰到其它线程的风险。(更多关于 Fetch-and-add 的资料 [详见这里](https://en.wikipedia.org/wiki/Fetch-and-add)) -> 如果我们完全没有用到写者的话,`readerCount` 会一直大于或等于 0 (译注:后面会讲到,一旦有写者调用 `Lock` ,`Lock`函数就会把 `readerCount` 设置为负数),并且读者获取锁的过程会走较快的非阻塞的分支,因为这时候读者获取锁的过程只涉及到 `atomic.AddInt32` 的调用。 +> 如果我们完全没有用到写者的话,`readerCount` 会一直大于或等于 0 (译注:后面会讲到,一旦有写者调用 `Lock` ,`Lock` 函数就会把 `readerCount` 设置为负数),并且读者获取锁的过程会走较快的非阻塞的分支,因为这时候读者获取锁的过程只涉及到 `atomic.AddInt32` 的调用。 ### 信号量(semaphore) @@ -192,9 +192,9 @@ func (rw *RWMutex) Lock() { `Lock` 方法让写者可以获得对共享数据的**独占**访问权: -首先它会获取一个叫 `w` 的互斥量(mutex),这会使得其它的写者无法访问这个共享数据,这个`w` 只有在 `Unlock` 函数快结束的时候,才会被解锁,**从而保证一次最多只能有一个写者进入临界区。** +首先它会获取一个叫 `w` 的互斥量(mutex),这会使得其它的写者无法访问这个共享数据,这个 `w` 只有在 `Unlock` 函数快结束的时候,才会被解锁,**从而保证一次最多只能有一个写者进入临界区。** -然后 `Lock` 方法会把 `readerCount` 的值设置成负数,(通过把`readerCount` 减掉 `rwmutexMaxReaders`(即`1 << 30`))。然后接下来任何读者调用 `RLock` 函数时,都会被阻塞掉了: +然后 `Lock` 方法会把 `readerCount` 的值设置成负数,(通过把 `readerCount` 减掉 `rwmutexMaxReaders`(即 `1 << 30`))。然后接下来任何读者调用 `RLock` 函数时,都会被阻塞掉了: ```go if atomic.AddInt32(&rw.readerCount, 1) < 0 { @@ -209,7 +209,7 @@ if atomic.AddInt32(&rw.readerCount, 1) < 0 { ### rwmutexMaxReaders -(译注:原文大量使用的 **pending** 这个词常常被翻译为「挂起」(有暂停的语义),但是在本文中,pending 表示的是「等待进入临界区(这时是线程是暂停的)或者正在临界区里面(这时是线程正在运行的)」这个状态。「挂起」不能很好的表达该语义,所以 pending 保留原文不翻译,但读者要注意 pending 在本文的语义,**例如:「一个 pending 的读者」可以理解为是一个调用了 `RLock` 函数但是还没调用 `RUnlock` 函数的读者。「一个 pending 的写者」则相应地表示一个调用了`Lock` 函数但是还没调用 `Unlock` 函数的写者**) +(译注:原文大量使用的 **pending** 这个词常常被翻译为「挂起」(有暂停的语义),但是在本文中,pending 表示的是「等待进入临界区(这时是线程是暂停的)或者正在临界区里面(这时是线程正在运行的)」这个状态。「挂起」不能很好的表达该语义,所以 pending 保留原文不翻译,但读者要注意 pending 在本文的语义,**例如:「一个 pending 的读者」可以理解为是一个调用了 `RLock` 函数但是还没调用 `RUnlock` 函数的读者。「一个 pending 的写者」则相应地表示一个调用了 `Lock` 函数但是还没调用 `Unlock` 函数的写者**) 在 [rwmutex.go](https://github.com/golang/go/blob/718d6c5880fe3507b1d224789b29bc2410fc9da5/src/sync/rwmutex.go) 里面有一个常量: @@ -277,7 +277,7 @@ func (rw *RWMutex) Unlock() { 要解锁写者拥有的写锁,首先 `readerCount` 的值要增加 `rwmutexMaxReaders`,这个操作会使得 `readerCount` 恢复成非负数,如果这时候 `readerCount` 大于 0,这意味着当前有读者在等待着写者离开临界区。最后写者释放掉它拥有的 `w` 这个互斥量(译注:上文说过,这个互斥量是写者用来防止**其它写者**进入临界区的),这使得其它写者能够有机会再次锁定 `w` 这个互斥量。 -如果读者或写者尝试在一个已经解锁的 RWMutex 上调用`Unlock` 和 `RUnlock` 方法会抛出错误([代码](https://play.golang.org/p/YMdFET74olU)): +如果读者或写者尝试在一个已经解锁的 RWMutex 上调用 `Unlock` 和 `RUnlock` 方法会抛出错误([代码](https://play.golang.org/p/YMdFET74olU)): ```go m := sync.RWMutex{} @@ -295,7 +295,7 @@ fatal error: sync: Unlock of unlocked RWMutex 文档里面写道: -如果一个 goroutine 拥有一个读锁,而另外一个 goroutine 又调用了 `Lock` 函数,那么在第一个读锁被释放之前,没有读者可以获得读锁。这尤其限制了我们不能递归地获取读锁,因为只有这样才能确保锁都能变得可用,一个 `Lock` 的调用会阻止新的读者获取到读锁。(上文已经多次提到这一点了) +如果一个 Goroutine 拥有一个读锁,而另外一个 Goroutine 又调用了 `Lock` 函数,那么在第一个读锁被释放之前,没有读者可以获得读锁。这尤其限制了我们不能递归地获取读锁,因为只有这样才能确保锁都能变得可用,一个 `Lock` 的调用会阻止新的读者获取到读锁。(上文已经多次提到这一点了) 因为 RWMutex 就是这么实现的:如果当前有一个 pending 的写者,那么所有尝试调用 `RLock` 的读者都会被阻塞,即使在这之前已经有读者获取到了读锁([源代码](https://play.golang.org/p/XNndlaZ6Ema)): @@ -322,7 +322,7 @@ func f(n int) int { } func main() { done := make(chan int) - go func() { + Go func() { time.Sleep(200 * time.Millisecond) fmt.Println("Lock") m.Lock() @@ -346,7 +346,7 @@ RLock fatal error: all goroutines are asleep - deadlock! ``` -(译注:上面的代码有两个 goroutine,一个是写者 routine,一个是主 goroutine(也是读者),通过程序的输出可以知道:前三行都是输出 RLock,表示这时候已经有 3 个读者获取到了读锁。后面接着输出了 Lock, 表示这时候写者开始请求写锁,后面接着输出一个 RLock,表示这时又多了一个读者请求读锁。因为 pending 的写者会阻塞掉后续调用 `RLock` 的读者,所以最后一个 RLock 的调用堵塞了主 routine,而写者的 routine 也在堵塞等待前面三个读者释放它们的读锁,所以两个 goroutine 都堵塞了,因此程序报错:`fatal error: all goroutines are asleep - deadlock!`) +(译注:上面的代码有两个 goroutine,一个是写者 routine,一个是主 goroutine(也是读者),通过程序的输出可以知道:前三行都是输出 RLock,表示这时候已经有 3 个读者获取到了读锁。后面接着输出了 Lock, 表示这时候写者开始请求写锁,后面接着输出一个 RLock,表示这时又多了一个读者请求读锁。因为 pending 的写者会阻塞掉后续调用 `RLock` 的读者,所以最后一个 RLock 的调用堵塞了主 routine,而写者的 routine 也在堵塞等待前面三个读者释放它们的读锁,所以两个 Goroutine 都堵塞了,因此程序报错:`fatal error: all goroutines are asleep - deadlock!`) ### 锁的拷贝 @@ -372,7 +372,7 @@ func main() { var mu sync.Mutex runtime.SetMutexProfileFraction(5) for i := 0; i < 10; i++ { - go func() { + Go func() { for { mu.Lock() time.Sleep(100 * time.Millisecond) @@ -385,14 +385,14 @@ func main() { ``` ```bash -> go build mutexcontention.go +> Go build mutexcontention.go > ./mutexcontention ``` 当程序 mutexcontention 运行时: ``` -> go tool pprof mutexcontention http://localhost:8888/debug/pprof/mutex?debug=1 +> Go tool pprof mutexcontention http://localhost:8888/debug/pprof/mutex?debug=1 Fetching profile over HTTP from http://localhost:8888/debug/pprof/mutex?debug=1 Saved profile in /Users/mlowicki/pprof/pprof.mutexcontention.contentions.delay.003.pb.gz File: mutexcontention @@ -403,7 +403,7 @@ Total: 57.28s ROUTINE ======================== main.main.func1 in /Users/mlowicki/projects/golang/src/github.com/mlowicki/mutexcontention/mutexcontention.go 0 57.28s (flat, cum) 100% of Total . . 14: for i := 0; i < 10; i++ { -. . 15: go func() { +. . 15: Go func() { . . 16: for { . . 17: mu.Lock() . . 18: time.Sleep(100 * time.Millisecond) @@ -417,7 +417,7 @@ ROUTINE ======================== main.main.func1 in /Users/mlowicki/projects/gol 上面的 57.28s 是什么,它为什么挨着 `mu.Unlock()` 呢? -当 goroutine 因为调用 `Lock` 方法而被阻塞的时候,这个时间点会被记录下来——aquiretime(获取时间)。当其他 goroutine 解锁了这个锁,并且起码有一个 goroutine 在等待获取这个锁的时候。其中一个 goroutine 可以获取到这个锁,这时他会自动调用 `mutexevent` 函数。函数 `mutexevent` 根据 `SetMutexProfileFraction` 函数设定的比率,来确定是否应该保存或忽略掉该事件。这种事件都包含了等待时间(当前时间 - 获取时间)。上述的代码中,所有阻塞在这个锁的 goroutine 的总等待时间会被收集和显示出来, +当 Goroutine 因为调用 `Lock` 方法而被阻塞的时候,这个时间点会被记录下来——aquiretime(获取时间)。当其他 Goroutine 解锁了这个锁,并且起码有一个 Goroutine 在等待获取这个锁的时候。其中一个 Goroutine 可以获取到这个锁,这时他会自动调用 `mutexevent` 函数。函数 `mutexevent` 根据 `SetMutexProfileFraction` 函数设定的比率,来确定是否应该保存或忽略掉该事件。这种事件都包含了等待时间(当前时间 - 获取时间)。上述的代码中,所有阻塞在这个锁的 Goroutine 的总等待时间会被收集和显示出来, 对于读锁(Rlock 和 `RUnlock`)争用的分析功能,将会在 Go 1.11 版本加入 ([patch 补丁](https://github.com/golang/go/commit/88ba64582703cea0d66a098730215554537572de)) @@ -425,7 +425,7 @@ ROUTINE ======================== main.main.func1 in /Users/mlowicki/projects/gol via: https://medium.com/golangspec/sync-rwmutex-ca6c6c3208a0 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[Alex-liutao](https://github.com/Aelx-liutao) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20180327-golang-HTTP-server-for-pro.md b/published/tech/20180327-golang-HTTP-server-for-pro.md index a9e9a7ee9..081e7a1d9 100644 --- a/published/tech/20180327-golang-HTTP-server-for-pro.md +++ b/published/tech/20180327-golang-HTTP-server-for-pro.md @@ -46,14 +46,14 @@ func main() { ## 使用 `Alice` 来管理我们的中间件 -如果您使用网络服务器软件包,[中间件模式](https://en.wikipedia.org/wiki/Middleware)非常常见。 如果您还没有看到它,您应该在 201 5年 Golang UK Conference 上观看Mat Ryer 的视频,了解中间件的强大功能。([完整的博客文章在这里](https://medium.com/@matryer/writing-middleware-in-golang-and-how-go-makes-it-so-much-fun-4375c1246e81)) +如果您使用网络服务器软件包,[中间件模式](https://en.wikipedia.org/wiki/Middleware)非常常见。 如果您还没有看到它,您应该在 201 5 年 Golang UK Conference 上观看 Mat Ryer 的视频,了解中间件的强大功能。([完整的博客文章在这里](https://medium.com/@matryer/writing-middleware-in-golang-and-how-go-makes-it-so-much-fun-4375c1246e81)) 视频链接:https://youtu.be/tIm8UkSf6RA 另一篇关于中间件模式的文章[http://www.alexedwards.net/blog/making-and-using-middleware](http://www.alexedwards.net/blog/making-and-using-middleware) 正如作者的描述([Github](https://github.com/justinas/alice)): -> `Alice` 提供了一种便捷的方式来链接您的HTTP中间件功能和应用程序处理程序。 +> `Alice` 提供了一种便捷的方式来链接您的 HTTP 中间件功能和应用程序处理程序。 简单说,它把 @@ -119,7 +119,7 @@ Alice 使中间件无处不在! ## HTTP 服务器不错,但 HTTPS 服务器更好! -使用 `Let's Encrypt` 服务,简单快捷的创建一个安全的HTTP服务器 。 `Let's Encrypt` 使用 [ACME协议](https://en.wikipedia.org/wiki/Automated_Certificate_Management_Environment) 来验证您是否控制指定的域名并向您颁发证书。 这就是所谓的认证,是的,有一个自动认证软件包:[acme / autocert](https://godoc.org/golang.org/x/crypto/acme/autocert) +使用 `Let's Encrypt` 服务,简单快捷的创建一个安全的 HTTP 服务器 。 `Let's Encrypt` 使用 [ACME 协议](https://en.wikipedia.org/wiki/Automated_Certificate_Management_Environment) 来验证您是否控制指定的域名并向您颁发证书。 这就是所谓的认证,是的,有一个自动认证软件包:[acme / autocert](https://godoc.org/golang.org/x/crypto/acme/autocert) ```go m := autocert.Manager{ diff --git a/published/tech/20180330-Go-On-Very-Small-Hardware-Part1.md b/published/tech/20180330-Go-On-Very-Small-Hardware-Part1.md index ba3b86a33..52fb0742b 100644 --- a/published/tech/20180330-Go-On-Very-Small-Hardware-Part1.md +++ b/published/tech/20180330-Go-On-Very-Small-Hardware-Part1.md @@ -249,7 +249,7 @@ func main() { } ``` -代码改动很小:第二个 LED 被添加,前面的 *main* 函数被重命名为 *blinky*,函数需要两个参数。*Main* 在一个新的 goroutine 中启动第一个 *blinky* 函数,这样两个 LED 同时 *并行* 运行。有必要提一下,*gpio.Pin* 类型支持并发访问在同一 GPIO 口的不同引脚。 +代码改动很小:第二个 LED 被添加,前面的 *main* 函数被重命名为 *blinky*,函数需要两个参数。*Main* 在一个新的 Goroutine 中启动第一个 *blinky* 函数,这样两个 LED 同时 *并行* 运行。有必要提一下,*gpio.Pin* 类型支持并发访问在同一 GPIO 口的不同引脚。 Emgo 仍然还有许多缺点。其中一个就是你必须提前对 goroutines(tasks)指定一个最大数值。是时候编辑一下 *script.Id* 了: @@ -273,7 +273,7 @@ $ arm-none-eabi-size cortexm0.elf 10020 172 172 10364 287c cortexm0.elf ``` -另外一个 LED 和 goroutine 花费了 248 字节的 Flash 空间。 +另外一个 LED 和 Goroutine 花费了 248 字节的 Flash 空间。 ![STM32F030F4P6](https://ziutek.github.io/images/mcu/f030-demo-board/goroutines.png) @@ -435,7 +435,7 @@ $ arm-none-eabi-size cortexm0.elf 这个新的例子占用了 11324 字节的 Flash 空间,比之前的多了 1132 字节。 -使用当前的时序,两个 *blinky* goroutines 从 channel 消费的速度比 *timerISR* 发送给它的速度快得多。因此,它们同时等待新数据到来,你可以观察到 [Go规范](https://golang.org/ref/spec#Select_statements) 所要求的 *select* 的随机性。 +使用当前的时序,两个 *blinky* goroutines 从 channel 消费的速度比 *timerISR* 发送给它的速度快得多。因此,它们同时等待新数据到来,你可以观察到 [Go 规范](https://golang.org/ref/spec#Select_statements) 所要求的 *select* 的随机性。 ![STM32F030F4P6](https://ziutek.github.io/images/mcu/f030-demo-board/channels1.png) @@ -457,7 +457,7 @@ Goroutines 和 channels 是很棒很便捷的语法。你可以用你自己的 via: https://ziutek.github.io/2018/03/30/go_on_very_small_hardware.html -作者:[Michał Derkacz ](https://ziutek.github.io) +作者:[Micha ł Derkacz ](https://ziutek.github.io) 译者:[PotoYang](https://github.com/PotoYang) 校对:[DingdingZhou](https://blog.zhoudingding.com) diff --git a/published/tech/20180330-Go-On-Very-Small-Hardware-Part2.md b/published/tech/20180330-Go-On-Very-Small-Hardware-Part2.md index 38f472b6f..f08dcb4c0 100644 --- a/published/tech/20180330-Go-On-Very-Small-Hardware-Part2.md +++ b/published/tech/20180330-Go-On-Very-Small-Hardware-Part2.md @@ -332,7 +332,7 @@ parity is : none databits are : 8 stopbits are : 1 escape is : C-a -local echo is : no +local Echo is : no noinit is : no noreset is : no hangup is : no @@ -522,7 +522,7 @@ echo "Hello, World!" > file.txt `>` 运算符将前一个命令的输出流写入文件。还有 `|` 运算符连接相邻命令的输出和输入流。 -通过流,我们可以轻松转换或过滤任何命令的输出。例如,要将所有字母转换为大写,我们可以通过 *tr* 命令过滤 echo 的输出: +通过流,我们可以轻松转换或过滤任何命令的输出。例如,要将所有字母转换为大写,我们可以通过 *tr* 命令过滤 Echo 的输出: ```bash echo "Hello, World!" | tr a-z A-Z > file.txt @@ -832,7 +832,7 @@ $ arm-none-eabi-size cortexm0.elf Flash 只剩下 140 个空闲字节。让我们使用启用了 semihosting 的 OpenOCD 加载它: ```bash -$ openocd -d0 -f interface/stlink.cfg -f target/stm32f0x.cfg -c 'init; program cortexm0.elf; arm semihosting enable; reset run' +$ openocd -d0 -f interface/stlink.cfg -f target/stm32f0x.cfg -c 'init; program cortexm0.elf; ARM semihosting enable; reset run' Open On-Chip Debugger 0.10.0+dev-00319-g8f1f912a (2018-03-07-19:20) Licensed under GNU GPL v2 For bug reports, read @@ -893,7 +893,7 @@ type(*p) = S via: https://ziutek.github.io/2018/04/14/go_on_very_small_hardware2.html -作者:[Michał Derkacz ](https://ziutek.github.io) +作者:[Micha ł Derkacz ](https://ziutek.github.io) 译者:[PotoYang](https://github.com/PotoYang) 校对:[zhoudingding](https://github.com/dingdinzhou) diff --git a/published/tech/20180330-Go-On-Very-Small-Hardware-Part3.md b/published/tech/20180330-Go-On-Very-Small-Hardware-Part3.md new file mode 100644 index 000000000..3c5f5c710 --- /dev/null +++ b/published/tech/20180330-Go-On-Very-Small-Hardware-Part3.md @@ -0,0 +1,400 @@ +首发于:https://studygolang.com/articles/24877 + +# Go 最小硬件编程(第三部分) + +[![STM32F030F4P6](https://ziutek.github.io/images/mcu/f030-demo-board/board.jpg)](https://ziutek.github.io/2018/05/03/go_on_very_small_hardware3.html) + +本系列的第一部分和第二部分中讨论的大多数示例都是以一种或另一种方式闪烁 LED。起初它可能很有趣,但过了一段时间它变得有点无聊。让我们做一些更有趣的事情...... + +......让我们点亮更多 LED! + +## WS281x LEDs + +[WS281x](http://www.world-semi.com/solution/list-4-1.html) RGB LED(和它们的克隆版)非常的流行。你可以将它们作为单个元素购买、链接成长条或组装成矩阵、环或其他形状。 + +![WS2812B](https://ziutek.github.io/images/led/ws2812b.jpg) + +它们可以串联连接,由于这个原因,你可以通过 MCU 的单个引脚控制长 LED 条。不幸的是,它们的内部控制器使用的物理协议并不适合你可以在 MCU 中找到的任何外设。你必须使用 bit-banging 或以不寻常的方式使用可用的外设。 + +哪种可用解决方案最有效,取决于同时控制的 LED 灯条的数量。如果你必须驱动 4 到 16 个条带,最有效的方法是 [使用定时器和 DMA](http://www.martinhubacek.cz/arm/improved-stm32-ws2812b-library)(不要忽视 Martin 的文章末尾的链接)。 + +如果你只需要控制一个或两个条带,请使用可用的 SPI 或 UART 外设。对于 SPI,你只能在发送的一个字节中编码两个 WS281x 位。通过巧妙地使用起始位和停止位,UART 允许更密集的编码:每个字节发送 3 位。 + +我在 [这个网站](http://mikrokontrolery.blogspot.com/2011/03/Diody-WS2812B-sterowanie-XMega-cz-2.html) 上找到的 UART 协议如何适合 WS281x 协议的最佳解释。如果你不懂波兰语,这里是 [英文翻译版](https://translate.google.pl/translate?sl=pl&tl=en&u=http://mikrokontrolery.blogspot.com/2011/03/Diody-WS2812B-sterowanie-XMega-cz-2.html)。 + +基于 WS281x 的 LED 仍然是最受欢迎的,但市场上也有 SPI 控制的 LED:APA102](http://neon-world.com/en/product.php),[SK9822](http://www.normandled.com/index.php/Product/view/id/800.html)。关于它们的三篇有趣的文章:[1](https://cpldcpu.wordpress.com/2014/08/27/apa102/),[2](https://cpldcpu.wordpress.com/2014/11/30/understanding-the-apa102-superled/),[3](https://cpldcpu.wordpress.com/2016/12/13/sk9822-a-clone-of-the-apa102/)。 + +## LED 环 + +市场上有许多基于 WS2812 的环状 LED。我弄了这个: + +![WS2812B](https://ziutek.github.io/images/led/rgbring.jpg) + +它有 24 个可单独寻址的 RGB LED(WS2812B),并有四个端子:GND、5V、DI 和 DO。你可以通过将 DI(数据输入)终端连接到前一个终端的 DO(数据输出)终端来链接更多环或其他基于 WS2812 的东西。 + +让我们将这个环连接到我们的 STM32F030 开发板上。我们将使用基于 UART 的驱动器,因此 DI 应连接到 UART 接头上的 TXD 引脚。WS2812B LED 需要至少 3.5V 电源。24 个 LED 可以消耗相当多的电流,因此在编程/调试过程中,最好将环上的 GND 和 5V 端子直接连接到 ST-LINK 编程器上的 GND 和 5V 引脚: + +![WS2812B](https://ziutek.github.io/images/led/ring-stlink-f030.jpg) + +我们的 STM32F030F4P6 MCU 和整个 STM32 F0、F3、F7、L4 系列有一个重要的东西,而 F1、F4、L1 MCU 没有:它允许反转 UART 信号,因此我们可以将环直接连接到 UART TXD 引脚。如果你不知道我们需要这样的反转,你可能没有阅读我上面提到的 [文章](https://translate.google.pl/translate?sl=pl&tl=en&u=http://mikrokontrolery.blogspot.com/2011/03/Diody-WS2812B-sterowanie-XMega-cz-2.html)。 + +所以你不能用这种方式使用 [Blue Pill](https://jeelabs.org/article/1649a/) 或 [STM32F4-DISCOVERY](http://www.st.com/en/evaluation-tools/stm32f4discovery.html) 。使用 SPI 外设或外部逆变器。请参阅 [Christmas Tree Lights](https://github.com/ziutek/emgo/tree/master/egpath/src/stm32/examples/minidev/treelights) 项目作为 UART + 逆变器的示例或使用 SPI 的 NUCLEO-F411RE 的 [WS2812 示例](https://github.com/ziutek/emgo/tree/master/egpath/src/stm32/examples/nucleo-f411re/ws2812)。 + +顺便说一句,可能大多数 DISCOVERY 开发板都有一个问题:它们工作在 VDD = 3V 而不是 3.3V。对于 DI 高电平,WS281x 至少需要 0.7 倍 *供给电压*。对于 5V 电源就是 3.5V,如果是 4.7V,则可以在 DISCOVERY 的 5V 引脚上找到 3.3V。如你所见,即使在我们的情况下,第一个 LED 工作电压低于额定电压 0.2V。在 DISCOVERY 的情况下,如果供电 4.7V,则工作电压低于额定电压 0.3V,如果供电 5V,则工作电压低于额定电压 0.5V。 + +让我们结束这个冗长的介绍并转到代码: + +```go +package main + +import ( + "delay" + "math/rand" + "rtos" + + "led" + "led/ws281x/wsuart" + + "stm32/hal/dma" + "stm32/hal/gpio" + "stm32/hal/irq" + "stm32/hal/system" + "stm32/hal/system/timer/systick" + "stm32/hal/usart" +) + +var tts *usart.Driver + +func init() { + system.SetupPLL(8, 1, 48/8) + systick.Setup(2e6) + + gpio.A.EnableClock(true) + tx := gpio.A.Pin(9) + + tx.Setup(&gpio.Config{Mode: gpio.Alt}) + tx.SetAltFunc(gpio.USART1_AF1) + + d := dma.DMA1 + d.EnableClock(true) + + tts = usart.NewDriver(usart.USART1, d.Channel(2, 0), nil, nil) + tts.Periph().EnableClock(true) + tts.Periph().SetBaudRate(3000000000 / 1390) + tts.Periph().SetConf2(usart.TxInv) + tts.Periph().Enable() + tts.EnableTx() + + rtos.IRQ(irq.USART1).Enable() + rtos.IRQ(irq.DMA1_Channel2_3).Enable() +} + +func main() { + var rnd rand.XorShift64 + rnd.Seed(1) + rgb := wsuart.GRB + strip := wsuart.Make(24) + black := rgb.Pixel(0) + for { + c := led.Color(rnd.Uint32()).Scale(127) + pixel := rgb.Pixel(c) + for i := range strip { + strip[i] = pixel + tts.Write(strip.Bytes()) + delay.Millisec(40) + } + for i := range strip { + strip[i] = black + tts.Write(strip.Bytes()) + delay.Millisec(20) + } + } +} + +func ttsISR() { + tts.ISR() +} + +func ttsDMAISR() { + tts.TxDMAISR() +} + +//c:__attribute__((section(".ISRs"))) +var ISRs = [...]func(){ + irq.USART1: ttsISR, + irq.DMA1_Channel2_3: ttsDMAISR, +} +``` + +### *import* 区域 + +对比之前的例子来说,*import* 区域新增的是 *rand/math* 包和 *led* 包及 *led/ws281x* 子包。*led* 包本身包含 *Color* 类型的定义。*led/ws281x/wsuart* 定义了 *ColorOrder*、*Pixel* 和 *Strip* 类型。 + +我想知道从 *image/color* 使用 *Color* 或是 *RGBA* 类型以及如何定义 *Strip*,它将实现 *image.Image* 接口,但是由于使用了 [伽马校正](https://en.wikipedia.org/wiki/Gamma_correction) 和 *image/draw* 大包,所以我简单的实现了: + +```go +type Color uint32 +type Strip []Pixel +``` + +同时加入一些有用的方法。然而,这个在未来是可以改变的。 + +### *init* 函数 + +*init* 函数没有太多新奇之处。UART 波特率从 115200 变为 3000000000/1390 ≈ 2158273,相当于每个 WS2812 位耗费 1390 纳秒。CR2 寄存器中的 *TxInv* 位设置为反转 TXD 信号。 + +### *main* 函数 + +*XorShift64* 伪随机数生成器用于生成随机颜色。[XORSHIFT](https://en.wikipedia.org/wiki/Xorshift) 是目前 *math/rand* 包实现的唯一算法。你必须使用带有非零参数的 *Seed* 方法显式地初始化它。 + +*rgb* 变量的类型为 *wsuart.ColorOrder*,并设置为 WS2812 使用的 GRB 颜色顺序(WS2811 使用 RGB 顺序)。然后它用于将颜色转换为像素。 + +`wsuart.Make(24)` 创建了 24 像素的初始化条带。它相当于: + +```go +strip := make(wsuart.Strip, 24) +strip.Clear() +``` + +其余代码使用随机颜色绘制类似于 “Please Wait ...” 微调器的内容。 + +*strip* 切片充当帧缓冲区。`tts.Write(strip.Bytes())` 将帧缓冲区的内容发送到环。 + +### 中断 + +该程序使用处理中断的代码,与前一个 [UART 示例](https://ziutek.github.io/2018/04/14/go_on_very_small_hardware2.html#uart) 相同。 + +让我们编译并运行: + +```bash +$ egc +$ arm-none-eabi-size cortexm0.elf + text data bss dec hex filename + 14088 240 204 14532 38c4 cortexm0.elf +$ openocd -d0 -f interface/stlink.cfg -f target/stm32f0x.cfg -c 'init; program cortexm0.elf; reset run; exit' +``` + +我已经跳过了 openod 的输出。下面这个视频展示了这个程序是如何运行的: + + + +## 让我们来做一点有用的事...... + +在第一部分的开头,我问过:“我们使用 Go 最低能到多低,仍然做一些有用的事情?”。我们的 MCU 实际上是一个低端设备(8 位可能我的还是不赞同的),但到目前为止我们还没有做任何有用的事情。 + +那么......让我们做一些有用的事情......*让我们制作一个时钟吧*! + +互联网上有许多由 RGB LED 构成的时钟示例。让我们自己使用我们的小开发板和 RGB 环。我们更改以前的代码,如下所述。 + +### *import* 区域 + +去掉 *math/rand* 包并添加 *stm32/hal/exti*。 + +### 全局变量 + +添加两个新的全局变量:*btn* 和 *btnev*: + +```go +var ( + tts *usart.Driver + btn gpio.Pin + btnev rtos.EventFlag +) +``` + +它们将会被用于处理 button,用于设置我们的时钟。我们的开发板没有重置按钮,但是一定层度上我们可以不用它也能管理。 + +### *init* 函数 + +把这个代码添加到 *init* 函数中: + +```go +btn = gpio.A.Pin(4) + +btn.Setup(&gpio.Config{Mode: gpio.In, Pull: gpio.PullUp}) +ei := exti.Lines(btn.Mask()) +ei.Connect(btn.Port()) +ei.EnableFallTrig() +ei.EnableRiseTrig() +ei.EnableIRQ() + +rtos.IRQ(irq.EXTI4_15).Enable() +``` + +PA4 引脚被配置为输入,并使能内部上拉电阻。它连接到板载 LED,但不会妨碍任何事情。更重要的是它位于 GND 引脚旁边,因此我们可以使用任何金属物体来模拟按钮并设置时钟。作为奖励,我们可以从板载 LED 获得额外的反馈。 + +我们使用 EXTI 外设来跟踪 PA4 状态。它被配置为在任何更改时生成中断。 + +### *btnWait* 函数 + +定义一个新的辅助函数: + +```go +func btnWait(state int, deadline int64) bool { + for btn.Load() != state { + if !btnev.Wait(1, deadline) { + return false // timeout + } + btnev.Reset(0) + } + delay.Millisec(50) // debouncing + return true +} +``` + +它等待 button 引脚上的指定状态,但仅在 *deadline* 出现之前。这是略微改进的轮询代码: + +```go +for btn.Load() != state { + if rtos.Nanosec() >= deadline { + // timeout + } +} +``` + +我们的 *btnWait* 函数,不是忙于等待 *state* 或 *deadline*,而是使用类型为 *rtos.EventFlag* 的 *btnev* 变量来睡眠,直到发生某些事情。你当然可以使用 channel 而不是 *rtos.EventFlag*,但后者要便宜得多。 + +### *main* 函数 + +我们需要完全新的 *main* 函数: + +```go +func main() { + rgb := wsuart.GRB + strip := wsuart.Make(24) + ds := 4 * 60 / len(strip) // Interval between LEDs (quarter-seconds). + adjust := 0 + adjspeed := ds + for { + qs := int(rtos.Nanosec() / 25e7) // Quarter-seconds since reset. + qa := qs + adjust + + qa %= 12 * 3600 * 4 // Quarter-seconds since 0:00 or 12:00. + hi := len(strip) * qa / (12 * 3600 * 4) + + qa %= 3600 * 4 // Quarter-seconds in the current hour. + mi := len(strip) * qa / (3600 * 4) + + qa %= 60 * 4 // Quarter-seconds in the current minute. + si := len(strip) * qa / (60 * 4) + + hc := led.Color(0x550000) + mc := led.Color(0x005500) + sc := led.Color(0x000055) + + // Blend the colors if the hands of the clock overlap. + if hi == mi { + hc |= mc + mc = hc + } + if mi == si { + mc |= sc + sc = mc + } + if si == hi { + sc |= hc + hc = sc + } + + // Draw the clock and write to the ring. + strip.Clear() + strip[hi] = rgb.Pixel(hc) + strip[mi] = rgb.Pixel(mc) + strip[si] = rgb.Pixel(sc) + tts.Write(strip.Bytes()) + + // Sleep until the button pressed or the second hand should be moved. + if btnWait(0, int64(qs+ds)*25e7) { + adjust += adjspeed + // Sleep until the button is released or timeout. + if !btnWait(1, rtos.Nanosec()+100e6) { + if adjspeed < 5*60*4 { + adjspeed += 2 * ds + } + continue + } + adjspeed = ds + } + } +} +``` + +我们使用 *rtos.Nanosec* 函数而不是 *time.Now* 来获取当前时间。这节省了大量的 Flash 空间,但也让我们的时钟减弱为原始设备,不知道几天、几个月和几年,最糟糕的是它不处理夏令时的变化。 + +我们的环有 24 个 LED,因此秒针的精度可达 2.5 秒。为了不牺牲这种精度并获得平稳运行,我们使用四分之一秒作为基本间隔。半秒就足够了,但是四分之一秒更准确,并且适用于 16 和 48 个 LED。 + +红色、绿色和蓝色分别用于时针、分针和秒针。这允许我们使用简单的逻辑或操作进行颜色混合。我们有 *Color.Blend* 方法可以混合任意颜色,但我们的 Flash 空间很小,所以我们更喜欢最简单的解决方案。 + +我们只在秒针移动时才重绘时钟: + +```go +btnWait(0,int64(qs + ds)* 25e7) +``` + +正在等待那个时刻或按下按钮。 + +每次按下按钮都会向前调整时钟。按住按钮一段时间后会有加速。 + +### 中断 + +定义新的中断处理程序: + +```go +func exti4_15ISR() { + pending := exti.Pending() & 0xFFF0 + pending.ClearPending() + if pending&exti.Lines(btn.Mask()) != 0 { + btnev.Signal(1) + } +} +``` + +同时添加 `irq.EXTI4_15: exti4_15ISR`,进入 *ISR* 数组。 + +此处理程序(或中断服务程序)处理 EXTI4_15 IRQ。Cortex-M0 CPU 支持的 IRQ 明显少于其兄弟,因此你经常可以看到一个 IRQ 由多个中断源共享。在我们的例子中,12 个 EXTI 线共享一个 IRQ。 + +*exti4_15ISR* 读取所有 *pending* 位并选择其中 12 个更重要的位。接下来,它清除 EXTI 中的选择的位并开始处理它们。在我们的例子中,只检查第 4 位。`btnev.Signal(1)` 导致 `btnev.Wait(1, deadline)` 唤醒并返回 *true*。 + +你可以在 [Github](https://github.com/ziutek/emgo/tree/master/egpath/src/stm32/examples/f030-demo-board/ws2812-clock) 上找到完整的代码。我们来编译它: + +```bash +$ egc +$ arm-none-eabi-size cortexm0.elf + text data bss dec hex filename + 15960 240 216 16416 4020 cortexm0.elf +``` + +任何 iprovements 只有 184 个字节。让我们再次重建所有内容,但这次没有 typeinfo 中的任何类型和字段名称: + +```bash +$ cd $HOME/emgo +$ ./clean.sh +$ cd $HOME/firstemgo +$ egc -nf -nt +$ arm-none-eabi-size cortexm0.elf + text data bss dec hex filename + 15120 240 216 15576 3cd8 cortexm0.elf +``` + +现在,利用一千字节的空闲空间,你可以做一些事情了。让我们来看看程序如何运行的: + + +我不知道我怎么才能准确的显示 3:00!? + +这就是全部了,大兄弟(大妹子)! 在第 4 部分(结束本系列)中,我们将尝试在 LCD 上显示某些内容。 + +--- + +via: https://ziutek.github.io/2018/05/03/go_on_very_small_hardware3.html + +作者:[Micha ł Derkacz](https://ziutek.github.io) +译者:[PotoYang](https://github.com/PotoYang) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20180410-Go-testing-style-guide.md b/published/tech/20180410-Go-testing-style-guide.md index 973bb4c22..27b7c5027 100644 --- a/published/tech/20180410-Go-testing-style-guide.md +++ b/published/tech/20180410-Go-testing-style-guide.md @@ -25,7 +25,7 @@ for _, tt := range tests { ## 使用子测试 -使用子测试可以从 table 中运行一个单独的测试,且可以容易的看出哪个测试完全失败了。由于子测试是比较新的版本( Go 1.7,2016年10月),所以许多现存的测试不能使用它们(子测试)。 +使用子测试可以从 table 中运行一个单独的测试,且可以容易的看出哪个测试完全失败了。由于子测试是比较新的版本( Go 1.7,2016 年 10 月),所以许多现存的测试不能使用它们(子测试)。 如果测试内容很明显,我倾向于简单地使用测试编号;如果不明显或有很多测试用例,就添加一个测试名。 @@ -136,7 +136,7 @@ t.Run(name, func(t *testing.T) { want: "this string" ``` -注意 `got:` 后面的俩个空格,是为了和 `want` 对齐的。如果我使用 `expected` 就要使用6个空格。 +注意 `got:` 后面的俩个空格,是为了和 `want` 对齐的。如果我使用 `expected` 就要使用 6 个空格。 我还倾向于使用 `%q` 或 `%#v`,因为这会很清楚的显示后面的空白或不可打印字符。 diff --git a/published/tech/20180410-the-Good-the-Bad-and-the-Ugly.md b/published/tech/20180410-the-Good-the-Bad-and-the-Ugly.md index ab0b6a372..4693c3600 100644 --- a/published/tech/20180410-the-Good-the-Bad-and-the-Ugly.md +++ b/published/tech/20180410-the-Good-the-Bad-and-the-Ugly.md @@ -2,11 +2,11 @@ # Go 语言的优点,缺点和令人厌恶的设计 -这是关于 「[Go是一门设计糟糕的编程语言 (Go is not good)](https://github.com/ksimka/go-is-not-good)」 系列的另一篇文章。Go 确实有一些很棒的特性,所以我在这篇文章中展示了它的优点。但是总体而言,当超过 API 或者网络服务器(这也是它的设计所在)的范畴,用 Go 处理商业领域的逻辑时,我感觉它用起来麻烦而且痛苦。就算在网络编程方面,Go 的设计和实现也存在诸多问题,这使它看上去简单实际则暗藏危险。 +这是关于 「[Go 是一门设计糟糕的编程语言 (Go is not good)](https://github.com/ksimka/go-is-not-good)」 系列的另一篇文章。Go 确实有一些很棒的特性,所以我在这篇文章中展示了它的优点。但是总体而言,当超过 API 或者网络服务器(这也是它的设计所在)的范畴,用 Go 处理商业领域的逻辑时,我感觉它用起来麻烦而且痛苦。就算在网络编程方面,Go 的设计和实现也存在诸多问题,这使它看上去简单实际则暗藏危险。 写这篇文章的动机是因为我最近重新开始用 Go 写一个业余项目。在以前的工作中我广泛的使用了 Go 为 SaaS 服务编写网络代理(包括 http 和原始的 tcp)。网络编程的部分是相当令人愉快的(我也正在探索这门语言),但随之而来的会计和账单部分则苦不堪言。因为我的业余项目只是一个简单的 API,我认为 Go 非常适合快速的完成这个任务。但是我们都知道,很多项目的增长会超过了预期的范围,所以我不得不写一些数据处理来计算统计数据,Go 的痛苦之处也随着而来。下面就是我对 Go 的困扰。 -一些背景情况:我喜欢静态类型的语言。我第一个标志性的项目是用 [Pascal](https://zh.wikipedia.org/wiki/Pascal_(%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80)) 写的。当90年代初我开始工作之后,开始使用 [Ada](https://zh.wikipedia.org/wiki/Ada) 和 C/C++。后来我转移到 Java 阵地,最后到了 Scala(中间夹杂着 Go),最近开始学习 [Rust](https://www.rust-lang.org/zh-CN/)。我也写了大量的 JavaScript,因为直到现在它依旧是浏览器端唯一可用的语言。我感觉动态类型的语言并不安全,并尽力将它们的使用限制在脚本级别。我习惯了命令式,函数式和面向对象的方法。 +一些背景情况:我喜欢静态类型的语言。我第一个标志性的项目是用 [Pascal](https://zh.wikipedia.org/wiki/Pascal_(%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80)) 写的。当 90 年代初我开始工作之后,开始使用 [Ada](https://zh.wikipedia.org/wiki/Ada) 和 C/C++。后来我转移到 Java 阵地,最后到了 Scala(中间夹杂着 Go),最近开始学习 [Rust](https://www.rust-lang.org/zh-CN/)。我也写了大量的 JavaScript,因为直到现在它依旧是浏览器端唯一可用的语言。我感觉动态类型的语言并不安全,并尽力将它们的使用限制在脚本级别。我习惯了命令式,函数式和面向对象的方法。 这是一篇很长的文章,所以我列出了菜单来“激发你的食欲”: @@ -41,9 +41,9 @@ - [结论](#conclusion) - [几天后: Hacker News 第三名!](#hacker-news) -## 优点 +## 优点 -### Go 很容易学习 +### Go 很容易学习 这是事实:如果你了解任何一种编程语言,那么通过在「[Go 语言之旅](http://go-tour-zh.appspot.com/welcome/1)」学习几个小时就能够掌握 Go 的大部分语法,并在几天后写出你的第一个真正的程序。阅读并理解 [实效 Go 编程](http://docscn.studygolang.com/doc/effective_go.html),浏览一下「[包文档](http://docscn.studygolang.com/pkg/)」,玩一玩 [Gorilla](http://www.gorillatoolkit.org/) 或者 [Go Kit](https://gokit.io/) 这样的网络工具包,然后你将成为一个相当不错的 Go 开发者。 @@ -51,15 +51,15 @@ Go 语言的简单可能是错误的。引用 Rob Pike 的话,[简单既是复杂](https://talks.golang.org/2015/simplicity-is-complicated.slide#1),我们会看到简单背后有很多的陷阱等着我们去踩,极简主义会让我们违背 DRY(Don't Repeat Yourself) 原则。 -### 基于 goroutines 和 channels 的简单并发编程 +### 基于 goroutines 和 channels 的简单并发编程 Goroutines 可能是 Go 的最佳特性了。它们是轻量级的计算线程,与操作系统线程截然不同。 -当 Go 程序执行看似阻塞 I/O 的操作时,实际上 Go 运行时挂起了 goroutine ,当一个事件指示某个结果可用时恢复它。与此同时,其他的 goroutines 已被安排执行。因此在同步编程模型下,我们具有了异步编程的可伸缩性优势。 +当 Go 程序执行看似阻塞 I/O 的操作时,实际上 Go 运行时挂起了 Goroutine ,当一个事件指示某个结果可用时恢复它。与此同时,其他的 goroutines 已被安排执行。因此在同步编程模型下,我们具有了异步编程的可伸缩性优势。 Goroutines 也是轻量级的:它们的堆栈 [随需求增长和收缩](https://dave.cheney.net/2013/06/02/why-is-a-goroutines-stack-infinite),这意味着有 100 个甚至 1000 个 goroutines 都不是问题。 -我以前的应用程序中有一个 goroutine 漏洞:这些 goroutines 结束之前正在等待一个 channel 关闭,而这个 channel 永远不会关闭(一个常见的死锁问题)。这个进程毫无任何理由吃掉了 90 % 的 CPU ,而检查 [expvars](http://docscn.studygolang.com/pkg/expvar/) 显示有 600 k 空闲的 goroutines! 我猜测 goroutine 调度程序占用了 CPU。 +我以前的应用程序中有一个 Goroutine 漏洞:这些 goroutines 结束之前正在等待一个 channel 关闭,而这个 channel 永远不会关闭(一个常见的死锁问题)。这个进程毫无任何理由吃掉了 90 % 的 CPU ,而检查 [expvars](http://docscn.studygolang.com/pkg/expvar/) 显示有 600 k 空闲的 goroutines! 我猜测 Goroutine 调度程序占用了 CPU。 当然,像 Akka 这样的 Actor 系统可以轻松 [处理数百万的 Actors](https://doc.akka.io/docs/akka/2.5/general/actor-systems.html#what-you-should-not-concern-yourself-with),部分原因是 actors 没有堆栈,但是他们远没有像 goroutines 那样简单地编写大量并发的请求/响应应用程序(即 http APIs)。 @@ -67,13 +67,13 @@ channel 是 goroutines 的通信方式:它们提供了一个便利的编程模 但是,channels 必须仔细考虑,因为错误大小的 channels (默认情况下没有缓冲) [会导致死锁](https://www.danmrichards.com/blog/2018/03/26/goroutines-channels-and-waitgroups/)。下面我们还将看到,使用通道并不能阻止竞争情况,因为它缺乏不可变性。 -### 丰富的标准库 +### 丰富的标准库 -Go 的 [标准库](http://docscn.studygolang.com/pkg/) 非常丰富,特别是对于所有与网络协议或 API 开发相关的: http 客户端和服务器,加密,档案格式,压缩,发送电子邮件等等。甚至还有一个html解析器和相当强大的模板引擎去生成 text & html,它会自动过滤 XSS 攻击(例如在 [Hugo](https://gohugo.io/templates/introduction/) 中的使用)。 +Go 的 [标准库](http://docscn.studygolang.com/pkg/) 非常丰富,特别是对于所有与网络协议或 API 开发相关的: http 客户端和服务器,加密,档案格式,压缩,发送电子邮件等等。甚至还有一个 html 解析器和相当强大的模板引擎去生成 text & html,它会自动过滤 XSS 攻击(例如在 [Hugo](https://gohugo.io/templates/introduction/) 中的使用)。 -各种 APIs 一般都简单易懂。它们有时看起来过于简单:这个某种程度上是因为 goroutine 编程模型意味着我们只需要关心“看似同步”的操作。这也是因为一些通用的函数也可以替换许多专门的函数,就像 [我最近发现的关于时间计算的问题](https://bluxte.net/musings/2018/03/22/local-date-time-calculations-in-go/)。 +各种 APIs 一般都简单易懂。它们有时看起来过于简单:这个某种程度上是因为 Goroutine 编程模型意味着我们只需要关心“看似同步”的操作。这也是因为一些通用的函数也可以替换许多专门的函数,就像 [我最近发现的关于时间计算的问题](https://bluxte.net/musings/2018/03/22/local-date-time-calculations-in-go/)。 -### Go 性能优越 +### Go 性能优越 Go 编译为本地可执行文件。许多 Go 的用户来自 Python、Ruby 或 Node.js。对他们来说,这是一种令人兴奋的体验,因为他们看到服务器可以处理的并发请求数量大幅增加。当您使用非并发(Node.js)或全局解释器锁定的解释型语言时,这实际上是相当正常的。结合语言的简易性,这解释了 Go 令人兴奋的原因。 @@ -85,23 +85,23 @@ Go 的垃圾回收器的设计目的是 [优先考虑延迟](https://blog.golang Go 同样在命令行实用程序中优于 Java :作为本地可执行文件,Go 程序没有启动消耗,反之 Java 首先需要加载和编译的字节码。 -### 语言层面定义源代码的格式化 +### 语言层面定义源代码的格式化 我职业生涯中一些最激烈的辩论发生在团队代码格式的定义上。 Go 通过为代码定义规范格式来解决这个问题。 `gofmt` 工具会重新格式化您的代码,并且没有选项。 不管你喜欢与否,`gofmt` 定义了如何对代码进行格式化,一次性解决了这个问题。 -### 标准化的测试框架 +### 标准化的测试框架 Go 在其标准库中提供了一个很好的 [测试框架](http://docscn.studygolang.com/pkg/testing/)。它支持并行测试、基准测试,并包含许多实用程序,可以轻松测试网络客户端和服务器。 -### Go 程序方便操作 +### Go 程序方便操作 与 Python,Ruby 或 Node.js 相比,必须安装单个可执行文件对于运维工程师来说是一个梦想。 随着越来越多的 Docker 的使用,这个问题越来越少,但独立的可执行文件也意味着小型的 Docker 镜像。 -Go还具有一些内置的观察性功能,可以使用 [`expvar`](http://docscn.studygolang.com/pkg/expvar/) 包发布内部状态和指标,并易于添加新内容。但要小心,因为它们在默认的 http 请求处理程序中 [自动公开](http://docscn.studygolang.com/pkg/expvar/#pkg-overview),不受保护。Java 有类似的 JMX ,但它要复杂得多。 +Go 还具有一些内置的观察性功能,可以使用 [`expvar`](http://docscn.studygolang.com/pkg/expvar/) 包发布内部状态和指标,并易于添加新内容。但要小心,因为它们在默认的 http 请求处理程序中 [自动公开](http://docscn.studygolang.com/pkg/expvar/#pkg-overview),不受保护。Java 有类似的 JMX ,但它要复杂得多。 -### Defer 声明,防止忘记清理 +### Defer 声明,防止忘记清理 defer 语句的目的类似于 Java 的 `finally`:在当前函数的末尾执行一些清理代码,而不管此函数如何退出。`defer` 的有趣之处在于它跟代码块没有联系,可以随时出现。这使得清理代码尽可能接近需要清理的代码: @@ -114,9 +114,9 @@ defer file.Close() // 用文件资源的时候,我们再也不需要考虑何时关闭它 ``` -当然,Java的 [试用资源](https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html) 没那么冗长,而且 Rust 在其所有者被删除时会 [自动声明资源](https://doc.rust-lang.org/rust-by-example/trait/drop.html),但是由于 Go 要求您清楚地了解资源清理情况,因此让它接近资源分配很不错。 +当然,Java 的 [试用资源](https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html) 没那么冗长,而且 Rust 在其所有者被删除时会 [自动声明资源](https://doc.rust-lang.org/rust-by-example/trait/drop.html),但是由于 Go 要求您清楚地了解资源清理情况,因此让它接近资源分配很不错。 -### 新类型 +### 新类型 我喜欢类型,因为有些事情让我感到恼火和害怕,举个例子,我们到处把持久对象标识符当做 `string` 或 `long` 类型传递使用。 我们通常会在参数名称中对 id 的类型进行编码,但是当函数具有多个标识符作为参数并且某些调用不匹配参数顺序时,会造成细微的错误。 @@ -138,17 +138,17 @@ func main() { // 错误的顺序:将会编译错误 AddProduct(productId, userId) // 编译错误: - // AddProduct 不能用 productId(type ProductId) 作为 type UserId的参数 - // Addproduct 不能用 userId(type UserId) 作为type ProfuctId 的参数 + // AddProduct 不能用 productId(type ProductId) 作为 type UserId 的参数 + // Addproduct 不能用 userId(type UserId) 作为 type ProfuctId 的参数 } ``` 不幸的是,缺乏泛型使得使用新类型变得麻烦,因为为它们编写可重用代码需要从原始类型转换值。 -## 缺点 +## 缺点 -### Go 忽略了现代语言设计的进步 +### Go 忽略了现代语言设计的进步 -在[少既是多](https://commandcenter.blogspot.tw/2012/06/less-is-exponentially-more.html)中,Rob Pike 解释说 Go 是为了在谷歌取代 C 和 C++,它的前身是 [Newsqueak](https://swtch.com/~rsc/thread/newsqueak.pdf) ,这是他在80年代写的一种语言。Go 也有很多关于 [Plan9](https://en.wikipedia.org/wiki/Plan_9_from_Bell_Labs) 的参考,Plan9 是一个分布式操作系统,在贝尔实验室的80年代开发的。 +在[少既是多](https://commandcenter.blogspot.tw/2012/06/less-is-exponentially-more.html)中,Rob Pike 解释说 Go 是为了在谷歌取代 C 和 C++,它的前身是 [Newsqueak](https://swtch.com/~rsc/thread/newsqueak.pdf) ,这是他在 80 年代写的一种语言。Go 也有很多关于 [Plan9](https://en.wikipedia.org/wiki/Plan_9_from_Bell_Labs) 的参考,Plan9 是一个分布式操作系统,在贝尔实验室的 80 年代开发的。 甚至有一个直接从 Plan9 获得灵感的[Go 汇编](https://golang.org/doc/asm)。为什么不使用 [LLVM](https://llvm.org/) 来提供目标范围广泛且开箱即用的体系结构?我此处可能也遗漏了某些东西,但是为什么需要汇编?如果你需要编写汇编以充分利用 CPU ,那么不应该直接使用目标 CPU 汇编语言吗? @@ -160,7 +160,7 @@ Go 的目标是替换 C 和 C++,很明显它的创建者也没有关注其他 Go 反而在操作工具的领域吸引了 Python 和 Ruby 等脚本语言的用户。他们在 Go 中找到了一种方法,可以提高性能,减少 内存/cpu/磁盘 占用。还有更多的静态类型,这对他们来说是全新的。Go 的杀手级应用是 Docker ,它在 devops 世界中引起了广泛的应用。Kubernetes 的崛起加强了这一趋势。 -### 接口是结构类型 +### 接口是结构类型 Go 接口就像 Java 接口或 Scala 和 Rust 特性(traits):它们定义了后来由类型实现的行为(我不称之为“类”)。 与 Java 接口和 Scala 和 Rust 特性不同,类型不需要显式地指定接口实现:它只需要实现接口中定义的所有函数。所以 Go 的接口实际上是结构化的。 @@ -174,7 +174,7 @@ Go 并不是唯一使用结构化类型的语言,但我发现它有几个缺 *更新* : 对于接口的一些丑陋问题,请参阅下面的 [无接口值(nil interface values)](#nil-interface-values)。 -### 没有枚举 +### 没有枚举 Go 没有枚举,在我看来,这是一个错失的机会。 @@ -182,11 +182,11 @@ Go 没有枚举,在我看来,这是一个错失的机会。 这也意味着没有办法让编译器彻底检查 `switch` 语句,也无法描述类型中允许的值。 -### `:=` / `var` 两难选择 +### `:=` / `var` 两难选择 Go 提供两种方法来声明一个变量,并为其赋值: `var x = "foo"` 和 `x:= "foo"`。这是为什么呢? -主要的区别是 var 允许未初始化的声明(然后您必须声明类型),比如在 `var x string` 中,而 `:=` 需要赋值,并且允许混合使用现有变量和新变量。我的猜测是`:=`被发明来使错误处理减少一点麻烦: +主要的区别是 var 允许未初始化的声明(然后您必须声明类型),比如在 `var x string` 中,而 `:=` 需要赋值,并且允许混合使用现有变量和新变量。我的猜测是 `:=` 被发明来使错误处理减少一点麻烦: 使用 `var` @@ -227,7 +227,7 @@ if someCondition { // foo == "bar" 即使 "someCondition" 为真 ``` -### 零值 panic +### 零值 panic Go 没有构造函数。正因为如此,它坚持认为“零值”应该是易于使用的。这是一个有趣的方法,但在我看来,它所带来的简化主要是针对语言实现者的。 @@ -287,20 +287,20 @@ m0["foo"] = "bar" // panics! ### Go 没有异常。哦,等一下……它有! -这篇博客文章「[为什么 Go 获得异常的方式是对的](https://dave.cheney.net/2012/01/18/why-go-gets-exceptions-right)」详细解释了为什么异常是糟糕的,为什么Go方法要求返回 `错误` 是更好的。我可以同意这一点,确实在使用异步编程或像 Java 流这样的函数风格时,异常是很难处理的(前者可以放到一边,由于 goroutines 的原因它在 Go +这篇博客文章「[为什么 Go 获得异常的方式是对的](https://dave.cheney.net/2012/01/18/why-go-gets-exceptions-right)」详细解释了为什么异常是糟糕的,为什么 Go 方法要求返回 ` 错误 ` 是更好的。我可以同意这一点,确实在使用异步编程或像 Java 流这样的函数风格时,异常是很难处理的(前者可以放到一边,由于 goroutines 的原因它在 Go 中是没有必要的,而后者几乎是不可能)。这篇博文提到 `panic` 「总是对你的程序抛出致命错误,游戏结束」,这很不错。 -在此之前,「[Defer, panic and recover](https://blog.golang.org/defer-panic-and-recover)」 解释了如何从 `panic` 中恢复(实际上是通过捕获它们),并提到在 go 标准库中 json 包可以看到 panic 和 recover 的真实使用。 +在此之前,「[Defer, panic and recover](https://blog.golang.org/defer-panic-and-recover)」 解释了如何从 `panic` 中恢复(实际上是通过捕获它们),并提到在 Go 标准库中 JSON 包可以看到 panic 和 recover 的真实使用。 -事实上, json 解码器有一个 [共同的错误处理函数](https://github.com/golang/go/blob/release-branch.go1.10/src/encoding/json/decode.go#L299) 去 panics,panic 在顶层 `unmarshal`函数中恢复(recover),[检查panic类型](https://github.com/golang/go/blob/release-branch.go1.10/src/encoding/json/decode.go#L173) 并返回一个错误如果它是一个“本地 panic ”或其它错误再次触发的 panic( 失去最初的 panic 的追溯)。 +事实上, JSON 解码器有一个 [共同的错误处理函数](https://github.com/golang/go/blob/release-branch.go1.10/src/encoding/json/decode.go#L299) 去 panics,panic 在顶层 `unmarshal` 函数中恢复(recover),[检查 panic 类型](https://github.com/golang/go/blob/release-branch.go1.10/src/encoding/json/decode.go#L173) 并返回一个错误如果它是一个“本地 panic ”或其它错误再次触发的 panic( 失去最初的 panic 的追溯)。 -对于任何 Java 开发人员来说,这明显看上去是一个`try` / `catch (DecodingException ex)`。所以 Go 确实有异常处理,它在内部使用了却告诉你不要用。 +对于任何 Java 开发人员来说,这明显看上去是一个 `try` / `catch (DecodingException ex)`。所以 Go 确实有异常处理,它在内部使用了却告诉你不要用。 -有趣的事实:几个星期前,一个非谷歌的人[修复了 json 解码器](https://github.com/golang/go/commit/74a92b8e8d0eae6bf9918ef16794b0363886713d),以使用常规的错误冒泡处理。 +有趣的事实:几个星期前,一个非谷歌的人[修复了 JSON 解码器](https://github.com/golang/go/commit/74a92b8e8d0eae6bf9918ef16794b0363886713d),以使用常规的错误冒泡处理。 -## 令人厌恶的点 +## 令人厌恶的点 -### 依赖管理噩梦 +### 依赖管理噩梦 首先引用一个在谷歌著名的 Go 语言使用者 Jaana Dogan (aka JBD) 的话,最近在推特上发泄她的不满: @@ -316,9 +316,9 @@ m0["foo"] = "bar" // panics! 另外,您自己的项目必须在 `GOPATH` 下,否则编译器无法找到它。想让你的项目在单独的目录里清晰地组织起来?那你必须配置每一个项目的 `GOPATH` ,或者使用符号链接。 -社区已经开发了 [大量的工具](https://github.com/golang/go/wiki/PackageManagementTools) 解决此问题。包管理工具引入了 `vendoring` 和锁文件来保存您克隆的任何仓库的Git sha1,以提供可复现的构建。 +社区已经开发了 [大量的工具](https://github.com/golang/go/wiki/PackageManagementTools) 解决此问题。包管理工具引入了 `vendoring` 和锁文件来保存您克隆的任何仓库的 Git sha1,以提供可复现的构建。 -最后,在Go 1.6中,`vendor` 目录得到了[官方支持](https://golang.org/cmd/go/#hdr-Vendor_Directories)。但它是关于你克隆的 `vendoring`,仍然不是正确的版本管理。没有对从传递依赖中导入发生冲突的解决方案,这通常是通过 [语义化版本](https://semver.org/) 来解决的。 +最后,在 Go 1.6 中,`vendor` 目录得到了[官方支持](https://golang.org/cmd/go/#hdr-Vendor_Directories)。但它是关于你克隆的 `vendoring`,仍然不是正确的版本管理。没有对从传递依赖中导入发生冲突的解决方案,这通常是通过 [语义化版本](https://semver.org/) 来解决的。 不过,情况正在好转:`dep`,[官方的依赖管理工具](https://golang.github.io/dep/) 最近被引入以支持文件控制(vendoring)。它支持版本(git tags),并有一个遵循语义版本控制约定的版本解决程序。它还不稳定,但方向是正确的。然而,它仍然需要你的项目存放在 `GOPATH` 里。 @@ -328,9 +328,9 @@ m0["foo"] = "bar" // panics! 现在让我们再次回到代码的问题上。 -### 易变性是用语言硬编码的。 +### 易变性是用语言硬编码的。 -在 Go 中没有定义不可变结构的方法: struct 字段是可变的,`const` 关键字不适用于它们。Go 通过简单的赋值就可以轻松地复制整个`struct`,因此,我们可能认为,通过值传递参数来保证不变性,只需要复制的代价。 +在 Go 中没有定义不可变结构的方法: struct 字段是可变的,`const` 关键字不适用于它们。Go 通过简单的赋值就可以轻松地复制整个 `struct`,因此,我们可能认为,通过值传递参数来保证不变性,只需要复制的代价。 然而,不出所料的,它不复制指针引用的值。而且由于内置的集合(map、slice 和 array)是引用和可变的,复制包含其中任意一项的 `struct` 只是复制了指向底层内层的指针。 @@ -355,9 +355,9 @@ func main() { 所以你必须非常小心,如果你通过值传递参数,不要认定它就是不变的。 -有一些 [深度复制库](https://github.com/jinzhu/copier) 试图使用(慢)反射来解决这个问题,但是它们有不足之处,因为私有字段不能通过反射访问。因此,为了避免竞争条件而进行防御性复制将会很困难,需要大量的重复代码。Go甚至没有一个可以标准化这个的克隆接口。 +有一些 [深度复制库](https://github.com/jinzhu/copier) 试图使用(慢)反射来解决这个问题,但是它们有不足之处,因为私有字段不能通过反射访问。因此,为了避免竞争条件而进行防御性复制将会很困难,需要大量的重复代码。Go 甚至没有一个可以标准化这个的克隆接口。 -### 切片(slice)陷阱 +### 切片(slice)陷阱 切片带来了很多问题。正如「[Go slice: usage and internals](https://blog.golang.org/go-slices-usage-and-internals)」中所解释的那样,考虑到性能原因,再次切片一个切片不会复制底层的数组。这是一个值得赞赏的目标,但也意味着切片的子切片只是遵循原始切片变化的视图。因此,如果您想要将它与初始的切片分开请不要忘记 `copy()`。 @@ -397,9 +397,9 @@ func main() { } ``` -### 易变性和 channels: 竞争条件更容易发生。 +### 易变性和 channels: 竞争条件更容易发生。 -Go 并发性是 [通过 channels 建立在CSP](https://golang.org/doc/faq#csp) 上的,它使用 channel 使得协调 goroutines 比在共享数据上同步更简单和安全。老话说的是「[不要通过共享内存来通信;而应该通过通信来共享内存](https://blog.golang.org/share-memory-by-communicating)」。这是一厢情愿的想法,在实践中是不能安全实现的。 +Go 并发性是 [通过 channels 建立在 CSP](https://golang.org/doc/faq#csp) 上的,它使用 channel 使得协调 goroutines 比在共享数据上同步更简单和安全。老话说的是「[不要通过共享内存来通信;而应该通过通信来共享内存](https://blog.golang.org/share-memory-by-communicating)」。这是一厢情愿的想法,在实践中是不能安全实现的。 正如我们在上面看到的那样,Go 没办法获得不可变的数据结构。这意味着一旦我们在 channel 上发送一个指针,游戏就结束了:我们在并发进程之间共享了可变的数据。当然,一个 channel 的结构是赋值 channel 传送的值(而不是指针),但是正如我们在上面看到的,这些没有深度复制引用,包括 slices 和 maps 本质上都是可变的。与接口类型的 struct 字段相同:它们是指针,接口定义的任何可变方法都是对竞争条件的开放。 @@ -407,7 +407,7 @@ Go 并发性是 [通过 channels 建立在CSP](https://golang.org/doc/faq#csp) 谈到竞争条件时,Go 包含一个 [竞争条件检测模式](https://blog.golang.org/race-detector),该模式检测代码以找到不同步的共享访问。它只能在事件发生的时候检测到竞争问题,所以大多数情况下是在集成或负载测试期间,希望这些能够运行比赛条件。由于它的高运行时成本(除了临时的调试会话),它不能实际应用于生产环境。 -### 嘈杂的错误管理 +### 嘈杂的错误管理 你可以很快学会 Go 的错误处理模式,重复到令人作呕: @@ -422,7 +422,7 @@ if err != nil { 您很快就会忽视这种模式,并将其识别为「好,错误处理了」,但是仍然很杂乱,有时很难在错误处理中找到实际的代码。 -这里有几个问题,因为一个错误的结果可能有名无实,例如当从无所不在的 io.Reader读取时: +这里有几个问题,因为一个错误的结果可能有名无实,例如当从无所不在的 io.Reader 读取时: ```go len, err := reader.Read(bytes) @@ -469,7 +469,7 @@ Rust 有类似的问题:没有异常(真的没有,跟 Go 由于 Go 没有泛型和宏,所以很不幸地,更换为 Rust 的方法是不可能的。 -### Nil 接口值 +### Nil 接口值 这是在看到 [redditor jmickeyd](https://www.reddit.com/r/programming/comments/8bj4yc/go_the_good_the_bad_and_the_ugly/dx82cgz/) 展示了 nil 和接口的怪异表现后的更新,这绝对称得上是丑陋的。我稍微扩展了一下: @@ -497,7 +497,7 @@ func main() { } } ``` -上面的代码验证了 `explodes` 不是nil,但是代码在 `Boom` 中 panics,在 `Bang` 中没有。这是为什么呢?解释在 `println` 这一行:`bomb` 指针是 `0x0`,它实际上是 `nil`,但是 `explodes` 是非nil `(0x10a7060,0x0)`。 +上面的代码验证了 `explodes` 不是 nil,但是代码在 `Boom` 中 panics,在 `Bang` 中没有。这是为什么呢?解释在 `println` 这一行:`bomb` 指针是 `0x0`,它实际上是 `nil`,但是 `explodes` 是非 nil `(0x10a7060,0x0)`。 这两个元素的第一个元素是通过 `Explodes` 类型来实现 `Bomb` 接口的方法分派表的指针,第二个元素是实际 `Explodes` 对象的地址,它是 `nil`。 @@ -517,11 +517,11 @@ if explodes != nil && !reflect.ValueOf(explodes).IsNil() { } ``` -这是漏洞还是特性? `Go语言之旅` 有一个 [专门的页面](https://tour.golang.org/methods/12) 来解释这种行为,并清楚地表示 *「注意,一个具有nil值的接口值本身就是非空值」*。 +这是漏洞还是特性? `Go 语言之旅` 有一个 [专门的页面](https://tour.golang.org/methods/12) 来解释这种行为,并清楚地表示 *「注意,一个具有 nil 值的接口值本身就是非空值」*。 尽管如此,这仍然是丑陋的,并且会导致非常细微的错误。在我看来,这是语言设计中的一个很大的缺陷,只是为了使它的实现更加容易。 -### Struct 字段标记:字符串中的运行时DSL。 +### Struct 字段标记:字符串中的运行时 DSL。 如果您在 Go 中使用了 JSON,您肯定遇到过类似的情况: @@ -539,7 +539,7 @@ type User struct { 为什么要决定使用一个原始字符串,任何库都可以决定使用它想要的任何 DSL ,在运行时解析? -当您使用多个库时,情况会变得很糟糕:这里有一个从协议缓冲区的 [Go文档](https://godoc.org/github.com/golang/protobuf/proto) 中取出的示例: +当您使用多个库时,情况会变得很糟糕:这里有一个从协议缓冲区的 [Go 文档](https://godoc.org/github.com/golang/protobuf/proto) 中取出的示例: ```go type Test struct { @@ -551,17 +551,17 @@ type Test struct { ``` 附注:为什么这些标签在使用 JSON 时如此常见?因为在 Go 公共字段中,必须使用大写字母,或者至少以大写字母开头,而在 JSON 中命名字段的常见约定是小写的 camelcase 或 snake_case。因此需要进行冗长的标记。 -标准的 JSON 编码器 / 解码器不允许提供自动转换的命名策略,就像 [Jackson在Java中所做的](https://github.com/FasterXML/jackson-databind/blob/master/src/main/java/com/fasterxml/jackson/databind/PropertyNamingStrategy.java)。这可能解释了为什么 Docker APIs 中的所有字段都是大写的:这避免了它的开发人员为他们的大型 API 编写这些笨拙的标签。 +标准的 JSON 编码器 / 解码器不允许提供自动转换的命名策略,就像 [Jackson 在 Java 中所做的](https://github.com/FasterXML/jackson-databind/blob/master/src/main/java/com/fasterxml/jackson/databind/PropertyNamingStrategy.java)。这可能解释了为什么 Docker APIs 中的所有字段都是大写的:这避免了它的开发人员为他们的大型 API 编写这些笨拙的标签。 -### 没有泛型…至少不是为了你。 +### 没有泛型…至少不是为了你。 很难想象一种没有泛型的现代静态类型化语言,但这就是你在 Go 中看到的:它没有泛型...或者更精确地说,几乎没有泛型,我们会看到它比没有泛型更糟糕。 -内置的 slice、map、array和 channel 都是泛型。声明一个 `map[string]MyStruct` 清楚地显示了具有两个参数的泛型类型的使用。这很好,因为它允许类型安全编程捕获各种错误。 +内置的 slice、map、array 和 channel 都是泛型。声明一个 `map[string]MyStruct` 清楚地显示了具有两个参数的泛型类型的使用。这很好,因为它允许类型安全编程捕获各种错误。 然而,没有用户可定义的泛型数据结构。这意味着您不能定义可重用的抽象,它可以以类型安全的方式使用任何类型。您必须使用非类型 `interface{}`,并将值转换为适当的类型。任何错误只会在运行时被抓住,会导致 panic。对于 Java 开发人员来说,这就像回到 [回退 Java 5 个版本到 2004 年](https://en.wikipedia.org/wiki/Java_version_history#J2SE_5.0)。 -在「[少即是多](https://commandcenter.blogspot.fr/2012/06/less-is-exponentially-more.html)」中,Rob Pike 意外地将泛型和继承放在同一个「类型编程」包中,并说他喜欢组合而不是继承。不喜欢继承很好(实际上我写了很多没有继承的Scala),但是泛型回答了另一个问题:可重用性,同时保护类型安全。 +在「[少即是多](https://commandcenter.blogspot.fr/2012/06/less-is-exponentially-more.html)」中,Rob Pike 意外地将泛型和继承放在同一个「类型编程」包中,并说他喜欢组合而不是继承。不喜欢继承很好(实际上我写了很多没有继承的 Scala),但是泛型回答了另一个问题:可重用性,同时保护类型安全。 正如下面我们将看到的,在用泛型做内部构建和用户无法定义泛型之间的区别会对开发人员「舒适」和编译时类型安全产生更多的影响:它会影响整个 Go 生态系统。 @@ -668,13 +668,13 @@ Go 1.4 介绍了 [`go generate` 命令](https://blog.golang.org/generate),从 第一个用例是可以的,附加价值是你不需要摆弄 `makefiles`,而生成的说明可以接近生成的代码的用法。 -对于第二个用例,许多语言,比如 Scala 和 Rust,有宏(在 [设计文档](https://docs.google.com/document/d/1V03LUfjSADDooDMhe-_K59EgpTEm3V8uvQRuNMAEnjg/edit) 中提到的)在编译过程中都可以访问源代码的 AST。Stringer 实际上 [导入了Go编译器的解析器](https://github.com/golang/tools/blob/master/cmd/stringer/stringer.go#L69) 来遍历 AST。Java 没有宏,但注释处理器扮演同样的角色。 +对于第二个用例,许多语言,比如 Scala 和 Rust,有宏(在 [设计文档](https://docs.google.com/document/d/1V03LUfjSADDooDMhe-_K59EgpTEm3V8uvQRuNMAEnjg/edit) 中提到的)在编译过程中都可以访问源代码的 AST。Stringer 实际上 [导入了 Go 编译器的解析器](https://github.com/golang/tools/blob/master/cmd/stringer/stringer.go#L69) 来遍历 AST。Java 没有宏,但注释处理器扮演同样的角色。 许多语言也不支持宏,所以在这里没有什么根本的错误,除了这个脆弱的由逗号驱动的语法,它看起来像一个快速的技巧,以某种方式完成工作,而不是作为清晰的语言设计被慎重考虑。 哦,你知道 Go 编译器实际上有 [很多注释/程序](https://dave.cheney.net/2018/01/08/gos-hidden-pragmas) 和 [条件编译](https://dave.cheney.net/2013/10/12/how-to-use-conditional-compilation-with-the-go-build-tool) 使用这个脆弱的注释语法吗? -## 结论 +## 结论 就像你猜到的,我对 Go 爱恨交加。Go 有点像这样的朋友,你喜欢和他一起出去玩,因为他很有趣和他喝啤酒聊天很棒,但是当你想进行更深入的交流时,你会觉得无聊和痛苦,然后你不想和他一起去度假。 @@ -682,23 +682,23 @@ Go 1.4 介绍了 [`go generate` 命令](https://blog.golang.org/generate),从 直到最近,在 Go 占据的领域中并没有出现真正的替代选择,它高效地开发本地可执行文件,而不会导致 C 或 C++ 的痛苦。[Rust](https://www.rust-lang.org/zh-CN/) 在飞速进步,我用得越多,越能发现它的有趣之处和优秀设计。我有一种感觉,Rust 是那些需要时间相处的朋友,你最终会想要和他们建立长期的关系。 -回归技术层面,你会发现一些文章说 Rust 和 Go 不是一个领域的,Rust 是一种系统语言,因为它没有内存回收机制 等等。我认为这越来越不真实了。在 [伟大的web框架](http://www.arewewebyet.org/) 和优秀的 [ORM](http://diesel.rs/)s 中 Rust 正在爬得更高。它也给你一种温暖的感觉,“如果它编译,错误将来自我写的逻辑,而不是我忘记注意的语言怪癖”。 +回归技术层面,你会发现一些文章说 Rust 和 Go 不是一个领域的,Rust 是一种系统语言,因为它没有内存回收机制 等等。我认为这越来越不真实了。在 [伟大的 web 框架](http://www.arewewebyet.org/) 和优秀的 [ORM](http://diesel.rs/)s 中 Rust 正在爬得更高。它也给你一种温暖的感觉,“如果它编译,错误将来自我写的逻辑,而不是我忘记注意的语言怪癖”。 -我们还在容器/服务网格区域看到一些有趣的行动, Buoyant([Linkerd](https://linkerd.io/)的开发商)正在开发它们的新 Kubernetes 服务网格 [Conduit](https://buoyant.io/2017/12/05/introducing-conduit/) 作为一个组合,来自控制层面(我猜可能是因为可用的 [Kubernetes库](https://bluxte.net/musings/2018/04/10/go-good-bad-ugly/#the-var-dilemma))的 Go 和数据层面拥有良好效率和鲁棒性的 Rust ,以及 [Sozu代理](https://www.sozu.io/)。 +我们还在容器/服务网格区域看到一些有趣的行动, Buoyant([Linkerd](https://linkerd.io/)的开发商)正在开发它们的新 Kubernetes 服务网格 [Conduit](https://buoyant.io/2017/12/05/introducing-conduit/) 作为一个组合,来自控制层面(我猜可能是因为可用的 [Kubernetes 库](https://bluxte.net/musings/2018/04/10/go-good-bad-ugly/#the-var-dilemma))的 Go 和数据层面拥有良好效率和鲁棒性的 Rust ,以及 [Sozu 代理](https://www.sozu.io/)。 Swift 也是这个家庭的一份子,或者是 C 和 C++ 的最新替代品。它的生态系统仍然过于以苹果为中心,即使它现在可以在 Linux 上使用,并且已经有了新的 [服务器端 APIs](https://swift.org/server-apis/) 和 [Netty 框架](https://github.com/apple/swift-nio)。 这里当然没有万能药和通用之法。但是知道你所用工具的问题至关重要。我希望这篇博文教会了你关于 Go 你以前没有意识到的问题,这样你就可以避开陷阱! -## 几天后: Hacker News 第三名! +## 几天后: Hacker News 第三名! -*更新,发布3天后*:这篇文章反响惊人。它已经成为了 [Hacker News](https://news.ycombinator.com/item?id=16830153) 的头版(我看到的最好排名是#3)和[/r/programming](https://www.reddit.com/r/programming/comments/8bj4yc/go_the_good_the_bad_and_the_ugly/)(我看到的最好排名是#5),并且在 [Twitter 上得到了一些关注](https://twitter.com/search?l=&q=https%3A%2F%2Fbluxte.net%2Fmusings%2F2018%2F04%2F10%2Fgo-good-bad-ugly%2F)。 +*更新,发布 3 天后*:这篇文章反响惊人。它已经成为了 [Hacker News](https://news.ycombinator.com/item?id=16830153) 的头版(我看到的最好排名是#3)和[/r/programming](https://www.reddit.com/r/programming/comments/8bj4yc/go_the_good_the_bad_and_the_ugly/)(我看到的最好排名是#5),并且在 [Twitter 上得到了一些关注](https://twitter.com/search?l=&q=https%3A%2F%2Fbluxte.net%2Fmusings%2F2018%2F04%2F10%2Fgo-good-bad-ugly%2F)。 这些评论通常都是正面的(甚至是在[/r/golang/](https://www.reddit.com/r/golang/comments/8bj4tx/go_the_good_the_bad_and_the_ugly/)),或者至少承认这篇文章是公平的,并且力求公正。[/r/rust](https://www.reddit.com/r/rust/comments/8bjio2/xpost_from_rprogramming_go_the_good_the_bad_and/)的人们当然喜欢我对 Rust 的兴趣。我从未听说过的人甚至给我发邮件说:“*我只是想让你知道,我认为你写的文章是最好的。感谢您为此付出的所有努力*”。 这是写作时最困难的部分:尽量做到客观公正。这当然不是完全可能的,因为每个人都有自己的偏好,为什么我关注意外的惊喜和语言工程学:语言对你有多大帮助,而不是妨碍你,或者至少是*我的方式*。 -我还在标准库或[ golang.org](https://golang.org/) 上搜索了代码样本,并引用了Go团队的人员,以我对权威材料的分析为基础,避免了“*meh,你引用了一个错误的人*”的反应。 +我还在标准库或[ golang.org](https://golang.org/) 上搜索了代码样本,并引用了 Go 团队的人员,以我对权威材料的分析为基础,避免了“*meh,你引用了一个错误的人*”的反应。 写这篇文章用了我两个星期的晚上时间,但是这真的很有趣。当你做严肃而诚实的工作时,你会得到这样的结果:来自技术网络的许多好的共鸣(如果你忽略掉少数捣乱的和一直脾气暴躁的人)。极大调用了我写更多的深度内容的积极性! diff --git a/published/tech/20180412-Why-you-can-have-millions-of-Goroutines-but-only-thousands-of-Java-Threads.md b/published/tech/20180412-Why-you-can-have-millions-of-Goroutines-but-only-thousands-of-Java-Threads.md index d2c77bf3d..f3df09882 100644 --- a/published/tech/20180412-Why-you-can-have-millions-of-Goroutines-but-only-thousands-of-Java-Threads.md +++ b/published/tech/20180412-Why-you-can-have-millions-of-Goroutines-but-only-thousands-of-Java-Threads.md @@ -13,62 +13,62 @@ [error] at java.base/java.lang.Thread.run(Thread.java:844) ``` -额,超出 thread 限制导致内存溢出。在作者的笔记本的 linux 上运行,这种情况一般发生在创建了 11500 个左右的 thread 时候。 +额,超出 thread 限制导致内存溢出。在作者的笔记本的 Linux 上运行,这种情况一般发生在创建了 11500 个左右的 thread 时候。 -但如果你用 Go 语言来做类似的尝试,每创建一个 Goroutine ,并让它永久的 Sleep ,你会得到一个完全不同的结果。在作者的笔记本上,在作者等待的不耐烦之前,GO语言创建了大约7千万个 Goroutine 。为什么我们可以创建的 Goroutines 比 thread 多这么多呢?回答这个问题需要回到操作系统层面来进行一次愉快的探索。这不仅仅是一个学术问题---在现实世界中它也揭示了如何进行软件设计。事实上,作者碰到过很多次软件出现 JVM 的 Thread 达到上限的情况,要么是因为垃圾代码导致 Thread 泄露,要么就是因为一些开发工程师压根不知道 JVM 有 Thread 限制这回事。 +但如果你用 Go 语言来做类似的尝试,每创建一个 Goroutine ,并让它永久的 Sleep ,你会得到一个完全不同的结果。在作者的笔记本上,在作者等待的不耐烦之前,GO 语言创建了大约 7 千万个 Goroutine 。为什么我们可以创建的 Goroutines 比 thread 多这么多呢?回答这个问题需要回到操作系统层面来进行一次愉快的探索。这不仅仅是一个学术问题---在现实世界中它也揭示了如何进行软件设计。事实上,作者碰到过很多次软件出现 JVM 的 Thread 达到上限的情况,要么是因为垃圾代码导致 Thread 泄露,要么就是因为一些开发工程师压根不知道 JVM 有 Thread 限制这回事。 ## **那么到底什么是 Thread ?** “Thread" 本身其实可以代表很多不同的意义。在这篇文章中,作者把它描述为一种逻辑上的 Thread。Thread 由如下内容组成:一系列按照线性顺序可以执行的指令(operations);和一个逻辑上可以执行的路径。CPUs 中的每一个 Core 在同一时刻只能真正并发执行一个 logic thread[1]。这就产生了一个结论:如果你的 threads 个数大于 CPU 的 Core 个数的话,有一部分的 Threads 就必须要暂停来让其他 Threads 工作,直到这些 Threads 到达一定的时机时才会被恢复继续执行。而暂停和恢复一个线程,至少需要记录两件事情: 1. 当前执行的指令位置。亦称为:说当前线程被暂停时,线程正在执行的代码行; -2. 还需要一个栈空间。 亦可认为:这个栈空间保存了当前线程的状态。一个栈包含了 local 变量也就是一些指针指向堆内存的变量(这个是对于 Java 来说的,对于 C/C++ 可以存储非指针)。一个进程里面所有的 threads 是共享一个堆内存的[2]。 +2. 还需要一个栈空间。 亦可认为:这个栈空间保存了当前线程的状态。一个栈包含了 local 变量也就是一些指针指向堆内存的变量(这个是对于 Java 来说的,对于 C/C++ 可以存储非指针)。一个进程里面所有的 threads 是共享一个堆内存的 [2]。 -有了上面两样东西后,cpu 在调度 thread 的时候,就有了足够的信息,可以暂停一个 thread,调度其他 thread 运行,然后再将暂停的 thread 恢复,从而继续执行。这些操作对于 thread 来说通常是完全透明的。从 thread 的角度来看,它一直都在连续的运行着。thread 被取消调度这样的行为可以被观察的唯一办法就是测量后续操作的时间[3]。 +有了上面两样东西后,cpu 在调度 thread 的时候,就有了足够的信息,可以暂停一个 thread,调度其他 thread 运行,然后再将暂停的 thread 恢复,从而继续执行。这些操作对于 thread 来说通常是完全透明的。从 thread 的角度来看,它一直都在连续的运行着。thread 被取消调度这样的行为可以被观察的唯一办法就是测量后续操作的时间 [3]。 让我们回到最初的问题,为什么我们可以创建那么多的 Goroutinues 呢? -## **JVM使用的是操作系统的Thread** +## **JVM 使用的是操作系统的 Thread** -尽管规范没有要求所有现代的通用 JVM,在我所知道的范围内,当前市面上所有的现代通用目的的 JVM 中的 thread 都是被设计成为了操作系统的thread。下面,我将使用“用户空间 threads" 的概念来指代被语言来调度而不是被操作系统内核调度的 threads。操作系统级别实现的 threads 主要有如下两点限制:首先限制了 threads 的总数量,其次对于语言层面的 thread 和操作系统层面的 thread 进行 1:1 映射的场景,没有支持海量并发的解决方案。 +尽管规范没有要求所有现代的通用 JVM,在我所知道的范围内,当前市面上所有的现代通用目的的 JVM 中的 thread 都是被设计成为了操作系统的 thread。下面,我将使用“用户空间 threads" 的概念来指代被语言来调度而不是被操作系统内核调度的 threads。操作系统级别实现的 threads 主要有如下两点限制:首先限制了 threads 的总数量,其次对于语言层面的 thread 和操作系统层面的 thread 进行 1:1 映射的场景,没有支持海量并发的解决方案。 ### **JVM 中固定的栈大小** **使用操作系统层面的 thread,每一个 thread 都需要耗费静态的大量的内存** -第二个使用操作系统层面的 thread 所带来的问题是,每一个 thread 都需要一个固定的栈内存。虽然这个内存大小是可以配置的,但在 64 位的 JVM 环境中,一个 thread 默认使用1MB的栈内存。虽然你可以将默认的栈内存大小改小一点,但是您会权衡内存使用情况, 从而增加堆栈溢出的风险。在你的代码中递归次数越大,越有可能触发栈溢出。如果使用1MB的栈默认值,那么创建1000个 threads ,将使用 1GB 的 RAM ,虽然 RAM 现在很便宜,但是如果要创建一亿个 threads ,就需要T级别的内存。 +第二个使用操作系统层面的 thread 所带来的问题是,每一个 thread 都需要一个固定的栈内存。虽然这个内存大小是可以配置的,但在 64 位的 JVM 环境中,一个 thread 默认使用 1MB 的栈内存。虽然你可以将默认的栈内存大小改小一点,但是您会权衡内存使用情况, 从而增加堆栈溢出的风险。在你的代码中递归次数越大,越有可能触发栈溢出。如果使用 1MB 的栈默认值,那么创建 1000 个 threads ,将使用 1GB 的 RAM ,虽然 RAM 现在很便宜,但是如果要创建一亿个 threads ,就需要 T 级别的内存。 ### **Go 语言的处理办法:动态大小的栈** -Go 语言为了避免是使用过大的栈内存(大部分都是未使用的)导致内存溢出,使用了一个非常聪明的技巧:Go 的栈大小是动态的,随着存储的数据大小增长和收缩。这不是一件简单微小的事情,这个特性经过了好几个版本的迭代开发[4]。很多其他人的关于 Go 语言的文章中都已经做了详细的说明,本文不打算在这里讨论内部的细节。结果就是新建的一个 Goroutine 实际只占用 4KB 的栈空间。一个栈只占用 4KB,1GB 的内存可以创建 250 万个 Goroutine,相对于 Java 一个栈占用 1MB 的内存,这的确是一个很大的提高。 +Go 语言为了避免是使用过大的栈内存(大部分都是未使用的)导致内存溢出,使用了一个非常聪明的技巧:Go 的栈大小是动态的,随着存储的数据大小增长和收缩。这不是一件简单微小的事情,这个特性经过了好几个版本的迭代开发 [4]。很多其他人的关于 Go 语言的文章中都已经做了详细的说明,本文不打算在这里讨论内部的细节。结果就是新建的一个 Goroutine 实际只占用 4KB 的栈空间。一个栈只占用 4KB,1GB 的内存可以创建 250 万个 Goroutine,相对于 Java 一个栈占用 1MB 的内存,这的确是一个很大的提高。 ### 在 JVM 中上下文的切换是很慢的 **使用操作系统的 threads 的最大能力一般在万级别,主要消耗是在上下文切换的延迟。** -因为 JVM 是使用操作系统的 threads ,也就是说是由操作系统内核进行 threads 的调度。操作系统本身有一个所有正在运行的进程和线程的列表,同时操作系统给它们中的每一个都分配一个“公平”的使用 CPU 的时间片[5]。当内核从一个 thread 切换到另外一个时候,它其实有很多事情需要去做。新线程或进程的运行必须以世界的视角开始,它可以抽象出其他线程在同一 CPU 上运行的事实。本文不想在这里多说,但是如果你感兴趣的话,可以参考[这里](https://en.wikipedia.org/wiki/Context_switch)。(t 问题的关键点是上下文的切换大概需要消耗 1-100µ 秒。这个看上去好像不是很耗时,但是在现实中每次平均切换需要消耗10µ秒,如果想让在一秒钟内,所有的 threads 都能被调用到,那么 threads 在一个 core 上最多只能有 10 万个 threads,而事实上这些 threads 自身已经没有任何时间去做自己的有意义的工作了。 +因为 JVM 是使用操作系统的 threads ,也就是说是由操作系统内核进行 threads 的调度。操作系统本身有一个所有正在运行的进程和线程的列表,同时操作系统给它们中的每一个都分配一个“公平”的使用 CPU 的时间片 [5]。当内核从一个 thread 切换到另外一个时候,它其实有很多事情需要去做。新线程或进程的运行必须以世界的视角开始,它可以抽象出其他线程在同一 CPU 上运行的事实。本文不想在这里多说,但是如果你感兴趣的话,可以参考[这里](https://en.wikipedia.org/wiki/Context_switch)。(t 问题的关键点是上下文的切换大概需要消耗 1-100 µ 秒。这个看上去好像不是很耗时,但是在现实中每次平均切换需要消耗 10 µ秒,如果想让在一秒钟内,所有的 threads 都能被调用到,那么 threads 在一个 core 上最多只能有 10 万个 threads,而事实上这些 threads 自身已经没有任何时间去做自己的有意义的工作了。 **Go 语言完全不同的处理:运行多个 Goroutines 在一个 OS thread 上** Golang 语言本身有自己的调度策略,允许多个 Goroutines 运行在一个同样的 OS thread 上。既然 Golang 能像内核一样运行代码的上下文切换,这样它就能省下大量的时间来避免从用户态切换到 ring-0 的内核态再切换回来的过程。但是这只是表面上能看到的,事实上为 Go 语言支持 100 万的 goroutines,Go 语言其实还做了更多更复杂的事情。 -即使 JVM 把 threads 带到了用户空间,它依然无法支持百万级别的 threads ,想象下在你的新的系统中,在 thread 间进行切换只需要耗费100 纳秒,即使只做上下文切换,有也只能使 100 万个 threads 每秒钟做 10 次上下文的切换,更重要的是,你必须要让你的 CPU 满负荷的做这样的事情。支持真正的高并发需要另外一种优化思路:当你知道这个线程能做有用的工作的时候,才去调度这个线程!如果你正在运行多线程,其实无论何时,只有少部分的线程在做有用的工作。Go 语言引入了 channel 的机制来协助这种调度机制。如果一个 goroutine 正在一个空的 channel 上等待,那么调度器就能看到这些,并不再运行这个 goroutine 。同时 Go 语言更进了一步。它把很多个大部分时间空闲的 goroutines 合并到了一个自己的操作系统线程上。这样可以通过一个线程来调度活动的 Goroutine(这个数量小得多),而是数百万大部分状态处于睡眠的 goroutines 被分离出来。这种机制也有助于降低延迟。 +即使 JVM 把 threads 带到了用户空间,它依然无法支持百万级别的 threads ,想象下在你的新的系统中,在 thread 间进行切换只需要耗费 100 纳秒,即使只做上下文切换,有也只能使 100 万个 threads 每秒钟做 10 次上下文的切换,更重要的是,你必须要让你的 CPU 满负荷的做这样的事情。支持真正的高并发需要另外一种优化思路:当你知道这个线程能做有用的工作的时候,才去调度这个线程!如果你正在运行多线程,其实无论何时,只有少部分的线程在做有用的工作。Go 语言引入了 channel 的机制来协助这种调度机制。如果一个 Goroutine 正在一个空的 channel 上等待,那么调度器就能看到这些,并不再运行这个 Goroutine 。同时 Go 语言更进了一步。它把很多个大部分时间空闲的 goroutines 合并到了一个自己的操作系统线程上。这样可以通过一个线程来调度活动的 Goroutine(这个数量小得多),而是数百万大部分状态处于睡眠的 goroutines 被分离出来。这种机制也有助于降低延迟。 -除非 Java 增加一些语言特性来支持调度可见的功能,否则支持智能调度是不可能实现的。但是你可以自己在“用户态”构建一个运行时的调度器,来调度何时线程可以工作。其实这就是构成Akka这种数百万 actors[6] 并发框架的基础概念。 +除非 Java 增加一些语言特性来支持调度可见的功能,否则支持智能调度是不可能实现的。但是你可以自己在“用户态”构建一个运行时的调度器,来调度何时线程可以工作。其实这就是构成 Akka 这种数百万 actors[6] 并发框架的基础概念。 ## **结语思考** -未来,会有越来越多的从操作系统层面的 thread 模型向轻量级的用户空间级别的 threads 模型迁移发生[7]。从使用角度看,使用高级的并发特性是必须的,也是唯一的需求。这种需求其实并没有增加过多的的复杂度。如果 Go 语言改用操作系统级别的 threads 来替代目前现有的调度和栈空间自增长的机制,其实也就是在 runtime 的代码包中减少数千行的代码。但对于大多数的用户案例上考虑,这是一个更好的的模式。复杂度被语言库的作者做了很好的抽象,这样软件工程师就可以写出高并发的程序了。 +未来,会有越来越多的从操作系统层面的 thread 模型向轻量级的用户空间级别的 threads 模型迁移发生 [7]。从使用角度看,使用高级的并发特性是必须的,也是唯一的需求。这种需求其实并没有增加过多的的复杂度。如果 Go 语言改用操作系统级别的 threads 来替代目前现有的调度和栈空间自增长的机制,其实也就是在 runtime 的代码包中减少数千行的代码。但对于大多数的用户案例上考虑,这是一个更好的的模式。复杂度被语言库的作者做了很好的抽象,这样软件工程师就可以写出高并发的程序了。 --- -1. 超线程技术(Hyperthreading)可以成倍的高效地使用cpu的核。指令流水线(Instruction pipelineing)也可以增加CPU的并行执行的能力,然而,到目前为止,它是 O(numCores)。 +1. 超线程技术(Hyperthreading)可以成倍的高效地使用 cpu 的核。指令流水线(Instruction pipelineing)也可以增加 CPU 的并行执行的能力,然而,到目前为止,它是 O(numCores)。 2. 这个观点在某些特殊的场景下是不成立的,如果有这种场景,麻烦告知作者。 3. 这其实是一种攻击媒介。Javascript 可以检测由键盘中断引起的时间微小差异。这可以被恶意网站用来侦听,而不是你的键盘中断,而是用于他们的时间。[https://mlq.me/download/keystroke_js.pdf](https://mlq.me/download/keystroke_js.pdf) [ -4. Go语言起初使用的是“分段栈模型“,即栈空间是被分割到内存中不同的区域(译者注:在其他语言中栈空间一般是连续的),同时使用一些非常聪明的 bookkeeping 机制进行栈追踪。后来的版本实现为了提升性能,在一些特殊的场景下, 使用连续的栈来替代“分割栈模型”。就像调整hash表一样,分配一个新的大的栈空间,并通过一些复杂的指针操作,把所有内容都复制到新的更大的栈空间去。 +4. Go 语言起初使用的是“分段栈模型“,即栈空间是被分割到内存中不同的区域(译者注:在其他语言中栈空间一般是连续的),同时使用一些非常聪明的 bookkeeping 机制进行栈追踪。后来的版本实现为了提升性能,在一些特殊的场景下, 使用连续的栈来替代“分割栈模型”。就像调整 hash 表一样,分配一个新的大的栈空间,并通过一些复杂的指针操作,把所有内容都复制到新的更大的栈空间去。 5. 线程可以通过调用 nice(请参阅 man nice)来标记它们的优先级,以获取更多信息来控制它们被安排调度。 6. 为了能实现大规模的高并发,Actor 和 Goroutines for Scala/Java 的用户相同。就和 Goroutines 一样,actors 的调度程序可以查看哪些 actors 在他们的邮箱中有消息,并且只运行准备好做有用工作的 actors。实际上你可以有更多的 actors,而不是你可以拥有的例程,因为 actors 不需要堆栈。然而,这意味着如果一个 actor 没有快速处理消息,调度器将被阻塞(因为 Actor 不具有它自己的堆栈,所以它不能在消息中间暂停)。阻塞的调度器意味着没有消息处理,事情会迅速停止。这是一种折衷的处理方案。 -7. 在 Apache 的 web 服务器上,每处理一个请求就需要一个 OS 级别的 Thread,所以一个 Apache 的 web 服务器的并发连接性能只有数千级别。Nginx 选择了另一种模型,即使用一个操作系统级别的 Thread 来处理成百甚至上千个并发连接,允许了更好程度的并发。Erlang 也使用了类似的模型,允许数百万个 actors 同时运行。Gevent 将 Python 的 greenlet(用户空间线程)带入 Python,从而实现比其他方式支持的更高程度的并发性(Python 线程是 OS 线程)。 +7. 在 Apache 的 Web 服务器上,每处理一个请求就需要一个 OS 级别的 Thread,所以一个 Apache 的 Web 服务器的并发连接性能只有数千级别。Nginx 选择了另一种模型,即使用一个操作系统级别的 Thread 来处理成百甚至上千个并发连接,允许了更好程度的并发。Erlang 也使用了类似的模型,允许数百万个 actors 同时运行。Gevent 将 Python 的 greenlet(用户空间线程)带入 Python,从而实现比其他方式支持的更高程度的并发性(Python 线程是 OS 线程)。 --- diff --git a/published/tech/20180416-Cgo-And-Python.md b/published/tech/20180416-Cgo-And-Python.md index 0d3ceb231..5f07033c8 100644 --- a/published/tech/20180416-Cgo-And-Python.md +++ b/published/tech/20180416-Cgo-And-Python.md @@ -71,7 +71,7 @@ print(sys.version) 你可以看到我们放了一个 `#cgo` 在前面;这些符号将会被传递给工具链,从而改编 build 的工作流。在这个例子,我们让 Cgo 去调用 “pkg-config” 来获取 build 需要的标志,并且链接到一个叫 “python-2.7” 的库,以及传递这些标志给 C 编译器。如果你的系统上安装了 CPython 开发库并用 pkg-config 连接,这将使得你可以继续使用普通的 `go build` 编译上面的例子。 -重新再看代码,我们使用 `Py_Initialize()` 和`Py_Finalize()` 来开启和关闭解释器,以及 C 函数 `Py_GetVersion` 来获取包含内嵌解释器版本信息的字符串。 +重新再看代码,我们使用 `Py_Initialize()` 和 `Py_Finalize()` 来开启和关闭解释器,以及 C 函数 `Py_GetVersion` 来获取包含内嵌解释器版本信息的字符串。 如果你有疑问,所有我们需要整合来调用 C Python API 的 Cgo 部分都是样板代码。这也是 Datadog Agent 依赖 [go-python](https://github.com/sbinet/go-python) 来执行所有内嵌操作的原因所在; go-python 库提供了 Go 风格的对 C API 的简单封装,并且隐藏了 Cgo 的细节。这是另一个简单的嵌入示例,这次使用 go-python: @@ -126,14 +126,14 @@ func main() { `foo.py` 模块。在 shell 中,命令类似下面: ```shell -$ go build main.go && PYTHONPATH=. ./main hello, world! +$ Go build main.go && PYTHONPATH=. ./main hello, world! ``` ![The dreadful Global Interpreter Lock](https://raw.githubusercontent.com/studygolang/gctt-images/master/cgo-python/cgo_python_divider_3.png) ## 糟糕的全局解释器锁( GIL ) -为了内嵌 Python 引入 Cgo 是一个妥协:build 过程会变慢,垃圾回收器不会帮我们管理外部系统使用的内存,并且交叉编译也会有难度。是否为一个特定项目引入可能会引发争论,但有一点我认为是无需讨论的: Go 并发模型。如果我们不能在一个 goroutine 里面运行 Python,这一切就毫无意义。 +为了内嵌 Python 引入 Cgo 是一个妥协:build 过程会变慢,垃圾回收器不会帮我们管理外部系统使用的内存,并且交叉编译也会有难度。是否为一个特定项目引入可能会引发争论,但有一点我认为是无需讨论的: Go 并发模型。如果我们不能在一个 Goroutine 里面运行 Python,这一切就毫无意义。 在用 Python 和 cgo 实现并发之前 ,有一点我们需要了解:就是全局解释器锁,简称 GIL 。GIL 是一个被语言解释器( CPython 只是一种)广泛采用的机制,目的是防止同一时刻有超过一个以上的线程运行。这意味着被 CPython 执行的 Python 程序不可能在同一个进程中并行。并发倒是仍然有可能,锁是在速度,安全性和实现难度上的一个较好权衡。那么它为什么会在内嵌的时候引出问题? @@ -141,7 +141,7 @@ $ go build main.go && PYTHONPATH=. ./main hello, world! 我们在 Go 程序中运行 Python 的时候,以上这些都不会自动发生。没有 GIL,我们的 Go 程序可能会创建多个 Python 线程。这将有可能引起竞争条件而导致致命的运行时错误,而且一个模块的错误极有可能摧毁整个 Go 应用。 -解决方案就是在任何时候运行 Go 中的多线程代码都要显式调用 GIL;代码不会太复杂,因为 C API 提供了我们需要的所有工具。为了更好的暴漏问题,我们需要在Python中做一些CPU绑定的事情。让我们把这些函数添加到前面示例中的 foo.py 中: +解决方案就是在任何时候运行 Go 中的多线程代码都要显式调用 GIL;代码不会太复杂,因为 C API 提供了我们需要的所有工具。为了更好的暴漏问题,我们需要在 Python 中做一些 CPU 绑定的事情。让我们把这些函数添加到前面示例中的 foo.py 中: ```python import sys @@ -157,7 +157,7 @@ def print_even(limit=10): sys.stderr.write("{}\n".format(i)) ``` -我们会在 Go 中尝试并发的打印奇数和事件编号,使用两个不同的 goroutine (由此引入线程): +我们会在 Go 中尝试并发的打印奇数和事件编号,使用两个不同的 Goroutine (由此引入线程): ```go package main @@ -165,7 +165,7 @@ import ( "sync" "github.com/sbinet/go-python" ) func main() { - // 下面代码会通过调用PyEval_InitThreads()显式调用 GIL , + // 下面代码会通过调用 PyEval_InitThreads()显式调用 GIL , // 无需等待解释器去执行 python.Initialize() var wg sync.WaitGroup wg.Add(2) @@ -173,7 +173,7 @@ func main() { odds := fooModule.GetAttrString("print_odds") even := fooModule.GetAttrString("print_even") // Initialize() 已经锁定 GIL ,但这时我们并不需要它。 - // 我们保存当前状态和释放锁,从而让 goroutine 能获取它 + // 我们保存当前状态和释放锁,从而让 Goroutine 能获取它 state := python.PyEval_SaveThread() go func() { _gstate := python.PyGILState_Ensure() @@ -201,13 +201,13 @@ func main() { 2. 执行 Python 。 3. 恢复状态,解锁 GIL。 -代码可以说得上简洁明了,但仍有一处细节需要指出:注意,即使是遵循 GIL 模式,在一个例子里面我们运行 GIL 时是通过调用`PyEval_SaveThread()`  和  `PyEval_RestoreThread()` ,在另一个例子里(请看 goroutines 里面的代码)我们是通过调用  `PyGILState_Ensure()` 和 `PyGILState_Release()` 。 +代码可以说得上简洁明了,但仍有一处细节需要指出:注意,即使是遵循 GIL 模式,在一个例子里面我们运行 GIL 时是通过调用 `PyEval_SaveThread()`  和  `PyEval_RestoreThread()` ,在另一个例子里(请看 goroutines 里面的代码)我们是通过调用  `PyGILState_Ensure()` 和 `PyGILState_Release()` 。 我们说过,当 Python 里面运行多线程时,解释器会负责创建储存当前状态的数据结构,但如果是在 C API 里面的话,需要我们亲自动手实现。 -当我们通过 go-python 初始化解释器的时候,我们是运行在 Python 上下文环境。所以当调用 `PyEval_InitThreads()` 时解释器会初始化数据结构并锁住 GIL 。我们可以使用`PyEval_SaveThread()` 和`PyEval_RestoreThread()` 对已经存在的状态进行操作。 +当我们通过 go-python 初始化解释器的时候,我们是运行在 Python 上下文环境。所以当调用 `PyEval_InitThreads()` 时解释器会初始化数据结构并锁住 GIL 。我们可以使用 `PyEval_SaveThread()` 和 `PyEval_RestoreThread()` 对已经存在的状态进行操作。 -在 goroutine 中,我们则是在一个 Go 上下文环境中运行,并且我们不需要显式的创建和删除状态, `PyGILState_Ensure()` 和 `PyGILState_Release()` 负责完成这些工作。 +在 Goroutine 中,我们则是在一个 Go 上下文环境中运行,并且我们不需要显式的创建和删除状态, `PyGILState_Ensure()` 和 `PyGILState_Release()` 负责完成这些工作。 ![Unleash the Gopher](https://raw.githubusercontent.com/studygolang/gctt-images/master/cgo-python/cgo_python_divider_4.png) @@ -215,19 +215,19 @@ func main() { 现在我们已经知道怎么处理多线程 Go 代码在一个内嵌解释器中执行 Python 的过程了,但是在 GIL 之后,我们又面临着一个新的挑战:Go 调度器。 -当一个 goroutine 启动时,它会被调度运行在 `GOMAXPROCS` 个可用线程中的其中一个线程——[点击这里](https://morsmachine.dk/go-scheduler) 可以了解更多细节。当一个 goroutine 执行系统调用或者调用 C 代码时,当前线程会把等待运行线程队列中的其它 goroutine 移交给另一个线程,从而让这些 goroutine 有更多机会执行;当前 goroutine 被挂起,直到系统调用或是 C 函数返回。如果有返回发生,线程就会试图唤醒被终止的 goroutine ,但如果没有返回的可能性,那该线程就会请求 Go 运行时去查找另一个线程来完成该 goroutine ,并且进入睡眠状态。 goroutine 最终被调度给另一个线程,然后结束。 +当一个 Goroutine 启动时,它会被调度运行在 `GOMAXPROCS` 个可用线程中的其中一个线程——[点击这里](https://morsmachine.dk/go-scheduler) 可以了解更多细节。当一个 Goroutine 执行系统调用或者调用 C 代码时,当前线程会把等待运行线程队列中的其它 Goroutine 移交给另一个线程,从而让这些 Goroutine 有更多机会执行;当前 Goroutine 被挂起,直到系统调用或是 C 函数返回。如果有返回发生,线程就会试图唤醒被终止的 Goroutine ,但如果没有返回的可能性,那该线程就会请求 Go 运行时去查找另一个线程来完成该 Goroutine ,并且进入睡眠状态。 Goroutine 最终被调度给另一个线程,然后结束。 -考虑到这些,让我们来看看当一个正在运行 Python 代码的 goroutine 被移动到一个新的线程时, goroutine 都会发生什么: +考虑到这些,让我们来看看当一个正在运行 Python 代码的 Goroutine 被移动到一个新的线程时, Goroutine 都会发生什么: -1. 我们的 goroutine 启动后,执行一个 C 函数调用,然后挂起。GIL 被锁住。 +1. 我们的 Goroutine 启动后,执行一个 C 函数调用,然后挂起。GIL 被锁住。 2. 当 C 函数调用返回,当前线程试图唤醒该 goroutine,但它失败了。 3. 当前线程告诉 Go 运行时去查找另一个线程来唤醒我们的 goroutine。 -4. Go 调度器找到一个可用的线程,并且 goroutine 也被唤醒。 -5. goroutine 基本完成,并且尝试在返回前解锁 GIL。 -6. 当前状态存储的线程 ID 是初始线程的ID,和当前线程的 ID 不一致。 +4. Go 调度器找到一个可用的线程,并且 Goroutine 也被唤醒。 +5. Goroutine 基本完成,并且尝试在返回前解锁 GIL。 +6. 当前状态存储的线程 ID 是初始线程的 ID,和当前线程的 ID 不一致。 7. Panic ! -幸运的是,我们可用强制要求 Go 运行时保证我们的 goroutine 一直运行在同一个线程上,只要通过 goroutine 调用 runtime 包里的 LockOSThread 函数就行。 +幸运的是,我们可用强制要求 Go 运行时保证我们的 Goroutine 一直运行在同一个线程上,只要通过 Goroutine 调用 runtime 包里的 LockOSThread 函数就行。 ```go go func() { @@ -247,7 +247,7 @@ go func() { - cgo 引入的间接损耗。 - 手动操作 GIL。 -- 运行期间绑定 goroutine 到同一个线程的限制。 +- 运行期间绑定 Goroutine 到同一个线程的限制。 考虑到在 Go 中运行 Python 检查的便利,我们很乐意接受这一切。但既然意识到了这些取舍,我们就能够最小化它们带来的影响。对于为支持 Python 而带来的其它限制,我们很难有对策处理可能的问题: diff --git a/published/tech/20180416-Good-Code-VS-Bad-Code-in-Golang.md b/published/tech/20180416-Good-Code-VS-Bad-Code-in-Golang.md index f3bb525cc..34eb0fe9f 100644 --- a/published/tech/20180416-Good-Code-VS-Bad-Code-in-Golang.md +++ b/published/tech/20180416-Good-Code-VS-Bad-Code-in-Golang.md @@ -35,44 +35,44 @@ > -EETFIR EHAA 0853 > -EETFIR EBBU 0908 -这个例子代表的是一个 `FIR` (飞行情报区)列表,第一个航班是 `EHAA 0853`,第二个 `EBBU 0908`。 +这个例子代表的是一个 `FIR` (飞行情报区)列表,第一个航班是 `EHAA 0853`, 第二个 `EBBU 0908`。 - 复杂的 > -GEO -GEOID GEO01 -LATTD 490000N -LONGTD 0500000W > -GEO -GEOID GEO02 -LATTD 500000N -LONGTD 0400000W -一个循环的 `tokens` 列表,每一行包含一个子 `token` 列表(在这个例子中是 `GEOID`, `LATTD`, `LONGTD`)。 +一个循环的 `tokens`  列表,每一行包含一个子 `token` 列表( 在这个例子中是 `GEOID`, `LATTD`, `LONGTD`)。 -结合项目背景,实现一个并行执行的转化代码变得很重要。所以有以下算法: +结合  项目背景, 实现一个  并行执行的转化代码变得很重要。 所以有以下  算法: -- 实现一个对输入进行清理和重新排列的预处理过程(我们需要清除可能存在的空格,重新排列例如 `COMMENT` 等的多行`tokens`) +-  实现一个对  输入进行清理和重新排列的预处理过程(我们需要清除可能存在的空格, 重新排列  例如 `COMMENT` 等的多行 `tokens`) -- 然后分割每一行至一个协程中。每一个协程将会负责处理一行 `tokens`,并且返回结果。 +-  然后分割每一行至  一个协程中。每一个协程将会负责处理一行 `tokens`,并且返回结果。 -- 最后一个步骤同样重要,整合结果并且返回一个 `Message` 消息)结构体,这是一个公共的结构,不管是 `ADEXP` 还是 `ICAO` 类型的信息。 +- 最后一个步骤同样重要,整合结果并且返回一个 `Message` 消息)结构体, 这是一个公共的结构,不管是 `ADEXP` 还是 `ICAO` 类型的信息。 -每一个包包含一个 `adexp.go` 文件暴露主要的 `ParseAdexpMessage()` 方法。 +每一个包包含一个 `adexp.go` 文件  暴露主要的 `ParseAdexpMessage()`  方法。 -## 逐步对比 +## 逐步  对比 现在我们逐步来看下我认为的不好的代码,并且我是如何重构它的。 -## String类型 vs []byte类型 +## String 类型 vs []byte 类型 -限制输入类型为字符串类型并不好。`Go` 对 `byte` 类型的处理提供了强大的支持(例如基础的 `trim`, `regexp` 等),并且输入将会很大程度上类似于 `[]byte` (鉴于 `AFTN` 信息是通过 `TCP` 协议来接收的),实际上没有理由强制要求字符串类型的输入。 + 限制  输入类型为字符串类型并不好。`Go` 对 `byte` 类型的处理提供了强大的支持(例如基础的 `trim`, `regexp` 等),并且输入将会很大程度上  类似于 `[]byte` ( 鉴于 `AFTN` 信息是通过 `TCP` 协议来接收的),实际上没有理由强制要求字符串类型的输入。 ## 错误处理 错误的处理实现有点糟糕。 -我们会发现忽视了在第二个参数中返回的一些可能存在的错误: +我们会发现  忽视了在第二个参数中返回的一些可能存在的错误 : ```go preprocessed, _ := preprocess(string) ``` -好的实现方式是捕获每一个可能的错误: + 好的实现方式是捕获每一个可能的错误: ```go preprocessed, err := preprocess(bytes) @@ -81,7 +81,7 @@ if err != nil { } ``` -我们可以在下面这种不好的代码中也能找到犯的一些错误: + 我们可以在下面这种不好的代码中也能找到犯的一些  错误: ```go if len(in) == 0 { @@ -89,11 +89,11 @@ if len(in) == 0 { } ``` -第一个错误是语法上的。根据Go的语法,错误提示的字符串既不是大写也不是以标点符号结尾。 +第一个错误是语法上的。根据 Go 的语法,错误提示的字符串既不是大写也不是以标点符号结尾。 -第二个错误是如果一个错误信息是一个简单的字符串常量(不需要格式化),使用轻量的 `errors.New()` 性能会更好。 +第二个错误是如果一个错误信息是一个简单的  字符串常量(不需要格式化),使用轻量的 `errors.New()` 性能会更好。 -好的实现如下: +好的实现  如下: ```go if len(in) == 0 { @@ -156,7 +156,7 @@ func mapLine(in []byte, ch chan interface{}) { ch <- nil } ``` -在我看来,这使得代码易读性更强。此外,这种扁平化的处理方式也应该加到错误捕获代码中,下面的例子: +在我看来,这使得代码  易读性更强。此外,这种扁平化的处理方式  也应该加到错误捕获代码中,下面的例子: ```go a, err := f1() @@ -171,7 +171,7 @@ if err == nil { return nil, err } ``` -应该被修改成: +应该被  修改成: ```go a, err := f1() @@ -186,7 +186,7 @@ return b, nil ``` 同样,第二段代码的可读性更好。 -## 传值采用value还是reference +## 传值采用 value 还是 reference 预处理方法的实现并不好: @@ -194,24 +194,24 @@ return b, nil func preprocess(in container) (container, error) { } ``` -结合项目的背景来说(考虑性能),考虑到一个信息的结构体有可能会比较大,更好的方式是在`container`结构内传入指针,否则,在上面例子的代码中`container`的值将会在每一次调用的时候被覆盖掉。 +结合项目的背景来说(考虑性能),考虑到  一个信息的  结构体有可能会比较大,更好的方式是在 `container` 结构内传入指针,否则,在上面例子的代码中 `container` 的值将会在每一次调用的时候被覆盖掉。 -好的实现代码将不会有这个问题因为它单个的处理方式(一个简单的24字节的结构,不管什么类型数据)。 +好的  实现  代码  将不会有这个问题因为它单个的处理方式(一个简单的 24 字节的结构,不管什么类型数据)。 ``` func preprocess(in []byte) ([][]byte, error) { } ``` -更广泛地说,无论是根据引用还是数值传递参数都不是一个符合语言习惯的用法。通过数值传递数据也能帮助确定一个方法将不会带来任何的副作用(就像在函数的输入中传递数据一样)。这样做有几个好处,例如单元测试、在代码并发上的重构(否则我们需要检查每个子函数来确定传递是否完成) +更广泛地说, 无论是根据  引用还是数值传递  参数都不是一个符合语言习惯的用法。通过数值传递数据也能帮助确定一个方法将不会  带来任何的副作用(就像在函数的输入中传递数据一样)。这样做有几个好处,例如  单元测试 、在代码并发上的重构(否则我们需要检查每个子函数来确定  传递  是否完成) -我确信这种写法需要根据实际项目背景小心地使用。 + 我确信这种写法  需要根据实际项目背景小心地使用。 ## 并发 -不好的实现方式源于一个最初的好的想法:利用协程并发处理数据(一个协程处理一行)。 +不好的实现方式源于一个最初的  好的想法:利用协程并发处理数据(一个协程处理一行)。 -这导致了在一个协程里反复调用`mapLine()`。 + 这导致了在一个协程里  反复  调用 `mapLine()`。 ``` for i := 0; i < len(lines); i++ { @@ -219,25 +219,25 @@ for i := 0; i < len(lines); i++ { } ``` -`mapLine()`方法三个参数: +`mapLine()` 方法  三个参数: -- 返回指向最后一个`Message`结构的指针。这意味着每个`mapLine()`将会被同一个变量填充。 +- 返回指向最后一个 `Message` 结构的指针。这意味着  每个 `mapLine()` 将会被同一个变量填充。 -- 当前行 +-  当前行 -- 一个`channel`通道用于处理行完成时发送消息 +-  一个 `channel` 通道用于  处理行完成  时发送消息 -为了共享`Message`消息而去传递一个指针违背了Go基本原则: +为了  共享 `Message` 消息而去传递  一个指针违背了 Go 基本原则: -> 不要通过共享内存来通信,而应该通过通信来共享内存 +>  不要通过共享内存来通信,而应该通过通信来共享内存 传递共享的变量有两个主要的缺点: -- 缺点 #1:分割一致的修饰 +- 缺点 #1:分割  一致的修饰 -因为结构中包含一些切片可以被同时修改(同时被两个或者更多的协程)我们得处理互斥的问题。 +因为  结构中包含一些切片可以被同时修改( 同时被两个或者更多的协程) 我们得处理互斥的问题。 -例如,`Message`消息结构包含一个`Estdata []estdata`,通过加上另一个`estdata`修改这部分必须像下面这样处理: +例如,`Message` 消息结构包含一个 `Estdata []estdata`,通过加上另一个 `estdata` 修改这部分必须像下面这样处理: ```go mutexEstdata.Lock() @@ -248,15 +248,15 @@ for _, v := range value { mutexEstdata.Unlock() ``` -事实上,排除特定用法,在协程中使用`mutex`(互斥锁)并不是一个好的选择。 +事实上, 排除特定用法,在协程中使用 `mutex`(互斥锁) 并不是一个好的选择。 - 缺点 #2:伪共享 -通过线程或者协程分享内存并不是一个好的方式因为可能存在伪共享(一个在`CPU`缓存中的进程可以被另一个`CPU`缓存)。这意味着我们需要尽可能地避免通过线程和协程来共享那些需要修改的变量。 +通过线程或者协程分享内存并不是一个好的方式因为可能存在伪共享( 一个在 `CPU` 缓  存中的进程可以被  另一个 `CPU` 缓存)。这意味着我们需要  尽可能地避免通过线程和协程来共享那些需要修改的变量。 -在这个例子中,虽然,我不认为伪共享在输入的文件教少的情况下有一个很大的影响(在`Message`消息结构体中增加文件的性能测试结果或多或少是一样的),但在我看来这也是一个很重要的需要牢记的点。 +在这个例子中,虽然,我不认为伪共享在输入的文件  教少的情况下有一个很大的  影响( 在 `Message` 消息结构体中增加文件的性能测试结果  或多或少是一样的 ),但在我看来这也是  一个很重要的需要牢记的点。  -现在让我们来看下好的并发处理: +现在让我们  来看下好的  并发处理: ```go for _, line := range in { @@ -264,13 +264,13 @@ for _, line := range in { } ``` -现在,`mapLine()`只接受两个参数: + 现在,`mapLine()` 只接受两个参数: -- 当前行 +- 当前  行 -- `channel`通道,当前行处理完后,这里的通道不再简单地用来发生消息,也用来传送实际的结果。这意味着不应该使用协程去修改最后的消息结构。 +- `channel` 通道,当前行处理完后,这里的通道不再  简单地用来发生消息,也用来传送实际的结果。这意味着不应该使用协程去修改最后的消息结构。 -结果在父级的协程中整合。(产生的`mapLine()`方法在各自的协程中被调用) +结果在父级的协程中整合。( 产生的 `mapLine()` 方法在各自的协程中被调用) ```go msg := Message{} @@ -284,27 +284,27 @@ for range in { } ``` -这个代码更加一致,在我看来,根据`Go`的原则:通过通信来共享内存。通过单一的协程来修改消息变量防止了可能由于并发导致的修改和伪共享问题。 +这个代码更加  一致,在我看来,根据 `Go` 的原则:通过通信来共享内存。 通过单一的协程来修改消息变量防止了可能由于并发导致的修改和伪共享问题。 -这部分代码潜在的问题是造成了每一行一个协程,这种实现将能够运行,因为`ADEXP`信息行数将不会太大,在这个简单实现中,一个请求将产生一个协程,在生成能力上将无法考量。一个更好的选择是创建一个协程池来复用协程。 +这部分代码  潜在的问题是造成了每一行一个协程,这种实现将能够运行,因为 `ADEXP` 信息行数将不会太大,在这个简单实现中,一个请求将产生一个协程,在生成能力上将无法考量。一个更好的选择是创建一个协程池来复用协程。 ## 行处理通知 -在上面不好的代码中,一旦`mapLine()`处理完一行,我们需要在父级的协程中进行标识。这部分将通过使用`chan string`通道和方法的调用: +在上面不  好的代码中, 一旦 `mapLine()` 处理完一行,我们需要  在父级的协程中进行标识。这部分将通过使用 `chan string` 通道和  方法的调用: ```go ch <- "ok" ``` -因为父级协程并不会检查通道传过来的结果,较好的处理是通过 `chan struct{}`使用`ch <- struct{}{}`,或者更好的选择( `GC` 会更差)是通过 `chan interface{}` 使用 `ch <- nil` 处理。 +因为父级协程  并不会  检查通道传过来的结果,较好的处理是通过 `chan struct{}` 使用 `ch <- struct{}{}`,或者更好的  选择( `GC` 会更差)是通过 `chan interface{}` 使用 `ch <- nil` 处理。 -另一个类似的方法(在我看来甚至会更简洁)是使用 `sync.WaitGroup`,因为当每一个 `mapLine()` 执行完了,父级的协程还需继续运行。 +另一个类似的方法(在我看来甚至会更简洁)是使用 `sync.WaitGroup`, 因为当每一个 `mapLine()` 执行完了,父级的协程还需继续运行。 -## If条件判断 +## If 条件判断 在 `Go` 的条件判断语句中,允许在条件前进行赋值。 -一个改进版的代码: +一个改进  版的代码: ```go f, contains := factory[string(token)] @@ -322,9 +322,9 @@ if f, contains := factory[sToken]; contains { ``` 这样代码的可读性更高。 -## Switch选择 +## Switch 选择 -代码中犯得另一个错误是没有设置`switch`中的`default`选项: +代码中犯得另一个错误是  没有设置 `switch` 中的 `default` 选项: ```go switch simpleToken.token { @@ -338,7 +338,7 @@ case tokenAltnz: } ```  -如果开发者能够考虑到所有的情况,`switch`的`default`项是可选的,但是像下面这样捕获特殊的情况肯定会更好。 +如果开发者能够考虑到所有的情况,`switch` 的 `default` 项是可选的,但是像下面这样捕获特殊的情况肯定会更好。 ```go switch simpleToken.token { @@ -355,11 +355,11 @@ default: } ``` -处理`default`选项会帮助开发者捕获开发过程中可能造成的`bugs`。 +处理 `default` 选项会帮助开发者捕获开发过程中  可能造成的 `bugs`。 ## 递归 -`parseComplexLines()`是一个解析复杂`token`的方法,在不好的代码中是使用递归来处理: +`parseComplexLines()` 是一个解析复杂 `token` 的方法, 在不好的代码中是使用递归来处理: ```go func parseComplexLines(in string, currentMap map[string]string, @@ -388,7 +388,7 @@ func parseComplexLines(in string, currentMap map[string]string, return parseComplexLines(in[len(sub):], currentMap, out) } ``` -但是`Go`不支持尾调用优化递归,好的代码使用迭代算法能得到同样的结果: +但是 `Go` 不支持尾调用优化递归, 好的代码使用迭代算法能得到同样的  结果: ```go func parseComplexToken(token string, value []byte) interface{} { @@ -418,11 +418,11 @@ func parseComplexToken(token string, value []byte) interface{} { } ``` -第二种写法在性能上会优于第一种。 +第二种写法在性能上会  优于第一种。 ## 常量管理 -我们需要管理一个常量来区分`ADEXP`和`ICAO`类型的消息。不好的写法如下: + 我们需要管理一个常量来  区分 `ADEXP` 和 `ICAO` 类型的  消息。不好的写法  如下: ```go const ( @@ -431,7 +431,7 @@ const ( ) ``` -反之,利用`Go`的`iota`(常量计数器)能写出更优雅的代码: +反之,利用 `Go` 的 `iota`(常量计数器)能写出更优雅的代码: ```go const ( @@ -439,7 +439,7 @@ const ( IcaoType ) ``` -输出的结果是一致的,但是规避了可能存在的错误。 +输出的结果是一致的,但是规避了  可能存在的错误。 ## 接收方法 @@ -492,7 +492,7 @@ for _, line := range in { msg := Message{} -// Gather the goroutine results +// Gather the Goroutine results for range in { // ... } @@ -511,7 +511,7 @@ func parseLine(in []byte) ([]byte, []byte) { 这样的一个额外的例子对另一个开发者更好地了解当前这个项目很有帮助。 -最后同样重要的,根据`Go`的最佳实践,包本身也需要注释。 +最后同样重要的,根据 `Go` 的最佳实践,包本身也需要注释。 ```go /* @@ -527,7 +527,7 @@ package good 另一个很明显的问题是在缺少日志处理。因为我并不喜欢 `Go` 提供的标准日志 `package` 包,我在这个项目中使用一个叫 `logrus` 第三方的日志包。 -## go 的fmt包 +## Go 的 fmt 包 `Go` 提供一个强力的工具集 `go fmt`。遗憾的是我们忘记去利用它。 @@ -535,7 +535,7 @@ package good `DDD` 带来了通用语言的概念,以强调所有项目之间共享语言的重要性( `business experts`, `dev`, `testers` 等)。 -这点在这个项目上可能无法衡量,但是从整个项目的可维护性上来考虑,保持一个简单的兼容上下文结构的 `Message` 也是重要的一点。 +这点在这个项目上可能无法衡量,但是从整个项目的可维护性上  来考虑,保持一个简单的兼容上下文结构的 `Message` 也是重要的一点。 ## 性能结果 @@ -544,7 +544,7 @@ package good - 好的代码: 60430 ns/op - 不好的代码: 45996 ns/op -不好的代码比好的代码慢了超过30%。 +不好的代码比好的代码慢了超过 30%。 ## 结论 @@ -556,7 +556,7 @@ package good > 性能的提升伴随着代码复杂性的增加。 -一个好的开发者能够在给定的环境中在上面这些特征里找到一个平衡。就像在`DDD`里,`context`就是解决方案🙂。 +一个好的开发者能够在给定的环境中在上面这些特征里找到一个平衡。就像在 `DDD` 里,`context` 就是解决方案🙂。 --- diff --git a/published/tech/20180416-Interactive-Go-programming-with-Jupyte.md b/published/tech/20180416-Interactive-Go-programming-with-Jupyte.md index cd1b5763f..a735f0e5c 100644 --- a/published/tech/20180416-Interactive-Go-programming-with-Jupyte.md +++ b/published/tech/20180416-Interactive-Go-programming-with-Jupyte.md @@ -26,7 +26,7 @@ [mybinder.org](https://mybinder.org/v2/gh/yunabe/lgo-binder/master?filepath=basics.ipynb) -感谢 binder [(mybinder.org)](https://mybinder.org/), 你可以在你的浏览器上使用 binder 上的临时 docker 容器尝试 Go 语言的 Jupyter环境(lgo)。从上面的按钮打开临时的 Jupyter Notebook,享受交互式 Go 编程! +感谢 binder [(mybinder.org)](https://mybinder.org/), 你可以在你的浏览器上使用 binder 上的临时 docker 容器尝试 Go 语言的 Jupyter 环境(lgo)。从上面的按钮打开临时的 Jupyter Notebook,享受交互式 Go 编程! ## 主要特点 @@ -36,7 +36,7 @@ * 拥有 Jupyter Notebook 一样的代码补全,检查和代码格式化。 * 显示图像,HTML,JavaScript,SVG 等。 * 控制台上的交互式解释器 -* 完全支持 goroutine 以及 channel +* 完全支持 Goroutine 以及 channel ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/jupyte/go_jupyter_3.jpeg) @@ -53,7 +53,7 @@ * [使用预先构建的 Docker 镜像](https://github.com/yunabe/lgo#quick-start-with-docker) * [源码安装(目前仅支持 Linux)](https://github.com/yunabe/lgo#install) -如果您想在计算机上快速尝试 Go 语言的 Jupyter环境,请先尝试 Docker 版本。 如果你使用 Linux 并且想要将 Jupyter 环境与 Go 环境集成到你的计算机中,那么你可以选择源码安装。 由于使用了 [`-buildmode = shared` 进行回归](https://github.com/golang/go/issues/24034),lgo 的代码在 go1.10 中运行起来很慢。 在 go1.10 修正 bug 之前,请使用 go1.9 来尝试 lgo 。 目前 lgo 在 go1.9 以及 go1.8 完美运行。 +如果您想在计算机上快速尝试 Go 语言的 Jupyter 环境,请先尝试 Docker 版本。 如果你使用 Linux 并且想要将 Jupyter 环境与 Go 环境集成到你的计算机中,那么你可以选择源码安装。 由于使用了 [`-buildmode = shared` 进行回归](https://github.com/golang/go/issues/24034),lgo 的代码在 go1.10 中运行起来很慢。 在 go1.10 修正 bug 之前,请使用 go1.9 来尝试 lgo 。 目前 lgo 在 go1.9 以及 go1.8 完美运行。 Windows 和 Mac 用户,请使用 Docker 版本,因为 lgo 不支持 Windows 和 Mac。你可以在 Windows 或 Mac 上的 Docker 来运行 lgo。 @@ -85,13 +85,13 @@ sum(3, 4) = 7 ## 与现有框架的比较 -对于那些了解其他现有的 golang Jupyter 内核的人,这里是与竞争对手的比较表。你可以阅读 [`READNE.MD` 中的这部分](https://github.com/yunabe/lgo#comparisons-with-similar-projects)获取更多细节。 +对于那些了解其他现有的 Golang Jupyter 内核的人,这里是与竞争对手的比较表。你可以阅读 [`READNE.MD` 中的这部分](https://github.com/yunabe/lgo#comparisons-with-similar-projects)获取更多细节。 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/jupyte/go_jupyter_8.jpeg) ## 了解更多 -如果你想了解更多,请浏览[本项目的主页](https://github.com/yunabe/lgo)并阅读 `README.md` 中的介绍。此外,你还可以通过这些[示例笔记](https://nbviewer.jupyter.org/github/yunabe/lgo/blob/master/examples/basics.ipynb)中了解更多 Go 语言的Jupyter环境的真正用途。尽情享受 Go 语言的交互式编程吧! +如果你想了解更多,请浏览[本项目的主页](https://github.com/yunabe/lgo)并阅读 `README.md` 中的介绍。此外,你还可以通过这些[示例笔记](https://nbviewer.jupyter.org/github/yunabe/lgo/blob/master/examples/basics.ipynb)中了解更多 Go 语言的 Jupyter 环境的真正用途。尽情享受 Go 语言的交互式编程吧! --- diff --git a/published/tech/20180428-notes-on-structured-concurrency-or-go-statement-considered-harmful.md b/published/tech/20180428-notes-on-structured-concurrency-or-go-statement-considered-harmful.md index a01c86c5e..59f6f59b0 100644 --- a/published/tech/20180428-notes-on-structured-concurrency-or-go-statement-considered-harmful.md +++ b/published/tech/20180428-notes-on-structured-concurrency-or-go-statement-considered-harmful.md @@ -54,7 +54,7 @@ async with trio.open_nursery() as nursery: 在这篇文章,我希望说服你 `nurseries` 并不总是怪异特殊的,而是一个新的控制流原语,它与循环或函数调用一样重要。此外,我们在上面看到的其他方法 - 线程派发和回调注册 - 应该被完全移除并替换为 `nurseries` 。 -听起来难以接受?历史发生过类似的事: 语句 `goto` 曾一度认为是控制流中的王者,现在依旧沦落为 [过去式](https://xkcd.com/292/) 。一些语言依旧有类似 `goto` 的语句,相比原来的 `goto` 有所不同或者被弱化。大部分语言甚至没有它。发生了什么?时间太长久以至于人们忘记了过去的故事,但是结果令人惊讶的类似。所以我们首先会提醒自己什么是goto,然后看看,关于并发API,它可以教给我们的东西。 +听起来难以接受?历史发生过类似的事: 语句 `goto` 曾一度认为是控制流中的王者,现在依旧沦落为 [过去式](https://xkcd.com/292/) 。一些语言依旧有类似 `goto` 的语句,相比原来的 `goto` 有所不同或者被弱化。大部分语言甚至没有它。发生了什么?时间太长久以至于人们忘记了过去的故事,但是结果令人惊讶的类似。所以我们首先会提醒自己什么是 goto,然后看看,关于并发 API,它可以教给我们的东西。 ## 大纲目录 @@ -66,7 +66,7 @@ async with trio.open_nursery() as nursery: 3. `goto` 语句:不再使用 - `go` 语句被认为有害 - 1. go 语句:不再使用 + 1. Go 语句:不再使用 - Nurseries: 一个替代 `go` 语句的构件 1. Nurseries 支持函数抽象 @@ -85,9 +85,9 @@ async with trio.open_nursery() as nursery: ## `goto` 到底是什么 -让我们回顾历史:早期的计算机是使用汇编语言编程的,或者其他更原始的机器机制。有些简陋。因此,在20世纪50年代,IBM 的 John Backus 和 Remington Rand 的 Grace Hopper 等人开始开发 FORTRAN 和 FLOW-MATIC 等语言(以其直接后继 COBOL 而闻名)。 +让我们回顾历史:早期的计算机是使用汇编语言编程的,或者其他更原始的机器机制。有些简陋。因此,在 20 世纪 50 年代,IBM 的 John Backus 和 Remington Rand 的 Grace Hopper 等人开始开发 FORTRAN 和 FLOW-MATIC 等语言(以其直接后继 COBOL 而闻名)。 -FLOW-MATIC当时非常雄心勃勃。你可以把它看作是Python的曾曾曾祖父母:第一种语言,首先为人类设计,其次是计算机。以下是一些FLOW-MATIC代码,让您体验它的外观: +FLOW-MATIC 当时非常雄心勃勃。你可以把它看作是 Python 的曾曾曾祖父母:第一种语言,首先为人类设计,其次是计算机。以下是一些 FLOW-MATIC 代码,让您体验它的外观: ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/flow-matic-code-0.png) @@ -95,7 +95,7 @@ FLOW-MATIC当时非常雄心勃勃。你可以把它看作是Python的曾曾曾 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/compare-sequential-to-goto.png) -反而, FLOW-MATIC 有两种控制流的方式,比较通常的是顺序(sequential),如你所想,从头到尾一句一句地、串行地执行语句,但如果你执行一句特殊的语句 `JUMP TO`,它会立马直接跳转到其他控制语句。例如,下图的语句13跳转到语句2: +反而, FLOW-MATIC 有两种控制流的方式,比较通常的是顺序(sequential),如你所想,从头到尾一句一句地、串行地执行语句,但如果你执行一句特殊的语句 `JUMP TO`,它会立马直接跳转到其他控制语句。例如,下图的语句 13 跳转到语句 2: ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/flow-matic-code-1.png) @@ -107,11 +107,11 @@ FLOW-MATIC当时非常雄心勃勃。你可以把它看作是Python的曾曾曾 如果你感到这看起来很费解,你不是一个人!这个跳转风格的程序是 `FLOW-MATIC` 非常直接地继承汇编语言而来。这很功能强大,非常适合计算机硬件的实际工作方式,但直接使用会让人感到非常困惑。乱七八糟的箭头让人发明了“意大利面代码”这个词。显然,我们需要更好的东西。 -但是... goto导致所有这些问题的关键是什么?为什么有的控制结构好,有些不是?我们如何选择好的?这时,这个问题真的很不清楚,如果你不了解问题,很难解决问题。 +但是... goto 导致所有这些问题的关键是什么?为什么有的控制结构好,有些不是?我们如何选择好的?这时,这个问题真的很不清楚,如果你不了解问题,很难解决问题。 ## `go` 到底是什么 -但让我们暂停回顾历史 - 每个人都知道 `goto` 是不好的。这与并发性有什么关系?那么,考虑Go语言的出名的 `go` 语句,用于产生一个新的 `goroutine` (轻量级线程): +但让我们暂停回顾历史 - 每个人都知道 `goto` 是不好的。这与并发性有什么关系?那么,考虑 Go 语言的出名的 `go` 语句,用于产生一个新的 `goroutine` (轻量级线程): ```golang // Golang @@ -122,31 +122,31 @@ go myfunc(); ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/go-myfunc.png) -这里的颜色被分成两条路径。从主线(绿线)的角度来看,控制流按顺序走:它出现在顶部,然后立即出现在底部。与此同时,从支线(淡紫色线)的角度看,控制流进入顶部,然后跳转到myfunc的主体。与常规函数调用不同,这种跳转是单向的:当运行myfunc时,我们切换到一个全新的堆栈,并且运行时立即忘记了我们来自哪里。 +这里的颜色被分成两条路径。从主线(绿线)的角度来看,控制流按顺序走:它出现在顶部,然后立即出现在底部。与此同时,从支线(淡紫色线)的角度看,控制流进入顶部,然后跳转到 myfunc 的主体。与常规函数调用不同,这种跳转是单向的:当运行 myfunc 时,我们切换到一个全新的堆栈,并且运行时立即忘记了我们来自哪里。 -但这不仅适用于Golang。这是我们在本文开头列出的所有基元的流程控制图: +但这不仅适用于 Golang。这是我们在本文开头列出的所有基元的流程控制图: - 线程库通常提供某种类型的句柄对象,让您稍后可以加入线程 - 但这是一种语言不知道的独立操作。实际的线程产生原语具有上面显示的控制流程。 -- 注册回调在语义上等同于启动一个后台线程,该后台线程(a)阻塞,直到发生某个事件,然后(b)运行回调。(虽然显然实现是不同的)因此,就高级别控制流而言,注册回调本质上是一种 go 语句。 +- 注册回调在语义上等同于启动一个后台线程,该后台线程(a)阻塞,直到发生某个事件,然后(b)运行回调。(虽然显然实现是不同的)因此,就高级别控制流而言,注册回调本质上是一种 Go 语句。 - `future` 和 `promise` 也是一样的:当你调用一个函数并且它返回一个 promise 时,这意味着它计划在后台发生的工作,然后给你一个句柄对象以后加入工作(如果你想的话)。就控制流语义而言,这就像产生一个线程一样。然后你在这个 promise 上注册回调,所以看到前面的要点。 同样的确切模式以许多形式出现:关键的相似之处在于,在所有这些情况下,控制流分离,一方进行单向跳转,另一方返回给调用者。一旦你知道要寻找什么,你就会开始在各地看到它 - 这是一个有趣的游戏![1] -令人烦恼的是,这类控制流结构没有标准名称。因此,就像 `goto语句` 成为所有不同类似 goto 结构的总称一样,我将使用 `go语句` 作为这些术语的总称。为什么这么做? 一个原因是 Go 语言给了我们一个特别纯粹的形式例子。另一个是......好吧,你可能已经猜到了我说什么了。看看这两个图。注意任何相似之处: +令人烦恼的是,这类控制流结构没有标准名称。因此,就像 `goto 语句` 成为所有不同类似 goto 结构的总称一样,我将使用 `go 语句` 作为这些术语的总称。为什么这么做? 一个原因是 Go 语言给了我们一个特别纯粹的形式例子。另一个是......好吧,你可能已经猜到了我说什么了。看看这两个图。注意任何相似之处: ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/compare-go-to-goto.png.png) -没错:go语句是goto语句的一种形式。 +没错:go 语句是 goto 语句的一种形式。 -并发程序的编写和推理是非常困难的。基于goto的程序也是如此。这可能是出于某些相同的原因吗?在现代语言中,goto引起的问题在很大程度上得到解决。如果我们从 ”研究他们如何修正“ 转到 ”它会教我们如何制作更多可用的并发API“,让我们来找出答案。 +并发程序的编写和推理是非常困难的。基于 goto 的程序也是如此。这可能是出于某些相同的原因吗?在现代语言中,goto 引起的问题在很大程度上得到解决。如果我们从 ”研究他们如何修正“ 转到 ”它会教我们如何制作更多可用的并发 API“,让我们来找出答案。 -## goto发生了什么事? +## goto 发生了什么事? 那么,为什么 goto 会导致如此多的问题?在二十世纪六十年代后期,Edsger W. Dijkstra 编写了两篇很著名的论文,帮助人们更清楚地了解这一点:[goto 语句被认为有害](https://scholar.google.com/scholar?cluster=15335993203437612903&hl=en&as_sdt=0,5) 与 [结构化编程的笔记](https://www.cs.utexas.edu/~EWD/ewd02xx/EWD249.PDF) ### goto:抽象的毁灭者 -在这些论文中,Dijkstra担心如何编写非凡的软件并使其正确。我无法在这里详细说明。例如,你可能听说过这句话: +在这些论文中,Dijkstra 担心如何编写非凡的软件并使其正确。我无法在这里详细说明。例如,你可能听说过这句话: > 程序测试可以用来发现 bug 的存在,但却没法证明 bug 不存在 @@ -156,17 +156,17 @@ go myfunc(); print("Hello world!") ``` -那么你不需要知道打印是如何实现的(字符串格式化,缓冲,跨平台差异......)的所有细节。您只需要知道它会以某种方式打印您提供的文本,然后您可以花费精力去考虑您的代码中是否希望在此时发生这种情况。 Dijkstra希望语言支持这种抽象。 +那么你不需要知道打印是如何实现的(字符串格式化,缓冲,跨平台差异......)的所有细节。您只需要知道它会以某种方式打印您提供的文本,然后您可以花费精力去考虑您的代码中是否希望在此时发生这种情况。 Dijkstra 希望语言支持这种抽象。 -至此,块语法已经被发明出来,像ALGOL这样的语言已经积累了5种不同类型的控制结构:它们仍然具有顺序流和 `goto` : +至此,块语法已经被发明出来,像 ALGOL 这样的语言已经积累了 5 种不同类型的控制结构:它们仍然具有顺序流和 `goto` : ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/compare-sequential-to-goto.png) -并且还获得了if / else,循环和函数调用的变体: +并且还获得了 if / else,循环和函数调用的变体: ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/if-loop-functioncall.png) -你可以使用goto来实现这些更高层次的结构,并且在早期,人们就会想到它们:作为一个方便的简写。但是Dijkstra指出的是,如果你看这些图表,goto和其他人之间有很大的区别。对于除goto以外的所有内容,流量控制位于顶部→[有问题]→流量控制位于底部。我们可以称之为“黑盒子规则”:如果一个控制结构具有这种形状,那么在你不关心内部发生的细节的上下文中,你可以忽略[stuff happen]部分,并把整个作为规则的顺序流程。而且更好的是,任何由这些部分组成的代码也是如此。当我看这个代码时: +你可以使用 goto 来实现这些更高层次的结构,并且在早期,人们就会想到它们:作为一个方便的简写。但是 Dijkstra 指出的是,如果你看这些图表,goto 和其他人之间有很大的区别。对于除 goto 以外的所有内容,流量控制位于顶部→[有问题]→流量控制位于底部。我们可以称之为“黑盒子规则”:如果一个控制结构具有这种形状,那么在你不关心内部发生的细节的上下文中,你可以忽略[stuff happen]部分,并把整个作为规则的顺序流程。而且更好的是,任何由这些部分组成的代码也是如此。当我看这个代码时: ```python print("Hello world!") @@ -174,25 +174,25 @@ print("Hello world!") 我不必去阅读 `print` 及其所有传递依赖的定义,只是想知道控制流程是如何工作的。也许在内部 `print` 有一个循环,并且在循环内部有一个 `if / else` ,并且在 `if / else` 内部还有另一个函数调用......或者也可能是其他内容。它并不重要:我知道控制将流入 `print` ,函数将完成它的事情,然后最终控制将返回到我正在阅读的代码。 -看起来这很明显,但如果你有一个带有 `goto` 的语言 —— 一种语言,其功能和其他所有内容都建立在 `goto` 之上,`goto` 可以随时随地跳转 - 然后这些控制结构根本不是黑匣子!如果你有一个函数,并且在函数内部有一个循环,并且在循环内部有一个`if/else`,并且在 `if/else` 中有一个 `goto` ...那么 `goto` 可以将控制发送到任何它想要的地方。也许控制会突然从另一个你还没有调用的函数完全返回,你不知道! +看起来这很明显,但如果你有一个带有 `goto` 的语言 —— 一种语言,其功能和其他所有内容都建立在 `goto` 之上,`goto` 可以随时随地跳转 - 然后这些控制结构根本不是黑匣子!如果你有一个函数,并且在函数内部有一个循环,并且在循环内部有一个 `if/else`,并且在 `if/else` 中有一个 `goto` ...那么 `goto` 可以将控制发送到任何它想要的地方。也许控制会突然从另一个你还没有调用的函数完全返回,你不知道! 这就打破了抽象:这意味着每一个函数调用都可能是一个变相的 `goto` 语句,唯一需要知道的就是将系统的整个源代码一次性保存在头脑中。只要 `goto` 使用你的语言,你就会停止对流量控制进行本地推理。这就是为什么 `goto` 会导致意大利面代码。 现在 Dijkstra 明白了这个问题,他能够解决这个问题。这是他的革命性建议:我们应该停止将 `if / loops /function call` 作为 `goto` 的简写,而应该将它们作为自己权利的基本原语 - 并且我们应该完全从我们的语言中删除 `goto`。 -从2018年起,这似乎显而易见。但是当你试图拿走他们的玩具时,你有没有看过程序员的反应,因为他们不够聪明,无法安全使用它们?是的,有些事情永远不会改变。 1969年,这个提议令人难以置信地引起争议。[Donald Knuth](https://en.wikipedia.org/wiki/Donald_Knuth) 为 `goto` [辩护](https://scholar.google.com/scholar?cluster=17147143327681396418&hl=en&as_sdt=0,5) 。曾经成为编写代码专家的人非常不满,他们基本上不得不基本学会如何重新编程,以便使用更新,更有约束的构造来表达自己的想法。当然,它需要建立一套全新的语言。 +从 2018 年起,这似乎显而易见。但是当你试图拿走他们的玩具时,你有没有看过程序员的反应,因为他们不够聪明,无法安全使用它们?是的,有些事情永远不会改变。 1969 年,这个提议令人难以置信地引起争议。[Donald Knuth](https://en.wikipedia.org/wiki/Donald_Knuth) 为 `goto` [辩护](https://scholar.google.com/scholar?cluster=17147143327681396418&hl=en&as_sdt=0,5) 。曾经成为编写代码专家的人非常不满,他们基本上不得不基本学会如何重新编程,以便使用更新,更有约束的构造来表达自己的想法。当然,它需要建立一套全新的语言。 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/goto-change.png) 最后,现代语言比 Dijkstra 的原始公式稍逊一筹。他们会让你使用 `break`,`continue` 或 `return` 等构造立即跳出多个嵌套结构。但基本上,他们都是围绕 Dijkstra 的想法而设计的;即使这些推动边界的构造只能以严格有限的方式进行。特别是,`function` - 这是在黑匣子内部封装控制流程的基本工具 - 被认为是不可侵犯的。你不能从一个函数中跳出另一个函数,并且返回可以将你从当前函数中取出,但不能再进一步。无论控制流程如何,一个函数在内部起作用,其他函数不必关心。 -这甚至延伸到 goto 本身。你会发现几种语言仍然有一些他们称之为 goto 的语言,比如 C,C#,Golang ......但是它们增加了很多限制。至少,他们不会让你跳出一个 function 并跳入另一个 function 。除非你在汇编[2]中工作,无限制的goto不见了。 Dijkstra赢了。 +这甚至延伸到 goto 本身。你会发现几种语言仍然有一些他们称之为 goto 的语言,比如 C,C#,Golang ......但是它们增加了很多限制。至少,他们不会让你跳出一个 function 并跳入另一个 function 。除非你在汇编[2]中工作,无限制的 goto 不见了。 Dijkstra 赢了。 ### 一个惊喜,移除 `goto` 语句带来新的特性 一旦 goto 消失,就会发生一些有趣的事情:语言设计者能够开始添加依赖于控制流程结构的功能。 -例如,Python有一些很好的资源清理语法: with 语句。你可以写下如下内容: +例如,Python 有一些很好的资源清理语法: with 语句。你可以写下如下内容: ```python # Python @@ -200,23 +200,23 @@ with open("my-file") as file_handle: ... ``` -并保证该文件将在 `...` 代码期间打开,但随后会立即关闭。大多数现代语言都有一些等效的(RAII,使用,试用资源,推迟 ......)。他们都假设控制流程是有序的,结构化的。如果我们使用goto语句跳入我们中间有块 `...` 你会怎么办?文件是否打开?如果我们再次跳出来,而不是正常退出?文件会关闭吗?此功能只是没有任何连贯的方式工作。 +并保证该文件将在 `...` 代码期间打开,但随后会立即关闭。大多数现代语言都有一些等效的(RAII,使用,试用资源,推迟 ......)。他们都假设控制流程是有序的,结构化的。如果我们使用 goto 语句跳入我们中间有块 `...` 你会怎么办?文件是否打开?如果我们再次跳出来,而不是正常退出?文件会关闭吗?此功能只是没有任何连贯的方式工作。 -错误处理有类似的问题:当出现问题时,你的代码应该做什么?答案常常是把堆栈中的堆栈传递给代码的调用者,让他们弄清楚如何处理它。现代语言具有专门的构造来使这更容易,例如异常或其他形式的[自动错误传播](https://doc.rust-lang.org/std/result/index.html#the-question-mark-operator-)。但是你的语言只能提供这种帮助,如果它有一个堆栈和一个可靠的“呼叫者”概念。再看一下我们FLOW-MATIC程序中的控制流面条,并想象在它的中间试图引发异常。它甚至会去哪里? +错误处理有类似的问题:当出现问题时,你的代码应该做什么?答案常常是把堆栈中的堆栈传递给代码的调用者,让他们弄清楚如何处理它。现代语言具有专门的构造来使这更容易,例如异常或其他形式的[自动错误传播](https://doc.rust-lang.org/std/result/index.html#the-question-mark-operator-)。但是你的语言只能提供这种帮助,如果它有一个堆栈和一个可靠的“呼叫者”概念。再看一下我们 FLOW-MATIC 程序中的控制流面条,并想象在它的中间试图引发异常。它甚至会去哪里? ### goto 语句:不再使用 所以 `goto` —— 忽略函数界限的传统类型 —— 不仅仅是一种常见的坏特性,难以正确使用。如果仅仅如此,它可能会保留下来。但实际情况更糟。 -即使你不使用 goto ,只要把它作为你的语言的一个选项,就会让所有的东西都难以使用。每当你开始使用第三方库时,你都不能把它当作一个黑匣子 - 你必须仔细阅读它以找出哪些函数是常规函数,哪些函数是伪装的特殊控制结构流。这是本地推断的严重障碍。你失去了强大的语言功能,如可靠的资源清理和自动错误传递。我们应该更好地完全移除goto,以支持遵循“黑匣子”规则的控制流构造。 +即使你不使用 goto ,只要把它作为你的语言的一个选项,就会让所有的东西都难以使用。每当你开始使用第三方库时,你都不能把它当作一个黑匣子 - 你必须仔细阅读它以找出哪些函数是常规函数,哪些函数是伪装的特殊控制结构流。这是本地推断的严重障碍。你失去了强大的语言功能,如可靠的资源清理和自动错误传递。我们应该更好地完全移除 goto,以支持遵循“黑匣子”规则的控制流构造。 -### go 语句被认为有害 +### Go 语句被认为有害 -所以这就是goto的历史。现在,这多少适用于 `go`语句? 那么......基本上,所有这一切!这个比喻结果令人震惊。 +所以这就是 goto 的历史。现在,这多少适用于 `go` 语句? 那么......基本上,所有这一切!这个比喻结果令人震惊。 -Go语句打破了抽象。请记住我们如何说如果我们的语言允许跳转,那么任何功能都可能是变相跳转?在大多数并发框架中,go语句会导致完全相同的问题:无论何时调用函数,它都可能会或可能不会产生一些后台任务。该功能似乎回来了,但它仍然在后台运行?如果没有阅读所有的源代码,就没有办法知道。何时完成?很难说。如果你有 go 语句,然后功能不再相对于黑盒子来控制流量。在我的第一篇关于并发API的文章中,我称之为“违反因果关系”,并发现它是使用 asyncio 和 Twisted 的程序中的许多常见现实问题的根源,例如背压问题,正确关闭问题等等。 +Go 语句打破了抽象。请记住我们如何说如果我们的语言允许跳转,那么任何功能都可能是变相跳转?在大多数并发框架中,go 语句会导致完全相同的问题:无论何时调用函数,它都可能会或可能不会产生一些后台任务。该功能似乎回来了,但它仍然在后台运行?如果没有阅读所有的源代码,就没有办法知道。何时完成?很难说。如果你有 Go 语句,然后功能不再相对于黑盒子来控制流量。在我的第一篇关于并发 API 的文章中,我称之为“违反因果关系”,并发现它是使用 asyncio 和 Twisted 的程序中的许多常见现实问题的根源,例如背压问题,正确关闭问题等等。 -Go语句会中断自动资源清理。让我们再一次`with` 语句示例: +Go 语句会中断自动资源清理。让我们再一次 `with` 语句示例: ```python # Python @@ -228,18 +228,18 @@ with open("my-file") as file_handle: 如果我们希望此代码正常工作,我们需要以某种方式跟踪任何后台任务,并且只有在完成后手动安排文件才能关闭。这是可行的 - 除非我们正在使用一些不提供任何方式在任务完成时得到通知的库,这是非常常见的(例如因为它没有公开任何可以加入的任务句柄)。但即使在最好的情况下,非结构化的控制流程也意味着语言无法帮助我们。我们现在回到实施资源清理手中,就像在过去的糟糕时期。 -Go语句打破错误处理。就像我们上面讨论的那样,现代语言提供了强大的工具,例如异常,以帮助我们确保检测到错误并将其传播到正确的位置。但是这些工具依赖于拥有“当前代码的调用者”的可靠概念。只要您产生任务或注册回调,该概念就会被破坏。结果,我所知道的每个主流并发框架都简单地放弃了。如果后台任务发生错误,而您没有手动处理它,那么运行时只是......将它放到地板上并不再管它,这不是太重要。如果幸运的话,它可能会在控制台上打印一些东西。(我唯一使用过的认为“打印某些内容并继续前进”的软件是一个很好的错误处理策略,它是古老的Fortran库,但我们在这里。)甚至 Rust - 这门语言在高中阶段被投票选为“正确度最高的人”。如果后台线程发生混乱,Rust [抛弃错误并期望变得更好](https://doc.rust-lang.org/std/thread/) 。 +Go 语句打破错误处理。就像我们上面讨论的那样,现代语言提供了强大的工具,例如异常,以帮助我们确保检测到错误并将其传播到正确的位置。但是这些工具依赖于拥有“当前代码的调用者”的可靠概念。只要您产生任务或注册回调,该概念就会被破坏。结果,我所知道的每个主流并发框架都简单地放弃了。如果后台任务发生错误,而您没有手动处理它,那么运行时只是......将它放到地板上并不再管它,这不是太重要。如果幸运的话,它可能会在控制台上打印一些东西。(我唯一使用过的认为“打印某些内容并继续前进”的软件是一个很好的错误处理策略,它是古老的 Fortran 库,但我们在这里。)甚至 Rust - 这门语言在高中阶段被投票选为“正确度最高的人”。如果后台线程发生混乱,Rust [抛弃错误并期望变得更好](https://doc.rust-lang.org/std/thread/) 。 -当然,您可以在这些系统中正确处理错误,仔细确保加入每个线程,或者通过构建自己的错误传播机制,如 [Javascript中的Twisted](https://twistedmatrix.com/documents/current/core/howto/defer.html#visual-explanation) 或 [Promise.catch中的errbacks](https://hackernoon.com/promises-and-error-handling-4a11af37cb0e) 。但是现在你正在编写一个特殊的,脆弱的重新实现你的语言已经有的功能。你失去了诸如“回溯”和“调试器”等有用的东西。所需要的只是忘记拨打 `Promise.catch` 一次,突然间,您甚至没有意识到地板上的严重错误。即使你以某种方式解决了所有这些问题,你仍然会得到两个冗余系统来做同样的事情。 +当然,您可以在这些系统中正确处理错误,仔细确保加入每个线程,或者通过构建自己的错误传播机制,如 [Javascript 中的 Twisted](https://twistedmatrix.com/documents/current/core/howto/defer.html#visual-explanation) 或 [Promise.catch 中的 errbacks](https://hackernoon.com/promises-and-error-handling-4a11af37cb0e) 。但是现在你正在编写一个特殊的,脆弱的重新实现你的语言已经有的功能。你失去了诸如“回溯”和“调试器”等有用的东西。所需要的只是忘记拨打 `Promise.catch` 一次,突然间,您甚至没有意识到地板上的严重错误。即使你以某种方式解决了所有这些问题,你仍然会得到两个冗余系统来做同样的事情。 -### go 语句:不再使用 +### Go 语句:不再使用 -就像goto是第一批实用的高级语言的明显原始代码一样,go是第一个实用并发框架的明显原语:它匹配底层调度程序实际工作的方式,并且它足够强大,可以实现任何其他并发流模式。但是,再次像goto一样,它打破了控制流抽象,所以只是将它作为您的语言的一个选项使得一切都变得更难以使用。 +就像 goto 是第一批实用的高级语言的明显原始代码一样,go 是第一个实用并发框架的明显原语:它匹配底层调度程序实际工作的方式,并且它足够强大,可以实现任何其他并发流模式。但是,再次像 goto 一样,它打破了控制流抽象,所以只是将它作为您的语言的一个选项使得一切都变得更难以使用。 好消息是,这些问题都可以解决,Dijkstra 向我们展示如何做到: - 找到具有类似功能的语句的替代品,但遵循“黑匣子规则”, -- 将这个新构造作为原语构建到我们的并发框架中,并且不包含任何形式的go语句。 +- 将这个新构造作为原语构建到我们的并发框架中,并且不包含任何形式的 go 语句。 这就是 Trio 所做的。 @@ -251,11 +251,11 @@ Go语句打破错误处理。就像我们上面讨论的那样,现代语言提 注意,这只有一个箭头出现在顶部,一个出现在底部,所以它遵循 Dijkstra 的黑盒子规则。现在,我们怎样才能把这个草图变成一个具体的语言结构呢?有一些现有的构造可以满足这个约束,但是(a)我的提议与我所知道的并且比它们有优势(特别是在想要使其成为独立原语的情况下)略有不同,并且(b)并发性是庞大而复杂的,试图把所有的历史和权衡分开将会使论证完全失败,所以我将把它推迟到另一篇单独的文章。在这里,我只关注解释我的解决方案。但请注意,我并不是说自己喜欢,发明了并发或某种东西,我是站在巨人的肩膀上,从很多来源吸取灵感。 -无论如何,下面是我们要做的事情:首先,我们声明父任务不能启动任何子任务,除非它首先为子任务创建一个地方: Nurseries 。它通过打开一个 Nurseries 块来实现这一点; 在Trio中,我们使用 Python 的 `async with` 语法来执行此操作: +无论如何,下面是我们要做的事情:首先,我们声明父任务不能启动任何子任务,除非它首先为子任务创建一个地方: Nurseries 。它通过打开一个 Nurseries 块来实现这一点; 在 Trio 中,我们使用 Python 的 `async with` 语法来执行此操作: ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/python-async-with.png) -打开一个nursery块会自动创建一个代表这个 Nurseries 的对象,并且 nursery 语法将这个对象赋给名为 nursery 的变量。然后我们可以使用 nursery 对象的 start_soon 方法来启动并发任务:在这种情况下,一个任务调用函数 myfunc,另一个调用函数 anotherfunc。从概念上讲,这些任务在 Nurseries 区内执行。实际上,将 nursery 块内写入的代码视为创建块时自动启动的初始任务通常很方便。 +打开一个 nursery 块会自动创建一个代表这个 Nurseries 的对象,并且 nursery 语法将这个对象赋给名为 nursery 的变量。然后我们可以使用 nursery 对象的 start_soon 方法来启动并发任务:在这种情况下,一个任务调用函数 myfunc,另一个调用函数 anotherfunc。从概念上讲,这些任务在 Nurseries 区内执行。实际上,将 nursery 块内写入的代码视为创建块时自动启动的初始任务通常很方便。 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/python-nursery.png) @@ -273,13 +273,13 @@ go 语句的基本问题是,当你调用一个函数时,你不知道它是 ### Nurseries 支持动态的任务派发 -这是一个更简单的原型,它也满足我们上面的流程控制图。它需要一个thunk的列表,并且同时运行它们: +这是一个更简单的原型,它也满足我们上面的流程控制图。它需要一个 thunk 的列表,并且同时运行它们: ```python run_concurrently([myfunc, anotherfunc]) ``` -但问题在于你必须知道你将要运行的任务的完整列表,而这并非总是如此。例如,服务器程序通常具有接受循环,它接收传入的连接并开始一个新的任务来处理它们中的每一个。这是Trio中最小的接受循环: +但问题在于你必须知道你将要运行的任务的完整列表,而这并非总是如此。例如,服务器程序通常具有接受循环,它接收传入的连接并开始一个新的任务来处理它们中的每一个。这是 Trio 中最小的接受循环: ```python async with trio.open_nursery() as nursery: @@ -288,11 +288,11 @@ async with trio.open_nursery() as nursery: nursery.start_soon(connection_handler, incoming_connection) ``` -有了 Nurseries ,这是微不足道的,但使用实现它 run_concurrently 会比较尴尬。如果你愿意,可以很容易地在 nurseries 之上实现 run_concurrently - 但这并不是必须的,因为在 run_concurrently 可以处理的简单情况下 ,nursery符号就像可读的一样。 +有了 Nurseries ,这是微不足道的,但使用实现它 run_concurrently 会比较尴尬。如果你愿意,可以很容易地在 nurseries 之上实现 run_concurrently - 但这并不是必须的,因为在 run_concurrently 可以处理的简单情况下 ,nursery 符号就像可读的一样。 ### 有一个逃逸 - Nurseries 对象也给我们一个逃逸的出口。如果你确实需要编写一个产生背景任务的函数,那么背景任务会超出函数本身呢?这也很容易:通过功能一个 Nurseries 对象。直接在open_nursery()块内异步的代码调用 nursery.start_soon - 只要nursery块保持打开 [4],那么任何获得对该 Nurseries 对象的引用的人都可以获得派发任务的能力进入那个 Nurseries 。你可以将它作为函数参数传递,通过队列发送。 + Nurseries 对象也给我们一个逃逸的出口。如果你确实需要编写一个产生背景任务的函数,那么背景任务会超出函数本身呢?这也很容易:通过功能一个 Nurseries 对象。直接在 open_nursery()块内异步的代码调用 nursery.start_soon - 只要 nursery 块保持打开 [4],那么任何获得对该 Nurseries 对象的引用的人都可以获得派发任务的能力进入那个 Nurseries 。你可以将它作为函数参数传递,通过队列发送。 在实践中,这意味着你可以编写“违反规则”的函数,但在以下限制内: @@ -319,7 +319,7 @@ async with my_supervisor_library.open_supervisor() as nursery_alike: 关于任务取消和任务加入如何相互作用也值得讨论,因为这里有一些微妙之处 - 如果处理不正确 - 就打破了 Nurseries 不变式。 -在Trio中,代码可能随时收到取消请求。请求取消之后,下一次代码执行“检查点”操作([详细信息](https://trio.readthedocs.io/en/latest/reference-core.html#checkpoints))时,会引发取消异常。这意味着请求取消与实际 发生时间之间存在差距- 任务执行检查点之前可能需要一段时间,然后异常必须展开堆栈,运行清理处理程序等。发生时, Nurseries 总是等待全面清理。我们永远不会终止任务,也不会让它有机会运行清理处理程序,而我们永远不会 即使在 Nurseries 正在取消的过程中,也可以让任务在 Nurseries 外无人监管。 +在 Trio 中,代码可能随时收到取消请求。请求取消之后,下一次代码执行“检查点”操作([详细信息](https://trio.readthedocs.io/en/latest/reference-core.html#checkpoints))时,会引发取消异常。这意味着请求取消与实际 发生时间之间存在差距- 任务执行检查点之前可能需要一段时间,然后异常必须展开堆栈,运行清理处理程序等。发生时, Nurseries 总是等待全面清理。我们永远不会终止任务,也不会让它有机会运行清理处理程序,而我们永远不会 即使在 Nurseries 正在取消的过程中,也可以让任务在 Nurseries 外无人监管。 ### 自动清理资源的工作 @@ -339,7 +339,7 @@ async with my_supervisor_library.open_supervisor() as nursery_alike: ### 一个令人惊讶的好处:移除 `go` 语句开启一个新的特性 -消除 goto 使得以前的语言设计师能够对程序结构做出更强的假设,从而实现了块和例外等新功能; 消除 go 语句也有类似的效果。例如: +消除 goto 使得以前的语言设计师能够对程序结构做出更强的假设,从而实现了块和例外等新功能; 消除 Go 语句也有类似的效果。例如: - Trio 的注销系统(cancellation system)比竞争对手更容易使用和更可靠,因为它可以假定任务嵌套在常规树形结构中; 请参阅 [完超时和人为取消](https://vorpus.org/blog/timeouts-and-cancellation-for-humans/) 。 - Trio 是唯一的 Python 并发库,其中 control-C 以 Python 开发人员期望的方式工作([详细信息](https://vorpus.org/blog/control-c-handling-in-python-and-trio/))。Nurseries 提供可靠的机制来处理异常。 @@ -350,19 +350,19 @@ async with my_supervisor_library.open_supervisor() as nursery_alike: 这是一个实践问题:你应该尝试一下并找出答案!但是,严重的是,我们经历过问题才明白过来。在这一点上,我非常确信基础是健全的,但是也许我们会意识到我们需要做一些调整,比如早期的结构化编程倡导者最终如何从消除 `break` 和 `continue` 中得到回应。 -如果你是一位经验丰富的并发程序员,他们只是学习Trio,那么你应该预料到这需要习惯它。你将不得不学习新的方法来做事情 - 就像在20世纪70年代的程序员一样,学习如何在没有 `goto` 下编写代码是一个挑战。 +如果你是一位经验丰富的并发程序员,他们只是学习 Trio,那么你应该预料到这需要习惯它。你将不得不学习新的方法来做事情 - 就像在 20 世纪 70 年代的程序员一样,学习如何在没有 `goto` 下编写代码是一个挑战。 -但当然,这是关键。正如Knuth所写的(Knuth,1974,p.275): +但当然,这是关键。正如 Knuth 所写的(Knuth,1974,p.275): > 也许最糟糕的错误任何一个可以相对于标的做出去报表是假设“结构化编程”是通过编写程序来实现,因为我们总是有,然后消除去的。大部分去的不应该在那里!我们真正需要的是这样一种方式,我们很少甚至设想我们的计划想约去 陈述,因为他们真正需要的几乎没有出现。我们表达思想的语言对我们的思维过程有着强烈的影响。因此,迪克斯特拉要求更多新的语言特征 - 鼓励清晰思考的结构 - 以避免这样做对并发症的诱惑。 到目前为止,这是我使用 Nurseries 的经验:它鼓励清晰的思维。它导致设计更加健壮,更易于使用,而且更好。而这些限制实际上使解决问题变得更容易,因为您花费更少的时间去尝试不必要的复杂问题。在一个非常真实的意义上,使用 Trio 已经教会我成为一个更好的程序员。 -例如,考虑 Happy Eyeballs 算法([RFC 8305](https://tools.ietf.org/html/rfc8305)),这是一种简单的并发算法,用于加快建立TCP连接。从概念上来说,算法并不复杂 - 您尝试了多次连接尝试,并且为了避免网络过载而采用错开的方式。但是如果你看看[Twisted的最佳实现](https://github.com/twisted/twisted/compare/trunk...glyph:statemachine-hostnameendpoint),它几乎有600行Python,并且仍然[至少有一个逻辑错误](https://twistedmatrix.com/trac/ticket/9345)。 比起 Trio 的同类型项目缩短了15倍以上。更重要的是,使用Trio,我可以在几分钟内写出它,而不是几个月,而且我在第一次尝试时就得到了正确的逻辑。我从来不可能在其他任何框架中做到这一点,即使我有更多的经验。有关更多详细信息,您可以观看 [我上个月在Pyninsula的演讲](https://www.youtube.com/watch?v=i-R704I8ySE)。这只是个例吗?时间会证明一切,它充满着希望。 +例如,考虑 Happy Eyeballs 算法([RFC 8305](https://tools.ietf.org/html/rfc8305)),这是一种简单的并发算法,用于加快建立 TCP 连接。从概念上来说,算法并不复杂 - 您尝试了多次连接尝试,并且为了避免网络过载而采用错开的方式。但是如果你看看[Twisted 的最佳实现](https://github.com/twisted/twisted/compare/trunk...glyph:statemachine-hostnameendpoint),它几乎有 600 行 Python,并且仍然[至少有一个逻辑错误](https://twistedmatrix.com/trac/ticket/9345)。 比起 Trio 的同类型项目缩短了 15 倍以上。更重要的是,使用 Trio,我可以在几分钟内写出它,而不是几个月,而且我在第一次尝试时就得到了正确的逻辑。我从来不可能在其他任何框架中做到这一点,即使我有更多的经验。有关更多详细信息,您可以观看 [我上个月在 Pyninsula 的演讲](https://www.youtube.com/watch?v=i-R704I8ySE)。这只是个例吗?时间会证明一切,它充满着希望。 ## 结论 -流行的并发原语 - go 语句, thread spawning functions, callbacks, futures, promises 等等,它们都是 goto 的变体,理论上和实践上都是如此。即使是现代化的驯化过的 `goto`,但旧的 `goto` 是可以跳出函数边界的。即使我们不直接使用它们,这些原语也是危险的,因为它们破坏了我们推理控制流的能力,并且从抽象的模块化部分组成复杂的系统,并干扰了自动资源清理和错误传播等有用的语言功能。因此,像 goto 一样,他们在现代高级语言中没有地位。 +流行的并发原语 - Go 语句, thread spawning functions, callbacks, futures, promises 等等,它们都是 goto 的变体,理论上和实践上都是如此。即使是现代化的驯化过的 `goto`,但旧的 `goto` 是可以跳出函数边界的。即使我们不直接使用它们,这些原语也是危险的,因为它们破坏了我们推理控制流的能力,并且从抽象的模块化部分组成复杂的系统,并干扰了自动资源清理和错误传播等有用的语言功能。因此,像 goto 一样,他们在现代高级语言中没有地位。 Nurseries 提供了一种安全方便的替代方案,保留了您语言的全部功能,实现了强大的新功能(如 Trio 的取消范围和控制 C 处理所证明的),并且可以显着提高可读性,生产力和正确性。 diff --git a/published/tech/20180502-Introducing-Corral-A-Serverless-MapReduce-Framework.md b/published/tech/20180502-Introducing-Corral-A-Serverless-MapReduce-Framework.md index 965041c3f..c199bb737 100644 --- a/published/tech/20180502-Introducing-Corral-A-Serverless-MapReduce-Framework.md +++ b/published/tech/20180502-Introducing-Corral-A-Serverless-MapReduce-Framework.md @@ -16,15 +16,15 @@ Hadoop 和 Spark 也需要了解一些基础设施知识。一些服务像 [EMR] ![](https://static.studygolang.com/gctt/introducing-corral/architecture.svg) -这是 [corral](https://github.com/bcongdon/corral) 的结果,一个用于编写可在 AWS Lambda 中执行的任意 MapReduce 应用程序框架。 +这是 [corral](https://github.com/bcongdon/corral) 的结果,一个用于编写可在 AWS Lambda 中执行  的任意 MapReduce 应用程序框架。 ## MapReduce 的 Golang 接口 -众所周知,Go 没有泛型,所以我不得不为 mappers 和 reducers 构建一个令人信服的接口而动些脑筋。Hadoop MapReduce 在指定输入/输出格式,分割记录的方式等方面有很大的灵活性。 +众所周知,Go 没有泛型,所以我不得不  为 mappers 和 reducers 构建一个令人信服的接口而动些脑筋。Hadoop MapReduce 在  指定输入/输出格式,分割记录  的方式等方面有很  大的灵活性。 -我之前考虑用 interface{} 类型做为健和值,但用 [Rob Pike 的话](https://www.youtube.com/watch?v=PAAkCSZUG1c&t=7m36s)说,“interface{} 什么也没说”。所以我决定使用极简主义接口:keys 和 values 都用字符串,输入文件按换行符分割。这些简化假设使整个系统的实现更简单和清晰。Hadoop MapReduce 赢得可定制性,因此我决定采用易用性。 +我之前考虑用 interface{} 类型做为健和值,但用 [Rob Pike 的话](https://www.youtube.com/watch?v=PAAkCSZUG1c&t=7m36s)说,“interface{} 什么也没说”。所以我决定使用  极简主义接口:keys 和 values 都用字符串,输入文件按换行符分割。这些简化假设使整个系统的实现更简单和清晰。Hadoop MapReduce 赢得可定制性,因此我决定采用易用性。 -我很满意 Map 和 Reduce 的最终接口(其中一些是受 Damian Gryski 的 [dmrgo](https://github.com/dgryski/dmrgo) 启发): +我很满意 Map 和 Reduce 的最  终接口(其中一些是受 Damian Gryski 的 [dmrgo](https://github.com/dgryski/dmrgo) 启发): ```go type Mapper interface { @@ -40,25 +40,25 @@ type Emitter interface { } ``` -`ValueIterator` 只有一个方法:`Iter()`,迭代一系列字符串。 +`ValueIterator`  只有一个方法:`Iter()`,迭代一系列字符串。 -`Emitter` 和 `ValueIterator` 隐藏了需要内部框架实现(改组,分区,文件系统交互等)。我也很高兴决定对值用迭代器来代替普通的切片(这可能更惯用),因为迭代器允许框架方面更加的灵活(例如:延迟流值而不是全部放入内存)。 +`Emitter` 和 `ValueIterator` 隐藏了需要内部框架实现(改组,分区,文件系统交互等)。我也很高兴决定对值用迭代器来代替  普通的切片(这可能更惯用),因为迭代器允许框架方面更加的灵活(例如:延迟流值而不是全部放入内存)。 ## 无服务 MapReduce -从框架方面,我花了些时间来决定用一个高效的方式将 MapReduce 实现为一个完全无状态的系统。 +从框架方面,我花了  些时间来决定  用一个高效的方式将 MapReduce 实现为一个  完全无状态的系统。 Hadoop MapReduce 架构为其带来以下好处…… - 持久,长时间运行的工作节点 - 数据局部性在工作节点 -- 通过 YARN/Mesos 等作为抽象的,容错的主节点和工作节点容器。 +-  通过 YARN/Mesos 等作为抽象的,容错的主节点和工作  节点容器。 使用 AWS 堆栈可以很容易地复制最后两方面。S3 和 Lambda 之间的带宽相对不错(至少对我而言),而 Lambda 的构建使得开发人员“不必考虑服务器”。 -在 Lambda 上复制最棘手的事情是持久工作节点。Lambda 有最大5分钟的超时时限。因此,Hadoop 使用 MapReduce 的很多方式都不再适用。例如,在 mapper worker 和 reducer worker 之间直接传输数据是不可行的,因为 mapper 需要“尽快”完成。否则,在 reducer 仍在工作时,您可能会冒 mapper 超时的风险。 +在 Lambda 上复制最棘手的事情是持久工作节点。Lambda 有最大 5 分钟的超时时限。因此,Hadoop 使用 MapReduce 的很多方式都不再适用。例如,在 mapper worker 和 reducer worker 之间直接传输数据是不可行的,因为 mapper 需要“尽快”完成。否则,在 reducer 仍在工作时,您可能会冒 mapper 超时的风险。 -这种限制在 shuffle/partition 阶段最明显。理想情况下,mappers 将“生存”足够长的时间以按需将数据传输到 reducers(即使在 map 阶段),并且 reducers 将“活”足够长时间去做一个完整的二级排序,使用它们的磁盘当对一个较大的合并排序溢出时。5分钟的上限使得这些方法难以实现。 +这种限制在 shuffle/partition 阶段最明显。理想情况下,mappers 将“生存”足够长的时间以按需将数据传输到 reducers(即使在 map 阶段),并且 reducers 将“活”足够长时间去做一个完整的二级排序,使用它们的磁盘当对一个较大的合并排序溢出时。5 分钟的上限使得这些方法难以实现。 最后,我决定使用 S3 作为无状态 partition/shuffle 的后端。 @@ -72,11 +72,11 @@ Hadoop MapReduce 架构为其带来以下好处…… ## 自发布应用 -Corroal 让我最兴奋的一点是,它能够自我部署到 AWS Lambda。我希望能够快速将 corral 作业部署到 Lambda 上——不得不通过 web 界面手动将发布包重新上传到 Lambda 上是一种拖累,而像 Serverless 这样的框架依赖于非 Go 工具,这些工具包含起来很繁琐。 +Corroal 让我最兴奋的一点是,它能够自我部署到 AWS Lambda。我希望能够快速将 corral 作业部署到 Lambda 上——不得不通过 Web 界面手动将发布包重新上传到 Lambda 上是一种拖累,而像 Serverless 这样的框架依赖于非 Go 工具,这些工具包含起来很繁琐。 我最初的想法是,构建 corral 二进制文件作为发布包上传到 Lambda 上。这个想法确实有效……直到您处理跨平台构建目标时。Lambda 期望使用 `GOOS=linux` 编译二进制文件,因此任何二进制文件在 macOS 或 Windows 上不能运行。 -我几乎放弃了这个想法,但后来我偶尔发现了 Kelsey Hightower 在2017年的GopherCon上发布的 [Self Deploying Kubernetes Applications](https://www.youtube.com/watch?v=XPC-hFL-4lU)。Kelsey 描述了一个类似的方法,尽管他的代码是在 Kubernetes 而不是 Lambda 上运行的。但是,他描述了我需要的“缺失链接”:让特定平台的二进制文件重新编译为目标 GOOO=linux。 +我几乎放弃了这个想法,但后来我偶尔发现了 Kelsey Hightower 在 2017 年的 GopherCon 上发布的 [Self Deploying Kubernetes Applications](https://www.youtube.com/watch?v=XPC-hFL-4lU)。Kelsey 描述了一个类似的方法,尽管他的代码是在 Kubernetes 而不是 Lambda 上运行的。但是,他描述了我需要的“缺失链接”:让特定平台的二进制文件重新编译为目标 GOOO=linux。 因此,总而言之,corral 用于部署到 Lambda 的过程如下: @@ -97,7 +97,7 @@ Corroal 让我最兴奋的一点是,它能够自我部署到 AWS Lambda。我 系统中的每个组件都运行相同的源,但有很多并行副本运行在 Lambda 上(由驱动协调)。这导致 MapReduce 快速的并行。 -## 像文件系统一样对待S3 +## 像文件系统一样对待 S3 像 mrjob 一样,Corral 试图与它运行到文件系统无关。这允许它在本地和 Lambda 执行之间透明地切换(并允许扩展空间,例如,如果 GCP 在云函数上开始支持 Go)。 diff --git a/published/tech/20180504-Debugging-latency-in-Go-1.11.md b/published/tech/20180504-Debugging-latency-in-Go-1.11.md index 776042030..27c40582a 100644 --- a/published/tech/20180504-Debugging-latency-in-Go-1.11.md +++ b/published/tech/20180504-Debugging-latency-in-Go-1.11.md @@ -36,7 +36,7 @@ 在 Go 1.11 下,我们将对执行追踪器有额外的支持,以便能指出 RPC 调用时的运行时事件。有了这个新特性,对于一个调用生命周期所发生的事,用户可以收集到更多的信息。 -在这个案例中,我们将聚焦于 auth.AccessToken 范围内的部分。在网络上一共花费了 30 + 18 µs,阻塞的系统调用 5µs,垃圾回收 21µs,真正执行 handler 花费了 123µs,其中大部分都花在序列化和反序列化上。 +在这个案例中,我们将聚焦于 auth.AccessToken 范围内的部分。在网络上一共花费了 30 + 18 µ s,阻塞的系统调用 5 µ s,垃圾回收 21 µ s,真正执行 handler 花费了 123 µ s,其中大部分都花在序列化和反序列化上。 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/debugging-latency/3.png) @@ -52,7 +52,7 @@ 执行追踪器引入两个上层的概念:*region* 及 *task*,以便用户来对他们的代码进行插桩。 -Region 是你希望收集追踪数据的代码区域。一个 region 开始和结束在同一个 goroutine 内。另一方面,task 是一个逻辑上的群组,将相关的 region 归在一起。一个 task 的开始和结束可以在不同的 goroutine 中。 +Region 是你希望收集追踪数据的代码区域。一个 region 开始和结束在同一个 Goroutine 内。另一方面,task 是一个逻辑上的群组,将相关的 region 归在一起。一个 task 的开始和结束可以在不同的 Goroutine 中。 我们预期用户为每个分布式追踪的 span 都启动一个执行追踪器,通过创建 region, 当问题发生时即刻启用执行追踪器,记录一些数据,分析输出,来对他们的 RPC 框架进行全面的插桩。 @@ -90,7 +90,7 @@ go func() { ``` $ curl http://server:6060/debug/pprof/trace?seconds=5 -o trace.out -$ go tool trace trace.out +$ Go tool trace trace.out 2018/05/04 10:39:59 Parsing trace... 2018/05/04 10:39:59 Splitting trace... 2018/05/04 10:39:59 Opening browser. Trace viewer is listening on http://127.0.0.1:51803 @@ -102,7 +102,7 @@ $ go tool trace trace.out *RPC task 的时间分布。* -你可以点击 3981µs 的那个异常的 bucket,进一步分析在那个特定 RPC 的生命周期里发生了什么。 +你可以点击 3981 µ s 的那个异常的 bucket,进一步分析在那个特定 RPC 的生命周期里发生了什么。 同时,/userregions 让你列出收集到的 region。你可以看到 connection.init 这个 region 以及所对应的多条记录。(注意到 connection.init 是为了演示而手动集成到 gRPC 框架的源码中的,更多的插桩工作还在进行中。) @@ -110,19 +110,19 @@ $ go tool trace trace.out *region 的时间分布。* -如果你点击了任意一个链接,它会给你更多关于处于那个延迟 bucket 中的 region 的详细信息。在下面的例子中,我们看到有一个 region 位于 1000µs 的 bucket。 +如果你点击了任意一个链接,它会给你更多关于处于那个延迟 bucket 中的 region 的详细信息。在下面的例子中,我们看到有一个 region 位于 1000 µ s 的 bucket。 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/debugging-latency/6.png) -*1000µs 的 region 在等待 GC 和调度器上花费了额外的时间。* +*1000 µ s 的 region 在等待 GC 和调度器上花费了额外的时间。* -这样你就看到了细粒度的延迟明细。你可以看到 1309µs 的 region 交叠了垃圾回收。这以垃圾回收和调度的形式在关键路径上增加了不少开销。除此之外,执行 handler 与处理阻塞的系统调用花费了差不多的时间。 +这样你就看到了细粒度的延迟明细。你可以看到 1309 µ s 的 region 交叠了垃圾回收。这以垃圾回收和调度的形式在关键路径上增加了不少开销。除此之外,执行 handler 与处理阻塞的系统调用花费了差不多的时间。 ## 局限 尽管新的执行追踪器的特性很强大,但还是有一些局限。 -- Region 只能在同一个 goroutine 中开始和结束。执行追踪器目前还不能自动记录跨越多个 goroutine 的数据。这就需要我们手动地插桩 region。下一个大的步伐将是在 RPC 框架及 net/http 这样的标准包里增加细粒度的插桩。 -- 执行追踪器输出的格式比较难解析,`go tool trace`是唯一的能理解这种格式的标准工具。并没有简单的方式能够自动将执行追踪器的数据与分布式追踪数据关联起来 - 所以我们分别搜集它们,之后再做关联。 +- Region 只能在同一个 Goroutine 中开始和结束。执行追踪器目前还不能自动记录跨越多个 Goroutine 的数据。这就需要我们手动地插桩 region。下一个大的步伐将是在 RPC 框架及 net/http 这样的标准包里增加细粒度的插桩。 +- 执行追踪器输出的格式比较难解析,`go tool trace` 是唯一的能理解这种格式的标准工具。并没有简单的方式能够自动将执行追踪器的数据与分布式追踪数据关联起来 - 所以我们分别搜集它们,之后再做关联。 ## 结论 diff --git a/published/tech/20180509-How-I-write-Go-HTTP-services.md b/published/tech/20180509-How-I-write-Go-HTTP-services.md index ecc7e8df4..49e46a856 100644 --- a/published/tech/20180509-How-I-write-Go-HTTP-services.md +++ b/published/tech/20180509-How-I-write-Go-HTTP-services.md @@ -47,7 +47,7 @@ func (s *server) routes() { func (s *server) handleSomething() http.HandlerFunc { ... } ``` -handler 可以通过 s 这个server变量来访问依赖项。 +handler 可以通过 s 这个 server 变量来访问依赖项。 ## 返回 handler diff --git a/published/tech/20180516-Dependency-Injection-in-Go.md b/published/tech/20180516-Dependency-Injection-in-Go.md index ed099f243..0d900de10 100644 --- a/published/tech/20180516-Dependency-Injection-in-Go.md +++ b/published/tech/20180516-Dependency-Injection-in-Go.md @@ -30,7 +30,7 @@ func New() *Server { 然而,这存在一些缺点。首先,如果我们想要改变我们 `Config` 的构建方式,我们不得不改变所有调用构建代码的地方。例如,假设我们的 `buildMyConfigSomehow` 函数现在需要一个参数。每个调用处都需要访问该参数并需要将其传递给构造函数。 -此外,这使得实现 `Config` 函数变得十分麻烦,我们得以某种方法进入 `new` 函数的内部,并创建`Config`。 +此外,这使得实现 `Config` 函数变得十分麻烦,我们得以某种方法进入 `new` 函数的内部,并创建 `Config`。 这是 DI 方式: @@ -46,7 +46,7 @@ func New(config *Config) *Server { } ``` -现在我们将 `Server` 与`Config` 分离。我们可以根据自己的逻辑创造 `Config` 然后将结果传递给 `New` 函数。 +现在我们将 `Server` 与 `Config` 分离。我们可以根据自己的逻辑创造 `Config` 然后将结果传递给 `New` 函数。 此外,如果 `Config` 是一个接口,这为我们提供了一个简单的模拟途径 。只要 `New` 实现了我们的接口,就可以传递任何我们想要的东西。这使得测试实现了 `Config` 接口的 `Server` 很简单。 @@ -61,7 +61,7 @@ DI 框架通常基于您告诉它的 “providers” 构建依赖图并确定如 ## 示例程序 -我们来看http服务器端的代码:客户端以 `GET` 方式请求 `/people` 路径时并返回 JSON 。我们将一步一步呈现代码,为简单起见,它们都存在于同一个包中(`main`)。请勿在真正的 Go 程序中执行此操作。可以在[此处](https://gitlab.com/drewolson/go_di_example)找到此示例的完整代码。 +我们来看 http 服务器端的代码:客户端以 `GET` 方式请求 `/people` 路径时并返回 JSON 。我们将一步一步呈现代码,为简单起见,它们都存在于同一个包中(`main`)。请勿在真正的 Go 程序中执行此操作。可以在[此处](https://gitlab.com/drewolson/go_di_example)找到此示例的完整代码。 首先,让我们看看我们的 `Person` 。仅有一些被 JSON 标签标记的属性。 @@ -93,7 +93,7 @@ func NewConfig() *Config { } ``` - `Enabled` 表示程序是否返回真实数据。`DatabasePath` 表示数据库的地址(使用 sqlite )。`Port` 表示服务器运行的端口。 + `Enabled` 表示程序是否返回真实数据。`DatabasePath` 表示数据库的地址(使用 SQLite )。`Port` 表示服务器运行的端口。 下方函数用来打开数据库连接。它依赖于 `Config` 并返回 `*sql.DB` 。 diff --git a/published/tech/20180516-Storing-Go-Structs-in-Redis-using-ReJSON.md b/published/tech/20180516-Storing-Go-Structs-in-Redis-using-ReJSON.md index c9e6e66e6..61bb0fdaf 100644 --- a/published/tech/20180516-Storing-Go-Structs-in-Redis-using-ReJSON.md +++ b/published/tech/20180516-Storing-Go-Structs-in-Redis-using-ReJSON.md @@ -10,7 +10,7 @@ 官方文档是这么描述 Redis 的: -> Redis 是一个开源(BSD 许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持的数据结构有字符串(strings),散列(hashes),列表(lists),集合(sets),有序集合(sorted sets)与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial)索引半径查询。 Redis 内置了复制(replication),LUA脚本(Lua scripting), LRU 驱动事件(LRU eviction),事务(transactions) 和不同级别的磁盘持久化(persistence), 并通过 Redis 哨兵(Sentinel)和自动分区(Cluster)提供高可用性(high availability)。 +> Redis 是一个开源(BSD 许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持的数据结构有字符串(strings),散列(hashes),列表(lists),集合(sets),有序集合(sorted sets)与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial)索引半径查询。 Redis 内置了复制(replication),LUA 脚本(Lua scripting), LRU 驱动事件(LRU eviction),事务(transactions) 和不同级别的磁盘持久化(persistence), 并通过 Redis 哨兵(Sentinel)和自动分区(Cluster)提供高可用性(high availability)。 将 Redis 与其它(传统)数据库区分开来的是,Redis 是一个键-值 存储(并且是在内存中)。这意味着在这个数据库中所有的值都与一个 key 相关联(想想字典的情况)。不过我跑题了,这篇文章可不是讲 Redis 的,让我们言归正传。 @@ -18,7 +18,7 @@ 当 Go 开发者使用 Redis 时,有时会需要将我们的对象缓存到 Redis 中。我们看看如何通过 Redis 中的 HMSET 来实现这点。 -一个简单的 go 结构体可能会像这样, +一个简单的 Go 结构体可能会像这样, ```go type SimpleObject struct { @@ -49,11 +49,11 @@ fieldB 好吧,现在我们知道对象是怎样序列化后存入数据库中的,让我们继续用程序的方式完成这个工作! -虽然 Redis 的 Go 客户端很多,但我使用 redigo,它在 github 上有一个很不错的社区,而且也是最常用的 Redis 的 Go 客户端之一,有超过 4K 个星星。 +虽然 Redis 的 Go 客户端很多,但我使用 redigo,它在 GitHub 上有一个很不错的社区,而且也是最常用的 Redis 的 Go 客户端之一,有超过 4K 个星星。 ### Redigo 助手函数 — AddFlat 和 ScanStruct -Redigo自带了一系列很棒的助手函数,其中我们将用到 AddFlat ,在我们将结构体存入 Redis 之前,用它将结构体扁平化。 +Redigo 自带了一系列很棒的助手函数,其中我们将用到 AddFlat ,在我们将结构体存入 Redis 之前,用它将结构体扁平化。 ```go // 获得链接对象 @@ -268,12 +268,12 @@ if err != nil { docker run -p 6379:6379 --name Redis-rejson Redislabs/rejson:latest ``` -### 从 github 上克隆这个例子 +### 从 GitHub 上克隆这个例子 ``` -# git clone https://github.com/nitishm/rejson-struct.git +# Git clone https://github.com/nitishm/rejson-struct.git # cd rejson-struct -# go run main.go +# Go run main.go ``` 想要了解更多 **Go-REJSON** 包,请访问 https://github.com/nitishm/go-rejson. diff --git a/published/tech/20180517-Learning-Go-Concurrency-Through-Illustrations.md b/published/tech/20180517-Learning-Go-Concurrency-Through-Illustrations.md index 223450c3c..a7790dad2 100644 --- a/published/tech/20180517-Learning-Go-Concurrency-Through-Illustrations.md +++ b/published/tech/20180517-Learning-Go-Concurrency-Through-Illustrations.md @@ -37,13 +37,13 @@ From Smelter: [smeltedOre smeltedOre smeltedOre] ![ore mining concurrent program](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/ore-mining-concurrent-program.jpeg) -这种设计使得 “挖矿” 更高效。现在多个线程 (gophers) 是独立运行的,从而 Gary 不再承担全部工作。其中一个 gopher 负责寻矿,一个负责挖矿,另一个负责练矿,这些工作可能同时进行。 +这种设计使得 “挖矿” 更高效。现在多个线程 (gophers) 是独立运行的,从而 Gary 不再承担全部工作。其中一个 Gopher 负责寻矿,一个负责挖矿,另一个负责练矿,这些工作可能同时进行。 为了将这种并发特性引入我们的代码,我们需要创建独立运行的 gophers 的方法以及它们之间彼此通信 (传送矿石) 的方法。这就需要用到 Go 的并发原语:goroutines 和 channels。 ## Goroutines -Goroutines 可以看作是轻量级线程。创建一个 goroutine 非常简单,只需要把 *go* 关键字放在函数调用语句前。为了说明这有多么简单,我们创建两个 finder 函数,并用 *go* 调用,让它们每次找到 "ore" 就打印出来。 +Goroutines 可以看作是轻量级线程。创建一个 Goroutine 非常简单,只需要把 *go* 关键字放在函数调用语句前。为了说明这有多么简单,我们创建两个 finder 函数,并用 *go* 调用,让它们每次找到 "ore" 就打印出来。 ![go myFunc()](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/go.jpeg) @@ -75,7 +75,7 @@ Finder 2 found ore! ![communication](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/communication.jpeg) -Channels 允许 go routines 之间相互通信。你可以把 channel 看作管道,goroutines 可以往里面发消息,也可以从中接收其它 go routines 的消息。 +Channels 允许 Go routines 之间相互通信。你可以把 channel 看作管道,goroutines 可以往里面发消息,也可以从中接收其它 Go routines 的消息。 ![my first channel](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/channel.jpeg) @@ -92,11 +92,11 @@ myFirstChannel <-"hello" // Send myVariable := <- myFirstChannel // Receive ``` -现在通过 channel 我们可以让寻矿 gopher 一找到矿石就立即传送给开矿 gopher ,而不用等发现所有矿石。 +现在通过 channel 我们可以让寻矿 Gopher 一找到矿石就立即传送给开矿 Gopher ,而不用等发现所有矿石。 ![ore channel](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/ore-channel.jpeg) -我重写了挖矿程序,把寻矿和开矿函数改写成了未命名函数。如果你从未见过 lambda 函数,不必过多关注这部分,只需要知道每个函数将通过 *go* 关键字调用并运行在各自的 goroutine 中。重要的是,要注意 goroutine 之间是如何通过 channel ```oreChan``` 传递数据的。别担心,我会在最后面解释未命名函数的。 +我重写了挖矿程序,把寻矿和开矿函数改写成了未命名函数。如果你从未见过 lambda 函数,不必过多关注这部分,只需要知道每个函数将通过 *go* 关键字调用并运行在各自的 Goroutine 中。重要的是,要注意 Goroutine 之间是如何通过 channel ```oreChan``` 传递数据的。别担心,我会在最后面解释未命名函数的。 ```go func main() { @@ -139,17 +139,17 @@ Channels 阻塞 goroutines 发生在各种情形下。这能在 goroutines 各 ![blocking on send](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/blocking-on-send.jpeg) -一旦一个 goroutine(gopher) 向一个 channel 发送数据,它就被阻塞了,直到另一个 goroutine 从该 channel 取走数据。 +一旦一个 goroutine(gopher) 向一个 channel 发送数据,它就被阻塞了,直到另一个 Goroutine 从该 channel 取走数据。 ### Blocking on a Receive ![blocking on receive](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/blocking-on-receive.jpeg) -和发送时情形类似,一个 goroutine 可能阻塞着等待从一个 channel 获取数据,如果还没有其他 goroutine 往该 channel 发送数据。 +和发送时情形类似,一个 Goroutine 可能阻塞着等待从一个 channel 获取数据,如果还没有其他 Goroutine 往该 channel 发送数据。 -一开始接触阻塞的概念可能令人有些困惑,但你可以把它想象成两个 goroutines(gophers) 之间的交易。 其中一个 gopher 无论是等着收钱还是送钱,都需要等待交易的另一方出现。 +一开始接触阻塞的概念可能令人有些困惑,但你可以把它想象成两个 goroutines(gophers) 之间的交易。 其中一个 Gopher 无论是等着收钱还是送钱,都需要等待交易的另一方出现。 -既然已经了解 goroutine 通过 channel 通信可能发生阻塞的不同情形,让我们讨论两种不同类型的 channels: *unbuffered* 和 *buffered* 。选择使用哪一种 channel 可能会改变程序的运行表现。 +既然已经了解 Goroutine 通过 channel 通信可能发生阻塞的不同情形,让我们讨论两种不同类型的 channels: *unbuffered* 和 *buffered* 。选择使用哪一种 channel 可能会改变程序的运行表现。 ### Unbuffered Channels @@ -161,7 +161,7 @@ Channels 阻塞 goroutines 发生在各种情形下。这能在 goroutines 各 ![buffered channel](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/buffered-channel.jpeg) -在并发程序中,时间协调并不总是完美的。在挖矿的例子中,我们可能遇到这样的情形:开矿 gopher 处理一块矿石所花的时间,寻矿 gohper 可能已经找到 3 块矿石了。为了不让寻矿 gopher 浪费大量时间等着给开矿 gopher 传送矿石,我们可以使用 *buffered* channel。我们先创建一个容量为 3 的 buffered channel。 +在并发程序中,时间协调并不总是完美的。在挖矿的例子中,我们可能遇到这样的情形:开矿 Gopher 处理一块矿石所花的时间,寻矿 gohper 可能已经找到 3 块矿石了。为了不让寻矿 Gopher 浪费大量时间等着给开矿 Gopher 传送矿石,我们可以使用 *buffered* channel。我们先创建一个容量为 3 的 buffered channel。 ```go bufferedChan := make(chan string, 3) @@ -210,7 +210,7 @@ third 为了简单起见,我们在最终的程序中不使用 buffered channels。但知道该使用哪种 channel 是很重要的。 -> 注意: 使用 buffered channels 并不会避免阻塞发生。例如,如果寻矿 gopher 比开矿 gopher 执行速度快 10 倍,并且它们通过一个容量为 2 的 buffered channel 进行通信,那么寻矿 gopher 仍会发生多次阻塞。 +> 注意: 使用 buffered channels 并不会避免阻塞发生。例如,如果寻矿 Gopher 比开矿 Gopher 执行速度快 10 倍,并且它们通过一个容量为 2 的 buffered channel 进行通信,那么寻矿 Gopher 仍会发生多次阻塞。 ## 把这些都放到一起 @@ -277,28 +277,28 @@ From Smelter: Ore is smelted ![anonymous goroutine](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/anonymous-go-routine.jpeg) -类似于如何利用 *go* 关键字使一个函数运行在自己的 goroutine 中,我们可以用如下方式创建一个匿名函数并运行在它的 goroutine 中: +类似于如何利用 *go* 关键字使一个函数运行在自己的 Goroutine 中,我们可以用如下方式创建一个匿名函数并运行在它的 Goroutine 中: ```go -// Anonymous go routine +// Anonymous Go routine go func() { - fmt.Println("I'm running in my own go routine") + fmt.Println("I'm running in my own Go routine") }() ``` -如果只需要调用一次函数,通过这种方式我们可以让它在自己的 goroutine 中运行,而不需要创建一个正式的函数声明。 +如果只需要调用一次函数,通过这种方式我们可以让它在自己的 Goroutine 中运行,而不需要创建一个正式的函数声明。 ### main 函数是一个 goroutine ![main func](https://raw.githubusercontent.com/studygolang/gctt-images/master/Learning-Go-s-Concurrency-Through-Illustrations/main-func.jpeg) -main 函数确实运行在自己的 goroutine 中!更重要的是要知道,一旦 main 函数返回,它将关掉当前正在运行的其他 goroutines。这就是为什么我们在 main 函数的最后设置了一个定时器—它创建了一个 channel,并在 5 秒后发送一个值。 +main 函数确实运行在自己的 Goroutine 中!更重要的是要知道,一旦 main 函数返回,它将关掉当前正在运行的其他 goroutines。这就是为什么我们在 main 函数的最后设置了一个定时器—它创建了一个 channel,并在 5 秒后发送一个值。 ```go <-time.After(time.Second * 5) // Receiving from channel after 5 sec ``` -还记得 goroutine 从 channel 中读数据如何被阻塞直到有数据发送到里面吧?通过添加上面这行代码,main routine 将会发生这种情况。它会阻塞,以给其他 goroutines 5 秒的时间来运行。 +还记得 Goroutine 从 channel 中读数据如何被阻塞直到有数据发送到里面吧?通过添加上面这行代码,main routine 将会发生这种情况。它会阻塞,以给其他 goroutines 5 秒的时间来运行。 现在有更好的方式阻塞 main 函数直到其他所有 goroutines 都运行完。通常的做法是创建一个 *done channel*, main 函数在等待读取它时被阻塞。一旦完成工作,向这个 channel 发送数据,程序就会结束了。 @@ -313,7 +313,7 @@ func main() { doneChan <- "I'm all done!" }() - <-doneChan // block until go routine signals work is done + <-doneChan // block until Go routine signals work is done } ``` @@ -336,11 +336,11 @@ go func() { 由于 miner 需要读取 finder 发送给它的所有数据,遍历 channel 能确保我们接收到已经发送的所有数据。 -> 遍历 channel 会阻塞,直到有新数据被发送到 channel。在所有数据发送完之后避免 go routine 阻塞的唯一方法就是用 "close(channel)" 关掉 channel。 +> 遍历 channel 会阻塞,直到有新数据被发送到 channel。在所有数据发送完之后避免 Go routine 阻塞的唯一方法就是用 "close(channel)" 关掉 channel。 ### 对 channel 进行非阻塞读 -但你刚刚告诉我们 channel 如何阻塞 goroutine 的各种情形?!没错,不过还有一个技巧,利用 Go 的 *select case* 语句可以实现对 channel 的非阻塞读。通过使用这这种语句,如果 channel 有数据,goroutine 将会从中读取,否则就执行默认的分支。 +但你刚刚告诉我们 channel 如何阻塞 Goroutine 的各种情形?!没错,不过还有一个技巧,利用 Go 的 *select case* 语句可以实现对 channel 的非阻塞读。通过使用这这种语句,如果 channel 有数据,goroutine 将会从中读取,否则就执行默认的分支。 ```go myChan := make(chan string) diff --git a/published/tech/20180518-packaging-go-for-mac.md b/published/tech/20180518-packaging-go-for-mac.md index 5583afbc8..62aa12f58 100644 --- a/published/tech/20180518-packaging-go-for-mac.md +++ b/published/tech/20180518-packaging-go-for-mac.md @@ -100,7 +100,7 @@ DMG 文件是你分发程序的文件。它压缩了整个 `.app` 文件,可 模板只需要制作一次。 -打开磁盘工具。按 ⌘N 创建一个新的磁盘镜像。给它取个名字,配置好足以容纳你的程序包的空间大小。 +打开磁盘工具。按 ⌘ N 创建一个新的磁盘镜像。给它取个名字,配置好足以容纳你的程序包的空间大小。 ![avatar](https://raw.githubusercontent.com/studygolang/gctt-images/master/package-for-mac/2.png) @@ -124,7 +124,7 @@ DMG 文件是你分发程序的文件。它压缩了整个 `.app` 文件,可 当前的 DMG 没有经过压缩并且是可写的。这对于发布程序是不够的,所以我们必须做一下转换来修复。 -打开磁盘工具,选择镜像->转换。给文件起一个有意义的名称,其他设置保持不变。(镜像格式应为“压缩”) +打开磁盘工具,选择镜像-> 转换。给文件起一个有意义的名称,其他设置保持不变。(镜像格式应为“压缩”) ![avatar](https://raw.githubusercontent.com/studygolang/gctt-images/master/package-for-mac/6.png) @@ -142,7 +142,7 @@ DMG 文件是你分发程序的文件。它压缩了整个 `.app` 文件,可 上面介绍的一些一次性的工作可以自动化(比如创建图标库)。但是有些很难用自动化取得很好的结果(比如定制化 DMG 的视图)。幸运的是,除此之外的部分是很容易做到自动化的,包括每次重新打包一个新的发布版本所需要的步骤。hdiutil 这个命令可以帮助你来创建、挂在和转换镜像。 -[这里](https://gist.github.com/mholt/11008646c95d787c30806d3f24b2c844)是我写的自动化的示例。它会创建一个打包的 .app,如果你给它模板 .dmg 它可以创建最终的 .dmg。它甚至封装了上文所述的创建图标的命令,可以帮你创建全部的不同尺寸的图标。它做了所有的拷贝、挂载、卸载和镜像转换。你可以对你不喜欢的部分或者它不能工作的地方(非常有可能发生,哈哈)做改动。它有很好的注释,很容易阅读。你只需要告诉它二进制文件在哪里、资源文件在哪里、二进制文件的名字、1024像素的图标文件和有意义的程序名字。 +[这里](https://gist.github.com/mholt/11008646c95d787c30806d3f24b2c844)是我写的自动化的示例。它会创建一个打包的 .app,如果你给它模板 .dmg 它可以创建最终的 .dmg。它甚至封装了上文所述的创建图标的命令,可以帮你创建全部的不同尺寸的图标。它做了所有的拷贝、挂载、卸载和镜像转换。你可以对你不喜欢的部分或者它不能工作的地方(非常有可能发生,哈哈)做改动。它有很好的注释,很容易阅读。你只需要告诉它二进制文件在哪里、资源文件在哪里、二进制文件的名字、1024 像素的图标文件和有意义的程序名字。 这不是一个会继续维护的开源项目。如果要使用它,你可以将它整合进实际的应用场景中。 diff --git a/published/tech/20180522-how-to-leak-a-goroutine-then-fix-it.md b/published/tech/20180522-how-to-leak-a-goroutine-then-fix-it.md index 7a30cb691..b72cde22d 100644 --- a/published/tech/20180522-how-to-leak-a-goroutine-then-fix-it.md +++ b/published/tech/20180522-how-to-leak-a-goroutine-then-fix-it.md @@ -2,32 +2,32 @@ # 如何泄漏一个协程然后修复它 -很多 go 语言开发者都知道这句格言,[永远不要启动一个你不知道如何停止的协程](https://dave.cheney.net/2016/12/22/never-start-a-goroutine-without-knowing-how-it-will-stop),但是泄漏一个协程还是超级的简单。让我们看一种常碰到的泄漏协程的方式,然后修复它。 +很多 Go 语言开发者都知道这句格言,[永远不要启动一个你不知道如何停止的协程](https://dave.cheney.net/2016/12/22/never-start-a-goroutine-without-knowing-how-it-will-stop),但是泄漏一个协程还是超级的简单。让我们看一种常碰到的泄漏协程的方式,然后修复它。 为了实现这个,我们先建立一个包含一个自定义 `map` 类型的库,这个 `map` 类型的 key 在经过了一段可配置的时间后过期。我们把这个库叫做 [ttl](https://en.wikipedia.org/wiki/Time_to_live) ,这个库有一个 `API` 类似如下: ```go -// 创建一个生存周期为5分钟的map +// 创建一个生存周期为 5 分钟的 map m := ttl.NewMap(5*time.Minute) -//设置一个key +//设置一个 key m.Set("my-key", []byte("my-value")) -// 读取一个key +// 读取一个 key v, ok := m.Get("my-key") //得到 "my-value" fmt.Println(string(v)) -// true, key存在 +// true, key 存在 fmt.Println(ok) -// ... 过了5分钟之后 +// ... 过了 5 分钟之后 v, ok := m.Get("my-key") // 没有值 fmt.Println(string(v) == "") -// false, key已经过期了 +// false, key 已经过期了 fmt.Println(ok) ``` -为了确保key会过期,我们在NewMap函数中启动一个协程。 +为了确保 key 会过期,我们在 NewMap 函数中启动一个协程。 ```go func NewMap(expiration time.Duration) *Map { @@ -86,7 +86,7 @@ func work() { if _, ok := m.Get("my-key"); !ok { panic("no value present") } - // m超出变量范围 + // m 超出变量范围 } ``` 不用很长时间,我们就可以看到分配的堆内存和运行的协程数增长得非常,非常的快。 @@ -139,7 +139,7 @@ func NewMap(expiration time.Duration) *Map { } ``` -现在工作协程包含了一个 `select` 语句,它会检查 `done通道` 也会检查 `ticker 的通道`,主要的,我们还删除了 [time.Tick](https://godoc.org/time#Tick),因为它并不能让协程顺利关闭还是会造成泄漏。 +现在工作协程包含了一个 `select` 语句,它会检查 `done 通道` 也会检查 `ticker 的通道`,主要的,我们还删除了 [time.Tick](https://godoc.org/time#Tick),因为它并不能让协程顺利关闭还是会造成泄漏。 经过以上的修改,我们简化的统计数据看起像这样: ``` diff --git a/published/tech/20180524-uh-oh-in-slice-of-pointers.md b/published/tech/20180524-uh-oh-in-slice-of-pointers.md index 351bcff1f..de5b71162 100644 --- a/published/tech/20180524-uh-oh-in-slice-of-pointers.md +++ b/published/tech/20180524-uh-oh-in-slice-of-pointers.md @@ -50,7 +50,7 @@ func main() { 运行上面的代码片段,你得到的输出是 ``` -➜ sample go run main.go +➜ sample Go run main.go #9 #9 #9 @@ -102,7 +102,7 @@ func main() { 调式代码的输出为 ``` -➜ sample go run main.go +➜ sample Go run main.go Adding number #0 to the slice Adding number #1 to the slice Adding number #2 to the slice @@ -192,7 +192,7 @@ for _, n := range listOfNumberStrings { 从最后一个迭代,我们知道最后被存储的值是 `"#9"`, 因此输出才像下面那样。 ``` -➜ sample go run main.go +➜ sample Go run main.go #9 #9 #9 @@ -245,7 +245,7 @@ func main() { 上面的代码的输出将会是 ``` -➜ sample go run main.go +➜ sample Go run main.go #0 #1 #2 diff --git a/published/tech/20180603-why-doesnt-go-have-variance-in.md b/published/tech/20180603-why-doesnt-go-have-variance-in.md index 1cb351226..044714c5c 100644 --- a/published/tech/20180603-why-doesnt-go-have-variance-in.md +++ b/published/tech/20180603-why-doesnt-go-have-variance-in.md @@ -6,9 +6,9 @@ 一个 Go 初学者经常问的问题是“为什么我不能把 `[]int` 类型变量传递给函数 `func ([]interface{ })`”?在这篇文章中,我想探讨这个问题及其对 Go 的影响。但是可变性(本文所描述)的概念在其他语言也是有用的。 -可变性描述了子类型关系应用在复合类型中使用时发生的情况。在这种情况下,“A是B的子类型”意味着,A类型的实例始终可以被用作需要B类型的场景。Go 没有明确的子类型关系,最接近的是可赋值性,它主要决定类型是否可以互换使用。接口也许是最重要的使用场景:如果类型T(无论它是具体类型,还是本身是接口)实现接口I,然后T可以被看作是I的子类型。从这个意义上讲, `*bytes.Buffer` 是 `io.ReadWriter` 的子类型,`io.ReadWriter` 是 `io.Reader` 的子类型。所有类型都是 `interface{}` 的子类型。 +可变性描述了子类型关系应用在复合类型中使用时发生的情况。在这种情况下,“A 是 B 的子类型”意味着,A 类型的实例始终可以被用作需要 B 类型的场景。Go 没有明确的子类型关系,最接近的是可赋值性,它主要决定类型是否可以互换使用。接口也许是最重要的使用场景:如果类型 T(无论它是具体类型,还是本身是接口)实现接口 I,然后 T 可以被看作是 I 的子类型。从这个意义上讲, `*bytes.Buffer` 是 `io.ReadWriter` 的子类型,`io.ReadWriter` 是 `io.Reader` 的子类型。所有类型都是 `interface{}` 的子类型。 -理解可变性含义的最简单方法是查看函数类型。假设我们有一个类型和一个子类型,例如 `*bytes.Buffer` 是 `io.Reader` 的子类型。可以定义这样一个函数 `func() *bytes.Buffer`。我们也可以把这个函数用作 `func() io.Reader`,我们只是把返回值重新定义为 `io.Reader`。但反方向的不成立的:我们不能把函数 `func() io.Reader` 用作函数 `func() *bytes.Buffer`,因为不是每个 `io.Reader` 都可以成为 `*bytes.Buffer`。因此,函数返回值可以保持子类型关系的方向为:如果A是B的子类型,则函数 `func() A` 可以是函数 `func() B` 的子类型。这叫做协变。 +理解可变性含义的最简单方法是查看函数类型。假设我们有一个类型和一个子类型,例如 `*bytes.Buffer` 是 `io.Reader` 的子类型。可以定义这样一个函数 `func() *bytes.Buffer`。我们也可以把这个函数用作 `func() io.Reader`,我们只是把返回值重新定义为 `io.Reader`。但反方向的不成立的:我们不能把函数 `func() io.Reader` 用作函数 `func() *bytes.Buffer`,因为不是每个 `io.Reader` 都可以成为 `*bytes.Buffer`。因此,函数返回值可以保持子类型关系的方向为:如果 A 是 B 的子类型,则函数 `func() A` 可以是函数 `func() B` 的子类型。这叫做协变。 ```go func F() io.Reader { @@ -30,7 +30,7 @@ func main() { ``` -另一方面,假设我们有函数 `func(*bytes.Buffer)`。现在我们不能把它当作函数 `func(io.Reader)`,你不能用 `io.Reader` 作为参数来调用它。但我们可以反方向调用。如果我们用 `*bytes.Buffer` 作为参数,可以用它调用 `func(io.Reader)`。因此,函数的参数颠倒了子类型关系:如果A是B的子类型,那么 `func(B)`可以是 `func(A)` 的子类型。这叫做逆变。 +另一方面,假设我们有函数 `func(*bytes.Buffer)`。现在我们不能把它当作函数 `func(io.Reader)`,你不能用 `io.Reader` 作为参数来调用它。但我们可以反方向调用。如果我们用 `*bytes.Buffer` 作为参数,可以用它调用 `func(io.Reader)`。因此,函数的参数颠倒了子类型关系:如果 A 是 B 的子类型,那么 `func(B)` 可以是 `func(A)` 的子类型。这叫做逆变。 ```go func F(r io.Reader) { @@ -54,7 +54,7 @@ func main() { ``` -因此,`func` 对于参数是逆变值的,对于返回值是协变的。当然,我们可以将这两种性质结合起来:如果A和C分别是B和D的子类型,我们可以使 `func(B) C` 成为 `func(A) D` 的子类型,可以这样转换: +因此,`func` 对于参数是逆变值的,对于返回值是协变的。当然,我们可以将这两种性质结合起来:如果 A 和 C 分别是 B 和 D 的子类型,我们可以使 `func(B) C` 成为 `func(A) D` 的子类型,可以这样转换: ```go // *os.PathError implements error @@ -122,7 +122,7 @@ a := as[0] // func Get(as []A, i int) A as[1] = a // func Set(as []A, i int, a A) ``` -这明显出现了问题:类型A既作为参数出现,也作为返回类型出现。因此,它既有协变又有逆变。因此,在调用函数时有一个相对明确的答案来解释可变性如何工作,它只是对于 `slices` 没有太多的意义。读取 `slices` 需要协变,但写入 `slices` 需要逆变。换句话说,如果你需要使 `[]int` 成为 `[]interface{}` 的子类,你需要解释这段代码是如何工作的: +这明显出现了问题:类型 A 既作为参数出现,也作为返回类型出现。因此,它既有协变又有逆变。因此,在调用函数时有一个相对明确的答案来解释可变性如何工作,它只是对于 `slices` 没有太多的意义。读取 `slices` 需要协变,但写入 `slices` 需要逆变。换句话说,如果你需要使 `[]int` 成为 `[]interface{}` 的子类,你需要解释这段代码是如何工作的: ```go func G() { @@ -139,7 +139,7 @@ func F(v []interface{}) { `channel` 提供了另一个有趣的视角。双向 `channel` 类型具有与 `slices` 类型相同的问题:接收时需要协变,而发送时需要逆变。但你可以限制 `channel` 的方向,只允许发送或接收操作。所以 `chan A` 和 `chan B` 可以没有关系,我们可以使 `<-chan A` 成为 `<-chan B` 的子类,或 `chan<-B` 成为 `chan<-A` 的子类。 -在这种意义上,只读类型至少在理论可以允许 `slices` 的可变性。`[]int` 仍然不是 `[]interface{}` 的子类型,我们可以使 `ro[] int` 成为 `ro []interface` 的子类型(借用proposal中的语法)。 +在这种意义上,只读类型至少在理论可以允许 `slices` 的可变性。`[]int` 仍然不是 `[]interface{}` 的子类型,我们可以使 `ro[] int` 成为 `ro []interface` 的子类型(借用 proposal 中的语法)。 最后,我想强调的是,所有这些都只是理论上为 Go 类型系统添加可变性的问题。我认为这很难,但即使我们能解决这些问题,仍然会遇到一些实际问题。其中最紧迫的是子类型的内存结构不同: @@ -182,7 +182,7 @@ func main() { } ``` -还需要在某个地方将H返回的 `io.ReadWriter` 接口包装成 `io.Reader` 接口,并需要在某个地方将G的返回的 `*bytes.Buffer` 可转换为正确的 `io.Reader` 接口。这对于函数来说,不是一个大问题:编译器可以在 `main` 函数调用时生成合适的包装。当代码中使用这种形式的子类型时会有一定的性能开销。然而,这对于 `slices` 来说是一个很重要的问题。 +还需要在某个地方将 H 返回的 `io.ReadWriter` 接口包装成 `io.Reader` 接口,并需要在某个地方将 G 的返回的 `*bytes.Buffer` 可转换为正确的 `io.Reader` 接口。这对于函数来说,不是一个大问题:编译器可以在 `main` 函数调用时生成合适的包装。当代码中使用这种形式的子类型时会有一定的性能开销。然而,这对于 `slices` 来说是一个很重要的问题。 对于 `slices` 我们有两种处理方式。(a)将 `[]int` 转换为 `[]interface{}` 进行传递,意味着一个分配并进行完整的拷贝。(b)延迟 `int` 与 `interface{}` 的转换,直到需要进行访问时在进行转换。这意味着现在每个 `slices` 访问都必须通过一个间接函数调用,以防万一有人传递给我们一个子类型。这两种选择都不符合 Go 的设计目标。 diff --git a/published/tech/20180604-Microservices-in-Go.md b/published/tech/20180604-Microservices-in-Go.md index 44dfbf4df..2ed1433a1 100644 --- a/published/tech/20180604-Microservices-in-Go.md +++ b/published/tech/20180604-Microservices-in-Go.md @@ -1,6 +1,6 @@ 首发于:https://studygolang.com/articles/22111 -# Go语言中的微服务 +# Go 语言中的微服务 ## 摘要 @@ -32,7 +32,7 @@ Go Micro 架构可以描述为三层堆栈。 -![图1.Go Micro架构](https://raw.githubusercontent.com/studygolang/gctt-images/master/microservices-in-go/goMicro.png) +![图 1.Go Micro 架构](https://raw.githubusercontent.com/studygolang/gctt-images/master/microservices-in-go/goMicro.png) 顶层包括 **Server-Client** 模型和服务抽象。该服务器是用于编写服务的基础。而客户端提供了一个接口,用于向服务端发起请求。 @@ -45,12 +45,12 @@ 它允许使用诸如 random,roundrobin,leastconn 等算法“选择”服务。 * Transport - 服务之间同步请求/响应通信的接口。 - Go Micro 还提供 Sidecar 等功能。这允许您使用Go以外的语言编写的服务。 - Sidecar 提供服务注册,gRPC 编码/解码和HTTP处理程序。它有多种语言版本。 + Go Micro 还提供 Sidecar 等功能。这允许您使用 Go 以外的语言编写的服务。 + Sidecar 提供服务注册,gRPC 编码/解码和 HTTP 处理程序。它有多种语言版本。 ### Go Kit -Go Kit 是一个用于在Go中构建微服务的编程工具包。与 Go Micro 不同,它是一个旨在导入二进制包的库。 +Go Kit 是一个用于在 Go 中构建微服务的编程工具包。与 Go Micro 不同,它是一个旨在导入二进制包的库。 Go Kit 遵循简单的规则,例如: @@ -62,15 +62,15 @@ Go Kit 遵循简单的规则,例如: 在 Go Kit 中,您可以找到以下包: -* 身份验证 - basic和JWT。 +* 身份验证 - basic 和 JWT。 * 传输 - HTTP,Nats,gRPC 等。 * 日志记录 - 服务中结构化日志记录的通用接口。 -* 软件度量 - CloudWatch,Statsd,Graphite等。 +* 软件度量 - CloudWatch,Statsd,Graphite 等。 * 追踪 - Zipkin 和 Opentracing。 -* 服务发现 - Consul,Etcd,Eureka等。 +* 服务发现 - Consul,Etcd,Eureka 等。 * 熔断器 - Hystrix 的 Go 语言实现。 -您可以在Peter Bourgon的文章和演示幻灯片中找到 Go Kit 的最佳描述之一: +您可以在 Peter Bourgon 的文章和演示幻灯片中找到 Go Kit 的最佳描述之一: * [Go kit: Go in the modern enterprise](https://peter.bourgon.org/go-kit/?source=post_page) * [Go + microservices](https://github.com/peterbourgon/go-microservices?source=post_page) @@ -78,7 +78,7 @@ Go Kit 遵循简单的规则,例如: 此外,在“Go + microservices”幻灯片中,您将找到使用 Go Kit 构建的服务架构的示例。 有关快速参考,请参阅服务架构图。 -![图2.使用Go Kit构建的服务架构示例 Go Micro 架构](https://raw.githubusercontent.com/studygolang/gctt-images/master/microservices-in-go/Go%2Bmicroservices.png) +![图 2.使用 Go Kit 构建的服务架构示例 Go Micro 架构](https://raw.githubusercontent.com/studygolang/gctt-images/master/microservices-in-go/Go%2Bmicroservices.png) ### Gizmo @@ -91,14 +91,14 @@ Gizmo 是纽约时报的微服务工具包。它提供了将服务器和 pubsub * [pubsub/pubsubtest](https://godoc.org/github.com/NYTimes/gizmo/pubsub/pubsubtest) - 包含发布者和订阅者接口的测试实现。 * [web](https://godoc.org/github.com/NYTimes/gizmo/web) - 公开用于从请求查询和有效负载中解析类型的函数。 -Pubsub包提供了使用以下队列的接口: +Pubsub 包提供了使用以下队列的接口: * [pubsub/aws](https://godoc.org/github.com/NYTimes/gizmo/pubsub/aws) - 适用于 Amazon SNS/SQS。 * [pubsub/gcp](https://godoc.org/github.com/NYTimes/gizmo/pubsub/gcp) - 适用于 Google Pubsub。 -* [pubsub/kafka](https://godoc.org/github.com/NYTimes/gizmo/pubsub/kafka) - 适用于 Kafka主题。 +* [pubsub/kafka](https://godoc.org/github.com/NYTimes/gizmo/pubsub/kafka) - 适用于 Kafka 主题。 * [pubsub/http](https://godoc.org/github.com/NYTimes/gizmo/pubsub/http) - 用于通过 HTTP 发布。 -因此,在我看来,Gizmo 介于 Go Micro 和 Go Kit 之间。它不像 Go Micro 那样完全的“黑盒”。与此同时,它并不像 Go Kit 那么粗糙。它提供更高级别的构建组件,例如config和pubsub包。 +因此,在我看来,Gizmo 介于 Go Micro 和 Go Kit 之间。它不像 Go Micro 那样完全的“黑盒”。与此同时,它并不像 Go Kit 那么粗糙。它提供更高级别的构建组件,例如 config 和 pubsub 包。 ### Kite @@ -116,9 +116,9 @@ Kite 是一个在 Go 中开发微服务的框架。它公开了 RPC 客户端和 * 用户和社区 * 代码质量。 -### GitHub统计 +### GitHub 统计 -![表1. Go 微服务框架统计(2018年4月收集)](https://raw.githubusercontent.com/studygolang/gctt-images/master/microservices-in-go/MicroStatics.png) +![表 1. Go 微服务框架统计(2018 年 4 月收集)](https://raw.githubusercontent.com/studygolang/gctt-images/master/microservices-in-go/MicroStatics.png) ### 文档和示例 @@ -135,7 +135,7 @@ Kite 是一个在 Go 中开发微服务的框架。它公开了 RPC 客户端和 ### 用户和社区 -Go Kit 是最受欢迎的微服务框架,基于 GitHub 统计数据 - 在本出版物发布时超过10k星。它拥有大量的贡献者(122)和超过1000个分叉。 +Go Kit 是最受欢迎的微服务框架,基于 GitHub 统计数据 - 在本出版物发布时超过 10k 星。它拥有大量的贡献者(122)和超过 1000 个分叉。 最后,Go Kit 由 [DigitalOcean](https://www.digitalocean.com/) 提供支持。 Go Micro 第二,拥有超过 3600 颗 stars ,27 个贡献者和 385 个 forks 。Six Micro 的最大赞助商之一是 [Sixt](https://www.sixt.com/)。 @@ -151,7 +151,7 @@ Gizmo 第三,超过 2200 颗 star, 31 个贡献者和 137 个 forks 。由纽 好吧,已有足够的理论。下边,为了更好地理解框架,我创建了三个简单的微服务。 -![图3.实际示例架构](https://raw.githubusercontent.com/studygolang/gctt-images/master/microservices-in-go/micro_practice.png) +![图 3.实际示例架构](https://raw.githubusercontent.com/studygolang/gctt-images/master/microservices-in-go/micro_practice.png) 这些是实现一个业务功能的服务——"Greeting"。 当用户将 "name" 参数传递给服务器时,该服务会发送 Greeting 响应。此外,所有服务均符合以下要求: @@ -189,7 +189,7 @@ message GreetingResponse { 接口包含一种方法—— "Greeting"。 请求中有一个参数—— 'name',响应中有一个参数 - 'greeting'。 -然后我使用修改后的 [protoc工具](https://github.com/micro/protoc-gen-micro) 通过 protobuf 文件生成服务接口。 +然后我使用修改后的 [protoc 工具](https://github.com/micro/protoc-gen-micro) 通过 protobuf 文件生成服务接口。 该生成器由 Go Micro fork 并进行了修改,以支持该框架的一些功能。 我在 “greeting” 服务中将这些连接在一起。此时,该服务正在启动并注册服务发现系统。 它只支持 gRPC 传输协议: @@ -230,7 +230,7 @@ func main() { } ``` -为了支持HTTP传输,我不得不添加其他模块。它将HTTP请求映射到 protobuf 定义的请求。并称为 gRPC 服务。 +为了支持 HTTP 传输,我不得不添加其他模块。它将 HTTP 请求映射到 protobuf 定义的请求。并称为 gRPC 服务。 然后,它将服务响应映射到 HTTP 响应并将其回复给用户。 ```go @@ -244,7 +244,7 @@ import ( proto "github.com/antklim/go-microservices/go-micro-greeter/pb" "github.com/micro/go-micro/client" - web "github.com/micro/go-web" + Web "github.com/micro/go-web" ) func main() { @@ -506,7 +506,7 @@ type errorWrapper struct { } // EncodeHTTPGenericResponse is a transport/http. -// EncodeResponseFunc 返回 json 响应。 +// EncodeResponseFunc 返回 JSON 响应。 func EncodeHTTPGenericResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error { if f, ok := response.(greeterendpoint.Failer); ok && f.Failed() != nil { encodeError(ctx, f.Failed(), w) @@ -580,7 +580,7 @@ func decodeGRPCGreetingRequest(_ context.Context, grpcReq interface{}) (interfac } // encodeGRPCGreetingResponse is a transport/grpc. -// DecodeRequestFunc 将 用户域的 greeting 转换为请求gRPC 请求。 +// DecodeRequestFunc 将 用户域的 greeting 转换为请求 gRPC 请求。 func encodeGRPCGreetingResponse(_ context.Context, response interface{}) (interface{}, error) { res := response.(greeterendpoint.GreetingResponse) return &pb.GreetingResponse{Greeting: res.Greeting}, nil @@ -719,7 +719,7 @@ func main() { var g group.Group { - // 调试功能带 http.DefaultServeMux, 并提供Go调试和分析路由等功能 + // 调试功能带 http.DefaultServeMux, 并提供 Go 调试和分析路由等功能 debugListener, err := net.Listen("tcp", *debugAddr) if err != nil { logger.Log("transport", "debug/HTTP", "during", "Listen", "err", err) @@ -809,7 +809,7 @@ import ( // ServiceMiddleware 定义了 service 中间件. type ServiceMiddleware func(Service) Service -// LoggingMiddleware 使用 logger 作为依赖,返回一个 Service中间件 +// LoggingMiddleware 使用 logger 作为依赖,返回一个 Service 中间件 func LoggingMiddleware(logger log.Logger) ServiceMiddleware { return func(next Service) Service { return loggingMiddleware{next, logger} @@ -876,9 +876,9 @@ func LoggingMiddleware(logger log.Logger) endpoint.Middleware { ### Gizmo greeter -我以与 Go Kit 类似的方式创建了Gizmo服务。我为服务,端点,传输和服务发现注册商定义了四个包。 +我以与 Go Kit 类似的方式创建了 Gizmo 服务。我为服务,端点,传输和服务发现注册商定义了四个包。 -服务实现和服务发现系统注册器与 Go Kit 服务共享相同的代码。但是端点定义和传输实现必须根据Gizmo功能完成。 +服务实现和服务发现系统注册器与 Go Kit 服务共享相同的代码。但是端点定义和传输实现必须根据 Gizmo 功能完成。 Gizmo Greeting 端点 @@ -957,7 +957,7 @@ type errorResponse struct { 如您所见,代码段与 Go Kit 类似。主要区别在于应该返回的接口类型: -GizmoGreeting HTTP终端 +GizmoGreeting HTTP 终端 ```go package greetertransport @@ -978,12 +978,12 @@ import ( ) type ( - // TService 会实现 server.RPCService (服务的RPC),以及处理服务端请求 + // TService 会实现 server.RPCService (服务的 RPC),以及处理服务端请求 TService struct { Endpoints greeterendpoint.Endpoints } - // Config 包含 server 相关 json 配置 + // Config 包含 server 相关 JSON 配置 Config struct { Server *server.Config } @@ -1040,7 +1040,7 @@ func (s *TService) ContextEndpoints() map[string]map[string]server.ContextHandle return map[string]map[string]server.ContextHandlerFunc{} } -// JSONEndpoints 是TService中可用的所有端点的列表。 +// JSONEndpoints 是 TService 中可用的所有端点的列表。 func (s *TService) JSONEndpoints() map[string]map[string]server.JSONContextEndpoint { return map[string]map[string]server.JSONContextEndpoint{ "/health": map[string]server.JSONContextEndpoint{ @@ -1070,7 +1070,7 @@ func (s *TService) Greeting(ctx ocontext.Context, r *pb.GreetingRequest) (*pb.Gr ``` Go Kit 和 Gizmo 之间的显着差异在于传输实现。 Gizmo 提供了几种可以使用的服务类型。 -我所要做的就是将HTTP路径映射到端点定义。低级HTTP请求/响应处理由 Gizmo 处理。 +我所要做的就是将 HTTP 路径映射到端点定义。低级 HTTP 请求/响应处理由 Gizmo 处理。 ## 结论 @@ -1082,7 +1082,7 @@ Go Kit 和 Gizmo 之间的显着差异在于传输实现。 Gizmo 提供了几 Gizmo 位于 Go Micro 和 Go Kit 之间。它提供了一些更高级别的抽象,例如 Service 包。 但缺乏文档和示例意味着我必须阅读源代码以了解不同的服务类型是如何工作的。使用 Gizmo 比使用 Go Kit 更容易。但它并不像 Go Micro 那么顺利。 -这就是今天的一切。谢谢阅读。请查看微服务代码库以获取更多信息。如果您对Go和微服务框架有任何经验,请在下面的评论中分享。 +这就是今天的一切。谢谢阅读。请查看微服务代码库以获取更多信息。如果您对 Go 和微服务框架有任何经验,请在下面的评论中分享。 -- diff --git a/published/tech/20180606-Go-Memory-Management.md b/published/tech/20180606-Go-Memory-Management.md index e77eb8629..6b22c7280 100644 --- a/published/tech/20180606-Go-Memory-Management.md +++ b/published/tech/20180606-Go-Memory-Management.md @@ -48,21 +48,21 @@ povilasv 16609 0.0 0.0 388496 5236 pts/9 Sl+ 17:21 0:00 ./main 随机访问存储器(RAM /ræm/)是一种计算机存储设备,用于存储当前被使用的数据和机器码。 随机访问内存设备允许在几乎相同的时间内读取或写入数据项,而不管数据在内存中的物理位置如何。 -我们可以将物理内存看作是一个槽/单元的数组,其中槽可以容纳 8 个位信息1。每个内存槽都有一个地址,在你的程序中你会告诉 CPU:“喂,CPU,你能在地址 0 处的内存中取出那个字节的信息吗?”,或者“喂,CPU,你能把这个字节的信息放在内存为地址 1 的地方吗?”。 +我们可以将物理内存看作是一个槽/单元的数组,其中槽可以容纳 8 个位信息 1。每个内存槽都有一个地址,在你的程序中你会告诉 CPU:“喂,CPU,你能在地址 0 处的内存中取出那个字节的信息吗?”,或者“喂,CPU,你能把这个字节的信息放在内存为地址 1 的地方吗?”。 ![物理内存](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-memory-management/Go_Memory_Management_1.png) 由于计算机通常要运行多个任务,所以直接从物理内存中读写是并不明智。想象一下,编写一个程序是一个很容易的事情,它会从内存中读取所有的东西(包括你的密码),或者编写一个程序,它会在不同的程序的内存地址中写入内容。那将是很荒唐的事情。 -因此,除了使用实际物理内存去处理任务我们还有*虚拟内存*的概念。当你的程序运行时,它只看到它的内存,它认为它独占了内存2。另外,程序中存储的内存字节也不可能都放在 RAM 中。如果不经常访问特定的内存块,操作系统可能会将一些内存块放入较慢的存储空间(比如磁盘),从而节省宝贵的 RAM。操作系统甚至不会承认对你的程序是这样操作的,但实际上,我们知道操作系统确实是那样运作的。 +因此,除了使用实际物理内存去处理任务我们还有*虚拟内存*的概念。当你的程序运行时,它只看到它的内存,它认为它独占了内存 2。另外,程序中存储的内存字节也不可能都放在 RAM 中。如果不经常访问特定的内存块,操作系统可能会将一些内存块放入较慢的存储空间(比如磁盘),从而节省宝贵的 RAM。操作系统甚至不会承认对你的程序是这样操作的,但实际上,我们知道操作系统确实是那样运作的。 -![虚拟内存->物理内存](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-memory-management/Go_Memory_Management_2.png) +![虚拟内存-> 物理内存](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-memory-management/Go_Memory_Management_2.png) -虚拟内存可以使用基于CPU体系结构和操作系统的段或页表来实现。我不会详细讲段,因为页表更常见,但你可以在附录3中读到更多关于段的内容。 +虚拟内存可以使用基于 CPU 体系结构和操作系统的段或页表来实现。我不会详细讲段,因为页表更常见,但你可以在附录 3 中读到更多关于段的内容。 在*分页虚拟内存*中,我们将虚拟内存划分为块,称为*页*。页的大小可以根据硬件的不同而有所不同,但是页的大小通常是 4-64 KB,此外,通常还能够使用从 2MB 到 1GB 的巨大的页。分块很有用,因为单独管理每个内存槽需要更多的内存,而且会降低计算机的性能。 -为了实现分页虚拟内存,计算机通常有一个称为*内存管理单元(MMU)*4的芯片,它位于 CPU 和内存之间。MMU 在一个名为*页表*的表(它存储在内存中)中保存了从虚拟地址到物理地址的映射,其中每页包含一个*页表项(PTE)*。MMU 还有一个物理缓存*旁路转换缓冲(TLB)*,用来存储最近从虚拟内存到物理内存的转换。大致是这样的: +为了实现分页虚拟内存,计算机通常有一个称为*内存管理单元(MMU)*4 的芯片,它位于 CPU 和内存之间。MMU 在一个名为*页表*的表(它存储在内存中)中保存了从虚拟地址到物理地址的映射,其中每页包含一个*页表项(PTE)*。MMU 还有一个物理缓存*旁路转换缓冲(TLB)*,用来存储最近从虚拟内存到物理内存的转换。大致是这样的: ![虚拟内存到物理内存转换](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-memory-management/Go_Memory_Management_3.png) @@ -71,14 +71,14 @@ povilasv 16609 0.0 0.0 388496 5236 pts/9 Sl+ 17:21 0:00 ./main 1. CPU 发出访问虚拟地址的命令,MMU 在其页面表中检查它并禁止访问,因为没有为该虚拟页面分配物理 RAM。 2. 然后 MMU 向 CPU 发送页错误。 3. 然后,操作系统通过查找 RAM 的备用内存块(称为帧)并设置新的 PTE 来映射它来处理页错误。 -4. 如果没有RAM是空闲的,它可以使用一些替换算法选择现有页面,并将其保存到磁盘(此过程称为分页)。 +4. 如果没有 RAM 是空闲的,它可以使用一些替换算法选择现有页面,并将其保存到磁盘(此过程称为分页)。 5. 对于一些内存管理单元,还可能出现页表入口不足的情况,在这种情况下,操作系统必须为新的映射释放一个表入口。 操作系统通常管理多个应用程序(进程),因此整个内存管理位如下所示: ![内存管理位](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-memory-management/Go_Memory_Management_4.png) -每个进程都有一个线性虚拟地址空间,地址从 0 到最大值。虚拟地址空间不需要是连续的,因此并非所有这些虚拟地址实际上都用于存储数据,并且它们不占用RAM或磁盘中的空间。很酷的一点是,真实内存的同一帧可以支持属于多个进程的多个虚拟页面。通常就是这种情况,虚拟内存占用 GNU C 库代码(libc),如果使用 `go build` 进行编译,则默认包含该代码。你可以通过添加 ldflags 参数来设置编译时不带 libc 的代码5: +每个进程都有一个线性虚拟地址空间,地址从 0 到最大值。虚拟地址空间不需要是连续的,因此并非所有这些虚拟地址实际上都用于存储数据,并且它们不占用 RAM 或磁盘中的空间。很酷的一点是,真实内存的同一帧可以支持属于多个进程的多个虚拟页面。通常就是这种情况,虚拟内存占用 GNU C 库代码(libc),如果使用 `go build` 进行编译,则默认包含该代码。你可以通过添加 ldflags 参数来设置编译时不带 libc 的代码 5: ```bash go build -ldflags '-libgcc=none' @@ -88,9 +88,9 @@ go build -ldflags '-libgcc=none' ## 操作系统相关 -为了运行程序,操作系统有一个模块,它负责加载程序和所需要的库,称为程序加载器。在 linux 系统中,你可以通过 `execve()` 系统调用来调用你的程序加载器。 +为了运行程序,操作系统有一个模块,它负责加载程序和所需要的库,称为程序加载器。在 Linux 系统中,你可以通过 `execve()` 系统调用来调用你的程序加载器。 -当加载程序运行时,它会进行一下步骤6: +当加载程序运行时,它会进行一下步骤 6: 1. 验证程序映像(权限、内存需求等); 2. 将程序映像从磁盘复制到主存储器中; @@ -101,9 +101,9 @@ go build -ldflags '-libgcc=none' ### 那么什么是程序呢? -我们通常用 Go 语言等高级语言编写程序,这些语言被编译成可执行的机器代码文件或不可执行的机器代码目标文件(库)。 这些可执行或不可执行的目标文件通常采用容器格式,例如[可执行文件和可链接格式](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format)(ELF)(通常在 Linux 中),[可执行文件](https://en.wikipedia.org/wiki/Portable_Executable)(通常在Windows 中)。但有时候,你并不能用你喜欢的 Go 语言来编写所有程序。在这种情况下,一种选择是手工制作你自己的 ELF 二进制文件并将机器代码放入正确的 ELF 结构中。另一种选择是用汇编语言开发一个程序,该程序在与机器代码指令更紧密地联系,同时仍然是便于人们阅读的。 +我们通常用 Go 语言等高级语言编写程序,这些语言被编译成可执行的机器代码文件或不可执行的机器代码目标文件(库)。 这些可执行或不可执行的目标文件通常采用容器格式,例如[可执行文件和可链接格式](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format)(ELF)(通常在 Linux 中),[可执行文件](https://en.wikipedia.org/wiki/Portable_Executable)(通常在 Windows 中)。但有时候,你并不能用你喜欢的 Go 语言来编写所有程序。在这种情况下,一种选择是手工制作你自己的 ELF 二进制文件并将机器代码放入正确的 ELF 结构中。另一种选择是用汇编语言开发一个程序,该程序在与机器代码指令更紧密地联系,同时仍然是便于人们阅读的。 -目标文件是直接在处理器上执行的程序的二进制表示。这些目标文件不仅包含机器代码,还包含有关应用程序的元数据,如操作系统体系结构,调试信息。目标文件还携带应用程序数据,如全局变量或常量。 通常,目标文件由以下段(section)组成,如:*.text(可执行代码)*,*.data(全局变量)* 和 *.rodata(全局常量)* 等7。 +目标文件是直接在处理器上执行的程序的二进制表示。这些目标文件不仅包含机器代码,还包含有关应用程序的元数据,如操作系统体系结构,调试信息。目标文件还携带应用程序数据,如全局变量或常量。 通常,目标文件由以下段(section)组成,如:*.text(可执行代码)*,*.data(全局变量)* 和 *.rodata(全局常量)* 等 7。 我在 linux(Ubuntu) 系统上把程序编译成可执行和可链接形式的文件(也就是执行 `go build` 命令后的输出文件)8。在 Go 语言中,我们可以轻松编写一个读取 ELF 可执行文件的程序,因为 Go 语言在标准库中有一个 `debug/elf` 包。以下是一个例子: @@ -170,7 +170,7 @@ func main() { 2018/05/06 14:26:08 &{{.strtab SHT_STRTAB 0x0 0 9944384 446735 0 0 1 0 446735} 0xc420080a80 0xc420080a80 0 0} ``` -你也可以通过一些 linux 工具来查看 ELF 文件信息,如: `size --format=sysv main` 或 `readelf -l main`(这里的 `main` 是指输出的二进制文件)。 +你也可以通过一些 Linux 工具来查看 ELF 文件信息,如: `size --format=sysv main` 或 `readelf -l main`(这里的 `main` 是指输出的二进制文件)。 显而易见,可执行文件只是具有某种预定义格式的文件。通常,可执行格式具有段,这些段是在运行映像之前映射的数据内存。下面是 segment 的一个常见视图,流程如下: @@ -186,7 +186,7 @@ func main() { 我们来看看进程如何分配内存。 -Libc 手册解释是9,程序可以使用 `exec` 系列函数和编程方式以两种主要方式进行分配。`exec` 调用程序加载器来启动程序,从而为进程创建虚拟地址空间,将程序加载进内存并运行它。常用的编程方式有: +Libc 手册解释是 9,程序可以使用 `exec` 系列函数和编程方式以两种主要方式进行分配。`exec` 调用程序加载器来启动程序,从而为进程创建虚拟地址空间,将程序加载进内存并运行它。常用的编程方式有: * *静态分配*是在声明全局变量时发生的事情。每个全局变量定义一个固定大小的空间块。当你的程序启动时(exec 操作的一部分),该空间被分配一次,并且永远不会被释放。 * *自动分配* - 声明自动变量(例如函数参数或局部变量)时会发生自动分配。输入包含声明的复合语句时会分配自动变量的空间,并在退出该复合语句时释放。 @@ -246,9 +246,9 @@ int main (){ ### TCMalloc -TCMalloc 性能背后的秘密在于它使用线程本地缓存来存储一些预先分配的内存“对象”,以便从线程本地缓存11中满足小分配。一旦线程本地缓存耗尽空间,内存对象就会从中心数据结构移动到线程本地缓存。 +TCMalloc 性能背后的秘密在于它使用线程本地缓存来存储一些预先分配的内存“对象”,以便从线程本地缓存 11 中满足小分配。一旦线程本地缓存耗尽空间,内存对象就会从中心数据结构移动到线程本地缓存。 -![中心数据结构->线程本地缓存](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-memory-management/Go_Memory_Management_6.png) +![中心数据结构-> 线程本地缓存](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-memory-management/Go_Memory_Management_6.png) TCMalloc 对小对象(大小 <= 32K)分配的处理与大对象不同。使用页级分配器直接从中心堆分配大型对象。同时,小对象被映射到大约 *170* 个可分配大小类中的一个。 @@ -279,17 +279,17 @@ TCMalloc 对小对象(大小 <= 32K)分配的处理与大对象不同。使用 ![中心页面堆](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-memory-management/Go_Memory_Management_8.png) -对于 i < 256,第 k 个项是由 k 个页组成的运行的空闲列表。第256项是长度 > = 256 页的运行的空闲列表。 +对于 i < 256,第 k 个项是由 k 个页组成的运行的空闲列表。第 256 项是长度 > = 256 页的运行的空闲列表。 以下描述了它如何适用于大型对象的: *满足 k 页的分配:* -1. 我们查看k-th列表。 +1. 我们查看 k-th 列表。 2. 如果这个空闲列表是空的,我们会查看下一个空闲列表,等等。 3. 所示。最后,如果有必要,我们会查看最后一个免费列表。 4. 所示。如果失败,我们将从系统中获取内存。 -5. 如果对k个页面的分配通过运行长度为> k的页面来满足,则运行的其余部分将重新插入到页面堆中的适当空闲列表中。 +5. 如果对 k 个页面的分配通过运行长度为 > k 的页面来满足,则运行的其余部分将重新插入到页面堆中的适当空闲列表中。 内存是根据连续页面的运行来管理的,这些页面称为 *Spans*(这很重要,因为 Go 语言耶稣根据 Spans 来管理内存的)。 @@ -304,15 +304,15 @@ TCMalloc 对小对象(大小 <= 32K)分配的处理与大对象不同。使用 ## Go 语言的内存分配器 -Go 语言的内存分配器与 TCMalloc 类似,它在页运行(spans/mspan 对象)中工作,使用线程局部缓存并根据大小划分分配。 跨度是 8K 或更大的连续内存区域。在 runtime/mheap.go 中你可以看到有一个名为 mspn 的结构体。Spans有3种类型: +Go 语言的内存分配器与 TCMalloc 类似,它在页运行(spans/mspan 对象)中工作,使用线程局部缓存并根据大小划分分配。 跨度是 8K 或更大的连续内存区域。在 runtime/mheap.go 中你可以看到有一个名为 mspn 的结构体。Spans 有 3 种类型: 1. *空闲* - span,没有对象,可以释放回操作系统,或重用于堆分配,或重用于堆栈内存。 2. *正在使用* - span,至少有一个堆对象,可能有更多的空间。 -3. *栈* - span,用于 goroutine 堆栈。此跨度可以存在于堆栈中或堆中,但不能同时存在。 +3. *栈* - span,用于 Goroutine 堆栈。此跨度可以存在于堆栈中或堆中,但不能同时存在。 当分配发生时,我们将对象映射到 3 个大小的类:对于小于 16 字节的对象的极小类,对于达到 32 kB 的对象的小类,以及对于其他对象的大类。小的分配大小被四舍五入到大约 *70* 个大小的类中的一个,每个类都有它自己的恰好大小的自由对象集。我在 runtime/malloc.go 中发现了一些有趣的注释:小分配器的主要目标是小字符串和独立转义变量。 -> 在json基准测试中,分配器将分配数量减少了大约12%,并将堆大小减少了大约20%。微型分配器将几个微小的分配请求组合成一个16字节的单个内存块。当所有子对象都无法访问时,将释放生成的内存块。子对象不能有指针。 +> 在 json 基准测试中,分配器将分配数量减少了大约 12%,并将堆大小减少了大约 20%。微型分配器将几个微小的分配请求组合成一个 16 字节的单个内存块。当所有子对象都无法访问时,将释放生成的内存块。子对象不能有指针。 下面描述极小对象是如何工作的: @@ -402,7 +402,7 @@ func main() { fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path)) }) - go func() { + Go func() { for { var m runtime.MemStats runtime.ReadMemStats(&m) @@ -556,7 +556,7 @@ ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 [vsyscall] ```go func main() { - go func() { + Go func() { for { var m runtime.MemStats runtime.ReadMemStats(&m) diff --git a/published/tech/20180608-Gracefully-Restarting-a-Go-Program-Without-Downtime.md b/published/tech/20180608-Gracefully-Restarting-a-Go-Program-Without-Downtime.md index 7130ba6d2..81ebf9c79 100644 --- a/published/tech/20180608-Gracefully-Restarting-a-Go-Program-Without-Downtime.md +++ b/published/tech/20180608-Gracefully-Restarting-a-Go-Program-Without-Downtime.md @@ -17,7 +17,7 @@ Marek Majkowski 在他的博客文章[《为什么一个 NGINX 工作线程会 在我们初期的讨论中,我们了解到几个关于 `SO_REUSEPORT` 的问题。我们的一个工程师之前使用这个方法,并且注意到由于其多个接受队列,有时候会丢弃挂起的 TCP 连接。除此之外,当我们进行这些讨论的时候,Go 并没有很好地支持在一个 `net.Listener` 上设置 `SO_REUSEPORT`。然而,在过去的几天中,在这个问题上有了进展,看起来像 [Go 不久就会支持设置套接字属性](https://github.com/golang/go/issues/9661)。 -第二种方法也很吸引人,因为它的简单性以及大多数开发人员熟悉的传统Unix 的 fork/exec 产生模型,即将所有打开文件传递给子进程的约定。需要注意的一点,`os/exec` 包实际上不赞同这种用法。主要是出于安全上的考量,它只传递 `stdin` , `stdout` 和 `stderr` 给子进程。然而, os 包确实提供较低级的原语,可用于将文件传递给子程序,这就是我们想做的。 +第二种方法也很吸引人,因为它的简单性以及大多数开发人员熟悉的传统 Unix 的 fork/exec 产生模型,即将所有打开文件传递给子进程的约定。需要注意的一点,`os/exec` 包实际上不赞同这种用法。主要是出于安全上的考量,它只传递 `stdin` , `stdout` 和 `stderr` 给子进程。然而, os 包确实提供较低级的原语,可用于将文件传递给子程序,这就是我们想做的。 ## 使用信号切换套接字进程所有者 @@ -40,7 +40,7 @@ Marek Majkowski 在他的博客文章[《为什么一个 NGINX 工作线程会 首先,编译和启动程序。 ``` -$ go build restart.go +$ Go build restart.go $ ./restart & [1] 95147 $ Created listener file descriptor for :8080. @@ -68,7 +68,7 @@ Hello from 95147! ``` $ kill -SIGTERM 95147 signal: killed -[1]+ Exit 1 go run restart.go +[1]+ Exit 1 Go run restart.go $ curl http://localhost:8080/hello Hello from 95170! $ curl http://localhost:8080/hello @@ -294,7 +294,7 @@ func main() { var addr string flag.StringVar(&addr, "addr", ":8080", "Address to listen on.") - // Create (or import) a net.Listener and start a goroutine that runs + // Create (or import) a net.Listener and start a Goroutine that runs // a HTTP server on that net.Listener. ln, err := createOrImportListener(addr) if err != nil { diff --git a/published/tech/20180608-Some-notes-about-the-upcoming-WebAssembly-support-in-Go.md b/published/tech/20180608-Some-notes-about-the-upcoming-WebAssembly-support-in-Go.md index d259b78a2..061e8d386 100644 --- a/published/tech/20180608-Some-notes-about-the-upcoming-WebAssembly-support-in-Go.md +++ b/published/tech/20180608-Some-notes-about-the-upcoming-WebAssembly-support-in-Go.md @@ -4,21 +4,21 @@ 这是一篇关于 webassembly 的即时记录,它的目的是给我做个备忘而不仅仅是如果使用它的教程。 -即将发布的 Go 1.11 版本将支持 Wasm。@neelance 做了大部分的实施工作。对 wasm 的支持已经可以通过他在 github 上的工作分支进行测试。 +即将发布的 Go 1.11 版本将支持 Wasm。@neelance 做了大部分的实施工作。对 wasm 的支持已经可以通过他在 GitHub 上的工作分支进行测试。 看[这篇文章](https://blog.gopheracademy.com/advent-2017/go-wasm/)了解更多信息 ## 工具链设置 -要从 go 源码生产一个 wasm 文件,您需要从源码获取并为 go 工具集打补丁: +要从 Go 源码生产一个 wasm 文件,您需要从源码获取并为 Go 工具集打补丁: ``` ~ mkdir ~/gowasm -~ git clone https://go.googlesource.com/go ~/gowasm +~ Git clone https://go.googlesource.com/go ~/gowasm ~ cd ~/gowasm -~ git remote add neelance https://github.com/neelance/go -~ git fetch --all -~ git checkout wasm-wip +~ Git remote add neelance https://github.com/neelance/go +~ Git fetch --all +~ Git checkout wasm-wip ~ cd src ~ ./make.bash ``` @@ -77,7 +77,7 @@ WebAssembly.instantiateStreaming(fetch("example.wasm"), go.importObject).then((r // ... ``` -理论上,任何 web 服务都可以运行它,但是当我们试着用 caddy 运行它时遇到一个问题。这个 javascript 加载器需要服务发送这个 wasm 文件的正确 mime 类型给它。 +理论上,任何 Web 服务都可以运行它,但是当我们试着用 caddy 运行它时遇到一个问题。这个 JavaScript 加载器需要服务发送这个 wasm 文件的正确 mime 类型给它。 这有一个快速的破解方法来运行我们的测试:为我们的 wasm 文件写个带有特殊处理的 Go 服务。 @@ -103,7 +103,7 @@ func main() { *注意* 设置一个特殊的路由器来处理所有的 wasm 文件没什么大不了,如我所说,这是一个 POC,这篇文章只是关于它的附注。 -然后使用`go run server.go`来启动服务,并打开浏览器访问 http://localhost:3000。 +然后使用 `go run server.go` 来启动服务,并打开浏览器访问 http://localhost:3000。 打开控制台看看! @@ -113,7 +113,7 @@ func main() { ### 解决 DOM 问题 -*syscall/js* 包中包含允许通过 javascript API 与 DOM 交互的函数。要获取此包的文档,只需运行: +*syscall/js* 包中包含允许通过 JavaScript API 与 DOM 交互的函数。要获取此包的文档,只需运行: `GOROOT=~/gowasm godoc -http=:6060` 然后用浏览器访问 http://localhost:6060/pkg/syscall/js/ 。 diff --git a/published/tech/20180613-Go-append-is-not-always-thread-safe.md b/published/tech/20180613-Go-append-is-not-always-thread-safe.md index c1f6ddf47..e56010308 100644 --- a/published/tech/20180613-Go-append-is-not-always-thread-safe.md +++ b/published/tech/20180613-Go-append-is-not-always-thread-safe.md @@ -65,13 +65,13 @@ func TestAppend(t *testing.T) { 如果我们执行这个测试时带上 `-race` flag ,我们可以注意到一个竞争条件。 ``` -< go test -race . +< Go test -race . ================== WARNING: DATA RACE -Write at 0x00c4200be060 by goroutine 8: +Write at 0x00c4200be060 by Goroutine 8: _/tmp.TestAppend.func2() /tmp/main_test.go:20 +0xcb -Previous write at 0x00c4200be060 by goroutine 7: +Previous write at 0x00c4200be060 by Goroutine 7: _/tmp.TestAppend.func1() /tmp/main_test.go:15 +0xcb Goroutine 8 (running) created at: @@ -87,10 +87,10 @@ testing.tRunner() ================== ================== WARNING: DATA RACE -Write at 0x00c4200be070 by goroutine 8: +Write at 0x00c4200be070 by Goroutine 8: _/tmp.TestAppend.func2() /tmp/main_test.go:20 +0x11a -Previous write at 0x00c4200be070 by goroutine 7: +Previous write at 0x00c4200be070 by Goroutine 7: _/tmp.TestAppend.func1() /tmp/main_test.go:15 +0x11a Goroutine 8 (running) created at: @@ -147,7 +147,7 @@ x 有更多的容量 1. `x=append(x, ...)` 看起来你要获得一个新的 slice。 2. 大多数返回值的函数都不会改变它们的输入。 3. 我们使用 `append` 通常都是得到一个新的 slice。 -4. 错误地认为append是只读的。 +4. 错误地认为 append 是只读的。 ## 认知这个 bug diff --git a/published/tech/20180617-Using-context-cancellation-in-Go.md b/published/tech/20180617-Using-context-cancellation-in-Go.md index e210c0ea6..9e8937c9c 100644 --- a/published/tech/20180617-Using-context-cancellation-in-Go.md +++ b/published/tech/20180617-Using-context-cancellation-in-Go.md @@ -2,13 +2,13 @@ # 在 Go 中用 Context 取消操作 -许多使用 Go 的人都会遇到 context 包。大多数时候 context 用在下游操作, 比如发送 Http 请求、查询数据库、或者开 go-routines 执行异步操作。最普通用法是通过它向下游操作传递数据。很少人知道,但是非常有用的context功能是在执行中取消或者停止操作。 +许多使用 Go 的人都会遇到 context 包。大多数时候 context 用在下游操作, 比如发送 Http 请求、查询数据库、或者开 go-routines 执行异步操作。最普通用法是通过它向下游操作传递数据。很少人知道,但是非常有用的 context 功能是在执行中取消或者停止操作。 这篇文章会解释我们如何使用 Context 的取消功能,还有通过一些 Context 使用方法和最佳实践让你的应用更加快速和健壮。 ## 我们为什么需要取消操作? -简单来说,我们需要取消来避免系统做无用的操作。想像一下,一般的http应用,用户请求 Http Server, 然后 Http Server查询数据库并返回数据给客户端: +简单来说,我们需要取消来避免系统做无用的操作。想像一下,一般的 http 应用,用户请求 Http Server, 然后 Http Server 查询数据库并返回数据给客户端: ![http 应用](https://raw.githubusercontent.com/studygolang/gctt-images/master/using-context-cancellation-in-go/1.png) @@ -24,7 +24,7 @@ ![理想耗时图](https://raw.githubusercontent.com/studygolang/gctt-images/master/using-context-cancellation-in-go/4.png) -## go context包的取消操作 +## Go context 包的取消操作 现在我们知道为什么需要取消操作了,让我们看看在 Golang 里如何实现。因为"取消操作"高度依赖上下文,或者已执行的操作,所以它非常容易通过 context 包来实现。 @@ -35,7 +35,7 @@ ## 监听取消事件 _context_ 包提供了 _Done()_ 方法, 它返回一个当 Context 收取到 _取消_ 事件时会接收到一个 _struct{}_ 类型的 _channel_。 -监听取消事件只需要简单的等待 _<- ctx.Done()_ 就好了例如: 一个 Http Server 会花2秒去处理事务,如果请求提前取消,我们想立马返回结果: +监听取消事件只需要简单的等待 _<- ctx.Done()_ 就好了例如: 一个 Http Server 会花 2 秒去处理事务,如果请求提前取消,我们想立马返回结果: ```go func main() { @@ -63,11 +63,11 @@ func main() { > 源代码地址: https://github.com/sohamkamani/blog-example-go-context-cancellation -你可以通过执行这段代码, 用浏览器打开 [localhost:8000](http://localhost:8000)。如果你在2秒内关闭浏览器,你会看到在控制台打印了 "request canceled"。 +你可以通过执行这段代码, 用浏览器打开 [localhost:8000](http://localhost:8000)。如果你在 2 秒内关闭浏览器,你会看到在控制台打印了 "request canceled"。 ## 触发取消事件 -如果你有一个可以取消的操作,你可以通过context触发一个 _取消事件_ 。 这个你可以用 context 包 提供的 _WithCancel_ 方法, 它返回一个 context 对象,和一个没有参数的方法。这个方法不会返回任何东西,仅在你想取消这个context的时候去调用。 +如果你有一个可以取消的操作,你可以通过 context 触发一个 _取消事件_ 。 这个你可以用 context 包 提供的 _WithCancel_ 方法, 它返回一个 context 对象,和一个没有参数的方法。这个方法不会返回任何东西,仅在你想取消这个 context 的时候去调用。 第二种情况是依赖。 依赖的意思是,当一个操作失败,会导致其他操作失败。 例如:我们提前知道了一个操作失败,我们会取消所有依赖操作。 @@ -97,7 +97,7 @@ func main() { // from the original context ctx, cancel := context.WithCancel(ctx) - // Run two operations: one in a different go routine + // Run two operations: one in a different Go routine go func() { err := operation1(ctx) // If this operation returns an error @@ -165,7 +165,7 @@ Response received, status code: 200 Request failed: Get http://google.com: context deadline exceeded ``` -你可以通过设置超时来获得以上2种结果。 +你可以通过设置超时来获得以上 2 种结果。 ## 陷阱和注意事项 diff --git a/published/tech/20180618-Experiments-with-image-manipulation-in-WASM-using-Go.md b/published/tech/20180618-Experiments-with-image-manipulation-in-WASM-using-Go.md index 6918ee65a..f4b2a82ae 100644 --- a/published/tech/20180618-Experiments-with-image-manipulation-in-WASM-using-Go.md +++ b/published/tech/20180618-Experiments-with-image-manipulation-in-WASM-using-Go.md @@ -4,7 +4,7 @@ Go 的主分支最近完成了一个 WebAssembly 的工作原型实现。作为 WASM 的爱好者,我自然要把玩一下。 -这篇文章,我要记下周末我用 Go 做的处理图像实验的想法。这个演示只是从浏览器中获取图像输入,然后应用各种图像变换,如亮度,对比度,色调,饱和度等,最后将其转储回浏览器。这测试了两件事 - 简单的CPU绑定执行,这是图像转换应该做的事情,以及在 JS 和 Go 之间传递数据。 +这篇文章,我要记下周末我用 Go 做的处理图像实验的想法。这个演示只是从浏览器中获取图像输入,然后应用各种图像变换,如亮度,对比度,色调,饱和度等,最后将其转储回浏览器。这测试了两件事 - 简单的 CPU 绑定执行,这是图像转换应该做的事情,以及在 JS 和 Go 之间传递数据。 ## 回调 @@ -59,21 +59,21 @@ func (s *Shimmer) setupHueCb() { ## 执行 -我吐槽的是图像数据从 Go 传给浏览器的方式。在图像上传时,我把 src 属性设置为整个图像的base64编码格式,该值传到 Go 代码中对其解码为二进制,应用转换然后再编回 base64 并设置目标图像的 src 属性。 +我吐槽的是图像数据从 Go 传给浏览器的方式。在图像上传时,我把 src 属性设置为整个图像的 base64 编码格式,该值传到 Go 代码中对其解码为二进制,应用转换然后再编回 base64 并设置目标图像的 src 属性。 这使得 DOM 非常沉重,需要从 Go 传递一个巨大的字符串到 JS。 如果 WASM 中 SharedArrayBuffer 有所支持可能会改善。我也在研究在画布中直接设置像素,看看有没有任何好处。即使为了消减这个 base64 转换也应该花些时间。(请不吝赐教其他方法) ## 性能 -对于一个 100KB 的 JPEG图像,应用转换所需时间约为180~190毫秒。这个时间随着图像大小而增加。这是使用 Chrome 65 测试的。(FF一直报错,我也没时间调查) +对于一个 100KB 的 JPEG 图像,应用转换所需时间约为 180 ~ 190 毫秒。这个时间随着图像大小而增加。这是使用 Chrome 65 测试的。(FF 一直报错,我也没时间调查) ![性能快照显示](https://raw.githubusercontent.com/studygolang/gctt-images/master/Experiments-with-image-manipulation-in-WASM-using-Go/wasm1.png) 性能快照显示 -![堆相当大。堆快照大约1GB](https://raw.githubusercontent.com/studygolang/gctt-images/master/Experiments-with-image-manipulation-in-WASM-using-Go/wasm2.png) +![堆相当大。堆快照大约 1GB](https://raw.githubusercontent.com/studygolang/gctt-images/master/Experiments-with-image-manipulation-in-WASM-using-Go/wasm2.png) -堆相当大。堆快照大约1GB +堆相当大。堆快照大约 1GB ## 整理想法 diff --git a/published/tech/20180618-anonymous-functions-and-reflection-in-go.md b/published/tech/20180618-anonymous-functions-and-reflection-in-go.md index bbb8c8fa6..97f524787 100644 --- a/published/tech/20180618-anonymous-functions-and-reflection-in-go.md +++ b/published/tech/20180618-anonymous-functions-and-reflection-in-go.md @@ -2,11 +2,11 @@ # Go 中的匿名函数和反射 -我最近在浏览 Hacker News 时看到一篇吸引我眼球的文章《[Python中的Lambdas和函数](http://www.thepythoncorner.com/2018/05/lambdas-and-functions-in-python.html?m=1)》,这篇文章 —— 我推荐你自己阅读一下 —— 详细讲解了如何运用 Python 的 lambda 函数,并举了一个例子展示如何使用 Lambda 函数实现干净,[DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) 风格的代码。 +我最近在浏览 Hacker News 时看到一篇吸引我眼球的文章《[Python 中的 Lambdas 和函数](http://www.thepythoncorner.com/2018/05/lambdas-and-functions-in-python.html?m=1)》,这篇文章 —— 我推荐你自己阅读一下 —— 详细讲解了如何运用 Python 的 lambda 函数,并举了一个例子展示如何使用 Lambda 函数实现干净,[DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) 风格的代码。 -读这篇文章,我大脑中喜欢设计模式的部分对文章里精巧的设计模式兴奋不已,然而同时,我大脑中讨厌动态语言的部分说,“呃~”。一点简短的题外话来表达一下我对动态语言的厌恶(如果你没有同感,请略过): +读这篇文章,我大脑中喜欢设计模式的部分对文章里精巧的设计模式兴奋不已,然而同时,我大脑中讨厌动态语言的部分说,“呃 ~”。一点简短的题外话来表达一下我对动态语言的厌恶(如果你没有同感,请略过): -我曾经是一个动态语言的狂热粉丝(对某些任务我仍然喜欢动态语言并且几乎每天都会使用到它)。Python 是我大学一直选择的语言,我用它做科学计算并且做小的,概念验证的项目(我的个人网站曾经使用Flask)。但是当我在现实世界([Qadium](https://www.qadium.com/))中开始为我的第一个大型 Python 项目做贡献时,一切都变了。这些项目包含了收集,处理并且增强各种定义好的数据类型的系统职责。 +我曾经是一个动态语言的狂热粉丝(对某些任务我仍然喜欢动态语言并且几乎每天都会使用到它)。Python 是我大学一直选择的语言,我用它做科学计算并且做小的,概念验证的项目(我的个人网站曾经使用 Flask)。但是当我在现实世界([Qadium](https://www.qadium.com/))中开始为我的第一个大型 Python 项目做贡献时,一切都变了。这些项目包含了收集,处理并且增强各种定义好的数据类型的系统职责。 最开始我们选择 Python 基于两个原因:1)早期的员工都习惯使用 2)它是一门快速开发语言。 @@ -153,7 +153,7 @@ func (s stack) Pop() (stack, int) { 以上的方案是可以工作的,但是有大堆的代码重复 —— 特别是获取提供给运算符的参数/操作的代码。 -Python-lambda 文章对这个方案做了一个改进,将运算函数写为 lambda 表达式并且放入一个字典中,这样它们可以通过名称来引用,在运行期查找一个运算所需要操作的数值,并用普通的代码将这些操作数提供给运算函数。最终的python代码如下: +Python-lambda 文章对这个方案做了一个改进,将运算函数写为 lambda 表达式并且放入一个字典中,这样它们可以通过名称来引用,在运行期查找一个运算所需要操作的数值,并用普通的代码将这些操作数提供给运算函数。最终的 python 代码如下: ```python """ @@ -238,7 +238,7 @@ class rpn_engine: print(error) ``` -> engine_peter_rel5.py 由GitHub托管 [查看源文件](https://gist.github.com/mastro35/66044197fa886bf842213ace58457687/raw/a84776f1a93919fe83ec6719954384a4965f3788/engine_peter_rel5.py) +> engine_peter_rel5.py 由 GitHub 托管 [查看源文件](https://gist.github.com/mastro35/66044197fa886bf842213ace58457687/raw/a84776f1a93919fe83ec6719954384a4965f3788/engine_peter_rel5.py) 这个方案比原来的方案只增加了一点点复杂度,但是现在增加一个新的运算符简直就像增加一条线一样简单!我看到这个的第一个想法就是:我怎么在 Go 中实现? @@ -263,7 +263,7 @@ func main() { } view rawrpn_operations_map.go hosted with ❤ by GitHub ``` -> rpn_operations_map.go 由gitHub托管 [查看源文件](https://gist.github.com/jholliman/9108b105be6ab136c2f163834b9e5e32/raw/bcd7634a1336b464a9957ea5f32a4868071bef6e/rpn_operations_map.go) +> rpn_operations_map.go 由 gitHub 托管 [查看源文件](https://gist.github.com/jholliman/9108b105be6ab136c2f163834b9e5e32/raw/bcd7634a1336b464a9957ea5f32a4868071bef6e/rpn_operations_map.go) 注意:在 Go 语言中,为了将我们所有的匿名函数保存在同一个 map 中,我们需要使用空接口类型,`interfa{}`。在 Go 中所有类型都实现了空接口(它是一个没有任何方法的接口;所有类型都至少有 0 个函数)。在底层,Go 用两个指针来表示一个接口:一个指向值,另一个指向类型。 @@ -301,7 +301,7 @@ func main() { } } ``` -> rpn_operations_map2.go 由GitHub托管 [查看源文件](https://gist.github.com/jholliman/497733a937fa5949148a7160473b7742/raw/7ec842fe18026bfc1c4d801eca566b4c0541008b/rpn_operations_map2.go) +> rpn_operations_map2.go 由 GitHub 托管 [查看源文件](https://gist.github.com/jholliman/497733a937fa5949148a7160473b7742/raw/7ec842fe18026bfc1c4d801eca566b4c0541008b/rpn_operations_map2.go) 这段代码会产生如下输出(请原谅语法上的瑕疵): @@ -352,7 +352,7 @@ func main() { } } ``` -> rpn_operations_map3.go 由GitHub托管 [查看源文件](https://gist.github.com/jholliman/e3d7abd71b9bf6cb71eb55d49c40b145/raw/ed64e1d3f718e4219c68d189a8149d8179cdc90c/rpn_operations_map3.go) +> rpn_operations_map3.go 由 GitHub 托管 [查看源文件](https://gist.github.com/jholliman/e3d7abd71b9bf6cb71eb55d49c40b145/raw/ed64e1d3f718e4219c68d189a8149d8179cdc90c/rpn_operations_map3.go) 类似与用 `.(type)` 来切换的方法,代码输出如下: @@ -404,7 +404,7 @@ func main() { fmt.Println("The result is ", int(results[0].Int())) } ``` -> rpn_operations_map4.go 由GitHub托管 [查看源文件](https://gist.github.com/jholliman/96bccb0c73c75a165211892da87cd676/raw/e4b420f77350e3f7e081dddb20c0d2a7232cc071/rpn_operations_map4.go) +> rpn_operations_map4.go 由 GitHub 托管 [查看源文件](https://gist.github.com/jholliman/96bccb0c73c75a165211892da87cd676/raw/e4b420f77350e3f7e081dddb20c0d2a7232cc071/rpn_operations_map4.go) 就像我们期待的那样,这段代码会输出: @@ -500,7 +500,7 @@ func (e *RPMEngine) Compute(operation string) error { return nil } ``` -> rpn_calc_solution2.go 由 GitHub托管 [查看源文件](https://gist.github.com/jholliman/c636340cac9253da98efdbfcfde56282/raw/b5f80bb79164b5250b088c6df094ca4ec9840009/rpn_calc_solution2.go) +> rpn_calc_solution2.go 由 GitHub 托管 [查看源文件](https://gist.github.com/jholliman/c636340cac9253da98efdbfcfde56282/raw/b5f80bb79164b5250b088c6df094ca4ec9840009/rpn_calc_solution2.go) 我们确定操作数的个数(第 64 行),从堆栈中获得操作数(第 69-72 行),然后调用需要的运算函数,而且对不同参数个数的运算函数的调用都是一样的(第 74 行)。而且与 Python 的解决方案一样,增加新的运算函数,只需要往 map 中增加一个匿名函数的条目就可以了(第 30 行)。 diff --git a/published/tech/20180708-Understanding-the-context-package-in-golang.md b/published/tech/20180708-Understanding-the-context-package-in-golang.md index 0eedc26b9..d2ef709f4 100644 --- a/published/tech/20180708-Understanding-the-context-package-in-golang.md +++ b/published/tech/20180708-Understanding-the-context-package-in-golang.md @@ -1,10 +1,10 @@ 首发于:https://studygolang.com/articles/13866 -# 理解 golang 中的 context(上下文) 包 +# 理解 Golang 中的 context(上下文) 包 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/understanding-the-context-package-in-golang/0_exTPQ4ppfrdjuXcR.jpg) -Go 中的 context 包在与 API 和慢处理交互时可以派上用场,特别是在生产级的 Web 服务中。在这些场景中,您可能想要通知所有的 goroutine 停止运行并返回。这是一个基本教程,介绍如何在项目中使用它以及一些最佳实践和陷阱。 +Go 中的 context 包在与 API 和慢处理交互时可以派上用场,特别是在生产级的 Web 服务中。在这些场景中,您可能想要通知所有的 Goroutine 停止运行并返回。这是一个基本教程,介绍如何在项目中使用它以及一些最佳实践和陷阱。 要理解 context 包,您应该熟悉两个概念。 @@ -12,7 +12,7 @@ Go 中的 context 包在与 API 和慢处理交互时可以派上用场,特别 ## Goroutine -来自 Go 语言官方文档:"goroutine 是一个轻量级的执行线程"。多个 goroutine 比一个线程轻量所以管理它们消耗的资源相对更少。 +来自 Go 语言官方文档:"goroutine 是一个轻量级的执行线程"。多个 Goroutine 比一个线程轻量所以管理它们消耗的资源相对更少。 Playground: https://play.golang.org/p/-TDMgnkJRY6 @@ -33,15 +33,15 @@ func main() { } ``` -如果您运行上面的程序,您只能看到 main 中打印的 Hello, 因为它启动了两个 goroutine 并在它们完成前退出了。为了让 main 等待这些 goroutine 执行完,您需要一些方法让这些 goroutine 告诉 main 它们执行完了,那就需要用到通道。 +如果您运行上面的程序,您只能看到 main 中打印的 Hello, 因为它启动了两个 Goroutine 并在它们完成前退出了。为了让 main 等待这些 Goroutine 执行完,您需要一些方法让这些 Goroutine 告诉 main 它们执行完了,那就需要用到通道。 ## 通道(channel) -这是 goroutine 之间的沟通渠道。当您想要将结果或错误,或任何其他类型的信息从一个 goroutine 传递到另一个 goroutine 时就可以使用通道。通道是有类型的,可以是 int 类型的通道接收整数或错误类型的接收错误等。 +这是 Goroutine 之间的沟通渠道。当您想要将结果或错误,或任何其他类型的信息从一个 Goroutine 传递到另一个 Goroutine 时就可以使用通道。通道是有类型的,可以是 int 类型的通道接收整数或错误类型的接收错误等。 假设有个 int 类型的通道 ch,如果你想发一些信息到这个通道,语法是 ch <- 1,如果你想从这个通道接收一些信息,语法就是 var := <-ch。这将从这个通道接收并存储值到 var 变量。 -以下程序说明了通道的使用确保了 goroutine 执行完成并将值返回给 main 。 +以下程序说明了通道的使用确保了 Goroutine 执行完成并将值返回给 main 。 注意:WaitGroup( https://golang.org/pkg/sync/#WaitGroup )也可用于同步,但稍后在 context 部分我们谈及通道,所以在这篇博客中的示例代码,我选择了它们。 @@ -84,7 +84,7 @@ func main() { } ``` -在 Go 语言中 context 包允许您传递一个 "context" 到您的程序。 Context 如超时或截止日期(deadline)或通道,来指示停止运行和返回。例如,如果您正在执行一个 web 请求或运行一个系统命令,定义一个超时对生产级系统通常是个好主意。因为,如果您依赖的API运行缓慢,你不希望在系统上备份(back up)请求,因为它可能最终会增加负载并降低所有请求的执行效率。导致级联效应。这是超时或截止日期 context 派上用场的地方。 +在 Go 语言中 context 包允许您传递一个 "context" 到您的程序。 Context 如超时或截止日期(deadline)或通道,来指示停止运行和返回。例如,如果您正在执行一个 Web 请求或运行一个系统命令,定义一个超时对生产级系统通常是个好主意。因为,如果您依赖的 API 运行缓慢,你不希望在系统上备份(back up)请求,因为它可能最终会增加负载并降低所有请求的执行效率。导致级联效应。这是超时或截止日期 context 派上用场的地方。 ## 创建 context @@ -155,7 +155,7 @@ ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second) 现在我们知道了如何创建 context(Background 和 TODO)以及如何派生 context(WithValue,WithCancel,Deadline 和 Timeout),让我们讨论如何使用它们。 -在下面的示例中,您可以看到接受 context 的函数启动一个 goroutine 并等待 该 goroutine 返回或该 context 取消。select 语句帮助我们选择先发生的任何情况并返回。 +在下面的示例中,您可以看到接受 context 的函数启动一个 Goroutine 并等待 该 Goroutine 返回或该 context 取消。select 语句帮助我们选择先发生的任何情况并返回。 `<-ctx.Done()` 一旦 Done 通道被关闭,这个 `<-ctx.Done():` 被选择。一旦发生这种情况,此函数应该放弃运行并准备返回。这意味着您应该关闭所有打开的管道,释放资源并从函数返回。有些情况下,释放资源可以阻止返回,比如做一些挂起的清理等等。在处理 context 返回时,您应该注意任何这样的可能性。 @@ -210,14 +210,14 @@ func sleepRandomContext(ctx context.Context, ch chan bool) { * main 调用取消函数或 * 超时到或 * doWorkContext 调用它的取消函数 -* 启动 goroutine 传入派生上下文执行一些慢处理 -* 等待 goroutine 完成或上下文被 main goroutine 取消,以优先发生者为准 +* 启动 Goroutine 传入派生上下文执行一些慢处理 +* 等待 Goroutine 完成或上下文被 main Goroutine 取消,以优先发生者为准 ***sleepRandomContext*** 函数 -* 开启一个 goroutine 去做些缓慢的处理 -* 等待该 goroutine 完成或, -* 等待 context 被 main goroutine 取消,操时或它自己的取消函数被调用 +* 开启一个 Goroutine 去做些缓慢的处理 +* 等待该 Goroutine 完成或, +* 等待 context 被 main Goroutine 取消,操时或它自己的取消函数被调用 ***sleepRandom*** 函数 diff --git a/published/tech/20180711-Writing-a-Reverse-Proxy-in-just-one-line-with-Go.md b/published/tech/20180711-Writing-a-Reverse-Proxy-in-just-one-line-with-Go.md index 73e0294b2..fa317e48c 100644 --- a/published/tech/20180711-Writing-a-Reverse-Proxy-in-just-one-line-with-Go.md +++ b/published/tech/20180711-Writing-a-Reverse-Proxy-in-just-one-line-with-Go.md @@ -140,7 +140,7 @@ type requestPayloadStruct struct { ProxyCondition string `json:"proxy_condition"` } -// Get a json decoder for a given requests body +// Get a JSON decoder for a given requests body func requestBodyDecoder(request *http.Request) *json.Decoder { // Read body to buffer body, err := ioutil.ReadAll(request.Body) @@ -149,7 +149,7 @@ func requestBodyDecoder(request *http.Request) *json.Decoder { panic(err) } - // Because go lang is a pain in the ass if you read the body then any susequent calls + // Because Go lang is a pain in the ass if you read the body then any susequent calls // are unable to read the body again.... request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) @@ -246,7 +246,7 @@ func serveReverseProxy(target string, res http.ResponseWriter, req *http.Request req.Header.Set("X-Forwarded-Host", req.Header.Get("Host")) req.Host = url.Host - // Note that ServeHttp is non blocking and uses a go routine under the hood + // Note that ServeHttp is non blocking and uses a Go routine under the hood proxy.ServeHTTP(res, req) } @@ -265,7 +265,7 @@ func handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) { 好了,现在启动我们的反向代理程序让其监听 `1330` 端口。让其他的 3 个简单的服务分别监听 `1331–1333` 端口(在各自的终端中)。 -1. `source .env && go install && $GOPATH/bin/reverse-proxy-demo` +1. `source .env && Go install && $GOPATH/bin/reverse-proxy-demo` 2. `http-server -p 1331` 3. `http-server -p 1332` 4. `http-server -p 1333` diff --git a/published/tech/20180713-what-is-webhook.md b/published/tech/20180713-what-is-webhook.md index 0fc04494e..795a260bd 100644 --- a/published/tech/20180713-what-is-webhook.md +++ b/published/tech/20180713-what-is-webhook.md @@ -51,7 +51,7 @@ func main() { dispatcher.add(wr.Name, wr.Destination) }) // start dispatching webhooks - go dispatcher.Start() + Go dispatcher.Start() fmt.Printf("Create webhooks on http://localhost%s/webhooks \n", port) // starting server err := srv.ListenAndServe() @@ -83,7 +83,7 @@ func (d *Dispatcher) dispatch() { d.mu.Lock() defer d.mu.Unlock() for user, destination := range d.destinations { - go func(user, destination string) { + Go func(user, destination string) { req, err := http.NewRequest("POST", destination, bytes.NewBufferString(fmt.Sprintf("Hello %s, current time is %s", user, time.Now().String()))) if err != nil { // probably don't allow creating invalid destinations @@ -100,14 +100,14 @@ func (d *Dispatcher) dispatch() { } ``` -## 运行这个webhook 应用 +## 运行这个 webhook 应用 要使用这个 webhook 应用,我们需要一个可以接收 webhook 消息并且调试的一个终端。为了完成这个任务,我们选择了 [https://bin.webhookrelay.com/](https://bin.webhookrelay.com/) 这个免费的服务。打开这个链接后,会被重定向到一个唯一的地址,那个就是我们要使用的终端地址,后面我们很快就会用到这个地址。 接下来让我们启动这个应用: ``` -$ go run main.go +$ Go run main.go Create webhooks on http://localhost:8090/webhooks ``` diff --git a/published/tech/20180717-12-Best-Golang-Practices-We-Must-Follow.md b/published/tech/20180717-12-Best-Golang-Practices-We-Must-Follow.md index 0788f96aa..bec3d3895 100644 --- a/published/tech/20180717-12-Best-Golang-Practices-We-Must-Follow.md +++ b/published/tech/20180717-12-Best-Golang-Practices-We-Must-Follow.md @@ -243,7 +243,7 @@ import ( ) ``` -这样的情况下,测试代码不能放在 foo 包中,因为它引入了 bar/testutil包,而它导入了 foo。所以我们用点导入 的形式让文件假装是包的一部分,而实际上它并不是。除了这个使用情形外,最好不要用点导入。因为它会让读者阅读代码时更加困难,因为很难确定像 Quux 这样的名字是当前包的顶层声明还是引入的包。 +这样的情况下,测试代码不能放在 foo 包中,因为它引入了 bar/testutil 包,而它导入了 foo。所以我们用点导入 的形式让文件假装是包的一部分,而实际上它并不是。除了这个使用情形外,最好不要用点导入。因为它会让读者阅读代码时更加困难,因为很难确定像 Quux 这样的名字是当前包的顶层声明还是引入的包。 ## 11、注释代码 diff --git a/published/tech/20180717-buffered-and-unbuffered-channels.md b/published/tech/20180717-buffered-and-unbuffered-channels.md index 8e2729081..ec376d586 100644 --- a/published/tech/20180717-buffered-and-unbuffered-channels.md +++ b/published/tech/20180717-buffered-and-unbuffered-channels.md @@ -24,12 +24,12 @@ func main() { var wg sync.WaitGroup wg.Add(2) - go func() { + Go func() { defer wg.Done() c <- `foo` }() - go func() { + Go func() { defer wg.Done() time.Sleep(time.Second * 1) @@ -40,7 +40,7 @@ func main() { } ``` -由于没有准备就绪的接收者,第一个`goroutine`在发送消息`foo`时将被阻塞。这个[说明文档](https://golang.org/ref/spec#Channel_types)很好地解释了这种行为: +由于没有准备就绪的接收者,第一个 `goroutine` 在发送消息 `foo` 时将被阻塞。这个[说明文档](https://golang.org/ref/spec#Channel_types)很好地解释了这种行为: > 如果容量为零或未设置,则通道将被无缓冲,只有在发送方和接收方都准备就绪时通信才能成功。 @@ -56,19 +56,19 @@ func main() { ![hchan 结构](https://raw.githubusercontent.com/studygolang/gctt-images2/master/buffered-and-unbufferd-channel/hchan-struct.png) -通道维护了指向接收方( `recvq` )和发送方( `sendq` )列表的指针,由链表 `waitq.sudog`表示 ,包含指向下一个元素的指针(next)和指向上一个元素的指针(previous),以及与处理 *接收方/发送方* 的 goroutine 相关的信息。有了这些信息,Go 程序就很容易知道,如果没有了发送方,通道就应该阻塞接收方,反之,没有了接收方,通道就应该阻塞发送方。 +通道维护了指向接收方( `recvq` )和发送方( `sendq` )列表的指针,由链表 `waitq.sudog` 表示 ,包含指向下一个元素的指针(next)和指向上一个元素的指针(previous),以及与处理 *接收方/发送方* 的 Goroutine 相关的信息。有了这些信息,Go 程序就很容易知道,如果没有了发送方,通道就应该阻塞接收方,反之,没有了接收方,通道就应该阻塞发送方。 下面是我们前面示例的工作流: 1. 通道是用一个空的接收方和发送方列表创建的。 -2. 第 16 行,我们的第一个 goroutine 将值 `foo` 发送到通道。 -3. 通道从(缓冲)池中获取一个结构体 `sudog`,用以表示发送者。这个结构将维护对 goroutine 和值 `foo` 的引用。 +2. 第 16 行,我们的第一个 Goroutine 将值 `foo` 发送到通道。 +3. 通道从(缓冲)池中获取一个结构体 `sudog`,用以表示发送者。这个结构将维护对 Goroutine 和值 `foo` 的引用。 4. 这个发送者现在进入队列(enqueued ) `sendq` 。 5. 由于“*chan send*”阻塞,goroutine 进入等待状态。 -6. 第 23 行,我们的第二个 goroutine 将读取来自通道的消息。 +6. 第 23 行,我们的第二个 Goroutine 将读取来自通道的消息。 7. 通道将弹出 `sendq` 队列,以获取步骤 3 中的等待发送的结构体。 8. 通道将使用 `memmove` 函数将发送方发送的值(封装装在 `sudog` 结构中)复制到读取的通道的变量。 -9. 现在,我们的第一个 goroutine 可以恢复在第 5 步,并将释放在第 3 步获得的 `sudog`。 +9. 现在,我们的第一个 Goroutine 可以恢复在第 5 步,并将释放在第 3 步获得的 `sudog`。 正如我们在工作流中再次看到的,goroutine 必须切换到等待,直到接收器可用为止。但是,如果需要,这种阻塞行为可以通过缓冲通道避免。 @@ -90,14 +90,14 @@ func main() { var wg sync.WaitGroup wg.Add(2) - go func() { + Go func() { defer wg.Done() c <- `foo` c <- `bar` }() - go func() { + Go func() { defer wg.Done() time.Sleep(time.Second * 1) @@ -109,7 +109,7 @@ func main() { } ``` -现在让我们根据这个例子分析结构`hchan`和与缓冲区相关的字段: +现在让我们根据这个例子分析结构 `hchan` 和与缓冲区相关的字段: ![缓冲通道的 hchan 结构](https://raw.githubusercontent.com/studygolang/gctt-images2/master/buffered-and-unbufferd-channel/hchan%20structure%20with%20buffer%20attributes.png) @@ -121,14 +121,14 @@ buffer(缓冲)由以下五个属性组成: * `sendx` 存储缓冲区中的位置,以便通道接收下一个元素 * `recvx` 在缓冲区中存储通道返回的下一个元素的位置 -通过`sendx`和`recvx`,这个缓冲区就像一个循环队列: +通过 `sendx` 和 `recvx`,这个缓冲区就像一个循环队列: ![通道结构中的循环队列](https://raw.githubusercontent.com/studygolang/gctt-images2/master/buffered-and-unbufferd-channel/circular%20queue%20in%20the%20channel%20struct.png) 这个循环队列允许我们在缓冲区中维护一个顺序,而不需要在其中一个元素从缓冲区弹出时不断移动元素。 -正如我们在前一节中看到的那样,一旦达到缓冲区的上限,尝试在缓冲区中发送元素的 goroutine 将被移动到发送者列表中,并切换到等待状态。 -然后,一旦程序读取缓冲区,从缓冲区中返回位于 `recvx` 位置的元素,将释放等待的 goroutine ,它的值将被推入缓冲中。 +正如我们在前一节中看到的那样,一旦达到缓冲区的上限,尝试在缓冲区中发送元素的 Goroutine 将被移动到发送者列表中,并切换到等待状态。 +然后,一旦程序读取缓冲区,从缓冲区中返回位于 `recvx` 位置的元素,将释放等待的 Goroutine ,它的值将被推入缓冲中。 这种属性使 通道有[FIFO(先进先出)](http://lsm6ds3%20fifo%20pattern/)的行为。 ## 由于缓冲区大小不足造成的延迟 @@ -167,7 +167,7 @@ func benchmarkWithBuffer(b *testing.B, size int) { var wg sync.WaitGroup wg.Add(1) - go func() { + Go func() { defer wg.Done() for i := uint32(0); i < 1000; i++ { @@ -179,7 +179,7 @@ func benchmarkWithBuffer(b *testing.B, size int) { var total uint32 for w := 0; w < 5; w++ { wg.Add(1) - go func() { + Go func() { defer wg.Done() for { @@ -203,10 +203,10 @@ func benchmarkWithBuffer(b *testing.B, size int) { ```sh name time/op -WithNoBuffer-8 306µs ± 3% -WithBufferSizeOf1-8 248µs ± 1% -WithBufferSizeEqualsToNumberOfWorker-8 183µs ± 4% -WithBufferSizeExceedsNumberOfWorker-8 134µs ± 2% +WithNoBuffer-8 306 µ s ± 3% +WithBufferSizeOf1-8 248 µ s ± 1% +WithBufferSizeEqualsToNumberOfWorker-8 183 µ s ± 4% +WithBufferSizeExceedsNumberOfWorker-8 134 µ s ± 2% ``` 一个适当大小的缓冲区确实可以使您的应用程序更快!让我们跟踪分析基准测试,以确定延迟在哪里。 diff --git a/published/tech/20180721-everything-you-need-to-know-about-packages-in-go.md b/published/tech/20180721-everything-you-need-to-know-about-packages-in-go.md index 7fec881b9..0f210cf8b 100644 --- a/published/tech/20180721-everything-you-need-to-know-about-packages-in-go.md +++ b/published/tech/20180721-everything-you-need-to-know-about-packages-in-go.md @@ -6,9 +6,9 @@ 如果你对像 **Java** 或者 **NodeJS** 这样的语言熟悉,那么你可能对**包**(译者注:原文中 **packages** ,后文中将其全部译为中文出现在文章表述中)相当熟悉了。包不是什么其他的,而是一个有着许多代码文件的目录,它从单个引用点显示不同的变量(特征)。让我来解释一下,这是什么意思。 -设想在某个项目上工作,你需要不断的修改超过一千个函数。这之中的一些函数有相同的行为。比如,`toUpperCase` 和 `toLowerCase` 函数转变 `字符串` 的大小写,因此你把它们写在了一个单独的文件(*可能*是 **case.go**)里。也有一些其他的函数对 `字符串` 数据类型做一些其他操作,因此你也把它们写在了独立的文件里。 +设想在某个项目上工作,你需要不断的修改超过一千个函数。这之中的一些函数有相同的行为。比如,`toUpperCase` 和 `toLowerCase` 函数转变 ` 字符串 ` 的大小写,因此你把它们写在了一个单独的文件(*可能*是 **case.go**)里。也有一些其他的函数对 ` 字符串 ` 数据类型做一些其他操作,因此你也把它们写在了独立的文件里。 -因为你可能有很多对于 `字符串` 数据类型进行一些操作的文件,因此你创建了一个名为 `string` 的目录,并将所有 `字符串` 相关的文件都放进去了。最后你将所有的这些目录放在一个将成为你的包的父目录里。整个包的结构看上去像下面这样。 +因为你可能有很多对于 ` 字符串 ` 数据类型进行一些操作的文件,因此你创建了一个名为 `string` 的目录,并将所有 ` 字符串 ` 相关的文件都放进去了。最后你将所有的这些目录放在一个将成为你的包的父目录里。整个包的结构看上去像下面这样。 ``` package-name diff --git a/published/tech/20180806-Basic-monitoring-of-Go-apps-with-the-runtime-package.md b/published/tech/20180806-Basic-monitoring-of-Go-apps-with-the-runtime-package.md index 32ce28377..363f6d7a0 100644 --- a/published/tech/20180806-Basic-monitoring-of-Go-apps-with-the-runtime-package.md +++ b/published/tech/20180806-Basic-monitoring-of-Go-apps-with-the-runtime-package.md @@ -8,7 +8,7 @@ ## Goroutines -goroutine 是 Go 的调度管理器为我们准备的非常轻量级的线程。在任何代码中可能会出现的一个典型问题被称为“ goroutines 泄露”。这个问题的原因有很多种,如忘记设置默认的 http 请求超时,SQL 超时,缺乏对上下文包取消的支持,向已关闭的通道发数据等。当这个问题发生时,一个 goroutine 可能无限期的存活,并且永远不释放它所使用的资源。 +goroutine 是 Go 的调度管理器为我们准备的非常轻量级的线程。在任何代码中可能会出现的一个典型问题被称为“ goroutines 泄露”。这个问题的原因有很多种,如忘记设置默认的 http 请求超时,SQL 超时,缺乏对上下文包取消的支持,向已关闭的通道发数据等。当这个问题发生时,一个 Goroutine 可能无限期的存活,并且永远不释放它所使用的资源。 我们可能会对 [runtime.NumGoroutine() int](https://golang.org/pkg/runtime/#NumGoroutine) 这个很基本当函数感兴趣,它会返回当前存在的 goroutines 数量。我们只要打印这个数字并在一段时间内检查它,就可以合理的确认我们可能 goroutines 泄漏,然后调查这些问题。 @@ -134,12 +134,12 @@ BenchmarkStringReverseBetter-4 775 ns/op 480 B/op + TotalAlloc -在堆中累计分配最大字节数(不会减少), + Sys -从系统获得的总内存, + Mallocs 和 Frees - 分配,释放和存活对象数(mallocs - frees), -+ PauseTotalNs -从应用开始总GC暂停, ++ PauseTotalNs -从应用开始总 GC 暂停, + NumGC - GC 循环完成数 ## 方法 -因此,我们开始的前提是,我们不希望使用外部服务来提供简单的应用程序监控。我的目标是每隔一段时间将收集到的度量指标打印到控制台上。我们应该启动一个 goroutine,每隔X秒就可以得到这个数据,然后把它打印到控制台。 +因此,我们开始的前提是,我们不希望使用外部服务来提供简单的应用程序监控。我的目标是每隔一段时间将收集到的度量指标打印到控制台上。我们应该启动一个 goroutine,每隔 X 秒就可以得到这个数据,然后把它打印到控制台。 ```go package main @@ -191,14 +191,14 @@ func NewMonitor(duration int) { m.PauseTotalNs = rtm.PauseTotalNs m.NumGC = rtm.NumGC - // Just encode to json and print + // Just encode to JSON and print b, _ := json.Marshal(m) fmt.Println(string(b)) } } ``` -要使用它,你可以用 go NewMonitor(300) 来调用它,它每5分钟打印一次你的应用程序度量。然后,您可以从控制台或历史日志中检查这些,以查看应用程序的行为。将其添加到应用程序中的任何性能影响都很小。 +要使用它,你可以用 Go NewMonitor(300) 来调用它,它每 5 分钟打印一次你的应用程序度量。然后,您可以从控制台或历史日志中检查这些,以查看应用程序的行为。将其添加到应用程序中的任何性能影响都很小。 ``` {"Alloc":1143448,"TotalAlloc":1143448,"Sys":5605624,"Mallocs":8718,"Frees":301,"LiveObjects":8417,"PauseTotalNs":0,"NumGC":0,"NumGoroutine":6} @@ -214,14 +214,14 @@ Go 实际上有两个内置插件,帮助我们监控生产中的应用程序 几分钟后,我注册了 expvar 的 HTTP 处理程序,我意识到完整的 MemStats 结构已经在上面了。那太好了! -除了添加HTTP处理程序外,此包还记录以下变量: +除了添加 HTTP 处理程序外,此包还记录以下变量: + cmdline os.Args + memstats runtime.Memstats -该包有时仅用于注册其HTTP处理程序和上述变量的副作用。要这样使用,把这个包链接到你的程序中:`import _ "expvar"` +该包有时仅用于注册其 HTTP 处理程序和上述变量的副作用。要这样使用,把这个包链接到你的程序中:`import _ "expvar"` -由于度量现在已经导出,您只需要在应用程序上指向监视系统,并在那里导入 memstats 输出。我知道,我们仍然没有 goroutine 计数,但这很容易添加。导入 expvar 包并添加以下几行: +由于度量现在已经导出,您只需要在应用程序上指向监视系统,并在那里导入 memstats 输出。我知道,我们仍然没有 Goroutine 计数,但这很容易添加。导入 expvar 包并添加以下几行: ```go // The next line goes at the start of NewMonitor() diff --git a/published/tech/20180809-A-Good-Markefile-for-Go.md b/published/tech/20180809-A-Good-Markefile-for-Go.md index 50dba4404..48a9b8f03 100644 --- a/published/tech/20180809-A-Good-Markefile-for-Go.md +++ b/published/tech/20180809-A-Good-Markefile-for-Go.md @@ -138,7 +138,7 @@ compile: ```makefile start-server: @echo " > $(PROJECTNAME) is available at $(ADDR)" - @-$(GOBIN)/$(PROJECTNAME) 2>&1 & echo $$! > $(PID) + @-$(GOBIN)/$(PROJECTNAME) 2>&1 & Echo $$! > $(PID) @cat $(PID) | sed "/^/s/^/ \> PID: /" stop-server: @@ -154,7 +154,7 @@ restart-server: stop-server start-server 我们需要一个文件观察器来观察变化。我尝试了很多并且感到不满意,所以最终创建了我自己的文件观察工具 [yolo](https://github.com/azer/yolo) 。通过下面命令安装在您的系统中 ```shell -$ go get github.com/azer/yolo +$ Go get github.com/azer/yolo ``` 一旦安装完毕,我们基本上可以开始观察项目目录中的更改,排除像 `vendor` 或者 `bin` 这样的目录,如下: @@ -204,7 +204,7 @@ make install get="github.com/foo/bar" 在内部,这个命令会转换成: ```shell -$ GOPATH=~/my-web-server GOBIN=~/my-web-server/bin go get github.com/foo/bar +$ GOPATH=~/my-web-server GOBIN=~/my-web-server/bin Go get github.com/foo/bar ``` 它是如何工作的?请参阅下一节,我们实际添加了用于实现更高级别命令的 Go 命令。 @@ -218,22 +218,22 @@ go-compile: go-clean go-get go-build go-build: @echo " > Building binary..." - @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go build -o $(GOBIN)/$(PROJECTNAME) $(GOFILES) + @GOPATH=$(GOPATH) GOBIN=$(GOBIN) Go build -o $(GOBIN)/$(PROJECTNAME) $(GOFILES) go-generate: @echo " > Generating dependency files..." - @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go generate $(generate) + @GOPATH=$(GOPATH) GOBIN=$(GOBIN) Go generate $(generate) go-get: @echo " > Checking if there is any missing dependencies..." - @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go get $(get) + @GOPATH=$(GOPATH) GOBIN=$(GOBIN) Go get $(get) go-install: - @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go install $(GOFILES) + @GOPATH=$(GOPATH) GOBIN=$(GOBIN) Go install $(GOFILES) go-clean: @echo " > Cleaning build cache" - @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go clean + @GOPATH=$(GOPATH) GOBIN=$(GOBIN) Go clean ``` ### 帮助 @@ -309,7 +309,7 @@ stop: stop-server start-server: stop-server @echo " > $(PROJECTNAME) is available at $(ADDR)" - @-$(GOBIN)/$(PROJECTNAME) 2>&1 & echo $$! > $(PID) + @-$(GOBIN)/$(PROJECTNAME) 2>&1 & Echo $$! > $(PID) @cat $(PID) | sed "/^/s/^/ \> PID: /" stop-server: @@ -342,22 +342,22 @@ go-compile: go-clean go-get go-build go-build: @echo " > Building binary..." - @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go build -o $(GOBIN)/$(PROJECTNAME) $(GOFILES) + @GOPATH=$(GOPATH) GOBIN=$(GOBIN) Go build -o $(GOBIN)/$(PROJECTNAME) $(GOFILES) go-generate: @echo " > Generating dependency files..." - @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go generate $(generate) + @GOPATH=$(GOPATH) GOBIN=$(GOBIN) Go generate $(generate) go-get: @echo " > Checking if there is any missing dependencies..." - @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go get $(get) + @GOPATH=$(GOPATH) GOBIN=$(GOBIN) Go get $(get) go-install: - @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go install $(GOFILES) + @GOPATH=$(GOPATH) GOBIN=$(GOBIN) Go install $(GOFILES) go-clean: @echo " > Cleaning build cache" - @GOPATH=$(GOPATH) GOBIN=$(GOBIN) go clean + @GOPATH=$(GOPATH) GOBIN=$(GOBIN) Go clean .PHONY: help all: help @@ -377,7 +377,7 @@ help: Makefile via: https://kodfabrik.com/journal/a-good-makefile-for-go/ -作者:[Azer Koçulu](http://kodfabrik.com) +作者:[Azer Ko ç ulu](http://kodfabrik.com) 译者:[lightfish-zhang](https://github.com/lightfish-zhang) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20180814-Refactoring-in-Go-gouroutine-concurrency.md b/published/tech/20180814-Refactoring-in-Go-gouroutine-concurrency.md index 0380d6534..22c4e389b 100644 --- a/published/tech/20180814-Refactoring-in-Go-gouroutine-concurrency.md +++ b/published/tech/20180814-Refactoring-in-Go-gouroutine-concurrency.md @@ -26,7 +26,7 @@ func AverageLatency(host string) (latency int64, err error) { errorsResults := make(chan string, REQUESTS_LIMIT) for w := 1; w <= CONCURRENCY; w++ { - go dnsTest(dnsRequests, results, errorsResults, host) + Go dnsTest(dnsRequests, results, errorsResults, host) } for j := 1; j <= REQUESTS_LIMIT; j++ { @@ -84,7 +84,7 @@ func AverageLatency(host string) (latency int64, err error) { errorsResults := make(chan string, REQUESTS_LIMIT) for w := 1; w <= REQUESTS_LIMIT; w++ { - go func() { + Go func() { start := time.Now() if _, err := net.LookupHost(host); err != nil { errorResults <- err.Error() @@ -126,7 +126,7 @@ func AverageLatency(host string) (latency int64, err error) { wg.Add(REQUESTS_LIMIT) for j := 0; j < REQUESTS_LIMIT; j++ { - go func() { + Go func() { defer wg.Done() start := time.Now() if _, err := net.LookupHost(host); err != nil { @@ -163,7 +163,7 @@ func AverageLatency(host string) Metrics { wg.Add(REQUESTS_LIMIT) for j := 0; j < REQUESTS_LIMIT; j++ { - go func() { + Go func() { defer wg.Done() start := time.Now() if _, err := net.LookupHost(host); err != nil { @@ -221,7 +221,7 @@ func CalculateStats(results *[]int64, errors *int64) Metrics { ```go func waitWithTimeout(wg *sync.WaitGroup, timeout time.Duration) bool { c := make(chan struct{}) - go func() { + Go func() { defer close(c) wg.Wait() }() @@ -249,7 +249,7 @@ func AverageLatency(host string) Metrics { wg.Add(REQUESTS_LIMIT) for j := 0; j < REQUESTS_LIMIT; j++ { - go func() { + Go func() { defer wg.Done() start := time.Now() if _, err := net.LookupHost(host); err != nil { @@ -301,7 +301,7 @@ func AverageLatency(host string) Metrics { wg.Add(DEFAULT_REQUESTS_LIMIT) for j := 0; j < REQUESTS_LIMIT; j++ { - go func() { + Go func() { start := time.Now() if _, err := net.LookupHost(host); err != nil { @@ -314,7 +314,7 @@ func AverageLatency(host string) Metrics { }() } - go func() { + Go func() { for t := range successfulRequestsQueue { results = append(results, t) wg.Done() diff --git a/published/tech/20180821-Introduction-to-Go-Modules.md b/published/tech/20180821-Introduction-to-Go-Modules.md index efaf6fd25..68a38e227 100644 --- a/published/tech/20180821-Introduction-to-Go-Modules.md +++ b/published/tech/20180821-Introduction-to-Go-Modules.md @@ -35,7 +35,7 @@ func Hi(name string) string { 这个包已经写完了,但是现在还不是一个 module,我们来把它初始化为 module: ```bash -$ go mod init github.com/robteix/testmod +$ Go mod init github.com/robteix/testmod go: creating new go.mod: module github.com/robteix/testmod ``` @@ -50,10 +50,10 @@ module github.com/robteix/testmod 我们现在可以把这个代码推送到代码仓库里面了: ```bash -$ git init -$ git add * -$ git commit -am "First commit" -$ git push -u origin master +$ Git init +$ Git add * +$ Git commit -am "First commit" +$ Git push -u origin master ``` (译注:在 `git push` 之前,你可能还要添加远程仓库地址,例如:`git remote add origin https://github.com/robteix/testmod.git`) @@ -61,7 +61,7 @@ $ git push -u origin master 到目前为止,任何想要用这个包的人,都可以 `go get` 之: ```bash -$ go get github.com/robteix/testmod +$ Go get github.com/robteix/testmod ``` 上述的命令会获取 `master` 分支上最新的代码。这个方法依然凑效,但是我们现在最好不要再这么做了,因为我们有更棒的方法了。获取 `master` 分支有潜在的危险,因为我们不能确定,包作者对包的改动会不会破坏掉我们的项目对该包的使用方式。(译注:也就是说,我们不能确定当前 `master` 分支的代码是否保持了对旧版本代码的兼容性)。而这个就是 modules 机制旨在解决的问题。 @@ -81,8 +81,8 @@ Go 的 modules 是*版本化的*,并且某些版本有特殊的含义,你需 我们的包已经准备好了,现在我们可以向全世界发布它。我们通过版本标签来实现这个发布,现在,我们一起来发布我们的 1.0.0 版本: ```bash -$ git tag v1.0.0 -$ git push --tags +$ Git tag v1.0.0 +$ Git push --tags ``` 上述命令在我们的仓库上面创建了一个标签,标记了我们当前的提交为 1.0.0 版本。 @@ -90,8 +90,8 @@ $ git push --tags 虽然 Go 没有强制要求,但是我们最好创建还是一个叫 `v1` 的分支,这样我们可以把针对这个版本的 bug 修复推送到这个分支: ```bash -$ git checkout -b v1 -$ git push -u origin v1 +$ Git checkout -b v1 +$ Git push -u origin v1 ``` 现在我们可以切换到 `master` 分支,做自己要做的事情,而不用担心会影响到我们已经发布的 1.0.0 版本的代码。 @@ -117,7 +117,7 @@ func main() { 到现在,你可以 `go get github.com/robteix/testmod` 来下载这个包。但是对于 module 来说,事情就变得有趣了。首先我们需要在我们新的程序里面启用 module 功能: ```bash -$ go mod init mod +$ Go mod init mod ``` 正如之前所发生的那样,上面的命令会创建一个 `go.mod` 文件,它的内容是: @@ -129,7 +129,7 @@ module mod 当我们尝试构建我们的程序时,事情变得更加有趣了: ```bash -$ go build +$ Go build go: finding github.com/robteix/testmod v1.0.0 go: downloading github.com/robteix/testmod v1.0.0 ``` @@ -163,9 +163,9 @@ func Hi(name string) string { 我们在 `v1` 分支做这些改动,因为这个 bug 只在 `v1` 版本中存在。当然,在实际的情况中,我们很有可能需要把这个改动应用到多个版本,这时候你可能就需要在 `master` 分支做这些改动,然后再向后移植(译注:back-port 或称 backporting, 参考[维基百科](https://zh.wikipedia.org/wiki/%E5%90%91%E5%BE%8C%E7%A7%BB%E6%A4%8D) )。无论怎样,我们都需要在 `v1` 分支上有这些改动,并且把它标记为一个新的发布: ```bash -$ git commit -m "Emphasize our friendliness" testmod.go -$ git tag v1.0.1 -$ git push --tags origin v1 +$ Git commit -m "Emphasize our friendliness" testmod.go +$ Git tag v1.0.1 +$ Git push --tags origin v1 ``` ## 更新 modules @@ -185,9 +185,9 @@ $ git push --tags origin v1 因为我们的程序使用的是包 1.0.0 的版本,并且我们刚刚创建了 1.0.1 版本,下面任意一条命令都可以让我们程序使用的包更新到 1.0.1 版本: ```bash -$ go get -u -$ go get -u=patch -$ go get github.com/robteix/testmod@v1.0.1 +$ Go get -u +$ Go get -u=patch +$ Go get github.com/robteix/testmod@v1.0.1 ``` 运行完其中一个(比如说 `go get -u`)之后,我们的 `go.mod` 文件变成了: @@ -230,7 +230,7 @@ func Hi(name, lang string) (string, error) { } ``` -以前使用我们的包的项目,如果直接使用现在这个新的版本,它们将不能编译通过,因为它们没有传递 `lang` 参数,并且它们没有接收返回的 `error` 错误。所以我们的 API 与 v1.x 版本的 API 不能兼容,是时候跃进新的2.0.0时代啦! +以前使用我们的包的项目,如果直接使用现在这个新的版本,它们将不能编译通过,因为它们没有传递 `lang` 参数,并且它们没有接收返回的 `error` 错误。所以我们的 API 与 v1.x 版本的 API 不能兼容,是时候跃进新的 2.0.0 时代啦! 我之前提到的,某些版本有特殊的含义,现在就是这种情况,版本 2 和更高版本需要改变导入路径,它们已经是不同的包了。 @@ -243,12 +243,12 @@ module github.com/robteix/testmod/v2 剩下的事情跟我们之前做的一样,我们把它标记成 v2.0.0,并推送到远程仓库(并且可选地,我们还能添加一个 `v2` 分支) ```bash -$ git commit testmod.go -m "Change Hi to allow multilang" -$ git checkout -b v2 # 可选的,但是推荐这么做 -$ echo "module github.com/robteix/testmod/v2" > go.mod -$ git commit go.mod -m "Bump version to v2" -$ git tag v2.0.0 -$ git push --tags origin v2 # 如果没有新建 v2 分支,就推送到 master 分支 +$ Git commit testmod.go -m "Change Hi to allow multilang" +$ Git checkout -b v2 # 可选的,但是推荐这么做 +$ Echo "module github.com/robteix/testmod/v2" > go.mod +$ Git commit go.mod -m "Bump version to v2" +$ Git tag v2.0.0 +$ Git push --tags origin v2 # 如果没有新建 v2 分支,就推送到 master 分支 ``` ## 更新到一个新的主要版本 @@ -313,7 +313,7 @@ require github.com/robteix/testmod/v2 v2.0.0 默认情况下,Go 并不会在 `go.mod` 上面移除掉依赖项,除非你明确地指示它这么做。如果你希望能够清理掉那些不再需要的依赖项,你可以使用新的 `tidy` 命令: ```bash -$ go mod tidy +$ Go mod tidy ``` 现在剩下的依赖项都是我们项目中使用到的了。 @@ -323,18 +323,18 @@ $ go mod tidy 默认情况下,Go modules 会忽略 `vendor/` 目录。这个想法是最终废除掉 vendor 机制[^1]。但如果我们仍然想要在我们的版本管理中添加 vendor 机制管理依赖,我们还是可以这么做的: ```bash -$ go mod vendor +$ Go mod vendor ``` -这会在你项目的根目录创建一个 `vendor/`目录,并包含你的项目的所有依赖项。 +这会在你项目的根目录创建一个 `vendor/` 目录,并包含你的项目的所有依赖项。 即使如此,`go build` 默认还是会忽略这个目录的内容,如果你想要构建的时候从 `vendor/` 目录中获取依赖的代码来构建,那么你需要明确的指示: ```bash -$ go build -mod vendor +$ Go build -mod vendor ``` -我猜想大多数要使用 vendor 机制的开发者,在他们自己的开发机器上会使用 `go build` ,而在他们的CI系统(Continuous Integration,持续集成)上则使用 `-mod vendor` 选项 +我猜想大多数要使用 vendor 机制的开发者,在他们自己的开发机器上会使用 `go build` ,而在他们的 CI 系统(Continuous Integration,持续集成)上则使用 `-mod vendor` 选项 还有,对于那些不想要直接依赖版本控制服务(译注:比如 github.com)上游代码的人来说,比起用 vendor 这种机制,更好的方法是使用 Go module 代理。 @@ -346,7 +346,7 @@ $ go build -mod vendor 当我们构建程序的时候,它的依赖项会被自动地获取。Go 的 module 还消除了 `$GOPATH` 的使用, `$GOPATH` 曾经使得很多 Go 开发新手难以理解为什么所以东西都要放到一个特定的目录。 -~~Vendor 机制已经被使用 module 代理的方法取代了~~[^1],我大概会专门新开一篇关于 Go module 代理的的文章。 +~~Vendor 机制已经被使用 module 代理的方法取代了 ~~[^1],我大概会专门新开一篇关于 Go module 代理的的文章。 --- diff --git a/published/tech/20180925-How-a-Go-Program-Compiles-down-to-Machine-Code.md b/published/tech/20180925-How-a-Go-Program-Compiles-down-to-Machine-Code.md index 01f8ae072..c6fc605a0 100644 --- a/published/tech/20180925-How-a-Go-Program-Compiles-down-to-Machine-Code.md +++ b/published/tech/20180925-How-a-Go-Program-Compiles-down-to-Machine-Code.md @@ -296,7 +296,7 @@ func main() { 为了展示生成的 SSA,我们需要对要查看 SSA 的方法设置环境变量 GOSSAFUNC,此处就是 main。我们还需要给编译器传递 -S 标志,这样它才能打印代码并创建一个 HTML 文件。我们也会针对 Linux 64-bit 环境进行编译,从而保证生成的机器码和你这边看到的一样。所以,我们运行: ```bash -$ GOSSAFUNC=main GOOS=linux GOARCH=amd64 go build -gcflags “-S” simple.go +$ GOSSAFUNC=main GOOS=linux GOARCH=amd64 Go build -gcflags “-S” simple.go ``` 这会打印整个 SSA,并生成一个相关联的 ssa.html 文件。 diff --git a/published/tech/20180927-Go-Face-Recognition-Tutorial-Part-1.md b/published/tech/20180927-Go-Face-Recognition-Tutorial-Part-1.md index fe158ccb5..0a9572925 100644 --- a/published/tech/20180927-Go-Face-Recognition-Tutorial-Part-1.md +++ b/published/tech/20180927-Go-Face-Recognition-Tutorial-Part-1.md @@ -46,7 +46,7 @@ $ sed -i '' 's/^Libs: .*/& -lblas -llapack/' /usr/local/lib/pkgconfig/dlib-1.pc 我们首先需要下载 `kagami/go-face 包`,可以使用如下 `go get` 命令: ``` -$ go get -u github.com/Kagami/go-face +$ Go get -u github.com/Kagami/go-face ``` 在你的 GOPATH 目录中创建一个名为 `go-face-recognition` 的新目录。在此目录中创建一个名为 `main.go` 的文件,这是我们所有源码所在位置。 @@ -139,7 +139,7 @@ func main() { 当我们运行它时,应该会看到以下输出: ``` -$ go run main.go +$ Go run main.go Facial Recognition System v0.01 Recognizer Initialized Number of Faces in Image: 1 @@ -259,7 +259,7 @@ fmt.Println(labels[avengerID]) 当你一起运行时,你应该会看到以下输出: ``` -$ go run main.go +$ Go run main.go Facial Recognition System v0.01 Recognizer Initialized Number of Faces in Image: 4 diff --git a/published/tech/20180927-learning-about-go-unaddressable-values-and-slicing.md b/published/tech/20180927-learning-about-go-unaddressable-values-and-slicing.md new file mode 100644 index 000000000..29027a271 --- /dev/null +++ b/published/tech/20180927-learning-about-go-unaddressable-values-and-slicing.md @@ -0,0 +1,80 @@ +首发于:https://studygolang.com/articles/26301 + +# 了解 Go 的不可寻址值和切片 + +Dave Cheney 最近在 Twitter 上发布了一个 Go 的小测验,和往常一样,我从中学到了一些有趣的东西。让我们从他的推文开始: + +`#golang` 小测验:该程序打印什么? + +```go +package main +import ( + "crypto/sha1" + "fmt" +) + +func main() { + input := []byte("Hello, playground") + hash := sha1.Sum(input)[:5] + fmt.Println(hash) +} +``` + +令我惊讶的是,答案是: + +```bash +./test.go:10:28: invalid operation sha1.Sum(input)[:5] (slice of unaddressable value) +``` + +我们收到此错误有三个原因。首先,[`sha1.Sum()`](https://golang.org/pkg/crypto/sha1/#Sum) 的返回值不寻常。大多数方法返回切片,而此代码对切片不会报错。但是 `sha1.Sum()` 返回的值很奇怪,它是一个固定大小的数组(具体来说是 `[20]byte` ),由于 Go 是返回数值的,这意味着它确实向 `main()` 返回了 20 字节的数组,而不是指向它的指针。 + +这就涉及到了不可寻址值的概念,与可寻址值相反。详细的介绍在 Go 编程语言规范的 [地址运算符](https://golang.org/ref/spec#Address_operators) 中。简单来说,大多数匿名值都不可寻址( [复合字面值](https://golang.org/ref/spec#Composite_literals) 是一个大大的例外)。在上面的代码中,`sha1.Sum()` 的返回值是匿名的,因为我们立即对其进行了切片操作。如果我们将它存在变量中,并因此使其变为非匿名,则该代码不会报错: + +```go +tmp := sha1.Sum(input) +hash := tmp[:5] +``` + +最后一个问题是为什么切片操作是错误的。这是因为对数组进行切片操作要求该数组是可寻址的(在 Go 编程语言规范的 [Slice 表达式](https://golang.org/ref/spec#Slice_expressions) 的末尾介绍)。`sha1.Sum()` 返回的匿名数组是不可寻址的,因此对其进行切片会被编译器拒绝。 + +(将返回值存储到我们的 tmp 变量中使其变成了可寻址。 `sha1.Sum()` 的返回值在复制到 `tmp` 后就消失了。) + +虽然我不能完全理解为什么 Go 的设计师限制了哪些值是可寻址的,但是我可以想到几条原因。例如,如果在这里允许切片操作,那么 Go 会默默地实现堆存储以容纳 `sha1.Sum()` 的返回值(然后将该值复制到另一个值),该返回值将一直存在直到那个切片被回收。 + +(如 [x86-64 上的 Go 低级调用惯例](https://science.raphael.poss.name/go-calling-convention-x86-64.html#arguments-and-return-value) 中所述,由于 Go 返回了栈中的所有值,因此需要将数据进行拷贝。对于 `sha1.Sum()` 的 20 字节的返回值来说,这并不是什么大事。我很确定人们经常使用更大的结构体作为返回值。) + +PS: Go 语言规范中的许多内容要求或仅对可寻址的值适用。例如,大多数 [赋值](https://golang.org/ref/spec#Assignments) 操作需要可寻址性。 + +## 补充:方法调用和可寻址性 + +假设有一个类型 `T`,并且在 `*T` 上定义了一些方法,例如 `*T.Op()`。就像 Go 允许在不取消引用指针的情况下进行字段引用一样,你可以在非指针值上调用指针方法: + +```go +var x T +x.Op() +``` + +这是 `(&x).Op()` 的简便写法(在 Go 编程语言规范文中靠后的 [调用](https://golang.org/ref/spec#Calls) 部分进行了介绍)。但是,由于此简便写法需要获取地址,因此需要可寻址性。因此,以下操作会报错: + +```go +// afunc() 返回一个 T +afunc().Op() + +// 但是这个可以运行: +var x T = afunc() +x.Op() +``` + +之前我已经看到人们在讨论 Go 在方法调用上的怪癖,但是当时我还不完全了解发生了什么,以及由于什么原因使方法调用无法正常工作。 + +(请注意,这种简写转换与 `*T` 具有所有 T 方法是根本不同的,这在我 [之前的一篇文章](https://utcc.utoronto.ca/~cks/space/blog/programming/GoInterfacesAutogenFuncs) 中提到过) + +--- + +via: https://utcc.utoronto.ca/~cks/space/blog/programming/GoUnaddressableSlice + +作者:[Chris wSiebenmann](https://utcc.utoronto.ca/~cks/space/People/ChrisSiebenmann) +译者:[zhiyu-tracy-yang](https://github.com/zhiyu-tracy-yang) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20181020_Go_Decorator_Function_Pattern_Tutorial.md b/published/tech/20181020_Go_Decorator_Function_Pattern_Tutorial.md index fe6cb31e5..27365e76c 100644 --- a/published/tech/20181020_Go_Decorator_Function_Pattern_Tutorial.md +++ b/published/tech/20181020_Go_Decorator_Function_Pattern_Tutorial.md @@ -31,7 +31,7 @@ func main() { 在这个例子中,我们定义了一个名为 `myFunc` 的函数,它只是打印出 `Hello World`。在我们 `main()` 函数的主体中,我们已经调用了 `fmt.Printf` 并使用 `%T` 打印出作为第二个参数传递的值的类型。在示例种,我们传入参数 `myFunc`,这将打印出以下内容: ``` -$ go run test.go +$ Go run test.go Type: func() ``` @@ -70,7 +70,7 @@ func main() { 当我们运行它时,可以看到新输出具有我们期望的字符串 `Hello World`: ``` -$ go run test.go +$ Go run test.go Type: func() Hello World ``` @@ -109,7 +109,7 @@ func main() { 在调用它时,您应该看到像这样的日志: ```go -$ go run test.go +$ Go run test.go Type: func() Starting function execution: 2018-10-21 11:11:25.011873 +0100 BST m=+0.000443306 Hello World diff --git a/published/tech/20181104-anatomy-of-goroutines-in-go-concurrency-in-go.md b/published/tech/20181104-anatomy-of-goroutines-in-go-concurrency-in-go.md index f21b945f4..35ce44cc9 100644 --- a/published/tech/20181104-anatomy-of-goroutines-in-go-concurrency-in-go.md +++ b/published/tech/20181104-anatomy-of-goroutines-in-go-concurrency-in-go.md @@ -45,7 +45,7 @@ go 协程总是在后台运行,当一个 Go 协程执行的时候(在这个
https://play.golang.org/p/ujQKjpALlRJ
-如上图所示,我们修改了程序,程序在 main 函数的最后一条语句之前调用了 `time.Sleep(10 * time.Millisecond)`,使得 `主协程` 在执行最后一条指令之前调度器就将控制权转移给了 `printhello 协程`。在这个例子中,我们通过调用 `time.Sleep(10 * time.Millisecond)` 强行让 `主协程` 休眠 10ms 并且在在这个 10ms 内不会再被调度器重新调度运行。 +如上图所示,我们修改了程序,程序在 main 函数的最后一条语句之前调用了 `time.Sleep(10 * time.Millisecond)`,使得 `主协程` 在执行最后一条指令之前调度器就将控制权转移给了 `printhello 协程`。在这个例子中,我们通过调用 `time.Sleep(10 * time.Millisecond)` 强行让 ` 主协程 ` 休眠 10ms 并且在在这个 10ms 内不会再被调度器重新调度运行。 一旦 `printHello 协程` 执行,它就会向控制台打印‘ Hello World !’,然后该 Go 协程(`printHello 协程`)就会随之终止,接下来 `主协程` 就会被重新调度(因为 main Go 协程已经睡够 10ms 了),并执行最后一条语句。因此,运行上面的程序就会得到以下的输出 : diff --git a/published/tech/20181105-graceful-upgrades-in-go.md b/published/tech/20181105-graceful-upgrades-in-go.md index 45d616f6c..23a8a2084 100644 --- a/published/tech/20181105-graceful-upgrades-in-go.md +++ b/published/tech/20181105-graceful-upgrades-in-go.md @@ -50,7 +50,7 @@ _如果不频繁地调用 `Accept()`,那么新的连接可能会被丢弃。_ 具体来说,新的二进制文件会在 `Exec()` 之后初始化的过程中花费一些时间进行初始化,这将会导致 `Accept()` 调用被推迟。这意味着新的连接将会持续堆积,直到一些连接被丢弃。所以普通的 `Exec()` 调用并不能完成优雅升级的工作。 -## `监听` (`Listen()`) 一切 +## 监听 (`Listen()`) 一切 刚才使用的 `Exec()` 调用并不能解决我们的问题,所以我们需要尝试下一种更好的方案。如果我们 `fork` 然后 `exec` 一个新的进程,然后按照它通用的启动例程开始。在某些时候,它会通过监听某些地址来创建套接字,但是可能会由于 `errno 48`( 也被称为地址已经被使用 ) 的错误返回码而导致这些套接字无法立即开始工作。这是因为操作系统内核阻止了我们想要在旧进程使用的地址和端口上进行监听的操作。 diff --git a/published/tech/20181112-Goroutine-Leaks-The-Forgotten-Sender.md b/published/tech/20181112-Goroutine-Leaks-The-Forgotten-Sender.md index ff098d2ee..4d2529e1d 100644 --- a/published/tech/20181112-Goroutine-Leaks-The-Forgotten-Sender.md +++ b/published/tech/20181112-Goroutine-Leaks-The-Forgotten-Sender.md @@ -26,11 +26,11 @@ https://play.golang.org/p/dsu3PARM24K // leak 是一个有 bug 程序。它启动了一个 goroutine // 阻塞接收 channel。一切都将不复存在 // 向那个 channel 发送数据,并且那个 channel 永远不会关闭 -// 那个 goroutine 会被永远锁死 +// 那个 Goroutine 会被永远锁死 func leak() { ch := make(chan int) - go func() { + Go func() { val := <-ch fmt.Println("We received a value:", val) }() diff --git a/published/tech/20181113-Things_to_avoid_while_using_Golang_plugins.md b/published/tech/20181113-Things_to_avoid_while_using_Golang_plugins.md index fed9ea2cf..367f16ef2 100644 --- a/published/tech/20181113-Things_to_avoid_while_using_Golang_plugins.md +++ b/published/tech/20181113-Things_to_avoid_while_using_Golang_plugins.md @@ -86,7 +86,7 @@ via: https://medium.com/@alperkose/things-to-avoid-while-using-golang-plugins-f34c0a636e8 -作者:[Alper Köse](https://medium.com/@alperkose) +作者:[Alper K ö se](https://medium.com/@alperkose) 译者:[herowk](https://github.com/herowk) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20181113-building-an-api-with-graphql-and-go.md b/published/tech/20181113-building-an-api-with-graphql-and-go.md new file mode 100644 index 000000000..b2daceb8b --- /dev/null +++ b/published/tech/20181113-building-an-api-with-graphql-and-go.md @@ -0,0 +1,503 @@ +首发于:https://studygolang.com/articles/28986 + +# 使用 Go 构建 GraphQL API + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/building-an-api-with-graphql/cover.png) + +> 2020/5/16 更新:大家好,我刚刚更新了该项目以使用 Go module。不幸的是,[realize](https://github.com/oxequa/realize)很长时间没有更新并且无法正常工作。如果您想使用实时重新加载器,则还有其他选择,例如 [air](https://github.com/cosmtrek/air)。否则,请随意忽略帖子中有关 realize 的任何内容,并按通常的方式运行项目。 + +本博文中将使用 **Go**、**GraphQL**、**PostgreSQL** 创建一个 API。我已在项目结构上迭代几个版本,这是我最喜欢的一个。在大部分的时间,我创建 Web APIs 都是通过 **Node.js** 和 **Ruby/Rails**。而第一次使用 **Go** 设计 Web apis 时,需要费很大的劲儿。**Ben Johnson** 的 [Structuring Applications in Go](https://medium.com/@benbjohnson/structuring-applications-in-go-3b04be4ff091) 文章对我有很大的帮助,本博文中的部分代码就得益于 **Ben Johnson** 文章的指导,推荐阅读。 + +## 配置 + +首先,从项目的配置开始。在本篇博文中,我将在 macOS 中完成,但这并不重要。如果在你的 macOS 上还没有 **Go** 和 **PostGreSQL**,[bradford-hamilton/go-graphql-api](https://github.com/github.com/bradford-hamilton/go-graphql-api) 详细讲解了如何在 macOS 上配置 **Go** 和 **PostgreSQL**. + +创建一个新项目--**go-graphal-api**,整体项目结构如下: + +```bash +├── gql +│ ├── gql.go +│ ├── queries.go +│ ├── resolvers.go +│ └── types.go +├── main.go +├── postgres +│ └── postgres.go +└── server + └── server.go +``` + +有一些额外依赖需要安装。开发中热加载的 [realize](https://github.com/oxequa/realize),go-chi 的轻量级路由 [chi](https://github.com/go-chi/chi) 和管理 request/response 负载的 [render](https://github.com/go-chi/render),以及 [graphql-go/graphql](https://github.com/graphql-go/graphql)。 + +```bash +go get github.com/oxequa/realize +go get github.com/go-chi/chi +go get github.com/go-chi/render +go get github.com/graphql-go/graphql +go get github.com/lib/pq +``` + +最后,创建一个数据库和一些测试使用的数据,在 Postgres 的命令行中输入 **psql**,创建一个数据库: + +```sql +CREATE DATABASE go_graphql_db; +``` + +然后连接上该库: + +```bash +\c go_graphql_db +``` + +连接上后,将以下 sql 语句粘贴到命令行: + +```sql +CREATE TABLE users ( + id serial PRIMARY KEY, + name VARCHAR (50) NOT NULL, + age INT NOT NULL, + profession VARCHAR (50) NOT NULL, + friendly BOOLEAN NOT NULL +); + +INSERT INTO users VALUES + (1, 'kevin', 35, 'waiter', true), + (2, 'angela', 21, 'concierge', true), + (3, 'alex', 26, 'zoo keeper', false), + (4, 'becky', 67, 'retired', false), + (5, 'kevin', 15, 'in school', true), + (6, 'frankie', 45, 'teller', true); +``` + +我们创建了一个基础的用户表并新增了 6 条新用户数据,对本博文来说已经足够。接下来开始构建我们的 API。 + +## API + +在这篇博文中,所有的代码片段都会包含一些注释,以帮助理解每一步。 + +从 **main.go** 开始: + +```go +package main + +import ( + "fmt" + "log" + "net/http" + + "github.com/bradford-hamilton/go-graphql-api/gql" + "github.com/bradford-hamilton/go-graphql-api/postgres" + "github.com/bradford-hamilton/go-graphql-api/server" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + "github.com/go-chi/render" + "github.com/graphql-go/graphql" +) + +func main() { + // Initialize our API and return a pointer to our router for http.ListenAndServe + // and a pointer to our db to defer its closing when main() is finished + router, db := initializeAPI() + defer db.Close() + + // Listen on port 4000 and if there's an error log it and exit + log.Fatal(http.ListenAndServe(":4000", router)) +} + +func initializeAPI() (*chi.Mux, *postgres.Db) { + // Create a new router + router := chi.NewRouter() + + // Create a new connection to our pg database + db, err := postgres.New( + postgres.ConnString("localhost", 5432, "bradford", "go_graphql_db"), + ) + if err != nil { + log.Fatal(err) + } + + // Create our root query for graphql + rootQuery := gql.NewRoot(db) + // Create a new graphql schema, passing in the the root query + sc, err := graphql.NewSchema( + graphql.SchemaConfig{Query: rootQuery.Query}, + ) + if err != nil { + fmt.Println("Error creating schema: ", err) + } + + // Create a server struct that holds a pointer to our database as well + // as the address of our graphql schema + s := server.Server{ + GqlSchema: &sc, + } + + // Add some middleware to our router + router.Use( + render.SetContentType(render.ContentTypeJSON), // set content-type headers as application/json + middleware.Logger, // log API request calls + middleware.DefaultCompress, // compress results, mostly gzipping assets and json + middleware.StripSlashes, // match paths with a trailing slash, strip it, and continue routing through the mux + middleware.Recoverer, // recover from panics without crashing server + ) + + // Create the graphql route with a Server method to handle it + router.Post("/graphql", s.GraphQL()) + + return router, db +} +``` + +在上面导入的 **gql**、**postgres** 和 **server** 的路径应该是你本地的路径,以及 **postgres.ConnString()** 中连接 PostgreSQL 的用户名也应该是你自己的,和我的不一样。 + +**initializeAPI()** 分为几大块主要的部分,接下来我们逐步构建每一块。 + +使用 **chi.NewRouter()** 创建 router 并返回一个 mux,接下来是创建一个 PostgreSQL 数据库连接。 + +使用 **postgres.ConnString()** 创建一个 **string** 类型的连接配置,并封装到 **postgres.New()** 函数中。这些逻辑在我们自己包中的 **postgres.go** 文件中构建: + +```go +package postgres + +import ( + "database/sql" + "fmt" + + // postgres driver + _ "github.com/lib/pq" +) + +// Db is our database struct used for interacting with the database +type Db struct { + *sql.DB +} + +// New makes a new database using the connection string and +// returns it, otherwise returns the error +func New(connString string) (*Db, error) { + db, err := sql.Open("postgres", connString) + if err != nil { + return nil, err + } + + // Check that our connection is good + err = db.Ping() + if err != nil { + return nil, err + } + + return &Db{db}, nil +} + +// ConnString returns a connection string based on the parameters it's given +// This would normally also contain the password, however we're not using one +func ConnString(host string, port int, user string, dbName string) string { + return fmt.Sprintf( + "host=%s port=%d user=%s dbname=%s sslmode=disable", + host, port, user, dbName, + ) +} + +// User shape +type User struct { + ID int + Name string + Age int + Profession string + Friendly bool +} + +// GetUsersByName is called within our user query for graphql +func (d *Db) GetUsersByName(name string) []User { + // Prepare query, takes a name argument, protects from sql injection + stmt, err := d.Prepare("SELECT * FROM users WHERE name=$1") + if err != nil { + fmt.Println("GetUserByName Preperation Err: ", err) + } + + // Make query with our stmt, passing in name argument + rows, err := stmt.Query(name) + if err != nil { + fmt.Println("GetUserByName Query Err: ", err) + } + + // Create User struct for holding each row's data + var r User + // Create slice of Users for our response + users := []User{} + // Copy the columns from row into the values pointed at by r (User) + for rows.Next() { + err = rows.Scan( + &r.ID, + &r.Name, + &r.Age, + &r.Profession, + &r.Friendly, + ) + if err != nil { + fmt.Println("Error scanning rows: ", err) + } + users = append(users, r) + } + + return users +} +``` + +上面的思想是:创建数据库的连接并返回持有该连接的**Db**对象。然后创建了一个 **db** 的 **GetUserByUsername()** 方法。 + +将关注点重新回到 **main.go** 文件,在 40 行处创建了一个 root query 用于构建 GraphQL 的 schema。我们在 **gql** 包下的 **queries.go** 中创建: + +```go +package gql + +import ( + "github.com/bradford-hamilton/go-graphql-api/postgres" + "github.com/graphql-go/graphql" +) + +// Root holds a pointer to a graphql object +type Root struct { + Query *graphql.Object +} + +// NewRoot returns base query type. This is where we add all the base queries +func NewRoot(db *postgres.Db) *Root { + // Create a resolver holding our databse. Resolver can be found in resolvers.go + resolver := Resolver{db: db} + + // Create a new Root that describes our base query set up. In this + // example we have a user query that takes one argument called name + root := Root{ + Query: graphql.NewObject( + graphql.ObjectConfig{ + Name: "Query", + Fields: graphql.Fields{ + "users": &graphql.Field{ + // Slice of User type which can be found in types.go + Type: graphql.NewList(User), + Args: graphql.FieldConfigArgument{ + "name": &graphql.ArgumentConfig{ + Type: graphql.String, + }, + }, + Resolve: resolver.UserResolver, + }, + }, + }, + ), + } + return &root +} +``` + +在 **NewRoot()** 方法中传入 **db**,并使用该 db 创建一个 **Resolver**。在**Resolver**方法中对数据库进行操作。 + +然后创建了一个 new root 用于用户的查询,需要**name**作为查询参数。类型是 **graphql.NewList** 的 **User**(切片或者数组类型),在 **gql** 包下的 **type.go** 文件中定义。如果有其他类型的查询,就在这个 root 中增加。要把引入的 **postgres** 包改成自己本地的包。 + +接下来看一下 **resolvers.go**: + +```go +package gql + +import ( + "github.com/bradford-hamilton/go-graphql-api/postgres" + "github.com/graphql-go/graphql" +) + +// Resolver struct holds a connection to our database +type Resolver struct { + db *postgres.Db +} + +// UserResolver resolves our user query through a db call to GetUserByName +func (r *Resolver) UserResolver(p graphql.ResolveParams) (interface{}, error) { + // Strip the name from arguments and assert that it's a string + name, ok := p.Args["name"].(string) + if ok { + users := r.db.GetUsersByName(name) + return users, nil + } + + return nil, nil +} +``` + +这里导入的 **postgres** 包同样是你本地的。在这个地方还可以增加其他需要的解析器。 + +接下来看 **types.go**: + +```go +package gql + +import "github.com/graphql-go/graphql" + +// User describes a graphql object containing a User +var User = graphql.NewObject( + graphql.ObjectConfig{ + Name: "User", + Fields: graphql.Fields{ + "id": &graphql.Field{ + Type: graphql.Int, + }, + "name": &graphql.Field{ + Type: graphql.String, + }, + "age": &graphql.Field{ + Type: graphql.Int, + }, + "profession": &graphql.Field{ + Type: graphql.String, + }, + "friendly": &graphql.Field{ + Type: graphql.Boolean, + }, + }, + }, +) +``` + +类似的,在这里添加我们不同的类型,每一个字段都指定了类型。在 **main.go** 文件的 42 行使用 root query 创建了一个新的查询。 + +## 差不多好了 + +在 **main.go** 往下的 51 行处,创建一个新的 server,server 持有 GraphQL schema 的指针。下面是 **server.go** 的内容: + +```go +package server + +import ( + "encoding/json" + "net/http" + + "github.com/bradford-hamilton/go-graphql-api/gql" + "github.com/go-chi/render" + "github.com/graphql-go/graphql" +) + +// Server will hold connection to the db as well as handlers +type Server struct { + GqlSchema *graphql.Schema +} + +type reqBody struct { + Query string `json:"query"` +} + +// GraphQL returns an http.HandlerFunc for our /graphql endpoint +func (s *Server) GraphQL() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Check to ensure query was provided in the request body + if r.Body == nil { + http.Error(w, "Must provide graphql query in request body", 400) + return + } + + var rBody reqBody + // Decode the request body into rBody + err := json.NewDecoder(r.Body).Decode(&rBody) + if err != nil { + http.Error(w, "Error parsing JSON request body", 400) + } + + // Execute graphql query + result := gql.ExecuteQuery(rBody.Query, *s.GqlSchema) + + // render.JSON comes from the chi/render package and handles + // marshalling to json, automatically escaping HTML and setting + // the Content-Type as application/json. + render.JSON(w, r, result) + } +} +``` + +在 server 中有一个 **GraphQL** 的方法,这个方法的主要作用就是处理 **GraphQL** 的查询。记得将 **gql** 的路径更新为你本地的路径。 + +接下来看最后一个文件 **gql.go**: + +```go +package gql + +import ( + "fmt" + + "github.com/graphql-go/graphql" +) + +// ExecuteQuery runs our graphql queries +func ExecuteQuery(query string, schema graphql.Schema) *graphql.Result { + result := graphql.Do(graphql.Params{ + Schema: schema, + RequestString: query, + }) + + // Error check + if len(result.Errors) > 0 { + fmt.Printf("Unexpected errors inside ExecuteQuery: %v", result.Errors) + } + + return result +} +``` + +这里只有一个简单的 **ExecuteQuery()** 函数用来执行 GraphQL 查询。在这里可能会有一个类似于 **ExecuteMutation()** 函数用来处理 GraphQL 的 mutations。 + +在 **initializeAPI()** 的最后,在 router 中增加一些中间工具,以及增加处理 **/graphql** POSTs 请求的 **GraphQL** server 方法。并且在这个地方增加其他 RESTful 请求的路由,并在 server 中增加处理路由的方法。 + +然后在项目的根目录运行 **realize init**,会有两次提示信息并且两次都输入 **n** + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/building-an-api-with-graphql/1.png) + +下面是在你项目的根目录下创建的 **.realize.yaml** 文件: + +```yaml +settings: + legacy: + force: false + interval: 0s +schema: +- name: go-graphql-api + path: . + commands: + run: + status: true + watcher: + extensions: + - go + paths: + - / + ignored_paths: + - .git + - .realize + - vendor +``` + +这段配置对于监控你项目里面的改变非常重要,如果检测到有改变,将自动重启 server 并重新运行 **main.go** 文件。 + +有一些开发 GraphQL API 非常好的工具,比如:**[graphiql](https://github.com/graphql/graphiql)**、**[insomnia](https://insomnia.rest/)**、**[graphql-playground](https://github.com/prisma/graphql-playground)**,还可以发送一个 application/json 请求体的 POST 请求,比如: + +```json +{ + "query": "{users(name:\"kevin\"){id, name, age}}" +} +``` + +在 [Postman](https://www.getpostman.com/) 里像下面这样: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/building-an-api-with-graphql/3.png) + +在查询中可以只请求一个属性或者多个属性的组合。在 GraphQL 的正式版中,可以只请求我们希望通过网络发送的信息。 + +## 很成功 + +大功告成!希望这篇博文对你在 Go 中编写 GraphQL API 有帮助。我尝试将功能分解到不同的包或文件中,使其更容易扩展,而且每一块也很容易测试。 + +--- + +via: https://medium.com/@bradford_hamilton/building-an-api-with-graphql-and-go-9350df5c9356 + +作者:[Bradford Lamson-Scribner](https://medium.com/@bradford_hamilton) +译者:[HelloJavaWorld123](https://github.com/HelloJavaWorld123) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20181122-The-Go-Object-Lifecycle.md b/published/tech/20181122-The-Go-Object-Lifecycle.md index a6afd13fb..d48a2109d 100644 --- a/published/tech/20181122-The-Go-Object-Lifecycle.md +++ b/published/tech/20181122-The-Go-Object-Lifecycle.md @@ -97,7 +97,7 @@ type Client struct { ```go type Client struct { - mu sync.Mutex + mu sync.RWMutex timeout time.Duration // Host and port of remote server. Must be set before Open(). @@ -197,7 +197,7 @@ func OpenClient(opts ...ClientOption) (*Client, error) { c := &Client{} for _, opt := range opts { if err := opt(c); err != nil { - return err + return nil, err } } // open client... diff --git a/published/tech/20181205-How-to-Send-and-Receive-SMS.md b/published/tech/20181205-How-to-Send-and-Receive-SMS.md index aaf519b5c..6753c9571 100644 --- a/published/tech/20181205-How-to-Send-and-Receive-SMS.md +++ b/published/tech/20181205-How-to-Send-and-Receive-SMS.md @@ -453,13 +453,13 @@ func (c *Client) Send(sender, receiver, message string) ([]string, error) { 首先,通过 `go get` 获取 CLI 和 SMSC 模拟器,并且确保 [redis](https://redis.io/) 运行在地址 `localhost:6379` 上 ``` -$ go get GitHub.com/go-gsm/ucp-cli -$ go get GitHub.com/jcaberio/ucp-smsc-sim +$ Go get GitHub.com/go-gsm/ucp-cli +$ Go get GitHub.com/jcaberio/ucp-smsc-sim ``` 导出以下环境变量 -``` +```bash $ export SMSC_HOST=127.0.0.1 $ export SMSC_PORT=16004 $ export SMSC_USER=emi_client diff --git a/published/tech/20181206-On-The-Tension-Between-Generic-Code-And-Special-Cases.md b/published/tech/20181206-On-The-Tension-Between-Generic-Code-And-Special-Cases.md index 0ac5eb7e0..4d157a8ef 100644 --- a/published/tech/20181206-On-The-Tension-Between-Generic-Code-And-Special-Cases.md +++ b/published/tech/20181206-On-The-Tension-Between-Generic-Code-And-Special-Cases.md @@ -2,7 +2,7 @@ # 关于通用代码和特殊情况之间的冲突 -`io.Reader` 和 `io.Writer` 接口几乎出现在所有的 Go 程序中,并代表了处理数据流的基本构建块。Go 的一个重要特性是,对象如套接字、文件或内存缓冲区的抽象都是用这些接口表示的。当 Go 程序对外部世界说话的时候,它几乎是通过 `io.Reader`s 和 `io.Writer` s 来表达,无论它使用的是特殊的平台或通信媒介。这种普遍性是编码处理可组合和可重复使用的数据流代码的关键因素1。 +`io.Reader` 和 `io.Writer` 接口几乎出现在所有的 Go 程序中,并代表了处理数据流的基本构建块。Go 的一个重要特性是,对象如套接字、文件或内存缓冲区的抽象都是用这些接口表示的。当 Go 程序对外部世界说话的时候,它几乎是通过 `io.Reader`s 和 `io.Writer` s 来表达,无论它使用的是特殊的平台或通信媒介。这种普遍性是编码处理可组合和可重复使用的数据流代码的关键因素 1。 这篇文章研究了 `io.Copy` 的设计和实现,该函数用可能是最简单的方法连接一个 `Reader` 到一个 `Writer`:该函数从一个地方传输数据到另一个地方。 @@ -137,7 +137,7 @@ func (cw *CountingWriter) Write(b []byte) (int, error) { via: https://blog.gopheracademy.com/advent-2018/generic-code-vs-special-cases/ -作者:[Andrei Tudor Călin](https://blog.gopheracademy.com/advent-2018/generic-code-vs-special-cases/) +作者:[Andrei Tudor C ă lin](https://blog.gopheracademy.com/advent-2018/generic-code-vs-special-cases/) 译者:[PotoYang](https://github.com/PotoYang) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20181208-Survey-Of-Race-Conditions-And-Detection.md b/published/tech/20181208-Survey-Of-Race-Conditions-And-Detection.md index 3c724ee4e..9265496b7 100644 --- a/published/tech/20181208-Survey-Of-Race-Conditions-And-Detection.md +++ b/published/tech/20181208-Survey-Of-Race-Conditions-And-Detection.md @@ -112,7 +112,7 @@ reqCount.Set(value + 1) 即使以上述例子的方式访问 `reqCount Counter` 是线程安全的,但仍然存在逻辑上的竞态条件问题。我们使用完全同步的线程安全计数器(稍后介绍)执行下面的测试用例([源码](https://github.com/dm03514/grokking-go/pull/3/files#diff-a507be0a589eb624edffd8260bba4bfdR14)),结果仍然不正确: ```bash -$ go test -run TestLogicalRace ./races/ -v -total-requests=200 -concurrent-requests=200 +$ Go test -run TestLogicalRace ./races/ -v -total-requests=200 -concurrent-requests=200 ... handling request: 25 handling request: 25 @@ -210,11 +210,11 @@ Goroutine 426 (running) created at: ```bash WARNING: DATA RACE -Write at 0x00c4200164e8 by goroutine 326: +Write at 0x00c4200164e8 by Goroutine 326: GitHub.com/dm03514/grokking-go/candidates-and-contexts/races.TestExplicitRace.func1.1() /vagrant_data/go/src/github.com/dm03514/grokking-go/candidates-and-contexts/races/counters.go:18 +0x115 ... -previous read at 0x00c4200164e8 by goroutine 426: +previous read at 0x00c4200164e8 by Goroutine 426: GitHub.com/dm03514/grokking-go/candidates-and-contexts/races.TestExplicitRace.func1.1() /vagrant_data/go/src/github.com/dm03514/grokking-go/candidates-and-contexts/races/counters.go:14 +0x5b ``` @@ -328,7 +328,7 @@ go func() { 这非常棒,因为这是一种混合方法: 它运行调度并发的操作,但是并发操作是唯一需要访问 `reqCount` 的东西。这就意味着 `reqCount` 不再需要同步。(除了在 `countChan` 关闭后主测试线程因为断言需要访问它之外。正如我们看到的那样,程序的行为与预期的一致,并没有产生任何的竞态条件 ([源码](https://github.com/dm03514/grokking-go/pull/3/files#diff-8222898e088ed9fee100b22308c43685R14)))。 ```bash -$ go test -run TestDesignNoRace ./races/ -v -total-requests=200 -concurrent-requests=200 -race +$ Go test -run TestDesignNoRace ./races/ -v -total-requests=200 -concurrent-requests=200 -race handling request: 1 ... handling request: 190 diff --git a/published/tech/20181215-Debugging-with-Mozilla-rr-project-and-GoLand.md b/published/tech/20181215-Debugging-with-Mozilla-rr-project-and-GoLand.md index 7630047e3..37e1c9c8d 100644 --- a/published/tech/20181215-Debugging-with-Mozilla-rr-project-and-GoLand.md +++ b/published/tech/20181215-Debugging-with-Mozilla-rr-project-and-GoLand.md @@ -66,7 +66,7 @@ echo 0 | sudo tee -a /proc/sys/kernel/kptr_restrict via: https://blog.gopheracademy.com/advent-2018/mozilla-rr-with-goland/ -作者:[FlorinPăţan](https://twitter.com/dlsniper) +作者:[FlorinP ăţ an](https://twitter.com/dlsniper) 译者:[wumansgy](https://github.com/wumansgy) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20181218-Migrating-to-go-mod-in-just-3-steps.md b/published/tech/20181218-Migrating-to-go-mod-in-just-3-steps.md index 2cb5198c3..18f0ca0a7 100644 --- a/published/tech/20181218-Migrating-to-go-mod-in-just-3-steps.md +++ b/published/tech/20181218-Migrating-to-go-mod-in-just-3-steps.md @@ -77,7 +77,7 @@ go mod init mollydb go mod tidy ``` -go.mod 文件就会生成,因为 go mod 检查了我们的 Go 文件 +go.mod 文件就会生成,因为 Go mod 检查了我们的 Go 文件 ``` module mollydb @@ -98,7 +98,7 @@ require ( 我们只需要运行下面的命令来验证项目是否像以前那样工作。 -> go run main.go +> Go run main.go ## 所有的都成功了 @@ -110,7 +110,7 @@ require ( via: https://medium.com/@ivan.corrales.solera/migrating-to-go-mod-in-just-3-steps-6b6a07a04640 -作者:[Iván Corrales Solera](https://medium.com/@ivan.corrales.solera) +作者:[Iv á n Corrales Solera](https://medium.com/@ivan.corrales.solera) 译者:[wumansgy](https://github.com/wumansgy) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/20181219-Goroutine-Leaks-The-Abandoned-Receivers.md b/published/tech/20181219-Goroutine-Leaks-The-Abandoned-Receivers.md index 9def9359d..df7cbed1d 100644 --- a/published/tech/20181219-Goroutine-Leaks-The-Abandoned-Receivers.md +++ b/published/tech/20181219-Goroutine-Leaks-The-Abandoned-Receivers.md @@ -41,7 +41,7 @@ Goroutine 内存泄漏是产生 Go 程序内存泄漏的常见原因。在我之 56 output := make(chan string, total) 57 workers := runtime.NumCPU() 58 for i := 0; i < workers; i++ { -59 go worker(i, input, output) +59 Go worker(i, input, output) 60 } 61 62 // Receive from output the expected number of times. If 10 @@ -57,7 +57,7 @@ Goroutine 内存泄漏是产生 Go 程序内存泄漏的常见原因。在我之 72 // This is a blog post so all the workers do is capitalize a 73 // string but imagine they are doing something important. 74 // -75 // Each goroutine can't know how many records it will get so +75 // Each Goroutine can't know how many records it will get so 76 // it must use the range keyword to receive in a loop. 77 func worker(id int, input <-chan string, output chan<- string) { 78 for v := range input { @@ -101,7 +101,7 @@ Goroutine 内存泄漏是产生 Go 程序内存泄漏的常见原因。在我之 ## 结论 -正如前一篇文章中所提到的,Go 使得启动 Goroutines 变得简单,但是你有责任仔细使用它们。在这篇文章中,我展示了另一个很容易出现的 Goroutine 错误。还有很多方法可以创建 Goroutine 内存泄漏以及使用并发时可能遇到的其他陷阱。未来的帖子将继续讨论这些问题。与往常一样,我将继续重复这一建议:“如果不知道它会如何停止,就不要开始使用 goroutine ”。 +正如前一篇文章中所提到的,Go 使得启动 Goroutines 变得简单,但是你有责任仔细使用它们。在这篇文章中,我展示了另一个很容易出现的 Goroutine 错误。还有很多方法可以创建 Goroutine 内存泄漏以及使用并发时可能遇到的其他陷阱。未来的帖子将继续讨论这些问题。与往常一样,我将继续重复这一建议:“如果不知道它会如何停止,就不要开始使用 Goroutine ”。 **_并发是一种有用的工具,但必须谨慎使用。_** diff --git a/published/tech/20181227-Yet-another-tool-to-mock-interfaces-in-Go.md b/published/tech/20181227-Yet-another-tool-to-mock-interfaces-in-Go.md index 74f4a807b..bbaad07c0 100644 --- a/published/tech/20181227-Yet-another-tool-to-mock-interfaces-in-Go.md +++ b/published/tech/20181227-Yet-another-tool-to-mock-interfaces-in-Go.md @@ -155,7 +155,7 @@ func (f doerFunc) Do() (int, error) { } ``` -也可以和一个便捷的 [vim 插件](https://github.com/romanyx/vim-go-adapt) 配合使用,可以再 vim 中直接调用该工具。 +也可以和一个便捷的 [vim 插件](https://github.com/romanyx/vim-go-adapt) 配合使用,可以再 VIM 中直接调用该工具。 ![use in vim](https://raw.githubusercontent.com/studygolang/gctt-images/master/mock-interface/1_PCMcTGnUNvjP0hooLXYBOw.gif) diff --git a/published/tech/20181229-Stop-Writing-Broken-Go-libraries.md b/published/tech/20181229-Stop-Writing-Broken-Go-libraries.md index 419dfdcb9..368c31f71 100644 --- a/published/tech/20181229-Stop-Writing-Broken-Go-libraries.md +++ b/published/tech/20181229-Stop-Writing-Broken-Go-libraries.md @@ -6,11 +6,11 @@ 上面说的每一个库都存在一些基本问题以至于它们在真实场景中不可用。并且每个库都以这样一种方式编写:不以非向后兼容的方式修改现有库的 API,这样是不可能修复问题的。不幸的是,由于很多其他的库也存在同样的问题,所以我会在下面列出一些作者错误的地方。 -# 不要对 `HTTP` 客户端硬编码 +## 不要对 `HTTP` 客户端硬编码 很对库都包含了对 `http.DefaultClient` 的硬编码。虽然对库本身来说这并不是问题,但是库的作者并未理解应该怎样使用 `http.DefaultClient` 。正如 `default client` 建议它只在用户没有提供其他 `http.Client` 时才被使用。相反的是,许多库作者乐意在他们代码中涉及 `http.DefaultClient` 的部分采用硬编码,而不是将它作为一个备选。这会导致在某些情况下这个库不可用。 -首先,我们很多人都读过这篇讲述 `http.DefaultClient` 不能自定义超时时间的文章《[Don’t use Go’s default HTTP client (in production)](https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779)》,当你没法保证你的`HTTP` 请求一定会完成(或者至少要等一个完全无法预估时间的响应)时,你的程序可能会遇到奇怪的 goroutine 泄漏和一些无法预知的行为。在我看来,这会使每一个对 `http.DefaultClient` 采用硬编码的库不可用。 +首先,我们很多人都读过这篇讲述 `http.DefaultClient` 不能自定义超时时间的文章《[Don’t use Go’s default HTTP client (in production)](https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779)》,当你没法保证你的 `HTTP` 请求一定会完成(或者至少要等一个完全无法预估时间的响应)时,你的程序可能会遇到奇怪的 Goroutine 泄漏和一些无法预知的行为。在我看来,这会使每一个对 `http.DefaultClient` 采用硬编码的库不可用。 其次,网络需要一些额外的配置。有时候需要用到代理,有时候需要对 `URL` 进行一丢丢的改写,甚至可能 `http.Transport` 需要被一个定制的接口替换。当一个程序员在你的库里用他们自己的 `http.Client` 实例时,以上这些都很容易被实现。 @@ -42,7 +42,7 @@ func (l *Library) getClient() *http.Client { 另外,如果一些全局的特性对于每个请求来讲都是必须的,人们经常感觉到需要用他们自己的实例来替换 `http.Client`。这是一个错误的方法 — 如果你需要在你的请求中设置一些额外的 `headers`,或者在你的客户端引入某类公共的特性,你只需要简单为每个请求进行设置或者用组装定制客户端的方式来代替完全替换它。 -# 不要引入全局变量 +## 不要引入全局变量 另一个反面模式是允许用户在一个库中设置全局变量。举个例子,在你的库中允许用户设置一个全局的 `http.Client` 并被所有的 `HTTP` 调用执行: @@ -56,7 +56,7 @@ func SetHttpClient(client *http.Client) { 通常在一个库中不应该存在一堆全局变量。当你写代码的时候,你应该想想用户在他们的程序中多次使用你的这个库会发生什么。全局变量会使不同的参数没有办法被使用。而且,在你的代码中引入全局变量会引起测试上的问题并造成代码上不必要的复杂度。使用全局变量可能会导致在你程序的不同模块有不必要的依赖。在写你的库的时候,避免全局状态是格外重要的。 -# 返回 structs,而不是 interfaces +## 返回 structs,而不是 interfaces 这是一个普遍的问题(实际上我在这一点上也犯过错)。很多库都有下面这类函数: @@ -78,9 +78,9 @@ func New() *LibraryStruct { 详情请参见 Go [http.Client](https://golang.org/pkg/net/http/#Client)。 -# 使用配置结构体来避免修改你的APIs +## 使用配置结构体来避免修改你的 APIs -另一种配置方法是在你的工厂函数中接收一个配置结构体,而不是直接传配置参数。你可以很随意的添加新的参数而不用破坏现有的 API。你只需要做一件事情,在Config结构体中添加一个新的字段,并且确保不会影响它原本的特性。 +另一种配置方法是在你的工厂函数中接收一个配置结构体,而不是直接传配置参数。你可以很随意的添加新的参数而不用破坏现有的 API。你只需要做一件事情,在 Config 结构体中添加一个新的字段,并且确保不会影响它原本的特性。 ```go func New(config Config) *LibraryStruct { @@ -92,7 +92,7 @@ func New(config Config) *LibraryStruct { 你能在 [golang.org/x/crypto](https://godoc.org/golang.org/x/crypto/openpgp#Sign) 包里看到对上面的补充。总之,对配置来说,我认为允许用户在返回的结构体里设置不同的参数是一个更好的方法,并且只在编写复杂方法时才使用这种特定方法。 -# 总结 +## 总结 根据经验来讲,在写一个库的时候,你应该总是允许用户指定他们自己的 `http.Client` 来执行 `HTTP` 调用。而且考虑到未来迭代修改带来的影响,你可以尝试用可扩展的方式编写代码。避免全局变量,库不能存储全局状态。如果你有任何疑问-参考标准库是怎么写的。 diff --git a/published/tech/20181230-building-immutable-data-structures-in-go.md b/published/tech/20181230-building-immutable-data-structures-in-go.md index 10af35603..04b37f75d 100644 --- a/published/tech/20181230-building-immutable-data-structures-in-go.md +++ b/published/tech/20181230-building-immutable-data-structures-in-go.md @@ -1,6 +1,6 @@ 首发于:https://studygolang.com/articles/23435 -# 用Go构建不可变的数据结构 +# 用 Go 构建不可变的数据结构 ![Photo by Frantzou Fleurine on Unsplash](https://raw.githubusercontent.com/studygolang/gctt-images/master/building-immutable-data-structures-in-go/cover.jpeg) @@ -17,13 +17,13 @@ type Person struct { } ``` -显然,我们可以实例化一个`Person`然后随心所欲地更改它的属性。事实上,这样做并没有任何错。但是,当你处理更加复杂的、传递引用和切片的嵌套式数据结构,或者利用通道传递副本时,以某些姿势更改这些共享的数据副本可能会导致不易察觉的 bugs。 +显然,我们可以实例化一个 `Person` 然后随心所欲地更改它的属性。事实上,这样做并没有任何错。但是,当你处理更加复杂的、传递引用和切片的嵌套式数据结构,或者利用通道传递副本时,以某些姿势更改这些共享的数据副本可能会导致不易察觉的 bugs。 ## 为啥我之前就没有遇到过这种问题呢? 如果没有重度使用 channel 或代码基本是串行执行的,由于从定义上讲每次只有一个操作能够作用在数据上,你不大可能会遇见这些不明显的 bugs。 -再者,除了避免 bugs外,不可变数据结构还有其他优势: +再者,除了避免 bugs 外,不可变数据结构还有其他优势: 1. 由于状态绝不会原地更新,这对一般的调试和记录每个变换步骤以用于后续监控是非常有用的 2. 撤销或“时光倒流”的能力不仅是可能的,而且是小菜一碟,只需一个赋值操作即可 @@ -138,7 +138,7 @@ func (p Person) WithFavoriteColorAt(i int, favoriteColor string) Person { } ``` -> 译者注:上述代码是错误的,如果`p.favoriteColors`的容量大于`i`则会就地改变副本的`favoriteColors`,参见[反例](https://gist.github.com/sammyne/e845c24ad89ef04fd2207cdc6196e29f),稍作调整即可得到[正确实现](https://gist.github.com/sammyne/d77be41112df33f53ccc40a20e5a605a#file-20181230-building-immutable-data-structures-in-go-ok-example-go-L11) +> 译者注:上述代码是错误的,如果 `p.favoriteColors` 的容量大于 `i` 则会就地改变副本的 `favoriteColors`,参见[反例](https://gist.github.com/sammyne/e845c24ad89ef04fd2207cdc6196e29f),稍作调整即可得到[正确实现](https://gist.github.com/sammyne/d77be41112df33f53ccc40a20e5a605a#file-20181230-building-immutable-data-structures-in-go-ok-example-go-L11) 现在我们就可以放心使用: @@ -148,7 +148,7 @@ func updateFavoriteColors(p Person) Person { } ``` -想要了解更多切片的妙用参见这篇牛逼的wiki:https://github.com/golang/go/wiki/SliceTricks +想要了解更多切片的妙用参见这篇牛逼的 wiki:https://github.com/golang/go/wiki/SliceTricks ## 构造函数 diff --git a/published/tech/2019-08-26-how-i-organize-packages-in-go.md b/published/tech/2019-08-26-how-i-organize-packages-in-go.md new file mode 100644 index 000000000..3bab149ab --- /dev/null +++ b/published/tech/2019-08-26-how-i-organize-packages-in-go.md @@ -0,0 +1,76 @@ +首发于:https://studygolang.com/articles/25301 + +# 在 Go 中我是如何组织包的 + +构建项目跟写代码一样具有挑战性。而且有很多种方法。使用错误的方法可能会让人很痛苦,但若要重构则又会非常耗时。另外,要想在一开始就设计出完美的程序几乎是不可能的。更重要的是,有些解决方法只适用于某特定大小的程序,但是程序的大小又是随着时间变化和增长的。所以我们的软件应该跟着出现过解决过的问题一起成长。 + +我主要从事微服务的开发,这种架构非常适合我。其他领域或其他基础架构的项目可能需要不同的方法。请在下面的评论中告诉我您的设计和最有意义的地方。 + +## 包及其依赖 + +在开发微服务时,按组件拆分服务很有用。每个组件都应该是独立的,理论上,如果需要,可以将其提取到外部服务。如何理解和实现呢? + +假设我们有一个服务,它处理与订单相关的所有事情,比如发送电子邮件的确认、将信息保存到数据库、连接到支付提供商等。每个包都应该有一个名称,该名称清楚地说明了它的用途,并且遵守命名标准。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/how-i-organize-packages-in-go/organize-go.png) + +这只是我们有 3 个包的项目的一个例子:**confemails**,**payproviders**和**warehouse**。包名应尽量简短并能让人一目了然。 + +每个包都有自己的 Setup()函数。该函数只接收能让该包运行的最基本的参数。例如,如果包对外提供 HTTP 服务,那么 Setup() 函数则仅需要接受一个类似 mux route 的 HTTP route。当包需要访问数据库时,Setup() 函数也是只接受 sql.DB 参数就可以了。当然,这个包也可能需要依赖另一个包。 + +## 包内的组成 + +知道了模块的外部依赖,下一步我们就可以专注于如何在模块内组织代码(包括相关依赖的处理)。在最开始,这个包包含以下文件: setup.go - 其中包含 Setup()函数, service.go - 它是逻辑文件, repository.go - 它是在读取/保存数据到数据的的文件。 + +Setup()函数负责构建模块的每个构建块,即服务、存储库、注册事件处理程序或 HTTP 处理程序等等。这是使用这种方法的实际生产代码的一个例子。 + +```go +func Setup(router *mux.Router, httpClient httpGetter, auth jwtmiddleware.Authorization, logger logger) { + h := httpHandler{ + logger: logger, + requestClaims: jwtutil.NewHTTPRequestClaims(client), + service: service{client: httpClient}, + } + auth.CreateRoute("/v1/lastAnswerTime", h.proxyRequest, http.MethodGet) +} +``` + +以上代码中,它构建了 JWT 中间件,这是一个处理所有业务逻辑(以及日志的位置)并注册 HTTP 处理程序的服务。正因为如此,模块是非常独立的,并且(理论上)可以转移到单独的微服务中,而不需要做太多工作。最后,所有的包都在 main 函数中配置。 + +有时,我们需要一些处理程序或数据库驱动。例如,一些信息可以被存储在数据库中,然后通过事件发送到平台的不同部分。使用像 saveToDb()这样的方法将数据只保存在同一个库中是很不方便的。所有类似的元素都应该由以下功能分割:repository_order.go 或 service_user.go。如果对象的类型超过 3 种,则将其移动到单独的子文件夹中。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/how-i-organize-packages-in-go/organizing-go-1.png) + +## 测试 + +说到测试,我坚持一些原则。首先,在 Setup()函数中使用接口。这些接口应该尽可能小。在上面的例子中,有一个 httpGetter 接口。接口中只有**Get**()函数。 + +```go +type httpGetter interface { + Get(url string) (resp *http.Response, err error) +} +``` + +谢天谢地,我只需要模拟一个方法。接口的定义需要尽可能地接近它的用途。 + +其次,尝试编写更少的测试用例的同时可以覆盖到更多的代码。对于每个主函数的决策/操作,一个成功的测试用例和一个失败的测试用例应该足够覆盖大约 80% 的代码。有时,程序中有一些关键部分,这部分可以被单独的测试用例覆盖。 + +最后,在以 `_test` 为后缀的单独包中编写测试,并将其放入模块中。把所有的东西都放在一个地方是很有用的。 + +当您想要测试整个应用程序时,请在主函数旁边的**setup**()函数中准备好每个依赖项。它将为生产环境和测试环境提供相同的设置,可以为您避免一些 bug。测试应该重用 setup()函数,并且只模拟那些不易模拟的依赖项(比如外部 api)。 + +## 总结 + +所有其他文件(比如 `.travis.yaml` 等)都保存在项目根目录中。这让我对整个项目有了一个清晰的认识。让我知道在哪里可以找到主文件,在哪里可以找到与基础结构相关的文件,并且没有混合在一起。否则,项目的主文件夹就会变得一团糟。 + +正如我在介绍中所说,我知道并非所有项目都能从中受益,但是像 microservices 这样的小型程序会发现它非常有用。 + +--- + +via: https://developer20.com/how-i-organize-packages-in-go/ + +作者:[Bartłomiej Klimczak](https://developer20.com/about-me/index.html) +译者:[shadowstorm97](https://github.com/shadowstorm97) +校对:[DingdingZhou](https://github.com/DingdingZhou) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190102-Database-migrations-in-Golang.md b/published/tech/20190102-Database-migrations-in-Golang.md index 1d61d81cb..dacc14d4d 100644 --- a/published/tech/20190102-Database-migrations-in-Golang.md +++ b/published/tech/20190102-Database-migrations-in-Golang.md @@ -74,7 +74,7 @@ func main() { 以上是在 Go 中进行数据库迁移的最简单方法。你可以继续从[这个 repo](https://github.com/adelowo/migration-demo) 下载以下文件,并将它们放在 migrations 目录中或你认为合适的任何位置。之后,你需要使用以下命令运行它 : ``` -$ go run main.go -mysql.dsn "root:@tcp(localhost)/xyz" +$ Go run main.go -mysql.dsn "root:@tcp(localhost)/xyz" ``` 如果一切顺利,你应该看到在标准输出上打印了 "Database migrated" ( 数据库迁移完成 )。 @@ -92,9 +92,9 @@ ADD . /go/src/github.com/adelowo/project ENV GO111MODULE=on -RUN go mod download -RUN go mod verify -RUN go install ./cmd +RUN Go mod download +RUN Go mod verify +RUN Go install ./cmd ## A better scratch FROM gcr.io/distroless/base diff --git a/published/tech/20190104-Why-You-Need-to-Learn-More-Programming-Languages.md b/published/tech/20190104-Why-You-Need-to-Learn-More-Programming-Languages.md index 0303f3d32..71d205e34 100644 --- a/published/tech/20190104-Why-You-Need-to-Learn-More-Programming-Languages.md +++ b/published/tech/20190104-Why-You-Need-to-Learn-More-Programming-Languages.md @@ -78,8 +78,8 @@ func main() { ## 最后的思考 -1. 你是一个Javascript或python开发人员。我强烈建议学习低级语言。你可以直接学习 C 或 C ++,但我会建议 Golang。您可以轻松获得类似 C++ 的速度,而不会受到 C 系列的挫折。 -2. 对于所有低级语言开发人员,请尝试使用 python 或 Javascript。如果您还没有尝试过这些语言,那么您就错过了。 Python 就像伪代码,现在 Javascript 无处不在。这两种语言都可以让您使用低级语言。您可以为 Node.js 和 Python 编写C ++模块。相信我,它会改变你的生活。 +1. 你是一个 Javascript 或 python 开发人员。我强烈建议学习低级语言。你可以直接学习 C 或 C ++,但我会建议 Golang。您可以轻松获得类似 C++ 的速度,而不会受到 C 系列的挫折。 +2. 对于所有低级语言开发人员,请尝试使用 python 或 Javascript。如果您还没有尝试过这些语言,那么您就错过了。 Python 就像伪代码,现在 Javascript 无处不在。这两种语言都可以让您使用低级语言。您可以为 Node.js 和 Python 编写 C ++ 模块。相信我,它会改变你的生活。 我希望我已经说服你与你的主要语言建立开放的关系,并获得一些新的令人兴奋的经历。 如果你知道两种截然不同的语言,到目前为止你的经验是什么?您认为它对您的职业生涯有何帮助?请在评论中告诉我。 diff --git a/published/tech/20190106-Go-GraphQL-Beginners-Tutorial-Part1.md b/published/tech/20190106-Go-GraphQL-Beginners-Tutorial-Part1.md index 0787fa421..0c3c1d578 100644 --- a/published/tech/20190106-Go-GraphQL-Beginners-Tutorial-Part1.md +++ b/published/tech/20190106-Go-GraphQL-Beginners-Tutorial-Part1.md @@ -135,7 +135,7 @@ func main() { 现在,如果我们尝试运行它,看看会发生什么? ```bash -$ go run ./... +$ Go run ./... {"data":{"hello":"world"}} ``` @@ -350,7 +350,7 @@ query := ` 当我们运行这个查询后,我们将会看到如下输出: ```bash -$ go run ./... +$ Go run ./... {"data":{"list":[{"author":{"Name":"Elliot Forbes","Tutorials":[1]},"comments":[{"body":"First Comment"}],"id":1,"title":"Go GraphQL Tutorial"}]}} ``` @@ -375,7 +375,7 @@ query := ` 当我们再一次运行它,我们会看到它成功地检索到了内存中唯一一个 `ID=1` 的教程。 ```bash -$ go run ./... +$ Go run ./... {"data":{"tutorial":{"author":{"Name":"Elliot Forbes","Tutorials":[1]},"title":"Go GraphQL Tutorial"}}} ``` diff --git a/published/tech/20190108-Avoid package names like base, util, or common.md b/published/tech/20190108-Avoid package names like base, util, or common.md index 69c7f4e1b..f47b1cb98 100644 --- a/published/tech/20190108-Avoid package names like base, util, or common.md +++ b/published/tech/20190108-Avoid package names like base, util, or common.md @@ -30,12 +30,12 @@ 4. [Why I think Go package management is important](https://dave.cheney.net/2013/10/10/why-i-think-go-package-management-is-important) - ​ 2019-01-08 +--- via:https://dave.cheney.net/2019/01/08/avoid-package-names-like-base-util-or-common -作者:[*[Dave Cheney](https://dave.cheney.net/)*](https://dave.cheney.net/) -译者:[Alihanniba](https://github.com/Alihanniba) / 柒呀 +作者:[Dave Cheney](https://dave.cheney.net/) +译者:[Alihanniba](https://github.com/Alihanniba) 校对:[zhoudingding](https://github.com/dingdingzhou) 本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190110-go-pointers-why-i-use-interface.md b/published/tech/20190110-go-pointers-why-i-use-interface.md new file mode 100644 index 000000000..4cc861865 --- /dev/null +++ b/published/tech/20190110-go-pointers-why-i-use-interface.md @@ -0,0 +1,213 @@ +首发于:https://studygolang.com/articles/27152 + +# 在 Go 语言中,我为什么使用接口 + +强调一下是**我个人**的见解以及接口在 **Go 语言**中的意义。 + +如果您写代码已经有了一段时间,我可能不需要过多解释接口所带来的好处,但是在深入探讨 Go 语言中的接口前,我想花一两分钟先来简单介绍一下接口。 +如果您对接口很熟悉,请先跳过下面这段。 + +## 接口的简单介绍 + +在任一编程语言中,接口——方法或行为的集合,在功能和该功能的使用者之间构建了一层薄薄的抽象层。在使用接口时,并不需要了解底层函数是如何实现的,因为接口隔离了各个部分(划重点)。 + +跟不使用接口相比,使用接口的最大好处就是可以使代码变得简洁。例如,您可以创建多个组件,通过接口让它们以统一的方式交互,尽管这些组件的底层实现差异很大。这样就可以在编译甚至运行的时候动态替换这些组件。 + +用 Go 的 `io.Reader` 接口举个例子。`io.Reader` 接口的所有实现都有 `Read(p []byte) (n int, err error)` 函数。使用 `io.Reader` 接口的使用者不需要知道使用这个 `Read` 函数的时候那些字节从何而来。 + +## 具体到 Go 语言 + +在我使用 Go 语言的过程中,与我使用过的其他任何编程语言相比,我经常发现其他的、不那么明显的使用接口的原因。今天,我将介绍一个很普遍的,也是我遇到了很多次的使用接口的原因。 + +## Go 语言没有构造函数 + +很多编程语言都有构造函数。构造函数是定义自定义类型(即 OO 语言中的类)时使用的一种建立对象的方法,它可以确保必须执行的任何初始化逻辑均已执行。 + +例如,假设所有 `widgets` 都必须有一个不变的,系统分配的标识符。在 Java 中,这很容易实现: + +```java +package io.krancour.widget; + +import java.util.UUID; + +public class Widget { + + private String id; + + // 使用构造函数初始化 + public Widget() { + id = UUID.randomUUID().toString(); + } + + public String getId() { + return id; + } +} +``` + +```java +class App { + public static void main( String[] args ){ + Widget w = new Widget(); + System.out.println(w.getId()); + } +} +``` + +从上面这个例子可以看到,没有执行初始化逻辑就无法实例化一个新的 `Widget` 。 + +但是 Go 语言没有此功能。 :( + +在 Go 语言中,可以直接实例化一个自定义类型。 + +定义一个 `Widget` 类型: + +```go +package widgets + +type Widget struct { + id string +} + +func (w Widget) ID() string { + return w.id +} +``` + +可以像这样实例化和使用一个 `widget`: + +```go +package main + +import ( + "fmt" + "github.com/krancour/widgets" +) + +func main() { + w := widgets.Widget{} + fmt.Println(w.ID()) +} +``` + +如果运行此示例,那么(也许)意料之中的结果是,打印出的 ID 是空字符串,因为它从未被初始化,而空字符串是字符串的“零值”。 +我们可以在 `widgets` 包中添加一个类似于构造函数的函数来处理初始化: + +```go +package widgets + +import uuid "github.com/satori/go.uuid" + +type Widget struct { + id string +} + +func NewWidget() Widget { + return Widget{ + id: uuid.NewV4().String(), + } +} + +func (w Widget) ID() string { + return w.id +} +``` + +然后我们简单地修改 `main` 来使用这个类似于构造函数的新函数: + +```go +package main + +import ( + "fmt" + "github.com/krancour/widgets" +) + +func main() { + w := widgets.NewWidget() + fmt.Println(w.ID()) +} +``` + +执行该程序,我们得到了想要的结果。 + +但是仍然存在一个严重问题!我们的 `widgets` 包没有强制用户在初始一个 `widget` 的时候使用我们的构造函数。 + +## 变量私有化 + +首先我们尝试把自定义类型的变量私有化,以此来强制用户使用我们规定的构造函数来初始化 `widget`。在 Go 语言中,类型名、函数名的首字母是否大写决定它们是否可被其他包访问。名称首字母大写的可被访问(也就是 `public` ),而名称首字母小写的不可被访问(也就是 `private` )。所以我们把类型 `Widget` 改为类型 `widget` : + +```go +package widgets + +import uuid "github.com/satori/go.uuid" + +type widget struct { + id string +} + +func NewWidget() widget { + return widget{ + id: uuid.NewV4().String(), + } +} + +func (w widget) ID() string { + return w.id +} +``` + +我们的 `main` 代码保持不变,这次我们得到了一个 ID 。这比我们想要的要近了一步,但是我们在此过程中犯了一个不太明显的错误。类似于构造函数的 `NewWidget` 函数返回了一个私有的实例。尽管编译器对此不会报错,但这是一种不好的做法,下面是原因解释。 + +在 Go 语言中,***包***是复用的基本单位。其他语言中的***类***是复用的基本单位。如前所述,任何无法被外部访问的内容实质上都是“包私有”,是该包的内部实现细节,对于使用这个包的使用者来说不重要。因此,Go 的文档生成工具 `godoc` 不会为私有的函数、类型等生成文档。 + +当一个公开的构造函数返回一个私有的 `widget` 实例,实际上就陷入了一条死胡同。调用这个函数的人哪怕有这个实例,也绝对在文档里找不到任何关于这个实例类型的描述,也更不知道 `ID()` 这个函数。Go 社区非常重视文档,所以这样做是不会被接受的。 + +## 轮到接口上场了 + +回顾一下,到目前为止,我们写了一个类似于构造函数的函数来解决 Go 语言缺乏构造函数的问题,但是为了确保人们用该函数而不是直接实例化 `Widget` ,我们更改了该类型的可见性——将其重命名为 `widget`,即私有化了。虽然编译器不会报错,但是文档中不会出现对这个私有类型的描述。不过,我们距离想要的目标还近了一步。接下来就要使用接口来完成后续的了。 + +通过创建一个***可被访问的***、`widget` 类型可以实现的接口,我们的构造函数可以返回一个公开的类型实例,并且会显示在 `godoc` 文档中。同时,这个接口的底层实现依然是私有的,使用者无法直接创建一个实例。 + +```go +package widgets + +import uuid "github.com/satori/go.uuid" + +// Widget is a ... +type Widget interface { + // ID 返回这个 widget 的唯一标识符 + ID() string +} + +type widget struct { + id string +} + +// NewWidget() 返回一个新的 Widget 实例 +func NewWidget() Widget { + return widget{ + id: uuid.NewV4().String(), + } +} + +func (w widget) ID() string { + return w.id +} +``` + +## 总结 + +我希望我已经充分地阐述了 Go 语言的这一特质——构造函数的缺失反而促进了接口的使用。 + +在我的下一篇文章中,我将介绍一种几乎与之相反的场景——在其他语言中要使用接口但是在 Go 语言中却不必。 + +--- + +via: https://medium.com/@kent.rancourt/go-pointers-why-i-use-interfaces-in-go-338ae0bdc9e4 + +作者:[Kent Rancourt](https://medium.com/@kent.rancourt) +译者:[zhiyu-tracy-yang](https://github.com/zhiyu-tracy-yang) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190111-Go-GraphQL-Beginners-Tutorial-Part2.md b/published/tech/20190111-Go-GraphQL-Beginners-Tutorial-Part2.md index aee1cb4b3..d105b1362 100644 --- a/published/tech/20190111-Go-GraphQL-Beginners-Tutorial-Part2.md +++ b/published/tech/20190111-Go-GraphQL-Beginners-Tutorial-Part2.md @@ -114,7 +114,7 @@ ___完整源代码___: 本小节所有的源代码都可以在[simple-mutation.g 当我们尝试运行这些代码,我们会看到 _变更_ 已经被成功地调用,并且,返回的教程列表中已经包含了我们最新定义的教程信息。 ```bash -$ go run ./... +$ Go run ./... {"data":{"create":{"title":"Hello World"}}} {"data":{"list":[{"id":1,"title":"Go GraphQL Tutorial"},{"id":2,"title":"Go GraphQL Tutorial - Part 2"},{"id":0,"title":"Hello World"}]}} ``` @@ -230,7 +230,7 @@ fmt.Printf("%s \n", rJSON) 运行该代码,我们可以看到从 SQLite3 数据库返回的三行数据,同时我们也可以看到从 GraphQL 查询中返回的 JSON 响应体。同样地,如果我们只想返回教程的 `id`,我们可以修改 `query` 删除 `title` 字段,一切也会如预期一样地工作。 ```bash -$ go run ./... +$ Go run ./... 2018/12/30 14:44:08 {1 First Tutorial { [] } []} 2018/12/30 14:44:08 {2 Second Tutorial { [] } []} 2018/12/30 14:44:08 {3 third Tutorial { [] } []} @@ -287,7 +287,7 @@ query := ` 运行代码,我们可以观察到该解析器函数已成功连接至 SQLite 数据库,并可以查询到相关的教程信息: ```bash -$ go run ./... +$ Go run ./... {"data":{"tutorial":{"id":1,"title":"First Tutorial"}}} ``` diff --git a/published/tech/20190111-Go-advanced-benchmarking.md b/published/tech/20190111-Go-advanced-benchmarking.md new file mode 100644 index 000000000..75bca6626 --- /dev/null +++ b/published/tech/20190111-Go-advanced-benchmarking.md @@ -0,0 +1,273 @@ +首发于:https://studygolang.com/articles/28461 + +# Go 高级基准测试 + +## 背景 + +有时你必须解决不同类型的问题。通常来说复杂的问题并不会只有单一的解决方案,但是解决方案的优劣取决于程序在运行时所要解决问题的子集。 + +我所遇到的一个例子是分析一些代理的连接中的某些数据流。 + +从流量中提取信息的方法主要有两种:保存整个数据流,当流量结束后立即分析;或者(使用一个缓存窗口)以降低速度为代价,在数据流传输过程中进行分析。 + +内存相对与处理能力来说要更加便宜,所以我的第一版解决方案是使用缓存的方案。 + +### 第一版代码:使用缓存(buffer) + +缓存连接是相对容易的:只需要将读到的所有数据复制到一个 `bytes.Buffer` 里,然后当连接关闭后对读到的数据进行分析。最简单的方式是包装( wrap )连接,在调用 `Read` 前先经过一个 [`io.TeeReader`](https://golang.org/pkg/io/#TeeReader)。 + +这十分简单,并且在低流量,短连接的场景下表现的很好。 + +```go +type bufferedScanner struct { + net.Conn + tee io.Reader + buffer *bytes.Buffer +} + +func NewBufferedScanner(original net.Conn) net.Conn { + var buffer bytes.Buffer + return bufferedScanner { + Conn: original, + tee: io.TeeReader(c, &buffer), + buffer: &buffer, + } +} + +func (b bufferedScanner) Read(p []byte) (n int, err error) { + return b.tee.Read(p) +} + +func (b bufferedScanner) Close() error{ + analyse(b.buffer) + return b.Conn.Close() +} +``` + +经过小的优化以[有效地重用缓存](https://golang.org/pkg/sync/#example_Pool)后,我对这个方案感到满意,至少有一会儿是这样。 + +### 第二版代码:使用扫描器(scanner) + +不久之后,我意识到这个方案处理不好长连接和突发流量的场景,于是我写了一些可以流式工作而不是缓存所有数据的代码。 + +就初始化的内存空间而言,这个方案有着更大的开销(用来构建 scanner 以及其他额外的数据结构),但是在同一个连接发送几十 kb 数据之后,这个方案在内存和计算方面变得更加的高效。 + +流式的解决方案实现起来比较棘手,但是得益于 `bufio` 包,这些困难是可控的。实现代码仅仅是有着自定义 [`SplitFunc`](https://golang.org/pkg/bufio/#SplitFunc) 的 [`scanner`](https://golang.org/pkg/bufio/#Scanner) 的包装。 + +## 核心问题 + +不管我的解决方案如何,我现在有两段代码,它们各有利弊:对于低流量生存周期短的连接,第一种方案更好,但是对于流量密集的场景,第二种方案是唯一可行的。 + +我有两个可能的优化办法:尝试优化第二个方案,让这个方案在小的连接上同样可行,或者基于在运行时看到的内容,选择合适的实现。 + +我选择了第二种,也是看起来更有趣的一个。 + +## 解决方案 + +我创建了一个构造器(builder)来提供以及实例化两个实现。这个构造器维护着每次流量的总流量的大小的一个[指数加权移动平均值(exponential weighted moving average)](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average)。大致上与 TCP 协议用来估算 RTT 和到达时间变化的算法类似。 + +很有意思的是,我实现它需要的字数要少于我描述它所用到的字数: + +```go +ewma := k * n + (1 - k) * ewma +``` + +其中 `n` 是在 `Close` 时从连接中读取到的总字节。`k` 在代码中仅仅是一个调节对大小变化的启发式反应速度的常量,我这里设定为二分之一。 + +困难的地方在于选择切换实现的阈值:我运行了一些基准测试,并且找到了流式方案开始表现得比缓存方案更好的那个临界点,但是我很快发现这个临界点的值很大程度上依赖于运行代码的计算机。 + +## 误伤 + +如同 Go [在 `math/big` 包](https://github.com/golang/go/blob/50bd1c4d4eb4fac8ddeb5f063c099daccfb71b26/src/math/big/calibrate_test.go#L18)中启发式选择正确的算法来进行计算,大部分情况下,在笔者的笔记本上运行的基准测试足以确定这个常量。按照 math/big 贡献者所说的那样,这种方式的问题是,它会导致[差异超过 100% 的错误](https://github.com/golang/go/issues/25580)。 + +不用说,我不喜欢这种方式,于是我开始整理我可用的工具。我不希望使用 makefile 或其他外部依赖。我也不希望我的用户能够使用我的代码之前要安装或者运行外部的代码,于是我思考**用户在编译我的库的时候什么是已经可以使用的**。 + +Go 是跨平台的,所以我不需要处理不同平台的差异,但是另一方面,我需要一些信息来感知现有的运算能力。 + +### 测量 + +基准测试相对简单:go 语言有着[内置的基准测试标准库](https://golang.org/pkg/testing/#hdr-Benchmarks)。 + +在默默地盯了文档几分钟之后,我意识到我可以通过调用 [`testing.Benchmark`](https://golang.org/pkg/testing/#Benchmark)的方式在非测试构建(non-testing build)中运行基准测试,testing.Benchmark 会返回一个不错的 [testing.BenchmarkResult](https://golang.org/pkg/testing/#BenchmarkResult)。 + +于是我设置了一些 `func(b *testing.B)` 的匿名函数对输入值(比如,存根连接大小)构建闭包,来对两个方案运行基准测试,看哪一个方案表现的更好。 + +```go +type ConfigurableBenchmarker struct { + Name string + GetBench func(input []byte) func(b *testing.B) +} +``` + +一个 `ConfigurableBenchmarker`(可配置基准测试)的例子,以及如何使用它: + +```go +ConfigurableBenchmarker{ + Name: "Buffered", + GetBench: func(input []byte) func(b *testing.B) { + return func(b *testing.B) { + for i := 0; i < b.N; i++ { + c := NewBufferedScanner(stubConnection{ + bytes.NewReader(input), + }) + io.Copy(ioutil.Discard, c) + c.Close() + } + } + }, +} + +// doBench 运行两个 ConfigurableBenchmarkers 并返回 +// 是否第一个方案耗时更少 +func doBench(size int, aa, bb ConfigurableBenchmarker) bool { + aaRes = testing.Benchmark(aa.GetBench(genInput(size))) + bbRes = testing.Benchmark(bb.GetBench(genInput(size))) + return aaRes.NsPerOp() < bbRes.NsPerOp() +} +``` + +使用这个构建块,我可以进行二分搜索,来确定一个方案比另一个方案效果更好的输入大小。 + +```go +func FindTipping(aa, bb ConfigurableBenchmarker, lower, upper int) (int,error) { + lowerCond := doBench(lower, aa, bb) + upperCond := doBench(upper, aa, bb) + if lowerCond == upperCond { + return 0, ErrTippingNotInRange + } + + // 经典的二分搜索 + tip = (lower + upper) / 2 + for tip > lower && tip < upper { + tipCond := doBench(tip, aa, bb) + if tipCond == lowerCond { + lower = tip + } else { + upper = tip + } + tip = (lower + upper) / 2 + } + return tip, nil +} +``` + +这里是在 `lower = 1` 并且 `upper = 100` 时的一些输出和一些日志: + +``` +Calculating initial values... +AnalysingTraffic: [1KB] Buffered 1107 ns/op < 1986 ns/op Streamed +AnalysingTraffic: [100KB] Buffered 87985 ns/op >= 69509 ns/op Streamed +Starting search... +Binsearch: lower: 1, upper: 100 +AnalysingTraffic: [50KB] Buffered 43455 ns/op >= 35242 ns/op Streamed +Binsearch: lower: 1, upper: 50 +AnalysingTraffic: [25KB] Buffered 22693 ns/op >= 19506 ns/op Streamed +Binsearch: lower: 1, upper: 25 +AnalysingTraffic: [13KB] Buffered 11355 ns/op >= 10263 ns/op Streamed +Binsearch: lower: 1, upper: 13 +AnalysingTraffic: [7KB] Buffered 4964 ns/op < 5824 ns/op Streamed +Binsearch: lower: 7, upper: 13 +AnalysingTraffic: [10KB] Buffered 7415 ns/op < 8140 ns/op Streamed +Binsearch: lower: 10, upper: 13 +AnalysingTraffic: [11KB] Buffered 8609 ns/op < 8765 ns/op Streamed +Binsearch: lower: 11, upper: 13 +AnalysingTraffic: [12KB] Buffered 9828 ns/op < 10157 ns/op Streamed +Tipping point was found at 12 +Most efficient for input of smaller sizes was "Buffered" +Most efficient for input of bigger sizes was "Streamed" +``` + +有了这些,我可以自动在当前机器上检测出代码的临界点。现在我需要运行这段代码。我可以在 README 文件中写个说明,但是这样做的话有什么意思? + +### Go generate + +`go generate` 命令可以解析具有特定语法的注释,并运行其中的内容。 + +运行 `go generate` 的话,下面的注释会让 Go 打印个招呼。 +```go +//go:generate Echo "Hello, World!" +``` + +所以,当用户 `go get` 一个包的时候,他们可以 `go generate` 一些代码,然后 `go build` 或者将他们与来源链接。 + +我将基准测试代码包装到一个 `generator.go` 中,这个文件运行基准测试并且将常量写入源文件。仅仅是将基准测试获取到的数字格式化为一个字符串,并写入到本地文件: + +```go +const src = `// Code generated; DO NOT EDIT. + +package main + +const streamingThreshold = %d +` + +func main() { + tip := FindTipping(/* params */) + // Omitted: open file "constants_generated.go" for writing in `f` + fmt.Fprintf(f, src, tip) +} +``` + +之后,我只需要在其他源文件中增加一个注释: + +```go +//go:generate Go run generator.go +``` + +目标机器必须安装了 `go`,用来编译我的代码。这意味着我没有要求用户增加其他的额外工具或依赖。 + +这很不错,但有一个严重的问题:在不使用外部构建工具的前提下,无法让 `main` 包和 `analyse` 共存于一个文件夹。 + +确实如此,除非你(滥)用[构建标签(build tags)](https://golang.org/pkg/go/build/#hdr-Build_Constraints):你可以防止一个文件在读取 `package` 语句之前考虑构建。 + +所以我用这样的开头修改了我的生成器代码: + +```go +// +build generate + +package main +``` + +并且将原来的代码注释改成了这样: +```go +//go:generate Go run generate.go -tags generate +``` + +现在的结构: + +``` +analyse +├── analyse.go ← Package analyse, 有着 //go:generate 指令 +├── analyse_test.go ← Package analyse, 测试 +├── constants_generated.go ← Package analyse, 生成的代码 +└── generate.go ← Package main, 隐藏着一个 tag +``` + +于是,我现在可以 `go get` 或者 `git clone` 我的 package,运行 `go generate`,之后可以针对我的机器优化后运行。 + +## 效果 + +这里是三种方案的最终基准测试结果。在我运行基准测试的机子的临界点是 12K。单位是 `ns/op`。 + +``` +Dim | Buff | Adapt | Stream +----|--------|--------|------- + 1K | 1159 | 1278 | 1965 + 2K | 1723 | 1868 | 2574 + 4K | 2842 | 3055 | 4450 + 8K | 5644 ← | → 5929 | 7446 +16K | 15359 | 13478 ← | → 13539 +32K | 29814 | 25430 | 24980 +64K | 58821 | 49078 | 48596 +``` + +适应性的解决方案在测量流量大小上有一点开销,所以它永远不如其他方案中最好的一个表现的那么好,但几乎是最佳的。另一方面,该适应性方案总是比更糟糕的方案要好。 + +--- + +via: https://blogtitle.github.io/go-advanced-benchmarking/ + +作者:[Rob](https://blogtitle.github.io/authors/rob/) +译者:[dust347](https://github.com/dust347) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190113-Create-New-SmartContract-Language-with-Go-Lexer-part.md b/published/tech/20190113-Create-New-SmartContract-Language-with-Go-Lexer-part.md index 674f355fb..ff0ceecb4 100644 --- a/published/tech/20190113-Create-New-SmartContract-Language-with-Go-Lexer-part.md +++ b/published/tech/20190113-Create-New-SmartContract-Language-with-Go-Lexer-part.md @@ -109,7 +109,7 @@ func NewLexer(input string) *Lexer { tokench: make(chan Token, 2), } - go l.run(input) + Go l.run(input) return l } // emit passes an token back to the client. diff --git a/published/tech/20190206-Debugging-with-Goland-Getting-Started.md b/published/tech/20190206-Debugging-with-Goland-Getting-Started.md index c2f403a70..42cf4a807 100644 --- a/published/tech/20190206-Debugging-with-Goland-Getting-Started.md +++ b/published/tech/20190206-Debugging-with-Goland-Getting-Started.md @@ -2,7 +2,7 @@ # 使用 Goland 调试 - 起步 -*由 [Florin Pățan](https://blog.jetbrains.com/go/author/florin-patanjetbrains-com/) 发布于 2019 年 2 月 6 日* +*由 [Florin P ăț an](https://blog.jetbrains.com/go/author/florin-patanjetbrains-com/) 发布于 2019 年 2 月 6 日* 调试是任何一个现代应用的生命周期中的必要部分。 @@ -240,7 +240,7 @@ CMD ["/dlv", "--listen=:40000", "--headless=true", "--api-version=2", "exec", "/ via: https://blog.jetbrains.com/go/2019/02/06/debugging-with-goland-getting-started/ -作者:[Florin Pățan](https://blog.jetbrains.com/go/author/florin-patanjetbrains-com/) +作者:[Florin P ăț an](https://blog.jetbrains.com/go/author/florin-patanjetbrains-com/) 译者:[130-133](https://github.com/130-133) 校对:[magichan](https://github.com/magichan) diff --git a/published/tech/20190207-Golang-timer-is-not-garbage-collected-before-expiry.md b/published/tech/20190207-Golang-timer-is-not-garbage-collected-before-expiry.md index bd91a5b0e..ddfdb3a7f 100644 --- a/published/tech/20190207-Golang-timer-is-not-garbage-collected-before-expiry.md +++ b/published/tech/20190207-Golang-timer-is-not-garbage-collected-before-expiry.md @@ -1,6 +1,6 @@ 首发于:https://studygolang.com/articles/22617 -# Golang <-time.After()在计时器过期前不会被垃圾回收 +# Golang <-time.After() 在计时器过期前不会被垃圾回收 最近我在调查 Go 应用程序中内存泄漏的问题,这个问题主要因为我没有正确的阅读文档。这是一段导致消耗了多个 Gbs 内存的代码: diff --git a/published/tech/20190207-How-to-choose-a-programming-language.md b/published/tech/20190207-How-to-choose-a-programming-language.md new file mode 100644 index 000000000..498ea72ee --- /dev/null +++ b/published/tech/20190207-How-to-choose-a-programming-language.md @@ -0,0 +1,195 @@ +首发于:https://studygolang.com/articles/25302 + +# 如何选择一门编程语言 + +> 我应该学习哪种编程语言? + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/how-to-choose-a-programming-language/1.jpg) + +如果你看到这篇文章,很有可能,在你的职业生涯中,你至少有一次在思考应该选择哪种语言。或者你甚至在编程生涯开始之前就已经考虑了。很高兴你能看到这篇文章。 + +我本人经常地(甚至可能过多地)去思考这个问题。不仅如此,我还会在实践中尝试许多不同的技术,得到或好或坏的结果。 + +在过去的几年,我曾经使用过以下语言: + +- Bash +- IBM RPG +- Java +- Scala +- C# +- C++ +- Ruby +- JavaScript, CoffeeScript +- Clojure +- Objective-C +- Elixir +- GO + +我说的使用并不是只读过一些教程,而是至少在一个生产项目上工作过一整年。 + +如果算上一些练习程序、学生项目、训练营和研讨会,我使用过更多: + +- Pascal +- PHP +- Swift +- Kotlin +- Groovy +- Python +- TypeScript +- Crystal +- OCaml +- MATLAB +- Visual Basic +- Solidity + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/how-to-choose-a-programming-language/2.jpg) + +尽管如此,我相信,针对如何选择一门新的编程语言,以及需要关注的重要的内容,我可以提出一些我自己的观点。 + +此外,根据 [CliftonStrengths assessment](https://www.gallupstrengthscenter.com/home/en-us/strengthsfinder) ,我非常擅长分析,所以我觉得自己在这方面更加胜任: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/how-to-choose-a-programming-language/3.png) + +我可以给你一个检查清单,包含一系列问题,你可以针对你想开始学习的任何新技术进行自测。 + +## 采用 + +- 什么样的公司使用它? +- 它究竟在哪些领域被使用? +- 它的一般用途是什么? + +你必须考虑特定的技术应用于哪个领域。也许这主要是自动化的(例如 C++ )、 ML / AI ( Python )、底层的和服务器相关的东西( Go )、web 应用程序( Ruby )、企业项目( Java / c# ) 等等。 + +你需要考虑你的兴趣是什么,你是否想要在创业公司、小型软件公司、大公司、金融技术公司或政府机构工作,并在那里找到匹配的技术。 + +例如,如果你看到 .NET 主要用于企业领域,但你更喜欢小型公司,那么你可能会考虑使用 Ruby。 + +## 社区 + +### 开发者 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/how-to-choose-a-programming-language/4.jpg) + +你是否曾经想过有多少特定技术的开发人员?是否有很多人使用 Rust 或 F# ?让我解释一下 —— 从别人那里得到支持。 + +在我看来,很多程序员在遇到任何问题时,通常都会使用 StackOverflow 或者在各种博客上寻找答案。如果某项特定技术的开发者寥寥无几,这一切都不太可能实现。他们可能会有不同的问题,而且不会有很多人在网上分享。 + +程序员越多,遇到过的问题就越多,在互联网上发布的解决方案也越多。如果你正在使用一些众所周知的语言,你可能还没有经历过这样的问题。尝试一些新奇的技术,你会立即开始与你不理解的错误作斗争,你自己解决不了,也没有人能帮助你。 + +### 招聘 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/how-to-choose-a-programming-language/5.png) + +我知道开发人员很少考虑这方面的问题,尤其是当你是初学者时,但是当你考虑你的产品和公司时,你必须记住这一点。 + +有时候,你可能是 CTO、技术或团队负责人,你的职责是为项目挑选工具、技术和招募新人。很明显,当你选择一些花哨的语言(如 Elm)时,你可能很难找到有经验的人加入你。 + +当你在找一份新工作时,情况可能是类似的,你唯一拥有的技能是一些未知的、不那么流行的技术。你可能会发现重新安排工作机会并找到一份令你满意的工作是一个很大的问题。 + +## 支持者 + +### 创造者 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/how-to-choose-a-programming-language/6.jpg) + +- 到底谁是创造者? +- 创造者如何对社区做出承诺? +- 创造者是否固执己见? + +这也是技术的一个重要方面。它的创造者可以积极参与社区,但也可以退出社区。 + +以 Elixir 为例。Jose Valim 是 Elixir 语言的联合创始人和创建者,他确实参与了这种语言的开发。他在 GitHub 上尽可能多地回复,在 Elixir 论坛上给出反馈,甚至参加小型聚会和当地会议,鼓励人们使用 Elixir。 + +回想 2012 年,当我在 Scala 编程时, Scala 语言设计师马丁.奥德斯基( Martin Odersky )很少出现在一些小型社区,更不用说程序员会议了。当然,他有时也会做一些演讲,他并没有完全脱离开发人员,但是他和所有使用 Scala 的程序员之间似乎有很大的距离。幸运的是,根据我从同事那里了解到的情况,现在已经发生了变化,情况好多了。 + +当你查看 Ruby on Rails 时,你将看到一个非常固执但非常有魅力的领导者。他喜欢表达主观的观点。 + +### 公司 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/how-to-choose-a-programming-language/7.jpg) + +- 支持该语言的主要公司有哪些? +- 语言将很快“消亡”的变化是什么? +- 在语言上投入了多少钱? +- 是否在持续迭代更新? + +有时候,考虑哪些公司支持某种特定的语言是很重要的。是倡导 GO 的谷歌吗?或者是支持 React 的 Facebook 吗? Rust 由 Mozilla 支持, C# 来自微软, Swift 由苹果开发。另一方面, Clojure 和 Python 被认为是社区驱动的,这可能带来各自的优缺点。 + +考虑为支持语言扩展提供了多少资金是很重要的。如果你确信 Oracle 将年复一年地发展 Java ,那么你就不必担心你的未来、市场需求和工作机会。如果你看到 Angular 将每 6 个月发布一次,你就会觉得很安全,因为新特性将会频繁出现,你也可以安排必要的升级。 + +这就是为什么你也要考虑这个方面,这在一开始并不明显,需要一些研究。 + +## 库 + +- 有多少库可以使用? +- 不同的服务提供了哪些集成? + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/how-to-choose-a-programming-language/8.jpg) + +举个例子,如果考虑 Ruby 或 JavaScript ,几乎可以 100%确定每个工具都与这些语言有一些集成。你不必担心围绕一些 API 编写自己的工具或封装器。 你会发现所有的库,甚至是官方库都可以与 Twitter、Twillio、GitHub、Dropbox 等集成。但是,如果你选择 Elixir, 那么你可能很难找到维护良好的社区库,更不用说官方库了。现在的情况比 4 年前要好,但有时这仍然是个问题,需要得到明确的验证。 + +因此,如果你计划构建一个严重依赖外部 API 并结合多种集成的工具,那么你可能需要考虑一些不那么生僻的技术来实现这一点。请注意,你的目标是构建产品并交付业务特性,而不是开发其他库或者集成 (当然,除非这是你的实际业务)。 + +## 薪水 + +- 这种语言的程序员薪水高吗? +- 这项技术的市场需求如何? +- 开发人员之间竞争激烈吗? + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/how-to-choose-a-programming-language/9.png) + +### 目前情况如何? + +Cobol 现在是一种报酬丰厚的技术,但你考虑过具体原因吗?是因为它很有前途,非常受欢迎,被广泛采用,还是因为它很老,但是很多系统都依赖它,没有专家? + +你需要考虑,使用某种特定的语言会被支付多少薪水。你可能非常喜欢 Elm,但经过一些研究,希望不要太深入地研究它,会发现没有人愿意为它支付很多钱。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/how-to-choose-a-programming-language/10.png) + +### 趋势 + +有时,这种语言的报酬还不高,但考虑到市场需求,薪酬上涨还是很有希望的。以 Rust 为例。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/how-to-choose-a-programming-language/11.png) + +如果你坚信某门语言在未来会快速发展,你可以马上投入你的时间,一旦它流行起来,你就已经是专家了。这当然会带来风险,因为这项技术可能不会被很好地采用,但想象一下如果不这样做会发生什么。你将拥有别人没有的经验,你将能够成为一名专家。你是选择紧跟潮流,还是为你的未来做出选择。 + +## 复杂度 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/how-to-choose-a-programming-language/12.png) + +这是一个非常个人的事情,但仍然值得考虑,检查一下你是否喜欢一种语言的语法。错误处理是如何完成的,你需要多少代码量来实现并发或者并行性,你可以多快地阅读和理解其他代码。 + +以 Ruby 或 Python 为例,它们是每一个说英语的人都能容易理解的语言。看看基于 C 的语言,你可能很难理解其中到底发生了什么。 Java 和 C# 也非常冗长,而 Clojure 有时可能过于复杂。 + +当然,随着时间的推移,你开始越来越多地理解任何语法,但在某些情况下,你可能对 Elixir 更有信心,而不是使用 Go。 在我看来,一门语言越底层,开发人员的灵活性和容忍度就越高它允许的抽象越多,表述性就越好。 + +## 工具 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/how-to-choose-a-programming-language/13.jpg) + +除了有可用的库之外,语言中还有一个重要部分是支持开发的可用工具,包括不同的构建器、编译器、静态分析程序、格式化程序、测试和部署工具。 + +考虑一下引导单个项目、编码、测试并将其部署到生产环境中有多难。在编写代码库之后,发布新应用程序版本需要多少时间。迁移数据库、下载和升级依赖项或构建单个可执行包是否简单易行。 + +一段时间之后,你可能会有一组脚本来完成这项工作,但是要考虑初级人员或新手。他们能像你一样轻松地做所有这些事情吗?这些特性是语言本身提供的,还是每个开发人员都必须准备自己的工具?语法是一回事,但总有一天,你必须向全世界公开你的应用程序。这需要花费你多少代价? + +## 总结 + +你看,有很多事情是需要考虑的。如果你的时间有限且昂贵,你可能要反思将把时间花在什么地方。 + +你刚刚读到的这些问题,一定会帮助你做出正确的选择,并对你的决定感到满意。 + +跟我聊聊你最近是否选择了一项新技术,你是如何分析它的,以及是否对自己的选择感到高兴。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/how-to-choose-a-programming-language/14.jpg) + +--- + +via:https://blog.lelonek.me/how-to-choose-a-programming-language-7805da7ec588 + +作者:[Kamil Lelonek](https://blog.lelonek.me/@KamilLelonek) +译者:[iris55](https://github.com/iris55) +校对:[JYSDeveloper](https://github.com/JYSDeveloper) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190214-Debugging-with-GoLand-Essentials.md b/published/tech/20190214-Debugging-with-GoLand-Essentials.md index dcf4b356e..91568949f 100644 --- a/published/tech/20190214-Debugging-with-GoLand-Essentials.md +++ b/published/tech/20190214-Debugging-with-GoLand-Essentials.md @@ -2,7 +2,7 @@ # 使用 GoLand 进行调试的要点 -*由 [Florin Păţan](https://blog.jetbrains.com/go/author/florin-patanjetbrains-com/) 于 [2019 年 2 月 14 日 ](https://blog.jetbrains.com/go/2019/02/14/debugging-with-goland-essentials/) 发表* +*由 [Florin P ăţ an](https://blog.jetbrains.com/go/author/florin-patanjetbrains-com/) 于 [2019 年 2 月 14 日 ](https://blog.jetbrains.com/go/2019/02/14/debugging-with-goland-essentials/) 发表* 在今天的帖子中,我们将继续探索 GoLand 中的调试器功能。如果你想知道如何配置调试器。请查看我们之前的帖子,其中包含关于如何配置 IDE 在各种方案中工作的所有信息。 @@ -70,7 +70,7 @@ via: https://blog.jetbrains.com/go/2019/02/14/debugging-with-goland-essentials/ -作者:[Florin Pățan](https://blog.jetbrains.com/go/author/florin-patanjetbrains-com/) +作者:[Florin P ăț an](https://blog.jetbrains.com/go/author/florin-patanjetbrains-com/) 译者:[piglig](https://github.com/piglig) 校对:[magichan](https://github.com/magichan) diff --git a/published/tech/20190222-Decoding-Why-GoLang-Stands-Apart-from-the-Other-Languages.md b/published/tech/20190222-Decoding-Why-GoLang-Stands-Apart-from-the-Other-Languages.md new file mode 100644 index 000000000..457ae79a6 --- /dev/null +++ b/published/tech/20190222-Decoding-Why-GoLang-Stands-Apart-from-the-Other-Languages.md @@ -0,0 +1,95 @@ +首发于:https://studygolang.com/articles/25280 + +# 解密为何 Golang 能从众多语言中脱颖而出 + +技术的突飞猛进,推动着世界上许多突破性的发现。 Golang ,作为如此革命性的发明,早已征服了整个世界。 + +当我们接触到 Golang 后,开发领域中已经发现了很多种(用 Golang)带来进步和革新的方法。在语言的万马千军纷纷争奇斗艳时,Golang 早已证明了自己才是最大的游戏规则改变者。 + +虽然对于初学者来说,这种新生的语言可能有一点点复杂和难以掌握,但是当你做了充足的练习后,你会很容易地处理 Golang 语言。 + +在(开发者)熟识关于 Golang 的一些基础知识之前,他们往往就已经被这种高级编程语言逼疯了。好了,闲话少说,我们一起来研究下这篇博客摘录。 + +## Golang -- 无限可能 + +你能相信在过去的几年中 Golang 的受欢迎度像飞火流星一样如此飙升吗?作为一种领先业界的结果导向的编程语言,Golang 已风靡全球,事实如此。 + +虽然几门其他的语言如 C, Java 仍在统治编程领域,但是一些能为现代计算带来更好结果的新模型也被引入了,尤其是在云计算中。 + +Go 之所以越来越受欢迎,主要归功于的它的轻量,还有它完美适配几乎所有的微服务架构。容器宠儿 Docker 和 Google 的 k8s 也使用了 Go 。 + +除了如此不可思议的受欢迎外,Go 凭借它(相对其他语言)有利的特性在数据科学领域也站稳了脚跟,Go 的这些功能正是数据科学所需要的,用(这些功能)来(为数据科学)带来更好的效果。 + +作为一种高级编程语言,Go 可以在多个方面方便开发者,诸如原生的并发,垃圾回收机制,等等。使用 Go 语言时,开发者不需要再依赖自己减少通过编码解决内存泄露等问题的必要的能力。Golang 的一些其他特性,可以恰到好处地适配解决数据科学和微服务架构的问题。 + +因为前面提到的这些特性,全球越来越多初创公司开始使用 Go 语言。Go 语言中包含了一套 Tensorflow 的 API ,还有一些项目如 Pachyderm 是用 Go 语言精心打造的。 + +Cloud Foundry 中也有一连串的零部件是用 Golang 写的。有趣的是,这份列表定期添加用 Go 语言写的不同(项目)名字。 + +> 推荐阅读: [*Advantages of Using Golang for Your Next Web Application Project*](https://www.mindinventory.com/blog/advantages-of-Golang-for-web-application-project/) + +## Golang 为何无可替代? + +Golang 开始为开发领域带来进步和缓和一些顽固问题的历史并不长。在追求为新世纪的开发者们提供一个统一和无缝体验的过程中,Golang 毫无疑问地已经一步步为自己开创了一片天地。 + +但是,Golang 真的名副其实吗? Golang 真的无可替代?它真的有资格坐头把交椅?我们一探究竟。 + +### 极简是 Golang 的杀手锏 + +极简是无与伦比的,Golang 毫无疑问已经把它发挥到了极致。很多表现很成功的语言像 Rust 、Scala 等都有很复杂的特性。而且,它们提供了先进的内存管理和类型系统,在开发领域表现得很优秀。这些语言也曾像 C# 、C++ ,一时成为主流语言,发挥了它们全部的能量。 + +Golang 走了一条稍微有些不同的路线:合情合理地摒弃了很多此类(上述)特性。下面列出了 Go 摒弃的几个特性和能力。 + +- #### 没有泛型 + +泛型和模板是不同编程语言区分于彼此的重要体现。加入与错误信息关联的复杂的泛型,往往让语言变得晦涩难懂。Go 的设计者们,抛弃了这部分,使得 Go 语言变得更简单。无疑,它虽然有争议但仍是个明智的设计。 + +- #### 一个可执行文件就够了 + +Golang 中确实没有与可执行文件隔离的运行库。能生成一个通过复制就可以轻易部署的的二进制文件是一项不错的能力。版本不匹配或依赖问题出现的风险被消除,要感谢 Golang 的这个特性。此外,这个特性也是基于容器开发的工程的巨大福音。 + +- #### 没有动态库 + +Golang 在 1.8 版本作了微调,现在开发者们可以通过插件包来加载动态库。但是因为这个特性并不是安装 Go 时自带的,所以它仍然只是为某些特定的功能而存在的扩展。 + +### Go 之所以流行,协程功不可没 + +从一个非常务实的角度看,Go 的协程是最有魅力的几个方面之一。专家们通过 Go 协程可以轻易(在单核机器上)实现出多核机器的能力。 + +- #### CSP + +C. A. R. 是 Golang 并发模型的原型。最初的想法是,规避掉所有的在劳动密集型和容易出错的多个运行线程间共享内存的同步问题。 + +- #### 协程同步 + +另一个等待协程停止的有效的方法是使用同步组。你可以声明一个等待组的对象,随后在每一个可以正常调用它的 Done() 函数的协程在完全停止时把声明的对象传给它。 + +- #### channel + +Go 协程通过 channel 可以轻松地实现互相访问信息。你可以创建一个 channel 变量后把它传递给协程。使用者既可以从 channel 接收数据,又可以向 channel 发送数据。 + +### 无缝处理错误 + +Golang 中处理错误的概念是(与其他语言)完全不同的。即使是普通的函数,也能顺利地返回一个错误作为它的最终返回值。你即便不能在协程中返回一个错误,也可以通过其他的方式让错误逃逸到协程外的作用域。 + +把一个内含数据是 error 类型的 channel 传给一个协程无疑是一次很好的实践。使用者也可以使用 Golang 把错误写到数据库、log 文件或请求远程服务。 + +## 结语 + +由于技术的突飞猛进,软件部署以及交付的概念已经发生了很大的变化。微服务架构在敏捷开发中扮演了一个关键的角色。绝大部分新生的应用是按照云原生且可以利用云平台提供的多种云服务这个思路设计的。 + +世界上有很多种编程语言,每种语言都有它独一无二的特性。但是,作为一种完美无瑕的工程语言,Golang 的设计方式使其可以满足新的要求。Golang 是为云而写的,由于其构建和对并行操作的掌控而获得了很大的欢迎。 + +过去的几年中,Go 见证了自己受欢迎度的一路高歌,尤其是在现代数据库方面。虽然 Go 如此风靡一时的很大一部分原因是跟谷歌的支持有关,但它在开发领域中独一无二的方式才是让它驰名中外最关键的因素。 + +如果你正在为你的事业寻找 [Golang Web development company](https://www.mindinventory.com/Golang-development.php) ,请从我们这儿雇佣 Golang 开发专家作为你的一站式软件开发伙伴。 + +--- + +via: https://www.mindinventory.com/blog/what-makes-Golang-stand-apart-from-other-languages/ + +作者:[Paresh Solanki](https://www.mindinventory.com/blog/author/pareshsolanki/) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studyGolang/GCTT) 原创编译,[Go 中文网](https://studyGolang.com/) 荣誉推出 diff --git a/published/tech/20190225-Design-of-a-Modern-Cache-Pare-Deux.md b/published/tech/20190225-Design-of-a-Modern-Cache-Pare-Deux.md index 576d87e3d..955fd1b64 100644 --- a/published/tech/20190225-Design-of-a-Modern-Cache-Pare-Deux.md +++ b/published/tech/20190225-Design-of-a-Modern-Cache-Pare-Deux.md @@ -38,7 +38,7 @@ ## 结论 -Caffeine 是一个开源的 Java 缓存库。本文和上一篇文章讨论的技术可以应用于任何语言,并且实现起来非常简单。除了我们上一篇文章中所感谢的人员,我还要特别感谢以下人员做出的贡献,Ohad Eytan,Julian Vassev,ViktorSzathmáry,Charles Allen,William Burns,Christian Sailer,Rick Parker,Branimir Lambov,Benedict Smith,Martin Grajcar,Kurt Kluever,Johno Crawford,Ken Dombeck 和 James Baker +Caffeine 是一个开源的 Java 缓存库。本文和上一篇文章讨论的技术可以应用于任何语言,并且实现起来非常简单。除了我们上一篇文章中所感谢的人员,我还要特别感谢以下人员做出的贡献,Ohad Eytan,Julian Vassev,ViktorSzathm á ry,Charles Allen,William Burns,Christian Sailer,Rick Parker,Branimir Lambov,Benedict Smith,Martin Grajcar,Kurt Kluever,Johno Crawford,Ken Dombeck 和 James Baker --- diff --git a/published/tech/20190225-Go-Advanced-concurrency-patterns-part-2-timers.md b/published/tech/20190225-Go-Advanced-concurrency-patterns-part-2-timers.md index deea15229..df7b4dfef 100644 --- a/published/tech/20190225-Go-Advanced-concurrency-patterns-part-2-timers.md +++ b/published/tech/20190225-Go-Advanced-concurrency-patterns-part-2-timers.md @@ -173,7 +173,7 @@ if !t.Stop() { t.Reset(d) ``` -不能与来自通道的其他接收者同时使用 `Stop` 和 `Reset` 方法, 为了使 `C` 上传递的消息有效,C 应该在每次 `重置` 之前被消费完。 +不能与来自通道的其他接收者同时使用 `Stop` 和 `Reset` 方法, 为了使 `C` 上传递的消息有效,C 应该在每次 ` 重置 ` 之前被消费完。 重置计时器而不清空它将使运行过程时丢弃该值,因为 `C` 缓存为 1,运行时对其他执行是[有损发送](https://golang.org/src/time/sleep.go?s=#L134)。 diff --git a/published/tech/20190227-Step-driven-evaluation.md b/published/tech/20190227-Step-driven-evaluation.md new file mode 100644 index 000000000..1a92e250f --- /dev/null +++ b/published/tech/20190227-Step-driven-evaluation.md @@ -0,0 +1,143 @@ +首发于:https://studygolang.com/articles/25296 + +# 类似 Go 中的表格驱动测试的步骤驱动评估 + +如果你听说过表驱动测试,那你就能更容易理解本文所描述的概念,因为它们使用的是相同的技术,只不过本文使用在非测试场景中。 + +假设你有一个函数,该函数中调用了多个其他函数。那么这个函数很可能主要有两个作用: + +1. 检查出现的所有错误返回。 +2. 传递一个函数的输出作为另一个函数的输入。 + +```go +// process is an example pipeline-like function. +func queryFile(filename, queryText string) (string, error) { + data, err := readData(filename) + if err != nil { + return nil, errors.Errorf("read data: %v", err) + } + rows, err := splitData(data) + if err != nil { + return nil, errors.Errorf("split data: %v", err) + } + q, err := compileQuery(queryText) + if err != nil { + return nil, errors.Errorf("compile query: %v", err) + } + rows, err = filterRows(rows, q) + if err != nil { + return nil, errors.Errorf("filter rows: %v", err) + } + result, err := rowsToString(rows) + if err != nil { + return nil, errors.Errorf("rows to string: %v", err) + } + return result, nil +} +``` + +这个函数包含了 5 个步骤。准确地说是 5 个相关的调用,其他所有的一切都不是重点。在这个算法中,函数的调用是有序的。 + +让我们使用步骤驱动评估算法重写上面的代码。 + +```go +func queryFile(filename, queryText string) ([]row, error) { + var ctx queryFileContext + steps := []struct { + name string + fn func() error + }{ + {"read data", ctx.readData}, + {"split data", ctx.splitData}, + {"compile query", ctx.compileQuery}, + {"filter rows", ctx.filterRows}, + {"rows to string", ctx.rowsToString}, + } + for _, step := range steps { + if err := step.fn(); err != nil { + return errors.Errorf("%s: %v", step.name, err) + } + } + return ctx.result +} +``` + +这种管道式的做法使得代码清晰、明确,也便于调整步骤的顺序、新增或者移除某些步骤。另外,在循环体中增加调试日志也非常的简单,你只需要在循环程序中新加一个声明语句就可以,不需要像一开始那样,在每个函数调用的地方都要增加声明语句。 + +当引入一个新类型的复杂性低于其带来的收益时,在 4 个或更多步骤的情况下,这种方法表现亮眼。 + +```go +// queryFileContext might look like the struct below. + +type queryFileContext struct { + data []byte + rows []row + q *query + result string +} +``` + +诸如方法 queryFileContext.splitData,仅调用了相同的函数并同时更新对象 ctx 的状态。 + +```go +func (ctx *queryFileContext) splitData() error { + var err error + ctx.rows, err = splitData(ctx.data) + return err +} +``` + +main 函数特别适合本文这种,能使各个步骤清晰明确的、适合 4+ 个步骤以上的方法。 + +```go +func main() { + ctx := &context{} + + steps := []struct { + name string + fn func() error + }{ + {"parse flags", ctx.parseFlags}, + {"read schema", ctx.readSchema}, + {"dump schema", ctx.dumpSchema}, // Before transformations + {"remove builtin constructors", ctx.removeBuiltinConstructors}, + {"add adhoc constructors", ctx.addAdhocConstructors}, + {"validate schema", ctx.validateSchema}, + {"decompose arrays", ctx.decomposeArrays}, + {"replace arrays", ctx.replaceArrays}, + {"resolve generics", ctx.resolveGenerics}, + {"dump schema", ctx.dumpSchema}, // After transformations + {"decode combinators", ctx.decodeCombinators}, + {"dump decoded combinators", ctx.dumpDecodedCombinators}, + {"codegen", ctx.codegen}, + } + + for _, step := range steps { + ctx.debugf("start %s step", step.name) + if err := step.fn(); err != nil { + log.Fatalf("%s: %v", step.name, err) + } + } +} +``` + +另外一个好处就是使得测试更加简单。即使我们要使用到函数 log.Fatalf,虽然这不是一个好的做法,在一个测试方法中,能够很容易的重新开启一个测试流程,同时,能够执行一系列失败的测试案例,而无需调用 os.Exit。 + +你也可以忽略测试中一些与 CLI 相关的步骤,比如“dump schema” 或者 “codegen”。你也可以在列表中插入测试专用的步骤。 + +这个方法也有一些缺点,比如: + +1. 你必须要定义新的类型和方法。 +2. 并不是总能很直接的找到合适的上下文对象,它只能适用于那些不会使得整个流程变得过于复杂的场景。 + +试着用用这个方法,也许你会喜欢上它的。 + +--- + +via: https://quasilyte.dev/blog/post/step-pattern/ + +作者:[Iskander Sharipov](https://github.com/quasilyte) +译者:[yangzhenxiong](https://github.com/yangzhenxiong) +校对:[DingdingZhou](https://github.com/DingdingZhou) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190305-Go-webserver-with-gracefull-shutdown.md b/published/tech/20190305-Go-webserver-with-gracefull-shutdown.md index 561b2483b..2c9c62f94 100644 --- a/published/tech/20190305-Go-webserver-with-gracefull-shutdown.md +++ b/published/tech/20190305-Go-webserver-with-gracefull-shutdown.md @@ -33,7 +33,7 @@ func main() { 程序会读取 `-listen-addr` 的命令行选项作为我们的变量 `listenAddr` 的值。如果没有值提供则使用 `:5000` 作为默认值。文本 `server listen address` 则会被用作帮助文档的描述。所以你可以使用**flag**包来管理所有想要的命令行选项。 ```shell -$ go build . +$ Go build . $ ./gracefull-webserver Server is ready to handle requests at :5000 @@ -154,7 +154,7 @@ func main() { signal.Notify(quit, os.Interrupt) server := newWebserver(logger) - go gracefullShutdown(server, logger, quit, done) + Go gracefullShutdown(server, logger, quit, done) logger.Println("Server is ready to handle requests at", listenAddr) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { diff --git a/published/tech/20190306-Build-and-Deploy-a-secure-REST-API-with-Go-Postgresql-JWT-and-GORM.md b/published/tech/20190306-Build-and-Deploy-a-secure-REST-API-with-Go-Postgresql-JWT-and-GORM.md index f333e908e..24ba6fcfc 100644 --- a/published/tech/20190306-Build-and-Deploy-a-secure-REST-API-with-Go-Postgresql-JWT-and-GORM.md +++ b/published/tech/20190306-Build-and-Deploy-a-secure-REST-API-with-Go-Postgresql-JWT-and-GORM.md @@ -6,7 +6,7 @@ ## 为什么用 Go -Go 是一个非常有意思的编程语言,它是一个强类型的语言,并且它编译得非常的快,它的性能可以与 C++ 相比较,Go 还有 goroutine —— 一个更加高效版本的线程——我知道这些特性都不是什么新鲜的东西了,但我就是喜欢 Go 这个样子的。 +Go 是一个非常有意思的编程语言,它是一个强类型的语言,并且它编译得非常的快,它的性能可以与 C++ 相比较,Go 还有 Goroutine —— 一个更加高效版本的线程——我知道这些特性都不是什么新鲜的东西了,但我就是喜欢 Go 这个样子的。 ## 我们将要做个什么东西? diff --git a/published/tech/20190314-What-are-goroutines-And-how-do-they-actually-work.md b/published/tech/20190314-What-are-goroutines-And-how-do-they-actually-work.md index b7c000a74..8c718eebb 100644 --- a/published/tech/20190314-What-are-goroutines-And-how-do-they-actually-work.md +++ b/published/tech/20190314-What-are-goroutines-And-how-do-they-actually-work.md @@ -101,7 +101,7 @@ Go Runtime Scheduler 执行协作调度,这意味着只有在当前 Goroutine via: https://medium.com/@joaoh82/what-are-goroutines-and-how-do-they-actually-work-f2a734f6f991 -作者:[João Henrique Machado](https://medium.com/@joaoh82) +作者:[Jo ã o Henrique Machado](https://medium.com/@joaoh82) 译者:[HN-JIE](https://github.com/HN-JIE) 校对:[magichan](https://github.com/magichan) diff --git a/published/tech/20190319-Using-Go-Modules.md b/published/tech/20190319-Using-Go-Modules.md index 5bca8fe5a..260b7e5ab 100644 --- a/published/tech/20190319-Using-Go-Modules.md +++ b/published/tech/20190319-Using-Go-Modules.md @@ -50,7 +50,7 @@ func TestHello(t *testing.T) { 到目前为止,目录里面包含了一个代码包,但是它还不是一个模块,因为这里面没有 `go.mod` 文件。如果我们现在的工作目录是 `/home/gopher/hello` 并且我们运行 `go test`,我们会看到如下的输出: ```bash -$ go test +$ Go test PASS ok _/home/gopher/hello 0.020s $ @@ -61,9 +61,9 @@ $ 让我们把当前的目录设置成模块的根目录吧,为此我们要用到 `go mod init` 命令然后再尝试运行 `go test`: ```bash -$ go mod init example.com/hello +$ Go mod init example.com/hello go: creating new go.mod: module example.com/hello -$ go test +$ Go test PASS ok example.com/hello 0.020s $ @@ -102,7 +102,7 @@ func Hello() string { 现在让我们再运行一遍测试: ```bash -$ go test +$ Go test go: finding rsc.io/quote v1.5.2 go: downloading rsc.io/quote v1.5.2 go: extracting rsc.io/quote v1.5.2 @@ -132,7 +132,7 @@ $ 第二次运行 `go test` 命令的时候 Go 命令工具就不再重复上述的工作了,因为 `go.mod` 已经是更新过了,并且刚才下载下来的模块已经缓存在本地(在 `$GOPATH/pkg/mod`)目录中: ```bash -$ go test +$ Go test PASS ok example.com/hello 0.020s $ @@ -143,7 +143,7 @@ $ 正如我们上面所见,添加一个直接依赖往往会带来其它间接的依赖。`go list -m all` 命令会把当前的模块和它所有的依赖项都列出来: ```bash -$ go list -m all +$ Go list -m all example.com/hello golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c rsc.io/quote v1.5.2 @@ -177,11 +177,11 @@ go 命令行工具使用 `go.sum` 文件来确保你的项目依赖的模块不 从 `go list -m all` 的输出中,我们可以看到我们在使用的 `golang.org/x/text` 模块还是以前没有被打过版本号标签的版本。让我们来把它更新到最新的有打过版本号标签的的版本,并测试是否能正常使用。 ```bash -$ go get golang.org/x/text +$ Go get golang.org/x/text go: finding golang.org/x/text v0.3.0 go: downloading golang.org/x/text v0.3.0 go: extracting golang.org/x/text v0.3.0 -$ go test +$ Go test PASS ok example.com/hello 0.013s $ @@ -190,7 +190,7 @@ $ 哇嗷,一切正常!我们再来看看现在 `go list -m all` 的输出和 `go.mod` 文件长什么样子: ```bash -$ go list -m all +$ Go list -m all example.com/hello golang.org/x/text v0.3.0 rsc.io/quote v1.5.2 @@ -212,11 +212,11 @@ $ 现在让我们来尝试更新 `rsc.io/sampler` 模块的次版本号。同样操作,先运行 `go get` 命令,然后跑一遍测试: ```bash -$ go get rsc.io/sampler +$ Go get rsc.io/sampler go: finding rsc.io/sampler v1.99.99 go: downloading rsc.io/sampler v1.99.99 go: extracting rsc.io/sampler v1.99.99 -$ go test +$ Go test --- FAIL: TestHello (0.00s) hello_test.go:8: Hello() = "99 bottles of beer on the wall, 99 bottles of beer, ...", want "Hello, world." FAIL @@ -228,7 +228,7 @@ $ 噢,糟糕,测试报错了,这个测试表明 `rsc.io/sampler` 模块的最新版本跟我们之前的用法不兼容。我们来列举一下这个模块能用的 tag 过的版本: ```bash -$ go list -m -versions rsc.io/sampler +$ Go list -m -versions rsc.io/sampler rsc.io/sampler v1.0.0 v1.2.0 v1.2.1 v1.3.0 v1.3.1 v1.99.99 $ ``` @@ -236,11 +236,11 @@ $ 我们之前用过 `v1.3.0`,而 `v1.99.99` 明显不能用了。也许我们能试一下 `v1.3.1` 版本 ```bash -$ go get rsc.io/sampler@v1.3.1 +$ Go get rsc.io/sampler@v1.3.1 go: finding rsc.io/sampler v1.3.1 go: downloading rsc.io/sampler v1.3.1 go: extracting rsc.io/sampler v1.3.1 -$ go test +$ Go test PASS ok example.com/hello 0.022s $ @@ -283,7 +283,7 @@ func TestProverb(t *testing.T) { 然后我们可以来测试我们的代码了: ```bash -$ go test +$ Go test go: finding rsc.io/quote/v3 v3.1.0 go: downloading rsc.io/quote/v3 v3.1.0 go: extracting rsc.io/quote/v3 v3.1.0 @@ -295,7 +295,7 @@ $ 请注意我们的模块现在既依赖 `rsc.io/quote` 也依赖 `rsc.io/quote/v3`: ```bash -$ go list -m rsc.io/q... +$ Go list -m rsc.io/q... rsc.io/quote v1.5.2 rsc.io/quote/v3 v3.1.0 $ @@ -310,7 +310,7 @@ $ 现在让我们把整个项目的 `rsc.io/quote` 都升级到 `rsc.io/quote/v3` 吧。因为主版本号改变了,所以我们应该做好心理准备,可能会有些 API 已经被移除、重命名或者被修改成了不兼容的方式。通过阅读文档,我们得知 `Hello` 已经变成了 `HelloV3`: ```go -$ go doc rsc.io/quote/v3 +$ Go doc rsc.io/quote/v3 package quote // import "rsc.io/quote" Package quote collects pithy sayings. @@ -344,7 +344,7 @@ func Proverb() string { 然后我们再重新运行一下测试确保一切正常: ```bash -$ go test +$ Go test PASS ok example.com/hello 0.014s ``` @@ -354,7 +354,7 @@ ok example.com/hello 0.014s 我们代码中已经没有用到 `rsc.io/quote` 的地方了,但是它还是会存在 `go list -m all` 的输出和 `go.mod` 文件中: ```bash -$ go list -m all +$ Go list -m all example.com/hello golang.org/x/text v0.3.0 rsc.io/quote v1.5.2 @@ -379,8 +379,8 @@ $ 可以用 `go mod tidy` 命令来清除这些没用到的依赖项: ```bash -$ go mod tidy -$ go list -m all +$ Go mod tidy +$ Go list -m all example.com/hello golang.org/x/text v0.3.0 rsc.io/quote/v3 v3.1.0 @@ -396,7 +396,7 @@ require ( rsc.io/sampler v1.3.1 // indirect ) -$ go test +$ Go test PASS ok example.com/hello 0.020s $ @@ -409,7 +409,7 @@ Go 的模块功能将会成为未来 Go 的依赖管理系统。在所有支持 本文介绍了使用 Go 模块过程中的几个工作流程: - `go mod init` 创建了一个新的模块,初始化 `go.mod` 文件并且生成相应的描述 -- `go build, go test` 和其它构建代码包的命令,会在需要的时候在 `go.mod` 文件中添加新的依赖项 +- `go build, Go test` 和其它构建代码包的命令,会在需要的时候在 `go.mod` 文件中添加新的依赖项 - `go list -m all` 列出了当前模块所有的依赖项 - `go get` 修改指定依赖项的版本(或者添加一个新的依赖项) - `go mod tidy` 移除模块中没有用到的依赖项。 diff --git a/published/tech/20190328-introduction-to-the-modern-server-side-stack-golang-protobuf-and-grpc.md b/published/tech/20190328-introduction-to-the-modern-server-side-stack-golang-protobuf-and-grpc.md index a75c67193..b9b07c6e0 100644 --- a/published/tech/20190328-introduction-to-the-modern-server-side-stack-golang-protobuf-and-grpc.md +++ b/published/tech/20190328-introduction-to-the-modern-server-side-stack-golang-protobuf-and-grpc.md @@ -433,4 +433,4 @@ via: https://medium.com/velotio-perspectives/introduction-to-the-modern-server-s 译者:[DoubleLuck](https://github.com/DoubleLuck) 校对:[polaris1119](https://github.com/polaris1119) -本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 \ No newline at end of file +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190330-Go-Protocol-Buffer-Tutorial.md b/published/tech/20190330-Go-Protocol-Buffer-Tutorial.md index 7ef15be14..8cfeb97b4 100644 --- a/published/tech/20190330-Go-Protocol-Buffer-Tutorial.md +++ b/published/tech/20190330-Go-Protocol-Buffer-Tutorial.md @@ -50,8 +50,8 @@ Protocol buffers 是基础的数据格式,和 JSON、XML 非常的相似,都 ## 一个简单的例子 ```bash -$ go get github.com/golang/protobuf -$ go get github.com/golang/protobuf/proto +$ Go get github.com/golang/protobuf +$ Go get github.com/golang/protobuf/proto ``` 上面下载一些必须的包,用于运行简单的例子。 @@ -131,7 +131,7 @@ func main() { 在运行之前,我们需要将 `test.pb.go` 编译通过以保证正常工作: ``` -➜ src go run main.go test.pb.go +➜ src Go run main.go test.pb.go [10 6 69 108 108 105 111 116 16 24] name:"Elliot" age:24 ``` @@ -216,7 +216,7 @@ func main() { 我们来最后一次运行它,我们看到了所有我们希望输出的内容: ``` -➜ src go run main.go test.pb.go +➜ src Go run main.go test.pb.go Elliot 24 1400 diff --git a/published/tech/20190330-golang-websockets-tutorial.md b/published/tech/20190330-golang-websockets-tutorial.md index 4c7b32e8b..ff7a1bc5e 100644 --- a/published/tech/20190330-golang-websockets-tutorial.md +++ b/published/tech/20190330-golang-websockets-tutorial.md @@ -138,7 +138,7 @@ Ok,我们完成了后端基于 Go 的 WebSocket 服务器,现在该做一个 然后跑 Go 的 Websocket 服务器 ```bash -$ go run main.go +$ Go run main.go 2018/06/10 07:54:06 Serving at localhost:5000... 2018/06/10 07:54:15 on connection 2018/06/10 07:54:16 on connection diff --git a/published/tech/20190331-Concurrency-in-golang.md b/published/tech/20190331-Concurrency-in-golang.md index 941105792..8c59313a8 100644 --- a/published/tech/20190331-Concurrency-in-golang.md +++ b/published/tech/20190331-Concurrency-in-golang.md @@ -42,7 +42,7 @@ go processdataFunction() myChannel := make(chan int64) ``` -在 goroutine 等待之前,创建一个缓冲通道来允许更多的值在通道中排队,如: +在 Goroutine 等待之前,创建一个缓冲通道来允许更多的值在通道中排队,如: ```go myBufferedChannel := make(chan int64,4) diff --git a/published/tech/20190412-how-i-investigated-memory-leaks-in-go-using-pprof-on-a-large-codebase.md b/published/tech/20190412-how-i-investigated-memory-leaks-in-go-using-pprof-on-a-large-codebase.md index 616f869d7..135b4e6ba 100644 --- a/published/tech/20190412-how-i-investigated-memory-leaks-in-go-using-pprof-on-a-large-codebase.md +++ b/published/tech/20190412-how-i-investigated-memory-leaks-in-go-using-pprof-on-a-large-codebase.md @@ -32,7 +32,7 @@ Golang 提供的工具集非常出色但也有其局限性。首先来看看这 Golang 为我们提供了一个神奇的工具叫 `pprof`。掌握此工具后,可以帮助调查并发现最有可能的内存问题。它的另一个用途是查找 CPU 问题,但我不会在这篇文章中介绍任何与 CPU 有关的内容。 -## go tool pprof +## Go tool pprof 把这个工具的方方面面讲清楚需要不止一篇博客文章。我将花一点时间找出怎么使用这个工具去获取有用的东西。在这篇文章里,将集中在它的内存相关功能上。 @@ -117,7 +117,7 @@ curl -sK -v http://localhost:8080/debug/pprof/heap > heap.out 一旦收集好画像文件后,就可以将其加载到 pprof 的交互式命令行中了,通过运行: -> go tool pprof heap.out +> Go tool pprof heap.out 我们可以观察到显示的信息 diff --git a/published/tech/20190415-Advanced-testing-patterns.md b/published/tech/20190415-Advanced-testing-patterns.md index b23a65bba..5997efc24 100644 --- a/published/tech/20190415-Advanced-testing-patterns.md +++ b/published/tech/20190415-Advanced-testing-patterns.md @@ -81,8 +81,6 @@ func TestTimeConsuming(t *testing.T) { 在单元测试中你应该专注于业务逻辑,并且通过集成测试,您将验证集成服务的功能,而不是标准库或第三方软件包如何实现集成。 -> ["Go 测试: 哪个适合你 - 单元测试还是集成测试? #golang" via @TitPetric](http://twitter.com/intent/tweet?url=https%3a%2f%2fscene-si.org%2f2019%2f04%2f15%2fnext-level-go-testing%2f&text=%22Go%20testing%3a%20which%20one%20are%20right%20for%20you%20-%20unit%20or%20integration%20tests%3f%20%23golang%22&via=TitPetric) - ## 边界情况 将你的应用程序与第三方服务集成是很常见的,由于 API 弃用是可能发生的,所以集成测试可能还需要验证应用程序的响应是否仍然有意义。 因此,Peter 的文章需要一点改进。 diff --git a/published/tech/20190418-concurrency-trap-2-incomplete-work.md b/published/tech/20190418-concurrency-trap-2-incomplete-work.md index 58c2a22f6..e30d790f9 100644 --- a/published/tech/20190418-concurrency-trap-2-incomplete-work.md +++ b/published/tech/20190418-concurrency-trap-2-incomplete-work.md @@ -19,7 +19,7 @@ Jacob Walker 2019 年 4 月 18 日 ```go 5 func main() { 6 fmt.Println("Hello") -7 go fmt.Println("Goodbye") +7 Go fmt.Println("Goodbye") 8 } ``` @@ -74,7 +74,7 @@ Jacob Walker 2019 年 4 月 18 日 30 31 // Fire and Hope. 32 // BUG: We are not managing this Goroutine. -33 go a.track.Event("this event") +33 Go a.track.Event("this event") 34 } ``` @@ -126,7 +126,7 @@ https://play.golang.org/p/BMah6_C57-l 21 t.wg.Add(1) 22 23 // Track event in a Goroutine so caller is not blocked. -24 go func() { +24 Go func() { 25 26 // Decrement counter to tell Shutdown this Goroutine finished. 27 defer t.wg.Done() @@ -184,7 +184,7 @@ https://play.golang.org/p/BMah6_C57-l 42 43 // Create a Goroutine to wait for all other Goroutines to 44 // be done then close the channel to unblock the select. -45 go func() { +45 Go func() { 46 t.wg.Wait() 47 close(ch) 48 }() diff --git a/published/tech/20190418-show-down-your-codes-with-goroutines.md b/published/tech/20190418-show-down-your-codes-with-goroutines.md index 74898384a..168a82e21 100644 --- a/published/tech/20190418-show-down-your-codes-with-goroutines.md +++ b/published/tech/20190418-show-down-your-codes-with-goroutines.md @@ -1,8 +1,8 @@ 首发于:https://studygolang.com/articles/21811 -# goroutine 可能使程序变慢 +# Goroutine 可能使程序变慢 -## 如何使用 goroutine 才能使你的 CPU 满负载运行呢 +## 如何使用 Goroutine 才能使你的 CPU 满负载运行呢 下面,我们将会展示一个关于 for 循环的代码,将输入分成几个序列添加到 Goroutines 里面!我敢打赌你之前可能有过几次这种情况,但是每次引入 gorountine 都让你的代码变得更快吗? @@ -93,7 +93,7 @@ func BenchmarkConcurrentSum(b *testing.B) { 我的 CPU 是一个小型笔记本电脑 CPU (两个超线程内核,Go runtime 看作是 4 个逻辑内核),预计,并发版本应该显示出明显的速度增益,然而,真实运行速度如何呢? ``` -$ go test -bench。 +$ Go test -bench。 goos: darwin goarch: amd64 pkg: github.com/appliedgo/concurrencyslower @@ -180,7 +180,7 @@ func ChannelSum() int { ## 测试文件中增加 BenchmarkChannelSum 测试结果如下 ``` -$ go test -bench . +$ Go test -bench . goos: darwin goarch: amd64 pkg: github.com/appliedgo/concurrencyslower @@ -197,7 +197,7 @@ ok github.com/appliedgo/concurrencyslower 23.807s ## 如何获取代码 -使用 go get,注意 -d 参数阻止自动安装二进制到 $GOPATH/bin。 +使用 Go get,注意 -d 参数阻止自动安装二进制到 $GOPATH/bin。 ``` go get -d github.com/appliedgo/concurrencyslower @@ -219,7 +219,7 @@ go test -bench . ```$GOPATH/pkg/mod/github.com/appliedgo/concurrencyslower@``` -如果 $GOPATH 丢失,默认使用 go get ~/go 或者 %USERPROFILE%\go +如果 $GOPATH 丢失,默认使用 Go get ~/go 或者 %USERPROFILE%\go ## 结论 diff --git a/published/tech/20190513-go-should-i-use-a-pointer-instead-of-a-copy-of-my-struct.md b/published/tech/20190513-go-should-i-use-a-pointer-instead-of-a-copy-of-my-struct.md index 58da88793..4d108de1d 100644 --- a/published/tech/20190513-go-should-i-use-a-pointer-instead-of-a-copy-of-my-struct.md +++ b/published/tech/20190513-go-should-i-use-a-pointer-instead-of-a-copy-of-my-struct.md @@ -219,14 +219,14 @@ func BenchmarkMemoryHeap(b *testing.B) { ``` name time/op -MemoryHeap-4 301µs ± 4% +MemoryHeap-4 301 µ s ± 4% name alloc/op MemoryHeap-4 0.00B name allocs/op MemoryHeap-4 0.00 ------------------ name time/op -MemoryStack-4 595µs ± 2% +MemoryStack-4 595 µ s ± 2% name alloc/op MemoryStack-4 0.00B name allocs/op diff --git a/published/tech/20190515-Garbage-Collection-In-Go-Part-II-GC-Traces.md b/published/tech/20190515-Garbage-Collection-In-Go-Part-II-GC-Traces.md index c3c946077..c20807873 100644 --- a/published/tech/20190515-Garbage-Collection-In-Go-Part-II-GC-Traces.md +++ b/published/tech/20190515-Garbage-Collection-In-Go-Part-II-GC-Traces.md @@ -38,7 +38,7 @@ https://github.com/ardanlabs/gotraining/tree/master/topics/go/profiling/project) **清单 1** ```bash -$ go build +$ Go build $ GOGC=off ./project > /dev/null ``` @@ -256,7 +256,7 @@ if strings.Contains(item.Description, term) { **清单 14** ```bash -$ go build +$ Go build $ GODEBUG=gctrace=1 ./project > /dev/null gc 3 @6.156s 0%: 0.011+0.72+0.068 ms clock, 0.13+0.21/1.5/3.2+0.82 ms CPU, 4->4->2 MB, 5 MB Goal, 12 P . diff --git a/published/tech/20190521-Go-Is-the-encoding-json-Package-Really-Slow.md b/published/tech/20190521-Go-Is-the-encoding-json-Package-Really-Slow.md new file mode 100644 index 000000000..5a11a4241 --- /dev/null +++ b/published/tech/20190521-Go-Is-the-encoding-json-Package-Really-Slow.md @@ -0,0 +1,311 @@ +首发于:https://studygolang.com/articles/25100 + +# Go 标准库 `encoding/json` 真的慢吗? + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-is-the-encoding-json-package-really-slow/A-Journey-With-Go.png) + +插图来自于“A Journey With Go”,由 Go Gopher 组织成员 Renee French 创作。 + +本文基于 Go 1.12。 + +关于标准库 `encoding/json` 性能差的问题在很多地方被讨论过,也有很多第三方库在尝试解决这个问题,比如[easyjson](https://github.com/mailru/easyjson),[jsoniter](https://github.com/json-iterator/go)和[ffjson](https://github.com/pquerna/ffjson)。但是标准库 `encoding/json` 真的慢吗?它一直都这么慢吗? + +## 标准库 `encoding/json` 的进化之路 + +首先,通过一个简短的 makefile 文件和一段基准测试代码,我们看下在各个 Go 版本中,标准库 `encoding/json` 的性能表现。以下为基准测试代码: + +```go +type JSON struct { + Foo int + Bar string + Baz float64 +} + +func BenchmarkJsonMarshall(b *testing.B) { + j := JSON{ + Foo: 123, + Bar: `benchmark`, + Baz: 123.456, + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = json.Marshal(&j) + } +} + +func BenchmarkJsonUnmarshal(b *testing.B) { + bytes := `{"foo": 1, "bar": "my string", bar: 1.123}` + str := []byte(bytes) + b.ResetTimer() + for i := 0; i < b.N; i++ { + j := JSON{} + _ = json.Unmarshal(str, &j) + } +} +``` + +makefile 文件在不同的文件夹中基于不同版本的 Go 创建 Docker 镜像,在各镜像启动的容器中运行基准测试。将从以下两个维度进行性能对比: + +* 比较 Go 各版本与 1.12 版本中标准库 `encoding/json` 的性能差异 +* 比较 Go 各版本与其下一个版本中标准库 `encoding/json` 的性能差异 + +第一个维度的对比可以得到在特定版本的 Go 与 1.12 版本的 Go 中 JSON 序列化和反序列化的性能差异;第二个维度的对比可以得到在哪次 Go 版本升级中 JSON 序列化和反序列化发生了最大的性能提升。 + +测试结果如下: + +* Go1.2 至 Go1.3 的版本升级,序列化操作耗时减少了约 28%,反序列化操作耗时减少了约 35% + +```bash +name old time/op new time/op delta +JsonMarshall 1.91 µ s ± 2% 1.37 µ s ± 2% -28.23% +JsonUnmarshal 2.70 µ s ± 2% 1.75 µ s ± 3% -35.18% +``` + +* Go1.6 至 Go1.7 的版本升级,序列化操作耗时减少了约 27%,反序列化操作耗时减少了约 40% + +```bash +name old time/op new time/op delta +JsonMarshall-4 1.24 µ s ± 1% 0.90 µ s ± 2% -27.65% +JsonUnmarshal-4 1.52 µ s ± 3% 0.91 µ s ± 2% -40.05% +``` + +* Go1.10 至 Go1.11 的版本升级,序列化内存消耗减少了约 60%,反序列化内存消耗减少了约 25% + +```bash +name old alloc/op new alloc/op delta +JsonMarshall-4 208B ± 0% 80B ± 0% -61.54% +JsonUnmarshal-4 496B ± 0% 368B ± 0% -25.81% +``` + +* Go1.11 至 Go1.12 的版本升级,序列化操作耗时减少了约 15%,反序列化操作耗时减少了约 6% + +```bash +name old time/op new time/op delta +JsonMarshall-4 670ns ± 6% 569ns ± 2% -15.09% +JsonUnmarshal-4 800ns ± 1% 747ns ± 1% -6.58% +``` + +可以在这里看到完整的[测试结果](https://gist.github.com/blanchonvincent/227b6691777a1de254ce75b304a36277)。 + +如果对比 Go1.2 与 Go1.12,会发现标准库 `encoding/json` 的性能有显著提高,操作耗时减少了约 69%/68%,内存消耗减少了约 74%/29%: + +```bash +name old time/op new time/op delta +JsonMarshall 1.72 µ s ± 2% 0.52 µ s ± 2% -69.68% +JsonUnmarshal 2.72 µ s ± 2% 0.85 µ s ± 5% -68.70% + +name old alloc/op new alloc/op delta +JsonMarshall 188B ± 0% 48B ± 0% -74.47% +JsonUnmarshal 519B ± 0% 368B ± 0% -29.09% +``` + +该基准测试使用了较为简单的 JSON 结构。使用更加复杂的结构(例如:Map or Array)进行测试会导致各版本之间性能增幅与本文不同。 + +## 速读源码 + +想了解标准库性能较差的原因的最好的办法就是读源码,以下为 Go1.12 版本中 `json.Marshal` 函数的执行流程: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-is-the-encoding-json-package-really-slow/json-marshal.png) + +在了解了 `json.Marshal` 函数的执行流程后,再来比较下在 Go1.10 和 Go1.12 版本中的 `json.Marshal` 函数在实现上有什么变化。通过之前的测试,可以发现从 Go1.10 至 Go1.12 版本中的 `json.Marshal` 函数的内存消耗上有了很大的改善。从源码的变化中可以发现在 Go1.12 版本中的 `json.Marshal` 函数添加了 encoder(编码器)的内存缓存: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/go-is-the-encoding-json-package-really-slow/json-marshal-diff.png) + +在使用了 `sync.Pool` 缓存 encoder 后,`json.Marshal` 函数极大地减少了内存分配操作。实际上 `newEncodeState()` 函数在 Go1.10 版本中就[已经存在了](https://github.com/golang/go/commit/c0547476f342665514904cf2581a62135d2366c3#diff-e79d4db81e8544657cb631be813f89b4),只不过没有被使用。为验证是添加了内存缓存带来了性能提升的猜想,可以在 Go1.10 版本中修改 `json.Marshal` 函数后,再进行测试: + +```bash +name old alloc/op new alloc/op delta +CodeMarshal-4 4.59MB ± 0% 1.98MB ± 0% -56.92% +``` + +可以直接在[Go 源码](https://github.com/golang/go)中,执行以下命令进行基准测试: + +```bash +go test encoding/json -bench=BenchmarkCodeMarshal -benchmem -count=10 -run=^$ +``` + +结果和我们的猜想是一致的。是[sync 包](https://golang.org/pkg/sync/)给 `json.Marshal` 函数带来了性能提升。同样也给我们带来一点启发,当项目也有这种对同一个结构体进行大量的内存分配时,也可以通过添加内存缓存的方式提升性能。 + +以下为 Go1.12 版本中,`json.Unmarshal` 函数的执行流程: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/go-is-the-encoding-json-package-really-slow/json-unmarshal.png) + +`json.Unmarshal` 函数同样使用 `sync.Pool` 缓存了 decoder。 +对于 JSON 序列化和反序列化而言,其性能瓶颈是迭代、反射 JSON 结构中每个字段。 + +## 与第三方库的性能对比 + +GitHub 上也有很多用于 JSON 序列化的第三方库,比如[ffjson](https://github.com/pquerna/ffjson)就是其中之一,ffjson 的命令行工具可以为指定的结构生成静态的 MarshalJSON 和 UnmarshalJSON 函数,MarshalJSON 和 UnmarshalJSON 函数在序列化和反序列化操作时会分别被 `ffjson.Marshal` 和 `ffjson.Unmarshal` 函数调用。以下为 ffjson 生成的解析器示例: + +```go +func (j *JSONFF) MarshalJSON() ([]byte, error) { + var buf fflib.Buffer + if j == nil { + buf.WriteString("null") + return buf.Bytes(), nil + } + err := j.MarshalJSONBuf(&buf) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// MarshalJSONBuf marshal buff to JSON - template +func (j *JSONFF) MarshalJSONBuf(buf fflib.EncodingBuffer) error { + if j == nil { + buf.WriteString("null") + return nil + } + var err error + var obj []byte + _ = obj + _ = err + buf.WriteString(`{"Foo":`) + fflib.FormatBits2(buf, uint64(j.Foo), 10, j.Foo < 0) + buf.WriteString(`,"Bar":`) + fflib.WriteJsonString(buf, string(j.Bar)) + buf.WriteString(`,"Baz":`) + fflib.AppendFloat(buf, float64(j.Baz), 'g', -1, 64) + buf.WriteByte('}') + return nil +} +``` + +现在比较一下标准库和 ffjson(使用了 `ffjson.Pool()`)的性能差异: + +```bash +standard lib: +name time/op +JsonMarshall-4 500ns ± 2% +JsonUnmarshal-4 677ns ± 2% + +name alloc/op +JsonMarshall-4 48.0B ± 0% +JsonUnmarshal-4 320B ± 0% + +ffjson: +name time/op +JsonMarshallFF-4 538ns ± 1% +JsonUnmarshalFF-4 827ns ± 3% + +name alloc/op +JsonMarshallFF-4 176B ± 0% +JsonUnmarshalFF-4 448B ± 0% +``` + +对于 JSON 序列化/反序列化,标准库与 ffjson 相比反而更加高效一些。 + +对于内存使用情况(堆分配),可以通过 `go run -gcflags="-m"` 命令进行测试: + +```bash +:46:19: buf escapes to heap +:48:23: buf escapes to heap +:27:26: &buf escapes to heap +:22:6: moved to heap: buf +``` + +[easyjson](https://github.com/mailru/easyjson)库也使用了和 ffjson 同样的策略,以下为基准测试结果: + +```bash +standard lib: +name time/op +JsonMarshall-4 500ns ± 2% +JsonUnmarshal-4 677ns ± 2% + +name alloc/op +JsonMarshall-4 48.0B ± 0% +JsonUnmarshal-4 320B ± 0% + +easyjson: +name time/op +JsonMarshallEJ-4 349ns ± 1% +JsonUnmarshalEJ-4 341ns ± 5% + +name alloc/op +JsonMarshallEJ-4 240B ± 0% +JsonUnmarshalEJ-4 256B ± 0% +``` + +这次,easyjson 比标准库更高效些,对于 JSON 序列化有 30%的性能提升,对于 JSON 反序列化性能提升接近 2 倍。通过阅读 `easyjson.Marshal` 的源码,可以发现它高效的原因: + +```go +func Marshal(v Marshaler) ([]byte, error) { + w := jwriter.Writer{} + v.MarshalEasyJSON(&w) + return w.BuildBytes() +} +``` + +通过 easyjson 的命令行工具生成的编码器 `MarshalEasyJSON` 方法可用于 JSON 序列化: + +```go +func easyjson42239ddeEncode(out *jwriter.Writer, in JSON) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"Foo\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Int(int(in.Foo)) + } + { + const prefix string = ",\"Bar\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.String(string(in.Bar)) + } + { + const prefix string = ",\"Baz\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Float64(float64(in.Baz)) + } + out.RawByte('}') +} + +func (v JSON) MarshalEasyJSON(w *jwriter.Writer) { + easyjson42239ddeEncode(w, v) +} +``` + +正如我们所见,这里没有使用反射。整体流程也很简单。而且,easyjson 也可以兼容标准库: + +```go +func (v JSON) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson42239ddeEncodeGithubComMyCRMTeamEncodingJsonEasyjson(&w, v) + return w.Buffer.BuildBytes(), w.Error +} +``` + +然而,使用这种兼容标准库的方式进行序列化会比直接使用标准库性能更差,因为在进行 JSON 序列化的过程中,标准库依然会通过反射构造 encoder,且 `MarshalJSON` 中这一段代码也会被执行。 + +## 结论 + +无论在标准库上做多少努力,它都不会比通过**对明确的 JSON 结构生成 encoder/decoder**的方式性能好。而通过结构生成解析器代码的方式需要生成和维护此代码,并且依赖于外部的库。 + +在做出使用第三方序列化库替换标准库的决定前,最好先测试下 JSON 序列化和反序列化是否是应用的性能瓶颈点,提高 JSON 序列化的效率是否能改善应用的性能。如果 JSON 序列化和反序列化并不是应用的性能瓶颈点,为了极少的性能提升,付出第三方库的维护成本是不值得的。毕竟,在大多数业务场景下,Go 的标准库 `encoding/json` 已经足够高效了。 + +--- + +via: https://medium.com/a-journey-with-go/go-is-the-encoding-json-package-really-slow-62b64d54b148 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[beiping96](https://github.com/beiping96) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190529-GO-MEMORY-MANAGEMENT-Part3.md b/published/tech/20190529-GO-MEMORY-MANAGEMENT-Part3.md index 6c8de1b83..404f9d081 100644 --- a/published/tech/20190529-GO-MEMORY-MANAGEMENT-Part3.md +++ b/published/tech/20190529-GO-MEMORY-MANAGEMENT-Part3.md @@ -75,9 +75,9 @@ func main() { cgo 程序和 Go 原生程序的追踪结果并没有太大差异。当然,我注意到有一些统计还是有点差别的。比如,cgo 程序没有包含堆的统计信息。 -![cgo 程序的追踪信息统计](https://github.com/studygolang/gctt-images/blob/master/go-memory-management-part-3/cgo.png?raw=true)
cgo 程序的追踪信息统计 +![cgo 程序的追踪信息统计](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-memory-management-part-3/cgo.png)
cgo 程序的追踪信息统计 -![Go 原生程序的追踪信息统计](https://github.com/studygolang/gctt-images/blob/master/go-memory-management-part-3/noncgo.png?raw=true)
Go 原生程序的追踪信息统计 +![Go 原生程序的追踪信息统计](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-memory-management-part-3/noncgo.png)
Go 原生程序的追踪信息统计 我试图使用不同的视图去观察,但是并没有发现更多的显著差异。我猜测这可能是由于 Go 不会为已编译的 C 代码添加追踪指令。 diff --git a/published/tech/20190530-Benefits-of-Using-Microservice-Architecture-in-Go.md b/published/tech/20190530-Benefits-of-Using-Microservice-Architecture-in-Go.md index 632ba7211..716f574c2 100644 --- a/published/tech/20190530-Benefits-of-Using-Microservice-Architecture-in-Go.md +++ b/published/tech/20190530-Benefits-of-Using-Microservice-Architecture-in-Go.md @@ -3,50 +3,56 @@ # 在 Go 中使用微服务架构的好处 ## 前言 + 我们已经讨论“微服务架构”很长一段时间了。它是软件架构中最新的热门话题。那么什么是微服务呢?我们为什么要使用它?为什么要在 Golang 中使用微服务架构?它有哪些优点? 本文中,我将会探讨一些相关的问题。废话不多说,让我们开始吧。 ## 什么是微服务? + 微服务是一种软件开发技术,属于 SOA(面向服务的架构)的一种形式。它的作用是,将应用程序构建为许多松耦合的服务的集合。在这种架构中,服务的编码通常是细粒度的,服务的协议更轻量。目前还没有对微服务的准确定义,但它有一些显著的特征:自动化部署、业务功能、去中心化的数据管理和智能端点。 (译者注:对于这个定义有兴趣的同学,不妨去看看 Martin 大神[关于微服务的文章](https://martinfowler.com/articles/microservices.html) ## 我们为什么使用微服务? + 这种架构有助于我们用各部分、小型模块描绘整个应用程序,使其更容易理解、开发和测试;有助于我们将各个服务视为独立且又清晰指明其用途的服务。更进一步地,它有助于保持项目架构的一致性(最初设计的架构和实际开发完成的架构差别不大)。它还可以通过建立不同的独立团队来进行服务的部署和扩展,从而各团队能够并行地开发。在这个架构中重构代码更容易。它也支持连续交付和部署流程(CI/CD)。 -## 为什么使用 go 构建微服务? -在深入研究这个问题之前。首先,我说一下 Golang 的优势。虽然 Golang 是一门新的语言,但是与其他语言相比,它有很多优势。用 Golang 编写的程序更加健壮。它们能够承受程序使用运行的服务构建的繁重负载。Golang 更适合多处理器系统和 web 应用程序。此外,它容易地与 GitHub 集成,管理非集中的代码包。微服务架构的用处大部分体现在当程序需要伸缩(scalable)时。如果有一种语言可以完全符合标准,那么它就是 Golang。原因是它继承自 C-family 编程语言,用 Golang 编写的组件更容易与同一家族中其他语言编写的组件相结合。 +## 为什么使用 Go 构建微服务? -尽管 Go 出身于 C-family,但它比 C / C ++更高效。 它语法更简单,有点像 Python。它稳定语法, 自第一次公开发布以来,它没有太大变化,也就是说它是后向兼容的。与其他语言相比,这让 golang 占了上风。 除此之外,Golang 的性能比 python 和 java 高出不少。锦上添花的是,它又像 C/C++ 简单的同时又易于阅读和理解,使它成为开发微服务应用的绝佳选择。 +在深入研究这个问题之前。首先,我说一下 Golang 的优势。虽然 Golang 是一门新的语言,但是与其他语言相比,它有很多优势。用 Golang 编写的程序更加健壮。它们能够承受程序使用运行的服务构建的繁重负载。Golang 更适合多处理器系统和 Web 应用程序。此外,它容易地与 GitHub 集成,管理非集中的代码包。微服务架构的用处大部分体现在当程序需要伸缩(scalable)时。如果有一种语言可以完全符合标准,那么它就是 Golang。原因是它继承自 C-family 编程语言,用 Golang 编写的组件更容易与同一家族中其他语言编写的组件相结合。 + +尽管 Go 出身于 C-family,但它比 C / C ++ 更高效。 它语法更简单,有点像 Python。它稳定语法, 自第一次公开发布以来,它没有太大变化,也就是说它是后向兼容的。与其他语言相比,这让 Golang 占了上风。 除此之外,Golang 的性能比 python 和 java 高出不少。锦上添花的是,它又像 C/C++ 简单的同时又易于阅读和理解,使它成为开发微服务应用的绝佳选择。 + +## Golang 中的微服务架构框架 -## Golang中的微服务架构框架 下面,我们讨论一下可以用于微服务架构的框架。有以下些框架: ### Go Micro -Go Micro 是目前为止我遇到的最流行的RPC框架。它是一个可插拔的RPC框架。Go Micro 为我们提供了以下功能: + +Go Micro 是目前为止我遇到的最流行的 RPC 框架。它是一个可插拔的 RPC 框架。Go Micro 为我们提供了以下功能: * 服务发现: 程序自动注册到服务发现系统 * 负载均衡: 它提供了客户端负载均衡,这有助于平衡服务实例之间的请求 * 同步通信: 提供 Request/Response 传输层 * 异步通信: 具有内置的发布和订阅功能 * 消息编码: 可以利用 header 中 Content-Type 进行编码和解码 -* RPC客户端/服务器端: 利用上述功能并提供构建微服务需要的接口 - -![](https://camo.githubusercontent.com/9057599d2bc2d3c79c43423521d71f4ea0851457/68747470733a2f2f6d6963726f2e6d752f646f63732f696d616765732f676f2d6d6963726f2e737667) +* RPC 客户端/服务器端: 利用上述功能并提供构建微服务需要的接口 Go Micro 架构由三层组成。第一层抽象为服务层。第二层为 client-server 模型层。serrver 用于编写服务的块组成,而 client 为我们提供接口,其唯一目的是向 server model 中编写的服务发出请求。 第三层有以下类型的插件: + * Broker: 在异步通信中为 message broker(消息代理)提供接口 * Codec: 用于加密或解密消息 * Registry: 提供服务搜索功能 * Selector: 在 register 上构建了负载均衡 -* Transport: Transport是服务与服务之间同步请求/响应的通信接口 +* Transport: Transport 是服务与服务之间同步请求/响应的通信接口 + +它还提供了一个名为 Sidecar 的功能。Sidecar 使您能够集成以 Go 以外的语言编写的服务。它还为我们提供了 gRPC 编码/解码、服务注册和 HTTP 请求处理 -它还提供了一个名为 Sidecar 的功能。Sidecar 使您能够集成以Go以外的语言编写的服务。它还为我们提供了gRPC编码/解码、服务注册和HTTP 请求处理 +### Go Kit -### GO Kit -Go Kit 是一个用于构建微服务的编程工具包。与 Go Micro不同,它是一个可以以二进制包导入的库。Go Kit 规则很简单。如下: +Go Kit 是一个用于构建微服务的编程工具包。与 Go Micro 不同,它是一个可以以二进制包导入的库。Go Kit 规则很简单。如下: * 没有全局变量 * 声明式组合 @@ -59,24 +65,25 @@ Go Kit 提供以下代码包: * Authentication 鉴权: BasicAuth 和 JWT * Transport 协议: HTTP, gRPC 等 * Logging 日志: 服务中的结构化日志接口 -* Metrics 度量: CloudWatch,Statsd, Graphite等 +* Metrics 度量: CloudWatch,Statsd, Graphite 等 * Tracing 分布式追踪: Zipkin and Opentracing -* Service discovery 服务发现: Consul, Etcd, Eureka等 +* Service discovery 服务发现: Consul, Etcd, Eureka 等 * Circuitbreaker 限流熔断: Hystrix 在 Go 语言的实现 Go Kit 服务架构如下 ## Gizmo + Gizmo 是来自《纽约时报》的一个微服务工具包。它提供了将服务器守护进程和 pubsub 守护进程放在一起的包。它公开了以下包: * Server: 提供两个服务器实现: SimpleServer(HTTP)和 RPCServer(gRPC) -* Server/kit: 基于Go Kit的实验代码包 -* Config 配置: 包含来自 JSON文件、Consul k/v 中的 JSON blob 或环境变量的配置功能 +* Server/kit: 基于 Go Kit 的实验代码包 +* Config 配置: 包含来自 JSON 文件、Consul k/v 中的 JSON blob 或环境变量的配置功能 * Pubsub: 提供用于从队列中发布和使用数据的通用接口 * Pubsub/pubsubtest: 包含发布者和订阅者接口的测试实现 * Web: 用于从请求查询和有效负载解析类型的外部函数 -Pubsub包提供了处理以下队列的接口: +Pubsub 包提供了处理以下队列的接口: * pubsub/aws: 用于 Amazon SNS/SQS * pubsub/gcp: 用于 Google Pubsub @@ -86,15 +93,17 @@ Pubsub包提供了处理以下队列的接口: 所以,在我看来,Gizmo 介于 Go Micro 和 Go Kit 之间。它不像 Go Micro 那样是一个完全的黑盒。同时,它也不像 Go Kit 那么原始。它提供了更高级别的构建组件,比如配置和 pubsub 包 ## Kite -Kite 是一个在 Go 中开发微服务的框架。它公开RPC client 和 Server 端代码包。创建的服务将自动注册到服务发现系统 Kontrol。Kontrol 是用 Kite 编写的,它本身就是一个 Kite service。这意味着 Kite 微服务在自身的环境中运行良好。如果需要将 Kite 微服务连接到另一个服务发现系统,则需要定制。这是我从列表中选择 Kite 并决定不介绍这个框架的重要原因之一 -因此,如果您觉得这个博客很有用,并且想知道如何在 Golang 中创建多功能的微服务,那么请从我们这里[雇佣 Golang开发者](https://www.bacancytechnology.com/hire-golang-developer?source=post_page---------------------------),并学习利用顶级的专业知识。 +Kite 是一个在 Go 中开发微服务的框架。它公开 RPC client 和 Server 端代码包。创建的服务将自动注册到服务发现系统 Kontrol。Kontrol 是用 Kite 编写的,它本身就是一个 Kite service。这意味着 Kite 微服务在自身的环境中运行良好。如果需要将 Kite 微服务连接到另一个服务发现系统,则需要定制。这是我从列表中选择 Kite 并决定不介绍这个框架的重要原因之一 + +因此,如果您觉得这个博客很有用,并且想知道如何在 Golang 中创建多功能的微服务,那么请从我们这里[雇佣 Golang 开发者](https://www.bacancytechnology.com/hire-golang-developer),并学习利用顶级的专业知识。 --- + via: https://medium.com/datadriveninvestor/benefits-of-using-microservice-architecture-in-go-4440e4fcd9c9 作者:[Katy Slemon](https://medium.com/@katyslemon) 译者:[Alex1996a](https://github.com/Alex1996a) 校对:[dingdingzhou](https://github.com/zhoudingding) -本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 \ No newline at end of file +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190602-Go-How-Does-the-Goroutine-Stack-Size-Evolve.md b/published/tech/20190602-Go-How-Does-the-Goroutine-Stack-Size-Evolve.md new file mode 100644 index 000000000..c03201354 --- /dev/null +++ b/published/tech/20190602-Go-How-Does-the-Goroutine-Stack-Size-Evolve.md @@ -0,0 +1,293 @@ +首发于:https://studygolang.com/articles/28971 + +# Go: Goroutine 的堆栈大小是如何演进的 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/how-goroutine-stack-size-evolve/cover.png) + +> Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French. + +*本文基于 Go 1.12* + +Go 提供了一套简单且智能的协程管理,简单是因为协程在最开始的时候只有 2Kb 大小,智能是指协程的大小是能随着实际情况变大缩小的。 + +至于堆栈的大小,我们可以在 `runtime/stack.go` 找到以下注释: + +```go +// The minimum size of stack used by Go code_StackMin = 2048 +``` + +我们需要注意,在不同的版本,有些设定是不一样的,比如: + +* [Go 1.2](https://golang.org/doc/go1.2#stacks) :协程的堆栈大小从 4Kb 增加到 8Kb。 +* [Go 1.4](https://golang.org/doc/go1.4#runtime) :协程的堆栈大小从 8Kb 减小到 2Kb。 + +堆栈的大小之所以改变是因为堆栈分配的策略改变了,这个话题我们待会还会谈到。 + +## 动态堆栈大小 + +如果 Go 能自动地增大其堆栈的大小,也就是说它能判断是否需要分配堆栈。让我们用下面这个例子来分析下协程是如何工作的。 + +```go +func main() { + a := 1 + b := 2 + + r := max(a, b) + println(`max: `+strconv.Itoa(r)) +} + +func max(a int, b int) int { + if a >= b { + return a + } + + return b +} +``` + +这个例子演示了取得两个整数中较大的数字。为了知道 Go 如何管理协程的堆栈,我们可以看下 Go 的汇编代码,使用命令 `go build -gcflags -S main.go`。整理得到有关堆栈分配的输出,它们能告诉我们 Go 都做了什么? + +```bash +"".main STEXT size=186 args=0x0 locals=0x70 + 0x0000 00000 (/go/src/main.go:5) TEXT "".main(SB), ABIInternal, $112-0 + [...] + 0x00b0 00176 (/go/src/main.go:5) CALL runtime.morestack_noctxt(SB) +[...] +0x0000 00000 (/go/src/main.go:13) TEXT "".max(SB), NOSPLIT|ABIInternal, $0-24 +``` + +这里有两个指令涉及到堆栈变化的: + +* `CALL runtime.morestack_noctxt`:这个方法会根据需要分配更多堆栈。 + +* `NOSPLIT` 这个指令代表不需要栈溢出检查。与其相似的有 [编译指令](https://golang.org/cmd/compile/) `//go:nosplit`。 +如果我们查阅方法 `runtime.morestack_noctxt`,它会从 `runtime/stack.go` 调用方法 `newstack`: + +```go +func newstack() { + [...] + // Allocate a bigger segment and move the stack. + oldsize := gp.stack.hi - gp.stack.lo + newsize := oldsize * 2 + if newsize > maxstacksize { + print("runtime: Goroutine stack exceeds ", maxstacksize, "-byte limit\n") + throw("stack overflow") + } + + // The Goroutine must be executing in order to call newstack, + // so it must be Grunning (or Gscanrunning). + casgstatus(gp, _Grunning, _Gcopystack) + + // The concurrent GC will not scan the stack while we are doing the copy since + // the gp is in a Gcopystack status. + copystack(gp, newsize, true) + if stackDebug >= 1 { + print("stack grow done\n") + } + casgstatus(gp, _Gcopystack, _Grunning) +} +``` + +当前堆栈的大小是通过 `gp.stack.hi` 与 `gp.stack.lo` 相减得到的,他们分别指向堆栈的起始位置和结束位置。 + +```go +type stack struct { + lo uintptr + hi uintptr +} +``` + +然后,将当前大小乘以 2 并检查它是否超过允许的最大值(该大小取决于平台): + +```go +// 64 位系统最大值为 1GB,32 位系统为 250MB. +// 使用十进制而不是二进制的 GB,MB 是因为 +// 它们在堆溢出的报错信息中看起来更合适 +if sys.PtrSize == 8 { + maxstacksize = 1000000000 +} else { + maxstacksize = 250000000 +} +``` + +现在我们知道了流程,那么我们可以写个简单的例子来验证。为了方便进行调试,我们将把在 `thenewstack` 方法中看到的常量 `stackDebug` 设置为 1 并运行。 + +```go +func main() { + var x [10]int + a(x) +} + +//go:noinline +func a(x [10]int) { + println(`func a`) + var y [100]int + b(y) +} + +//go:noinline +func b(x [100]int) { + println(`func b`) + var y [1000]int + c(y) +} + +//go:noinline +func c(x [1000]int) { + println(`func c`) +} +``` + +指令 *//go:noinline* 可以避免在 `main` 函数中,内联所有函数。如果内联是由编译器完成的,我们将不会看到每个函数中堆栈的动态增长。 + +这里是部分调试信息: + +```go +runtime: newstack sp=0xc00002e6d8 stack=[0xc00002e000, 0xc00002e800] +stack grow done +func a +runtime: newstack sp=0xc000076888 stack=[0xc000076000, 0xc000077000] +stack grow done +runtime: newstack sp=0xc00003f888 stack=[0xc00003e000, 0xc000040000] +stack grow done +runtime: newstack sp=0xc000081888 stack=[0xc00007e000, 0xc000082000] +stack grow done +func b +runtime: newstack sp=0xc0000859f8 stack=[0xc000082000, 0xc00008a000] +func c +``` + +我们可以看到堆栈大小增大了 4 次,实际上,函数会根据需要尽可能地增加堆栈。正如我们在代码中看到的,堆栈大小由堆栈的边界定义,所以我们可以计算新分配的堆栈的大小,根据指令 `newstack stack=[...]` 提供的当前堆栈边界的指针。 + +```bash +runtime: newstack sp=0xc00002e6d8 stack=[0xc00002e000, 0xc00002e800] +0xc00002e800 - 0xc00002e000 = 2048 +runtime: newstack sp=0xc000076888 stack=[0xc000076000, 0xc000077000] +0xc000077000 - 0xc000076000 = 4096 +runtime: newstack sp=0xc00003f888 stack=[0xc00003e000, 0xc000040000] +0xc000040000 - 0xc00003e000 = 8192 +runtime: newstack sp=0xc000081888 stack=[0xc00007e000, 0xc000082000] +0xc000082000 - 0xc00007e000 = 16384 +runtime: newstack sp=0xc0000859f8 stack=[0xc000082000, 0xc00008a000] +0xc00008a000 - 0xc000082000 = 32768 +``` + +对协程内部的分析告诉了我们协程最开始的大小的确只有 2Kb,并且尽可能的在函数逻辑,编译阶段扩大大小,知道内存饱和,或者堆栈大小达到最大值。 + +## 堆栈分配管理 + +动态分配系统并不是影响我们的应用程序的唯一因素。它的分配方式也会产生巨大的影响。让我们试着从两个堆栈增长的跟踪记录来理解它是如何管理的。 + +```bash +runtime: newstack sp=0xc00002e6d8 stack=[0xc00002e000, 0xc00002e800] +copystack gp=0xc000000300 [0xc00002e000 0xc00002e6e0 0xc00002e800] -> [0xc000076000 0xc000076ee0 0xc000077000]/4096 +stackfree 0xc00002e000 2048 +stack grow done +runtime: newstack sp=0xc000076888 stack=[0xc000076000, 0xc000077000] +copystack gp=0xc000000300 [0xc000076000 0xc000076890 0xc000077000] -> [0xc00003e000 0xc00003f890 0xc000040000]/8192 +stackfree 0xc000076000 4096 +stack grow done +``` + +第一个指令显示当前堆栈的地址,`stack=[0xc00002e000, 0xc00002e800]` ,然后把它复制到一个两倍大的新空间上 `copystack [0xc00002e000 [...] 0xc00002e800] -> [0xc000076000 [...] 0xc000077000]`,4096 bits 长度,就和我们之前看到的一样。之前的堆栈现在被释放了,`0xc00002e000`。通过下方的图,可以帮助更直观的观察正在发生的事情 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/how-goroutine-stack-size-evolve/1.png) + +指令 `copystack` 复制整个堆栈,并将所有内容移动到这个新分配的堆栈。我们可以通过以下简单的代码来地验证这个逻辑: + +```go +func main() { + var x [10]int + println(&x) + a(x) + println(&x) +} +``` + +打印得到的地址: + +```bash +0xc00002e738 +[...] +0xc000089f38 +``` + +地址 `0xc00002e738` 包含在我们在调试跟踪中看到的第一个堆栈地址 `stack=[0xc00002e000, 0xc00002e800]` 中,而 `0xc000089f38` 包含在最后一个堆栈边界 `stack=[0xc000082000, 0xc00008a000]` 中。它证实了所有的值已经从一个堆栈移动到另一个堆栈。 + +另外,有趣的是,当触发 GC 时,堆栈将视情况而缩小。 + +在我们的例子中,所有函数调用后,除了 `main` 函数中的堆栈外,不再有其它堆栈,因此当 GC 运行时系统将会缩小堆栈。为此,我们可以强制 GC 运行。 + +```go +func main() { + var x [10]int + println(&x) + a(x) + runtime.GC() + println(&x) +} +``` + +调试信息显示了堆栈的缩小。 + +```bash +func c +shrinking stack 32768->16384 +copystack gp=0xc000000300 [0xc000082000 0xc000089e60 0xc00008a000] -> [0xc00007e000 0xc000081e60 0xc000082000]/16384 +``` + +如我们所见,堆栈大小已除以 2 ,并重新使用了先前的堆栈地址 `stack = [0xc00007e000,0xc000082000]`。 再一次,我们可以在 `runtime/stack.go-rinklestack()` 中看到,收缩后的大小总是将当前大小除以 2: + +```go +oldsize := gp.stack.hi - gp.stack.lo +newsize := oldsize / 2 +``` + +## 连续堆栈 VS 分段堆栈 + +把当前堆栈复制到另一个更大的堆栈空间的策略称为连续堆栈,相反的还有分段堆栈策略。Go 在 1.3 版本开始使用连续堆栈策略,为了看到两种策略的差异,我们将在 Go 1.2 的环境下运行同一个例子。同样,我们需要更新常量 `stackDebug` 以输出调试信息。由于在 1.2 版本的 `runtime` 是 C 编写的,因此我们不得不[编译源代码](https://golang.org/doc/install/source#install)。 结果如下: + +```bash +func a +runtime: newstack framesize=0x3e90 argsize=0x320 sp=0x7f8875953848 stack=[0x7f8875952000, 0x7f8875953fa0] + -> new stack [0xc21001d000, 0xc210021950] +func b +func c +runtime: oldstack gobuf={pc:0x400cff sp:0x7f8875953858 lr:0x0} cret=0x1 argsize=0x320 +``` + +当前堆栈大小为 8Kb `stack=[0x7f8875952000, 0x7f8875953fa0]`,8192 字节加上堆栈头信息。新创建的堆栈大小为 18864 字节大小(18768 字节加上堆栈头信息),内存分配信息如下: + +```bash +// allocate new segment. +framesize += argsize; +framesize += StackExtra; // room for more functions, Stktop. +if(framesize < StackMin) + framesize = StackMin; +framesize += StackSystem; +``` + +因为常量 `StackExtra` 被设置为 [2048](https://github.com/golang/go/blob/release-branch.go1.2/src/pkg/runtime/stack.h#L74),`StackMin` 被设置为 [8192](https://github.com/golang/go/blob/release-branch.go1.2/src/pkg/runtime/stack.h#L79),`StackSystem` 被[最小值被设置为 0,最大值为 512](https://github.com/golang/go/blob/release-branch.go1.2/src/pkg/runtime/stack.h#L61-L68)。因此我们的新堆栈由两部分组成:16016 ( frame size ) + 800 ( arguments ) + 2048 ( StackExtra ) + 0 ( StackSystem ). + +一旦所有函数都被调用,新分配堆栈就被释放了(见 `runtime:oldstack`),这种行为是促使 [ Golang 团队采用连续堆栈策略](https://docs.google.com/document/d/1wAaf1rYoM4S4gtnPh0zOlGzWtrZFQ5suE8qr2sD8uWQ/pub)的原因之一: + +*如果在一个快速紧密的循环中,连续调用堆栈分配操作,那么分配/释放操作将会造成巨大开销。* + +由于分段堆栈的这个缺陷,Go 不得不将堆栈的最小值从 2Kb 加到 8Kb,以避免频繁的分配/释放操作,在采用连续堆栈策略后,又将其减小回 2Kb。 + +下方是一张分段堆栈策略的直观图: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/how-goroutine-stack-size-evolve/2.png) + +## 总结 + +Go 的栈管理非常高效,也很容易理解。Golang 并不是唯一选择不使用分段堆栈的语言,[Rust 也出于同样的原因](https://mail.mozilla.org/pipermail/rust-dev/2013-November/006314.html)决定不使用这个这个策略。 + +--- +via: https://medium.com/a-journey-with-go/go-how-does-the-goroutine-stack-size-evolve-447fc02085e5 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[Jun10ng](https://github.com/Jun10ng) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190606-Go-Improve-the-Usage-of-Your-Goroutines-with-GODEBUG.md b/published/tech/20190606-Go-Improve-the-Usage-of-Your-Goroutines-with-GODEBUG.md new file mode 100644 index 000000000..ef6616136 --- /dev/null +++ b/published/tech/20190606-Go-Improve-the-Usage-of-Your-Goroutines-with-GODEBUG.md @@ -0,0 +1,114 @@ +首发于:https://studygolang.com/articles/28430 + +# Go:通过 GODEBUG 提升对协程的利用 + +![Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20190606-Go-Improve-the-Usage-of-Your-Goroutines-with-GODEBUG/00.png) + +Go 协程是轻量的,在很多场景能提升你的程序性能。不幸的是,如果使用不当,也可能降低你程序的性能,因为 Go 协程的上线文切换也需要消耗一定的资源。 + +## 项目上下文和测试基准 + +在我的公司—[PropertyFinder](https://www.propertyfinder.ae/)—一个阿联酋的实业地产门户网站—我的团队维护一个微服务,用于为我们的客户和地产经纪人寻找潜在的机会。我们来通过一个算法 demo 概览下这个 Go 微服务: + +``` +Variables: +lead struct +Start: +// a profile of this lead is made based on its interest +MakeProfile(lead) +// we get all listing that are around of this kind of profile +listings <- GetListings() +For each chunk of 1000 in listings + Start goroutine + For each listing in chunk + score <- CalculateMatching(listing, lead) + Add the score to the bulk object + Bulk insert the 100 scores of the chunk +Stop +``` + +因为这里的 *listings* 可能达到 10k 以上,所以我们决定每 1000 个作为一个块,创建一个协程。下面是我们对计算过程跑的基准测试以及 10k 个匹配结果的记录: + +```bash +name time/op +LeadMatchingGenerationFor10000Matches-4 626ms ± 6% +``` + +我们在计算部分再增加更多的协程。我们对代码作如下更改: + +``` +// we get all listing that are around of this kind of profile +listings <- GetListings() +For each chunk of 1000 in listings + Start goroutine + For each listing in chunk + Start goroutine + score <- CalculateMatching(listing, lead) + Add the score to the bulk object + Bulk insert the 1000 scores +``` + +我们再运行一次: + +```bash +name time/op +LeadMatchingGenerationFor10000Matches-4 698ms ± 4% +``` + +比上次慢了 11%,但在预期内。实际上,分数计算部分是纯粹的数学计算,因此 Go 调度器在新的协程里并不能利用任何的停顿时间(如系统调用)。 + +*如果你想了解更多关于 Go 调度器和协程上下文切换的知识,我强烈推荐你阅读 William Kennedy 写的 [Go 调度之上下文切换](https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part2.html#context-switching)*。 + +## 上下文切换导致的协程等待 + +我们现在来分析 Go 调度器是怎么运行协程的。我们先来看我们的第一个算法,每 1000 个作为一个块,启动一个协程。打开 Go 调度器的 schedtrace 来运行基准: + +```bash +GODEBUG=schedtrace=1 Go test ./... -run=^$ -bench=LeadMatchingGenerationFor10000Matches -benchtime=1ns +``` + +参数 `schedtrace=1` 会[按毫秒打印 Go 调度器的调度事件](https://golang.org/doc/diagnostics.html#godebug)。下面是之前处理器空闲时新起的协程的相关信息的一部分追踪信息: + +```bash +gomaxprocs=2 idleprocs=1 runqueue=0 [0 0] +gomaxprocs=2 idleprocs=1 runqueue=0 [0 0] +gomaxprocs=2 idleprocs=1 runqueue=0 [0 0] +gomaxprocs=2 idleprocs=0 runqueue=1 [0 0] +gomaxprocs=2 idleprocs=2 runqueue=0 [0 0] +gomaxprocs=2 idleprocs=2 runqueue=0 [0 0] +``` + +`gomaxprocs` 表示可用的处理器的最大数,空闲线程数是 `idleprocs`,`runqueue` 表示等待被分配的协程数,中括号中的数表示每个处理器中等待执行的协程(`[0 0]`)。在 [关于性能的 wiki](https://github.com/golang/go/wiki/Performance#scheduler-trace) 中你可以找到更多相关信息。 + +我们可以清晰地看到并没有充分利用协程。我们也可以看到我们的处理器不是一直在工作,进而可能会想我们是否应该增加更多的协程来充分利用那些空闲的资源。我们以同样的步骤来运行第二个算法,每个分数计算起一个协程。 + +```bash +gomaxprocs=2 idleprocs=0 runqueue=645 [116 186] +gomaxprocs=2 idleprocs=0 runqueue=514 [77 104] +gomaxprocs=2 idleprocs=0 runqueue=382 [57 64] +gomaxprocs=2 idleprocs=0 runqueue=124 [57 88] +gomaxprocs=2 idleprocs=0 runqueue=0 [28 17] +gomaxprocs=2 idleprocs=1 runqueue=0 [0 0] +``` + +现在我们看到全局和本地队列的协程栈,它使我们的处理器一直处于工作状态。然而,我们的处理器很快又空闲了。使用追踪器的额外功能能解释这个现象: + +![goroutines profiling with tracer](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20190606-Go-Improve-the-Usage-of-Your-Goroutines-with-GODEBUG/01.png) +![goroutine 209 waiting for server response](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20190606-Go-Improve-the-Usage-of-Your-Goroutines-with-GODEBUG/02.png) +我们可以看到,大部分协程在数据记录时都在等待服务返回的 response。我们的优化应该关注这个地方,充分利用这个等待时间。这就是我们以 1000 份文件为一束来创建协程进行记录的原因。 + +我们也理解了,在计算过程中增加协程数只能增加对程序的压力而没有其他的收益。因为计算过程中在当前协程等待时,系统并没有足够的时间来运行另一个协程,所以切换到另一个协程所耗的时间仅仅是浪费而已。 + +## 更多关于协程 + +如果你想提升创建协程的方式,那么 Go 的调度器和并发是很重要的内容。Wiliam Kennedy 的 [Go 的调度机制](https://studygolang.com/articles/14264) 可能是最好的在线资源之一,我强烈推荐。 + +--- + +via: https://medium.com/a-journey-with-go/go-improve-the-usage-of-your-goroutines-with-godebug-4d1f33970c33 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190615-Go-How-Does-defer-Statement-Work.md b/published/tech/20190615-Go-How-Does-defer-Statement-Work.md new file mode 100644 index 000000000..48fd35727 --- /dev/null +++ b/published/tech/20190615-Go-How-Does-defer-Statement-Work.md @@ -0,0 +1,236 @@ +首发于:https://studygolang.com/articles/28431 + +# Go:defer 语句如何工作 + +![Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.](https://raw.githubusercontent.com/studygolang/gctt-images2/master/how-does-defer-statement-work/1.png) + +ℹ️ *这篇文章基于 Go 1.12。* + +[`defer` 语句](https://golang.org/ref/spec#Defer_statements)是在函数返回前执行一段代码的便捷方法,如 [Golang 规范](https://golang.org/ref/spec#Defer_statements)所描述: + +> 延迟函数( deferred functions )在所在函数返回前,以与声明相反的顺序立即被调用 + +以下是 LIFO (后进先出)实现的例子: + +```go +func main() { + defer func() { + println(`defer 1`) + }() + defer func() { + println(`defer 2`) + }() +} +defer 2 <- 后进先出 +defer 1 +``` + +来看一下内部的实现,然后再看一个更复杂的案例。 + +## 内部实现 + +Go 运行时(runtime)使用一个**链表**来实现 LIFO。实际上,一个 defer 结构体持有一个指向下一个要被执行的 defer 结构体的指针: + +```go +type _defer struct { + siz int32 + started bool + sp uintptr + pc uintptr + fn *funcval + _panic *_panic + link *_defer // 下一个要被执行的延迟函数 +``` + +当一个新的 defer 方法被创建的时候,它被附加到当前的 Goroutine 上,然后之前的 defer 方法作为下一个要执行的函数被链接到新创建的方法上: + +```go +func newdefer(siz int32) *_defer { + var d *_defer + gp := getg() // 获取当前 goroutine + [...] + // 延迟列表现在被附加到新的 _defer 结构体 + d.link = gp._defer + gp._defer = d // 新的结构现在是第一个被调用的 + return d +} +``` + +现在,后续调用会从栈的顶部依次出栈延迟函数: + +```go +func deferreturn(arg0 uintptr) { + gp := getg() // 获取当前 goroutine + d:= gp._defer // 拷贝延迟函数到一个变量上 + if d == nil { // 如果不存在延迟函数就直接返回 + return + } + [...] + fn := d.fn // 获取要调用的函数 + d.fn = nil // 重置函数 + gp._defer = d.link // 把下一个 _defer 结构体依附到 Goroutine 上 + freedefer(d) // 释放 _defer 结构体 + jmpdefer(fn, uintptr(unsafe.Pointer(&arg0))) // 调用该函数 +} +``` + +如我们所见,并没有循环地去调用延迟函数,而是一个接一个地出栈。这一行为可以通过生成[汇编](https://golang.org/doc/asm)代码得到验证: + +```asm +// 第一个延迟函数 +0x001d 00029 (main.go:6) MOVL $0, (SP) +0x0024 00036 (main.go:6) PCDATA $2, $1 +0x0024 00036 (main.go:6) LEAQ "".main.func1·f(SB), AX +0x002b 00043 (main.go:6) PCDATA $2, $0 +0x002b 00043 (main.go:6) MOVQ AX, 8(SP) +0x0030 00048 (main.go:6) CALL runtime.deferproc(SB) +0x0035 00053 (main.go:6) TESTL AX, AX +0x0037 00055 (main.go:6) JNE 117 +// 第二个延迟函数 +0x0039 00057 (main.go:10) MOVL $0, (SP) +0x0040 00064 (main.go:10) PCDATA $2, $1 +0x0040 00064 (main.go:10) LEAQ "".main.func2·f(SB), AX +0x0047 00071 (main.go:10) PCDATA $2, $0 +0x0047 00071 (main.go:10) MOVQ AX, 8(SP) +0x004c 00076 (main.go:10) CALL runtime.deferproc(SB) +0x0051 00081 (main.go:10) TESTL AX, AX +0x0053 00083 (main.go:10) JNE 101 +// main 函数结束 +0x0055 00085 (main.go:18) XCHGL AX, AX +0x0056 00086 (main.go:18) CALL runtime.deferreturn(SB) +0x005b 00091 (main.go:18) MOVQ 16(SP), BP +0x0060 00096 (main.go:18) ADDQ $24, SP +0x0064 00100 (main.go:18) RET +0x0065 00101 (main.go:10) XCHGL AX, AX +0x0066 00102 (main.go:10) CALL runtime.deferreturn(SB) +0x006b 00107 (main.go:10) MOVQ 16(SP), BP +0x0070 00112 (main.go:10) ADDQ $24, SP +0x0074 00116 (main.go:10) RET +``` + +`deferproc` 方法被调用了两次,并且内部调用了 `newdefer` 方法,我们之前已经看到该方法将我们的函数注册为延迟函数。之后,在函数的最后,在 `deferreturn` 函数的帮助下,延迟方法会被一个接一个地调用。 + +Go 标准库向我们展示了结构体 `_defer` 同样链接了一个 `_panic *_panic` 属性。来通过另一个例子看下它在哪里会起作用。 + +## 延迟和返回值 + +如规范所描述,延迟函数访问返回的结果的唯一方法是使用[命名返回参数](https://golang.org/ref/spec#Function_types): + +> 如果延迟函数是一个[匿名函数( function literal )](https://golang.org/ref/spec#Function_literals),并且所在函数存在[命名返回参数](https://golang.org/ref/spec#Function_types),同时该命名返回参数在匿名函数的作用域中,匿名函数可能会在返回参数返回前访问并修改它们。 + +这里有个例子: + +```go +func main() { + fmt.Printf("with named param, x: %d\n", namedParam()) + fmt.Printf("without named param, x: %d\n", notNamedParam()) +} +func namedParam() (x int) { + x = 1 + defer func() { x = 2 }() + return x +} + +func notNamedParam() (int) { + x := 1 + defer func() { x = 2 }() + return x +} +with named param, x: 2 +without named param, x: 1 +``` + +确实就像这篇“[defer, panic 和 recover](https://blog.golang.org/defer-panic-and-recover)”博客所描述的一样,一旦确定这一行为,我们可以将其与 recover 函数混合使用: + +> **recover 函数** 是一个用于重新获取对恐慌(panicking)goroutine 控制的内置函数。recover 函数仅在延迟函数内部时才有效。 + +如我们所见,`_defer` 结构体链接了一个 `_panic` 属性,该属性在 panic 调用期间被链接。 + +```go +func gopanic(e interface{}) { + [...] + var p _panic + [...] + d := gp._defer // 当前附加的 defer 函数 + [...] + d._panic = (*_panic)(noescape(unsafe.Pointer(&p))) + [...] +} +``` + +确实,在发生 panic 的情况下,调用延迟函数之前会调用 `gopanic` 方法: + +```ams +0x0067 00103 (main.go:21) CALL runtime.gopanic(SB) +0x006c 00108 (main.go:21) UNDEF +0x006e 00110 (main.go:16) XCHGL AX, AX +0x006f 00111 (main.go:16) CALL runtime.deferreturn(SB) +``` + +这里是一个 recover 函数利用命名返回参数的例子: + +```go +func main() { + fmt.Printf("error from err1: %v\n", err1()) + fmt.Printf("error from err2: %v\n", err2()) +} + +func err1() error { + var err error + + defer func() { + if r := recover(); r != nil { + err = errors.New("recovered") + } + }() + panic(`foo`) + + return err +} + +func err2() (err error) { + defer func() { + if r := recover(); r != nil { + err = errors.New("recovered") + } + }() + panic(`foo`) + + return err +} +error from err1: +error from err2: recovered +``` + +两者的结合是我们可以正常使用 recover 函数将我们希望的 error 返回给调用方。 +作为这篇关于延迟函数的文章的总结,让我们来看看延迟函数的提升。 + +## 性能提升 + +[Go 1.8](https://golang.org/doc/go1.8#defer)是提升 defer 的最近的一个版本(译者注:目前 Go 1.14 才是提升 defer 性能的最近的一个版本),我们可以通过运行 Go 的基准测试来看到这些提升(在 1.7 和 1.8 之间进行对比): +``` +name old time/op new time/op delta +Defer-4 99.0ns ± 9% 52.4ns ± 5% -47.04% (p=0.000 n=9+10) +Defer10-4 90.6ns ± 13% 45.0ns ± 3% -50.37% (p=0.000 n=10+10) +``` + +这样的提升得益于[这个提升分配方式的 CL ](https://go-review.googlesource.com/c/go/+/29656/),避免了栈的增长。 + +不带参数的 defer 语句避免内存拷贝也是一个优化。下面是带参数和不带参数的延迟函数的基准测试: + +``` +name old time/op new time/op delta +Defer-4 51.3ns ± 3% 45.8ns ± 1% -10.72% (p=0.000 n=10+10) +``` + +由于第二个优化,现在速度也提高了 10%。 + +--- + +via: https://medium.com/a-journey-with-go/go-how-does-defer-statement-work-1a9492689b6e + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[dust347](https://github.com/dust347) +校对:[@unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190622-Go-Shared-Objects-for-Cross-Team-Collaboration.md b/published/tech/20190622-Go-Shared-Objects-for-Cross-Team-Collaboration.md new file mode 100644 index 000000000..326c561d2 --- /dev/null +++ b/published/tech/20190622-Go-Shared-Objects-for-Cross-Team-Collaboration.md @@ -0,0 +1,151 @@ +首发于:https://studygolang.com/articles/28432 + +# Go:跨团队协作时共享对象 + +![Illustration created for “A Journey With Go”, made from *the original Go Gopher, created by Renee French.*](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20190622-Go-Shared-Objects-for-Cross-Team-Collaboration/00.png) + +在一个公司中,跨小组/团队协作时如果各方使用不同的语言,有时会很复杂。在[我的团队](https://www.propertyfinder.ae/)中,我们维护用 Go 语言写的项目,而我们的合作方,数据科学小组使用 Python。他们提供给我们一些公式,我们需要在项目中翻译成 Go。 + +当我们实现了这个公式后,我们需要对方组的验证,以确保我们实现得没有错误。让他们来测试并让他们自己提供测试数据无疑是最好的方法。幸运的是,我们可以用 Go 来实现这个过程。 + +## cgo 和共享对象 + +构建命令 `go build` 有一个参数可以[把你的 Go 包编进 C shared library](https://golang.org/pkg/cmd/go/internal/help/#pkg-variables): + +```bash +-buildmode=c-shared + Build the listed main package, plus all packages it imports, + into a C shared library. The only callable symbols will + be those functions exported using a cgo //export comment. + Requires exactly one main package to be listed. +``` + +用这个模式你可以编译一个 shared library,文件以 “.so” 结尾,你可以在其他语言中如 C,Java,Ruby 或 Python 中直接使用。然而,这个模式只有 [Cgo](https://golang.org/cmd/cgo/) 支持,你可以在你的 Go 包中写、调用 C 代码。基于此,你可以写一个自己的库,让别的小组用他们自己的语言来调你的库。 + +## 实现 + +Go 和共享对象间的网关的实现看起来很简单。首先你需要在你想导出的每一个函数前添加注释 `//export MyFunction`。然后你需要在强制性 `import "C"` 之前前置声明你的 C 结构体。下面是我们代码的简化版: + +```go +import ( + /* + typedef struct{ + int from_bedroom; + int to_bedroom; + int from_price; + int to_price; + int from_size; + int to_size; + int types[5]; + } lead; + + typedef struct{ + int bedroom; + int price; + int size; + int type_id; + } property; + */ + "C" +) +//export NewProperty +func NewProperty(b int, p int, s int, t int) C.property { + // business logic + + return C.property{ + bedroom: C.int(b), + price: C.int(p), + size: C.int(s), + type_id: C.int(t), + } +} +//export NewLead +func NewLead(fb int, tb int, fp int, tp int, fs int, ts int, t []int) C.lead { + // business logic + return C.lead{ + from_bedroom: C.int(fb), + to_bedroom: C.int(tb), + from_price: C.int(fp), + to_price: C.int(tp), + from_size: C.int(fs), + to_size: C.int(ts), + types: types, + } +} +//export CalculateDistance +func CalculateDistance(l C.lead, p C.property) { + // business logic here +} +``` + + 因为你不能导出 Go 的结构体,所以你需要把 C 的结构体作为输入/输出参数进行处理。当你写完代码后,可以使用命令 `go build -o main.so -buildmode=c-shared main.go` 来编译。为了能编译成功,你的 Go 代码中需要有 main 包和 main 函数。然后,你就可以写你的 Python 脚本了: + +```python +#!/usr/bin/env python +from ctypes import * + +# loading shared object +matching = cdll.LoadLibrary("main.so") + +# Go type +class GoSlice(Structure): + _fields_ = [("data", POINTER(c_void_p)), ("len", c_longlong), ("cap", c_longlong)] + +class Lead(Structure): + _fields_ = [('from_bedroom', c_int), + ('to_bedroom', c_int), + ('from_price', c_int), + ('to_price', c_int), + ('from_size', c_int), + ('to_size', c_int), + ('types', GoSlice)] + +class Property(Structure): + _fields_ = [('bedroom', c_int), + ('price', c_int), + ('size', c_int), + ('type_id', c_int)] + +#parameters definition +matching.NewLead.argtypes = [c_int, c_int, c_int, c_int, c_int, c_int, GoSlice] +matching.NewLead.restype = Lead + +matching.NewProperty.argtypes = [c_int, c_int, c_int, c_int] +matching.NewProperty.restype = Property + +matching.CalculateDistance.argtypes = [Lead, Property] + +lead = lib.NewLead( + # from bedroom, to bedroom + 1, 2, + # from price, to price + 80000, 100000, + # from size, to size + 750, 1000, + # type + GoSlice((c_void_p * 5)(1, 2, 3, 4, 5), 5, 5) +) +property = lib.NewProperty(2, 90000, 900, 1) + +matching.CalculateDistance(lead, property) +``` + +你的共享对象中的所有导出的方法都应该在你的 Python 文件中有描述:类型、参数顺序及返回参数。 + +之后,你可以运行你的 Python 脚本 `python3 main.py`。 + +## 使用的方便程度 + +乍一看很简单,但可能需要花费很长时间才能正常运行。Python 中没有很多关于这个的文档或例子,而且很难调试。如果用 `.argtypes` 或 `.restype` 对你暴露的方法描述不当,可能会导致意想不到的结果或出现 `segmentation fault` 错误信息,且不会有足够多的信息来帮助调试。 + +这个 Go/Python 的通信方式很适合跨团队测试,但我不建议在大型的项目或以后在生产环境中用。因为这种开发方式很复杂,容易耗费较长时间。 + +--- + +via: https://medium.com/a-journey-with-go/go-shared-objects-for-cross-team-collaboration-b3af7d9e73af + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190702-go-concurrency-access-with-maps-part-III.md b/published/tech/20190702-go-concurrency-access-with-maps-part-III.md index f73c9d742..e6811e08e 100644 --- a/published/tech/20190702-go-concurrency-access-with-maps-part-III.md +++ b/published/tech/20190702-go-concurrency-access-with-maps-part-III.md @@ -1,4 +1,4 @@ -首发于:https://studygolang.com/articles/22773 +首发于:https://studygolang.com/articles/22778 # Go: 并发访问 Map — Part III @@ -8,7 +8,7 @@ [Go blog](https://blog.golang.org/go-maps-in-action) 中专门讲解 map 的文章明确地表明: -> [map 是非并发安全的](https://golang.org/doc/faq#atomic_maps):并发读写 map 时,map 的行为是未知的。如果你需要使用并发执行的 goroutine 同时读写 map,必须使用某种同步机制来协调访问。 +> [map 是非并发安全的](https://golang.org/doc/faq#atomic_maps):并发读写 map 时,map 的行为是未知的。如果你需要使用并发执行的 Goroutine 同时读写 map,必须使用某种同步机制来协调访问。 然而,正如 [FAQ](https://golang.org/doc/faq#atomic_maps) 中解释的,Google 提供了一些帮助: @@ -26,12 +26,12 @@ func main() { var wg sync.WaitGroup wg.Add(2) - go func() { + Go func() { for i := 0; i < 1000; i++ { m[`foo`]++ } }() - go func() { + Go func() { for i := 0; i < 1000; i++ { m[`foo`]++ } @@ -40,25 +40,25 @@ func main() { } ``` -在这个例子中,我们清晰地看到,在某一时刻,两个 goroutine 尝试同时写入一个新值。下面是争用检测器的输出: +在这个例子中,我们清晰地看到,在某一时刻,两个 Goroutine 尝试同时写入一个新值。下面是争用检测器的输出: ``` ================== WARNING: DATA RACE -Read at 0x00c00008e000 by goroutine 6: +Read at 0x00c00008e000 by Goroutine 6: runtime.mapaccess1_faststr() /usr/local/go/src/runtime/map_faststr.go:12 +0x0 main.main.func2() main.go:19 +0x69 -Previous write at 0x00c00008e000 by goroutine 5: +Previous write at 0x00c00008e000 by Goroutine 5: runtime.mapassign_faststr() /usr/local/go/src/runtime/map_faststr.go:202 +0x0 main.main.func1() main.go:14 +0xb8 ``` -争用检测器解释道,当第二个 goroutine 正在读变量时,第一个 goroutine 正在向同一个内存地址写一个新值。如果你想要了解更多,我建议你阅读我的一篇关于[数据争用检测器](https://medium.com/@blanchon.vincent/go-race-detector-with-threadsanitizer-8e497f9e42db)的文章。 +争用检测器解释道,当第二个 Goroutine 正在读变量时,第一个 Goroutine 正在向同一个内存地址写一个新值。如果你想要了解更多,我建议你阅读我的一篇关于[数据争用检测器](https://medium.com/@blanchon.vincent/go-race-detector-with-threadsanitizer-8e497f9e42db)的文章。 ## 并发写入检测 @@ -120,15 +120,15 @@ func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) { 让我们运行一个简单的基准测试,比较带有锁的常规 map 和 `sync` 包的 map。一个基准测试并发写入 map,另一个仅仅读取 map 中的值: ```go -MapWithLockWithWriteOnlyInConcurrentEnc-8 68.2µs ± 2% -SyncMapWithWriteOnlyInConcurrentEnc-8 192µs ± 2% -MapWithLockWithReadOnlyInConcurrentEnc-8 76.8µs ± 3% -SyncMapWithReadOnlyInConcurrentEnc-8 55.7µs ± 4% +MapWithLockWithWriteOnlyInConcurrentEnc-8 68.2 µ s ± 2% +SyncMapWithWriteOnlyInConcurrentEnc-8 192 µ s ± 2% +MapWithLockWithReadOnlyInConcurrentEnc-8 76.8 µ s ± 3% +SyncMapWithReadOnlyInConcurrentEnc-8 55.7 µ s ± 4% ``` 我们可以看到,两种 map 各有千秋。我们可以根据具体的情况选择其中之一。[文档](https://golang.org/pkg/sync/#Map)中很好地解释了这些情况: -> map 类型针对两种常见使用场景做了优化:(1) 指定 key 的 entry 仅写入一次,但多次读取,比如只增长的缓存; (2) 多个 goroutine 读取、写入、覆盖不相交的 key 的集合指向的 entry。 +> map 类型针对两种常见使用场景做了优化:(1) 指定 key 的 entry 仅写入一次,但多次读取,比如只增长的缓存; (2) 多个 Goroutine 读取、写入、覆盖不相交的 key 的集合指向的 entry。 ## Map vs sync.Map @@ -136,7 +136,7 @@ SyncMapWithReadOnlyInConcurrentEnc-8 55.7µs ± 4% > 因此,要求所有的 map 操作都获取互斥锁,会拖慢大多数程序,但只为很少的程序增加了安全性 -让我们运行一个不使用并发 goroutine 的基准测试,来理解当你不需要并发但标准库默认提供并发安全的 map 时,可能带来的影响: +让我们运行一个不使用并发 Goroutine 的基准测试,来理解当你不需要并发但标准库默认提供并发安全的 map 时,可能带来的影响: ``` MapWithWriteOnly-8 11.1ns ± 3% diff --git a/published/tech/20190703-Go-Fan-out-Pattern-in-Our-ET.md b/published/tech/20190703-Go-Fan-out-Pattern-in-Our-ET.md new file mode 100644 index 000000000..c27b5a5bd --- /dev/null +++ b/published/tech/20190703-Go-Fan-out-Pattern-in-Our-ET.md @@ -0,0 +1,69 @@ +首发于:https://studygolang.com/articles/33989 + +# Go: 在我们的 ETL 中使用扇出模式 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/20190703-go-fan-out-pattern/cover.png) + +Go 语言在构建微服务、特别是有使用 gRPC 的应用中,非常地流行,其实在构建命令行程序时也是特别地好用。为了学习扇出模式,我会基于我们公司使用 ETL 的例子,来介绍这个模式。 + +## ETL + +ETL(提取(Extract),转换(Transform),加载(Load))通常都需要处理大量的数据。在这样的场景下,有一个好的并发策略对于 ETL 来说,能够带来巨大的性能提升。 + +ETL 中有两个最重要的部分是提取(extracting)和加载(Load),通常它们都跟数据库有关,瓶颈通常也属于老生常谈的话题:网络带宽,查询性能等等。基于我们要处理的数据以及瓶颈所在,两种模式对于处理数据或者处理输入流的编码和解码过程中,非常有用。 + +## 扇入扇出模式(Fan-in, fan-out pattern) + +扇入和扇出模式在并发场景中能得到较大的好处。这里将对它们逐个做专门的介绍(review): + +扇出,在 GO 博客中这样定义: + +多个函数能够同时从相同的 channel 中读数据,直到 channel 关闭。 + +这种模式在快速输入流到分布式数据处理中,有一定的优势: + +![fan-out pattern with distributed work](https://raw.githubusercontent.com/studygolang/gctt-images/master/20190703-go-fan-out-pattern/1.png) + +扇入,在 Google 这样定义: + +一个函数可以从多个输入中读取,并继续操作,直到所有 channel 所关联的输入端,都已经关闭。 + +这种模式,在有多个输入源,且需要快速地数据处理中,有一定的优势: + +![fan-in pattern with multiple inputs](https://raw.githubusercontent.com/studygolang/gctt-images/master/20190703-go-fan-out-pattern/2.png) + +## 在实际中使用扇出模式(Fan-out in action) + +在我们的项目中,我们需要处理存储在 CSV 文件的大量数据,它们加载后,将在 elastic 中被检索。输入的处理必须快,否则(阻塞加载)加载就会变得很慢。因此,我们需要比输入生成器更多的数据处理器。扇出模式在这个例子中,看起来非常适合: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/20190703-go-fan-out-pattern/3.png) + +下面是我们的伪代码: + +```bash +Variables: +data chan +Start: +// a Goroutine will parse the CSV and will send it to the channel +ParseCSV(data<-) +// a Goroutine is started for each workers, defined as command line arguments +For each worker in workers + Start goroutine + For each value in <-data + Insert the value in database by chunk of 50 +Wait for the workers +Stop +``` + +输入和加载程序是并发执行的,我们不需要等到解析完成后再开始启动数据处理程序。 + +这种模式让我们可以单独考虑业务逻辑的同时,还可以使用(Go)并发的特性。几个工作器之间原生的分布式负载能力,有助于我们解决此类过程中的峰值负载问题。 + +--- +via: https://medium.com/a-journey-with-go/go-fan-out-pattern-in-our-etl-9357a1731257 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[gogeof](https://github.com/gogeof) +校对:[Xiaobin.Liu](https://github.com/lxbwolf) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190703-Go-compiler-internals-adding-a-new-statement-to-Go-Part-1.md b/published/tech/20190703-Go-compiler-internals-adding-a-new-statement-to-Go-Part-1.md new file mode 100644 index 000000000..1f5ebceeb --- /dev/null +++ b/published/tech/20190703-Go-compiler-internals-adding-a-new-statement-to-Go-Part-1.md @@ -0,0 +1,468 @@ +首发于:https://studygolang.com/articles/25101 + +# Go 编译器内核:给 Go 新增一个语句 —— 第一部分 + +这是两部分系列文章中的第一部分,该文章采用教程的方式来探讨 Go 编译器。Go 编译器复杂而庞大,需要一本书才可能描述清楚,所以这个系列文章旨在提供一个快速而深度优先的方式进入学习。我计划在以后会写更多关于编译器领域的描述文章。 + +我们会修改 Go 编译器来增加一个新的(玩具性质)语言特性,并构建一个经过修改的编译器进行使用。 + +## 任务 —— 增加新的语句 + +很多语言都有 `while` 语句,在 Go 中对应的是 `for`: + +```go +for { + +} +``` + +增加 `while` 语句是比较简单的,因此 —— 我们只需简单将其转换为 `for` 语句。所以我选择了一个稍微有点挑战性的任务,增加 `until`。`until` 语句和 `while` 语句是一样的,只是有了条件判断。例如下面的代码: + +```go +i := 4 +until i == 0 { + i-- + fmt.Println("Hello, until!") +} +``` + +等价于: + +```go +i := 4 +for i != 0 { + i-- + fmt.Println("Hello, until!") +} +``` + +事实上,我们甚至可以像下面代码一样,在循环声明中使用一个初始化语句: + +```go +until i := 4; i == 0 { + i-- + fmt.Println("Hello, until!") +} +``` + +我们的目标是支持这个特性。 + +特别声明 —— 这只是一个玩具性的探索。我觉得在 Go 中添加 `until` 并不好,因为 Go 的极简主义设计思想本身就是非常正确的理念。 + +## Go 编译器的高级结构 + +默认情况下,Go 编译器(`gc`)是以相当传统的结构来设计的,如果你使用过其他编译器,你应该很快就能熟悉它: + +![](https://eli.thegreenplace.net/images/2019/go-compiler-flow.png) + +Go 仓库中相对路径的根目录下,编译器实现位于 `src/cmd/compile/internal`;本文后续提到的所有代码路径都是相对于这个目录。编译器是用 Go 编写的,代码可读性很强。在这篇文章中,我们将一点一点的研究这些代码,同时添加支持 `until` 语句的实现代码. + +查看 `src/cmd/compile` 中的 `README` 文件,了解编译步骤的详细说明。它将与本文息息相关。 + +## 扫描 + +扫描器(也称为 _词法分析器_)将源码文本分解为编译器所需的离散实体。例如 `for` 关键字会转变成常量 `_For`;符号 `...` 转变成 `_DotDotDot`,`.` 将转变成 `_Dot` 等等。 + +扫描器的实现位于 `syntax` 包中。我们需要做的就是理解关键字 —— `until`。`syntax/tokens.go` 文件中列出了所有 token,我们要添加一个新的: + +``` +_Fallthrough // fallthrough +_For // for +_Until // until +_Func // func +``` + +token 常量右侧的注释非常重要,它们用来标识 token。这是通过 `syntax/tokens.go` 生成代码来实现的,文件上面的 token 列表有如下这一行: + +```go +//go:generate stringer -type token -linecomment +``` + +`go generate` 必须手动执行,输出文件(`syntax/token_string.go`)被保存在 Go 源码仓库中。为了重新生成它,我在 `syntax` 目录中执行如下命令: + +``` +GOROOT= Go generate tokens.go +``` + +环境变量 `GOROOT` 是[从 Go 1.12 开始必须设置](https://github.com/golang/go/issues/32724),并且必须指向检出的源码根目录,我们要修改这个编译器。 + +运行代码生成器并验证包含新的 token 的 `syntax/token_string.go` 文件,我试着重新编译编译器,却出现了 panic 提示: + +``` +panic: imperfect hash +``` + +这个 panic 是 `syntax/scanner.go` 中代码引起的: + +```go +// hash 是针对关键词的完美哈希函数 +// 它假定参数 s 的长度至少为 2 +func hash(s []byte) uint { + return (uint(s[0])<<4 ^ uint(s[1]) + uint(len(s))) & uint(len(keywordMap)-1) +} + +var keywordMap [1 << 6]token // 大小必须是 2 的整数倍(2 的整数次幂) + +func init() { + // 填充 keywordMap + for tok := _Break; tok <= _Var; tok++ { + h := hash([]byte(tok.String())) + if keywordMap[h] != 0 { + panic("imperfect hash") + } + keywordMap[h] = tok + } +} +``` + +编译器试图构建一个“完美”哈希表来执行关键字字符串到 token 的查询。“完美”意味着它不太可能发生冲突,是一个线性的数组,其中每个关键字都映射为一个单独的索引。哈希函数相当特殊(例如,它查看字符串 token 的第一个字符),并且不容易调试新 token 为何出现冲突等问题。为了解决这个问题,我将查找表的大小更改为 `[1 << 7]token`,从而将查找数组的大小从 64 改成 128。这给予哈希函数更多的空间来分配对应的键,冲突也就消失了。 + +## 解析 + +Go 有一个相当标准的递归下降算法的解析器,它把扫描生成的 token 流转换为 _具体语法树_。我们开始为 `syntax/nodes.go` 中的 `until` 添加新的节点类型: + +```go +UntilStmt struct { + Init SimpleStmt + Cond Expr + Body *BlockStmt + stmt +} +``` + +我借鉴了用于 `for` 循环的 `ForStmt` 的整体结构。类似于 `for`,`until` 语句有几个可选的子语句: + +``` +until ; { + +} +``` + +`` 和 `` 是可选的,不过省略 `` 也不是很常见。`UntilStmt.stmt` 中嵌入的字段用于表示整个语法树语句,并包含对应的位置信息。 + +解析过程在 `syntax/parser.go` 中实现。`parser.stmtOrNil` 方法解析当前位置的语句。它查看当前 token 并决定解析哪个语句。下方是添加的代码片段: + +```go +switch p.tok { +case _Lbrace: + return p.blockStmt("") + +// ... + +case _For: + return p.forStmt() + +case _Until: + return p.untilStmt() +``` + +And this is `untilStmt`: + +```go +func (p *parser) untilStmt() Stmt { + if trace { + defer p.trace("untilStmt")() + } + + s := new(UntilStmt) + s.pos = p.pos() + + s.Init, s.Cond, _ = p.header(_Until) + s.Body = p.blockStmt("until clause") + + return s +} +``` + +我们复用现有的 `parser.header` 方法,因为它解析了 `if` 和 `for` 语句对应的 header。在常用的形式中,它支持三个部分(分号分隔)。在 `for` 语句中,第三部分常被用于 ["post" 语句](https://golang.org/ref/spec#PostStmt),但我们不打算为 `until` 实现这种形式,而只需实现前两部分。注意 `header` 接收源 token,以便能够区分 `header` 所处的具体场景;例如,编译器会拒绝 `if` 的“post”语句。虽然现在还没有费力气实现“post”语句,但我们在 `until` 的场景中应该明确地拒绝“post”语句。 + +这些都是我们需要对解析器进行的修改。因为 `until` 语句在结构上跟现有的一些语句非常相似,所以我们可以复用已有的大部分功能。 + +假如在编译器解析后输出语法树(使用 `syntax.Fdump`)然后使用语法树: + +```go +i = 4 +until i == 0 { + i-- + fmt.Println("Hello, until!") +} +``` + +我们会得到 `until` 语句的相关片段: + +``` +84 . . . . . 3: *syntax.UntilStmt { + 85 . . . . . . Init: nil + 86 . . . . . . Cond: *syntax.Operation { + 87 . . . . . . . Op: == + 88 . . . . . . . X: i @ ./useuntil.go:13:8 + 89 . . . . . . . Y: *syntax.BasicLit { + 90 . . . . . . . . Value: "0" + 91 . . . . . . . . Kind: 0 + 92 . . . . . . . } + 93 . . . . . . } + 94 . . . . . . Body: *syntax.BlockStmt { + 95 . . . . . . . List: []syntax.Stmt (2 entries) { + 96 . . . . . . . . 0: *syntax.AssignStmt { + 97 . . . . . . . . . Op: - + 98 . . . . . . . . . Lhs: i @ ./useuntil.go:14:3 + 99 . . . . . . . . . Rhs: *(Node @ 52) +100 . . . . . . . . } +101 . . . . . . . . 1: *syntax.ExprStmt { +102 . . . . . . . . . X: *syntax.CallExpr { +103 . . . . . . . . . . Fun: *syntax.SelectorExpr { +104 . . . . . . . . . . . X: fmt @ ./useuntil.go:15:3 +105 . . . . . . . . . . . Sel: Println @ ./useuntil.go:15:7 +106 . . . . . . . . . . } +107 . . . . . . . . . . ArgList: []syntax.Expr (1 entries) { +108 . . . . . . . . . . . 0: *syntax.BasicLit { +109 . . . . . . . . . . . . Value: "\"Hello, until!\"" +110 . . . . . . . . . . . . Kind: 4 +111 . . . . . . . . . . . } +112 . . . . . . . . . . } +113 . . . . . . . . . . HasDots: false +114 . . . . . . . . . } +115 . . . . . . . . } +116 . . . . . . . } +117 . . . . . . . Rbrace: syntax.Pos {} +118 . . . . . . } +119 . . . . . } +``` + +## 创建 AST + +由于有了源代码的语法树表示,编译器才能构建一个*抽象语法树*。我曾写过关于[抽象 vs 具体语法树](http://eli.thegreenplace.net/2009/02/16/abstract-vs-concrete-syntax-trees)的文章 —— 如果你不熟悉他们之间的区别,可以好好看看这个文章。在 Go 中,未来可能会有所变动。Go 编译器最初是用 C 语言编写的,后来自动翻译成 Go;所以编译器的某些部分是 C 时期遗留下来的,有些部分是比较新的。未来的重构可能只会留下一种语法树,但是现在(Go 1.12)我们必须遵循这个流程。 + +AST 代码位于 `gc` 包中,节点类型在 `gc/syntax.go` 中定义。(不要跟 `syntax` 包中的 CST 混淆) + +Go AST 的结构与 CST 不同。所有的 AST 节点都是 `syntax.Node` 类型而非有各自的类型。`syntax.Node` 类型是一种 _可区分的联合体_,其中的字段有很多不同的类型。然而,这些字段是通用的,并且可用于大多数节点类型: + +```go +// 一个 Node 代表语法树中的单个节点 +// 实际上,因为只有一个,所以语法树就是一个语法 DAG +// 对于一个给定的变量,使用 Op=ONAME 作为节点 +// Op=OTYPE、Op=OLITERAL 也是这样,参考 Node.mayBeShared +type Node struct { + // 树结构 + // 普通的递归遍历应该包含以下字段 + Left *Node + Right *Node + Ninit Nodes + Nbody Nodes + List Nodes + Rlist Nodes + + // ... +``` + +我们以增加一个新的常量标识 `until` 节点作为开始: + +```go +// 语句 +// ... +OFALL // fallthrough +OFOR // for Ninit; Left; Right { Nbody } +OUNTIL // until Ninit; Left { Nbody } +``` + +我们再运行一下 `go generate`,这次在 `gc/syntax.go` 文件中,生成了一个代表新节点类型的字符串: + +``` +// 在 gc 的目录中 +GOROOT= Go generate syntax.go +``` + +应该更新 `gc/op_string.go` 文件使其包含 `OUNTIL`。现在是时候为新节点类型编写 CST->AST 的转换代码了。 + +转换是在 `gc/noder.go` 中实现的。我们基于现有的 `for` 语句支持,对 `until` 修改建模,从包含一个分支语句类型的 `stmtFall` 开始: + +```go +case *syntax.ForStmt: + return p.forStmt(stmt) +case *syntax.UntilStmt: + return p.untilStmt(stmt) +``` + +然后是新的 `untilStmt` 方法,我们将其添加到 `noder` 类型上: + +```go +// untilStmt 把具体语法树节点 UntilStmt 转换为对应的 AST 节点 +func (p *noder) untilStmt(stmt *syntax.UntilStmt) *Node { + p.openScope(stmt.Pos()) + var n *Node + n = p.nod(stmt, OUNTIL, nil, nil) + if stmt.Init != nil { + n.Ninit.Set1(p.stmt(stmt.Init)) + } + if stmt.Cond != nil { + n.Left = p.expr(stmt.Cond) + } + n.Nbody.Set(p.blockStmt(stmt.Body)) + p.closeAnotherScope() + return n +} +``` + +回想一下上面解释过的 `Node` 字段。这里我们使用 `Init` 作为可选的初始化操作,`Left` 字段作用于条件,`Nbody` 字段作用于循环体。 + +这就是新增 `until` 语句 AST 节点所需的全部内容。如果在构建后输出 AST,将得到以下这些: + +``` +. . UNTIL l(13) +. . . EQ l(13) +. . . . NAME-main.i a(true) g(1) l(6) x(0) class(PAUTO) +. . . . LITERAL-0 l(13) untyped number +. . UNTIL-body +. . . ASOP-SUB l(14) implicit(true) +. . . . NAME-main.i a(true) g(1) l(6) x(0) class(PAUTO) +. . . . LITERAL-1 l(14) untyped number + +. . . CALL l(15) +. . . . NONAME-fmt.Println a(true) x(0) fmt.Println +. . . CALL-list +. . . . LITERAL-"Hello, until!" l(15) untyped string +``` + +## 类型检查 + +编译的下一步是类型检查,这是在 AST 的基础上完成的。除了检查类型错误外,Go 中的类型检查还包括 _类型推导_,类型推导可以让我们编写如下语句: + +```go +res, err := func(args) +``` + +无需显示的声明 `res` 和 `err` 的类型。Go 类型检查器还会做一些其它事情,比如链接标识符到对应的声明上,和计算“编译时”常量。代码在 `gc/typecheck.go` 文件中。同样,在 `for` 语句的引导下,我们把这个子句添加到 `typecheck` 的分支中: + +```go +case OUNTIL: + ok |= ctxStmt + typecheckslice(n.Ninit.Slice(), ctxStmt) + decldepth++ + n.Left = typecheck(n.Left, ctxExpr) + n.Left = defaultlit(n.Left, nil) + if n.Left != nil { + t := n.Left.Type + if t != nil && !t.IsBoolean() { + yyerror("non-bool %L used as for condition", n.Left) + } + } + typecheckslice(n.Nbody.Slice(), ctxStmt) + decldepth-- +``` + +只有一部分的语句分配了类型,并且在布尔上下文中检查条件是否合法。 + +## 分析并重写 AST + +在类型检查后,编译器会经历 AST 分析和重写等几个阶段。具体的序列在 `gc/main.go` 文件的 `gc.Main` 函数中列出。在编译器术语中,这个阶段通常称为 _passes_。 + +很多 passes 中无需修改就能支持 `until`,因为这些 passes 对所有类型语句都是通用的(在这里通用的 `gc.Node` 结构也有效)。然而,只是有些场景是这样。比如逃逸分析中,它试图找到那些变量“逃离”函数作用域,并分配在堆上而非栈空间。 + +“逃逸分析”适用于每个语句类型,所以我们必须把它加在 `Escape.stmt` 对应的分支中: + +```go +case OUNTIL: + e.loopDepth++ + e.discard(n.Left) + e.stmts(n.Nbody) + e.loopDepth-- +``` + +最后,`gc.Main` 可以调用可移植的代码生成器(`gc/pgen.go`)来编译分析代码。代码生成器首先进行一系列 AST 转换,将 AST 维度降低便于编译。这是在先调用 `order` 的 `compile` 函数中完成的。 + +这个转换(在 `gc/order.go` 中)对语句和表达式重新排序,以强制执行计算标识符顺序。比如,把 `foo /= 10` 重写为 `foo = foo / 10`,用多个单赋值语句替换多赋值语句等等。 + +为了支持 `until` 语句,我们需要向 `Order.stmt` 增加以下内容: + +```go +case OUNTIL: + t := o.markTemp() + n.Left = o.exprInPlace(n.Left) + n.Nbody.Prepend(o.cleanTempNoPop(t)...) + orderBlock(&n.Nbody, o.free) + o.out = append(o.out, n) + o.cleanTemp(t) +``` + +在 `order`、`compile` 调用位于 `gc/walk.go` 中的 `walk` 后。这个传递过程收集了一系列 AST 转换,这些语句在后面有助于降低 AST 的维度成为 SSA。比如,在 for 循环中重写 range 语句,转变成更为简单的、有具体变量的 for 循环的形式 [\[1\]](https://eli.thegreenplace.net/2019/go-compiler-internals-adding-a-new-statement-to-go-part-1/#id2)。[运行时重写调用 map 的访问方式](https://dave.cheney.net/2018/05/29/how-the-go-runtime-implements-maps-efficiently-without-generics)等等。 + +为了支持 `walk` 中的新语句,我们必须在 `walkstmt` 函数中添加一个 switch 子句。顺便说一下,这也是我们实现 `until` 语句要修改的地方,主要是将它重写为编译器能识别的 AST 节点。在 `until` 的例子中,这比较简单 —— 如文章开头所示,我们只是用倒装条件将它重写为一个 `for` 循环。具体转换代码如下: + +```go +case OUNTIL: + if n.Left != nil { + walkstmtlist(n.Left.Ninit.Slice()) + init := n.Left.Ninit + n.Left.Ninit.Set(nil) + n.Left = nod(ONOT, walkexpr(n.Left, &init), nil) + n.Left = addinit(n.Left, init.Slice()) + n.Op = OFOR + } + walkstmtlist(n.Nbody.Slice()) +``` + +注意我们替换了 n.Left(条件),它带有类型为 ONOT 的新节点(它代表一元操作符 !),新节点包装了之前的 n.Left,然后我们用 OFOR 替换 n.Op。就是这样! + +如果在遍历之后输出 AST,我们会看到 OUNTIL 节点不见了,取而代之的是新的 OFOR 节点。 + +## 尝试 + +我们现在可以尝试修改编译器,然后运行一个使用了 `until` 语句的示例程序, + +```go +$ cat useuntil.go +package main + +import "fmt" + +func main() { + i := 4 + until i == 0 { + i-- + fmt.Println("Hello, until!") + } +} + +$ /bin/go run useuntil.go +Hello, until! +Hello, until! +Hello, until! +Hello, until! +``` + +成功了! + +提醒:`` 是我们检出的 Go 代码仓库,我们需要修改它、编译它(更多细节参见附录) + +## 结论部分 1 + +这是结论第一部分。我们成功地在 Go 编译器中实现了新增一个语句。这个过程没有涵盖编译器的所有方面,因为这种通过使用 `for` 节点的方式重写 `until` 节点的 AST 方式像是一条捷径。这是一种有效的编译策略,Go 编译器已经有了很多类似的优化手段来 _转换_ AST(这将减少构成的形式,便于编译的最后阶段做更少的工作)。即便如此,我们仍然有兴趣探索最后两个编译阶段 —— _转换为 SSA_ 和 _生成机器码_。这些将在[第 2 部分]((http://eli.thegreenplace.net/2019/go-compiler-internals-adding-a-new-statement-to-go-part-2/)) 讨论。 + +## Appendix - 构建 Go 的工具链 + +请先浏览 [Go 贡献指南](https://golang.org/doc/contribute.html)。下面是类似于本文关于重述修改 Go 编译器的简要说明。 + +有两种方法: + + 1. 克隆官方的 [Go 仓库](https://github.com/golang/go)并实践本文所描述的内容。 + 2. 克隆官方的 [Go 仓库](https://github.com/golang/go),并检出 `adduntil` 分支,这个分支中已经有很多基于调试工具的修改。 + +克隆的路径是本文中 `` 所表示的路径。 + +要编译工具链,进入 `src/` 目录并运行 `./make.bash`。也能通过 `./all.bash` 来运行所有的测试用例并构建。运行 `make.bash` 将调用构建 Go 完整的 3 个引导步骤,这在我的旧机器上大概需要 50 秒时间。 + +一旦构建完成,工具链安装在 `src` 同级的 `bin` 目录中。然后,我们可以通过运行 `bin/go` 安装 `cmd/compile` 来快速重新构建编译器。 + +[\[1\]](https://eli.thegreenplace.net/2019/go-compiler-internals-adding-a-new-statement-to-go-part-1/#id1) Go 有一些特殊的“魔法”、`range` 子句,比如在字符串上使用 `range` 子句,把字符串分隔成字符。这个地方就用了“转换”来实现。 + +如果要评论,请给我发 [邮件](eliben@gmail.com),或者在[推特](https://twitter.com/elibendersky)上联系我。 + +--- + +via: https://eli.thegreenplace.net/2019/go-compiler-internals-adding-a-new-statement-to-go-part-1/ + +作者:[Eli Bendersky](https://eli.thegreenplace.net) +译者:[suhanyujie](https://github.com/suhanyujie) +校对:[JYSDeveloper](https://github.com/JYSDeveloper) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190704-Go-compiler-internals-adding-a-new-statement-to-Go-Part2.md b/published/tech/20190704-Go-compiler-internals-adding-a-new-statement-to-Go-Part2.md new file mode 100644 index 000000000..4215fe3d4 --- /dev/null +++ b/published/tech/20190704-Go-compiler-internals-adding-a-new-statement-to-Go-Part2.md @@ -0,0 +1,344 @@ +首发于:https://studygolang.com/articles/27154 + +# Go 编译器内部知识:向 Go 添加新语句-第 2 部分 + +这是探讨 Go 编译器两篇文章的最后一篇。在[第 1 部分中](https://studygolang.com/articles/25101),我们通过构建自定义的编译器,向 Go 语言添加了一条新语句。为此,我们按照此图介绍了编译器的前五个阶段: + +![go compiler flow](https://raw.githubusercontent.com/studygolang/gctt-images/master/compiler-internal/go-compiler-flow.png) + +在"rewrite AST"阶段前,我们实现了 until 到 for 的转换;具体来说,在[gc/walk.go](https://github.com/golang/go/blob/master/src/cmd/compile/internal/gc/walk.go)文件中,在编译器进行 SSA 转换和代码生成之前,就已进行了类似的转换。 + +在这一部分中,我们将通过在编译流程中处理新的 until 关键字来覆盖编译器的剩余阶段。 + +## SSA + +在 GC 运行 walk 变换后,它调用 buildssa([gc/ssa.go](https://github.com/golang/go/blob/master/src/cmd/compile/internal/gc/ssa.go#L281))函数将 AST 转换为[静态单赋值(SSA)形式](https://en.wikipedia.org/wiki/Static_single_assignment_form)的中间表示。 + +SSA 是什么意思,为什么编译器会这样做?让我们从第一个问题开始;我建议阅读上面链接的 SSA 维基百科页面和其他资源,但这里一个快速说明。 + +静态单赋值意味着 IR 中分配的每个变量仅分配一次。考虑以下伪 IR: + +``` +x = 1 +y = 7 +// do stuff with x and y +x = y +y = func() +// do more stuff with x and y +``` + +这不是 SSA,因为名称 x 和 y 被分配了多次。如果将此代码片段转换为 SSA,我们可能会得到类似以下内容: + +``` +x = 1 +y = 7 +// do stuff with x and y +x_1 = y +y_1 = func() +// do more stuff with x_1 and y_1 +``` + +注意每个赋值如何得到唯一的变量名。当 x 重新分配了另一个值时,将创建一个新名称 x_1。你可能想知道这在一般情况下是如何工作的……像这样的代码会发生什么: + +``` +x = 1 +if condition: x = 2 +use(x) +``` + +如果我们简单地将第二次赋值重命名为 x_1 = 2,那么 use 呢?x 或 x_1 或...呢?为了处理这一重要情况,SSA 形式的 IR 具有特殊的 phi(originally phony)功能,以根据其来自哪个代码路径来选择一个值。它看起来是这样的: + +![simple ssa phi](https://raw.githubusercontent.com/studygolang/gctt-images/master/compiler-internal/simple-ssa-phi.png) + +编译器使用此 phi 节点来维护 SSA,同时分析和优化此类 IR,并在以后的阶段用实际的机器代码代替。 + +SSA 名称的静态部分起着与静态类型类似的作用;这意味着在查看源代码时(在编译时或静态时),每个名称的分配都是唯一的,而它可以在运行时发生多次。如果上面显示的代码片段是在一个循环中,那么实际的 x_1 = 2 的赋值可能会发生多次。 + +现在我们对 SSA 是什么有了基本的了解,接下来的问题是为什么。 + +优化是编译器后端的重要组成部分[[1](#jump1)],并且通常对后端进行结构化以促进有效和高效的优化。再次查看此代码段: + +``` +x = 1 +if condition: x = 2 +use(x) +``` + +假设编译器想要运行一个非常常见的优化——常量传播; 也就是说,它想要在 x = 1 的赋值后,将所有的 x 替换为 1。这会怎么样呢?它不能只找到赋值后对 x 的所有引用,因为 x 可以重写为其他内容(例如我们的例子)。 + +考虑以下代码片段: + +``` +z = x + y +``` + +一般情况下,编译器必须执行数据流分析才能找到: + +1. x 和 y 指的是哪个定义?存在控制语句情况下,这并不容易,并且还需要进行优势分析(dominance analysis)。 +2. 在此定义之后使用 z 时,同样具有挑战性。 + +就时间和空间而言,这种分析的创建和维护成本很高。此外,它必须在每次优化之后重新运行它(至少一部分)。 + +SSA 提供了一个很好的选择。如果 z = x + y 在 SSA 中,我们立即知道 x 和 y 所引用的定义(只能有一个),并且我们立即知道在哪里使用 z(在这个语句之后对 z 的所有引用)。在 SSA 中,用法和定义都在 IR 中进行了编码,并且优化不会违反不变性。 + +## Go 编译器中的 SSA + +我们继续描述 Go 编译器中如何构造和使用 SSA。SSA 是 Go 的一个[相对较新的功能](https://blog.golang.org/go1.7)。除了将 AST 转换为 SSA 的大量代码([gc/ssa.go](https://github.com/golang/go/blob/master/src/cmd/compile/internal/gc/ssa.go)),其它大部分代码都位于[ssa](https://github.com/golang/go/tree/master/src/cmd/compile/internal/ssa)目录中,ssa 目录中的 README 文件是对 Go SSA 的非常有用的说明,请阅读一下! + +Go SSA 实现还拥有我见过的一些最好的编译器工具(已经在编译器上工作了很多年)。通过设置 GOSSAFUNC 环境变量,我们将获得一个 HTML 页面,其中包含所有编译阶段以及每个编译阶段之后的 IR,因此我们可以轻松地检索出需要进行哪些优化。额外的设置可以将控制流程图绘制成 SVG。 + +让我们研究一下从 AST 为该以下代码段创建的初始 SSA: + +```go +func usefor() { + i := 4 + for !(i == 0) { + i-- + sayhi() + } +} + +func sayhi() { + fmt.Println("Hello, for!") +} +``` + +我将移除打印输出函数的原因是为了使输出的 SSA 更简洁。使用-l 进行编译以禁用内联,这将导致对 sayhi()的微小调用(由于常量字符串而生成更多的代码,对 fmt.Println()[[2](#jump2)]的调用会生成更多代码)。 + +产生的 SSA 为: + +``` +b1: + + v1 (?) = InitMem + v2 (?) = SP + v3 (?) = SB + v4 (?) = Const64 [4] (i[int]) + v6 (?) = Const64 [0] + v9 (?) = Const64 [1] + Plain → b2 (10) + + b2: ← b1 b4 + + v5 (10) = Phi v4 v10 (i[int]) + v14 (14) = Phi v1 v12 + v7 (10) = Eq64 v5 v6 + If v7 → b5 b3 (unlikely) (10) + + b3: ← b2 + + v8 (11) = Copy v5 (i[int]) + v10 (11) = Sub64 v8 v9 (i[int]) + v11 (12) = Copy v14 + v12 (12) = StaticCall {"".sayhi} v11 + Plain → b4 (12) + + b4: ← b3 + Plain → b2 (10) + + b5: ← b2 + + v13 (14) = Copy v14 + Ret v13 +``` + +这里要注意的有趣部分是: + +- bN 是控制流图的基本块。 +- Phi 节点是显式的。最有趣的是对 v5 的分配。这恰恰是分配给 i 的选择器;一条路径来自 V4(初始化),从另一个 v10(在 i--)内循环中。 +- 出于本练习的目的,请忽略带有 的节点。Go 有一种有趣的方式来显式地在其 IR 中传播内存状态,在这篇文章中我们不讨论它。如果感兴趣,请参阅前面提到的 README 以了解更多详细信息。 + +顺便说一句,这里的 for 循环正是我们想要将 until 语句转换成的形式。 + +## 将 until AST 节点转换为 SSA + +与往常一样,我们的代码将以 for 语句的处理为模型。首先,让我们从控制流程图开始应该如何寻找 until 语句: + +![until cfg](https://raw.githubusercontent.com/studygolang/gctt-images/master/compiler-internal/until-cfg.png) + +现在我们只需要在代码中构建这个 CFG。提醒:我们在[第 1 部分](https://eli.thegreenplace.net/2019/go-compiler-internals-adding-a-new-statement-to-go-part-1/)中添加的新 AST 节点类型为 OUNTIL。我们将在 gc/ssa.go 中的[state.stmt](https://github.com/golang/go/blob/master/src/cmd/compile/internal/gc/ssa.go#L1024)方法中添加一个新的分支语句,以将具有 OUNTIL 操作的 AST 节点转换为 SSA。case 块和注释的命名应使代码易于阅读,并与上面显示的 CFG 相关。 + +```go +case OUNTIL: + // OUNTIL: until Ninit; Left { Nbody } + // cond (Left); body (Nbody) + bCond := s.f.NewBlock(ssa.BlockPlain) + bBody := s.f.NewBlock(ssa.BlockPlain) + bEnd := s.f.NewBlock(ssa.BlockPlain) + + bBody.Pos = n.Pos + + // first, entry jump to the condition + b := s.endBlock() + b.AddEdgeTo(bCond) + // generate code to test condition + s.startBlock(bCond) + if n.Left != nil { + s.condBranch(n.Left, bEnd, bBody, 1) + } else { + b := s.endBlock() + b.Kind = ssa.BlockPlain + b.AddEdgeTo(bBody) + } + + // set up for continue/break in body + prevContinue := s.continueTo + prevBreak := s.breakTo + s.continueTo = bCond + s.breakTo = bEnd + lab := s.labeledNodes[n] + if lab != nil { + // labeled until loop + lab.continueTarget = bCond + lab.breakTarget = bEnd + } + + // generate body + s.startBlock(bBody) + s.stmtList(n.Nbody) + + // tear down continue/break + s.continueTo = prevContinue + s.breakTo = prevBreak + if lab != nil { + lab.continueTarget = nil + lab.breakTarget = nil + } + + // done with body, goto cond + if b := s.endBlock(); b != nil { + b.AddEdgeTo(bCond) + } + + s.startBlock(bEnd) +``` + +如果您想知道 n.Ninit 的处理位置——它在 switch 之前针对所有节点类型统一完成。 + +实际上,这是我们要做的全部工作,直到在编译器的最后阶段执行语句为止!如果我们运行编译器-像以前一样在此代码上转储 SSA: + +```go +func useuntil() { + i := 4 + until i == 0 { + i-- + sayhi() + } +} + +func sayhi() { + fmt.Println("Hello, for!") +} +``` + +正如预期的那样,我们将获得 SSA,该 SSA 在结构上等效于条件为否的 for 循环的 SSA 。 + +## 转换 SSA + +构造初始 SSA 之后,编译器会在 SSA IR 上执行以下较长的遍历过程: + +1. 执行优化 +2. 将其降低到更接近机器代码的形式 + +所有这些都可以在在 ssa/compile.go 中的[passes](https://github.com/golang/go/blob/master/src/cmd/compile/internal/ssa/compile.go#L413)切片以及它们运行顺序的一些限制[passOrder](https://github.com/golang/go/blob/master/src/cmd/compile/internal/ssa/compile.go#L475)切片中找到。这些优化对于现代编译器来说是相当标准的。降低由我们正在编译的特定体系结构的指令选择以及寄存器分配。 + +有关这些遍的更多详细信息,请参见[SSA README](https://github.com/golang/go/blob/master/src/cmd/compile/internal/ssa/README.md)和[这篇帖子](https://quasilyte.dev/blog/post/go_ssa_rules/),其中详细介绍了如何指定 SSA 优化规则。 + +## 生成机器码 + +最后,编译器调用 genssa 函数([gc/ssa.go](https://github.com/golang/go/blob/master/src/cmd/compile/internal/gc/ssa.go#L5903))从 SSA IR 发出机器代码。我们不必修改任何代码,因为 until 语句包含在编译器其他地方使用的构造块,我们才为之发出的 SSA-我们不添加新的指令类型,等等。 + +但是,研究的 useuntil 函数生成的机器代码对我们是有指导意义的。Go 有[自己的具有历史根源的汇编语法](https://golang.org/doc/asm)。我不会在这里讨论所有细节,但是以下是带注释的(带有#注释)程序集转储,应该相当容易。我删除了一些垃圾回收器的指令(PCDATA 和 FUNCDATA)以使输出变小。 + +``` +"".useuntil STEXT size=76 args=0x0 locals=0x10 + 0x0000 00000 (useuntil.go:5) TEXT "".useuntil(SB), ABIInternal, $16-0 + + # Function prologue + + 0x0000 00000 (useuntil.go:5) MOVQ (TLS), CX + 0x0009 00009 (useuntil.go:5) CMPQ SP, 16(CX) + 0x000d 00013 (useuntil.go:5) JLS 69 + 0x000f 00015 (useuntil.go:5) SUBQ $16, SP + 0x0013 00019 (useuntil.go:5) MOVQ BP, 8(SP) + 0x0018 00024 (useuntil.go:5) LEAQ 8(SP), BP + + # AX will be used to hold 'i', the loop counter; it's initialized + # with the constant 4. Then, unconditional jump to the 'cond' block. + + 0x001d 00029 (useuntil.go:5) MOVL $4, AX + 0x0022 00034 (useuntil.go:7) JMP 62 + + # The end block is here, it executes the function epilogue and returns. + + 0x0024 00036 () MOVQ 8(SP), BP + 0x0029 00041 () ADDQ $16, SP + 0x002d 00045 () RET + + # This is the loop body. AX is saved on the stack, so as to + # avoid being clobbered by "sayhi" (this is the caller-saved + # calling convention). Then "sayhi" is called. + + 0x002e 00046 (useuntil.go:7) MOVQ AX, "".i(SP) + 0x0032 00050 (useuntil.go:9) CALL "".sayhi(SB) + + # Restore AX (i) from the stack and decrement it. + + 0x0037 00055 (useuntil.go:8) MOVQ "".i(SP), AX + 0x003b 00059 (useuntil.go:8) DECQ AX + + # The cond block is here. AX == 0 is tested, and if it's true, jump to + # the end block. Otherwise, it jumps to the loop body. + + 0x003e 00062 (useuntil.go:7) TESTQ AX, AX + 0x0041 00065 (useuntil.go:7) JEQ 36 + 0x0043 00067 (useuntil.go:7) JMP 46 + 0x0045 00069 (useuntil.go:7) NOP + 0x0045 00069 (useuntil.go:5) CALL runtime.morestack_noctxt(SB) + 0x004a 00074 (useuntil.go:5) JMP 0 +``` + +如果您注意的话,您可能已经注意到“cond”块移到了函数的末尾,而不是最初在 SSA 表示中的位置。是什么赋予的? + +答案是,“loop rotate”遍历将在 SSA 的最末端运行。此遍历对块重新排序,以使主体直接流入 cond,从而避免每次迭代产生额外的跳跃。如果您有兴趣,请参阅[ssa/looprotate.go](https://github.com/golang/go/blob/master/src/cmd/compile/internal/ssa/looprotate.go)了解更多详细信息。 + +## 结论 + +就是这样!在这两篇文章中,我们以两种不同的方式实现了一条新语句,从而知道了 Go 编译器的内部结构。当然,这只是冰山一角,但我希望它为您自己开始探索提供了一个良好的起点。 + +最后一点:我们在这里构建了一个可运行的编译器,但是 Go 工具都无法识别新的 until 关键字。不幸的是,此时 Go 工具使用了完全不同的路径来解析 Go 代码,并且没有与 Go 编译器本身共享此代码。我将在以后的文章中详细介绍如何使用工具处理 Go 代码。 + +## 附录-复制这些结果 + +要重现我们到此为止的 Go 工具链的版本,您可以从第 1 部分开始 ,还原 walk.go 中的 AST 转换代码,然后添加上述的 AST 到 SSA 转换。或者,您也可以从[我的 fork 中](https://github.com/eliben/go/tree/adduntil2)获取[adduntil2 分支](https://github.com/eliben/go/tree/adduntil2)。 + +要获得所有 SSA 的 SSA,并在单个方便的 HTML 文件中传递代码生成,请在构建工具链后运行以下命令: + +``` +GOSSAFUNC=useuntil /bin/go tool compile -l useuntil.go +``` + +然后在浏览器中打开 ssa.html。如果您还想查看 CFG 的某些通行证,请在函数名后添加通行名,以:分隔。例如 GOSSAFUNC = useuntil:number_lines。 + +要获取汇编代码码,请运行: + +``` +/bin/go tool compile -l -S useuntil.go +``` + +[1] 我特别尝试避免在这些帖子中过多地讲“前端”和“后端”。这些术语是重载和不精确的,但通常前端是在构造 AST 之前发生的所有事情,而后端是在表示形式上更接近于机器而不是原始语言的阶段。当然,这在中间位置留有很多地方,并且 中间端也被广泛使用(尽管毫无意义)来描述中间发生的一切。 + +在大型和复杂的编译器中,您会听到有关“前端的后端”和“后端的前端”以及类似的带有“中间”的混搭的信息。 + +在 Go 中,情况不是很糟糕,并且边界已明确明确地确定。AST 在语法上接近输入语言,而 SSA 在语法上接近。从 AST 到 SSA 的转换非常适合作为 Go 编译器的前/后拆分。 + +[2] -S 告诉编译器将程序集源代码转储到 stdout; -l 禁用内联,这会通过内联 fmt.Println 的调用而使主循环有些模糊。 + +--- +via: https://eli.thegreenplace.net/2019/go-compiler-internals-adding-a-new-statement-to-go-part-2/ + +作者:[Eli Bendersky](https://eli.thegreenplace.net/) +译者:[keob](https://github.com/keob) +校对:[@unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190707-go-unknown-parts-of-the-test-package.md b/published/tech/20190707-go-unknown-parts-of-the-test-package.md index 00510ee2d..e1ebe0df6 100644 --- a/published/tech/20190707-go-unknown-parts-of-the-test-package.md +++ b/published/tech/20190707-go-unknown-parts-of-the-test-package.md @@ -4,16 +4,16 @@ ![test package](https://github.com/studygolang/gctt-images/blob/master/20190707-go-unknown-parts-of-the-test-package/test-pkg.png?raw=true) -Go 被用得最频繁的命令我想应该是`go test`。然而,这个命令一些有趣的细节和用法可能你还不知道哟。下面让我们从测试本身讲起。 +Go 被用得最频繁的命令我想应该是 `go test`。然而,这个命令一些有趣的细节和用法可能你还不知道哟。下面让我们从测试本身讲起。 ## 规避缓存的习惯用法 -如果连续两次运行同一份测试且第一次完全通过的话,会发现测试只真正被运行了一次。事实上,所有测试都采用缓存机制来避免运行没有变化的测试样例。下面看`math`包的一个测试用例: +如果连续两次运行同一份测试且第一次完全通过的话,会发现测试只真正被运行了一次。事实上,所有测试都采用缓存机制来避免运行没有变化的测试样例。下面看 `math` 包的一个测试用例: ```bash -root@91bb4e4ab781:/usr/local/go/src# go test ./math/ +root@91bb4e4ab781:/usr/local/go/src# Go test ./math/ ok math 0.007s -root@91bb4e4ab781:/usr/local/go/src# go test ./math/ +root@91bb4e4ab781:/usr/local/go/src# Go test ./math/ ok math (cached) ``` @@ -28,15 +28,15 @@ PASS ok math 0.007s ``` -再执行一次的话缓存就会生效了。缓存是测试内容、环境变量和命令行参数的哈希。一旦计算出来,这个缓存会转储到`$GOCACHE`指向的文件夹(Unix 系统下默认是`$XDG_CACHE_HOME`或`$HOME/.cache`)。清空这个文件夹也就会清空缓存。 +再执行一次的话缓存就会生效了。缓存是测试内容、环境变量和命令行参数的哈希。一旦计算出来,这个缓存会转储到 `$GOCACHE` 指向的文件夹(Unix 系统下默认是 `$XDG_CACHE_HOME` 或 `$HOME/.cache`)。清空这个文件夹也就会清空缓存。 关于标识符的话,如[文档](https://golang.org/cmd/go/#hdr-Test_packages)所述,并不是所有标识符都是可缓存的: -> 缓存匹配的规则为:测试涉及的二进制可执行文件一样,同时命令行标识符属于'可缓存的'测试标识符限定子集(包括`-cpu`,`-list`,`parallel`,`-run`,`-short`和`-v`等)。使用任何不属于可缓存范围的标识符或参数都会导致缓存失效。显式屏蔽缓存的习惯用法是采用`-count=1`标识符。 +> 缓存匹配的规则为:测试涉及的二进制可执行文件一样,同时命令行标识符属于'可缓存的'测试标识符限定子集(包括 `-cpu`,`-list`,`parallel`,`-run`,`-short` 和 `-v` 等)。使用任何不属于可缓存范围的标识符或参数都会导致缓存失效。显式屏蔽缓存的习惯用法是采用 `-count=1` 标识符。 -因为`count`规定测试必须执行的次数,因此`-count=1`显式地声明测试应该不多不少地只运行一次,使得这个标识符成为规避缓存的最优习惯用法。 +因为 `count` 规定测试必须执行的次数,因此 `-count=1` 显式地声明测试应该不多不少地只运行一次,使得这个标识符成为规避缓存的最优习惯用法。 -再提一下:Go 1.12 之前可通过设置[`GOCACHE`](https://golang.org/doc/go1.12#gocache)环境变量`GOCACHE=off go test math/`的方式绕过缓存。 +再提一下:Go 1.12 之前可通过设置[`GOCACHE`](https://golang.org/doc/go1.12#gocache)环境变量 `GOCACHE=off Go test math/` 的方式绕过缓存。 运行测试时,Go 会逐个包依次运行它们。Go 处理测试包名的方式也给测试提供了更多策略。 @@ -117,9 +117,9 @@ func TestDeckCanDrawCards(t *testing.T) { } ``` -> 译者注:原文给出的`deck`包的`import`路径不对,已修正如上 +> 译者注:原文给出的 `deck` 包的 `import` 路径不对,已修正如上 -编写黑盒测试的唯一要求是给包名加上`_test`后缀。这个包被看作不同于`deck`的包,所以无法访问到非导出的函数。Go 原生支持这个方式,编译器不会抱怨同一个文件夹下有两个不同包名。 +编写黑盒测试的唯一要求是给包名加上 `_test` 后缀。这个包被看作不同于 `deck` 的包,所以无法访问到非导出的函数。Go 原生支持这个方式,编译器不会抱怨同一个文件夹下有两个不同包名。 白盒测试则检验牌组只会在第一次抽取时被洗一次: @@ -149,7 +149,7 @@ func TestDeckShouldBeShuffledOnce(t *testing.T) { } ``` -上述测试采用和`deck`相同的包名,因此能够访问到非导出的函数。 +上述测试采用和 `deck` 相同的包名,因此能够访问到非导出的函数。 但是,白盒测试有这么一个短板。黑盒测试导出的函数能够保证正确的结果不受包的内部实现影响。因此,我们能够自由地改变和优化内部实现而不破坏现有测试。而对于白盒测试,它是和内部实现绑定的,优化操作有可能会破坏现有测试。 @@ -157,13 +157,13 @@ func TestDeckShouldBeShuffledOnce(t *testing.T) { ## 只执行一次性能测试 -[Go 1.12](https://golang.org/doc/go1.12#testing)引入的`-benchtime=1x`、`-benchtime=10x`等允许性能测试只执行我们想要的次数。这个`-benchtime=1x`标识符对测试套件(test suite)是很有用的,它使得我们只需运行至少一次性能测试即可验证上一次变更是否破坏了现有代码。 +[Go 1.12](https://golang.org/doc/go1.12#testing)引入的 `-benchtime=1x`、`-benchtime=10x` 等允许性能测试只执行我们想要的次数。这个 `-benchtime=1x` 标识符对测试套件(test suite)是很有用的,它使得我们只需运行至少一次性能测试即可验证上一次变更是否破坏了现有代码。 -Go 1.12 之前的`-benchtime=1ns`标识符也能起到相同的效果,它会指示1ns后跳出性能测试的循环。因为 1 ns 是最小的时间单位,所以性能测试只会运行一次。性能测试为我们汇报诸如执行操作的时间、所需内存或堆上的内存分配次数等指标。Go 1.13 更是允许我们获取更多想要的指标。 +Go 1.12 之前的 `-benchtime=1ns` 标识符也能起到相同的效果,它会指示 1ns 后跳出性能测试的循环。因为 1 ns 是最小的时间单位,所以性能测试只会运行一次。性能测试为我们汇报诸如执行操作的时间、所需内存或堆上的内存分配次数等指标。Go 1.13 更是允许我们获取更多想要的指标。 ## 汇报自定义的指标 -Go 1.13 引入的`ReportMetric`方法允许我们汇报自定义的指标。复用一下之前牌组的示例,并修改为:抽取第一张牌之前先把牌组随机洗多次,次数在 1 到 20 次。以下是汇报洗牌次数的性能测试: +Go 1.13 引入的 `ReportMetric` 方法允许我们汇报自定义的指标。复用一下之前牌组的示例,并修改为:抽取第一张牌之前先把牌组随机洗多次,次数在 1 到 20 次。以下是汇报洗牌次数的性能测试: ```go package deck @@ -250,11 +250,11 @@ ok 1.529s 如结果所示,Go 1.13 还引入另一项变更:用准确的次数取代之前大致的[`b.N`](https://tip.golang.org/pkg/testing/#B.N)。这个[CL](https://go-review.googlesource.com/c/go/+/112155/)降低了诸如 GC 等外界噪声对性能测试的影响,尤其利于时长非常短的测试。测试速度也得到了提升: ```bash -// go 1.13 +// Go 1.13 BenchmarkDeckWithRandomShuffle-8 88666 12389 ns/op PASS ok 1.529s -// go 1.12 +// Go 1.12 BenchmarkDeckWithRandomShuffle-8 100000 12765 ns/op PASS ok 1.890s diff --git a/published/tech/20190707-let-us-write-a-simple-event-bus-in-go.md b/published/tech/20190707-let-us-write-a-simple-event-bus-in-go.md index 9439bf37e..46a22675d 100644 --- a/published/tech/20190707-let-us-write-a-simple-event-bus-in-go.md +++ b/published/tech/20190707-let-us-write-a-simple-event-bus-in-go.md @@ -59,7 +59,7 @@ type EventBus struct { ## 订阅主题 -对于订阅主题,使用 `channel`。它就像传统方法中的回调一样。当发布者向主题发布数据时,`channel`将接收数据。 +对于订阅主题,使用 `channel`。它就像传统方法中的回调一样。当发布者向主题发布数据时,`channel` 将接收数据。 ```go func (eb *EventBus)Subscribe(topic string, ch DataChannel) { @@ -86,7 +86,7 @@ func (eb *EventBus) Publish(topic string, data interface{}) { // 这样做是因为切片引用相同的数组,即使它们是按值传递的 // 因此我们正在使用我们的元素创建一个新切片,从而能正确地保持锁定 channels := append(DataChannelSlice{}, chans...) - go func(data DataEvent, dataChannelSlices DataChannelSlice) { + Go func(data DataEvent, dataChannelSlices DataChannelSlice) { for _, ch := range dataChannelSlices { ch <- data } @@ -134,16 +134,16 @@ func main() { eb.Subscribe("topic1", ch1) eb.Subscribe("topic2", ch2) eb.Subscribe("topic2", ch3) - go publisTo("topic1", "Hi topic 1") - go publisTo("topic2", "Welcome to topic 2") + Go publisTo("topic1", "Hi topic 1") + Go publisTo("topic2", "Welcome to topic 2") for { select { case d := <-ch1: - go printDataEvent("ch1", d) + Go printDataEvent("ch1", d) case d := <-ch2: - go printDataEvent("ch2", d) + Go printDataEvent("ch2", d) case d := <-ch3: - go printDataEvent("ch3", d) + Go printDataEvent("ch3", d) } } } diff --git a/published/tech/20190708-go-map-design-by-example-part-I.md b/published/tech/20190708-go-map-design-by-example-part-I.md index d4fef2ddc..03e159dd8 100644 --- a/published/tech/20190708-go-map-design-by-example-part-I.md +++ b/published/tech/20190708-go-map-design-by-example-part-I.md @@ -36,7 +36,7 @@ Go 将键值对存储在桶的列表中,每个桶容纳 8 个键值对,当 m ## Map 扩容 -在存储键值对时,桶会将它存储在 8 个可用的插槽中。如果这些插槽全部不可用,Go 会创建一个溢出桶,并于当前桶连接。 +在存储键值对时,桶会将它存储在 8 个可用的插槽中。如果这些插槽全部不可用,Go 会创建一个溢出桶,并与当前桶连接。 ![overflow bucket](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-map-design-by-example-part-I/1_ZfDObIafsML18crqW-MX_Q.png) diff --git a/published/tech/20190715-Passing-callbacks-and-pointers-to-Cgo.md b/published/tech/20190715-Passing-callbacks-and-pointers-to-Cgo.md new file mode 100644 index 000000000..305957fa3 --- /dev/null +++ b/published/tech/20190715-Passing-callbacks-and-pointers-to-Cgo.md @@ -0,0 +1,254 @@ +首发于:https://studygolang.com/articles/24447 + +# 传递回调函数和指针到 Cgo + +`Cgo` 允许 Go 程序调用 C 库或其他暴露了 C 接口的库。正是如此,这也成为 Go 程序员工具箱的重要组成部分。 + +使用 `Cgo` 可能会比较棘手,特别是在 Go 和 C 代码中传递指针和回调函数时。 +这篇文章讨论了一个端到端当例子,包含了如下几方面: +* `Cgo` 的基本使用,包括链接一个传统的 C 库到 Go 二进制文件中。 +* 从 Go 语言中传递 struct 到 C 语言中。 +* 传递 Go 函数到 C 程序中,并安排 C 程序在随后调用它们。 +* 安全的传递任意的 Go 数据到 C 代码中,这些 C 代码后续会回传这些数据到它所调用的 Go 回调中。 + +本文并不是一个 `Cgo` 的使用教程-在阅读前,需要你对它对简单使用案例有所熟悉。 +在本文最后列了一些有用的 `Cgo` 使用教程和相关的文章。这个案例的全部源代码详见[Github](https://github.com/eliben/code-for-blog/tree/master/2019/cgo-callback)。 + +## 问题所在-一个 C 库调用多个 Go 回调程序 + +如下是一个虚构的 C 库的头文件,该库处理(输入)数据,并基于事件调用回调函数。 + +```c +typedef void (*StartCallbackFn)(void* user_data, int i); +typedef void (*EndCallbackFn)(void* user_data, int a, int b); + +typedef struct { + StartCallbackFn start; + EndCallbackFn end; +} Callbacks; +// Processes the file and invokes callbacks from cbs on events found in the +// file, each with its own relevant data. user_data is passed through to the +// callbacks. +void traverse(char* filename, Callbacks cbs, void* user_data); +``` + +回调标签是由几个重要的模式组成,所展示的这些模式在现实中也同样普遍: + +* 每一个回调拥有自己的类型签名,这里为了简便,我们使用 `int` 类型的参数,这个参数可以是其他任何类型。 +* 当只有较小数量的回调被调用时,它们可能作为独立的参数被传递到 traverse 中;然而,回调的数量非常大时(比如说,超过三个),后几乎总是有一个汇集它们的结构体被传递。 +允许用户将某些回调参数设置为 null 很常见,以向底层库传达:对于某些特定事件并没有意义,也不应为此调用任何用户代码。 +* 每个回调都获得一个不透明指针 user_data,该指针从调用者传递到 traverse (最终传递到回调函数)。它用于区分互不相同的遍历,并传递用户特定的状态。 +典型的,traverse 会透传 user_data,而不尝试访问他; 由于它是 `void *`,因此它对于库是完全模糊的, +并且用户代码会将其强制转换为回调中的某些具体类型。 + +我们对 traverse 的实现仅是一个简单的模拟: + +```c +void traverse(char* filename, Callbacks cbs, void* user_data) { + // 模拟某些遍历,调用 start 回调,之后调用 end 回调 + // callback, if they are defined. + if (cbs.start != NULL) { + cbs.start(user_data, 100); + } + if (cbs.end != NULL) { + cbs.end(user_data, 2, 3); + } +} +``` + +我们的任务是包装这个库,在 Go 代码中进行使用。我们想要在遍历中调用 Go 回调,不用再写任何多余的 C 代码。 + +## Go 接口 + +让我们从构思在 Go 代码中我们接口的样式开始,如下是一个方式: + +```go +type Visitor interface { + Start(int) + End(int, int) +} +func GoTraverse(filename string, v Visitor) { + // ... 实现 +} +```` + +本文后续部分显示了使用此方法的完整实现。但是,它有一些缺点: +* 当我们需要提供大量的回调时,如果我们仅对几个回调感兴趣,编写 Visitor 的实现可能会很乏味。 +可以通过提供一个结构体来实现带有某些默认操作(例如,无操作)的完整接口来减轻这种情况,然后用户结构可以匿名继承此默认结构,而不必实现每个方法。 +尽管如此,带有大量方法的接口通常不是一个好的 Go 实践。 +* 一个更严重的限制是,很难向 C 遍历传达我们对某些回调不感兴趣的信息。 +根据定义,实现 Visitor 的对象将具有所有方法的实现,因此没有简单的方法来判断我们是否对调用其中的某些方法不感兴趣。 +这可能会对性能产生严重影响。 + +一个可替换的方法是模仿我们在 C 语言中拥有的方式;也就是说,创建一个整合函数对象的结构体: + +```go +type GoStartCallback func(int) +type GoEndCallback func(int, int) + +type GoCallbacks struct { + startCb GoStartCallback + endCb GoEndCallback +} +func GoTraverse(filename string, cbs *GoCallbacks) { + // ... 实现 +} +``` + +这立即解决了两个缺点:函数对象的默认值为 `nil`,GoTraverse 可以将其解释为“对此事件不感兴趣”,其中可以将相应的 C 回调设置为 `NULL`。 +由于 Go 函数对象可以是闭包或绑定方法,因此在不同的回调之间保留状态没有困难。 + +后附的代码示例在单独的目录中提供了此替代实现,但是在其余文章中,我们将继续使用 Go 接口的更惯用的方法。 +对于实现而言,选择哪种方法并不重要。 + +## Cgo 包装函数的实现 + +`Cgo` 指针传递规则不允许将 Go 函数值直接传递给 C,因此要注册回调,我们需要在 C 中创建包装器函数。 + +而且,我们也不能直接传递 Go 程序分配的指针到 C 程序中,因为 Go 的并发垃圾回收器会移动数据。 +`Cgo` 的[Wiki](https://github.com/golang/go/wiki/cgo#function-variables)提供了使用间接寻址的解决方法。 +在这里,我将使用 go-pointer 程序包,该程序包以稍微更方便,更通用的方式实现了相同目的。 + +考虑到这些,让我们之间进行实现。该代码初步看起来可能会比较晦涩,但这很快就会展现出他的意义。如下是 GoTraverse 的代码。 + +```go +import gopointer "github.com/mattn/go-pointer" + +func GoTraverse(filename string, v Visitor) { + cCallbacks := C.Callbacks{} + + cCallbacks.start = C.StartCallbackFn(C.startCgo) + cCallbacks.end = C.EndCallbackFn(C.endCgo) + + var cfilename *C.char = C.CString(filename) + defer C.free(unsafe.Pointer(cfilename)) + + p := gopointer.Save(v) + defer gopointer.Unref(p) + + C.traverse(cfilename, cCallbacks, p) +} +``` + +我们先在 Go 代码中创建 C 的回调结构,然后封装。因为我们不能直接将 Go 函数赋值给 C 函数指针,我们将在独立的 Go 文件[注 1]中定义这些包装函数。 + +```c +/* +extern void goStart(void*, int); +extern void goEnd(void*, int, int); + +void startCgo(void* user_data, int i) { + goStart(user_data, i); +} + +void endCgo(void* user_data, int a, int b) { + goEnd(user_data, a, b); +} +*/ +import "C" +``` + +这些是非常轻量的、调用 Go 函数的包装器——我们不得不为每一类的回调写这样一个 C 函数。我们很快就会看到 Go 函数 goStart 和 goEnd。 +在填充这个 C 回调结构体后,GoTraverse 会将文件名从 Go 字符串转换为 C 字符串(`Wiki` 中有详细信息)。 +之后,它创建一个代表 Go 访问者的值,我们可以使用 go-pointer 包将其传递给 C。最后,它调用 traverse。 + +完成这个实现,goStart 和 goEnd 代码如下: + +```go +//export goStart +func goStart(user_data unsafe.Pointer, i C.int) { + v := gopointer.Restore(user_data).(Visitor) + v.Start(int(i)) +} + +//export goEnd +func goEnd(user_data unsafe.Pointer, a C.int, b C.int) { + v := gopointer.Restore(user_data).(Visitor) + v.End(int(a), int(b)) +} +``` + +导出指令意味着这些功能对于 C 代码是可见的。 它们的签名应具有 C 类型或可转换为 C 类型的类型。 它们的行为类似: +1. 从 user_data 解压缩访问者对象 +2. 在访问者上调用适当的方法 + +## 详细的调用流程 + +让我们研究一下“开始”事件的回调调用流程,以更好地了解各个部分是如何连接在一起的。 +GoTraverse 将 startCgo 赋值给 Callbacks 结构体中的 start 指针,Callbacks 结构体将被传递给 traverse。因此,traverse 遇到 start 事件时,它将调用 startCgo。 +回调的参数包括:传递给 traverse 的 user_data 指针以及事件特定的参数(该例中为一个 int 类型的参数)。 + +startCgo 是 goStart 的填充程序,并使用相同的参数调用它。 + +goStart 解压缩由 GoTraverse 打包到 user_data 中的 Visitor 实现,并从那里调用 Start 方法,并向其传递事件特定的参数。 +到这一点为止,所有代码都由 Go 库包装 traverse 提供;从这里开始,我们进入由 `API` 用户编写的自定义代码。 + +## 通过 C 代码传递 Go 指针 + +此实现的另一个关键细节是我们用于将 Visitor 封装在 `void * user_data` 内在 C 回调来回传递的的技巧。 + +[Cgo 文档](https://golang.org/cmd/cgo/#hdr-Passing_pointers)指出: + +> 如果 Go 代码指向的 Go 内存不包含任何 Go 指针,则 Go 代码可以将 Go 指针传递给 C。 + +但是,我们当然不能保证任意的 Go 对象不包含任何指针。除了明显使用指针外,函数值,切片,字符串,接口和许多其他对象还包含隐式指针。 + +限制源于 Go 垃圾收集器的性质,该垃圾收集器与其他代码同时运行,并允许移动数据,从 C 角度来看,会使指针无效。 + +所以,我们能做些什么?如上所述,解决方案是间接的,Cgo`Wiki` 提供了一个简单的示例。 +我们没有直接将指针传递给 C ,而是将其保留在 Go 板块中,并找到了一种间接引用它的方法; +例如,我们可以使用一些数字索引。这保证了所有指针对于 Go 的垃圾回收仍然可见,但是我们可以在 C 板块中保留一些唯一的标识符,以便以后我们访问它们。 + +通过在 unsafe.Pointer(映射到 Cgo 对 C 的调用中直接 `void *`)和 `interface{}` 之间创建一个映射,go-pointer 包便可以做到这一点,从本质上讲,我们可以存储任意的 Go 数据并提供唯一的 ID(unsafe.Pointer)以供后续引用。 +为什么不像 `Wiki` 示例中那样使用 unsafe.Pointer 代替 `int`?因为不明确的数据通常在 C 语言中用 `void *` 表示,所以不安全。指针是自然映射到它的东西。如果使用 `int`,我们将不得不去考虑在其他几个地方进行转换。 + +## 如果没有 user_data 呢? + +看到我们如何使用 user_data, 使其穿越 C 代码回到我们的回调函数,以传输特定于用户 Visitor 的实现,人们可能会想-如果没有可用的 user_data 怎么办? +事实证明,在大多数情况下,都存在诸如 user_data 之类的东西,因为没有它,原始的 C `API` 就有缺陷。再次考虑遍历示例,但是这项没有 user_data: + +```c +typedef void (*StartCallbackFn)(int i); +typedef void (*EndCallbackFn)(int a, int b); + +typedef struct { + StartCallbackFn start; + EndCallbackFn end; +} Callbacks; + +void traverse(char* filename, Callbacks cbs); +``` + +假设我们提供一个回调作为开始: + +```c +void myStart(int i) { + // ... +} +``` + +在 myStart 中,我们有些困惑了。我们不知道调用哪个遍历-可能有许多不同的遍历,不同的文件和数据结构满足不同的需求。我们也不知道在哪里记录事件的结果。这里唯一的办法是使用全局数据。这是一个不好的 `API`! + +有了这样的 `API`,我们在 Go 板块的情况就不会差很多。我们还可以依靠全局数据来查找与此特定遍历有关的信息,并且我们可以使用相同的 Go 指针技巧在此全局数据中存储任意 Go 对象。但是,这种情况不太可能出现,因为 C `API` 不太可能忽略此关键细节。 + +## 附属资源链接 + +关于使用 `Cgo` 的信息还有很多,其中有些是过时的(在明确定义传递指针的规则之前)。如下是我在准备这篇文章时发现的特别有用的链接集合: + +* [官方的 Cgo 文档](https://golang.org/cmd/cgo/)是权威指南。 +* [Cgo 的 Wiki 页面](https://github.com/golang/go/wiki/cgo)是相当有用的。 +* [Go 语言并发垃圾回收的一些细节](https://blog.golang.org/go15gc)。 +* Yasuhiro Matsumoto 的[从 C 调用 Go](https://dev.to/mattn/call-go-function-from-c-function-1n3)的文章。 +* 指针传递规则的[详细细节](https://github.com/golang/proposal/blob/master/design/12416-cgo-pointers.md)。 + +[注 1]由于 Cgo 生成和编译 C 代码的特殊性,它们位于单独的文件中-有关[Wiki](https://github.com/golang/go/wiki/cgo#export-and-definition-in-preamble)的更多详细信息。我没有对这些函数使用静态内联技巧的原因是我们必须获取它们的地址。 + +---------------- + +via: https://eli.thegreenplace.net/2019/passing-callbacks-and-pointers-to-cgo/ + +作者:[Eli Bendersky](https://eli.thegreenplace.net) +译者:[amzking](https://github.com/amzking) +校对:[DingdingZhou](https://github.com/DingdingZhou) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190717-The-cost-of-syntactic-sugar-in-Go.md b/published/tech/20190717-The-cost-of-syntactic-sugar-in-Go.md new file mode 100644 index 000000000..b6a085e7f --- /dev/null +++ b/published/tech/20190717-The-cost-of-syntactic-sugar-in-Go.md @@ -0,0 +1,134 @@ +首发于:https://studygolang.com/articles/25120 + +# 语法糖的代价 + +在 Go 语言中,你可以用少量的代码表达很多东西。您通常可以查看一小段代码并清楚地了解此程序的功能。这在 Go 社区中被称为地道的 Go 代码。保持跨项目代码的一致性需要持续不断地努力。 + +当我遇到 Go 的部分看起来不像地道 Go 代码时,这通常是有原因的。最近,我注意到 Go 中的接口切片(或抽象数组)工作方式有点怪异。这种怪异有助于理解在 Go 中使用复杂类型会带来一些成本,而且这些[语法糖](https://en.wikipedia.org/wiki/Syntactic_sugar)并不总是没有代价的。为深入了解问题出现的原因,我对遇到的行为进行拆分,这助于阐明 Go 的一些设计原则 + +## 举例说明 + +我们将编写一个小型程序,它定义一个动物列表(例如,dogs),并调用一个函数,将每个动物的噪声输出到控制台 + +```go +animals := []Animal{Dog{}} +PrintNoises(animals) +``` + +程序成功通过编译,并在控制台打印出了“ Woof!”。下面就是这个程序的类似的版本: + +```go +dogs := []Dog{Dog{}} +PrintNoises(dogs) +``` + +程序无法编译,并将以下错误打印到控制台,而不是输出 "Woof!" + +```go +cannot use dogs (type []Dog) as type []Animal in argument to PrintNoises +``` + +如果你熟悉 Go,你可能会认为应该检查一下 `Dog` 实现了 `Animal`,对吧? 如果是实现错误,它的输出应该类似于 + +```go +cannot use dogs (type []Dog) as type []Animal in argument to PrintNoises: []Dog does not implement []Animal (missing Noise method) +``` + +为什么第一个程序可以用 `Dog` 作为 `Animal` 来编译和运行,而第二个程序却不能,即使它们看起来都是地道的和正确的 + +下面是本例中用作参考的其余代码。你可以通过编译它,来了解上述用法的内部原理 + +```go +type Animal interface { + Noise() string +} + +type Dog struct{} + +func (Dog) Noise() string { + return "Woof!" +} + +func PrintNoises(as []Animal) { + for _, a := range as { + fmt.Println(a.Noise()) + } +} +``` + +## 进一步简化问题 + +让我们试着用一种更简单的方法来复现这个问题,以便更好地理解它。静态类型检查是一种有用的 Go pattern,用于断言类型是否实现了接口。让我们先检查一下 `Dog` 是否实现了 `Animal` + +```go +var _ Animal = Dog{} +``` + +上面代码编译成功。那我们接下来就检查程序中用到的 `slices` + +```go +var _ []Animal = []Dog{} +``` + +上面代码没有编译成功,编译器报错: + +```go +cannot use []Dog literal (type []Dog) as type []Animal in assignment +``` + +现在,我们已经复现了一个与例子类似(但不是完全相同)的错误。利用这些不同的线索,我做了一些研究来找出如何解决这个问题,以及为什么会发生这样的事情 + +## 寻找解决方案 + +在做了一些研究之后,我发现了两件事:一个是解决方案,另一个是背后的原理。我们从修正程序开始,因为它有助于说明基本原理 + +下面是最初未能编译的代码的一个修复: + +```go +dogs := []Dog{Dog{}} +// 新逻辑: 把 dogs 的切片转换成 animals 的切片 +animals := []Animal{} +for _, d := range dogs { + animals = append(animals, Animal(d)) +} +PrintNoises(animals) +``` + +通过将 `Dog` 的切片转换为 `Animal` 的切片,现在可以将其传入 `Printnoise` 函数并成功运行。当然,这看起来有点傻,因为它基本上是已经运行的第一个程序的冗长版本。然而,在一个更大的项目中,这一点可能并那么明显。修复的代价是多了四行代码。这四行代码似乎是多余,直到你开始考虑作为开发人员必须修复它的原因 + +## 寻找原理 + +现在你知道如何修复它,我们来探究它背后的原理。我找到了不错的解析:[go 不支持切片中协变](https://www.reddit.com/r/golang/comments/3gtg3i/passing_slice_of_values_as_slice_of_interfaces/) +(译者注: [协变](https://zh.wikipedia.org/wiki/%E5%8D%8F%E5%8F%98%E4%B8%8E%E9%80%86%E5%8F%98): 原文单词为 covariance, 是指在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语) + +换句话说,Go 不会执行导致 O(N) 线性操作的类型转换(如切片的情况),而是将责任委托给开发人员。也就是说,执行这种类型的转换是有成本的。不过,Go 并不是每次都这样做。例如,当将字符串转换为 []byte 节时,Go 将免费为您执行这种线性转换,这可能是因为这种转换通常很方便。这只是语言中语法糖的众多例子之一。对于切片(和其他非基本类型),Go 选择不为您承担执行此操作的额外成本 + +这是有道理的——在我使用 Go 的 3 年里,这是我第一次遇到这种情况。这可能是因为 Go 在语法中灌输了“simpler is better”的思想 + +## 结论 + +一门语言的作者通常会在语法糖方面做出权衡,有时他们会添加功能,即便这会使语言变得更加臃肿,有时他们会将成本转嫁给开发人员。我认为,不隐式地执行高开销的操作的决定在保持 Go 语言地道、整洁、可控上有积极的影响。 + +上面的例子只是这个道理的一个应用。这个例子表明,熟悉一种语言的习惯用法有副作用。对设计决策保持深思熟虑总是一个好主意,而不是期望语言或编译器能帮到你。 + +我鼓励您在 Go 中寻找更多存在权衡语法的地方。它能帮助你更好地理解这门语言。我亦如此。 + +## 引用 + +以下是本文的引用: + +* [GitHub Gist of the above example](https://gist.github.com/asilvr/4d4da3cdc8180c5a9740d2890d833923) +* [Go 语言官网](https://golang.org) +* [Thread on covariance in Go](https://www.reddit.com/r/golang/comments/3gtg3i/passing_slice_of_values_as_slice_of_interfaces/) +* [Big-O notation](https://en.wikipedia.org/wiki/Big_O_notation) +* [Syntactic sugar](https://en.wikipedia.org/wiki/Syntactic_sugar) + +--- + +via: https://medium.com/@asilvr/the-cost-of-syntactic-sugar-in-go-5aa9dc307fe0 + +作者:[Katy Slemon](https://medium.com/@katyslemon) +译者:[Alex1996a](https://github.com/Alex1996a) +校对:[lxbwolf](https://github.com/lxbwolf) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190718-Garbage-Collection-In-Go-Part-III-GC-Pacing.md b/published/tech/20190718-Garbage-Collection-In-Go-Part-III-GC-Pacing.md new file mode 100644 index 000000000..ae4392b91 --- /dev/null +++ b/published/tech/20190718-Garbage-Collection-In-Go-Part-III-GC-Pacing.md @@ -0,0 +1,378 @@ +首发于:https://studygolang.com/articles/24562 + +# Go 垃圾回收:第三部分 - GC 的步调 + +## 前言 + +这是三篇系列文章中的第三篇。该系列文章提供了一种对 Go 垃圾回收背后的机制和概念的理解。本篇的主要内容是 GC 如何控制自己的步调。 + +三篇文章的索引:
+1)[Go 垃圾回收:第一部分 - 概念](https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part1-semantics.html)
+2)[Go 垃圾回收:第二部分 - GC 追踪](https://www.ardanlabs.com/blog/2019/05/garbage-collection-in-go-part2-gctraces.html)
+3)[Go 垃圾回收:第三部分 - GC 的步调](https://www.ardanlabs.com/blog/2019/07/garbage-collection-in-go-part3-gcpacing.html)
+ +## 简介 + +在第二篇文章里,我向你展示了垃圾回收器的行为以及如何使用工具查看回收器给你的运行程序带来的延迟。我带着你运行了一个真实的 Web 应用并向你展示了如何生成 GC 追踪和应用性能分析。接着我还向你展示了如何解读这些工具的输出,以便你找到提高应用程序性能的方法。 + +第二篇文章的结论与第一篇相同:如果你减少了堆上的压力,就可以降低延迟成本,从而提高应用程序的性能。对回收器友好的最佳策略就是减少每个任务所需内存分配的次数或大小。本篇中,我将向你展示步调算法是如何持续找出给定工作压力下的最优步调的。 + +## 并发示例代码 + +我将使用位于该链接处的代码:https://github.com/ardanlabs/gotraining/tree/master/topics/go/profiling/trace + +这个程序将找出某个特定主题在一组 RSS 新闻摘要文档中的出现频率。程序中包含了不同版本的查找算法以测试不同的并发模式。我将主要关注 `freq`,`freqConcurrent` 和 `freqNumCPU` 这几个版本。 + +*注:我是在一台 Macbook Pro 上使用 go1.12.7 运行的代码,该机配备了一个具有 12 个硬件线程的 Intel i9 处理器。在不同的体系架构、操作系统和 Go 版本下,你会看到不同的结果,但本篇的核心结论应该保持不变。* + +我首先从 `freq` 版本开始。它表示这个程序的非并发串行版本,并将为下文的并发版本提供一个基准。 + +**清单 1** +```go +01 func freq(topic string, docs []string) int { +02 var found int +03 +04 for _, doc := range docs { +05 file := fmt.Sprintf("%s.xml", doc[:8]) +06 f, err := os.OpenFile(file, os.O_RDONLY, 0) +07 if err != nil { +08 log.Printf("Opening Document [%s] : ERROR : %v", doc, err) +09 return 0 +10 } +11 defer f.Close() +12 +13 data, err := ioutil.ReadAll(f) +14 if err != nil { +15 log.Printf("Reading Document [%s] : ERROR : %v", doc, err) +16 return 0 +17 } +18 +19 var d document +20 if err := xml.Unmarshal(data, &d); err != nil { +21 log.Printf("Decoding Document [%s] : ERROR : %v", doc, err) +22 return 0 +23 } +24 +25 for _, item := range d.Channel.Items { +26 if strings.Contains(item.Title, topic) { +27 found++ +28 continue +29 } +30 +31 if strings.Contains(item.Description, topic) { +32 found++ +33 } +34 } +35 } +36 +37 return found +38 } +``` + +清单 1 展示的是 `freq` 函数。这个串行版本遍历一个文件名集合并执行 4 种操作:打开文件、读取文件、解码和检索。它对每个文件执行上述操作,一次一个。 + +在我的机器上运行这个版本的 `freq` 时,我得到了如下的结果: + +**清单 2** +```c +$ time ./trace +2019/07/02 13:40:49 Searching 4000 files, found president 28000 times. +./trace 2.54s user 0.12s system 105% CPU 2.512 total +``` + +从 `time` 的输出中可以看到,程序处理 4000 个文件用了大约 2.5 秒。能看到垃圾回收在其中所占的比例当然是很好的,你可以通过查看程序的追踪结果做到这点。由于这是一个启动并完成的程序,你可以使用 `trace` 包生成一个追踪。 + +**清单 3** +```go +03 import "runtime/trace" +04 +05 func main() { +06 trace.Start(os.Stdout) +07 defer trace.Stop() +``` + +清单 3 展示的是从程序生成追踪所需的代码。从标准库的 `runtime` 文件夹里导入 `trace` 包后,调用 `trace.Start` 和 `trace.Stop`。将追踪输出定向到 `os.Stdout` 只是简化了代码。 + +有了这段代码,现在你可以重新编译和运行这个程序了。不要忘了把标准输出重定向到一个文件。 + +**清单 4** +```c +$ Go build +$ time ./trace > t.out +Searching 4000 files, found president 28000 times. +./trace > t.out 2.67s user 0.13s system 106% CPU 2.626 total +``` + +运行时间增加了 100 毫秒多一点,但这是意料之中的。追踪捕捉了每次函数调用,进入和退出,直到微秒精度。重要的是现在有了一个名为 `t.out` 的文件,里面有追踪数据。 + +为了查看追踪结果,需要使用追踪工具来运行追踪数据。 + +**清单 5** +```c +$ Go tool trace t.out +``` + +执行以上命令会启动 Chrome 浏览器并显示以下内容: + +*注:追踪工具使用了 Chrome 浏览器内置的工具,所以它只能在 Chrome 里工作。* + +**图 1** + +![图 1](https://github.com/studygolang/gctt-images/blob/master/Garbage-Collection-In-Go-Part-3-GC-Pacing/103_figure1.png?raw=true) + +图 1 展示的是追踪工具启动时显示的 9 个链接。当下最重要的是第一个标着 `View trace` 的链接。点击之后,你就会看到与下图类似的画面: + +**图 2** + +![图 2](https://github.com/studygolang/gctt-images/blob/master/Garbage-Collection-In-Go-Part-3-GC-Pacing/103_figure2.png?raw=true) + +图 2 展示的是在我的机器上运行程序时的完整追踪窗口。本篇中,我会重点关注与垃圾回收器相关的部分,即标着 `Heap` 的第二部分和标着 `GC` 的第四部分。 + +**图 3** + +![图 3](https://github.com/studygolang/gctt-images/blob/master/Garbage-Collection-In-Go-Part-3-GC-Pacing/103_figure3.png?raw=true) + +图 3 更详细地展示了追踪的前 200 毫秒。注意观察 `Heap`(绿色和橙色区域)和 `GC`(底部的蓝线)。`Heap` 部分向你展示了两个信息:橙色区域代表在每个微秒时刻对应的堆上正在使用的空间,绿色代表触发下次回收的堆使用量。这就是为什么每当橙色区域到达绿色区域的顶端就会发生一次垃圾回收的原因。蓝线表示一次垃圾回收。 + +在这个版本的程序整个运行过程中,堆上内存使用量一直保持在大约 4 MB。要想查看每次垃圾回收时的统计数据,你可以使用选择工具在所有蓝线周围绘制一个框。 + +**图 4** + +![图 4](https://github.com/studygolang/gctt-images/blob/master/Garbage-Collection-In-Go-Part-3-GC-Pacing/103_figure4.png?raw=true) + +图 4 展示的是如何使用箭头工具在蓝线周围绘制蓝框。你应该想要框住每一条线。框中的数字表示选中的项目消耗的总时间。上图中,有接近 316 毫秒(ms,μ s,ns)的区域被选中。当所有的蓝色线条被选中时,就得到了如下的统计结果: + +**图 5** + +![图 5](https://github.com/studygolang/gctt-images/blob/master/Garbage-Collection-In-Go-Part-3-GC-Pacing/103_figure5.png?raw=true) + +图 5 显示所有的蓝线都在 15.911 毫秒标记到 2.596 秒标记之间。一共有 232 次垃圾回收,共消耗 64.524 毫秒,平均每次消耗 287.121 微秒。而程序运行需要 2.626 秒,这就意味着垃圾回收只占总运行时间的 2%。基本上,垃圾回收是运行该程序的一个微不足道的成本。 + +有了这个基准,可以使用并发算法完成相同的工作,以期加快程序的速度。 + +**清单 6** +```go +01 func freqConcurrent(topic string, docs []string) int { +02 var found int32 +03 +04 g := len(docs) +05 var wg sync.WaitGroup +06 wg.Add(g) +07 +08 for _, doc := range docs { +09 Go func(doc string) { +10 var lFound int32 +11 defer func() { +12 atomic.AddInt32(&found, lFound) +13 wg.Done() +14 }() +15 +16 file := fmt.Sprintf("%s.xml", doc[:8]) +17 f, err := os.OpenFile(file, os.O_RDONLY, 0) +18 if err != nil { +19 log.Printf("Opening Document [%s] : ERROR : %v", doc, err) +20 return +21 } +22 defer f.Close() +23 +24 data, err := ioutil.ReadAll(f) +25 if err != nil { +26 log.Printf("Reading Document [%s] : ERROR : %v", doc, err) +27 return +28 } +29 +30 var d document +31 if err := xml.Unmarshal(data, &d); err != nil { +32 log.Printf("Decoding Document [%s] : ERROR : %v", doc, err) +33 return +34 } +35 +36 for _, item := range d.Channel.Items { +37 if strings.Contains(item.Title, topic) { +38 lFound++ +39 continue +40 } +41 +42 if strings.Contains(item.Description, topic) { +43 lFound++ +44 } +45 } +46 }(doc) +47 } +48 +49 wg.Wait() +50 return int(found) +51 } +``` + +清单 6 展示的是 `freq` 的一个可能的并发版本。这个版本的核心设计模式是使用扇出模式。对于 `docs` 集合中的每个文件,都会创建一个 Goroutine 来处理。如果有 4000 个文档要处理,就要使用 4000 个 goroutine。这个算法的优点就是它是利用并发性的最简单方法。每个 Goroutine 处理一个且仅处理一个文件。可以使用 `WaitGroup` 执行等待处理每个文档的编排,并且原子指令可以使计数器保持同步。 + +这个算法的缺点在于它不能很好地适应文档或 CPU 核心的数量。所有的 Goroutine 在程序启动的时候就开始运行,这意味着很快就会消耗大量内存。由于在第 12 行使用了 `found` 变量,它还导致了缓存一致性的问题。由于每个核心共享这个变量的高速缓存行,这将导致内存抖动。随着文件或核心数量的增加,这会变得更糟。 + +有了代码,现在你可以重新编译和运行这个程序了。 + +**清单 7** +```c +$ Go build +$ time ./trace > t.out +Searching 4000 files, found president 28000 times. +./trace > t.out 6.49s user 2.46s system 941% CPU 0.951 total +``` + +从清单 7 中的输出可以看到,现在程序花了 951 毫秒来处理相同的 4000 个文件。这是大约 64% 的性能提升。看一下追踪结果。 + +**图 6** + +![图 6](https://github.com/studygolang/gctt-images/blob/master/Garbage-Collection-In-Go-Part-3-GC-Pacing/103_figure6.png?raw=true) + +图 6 展示了这个版本的程序在我的机器上运行时是怎样占用了比之前多得多的 CPU。图中的开始部分有很多密集的线条,这是因为所有的 Goroutine 都被创建之后,它们运行并且开始尝试从堆上分配内存。很快的,前 4 MB 内存被分配了,紧接着就有一个 GC 启动了。在这次 GC 当中,每个 Goroutine 都有时间运行,大部分由于在堆上申请内存而被置于等待状态。至少有 9 个 Goroutine 继续运行并在 GC 结束时将堆增长到大约 26 MB。 + +**图 7** + +![图 7](https://github.com/studygolang/gctt-images/blob/master/Garbage-Collection-In-Go-Part-3-GC-Pacing/103_figure7.png?raw=true) + +图 7 展示了在首次 GC 的大部分时间里有很多 Goroutine 处于已就绪(Runnable)和运行中(Running)的状态以及这一幕是如何再次快速发生的。请注意,堆性能概要(profile)看起来不规则并且垃圾回收也不像之前那样有规律。如果你仔细看,就会发现第二次 GC 几乎是在第一次 GC 结束之后就立即开始了。 + +如果选中了所有的垃圾回收,你就能看到下面的一幕: + +**图 8** + +![图 8](https://github.com/studygolang/gctt-images/blob/master/Garbage-Collection-In-Go-Part-3-GC-Pacing/103_figure8.png?raw=true) + +图 8 显示图中所有的蓝线都在 4.828 毫秒标记到 906.939 毫秒标记之间。一共有 23 次垃圾回收,共占用 284.447 毫秒,平均每次占用 12.367 毫秒。知道程序运行需要 951 毫秒,垃圾回收在整个运行时间里占用了大约 34%。 + +这与串行版本在性能和 GC 时间上有显著差异。并行运行更多的 Goroutine 让工作时间缩短了大约 64%。代价就是大幅增加了机器上各种资源的占用。不幸的是,堆上内存的占用最高达到了大约 200 MB。 + +有了这个并发的基准,下一个并发算法试图更有效率的使用资源。 + +**清单 8** +```go +01 func freqNumCPU(topic string, docs []string) int { +02 var found int32 +03 +04 g := runtime.NumCPU() +05 var wg sync.WaitGroup +06 wg.Add(g) +07 +08 ch := make(chan string, g) +09 +10 for i := 0; i < g; i++ { +11 Go func() { +12 var lFound int32 +13 defer func() { +14 atomic.AddInt32(&found, lFound) +15 wg.Done() +16 }() +17 +18 for doc := range ch { +19 file := fmt.Sprintf("%s.xml", doc[:8]) +20 f, err := os.OpenFile(file, os.O_RDONLY, 0) +21 if err != nil { +22 log.Printf("Opening Document [%s] : ERROR : %v", doc, err) +23 return +24 } +25 +26 data, err := ioutil.ReadAll(f) +27 if err != nil { +28 f.Close() +29 log.Printf("Reading Document [%s] : ERROR : %v", doc, err) +23 return +24 } +25 f.Close() +26 +27 var d document +28 if err := xml.Unmarshal(data, &d); err != nil { +29 log.Printf("Decoding Document [%s] : ERROR : %v", doc, err) +30 return +31 } +32 +33 for _, item := range d.Channel.Items { +34 if strings.Contains(item.Title, topic) { +35 lFound++ +36 continue +37 } +38 +39 if strings.Contains(item.Description, topic) { +40 lFound++ +41 } +42 } +43 } +44 }() +45 } +46 +47 for _, doc := range docs { +48 ch <- doc +49 } +50 close(ch) +51 +52 wg.Wait() +53 return int(found) +54 } +``` + +清单 8 展示的是 `freqNumCPU` 版本的程序。该版本的核心设计模式是池模式。由一个基于逻辑处理器数目的 Goroutine 池来处理所有的文件。如果有 12 个逻辑处理器可以使用,就使用 12 个 goroutine。这个算法的优点是它从头到尾保持了资源占用的一致性。由于使用了固定数量的 goroutine,所以只需要分配那 12 个 Goroutine 运行所需的内存。这也解决了高速缓存一致性问题导致的内存抖动。这是因为第 14 行调用的原子指令只会发生一个很小的固定次数。 + +该算法的缺点就是它更复杂了。它额外使用了一个 channel 来给 Goroutine 池分发工作。要想在每次使用池模式时都为池子指定一个“正确”数量的 Goroutine 是非常复杂的。作为一个通常的做法,我为池子启动了与逻辑处理器相同数量的 goroutine。然后通过压力测试或者使用生产指标,可以计算出一个最终的池子大小。 + +有了代码,现在你可以重新编译和运行这个程序了。 + +**清单 9** +```c +$ Go build +$ time ./trace > t.out +Searching 4000 files, found president 28000 times. +./trace > t.out 6.22s user 0.64s system 909% CPU 0.754 total +``` + +从清单 9 中的输出可以看到,现在程序花了 754 毫秒来处理相同的 4000 个文件。程序快了大约 200 毫秒,对于这样一个小的工作量而言,这是一个很大的提升。看一下追踪结果。 + +**图 9** + +![图 9](https://github.com/studygolang/gctt-images/blob/master/Garbage-Collection-In-Go-Part-3-GC-Pacing/103_figure9.png?raw=true) + +图 9 显示了这个版本的程序运行时也同样使用了我机器上所有的 CPU。仔细看的话,你就会发现一个固定的节奏在这个程序里又出现了,跟串行版本很像。 + +**图 10** + +![图 10](https://github.com/studygolang/gctt-images/blob/master/Garbage-Collection-In-Go-Part-3-GC-Pacing/103_figure10.png?raw=true) + +图 10 展示的是程序运行前 20 毫秒的核心指标的一个特写。与串行版本相比,垃圾回收肯定占用了更长的时间,但是有 12 个 Goroutine 同时在运行。使用的堆内存在整个程序的运行当中一直保持在 4 MB 左右,这又与串行版本一样了。 + +如果选中了所有的垃圾回收,你就能看到下面的一幕: + +**图 11** + +![图 11](https://github.com/studygolang/gctt-images/blob/master/Garbage-Collection-In-Go-Part-3-GC-Pacing/103_figure11.png?raw=true) + +图 11 显示图中所有的蓝线都位于 3.055 毫秒标记到 719.928 毫秒标记之间。共有 467 次垃圾回收,耗时 177.709 毫秒,平均每次花费 380.535 微秒。知道程序运行需要 754 毫秒,这意味着垃圾回收在总的运行时间里占了大约 25%,相比另一个并发版本降低了 9%。 + +这个版本的并发算法看起来可以适应更多的文件和核心。我认为它增加的复杂性成本是值得的。可以使用列表切片来代替 channel 将每个 Goroutine 的任务放到一个桶里。这必然会增加复杂度,尽管它可以减少由 channel 带来的延迟成本。虽然随着文件和核心的增多,这个收益可能会不容忽视,但是它带来的复杂度成本还是需要衡量的。这是你可以自己尝试的东西。 + +## 结语 + +我喜欢比较算法的三个版本就是要看看 GC 如何处理每种情况。处理文件所需的内存总量并不随程序版本而变化,不同的是程序如何分配内存。 + +当只有一个 Goroutine 时,只需要最起码的 4 MB 堆内存。当程序一次性分派了所有的工作,GC 采取的策略是使堆增长,减少收集的次数,但是每次运行更长的时间。当程序限制了并行处理的文件数量时,GC 又采取了保持一个小堆的策略,增加回收的次数,但是每次运行更少的时间。基本上,GC 采取的每个策略都是为了使 GC 对程序运行的影响最小。 + +```text +| Algorithm | Program | GC Time | % Of GC | # of GC’s | Avg GC | Max Heap | +|------------|---------|----------|---------|-----------|----------|----------| +| freq | 2626 ms | 64.5 ms | ~2% | 232 | 278 μ s | 4 meg | +| concurrent | 951 ms | 284.4 ms | ~34% | 23 | 12.3 ms | 200 meg | +| numCPU | 754 ms | 177.7 ms | ~25% | 467 | 380.5 μ s | 4 meg | +``` + +就两个并发版本而言,`freqNumCPU` 版本带来的额外好处是更好的处理高速缓存一致性的问题,这很有帮助。然而,每个程序在 GC 上花费的总时间差别不大,大约 284.4 毫秒对大约 177.7 毫秒。有时在我的机器上运行这些程序,这些数字会更加接近。使用 Go 1.13.beta1 版本做了一些实验,我甚至看到这两个算法运行了相同的时间。这可能意味着新版的 Go 有一些改进能够让 GC 更好的预测如何运行。 + +所有这些都让我有信心在程序运行时抛出大量任务。一个例子就是一个使用 50000 Goroutine 的 Web 服务,这就是一个本质上与第一个并发算法类似的扇出模式。GC 会研究工作量并为服务找到最优的步调来避开它。至少对我而言,使用 GC 是值得的,因为不用去考虑所有这些。 + +--- + +via: https://www.ardanlabs.com/blog/2019/07/garbage-collection-in-go-part3-gcpacing.html + +作者:[William Kennedy](https://github.com/ardanlabs) +译者:[Stonelgh](https://github.com/stonglgh) +校对:[DingdingZhou](https://github.com/DingdingZhou) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 + diff --git a/published/tech/20190724-vuejs-golang-a-rare-combination.md b/published/tech/20190724-vuejs-golang-a-rare-combination.md index cd6253d45..5463b50a3 100644 --- a/published/tech/20190724-vuejs-golang-a-rare-combination.md +++ b/published/tech/20190724-vuejs-golang-a-rare-combination.md @@ -4,7 +4,7 @@ ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/vuejs-golang/0*SJ43Bk4fxc44mgVR.jpg) -时间回到 2018 年,我写了一篇获得 15k 阅读的文章:Django + Angular 4 = A powerful web application。出于好奇心,我尝试了Angular4 和 Django 的组合。接着上个系列,这是一篇使用 Vuejs 和 Golang 来帮助你构建极佳应用的文章。 +时间回到 2018 年,我写了一篇获得 15k 阅读的文章:Django + Angular 4 = A powerful Web application。出于好奇心,我尝试了 Angular4 和 Django 的组合。接着上个系列,这是一篇使用 Vuejs 和 Golang 来帮助你构建极佳应用的文章。 我知道这两者一起用不是很常见,但是,让我们试一试。 @@ -74,7 +74,7 @@ Golang 很快是因为它的编译器,它不允许你定义多余的变量。 回到代码上。 -因此,我们将会使用 `encoding/json` and `net/http` 包。然后,我们定义 JSON 类型的数据结构。 +因此,我们将会使用 `encoding/json` and `net/http` 包。然后,我们  定义 JSON 类型的数据结构。 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/vuejs-golang/1*bBx8qYZkWpg8R92e8gT_5g.png) @@ -86,11 +86,11 @@ Golang 很快是因为它的编译器,它不允许你定义多余的变量。 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/vuejs-golang/1*JaFzDlfYH0LwTwLTOIs2Iw.png) -在代码第 33 行,我们定义了一个 `JSON`的译码器来转译从请求的实体中的传过来的 JSON 数据。 +在代码第 33 行,我们定义了一个 `JSON` 的译码器来转译从请求的实体中的传过来的 JSON 数据。 `numsData` 和 `numsResData` 是定义好的数据结构。 -接收的数据存储在 `numsData`中 并且在 38 行被转译。 +接收的数据存储在 `numsData` 中 并且在 38 行被转译。 然后我们设置 `ResponseWriter` 的 header 头部,并且在 47 行返回 JSON 格式的响应数据以及检查错误。 @@ -105,16 +105,16 @@ Golang 很快是因为它的编译器,它不允许你定义多余的变量。 首先使用 `cd` 进入前端目录并且使用以下命令安装依赖: ```shell -npm install --save bootstrap-vue bootstrap axios vee-validate +npm install --save bootstrap-vue Bootstrap axios vee-validate ``` -我们将使用 `axios` 来处理 POST 请求,使用`vee-validate` 校验表单的输入数据,使用 `bootstrap-vue` 构建优美的界面。 +我们将使用 `axios` 来处理 POST 请求,使用 `vee-validate` 校验表单的输入数据,使用 `bootstrap-vue` 构建优美的界面。 在 `src/Calculator.vue` 文件里编写前端部分代码: ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/vuejs-golang/1*13qy_tphvGcHiOM1wR3KIg.png) -从 17 行到 25 行,我们定义 input 输入框和 label 标签来获取数据。在Vue 中使用 `v-model`指令获取数据。 +从 17 行到 25 行,我们定义 input 输入框和 label 标签来获取数据。在 Vue 中使用 `v-model` 指令获取数据。 30-35 行完成计算器 UI 层的展示,并且 43 行定义了一个按钮,将会被触发 `postreq` 方法,这个方法会在接下来完成。 @@ -122,10 +122,10 @@ npm install --save bootstrap-vue bootstrap axios vee-validate ![](https://miro.medium.com/max/1400/1*2oy5ZoqYZVh0bF_iml9onw.png) -54-59 行是必须的,用于引入`axios` 和 `vee-validate`。 +54-59 行是必须的,用于引入 `axios` 和 `vee-validate`。 然后在 64-69 行我们定义一些变量,这些 `data` 变量用来存储计算器组件的变量的值。 -所有的函数都会定义在 `methods` 对象里。我们创建 `postreq()` 方法用来向 `http://localhost:8090/calc` 发送 JSON 格式的 POST 请求。还记得之前在 `server.go` 文件创建的 `calc` 方法吗?我们发送 JSON 数据后,后端返回结果后数据会被储存在 `add`, `mul`, `sub` 和 `div`等变量中,这些绑定在 HTML 的变量例如 {{ add }} 的占位符将会显示结果。 +所有的函数都会定义在 `methods` 对象里。我们创建 `postreq()` 方法用来向 `http://localhost:8090/calc` 发送 JSON 格式的 POST 请求。还记得之前在 `server.go` 文件创建的 `calc` 方法吗?我们发送 JSON 数据后,后端返回结果后数据会被储存在 `add`, `mul`, `sub` 和 `div` 等变量中,这些绑定在 HTML 的变量例如 {{ add }} 的占位符将会显示结果。 很简单是吧? 是的。 diff --git a/published/tech/20190725-Deploy-a-Golang-App-in-5-Minutes.md b/published/tech/20190725-Deploy-a-Golang-App-in-5-Minutes.md new file mode 100644 index 000000000..32f449aa1 --- /dev/null +++ b/published/tech/20190725-Deploy-a-Golang-App-in-5-Minutes.md @@ -0,0 +1,216 @@ +首发于:https://studygolang.com/articles/25297 + +# 在 5 分钟之内部署一个 Go 应用 + +在有些程序人写完了他们的 Go 应用之后,这总会成为一个大问题——“我刚写的这个 Go 应用,当它崩溃的时候我要怎么重启?”,因为你没法用 `go run main.go` 或者 `./main` 这样的命令让它持续运行,并且当程序崩溃的时候能够重启。 + +一个普通使用的好办法是使用 Docker。但是,设置 Docker 以及为容器配置你的应用需要花费时间,当你的程序需要和 MySQL、Redis 这样的服务器/进程交互时更是如此。对于一个大型或长期项目来说,毋庸置疑这是一个正确的选择。但是如果在你手上的是个小应用,你想要快速部署并且实时地服务器上查看状态,那么你可能需要考虑别的选择。 + +另一个选择就是在你的 Linux 服务器上创建一个守护进程,然后让它作为一个服务运行,但是这需要花费一些额外的工夫。而且,如果你并不具备 Linux 系统和服务相关的知识的话,这就不是一件简单的事情了。所以,这里有一个最简单的解决方案——使用 [Supervisor](http://supervisord.org/) 来部署你的 Go 应用,然后它会为你处理好其余的工作。它是一个能够帮你监控你的应用程序并在其崩溃时进行重启的工具。 + +## 安装 + +安装 Supervisor 相当简单,在 Ubuntu 上这条命令就会在你的系统上安装 Supervisor。 + +```bash +sudo apt install supervisor +``` + +然后你需要将 Supervisor 添加到系统的用户组中: + +```bash +sudo addgroup --system supervisor +``` + +现在,在创建 Supervisor 的配置文件之前,我们先写一个简单的 Go 程序。这个程序将会读取 .env 文件中的配置项,然后和 MySQL 数据库进行交互。代码如下: + +(为了方便演示,我们会让代码简单些) + +```go +package main + +import ( + "database/sql" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + + _ "github.com/go-sql-driver/mysql" + "github.com/gorilla/mux" + "github.com/joho/godotenv" +) + +type User struct { + Email string `json:"email"` + Password string `json:"password"` +} + +var db *sql.DB + +func init() { + var err error + err = godotenv.Load() + if err != nil { + log.Println("Error readin .env: ", err) + os.Exit(1) + } + + dbUserName := os.Getenv("DB_USERNAME") + dbPassword := os.Getenv("DB_PASSWORD") + dbNAME := os.Getenv("DB_NAME") + + dsn := dbUserName + ":" + dbPassword + "@/" + dbNAME + + db, err = sql.Open("mysql", dsn) + + if err != nil { + log.Println(err) + os.Exit(1) + } + +} + +func main() { + + r := mux.NewRouter() + + r.Use(middleware) + + r.HandleFunc("/", rootHandler) + r.HandleFunc("/user", createUserHandler).Methods("POST") + + fmt.Println("Listening on :8070") + if err := http.ListenAndServe(":8070", r); err != nil { + // 退出程序 + log.Println("Failed starting server ", err) + os.Exit(1) + } + +} + +func rootHandler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "This is root handler") +} + +func createUserHandler(w http.ResponseWriter, r *http.Request) { + + user := &User{} + + err := json.NewDecoder(r.Body).Decode(user) + + // 对请求响应 JSON 数据 + // 在实际应用中你可能想要创建一个进行错误处理的函数 + if err != nil { + // 我们也可以这么做 + // errREsp := `"error": "Invalid input", "status": 400` + // w.Header().Set("Content-Type", "application/json") + // w.WriteHeader(400) + // w.Write([]byte(errREsp)) + + // 然而我们会让服务器崩溃 + log.Fatal(err) + + return + } + + // 在实际应用中必须对密码进行哈希,可以使用 bcrypt 算法 + _, err = db.Exec("INSERT INTO users(email, password) VALUES(?,?)", user.Email, user.Password) + + if err != nil { + log.Println(err) + // 简单起见,发送明文字符串 + // 创建一个有效的 JSON 响应 + errREsp := `"error": "Internal error", "status": 500` // 返回 500 状态码,因为这是我们而非用户的问题 + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + w.Write([]byte(errREsp)) + + return + } + +} + +// 一个简单的中间件,只用来记录请求的 URI +var middleware = func(next http.Handler) http.Handler { + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + requestPath := r.URL.Path + log.Println(requestPath) + next.ServeHTTP(w, r) // 在中间件调用链中进行处理! + + }) +} +``` + +现在,如果我们想要用 Supervisor 来运行这个程序,我们需要构建程序的二进制文件。同时在项目的根目录下创建一个 `.env` 文件 —— 如果你想把配置文件和项目放在一起的话,在这个文件中写上 MySQL 数据库需要的变量。 + +将这个仓库克隆到你想要运行的服务器上。确保你遵循了 Go 目录路径的惯例: + +```bash +$ Go build . +``` + +Go 的这个命令最终会创建一个以项目根目录命名的二进制文件,所以如果项目的根目录是 `myapp`,那么文件的名称就是 `myapp`。 + +现在,在服务器上创建 Supervisor 的配置文件 `/etc/supervisor/conf.d`。 + +```bash +#/etc/supervisor/conf.d/myapp.conf + +[program:myapp] +directory=/root/gocode/src/github.com/monirz/myapp +command=/root/gocode/src/github.com/monirz/myapp/myapp +autostart=true +autorestart=true +stderr_logfile=/var/log/myapp.err +stdout_logfile=/var/log/myapp.log +environment=CODENATION_ENV=prod +environment=GOPATH="/root/gocode" +``` + +这里的 directory 和 command 变量很重要。directory 变量应该设置为项目的根目录,因为程序将会尝试在 directory 指定的路径下读取 .env 文件或是其他需要的配置文件。`autorestart` 变量设置为 `true`,这样当程序崩溃时就会重启。 + +现在通过下面的命令重新加载 Supervisor: + +```bash +$ sudo supervisorctl reload +``` + +来检查下它的状态。 + +```bash +$ sudo supervisorctl status +``` + +一切都正确配置的话,你应该会看到类似下面的输出内容: + +```bash +myapp RUNNING pid 2023, uptime 0:00:03 +``` + +我们名为 myapp 的 Go 服务端程序正在后台运行。 + +现在向我们刚写的 API 发起一些请求。首先检查 `rootHandler` 是否正在工作。然后向 `/user` 结点发送一个包含无效 JSON 格式数据的请求。这应当会让服务器崩溃。但是服务器上没有存储任何日志,不是吗?因为我们还没有实现日志功能? + +等等,Supervisor 实际上已经为我们处理了日志。如果你到 `/var/log` 目录下查看 myapp.log 文件,你就会看到它记录着已经向服务器发起过的请求的 URI 路径。 + +```bash +$ cat /var/log/myapp.log +``` + +错误日志也是如此。好了,我们的服务器程序已经运行了——崩溃的话会重启,还会记录每个请求和错误信息。我觉得我们应该是在大约 5 分钟以内做完了这些事吧?(大概是吧,谁在乎呢。)但关键是,用 Supervisor 来部署和监控你的 Go 应用程序时十分简单的。 + +你觉得呢?毫不犹豫地回复我吧。周末愉快。 + +--- + +via: https://medium.com/@monirz/deploy-golang-app-in-5-minutes-ff354954fa8e + +作者:[Monir Zaman](https://medium.com/@monirz) +译者:[maxwellhertz](https://github.com/maxwellhertz) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190725-GopherCon-2019-Go-Module-Proxy-Life-of-a-query.md b/published/tech/20190725-GopherCon-2019-Go-Module-Proxy-Life-of-a-query.md new file mode 100644 index 000000000..7f2d2a929 --- /dev/null +++ b/published/tech/20190725-GopherCon-2019-Go-Module-Proxy-Life-of-a-query.md @@ -0,0 +1,556 @@ +首发于:https://studygolang.com/articles/25559 + +# GopherCon 2019 - Go 模块代理:查询的生命周期 + +## 概述 + +Go 团队已经搭建了模块镜像与校验和数据库,这将提升 Go 生态环境的可靠性与安全性。这次的交流会通过 Go 命令、代理与校验和数据库讨论经过身份验证的模块代理的技术细节。 + +## 介绍 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-katie.jpg) + +Katie Hockman ,谷歌软件工程师,在 NYC 的 Go 开源团队工作,是构建 Go 模块镜像与校验和数据库的工程师之一。 + +Katie 演讲的是关于 Go 的包管理与身份验证发生的一些新事物。她希望这样可以帮助你更确切的理解这些事情如何运作的。 + +点击[这里](https://github.com/katiehockman/puppies/blob/master/presentation_slides.pdf)可以观看 Katie 演讲时使用的幻灯片。 + +## 一些背景 + +Katie 十分喜爱狗。 + +她希望创建一套 Go 包,以帮助其他人可以成为更好的狗主人。如果狗开心,她就开心。 + +她在 GitHub 创建了新的[储存库](https://github.com/katiehockman/puppies),并开始为她想要提供的所有源代码创建 Go 包。 + +* 首先她创建了 [walk](https://github.com/katiehockman/puppies/tree/master/walk) 包,可以通过算法算出在你附近最适合的狗子步行路线。 +* 然后她创建了 [bark](https://github.com/katiehockman/puppies/tree/master/bark) 包,可以根据你提供的狗子吠叫的视频告诉你狗在想什么。 +* 最后她创建了 [toys](https://github.com/katiehockman/puppies/tree/master/toys) 包,可以提醒你每周买新的玩具给你狗,这样它就不会感到无聊。 + +这样代码就必须依赖大量的 API。她需要依赖存储、音频处理和地图 API。 + +因此产生了忧虑: + +1. 她担心会因不可重现的构建而面临挑战。她依赖正在使用的地图服务的全新 API 端点呢。如果引入她的包的人使用的是旧版本的 API ,那么他们的程序就可能构建失败,而且她无法保证他们的构建是一致的。 +2. 也许可以完全停止依赖。但是真的很难,因为这样她必须从头编写一堆代码;她还不可以复制所依赖的一些 API ,因为代码质量会受到影响。 +3. 更糟糕的情况是,有不怀好意的人试图做出不轨的行为,攻击保存了她所依赖的源代码的服务器。因此使得她获取到错误的代码导致她的包失去可靠性。这些恶意的代码对依赖她代码的人来说可能具有危险性,当她弄清楚情况时,可能为时已晚! + +为了保护自己依赖她代码的人,她想到了一些方案和问题。 + +1. 也许可以完全停止依赖。但是真的很难,因为这样她必须从头编写一堆代码,她还不可以复制她所依赖的一些 API ,因此她的代码质量会收到影响。 +2. 她可以在代码里提供所有的依赖,但是这样子会造成她代码库的过大,并且她担心随着时间的推移代码很难维护和更新。 +3. 或者,她可以选择什么都不做,认为今天的所用的依赖是可信的。相信她所用到的依赖不会消失,github 和其他代码托管网站会一直为我和其他人提供所需要的代码。但说实话,她更相信她很随性。 + +她有一些想法,但是没有一个可以解决所有问题的同时规避新的问题与风险。 + +因此,让我们一起探索最近在 Go 中出现的一些更好的解决方案。 + +1. 模块可以帮助我们解决可重现的构建问题 +2. 模块镜像可以帮助我们避免依赖丢失的风险 +3. 校验和数据库可以帮助我们避免获取到错误代码的风险,保证每一位 Go 开发者在同一时间获取到的代码是相同的 +今天她打算讨论下面三个事情:模块,镜像与校验和数据库 + +## 更好的解决方法:模块 + +模块是一组版本化的 Go 包,它们以某种方式相互关联。 + +在她的 存储库,她有用到一些 Go 包:walk ,bark ,还有 toys 。这些包相互关联,并共享许多相同的依赖。她将这些包放在一个现在可以进行版本控制的模块中。 + +![ puppies 模块](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-9.png) + +模块版本有一个主版本号,次版本号和修订号来组成它的语义版本。 + +![语义版本]](https://d33wubrfki0l68.cloudfront.net/03a164b7facd35432ff6aebea65826dfe8bc4e48/13c23/gophercon-2019/go-module-proxy-life-of-a-query-10.png) + +如果你想在模块存在之前导入包,你要么建一个 vendor 库保存你引用的包的源代码,或者依赖最新版本的包。 + +现在,包可以放置在模块里面,该模块是及时版本化的快照,作为唯一标识。版本的内容永远不允许改变。 + +![包可以放置在模块里面,不可改变且向后兼容](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-11.png) + +每一次模块的主版本提交必须向后兼容。 + +对于某个模块,它只需要一个位于其根目录下的 go.mod 文件。 + +go.mod 文件内容大致如下: +``` +module github.com/katiehockman/puppies + +require ( + github.com/maps/neighborhood v1.4.1 + github.com/audio/dogs v0.19.2 + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 +) +``` +go.mod 文件指定了你使用的包最低版本。该文件是你唯一需要去查看的文件,以便了解模块具有哪些直接依赖关系。 + +你可以看到 v1.4.1 这样符合语义化版本的版本号,也可以看到 v0.0.0 后面接着时间和提交哈希值这样的伪版本号,这样方便你依赖特定的提交或者存数库没有配置版本标签( version tags )。**(译者注:这里她应该是想表达的是,从 go.mod 可以了解模块依赖的哪个版本标签的代码或特定的某一次提交的代码,同理,模块的作者可以通过编辑 go.mod 的方式去指定依赖的代码版本)** + +![ go.mod ](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-13.png) + +通过指定你的代码依赖于 1.4.1 或更高版本的模块,你就可以保证每个引入你包的人将永远不会使用 1.4.1 之前的版本。**(译者注:就是 go.mod 指定你模块的依赖项是某个特定的版本,比如 1.4.1 ,那么引入你模块的人在使用模块封装的方法的时候,所使用依赖项不会是 1.4.1 之前的版本)** + +go 命令使用称为『最小版本选择』( minimal version selection )来构建模块,该模块基于 go.mod 文件中指定的版本。 + +举个例子,假设我们有模块 A 和模块 B ,都是 github.com/katiehockman/puppies 的依赖项。 + +![ 『最小版本选择』例子 ](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-15.png) + +每一个模块都有依赖项 C ,但版本不一样。 A 要求 C 的最低版本是 1.5 ,而 B 要求 C 的最低版本是 1.6。 + +C 还发布了另一个更新的版本: 1.7 。 + +如果她的模块依赖模块 A 和 B ,那么当她进行构建的时候 Go 命令会选择同时满足 A 和 B 的 go.mod 文件约束的最小版本。在这个例子里,将选择 1.6 版本。**(译者注:A 要求最低版本是 1.5 ,而 B 要求最低版本是 1.6 ,因为模块的向后兼容性,所以这里 1.6 版本 理应兼容 1.5 版本, 所以 Go 命令会选择 1.6 版本)** + +![ 『最小版本选择』例子 ](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-16.png) + +这个最小版本选择保证了构建的可重复性( reproducible builds ), 因为 Go 命令构建的时候用的是每个依赖项的允许的最小版本,而不是每天可能更改的最新版本。**(译者注:如果选择的版本不能兼容模块 A 和 B 的要求,可能导致其中一方的代码错误,比如:某个函数的不存在,导致构建失败。所以我们在编写模块的时候,也要注意在更新代码的时候尽量不要删除旧的方法或常量,否则会破坏模块的可向后兼容性。这里有个词语:reproducible builds ,可以点击[这里](https://reproducible-builds.org)了解一下)** + +现在我们就拥有了一致的,可重复的构建方案,而不再依赖于依赖项的最新提交历史。 + +一个问题解决了,就着手第二个问题。现在我们完成了可重复构建,接下来让我们讨论如何确保我们的依赖关系不会消失。 + +## 更好的解决方法:模块镜像与代理 + +现在我们的模块已经版本化,对应版本的内容是固定的,不允许改动的,我们可以将它缓存下来并做验证,而这是非模块模式没有的功能。 + +这里是模块代理进入的地方。 + +![ 模块代理 ](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-19.png) + +上图就是没有代理的流程。很简单,是吧?当 Go 开发者执行 Go 命令时,比如: `go get` , 当本地缓存没有对应的包的时候, Go 命令会直接访问源服务。这里通常指的是托管 Go 源代码的地方,比如: Github 。 + +`go get` 的行为是否更改是根据 Go 命令是否在 module-aware 模式或 GOPATH 模式下运行。代理仅在 module-aware 模式下运行,这我们一会讨论。 + +我们将讨论 `go get` 两个主要的工作: + +* 请求并拉取你请求的源码 +* 基于你拉取的源码感知你的模块是否有新的依赖。它必须解决这些依赖关系,并可能需要更新你的 go.mod 文件 + +但是没有代理,这过程的代价可能变得非常昂贵。延迟和系统存储都是如此。 + +go 命令可能会强制拉取整个源代码,即使现在不会去构建它,只是为了在解析期间做出关于依赖版本的一致决定。 + +所以对于依赖解析,这就是 Go 命令获取的内容: +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-21.png) + +但是这才是它实际需要的: +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-22.png) + +请记住, Go 命令唯一需要理解模块依赖关系的是一个 go.mod 文件。因此,对于 20 MB 的模块,只需要几 KB 的 go.mod 来执行此依赖解析。有许多缓存在你系统上的数据是用不到的,并且还浪费你大量的时间去请求源服务与拉取,这本是不必要的行为。 + +对于那些一直没有使用模块代理的人来说,这就是你看到的一些延迟背后的原因。这就是模块代理的用武之地。 + +回到我们的示例,使用 Go 命令获取源代码,让我们将模块代理放入图片中。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-24.png) + +如果你告诉 Go 命令使用代理,就不是像以前那样直接向源服务器发起请求,它将会向代理请求它想要的东西。 + +现在,我们先不关注源服务器, Go 命令可以与具有更适合其需求的 API 的代理进行交互。 + +让我们来看看与代理的这种交互是什么样的。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-33.png) + +她正在研究她的 puppies 模块,她决定要导入一个新的包,告诉我们有关不同犬种的信息。 + +首先要做的是她要执行 `go get go.dog/breeds` 。 + +代理将会返回版本列表, Go 命令查询最新的版本,在这里指的是 v0.3.2 。 + +现在 Go 命令得知了最新的版本编号,准备对 go.dog/breeds v0.3.2 的代理 /info 端点发起请求。此信息端点将提供有关该版本的一些额外元数据。 + +此元数据包括此标签或分支的规范版本,以及其提交时的时间戳。**(译者注:这两段对应图上的请求 /v0.3.2.info 接口,返回 {"Version": "v0.3.2", "Time": "2019..."})** + +go 命令将使用代理服务器提供的这个规范版本。 + +下一步, Go 命令开始捋清和解决 go.dog/breeds v0.3.2 的依赖。这个过程与 go.mod 文件有关。 + +现在已经完成了依赖项解析,它必须实际获取最初请求的源代码,因此接下来会再次从代理请求包含源码的 .zip 文件。 + +这里真正有趣的是能够从代理获取有关模块的递进依赖的信息,而不需要再通过整个源码 zip 来执行此操作。**(译者注:我的理解是 go.mod 文件已经把依赖项的依赖项也列了出来,因此 Go get 的时候不需要通过解压依赖项的源码 zip 文件,来查询该依赖项的依赖项。)** + +go 命令只需要根据最小版本选择去请求得到它进行依赖项解析所需要的信息,而不必查看其余部分。 + +在这一点上,你可能想知道这个流程的某一部分。 .info 文件是做什么用的,为什么 Go 命令需要它?在这个例子中,我们请求 0.3.2 版本,它只是返回我们提供的相同规范版本。**(译者注:这里应该指的是符合[语义化版本控制规范](https://semver.org/lang/zh-CN/)的版本号)** + +让我们看一个不是这种情况的例子。 + +现在在命令 `go get go.dog/breeds` 的末尾添加 `@master` (`go get go.dog/breeds@master`)。这是告诉 Go 命令我们要请求的是当前 master 分支的代码,所以我们可以完全跳过命中列表端点。**(译者注:就是不获取版本列表了,可以对比上面的图)** + +我们将直接跳转到请求 master 分支的信息。 + +然后你可以看到返回的信息和上一个例子的情况有点不一样,这次我们得到的是伪版本号,它是请求时『 master 』的规范版本。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-39.png) + +当它获取到这个版本号,它可以像前面那样处理,下拉并解决依赖关系,最后请求 zip 文件中的内容。 + +我一直在谈论代理,因为那里没有单一的代理。 任何服务器都可以实现模块代理规范并提供给 Go 命令使用。可以通过运行 `go help goproxy` 获取该规范。 + +你可以在这里查看规范, /list 端点提供版本列表, .info 端点返回 Json 格式的元数据, .mod 文件提供依赖项解析, .zip 端点提供完整源代码。 + +我开始介绍这一部分,说模块镜像将解决我们的问题,但后来她开始讨论代理。 + +好吧,镜像只是一种特殊类型的代理,它将数据和源代码缓存在自己的存储系统中以重新提供给客户端。 + +镜像可以通过多种方式帮助你。 + +镜像可以帮助解决我们最初考虑的问题:代码会从源头消失。 + +它们(镜像)将源代码缓存在它们的存储系统,开发者从 GitHub 拉取他们的代码可能出现的风险将不会出现在它们身上。如果源服务突然因为宕机或者其他原因导致服务不可用,你可以拉取保存在你镜像缓存的源码备份以便抢救。**(译者注:这里翻译起来太难受了... 这里应该说的就是,当我们从 GitHub 拉取代码时可能会遇到代码已删除或别的原因导致拉取失败,这个时候如果我们有搭建一台镜像服务器,且已经把目标代码缓存了下来,那么我们就可以从镜像服务器拉取代码。在我看来,这样只是把风险降低了,并不能说因为有镜像服务器就没有类似上面的风险。)** + +你会发现下载速度变得更快。 + +因为 Go 命令只会查询它所需要的,而不关注所需以外的东西,所以系统上的存储使用会减少。**(译者注:这里应该是相对于执行命令的机器而言)** + +幸运的是,使用代理是非常简单的事情。你不需要安装任何东西即可使用代理。你甚至不需要安装 Git 或者 mercurial ,因为代理都可以完成相应的工作。 + +你的系统需要做的就是能够向代理发出 HTTP 请求。 + +现在我们拥有可重复的构建,并且我们对我们的依赖关系不会消失更有信心。 + +让我们谈谈我们如何信任 Go 命令为你提取的源码。 + +如果没有模块或者代理, Go 命令将使用头部的 HTTPS 直接从源服务器获取源码。 + +你有理由相信这样子做所获得的内容就是你想要的,但是,源服务器是有可能被黑客入侵,这是 Go 命令无法检测的。 + +因为你的代码依赖最新的提交,因此当你所引用的包的作者突然在你不知道的情况下修改源码,你的代码能否不受到影响这一点是无法得到保障的。 + +随着模块的引入,我们还得到一个叫做 go.sum 文件的东西。 + +当你使用模块的时候你可能就会看到这个文件,并且可能会想知道它存在的目的和你可以用它来做什么(如果有的话)。 + +这个 go.sum 文件可以作为你下载源代码时代码的样子,即 Go 命令第一次从你的机器上看到它。 Go 命令可以在某些情况下使用它们来检测来自源服务器或代理的不当行为:可能提供与你之前看到的代码不同的代码。**(译者注: go.sum 记录了第一次拉取代码时,代码的样子。因此当源服务器或代理发生了不确定的问题导致源代码发生变动, Go 命令可以在一定情况下,检测出来。)** + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-51.png) + +所以她谈论的这看起来令人有些困惑的 go.sum 文件,跟 go.mod 文件一样位于模块根目录。 + +它基本上是依赖项的 SHA-256 哈希列表及其 go.mod 文件。因为这是加密哈希,所以基本上不可能在不影响哈希的情况下对特定版本中的文件进行任何更改。 + +你可能从来没有接触过这文件,它是由 Go 命令产生、更新与使用的东西。但了解它的使用方式对于理解其局限性非常重要。 + +顺便说一句,如果你在别的项目看到 go.sum 文件,它可能比图上这个 go.sum 文件的大的多。它还包括一些不会出现在你的 go.mod 文件中的模块。这只是因为 Go 命令提取的所有依赖项,它还包括依赖项的依赖项、依赖项的依赖项的依赖项等等,即使它们不会出现在你的 go.mod 文件中。 + +go 命令有一个非常酷的地方,就是可以使用这些校验和:用于检测你准备下载下来的代码是否与你一个月之前看到的是否有不同。 + +这个 go.sum 文件应该添加到你的存储库,当有人视图下载你的依赖项时, Go 命令可以将其作为信任源。**(译者注:将存储库里的 go.sum 文件每行的源地址作为信任源?)** + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-53.png) + +一旦将 go.sum 保存到你的存储库, Go 命令会在获取你代码的时候检查你模块的 go.sum 文件。 + +假设我们清楚了模块缓存,需要再次重新获取模块的依赖关系。它将获取依赖项的 mod 和 zip 文件,执行哈希,并且可能会看到它刚刚生成的 go.sum 行与你之前的 go.sum 文件中保存的行匹配。 + +这可能意味着此源代码已被修改,代理或源服务器已被黑客入侵,或者无数的事情。我们所知道的是,它与我们之前信任的不同,我们不应该使用它。 + +所有这一切都完全没有 go.sum,但它有其局限性。 + +缺点:它的工作原理是“信任第一次使用”,更具体地说,是你第一次使用。 + +当你向模块添加一个你以前从未见过的新依赖项时,包括升级到你正在使用的新版本的依赖项时,go 命令会获取代码并动态创建 go.sum 行。 它没有任何东西可以检查它,所以它只是将它们弹出到你的 go.sum 文件中。 + +问题是,那些 go.sum 行没有与其他人交叉检查。你只是接受你刚下载的代码是正确的代码,并且你的 go.sum 文件将成为你依赖的真实来源。 + +这意味着你的模块的 go.sum 行可能与 Go 命令刚刚为其他人生成的 go.sum 行不同,可能是因为你在一周内请求它们并且代码已经更改,或者是因为有人给你指向恶意代码。 所以对于我们的问题来说这不是完美,完整的解决方案。 + +为了增加接收错误代码的额外风险,当她开始讨论代理时,你可能已经意识到了一些非常重要的东西。**(译者注:为了减少风险?)** + +谁能说代理实际上是在为你提供你要求的代码?你有什么样的信心可以让代理不是故意瞄准你,并为你提供与其他人不同的东西来伤害你?如果你没有将 go.sum 文件保存到存储库,那么当你从现在起一个月内要求相同的源代码时,如果代理为你提供了不同的服务,会发生什么? + +突然之间,我们相对安全的直接端点已经被一个能够欺骗我们的代理所取代,而且它本身并不值得信赖。 + +在最好的情况下,我们可以想象一个代码作者告诉我们 go.sum 行应该是什么样的,我们总是可以对此进行验证。但是 Go 代码生活在 Github 和 Bitbucket 这么多不同的起源中,没有一个地方可以托管代码,而且这是我们不想改变的 Go 生态系统的重要组成部分。 + +我们可以做的是下一个最好的事情:让我们确保每个人都同意将相同的 go.sum 行添加到他们模块的 go.sum 文件中。 +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-58.png) + +[sumdb 的设计文档](https://go.googlesource.com/proposal/+/master/design/25530-sumdb.md) + +我们可以通过创建 go.sum 行的全局源来实现这一点,称为校验和数据库( checksum database )。 + +当 Go 命令从代理获取其代码,它就可以通过校验和数据库对内容进行哈希匹配。 + +你可以想象一种简单的方法。 + +我们可以在某个服务器上运行一个可以根据请求提供 go.sum 文件的数据库。我们可以告诉社区我们会表现出来,并要求他们相信我们做正确的事情。 + +但是,真正做的就是解决问题. + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-61.png) + +并把它移到其他地方。我们所要做的就是为攻击者创建一个不同的目标。 + +你还可以想象一个校验和数据库由她之前担心的那些猫人运行的场景。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-63.png) + +使用简单的数据库,校验和数据库很容易开始针对狗。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-64.png) + +他们可以为所有猫提供真实代码的校验和,但是为狗提供该代码的恶意版本的校验和。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-65.png) + +审计整个数据库既困难又昂贵。 + +中间人攻击不容易被检测到,并且操纵数据不容易被客户端注意。 + +我们如果无法对它负责就不应该相信校验和数据库是真相的来源。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-66.png) + +我们需要一种不会让校验和数据库行为不当,并且会使审计员和 Go 命令容易检测到有针对性的攻击的解决方案. + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-67.png) + +[透明日志和 Merkle 树研究 ( Transparent Logs and Merkle Trees Research )](https://research.swtch.com/tlog) + +我们将把 go.sum 行存储在所谓的透明日志中。它是由散列节点对构建的树结构。 + +这与用于保护 HTTPS 的证书透明度技术相同。**(译者注:[ 证书透明度 wiki ](https://zh.wikipedia.org/wiki/%E8%AF%81%E4%B9%A6%E9%80%8F%E6%98%8E%E5%BA%A6))** + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-68.png) + +如果你有花比较多的时间与密码学的朋友在一起的话,你可能也有听说过梅克尔树( merkle tree )。 + +我们使用梅克尔树代替简单的数据库来作为我们的真实来源,因为梅克尔树更值得信赖。它主要的优点就是它具有防篡改功能。 + +它具有不允许执行未发现的不当行为。**(译者注:具有可执行的行为白名单?)** + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-70.png) + +将记录放入日志后,永远不会修改或删除它。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-71.png) + +如果日志中的单个记录发生更改,则哈希将不再排列,并且可以立即检测到。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-72.png) + +因此,如果 Go 命令可以证明它将要添加到你模块的 go.sum 文件中的行在此透明日志中,那么可以非常确信这些将要添加到你 go.sum 文件的行是正确的。 + +请记住,我们的目标是确保每个人每次都从代理服务器或源服务器获取“正确”的模块版本。 + +从我们和 Go 命令的角度来看,“正确”意味着...... + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-74.png) + +“就像昨天和之前的每一天一样,对于每一个要求它的人来说都是如此”,所以我们拥有一个无法篡改的数据结构非常重要。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-75.png) + +此日志提供了一种非常可靠的方法来向审计员和 Go 命令证明两个关键事项: + +1. 通过称为“包含证明”的东西在日志中存在特定记录。 +2. 那棵树没有被篡改过。具体来说,后面的树包含我们已经知道的旧树,称为“一致性证明”。 + +在针对日志中的一组 go.sum 行进行验证时,这两个证明可疑给 Go 命令置信度。在将新的 go.sum 行添加到模块的 go.sum 文件之前 Go 命令会动态执行这样的校验。 + +我们希望社区将会有一些外部审计员,因为这些审计员对该系统的工作至关重要。他们可以一起工作,观察和探讨梅克尔树的变化,以警惕任何可疑行为。 + +我不会在这次演示文稿中介绍所有的加密技术,但会稍微谈一点,让你有一个大概的了解,知道它是如何工作的。嘿,谁不爱一点点数学 :) + +让我们一起了解一下包含证明 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-76.png) + +让我们从这个数据结构的实际内容以及它的构建方式开始。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-77.png) + +透明度日志的基础是 go.sum 行,由此图像中的绿色框表示。 + +让我们首先假设我们的日志目前有 16 条记录。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-78.png) + +例如,记录 0 是 go.sum 行 go.opencensus.io v0.19.2 。这些是日志中此模块版本的唯一 go.sum 行,以及校验和数据库应该提供的唯一 go.sum 行。这是审计师可以负责验证的事情。 + +从这里开始,我们可以开始创建树的其余部分。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-79.png) + +我们对每个记录的 go.sum 行执行 SHA-256 哈希,并将它们存储为 0 级节点。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-80.png) + +然后我们将 0 级节点对混合在一起以创建 1 级节点。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-81.png) + +然后散列 1 级节点对以创建 2 级节点。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-82.png) + +然后哈希 2 级节点对以创建 3 级节点。 +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-83.png) + +直到最后,我们最终在这个例子中的第 4 级,在树的顶部有一个哈希,我们称之为树头。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-84.png) + +那么为什么要创建哈希呢? + +那么,还记得她谈过的那些证据吗?它们基本上归结为比较这个顶级树头或哈希,看看它是否与你计算的那些以及你之前看过的那些匹配。 + +让我们来看一个包含证明的例子,它只需要几个哈希就能工作。证明树中包含一组 go.sum 行是 Go 命令如何验证我们刚刚从代理返回的源代码。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-85.png) + +假设我们要验证 go.dog/breeds 的 g o.sum 行,它们是我们刚刚从代理获取的版本 0.3.2 ,在本例中,恰好是记录 9 。 + +我们要做的第一件事就是使用我们的模块版本在校验和数据库中命中一个名为 “ lookup ” 的端点,它给了我们 3 个信息 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-87.png) + +* 在日志中标识它的唯一记录号,在本例中为 9 +* go.dog/breeds 在 go.sum 记录的版本号为 0.3.2 +* 包含此记录的树头 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-88.png) + +为了证明记录 9 存在于树中,我们需要形成从叶子到头部的路径,并确保它与我们给出的头部一致。如果我们在此示例中从级别 0 开始按级别向上走,那将是节点 9 , 4 , 2 , 1 和 0 (级别 4 的头部)。 + +go 命令可以通过散列它给出的 go.sum 行来创建 0 级节点 9 ,但它需要更多的节点才能创建其余的路径。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-89.png) + +这里,为了计算节点 4 的散列,我们需要将节点 8 和 9 一起散列。然后,我们可以在节点 4 使用新创建的散列并将其与节点 5 一起散列以创建节点 2 ,依此类推,直到我们计算出级别 4 的哈希。 + +如果我们刚刚在树顶创建的级别 4 的哈希与我们从 lookup 端点获取到的树头是一致的,那么我们就完成了包含证明校验,并验证了我们正在查询的 go.sum 行存在于我们的校验和数据库中,流程结束。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-90.png) + +随着这棵树的增长,你会得到新的树头,你应该检查这些新树是旧树的超集。所以 Go 命令会在发现它们时存储这些树头,进行一致性验证以验证它刚刚找到的新树头是否与之前知道的旧树头一致,以确保树没有被篡改。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-91.png) + +通常情况下,树的大小不会是 2 的幂,但我们仍然希望能够在这些情况下进行包含证明。那仍然是可能的! + +在这个例子中,我们的树中有 13 条记录,其中包括一些 “ 临时 ” 节点,在此图中用 x 标记。 + +唯一的区别是我们从叶子到树头的路径包含一些我们动态创建的临时节点,当我们完成它们时可以丢弃它们。 + +这就是包含证据的全部内容。在实践中,我们需要弄清楚的最后一件事是如何从校验和数据库中将这些内部节点用蓝色圈起来,以便进行我们的证明。 + +这些内部节点是通过称之为『 tile 』新的方式进行存储和服务。 + +在下图中,校验和数据库将这棵树分成了一块一块,称之为『 tile 』。每个 tile 包含一组散列节点,可用于证明并由客户端访问。 + +在这个例子中,我们选择了一个 2 的 tile 高度,这意味着在树的每两个级别创建一个新的 tile 。实际的校验和数据库树比这大得多,因此在实践中使用高度为 8 的 tile 。 + +我们知道,这个证明需要的节点之一是级别 2 的节点 3 ,就像之前一样。 + +此节点包含在 tile(1,0) 内,因此在执行证明时, Go 命令将从校验和数据库中请求该节点。 + +使用 tile 有一些很大的好处。 + +对于校验和数据库服务器来说,tiles 很不错,因为它们在前端非常适合缓存。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-96.png) + +但它对客户端也很好,因为它们只缓存每个 tile 的底行,并从中构建任何必要的中间节点。我们选择的 tile 高度为 8 ,因此可以降低你的存储成本。 Go 命令不是缓存整个树,而是缓存树中的每个第 8 级,并根据需要动态构建内部节点。 + +现在你已经看过一些数学,让我们回到它在 Go 的上下文中是如何工作的。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-97.png) + +通过你在此处看到的校验和数据库规范,此树可用于 Go 命令。 它使用 lookup 和 tile 端点来检索我们刚才谈到的数据。 + +还有一个额外的端点 /latest ,它为校验和数据库创建的最新树头提供服务。它仅用于审计员根据提供的越来越大的 STH 逐步验证记录。 + +签名的树头看起来像这样: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-98.png) + +它告诉你这个树头的树的大小,以及它的哈希值。 在此示例中,树大小为 11,131 。 + +底部是签名,其中包含校验和数据库的名称 sum.golang.org ,后面是该树头的唯一签名。这个签名很重要,因为它允许审计人员轻松地将责任归咎于 sum.golang.org,如果它服务了它不应该服务的。 + +让我们回到我们的代理示例。我们做的最后一件事是获取版本 0.3.2 的 go.dog/breeds 的 zip 。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-102.png) + +在更新 go.sum 和 go.mod 文件之前,它将生成一个哈希值,然后检查这是否与校验和数据库具有相同的哈希值。 + +它将从查找该模块版本开始。 + +校验和数据库返回其记录号, go.sum 行和包含它的签名树头(或 STH )。 + +根据此记录编号以及已通过 Go 命令在你的计算机上缓存和验证的记录和 tile ... + +它现在可以开始请求 /tile 端点以获取其证明所需的切片。 + +一旦 Go 命令完成了它的证明,它就可以使用新的 go.sum 行更新模块的 go.sum 文件,我们就完成了! + +现在,而不是世界上每个人都单独信任他们第一次下载模块,校验和数据库签名的第一个版本是唯一受信任的版本。 这确保了模块版本的源代码对于世界上的每个人都是相同的,因为有一个可信任的校验和源可以被验证和审计。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/module-life-of-query/go-module-proxy-life-of-a-query-103.png) + +这一切都运行得很好,即使只有一个每个人都使用的校验和数据库。社区有办法让它负责, Go 命令也可以即时进行校对,验证校验和数据库是否未被篡改。 + +此校验和数据库使不受信任的代理成为可能。代理无法向你提供错误的代码,因为在任何源代码到达你之前,有一个可审计的安全层位于其前面。 + +如果原始服务器被黑客攻击,那么这将被立即捕获,因为我们有一个不可变的校验和,当我们第一次看到它时,它会识别内容。 + +即使是模块的作者也无法改变他们的标签移动,将与特定版本相关的位从一天更改为下一天。 + +对此非常好的是,作为开发人员,你不必做任何事情来完成这项工作。你无需在任何地方注册代码,也无需管理自己的私钥或在安全源中创建自己的哈希。 此校验和数据库为该代码创建唯一的哈希值,并将其永久存储在日志中。 + +重要的是要注意,此校验和不仅解决了代理创建的问题。它实际上创建了比直接源连接更安全的用户体验,因为它可以更好地保护您免受更改依赖关系和针对性攻击。 + +现在我们全面了解:依赖性,不会消失,并且每个人都可以信任的依赖关系! + +幸运的是,如果你对这些感兴趣,那么她对你有好消息。 Katie 和她的同事在团队中构建了一个模块镜像和校验和数据库,您可以立即开始使用它。 + +默认情况下,我们的模块镜像和校验和数据库由模块用户的 Go 1.13 中的 Go 命令使用。如果您想立即开始使用它,只需升级即可使用 1.13 测试版。 + +在底层, Go 命令有一些可以配置的环境变量。 + +自 Go1.11 以来, GO111MODULE 和 GOPROXY 一直存在。 + +您可以将 GO111MODULE 设置为“on”以启用模块模式,或将其保留为“auto”。**(译者注: `export GO111MODULE=on` )** + +您可以将 GOPROXY 设置为您选择的代理,以便在模块模式下通过 Go 命令获取。尽管从 1.11 始就存在这种情况,但提供逗号分隔列表的能力对于 1.13 来说是新的。这告诉 Go 命令在放弃之前尝试多个源。如果您想使用 Go 团队的模块镜像,可以将其设置为 https://proxy.golang.org 。**(译者注: `export GOPROXY=proxy.golang.org` )** + +代理和校验和数据库的本质是源代码必须在公共互联网上可用,因此每个人都可以对其进行审核和使用。但是,如果您正在使用私有模块,则可以通过将它们列在 GOPRIVATE 环境变量中来禁用要跳过的域的代理和校验和数据库。 + +她提到了 Go 团队用于透明度日志的开源项目。 + +他们使用 [Trillian](https://github.com/google/trillian) 来实现她之前描述的 merkle 树数据结构。 他们依靠他们的数据存储来保存 go.sum 行以及 Go 命令用于其证明的相应散列。 + +我们已经讨论过 proxy.golang.org 和 sum.golang.org ,但 Go 团队还提供了另外一项服务,即 Module 模块索引。 + +index.golang.org 是 proxy.golang.org 发现的新模块的简单提要( https://proxy.golang.org )。您可以在 index.golang.org/index 上看到此 Feed ,如果您只想查看比特定时间戳更新的模块,则可以选择提供 since 参数。**(译者注 https://index.golang.org/index?since=2019-04-10T19:08:52.997264Z )** + +Go 团队对模块的未来感到非常兴奋,通过创建可重现的构建为开发人员提供更好的依赖管理体验,确保依赖关系不会在一夜之间消失,并确保您要求的源代码是源代码,你和世界上的其他人每次都会得到你。 + +她个人很开心,因为现在她有一套可以帮助她建立模块的解决方案...... + +......这将使到处都是更快乐的小狗。:) + +Go 团队计划对这些功能进行微调,他们希望您能够试用它们并在可能的时候给予反馈!他们很想知道镜像和校验和数据库是如何为您工作的,我们鼓励您在发现它们时在 Github 上提交问题。世界上的狗都感谢你。 + +Katie Hockman ,谷歌, Go 开源团队。 + +[github.com/katiehockman](https://github.com/katiehockman) + +[@katie_hockman](https://twitter.com/katie_hockman) + +内容由 Katie Hockman 的幻灯片提供。 + +--- + +via: https://about.sourcegraph.com/go/gophercon-2019-go-module-proxy-life-of-a-query + +作者:[Royce Miller](https://github.com/r0yce) +译者:[ZackLiuCH](https://github.com/ZackLiuCH) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190725-Writing-delightful-HTTP-middleware-in-Go.md b/published/tech/20190725-Writing-delightful-HTTP-middleware-in-Go.md new file mode 100644 index 000000000..e5a72a544 --- /dev/null +++ b/published/tech/20190725-Writing-delightful-HTTP-middleware-in-Go.md @@ -0,0 +1,131 @@ +首发于:https://studygolang.com/articles/25913 + +# 在 Go 中编写令人愉快的 HTTP 中间件 + +在使用 Go 编写复杂的服务时,您将遇到一个典型的主题是中间件。这个话题在网上被讨论了一次又一次。本质上,中间件允许我们做了如下事情: + +* 拦截 `ServeHTTP` 调用,执行任意代码 +* 对调用链(Continuation Chain) 上的请求/响应流进行更改 +* 打断中间件链,或继续下一个中间件拦截器并最终到达真正的请求处理器 + +这些与 express.js 中间件所做的工作非常类似。我们探索了各种库,找到了接近我们想要的现有解决方案,但是他们要么有不要的额外内容,要么不符合我们的品位。显然,我们可以在 express.js 中间件的启发下,写出 20 行代码以下的更清晰的易用的 API(Installation API) + +## 抽象 + +在设计抽象时,我们首先设想如何编写中间件函数(下文开始称为拦截器),答案非常明显: + +```go +func NewElapsedTimeInterceptor() MiddlewareInterceptor { + return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + startTime := time.Now() + defer func() { + endTime := time.Now() + elapsed := endTime.Sub(startTime) + // 记录时间消耗 + }() + + next(w, r) + } +} + +func NewRequestIdInterceptor() MiddlewareInterceptor { + return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + if r.Headers.Get("X-Request-Id") == "" { + r.Headers.Set("X-Request-Id", generateRequestId()) + } + + next(w, r) + } +} +``` + +它们看起来就像 `http.HandlerFunc`,但有一个额外的参数 `next`,该函数(参数)会继续处理请求链。这将允许任何人像编写类似 `http.HandlerFunc` 的简单函数一样写拦截器,它可以拦截调用,执行所需操作,并在需要时传递控制权。 + +接下来,我们设想如何将这些拦截器连接到 `http.Handler` 或 `http.HandlerFunc` 中。为此,首先要定义 `MiddlewareHandlerFunc`,它只是 `http.HandlerFunc` 的一种类型。(type MiddlewareHandlerFunc http.HandlerFunc)。这将允许我们在 `http.HandlerFunc` 栈上之上构建一个更好的 API。现在给定一个 `http.HandlerFunc` 我们希望我们的链式 API 看起来像这样: + +```go +func HomeRouter(w http.ResponseWriter, r *http.Request) { + // 处理请求 +} + +// ... +// 在程序某处注册 Hanlder +chain := MiddlewareHandlerFunc(HomeRouter). + Intercept(NewElapsedTimeInterceptor()). + Intercept(NewRequestIdInterceptor()) + +// 像普通般注册 HttpHandler +mux.Path("/home").HandlerFunc(http.HandlerFunc(chain)) +``` + +将 `http.HandlerFunc` 传递到 `MiddlewareHandlerFunc`,然后调用 `Intercept` 方法注册我们的 `Interceptor`。`Interceptor` 的返回类型还是 `MiddlewareHandlerFunc`,它允许我们再次调用 `Intercept`。 + +使用 `Intercept` 组合需要注意的一件重要事情是执行的顺序。由于 chain(responseWriter, request)是间接调用最后一个拦截器,拦截器的执行是反向的,即它从尾部的拦截器一直返回到头部的处理程序。这很有道理,因为你在拦截调用时,拦截器应该要在真正的请求处理器之前执行。 + +## 简化 + +虽然这种反向链系统使抽象更加流畅,但事实证明,大多数情况下 s 我们有一个预编译的拦截器数组,能够在不同的 handlers 之间重用。同样,当我们将中间件链定义为数组时,我们自然更愿意以它们执行顺序声明它们(而不是相反的顺序)。让我们将这个数组拦截器称为中间件链。我们希望我们的中间件链看起来有点像: + +```go +// 调用链或中间件可以按下标的顺序执行 +middlewareChain := MiddlewareChain{ + NewRequestIdInterceptor(), + NewElapsedTimeInterceptor(), +} + +// 调用所有以 HomeRouter 结尾的中间件 +mux.Path("/home").Handler(middlewareChain.Handler(HomeRouter)) +``` + +## 实现 + +一旦我们设计好抽象的概念,实现就显得简单多了 + +```go +package middleware + +import "net/http" + +// MiddlewareInterceptor intercepts an HTTP handler invocation, it is passed both response writer and request +// which after interception can be passed onto the handler function. +type MiddlewareInterceptor func(http.ResponseWriter, *http.Request, http.HandlerFunc) + +// MiddlewareHandlerFunc builds on top of http.HandlerFunc, and exposes API to intercept with MiddlewareInterceptor. +// This allows building complex long chains without complicated struct manipulation +type MiddlewareHandlerFunc http.HandlerFunc + + +// Intercept returns back a continuation that will call install middleware to intercept +// the continuation call. +func (cont MiddlewareHandlerFunc) Intercept(mw MiddlewareInterceptor) MiddlewareHandlerFunc { + return func(writer http.ResponseWriter, request *http.Request) { + mw(writer, request, http.HandlerFunc(cont)) + } +} + +// MiddlewareChain is a collection of interceptors that will be invoked in there index order +type MiddlewareChain []MiddlewareInterceptor + +// Handler allows hooking multiple middleware in single call. +func (chain MiddlewareChain) Handler(handler http.HandlerFunc) http.Handler { + curr := MiddlewareHandlerFunc(handler) + for i := len(chain) - 1; i >= 0; i-- { + mw := chain[i] + curr = curr.Intercept(mw) + } + + return http.HandlerFunc(curr) +} +``` + +因此,在不到 20 行代码(不包括注释)的情况下,我们就能够构建一个很好的中间件库。它几乎是简简单单的,但是这几行连贯的抽象实在是太棒了。它使我们能够毫不费力地编写一些漂亮的中间件链。希望这几行代码也能激发您的中间件体验。 + +--- + +via: https://doordash.engineering/2019/07/22/writing-delightful-http-middlewares-in-go/ + +作者:[Katy Slemon](https://medium.com/@katyslemon) +译者:[Alex1996a](https://github.com/Alex1996a) +校对:[DingdingZhou](https://github.com/DingdingZhou) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190725-go-finalizer.md b/published/tech/20190725-go-finalizer.md index c209feceb..594a46e18 100644 --- a/published/tech/20190725-go-finalizer.md +++ b/published/tech/20190725-go-finalizer.md @@ -163,9 +163,9 @@ Allocation: 0.099220 Mb, Number of allocation: 166 下文阐述了为何 finalizers 逐个运行: -> 一个单独 goroutine 为了一个程序运行了所有的 finalizers,然而,如果一个 finalizer 必须长时间运行,则需要开启一个新的 gorountine。 +> 一个单独 Goroutine 为了一个程序运行了所有的 finalizers,然而,如果一个 finalizer 必须长时间运行,则需要开启一个新的 gorountine。 -仅一个 goroutine 将会运行 finalizers,并且任何超重任务都需要开启一个新的 gorountine。当 finalizers 运行时,垃圾回收器并没有停止且并发运行中。因此 finalizer 并不该影响你的应用的性能表现。 +仅一个 Goroutine 将会运行 finalizers,并且任何超重任务都需要开启一个新的 gorountine。当 finalizers 运行时,垃圾回收器并没有停止且并发运行中。因此 finalizer 并不该影响你的应用的性能表现。 同时,一旦 finalizer 不再被需要,Go 提供了一个方法来移除它。 diff --git a/published/tech/20190731-chat-system-in-go-and-react-course-series/part_1.md b/published/tech/20190731-chat-system-in-go-and-react-course-series/part_1.md index bd062c4e9..b127fb370 100644 --- a/published/tech/20190731-chat-system-in-go-and-react-course-series/part_1.md +++ b/published/tech/20190731-chat-system-in-go-and-react-course-series/part_1.md @@ -33,10 +33,10 @@ ```shell $ cd backend $ export GO111MODULE=on -$ go mod init github.com/TutorialEdge/realtime-chat-go-react +$ Go mod init github.com/TutorialEdge/realtime-chat-go-react ``` -应该在 `backend` 目录中使用 go modules 初始化我们的项目,初始化之后我们就可以开始写项目并使其成为一个完整的 Go 应用程序。 +应该在 `backend` 目录中使用 Go modules 初始化我们的项目,初始化之后我们就可以开始写项目并使其成为一个完整的 Go 应用程序。 - **go.mod** - 这个文件有点像 NodeJS 项目中的 package.json。它详细描述了我们项目所需的包和版本,以便项目的构建和运行。 - **go.sum** - 这个文件用于校验,它记录了每个依赖库的版本和哈希值。 @@ -62,7 +62,7 @@ func main() { 将该内容保存到 `main.go` 后,运行后会得到如下内容: ```shell -$ go run main.go +$ Go run main.go Chat App v0.01 ``` @@ -81,7 +81,7 @@ $ cd frontend 然后,你需要使用 `create-react-app` 包创建一个新的 ReactJS 应用程序。这可以用 `npm` 安装: ```shell -$ npm install -g create-react-app +$ NPM install -g create-react-app ``` 安装完成后,你应该能够使用以下命令创建新的 ReactJS 应用程序: @@ -109,7 +109,7 @@ yarn.lock 现在已经成功创建了基本的 ReactJS 应用程序,我们可以测试一下是否正常。输入以下命令来运行应用程序: ```shell -$ npm start +$ NPM start ``` 如果一切正常的话,将会看到 ReactJS 应用程序编译并在本地开发服务器上运行:[http://localhost:3000](http://localhost:3000) diff --git a/published/tech/20190731-chat-system-in-go-and-react-course-series/part_2.md b/published/tech/20190731-chat-system-in-go-and-react-course-series/part_2.md index cd3e0d237..271b58f39 100644 --- a/published/tech/20190731-chat-system-in-go-and-react-course-series/part_2.md +++ b/published/tech/20190731-chat-system-in-go-and-react-course-series/part_2.md @@ -19,7 +19,7 @@ 这需要在我们的 `backend/` 目录中运行此命令来安装它: ```shell -$ go get github.com/gorilla/websocket +$ Go get github.com/gorilla/websocket ``` 一旦我们成功安装了这个包,我们就可以开始构建我们的 Web 服务了。我们首先创建一个非常简单的 `net/http` 服务: diff --git a/published/tech/20190731-chat-system-in-go-and-react-course-series/part_4.md b/published/tech/20190731-chat-system-in-go-and-react-course-series/part_4.md index cccad673a..94565c8b0 100644 --- a/published/tech/20190731-chat-system-in-go-and-react-course-series/part_4.md +++ b/published/tech/20190731-chat-system-in-go-and-react-course-series/part_4.md @@ -147,7 +147,7 @@ func main() { ```shell $ cd backend/ -$ go run main.go +$ Go run main.go ``` 如果成功,我们可以继续扩展代码库来处理多客户端。 @@ -389,7 +389,7 @@ func main() { 启动你的后端应用程序: ```shell -$ go run main.go +$ Go run main.go Distributed Chat App v0.01 ``` diff --git a/published/tech/20190731-chat-system-in-go-and-react-course-series/part_5.md b/published/tech/20190731-chat-system-in-go-and-react-course-series/part_5.md index 2ce211861..33decdc2c 100644 --- a/published/tech/20190731-chat-system-in-go-and-react-course-series/part_5.md +++ b/published/tech/20190731-chat-system-in-go-and-react-course-series/part_5.md @@ -172,7 +172,7 @@ render() { }; ``` -在第 3 行,可以看到已更新的 `.map` 函数返回 ``组件,并将消息 `prop` 设置为 `msg.data`。随后会将 JSON 字符串传递给每个消息组件,然后它将能够按照自定义的格式解析和展示它。 +在第 3 行,可以看到已更新的 `.map` 函数返回 `` 组件,并将消息 `prop` 设置为 `msg.data`。随后会将 JSON 字符串传递给每个消息组件,然后它将能够按照自定义的格式解析和展示它。 现在我们可以看到,每当我们从 WebSocket 端点收到新消息时,它就会在 `ChatHistory` 组件中很好地展示出来! diff --git a/published/tech/20190731-chat-system-in-go-and-react-course-series/part_6.md b/published/tech/20190731-chat-system-in-go-and-react-course-series/part_6.md index d2cb7aab3..d94174748 100644 --- a/published/tech/20190731-chat-system-in-go-and-react-course-series/part_6.md +++ b/published/tech/20190731-chat-system-in-go-and-react-course-series/part_6.md @@ -31,8 +31,8 @@ FROM golang:1.11.1-alpine3.8 RUN mkdir /app ADD . /app/ WORKDIR /app -RUN go mod download -RUN go build -o main ./... +RUN Go mod download +RUN Go build -o main ./... CMD ["/app/main"] ``` @@ -45,7 +45,7 @@ $ docker build -t backend . Sending build context to Docker daemon 11.26kB Step 1/8 : FROM golang:1.11.1-alpine3.8 ---> 95ec94706ff6 -Step 2/8 : RUN apk add bash ca-certificates git gcc g++ libc-dev +Step 2/8 : RUN apk add bash ca-certificates Git gcc g++ libc-dev ---> Running in 763630b369ca ... ``` diff --git a/published/tech/20190801-A-Million-WebSockets-and-Go.md b/published/tech/20190801-A-Million-WebSockets-and-Go.md index b15c38541..162b568e8 100644 --- a/published/tech/20190801-A-Million-WebSockets-and-Go.md +++ b/published/tech/20190801-A-Million-WebSockets-and-Go.md @@ -65,16 +65,16 @@ func NewChannel(conn net.Conn) *Channel { send: make(chan Packet, N), } - go c.reader() - go c.writer() + Go c.reader() + Go c.writer() return c } ``` -我想让你注意的是 `reader` 和 `writer` goroutines。每个 goroutine 都需要内存栈,初始大小可能为 2 到 8 KB,具体[取决于操作系统](https://github.com/golang/go/blob/release-branch.go1.8/src/runtime/stack.go#L64-L82)和 Go 版本。 +我想让你注意的是 `reader` 和 `writer` goroutines。每个 Goroutine 都需要内存栈,初始大小可能为 2 到 8 KB,具体[取决于操作系统](https://github.com/golang/go/blob/release-branch.go1.8/src/runtime/stack.go#L64-L82)和 Go 版本。 -关于上面提到的 300 万个线上连接,为此我们需要消耗 24 GB 的内存(假设单个 goroutine 消耗 4 KB 栈内存)用于所有的连接。并且这还没包括为 `Channel` 结构体分配的内存,`ch.send`传出的数据包占用的内存以及其他内部字段的内存。 +关于上面提到的 300 万个线上连接,为此我们需要消耗 24 GB 的内存(假设单个 Goroutine 消耗 4 KB 栈内存)用于所有的连接。并且这还没包括为 `Channel` 结构体分配的内存,`ch.send` 传出的数据包占用的内存以及其他内部字段的内存。 ### 2.2 I/O goroutines @@ -154,7 +154,7 @@ http.HandleFunc("/v1/ws", func(w http.ResponseWriter, r *http.Request) { ### 3.1 Netpoll -`Channel.reader()` 通过给 `bufio.Reader.Read()` 内的 `conn.Read()` 加锁来**等待新数据的到来**(译者注:上文中的伏笔),一旦连接中有数据,Go runtime(译者注:runtime 包含 Go 运行时的系统交互的操作,这里保留原文)“唤醒” goroutine 并允许它读取下一个数据包。在此之后,goroutine 再次被锁定,同时等待新的数据。让我们看看 Go runtime 来理解 goroutine 为什么必须“被唤醒”。 +`Channel.reader()` 通过给 `bufio.Reader.Read()` 内的 `conn.Read()` 加锁来**等待新数据的到来**(译者注:上文中的伏笔),一旦连接中有数据,Go runtime(译者注:runtime 包含 Go 运行时的系统交互的操作,这里保留原文)“唤醒” Goroutine 并允许它读取下一个数据包。在此之后,goroutine 再次被锁定,同时等待新的数据。让我们看看 Go runtime 来理解 Goroutine 为什么必须“被唤醒”。 如果我们查看 [`conn.Read()` 的实现](https://github.com/golang/go/blob/release-branch.go1.8/src/net/net.go#L176-L186),将会在其中看到 [`net.netFD.Read()` 调用](https://github.com/golang/go/blob/release-branch.go1.8/src/net/fd_unix.go#L245-L257): @@ -213,8 +213,8 @@ ch := NewChannel(conn) // 通过 netpoll 实例观察 conn poller.Start(conn, netpoll.EventRead, func() { - // 我们在这里产生 goroutine 以防止在轮询从 ch 接收数据包时被锁。 - go Receive(ch) + // 我们在这里产生 Goroutine 以防止在轮询从 ch 接收数据包时被锁。 + Go Receive(ch) }) // Receive 从 conn 读取数据包并以某种方式处理它。 @@ -225,13 +225,13 @@ func (ch *Channel) Receive() { } ``` -`Channel.writer()` 更简单,因为我们只能在发送数据包时运行 goroutine 并分配缓冲区: +`Channel.writer()` 更简单,因为我们只能在发送数据包时运行 Goroutine 并分配缓冲区: ```go // 当我们需要时启动 writer goroutine func (ch *Channel) Send(p Packet) { if c.noWriterYet() { - go ch.writer() + Go ch.writer() } ch.send <- p } @@ -239,9 +239,9 @@ func (ch *Channel) Send(p Packet) { > 需要注意的是,当操作系统在 `write()` 调用上返回 `EAGAIN` 时,我们不处理这种情况。我们依靠 Go runtime 来处理这种情况,因为这种情况在服务器上很少见。然而,如果有必要,它可以以与 `reader()` 相同的方式处理。 -当从 `ch.send`(一个或几个)读取传出数据包后,writer 将完成其操作并释放 goroutine 的内存和发送缓冲区的内存。 +当从 `ch.send`(一个或几个)读取传出数据包后,writer 将完成其操作并释放 Goroutine 的内存和发送缓冲区的内存。 -完美!我们通过去除两个运行的 goroutine 中的内存消耗和 I/O 缓冲区的内存消耗节省了 48 GB。 +完美!我们通过去除两个运行的 Goroutine 中的内存消耗和 I/O 缓冲区的内存消耗节省了 48 GB。 ### 3.3 资源控制 @@ -251,14 +251,14 @@ func (ch *Channel) Send(p Packet) { 被锁或超载的服务器停止服务,如果它之前的负载均衡器(例如,nginx)将请求传递给下一个服务器实例,这将是不错的。 -此外,无论服务器负载如何,如果所有客户端突然(可能是由于错误原因)向我们发送数据包,之前的 48 GB 内存的消耗将不可避免,因为需要为每个连接分配 goroutine 和缓冲区。 +此外,无论服务器负载如何,如果所有客户端突然(可能是由于错误原因)向我们发送数据包,之前的 48 GB 内存的消耗将不可避免,因为需要为每个连接分配 Goroutine 和缓冲区。 #### Goroutine 池 -上面的情况,我们可以使用 goroutine 池限制同时处理的数据包数量。下面是这种池的简单实现: +上面的情况,我们可以使用 Goroutine 池限制同时处理的数据包数量。下面是这种池的简单实现: ```go -// goroutine 池的简单实现 +// Goroutine 池的简单实现 package gopool func New(size int) *Pool { @@ -272,7 +272,7 @@ func (p *Pool) Schedule(task func()) error { select { case p.work <- task: case p.sem <- struct{}{}: - go p.worker(task) + Go p.worker(task) } } @@ -288,7 +288,7 @@ func (p *Pool) worker(task func()) { 现在我们的 netpoll 代码如下: ```go -// 处理 goroutine 池中的轮询事件。 +// 处理 Goroutine 池中的轮询事件。 pool := gopool.New(128) poller.Start(conn, netpoll.EventRead, func() { @@ -315,7 +315,7 @@ func (ch *Channel) Send(p Packet) { } ``` -取代 `go ch.writer()` ,我们想写一个复用的 goroutines。因此,对于拥有 `N` 个 goroutines 的池,我们可以保证同时处理 `N` 个请求并且在 `N + 1`的时候, 我们不会分配 `N + 1` 个缓冲区。 goroutine 池还允许我们限制新连接的 `Accept()` 和 `Upgrade()` ,并避免大多数的 DDoS 攻击。 +取代 `go ch.writer()` ,我们想写一个复用的 goroutines。因此,对于拥有 `N` 个 goroutines 的池,我们可以保证同时处理 `N` 个请求并且在 `N + 1` 的时候, 我们不会分配 `N + 1` 个缓冲区。 Goroutine 池还允许我们限制新连接的 `Accept()` 和 `Upgrade()` ,并避免大多数的 DDoS 攻击。 ### 3.4 upgrade 零拷贝 @@ -353,7 +353,7 @@ func WriteFrame(io.Writer, Frame) error 如果有一个这种 API 的库,我们可以按下面的方式从连接中读取数据包(数据包的写入也一样): ```go -// 预期的 WebSocket 实现API +// 预期的 WebSocket 实现 API // getReadBuf, putReadBuf 用来复用 *bufio.Reader (with sync.Pool for example). func getReadBuf(io.Reader) *bufio.Reader func putReadBuf(*bufio.Reader) @@ -391,8 +391,8 @@ BenchmarkUpgradeTCP 973 ns/op 0 B/op 0 allocs/op 我们总结一下这些优化。 -- 内部有缓冲区的 read goroutine 是代价比较大的。解决方案:netpoll(epoll,kqueue); 重用缓冲区。 -- 内部有缓冲区的 write goroutine 是代价比较大的。解决方案:需要的时候才启动 goroutine; 重用缓冲区。 +- 内部有缓冲区的 read Goroutine 是代价比较大的。解决方案:netpoll(epoll,kqueue); 重用缓冲区。 +- 内部有缓冲区的 write Goroutine 是代价比较大的。解决方案:需要的时候才启动 goroutine; 重用缓冲区。 - 如果有大量的连接,netpoll 将无法正常工作。解决方案:使用 goroutines 池并限制池的 worker 数。 - `net/http` 不是处理升级到 WebSocket 的最快方法。解决方案:在裸 TCP 连接上使用内存零拷贝升级。 diff --git a/published/tech/20190802-go-vet-command-is-more-powerful-than-you-think.md b/published/tech/20190802-go-vet-command-is-more-powerful-than-you-think.md index 7b15427a2..65d5a1907 100644 --- a/published/tech/20190802-go-vet-command-is-more-powerful-than-you-think.md +++ b/published/tech/20190802-go-vet-command-is-more-powerful-than-you-think.md @@ -21,7 +21,7 @@ func main() { var wg sync.WaitGroup for i := 0; i < 500; i++ { wg.Add(1) - go func() { + Go func() { a = atomic.AddInt32(&a, 1) // 改为 atomic.AddInt32(&a, 1) 即可 wg.Done() }() @@ -68,14 +68,14 @@ func main() { ### loopclosure -当您启动一个新的 goroutine 时,主 goroutine 将继续执行。在执行时,将进行评估 goroutine 及其变量的代码将,当一个变量仍然被主 goroutine 更新时使用,这可能会导致一些常见的错误: +当您启动一个新的 Goroutine 时,主 Goroutine 将继续执行。在执行时,将进行评估 Goroutine 及其变量的代码将,当一个变量仍然被主 Goroutine 更新时使用,这可能会导致一些常见的错误: ```go func main() { var wg sync.WaitGroup for _, v := range []int{0,1,2,3} { // 需引入临时变量解决,或 通过传值参数解决 wg.Add(1) - go func() { + Go func() { print(v) wg.Done() }() diff --git a/published/tech/20190802-why-you-should-use-a-go-module-proxy.md b/published/tech/20190802-why-you-should-use-a-go-module-proxy.md index 693bc1461..612b2f15c 100644 --- a/published/tech/20190802-why-you-should-use-a-go-module-proxy.md +++ b/published/tech/20190802-why-you-should-use-a-go-module-proxy.md @@ -34,7 +34,7 @@ * `go get` 需要获取 `go.mod` 列出的每个依赖项的源代码来解决递归依赖(需相应的 `go.mod` 文件)。因为它意味着必须下载(例如 `git clone` )每个存储库以[获取单个文件](https://about.sourcegraph.com/go/gophercon-2019-go-module-proxy-life-of-a-query),这显然会使得整个构建过程变慢。 -`那我们怎么解决这些问题呢?` +那我们怎么解决这些问题呢? ## 使用 Go module proxy 的好处 @@ -47,7 +47,7 @@ * 抛弃 `vendor` 文件夹,它将不会再消耗代码库的空间。 -* 因为依赖项存储在 ` 不可变存储 ` 中,即使依赖项从网上消失,你的代码也会受到保护。 +* 因为依赖项存储在 `不可变存储` 中,即使依赖项从网上消失,你的代码也会受到保护。 * 一旦 `Go module`(依赖) 存储在 `Go proxy` 中,就无法覆盖或删除它。这可以保护你免受可能使用相同版本注入恶意代码的攻击。 @@ -117,7 +117,7 @@ ## 总结 -无论使用公共网络,还是专用网络, `GOPROXY` 都有很多优势。这是一个很棒的工具,它可以和 `go` 命令无缝协作。鉴于它具有如此多的优势(安全,快速,存储高效),明智的做法是在您的项目或组织中快速接受它。此外,在 `Go v1.13` 版本中,默认情况下会启用它,这是另一个受欢迎的步骤,它改善了 Go 中依赖项管理的现状。 +无论使用公共网络,还是专用网络,`GOPROXY` 都有很多优势。这是一个很棒的工具,它可以和 `go` 命令无缝协作。鉴于它具有如此多的优势(安全,快速,存储高效),明智的做法是在您的项目或组织中快速接受它。此外,在 `Go v1.13` 版本中,默认情况下会启用它,这是另一个受欢迎的步骤,它改善了 Go 中依赖项管理的现状。 --- diff --git a/published/tech/20190805-Inclusion-No-Go-Files-in-a-Go-Program.md b/published/tech/20190805-Inclusion-No-Go-Files-in-a-Go-Program.md new file mode 100644 index 000000000..778441e56 --- /dev/null +++ b/published/tech/20190805-Inclusion-No-Go-Files-in-a-Go-Program.md @@ -0,0 +1,117 @@ +首发于:https://studygolang.com/articles/25119 + +# Go 程序的包含物:Go 程序中的非 Go 后缀文件 + +静态文件,也有人叫资产或资源,是一些被程序使用、没有代码的文件。在 Go 中,这类文件就是非 `.go` 的文件。它们大部分被用在 Web 内容,像 HTML 、javascript 还有网络服务器处理的图片,然而它们也可以以模板、配置文件、图片等等形式被用在任何程序中。主要问题是这些文件不会随代码一起被编译。开发一个程序时,我们可以通过本地的文件系统访问它们,但是当软件被编译和部署后,这些文件就不再在部署环境中的本地文件系统了,我们必须提供给程序一种访问它们的方式。Go 语言对这个问题并没有提供一种开箱即用的解决方案。本文会讨论这个问题,此问题的通用解决方案,以及 [gitfs](https://github.com/posener/gitfs) 中处理它的方法。[这部分](https://posener.github.io/static-files/#fsutil) 额外赠送,讲述下 `http.FileSystem` 接口一些有趣的方面。 + +我希望能听到你的想法。请用文末评论平台来讨论。 + +## 问题 + +很多情况下,Go 程序需要访问 非 Go 文件。开发过程中,可以从本地文件系统访问它们。例如,用 `os.Open` 函数通过本地文件系统读一个文件。很多标准库的函数就是用本地文件系统。例如用于(静态文件服务)为文件提供服务的 `http.Dir` 函数(译注:此处应为 `http.Dir` 类型,不应为函数),还有全部工作都在本地文件系统完成用于加载模板的 `template.ParseFiles` 和 `template.ParseGlob` 。 + +开发过程中使用本地文件系统是一种没有任何困难的体验。用 `go run` 启动程序,用 `go test` 进行测试,通过当前工作目录(CWD)的相对路径就可以访问文件。(用 `go buid`)构建完工程部署二进制文件时,问题就来了。(因为)在部署的环境中再用跟开发环境相同的路径不一定能访问到静态文件,也可能任何路径都访问不到。在下一段我们会讨论部署后让程序访问这些文件的不同的解决方案。 + +## 可选的解决方案 + +在讨论 Go 之前, 我们先来看一下 Python 是怎么解决的。pip 是 Python 的包管理工具。在众多功能中,它让程序可以定义 [data_files](https://docs.python.org/2/distutils/setupscript.html#installing-additional-files) ,这些文件随程序一起打包,被安装在一个可以在部署的环境中访问到的位置。Python 开发者不需要考虑程序环境的问题。不论是在开发环境还是生产环境,只要配置得当,静态文件就可以访问到。 + +Go 的 modules 不支持打包静态文件。在 Go 语言中,最常见的解决方案是 **binary-packing** (二进制打包)和 [resource-embedding](https://github.com/avelino/awesome-go#resource-embedding) (资源嵌入),像流行的库 [statik](https://github.com/rakyll/statik) ,Buffalo 写的 [packr](https://github.com/gobuffalo/packr) ,历久弥坚的 [go-bindata](https://github.com/go-bindata/go-bindata) 等等等等。据我所知,(上述)这些实现方式,都是用一个 CLI 工具通过把文件进行编码后存进一个生成的 Go 文件中的方式来把资产文件打包进一个 Go 文件。生成的文件提供一个公开接口来访问资产文件,构建程序时,这些文件被编译进了 Go 二进制文件。通常情况下,CLI 命令会以 `//go:generate` 进行注释,那些(以 `//go:generate` 注释的)文件会在每次 `go generate` 被调用的时候生成。 + +这种解决方案有一个好处就是安全 -- 无论 Go 代码是运行在开发环境中、测试环境中还是生成环境中,它都会使用静态文件内容新生成的版本 -- 所有环境中的版本和内容都相同。然而,这种方法有几个坏处。第一,(这种方法会让)开发流程繁琐累赘,尤其是在修改那些资产文件时。每次修改后,我们需要花费很长时间去(调用 `go generate`)重新生成那些文件。有一些工具给出了解决这个问题的部分解决方案,(但是)没有一种是小白式、能很容易地跟其他 Go 工具或命令整合。另一个缺点是,我们在提交流程中需要额外再增加一次验证 -- 例如运行 `go generate` 后执行 `git diff` 命令(diff 的结果为空,证明可以提交)。最后一个缺点,修改静态文件的那次提交,生成文件的 diff 通常会很难看,或者需要为这次的 diff 额外增加一次提交。 + +我个人以为这种方式并不方便,我更喜欢简单的,直接的解决方案:手动把静态内容嵌入到 Go 文件中。通过把依赖的内容添加到 Go 文件中可以实现。例如: + +```go +var tmpl = template.Must(template.New("tmpl").Parse(` +… +`)) + +const htmlContent = ` + +… + +` +``` + +上述方案在规模小的工程中可以使用。但是它也有缺点:嵌入到 Go 文件中的静态内容很难编辑和管理。第一,编辑器/IDE 是解析 Go 代码的,所以这些静态内容不会有语法高亮。其次,提示语法错误的行号是从嵌入的文本里计算的,并不是整个 Go 文件的行号。例如,如果 `template` 有个错误,`template.Must(template.New("tmpl").Parse("..."))` panic 了,提示的错误行号会是 `template` 文本里的而不是 Go 文件的。最后,用这种方式嵌入二进制内容是相当困难的。 + +另一个可选的方案是(维持)一个外部打包机制。例如,提供一个 docker 容器,这个容器包含需要的静态文件或包含诸如 RPM 等在给定位置保存静态文件的可安装的包。这种方法有几个缺点 -- docker daemon 运行的必要、抑或为不同的操作系统打不同的包的必要。但是最大的缺点是,程序不是自包含的,在开发环境和生产环境运行的方式有很大不同,且很难管理。 + +## gitfs + +gitfs 是一个集上述几个方案众家之所长的库。它的设计目的是实现让开发者开发过程中在本地运行代码,可以快速修改静态内容,无缝衔接到生成环境中运行该份代码,并且可以不用二进制包。 + +它的设计原则之一就是 **seamless transition** (无损过渡)-- 一个 flag 或环境变量就可以改变程序运行的方式。这是通过使用抽象了底层文件系统的 `http.FileSystem` 来实现的。`http.FileSystem` 的具体实现可以是一个本地的目录、被打包进 Go 文件的文件以及从远程服务器上拉取下来的文件。要使用静态文件,开发者需要调用 `gitfs.new` ,这个函数返回 `http.FileSystem` 。他们使用这个抽象的文件系统去读静态内容,无视底层的实现。 + +下一个问题是,本地和生产环境中的相同的路径怎么用同一位置表示。Go 引入包的方式能一定程度上解决这个问题。域名加路径的格式,如 `github.com/user/project` ,被广泛地用来在一个工程中表示路径。 `gitfs` 采用了这个命名文件系统的方式,因此 Go 开发者会习惯这种方式。一个工程中的所有路径,特定分支或标签,都可以用相同的规则来确定。例如: `github.com/user/project/path@v1.2.3` 表示 `github.com/user/project` 这个工程下 `path` 这个路径的 `v1.2.3` 标签。 + +想象一个不用二进制包来访问静态内容的生产环境系统。`gitfs` 可以通过调 Github 的公开接口的方式来从获取文件系统结构和文件内容进而实现这种系统。程序创建文件系统时会通过 Github 的公开接口加载文件结构。文件自己的内容可以通过两个模式来获取:懒加载,仅在被访问时加载;文件系统加载时预获取所有内容。 + +`gitfs` 也实现了二进制打包,但是它(比一般意义上的二进制打包)体验更流畅平顺。第一,生成 Go 代码包的 CLI 工具会探测所有用 `gitfs.New` 创建文件系统的请求,因此开发者们运行 CLI 时不需要指定特定的文件系统,因为它能自动推断出来。而后,它下载所有依赖的内容并保存在生成的 Go 文件中。 这个 Go 文件在 `init()` 函数中注册有效的内容。当 `gitfs.New` 函数在程序里再次被调用来创建一个文件系统时,它会检查被注册过的内容,如果被已经被注册了,就直接使用注册的内容,而不是从远程仓库里获取。这样做的结果就是无缝衔接 -- 如果二进制中的内容是有效的,就直接使用,否则,从远程服务器上拉取。 + +前面提到过,生成二进制内容的缺点之一就是静态内容和打包好的内容会有差异的可能性。如果开发者修改了静态文件而没有运行 `go generate` ,程序就可能不按预期运行。`gitfs` 处理这个问题的方式是,额外生成一个加载和比较生成的内容和静态文件(差异)的 Go 测试文件,如果本地有修改而没有重新运行 `go generate` , 测试会不通过。 + +一件有趣的轶事:`gitfs` 工具用它自己来把它的模板文件打包成二进制,使用 gitfs 库加载它们。 + +## 实例 + +我们一起来看一个 `gitfs` 库中用 glob 匹配模式来加载模板文件的 [例子](https://github.com/posener/gitfs/blob/master/examples/templates/templates.go) + +```go +// Add debug mode environment variable. When running with +// `LOCAL_DEBUG=.`, the local Git repository will be used +// instead of the remote github. +var localDebug = os.Getenv("LOCAL_DEBUG") + +func main() { + ctx := context.Background() + // Open repository 'github.com/posener/gitfs' at path + // 'examples/templates' with the local option from + // environment variable. + fs, err := gitfs.New(ctx, + "github.com/posener/gitfs/examples/templates", + gitfs.OptLocal(localDebug)) + if err != nil { + log.Fatalf("Failed initializing Git filesystem: %s.", err) + } + // Parse templates from the loaded filesystem using a glob + // pattern. + tmpls, err := fsutil.TmplParseGlob(fs, nil, "*.gotmpl") + if err != nil { + log.Fatalf("Failed parsing templates.") + } + // Execute a template according to its file name. + tmpls.ExecuteTemplate(os.Stdout, "tmpl1.gotmpl", "Foo") +} +``` + +通过命令 `go run main.go` 来运行这段代码,运行后会从 Github 加载模板。如果执行的命令是 `LOCAL_DEBUG=. Go run main.go` 则会加载本地文件。 + +## fsutil + +`http.FileSystem` 是一个表示抽象文件系统的 interface 。它只有一个方法,`Open` 入参是从文件系统根目录开始的一个相对路径,返回值是一个实现了 `http.File` interface 的对象。`http.File` 是一个表示文件或目录的通用 interface 。因为 `gitfs` 大量使用了 `http.File` ,所以 `gitfs` 模块就包含了提供了使用这个 interface 的很多工具的 [`fsutil`](https://godoc.org/github.com/posener/gitfs/fsutil) 包。 + +`Walk` 函数,整合了 `http.FileSystem` interface 和可能访问所有文件系统的文件的 [`github.com/kr/fs.Walker`](https://godoc.org/github.com/kr/fs#Walker) 。 + +Go 语言标准库模板加载函数只能访问本地文件系统。 `fsutil` 里是一个可以使用 `http.FileSystem` 任何实现的移植版本。用 [`fsuitl.TmplParse`](https://godoc.org/github.com/posener/gitfs/fsutil#TmplParse) 代替 [`text/template.ParseFiles`](https://golang.org/pkg/text/template/#ParseFiles) 。[`fsuitl.TmplParseGlob`](https://godoc.org/github.com/posener/gitfs/fsutil#TmplParseGlob) 代替 [`text/template.ParseGlob`](https://golang.org/pkg/text/template/#ParseGlob) 。以此类比到 HTML ,[`fsutil.TmplParseHTML`](https://godoc.org/github.com/posener/gitfs/fsutil#TmplParseHTML) 代替 [`html/template.ParseFiles`](https://golang.org/pkg/html/template/#ParseFiles) , [`fsutil.TmplParseGlobHTML`](https://godoc.org/github.com/posener/gitfs/fsutil#TmplParseGlobHTML) 代替 [`html/template.ParseGlob`](https://golang.org/pkg/html/template/#ParseGlob) 。 + + [`Glob`](https://godoc.org/github.com/posener/gitfs/fsutil#Glob) 函数的入参是 `http.FileSystem` 和 glob 匹配模式的 list ,返回值是仅包含命中给定的 glob 模式的文件的文件系统。 + + [`Diff`](https://godoc.org/github.com/posener/gitfs/fsutil#Diff) 函数计算文件系统结构的差异和两个文件系统间文件内容的差异。 + +如果你有关于此类使用函数的更多想法,请付诸行动, [open an issue](https://github.com/posener/gitfs/issues) 。 + +## 总结 + +Go 程序中的非 Go 文件需要特殊对待。文本中我试着阐述现在面临的挑战、现有的有效的解决方案和 `gitfs` 是怎样让使用静态文件变得简单地。我们已经学习过 http.FileSystem interface,及它对文件系统操作进行抽象的强大能力。最后的想法:新的 Go 模块系统是否为对静态文件进行内建处理保留了一席之地。 + +--- + +via: https://posener.github.io/static-files/ + +作者:[Eyal Posener](https://posener.github.io/) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190807-go-thoughts-about-cobra.md b/published/tech/20190807-go-thoughts-about-cobra.md index 15529c8f5..39761a809 100644 --- a/published/tech/20190807-go-thoughts-about-cobra.md +++ b/published/tech/20190807-go-thoughts-about-cobra.md @@ -2,9 +2,9 @@ # Go:关于 Cobra 的想法 -!["Golang之旅"插图,来自 Go Gopher 的 Renee French 创作](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-thoughts-about-cobra/A%20Journey%20With%20Go.png) +!["Golang 之旅"插图,来自 Go Gopher 的 Renee French 创作](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-thoughts-about-cobra/A%20Journey%20With%20Go.png) -Cobra 是 Golang 生态系统中最着名的项目之一。它简单,高效,并得到 Go 社区的大力支持。让我们来深入探索一下。 +Cobra 是 Golang 生态系统中最著名的项目之一。它简单,高效,并得到 Go 社区的大力支持。让我们来深入探索一下。 ## 设计 @@ -19,7 +19,7 @@ cmd := &cobra.Command{ } ``` -设计非常类似于原生的 go 标准库命令,如 `go env`,`go fmt`等 +设计非常类似于原生的 Go 标准库命令,如 `go env`,`go fmt` 等 比如,`go fmt` 命令结构: @@ -36,7 +36,7 @@ For more about specifying packages, see 'go help packages'. The -n flag prints commands that would be executed. The -x flag prints commands as they are executed. To run gofmt with specific options, run gofmt itself. -See also: go fix, go vet. +See also: Go fix, Go vet. `, } ``` @@ -101,7 +101,7 @@ main.go:4:2: use of internal package cmd/go/internal/base not allowed 然而,正如 Cobra 创建者 [Steve Francia](https://www.linkedin.com/in/stevefrancia/) 所解释的那样:这个内部界面设计 [催生了了 Cobra](https://blog.gopheracademy.com/advent-2014/introducing-cobra/)(Steve Franci 在 Google 工作并曾直接参与了 Go 项目。)。 -该项目也建立在来自同一作者的 [pflag 项目](https://github.com/spf13/pflag) 之上,提供符合 POSIX 标准。因此,程序包支持短标记和长标记,如`-e`替代`--example` ,或者多个选项,如`-abc` 和`-a`,`-b` 和`-c` 都是是有效选项。这旨在改进 Go 库中的 `flag` 包,该库仅支持标志`-xxx`。 +该项目也建立在来自同一作者的 [pflag 项目](https://github.com/spf13/pflag) 之上,提供符合 POSIX 标准。因此,程序包支持短标记和长标记,如 `-e` 替代 `--example` ,或者多个选项,如 `-abc` 和 `-a`,`-b` 和 `-c` 都是是有效选项。这旨在改进 Go 库中的 `flag` 包,该库仅支持标志 `-xxx`。 ## 特性 @@ -109,7 +109,7 @@ Cobra 有一些值得了解的简便方法: * Cobra 提供了两种方法来运行我们的逻辑: `Run func(cmd *Command, args []string)` 和 `RunE func(cmd *Command, args []string) error` ,后者可以返回一个错误,我们将能够从 `Execute()` 方法的返回中捕获。 -* `Command` 结构 提供了一个 `Aliases(别名)` 属性,允许我们将命令迁移到一个新名称,而不需要在`alias`属性中通过映射旧名称来破坏现有的行为。这种兼容性策略甚至可以通过使用 `Deprecated` 属性来增强,该属性允许您将一个命令标记为`Deprecated(即将弃用,不推荐使用)`,并在删除它之前提供一个简短的说明。 +* `Command` 结构 提供了一个 `Aliases(别名)` 属性,允许我们将命令迁移到一个新名称,而不需要在 `alias` 属性中通过映射旧名称来破坏现有的行为。这种兼容性策略甚至可以通过使用 `Deprecated` 属性来增强,该属性允许您将一个命令标记为 `Deprecated(即将弃用,不推荐使用)`,并在删除它之前提供一个简短的说明。 * 由于每个命令都可以嵌入其他命令,因此 Cobra 本身支持嵌套命令,并允许我们像下边这样编写: @@ -212,11 +212,11 @@ func BenchmarkCmd(b *testing.B) { } ``` -Cobra 运行 50 条命令只有 49.0μs 负载: +Cobra 运行 50 条命令只有 49.0 μ s 负载: ```sh name time/op -Cmd-8 49.0µs ± 1% +Cmd-8 49.0 µ s ± 1% name alloc/op Cmd-8 78.3kB ± 0% @@ -233,7 +233,7 @@ Cmd-8 646 ± 0% 让我们回顾两个可以替代 Cobra 的项目: -* [cli](https://github.com/urfave/cli),一个用于构建命令行应用程序的包。这个包和 Cobra 一样流行,与嵌套命令,bash 补全,hook(钩子),alias(别名)等非常相似。但是,与 Cobra 不同,这个包使用 Go 库中的原生`flag`包。 +* [cli](https://github.com/urfave/cli),一个用于构建命令行应用程序的包。这个包和 Cobra 一样流行,与嵌套命令,bash 补全,hook(钩子),alias(别名)等非常相似。但是,与 Cobra 不同,这个包使用 Go 库中的原生 `flag` 包。 urfave/cli 例子: diff --git a/published/tech/20190807-overview-of-compiler.md b/published/tech/20190807-overview-of-compiler.md new file mode 100644 index 000000000..54c79e21c --- /dev/null +++ b/published/tech/20190807-overview-of-compiler.md @@ -0,0 +1,119 @@ +首发于:https://studygolang.com/articles/24554 + +# 编译器概述 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-overview-of-compile/go-compiler.png "'Golang 之旅'插图,由 Go Gopher 的 Renee French 创作") + +> *本文基于 Go 1.13* + +Go 编译器是 Go 生态系统中的一个重要工具,因为它是将程序构建为可执行二进制文件的基本步骤之一。编译器的历程是漫长的,它先用 C 语言编写,迁移到 Go,许多优化和清理将在未来继续发生,让我们来看看它的高级操作。 + +## 阶段(phases) + +Go 编译器由四个阶段组成,可以分为两类: + +* 前端(frontend):这个阶段从源代码进行分析,并生成一个抽象的源代码语法结构,称为 [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree) +* 后端(backend):第二阶段将把源代码的表示转换为机器码,并进行一些优化。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-overview-of-compile/layer.png) + +[编译器文档](https://github.com/golang/go/blob/release-branch.go1.13/src/cmd/compile/README.md) + +为了更好理解每个阶段,我们看个简单的程序: + +```go +package main + +func main() { + a := 1 + b := 2 + if true { + add(a, b) + } +} + +func add(a, b int) { + println(a + b) +} +``` + +## 解析 + +第一阶段非常简单,在 [文档](https://github.com/golang/go/blob/release-branch.go1.13/src/cmd/compile/README.md) 中有很好的解释: + +> 在编译的第一阶段,对源代码进行标记(词法分析)、解析(语法分析),并为每个源文件构建语法树。 + +lexer 是第一个运行用来标记源代码的包。下面是上边例子的 [标记化](https://gist.github.com/blanchonvincent/1f1cb850a436ffbb81df14eb586f52df) 输出: + +![Go 源码标记化](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-overview-of-compile/Go%20source%20code%20tokenized.png) + +一旦被标记化,代码将被解析、构建代码树。 + +## AST(抽象语法树) 转换 + +可以通过 `go tool compile` 命令和标志 `-w` 展示 [抽象语法树](https://en.wikipedia.org/wiki/Abstract_syntax_tree) 的转换: + +![构建 AST 的简单过程](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-overview-of-compile/sample%20of%20the%20generated%20AST.png) + +此阶段还将包括内联等优化。在我们的示例中,由于我们没有看到 `CALLFUNC` 该方法的任何 `add` 指令,该方法 `add` 已经内联。让我们使用禁用内联的标志 `-l` 再次运行。 + +![构建 AST 的简单过程](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-overview-of-compile/sample%20of%20the%20generated%20AST%202.png) + +AST 生成后,它允许编译器使用 SSA 表示转到较低级别的中间表示。 + +## SSA(静态单赋值)的生成 + +[静态单赋值](https://en.wikipedia.org/wiki/Static_single_assignment_form) 阶段进行优化:消除死代码,删除不使用的分支,替换一些常量表达式等等。 + +使用 `GOSSAFUNC=main Go tool compile main.go && open ssa.html` 命令,生成 HTML 文档的命令将在 SSA 包中完成所有不同的过程,因此可以转储 SSA 代码: + +![SSA 过程](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-overview-of-compile/SSA%20passes.png) + +生成的 SSA 位于 “start” 选项卡中: + +![SSA 代码](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-overview-of-compile/SSA%20code.png) + +在这里,高亮显示变量 `a` 和 `b` 以及 `if` 条件表达式,将向我们展示这些行是怎么变化的。这些代码也向我们描述了编译器如何管理 `println` 函数,该函数被分解为 4 个步骤:printlock、printint、printnl、printunlock。编译器会自动为我们添加一个锁,并根据参数的类型,调用相关的方法来正确输出。 + +在我们的示例中,由于编译时已知 `a` 和 `b`,所以编译器可以计算最终结果并将变量标记为不必要的。通过 `opt` 优化这部分: + +![SSA code — “opt” 过程](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-overview-of-compile/SSA%20code%20%E2%80%94%20%E2%80%9Copt%E2%80%9D%20pass.png) + +在这里,`v11` 已经被添加的 `v4` 和 `v5` 所替代,这两个 `v4` 和 `v5` 被标记为死代码。然后通过 `opt deadcode` 将删除这些代码。 + +![SSA code — “opt deadcode” 过程](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-overview-of-compile/SSA%20code%20%E2%80%94%20%E2%80%9Copt%20deadcode%E2%80%9D%20pass.png) + +对于 `if` 条件,`opt` 阶段将常量 `true` 标记为死代码,然后删除: +![删除布尔常量](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-overview-of-compile/constant%20boolean%20is%20removed.png) + +然后,通过将不必要的块和条件标记为无效,另一次传递将简化控制流。这些块稍后将被另一个专用于死代码的阶段删除 + +![删除不必要控制流](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-overview-of-compile/unnecessary%20control%20flow%20is%20removed.png) + +完成所有过程之后,Go 编译器现在将生成一个中间汇编代码 + +![Go 汇编码](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-overview-of-compile/Go%20asm%20code.png) + +下一阶段将把机器码生成到二进制文件中。 + +## 生成机器码 + +编译器的最后一步是生成目标(object)文件,在我们的例子中生成 `main.c`。从这个文件中,现在可以使用 `objdumptool` 对其进行反编译。下面是一个很好的图表,由 Grant Seltzer Richman 创建: + +![compile 工具](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-overview-of-compile/go%20tool%20compile.png) + +![objdump 工具](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-overview-of-compile/go%20tool%20objdump.png) + +*您可以在“[Dissecting Go Binaries](https://www.grant.pizza/dissecting-go-binaries/)”中找到有关对象文件和二进制文件的更多信息。* + +生成目标文件后,现在可以使用 `go tool link` 将其直接传递给链接器,二进制文件将最终就绪。 + +--- + +via: https://medium.com/a-journey-with-go/go-overview-of-the-compiler-4e5a153ca889 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[TomatoAres](https://github.com/TomatoAres) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190809-rate-limiting-http-requests-based-on-the-client-ip-address.md b/published/tech/20190809-rate-limiting-http-requests-based-on-the-client-ip-address.md new file mode 100644 index 000000000..8a47acc86 --- /dev/null +++ b/published/tech/20190809-rate-limiting-http-requests-based-on-the-client-ip-address.md @@ -0,0 +1,188 @@ +首发于:https://studygolang.com/articles/33988 + +# Go 中基于 IP 地址的 HTTP 请求限流 + +如果你在运行 HTTP 服务并且想对 endpoints 进行限速,你可以使用维护良好的工具,例如 [github.com/didip/tollbooth](https://github.com/didip/tollbooth)。但是如果你在构建一些非常简单的东西,自己实现并不困难。 + +我们可以使用已经存在的试验性的 Go 包 `x/time/rate`。 + +在本教程中,我们将创建一个基于用户 IP 地址进行速率限制的简单的中间件。 +## 「干净的」HTTP 服务 + +让我们从构建一个简单的 HTTP 服务开始,该服务具有非常简单的 endpiont。这可能是个非常「重」的 endpoint,因此我们想在这里添加速率限制。 + +```go +package main + +import ( + "log" + "net/http" +) + +func main() { + mux := http.NewServeMux() + mux.HandleFunc("/", okHandler) + + if err := http.ListenAndServe(":8888", mux); err != nil { + + // log.Fatalf("unable to start server: %s", err.Error()) + } +} + +func okHandler(w http.ResponseWriter, r *http.Request) { + // Some very expensive database call + w.Write([]byte("alles gut")) +} +``` + +在 `main.go` 中,我们启动服务,该服务监听 `:8888` 并拥有一个 endpoint `/`。 + +## golang.org/x/time/rate + +我们将使用 Go 中 `x/time/rate` 包,该包提供了令牌桶限速算法。[rate#Limiter](https://godoc.org/golang.org/x/time/rate#Limiter) 控制事件发生的频率。它实现了一个大小为 `b` 的「令牌桶」,初始化时是满的,并且以每秒 `r` 个令牌的速率重新填充。非正式地,在任意足够长的时间间隔中,限速器将速率限制在每秒 r 个令牌,最大突发事件为 b 个。 + +既然我们想要实现基于 IP 地址的限速器,我们还需要维护一个限速器的 map。 + +```go +package main + +import ( + "sync" + + "golang.org/x/time/rate" +) + +// IPRateLimiter . +type IPRateLimiter struct { + ips map[string]*rate.Limiter + mu *sync.RWMutex + r rate.Limit + b int +} + +// NewIPRateLimiter . +func NewIPRateLimiter(r rate.Limit, b int) *IPRateLimiter { + i := &IPRateLimiter{ + ips: make(map[string]*rate.Limiter), + mu: &sync.RWMutex{}, + r: r, + b: b, + } + + return i +} + +// AddIP creates a new rate limiter and adds it to the ips map, +// using the IP address as the key +func (i *IPRateLimiter) AddIP(ip string) *rate.Limiter { + i.mu.Lock() + defer i.mu.Unlock() + + limiter := rate.NewLimiter(i.r, i.b) + + i.ips[ip] = limiter + + return limiter +} + +// GetLimiter returns the rate limiter for the provided IP address if it exists. +// Otherwise calls AddIP to add IP address to the map +func (i *IPRateLimiter) GetLimiter(ip string) *rate.Limiter { + i.mu.Lock() + limiter, exists := i.ips[ip] + + if !exists { + i.mu.Unlock() + return i.AddIP(ip) + } + + i.mu.Unlock() + + return limiter +} +``` + +`NewIPRateLimiter` 创建了一个 IP 限速器的实例,HTTP 服务需要调用该实例的 `GetLimiter` 方法去获取特定 IP 的限速器(从 map 中获取,或者生成一个新的)。 + +## 中间件 + +让我们升级我们的 HTTP 服务,将中间件添加到所有的 endpoints 中,因此,如果某 IP 达到了限制速率,服务将会返回 429 Too Many Requests,否则服务将处理该请求。 + +在 `limitMiddleware` 函数中,每次中间件收到 HTTP 请求,我们都会调用全局限速器的 `Allow()` 方法。如果令牌桶中没有剩余的令牌,`Allow()` 将返回 false,我们返回给用户 429 Too Many Requests 响应。否则,调用 `Allow()` 将消耗桶中的一个令牌,我们将控制权传递给调用链的下一个处理器。 + +```go +package main + +import ( + "log" + "net/http" +) + +var limiter = NewIPRateLimiter(1, 5) + +func main() { + mux := http.NewServeMux() + mux.HandleFunc("/", okHandler) + + if err := http.ListenAndServe(":8888", limitMiddleware(mux)); err != nil { + log.Fatalf("unable to start server: %s", err.Error()) + } +} + +func limitMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + limiter := limiter.GetLimiter(r.RemoteAddr) + if !limiter.Allow() { + http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests) + return + } + + next.ServeHTTP(w, r) + }) +} + +func okHandler(w http.ResponseWriter, r *http.Request) { + // Some very expensive database call + w.Write([]byte("alles gut")) +} +``` + +## 构建 & 运行 + +```bash +go get golang.org/x/time/rate +go build -o server . +./server +``` + +## 测试 + +有一个非常棒的工具称作 vegeta,我喜欢在 HTTP 负载测试中使用(它也是用 Go 编写的) + +```bash +brew install vegeta +``` + +我们需要创建一个简单的配置文件,声明我们想要发送的请求。 + +```text/plain +GET http://localhost:8888/ +``` + +然后,以每个时间单元 100 个请求的速率攻击 10 秒。 + +```bash +vegeta attack -duration=10s -rate=100 -targets=vegeta.conf | vegeta report +``` + +结果,你将看到一些请求返回 200,但大多数返回 429。 + +--- + +via: https://pliutau.com/rate-limit-http-requests/ + +作者:[ALEX PLIUTAU](https://pliutau.com/) +译者:[DoubleLuck](https://github.com/DoubleLuck) +校对:[unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190810-go-monitor-pattern.md b/published/tech/20190810-go-monitor-pattern.md index 4000e7adb..11dc24011 100644 --- a/published/tech/20190810-go-monitor-pattern.md +++ b/published/tech/20190810-go-monitor-pattern.md @@ -4,7 +4,7 @@ ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-monitor-pattern/1.png) -Go 能实现[监控模式](),归功于 `sync` 包和 `sync.Cond` 结构体。监控模式允许 goroutine 在进入睡眠模式前等待一个定特定条件,而不会阻塞执行或消耗资源。 +Go 能实现[监控模式](),归功于 `sync` 包和 `sync.Cond` 结构体。监控模式允许 Goroutine 在进入睡眠模式前等待一个定特定条件,而不会阻塞执行或消耗资源。 ## 条件变量 @@ -66,8 +66,8 @@ func main() { `Queue` 是一个非常简单的结体构,由一个切片和 `sync.Cond` 结构组成。然后,我们做两件事: -- 启动 10 个 goroutines,并将尝试一次消费 X 个元素。如果这些元素不够数目,那么 goroutine 将进去睡眠状态并等待被唤醒 -- 主 goroutine 将用 100 个元素填入队列。每添加一个元素,它将唤醒一个等待消费的 goroutine。 +- 启动 10 个 goroutines,并将尝试一次消费 X 个元素。如果这些元素不够数目,那么 Goroutine 将进去睡眠状态并等待被唤醒 +- 主 Goroutine 将用 100 个元素填入队列。每添加一个元素,它将唤醒一个等待消费的 goroutine。 程序的输出, @@ -84,7 +84,7 @@ func main() { 7: [ 0 1 2 3 4 5 6] ``` -如果多次运行此程序,将获得不同的输出。我们可以看到,由于是按批次检索值的,每个 goroutine 获取的值是一个连续的序列。这一点对于理解 `sync.Cond` 与 `channels` 的差异很重要。 +如果多次运行此程序,将获得不同的输出。我们可以看到,由于是按批次检索值的,每个 Goroutine 获取的值是一个连续的序列。这一点对于理解 `sync.Cond` 与 `channels` 的差异很重要。 ## sync.Cond vs Channels @@ -169,15 +169,15 @@ func (q *Queue) GetMany(n int) []Item { 我们运行包含 100 个元素的基准测试,如示例所示: ```text -WithCond-8 15.7µs ± 2% -WithChan-8 19.4µs ± 1% +WithCond-8 15.7 µ s ± 2% +WithChan-8 19.4 µ s ± 1% ``` 在这里使用条件变量要快一些。让我们试试 10k 个元素的基准测试: ```text WithCond-8 2.84ms ± 1% -WithChan-8 917µs ± 1% +WithChan-8 917 µ s ± 1% ``` 可以看到 `channel` 的速度要快得多。 [Bryan Mills 在“饥饿”部分(第 45 页)](https://drive.google.com/file/d/1nPdvhB0PutEJzdCq5ms6UI58dp50fcAN/view)中解释了这个问题: @@ -192,9 +192,9 @@ WithChan-8 917µs ± 1% ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-monitor-pattern/2.png) -进入等待模式的每个 goroutine 将从变量 `wait` 开始分号,该变量从 0 开始。这表示等待队列。 +进入等待模式的每个 Goroutine 将从变量 `wait` 开始分号,该变量从 0 开始。这表示等待队列。 -然后,每次调用 `Signal()` 都会增加另一个名为 `notify` 的计数器,该计数器代表需要通知或唤醒的 goroutine 队列。 +然后,每次调用 `Signal()` 都会增加另一个名为 `notify` 的计数器,该计数器代表需要通知或唤醒的 Goroutine 队列。 我们的 `sync.Cond` 结构包含一个负责发号的结构: @@ -208,9 +208,9 @@ type notifyList struct { } ``` -这是就是上面提到的 `wait` 和 `notify` 变量。该结构还通过 `head` 和 `tail` 保存等待的 goroutine 的链表,其中每个 goroutine 在其内部结构中保持对所获取的票号的引用。 +这是就是上面提到的 `wait` 和 `notify` 变量。该结构还通过 `head` 和 `tail` 保存等待的 Goroutine 的链表,其中每个 Goroutine 在其内部结构中保持对所获取的票号的引用。 -当收到信号时,Go 会在链表上进行迭代,直到分配给被检查的 goroutine 的票号与 `notify` 变量的编号匹配,如匹配则唤醒当前票号的 goroutine。一旦找到 goroutine,其状态将从等待模式变为可运行模式,然后在 Go 调度程序中处理。 +当收到信号时,Go 会在链表上进行迭代,直到分配给被检查的 Goroutine 的票号与 `notify` 变量的编号匹配,如匹配则唤醒当前票号的 goroutine。一旦找到 goroutine,其状态将从等待模式变为可运行模式,然后在 Go 调度程序中处理。 如果你想深入了解 Go 调度程序,我强烈建议你阅读 [William Kennedy 关于 Go 调度程序的教程](https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part1.html)。 diff --git a/published/tech/20190811-Go-Context-and-Cancellation-by-Propagation.md b/published/tech/20190811-Go-Context-and-Cancellation-by-Propagation.md index c7c143a8c..b116d14c3 100644 --- a/published/tech/20190811-Go-Context-and-Cancellation-by-Propagation.md +++ b/published/tech/20190811-Go-Context-and-Cancellation-by-Propagation.md @@ -7,6 +7,7 @@ [context 包](https://blog.golang.org/context)在 Go 1.7 中引入,它为我们提供了一种在应用程序中处理 context 的方法。这些 context 可以为取消任务或定义超时提供帮助。通过 context 传播请求的值也很有用,但对于本文,我们将重点关注 context 的取消功能。 ## 默认的 contexts + Go 的 `context` 包基于 TODO 或者 Background 来构建 context。 ```go @@ -41,6 +42,7 @@ func (r *Request) Context() context.Context { 如果你在自己的包中工作并且没有任何可用的 context,在这种情况下你应该使用 TODO context。通常,或者如果你对必须使用的 context 有任何疑问,可以使用 TODO context。现在我们知道了主 context,让我们看看它是如何派生子 context 的。 ## Contexts 树 + 父 context 派生出的子 context 会在在其内部结构中创建一个和父 context 之间的联系: ```go @@ -80,7 +82,8 @@ func (c *cancelCtx) cancel(removeFromParent bool, err error) { 这种取消传播允许我们定义更高级的例子,这些例子可以帮助我们根据主 context 处理多个/繁重的工作。 ## 取消传播 -让我们通过 goroutine A 和 B 来展示一个取消的例子,它们将并行运行,因为拥有共同的 context ,当一个发生错误取消时,另外一个也会被取消: + +让我们通过 Goroutine A 和 B 来展示一个取消的例子,它们将并行运行,因为拥有共同的 context ,当一个发生错误取消时,另外一个也会被取消: ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/context-and-cancellation-by-propagation/image_4.png) @@ -114,9 +117,10 @@ B - 200ms ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/context-and-cancellation-by-propagation/image_5.png) -我们可以在这里看到 context 对于**多个 goroutine 是线程安全的**。实际上,有可能是因为我们之前在结构中看到的 mutex,它保证了对 context 的并发安全。 +我们可以在这里看到 context 对于**多个 Goroutine 是线程安全的**。实际上,有可能是因为我们之前在结构中看到的 mutex,它保证了对 context 的并发安全。 ## context 泄漏 + 正如我们在内部结构中看到的那样,当前 context 在 `Context` 属性中保持其父级的链接,而父级将当前 context 保留在 `children` 属性中。对 cancel 函数的调用将把当前 context 中的子项清除并删除与父项的链接: ```go @@ -139,6 +143,7 @@ the cancel function returned by context.WithCancel should be called, not discard ``` ## 总结 + `context` 包还有另外两个利用 cancel 函数的函数:`WithTimeout` 和 `WithDeadline`。在定义的超时/截止时间后,它们都会自动触发 cancel 函数。 `context` 包还提供了一个 `WithValue` 的方法,它允许我们在 context 中存储任何对键/值。此功能受到争议,因为它不提供明确的类型控制,可能导致糟糕的编程习惯。如果你想了解 `WithValue` 的更多信息,我建议你阅读[Jack Lindamood 关于 context 值的文章](https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39)。 diff --git a/translated/tech/20171117-Golang-tutorial-series/20190814-go-understand-the-empty-interface.md b/published/tech/20190814-go-understand-the-empty-interface.md similarity index 75% rename from translated/tech/20171117-Golang-tutorial-series/20190814-go-understand-the-empty-interface.md rename to published/tech/20190814-go-understand-the-empty-interface.md index 2a6ca1e76..b4eb6437c 100644 --- a/translated/tech/20171117-Golang-tutorial-series/20190814-go-understand-the-empty-interface.md +++ b/published/tech/20190814-go-understand-the-empty-interface.md @@ -1,3 +1,5 @@ +首发于:https://studygolang.com/articles/24407 + # 理解 Go 的空接口 @@ -15,16 +17,16 @@ 因此,空接口作为参数的方法可以接受任何类型。Go 将继续转换为接口类型以满足这个函数。 -Russ Cox 撰写了一篇 [关于接口内部结构的精彩文章](https://research.swtch.com/interfaces),并解释了接口由两个单词组成: +Russ Cox 撰写了一篇 [关于接口内部结构的精彩文章](https://research.swtch.com/interfaces),并解释了接口由两个指针组成: -* 指向存储类型信息的指针 -* 指向关联数据的指针 +* 指向类型相关信息的指针 +* 指向数据相关信息的指针 以下是 Russ 在 2009 年画的示意图,[当时 `runtime` 包还是用 C 语言编写](https://go.googlesource.com/go/+/refs/heads/release-branch.go1/src/pkg/runtime/iface.c): ![internal-representation](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-understand-the-empty-interface/internal-representation.png) -`runtime` 包现在用 Go 编写,但结构未变。我们可以通过打印空接口来验证: +现在,`runtime` 包改用 Go 编写,但结构未变。我们可以通过打印空接口来验证: ```go func main() { @@ -46,12 +48,12 @@ func read(i interface{}) { ## 底层结构 -空接口的底层结构记录在反射包中: +空接口的底层结构记录在反射包中 `reflect/value.go`: ```go type emptyInterface struct { - typ *rtype // word 1 with type description - word unsafe.Pointer // word 2 with the value + typ *rtype // 类型描述 + Word unsafe.Pointer // 值 } ``` @@ -75,15 +77,14 @@ type rtype struct { } ``` -在这些字段中,有些非常简单,很容易可以看出: +在这些字段中,有些非常简单,且广为人知: - * `size` 是以字节为单位的大小 * `kind` 包含类型有:int8,int16,bool 等。 * `align` 是变量与此类型的对齐方式 - 根据空接口嵌入的类型,我们可以映射导出字段或列出方法: +| 译者注:方法在结构体最下面,这篇文章中是看不到的;需要先将这个 `rtye` 映射成 结构体才能看到,映射是基于 `tflag` 做的 ```go type structType struct { @@ -93,15 +94,16 @@ type structType struct { } ``` - -这个结构还有两个映射,包含字段列表。它清楚地表明,将内建类型转换为空接口将导致平面转换(不需要做其他额外的处理),其中字段的描述及值将存储在内存中。 +这个结构还有两个映射,包含字段列表。它清楚地表明,将内建类型转换为空接口将导致*扁平转换*(译者注:不需要做其他额外的处理),其中字段的描述及值将存储在内存中。 下边是我们看到的空结构体的表示: -![结构体由两个词构成](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-understand-the-empty-interface/interface-representation.png) +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-understand-the-empty-interface/interface-representation.png) +
结构体由两个词构成 +
现在让我们看看空接口实际上可以实现哪种转换。 @@ -141,15 +143,16 @@ exit status 2 有以下几个步骤: -* 步骤 1:比较(指令`CMPQ`)`int16`类型(加载指令`LEAQ`(Load Effective Address,加载有效地址)到空接口的内部类型(从空接口`MOVQ`的内存段读取 48 字节偏移量的内存的指令) +* 步骤 1:比较 `int16` 类型与 ` 空接口 ` 的内部类型:比较(指令 `CMPQ`)`int16` 类型(加载有效地址 `LEAQ`(Load Effective Address)到空接口的内部类型(从空接口 `MOVQ` 的内存段读取 48 字节偏移量的内存的指令) -* step 2:`JNE` 指令(Jump if Not Equal),将跳转到生成的指令,这些指令将在步骤中处理错误 3 +* step 2:`JNE` 指令,即不相等则跳转指令(Jump if Not Equal),会跳转到已生成的处理错误的指令,这些指令将在步骤中处理错误 3 -* 步骤 3:代码将发生混乱并生成我们之前看到的错误消息 +* 步骤 3:代码将 `panic` 并生成我们上面看到的错误信息 * 步骤 4:这是错误指令的结束。此特定指令由显示指令的错误消息引用:`main.go:10 +0x7d` -在转换原始类型之后,应该从空接口的内部类型进行任意类型转换。这种转换为空接口,然后转换回原始类型会导致程序损耗。让我们运行一些基准测试来简单了解一下。 +任何从空接口内部类型的转换,都应该在原始类型转换完成后进行。这种转换为空接口,然后转换回原始类型会导致程序损耗。让我们运行一些基准测试来简单了解一下。 +| 译者注:这句话是说,比如 `interface{}` 存了一个 `int16`; 需要转换为 `int32` 时,不能直接 `interface{}-> int32`;应该是 `interface{}->int16->int32`,这也是上面的例子 panic 的原因 ## 性能 @@ -209,7 +212,7 @@ BenchmarkWithType-8 300000000 4.24 ns/op BenchmarkWithEmptyInterface-8 20000000 60.4 ns/op ``` -与复制结构相比,将双重转换(类型转换为空接口然后再转换回类型)多消耗 55 纳秒以上的时间。如果结构中字段的数量增加,时间还会增加: +与结构副本(typed 函数)相比,使用空接口需要双重转换(原始类型转换为空接口然后再转换回原始类型)多消耗 55 纳秒以上的时间。如果结构中字段的数量增加,时间还会增加: ```shell BenchmarkWithType-8 100000000 17 ns/op @@ -250,6 +253,6 @@ via: + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[lts8989](https://github.com/lts8989) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190821-Migrating-to-Go-Modules.md b/published/tech/20190821-Migrating-to-Go-Modules.md index 039a196ac..f0a81e573 100644 --- a/published/tech/20190821-Migrating-to-Go-Modules.md +++ b/published/tech/20190821-Migrating-to-Go-Modules.md @@ -1,4 +1,4 @@ -首发于:https://studygolang.com/articles/12399 +首发于:https://studygolang.com/articles/23133 # 使用 Go Modules(模块)进行依赖项迁移 @@ -31,7 +31,7 @@ Go 1.11 中引入的 Go Modules(模块)系统提供了一个内置在 Go 命 若要转换已使用依赖关系管理工具的项目,请运行以下命令: ```bash -$ git clone https://github.com/my/project +$ Git clone https://github.com/my/project [...] $ cd project $ cat Godeps/Godeps.json @@ -52,7 +52,7 @@ $ cat Godeps/Godeps.json } ] } -$ go mod init github.com/my/project +$ Go mod init github.com/my/project go: creating new go.mod: module github.com/my/project go: copying requirements from Godeps/Godeps.json $ cat go.mod @@ -69,7 +69,7 @@ $ 在继续执行之前,是时候停下来,运行 `go build ./...` 和 `go test ./...`。接下来的步骤就是修改你的 `go.mod` 文件,因此,如果您喜欢采用迭代方法,这是您的 `go.mod` 文件最接近模块前依赖项规范的地方。 ```bash -$ go mod tidy +$ Go mod tidy go: downloading rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca go: extracting rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca $ cat go.sum @@ -83,8 +83,8 @@ $ 让我们确保代码能够成功编译和测试通过: ```bash -$ go build ./... -$ go test ./... +$ Go build ./... +$ Go test ./... [...] $ ``` @@ -92,7 +92,7 @@ $ 注意,其他依赖项管理工具可能在单个包或整个整个仓库(而不是模块)级别指定依赖项,并且通常不识别依赖项文件 `go.mod` 中指定的需求。因此,您可能无法获得与之前完全相同的每个包的版本,这会提高风险。因此,按照上面的命令对最后依赖项进行检查非常重要。所以我们需要这样做,输入下面的命令: ```bash -$ go list -m all +$ Go list -m all go: finding rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca github.com/my/project rsc.io/binaryregexp v0.2.1-0.20190524193500-545cabda89ca @@ -102,11 +102,11 @@ $ 并将结果版本与旧的依赖关系管理文件进行比较,以确保所选版本是适合自己当前项目的。如果你发现一个版本不是你想要的,你可以通过使用 `go mod why -m` 和/或 `go mod graph` 找到原因,并使用 `go get` 升级或降级到正确的版本。(如果您请求的版本比之前选择的版本更旧,`go get` 将根据需要降低其他依赖关系,以保持兼容性。) 例如: ```bash -$ go mod why -m rsc.io/binaryregexp +$ Go mod why -m rsc.io/binaryregexp [...] -$ go mod graph | grep rsc.io/binaryregexp +$ Go mod graph | grep rsc.io/binaryregexp [...] -$ go get rsc.io/binaryregexp@v0.2.0 +$ Go get rsc.io/binaryregexp@v0.2.0 $ ``` @@ -115,10 +115,10 @@ $ 对于没有依赖关系管理系统的 Go 项目,首先创建一个 `go.mod` 文件: ```bash -$ git clone https://go.googlesource.com/blog +$ Git clone https://go.googlesource.com/blog [...] $ cd blog -$ go mod init golang.org/x/blog +$ Go mod init golang.org/x/blog go: creating new go.mod: module golang.org/x/blog $ cat go.mod module golang.org/x/blog @@ -127,14 +127,14 @@ go 1.12 $ ``` -如果没有以前依赖项管理中的配置文件,`go mod init` 将创建一个 `go.mod` 文件只有模块和 go 指令。在当前案例中,我们将模块路径设置为 `golang.org/x/blog`,因为这是它的[自定义导入路径](https://golang.org/cmd/go/#hdr-Remote_import_paths)。用户可以使用此路径导入包,我们必须注意不要更改它。 +如果没有以前依赖项管理中的配置文件,`go mod init` 将创建一个 `go.mod` 文件只有模块和 Go 指令。在当前案例中,我们将模块路径设置为 `golang.org/x/blog`,因为这是它的[自定义导入路径](https://golang.org/cmd/go/#hdr-Remote_import_paths)。用户可以使用此路径导入包,我们必须注意不要更改它。 -模块指令声明模块路径,go 指令声明用于编译模块内代码的 go 语言的预期版本。 +模块指令声明模块路径,go 指令声明用于编译模块内代码的 Go 语言的预期版本。 接下来,运行 `go mod tidy` 添加模块的依赖项: ```bash -$ go mod tidy +$ Go mod tidy go: finding golang.org/x/website latest go: finding gopkg.in/tomb.v2 latest go: finding golang.org/x/net latest @@ -175,8 +175,8 @@ $ `go mod tidy` 为模块中的包以及导入的包传递添加到模块中,并为特定版本的每个库构建了一个带有校验和的 `go.sum` 文件。让我们通过代码构建和测试试一试: ```bash -$ go build ./... -$ go test ./... +$ Go build ./... +$ Go test ./... ok golang.org/x/blog 0.335s ? golang.org/x/blog/content/appengine [no test files] ok golang.org/x/blog/content/cover 0.040s @@ -193,19 +193,19 @@ $ 在迁移到 Go 模块之后,有些测试可能需要调整。 -如果测试需要在包目录中写入文件,那么当包目录位于模块缓存(只读)中时,测试可能会失败。特别是,这可能导致 go test all 失败。测试应该将需要写入的文件复制到临时目录。 +如果测试需要在包目录中写入文件,那么当包目录位于模块缓存(只读)中时,测试可能会失败。特别是,这可能导致 Go test all 失败。测试应该将需要写入的文件复制到临时目录。 如果测试依赖于相对路径(../package-in-another-module)来定位和读取另一个包中的文件,那么如果该包位于另一个模块中,则测试将失败,该模块将位于模块缓存的版本控制子目录中,或者位于 replace 指令中指定的路径中。如果是这种情况,您可能需要将测试输入复制到模块中,或者将测试输入从原始文件转换为嵌入.go 源文件中的数据。 -如果测试期望测试中的 go 命令以 GOPATH 模式运行,那么它可能会失败。如果是这种情况,您可能需要添加一个 go.mod 到要测试的源树,或显式地设置 GO111MODULE=off。 +如果测试期望测试中的 Go 命令以 GOPATH 模式运行,那么它可能会失败。如果是这种情况,您可能需要添加一个 go.mod 到要测试的源树,或显式地设置 GO111MODULE=off。 ## 发布一个版本 最后,您应该标记并发布新模块的发布版本。如果还没有发布任何版本,这是可选的,但是没有正式的版本,下游用户将依赖使用伪版本([pseudo-versions](https://golang.org/cmd/go/#hdr-Pseudo_versions))的特定提交,而伪版本可能更难支持。 ```bash -$ git tag v1.2.0 -$ git push origin v1.2.0 +$ Git tag v1.2.0 +$ Git push origin v1.2.0 ``` 新的 `go.mod` 文件为模块定义了一个规范导入路径,并添加了新的最低版本需求。如果您的用户已经使用了正确的导入路径,并且您的依赖项没有进行破坏(兼容性)的更改,则添加 go.mod 文件是向下(后)兼容(向旧版本兼容)的,但这是一个重要的改变,可能会暴露现有的问题。如果已有版本标记,则应增加次要版本([minor version](https://semver.org/#spec-item-7))。 diff --git a/published/tech/20190829-Module-Mirror-and-Checksum-Database-Launched.md b/published/tech/20190829-Module-Mirror-and-Checksum-Database-Launched.md new file mode 100644 index 000000000..52d9fad2c --- /dev/null +++ b/published/tech/20190829-Module-Mirror-and-Checksum-Database-Launched.md @@ -0,0 +1,59 @@ +首发于:https://studygolang.com/articles/24556 + +# Go1.13 推出模块镜像和校验和数据库(Module Mirror and Checksum Database Launched) + +我们很高兴地分享我们的模块 [镜像](https://proxy.golang.org/) ,[索引](https://index.golang.org/) 和 [校验和数据库](https://sum.golang.org/) 现已准备就绪! 对于 [Go 1.13 模块用户](https://golang.org/doc/go1.13#introduction) ,go 命令将默认使用模块镜像和校验和数据库。 有关这些服务的隐私信息,请参阅 [proxy.golang.org/privacy](proxy.golang.org/privacy) ,有关配置详细信息,请参阅 [go 命令文档](https://golang.org/cmd/go/#hdr-Module_downloading_and_verification) ,包括如何禁用这些服务器或使用不同的服务器。 如果您依赖于非公共模块,请参阅 [配置你的环境的文档](https://golang.org/cmd/go/#hdr-Module_configuration_for_non_public_modules) 。 + +这篇文章将描述这些服务及使用它们的好处,并总结了 Gophercon 2019 提到 [Go Module Proxy: Life of a Query](https://www.youtube.com/watch?v=KqTySYYhPUE&feature=youtu.be) 的一些要点。如果您对完整的演讲感兴趣,请参阅 [录制内容](https://www.youtube.com/watch?v=KqTySYYhPUE&feature=youtu.be) 。 + +## 模块镜像(Module Mirror) + +模块是一组版本化的 Go 包,每个版本的内容都是不可变的。这种不变性为缓存和身份验证提供了新的机会。当以模块模式运行时,它必须获取包含所请求的包的模块,以及该模块引入的任何新依赖项,根据需要更新 go.mod 和 go.sum 文件。从版本控制中获取模块在系统的延迟和存储方面可能是昂贵的:go 命令可能被迫下载包含传递依赖的存储库的完整提交历史记录,即使是未构建的存储库,也只是解决它的版本。 + +解决方法是使用模块代理,它代表一种更适合 Go 命令需求的 API(参考 Go help goproxy )。当使用代理以模块模式运行时,它只需要请求指定的模块元数据或源代码,所以它可以更快地工作,而不用担心其余部分。下面是一个示例,说明 Go 命令如何通过请求版本列表来获取代理,然后使用最新标记版本的 info,mod 和 zip 文件。 +![an example of how the Go command may use a proxy](https://blog.golang.org/module-mirror-launch/proxy-protocol.png) + +模块镜像是一种特殊的模块代理,它将元数据和源代码缓存在自己的存储系统中,允许镜像继续提供原始位置不再提供的源代码。这可以加快下载速度并防止因为代码更迭导致的依赖关系丢失。有关更多信息,请参阅 [2019 年的 Go Modules](https://blog.golang.org/modules2019) 。 + +Go 团队维护一个模块镜像,在 [proxy.golang.org](proxy.golang.org) 上提供,模块用户从 Go 1.13 开始默认使用这个模块镜像。 如果您运行的是早期版本的 Go 命令,则可以通过在本地环境中设置 `GOPROXY=https://proxy.golang.org` 来使用此服务。 + +## 校验和数据库(Checksum Database) + +模块引入了 go.sum 文件, 该文件保存首次下载时 go.mod 文件下的依赖项和每个依赖项的源代码 SHA-256 。go 命令可以使用哈希去检测原始服务器或代理是否有提供给你相同版本但代码不同的不当行为。 + +go.sum 的局限性在于它完全信任你第一次拉取的代码。当你添加新的依赖到你的模块时(可能通过升级现有的依赖),go 命令将获取代码和动态地将依赖添加到 go.sum 文件中。问题是那些 go.sum 行没有被别人检查:他们可能与 Go 命令刚刚为其他人生成的 go.sum 行不同,可能是因为代理故意提供针对你的恶意代码。 + +Go 的解决方案就是将 go.sum 的每一行记录的全局源,称为校验和数据库,它确保 Go 命令总是向每个人的 go.sum 文件添加相同的行。不论什么时候,go 命令接受新的源码,它可以通过全局数据库校验代码的哈希值来确保哈希值是否匹配,以此保证每个人使用相同版本是相同的代码。 + +[sum.golang.org](sum.golang.org) 校验和数据库提供了校验和数据库,并构建在由 [Trillian](https://github.com/google/trillian) 支持的哈希的 [透明日志](https://research.swtch.com/tlog)(或"Merkle 树")。 Merkle 树的主要优点就是它具有防篡改功能,并且具有不允许未被发现的不良行为的属性,这使得它比简单的数据库更可靠。go 命令使用树来检查『包含』证明(日志中存在特定记录)和『一致性』证明(树未被篡改),然后将新的 go.sum 行添加到模块中。下面是这种树的样子: +![tree](https://blog.golang.org/module-mirror-launch/tree.png) + +校验和数据库支持一系列端点给 Go 命令请求和校验 go.sum。 `/lookup` 端点提供 "signed tree head"(STH)和请求 go.sum 行。`/tile` 端点提供称为 tiles 的树的块,go 命令可以使用它来进行校样。下面是 Go 命令如何通过执行 `/lookup` 模块版本,然后证明所需的 tiles 来与校验和数据库交互的示例。 +![how the Go command may interact with the checksum database](https://blog.golang.org/module-mirror-launch/sumdb-protocol.png) + +如果你在使用 Go 1.12 或更早的版本,你可以手动敲入 gosumcheck 检查校验和数据库中的 go.sum 文件: + +``` +$ Go get golang.org/x/mod/gosumcheck +$ gosumcheck /path/to/go.sum +``` + +除了通过 Go 命令执行校验外,第三方审计员还可以通过迭代日志来查找错误条目。他们可以一起工作,闲聊树的状态,以确保它保持不受影响,我们希望 Go 社区能够运行它。 + +## 模块索引(Module Index) + +[index.golang.org](index.golang.org) 提供了 [proxy.golang.org](proxy.golang.org) 可用的模块索引服务,对于希望保留他们的可用缓存开发者来说特别有用,或者保持一些人们正在使用的模块是最新的版本。 + +## 反馈与建议 + +我们希望这些服务可以提升您使用模块时的体验,如果在使用过程中遇到问题,希望获得您的反馈或建议。 + +--- + +via: https://blog.golang.org/module-mirror-launch + +作者:[Katie Hockman](https://twitter.com/katie_hockman) # 原文没有这名作者的链接,这个是我 google 搜出来的。 +译者:[ZackLiuCH](https://github.com/ZackLiuCH) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190829-Using-w-and-s-Flags-in-Golang.md b/published/tech/20190829-Using-w-and-s-Flags-in-Golang.md new file mode 100644 index 000000000..df29ac0d2 --- /dev/null +++ b/published/tech/20190829-Using-w-and-s-Flags-in-Golang.md @@ -0,0 +1,126 @@ +首发于:https://studygolang.com/articles/25914 + +# 在 Golang 中使用 -w 和 -s 标志 + +今天的博客文章来自 Valery,这是 [Spiral Scout](https://spiralscout.com/) 的一名高级软件工程师,专门从事 Golang(Go)开发。 + +作为在 Golang 以及许多其他编程语言方面具有专业知识的软件开发机构,我们知道对于我们的工程师和质量保证专家而言,能够与社区分享他们的知识和经验非常重要。 感谢 Valery 这篇出色的文章和有用的 Golang 测试技巧! + +当我在 GitHub 上查找一些良好的工程实践以备应用时,我注意到许多开发人员编译他们的 Go 程序时经常出现的问题,他们中许多人都使用链接器标记(linker flags)来减小输出文件大小,尤其是同时使用 `-w` 和 `-s` 标记所带来的叠加效果。 + +在软件测试中,标记也被称为参数。当从命令行运行程序时,它们用于标识特定的状态或条件。 标记可以打开或关闭,并且在整个软件开发过程中大量语言和框架都采用这种方式。 + +本文致力于说明在 Go 中实现 `-w` 和 `-s` 标志的效果,并提供可以更有效地使用它们的方法。 + +## `-w` 和 `-s` 标志如何与 DWARF 和 ELF 配合使用 + +在讨论何时以及如何使用 `-w` 和 `-s` 标志之前,先简要介绍一下我的测试环境。 我使用的硬件/软件组合包括: + +* A Dell XPS 9570 laptop +* Manjaro Linux OS +* Testing branch + +`-w` 和 `-s` 标志通常用在 App 链接阶段和 Go 编译阶段 `-ldflags` 指令结合使用 (参见 [Go 命令文档](https://golang.org/src/cmd/go/alldocs.go) )。有关标志的更多信息,请参见:https://golang.org/cmd/link/。 + +在我们仔细查看 `-w` 标志并拆解二进制代码以检查 DWARF 符号表是否消失之前,我建议先明确 DWARF 符号表的定义。 + +DWARF 是一种可以包含在二进制文件中的调试数据格式。 根据维基百科 [DWARF 条目](https://en.wikipedia.org/wiki/DWARF),此格式是与称为 ELF(可执行和可链接格式)的标准通用文件格式一起开发的。 [这篇文章](https://eli.thegreenplace.net/2011/02/07/how-debuggers-work-part-3-debugging-information/) 很好地解释了调试器如何与 DWARF 表配合工作。 + +Golang 的创建者们在 [Go DWARF 源代码](https://golang.org/src/cmd/link/internal/ld/dwarf.go) 中分享了更多信息,包括有关如何形成此表并将其嵌入以 Go 编写的二进制文件的详细信息。 + +我将通过示例代码介绍以下一些要点。 + +首先,我们要使用以下步骤读取 DWARF 信息: + +1. 编译 Go 程序(开始我们仅使用 Go build 命令) + +```shell +go build -o simple_build cmd/main.go +``` + +2. 读取符号表。使用 `readelf -Ws` 可以方便的实现符号表读取。但是你也可以使用其他更熟悉的工具读取文件头(比如 `objdump -h`)。 + +3. 请注意生成的程序的头部内容。 + +![](https://cdn.jsdelivr.net/gh/studygolang/gctt-images2@master/using-w-and-s-flags-in-golang/section-headers-no-flags.png) + +我们可以看到这个二进制文件中包含了用于调试的数据(从第 24 节到第 32 节),并且还有一个符号表和字符串表。(如下所述) + +4. 使用如下命令进行读表 + +```shell +> objdump - dwarf=info main +``` + +输出看起来有些长,所以我用下面的命令把输出保存到文件中。 + +```shell +> objdump - dwarf=info main &> main.txt +``` + +你可以在下面看到输出的一部分: + +![](https://cdn.jsdelivr.net/gh/studygolang/gctt-images2@master/using-w-and-s-flags-in-golang/objdump.png) + +为了根据地址查找必要的函数,我们需要知道 PC (程序计数器)。你可以在 EIP 寄存器中找到 PC 值,它由 DW_AT_low_pc 和 DW_AT_high_pc 表示。举个例子,对于 `main.main` 函数(`main` 是 Go 运行时的函数)使用 `low_pc` ,并尝试使用 `objdump -d` 在二进制文件的位置 `0x44f930` 找到它。 + +![](https://cdn.jsdelivr.net/gh/studygolang/gctt-images2@master/using-w-and-s-flags-in-golang/objdump-pc.png) + +很棒。现在我们使用 `-w` 标志编译程序并且和不使用标志编译出来的程序进行比较。 + +5. 运行下面的命令 + +```shell +go build -ldflags=”-w” -o build_with_w cmd/main.go +``` + +然后看看生成文件的头部发生了什么变化: +![](https://cdn.jsdelivr.net/gh/studygolang/gctt-images2@master/using-w-and-s-flags-in-golang/section-headers-flags-w.png) + +正如我们看到的,`.zdebug` 部分完全消失了。通过对顶部低位(Off 列)地址相减,我们可以精确计算二进制文件减小了多少。当你把这个差值从 Bytes 转换到 KB 时,可以对实际情况有更直观的体会。 + +在这个案例中,二进制文件的总大小大约 25MB,这意味着我们节省了大约 3.7KB。这让我好奇如果我尝试使用 [Delve Go 调试器工具](https://github.com/go-delve/delve) 运行 dvl 时会发生什么? + +6. 运行 + +```shell +dlv — listen=:43671 — headless=true — api-version=2 — accept-multiclient exec ./build_with_w +``` + +... 返回了你所期待的结果: + +```shell +API server listening at: [::]:43671 +could not launch process: could not open debug info +``` + +好了,现在关于 DWARF 表和 `-w` 标志的作用变得更加清楚了。 + +让我们继续看看 `-s` 标志。根据文档,`-s` 标志不仅删除了调试信息,同时还删除了指定的符号表。不过,它与 `-s` 标志有何不同呢? + +首先,快速了解一下 — 符号表包含了局部变量、全局变量和函数名等的信息。在上图中,这些信息在第 26 和第 27 节(.symtab 和 .strtab)给出。更多关于符号表的详细信息,可以在这里找到: +[http://refspecs.linuxbase.org/elf/gabi4+/ch4.symtab.html](http://refspecs.linuxbase.org/elf/gabi4+/ch4.symtab.html) 和 [http://refspecs.linuxbase.org/elf/gabi4+/ch4.strtab.html](http://refspecs.linuxbase.org/elf/gabi4+/ch4.strtab.html). + +这次我们试试用 `-s` 标志编译一个二进制文件: + +![](https://cdn.jsdelivr.net/gh/studygolang/gctt-images2@master/using-w-and-s-flags-in-golang/section-headers-flags-s.png) + +如同我们期待的一样,关于 DWARF 的信息以及符号表和字符串表(一种发布标志)的内容都消失不见了。 + +## 这意味着什么? + +如果只想删除调试信息,只使用 `-w` 标志是最合适的。如果要另外删除符号和字符串表以减小二进制文件的大小,请使用 `-s` 标志。 + +下面是在 Golang 中使用这些 flag 的的**反面教材**,不建议大家这样使用。 +尽管两个标志似乎比一个标志好,但是对于 `-w` 和 `-s` 标志,情况却并非如此: +![](https://cdn.jsdelivr.net/gh/studygolang/gctt-images2@master/using-w-and-s-flags-in-golang/github-search.png) + +--- + +via: https://blog.spiralscout.com/using-w-and-s-flags-in-golang-97ae59b50e26 + +作者:[John Griffin](https://blog.spiralscout.com/@johnwgriffin) +译者:[befovy](https://github.com/befovy) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190905-Go-Retrospective.md b/published/tech/20190905-Go-Retrospective.md new file mode 100644 index 000000000..1e912679c --- /dev/null +++ b/published/tech/20190905-Go-Retrospective.md @@ -0,0 +1,190 @@ +首发于:https://studygolang.com/articles/28435 + +# Go 各版本回顾 + +![Illustration created for “A Journey With Go”, made from *the original Go Gopher, created by Renee French.](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-Retrospective/00.png) + +对每一个开发者来说,Go 的发展历史是必须知道的知识。了解几年来每个发行版本的主要变化,有助于理解 Go 的设计思想和每个版本的优势/弱点。想了解特定版本的更详细信息,可以点击每个版本号的链接来查看修改记录。 + +## [Go 1.0](https://blog.golang.org/go-version-1-is-released) — 2012 年 3 月: + +Go 的第一个版本,带着一份[兼容性说明文档](https://golang.org/doc/go1compat)来保证与未来发布版本的兼容性,进而不会破坏已有的程序。 + +第一个版本已经有 `go tool pprof` 命令和 `go vet` 命令。 `go tool pprof` 与 Google 的 [pprof C++ profiler](https://github.com/gperftools/gperftools) 稍微有些差异。`go vet`(前身是 `go tool vet`)命令可以检查包中潜在的错误。 + +## [Go 1.1](https://blog.golang.org/go-11-is-released) — 2013 年 5 月: + +这个 Go 版本专注于优化语言(编译器,gc,map,go 调度器)和提升它的性能。下面是一些提升的例子: + +![https://dave.cheney.net/2013/05/21/go-11-performance-improvements](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-Retrospective/01.png) + +这个版本也嵌入了[竞争检测](https://blog.golang.org/race-detector),在语言中是一个很强大的工具。 + +*你可以在我的文章 [ThreadSanitizer 竞争检测](https://medium.com/a-journey-with-go/go-race-detector-with-threadsanitizer-8e497f9e42db)中发现更多的信息。* + +[重写了 Go 调度器](https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw/edit) 显著地提升了性能。Go 调度器现在被设计成这样: + +![https://rakyll.org/scheduler/](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-Retrospective/02.png) + +`M` 是一个 OS 线程,`P` 表示一个处理器(`P` 的数量不能大于 GOMAXPROCS),每个 `P` 作为一个本地协程队列。在 1.1 之前 `P` 不存在,协程是用一个全局的 mutex 在全局范围内管理的。随着这些优化,工作窃取也被实现了,允许一个 `P` 窃取另一个 `P` 的协程: + +![https://rakyll.org/scheduler/](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-Retrospective/03.png) + +*阅读 [Jaana B.Dogan](https://twitter.com/rakyll) 的 [Go 的工作窃取调度器](https://rakyll.org/scheduler/) 可以查看更多关于 Go 调度器和工作窃取的信息。* + +## [Go 1.2](https://blog.golang.org/go12) — 2013 年 12 月: + +本版本中 `test` 命令支持测试代码覆盖范围并提供了一个新命令 `go tool cover` ,此命令能测试代码覆盖率: + +![https://blog.golang.org/cover](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-Retrospective/04.png) + +这个命令也能提供覆盖信息: + +![https://blog.golang.org/cover](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-Retrospective/05.png) + +## [Go 1.3](https://blog.golang.org/go1.3) — 2014 年 6 月: + +这个版本对栈管理做了重要的改进。栈可以申请[连续的内存片段](https://docs.google.com/document/d/1wAaf1rYoM4S4gtnPh0zOlGzWtrZFQ5suE8qr2sD8uWQ/pub),提高了分配的效率,使下一个版本的栈空间降到 2KB。 + +栈频繁申请/释放栈片段会导致某些元素变慢,本版本也改进了一些由于上述场景糟糕的分配导致变慢的元素。下面是一个 `json` 包的例子,展示了它对栈空间的敏感程度: + +![https://docs.google.com/document/d/1wAaf1rYoM4S4gtnPh0zOlGzWtrZFQ5suE8qr2sD8uWQ/pub](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-Retrospective/06.png) + +使用连续的栈修复了这个元素效率低下的问题。下面是另一个例子,`html/template` 包的性能对栈大小也很敏感: + +![图 7](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-Retrospective/07.png) + +*阅读我的[Go 协程栈空间的发展](https://medium.com/a-journey-with-go/go-how-does-the-goroutine-stack-size-evolve-447fc02085e5)查看更多信息。* + +这个版本在 `sync` 包中发布了 `Pool`。 这个元素允许我们复用结构体,减少了申请的内存的次数,同时也是很多 Go 生态获得改进的根源,如标准库或包里的 `encoding/json` 或 `net/http`,还有 Go 社区里的 `zap`。 + +*可以在我的文章 [Sync.Pool 设计理解](https://medium.com/@blanchon.vincent/go-understand-the-design-of-sync-pool-2dde3024e277) 中查看更多关于 `Pool` 的信息。* + +Go 团队也[对通道作了改进](https://docs.google.com/document/d/1yIAYmbvL3JxOKOjuCyon7JhW4cSv1wy5hC0ApeGMV9s/pub),让它们变得更快。下面是以 Go 1.2 和 Go 1.3 作对比运行的基准: + +![图 8](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-Retrospective/08.png) + +## [Go 1.4](https://blog.golang.org/go1.4) — 2014 年 12 月: + +此版本带来了官方对 Android 的支持,[golang.org/x/mobile]([Go 1.4](https://blog.golang.org/go1.4) ) 让我们可以只用 Go 代码就能写出简单的 Android 程序。 + +归功于更高效的 gc,之前用 C 和汇编写的运行时代码被翻译成 Go 后,堆的大小降低了 10% 到 30%。 + +与版本无关的一个巧合是,Go 项目管理从 Mercurial 移植到了 Git,代码从 Google Code 移到了 Github。 + +Go 也提供了 `go generate` 命令通过扫描用 `//go:generate` 指示的代码来简化代码生成过程。 + +*在 [Go 博客](https://blog.golang.org/) 和文章[生成代码](https://blog.golang.org/generate)中可以查看更多信息。* + +## [Go 1.5](https://blog.golang.org/go1.5) — 2015 年 8 月: + +这个新版本,[发布时间推迟](https://docs.google.com/document/d/106hMEZj58L9nq9N9p7Zll_WKfo-oyZHFyI6MttuZmBU/edit#)了两个月,目的是在以后每年八月和二月发布新版本: + +![https://github.com/golang/go/wiki/Go-Release-Cycle](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-Retrospective/09.png) + +这个版本对 [gc](https://golang.org/doc/go1.5#gc) 进行了[重新设计](https://docs.google.com/document/d/1wmjrocXIWTr1JxU-3EQBI6BK6KgtiFArkG47XK73xIQ/edit#)。归功于并发的回收,在回收期间的等待时间大大减少。下面是一个 Twitter 生产环境的服务器的例子,等待时间由 300ms 降到 30ms: + +![https://blog.golang.org/ismmkeynote](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-Retrospective/10.png) + +这个版本也发布了运行时追踪,用命令 `go tool trace` 可以查看。测试过程或运行时生成的追踪信息可以用浏览器窗口展示: + +![[Original Go Execution Tracer Document](https://docs.google.com/document/d/1FP5apqzBgr7ahCCgFO-yoVhk4YZrNIDNf9RybngBc14/pub)](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-Retrospective/11.png) + +## [Go 1.6](https://blog.golang.org/go1.6) — 2016 年 2 月: + +这个版本最重大的变化是使用 HTTPS 时默认支持 HTTP/2。 + +在这个版本中 gc 等待时间也降低了: + +![https://blog.golang.org/ismmkeynote](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-Retrospective/12.png) + +## [Go 1.7](https://blog.golang.org/go1.7) — 2016 年 8 月: + +这个版本发布了 [context 包](https://medium.com/a-journey-with-go/go-context-and-cancellation-by-propagation-7a808bbc889c),为用户提供了处理超时和任务取消的方法。 + +*阅读我的文章 [传递上下文和取消](https://medium.com/a-journey-with-go/go-context-and-cancellation-by-propagation-7a808bbc889c)来获取更多关于 context 的信息。* + +对编译工具链也作了优化,编译速度更快,生成的二进制文件更小,有时甚至可以减小 20% 到 30%。 + +## [Go 1.8](https://blog.golang.org/go1.8) — 2017 年 2 月: + +把 gc 的停顿时间减少到了 1 毫秒以下: + +![https://blog.golang.org/ismmkeynote](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-Retrospective/13.png) + +其他的停顿时间已知,并会在下一个版本中降到 100 微秒以内。 + +这个版本也改进了 defer 函数: + +![https://medium.com/@blanchon.vincent/go-how-does-defer-statement-work-1a9492689b6e](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-Retrospective/14.png) + +*我的文章 [defer 语句工作机制](https://medium.com/a-journey-with-go/go-how-does-defer-statement-work-1a9492689b6e)中有更多信息。* + +## [Go 1.9](https://blog.golang.org/go1.9) — 2017 年 8 月: + +这个版本支持下面的别名声明: + +```go +type byte = uint8 +``` + +这里 `byte` 是 `uint8` 的一个别名。 + +`sync` 包新增了一个 [Map](https://golang.org/pkg/sync/#Map) 类型,是并发写安全的。 + +*我的文章 [Map 与并发写](https://medium.com/a-journey-with-go/go-concurrency-access-with-maps-part-iii-8c0a0e4eb27e) 中有更多信息。* + +## [Go 1.10](https://blog.golang.org/go1.10) — 2018 年 2 月: + +`test` 包引进了一个新的智能 cache,运行会测试后会缓存测试结果。如果运行完一次后没有做任何修改,那么开发者就不需要重复运行测试,节省时间。 + +```bash +first run: +ok /go/src/retro 0.027s +second run: +ok /go/src/retro (cached) +``` + +为了加快构建速度,`go build` 命令现在也维持了一份最近构建包的缓存。 + +这个版本没有对 gc 做实际的改变,但是确定了一个新的 SLO(Service-Level Objective): + +![https://blog.golang.org/ismmkeynote](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-Retrospective/15.png) + +## [Go 1.11](https://blog.golang.org/go1.11) — 2018 年 8 月: + +Go 1.11 带来了一个重要的新功能:[Go modules](https://blog.golang.org/using-go-modules)。去年的调查显示,Go modules 是 Go 社区遭遇重大挑战后的产物: + +![https://blog.golang.org/survey2018-results](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-Retrospective/16.png) + +第二个特性是实验性的 [WebAssembly](https://webassembly.org/),为开发者提供了把 Go 程序编译成一个可兼容四大主流 Web 浏览器的二进制格式的能力。 + +## [Go 1.12](https://blog.golang.org/go1.12) — 2019 年 2 月: + +基于 `analysis` 包重写了 `go vet` 命令,为开发者写自己的检查器提供了更大的灵活性。 + +*我的文章[构建自己的分析器](https://medium.com/@blanchon.vincent/go-how-to-build-your-own-analyzer-f6d83315586f)中有更多信息。* + +## [Go 1.13](https://blog.golang.org/go1.13) — 2019 年 9 月: + +改进了 `sync` 包中的 `Pool`,在 gc 运行时不会清除 pool。它引进了一个缓存来清理两次 gc 运行时都没有被引用的 pool 中的实例。 + +重写了逃逸分析,减少了 Go 程序中堆上的内存申请的空间。下面是对这个新的逃逸分析运行基准的结果: + +![https://github.com/golang/go/issues/23109](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-Retrospective/17.png) + +## [Go1.14](https://blog.golang.org/go1.14) - 2020 年 2 月: + +现在 Go Module 已经可以用于生产环境,鼓励所有用户迁移到 Module。该版本支持嵌入具有重叠方法集的接口。性能方面做了较大的改进,包括:进一步提升 defer 性能、页分配器更高效,同时 timer 也更高效。 + +现在,Goroutine 支持异步抢占。 + +--- + +via: https://medium.com/a-journey-with-go/go-retrospective-b9723352e9b0 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190905-Logging-HTTP-requests-in-Go.md b/published/tech/20190905-Logging-HTTP-requests-in-Go.md new file mode 100644 index 000000000..620222107 --- /dev/null +++ b/published/tech/20190905-Logging-HTTP-requests-in-Go.md @@ -0,0 +1,365 @@ +首发于:https://studygolang.com/articles/24876 + +# Go 中记录 HTTP 请求 + +如果你有运行的 HTTP 服务,你可能想记录 HTTP 请求。 + +请求日志有助于诊断问题。(哪些请求失败了?我们一天处理多少请求?哪些请求比较慢?) + +这对于分析是必需的。(哪个页面受欢迎?网页的浏览者都来自哪里?) + +这篇文章介绍了在 Go Web 服务器中,记录 HTTP 请求日志相关的全部内容。 + +这不是关于可复用的库,而是关于实现你自己的解决方案需要知道的事情,以及关于我日志记录的选择的描述。 + +你可以在示例应用上查看详细内容: https://github.com/essentialbooks/books/tree/master/code/go/logging_http_requests + +我在 Web 服务 [OnePage](https://onepage.nopub.io/) 中用到了这个记录系统。 + +[记录什么信息](https://onepage.nopub.io/p/Logging-HTTP-requests-in-Go-233de7fe59a747078b35b82a1b035d36#63fd0006-6ebd-442c-a463-d11862e8c33c) + +[获取要记录的信息](https://onepage.nopub.io/p/Logging-HTTP-requests-in-Go-233de7fe59a747078b35b82a1b035d36#c8a27402-1650-402a-8679-69214078b88a) + +[日志文件的格式](https://onepage.nopub.io/p/Logging-HTTP-requests-in-Go-233de7fe59a747078b35b82a1b035d36#97da9f14-289e-42f6-94fd-936a4eb88f26) + +[每日滚动日志](https://onepage.nopub.io/p/Logging-HTTP-requests-in-Go-233de7fe59a747078b35b82a1b035d36#99565a90-2f57-4aab-a5e7-5eb9a9194adc) + +[长期存储以及分析](https://onepage.nopub.io/p/Logging-HTTP-requests-in-Go-233de7fe59a747078b35b82a1b035d36#a099947d-2079-4d1d-a996-41e4ed1ff02a) + +[更多的 Go 资源](https://onepage.nopub.io/p/Logging-HTTP-requests-in-Go-233de7fe59a747078b35b82a1b035d36#4405e240-bd60-45a8-ba47-65e175eb7f8f) + +[招聘 Go 开发者](https://onepage.nopub.io/p/Logging-HTTP-requests-in-Go-233de7fe59a747078b35b82a1b035d36#5076eef2-d176-43f3-bab5-c0d3030efa23) + +## 记录什么信息 + +为了展示通常会记录什么信息,这里有一条 Apache 的扩展日志文件格式的日志记录样本。 + +``` +111.222.233.123 HOME - [01/Feb/1998:01:08:39 -0800] "GET /bannerad/ad.htm HTTP/1.0" 200 198 "http://www.referrer.com/bannerad/ba_intro.htm" "Mozilla/4.01 (Macintosh; I; PPC)" +``` + +我们能看到: + +- `111.222.233.123` :客户端发起 HTTP 请求的 IP 地址。 +- `HOME` : 域(适用单个 Web 服务器提供多个域的情况)。 +- `-` :用户认证信息(这个例子下为空)。 +- `[01/Feb/1998:01:08:39 -0800]` :请求被记录的时间。 +- `"GET /bannerad/ad.htm HTTP/1.0"` :HTTP 方法,URL 以及协议类型。 +- `200`:HTTP 状态码。200 代表请求被成功处理。 +- `198`:响应体的大小。 +- `"http://www.referrer.com/bannerad/ba_intro.htm"` :引荐来源(referer)。 +- `"Mozilla/4.01 (Macintosh; I; PPC)"` :应该认为用户代理标志 HTTP 客户端(极大程度上是一个 Web 浏览器) + +我们可以记录更多的信息,或者选择不去记录上面的某些信息。 + +个人而言: + +- 我也会记录服务器处理单次请求的耗时,毫秒为单位。(毫秒对我而言已经足够了,用微秒来记录也可以但有点过度了) +- 我不记录协议(比如 HTTP/1.0)。 +- 服务器通常只提供单一用途,所以不需要记录域。 +- 如果服务有用户认证信息,我也会记录用户 ID。 + +## 获取记录的信息 + +Go 中标准 HTTP 处理函数的签名如下: + +```go +func(w http.ResponseWriter, r *http.Request) +``` + +我们会把日志记录作为所谓的中间件,这是一种向 HTTP 服务管道中添加可复用功能的一个方法。 + +我们有 `logReqeustHandler` 函数,它以 `http.Handler` 接口作为参数,然后返回另一个包装了原有处理器并添加了日志记录功能的 `http.Handler`。 + +```go +func logRequestHandler(h http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + + // 在我们包装的时候调用原始的 http.Handle + h.ServeHTTP(w, r) + + // 得到请求的有关信息,并记录之 + uri := r.URL.String() + method := r.Method + // ... 更多信息 + logHTTPReq(uri, method, ....) + } + + // 用 http.HandlerFunc 包装函数,这样就实现了 http.Handler 接口 + return http.HandlerFunc(fn) +} +``` + +我们可以把中间件处理器嵌套到每一个(HTTP 处理器)的顶部,这样所有(处理器)都会拥有这些功能。 + +下面介绍了我们如何使用它来把日志记录功能添加到所有的请求函数: + +```go +func makeHTTPServer() *http.Server { + mux := &http.ServeMux{} + mux.HandleFunc("/", handleIndex) + // ... 可能会添加更多处理器 + + var handler http.Handler = mux + // 用我们的日志记录器包装 mux 。 this will (译者注:应当是注释没写全) + handler = logRequestHandler(handler) + // ... 可能会添加更多中间件处理器 + + srv := &http.Server{ + ReadTimeout: 120 * time.Second, + WriteTimeout: 120 * time.Second, + IdleTimeout: 120 * time.Second, // Go 1.8 开始引进 + Handler: handler, + } + return srv +} +``` + +首先,我们定义一个 struct 封装所有需要记录的信息: + +```go +// LogReqInfo 描述了有关 HTTP 请求的信息(译者注:此处为作者笔误,应当是 HTTPReqInfo) +type HTTPReqInfo struct { + // GET 等方法 + method string + uri string + referer string + ipaddr string + // 响应状态码,如 200,204 + code int + // 所发送响应的字节数 + size int64 + // 处理花了多长时间 + duration time.Duration + userAgent string +} +``` + +下面是 `logRequestHandler` 的全部实现: + +```go +func logRequestHandler(h http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + ri := &HTTPReqInfo{ + method: r.Method, + uri: r.URL.String(), + referer: r.Header.Get("Referer"), + userAgent: r.Header.Get("User-Agent"), + } + + ri.ipaddr = requestGetRemoteAddress(r) + + // this runs handler h and captures information about + // HTTP request + // 这里运行处理器 h 并捕获有关 HTTP 请求的信息 + m := httpsnoop.CaptureMetrics(h, w, r) + + ri.code = m.Code + ri.size = m.BytesWritten + ri.duration = m.Duration + logHTTPReq(ri) + } + return http.HandlerFunc(fn) +} +``` + +我们复盘下这个简单的例子: + +- `r.Method` 返回 HTTP 的方法,如 "GET", "POST" 等。 +- `r.URL` 是一个解析后的 url,如 `/getname?id=5`,然后 `String()返回我们需要的字符串形式。 +- `r.Header` 是代表 HTTP 头部信息的结构体。头部信息包含 `Referer` 和 `User-Agent` 以及其他信息。 +- 为了记录服务器处理请求的耗时,我们在开始时记录了 `timeStart`, 调用处理器候,通过 `time.Since(timeStart)` 获取时长。 + +其他的信息则比较难获取。 + +获取客户端 IP 地址的问题是有可能涉及到 HTTP 代理。客户端向代理发起请求,然后代理向我们请求。于是,我们拿到了代理的 IP 地址,而不是客户端的。 + +因为这样,代理通常在请求的 HTTP 头部信息中以 `X-Real-Ip` 或者 `X-Forwarded-For` 来携带客户端真正的 IP 地址。 + +下面展示了如何提取这个信息: + +```go +// Request.RemoteAddress 包含了端口,我们需要把它删掉,比如: "[::1]:58292" => "[::1]" +func ipAddrFromRemoteAddr(s string) string { + idx := strings.LastIndex(s, ":") + if idx == -1 { + return s + } + return s[:idx] +} + +// requestGetRemoteAddress 返回发起请求的客户端 ip 地址,这是出于存在 http 代理的考量 +func requestGetRemoteAddress(r *http.Request) string { + hdr := r.Header + hdrRealIP := hdr.Get("X-Real-Ip") + hdrForwardedFor := hdr.Get("X-Forwarded-For") + if hdrRealIP == "" && hdrForwardedFor == "" { + return ipAddrFromRemoteAddr(r.RemoteAddr) + } + if hdrForwardedFor != "" { + // X-Forwarded-For 可能是以","分割的地址列表 + parts := strings.Split(hdrForwardedFor, ",") + for i, p := range parts { + parts[i] = strings.TrimSpace(p) + } + // TODO: 应当返回第一个非本地的地址 + return parts[0] + } + return hdrRealIP +} +``` + +捕获响应写对象(ResponseWriter)的状态码以及响应的大小更为困难。 + +`http.ResponseWriter` 并没有给我们这些信息。但幸运的是,这是一个简单的接口: + +```go +type ResponseWriter interface { + Header() Header + Write([]byte) (int, error) + WriteHeader(statusCode int) +} +``` + +写一个包装了原始响应的接口实现,并记录我们想要了解的信息,这是可行的。幸运如我们,已经有人在包 [httpsnoop](https://github.com/felixge/httpsnoop) 中实现了。 + +## 日志文件的格式 + +Apache 的日志格式比较紧凑,虽然具备人类可读性但却难于解析。 + +有的时候,我们也需要阅读日志分析,然后我不赞成为这个格式的实现解析器的想法。 + +从实现的角度来看,一个简单的方式是用 JSON 来记录,并且换行隔开。 + +对于这种方法我不喜欢的是:JSON 不易于阅读。 + +作为一个中间层,我创建了 `siser` 库,它实现了一个可扩展,易于实现和人类可读的序列化格式。 它非常适合用于记录结构化信息,我已经在多个项目用到它了。 + +下面展示了一个简单请求是如何被序列化的: + +``` +171 1567185903788 httplog +method: GET +uri: /favicon.ico +ipaddr: 204.14.239.58 +code: 404 +size: 758 +duration: 0 +ua: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:68.0) Gecko/20100101 Firefox/68.0 +``` + +每个记录的第一行包含了以下信息: + +- `171` 是其下记录的数据的大小。提前知道数据的大小确保了安全和高效的实现。 +- `1567185903788` 是时间戳的 UNIX 格式(从系统纪元(Epoch)至今的秒数)。它让我们避免在数据里记录重复的时间戳。 +- `httplog` 是记录的类型。这让我们可以往同一文件写不同类型的日志。在我们的场景下,所有记录的类型都是一样的。 + +然后第一行之后的数据都是 `key:value` 格式。 + +下面展示了我们如何序列化一条记录并把它写到日志文件: + +```go +var ( + muLogHTTP sync.Mutex +) + +func logHTTPReq(ri *HTTPReqInfo) { + var rec siser.Record + rec.Name = "httplog" + rec.Append("method", ri.method) + rec.Append("uri", ri.uri) + if ri.referer != "" { + rec.Append("referer", ri.referer) + } + rec.Append("ipaddr", ri.ipaddr) + rec.Append("code", strconv.Itoa(ri.code)) + rec.Append("size", strconv.FormatInt(ri.size, 10)) + durMs := ri.duration / time.Millisecond + rec.Append("duration", strconv.FormatInt(int64(durMs), 10)) + rec.Append("ua", ri.userAgent) + + muLogHTTP.Lock() + defer muLogHTTP.Unlock() + _, _ = httpLogSiser.WriteRecord(&rec) +} +``` + +## 日志每日滚动 + +我通常在 Ubuntu 上部署服务器,并把日志记录到 `/data//log` 目录。 + +我们不能一直往同一个日志文件里写。否则到最后会用完所有空间。 + +对于长时间的日志,我通常每天一个日志文件,以日期命名。如 `2019-09-23.txt`, `2019-09-24.txt` 等等。 + +这有时称为日志滚动 ( log rotate). + +为了避免重复实现这个功能,我写了一个库 [dailyrotate](https://github.com/kjk/dailyrotate)。 + +它实现了 `Write`, `Close` 以及 `Flush` 方法,所以它易于接入到现有已使用 `io.Reader` 等的代码。 + +你要指定使用哪个目录,以及日志命名的格式。这个格式通过 Go 的时间格式化函数来实现的。我通常使用 `2006-01-02.txt` 每天生成一个唯一的时间,并根据日期来排序,`txt` 则是工具识别文本文件而不是二进制文件的标志。 + +接着就和写普通的文件一样,以及确保代码会每天创建文件。 + +你也可以提供一个通知的回调,当发生日志滚动时会通知你,这样就可以做一些动作,例如把刚刚关闭的文件上传线上存储,或者对它做分析。 + +下面是代码: + +```go +pathFormat := filepath.Join("dir", "2006-01-02.txt") +func onClose(path string, didRotate bool) { + fmt.Printf("we just closed a file '%s', didRotate: %v\n", path, didRotate) + if !didRotate { + return + } + // process just closed file e.g. upload to backblaze storage for backup + go func() { + // if processing takes a long time, do it in a background goroutine + }() +} + +w, err := dailyrotate.NewFile(pathFormat, onClose) +panicIfErr(err) + +_, err = io.WriteString(w, "hello\n") +panicIfErr(err) + +err = w.Close() +panicIfErr(err) +``` + +## 长期存储以及分析 + +为了长期存储我把它们压缩成 gzip 并把文件上传到线上存储。这有很多选择:S3, Google Storage, Digital Ocean Spaces, BackBlaze。 + +我倾向于使用 Digital Ocean Spaces 或者 BackBlaze,因为他们足够廉价(存储成本和带宽成本)。 + +它们均支持 S3 协议,所以我使用 [go-minio](https://github.com/minio/minio-go) 库。 + +为了分析,我每天都会运行代码,生成大部分有用信息的总结。 + +还有其他的做法,可以把数据引入到如 [BigQuery](https://cloud.google.com/bigquery/what-is-bigquery) 的系统。 + +## 更多的 Go 资源 + +- [Essential Go](https://www.programming-books.io/essential/go/) 是由我所维护关于 Go ,免费且全面的书籍。 +- [siser](https://github.com/kjk/siser) 是我写的库,实现了简单的序列化格式。 +- 我还写了一篇有关 `siser` 设计的[深度文章](https://blog.kowalczyk.info/article/fc9203f7c72a4532b1ae51d018fef7b3/trade-offs-in-designing-versatile-log-format.html) 。 +- [dailyrotate](https://github.com/kjk/dailyrotate) 是我写的库,实现了文件每日滚动。 + +## 招聘 Go 程序员 + +如果你正在寻找程序员一起工作,[希望一起谈一下](https://blog.kowalczyk.info/goconsultantforhire.html)。 + +由 [Krzysztof Kowalczyk](https://blog.kowalczyk.info/) 所著。 + +--- + +via: https://onepage.nopub.io/p/Logging-HTTP-requests-in-Go-233de7fe59a747078b35b82a1b035d36 + +作者:[Krzysztof Kowalczyk](https://onepage.nopub.io/u/bb760e2dd6794b64b2a903005b21870a) +译者:[LSivan](https://github.com/LSivan) +校对:[JYSDeveloper](https://github.com/JYSDeveloper) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190906-Go-Concurrency-Bugs-in-Go.md b/published/tech/20190906-Go-Concurrency-Bugs-in-Go.md index 3f0c54612..84329cbb5 100644 --- a/published/tech/20190906-Go-Concurrency-Bugs-in-Go.md +++ b/published/tech/20190906-Go-Concurrency-Bugs-in-Go.md @@ -17,15 +17,15 @@ Go 目前正在通过新的并发原语(concurrency primitives)goroutine 和 ## 研究发现 这项研究发现,大多数开发人员认为 channel 不会像其他常用同步方法那样不可靠,但其实并不是这样: -> 结论2:与通常的看法相反,消息传递比共享内存可能导致更多的阻塞 bug 。希望大家注意消息传递带来的潜在危险,我们也会进一步研究该领域中的错误检测机制。 +> 结论 2:与通常的看法相反,消息传递比共享内存可能导致更多的阻塞 bug 。希望大家注意消息传递带来的潜在危险,我们也会进一步研究该领域中的错误检测机制。 尽管如此,该研究也表明 channel 的前景很好: -> 发现9:共享内存同步仍然是主要的非阻塞性 bug 的修复方法,但是 channel 不仅被广泛地用于修复与 channel 相关的 bug,而且还用于修复共享内存的 bug。 +> 发现 9:共享内存同步仍然是主要的非阻塞性 bug 的修复方法,但是 channel 不仅被广泛地用于修复与 channel 相关的 bug,而且还用于修复共享内存的 bug。 ## 结论 这项研究清楚地表明,新的原语不会减少并发环境中的 bug。但是,如果更好地了解 channel 和 goroutines,可以避免大多数这类问题: -> 发现6:我们研究中的大多数阻塞 bug(传统的共享内存 bug 和消息传递 bug)都可以通过简单的解决方案来解决。 +> 发现 6:我们研究中的大多数阻塞 bug(传统的共享内存 bug 和消息传递 bug)都可以通过简单的解决方案来解决。 例如 Docker 中的这个问题: @@ -33,15 +33,15 @@ Go 目前正在通过新的并发原语(concurrency primitives)goroutine 和 运行 ```go vet``` 命令可以避免上面这种错误。 -## 对Go生态系统的贡献 +## 对 Go 生态系统的贡献 在研究期间,研究员们还发现有可能开发出更好的静态代码检查器: -> 结论3: 由于 Go 中的阻塞 bug 的原因与其修复方法密切相关,并且修复方法不复杂,开发出全自动或半自动化的工具来修复 Go 的阻塞 bug 是很有希望的。 +> 结论 3: 由于 Go 中的阻塞 bug 的原因与其修复方法密切相关,并且修复方法不复杂,开发出全自动或半自动化的工具来修复 Go 的阻塞 bug 是很有希望的。 根据 Go 社区多年来的经验,运行时的检测工具也可以得到改进: -> 结论4:仅仅用死锁检测器在运行时检测 Go 阻塞 bug 不是很有效。未来的研究应侧重于构建新颖的阻塞 bug 检测技术,例如,结合使用静态和动态阻塞模式的检测。 +> 结论 4:仅仅用死锁检测器在运行时检测 Go 阻塞 bug 不是很有效。未来的研究应侧重于构建新颖的阻塞 bug 检测技术,例如,结合使用静态和动态阻塞模式的检测。 -基于以上这些发现,项目负责人 Ziheng Liu 带领研究员们开始研究新的代码分析器。以 [SSA](https://godoc.org/golang.org/x/tools/go/ssa) 软件包为基础,该新式代码分析器通过在 GraphQL Go 项目中发现了 [bug](https://github.com/graphql-go/graphql/pull/434) 证明了它的有效性。这个新式代码分析器预计于2020年1月发布。 +基于以上这些发现,项目负责人 Ziheng Liu 带领研究员们开始研究新的代码分析器。以 [SSA](https://godoc.org/golang.org/x/tools/go/ssa) 软件包为基础,该新式代码分析器通过在 GraphQL Go 项目中发现了 [bug](https://github.com/graphql-go/graphql/pull/434) 证明了它的有效性。这个新式代码分析器预计于 2020 年 1 月发布。 有关该研究的更多信息,请访问该研究文献 [Github](https://songlh.github.io/paper/go-study.pdf) 地址。 diff --git a/published/tech/20190907-Go-build-a-minimal-docker-image-in-just-three-steps.md b/published/tech/20190907-Go-build-a-minimal-docker-image-in-just-three-steps.md new file mode 100644 index 000000000..a71406f03 --- /dev/null +++ b/published/tech/20190907-Go-build-a-minimal-docker-image-in-just-three-steps.md @@ -0,0 +1,114 @@ +首发于:https://studygolang.com/articles/24875 + +# 只用 3 步构建 Go docker 最小镜像 + +![DockerGopher](https://raw.githubusercontent.com/studygolang/gctt-images/master/build-mini-docker-image/DockerGopher.png) + +## Go——仅需三个步骤即可构建最小的 Docker 映像 + +当您为 docker 构建 Go 应用程序时,通常从诸如 `golang:1.13` 之类的映像开始。但将这个映像实际运行时会浪费资源。让我们看一下如何将 Go 应用程序构建为绝对最小的 Docker 映像。 + +## 1. 选择 Go 版本 + +尽管使用 `golang:latest` 或者 仅使用 `golang` 的版本镜像很诱人,但由于各种问题,这样做都不太好,其中主要的一个问题是这样做构建(可能)不具有重复性。无论是开发测试需要部署产品时使用的相同版本(的镜像),还是你发现自己需要修补旧版本(镜像)的应用程序,最好将 Go 发行版镜像的版本固定,只有当你知道你需要更新 Go 版本的时候你再去更新它。 + +因此,需要一直使用完整的说明,包含补丁版本号,而且最好说明镜像的基本系统,比如:`1.13.0-alpine3.10`。 + +## 2. 保持最小 + +这个*最小*包含两个方面: + +1. 最短构建时间 +2. 最小产出镜像 + +### 快速构建 + +Docker 为您缓存中间层,因此如果您正确地构造 Dockerfile,您可以减少每次(更改后)后续重建所需的时间。根据经验来说,根据命令的源(例如:`COPY` 源)更改的频率对命令进行排序。 + +另外,请考虑使用 `.dockerignore` 文件,该文件有助于保持构建上下文较小——当您执行 `docker build` 时,docker 需要将当前目录中的所有内容都提供给构建 docker 守护进程(即在 docker 构建开始时向 docker 守护进程发送构建上下文)。简单来说,如果你的代码仓库包含了很多构建你的程序所不需要的数据(比如测试,markdown 格式文档生成等),`.dockerignore` 将有助于加快构建速度。 + +至少,您可以从下边的示例内容开始尝试。如果你 `COPY . .`,Dockerfile 就会进入上下文中(不应该这样做),当你只修改 Dockerfile 时,不需要执行并使所有的东西无效。 + +```shell +.git +Dockerfile +testdata +``` + +### 最小镜像 + +最简单的手段是使用 `scratch`(构建基础镜像),没有其他手段能与之相比。Scratch 是特殊的 `base` (基础)镜像,它不是一个真正的镜像,而是一个完全空的系统。注意:在老版本 docker 中,显式的 scratch 镜像作为一个真正的镜像层,`docker 1.5` 之后的版本就不再是这样。 + +你的 Dockerfile 只需两步: + +1. 基于一个镜像(比如 `builder` 镜像,你想叫什么都行,编译你的应用程序; +2. 然后将编译产出的二进制程序和所有其他依赖拷贝到基于 scratch 的镜像中; + +这样做贼管用! + +## 3. 放在一起 + +看看完整的 Dockerfile 长啥样: + +```dockerfile +FROM golang:1.13.0-stretch AS builder + +ENV GO111MODULE=on \ + CGO_ENABLED=1 + +WORKDIR /build + +# 缓存 mod 检索-那些不常更改的模块 +COPY go.mod . +COPY go.sum . +RUN Go mod download + +# 复制构建应用程序所需的代码 +# 可能需要更改下边的命令,只复制您实际需要的内容。 +COPY . . + +# 构建应用程序 +RUN Go build ./cmd/my-awesome-go-program + +# 我们创建一个 /dist 目录, 仅包含运行时必须的文件 +# 然后,他会被复制到输出镜像的 / (根目录) +WORKDIR /dist +RUN cp /build/my-awesome-go-program ./my-awesome-go-program + +# 可选项:如果您的应用程序使用动态链接(通常情况下使用 CGO), +# 这将收集相关库,以便稍后将它们复制到最终镜像 +# 注意: 确保您遵守您复制和分发的库的许可条款 +RUN ldd my-awesome-go-program | tr -s '[:blank:]' '\n' | grep '^/' | \ + xargs -I % sh -c 'mkdir -p $(dirname ./%); cp % ./%;' +RUN mkdir -p lib64 && cp /lib64/ld-linux-x86-64.so.2 lib64/ + +# 在运行时复制或创建您的应用程序需要的其他目录/文件。 +# 例如,本例使用 /data 作为工作目录,在正常运行容器时,该目录可能绑定到永久目录 +RUN mkdir /data + +# 构建最小运行时镜像 +FROM scratch + +COPY --chown=0:0 --from=builder /dist / + +# 设置应用程序以 /data 文件夹中的非 root 用户身份运 +# User ID 65534 通常是 'nobody' 用户. +# 映像的执行者仍应在安装过程中指定一个用户。 +COPY --chown=65534:0 --from=builder /data /data +USER 65534 +WORKDIR /data + +ENTRYPOINT ["/my-awesome-go-program"] +``` + +如果您觉得这些功能有用,或者想要分享一些自己的方法或技巧,请在下边发表评论。 + +--- + +via: https://dev.to/ivan/go-build-a-minimal-docker-image-in-just-three-steps-514i + +作者:[Ivan Dlugos](https://github.com/vaind) +译者:[TomatoAres](https://github.com/TomatoAres) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190909-go-instrumentation-in-go.md b/published/tech/20190909-go-instrumentation-in-go.md new file mode 100644 index 000000000..ac3edfed7 --- /dev/null +++ b/published/tech/20190909-go-instrumentation-in-go.md @@ -0,0 +1,117 @@ +首发于:https://studygolang.com/articles/27143 + +# Go 中的性能测量 + +Vincent Blanchon + +2019 年 9 月 19 日 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/intrumentation-in-go/1.png) + +“A Journey With Go” 专属插图,由 Renee French 根据原始 Go Gopher 制作。 + +ℹ️ *本文基于 Go 1.13.* + +`go test` 命令提供了许多出色的功能,比如代码覆盖率,CPU 和 内存分析。要提供这些统计信息,Go 就需要一种方式来跟踪 CPU 使用率,或在代码覆盖中跟踪一个函数何时被用到。 + +## 性能测量 + +Go 使用多种方式来产生这些统计信息: + +- 动态插入性能测量语句,使其可以跟踪到代码何时进入一个函数或条件。这个策略在[代码覆盖率](https://golang.org/doc/go1.2#cover)中使用。 +- 每秒记录多次程序样本。这个策略在[CPU 分析](https://blog.golang.org/profiling-go-programs)中用到。 +- 在代码中使用静态 hook,以便在执行期间调用所需函数。这个策略在内存分析中用到。 + +我们来写一个简单的程序并回顾所有内容。这是我们在后面的章节将使用的代码: + +```go +package main + +import "math/rand" + +func main() { + println(run()) +} + +//go:noinline +func run() int { + a := 0 + for i:= 0; i < rand.Intn(100000); i++ { + if i % 2 == 0 { + add(&a) + } else { + sub(&a) + } + } + + return a +} + +//go:noinline +func add(a *int) { + *a += rand.Intn(10) +} + +//go:noinline +func sub(a *int) { + *a -= rand.Intn(10) +} +``` + +> main.go 托管在 [GitHub] (https://github.com/) [查看](https://gist.github.com/blanchonvincent/d4ed01d31b3ed99eb5cd87629ecfe926/raw/1fbac76f932d020a2b172b2385fb1cda69b83b1e/main.go) + +## 代码覆盖率 + +通过 `GOSSAFUNC=run Go test -cover` 命令生成的 SSA 代码,我们可以查看 Go 对程序进行了什么样的修改: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/intrumentation-in-go/2.png) + +变量 `GoCover_0_313837343662366134383538` 是一个标志数组,其中每个键是一个代码块,当代码实际进入这一块时对应的标志设置为 1. + +*你可以在我的文章 [“Go: Compiler Phases”](https://medium.com/@blanchon.vincent/go-compiler-phases-4e5a153ca889) 中找到更多关于 SSA 的信息。* + +生成的代码将稍后在管理代码覆盖率报告的函数中使用。 我们可以通过使用 `objdump` 命令反汇编代码覆盖期间生成的目标文件来进行验证。 运行 `go test -cover -o main.o && Go tool objdump main.go` 将反汇编代码并显示缺少的部分。 它首先初始化并在自动生成的 init 函数中注册 coverage: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/intrumentation-in-go/3.png) +test.go 添加的 init 方法 + +然后,如前所述,测试将在执行期间收集覆盖率数据并且会触发一个方法来实际写入和显示覆盖率: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/intrumentation-in-go/4.png) +go test 调用的 after 函数 + +## CPU 分析 + +跟踪 CPU 使用率的策略则有所不同。Go 会停止程序并收集正在运行程序的样本。这里是未开启 CPU 分析的代码的 trace: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/intrumentation-in-go/5.png) + +这里是相同代码开启了 CPU 分析的 trace: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/intrumentation-in-go/6.png) + +增加的 trace 与 `pprof` 及性能分析相关。这里是其中一个的放大图: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/intrumentation-in-go/7.png) + +`profileWriter` 方法将循环调用,每 100 毫秒收集 CPU 数据,以在性能分析结束时最终生成报告。 + +## 内存分析 + +内存分析包含在源码中,并已集成在内存分配系统中。在使用 `-memprofile` [开启内存分析](https://github.com/golang/go/blob/release-branch.go1.13/src/cmd/compile/internal/gc/util.go#L55-L77)的情况下,位于 [malloc.go](https://github.com/golang/go/blob/release-branch.go1.13/src/runtime/malloc.go#L877) 中的内存分配器,将[对已分配的内存进行分析](https://github.com/golang/go/blob/release-branch.go1.13/src/runtime/malloc.go#L1097-L1105)。这里,依然可以通过反汇编代码进行验证。这里是内存分配器的使用: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/intrumentation-in-go/8.png) + +开启了内存分配分析 + +*你可以在我的文章 “[Go: Unknown Parts of the Test Package](https://medium.com/a-journey-with-go/go-unknown-parts-of-the-test-package-df8988b2ef7f)” 中找到更多关于 test 包的信息.* + +--- + +via: https://medium.com/a-journey-with-go/go-instrumentation-in-go-e845cdae0c51 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[krystollia](https://github.com/krystollia) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190920-Create-your-developer-portfolio-using-Hugo-and-Github-Pages.md b/published/tech/20190920-Create-your-developer-portfolio-using-Hugo-and-Github-Pages.md new file mode 100644 index 000000000..2277100e7 --- /dev/null +++ b/published/tech/20190920-Create-your-developer-portfolio-using-Hugo-and-Github-Pages.md @@ -0,0 +1,206 @@ +首发于:https://studygolang.com/articles/25281 + +# 使用 Hugo 和 Github Pages 创建你的开发者作品集 + +拥有一个作品集网站可以使你在寻找一个开发外包时脱颖而出。作品集网站可以让潜在的客户或雇主了解你是一个专家,了解你过去和正在做的工作。不幸的是,一些常见的困难阻碍了许多人拥有作品集网站,包括最近的我--害怕所有的工作,计划并且从一个草图中构建一个网站,选择一个主机提供商,如果你想域名可用,那些主机和域名会让你破费(特别当你缺钱的时候)等等。 + +在本指导中,我会带领你快速且免费的建立并且上线你的工作集网站。 + +## 但是,首先 ... 有一个作品集网站真的那么重要吗? + +当然。以下是原因: + +1. 它列出了你作为一个开发者的技术技能。你不仅可以列出你在职业生涯中磨练出的技能,还能连接到一切能够具体表现他们的东西。这些东西可以包括你的项目,你参加过的黑客马拉松,你参加过的面试,你发表过的文章,你做过的教程、演讲、开源工作等。 +2. 它支持你建立关系网。[关系网就是和兴趣相投的人建立联系](https://podcasts.google.com/?feed=aHR0cHM6Ly9mZWVkcy5mZWVkYnVybmVyLmNvbS9Xb3JrbGlmZVdpdGhBZGFtR3JhbnQ%3D&episode=cHJ4XzEzMV9iMjNiZDRlYi04YWYyLTQ3ZmMtYTYwZi1iOGMwMWVjMTBmYjg%3D),而且,你的作品集网站可以给 SWE 空间中的其他人展示你的兴趣。 +3. 这是一种吸引潜在客户和雇主的方式。 +4. 它帮助你建立你的品牌。它允许你来定义那些看到你作品集的陌生人如何看待你。 +5. 它允许你来展现你的风格。 +6. 作为开发者,它是你在网络上其他部分的门户。你能把人们指向到你的 Dribble, Github, LinkedIn, Codepen, Dev.to, Medium, Youtube, Twitch 和你的其它的开发者账户。 + +## 前置条件 + +1. Github 账户:如果你还没有账户的话,通过 [这个链接](https://github.com/join) 创建一个。 +2. 安装好的 Hugo:[macOS](https://gohugo.io/getting-started/installing#macos),[Windows](https://gohugo.io/getting-started/installing#windows) 和 [Linux](https://gohugo.io/getting-started/installing#linux) 的安装说明。 + +## 第一步:生成你的网站 + +1. 打开你的终端然后使用 `cd` 命令进入一个你用来安装网站的目录。 + +2. 为你的网站的文件夹起一个名字。我们使用 `` 来作为占位符。 + +3. 就像下面这样生成你的站点。 + + ```shell + hugo new site + ``` + +4. 使用 `cd` 命令进入刚新生成的文件夹并且初始化为一个 Git 仓库。 + + ```shell + cd && Git init + ``` + +## 第二步:选择并添加一个主题 + +1. 前往 [Hugo 的作品集主题页](https://themes.gohugo.io/tags/portfolio/) 然后选择一个你喜欢的主题。在本篇教程中,我选择了一个叫做 [UILite](https://themes.gohugo.io/tags/portfolio/) 的主题。它简洁,看起来相当酷而且满足一个作品集的基本需求。当然那儿也有很多其他很酷的主题可供选择。 + UILite 主题看起来像这样。 + ![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/create-your-developer-portfolio-using-hugo-and-github-pages-35en/pic1.png) + +2. 把这个主题作为一个 Git 子模块添加到 ``。这步因主题不同而异,但差不多都鼓励作为一个子模块来使用主题。你可以通过下面的命令添加主题: + + ```shell + Git submodule add themes/ + ``` + + 在我们的例子中需要这样: + + ```shell + Git submodule add https://github.com/uicardiodev/hugo-uilite themes/hugo-uilite + ``` + +3. 在已经自动生成的 **config.toml** 文件中指明你将在你的作品集网站中使用的 ``。这个 **config.toml** 文件允许你对于你的整个站点进行详细设定。 + + ```shell + Echo 'theme = ""' >> config.toml + ``` + + 对于我们的主题,我们这样写: + + ```shell + Echo 'theme = "hugo-uilite"' >> config,toml + ``` + + 在这一步的最后,你的 **config.toml** 文件应该看起来像这个样子: + + ```shell + baseURL = "http://example.org/" + languageCode = "en-us” + title = "My New Hugo Site” + theme = "hugo-uilite” + ``` + + 改变作品集网站的 `title` 和 `baseURL` 会是个很好的想法。 + +## 第三步:测试你的网站 + +1. 打开 Hugo 服务器。 + + ```shell + hugo server + ``` + +2. 在浏览器中打开 [http://localhost:1313](http://localhost:1313)。你的站点现在应该正在工作并且看起来应该像这样: + ![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/create-your-developer-portfolio-using-hugo-and-github-pages-35en/pic2.png) + 它看起来像是有点儿缺陷,但它是正常的。只是我们还没有在网站中添加内容。这是我们接下来将会做的事情。 + +## 第四步:调整你的主题 + +这步是因人而异的,并且取决于你的主题,你想添加的内容和你的设计感。一些常见的作品集包括突出显示的姓名,简介,开发者社交简历的链接,技能,项目,工作经验,成就等等。 + +以下是调整 **hugo-uilite** 主题的一种方式: + +1. 首先把主题恢复到它原来的样子。对于 **hugo-uilite** 主题,我们可以通过从它的 *exampleSite* 文件夹中复制文件来实现。如果刚开始你的主题看起来坏掉了,首先从主题的 README 中寻求解决方法。如果就像我们这个例子一样什么都没有说明,那么寻找你的主题仓库中的 *exampleSite* 文件夹。通过从 *exampleSite* 的 data 文件夹中复制缺失的文件到你的作品集网站的 data 文件夹中,来修复你的站点。 + + ```shell + mkdir data + cp themes/hugo-uilite/exampleSite/data/* data/ + ``` + + 以下展示了在我们的 data 文件夹中的文件层级: + + ```shell + data + ├── config.json + ├── experience.json + ├── services.json + ├── sidebar.json + ├── skills.json + └── social.json + ``` + + Hugo 服务器发现了这些更改然后重新载入,现在我们的网站看就来就像 [主题示例页](https://themes.gohugo.io/theme/hugo-uilite/) 显示的那样了。 + +2. 下一步,我们将会在网站上添加专业信息。就像上面提到的那样,你应该在网站上添加一些细节。也能做一些对主题样式的改变。以下,我会描述我做了哪些工作来更改主题,使得网站看起来像下面的网站截图那样。变化很大,因为并不是所有的主题都是一样的所以没有详细描述。以下是一些总结: + **a. 更改主题的色调** + + * 我在 `static` 文件夹中添加了一个额外的 CSS 文件。 + * 任何你在此处添加的样式都会覆盖主题本身的样式。 + * 主题的 `layout` 文件夹是查找你想修改样式的组件的 id 和类的好地方。 + + **b. 添加专业信息** + + * 因为我们已经复制了 `data` 文件夹,所以我们需要做的就是修改对应文件中信息,使其能够反映在站点上。举例来说,我们可以通过修改 `experience.json` 文件来修改 `Experience` 。 + + **c. 改变站点图标** + + * 如果一个主题没有提供能够改变站点图标的配置设置项,那需要将其添加到站点的头部中。 + * 在本文示例中,头文件存放在主题的 `layouts/partials` 文件夹中。 + * 为了增加图标,复制头文件到作品集的 `layout/partials` 文件夹中并且修改必要项。 + * 另外,如果图标不是一个链接而是文件的话,记得把这个图标文件添加到 `static` 文件夹中。 + + **d. 增加项目栏** + + * 这个主题没有提供**项目栏**。 + * 因为项目栏与 **服务栏** 是相似的,所以我复制它的 HTML 文件( `layouts/partials/project.html` ),修改它使得项目栏可以被链接,为它增加一个数据文件( `data/projects.json` )并且把它添加到 `layout/index.html` 文件中(我从主题中复制过来的)。 + * 在这个示例中,你没有提供服务,所有你所做的事情只是更改了 `data/services.json` 文件中的内容来反映到 **项目** 或其他地方,而不是 **服务**。 + + **e. 增加社交信息** + + * 已经提供了 `data/socials.json` 和 `data/socialsfas.json` (用于 Awesome-Font 实体风格图标)文件可以用来列举社交信息。 + + 最后站点看起来像这样: + + ![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/create-your-developer-portfolio-using-hugo-and-github-pages-35en/pic3.png) + +## 第五步:在 Github 上创建仓库并把源码推上去 + +1. 向主分支提交更改。 + +2. 在 Github 上创建一个仓库来存放你的站点的源码,举例来说叫做 ``。你可以把这个仓库设置为私有的,从而它只用来存放你的站点的源码。 + +3. 把你最近的更改推送到这个仓库中。 + +4. 创建第二个仓库来部署你的站点,它的名字应该按照 `.github.io` 这种格式。 + +5. 在你本地计算机中,使用 `cd` 命令回到 `` 文件夹,并且通过运行 Hugo 服务器来检查你是否满意站点的样子。如果你很满意作品集的最后样子,关掉 Hugo 服务器。 + +6. 然后我们需要把本地编辑站点生成的 `public` 文件夹与我们创建的 `.github.io` 仓库连接起来。我们会使 `.github.io` 仓库作为 public 文件夹的远程源,并且使 `public` 文件夹作为我们 `` 项目的一个子模块。运行下面这个命令: + + ```shell + Git submodule add -b master git@github.com:/.github.io.git public + ``` + +7. 我们可能会经常修改我们的站点,那么就需要能够在更改后能够很容易的进行我们作品集的部署。 Hugo 提供了一个能够把你的更改推送到源(带有可选的提交信息)并且部署作品集网站的脚本。它可以被添加到你的 `` 项目中并且当你做出更改后只要简单的运行一下就可以了。脚本叫做 **deploy.sh** ,在 [这里](https://gohugo.io/hosting-and-deployment/hosting-on-github/#put-it-into-a-script) 。如果你已经复制好了这个脚本,在第一次部署的时候你需要做的事情就是: + + ```shell + # 给脚本执行权限 + chmod +x deploy.sh + # 部署你的作品集,带有一个可选的信息 + ./deploy.sh "Deploy the first version of my portfolio site” + ``` + +## 第六步:在 Github Pages 上面部署你的网站 + +如果你对 Github 用户页面不熟悉或者不感兴趣,你可以读一下 [这些](https://help.github.com/en/articles/user-organization-and-project-pages#user--organization-pages) 。 + +我们在 **用户页面** 中部署的作品集可以通过 `.github.io` URL 进行访问。我们直接从第五步创建的 `.github.io` 仓库是私有的,通过 **Settings > Options > Danger Zone** 把它改回公开。 + ![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/create-your-developer-portfolio-using-hugo-and-github-pages-35en/pic4.png) +2. 然后,我们需要设置 GIthub 用户页面的来源。如果你的仓库是以 `.github.io` 进行命名的,Github 页面自动能够使用,默认是公开仓库,如果你是升级用户,私有仓库也是可以的。在 **Settings > Options > Github Pages** 中进行设置。我们将会选择 `master` 分支作为页面的来源。![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/create-your-developer-portfolio-using-hugo-and-github-pages-35en/pic5.png) +3. 打开你仓库页面的 **Code** 标签页下的 **Environments** 标签页,确保部署已经成功。在这个标签页下面,你会看到你过去部署的日志,最上面高亮的就是你最近的部署。![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/create-your-developer-portfolio-using-hugo-and-github-pages-35en/pic6.png) +4. 最后,在一个新浏览器标签页输入 `.github.io` ,就可以看到你的作品集了。 + +## 结语 + +我希望本篇教程是有用的。尽管在 Github 页面上得到一个静态作品集是很容易的,但我更喜欢 Hugo 的方式,因为它的可定制性,题材广泛的主题和模板内建的东西,如谷歌分析,DIsqus 等等。在此,我也很感激 Github 的用户页面免费静态站点。 + +--- + +via: https://dev.to/zaracooper/create-your-developer-portfolio-using-hugo-and-github-pages-35en + +作者:[Zara Cooper](http://github.com/zaracooper) +译者:[Ollyder](https://github.com/Ollyder) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190920-Rate-Limiting-HTTP-Request-in-Go-based-on-IP-address.md b/published/tech/20190920-Rate-Limiting-HTTP-Request-in-Go-based-on-IP-address.md new file mode 100644 index 000000000..f843a139a --- /dev/null +++ b/published/tech/20190920-Rate-Limiting-HTTP-Request-in-Go-based-on-IP-address.md @@ -0,0 +1,188 @@ +首发于:https://studygolang.com/articles/25121 + +# Go 中基于 IP 地址的 HTTP 限流 + +如果你想限制一个正在运行的 HTTP 服务的请求量,你可以使用现有的轮子工具,比如说 [github.com/didip/tollbooth](https://github.com/didip/tollbooth) ,但是如果写一些简单的东西,你自己去实现也没有那么难。 + +我们可以用这个包 `x/time/rate` 。 + +在这篇教程中,我们将基于用户的 IP 地址构造一个简单的限流中间件。 + +## Pure HTTP Server + +我们来开始构建一个简单的 HTTP 服务,这是一个大流量的服务,这也是我们为什么要在这里加上限制的原因。 + +```go +package main + +import ( + "log" + "net/http" +) + +func main() { + mux := http.NewServeMux() + mux.HandleFunc("/", okHandler) + + if err := http.ListenAndServe(":8888", mux); err != nil { + log.Fatalf("unable to start server: %s", err.Error()) + } +} + +func okHandler(w http.ResponseWriter, r *http.Request) { + // Some very expensive database call + w.Write([]byte("alles gut")) +} +``` + +在 `main.go` 文件中,我们用 `:8888` 端口启动了一个仅有单一控制器的服务。 + +## golang.org/x/time/rate + +我们将用 `x/time/rate` 这个包,它提供了一个令牌桶限流算法。 [rate#Limiter](https://godoc.org/golang.org/x/time/rate#Limiter) 控制事件允许发生的频率,它实现了一个容量为 b 的“令牌桶”,最初是满的并以每秒 r 个令牌的速度重新填充。在足够的时间间隔里,限流器限制速度为每秒 r 个令牌,最多为桶的最大容量 b 。 + +因为我们想根据 IP 地址来限流,我们需要维护一个限流器字典。 + +```go +package main + +import ( + "sync" + + "golang.org/x/time/rate" +) + +// IPRateLimiter . +type IPRateLimiter struct { + ips map[string]*rate.Limiter + mu *sync.RWMutex + r rate.Limit + b int +} + +// NewIPRateLimiter . +func NewIPRateLimiter(r rate.Limit, b int) *IPRateLimiter { + i := &IPRateLimiter{ + ips: make(map[string]*rate.Limiter), + mu: &sync.RWMutex{}, + r: r, + b: b, + } + + return i +} + +// AddIP creates a new rate limiter and adds it to the ips map, +// using the IP address as the key +func (i *IPRateLimiter) AddIP(ip string) *rate.Limiter { + i.mu.Lock() + defer i.mu.Unlock() + + limiter := rate.NewLimiter(i.r, i.b) + + i.ips[ip] = limiter + + return limiter +} + +// GetLimiter returns the rate limiter for the provided IP address if it exists. +// Otherwise calls AddIP to add IP address to the map +func (i *IPRateLimiter) GetLimiter(ip string) *rate.Limiter { + i.mu.Lock() + limiter, exists := i.ips[ip] + + if !exists { + i.mu.Unlock() + return i.AddIP(ip) + } + + i.mu.Unlock() + + return limiter +} +``` + +`NewIPRateLimiter` 创建了一个 IP 限流器的实例,HTTP 服务将调用这个实例的 `GetLimiter` 来获取指定的 IP 限流器(从字典里获取或者构造一个新的)。 + +## Middleware + +让我们来升级我们的 HTTP Server 并在所有的控制器中添加中间件。所以如果 IP 达到限制将返回 429 表示大量请求,否则,它将继续执行请求。 + +在 `limitMiddleware` 方法中,每一次中间件接受一个请求,我们都会调用全局限制器的 `Allow()` 方法。如果桶里没有令牌了,`Allow()` 方法将返回 false 并且我们返回给用户 429 。否则,调用 `Allow()` 将从桶里消耗一个令牌并且我们将控制权传递给下一个处理程序。 + +```go +package main + +import ( + "log" + "net/http" +) + +var limiter = NewIPRateLimiter(1, 5) + +func main() { + mux := http.NewServeMux() + mux.HandleFunc("/", okHandler) + + if err := http.ListenAndServe(":8888", limitMiddleware(mux)); err != nil { + log.Fatalf("unable to start server: %s", err.Error()) + } +} + +func limitMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + limiter := limiter.GetLimiter(r.RemoteAddr) + if !limiter.Allow() { + http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests) + return + } + + next.ServeHTTP(w, r) + }) +} + +func okHandler(w http.ResponseWriter, r *http.Request) { + // Some very expensive database call + w.Write([]byte("alles gut")) +} +``` + +## Build & Run + +```go +go get golang.org/x/time/rate +go build -o server . +./server +``` + +## Test + +[vegeta](https://github.com/tsenart/vegeta)(它是用 Go 写的)是一个很不错的工具,我喜欢用它来做 HTTP 负荷测试。 + +``` +brew install vegeta +``` + +我们需要创建一个简单的配置文件说明我们想测试什么请求。 + +``` +GET http://localhost:8888/ +``` + +运行 10 秒钟,每个单位时间发 100 个请求 。 + +```go +vegeta attack -duration=10s -rate=100 -targets=vegeta.conf | vegeta report +``` + +结果你将会看到一些请求返回 200 ,但是大部分返回 429 。 + +--- + +via: https://dev.to/plutov/rate-limiting-http-requests-in-go-based-on-ip-address-542g + +作者:[Alex Pliutau](https://dev.to/plutov) +译者:[Alihanniba](https://github.com/Alihanniba) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190922-building-a-fast-modern-web-crawler.md b/published/tech/20190922-building-a-fast-modern-web-crawler.md new file mode 100644 index 000000000..7c8e05fa6 --- /dev/null +++ b/published/tech/20190922-building-a-fast-modern-web-crawler.md @@ -0,0 +1,92 @@ +首发于:https://studygolang.com/articles/25282 + +# 构建一个快速的现代化网络爬虫 + +很久以来,我一直对网络爬虫充满热情,特别是它背后的理论。我曾经使用过许多语言来构建它,例如:C++、JavaScript(Node.JS)、Python 等。 + +但是首先,什么是网络爬虫? + +## 什么是网络爬虫? + +网络爬虫是一个计算机程序,它通过浏览互联网来将现有的网页、图像、PDF 等编入索引,并允许用户使用[搜索引擎](https://en.wikipedia.org/wiki/Web_search_engine)来检索这些内容。 这基本上就是著名的[谷歌搜索引擎](https://google.com/)背后的技术了。 + +通常,一个高效的网络爬虫被设计成分布式的,即并非一个运行在专用服务器上的独立程序,而是运行在多个服务器上(例如:在云上)的一些程序的多个实例。这样的设计使得爬取任务能够得到更合适的重新分割,从而达到提高性能、增加带宽的效果。 + +但是,分布式软件并非没有缺点,有一些因素可能会给程序增加额外的延迟,从而引起性能上的降低,例如:网络延迟、同步问题、设计不良的通讯协议等。 + +为了提高效率,分布式网络爬虫必须得到精心的设计,尽可能的消除一切瓶颈,正如法国海军上将 Olivier Lajous 曾经说过的: + +> 最薄弱的链接决定了整个链条的强度。 + +## Trandoshan: 一个暗网爬虫 + +您也许知道,已经有一些比较成功的爬虫正在网络上运行,例如 google bot,所以这次我不打算再做一个类似的,而是要做一个专门用于暗网的网络爬虫。 + +## 什么是暗网? + +在这里我将不再从技术的角度来阐述什么是暗网,因为有专门的文章来描述它。 + +互联网是由三层组成的,我们可以把它想象成一个冰山,如下图所示: + +* 表层网或者透明网是我们每天浏览的那部分网络,它被 Google,Qwant,Duckduckgo 等流行的网络爬虫编入索引。 +* 深网是未经索引的网络的一部分,这意味着您无法通过搜索引擎来找到其中的网站,但是却可以通过 URL 或者 IP 地址来直接访问它们。 +* 暗网是未经索引的网络的另一部分,访问它需要借助特殊的应用程序或代理,使用常规的浏览器是办不到的。例如:建立在 Tor 网络上的最著名的暗网,它需要通过以 .onion 结尾的特殊 URL 来访问。 + +![Existing Web layers](https://raw.githubusercontent.com/studygolang/gctt-images/master/modern-web-crawler/image-1.png) + +## Trandoshan 的设计是怎样的? + +![Big picture of Trandoshan](https://raw.githubusercontent.com/studygolang/gctt-images/master/modern-web-crawler/Trandoshan-1.png) + +上图显示了 Trandoshan 的大致架构,在探讨它的每个进程的职责之前,我们需要先了解一下它们之间是如何通信的。 + +Trandoshan 的进程间通信(IPC)主要是依靠 [NATS](https://nats.io/) 的消息传递协议(上图中的黄线),并基于生产者 / 消费者模型来实现的。 NATS 中的每个消息都有一个主题(类似于电子邮件),该主题允许其他进程对消息进行过滤,以便只读取它们感兴趣的消息。NATS 是可伸缩的:例如,可以同时有 10 个爬虫进程从消息服务器中读取 URL,每个进程都将得到一个唯一的需要爬取的 URL。 这使得爬虫进程可以并发,即许多爬虫实例可以同时运行而不会产生任何错误,进而提高性能。 + +Trandoshan 被拆分为 4 个主要进程: + +* **爬虫**:爬虫进程负责爬取页面,它从 NATS 中读取 URL(由主题 **"todoUrls"** 标识的消息),然后爬取相应的页面,并提取所有包含在页面内的 URL。这些被提取到的 URL 将被以 **"crawledUrls"** 为主题发送给 NATS,相应的,页面正文(整个内容)则会被以 **"content"** 为主题发给 NATS。 +* **调度器**:调度器负责对 URL 进行审核,它从 NATS 中读取主题为 **"crawledUrls"** 的消息,然后根据该消息中的 URL 是否已经被爬取过做出要不要爬取的决定,如果需要爬取,则将 URL 以 **"todoUrls"** 为主题发送给 NATS。 +* **持久器**:持久器负责将页面内容归档,它读取页面的内容(由主题 **"content"** 标识的消息)并将其存储到 NoSQL 数据库(MongoDB)中。 +* **API**:API 负责收集信息以供其它进程使用。例如,**调度器**通过它来确定某个页面是否已经被爬取过。调度程序并不通过与数据库的直接交互来确定相应的 URL 是否被爬取过(直接交互将增加调度器和数据库之间的耦合度),相反它与 API 进程进行交互,这使得数据库和进程之间有了一层抽象。 + +Go 是经过精心设计的专门用于构建高性能分布式系统的语言。以上这些不同的进程都是使用 Go 语言编写的,因为它能将性能提高很多(由于程序被编译成了二进制文件),除此之外, Go 语言也有很多的库可以使用。 + +Trandoshan 的源代码可在 GitHub 上找到:[https://github.com/trandoshan-io](https://github.com/trandoshan-io)。 + +## 怎样运行 Trandoshan? + +正如之前所述,Trandoshan 被设计为运行在分布式系统之上的网络爬虫,它可以通过 docker 镜像来获取,这使得它非常适合云环境。[https://github.com/trandoshan-io/k8s](https://github.com/trandoshan-io/k8s)仓库中包含了在 Kubernetes 集群上部署 Trandoshan 生产实例所需的所有配置文件,对应的容器镜像则位于 [docker hub](https://hub.docker.com/u/trandoshanio) 上。 + +如果您正确配置了 kubectl,那么就可以通过如下简单的命令来部署 Trandoshan: + +```bash +./bootstrap.sh +``` + +除此之外,您也可以通过 docker 和 docker-compose 在本地运行 Trandoshan。 通过仓库 [trandoshan-parent](https://github.com/trandoshan-io/trandoshan-parent) 中的 docker-compose 文件和一个 shell 脚本,使用以下命令便可运行它: + +```bash +./deploy.sh +``` + +## 怎样使用 Trandoshan? + +目前,有一个小巧的 Angular 应用程序可以用于查询被编入索引的内容,这个程序通过与 **API** 进程的交互来完成对数据库中内容的检索。 + +![Screenshot of the dashboard](https://raw.githubusercontent.com/studygolang/gctt-images/master/modern-web-crawler/Screenshot-from-2019-09-22-17-09-49.png) + +## 总结 + +这次就先到这里,尽管 Trandoshan 已经可以被用于生产环境,但是仍有许多优化工作和新功能需要完成。由于它是一个开源项目,所以每个人都可以通过对相应项目发起 PR 来做出贡献。 + +Happy hacking! + +--- + +via: https://creekorful.me/building-fast-modern-web-crawler/ + +作者:[Aloïs Micard](https://creekorful.me/author/creekorful/) +译者:[Anxk](https://github.com/Anxk) +校对:[lxbwolf](https://github.com/lxbwolf) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20190926-publishing-go-modules.md b/published/tech/20190926-publishing-go-modules.md new file mode 100644 index 000000000..dc9ec767d --- /dev/null +++ b/published/tech/20190926-publishing-go-modules.md @@ -0,0 +1,196 @@ +首发于:https://studygolang.com/articles/25129 + +# 发布 Go Modules + +## 简介 + +本文是 Go modules 系统的第三部分 + +- Part 1: [使用 Go Modules](https://blog.golang.org/using-go-modules)  [译文](https://studygolang.com/articles/19334) +- Part 2: [迁移到 Go Modules](https://blog.golang.org/migrating-to-go-modules)  [译文](https://studygolang.com/articles/23133) +- Part 3: 发布 `go modules` (本文) +- Part 4: [Go Modules: v2 及以后的版本](https://blog.golang.org/v2-go-modules) + +本文讨论如何编码和发布 Go 模块,发布后就可以被其他模块依赖使用了。 + +注意: 本文只涉及到 v1 及以前的版本, 如果你想了解 v2 版本, 请参照 [Go Modules: v2 及以后的版本](https://blog.golang.org/v2-go-modules) 。 + +本文中列出的例子使用的是 Git ,其他版本控制工具如 `Mercurial`、`Bazaar` 等等也支持。 + +## 工程配置 + +本文的前期准备:有一个已经建好的工程。让我们以 [使用 `Go Modules`](https://blog.golang.org/using-go-modules) 篇尾的文章来开始示例。 + +```bash +$ cat go.mod +module example.com/hello + +go 1.12 + +require rsc.io/quote/v3 v3.1.0 +``` + +```bash +$ cat go.sum +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +``` + +```go +$ cat hello.go +package hello + +import "rsc.io/quote/v3" + +func Hello() string { + return quote.HelloV3() +} + +func Proverb() string { + return quote.Concurrency() +} +``` + +```go +$ cat hello_test.go +package hello + +import ( + "testing" +) + +func TestHello(t *testing.T) { + want := "Hello, world." + if got := Hello(); got != want { + t.Errorf("Hello() = %q, want %q", got, want) + } +} + +func TestProverb(t *testing.T) { + want := "Concurrency is not parallelism." + if got := Proverb(); got != want { + t.Errorf("Proverb() = %q, want %q", got, want) + } +} + +$ +``` + +创建一个 `git` 仓库, 添加一条初始化的信息。 如果你是要发布你自己的工程,请确保你的工程里包含 `LICENSE` (许可)文件。进入到包含 `go.mod` 的目录, 创建仓库。 + +```bash +$ Git init +$ Git add LICENSE go.mod go.sum hello.go hello_test.go +$ Git commit -m "hello: initial commit" +$ +``` + +## 语义版本和模块 + +`go.mod` 中每一个被依赖的模块都有一个语义版本,该语义版本是依赖该模块构建本模块时使用的最小版本。 + +语义版本格式:`vMAJOR.MINOR.PATCH` (v 主版本号.次版本号.修订版本号) + +- 当你模块的公开接口作了向后不兼容的修改后,需要增加主版本号。不到万不得已时,不要这么做。 +- 当你作了向后兼容的修改,如修改依赖或增加新的函数、方法、结构体的字段或类型时,增加次版本号。 +- 当你作了并未影响模块的公开接口或依赖的微小的修改,如修复一个 bug 时,增加修订版本号。 + +你可以在版本号后加连字符和点分隔的标识以指定一个预发布的版本(如 `v1.0.1-alpha` 或 `v2.2.2-beta.2`)。go 命令选择正常版本的优先级高于预发布版本,因此如果你的模块有正常发布的版本,使用者必须显示指定预发布版本号(例如:`go get example.com/hello@v1.0.1-alpha`)才能使用预发布版本。 + +v0 主版本和预发布版本不需要考虑向后兼容,仅作为提交给使用者稳定版本之前的一份 API 精选。然而,v1 及之后的版本需要保证在当前主版本内向后兼容。 + +`go.mod` 中引入的版本可以是仓库中明确打上发布标签的版本(如 `v1.5.2`),也可以是一个基于某次提交的 [伪版本](https://golang.org/cmd/go/#hdr-Pseudo_versions)(如 `v0.0.0-20170915032832-14c0d48ead0c`)。伪版本是一种特殊的预发布版本。当开发者依赖一个尚未发布任何语义版本标签的工程,或基于一次尚未打标签的提交进行开发时,伪版本就派上了用场。但是开发者不能假定伪版本提供的是稳定的、经过完整测试的接口。给你的模块打上明确的版本标签就意味着向该模块的使用者保证了该版本是经过完整测试且稳定的。 + +不要删除你仓库里的版本标签。如果你在某版本中发现了一个 bug 或 issue ,那就发布一个新版本,因为依赖被你删除的版本的工程会编译失败。同样,一旦你发布了一个版本,就不要修改或重写它。[go 模块及检验和数据库](https://blog.golang.org/module-mirror-launch) 储存模块、它们的版本和加密签名哈希,以确保给定版本的构建随时间推移依然保持可复制性。 + +### v0: 初始化的版本, 非稳定版 + +现在我们来给模块打上 `v0` 语义版本标签。`v0` 版本不保证稳定,因此如果一个工程还在提炼公开接口(还未到稳定版本)的阶段,那么就应该以 `v0` 开始其版本。 + +打一个新的版本标签分为以下几步: + +1. 执行 `go mod tidy` ,清理模块不再需要的依赖 +2. 执行 `go test ./...` ,最终确认一下没有任何问题 +3. 用命令 `git tag` 给工程打上一个新版本标签 +4. 把新标签 push 到仓库 + +```bash +$ Go mod tidy +$ Go test ./... +ok example.com/hello 0.015s +$ Git add go.mod go.sum hello.go hello_test.go +$ Git commit -m "hello: changes for v0.1.0" +$ Git tag v0.1.0 +$ Git push origin v0.1.0 +$ +``` + +现在其他的工程就可以依赖 `example.com/hello` 的 `v0.1.0` 版本了。对于你自己的模块,你可以执行 `go list -m example.com/hello@v0.1.0` 来确认最新的版本可用(本例中的模块并不存在,所以不可用)。如果你没有即时看到最新的版本且你用了 `Go module proxy`(Go 1.13 后的版本默认使用),给代理一点加载新版本的时间,几分钟后再试一试。 + +如果你修改了公开接口、在版本为 `v0` 的模块基础上做出了重大改变、抑或更新了你依赖的某个模块的次版本或(完整)版本,那么在你的下次发布中增加次版本号。例如,`v0.1.0` 的下一个版本为 `v0.2.0` 。 + +如果你在已有版本上修改了一个 bug ,增加修订版本号。例如,`v0.1.0` 的下一个版本为 `v0.1.1` 。 + +### v1: 第一个稳定版 + +当你确认你模块的公开接口完全稳定时,可以发布版本 `v1.0.0` 。`v1` 主版本号向使用者声明了该模块的公开接口不会做任何不兼容的修改。他们可以升级到新的 `v1` 主版本下的任何次版本和修订版本,并且自己的代码运行不会崩溃,函数和方法签名不会修改,向外暴露的类型不会被删除,等等。如果修改了公开接口,那么这些修改也都是向后兼容的(例如,给一个结构体增加新的字段)且会在后面发布的次版本中包含进来。如果有修复 bug 的修改(例如安全 bug 修复),这些修改会在后面发布的修订版本(或作为次版本的一部分)包含进来。 + +有时维持向后兼容可能导致写出糟糕的公开接口,这也可以接受。不完美的公开接口总好过让使用者既有的代码崩溃。 + +标准库的 `strings` 包就是一个以保持公开接口一致性的代价来维持向后兼容的典型例子。 + +- [Split](https://godoc.org/strings#Split) 把字符串以指定的分隔符进行分割,切成多个子字符串,返回分隔符分隔的子字符串的切片 +- [SplitN](https://godoc.org/strings#SplitN) 可以控制返回的子字符串的个数 + +[Replace](https://godoc.org/strings#Replace) 有个入参, 指定从字符串开头处要替换的实例的个数(这点与 Split 用法不一样)。 + +联想到 Split 和 SplitN ,你会想当然的认为有 Replace 和 ReplaceN ,且用法与之相似。但是我们不能做到像我们承诺的那样在不让使用者的代码崩溃的前提下修改已有的 Replace。因此,在 Go 1.12 中我们加入了一个新的函数,[ReplaceAll](https://godoc.org/strings#ReplaceAll) 。这样导致公开接口有点别扭,Split 和 Replace 用法完全不同,但是这种不一致的情况好过一次重大变革。 + +假定你对 `example.com/hello` 的公开接口很满意, 且你希望发布第一个稳定版本 `v1` 。 + +用给 `v0` 版本打标签相同的处理打上 `v1` 标签:执行 `go mod tidy` `go test ./...` ,给版本打上标签,push 到 origin 仓库 + +```bash +$ Go mod tidy +$ Go test ./... +ok example.com/hello 0.015s +$ Git add go.mod go.sum hello.go hello_test.go +$ Git commit -m "hello: changes for v1.0.0" +$ Git tag v1.0.0 +$ Git push origin v1.0.0 +$ +``` + +至此,`example.com/hello` v1 版本的公开接口就发布完了。这就向所有人传递了一个信息:我们的公开接口很稳定,他们使用时不会有任何问题。 + +## 总结 + +本文讲了给模块打语义版本标签以及何时发布 `v1` 版本的完整流程。后续会撰文讲述如果维持和发布 `v2` 及其他版本。 + +如果你想提供反馈意见,或参与塑造 Go 依赖管理的未来,请给我们发 [bug reports](https://github.com/golang/go/issues/new) 或 [experience reports](https://github.com/golang/go/wiki/ExperienceReports) + +关联文章: + +- [Go Modules: v2 and Beyond](https://blog.golang.org/v2-go-modules) +- [Module Mirror and Checksum Database Launched](https://blog.golang.org/module-mirror-launch) +- [Migrating to Go Modules](https://blog.golang.org/migrating-to-go-modules) +- [Using Go Modules](https://blog.golang.org/using-go-modules) +- [Go Modules in 2019](https://blog.golang.org/modules2019) +- [A Proposal for Package Versioning in Go](https://blog.golang.org/versioning-proposal) +- [The cover story](https://blog.golang.org/cover) +- [The App Engine SDK and workspaces (GOPATH)](https://blog.golang.org/the-app-engine-sdk-and-workspaces-gopath) +- [Organizing Go code](https://blog.golang.org/organizing-go-code) + +--- + +via: https://blog.golang.org/publishing-go-modules + +作者:[Tyler Bui-Palsulich](https://blog.golang.org/publishing-go-modules) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191002-Go-How-Does-the-Garbage-Collector-Watch-Your-Application.md b/published/tech/20191002-Go-How-Does-the-Garbage-Collector-Watch-Your-Application.md new file mode 100644 index 000000000..3dc147f05 --- /dev/null +++ b/published/tech/20191002-Go-How-Does-the-Garbage-Collector-Watch-Your-Application.md @@ -0,0 +1,129 @@ +首发于:https://studygolang.com/articles/25915 + +# Go: GC 是怎样监听你的应用的? + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191002-Go-How-Does-the-Garbage-Collector-Watch-Your-Application/1.png) + +

Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.

+ +> 这篇文章是基于 Go 的 *1.13* 版本 + +Go 语言的垃圾收集器 (下文简称 GC )能够帮助到开发者,通过自动地释放掉一些程序中不再需要使用的内存。但是,跟踪并清理掉这些内存也可能影响我们程序的性能。Go 语言的 GC 旨在实现 [这些目标](https://blog.golang.org/ismmkeynote) 并且关注如下几个问题: + +- 当程序被终止时,尽可能多的减少在这两个阶段的 STW (的次数) 。 +- 一个 GC 周期的时间要少于 10 毫秒。 +- 一次 GC 周期不能占用超过 25% 的 CPU 资源。 + +这是一些很有挑战性的目标,如果 GC 从我们的程序中了解到足够多的信息,它就能去解决这些问题。 + +## 到达堆阈值 + +GC 将会关注的第一个指标是堆的使用增长。默认情况下,它将在堆大小加倍时运行。这是一个在循环中分配内存的简单程序。 + +```go +func BenchmarkAllocationEveryMs(b *testing.B) { + // need permanent allocation to clear see when the heap double its size + var s *[]int + tmp := make([]int, 1100000, 1100000) + s = &tmp + + var a *[]int + for i := 0; i < b.N; i++ { + tmp := make([]int, 10000, 10000) + a = &tmp + + time.Sleep(time.Millisecond) + } + _ = a + runtime.KeepAlive(s) +} +``` + +追踪器向我们展示了 GC 什么时候被调用: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191002-Go-How-Does-the-Garbage-Collector-Watch-Your-Application/2.png) +

Garbage collector cycles and heap size

+ +一旦堆的大小增加了一倍,内存分配器就会触发执行 GC 。通过设置 `GODEBUG=gctrace=1` ,来打印出若干循环的信息就能够证实这一点: + +``` +gc 8 @0.251s 0%: 0.004+0.11+0.003 ms clock, 0.036+0/0.10/0.15+0.028 ms cpu, 16->16->8 MB, 17 MB goal, 8 P + +gc 9 @0.389s 0%: 0.005+0.11+0.007 ms clock, 0.041+0/0.090/0.11+0.062 ms cpu, 16->16->8 MB, 17 MB goal, 8 P + +gc 10 @0.526s 0%: 0.046+0.24+0.014 ms clock, 0.37+0/0.14/0.23+0.11 ms cpu, 16->16->8 MB, 17 MB goal, 8 P +``` + +第九个循环就是我们之前看到的那个循环,运行在第 389 ms 。有意思的部分是 `16->16->8 MB` ,它展示了在 GC 被调用前堆使用的内存有多大,以及在 GC 执行后它们还剩下多少。我们可以清楚地看到,当第八个循环将堆大小减少到 8 MB 时,第九个 GC 周期将在 16 MB 时刻触发。 + +这个阈值的比例由环境变量 GOGC 决定,默认值为 100 % —— 也就是说,在堆的大小增加了一倍之后,GC 就会被调用。出于性能原因,并且为了避免经常启动一个循环,当堆的大小低于 4 MB * GOGC 的时候, GC 将不会被执行。——当 GOGC 被设置为 100 % 时,在堆内存低于 4 MB 时 GC 将不会被触发。 + +## 到达时间阈值 + +GC 关注的第二个指标是在两次 GC 之间的时间间隔。如果超过两分钟 GC 还未执行,那么就会强制启动一次 GC 循环。 + +由 `GODEBUG` 给出的跟踪显示,两分钟后会强制启动一次循环。 + +``` +GC forced +gc 15 @121.340s 0%: 0.058+1.2+0.015 ms clock, 0.46+0/2.0/4.1+0.12 ms cpu, 1->1->1 MB, 4 MB goal, 8 P +``` + +## 需要协助 + +GC 主要由两个主要阶段组成: + +- 标记仍在使用的内存 +- 清理未标记为使用中的内存 + +在标记期间,Go 必须确保 GC 标记内存的速度比新分配内存的速度更快。事实是,如果 GC 正在标记 4 MB 大小的内存,然而同时程序正在分配同样大小的内存,那么 GC 必须在完成后立即触发。 + +为了解决这个问题,Go 在标记内存的同时会跟踪新的内存分配,并关注 GC 何时过载。第一步在 GC 触发时执行,它会首先为每一个处理器准备一个 处于 sleep 状态的 goroutine,等待标记阶段。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191002-Go-How-Does-the-Garbage-Collector-Watch-Your-Application/3.png) + +

Goroutines for marking phase

+ +跟踪器能够显示出这些 goroutines: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191002-Go-How-Does-the-Garbage-Collector-Watch-Your-Application/4.png) + +

Goroutines for marking phase

+ +当这些 Goroutine 生成后, GC 就开始标记阶段,该阶段会检查哪些变量应收集并清除。被标记为 `GC dedicated` 的 goroutines 会运行标记,并不会被抢占,然而那些标记为 `GC idle` 的 goroutines 就会去工作,因为他们没有任何其他事情。可以被抢占。 + +GC 现在已经能够去标记那些不再使用的变量。对于每一个被扫描到的变量,它会增加一个计数器,以便继续跟踪当前的工作并且也能够获得剩余工作的快照。当一个 Goroutine 在 GC 期间被安排了任务,Go 将会比较所需要的分配和已经扫描到的,以便对比扫描的速度和分配的需求。如果比较的结果是扫描内容较多,那么当前的 Goroutine 并不需要去提供帮助。换句话说,如果扫描与分配相比有所欠缺,那么 Go 就会使用 Goroutine 来协助。这有一个图表来反应这个逻辑: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191002-Go-How-Does-the-Garbage-Collector-Watch-Your-Application/5.png) +

Mark assist based on scanning debt

+ +在我们的示例中,因为扫描 / 分配的差值为负数,所以 Goroutine 14 被请求工作: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191002-Go-How-Does-the-Garbage-Collector-Watch-Your-Application/6.png) +

Assistance for marking

+ +## CPU 限制 + +Go 语言 GC 的目标之一是不占用 25 % 的 CPU。这就意味着 Go 在标记阶段不应分配超过四分之一的处理器。实际上,这正是我们在前面的示例中所看到的,在八个处理器中只有两个 Goroutine 被 GC 充分利用: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191002-Go-How-Does-the-Garbage-Collector-Watch-Your-Application/7.png) + +

Dedicated Goroutine for marking phase

+ +正如我们所看到的,其他的 Goroutine 仅在没有其他事情要做的情况下才会在标记阶段工作。但是,在 GC 的协助请求下,Go 程序可能会在高峰时间最终占用超过 25 % 的 CPU ,如 Goroutine 14 所示: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191002-Go-How-Does-the-Garbage-Collector-Watch-Your-Application/8.png) + +

Mark assistance with dedicated goroutines

+ +在我们的示例中,短时间内将我们处理器的 37.5 % (八分之三)分配给了标记阶段。这可能很少见,只有在分配很高的情况下才会发生。 + +--- + +via:https://medium.com/a-journey-with-go/go-how-does-the-garbage-collector-watch-your-application-dbef99be2c35 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[sh1luo](https://github.com/sh1luo) +校对:[lxbwolf](https://github.com/lxbwolf) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191002-Go-Keeping-a-Variable-Alive.md b/published/tech/20191002-Go-Keeping-a-Variable-Alive.md new file mode 100644 index 000000000..f72e34c33 --- /dev/null +++ b/published/tech/20191002-Go-Keeping-a-Variable-Alive.md @@ -0,0 +1,130 @@ +首发于:https://studygolang.com/articles/28437 + +# Go: 延长变量的生命周期 + +![Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191002-Go-Keeping-a-Variable-Alive/00.png) + +本文基于 Go 1.13。 + +在 Go 中,我们不需要自己管理内存分配和释放。然而,有些时候我们需要对程序进行更细粒度的控制。Go 运行时提供了很多种控制运行时状态及其与内存管理器之间相互影响的方式。本文中,我们来审查让变量不被 GC 回收的能力。 + +## 场景 + +我们来看一个基于 [Go 官方文档](https://golang.org/pkg/runtime/#KeepAlive) 的例子: + +```go +type File struct { d int } + +func main() { + p := openFile("t.txt") + content := readFile(p.d) + + println("Here is the content: "+content) +} + +func openFile(path string) *File { + d, err := syscall.Open(path, syscall.O_RDONLY, 0) + if err != nil { + panic(err) + } + + p := &File{d} + runtime.SetFinalizer(p, func(p *File) { + syscall.Close(p.d) + }) + + return p +} + +func readFile(descriptor int) string { + doSomeAllocation() + + var buf [1000]byte + _, err := syscall.Read(descriptor, buf[:]) + if err != nil { + panic(err) + } + + return string(buf[:]) +} + +func doSomeAllocation() { + var a *int + + // memory increase to force the GC + for i:= 0; i < 10000000; i++ { + i := 1 + a = &i + } + + _ = a +} +``` + +[源码地址](https://gist.githubusercontent.com/blanchonvincent/a247b6c2af559b62f93377b5d7581b7f/raw/6488ec2a36c28c46f942b7ac8f24af4e75c19a2f/main.go) + +这个程序中一个函数打开文件,另一个函数读取文件。代表文件的结构体注册了一个 finalizer,在 gc 释放结构体时自动关闭文件。运行这个程序,会出现 panic: + +```bash +panic: bad file descriptor + +goroutine 1 [running]: +main.readFile(0x3, 0x5, 0xc000078008) +main.go:42 +0x103 +main.main() +main.go:14 +0x4b +exit status 2 +``` + +下面是流程图: + +- 打开文件,返回一个文件描述符 +- 这个文件描述符被传递给读取文件的函数 +- 这个函数首先做一些繁重的工作: + +![图 01](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191002-Go-Keeping-a-Variable-Alive/01.png) + +allocate 函数触发 gc: + +![02.png](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191002-Go-Keeping-a-Variable-Alive/02.png) + +因为文件描述符是个整型,并以副本传递,所以打开文件的函数返回的结构体 `*File*` 不再被引用。Gc 把它标记为可以被回收的。之后触发这个变量注册的 finalizer,关闭文件。 + +然后,主协程读取文件: + +![03.png](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191002-Go-Keeping-a-Variable-Alive/03.png) + +因为文件已经被 finalizer 关闭,所以会出现 panic。 + +## 让变量不被回收 + +`runtime` 包暴露了一个方法,用来在 Go 程序中避免出现这种情况,并显式地声明了让变量不被回收。在运行到这个调用这个方法的地方之前,gc 不会清除指定的变量。下面是加了对这个方法的调用的新代码: + +![04.png](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191002-Go-Keeping-a-Variable-Alive/04.png) + +在运行到 `keepAlive` 方法之前,gc 不能回收变量 `p`。如果你再运行一次程序,它会正常读取文件并成功终止。 + +## 追本逐源 + +`keepAlive` 方法本身没有做什么: + +![05.png](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191002-Go-Keeping-a-Variable-Alive/05.png) + +运行时,Go 编译器会以很多种方式优化代码:函数内联,死码消除,等等。这个函数不会被内联,Go 编译器可以轻易地探测到哪里调用了 `keepAlive`。编译器很容易追踪到调用它的地方,它会发出一个特殊的 SSA 指令,以此来确保它不会被 gc 回收。 + +![06.png](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191002-Go-Keeping-a-Variable-Alive/06.png) + +在生成的 SSA 代码中也可以看到这个 SSA 指令: + +![07.png](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191002-Go-Keeping-a-Variable-Alive/07.png) + +在我的文章 [Go 编译器概述](https://medium.com/a-journey-with-go/go-overview-of-the-compiler-4e5a153ca889) 中你可以看到更多关于 Go 编译器和 SSA 的信息。 + +--- +via: https://medium.com/a-journey-with-go/go-keeping-a-variable-alive-c28e3633673a + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191003-Integeration-testing-in-go-part-2.md b/published/tech/20191003-Integeration-testing-in-go-part-2.md new file mode 100644 index 000000000..862874226 --- /dev/null +++ b/published/tech/20191003-Integeration-testing-in-go-part-2.md @@ -0,0 +1,429 @@ +首发于:https://studygolang.com/articles/25564 + +# Go 语言中的集成测试:第二部分 - 设计和编写测试 + +## 序幕 + +这篇文章是集成测试系列两个部分中的第二部分。你可以先读 [第一部分:使用 Docker 在有限的环境中执行测试](https://studygolang.com/articles/21759)。本文中的示例可以从 [代码仓库](https://github.com/george-e-shaw-iv/integration-tests-example) 获取。 + +## 简介 + +> “比起测试行为,设计测试行为是已知的最好的错误预防程序之一。” —— Boris Beizer + +在执行集成测试之前,必须正确配置该测试相关的外部系统。否则,测试结果是无效和不可靠的。例如,数据库需要有定义好的数据,这些数据对于要测试的行为是正确的。测试期间更改的数据需要进行验证,尤其是如果要求更改的数据对于后续测试而言是准确的时侯。 + +Go 测试工具提供了有在执行测试函数前执行代码的能力,使用叫做 `TestMain` 的入口函数实现。它类似于 Go 应用程序的 `Main` 函数。有了 `TestMain` 函数,我们可以在执行测试之前做其他系统配置,比如数据库连接之类的。在本文中,我将分享如何使用它 `TestMain` 来配置和连接 Postgres 数据库,以及如何针对该数据库编写和运行测试。 + +## 填充初始数据 + +为了填充数据库,需要定义数据并将其放置在测试工具可以访问的位置。一种常见的方法是定义一个 SQL 文件,该文件是项目的一部分,并且包含所有需要执行的 SQL 命令。另一种方法是将 SQL 命令存储在代码内部的常量中。不同于这两种方法,我将只使用 Go 语言实现来解决此问题。 + +通常情况下,你已将你的数据结构定义为 Go 结构体类型,用于数据库通信。我将利用这些已存在的数据结构,已经可以控制数据从数据库中流入流出。基于已有的数据结构声明变量,构造所有填充数据,而无需 SQL 语句。 + +我喜欢这种解决方式,因为它简化了编写集成测试和验证数据是否能够正确用于数据库和应用程序之间的通信的。不必将数据直接与 JSON 比较,就可以将数据解编为适当的类型,然后直接与为之前数据结构定义的变量进行比较。这不仅可以最大程度地减少测试中的语法比较错误,还可以使您的测试更具可维护性、可扩展性和可读性。 + +## 填充数据库 + +> 译者注:原文为 `Seeding The Database`,下面部分相关功能函数就称为种子函数 + +本文提到的,所有用于填充数数据库功能函数,都在 [`testdb`](https://github.com/george-e-shaw-iv/integration-tests-example) 包中。这个包仅用于测试,不用做第三方依赖。用来辅助填充测试数据库的三个核心函数分别是:`SeedLists`, `SeedItems`, 和 `Truncate`,如下: + +这是 `SeedLists` 函数: + +### 代码清单 1 + +```go +func SeedLists(dbc *sqlx.DB) ([]list.List, error) { + now := time.Now().Truncate(time.Microsecond) + + lists := []list.List{ + { + Name: "Grocery", + Created: now, + Modified: now, + }, + { + Name: "To-do", + Created: now, + Modified: now, + }, + { + Name: "Employees", + Created: now, + Modified: now, + }, + } + + for i := range lists { + stmt, err := dbc.Prepare("INSERT INTO list (name, created, modified) VALUES ($1, $2, $3) RETURNING list_id;") + if err != nil { + return nil, errors.Wrap(err, "prepare list insertion") + } + + row := stmt.QueryRow(lists[i].Name, lists[i].Created, lists[i].Modified) + + if err = row.Scan(&lists[i].ID); err != nil { + if err := stmt.Close(); err != nil { + return nil, errors.Wrap(err, "close psql statement") + } + + return nil, errors.Wrap(err, "capture list id") + } + + if err := stmt.Close(); err != nil { + return nil, errors.Wrap(err, "close psql statement") + } + } + + return lists, nil +} +``` + +代码清单 1 展示了 `SeedLists` 函数及其如何创建测试数据。`list.List` 定义了一个用于插入的数据表。然后,将测试数据插入数据库。为了帮助将插入的数据与测试期间进行的任何数据库调用的结果进行比较,测试数据集返回给调用方。 + +接下来,我们看看将更多测试数据插入数据库的 `SeedItems` 函数。 + +### 代码清单 2 + +```go +func SeedItems(dbc *sqlx.DB, lists []list.List) ([]item.Item, error) { + now := time.Now().Truncate(time.Microsecond) + + items := []item.Item{ + { + ListID: lists[0].ID, // Grocery + Name: "Chocolate Milk", + Quantity: 1, + Created: now, + Modified: now, + }, + { + ListID: lists[0].ID, // Grocery + Name: "Mac and Cheese", + Quantity: 2, + Created: now, + Modified: now, + }, + { + ListID: lists[1].ID, // To-do + Name: "Write Integration Tests", + Quantity: 1, + Created: now, + Modified: now, + }, + } + + for i := range items { + stmt, err := dbc.Prepare("INSERT INTO item (list_id, name, quantity, created, modified) VALUES ($1, $2, $3, $4, $5) RETURNING item_id;") + if err != nil { + return nil, errors.Wrap(err, "prepare item insertion") + } + + row := stmt.QueryRow(items[i].ListID, items[i].Name, items[i].Quantity, items[i].Created, items[i].Modified) + + if err = row.Scan(&items[i].ID); err != nil { + if err := stmt.Close(); err != nil { + return nil, errors.Wrap(err, "close psql statement") + } + + return nil, errors.Wrap(err, "capture list id") + } + + if err := stmt.Close(); err != nil { + return nil, errors.Wrap(err, "close psql statement") + } + } + + return items, nil +} +``` + +代码清单 2 显示了 `SeedItems` 函数如何创建测试数据。除了使用 `item.Item` 数据类型,该代码与清单 1 基本相同。`testdb` 包中还有一个未提到的函数 `Truncate`。 + +### 代码清单 3 + +```go +func Truncate(dbc *sqlx.DB) error { + stmt := "TRUNCATE TABLE list, item;" + + if _, err := dbc.Exec(stmt); err != nil { + return errors.Wrap(err, "truncate test database tables") + } + + return nil +} +``` + +代码清单 3 展示了 `Truncate` 函数。顾名思义,它用于删除 `SeedLists` 和 `SeedItems` 函数插入的所有数据。 + +## 使用 testing.M 创建 TestMain + +使用便于 ` 填充/清除 ` 数据库的软件包后,该集中精力配置以运行真正的集成测试了。Go 自带的测试工具可以让你在 `TestMain` 函数中定义需要的行为,在测试函数执行前执行。 + +### 代码清单 4 + +```go +func TestMain(m *testing.M) { + os.Exit(testMain(m)) +} +``` + +代码清单 4 是 `TestMain` 函数,它在所有集成测试之前执行。在 23 行,叫做 `testMain` 的未导出的函数被 `os.Exit` 调用。这样做是为了 `testMain` 可以执行其中的延迟函数,并且仍可以在 `os.Exit` 调用内部设置适当的整数值。以下是 `testMain` 函数的实现。 + +### 代码清单 5 + +```go +func testMain(m *testing.M) int { + dbc, err := testdb.Open() + if err != nil { + log.WithError(err).Info("create test database connection") + return 1 + } + defer dbc.Close() + + a = handlers.NewApplication(dbc) + + return m.Run() +} +``` + +在代码清单 5 中,你可以看到 `testMain` 只有 8 行代码。28 行,函数调用 `testdb.Open()` 开始建立数据库连接。此调用的配置参数在 `testdb` 包中设置为常量。重要的是要注意,如果测试用的数据库未运行,调用 `Opne` 连接数据库会失败。该测试数据库是由 `docker-compose` 创建提供的,详细说明在本系列的第 1 部分中(单击 [这里](https://studygolang.com/articles/21759) 阅读第 1 部分)。 + +成功连接测试数据库后,连接将传递给 `handlers.NewApplication()`,并且此函数的返回值用于初始化的包级变量 `*handlers.Application` 类型。`handlers.Application` 类型是这个项目自定义的结构体,有用于 `http.Handler` 接口的字段,以简化 Web 服务的路由以及对已创建的数据库连接的引用。 + +现在,应用程序值已初始化,可以调用 `m.Run` 来执行所有测试函数。对 `m.Run` 的调用处于阻塞状态,直到所有确定要运行的测试函数都执行完之后,该调用才会返回。非零退出代码表示失败,0 表示成功。 + +## 编写 Web 服务的集成测试 + +集成测试将多个代码单元以及所有集成服务(例如数据库)组合在一起,并测试各个单元的功能以及各个单元之间的关系。为 Web 服务编写集成测试通常意味着每个集成测试的所有入口点都是一个路由。`http.Handler` 接口是任何 Web 服务的必需组件,它包含的 `ServeHTTP` 函数使我们能够利用应用程序中定义的路由。 + +在 Web 服务的集成测试中,构建初始化数据并且以 Go 类型返回初始数据,对返回的响应体的结构进行断言非常有用。在接下来的代码清单中,我将一个典型的 API 路由集成测试分解成几个不同的部分。第一步是使用代码清单 1 和代码清单 2 中定义的种子数据。 + +### 清单 6 + +```go +func Test_getItems(t *testing.T) { + defer func() { + if err := testdb.Truncate(a.DB); err != nil { + t.Errorf("error truncating test database tables: %v", err) + } + }() + + expectedLists, err := testdb.SeedLists(a.DB) + if err != nil { + t.Fatalf("error seeding lists: %v", err) + } + + expectedItems, err := testdb.SeedItems(a.DB, expectedLists) + if err != nil { + t.Fatalf("error seeding items: %v", err) + } +} +``` + +在获取种子数据失败前,必须设置延迟函数清理数据库,这样,无论函数失败与否,测试结束后保证数据库是干净的。然后,调用 `testdb` 中的种子函数(`testdb.SeedLists` 和 `testdb.SeedItems` )构造初始数据,并获取他们的返回值作为预期值,以便在集成测试中与实际路由请求结果(真实值)做对比。如果这两个种子函数中的任何一个失败,测试就会调用 `t.Fatalf` 。 + +### 清单 7 + +```go +// Application is the struct that contains the server handler as well as +// any references to services that the application needs. +type Application struct { + DB *sqlx.DB + handler http.Handler +} + +// ServeHTTP implements the http.Handler interface for the Application type. +func (a *Application) ServeHTTP(w http.ResponseWriter, r *http.Request) { + a.handler.ServeHTTP(w, r) +} + +``` + +为了调用注册的路由,`Application` 类型实现 `http.Handler` 接口。`http.Handler` 作为 `Application` 的内嵌结构体字段,因此 `Application` 可以调用 `http.Handler` 接口实现的 `ServeHTTP` 函数 + +### 清单 8 + +```go +req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/list/%d/item", test.ListID), nil) +if err != nil { + t.Errorf("error creating request: %v", err) +} + +w := httptest.NewRecorder() +a.ServeHTTP(w, req) +``` + +回顾一下代码清单 5,构造 `Application` 是为了在测试中使用。`ServeHTTP` 函数需要两个参数: `http.ResponseWriter` 和 `http.Request`。`http.NewRequest` 构造 `http.Request`,`httptest.NewRecorder` 构造 `http.ResponseRecorder`——即 `http.Response` 。 + + `http.NewRecorder` 函数的返回 `ResponseRecorder` 值实现了 `ResponseWriter` 接口。调用路由请求后,`ResponseRecorder` 可以用来分析了。其中最关键的字段 `Code` 和 `Body`,前者是该请求的实际响应码,后者是一个指向响应内容的 `bytes.Buffer` 类型的指针。 + +> 译者注:这里的 `http.ResponseWriter` 和 `http.Request` 实现了 Golang 中常见的 `Writer` 和 `Reader` 接口,即 **输出** 和 **输入**,在 http 请求中即 `Response` 和 `Request`。 + +### 清单 9 + +```go +if want, got := http.StatusOK, w.Code; want != got { + t.Errorf("expected status code: %v, got status code: %v", want, got) +} +``` + +清单 9 中,实际的响应码和预期的响应码做对比。如果不同,将调用 `t.Errorf`,它将输出失败原因。 + +### 清单 10 + +```go +var items []item.Item +resp := web.Response{ + Results: items, +} + +if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { + t.Errorf("error decoding response body: %v", err) +} + +if d := cmp.Diff(expectedItems, items); d != "" { + t.Errorf("unexpected difference in response body:\n%v", d) +} +``` + +示例中使用自定义响应体 `web.Response`,使用 键为 `results` 的 JSON 字符串存储路由返回信息。代码清单 10 中声明了一个 []item.Item 类型的变量 items,用于和预期值对比。 初始化 items 变量传递给 resp 的字段 results。接下来,items 会随着解析路由响应体数据到 resp 中,从而包含响应体的数据。 + +Google 的 [go-cmp](https://github.com/google/go-cmp) 包可替代 `reflect.DeepEqual` ,在对比 struct,map,slice 和 array 时更安全,更易用。调用 cmp.Diff 对比清单 6 中定义的种子数据和实际响应体中返回的数据,如果不等,测试将失败,并且将差异输出到标准输出(stdout)中。 + +## 测试技巧 + +就测试而言,最好的建议是尽早测试,并且经常测试,而不是将测试放到开发之后考虑,而且测试应该推动、驱动应用程序的开发。这就是“测试驱动开发(TDD)”。通常情况下,没有随时测试代码。在编写代码时,将测试的想法抛到脑后,自己(开发人员)默认编写的代码是可测试的。代码单元(通常是一个函数)不管再小都能进行测试。你的服务进行越多测试,未知的就越少,隐藏的副作用(bug)就越少。 + +有了下面这些技巧,你的测试将洞察力,更易读,更快。 + +### 表测试 + +表测试是一种编写测试的方式,可以防止针对同一代码单元的不同可测试结果重复测试断言。以下面的求和函数为例: + +### 清单 11 + +```go +// Add takes an indefinite amount of operands and adds them together, returning +// the sum of the operation. +func Add(operands ...int) int { + var sum int + + for _, operand := range operands { + sum += operand + } + + return sum +} +``` + +在测试中,我想确保函数可以处理以下情况: + +* 没有参数(operands),应返回 0。 +* 一个参数,直接返回参数值。 +* 两个参数,返回这两个数之和。 +* 三个参数,则返回这三个数之和。 + +彼此独立地编写这些测试将导致重复许多相同的调用和断言。我认为,更好的方法是利用表测试。为了编写表测试,必须定义一片匿名声明的结构,其中包含我们每个测试用例的元数据。然后可以使用循环遍历不同测试用例的这些条目,并可以对用例进行测试和独立运行 `t.Run`。`t.Run` 需要两个参数,子测试函数和这个子测试函数的函数名,子测试函数必须符合这种类型:`func(*testing.T)`。 + +### 清单 12 + +```go +// TestAdd tests the Add function. +func TestAdd(t *testing.T) { + tt := []struct { + Name string + Operands []int + Sum int + }{ + { + Name: "NoOperands", + Operands: []int{}, + Sum: 0, + }, + { + Name: "OneOperand", + Operands: []int{10}, + Sum: 10, + }, + { + Name: "TwoOperands", + Operands: []int{10, 5}, + Sum: 15, + }, + { + Name: "ThreeOperands", + Operands: []int{10, 5, 4}, + Sum: 19, + }, + } + + for _, test := range tt { + fn := func(t *testing.T) { + if e, a := test.Sum, Add(test.Operands...); e != a { + t.Errorf("expected sum %d, got sum %d", e, a) + } + } + + t.Run(test.Name, fn) + } +} +``` + +测试清单 12 中,使用匿名声明的结构体定义了不同的情况。遍历这些情况,执行这些测试用例。比较实际返回值和预期值,如果不等,则调用 `t.Errorf`,返回测试失败的信息。清单中,遍历调用 t.Run 执行每个测试用例。 + +### t.Helper() 和 t.Parallel() + +标准库中的 `testing` 包提供了很多有用的程序(函数)辅助测试,而不用导入之外的第三方包。其中我最喜欢的两个函数是 `t.Helper()` 和 `t.Parallel()`,它们都定义为 `testing.T` 接收者,它是在 `_test.go` 文件中每个 `Test` 函数都必需的一个的参数。 + +### 清单 13 + +```go +// GenerateTempFile generates a temp file and returns the reference to +// the underlying os.File and an error. +func GenerateTempFile() (*os.File, error) { + f, err := ioutil.TempFile("", "") + if err != nil { + return nil, err + } + + return f, nil +} +``` + +在代码清单 13 中,为特定的测试包定义了一个辅助函数。这个函数返回 `os.File` 指针和 `error`。每次测试调用这个辅助函数必须判断 error 是一个 non-nil 。通常情况这也没什么,但是有一个更好的方式:使用 t.Helper() ,这种方式省略了 `error` 返回。 + +### 清单 14 + +```go +// GenerateTempFile generates a temp file and returns the reference to +// the underlying os.File. +func GenerateTempFile(t *testing.T) *os.File { + t.Helper() + + f, err := ioutil.TempFile("", "") + if err != nil { + t.Fatalf("unable to generate temp file: %v", err) + } + + return f +} +``` + +清单 14 和清单 13 相同,只是使用 `t.Helper()`。这个函数定义使用了 `*testing.T` 作为参数,省略了 error 的返回。函数先调用 `t.Helper()`,这在编译测试二进制文件时发出信号:如果 t 在这个函数中调用任何接收器函数,则将其报告给调用函数(Test*)。与辅助函数不同,所有行号和文件信息会都会关联到这个函数。 + +一些测试可以进行安全的并行进行,并且 Go testing 包原生支持并行运行测试。在所有 Test* 函数开始调用 t.Parallel(), 可以编译出可以安全并行运行的测试二进制文件。就是这么简单,就是这么强大! + +## 结论 + +如果不配置程序运行时所需的外部系统,则无法在集成测试的上下文中完全验证程序的行为。此外,需要持续监测那些外部系统(特别是当它们包含应用程序状态数据的情况下),以确保它们包含有效和有意义的数据。Go 使开发人员不仅可以在测试过程中进行配置,还可以无需标准库之外的包就能维护外部数据。因此,我们可以编写可读性,一致性,性能和可靠性同时都能保证的集成测试。Go 的真正魅力正在于其简约而功能齐全的工具集,它为开发人员提供了无需依赖外部库或任何非常规限制的功能。 + +--- + +via: https://www.ardanlabs.com/blog/2019/10/integration-testing-in-go-set-up-and-writing-tests.html + +作者:[George Shaw](https://github.com/george-e-shaw-iv/) +译者:[TomatoAres](https://github.com/TomatoAres) +校对:[lxbwolf](https://github.com/lxbwolf) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191025-Go-Fuzz-Testing-in-Go.md b/published/tech/20191025-Go-Fuzz-Testing-in-Go.md new file mode 100644 index 000000000..0ac1bbbda --- /dev/null +++ b/published/tech/20191025-Go-Fuzz-Testing-in-Go.md @@ -0,0 +1,176 @@ +首发于:https://studygolang.com/articles/28987 + +# Go 中的模糊(Fuzz)测试 + +![由 Renee French 创作的原始 Go Gopher 作品,为“ Go 的旅程”创作的插图。](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191025-Go-Fuzz-Testing-in-Go/Illustration.png) + +模糊测试(Fuzzing)是一项使用随机数据加载我们程序的测试技术。是[对常规测试的补充](https://docs.google.com/document/d/1N-12_6YBPpF9o4_Zys_E_ZQndmD06wQVAM_0y9nZUIE/edit),并且使开发者可以发现那些在手工生成的输入下难以发现的 bug。模糊测试在 Go 程序中很容易设置,并且可以适应于几乎所有类型的代码。 + +## 模糊测试项目 + +在 Go 社区中两个项目适用于模糊测试:Google 开发的 [gofuzz](https://github.com/google/gofuzz) 和 [Dmitry Vyukov](https://github.co,/dvyukov) 开发的 [go-fuzz](https://github.com/dvyukov/go-fuzz),Dmitry Vyukov 同样为 Google 工作。两个项目都是有用的,同时适用于不同的用法。来逐一了解它们: + +- [gofuzz](https://github.com/google/gofuzz) 提供了一个可以用随机值填充你的 Go 结构体的包。而你需要做的是编写测试代码,并且调用这个包来获取随机数据。当你想要模糊测试结构化数据的时候,这个包是完美的。这里是使用随机数据对一个结构体进行 50000 次模糊测试的例子,其中指针/切片/map 有 50%的几率被设置为空: + +![模糊测试结构化数据](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20191025-Go-Fuzz-Testing-in-Go/fuzzing-structured-data.png) + +- [go-fuzz](https://github.com/dvyukov/go-fuzz) 基于已经在大多数知名的软件或库中发现了上百个 bug 的 [American Fuzzy Lop](http://lcamtuf.coredump.cx/afl/)。Go-Fuzz 会连续运行,并且根据提供的样本生成随机的字符串。之后必须解析这些字符串,并且明确地将其标记为是否可用于测试。任何有趣的生成的数据都会被该工具所报告,这些数据增加了代码的覆盖率或者导致崩溃。该工具十分适合那些管理诸如 XML,JSON,图像等字符串信息的程序。这里是该工具运行以及发行问题的预览,被称为 crasher。 + +![使用 go-fuzz 进行模糊测试](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20191025-Go-Fuzz-Testing-in-Go/fuzzing-with-go-fuzz.png) + +每个包都有自己的长处,并且这两个工具至少有一个会适用于你的程序以及你在开发的项目。来了解下第二个工具,有着更加复杂的工作流程的工具。 + +## 通过例子了解 Go-Fuzz + +先从一个借助模糊测试解决 `encoding/xml` 包中[一个 bug](https://github.com/golang/go/issues/11112) 的例子开始。这里是该问题的复现步骤: + +- 定义用于接收生成数据的 ` 模糊(Fuzz)` 方法: + +```go +// +build gofuzz + +package fuzzing + +import "encoding/xml" + +type X struct { + D string `xml:",comment"` +} + +func FuzzXMLComment(data []byte) int { + v := new(X) + if xml.Unmarshal(data, v) != nil { + return -1 + } + if _, err := xml.Marshal(v); err != nil { + panic(err) + } + + return 1 +} +``` + +- 定义一个会被工具使用的初始*语料*: + +```xml + + + foo + +``` + +然后,由于该 bug 已在 Go 1.6 中被合入,确保在你的标准库中还原了提交 [97c859f8da0c85c33d0f29ba5e11094d8e691e87](https://github.com/golang/go/commit/97c859f8da0c85c33d0f29ba5e11094d8e691e87)——同样含有这个 bug 的 Go 1.5 与最新版本的 go-fuzz 不兼容。你的迷你项目应该遵循这样的结构: + +![模糊测试 encoding/xml](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20191025-Go-Fuzz-Testing-in-Go/fuzzing-encoding:xml-structure.png) + +你现在可以运行 `go-fuzz-build` 和 `go-fuzz -bin=./main.zip -workdir=.` 来开始模糊测试: + +![模糊测试 encoding/xml](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20191025-Go-Fuzz-Testing-in-Go/fuzzing-encoding:xml.png) + +经过了最初的几秒钟后,go-fuzz 已经发现了一个 crasher(译注:指引起崩溃的数据,这里保留原文),crasher 被保存在 `crasher/` 文件夹中: + +![模糊测试期间记录的 crasher](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20191025-Go-Fuzz-Testing-in-Go/crasher-recorded-during-fuzzing.png) + +crasher 文件包含了引起 panic 的字符串: + +```xml + +``` + +`.output` 文件包含了发生的 panic 信息: + +```bash +panic: xml: comments must not contain "--" +``` + +确实,按照 XML 的规范,注释有两个约束。 + +> 字符串“--”(双连字符)不得在注释中使用。[...] 注意,语法上不允许注释以 `--->` 结束 + +借助 go-fuzz 这个 panic 的问题已经在标准库中被修复,并且这种问题现在会返回一个 error。现在来深入研究这个包,来了解这个包如何成功找到这个问题。 + +## Go-Fuzz 工作流程 + +如之前所见,[go-fuzz](https://github.com/dvyukov/go-fuzz) 的工作流程包含两个步骤: + +- 通过命令 `go-fuzz-build` 从你代码中定义的指令来构建工具: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20191025-Go-Fuzz-Testing-in-Go/go-fuzz-build.png) + +由于构建嵌入了 `Fuzz` 方法,如果修改了这些方法,不要忘了运行 `go-fuzz-build`。 + +- 通过 `go-fuzz -bin=./my-package.zip -workdir=.` 命令,持续运行工具,并且收集有趣的输入和崩溃: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20191025-Go-Fuzz-Testing-in-Go/go-fuzz.png) + +语料生成是 go-fuzz 的核心重点。[Dmitry Vyukov](https://github.com/dvyukov) 在 [GopherCon 2015](https://www.youtube.com/watch?v=a9xrxRsIbSU&t=459s) 上给出了这个核心功能的流程图: + +![语料生成的流程图](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20191025-Go-Fuzz-Testing-in-Go/workflow-of-the-corpus-generation.png) + +语料生成在初试语料库上循环,并且使用两个方法: + +- **mutation 方法**,该方法对语料进行字节上的微小修改,比如消除,插入,重复,交换,翻转等修改。这里是一个发现崩溃前发生的不同突变(mutation)的例子: + +```xml + + + +``` + +- **versifying 方法**,这是最先进的方法。会学习文本的结构(数字,字母,列表键-值,等等。),然后应用不同部分的 mutation 方法。这是一个仅在字符串上突变的先前语料的例子: + +```xml +- +foo + +``` + +这是标签上突变的另一个例子: + +```xml +<> + +foo<:b> +[/a] +``` + +Go-Fuzz 运行期间主要使用 mutation 方法(占 90%的迭代),但是组合使用两种方法对发现 bug 是有帮助的: + +> 对 xml 文本进行 2.5 小时的模糊测试后: +> +> 没有使用验证(versifier)的模糊测试发现了 902 个(有问题的)输入。 +> +> 使用了验证的模糊测试发现了 1055 个(有问题的)输入,其中验证方法发现了 83 个。 +> +> 验证方法生成了新的输入,并且增加了 25%的模糊验证效率。 + +这个工作流程十分高效且易于集成。有助于所有处理文本的包,不论是 Go 标准库还是你自己的代码。 + +## 模糊测试集成 + +[自 Go 1.5 开始](https://golang.org/doc/go1.5#hardening),模糊测试被应用于 Go 标准库中,并且已经发现了[超过 200 个 bug](https://github.com/dvyukov/go-fuzz#trophies)。然而,尽管一些包已经存在一些 `Fuzz` 函数,比如 `encoding/csv` 或 `image/png`,Go 并没有原生集成模糊测试。是否[让模糊测试成为 Go 的一等公民](https://github.com/golang/go/issues/19109)的讨论已在 GitHub 上展开。 + +就与模糊测试持续集成的有效在线工具而言,两个工具使用 Go 和 Go-fuzz: + +- [fuzzit.dev](https://fuzzit.dev/), +- [fuzzbuzz.io](https://fuzzbuzz.io/) + +与 GitHub 集成的话,两者有着差不多的价格。他们有免费的账户可以让你在自己的管道中进行模糊测试。 + +--- + +via: https://medium.com/a-journey-with-go/go-fuzz-testing-in-go-deb36abc971f + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[dust347](https://github.com/dust347) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191030-Graceful-Restart-in-Golang.md b/published/tech/20191030-Graceful-Restart-in-Golang.md new file mode 100644 index 000000000..4e844469c --- /dev/null +++ b/published/tech/20191030-Graceful-Restart-in-Golang.md @@ -0,0 +1,197 @@ +首发于:https://studygolang.com/articles/25123 + +# Golang Http 服务的优雅重启 + +(2015 年 4 月更新):[Florian von Bock](https://github.com/fvbock) 已将本文中描述的内容实现成了一个名为[Endless](https://github.com/fvbock/endless)的 Go 程序包。 + +对于 Golang HTTP 的服务,我们可能需要重启来升级或者更改某些配置。如果你(像我曾经一样)因为网络服务器对优雅重启很重视就理所当然地认为它(优雅重启)早已实现了,那么这份教程将会对你很有用处。因为在 Golang 中,你需要自己动手来实现。 + +实际上,这里需要解决两个问题。首先是 UNIX 端的优雅重启,即进程无需关闭监听套接字即可自行重启的机制。第二个问题是确保所有进行中的请求被正确完成或超时。 + +## 在不关闭套接字的情况下重启 + +- 派生一个继承监听中的套接字的新进程。 +- 新进程执行初始化并开始接受套接字上的连接。 +- 之后,子进程立即向父进程发送信号,使父进程停止接收连接并终止。 + +### 派生一个新的进程 + +有多种使用 Golang 的库去实现派生进程的方法,在本文中的例子中,我们选择用 [exec.Command](https://golang.org/pkg/os/exec/#Command)。这是因为此函数返回的 [Cmd 结构体](https://golang.org/pkg/os/exec/#Cmd) 具有 `ExtraFiles` 成员,该成员可以使打开的文件(除了 `stdin/err/out` 之外)被新进程继承。 + +看起来如下所示: + +```go +file := netListener.File() // this returns a Dup() +path := "/path/to/executable" +args := []string{ + "-graceful"} + +cmd := exec.Command(path, args...) +cmd.Stdout = os.Stdout +cmd.Stderr = os.Stderr +cmd.ExtraFiles = []*os.File{file} + +err := cmd.Start() +if err != nil { + log.Fatalf("gracefulRestart: Failed to launch, error: %v", err) +} +``` + +在上面的代码中,`netListener` 是指向 [net.Listener](https://golang.org/pkg/net/#Listener) 的指针,`net.Listener` 用于侦听 HTTP 请求。如果你要升级,则 `path` 变量应包含新可执行文件的路径(可能与当前正在运行的文件相同)。 + +上面代码中的关键是 `netListener.File()` 会返回一个文件描述符 [dup(2)](https://pubs.opengroup.org/onlinepubs/009695399/functions/dup.html)。这个文件描述符不会设置 `FD_CLOEXEC` [标识](https://pubs.opengroup.org/onlinepubs/009695399/functions/fcntl.html),这会导致文件在子进程中被关闭(不是我们想要的的情况)。 + +你可能会看到一些示例,通过命令行参数将需要继承的文件描述符传递给子进程,但是 `ExtraFiles` 实现的方式使这些变得没有必要。文档指出 "如果输入 non-nil,输入的切片索引为 i 则读取的文件描述符为 3 + i。” 这意味着在上述代码段中,子进程中继承的文件描述符将始终为 3,因此无需显式传递它(译者注:这一段有点难以理解,ExtraFiles 可以指定额外的文件句柄传递给子进程,也就是可以通过这个方法将 `netListener.File()` 传递给子进程,而子进程通过 `f := os.NewFile(3, "")` 来读取,为什么是 3 呢?因为前面还有几个默认句柄,所以传入额外的句柄需要从 3 开始读取)。 + +最后,`args` 数组包含 `-graceful` 选项:你的程序将需要某种方式来通知子进程这是正常重启的一部分,并且子进程应重新使用套接字,而不是打开新的套接字。还有一种方法是通过环境变量来实现这个功能。 + +### 初始化子进程 + +这是程序启动序列的一部分 + +```go +server := &http.Server{Addr: "0.0.0.0:8888"} + +var gracefulChild bool +var l net.Listever +var err error + +flag.BoolVar(&gracefulChild, "graceful", false, "listen on fd open 3 (internal use only)") + +if gracefulChild { + log.Print("main: Listening to existing file descriptor 3.") + f := os.NewFile(3, "") + l, err = net.FileListener(f) +} else { + log.Print("main: Listening on a new file descriptor.") + l, err = net.Listen("tcp", server.Addr) +} +``` + +### 通知父进程停止 + +至此,子进程已经准备好接收请求,但是在此之前,需要告诉父进程停止接收请求并退出,这可能是这样的: + +```go +if gracefulChild { + parent := syscall.Getppid() + log.Printf("main: Killing parent pid: %v", parent) + syscall.Kill(parent, syscall.SIGTERM) +} + +server.Serve(l) +``` + +### 进行中的请求 完成/超时 + +为此,我们需要使用 [sync.WaitGroup](https://golang.org/pkg/sync/#WaitGroup) 跟踪打开的连接。我们需要给每次新增的连接使用 `wg.Add`,并在每次关闭连接时使用 `wg.Done`。 + +```go +var httpWg sync.WaitGroup +``` + +乍一看,Golang 标准的 http 包没有提供任何对 Accept() 或 Close() 操作的钩子,但是使用接口解决了这个问题。(非常感谢 [Jeff R. Allen](http://nella.org/jra/)的这篇[文章](http://blog.nella.org/zero-downtime-upgrades-of-tcp-servers-in-go/))。 + +这是一个 listener 的示例,它在每个 Accept() 上使用 `httpWg.Add(1)` 计数。首先,我们对 `net.Listener` 进行“子类化”(`stop` 和 `stopped` 的作用将在下文体现): + +```go +type gracefulListener struct { + net.Listener + stop chan error + stopped bool +} +``` + +接下来,我们“覆盖” Accept 方法。(暂时不要考虑 `gracefulConn`,稍后再介绍)。 + +```go +func (gl *gracefulListener) Accept() (c net.Conn, err error) { + c, err = gl.Listener.Accept() + if err != nil { + return + } + + c = gracefulConn{Conn: c} + + httpWg.Add(1) + return +} +``` + +我们还需要一个构造函数: + +```go +func newGracefulListener(l net.Listener) (gl *gracefulListener) { + gl = &gracefulListener{Listener: l, stop: make(chan error)} + Go func() { + _ = <-gl.stop + gl.stopped = true + gl.stop <- gl.Listener.Close() + }() + return +} +``` + +上面的函数启动 Goroutine 的原因是因为在上面的 `Accept()` 中无法完成此操作,因为它会被 `gl.Listener.Accept()` 阻塞。 Goroutine 通过关闭文件描述符来解除阻塞。 + +我们的 `Close()` 方法仅将 `nil` 发送给上述 Goroutine 的 `stop channel` 即可完成其余工作。 + +```go +func (gl *gracefulListener) Close() error { + if gl.stopped { + return syscall.EINVAL + } + gl.stop <- nil + return <-gl.stop +} +``` + +最后,从 `net.TCPListener` 中提取文件描述符。 + +```go +func (gl *gracefulListener) File() *os.File { + tl := gl.Listener.(*net.TCPListener) + fl, _ := tl.File() + return fl +} +``` + +当然,我们还需要一个嵌入了 `net.Conn` 的结构体,该结构体会通过 `httpWg.Done()` 来减少 `Close()` 上的计数: + +```go +type gracefulConn struct { + net.Conn +} + +func (w gracefulConn) Close() error { + httpWg.Done() + return w.Conn.Close() +} +``` + +如果要使用上述优美版本的 Listener,我们需要将 `server.Serve(l)` 行替换为: + +```go +netListener = newGracefulListener(l) +server.Serve(netListener) +``` + +还有一件事。你应避免挂起客户端无意关闭的连接。最好按以下方式创建服务: + +```go +server := &http.Server{ + Addr: "0.0.0.0:8888", + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 16} +``` + +--- + +via: https://grisha.org/blog/2014/06/03/graceful-restart-in-golang/ + +作者:[humblehack](https://twitter.com/humblehack) +译者:[咔叽咔叽](https://github.com/watermelo) +校对:[lxbwolf](https://github.com/lxbwolf) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com) diff --git a/published/tech/20191103-Go-How-Does-the-Garbage-Collector-Mark-the-Memory.md b/published/tech/20191103-Go-How-Does-the-Garbage-Collector-Mark-the-Memory.md new file mode 100644 index 000000000..e4eab4303 --- /dev/null +++ b/published/tech/20191103-Go-How-Does-the-Garbage-Collector-Mark-the-Memory.md @@ -0,0 +1,139 @@ +首发于:https://studygolang.com/articles/25916 + +# Go GC 怎么标记内存? + +![Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-How-Does-the-Garbage-Collector-Mark-the-Memory/00.png) + +ℹ️ 本文基于 *Go 1.13*。关于内存管理的概念的讨论在我的文章 [Go 中的内存管理和分配](https://medium.com/a-journey-with-go/go-memory-management-and-allocation-a7396d430f44) 中有详细的解释。 + +Go GC 的作用是回收不再使用的内存。实现的算法是并发的三色标记和清除回收法。本中文,我们研究三色标记法,以及各个颜色的不同用处。 + +你可以在 Ken Fox 的 [解读垃圾回收算法](https://spin.atomicobject.com/2014/09/03/visualizing-garbage-collection-algorithms/) 中了解更多关于不同垃圾回收机制的信息。 + +## 标记阶段 + +这个阶段浏览内存来了解哪些块儿是在被我们的代码使用和哪些块儿应该被回收。 + +然而,因为 GC 和我们的 Go 程序并行,GC 扫描期间内存中某些对象的状态可能被改变,所以需要一个检测这种可能的变化的方法。为了解决这个潜在的问题,实现了 [写屏障](https://en.wikipedia.org/wiki/Write_barrier) 算法,GC 可以追踪到任何的指针修改。使写屏障生效的唯一条件是短暂终止程序,又名 “Stop the World”。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-How-Does-the-Garbage-Collector-Mark-the-Memory/01.png) + +在进程启动时,Go 也在每个 processor 起了一个标记 worker 来辅助标记内存。 + +然后,当 root 被加入到处理队列中后,标记阶段就开始遍历和用颜色标记内存。 + +为了了解在标记阶段的每一步,我们来看一个简单的程序示例: + +```go +type struct1 struct { + a, b int64 + c, d float64 + e *struct2 +} + +type struct2 struct { + f, g int64 + h, i float64 +} + +func main() { + s1 := allocStruct1() + s2 := allocStruct2() + + func () { + _ = allocStruct2() + }() + + runtime.GC() + + fmt.Printf("s1 = %X, s2 = %X\n", &s1, &s2) +} + +//go:noinline +func allocStruct1() *struct1 { + return &struct1{ + e: allocStruct2(), + } +} + +//go:noinline +func allocStruct2() *struct2 { + return &struct2{} +} +``` + +`struct2` 不包含指针,因此它被储存在一个专门存放不被其他对象引用的对象的 span 中。 + +![不包含指针的结构体储存在专有的 span 中](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-How-Does-the-Garbage-Collector-Mark-the-Memory/02.png) + +这减少了 GC 的工作,因为标记内存时不需要扫描这个 span。 + +分配工作结束后,我们的程序强迫 GC 重复前面的步骤。下面是流程图: + +![扫描内存](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-How-Does-the-Garbage-Collector-Mark-the-Memory/03.png) + +GC 从栈开始,递归地顺着指针找指针指向的对象,遍历内存。扫描到被标记为 `no scan` 的 span 时,停止扫描。然而,这个工作是在多个协程中完成的,每个指针被加入到一个 work pool 中的队列。然后,后台运行的标记 worker 从这个 work pool 中拿到前面出列的 work,扫描这个对象然后把在这个对象里找到的指针加入到队列。 + +![garbage collector work pool](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-How-Does-the-Garbage-Collector-Mark-the-Memory/04.png) + +## 颜色标记 + +worker 需要一种记录哪些内存需要扫描的方法。GC 使用一种 [三色标记算法](https://en.wikipedia.org/wiki/Tracing_garbage_collection#Tri-color_marking),工作流程如下: + +- 开始时,所有对象都被认为是**白色** +- root 对象(栈,堆,全局变量)被标记为**灰色** + +这个初始步骤完成后,GC 会: + +- 选择一个**灰色**的对象,标记为**黑色** +- 追踪这个对象的所有指针,把所有引用的对象标记为**灰色** + +然后,GC 重复以上两步,直到没有对象可被标记。在这一时刻,对象非黑即白,没有灰色。白色的对象表示没有其他对象引用,可以被回收。 + +下面是前面例子的图示: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-How-Does-the-Garbage-Collector-Mark-the-Memory/05.png) + +初始状态下,所有的对象被认为是白色的。然后,遍历到的且被其他对象引用的对象,被标记为灰色。如果一个对象在被标记为 `no scan` 的 span 中,因为它不需要被扫描,所以可以标记为黑色。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-How-Does-the-Garbage-Collector-Mark-the-Memory/06.png) + +现在灰色的对象被加入到扫描队列并被标记为黑色: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-How-Does-the-Garbage-Collector-Mark-the-Memory/07.png) + +对加入到扫描队列的所有对象重复做相同的操作,直到没有对象需要被处理: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-How-Does-the-Garbage-Collector-Mark-the-Memory/08.png) + +处理结束时,黑色对象表示内存中在使用的对象,白色对象是要被回收的对象。我们可以看到,由于 `struct2` 的实例是在一个匿名函数中创建的且不再存在于栈上,因此它是白色的且可以被回收。 + +归功于每一个 span 中的名为 `gcmarkBits` 的 bitmap 属性,三色被原生地实现了,bitmap 对 scan 中相应的 bit 设为 1 来追踪 scan。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-How-Does-the-Garbage-Collector-Mark-the-Memory/09.png) + +我们可以看到,黑色和灰色表示的意义相同。处理的不同之处在于,标记为灰色时是把对象加入到扫描队列,而标记为黑色时,不再扫描。 + +GC 最终 STW,清除每一次写屏障对 work pool 做的改变,继续后续的标记。 + +*你可以在我的文章 [Go GC 怎样监控你的应用](https://medium.com/a-journey-with-go/go-how-does-the-garbage-collector-watch-your-application-dbef99be2c35) 中找到关于并发处理和 GC 的标记阶段更详细的描述*。 + +## runtime 分析器 + +Go 提供的工具使我们可以对每一步进行可视化,观察 GC 在我们的程序中的影响。开启 tracing 运行我们的代码,可以看到前面所有步骤的一个概览。下面是追踪结果: + +![traces of the garbage collector](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-How-Does-the-Garbage-Collector-Mark-the-Memory/10.png) + +标记 worker 的生命周期也可以在追踪结果中以协程等级可视化。下面是在启动之前先在后台等待标记内存的 Goroutine #33 的例子。 + +![marking worker](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-How-Does-the-Garbage-Collector-Mark-the-Memory/11.png) + +--- + +via: https://medium.com/a-journey-with-go/go-how-does-the-garbage-collector-mark-the-memory-72cfc12c6976 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191103-Go-Memory-Management-and-Allocation.md b/published/tech/20191103-Go-Memory-Management-and-Allocation.md new file mode 100644 index 000000000..e54049e30 --- /dev/null +++ b/published/tech/20191103-Go-Memory-Management-and-Allocation.md @@ -0,0 +1,111 @@ +首发于:https://studygolang.com/articles/28436 + +# Go:内存管理分配 + +![Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-Memory-Management-and-Allocation/cover.png) + +ℹ️ *这篇文章基于 Go 1.13。* + +在内存从分配到回收的生命周期中,内存不再被使用的时候,标准库会自动执行 Go 的内存管理。虽然开发者不必操心这些细节,但是 Go 语言所做的底层管理经过了很好的优化,同时有很多有趣的概念。 + +## 堆上的分配 + +内存管理被设计为可以在并发环境快速执行,同时与垃圾收集器集成在了一起。从一个简单的例子开始: + +```go +package main + +type smallStruct struct { + a, b int64 + c, d float64 +} + +func main() { + smallAllocation() +} + +//go:noinline +func smallAllocation() *smallStruct { + return &smallStruct{} +} +``` + +注释 `//go:noinline` 会禁用内联,以避免内联通过移除函数的方式优化这段代码,从而造成最终没有分配内存的情况出现。 + +通过运行逃逸分析命令 `go tool compile "-m" main.go` 可以确认 Go 执行了的分配: + +``` +main.go:14:9: &smallStruct literal escapes to heap +``` + +借助 `go tool compile -S main.go` 命令得到这段程序的汇编代码,可以同样明确地向我们展示具体的分配细节: +``` +0x001d 00029 (main.go:14) LEAQ type."".smallStruct(SB), AX +0x0024 00036 (main.go:14) PCDATA $0, $0 +0x0024 00036 (main.go:14) MOVQ AX, (SP) +0x0028 00040 (main.go:14) CALL runtime.newobject(SB) +``` + +函数 `newobject` 是用于新对象的分配以及代理 `mallocgc` 的内置函数,该函数在堆上管理这些内存。在 Go 语言中有两种策略,一种用于较小的内存空间的分配,而另一种则用于较大的内存空间的分配。 + +## 较小内存空间的分配策略 + +对于小于 32kb 的,较小的内存空间的分配策略,Go 会从被叫做 `mcache` 的本地缓存中尝试获取内存。 这个缓存持有一个被叫做 `mspan` 的内存块(span ,32kb 大小的内存块)列表, mspan 包含着可用于分配的内存: + +![用 mcache 分配内存](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-Memory-Management-and-Allocation/allocation-with-mcache.png) + +每个线程 `M` 被分配一个处理器 `P`,并且一次最多处理一个 goroutine。在分配内存时,当前的 Goroutine 会使用它当前的 P 的本地缓存,在 span 链表中寻找第一个可用的空闲对象。使用这种本地缓存不需要锁操作,从而分配效率更高。 + +span 链表被划分为 8 字节大小到 32k 字节大小的,约 70 个的大小等级,每个等级可以存储不同大小的对象。 + +![span 的大小等级](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-Memory-Management-and-Allocation/span-size-classes.png) + +每个 span 链表会存在两份:一个链表用于不包含指针的对象而另一个用于包含指针的对象。这种区别使得垃圾收集器更加轻松,因为它不必扫描不包含任何指针的 span。 + +在我们前面的例子中,结构体的大小是 32 字节,因此它会适合于 32 字节的 span : + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-Memory-Management-and-Allocation/previous-example.png) + +现在,我们可能会好奇,如果在分配期间 span 没有空闲的插槽会发生什么。Go 维护着每个大小等级的 span 的中央链表,该中央链表被叫做 `mcentral`,其中维护着包含空闲对象的 span 和没有空闲对象的 span : + +![span 的中央链表](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-Memory-Management-and-Allocation/central-lists-of-spans.png) + +`mcentral` 维护着 span 的双向链表;其中每个链表节点有着指向前一个 span 和后一个 span 的引用。非空链表中的 span 可能包含着一些正在使用的内存,“非空”表示在链表中至少有一个空闲的插槽可供分配。当垃圾收集器清理内存时,可能会清理一部分 span,将这部分标记为不再使用,并将其放回非空链表。 + +我们的程序现在可以在没有插槽的情况下向中央链表请求 span : + +![从 mcentral 中替换 span ](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-Memory-Management-and-Allocation/span-replacement-from-mcentral.png) + +如果空链表中没有可用的 span,Go 需要为中央链表获取新的 span 。新的 span 会从堆上分配,并链接到中央链表上: + +![从堆上分配 span ](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-Memory-Management-and-Allocation/span-allocation-from-the-heap.png) + +堆会在需要的时候从系统( OS )获取内存,如果需要更多的内存,堆会分配一个叫做 `arena` 的大块内存,在 64 位架构下为 64Mb,在其他架构下大多为 4Mb。arena 同样适用 span 映射内存。 + +![堆由 arena 组成](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-Memory-Management-and-Allocation/heap-is-composed-by-arenas.png) + +## 较大内存空间的分配策略 + +Go 并不适用本地缓存来管理较大的内存空间分配。对于超过 32kb 的分配,会向上取整到页的大小,并直接从堆上分配。 + +![直接从堆上进行大的内存空间分配](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-Memory-Management-and-Allocation/large-allocation-directly-from-the-heap.png) + +## 全景图 + +现在我们对内存分配的时候发生了什么有了更好的认识。现在将所有的组成部分放在一起来得到完整的图画。 + +![内存分配的组成](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191103-Go-Memory-Management-and-Allocation/components-of-the-memory-allocation.png) + +## 灵感来源 + +该内存分配最初基于 TCMalloc,一个 Google 创建的,并发环境优化的内存分配器。这个 [TCMalloc 的文档](http://goog-perftools.sourceforge.net/doc/tcmalloc.html)值得阅读;你会发现上面解释过的概念。 + +--- + +via: https://medium.com/a-journey-with-go/go-memory-management-and-allocation-a7396d430f44 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[dust347](https://github.com/dust347) +校对:[@unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191105-I-wrote-Gocache-a-complete-and-extensible-Go-cache-library.md b/published/tech/20191105-I-wrote-Gocache-a-complete-and-extensible-Go-cache-library.md new file mode 100644 index 000000000..db977061e --- /dev/null +++ b/published/tech/20191105-I-wrote-Gocache-a-complete-and-extensible-Go-cache-library.md @@ -0,0 +1,239 @@ +首发于:https://studygolang.com/articles/25283 + +# Gocache:一个功能齐全且易于扩展的 Go 缓存库 + +在先前几周的时候,我完成了 [Gocache](https://github.com/eko/gocache),对于 Go 开发者而言,它是功能齐全且易于扩展的。 + +这个库的设计目的是为了解决在使用缓存或者使用多种(多级)缓存时所遇到的问题,它为缓存方案制定了一个标准。 + +## 背景 + +当我一开始在为 GraphQL 的 Go 项目构建缓存时,该项目已经包含了一套有简单 API 的内存缓存,还使用了另外一套有不同 API 的内存缓存和加载缓存数据的代码,它们实际上都是在只做了同一件事:缓存。 + +后来,我们又有了另一个需求:除了内存缓存外,我们还想添加一套基于 Redis 集群的分布式缓存,其主要目的是为了在新版本上线时,避免 Kubernetes 的 Pods 使用的缓存为空。 + +于是创造 Gocache 的契机出现了,是时候用一套统一的规则来管理多种缓存方案了,不管是内存、Redis、Memcache 或者其他任何形式的缓存。 + +哦,还不止这些,我们还希望缓存的数据可以被 Metrics 监控(后来被我们的 Prometheus 替代)。(译者注:Metrics 和 Prometheus 均是监控工具。) + +Gocache 项目诞生了:https://github.com/eko/gocache 。 + +## 存储接口 + +首先,当你准备缓存一些数据时,你必须选择缓存的存储方式:简单的直接放进内存?使用 Redis 或者 Memcache?或者其它某种形式的存储。 + +目前,Gocache 已经实现了以下存储方案: + +* **Bigcache**: 简单的内存存储。 +* **Ristretto**: 由 DGraph 提供的内存存储。 +* **Memcache**: 基于 bradfitz/gomemcache 的 Memcache 存储。 +* **Redis**: 基于 go-redis/redis 的 Redis 存储。 + +所有的存储方案都实现了一个非常简单的接口: + +``` go +type StoreInterface interface { + Get(key interface{}) (interface{}, error) + Set(key interface{}, value interface{}, options *Options) error + Delete(key interface{}) error + Invalidate(options InvalidateOptions) error + Clear() error + GetType() string +} +``` + +这个接口展示了可以对存储器执行的所有操作,每个操作只调用了存储器客户端的必要方法。 + +这些存储器都有不同的配置,具体配置取决于实现存储器的客户端,举个例子,以下为初始化 Memcache 存储器的示例: + +``` go +store := store.NewMemcache( + memcache.New("10.0.0.1:11211", "10.0.0.2:11211", "10.0.0.3:11212"), + &store.Options{ + Expiration: 10*time.Second, + }, +) +``` + +然后,必须将初始化存储器的代码放进缓存的构造函数中。 + +## 缓存接口 + +以下为缓存接口,缓存接口和存储接口是一样的,毕竟,缓存就是对存储器做一些操作。 + +``` go +type CacheInterface interface { + Get(key interface{}) (interface{}, error) + Set(key, object interface{}, options *store.Options) error + Delete(key interface{}) error + Invalidate(options store.InvalidateOptions) error + Clear() error + GetType() string +} +``` + +该接口包含了需要对缓存数据进行的所有必要操作:Set,Get,Delete,使某条缓存失效,清空缓存。如果需要的话,还可以使用 GetType 方法获取缓存类型。 + +缓存接口已有以下实现: + +* **Cache**: 基础版,直接操作存储器。 +* **Chain**: 链式缓存,它允许使用多级缓存(项目中可能同时存在内存缓存,Redis 缓存等等)。 +* **Loadable**: 可自动加载数据的缓存,它可以指定回调函数,在缓存过期或失效的时候,会自动通过回调函数将数据加载进缓存中。 +* **Metric**: 内嵌监控的缓存,它会收集缓存的一些指标,比如 Set、Get、失效和成功的缓存数量。 + +最棒的是:这些缓存器都实现了相同的接口,所以它们可以很容易地相互组合。你的缓存可以同时具有链式、可自动加载数据、包含监控等特性。 + +还记得吗?我们想要简单的 API,以下为使用 Memcache 的示例: + +``` go +memcacheStore := store.NewMemcache( + memcache.New("10.0.0.1:11211", "10.0.0.2:11211", "10.0.0.3:11212"), + &store.Options{ + Expiration: 10*time.Second, + }, +) + +cacheManager := cache.New(memcacheStore) +err := cacheManager.Set("my-key", []byte("my-value"), &cache.Options{ + Expiration: 15*time.Second, // 设置过期时间 +}) +if err != nil { + panic(err) +} + +value := cacheManager.Get("my-key") + +cacheManager.Delete("my-key") + +cacheManager.Clear() // 清空缓存 +``` + +现在,假设你想要将已有的缓存修改为一个链式缓存,该缓存包含 Ristretto(内存型)和 Redis 集群,并且具备缓存数据序列化和监控特性: + +``` go +// 初始化 Ristretto 和 Redis 客户端 +ristrettoCache, err := ristretto.NewCache(&ristretto.Config{NumCounters: 1000, MaxCost: 100, BufferItems: 64}) +if err != nil { + panic(err) +} + +redisClient := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379"}) + +// 初始化存储器 +ristrettoStore := store.NewRistretto(ristrettoCache, nil) +redisStore := store.NewRedis(redisClient, &cache.Options{Expiration: 5*time.Second}) + +// 初始化 Prometheus 监控 +promMetrics := metrics.NewPrometheus("my-amazing-app") + +// 初始化链式缓存 +cacheManager := cache.NewMetric(promMetrics, cache.NewChain( + cache.New(ristrettoStore), + cache.New(redisStore), +)) + +// 初始化序列化工具 +marshal := marshaler.New(cacheManager) + +key := BookQuery{Slug: "my-test-amazing-book"} +value := Book{ID: 1, Name: "My test amazing book", Slug: "my-test-amazing-book"} + +// 插入缓存 +err = marshal.Set(key, value) +if err != nil { + panic(err) +} + +returnedValue, err := marshal.Get(key, new(Book)) +if err != nil { + panic(err) +} + +// Then, do what you want with the value +``` + +我们不对序列化做过多的讨论,因为这个是 Gocache 的另外一个特性:提供一套在存储和取出缓存对象时可以自动序列化和反序列化缓存对象的工具。 + +该特性在使用对象作为缓存 Key 时会很有用,它省去了在代码中手动转换对象的操作。 + +所有的这些特性:包含内存型和 Redis 的链式缓存、包含 Prometheus 监控功能和自动序列化功能,都可以在 20 行左右的代码里完成。 + +## 定制你自己的缓存 + +如果你想定制自己的缓存也很容易。 + +以下示例展示了如何给对缓存的每个操作添加日志(这不是一个好主意,只是作为示例): + +``` go +package customcache + +import ( + "log" + + "github.com/eko/gocache/cache" + "github.com/eko/gocache/store" +) + +const LoggableType = "loggable" + +type LoggableCache struct { + cache cache.CacheInterface +} + +func NewLoggable(cache cache.CacheInterface) *LoggableCache { + return &LoggableCache{ + cache: cache, + } +} + +func (c *LoggableCache) Get(key interface{}) (interface{}, error) { + log.Print("Get some data...") + return c.cache.Get(key) +} + +func (c *LoggableCache) Set(key, object interface{}, options *store.Options) error { + log.Print("Set some data...") + return c.cache.Set(key, object, options) +} + +func (c *LoggableCache) Delete(key interface{}) error { + log.Print("Delete some data...") + return c.cache.Delete(key) +} + +func (c *LoggableCache) Invalidate(options store.InvalidateOptions) error { + log.Print("Invalidate some data...") + return c.cache.Invalidate(options) +} + +func (c *LoggableCache) Clear() error { + log.Print("Clear some data...") + return c.cache.Clear() +} + +func (c *LoggableCache) GetType() string { + return LoggableType +} +``` + +通过同样的方法,你也可以自己实现存储接口。 + +如果你认为其他人也可以从你的缓存实现中获得启发,请不要犹豫,直接在项目中发起合并请求。通过共同讨论你的想法,我们会提供一个功能更加强大的缓存库。 + +## 结论 + +在构建这个库的过程中,我还尝试改进了 Go 的社区工具。 + +希望你喜欢这篇博客,如果有需要的话,我非常乐意和你讨论需求或者你的想法。 + +最后,如果你在缓存方面需要帮助,你可以随时通过 Twitter 或者邮件联系我。 + +--- + +via: https://vincent.composieux.fr/article/i-wrote-gocache-a-complete-and-extensible-go-cache-library + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[beiping96](https://github.com/beiping96) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191107-Migrating-From-Travis-To-Github-Actions.md b/published/tech/20191107-Migrating-From-Travis-To-Github-Actions.md new file mode 100644 index 000000000..ca491d305 --- /dev/null +++ b/published/tech/20191107-Migrating-From-Travis-To-Github-Actions.md @@ -0,0 +1,186 @@ +首发于:https://studygolang.com/articles/25290 + +# 从 Travis 迁移至 GitHub Actions + +周末的时候,我决定将我 Go 语言的开源项目 [Flipt](https://github.com/markphelps/flipt) 的 CI 流程从 TravisCI 转移到 GitHub Actions,我想要替换我现有的 CI,并尝试使用新的 GitHub Actions 将手动发版过程自动化。 + +*说明*:我在 GitHub 工作,但不在 Actions 团队。我想在我的开源项目中配置 Actions,并且不从 Actions 团队或 GitHub 的任何人那里获得任何帮助。我没有被 Github 的同事要求写这篇文章,我的目的很简单,以一个用户的经验来使用这个平台。仅代表个人观点和想法。 + +不用说,经过我几个小时的调试,我成功了[twitter 链接](https://twitter.com/mark_a_phelps/status/1172935552947118081?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1172935552947118081&ref_url=https%3A%2F%2Fmarkphelps.me%2F2019%2F09%2Fmigrating-from-travis-to-github-actions%2F)。 + +![推特截图](https://raw.githubusercontent.com/studygolang/gctt-images/master/migrating-from-travis-to-action/BDy3YCr5ZwgEbdL.png) + +## 管道 + +我不打算对比 workflow (流程) 、job(任务)、step(步骤) 等细节, GitHub 有广泛的文档来介绍 Actions 的 [用法](https://help.github.com/en/articles/workflow-syntax-for-github-actions) 和 [概念](https://help.github.com/en/articles/about-github-actions#core-concepts-for-github-actions),我认为我想要的是很普通的一个 CI/CD 流程: + +. push 代码到分支后运行一些单元测试,最好能够使用 Go 的多个版本 +. 在 PR 上,我还希望运行一些更广泛的集成测试,用来测试面向公众的 API 和 CLI +. 推送 tag 后,我想触发 [goreleaser](https://github.com/goreleaser/goreleaser) 来构建一个 Docker 镜像并推送到 [Docker Hub](https://hub.docker.com/r/markphelps/flipt),同时打包一个发版的压缩文件 +. 在新版本更新文档时更新 [文档网站](https://flipt.dev/) + +前两个步骤主要的 TravisCI 工作是在这个 [config 文件](https://github.com/markphelps/flipt/blob/90bafa834aec29cdaa3620b8ea30aa89466fe7d0/.travis.yml)配置的,虽然有一些差异: + +1. 我只测试了 Go 一个版本 (1.12.x),我知道我可以使用 travis-ci 的 [matrix](https://docs.travis-ci.com/user/build-matrix/)设置来测试多个版本,只是我从来没有这样去用。 +2. 我只针对 PR 在 Postgres DB 实体环境上运行测试, + +我缺少的是用于实际构建发版和更新文档的 CD (持续部署)部分。我在本地机器上运行脚本依赖于设置一些需要保密的环境变量,依然是一个手动操作过程。这不是最理想的情况。 + +## 容易实现的目标 + +我创建的第一个 action 实际上是自动更改文档部分。这一部分会被移动到管道作业流的最后一步,但也是能正常运行的最简单的一步。 + +它主要由两个文件组成,一个 [Dockerfile](https://github.com/markphelps/flipt/blob/4157e9b154a01b09a4eb60a8e43484cd3928fc89/.github/actions/publish-docs/Dockerfile) 用于安装必要的依赖项,另一个[脚本](https://github.com/markphelps/flipt/blob/master/.github/actions/publish-docs/entrypoint.sh) 用于运行构建和部署步骤。 +我使用 [mkdocs](https://www.mkdocs.org/) 来构建文档并发布到 [GitHub pages](https://help.github.com/en/articles/creating-project-pages-using-the-command-line)。 + +我(最终)把它连接起来作为发布工作流程的最后一步: + +```bash +name: Publish Docs +uses: ./.github/actions/publish-docs +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +``` + +它将通知 Actions 我希望使用 [local action](https://help.github.com/en/articles/workflow-syntax-for-github-actions#example-using-action-in-the-same-repository-as-the-workflow) 存在的 action,并设置 `GITHUB_TOKEN` 环境变量,该变量是推送到 GitHub pages 所必需的。 + +## 这些繁琐的测试 + +接下来我做的是让管道的单元测试部分运作起来。因为 [Flipt](https://github.com/markphelps/flipt) 是一个服务端应用程序,所以我目前只针对 Linux 环境,因此我不需要测试 Windows 或 MacOS 环境。虽然我知道 Actions 很酷并且也支持 😉。 + +然而,我确实希望能够使用多个版本的 Go 进行测试(撰写本文时为 1.12 和 1.13)。Actions 的 [matrix strategy]矩阵策略特性让这一切变得超级简单。 + +对于我的 workflow 工作流,它看起来像这样: + +```bash +test: + name: Test + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + go: ['1.12', '1.13'] +``` + +这里设置两个作业并行运行,运行下面的所有步骤,其中一个 `{{ matrix.go }}` 设置为 `1.12`,另一个设置为 `1.13`。 + +稍后在工作流文件中,我创建了一个步骤,这些值来将被用来在虚拟机上安装可用版本的 Go: + +```bash +steps: +- name: Setup Go + uses: actions/setup-go@v1 + with: + go-version: ${{ matrix.go }} + id: go +``` + +它使用 [actions/setup-go](https://github.com/actions/setup-go) action 来安装我们指定的 Go 版本。这很酷。 + +实际上,我几乎立刻就看到了使用多个 Go 版本运行测试的好处,因为 Go 1.13 增 加了一些新功能,我的一些测试代码已经无法通过。 + +查看发布说明: + +> 测试 flags 标识现在被注册到新的 Init 函数中,该函数会在测试生成的主函数调用。因此,测试 flags 标识现在只在运行测试二进制文件时注册,并且包名为 flag。包初始化期间的解析可能导致测试失败。 + +说明太长不建议阅读。我曾经在我的一个 [测试](https://github.com/markphelps/flipt/blob/fdf45bff66c325d702b54ae334e53ae8e3cac176/storage/db_test.go#L88) 中使用 init 函数来打开一些调试日志,如果设置了一个标志的话。事实证明这在 Go 1.13.1 会出现 [问题](https://github.com/golang/go/issues/31859)。 + +我不认为在我真正尝试更新 Flipt 到 Go 1.13 之前能发现这个问题,目前我能够通过完全的测试在早期发现这个问题,这很酷。 + +## 不愿多谈的问题 + +我在前面提到过,我还希望使用正式环境的 Postgres 数据库中运行单元测试。这是因为 Flipt 同时支持 [SQLite 和 Postgres](https://github.com/markphelps/flipt#database://github.com/markphelps/flipt#databases),我希望对代码进行同等的测试。 + +幸运的是运行 Actions 构建操作的 Ubuntu 虚拟机似乎已经安装了 SQLite 所需的库,但是它们似乎没有安装 Postgres,这点与 Travis 不同。你可以在[文档](https://help.github.com/en/articles/software-in-virtual-environments-for-github-actions)中看到每个 VM 的所有已安装软件/库的列表。 + +这意味着我需要想办法找到一个 Postgres 服务来运行我的构建,这样我才能完成测试。 + +我最初尝试使用的一个步骤是使用 Docker 容器内使用 `docker run` 命令来运行 Postgres 。然而我很快发现 Actions 有一个针对这类问题的内置解决方案 - [services](https://help.github.com/en/articles/workflow-syntax-for-github-actions#jobsjob_idservices)! + +事实证明,`services` 指令正是我所需要的: + +```bash +services: + postgres: + image: postgres:11 + ports: + - 5432:5432 + env: + POSTGRES_DB: flipt_test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: '' +``` + +这与我在 Docker 中通过在容器中运行 Postgres 所做的事情是一样的,但是这里是通过 Actions 来管理的。 + +## Bats 和 REST API + +更进一步,测试管道一直存在于集成测试里。在这我希望能够验证 Flipt 能完成 `public API` 的方面所需要做的事情。我希望 Flipt 的 REST API 以及它的 CLI 都是公开的,因此应该对它们进行彻底的测试并防止版本回退。 + +幸运的是使用诸如 [bats](https://github.com/sstephenson/bats/) 之类的工具,CLI 的测试变得相当容易。我有一些现有的正在使用的 bats 测试 [脚本代码](https://github.com/markphelps/flipt/blob/4157e9b154a01b09a4eb60a8e43484cd3928fc89/script/test/cli.bats) 运行在 Travis 构建中,所以我只需要找到一种方法让他们运行在 Actions 上即可。 + +同样,看起来 Actions 的虚拟机并没有安装 bats,但是 GitHub Actions 的 fork 版本似乎已经意识到到了这一点,可以构建了一个你可以在工作流程中引用的 [bats action](https://github.com/actions/bin/tree/master/bats)。我就是这么做的: + +```bash +- name: Test CLI + uses: actions/bin/bats@master + with: + args: ./script/test/*.bats +``` + +在 Linux VM 中的构建二进制文件之前,我还有一个步骤,由这个 bats action 来调用它来测试 CLI 输入/输出。 + +集成测试的最后一部分是测试 REST API。我之前发现了一个很酷的叫 [shakedown](https://github.com/robwhitby/shakedown) 的 bash 库,它让 HTTP 测试变得轻而易举。 + +因为 VM 虚拟机似乎已经安装了所需的依赖,我最初尝试在原来的 VM 上运行这些测试,但是我在彻底地完成运行测试时遇到了一些问题,所以我决定迁移到一个“干净的环境” - 只在容器中运行测试。 + +在对不同的基础 Docker 镜像进行了一些的修改并安装了必要的依赖项之后,我最终通过安装正确的工具构建了自己的 action,从而使 shakedown 测试能够正常工作。 + +## 愉快的发版 + +最后,管道的最后一部分是建立发版: + +- 为 *nix 创建 tarball 文件 +- 创建一个 Docker 镜像 +- 推送 tarball 文件到 GitHub 并且发布新的版本 +- 创建 Tag 版本推送 Docker 镜像到 Docker Hub + +幸运的是 [goreleaser](https://goreleaser.com/) 已经为此做了 100% 工作! 我所需要做的就是在管道中的最后一步为它提供所需的环境变量,并使用正确的参数调用它。 + +我已经在本地[使用脚本](https://github.com/markphelps/flipt/blob/c82b47b7522caf80bc3f5219ea62e9e37c416dd2/script/build/release)运行,这意味着在调用脚本之前,我必须在本地机器上设置 `GITHUB_TOKEN`、`DOCKER_USERNAME` 和 `DOCKER_PASSWORD`。 + +为了将这个过程转移到 GitHub Actions 操作,我需要一种安全的方法来存储这些值并将它们注入到工作流中。幸运的是 GitHub 也为我们提供了对[保密](https://help.github.com/en/articles/virtual-environments-for-github-actions#creating-and-using-secrets-encrypted-variables)的  支持: + +```bash +- name: Release + run: ./script/build/release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} +``` + +这段代码展示了如何引用 `secrets` 并将它们设置为脚本运行时使用的环境变量。这使我可以通过 Actions 运行 goreleaser,而不必担心这些保密文件被暴露在日志或仓库本身中。 + +## 小结 + +如果你决定迁移你的 pipelines 管道,这里有一些 ProTips,可以帮助你: + +1. **从简单的开始**。不要试图一下就替换掉整个 CI/CD 方案。看看是否有一些可以先迁移的非关键任务。 +2. **保证现有 CI 系统正常运行**。这个不用说,不要删除你的 `travis.yml` 文件,直到你确信新的 Actions 设置一切运行正常。 +3. **优先寻找现有的解决方案**。Actions 社区中已经有很多很酷的东西,包括 [github/ Actions](https://github.com/actions) 项目。在尝试创建自己的特定任务之前先查看一下,你会发现有可能已经存在了。 +4. **阅读文档**。认真地说文档有丰富的信息,可能会帮助你弄清楚如何去做你想做的事情,它能解决问题并且省下很多时间。 + +正如你可能猜到的那样,使用 Actions 设置完美的 CI/CD 管道流需要一些工作,这主要需要阅读文档。每当我遇到困难的时候,最终都是因为我不理解这个系统是如何工作的。我欣赏 GitHub Actions 提供的扩展性和强大功能,因为你可以正确地用它做任何事情。这伴随着需要学习稍微不同的语法和一些规范,但我认为好处远远大于缺点。 + +我引用的所有工作流文件都可以在[这里](https://github.com/markphelps/flipt/tree/master/.github/workflows)找到。 + +--- + +via: https://www.markphelps.me/2019/09/migrating-from-travis-to-github-actions/ + +作者:[Mark Phelps](https://www.markphelps.me/) +译者:[M1seRy](https://github.com/M1seRy) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191107-go-modules-v2-and-beyond.md b/published/tech/20191107-go-modules-v2-and-beyond.md new file mode 100644 index 000000000..80b74ae4f --- /dev/null +++ b/published/tech/20191107-go-modules-v2-and-beyond.md @@ -0,0 +1,151 @@ +首发于:https://studygolang.com/articles/25130 + +# Go Modules: v2 及更高版本 + +## 简介 + +本文是 Go modules 系统的第四部分 + +- Part 1: [使用 Go Modules](https://blog.golang.org/using-go-modules)  [译文](https://studygolang.com/articles/19334) +- Part 2: [迁移到 Go Modules](https://blog.golang.org/migrating-to-go-modules)  [译文](https://studygolang.com/articles/23133) +- Part 3: [发布 Go Modules](https://blog.golang.org/publishing-go-modules) [译文](https://studygolang.com/articles/25129) +- Part 4: [Go Modules : v2 及更高版本](https://blog.golang.org/v2-go-modules) (本文)  + +随着成功的项目逐渐成熟以及新需求的加入,早期的功能和设计决策可能不再适用。 开发者们可能希望通过删除废弃使用的功能、重命名类型或将复杂的程序拆分为可管理的小块来融入他们的经验教训。这种类型的变更要求下游用户进行更改才能将其代码迁移到新的 API,因此在没有认真考虑收益成本比重的情况下,不应进行这种变更。 + +对于还在早期开发阶段的项目(主版本号是 `v0`),用户会期望偶尔的重大变更。对于声称已经稳定的项目(主版本是 `v1` 或者更高版本),必须在新的主版本进行重大变更。这篇文章探讨了主版本语义、如何创建并发布新的主版本以及如何维护一个 Go Modules 的多个主版本。 + +## 主版本和模块路径 + +模块在 Go 中确定了一个重要的原则,即 “[导入兼容性规则](https://research.swtch.com/vgo-import)” + +> 如果旧包和新包的导入路径相同,新包必须向后兼容旧的包 + +根据这条原则,一个软件包新的主版本没有向后兼容以前的版本。这意味着这个软件包新的主版本必须使用和之前版本不同的模块路径。从 `v2` 开始,主版本号必须出现在模块路径的结尾(在 go.mod 文件的 `module` 语句中声明)。例如,当模块 `github.com/googleapis/gax-go` 的开发者们开发完 `v2` ,他们用了新的模块路径 `github.com/googleapis/gax-go/v2` 。想要使用 `v2` 的用户必须把他们的包导入和模块要求更改为 ``github.com/googleapis/gax-go/v2``  + +需要主版本号后缀是 Go 模块和大多数其他依赖管理系统不同的方式之一。后缀用于解决[菱形依赖问题](https://research.swtch.com/vgo-import#dependency_story)。在 Go 模块之前,[gopkg.in](http://gopkg.in/) 允许软件包维护者遵循我们现在称为导入兼容性规则的内容。使用 gopkg.in 时,如果你依赖一个导入了 `gopkg.in/yaml.v1` 的包以及另一个导入了 `gopkg.in/yaml.v2` 的包,这不会发生冲突,因为两个 `yaml` 包有着不同的导入路径(它们使用和 Go Modules 类似的版本后缀)。由于 gopkg.in 和 Go Modules 共享相同的版本号后缀方法,因此 Go 命令接受 `gopkg.in/yaml.v2` 中的 `.v2` 作为有效的版本号。这是一个为了和 gopkg.in 兼容的特殊情况,在其他域托管的模块需要使用像 `/v2` 这样的斜杠后缀。 + +## 主版本策略 + +推荐的策略是在以主版本后缀命名的目录中开发 `v2+` 模块。 + +```bash +github.com/googleapis/gax-go @ master branch +/go.mod → module github.com/googleapis/gax-go +/v2/go.mod → module github.com/googleapis/gax-go/v2 +``` + +这种方式与不支持 Go Modules 的一些工具兼容:仓库中的文件路径与 `GOPATH` 模式下 `go get` 命令预期的路径匹配。这一策略也允许所有的主版本一起在不同的目录中开发。 + +其他的策略可能是将主版本放置在单独的分支上。然而,如果 `v2+` 的源代码在仓库的默认分支上(一般是 master),不支持版本的工具(包括 GOPATH 模式下的 Go 命令)可能无法区分不同的主版本。 + +本文中的示例遵循主版本子目录策略,所以提供了最大的兼容性。我们建议模块的作者遵循这种策略,只要他们还有用户在使用 `GOPATH` 模式开发。 + +## 发布 v2 及更高版本 + +这篇文章以 `github.com/googleapis/gax-go` 为例: + +```bash +$ pwd +/tmp/gax-go +$ ls +CODE_OF_CONDUCT.md call_option.go internal +CONTRIBUTING.md gax.go invoke.go +LICENSE go.mod tools.go +README.md go.sum RELEASING.md +header.go +$ cat go.mod +module github.com/googleapis/gax-go + +go 1.9 + +require ( + github.com/golang/protobuf v1.3.1 + golang.org/x/exp v0.0.0-20190221220918-438050ddec5e + golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3 + golang.org/x/tools v0.0.0-20190114222345-bf090417da8b + google.golang.org/grpc v1.19.0 + honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099 +) +$ +``` + +要开始开发 `github.com/googleapis/gax-go` 的 `v2` 版本,我们将创建一个新的 `v2/` 目录并将包的内容复制到该目录中。 + +```bash +$ mkdir v2 +$ cp *.go v2/ +building file list ... done +call_option.go +gax.go +header.go +invoke.go +tools.go + +sent 10588 bytes received 130 bytes 21436.00 bytes/sec +total size is 10208 speedup is 0.95 +$ +``` + +现在,我们通过复制当前的 `go.mod` 文件并且在 module 路径上添加 `/v2` 后缀来创建属于 v2 的 `go.mod` 文件。 + +```bash +$ cp go.mod v2/go.mod +$ Go mod edit -module github.com/googleapis/gax-go/v2 v2/go.mod +$ +``` + +注意: `v2` 版本被视为与 `v0 / v1` 版本分开的模块,两者可以共存于同一构建中。因此,如果你的 `v2+` 模块具有多个软件包,你应该更新它们使用新的 `/v2` 导入路径,否则,你的 `v2+` 模块会依赖你的 `v0 / v1` 模块。要升级所有 `github.com/my/project` 为 `github.com/my/project/v2` ,可以使用 `find` 和 `sed` 命令: + +```bash +$ find . -type f \ + -name '*.go' \ + -exec sed -i -e 's,github.com/my/project,github.com/my/project/v2,g' {} \; +$ +``` + +现在我们有了一个 `v2` 模块,但是我们要在版本发布之前进行实验并进行修改。在我们发布 `v2.0.0` (或者其他没有预发布后缀的版本)之前,我们可以进行开发并且可以做出重大变更,就如同我们决定实现新 API 一样。  如果我们希望用户能够在正式发布新 API 之前对其进行试验,可以选择发布 `v2` 预发布版本: + +```bash +$ Git tag v2.0.0-alpha.1 +$ Git push origin v2.0.0-alpha.1 +$ +``` + +一旦我们对 `v2` API 感到满意并且确定不会再有别的重大变更,我们可以打上 Git 标记 `v2.0.0` 。 + +```bash +$ Git tag v2.0.0 +$ Git push origin v2.0.0 +$ +``` + +到那时,就有两个主版本需要维护。向后兼容的更改和错误修复使用新的次版本或者补丁版本发布(比如 `v1.1.0` , `v2.0.1` 等)。 + +## 总结 + +主版本变更会带来开发和维护的开销,并且需要下游用户的额外付出才能迁移。越大的项目中这种主版本变更的开销就越大。只有在确定了令人信服的理由之后,才应该进行主版本变更。一旦确定了令人信服的重大变更原因,我们建议在 master 分支进行多个主版本的开发,因为这样能与各种现有工具兼容。 + +对 `v1+` 模块的重大变更应该始终发生在新的 `vN+1` 模块中。一个新模块发布时,对于维护者和需要迁移到这个新软件包的用户来说意味着更多的工作。因此,维护人员应该在发布稳定版本之前对其 API 进行验证,并仔细考虑在 `v1` 版本之后是否确有必要进行重大变更。 + +## 相关文章 + +- [发布 Go Modules](https://blog.golang.org/publishing-go-modules) +- [启用模块镜像和校验数据库](https://blog.golang.org/module-mirror-launch) +- [迁移到 Go Modules](https://blog.golang.org/migrating-to-go-modules) +- [使用 Go Modules](https://blog.golang.org/using-go-modules) +- [2019 年的 Go Modules](https://blog.golang.org/modules2019) +- [Go 中软件包版本控制的建议](https://blog.golang.org/versioning-proposal) +- [封面故事](https://blog.golang.org/cover) +- [App Engine SDK 和工作区](https://blog.golang.org/the-app-engine-sdk-and-workspaces-gopath) +- [组织 Go 代码](https://blog.golang.org/organizing-go-code) + +--- + +via: https://blog.golang.org/v2-go-modules + +作者:[Jean de Klerk 和 Tyler Bui-Palsulich](https://blog.golang.org) +译者:[befovy](https://github.com/befovy) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191109-Go-Memory-Management-and-Memory-Sweep.md b/published/tech/20191109-Go-Memory-Management-and-Memory-Sweep.md new file mode 100644 index 000000000..877cf648d --- /dev/null +++ b/published/tech/20191109-Go-Memory-Management-and-Memory-Sweep.md @@ -0,0 +1,94 @@ +首发于:https://studygolang.com/articles/27144 + +# Go:内存管理与内存清理 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191109-Go-Memory-Management-and-Memory-Sweep/01.png) +

Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.

+ +*这篇文章基于 Go 1.13 版本。有关内存管理的讨论在我的文章 ”[Go:内存管理与分配](https://studygolang.com/articles/28436) ” 中有解释。* + +清理内存是一个过程,它能够让 Go 知道哪些内存段最近可用于分配。但是,它并不会使用将位置 0 的方式来清理内存。 + +## 将内存置 0 + +将内存置 0 的过程 —— 就是把内存段中的所有位赋值为 0 —— 是在分配过程中即时执行的。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191109-Go-Memory-Management-and-Memory-Sweep/02.png) +

Zeroing the memory

+ +但是,我们可能想知道 Go 采用什么样的策略去知道哪些对象能够用于分配。由于在每个范围内有一个内部位图 `allocBits`,Go 实际上会追踪那些空闲的对象。让我们从初始态开始来回顾一下它的工作流程, + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191109-Go-Memory-Management-and-Memory-Sweep/03.png) +

Free objects tracking with allocBits

+ +就性能角度来看,`allocBits` 代表了一个初始态并且会保持不变,但是它会由 `freeIndex`(一个指向第一个空闲位置的增量计数器)所协助。 + +然后,第一个分配就开始了: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191109-Go-Memory-Management-and-Memory-Sweep/04.png) +

Free objects tracking with allocBits

+ +`freeIndex` 现在增加了,并且基于 `allocBits` 知道了下一段空闲位置。 + +分配过程将会再一次出现,之后, GC 将会启动去释放不再被使用的内存。在标记期间,GC 会用一个位图 `gcmarkBits` 来跟踪在使用中的内存。让我们通过我们运行的程序以相同的示例为例,在第一个块不再被使用的地方。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191109-Go-Memory-Management-and-Memory-Sweep/05.png) +

Memory tracking during the garbage collector

+ +正在被使用的内存被标记为黑色,然而当前执行并不能够到达的那些内存会保持为白色。 + +> 有关更多关于标记和着色阶段的信息,我建议你阅读我的这篇文章 [Go:GC 是如何标记内存的?](https://studygolang.com/articles/25916) + +现在,我们可以使用 `gomarkBits` 精确查看可用于分配的内存。Go 现在也使用 `gomarkBits` 代替了 `allocBits` ,这个操作就是内存清理: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191109-Go-Memory-Management-and-Memory-Sweep/06.png) +

Sweeping a span

+ +但是,这必须在每一个范围内执行完毕并且会花费许多时间。Go 的目标是在清理内存时不阻碍执行,并为此提供了两种策略。 + +## 清理阶段 + +Go 提供了两种方式来清理内存: + +- 使用一个工作程序在后台等待,一个一个的清理这些范围。 +- 当分配需要一个范围的时候即时执行。 + +关于后台工作程序,当开始运行程序时,Go 将设置一个后台运行的 Worker(唯一的任务就是去清理内存),它将进入睡眠状态并等待内存段扫描: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191109-Go-Memory-Management-and-Memory-Sweep/07.png) +

Background sweeper

+ +通过追踪过程的周期,我们也能看到这个后台工作程序总是出现去清理内存: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191109-Go-Memory-Management-and-Memory-Sweep/08.png) +

Background sweeper

+ +清理内存段的第二种方式是即时执行。但是,由于这些内存段已经被分发到每一个处理器的本地缓存 `mcache` 中,因此很难追踪首先清理哪些内存。这就是为什么 Go 首先将所有内存段移动到 `mcentral` 的原因。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191109-Go-Memory-Management-and-Memory-Sweep/09.png) +

Spans are released to the central list

+ +然后,它将会让本地缓存 `mcache` 再次请求它们,去即时清理: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191109-Go-Memory-Management-and-Memory-Sweep/10.png) +

Sweep span on the fly during allocation

+ +即时扫描确保所有内存段在保存资源的过程中都会得到清理,同时会保存资源以及不会阻塞程序执行。 + +## 与 GC 周期的冲突 + +正如之前看到的,由于后台只有一个 worker 在清理内存块,清理过程可能会花费一些时间。但是,我们可能想知道如果另一个 GC 周期在一次清理过程中启动会发生什么。在这种情况下,这个运行 GC 的 Goroutine 就会在开始标记阶段前去协助完成剩余的清理工作。让我们举个例子看一下连续调用两次 GC,包含数千个对象的内存分配的过程。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191109-Go-Memory-Management-and-Memory-Sweep/11.png) + +

Sweeping must be finished before a new cycle

+但是,如果开发者没有强制调用 GC,这个情况并不会发生。在后台运行的清理工作以及在执行过程中的清理工作应该足够多,因为清理内存块的数量和去触发一个新的周期(译者注:GC 周期)的所需的分配的数量成正比。 + +--- +via: + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[sh1luo](https://github.com/sh1luo) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191109-Lets-Create-a-Simple-Load-Balancer-With-Go.md b/published/tech/20191109-Lets-Create-a-Simple-Load-Balancer-With-Go.md new file mode 100644 index 000000000..26e2a5dfd --- /dev/null +++ b/published/tech/20191109-Lets-Create-a-Simple-Load-Balancer-With-Go.md @@ -0,0 +1,371 @@ +首发于:https://studygolang.com/articles/28988 + +# 用 Go 创建一个简易负载均衡器 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191109-Lets-Create-a-Simple-Load-Balancer-With-Go/00.png) + +负载均衡器在 Web 架构中扮演了很关键的角色。它们能在一组后端机器分配负载。这使得服务扩展性更好。因为配置了很多的后端机器,服务也因此能在某次请求失败后找到正常运行的服务器而变得高可用。 + +在使用了像 [NGINX](https://www.nginx.com/) 等专业的负载均衡器后,我自己也尝试着用 [Golang](https://golang.org/) 创建了一个简易负载均衡器。Go 是一种现代语言,第一特性是支持并发。Go 有丰富的标准库,使用这些库你可以用更少的代码写出高性能的应用程序。对每一个发行版本它都有静态链接库。 + +## 我们的简易负载均衡器工作原理 + +负载均衡器有不同的策略用来在一组后端机器中分摊负载。 + +例如: + +- **轮询** 平等分摊,认为后端的所有机器处理能力相同 +- **加权轮询** 基于后端机器不同的处理能力,为其加上不同的权重 +- **最少连接数** 负载被分流到活跃连接最少的服务器 + +至于我们的简易负载均衡器,我们会实现这里边最简单的方式 **轮询**。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191109-Lets-Create-a-Simple-Load-Balancer-With-Go/01.png) + +## 轮询选择 + +轮询无疑是很简单的。它轮流给每个 worker 相同的执行任务的机会。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191109-Lets-Create-a-Simple-Load-Balancer-With-Go/02.png) + +上图已经说明了,负载均衡器周期性地选择某台服务器。但是我们不能*直接*使用它,不是吗? + +**如果后端机器宕机了怎么办?**恐怕我们不会希望流量被路由到挂掉的机器上去。因此除非我们添加一些条件,否则不能直接使用这个算法。我们需要**把流量只路由到没有挂掉且正常运行的后端机器上**。 + +## 定义几个结构体 + +修正思路后,现在我们清楚我们是想要一种能跟踪后端机器状态信息的方法。我们需要检查机器是否存活,也需要跟踪 Url。 + +我们可以简单地定义一个下面的结构体来维护我们的后端机器。 + +```go +type Backend struct { + URL *url.URL + Alive bool + mux sync.RWMutex + ReverseProxy *httputil.ReverseProxy +} +``` + +不要担心,**后面我会解释 `Backend` 里的字段**。 + +现在我们要在负载均衡器中跟踪所有后端机器的状态,可以简单地使用一个切片来实现。另外还需要一个计算变量。我们可以定义为 `ServerPool` + +```go +type ServerPool struct { + backends []*Backend + current uint64 +} +``` + +## ReverseProxy 的使用 + +前面已经声明过了,负载均衡器是专门用来把流量路由到不同的后端机器以及把结果返回给来源客户端的。 + +Go 官方文档的描述: + +> ReverseProxy 是一种 HTTP Handler,接收请求并发送到另一台服务器,把响应代理回客户端。 + +**而这正是我们需要的。**我们不需要重复造轮子了。我们可以简单地通过 `ReverseProxy` 转发原始请求。 + +```go +u, _ := url.Parse("http://localhost:8080") +rp := httputil.NewSingleHostReverseProxy(u) + +// initialize your server and add this as handler +http.HandlerFunc(rp.ServeHTTP) +``` + +通过 `httputil.NewSingleHostReverseProxy(url)` 我们可以初始化一个把请求转发给 `url` 的反向代理。在上面的例子中,所有的请求会被转发到 localhost:8080,结果会发回到来源客户端。这里你可以找到更多例子。 + +如果我们看一下 ServeHTTP 方法的签名,它有 HTTP handler 的签名,因此我们可以把它传给 `http` 的 `HandlerFunc`。 + +你可以在[文档](https://golang.org/pkg/net/http/httputil/#ReverseProxy)中找到更多例子。 + +在我们的简易负载均衡器中,我们可以用与 `ReverseProxy` 相关联的 `Backend` 中的 `URL` 初始化 `ReverseProxy`,这样 `ReverseProxy` 就会把我们请求路由到 `URL`. + +## 选择处理过程 + +我们要在下一次轮询中**跳过挂掉的后端机器**。但是无论如何我们需要一种计数的方式。 + +很多客户端会连接到负载均衡器,当某一个客户端发来请求时,我们要转发流量的目标机器会出现竞争。我们可以使用 `mutex` 为 `ServerPool` 加锁来避免这种现象。但这是一种过犹不及的手段,毕竟我们不希望锁住 ServerPool。我们的需求只是让计数器加 1. + +为了满足这个需求,最理想的解决方案是让加 1 成为原子操作。Go 的 `atomic` 包能完美支持。 + +```go +func (s *ServerPool) NextIndex() int { + return int(atomic.AddUint64(&s.current, uint64(1)) % uint64(len(s.backends))) +} +``` + +这里我们的加 1 是原子操作,通过对切片的长度取模返回了 index。这意味着返回的值一定在 0 与切片长度之间。归根结底,我们需要的是一个特定的 index,而不是所有数。 + +## 选中存活的后端机器 + +我们已经知道我们的请求是被周期性的路由到每台后端机器上的。我们要做的就是跳过挂掉的机器。 + +`GetNext()` 返回的一定是 0 与 切片长度之间的值。每次我们要转发请求到后端某台机器时,如果它挂掉了,我们必须循环地查找整个切片。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191109-Lets-Create-a-Simple-Load-Balancer-With-Go/03.png) + +从上图可以看出,我们要想从 next 开始遍历整个 list,只需要遍历 `next + length`。但是我们要覆盖整个切片的长度才能选中一个 index。我们可以用取模操作很容易地实现。 + +当我们通过搜索找到了一台正常工作的后端机器时,我们把它标记为 current。 + +下面是对应上面操作的代码。 + +```go +// GetNextPeer returns next active peer to take a connection +func (s *ServerPool) GetNextPeer() *Backend { + // loop entire backends to find out an Alive backend + next := s.NextIndex() + l := len(s.backends) + next // start from next and move a full cycle + for i := next; i < l; i++ { + idx := i % len(s.backends) // take an index by modding with length + // if we have an alive backend, use it and store if its not the original one + if s.backends[idx].IsAlive() { + if i != next { + atomic.StoreUint64(&s.current, uint64(idx)) // mark the current one + } + return s.backends[idx] + } + } + return nil +} +``` + +## 在 Backend 结构体中避免竞争条件 + +我们需要考虑一个很严重的问题。我们的 `Backend` 结构体有一个可能被不同协程同时修改或访问的变量。 + +我们知道读的协程数比写的多。因此我们用 `RWMutex` 来串行化对 `Alive` 的读写。 + +```go +// SetAlive for this backend +func (b *Backend) SetAlive(alive bool) { + b.mux.Lock() + b.Alive = alive + b.mux.Unlock() +} + +// IsAlive returns true when backend is alive +func (b *Backend) IsAlive() (alive bool) { + b.mux.RLock() + alive = b.Alive + b.mux.RUnlock() + return +} +``` + +## 让负载均衡器发请求 + +所有的准备工作都做完了,我们可以用下面的方法对我们的请求实现负载均衡。只有在所有的后端机器都离线后它才会返回失败。 + +```go +// lb load balances the incoming request +func lb(w http.ResponseWriter, r *http.Request) { + peer := serverPool.GetNextPeer() + if peer != nil { + peer.ReverseProxy.ServeHTTP(w, r) + return + } + http.Error(w, "Service not available", http.StatusServiceUnavailable) +} +``` + +这个方法可以简单地作为一个 `HandleFunc` 传给 http server。 + +```go +server := http.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: http.HandlerFunc(lb), +} +``` + +## 仅把流量路由到健康的后端机器 + +现在我们的 `lb` 有个严重的问题。我们不知道后端某台机器是否健康。我们必须向后端机器发送请求再检查它是否存活才能知道。 + +我们可以用两种方法实现: + +- **主动:**在处理当前的请求时,选中的某台机器没有响应,我们把它标记为挂掉。 +- **被动:**我们可以以固定的周期 ping 后端机器,检查其状态 + +## 主动检查后端健康机器 + +`ReverseProxy` 在有错误时会触发一个回调函数 `ErrorHandler`。我们可以用它来检测失败。下面是其实现: + +```go +proxy.ErrorHandler = func(writer http.ResponseWriter, request *http.Request, e error) { + log.Printf("[%s] %s\n", serverUrl.Host, e.Error()) + retries := GetRetryFromContext(request) + if retries < 3 { + select { + case <-time.After(10 * time.Millisecond): + ctx := context.WithValue(request.Context(), Retry, retries+1) + proxy.ServeHTTP(writer, request.WithContext(ctx)) + } + return + } + + // after 3 retries, mark this backend as down + serverPool.MarkBackendStatus(serverUrl, false) + + // if the same request routing for few attempts with different backends, increase the count + attempts := GetAttemptsFromContext(request) + log.Printf("%s(%s) Attempting retry %d\n", request.RemoteAddr, request.URL.Path, attempts) + ctx := context.WithValue(request.Context(), Attempts, attempts+1) + lb(writer, request.WithContext(ctx)) +} +``` + +这里我们利用闭包的特性来设计这个错误 handler。我们能把 serverUrl 等外部的变量捕获到方法内。它会检查已存在的重试次数,如果小于 3,就会再发送同样的请求到同一台机器。这么做的原因是可能会有临时的错误,服务器可能暂时拒绝你的请求而在短暂的延迟之后又变得可用了(可能服务器为了接受更多的客户端耗尽了 socket)。因此我们加了一个定时器,延迟 10 毫秒左右进行重试。每次请求都会加一次重试次数的计数。 + +3 次请求都失败后,我们把这台后端机器标记为挂掉。 + +下一步要做的是,把这个请求发送到另外一台后端机器。我们通过使用 context 包来维护一个尝试次数的计数来实现。增加了尝试次数的计数后,我们把它返回给 `lb`,寻找下一台可用的后端机器来处理这个请求。 + +我们不能无限地重复这个过程,因此我们需要在 `lb` 中检查在继续处理该请求之前是否已达到了最大尝试次数。 + +我们可以简单地从请求中取得尝试次数,如果它超过了最大数,取消这次请求。 + +```go +// lb load balances the incoming request +func lb(w http.ResponseWriter, r *http.Request) { + attempts := GetAttemptsFromContext(r) + if attempts > 3 { + log.Printf("%s(%s) Max attempts reached, terminating\n", r.RemoteAddr, r.URL.Path) + http.Error(w, "Service not available", http.StatusServiceUnavailable) + return + } + + peer := serverPool.GetNextPeer() + if peer != nil { + peer.ReverseProxy.ServeHTTP(w, r) + return + } + http.Error(w, "Service not available", http.StatusServiceUnavailable) +} +``` + +这个实现是递归的。 + +## context 的使用 + +`context` 包能让你保存 HTTP 请求中有用的数据。我们大量地使用了它来跟踪请求中特定的数据,如尝试次数和重试次数。 + +首先,我们需要指定 context 的 key。推荐用不重复的整型而不是字符串作为 key。Go 提供了 `iota` 关键字实现常量的增加,每个 `iota` 含有一个独一无二的值。这是一个定义整型 key 的完美解决方案。 + +```go +const ( + Attempts int = iota + Retry +) +``` + +然后我们可以像在 HashMap 中检索值一样检索定义的值。返回的默认值随实际用例的不同而不同。 + +```go +// GetAttemptsFromContext returns the attempts for request +func GetRetryFromContext(r *http.Request) int { + if retry, ok := r.Context().Value(Retry).(int); ok { + return retry + } + return 0 +} +``` + +## 被动健康检查 + +通过被动健康检查我们可以恢复挂掉的后端机器或识别它们。我们以固定的时间周期 ping 后端机器来检查它们的状态。 + +我们尝试建立 TCP 连接来 ping 机器。如果后端机器有响应,我们把它比较为存活的。如果你愿意,这种方法可以修改为请求一个类似 `/status` 的特定的服务终端。建立连接之后不要忘记关闭连接,以免对服务器造成额外的负载。否则,它会尝试一直维持连接最终耗尽资源。 + +```go +// isAlive checks whether a backend is Alive by establishing a TCP connection +func isBackendAlive(u *url.URL) bool { + timeout := 2 * time.Second + conn, err := net.DialTimeout("tcp", u.Host, timeout) + if err != nil { + log.Println("Site unreachable, error: ", err) + return false + } + _ = conn.Close() // close it, we dont need to maintain this connection + return true +} +``` + +现在我们可以像下面这样遍历服务器并标记它们的状态。 + +```go +// HealthCheck pings the backends and update the status +func (s *ServerPool) HealthCheck() { + for _, b := range s.backends { + status := "up" + alive := isBackendAlive(b.URL) + b.SetAlive(alive) + if !alive { + status = "down" + } + log.Printf("%s [%s]\n", b.URL, status) + } +} +``` + +在 Go 中,我们可以起一个定时器来周期性地运行它。当定时器创建后,你可以用通道监听事件。 + +```go +// healthCheck runs a routine for check status of the backends every 20 secs +func healthCheck() { + t := time.NewTicker(time.Second * 20) + for { + select { + case <-t.C: + log.Println("Starting health check...") + serverPool.HealthCheck() + log.Println("Health check completed") + } + } +} +``` + +在上一段中,`<-t.C` 通道会每 20 秒接收一次数据。`select` 探测这个事件。如果没有 `default` 分支,`select` 会一直等待,直到至少一个分支执行。 + +最后,在一个单独的协程中运行。 + +## 总结 + +本文讲了很多 + +- 轮询选择 +- 标准库中的 ReverseProxy +- Mutex +- 原子操作 +- 闭包 +- 回调 +- select 操作 + +还有很多可以做的来改进我们的简易负载均衡器。 + +例如: + +- 用堆来对后端机器进行排序,减少搜索范围 +- 采集统计信息 +- 实现加权轮询/最少连接 +- 支持配置文件 + +等等。 + +你可以在[这里](https://github.com/kasvith/simplelb/)找到代码库。 + +感谢阅读😄 + +--- + +via: https://kasvith.me/posts/lets-create-a-simple-lb-go/ + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191120-Beating-C-with-70-Lines-of-Go.md b/published/tech/20191120-Beating-C-with-70-Lines-of-Go.md new file mode 100644 index 000000000..26a454961 --- /dev/null +++ b/published/tech/20191120-Beating-C-with-70-Lines-of-Go.md @@ -0,0 +1,386 @@ +首发于:https://studygolang.com/articles/25291 + +# 用 70 行 Go 代码击败 C 语言 + +Chris Penner 最近发布的一篇文章 [Beating C with 80 Lines of Haskell](https://chrispenner.ca/posts/wc) 引发了 Internet 领域内广泛的论战,进而引发了一场用不同语言实现 `wc` 的圣战: + +- [Ada](http://verisimilitudes.net/2019-11-11) +- [C](https://github.com/expr-fi/fastlwc/) +- [Common Lisp](http://verisimilitudes.net/2019-11-12) +- [Dyalog APL](https://ummaycoc.github.io/wc.apl/) +- [Futhark](https://futhark-lang.org/blog/2019-10-25-beating-c-with-futhark-on-gpu.html) +- [Haskell](https://chrispenner.ca/posts/wc) +- [Rust](https://medium.com/@martinmroz/beating-c-with-120-lines-of-rust-wc-a0db679fe920) + +今天我们用 Go 语言来实现 `wc` 的功能。作为有着杰出的并发基因的语言,实现与 C 语言相当的性能(原文为 [comparable performance](https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/go-gcc.html))应该是小菜一碟。 + +虽然 `wc` 也被设计为从 stdin 读取信息,处理 non-ASCII 文本编码,从命令行解析 flags([manpage](https://ss64.com/osx/wc.html)),但我们并不去这样做。我们要做的是,像前面提到的那篇文章一样,让我们的实现尽可能简单。 + +本文涉及的源码可以在 [这里](https://github.com/ajeetdsouza/blog-wc-go) 找到。 + +```bash +$ /usr/bin/time -f "%es %MKB" wc test.txt +``` + +我们使用 [与原文相同版本的 `wc`](https://opensource.apple.com/source/text_cmds/text_cmds-68/wc/wc.c.auto.html) ,用 gcc 9.2.1 编译,编译优化选项为 `-O3`。在我们的实现中,使用 Go 1.13.4(我确实也试过用 gccgo,但结果不是很理想)。我们用以下配置来运行所有的基准: + +- Intel Core i5-6200U @ 2.30 GHz (2 physical cores, 4 threads) +- 4+4 GB RAM @ 2133 MHz +- 240 GB M.2 SSD +- Fedora 31 + +公平起见,所有实现都使用一个 16 KB 的 buffer 来读取输入。输入是两个 us-ascii 编码的文本文件,大小分别是 100 MB 和 1 GB。 + +## 一个纯朴的方法 + +因为我们只需要输入文件路径,所以解析参数很容易: + +```go +if len(os.Args) < 2 { + panic("no file path specified") +} +filePath := os.Args[1] + +file, err := os.Open(filePath) +if err != nil { + panic(err) +} +defer file.Close() +``` + +我们会逐字节遍历文本,跟踪状态。幸运的是,在本文案例中,我们只需要引入两个状态: + +- 前一个字节是 whitespace +- 前一个字节不是 whitespace + +当从一个 whitespace 字符跳到一个 non-whitespace 字符时,单词计数加 1。这种方法可以直接从字节流读取信息,保持低内存消耗。 + +```go +const bufferSize = 16 * 1024 +reader := bufio.NewReaderSize(file, bufferSize) + +lineCount := 0 +wordCount := 0 +byteCount := 0 + +prevByteIsSpace := true +for { + b, err := reader.ReadByte() + if err != nil { + if err == io.EOF { + break + } else { + panic(err) + } + } + + byteCount++ + + switch b { + case '\n': + lineCount++ + prevByteIsSpace = true + case ' ', '\t', '\r', '\v', '\f': + prevByteIsSpace = true + default: + if prevByteIsSpace { + wordCount++ + prevByteIsSpace = false + } + } +} +``` + +为了展示结果,我们用原生的 println() 函数 — 在我的试验中,导入 fmt 包会导致运行时空间增加约 400 KB。 + +```go +println(lineCount, wordCount, byteCount, file.Name()) +``` + +运行结果: + +| | input size | elapsed time | max memory | +| -------- | ---------- | ------------ | ---------- | +| wc | 100 MB | 0.58 s | 2052 KB | +| wc-naive | 100 MB | 0.77 s | 1416 KB | +| wc | 1 GB | 5.56 s | 2036 KB | +| wc-naive | 1 GB | 7.69 s | 1416 KB | + +好消息是我们的第一次尝试在性能方面非常接近 C 语言。事实上,在内存使用方面,我们做得比 C 语言*更好*。 + +## 分割输入 + +虽然对 I/O 读取进行缓冲显著提升了性能,但是调用 ReadByte() 和在循环中检查 error 造成了一大笔不必要的开销。我们可以通过手动缓冲读请求来规避上述情况,而不再依赖 bufio.Reader。 + +为了实现手动缓冲,我们把输入分割成多个可以单独处理的缓冲块。幸运的是,我们只要知道前一个缓冲块(我们之前看到过)的最后一个字符是否是 whitespace 就可以处理当前的块。 + +我们写几个实用函数: + +```go +type Chunk struct { + PrevCharIsSpace bool + Buffer []byte +} + +type Count struct { + LineCount int + WordCount int +} + +func GetCount(chunk Chunk) Count { + count := Count{} + + prevCharIsSpace := chunk.PrevCharIsSpace + for _, b := range chunk.Buffer { + switch b { + case '\n': + count.LineCount++ + prevCharIsSpace = true + case ' ', '\t', '\r', '\v', '\f': + prevCharIsSpace = true + default: + if prevCharIsSpace { + prevCharIsSpace = false + count.WordCount++ + } + } + } + + return count +} + +func IsSpace(b byte) bool { + return b == ' ' || b == '\t' || b == '\n' || b == '\r' || b == '\v' || b == '\f' +} +``` + +现在,我们可以把输入分割成多个块,然后传入 GetCount 函数。 + +```go +totalCount := Count{} +lastCharIsSpace := true + +const bufferSize = 16 * 1024 +buffer := make([]byte, bufferSize) + +for { + bytes, err := file.Read(buffer) + if err != nil { + if err == io.EOF { + break + } else { + panic(err) + } + } + + count := GetCount(Chunk{lastCharIsSpace, buffer[:bytes]}) + lastCharIsSpace = IsSpace(buffer[bytes-1]) + + totalCount.LineCount += count.LineCount + totalCount.WordCount += count.WordCount +} +``` + +为了计数字节,我们可以进行一次系统调用来查询文件的大小: + +```go +fileStat, err := file.Stat() +if err != nil { + panic(err) +} +byteCount := fileStat.Size() +``` + +现在做完该做的了,来看一下表现如何: + +| | input size | elapsed time | max memory | +| --------- | ---------- | ------------ | ---------- | +| wc | 100 MB | 0.58 s | 2052 KB | +| wc-chunks | 100 MB | 0.34 s | 1404 KB | +| wc | 1 GB | 5.56 s | 2036 KB | +| wc-chunks | 1 GB | 3.31 s | 1416 KB | + +看起来我们在两个统计维度上都超过了 `wc`,而且我们还没有开始并行化我们的程序。[`tokei`](https://github.com/XAMPPRocky/tokei) 统计结果显示这个程序一共只有 70 行代码! + +## 并行化 + +诚然,并行化实现 `wc` 是大材小用了,但我们还是来看一下到底能达到什么程度。原文是并行地从输入文件中读的,尽管它缩短了运行时间,但作者同时也承认,并行读仅在特定几种存储的情况下对性能有提升,其他的情况下可能降低性能。 + +在我们的实现中,我们希望我们的代码在*所有设备*上运行都能有很好的性能,所以我们不像原文作者那样做。我们会创建两个 channel:chunks 和 counts 。每个 worker 都会从 chunks 读取和处理数据直到 channel 被 close,之后把结果写进 counts。 + +```go +func ChunkCounter(chunks <-chan Chunk, counts chan<- Count) { + totalCount := Count{} + for { + chunk, ok := <-chunks + if !ok { + break + } + count := GetCount(chunk) + totalCount.LineCount += count.LineCount + totalCount.WordCount += count.WordCount + } + counts <- totalCount +} +``` + +我们在每个 CPU core 上起一个 worker: + +```go +numWorkers := runtime.NumCPU() + +chunks := make(chan Chunk) +counts := make(chan Count) + +for i := 0; i < numWorkers; i++ { + Go ChunkCounter(chunks, counts) +} +``` + +现在,我们在循环中从硬盘读取数据和给每个 worker 分配 job: + +```go +const bufferSize = 16 * 1024 +lastCharIsSpace := true + +for { + buffer := make([]byte, bufferSize) + bytes, err := file.Read(buffer) + if err != nil { + if err == io.EOF { + break + } else { + panic(err) + } + } + chunks <- Chunk{lastCharIsSpace, buffer[:bytes]} + lastCharIsSpace = IsSpace(buffer[bytes-1]) +} +close(chunks) +``` + +这些完成后,我们可以很简单地把所有 worker 的计数相加。 + +```go +totalCount := Count{} +for i := 0; i < numWorkers; i++ { + count := <-counts + totalCount.LineCount += count.LineCount + totalCount.WordCount += count.WordCount +} +close(counts) +``` + +我们运行起来然后看一下与之前结果的对比: + +| | input size | elapsed time | max memory | +| ---------- | ---------- | ------------ | ---------- | +| wc | 100 MB | 0.58 s | 2052 KB | +| wc-channel | 100 MB | 0.27 s | 6644 KB | +| wc | 1 GB | 5.56 s | 2036 KB | +| wc-channel | 1 GB | 2.22 s | 6752 KB | + +我们实现的 `wc` 在速度方面有很大提升,但在内存使用方面与之前相比有些倒退。请特别留意我们的输入循环在每一次执行中是怎么样申请内存的!channel 是对共享内存的高度抽象,但在实际使用时,*不使用* channel 可能会大幅提升性能。 + +## 并行化升级版 + +在这部分,我们让每个 worker 都读取文件,并使用 sync.Mutex 来确保不会同时读取。我们可以创建一个 struct 来为我们处理这种情况: + +```go +type FileReader struct { + File *os.File + LastCharIsSpace bool + mutex sync.Mutex +} + +func (fileReader *FileReader) ReadChunk(buffer []byte) (Chunk, error) { + fileReader.mutex.Lock() + defer fileReader.mutex.Unlock() + + bytes, err := fileReader.File.Read(buffer) + if err != nil { + return Chunk{}, err + } + + chunk := Chunk{fileReader.LastCharIsSpace, buffer[:bytes]} + fileReader.LastCharIsSpace = IsSpace(buffer[bytes-1]) + + return chunk, nil +} +``` + +为了能直接读取文件,我们重写 worker 函数: + +```go +func FileReaderCounter(fileReader *FileReader, counts chan Count) { + const bufferSize = 16 * 1024 + buffer := make([]byte, bufferSize) + + totalCount := Count{} + + for { + chunk, err := fileReader.ReadChunk(buffer) + if err != nil { + if err == io.EOF { + break + } else { + panic(err) + } + } + count := GetCount(chunk) + totalCount.LineCount += count.LineCount + totalCount.WordCount += count.WordCount + } + + counts <- totalCount +} +``` + +像之前一样,我们还是在每个 CPU core 起一个 worker: + +```go +fileReader := &FileReader{ + File: file, + LastCharIsSpace: true, +} +counts := make(chan Count) + +for i := 0; i < numWorkers; i++ { + Go FileReaderCounter(fileReader, counts) +} + +totalCount := Count{} +for i := 0; i < numWorkers; i++ { + count := <-counts + totalCount.LineCount += count.LineCount + totalCount.WordCount += count.WordCount +} +close(counts) +``` + +来看看表现如何: + +| | intput size | elapsed time | max memory | +| -------- | ----------- | ------------ | ---------- | +| wc | 100 MB | 0.58 s | 2052 KB | +| wc-mutex | 100 MB | 0.12 s | 1580 KB | +| wc | 1 GB | 5.56 s | 2036 KB | +| wc-mutex | 1 GB | 1.21 s | 1576 KB | + +我们的并行化实现用更小的内存消耗比 `wc` 的运行速度快了 4.5 倍!这意义非凡,尤其是当你意识到 Go 是一种有垃圾回收机制的语言时。 + +## 总结 + +尽管本文结论并不意味着 Go > C,但我希望它能证明 Go 作为一种系统编程语言可以是 C 语言的可替代项。 + +如果你有任何建议、问题、意见,尽情 [给我发邮件](mailto:98ajeet@gmail.com)! + +--- + +via: https://ajeetdsouza.github.io/blog/posts/beating-c-with-70-lines-of-go/ + +作者:[Ajeet D'Souza](https://ajeetdsouza.github.io/blog/) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191125-Go-Goroutine-OS-Thread-and-CPU-Management.md b/published/tech/20191125-Go-Goroutine-OS-Thread-and-CPU-Management.md new file mode 100644 index 000000000..ea81b0557 --- /dev/null +++ b/published/tech/20191125-Go-Goroutine-OS-Thread-and-CPU-Management.md @@ -0,0 +1,152 @@ +首发于:https://studygolang.com/articles/25292 + +# Go:协程,操作系统线程和 CPU 管理 + +![Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-goroutines-os-thread-and-cpu-management/00.png) + +ℹ️ *本文运行环境为 Go 1.13* + +对于一个程序来说,从内存和性能角度讲创建一个 OS 线程或切换线程花费巨大。Go 志在极尽所能地充分利用内核资源。从第一天开始,它就是为并发而生的。 + +## M, P, G 编排 + +为了解决这个问题,Go 有它自己的在线程间调度协程的调度器。这个调度器定义了三个主要概念,如源码中解释的这样: + +``` +The main concepts are: +G - goroutine. +M - worker thread, or machine. +P - processor, a resource that is required to execute Go code. + M must have an associated P to execute Go code[...]. +``` + +`P`, `M`, `G` 模型图解: + +![P, M, G diagram](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-goroutines-os-thread-and-cpu-management/01.png) + +每个协程(`G`)运行在与一个逻辑 CPU(`P`)相关联的 OS 线程(`M`)上。我们一起通过一个简单的示例来看 Go 是怎么管理他们的: + +```go +func main() { + var wg sync.WaitGroup + wg.Add(2) + + Go func() { + println(`hello`) + wg.Done() + }() + + Go func() { + println(`world`) + wg.Done() + }() + + wg.Wait() +} +``` + +首先,Go 根据机器逻辑 CPU 的个数来创建不同的 `P`,并且把它们保存在一个空闲 `P` 的 list 里。 + +![P initialization](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-goroutines-os-thread-and-cpu-management/02.png) + +然后,为了更好地工作新创建的已经准备好的协程会唤醒一个 `P`。这个 `P` 通过与之相关联的 OS 线程来创建一个 `M`: + +![OS thread creation](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-goroutines-os-thread-and-cpu-management/03.png) + +然而,像 `P` 那样,系统调用返回的甚至被 gc 强行停止的空闲的 `M` — 比如没有协程在等待运行 — 也会被加到一个空闲 list: + +![M and P idle list](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-goroutines-os-thread-and-cpu-management/04.png) + +在程序启动阶段,Go 就已经创建了一些 OS 线程并与 `M` 想关联了。在我们的例子中,打印 `hello` 的第一个协程会使用主协程,第二个会从这个空闲 list 中获取一个 `M` 和 `P`: + +![M and P pulled from the idle list](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-goroutines-os-thread-and-cpu-management/05.png) + +现在我们已经掌握了协程和线程管理的基本要义,来一起看看什么情形下 Go 会用比 `P` 多的 `M`,在系统调用时怎么管理协程。 + +## 系统调用 + +Go 会优化系统调用 — 无论阻塞与否 — 通过运行时封装他们。封装的那一层会把 `P` 和线程 `M` 分离,并且可以让另一个线程在它上面运行。我们拿文件读取举例: + +```go +func main() { + buf := make([]byte, 0, 2) + + fd, _ := os.Open("number.txt") + fd.Read(buf) + fd.Close() + + println(string(buf)) // 42 +} +``` + +文件读取的流程如下: + +![Syscall handoffs P](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-goroutines-os-thread-and-cpu-management/06.png) + +`P0` 现在在空闲 list 中,有可能被唤醒。当系统调用 exit 时,Go 会遵守下面的规则,直到有一个命中了。 + +- 尝试去捕获相同的 `P`,在我们的例子中就是 `P0`,然后 resume 执行过程 +- 尝试从空闲 list 中捕获一个 `P`,然后 resume 执行过程 +- 把协程放到全局队列里,把与之相关联的 `M` 放回空闲 list 去 + +然而,在像 http 请求等 non-blocking I/O 情形下,Go 在资源没有准备好时也会处理请求。在这种情形下,第一个系统调用 — 遵循上述流程图 — 由于资源还没有准备好所以不会成功,(这样就)迫使 Go 使用 network poller 并使协程停驻。请看示例: + +```go +func main() { + http.Get(`https://httpstat.us/200`) +} +``` + +当第一个系统调用完成且显式地声明了资源还没有准备好,协程会在 network poller 通知它资源准备就绪之前一直处于停驻状态。在这种情形下,线程 `M` 不会阻塞: + +![Network poller waiting for the resource](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-goroutines-os-thread-and-cpu-management/07.png) + +在 Go 调度器在等待信息时协程会再次运行。调度器在获取到等待的信息后会询问 network poller 是否有协程在等待被运行。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-goroutines-os-thread-and-cpu-management/08.png) + +如果多个协程都准备好了,只有一个会被运行,其他的会被加到全局的可运行队列中,以备后续的调度。 + +## OS 线程方面的限制 + +在系统调用中,Go 不会限制可阻塞的 OS 线程数,源码中有解释: + +> *The GOMAXPROCS variable limits the number of operating system threads that can execute user-level Go code simultaneously. There is no limit to the number of threads that can be blocked in system calls on behalf of Go code; those do not count against the GOMAXPROCS limit. This package’s GOMAXPROCS function queries and changes the limit.* + +译注:**GOMAXPROCS** 变量表示可同时运行用户级 Go 代码的操作系统线程的最大数量。系统调用中可被阻塞的最大线程数并没有限制;可被阻塞的线程数对 **GOMAXPROCS** 没有影响。这个包的 ***GOMAXPROCS*** 函数查询和修改这个最大数限制。 + +对这种情形举例: + +```go +func main() { + var wg sync.WaitGroup + + for i := 0;i < 100 ;i++ { + wg.Add(1) + + Go func() { + http.Get(`https://httpstat.us/200?sleep=10000`) + + wg.Done() + }() + } + + wg.Wait() +} +``` + +利用追踪工具得到的线程数如下: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/go-goroutines-os-thread-and-cpu-management/09.png) + +由于 Go 优化了线程使用,所以当协程阻塞时,它仍可复用,这就解释了为什么图中的数跟示例代码循环中的数不一致。 + +--- + +via: https://medium.com/a-journey-with-go/go-goroutine-os-thread-and-cpu-management-2f5a5eaf518a + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191127-Circuit-Breaker-and-Retry.md b/published/tech/20191127-Circuit-Breaker-and-Retry.md new file mode 100644 index 000000000..c6002ad1d --- /dev/null +++ b/published/tech/20191127-Circuit-Breaker-and-Retry.md @@ -0,0 +1,217 @@ +首发于:https://studygolang.com/articles/25294 + +# Go 微服务中的熔断器和重试 + +今天我们来讨论微服务架构中的自我恢复能力。通常情况下,服务间会通过同步或异步的方式进行通信。我们假定把一个庞大的系统分解成一个个的小块能将各个服务解耦。管理服务内部的通信可能有点困难了。你可能听说过这两个著名的概念:熔断和重试。 + +## 熔断器 + +![01](https://raw.githubusercontent.com/studygolang/gctt-images2/master/circuit-breaker-and-retry/01.png) + +想象一个简单的场景:用户发出的请求访问服务 A 随后访问另一个服务 B。我们可以称 B 是 A 的依赖服务或下游服务。到服务 B 的请求在到达各个实例前会先通过负载均衡器。 + +后端服务发生系统错误的原因有很多,例如慢查询、network blip 和内存争用。在这种场景下,如果返回 A 的 response 是 timeout 和 server error,我们的用户会再试一次。在混乱的局面中我们怎样来保护下游服务呢? + +![02](https://raw.githubusercontent.com/studygolang/gctt-images2/master/circuit-breaker-and-retry/02.png) + +熔断器可以让我们对失败率和资源有更好的控制。熔断器的设计思路是不等待 TCP 的连接 timeout 快速且优雅地处理 error。这种 fail fast 机制会保护下游的那一层。这种机制最重要的部分就是立刻向调用方返回 response。没有被 pending request 填充的线程池,没有 timeout,而且极有可能烦人的调用链中断者会更少。此外,下游服务也有了充足的时间来恢复服务能力。完全杜绝错误很难,但是减小失败的影响范围是有可能的。 + +![03](https://raw.githubusercontent.com/studygolang/gctt-images2/master/circuit-breaker-and-retry/03.png) + +通过 hystrix 熔断器,我们可以采用降级方案,对上游返回降级后的结果。例如,服务 B 可以访问一个备份服务或 cache,不再访问原来的服务 C。引入这种降级方案需要集成测试,因为我们在 happy path(译注:所谓 happy path,即测试方法的默认场景,没有异常和错误信息。具体可参见 [wikipedia](https://en.wikipedia.org/wiki/Happy_path))可能不会遇到这种网络模式。 + +## 状态 + +![04](https://raw.githubusercontent.com/studygolang/gctt-images2/master/circuit-breaker-and-retry/04.png) + +熔断器有三个主要的状态: + +- Closed:让所有请求都通过的默认状态。在阈值下的请求不管成功还是失败,熔断器的状态都不会改变。可能出现的错误是 **Max Concurrency**(最大并发数)和 **Timeout**(超时)。 +- Open:所有的请求都会返回 **Circuit Open** 错误并被标记为失败。这是一种不等待处理结束的 timeout 时间的 fail-fast 机制。 +- Half Open:周期性地向下游服务发出请求,检查它是否已恢复。如果下游服务已恢复,熔断器切换到 Closed 状态,否则熔断器保持 Open 状态。 + +## 熔断器原理 + +控制熔断的设置共有 5 个主要参数。 + +```go +// CommandConfig is used to tune circuit settings at runtime +type CommandConfig struct { + Timeout int `json:"timeout"` + MaxConcurrentRequests int `json:"max_concurrent_requests"` + RequestVolumeThreshold int `json:"request_volume_threshold"` + SleepWindow int `json:"sleep_window"` + ErrorPercentThreshold int `json:"error_percent_threshold"` +} +``` + +[查看源码](https://gist.githubusercontent.com/aladine/18b38b37f838c1938131f67da0648e92/raw/8f97b8ef0b796ea5355b8f895b4009adfe472668/command.go) + +可以通过根据两个服务的 SLA(‎ Service Level Agreement,[服务级别协议](https://zh.wikipedia.org/zh-hans/服务级别协议))来定出阈值。如果在测试时把依赖的其他服务也涉及到了,这些值会得到很好的调整。 + +一个好的熔断器的名字应该能精确指出哪个服务连接出了问题。实际上,请求一个服务时可能会有很多个 API endpoint。每一个 endpoint 都应该有一个对应的熔断器。 + +## 生产上的熔断器 + +熔断器通常被放在聚合点上。尽管熔断器提供了一种 fail-fast 机制,但我们仍然需要确保可选的降级方案可行。如果我们因为假定需要降级方案的场景出现的可能性很小就不去测试它,那(之前的努力)就是白费力气了。即使在最简单的演练中,我们也要确保阈值是有意义的。以我的个人经验,把参数配置在 log 中 print 出来对于 debug 很有帮助。 + +## Demo + +这段实例代码用的是 [hystrix-go](http://github.com/afex/hystrix-go/hystrix) 库,hystrix Netflix 库在 Golang 的实现。 + +```go +package main + +import ( + "errors" + "fmt" + "log" + "net/http" + "os" + + "github.com/afex/hystrix-go/hystrix" +) + +const commandName = "producer_api" + +func main() { + + hystrix.ConfigureCommand(commandName, hystrix.CommandConfig{ + Timeout: 500, + MaxConcurrentRequests: 100, + ErrorPercentThreshold: 50, + RequestVolumeThreshold: 3, + SleepWindow: 1000, + }) + + http.HandleFunc("/", logger(handle)) + log.Println("listening on :8080") + http.ListenAndServe(":8080", nil) +} + +func handle(w http.ResponseWriter, r *http.Request) { + output := make(chan bool, 1) + errors := hystrix.Go(commandName, func() error { + // talk to other services + err := callChargeProducerAPI() + // err := callWithRetryV1() + + if err == nil { + output <- true + } + return err + }, nil) + + select { + case out := <-output: + // success + log.Printf("success %v", out) + case err := <-errors: + // failure + log.Printf("failed %s", err) + } +} + +// logger is Handler wrapper function for logging +func logger(fn http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + log.Println(r.URL.Path, r.Method) + fn(w, r) + } +} + +func callChargeProducerAPI() error { + fmt.Println(os.Getenv("SERVER_ERROR")) + if os.Getenv("SERVER_ERROR") == "1" { + return errors.New("503 error") + } + return nil +} +``` + +demo 中分别测试了请求调用链 closed 和 open 两种情况: + +```bash +/* Experiment 1: success path */ +// server +go run main.go + +// client +for i in $(seq 10); do curl -x '' localhost:8080 ;done + +/* Experiment 2: circuit open */ +// server +SERVER_ERROR=1 Go run main.go + +// client +for i in $(seq 10); do curl -x '' localhost:8080 ;done +``` + +[查看源码](https://gist.github.com/aladine/48d935c44820508e5bca2f061e3a7c1d/raw/930cdc10c41e8b9b37018f2be36bc421e6df481a/demo.sh) + +## 重试问题 + +在上面的熔断器模式中,如果服务 B 缩容,会发生什么?大量已经从 A 发出的请求会返回 5xx error。可能会触发熔断器切换到 open 的错误报警。因此我们需要重试以防间歇性的 network hiccup 发生。 + +一段简单的重试代码示例: + +```go +package main + +func callWithRetryV1() (err error) { + for index := 0; index < 3; index++ { + // call producer API + err := callChargeProducerAPI() + if err != nil { + return err + } + } + + // adding backoff + // adding jitter + return nil +} +``` + +[查看源码](https://gist.githubusercontent.com/aladine/6d65d1db78b020ef9866e3a8ad2516aa/raw/a4d3b65cc4ef920cdfc7e898c130b92371007785/retry.go) + +## 重试模式 + +为了实现乐观锁,我们可以为不同的服务配置不同的重试次数。因为立即重试会对下游服务产生爆发性的请求,所以不能用立即重试。加一个 backoff 时间可以缓解下游服务的压力。一些其他的模式会用一个随机的 backoff 时间(或在等待时加 jitter)。 + +一起来看下列算法: + +- Exponential: bash * 2attemp +- Full Jitter: sleep = rand(0, base * 2attempt) +- Equal Jitter: temp = base * 2attemp; sleep = temp/2+rand(0, temp/2) +- De-corredlated Jitter: sleep = rand(base, sleep*3) + +【译注】关于这几个算法,可以参考[这篇文章](https://amazonaws-china.com/cn/blogs/architecture/exponential-backoff-and-jitter/) 。**Full Jitter**、 **Equal Jitter**、 **De-corredlated** 等都是原作者自己定义的名词。 + +![05](https://raw.githubusercontent.com/studygolang/gctt-images2/master/circuit-breaker-and-retry/05.png) + +客户端的数量与服务端的总负载和处理完成时间是有关联的。为了确定什么样的重试模式最适合你的系统,在客户端数量增加时很有必要运行基准测试。详细的实验过程可以在[这篇文章](https://amazonaws-china.com/cn/blogs/architecture/exponential-backoff-and-jitter/)中看到。我建议的算法是 de-corredlated Jitter 和 full jitter 选择其中一个。 + +## 两者结合 + +![Example configuration of both tools](https://raw.githubusercontent.com/studygolang/gctt-images2/master/circuit-breaker-and-retry/06.png) + +熔断器被广泛用在无状态线上事务系统中,尤其是在聚合点上。重试应该用于调度作业或不被 timeout 约束的 worker。经过深思熟虑后我们可以同时用熔断器和重试。在大型系统中,service mesh 是一种能更精确地编排不同配置的理想架构。 + +## 参考文章 + +1. https://github.com/afex/hystrix-go/ +2. https://github.com/eapache/go-resiliency +3. https://github.com/Netflix/Hystrix/wiki +4. https://www.awsarchitectureblog.com/2015/03/backoff.html +5. https://dzone.com/articles/go-microservices-part-11-hystrix-and-resilience + +--- + +via: https://medium.com/@trongdan_tran/circuit-breaker-and-retry-64830e71d0f6 + +作者:[Dan Tran](https://medium.com/@trongdan_tran) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191201-Introduction-to-Protobuf-Messages.md b/published/tech/20191201-Introduction-to-Protobuf-Messages.md new file mode 100644 index 000000000..c9450d878 --- /dev/null +++ b/published/tech/20191201-Introduction-to-Protobuf-Messages.md @@ -0,0 +1,215 @@ +首发于:https://studygolang.com/articles/25295 + +# Go 语言 Protobuf 教程之 Message + +在序列化结构数据的各种方式中,protocol buffer(或 protobuf)是资源开销最小的一种。protobuf 需要客户端和服务端都知道数据结构而且兼容,不像 JSON 那样结构本身就是编码的一部分。 + +## Protobuf 和 Go + +最基本的 protobuf message 定义像下面这样: + +```protobuf +message ListThreadRequest { + // session info + string sessionID = 1; + + // pagination + uint32 pageNumber = 2; + uint32 pageSize = 3; +} +``` + +上面的 message 结构定义了字段名字、类型和它编码后的二进制结构中的顺序。管理 protobuf 和 JSON 编码的数据稍微有些不同,protobuf 有一些依赖。 + +例如,下面是 `protoc` 生成的上面 message 对应的代码: + +```go +type ListThreadRequest struct { + // session info + SessionID string `protobuf:"bytes,1,opt,name=sessionID,proto3" json:"sessionID,omitempty"` + // pagination + PageNumber uint32 `protobuf:"varint,2,opt,name=pageNumber,proto3" json:"pageNumber,omitempty"` + PageSize uint32 `protobuf:"varint,3,opt,name=pageSize,proto3" json:"pageSize,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} +``` + +开发过程中,请以向前兼容的方式管理 protobuf message,需要遵循以下几条规则: + +- 增加新的字段不是破坏性改动 +- 删除字段不是破坏性改动 +- 不要复用一个字段的序号(破坏已有的 protobuf 客户端) +- 不要改变已有的字段序号(破坏已有的 protobuf 客户端) +- 对字段重命名在 protobuf 中不是破坏性改动 +- 对字段重命名在 JSON 中是破坏性改动 +- 修改字段的类型在 protobuf 中是破坏性改动,在大部分 JSON 中也是 +- JSON 中修改数字类型如 uint16 改为 uint32 通常是安全的 +- 如果你调用 JavaScript 的 API,JSON 和 unit64 不建议使用 + +因此,如果你开发过程中需要修改 protobuf message 的定义,请保持客户端和服务端的同步。如果你用移动客户端(Android 和 iPhone)调用 protobuf API,那么这(保持同步)将尤其重要,因为做了重大变更后是有破坏性影响的。增加新的字段或删除字段是修改 API 最安全的方式,这样 protobuf 定义能保持兼容。 + +## 生成 Protobuf Go 代码 + +在本系列的文章中,我将搭建真实的微服务,追踪和聚合多个服务的可视化数据。它不需要认证,仅需要一个 API endpoint,因此从定义上来说是一个真正的微服务。 + +首先创建一个 `rpc/stats/stats.proto`,然后创建我们的 `stats` 微服务。 + +```protobuf +syntax = "proto3"; + +package stats; + +option go_package = "github.com/titpetric/microservice/rpc/stats"; + +message PushRequest { + string property = 1; + uint32 section = 2; + uint32 id = 3; +} + +message PushResponse {} +``` + +该实例用 `proto3` 声明了 protobuf 的版本。最重要的部分是 `go_package` option:为我们的服务定义了一个重要的路径,如果其他的服务想要导入和使用这里的 message 定义就需要用到这个路径。可复用性是 protobuf 是自带的属性。 + +由于我们不想要半途而废,因此我们用 CI-first 的方式实现我们的微服务。在一开始使用 Drone CI 是用 CI 的一个伟大选项,因为它的 [drone/drone-cli](https://github.com/drone/drone-cli) 不需要搭建 CI 服务,你仅需要在本地通过运行 `drone exec` 来执行 CI 步骤。 + +为了搭建我们的微服务框架,我们需要: + +1. 安装 Drone CI drone-cli +2. 安装 `protoc` 和 `protoc-gen-go` 的 docker 环境 +3. 长远着想,加一个 `Makefile` 文件 +4. 加一个 `.drone.yml` 配置文件,写明生成 Go 代码的构建步骤 + +### 安装 Drone CI + +安装 drone-cli 很简单。如果你使用的是 amd64 Linux 机器,执行下面的命令。或者从 [drone/drone-cli](https://github.com/drone/drone-cli) 发布页拉取你需要的版本解包到你机器的 `/usr/local/bin` 或通用的执行路径。 + +```bash +cd /usr/local/bin +wget https://github.com/drone/drone-cli/releases/download/v1.2.0/drone_linux_amd64.tar.gz +tar -zxvf drone*.tar.gz && rm drone*.tar.gz +``` + +### 创建一个构建环境 + +Drone CI 通过运行你提供的 Docker 环境中声明的 `.drone.yml` 文件里的 CI 步骤来工作。在我们的构建环境中,我已经创建了 `docker/build/` ,下面有一个 `Dockerfile` 和 `Makefile` ,以此来协助构建和发布我们的案例中需要的构建镜像。 + +```dockerfile +FROM golang:1.13 + +# install protobuf +ENV PB_VER 3.10.1 +ENV PB_URL https://github.com/google/protobuf/releases/download/v${PB_VER}/protoc-${PB_VER}-linux-x86_64.zip + +RUN apt-get -qq update && apt-get -qqy install curl Git make unzip gettext rsync + +RUN mkdir -p /tmp/protoc && \ + curl -L ${PB_URL} > /tmp/protoc/protoc.zip && \ + cd /tmp/protoc && \ + unzip protoc.zip && \ + cp /tmp/protoc/bin/protoc /usr/local/bin && \ + cp -R /tmp/protoc/include/* /usr/local/include && \ + chmod go+rx /usr/local/bin/protoc && \ + cd /tmp && \ + rm -r /tmp/protoc + +# Get the source from GitHub +RUN Go get -u google.golang.org/grpc + +# Install protoc-gen-go +RUN Go get -u github.com/golang/protobuf/protoc-gen-go +``` + +在 `Makefile` 中实现 `make && make push` ,这样就可以快速构建和发布我们的镜像到 docker 注册中心。本例中的镜像发布在 `titpetric/microservice-build`,但我建议这里你用自己的镜像来操作。 + +```makefile +.PHONY: all docker push test + +IMAGE := titpetric/microservice-build + +all: docker + +docker: + docker build --rm -t $(IMAGE) . + +push: + docker push $(IMAGE) + +test: + docker run -it --rm $(IMAGE) sh +``` + +### 创建 Makefile helper + +运行 `drone exec` 很简单,但是我们的需要会随着时间推移越来越多,Drone CI 步骤也会变得越来越复杂和难以管理。使用 Makefile 可以让我们添加更复杂的在 Drone 运行的目标。目前我们先以一个最简单的 Makefile 起步,这个 Makefile 仅包含对 `drone exec` 的一次调用: + +```makefile +.PHONY: all + +all: + drone exec +``` + +这是一个简单的 Makefile,意味着我们仅通过运行 `make` 就可以构建我们的 Drone CI 工程。以后我们会为了支持新的需要去扩展它,但是目前我们仅需要确保它可用就行了。 + +### 创建 Drone CI 配置 + +通过下面的代码,我们可以定义构建我们的 protobuf 结构定义的初始 `.drone.yml` 文件,也可以对基本代码做一些修改: + +```yaml +workspace: + base: /microservice + +kind: pipeline +name: build + +steps: +- name: test + image: titpetric/microservice-build + pull: always + commands: + - protoc --proto_path=$GOPATH/src:. -Irpc/stats --go_out=paths=source_relative:. rpc/stats/stats.proto + - Go mod tidy > /dev/null 2>&1 + - Go mod download > /dev/null 2>&1 + - Go fmt ./... > /dev/null 2>&1 +``` + +这几步操作是为了处理我们的 go.mod/go.sum 文件,在我们的基础代码上运行 `go fmt` 也是。 + +`commands:` 下面定义的第一步是可以生成我们声明的 message 对应的 Go 定义的 `protoc` 命令。我们的 `stats.proto` 文件所在的文件夹中,会创建一个 `stats.pb.go` 文件,每个结构都声明了 `message {}`。 + +## 结语 + +所以,这里我们做了什么来实现上面的结果: + +- 我们用我们的 `protoc` 代码生成环境创建的我们的 CI 构建镜像 +- 我们使用 Drone CI 作为我们的本地构建服务,未来可以迁移到宿主 CI +- 我们为微服务 message 结构创建了 protobuf 定义 +- 我们为编码/解码 protobuf message 生成了合适的 Go 代码 + +从现在起,我们将尝试去实现一个 RPC 服务。 + +本文是 [Go 微服务驾到](https://leanpub.com/go-microservices) 书的一部分。在圣诞节之前我们会每天发布一篇文章。请考虑购买电子书支持我们的创作,反馈给我,这会让以后的文章对你更有用处。 + +本系列的所有文章都在这里 [the advent2019 tag](https://scene-si.org/tags/advent2019/)。 + +## 如果你读到了这里 + +推荐购买我的书: + +- [Advent of Go Microservices](https://leanpub.com/go-microservices) +- [API Foundations in Go](https://leanpub.com/api-foundations) +- [12 Factor Apps with Docker and Go](https://leanpub.com/12fa-docker-golang) + +--- + +via: https://scene-si.org/2019/12/01/introduction-to-protobuf-messages/ + +作者:[Tit Petric](http://github.com/titpetric) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191202-Go-How-Are-Random-Numbers-Generated.md b/published/tech/20191202-Go-How-Are-Random-Numbers-Generated.md new file mode 100644 index 000000000..ba489c1db --- /dev/null +++ b/published/tech/20191202-Go-How-Are-Random-Numbers-Generated.md @@ -0,0 +1,165 @@ +首发于:https://studygolang.com/articles/25930 + +# Go:随机数是怎样产生的? + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191202-Go-How-Are-Random-Numbers-Generated/01.png) +

Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.

+ +*这篇文章基于 Go 1.13 版本* + +Go 实现了两个包来产生随机数: + +- 在包 `math/rand` 的一个伪随机数生成器( PRNG ) +- 在包 `crypto/rand` 中实现的加密伪随机数生成器( CPRNG ) + +如果这两个包都产生了随机数,则将基于真正的随机数和性能之间取舍 + +## 确定的结果 + +Go 的 `rand` 包会使用相同的源来产生一个确定的伪随机数序列。这个源会产生一个不变的数列,稍后在执行期间使用。将你的程序运行多次将会读到一个完全相同的序列并产生相同的结果。让我们用一个简单的例子来尝试一下: + +```go +func main() { + for i := 0; i < 4; i++ { + println(rand.Intn(100)) + } +} +``` + +多次运行这个程序将会产生相同的结果: + +``` +81 +87 +47 +59 +``` + +由于源代码已经发布到 Go 的官方标准库中,因此任何运行此程序的计算机都会得到相同的结果。但是,由于 Go 仅保留一个生成的数字序列,我们可能想知道 Go 是如何管理用户请求的时间间隔的。Go 实际上使用此数字序列来播种一个产生这个随机数的源,然后获取其请求间隔的模。例如,运行相同的程序,最大值为 10,则模 10 的结果相同。 + +``` +1 +7 +7 +9 +``` + +让我们来看一下如何在每次运行我们的程序时得到不同的序列。 + +## 播种 + +Go 提供一个方法, `Seed(see int64)` ,该方法能让你初始化这个默认序列。默认情况下,它会使用变量 1。使用另一个变量将会提供一个新的序列,但会保持确定性: + +```go +func main() { + rand.Seed(2) + for i := 0; i < 4; i++ { + println(rand.Intn(100)) + } +} +``` + +这些是新的结果: + +``` +86 +86 +92 +40 +``` + +在你每次运行这个程序时,这个序列将会保持不变。这是构建此序列的工作流: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191202-Go-How-Are-Random-Numbers-Generated/02.png) + +

The sequence is pre-generated at the bootstrap

+获取一个全新序列的解决方案是使用一个在运行时能改变的变量,比如当前时间: + +```go +func main() { + rand.Seed(time.Now().UnixNano()) + for i := 0; i < 3; i++ { + println(rand.Intn(100)) + } +} +``` + +由于当前纳秒数在任何时刻都是不同的,因此这个程序每次运行都会使用一个不同的序列。然而,尽管这个序列在每次运行都是不同的,可这些数字仍是伪随机数。如果你准备牺牲性能来获得更好的随机性,那么 Go 已经为你提供了另一种实现方式。 + +## 随机数生成器 + +Go 的标准库也提供了一个适用于加密应用的随机数生成器。因此,理所当然的,生成的随机数并不固定,并且一定会提供更好的随机性。这有一个例子使用了这个新包 `cryto/rand` : + +```go +func main() { + for i := 0; i < 4; i++ { + n, _ := rand.Int(rand.Reader, big.NewInt(100)) + println(n.Int64()) + } +} +``` + +这是结果: + +``` +12 +24 +56 +19 +``` + +多次运行这个程序将会得到不同的结果。在内部,Go 应用了如下规则: + +> *在 Linux 和 FreeBSD 系统上,Reader 会使用 getrandom(2) (如果可用的话),否则使用 /dev/urandom。* +> +> *在 OpenBSD 上,Reader 会使用 getentropy(2)。* +> +> *在其他的类 Unix 系统上,Reader 会读取 /dev/urandom。* +> +> *在 Windows 系统上,Reader 会使用 CryptGenRandom API.* +> +> *在 Wasm 上,Reader 会使用 Web Cryto API。* + +但是,获得更好的质量意味着性能降低,因为它必须执行更多的操作并且不能使用预生成的序列。 + +## 性能 + +为了理解生成随机数的两种不同方式之间的折衷,我基于先前的两个例子运行了一个基准测试。结果如下: + +``` +name time/op +RandWithCrypto-8 272ns ± 3% +name time/op +RandWithMath-8 22.8ns ± 4% +``` + +不出所料,`crypto` 包更慢一些。但是,如果你不用去处理安全的随机数,那么 `math` 包就足够了并且它将会给你提供最好的性能。 + +你也可以调整默认数字生成器,由于内部互斥锁的存在,它是并发安全的。如果生成器并不在并发环境下使用,那么你就可以在不使用锁的情况下创建你自己的生成器: + +```go +func main() { + gRand := rand.New(rand.NewSource(1).(rand.Source64)) + for i := 0; i < 4; i++ { + println(gRand.Intn(100)) + } +} + +``` + +性能会更好: + +``` +name time/op +RandWithMathNoLock-8 10.7ns ± 4% +``` + +--- + +via:https://medium.com/a-journey-with-go/go-how-are-random-numbers-generated-e58ee8696999 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[sh1luo](https://github.com/sh1luo) +校对:[lxbwolf](https://github.com/lxbwolf) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191202-Writing-Friendly-Command-Line-Applications.md b/published/tech/20191202-Writing-Friendly-Command-Line-Applications.md new file mode 100644 index 000000000..ab9a4ef20 --- /dev/null +++ b/published/tech/20191202-Writing-Friendly-Command-Line-Applications.md @@ -0,0 +1,284 @@ +首发于:https://studygolang.com/articles/27145 + +# 编写友好的命令行应用程序 + +我来给你讲一个故事... + +1986 年,[Knuth](https://en.wikipedia.org/wiki/Donald_Knuth) 编写了一个程序来演示[文学式编程](https://en.wikipedia.org/wiki/Literate_programming) 。 + +这段程序目的是读取一个文本文件,找到 n 个最常使用的单词,然后有序输出这些单词以及它们的频率。 Knuth 写了一个完美的 10 页程序。 + +Doug Mcllory 看到这里然后写了 `tr -cs A-Za-z '\n' | tr A-Z a-z | sort | uniq -c | sort -rn | sed ${1}q` 。 + +现在是 2019 年了,为什么我还要给你们讲一个发生在 33 年前(可能比一些读者出生的还早)的故事呢? 计算领域已经发生了很多变化了,是吧? + +[林迪效应](https://en.wikipedia.org/wiki/Lindy_effect) 是指如一个技术或者一个想法之类的一些不易腐烂的东西的未来预期寿命与他们的当前存活时间成正比。 太长不看版——老技术还会存在。 + +如果你不相信的话,看看这些: + +* [oh-my-zsh](https://github.com/ohmyzsh/ohmyzsh) 在 GitHub 上已经快有了 100,000 个 星星了 +* [《命令行中的数据科学》](https://www.datascienceatthecommandline.com/) +* [命令行工具能够比你的 Hadoop 集群快 235 倍](https://adamdrake.com/command-line-tools-can-be-235x-faster-than-your-hadoop-cluster.html) +* ... + +现在你应该被说服了吧, 让我们来讨论以下怎么使你的 Go 命令行程序变得友好。 + +## 设计 + +当你在写命令行应用程序的时候, 试试遵守 基础的 [Unix 哲学](http://www.catb.org/esr/writings/taoup/html/ch01s06.html) + +* 模块性规则: 编写通过清晰的接口连接起来的简单的部件 +* 组合性规则: 设计可以和其他程序连接起来的程序 +* 缄默性规则:当一个程序没有什么特别的事情需要说的时候,它就应该闭嘴 + +这些规则能指导你编写做一件事的小程序。 + +* 用户需要从 REST API 中读取数据的功能 ? 他们会将 `curl` 命令的输出通过管道输入到你的程序中 +* 用户只想要前 n 个结果 ? 他们可以把你的程序的输出结果通过管道输入到 `head` 命令中 +* 用户指向要第二列数据 ? 如果你的输出结果以 tab 为分割, 他们就可以把你的输出通过管道输入到 `cut` 或 `awk` 命令 + +如果你没有遵从上述要求 , 没有结构性的组织你的命令行接口 , 你可能会像下面这种情况一样的停止。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/writing-friendly-command-line-application/1.png) + +## 帮助 + +让我们来假定你们团队有一个叫做 `nuke-db` 的实用工具 。 你忘了怎么调用它然后你: + +```shell +$ ./nuke-db --help +database nuked (译者注:也就说本意想看使用方式,但却直接执行了) +``` + +OMG! + +使用 [flag 库](https://golang.org/pkg/flag/) ,你可以用额外的两行代码添加对于 `--help` 的支持。 + +```go +package main + +import ( + "flag" // extra line 1 + "fmt" +) + +func main() { + flag.Parse() // extra line 2 + fmt.Println("database nuked") +} +``` + +现在你的程序运行起来是这个样子: + +```shell +$ ./nuke-db --help +Usage of ./nuke-db: +$ ./nuke-db +database nuked +``` + +如果你想提供更多的帮助 , 使用 `flag.Usage` + +```go +package main + +import ( + "flag" + "fmt" + "os" +) + +var usage = `usage: %s [DATABASE] + +Delete all data and tables from DATABASE. +` + +func main() { + flag.Usage = func() { + fmt.Fprintf(flag.CommandLine.Output(), usage, os.Args[0]) + flag.PrintDefaults() + } + flag.Parse() + fmt.Println("database nuked") +} +``` + +现在 : + +```shell +$ ./nuke-db --help +usage: ./nuke-db [DATABASE] + +Delete all data and tables from DATABASE. +``` + +## 结构化输出 + +纯文本是通用的接口。 然而,当输出变得复杂的时候, 对机器来说处理格式化的输出会更容易。最普遍的一种格式当然是 JSON。 + +一个打印的好的方式不是使用 `fmt.Printf` 而是使用你自己的既适合于文本也适合于 JSON 的打印函数。让我们来看一个例子: + +```go +package main + +import ( + "encoding/json" + "flag" + "fmt" + "log" + "os" +) + +func main() { + var jsonOut bool + flag.BoolVar(&jsonOut, "json", false, "output in JSON format") + flag.Parse() + if flag.NArg() != 1 { + log.Fatal("error: wrong number of arguments") + } + + write := writeText + if jsonOut { + write = writeJSON + } + + fi, err := os.Stat(flag.Arg(0)) + if err != nil { + log.Fatalf("error: %s\n", err) + } + + m := map[string]interface{}{ + "size": fi.Size(), + "dir": fi.IsDir(), + "modified": fi.ModTime(), + "mode": fi.Mode(), + } + write(m) +} + +func writeText(m map[string]interface{}) { + for k, v := range m { + fmt.Printf("%s: %v\n", k, v) + } +} + +func writeJSON(m map[string]interface{}) { + m["mode"] = m["mode"].(os.FileMode).String() + json.NewEncoder(os.Stdout).Encode(m) +} +``` + +那么 + +```shell +$ ./finfo finfo.go +mode: -rw-r--r-- +size: 783 +dir: false +modified: 2019-11-27 11:49:03.280857863 +0200 IST +$ ./finfo -json finfo.go +{"dir":false,"mode":"-rw-r--r--","modified":"2019-11-27T11:49:03.280857863+02:00","size":783} + +``` + +## 处理 + +有些操作是比较耗时的,一个是他们更快的方法不是优化代码,而是显示一个旋转加载符或者进度条。不要不信我,这有一个来自 [Nielsen 的研究](https://www.nngroup.com/articles/progress-indicators/) 的引用 + +> 看到运动的进度条的人们会有更高的满意度体验而且比那些得不到任何反馈的人平均多出三倍的愿意等待时间。 + +## 旋转加载 + +添加一个旋转加载不需要任何特别的库 + +```go +package main + +import ( + "flag" + "fmt" + "os" + "time" +) + +var spinChars = `|/-\` + +type Spinner struct { + message string + i int +} + +func NewSpinner(message string) *Spinner { + return &Spinner{message: message} +} + +func (s *Spinner) Tick() { + fmt.Printf("%s %c \r", s.message, spinChars[s.i]) + s.i = (s.i + 1) % len(spinChars) +} + +func isTTY() bool { + fi, err := os.Stdout.Stat() + if err != nil { + return false + } + return fi.Mode()&os.ModeCharDevice != 0 +} + +func main() { + flag.Parse() + s := NewSpinner("working...") + for i := 0; i < 100; i++ { + if isTTY() { + s.Tick() + } + time.Sleep(100 * time.Millisecond) + } + +} + +``` + +运行它你就能看到一个小的旋转加载在运动。 + +## 进度条 + +对于进度条, 你可能需要一个额外的库如 `github.com/cheggaaa/pb/v3` + +```go +package main + +import ( + "flag" + "time" + + "github.com/cheggaaa/pb/v3" +) + +func main() { + flag.Parse() + count := 100 + bar := pb.StartNew(count) + for i := 0; i < count; i++ { + time.Sleep(100 * time.Millisecond) + bar.Increment() + } + bar.Finish() + +} +``` + +## 结语 + +现在差不多 2020 年了,命令行应用程序仍然会存在。 它们是自动化的关键,如果写得好,能提供优雅的“类似乐高”的组件来构建复杂的流程。 + +我希望这篇文章将激励你成为一个命令行之国的好公民。 + +--- +via: https://blog.gopheracademy.com/advent-2019/cmdline/ + +作者:[Miki Tebeka](https://blog.gopheracademy.com) +译者:[Ollyder](https://github.com/Ollyder) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191204-Go-Work-Stealing-in-Go-Scheduler.md b/published/tech/20191204-Go-Work-Stealing-in-Go-Scheduler.md new file mode 100644 index 000000000..a78aa65d4 --- /dev/null +++ b/published/tech/20191204-Go-Work-Stealing-in-Go-Scheduler.md @@ -0,0 +1,123 @@ +首发于:https://studygolang.com/articles/27146 + +# Go 调度器的任务窃取(Work-Stealing) + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-work-stealing-in-go-Scheduler/cover.png) +> Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French. + +ℹ️ *这篇文章基于 Go 1.13 环境。* + +在 Go 中创建 Goroutine 既方便又快捷,然而 Go 在同一时间内最多在一个核上运行一个 Goroutine,因此需要一种方法来存放其他的 Goroutine,从而确保处理器(processor)负载均衡。 + +## Goroutine 队列 + +Go 使用两级队列来管理等待中的 Goroutine,分别为本地队列和全局队列。每一个处理器都拥有本地队列,而全局队列是唯一的,且能被所有的处理器访问到: + +![Global and local queues](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-work-stealing-in-go-Scheduler/ws-1.png) + +每个本地队列都有最大容量,为 256。在容量满了之后,任意新到来的 Goroutine 都会被放置到全局队列。下面的例子是,生产了上千个 Goroutine 的程序: + +```go +func main() { + var wg sync.WaitGroup + + for i := 0;i < 2000 ;i++ { + wg.Add(1) + Go func() { + a := 0 + + for i := 0; i < 1e6; i++ { + a += 1 + } + + wg.Done() + }() + } + + wg.Wait() +} +``` + +下面是拥有两个处理器的调度器追踪数据(traces): + +![Details of the local and global queues](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-work-stealing-in-go-Scheduler/ws-2.png) + +追踪数据通过 `runqueue` 展示了全局队列中 Goroutine 的数量,以及方括号中 `[3 256]` 的本地队列 Goroutine 数量(分别为 `P0` 和 `P1`)。当本地队列满了,积压了 256 个等待中的 Goroutine 后,下一个 Goroutine 会被压栈到全局队列中,正如我们从 `runqueue` 看到的数量增长一样。 + +*Goroutine 仅在本地队列满载之后才会加入到全局队列;它也会在 Go 往调度器中批量注入时被加到全局队列,例如,网络轮询器(network poller) 或者在垃圾回收期间等待的 Goroutine。* + +下面是上一个例子的图示: + +![Local queues have up to 256 Goroutines](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-work-stealing-in-go-Scheduler/ws-3.png) + +不过,我们还想知道,为什么本地队列 `P0` 在上一个列子中不为空。因为 Go 使用了其他策略确保每个处理器都有任务处理。 + +## 任务窃取 + +如果处理器没有任务可处理,它会按以下规则来执行,直到满足某一条规则: + +- 从本地队列获取任务 +- 从全局队列获取任务 +- 从网络轮询器获取任务 +- 从其它的处理器的本地队列窃取任务 + +在我们前面的例子中,主函数在 `P1` 上运行并创建 Goroutine。当第一批 gourinte 已经进入了 `P1` 的本地队列时,`P0` 正在寻找任务。然而,它的本地队列,全局队列,以及网络轮询器都是空的。最后的解决方法是从 `P1` 中窃取任务。 + +![Work-stealing by P0](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-work-stealing-in-go-Scheduler/ws-4.png) + +下面是调度器在发生任务窃取前后的追踪数据: + +![Work-stealing by P0](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-work-stealing-in-go-Scheduler/ws-8.png) + +追踪数据展示了,处理器是如何从其它处理器中窃取任务的。它从(其他处理器的)本地队列中取走一半的 Goroutine;在七个 Goroutine 中,偷走了四个 —— 其中一个立马在 `P0` 执行,剩下的放到本地队列。现在处理器间工作处于负载良好的状态。这能通过执行 tracing 来确认: + +![img](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-work-stealing-in-go-Scheduler/ws-5.png) + +Goroutine 被合理地分发,然后因为没有 I/O,Goroutine 被链式执行而不需要切换。我们现在看一下,当出现例如涉及到文件操作等 I/O 时,会发生什么。 + +## I/O 与全局队列 + +一起看下涉及到文件操作的例子: + +```go +func main() { + var wg sync.WaitGroup + + for i := 0;i < 20 ;i++ { + wg.Add(1) + Go func() { + a := 0 + for i := 0; i < 1e6; i++ { + a += 1 + if i == 1e6/2 { + bytes, _ := ioutil.ReadFile(`add.txt`) + inc, _ := strconv.Atoi(string(bytes)) + a += inc + } + } + wg.Done() + }() + } + + wg.Wait() +} +``` + +变量 `a` 随着时间以文件的字节数增加,下面是新的追踪数据: + +![img](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-work-stealing-in-go-Scheduler/ws-6.png) + +在这个例子中,我们能看到每一个 Goroutine 不只被一个处理器处理。在系统调用的情况下,当调用完成后,Go 使用网络轮询器从全局队列中把 gouroutine 取回来。这里是 Goroutine #35 的一个示意图: + +![I/O operations put the work back to the global queue](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-work-stealing-in-go-Scheduler/ws-7.png) + +当一个处理器能从全局队列中获取任务,第一个可用的处理器( `P`) 会执行这个 Goroutine。这个行为解释了,为什么一个 Goroutine 能在不同的处理器中运行,也展示了 Go 是如何让空闲的处理器资源运行 Goroutine,从而进行系统调用的优化。 + +--- +via: https://medium.com/a-journey-with-go/go-work-stealing-in-go-scheduler-d439231be64d + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[LSivan](https://github.com/LSivan) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191210-Go-GOMAXPROCS-Live-Updates.md b/published/tech/20191210-Go-GOMAXPROCS-Live-Updates.md new file mode 100644 index 000000000..79c4f7c9c --- /dev/null +++ b/published/tech/20191210-Go-GOMAXPROCS-Live-Updates.md @@ -0,0 +1,79 @@ +首发于:https://studygolang.com/articles/28989 + +# Go:GOMAXPROCS 和实时更新 + +![由 Renee French 创作的原始 Go Gopher 作品,为“ Go 的旅程”创作的插图。](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20191210-Go-GOMAXPROCS-And-Live-Updates/1_Ct_BMGzFD4eKn6ztnR1iYA.png) + +ℹ️ 这篇文章基于 Go 1.13。 + +`GOMAXPROCS` 控制着同时执行代码的 OS 线程的最大数量。这(GOMAXPROCS 值的设定)在程序启动期间,甚至在程序运行期间完成。一般来说,Go 将这个值设置为可用逻辑 CPU 的数量,但并不总是这样。 + +## 默认值 + +从 [Go 1.5](https://golang.org/doc/go1.5) 开始,`GOMAXPROCS` 的默认值由 1 改成了可见 CPU 的数量。这个改动也许是由于 Go 调度器和 Goroutine 上下文切换的改善。确实,在 Go 的早期阶段,旨在以频繁切换 Goroutine 的方式来并行工作的程序遭受了进程切换的困扰。 + +这个 `GOMAXPROCS` 新值的[提案](https://docs.google.com/document/d/1At2Ls5_fhJQ59kDK2DFVhFu3g5mATSXqqV5QrxinasI/edit)提供了展示这个提升的基准测试结果: + +- 第一个基准测试,创建了 100 个由 channel 通信的 goroutine, 这些 channel 包括缓存的和非缓存的: + +![使用更高的 `GOMAXPROCS` 值带来的调度器性能提升](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20191210-Go-GOMAXPROCS-And-Live-Updates/Scheduler-improvement-with-higher-value-of-GOMAXPROCS.png) + +- 用于质数生成的第二个基准测试展示了使用更多的核心将一个性能急剧下降的趋势转变为了一个巨大提升的趋势: + +![更高的 `GOMAXPROCS` 值现在产生了很大的积极影响](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20191210-Go-GOMAXPROCS-And-Live-Updates/Higher-value-for-GOMAXPROCS-has-now-a-great-positive-impact.png) + +调度器已明显解决了单线程程序中遇到的问题,且现在可以很好的扩展。 + +## 在运行期间更新 + +Go 允许 `GOMAXPROCS` 在程序执行期间的任何时候进行更新。更新可以是虚拟机或者容器重新配置可用 CPU 数量所造成的。由于增减处理器数量的指令可以在任何时候发生,因此 Go 一进入“停止世界(Stop the World)”阶段该指令就生效。增加新的处理器是十分简单直接的,创建本地缓存 `mcache` 并且将新添加的处理器放入空闲队列中。这是当处理器从两个变成增长为三个时,一个新分配的 `P` 的例子: + +![`GOMAXPROCS` 增加一个处理器](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20191210-Go-GOMAXPROCS-And-Live-Updates/GOMAXPROCS-is-growing-by-one-processor.png) + +之后,当重新开始调度的时候,新的 `P` 获取一个 Goroutine 运行: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20191210-Go-GOMAXPROCS-And-Live-Updates/the-new-P-gets-a-goroutine-to-run.png) + +减少处理器数量稍微有点复杂。移除一个 `P` 需要通过将 Goroutine 转移到全局队列的方式,将其本地的 Goroutine 队列清空: + +![goroutine 转移到全局队列](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20191210-Go-GOMAXPROCS-And-Live-Updates/Goroutines-moves-to-the-global-queue.png) + +之后,要移除的 `P` 必须释放本地的 `mcache` 以便重复使用: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20191210-Go-GOMAXPROCS-And-Live-Updates/free-the-local-mcache.png) + +这是当 `P` 从二调整到一,之后再从一调整到三个 `P` 的追踪信息的例子: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20191210-Go-GOMAXPROCS-And-Live-Updates/example-of-the-tracing.png) + +## GOMAXPROCS=1 + +调整 `GOMAXPROCS` 到一个更大的值并不意味着你的程序一定会运行的更快。Go 文档解释地很清楚: + +> 这取决于程序的性质。本质上是顺序处理的问题无法通过增加 Goroutine 的方式提高处理速度。当问题本质上是并行处理的时候,并发才会变成并行。 + +来看下对于某些程序而言,并发是如何满足需要的。这是一些检查某些 URL 的代码,用来感知这些网站是否正常运行: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20191210-Go-GOMAXPROCS-And-Live-Updates/code-that-checks-some-URLs%20.png) + +由于代码执行过程中有很多停顿,这段示例代码在并发情况下工作地很好,给了 Go 调度器空间在等待期间运行其他 goroutine。这是通过 `test` 包的 `-cpu=1,2,4,5` 标志,拿到的不同 `GOMAXPROCS` 的值的情况下的基准测试结果: + +```bash +name time/op +URLsCheck-8 4.19s ± 2% +URLsCheck-4 4.30s ± 5% +URLsCheck-2 4.33s ± 4% +URLsCheck-1 4.14s ± 1% +``` + +在这里增加并行不会带来任何的性能提升。使用 CPU 的全部容量在许多时候会带来性能提升。然而,最好在不同的(GOMAXPROCS)值下运行测试或基准测试来确定具体的表现。 + +--- + +via: https://medium.com/a-journey-with-go/go-gomaxprocs-live-updates-407ad08624e1 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[dust347](https://github.com/dust347) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://raw.githubusercontent.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191210-The-Go-runtime-scheduler-clever-way-of-dealing-with-system-calls.md b/published/tech/20191210-The-Go-runtime-scheduler-clever-way-of-dealing-with-system-calls.md new file mode 100644 index 000000000..5bbc1c672 --- /dev/null +++ b/published/tech/20191210-The-Go-runtime-scheduler-clever-way-of-dealing-with-system-calls.md @@ -0,0 +1,25 @@ +首发于:https://studygolang.com/articles/28438 + +# Go 运行时调度器处理系统调用的巧妙方式 + +[goroutine](https://tour.golang.org/concurrency/1) 是 Go 的一个标志性特点,是被 Go 运行时所管理的轻量线程。Go 运行时使用[一个 M:N 工作窃取调度器](https://rakyll.org/scheduler/)实现 goroutine,将 Goroutine 复用在操作系统线程上。调度器有着特殊的术语用来描述三个重要的实体;G 是 goroutine,M 是 OS 线程(一个“机器 machine”),P 是“处理器(processor)”,它的核心是有限的资源,而 M 需要这些资源来运行 Go 代码。限制 P 的供应是 Go 用来限制一次执行多少操作以避免整个系统超载的手段。通常来说,每个 OS 所报告的实际的 CPU 有一个对应的 P (P 的数量是 [GOMAXPROCS](https://golang.org/pkg/runtime/))。 + +当 Goroutine 执行 网络 IO 或者任何觉得可以异步完成的系统调用操作时,Go 有一个完整的运行时子系统,[netpoller](https://morsmachine.dk/netpoller),(使用类似 [epoll](https://medium.com/@copyconstruct/the-method-to-epolls-madness-d9d2d6378642) 的系统调用机制)将看起来像多个单独的同步操作转换为一个单独的等待。goroutine 并没有真正进行阻塞的系统调用,而是像等待一个 channel 就绪那样进入休眠状态等待其网络套接字。如果很难有效地实现,概念上讲这些都是直白的。 + +无论如何,网络 IO 以及类似的东西远不是 Go 程序可以处理的唯一的系统调用,因此 Go 也必须处理阻塞的系统调用。对 Goroutine 的 M 来说,处理阻塞的系统调用的直接方式是在系统调用前释放 P ,并且在系统调用恢复后尝试重新获取 P 。如果那时候没有空闲的 P ,goroutine 会随着其他等待运行的任务被停放在调度器中。 + +虽然理论上所有的系统调用都是阻塞的,在实践中不是所有的调用都会阻塞。例如,在现代系统中,获取当前时间的“系统调用”可能甚至没有进入内核(见 Linux 的 [vdso(7)](http://man7.org/linux/man-pages/man7/vdso.7.html))。让 Goroutine 完成释放他们当前的 P 的全部工作再为了这些系统调用重新获取一个 P 有两个问题:首先,所有涉及到的数据结构的锁定(和释放)有着很大的开销。其次,如果可运行的 Goroutine 比 P 多,进行这类系统调用的 Goroutine 无法重新获取 P 并且不得不把自己停放;释放 P 的瞬间,其他 Goroutine 就会被调度到上面。这是额外的运行时开销,有点不公平,并且不利于进行快速系统调度的目的(尤其是那些不进入内核的调用)。 + +所以 Go 运行时和调度器实际上有两种处理阻塞系统调用的方法,一种悲观方式,应用于预计会很慢的系统调用;另一种乐观方式,应用于预计会很快的系统调用。悲观的系统调用路径实现了直接的方法,运行时在系统调用前主动释放 P,之后尝试将 P 找回来,如果无法获取则停放自身。乐观的系统调用路径不会释放 P,相反,会设置一个特殊的 P 的状态标识并继续进行系统调用。一个特殊的内部 goroutine,sysmon goroutine,定期执行并寻找设置了这个“进行系统调用中”状态的时间太长了的 P,并将 P 从进行系统调用的 Goroutine 那里偷走。当系统调用返回,运行时代码检查它的 P 是否被偷走,如果没有则继续执行(如果 P 被偷走了的话,运行时会尝试获取其他的 P,如果失败可能会停放 goroutine)。 + +如果一切顺利,乐观的系统调用路径有着非常低的开销(大多数情况下,需要几个[原子比较和交换](https://en.wikipedia.org/wiki/Compare-and-swap)操作)。如果不顺利并且可运行的 Goroutine 的数量比 P 多,一个 P 会有不必要的空闲,通常可能是数十微秒(sysmon Goroutine 最多每 20 微秒运行一次,但如果似乎没有必要的话可以减少运行频率)。可能存在着最坏的情况,但是一般来说,在 Go 运行时方面这是一个值得的抉择。 + +--- + +via: https://utcc.utoronto.ca/~cks/space/blog/programming/GoSchedulerAndSyscalls + +作者:[ChrisSiebenmann](https://twitter.com/thatcks/) +译者:[dust347](https://github.com/dust347) +校对:[JYSDeveloper](https://github.com/JYSDeveloper) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191211-A-quick-trick-using-Go-structs-to-create-namespaces.md b/published/tech/20191211-A-quick-trick-using-Go-structs-to-create-namespaces.md new file mode 100644 index 000000000..45ca4b86e --- /dev/null +++ b/published/tech/20191211-A-quick-trick-using-Go-structs-to-create-namespaces.md @@ -0,0 +1,47 @@ +首发于:https://studygolang.com/articles/26300 + +# Go 中的黑桃 A:使用结构体创建命名空间 + +假设,但不是凭空想象,在你的程序中,你注册了一堆 [expvar 包的统计变量](https://golang.org/pkg/expvar/),用来在暴露出去的 JSON 结果中能有一个容易辨识的名字。普通的实现方式下,你可能会有一大堆全局变量,对应着程序追踪的各种信息。这些变量与其他的全局变量混成一团,这毫无美感,如果我们能规避这种情况,那么事情会变得不那么糟糕。 + +归功于 Go 对匿名结构类型的支持,我们可以实现。我们可以基于匿名结构类型创建一个变量集合的命名空间: + +```go +var events struct { + connections, messages [expvar.Int](expvar.Int) + + tlsconns, tlserrors [expvar.Int](expvar.Int) +} +``` + +在我们的代码中,我们可以使用 `events.connects` 等等,而不是必须用一些很糟糕或容易引起歧义的名字。 + +我们也可以在全局等级范围外用这种方法。你可以在这种命名空间结构内把任意的变量名集合隔离开。一个例子就是把计数变量嵌入到另一个结构体中: + +```go +type ipMap struct { + sync.Mutex + ips map[string]int + stat struct { + Size, Adds, Lookups, Dels int + } +} +``` + +原因很明显,这对于不需要进行初始化的变量类型是最好的解决方案;其他的变量类型需要进行一些初始化,这稍微有一点点笨重。 + +这可能不合某些人的口味,我也不知道这在 Go 中是不是好的做法。我的个人观点是,我与其用前缀 `prefix_` 来隔离变量名,不如用前缀 `prefix.` ,尽管人为地引入了这样的匿名结构体。但是一些人可能会有不同看法。即使在我看来,它也确实是侵入较大的修改,但可能它是合法的。 + +(为了更加明确地统计计数信息,[这也是一种方便地暴露所有信息的方法](https://utcc.utoronto.ca/~cks/space/blog/programming/GoExpvarNotes)) + +出于好奇我快速浏览了当前开发中的 Go 编译器和标准库,隐约在几处地方发现了疑似使用这种方式的地方。并不是所有的使用方式都一样,所以看了源码后我要说的重点是,这似乎也不是一种完全不可容忍或被原作者反对的观点。 + +--- + +via: https://utcc.utoronto.ca/~cks/space/blog/programming/GoStructsForNamespaces + +作者:[Chris Siebenmann](https://utcc.utoronto.ca/~cks/) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191211-An-interesting-way-to-leak-memory-with-Go-slices.md b/published/tech/20191211-An-interesting-way-to-leak-memory-with-Go-slices.md new file mode 100644 index 000000000..eefa11c7b --- /dev/null +++ b/published/tech/20191211-An-interesting-way-to-leak-memory-with-Go-slices.md @@ -0,0 +1,36 @@ +首发于:https://studygolang.com/articles/28439 + +# Go 切片的一种有趣内存泄漏方式 + +今天我在看 Prashant Varanasi 的 Go 发布会演讲:[使用火焰图进行生产分析](https://www.youtube.com/watch?v=aAhNDgEZj_U)(Analyzing production using Flamegraphs),在演讲开始的第 28 分钟他提到了一种涉及切片的有趣且棘手的内存泄漏。为了自我提升,我将在这里写一下该内存泄漏的一种形式,并说明它是如何发生的。 + +首先,对于像 Go 这样的垃圾收集语言来说,**内存泄漏是保留了对对象的非预期引用所造成的**。垃圾收集器会帮你寻找并释放对象,但前提是它们事实上并没有被使用。如果你保留了对它们的引用,它们会留下来。 +有时最终结果很简单(也行你故意保留一个较小的结构,但没意识到它引用了一个较大的结构),但有时候这种保留隐藏在某些东西的运行时实现里。这改变了我们对切片的看法。 + +简化之后,Prashant 处理的代码在一个切片中维护了当前在使用的元素的集合。当一个元素不再被使用时,它被转移到了切片的末尾,然后切片被截断而缩小(保持不变的是切片只保留使用的元素)。然而,*缩小切片并不会缩小其依赖的数组*,用 Go 的术语来说,减小了切片的长度但是并没有减少容量。由于底层依赖的数组没有变动,而该数组保留了一个理论上已经被丢弃了的元素的引用,以及该元素所引用的所有其他对象。即使是代码不可见的引用被保留,Go 垃圾收集器仍然会将该元素看做是还在使用中。代码认为以及被丢弃了的元素实际上并没有被释放,这就造成了内存泄漏。 + +现在,我查看了 Go 运行时和编译器代码,并对该问题进行了一些思考,我清楚地意识到了这是任何切片截断的通用问题。Go 绝不会尝试缩小切片的底层数组,而且通常来说这样做是不可能的,因为[一个底层数组可能被多个切片](https://utcc.utoronto.ca/~cks/space/blog/programming/GoSliceMutability)或其他引用所共享。这显然会严重影响指向包含指针的对象的切片,但对于指向普通的旧数据的切片也可能很重要,尤其是当它们比较大的时候(比如你有一个 Point 的切片,每个 Point 有三个浮点数)。 + +对于包含指针或者包含持有着指针的结构的切片来说,明显的修复方式(这是[Uber 代码中采用的修复方式](https://github.com/uber/tchannel-go/commit/63a486b96821eaa6fb2299663dda5c529cc04666#diff-32e1ab53c69bf3272bd9e4b51b9bb105))是在截断切片之前将末尾的指针置为空。这样保留了完整的底层数组,但抛弃了对其他内存的引用,而这些其他的内存是真正内存泄漏的地方。 + +对于实际的底层数组可能会有大量内存消耗的切片来说,我想到可能有两种做法,一种特殊,一种通用。特殊的一种是检查代码中“大小截断为零”的情况,并专门将切片本身置为空,而不是仅仅使用标准的切片截断功能来截断。通用的做法是明确地强制使用切片拷贝而不是仅仅截断(就如[我对切片可变性的评论](https://utcc.utoronto.ca/~cks/space/blog/programming/GoSliceMutability)提到的)。强制使用拷贝所带来的缺点是,某些时候可能会带来更大的开销。你可以通过仅在切片的容量远远超出新切片的长度的时候才强制使用拷贝的方式来进行优化。 + +## 补充:(对垃圾收集而言)三索引的切片截断是危险的 + +[Go 切片表达式](https://golang.org/ref/spec#Slice_expressions)允许在起终点之外,使用很少使用的第三个索引来设置新切片的容量。你也许会想到采用这种形式限制切片,来作为解决垃圾收集问题的办法: + +```go +slc = slc[:newlen:newlen] +``` + +不幸的是,这样并不会达到你想要的效果,而且会适得其反。设置新切片的容量完全不会改变底层的依赖数组,也不会让 Go 分配一个新的内存,但这却意味着你无法获取数组大小的信息(否则可以通过切片的容量来得到它)。这样造成的唯一影响是强制随后的 ```append()``` 重新分配新的底层数组。 + +--- + +via: https://utcc.utoronto.ca/~cks/space/blog/programming/GoSlicesMemoryLeak + +作者:[ChrisSiebenmann](https://utcc.utoronto.ca/~cks/space/People/ChrisSiebenmann) +译者:[dust347](https://github.com/dust347) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191211-CGos-Go-string-functions-explained.md b/published/tech/20191211-CGos-Go-string-functions-explained.md new file mode 100644 index 000000000..ece93d1a4 --- /dev/null +++ b/published/tech/20191211-CGos-Go-string-functions-explained.md @@ -0,0 +1,74 @@ +首发于:https://studygolang.com/articles/28440 + +# 关于 CGO 的字符串函数的解释 + +[cgo](https://github.com/golang/go/wiki/cgo) 的大量文档都提到过,它提供了四个用于转换 Go 和 C 类型的字符串的函数,都是通过复制数据来实现。在 CGo 的文档中有简洁的解释,但我认为解释得太简洁了,因为文档只涉及了定义中的某些特定字符串,而忽略了两个很重要的注意事项。我曾经踩过这里的坑,现在我要详细解释一下。 + +四个函数分别是: + +```go +func C.CString(string) *C.char +func C.GoString(*C.char) string +func C.GoStringN(*C.char, C.int) string +func C.GoBytes(unsafe.Pointer, C.int) []byte +``` + +`C.CString()` 等价于 C 的 `strdup()`,像文档中提到的那样,把 Go 的字符串复制为可以传递给 C 函数的 C 的 `char *`。很讨厌的一件事是,由于 Go 和 CGo 类型的定义方式,调用 `C.free` 时需要做一个转换: + +```go +cs := C.CString("a string") +C.free(unsafe.Pointer(cs)) +``` + +请留意,Go 字符串中可能嵌入了 `\0` 字符,而 C 字符串不会。如果你的 Go 字符串中有 `\0` 字符,当你调用 `C.CString()` 时,C 代码会从 `\0` 字符处截断你的字符串。这往往不会被注意到,但[有时文本并不保证不含 null 字符](https://utcc.utoronto.ca/~cks/space/blog/programming/BeSureItsACString)。 + +`C.GoString()` 也等价于 `strdup()`,但与 `C.CString()` 相反,是把 C 字符串转换为 Go 字符串。你可以用它定义结构体的字段,或者是声明为 C 的 `char *`(在 Go 中叫 `*C.cahr`) 的其他变量,抑或其他的一些变量(我们后面会看到)。 + +`C.GoStringN()` 等价于 C 的 `memmove()`,与 C 中普通的字符串函数不同。**它把整个 N 长度的 C buffer 复制为一个 Go 字符串,不单独处理 null 字符。**再详细点,它也通过复制来实现。如果你有一个定义为 `char feild[64]` 的结构体的字段,然后调用了 `C.GoStringN(&field, 64)`,那么你得到的 Go 字符串一定是 64 个字符,字符串的末尾有可能是一串 `\0` 字符。 + +(我认为这是 cgo 文档中的一个 bug。它宣称 GoStringN 的入参是一个 C 的字符串,但实际上很明显不是,因为 C 的字符串不能以 null 字符结束,而 GoStringN 不会在 null 字符处结束处理。) + +`C.GoBytes()` 是 `C.GoStringN()` 的另一个版本,不返回 `string` 而是返回 `[]byte`。它没有宣称以 C 字符串作为入参,它仅仅是对整个 buffer 做了内存拷贝。 + +如果你要拷贝的东西不是以 null 字符结尾的 C 字符串,而是固定长度的 memory buffer,那么 `C.GoString()` 正好能满足需求;它避开了 C 中传统的问题[处理不是 C 字符串的 ’string‘](https://utcc.utoronto.ca/~cks/space/blog/programming/BeSureItsACString)。然而,如果你要处理定义为 `char field[N]` 的结构体字段这种限定长度的 C 字符串时,这些函数*都不能*满足需求。 + +传统语义的结构体中固定长度的字符串变量,定义为 `char field[N]` 的字段,以及“包含一个字符串”等描述,都表示当且仅当字符串有足够空间时以 null 字符结尾,换句话说,字符串最多有 N-1 个字符。如果字符串正好有 N 个字符,那么它不会以 null 字符结尾。这是 [C 代码中诸多 bug 的根源](https://utcc.utoronto.ca/~cks/space/blog/programming/UnixAPIMistake),也不是一个好的 API,但我们却摆脱不了这个 API。每次我们遇到这样的字段,文档不会明确告诉你字段的内容并不一定是 null 字符结尾的,你需要自己假设你有这种 API。 + +`C.GoString()` 或 `C.GoStringN()` 都不能正确处理这些字段。使用 `GoStringN()` 相对来说出错更少;它仅仅返回一个末尾有一串 `\0` 字符长度为 N 的 Go 字符串(如果你仅仅是把这些字段打印出来,那么你可能不会留意到;我经常干这种事)。使用有诱惑力的 `GoString()` 更是引狼入室,因为它内部会对入参做 `strlen()`;如果字符末尾没有 null 字符,`strlen()` 会访问越界的内存地址。如果你走运,你得到的 Go 字符串末尾会有大量的垃圾。如果你不走运,你的 Go 程序出现段错误,因为 `strlen()` 访问了未映射的内存地址。 + +(总的来说,如果字符串末尾出现了大量垃圾,通常意味着在某处有不含结束符的 C 字符串。) + +你需要的是与 C 的 `strndup()` 等价的 Go 函数,以此来确保复制不超过 N 个字符且在 null 字符处终止。下面是我写的版本,不保证无错误: + +```go +func strndup(cs *C.char, len int) string { + s := C.GoStringN(cs, C.int(len)) + i := strings.IndexByte(s, 0) + if i == -1 { + return s + } + return C.GoString(cs) +} +``` + +由于有 [Go 的字符串怎样占用内存](https://utcc.utoronto.ca/~cks/space/blog/programming/GoStringsMemoryHolding)的问题,这段代码做了些额外的工作来最小化额外的内存占用。你可能想用另一种方法,返回一个 `GoStringN()` 字符串的切片。你也可以写复杂的代码,根据 i 和 len 的不同来决定选用哪种方法。 + +更新:[Ian Lance Taylor 给我展示了份更好的代码](https://github.com/golang/go/issues/12428#issuecomment-136581154): + +```go +func strndup(cs *C.char, len int) string { + return C.GoStringN(cs, C.int(C.strnlen(cs, C.size_t(len)))) +} +``` + +是的,这里有大量的转换。这篇文章就是你看到的 Go 和 Gco 类型的结合。 + +--- + +via: https://utcc.utoronto.ca/~cks/space/blog/programming/GoCGoStringFunctions + +作者:[ChrisSiebenmann](https://utcc.utoronto.ca/~cks/space/People/ChrisSiebenmann) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191211-Dynamically-scoped-variables-in-Go.md b/published/tech/20191211-Dynamically-scoped-variables-in-Go.md new file mode 100644 index 000000000..02984054f --- /dev/null +++ b/published/tech/20191211-Dynamically-scoped-variables-in-Go.md @@ -0,0 +1,163 @@ +首发于:https://studygolang.com/articles/27147 + +# Go 中的动态作用域变量 + +这是一个 API 设计的思想实验,它从典型的 Go 单元测试惯用形式开始: + +```go +func TestOpenFile(t *testing.T) { + f, err := os.Open("notfound") + if err != nil { + t.Fatal(err) + } + + // ... +} +``` + +这段代码有什么问题?断言 `if err != nil { ... }` 是重复的,并且需要检查多个条件的情况下,如果测试的作者使用 `t.Error` 而不是 `t.Fatal` 的话会容易出错,例如: + +```go +f, err := os.Open("notfound") +if err != nil { + t.Error(err) +} +f.Close() // boom! +``` + +有什么解决方案?当然,通过将重复的断言逻辑移到辅助函数中,来达到 DRY(Don't Repeat Yourself)。 + +```go +func TestOpenFile(t *testing.T) { + f, err := os.Open("notfound") + check(t, err) + + // ... +} + +func check(t *testing.T, err error) { + if err != nil { + t.Helper() + t.Fatal(err) + } +} +``` + +使用 `check` 辅助函数使得这段代码更简洁一些,并且更加清晰地检查错误,同时有望解决 `t.Error` 与 `t.Fatal` 的混淆使用。 +将断言抽象为一个辅助函数的缺点是,现在你需要将一个 `testing.T` 传递到每一个调用上。更糟糕的是,为了以防万一,你需要传递 `*testing.T` 到每一个需要调用 `check` 的地方。 + +我猜,这并没有关系。但我会观察到只有在断言失败的时候才会用到变量 t —— 即使在测试场景下,大多数时候,大部分的测试是通过的,因此在相对罕见的测试失败的情况下,会产生对这些变量 t 的固定读写开销。 + +如果我们这样做怎么样? + +```go +func TestOpenFile(t *testing.T) { + f, err := os.Open("notfound") + check(err) + + // ... +} + +func check(err error) { + if err != nil { + panic(err.Error()) + } +} +``` + +是的,可以,但是有一些问题。 + +``` +% Go test +--- FAIL: TestOpenFile (0.00s) +panic: open notfound: no such file or directory [recovered] + panic: open notfound: no such file or directory + +goroutine 22 [running]: +testing.tRunner.func1(0xc0000b4400) + /Users/dfc/go/src/testing/testing.go:874 +0x3a3 +panic(0x111b040, 0xc0000866f0) + /Users/dfc/go/src/runtime/panic.go:679 +0x1b2 +github.com/pkg/expect_test.check(...) + /Users/dfc/src/github.com/pkg/expect/expect_test.go:18 +github.com/pkg/expect_test.TestOpenFile(0xc0000b4400) + /Users/dfc/src/github.com/pkg/expect/expect_test.go:10 +0xa1 +testing.tRunner(0xc0000b4400, 0x115ac90) + /Users/dfc/go/src/testing/testing.go:909 +0xc9 +created by testing.(*T).Run + /Users/dfc/go/src/testing/testing.go:960 +0x350 +exit status 2 +``` + +先从好的方面说起,我们不需要传递一个 `testing.T` 到每一个调用 `check` 函数的地方,且测试会立即失败。我们还从 panic 中获得了一条不错的信息 —— 尽管重复出现了两次。但是,哪里断言失败却不容易看到。它发生在 `expect_test.go:11`,你知道这一点是不可以原谅的。 + +所以 panic 不是一个好的解决办法,但是你能从堆栈跟踪信息里面看到什么有用的信息吗?这有一个提示:`github.com/pkg/expect_test.TestOpenFile(0xc0000b4400)`。 + +TestOpenFile 有一个 t 的值,它由 tRunner 传递过来,所以 testing.T 在内存中位于地址 0xc0000b4400 上。如果我们可以在 check 函数内部获取 t 会怎样?那我们可以通过它来调用 t.Helper 来 t.Fatal。这可能吗? + +## 动态作用域 +我们想要的是能够访问一个变量,而该变量的申明既不是在全局范围,也不是在函数局部范围,而是在调用堆栈的更高的位置上。这被称之为*动态作用域*。Go 并不支持动态作用域,但事实证明,某些情况下,我们可以模拟它。回到正题: + +```go +// getT 返回由 testing.tRunner 传递过来的 testing.T 地址 +// 而调用 getT 的函数由它(tRunner)所调用. 如果在堆栈中无法找到 testing.tRunner +// 说明 getT 在主测试 Goroutine 没有被调用, +// 这时 getT 返回 nil. +func getT() *testing.T { + var buf [8192]byte + n := runtime.Stack(buf[:], false) + sc := bufio.NewScanner(bytes.NewReader(buf[:n])) + for sc.Scan() { + var p uintptr + n, _ := fmt.Sscanf(sc.Text(), "testing.tRunner(%v", &p) + if n != 1 { + continue + } + return (*testing.T)(unsafe.Pointer(p)) + } + return nil +} +``` + +我们知道每个测试(Test)由 testing 包在自己的 Goroutine 上调用(看上面的堆栈信息)。testing 包通过一个名为 tRunner 的函数来启动测试,该函数需要一个*testing.T 和一个 func(*testing.T)来调用。因此我们抓取当前 Goroutine 的堆栈信息,从中扫描找到已 testing.tRunner 开头的行——由于 tRunner 是私有函数,只能是 testing 包——并解析第一个参数的地址,该地址是一个指向 testing.T 的指针。有点不安全,我们将这个原始指针转换为一个 *testing.T 我们就完成了。 + +如果搜索不到则可能是 getT 并不是被 Test 所调用。这实际上是行的通的,因为我们需要*testing.T 是为了调用 t.Fatal,而 testing 包要求 t.Fatal 被[主测试 goroutine](https://golang.org/pkg/testing/#T.FailNow)所调用。 + +```go +import "github.com/pkg/expect" + +func TestOpenFile(t *testing.T) { + f, err := os.Open("notfound") + expect.Nil(err) + + // ... +} +``` + +综上,在预期打开文件所产生的 err 为 nil 后,我们消除了断言样板,并且是测试看起来更加清晰易读。 + +## 这样好吗? + +这时你应该会问,*这样好吗?*答案是,不,这不好。此时你应该会感到震惊,但是这些不好的感觉可能值得反思。除了在 Goroutine 的调用堆栈乱窜的固有不足以外,同样存在一些严重的设计问题: +1. expect.Nil 的行为依赖于谁调用它。同样的参数,由于调用堆栈位置的原因可能导致行为的不同——这是不可预期的。 +2. 采取极端的动态作用域,将传递给单个函数之前的所有函数的所有变量纳入单个函数的作用域中。这是一个在函数申明没有明确记录的情况下将数据传入和传出的辅助手段。 + +讽刺的是,这恰恰是我对[context.Context](https://dave.cheney.net/2017/01/26/context-is-for-cancelation)的评价。我会将这个问题留给你自己判断是否合理。 + +## 最后的话 + +这是个坏主意,这点没有异议。这不是你可以在生产模式中使用的模式。但是,这也不是生产代码。这是在测试,也许有着不同的规则适用于测试代码。毕竟,我们使用模拟(mocks)、桩(stubs)、猴子补丁(monkey patching)、类型断言、反射、辅助函数、构建标志以及全局变量,所有这些使得我们更加有效率得测试代码。所有这些,*奇技淫巧*是不会让它们出现在生产代码里面的,所以这真的是世界末日吗? + +如果你读完本文,你也许会同意我的观点,尽管不太符合常规,并无必要将*testing.T 传递到所有需要断言的函数中去,从而使测试代码更加清晰。 + +如果你感兴趣,我已分享了一个应用这个模式的[小的断言库](https://github.com/pkg/expect)。小心使用。 + +--- + +via: https://dave.cheney.net/2019/12/08/dynamically-scoped-variables-in-go + +作者:[Dave Cheney](https://dave.cheney.net/) +译者:[dust347](https://github.com/dust347) +校对:[@unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191211-Go-CGO-Compatible-Structs.md b/published/tech/20191211-Go-CGO-Compatible-Structs.md new file mode 100644 index 000000000..1c6883741 --- /dev/null +++ b/published/tech/20191211-Go-CGO-Compatible-Structs.md @@ -0,0 +1,110 @@ +首发于:https://studygolang.com/articles/27148 + +# 用 cgo 生成用于 cgo 的 C 兼容的结构体 + +假设([并非完全假设,这里有 demo](https://github.com/siebenmann/go-kstat/))你正在编写一个程序包,用于连接 Go 和其它一些提供大量 C 结构体内存的程序。这些结构可能是系统调用的结果,也可能是一个库给你提供的纯粹信息性内容。无论哪种情况,你都希望将这些结构传递给你的程序包的用户,以便他们可以使用这些结构执行操作。在你的包中,你可以直接使用 cgo 提供的 C. 类型。但这有点恼人(这些整型它们没有对应的原生 Go 类型,使得与常规 Go 代码交互需要乱七八糟的强制转换),并且对于其它导入你的包的代码没有帮助。因此,你需要以某种方式使用原生的 Go 结构体。 + +一种方式是手动为这些 C 结构体的定义你自己的 Go 版本。这有两个缺点。这太枯燥了(还很容易出错),并且不能保证你能获得与 C 完全相同的内存布局(后者通常但并非总是很重要)。幸运的是有一种更好的方法,那就是使用 cgo 的 `-godefs` 功能或多或少地为你自动生成结构体声明。生成结果并不总是完美的,但可能会为你带来最大的收益。 + +使用 `-godefs` 的起点是特殊的 cgo Go 源文件,该文件需要将某些 Go 类型声明为某些 C 类型。例如: + +```go +// +build ignore +package kstat +// #include +import "C" + +type IO C.kstat_io_t +type Sysinfo C.sysinfo_t + +const Sizeof_IO = C.sizeof_kstat_io_t +const Sizeof_SI = C.sizeof_sysinfo_t +``` + +这些常量对于喜欢较真的人很有用,可以用来在后面对比检查 Go 类型的 `unsafe.Sizeof()` 和 C 类型的大小是否一致。 + +运行 `go tool cgo -godefs .go` ,它将打印一系列带有导出字段和所有内容的标准 Go 类型到标准输出。然后,你可以将其保存到文件中并使用。如果你认为 C 类型可能会更改,则应将生成的文件保留下来,这样就避免重新生成文件遇到的很多麻烦。如果 C 类型基本上是固定的,则可以使用 godoc 对生成的输出进行注释。 cgo 会考虑类型匹配问题,它会把原始的 C 结构中存在的 padding 也插入到输出中。 + +我不知道如果原始的 C 结构体不可能在 Go 中重建出来,cgo 会怎么办。 比如 Go 需要 padding,但是 C 不需要。希望它会指出错误。这是你以后可能要检查这些 sizeof 的原因之一。 + +`-godefs` 最大的限制是与 cgo 通常具有的限制相同:它没有对 C 联合类型(union)的真正支持,因为 Go 确实没有这个。如果你的 C 结构体中有联合,你得自己弄清楚如何处理它们;我相信 cgo 把这些转换为大小合适的 uint8 数组,但这对于实际访问内容不是很有用。 + +这里有两个问题。假设你有一个嵌入了另一个结构体类型的结构体: + +```c +struct cpu_stat { + struct cpu_sysinfo cpu_sysinfo; + struct cpu_syswait cpu_syswait; + struct vminfo cpu_vminfo; +} +``` + +在这里,你必须给 cgo 一些帮助,方式是在主结构体类型之前创建嵌入结构类型的 Go 版本: + +```go +type Sysinfo C.struct_cpu_sysinfo +type Syswait C.struct_cpu_syswait +type Vminfo C.struct_cpu_vminfo + +type CpuStat C.struct_cpu_stat +``` + +然后 cgo 才能生成正确的内嵌的 Go 结构的 CpuStat 结构。如果不这样做,你将获得一个 CpuStat 结构类型,该结构类型具有不完整的类型信息,其中的 `Sysinfo` 等字段将引用名为 `_Ctype_…` 的未在任何地方定义的类型。 + +顺便说一句,我在这确实是指 `Sysinfo` ,而不是 `Cpu_sysinfo` 。cgo 足够聪明,可以从结构字段名称中删除这种常见的前缀。我不知道它的算法是怎样的,但至少是有用的。 + +第二个问题是嵌入了匿名结构: + +```c +struct mntinfo_kstat { + .... + struct { + uint32_t srtt; + uint32_t deviate; + } m_timers[4]; + .... +} +``` + +不幸的是,cgo 根本无法处理这种问题。具体可以去看 [issue 5253](https://github.com/golang/go/issues/5253) ,你有两个选择,第一种是使用[建议的 CL 修复](https://codereview.appspot.com/122900043),这个目前仍然适用于 src/cmd/cgo/gcc.go 并且能够工作(对我来说)。如果你不想构建自己的 Go 工具链(或者如果 CL 不再适用或无法工作),另一种解决方案是创建一个新的 C 头文件,该文件具有整个结构体的变体,通过创建具名结构体去除结构体的匿名化。 + +```c +struct m_timer { + uint32_t srtt; + uint32_t deviate; +} + +struct mntinfo_kstat_cgo { + .... + struct m_timer m_timers [4]; + .... +} +``` + +然后,在你的 Go 文件中, + +```go +... +// #include "myhacked.h" +... + +type MTimer C.struct_m_timer +type Mntinfo C.struct_mntinfo_kstat_cgo +``` + +除非你搞错了,否则两个 C 结构体应具有完全相同的大小和布局,并且彼此完全兼容。现在你可以在你的版本上使用 `-godefs` 了,记住按照前面问题 1 的处理,需要为 `m_timer` 创建明确的 Go 类型。 +如果你飘了(你认为你不在需要重新生成这些内容了),你可以在生成的 Go 文件中逆转这个过程,重新将 MTimer 类型匿名化到结构体中(因为 Go 对匿名结构体有很好的支持)。因为你没有更改实际内容,只是改了类型声明,所以结果应该与原始的布局相同。 + +PS:`-godefs` 的输入文件被设置为不被正常 `go build` 过程构建,因为它只用于 godefs 生成。如果这个文件也被包含在 `go build` 构建的源码中,你会得到关于 Go 类型多处定义的构建错误。必然的结果是,你不需要将此文件和任何相关 .h 文件与软件包的常规 .go 文件放在同一目录。你可以把他们放在子目录,或者放在完全独立的位置。 + +(我认为该 `package` 行在 godefs .go 文件中唯一要做的就是设置 cgo 将在输出中打印的软件包名称。) + +--- + +via: https://utcc.utoronto.ca/~cks/space/blog/programming/GoCGoCompatibleStructs + +作者:[ChrisSiebenmann](https://utcc.utoronto.ca/~cks/space/People/ChrisSiebenmann) +译者:[befovy](https://github.com/befovy) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191211-Goroutines-versus-other-concurrency-handling-options-in-Go.md b/published/tech/20191211-Goroutines-versus-other-concurrency-handling-options-in-Go.md new file mode 100644 index 000000000..73678979c --- /dev/null +++ b/published/tech/20191211-Goroutines-versus-other-concurrency-handling-options-in-Go.md @@ -0,0 +1,67 @@ +首发于:https://studygolang.com/articles/28441 + +# Go 中的 Goroutine 和其他并发处理方案的对比 + +Go 语言让使用 Goroutine 和通道变得非常有吸引力,作为在 Go 中进行并发的主要方式,它们是被有意识的提出的。因此对于你所遇到的任何与并发相关的问题,它们都可能成为首选方案。但是我不确定它们是否适合于我遇到的所有问题,我仍在考虑其中的平衡点。 + +通道和 Goroutine 对于查询共享状态(或从共享状态中获取某些信息)这类问题看起来似乎并不完全契合。假设你想要记录那些与服务端建立 TLS 通信失败的 SMTP 客户端的 IP,以便在 TLS 握手失败的情况下,不再提供 TLS 通信(或至少在给定的时间段内不提供)。大多数基于通道的解决方案都很直白:一个主 Goroutine 维护一个 IP 集合,通过通道向主 Goroutine 发送一条消息来向其添加 IP。但是,如何询问主 Goroutine 某个 IP 是否已经存在?问题的关键在于,无法在共享通道上收到来自主 Goroutine 的答复,因为主 Goroutine 无法专门答复你。 + +针对这个问题,目前我看到的基于通道的解决方案是将一个回复通道作为查询消息的一部分一起发送给主 goroutine(通过共享通道发送)。但是这种方法的有个副作用,那就是通道的频繁分配和释放,每次请求都会对通道进行分配、初始化、使用一次然后销毁(我认为这些通道必须通过垃圾回收机制回收,而不是在栈上分配和释放)。另一种方案是提供一个由 sync 包中锁或其他同步工具显式保护的数据结构,这是更底层的解决方案,需要更多的管理操作,但是却避免了通道的频繁分配和释放。 + +对于我将要编写的大多数 Go 程序来说,效率往往不是首要的关注点,真正的问题是,如何使编写更容易并且代码更清晰。目前我没有一个彻底的结论,但却有一个初步的、并不完全是我所期待的那个:如果要针对同一共享状态处理不止一种查询,那么基于锁的方案会更容易些。 + +在面对多种类型的状态查询时,通道方案的问题在于它需要很多我称之为“类型官僚主义”的东西。因为通道是拥有类型的,所以对于每种不同类型的答复都需要定义相应的答复通道类型(显式或隐式)。然后,基本上每个不同的查询也都需要自己的类型,因为查询消息必须包含(类型化的)回复通道。基于锁的方案并不会使这些类型相关的琐事消失,但会减轻它们带来的痛苦,因为此时查询消息和答复只是函数的参数和返回值,因此不必将它们正式的定义为 Go 类型(struct)。实际上,即便需要进行额外的手动加锁,这对我来说已经是很轻松了。 + +(可以通过各种手段将这些类型合并在一起从而减少通道方案中所需类型的数量,但是此时便开始失去类型的安全性,尤其是编译时类型检查。我喜欢 Go 中的编译时类型检查,因为它会很靠谱的告诉我是否遇到了明显的错误,并且这也有助于加快重构的速度。) + +从某种意义上说,我认为通道和 Goroutine 是 Turing tarpit 的一种形式,因为如果你足够聪明的话,可以将它们应用于所有问题。 + +(另一方面,[有时通道是解决看似与它们无关的问题的绝佳方案](http://blog.golang.org/two-go-talks-lexical-scanning-in-go-and),在看到该文章之前,我从未想过在词法分析器中使用 Goroutine 和通道。) + +## 侧边栏:我所采用的 Go 锁模式 + +这不是我的原创,而是来源于 Go blog 中的 [Go maps 实战](http://blog.golang.org/go-maps-in-action)一文,如下所示: + +```go +// ipEnt 是共享数据结构中的真实条目。 +type ipEnt struct { + when time.time + count int +} + +// ipMap 是由读写锁保护的共享数据结构。 +type ipMap struct { + sync.RWMutex + ips map[string]*ipEnt +} + +var notls = &ipMap{ips: make(map[string]*ipEnt)} + +// Add 方法用于外部调用对共享数据进行操作,每次都会首先获取锁,随后释放它。 +func (i *ipMap) Add(ip string) { + i.Lock() + ... manipulate i.ips ... + i.Unlock() +} +``` + +使用方法来操作数据结构感觉是最自然的方式,部分原因是由于操作和锁定的约束条件紧密的耦合在一起。而我喜欢它纯粹是因为它所带来的简洁书写方式: + +```go +if res == TLSERROR { + notls.Add(remoteip) + .... +} +``` + +最后这点只是个人喜好,当然,也有人更喜欢将 `ipMap` 作为参数传递给独立的函数。 + +--- + +via: https://utcc.utoronto.ca/~cks/space/blog/programming/GoGoroutinesVsLocks + +作者:[ChrisSiebenmann](https://utcc.utoronto.ca/~cks/space/People/ChrisSiebenmann) +译者:[anxk](https://github.com/anxk) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) diff --git a/published/tech/20191211-In-Go-sometimes-a-nil-is-not-a-nil.md b/published/tech/20191211-In-Go-sometimes-a-nil-is-not-a-nil.md new file mode 100644 index 000000000..ede643152 --- /dev/null +++ b/published/tech/20191211-In-Go-sometimes-a-nil-is-not-a-nil.md @@ -0,0 +1,102 @@ +首发于:https://studygolang.com/articles/27149 + +# 在 Go 语言中,有时 nil 并不是一个 nil + +今天,我遇到了一个 [Go FAQ](http://golang.org/doc/faq#nil_error)。首先,作为一个小小的 Go 语言测验,看看您是否在 Go playground 中运行该程序之前就能推断出它应该打印出的内容(我已经将程序放在侧边栏中,以防它在 Go playground 上消失)。该程序的关键代码是: + +```go +type fake struct { io.Writer } + +func fred (logger io.Writer) { + if logger != nil { + logger.Write([]byte("...")) + } +} + +func main() { + var lp *fake + fred(nil) + fred(lp) +} +``` + +由于 Go 语言中的变量是使用它们的零值显式创建的,在指针的情况下,例如 `lp` 将会是 `nil`,您可能期待上述代码会正常运行(即不执行任何操作)。实际上,它会在对 `fred()` 的第二次调用时崩溃。原因是,在 Go 语言中,有时以 `nil` 为值的变量,如果直接打印的话,它虽然看起来像 `nil`,但实际上并不是真的 `nil` 。简而言之,Go 语言区别对待 `nil` 接口值和转换为接口的值为 `nil` 的具体类型。只有前者确实为 `nil`,因此与字面上的 `ni ​​ l` 相等,就像 `fred()` 在这里做的一样。 + +(因此,可以使用 `nil f` 调用 `(f *fake)` 上的具体方法。它也许是一个 `nil` 指针,但是它是类型化的 `nil` 指针,所以可以拥有有方法。甚至在接口转换后依然可以拥有方法,正如上述的例子。) + +对于这里的情况,其解决方法是更改​​初始化的过程。实际的程序条件性地设置了 `fake`,类似于下面的代码: + +```go +var l *sLogger + +if smtplog != nil { + l = &sLogger + l.prefix = logpref + l.writer = bufio.NewWriterSize(smtplog, 4096) +} +convo = smtpd.NewConvo(conn, l) +``` + +这会将具体类型为 `*sLogger` 的 `nil` 传递给期望参数为 `io.Writer` 的对象,从而导致接口转换并掩盖了 `nil`。为了解决这个问题,我们可以添加一个必须显式设置的中间变量 `io.Writer`: + +```go +var l2 io.Writer + +if smtplog != nil { + l := &sLogger + l.prefix = logpref + l.writer = .... + l2 = l +} +convo = smtpd.NewConvo(conn, l2) + +``` + +如果我们不初始化这个特殊的日志记录器 `sLogger`,则 `l2` 会是一个真正的 `io.Writer nil`,并会在 `smtpd` 包中被检测到。 + +(您可以将类似的初始化操作封装进一个返回类型为 `io.Writer` 的函数中,并在没有提供日志记录器的情况下显式返回 `nil`,通过这样的技巧来达到类似的效果。需要强调的一点是,函数必须返回接口类型,如果返回类型为 `*sLogger`,那么您将再次遇到相同的问题。) + +在 `sLogger` 的方法中保留对零值的防护代码,这是一个个人喜好问题。然而,我不想这么做,如果将来我在代码中遇到类似的初始化错误,我希望它崩溃,以便对其进行修复。 + +我从这件事中学到的另一个教训是,如果是出于调试的目的而进行的打印,我不会再使用 `%v` 作为格式说明符,而会使用 `%#v`。因为前者将会为接口 `nil` 和具体类型的 `nil` 同样打印一个普通且具有误导性的 ``,而 `%#v` 将为前者打印出 ``,为后者打印 `(*main.fake)(nil)` 。 + +## 边注栏: 测试程序 + +```go +package main + +import ( + "fmt" + "io" +) + +type fake struct { + io.Writer +} + +func fred(logger io.Writer) { + if logger != nil { + logger.Write([]byte("a test\n")) + } +} + +func main() { + // 这里的 t 的值是 nil + var t *fake + + fred(nil) + fmt.Printf("passed 1\n") + fred(t) + fmt.Printf("passed 2\n") +} +``` + +--- + +via: https://utcc.utoronto.ca/~cks/space/blog/programming/GoNilNotNil + +作者:[ChrisSiebenmann](https://utcc.utoronto.ca/~cks/space/People/ChrisSiebenmann) +译者:[anxk](https://github.com/anxk) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191211-Some-notes-on-Go-runtime-keepalive-function.md b/published/tech/20191211-Some-notes-on-Go-runtime-keepalive-function.md new file mode 100644 index 000000000..a13be6664 --- /dev/null +++ b/published/tech/20191211-Some-notes-on-Go-runtime-keepalive-function.md @@ -0,0 +1,32 @@ +首发于:https://studygolang.com/articles/28442 + +# Go 语言中 runtime.KeepAlive() 方法的一些随笔 + +我在看 go101 网站的 [类型不安全指针](https://go101.org/article/unsafe.html)[(来源)](https://old.reddit.com/r/golang/comments/8ll6lf/how_to_safely_use_typeunsafe_pointers_in_go/) 一文时,偶然发现了 [runtime](https://golang.org/pkg/runtime/) 库的一个有趣的新方法 [runtime.KeepAlive()](https://golang.org/pkg/runtime/#KeepAlive) 的一个用法。刚开始我对于怎么使用它是很困惑的, 那么按我的性格肯定要探究它是怎么工作的。 + +`runtime.KeepAlive` 所做的事就是使一个变量保持 '存活',这就意味着它(或者它引用的变量)不会被垃圾收集,而且它所注册的任何终止器(finalizer)都不会被执行。 [这个文档](https://golang.org/pkg/runtime/#KeepAlive) 中有一个如何使用它的例子。我的第一个疑问是为什么在代码中 `runtime.KeepAlive()` 的使用时机那么的靠后;我比较希望它能够更早的被调用,就像终止器被注入时,但是后来我明白了它这样做的真正意图。 简而言之, `runtime.KeepAlive()` 是调用一下变量。显而易见的,一个变量直至它的最后一次使用期间都是存活的,所以如果你在后面使用一个变量,那么 Go 必须让它一直存活到最后使用的时候。 + +一方面,`runtime.keepAlive` 没有什么神奇的地方;任何一种使用某个变量的方式,都会使它保持存活。另一方面,`runtime.KeepAlive()` 是一种很重要的魔法,它表示 Go 保证了你所使用的变量不会被优化清除掉,因为编译器能明白没有什么能真正依赖于你的使用。虽然有很多其它的方式来使用一个变量,但即使是最聪明的方式也很容易受到编译器的影响,最聪明的方式也会有不利的一面,他们会影响 [Go 的智能合理逃逸分析](https://utcc.utoronto.ca/~cks/space/blog/programming/GoReflectEscapeHack),强行将一个本属于本地栈的变量分配到堆上。 + +关于 `runtime.KeepAlive()` 的另一个特殊戏法是它的是实现方式,代码里什么都没做。实际上,它不是作为一个被调用的函数,而是由 [ssa.go](https://github.com/golang/go/blob/master/src/cmd/compile/internal/gc/ssa.go#L2828) 实现的编译器内部实现,类似于 `unsafe.Pointer`。当你的代码中使用了 `runtime.KeepAlive()`,Go 编译器会设置一个名为 `OpKeepAlive` 的静态单赋值(SSA),然后剩余的编译就会知道将这个变量的存活期保证到使用了 `runtime.KeepAlive()` 的时刻。 + +(阅读 ssa.go 的初始化函数是很有趣的。不出所料,有许多语义化包函数调用被直接映射到将指令内联在代码中,如 math.Sqrt。有些是平台相关的,包括 [bits](https://golang.org/pkg/math/bits/) 的函数) + +`runtime.KeepAlive()` 是一个特别的魔法有一个直接的后果就是你不能得到它的地址。如果你这样做的话, Go 会报错: + +``` +./tst.go:20:22: cannot take the address of runtime.KeepAlive +``` + +我不知道 Go 是否会聪明地优化掉一个只调用 `runtime.KeepAlive` 的函数, 但希望你永远不需要间接调用 `runtime.KeepAlive`。 + +PS:尽管我很想说没有人应该需要对分配在栈上的本地变量(包括参数)调用 `runtime.KeepAlive`,因为在函数返回之前栈是不会被回收的,但这是一个危险的假设。编译器可以非常聪明地为两个不同的、没有重叠生存期的变量重用堆栈槽,或者简单地告诉垃圾收集它已经完成了某些工作(例如,用 nil 覆盖指向对象的指针)。 + +--- +via: https://utcc.utoronto.ca/~cks/space/blog/programming/GoRuntimeKeepAliveNotes + +作者:[ChrisSiebenmann](https://utcc.utoronto.ca/~cks/space/People/ChrisSiebenmann) +译者:[yuhang-dong](https://github.com/yuhang-dong) +校对:[unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191211-The-potential-issue-with-Go-s-strings.md b/published/tech/20191211-The-potential-issue-with-Go-s-strings.md new file mode 100644 index 000000000..b91a48d48 --- /dev/null +++ b/published/tech/20191211-The-potential-issue-with-Go-s-strings.md @@ -0,0 +1,31 @@ +首发于:https://studygolang.com/articles/25537 + +# Go 字符串中的潜在问题 + +在我之前的文章 [Go 中我喜欢的东西](https://utcc.utoronto.ca/~cks/space/blog/programming/GoThingsILike)中提到过,我喜欢的 [Go](https://golang.org/) 的东西其中之一就是它的字符串(通常还有切片)。从一个 Python 开发者的角度看,它们之所以伟大,是因为创建它们时开销很少,因为它们通常不需要复制。在 Python 中,任何时候操作字符串都需要复制一部分或全部字符串,而 [这很容易对性能造成影响](https://utcc.utoronto.ca/~cks/space/blog/python/StringSpeedSurprises)。想要写高性能的 Python 代码需要谨慎考虑复制的问题。在 Go 中,几乎所有的字符串操作都是不复制的,仅仅是从原字符串取一个子集(例如去除字符串首尾的空白字符),因此你可以更自由地操作字符串。这个机制可以非常直接地解决你的问题,并且非常高效。 + +(当然,不是所有的字符串操作都不复制。例如,把一个字符串转换成大写需要复制,尽管 Go 中的实现已经足够智能,在不需要改变原字符串时 — 例如由于它已经是一个全大写的字符串 — 可以规避掉复制。) + +但是这个优势也带来了潜在的坏处,那些没有开销的子字符串使原来的整个字符串一直存在于内存中。Go 中的字符串(和切片)操作之所以内存开销很少,是因为它们只是底层存储(字符串或切片底层的数组的真实数据)的一些部分的引用;创建一个字符串做的操作就是创建了一个新的引用。但是 Go(目前)不会对字符串数据或数组进行部分的垃圾回收,所以即使它一个很小的 bit 被其它元素引用,整个对象也会一直保持在内存中。换句话说,一个单字符的字符串(目前)足够让一个巨大的字符串不被 GC 回收。 + +当然,不会有很多人遇到这个问题。为了遇到它,你需要处理一个非常庞大的原字符串,或造成大量的内存消耗(或者两者都做),在这个基础上,你必须创建那些不持久的字符串的持久的小子字符串(好吧,你是多么希望它是非持久的)。很多使用场景不会复现这个问题;要么你的原字符串不够大,要么你的子集获取了大部分原字符串(例如你把原字符串进行了分词处理),要么子字符串生命周期不够长。简而言之,如果你是一个普通的 Go 开发者,你可以忽略这个问题。处理长字符串并且长时间维持原字符串的很小部分的人才会关注这个问题。 + +(我之所以关注到这个问题,是因为一次我花了大量精力用尽可能少的内存写 Python 程序,尽管它是从一个大的配置文件解析结果然后分块储存。这让我联想到了一些其他的事,如字符串的生命周期、限制字符串只复制一次,等等。然后我用 Go 语言写了一个解析器,这让我由重新考虑了一下这些问题,我意识到由于我的解析器截取出和维持的 bit 一直存在于内存中,从输入文件解析出的庞大字符串也会一直存在与内存中。) + +顺便说一下,我认为这是 Go 做了权衡之后的正确结果。大部分使用字符串的开发者不会遇到这个问题,而且截取子字符串开销很小对于开发者来说用处很大。这种低开销的截取操作也减轻了 GC 的负担;当代码使用大量的子字符串截取(像 Python 中那样)时,你只需要处理固定长度的字符串引用就可以了,而不是需要处理长度变化的字符串。 + +当你的代码遇到这个问题时,当然有明显的解决方法:创建一个函数,通过把字符串转换成 `[]byte` 来 ”最小化“ 字符串,然后返回。这种方法生成了一个最小化的字符串,内存开销是理论上最完美实现的只复制一次,而 Go 现在很容易就可以实现。 + +## 附加问题:`strings.ToUpper()` 等怎样规避没有必要的复制 + +所有的主动转换函数像 `ToUpper()` 和 `ToTitle()` 是用 `strings.Map()` 和 [unicode 包](http://golang.org/pkg/unicode/) 中的函数实现的。`Map()` 足够智能,在映射的函数返回一个与已存在的 `rune` 不同的结果之前不会创建新的字符串。因此,你代码中所有类似的直接使用 `Map()` 的地方都不会有内存开销。 + +--- + +via: https://utcc.utoronto.ca/~cks/space/blog/programming/GoStringsMemoryHolding + +作者:[Chris Siebenmann](https://utcc.utoronto.ca/~cks/space/People/ChrisSiebenmann) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[dingdingzhou](https://github.com/dingdingzhou) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191211-Why-your-64-bit-Go-programs-may-have-a-huge-virtual-size.md b/published/tech/20191211-Why-your-64-bit-Go-programs-may-have-a-huge-virtual-size.md new file mode 100644 index 000000000..c8f213bbe --- /dev/null +++ b/published/tech/20191211-Why-your-64-bit-Go-programs-may-have-a-huge-virtual-size.md @@ -0,0 +1,37 @@ +首发于:https://studygolang.com/articles/27150 + +# 为什么你的 64-bit 程序可能占用巨大的虚拟空间 + +出于很多目的,我从最新的 Go 系统内核开发源码复制了一份代码,在一个正常的运行环境中构建(和重新构建)它,在构建版本基础上周期性地重新构建 Go 程序。近期我在用 `ps` 查看我的[一个程序](https://github.com/siebenmann/smtpd/)的内存使用情况时,发现它占用了约 138 GB 的巨大虚拟空间(Linux ps 命令结果的 `VSZ` 字段),尽管它的常驻内存还不是很大。某个进程的常驻内存很小,但是需要内存很大,通常是表示有内存泄露,因此我心里一颤。 + +(用之前版本的 Go 构建后,根据运行时间长短不同,通常会有 32 到 128 MB 不同大小的虚拟内存占用,比最新版本小很多。) + +还好这不是内存泄漏。事实上,之后的实验表明即使是个简单的 `hello world` 程序也会有占用很大的虚拟内存。通过查看进程的 `/proc//smaps` 文件([cf](https://utcc.utoronto.ca/~cks/space/blog/linux/SmapsFields))可以发现几乎所有的虚拟空间是由两个不可访问的 map 占用的,一个占用了约 8 GB,另一个约 128 GB。这些 map 没有可访问权限(它们取消了读、写和可执行权限),所以它们的全部工作就是专门为地址空间预留的(甚至没有用任何实际的 RAM)。大量的地址空间。 + +这就是现在的 Go 在 64 位系统上的低级内存管理的工作机制。简而言之,Go (理论上)从连续的 arena 区域上进行低级内存分配,申请 8 KB 的页;哪些页可以无限申请存储在一个巨大的 bitmap。在 64 位机器上,Go 会把全部的内存地址空间预留给 bitmap 和 arena 区域本身。程序运行时,当你的 Go 程序真正使用内存时,arena bitmap 和内存 arena 片段会从简单的预留地址空间变为由 RAM 备份的内存,供其他部分使用。 + +(bitmap 和 arena 通常是通过给 `mmap` 传入 `PROT_NONE` 参数进行初始化的。当内存被使用时,会使用 `PROT_READ|PROT_WRITE` 重新映射。当释放时,我不确定它做了什么,所以对此我不发表意见。) + +这个例子是用当前发布的 Go 1.4 开发版本复现的。之前的版本的 64 位程序运行时会占用更小的需要空间,虽然读 Go 1.4 源码时我也没找到原因。 + +以我的理解,一个有意思的影响是 64 位 Go 程序的大部分内存分配都可能占用至多 128 GB 的空间(也可能在整个运行周期内所有的内存分配都会,我不确定)。 + +了解更多细节,请看 [src/runtime/malloc2.go](https://github.com/golang/go/blob/master/src/runtime/malloc2.go) 的注释和 [src/runtime/malloc1.go](https://github.com/golang/go/blob/master/src/runtime/malloc1.go) 的 `mallocinit()`。 + +我不得不说,这个比我最初以为地更有意思也更有教育意义,尽管这意味着查看 `ps` 不再是一个检测你的 Go 程序中内存泄露的好方法(温馨提示,我不确定它曾经是不是)。结论是,检测这类内存使用最好的方法是同时使用 `runtime.ReadMemStats()`(可以通过 [net/http/pprof](http://golang.org/pkg/net/http/pprof/) 暴露出去)和 Linux 的 `smem` 程序或者养成对有意义的内存地址空间占用生成详细信息的习惯。 + +PS: Unix 通常足够智能,可以理解 `PROT_NONE` 映射不会耗尽内存,因此不应该对系统内存过量使用的限制进行统计。然而,它们会统计每一个进程的总地址空间进行统计,这意味着你运行 1.4 的 Go 程序时不能真的使用这么多。由于总内存地址空间的最大数几乎不会达到,因此这似乎不是一个问题。 + +## 附录:在 32 位系统上是怎样的 + +所有的信息都在 `mallocinit()` 注释中。简而言之,就是运行时预留了足够大的 arena 来处理 2 GB 的内存(「仅」占用 256 MB)但是仅预留 2 GB 中理论上它可以使用的 512 MB 地址空间。如果后续的运行过程中需要更多内存,就向操作系统申请另一个块的地址空间,优先 arena 区域剩下的 1.5 GB 的地址空间中分配。大多数情况下,运行的程序都会正常申请到需要分配的空间。 + +--- + +via: https://utcc.utoronto.ca/~cks/space/blog/programming/GoBigVirtualSize + +作者:[ChrisSiebenmann](https://utcc.utoronto.ca/~cks/space/People/ChrisSiebenmann) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191211-go-uintptr-vs-unsafe-pointer.md b/published/tech/20191211-go-uintptr-vs-unsafe-pointer.md new file mode 100644 index 000000000..634af46bf --- /dev/null +++ b/published/tech/20191211-go-uintptr-vs-unsafe-pointer.md @@ -0,0 +1,31 @@ +首发于:https://studygolang.com/articles/25931 + +# 为什么 Go 关心 unsafe.Pointer 和 uintptr 之间的差别 + +Go 有两样东西或多或少是无类型指针的表示:uintptr 和 unsafe.Pointer (和外表相反,它们是内置类型)。 +从表面上看这有点奇怪,因为 unsafe.Pointer 和 uintptr 可以彼此来回转换。为什么不只有一种指针表现形式?两者之间有什么区别? + +表面的区别是可以对 uintptr 进行算数运算但不能对 unsafe.Pointer(或任何其他 Go 指针)进行运算。unsafe 包的文档指出了重要的区别: + +> uintptr 是整数,不是引用。将 Pointer 转换为 uintptr 会创建一个没有指针语义的整数值。即使 uintptr 持有某个对象的地址,如果对象移动,垃圾收集器并不会更新 uintptr 的值,uintptr 也无法阻止该对象被回收。 + +尽管 unsafe.Pointer 是通用指针,但 Go 垃圾收集器知道它们指向 Go 对象;换句话说,它们是真正的 Go 指针。通过内部魔法,垃圾收集器可以并且将使用它们来防止活动对象被回收并发现更多活动对象(如果 unsafe.Pointer 指向的对象自身持有指针)。因此,对 unsafe.Pointer 的合法操作上的许多限制归结为“在任何时候,它们都必须指向真正的 Go 对象”。如果创建的 unsafe.Pointer 并不符合,即使很短的时间,Go 垃圾收集器也可能会在该时刻扫描,然后由于发现了无效的 Go 指针而崩溃。 + +相比之下,uintptr 只是一个数字。这种特殊的垃圾收集魔法机制并不适用于 uintptr 所“引用”的对象,因为它仅仅是一个数字,一个 uintptr 不会引用任何东西。反过来,这导致在将 unsafe.Pointer 转换为 uintptr,对其进行操作然后再将其转回的各种方式上存在许多微妙的限制。基本要求是以这种方式进行操作,使编译器和运行时可以屏蔽不安全的指针的临时非指针性,使其免受垃圾收集器的干扰,因此这种临时转换对于垃圾收集将是原子的。 + +(我想,我在文章[将内存块复制到 Go 结构中](https://utcc.utoronto.ca/~cks/space/blog/programming/GoMemoryToStructures)里对 unsafe.Pointer 的使用是安全的,但我承认我现在不确定。 +我相信 cgo 会有一些不可思议的机制,因为它可以安全地制造出不安全的指针,这些指针指向 C 内存而不是 Go 内存。) + +PS:从 Go 1.8 开始,即使当时没有运行垃圾回收,所有 Go 指针必须始终有效(我相信也包括 unsafe.Pointer)。如果您在变量或字段中存储了无效的指针,则仅通过将字段更新为包括 nil 在内的完全有效的值即可使代码崩溃。例如,请参阅[这个有教育意义的 Go bug report](https://github.com/golang/go/issues/19135)。 + +(我本想尝试讲一下内部魔法,它允许垃圾收集器处理未类型化的 unsafe.Pointer 指针,但我认为对其了解不足,甚至无法说出它使用的是哪种魔法。 ) + +--- + +via: https://utcc.utoronto.ca/~cks/space/blog/programming/GoUintptrVsUnsafePointer + +作者:[ChrisSiebenmann](https://utcc.utoronto.ca/~cks/space/People/ChrisSiebenmann) +译者:[dust347](https://github.com/dust347) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191213-Go-Goroutine-and-Preemption.md b/published/tech/20191213-Go-Goroutine-and-Preemption.md new file mode 100644 index 000000000..195dfe8d8 --- /dev/null +++ b/published/tech/20191213-Go-Goroutine-and-Preemption.md @@ -0,0 +1,188 @@ +首发于:https://studygolang.com/articles/28972 + +# Go:Goroutine 与抢占机制 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-goroutine-and-preemption/cover.png) +> Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French. + +*本篇文章讨论实现原理基于 Go 1.13.* + +Go 通过一个内部调度器(scheduler)来管理 goroutines。该调度器负责切换和调度多个 goroutine,保证每个 Goroutine 都可以得到执行时间。同时,为了执行这些切换调度操作,调度器需要一种抢占机制,用以抢占运行的 goroutine。 + +## 调度器与抢占机制 + +我们先看一个简单的例子, 以初步了解调度器是如何工作的: + +*为了方便阅读,本文的代码示例都是非原子操作* + +```go +func main() { + var total int + var wg sync.WaitGroup + + for i := 0; i < 10; i++ { + wg.Add(1) + Go func() { + for j := 0; j < 1000; j++ { + total += readNumber() + } + wg.Done() + }() + } + + wg.Wait() +} + +//go:noinline +func readNumber() int { + return rand.Intn(10) +} +``` + +下面我们来看一下 tracing 中记录的这段代码的运行状态: +![STW_goroutines_preemption](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-goroutine-and-preemption/scheduler_preemption_tracing.png) +
+ +从上图我们可以很清晰的看到调度器在不同处理器(processor)上,切换不同的 goroutine,以保证所有的 Goroutine 都可以得到运行时间。同时,为了给 goroutines 轮流分发运行时间,当 Goroutine 由于某些原因(如等待系统调用,block 在 channel,处于 sleeping 状态,或者等待互斥锁等)而停止,Go 会负责调度这些 goroutine。 + +那么,在我们这段数字生成器程序中,互斥锁(mutex)使得调度器更方便的为所有 goroutines 分配运行时间。我们可以从 tracing 中看到清晰的看到这一点: + +![STW_goroutines_preemption](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-goroutine-and-preemption/scheduler_preemption_tracing_mutex.png) +
+ +当然,Go 也需要一种方式来停止一个 goroutine,尤其是这个 Goroutine 本身没有实现停止。我们称之为抢占(preemption)。 抢占允许调度器可以在 goroutines 直接切换。任何运行时间超过 10 毫秒的 Goroutine 都会被标记为“可抢占”状态。之后,抢占操作会在 Goroutine 的函数序言(function prologue)阶段执行。 + +下面,我们来改写一下之前的数字生成器程序代码,虽然改写后不能正常运行,但我们可以通过 tracing 来印证抢占操作执行过程: + +```go +func main() { + var total int + var wg sync.WaitGroup + + for i := gen(0); i < 20; i++ { + wg.Add(1) + Go func(g gen) { + for j := 0; j < 1e7; j++ { + total += g.readNumber() + } + wg.Done() + }(i) + } + + wg.Wait() +} + +var generators [20]*rand.Rand + +func init() { + for i := int64(0); i < 20; i++ { + generators[i] = rand.New(rand.NewSource(i).(rand.Source64)) + } +} + +type gen int +//go:noinline +func (g gen) readNumber() int { + return generators[int(g)].Intn(10) +} +``` + +我们来看一下这段代码的 tracing 情况: +![STW_goroutines_preemption](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-goroutine-and-preemption/scheduler_preemption_tracing_preempt.png) +
+ +我们从下面的细节可以看到,goroutine 在函数序言的时候,就已经被抢占: + +![STW_goroutines_preemption](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-goroutine-and-preemption/scheduler_preemption_tracing_preempt2.png) +
+ +我们通过这个例子的[asm](https://golang.org/doc/asm)代码片段可以看到,这段检查动作是编译器自动加入的: +![STW_goroutines_preemption](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-goroutine-and-preemption/scheduler_preemption_asm.png) +
+ +运行时(Runtime)通过在每个函数序言中插入指令的方式,来保证栈空间的持续运行。同时,这些函数序言中的指令也保证调度器的正常运行。 + +大部分情况,goroutine 会允许调度器来执行他们,但是如果一个循环没有任何函数调用,它是不会被调度器切换调度的,这个是一个典型的需要强制抢占的例子。 + +## 强制抢占 + +我们还是通过一个例子来看探究一个没有函数调用的循环是如何无法被调度的: + +```go +func main() { + var total int + var wg sync.WaitGroup + + for i := 0; i < 20; i++ { + wg.Add(1) + Go func() { + for j := 0; j < 1e6; j++ { + total ++ + } + wg.Done() + }() + } + + wg.Wait() +} +``` + +由于没有函数调用,所有的 Goroutine 永远都不会被 block,所以调度器没有抢占这些 goroutine。我们来看一下 tracing 中的状态:*(所有的 goroutines 没有被抢占)* +![STW_goroutines_preemption](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-goroutine-and-preemption/force_preemption_tracing.png) +
+ +为了解决这个问题,Go 提供了多种的解决办法: + +- 通过调用 `runtime.Gosched()` 强制调度器进行调度操作: + ```go + for j := 0; j < 1e8; j++ { + if j % 1e7 == 0 { + runtime.Gosched() + } + total ++ + } + ``` + 我们再看看一下上面这段程序的 tracing: +![STW_goroutines_preemption](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-goroutine-and-preemption/force_preemption_tracing1.png) +
+ +- 通过 Go 的配置,使得循环可以被抢占。具体来说,我们可以通过 `GOEXPERIMENT=preemptibleloops` 指令来重建 Go 的工具链(toolchain)或者在使用 `go build` 的时候,加上 `-gcflags -d=ssa/insert_resched_checks/on` 这个参数。 + + 我们回到之前那段没有被抢占的循环代码,通过修改配置,循环被抢占,下面是 tracing 状态: + ![STW_goroutines_preemption](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-goroutine-and-preemption/force_preemption_tracing2.png) +
+ + 从下面这段代码对应的 SSA(Static Single Assignment)代码中可以看到,当我们通过配置激活了循环中的抢占机制,编译器会在 SSA 代码中加入一段 `insert resched checks` 的 `pass`: + ![STW_goroutines_preemption](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-goroutine-and-preemption/force_preemption_pass.png) +
+ + 这段 `pass` 会新加入一系列指令,以调用调度器: + ![STW_goroutines_preemption](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-goroutine-and-preemption/force_preemption_pass2.png) +
+ + 关于 Go 编译器的实现原理,请参考我的另一篇文章 ["Go: 编译器总览"](https://medium.com/a-journey-with-go/go-overview-of-the-compiler-4e5a153ca889) + + 不过,介于在这种模式下,调度器可能会被强制触发多次,所以我们的代码效率会比没开启设置的时候低一些。下面是开启前后的对比情况: + + ``` + name old time/op new time/op delta + Loop-8 2.18s ± 2% 2.05s ± 1% -6.23% + ``` + +## 后续改进 + +目前,调度器使用协作式抢占技术,这种技术在可以支持大部分使用场景。但是,在一些特殊场景中,它也会成为痛点所在。所以,一篇文章提出了另一种 “非协作式抢占”方案,旨在解决如下问题: + +*我提议 Go 转向使用 “非协作式抢占”的实现方式,使得 Goroutine 可以在必要的时候被抢占,而不再需要显示的做抢占检查。这样就可以解决抢占延迟的问题,也会减少运行时的开销。* + +同时这篇文章也对比了几种实现技术的利弊,预计这个“非协作式抢占”会在下一个 Go 版本中实现。 + +--- + +via: https://medium.com/a-journey-with-go/go-goroutine-and-preemption-d6bc2aa2f4b7 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[SarahChenBJ](https://github.com/SarahChenBJ) +校对:[unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20191229-Using-gRPC-with-JSON.md b/published/tech/20191229-Using-gRPC-with-JSON.md new file mode 100644 index 000000000..59dd78f0b --- /dev/null +++ b/published/tech/20191229-Using-gRPC-with-JSON.md @@ -0,0 +1,115 @@ +首发于:https://studygolang.com/articles/27151 + +# 使用 JSON 协议的 gRPC + +JSON payload 实现简易的请求和响应的内省。 + +## 介绍 + +大家经常说 gRPC 是基于 [Google Protocol Buffers](https://developers.google.com/protocol-buffers/) payload 格式的,然而这不完全正确。gRPC payload 的*默认*格式是 Protobuf,但是 gRPC-Go 的实现中也对外暴露了 `Codec` [interface](https://godoc.org/google.golang.org/grpc/encoding#Codec) ,它支持任意的 payload 编码。我们可以使用任何一种格式,包括你自己定义的二进制格式、[flatbuffers](https://grpc.io/blog/flatbuffers)、或者使用我们今天要讨论的 JSON ,作为请求和响应。 + +## 服务端准备 + +我已经基于 JSON payload [实现](https://github.com/johanbrandhorst/grpc-json-example/blob/master/codec/json.go) 了 `grpc/encoding.Codec`,创建了[一个示例库](https://github.com/johanbrandhorst/grpc-json-example)。服务端的准备工作仅仅像引入一个包那样简单; + +```go +import _ "github.com/johanbrandhorst/grpc-json-example/codec" +``` + +这行代码注册了一个基于 `json` 内容的子类型 JSON `Codec`,我们在后面会看到这对于方便记忆很重要。 + +## Request 示例 + +### gRPC 客户端 + +使用 gRPC 客户端,你只需要使用合适的内容子类型作为 `grpc.DialOption` 来初始化: + +```go +import "github.com/johanbrandhorst/grpc-json-example/codec" +func main() { + conn := grpc.Dial("localhost:1000", + grpc.WithDefaultCallOptions(grpc.CallContentSubtype(codec.JSON{}.Name())), + ) +} +``` + +示例库代码包含有完整示例的[客户端](https://github.com/johanbrandhorst/grpc-json-example/blob/master/cmd/client/main.go)。 + +### cURL + +更有趣的是,现在我们可以用 cURL 写出请求(和读取响应)!请求示例: + +```bash +$ Echo -en '\x00\x00\x00\x00\x17{"id":1,"role":"ADMIN"}' | curl -ss -k --http2 \ + -H "Content-Type: application/grpc+json" \ + -H "TE:trailers" \ + --data-binary @- \ + https://localhost:10000/example.UserService/AddUser | od -bc +0000000 000 000 000 000 002 173 175 + \0 \0 \0 \0 002 { } +0000007 +$ Echo -en '\x00\x00\x00\x00\x17{"id":2,"role":"GUEST"}' | curl -ss -k --http2 \ + -H "Content-Type: application/grpc+json" \ + -H "TE:trailers" \ + --data-binary @- \ + https://localhost:10000/example.UserService/AddUser | od -bc +0000000 000 000 000 000 002 173 175 + \0 \0 \0 \0 002 { } +0000007 +$ Echo -en '\x00\x00\x00\x00\x02{}' | curl -k --http2 \ + -H "Content-Type: application/grpc+json" \ + -H "TE:trailers" \ + --data-binary @- \ + --output - \ + https://localhost:10000/example.UserService/ListUsers +F{"id":1,"role":"ADMIN","create_date":"2018-07-21T20:18:21.961080119Z"}F{"id":2,"role":"GUEST","create_date":"2018-07-21T20:18:29.225624852Z"} +``` + +#### 解释 + +使用 `cURL` 发送请求需要手动把 [gRPC HTTP2 message payload header](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests) 加到 payload: + +```bash +'\x00\x00\x00\x00\x17{"id":1,"role":"ADMIN"}' +#<-->----------------------------------------- Compression boolean (1 byte) +# <-------------->------------------------- Payload size (4 bytes) +# <--------------------->-- JSON payload +``` + +请求头必须包含 `TE` 和正确的 `Content-Type`: + +```bash +-H "Content-Type: application/grpc+json" -H "TE:trailers" +``` + +在 `Content-Type` 头中 `application/grpc+` 后的字符串需要与服务端注册的 codec 的 `Name()` 相吻合。这就是*内容子类*. + +endpoint 需要与 proto 包的名字、服务和方法三者的名字都匹配: + +```bash +https://localhost:10000/example.UserService/AddUser +``` + +响应头与请求头一致: + +```bash +'\0 \0 \0 \0 002 { }' +#<-->------------------------ Compression boolean (1 byte) +# <------------>---------- Payload size (4 bytes) +# <--->-- JSON payload +``` + +## 总结 + +我们已经展示了我们可以轻易地在 gRPC 中使用 JSON payload,甚至可以用 JSON payload 直接发送 cURL 请求到我们的 gRPC 服务,没有代理,没有 grpc 网关,除了引入一个必要的包也没有其他的准备工作。 + +如果你对本文感兴趣,或者有任何问题和想法,请在 [@johanbrandhorst](https://twitter.com/JohanBrandhorst) 上或 在 Gophers Slack `jbrandhorst` 下联系我。很高兴听到你的想法。 + +--- +via: https://jbrandhorst.com/post/grpc-json/ + +作者:[Johan Brandhorst](https://jbrandhorst.com/) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/2020-09-08-Introduction-of-goLang-AST.md b/published/tech/2020-09-08-Introduction-of-goLang-AST.md new file mode 100644 index 000000000..e3afe3bda --- /dev/null +++ b/published/tech/2020-09-08-Introduction-of-goLang-AST.md @@ -0,0 +1,493 @@ +首发于:https://studygolang.com/articles/34004 + +# GoLang AST 简介 + +## 写在前面 + +当你对 GoLang AST 感兴趣时,你会参考什么?文档还是源代码? + +虽然阅读文档可以帮助你抽象地理解它,但你无法看到 API 之间的关系等等。 + +如果是阅读整个源代码,你会完全看懂,但你想看完整个代码我觉得您应该会很累。 + +因此,本着高效学习的原则,我写了此文,希望对您能有所帮助。 + +让我们轻松一点,通过 AST 来了解我们平时写的 Go 代码在内部是如何表示的。 + +本文不深入探讨如何解析源代码,先从 AST 建立后的描述开始。 + +> 如果您对代码如何转换为 AST 很好奇,请浏览[深入挖掘分析 Go 代码](https://nakabonne.dev/posts/digging-deeper-into-the-analysis-of-go-code/)。 + +让我们开始吧! + +## 接口(Interfaces) + +首先,让我简单介绍一下代表 AST 每个节点的接口。 + +所有的 AST 节点都实现了 `ast.Node` 接口,它只是返回 AST 中的一个位置。 + +另外,还有 3 个主要接口实现了 `ast.Node`。 + +- ast.Expr - 代表表达式和类型的节点 +- ast.Stmt - 代表报表节点 +- ast.Decl - 代表声明节点 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200908-Introduction-of-goLang-AST/图 1.png) + +从定义中你可以看到,每个 Node 都满足了 `ast.Node` 的接口。 + +[ast/ast.go](https://github.com/golang/go/blob/0b7c202e98949b530f7f4011efd454164356ba69/src/go/ast/ast.go#L32-L54) + +```go +// All node types implement the Node interface. +type Node interface { + Pos() token.Pos // position of first character belonging to the node + End() token.Pos // position of first character immediately after the node +} + +// All expression nodes implement the Expr interface. +type Expr interface { + Node + exprNode() +} + +// All statement nodes implement the Stmt interface. +type Stmt interface { + Node + stmtNode() +} + +// All declaration nodes implement the Decl interface. +type Decl interface { + Node + declNode() +} +``` + +## 具体实践 + +下面我们将使用到如下代码: + +```go +package hello + +import "fmt" + +func greet() { + fmt.Println("Hello World!") +} +``` + +首先,我们尝试[生成上述这段简单的代码 AST](https://golang.org/src/go/ast/example_test.go): + +```go +package main + +import ( + "go/ast" + "go/parser" + "go/token" +) + +func main() { + src := ` +package hello + +import "fmt" + +func greet() { + fmt.Println("Hello World!") +} +` + // Create the AST by parsing src. + fset := token.NewFileSet() // positions are relative to fset + f, err := parser.ParseFile(fset, "", src, 0) + if err != nil { + panic(err) + } + + // Print the AST. + ast.Print(fset, f) +} +``` + +执行命令: + +```bash +F:\hello>go run main.go +``` +上述命令的输出 ast.File 内容如下: + +```bash + 0 *ast.File { + 1 . Package: 2:1 + 2 . Name: *ast.Ident { + 3 . . NamePos: 2:9 + 4 . . Name: "hello" + 5 . } + 6 . Decls: []ast.Decl (len = 2) { + 7 . . 0: *ast.GenDecl { + 8 . . . TokPos: 4:1 + 9 . . . Tok: import + 10 . . . Lparen: - + 11 . . . Specs: []ast.Spec (len = 1) { + 12 . . . . 0: *ast.ImportSpec { + 13 . . . . . Path: *ast.BasicLit { + 14 . . . . . . ValuePos: 4:8 + 15 . . . . . . Kind: STRING + 16 . . . . . . Value: "\"fmt\"" + 17 . . . . . } + 18 . . . . . EndPos: - + 19 . . . . } + 20 . . . } + 21 . . . Rparen: - + 22 . . } + 23 . . 1: *ast.FuncDecl { + 24 . . . Name: *ast.Ident { + 25 . . . . NamePos: 6:6 + 26 . . . . Name: "greet" + 27 . . . . Obj: *ast.Object { + 28 . . . . . Kind: func + 29 . . . . . Name: "greet" + 30 . . . . . Decl: *(obj @ 23) + 31 . . . . } + 32 . . . } + 33 . . . Type: *ast.FuncType { + 34 . . . . Func: 6:1 + 35 . . . . Params: *ast.FieldList { + 36 . . . . . Opening: 6:11 + 37 . . . . . Closing: 6:12 + 38 . . . . } + 39 . . . } + 40 . . . Body: *ast.BlockStmt { + 41 . . . . Lbrace: 6:14 + 42 . . . . List: []ast.Stmt (len = 1) { + 43 . . . . . 0: *ast.ExprStmt { + 44 . . . . . . X: *ast.CallExpr { + 45 . . . . . . . Fun: *ast.SelectorExpr { + 46 . . . . . . . . X: *ast.Ident { + 47 . . . . . . . . . NamePos: 7:2 + 48 . . . . . . . . . Name: "fmt" + 49 . . . . . . . . } + 50 . . . . . . . . Sel: *ast.Ident { + 51 . . . . . . . . . NamePos: 7:6 + 52 . . . . . . . . . Name: "Println" + 53 . . . . . . . . } + 54 . . . . . . . } + 55 . . . . . . . Lparen: 7:13 + 56 . . . . . . . Args: []ast.Expr (len = 1) { + 57 . . . . . . . . 0: *ast.BasicLit { + 58 . . . . . . . . . ValuePos: 7:14 + 59 . . . . . . . . . Kind: STRING + 60 . . . . . . . . . Value: "\"Hello World!\"" + 61 . . . . . . . . } + 62 . . . . . . . } + 63 . . . . . . . Ellipsis: - + 64 . . . . . . . Rparen: 7:28 + 65 . . . . . . } + 66 . . . . . } + 67 . . . . } + 68 . . . . Rbrace: 8:1 + 69 . . . } + 70 . . } + 71 . } + 72 . Scope: *ast.Scope { + 73 . . Objects: map[string]*ast.Object (len = 1) { + 74 . . . "greet": *(obj @ 27) + 75 . . } + 76 . } + 77 . Imports: []*ast.ImportSpec (len = 1) { + 78 . . 0: *(obj @ 12) + 79 . } + 80 . Unresolved: []*ast.Ident (len = 1) { + 81 . . 0: *(obj @ 46) + 82 . } + 83 } +``` + +### 如何分析 + +我们要做的就是按照深度优先的顺序遍历这个 AST 节点,通过递归调用 `ast.Inspect()` 来逐一打印每个节点。 + +如果直接打印 AST,那么我们通常会看到一些无法被人类阅读的东西。 + +为了防止这种情况的发生,我们将使用 `ast.Print`(一个强大的 API)来实现对 AST 的人工读取。 + +代码如下: + +```go +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" +) + +func main() { + fset := token.NewFileSet() + f, _ := parser.ParseFile(fset, "dummy.go", src, parser.ParseComments) + + ast.Inspect(f, func(n ast.Node) bool { + // Called recursively. + ast.Print(fset, n) + return true + }) +} + +var src = `package hello + +import "fmt" + +func greet() { + fmt.Println("Hello, World") +} +` +``` + +**ast.File** + +第一个要访问的节点是 `*ast.File`,它是所有 AST 节点的根。它只实现了 `ast.Node` 接口。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200908-Introduction-of-goLang-AST/图 2.png) + +`ast.File` 有引用 ` 包名 `、` 导入声明 ` 和 ` 函数声明 ` 作为子节点。 + +> 准确地说,它还有 `Comments` 等,但为了简单起见,我省略了它们。 + +让我们从包名开始。 + +> 注意,带 nil 值的字段会被省略。每个节点类型的完整字段列表请参见文档。 + +### 包名 + +**ast.Indent** + +```bash +*ast.Ident { +. NamePos: dummy.go:1:9 +. Name: "hello" +} +``` + +一个包名可以用 AST 节点类型 `*ast.Ident` 来表示,它实现了 `ast.Expr` 接口。 + +所有的标识符都由这个结构来表示,它主要包含了它的名称和在文件集中的源位置。 + +从上述所示的代码中,我们可以看到包名是 `hello`,并且是在 `dummy.go` 的第一行声明的。 + +> 对于这个节点我们不会再深入研究了,让我们再回到 `*ast.File.Go` 中。 + +### 导入声明 + +**ast.GenDecl** + +```bash +*ast.GenDecl { +. TokPos: dummy.go:3:1 +. Tok: import +. Lparen: - +. Specs: []ast.Spec (len = 1) { +. . 0: *ast.ImportSpec {/* Omission */} +. } +. Rparen: - +} +``` + +`ast.GenDecl` 代表除函数以外的所有声明,即 `import`、`const`、`var` 和 `type`。 + +`Tok` 代表一个词性标记--它指定了声明的内容(import 或 const 或 type 或 var)。 + +这个 AST 节点告诉我们,`import` 声明在 dummy.go 的第 3 行。 + +让我们从上到下深入地看一下 `ast.GenDecl` 的下一个节点 `*ast.ImportSpec`。 + +**ast.ImportSpec** + +```bash +*ast.ImportSpec { +. Path: *ast.BasicLit {/* Omission */} +. EndPos: - +} +``` + +一个 `ast.ImportSpec` 节点对应一个导入声明。它实现了 `ast.Spec` 接口,访问路径可以让导入路径更有意义。 + +**ast.BasicLit** + +```bash +*ast.BasicLit { +. ValuePos: dummy.go:3:8 +. Kind: STRING +. Value: "\"fmt\"" +} +``` + +一个 `ast.BasicLit` 节点表示一个基本类型的文字,它实现了 `ast.Expr` 接口。 + +它包含一个 token 类型,可以使用 token.INT、token.FLOAT、token.IMAG、token.CHAR 或 token.STRING。 + +从 `ast.ImportSpec` 和 `ast.BasicLit` 中,我们可以看到它导入了名为 `"fmt "` 的包。 + +我们不再深究了,让我们再回到顶层。 + +### 函数声明 + +**ast.FuncDecl** + +```bash +*ast.FuncDecl { +. Name: *ast.Ident {/* Omission */} +. Type: *ast.FuncType {/* Omission */} +. Body: *ast.BlockStmt {/* Omission */} +} +``` + +一个 `ast.FuncDecl` 节点代表一个函数声明,但它只实现了 `ast.Node` 接口。我们从代表函数名的 `Name` 开始,依次看一下。 + +**ast.Ident** + +```go +*ast.Ident { +. NamePos: dummy.go:5:6 +. Name: "greet" +. Obj: *ast.Object { +. . Kind: func +. . Name: "greet" +. . Decl: *(obj @ 0) +. } +} +``` + +第二次出现这种情况,我就不做基本解释了。 + +值得注意的是 `*ast.Object`,它代表了标识符所指的对象,但为什么需要这个呢? + +大家知道,GoLang 有一个 `scope` 的概念,就是源文本的 `scope`,其中标识符表示指定的常量、类型、变量、函数、标签或包。 + +`Decl 字` 段表示标识符被声明的位置,这样就确定了标识符的 `scope`。指向相同对象的标识符共享相同的 `*ast.Object.Label`。 + +**ast.FuncType** + +```bash +*ast.FuncType { +. Func: dummy.go:5:1 +. Params: *ast.FieldList {/* Omission */} +} +``` + +一个 `ast.FuncType` 包含一个函数签名,包括参数、结果和 "func "关键字的位置。 + +**ast.FieldList** + +```bash +*ast.FieldList { +. Opening: dummy.go:5:11 +. List: nil +. Closing: dummy.go:5:12 +} +``` + +`ast.FieldList` 节点表示一个 Field 的列表,用括号或大括号括起来。如果定义了函数参数,这里会显示,但这次没有,所以没有信息。 + +列表字段是 `*ast.Field` 的一个切片,包含一对标识符和类型。它的用途很广,用于各种 Nodes,包括 `*ast.StructType`、`*ast.InterfaceType` 和本文中使用示例。 + +也就是说,当把一个类型映射到一个标识符时,需要用到它(如以下的代码): + +```bash +foot int +bar string +``` + +让我们再次回到 `*ast.FuncDecl`,再深入了解一下最后一个字段 `Body`。 + +**ast.BlockStmt** + +```bash +*ast.BlockStmt { +. Lbrace: dummy.go:5:14 +. List: []ast.Stmt (len = 1) { +. . 0: *ast.ExprStmt {/* Omission */} +. } +. Rbrace: dummy.go:7:1 +} +``` + +一个 `ast.BlockStmt` 节点表示一个括号内的语句列表,它实现了 `ast.Stmt` 接口。 + +**ast.ExprStmt** + +```bash +*ast.ExprStmt { +. X: *ast.CallExpr {/* Omission */} +} +``` + +`ast.ExprStmt` 在语句列表中表示一个表达式,它实现了 `ast.Stmt` 接口,并包含一个 `ast.Expr`。 + +**ast.CallExpr** + +```bash +*ast.CallExpr { +. Fun: *ast.SelectorExpr {/* Omission */} +. Lparen: dummy.go:6:13 +. Args: []ast.Expr (len = 1) { +. . 0: *ast.BasicLit {/* Omission */} +. } +. Ellipsis: - +. Rparen: dummy.go:6:28 +} +``` + +`ast.CallExpr` 表示一个调用函数的表达式,要查看的字段是: + +- Fun +- 要调用的函数和 Args +- 要传递给它的参数列表 + +**ast.SelectorExpr** + +```bash +*ast.SelectorExpr { +. X: *ast.Ident { +. . NamePos: dummy.go:6:2 +. . Name: "fmt" +. } +. Sel: *ast.Ident { +. . NamePos: dummy.go:6:6 +. . Name: "Println" +. } +} +``` + +`ast.SelectorExpr` 表示一个带有选择器的表达式。简单地说,它的意思是 `fmt.Println`。 + +**ast.BasicLit** + +```bash +*ast.BasicLit { +. ValuePos: dummy.go:6:14 +. Kind: STRING +. Value: "\"Hello, World\"" +} +``` + +这个就不需要多解释了,就是简单的"Hello, World。 + +## 小结 + +需要注意的是,在介绍的节点类型时,节点类型中的一些字段及很多其它的节点类型都被我省略了。 + +尽管如此,我还是想说,即使有点粗糙,但实际操作一下还是很有意义的,而且最重要的是,它是相当有趣的。 + +复制并粘贴本文第一节中所示的代码,在你的电脑上试着实操一下吧。 + +--- +via: https://nakabonne.dev/posts/take-a-walk-the-go-ast/ + +作者:[nakabonne](https://github.com/nakabonne) +译者:[double12gzh](https://github.com/double12gzh) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200102-Go-Concurrency-And-Scheduler-Affinity.md b/published/tech/20200102-Go-Concurrency-And-Scheduler-Affinity.md new file mode 100644 index 000000000..d00490a31 --- /dev/null +++ b/published/tech/20200102-Go-Concurrency-And-Scheduler-Affinity.md @@ -0,0 +1,127 @@ +首发于:https://studygolang.com/articles/28973 + +# Go:并发以及调度器亲和 + +![由 Renee French 创作的原始 Go Gopher 作品,为“ Go 的旅程”创作的插图](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200102-Go-Concurrency-And-Scheduler-Affinity/Illustration.png) + +将 Goroutine 从一个 OS 线程切换到另一个线程需要一定开销,并且,如果这种操作过于频繁的话会降低应用性能。无论如何,随着时间的流逝,Go 的调度器已经解决了这个问题。现在,当并发工作的时候,调度器提供了 Goroutine 和线程之间的亲和性。让我们回顾历史来了解这一改进。 + +## 最初的问题 + +在 Go 的早期阶段,Go 1.0 和 1.1,当以更多的 OS 线程(即,更高的 `GOMAXPROCS` 的值)运行并发代码的时候,该语言会面临性能下降的问题。让我们以一个在文档中使用的示例开始,该示例计算质数: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20200102-Go-Concurrency-And-Scheduler-Affinity/example-code.png) + +这是在 Go 1.0.3,使用不同 `GOMAXPROCS` 值计算前十万个质数的基准测试结果: + +```bash +name time/op +Sieve 19.2s ± 0% +Sieve-2 19.3s ± 0% +Sieve-4 20.4s ± 0% +Sieve-8 20.4s ± 0% +``` + +要理解这样的结果,我们需要先理解当时调度器是如何设计的。在 Go 的最初版本,调度器只有一个全局队列,而所有的线程都可以向该队列推送和获取 goroutine。这里是一个最多以两个线程运行的应用的例子,线程数通过将 `GOMAXPROCS` 设置为 2 来定义,而线程也是之后的架构中的 `M`。 + +![最初版本的调度器只有一个全局队列](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20200102-Go-Concurrency-And-Scheduler-Affinity/first-version-of-scheduler.png) + +只有一个队列无法保证 Goroutine 能够被分配到与原来相同的线程上。最先就绪的线程会获取一个等待状态的 Goroutine 并执行该 goroutine。因此,这涉及 Goroutine 从一个线程转移到另一个线程,而这在性能方面开销很大。这里是一个阻塞 channel 的例子: + +- 7 号 Goroutine 在 channel 上阻塞并且等待信息的到来。一旦获取信息,该 Goroutine 就被放在全局队列中: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20200102-Go-Concurrency-And-Scheduler-Affinity/goroutine7-pushed-to-the-global-queue.png) + +- 之后,channel 推送消息,并且 8 号 Goroutine 在 channel 上阻塞,在此期间,X 号 Goroutine 会在可用线程上运行。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20200102-Go-Concurrency-And-Scheduler-Affinity/goroutine-8-blocks.png) + +- 7 号 Goroutine 现在运行在可用线程上: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20200102-Go-Concurrency-And-Scheduler-Affinity/goroutine-7-now-runs.png) + +goroutine 现如今运行在与之前不同的线程上。具有一个单一的全局队列也迫使调度器去持有一个涵盖所有 Goroutine 调度操作的,单个的全局互斥锁。这里是将 `GOMAXPROCS` 调高后,`pprof` 生成的 CPU profile 信息: + +```bash +Total: 8679 samples +3700 42.6% 42.6% 3700 42.6% runtime.procyield +1055 12.2% 54.8% 1055 12.2% runtime.xchg +753 8.7% 63.5% 1590 18.3% runtime.chanrecv +677 7.8% 71.3% 677 7.8% dequeue +438 5.0% 76.3% 438 5.0% runtime.futex +367 4.2% 80.5% 5924 68.3% main.filter +234 2.7% 83.2% 5005 57.7% runtime.lock +230 2.7% 85.9% 3933 45.3% runtime.chansend +214 2.5% 88.4% 214 2.5% runtime.osyield +150 1.7% 90.1% 150 1.7% runtime.cas +``` + +其中的 `procyield`,`xchg`,`futex` 和 `lock` 与 Go 调度器的全局互斥锁有关。我们清楚地看到应用浪费了大量时间在锁上。 + +这些问题让 Go 无法充分发挥处理器性能,而使用了新调度器的 Go 1.1 解决了这些问题。 + +## 并发中的亲和性 + +Go 1.1 带来了[新的调度器实现](https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw/edit?pli=1)以及本地 Goroutine 队列的建立。如果是本地的 goroutine,这个提升避免了将整个调度器锁住,同时允许这些本地 Goroutine 运行在与原来一样的 OS 线程上。 + +由于线程会因系统调用而阻塞,同时阻塞的线程数是没有限制的,Go 引入了处理器的概念。一个处理器 `P` 代表一个运行的 OS 线程,并且会管理本地的 Goroutine 队列。这是新架构: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20200102-Go-Concurrency-And-Scheduler-Affinity/new-schema.png) + +这是使用 Go 1.1.2 中新架构的新的基准测试结果: + +```bash +name time/op +Sieve 18.7s ± 0% +Sieve-2 8.26s ± 0% +Sieve-4 3.30s ± 0% +Sieve-8 2.64s ± 0% +``` + +Go 现在真正利用了所有的可用 CPU。CPU profile 信息也同样产生了变化: + +```bash +Total: 630 samples +163 25.9% 25.9% 163 25.9% runtime.xchg +113 17.9% 43.8% 610 96.8% main.filter +93 14.8% 58.6% 265 42.1% runtime.chanrecv +87 13.8% 72.4% 206 32.7% runtime.chansend +72 11.4% 83.8% 72 11.4% dequeue +19 3.0% 86.8% 19 3.0% runtime.memcopy64 +17 2.7% 89.5% 225 35.7% runtime.chansend1 +16 2.5% 92.1% 280 44.4% runtime.chanrecv2 +12 1.9% 94.0% 141 22.4% runtime.lock +9 1.4% 95.4% 98 15.6% runqput +``` + +大多数与锁相关的操作都已被移除,被标记为 `chanXXXX` 的操作仅仅与 channel 相关。然而,如果调度器提升了 Goroutine 与线程之间的亲和,在某些情况下,也可以降低这种亲和。 + +## 亲和性限制 + +为了理解亲和性的限制,我们必须理解本地和全局队列的内容。本地队列将被用于所有需要系统调用的操作,比如在 channel 和 select 上的阻塞操作,等待计时器和锁。总之,两种功能会限制 Goroutine 和线程之间的亲和性: + +- 工作窃取(Work-stealing)。当一个处理器 `P` 的本地队列没有足够的任务,如果全局队列和网络轮询器是空的,会从其他的 `P` 那里窃取任务。当完成窃取,被窃取的 Goroutine 将在其他线程上运行。 +- 系统调用。当一个系统调用发生(例如,文件操作,http 通信,数据库操作等),Go 以一个阻塞模式移动正在运行的 OS 线程,让一个新的线程处理当前 `P` 的本地队列。 + +然而,通过更好地管理本地队列的优先级,这两个限制可以避免。Go 1.5 旨在给予在 channel 上来回通信的 Goroutine 更多优先权,并因此优化了与被分配的线程的亲和性。 + +## 为了提升亲和性 + +在 channel 上来回通信的 Goroutine 最终会频繁阻塞,也就是像之前看到的那样,频繁在本地队列重新排队。然而,由于本地队列是一个 FIFO(先进先出)的实现,如果其他 Goroutine 占用了线程,解除阻塞的 Goroutine 无法保证能够尽快运行。这是一个先前在 channel 上阻塞,现在可以运行的 Goroutine 的例子: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20200102-Go-Concurrency-And-Scheduler-Affinity/goroutine-blocked-now-runnable.png) + +9 号 Goroutine 在 channel 上阻塞后恢复。然后,在它运行之前必须等待 2 号,5 号和 4 号先运行。在这个例子中,5 号 Goroutine 会占用线程,延迟了 9 号 Goroutine 的运行,同时使得 9 号可能被其他处理器所窃取。从 Go 1.5 开始,得益于 `P` 的特殊属性,从阻塞 channel 返回的 Goroutine 会优先运行: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20200102-Go-Concurrency-And-Scheduler-Affinity/run-in-priority.png) + +9 号 Goroutine 现在被标记为下一个可运行。这个新的优先次序使得该 Goroutine 在通道上再次阻塞之前快速运行。之后,其他的 Goroutine 再分配运行时间。这个改动对于 Go 标准库[提升某些包的性能](https://raw.githubusercontent.com/golang/go/commit/e870f06c3f49ed63960a2575e330c2c75fc54a34)有着总体上正向的影响。 + +--- +via: https://medium.com/a-journey-with-go/go-concurrency-scheduler-affinity-3b678f490488 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[dust347](https://github.com/dust347) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200104-Go-g0-Special-Goroutine.md b/published/tech/20200104-Go-g0-Special-Goroutine.md new file mode 100644 index 000000000..6f984a6dd --- /dev/null +++ b/published/tech/20200104-Go-g0-Special-Goroutine.md @@ -0,0 +1,81 @@ +首发于:https://studygolang.com/articles/28443 + +# Go:g0,特殊的 Goroutine + +![Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200104-Go-g0-Special-Goroutine/cover.png) + +ℹ️ 这篇文章基于 Go 1.13。 + +在 Go 中创建的所有 Goroutine 都会被一个内部的调度器所管理。Go 调度器尝试为所有的 Goroutine 分配运行时间,并且在当前的 Goroutine 阻塞或者终止的时候,Go 调度器会通过运行 Goroutine 的方式使所有 CPU 保持忙碌状态。这个调度器实际上是作为一个特殊的 Goroutine 运行的。 + +## 调度 goroutine + +Go 使用 `GOMAXPROCS` 变量限制同时运行的 OS 线程数量,这意味着 Go 必须对每个运行着的线程上的 Goroutine 进行调度和管理。这个调度的功能被委托给了一个叫做 `g0` 的特殊的 goroutine, g0 是为每个 OS 线程创建的第一个 goroutine: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200104-Go-g0-Special-Goroutine/g0-created-for-each-OS-thread.png) + +之后,g0 会把就绪状态的 Goroutine 调度到线程上去运行。 + +我建议你阅读我的文章“ [Go:Goroutine,OS 线程和 CPU 管理](https://medium.com/a-journey-with-go/go-goroutine-os-thread-and-cpu-management-2f5a5eaf518a)”,来了解更多关于 `P`,`M`,`G` 模型的信息。 + +为了更好的理解 `g0` 的调度策略,来回顾下 channel 的用法。下面是一个 Goroutine 在向 channel 发送数据是阻塞的例子: + +```go +ch := make(chan int) +[...] +ch <- v +``` + +当在 channel 上阻塞时,当前的 Goroutine 会被停放( parked ),即处于等待状态( waiting mode ),并且不会被放在任何 Goroutine 队列中: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200104-Go-g0-Special-Goroutine/blocking-goroutine-will-beparked.png) + +之后,`g0` 会替换 Goroutine 并进行一轮调度: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200104-Go-g0-Special-Goroutine/g0-replaces-the-goroutine.png) + +本地队列在调度过程中具有优先级,2 号 Goroutine 会被运行: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200104-Go-g0-Special-Goroutine/local-queue-scheduling.png) + +我建议你阅读我的文章“ [Go: Go 调度器中的工作窃取(Work-Stealing)](https://medium.com/a-journey-with-go/go-work-stealing-in-go-scheduler-d439231be64d)” 来了解更多关于调度优先级的细节。 + +一旦有接收者读取 channel 中的数据,7 号 Goroutine 就会解除阻塞状态: + +```go +v := <-ch +``` + +收到消息的 Goroutine 会切换到 `g0`,并且通过放入本地队列的方式将该 Goroutine 从停放状态解锁: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200104-Go-g0-Special-Goroutine/unlock-the-parked-goroutine.png) + +虽然这个特殊的 Goroutine 管理调度策略,但这并不是它唯一的工作,它还负责着更多的工作。 + +## 职责 + +与普通 Goroutine 不同的是,`g0` 有着固定且更大的栈,这使得在需要更大的栈的时候,以及栈不宜增长的时候,Go 可以进行操作。在 `g0` 的职责中,我们可以列出: + +- Goroutine 创建。当调用 `go func(){ ... }()` 或 `go myFunction()` 时,Go 会在把它们放入本地队列前,将函数的创建委托给 `g0` 去做: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200104-Go-g0-Special-Goroutine/goroutine-creation.png) + +新创建的 Goroutine 优先运行,并且被放在本地队列的顶部。 + +建议阅读我的文章“[Go:并发与调度器亲和性( Go: Concurrency & Scheduler Affinity )](https://medium.com/a-journey-with-go/go-concurrency-scheduler-affinity-3b678f490488)”了解更多关于 Goroutine 优先级的信息。 + +- defer 函数分配。 +- 垃圾收集操作,比如 STW( stopping the world ),扫描 Goroutine 的栈,以及一些标记清理操作。 +- 栈增长。当需要的时候,Go 会增加 Goroutine 的大小。这个操作是由 `g0` 的 prolog 函数完成的。 + +这个特殊的 Goroutine 涉及许多其他操作(较大空间的对象分配,cgo 等),需要较大的栈来保证我们的程序进行更高效的管理操作,以保持程序的低内存打印效率。 + +--- + +via: https://medium.com/a-journey-with-go/go-g0-special-goroutine-8c778c6704d8 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[dust347](https://github.com/dust347) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200105-Make-resilient-Go-net-http-servers-using-timeouts-deadlines-and-context-cancellation.md b/published/tech/20200105-Make-resilient-Go-net-http-servers-using-timeouts-deadlines-and-context-cancellation.md new file mode 100644 index 000000000..ea02f6b58 --- /dev/null +++ b/published/tech/20200105-Make-resilient-Go-net-http-servers-using-timeouts-deadlines-and-context-cancellation.md @@ -0,0 +1,431 @@ +首发于:https://studygolang.com/articles/28444 + +# 使用 timeout、deadline 和 context 取消参数使 Go net/http 服务更灵活 + +关于超时,可以把开发者分为两类:一类是了解超时多么难以捉摸的人,另一类是正在感受超时如何难以捉摸的人。 + +超时既难以捉摸,却又真实地存在于我们生活的由网络连接的世界中。在我写这篇文章的同时,隔壁两个同事正在用他们的智能手机打字,也许是在跟与他们相距万里的人聊天。网络使这一切变为可能。 + +这里要说的是网络及其复杂性,作为写网络服务的我们,必须掌握如何高效地驾驭它们,并规避它们的缺陷。 + +闲话少说,来看看超时和它们是如何影响我们的 `net/http` 服务的。 + +## 服务超时 — 基本原理 + +web 编程中,超时通常分为客户端和服务端超时两种。我之所以要研究这个主题,是因为我自己遇到了一个有意思的服务端超时的问题。这也是本文我们将要重点讨论服务侧超时的原因。 + +先解释下基本术语:超时是一个时间间隔(或边界),用来标识在这个时间段内要完成特定的行为。如果在给定的时间范围内没有完成操作,就产生了超时,这个操作会被取消。 + +从一个 `net/http` 的服务的初始化中,能看出一些超时的基础配置: + +```go +srv := &http.Server{ + ReadTimeout: 1 * time.Second, + WriteTimeout: 1 * time.Second, + IdleTimeout: 30 * time.Second, + ReadHeaderTimeout: 2 * time.Second, + TLSConfig: tlsConfig, + Handler: srvMux, +} +``` + +`http.Server` 类型的服务可以用四个不同的 timeout 来初始化: + +- `ReadTimeout`:读取包括请求体的整个请求的最大时长 +- `WriteTimeout`:写响应允许的最大时长 +- `IdleTimetout`:当开启了保持活动状态(keep-alive)时允许的最大空闲时间 +- `ReadHeaderTimeout`:允许读请求头的最大时长 + +对上述超时的图表展示: + +![Server lifecycle and timeouts](https://raw.githubusercontent.com/studygolang/gctt-images2/master/context-cancel-deadline-timeout/request-lifecycle-timeouts.png)服务生命周期和超时 + +当心!不要以为这些就是你所需要的所有的超时了。除此之外还有很多超时,这些超时提供了更小的粒度控制,对于我们的持续运行的 HTTP 处理器不会生效。 + +请听我解释。 + +## timeout 和 deadline + +如果我们查看 `net/http` 的源码,尤其是看到 [`conn` 类型](https://github.com/golang/go/blob/bbbc658/src/net/http/server.go#L248) 时,我们会发现 `conn` 实际上使用了 `net.Conn` 连接,`net.Conn` 表示底层的网络连接: + +```go +// Taken from: https://github.com/golang/go/blob/bbbc658/src/net/http/server.go#L247 +// A conn represents the server-side of an HTTP connection. +type conn struct { + // server is the server on which the connection arrived. + // Immutable; never nil. + server *Server + + // * Snipped * + + // rwc is the underlying network connection. + // This is never wrapped by other types and is the value given out + // to CloseNotifier callers. It is usually of type *net.TCPConn or + // *tls.Conn. + rwc net.Conn + + // * Snipped * +} +``` + +换句话说,我们的 HTTP 请求实际上是基于 TCP 连接的。从类型上看,TLS 连接是 `*net.TCPConn` 或 `*tls.Conn` 。 + +`serve` [函数](https://github.com/golang/go/blob/bbbc658/src/net/http/server.go#L1765)[处理每一个请求](https://github.com/golang/go/blob/bbbc658/src/net/http/server.go#L1822)时调用 `readRequest` 函数。 `readRequest` 使用我们设置的 [timeout 值](https://github.com/golang/go/blob/bbbc658/src/net/http/server.go#L946-L958)**来设置 TCP 连接的 deadline**: + +```go +// Taken from: https://github.com/golang/go/blob/bbbc658/src/net/http/server.go#L936 +// Read next request from connection. +func (c *conn) readRequest(ctx context.Context) (w *response, err error) { + // *Snipped* + + t0 := time.Now() + if d := c.server.readHeaderTimeout(); d != 0 { + hdrDeadline = t0.Add(d) + } + if d := c.server.ReadTimeout; d != 0 { + wholeReqDeadline = t0.Add(d) + } + c.rwc.SetReadDeadline(hdrDeadline) + if d := c.server.WriteTimeout; d != 0 { + defer func() { + c.rwc.SetWriteDeadline(time.Now().Add(d)) + }() + } + + // *Snipped* +} +``` + +从上面的摘要中,我们可以知道:我们对服务设置的 timeout 值最终表现为 TCP 连接的 deadline 而不是 HTTP 超时。 + +所以,deadline 是什么?工作机制是什么?如果我们的请求耗时过长,它们会取消我们的连接吗? + +一种简单地理解 deadline 的思路是,把它理解为对作用于连接上的特定的行为的发生限制的一个时间点。例如,如果我们设置了一个写的 deadline,当过了这个 deadline 后,所有对这个连接的写操作都会被拒绝。 + +尽管我们可以使用 deadline 来模拟超时操作,但我们还是不能控制处理器完成操作所需的耗时。deadline 作用于连接,因此我们的服务仅在处理器尝试访问连接的属性(如对 `http.ResponseWriter` 进行写操作)之后才会返回(错误)结果。 + +为了实际验证上面的论述,我们来创建一个小的 handler,这个 handler 完成操作所需的耗时相对于我们为服务设置的超时更长: + +```go +package main + +import ( + "fmt" + "io" + "net/http" + "time" +) + +func slowHandler(w http.ResponseWriter, req *http.Request) { + time.Sleep(2 * time.Second) + io.WriteString(w, "I am slow!\n") +} + +func main() { + srv := http.Server{ + Addr: ":8888", + WriteTimeout: 1 * time.Second, + Handler: http.HandlerFunc(slowHandler), + } + + if err := srv.ListenAndServe(); err != nil { + fmt.Printf("Server failed: %s\n", err) + } +} +``` + +上面的服务有一个 handler,这个 handler 完成操作需要两秒。另一方面,`http.Server` 的 `WriteTimeout` 属性设为 1 秒。基于服务的这些配置,我们猜测 handler 不能把响应写到连接。 + +我们可以用 `go run server.go` 来启动服务。使用 `curl localhost:8888` 来发送一个请求: + +```shell +$ time curl localhost:8888 +curl: (52) Empty reply from server +curl localhost:8888 0.01s user 0.01s system 0% CPU 2.021 total +``` + +这个请求需要两秒来完成处理,服务返回的响应是空的。虽然我们的服务知道在 1 秒之后我们写不了响应了,但 handler 还是多耗了 100% 的时间(2 秒)来完成处理。 + +虽然这是个类似超时的处理,但它更大的作用是在到达超时时间时,阻止服务进行更多的操作,结束请求。在我们上面的例子中,handler 在完成之前一直在处理请求,即使已经超出响应写超时时间(1 秒)100%(耗时 2 秒)。 + +最根本的问题是,对于处理器来说,我们应该怎么设置超时时间才更有效? + +## 处理超时 + +我们的目标是确保我们的 `slowHandler` 在 1s 内完成处理。如果超过了 1s,我们的服务会停止运行并返回对应的超时错误。 + +在 Go 和一些其它编程语言中,组合往往是设计和开发中最好的方式。标准库的 [`net/http` 包](https://golang.org/pkg/net/http)有很多相互兼容的元素,开发者可以不需经过复杂的设计考虑就可以轻易将它们组合在一起。 + +基于此,`net/http` 包提供了[`TimeoutHandler`](https://golang.org/pkg/net/http/#TimeoutHandler) — 返回了一个在给定的时间限制内运行的 handler。 + +函数签名: + +```go +func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler +``` + +第一个参数是 `Handler`,第二个参数是 `time.Duration` (超时时间),第三个参数是 `string` 类型,当到达超时时间后返回的信息。 + +用 `TimeoutHandler` 来封装我们的 `slowHandler`,我们只需要: + +```go +package main + +import ( + "fmt" + "io" + "net/http" + "time" +) + +func slowHandler(w http.ResponseWriter, req *http.Request) { + time.Sleep(2 * time.Second) + io.WriteString(w, "I am slow!\n") +} + +func main() { + srv := http.Server{ + Addr: ":8888", + WriteTimeout: 5 * time.Second, + Handler: http.TimeoutHandler(http.HandlerFunc(slowHandler), 1*time.Second, "Timeout!\n"), + } + + if err := srv.ListenAndServe(); err != nil { + fmt.Printf("Server failed: %s\n", err) + } +} +``` + +两个需要留意的地方是: + +- 我们在 `http.TimetoutHandler` 里封装 `slowHanlder`,超时时间设为 1s,超时信息为 “Timeout!”。 +- 我们把 `WriteTimeout` 增加到 5s,以给予 `http.TimeoutHandler` 足够的时间执行。如果我们不这么做,当 `TimeoutHandler` 开始执行时,已经过了 deadline,不能再写到响应。 + +如果我们再启动服务,当程序运行到 slow handler 时,会有如下输出: + +```shell +$ time curl localhost:8888 +Timeout! +curl localhost:8888 0.01s user 0.01s system 1% CPU 1.023 total +``` + +1s 后,我们的 `TimeoutHandler` 开始执行,阻止运行 `slowHandler`,返回文本信息 ”Timeout!“。如果我们设置信息为空,handler 会返回默认的超时响应信息,如下: + +```html + + + Timeout + + +

Timeout

+ + +``` + +如果忽略掉输出,这还算是整洁,不是吗?现在我们的程序不会有过长耗时的处理;也避免了有人恶意发送导致长耗时处理的请求时,导致的潜在的 DoS 攻击。 + +尽管我们设置超时时间是一个伟大的开始,但它仍然只是初级的保护。如果你可能会面临 DoS 攻击,你应该采用更高级的保护工具和技术。(可以试试 [Cloudflare](https://www.cloudflare.com/ddos/) ) + +我们的 `slowHandler` 仅仅是个简单的 demo。但是,如果我们的程序复杂些,能向其他服务和资源发出请求会发生什么呢?如果我们的程序在超时时向诸如 S3 的服务发出了请求会怎么样? + +会发生什么? + +## 未处理的超时和请求取消 + +我们稍微展开下我们的例子: + +```go +func slowAPICall() string { + d := rand.Intn(5) + select { + case <-time.After(time.Duration(d) * time.Second): + log.Printf("Slow API call done after %s seconds.\n", d) + return "foobar" + } +} + +func slowHandler(w http.ResponseWriter, r *http.Request) { + result := slowAPICall() + io.WriteString(w, result+"\n") +} +``` + +我们假设最初我们不知道 `slowHandler` 由于通过 `slowAPICall` 函数向 API 发请求导致需要耗费这么长时间才能处理完成, + +`slowAPICall` 函数很简单:使用 `select` 和一个能阻塞 0 到 5 秒的 `time.After` 。当经过了阻塞的时间后,`time.After` 方法通过它的 channel 发送一个值,返回 `"foobar"` 。 + +(另一种方法是,使用 `sleep(time.Duration(rand.Intn(5)) * time.Second)`,但我们仍然使用 `select`,因为它会使我们下面的例子更简单。) + +如果我们运行起服务,我们预期超时 handler 会在 1 秒之后中断请求处理。来发送一个请求验证一下: + +```shell +$ time curl localhost:8888 +Timeout! +curl localhost:8888 0.01s user 0.01s system 1% CPU 1.021 total +``` + +通过观察服务的输出,我们会发现,它是在几秒之后打出日志的,而不是在超时 handler 生效时打出: + +```shell +$ Go run server.go +2019/12/29 17:20:03 Slow API call done after 4 seconds. +``` + +这个现象表明:虽然 1 秒之后请求超时了,但是服务仍然完整地处理了请求。这就是在 4 秒之后才打出日志的原因。 + +虽然在这个例子里问题很简单,但是类似的现象在生产中可能变成一个严重的问题。例如,当 `slowAPICall` 函数开启了几个百个协程,每个协程都处理一些数据时。或者当它向不同系统发出多个不同的 API 发出请求时。这种耗时长的的进程,它们的请求方/客户端并不会使用服务端的返回结果,会耗尽你系统的资源。 + +所以,我们怎么保护系统,使之不会出现类似的未优化的超时或取消请求呢? + +## 上下文超时和取消 + +Go 有一个包名为 [`context`](https://golang.org/pkg/context/) 专门处理类似的场景。 + +`context` 包在 Go 1.7 版本中提升为标准库,在之前的版本中,以 [`golang.org/x/net/context`](https://godoc.org/golang.org/x/net/context) 的路径作为 [Go Sub-repository Packages](https://godoc.org/-/subrepo) 出现。 + +这个包定义了 `Context` 类型。它最初的目的是保存不同 API 和不同处理的截止时间、取消信号和其他请求相关的值。如果你想了解关于 context 包的其他信息,可以阅读 [Golang's blog](https://blog.golang.org/context) 中的 “Go 并发模式:Context”(译注:Go Concurrency Patterns: Context) . + + `net/http` 包中的的 `Request` 类型已经有 `context` 与之绑定。从 Go 1.7 开始,`Request` 新增了一个返回请求的上下文的 [`Context` 方法](https://golang.org/pkg/net/http/#Request.Context)。对于进来的请求,在客户端关闭连接、请求被取消(HTTP/2 中)或 `ServeHTTP` 方法返回后,服务端会取消上下文。 + +我们期望的现象是,当客户端取消请求(输入了 `CTRL + C`)或一段时间后 `TimeoutHandler` 继续执行然后终止请求时,服务端会停止后续的处理。进而关闭所有的连接,释放所有被运行中的处理进程(及它的所有子协程)占用的资源。 + +我们把 `Context` 作为参数传给 `slowAPICall` 函数: + +```go +func slowAPICall(ctx context.Context) string { + d := rand.Intn(5) + select { + case <-time.After(time.Duration(d) * time.Second): + log.Printf("Slow API call done after %d seconds.\n", d) + return "foobar" + } +} + +func slowHandler(w http.ResponseWriter, r *http.Request) { + result := slowAPICall(r.Context()) + io.WriteString(w, result+"\n") +} +``` + +在例子中我们利用了请求上下文,实际中怎么用呢?[`Context` 类型](https://golang.org/pkg/context/#Context)有个 `Done` 属性,类型为 `<-chan struct{}`。当进程处理完成时,`Done` 关闭,此时表示上下文应该被取消,而这正是例子中我们需要的。 + +我们在 `slowAPICall` 函数中用 `select` 处理 `ctx.Done` 通道。当我们通过 `Done` 通道接收一个空的 `struct` 时,意味着上下文取消,我们需要让 `slowAPICall` 函数返回一个空字符串。 + +```go +func slowAPICall(ctx context.Context) string { + d := rand.Intn(5) + select { + case <-ctx.Done(): + log.Printf("slowAPICall was supposed to take %s seconds, but was canceled.", d) + return "" + //time.After() 可能会导致内存泄漏 + case <-time.After(time.Duration(d) * time.Second): + log.Printf("Slow API call done after %d seconds.\n", d) + return "foobar" + } +} +``` + +(这就是使用 `select` 而不是 `time.Sleep` -- 这里我们只能用 `select` 处理 `Done` 通道。 ) + +在这个简单的例子中,我们成功得到了结果 -- 当我们从 `Done` 通道接收值时,我们打印了一行日志到 STDOUT 并返回了一个空字符串。在更复杂的情况下,如发送真实的 API 请求,你可能需要关闭连接或清理文件描述符。 + +我们再启动服务,发送一个 `cRUL` 请求: + +```shell +# The cURL command: +$ curl localhost:8888 +Timeout! + +# The server output: +$ Go run server.go +2019/12/30 00:07:15 slowAPICall was supposed to take 2 seconds, but was canceled. +``` + +检查输出:我们发送了 `cRUL` 请求到服务,它耗时超过 1 秒,服务取消了 `slowAPICall` 函数。我们几乎不需要写任何代码。`TimeoutHandler` 为我们代劳了 -- 当处理耗时超过预期时,`TimeoutHandler` 终止了处理进程并取消请求上下文。 + +`TimeoutHandler` 是在 [`timeoutHandler.ServeHTTP` 方法](https://github.com/golang/go/blob/bbbc658/src/net/http/server.go#L3217-L3263) 中取消上下文的: + +```go +// Taken from: https://github.com/golang/go/blob/bbbc658/src/net/http/server.go#L3217-L3263 +func (h *timeoutHandler) ServeHTTP(w ResponseWriter, r *Request) { + ctx := h.testContext + if ctx == nil { + var cancelCtx context.CancelFunc + ctx, cancelCtx = context.WithTimeout(r.Context(), h.dt) + defer cancelCtx() + } + r = r.WithContext(ctx) + + // *Snipped* +} +``` + +上面例子中,我们通过调用 `context.WithTimeout` 来使用请求上下文。超时值 `h.dt` (`TimeoutHandler` 的第二个参数)设置给了上下文。返回的上下文是请求上下文设置了超时值后的一份拷贝。随后,它作为请求上下文传给 `r.WithContext(ctx)`。 + +`context.WithTimeout` 方法执行了上下文取消。它返回了 `Context` 设置了一个超时值之后的副本。当到达超时时间后,就取消上下文。 + +这里是执行的代码: + +```go +// Taken from: https://github.com/golang/go/blob/bbbc6589/src/context/context.go#L486-L498 +func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { + return WithDeadline(parent, time.Now().Add(timeout)) +} + +// Taken from: https://github.com/golang/go/blob/bbbc6589/src/context/context.go#L418-L450 +func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { + // *Snipped* + + c := &timerCtx{ + cancelCtx: newCancelCtx(parent), + deadline: d, + } + + // *Snipped* + + if c.err == nil { + c.timer = time.AfterFunc(dur, func() { + c.cancel(true, DeadlineExceeded) + }) + } + return c, func() { c.cancel(true, Canceled) } +} +``` + +这里我们又看到了截止时间。`WithDeadline` 函数设置了一个 `d` 到达之后执行的函数。当到达截止时间后,它调用 `cancel` 方法处理上下文,此方法会关闭上下文的 `done` 通道并设置上下文的 `timer` 属性为 `nil`。 + +`Done` 通道的关闭有效地取消了上下文,使我们的 `slowAPICall` 函数终止了它的执行。这就是 `TimeoutHandler` 终止耗时长的处理进程的原理。 + +(如果你想阅读上面提到的源码,你可以去看 [`cancelCtx` 类型](https://github.com/golang/go/blob/bbbc6589dfbc05be2bfa59f51c20f9eaa8d0c531/src/context/context.go#L389-L416) 和 [`timerCtx` 类型](https://github.com/golang/go/blob/bbbc6589dfbc05be2bfa59f51c20f9eaa8d0c531/src/context/context.go#L472-L484)) + +## 有弹性的 `net/http` 服务 + +连接截止时间提供了低级的细粒度控制。虽然它们的名字中含有“超时”,但它们并没有表现出人们通常期望的“超时”。实际上它们非常强大,但是使用它们有一定的门槛。 + +另一个角度讲,当处理 HTTP 时,我们仍然应该考虑使用 `TimeoutHandler`。Go 的作者们也选择使用它,它有多种处理,提供了如此有弹性的处理以至于我们甚至可以对每一个处理使用不同的超时。`TimeoutHandler` 可以根据我们期望的表现来控制执行进程。 + +除此之外,`TimeoutHandler` 完美兼容 `context` 包。`context` 包很简单,包含了取消信号和请求相关的数据,我们可以使用这些数据来使我们的应用更好地处理错综复杂的网络问题。 + +结束之前,有三个建议。写 HTTP 服务时,怎么设计超时: + +1. 最常用的,到达 `TimeoutHandler` 时,怎么处理。它进行我们通常期望的超时处理。 +2. 不要忘记上下文取消。`context` 包使用起来很简单,并且可以节省你服务器上的很多处理资源。尤其是在处理异常或网络状况不好时。 +3. 一定要用截止时间。确保做了完整的测试,验证了能提供你期望的所有功能。 + +更多关于此主题的文章: + +- “The complete guide to Go net/http timeouts” on [Cloudflare's blog](https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/) +- “So you want to expose Go on the Internet” on [Cloudflare's blog](https://blog.cloudflare.com/exposing-go-on-the-internet/) +- “Use http.TimeoutHandler or ReadTimeout/WriteTimeout?” on [Stackoverflow](https://stackoverflow.com/questions/51258952/use-http-timeouthandler-or-readtimeout-writetimeout) +- “Standard net/http config will break your production environment” on [Simon Frey's blog](https://blog.simon-frey.eu/go-as-in-golang-standard-net-http-config-will-break-your-production) + +--- + +via: https://ieftimov.com/post/make-resilient-golang-net-http-servers-using-timeouts-deadlines-context-cancellation/ + +作者:[Ilija Eftimov](https://ieftimov.com/) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200115-How-Does-Go-Stop-the-World.md b/published/tech/20200115-How-Does-Go-Stop-the-World.md new file mode 100644 index 000000000..73334cb5a --- /dev/null +++ b/published/tech/20200115-How-Does-Go-Stop-the-World.md @@ -0,0 +1,128 @@ +首发于:https://studygolang.com/articles/28450 + +# Go 语言如何实现垃圾回收中的 Stop the World (STW) + +![Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.](https://raw.githubusercontent.com/studygolang/gctt-images/master/how-does-go-stop-the-world/cover.png) + +## 目录 + +- [Stop The World(STW)](#stop-the-worldstw) +- [系统调用](#%e7%b3%bb%e7%bb%9f%e8%b0%83%e7%94%a8) +- [延迟](#%e5%bb%b6%e8%bf%9f) + +*本篇文章讨论实现原理基于 Go 1.13.* + +在垃圾回收机制 (GC) 中,"Stop the World" (STW) 是一个重要阶段。 顾名思义, 在 "Stop the World" 阶段, 当前运行的所有程序将被暂停, 扫描内存的 root 节点和添加写屏障 (write barrier) 。 本篇文章讨论的是, "Stop the World" 内部工作原理及我们可能会遇到的潜在风险。 + +## Stop The World(STW) + +这里面的"停止", 指的是停止正在运行的 goroutines。 下面这段程序就执行了 "Stop the World": + +```go +func main() { + runtime.GC() +} +``` + +这段代码中, 调用 `runtime.GC()` 执行垃圾回收, 会触发 "Stop the World"的三个步骤。 + +(关于关于垃圾回收机制, 可以参考我的另外一篇文章 ["Go: 内存标记在垃圾回收中的实现"](https://medium.com/a-journey-with-go/go-how-does-the-garbage-collector-mark-the-memory-72cfc12c6976)): + +这个阶段的第一步, 是抢占所有正在运行的 goroutine(即图中 `G`): + +![STW_goroutines_preemption](https://raw.githubusercontent.com/studygolang/gctt-images/master/how-does-go-stop-the-world/STW_goroutines_preemption.png) + +被抢占之后, 这些 Goroutine 会被悬停在一个相对安全的状态。 同时,承载 Goroutine 的处理器 `P` (无论是正在运行代码的处理器还是已在 idle 列表中的处理器), 都会被被标记成停止状态 (stopped), 不再运行任何代码: + +![STW_P_stopped](https://raw.githubusercontent.com/studygolang/gctt-images/master/how-does-go-stop-the-world/STW_P_stopped.png) + +接下来, Go 调度器 (Scheduler) 开始调度, 把每个处理器的 Marking Worker (即图中 `M`) 从各自对应的处理器 `P` 分离出来, 放到 idle 列表中去, 如下图: + +![STW_M_Detach](https://raw.githubusercontent.com/studygolang/gctt-images/master/how-does-go-stop-the-world/STW_M_Detach.png) + +在停止了处理器和 Marking Worker 之后, 对于 Goroutine 本身, 他们会被放到一个全局队列中等待: + +![STW_G_Queue](https://raw.githubusercontent.com/studygolang/gctt-images/master/how-does-go-stop-the-world/STW_G_Queue.png) + +到目前为止, 整个"世界"被停止. 至此, 仅存的 "Stop The World" (STW)goroutine 可以开始接下来的回收工作, 在一些列的操作结束之后, 再启动整个"世界"。 + +我们也可以在 Tracing 工具中看到一次 STW 的运行状态: + +![STW_TRACING](https://raw.githubusercontent.com/studygolang/gctt-images/master/how-does-go-stop-the-world/STW_TRACING.png) + +## 系统调用 + +下面我们来讨论一下 STW 是如何处理系统调用的。 + +我们知道, 系统调用是需要返回的, 那么当整个"世界"被停止的时候, 已经存在的系统调用如何被处理呢? + +我们通过一个实际例子来理解: + +```go +func main() { + var wg sync.WaitGroup + wg.Add(10) + for i := 0; i < 10; i++ { + Go func() { + http.Get(`https://httpstat.us/200`) + wg.Done() + }() + } + wg.Wait() +} +``` + +这是一段简单的系统调用的程序, 我们通过 Tracing 工具看一下它是如何被处理的: + +![SC_tracing](https://raw.githubusercontent.com/studygolang/gctt-images/master/how-does-go-stop-the-world/SC_tracing.png) + +我们可以看到, 这个系统调用 Goroutine (即图中 `G30`) 在"世界"被停止的时候, 就已经存在了。 + +但是, 我们之前提到, STW 把所有的处理器 `P` 都标为停止状态 (stopped) , 所以, 这个系统调用的 Goroutine 也会被放到全局队列中, 等待 Golang 世界恢复之后, 被重新启用。 + +## 延迟 + +前文提到 STW 的第三步是将 Marking Worker(`M`) 从处理器(`P`)上分离, 然后放入 idle 列表中。 + +而实际上, Go 会等待他们自发停止, 也就是说当调度器(scheduler)运行的时候, 系统调用在运行的时候, STW 会等待。 + +理论上, 等待一个 Goroutine 被抢占是很快的, 但是在有些情况下, 还是会出现相应的延迟。 + +我们通过一个例子来模拟类似情况: + +```go +func main() { + var t int + for i := 0;i < 20 ;i++ { + Go func() { + for i := 0;i < 1000000000 ;i++ { + t++ + } + }() + } + + runtime.GC() +} +``` + +我们还是来看一下这段代码运行的 Tracing 情况, 从下图我们可以看到 STW 阶段总共耗时 2.6 秒: + +![STW_26S](https://raw.githubusercontent.com/studygolang/gctt-images/master/how-does-go-stop-the-world/STW_26S.png) + +我们来简单分析为什么会出现这么长的 STW: 正如例子中的 main 函数, 一个没有函数调用的 Goroutine 一般不会被抢占, 那么这个 Goroutine 对应的处理器 `P` 在任务结束之前不会被释放。 + +而 STW 的机制是等待它自发停止, 因此就出现了 2.6 秒的 STW。 + +为了提高整体程序的效率, 我们一般需要避免或者改进这种情况。 + +关于这部分, 大家可以参考我的另一篇文章 ["Go: Goroutine 和抢占"](https://medium.com/a-journey-with-go/go-goroutine-and-preemption-d6bc2aa2f4b7) + +--- + +via: https://medium.com/a-journey-with-go/go-how-does-go-stop-the-world-1ffab8bc8846 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[SarahChenBJ](https://github.com/SarahChenBJ) +校对:[@unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200201-Go How-Are-Loops-Translated-to-Assembly.md b/published/tech/20200201-Go How-Are-Loops-Translated-to-Assembly.md new file mode 100644 index 000000000..9d6a307d1 --- /dev/null +++ b/published/tech/20200201-Go How-Are-Loops-Translated-to-Assembly.md @@ -0,0 +1,174 @@ +首发于:https://studygolang.com/articles/28455 + +# Go 中的循环是如何转为汇编的? + +![Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200201-how-are-loops/cover.png) + +*本文基于 Go 1.13 版本* + +循环在编程中是一个重要的概念,且易于上手。但是,循环必须被翻译成计算机能理解的底层指令。它的编译方式也会在一定程度上影响到标准库中的其他组件。让我们开始分析循环吧。 + +## 循环的汇编代码 + +使用循坏迭代 `array`,`slice`,`channel`,以下是一个使用循环对 `slice` 计算总和的例子。 + +```go +func main() { + l := []int{9, 45, 23, 67, 78} + t := 0 + + for _, v := range l { + t += v + } + + println(t) +} +``` + +使用 `go tool compile -S main.go` 生成的汇编代码,以下为相关输出: + +```go +0x0041 00065 (main.go:4) XORL AX, AX +0x0043 00067 (main.go:4) XORL CX, CX + +0x0045 00069 (main.go:7) JMP 82 +0x0047 00071 (main.go:7) MOVQ ""..autotmp_5+16(SP)(AX*8), DX +0x004c 00076 (main.go:7) INCQ AX +0x004f 00079 (main.go:8) ADDQ DX, CX +0x0052 00082 (main.go:7) CMPQ AX, $5 +0x0056 00086 (main.go:7) JLT 71 +0x0058 00088 (main.go:11) MOVQ CX, "".t+8(SP) +``` + +我把这些指令分为了两个部分,初始化部分和循环主体。前两条指令,将两个寄存器初始化为零值。 + +```go +0x0041 00065 (main.go:4) XORL AX, AX +0x0043 00067 (main.go:4) XORL CX, CX +``` + +寄存器 `AX` 包含着当前循环所处位置,而 `CX` 包含着变量 `t` 的值,下面为带有指令和通用寄存器的直观表示: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200201-how-are-loops/1.png) + +循环从表示「跳转到指令 82 」的 `JMP 82` 开始,这条指令的作用可以通过第二行来判断: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200201-how-are-loops/2.png) + +接下来的指令 `CMPQ AX,$5` 表示「比较寄存器 `AX` 和 `5`」,事实上,这个操作是把 `AX` 中的值减去 5 ,然后储存在另一个寄存器中,这个值可以被用在下一条指令 `JLT 71` 中,它的含义是 「如果值小于 0 则跳转到指令 71 」,以下是更新后的直观表示: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200201-how-are-loops/3.png) + +如果不满足条件,则程序将会跳转到循环体之后的下一条指令执行。 + +所以,我们现在有了对循环的基本框架,以下是转换后的 Go 循环: + +```go +goto end +start: + ? +end: + if i < 5 { + goto start + } + +println(t) +``` + +我们缺少了循环的主体,接下来,我们看看这部分的指令: + +```go +0x0047 00071 (main.go:7) MOVQ ""..autotmp_5+16(SP)(AX*8), DX +0x004c 00076 (main.go:7) INCQ AX +0x004f 00079 (main.go:8) ADDQ DX, CX +``` + +第一条指令 `MOVQ ""..autotmp_5+16(SP)(AX*8), DX` 表示 「将内存从源位置移动到目标地址」,它由以下几个部分组成: + +* `""..autotmp_5+16(SP)` 表示 `slice` ,而 `SP` 表示了栈指针即我们当前的内存空间, `autotmp_*` 是自动生成变量名。 +* 偏差为 8 是因为在 64 位计算机架构中,`int` 类型是 8 字节的。偏差乘以寄存器 `AX` 的值,表示当前循环中的位置。 +* 寄存器 `DX` 代表的目标地址内包含着循环的当前值。 + +之后,`INCQ` 表示自增,然后会增加循环的当前位置: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200201-how-are-loops/4.png) +循环主体的最后一条指令是 `ADDQ DX, CX` ,表示把 `DX` 的值加在 `CX`,所以我们可以看出,`DX` 所包含的值是目前循环所代表的的值,而 `CX` 代表了变量 `t` 的值。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200201-how-are-loops/5.png) + +他会一直循环至计数器到 5 ,之后循环体之后的指令表示为将寄存器 `CX` 的值赋予 `t` : + +```GO +0x0058 00088 (main.go:11) MOVQ CX, "".t+8(SP) +``` + +以下为最终状态的示意图: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200201-how-are-loops/6.png) + +我们可以完善 Go 中循环的转换: + +```go +func main() { + l := []int{9, 45, 23, 67, 78} + t := 0 + i := 0 + + var tmp int + + goto end +start: + tmp = l[i] + i++ + t += tmp +end: + if i < 5 { + goto start + } + + println(t) +} +``` + +这个程序生成的汇编代码与上文所提到的函数生成的汇编代码有着相同的输出。 + +## 改进 + +循环的内部转换方式可能会对其他特性(如 Go 调度器)产生影响。在 Go 1.10 之前,循环像下面的代码一样编译: + +```go +func main() { + l := []int{9, 45, 23, 67, 78} + t := 0 + i := 0 + + var tmp int + p := uintptr(unsafe.Pointer(&l[0])) + + if i >= 5 { + goto end + } +body: + tmp = *(*int)(unsafe.Pointer(p)) + p += unsafe.Sizeof(l[0]) + i++ + t += tmp + if i < 5 { + goto body + } +end: + println(t) +} +``` + +这种实现方式的问题是,当 `i` 达到 5 时,指针 `p` 已经超过了内存分配空间的尾部。这个问题使得循环不容易抢占,因为它的主体是不安全的。循环编译的优化确保它不会创建任何越界的指针。这个改进是为 Go 调度器中的非合作抢占做准备的。你可以在这篇 [Proposal](https://github.com/golang/proposal/blob/master/design/24543-non-cooperative-preemption.md) 中到更详细的讨论。 + +--- + +via: https://medium.com/a-journey-with-go/go-how-are-loops-translated-to-assembly-835b985309b3 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[Jun10ng](https://github.com/Jun10ng) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200203-is-unsafe-unsafe-part1.md b/published/tech/20200203-is-unsafe-unsafe-part1.md new file mode 100644 index 000000000..91eda6b24 --- /dev/null +++ b/published/tech/20200203-is-unsafe-unsafe-part1.md @@ -0,0 +1,164 @@ +首发于:https://studygolang.com/articles/28433 + +# unsafe 真就 unsafe 吗 - part1 + +## unsafe 包详解 + +在乌克兰的利沃夫举行的 [Lviv Golang community event](https://www.facebook.com/events/470065893928934/482981832637340/?notif_t=admin_plan_mall_activity¬if_id=1580732874088578) 中,我发表了一个关于 `unsafe` 包的演讲,这个演讲中我尝试回答了标题中提到的问题:`unsafe` 包究竟有多 unsafe。 + +从 `unsafe` 包的名字就能感受到 Go 研发团队的警告:使用这个包的代价将是巨大的。我觉得这个包名起的非常巧妙,它完美地符合了 《Effective Go》中对包名的所有建议。在使用 unsafe 包的时候,我们应当严格遵循 Go 研发团队的文档和建议。这个包的官方概述就只有简单的一段话: + +> unsafe 包里面包含了一些能让你践踏 Go 语言的类型安全特性的操作。[golang.org](https://golang.org/pkg/unsafe/#pkg-overview) + +附带一段简单的警告: + +> 引用 unsafe 包可能会导致你代码的不具备可移植性,并且不再受到 Go 1 兼容性规约的保护。[golang.org](https://golang.org/pkg/unsafe/#pkg-overview) + +函数功能的描述看起来非常的抽象,我们来瞅一眼这些 "unsafe" 的操作: + +```go +func Alignof(x ArbitraryType) uintptr +func Offsetof(x ArbitraryType) uintptr +func Sizeof(x ArbitraryType) uintptr +type ArbitraryType +type Pointer +``` + +其中有一个 `ArbitraryType` 类型: + +> 只是为了文档记录的目的而存在,实际上它没有参与到 unsafe 包的实现。这个类型代表了任意的 Go 语言表达式。 + +所以实际上 unsafe 包就只包含三个函数和一个类型,既然就这么点东西,那我们试着把这个包全部过一遍。现在我们手头上已经有了 Go 研发团队给我的文档和源码,下一步要怎么做?这时候不妨重温一句名人名言: + +> 多说无益,放码过来 —— [Linus Torvalds](https://lkml.org/lkml/2000/8/25/132) + +好,既然如此我们就直接看源码吧…… + +![Gif](https://raw.githubusercontent.com/studygolang/gctt-images2/master/is-unsafe/1.gif) + +神奇的事情发生了——这个 unsafe 包压根就[没有源码](https://golang.org/src/unsafe/unsafe.go)呀。它有函数的签名和类型定义,但是没有实现的代码:无论是 Go 还是汇编的代码都没有。之所以会出现这个情况,是因为 unsafe 包的功能需要在层次更低的编译器层面实现,所以这个包其实是内置在编译器里面实现的,这个 .go 文件只是为了达到文档记录的目的。所以我在上文反复强调要严格遵循 Go 研发团队的文档和建议,因为你也只能看到这些文档。废话不多说,先来看看 `Sizeof` 函数吧。 + +### `func Sizeof(x ArbitraryType) uintptr` + +函数接受某个变量,然后返回 `uintptr` 类型的结果。这个函数的名字可以看出,这个函数返回某个变量的大小。为了理解方便,请允许我用几个图示来可视化一下这些概念。众所周知,我们的 Go 程序需要内存来完成各种功能,其中就包含使用内存来保存变量。下面我将用这些标签来表示内存: + +🎁 - 1 个字节的内存 + +📦 - 1 个字节的被占用的内存 + +🥡 - 1 个字节的被占用但实际没有作用的内存(后面会详细解释这个) + +⬆️ - 指向内存地址的指针 + +下面我使用这些标签来展示这个结构的内存布局: + +```go +type X struct { + n1 int16 + n2 int16 +} +``` + +它在内存中的布局是这样的 + +![Sizeof memory usage](https://raw.githubusercontent.com/studygolang/gctt-images2/master/is-unsafe/2.jpg) + +`X` 结构体有两个字段,其中每一个都占 2 个字节,所以整个结构体占用 size(`n1`) + size(`n2`) + size(`X`) = 2 + 2 + 0 = 4。显然,下面语句是成立的: + +```go +unsafe.Sizeof(X) == 4 // true +``` + +### `func Offsetof(x ArbitraryType) uintptr` + +这个函数就有点难度了,函数签名和上面的函数是同样的,但是它返回的是 offset(偏移值)。我再次使用标签来解释这个机制——还是用刚才的 `X` 结构体,还是同样的两个字段: + +```go +type X struct { + n1 int16 + n2 int16 +} +``` + +现在我们已经知道他在内存里面是怎样布局的了,这一次我们来看看每个字段各占多少个字节,内存分配的情况如下图: + +![Offsetof memory usage](https://raw.githubusercontent.com/studygolang/gctt-images2/master/is-unsafe/3.jpg) + +不难猜到,内存的布局是这样的:第一个字段 `X.n1` 占了前 2 个字节,而第二个字段 `X.n2` 占了接下来的 2 个字节。所以下面两个语句都是成立的: + +```go +unsafe.Offsetof(X.n1) == 0 // true +unsafe.Offsetof(X.n2) == 2 // true +``` + +### `func Alignof(x ArbitraryType) uintptr` + +这个函数是最好玩的一个,因为要透彻了解这个函数,你需要了解 [alignment(数据结构对齐)](https://zh.wikipedia.org/wiki/数据结构对齐) 是怎么回事。简单来说,它让数据在内存中以某种的布局来存放,使该数据的读取能够更加的快速。这个接收一个变量作为参数,并返回这个变量的对齐字节。为了更加直观,我们需要修改一下上面的例子: + +```go +type X struct { + n1 int8 + n2 int16 +} +``` + +可以看到现在 `n1` 的类型变成了 `int8`,这会有什么变化吗,我们先看看 `Sizeof`, 因为 `n1` 只占 1 个字节了,所以合理地推测,`X` 结构体的大小会变成 3,因为:size(`X`) = size(`n1`) + size(`n2`) = 1 + 2 = 3。**但是**现实真的如此吗 ? + +…… + +不是的,因为 alignment 的缘故,`X` 结构体在内存的结构如下: + +![Alignof memory usage](https://raw.githubusercontent.com/studygolang/gctt-images2/master/is-unsafe/4.jpg) + +由于 alignment 机制的要求,`n2` 的**内存起始地址应该是自身大小的整数倍**,也就是说它的起始地址只能是 0、2、4、6、8 等偶数,所以 `n2` 的起始地址没有紧接着 `n1` 后面,而是空出了 1 个字节。最后导致结构体 `X` 的大小是 4 而不是 3。机智的读者可能会想到:`n1` 和 `n2` 换个位置会怎样呢?这样一来,`n2` 的起始地址是 0,而 `n1` 的其实地址是 2,这么一来结构体 `X` 的大小就变成 3 了吧?答案是……不对的。原因还是因为 alignment,因为 alignment 除了要求字段的其实地址应该是自身大小的整数倍,还要求**整个结构体的大小,是结构体中最大的字段的大小的整数倍**,这使得结构体可以由多个内存块组成,其中每个内存块的大小都等于最大的字段的大小。我们可以利用这个知识来减少结构体的内存占用。考察以下代码: + +```go +type First struct { + a int8 + b int64 + c int8 +} + +type Second struct { + a int8 + c int8 + b int64 +} + +fmt.Println("Big brain time: ", unsafe.Sizeof(First{}) == unsafe.Sizeof(Second{})) +``` + +上面两个结构体大小不同,是因为 `First` 结构体由三个大小为 8 字节的内存块组成:`Sizeof(First.a) + 7 个空闲的字节 + Sizeof(First.b) + Sizeof(First.c) + 7 个空闲的字节 = 24 字节`。而 `Second` 结构体只包含 2 个 大小为 8 字节的内存块:`Sizeof(Second.a) + Sizeof(Second.b) + 6 个空闲的字节 + Sizeof(Second.b) = 16 字节`。下次你定义结构体的时候可以用上这个小知识🙂。 + +下面的代码片段总结了上述三个函数的用法: + +```go +var x struct { + a int64 + b bool + c string +} + +fmt.Println("Size of x: ", unsafe.Sizeof(x)) +fmt.Println("Size of x.c: ", unsafe.Sizeof(x.c)) + +fmt.Println("Alignment of x.a: ", unsafe.Alignof(x.a)) +fmt.Println("Alignment of x.b: ", unsafe.Alignof(x.b)) +fmt.Println("Alignment of x.c: ", unsafe.Alignof(x.c)) + +fmt.Println("\nOffset of x.a: ", unsafe.Offsetof(x.a)) +fmt.Println("Offset of x.b: ", unsafe.Offsetof(x.b)) +fmt.Println("Offset of x.c: ", unsafe.Offsetof(x.c)) +``` + +上述的三个方法都是在[编译期](https://en.wikipedia.org/wiki/Compile_time)执行的,这意味着只要它们在编译器没有报错,在运行时不会有问题发生。但是我们的下一位嘉宾 `unsafe.Pointer` 可就没那么好惹了,它有可能会发生[运行时的错误](https://en.wikipedia.org/wiki/Runtime_(program_lifecycle_phase))。我将会在本文的第二部分详细介绍 `unsafe.Pointer` 以及使用它的过程中容易出现的问题。 + +--- + +via: https://www.dnahurnyi.com/is-unsafe-...unsafe-pt.-1/ + +作者:[Denys Nahurnyi](https://www.dnahurnyi.com/) +译者:[Alex-liutao](https://github.com/Aelx-liutao) +校对:[@unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200203-is-unsafe-unsafe-part2.md b/published/tech/20200203-is-unsafe-unsafe-part2.md new file mode 100644 index 000000000..64dcc64ce --- /dev/null +++ b/published/tech/20200203-is-unsafe-unsafe-part2.md @@ -0,0 +1,157 @@ +首发于:https://studygolang.com/articles/35043 + +# unsafe 真就不安全吗?- part2 + +在[上篇文章](https://studygolang.com/articles/28433)中,我已经谈到了 unsafe 包的初衷和功能。但还有一件事情没有解释。 + +## type pointer + +此类型表示指向任意类型的指针,这意味着,unsafe.Pointer 可以转换为任何类型或 uintptr 的指针值。你可能会想: 有什么限制吗?没有,是的... 你可以转换 Pointer 为任何你想要的,但你必须处理可能的后果。为了减少可能出现的问题,你可以使用某些模式: + +> “*以下涉及 Pointer 的模式是有效的。不使用这些模式的代码今天可能无效,或者将来可能无效。即使是下面这些有效的模式,也带有重要的警告。*” —— golang.org + +你也可以使用 go vet,但是它不能解决所有的问题。因此,我建议你遵循这些模式,因为这是减少错误的唯一方法。 + +## 快速拷贝 + +如果两种类型的内存布局相同,为了避免内存分配,你可以通过以下机制将类型 `*T1` 的指针转换为类型 `*T2` 的指针,将类型 T1 的值复制到类型 T2 的变量中: + +```go +ptrT1 := &T1{} +ptrT2 = (*T2)(unsafe.Pointer(ptrT1)) +``` + +但是要小心,这种转换是有代价的,现在两个指针指向同一个内存地址,所以每个指针的改变也会反应到另一个指针上。可以通过[这里验证](https://play.studygolang.com/p/bZGEHrHp4LM)。 + +## unsafe.Pointer != uintptr + +我已经提到过,指针可以转换为 uintptr 并转回来,但是转回来是有一些特殊的条件限制的。unsafe.Pointer 是一个真正的指针,它不仅保持内存地址,包括动态链接的地址,但 uintptr 只是一个数字,因此它更小,但有代价。如果你转换 unsafe.Pointer 为 uintptr 后,指针不再引用指向的变量,而且在将 uintptr 转换回 unsafe.Pointer 变量之前,垃圾收集器可以轻松地回收该内存。至少有两种解决方案可以避免此问题。第一个更复杂的,但也真正显示了,为了使用 unsafe 包,你必须牺牲什么。有一个特殊的函数,runtime.KeepAlive 可以避免 GC 不恰当的回收。它听起来很复杂,而且使用起来更加复杂。这里为你准备了[实际例子](https://play.studygolang.com/p/L7rgheqNo9w)。 + +## 指针算法 + +还有另一种方法避免 GC 不恰当回收。即在同一个语句中做以下事情:将 unsafe.Poniter 转为 uintptr,以及将 uintptr 做其他运算,最后转回 unsafe.Pointer 。因为 uintptr 只是一个数字,我们可以做所有特殊的算术运算,比如加法或减法。我们如何使用它?指针算法通过了解内存布局和算术运算,可以得到任何需要的数据。让我们来看看下一个例子: + +```go +x := [4]byte{10, 11, 12, 13} +elPtr := unsafe.Pointer(uintptr(unsafe.Pointer(&x[0])) + 3*unsafe.Sizeof(x[0])) +``` + +有了指向字节数组第一个元素的指针,我们就可以在不使用索引的情况下获得最后一个元素。如果将指针移动三个字节,我们就可以得到最后一个元素。让我可视化展示: + +![Pointer arithmetic](https://www.dnahurnyi.com/img/Pointer-arithmetic.png) + +因此,在一个表达式中执行所有转换可以省去 GC 清理的麻烦。上述三种模式说明了如何在不同情况下正确地转换 unsafe.Pointer 为其他数据类型的指针。 + +## Syscalls + +在包 syscall 中,有一个函数 syscall.Syscall 接收 uintptr 格式的指针的系统调用,我们可以通过 unsafe.Pointer 得到 uintptr。重要的是,你必须进行正确的转换: + +```go +a := &A{1} +b := &A{2} +syscall.Syscall(0, uintptr(unsafe.Pointer(a)), uintptr(unsafe.Pointer(b))) // Right + +aPtr := uintptr(unsafe.Pointer(a) +bPtr := uintptr(unsafe.Pointer(b) +syscall.Syscall(0, aPtr, bPtr) // Wrong +``` + +## reflect.Value.Pointer 和 reflect.Value.UnsafeAddr + +reflect 包中有两个方法: Pointer 和 UnsafeAddr,它们返回 uintptr,因此我们应该立即将结果转换为 unsafe.Pointer,因为我们需要时刻“提防”我们的 GC 朋友: + +```go +p1 := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer())) // Right + +ptr := reflect.ValueOf(new(int)).Pointer() // Wrong +p2 := (*int)(unsafe.Pointer(ptr) // Wrong +``` + +## reflect.SliceHeader 和 reflect.StringHeader + +reflect 包中有两种类型: SliceHeader 和 StringHeader,它们都具有字段 Data uintptr。正如你所记得的那样,uintptr 通常与 unsafe.Pointer 联系在一起,见下面代码: + +```go +var s string +hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) +hdr.Data = uintptr(unsafe.Pointer(p)) +hdr.Len = n +``` + +--- + +以上就是所有可能关于 unsafe.Pointer 使用的模式,所有不遵循这些模式或从这些模式派生的情况很可能是无效的。但是 unsafe 包不仅在代码中而且在代码之外都会带来问题。让我们回顾一下其中的几个。 + +## 兼容性 + +Go 有[兼容性指南](https://docs.studygolang.com/doc/go1compat),保证版本更新的兼容性。简单地说,它保证你的代码在升级后仍然可以工作,但是不能保证你已经导入了 unsafe 的包。unsafe 包的使用可能会破坏你的代码的每个版本: major,minor,甚至安全修补程序。所以在导入之前,试着想一下这样一种情况:你的客户问你为什么我们不能通过升级 Go 版本来消除漏洞,或者为什么在更新之后什么都不能工作了。 + +## 不同的行为 + +你知道所有的 Go 数据类型吗?你听说过 int 吗?如果我们已经有 int32 和 int64,为什么还有 int?实际上 int 类型是根据计算机体系结构(x32 或 x64)将其转换为 int32 或 int64 类型。所以请记住,unsafe 的函数结果和内存布局在不同的架构上可能是不同的,例如: + +```go +var s string +unsafe.Sizeof(s) // x32 上是 8,而 x64 上是 16 +``` + +## 社区的情况 + +我想知道:如果这个包如此危险,有多少冒险者在使用它。我已经在 [GitHub](https://github.com/search?l=Go&q=unsafe&type=Repositories) 上搜索过了。与 [crypto](https://github.com/search?l=Go&q=crypto&type=Repositories) 或 [math](https://github.com/search?l=Go&q=math&type=Repositories) 相比,数量并不多。其中超过一半的内容是关于使用 unsafe 的方法的技巧和可能的偏差,而不是一些真正的用法。 + +Rust 社区有一个事件:一个叫 Nikolay Kim 的,他是 [activex](https://github.com/actix) 项目的创始人,在社区的巨大压力下,将 activex 库变成了私有。后来再公开该仓库时,将其中一个贡献者提升为所有者,然后[离开](https://github.com/actix/actix-web/issues/1289)。所有这一切的发生都是因为一些人认为使用了 unsafe 包,这太危险不应该使用。我知道 Go 社区目前没有这种情况,而且 Go 社区里也没有唯一正确的观点。我想要提醒的是,如果你在代码中导入了 unsafe 的代码,请做好准备,社区可能会。。。 + +## 爱好者 + +有很多人和很多想法,[这篇文章](https://nullprogram.com/blog/2019/06/30/)展示了使用 int 和使用指针操作的新方法,简而言之,它看起来像这样: + +```go +var foo int +fooslice = (*[1]int)(unsafe.Pointer(&foo))[:] +``` + +对此,我不发表意见,我只会提到,你应该注意导入 unsafe 可能的问题。 + +## 最后 + +我个人试着去思考 unsafe 带来问题的可能性,这里有一个使用 unsafe 的例子。假设你导入了一些执行某些有用操作的第三方包,比如将 DB 客户端对象和日志记录器包装到一个实体中,以使所有操作的日志记录更加容易,或者像我的例子中那样,导入一些返回对象的动物的函数... + +```go +package main + +import ( + "fmt" + "third-party/safelib" +) + +func main() { + a := safelib.NewA("https://google.com", "1234") // Url and password + fmt.Println("My spiritual animal is: ", safelib.DoSomeHipsterMagic(a)) + a.Show() +} +``` + +在这个函数中,我们将 interface{} 断言为一些已知类型,并快速复制到一些 `Malicious` 类型,这些 `Malicious` 类型具有获取和设置私有字段的方法,如 url 和密码。所以这个包可以提取出所有有趣的数据,甚至替换 url,这样下次你尝试连接到 DB 时,有人会获得你的凭证。 + +```go +func DoSomeHipsterMagic(any interface{}) string { + if a, ok := any.(*A); ok { + mal := (*Malicious)(unsafe.Pointer(a)) + mal.setURL("http://hacker.com") + } + + return "Cool green dragon, arrh 🐉" +} +``` + +最后的最后,切记所有的技术都有一定的代价,但是 unsafe 技术尤其“昂贵”,所以我的建议是在使用它之前要三思。 + +--- + +via: https://www.dnahurnyi.com/is-unsafe-...unsafe-pt.-2/ + +作者:[Denys Nahurnyi](https://www.dnahurnyi.com/) +译者:[polaris1119](https://github.com/polaris1119) +校对:[unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 \ No newline at end of file diff --git a/published/tech/20200205-Go-Image-Rendering-in-2D-Video-Games-with-Ebiten.md b/published/tech/20200205-Go-Image-Rendering-in-2D-Video-Games-with-Ebiten.md new file mode 100644 index 000000000..f3e5d19a4 --- /dev/null +++ b/published/tech/20200205-Go-Image-Rendering-in-2D-Video-Games-with-Ebiten.md @@ -0,0 +1,132 @@ +首发于:https://studygolang.com/articles/34003 + +# Go:使用 Ebiten 在 2D 视频游戏中进行图像渲染 + +![Ebiten](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200205-Go-Image-Rendering-in-2D-Video-Games-with-Ebiten/illustration.png) +插图由创作原始 Go Gopher 作品的 Renee French 为“ Go 的旅程”创作。 + +*本文基于 Ebiten 1.10。* + +[Ebiten](https://ebiten.org/) 是由 [Hajime Hosh](https://github.com/hajimehoshi) 用 Go 语言编写的成熟的 2D 游戏库。它是 Apple Store 上一些手机游戏如 [Bear's Restaurant](https://daigostudio.com/bearsrestaurant/en/) 或桌面游戏如 [OpenDiablo2](https://github.com/OpenDiablo2) 的引擎,OpenDiablo2 是 Go 版本暗黑 2 的开源实现。现在,让我们深入了解电子游戏中的一些基本概念以及它们在 Ebiten 中的实现。 + +## 动画制作 +在电子游戏世界中,Ebiten 通过分离的静态图像来渲染动画。这些图像集合被组合成一个更大的图像,通常称为“[纹理图集](https://en.wikipedia.org/wiki/Texture_atlas)”,也称为“精灵图”。这是 [网站](https://ebiten.org/examples/animation.html) 上提供的示例: + +![image1](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200205-Go-Image-Rendering-in-2D-Video-Games-with-Ebiten/image1.png) + +然后,经过简单的数学运算,这张图被加载到内存中,然后被一部分一部分地渲染。在前面的示例中,每个部分的宽度为 32 个像素。渲染每个图像非常简单,只需将 X 坐标移动 32 个像素。这是第一步: + +![image2](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200205-Go-Image-Rendering-in-2D-Video-Games-with-Ebiten/image2.png) + +![image3](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200205-Go-Image-Rendering-in-2D-Video-Games-with-Ebiten/image3.png) + +渲染动画时,只需要当前图像的值即可渲染精灵图的正确部分。Ebiten 提供了所有的 API 来轻松地渲染它,下面是之前第一步中精灵图的示例: + +```go +screen.DrawImage( + // 在坐标上绘制子画面的子图像 + // x=0 to x=32 and y=32 to y=64 + runnerImg.SubImage(image.Rect(0, 32, 32, 64)).(*ebiten.Image), + // 在该位置之前声明的变量定义了屏幕上的位置 + op, +) +``` + +Ebiten 的创建者 Hajime Hosh 还开发了“ [file2byteslice](https://github.com/hajimehoshi/file2byteslice)”,该工具可将任何图像转换为字符串,并允许将任何文件嵌入到 Go 中。它可以通过注释 `go:generate` 与 Go 工具轻松集成,自动将图像转储到 Go 文件中。下面是一个例子: + +```go +//go:generate file2byteslice + -package=img + -input=./images/sprite.png + -output=./images/sprite.go -var=Runner_png +``` + +现在可以从以下代码的 `img.Runner_png` 变量中获取精灵图: + +![image4](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200205-Go-Image-Rendering-in-2D-Video-Games-with-Ebiten/image4.png) + +然后,必须先对图像进行解码,然后再由 Ebiten 加载并在游戏中进行渲染。让我们转到另一种渲染较大背景图像的方法,该图像由循环元素组成。 + +## 瓷砖背景 + +渲染背景使用相同的技术,将主要地图集分为许多小图像,称为“瓷砖”。这是网站上提供的纹理图集的示例: + +![tiles1](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200205-Go-Image-Rendering-in-2D-Video-Games-with-Ebiten/tiles1.png) + +该图集可以被 16 像素的图块分割 ,这是使用 [Tiled](https://www.mapeditor.org/) 软件创建的图块集合: + +![tiles2](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200205-Go-Image-Rendering-in-2D-Video-Games-with-Ebiten/tiles2.png) + +每个图块将分配一个数字,从 0 开始,逐次加 1。由于每行都有相同数量的图块,因此可以通过除法和取模来获取图块的坐标。这是蓝色花朵的示例: + +![tiles3](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200205-Go-Image-Rendering-in-2D-Video-Games-with-Ebiten/tiles3.png) + +蓝色花朵的编号为 303,这意味着它们位于第 4 列(303 以每行的图块数为模,例如 303%25 = 3)和第 13 行(303 除以每行的图块数,例如,303/25 = 12)。 + +现在,我们可以使用索引数组来构建地图: + +![map_array](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200205-Go-Image-Rendering-in-2D-Video-Games-with-Ebiten/map_array.png) + +从这张地图上绘制图像可以得到主要装饰: + +![bg1](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200205-Go-Image-Rendering-in-2D-Video-Games-with-Ebiten/bg1.png) + +但是缺少背景。我们必须构建一个表示背景的类似数组。结果如下: + +![bg2](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200205-Go-Image-Rendering-in-2D-Video-Games-with-Ebiten/bg2.png) + +现在,这些图层已经准备好了,我们只需要迭代两个图层就可以得到最终结果: + +![bg3](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200205-Go-Image-Rendering-in-2D-Video-Games-with-Ebiten/bg3.png) + +此示例中的代码可在 [Ebiten 网站](https://ebiten.org/examples/tiles.html) 上找到。 + +生成图像后,Ebiten 必须管理屏幕更新并将指令发送到图形卡片。 + +## 屏幕更新 + +Ebiten 提供了 iOS(使用 Metal)和 Android(使用 OpenGL ES)使用的驱动程序的抽象,这使开发更加容易。它还允许您定义一个函数,该函数可以更新屏幕并绘制所有更改。但是,出于性能原因,该库会将源打包在一起,并将这些更改存储在缓冲区中,然后再发送给驱动程序: + +![screen_update1](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200205-Go-Image-Rendering-in-2D-Video-Games-with-Ebiten/screen_update1.png) + +该缓冲区还能够合并绘图指令,以减少对 GPU 的调用次数。在上图中,这三个指令现在可以合并为一个: + +![screen_update2](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200205-Go-Image-Rendering-in-2D-Video-Games-with-Ebiten/screen_update2.png) + +这项改进很重要,因为它可以减少发送指令时的开销。这是合并指令的性能: + +![perf1](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200205-Go-Image-Rendering-in-2D-Video-Games-with-Ebiten/perf1.png) + +通过逐个发送指令,性能会大大降低: + +![perf2](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200205-Go-Image-Rendering-in-2D-Video-Games-with-Ebiten/perf2.png) + +Ebiten 还提供了对屏幕刷新的控制,这有助于调整性能。 + +## TPS 管理 + +Ebiten 的默认 TPS 是 60。但是,这可以用 Ebiten 提供的 API `ebiten.SetMaxTPS()` 来轻松配置。它有助于减少机器上的压力。这是具有 25 TPS 的简单程序: + +![tps1](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200205-Go-Image-Rendering-in-2D-Video-Games-with-Ebiten/tps1.png) + +TPS(每秒 tick 数)与 FPS(每秒帧数)不同。[Hajime Hosh](https://github.com/hajimehoshi) 很好地描述了这些差异: + +> 帧代表图形更新。这取决于用户显示屏上的刷新率。那么 FPS 可能是 60、70、120,依此类推。这个数字基本上是不可控制的。Ebiten 可以打开或关闭 vsync。如果关闭了 vsync,则 Ebiten 会尝试尽可能多地更新图形,那么 FPS 可以为 1000 左右。 +> tick 表示逻辑更新。TPS 表示每秒调用更新功能的次数。默认情况下固定为 60。游戏开发人员可以通过 SetMaxTPS 配置 TPS。如果设置了 UncappedTPS,则 Ebiten 会尝试尽可能多地调用更新函数。 + +当窗口在后台运行时,Ebiten 为渲染带来了另一种优化。失去焦点时,游戏将进入休眠状态,直到重新获得焦点。它实际上 sleep 了 1/60 秒,然后再次检查焦点。这是保存的资源的示例: + +![tps2](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200205-Go-Image-Rendering-in-2D-Video-Games-with-Ebiten/tps2.png) + +尽管可以关闭此优化,但是运行的系统可能会限制游戏的运行。例如,在 [Firefox](https://hacks.mozilla.org/2018/01/firefox-58-the-quantum-era-continues/),[Chrome](https://developers.google.com/web/updates/2017/03/background_tabs) 或其他浏览器上的后台标签中运行时,在浏览器中运行的游戏会受到限制。 + +Ebiten 在 [Github](https://github.com/sponsors/hajimehoshi) 上接受赞助。如果您希望看到更多 Go 编写的游戏,请随时贡献力量。 + +--- +via: https://medium.com/a-journey-with-go/go-image-rendering-in-2d-video-games-with-ebiten-912cc2360c4f + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[alandtsang](https://github.com/alandtsang) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200301-Go-What-Does-a-Goroutine-Switch-Actually-Involve.md b/published/tech/20200301-Go-What-Does-a-Goroutine-Switch-Actually-Involve.md new file mode 100644 index 000000000..e5807a988 --- /dev/null +++ b/published/tech/20200301-Go-What-Does-a-Goroutine-Switch-Actually-Involve.md @@ -0,0 +1,113 @@ +首发于:https://studygolang.com/articles/30251 + +# Go:Goroutine 的切换过程实际上涉及了什么 + +![Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-what-does-a-goroutine/1.png) + +本文基于 Go 1.13 版本。 + +Goroutine 很轻,它只需要 2Kb 的内存堆栈即可运行。另外,它们运行起来也很廉价,将一个 Goroutine 切换到另一个的过程不牵涉到很多的操作。在深入 Goroutine 切换过程之前,让我们回顾一下 Goroutine 的切换在更高的层次上是如何进行的。 + +在继续阅读本文之前,我强烈建议您阅读我的文章 [Go:Goroutine、操作系统线程和 CPU 管理](https://medium.com/a-journey-with-go/go-goroutine-os-thread-and-cpu-management-2f5a5eaf518a) 以了解本文中涉及的一些概念。 + +## 案例 + +Go 根据两种断点将 Goroutine 调度到线程上: + +* 当 Goroutine 因为系统调用、互斥锁或通道而被阻塞时,goroutine 将进入睡眠模式(等待队列),并允许 Go 调度运行另一个处于就绪状态的 goroutine; +* 在函数调用时,如果 Goroutine 必须增加其堆栈,这会使 Go 调度另一个 Goroutine 以避免运行中的 Goroutine 独占 CPU 时间片; + +在这两种情况下,运行调度程序的 `g0` 会替换当前的 goroutine,然后选出下一个将要运行的 Goroutine 替换 `g0` 并在线程上运行。 + +有关 `g0` 的更多信息,建议您阅读我的文章 [Go:特殊的 Goroutine g0](https://medium.com/a-journey-with-go/go-g0-special-goroutine-8c778c6704d8)。 + +将一个运行中的 Goroutine 切换到另一个的过程涉及到两个切换: + +* 将运行中的 `g` 切换到 `g0`: + + ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-what-does-a-goroutine/2.png) + +* 将 `g0` 切换到下一个将要运行的 `g`: + + ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-what-does-a-goroutine/3.png) + +在 Go 中,goroutine 的切换相当轻便,其中需要保存的状态仅仅涉及以下两个: + +* Goroutine 在停止运行前执行的指令,程序当前要运行的指令是记录在程序计数器(`PC`)中的, Goroutine 稍后将在同一指令处恢复运行; + +* Goroutine 的堆栈,以便在再次运行时还原局部变量; + +让我们看看实际情况下的切换是怎样进行的。 + +## 程序计数器 + +这里通过基于通道的 ` 生产者/消费者模式 ` 来举例说明,其中一个 Goroutine 产生数据,而另一些则消费数据,代码如下: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-what-does-a-goroutine/4.png) + +消费者仅仅是打印从 0 到 99 的偶数。我们将注意力放在第一个 goroutine(生产者)上,它将数字添加到缓冲区。当缓冲区已满时,它将在发送消息时被阻塞。此时,Go 必须切换到 `g0` 并调度另一个 Goroutine 来运行。 + +如前所述,Go 首先需要保存当前执行的指令,以便稍后在同一条指令上恢复 goroutine。程序计数器(`PC`)保存在 Goroutine 的内部结构中: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-what-does-a-goroutine/5.png) + +可以通过 `go tool objdump` 命令找到对应的指令及其地址,这是生产者的指令: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-what-does-a-goroutine/6.png) + +程序逐条指令的执行直到在函数 `runtime.chansend1` 处阻塞在通道上。 Go 将当前程序计数器保存到当前 Goroutine 的内部属性中。在我们的示例中,Go 使用运行时的内部地址 `0x4268d0` 和方法 `runtime.chansend1` 保存程序计数器: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-what-does-a-goroutine/7.png) + +然后,当 `g0` 唤醒 Goroutine 时,它将在同一指令处继续执行,继续将数值循环的推入通道。现在,让我们将视线移到 Goroutine 切换期间堆栈的管理。 + +## 堆栈 + +在被阻塞之前,正在运行的 Goroutine 具有其原始堆栈,该堆栈包含临时存储器,例如变量 `i`: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-what-does-a-goroutine/8.png) + +然后,当它在通道上阻塞时,goroutine 将切换到 `g0` 及其堆栈(更大的堆栈): + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-what-does-a-goroutine/9.png) + +在切换之前,堆栈将被保存,以便在 Goroutine 再次运行时进行恢复: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-what-does-a-goroutine/10.png) + +现在,我们对 Goroutine 切换中涉及的不同操作有了一个完整的了解,让我们继续看看它是如何影响性能的。 + +我们应该注意,诸如 `arm` 等 CPU 架构需要再保存一个寄存器,即 `LR` 链接寄存器。 + +## 性能 + +我们仍然使用上述的程序来测量一次切换所需的时间。但是,由于切换时间取决于寻找下一个要调度的 Goroutine 所花费的时间,因此无法提供完美的性能视图。在函数调用情况下进行的切换要比阻塞在通道上的切换执行更多的操作,这也会影响到性能。 + +让我们总结一下我们将要测量的操作: + +* 当前 `g` 阻塞在通道上并切换到 `g0`: + * `PC` 和堆栈指针一起保存在内部结构中 + * 将 `g0` 设置为正在运行的 goroutine + * `g0` 的堆栈替换当前堆栈 +* `g0` 寻找新的 Goroutine 来运行; +* `g0` 使用所选的 Goroutine 进行切换: + * `PC` 和堆栈指针是从其内部结构中获取的 + * 程序跳转到对应的 `PC` 地址 + +结果如下: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-what-does-a-goroutine/11.png) + +从 `g` 到 `g0` 或从 `g0` 到 `g` 的切换是相当迅速的,它们只包含少量固定的指令。相反,对于调度阶段,调度程序需要检查许多资源以便确定下一个要运行的 goroutine,根据程序的不同,此阶段可能会花费更多的时间。 + +该基准测试给出了性能的数量级估计,由于没有标准的工具可以衡量它,所以我们并不能完全依赖于这个结果。此外,性能也取决于 CPU 架构、机器(本文使用的机器是 Mac 2.9 GHz 双核 Intel Core i5)以及正在运行的程序。 + +--- + +via: + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[anxk](https://github.com/anxk) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200303-Optimizing-a-Golang-service-to-reduce-over-40%-CPU.md b/published/tech/20200303-Optimizing-a-Golang-service-to-reduce-over-40%-CPU.md new file mode 100644 index 000000000..900bdd806 --- /dev/null +++ b/published/tech/20200303-Optimizing-a-Golang-service-to-reduce-over-40%-CPU.md @@ -0,0 +1,139 @@ +首发于:https://studygolang.com/articles/27429 + +# 优化 Golang 服务来减少 40% 以上的 CPU + +十年前,谷歌正在面临一个由 C++ 编译时间过长所造成的严重瓶颈,并且需要一个全新的方式来解决这个问题。谷歌的工程师们通过创造了一种新的被称作 Go (又名 Golang)的语言来应对挑战。这个新语言 Go 带来了 C++ 最好的部分(最主要的是它的性能和稳定性),又与 Python 的速度相结合,使得 Go 能够在实现并发的同时快速地使用多核心。 + +在 Coralogix(译者注:一个提供全面日志分析的服务产品,[官网](https://coralogix.com/)),我们为了去给我们的客户提供关于他们日志实时的分析、警报和元数据,要去解析他们的日志。在解析阶段,我们需要非常快速地解析包含多个复杂规则的服务日志,这个目标是促使我们决定使用 Golang 的原因之一。 + +这项新的服务现在就全天候的跑在生产阶段,尽管我们看到了非常好的结果,但是它也需要跑在高性能的机器上。这项 Go 的服务跑在一台 AWS m4.2xlarge 实例上 ,带有 8 CPUs 和 36 GB 的配置,每天要解析几十亿的日志。 + +在这个阶段一切都运行正常,我们本可以自我感觉良好,但是那并不是我们在 Coralogix 想要的表现。我们想要更多的特性,比如性能等等,或者使用更少的 AWS 实例。为了改进,我们首先需要理解瓶颈的本质以及我们如何能够减少或者完全解决这些问题。 + +我们决定在我们的服务上进行一些分析,检查一下到底是什么造成了 CPU 的高消耗,看看我们是否能够优化。 + +首先,我们将 Go 升级到最新的稳定版本(这是软件生命周期中的关键一步)。我们是用的 Go 1.12.4 版本,最新的是 1.13.8。根据 [文档](https://golang.org/doc/devel/release.html) ,Go 1.13 发行版在运行时库方面和一些其他主要利用内存使用的组件方面已经有了长足的进步。总之,使用最新的稳定版本能帮助我们节省许多工作。 + +因此,内存消耗**由大约 800 MB 降低到了仅 180 MB**。 + +第二,为了更好的理解我们的流程以及弄清楚我们应该在哪花费时间和资源,我们开始去进行分析。 + +分析不同的服务和程序语言可能看起来很复杂并且令人望而生畏,但是对于 Go 来说它实际上十分容易,仅仅几个命令就能够描述清楚。Go 有一个专门的工具叫“pprof”,它通过监听一个路由(默认端口 6060)能够应用在你的 app 上,并且使用 Go 的包来管理 HTTP 连接: + +```go +import _ "net/http/pprof" +``` + +接着在你的 main 函数中或者路由包下按照如下操作初始化: + +```go +go func() { + log.Println(http.ListenAndServe("localhost:6060", nil)) +}() +``` + +现在你可以启动你的服务并且连接到: + +``` +http://localhost:6060/debug/pprof +``` + +Go 官方提供的完整文档可以 [在这](https://golang.org/pkg/net/http/pprof) 找到。 + +pprof 的默认配置是每 30 秒对 CPU 的使用情况进行采样。有许多不同的选择,也可以对 CPU 的使用,堆的使用或者其他更多的使用情况进行采样。 + +我们主要关注 CPU 使用,因此在生产阶段采取了一个 30 秒的性能分析,并且发现了你在下图所看到的情况(提醒一下:这是在我们把 Go 版本升级并且将 Go 的内部组件降到最低之后的结果): + +![Go profiling — Coralogix](https://raw.githubusercontent.com/studygolang/gctt-images/master/optimizing-cpu/0.png) + +正如你所看到的,我们发现了许多运行时库的活动:GC 几乎使用了 **29% 的 CPU**(还仅仅只是消耗最多的前 20 个对象)。因为 Go 的 GC 非常快并且做了巨大的优化,最好的实践就是不要去改变或者修改它。因为我们的内存消耗非常低(与我们先前的 Go 版本相比),所以主要的怀疑对象就变成了较高的对象分配率。 + +如果是那种情况的话,我们就能做两件事情了: + +- 调整 Go GC 活动,使其适合我们的服务行为,意味着 —— 延缓它的触发以使 GC 变的不那么频繁。**这将使我们不得不补偿更多的内存使用。** +- 找出我们代码中那些分配了太多对象的函数、区段或者行。 + +观察一下我们的实例类型,很明显我们有大量的内存可供使用,并且我们正在被机器的 CPU 数量所限制。因此我们仅仅需要调整一下比率。因为在 Golang 的早期有一个大多数开发者都不关注的数据,叫 GOGC。这个数值默认是 100,简单地告诉你的系统什么时候触发 GC。这个默认值使得堆的大小在到达它初始态的两倍时触发 GC。将这个数值改成一个更大的数将会延缓 GC 的触发,降低它的频率。我们基准测试了许多不同的数,最终对于我们的目标来说最好的性能是在使用 GOGC = 2000 的时候。 + +这立刻**增加了我们的内存使用,从大约 200 MB 到 大约 2.7 GB**(那还是由于我们的 Go 版本更新,在内存消耗降低的情况下),另外也**减少了我们 CPU 大约 10% 的使用。** + +这个接下来的截图就展示了这些基准测试的结果: + +![GOGC =2000 results — Coralogix benchmark](https://raw.githubusercontent.com/studygolang/gctt-images/master/optimizing-cpu/01.png) + +前面的四个 CPU 的消耗函数就是我们的服务函数,这十分有意义。全部的 GC 使用现在**大约是 13%,是先前消耗的一半还少!** + +我们其实可以在这就停下来了,但是我们还是决定去揭露我们在哪并且为什么会分配这么多对象。很多时候,这么做有充分理由(比如在流式处理的情况下,我们为每条获取的消息创建了许多新的对象,并且因为它与下一条消息无关,需要去移除它),但是在某些情况下有一种简单的方法可以去优化并且动态地减少对象的创建。 + +首先,让我们运行一个和之前同样的命令,有一点小的改变,采用堆调试: + +``` +http://localhost:6060/debug/pprof/heap +``` + +为了查询结果文件,你可以运行如下命令在你的代码目录下来分析调试结果: + +```bash +go tool pprof -alloc_objects +``` + +我们的截图看起来像这样: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/optimizing-cpu/02.png) + +除了第三行一切似乎都很合理,这是一个监控函数,在每个 Carologix 规则解析阶段的末尾向我们的 Promethes 调用者展示结果。为了获取进一步信息,我们运行如下命令: + +```bash +list +``` + +例如: + +``` +list reportRuleExecution +``` + +然后我们会获得如下结果: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/optimizing-cpu/3.png) + +WithLabelValues 的两个调用都是为了软件度量的 Prometheus 函数(我们将这个留给产品去决定是否真正需要)。而且,我们可以看到第一行创建了大量的对象(由这个函数所创建的全部对象的 10%)。我们进一步查看发现它是一个对于绑定到导出数据的消费者 ID 从 int 到 string 的转换,十分重要,但是考虑到实际情况,我们数据库中消费者的数量十分有限,我们不应该采用 Prometheus 的方式来接收变量作为 string 类型。因此取代了每次创建一个新的 string 并且在函数末尾都抛弃的这种方法(浪费分配还有 GC 的多余工作),我们在对象的分配阶段定义了 map,配对了所有从 1 到 10 万的数字和一个需要执行的 “get” 方法。 + +现在运行一个新的性能分析会话来验证我们的论点并且它的对的(你可以看到这一部分并不会再分配对象了): +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/optimizing-cpu/4.png) + +这并不是一个显著的改进,但是总体来说为我们节省了另一个 GC 的活动,说的更具体一点就是节省了大约 1% 的 CPU。 + +最终的状态就是下面的截图: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/optimizing-cpu/5.png) + +## 最终结果 + +**1) 内存使用:大约 1.3 GB -> 大约 2.7 GB** + +**2) CPU 使用:大约 2.55 avg 和 大约 5.05 峰值期 -> 大约 2.13 avg 和 大约 2.9 峰值期。** + +在我们 Golang 优化前的 CPU: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/optimizing-cpu/6.png) + +在我们 Golang 优化后的 CPU: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/optimizing-cpu/7.png) + +总体来说,我们可以看到主要的改进是在每秒日志处理量增加时的高峰时间。这就意味着我们的基础架构不仅不需要再为了异常值进行调整,而且变得更加稳定了。 + +## 总结 + +通过对我们的 Go 解析服务进行性能测试,我们能够查明有问题的地方,更好的理解我们的服务并且确定在哪里(如果有的话)投资时间进行改进。大多数性能分析工作都会以一些基础数值或配置的调整,更合适你的使用情况并且最终展现更好的性能而结束。 + +--- + +via:https://medium.com/coralogix-engineering/optimizing-a-golang-service-to-reduce-over-40-cpu-366b67c67ef9 + +作者:[Eliezer Yaacov](https://medium.com/@eliezerj8) +译者:[sh1luo](https://github.com/sh1luo) +校对:[@unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200304-Go-Memory-Safety-with-Bounds-Check.md b/published/tech/20200304-Go-Memory-Safety-with-Bounds-Check.md new file mode 100644 index 000000000..112b35ac3 --- /dev/null +++ b/published/tech/20200304-Go-Memory-Safety-with-Bounds-Check.md @@ -0,0 +1,155 @@ +首发于:https://studygolang.com/articles/28456 + +# Go:边界检查确保内存安全 + +![Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200304-Go-Memory-Safety-with-Bounds-Check/00.png) + +ℹ️*这篇文章基于 Go 1.13 编写。* + +Go 的一系列内存管理手段(内存分配,垃圾回收,内存访问检查)使许多开发者的开发工作变得很轻松。编译器通过在代码中引入“边界检查” 来确保安全地访问内存。 + +## 生成的指令 + +Go 引入了一些控制点位,来确保我们的程序访问的内存片段安全且有效的。让我们从一个简单的例子开始: + +```go +package main + +func main() { + list := []int{1, 2, 3} + + printList(list) +} + +func printList(list []int) { + println(list[2]) + println(list[3]) +} +``` + +这段代码跑起来之后会 panic: + +``` +3 +panic: runtime error: index out of range [3] with length 3 +``` + +Go 通过添加边界检查来防止不正确的内存访问 + +*如果你想知道没有这些检查会怎么样,你可以使用 `-gcflags="-B"` 的选项,输出如下* + +``` +3 +824633993168 +``` + +*因为这块内存是无效的,它会读取不属于这个 slice 的下一个 bytes。* + +利用命令 `go tool compile -S main.go` 来生成对应的[汇编](https://golang.org/doc/asm)代码,就可以看到这些检查点: + +``` +0x0021 00033 (main.go:10) MOVQ "".list+48(SP), CX +0x0026 00038 (main.go:10) CMPQ CX, $2 +0x002a 00042 (main.go:10) JLS 161 +[...] here Go prints the third element +0x0057 00087 (main.go:11) MOVQ "".list+48(SP), CX +0x005c 00092 (main.go:11) CMPQ CX, $3 +0x0060 00096 (main.go:11) JLS 151 +[...] +0x0096 00150 (main.go:12) RET +0x0097 00151 (main.go:11) MOVL $3, AX +0x009c 00156 (main.go:11) CALL runtime.panicIndex(SB) +0x00a1 00161 (main.go:10) MOVL $2, AX +0x00a6 00166 (main.go:10) CALL runtime.panicIndex(SB) +``` + +Go 先使用 `MOVQ` 指令将 list 变量的长度放入寄存器 `CX` 中 + +``` +0x0021 00033 (main.go:10) MOVQ "".list+48(SP), CX +``` + +*友情提醒,slice 类型的变量由三部分组成,指向底层数组的指针、长度,容量(capacity)。list 变量在栈中的位置如下图:* + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200304-Go-Memory-Safety-with-Bounds-Check/00.png) + +*通过将栈指针移动 48 个字节就可以访问长度* + +下一条指令将 slice 的长度与程序即将访问的偏移量进行比较 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200304-Go-Memory-Safety-with-Bounds-Check/01.png) + +`CMPQ` 指令会将两个值相减,并在下一条指令中与 0 进行比较。如果 slice 的长度(寄存器 `CX`)减去要访问的偏移量(在这个例子当中是 2)小于或等于 0(`JLS` 是 *Jump on lower or the same* 的缩写),程序就会跳到 `161` 处继续执行。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200304-Go-Memory-Safety-with-Bounds-Check/02.png) + +两种边界检查使用的都是相同的指令。除了看生成的汇编代码,Go 提供了一个编译期的通行证去打印出边界检查的点,你可以在 `build` 和 `run` 的时候使用标志 `-gcflags="-d=ssa/check_bce/debug=1"` 去开启。输出如下: + +``` +./main.go:10:14: Found IsInBounds +./main.go:11:14: Found IsInBounds +``` + +我们可以看到输出里生成了两个检查点。不过 Go 编译器足够聪明,在不需要的情况下,它不会生成边界检查的指令。 + +## 规则 + +在每次访问内存的时候都生成检查指令是非常低效的,让我们稍微修改一下前面的例子。 + +```go +package main + +func main() { + list := []int{1, 2, 3} + + printList(list) +} + +func printList(list []int) { + println(list[3]) + println(list[2]) +} +``` + +两个 `println` 指令对调了,用 `check_bce` 标志再去跑一遍程序,这次只有一处边界检查: + +``` +./main.go:11:14: Found IsInBounds +``` + +程序先检查了偏移量 `3` 。如果是有效的,那么 `2` 很明显也是有效的,没必要再去检查了。可以通过命令 `GOSSAFUNC=printList Go run main.go` 来生成 SSA 代码看编译过程。这张图就是生成的带边界检查的 SSA 代码: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200304-Go-Memory-Safety-with-Bounds-Check/03.png) + +里面的 `prove` pass 将边界检查标记为移除,这样后面的 pass 将会收集这些 dead code: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200304-Go-Memory-Safety-with-Bounds-Check/04.png) + +用这条命令 `GOSSAFUNC=printList Go run -gcflags="-d=ssa/prove/debug=3" main.go` 可以把 pass 背后的逻辑打印出来,它也会生成 SSA 文件来帮助你 debug,接下来看命令的输出: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200304-Go-Memory-Safety-with-Bounds-Check/05.png) + +这个 pass 实际上会采取不同的策略,并建立了 fact 表。 这些 fact 决定了矛盾点在哪里。 在我们这个例子里,我们可以通过 SSA 的 pass 来解读这些规则: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200304-Go-Memory-Safety-with-Bounds-Check/06.png) + +第一个阶段从代表指令 `println(list[3])` 的分析块 `b1` 开始,这个指令有两种可能: + +- 偏移量 `[3]` 在边界中,跳到第二个指令 b2。在这个例子中,Go 指定 v7 的限制(slice 的长度)是 `[4, max(int)]`。 +- 偏移量 `[3` 不在边界中, 程序跳转到 b3 指令并 panic。 + +接下来,Go 开始处理 `b2` 块(第二个指令)。这里也有两种可能 + +- 偏移量 `[2]` 在边界中,这意味着 slice 的长度 `v7` 比 `v23`(偏移量 `[2]`) 要大。在先前的 b1 块中 Go 已经判断了 `v7 > 4`, 所以这个已经被确认了。 +- 偏移量 [2] 不在边界中,这意味着它比 slice 的长度 `v7` 更大,但 `v7` 的限制是 `[4, max(int)]` ,所以 Go 会将这个分之标记为矛盾,意味着这种情况永远不会发生,这条指令的边界检查可以被移除。 + +这个 pass 在随着时间不断地改善,现在可以参考[更多的 case](https://github.com/golang/go/blob/master/test/prove.go)。消除边界检查可以略微提升 Go 程序的运行速度,但除非你的程序是微妙级敏感的,不然没有必要去优化它。 + +--- + +via: + +作者: [Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[yxlimo](https://github.com/yxlimo) +校对:[Alex.Jiang](https://github.com/JYSDeveloper) +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200309-Go-gsignal-Master-of-Signals.md b/published/tech/20200309-Go-gsignal-Master-of-Signals.md new file mode 100644 index 000000000..3702a1c53 --- /dev/null +++ b/published/tech/20200309-Go-gsignal-Master-of-Signals.md @@ -0,0 +1,64 @@ +首发于:https://studygolang.com/articles/28974 + +# Go:gsignal,信号的掌控者 + +![Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200309-Go-gsignal-Master-of-Signals/00.png) + +ℹ️ *本文基于 Go 1.13。* + +`signal` 包提供了信号处理器,让我们的 Go 程序可以与发送来的信号进行交互。在进入内部细节之前,我们先来了解下 listener。 + +## 订阅 + +对信号的订阅是通过通道实现的。下面是一个监听所有中断信号和终端大小改变信号的程序的例子: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200309-Go-gsignal-Master-of-Signals/01.png) + +每个 `os.Signal` 通道监听各自对应的事件。下面是前面例子订阅工作流的示意图: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200309-Go-gsignal-Master-of-Signals/02.png) + +Go 也赋予了通道停止被通知的能力(`Stop(os.Signal)` 函数)和忽略信号的能力(`Ignore(...os.Signal)` 函数)。下面是两个函数的例子: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200309-Go-gsignal-Master-of-Signals/03.png) + +这个程序无法被 `CTRL + C` 中断,并且永远不会停止,因为在第二次从该通道接收信号之前,该通道已停止侦听终端调整大小的信号。现在我们来看下处理发送来的信号的 `listener` 和 `process` 两个阶段是如何内建的。 + +## gsignal + +在初始化阶段,`signal` 开启了一个在循环中运行的协程,作为处理信号的消费者。在循环被通知激活之前,一直处于睡眠状态。下面是第一步的示意图: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200309-Go-gsignal-Master-of-Signals/04.png) + +然后,当一个信号到达程序时,信号处理器把它代理到一个名为 `gsignal` 的专用协程。这个协程是用比较大的栈空间(32 K,目的是满足不同操作系统的不同需求)创建的,并且空间固定不能增长。每个线程(图片的 `M`)内部都有 `gsignal` 协程来处理信号。下面是更新后的示意图: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200309-Go-gsignal-Master-of-Signals/05.png) + +`gsignal` 分析信号,检查信号是否可处理,在把信号发送到队列里时唤醒处于睡眠状态的协程。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200309-Go-gsignal-Master-of-Signals/06.png) + +*`SIGBUS` 或 `SIGFPE` 等同步信号不能被管理,会被转为 panic* + +然后,循环中的协程可以处理它。它首先找到订阅了这个事件的通道,再把信号推进通道: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200309-Go-gsignal-Master-of-Signals/07.png) + +通过工具 `go tool trace` 可以可视化对循环处理信号的协程的追踪过程。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200309-Go-gsignal-Master-of-Signals/08.png) + +`gsignal` 的阻塞或锁会让信号处理陷入麻烦。由于它的栈空间固定,因此不能再申请内存。这就是在信号处理链中有两个独立的协程很重要的原因:一个协程用于在信号到达时尽快排列信号,另一个协程用循环处理该队列中的信号。 + +现在我们可以用新的组件来更新第一节的插图了: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200309-Go-gsignal-Master-of-Signals/09.png) + +--- +via: https://medium.com/a-journey-with-go/go-gsignal-master-of-signals-329f7ff39391 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200401-Go-How-Does-a-Goroutine-Start-and-Exit.md b/published/tech/20200401-Go-How-Does-a-Goroutine-Start-and-Exit.md new file mode 100644 index 000000000..18f6a2ab0 --- /dev/null +++ b/published/tech/20200401-Go-How-Does-a-Goroutine-Start-and-Exit.md @@ -0,0 +1,76 @@ +首发于:https://studygolang.com/articles/28457 + +# Go 协程的开启和退出 + +![Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200401-Go-How-Does-a-Goroutine-Start-and-Exit/00.png) + +ℹ️本文基于 Go 1.14。 + +在 Go 中,协程就是一个包含程序运行时的信息的结构体,如栈,程序计数器,或者它当前的 OS 线程。调度器还必须注意 Goroutine 的开始和退出,这两个阶段需要谨慎管理。 + +*如果你想了解更多关于栈和程序计数器的信息,我推荐你阅读我的文章 [Go:协程切换时涉及到哪些资源?](https://medium.com/a-journey-with-go/go-what-does-a-goroutine-switch-actually-involve-394c202dddb7)。* + +## 开启 + +开启一个协程的处理过程相当简单。我们用一个程序作为例子: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200401-Go-How-Does-a-Goroutine-Start-and-Exit/01.png) + +`main` 函数在打印信息之前开启了一个协程。由于协程会有自己的运行时间,因此 Go 通知运行时配置一个新协程,意味着: + +- 创建栈 +- 收集当前程序计数器或调用方数据的信息 +- 更新协程内部数据,如 ID 或 状态 + +然而,协程没有立即获取运行时状态。新创建的协程被加入到了本地队列的最前端,会在 Go 调度的下一周期运行。下面是现在这种状态的示意图: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200401-Go-How-Does-a-Goroutine-Start-and-Exit/02.png) + +把协程放在队列的前端,这样它就会在当前协程运行之后第一个运行。如果有工作窃取发生,它不是在当前线程就是在另一个线程运行。 + +*我推荐你阅读我的文章 [Go: Go 调度器中的工作窃取](https://medium.com/a-journey-with-go/go-work-stealing-in-go-scheduler-d439231be64d)来获取更多信息。* + +在汇编指令中也可以看到协程的创建过程: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200401-Go-How-Does-a-Goroutine-Start-and-Exit/03.png) + +协程被创建并被加入到本地协程队列后,它直接执行主函数的下一个指令。 + +## 退出 + +协程结束时,为了不浪费 CPU 资源,Go 必须调度另一个协程。这也使协程可以在以后复用。 + +*在我的文章 [Go: 协程怎么复用?](https://medium.com/a-journey-with-go/go-how-does-go-recycle-goroutines-f047a79ab352)中你可以找到更多信息。* + +然而,Go 需要一个能识别到协程结束的方法。这个方法是在协程创建时控制的。创建协程时,Go 在将程序计数器设置为协程真实调用的函数之前,将堆栈设置为名为 `goexit` 的函数。这个技巧可以使协程在结束时必须调 `goexit` 函数。下面的程序可以使我们理解得更形象: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200401-Go-How-Does-a-Goroutine-Start-and-Exit/04.png) + +根据输出信息进行堆栈追踪: + +```bash +/path/to/src/main.go:16 +/usr/local/go/src/runtime/asm_amd64.s:1373 +``` + +用汇编写的 `asm_amd64` 文件包含这个函数: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200401-Go-How-Does-a-Goroutine-Start-and-Exit/05.png) + +之后,Go 切换到 `g0` 调度另一个协程。 + +我们也可以调用 `runtime.Goexit()` 来手动终止协程: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200401-Go-How-Does-a-Goroutine-Start-and-Exit/06.png) + +这个函数首先运行 defer 中的函数,然后会运行前面在协程退出时我们看到的那个函数。 + +--- + +via: https://medium.com/a-journey-with-go/go-how-does-a-goroutine-start-and-exit-2b3303890452 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200405-go-how-does-gops-interact-with-the-runtime.md b/published/tech/20200405-go-how-does-gops-interact-with-the-runtime.md new file mode 100644 index 000000000..f1289033e --- /dev/null +++ b/published/tech/20200405-go-how-does-gops-interact-with-the-runtime.md @@ -0,0 +1,85 @@ +首发于:https://studygolang.com/articles/30246 + +# Go:gops 如何与 runtime 交互? + +![](hhttps://raw.githubusercontent.com/studygolang/gctt-images2/master/20200405-go-how-does-gops-interact-with-the-runtime/1_3PCyB5PhH_NEZoNnj693dA.png) + +本文基于 Go 1.13 和 gops 0.3.7。 + +`gops` 旨在帮助开发人员诊断 Go 进程并与之交互。它提供了追踪运行中的程序数秒钟,通过 `pprof` 获取 CPU 的 profile,甚至直接与垃圾回收器交互的能力。 + +## 发现 + +`gops` 提供了一种发现服务,它可以列出计算机上运行的 Go 进程。不带参数运行 `gops` 仅显示 Go 进程。为了举例说明,我启动了一个程序,该程序计算素数直到一百万。这是发现程序的输出: + +```bash +295 1 gops go1.13 /go/src/github.com/google/gops/gops +168 1 prime-number* go1.13 /go/prime-number/prime-number +``` + +`gops` 发现了上面启动的程序和它自己的进程。我们需要的仅仅是进程 ID,因此,基于此输出,我们可以开始与程序进行交互。不过,还是让我们了解一下 `gops` 如何过滤 Go 进程。 + +首先,`gops` 列出所有的进程。接着,对于每个进程,它打开二进制文件读取符号表: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200405-go-how-does-gops-interact-with-the-runtime/1_LyVcQzBGP3i4aCwzwmzoFA.png) + +如果符号表包含了 `runtime.man` (主 Goroutine 的入口)或者 `main.main` (我们程序的入口),则可以将其标记为一个 Go 程序。 + +*有关符号表的更多信息,我建议你阅读我的文章“Go:如何使用符号表”。了解关于主 Goroutine 的更多信息,建议阅读我的文章“[Go: g0,特殊的 Goroutine](https://medium.com/a-journey-with-go/go-g0-special-goroutine-8c778c6704d8)”* + +`gops` 还会通过读取符号表的 `runtime.buildVersion` 来读取 Go 的版本。然而,由于二进制文件中的符号表可以被剥离,`gops` 需要另一种方法来检测 Go 二进制文件。我们用剥离后的二进制文件再试一次: + +```bash +295 1 gops go1.13 /go/src/..../gops +168 1 prime-number-s* unknown Go version /go/.../prime-number-s +``` + +由于缺少符号表,即使程序被正确的标记为 Go 二进制文件,它也无法检测 Go 版本。根据[可执行文件格式](https://en.wikipedia.org/wiki/Comparison_of_executable_file_formats) -- `ELF`,`MZ`,等等 --`gops` 会读取各段来查找嵌在二进制文件中的构建 ID。一旦发现过程结束,它就可以开始与程序交互。 + +## 交互 + +与其他 Go 程序交互的唯一条件是确保它们启动了 `gops` agent。该 agent 是一个简单的 listener,它将为 gops 请求提供服务。这很简单,只需添加以下几行: + +```go +if err := agent.Listen(agent.Options{}); err != nil { + log.Fatal(err) +} +``` + +然后,任何启动了 agent 的程序都可以与 `gops` 交互。这里是执行 `stats` 命令的例子: + +```bash +# gops stats 168 +goroutines: 6210 +OS threads: 9 +GOMAXPROCS: 2 +num CPU: 2 +``` + +有关更多命令,你可以参考[项目的文档](https://github.com/google/gops#manual)。如果缺少该 agent,你在与其交互时会收到一个错误: + +```bash +Couldn't resolve addr or pid 168 to TCPAddress: couldn't get port for PID 168 +``` + +该错误表明 `gops` 在通过 TCP 寻找暴露的 endpoint 以便与程序通信。让我们画出这个 package 的工作流来了解它的工作原理: + +## 工作流 + +`gops` 通过 TCP 和要读取的程序暴露的 endpoint 来与其通信: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200405-go-how-does-gops-interact-with-the-runtime/1_V5turxRPbzzq9rHbqOjpbg.png) + +分配给每个程序的端口都写在一个配置文件中,例如 `path/to/config/{processID}` ,这使得 `gops` 很容易知道暴露的端口。然后,`gops` 可以将命令标记发送给程序,agent 将会收集数据并响应: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200405-go-how-does-gops-interact-with-the-runtime/1_PvRmDO4yXEdm6Z7xeysh8A.png) + +--- + +via: + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[DoubleLuck](https://github.com/DoubleLuck) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200408-Implementing-RSA-Encryption-and-Signing-in-Golang.md b/published/tech/20200408-Implementing-RSA-Encryption-and-Signing-in-Golang.md new file mode 100644 index 000000000..4058e6b87 --- /dev/null +++ b/published/tech/20200408-Implementing-RSA-Encryption-and-Signing-in-Golang.md @@ -0,0 +1,174 @@ +首发于:https://studygolang.com/articles/28458 + +# 用 Golang 实现 RSA 加密和签名(有示例) + +本文介绍 RSA 干了什么,以及我们怎样用 Go 实现它。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200408-Implementing-RSA-Encryption-and-Signing-in-Golang/00.jpg) + +RSA(*Rivest–Shamir–Adleman*)加密是使用最广的安全数据加密算法之一。 + +它是一种非对称加密算法,也叫”单向加密“。用这种方式,任何人都可以很容易地对数据进行加密,而只有用正确的”秘钥“才能解密。 + +> 如果你想跳过解释直接看源码,点击[这里](https://gist.github.com/sohamkamani/08377222d5e3e6bc130827f83b0c073e)。 + +## RSA 加密,一言以蔽之 + +RSA 是通过生成一个公钥和一个私钥进行加/解密的。公钥和私钥是一起生成的,组成一对秘钥对。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200408-Implementing-RSA-Encryption-and-Signing-in-Golang/01.svg) + +公钥可以用来加密任意的数据,但不能用来解密。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200408-Implementing-RSA-Encryption-and-Signing-in-Golang/02.svg) + +私钥可以用来解密由它对应的公钥加密的数据。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200408-Implementing-RSA-Encryption-and-Signing-in-Golang/03.svg) + +这意味着我们可以把我们的公钥给任何想给的人。之后他们可以把想发送给我们的信息进行加密,唯一能访问这些信息的方式就是用我们的私钥进行解密。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200408-Implementing-RSA-Encryption-and-Signing-in-Golang/04.svg) + +> 秘钥的生成过程,以及信息的加密解密过程不在本文讨论范围内,但是如果你想研究详细信息,这里有一个关于此主题的[强大视频](https://www.youtube.com/watch?v=wXB-V_Keiu8)。 + +## 秘钥的生成 + +我们要做的第一件事就是生成公钥私钥对。这些秘钥是随机生成的,在后面所有的处理中都会用到。 + +我们用标准库 [crypto/rsa](https://pkg.go.dev/crypto/rsa?tab=doc) 来生成秘钥,用 [crypto/rand](https://pkg.go.dev/crypto/rand?tab=doc) 库来生成随机数。 + +```go +// The GenerateKey method takes in a reader that returns random bits, and +// the number of bits +privateKey, err := rsa.GenerateKey(rand.Reader, 2048) +if err != nil { + panic(err) +} + +// The public key is a part of the *rsa.PrivateKey struct +publicKey := privateKey.PublicKey + +// use the public and private keys +// ... +``` + +`publicKey` 和 `privateKey` 变量分别用于加密和解密。 + +## 加密 + +我们用 [EncryptOEAP](https://pkg.go.dev/crypto/rsa?tab=doc#EncryptOAEP) 函数来加密一串随机的信息。我们需要为这个函数提供一些输入: + +1. 一个哈希函数,用了它之后要能保证即使输入做了微小的改变,输出哈希也会变化很大。SHA256 适合于此。 +2. 一个用来生成随机位的 random reader,这样相同的内容重复输入时就不会有相同的输出 +3. 之前生成的公钥 +4. 我们想加密的信息 +5. 可选的标签参数(本文中我们忽略) + +```go +encryptedBytes, err := rsa.EncryptOAEP( + sha256.New(), + rand.Reader, + &publicKey, + []byte("super secret message"), + nil) +if err != nil { + panic(err) +} + +fmt.Println("encrypted bytes: ", encryptedBytes) +``` + +这段代码会打印加密后的字节,看起来有点像无用的信息。 + +## 解密 + +如果想访问加密字节承载的信息,就需要对它们进行解密。 + +解密它们的唯一方法就是使用与加密时的公钥对应的私钥。 + +`*rsa.PrivateKey` 结构体有一个方法 [Decrypt](https://pkg.go.dev/crypto/rsa?tab=doc#PrivateKey.Decrypt),我们使用这个方法从加密数据中解出原始的信息。 + +解密时我们需要输入的参数有:1. 被加密的数据(称为*密文*)2. 加密数据用的哈希 + +```go +// The first argument is an optional random data generator (the rand.Reader we used before) +// we can set this value as nil +// The OEAPOptions in the end signify that we encrypted the data using OEAP, and that we used +// SHA256 to hash the input. +decryptedBytes, err := privateKey.Decrypt(nil, encryptedBytes, &rsa.OAEPOptions{Hash: crypto.SHA256}) +if err != nil { + panic(err) +} + +// We get back the original information in the form of bytes, which we +// the cast to a string and print +fmt.Println("decrypted message: ", string(decryptedBytes)) +``` + +## 签名和校验 + +RSA 秘钥也用于签名和校验。签名不同于加密,签名可以让你宣示真实性,而不是机密性。 + +也就是说,由原始信息生成一段数据,称为“签名”,而不是伪装原始信息的内容(像[加密](https://www.sohamkamani.com/golang/rsa-encryption/#encryption)中做的那样)。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200408-Implementing-RSA-Encryption-and-Signing-in-Golang/05.svg) + +有签名、信息和公钥的任何人,可以用 RSA 校验来确保信息就是来自拥有公钥的人。如果数据和签名不匹配,校验不通过。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200408-Implementing-RSA-Encryption-and-Signing-in-Golang/06.svg) + +请注意,只有拥有私钥的人才能对信息进行签名,但是有公钥的人可以验证它。 + +```go +msg := []byte("verifiable message") + +// Before signing, we need to hash our message +// The hash is what we actually sign +msgHash := sha256.New() +_, err = msgHash.Write(msg) +if err != nil { + panic(err) +} +msgHashSum := msgHash.Sum(nil) + +// In order to generate the signature, we provide a random number generator, +// our private key, the hashing algorithm that we used, and the hash sum +// of our message +signature, err := rsa.SignPSS(rand.Reader, privateKey, crypto.SHA256, msgHashSum, nil) +if err != nil { + panic(err) +} + +// To verify the signature, we provide the public key, the hashing algorithm +// the hash sum of our message and the signature we generated previously +// there is an optional "options" parameter which can omit for now +err = rsa.VerifyPSS(&publicKey, crypto.SHA256, msgHashSum, signature, nil) +if err != nil { + fmt.Println("could not verify signature: ", err) + return +} +// If we don't get any error from the `VerifyPSS` method, that means our +// signature is valid +fmt.Println("signature verified") +``` + +## 总结 + +本文中我们看到了如何生成 RSA 公钥和私钥,以及怎样使用它们进行加密、解密、签名和验证任意数据。 + +在将它们用于你的数据之前,你需要了解一些使用限制。首先,你要加密的数据必须比你的秘钥短。例如,[EncryptOAEP 文档](https://pkg.go.dev/crypto/rsa?tab=doc#EncryptOAEP) 中说“(要加密的)信息不能比公布的模数减去哈希长度的两倍后再减去 2 长”。 + +使用的哈希算法要适合你的需求。SHA256(在本例中用的就是 SHA256)可以用于大部分案例,但是如果是对数据要求更高的应用,你可能需要用 SHA512。 + +你可以在[这里](https://gist.github.com/sohamkamani/08377222d5e3e6bc130827f83b0c073e)找到所有示例的源码。 + +--- + +via: https://www.sohamkamani.com/golang/rsa-encryption/ + +作者:[Soham Kamani](https://twitter.com/sohamkamani) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200413-Go-Aliases-Simple-and-Efficient.md b/published/tech/20200413-Go-Aliases-Simple-and-Efficient.md new file mode 100644 index 000000000..beeeb0f1c --- /dev/null +++ b/published/tech/20200413-Go-Aliases-Simple-and-Efficient.md @@ -0,0 +1,141 @@ +首发于:https://studygolang.com/articles/28459 + +# Go 中使用别名,简单且高效 + +![Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-Aliases-Simple-and-Efficient/00.png) + +ℹ️ 本文基于 Go 1.13。 + +Go 1.9 版本引入了别名,开发者可以为一个已存在的类型赋其他的名字。这个特性旨在促进大型代码库的重构,这对大型的项目至关重要。在思考了几个月应该以哪种方式让 Go 语言支持别名后,这个特性才被实现。[最初的提案](https://go.googlesource.com/proposal/+/master/design/16339-alias-decls.md)是引入广泛的别名(支持对类型、函数等等赋别名),但这个提案后来被另一个[更简单的别名机制](https://go.googlesource.com/proposal/+/master/design/16339-alias-decls.md)所替代,新提案只关注对类型赋别名,因为对这个特性需求最大的就是类型。只支持对类型赋别名让实现方式变得简单,因为只需要解决最初始的问题就可以了。我们一起来看看这个解决方案。 + +## 重构 + +引入别名的最主要的意图是简化对大型代码库的重构。开发者们对旧名字赋一个新的别名,就可以避免破坏已存在代码的兼容性。下面是一个 `docker/cli` 的例子: + +```go +package command// Deprecated: Use github.com/docker/cli/cli/streams.In instead +type InStream = streams.In + +// Deprecated: Use github.com/docker/cli/cli/streams.Out instead +type OutStream = streams.Out +``` + +这样不会影响使用 `command.InStream` 的旧代码,而新代码使用新类型 `streams.In` 。 + +然而,为了完全支持兼容,别名还需要有以下特性: + +- 可互相转换的参数类型。新旧类型都可以被作为参数接收。下面是一个 T1 和 T2 可以相互转换的例子: + +```go +type T2 struct {} + +// T2 is deprecated, please use T1 +type T1 = T2 + +func main() { + var t T1 + f(t) +} + +func f(t T1) { + // print main.T2 + println(reflect.TypeOf(t).String()) +} +``` + +- 新旧两种类型都可以从空接口转换而来。下面是例子: + +```go +type T2 struct {} + +// T2 is deprecated, please use T1 +type T1 = T2 + +func main() { + var t T1 + f(t) +} + +func f(t interface{}) { + t, ok := t.(T1) + if !ok { + log.Fatal("t is not a T1 type") + } + // print main.T2 + println(reflect.TypeOf(t).String()) +} +``` + +因为新旧类型可以在任何时间相互转换,所以已有代码不会被破坏,可以实现平滑迁移。 + +## 可读性 + +别名也可以提高代码的可读性。下面是 Go 标准库和反汇编器包里的例子: + +```go +type lookupFunc = func(addr uint64) (sym string, base uint64) +``` + +一个 lookup 函数接收一个 address 作为参数,返回另一个 address 的 symbol。相比于把这个函数原型作为参数传递给每一个函数,使用这个新别名可读性更好。下面是使用别名作为参数的函数原型: + +```go +func disasm_amd64([]byte, uint64, lookupFunc, binary.ByteOrder) +``` + +`golang.org/x/sys/unix` 中的包通过对经常使用的类型赋别名减少了样板代码量。那些别名声明在单独的文件中: + +```go +type Signal = syscall.Signal +type Errno = syscall.Errno +type SysProcAttr = syscall.SysProcAttr +``` + +声明后,在包中只能引用 `Errno` 而不能再引用 `syscal.Errno`。 + +## 运行时 + +现在我们看到了在程序中使用别名的好处,但是我们还不知道在运行时有什么影响。我们来看之前结构体与空接口相互转换的例子: + +```go +type T2 struct {} + +// T2 is deprecated, please use T1 +type T1 = T2 + +func main() { + var t T1 + f(t) +} + +func f(t interface{}) { + t, ok := t.(T1) + if !ok { + log.Fatal("t is not a T1 type") + } + // print main.T2 + println(reflect.TypeOf(t).String()) +} +``` + +虽然从最终的输出看 `t` 的类型是 `T2`,但是这个程序仍然可以把 `t` 转换为 `T1`。我们把代码转换成[汇编](https://golang.org/doc/asm)。下面是输出的部分信息: + +``` +0x0021 00033 (main.go:19) MOVQ "".t+88(SP), AX +0x0026 00038 (main.go:19) PCDATA $0, $1 +0x0026 00038 (main.go:19) LEAQ type."".T2(SB), CX +0x002d 00045 (main.go:19) CMPQ AX, CX +0x0030 00048 (main.go:20) JNE 172 +``` + +第一行指令 `MOVQ` 读取空接口的类型并把它储存在寄存器 `AX`。然后 `LEAQ` 把 `T2` 类型加载到寄存器 `CX`,两个寄存器可以做比较。 + +我们可以看到,代码中的转换是基于 `T2` 而不是 `T1`。别名在编译时被改变,这样可以消除掉我们程序中的开销。 + +--- +via: https://medium.com/a-journey-with-go/go-aliases-simple-and-efficient-8506d93b079e + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200418-Naming-Conventions-in-Go-short-but-descriptive.md b/published/tech/20200418-Naming-Conventions-in-Go-short-but-descriptive.md new file mode 100644 index 000000000..12ba0b62c --- /dev/null +++ b/published/tech/20200418-Naming-Conventions-in-Go-short-but-descriptive.md @@ -0,0 +1,112 @@ +首发于:https://studygolang.com/articles/28975 + +# Go 语言中命名规范——如何简短却更具描述性 + +> 在计算机科学与技术中,有两件事情最难,第一是缓存无效,第二就是给一些东西命名 —— Phil Karlton + +上面的话可不是一个笑话。写代码很容易,但是阅读起来却很痛苦。你是否有想知道一个变量具体指什么或者某个包的具体含义是什么这种类似的经历?这就是为什么我们需要一些规则和约定。 + +不过,约定虽然能够让我们的生活变得更轻松,但是也容易被高估和滥用。设置一些合理的命名约定和规则非常重要,不过盲目的遵循它也可能带来很多弊端。 + +在这篇文章里面,我将介绍在[Go](https://golang.org/)中,一些重要的变量命名约定(官方的以及非官方的规则)以及在什么场景下会存在滥用的情况,特别是那些短变量命名的场景。篇幅有限,包和文件的命名以及项目结构命名有关的内容不在本文讨论范围内,他们应该可以单独再写一篇文章。 + +## Go 官方的书写规范 + +与其他编程语言类似,Go 有自己的[命名规范](https://golang.org/doc/effective_go.html#names)。此外,命名也具有一些语义的效果,它决定了包外部对他们的可见性。 + +### 大小写混合 + +Go 中的约定是使用 `MixedCaps` 或 `mixedCaps` 这种形式(简称为**驼峰命名**),而不是使用下划线来编写多词名称。 如果需要在包外部可见,则其第一个字符应为大写。 如果您不想将其用于其他包,则可以放心地使用 `mixedCaps`。 + +```go +package awesome +type Awesomeness struct { +} +//Do 方法是一个外部方法,可以被别的包调用 +func (a Awesomeness) Do() string { + return a.doMagic("Awesome") +} +func (a Awesomeness) doMagic(input string) string { + return input +} +``` + +如果你尝试在外部使用 `doMagic` 方法,你会得到一个编译错误。 + +### 接口名称 + +> 根据命名规则,一种方法的接口,需要在名称后面加上 `-er` 的后缀,或者通过代理名词的方式来进行修饰:`Reader, Writer,Formatter,CloseNotifier` 等 —— Go 官方文档 + +按照这个规则,`MethodName + er = InterfaceName`。这里最麻烦的是,你的一个接口有多个方法的时候,按照这个方式命名,就不总是很清晰明了。那是否要把结构拆分,变成一个接口对应一个方法呢?我觉得这个取决具体的使用场景了。 + +### Getters + +[官方文档](https://golang.org/doc/effective_go.html#Getters)中提及了,Go 并没有自动支持 setters 与 getters,不过这里并不禁止它,而是有一些特定的规则: + +> 自己实现 getters 和 setters 方法并没有什么问题,而且大部分场景是有用处的。不过没有必要也不需要一直把 `Get` 放在 getter 的方法名字上面。(译者注:编写者希望 Getter 方法前面不需要用 Get 再修饰了) + +```go +owner := obj.Owner() +if owner != user { + obj.SetOwner(user) +} +``` + +这里需要额外提醒一下,如果你的 setter 方法里面没有任何特殊的逻辑,建议直接导出这个属性,并摆脱掉 setter 和 getter 方法。如果你是一个 oop(面向对象)的死忠粉,可能会听起来比较奇怪,但事实并非如此 + +## Go 非官方的书写规范 + +一些规则虽然在官方文档里面没有提及,不过却在社区中被普遍使用。 + +### 简短的变量名 + +Go 社区推荐使用简短的变量名,不过我认为这个约定遭到了滥用。有些时候,通常会忘记添加一些描述性的部分。一个描述性的名称,可以帮助读者理解他的实际作用,甚至在使用过程中也能快速理解其含义。 + +> 编写的程序必须能够让人们阅读它,只不过是顺带让机器执行了一下!—— Harold Abelson + +- 单个字母标识符:通常只在范围有限的局部变量里面使用。我们都认可不需要通过 `index` 或者 `idx` 来标识自增变量。单字母标识符只推荐在循环范围内使用。 + +```go +for i := 0; i < len(pods); i++ { + // +} +... +for _, p := range pods { + // +} +``` + +- 简写名称:只要可能,建议速记名称,只要对于第一次阅读该代码的人来说都易于理解。使用范围越广,就越需要描述。 + +```go +pid // Bad (does it refer to podID or personID or productID?) +spec // good (refers to Specification) +addr // good (refers to Address) +``` + +### 唯一名称 + +这种通常都是一些缩写,例如:API,HTTP 等等,或者类似于 ID,DB 这种。按照惯例,我们保留原样: + +- `userID` 来替代 `userId` +- `productAPI` 来替代 `productApi` + +### 长度限制 + +在 Go 中没有长度限制,不过避免特别长的内容也是值得推荐的。 + +## 结论 + +我总结了 Go 语言中的一些常用的命名规范,在什么时候以及场景应用他们。我也解释了 Go 语言中简短命名背后主要的思想——在简洁和描述之间找到平衡点。 + +规定是为了引导你,而不是阻碍你。只要合适,您就应该轻松地打破它们,并且仍然可以满足一般目的。 + +--- + +via: https://medium.com/better-programming/naming-conventions-in-go-short-but-descriptive-1fa7c6d2f32a + +作者:[Dhia Abbassi](https://medium.com/@dhiatn) +译者:[Alex.Jiang](https://github.com/JYSDeveloper) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200422-Why-Golang-Settling-the-Debate-Once-and-For-All.md b/published/tech/20200422-Why-Golang-Settling-the-Debate-Once-and-For-All.md new file mode 100644 index 000000000..bb1133958 --- /dev/null +++ b/published/tech/20200422-Why-Golang-Settling-the-Debate-Once-and-For-All.md @@ -0,0 +1,82 @@ +首发于:https://studygolang.com/articles/34445 + +# 为什么选择 Golang?彻底解决争论 + +我们都经历过绿地(greenfield)项目初期的幸福感。对于每一个决定,你都有无限的选择余地,当你启动项目编码的时候,你首先要处理的问题是我应该选择哪种编程语言来开发我的项目? + +我是应该使用一种新的有趣的语言还是应该坚持使用最流行的语言?幸运的是,在 baconce Technology,我们的专家对这些问题非常熟悉,因为我们处理过各种宽高比(shapes)和尺寸的客户端(屏幕),而且我们总是在考虑编程语言应该适合服务器端应用程序开发这一共同可能性的情况下做出选择。这就是为什么我们特别重视 Golang 。 + +如今,大多数科技创始人都在使用 Go 语言。这引起了很多人的好奇,为什么有十年历史的语言正在成功地取代 1991 年出现的 Java 和其他编程语言,它们比 Golang 要古老得多。 + +为了验证,我检查 [TIOBE Index](https://www.tiobe.com/tiobe-index/) 的流行程度,TIOBE 是一个衡量编程语言流行程度的编程社区指数,根据 TOIBE 公布数据,Go 编程语言的使用和采用都有显著且稳定的增长。Go 是一种严肃的语言,在提升企业级项目方面具有良好的记录。 + +## 回到最初的问题:为什么选择 Golang ? + +假设您是一名 Golang 程序员,并且在一个完全不同的城市参加活动。您决定与周围的人分享一些有关 [Golang 中微服务架构](https://www.bacancytechnology.com/blog/microservice-architecture-in-golang) 的知识; 它是一个二进制文件,所以 Golang 相对稳定,成熟且快速。你的知识吸引了其他人的注意,其中一个人立刻使您认为 [Golang 比 Rust 更好](https://www.bacancytechnology.com/blog/golang-vs-rust),另一个人也好奇地问 [Golang 是否会超越 Python](https://www.bacancytechnology.com/blog/9-reasons-to-choose-golang) ? + +不幸的是,您感觉不太好,因为您只能回答 Go 语言的问题,使 Golang 成为 Web 应用程序开发的最佳编程语言,但这并不是一个令人满意的答案。 突然之间,您选择使用 Go 语言而不是其他编程语言开始感到难过,因为您被告知 Go 相当快并且在语法上类似于 C,但是具有 CSP 样式的并发,垃圾回收和内存安全性。 + +您选择了 Go 作为一种有前途的语言,但是另一种对 Golang 的看法则完全不同,然而你早就做出了选择,现在您很高兴加入圈子,是因为您想一劳永逸地解决辩论。 + +## 那么让我们一起讨论:为什么 Golang 比 Python 好? + +- Go 和 Python 都以面向简单的语言呈现。 两种语言的代码都易于阅读和理解。 +- 与 Python 相比,Golang 提供了更流畅的调试过程。 Python 是动态类型语言; 它在运行时提示错误,而 Go 是静态类型语言。 它在编译时而不是执行时提示错误。 +- Go 提供了对 Web 应用程序开发的内置支持; 因此,Go 开发者表示不需要使用框架。 但是,Python 为您提供了便利的基础方法。 +- **Golang vs. Python: 性能** + +通过包括内存管理,功能和速度等多个因素,可以轻松测试这两种编程语言的性能。 两种编程语言都表现出色。 + +- **Golang vs. Python: 扩展性** + +在当今时代,拥有可扩展的应用程序构建过程是研发类型公司要考虑的最重要的事情之一。 Golang 是一种参数类型强校验的编程语言。 对于像 Google 这样的 Golang 研发类型公司来说,并发过程通道的支持和想法被证明是一个重要因素。 + +但是,当涉及到 Python 时,该语言在并发情况下肯定会出现一些问题,但在并行性方面却总能得出令人莫名其妙的结果。当涉及到分解任务以提供更好的可扩展性时,Python 提供了一致的结果。 + +## 有人好奇地问您对此有何看法 + +### Golang vs. Rust + +好吧,Go vs. Rust 一直是讨论的热门话题,因为它们大约同时发布。 在设计 Web 应用程序时,Rust 和 Golang 都是能胜任的语言。 两种语言都能够处理高流量,并且还能够垂直和水平缩放。 Rust 和 Golang 语言都不容许不安全的内存访问。 + +相反,行业专家建议,与 Rust 相比,Go 在所有可能的方式上都是一种优越的语言。 与其他编程语言相比,它是完全安全的。 两种语言都在增长,并在开发者社区中越来越流行。 [为什么 Go 比 Rust 更好?](https://www.bacancytechnology.com/blog/golang-vs-rust) 由于其简单性和易用性。 它的学习速度也更快,为开发人员提供了更轻松的编程体验。 因此,Go 是开发人员团队更喜欢的语言。 Go 语言注重简单性和统一性。 通过提供严格的简化应用程序,可以使团队更加高效。 + +Rust 作为一种编程语言,为开发人员团队提供了精细的控制能力,还可以控制线程及其在系统其余部分的行为。 然而,由于 Rust 的复杂性以及学习和适应的难度,Rust 并未得到广泛的欢迎。 此外,Rust 无法轻易适应其他语言(例如 Python),并且正在竞争寻找其在 C ++ 和 D 等语言中的重要性。 + +## 然后我问,您对 Golang 与 Node.js 有何看法? + +### Golang Vs. NodeJS:性能 + +在 Node.js 的比较中,Golang 非常轻巧,其性能特征类似于 C / C ++。 在现实生活中,两种技术都显示出相似的结果。 当涉及数据库交互或网络通信时,Node.js 和 Go 几乎是相等的,它们以相似的速度工作。 + +Go 的性能始终如一,但 Node.js 的性能忽高忽低,这取决于要构建的应用程序的类型。 + +### Golang Vs. NodeJS: 错误处理 + +当涉及到编译时和运行时错误时,肯定会给 Go 开发人员带来麻烦。 Golang 的创建者还开始执行进一步的错误处理功能,以缩短开发时间。 而 Node.js 的错误处理机制在常规方法中非常流行,因为错误会在执行进一步操作之前立即容错修复。 但是 Node.js 的执行结果有时会不一致。 + +### Go vs. Node JS: 开发工具 + +在 Nodejs 的比较中,Golang 没有过多的库和包。 Go 需要对手动配置进行研究,而 Node.js 具有广泛的工具,框架,库和庞大的社区,可为各种开发工具提供扩展支持。 在开发工具中,Go 输给了 Node.js。 + +## Golang 如何与其他编程语言相提并论? + +Golang 从根本上更好,因为它具有在所有任务中都能平稳运行的能力和可伸缩性。 在机器学习,物联网,云编码和后端方面,它是卓越的–显而易见的赢家,因为它节省了时间。 这就是它给我们带来的强项和范例。 + +如果您打算在考虑跨平台支持的情况下构建 Web 应用程序,并充分利用并发性和代码库,那么毫无疑问,您应该 [雇用 golang 开发人员](https://www.bacancytechnology.com/hire-golang-developer)。 Go 是 Google 的一项出色的语言计划,并且迁移到 Golang 可能会帮助您制定软件策略,并且在不久的将来也很有用。 + +## 何时应该将您的应用迁移到 Golang? + +在短时间内,Netflix 已将其服务扩展到全球范围,而 Netflix 利用 Golang 进行了优化的服务器加载,并通过 Golang 进行无中断的后台加载来对服务器进行有效的数据处理。 + +如果你的业务需要实施流行(modern )功能,例如新的电子商务功能和按需服务,那么毫无疑问,迁移到 Golang 是一个可行的选择。 当应用程序扩展并添加新功能时,由其前辈编写代码变得有些困难。 因为这会导致维护和故障排除中的响应时间增加。 因此,如果需求激增并且您现有的应用程序无法处理容量,那么 Golang 是确保服务器快速响应和可预测的业务增长的肯定选择。 + +--- + +via: https://www.bacancytechnology.com/blog/why-choose-golang-over-python-rust-and-nodejs + +作者:[Riken Solanki](https://www.bacancytechnology.com/blog/author/riken-solanki/) +译者:[lts8989](https://github.com/lts8989) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200423-Cancelable-Reads-in-Go.md b/published/tech/20200423-Cancelable-Reads-in-Go.md new file mode 100644 index 000000000..e67cde03e --- /dev/null +++ b/published/tech/20200423-Cancelable-Reads-in-Go.md @@ -0,0 +1,191 @@ +首发于:https://studygolang.com/articles/30249 + +# Go 中可取消的读取操作 + +在使用 Go 进行工作时,使用 `io.Reader` 和 `io.Writer` 接口是最常见的情场景之一。这很合理,它们是数据传输的主力接口。 + +关于 `io.Reader` 接口,有一点令我困惑:它的 `Read()` 方法是阻塞的,一旦读取操作开始,你没有办法去抢占它。同样,也无法在读取操作上执行 `select` 操作,异步协调多个 `io.Reader` 时的读取操作会有点棘手。 + +`io.ReadCloser` 是 Go 提供的一个常用的退出通道,在许多情况下,它确实允许你抢占一个读取操作。在某些实现中,调用 reader 的 `Close()` 方法将会取消剩余的读取操作。但是,你只能执行一次,当一个 reader 被关闭后,后续的读取操作将会失败。 + +我正在维护的应用中有很多地方可以有效地执行以下操作: + +```go +func asyncReader(r io.Reader) { + ch := make(chan []byte) + Go func() { + defer close(ch) + for { + var b []byte + // Point of no return + if _, err := r.Read(b); err != nil { + return + } + ch <- b + } + }() + + select { + case data <- ch: + doSomething(data) + //... + // Other asynchronous cases + //... + } +} +``` + +当必须将 `Read()` 方法和一些其他异步数据源,如 gRPC 流一起混用时,我们使用上述模式。你生成一个 Goroutine 从 reader 中不停地读取数据,将接收到的数据从一个 channel 中发送出去,当 reader 关闭时执行一些清理操作。 + +当你尝试清理整个系统时,会遇到棘手的部分:如果我们想要发送结束该过程的信号,该怎么办?考虑一个改造过的例子: + +```go +func asyncReader(r io.Reader, doneCh <-chan struct{}) { + ch := make(chan []byte) + continueReading := false + Go func() { + defer close(ch) + for !continueReading { + var b []byte + // Point of no return + if _, err := r.Read(b); err != nil { + return + } + ch <- b + } + }() + + select { + case data <- ch: + doSomething(data) + case <-doneCh: + continueReading = false + return + } +} +``` + +当上面的 `doneCh` 被关闭时,`asyncReader` 方法将返回。我们创建的 Goroutine 也将在下次计算 `for` 循环中的条件时返回。但是,如果 Goroutine 阻塞在 `r.Read()` 该怎么办?那样的话,我们实质上泄露了一个 goroutine。在 reader 离开阻塞状态之前,我们将一直陷入困境。 + +reader 有可能永远阻塞下去吗?也许会,也许不会。这取决于底层的 reader。至关重要的是,接口无法向你保证它一定会解除阻塞,因此仍然存在着 Goroutine 泄露的可能性。 + +如果只有 `io.Reader` 接口,那么你现在就陷入了困境。如果你可以切换到 `io.ReadCloser` 接口,并且控制 Close() 方法的行为,你可以将其用作取消最后读取操作的辅助通道。但是,也只能这样。 + +这令人沮丧,这让我好奇,为什么下述接口这样的方法并不常见: + +```go +interface PreemptibleReader { + Read(ctx context.Context, p []byte) (n int, err error) +} +``` + +假如 `Read()` 方法携带一个 context 参数,就可以抢占读取操作。 + +## 为什么 `io.Reader` 会阻塞 + +退一步讲,为什么读取操作默认不支持抢占,值得思考一下。考虑 `io.Reader` 的接口: + +```go +interface Reader { + Read(p []byte) (n int, err error) +} +``` + +这究竟是什么意思呢?[1](#fn1) 实际上,接口规定:“拿着字节切片 `p`,向里面写入一些内容。告诉我你写了多少字节(`n`),以及在执行过程中是否某处遇到了错误(`err`)”。 + +这个接口令人惊讶的通用,因为它需要适应多种用途。从内存缓冲到 HTTP 响应到数据库事务结果的所有内容都可以实现为一个 `io.Reader` 接口。因此,许多 `io.Reader` 的实现有内部状态。 + +同样,对 `p` 的修改也不是原子操作。如果你要在执行过程中的任意地方取消读取,你可能得到不一致的状态 - 同时在目标字节切片和 reader 的内部状态中,如果适用的话。 + +如果我们允许可被任意抢占的读取,这些会导致大量的复杂性。如果抢占发生在修改字节切片的中途会导致什么?reader 是否需要将字节切片清零?如果发生在 reader 即将返回错误并进行清理工作时呢? + +抢占使事情变得混乱。当然,有一些方法可以进行“优雅的清理”。当我们想要取消 Go 中的操作时,通常使用 context,例如网络请求。取消一个 context 并不保证操作会立刻被终止,因此有一些空间可以进行记录和清理。 + +我认为要记住的是,`io.Reader` 被设计为几乎可以在任意地方工作。因此,尤其当 `io.Reader` 实际完成的工作更类似 `memcpy`,而非一个 HTTP 请求时,管理 contexts 和内部记录带来的额外开销会导致令人厌烦的性能问题。 + +## 那 `Close()` 方法呢 + +正如上面提到的,`Close()` 可以作为一种机制来取消挂起的 `Read()`。然而,仍然有一些场景适合不关闭底层 reader 直接抢占读取。 + +比如说,考虑从 `os.Stdin` 读取的程序。如果你想使用 `Close()` 来抢占一个执行中的从 `stdin` 读取操作,那么你将需要关闭 `stdin`。这并不是很好,因为一旦关闭 `stdin`,你就无法再重新打开并再次读取。 + +由于 `Close()` 通常时一次性操作,在通常情况下,它并不是抢占读取的理想选择。`Close()` 最好被归类为清理方法,而非抢占信号。 + +## 你如何解决这个问题 + +考虑到上述注意事项,仍然有办法有效地取消进行中的读取。那并没有绕开 `io.Reader` 接口的限制,但是你可以为接口的使用者铺平道路。 + +```go +type CancelableReader struct { + ctx context.Context + data chan []byte + err error + r io.Reader +} + +func (c *CancelableReader) begin() { + buf := make([]byte, 1024) + for { + n, err := c.r.Read(buf) + if err != nil { + c.err = err + close(c.data) + return + } + tmp := make([]byte, n) + copy(tmp, buf[:n]) + c.data <- tmp + } +} + +func (c *CancelableReader) Read(p []byte) (int, error) { + select { + case <-c.ctx.Done(): + return 0, c.ctx.Err() + case d, ok := <-c.data: + if !ok { + return 0, c.err + } + copy(p, d) + return len(d), nil + } +} + +func New(ctx context.Context, r io.Reader) *CancelableReader { + c := &CancelableReader{ + r: r, + ctx: ctx, + data: make(chan []byte), + } + Go c.begin() + return c +} +``` + +上述是 `io.Reader` 接口的包装,它的构造器中包含了 `context.Context`。当 context 被取消,任何进行中的读取操作都会立即返回。稍微调整一下,上述方法也可以很好的适用于 `io.ReadCloser`。 + +`CancelableReader` 包装器上有一个*巨大的星号*:它仍然存在 Goroutine 泄漏。如果底层的 `io.Reader` 永远不返回,那么 `begin()` 中的 Goroutine 将永远不会被清理。 + +至少,使用这种方法可以更清楚的知道泄漏发生的位置,你可以在 struct 上存储一些额外的状态来追踪 Goroutine 是否结束。或许,你可以将这些 `CancelableReader` 组成一个池,并在读取全部完成时回收它们。 + +--- + +这并不像是个令人满意的结论。我很好奇是否有更好的方法来抢占阅读。或许答案是“增加足够多层的包装器,直到问题消失”?或许答案是“了解底层 reader 的实现”?- 例如,在 `stdin` 上使用 [syscall.SetNonblock](https://golang.org/pkg/syscall/#SetNonblock)。 + +如果有人有更好的办法,我很想听听!😃 + +--- + + +1. 我喜欢 [Go 的简单性](https://benjamincongdon.me/blog/2019/11/11/The-Value-in-Gos-Simplicity/)的一点是,它经常使你陷入这些表面的存在性问题,比如“从阻塞接口读取意味着什么?”😜 + + +--- + +via: + +作者:[Ben Congdon](https://benjamincongdon.me/) +译者:[DoubleLuck](https://github.com/doubleluck) +校对:[unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200425-Inlining-optimisations-Go.md b/published/tech/20200425-Inlining-optimisations-Go.md new file mode 100644 index 000000000..ed82f2cb1 --- /dev/null +++ b/published/tech/20200425-Inlining-optimisations-Go.md @@ -0,0 +1,177 @@ +首发于:https://studygolang.com/articles/28335 + +# Go 中的内联优化 + +本文讨论 Go 编译器是如何实现内联的以及这种优化方法如何影响你的 Go 代码。 + +*请注意:*本文重点讨论 *gc*,实际上是 [golang.org](https://github.com/golang/go) 的 Go 编译器。讨论到的概念可以广泛用于其他 Go 编译器,如 gccgo 和 llgo,但它们在实现方式和功能上可能有所差异。 + +## 内联是什么? + +内联就是把简短的函数在调用它的地方展开。在计算机发展历程的早期,这个优化是由程序员手动实现的。现在,内联已经成为编译过程中自动实现的基本优化过程的其中一步。 + +## 为什么内联很重要? + +有两个原因。第一个是它消除了函数调用本身的开销。第二个是它使得编译器能更高效地执行其他的优化策略。 + +### 函数调用的开销 + +在任何语言中,调用一个函数 [1](https://dave.cheney.net/2020/04/25/inlining-optimisations-in-go#easy-footnote-bottom-1-4053) 都会有消耗。把参数编组进寄存器或放入栈中(取决于 ABI),在返回结果时倒序取出时会有开销。引入一次函数调用会导致程序计数器从指令流的一点跳到另一点,这可能导致管道阻塞。函数内部通常有前置处理,需要为函数执行准备新的栈帧,还有与前置相似的后续处理,需要在返回给调用方之前释放栈帧空间。 + +在 Go 中函数调用会消耗额外的资源来支持栈的动态增长。在进入函数时,goroutine 可用的栈空间与函数需要的空间大小相等。如果可用空间不同,前置处理就会跳到把数据复制到一块新的、更大的空间的运行时逻辑,而这会导致栈空间变大。当这个复制完成后,运行时跳回到原来的函数入口,再执行栈空间检查,函数调用继续执行。这种方式下,goroutine 开始时可以申请很小的栈空间,在有需要时再申请更大的空间。[2](https://dave.cheney.net/2020/04/25/inlining-optimisations-in-go#easy-footnote-bottom-2-4053) + +这个检查消耗很小 — 只有几个指令 — 而且由于 Goroutine 是成几何级数增长的,因此这个检查很少失败。这样,现代处理器的分支预测单元会通过假定检查肯定会成功来隐藏栈空间检查的消耗。当处理器预测错了栈空间检查,必须要抛弃它推测性执行的操作时,与为了增加 Goroutine 的栈空间运行时所需的操作消耗的资源相比,管道阻塞的代价更小。 + +虽然现代处理器可以用预测性执行技术优化每次函数调用中的泛型和 Go 特定的元素的开销,但那些开销不能被完全消除,因此在每次函数调用执行必要的工作过程中都会有性能消耗。一次函数调用本身的开销是固定的,与更大的函数相比,调用小函数的代价更大,因为在每次调用过程中它们做的有用的工作更少。 + +消除这些开销的方法必须是要消除函数调用本身,Go 的编译器就是这么做的,在某些条件下通过用函数的内容来替换函数调用来实现。这个过程被称为*内联*,因为它在函数调用处把函数体展开了。 + +### 改进的优化机会 + +Cliff Click 博士把内联描述为现代编译器做的优化措施,像常量传播(译注:此处作者笔误,原文为 constant proportion,修正为 constant propagation)和死码消除一样,都是编译器的基本优化方法。实际上,内联可以让编译器看得更深,使编译器可以观察调用的特定函数的上下文内容,可以看到能继续简化或彻底消除的逻辑。由于可以递归地执行内联,因此不仅可以在每个独立的函数上下文处进行这种优化,也可以在整个函数调用链中进行。 + +## 实践中的内联 + +下面这个例子可以演示内联的影响: + +```go +package main + +import "testing" + +//go:noinline +func max(a, b int) int { + if a > b { + return a + } + return b +} + +var Result int + +func BenchmarkMax(b *testing.B) { + var r int + for i := 0; i < b.N; i++ { + r = max(-1, i) + } + Result = r +} +``` + +运行这个基准,会得到如下结果:[3](https://dave.cheney.net/2020/04/25/inlining-optimisations-in-go#easy-footnote-bottom-3-4053) + +```bash +% Go test -bench=. +BenchmarkMax-4 530687617 2.24 ns/op +``` + +在我的 2015 MacBook Air 上 `max(-1, i)` 的耗时约为 2.24 纳秒。现在去掉 `//go:noinline` 编译指令,再看下结果: + +```bash +% Go test -bench=. +BenchmarkMax-4 1000000000 0.514 ns/op +``` + +从 2.24 纳秒降到了 0.51 纳秒,或者从 `benchstat` 的结果可以看出,有 78% 的提升。 + +```bash +% benchstat {old,new}.txt +name old time/op new time/op delta +Max-4 2.21ns ± 1% 0.49ns ± 6% -77.96% (p=0.000 n=18+19) +``` + +这个提升是从哪儿来的呢? + +首先,移除掉函数调用以及与之关联的前置处理 [4](https://dave.cheney.net/2020/04/25/inlining-optimisations-in-go#easy-footnote-bottom-4-4053) 是主要因素。把 `max` 函数的函数体在调用处展开,减少了处理器执行的指令数量并且消除了一些分支。 + +现在由于编译器优化了 `BenchmarkMax`,因此它可以看到 `max` 函数的内容,进而可以做更多的提升。当 `max` 被内联后,`BenchmarkMax` 呈现给编译器的样子,看起来是这样的: + +```go +func BenchmarkMax(b *testing.B) { + var r int + for i := 0; i < b.N; i++ { + if -1 > i { + r = -1 + } else { + r = i + } + } + Result = r +} +``` + +再运行一次基准,我们看一下手动内联的版本和编译器内联的版本的表现: + +```bash +% benchstat {old,new}.txt +name old time/op new time/op delta +Max-4 2.21ns ± 1% 0.48ns ± 3% -78.14% (p=0.000 n=18+18) +``` + +现在编译器能看到在 `BenchmarkMax` 里内联 `max` 的结果,可以执行以前不能执行的优化措施。例如,编译器注意到 `i` 初始值为 `0`,仅做自增操作,因此所有与 `i` 的比较都可以假定 `i` 不是负值。这样条件表达式 `-1 > i` 永远不是 true。[5](https://dave.cheney.net/2020/04/25/inlining-optimisations-in-go#easy-footnote-bottom-5-4053) + +证明了 `-1 > i` 永远不为 true 后,编译器可以把代码简化为: + +```go +func BenchmarkMax(b *testing.B) { + var r int + for i := 0; i < b.N; i++ { + if false { + r = -1 + } else { + r = i + } + } + Result = r +} +``` + +并且因为分支里是个常量,编译器可以通过下面的方式移除不会走到的分支: + +```go +func BenchmarkMax(b *testing.B) { + var r int + for i := 0; i < b.N; i++ { + r = i + } + Result = r +} +``` + +这样,通过内联和由内联解锁的优化过程,编译器把表达式 `r = max(-1, i))` 简化为 `r = i`。 + +## 内联的限制 + +本文中我论述的内联称作*叶子*内联;把函数调用栈中最底层的函数在调用它的函数处展开的行为。内联是个递归的过程,当把函数内联到调用它的函数 A 处后,编译器会把内联后的结果代码再内联到 A 的调用方,这样持续内联下去。例如,下面的代码: + +```go +func BenchmarkMaxMaxMax(b *testing.B) { + var r int + for i := 0; i < b.N; i++ { + r = max(max(-1, i), max(0, i)) + } + Result = r +} +``` + +与之前的例子中的代码运行速度一样快,因为编译器可以对上面的代码重复地进行内联,也把代码简化到 `r = i` 表达式。 + +下一篇文章中,我会论述当 Go 编译器想要内联函数调用栈中间的某个函数时选用的另一种内联策略。最后我会论述编译器为了内联代码准备好要达到的极限,这个极限 Go 现在的能力还达不到。 + +文中的引用说明: + +1. 在 Go 中,一个方法就是一个有预先定义的形参和接受者的函数。假设这个方法不是通过接口调用的,调用一个无消耗的函数所消耗的代价与引入一个方法是相同的。 +2. 在 Go 1.14 以前,栈检查的前置处理也被 gc 用于 STW,通过把所有活跃的 Goroutine 栈空间设为 0,来强制它们切换为下一次函数调用时的运行时状态。这个机制[最近被替换][https://github.com/golang/proposal/blob/master/design/24543-non-cooperative-preemption.md]为一种新机制,新机制下运行时可以不用等 Goroutine 进行函数调用就可以暂停 goroutine。[][9] +3. 我用 `//go:noinline` 编译指令来阻止编译器内联 `max`。这是因为我想把内联 `max` 的影响与其他影响隔离开,而不是用 `-gcflags='-l -N'` 选项在全局范围内禁止优化。关于 `//go:` 注释在[这篇文章][https://dave.cheney.net/2018/01/08/gos-hidden-pragmas]中详细论述。 +4. 你可以自己通过比较 `go test -bench=. -gcflags=-S` 有无 `//go:noinline` 注释时的不同结果来验证一下。 +5. 你可以用 `-gcflags=-d=ssa/prove/debug=on` 选项来自己验证一下。 + +--- + +via: https://dave.cheney.net/2020/04/25/inlining-optimisations-in-go + +作者:[Dave Cheney](https://dave.cheney.net/) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200429-go-ordering-in-select-statements.md b/published/tech/20200429-go-ordering-in-select-statements.md new file mode 100644 index 000000000..ee65d2eac --- /dev/null +++ b/published/tech/20200429-go-ordering-in-select-statements.md @@ -0,0 +1,174 @@ +首发于:https://studygolang.com/articles/28990 + +# Go: Select 语句的执行顺序 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-ordering-in-select-statements/20200429220520.png) + +> 本文基于 Go 1.14 + +`select` 允许在一个 Goroutine 中管理多个 channel。但是,当所有 channel 同时就绪的时候,go 需要在其中选择一个执行。此外,go 还需要处理没有 channel 就绪的情况,我们先从就绪的 channel 开始。 + +## 顺序 + +`select` 不会按照任何规则或者优先级选择就绪的 channel。go 标准库在每次执行的时候,都会将他们顺序打乱,也就是说不能保证任何顺序。 + +看一个有三个就绪的 channel 的例子: + +``` go +func main() { + a := make(chan bool, 100) + b := make(chan bool, 100) + c := make(chan bool, 100) + for i := 0; i < 10; i++ { + a <- true + b <- true + c <- true + } + for i := 0; i < 10; i++ { + select { + case <-a: + print("< a") + + case <-b: + print("< b") + + case <-c: + print("< c") + + default: + print("< default") + } + } +} +``` + +这三个 channel 的缓冲区都填满了,使得 select 选择时不会堵塞。下面是程序的输出: + +```bash +< b< a< a< b< c< c< c< a< b< b +``` + +在 select 的每次迭代中,case 都会被打乱: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-ordering-in-select-statements/20200429223415.png) + +由于 Go 不会删除重复的 channel,所以可以使用多次添加 case 来影响结果,代码如下: + +```go +func main() { + a := make(chan bool, 100) + b := make(chan bool, 100) + c := make(chan bool, 100) + for i := 0; i < 10; i++ { + a <- true + b <- true + c <- true + } + for i := 0; i < 10; i++ { + select { + case <-a: + print("< a") + case <-a: + print("< a") + case <-a: + print("< a") + case <-a: + print("< a") + case <-a: + print("< a") + case <-a: + print("< a") + case <-a: + print("< a") + + case <-b: + print("< b") + + case <-c: + print("< c") + + default: + print("< default") + } + } +} +``` + +输出的结果: + +```shell +< c< a< b< a< b< a< a< c< a< a +``` + +当所有 channel 同时准备就绪时,有 80%的机会选择通道 a。下面来看一下 channel 未就绪的情况。 + +## 没有就绪 channels + +`select` 运行时,如果没有一个 case channel 就绪,那么他就会运行 `default:`,如果 `select` 中没有写 default,那么他就进入等待状态,如下面这个例子 + +```go +func main() { + a := make(chan bool, 100) + b := make(chan bool, 100) + Go func() { + time.Sleep(time.Minute) + for i := 0; i < 10; i++ { + a <- true + b <- true + } + }() + + for i := 0; i < 10; i++ { + select { + case <-a: + print("< a") + case <-b: + print("< b") + } + } +} +``` + +上面那个例子中,将在一分钟后打印结果。`select` 阻塞在 channel 上。这种情况下,处理 `select` 的函数将会订阅所有 channel 并且等待,下面是一个 goroutine#7 在 select 中等待的示例,其中另一个 goroutine#4 也在等待 channel: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-ordering-in-select-statements/20200429225528.png) + +Goroutine(G7)订阅所有频道并在列表末尾等待。 如果 channel 发送了一条消息,channel 将通知已在等待该消息的另一个 Goroutine。一旦收到通知,`select` 将取消订阅所有 channel,并且返回到代码运行. + +更多关于 channel 与等待队列的信息,请查看作者另外一篇文章[*Go: 带缓冲和不带缓冲的 Channels*](https://studygolang.com/articles/23538)。 + +上面介绍的逻辑,都是针对于有两个或者以上的活动的 channel,实际上如果只有一个活动的 channel,Go 乐意简化 select。 + +## 简化 + +如果只有一个 case 加上一个 default,例子: + +```go +func main() { + t:= time.NewTicker(time.Second) + for { + select { + case <-t.C: + print("1 second ") + default: + print("default branch") + } + } +} +``` + +这种情况下。Go 会以非阻塞模式读取 channel 的操作替换 select 语句。如果 channel 在缓冲区中没有任何值,或者发送方准备发送消息,将会运行 default。就像下面这张图: + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-ordering-in-select-statements/20200429231908.png) + +如果没有 default,则 Go 通过阻塞 channel 的操作方式重写 select 语句。 + +--- + +via: https://medium.com/a-journey-with-go/go-ordering-in-select-statements-fd0ff80fd8d6 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[yixiao9206](https://github.com/yixiao9206) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200501-Go-Asynchronous-Preemption.md b/published/tech/20200501-Go-Asynchronous-Preemption.md new file mode 100644 index 000000000..116770027 --- /dev/null +++ b/published/tech/20200501-Go-Asynchronous-Preemption.md @@ -0,0 +1,70 @@ +首发于:https://studygolang.com/articles/28460 + +# Go:异步抢占 + +![Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200501-Go-Asynchronous-Preemption/00.png) + +ℹ️ 本文基于 Go 1.14。 + +抢占是调度器的重要部分,基于抢占调度器可以在各个协程中分配运行的时间。实际上,如果没有抢占机制,一个长时间占用 CPU 的协程会阻塞其他的协程被调度。1.14 版本引入了一项新的异步抢占的技术,赋予了调度器更大的能力和控制力。 + +*我推荐你阅读我的文章[”Go:协程和抢占“](https://medium.com/a-journey-with-go/go-goroutine-and-preemption-d6bc2aa2f4b7)来了解更多之前的特性和它的弊端。* + +## 工作流 + +我们以一个需要抢占的例子来开始。下面一段代码开启了几个协程,在几个循环中没有其他的函数调用,意味着调度器没有机会抢占它们: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200501-Go-Asynchronous-Preemption/01.png) + +然而,当把这个程序的追踪过程可视化后,我们清晰地看到了协程间的抢占和切换: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200501-Go-Asynchronous-Preemption/02.png) + +我们还可以看到表示协程的每个块儿的长度都相等。所有的协程运行时间相同(约 10 到 20 毫秒)。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200501-Go-Asynchronous-Preemption/03.png) + +异步抢占是基于一个时间条件触发的。当一个协程运行超过 10ms 时,Go 会尝试抢占它。 + +抢占是由线程 `sysmon` 初始化的,该线程专门用于监控包括长时间运行的协程在内的运行时。当某个协程被检测到运行超过 10ms 后,`sysmon` 向当前的线程发出一个抢占信号。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200501-Go-Asynchronous-Preemption/04.png) + +之后,当信息被信号处理器接收到时,线程中断当前的操作来处理信号,因此不会再运行当前的协程,在我们的例子中是 `G7`。取而代之的是,`gsignal` 被调度为管理发送来的信号。当它发现它是一个抢占指令后,在程序处理信号后恢复时它准备好指令来中止当前的协程。下面是这第二个阶段的示意图: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200501-Go-Asynchronous-Preemption/05.png) + +*如果你想了解更多关于 `gsignal` 的信息,我推荐你读一下我的文章[”Go:gsignal,信号的掌控者“](https://medium.com/a-journey-with-go/go-gsignal-master-of-signals-329f7ff39391)。* + +## 实现 + +我们在被选中的信号 `SIGURG` 中第一次看到了实现的细节。这个选择在提案[”提案:非合作式协程抢占“](https://github.com/golang/proposal/blob/master/design/24543-non-cooperative-preemption.md)中有详细的解释: + +> - 它应该是调试者默认传递过来的一个信号。 +> - 它不应该是 Go/C 混合二进制中 libc 内部使用的信号。 +> - 它应该是一个可以伪造而没有其他后果的信号。 +> - 我们需要在没有实时信号时与平台打交道。 + +然后,当信号被注入和接收时,Go 需要一种在程序恢复时能终止当前协程的方式。为了实现这个过程,Go 会把一条指令推进程序计数器,这样看起来运行中的程序调用了运行时的函数。该函数暂停了协程并把它交给了调度器,调度器之后还会运行其他的协程。 + +*我们应该注意到 Go 不能做到在任何地方终止程序;当前的指令必须是一个安全点。例如,如果程序现在正在调用运行时,那么抢占协程并不安全,因为运行时很多函数不应该被抢占。* + +这个新的抢占机制也让垃圾回收器受益,可以用更高效的方式终止所有的协程。诚然,STW 现在非常容易,Go 仅需要向所有运行的线程发出一个信号就可以了。下面是垃圾回收器运行时的一个例子: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200501-Go-Asynchronous-Preemption/06.png) + +然后,所有的线程都接收到这个信号,在垃圾回收器重新开启全局之前会暂停执行。 + +*如果你想了解更多关于 STW 的信息,我建议你阅读我的文章[”Go:Go 怎样实现 STW?“](https://medium.com/a-journey-with-go/go-how-does-go-stop-the-world-1ffab8bc8846)。* + +最后,这个特性被封装在一个参数中,你可以用这个参数关闭异步抢占。你可以用 `GODEBUG=asyncpreemptoff=1` 来运行你的程序,如果你因为升级到了 Go 1.14 发现了不正常的现象就可以调试你的程序,或者观察你的程序有无异步抢占时的不同表现。 + +--- + +via: https://medium.com/a-journey-with-go/go-asynchronous-preemption-b5194227371c + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200503-slices-from-the-ground-up.md b/published/tech/20200503-slices-from-the-ground-up.md new file mode 100644 index 000000000..0f65b799b --- /dev/null +++ b/published/tech/20200503-slices-from-the-ground-up.md @@ -0,0 +1,210 @@ +首发于:https://studygolang.com/articles/28976 + +# 重新学习 slice By Dave Cheney + +## 数组 Arrays + +每次讨论到 Go 的切片问题,都会从这个变量是不是切片开始,换句话说,就是 Go 的序列类型,在 Go 中,数组有两种关联属性。 + +1. 数组拥有固定的大小;`[5]int` 即表明是一个有 5 个 `int` 的数组,又与 `[3]int` 相区分开。 +2. 他们是值类型。思考下面这个例子。 + +```go +package main +import "fmt" +func main() { + var a [5]int + b := a + b[2] = 7 + fmt.Println(a, b) // prints [0 0 0 0 0] [0 0 7 0 0] +} +``` + +声明语句 `b:=a` 声明一个新变量 `b`,一个 `[5]int` 的数据类型,并把 `a` 的内容拷贝到 `b` 中,更改 `b` 中的值并不会对 `a` 中内容造成影响,因为 `a` 和 `b` 是独立的。 + +*作者注:这并不是数组的特殊属性,在 Go 中,每次分配其实都是副本值传递*。 + +## 切片 slices + +Go 的切片类型与数组类型有两个不同的地方: + +1. 切片其实没有固定长度,一个切片的长度没有被声明为其类型的一部分,而是被保留在切片结构本身中并且可以通过内置函数 `len` 来重置他。 +2. 用一个切片赋值给另一个切片并不会创建前一个切片的内容副本,因为切片类型没有直接拥有它的内容,而是拥有一个指针,而这个指针指向切片下方的数组,数组内的元素才是切片的内容。 + +*作者注:这有时也被成为后台数组(backing arrays)*。 + +由于第二个特性,两个数组可以同时分享一个后台数组,思考以下例子: + +### 例 1:对切片再切片 + +```go +package main + +import "fmt" + +func main() { + var a = []int{1,2,3,4,5} + b := a[2:] + b[0] = 0 + fmt.Println(a, b) // prints [1 2 0 4 5] [0 4 5] +} +``` + +*译者注:`a` 也是个切片,而不是数组,只要 `[]` 内没有数字,就是切片,在本例中,`a` 是数组{1,2,3,4,5}的一个切片*。 + +在这个例子中,`a` 和 `b` 共同分享同一个后台数组,尽管 `b` 开始的偏移量和和长度都不同于 `a`,所以底层数组的更改会用同时影响到 `a` 和 `b`。 + +### 例 2:传切片变量给函数 + +```go +package main + +import "fmt" + +func negate(s []int) { + for i := range s { + s[i] = -s[i] + } +} + +func main() { + var a = []int{1, 2, 3, 4, 5} + negate(a) + fmt.Println(a) // prints [-1 -2 -3 -4 -5] +} +``` + +在例 2 中,`a` 被传值给 `negate` 函数作为形式参数 `s`,函数遍历 `s` 中的元素,将他们转为相反数,尽管 `negate` 函数没有返回任何值或者用任何方式去在 `main` 中访问 `a`,但是 `a` 中的内容还是被 `negate` 所修改了。大多程序员程序员对 Go 中的切片与数组有一个直观的了解,应为这样的概念在其他语言中也有,例如: + +### Python 重写例 1 + +```python +Python 2.7.10 (default, Feb 7 2017, 00:08:15) +[GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.34)] on darwin +Type "help", "copyright", "credits" or "license" for more information. +>>> a = [1,2,3,4,5] +>>> b = a +>>> b[2] = 0 +>>> a +[1, 2, 0, 4, 5] + +``` + +### Ruby + +```ruby +irb(main):001:0> a = [1,2,3,4,5] +=> [1, 2, 3, 4, 5] +irb(main):002:0> b = a +=> [1, 2, 3, 4, 5] +irb(main):003:0> b[2] = 0 +=> 0 +irb(main):004:0> a +=> [1, 2, 0, 4, 5] +``` + +## slice Header + +想要理解 slice 是如何做到本身是一个类,并且又是一个指针的话,就得理解 [slice 的底层结构](https://golang.org/pkg/reflect/#sliceHeader)。 + +`slicet Header` 看起来就像这样: + +![Slice Header.png](https://raw.githubusercontent.com/studygolang/gctt-images/master/slice-from-the-ground-up/Slice%20Header.png) + +```go +package runtime + +type slicece struct { + ptr unsafe.Pointer + len int + cap int +} +``` + +它不像[`map` 和 `chan` 类型](https://dave.cheney.net/2017/04/30/if-a-map-isnt-a-reference-variable-what-is-it),它们是引用类型,而切片是值类型,在赋值或者作为参数传递给函数的时候会复制。 + +为了说明这些,程序员可以直观的将 `square()` 的形参理解为 `main` 中 `v` 的副本。 + +```go +package main + +import "fmt" + +func square(v int) { + v = v * v +} + +func main() { + v := 3 + square(v) + fmt.Println(v) // prints 3, not 9 +} +``` + +`square` 的操作并不会对原本的 `v` 有任何影响,形参可以理解为所传值的单独拷贝。 + +```go +package main + +import "fmt" + +func double(s []int) { + s = append(s, s...) +} + +func main() { + s := []int{1, 2, 3} + double(s) + fmt.Println(s, len(s)) // prints [1 2 3] 3 +} +``` + +Go 的 slice 变量稍微有点不一样的特性就是他是作为值传递的,不仅仅是一个指针,90% 的时间当我们在 Go 中声明一个结构体的时候,你都会传递一个结构体指针。slice 的值传递很不常见,我能想到的另一个值传递的结构体为 `time.time`。 + +*作者注:当结构体实现了某个接口的时候,那么传递指针的概率这接近 100%*。 + +正是这种异常的,将 slice 作为值传递,而不是指针传递,这引起了 Go 程序员的混乱思考,但是只要记住:当我们赋值,截取,传递或者返回一个切片的时候,你只是在创建一个 slice Header 结构体,这个结构体有着三个字段:指向后台数组的指针,当前长度 `len`,容量 `cap`。 + +## 总结 + +写一个用切片作为栈的例子。 + +```go +package main + +import "fmt" + +func f(s []string, level int) { + if level > 5 { + return + } + s = append(s, fmt.Sprint(level)) + f(s, level+1) + fmt.Println("level:", level, "slicece:", s) +} + +func main() { + f(nil, 0) +} +``` + +从 `main` 开始,我们传递一个空值给 `f` 作为 `level 0`,在 `f` 内部,我们添加当前的 `level` 给 `s`,一旦 `level` 大于 5,`f` 就会执行 return 语句 ,打印 `s` 的副本。 + +```bash +level: 5 slicece: [0 1 2 3 4 5] +level: 4 slicece: [0 1 2 3 4] +level: 3 slicece: [0 1 2 3] +level: 2 slicece: [0 1 2] +level: 1 slicece: [0 1] +level: 0 slicece: [0] +``` + +--- + +via: https://dave.cheney.net/2018/07/12/slices-from-the-ground-up + +作者:[Dave Cheney](https://dave.cheney.net/about) +译者:[Jun10ng](https://github.com/Jun10ng) +校对:[unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200503-there-is-no-pass-by-reference-in-go.md b/published/tech/20200503-there-is-no-pass-by-reference-in-go.md new file mode 100644 index 000000000..f1d1de374 --- /dev/null +++ b/published/tech/20200503-there-is-no-pass-by-reference-in-go.md @@ -0,0 +1,135 @@ +首发于:https://studygolang.com/articles/28977 + +# Go 中没有引用传递 + +先说清楚,在 Go 中没有引用变量,所以更不存在什么引用传值了。 + +## 什么是引用变量 + +在类 C++ 语言中,你可以声明一个别名,给一个变量安上一个其他名字,我们把这称为引用变量。 + +```c +#include + +int main() { + int a = 10; + int &b = a; + int &c = b; + + printf("%p %p %p\n", &a, &b, &c); // 0x7ffe114f0b14 0x7ffe114f0b14 0x7ffe114f0b14 + return 0; +} +``` + +你可以看到 `a`,`b`,`c` 都指向同一块内存地址,三者的值相同,当你要在不同范围内声明引用变量(即函数调用)时,此功能很有用。 + +## Go 中不存在引用变量 + +与 C++ 不同的是,Go 中的每一个变量都有着独一无二的内存地址。 + +```go +package main + +import "fmt" + +func main() { + var a, b, c int + fmt.Println(&a, &b, &c) // 0x1040a124 0x1040a128 0x1040a12c +} +``` + +你不可能在 Go 程序中找到两个变量共享一块内存,但是可以让两个变量指向同一个内存。 + +```go +package main + +import "fmt" + +func main() { + var a int + var b, c = &a, &a + fmt.Println(b, c) // 0x1040a124 0x1040a124 + fmt.Println(&b, &c) // 0x1040c108 0x1040c110 +} +``` + +在这个例子中,`b` 和 `c` 拥有 `a` 的地址,但是 `b` 和 `c` 这两个变量却被存储在不同的内存地址中,更改 `b` 的值并不会影响到 `c`。 + +## `map` 和 `channel` 是引用吗 + +不是,map 和 channel 都不是引用,如果他们是的话,下面这个例子就会输出 `false` + +```go +package main + +import "fmt" + +func fn(m map[int]int) { + m = make(map[int]int) +} + +func main() { + var m map[int]int + fn(m) + fmt.Println(m == nil) +} +``` + +如果是引用变量的话,`main` 中的 `m` 被传到 `fn` 中,那么经过函数的处理 `m` 应该已经被初始化了才对,但是可以看出 `fn` 的处理对 `m` 并没有影响,所以 `map` 也不是引用。 + +`map` 是一个指向 `runtime.hmap` 结构的指针,如果你还有疑问的话,请继续阅读下去。 + +## map 类型是什么 + +当我们这样声明的时候。 + +```go +m := make(map[int]int) +``` + +编译器将其替换为调用 map.go/[makemap](https://golang.org/src/runtime/map.go?h=makemap%28%29) + +```go +// makemap implements Go map creation for make(map[k]v, hint). +// If the compiler has determined that the map or the first bucket +// can be created on the stack, h and/or bucket may be non-nil. +// If h != nil, the map can be created directly in h. +// If h.buckets != nil, bucket pointed to can be used as the first bucket. +func makemap(t *maptype, hint int, h *hmap)*hmap +``` + +可以看到,`makemap` 函数返回 `*hmap`,一个指向[hmap](https://golang.org/src/runtime/map.go?h=hmap#L115)结构的指针,我们可以从 Go 源码中看到这些,除此之外,我们还可以证明 map 值的大小和 `uintptr` 一样。 + +```go +package main +import ( + "fmt" + "unsafe" +) + +func main() { + var m map[int]int + var p uintptr + fmt.Println(unsafe.Sizeof(m), unsafe.Sizeof(p)) // 8 8 (linux/amd64) +} +``` + +## 如果 map 是指针的话,它不应该返回 *map[key]value 吗 + +这是个好问题,为什么表达式 `make(map[int]int)` 返回一个 map[int]int 类型的结构?不应该返回 `*map[int]int` 吗? + +Ian Taylor [在这个回答](https://groups.google.com/forum/#!msg/golang-nuts/SjuhSYDITm4/jnrp7rRxDQAJ)中说: + +> In the very early days what we call maps now were written as pointers, so you wrote *map[int]int. We moved away from that when we realized that no one ever wrote `map` without writing `*map`. + +所以说,Go 把 `*map[int]int` 重命名为 `map[int]int` + +--- + +via: https://dave.cheney.net/2017/04/29/there-is-no-pass-by-reference-in-go + +作者:[Dave Cheney](https://dave.cheney.net/about) +译者:[Jun10ng](https://github.com/Jun10ng) +校对:[unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200505-Go-Samples-Collection-with-pprof.md b/published/tech/20200505-Go-Samples-Collection-with-pprof.md new file mode 100644 index 000000000..75c5d8178 --- /dev/null +++ b/published/tech/20200505-Go-Samples-Collection-with-pprof.md @@ -0,0 +1,97 @@ +首发于:https://studygolang.com/articles/28982 + +# Go:使用 pprof 收集样品数据 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200505-Go-Samples-Collection-with-pprof/00.png) + +> Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French. + +ℹ️ *本文基于 Go 1.13。* + +`pprof` 是用于分析诸如 CPU 或 内存分配等 profile 数据的工具。分析程序的 profile 数据需要收集运行时的数据用来在之后统计和生成画像。我们现在来研究下数据收集的工作流以及怎么样去调整它。 + +## 工作流 + +`pprof` 以一个固定的时间间隔基础来收集数据,这个时间间隔是以每秒的收集器的个数定义的。默认参数是 `100`,即 `pprof` 每秒收集 100 次数据,例如,每 10 毫秒收集一次。 + +可以通过调用 `StartCPUProfile` 来启动 `pprof`。 + +```go +func main() { + f, _ := os.Create(`cpu.prof`) + if err != nil { + log.Fatal(err) + } + pprof.StartCPUProfile(f) + defer pprof.StopCPUProfile() + + ... +} +``` + +这个过程会在运行的线程中自动设置一个定时器(下图中 `M` 表示线程),让 Go 定期地收集 profile 数据。下面是第一个示意图: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200505-Go-Samples-Collection-with-pprof/01.png) + +*想了解更多关于 MPG 调度模型的信息,我推荐你阅读我的文章”[Go:协程,操作系统线程和 CPU 管理](https://studygolang.com/articles/25292)。“* + +然而,目前为止 `pprof` 仅在收集当前运行的线程的 profile 信息。当 Go 调度器想调度一个协程运行在某个线程上时,这个线程也可以实时被追踪。下面是更新后的示意图: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200505-Go-Samples-Collection-with-pprof/02.png) + +*想了解更多关于 Go 调度器的信息,我建议你阅读我的文章”[Go: g0,特殊的协程](https://medium.com/a-journey-with-go/go-g0-special-goroutine-8c778c6704d8)“。* + +之后,profile 数据会在定义好的每个时间间隔到期后定期地被 dump 到一个缓冲区: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200505-Go-Samples-Collection-with-pprof/03.png) + +数据实际上是由 `gsignal` 进行 dump 的,这个协程是用来处理发来的信号的。实际上,在每个时间间隔到期后定时器会发送信号。 + +*想了解更多关于信号和 `gsignal` 的信息,我推荐你阅读我的文章”[Go:gsignal,信号的掌控者](https://studygolang.com/articles/28974)“。* + +## 基于信号的机制 + +在每个线程上创建的定时器是由 `settimer` 方法和时间间隔定时器 `ITIMER_PROF` 管理的。时间计数仅在系统代表这个处理运行时才会减少,这样就能确保 profile 数据的准确。 + +当经过了定义的时间间隔后,定时器发送一个 `SIGPROF` 信号,这个信号会被 Go 截获,profile 数据会被 dump 到缓冲区。频率可以通过调用 `runtime` 包里的 `SetCPUProfileRate` 函数进行配置。这个配置操作需要在启动 profiler 之前完成: + +```go +func main() { + f, err := os.Create(`cpu.prof`) + if err != nil { + log.Fatal(err) + } + + runtime.SetCPUProfileRate(10) + pprof.StartCPUProfile(f) + defer pprof.StopCPUProfile() + + ... +} +``` + +profile 数据采集率只能定义一次,在启动 profiler 时 `pprof` 定义它。在启动 profiler 之前调用该方法会导致 `pprof` 忽略默认值。 + +然而,默认值能满足大部分场景中。包的文档中有对此的详细解释: + +> 100 Hz 是合理的值:既能满足产出有用数据的频率需求,又不至于过快而使系统 hang 住。 + +当然,更高的频率值似乎也可以,因为它可能会导致一些 [`SIGPROF` 事件](https://github.com/golang/go/issues/35057)从 `250` 或更高的值降下来。`pprof` 文档也陈述了怎样让它表现得更好: + +> *[…]* 实践中操作系统不能以比 500 Hz 更高的频率触发信号 + +## 收集数据 + +现在所有的定时器都已经设置完了。当数据被 dump 到缓冲区后,`pprof` 需要一种把所有数据收集起来生成报告的方法。这个处理过程是在独立的协程中进行的,每 100 毫秒收集和格式化一次数据。至于收集到的数据,Go 生成回溯信息,用来找到函数调用关系以及处理它来格式化内联调用。 + +当信息生成过程完成后,例如 profile 数据收集结束后,特定的协程会把报告 dump 到文件,这样数据就可用和完全可视化了。 + +--- + +via: https://medium.com/a-journey-with-go/go-samples-collection-with-pprof-2a63c3e8a142 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200510-Go-How-to-Take-Advantage-of-the-Symbols-Table.md b/published/tech/20200510-Go-How-to-Take-Advantage-of-the-Symbols-Table.md new file mode 100644 index 000000000..995041efa --- /dev/null +++ b/published/tech/20200510-Go-How-to-Take-Advantage-of-the-Symbols-Table.md @@ -0,0 +1,153 @@ +首发于:https://studygolang.com/articles/28991 + +# Go:如何利用符号表 + +![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-hwo-to-take-symbol-table/cover.png) + +> Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French. + +ℹ️ *本文基于 Go 1.13。* + +符号表是由编译器生成和维护的,保存了与程序相关的信息,如函数和全局变量。理解符号表能帮助我们更好地与之交互和利用它。 + +## 符号表 + +Go 编译的所有二进制文件默认内嵌了符号表。我们来举一个例子并研究它。下面是代码: + +```go +var AppVersion string + +func main() { + fmt.Println(`Version: `+AppVersion) +} +``` + +可以通过命令 `nm` 来展示符号表;下面是从 [OSX](https://www.unix.com/man-page/osx/1/nm/) 的结果中提取的部分信息: + +```bash +0000000001177220 b io.ErrUnexpectedEOF +[...] +0000000001177250 b main.AppVersion +00000000010994c0 t main.main +[...] +0000000001170b00 d runtime.buildVersion +``` + +用 `b`(全称为 [bss](https://en.wikipedia.org/wiki/.bss))标记的符号是未初始化的数据。由于我们前面的变量 `AppVersion` 没有初始化,因此它属于 `b`。符号 `d` 表示已初始化的数据,`t` 表示文本符号, 函数属于其中之一。 + +Go 也封装了 `nm` 命令,可以用命令 `go tool nm` 来使用它,也能生成相同的结果: + +```bash +1177220 B io.ErrUnexpectedEOF +[...] +1177250 B main.AppVersion +10994c0 T main.main +[...] +1170b00 D runtime.buildVersion +``` + +当我们知道了暴露的变量的名字后,我们就可以与之交互。 + +## 自定义变量 + +当执行命令 `go build` 时,经过了两个阶段:编译和构建。构建阶段通过编译过程中生成的对象文件生成了一个可执行文件。为了实现这个阶段,构建器把符号表中的符号重定向到最终的二进制文件。 + +在 Go 中我们可以用 `-X` 来重写一个符号定义,`-X` 两个入参:名称和值。下面是承接前面的代码的例子: + +```bash +go build -o ex -ldflags="-X main.AppVersion=v1.0.0" +``` + +构建并运行程序,现在会展示在命令行中定义的版本: + +```bash +Version: v1.0.0 +``` + +运行 `nm` 命令会看到变量已被初始化: + +```bash +1170a90 D main.AppVersion +``` + +投建器赋予了我们重写数据符号(类型 `b` 或 `d`)的能力,现在它们有了 Go 中的 `string` 类型。下面是那些符号列表: + +```bash +D runtime.badsystemstackMsg +D runtime.badmorestackgsignalMsg +D runtime.badmorestackg0Msg +B os.executablePath +B os.initCwd +B syscall.freebsdConfArch +D runtime/internal/sys.DefaultGoroot +B runtime.modinfo +B main.AppVersion +D runtime.buildVersion +``` + +在列表中我们看到了之前的变量和 `DefaultGoroot`,它们都是被构建器自动设置的。我们来看一下运行时这些符号的意义。 + +## 调试 + +符号表的存在是为了确保标识符在使用之前已被声明。这意味着当程序被构建后,它就不再需要这个表了。然而,默认情况下符号表是被嵌入到了 Go 的二进制文件以便调试。我们先来理解如何利用它,之后再来看怎么把它从二进制文件中删除。 + +我会用 `gdb` 来调试。只需要执行 `gdb ex` 就可以加载二进制文件。现在程序已被加载,我们用 `list` 命令来展示源码。下面是输出: + +```bash +GNU gdb (GDB) 8.3.1 +[...] +Reading symbols from ex... +Loading Go Runtime support. +(gdb) list 10 +6 +7 var AppVersion string +8 +9 func main() { +10 fmt.Println(`Version: `+AppVersion) +11 } +12 +(gdb) +``` + +`gdb` 初始化的第一步是读取符号表,为了提取程序中函数和符号的信息。我们现在可以用 `-ldflags=-s` 参数不把符号表编译进程序。下面是新的输出: + +```bash +GNU gdb (GDB) 8.3.1 +[...] +Reading symbols from ex... +(No debugging symbols found in ex) +(gdb) list +No symbol table is loaded. Use the "file" command. +``` + +现在调试器由于找不到符号表不能展示源码。我们应该留意到使用 `-s` 参数去掉了符号表的同时,也去掉了对调试器很有用的 `[DWARF](https://golang.org/pkg/debug/dwarf/)` 调试信息。 + +## 二进制文件的大小 + +去掉符号表后会让调试器用起来很困难,但是会减少二进制文件的大小。下面是有无符号表的二进制文件的区别: + +```bash +2,0M 7 f é v 15:59 ex +1,5M 7 f é v 15:22 ex-s +``` + +没有符号表比有符号表会小 25%。下面是编译 `cmd/go` 源码的另一个例子: + +```bash +14M 7 f é v 16:58 go +11M 7 f é v 16:58 go-s +``` + +这里没有符号表和 DWARF 信息,也小了 25%。 + +*如果你想了解为什么二进制文件会变小,我推荐你阅读 WebKit 团队的 [Benjamin Poulain](https://twitter.com/awfulben) 的文章“[不寻常的加速:二进制文件大小](https://webkit.org/blog/2826/unusual-speed-boost-size-matters/)”。* + +--- + +via: https://medium.com/a-journey-with-go/go-how-to-take-advantage-of-the-symbols-table-360dd52269e5 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200516-Writing-Great-Go-Code.md b/published/tech/20200516-Writing-Great-Go-Code.md new file mode 100644 index 000000000..de58df3cf --- /dev/null +++ b/published/tech/20200516-Writing-Great-Go-Code.md @@ -0,0 +1,270 @@ +首发于:https://studygolang.com/articles/28983 + +# 如何写好 Go 代码 + +我写了多年的 Go 微服务,并在写完两本关于 ([API Foundations in Go](https://leanpub.com/api-foundations) 和 [12 Factor Applications with Docker and Go](https://leanpub.com/12fa-docker-golang)) 主题的书之后,有了一些关于如何写好 Go 代码的想法 + +但首先,我想给阅读这篇文章的读者解释一点。好代码是主观的。你可能对于好代码这一点,有完全不同的想法,而我们可能只对其中一部分意见一致。另一方面,我们可能都没有错,只是我们从两个角度出发,从而选择了不同的方式解决工程问题,并不意味着意见不一致的不是好代码。 + +## 包 + +包很重要,你可能会反对 - 但是如果你在用 Go 写微服务,_你可以将所有代码放在一个包中_。当然,下面也有一些反对的观点: + +1. 将定义的类型放入单独的包中 +2. 维护与传输无关的服务层 +3. 在服务层之外,维护一个数据存储(repository)层 + +我们可以计算一下,一个微服务包的最小数量是 1。如果你有一个大型的微服务,它拥有 websocket 和 http 网关,你最终可能需要 5 个包(类型,数据存储,服务,websocket 和 http 包)。 + +简单的微服务实际上并不关心从数据存储层(repository),或者从传输层(websocket,http)抽离业务逻辑。你可以写简单的代码,转换数据然后响应,也是可以运行的。但是,添加更多的包可以解决一些问题。例如,如果你熟悉 SOLID 原则,`S` 代表单一职责。如果我们拆分成包,这些包就可以是单一职责的。 + +* `types` - 声明一些结构,可能还有一些结构的别名等 +* `repository` - 数据存储层,用来处理存储和读取结构 +* `service` - 服务层,包装存储层的具体业务逻辑实现 +* `http`, `websocket`, ... - 传输层,用来调用服务层 + +当然,根据你使用的情况,还可以进一步细分,例如,可以使用 `types/request` 和 `types/response` 来更好的分隔一些结构。这样就可以拥有 `request.Message` 和 `response.Message` 而不是 `MessageRequest` 和 `MessageResponse`。如果一开始就像这样拆分开,可能会更有意义。 + +但是,为了强调最初的观点 - 如果你只用了这些声明包中的一部分,也没什么影响。像 Docker 这样的大型项目在 `server` 包下只使用了 `types` 包,这是它真正需要的。它使用的其他包(像 errors 包),可能是第三方包。 + +同样需要注意的是,在一个包中,共享正在处理的结构和函数会很容易。如果你有相互依赖的结构,将它们拆分为两个或多个不同的包可能会导致[钻石依赖问题](https://www.well-typed.com/blog/2008/04/the-dreaded-diamond-dependency-problem/)。解决方案也很显然 - 将代码放到一块儿,或者将所有代码放在一个包中。 + +到底选哪一个呢?两种方法都行。如果我非要按规则来的话,将其拆分更多的包可能会使添加新代码变得麻烦。因为你可能要修改这些包才能添加单个 API 调用。如果不是很清楚如何布局,那么在包之间跳转可能会带来一些认知上的开销。在很多情况下,如果项目只有一两个包,阅读代码会更容易。 + +你肯定也不想要太多的小包。 + +## 错误 + +如果是描述性的 Errors 可能是开发人员检查生产问题的唯一工具。这就是为什么我们要优雅地处理错误,要么将它们一直传递到应有程序的某一层,如果错误无法处理,该层就接收错误并记录下来,这一点非常重要。以下是标准库错误类型缺少的一些特性: + +* 错误信息不含堆栈跟踪 +* 不能堆积错误 +* errors 是预实例化的 + +但是,通过使用第三方错误包(我最喜欢的是[pkg/Errors](https://github.com/pkg/errors).))可以帮助解决这些问题。也有其他的第三方错误包,但是这个是 [Dave Cheney](https://dave.cheney.net) (Go 语言大神)编写的,它在错误处理的方式在一定程度上是一种标准。他的文章 [Don’t just check errors, handle them gracefully](https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully) 是推荐必读的。 + +### 错误的堆栈跟踪 + +`pkg/errors` 包在调用 `errors.New` 时,会将上下文(堆栈跟踪)添加到新建的错误中。 + +```bash +users_test.go:34: testing error Hello world + github.com/crusttech/crust/rbac_test.TestUsers + /go/src/github.com/crusttech/crust/rbac/users_test.go:34 + testing.tRunner + /usr/local/go/src/testing/testing.go:777 + runtime.goexit + /usr/local/go/src/runtime/asm_amd64.s:2361 +``` + +考虑到完整的错误信息是 "Hello world",使用 `fmt.Printf` 带有 `%+v` 的参数或者类似的方式来打印少量的上下文 - 对于查找错误的而言,是一件很棒的事。你可以确切知道是哪里创建了错误(关键字)。当然,当涉及到标准库时,`errors` 包和本地 `error` 类型 - 不提供堆栈跟踪。但是,使用 `pkg/errors` 可以很容易地添加一个。例如: + +```go +resp, err := u.Client.Post(fmt.Sprintf(resourcesCreate, resourceID), body) +if err != nil { + return errors.Wrap(err, "request failed") +} +``` + +在上面这个例子中,`pkg/errors` 包将上下文添加到 err 中,加的错误消息(`"request failed"`) 和堆栈跟踪都会抛出来。通过调用 `errors.Wrap` 来添加堆栈跟踪,所以你可以精准追踪到此行的错误。 + +### 堆积错误 + +你的文件系统,数据库,或者其他可能抛出相对不太好描述的错误。例如,Mysql 可能会抛出这种强制错误: + +```bash +ERROR 1146 (42S02): Table 'test.no_such_table' doesn't exist +``` + +这不是很好处理。然而,你可以使用 `errors.Wrap(err,"database aseError")` 在上面堆积新的错误。这样,就可以更好地处理 `"databaseError"` 等。`pkg/errors` 包将在 `causer` 接口后面保留实际的错误信息。 + +```go +type causer interface { + Cause() error +} +``` + +这样,错误堆积在一起,不会丢失任何上下文。附带说一下,mysql 错误是一个[类型错误](https://github.com/go-sql-driver/mysql/blob/a8b7ed4454a6a4f98f85d3ad558cd6d97cec6959/errors.go#L58),其背后包含的不仅仅是错误字符串的信息。这意味着它有可能被处理的更好: + +```go +if driverErr, ok := err.(*mysql.MySQLError); ok { + if driverErr.Number == mysqlerr.ER_ACCESS_DENIED_ERROR { + // Handle the permission-denied error + } +} +``` + +此例子来自于 [this Stack Overflow thread](https://stackoverflow.com/questions/47009068/how-to-get-the-mysql-error-type-in-golang)。 + +### 错误预实例化 + +究竟什么是错误(error)呢?非常简单,错误需要实现下面的接口: + +```go +type error interface { + Error() string +} +``` + +在 `net/http` 的例子中,这个包将几种错误类型暴露为变量,如[文档](https://golang.org/pkg/net/http/#pkg-variables)所示。在这里添加堆栈跟踪是不可能的(Go 不允许对全局 var 声明可执行代码,只能进行类型声明)。其次,如果标准库将堆栈跟踪添加到错误中 - 它不会指向返回错误的位置,而是指向声明变量(全局变量)的位置。 + +这意味着,你仍然需要在后面的代码中强制调用类似于 `return errors.WithStack(ErrNotSupported)` 的代码。这也不是很痛苦,但不幸的是,你不能只导入 `pkg/errors` ,就让所有现有的错误都带有堆栈跟踪。如果你还没有使用 `errors.New` 来实例化你的错误,那么它需要一些手动调用。 + +## 日志 + +接下来是日志,或者更恰当的说,结构化日志。这里提供了许多软件包,类似于 [sirupsen/logrus](https://github.com/sirupsen/logrus)或我最喜欢的[APEX/LOG](https://github.com/apex/log)。这些包也支持将日志发送到远程的机器或者服务,我们可以用工具来监控这些日志。 + +当谈到标准日志包时,我不常看到的一个选项是创建一个自定义 logger,并将 `log.LShorfile` 或 `log.LUTC` 等标志传递给它,以再次获得一点上下文,这能让你的工作变轻松 - 尤其在处理不同时区的服务器时。 + +```go +const ( + Ldate = 1 << iota // the date in the local time zone: 2009/01/23 + Ltime // the time in the local time zone: 01:23:23 + Lmicroseconds // microsecond resolution: 01:23:23.123123. assumes Ltime. + Llongfile // full file name and line number: /a/b/c/d.go:23 + Lshortfile // final file name element and line number: d.go:23. overrides Llongfile + LUTC // if Ldate or Ltime is set, use UTC rather than the local time zone + LstdFlags = Ldate | Ltime // initial values for the standard logger +) +``` + +即使你没有创建自定义 logger,你也可以使用 `SetFlags` 来修改默认 logger。([playground link](https://play.golang.org/p/jlplSGTDoyI)): + +```go +package main + +import ( + "log" +) + +func main() { + log.SetFlags(log.LstdFlags | log.Lshortfile) + log.Println("Hello, playground") +} +``` + +结果如下: + +```bash +2009/11/10 23:00:00 main.go:9: Hello, playground +``` + +你不想知道你在哪里打印了日志吗?这会让跟踪代码变得更容易。 + +## 接口 + +如果你正在写接口并命名接口中的参数,请考虑以下的代码片段: + +```go +type Mover interface { + Move(context.Context, string, string) error +} +``` + +你知道这里的参数代表什么吗?只需要在接口中使用命名参数就可以让它很清晰。 + +```go +type Mover interface { + Move(context.Context, source string, destination string) +} +``` + +我还经常看到一些使用一个具体类型作为返回值的接口。一种未得到充分利用的做法是,根据一些已知的结构体或接口参数,以某种方式声明接口,然后在接收器中填充结果。这可能是 Go 中最强大的接口之一。 + +```go +type Filler interface { + Fill(r *http.Request) error +} + +func (s *YourStruct) Fill(r *http.Request) error { + // here you write your code... +} +``` + +更可能的是,一个或多个结构体可以实现该接口。如下: + +```go +type RequestParser interface { + Parse(r *http.Request) (*types.ServiceRequest, error) +} +``` + +此接口返回具体类型(而不是接口)。通常,这样的代码会使你代码库中的接口变得杂乱无章,因为每个接口只有一个实现,并且在你的应用包结构之外会变得不可用。 + +### 小帖士 + +如果你希望在编译时确保你的结构体符合并完全实现一个接口(或多个接口),你可以这么做: + +```go +var _ io.Reader = &YourStruct{} +var _ fmt.Stringer = &YourStruct{} +``` + +如果你缺少这些接口所需的某些函数,编译器就会报错。字符 `_` 表示丢弃变量,所以没有副作用,编译器完全优化了这些代码,会忽视这些被丢弃的行。 + +## 空接口 + +与上面的观点相比,这可能是更有争议的观点 - 但是我觉得使用 ``interface{}`` 有时非常有效。在 HTTP API 响应的例子中,最后一步通常是 JSON 编码,它接收一个接口参数: + +```go +func (enc *Encoder) Encode(v interface{}) error +``` + +因此,完全可以避免将 API 响应设置成具体类型。我并不建议对所有情况都这么处理,但是在某些情况下,可以在 API 中完全忽略响应的具体类型,或者至少说明具体类型声明的意义。脑海中浮现的一个例子是使用匿名结构体。 + +```go +body := struct { + Username string `json:"username"` + Roles []string `json:"roles,omitempty"` +}{username, roles} +``` + +首先,不使用 `interface{}` 的话,无法从函数里返回这种结构体。显然,json 编码器可以接受任何类型的内容,因此,按传递空接口(对我来说)是完全有意义的。虽然趋势是声明具体类型,但有时候你可能不需要一层中间层。对于包含某些逻辑并可能返回各种形式的匿名结构体的函数,空接口也很合适。 + +> 更正:匿名结构体不是不可能返回,只是做起来很麻烦:[playground](https://play.golang.org/p/turu_Yg--6h) +> +> 感谢 @Ikearens at [Discord Gophers](https://discord.gg/quNN7yP) #golang channel + +第二个用例是数据库驱动的 API 设计,我之前写过一些[有关内容](https://scene-si.org/2018/02/07/sql-as-an-api/),我想指出的是,实现一个完全由数据库驱动的 API 是非常可能的。这也意味着添加和修改字段是*仅仅在数据库中*完成的,而不会以 ORM 的形式添加额外的间接层。显然,你仍然需要声明类型才能在数据库中插入数据,但是从数据库中读取数据可以省略声明。 + +```go +// getThread fetches comments by data, order by ID +func (api *API) getThread(params *CommentListThread) (comments []interface{}, err error) { + // calculate pagination parameters + start := params.PageNumber * params.PageSize + length := params.PageSize + query := fmt.Sprintf("select * from comments where news_id=? and self_id=? and visible=1 and deleted=0 order by id %s limit %d, %d", params.Order, start, length) + err = api.db.Select(&comments, query, params.NewsID, params.SelfID) + return +} +``` + +同样,你的应用程序可能充当反向代理,或者只使用无模式(schema-less)的数据库存储。在这些情况下,目的只是传递数据。 + +一个大警告(这是你需要输入结构体的地方)是,修改 Go 中的接口值并不是一件容易的事。你必须将它们强制转换为各种内容,如 map、slice 或结构体,以便可以在访问这些返回的数据。如果你不能保持结构体一成不变,而只是将它从 DB(或其他后端服务)传递到 JSON 编码器(会涉及到断言成具体类型),那么显然这个模式不适合你。这种情况下不应该存在这样的空接口代码。也就是说,当你不想了解任何关于载荷的信息时,空接口就是你需要的。 + +## 代码生成 + +尽可能使用代码生成。如果你想生成用于测试的 mock,如果你想生成 proc/GRPC 代码,或者你可能拥有的任何类型的代码生成,可以直接生成代码并提交。在发生冲突的情况下,可以随时将其丢弃,然后重新生成。 + +唯一可能的例外是提交类似于 `public_html` 文件夹的内容,其中包含你将使用 [rakyll/statik](https://github.com/rakyll/statik) 打包的内容。如果有人想告诉我,由 [gomock](https://github.com/golang/mock) 生成的代码在每次提交时都会以兆字节的数据污染 GIT 历史记录?不会的。 + +## 结束语 + +关于 Go 的最佳实践和最差实践的另一篇文章应该是[Idiomatic Go](https://about.sourcegraph.com/go/idiomatic-go/)。如果你不熟悉的话,可以阅读一下 - 它是与本文很好的搭配。 + +我想在这里引用[Jeff Atwood post - The Best Code is No Code At All](https://blog.codinghorror.com/the-best-code-is-no-code-at-all/)文章的一句话,这是一句令人难忘的结束语: + +> 如果你真的喜欢写代码,你会非常喜欢尽可能少地写代码。 + +但是,一定要编写那些单元测试。_完结_。 + +--- + +via: https://scene-si.org/2018/07/24/writing-great-go-code/ + +作者:[Tit Petric](https://scene-si.org/) +译者:[咔叽咔叽](https://github.com/watermelo) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200521-Go-as-a-Scripting-Language.md b/published/tech/20200521-Go-as-a-Scripting-Language.md new file mode 100644 index 000000000..d14bf854b --- /dev/null +++ b/published/tech/20200521-Go-as-a-Scripting-Language.md @@ -0,0 +1,90 @@ +首发于:https://studygolang.com/articles/34517 + +# 将 Go 作为脚本语言用 + +Go 作为一种可用于创建高性能网络和并发系统的编程语言,它的生态应用变得[越来越广泛](https://blog.golang.org/survey2019-results),同时,这也激发了开发人员使用 Go 作为脚本语言的兴趣。虽然目前 Go 还未准备好作为脚本语言 “开箱即用” 的特性,用来替代 Python 和 Bash ,但是我们只需要一点点准备工作就可以达到想要的目标。 + +[正如来自 Codelang 的 Elton Minetto 所说的那样](https://dev.to/codenation/using-golang-as-a-scripting-language-jl2),Go 作为一门脚本语言的同时,也具有相当大的吸引力,这不仅包括 Go 本身强大的功能和简洁的语法,还包括对 goroutines 的良好支持等。Google 公司的软件工程师 [Eyal Posener](https://posener.github.io/about/) 为 Go 用作脚本语言提供了[更多的理由](https://gist.github.com/posener/73ffd326d88483df6b1cb66e8ed1e0bd),例如,丰富的标准库和语言的简洁性使得维护工作变得更加容易。与之相对的是,Go 的贡献者和前 Google 公司员工 David Crawshaw 则[强调了使用 Go 编写脚本任务的便利程度](https://news.ycombinator.com/item?id=15623106),因为几乎所有的程序员都在花费大量的时间编写复杂的程序。 + +> 基本上,我一直在编写 Go 程序,偶尔会写写 Bash、perl 和 python 。有时候,这些编程语言会落入我的脑海。 + +对于日常任务和不太频繁的脚本编写任务,倘若能够使用相同的编程语言,它将会大大提高效率。Cloudflare 公司的工程师 Ignat Korchagin 指出,Go 是一种强类型语言,它能够[帮助 Go 脚本变得更加可靠,并且避免出现像拼写之类的小错误,从而不会出现发生在运行时的报错](https://blog.cloudflare.com/using-go-as-a-scripting-language-in-linux/)。 + +Codenation 使用 Go 编写的脚本文件,用来自动化地执行重复性的任务,这不仅是开发流程的一部分,还是其 CI/CD 管道中的任务。在 Codenation 内部,Go 脚本是通过 [`go run`](https://golang.org/cmd/go/#hdr-Compile_and_run_Go_program) 来执行的,`go run` 是 Go 构建工具链中的默认命令,能够一步一步地编译和运行 Go 程序。Posener 写道:“事实上,`go run` 并非作为解释器来使用。” + +>[...] bash 和 python 都是解释型语言 —— 它们在读取脚本的时候,然后执行脚本文件。另一方面,当您键入 `go run` 时,Go 编译器就会编译程序,然后运行它们。Go 程序的编译时间很短,这使它看起来就像是解释型语言一般。 + +为了让 Go 编写的脚本在 shell 脚本程序中表现良好,Codenation 的工程师使用了许多有用的 Go 软件包: + +- [github.com/fatih/color](https://github.com/fatih/color) 是用于输出对应编码颜色的包。 +- [github.com/schollz/progressbar](https://github.com/schollz/progressbar) 是用于为执行时间过久的任务创建进度条的包。 +- [github.com/jimlawless/whereami](https://github.com/jimlawless/whereami) 是用于捕获源代码的文件名、行号、函数等信息的包,这对于改正错误信息十分有用! +- [github.com/spf13/cobra](https://github.com/spf13/cobra) 是用于更轻松地创建带有输入选项和相关文档的复杂脚本的包。 + + Crawshaw 写道:“ 对于 Codenation 来说,虽然使用命令行工具 `go run` 来运行 Go 程序非常有效,但是它并非最完美的解决方案。” 特别是,Go 缺乏对读取-求值-输出循环 (REPL) 的支持,并且无法轻松地将 `Shebang` ( 译者注:Unix 系统中,通常称 `#` 为 `sharp` 或 `she`;而称 `!` 为 `bang` ) 集成在一起,这使得脚本可以像二进制程序一样执行。此外,比起短小精悍的脚本文件,Go 错误处理更适合大型程序项目使用。由于这些原因,他开始研究 [Neugram](https://github.com/neugram/ng) ,该项目旨在创建一个 Go 克隆程序用来解决上述所有限制。不巧的是,Neugram 项目现在似乎已经被废弃,这可能是[由于 Go 语法的所有细节的复杂性](https://news.ycombinator.com/item?id=15623244)。 + +[Gomacro](https://github.com/cosmos72/gomacro) 项目使用了与 `Neugram` 类似的方法,它是一种 Go 的解释器,还支持类似 Lisp 的宏,既可以生成代码,又可以实现某种形式的[泛型](https://github.com/cosmos72/gomacro#generics)。 + +>`Gomacro` 几乎是一个完整的 Go 解释器,它使用纯 Go 语言实现。它提供了交互式的 `REPL` 模式和脚本模式,并且在运行时不需要 Go 构建工具链。(除了在在一种非常特殊的情况以外:在运行时导入第三方包。) + +除了非常适合写脚本外,`Gomacro` 还旨在使得 Go 成为一种中间语言,表示要将它[解释为 Go 的标准详细规范](https://github.com/cosmos72/gomacro/blob/master/doc/code_generation.pdf),还会[提供 Go 源代码的调试器](https://github.com/cosmos72/gomacro#debugger)。 + +尽管在使用 Go 编写脚本的情况下,`Gomacro` 为其提供了最大灵活性,但不幸的是,它不是标准的 Go 语言,这引起了另一种程度的担忧。[Posener 对使用标准的 Go 语言作为脚本语言的可能性进行了详细分析](https://gist.github.com/posener/73ffd326d88483df6b1cb66e8ed1e0bd),包括针对丢失 `Shebang` 的解决方法。但是,这些解决方法都在某种程度上均有不足的体现。 + +> 似乎没有完美的解决方案,而且我也不明白为什么不能有一个完美的解决方案。看起来,运行 Go 脚本的方式最为简单,而最没有问题的方法就是使用 `go run` 命令。[...] 这就是我为什么认为在该语言领域上,仍需要做未完成的工作。同样的,我认为更改程序语言用来忽略 `Shebang` 不会有任何的危害。 + +但是,对于 Linux 系统,这里可能会有一个高级技巧,能够在具有完全 `Shebang` 支持的情况下,从命令行运行 Go 脚本。由 Korchagin 举例子并说明的这种方法,依赖于 `Shebang` 对 Linux 内核的支持,以及从 Linux 用户空间扩展受到支持二进制格式的可能性。长话短说,Korchagin 建议使用以下方式注册二进制: + +```bash +$ Echo ':golang:E::go::/usr/local/bin/gorun:OC' | sudo tee /proc/sys/fs/binfmt_misc/register +:golang:E::go::/usr/local/bin/gorun:OC +``` + +这样就可以设置完全标准的 Go 语言的可执行位,例如: + +```go +package main +import ( + "fmt" + "os" +) +func main() { + s := "world" + + if len(os.Args) > 1 { + s = os.Args[1] + } + + fmt.Printf("Hello, %v!", s) + fmt.Println("") + + if s == "fail" { + os.Exit(30) + } +} +``` + +然后执行: + +```bash +$ chmod u+x helloscript.go +$ ./helloscript.go +Hello, world! +$ ./helloscript.go gopher +Hello, gopher! +$ ./helloscript.go fail +Hello, fail! +$ Echo $? +30 +``` + +尽管这种方法无法提供对 `REPL` 的支持,但是 `Shebang` 可能足以满足典型的用例。 + +--- +via: https://www.infoq.com/news/2020/04/go-scripting-language/ + +作者:[Sergio De Simone](https://www.infoq.com/profile/Sergio-De-Simone/) +译者:[sunlingbot](https://github.com/sunlingbot) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200523-Go-Debugging-with-Delve-and-Core-Dumps.md b/published/tech/20200523-Go-Debugging-with-Delve-and-Core-Dumps.md new file mode 100644 index 000000000..d417e8897 --- /dev/null +++ b/published/tech/20200523-Go-Debugging-with-Delve-and-Core-Dumps.md @@ -0,0 +1,67 @@ +首发于:https://studygolang.com/articles/30252 + +# Go:使用 Delve 和 Core Dump 调试代码 + +![由 Renee French 创作的原始 Go Gopher 为“ Go Go 之旅”创建的插图。](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200523-Go-Debugging-with-Delve-and-Core-Dumps/Illustration.png) +ℹ️ 这篇文章基于 Go Delve 1.4.1。 + +core dump 是一个包含着意外终止的程序其内存快照的文件。这个文件可以被用来事后调试(debugging)以了解为什么会发生崩溃,同时了解其中涉及到的变量。通过 `GOTRACEBACK`,Go 提供了一个环境变量用于控制程序崩溃时生成的输出信息。这个变量同样可以强制生成 core dump,从而使调试成为可能。 + +## GOTRACEBACK + +`GOTRACEBACK` 控制着当程序崩溃时输入的详细程度。它可以使用不同的值: + +- `none` 不显示任何 Goroutine 的堆栈信息。 +- `single`,默认选项,显示当前 Goroutine 的堆栈信息。 +- `all` 显示所有用户创建的 Goroutine 的堆栈信息。 +- `system` 显示所有 Goroutine 的堆栈信息,即使是来自运行时的 goroutine。 +- `crash` 与 `system` 类似,但是会生成 core dump。 + +最后的那个选项,给了我们在程序崩溃的情况下,调试我们程序的能力。如果没有足够的日志,或者崩溃无法复现时,这是一个好的选择。让我们以下面的程序为例: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20200523-Go-Debugging-with-Delve-and-Core-Dumps/example-program.png) + +这个程序会很快崩溃: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20200523-Go-Debugging-with-Delve-and-Core-Dumps/crash.png) + +从堆栈信息中我们无法获知那个值涉及了程序的崩溃。增加日志是一个解决办法,但我们无法一直知道要在哪里加日志。当问题无法复现的时候,编写测试用例以确保问题被修复是十分困难的。我们可以不断重复增加日志和运行程序的步骤,直到程序崩溃,再查看可能的原因后再运行。 + +设置 `GOTRACEBACK=crash` 后再次运行。输出信息更加详细,因为现在所有的 Goroutine 信息打印了出来,包括运行时的。无论如何,我们现在有了 core dump。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20200523-Go-Debugging-with-Delve-and-Core-Dumps/core-dump.png) + +core dump 通过 `SIGABRT` 信号触发,该信号[生成 core dump 作为处置](http://man7.org/linux/man-pages/man7/signal.7.html)。 + +core dump 可以被诸如 [Go delve](https://github.com/go-delve/delve) 或者 [GBD](https://www.gnu.org/s/gdb/) 的调试信息分析。 + +## Delve + +Delve 是用 Go 语言编写的 Go 程序调试器。它允许通过在用户代码和运行时代码的任意位置加断点来逐步调试,甚至通过 `dlv core` 命令来调试 core dump,这个命令以二进制和 core dump 为参数。 + +一旦命令运行,我们就可以开始与 core dump 进行交互: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20200523-Go-Debugging-with-Delve-and-Core-Dumps/interacting-with-the-core-dump.png) + +`dlv` 命令 `bt` 打印堆栈信息并且显示程序生成的 panic 信息。之后,我们可以通过 `frame 9` 命令来访问 9 号帧: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20200523-Go-Debugging-with-Delve-and-Core-Dumps/frame9.png) + +最终,用 `locals` 命令打印本地变量,来帮助了解哪个变量与崩溃有关: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20200523-Go-Debugging-with-Delve-and-Core-Dumps/value-was-involved-in-the-crash.png) + +channel 是满的,并且生成的随机数是 203,300。而对于变量 `sum`,可以通过命令 `vars` 打印出它的内容,该命令用于打印包级别变量: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20200523-Go-Debugging-with-Delve-and-Core-Dumps/prints-the-package-variables.png) + +*如果没有看到本地变量 `n` ,请确保使用编辑标志 `-N` 和 `-l` 来构建二进制程序,这些标志禁用编译器优化,而这些优化会是调试更加困难。完整的编译命令是:`go build -gcflags=all="-N -l"` 不要忘记运行 `ulimit -c unlimited`,选项 `-c` 定义了 core dump 的最大尺寸。* + +--- +via: https://medium.com/a-journey-with-go/go-debugging-with-delve-core-dumps-384145b2e8d9 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[dust347](https://github.com/dust347) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200523-How-Fast-Is-Golang.md b/published/tech/20200523-How-Fast-Is-Golang.md new file mode 100644 index 000000000..00c494b70 --- /dev/null +++ b/published/tech/20200523-How-Fast-Is-Golang.md @@ -0,0 +1,130 @@ +首发于:https://studygolang.com/articles/30253 + +# Golang 有多快?—— 对比 Go 和 Python 实现的 Eratosthenes 筛选法 + +![Photo by Max Duzij on Unsplash](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200523-How-Fast-Is-Golang/Photo.jpeg) + +时间宝贵,所以为什么浪费时间等待程序运行?除非过去几年与世隔绝,否则不会错过 Go 的兴起。由谷歌工程师 Robert Griesemer,Rob Pike 和 Ken Thompson [创造的](https://golang.org/doc/faq#Origins) 新颖的编程语言,Go 被誉为[近乎完美](https://towardsdatascience.com/why-we-deploy-machine-learning-models-with-go-not-python-a4e35ec16deb),在易用性上可以与 Python 相媲美,而在执行速度上又可以与 C 语言相媲美。真的如传闻所说吗?今天,我们会分别使用 Go 和 Python 实现埃拉托色尼筛选法,并以耗时为结果。最终,得到的问题的结果,也就是“Golang 到底有多快?” + +## Eratosthenes 筛选法 + +Eratosthenes 是古希腊博学家。对很多领域均有涉猎(数学,地理,诗歌,天文学,音乐 —— 还不仅仅只是这些)他是一位著名的学者,[据称他是第一个](https://en.wikipedia.org/wiki/Eratosthenes)测量地球周长及其轴线倾斜度的人。要知道当时是公元前 3 世纪,就会足够让人惊奇。 + +![Eratosthenes 的蚀刻(维基共享资源/公共领域)](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200523-How-Fast-Is-Golang/An-etching-of-Eratosthenes.jpg) + +尽管如此,我们将从他的一项尝试中使用一种技术进入数论世界:Eratosthenes 筛选法。简单来说,这是一个从整数集合中生成(或者说“筛选”)质数列表的相对有效的方法。生成最大为 n 的质数的方法如下: + +- 构造一个整数集合,a,例如 a = {2, 3, ..., n}。 +- 遍历 a 的元素,每轮遍历移除该原色的倍数(但是不移除这个原始的元素)。 +- a 现在是一个包含着最大为 n 的质数集合。 + +理解该方法的一个奇妙方式是看下面的这个动图: + +![动态展示 Eratosthenes 筛选法 的动画](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20200523-How-Fast-Is-Golang/An-animation-showing-the-Sieve-of-Eratosthenes-in-action.gif) + +## Python 代码 +先从 Python 代码开始,因为它可能是最简单易懂的。下面是实际运行代码的精简版本(如果想看带错误捕获的实际运行版本,从[我的 GitHub 仓库](https://github.com/8F3E/sieve-of-eratosthenes)检出)。 + +```python +def sieve(primes, factor): + for p in primes: + if p != 0 and p != factor: + if p % factor == 0: + primes[primes.index(p)] = 0 + return primes + + +def main(n): + primes = [i for i in range(2, n + 1)] + for p in primes: + if p != 0: + primes = sieve(primes, p) + print('\n'.join([str(p) for p in primes if p != 0])) +``` + +这段代码是如何工作的? + +- 主函数接受一个参数,n,这个整型参数限定了我们要生成的质数的大小。 +- 之后,创建一个包含 2 到 n 的数字的列表。 +- 对于这些数字中的每一个数字,通过向列表中的每一个元素调用 sieve 函数来移除其所有的倍数。(实际上是将倍数设置为 0,因为遍历过程中移除他们会导致错误) +- 最后我们输出一个剩下的所有质数的列表(即,原列表中所有不是 0 的元素)确保另起一行分隔他们。 +- 注意第 4 行的 `p % factor`。这是一个获取(在这个例子中)p 除以 factor 的余数的“取模函数”。如果余数是 0,那我们便得到了一个倍数(比如,10 ÷ 5=2,余数是 0)。 + +## Go 代码 + +为了保证对比公平,在 Go 的筛选代码中使用完全一样的算法。出来这里是(从 [GitHub](https://github.com/8F3E/sieve-of-eratosthenes)上面的完整代码中剥离出来的)精简代码外,我不会重复深入解释这段代码。来将这段代码与上面的 Python 代码进行对比。 +```go +func main() { + var primes []int + + for i := 2; i <= n; i++ { + primes = append(primes, i) + } + + for i := 0; i < len(primes); i++ { + if primes[i] != 0 { + sieve(primes, primes[i]) + } + } + + for i := 0; i < len(primes); i++ { + if primes[i] != 0 { + fmt.Println(primes[i]) + } + } +} + +func sieve(primes []int, factor int) { + for index, value := range(primes) { + if value != 0 && value != factor { + if value % factor == 0 { + primes[index] = 0 + } + } + } +} +``` + +- 第 2 行到第 6 行的代码创建我们的数字列表。 +- 第 8 行到第 12 行代码遍历该列表,使用 sieve 函数(工作流程与 Python 代码中的完全一样)移除(每个元素的)倍数。 +- 最后输出列表,每个指数另起一行来进行分隔。 + +## 结果出来了! + +我在 bash shell 中使用 `time` 函数(见下面的例子)来测试这些代码[^1],使用不同的 n (范围从 1 到 100,000)同时使用 [Jupyter Notebook](https://nbviewer.jupyter.org/github/8F3E/sieve-of-eratosthenes/blob/61876c17a697d1c8439e39ab790de15adc678804/testing/Testing.ipynb) 生成下面的结果。图表由 [Plotly](https://medium.com/swlh/forget-matplotlib-you-should-be-using-plotly-ada76b650ff4) 生成 + +``` +$ time bin/sieve 10 +2 +3 +5 +7 +real 0m0.004s +user 0m0.000s +sys 0m0.005s +``` + +![结果数据,首先是标准比例,然后是对数比例。使用 Plotly 制作。](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20200523-How-Fast-Is-Golang/The-resulting-data-1.png) + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/blob/master/20200523-How-Fast-Is-Golang/The-resulting-data-2.png) + +结果十分明显,Go 要比 Python 要快,尤其是在大规模数字计算领域上。当在在较小规模时(n < 1000)他们并没有明显差异(Go 相较于 Python 轻微缓慢的节奏来说,几乎是瞬时的),当达到了 10,000 的规模,Python 落后了很多。 + +## 总结 + +> Go 是一种开源编程语言,可轻松构建简单,可靠和高效的软件。—— golang.org + +总结如下,Go 明显比 Python 快得多。正如该语言的网站所说,它简单,可靠且*非常*高效。因此,是的,如果您发现 Python 更容易或更简单或者只是更快地编程,请使用 Python。但是对于时间紧迫,计算能力强的软件,Go 可能是正确的选择。 + +> 更新:如果您需要坚持使用 Python 但又想尽可能地提高它的速度,为什么不看看我的其他文章:[5 种可以立即提高 Python 速度的方法](https://medium.com/@8F3E/how-you-can-improve-pythons-speed-right-now-6a0ec2234618)。 + +[^1]: 确实,我应该每个 n 次测试一次。但是通过一遍就说明了这种趋势,所以我节省了一些时间,只停留了一轮。 + +--- +via: https://medium.com/swlh/how-fast-is-golang-135c658205eb + +作者:[8F3E](https://medium.com/@8F3E) +译者:[dust347](https://github.com/dust347) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200524-Diamond-interface-composition-in-Go-114.md b/published/tech/20200524-Diamond-interface-composition-in-Go-114.md new file mode 100644 index 000000000..b850f6d1a --- /dev/null +++ b/published/tech/20200524-Diamond-interface-composition-in-Go-114.md @@ -0,0 +1,94 @@ +首发于:https://studygolang.com/articles/28992 + +# Go 1.14 中接口的菱形组合 + +按照[部分重叠的接口提议](https://github.com/golang/proposal/blob/master/design/6977-overlapping-interfaces.md),Go 1.14 现在允许嵌入有部分方法重叠的接口。本文是一篇解释这次修改的简要说明。 + +我们先来看 io 包中的三个关键接口:io.Reader、io.Writer 和 io.Closer: + +```go +package io + +type Reader interface { + Read([]byte) (int, error) +} + +type Writer interface { + Write([]byte) (int, error) +} + +type Closer interface { + Close() error +} +``` + +在结构体中嵌入类型时,如果在结构体中声明了被嵌入的类型,那么该类型的字段和方法允许被访问[^1],对于接口来说这个处理也成立。因此下面两种方式:显式声明 + +```go +type ReadCloser interface { + Read([]byte) (int, error) + Close() error +} +``` + +和使用嵌入来组成接口 + +```go +type ReadCloser interface { + Reader + Closer +} +``` + +没有区别。 + +你甚至可以混合使用: + +```go +type WriteCloser interface { + Write([]byte) (int, error) + Closer +} +``` + +然而,在 Go 1.14 之前,如果你用这种方式来声明接口,你可能会得到类似这样的结果: + +```go +type ReadWriteCloser interface { + ReadCloser + WriterCloser +} +``` + +编译错误: + +```bash +% Go build interfaces.go +command-line-arguments +./interfaces.go:27:2: duplicate method Close +``` + +幸运的是,在 Go 1.14 中这不再是一个限制了,因此这个改动解决了在菱形嵌入时出现的问题。 + +然而,在我向本地的用户组解释这个特性时也陷入了麻烦 — 只有 Go 编译器使用 1.14(或更高版本)语言规范时才支持这个特性。 + +我理解的编译过程中 Go 语言规范所使用的版本的规则似乎是这样的: + +1. 如果你的源码是在 GOPATH 下(或者你用 GO111MODULE=off *关闭*了 module),那么 Go 语言规范会使用你编译器的版本来编译。换句话说,如果安装了 Go 1.13,那么你的 Go 版本就是 1.13。如果你安装了 Go 1.14,那么你的版本就是 1.14。这里符合认知。 +2. 如果你的源码保存在 GOPATH 外(或你用 GO111MODULE=on 强制开启了 module),那么 Go tool 会从 go.mod 文件中获取 Go 版本。 +3. 如果 go.mod 中没有列出 Go 版本,那么语言规范会使用安装的 Go 的版本。这跟第 1 点是一致的。 +4. 如果你用的是 Go module 模式,不管是源码在 GOPATH 外还是设置了 GO111MODULE=on,但是在当前目录或所有父目录中都没有 go.mod 文件,那么 Go 语言规范会默认用 Go 1.13 版本来编译你的代码。 + +我曾经遇到过第 4 点的情况。 + +[^1]: 也就是说,嵌入提升了类型的字段和方法。 + +--- + +via: https://dave.cheney.net/2020/05/24/diamond-interface-composition-in-go-1-14 + +作者:[Dave Cheney](https://dave.cheney.net/) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200528-How-I-Structure-Web-Servers-in-Go.md b/published/tech/20200528-How-I-Structure-Web-Servers-in-Go.md new file mode 100644 index 000000000..01e2b0c14 --- /dev/null +++ b/published/tech/20200528-How-I-Structure-Web-Servers-in-Go.md @@ -0,0 +1,242 @@ +首发于:https://studygolang.com/articles/30254 + +# 我是如何在 Go 中构建 Web 服务的 + +从用了近十年的 C# 转到 Go 是一个有趣的旅程。有时,我陶醉于 Go 的[简洁](https://www.youtube.com/watch?v=rFejpH_tAHM);也有些时候,当熟悉的 OOP (面向对象编程)[模式](https://en.wikipedia.org/wiki/Software_design_pattern)无法在 Go 代码中使用的时候会感到沮丧。幸运的是,我已经摸索出了一些写 HTTP 服务的模式,在我的团队中应用地很好。 + +当在公司项目上工作时,我倾向把可发现性放在最高的优先级上。这些应用会在接下来的 20 年运行在生产环境中,必须有众多的开发人员和网站可靠性工程师(可能是指运维)来进行热补丁,维护和调整工作。因此,我不指望这些模式能适合所有人。 + +> [Mat Ryer 的文章](https://pace.dev/blog/2018/05/09/how-I-write-http-services-after-eight-years.html)是我使用 Go 试验 HTTP 服务的起点之一,也是这篇文章的灵感来源。 + +## 代码组成 + +### Broker + +一个 `Broker` 结构是将不同的 service 包绑定到 HTTP 逻辑的胶合结构。没有包作用域结级别的变量被使用。依赖的接口得益于了 [Go 的组合](https://www.ardanlabs.com/blog/2015/09/composition-with-go.html)的特点被嵌入了进来。 + +```go +type Broker struct { + auth.Client // 从外部仓库导入的身份验证依赖(接口) + service.Service // 仓库的业务逻辑包(接口) + + cfg Config // 该 API 服务的配置 + router *mux.Router // 该 API 服务的路由集 +} +``` + +broker 可以使用[阻塞](https://stackoverflow.com/questions/2407589/what-does-the-term-blocking-mean-in-programming)函数 `New()` 来初始化,该函数校验配置,并且运行所有需要的前置检查。 + +```go +func New(cfg Config, port int) (*Broker, error) { + r := &Broker{ + cfg: cfg, + } + + ... + + r.auth.Client, err = auth.New(cfg.AuthConfig) + if err != nil { + return nil, fmt.Errorf("Unable to create new API broker: %w", err) + } + + ... + + return r, nil +} +``` + +初始化后的 `Broker` 满足了暴露在外的 `Server` 接口,这些接口定义了所有的,被 route 和 中间件(middleware)使用的功能。`service` 包接口被嵌入,这些接口与 `Broker` 上嵌入的接口相匹配。 + +```go +type Server interface { + PingDependencies(bool) error + ValidateJWT(string) error + + service.Service +} +``` + +web 服务通过调用 `Start()` 函数来启动。路由绑定通过一个[闭包函数](https://gobyexample.com/closures)进行绑定,这种方式保证循环依赖不会破坏导入周期规则。 + +```go +func (bkr *Broker) Start(binder func(s Server, r *mux.Router)) { + ... + + bkr.router = mux.NewRouter().StrictSlash(true) + binder(bkr, bkr.router) + + ... + + if err := http.Serve(l, bkr.router); errors.Is(err, http.ErrServerClosed) { + log.Warn().Err(err).Msg("Web server has shut down") + } else { + log.Fatal().Err(err).Msg("Web server has shut down unexpectedly") + } +} +``` + +那些对故障排除(比如,[Kubernetes 探针](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/)0))或者灾难恢复方案方面有用的函数,挂在 `Broker` 上。如果被 routes/middleware 使用的话,这些仅仅被添加到 `webserver.Server` 接口上。 + +```go +func (bkr *Broker) SetupDatabase() { ... } +func (bkr *Broker) PingDependencies(failFast bool)) { ... } +``` + +### 启动引导 + +整个应用的入口是一个 `main` 包。默认会启动 Web 服务。我们可以通过传入一些命令行参数来调用之前提到的故障排查功能,方便使用传入 `New()` 函数的,经过验证的配置来测试代理权限以及其他网络问题。我们所要做的只是登入运行着的 pod 然后像使用其他命令行工具一样使用它们。 + +```go +func main() { + subCommand := flag.String("start", "", "start the webserver") + + ... + + srv := webserver.New(cfg, 80) + + switch strings.ToLower(subCommand) { + case "ping": + srv.PingDependencies(false) + case "start": + srv.Start(BindRoutes) + default: + fmt.Printf("Unrecognized command %q, exiting.", subCommand) + os.Exit(1) + } +} +``` + +HTTP 管道设置在 `BindRoutes()` 函数中完成,该函数通过 `ser.Start()` 注入到服务(server)中。 + +```go +func BindRoutes(srv webserver.Server, r *mux.Router) { + r.Use(middleware.Metrics(), middleware.Authentication(srv)) + r.HandleFunc("/ping", routes.Ping()).Methods(http.MethodGet) + + ... + + r.HandleFunc("/makes/{makeID}/models/{modelID}", model.get(srv)).Methods(http.MethodGet) +} +``` + +### 中间件 + +中间件(Middleware)返回一个带有 handler 的函数,handler 用来构建需要的 `http.HandlerFunc`。这使得 `webserver.Server` 接口被注入,同时所有的安静检查只在启动时执行,而不是在所有路由调用的时候。 + +```go +func Authentication(srv webserver.Server) func(h http.Handler) http.Handler { + if srv == nil || !srv.Client.IsValid() { + log.Fatal().Msg("a nil dependency was passed to authentication middleware") + } + + // additional setup logic + ... + + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := strings.TrimSpace(r.Header.Get("Authorization")) + if err := srv.ValidateJWT(token); err != nil { + ... + w.WriteHeader(401) + w.Write([]byte("Access Denied")) + + return + } + + next.ServeHTTP(w, r) + } + } +} +``` + +### 路由 + +路由有着与中间件有着类似的套路——简单的设置,但是有着同样的收益。 + +```go +func GetLatest(srv webserver.Server) http.HandlerFunc { + if srv == nil { + log.Fatal().Msg("a nil dependency was passed to the `/makes/{makeID}/models/{modelID}` route") + } + + // additional setup logic + ... + + return func(w http.ResponseWriter, r *http.Request) { + ... + + makeDTO, err := srv.Get + } +} +``` + +## 目录结构 + +代码的目录结构对可发现性进行了*高度*优化。 + +``` +├── app/ +| └── service-api/** +├── cmd/ +| └── service-tool-x/ +├── internal/ +| └── service/ +| └── mock/ +├── pkg/ +| ├── client/ +| └── dtos/ +├── (.editorconfig, .gitattributes, .gitignore) +└── go.mod +``` + +- app/ 用于项目应用——这是新来的人了解代码倾向的切入点。 +dd + - ./service-api/ 是该仓库的微服务 API;所有的 HTTP 实现细节都在这里。 +- cmd/ 是存放命令行应用的地方。 +- internal/ 是不可以被该仓库以外的项目引入的一个[特殊目录](https://dave.cheney.net/2019/10/06/use-internal-packages-to-reduce-your-public-api-surface)。 + - ./service/ 是所有领域逻辑(domain logic)所在的地方;可以被 `service-api`,`service-tool-x`,以及任何未来直接访问这个目录可以带来收益的应用或者包所引入。 +- pkg/ 用于存放鼓励被仓库以外的项目所引入的包。 + - ./client/ 是用于访问 `service-api` 的 client 库。其他团队可以使用而不是自己写一个 client,并且我们可以借助我们在 `cmd/` 里面的 CI/CD 工具来 “[dogfood it](https://en.wikipedia.org/wiki/Eating_your_own_dog_food)” (使用自己产品的意思)。 + - ./dtos/ 是存放项目的数据传输对象,不同包之间共享的数据且以 JSON 形式在线路上编码或传输的结构体定义。没有从其他仓库包导出的模块化的结构体。`/internal/service` 负责 这些 DTO (数据传输对象)和自己内部模型的相互映射,避免实现细节的遗漏(如,数据库注释)并且该模型的改变不破坏下游客户端消费这些 DTO。 +- .editorconfig,.gitattributes,.gitignore 因为[所有的仓库必须使用 .editorconfig,.gitattributes,.gitignore](https://www.dudley.codes/posts/2020.02.16-git-lost-in-translation/)! +- go.mod 甚至可以在[有限制的且官僚的公司环境](https://www.dudley.codes/posts/2020.04.02-golang-behind-corporate-firewall/)工作。 + +> 最重要的:每个包只负责意见事情,一件事情! + +### HTTP 服务结构 + +``` +└── service-api/ + ├── cfg/ + ├── middleware/ + ├── routes/ + | ├── makes/ + | | └── models/** + | ├── create.go + | ├── create_test.go + | ├── get.go + | └── get_test.go + ├── webserver/ + ├── main.go + └── routebinds.go +``` + +- ./cfg/ 用于存放配置文件,通常是以 JSON 或者 YAML 形式保存的纯文本文件,它们也应该被检入到 Git 里面(除了密码,秘钥等)。 +- ./middleware 用于所有的中间件。 +- ./routes 采用类似应用的类 RESTFul 形式的目录对路由代码进行分组和嵌套。 +- ./webserver 保存所有共享的 HTTP 结构和接口(Broker,配置,`Server` 等等)。 +- main.go 启动应用程序的地方(`New()`,`Start()`)。 +- routebinds.go `BindRoutes()` 函数存放的地方。 + +## 你觉得呢? + +如果你最终采用了这种模式,或者有其他的想法我们可以讨论,我乐意听到这些想法! + +--- +via: https://www.dudley.codes/posts/2020.05.19-golang-structure-web-servers/ + +作者:[James Dudley](https://www.dudley.codes/) +译者:[dust347](https://github.com/dust347) +校对:[unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200528-Sorting-in-Go-from-JavaScript.md b/published/tech/20200528-Sorting-in-Go-from-JavaScript.md new file mode 100644 index 000000000..bda74944b --- /dev/null +++ b/published/tech/20200528-Sorting-in-Go-from-JavaScript.md @@ -0,0 +1,217 @@ +首发于:https://studygolang.com/articles/35260 + +# 从 JavaScript 到 Go 语言的排序算法 + +在计算机科学中,排序的意思是获取一个数组,然后重新对他们进行排列,使他们遵循指定的顺序,例如按字母顺序对字符串进行排序、按最小到最大的顺序对数字进行排序,或按结构中的一个字段对结构数组进行排序。您可以使用它(排序)来提高算法的工作效率,或按特定顺序显示数据(例如时间上的从最近到最远)。 + +对于 Go 中的排序,标准库提供了 sort 包,有意思的是,它使用了 Go 接口来定义对数据进行排序的规则。如果您使用过 JavaScript 的 Array.prototype.sort 方法,(那么,您对此)会很熟悉! + +## 字符串排序 + +让我们从按字母顺序排列一组字符串开始: + +```go +var languages = []string{"Go", "C", "Ruby", "JavaScript", "XML"} +``` + +在 JavaScript 中,对它们进行排序(代码)就像这样: + +```javascript +let languages = ["Go", "C", "Ruby", "JavaScript", "XML"]; + +languages.sort(); +console.log(languages); // ["C", "Go", "JavaScript", "Ruby", "XML"] +``` + +由于 ```languages``` 是一个数组,我们可以使用 Array.prototype.sort, 对它们进行排序。 + +由于与 JS 数组不同,Go 切片没有开箱即用的方法,我们没办法直接使用已有的排序算法,我们需要导入 sort 包并使用其 Sort 函数来对数组重新排列。让我们试试吧!(首先,)将此代码放在一个名为 sort-strings.go 的文件中: + +```go +package main + +import ( + "fmt" + "sort" +) + +func main() { + languages := []string{"Go", "C", "Ruby", "JavaScript", "XML"} + sort.Sort(languages) + + fmt.Println(languages) +} +``` + +然后,运行 ```go run sort-strings.go```,你应该会得到(如下错误): + +```bash +./sort-strings.go:10:14: cannot use languages (type []string) as type sort.Interface in argument to sort.Sort: + []string does not implement sort.Interface (missing Len method) +``` + +编译器错误?之所以会这样,是因为 ```sort.Sort``` 不接受切片类型,它无法对切片类型进行自动转换。它的函数签名实际上是这样的: + +```go +func Sort(data Interface) +``` + +sort.Interface(带有一个大 I)是一个 Go 接口,表示可以排序的数据集合,如字符串、数字抑或是结构体列表。由于对字符串和整数的切片进行排序很常见,所以 sort 包也提供了一些内置方法,使 sort.Sort 方法与字符串或整数切片可以兼容. 试试这个! + +```go +func main() { + languages := []string{"Go", "C", "Ruby", "JavaScript", "XML"} +- sort.Sort(languages) ++ sort.Sort(sort.StringSlice(languages)) + + fmt.Println(languages) +} +``` + +sort.StringSlice 是一个字符串切片方法,但它实现了 sort.Interface 接口. 因此,通过将一个 []string 类型转换为 StringSlice 类型,就可以使用 sort.Sort! 现在,如果您(再)执行 ```go run sort-strings.go``` 命令,您应该会看到按字母顺序排列的编程语言列表! + +为什么我们需要使用一个特殊的接口来对数据进行排序,而不是让 Go 语言的 sort.Sort 方法直接接受(字符串或整型)切片?原因是因为,我们传入的是一个元素集合,Go 语言需要通过某种方法来知道元素的顺序。为了编写这些规则来对切片进行排序,您需要实现 sort.Interface 方法。正如您看到的,Interface 使我们可以灵活地以任何您喜欢的方式来定义元素的顺序! + +## 实现自定义排序类型 + +假设我们的 languages 切片包含 "fish"(一种 shell 脚本语言)。如果您按字母顺序对 "编程工具" 进行排序,那么像这样有意义的排序是: + +```go +[C, fish, Go, JavaScript, Ruby, XML] +``` + +但是,即使有 XML,"fish" 也排在最后!(这是因为)使用 sort.StringSlice, 与使用 JS 中的字符串列表排序算法 Array.prototype.sort 相同,默认按照字典顺序排序,而不是字母顺序。在字典顺序中,小写字母(如 fish 中的 f)在大写字母(如 XML 中的 X)之后。如果我们想不区分大小写,就按照字母的顺序排序,我们需要实现一些自定义行为。那会是什么样子呢? + +在实现自定义排序规则之前,我们需要想想排序的作用。在本教程中,我们不会研究不同排序算法(如快速排序、归并排序和冒泡排序)的细节,虽然学习它们在编程中很重要。关于在 Go 和 JS 中编写自定义排序算法,您需要了解的是,它们需具备: + +- 查看集合中的元素 +- 比较它们,看看哪些元素应该排在前面 +- 根据这些比较将元素按顺序排列 + +在 JavaScript 中,您将传入一个自定义函数来告诉 sort 如何对数组中的元素进行比较,如下所示: + +```javascript +languages.sort((langA, langB) => { + langA = langA.toLowerCase(); + langB = langB.toLowerCase(); + if (langA < langB) { + return -1; // return -1 if langA should Go before langB in the array + } else if (langB > langA) { + return 1; // return 1 if langB should Go before langA in the array + } + return 0; // return 0 if they can Go in either order +}) +``` + +因为我们在比较之前已使用 toLowerCase 方法,(这样可以)使得 fish 语言排在 Go、JavaScript、Ruby 和 XML 语言之前,但在 C 语言之后! + +如果我们查看 Go sort.Interface,我们可以看到需要实现的方法如下: + +```go +type Interface interface { + Len() int + Less(i, j int) bool + Swap(i, j int) +} +``` + +所以要创建一个可以排序的类型,我们需要实现 sort.Interface 接口: + +- 告诉 Go sort 包集合的长度 +- 取集合中的任意两个元素(元素 i 和 j),并将他们进行交换 +- 查看集合中的任意两个元素,看看 Less 方法在对集合进行排序时哪个应该排在前面 + +让我们以 Len 和 Swap 的实现方法开始。 + +```go +type alphabeticalStrings []string + +func (a alphabeticalStrings) Len() int { return len(a) } + +func (a alphabeticalStrings) Swap(i, j int) { + placeholder := a[j] + a[j] = a[i] + a[i] = placeholder +} +``` + +首先,我们对字符串切片进行封装,定义一个新的类型,alphabeticalStrings。在 Go 语言中,我们通过定义自己的类型,我们可以为它编写方法。 + +对于 Len 方法,我们只是使用 Go 的内置 len 函数来获取切片的长度,对于 Swap,我们交换切片中的两个元素。目前为止一切顺利。现在让我们实现 Less 方法。先导入 strings 包,并添加这个函数: + +```go +func (a alphabeticalStrings) Less(i, j int) bool { + return strings.ToLower(a[i]) < strings.ToLower(a[j]) +} +``` + +注意到关于 Less 方法了吗?它看起来非常像我们在 Array.prototype.sort 函数中定义的比较方法,除了它返回一个 bool 类型而不是 int 类型,并接受的是切片索引而不是元素本身! + +现在,让我们来试试!编辑 main 函数,让它像这样: + +```go +func main() { + languages := []string{"Go", "C", "fish", "Ruby", "JavaScript", "XML"} +- sort.Sort(sort.StringSlice(languages)) ++ sort.Sort(alphabeticalStrings(languages)) + + fmt.Println(languages) +} +``` + +如果您执行 ```go run sort-strings.go``` 命令,现在您应该可以看到按预期排序的列表! + +```go +[C, fish, Go, JavaScript, Ruby, XML] +``` + +你知道 Go 有什么好玩的 sort.Interface 接口吗?我们编写的字母字符串类型和 Go 团队编写的 StringSlice 类型都建立在一个普通的旧类型之上,[]string 并且都可以传递到 sort.Sort. 我们可以通过选择我们将字符串切片转换为哪种类型来选择我们想要的字符串顺序! + +## 使用 sort.Slice 简化我们的排序! + +JS 和 Go 语言不同版本的 sort 方法之间的一个重要区别是,对 Go 切片进行排序时,除了比较函数之外,我们还需要编写 Len 和 Swap 方法。对于不同的切片类型,Len 和 Swap 看起来都差不多。故,定义一个新的(元素)排序,都必须实现这三种方法感觉有点麻烦。 + +需要实现这三种方法的原因是,你实现 sort.Interface 接口时,对应的数据,并不一定是数组或切片。我只是使用了切片的 sort 包,但您可以使用其他数据类型实现 sort.Interface 接口,例如链表。 + +对于切片,在 Len 和 Swap 方法中,我们经常使用的是相同的逻辑,我们能不能只实现 Less,就像在 JavaScript 中一样?(其实)sort 包就有这样的方法,sort.Slice! + +```go +func Slice( + slice interface{}, + less func(i, j int) bool, +) +``` + +我们传入我们想要排序的数据切片作为第一个参数,以及一个函数来比较切片的元素作为第二个参数。甚至无需创建新类型,现在我们就可以对数据进行排序!让我们再一次重构我们的 main 函数来试试: + +```go +func main() { + languages := []string{"Go", "C", "fish", "Ruby", "JavaScript", "XML"} +- sort.Sort(alphabeticalStrings(languages)) ++ sort.Slice(languages, func(i, j int) bool { ++ return strings.ToLower(languages[i]) < strings.ToLower(languages[j]) ++ }) + + fmt.Println(languages) +} +``` + +完成了!我们已经获取到我们要的排序切片了! + +sort 包还有一些很酷的地方,除了我们能够选择排序所依据的顺序外,注意在 sort.Sort 和 sort.Slice 中,我们不需要知道我们正在使用哪种排序算法。sort.Sort 已包含了具体的实现算法,所有需要从我们这边获取到的信息是,如何比较元素,如何交换它们,以及我们有多少元素。这就是接口的作用! + +顺便说一下,熟悉排序算法的工作原理仍然是绝对值得的,这会让您知道如何让你的计算机做更少的工作来排列数据,而其(原理)的应用场景非常多。因此,如果您想了解它们是如何工作,sort.Sort 和我们编写的这些函数在背后都做了什么,下面是一些关于算法本身的材料(可供学习)。 + +- [Free Code Camp - 解释排序算法](https://www.freecodecamp.org/news/sorting-algorithms-explained/) +- [Toptal - 排序算法动画](https://www.toptal.com/developers/sorting-algorithms) +- [在 JavaScript 中实现排序算法](https://medium.com/@rwillt/implementing-sorting-algorithms-in-javascript-b08504cdf4a9) + +--- + +via: https://dev.to/andyhaskell/sorting-in-go-from-javascript-4k8o + +作者:[andyhaskell](https://dev.to/andyhaskell) +译者:[gogeof](https://github.com/gogeof) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200528-Storing-Empty-Interfaces-in-BigCache.md b/published/tech/20200528-Storing-Empty-Interfaces-in-BigCache.md new file mode 100644 index 000000000..2ebceb47f --- /dev/null +++ b/published/tech/20200528-Storing-Empty-Interfaces-in-BigCache.md @@ -0,0 +1,133 @@ +首发于:https://studygolang.com/articles/34518 + +# 在 BigCache 中存储任意类型(interface{}) + +这篇文章也发在我的个人 [博客](https://calebschoepp.com/blog) + +最近在工作中,我的任务是向我们的一个 Golang 服务添加缓存。这个服务需要传入请求以提供用于身份验证的 API key。因此,对于每个请求,该服务都会额外查询数据库以验证 API key,尽管它通常是相同的 key。这很不好。实现缓存最终比我想象的要难得多。 + +经过调研和工程师之间详尽讨论之后,我们认为 [BigCache](https://github.com/allegro/bigcache) 最适合我们的需求。 + +这里有一个问题。BigCache 中的 set 方法的声明为 `Set(key string, entry []byte) error`。它期望存储一个 byte slice。但是我们希望存储一个 struct,该 struct 具有多个表示 API key 的字段。这次我们可能只能够存储实际 key 的 bytes。但这只是推迟解决问题。 我们需要的是像其他 Golang 缓存实现中的声明 `Set(key, entry interface{})`。这样我们就可以存储任何我们想要的东西。 + +这个问题的明显解决方案是序列化。如果我们可以将任意结构序列化为 byte slice,那么我们可以存储任何内容。要使用我们存储的结构,可以从缓存中反序列化获取 byte slice。序列化结构就像在 Golang 中导入任意数量的可用 encoding 库一样容易。但是现在头疼的问题来了。当我们反序列化 bytes 时,Go 语言如何知道将数据存入什么类型的结构?事实证明,Golang 特有的序列化库 `encoding/gob` 具有此功能。 + +我强烈建议您阅读 Rob Pike 写的关于 Gob 的 [博客文章](https://blog.golang.org/gob),这是一篇好文章。简而言之,Gob 是一种 Go 原生的数据序列化方式,它还具有序列化 interface 类型的功能。为此,您需要在序列化之前使用恰当命名的 [register funtion](https://golang.org/pkg/encoding/gob/#Register) 注册您的类型。我在这里卡住了,因为我找到的关于 `register()` 的任何代码示例总是注册一个单一的 struct 或 interface;我需要注册任意 `interface{}` 类型。我在 Go playground 上摸索了一下,发现它也可以做到。 + +```go +// 大多数示例中注册的类型 +type foo struct { + bar string +} + +gob.register(foo{}) + +// 我想要注册的类型 +var type interface{} // 可以是任何结构 + +gob.register(type) +``` + +## 把它们组合在一起 + +解决了将任意 struct 存储为 bytes 的问题后,我将向您展示如何将它们组合在一起。首先,我们需要一个缓存 interface,以便系统的其余部分能够与之交互。对于一个简单的缓存,我们只需要 get 和 set 方法。 + +```go +type Cache interface { + Set(key, value interface{}) error + Get(key interface{}) (interface{}, error) +} +``` + +现在,让我们定义实现上述接口的 BigCache 实现。首先,我们需要一个结构来保存缓存并可以向其中添加方法。 您还可以在此结构中添加其他字段,例如 metrics。 + +```go +type bigCache struct { + cache *bigcache.BigCache +} +``` + +接下来是 get 和 set 方法的实现。两种方法都断言 key 是 string。由此开始,get 和 set 的实现就彼此独立了。一个序列化一个值并存储它。另一个获取值并将其反序列化。 + +```go +func (c *bigCache) Set(key, value interface{}) error { + // 断言 key 为 string 类型 + keyString, ok := key.(string) + if !ok { + return errors.New("a cache key must be a string") + } + + // 将 value 序列化为 bytes + valueBytes, err := serialize(value) + if err != nil { + return err + } + + return c.cache.Set(keyString, valueBytes) +} + +func (c *bigCache) Get(key interface{}) (interface{}, error) { + // 断言 key 为 string 类型 + keyString, ok := key.(string) + if !ok { + return nil, errors.New("a cache key must be a string") + } + + // 获取以 bytes 格式存储的 value + valueBytes, err := c.cache.Get(keyString) + if err != nil { + return nil, err + } + + // 反序列化 valueBytes + value, err := deserialize(valueBytes) + if err != nil { + return nil, err + } + + return value, nil +} +``` + +最后是 `encoding/gob` 序列化逻辑。除了使用 `register()` 之外,这是 Go 中序列化内容相当标准的用法。 + +```go +func serialize(value interface{}) ([]byte, error) { + buf := bytes.Buffer{} + enc := gob.NewEncoder(&buf) + gob.Register(value) + + err := enc.Encode(&value) + if err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func deserialize(valueBytes []byte) (interface{}, error) { + var value interface{} + buf := bytes.NewBuffer(valueBytes) + dec := gob.NewDecoder(buf) + + err := dec.Decode(&value) + if err != nil { + return nil, err + } + + return value, nil +} +``` + +通过这些,我们已经设法在 BigCache 中存储 `interface{}` 值了。现在我的团队的服务效率提高了一些。太酷了!如果您正在寻找一个更全面的实现,请查看我的 [gist](https://gist.github.com/calebschoepp/0165d92de412e288aa7441e792d0aa3a)。 + +如果您喜欢这篇文章,请查看我的 [博客](https://calebschoepp.com/blog) 以获取类似内容。 + +--- +via: https://dev.to/calebschoepp/storing-empty-interfaces-in-bigcache-1b33 + +作者:[calebschoepp](https://dev.to/calebschoepp) +译者:[alandtsang](https://github.com/alandtsang) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200601-How-to-Write-a-Lexer-in-Go.md b/published/tech/20200601-How-to-Write-a-Lexer-in-Go.md new file mode 100644 index 000000000..774dbc204 --- /dev/null +++ b/published/tech/20200601-How-to-Write-a-Lexer-in-Go.md @@ -0,0 +1,385 @@ +首发于:https://studygolang.com/articles/34519 + +# 如何用 Go 编写词法分析器 + +*词法分析器是所有现代编译器的第一阶段,但是如何编写呢?让我们用 Go 从头开始构建一个。* + +![lexer](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200601-How-to-Write-a-Lexer-in-Go/how-to-write-a-lexer-in-go-featured.jpg) + +## 什么是词法分析器? + +词法分析器有时也称为扫描器,它读取源程序并将输入转换为标记流。这是编译过程中非常重要的一步,因为解析器使用这些标记来创建 AST(抽象语法树)。如果你不熟悉解析器和 AST 也不用担心,这篇文章将只专注于构建词法分析器。 + +## 词法分析器实战 + +在我们编写自己的词法分析器之前,让我们看一下 Go 的词法分析器,以便更好地了解“标记”的含义。你可以使用 Go 的 scanner package 来仔细查看 Go 的词法分析器到底发出了什么标记。这个 [文档](https://golang.org/pkg/go/scanner/#Scanner.Scan) 中有实现这一点的示例。这是我们测试 scanner 的程序: + +*main.go* +```go +package main + +import "fmt" + +func main() { + fmt.Println("Hello world!") +} +``` + +这是发出的标记: + +*输出* +``` +1:1 package "package" +1:9 IDENT "main" +1:13 ; "\n" +3:1 import "import" +3:8 STRING "\"fmt\"" +3:13 ; "\n" +5:1 func "func" +5:6 IDENT "main" +5:10 ( "" +5:11 ) "" +5:13 { "" +6:2 IDENT "fmt" +6:5 . "" +6:6 IDENT "Println" +6:13 ( "" +6:14 STRING "\"Hello world!\"" +6:28 ) "" +6:29 ; "\n" +7:1 } "" +7:2 ; "\n" +``` + +第一列包含标记的位置,第二列是标记的类型,最后一列是标记的字面值。这里有一些重要的事情需要注意。首先,词法分析器不发出任何制表符或空格。这是因为 Go 的语法并不依赖于这些东西。另一件需要注意的事情是 **IDENT** 标记。基本上,在 Go 中任何不是关键字的东西都将被标记为标识符,伴随的字符串将被标记为字面值。 + +如果你想知道这些标记为什么有用,那是因为它们以解析器能够理解的方式表示源程序! + +## 语言 + +现在我们已经在实践中了解了词法分析器,让我们从头开始构建自己的词法分析器!我们首先需要为编程语言定义语法。为简单起见,只包括一些不同的东西: + +*program → expr** + +*expr → assignment | infixExpr | **int*** + +*assignment → **id** = expr ;* + +*infixExp → expr infixOp expr ;* + +*infixOp → **+** | **-** | **\*** | **/*** + +任何粗体被称为 *terminal*,意味着它不能被进一步扩展。在构建词法分析器时,*terminals* 在构建词法分析器时非常重要,稍后我们将看到这一点。语法可以这样读取: + +*程序是零个或多个表达式组成的列表。表达式可以是赋值、中缀表达式或整数等等。* + +在构建解析器时,语法变得更加重要,但是现在定义语法很重要,这样才能知道词法分析器应该发出哪些标记! + +## 标记 + +上面的语法使我们可以定义词法分析器在扫描时应发出的标记。标记只是 *terminals*!我们还将包括一个 **EOF** 和 **ILLEGAL** 标记,以便分别表示程序的结束和语言中不合法的字符。 + +*lexer.go* +```go +type Token int + +const ( + EOF = iota + ILLEGAL + IDENT + INT + SEMI // ; + + // 中缀操作 + ADD // + + SUB // - + MUL // * + DIV // / + + ASSIGN // = +) + +var tokens = []string{ + EOF: "EOF", + ILLEGAL: "ILLEGAL", + IDENT: "IDENT", + INT: "INT", + SEMI: ";", + + // 中缀操作 + ADD: "+", + SUB: "-", + MUL: "*", + DIV: "/", + + ASSIGN: "=", +} + +func (t Token) String() string { + return tokens[t] +} +``` + +## 扫描输入 + +现在我们准备扫描源程序并发出一些标记!首先,我们将创建一个保留某些状态的 Lexer 结构: + +*lexer.go* +```go +type Position struct { + line int + column int +} + +type Lexer struct { + pos Position + reader *bufio.Reader +} + +func NewLexer(reader io.Reader) *Lexer { + return &Lexer{ + pos: Position{line: 1, column: 0}, + reader: bufio.NewReader(reader), + } +} +``` + +调用者需要创建带有适当源文件的 reader,并在创建 Lexer 时将其作为参数传递。 + +接下来,添加一个每次返回单个标记的 `Lex` 函数。然后,调用者将能够连续调用 `Lex`,直到返回 EOF 标记。首先,处理输入文件末尾的情况。 + +*lexer.go* +```go +// Lex 扫描输入中的下一个标记。返回标记的位置,标记的类型和字面值。 +func (l *Lexer) Lex() (Position, Token, string) { + // 循环直到返回一个标记为止 + for { + r, _, err := l.reader.ReadRune() + if err != nil { + if err == io.EOF { + return l.pos, EOF, "" + } + + // 在这一点上我们无能为力,编译器应该将原始错误返回给用户 + panic(err) + } + } +} +``` + +然后,添加一些逻辑来处理语法中的一些更基本的 *terminals*。我们可以使用 switch 语句来检查是否遇到了以下 *terminals* 之一: + +*lexer.go* +```go +func (l *Lexer) Lex() (Position, Token, string) { + // 循环直到返回一个标记位置 + for { + … + // 将列更新为新读取的字符的位置 + l.pos.column++ + + switch r { + case '\n': + l.resetPosition() + case ';': + return l.pos, SEMI, ";" + case '+': + return l.pos, ADD, "+" + case '-': + return l.pos, SUB, "-" + case '*': + return l.pos, MUL, "*" + case '/': + return l.pos, DIV, "/" + case '=': + return l.pos, ASSIGN, "=" + default: + if unicode.IsSpace(r) { + continue + } + } + } +} + +func (l *Lexer) resetPosition() { + l.pos.line++ + l.pos.column = 0 +} +``` + +这使我们可以对除标识符和整数之外的所有内容进行 lex,这非常简洁!接下来我们处理整数。我们需要检测是否看到了数字。如果有的话,扫描后面剩下的数字来标记这个整数。 + +*lexer.go* +```go +func (l *Lexer) Lex() (Position, Token, string) { + // 循环直到返回一个标记为止 + for { + … + switch r { + … + default: + if unicode.IsSpace(r) { + continue + } else if unicode.IsDigit(r) { + // 备份并让 lexInt 重新扫描 int 的开头 + startPos := l.pos + l.backup() + lit := l.lexInt() + return startPos, INT, lit + } + } + } +} + +func (l *Lexer) backup() { + if err := l.reader.UnreadRune(); err != nil { + panic(err) + } + + l.pos.column-- +} + +// lexInt 扫描输入直到整数的结尾,然后返回字面值。 +func (l *Lexer) lexInt() string { + var lit string + for { + r, _, err := l.reader.ReadRune() + if err != nil { + if err == io.EOF { + return lit + } + } + + l.pos.column++ + if unicode.IsDigit(r) { + lit = lit + string(r) + } else { + // 不是整型 + l.backup() + return lit + } + } +} +``` + +如你所见,`lexInt` 仅扫描所有连续的数字,然后返回字面值。处理标识符可以用类似的方式完成,但是,我们应该定义标识符中哪些字符有效。对于我们的语言,我们只允许使用大写和小写字母,其他所有内容都应视为 **ILLEGAL** 标记。 + +*lexer.go* +```go +// Lex 扫描输入中的下一个标记。它返回标记的位置,标记的类型和字面值。 +func (l *Lexer) Lex() (Position, Token, string) { + // 循环直到返回一个标记为止 + for { + ... + switch r { + ... + default: + if unicode.IsSpace(r) { + continue + } else if unicode.IsDigit(r) { + // 备份并让 lexInt 重新扫描 int 的开头 + startPos := l.pos + l.backup() + lit := l.lexInt() + return startPos, INT, lit + } else if unicode.IsLetter(r) { + // 备份并让 lexIdent 重新扫描 ident 的开头 + startPos := l.pos + l.backup() + lit := l.lexIdent() + return startPos, IDENT, lit + } else { + return l.pos, ILLEGAL, string(r) + } + } + } +} + +// lexIdent 扫描输入,直到标识符结尾,然后返回字面值。 +func (l *Lexer) lexIdent() string { + var lit string + for { + r, _, err := l.reader.ReadRune() + if err != nil { + if err == io.EOF { + // 到达标识符末尾 + return lit + } + } + + l.pos.column++ + if unicode.IsLetter(r) { + lit = lit + string(r) + } else { + // 扫描到标识符中没有的东西 + l.backup() + return lit + } + } +} +``` + +请务必注意,词法分析不会捕获诸如未定义的变量或无效的语法之类的错误。它唯一关心的是词法正确性(即我们的语言中允许使用的字符)。下面是运行词法分析器并查看输出的方法: + +*lexer.go* +```go +func main() { + file, err := os.Open("input.test") + if err != nil { + panic(err) + } + + lexer := NewLexer(file) + for { + pos, tok, lit := lexer.Lex() + if tok == EOF { + break + } + + fmt.Printf("%d:%d\t%s\t%s\n", pos.line, pos.column, tok, lit) + } +} +``` + +如果通过运行 `go run lexer.go` 并使用以下输入运行我们的词法分析器,你将看到发出了标记! + +*input.test* +``` +a = 5; +b = a + 6; +c + 123; +5+12; +``` + +*输出* +``` +1:1 IDENT a +1:3 = = +1:5 INT 5 +1:6 ; ; +2:1 IDENT b +2:3 = = +2:5 IDENT a +2:7 + + +2:9 INT 6 +2:10 ; ; +3:1 IDENT c +3:3 + + +3:5 INT 123 +3:8 ; ; +4:1 INT 5 +4:2 + + +4:3 INT 12 +4:5 ; ; +``` + +希望你能从这篇文章中学到一些东西,我也很乐意在评论区听到你的反馈。这篇文章的所有代码都可以在我的 [GitHub](https://github.com/aaronraff/blog-code/tree/master/how-to-write-a-lexer-in-go) 上找到。里面还包含我未介绍的单元测试。 + +--- +via: https://www.aaronraff.dev/blog/how-to-write-a-lexer-in-go + +作者:[Aaron RaffLogo](https://www.aaronraff.dev/) +译者:[alandtsang](https://github.com/alandtsang) +校对:[unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200603-Why-Not-Use-Kubernetes.md b/published/tech/20200603-Why-Not-Use-Kubernetes.md new file mode 100644 index 000000000..2ccc8eb44 --- /dev/null +++ b/published/tech/20200603-Why-Not-Use-Kubernetes.md @@ -0,0 +1,74 @@ +首发于:https://studygolang.com/articles/30256 + +# 为什么不使用 Kubernetes + +![When to choose Kubernetes?](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200603-Why-Not-Use-Kubernetes/00.png) + +很多团队都很兴奋地开始使用 Kubernetes。其中一些团队希望能充分利用它的弹性、灵活性、可移植性、可靠性以及其他的一些 Kubernetes 能原生地提供的优势。也有些团队只是热衷于技术,仅仅想使用下这个平台,来更好地了解它。还有一些开发者想获得一些使用它的经验,这样他们的简历上就可以添加一项很多公司急需的技能。总之,现在大部分开发者出于不同的目的都想要使用 Kubernetes。 + +使用 Kubernetes 有好处也有坏处。 + +## 设计 Kubernetes 的初衷是用来解决分布式架构的问题的 + +来看[官网文档网站](https://kubernetes.io/docs/concepts/overview/what-is-kubernetes/)的定义: + +> “Kubernetes 为你提供了能灵活运行分布式系统的框架。它的能力体现在对你的应用进行扩缩容和故障转移,提供部署模式,等等”。 + +它不是仅用于分布式系统的,而是用于容器化应用。即便如此,它提供了很多可以让管理分布式系统和扩缩容变得更容易地资源,就像微服务解决方案一样。它也被认为是一个编排系统。 + +> [自动化](https://www.redhat.com/en/topics/automation)和编排不一样,但是也有关联。自动化通过减少和替换掉人与 IT 系统的交互使用软件来执行任务,这样能减少资源消耗,降低复杂度和减少错误,进而使得系统更加高效。 +> +> 总之,自动化表示使某单一的任务自动执行。它与编排不同,编排是指在多个不同的系统间的多个步骤中如何自动化执行某一个处理或工作流。当你开始把自动化编进你的处理流程中时,你可以编排它们,让他们自动执行。 +> +> — [编排是什么?RedHat 官方网站](https://www.redhat.com/en/topics/automation/what-is-orchestration) + +换句话说,Kubernetes 使管理复杂的解决方案变得更加容易,而如果没有适当的编排系统,这些解决方案将很难维护。虽然您可以自己实施 DevOps 工程实践,但如果要从数十种服务扩展到数百种服务,则无法扩展。 + +## Kubernetes 很复杂 + +为了充分利用它的各个功能,开发者和 IT 操作者们必须掌握容器、网络、安全、移植性、弹性和 Kubernetes 本身相关的知识。为了合理地使用它的负载,你应该先了解每个组件是如何工作的。为了管理一个集群,你应该了解它的架构、存储、API 和后台管理系统,而这可能与传统的虚拟化环境不一样。为了实施某个方案,你应该先了解如何集成工具来部署、监控以及追踪服务,诸如 [Helm](https://helm.sh/) 和 [Istio](https://istio.io/)。这里涉及大量的新概念,因此你的团队要做好充足的准备来迎接挑战。 + +## Kubernetes 对于小的解决方案花费很大 + +为了理解原因,我们先来加深下对 Kubernetes 的一个很重要的概念的认识 — 弹性。为了体现弹性,你需要更多的节点 — 比运行应用程序所需的最少节点数要多一点。当某个节点挂掉时,请求的 pod 会迁移到可用的节点。在生产中的工作负载,为了集群有弹性,推荐至少部署三个节点。 + +如果你只需要维护一个单一的应用,那么显然不需要像上面那样做。但是即使你有十几个应用,你仍然要考虑维护集群的收益和集群的开销是否平衡。 + +**维护环境的开销还包括运维支持。**平台越复杂,对运维人员的专业性要求就越高。你可能需要雇佣第三方的专业团队来提供支持,或者需要一个诸如 Openshift 的包含支持服务的解决方案。 + +## 什么时候该选择 Kubernetes + +基于你使用的架构,应用的数量和各应用间的依赖程度,你的团队能提供的运维能力,你可以在众多可用的技术中判断 Kubernetes 是否是最佳选择。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200603-Why-Not-Use-Kubernetes/01.jpeg) + +按照[基于容器的 Web 应用](https://azure.microsoft.com/en-us/services/app-service/containers/)部署完后,你有了一套可以用于生产的环境。在遵照流程做了完整的计划,有 SSL 特性,并安装了 [Application Insights](https://docs.microsoft.com/en-us/azure/azure-monitor/app/cloudservices) 后,你的环境会变得安全、可伸缩,几乎不需要运维工作。 + +如果你的应用都是独立的,或者只连接了少量的应用,也许在同一个虚拟网络中组合使用 Azure Web Apps 和 [容器实例](https://azure.microsoft.com/en-us/services/container-instances/)就足够了。 + +然而,如果你的容器化的应用数量会增长,那么使用 Kubernetes 管理他们会很有趣。你会在单一的、集中式的环境中管理像 Web 应用,API 和 循环的任务等不同类型的应用。你的团队也能把精力放在 Kubernetes 而不是浪费在选择不同的云原生解决方案上。 + +如果你要处理分布式的场景,像微服务,那么请选择 Kubernetes。分布式架构很复杂,而 Kubernetes 就是为它设计的。能完全契合分布式的应用,以及可以根据应用的需求进行扩展,除了 Kubernetes,我想不到更好的任何其他平台。 + +## 总结 + +当你只需要处理少量的容器化的应用、各应用相互独立的或者应用间几乎不相互依赖时,选择其他的管理方式如[基于容器的 Web 应用](https://azure.microsoft.com/en-us/services/app-service/containers/)或[Azure 容器实例](https://azure.microsoft.com/en-us/services/container-instances/) — 或者两者结合 — 可能更简单,花费更少。 + +如果你的团队对 Kubernetes 的能力很满意,并且你的容器化的应用数量会越来越多,那么可能值得你在一个 Kubernetes 平台(如 [Azure Kubernetes 服务](https://azure.microsoft.com/en-us/services/kubernetes-service/))中进行集中式管理。 + +*Kubernetes 是一个用来提高性能和减少分布式系统中运维工作的平台。*它主要用来降低复杂场景(如 微服务)中运维的复杂度。 + +如果你不需要处理大量的应用,没有使用分布式架构,或者团队中没有技术专家,那么你就不能享受到 Kubernetes 带来的便利 — 因为它不是为你设计的。使用 Kubernetes 只会给你的解决方案增加意外和不符合预期的复杂度。 + +如果你想更好地了解容器化应用的管理方式和如何选择最适合的方式,那么请查阅下面的文章: + +[为你的应用选择 Azure 计算服务](https://docs.microsoft.com/en-us/azure/architecture/guide/technology-choices/compute-decision-tree) + +--- +via: https://medium.com/better-programming/why-not-use-kubernetes-52a89ada5e22 + +作者:[Grazi Bonizi](https://medium.com/@grazibonizi) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200605-Go-Stringer-Command-Efficiency-Through-Code-Generation.md b/published/tech/20200605-Go-Stringer-Command-Efficiency-Through-Code-Generation.md new file mode 100644 index 000000000..1c0f9892e --- /dev/null +++ b/published/tech/20200605-Go-Stringer-Command-Efficiency-Through-Code-Generation.md @@ -0,0 +1,132 @@ +首发于:https://studygolang.com/articles/34521 + +# Go: stringer 命令,通过代码生成提高效率 + +![由 Renee French 创作的原始 Go Gopher 作品,为“ Go 的旅程”创作的插图。](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200605-Go-Stringer-Command-Efficiency-Through-Code-Generation/00.png) + +ℹ️ 这篇文章基于 Go 1.13。 + +`stringer` 命令的目标是自动生成满足 `fmt.Stringer` 接口的方法。它将为指定的类型生成 `String()` 方法, `String()` 返回的字符串用于描述该类型。 + +## 例子 + +这个[命令的文档](https://godoc.org/golang.org/x/tools/cmd/stringer) 中给我们提供了一个学习的例子,如下: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200605-Go-Stringer-Command-Efficiency-Through-Code-Generation/01.png) + +输出如下: + +``` +1 +``` + +产生的日志是一个常量的值,这可能会让你感到困惑。 +让我们用命令 `stringer -type=Pill` 生成 `String()` 方法吧: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200605-Go-Stringer-Command-Efficiency-Through-Code-Generation/02.png) + +生成了新的 `String()` 函数,运行当前代码时输出如下: + +``` +Aspirin +``` + +现在描述该类型的是一个字符串,而不是它实际的常量值了。 + `stringer` 也可以与 `go generate` 命令完美配合,使其功能更强大。只需在代码中添加以下指令即可: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200605-Go-Stringer-Command-Efficiency-Through-Code-Generation/03.png) + +然后,运行 `go generate` 命令将会为你所有的类型自动生成新的函数。 + +## 效率 + +`stringer` 生成了一个包含每一个字符串的 ` 长字符串 ` 和一个包含每一个字符串索引的数组。在我们这个例子里,读 `Aspirin` 即是读 ` 长字符串 ` 的索引 7 到 13 组成的字符串: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200605-Go-Stringer-Command-Efficiency-Through-Code-Generation/04.png) + +但是它有多快、多高效?我们来和另外两种方案对比一下: + +* 硬编码 `String()` 函数 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200605-Go-Stringer-Command-Efficiency-Through-Code-Generation/05.png) + +下面是一个包含 20 个常量的基准测试: + +``` +name time/op +Stringer-4 4.16ns ± 2% +StringerWithSwitch-4 3.81ns ± 1% +``` + +包含 100 个常量的基准测试: + +``` +name time/op +Stringer-4 4.96ns ± 0% +StringerWithSwitch-4 4.99ns ± 1% +``` + +常量越多,效率越高。这是有道理的。从内存中加载一个值比一些跳转指令(表示 if 条件的汇编指令)更具有膨胀性。 + +然而,switch 语句分支越多,跳转指令的数量就越多。从某种程度上来说,从内存中加载将会变得更有效。 + +* `String()` 函数输出一个 map + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200605-Go-Stringer-Command-Efficiency-Through-Code-Generation/06.png) + +下面是一个包含 20 个常量的基准测试: + +``` +name time/op +Stringer-4 4.16ns ± 2% +StringerWithMap-4 28.60ns ± 2% +``` + +使用 map 要慢得多,因为它必须进行函数调用,并且在 bucket 中查找不像访问切片的索引那么简单。 + +想了解更多关于 map 的信息和内部结构,我建议你阅读我的文章 "[Go: Map Design by Code](https://medium.com/a-journey-with-go/go-map-design-by-code-part-ii-50d111557c08)" + +## 自检器 + +在生成的代码中,有一些纯粹是为了校验的目的。下面是这些指令: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200605-Go-Stringer-Command-Efficiency-Through-Code-Generation/07.png) + +`stringer` 将常量的名称与值一起写入每行。在本例中,`Aspirin` 的值为 `2`。更新常量的名称或其值会产生错误 + +* 更新名称但不重新生成 `String()` 函数: + +``` +./pill_string.go:12:8: undefined: Aspirin +``` + +* 更新值但不重新生成 `String()` 函数: + +``` +./pill_string.go:12:7: invalid array index Aspirin - 1 (out of bounds for 1-element array) +./pill_string.go:13:7: invalid array index Ibuprofen - 2 (index must be non-negative +``` + +然而,当我们添加一个新的常量的情况下 -- 这里下一个值为 `3`,并且不更新生成的文件,`stringer` 会有一个默认值: + +``` +Pill(3) +``` + +添加这个自检不会有任何影响,因为它在编译时被删除了。可以通过查看程序生成的 asm 代码来确认 : + +``` +➜ go tool compile -S main.go pill_string.go | grep "\"\".Pill\.[^\s]* STEXT" +"".Pill.String STEXT size=275 args=0x18 locals=0x50 +``` + +只有 `String()` 函数被编译到二进制文件中 , 自检对性能或二进制大小没有影响。 + +--- +via: https://medium.com/a-journey-with-go/go-stringer-command-efficiency-through-code-generation-df49f97f3954 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[kagxin](https://github.com/kagxin) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200608-Duck-Typing-vs-Structural-Typing-vs-Nominal-Typing.md b/published/tech/20200608-Duck-Typing-vs-Structural-Typing-vs-Nominal-Typing.md new file mode 100644 index 000000000..43eac9310 --- /dev/null +++ b/published/tech/20200608-Duck-Typing-vs-Structural-Typing-vs-Nominal-Typing.md @@ -0,0 +1,122 @@ +首发于:https://studygolang.com/articles/34522 + +# 鸭子类型 vs 结构化类型 vs 标称类型 + +Go 语言是哪一种? + +编程语言具有类型概念 - 布尔类型,字符串,整型或者被称为类或者结构体的更加复杂的结构。根据如何将类型解析并赋值给各种构造(例如变量,表达式,函数,函数参数等),编程语言可以归类为鸭子类型,结构化类型或标称类型。 + +本质上,分类决定了对象如何被解析并推断为具体的类型。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200608-duck-typing-vs-structural-typing-vs-nominal-typing/1_zPb6iQvY7faJQ12GqCpqrQ.png) + +**鸭子类型语言**使用鸭子测试来评估对象是否可以被解析为特定的类型。鸭子测试表示: +> 如果它看起来像鸭子,像鸭子一样游泳,像鸭子一样嘎嘎叫,那它很可能就是鸭子。 + +**我将使用 Go 语言语法来解释这些想法 - 将这些示例作为伪代码阅读 - 它与 Go 语言规则无关* + +以下代码片段是鸭子类型语言的示例。因为 Mallard 可以嘎嘎叫,所以它是一只鸭子。 + +```go +type Mallard struct { +} +func (m Mallard) quack() { +} +func makeDuckQuack(duck Duck) { + duck.quack() +} +func main() { + makeDuckQuack(Mallard{}) +} +``` + +在上面的示例中,Duck 可以是任意类型,它可以是接口或者另一个类型,但是对于 makeDuckQuack 函数而言,唯一的要求就是传递一个可以执行 quack 函数的对象作为参数。 + +鸭子类型语言通常没有编译期检查。类型的解析和解释发生在运行时 - 这可能导致运行时错误。 + +例如,以下代码片段可以正常编译,但是由于 *Dog* 类型不支持 *quack()* 函数,会产生运行时错误。 + +```go +type Dog struct { +} +func (d Dog) bark() { +} +func makeDuckQuack(duck Duck) { + duck.quack() +} +func main() { + makeDuckQuack(Dog{}) +} +``` + +Python 和 Javascript 是流行的鸭子类型语言。 + +在另一端,**标称类型语言**期望程序员明确地对类型进行调用以供编译器解释。 + +```go +type Duck interface { + quack() +} +type Mallard struct { //Mallard doesn't implement Duck interface +} +func (m Mallard) quack() { +} +func makeDuckQuack(duck Duck) { + duck.quack() +} +func main() { + makeDuckQuack(Mallard{}) //This will not work as Mallard doesn't explicitly implement Duck. +} +``` + +在上面的示例中,程序员需要明确地实现 Duck 接口。可以说,显示关系意味着更强的可读性。 + +明确定义 Marllard 和 Duck 之间的关系也意味着包含 Marllard 结构的包依赖于包含 Duck 结构的包。这可能永远也不是一件好事,并且增加了整个应用程序的复杂性。 + +标称类型语言主要包括 Java,C++。 + +结构化类型语言介于两者之间,应用程序员无需明确定义用于解释的类型,但是编译器会进行编译期检测来确保程序的完整性。 + +```go +type Duck interface { + quack() +} +type Mallard struct { //Mallard doesn't implement Duck interface +} +func (m Mallard) quack() { +} +type Dog struct { +} +func (d Dog) bark() { +} +func makeDuckQuack(duck Duck) { + duck.quack() +} +func main() { + makeDuckQuack(Mallard{}) // Okay + makeDuckQuack(Dog{}) // Not Okay +} +``` + +在上面的示例中,程序员无需指定 Mallard 是 Duck 类型。语言编译器将 Mallard 解释为 Duck - 因为它具有 quack 函数。但是 Dog 不是一个 Duck,因为 Dog 不具有 quack 函数。 + +Go 是结构化类型语言。 + +## 结论 + +*鸭子类型语言*为程序员提供了最大的灵活性。程序员只需写最少量的代码。但是这些语言可能并不安全,会产生运行时错误。 + +*标称类型语言*要求程序员显示调用类型,这意味着更多的代码和更少的灵活性(附加的依赖)。 + +*结构化类型语言*提供了一种平衡,它需要编译期检查,但不需要显示声明依赖。 + +在使用 Go(结构化类型)编程之前,我主要使用 Java(标称类型)。我喜欢结构化类型语言提供的灵活性,而又不会影响编译期类型安全。 + +--- +via: https://medium.com/higher-order-functions/duck-typing-vs-structural-typing-vs-nominal-typing-e0881860bf10 + +作者:[Saurabh Nayar](https://medium.com/@nayar.saurabh) +译者:[DoubleLuck](https://github.com/DoubleLuck) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200610-Go-String-and-Conversion-Optimization.md b/published/tech/20200610-Go-String-and-Conversion-Optimization.md new file mode 100644 index 000000000..b2cd3e13d --- /dev/null +++ b/published/tech/20200610-Go-String-and-Conversion-Optimization.md @@ -0,0 +1,85 @@ +首发于:https://studygolang.com/articles/34525 + +# Go:字符串以及转换优化 + +![由 Renee French 创作的原始 Go Gopher 作品,为“ Go 的旅程”创作的插图。](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200610-Go-String-and-Conversion-Optimization/Illustration.png) + +ℹ️ 这篇文章基于 Go 1.14。 + +在 Go 语言中,将 byte 数组转换为 string 时,随着转换后字符串的拷贝,可能会触发内存分配。然而,将 bytes 转换为 string 仅仅是为了满足代码约束,比如在 switch 语句中的比较,又比如在 map 中的 key,这些场景下的转换绝对是在浪费 CPU 时间。来一起看一些案例,以及一些已有的优化。 + +## 转换操作(Conversion) + +将 byte 数组转换为 string 涉及的操作有: + +- 如果变量超过了当前堆栈帧的作用域,在堆上为新的 string 分配内存。 +- bytes 到 string 的拷贝 + +*关于逃逸分析的更多细节,建议阅读我的文章:“[Go:介绍逃逸分析。](https://studygolang.com/articles/34524)”* + +这是完成这两个步骤的简要程序: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200610-Go-String-and-Conversion-Optimization/a-simple-program.png) + +这是该转换操作的示意图: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200610-Go-String-and-Conversion-Optimization/diagram-of-conversion.png) + +*如果想更多了解 copy 函数,建议阅读我的文章“[Go:切片以及内存管理](https://medium.com/a-journey-with-go/go-slice-and-memory-management-670498bb52be)”* + +在运行时层面,Go 在转换期间只提供一种优化。如果转换的 byte 数字实际只包含一个字节,返回的 string 会指向一个静态的 byte 数组,该数组嵌入在运行时中: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200610-Go-String-and-Conversion-Optimization/point-to-a-static-array-of-byte.png) + +然而,如果这个 string 之后被修改,分配新值之前会从堆上面分配内存。 + +Go 编译器同样提供一些优化,可以省略我们所见到的两个转换阶段。 + +## Switch + +先以一个以比较为目的,转换为 string 的示例开始: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200610-Go-String-and-Conversion-Optimization/an-example-of-conversion-to-string.png) + +*这个用来说明字符串优化的实例通过使用 `getBytes` 函数强制在堆上进行分配。这样避免了要介绍的字符串优化被编译器的其他优化所隐藏。* + +在这个示例中,仅有 `switch` 指令使用了转换,而且由于仅仅需要与实际内容进行比较,Go 可以避免转换操作。Go 实际上通过移除转换操作,并且直接指向底层的 byte 数组来优化这段代码。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200610-Go-String-and-Conversion-Optimization/pointing-directly-to-the-backed-array-of-bytes.png) + +我们也可以通过生成的汇编指令来了解具体优化细节: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200610-Go-String-and-Conversion-Optimization/the-exact-optimization.png) + +Go 在比较操作中直接使用返回的 bytes。首先比较 byte 数组和 `case` 语句(case 后面的字符串)的大小,之后检查字符串本身(字面值)。在 `switch` 语句外分配 string,会导致内存的分配,因为编译器无法得知这个 string 后续是否还会使用。 + +## 优化 + +`switch` 并不是字符串转换的唯一的一个优化。Go 编译器会在其他示例中应用这样的优化,比如: + +- 访问 map 中的元素。这是一个例子: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200610-Go-String-and-Conversion-Optimization/Accessing-to-an-element-of-a-map.png) + +当访问 map 时,实际上不需要进行转换,这样能使访问更快。 + +- 字符串连接。这是一个例子: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200610-Go-String-and-Conversion-Optimization/String-concatenation.png) + +byte 数组与一些 string 的连接不会引起任何内存分配,也不会引起 byte 的任何转换。就像前面看到的一样,连接会直接引用底层的数组。 + +- 字符串比较。这里是一些例子: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200610-Go-String-and-Conversion-Optimization/String-comparisons.png) + +这个例子与 `switch` 类似。首先比较 string 的大小和 byte 数组的大小,之后再比较字符串。 + +--- +via: https://medium.com/a-journey-with-go/go-string-conversion-optimization-767b019b75ef + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[dust347](https://github.com/dust347) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200611-Why-Golang-Is-Top-of-Mind-for-DevOps-Professionals.md b/published/tech/20200611-Why-Golang-Is-Top-of-Mind-for-DevOps-Professionals.md new file mode 100644 index 000000000..eebf5e5fe --- /dev/null +++ b/published/tech/20200611-Why-Golang-Is-Top-of-Mind-for-DevOps-Professionals.md @@ -0,0 +1,120 @@ +首发于:https://studygolang.com/articles/35193 + +# 为什么说 Golang 是 DevOps 专业人士的第一首选? + +Golang 是当今最受欢迎的编程语言之一,现在就让我们来看看它在 DevOps 空间中能够做什么? + +Golang,也称为 “Go”,是一种具备快速和高性能的编译型语言,这是被设计成为易于阅读和理解的原因。Go 是由 Rob Pike,Robert Griesemer 和 Ken Thompson 等人在 Google 时编写的,于 2009 年 11 月首次发布。 + +Golang 被设计成高度简洁和易于理解的语法。 + +这是 Golang 中经典的 “hello world” 示例代码。 + +```go +package main +import "fmt" + func main() { + fmt.Println("hello world") +} +``` + +要想运行这段代码,就要在 `hello-world.go` 所在目录中输入以下命令,并且使用 `go run` 运行。 + +```shell +$ go run hello-world.go +hello world +$ go build hello-world.go +$ ls +hello-world hello-world.go +$ ./hello-world +hello world +``` + +## Go 的介绍 + +Go 诞生于 2007 年,当时多核 CPU 的架构随处可见,而且没有编程语言能够简化多线程应用程序的开发工作。安全和高效地管理不同线程是开发人员的重大责任。这和其他编程语言不同,Go 虽然很年轻,但是也很强大。Goroutines 在另一个层面上彻底革新了竞争性编程。 + +经过测试和证明,用 Go 编写的应用程序具有高性能和可伸缩性。Golang 是一种非常高效的语言,就像 C/C++ 一样,还具备像 Java 一样处理并行任务的特性,同时兼具 Python 和 Perl 代码的易于阅读性。相比其他的编程语言,Golang 具有无可争议的架构优势。 + +Go 还被一些大公司使用,例如 BBC、Uber、Novartis、Basecamp and Soundcloud。Uber 公司报道过更高的吞吐量、高性能、延迟和正常运行时间。英国广播公司(BBC)是一个闻名于广播世界新闻中的机构,它将 Go 应用于 Web 后端领域,包括网络爬虫和网页数据提取。而 SoundCloud 公司则将 Go 用于构建和部署系统中。 + +以下是关于 Go 编程语言的 Google 趋势概览,它正在持续且稳定的增长。 + +## 为什么选择 Go ? + +对于具备 C/C++ 学习经验的程序员来讲,学习 Go 是一件毫不费力的事情,并且将祖传代码转换成 Go 程序也是非常简单的。作为一种编译型的静态语言,它比解释型语言要快得多,同时具备了大部分的性能优势。 + +- Go 作为一种与 C 很相似的编程语言,但是除了具有 C 语言的特性之外,Go 还提供了内存安全性、[垃圾回收](https://dzone.com/articles/garbage-collection-a-brief-introduction)、[结构化类型](https://dzone.com/articles/dynamic-static-optional-structural-typing-and-engi)和 CSP 风格的并发性。 +- 在最近的 [Stack Overflow 2020 的调查结果](https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-languages-loved)中,Go 是开发人员中最喜欢和最想要使用的编程语言之一。 + +### 最喜欢和最想要使用的编程语言 + +- Go 很适合用于一般绩效导向的云计算软件。流行的 DevOps 工具是用 Go 编写的,例如 Docker ,甚至是开源的容器编排系统 Kubernetes 都是用 Go 编写的。自 2011 年以来,YouTube 一直在使用 [Vitess](https://opensource.google/projects/vitess) ,它是一个由 Google 构建的分布式数据库系统,而且这个分布式数据库的 MySQL 后端是由 Golang 构建。 +- 在 [2018 年的 Stack Overflow 调查结果](https://insights.stackoverflow.com/survey/2018/#technology)中,Golang 排名第五。根据 [GitHub 关于 2018 年的第二季度报告](https://madnight.github.io/githut/#/pull_requests/2019/4),Golang 的整体增长率接近于 7% ,与上一季度相比增长 1.5 点。到 2019 年第四季度,Golang 的整体增长率已经达到 8% 。 + +## Go 如此受欢迎的原因 + +- Go 是一种静态类型的编译语言,因此你可以更早地发现问题。 +- Go 可以被立即编译为机器代码,因此它的编辑/刷新周期相对较快,并且仍然会编译出更高效的机器代码。 +- Go 的语法设计使得编写高度并发的网络程序变得容易。 +- Go 内置了许多库来支持测试,您可以轻松地定义和测试模块,这进一步提高了代码规范。 +- Go 跨平台特性使得移植代码非常容易,这也是 Go 的最大优势。 +- Go 提供了自动的代码格式化、代码检查和审核工具,它们作为软件包的默认部分;Go 编译器甚至会执行像变量没有被使用的操作。这使其成为一种专业的语言。 +- 正是因为 Go 对并行和并发的原生支持,所以它才会变得如此特别。对于需要大量并发或并行处理、联网、海量计算的应用程序,使得 Go 成为一种更完美的编程语言。 +- Go 是实现云兼容性的最佳选择。Go 还具有更好的垃圾回收能力和性能优异的 network 包,而且还解决了变量没有被使用、多编译和交叉编译的问题。 + +## 让我们看一些是谁在使用 Go 的实际案例 + +### SendGrid 投入 Go + +SendGrid 是一个客户沟通的平台,并于 2014 年将 Go 作为主要开发语言。SendGrid 开发团队需要从根本上转变它们的开发语言,归结为 Scala、Java 和 Go 之间的竞争。当时,SendGrid 在开发中面临最大的挑战是并发编程。寻找具有并发的异步编程的特性,然后将其作为编程语言中的一部分,这是 SendGrid 选择 Go 最令人信服的原因之一。 + +可以在他们的博客上阅读全文:[如何说服您的公司选择 Golang?](https://sendgrid.com/blog/convince-company-go-golang/) + +### Hexac 已经从 Python 转换到 Go + +Hexac 的联合创始人兼 CTO Tigran Bayburtsyan 写了一篇独家文章,分享了他的公司[从 Python 转到 Go 的原因](https://hackernoon.com/5-reasons-why-we-switched-from-python-to-go-4414d5f42690)。根据他们的代码库统计信息,在使用 Go 重构了所有项目之后,他们的代码量比以前减少了 64% 。 + +由于 Go 内置的语言特性,他们节省了大量资源(内存和 CPU )。 + +Go 为他们的开发团队提供了极大的灵活性,可以在所有用例中使用单一的语言,并且效率很高。在 Hexact 股份有限公司中,他们的后端和 API 服务的性能提高了约 30%。 +现在,他们可以实时地处理日志,然后将其传输到数据库;在单个或多个服务中,使用 Websocket 进行流式传输。这就是 Go 语言带来的出色表现。 + +### Salesforce 抛弃 Python 而选择了 Go + +在 2017 年推出 Einstein Analytics 之前,Salesforce 使用 Google 流行的 Go 语言完全重构了他们的后端。Salesforce 首席架构师 Guillaume Le Stum 表示:“ Python 并不能很好地完成多线程工作,而 Go 是专为 Google 生产系统中的重型应用程序而构建的,这门编程语言已经通过了 Google 的测试和许可。因此 Salesforce 选择将 Einstein Analytics(Salesforce 的重要组成部分)从混合 C-Python 应用程序转变为完全 Go 应用程序。请阅读原文:[我们为什么在 Einstein Analytics 放弃 Python 而选择了 Google 的 Go](https://www.zdnet.com/article/salesforce-why-we-ditched-python-for-googles-go-language-in-einstein-analytics/)。 + +### Containerum 优先选择 Go + +Containerum 是一个使用 Go 作为主要开发语言的容器管理平台,已经有大约四年的历史,尽管面临着某些挑战,工程团队仍然认为这是一个不错的选择。选择在 Containerum Platform 上使用 Go 的主要原因是,它由一组比较小的服务组成,这些服务与其他组件进行通信。为了确保这一点,我们非常需要确保接口的兼容性并且要编写简洁、易于阅读和维护的代码。 + +Go 支持添加补丁并允许在代码库中使用准备就绪的组件。例如,图像名称解析认证,关键对象模型等,这是 Containerum 选择 Go 的原因之一。 + +Containerum 之所以考虑使用 Go 语言,是因为它具有许多专业的功能,例如静态类型,语法简洁,标准库,出色的性能,快速的编译等。请阅读原文:[我们为什么使用 Go 来为 Kubernetes 开发 Containerum 平台](https://medium.com/containerum/why-we-use-go-to-develop-containerum-platform-for-kubernetes-3a33d5bdc5ec)。 + +### 流行的 DevOps 工具是用 Go 编写的 + +> Kubernetes, Docker, and Istio + +像 Google 这样的巨型公司曾经考虑使用其他语言编写 Kubernetes ,但是据 Kubernetes 的联合创始人 Joe Beda 称,这些语言都没有 Go 这么有效。这里有一些关于 Kubernetes 为什么要使用 Go 编写的原因,其中包括广泛的标准库、快速的工具、内置并发、垃圾回收和类型安全等。根据 Joe 的说法,Go 中的这些模式和工具鼓励 Kubernetes 开发团队编写结构合理和可重用的代码,这些代码同时将会为他们提供高度的灵活性和速度。 + +[Docker](https://www.docker.com/) 是使用 Go 语言的最大用户。Docker 开发团队之所以喜欢使用 Go,是因为 Go 为他们提供了许多好处:无需依赖项的静态编译、自然语言、完整的开发环境、广泛和强大的标准库和数据类型、强大的鸭子类型以及使用最小的代价为多种架构进行构建的能力。 + +[Istio](https://istio.io/) 是 Kubernetes 生态系统的一部分,也是用 Go 编写的。 + +因为 Kubernetes 也是用 Go 编写的,所以 Istio 使用 Go 进行开发也是一种完美的方法。这不仅是 Go 适应分散的和分布式网络项目的原因之一,也是在 Istio 选择 Go 的主要原因之一。 + +更多详细内容请访问:[证明 Google Go 功能强大的 10 个开源项目](https://www.infoworld.com/article/3442978/10-open-source-projects-proving-the-power-of-google-go.html)。 + +如果您正在使用 Golang 编写应用程序,那么实践 CI / CD 有多困难?这很难,不是吗? + +好吧,不是现在。但是借助 [Go Center GOPROXY](https://search.gocenter.io/) 等最新技术,增长 CI / CD 质量的途径将会变得更加清晰。GoCenter 是不可变的 Go 模块的公共的中央仓库,它允许您搜索模块和版本,也可以轻松地将模块添加到中央仓库,然后公开分享它们。 + +--- +via: https://dzone.com/articles/why-golang-is-top-of-mind-for-devops-professionals + +作者:[Pavan Belagatti](https://dzone.com/users/2879134/pavanshippable.html) +译者:[sunlingbot](https://github.com/sunlingbot) +校对:[unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200619-Containers-the-hard-way-gocker.md b/published/tech/20200619-Containers-the-hard-way-gocker.md new file mode 100644 index 000000000..6e11b9d84 --- /dev/null +++ b/published/tech/20200619-Containers-the-hard-way-gocker.md @@ -0,0 +1,528 @@ +首发于:https://studygolang.com/articles/35194 + +# 容器的艰难之旅:gocker —— Go 实现的迷你 Docker + +容器很受欢迎,但是被误解了。 容器已成为应用程序在服务器上打包和运行的默认方式,最初是由 Docker 普及的。现在,Docker 本身被误解了。它是一个公司的名字和一条命令(更确切地说是一组命令),使你容易地管理容器(创建,运行,删除,连接网络)。但是容器本身是由一组操作系统原语创建的。在本文中,我们将关注 Linux 操作系统上的容器,就像 [Windows 上的容器](https://docs.microsoft.com/en-us/virtualization/windowscontainers/about/) 根本不存在一样。 + +Linux 下没有创建容器的单个系统调用。它们是利用 Linux namespaces 和 control groups 或 cgroups 创建的松散结构。 + +## Gocker 是什么? + +[Gocker](https://github.com/shuveb/containers-the-hard-way) 是一个用 Go 语言从头实现 Docker 核心功能的项目。主要目的是让你了解容器在 Linux 系统调用级别上是如何工作的。Gocker 允许你创建容器,管理容器镜像,执行现有容器中的进程等等。 + +## Gocker 的功能 + +Gocker 可以模拟 Docker 的核心,让你管理 Docker 镜像(从 Docker Hub 获取),运行容器,列出正在运行的容器或在已运行的容器中执行进程: + +- 在容器中运行进程 + - gocker run <--cpus=cpus-max> <--mem=mem-max> <--pids=pids-max> +- 列出正在运行的容器 + - gocker ps +- 在运行的容器中执行进程 + - gocker exec +- 列出本地可用的镜像 + - gocker images +- 删除本地可用的镜像 + - gocker rmi + +### 其他功能 + +- Gocker 使用 Ovelay 文件系统快速创建容器而无需复制整个文件系统,同时还可以在多容器实例间共享容器镜像。 +- Gocker 容器拥有自己的网络命名空间,并且能够访问 Internet。请参阅下面的限制。 +- 你可以控制系统资源,如 CPU 百分比,RAM 数量和进程数。 Gocker 通过利用 cgroups 来实现这一点。 + +## Gocker 容器隔离 + +用 Gocker 创建的容器拥有自己的以下命名空间(参见 run.go 和 network.go): + +- File system (via chroot) +- PID +- IPC +- UTS (hostname) +- Mount +- Network + +在创建用于限制以下内容的 cgroups 时,除非为 gocker run 命令指定了 --mem,--cpus 或 --pids 选项,否则容器将使用无限制的资源。这些标志分别限制了容器可以使用的最大 RAM,CPU 核数和 PID。 + +- CPU 核数 +- RAM +- PID 个数 (限制进程) + +## Namespaces 基础 + +所有 Linux 计算机在启动时都是 “default” 命名空间的一部分。在计算机上创建的进程也继承默认命名空间。换句话说,进程可以查看正在运行的其他进程,列出网络接口,列出挂载点,列出名为 IPC 的对象或权限允许的文件,因为所有对象也都存在于默认命名空间中。例如,创建一个进程时,我们可以告诉 Linux 为我们创建一个新的 PID 命名空间,在这种情况下,新进程及其任何后代形成一个新的层次结构或 PID,而新创建的初始进程 PID 为 1, 就像一个 Linux 机器上特殊的 init 进程一样。假设使用新的 PID 命名空间创建了一个名为“ new_child”的进程。当该进程或其后代使用诸如 getpid() 或 getppid() 之类的系统调用时,它们将看到来自新命名空间的 PID。例如,在新创建的 PID 命名空间中,这两个系统调用的结果都是 1。而当你从默认命名空间查看 new_child 的 PID 时,当然不会为它分配 1,也就是默认命名空间中的 init。将根据在分配时间前后分配的一系列 PID 进程,为它分配更多的 PID。 + +Linux 操作系统提供了在创建进程时或与之关联的正在运行的进程创建新命名空间的方法。所有命名空间,无论何种类型,都被分配了内部 ID。命名空间是一种内核对象。一个进程只能属于一个命名空间。例如,假设进程新的子 PID 命名空间被设置为内部 ID 为 0x87654321 的命名空间,它不能属于另一个 PID 命名空间。但是可能存在其他属于同一 PID 命名空间 0x87654321 的其他进程。同样,new_child 的后代将自动属于相同的 PID 命名空间。命名空间是继承的。 + +你可以使用 lsns 列出计算机中的所有命名空间。即使你的计算机上没有运行任何容器,也可能会看到与各种命名空间相关的其他进程。这表明命名空间并不仅仅是在容器的上下文中使用,它们可以在任何地方使用。它们提供隔离,是一项强大的安全功能。在现代 Linux 系统上,你会看到 init,systemd,多个系统守护进程,Chrome,Slack,当然还有使用各种命名空间的 Docker 容器。让我们看一下我机器上 lsns 的输出: + +```bash + NS TYPE NPROCS PID USER COMMAND +4026532281 mnt 1 313 root /usr/lib/systemd/systemd-udevd +4026532282 uts 1 313 root /usr/lib/systemd/systemd-udevd +4026532313 mnt 1 483 systemd-timesync /usr/lib/systemd/systemd-timesyncd +4026532332 uts 1 483 systemd-timesync /usr/lib/systemd/systemd-timesyncd +4026532334 mnt 1 502 root /usr/bin/NetworkManager --no-daemon +4026532335 mnt 1 503 root /usr/lib/systemd/systemd-logind +4026532336 uts 1 503 root /usr/lib/systemd/systemd-logind +4026532341 pid 1 1943 shuveb /opt/google/chrome/nacl_helper +4026532343 pid 2 1941 shuveb /opt/google/chrome/chrome --type=zygote +4026532345 net 50 1941 shuveb /opt/google/chrome/chrome --type=zygote +4026532449 mnt 1 547 root /usr/lib/boltd +4026532489 mnt 1 580 root /usr/lib/bluetooth/bluetoothd +4026532579 net 1 1943 shuveb /opt/google/chrome/nacl_helper +4026532661 mnt 1 766 root /usr/lib/upowerd +4026532664 user 1 766 root /usr/lib/upowerd +4026532665 pid 1 2521 shuveb /opt/google/chrome/chrome --type=renderer +4026532667 net 1 836 rtkit /usr/lib/rtkit-daemon +4026532753 mnt 1 943 colord /usr/lib/colord +4026532769 user 1 1943 shuveb /opt/google/chrome/nacl_helper +4026532770 user 50 1941 shuveb /opt/google/chrome/chrome --type=zygote +4026532771 pid 1 2010 shuveb /opt/google/chrome/chrome --type=renderer +4026532772 pid 1 2765 shuveb /opt/google/chrome/chrome --type=renderer +4026531835 cgroup 294 1 root /sbin/init +4026531836 pid 237 1 root /sbin/init +4026531837 user 238 1 root /sbin/init +4026531838 uts 289 1 root /sbin/init +4026531839 ipc 292 1 root /sbin/init +4026531840 mnt 283 1 root /sbin/init +4026531992 net 236 1 root /sbin/init +4026532912 pid 2 3249 shuveb /usr/lib/slack/slack --type=zygote +4026532914 net 2 3249 shuveb /usr/lib/slack/slack --type=zygote +4026533003 user 2 3249 shuveb /usr/lib/slack/slack --type=zygote +``` + +即使你没有显式创建命名空间,进程也将成为默认命名空间的一部分。所有命名空间的详细信息都记录在 /proc 文件系统中。你可以通过输入 ls -l /proc/self/ns/ 来查看你的 shell 进程所属的命名空间。下面是我的执行结果。另外,这些大多是从 init 继承的: + +```bash +➜ ~ ls -l /proc/self/ns +total 0 +lrwxrwxrwx 1 shuveb shuveb 0 Jun 13 11:44 cgroup -> 'cgroup:[4026531835]' +lrwxrwxrwx 1 shuveb shuveb 0 Jun 13 11:44 ipc -> 'ipc:[4026531839]' +lrwxrwxrwx 1 shuveb shuveb 0 Jun 13 11:44 mnt -> 'mnt:[4026531840]' +lrwxrwxrwx 1 shuveb shuveb 0 Jun 13 11:44 net -> 'net:[4026531992]' +lrwxrwxrwx 1 shuveb shuveb 0 Jun 13 11:44 pid -> 'pid:[4026531836]' +lrwxrwxrwx 1 shuveb shuveb 0 Jun 13 11:44 pid_for_children -> 'pid:[4026531836]' +lrwxrwxrwx 1 shuveb shuveb 0 Jun 13 11:44 user -> 'user:[4026531837]' +lrwxrwxrwx 1 shuveb shuveb 0 Jun 13 11:44 uts -> 'uts:[4026531838]' +``` + +## 没有容器的命名空间 + +从 lsns 的输出中我们看到容器并不是唯一使用命名空间的对象。为此,我们来创建一个具有自己的 PID 命名空间的 shell 实例。我们将使用 unshare 来做到这一点。“unshare”这个名字很明显。还有一个 [同名的 Linux 系统调用](https://man7.org/linux/man-pages/man2/unshare.2.html),用来取消共享默认命名空间,使调用进程加入一个新创建的命名空间。 + +```bash +➜ ~ sudo unshare --fork --pid --mount-proc /bin/bash +[root@kodai shuveb]# ps aux +USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND +root 1 0.5 0.0 8296 4944 pts/1 S 08:59 0:00 /bin/bash +root 2 0.0 0.0 8816 3336 pts/1 R+ 08:59 0:00 ps aux +[root@kodai shuveb]# +``` + +上面的命令中 unshare 创建一个新进程,调用 unshare() 系统调用来创建一个新的 PID 命名空间,然后在其中执行 /bin/bash。我们还告诉 unshare 在新进程中挂载 proc 文件系统,这是 ps 获取信息的地方。从 ps 命令的输出中,确实可以看到该 shell 有一个新的 PID 命名空间,PID 为 1,并且由于 ps 是由具有新 PID 命名空间的 shell 程序启动的,因此它继承了命名空间并获得 PID 为 2。作为练习,你可以找出此容器中运行的 shell 进程在主机上具有哪些 PID。 + +## 命名空间的类型 + +了解 PID 命名空间后,让我们尝试理解其他命名空间以及它们的含义。[命名空间手册页](https://man7.org/linux/man-pages/man7/namespaces.7.html) 讨论了 8 种不同的命名空间。以下是带有简短说明的各种类型,以及指向相关手册页的链接: + +Namespace | Flag |Isolates +--|--|-- +[Cgroup](https://man7.org/linux/man-pages/man7/cgroup_namespaces.7.html) | CLONE_NEWCGROUP | Cgroup root directory +[IPC](https://man7.org/linux/man-pages/man7/ipc_namespaces.7.html) | CLONE_NEWIPC | System V IPC,POSIX message queues +[Network](https://man7.org/linux/man-pages/man7/network_namespaces.7.html) | CLONE_NEWNET | Network devices,stacks, ports, etc. +[Mount](https://man7.org/linux/man-pages/man7/mount_namespaces.7.html) | CLONE_NEWNS | Mount points +[PID](https://man7.org/linux/man-pages/man7/pid_namespaces.7.html) | CLONE_NEWPID | Process IDs +[Time](https://man7.org/linux/man-pages/man7/time_namespaces.7.html) | CLONE_NEWTIME | Boot and monotonic clocks +[User](https://man7.org/linux/man-pages/man7/user_namespaces.7.html) | CLONE_NEWUSER | User and group IDs +[UTS](https://man7.org/linux/man-pages/man7/uts_namespaces.7.html) | CLONE_NEWUTS | Hostname and NIS domain name + +你可以想象能够用这些命名空间为新的或现有的进程做什么。当它们在同一台计算机上运行时,你几乎可以将它们隔离开就像它们在单独的虚拟机中运行一样。你可以将多个进程隔离在各自的命名空间中,并在同一主机内核上运行。这比运行多个虚拟机要有效得多。 + +## 创建新的命名空间或加入现有的命名空间 + +默认情况下,当使用 fork() 创建进程时,子进程继承调用 fork() 的进程的命名空间。如果希望创建的新进程成为一组新命名空间的一部分该怎么办?如你所见,fork() 没有参数,并且不允许我们在创建子进程之前对其进行控制。但是可以通过 clone() 系统调用施加这种控制,从而可以非常精细地控制它创建的新进程。 + +### 关于 clone() 的附注 + +Linux 下虽然有不同的系统调用,例如 fork(),vfork() 和 clone() 来创建新进程,但是内核中的 fork() 和 vfork() 只是使用不同的参数调用 clone()。围绕此的内核源码(为了更好的说明,我进行了一些编辑)非常容易理解。在文件 [kernel/fork.c](https://elixir.bootlin.com/linux/v5.7.2/source/kernel/fork.c#L2521) 中可以看到以下内容: + +```c +SYSCALL_DEFINE0(fork) +{ + struct kernel_clone_args args = { + .exit_signal = SIGCHLD, + }; + + return _do_fork(&args); +} + +SYSCALL_DEFINE0(vfork) +{ + struct kernel_clone_args args = { + .flags = CLONE_VFORK | CLONE_VM, + .exit_signal = SIGCHLD, + }; + + return _do_fork(&args); +} + + +SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp, + int __user *, parent_tidptr, + int __user *, child_tidptr, + unsigned long, tls) +{ + struct kernel_clone_args args = { + .flags = (lower_32_bits(clone_flags) & ~CSIGNAL), + .pidfd = parent_tidptr, + .child_tid = child_tidptr, + .parent_tid = parent_tidptr, + .exit_signal = (lower_32_bits(clone_flags) & CSIGNAL), + .stack = newsp, + .tls = tls, + }; + + if (!legacy_clone_args_valid(&args)) + return -EINVAL; + + return _do_fork(&args); +} +``` + +如你所见,这三个系统调用只是使用不同的参数调用 _do_fork(),_do_fork() 实现创建新进程的逻辑。 + +### 使用 clone() 创建具有新命名空间的进程 + +Gocker 通过 Go 的 exec 包使用 clone() 系统调用。在处理与运行容器有关的内容的 [run.go](https://github.com/shuveb/containers-the-hard-way/blob/master/run.go) 中,可以看到以下内容: + +```go +cmd = exec.Command("/proc/self/exe", args...) +cmd.Stdin = os.Stdin +cmd.Stdout = os.Stdout +cmd.Stderr = os.Stderr +cmd.SysProcAttr = &syscall.SysProcAttr{ + Cloneflags: syscall.CLONE_NEWPID | + syscall.CLONE_NEWNS | + syscall.CLONE_NEWUTS | + syscall.CLONE_NEWIPC, +} +doOrDie(cmd.Run()) +``` + +在 syscall.SysProcAttr 中,我们可以传入 Cloneflags,然后将其传递给对 clone() 系统调用的调用。聪明的读者会注意到我们在这里没有设置单独的网络命名空间。在 Gocker 中我们设置了一个虚拟以太网接口,将其添加到新的网络命名空间,并让容器使用不同的 Linux 系统调用加入该命名空间。我们将在后面讨论这个问题。 + +### 使用 unshare() 创建和加入新的命名空间 + +如果要为现有进程创建新的命名空间,不必使用 clone() 创建新的子进程,Linux 提供了 [unshare()](https://man7.org/linux/man-pages/man2/unshare.2.html) 系统调用。 + +### 加入其他进程所属的命名空间 + +为了加入文件引用的命名空间或加入其他进程所属的命名空间,Linux 允许使用 [setns()](https://man7.org/linux/man-pages/man2/setns.2.html) 系统调用。 我们将很快看到,这非常有用。 + +## Gocker 是如何创建容器的 + +由于 Gocker 的主要目的是帮助理解 Linux 容器,因此保留了一些来自 Gocker 的日志消息。从这个意义上讲,它比运行 Docker 更为冗长。让我们看一下日志,以指导我们执行程序。然后我们可以进行深入分析,看看实际是如何运作的: + +```bash +➜ sudo ./gocker run alpine /bin/sh +2020/06/13 12:37:53 Cmd args: [./gocker run alpine /bin/sh] +2020/06/13 12:37:53 New container ID: 33c20f9ee600 +2020/06/13 12:37:53 Image already exists. Not downloading. +2020/06/13 12:37:53 Image to overlay mount: a24bb4013296 +2020/06/13 12:37:53 Cmd args: [/proc/self/exe setup-netns 33c20f9ee600] +2020/06/13 12:37:53 Cmd args: [/proc/self/exe setup-veth 33c20f9ee600] +2020/06/13 12:37:53 Cmd args: [/proc/self/exe child-mode --img=a24bb4013296 33c20f9ee600 /bin/sh] +/ # +``` + +这里我们要求 Gocker 从 Alpine Linux 镜像运行 shell。稍后我们将了解如何管理镜像。现在请注意以“ Cmd args:”开头的日志行。这行表示产生了一个新进程。第一行日志向我们显示运行 Gocker 命令后运行 shell 启动的过程。但是最后我们看到又启动了三个进程。最后一个带有第二个参数为“child-mode”的参数是在 Alpine Linux 镜像内部执行 shell 的程序 /bin/sh。在此之前,我们看到另外两个分别带有参数“setup-netns”和“setup-veth”的进程。这些进程设置了新的网络命名空间,并设置了虚拟以太网设备对的容器端,对使容器与外界通信。 + +由于各种原因,Go 语言不直接支持 fork() 系统调用。我们通过创建一个新进程来解决此限制,但是要在其中再次执行当前程序。 /proc/self/exe 指向当前正在运行的可执行文件的路径。我们根据传递不同的命令行参数来调用适当的函数(当 fork() 在子进程中返回时将调用该函数)。 + +### 源代码的组织 + +Gocker 源代码通过命令(如参数)组织在文件中。例如,主要服务于 gocker run 命令行参数的函数位于 run.go 文件中。类似地,gocker exec 主要需要的功能在 exec.go 文件中。这并不意味着这些文件是独立的。他们从其他文件中自由调用函数。还有一些文件可以实现常见功能,例如 cgroups.go 和 utils.go。 + +### 运行容器 + +在 [main.go](https://github.com/shuveb/containers-the-hard-way/blob/master/main.go) 中,你可以看到是否运行了 Gocker 命令,我们检查以确保 gocker0 桥接器已启动并正在运行。否则我们通过调用完成工作的 setupGockerBridge() 来启动它。最后,我们调用函数 initContainer(),该函数在 run.go 中实现。让我们仔细看看该函数: + +```go +func initContainer(mem int, swap int, pids int, cpus float64, + src string, args []string) { + containerID := createContainerID() + log.Printf("New container ID: %s\n", containerID) + imageShaHex := downloadImageIfRequired(src) + log.Printf("Image to overlay mount: %s\n", imageShaHex) + createContainerDirectories(containerID) + mountOverlayFileSystem(containerID, imageShaHex) + if err := setupVirtualEthOnHost(containerID); err != nil { + log.Fatalf("Unable to setup Veth0 on host: %v", err) + } + prepareAndExecuteContainer(mem, swap, pids, cpus, containerID, + imageShaHex, args) + log.Printf("Container done.\n") + unmountNetworkNamespace(containerID) + unmountContainerFs(containerID) + removeCGroups(containerID) + os.RemoveAll(getGockerContainersPath() + "/" + containerID) +} +``` + +首先,我们通过调用 createContainerID() 创建唯一的容器 ID。然后,我们调用 downloadImageIfRequired(),以便可以从 Docker Hub 下载容器镜像(如果本地尚不可用)。 Gocker 使用 /var/run/gocker/containers 中的子目录来挂载容器根文件系统。createContainerDirectories() 会解决这个问题。mountOverlayFileSystem() 知道如何处理多层 Docker 镜像,并在 `/var/run/gocker/containers//fs/mnt` 上为可用镜像安装合并的文件系统。尽管这看起来令人生畏,但如果阅读源代码并不难理解。覆盖文件系统允许创建一个堆叠的文件系统,其中较低的层(在这种情况下是 Docker 根文件系统)是只读的,而任何更改都将保存到“upperdir” 而无需更改较低层中的任何文件。这允许多容器共享一个 Docker 镜像。当我们在虚拟机上下文中说“镜像”时,它通常是指磁盘镜像。但是在这里,它只是一个目录或一组目录(奇特的名字:layers),带有构成 Docker“镜像”根文件系统的文件,这些文件可以使用 Overlay 文件系统挂载它,为新容器创建根文件系统。 + +接下来,我们创建一个虚拟的以太网配对设备,它非常类似于调用 setupVirtualEthOnHost() 的管道。它们采用名称 `veth0_` 和 `veth1_` 的形式。我们将一对中的 veth0 部分连接到主机上的网桥 gocker0。稍后我们将在容器内部使用该对的 veth1 部分。这对就像管道一样,是从具有自己的网络命令空间的容器内部进行网络通信的密钥。随后,我们将介绍如何在容器内设置 veth1 部件。 + +最后,调用 prepareAndExecuteContainer(),它实际上在容器中执行该过程。当此函数返回时,容器已完成执行。最后,我们然后进行一些清理并退出。让我们看看 prepareAndExecuteContainer() 的作用。它实际上创建了我们看到日志的 3 个进程,并使用参数 setup-netns,setup-veth 和 child-mode 运行相同的 gocker 二进制文件。 + +### 设置可在容器内工作的网络 + +设置新的网络命名空间非常容易,只需将 CLONE_NEWNET 包括在传递给 clone() 系统调用的标志位掩码中即可。棘手的是确保容器内部可以具有网络接口,通过该接口可以与外部进行通信。在 Gocker 中,我们创建的第一个新命名空间是网络命名空间。当使用 setup-ns 和 setup-veth 参数调用 gocker 时会发生这种情况。首先,我们设置一个新的网络命名空间。setns() 系统调用可以将调用进程的命名空间设置为文件描述符所引用的命名空间,该文件描述符指向 /proc//ns 中的文件,该文件列出了进程所属的所有命名空间。让我们看一下 setupNewNetworkNamespace() 函数,该函数是通过调用 setup-netns 参数调用 gocker 的结果而被调用的。 + +```go +func setupNewNetworkNamespace(containerID string) { + _ = createDirsIfDontExist([]string{getGockerNetNsPath()}) + nsMount := getGockerNetNsPath() + "/" + containerID + if _, err := syscall.Open(nsMount, + syscall.O_RDONLY|syscall.O_CREAT|syscall.O_EXCL, + 0644); err != nil { + log.Fatalf("Unable to open bind mount file: :%v\n", err) + } + + fd, err := syscall.Open("/proc/self/ns/net", syscall.O_RDONLY, 0) + defer syscall.Close(fd) + if err != nil { + log.Fatalf("Unable to open: %v\n", err) + } + + if err := syscall.Unshare(syscall.CLONE_NEWNET); err != nil { + log.Fatalf("Unshare system call failed: %v\n", err) + } + if err := syscall.Mount("/proc/self/ns/net", nsMount, + "bind", syscall.MS_BIND, ""); err != nil { + log.Fatalf("Mount system call failed: %v\n", err) + } + if err := unix.Setns(fd, syscall.CLONE_NEWNET); err != nil { + log.Fatalf("Setns system call failed: %v\n", err) + } +} +``` + +每当 Linux 内核中的最后一个进程终止时,它都会自动删除该命名空间。但是,有一种技术可以通过绑定安装来保留命名空间,即使其中没有任何进程。我们在 setupNewNetworkNamespace() 函数中使用此技术。我们首先打开进程的网络命名空间文件,该文件位于 /proc/self/ns/net 中。然后,我们使用 CLONE_NEWNET 参数调用 unshare() 系统调用。这将调用过程与其所属的命名空间解除关联,创建一个新的新网络命名空间,并将其设置为该进程的网络命名空间。然后,我们将此进程的网络命名空间专用文件的安装挂载绑定到已知文件名,即 `/var/run/gocker/net-ns/`。该文件可随时用于引用该网络命名空间。现在,我们可以退出此进程,但是由于此进程的新网络命名空间已绑定安装到新文件上,因此内核将保留此命名空间。 + +接下来,使用 setup-veth 参数调用 gocker。 这将调用函数 setupContainerNetworkInterfaceStep1() 和 setupContainerNetworkInterfaceStep2()。在第一个函数中,我们查找 `veth1_` 接口,并将其命名空间设置为在上一步中创建的新网络命名空间。现在,该接口将不再在主机上可见。 但问题是:由于它与 `veth0_` 接口配对,该接口在主机上仍然可见,因此,加入此网络命名空间的任何进程都可以与主机进行通信。 第二个功能将 IP 地址添加到网络接口,并将 gocker0 网桥设置为其默认网关设备。 + +现在,主机上有一个网络接口,而新的网络命名空间上有一个可以相互通信的接口。而且由于该网络命名空间可以由文件引用,因此我们可以随时使用 setns() 系统调用打开该文件并加入该网络命名空间。而且,这正是我们要做的。 + +此后,prepareAndExecuteContainer() 调用将设置一个新进程,该进程使用 child-mode 参数运行 gocker。 这是最后的进程,将产生我们要在容器中运行的命令。让我们看一下运行 child-mode 的进程的新命名空间。我们之前已经看过了这段代码: + +```go +cmd = exec.Command("/proc/self/exe", args...) +cmd.Stdin = os.Stdin +cmd.Stdout = os.Stdout +cmd.Stderr = os.Stderr +cmd.SysProcAttr = &syscall.SysProcAttr{ + Cloneflags: syscall.CLONE_NEWPID | + syscall.CLONE_NEWNS | + syscall.CLONE_NEWUTS | + syscall.CLONE_NEWIPC, +} +doOrDie(cmd.Run()) +``` + +在这里,我们设置新的 PID,mount,UTS 和 IPC 命名空间。请记住,我们有一个文件可以引用的新网络命名空间。我们只需要加入它就会很快完成。child-mode 进程将调用函数 execContainerCommand()。以下是代码: + +```go +func execContainerCommand(mem int, swap int, pids int, cpus float64, + containerID string, imageShaHex string, args []string) { + mntPath := getContainerFSHome(containerID) + "/mnt" + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + imgConfig := parseContainerConfig(imageShaHex) + doOrDieWithMsg(syscall.Sethostname([]byte(containerID)), "Unable to set hostname") + doOrDieWithMsg(joinContainerNetworkNamespace(containerID), "Unable to join container network namespace") + createCGroups(containerID, true) + configureCGroups(containerID, mem, swap, pids, cpus) + doOrDieWithMsg(copyNameserverConfig(containerID), "Unable to copy resolve.conf") + doOrDieWithMsg(syscall.Chroot(mntPath), "Unable to chroot") + doOrDieWithMsg(os.Chdir("/"), "Unable to change directory") + createDirsIfDontExist([]string{"/proc", "/sys"}) + doOrDieWithMsg(syscall.Mount("proc", "/proc", "proc", 0, ""), "Unable to mount proc") + doOrDieWithMsg(syscall.Mount("tmpfs", "/tmp", "tmpfs", 0, ""), "Unable to mount tmpfs") + doOrDieWithMsg(syscall.Mount("tmpfs", "/dev", "tmpfs", 0, ""), "Unable to mount tmpfs on /dev") + createDirsIfDontExist([]string{"/dev/pts"}) + doOrDieWithMsg(syscall.Mount("devpts", "/dev/pts", "devpts", 0, ""), "Unable to mount devpts") + doOrDieWithMsg(syscall.Mount("sysfs", "/sys", "sysfs", 0, ""), "Unable to mount sysfs") + setupLocalInterface() + cmd.Env = imgConfig.Config.Env + cmd.Run() + doOrDie(syscall.Unmount("/dev/pts", 0)) + doOrDie(syscall.Unmount("/dev", 0)) + doOrDie(syscall.Unmount("/sys", 0)) + doOrDie(syscall.Unmount("/proc", 0)) + doOrDie(syscall.Unmount("/tmp", 0)) +} +``` + +在这里,我们将容器的主机名设置为容器 ID,加入我们先前创建的新网络命名空间,创建允许我们控制 CPU,PID 和 RAM 使用情况的 Linux 控制组,加入这些 Cgroup,然后复制主机的 DNS 解析文件进入容器的文件系统,对已安装的 Overlay 文件系统执行 chroot(),安装所需的文件系统,使容器能够平稳运行,设置本地网络接口,根据容器镜像的建议设置环境变量并最终运行用户希望我们运行的命令。现在,此命令将在一组新的命名空间中运行,从而使它几乎完全与主机隔离。 + +## 限制容器资源 + +除了使用命名空间实现隔离之外,容器还有另一个重要特征:限制容器消耗的资源量的能力。Linux 下的 Cgroup 很简单,通过它我们能够做到这一点。虽然命名空间是通过诸如 unshare(),setns() 和 clone() 之类的系统调用实现的,但 Cgroup 是通过创建目录并将文件写入到虚拟文件系统(位于 /sys/fs/cgroup 下)来管理的。在 Cgroups 虚拟文件系统层次结构中,每个容器创建了 3 个目录: + +- `/sys/fs/cgroup/pids/gocker/` +- `/sys/fs/cgroup/cpu/gocker/` +- `/sys/fs/cgroup/mem/gocker/` + +对于每个创建的目录,内核会添加各种文件来自动配置 cgroup。 + +这是我们配置容器的方式: + +- 当容器启动时,我们创建 3 个目录,每个目录分别对应我们关心的三个 cgroup:CPU,PID 和 Memory。 +- 然后,我们通过写入该目录内的文件来设置 cgroup 的限制。例如,要设置容器中允许的最大 PID 数量,我们将该最大数量写入 `/sys/fs/cgroup/pids/gocker//pids.max`,这将配置此 Cgroup。 +- 现在,我们可以通过将其 PID 添加到 `/sys/fs/cgroup/pids/gocker//cgroup.procs` 中来添加需要由该 Cgroup 控制的进程。 + +这就是全部。一旦添加了要由 Cgroup 控制的进程,内核就会将所有进程后代的 PID 自动添加到适当的 Cgroup 的 cgroup.procs 文件中。由于我们在容器中启动一个添加到所有 3 个 Cgroup 的进程,并且该进程是容器启动其他进程的通常方式,因此它们也继承了所有限制。 + +### 限制 CPU + +让我们尝试将容器可以使用的 CPU 限制为主机系统 1 个 CPU 内核的 20%。让我们开始一个受此限制的容器,安装 Python 并运行一个 while 循环。我们通过向 gocker 传递 --cpu = 0.2 标志来实现: + +```bash +sudo ./gocker run --cpus=0.2 alpine /bin/sh +2020/06/13 18:14:09 Cmd args: [./gocker run --cpus=0.2 alpine /bin/sh] +2020/06/13 18:14:09 New container ID: d87d44b4d823 +2020/06/13 18:14:09 Image already exists. Not downloading. +2020/06/13 18:14:09 Image to overlay mount: a24bb4013296 +2020/06/13 18:14:09 Cmd args: [/proc/self/exe setup-netns d87d44b4d823] +2020/06/13 18:14:09 Cmd args: [/proc/self/exe setup-veth d87d44b4d823] +2020/06/13 18:14:09 Cmd args: [/proc/self/exe child-mode --cpus=0.2 --img=a24bb4013296 d87d44b4d823 /bin/sh] +/ # apk add python3 +fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86_64/APKINDEX.tar.gz +fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/community/x86_64/APKINDEX.tar.gz +(1/10) Installing libbz2 (1.0.8-r1) +(2/10) Installing expat (2.2.9-r1) +(3/10) Installing libffi (3.3-r2) +(4/10) Installing gdbm (1.13-r1) +(5/10) Installing xz-libs (5.2.5-r0) +(6/10) Installing ncurses-terminfo-base (6.2_p20200523-r0) +(7/10) Installing ncurses-libs (6.2_p20200523-r0) +(8/10) Installing readline (8.0.4-r0) +(9/10) Installing sqlite-libs (3.32.1-r0) +(10/10) Installing python3 (3.8.3-r0) +Executing busybox-1.31.1-r16.trigger +OK: 53 MiB in 24 packages +/ # python3 +Python 3.8.3 (default, May 15 2020, 01:53:50) +[GCC 9.3.0] on linux +Type "help", "copyright", "credits" or "license" for more information. +>>> while True: +... pass +... +``` + +我们在主机运行 top,查看在容器内部运行的 python 进程占用了多少 CPU。 + +![Cgroup 限制 CPU 为 20%](https://raw.githubusercontent.com/alandtsang/gctt-images2/master/20200619-Containers-the-hard-way-Gocker-A-mini-Docker-written-in-Go/Top_command_cgroups.png) +Cgroup 限制 CPU 为 20% + +从另一个终端,让我们使用 gocker exec 命令在同一容器内启动另一个 python 进程,并在其中运行 while 循环。 + +```bash +➜ sudo ./gocker ps +2020/06/13 18:21:10 Cmd args: [./gocker ps] +CONTAINER ID IMAGE COMMAND +d87d44b4d823 alpine:latest /usr/bin/python3.8 +➜ sudo ./gocker exec d87d44b4d823 /bin/sh +2020/06/13 18:21:24 Cmd args: [./gocker exec d87d44b4d823 /bin/sh] +/ # python3 +Python 3.8.3 (default, May 15 2020, 01:53:50) +[GCC 9.3.0] on linux +Type "help", "copyright", "credits" or "license" for more information. +>>> while True: +... pass +... +``` + +现在有 2 个 python 进程,如果不受 Cgroup 的限制,它们将消耗 2 个完整的 CPU 核数。现在,让我们看一下主机上 top 命令的输出: + +![Cgroup 限制 2 个进程的 CPU 为 20%](https://raw.githubusercontent.com/alandtsang/gctt-images2/master/20200619-Containers-the-hard-way-Gocker-A-mini-Docker-written-in-Go/Top_command_cgroups_2.png) +Cgroup 限制 2 个进程的 CPU 为 20% + +从主机 top 命令的输出中可以看到,这两个 python 进程都运行循环,每个进程的 CPU 限制为 10%。容器的 20% CPU 配额由调度程序公平分配给容器中的 2 个进程。请注意,也可以指定一个以上 CPU 核数的余量。例如,如果要允许一个容器最大使用 2 个半核,请在标志中将其指定为 --cpu = 2.5。 + +### 限制 PID + +在新的 PID 命名空间中运行 shell 程序的容器似乎消耗 7 个 PID。这意味着,如果启动的 PID 最高限制为 7,你将无法在 shell 上启动其他进程。让我们对此进行测试。[虽然容器中只有 2 个处于运行状态的进程,但我不确定为什么要消耗 7 个 PID。这需要进一步研究。] + +```bash +➜ sudo ./gocker run --pids=7 alpine /bin/sh +[sudo] password for shuveb: +2020/06/13 18:28:00 Cmd args: [./gocker run --pids=7 alpine /bin/sh] +2020/06/13 18:28:00 New container ID: 920a577165ef +2020/06/13 18:28:00 Image already exists. Not downloading. +2020/06/13 18:28:00 Image to overlay mount: a24bb4013296 +2020/06/13 18:28:00 Cmd args: [/proc/self/exe setup-netns 920a577165ef] +2020/06/13 18:28:00 Cmd args: [/proc/self/exe setup-veth 920a577165ef] +2020/06/13 18:28:00 Cmd args: [/proc/self/exe child-mode --pids=7 --img=a24bb4013296 920a577165ef /bin/sh] +/ # ls -l +/bin/sh: can't fork: Resource temporarily unavailable +/ # +``` + +### 限制 RAM + +让我们开启一个新容器,将最大允许内存设置为 128M。现在,我们将在其中安装 python,并在其中分配大量 RAM。 这将触发内核的内存不足(OOM)killer,来杀死我们的 python 进程。让我们看看实际情况: + +```bash +➜ sudo ./gocker run --mem=128 --swap=0 alpine /bin/sh +2020/06/13 18:30:30 Cmd args: [./gocker run --mem=128 --swap=0 alpine /bin/sh] +2020/06/13 18:30:30 New container ID: b22bbc6ee478 +2020/06/13 18:30:30 Image already exists. Not downloading. +2020/06/13 18:30:30 Image to overlay mount: a24bb4013296 +2020/06/13 18:30:30 Cmd args: [/proc/self/exe setup-netns b22bbc6ee478] +2020/06/13 18:30:30 Cmd args: [/proc/self/exe setup-veth b22bbc6ee478] +2020/06/13 18:30:30 Cmd args: [/proc/self/exe child-mode --mem=128 --swap=0 --img=a24bb4013296 b22bbc6ee478 /bin/sh] +/ # apk add python3 +fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86_64/APKINDEX.tar.gz +fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/community/x86_64/APKINDEX.tar.gz +(1/10) Installing libbz2 (1.0.8-r1) +(2/10) Installing expat (2.2.9-r1) +(3/10) Installing libffi (3.3-r2) +(4/10) Installing gdbm (1.13-r1) +(5/10) Installing xz-libs (5.2.5-r0) +(6/10) Installing ncurses-terminfo-base (6.2_p20200523-r0) +(7/10) Installing ncurses-libs (6.2_p20200523-r0) +(8/10) Installing readline (8.0.4-r0) +(9/10) Installing sqlite-libs (3.32.1-r0) +(10/10) Installing python3 (3.8.3-r0) +Executing busybox-1.31.1-r16.trigger +OK: 53 MiB in 24 packages +/ # python3 +Python 3.8.3 (default, May 15 2020, 01:53:50) +[GCC 9.3.0] on linux +Type "help", "copyright", "credits" or "license" for more information. +>>> a1 = bytearray(100 * 1024 * 1024) +Killed +/ # +``` + +需要注意的一件事是,我们使用 --swap = 0 将分配给该容器的 swap 设置为零。否则 Cgroup 虽然限制 RAM 使用,但它允许容器使用无限的交换空间。当 swap 设置为零时,容器将完全限制为允许的 RAM 总量。 + +## 关于作者 + +我是 Shuveb Hussain,是 Linux-focused 博客的作者。 你可以在 [Twitter](https://twitter.com/shuveb) 上关注我,在那里我发布与技术相关的内容,主要针对 Linux,性能,可扩展性和云技术。 + +--- + +via: https://unixism.net/2020/06/containers-the-hard-way-gocker-a-mini-docker-written-in-go/ + +作者:[unixism](https://unixism.net/about-unixism/) +译者:[alandtsang](https://github.com/alandtsang) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200630-Go-Safe-HTML.md b/published/tech/20200630-Go-Safe-HTML.md new file mode 100644 index 000000000..63a48cb24 --- /dev/null +++ b/published/tech/20200630-Go-Safe-HTML.md @@ -0,0 +1,149 @@ +首发于:https://studygolang.com/articles/35195 + +# Go 语言 HTML 安全编码 + +> 免责声明:这不是一个官方的谷歌帖子或公告,本文只是我理解的一些可以用的理论方法。 + +谷歌信息安全小组谷歌发布了 [Go 语言 “safehtml” 包](https://github.com/google/safehtml) 。如果你有兴趣使你的应用程序能够自适应服务器端 XSS,那么你可能希望采用它来替代“html/template”。(将你的应用程序中的 HTML 类库)迁移到 safehtml 会非常简单,因为它只是原始 html/template 标准包的一个强化分支。如果你的应用程序没有大的缺陷,那么将它转换为使用安全版本应该不会太复杂。 + +这是谷歌内部用来保护产品免受 XSS 攻击的方法。 + +如果你只想使用它而不想了解其中的原理,可以跳到[结论列表](#checklist)。 + +## "html/template" 的问题 + +“html/template” 没有 ”tainting“(污染)的概念,也没有跟踪像 [template.HTML](https://golang.org/pkg/html/template/#HTML) 这样危险的类型。HTML 被构造后,只有一行文档说明: + +> 使用这种类型会带来安全风险:封装的内容应该来自受信任的源,因为它将被逐字逐句包含在模板的输出中。 + +这不仅缺乏对“为什么”和“如何”使用该类型的解释,而且它的使用也非常普遍,这使得它与文档中一同被提及的所有其他类型(总共七个)一起成为一个非常危险的隐患。 + +此外,“html/template”还有一些长期存在的问题,在不破坏向后兼容性的情况下,没有一种很好的方法来正确地修复这些问题。潜在的破坏兼容可能性和安全性之间的权衡还不清楚,因此,如果你想 **选择** 更安全的方式,你可能应该放弃使用 “html/template”。 + +请注意,我正在维护“html/template”(我在 [这个项目](https://dev.golang.org/owners) 的昵称是 empijei ),所以我告诉你一些被那个包坑过的背景。如果我能很神奇的将所有用户迁移到安全版本,我肯定会。 + +## 结构 + +safehtml 由几个包组成。根据你的构建系统或公司的工具栈,你应该约束一些限制条件。你公司的安全团队或安全意识强的人应该为每个人灌输这个观点。 + +### safehtml + +这是基础包,它只是提供结构安全的类型。简而言之,它是如何工作的: + +- 这里有三种方式构建 [HTML type](https://godoc.org/github.com/google/safehtml#HTML) + +- [ ] [通过转义不受信任的字符串](https://godoc.org/github.com/google/safehtml#HTMLEscaped) ,使其安全使用。 +- [ ] [通过提供一个安全模板](https://godoc.org/github.com/google/safehtml/template#Template.ExecuteToHTML) ,它使用上下文自动转义,所以它是安全的(你可以在 [我之前的文章](https://blogtitle.github.io/robn-go-security-pearls-cross-site-scripting-xss/) 中阅读更多关于这部分内容) +- [ ] [通过连接已经被信任的 HTML](https://godoc.org/github.com/google/safehtml#HTMLConcat) ,这基本上是安全的,实际上只有心怀不轨的开发者才能制造出错误。 + +这保证了 HTML 类型的每个实例都是安全的。[脚本类型](https://godoc.org/github.com/google/safehtml#Script) 的行为类似,但它不是模板,而是只能从常量或数据构建。为了表达“编译时常量”的概念,它有[接受不可被包外访问字符串类型的构造函数](https://godoc.org/github.com/google/safehtml#ScriptFromConstant),因此调用它们的唯一方法是使用 [字符串文本](https://golang.org/ref/spec#String_literals) (我发现这是一个非常巧妙的技巧)。 + +此包中的所有其他类型都遵循类似的模式。 + +### safehtml/template + +这是真正的“html/template”替代品,也是每个人都应该使用的包。如果不使用“legacyconversions”和“uncheckedconversions”(请参阅下文),并且**你的所有 HTML 响应都是由这个包生成的**,那么你可以保证在你的程序(products)中不会有任何服务器端 XSS。 + +我们正在研究确保最后一个条件为真的工具,但这需要一些时间。请 [继续关注](https://blogtitle.github.io/index.xml) 最新消息。 + +### safehtml/legacyconversions + +此包只能用于转换到安全 API 。它允许任何任意字符串都是安全类型,这样转换到 safehtml 可以非常迅速,所有新代码都将是安全的。 +++ 一旦迁移发生,就应该阻止使用这个包。++ +顾名思义:这只是针对遗留代码,不应该有新代码使用它,**并且应该逐步重构此包的所有用法,以使用安全构造函数**。 + +### safehtml/testconversions + +此软件包只能在测试目标中使用,并且仅在必要时使用。你应该设置一些 [linters](https://godoc.org/golang.org/x/tools/go/analysis) 来确保这一点。 + +### safehtml/uncheckedconversions + +这是最微妙的问题。有时 safehtmlapi 太不方便,甚至无法使用。有时你不得不使用不安全的模式,因为你想做一些不能被证明是安全的事情(例如,从数据库中获取一些你信任的 HTML 并将其提供给客户端)。 + +对于这些**非常罕见**的情况,您可以使用此软件包。导入它应该限制在一组手工挑选的依赖项,并且每一个新的导入都需要一些安全意识强的人来审查它。 + +确保使用是**安全的,并将保持安全**,因为 uncheckedconversions 不会增加安全性。它们只是通知编译器您已经检查了代码,并希望它是可信的。遵循以下准则: + +- 仅在严格必要的情况下使用(例如,如果使用 safehtml/template 需要更多的工作,但需要做额外的工作)。 +- 为将来的审查者和维护者提供安全使用方法的文档。 +- 通过减少 uncheckedconversion 对闭包参数、结构不确定类型字段的依赖性,缩小上下文范围。 + +这个包的用法是您的单点故障,所以请确保您遵循这些。(这句话假设您最终将摆脱 legacyconversions )。 + +正确使用这个包的一个例子是(HTML)清理器的输出。(原文 sanitizer 这里译为清理器)如果您需要将用户提供的一些 HTML 嵌入到响应中(例如,因为您呈现 Markdown 或网页邮箱),您将清理该 HTML 。一旦它被清理(如果你的清理器程序被正确实现),就可以使用未经检查的转换(unchecked conversion)将其升级为 HTML 类型。 + +### safehtml/raw + +应阻止导入此包。“safehtml/”目录树之外的任何内容都不可见此包。 + +### safehtml/safehtmlutil + +是的,我知道,名字不好。考虑一下,这个包和前一个一样,也不应该在 safehtml 之外导入,它只是为了减少代码重复和避免循环依赖而创建的。我同意可以用不同的名称或结构来命名它,但是既然你永远不会和这个包交互,就不必太麻烦你了。 + +## 如何进行重构 + +### `Printf` 和嵌套模板 + +您可能拥有的一个代码示例是 + +``` go +var theLink template.HTML = fmt.Sprintf("the link", myLink) +myTemplate.Execute(httpResponseWriter, theLink) +``` + +要重构它,您有多种选择:要么用另一个模板构建字符串(注意这里的“template”变量是“safehtml/template”类型)。 + +``` go +myLinkTpl := template.Must(template.New("myUrl").Parse("the link")) +theLink, err := myLinkTpl.ExecuteToHtml(myLink) +// handle err +myTemplate.Execute(httpResponseWriter, theLink) +``` + +或者,对于更复杂的情况,可以使用嵌套模板: + +``` go +const outer = `

This is a title

{{ template "inner" .URL }}` +const inner = `the link` +t := template.Must(template.New("outer").Parse(outer)) +t = template.Must(t.New("inner").Parse(inner)) +t.ExecuteTemplate(os.Stdout, "outer", map[string]string{"URL": myLink}) +``` + +## 常量 + +如果代码中有一个 HTML ` 常量 `,那么可以将其用作模板并将其执行为 HTML。这将检查所有标签是否配对以及其他内容,并返回一个 HTML 类型的实例。 + +如下: + +``` go +var myHtml template.HTML := `

This is a title

` +``` + + 结论列表 + +1. 组织访问这些包 +- 防止“safehtml”目录外的包导入“raw”、“uncheckedconversions”和“safehtmlutil”。 +- 只允许测试构建时导入“testconversions”包。 +2. 从“html/template”迁移并替换为“safehtml/template”。 +- 对于每一个破损或每一个问题,使用“legacyconversions”调用。可能需要一些手动重构,但迁移应该相当简单。 +- **运行所有的集成和 E2E 测试**。这很重要,所以我用 SHIFT 而不是 CAPS 来输入。 +- 封锁 legacyconversions 列表:从现在起,禁止新导入“legacyconversions”包。 +- 禁止使用“html/template”,这样所有新代码都是安全的。 +3. 重构 legacyconversions 以使用安全模式。 +- 尽可能以安全的方式构造 HTML 并删除 legacy conversions。 +- 如果不可能使用 unchecked conversions。“uncheckedconversions”包的每一个新导入都应该被审查。 + +## 结论 + +如果您想确保 Go 代码中没有服务器端 XSS,这可能是最好的方法。如果您有任何问题或需要更多的重构示例,请让我知道,您可以[通过 twitter](https://twitter.com/empijei)(直接消息是开放的)或通过[电子邮件](mailto:empijei@gmail.com)与我联系。 + +--- + +via: https://blogtitle.github.io/go-safe-html/ + +作者:[Rob](https://blogtitle.github.io/authors/rob/) +译者:[lts8989](https://github.com/lts8989) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200701-Go-Object-File-Relocations.md b/published/tech/20200701-Go-Object-File-Relocations.md new file mode 100644 index 000000000..5062d3d56 --- /dev/null +++ b/published/tech/20200701-Go-Object-File-Relocations.md @@ -0,0 +1,149 @@ +首发于:https://studygolang.com/articles/35196 + +# Go:对象文件&重定位 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200701-Go:Object-File%26Relocations/1_HxAju6n33e9Y8AJwMuQL3w.png) + +**本文章基于 Go 1.14** + +重定位是链接过程中的一个阶段,重定位是链接过程中为每个外部符号分配适当地址。由于每个包都是单独编译的,因此它们不知道来自其它包的函数或者变量在哪里。 让我们从一个需要重定位的简单示例开始。 + +## 编译 + +以下程序涉及两个不同的程序包:main 和 fmt。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200701-Go:Object-File%26Relocations/1_4_DaAwHmqJbhwP8Tn10Dzg.png) + +构建此程序将首先涉及编译器,该编译器分别编译每个包。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200701-Go:Object-File%26Relocations/1_4HLpept1qBXFJvL_r4qptQ.png) + +通过命令 + +```bash +go tool compile -S -l main.go +``` + +我们可以查看指令在中间文件(译者注:目标文件)中的临时地址。 + +一旦我们的程序被编译,我们可以使用 + +```bash +go tool compile -S -l main.go +``` + +来查看程序对应的汇编代码 + +要查看编译器生成的指令,你有多种不同的方法: + +* 重新编译,并打印汇编指令。命令是: + +```bash +go tool compile -S -l main.go + +"".main STEXT size=137 args=0x0 locals=0x58 + 0x0000 00000 (main.go:7) TEXT "".main(SB) + [...] + 0x0058 00088 (main.go:8) CALL fmt.Println(SB) +``` + +参数 `-l` 用于避免内联,使得汇编代码更容易被阅读。 + +生成的汇编文件表明调用 `Println` 的指令相对 `main` 函数入口偏移 88 个字节。这个偏移对于链接器重新定位函数调用将会非常有用。 + +* 使用以下命令,反汇编已经生成的 main.o: + +```bash +go tool objdump main.o + +TEXT %22%22.main(SB) + [...] + main.go:8 0x57e e800000000 CALL 0x583 [1:5]R_CALL:fmt.Println +``` + +标识符 `R_CALL` 代表重定位调用 + +然而由于该函数属于另一个包,因此编译器不知道该函数实际位于何处。使用命令: + +```bash +go tool nm main.o +``` + +可以检查生成的文件 `main.o`,并列出其中包含的符号。下图是输出 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200701-Go:Object-File%26Relocations/1__cz0Ozr4acR3Sj0GbirP2Q.png) + +我们可以注意到,它需要使用 go 工具 nm 命令而不是本机 nm 命令。 实际上,Go 生成的目标文件(.o)具有自定义格式。 + +符号 U 代表未定义,表示编译器不知道该符号在哪里。该符号必须重定位,即找到 `Println` 的地址,才能成功的进行调用。这就是链接器需要参与的工作。在介绍链接器的工作之前,我们分析了目标文件 `main.o`, 以及它能够提供所有可用数据。链接器可以基于这些数据开展工作。 + +## 目标文件 + +这篇[文档](https://golang.org/pkg/cmd/internal/objabi/)解释了目标文件的内容和格式 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200701-Go:Object-File%26Relocations/1_WwlsAnj0J9-dUkvBYWS5sQ.png) + +该文件由依赖项,调试信息(DWARF), 索引符号列表,数据段以及符号列表。符号列表中包含每个符号都需要我们进行重定向。以下是它的格式: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200701-Go:Object-File%26Relocations/1_so340hPaauZOPChu3tvSCA.png) + +每个符号均以十六进制字节 fe 开头。可以使用十六进制编辑器打开目标文件 main.o 时。例如,对于 Mac,可以使用 xxd(译者注:xxd 是 mac 下的一个命令)。 下面是内容的一部分,对符号(译者注:实际是对符号开头的标志"fe")进行了高亮显示。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200701-Go:Object-File%26Relocations/1_PL_o1t7dokehoO3X6rbUaw.png) + +符号 `main.main` 是符号列表中的第一个符号。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200701-Go:Object-File%26Relocations/1_KOng-8Ed1XkprfvxSsqaXg.png) + +前几个字节 `0102 00dc 0100 dc01 0a` 代表了前面定义的一系列属性:type、flag、size、data、以及重定位的次数。 + +字节以 `zigzag-varint` 格式存储。`varint` 是以可变长字节的方式存储整数的值。 `zigzag` 通过对最少有效位进行编码来是以减少编码后数据的大小 + +然后,重定位 `Println` 是一组字节序列 `b201 0810 0008`: + +* `b201` 是偏移值 89 编码后的结果。这个偏移值是一个 `int32` 类型。感谢 `varint`,存储它仅耗费了 2 个字节. +* `08` 是需要重写的字节的数量,编码后的值是 4 +* `10` 是重定位的类型,编码值 8 表示 `R_CALL`, 即重定位函数调用 +* `08` 是对索引符号的引用 + +装载器现在已经拥有了重定位所需的所有必要信息,可以生成可执行的二进制文件了。 + +## 重定位 + +链接器的其中一个阶段是分配虚地址给所有的段和指令。可以使用命令 + +```bash +objdump -h my-binary +``` + +可视化每个段的地址。下面是前面示例的输出 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200701-Go:Object-File%26Relocations/1_JGMu2mnGI-HTp35GHqx3mg.png) + +函数 `main` 位于__text 段,它也能通过命令 + +```bash +objdump -d my-binary +``` + +找到,这个命令显示了指令的地址。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200701-Go:Object-File%26Relocations/1_tZX5Ills5d4Dnk0Z5iZ1pA.png) + +函数 `main` 入口地址是 `109cfa0`,函数 `fmt.Println` 的入口地址是 `1096a00`。一旦虚地址被分配,就会非常容易的重定位 `fmt.Println` 的入口地址。链接器将会用 `fmt.Println` 的入口地址依次减去 `main` 的入口地址、指令的偏移值、指令所占的字节大小。这样我们就能得到调用 `fmt.Println` 的全局偏移。对于前面的例子中,我们可以进行如下的操作: + +```bash +1096a00 (fmt.Println) — 109cfa0 (main) — 84 (offset inside the main function) — 4 (size) = -26109 +``` + +现在,指令知道函数 `fmt.Println` 的入口地址与当前内存地址的偏移是 `-26109`,调用可以成功执行。 + +--- + +via: https://medium.com/a-journey-with-go/go-object-file-relocations-804438ec379b + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[vearne](https://github.com/vearne) +校对:[unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200701-When-microservices-in-Go-are-not-enough-introduction-to-DDD-Lite.md b/published/tech/20200701-When-microservices-in-Go-are-not-enough-introduction-to-DDD-Lite.md new file mode 100644 index 000000000..2a43ec1c7 --- /dev/null +++ b/published/tech/20200701-When-microservices-in-Go-are-not-enough-introduction-to-DDD-Lite.md @@ -0,0 +1,423 @@ +首发于:https://studygolang.com/articles/35197 + +# 当在 Go 中使用微服务还不够时:介绍 DDD Lite + +当我开始用 Go 工作时,社区并不看好类似 DDD(Domain-Driven Design 领域驱动设计)和清晰架构这样的技术。我很多次听到这样的声音:*“不要在 Golang 中用 Java!”,“我已经在 Java 中见过了,请别这样做!”*。 + +这些时候,我已经有了近 10 年的 PHP 和 Python 经验。我已经见过太多糟糕的事情了。我记得所有那些“八千行”(有着 8 千行以上代码的方法 😉)和没有人愿意维护的应用。我查看了这些丑陋怪物以前的 Git 历史,他们最初看起来是无害的。但是随着时间的推移,微小,无辜的问题开始变得越发明显且越发严重。**我也同样见过 DDD 和清晰架构如何解决了这些问题。** + +也许 Golang 是不一样的?也许用 Golang 写微服务可以修复这些问题? + +## 原本应当是美好的 + +现在,与许多人交流了经验,且能够看到很多代码库之后,我的观点较三年前更清晰了一些。很不幸,我现在并不认为仅靠着 Golang 和微服务就可以解决我之前面对过的那些问题。我开始回顾过去困难的那些时候。 + +由于是相对初期的代码库,这些问题不太明显。由于 Golang 设计,这些问题还是不太明显。但是我确定随着时间推移,我们会有越来越多的,没人愿意维护的老的 Golang 应用。 + +幸亏,3 年前我并没有因别人的冷嘲热讽而放弃。我觉定在 Go 中尝试应用我原来在工作中用到的 DDD 以及相关技术。我与 Milosz 一起带领团队 3 年,并且都成功使用了 DDD,清晰架构,和所有相关的,但在 Golang 中还不受欢迎的技术。**这些技术使我们能够以恒定的速度开发我们的应用和产品,而不管代码的年头有多久。** + +(DDD 以及相关技术)从一开始就效果显著,其他技术的 1:1 移动模式将不会起作用。必不可少的,我们不会抛弃惯用的 Go 代码和微服务架构——它们完美契合! + +今天我会先分享最简单直接的技术—— DDD lite。 + +## Golang 中 DDD 的状态 + +坐下来写这篇文章之前,我在 Google 上查看了关于 Go 语言中 DDD 的几个文章。不客气地说,这些文章都没抓住应用 DDD 最核心的东西。**如果我读这些文章的时候根本不了解 DDD 的话,我想我不会在自己的团队应用这些技术。这种肤浅的方法也许也是 DDD 没有在 Go 社区推广的一个原因。** + +在这个系列中,我们会展示所有必要的技术,同时以使用的方式来实现。在描述任何一个模式之前,我们以一个问题开始:它能给我们带来什么?这是一个挑战我们当前思想的一个不错的方式。 + +我确定我们可以通过这系列文章改变 Go 社区对这些技术的接受度。我们相信这些技术是实现复杂业务项目的最好方法。**我相信我们会在确立 Go 的地位上能有所贡献,使其成为构建基础设施以及业务软件的出色的语言。** + +## 你需要慢下来,才能走得更快 + +以最简单的方式实现项目或许是诱人的,甚至当你感受到来自“上层”的压力时会更加诱人。我们用了微服务吗?如果需要的话,我们会仅仅重写该服务吗?我听到这种事情好多次了,绝大部分最后都不如人意。😉**老实说最简单方式实现会短期节省一些时间。但仅仅是短期。** + +考虑下任何形式的测试用例的例子。你可以在项目开始时省略写测试用例。你明显会节省一些时间,并且管理者也会满意。**计算方式似乎很简单——项目更快交付了。** + +但是长远来看,这种走捷径的方式是不值得的。随着工程的成长,团队会开始惧怕对其做任何的修改。最终,(开发)消耗的总时间会高于在一开始实现了测试用例所需要的时间。**开始为了快速的性能提升而牺牲质量的做法在长远上会拖慢脚步。** 另一方面——如果项目不是很关键,且需要快速创建,可以省略测试用例。这应当是一个务实的决定,而不仅仅是所谓 *“我们了解的更多,我们不会有 bug”。* + +DDD 的情形也类似。当你想要使用 DDD,你需要在开始的时候需要一点更多的时间,但是长线来说会节省更多。然而,并不是所有的项目都足够复杂到需要使用类似 DDD 这样的高级技术。 + +**没有质量与开发速度的权衡,如果想要长期快速迭代,就需要保持高质量。** + +![高质量的软件值得吗?](https://github.com/studygolang/gctt-images2/blob/master/20200701-When-microservices-in-Go-are-not-enough-introduction-to-DDD-Lite/quality-fowler.png?raw=true) + +## 听起来很棒,但是是否有证据证明它可行? + +如果你在两年前问我这个问题,我会说: *“好吧,我觉得这样效果更好!”*。但是仅仅相信我的话似乎还不够。😉有太多的教程展示了愚蠢的想法和主张,而它们可没有什么证据——不要盲目相信他们。 + +要记住:如果某人有成千上万的 Twitter 关注这,[仅凭这个可不是相信他们的理由](https://en.wikipedia.org/wiki/Authority_bias)! + +幸运的是,2 年前,[《Accelerate: The Science of Lean Software and DevOps: Building and Scaling High Performing Technology Organizations》](https://www.amazon.com/Accelerate-Software-Performing-Technology-Organizations/dp/1942788339)发布了。简单来说,这本书描述了影响开发团队表现的因素。但是这本书可不是靠着一堆未经证实的想法而出名的—— **而是基于科学探究。** + +**我最感兴趣的是展示到底是什么塑造了高水平团队的那部分内容**。这本书展示了几个明显的因素,比如引进了 DevOps,CI/CD,以及松耦合架构,这些都是高水平团队必不可少的因素。 + +> 如果像 DevOps 和 CI/CD 对你来说并不明显,可以先看看这些书:[《The Phoenix Project》](https://www.amazon.com/Phoenix-Project-DevOps-Helping-Business/dp/0988262592) 和 [《The DevOps Handbook》](https://www.amazon.com/DevOps-Handbook-World-Class-Reliability-Organizations/dp/1942788002)。 + +那么 《Accelerate》所告诉我们的塑造高水平团队的因素有什么? + +> 我们发现,只要系统与构建以及维护它们的团队是松耦合的,所有类型的系统是有可能表现出高性能的。 +> +> 这一关键架构属性使得团队即使在组织以及运作的系统数量不断增长也可以很容易地测试和部署单个组件或服务。它使组织可以在扩展规模时提高生产力。 + +所以,我们使用微服务的话,就可以了吗?如果使用微服务就足够的话,我就不用写这篇文章了。 😉 + +> - 对系统设计进行大规模修改,而不依赖其他团队修改它们的系统或者不造成其他团队的大量工作。 +> - 无需与外部团队沟通协调就可以完整自己的工作。 +> - 按需求部署或发布服务,不需要关系依赖的其他服务。 +> - 不需要集成测试环境即可按需求进行大多数测试,并且可以在正常时间部署,停机时间可以忽略不计。 +> +> 不幸的是,在现实生活中,许多所谓的面向服务架构不允许服务间独立测试和部署。因此无法使团队获得更高的表现。 +> +> [...] 如果忽视了这些特点,即使采用了最新的微服务架构,在容器上进行部署,也无法获得更高的表现。[...] 为了获得这些特点,设计系统要松耦合——这可以让服务间独立更改和验证。 + +**仅仅使用微服务架构将服务拆分为更小单位是不够的。如果以错误的方式实现,这么做会增加额外的复杂度并行拖慢团队的节奏**。DDD 可以帮助我们。 + +我提到 DDD 这一术语好几次了。DDD 实际上是什么? + +## 什么是 DDD (领域驱动设计) + +先看下 Wikipedia 的定义: +> 领域驱动设计(DDD)是一个概念,代码的结构和语言(类名称,类方法,类变量)应当与业务领域相匹配。比如,如果是处理贷款申请的软件,该软件可能会有诸如 LoanApplication 和 Customer 的类,以及像 AcceptOffer 和 Withdraw 之类的方法。 + +![](https://github.com/studygolang/gctt-images2/blob/master/20200701-When-microservices-in-Go-are-not-enough-introduction-to-DDD-Lite/no-god-please-no.jpg?raw=true) + +好吧,这不是一个完美的解释。 😅 它仍然缺少了一些重要的要点。 + +值得一提的是,DDD 在 2003 年提出。算是很早了。它一些留下来的精华也许能对 DDD 在 2020 年的今天以及 Go 的环境下的应用有所帮助。 + +> 如果你对 DDD 诞生的历史背景有兴趣,可以看看[解决软件核心中的复杂性](https://youtu.be/dnUFEg68ESM?t=1109),演讲者是 DDD 的创造者—— Eric Evans + +![Eric Evans DDD 的创造者,请把这个图片答应下来挂在床头,来获得 +10 的 DDD 保佑](https://github.com/studygolang/gctt-images2/blob/master/20200701-When-microservices-in-Go-are-not-enough-introduction-to-DDD-Lite/eric-evans.jpg?raw=true) + +我对 DDD 的简单定义是:保证以**最佳方式**解决**有效问题**。之后**以你的业务逻辑会被理解的方式实施解决方案,而不需要技术语言的额外翻译。** + +如何实施呢? + +**编码就好比打仗,取胜需要策略!** + +我喜欢说 *“5 天的编码能节约 15 分钟的计划时间”。* + +在开始编码之前,需要确定要解决的是一个有效的问题。听起来似乎是废话,但是以我的经验看,这并不像听起来那么容易。通常的情况是由工程师所创建的解决方案并没有实际解决业务需要解决的问题。在这一领域对我们有帮助的一系列模式被命名为**策略 DDD 模式(Strategic DDD patterns)。** + +根据我的经验,DDD 策略模式(DDD Strategic Patterns)经常被忽略。原因很简单:我们都是开发,我们更喜欢写代码而不是与 *“业务人员(business people)”* 交谈。😉 不幸的是,自我封闭,不与业务人员交流的方法有很多缺点。对业务缺少信任,对系统如何运作缺少认知(业务侧和研发侧都有这个问题),解决错误的问题——这些仅仅是最常见的问题中的一部分。 + +好消息是大多数情况是由于缺少类似事件风暴(Event Storming)这样合适的技术导致的。这些技术可以为双方都带来好处。令人惊讶的是交流业务逻辑可能是工作中最有意思的一部分。 + +除此之外,我们会从适用于代码的模式开始。这些模式会带给我们**一些** DDD 的好处。它们也会更快的对你产生用处。**没有策略模式,我得说这样你仅仅会拥有 DDD 可以带来的优势中的 30%,在下一个文章中,我们会回到策略模式上。** + +## Go 中的 DDD Lite + +在相当长的介绍之后,终于到了接触一些代码的时候了!在这篇文章中,我们会涵盖 **Go 中的战术领域驱动设计模式(Tactical Domain-Driven Design patterns in Go)** 的一些基本知识。请记住这仅仅是开始。会有更多文章来涵盖整个主题。 + +战术 DDD 中最关键的部分之一是试图直接在代码中反映领域逻辑。 + +但是这依然是一些非特定的定义——并且现在并不需要。我也不想从描述什么是 *值对象,实体,集合* 开始。从实际例子开始会更好。 + +### Wild workouts + +> **这不是另一篇带着随机代码片段的文章。** +> +> 这篇博客是一个更大的系列文章的一部分,而在这些文章中,我们会展示如何构建**长期上易于开发,维护,且能够愉快工作的 Go 应用程序。** 我们通过分享已被证明的技术来做到这一点,这些技术基于我们与团队所做的许多实验,以及基于[科学研究](https://threedots.tech/post/ddd-lite-in-go-introduction/?utm_source=about-wild-workouts#thats-great-but-do-you-have-any-evidence-if-that-is-working)。 +> +> 你可以通过与我们一起构建[功能完整](https://threedots.tech/post/serverless-cloud-run-firebase-modern-go-application/?utm_source=about-wild-workouts#what-wild-workouts-can-do)的实例 Go Web 应用——**Wild Workouts** 来学习这些模式。 +> +> 我们用完全不同的方式做着同一件事—— **我们在最初的 Wild Workouts 实现中引入了一些微小的问题。** 我们是失去了理智了吗?不是的。😉 这些问题是很多 Go 项目的共性问题。长远看,这些小小的问题会变得棘手,进而让新功能的添加变得困难无比。 +> +> 对高级或首席开发人员来说,最重要的技能一直就是就是,需要始终关注长期影响。 +> +> 我们会重构“太现代的” Wild Workouts 来修复这些问题。通过这种方式,你会轻松理解我们分享的技术。 +> +> 你了解那种读了一些技术文章之后,试着实现的时候却因为几个指南中略过的问题而被卡住的感觉吗?省略这些细节无疑会让文章更简短,并且提升浏览量,但这不是我们的目标。我们的目标是创建提供了足够的技术诀窍以应用所介绍技术的文章。如果你还没有读过[这个系列之前的文章](https://threedots.tech/tags/building-business-applications/),我们强烈建议读一下。 +> +> 我们相信在某些方面没有捷径可走。如果想用快速且高效的方式构建复杂的应用,你仅仅需要花些时间学习这些技术。如果问题很简单的话,就不会有这么让人头疼的遗留代码了。 +> +> 这里是目前为止发布的 [8 篇文章的完整列表](https://threedots.tech/tags/building-business-applications/?utm_source=about-wild-workouts)。 +> +> Wild Workouts 的**全部源码**可以从 [GitHub](https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example?utm_source=about-wild-workouts) 上获得。 + +我还没有提到,我们特别为了这些文章,创建了一个叫做 Wild Workouts 的整个应用。有趣的是,我们在这个应用中引入了一些微妙的问题,以进行重构。如果 Wild Workouts 看起来像是你原来接触过的应用——最好多在我们这里驻足一会儿😉。 + +### 重构 `trainer` 服务 + +我们开始重构的第一个(微)服务是 `trainer`。我们现在先不动其他服务——以后会回过头处理他们。 + +这个服务的职责是维护教练的时间表,并保证我们每一个小时之内只安排一个培训。该服务统一维护有效时间(教练的时间表)的信息。 + +最初的实现不是最好的。即使没有大量的逻辑,代码的一些部分也开始变得混乱了。基于我的经验,我感觉这些代码随着时间的推移会变得更糟。 😉 + +```go +func (g GrpcServer) UpdateHour(ctx context.Context, req *trainer.UpdateHourRequest) (*trainer.EmptyResponse, error) { + trainingTime, err := grpcTimestampToTime(req.Time) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "unable to parse time") + } + + date, err := g.db.DateModel(ctx, trainingTime) + if err != nil { + return nil, status.Error(codes.Internal, fmt.Sprintf("unable to get data model: %s", err)) + } + + hour, found := date.FindHourInDate(trainingTime) + if !found { + return nil, status.Error(codes.NotFound, fmt.Sprintf("%s hour not found in schedule", trainingTime)) + } + + if req.HasTrainingScheduled && !hour.Available { + return nil, status.Error(codes.FailedPrecondition, "hour is not available for training") + } + + if req.Available && req.HasTrainingScheduled { + return nil, status.Error(codes.FailedPrecondition, "cannot set hour as available when it have training scheduled") + } + if !req.Available && !req.HasTrainingScheduled { + return nil, status.Error(codes.FailedPrecondition, "cannot set hour as unavailable when it have no training scheduled") + } + hour.Available = req.Available + + if hour.HasTrainingScheduled && hour.HasTrainingScheduled == req.HasTrainingScheduled { + return nil, status.Error(codes.FailedPrecondition, fmt.Sprintf("hour HasTrainingScheduled is already %t", hour.HasTrainingScheduled)) + } + + hour.HasTrainingScheduled = req.HasTrainingScheduled +``` + +尽管这还不是最糟糕的代码,它也让我想起了我检查代码的 Git 历史记录时所看到的。我可以想象到,一段时间后,经过几次新功能迭代后,这些代码的情况会变得更糟。 + +这些代码同样难以 mock 依赖,所以同样没有单元测试。 + +### 第一条规则——直白地去反映你的业务逻辑 + +实现 domain 的时候,请不要总去想着类似拘泥数据结构这样的结构体,或者是带着一大堆 setter 和 getter 的“类 ORM” 实体。相反,应该将他们看作**带着行为的类型**。 + +当与业务相关的人聊天时,他们会说 *“我在 13:00 安排了训练”*,而不是 *“我将 13:00 的属性状态设置为了‘安排训练’”*。 + +他们也不会说: *“你无法将属性状态设置为‘安排训练’”*。而是:*“如果时间不合适的话,就无法安排训练”*。那么如何直接把这些反映在代码里面呢? + +```go +func (h *Hour) ScheduleTraining() error { + if !h.IsAvailable() { + return ErrHourNotAvailable + } + + h.availability = TrainingScheduled + return nil +} +``` + +一个可以帮助我们更好实现代码的问题是:*“业务人员可以不需要技术术语的翻译就能够读懂我的代码吗?”*。你可以看下上面的片段,**即使是非技术人员,也能够明白什么时候你可以安排培训**。 + +这个方法的代价不高,并且有助于应对复杂情况,是规则更加易于理解。即使带来的变化不大,我们也摆脱了一大串 `if`,而这一大串 if 在未来会是代码变得更加复杂。 + +我们也同样能够容易地添加单元测试。这很好——我们不需要 mock 什么了。这些测试同样是有助于我们理解 `Hour` 行为的文档。 + +```go +func TestHour_ScheduleTraining(t *testing.T) { + h, err := hour.NewAvailableHour(validTrainingHour()) + require.NoError(t, err) + + require.NoError(t, h.ScheduleTraining()) + + assert.True(t, h.HasTrainingScheduled()) + assert.False(t, h.IsAvailable()) +} + +func TestHour_ScheduleTraining_with_not_available(t *testing.T) { + h := newNotAvailableHour(t) + assert.Equal(t, hour.ErrHourNotAvailable, h.ScheduleTraining()) +} +``` + +现在,如果有人问“什么时候我可以安排训练”,你可以很快回答。在一个更大的系统中,这一类问题的答案相对不怎么明显——好几次我花费数小时去寻找一些对象被意外应用的地方。下一条规则会进一步帮助我们。 + +### 第二条规则——在内存中始终保持一个有效的状态 + +> 我意识到我的代码会以我无法预料的方式使用,也会以并非其设计的方式被使用,并且这种错误使用比按照预期使用的时间更长。——[坚固宣言(The Rugged Manifesto)](https://ruggedsoftware.org/) + +如果所有人将这段引言记住那这个世界会更好。我在这里也不是没有错。😉 + +据我观察,当你确定你使用的对象始终是有效的,那么这会避免许多的 `if` 语句以及避免许多的 bug。无法对当前的代码做任何愚蠢的事情会让你感到更加的自信。 + +我多次提到我害怕去做一些修改,因为我不清楚会带来什么副作用。**在没有把握是否正确使用代码的情况下,开发新功能要慢得多!** + +我们的目标是仅在一个地方去做校验(良好的 DRY)并且确保没有人可以修改 `Hour` 的内在状态。该对象的唯一公共 API 应当是描述行为的方法。而不是愚蠢的 getter 和 setter !我们还需要将类型分开包装,并且所有属性设置为私有。 + +```go +type Hour struct { + hour time.Time + + availability Availability +} + +// ... + +func NewAvailableHour(hour time.Time) (*Hour, error) { + if err := validateTime(hour); err != nil { + return nil, err + } + + return &Hour{ + hour: hour, + availability: Available, + }, nil +} +``` + +我们也应当保证没有破坏我们类型内在的任何规则。 + +不好的例子: + +```go +h := hour.NewAvailableHour("13:00") + +if h.HasTrainingScheduled() { + h.SetState(hour.Available) +} else { + return errors.New("unable to cancel training") +} +``` + +好的例子: + +```go +func (h *Hour) CancelTraining() error { + if !h.HasTrainingScheduled() { + return ErrNoTrainingScheduled + } + + h.availability = Available + return nil +} + +// ... + +h := hour.NewAvailableHour("13:00") +if err := h.CancelTraining(); err != nil { + return err +} +``` + +### 第三条规则——领域需要与数据库无关 + +这里有很多流派——其中一些会告诉你,领域受数据库客户端影响是可以的。以我们的经验来说,严格保证领域不受任何数据库影响会更好。 + +主要的原因是: + +- 领域类型不受所使用的数据库方案影响——他们应当仅受业务规则的影响。 +- 这样我们能够以更好的方式将数据保存在数据库中。 +- 由于 Go 的设计以及缺少类似注解这样的“魔法”,ORM 或者任何数据库解决方案都会以更显著的方式产生影响。 + +> 领域优先方法 +> +> 如果项目足够复杂,我们甚至可以花上 2-4 周时间在领域层上,仅适用纯内存的数据库实现。这样的话,我们可以更深层地探索想法,并且延后选择数据库层的决定。所有我们的实现仅仅基于单元测试。 +> +> 我们尝试了几次这个方法,效果都还不错。在这里使用一些时间框也是不错的主意,这些不会消耗很长时间。 +> +> 请记住,这一方法需要与业务人员有着良好的关系以及充分的信任!**如果与业务人员的关系远远说不上良好,策略 DDD 模式会改善这一情况。去过也做过!** + +为了不使这个文章太长,这里仅介绍下 Repository 接口,并且假定可以正常工作。😉 在接下来的文章中我会更深入地涵盖这个主题。 + +```go +type Repository interface { + GetOrCreateHour(ctx context.Context, time time.Time) (*Hour, error) + UpdateHour( + ctx context.Context, + hourTime time.Time, + updateFn func(h *Hour) (*Hour, error), + ) error +} +``` + +> 你也许会问 为什么 `UpdateHour` 会有 `updateFn func(h *Hour) (*Hour, error)` —— 我们会用它以一种巧妙的方式处理事务。更多信息请看关于 repositories 的文章!😉 + +## 使用领域对象 + +我对我们的 gRPC 端点进行了小小的重构,提供更“行为导向”而不是 [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) 导向的 API。它更好地反映了领域的新特征。以我的经验来说,维护多个,小的方法,相比维护一个,“全能”的,可以让我们更新所有东西的方法要容易的多。 + +```diff +--- a/api/protobuf/trainer.proto ++++ b/api/protobuf/trainer.proto +@@ -6,7 +6,9 @@ import "google/protobuf/timestamp.proto"; + + service TrainerService { + rpc IsHourAvailable(IsHourAvailableRequest) returns (IsHourAvailableResponse) {} +- rpc UpdateHour(UpdateHourRequest) returns (EmptyResponse) {} ++ rpc ScheduleTraining(UpdateHourRequest) returns (EmptyResponse) {} ++ rpc CancelTraining(UpdateHourRequest) returns (EmptyResponse) {} ++ rpc MakeHourAvailable(UpdateHourRequest) returns (EmptyResponse) {} + } + + message IsHourAvailableRequest { +@@ -19,9 +21,6 @@ message IsHourAvailableResponse { + + message UpdateHourRequest { + google.protobuf.Timestamp time = 1; +- +- bool has_training_scheduled = 2; +- bool available = 3; + } + + message EmptyResponse {} +``` + +现在的实现比原来简单多了,并且容易理解。我们这里也没有逻辑——只是一些编排。我们的 gRPC hander 现在有 18 行,并且没有领域逻辑! + +```go +func (g GrpcServer) MakeHourAvailable(ctx context.Context, request *trainer.UpdateHourRequest) (*trainer.EmptyResponse, error) { + trainingTime, err := protoTimestampToTime(request.Time) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "unable to parse time") + } + + if err := g.hourRepository.UpdateHour(ctx, trainingTime, func(h *hour.Hour) (*hour.Hour, error) { + if err := h.MakeAvailable(); err != nil { + return nil, err + } + + return h, nil + }); err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + return &trainer.EmptyResponse{}, nil +} +``` + +> **不要再有八千(Eight-thousanders)** +> 依据我过去的记忆,许多的所谓八千行代码实际上是在 HTTP controller 中有着大量领域逻辑的 controller。 +> +> 在我们的领域类型内部隐蔽复杂性,并且坚持我提到的那些规则,我们可以阻止在这个地方代码不可阻止的增长。 + +## 今天就到这里 + +我不希望让这片文章太冗长——咱们一步一步来! + +如果你等不及,重构的整个 diff 可以在 [GitHub](https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/commit/0249977c58a310d343ca2237c201b9ba016b148e) 上面找到。在下一篇文章中,我会覆盖这些 diff 中没有讲到的一部分:repositories。 + +即使还在刚开始的阶段,在我们代码中一些简化还是明显的。 + +这个模型目前的实现还是不完美的——这很好!在一开始你不会实现完美的模型。**最好是去准备好轻松地修改这个模型,而不是浪费时间取让它变得完美。** 给这个模型添加了测试代码,以及将他与应用中的其他部分分离开来后,我不再惧怕对它进行修改。 + +**我可以在我的简历上说我了解 DDD 了吗?** + +还不行。 + +从听说 DDD 之后到融会贯通我花费了 3 年时间(比我听说 Go 语言还要早 😉 )。此外,我知道为什么我们下一篇文章会讲的所有技术十分重要。在将这些技术融会贯通之前,需要一些耐心而且要相信这些技术会起作用。这是值得的!你不需要想我一样需要 3 年时间,但是我们现在计划了大约 10 篇关于策略和战术模式的文章。😉 在 Wild Workouts 项目中还有很多新的特性以及要重构的部分! + +我知道,现如今有许多人保证经过 10 分钟的文章或视频之后,你会成为某个领域的专家。如果这可能的话世界会是美好的,然而,在现实生活中并不会如此简单。 + +幸运的是,我们所分析的大部分知识是通用且可以应用在多个技术上面,而不仅仅是 Go。长远上,你可以将这些学校视为对你职业生涯以及心理健康的投资 😉。没有比解决正确的问题,同时没有不可维护的代码更好的事情了。 + +**你在 Go 语言中应用 DDD 有什么经验吗?是好是坏?与我们的做法有什么不同吗?你觉得 DDD 在你的项目中是否有用?请在评论中告诉我们!** + +**有没有身边的同事你觉得可能会对这个主题感兴趣?请把这篇文章分享给他们!即使他们不使用 Go。** 😉 + +--- + +via: https://threedots.tech/post/ddd-lite-in-go-introduction/ + +作者:[Robert Laszczak](https://twitter.com/roblaszczak) +译者:[dust347](https://github.com/dust347) +校对:[unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200704-go-timers-life-cycle.md b/published/tech/20200704-go-timers-life-cycle.md new file mode 100644 index 000000000..83e516c78 --- /dev/null +++ b/published/tech/20200704-go-timers-life-cycle.md @@ -0,0 +1,130 @@ +首发于:https://studygolang.com/articles/35198 + +# Go: 定时器的生命周期 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200704-go-timers-life-cycle/图0.png) + +> 本篇文章基于 Go `1.14` + +`定时器` 对于在将来的某个时刻执行代码时非常有用。Go 内部在管理创建的定时器的同时,也会对其执行进行规划。后者可能有点棘手,因为 Go 调度器是一个协作式(`cooperative`)调度器,这意味着一个 goroutine 必须自己停止(阻塞在 `channel` 上,系统调用, 等等)或由调度器在某个调度点暂停。 + +> 如果想要获取更多关于优先权的信息,我建议你阅读我的文章:[Go:Goroutine 与抢占机制](https://studygolang.com/articles/28972)。 + +## 生命周期 + +下面是一个关于 ` 定时器 ` 的最简单的示例: + +```go +package main + +import ( + "os" + "os/signal" + "syscall" + "time" +) + +func main() { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + + time.AfterFunc(time.Second, func() { + println("done") + }) + + <-sigs +} + +``` + +当一个定时器被创建时,它会被保存在与当前 P 关联的定时器的内部列表中,上面的代码可以用下图来表示: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200704-go-timers-life-cycle/图1.png) + +> 如果想要获取更多关于 GMP 模型的内容,建议您可以参考一理我的这篇文章: [Go:协程,操作系统线程和 CPU 管理](https://studygolang.com/articles/25292)。 + +如图所示,一旦定时器被创建,它就会注册一个内部回调,该回调将用关键字 `go` 调用用户回调,将其转换为 goroutine。 + +然后,定时器将由调度器进行管理。在每一轮调度中,它都会检查定时器是否准备好运行,如果准备好了,就准备运行。事实上,由于 Go 调度器本身并不运行任何代码,运行定时器的回调会将其 goroutine 加到本地队列中。然后,当调度器在队列中选中它时,goroutine 就会运行。如下图所示: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200704-go-timers-life-cycle/图2.png) + +根据本地队列的大小,定时器的运行可能会有一些小的延迟。事实上,由于 Go 1.14 中的 `异步抢占`,goroutine 在运行 `10ms` 后就会被抢占,减少了延迟的概率。 + +## 时延 + +为了理解 ` 时延 ` 的可能性,我们来分析一个**从同一个 goroutine 中创建大量定时器的情况**。由于定时器是与当前 P 相连的,所以一个被占用的 P 将无法运行其定时器。这里有一个程序,它创建了数百个定时器,并在其余时间内保持忙碌状态: + +```go +package main + +import ( + "os" + "os/signal" + "sync/atomic" + "syscall" + "time" +) + +func main() { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + + var num int64 = 0 + for i := 0; i < 1e3; i++ { + time.AfterFunc(time.Second, func() { + atomic.AddInt64(&num, 1) + }) + } + + // 耗时超过 1s + t := 0 + for i := 0; i < 1e10; i++ { + t++ + } + + _ = t + + <-sigs + + println(num, "timers created,", t, "iterations done") +} + +``` + +通过下图的 `tracing`,我们可以清楚的看到 goroutine 占用处理器的情况: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200704-go-timers-life-cycle/图3.png) + +图中的每一个区块表示,由于异步抢占,运行中的 goroutine 的被分成了大量的区块。 + +> 更多关于异步抢占的内容,请参考我的这篇文章: [Go: 异步抢占](https://studygolang.com/articles/28460) + +在这些块中,有一个空间看起来比其他的大。让我们把它放大看一下: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200704-go-timers-life-cycle/图4.png) + +这个间隔发生在定时器必须运行的时候。此时,当前的 goroutine 已经被抢占,并被 Go 调度器所取代。正如图中高亮部分所示, 调度器将定时器转换为可执行的 goroutine。 + +然而,当前线程的 Go 调度器并不是唯一一个运行定时器的调度器。Go 实现了一个定时器 `窃取策略`,以确保当前线程相当繁忙时,定时器可以由另一个 `P` 运行。由于异步抢占,这种情况不太可能发生,但在我们的例子中,由于定时器的数量非常多,这种情况还是发生了。如图所示: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200704-go-timers-life-cycle/图5.png) + +如果我们不考虑定时器 `窃取策略`,下图展示了将会发生的事情: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200704-go-timers-life-cycle/图6.png) + +所有持有定时器的 goroutine 都被添加到本地队列中。然后,基于 `P` 之间的 `work-stealing` 策略对其重新进行调度分发。 + +> 更多关于 `work-stealing` 相关资料,请参考我的文章:关于 Go 中工作偷窃的更多信息,我建议你阅读我的文章:[Go 调度器的任务窃取](https://studygolang.com/articles/27146) + +综上所述,由于异步抢占和 `work-stealing` 机制,导致延迟发生的可能性很小。 + +--- +via: https://medium.com/a-journey-with-go/go-timers-life-cycle-403f3580093a + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[double12gzh](https://github.com/double12gzh) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200711-Go-How-Are-Deadlocks-Triggered.md b/published/tech/20200711-Go-How-Are-Deadlocks-Triggered.md new file mode 100644 index 000000000..451f989c3 --- /dev/null +++ b/published/tech/20200711-Go-How-Are-Deadlocks-Triggered.md @@ -0,0 +1,87 @@ +首发于:https://studygolang.com/articles/30260 + +# Go:死锁是如何触发的? + +![illustration](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200711-Go-How-Are-Deadlocks-Triggered/illustration.png) + +由创作原始 Go Gopher 作品的 Renee French 为“ Go 的旅程”创作的插图。 + +*本文基于 Go 1.14。* + +死锁是当 Goroutine 被阻塞而无法解除阻塞时产生的一种状态。Go 提供了一个死锁检测器,可以帮助开发人员避免陷入这种情况。 + +## 检测 + +让我们从创建这种情况的示例开始: + +![example](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200711-Go-How-Are-Deadlocks-Triggered/example.png) + +主 Goroutine 在 channel 上被阻塞,并等待另一个 Goroutine 将数据写入 channel。然而,没有其他的 Goroutine 在运行,它不能被解除阻塞。这种情况将触发死锁错误: + +![deadlock](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200711-Go-How-Are-Deadlocks-Triggered/deadlock.png) + +死锁检测器基于对应用程序创建的线程的分析。如果已创建并活动的线程数大于等待工作的线程数,则会出现死锁情况。 + +*这个公式中不包括为监视系统而创建的线程。* + +在检测到死锁时将创建四个线程: + +- 一个用于主 goroutine,启动程序的那个。 + +- 一个叫做 `sysmon`,用于监视系统。 + +- 一个专用于垃圾收集器的 Goroutine 启动的。 + +- 在初始化过程中阻塞主 Goroutine 时创建的一个线程。由于此 Goroutine 被锁定在它的线程上,因此 Go 需要创建一个新的 Goroutine 来为其他 Goroutine 提供运行时间。 + +每次调用死锁检测器时,也可以通过一些调试信息将其可视化: + +![detector](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200711-Go-How-Are-Deadlocks-Triggered/detector.png) + +每当线程空闲时,就会通知检测器。调试的每一行显示空闲线程的递增数量。当空闲线程数等于活动线程数减去系统线程数时,就会发生死锁。在本例中,我们有三个空闲线程和三个活动线程(四个线程减去系统线程)。由于没有活动线程能够解除阻塞空闲线程,因此存在死锁情况。 + +但是,这种行为有一些限制。实际上,任何自旋的 Goroutine 都会使死锁检测器失效,因为线程将保持活动状态。 + +## 限制 + +现在,通过发送中断信号使 OS 信号停止程序来改进前面的示例: + +![example2](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200711-Go-How-Are-Deadlocks-Triggered/example2.png) + +这是新的输出: + +![output](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200711-Go-How-Are-Deadlocks-Triggered/output.png) + +通过键盘发送中断信号后,程序停止了。不再检测到死锁。具有 `signal.Notify` 的任何活动程序都将运行后台 goroutine,等待输入信号。该 Goroutine 保持活动状态,并且永远不会使活动线程数等于空闲线程数。这是此 Goroutine 的跟踪: + +![trace](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200711-Go-How-Are-Deadlocks-Triggered/trace.png) + +它的大部分时间都花在等待系统调用上。syscall 中的线程不在空闲列表中,因此不会导致死锁。 + +但是,可以通过调试工具找到它们。 + +## 调试 + +发现这些死锁的最好方法可能是编写单元测试。编写测试确保一次运行较小的代码段。在这种情况下,不应该受到信号处理程序或阻塞系统调用的干扰。然而,即使这样做 ,测试也会挂起,我们肯定会发现有可疑的地方。 + +如果你想可视化运行程序上的死锁,可以使用 `pprof` 之类的工具来可视化它。下面是我们修改后的第一个程序,添加了调试功能: + +![debugging](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200711-Go-How-Are-Deadlocks-Triggered/debugging.png) + +一旦程序运行,我们就可以使用命令 `wget http://localhost:6060/debug/pprof/trace?seconds=5` 对我们的应用程序进行配置,该命令会生成 5s 的跟踪信息。 这些痕迹告诉我们所有活动: + +![profile](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200711-Go-How-Are-Deadlocks-Triggered/profile.png) + +没有 Goroutine 一直在运行。可以使用以下命令通过 CPU 配置文件进行确认 `go tool pprof http://localhost:6060/debug/pprof/profile?seconds=5`。下面是未显示活动的配置文件: + +![no-activity](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200711-Go-How-Are-Deadlocks-Triggered/no-activity.png) + +--- + +via: https://medium.com/a-journey-with-go/go-how-are-deadlocks-triggered-2305504ac019 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[alandtsang](https://github.com/alandtsang) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200801-Go-How-to-Reduce-Lock-Contention-with-the-Atomic-Package.md b/published/tech/20200801-Go-How-to-Reduce-Lock-Contention-with-the-Atomic-Package.md new file mode 100644 index 000000000..0e3471891 --- /dev/null +++ b/published/tech/20200801-Go-How-to-Reduce-Lock-Contention-with-the-Atomic-Package.md @@ -0,0 +1,330 @@ +首发于:https://studygolang.com/articles/35385 + +# 如何使用 atomic 包减少锁冲突 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-How-to-Reduce-Lock-Contention-with-the-Atomic-Package/1.png) + +## 写在前面 + +> 本文基于 Golang 1.14 + +Go 提供了 channel 或 mutex 等内存同步机制,有助于解决不同的问题。在共享内存的情况下,mutex 可以保护内存不发生数据竞争(data race)。不过,虽然存在两个 mutex,但 Go 也通过 `atomic` 包提供了原子内存基元来提高性能。在深入研究解决方案之前,我们先回过头来看看数据竞争。 + +## 数据竞争 + +当两个或两个以上的 goroutine 同时访问同一块内存区域,并且其中至少有一个在写时,就会发生数据竞争。虽然 `map` 内部有一定的机制来防止数据竞争,但一个简单的结构体并没有任何的机制,因此容易发生数据竞争。 + +为了说明数据竞争,我以一个**goroutine 持续更新的配置**为例向大家展示一下。 + +```go +package main + +import ( + "fmt" + "sync" +) + +type Config struct { + a []int +} + +func main() { + cfg := &Config{} + + // 启动一个 writer goroutine,不断写入数据 + go func() { + i := 0 + + for { + i++ + cfg.a = []int{i, i + 1, i + 2, i + 3, i + 4, i + 5} + } + }() + + // 启动多个 reader goroutine,不断获取数据 + var wg sync.WaitGroup + for n := 0; n < 4; n++ { + wg.Add(1) + go func() { + for n := 0; n < 100; n++ { + fmt.Printf("%#v\n", cfg) + } + wg.Done() + }() + } + + wg.Wait() +} +``` + +运行这段代码可以清楚地看到,原本期望是运行上述代码后,每一行的数字应该是连续的,但是由于数据竞争的存在,导致结果是非确定性的。 + +```bash +F:\hello>go run main.go +[...] +&main.Config{a:[]int{180954, 180962, 180967, 180972, 180977, 180983}} +&main.Config{a:[]int{181296, 181304, 181311, 181318, 181322, 181323}} +&main.Config{a:[]int{181607, 181617, 181624, 181631, 181636, 181643}} +``` + +我们可以在运行时加入参数 `--race` 看一下结果: + +```bash + +F:\hello>go run --race main.go +[...] +&main.Config{a:[]int(nil)} +================== +&main.Config{a:[]int(nil)} +WARNING: DATA RACE&main.Config{a:[]int(nil)} + +Read at 0x00c00000c210 by Goroutine 9: + reflect.Value.Int() + D:/Go/src/reflect/value.go:988 +0x3584 + fmt.(*pp).printValue() + D:/Go/src/fmt/print.go:749 +0x3590 + fmt.(*pp).printValue() + D:/Go/src/fmt/print.go:860 +0x8f2 + fmt.(*pp).printValue() + D:/Go/src/fmt/print.go:810 +0x289a + fmt.(*pp).printValue() + D:/Go/src/fmt/print.go:880 +0x261c + fmt.(*pp).printArg() + D:/Go/src/fmt/print.go:716 +0x26b + fmt.(*pp).doPrintf() + D:/Go/src/fmt/print.go:1030 +0x326 + fmt.Fprintf() + D:/Go/src/fmt/print.go:204 +0x86 + fmt.Printf() + D:/Go/src/fmt/print.go:213 +0xbc + main.main.func2() + F:/hello/main.go:31 +0x42 + +Previous write at 0x00c00000c210 by goroutine 7: + main.main.func1() + F:/hello/main.go:21 +0x66 + +goroutine 9 (running) created at: + main.main() + F:/hello/main.go:29 +0x124 + +goroutine 7 (running) created at: + main.main() + F:/hello/main.go:16 +0x95 +================== +``` + +为了避免同时读写过程中产生的数据竞争最常采用的方法可能是使用 `mutex` 或 `atomic` 包。 + +## Mutex?还是 Atomic? + +标准库在 `sync` 包提供了两种互斥锁 :**sync.Mutex** 和 **sync.RWMutex**。后者在你的程序需要处理多个读操作和极少的写操作时进行了优化。 + +针对上面代码中产生的数据竞争问题,我们看一下,如何解决呢? + +### 使用 `sync.Mutex` 解决数据竞争 + +```go +package main + +import ( + "fmt" + "sync" +) + +// Config 定义一个结构体用于模拟存放配置数据 +type Config struct { + a []int +} + +func main() { + cfg := &Config{} + var mux sync.RWMutex + + // 启动一个 writer goroutine,不断写入数据 + go func() { + i := 0 + + for { + i++ + // 进行数据写入时,先通过锁进行锁定 + mux.Lock() + cfg.a = []int{i, i + 1, i + 2, i + 3, i + 4, i + 5} + mux.Unlock() + } + }() + + // 启动多个 reader goroutine,不断获取数据 + var wg sync.WaitGroup + for n := 0; n < 4; n++ { + wg.Add(1) + go func() { + for n := 0; n < 100; n++ { + // 因为这里只是需要读取数据,所以只需要加一个读锁即可 + mux.RLock() + fmt.Printf("%#v\n", cfg) + mux.RUnlock() + } + wg.Done() + }() + } + + wg.Wait() +} +``` + +通过上面的代码,我们做了两处改动。第一处改动在写数据前通过 `mux.Lock()` 加了一把锁;第二处改动在读数据前通过 `mux.RLock()` 加了一把读锁。 + +运行上述代码看一下结果: + +```bash +F:\hello>go run --race main.go +&main.Config{a:[]int{512, 513, 514, 515, 516, 517}} +&main.Config{a:[]int{512, 513, 514, 515, 516, 517}} +&main.Config{a:[]int{513, 514, 515, 516, 517, 518}} +&main.Config{a:[]int{513, 514, 515, 516, 517, 518}} +&main.Config{a:[]int{513, 514, 515, 516, 517, 518}} +&main.Config{a:[]int{513, 514, 515, 516, 517, 518}} +&main.Config{a:[]int{514, 515, 516, 517, 518, 519}} +[...] +``` + +这次达到了我们的预期并且也没有产生数据竞争。 + +### 使用 `atomic` 解决数据竞争 + +```go +package main + +import ( + "fmt" + "sync" + "sync/atomic" +) + +type Config struct { + a []int +} + +func main() { + var v atomic.Value + + // 写入数据 + go func() { + var i int + for { + i++ + cfg := Config{ + a: []int{i, i + 1, i + 2, i + 3, i + 4, i + 5}, + } + v.Store(cfg) + } + }() + + // 读取数据 + var wg sync.WaitGroup + for n := 0; n < 4; n++ { + wg.Add(1) + go func() { + for n := 0; n < 100; n++ { + cfg := v.Load() + fmt.Printf("%#v\n", cfg) + } + wg.Done() + }() + } + + wg.Wait() +} +``` + +这里我们使用了 `atomic` 包,通过运行我们发现,也同样达到了我们期望的结果: + +```bash +[...] +main.Config{a:[]int{219142, 219143, 219144, 219145, 219146, 219147}} +main.Config{a:[]int{219491, 219492, 219493, 219494, 219495, 219496}} +main.Config{a:[]int{219826, 219827, 219828, 219829, 219830, 219831}} +main.Config{a:[]int{219948, 219949, 219950, 219951, 219952, 219953}} +``` + +从生成的输出结果而言,看起来使用 `atomic` 包的解决方案要快得多,因为它可以生成更高的数字序列。 +为了更加严谨的证明这个结果,我们下面将对这两个程序进行基准测试。 + +## 性能分析 + +一个 benchmark 应该根据被测量的内容来解释。因此,我们假设之前的程序,有一个不断存储新配置的 ` 数据写入器 `,同时也有多个不断读取配置的 ` 数据读取器 `。为了涵盖更多潜在的场景,我们还将包括一个只有 ` 数据读取器 ` 的 benchmark,假设 Config 不经常改变。 + +下面是部分 benchmark 的代码: + +```go +func BenchmarkMutexMultipleReaders(b *testing.B) { + var lastValue uint64 + var mux sync.RWMutex + var wg sync.WaitGroup + + cfg := Config{ + a: []int{0, 0, 0, 0, 0, 0}, + } + + for n := 0; n < 4; n++ { + wg.Add(1) + + go func() { + for n := 0; n < b.N; n++ { + mux.RLock() + atomic.SwapUint64(&lastValue, uint64(cfg.a[0])) + mux.RUnlock() + } + wg.Done() + }() + } + + wg.Wait() +} +``` + +执行上面的测试代码后我们可以得到如下的结果: + +```bash +name time/op +AtomicOneWriterMultipleReaders-4 72.2ns ± 2% +AtomicMultipleReaders-4 65.8ns ± 2% + +MutexOneWriterMultipleReaders-4 717ns ± 3% +MutexMultipleReaders-4 176ns ± 2% +``` + +基准测试证实了我们之前看到的性能情况。为了了解 mutex 的瓶颈到底在哪里,我们可以在启用 `tracer` 的情况下重新运行程序。 + +> 更多关于 `tracer` 的内容,请参考[trace](https://medium.com/a-journey-with-go/go-discovery-of-the-trace-package-e5a821743c3c)这篇文章。 + +下图是使用 `atomic` 包时,使用 `pprof` 分析后得到 profile 结果: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-How-to-Reduce-Lock-Contention-with-the-Atomic-Package/2.png) + +goroutines 运行时不间断,能够完成任务。对于带有 `mutex` 的程序的配置文件,得到的结果那是完全不同的。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-How-to-Reduce-Lock-Contention-with-the-Atomic-Package/3.png) + +现在运行时间相当零碎,这是由于停放 goroutine 的 mutex 造成的。这一点可以从 goroutine 的概览中得到证实,其中显示了同步时被阻塞的时间(如下图)。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-How-to-Reduce-Lock-Contention-with-the-Atomic-Package/4.png) + +屏蔽时间大概占到三分之一的时间,这一点可以从下面的 block profile 的图中详细看到。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/Go-How-to-Reduce-Lock-Contention-with-the-Atomic-Package/5.png) + +在这种情况下,`atomic` 包肯定会带来优势。但是,在某些方面可能会降低性能。例如,如果你要存储一张大地图,每次更新地图时都要复制它,这样效率就很低。 + +> 更多关于 `mutex` 的内容可以参考[Go: Mutex and Starvation](https://medium.com/a-journey-with-go/go-mutex-and-starvation-3f4f4e75ad50) + +--- + +via: https://medium.com/a-journey-with-go/go-how-to-reduce-lock-contention-with-the-atomic-package-ba3b2664b549 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[double12gzh](https://github.com/double12gzh) +校对:[lxbwolf](https://github.com/lxbwolf) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200813-Go-Built-in-Functions-Optimizations.md b/published/tech/20200813-Go-Built-in-Functions-Optimizations.md new file mode 100644 index 000000000..bf5f385f1 --- /dev/null +++ b/published/tech/20200813-Go-Built-in-Functions-Optimizations.md @@ -0,0 +1,96 @@ +首发于:https://studygolang.com/articles/35199 + +# Go: 内置函数优化 + +![由 Renee French 创作的原始 Go Gopher 作品,为“ Go 的旅程”创作的插图。](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200813-Go-Built-in-Functions-Optimizations/Illustration.png) + +ℹ️ 这篇文章基于 Go 1.13。 + +Go 语言提供内置函数来辅助开发者处理 channel,slice,或者 map。一些内置函数有着像样的内部实现,比如 `make()`,而有的内置函数完全没有实现,而是由编译器所管理。让我们一起分析一些内置函数,来理解 Go 如何处理它们。 + +## Slices + +如果可以事先知道的话,Go 有时可以把一个在运行时完成的函数调用替换为它的结果。来看一个使用切片的例子: + +```go +func main() { + s := make([]int, 0, 6) + s = append(s, 12) + s = append(s, 34) + + l := len(s) + println("the length is ", l) + + c := cap(s) + println("the capacity is ", c) +} +``` + +函数 `len` 和 `cap` 实际上没有具体实现。编译器能够跟踪在切片上所做的更改,并将长度或者容量函数替换为可以代表它们的常量。这是[汇编](https://golang.org/doc/asm)代码表示: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200813-Go-Built-in-Functions-Optimizations/replace-length-or-capacity%20function-constant.png) + +但是,编译器不可能永远确定切片的大小。在这种情况下,比如,切片是一个函数的参数,这个函数没有指定其大小。这是一个例子: + +```go +func main() { + s := make([]int, 0, 6) + s = append(s, 12, 34) + getLength(s) +} + +//go:noinline +func getLength(s []int) { + l := len(s) + println("the length is", l) +} +``` + +这是生成的指令: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200813-Go-Built-in-Functions-Optimizations/1_9eu8BEzj2ATNR2tmdsCMfg.png) + +由于 Go 无法了解 `getLength()` 方法被如何使用,只能直接从内存里面读取切片的长度。同样的行为也会引用在切片的容量上。 + +*Go 通常会指向切片的长度,防止不良的内存访问,比如越界读取。更多信息,建议阅读我的文章“[Go:边界检查保证内存安全](https://studygolang.com/articles/28456)”。* + +## Unsafe + +`unsafe` 包同样暴露了没有任何实现的函数。由 Go 标准库提供的定义原型,仅仅用作说明文档: + +![Example of the documentation of the unsafe package](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200813-Go-Built-in-Functions-Optimizations/Example-of-the-documentation-of-the-unsafe-package.png) + +这是这个包的例子: + +```go +type T1 struct { + a int64 + b bool +} + +func main() { + t1 := T1{} + + println("size of the struct:", unsafe.Sizeof(t1)) + println("alignment of the struct:", unsafe.Alignof(t1)) +} +Output: +size of the struct: 16 +alignment of the struct: 8 +``` + +再次,编译器有着关于结构的足够信息,可以直接将值写为常量: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200813-Go-Built-in-Functions-Optimizations/write-the-values-as-constants-directly.png) + +*Go 语言提供了更多的内置函数,比如 `make` 或 `copy`。关于 `copy` 函数的更多细节信息,建议阅读我的文章“Go:切片以及内存管理”,该文章详细阐述了优化细节。关于在 map 上的优化,建议阅读“[Go:根据代码设计 map——第二部分](https://medium.com/a-journey-with-go/go-map-design-by-code-part-ii-50d111557c08)” 来深入了解。* + +--- + +via: https://medium.com/a-journey-with-go/go-built-in-functions-optimizations-70c5abb3a680 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[dust347](https://github.com/dust347) +校对:[polaris1119](polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200820-Interface-Segregation-In-Action-with-Go.md b/published/tech/20200820-Interface-Segregation-In-Action-with-Go.md new file mode 100644 index 000000000..85606b84f --- /dev/null +++ b/published/tech/20200820-Interface-Segregation-In-Action-with-Go.md @@ -0,0 +1,120 @@ +首发于:https://studygolang.com/articles/35200 + +# 接口分离原则在 Go 语言中的实践 + +每个人都应该写一篇关于 Golang 接口的文章!不知道我为什么等了这么久才写了这篇! + +当你需要 mock 一个对象或者函数需要接受一组相关的功能从而来与对象进行交互时,Golang 的接口都可以 +使这些变得更为简单。 + +是的!实际上接口就是被用来实现这些目的的,你或许有一个实现了很多方法的对象实例,但当你将它作为参数传递 +给另外一个函数的时候,该函数可能仅仅使用了对象实例的一部分方法,为了解决这个问题,你可以通过更改函数签名的方式来解决, +你可以定义一个新的接口,函数接收实现了该接口的对象实例,该接口只包含函数所需要的功能方法。 + +通过上述的方法,当你对函数进行单元测试的时候,用来 mock 传参对象实例的代码也会更少,更容易处理(这种方式很好地将 +不必要暴露的方法影藏了起来)。 + +当你将接口定义得更小,并通过组合来使用这些接口的时候,上述方式带来的好处就显得更为明显。 + +举个例子来说,假设你需要对一个可被增删改查的资源设计接口。这种做法在实际工作中是很有用的,因为通过这种方式, +对于那些可以被数据库持久化的资源,我们可以更好的规范它们的行为,使得对它们的操作更标准化。接下来我会具体阐述这个例子。 + +我在下面的例子中使用了 `interface{}`,但你在实际工作中,应该尽量避免使用,因为它实在太宽泛了。但是在例如 `Kubernetes` 的实现中使用了 +[runtime.Object](https://godoc.org/k8s.io/apimachinery/pkg/runtime),实际上是一种更好的选择。即将在 Go 2.0 版本中引入 +的泛型支持会使得类似场景中的实现更简单。或者你也可以用代码生成来实现。但总而言之,`Kubernetes` 中使用可序列化的对象这一思想是非常优秀的。 + +```go +type Resource interface { + Create(ctx context.Context) error + Update(ctx context.Context, updated interface{}) error + Delete(ctx context.Context) error +} +``` + +上述接口中定义的方法并不多,可以满足场景需求,但我并不是很喜欢接口的命名。我并不能通过接口名清楚地了解其意图。该接口定义了一种资源,但是 +我在命名接口的时候通常更喜欢选用动词或形容词。在我们所描述的场景中,实现该接口的是一种可以被数据库持久化的资源。因此我认为更确切的接口名称应该是: +["Persistable"](https://en.wiktionary.org/wiki/persistable),因为它使接口的意图更为明显。 + +我们根据动作将接口进行拆分: + +```go +type Creatable interface { + Create(ctx context.Context) error +} + +type Updatable interface { + Update(ctx context.Context, updated interface{}) error +} + +type Deletable interface { + Delete(ctx context.Context) error +} +``` + +如果需要的话,你可以借助 Go 语言的特性,利用组合的方式将上述三个接口组合成一个新的接口: + +```go +type Persistable interface { + Deletable + Updatable + Creatable +} +``` + +当函数需要两种或两种或两种以上动作的时候,上述的方式是很有用的。假设你需要定义一个接口包含 `Get` 或 `View` 操作,你可以考虑 +重新定义一个 `ReadOnly` 的接口,包含 `Get`,`View` 操作,并定义一个 `Modifiable` 的接口,包含 `Update`, `Create`, `Delete` 操作。 + +试想一下你正在编写一组 http handlers 来实现对资源进行增删改查(CRUD)的 API 接口: + +```bash +Create +Update +Delete +List +GetByID +``` + +通常来说,会像下面代码中这样,你可以对每个函数都定义一个接口,你所有的资源都需要实现这个接口里的方法,这样对于所有的实现,你都可以 +统一调用 "Create" 方法来进行资源的创建: + +```go +func CreateHandle(c Creatable) func(w http.ResponseWriter, r *http.Request) { + return http.HandleFunc("/resource", func(w http.ResponseWriter, r *http.Request) { + if err := c.Create(r.Context); if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusCreated) + }) +} +``` + +如果你想要为 handler 编写测试的话,不论 resource 的实现有多复杂,你只需要确保 mock 对象实现了 `Creatable` 接口,这意味着你的 mock +对象只需要实现一个方法(因为 `Creatable` 接口只包含一个方法)。 文中描述的仅仅是一个简单的例子,假设你希望增加验证的逻辑,那么你仅需要在 +`Creatable` 接口中添加方法 `func Valid() error`。 + +```go +func CreateHandle(c Creatable) func(w http.ResponseWriter, r *http.Request) { + return http.HandleFunc("/resource", func(w http.ResponseWriter, r *http.Request) { + if err := c.Valid(); err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + if err := c.Create(r.Context); if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusCreated) + }) +} +``` + +--- + +via: https://gianarb.it/blog/interface-segreation-in-action-with-go + +作者:[gianarb](https://twitter.com/gianarb) +译者:[jamesxuhaozhe](https://github.com/jamesxuhaozhe) +校对:[unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200821-Avoid-using-golang-http.DefaultServerMux-for-production-servers.md b/published/tech/20200821-Avoid-using-golang-http.DefaultServerMux-for-production-servers.md new file mode 100644 index 000000000..e5c3150bf --- /dev/null +++ b/published/tech/20200821-Avoid-using-golang-http.DefaultServerMux-for-production-servers.md @@ -0,0 +1,70 @@ +首发于:https://studygolang.com/articles/30837 + +# 不要在生产环境使用 Golang 的 http.DefaultServerMux + +我看到许多文章和帖子都显示了一种方便简单的方法来这样创建 Go 的 Web 服务: + +```golang +package main + +import ( + "fmt" + "log" + "net/http" +) + +func main() { + http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request){ + fmt.Fprintf(w, "pong") + }) + + fmt.Printf("Starting server at port 8080\n") + if err := http.ListenAndServe(":8080", nil); err != nil { + log.Fatal(err) + } +} +``` + +上边的代码里将注册路由 `http.HandleFunc` 和 处理函数 `http.Handle` 注册到默认多路复用器 `DefaultServerMux`。问题是 `DefaultServerMux` 是一个全局的并且可导出的变量。 + +黑客可能开发一个恶意的包(lib)或者劫持伪装一个正常的包,将破坏性的 `Handle` 函数注册到 `DefaultServerMux`,例如使用 `init` 函数: + +```golang +package evillogger + +func init(){ + someBoringSetUp() +} + +func someBoringSetUp(){ + http.HandleFunc("/xd", commonAndBoringFunctionname) +} + +func commonAndBoringFunctionname(w http.ResponseWriter, r *http.Request){ + type osenv struct { + Key string + Value string + } + envs := []osenv{} + for _, element := range os.Environ() { + variable := strings.Split(element, "=") + envs = append(envs, osenv{Key: variable[0], Value: variable[1]}) + } + _ = json.NewEncoder(w).Encode(map[string]interface{}{"inyected: ": &envs}) +} +``` + +在大型项目中混入恶意程序并非难事,但是避免这个问题的方法也很简单,只需要新建一个多路复用器即可: + +`serverMux := http.NewServeMux()` + +在我看来,最大的收获是:**没有经过任何验证,不要引入任何不可信的第三方库!** + +--- +via: + +作者:[Santiago Rodriguez](https://sgrodriguez.github.io/about.html) +译者:[TomatoAres](https://github.com/TomatoAres) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200901-Go-Multiple-Errors-Management.md b/published/tech/20200901-Go-Multiple-Errors-Management.md new file mode 100644 index 000000000..47775b863 --- /dev/null +++ b/published/tech/20200901-Go-Multiple-Errors-Management.md @@ -0,0 +1,93 @@ +首发于:https://studygolang.com/articles/35246 + +# Go:多错误管理 + +![由Renee French创作的原始Go Gopher制作的“ Go的旅程”插图。](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200901-Go-Multiple-Errors-Management/Illustration.png) + +Go 语言中的错误(error)管理总是能引起争论,同时,在关于使用 Go 语言的时候,开发者面对最大的挑战的[年度调查](https://blog.golang.org/survey2019-results)中也是一个经常性的话题。然而,在并发环境处理 error 的场景下,或者在同一个 goroutine 中合并多个错误的场景下,Go 提供了很不错的包可以让多个错误的处理变得简单:来看看如何合并由单个 goroutine 生成的多个 error。 + +## 一个 goroutine,多个 error + +当编写有着重试策略的代码时,将多个 error 合并为一个会十分有用,比如,下面是我们需要收集生成的 error 的一个基本例子: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200901-Go-Multiple-Errors-Management/a-basic-example.png) + +这个程序读取并解析一个 CSV 文本,并且展示发现的错误。如果将 error 聚合为一个完整的报告,会更加方便。为了将错误合并为一个,我们可以在两个不错的包中进行选择: + +- 使用 [HashiCorp](https://github.com/hashicorp) 的 [go-multierror](https://github.com/hashicorp/go-multierror) ,error 可以被合并为一个标准 error: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200901-Go-Multiple-Errors-Management/Using-go-multierror.png) + +之后可以打印出一个报告: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200901-Go-Multiple-Errors-Management/a-report.png) + +- 使用 [Uber](https://github.com/uber-go) 的 [multierr](https://github.com/uber-go/multierr): + +这里的实现是类似的,这是输出: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200901-Go-Multiple-Errors-Management/Using-multierr.png) + +error 通过分号连接,没有经过其他格式化。 + +关于两个包的性能,这是一个使用相同程序,有着更高次数失败的基准测试: + +``` +name time/op alloc/op allocs/op +HashiCorpMultiErrors-4 6.01µs ± 1% 6.78kB ± 0% 77.0 ± 0% +UberMultiErrors-4 9.26µs ± 1% 10.3kB ± 0% 126 ± 0% +``` + +Uber 的实现略慢,同时消耗更多内存。但是,这个包被设计为一次将错误聚合在一起,而不是每次都追加它们。在聚合 error 的时候,结果是接近的。但是由于需要额外步骤,代码有点不太优雅。这是新的结果: + +``` +name time/op alloc/op allocs/op +HashiCorpMultiErrors-4 6.01µs ± 1% 6.78kB ± 0% 77.0 ± 0% +UberMultiErrors-4 6.02µs ± 1% 7.06kB ± 0% 77.0 ± 0% +``` + +两个包都通过在自定义实现中实现了 `Error() string` 函数的方式利用了 Go 的 `error` 接口。 + +## 一个 error,多个 goroutine + +在操作多个 goroutine 来处理一个任务的时候,为了保证程序的正确性,正确地管理结果和错误汇总是有必要的。 + +以一个程序开始,该程序使用多个 goroutine 执行一系列行为(action);每个行为持续一秒: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200901-Go-Multiple-Errors-Management/use-multiple-goroutines-to-perform-a-series-of-actions.png) + +为了描绘 error 传播,第三个 goroutine 的第一个 action 会失败。这是发生的事情: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200901-Go-Multiple-Errors-Management/illustrate-the-error-propagation.png) + +如同预期的一样,这个程序大致用了三秒钟,因为大多数 goroutine 需要经历三个 action,每一个需要一秒: + +```bash +go run . 0.30s user 0.19s system 14% cpu 3.274 total +``` + +然而,我们可能希望使 goroutine 之间相互依赖,并且如果其中一个失败就取消他们。避免无谓工作的解决方案可以是加一个 context,并且,一旦一个 goroutine 失败,就会取消它: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200901-Go-Multiple-Errors-Management/avoid-unnecessary-work.png) + +这恰好就是 [`errgroup`](https://pkg.go.dev/golang.org/x/sync/errgroup?tab=doc) 所提供的;当处理一组 goroutine 的时候,一个错误以及上下文传播。这是使用 [`errgroup`](https://pkg.go.dev/golang.org/x/sync/errgroup?tab=doc) 包的新代码: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200901-Go-Multiple-Errors-Management/using-the-package-errgroup.png) + +由于通过 error 传播了取消的上下文,这个程序现在运行地更快了: + +```bash +go run . 0.30s user 0.19s system 38% cpu 1.269 total +``` + +这个包所带来的其他好处是,我们不需要再操心等待组的增加以及将 goroutine 标记为已完成。这个包为我们管理了这些,我们仅仅需要说明什么时候我们准备好了等待过程的结束。 + +--- + +via: https://medium.com/a-journey-with-go/go-multiple-errors-management-a67477628cf1 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[dust347](https://github.com/dust347) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200907-Go-Introduction-to-the-Escape-Analysis.md b/published/tech/20200907-Go-Introduction-to-the-Escape-Analysis.md new file mode 100644 index 000000000..a287075df --- /dev/null +++ b/published/tech/20200907-Go-Introduction-to-the-Escape-Analysis.md @@ -0,0 +1,196 @@ +首发于:https://studygolang.com/articles/34524 + +# Golang 逃逸分析简介 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200907-Go-Introduction-to-the-Escape-Analysis/0.png) + +> 本篇文章基于 Golang 1.13. + +`逃逸分析` 是 Golang 编译器中的一个阶段,它通过分析用户源码,决定哪些变量应该在堆栈上分配,哪些变量应该逃逸到堆中。 + +## 静态分析 + +Go 静态地定义了在编译阶段应该被堆或栈分配的内容。当编译(`go build`)和/或运行(`go run`)你的代码时,可以通过标志 `-gcflags="-m "` 进行分析。下面是一个简单的例子。 + +```go +package main + +import "fmt" + +func main() { + num := GenerateRandomNum() + fmt.Println(*num) +} + +//go:noinline +func GenerateRandomNum() *int { + tmp := rand.Intn(500) + + return &tmp +} +``` + +运行逃逸分析,具体命令如下: + +```bash +F:\hello>go build -gcflags="-m" main.go +# command-line-arguments +.\main.go:15:18: inlining call to rand.Intn +.\main.go:10:13: inlining call to fmt.Println +.\main.go:15:2: moved to heap: tmp +.\main.go:10:14: *num escapes to heap +.\main.go:10:13: []interface {} literal does not escape +:1: .this does not escape +:1: .this does not escape +``` + +从上面的结果 `.\main.go:15:2: moved to heap: tmp` 中我们发现 `tmp` 逃逸到了堆中。 + +静态分析的第一步是**生成源码的抽象语法树**(具体命令:`go build -gcflags="-m -m -m -m -m -W -W" main.go`),让 GoLang 了解在哪里进行了赋值和分配,以及变量的寻址和解引用。 + +下面是之前代码生成的 ` 抽象语法树 ` 的一个例子: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200907-Go-Introduction-to-the-Escape-Analysis/1.png) + +> 关于抽象语法树请参考: [package ast](https://golang.org/pkg/go/ast/#example_Print), [ast example](https://golang.org/src/go/ast/example_test.go) + +为了简化分析, 下面我给出了一个简化版的 ` 抽象语法树 ` 的结果: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200907-Go-Introduction-to-the-Escape-Analysis/2.png) + +由于该树暴露了定义的变量(用 `NAME` 表示)和对指针的操作(用 `ADDR` 或 `DEREF` 表示),故它可以向 GoLang 提供进行 ` 逃逸分析 ` 所需要的所有信息。一旦建立了树,并解析了函数和参数,GoLang 现在就可以应用 ` 逃逸分析 ` 逻辑来查看哪些应该是堆或栈分配的。 + +## 超过堆栈框架的生命周期 + +在运行 ` 逃逸分析 ` 并从 AST 图中遍历函数(即: 标记)的同时,Go 会寻找那些超过当前栈框架并因此需要进行堆分配的变量。假设没有堆分配,在这个基础上,通过前面例子的栈框架来表示,我们先来定义一下 `outlive` 的含义。下面是调用这两个函数时,堆栈向下生长的情况。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200907-Go-Introduction-to-the-Escape-Analysis/3.png) + +在这种情况下,变量 `num` 不能指向之前堆上分配的变量。在这种情况下,Go 必须在 ` 堆 ` 上分配变量,确保它的生命周期超过堆栈框架的生命周期。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200907-Go-Introduction-to-the-Escape-Analysis/4.png) + +变量 `tmp` 现在包含了分配给堆栈的内存地址,可以安全地从一个堆栈框架复制到另一个堆栈框架。然而,并不是只有返回的值才会失效。下面是规则: + +- 任何返回的值都会超过函数的生命周期,因为被调用的函数不知道这个值。 +- 在循环外声明的变量在循环内的赋值后会失效。如下面的例子: + +```go +package main + +func main() { + var l *int + for i := 0; i < 10; i++ { + l = new(int) + *l = i + } + println(*l) +} + +./main.go:8:10: new(int) escapes to heap + +``` + +- 在闭包外声明的变量在闭包内的赋值后失效。 + +```go +package main + +func main() { + var l *int + func() { + l = new(int) + *l = 1 + }() + println(*l) +} + +./main.go:10:3: new(int) escapes to heap +``` + +`逃逸分析` 的第二部分包括确定它是如何操作指针的,帮助了解哪些东西可能会留在堆栈上。 + +## 寻址和解引用 + +构建一个表示寻址/引用次数的加权图,可以让 Go 优化堆栈分配。让我们分析一个例子来了解它是如何工作的: + +```go +package main + +func main() { + n := getAnyNumber() + println(*n) +} + +//go:noinline +func getAnyNumber() *int { + l := new(int) + *l = 42 + + m := &l + n := &m + o := **n + + return o +} +``` + +运行 ` 逃逸分析 ` 表明,分配逃逸到了堆。 + +```bash +./main.go:12:10: new(int) escapes to heap +``` + +下面是一个简化版的 AST 代码: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200907-Go-Introduction-to-the-Escape-Analysis/5.png) + +Go 通过建立加权图来定义分配。每一次解引用,在代码中用 `*` 表示,或者在节点中用 `DEREF` 表示,权重增加 `1`;每一次寻址操作,在代码中用 `&` 表示,或者在节点中用 `ADDR` 表示,权重减少 `1`。 + +下面是由 ` 逃逸分析 ` 定义的序列: + +```bash +variable o has a weight of 0, o has an edge to n +variable n has a weight of 2, n has an edge to m +variable m has a weight of 1, m has an edge to l +variable l has a weight of 0, l has an edge to new(int) +variable new(int) has a weight of -1 +``` + +每个变量最后的计数为负数,如果超过了当前的栈帧,就会逃逸到堆中。由于返回的值超过了其函数的堆栈框架,并通过其边缘得到了负数,所以分配逃到了堆上。 + +构建这个图可以让 Go 了解哪个变量应该留在栈上(尽管它超过了栈的时间)。下面是另一个基本的例子: + +```go +func main() { + num := func1() + println(*num) +} + +//go:noinline +func func1() *int { + n1 := func2() + *n1++ + + return n1 +} + +//go:noinline +func func2() *int { + n2 := rand.Intn(99) + + return &n2 +} +./main.go:20:2: moved to heap: n2 +``` + +变量 `n1` 超过了堆栈框架,但它的权重不是负数,因为 `func1` 没有在任何地方引用它的地址。然而,`n2` 会超过栈帧并被取消引用,Go 可以安全地在堆上分配它。 + +--- +via: https://medium.com/a-journey-with-go/go-introduction-to-the-escape-analysis-f7610174e890 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[double12gzh](https://github.com/double12gzh) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200912-Optimizing-String-Comparisons-in-Go.md b/published/tech/20200912-Optimizing-String-Comparisons-in-Go.md new file mode 100644 index 000000000..a5e3ad13d --- /dev/null +++ b/published/tech/20200912-Optimizing-String-Comparisons-in-Go.md @@ -0,0 +1,393 @@ +首发于:https://studygolang.com/articles/35247 + +# Go 中优化字符串的比较操作 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200912-Optimizing-String-Comparisons-in-Go/00.jpeg) + +你想让你的 Go 程序运行得更快些吗?优化下 Go 程序中的字符串比较可以减少程序的响应时间,以及增强它的可扩展性。对比两个字符串来检查它们是否相等需要一定的处理能力,并不是所有的比较操作都是相同的。 + +在之前的一篇文章中,我们研究了 [Go 中如何比较字符串](https://www.jeremymorgan.com/tutorials/go/how-do-i-compare-strings-go/?ref=hackernoon.com),也做了一些基准测试。今天我们要在这里展开讨论下。 + +它貌似是一件小事,但是不积跬步无以至千里。我们一起来研究下。 + +## 测量大小写敏感的比较 + +首先,我们来测量下两种字符串比较 + +### 第一种:使用比较操作符 + +```go +if a == b { return true +2 +}else { return false +3 +} +``` + +### 第二种:使用 Strings.Compare + +```go +if strings.Compare(a, b) == 0 { + return true +} +return false +``` + +我们看到第一种方法相对简单点。我们不需要引入标准库的包,代码量也少一点。这看起来很好,但是哪一种更快呢?我们来验证一下。 + +首先,我们创建一个带有测试文件的应用。我们将使用 Go 测试工具中的 Benchmarking 实用工具。 + +#### compare.go + +```go +package main +import ( + "strings" +) + +func main() { +} +// operator compare +func compareOperators(a string, b string) bool { + if a == b { + return true + } else { + return false + } +} +// strings compare +func compareString(a string, b string) bool { + if strings.Compare(a, b) == 0 { + return true + } + return false +} +``` + +我们还会为它创建几个测试用例: + +#### compare_test.go + +```go +package main + +import ( + "testing" +) + +func BenchmarkCompareOperators(b *testing.B) { + for n := 0; n < b.N; n++ { + compareOperators("This is a string", "This is a strinG") + } +} + +func BenchmarkCompareString(b *testing.B) { + for n := 0; n < b.N; n++ { + compareString("This is a string", "This is a strinG") + } +} +``` + +我会修改示例字符串的最后一个字符,以此来确认下比较时两个方法是否都解析了整个字符串。 + +如果你之前没有用过这种方式,可以看一下这个提示: + +- 使用的包是 [testing](https://golang.org/pkg/testing/) +- 文件命名为 **compare_test.go**,Go 就自动识别为测试文件 +- 我们没有使用 test,而是插入了 benchmark。每个函数都以 **Benchmark** 开头 +- 我们使用 bench 参数来运行我们的测试 + +使用下面的命令来运行我们的基准测试: + +```go +go test -bench=. +``` + +下面是运行结果: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200912-Optimizing-String-Comparisons-in-Go/01.png) + +从上图可以看出,使用标准的比较操作符比使用 Strings 包的方法要快。2.92 纳秒比 7.39 纳秒。 + +重复跑了几次测试,结果都差不多: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200912-Optimizing-String-Comparisons-in-Go/02.png) + +很明显第一种方式要快。如果数量级足够大,5 ns 的差别可能会非常大。 + +--- + +**结论:*在对字符串进行大小写敏感的比较时,最基本的字符串比较操作符比使用 strings 包进行比较要快。*** + +--- + +## 测量大小写不敏感的比较 + +我们来改变一下条件。通常情况下,我做字符串比较时,我是想看下两个字符串的字母是否一样,而不关心字母的大小写。这对于我们的操作来说就增加了一些复杂性。 + +```go +sampleString := "This is a sample string" +compareString := "this is a sample string" +``` + +使用标准的比较操作进行对比,由于 T 字母大写,因此两个字符串不相等。 + +然而,我们关心的是字母是否相同,而不关心字母的大小写。所以,我们来改一下代码: + +```go +// operator compare +func compareOperators(a string, b string) bool { + if strings.ToLower(a) == strings.ToLower(b) { + return true + } + return false +} +// strings compare +func compareString(a string, b string) bool { + if strings.Compare(strings.ToLower(a), strings.ToLower(b)) == 0 { + return true + } + return false +} +``` + +我们先把字符串的字母都变成小写的,再进行比较。为了确保结果可信,我们多执行几次。看一下基准测试结果: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200912-Optimizing-String-Comparisons-in-Go/03.png) + +两个操作看起来耗时相同。我多跑了几次: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200912-Optimizing-String-Comparisons-in-Go/04.png) + +它们耗时是一样的。但是为什么呢? + +其中一个原因是,我们在每一次执行过程中都加入了 [Strings.ToLower](https://golang.org/pkg/strings/#ToLower)。这会影响整体的性能。字符串就是 rune 字符的集合,ToLower() 方法会遍历每个 rune 字符,把每个字符转换成小写,然后再进行比较。而这段额外的时间掩盖了测量中的两种方式的差别。 + +## EqualFold 介绍 + +我们上一篇文章中说过,EqualFold 是另一种用来对字符串进行大小写不敏感的比较操作的方法。我们认为 Equalfold 是三种方法中最快的。我们来看看基准测试结果是否与我们的结论吻合。 + +向 **compare.go** 添加下面的代码: + +```go +// EqualFold compare +func compareEF(a string, b string) bool { + if strings.EqualFold(sampleString, compareString) { + return true + } else { + return false + } +} +``` + +向 **compare_test.go** 文件添加下面的测试代码 + +```go +func BenchmarkEqualFold(b *testing.B) { + for n := 0; n < b.N; n++ { + compareEF("This is a string", "This is a strinG") + } +} +``` + +现在基于这个方法,我们运行基准测试: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200912-Optimizing-String-Comparisons-in-Go/05.png) + +哇!EqualFold 很明显地比另外两个快。我运行了几次,结果都一样。 + +它为什么会快呢?因为虽然 Equalfold 也会逐个字符进行解析,但是当它解析到两个字符串中不同的字符时,就会“提前下车”。 + +--- + +**结论:*对于大小写不敏感的比较,EqualFold(Strings 包)比较快*。** + +--- + +## 进行更深入的测试 + +我们现在了解了几个方法运行基准测试后的不同结果。现在再加入一些复杂性进行测试。 + +上篇文章中,我们用这个 [20 万行的列表](https://github.com/JeremyMorgan/Compare-Strings-Go/blob/master/names.txt)来进行比较。我们会修改代码中的方法,改成打开文件后进行字符串比较,直到找到想找的字符串。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200912-Optimizing-String-Comparisons-in-Go/06.png) + +这个文件中,我把想找的名字加到了*最后一行*,因此这个测试过程会在找到匹配的结果之前遍历前面的 199000 个单词。 + +修改下代码: + +**compare.go** + +```go +// operator compare +func compareOperators(a string) bool { + file, err := os.Open("names.txt") + result := false; + if err != nil { + log.Fatalf("failed opening file: %s", err) + } + scanner := bufio.NewScanner(file) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + if strings.ToLower(a) == strings.ToLower(scanner.Text()) { + result = true + }else { + result = false + } + } + file.Close() + return result +} +// strings compare +func compareString(a string) bool { + file, err := os.Open("names.txt") + result := false; + if err != nil { + log.Fatalf("failed opening file: %s", err) + } + scanner := bufio.NewScanner(file) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + if strings.Compare(strings.ToLower(a), strings.ToLower(scanner.Text())) == 0 { + result = true + }else { + result = false + } + } + file.Close() + return result +} +// EqualFold compare +func compareEF(a string) bool { + file, err := os.Open("names.txt") + result := false; + if err != nil { + log.Fatalf("failed opening file: %s", err) + } + scanner := bufio.NewScanner(file) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + if strings.EqualFold(a, scanner.Text()) { + result = true + }else { + result = false + } + } + file.Close() + return result +} +``` + +每个方法的逻辑都是: + +- 打开一个文件文件 +- 逐行解析 +- 查找字符 + +我们把测试方法改成只有一个入参: + +**compare_test.go** + +```go +func BenchmarkCompareOperators(b *testing.B) { + for n := 0; n < b.N; n++ { + compareOperators("Immanuel1234") + } +} +func BenchmarkCompareString(b *testing.B) { + for n := 0; n < b.N; n++ { + compareString("Immanuel1234") + } +} +func BenchmarkEqualFold(b *testing.B) { + for n := 0; n < b.N; n++ { + compareEF("Immanuel1234") + } +} +``` + +现在我们可以让测试运行的时间长一点,基准测试工具的重复用例也会少一点。下面是测试结果: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200912-Optimizing-String-Comparisons-in-Go/07.png) + +EqualFold 仍以相当大的优势领先。 + +添加了这个复杂性,有好处也有坏处。 + +**好处:**读取文本文件并进行序列化的测试更接近真实的生产环境 + +**好处:**我们可以用不同的字符串进行多种多样的测试 + +**坏处:**我们引入了多个因素(如读取文件),可能会影响最终结果的真实性 + +--- + +**结论:*对于大小写不敏感的比较,EqualFold(Strings 包)仍然比较快*。** + +--- + +## 等一下,还没完呢! + +我们还能让比较操作更快点吗?当然。我决定统计下字符串的字符个数。如果字符个数不一样,它们肯定不相等,我们就可以提前结束比较过程。 + +但是在字符串长度相同而字符不同时,我们仍然需要引入 EqualFold。后面加上的长度的检查使整个操作更繁琐,它会更快吗?我们来看看。 + +**compare.go** + +```go +func compareByCount(a string) bool { + file, err := os.Open("names.txt") + result := false; + if err != nil { + log.Fatalf("failed opening file: %s", err) + } + scanner := bufio.NewScanner(file) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + if len(a) == len(scanner.Text()) && strings.EqualFold(a, scanner.Text()){ + result = true + }else { + result = false + } + } + file.Close() + return result +} +``` + +**compare_test.go** + +```go +func BenchmarkCompareByCount(b *testing.B){ + for n := 0; n < b.N; n++ { + compareByCount("Immanuel1234") + } +} +``` + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20200912-Optimizing-String-Comparisons-in-Go/08.png) + +它确实更快!每个小小的改动都很重要。 + +--- + +**结论:*使用 EqualFold 时先进行字符数量对比,速度会更快*。** + +--- + +## 总结 + +本文中,我们研究了几种比较字符串的方法以及哪种方法比较快。概括一下:**对于大小写敏感的比较,使用基本的比较操作,对于大小写不敏感的比较,使用字符数量对比 + EqualFold**。 + +我喜欢做这类事,你会发现在做优化的过程中,一点一滴的小改变叠加起来后会有很大的影响。敬请期待本系列的其他文章。 + +[请让我们知道你的想法](https://twitter.com/JeremyCMorgan) + +--- +via: https://hackernoon.com/optimizing-string-comparisons-in-go-7h1b3udm + +作者:[jeremymorgan](https://hackernoon.com/u/jeremymorgan) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200923-Go-Goroutine-Leak-Detector.md b/published/tech/20200923-Go-Goroutine-Leak-Detector.md new file mode 100644 index 000000000..a5619e094 --- /dev/null +++ b/published/tech/20200923-Go-Goroutine-Leak-Detector.md @@ -0,0 +1,96 @@ +首发于:https://studygolang.com/articles/35248 + +# Go: Goroutine 泄漏检查器 + +![Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.](https://raw.githubusercontent.com/studygolang/gctt-images2/master/goroutine_leak_detector/header_img.png) + +具有监控存活的 goroutine 数量功能的 APM (Application Performance Monitoring)应用程序性能监控可以轻松查出 goroutine 泄漏。例如 NewRelic APM 中 goroutine 的监控。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/goroutine_leak_detector/goroutinemonitor.png) + +见:。 + +goroutine 泄漏会导致内存中存活的 goroutine 数量不断上升,直到服务宕机为止。因此,可以在代码部署之前,通过一些方法来检查程序中是否存在泄漏 + +## 泄漏检测 + +隶属于 Uber 公司的 Go 团队在 GitHub 开源了他们的[goroutine 泄漏检测器](https://github.com/uber-go/goleak) 出来,一个与单元测试结合使用的工具。 + +goleak 可以监控当前测试代码中泄漏的 goroutine。下面有一个 goroutine 泄漏的例子: + +```go +func leak() error { + go func() { + time.Sleep(time.Minute) + }() + + return nil +} +``` + +测试代码: + +```go +func TestLeakFunction(t *testing.T) { + defer goleak.VerifyNone(t) + + if err := leak(); err != nil { + t.Fatal("error not expected") + } +} +``` + +运行结果中展示了 goroutine 的泄漏情况: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/goroutine_leak_detector/testcode_1.png) + +从报错信息中我们可以提取出两个有用的信息: + +- 报错信息顶部为泄漏的 goroutine 的堆栈信息,以及 goroutine 的状态,可以帮我们快速调试并了解泄漏的 goroutine +- 之后为 goroutineID,在使用 trace 可视化的时候很有用,以下是通过 `go test -trace trace.out` 生成的用例截图: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/goroutine_leak_detector/trace_example.png) + +之后,我们就可以从这些 trace 中获取到 goroutine 的详细执行情况。 + +到此,我们已经检测到了泄漏的 goroutine,并且知道了它详细的运行情况。现在,我们需要通过学习这个库的运行原理来了解这种检测方法的局限性。 + +## 运行原理 + +启用泄漏检测的唯一要求就是在测试代码结束之前,调用 goleak 库来检测泄漏的 goroutine。事实上,goleak 检测了所有的 goroutine 而不是只检测泄漏的 goroutine + +goleak 运行结果中首先列出了所有存在的 goroutine,以下是运行结果的完成截图: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/goroutine_leak_detector/running_result.png) + +> goroutine 的堆栈信息由 golang 标准库中的 `runtime.Stack`,它可以被任何人取到。不过,[Goroutine 的 ID 是拿不到的](https://groups.google.com/forum/#!topic/golang-nuts/0HGyCOrhuuI) + +之后,goleak 解析所有的 goroutine 出并通过以下规则过滤 go 标准库中产生的 goroutine: + +- 由 go test 创建来运行测试逻辑的 goroutine。例如上图中的第二个 goroutine +- 由 runtime 创建的 goroutine,例如监听信号接收的 goroutine。想要了解更多相关信息,请参阅[Go: gsignal, Master of goroutine](https://medium.com/a-journey-with-go/go-gsignal-master-of-signals-329f7ff39391) +- 当前运行的 goroutine,例如上图的第一个 goroutine + +经过此次过滤后,如果没有剩余的 goroutine,则表示没有发生泄漏。但是 goleak 还是存在一下缺陷: + +- 三方库或者运行在后台中,遗漏的 goroutine 将会造成虚假的结果(无 goroutine 泄漏) +- 如果在其他未使用 goleak 的测试代码中使用了 goroutine,那么泄漏结果也是错误的。如果这个 goroutine 一直运行到下次使用 goleak 的代码, +则结果也会被这个 goroutine 影响,发生错误。 + +goleak 库虽然不是完美的,但是了解其局限性和缺陷,也可以尽量避免因为 goroutine 泄漏,而要调试在生产环境中的代码。 + +有意思的是,在 `net/http` 库中也使用了这个库来检测泄漏的 goroutine。下面是一些测试代码中的使用 demo: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/goroutine_leak_detector/test_demo.png) + +上图中的 `afterTest` 中可以添加 goleak 的调用逻辑以查看 goroutine 的信息,以发现可能会出现泄漏的 goroutine。 + +--- + +via: https://medium.com/a-journey-with-go/go-goroutine-leak-detector-61a949beb88 + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[CengSin](https://github.com/CengSin) +校对:[unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20200924-Functional-Options-Pattern-in-Go.md b/published/tech/20200924-Functional-Options-Pattern-in-Go.md new file mode 100644 index 000000000..3eafa4fde --- /dev/null +++ b/published/tech/20200924-Functional-Options-Pattern-in-Go.md @@ -0,0 +1,240 @@ +首发于:https://studygolang.com/articles/34446 + +# Go 函数选项模式 + +作为 Golang 开发者,遇到的许多问题之一就是尝试将函数的参数设置成可选项。这是一个十分常见的场景,您可以使用一些已经设置默认配置和开箱即用的对象,同时您也可以使用一些更为详细的配置。 + +对于许多编程语言来说,这很容易。在 C 语言家族中,您可以提供具有同一个函数但是不同参数的多个版本;在 PHP 之类的语言中,您可以为参数提供默认值,并在调用该方法时将其忽略。但是在 Golang 中,上述的做法都不可以使用。那么您如何创建具有一些其他配置的函数,用户可以根据他的需求(但是仅在需要时)指定一些额外的配置。 + +有很多的方法可以做到这一点,但是大多数方法都不是尽如人意,要么需要在服务端的代码中进行大量额外的检查和验证,要么通过传入他们不关心的其他参数来为客户端进行额外的工作。 + +下面我将会介绍一些不同的选项,然后为其说明为什么每个选项都不理想,接着我们会逐步构建自己的方式来作为最终的干净解决方案:函数选项模式。 + +让我们来看一个例子。比方说,这里有一个叫做 `StuffClient` 的服务,它能够胜任一些工作,同时还具有两个配置选项(超时和重试)。 + +```go +type StuffClient interface { + DoStuff() error +} + +type stuffClient struct { + conn Connection + timeout int + retries int +} +``` + +这是个私有的结构体,因此我们应该为它提供某种构造函数: + +```go +func NewStuffClient(conn Connection, timeout, retries int) StuffClient { + return &stuffClient{ + conn: conn, + timeout: timeout, + retries: retries, + } +} +``` + +嗯,但是现在我们每次调用 `NewStuffClient` 函数时都要提供 `timeout` 和 `retries`。因为在大多数情况下,我们只想使用默认值,我们无法使用不同参数数量带定义多个版本的 NewStuffClient ,否则我们会得到一个类似 `NewStuffClient redeclared in this block` 编译错误。 + +一个可选方案是创建另一个具有不同名称的构造函数,例如: + +```go +func NewStuffClient(conn Connection) StuffClient { + return &stuffClient{ + conn: conn, + timeout: DEFAULT_TIMEOUT, + retries: DEFAULT_RETRIES, + } +} +func NewStuffClientWithOptions(conn Connection, timeout, retries int) StuffClient { + return &stuffClient{ + conn: conn, + timeout: timeout, + retries: retries, + } +} +``` + +但是这么做的话有点蹩脚。我们可以做得更好,如果我们传入了一个配置对象呢: + +```go +type StuffClientOptions struct { + Retries int //number of times to retry the request before giving up + Timeout int //connection timeout in seconds +} +func NewStuffClient(conn Connection, options StuffClientOptions) StuffClient { + return &stuffClient{ + conn: conn, + timeout: options.Timeout, + retries: options.Retries, + } +} +``` + +但是,这也不是很好的做法。现在,我们总是需要创建 `StuffClientOption` 这个结构体,即使不想在指定任何选项时还要传递它。另外我们也没有自动填充默认值,除非我们在代码中的某处添加了一堆检查,或者也可以传入一个 `DefaultStuffClientOptions` 变量(不过这么做也不好,因为在修改某一处地方后可能会导致其他的问题。) + +所以,更好的解决方法是什么呢?解决这个难题最好的解决方法是使用函数选项模式,它利用了 Go 对闭包更加方便的支持。让我们保留上述定义的 `StuffClientOptions` ,不过我们仍需要为其添加一些内容。 + +```go +type StuffClientOption func(*StuffClientOptions) +type StuffClientOptions struct { + Retries int //number of times to retry the request before giving up + Timeout int //connection timeout in seconds +} +func WithRetries(r int) StuffClientOption { + return func(o *StuffClientOptions) { + o.Retries = r + } +} +func WithTimeout(t int) StuffClientOption { + return func(o *StuffClientOptions) { + o.Timeout = t + } +} +``` + +泥土般芬芳, 不是吗?这到底是怎么回事?基本上,我们有一个结构来定义 `StuffClient` 的可用选项。 另外,现状我们还定义了一个叫做 `StuffClientOption` 的东西(次数是单数),它只是接受我们选项的结构体作为参数的函数。我们还定义了另外两个函数 `WithRetries` 和 `WithTimeout` ,它们返回一个闭包,现在就是见证奇迹的时刻了! + +```go +var defaultStuffClientOptions = StuffClientOptions{ + Retries: 3, + Timeout: 2, +} +func NewStuffClient(conn Connection, opts ...StuffClientOption) StuffClient { + options := defaultStuffClientOptions + for _, o := range opts { + o(&options) + } + return &stuffClient{ + conn: conn, + timeout: options.Timeout, + retries: options.Retries, + } +} +``` + +现在,我们定义了一个额外和包含默认选项的没有导出的变量,同时我们已经调整了构造函数,用来接收[可变参数](https://gobyexample.com/variadic-functions)。然后, 我们遍历 `StuffClientOption` 列表(单数),针对每一个列表,将列表中返回的闭包使用在我们的 `options` 变量(需要记住,这些闭包接收一个 `StuffClientOptions` 变量,仅需要在选项的值上做出少许修改)。 + +现在我们要做的事情就是使用它! + +```go +x := NewStuffClient(Connection{}) +fmt.Println(x) // prints &{{} 2 3} +x = NewStuffClient( + Connection{}, + WithRetries(1), +) +fmt.Println(x) // prints &{{} 2 1} +x = NewStuffClient( + Connection{}, + WithRetries(1), + WithTimeout(1), +) +fmt.Println(x) // prints &{{} 1 1} +``` + +这看起来相当不错,已经可以使用了!而且,它的好处是,我们只需要对代码进行很少的修改,就可以随时随地添加新的选项。 + +把这些修改放在一起,就是这样: + +```go +var defaultStuffClientOptions = StuffClientOptions{ + Retries: 3, + Timeout: 2, +} +type StuffClientOption func(*StuffClientOptions) +type StuffClientOptions struct { + Retries int //number of times to retry the request before giving up + Timeout int //connection timeout in seconds +} +func WithRetries(r int) StuffClientOption { + return func(o *StuffClientOptions) { + o.Retries = r + } +} +func WithTimeout(t int) StuffClientOption { + return func(o *StuffClientOptions) { + o.Timeout = t + } +} +type StuffClient interface { + DoStuff() error +} +type stuffClient struct { + conn Connection + timeout int + retries int +} +type Connection struct {} +func NewStuffClient(conn Connection, opts ...StuffClientOption) StuffClient { + options := defaultStuffClientOptions + for _, o := range opts { + o(&options) + } + return &stuffClient{ + conn: conn, + timeout: options.Timeout, + retries: options.Retries, + } +} +func (c stuffClient) DoStuff() error { + return nil +} +``` + +如果你想自己尝试一下,请在 [Go Playground](https://play.golang.org/p/VcWqWcAEyz) 上查找。 + +但这也可以通过删除 `StuffClientOptions` 结构体进一步简化,并将选项直接应用在我们的 `StuffClient` 上。 + +```go +var defaultStuffClient = stuffClient{ + retries: 3, + timeout: 2, +} +type StuffClientOption func(*stuffClient) +func WithRetries(r int) StuffClientOption { + return func(o *stuffClient) { + o.retries = r + } +} +func WithTimeout(t int) StuffClientOption { + return func(o *stuffClient) { + o.timeout = t + } +} +type StuffClient interface { + DoStuff() error +} +type stuffClient struct { + conn Connection + timeout int + retries int +} +type Connection struct{} +func NewStuffClient(conn Connection, opts ...StuffClientOption) StuffClient { + client := defaultStuffClient + for _, o := range opts { + o(&client) + } + client.conn = conn + return client +} +func (c stuffClient) DoStuff() error { + return nil +} +``` + +从[这里](https://play.golang.org/p/Z5P5Om4KDL)就能够开始尝试。在我们的示例中,我们只是将配置直接应用于结构体中,如果中间有一个额外的结构体是没有意义的。但是,请注意,在许多情况下,您可能仍然想使用上一个示例中的 `config` 结构。例如,如果您的构造函数正在使用 `config` 选项执行某些操作时,但是并没有将它们存储到结构体中,或者被传递到其他地方,配置结构的变体是更通用的实现。 + +感谢 [Rob Pike](https://commandcenter.blogspot.de/2014/01/self-referential-functions-and-design.html) 和 [Dave Cheney](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis) 推广这种设计模式。 + +--- +via: https://halls-of-valhalla.org/beta/articles/functional-options-pattern-in-go,54/ + +作者:[ynori7](https://halls-of-valhalla.org/beta/user/ynori7) +译者:[sunlingbot](https://github.com/sunlingbot) +校对:[unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20201002-Go-How-Does-a-Program-Recover.md b/published/tech/20201002-Go-How-Does-a-Program-Recover.md new file mode 100644 index 000000000..a48b25ace --- /dev/null +++ b/published/tech/20201002-Go-How-Does-a-Program-Recover.md @@ -0,0 +1,63 @@ +首发于:https://studygolang.com/articles/35249 + +# Go:程序如何恢复(recover)? + +![由 Renee French 创作的原始 Go Gopher 作品,为“ Go 的旅程”创建插图。](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20201002-Go-How-Does-a-Program-Recover/1_4zRau44piN5HjUnTnJsMOw.png) + +当程序无法适当处理错误时,比如无效的内存访问,Go 中的 panic 就会被触发。如果错误是意料之外,且没有其他方式处理该错误时,同样可以由开发者触发 panic。了解 recover 或者终止的过程,可以更好地理解一个会发生 panic 的程序的后果。 + +## 多帧的情况 + +关于 panic 以及它 recover 函数的经典例子已经有着充分的说明,该例子收录在 Go blog 文章“[Defer, Panic, and Recover](https://blog.golang.org/defer-panic-and-recover)” 中。让我们关注下其他例子,当一个 panic 涉及多个 defer 函数的帧(frame)。这里是一个例子: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20201002-Go-How-Does-a-Program-Recover/a-panic-involves-multiple-frames-of-deferred-functions.png) + +该程序由三个链式调用的函数组成。一旦这段代码到了最后层级产生 panic 的地方,Go 会构建 defer 函数的第一个帧并运行它: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20201002-Go-How-Does-a-Program-Recover/build-the-first-frame-of-deferred-functions.png) + +这个帧里面的代码没有 recover 这个 panic。之后,Go 构建父帧(译者注:level1 函数的帧),并在该帧中调用其中的每个延迟函数: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20201002-Go-How-Does-a-Program-Recover/builds-the-parent-frame-and-calls-each-deferred-function.png) + +*提醒一下,defer 函数 按照 LIFO(后进先出)的顺序执行。想要了解更多关于 defer 函数内部管理的方式,建议阅读我的文章“[Go: How Does defer Statement Work?](https://medium.com/a-journey-with-go/go-how-does-defer-statement-work-1a9492689b6e)”* + +由于一个函数 recover 了 panic,Go 需要一种跟踪,并恢复这个程序的方法。为了达到这个目的,每一个 Goroutine 嵌入了一个特殊的属性,指向一个代表该 panic 的对象: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20201002-Go-How-Does-a-Program-Recover/special-attribute.png) + +当 panic 发生的时候,该对象会在运行 defer 函数前被创建。然后,recover 这个 panic 的函数仅仅返回这个对象的信息,同时将这个 panic 标记为已恢复(recovered): + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20201002-Go-How-Does-a-Program-Recover/returns-the-information-of-that-object.png) + +一旦 panic 被认为已经恢复,Go 需要恢复当前的工作。但是,由于运行时处于 defer 函数的帧中,它不知道恢复到哪里。出于这个原因,当 panic 标记已恢复的时候,Go 保存当前的程序计数器和当前帧的堆栈指针,以便 panic 发生后恢复该函数: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20201002-Go-How-Does-a-Program-Recover/Go-saves-the-current-program-counter-and-stack-pointer.png) + +我们也可以使用 `objdump` 查看 程序计数器的指向(e.g. `objdump -D my-binary` | `grep 105acef`): + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20201002-Go-How-Does-a-Program-Recover/objdump.png) + +该指令指向函数调用 `runtime.deferreturn`,这个指令被编译器插入到每个函数的末尾,而它运行 defer 函数。在前面的例子中,这些 defer 函数中的大多数已经运行了——直到恢复,因此,只有剩下的那些会在调用者返回前运行。 + +## WaitGroup + +理解这个工作流程会让我们了解 defer 函数的重要性以及它如何起作用,比如,在处理若干个 Goroutine 的时候,在一个 defer 函数中延迟调用 `WaitGroup` 可以避免死锁。这是一个例子: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20201002-Go-How-Does-a-Program-Recover/WaitGroup.png) + +这个程序由于 `wg.Done` 无法被调用而导致死锁。将它移动到一个 defer 函数中会确保执行并且能让这个程序继续运行。 + +## Goexit + +有趣的是,函数 `runtime.Goexit` 使用完全相同的工作流程。`runtime.Goexit` 实际上创造了一个 panic 对象,且有着一个特殊标记来让它与真正的 panic 区别开来。这个标记让运行时可以跳过恢复以及适当的退出,而不是直接停止程序的运行。 + +--- + +via: https://medium.com/a-journey-with-go/go-how-does-a-program-recover-fbbbf27cc31e + +作者:[Vincent Blanchon](https://medium.com/@blanchon.vincent) +译者:[dust347](https://github.com/dust347) +校对:[lxbwolf](https://github.com/lxbwolf) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20201030-How-to-compile-code-in-the-browser-with-WebAssembly.md b/published/tech/20201030-How-to-compile-code-in-the-browser-with-WebAssembly.md new file mode 100644 index 000000000..c18d61b41 --- /dev/null +++ b/published/tech/20201030-How-to-compile-code-in-the-browser-with-WebAssembly.md @@ -0,0 +1,107 @@ +首发于:https://studygolang.com/articles/35261 + +# 如何使用 WebAssembly 在浏览器中编译代码 + +浏览器的功能日益强大,从最早在 [CERN](https://home.cern/science/computing/birth-web) 上分享文章,到今天运行 [Google Earth](https://earth.google.com/web) ,玩 [Unity 3D](https://blogs.unity3d.com/2018/08/15/webassembly-is-here/) 游戏,甚至用 [AutoCAD](https://www.autodesk.com/products/autocad-web-app/overview) 设计建筑。 + +既然浏览器已然具有如此强大的功能,那么它能不能编译运行代码呢?可笑。当然不可能... + +但是转念一想,为什么不呢?我不可能忽视这么一个令人兴奋的挑战的。在为期四个月的敲键盘和研读文档之后,我终于给出了自己的答案: [Go Wasm](https://go-wasm.johnstarich.com/)。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20201030-How-to-compile-code-in-the-browser-with-WebAssembly/1.gif) + +Go Wasm 运用 [WebAssembly](https://webassembly.org/) 提供了一个完全在浏览器中书写和运行代码的 [Go](https://golang.org/) 开发环境。它完全开源。 Go Wasm 由三个 WebAssembly 组件组成: “操作系统”、编辑器和 shell。 + +本文接下来会从 Go Wasm 是什么,怎么运行的,以及未来发展三个方面展开介绍。 + +## 用 Go Wasm 写代码 + +Go Wasm 让你能用 Go 编译器写 Go 代码,运行 Go 代码。换句话说:我们写好代码,控制台输入 Go build ,然后就可以运行了。这跟我们熟悉的 [Go Playground](https://play.golang.org/) 很不一样。在 Go Wasm 中,代码实际上是在浏览器中运行的,即便你断开网络。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20201030-How-to-compile-code-in-the-browser-with-WebAssembly/2.gif) + +当你打开 [样例网站](https://go-wasm.johnstarich.com/) ,Go Wasm 首先启动操作系统,然后在虚拟文件系统中安装 Go ,然后打开编辑器,最后启动终端和编译控制等工具。在接下来的章节,我会对其中的三个关键程序详细介绍。首先,来快速的看一下 IDE 。 + +在编辑器中,你可以进行多面板编辑,引入外部库,重排格式,运行程序等操作。 + +想要运行传统 Go CLI 命令,你只需打开一个终端。程序的输入输出可以通过文件重定向操作,例如 ```./playground > output.txt``` 或者通过 ```|``` 管道连接输入与输出。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20201030-How-to-compile-code-in-the-browser-with-WebAssembly/3.png) + +你也可以在虚拟文件系统中的任意位置生成、安装 Wasm 程序。 在上面的截屏中, ```count-lines``` 程序被编译并保存在了 ```/bin``` 目录中。在这个目录,它可以像其他内建命令一样被运行。 + +## 原理 + +Go Wasm 由三个 WebAssembly 组件组成: “操作系统”、编辑器和 shell 。这三个组件在浏览器中被层叠组装。在浏览器中,操作系统扮演解释器的功能,为访问虚拟文件和进程提供服务。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20201030-How-to-compile-code-in-the-browser-with-WebAssembly/4.png) + +在上层,操作系统拦截 Go 程序和浏览器之间的系统调用,以提供虚拟文件系统和进程的服务。因为浏览器没有任何文件和进程的概念,操作系统对运行 Go 程序尤其重要。这里的“操作系统”并不是真正的操作系统,但是它提供了浏览器所不能提供的重要的对操作系统概念的抽象。 + +我们还需要 Go 编译器和标准库来写程序。 Go Wasm 启动时,会自动下载并安装 Go 到虚拟文件系统。 + +编辑器和 shell 程序建立在操作系统上,就像由编辑器编译的程序。编辑器提供文件管理面板以及用于写代码、运行代码的终端。每个终端面板启动一个 shell 用于运行传统 Go 命令或者你自己的程序,就像一个真正的终端一样。 + +## “操作系统” + +操作系统为用户程序在各种不同硬件上运行的环境。操作系统为用户程序调用系统资源提供接口,但不允许用户程序直接访问系统资源。如果一个用户程序要打开一个文件并写入了一些数据,操作系统会返回一个文件句柄,并忠实地将用户程序写入的数据写到硬盘上。这些重要的操作通常被称为系统调用,即 "syscalls"。 + +类似地, Go Wasm 的操作系统对 Go 的系统调用采用的方式是拦截系统调用,操作虚拟资源而非实际资源的方式。Wasm 上 Go 程序执行系统调用依赖 JavaScript 的全局函数, Wasm 操作系统的组件将这些系统调用替换成普通代码。浏览器并不真的提供文件和进程服务,这些系统调用函数被替换成虚拟版本,并返回给 Go 程序。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20201030-How-to-compile-code-in-the-browser-with-WebAssembly/5.png) +
打开文件过程的时序图
+ +浏览器中的进程是一个有趣的问题。熟练的网络程序开发者都知道,并没有所谓的 "JavaScript process" 的概念,也绝对不可能在同一时间运行两个程序。为了解决这个限制,我通过上下文切换的方式在进程间来回切换,使得这些进程看起来像运行在一个处理器核心上。上下文切换使得操作系统能够在新进程取得优先级的时候对进程相关的文件表和环境变量进行换出 ( swap out ) 操作。这个操作较为复杂,但是剩下的工作就可以完全交给 JavaScript 的内建任务调度器。 + +## 虚拟文件系统 + +先将虚拟文件系统放在一边,因为想要把它弄对非常困难。一个现代文件系统 ( FS ) 包含许多的边界情况和特点。文件许可,原生管道,文件锁等都需要正常工作以满足 ```go build``` 的要求,以使程序运行不会碰到致命错误。呃。 + +为节省时间,我没有自己从零开始写一个基于内存的文件系统,选用了现成的 [Afero](https://github.com/spf13/afero) 。Afero 提供了一个很好的起点,它定义了一个非常强大的文件系统的抽象。基于此,我建立了几个文件系统:一个可挂载的文件系统,一个流式 gzip 文件系统和一个实验性的 IndexedDB 文件系统。不幸的是,即使有了这么强的基础, bug 仍然层出不穷。 + +我发现在底层的操作系统不是真实的时候 Go CLI 的表现不好。令人吃惊,是的。 + +我花了一个多月来解决每个文件系统的 bug 。跟踪 bug 尤其困难,因为多数的 bug 会使操作系统崩溃,并输出难以看懂的错误信息。我希望浏览器的开发者和 Wasm 社区能在 debug 的体验上花更多时间,因为这真是一个痛点。 + +## 编辑器 + +编辑器扮演了一个更加传统的网络应用的角色。它在网页上搭建了代码编辑器面板、控制窗口、 build 控制台和终端面板。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20201030-How-to-compile-code-in-the-browser-with-WebAssembly/6.png) + +编辑器程序在操作系统的上层运行,就像其他 Go 程序一样启动新的进程。今天,编辑器运行 [CodeMirror](https://codemirror.net/) 并将其中的线上文件与我们的文件系统保持同步。 + +终端启动 shell 进程。为了提供人们更加熟悉的终端体验,终端将输入、输出与 [xterm.js](https://xtermjs.org/) 连接。 + +## Shell + +每个终端面板的 shell 提供了一个在文件系统上运行 Go 命令和其他 Wasm 程序的接口。现在市面上有一些现成的用 Go 写的 shell。 但是我试用的一些并不能与 Wasm 兼容。幸运的是,写一个 shell 是一个很有趣的学习进程的方式。为了写 shell ,我不得不学习了所有的终端逃逸码,来支持命令行输入编辑。后来我又对进程文件重定向和环境变量产生了兴趣。 + +好了!我非常喜欢在 Go Wasm 中加入新库来实验我的疯狂的想法。我迫不及待的想看看小伙伴门会用它来做什么了。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20201030-How-to-compile-code-in-the-browser-with-WebAssembly/7.gif) + +## 未来发展 + +现今的 WebAssembly 不可否认地有各种优点和缺点,但它也同样有着巨大的潜力等着被实现。 + +关于优点,我们的社区一直很忙。人们不停地提出具有前景的提议,提出各种标准以尝试突破极限。特别地,我一直在关注 [Wasm Threads](https://github.com/WebAssembly/threads) 和由其衍生的 [WASI](https://wasi.dev/) 标准,这会成为下一个里程碑。 + +不幸的是,仍然有很多不足。我最感到遗憾的问题之一就是缺少 [调试支持](https://github.com/WebAssembly/debugging) 。浏览器自带的调试器只能支持步进汇编码——不是源码。由于我对 Wasm 的文本格式不熟悉,对我源码中的问题进行诊断和修复的工作非常令人沮丧。 + +对于 Go Wasm ,未来是非常令人兴奋的。如果社区感兴趣的话,我非常乐于加入如移动设备支持(低内存消耗),会话之间的文件存取,或者 WASI 程序的原生运行。如果你想参与进来,在 [GitHub](https://github.com/johnstarich/go-wasm) 上吼一声。 + +
. . .
+ +Go Wasm 将 WebAssembly 的边界向外扩展了一点。尽管现在 Wasm 还处在早期,但是其发展势头迅猛。我等不及想看看接下来会发生的事了。 + +你会创造什么呢? + +--- +via: https://johnstarich.medium.com/how-to-compile-code-in-the-browser-with-webassembly-b59ffd452c2b + +作者:[hongchi](http://www.hongchiworld.cn/) +译者:[hhh811](https://github.com/hhh811) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20210219-Writing-A-Simple-TCP-Server-Using-Kqueue.md b/published/tech/20210219-Writing-A-Simple-TCP-Server-Using-Kqueue.md new file mode 100644 index 000000000..b7d25fb12 --- /dev/null +++ b/published/tech/20210219-Writing-A-Simple-TCP-Server-Using-Kqueue.md @@ -0,0 +1,326 @@ +首发于:https://studygolang.com/articles/35251 + +# Go:用 kqueue 实现一个简单的 TCP Server + +## 介绍 + +在 [非阻塞 I/O 超简明介绍](https://dev.to/frosnerd/explain-non-blocking-i-o-like-i-m-five-2a5f) 中,我们已经讨论过现代 Web 服务器可以处理大量并发请求,这得益于现代操作系统内核内置的事件通知机制。受 Linux epoll [ [文档](https://man7.org/linux/man-pages/man7/epoll.7.html) ]启发,FreeBSD 发明了 kqueue [ [论文](https://people.freebsd.org/~jlemon/papers/kqueue.pdf),[文档](https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2) ] + +这篇文章我们将仔细研究下 kqueue,我们会用 Go 实现一个基于 kqueue event loop 的 TCP server, 你可以在 Github 上找到 [源代码](https://github.com/FRosner/FrSrv)。要运行代码必须使用和 FreeBSD 兼容的操作系统,比如 macOS。 + +注意 kqueue 不仅能处理 socket event,而且还能处理文件描述符 event、信号、异步 I/O event、子进程状态改变 event、定时器以及用户自定义 event。它确实通用和强大。 + +我们这篇文章主要分为以下几部分讲解。首先,我们会先从理论出发设计我们的 TCP Server。然后,我们会去实现它的必要的模块。最后我们会对整个过程进行总结以及思考。 + +## 设计 + +我们 TCP Server 大概有以下几部分:一个监听 TCP 的 socket、接收客户端连接的 socket、一个内核事件队列(kqueue),还有一个事件循环机制来轮询这个队列。下面这个图描述了接收连接的场景。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210219-Writing-A-Simple-TCP-Server-Using-Kqueue/accepting-incoming-connections.png) + +当客户端想要连接服务端,一个连接请求将会被放到 TCP 连接队列中,而内核会将一个新的事件放到 kqueue 中。这个事件将会在事件循环时被处理,事件循环会接受请求,并创建一个新的客户端连接。下面这个图描述了新创建的 socket 如何从客户端读取请求。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210219-Writing-A-Simple-TCP-Server-Using-Kqueue/read-data-from-the-client.png) + +客户端写数据到新创建的 socket,内核会将一个新 event 放到 kqueue 中,表示在这个 socket 中有等待读取的数据。事件循环将轮询到这个事件,并从 socket 读取数据。注意只有一个 socket 监听连接,而我们将为每一个客户端连接创建新的 socket。 + +下文要讨论实现细节,可以大概按照下面的步骤实现我们的设计。 + +1. 创建,绑定以及监听新的 socket +2. 创建 kqueue +3. 订阅 socket event +4. 循环队列获取 event 并处理它们 + +## 实现 + +为了避免单个文件有大量系统调用,我们拆分成几个不同模块: + +* 一个 `socket` 模块来处理所有管理 socket 的相关功能; +* 一个 `kqueue` 模块来处理事件循环; +* 最后 `main` 模块用来整合所有模块并启动我们的 TCP server。 + +我们下面从 `socket` 模块开始。 + +### 定义 Socket + +首先,让我们创建一个 socket 结构体。类 Unix 操作系统,比如 FreeBSD,会把 socket 作为文件。为了用 Go 实现 socket,我们需要了解 [文件描述符](https://www.freebsd.org/cgi/man.cgi?query=fd&sektion=4&manpath=freebsd-release-ports)。所以我们可以创建一个类似下面带有文件描述符的结构体。 + +```go +type Socket struct { + FileDescriptor int +} +``` + +我们期望我们的 socket 可以应对不同的场景,比如:读、写 socket 数据,以及关闭 socket。在 Go 中,要支持这些操作,需要实现通用的 interface,比如 `io.Reader`,`io.Writer`,还有 `io.Closer`。 + +首先,实现 `io.Reader` 这个接口,他会调用 [read()](https://www.freebsd.org/cgi/man.cgi?query=read&sektion=2) 系统函数。这个函数会返回读到字节的数量,以及进行读操作时可能发生的错误。 + +```go +func (socket Socket) Read(bytes []byte) (int, error) { + if len(bytes) == 0 { + return 0, nil + } + numBytesRead, err := syscall.Read(socket.FileDescriptor, bytes) + if err != nil { + numBytesRead = 0 + } + + return numBytesRead, err +} +``` + +类似的,我们通过调用 [write()](https://www.freebsd.org/cgi/man.cgi?query=write&sektion=2) 来实现 `io.Writer` 接口。 + +```go +func (socket Socket) Write(bytes []byte) (int, error) { + numBytesWritten, err := syscall.Write(socket.FileDescriptor, bytes) + if err != nil { + numBytesWritten = 0 + } + return numBytesWritten, err +} +``` + +最后关闭 socket 可以调用 [close()](https://www.freebsd.org/cgi/man.cgi?query=close&apropos=0&sektion=2),并传入 socket 对应的文件描述符。 + +```go +func (socket *Socket) Close() error { + return syscall.Close(socket.FileDescriptor) +} +``` + +为了稍后能打印一些有用的错误和日志,我们也需要实现 `fmt.Stringer` 接口。我们通过不同的文件描述符来区分不同的 socket。 + +```go +func (socket *Socket) String() string { + return strconv.Itoa(socket.FileDescriptor) +} +``` + +### 监听一个 Socket + +定义好 Socket 之后,我们需要初始化它,并让它一个监听特定 IP 和 端口的。监听一个 socket 也可以通过一些系统函数来实现。现在先整体看一下我们实现的 `Listen()` 方法,然后再一步步进行分析。 + +```go +func Listen(ip string, port int) (*Socket, error) { + socket := &Socket{} + + socketFileDescriptor, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0) + if err != nil { + return nil, fmt.Errorf("failed to create socket (%v)", err) + } + + socket.FileDescriptor = socketFileDescriptor + + socketAddress := &syscall.SockaddrInet4{Port: port} + copy(socketAddress.Addr[:], net.ParseIP(ip)) + + if err = syscall.Bind(socket.FileDescriptor, socketAddress); err != nil { + return nil, fmt.Errorf("failed to bind socket (%v)", err) + } + + if err = syscall.Listen(socket.FileDescriptor, syscall.SOMAXCONN); err != nil { + return nil, fmt.Errorf("failed to listen on socket (%v)", err) + } + + return socket, nil +} +``` + +首先调用了 [socket()](https://www.freebsd.org/cgi/man.cgi?query=socket&apropos=0&sektion=2) 函数,这将会创建通信的接入点,并返回描述符编号。 + +它需要 3 个参数: + +* 地址类型:我们用的是 `AF_INET` (IPv4)。 +* socket 类型:我们用 `SOCKET_STREAM`,代表基于字节流连续、可靠的双向连接。 +* 协议类型:`0` 在 `SOCKET_STREAM` 类型下代表的是 TCP。 + +然后,我们调用了 [bind()](https://www.freebsd.org/cgi/man.cgi?query=bind&apropos=0&sektion=2) 方法来指定新创建 socket 的协议地址。`bind()` 方法的第一个参数是文件描述符,第二个参数是包含地址信息的结构体指针。我们在这里使用了 Go 预定义的 `SockaddrInet4` 结构体,并指定要绑定的 IP 地址和端口。 + +最后,我们调用了 [listen()](https://www.freebsd.org/cgi/man.cgi?query=listen&apropos=0&sektion=2) 方法,这样我们就能等待接收连接了。它的第二个参数是连接请求队列的最大长度。我们使用了内核参数 `SOMAXCONN` ,在我的 Mac 上默认是 128。你可以通过执行 `sysctl kern.ipc.somaxconn` 来获取这个值。 + +### 定义事件循环 + +同样的,我们将定义一个结构体来表示 kqueue 的事件循环。我们必须要保存 kqueue 的文件描述符以及 socket 的文件描述符,我们当然也能将我们前面定义的 socket 对象作为指针来替代 `SocketFileDescriptor`。 + +```go +type EventLoop struct { + KqueueFileDescriptor int + SocketFileDescriptor int +} +``` + +接下来,我们需要一个函数根据我们提供的 socket 创建一个事件循环。和之前一样,我们需要用一系列系统函数去创建 Kqueue。我们还是先看下整个函数,然后再一步步拆解来看。 + +```go +func NewEventLoop(s *socket.Socket) (*EventLoop, error) { + kQueue, err := syscall.Kqueue() + if err != nil { + return nil, + fmt.Errorf("failed to create kqueue file descriptor (%v)", err) + } + + changeEvent := syscall.Kevent_t{ + Ident: uint64(s.FileDescriptor), + Filter: syscall.EVFILT_READ, + Flags: syscall.EV_ADD | syscall.EV_ENABLE, + Fflags: 0, + Data: 0, + Udata: nil, + } + + changeEventRegistered, err := syscall.Kevent( + kQueue, + []syscall.Kevent_t{changeEvent}, + nil, + nil + ) + if err != nil || changeEventRegistered == -1 { + return nil, + fmt.Errorf("failed to register change event (%v)", err) + } + + return &EventLoop{ + KqueueFileDescriptor: kQueue, + SocketFileDescriptor: s.FileDescriptor + }, nil +} +``` + +第一个系统函数 [kqueue()](https://www.freebsd.org/cgi/man.cgi?query=kqueue&apropos=0&sektion=0&format=html) 创建了一个新的内核事件队列,并且返回了它的文件描述符。我们等会调用 [`kevent()`](https://www.freebsd.org/cgi/man.cgi?query=kqueue&apropos=0&sektion=0&format=html) 的时候会用到这个队列。`kevent()` 有两个功能,订阅新事件和轮询队列。 + +我们的例子是要订阅传入连接的事件,可以通过传递 `kevent` 结构体(在 Go 中,用 `Kevent_t` 表示)给 `kevent()` 这个系统函数来实现订阅。`Kevent_t` 需要包含以下信息: + +* `Ident` 的文件描述符:值是我们 socket 的文件描述符。 +* 处理事件的 `Filter`:设置为 `EVFILT_READ`,当和监听 socket 一起用时,它代表我们只关心传入连接的事件。 +* 代表对这个事件要执行操作的 `Flag`:在我们例子中,我们想要添加(EV_ADD)事件到 `kqueue`,比如说订阅事件,同时要启用(EV_ENABLE)它。Flag 可以使用 `或` 这个位操作进行结合。 + +其他的几个参数我们就不需要了,创建好这个事件之后,要把它用一个数组包裹,并传递给 `kevent()` 这个系统函数。 + +最后,我们返回这个等待被轮询的事件循环。接下来让我们实现轮询的函数。 + +### 事件循环轮询 + +事件循环是一个简单的 for 循环,可以轮询新的内核事件并进行处理。之前使用系统函数 `kevent()` 时,订阅轮询就已经完成了,但是现在我们又传递一个空的事件数组给它,目的是当有新的事件时,新的事件会填充到这个数组。 + +然后我们就可以一个个循环这些事件并处理它们了。新的客户端连接会被转换成客户端 socket,所以我们可以从客户端读取或写入数据。现在让我们看下代码如何循环不同的事件类型。 + +```go +func (eventLoop *EventLoop) Handle(handler Handler) { + for { + newEvents := make([]syscall.Kevent_t, 10) + numNewEvents, err := syscall.Kevent( + eventLoop.KqueueFileDescriptor, + nil, + newEvents, + nil + ) + if err != nil { + continue + } + + for i := 0; i < numNewEvents; i++ { + currentEvent := newEvents[i] + eventFileDescriptor := int(currentEvent.Ident) + + if currentEvent.Flags&syscall.EV_EOF != 0 { + // client closing connection + syscall.Close(eventFileDescriptor) + } else if eventFileDescriptor == eventLoop.SocketFileDescriptor { + // new incoming connection + socketConnection, _, err := syscall.Accept(eventFileDescriptor) + if err != nil { + continue + } + + socketEvent := syscall.Kevent_t{ + Ident: uint64(socketConnection), + Filter: syscall.EVFILT_READ, + Flags: syscall.EV_ADD, + Fflags: 0, + Data: 0, + Udata: nil, + } + socketEventRegistered, err := syscall.Kevent( + eventLoop.KqueueFileDescriptor, + []syscall.Kevent_t{socketEvent}, + nil, + nil + ) + if err != nil || socketEventRegistered == -1 { + continue + } + } else if currentEvent.Filter&syscall.EVFILT_READ != 0 { + // data available -> forward to handler + handler(&socket.Socket{ + FileDescriptor: int(eventFileDescriptor) + }) + } + + // ignore all other events + } + } +} +``` + +第一种情况,我们要处理 `EV_EOF` 事件,代表客户端想要关闭它的连接的事件。这种情况我们简单的关闭了对应 socket 的文件描述符。 + +第二种情况代表我们的监听 socket 有连接请求。我们可以使用系统函数 [accept()](https://www.freebsd.org/cgi/man.cgi?query=accept) 从 TCP 连接请求队列中获取连接请求,它会为监听 socket 创建一个新的客户端 socket 和新的文件描述符。我们为这个新创建的 socket 订阅一个新的 `EVFILT_READ` 事件。在新创建的客户端 socket 中,无论什么时候有可以读取的数据,就会有 `EVFILT_READ` 事件发生。 + +第三种情况就是处理刚提到的 `EVFILT_READ` 事件,这些事件有客户端 socket 的文件描述符,我们将其封装在 `Socket` 对象中并传递给要处理它的方法。 + +要注意我们省略一些错误然后使用了简单的 continue 继续执行循环。现在事件循环函数也写好了,让我们将所有的逻辑封装在 main 函数中并执行。 + +### main 函数 + +因为之前已经定义好了 `socket` 和 `kqueue` 模块,我们现在可以非常容易地实现服务器。我们首先创建一个监听特定 IP 地址和端口的 socket,然后基于它创建一个新的事件循环,最后我们定义处理输出的函数,来开启我们的事件循环。 + +```go +func main() { + s, err := socket.Listen("127.0.0.1", 8080) + if err != nil { + log.Println("Failed to create Socket:", err) + os.Exit(1) + } + + eventLoop, err := kqueue.NewEventLoop(s) + if err != nil { + log.Println("Failed to create event loop:", err) + os.Exit(1) + } + + log.Println("Server started. Waiting for incoming connections. ^C to exit.") + + eventLoop.Handle(func(s *socket.Socket) { + reader := bufio.NewReader(s) + for { + line, err := reader.ReadString('\n') + if err != nil || strings.TrimSpace(line) == "" { + break + } + s.Write([]byte(line)) + } + s.Close() + }) +} +``` + +处理函数会根据换行符逐行读取数据内容,直到它读取到空行,然后会关闭连接。我们可以通过 `curl` 来测试,`curl` 将会发送一个 GET 请求,并输出响应的内容,响应内容其实就是它发送的 GET 请求体的内容。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210219-Writing-A-Simple-TCP-Server-Using-Kqueue/demo.gif) + +## 思考 + +我们成功用 `kqueue` 实现了一个简单的 TCP Server,当然,这个代码想用于生产环境还需要做很多工作。 我们使用单进程和阻塞 socket 运行,另外,也没有去处理错误。其实大多数情况下,使用已经存在的库而不是自己调用操作系统内核函数会更好。 + +没想到用内核操作事件这么难,API 非常复杂,而且必须要读好多文档去找需要怎么做。然而,这是一个惊人的学习体验。 + +--- + +via: + +作者:[Frank Rosner](https://dev.to/frosnerd) +译者:[h1z3y3](https://github.com/h1z3y3) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20210220-Life-of-an-HTTP-request-in-a-Go-server.md b/published/tech/20210220-Life-of-an-HTTP-request-in-a-Go-server.md new file mode 100644 index 000000000..0071762be --- /dev/null +++ b/published/tech/20210220-Life-of-an-HTTP-request-in-a-Go-server.md @@ -0,0 +1,326 @@ +首发于:https://studygolang.com/articles/35252 + +# Go 服务中 HTTP 请求的生命周期 + +Go 语言对于编写 HTTP 服务来说是一个常见且非常合适的工具。这篇博文通过一个 Go 服务来探讨一个典型 HTTP 请求的路由,涉及路由,中间件以及比如并发之类的相关问题。 + +为了有具体的代码可以参考,让我们先从这段简单的服务代码开始(来自于 [https://gobyexample.com/http-servers](https://gobyexample.com/http-servers)) + +```go +package main + +import ( + "fmt" + "net/http" +) + +func hello(w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, "hello\n") +} + +func headers(w http.ResponseWriter, req *http.Request) { + for name, headers := range req.Header { + for _, h := range headers { + fmt.Fprintf(w, "%v: %v\n", name, h) + } + } +} + +func main() { + http.HandleFunc("/hello", hello) + http.HandleFunc("/headers", headers) + + http.ListenAndServe(":8090", nil) +} +``` + +我们会通过查看 `http.ListenAndServe` 函数来开始跟踪一个 HTTP 请求在这个服务中的生命周期: + +```go +func ListenAndServe(addr string, handler Handler) error +``` + +这张图展示了调用时所发生的简要流程: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210220-Life-of-an-HTTP-request-in-a-Go-server/http-request-listenandserve.png) + +这是函数和方法调用的实际序列的高度“内联”版本,但是[原始的代码](https://go.googlesource.com/go/+/go1.15.8/src/net/http/server.go)并不难理解。 + +主流程正如你期望的那样:`ListenAndServe` 监听给定地址的 TCP 端口,之后循环接受新的连接。对于每一个新连接,它都会调度一个 goroutine 来处理这个连接(稍后详细说明)。处理连接涉及一个这样的循环: + +- 从连接中解析 HTTP 请求;产生 `http.Request` +- 将这个 `http.Request` 传递给用户定义的 handler + +handler 是一个实现 `http.Handler` 接口的任意实例: + +```go +type Handler interface { + ServeHTTP(ResponseWriter, *Request) +} +``` + +## 默认的 handler + +在我们的实例代码中,`ListenAndServe` 被调用的时候使用 `nil` 作为第二个参数,而这个位置本应该使用用户定义的 handler,这是怎么回事? + +我们的图简化了一些细节;实际上,当这个 HTTP 包处理一个请求的时候,它并不会直接调用用户的 handler,而是使用这个适配器: + +```go +type serverHandler struct { + srv *Server +} + +func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { + handler := sh.srv.Handler + if handler == nil { + handler = DefaultServeMux + } + if req.RequestURI == "*" && req.Method == "OPTIONS" { + handler = globalOptionsHandler{} + } + handler.ServeHTTP(rw, req) +} +``` + +注意高亮的部分(if handler == nil ...),如果 `handler == nil`,则 `http.DefaultServeMux` 被用作 handler。这个是*默认的 server mux*,`http` 包中所包含的一个 `http.ServeMux` 类型的全局实例。顺便一提,当我们的示例代码使用 `http.HandleFunc` 注册 handler 函数的时候,会在同一个默认的 mux 上注册这些handler。 + +我们可以如下所示这样重写我们的示例代码,不再使用默认的 mux。只修改 `main` 函数,所以这里没有展示 `hello` 和 `headers` handler 函数,我们可以在这看[完整的代码](https://github.com/eliben/code-for-blog/blob/master/2021/go-life-http-request/basic-server-mux-object.go)。功能上没有任何变化[^1]: + +```go +func main() { + mux := http.NewServeMux() + mux.HandleFunc("/hello", hello) + mux.HandleFunc("/headers", headers) + + http.ListenAndServe(":8090", mux) +} +``` + +## 一个 `ServeMux` 仅仅是一个 `Handler` + +当看多了 Go 服务的例子后,很容易给人一种 `ListenAndServe` 函数“需要一个 mux” 作为参数的印象,但是这是不准确的。就像我们之前所见到的那样,`ListenAndServe` 函数需要的是一个实现了 `http.Handler` 接口的值。我们可以写下面这样的服务而没有任何 mux: + +```go +type PoliteServer struct { +} + +func (ms *PoliteServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, "Welcome! Thanks for visiting!\n") +} + +func main() { + ps := &PoliteServer{} + log.Fatal(http.ListenAndServe(":8090", ps)) +} +``` + +由于这里没有路由逻辑;所有到达 `PoliteServer` 的 `ServeHTTP` 方法的 HTTP 请求都会以同样的信息所回复。试着用不同的路径和方法 `curl` -ing 这个服务;返回一定是一致的。 + +我们可以使用 `http.HandlerFunc` 来进一步简化我们的 polite 服务: + +```go +func politeGreeting(w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, "Welcome! Thanks for visiting!\n") +} + +func main() { + log.Fatal(http.ListenAndServe(":8090", http.HandlerFunc(politeGreeting))) +} +``` + +`HandlerFunc` 是这样一个位于 `http` 包中的巧妙的适配器: + +```go +// The HandlerFunc type is an adapter to allow the use of +// ordinary functions as HTTP handlers. If f is a function +// with the appropriate signature, HandlerFunc(f) is a +// Handler that calls f. +type HandlerFunc func(ResponseWriter, *Request) + +// ServeHTTP calls f(w, r). +func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { + f(w, r) +} +``` + +如果你在这篇博文的第一个示例中注意到 `http.HandleFunc`[^2], 它对具有 `HandlerFunc` 签名的函数使用同样的适配器。 + +就像 `PoliteServer` 一样,`http.ServeMux` 是一个实现了 `http.Handler` 接口的类型。如果愿意的话你可以仔细阅读[完整代码](https://go.googlesource.com/go/+/go1.15.8/src/net/http/server.go);这是一个大纲: + +- `ServeMux` 维护了一个(根据长度)排序的 `{pattern, handler}` 的切片。 +- `Handle` 或 `HandleFunc` 向该切片增加新的 handler。 +- `ServeHTTP`: + - (通过查找这个排序好的 handler 对的切片)为请求的 path 找到对应的 handler + - 调用 handler 的 `ServeHTTP` 方法 + +因此,mux 可以被看做是一个*转发 handler*;这种模式在 HTTP 服务开发中极为常见,这就是*中间件*。 + +## `http.Handler` 中间件 + +由于中间件在不同的上下文,不同的语言以及不同的框架中意味着不同的东西,所以它很难被准确定义。 + +让我们回到这篇博文开头的流程图上,对它进行一点简化,隐藏 `http` 包所执行的细节: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210220-Life-of-an-HTTP-request-in-a-Go-server/http-request-simplified.png) + +现在,当我们加了中间件的话,流程图看起来是这样的: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210220-Life-of-an-HTTP-request-in-a-Go-server/http-request-with-middleware.png) + +在 Go 语言中,中间件只是另一个 HTTP handler,它包裹了一个其他的 handler。中间件 handler 通过调用 `ListenAndServe` 被注册进来;当调用的时候,它可以执行任意的预处理,调用自身包裹的 handler 然后可以执行任意的后置处理。 + +我们之前已经见过了一个中间件的例子—— `http.ServeMux`;在这个例子中,预处理是基于请求的 path 来选择正确的用户 handler 来调用。没有后置处理。 + +再来另一个具体的例子,回到我们的 polite 服务上,新增一些基本的*日志中间件*。这个中间件记录每个请求的具体日志,包括执行了多长时间: + +```go +type LoggingMiddleware struct { + handler http.Handler +} + +func (lm *LoggingMiddleware) ServeHTTP(w http.ResponseWriter, req *http.Request) { + start := time.Now() + lm.handler.ServeHTTP(w, req) + log.Printf("%s %s %s", req.Method, req.RequestURI, time.Since(start)) +} + +type PoliteServer struct { +} + +func (ms *PoliteServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, "Welcome! Thanks for visiting!\n") +} + +func main() { + ps := &PoliteServer{} + lm := &LoggingMiddleware{handler: ps} + log.Fatal(http.ListenAndServe(":8090", lm)) +} +``` + +注意 `LoggingMiddleware` 本身是一个 `http.Handler`,它持有一个用户 handler 作为字段。当 `ListenAndServe` 调用它的 `ServeHTTP` 方法,它做了如下事情: + +1. 预处理:在用户的 handler 执行之前记录一个时间戳 +2. 使用请求和返回 writer 调用用户 handler +3. 后置处理:记录请求详细日志,包括耗时 + +中间件最大的优点是可以组合。被中间件所包裹“用户 handler” 也可以是另一个中间件,依次类推。这是一个互相包裹的 `http.Handler` 链。事实上,这在 Go 中是一个常见的模式,来看看 Go 中间件的经典用法。还是我们的日志 polite 服务,这次使用了更有识别度的 Go 中间件实现: + +```go +func politeGreeting(w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, "Welcome! Thanks for visiting!\n") +} + +func loggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + start := time.Now() + next.ServeHTTP(w, req) + log.Printf("%s %s %s", req.Method, req.RequestURI, time.Since(start)) + }) +} + +func main() { + lm := loggingMiddleware(http.HandlerFunc(politeGreeting)) + log.Fatal(http.ListenAndServe(":8090", lm)) +} +``` + +相对于创建一个带有方法的结构体,`loggingMiddleware` 利用 `http.HandlerFunc` 和闭包使代码更加简洁,同时保留了相同的功能。更重要的是这个例子展示了中间件事实上的标准*签名*:一个函数传入一个 `http.Handler`,有时还有其他状态,之后返回一个不同的 `http.Handler`。返回的 handler 现在应该替换掉传入中间件的那个 handler,之后会“神奇地”执行它原有的功能,并且与中间件的功能包装在一起。 + +比如。标准库包含了以下的中间件: + +```go +func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler +``` + +如果我们的代码中有 `http.Handler`,像这样包装它: + +```go +handler = http.TimeoutHandler(handler, 2 * time.Second, "timed out") +``` + +创建了一个新版本的 handler,这个版本内置了2秒的超时机制。 + +中间件的组合可以像下面这样展示: + +```go +handler = http.TimeoutHandler(handler, 2 * time.Second, "timed out") +handler = loggingMiddleware(handler) +``` + +经过这样两行代码之后,`handler` 会带有超时*和日志*功能。你也许会注意到链路长的中间件编写起来会很繁琐;Go 有很多流行的包可以解决这个问题,不过这不在这篇文章的讨论范围内。 + +顺便一提,虽然 `http` 包在内部使用中间件满足自身需要;具体见这篇博文之前关于 `serverHandler` 适配器的例子。但是它提供了一个清晰的方式以默认行为处理用户 handler 为 nil 的情形(把请求传入默认的 mux)。 + +希望这样可以让大家明白为什么中间件是一个很吸引人的辅助设计。我们可以专注于我们的“业务逻辑” handler 上,尽管完全正交,我们利用通用的中间件,在许多方面提升我们的 handler。在其他文章中,会进行全面的探讨。 + +## 并发和 panic 处理 + +为了结束我们对于 Go HTTP 服务中 HTTP 请求的探索,来介绍另外两个主题:并发和 panic 处理。 + +首先是*并发*。之前简单提到,每个连接由 `http.Server.Serve` 在一个新的 goroutine 中处理。 + +这是 Go 的 `net/http` 的一个强大的功能,它利用了 Go 出色的并发性能,使用轻量的 goroutine 使 HTTP handler 保持了一个非常简单的并发模型。一个 handler 阻塞的时候(比如,读取数据库)不需要担心拖慢其他 handler。但是,编写存在共享数据的 handler 的时候需要格外小心。具体细节参考[之前的文章](https://eli.thegreenplace.net/2019/on-concurrency-in-go-http-servers)。 + +最后,*panic 处理*。一个 HTTP 服务通常是一个长期运行的后台进程。假如在用户提供的请求 handler 中发生了什么糟糕的事情,比如,一些导致运行时 panic 的bug。会导致整个服务崩溃,这可不是什么好事情。为了避免这样的惨剧,你也许会考虑在你服务的 `main` 函数中加上 `recover`,但是并没什么用,原因如下: + +1. 当控制返还给 `main` 函数的时候,`ListenAndServe` 已经执行完毕而不会再提供任何服务。 +2. 由于每个连接在分开的 goroutine 中处理,当 handler 中发送 panic 的时候,甚至不会影响到 `main` 函数,但是会导致对应进程的崩溃。 + +为了提供些许的帮助,`net/http` 包(在 `conn.serve` 方法中)内置对每个服务 goroutine 有 recovery。我们可以通过简单的例子来看到它的作用: + +```go +func hello(w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, "hello\n") +} + +func doPanic(w http.ResponseWriter, req *http.Request) { + panic("oops") +} + +func main() { + http.HandleFunc("/hello", hello) + http.HandleFunc("/panic", doPanic) + + http.ListenAndServe(":8090", nil) +} +``` + +如果我们运行这个服务,并且 `curl` `/panic` 路径,我们可以看到: + +``` +$ curl localhost:8090/panic +curl: (52) Empty reply from server +``` + +并且服务会在自身 log 中打印这样的信息: + +``` +2021/02/16 09:44:31 http: panic serving 127.0.0.1:52908: oops +goroutine 8 [running]: +net/http.(*conn).serve.func1(0xc00010cbe0) + /usr/local/go/src/net/http/server.go:1801 +0x147 +panic(0x654840, 0x6f0b80) + /usr/local/go/src/runtime/panic.go:975 +0x47a +main.doPanic(0x6fa060, 0xc0001401c0, 0xc000164200) +[... rest of stack dump here ...] +``` + +不过,这个服务会保持运行并且我们可以继续访问它! + +尽管这个内置的保护机制相比服务崩溃要好,许多开发者还是发现了它的局限。这个保护机制只关闭了连接以及在日志中输出错误;通常来说,向客户端返回某种错误响应(比如 code 500 —— 内置错误)和附加详细信息会有用得多。 + +阅读了这个博文后,再写实现这个功能的中间件应该是很容易的。将它作为练习!我会在之后的博文中介绍这个用例。 + +[^1]: 与使用默认的 mux 的版本相比,这个版本有充分的理由更喜欢这一版本。默认的 mux 有着一定的安全风险;作为全局实例,它可以被你工程中引入的任何包所修改。一个恶意的包也许会出于邪恶的目的而使用它。 +[^2]: 注意:`http.HandleFunc` 和 `http.HandlerFunc` 是具有不同而有相互关联的角色的不同实体。 + +--- + +via: https://eli.thegreenplace.net/2021/life-of-an-http-request-in-a-go-server/ + +作者:[Eli Bendersky](https://eli.thegreenplace.net/pages/about) +译者:[dust347](https://github.com/dust347) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20210413-Demystifying-Pprof-Labels-with-Go.md b/published/tech/20210413-Demystifying-Pprof-Labels-with-Go.md new file mode 100644 index 000000000..f3a04b6e8 --- /dev/null +++ b/published/tech/20210413-Demystifying-Pprof-Labels-with-Go.md @@ -0,0 +1,167 @@ +首发于:https://studygolang.com/articles/35253 + +# 深入剖析 Golang Pprof 标签 + +Polar Signals 提供的持续分析工具可以和任何格式的 pprof 分析适配,Go 深度集成了 [pprof](https://github.com/google/pprof) 甚至支持了它的`标签`特性。 + +然而,自从我们发布了我们的 [持续分析产品](https://www.polarsignals.com/blog/posts/2021/02/09/announcing-polar-signals-continuous-profiler/) 之后,收到了很多工程师的反馈,发现许多工程师不知道如何去分析, 或者不知道分析能给他们带来什么好处。这篇文章主要剖析 pprof 标签,并会结合一些 Go 的示例代码去分析。 + +## 基础 + +pprof 标签只支持 Go 的 CPU 分析器。Go 的分析器是抽样分析,这意味着它只会根据特定的频率(默认是 1 秒钟 100 次)去获取执行中函数的调用栈并记录。简单来说,开发者如果使用标签,在分析器取样时就可以将函数的调用栈进行区分,然后只聚合具有相同标签的函数调用栈。 + +Go 在 `runtime/pprof` 包中已经支持了标签检测,可以使用 [`pprof.Do`](https://golang.org/pkg/runtime/pprof/#Do) 函数非常方便的使用。 + +```golang +pprof.Do(ctx, pprof.Labels("label-key", "label-value"), func (ctx context.Context) { + // execute labeled code +}) +``` + +## 进阶 + +为了进行演示如何使用 pprof 标签,我们创建了一个包含许多示例的仓库,这个示例仓库代码作为这篇文章的内容指导。仓库地址:[https://github.com/polarsignals/pprof-labels-example](https://github.com/polarsignals/pprof-labels-example) + +示例代码的 [`main.go`](https://github.com/polarsignals/pprof-labels-example/blob/60accf8b4fbebcd5f96b3743663af5745ef74596/main.go) 通过将 `tanant` 传递给 `iterate` 函数实现了大量的 for 循环, 其中`tanant1` 做了 10 亿次循环,而 `tanant2` 做了 1 亿次循环,同时会记录 CPU 的分析日志并将其写入 `./cpuprofile.pb.gz`。 + +为了演示如何在 pprof 的分析日志中展示 pprof 标签,用 [`printprofile.go`](https://github.com/polarsignals/pprof-labels-example/blob/60accf8b4fbebcd5f96b3743663af5745ef74596/printprofile.go) 来打印每次抽样函数调用栈以及样本值,还有收集到样本的标签。 + +如果我们注释掉 [`pprof.Do` 的这部分](https://github.com/polarsignals/pprof-labels-example/blob/60accf8b4fbebcd5f96b3743663af5745ef74596/main.go#L39-L41) ,我们将无法进行标签检测,运行 [`printprofile.go`](https://github.com/polarsignals/pprof-labels-example/blob/60accf8b4fbebcd5f96b3743663af5745ef74596/printprofile.go) 代码,让我们看看没有标签的抽样分析结果: + +```bash +runtime.main +main.main +main.iteratePerTenant +main.iterate +2540000000 +--- +runtime.main +main.main +main.iteratePerTenant +main.iterate +250000000 +--- +Total: 2.79s +``` + +CPU 分析数据的单位是 纳秒,所以这些抽样总共花费时间是 2.79 秒(2540000000ns + 250000000ns = 2790000000ns = 2.79s)。 + +同样的,现在当每次调用 `iterate` 时添加标签,用 pprof 分析,这些数据看起来就不太一样,打印出带有标签的抽样分析结果: + +```bash +runtime.main +main.main +main.iteratePerTenant +runtime/pprof.Do +main.iteratePerTenant.func1 +main.iterate +10000000 +--- +runtime.main +main.main +main.iteratePerTenant +runtime/pprof.Do +main.iteratePerTenant.func1 +main.iterate +2540000000 +map[tenant:[tenant1]] +--- +runtime.main +main.main +main.iteratePerTenant +runtime/pprof.Do +main.iteratePerTenant.func1 +main.iterate +10000000 +map[tenant:[tenant1]] +--- +runtime.main +main.main +main.iteratePerTenant +runtime/pprof.Do +main.iteratePerTenant.func1 +main.iterate +260000000 +map[tenant:[tenant2]] +--- +Total: 2.82s +``` + +将所有抽样加起来总共花费了 2.82 秒,然而,因为调用 `iterate` 时,我们添加了标签,所以我们能在结果中区分哪个 `tenant` 导致了更多的 CPU 占用。现在我们可以看到 `tenant1` 花费了总时间 2.82 秒中的 2.55 秒(2540000000ns + 10000000ns = 2550000000ns = 2.55s)。 + +让我们看看抽样的原始日志(还有它们的元数据),去更深入理解一下它们的格式: + +```bash +$ protoc --decode perftools.profiles.Profile --proto_path ~/src/github.com/google/pprof/proto profile.proto < cpuprofile.pb | grep -A12 "sample {" +sample { + location_id: 1 + location_id: 2 + location_id: 3 + location_id: 4 + location_id: 5 + value: 1 + value: 10000000 +} +sample { + location_id: 1 + location_id: 2 + location_id: 3 + location_id: 4 + location_id: 5 + value: 254 + value: 2540000000 + label { + key: 14 + str: 15 + } +} +sample { + location_id: 6 + location_id: 2 + location_id: 3 + location_id: 4 + location_id: 5 + value: 1 + value: 10000000 + label { + key: 14 + str: 15 + } +} +sample { + location_id: 1 + location_id: 2 + location_id: 3 + location_id: 7 + location_id: 5 + value: 26 + value: 260000000 + label { + key: 14 + str: 16 + } +} +``` + +我们可以看到每个抽样都由许多 ID 组成,这些 ID 指向它们在分析日志 `location` 数组中的位置,除了这些 ID 还有几个值。仔细看下 `printprofile.go` 程序,你会发现它使用了每个抽样的最后一个抽样 value。实际上,Go 的 CPU 分析器会记录两个 value,第一个代表这个调用栈在一次分析区间被记录样本的数量,第二个代表它花费了多少纳秒。pprof 的定义描述当没设置 `default_sample_type` 时(在 Go 的 CPU 配置中设置),就使用所有 value 中的最后一个,因此我们使用的是代表纳秒的 value 而不是样本数的 value。最后,我们可以打印出标签,它是 pprof 定义的一个以字符串组成的字典。 + +最后,因为用标签去区分数据,我们可以让可视化界面更直观。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210413-Demystifying-Pprof-Labels-with-Go/pprof-callgraph-with-labels.png) + +你可以在 Polar Signals 网站去更详细的了解上面的这次分析:[https://share.polarsignals.com/2063c5c/](https://share.polarsignals.com/2063c5c/)。 + +## 结论 + +pprof 标签是帮助我们理解程序不同执行路径非常有用的方法,许多人喜欢在多租户系统中使用它们,目的就是为了能够定位在他们系统中出现的由某一个租户导致的性能问题。就像开头说的,只需要调用 [`pprof.Do`](https://golang.org/pkg/runtime/pprof/#Do) 就可以了。 + +Polar Signals 提供的持续分析工具也支持了 pprof 标签的可视化界面和报告,如果你想参与个人体验版请点击:[申请资格](https://www.polarsignals.com/#request-access)。 + +--- +via: https://www.polarsignals.com/blog/posts/2021/04/13/demystifying-pprof-labels-with-go/ + +作者:[Frederic Branczyk](https://twitter.com/fredbrancz) +译者:[h1z3y3](https://h1z3y3.me) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20210421-How-I-Structure-Services-in-Go.md b/published/tech/20210421-How-I-Structure-Services-in-Go.md new file mode 100644 index 000000000..706f15300 --- /dev/null +++ b/published/tech/20210421-How-I-Structure-Services-in-Go.md @@ -0,0 +1,256 @@ +首发于:https://studygolang.com/articles/35254 + +# 如何在 Go 中组织项目结构 + +GCTT 译者注:在翻译这篇文章之前,我自己其实对 Bob 大叔的 Clean Architecture 也做过一些研究,在项目中实践之后,也确确实实体验到了分层的魅力。在层与层之间将依赖进行隔离,各个层只关注自己本身的逻辑,所以能让开发者只关注本层的业务逻辑,也更容易进行单元测试,无形中就提高了你代码的质量和可阅读性。我觉得如果你对自己的代码有追求,就一定要去学习一下 Clean Architecture。 + +当然另一方面,Clean Architecture 也不是银弹,在复杂的项目中确实能帮助我们解藕,但是如果你的项目非常简单,那传统的 MVC 就足够了,就像本文作者最后说的,千万不要让简单的事情变复杂。 + +另外,其实对于 Golang 的项目组织方式,github 上面有些 star 非常多的项目,大多开箱即用,比如:[go-gin-api](https://github.com/xinliangnote/go-gin-api) (国人开源的)、[go-clean-arch](https://github.com/bxcodec/go-clean-arch) ,这里分享给大家,也是给大家提供更多的选择。 + +以下是原文: + +一个 main.go 文件,几个 HTTP handler 就可以构建一个新的 HTTP 服务。然而,当你开始添加更多的路由规则,开始将不同的功能拆分到不同的文件中,可能会随处创建好多 packages,但是你不确定长远来看将会它们怎样发展,同时你也希望它们能够随着服务增长而有意义。 + +这几年我经历过几次这样的场景,后来读了一些文章、博客还有 Robert Martin 的 [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) (干净架构),我找到了一种适合我的通用的代码结构,所以我想我应该分享出来。需要注意的是它可能并不能刚好适合你的应用场景,特别是一些特别简单的服务,比如说一个 main.go 文件,几个 packages 就已经足够的服务。 + +让我们直接开始! + +## 概览 + +```bash +Go-Service +- cmd/ + - api/ +- pkg/ + - api/ + - db/ + - services/ + - serviceA/ + - serviceB/ + - ... + - utils/ +- docker-compose.yml +- Dockerfile +- Makefile +- go.mod +- go.sum +- .env +- README.md +- ... +``` + +整个结构有 3 个重要的部分:root(译者注:根目录)、cmd 和 pkg。我将逐个解释各个文件夹的职责,然后我们再来仔细看看每个 service (`pkg/service/...`) 如何组织。 + +### Root(根目录) + +我喜欢将一些启动和运行的代码放到根目录,比如:构建工具、配置文件、依赖管理等等。 它也提供给阅读代码的人或开发代码的人一个很好的切入点,他们启动服务所需的所有配置都在项目根目录下。 + +### Cmd + +这里会被分成几个目录,每个目录都是我们整个服务一部分,比如 API 服务,定时脚本任务等等。实际上这里会有各子服务的 main package,所以我们在这里初始化配置和我们需要的依赖包,最后子服务会被编译成对应的二进制来提供服务。 + +### Pkg + +这里包含了我们项目的主要部分:定义我们服务业务逻辑的一些 package。 + +* api/ + + 在这里,我定义了如何通过初始化数据库,服务,HTTP路由器+中间件来连接API,并定义了运行API所需的配置。 + 我一般会加一个 `Start(cfg *Config)` 函数,提供给 `cmd/api/main.go` 调用。 + +* db/ + + 顾名思义,这里是连接、迁移数据库逻辑,我也倾向于将任何关于迁移的文件夹或文件都放在这里。 + +* utils/ + + 我会将任何对请求、日志、自定义中间件等提供辅助功能的 pakcage 放在这里。我虽然不太喜欢这个名字,但是我也没找到更适合它的名字了。 + +* services/ + + 这个需要详细解释一下,因为我用特定的方式去组织所有的 service。通常来说,每个 package + 都定义了各自服务的功能(基于功能而不是函数进行组织结构)。 + +## Services + +让我们通过一个例子来看看他们是如何组织的。我们要创建一个服务,可以让我们保存并创建文章,他看起来是下面这样: + +```bash +... +- Services/ + - Article/ + - store/ + - repo.go + - transport/ + - http.go + - article.go + - errors.go + - models.go +``` + +我们将数据的存储和传输逻辑分到了不同的 package,这帮助我们专注于我们的业务逻辑而不需要关心我们应该如何保存数据或者其如何传递给调用方。 + +此外,当我们想要改变我们的底层存储时,我们只需要定义好存储的 interface,就可以轻松地更换底层存储,而不需要修改其余的逻辑( 一个简单的 [依赖反转原则](https://en.wikipedia.org/wiki/Dependency_inversion_principle) 的例子 )。 + +`error.go` 和 `models.go` 比较简单,就不赘述了,让我们看看 `article.go` 都有什么功能: + +```go +package articles + +import ( + "context" +) + +// Repo defines the DB level interaction of articles +type Repo interface { + Get(ctx context.Context, id string) (Article, error) + Create(ctx context.Context, ar ArticleCreateUpdate) (string, error) +} + +// Service defines the service level contract that other services +// outside this package can use to interact with Article resources +type Service interface { + Get(ctx context.Context, id string) (Article, error) + Create(ctx context.Context, ar ArticleCreateUpdate) (Article, error) +} + +type article struct { + repo Repo +} + +// New Service instance +func New(repo Repo) Service { + return &article{repo} +} + +// Get sends the request straight to the repo +func (s *article) Get(ctx context.Context, id string) (Article, error) { + return s.repo.Get(ctx, id) +} + +// Create passes of the created to the repo and retrieves the newly created record +func (s *article) Create(ctx context.Context, ar ArticleCreateUpdate) (Article, error) { + id, err := s.repo.Create(ctx, ar) + if err != nil { + return Article{}, err + } + return s.repo.Get(ctx, id) +} + +``` + +这里需要注意的是,在调用 `New()` 创建我们 `Article` 服务实例的时候,传递了一个 `Repo` interface。这个是我们刚刚说的解耦的好处,而且也能帮助我们更好地去做单元测试。我们可以通过创建一个实现了 `Repo` interface 的 mock 实例,然后作为 `New()` 的参数传递给 `Article`,这样我们就可以绕过我们的数据库去对我们的逻辑进行单元测试。 + +### 如何暴露服务 + +设置方法不需要知道每个服务的接入点、如何初始化存储层或者其他的一些事项。只需要将数据库连接和路由实例传递给 `Activate()` 方法,然后 `transport` package 中的路由注册程序将其路由进行注册,就可以对外提供服务了: + +```go +package transport + +import ( + "database/sql" + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/kott/go-service-example/pkg/services/articles" + "github.com/kott/go-service-example/pkg/services/articles/store" +) + +type handler struct { + ArticleService articles.Service +} + +// Activate sets all the services required for articles and registers all the endpoints with the engine. +func Activate(router *gin.Engine, db *sql.DB) { + articleService := articles.New(store.New(db)) + newHandler(router, articleService) +} + +func newHandler(router *gin.Engine, as articles.Service) { + h := handler{ + ArticleService: as, + } + router.GET("/articles/:id", h.Get) + router.POST("/articles/", h.Create) +} + +func (h *handler) Get(c *gin.Context) {...} + +func (h *handler) Create(c *gin.Context) {...} +``` + +还记得我之前说的 `Start()` 方法(在 `pkg/api` 中)吗?它是我们启动我们服务和配置的入口: + +```go +package api + +import ( + "context" + "fmt" + + "github.com/gin-gonic/gin" + + "github.com/kott/go-service-example/pkg/db" + articles "github.com/kott/go-service-example/pkg/services/articles/transport" + "github.com/kott/go-service-example/pkg/utils/log" + "github.com/kott/go-service-example/pkg/utils/middleware" +) + +// Config defines what the API requires to run +type Config struct { + DBHost string + DBPort int + DBUser string + DBPassword string + DBName string + AppHost string + AppPort int +} + +// Start initializes the API server, adding the required middleware and dependent services +func Start(cfg *Config) { + conn, err := db.GetConnection( + cfg.DBHost, + cfg.DBPort, + cfg.DBUser, + cfg.DBPassword, + cfg.DBName) + if err != nil { + log.Error(ctx, "unable to establish a database connection: %s", err.Error()) + } + defer func() { + if conn != nil { + conn.Close() + } + }() + + router := gin.New() + router.Use(/* some middleware */) + articles.Activate(router, conn) + + if err := router.Run(fmt.Sprintf("%s:%d", cfg.AppHost, cfg.AppPort)); err != nil { + log.Fatal(context.Background(), err.Error()) + } +} +``` + +以上就是全部内容,实际上只是试图去找到一种抽象的方法,以便于让你的程序更易读,而不需要增加你项目的复杂度。 + +## 总结 + +有许许多多可以组织项目的方式,这个方式是我认为最好的。根据功能将 service 分开有助于之后进行修改时定义上下文边界和代码导航。将路由注册、业务逻辑、存储等放到同一个 `service` 层中,也让我们更关注业务逻辑本身和更容易地进行测试。不要让简单的事情变复杂!如果您想节约时间,那就一定要这样做。 + +我希望能帮助到你,如果你想阅读所有源码,你可以在 [Github](https://github.com/kott/go-service-example) 下载。 + +--- + +via: https://medium.com/@ott.kristian/how-i-structure-services-in-go-19147ad0e6bd + +作者:[Kristian Ott](https://medium.com/@ott.kristian) +译者:[h1z3y3](https://h1z3y3.me) +校对:[lxbwolf](https://github.com/lxbwolf) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20210426-html-for-pdf-reports-in-go.md b/published/tech/20210426-html-for-pdf-reports-in-go.md new file mode 100644 index 000000000..a0cc41cf7 --- /dev/null +++ b/published/tech/20210426-html-for-pdf-reports-in-go.md @@ -0,0 +1,219 @@ +首发于:https://studygolang.com/articles/35259 + +# 如何使用 Go 从 HTML 生成 PDF 报告 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/how-to-generate-a-pdf-report-from-html-with-go/1.jpg) + +作为一名开发人员,我有时需要为我的应用程序创建 PDF 报告。完全通过编程来创建它们可能很麻烦,并且每个库都有些不同。最后,让事物看起来像设计师想要的那样可能具有挑战性。如果我们能在不花大量时间的情况下让它看起来像设计,那不是很好吗?设计师和前端通常会 HTML 和 CSS,所以使用 HTML 是说得通的。但网站通常在打印出来时看起来不太好,而且不是为多页设计的。我们提出了一个解决方案,我们相信它可以解决上述所有问题。 + +## 认识 UniHTML 与 UniPDF 的结合 + +[UniHTML](https://github.com/unidoc/unihtml) 是 [UniPDF](https://github.com/unidoc/unipdf) 的新插件,[UniDoc](https://unidoc.io/) 是我们在 UniDoc 的旗舰库之一。 + +它是基于容器的解决方案,带有 Go 驱动程序,根据原理图: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/how-to-generate-a-pdf-report-from-html-with-go/2.jpg) + +[Docker 映像](https://hub.docker.com/repository/docker/unidoccloud/unihtml)在 Docker Hub 上公开可用。 + +UniPDF Creator 软件包可以[创建灵活的 PDF 报告](https://www.unidoc.io/post/creating-pdf-reports-in-golang)和[发票](https://www.unidoc.io/post/simple-invoices)。 UniHTML 基于容器的模块具有灵活的 Web 渲染引擎,并且与 UniPDF 相结合汇集了为 UniPDF 报告生成添加完整 HTML 支持的功能。 + +## 试一试 + +让我们试试看。 + +**第 1 步:创建一个免费的计量的 API 密钥** + +这很简单,只需在 上注册一个帐户并在 UI 中创建一个计量 API 密钥。 + +有关这方面的分步说明,请参阅: + +- [如何注册 UniCloud](https://help.unidoc.io/article/142-how-to-sign-up-for-unicloud) +- [如何生成计量 API 密钥](https://help.unidoc.io/article/141-metered-license-api-key) + +**第 2 步:让 UniHTML 容器运行** + +```shell +$ docker run -p 8080:8080 -e UNIDOC_METERED_API_KEY=mymeteredkey unidoccloud/unihtml +Unable to find image 'unidoccloud/unihtml:latest' locally +latest: Pulling from unidoccloud/unihtml +6e640006d1cd: Pull complete +1a3def68b0c4: Pull complete +5b1718db67b4: Pull complete +8d4c41b870b6: Pull complete +b1a4436c2bab: Pull complete +3c3af5a4fff5: Pull complete +29863d0ede88: Pull complete +Digest: sha256:c1c69af194358179d836a648f07f71af07ed0c968938abe3a3e2550e49980728 +Status: Downloaded newer image for unidoccloud/unihtml:latest +[INFO] server.go:173 Listening private API on: :8081 +[INFO] server.go:164 Listening public API on: :8080 +``` + +**第 3 步:运行一个示例** + +受博客文章“[使用 CSS 创建漂亮的 HTML 表格](https://dev.to/dcodeyt/creating-beautiful-html-tables-with-css-428l)”的启发,我们将以下 HTML 文件放在一起,以说明带有 HTML 表格的 PDF 报告。 + +**sample.html** + +```html + + + + + + + + + + + + + + + + + + + + + + + +
NamePoints
Dom6000
Melissa5150
+ + +``` + +**example.go** + +```go +package main + +import ( + "fmt" + "os" + + "github.com/unidoc/unihtml" + "github.com/unidoc/unipdf/v3/common/license" + "github.com/unidoc/unipdf/v3/creator" +) + +func main() { + // Set the UniDoc license. + if err := license.SetMeteredKey("my API key goes here"); err != nil { + fmt.Printf("Err: setting metered key failed: %v\n", err) + os.Exit(1) + } + + // Establish connection with the UniHTML Server. + if err := unihtml.Connect(":8080"); err != nil { + fmt.Printf("Err: Connect failed: %v\n", err) + os.Exit(1) + } + + // Get new PDF Creator. + c := creator.New() + + // AddTOC enables Table of Contents generation. + c.AddTOC = true + + chapter := c.NewChapter("Points") + + // Read the content of the sample.html file and load it to the conversion. + htmlDocument, err := unihtml.NewDocument("sample.html") + if err != nil { + fmt.Printf("Err: NewDocument failed: %v\n", err) + os.Exit(1) + } + + // Draw the HTML document file in the context of the creator. + if err = chapter.Add(htmlDocument); err != nil { + fmt.Printf("Err: Draw failed: %v\n", err) + os.Exit(1) + } + + if err = c.Draw(chapter); err != nil { + fmt.Printf("Err: Draw failed: %v\n", err) + os.Exit(1) + } + + + // Write the result file to PDF. + if err = c.WriteToFile("sample.pdf"); err != nil { + fmt.Printf("Err: %v\n", err) + os.Exit(1) + } +} +``` + +## 结果 + +运行结果: + +```bash +$ go run example.go +``` + +创建了一个看起来这样的 sample.pdf: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/how-to-generate-a-pdf-report-from-html-with-go/3.jpg) + +我们注意到我们还有目录,这对于高质量的 PDF 制作至关重要,以及链接到 PDF 中每一章的书签,页眉和页脚也可以轻松创建。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/how-to-generate-a-pdf-report-from-html-with-go/4.jpg) + +## 结论 + +UniPDF 中的 UniHTML 通过一个完整的渲染引擎提供了简单的 HTML 到 PDF 的转换。对于已经拥有 HTML 设计并需要添加专业 PDF 报告的团队而言,这将使 PDF 报告生成过程变得非常容易,而网站的纯 PDF 打印输出是不够的。 + +--- + +via: + +作者:[Gus Hall](https://hackernoon.com/u/gushall) +译者:[lavaicer](https://github.com/lavaicer) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20210429-How-and-Why-to-Write-Enums-in-Go.md b/published/tech/20210429-How-and-Why-to-Write-Enums-in-Go.md new file mode 100644 index 000000000..256cf7019 --- /dev/null +++ b/published/tech/20210429-How-and-Why-to-Write-Enums-in-Go.md @@ -0,0 +1,119 @@ +首发于:https://studygolang.com/articles/35255 + +# 如何以及为什么在 Go 中编写枚举 + +一个**枚举**(enum,**enumerator** 的缩写),是一组命名的常量值。枚举是一个强大 的工具,让开发者可以创建复杂的常量集,而这些常量集有着有用的名称和简单且唯一的取值。 + +*在我们走远之前,我想提一下[我最近启动了 Go Mastery,一门动手的 Golang 课程](https://qvault.io/go-mastery-course/)。如果想要了解更多关于 Go 的信息,请尝试下该课程,现在让我们回到枚举上面。* + +## 语法示例 + +在一个常量声明中,[iota](https://golang.org/ref/spec#Iota) 关键字创建枚举作为连续的无类型整型常量。 + +```go +type BodyPart int + +const ( + Head BodyPart = iota // Head = 0 + Shoulder // Shoulder = 1 + Knee // Knee = 2 + Toe // Toe = 3 +) +``` + +## 为什么应该使用枚举? +来看一些关于枚举你可能会有的几个疑问。首先枚举也许看起来没那么有用,但是我向你保证枚举是有用的。 + +**而且,如果想要一个整型常量,就不能用一个普通的 `const` 吗?比如,`const head = 0` ?** + +可以,*可以*这么做,但是枚举的强大之处在于将常量*集*聚合在一起并且保证值*唯一*。通过使用枚举,编译器层面保证了你的常量(比如,`Head`,`Shoulder`,`Knee`,和 `Toe`)不会有相同的值。 + +**为什么不直接使用字符串作为唯一值?比如说,`const Head = "head"` 以及 `const Shoulder = "shoulder"` ?** + +除了编译器无法保证唯一这个老生常谈的回答之外,一个字符串需要更多内存,并且在一些受限的情形下会导致性能问题。如果你有一组4个,10个,甚至100个唯一的值,你真的需要存储整个 `string` 吗?一个 `int` 型会占用更少的程序内存。 + +不仅仅与空间有关系,尤其是在现代硬件十分强大。假如你有类似下面这样的一些配置变量。 + +```go +const ( + statusSuccess = iota + statusFailed + statusPending + statusComplete +) +``` + +假装你需要将 `statusFailed` 变更为 `statusCancelled`,以便和其他代码库保持一致。如果你先前没有使用枚举而是使用 `failed` 这个(字符串类型的)值,而现如今这个值散布在不同数据库中,那变更会变的*非常*困难。如果你使用的是`枚举`,你可以[修改名字](https://qvault.io/clean-code/naming-variables/)而不需要触及底层的值,你的代码还能保持干净。 + +## 从 1 开始枚举 + +有的时候,如果你是受虐狂,或者如果你是一个 Lua 开发者,希望你的枚举列表从 `1` 开始而不是从默认的 `0` 开始,在 Go 中你可以很轻易地实现。 + +```go +const ( + Head = iota + 1 // 1 + Shoulder // 2 + Knee // 3 + Toe // 4 +) +``` + +## 带有乘法的枚举 + +`iota` 关键字简单地代表一个自增的整型常量,即在同一个 `const` 块中每使用一次就会变大的一个数字。你可以对它使用任何你想要使用的数学运算。 + +```go +const ( + Head = iota + 1 // 0 + 1 = 1 + Shoulder = iota + 2 // 1 + 2 = 3 + Knee = iota * 10 // 2 * 10 = 20 + Toe = iota * 100 // 3 * 100 = 300 +) +``` + +考虑到这一点,请记住你*可以*做不代表你*应该*这样做。 + +## 跳过的值的枚举 + +如果你想要跳过某个值,可以使用 _ 字符,就如同忽略(函数)返回的变量一样。 + +```go +const ( + Head = iota // Head = 0 + _ + Knee // Knee = 2 + Toe // Toe = 3 +) +``` + +## 在 Go 中枚举的 String + +Go 对于枚举并没有内置的 `string` 函数,但是可以很容易地通过实现 `String()` 。通过使用 `String()` 方法而非将常量设置为字符串类型,可以使枚举带有“可打印性”从而获得和使用字符串相同的好处。 + +```go +type BodyPart int + +const ( + Head BodyPart = iota // Head = 0 + Shoulder // Shoulder = 1 + Knee // Knee = 2 + Toe // Toe = 3 +) + +func (bp BodyPart) String() string { + return [...]string{"Head", "Shoulder", "Knee", "Toe"} + // 译注:这里应该是 return [...]string{"Head", "Shoulder", "Knee", "Toe"}[bp] +} +``` + +但是这个方法存在一些“陷阱”,需要注意。如果 `const` 块中声明的数量与 `String()` 方法中所创建的“[常量切片](https://qvault.io/golang/golang-constant-maps-slices/)”中条目数对不上,编译器并不会警告可能存在的“越界”错误。同样的,如果你曾更新了常量中的某个名称,不要忘记更新列表中对应的字符串。 + +--- + +via: https://qvault.io/golang/golang-enum/ + +作者:[Lane Wagner](https://qvault.io/author/lane-c-wagner/) +译者:[dust347](https://github.com/dust347) +校对:[lxbwolf](https://github.com/lxbwolf) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20210502-Error-Handling-in-Go-made-more-Powerful.md b/published/tech/20210502-Error-Handling-in-Go-made-more-Powerful.md new file mode 100644 index 000000000..ccc12f210 --- /dev/null +++ b/published/tech/20210502-Error-Handling-in-Go-made-more-Powerful.md @@ -0,0 +1,107 @@ +首发于:https://studygolang.com/articles/35386 + +# 让 Go 的错误处理更加强大 + +Go 所提供的默认的 `errors` 包有很多的不足。编写多层架构应用程序并使用 API 公开功能的时候,相比于单纯的 `string` 类型的值,更需要具有上下文信息的错误处理。意识到这个缺点后,我开始实现一个更强大,更优雅的 error 包。这是一个逐渐演化的过程,随着时间推移,我需要在这个包中引入更多的功能。 + +在此,我们会探讨我们如何使用一个 `CustomError` 数据类型为应用中带来更多的价值,并且使错误处理更强大。 + +首先需要明白的是,如果实现了 `Error()` 方法,Go 允许使用任何用户定义的数据类型替代内置的 `error` 数据类型。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210502-Error-Handling-in-Go-made-more-Powerful/implements-an-Error.png) + +换言之,只要我们自定义的数据类型实现了返回一个 `string` 类型的 `Error()` 方法,我们就可以使用我们自己的数据类型代替 Go 默认提供的那个;任何应该返回 `error` 类型的函数都可以返回我们自定义的数据类型,并且运行如常。(译者注:这里所谓原生的 error “数据类型”,就是 error interface) + +## 构建 'CustomError' 类型 + +1、我们创建一个新的数据类型,该数据类型在应用程序中被解释为 error。我们将它命名为 `CustomError`,首先,它会包含一个默认的 `error` 类型。这个 `error` 字段能够让我们在 `CustomError` 初始化的时候使用堆栈的跟踪信息对其进行注释(更多相关细节请看[这里](https://github.com/abhinav-codealchemist/custom-error-go/blob/ca21f0e42b4ed57b5390491fe25fcb16eec7cffa/CustomError.go#L23))。记录这些堆栈信息可以让我们在诸如 NewRelic APM 之类的平台上更容易地调试错误。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210502-Error-Handling-in-Go-made-more-Powerful/CustomError.png) + +2、Go 内置的 `error` 类型将错误当做 `string` 类型的值。我一直认为这种方式是不对的,至少这样是不足的。一个错误应当有与之关联的类型——比如错误是否由于 SQL DB 写入/更新/获取操作失败而导致的?或者错误是否是由于请求中没有提供足够的数据导致的? + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210502-Error-Handling-in-Go-made-more-Powerful/CustomError-add-code.png) + +现在,更加深入地了解下 `ErrorCode` 类型的实际情况。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210502-Error-Handling-in-Go-made-more-Powerful/ErrorCode.png) + +我们依据可能在应用中捕获到的每个错误类型创建多个 `ErrorCode` 常量。这样一个 error 的解释就基于 `ErrorCode` 而不是字符串了。 + +获知一个 error 的类型可以让我们以不同的方式处理不同的错误,也可以基于 error 的不同类型采取业务层面的决策。不仅仅只是业务决策,快速浏览下 `ErrorCode` 甚至可以指出系统出故障的精确范围。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210502-Error-Handling-in-Go-made-more-Powerful/precise-region.png) + +举例来说,我在系统中的主要 RPC 中使用了大量的 5xx 错误在不同时间段的计数 (`5xx Count vs Time `)。通过浏览这个图可以有助于定位系统错误的准确范围。 + +3、出于提高可读性的目标,我们同时也希望保留 error 的 `string` 解释,这样也能让我们一眼就了解哪里出了问题。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210502-Error-Handling-in-Go-made-more-Powerful/CustomError-add-msg.png) + +`ErrorCode` 告诉你一个错误的类型,但是不会告诉你它发生在代码中的哪个位置。`ErrorMsg` 则服务于这个目的,并保持现有功能不变。 + +这里可能有争议,“为什么不使用在第 1 步中引入的 `error` 字段的错误信息?为什么要新加一个字段?”——这是因为我们会使用 `ErrorCode` 和 `ErrorMsg` 的组合来作为一个 `error` (代码请参考[这里](https://github.com/abhinav-codealchemist/custom-error-go/blob/ca21f0e42b4ed57b5390491fe25fcb16eec7cffa/CustomError.go#L22))。 + +但是仅有这两个就足够了吗?如果我们想捕获更多信息——比如导致一个 error 的上下文数据呢? + +比方说,一个顾客打开一个查询周围餐厅的页面,但是当他打开应用的时候,却没有给他展示任何东西。你也许需要捕获更多上下文信息,比如 ` 纬度 `,` 经度 ` 和其他一系列信息,方便你用来判断是该区域真的没有餐厅,还是这是一个维护性上的问题。 + +`LoggingParams` 正是解决办法! + +4、`LoggingParams` 允许你捕获各种依赖于上下文的参数。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210502-Error-Handling-in-Go-made-more-Powerful/LoggingParams.png) + +比如,我将这些在严重错误中产生的参数追加在告警信息中,看一下这些参数,有时就可以找出错误的根本原因。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210502-Error-Handling-in-Go-made-more-Powerful/root-cause-of-the-error.png) + +如果没有找到原因,它也有助于过滤出特定事件的错误日志。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210502-Error-Handling-in-Go-made-more-Powerful/error-log-1.png) + +![这是我们记录 CustomError 时 Kibana 日志看起来的样子](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210502-Error-Handling-in-Go-made-more-Powerful/error-log-2.png) + +5、最后,我们需要一些类似于 Go 中默认 `error` 类型申明 `error != nil` 之类的东西。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210502-Error-Handling-in-Go-made-more-Powerful/add-exists.png) + +我们引入了一个名为 `exists` 的 `bool` 字段,当 `CustomError` 被创建时,该字段会被初始化为 `true`。 + +6、对于小部分错误,我们希望想向最终用户展示特定信息。这些信息便于他们理解并做出反应。 + +我们同样维护了一个 `ErrorCode` 到用户界面友好(UI-friendly)的信息的映射。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210502-Error-Handling-in-Go-made-more-Powerful/UI-friendly-message.png) + +使用: +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210502-Error-Handling-in-Go-made-more-Powerful/Usage.png) + +看上面的例子,你会意识到每次 `Acquire()` 被调用,调用函数会期望返回 `CustomError` 的错误类型,与此同时,这会给我们带来全面提升。 + +> 译者注:站在 error 使用角度。上图中的做法可能并不是很好。 Acquire() 函数返回的不是 error 而是作者自定义的 CustomError 结构体,等同于要与这个 CustomError 耦合在一起。如果 Acquire() 函数仍返回 error,上层通过 errors.As 解析出 CustomError 可能会更好。 +> 另外,error 实际上是 interface。将一个 struct 类型或者 struct 的指针赋值给 interface 的变量,则必定不为空。执行下面的语句,err 必定不等于 nil。 +> +>```go +> var err error +> err = Acquire() +> +> if err != nil { +> ...... +> } +>``` + + +## 结论 + +我们已经看到了 `CustomError` 可以被用来使得 error 在多层次应用中更有解释力。您可以在[这里](https://github.com/abhinav-codealchemist/custom-error-go)查看具有正确接口定义和实现的完整代码。 + +--- + +via: https://medium.com/codealchemist/error-handling-in-go-made-more-powerful-ce164c2384ee + +作者:[Abhinav Bhardwaj](https://codealchemist.medium.com/) +译者:[dust347](https://github.com/dust347) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20210525-Simple-and-Powerful-ReverseProxy-in-Go.md b/published/tech/20210525-Simple-and-Powerful-ReverseProxy-in-Go.md new file mode 100644 index 000000000..c2b4d4b1e --- /dev/null +++ b/published/tech/20210525-Simple-and-Powerful-ReverseProxy-in-Go.md @@ -0,0 +1,250 @@ +首发于:https://studygolang.com/articles/35262 + +# Go 简单而强大的反向代理(Reverse Proxy) + +在本文中,我们将了解反向代理,它的应用场景以及如何在 Golang 中实现它。 + +反向代理是位于 Web 服务器前面并将客户端(例如 Web 浏览器)的请求转发到 Web 服务器的服务器。它们让你可以控制来自客户端的请求和来自服务器的响应,然后我们可以利用这个特点,可以增加缓存、做一些提高网站的安全性措施等。 + +在我们深入了解有关反向代理之前,让我们快速看普通代理(也称为正向代理)和反向代理之间的区别。 + +在**正向代理**中,代理代表原始客户端从另一个网站检索数据。它位于客户端(浏览器)前面,并确保没有后端服务器直接与客户端通信。所有客户端的请求都通过代理被转发,因此服务器只与这个代理通信(服务器会认为代理是它的客户端)。在这种情况下,代理可以隐藏真正的客户端。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210525-Simple-and-Powerful-ReverseProxy-in-Go/forward-proxy.png) + +另一方面,**反向代理**位于后端服务器的前面,确保没有客户端直接与服务器通信。所有客户端请求都会通过反向代理发送到服务器,因此客户端始终只与反向代理通信, 而从不会直接与实际服务器通信。在这种情况下,代理可以隐藏后端服务器。 + +几个常见的反向代理有 Nginx, HAProxy。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210525-Simple-and-Powerful-ReverseProxy-in-Go/reverse-proxy.png) + +## 反向代理使用场景 + +负载均衡(Load balancing):反向代理可以提供负载均衡解决方案,将传入的流量均匀地分布在不同的服务器之间,以防止单个服务器过载。 + +防止安全攻击:由于真正的后端服务器永远不需要暴露公共 IP,所以 DDoS 等攻击只能针对反向代理进行, +这能确保在网络攻击中尽量多的保护你的资源,真正的后端服务器始终是安全的。 + +缓存:假设你的实际服务器与用户所在的地区距离比较远,那么你可以在当地部署反向代理,它可以缓存网站内容并为当地用户提供服务。 + +SSL 加密:由于与每个客户端的 SSL 通信会耗费大量的计算资源,因此可以使用反向代理处理所有与 SSL 相关的内容,然后释放你真正服务器上的宝贵资源。 + +## Golang 实现 + +```go +import ( + "log" + "net/http" + "net/http/httputil" + "net/url" +) + +// NewProxy takes target host and creates a reverse proxy +// NewProxy 拿到 targetHost 后,创建一个反向代理 +func NewProxy(targetHost string) (*httputil.ReverseProxy, error) { + url, err := url.Parse(targetHost) + if err != nil { + return nil, err + } + + return httputil.NewSingleHostReverseProxy(url), nil +} + +// ProxyRequestHandler handles the http request using proxy +// ProxyRequestHandler 使用 proxy 处理请求 +func ProxyRequestHandler(proxy *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + proxy.ServeHTTP(w, r) + } +} + +func main() { + // initialize a reverse proxy and pass the actual backend server url here + // 初始化反向代理并传入真正后端服务的地址 + proxy, err := NewProxy("http://my-api-server.com") + if err != nil { + panic(err) + } + + // handle all requests to your server using the proxy + // 使用 proxy 处理所有请求到你的服务 + http.HandleFunc("/", ProxyRequestHandler(proxy)) + log.Fatal(http.ListenAndServe(":8080", nil)) +} +``` + +是的没错!这就是在 Go 中创建一个简单的反向代理所需的全部内容。我们使用标准库 `net/http/httputil` 创建了一个单主机的反向代理。到达我们代理服务器的任何请求都会被代理到位于 `http://my-api-server.com`。如果你对 Go 比较熟悉,这个代码的实现一目了然。 + +## 修改响应 + +`HttpUtil` 反向代理为我们提供了一种非常简单的机制来修改我们从服务器获得的响应, +可以根据你的应用场景来缓存或更改此响应,让我们看看应该如何实现: + +```go +// NewProxy takes target host and creates a reverse proxy +func NewProxy(targetHost string) (*httputil.ReverseProxy, error) { + url, err := url.Parse(targetHost) + if err != nil { + return nil, err + } + + proxy := httputil.NewSingleHostReverseProxy(url) + proxy.ModifyResponse = modifyResponse() + return proxy, nil +} + +func modifyResponse() func(*http.Response) error { + return func(resp *http.Response) error { + resp.Header.Set("X-Proxy", "Magical") + return nil + } +} + +``` + +可以在 `modifyResponse` 方法中看到 ,我们设置了自定义 Header 头。同样,你也可以读取响应体正文,并对其进行更改或缓存,然后将其设置回客户端。 + +在 `modifyResponse` 中,可以返回一个错误(如果你在处理响应发生了错误),如果你设置了 `proxy.ErrorHandler`, `modifyResponse` 返回错误时会自动调用 `ErrorHandler` 进行错误处理。 + +```go +// NewProxy takes target host and creates a reverse proxy +func NewProxy(targetHost string) (*httputil.ReverseProxy, error) { + url, err := url.Parse(targetHost) + if err != nil { + return nil, err + } + + proxy := httputil.NewSingleHostReverseProxy(url) + proxy.ModifyResponse = modifyResponse() + proxy.ErrorHandler = errorHandler() + return proxy, nil +} + +func errorHandler() func(http.ResponseWriter, *http.Request, error) { + return func(w http.ResponseWriter, req *http.Request, err error) { + fmt.Printf("Got error while modifying response: %v \n", err) + return + } +} + +func modifyResponse() func(*http.Response) error { + return func(resp *http.Response) error { + return errors.New("response body is invalid") + } +} +``` + +## 修改请求 + +你也可以在将请求发送到服务器之前对其进行修改。在下面的例子中,我们将会在请求发送到服务器之前添加了一个 Header 头。同样的,你可以在请求发送之前对其进行任何更改。 + +```go +// NewProxy takes target host and creates a reverse proxy +func NewProxy(targetHost string) (*httputil.ReverseProxy, error) { + url, err := url.Parse(targetHost) + if err != nil { + return nil, err + } + + proxy := httputil.NewSingleHostReverseProxy(url) + + originalDirector := proxy.Director + proxy.Director = func(req *http.Request) { + originalDirector(req) + modifyRequest(req) + } + + proxy.ModifyResponse = modifyResponse() + proxy.ErrorHandler = errorHandler() + return proxy, nil +} + +func modifyRequest(req *http.Request) { + req.Header.Set("X-Proxy", "Simple-Reverse-Proxy") +} + +``` + +## 完整代码 + +```go +package main + +import ( + "errors" + "fmt" + "log" + "net/http" + "net/http/httputil" + "net/url" +) + +// NewProxy takes target host and creates a reverse proxy +func NewProxy(targetHost string) (*httputil.ReverseProxy, error) { + url, err := url.Parse(targetHost) + if err != nil { + return nil, err + } + + proxy := httputil.NewSingleHostReverseProxy(url) + + originalDirector := proxy.Director + proxy.Director = func(req *http.Request) { + originalDirector(req) + modifyRequest(req) + } + + proxy.ModifyResponse = modifyResponse() + proxy.ErrorHandler = errorHandler() + return proxy, nil +} + +func modifyRequest(req *http.Request) { + req.Header.Set("X-Proxy", "Simple-Reverse-Proxy") +} + +func errorHandler() func(http.ResponseWriter, *http.Request, error) { + return func(w http.ResponseWriter, req *http.Request, err error) { + fmt.Printf("Got error while modifying response: %v \n", err) + return + } +} + +func modifyResponse() func(*http.Response) error { + return func(resp *http.Response) error { + return errors.New("response body is invalid") + } +} + +// ProxyRequestHandler handles the http request using proxy +func ProxyRequestHandler(proxy *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + proxy.ServeHTTP(w, r) + } +} + +func main() { + // initialize a reverse proxy and pass the actual backend server url here + proxy, err := NewProxy("http://my-api-server.com") + if err != nil { + panic(err) + } + + // handle all requests to your server using the proxy + http.HandleFunc("/", ProxyRequestHandler(proxy)) + log.Fatal(http.ListenAndServe(":8080", nil)) +} + +``` + +反向代理非常强大,如文章之前所说,它有很多应用场景。你可以根据你的情况对其进行自定义。 如果遇到任何问题,我非常乐意为你提供帮助。如果你觉得这篇文章有趣,请分享一下,让更多 Gopher 可以阅读! 非常感谢你的阅读。 + +--- + +via: https://blog.joshsoftware.com/2021/05/25/simple-and-powerful-reverseproxy-in-go/ + +作者:[Anuj Verma](https://blog.joshsoftware.com/author/devanujverma/) +译者:[h1z3y3](https://www.h1z3y3.me) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/20210918-Simplified-JSON-Handling-in-Go.md b/published/tech/20210918-Simplified-JSON-Handling-in-Go.md new file mode 100644 index 000000000..c563ba646 --- /dev/null +++ b/published/tech/20210918-Simplified-JSON-Handling-in-Go.md @@ -0,0 +1,187 @@ +首发于:https://studygolang.com/articles/35661 + +# 简化 Go 中对 JSON 的处理 + +我的第一个 Go 工程需要处理一堆 JSON 测试固件并把 JSON 数据作为参数传给我们搭建的 API 处理。另一个团队为了给 API 提供语言无关的、可预期的输入和输出,创建了这些测试固件。 + +在强类型语言中,JSON 通常很难处理 —— JSON 类型有字符串、数字、字典和数组。如果你使用的语言是 javascript、python、ruby 或 PHP,那么 JSON 有一个很大的好处就是在解析和编码数据时你不需要考虑类型。 + +```bash +// in PHP +$object = json_decode('{"foo":"bar"}'); + +// in javascript +const object = JSON.parse('{"foo":"bar"}') +``` + +在强类型语言中,你需要自己去定义怎么处理 JSON 对象的字符串、数字、字典和数组。在 Go 语言中,你使用内建的 API 时需要考虑如何更好地把一个 JSON 文件转换成 Go 的数据结构。我不打算深入研究在 Go 中如何处理 JSON 这个复杂的话题,我只列出两个代码的例子来阐述下这个问题。源码详情请见 [Go 实例教程](https://gobyexample.com/json) + +## 解析/序列化为 map[string]interface + +首先,来看这个程序 + +```go +package main + +import ( + "encoding/json" + "fmt" +) + + +func main() { + + byt := []byte(`{ + "num":6.13, + "strs":["a","b"], + "obj":{"foo":{"bar":"zip","zap":6}} + }`) + var dat map[string]interface{} + if err := json.Unmarshal(byt, &dat); err != nil { + panic(err) + } + fmt.Println(dat) + + num := dat["num"].(float64) + fmt.Println(num) + + strs := dat["strs"].([]interface{}) + str1 := strs[0].(string) + fmt.Println(str1) + + obj := dat["obj"].(map[string]interface{}) + obj2 := obj["foo"].(map[string]interface{}) + fmt.Println(obj2) + +} +``` + +我们把 JSON 数据从 byt 变量反序列化(如解析、解码等等)成名为 dat 的 map/字典对象。这些操作跟其他语言类似,不同的是我们的输入需要是字节数组(不是字符串),对于字典的每个值时需要有[类型断言](https://www.sohamkamani.com/golang/type-assertions-vs-type-conversions/)才能读取或访问该值。 + +当我们处理一个多层嵌套的 JSON 对象时,这些类型断言会让处理变得非常繁琐。 + +## 解析/序列化为 struct + +第二种处理如下: + +```go +package main + +import ( + "encoding/json" + "fmt" +) + +type ourData struct { + Num float64 `json:"num"` + Strs []string `json:"strs"` + Obj map[string]map[string]string `json:"obj"` +} + +func main() { + byt := []byte(`{ + "num":6.13, + "strs":["a","b"], + "obj":{"foo":{"bar":"zip","zap":6}} + }`) + + res := ourData{} + json.Unmarshal(byt, &res) + fmt.Println(res.Num) + fmt.Println(res.Strs) + fmt.Println(res.Obj) +} +``` + +我们利用 Go struct 的标签功能把 byt 变量中的字节反序列化成一个具体的结构 ourData。 + +标签是结构体成员定义后跟随的字符串。我们的定义如下: + +```go +type ourData struct { + Num float64 `json:"num"` + Strs []string `json:"strs"` + Obj map[string]map[string]string `json:"obj"` +} +``` + +你可以看到 Num 成员的 JSON 标签 “num”、Str 成员的 JSON 标签 “strs”、Obj 成员的 JSON 标签 “obj”。这些字符串使用[反引号](https://golangbyexample.com/double-single-back-quotes-go/)把标签声明为文字串。除了反引号,你也可以使用双引号,但是使用双引号可能会需要一些额外的转义,这样看起来会很凌乱。 + +```go +type ourData struct { + Num float64 "json:\"num\"" + Strs []string "json:\"strs\"" + Obj map[string]map[string]string "json:\"obj\"" +} +``` + +在 struct 的定义中,标签不是必需的。如果你的 struct 中包含了标签,那么它意味着 Go 的 [反射 API](https://pkg.go.dev/reflect) 可以[访问标签的值](https://stackoverflow.com/questions/23507033/get-struct-field-tag-using-go-reflect-package/23507821#23507821)。Go 中的包可以使用这些标签来进行某些操作。 + +Go 的 `encoding/json` 包在反序列化 JSON 成员为具体的 struct 时,通过这些标签来决定每个顶层的 JSON 成员的值。换句话说,当你定义如下的 struct 时: + +```go +type ourData struct { + Num float64 `json:"num"` +} +``` + +意味着: + +> 当使用 json.Unmarshal 反序列化 JSON 对象为这个 struct 时,取它顶层的 num 成员的值并把它赋给这个 struct 的 Num 成员。 + +这个操作可以让你的反序列化代码稍微简洁一点,因为程序员不需要对每个成员取值时都显式地调用类型断言。然而,这个仍不是最佳解决方案。 + +首先 —— 标签只对顶层的成员有效 —— 嵌套的 JSON 需要对应嵌套的类型(如 Obj map[string]map[string]string),因此繁琐的操作仍没有避免。 + +其次 —— 它假定你的 JSON 结构不会变化。如果你运行上面的程序,你会发现 `"zap":6` 并没有被赋值到 Obj 成员。你可以通过创建类型 `map[string]map[string]interface{}` 来处理,但是在这里你又需要进行类型断言了。 + +这是我第一个 Go 工程遇到的情况,曾让我苦不堪言。 + +幸运的是,现在我们有了更有效的办法。 + +## SJSON 和 GJSON + +Go 内建的 JSON 处理并没有变化,但是已经出现了一些成熟的旨在用起来更简洁高效的处理 JSON 的包。 + +[SJSON](https://github.com/tidwall/sjson)(写 JSON)和 [GJSON](https://github.com/tidwall/gjson)(读 JSON)是 [Josh Baker](https://github.com/tidwall) 开发的两个包,你可以用来读写 JSON 字符串。你可以参考 README 来获取代码实例 —— 下面是使用 GJSON 从 JSON 字符串中获取嵌套的值的示例: + +```go +package main + +import "github.com/tidwall/gjson" + +const JSON = `{"name":{"first":"Janet","last":"Prichard"},"age":47}` + +func main() { + value := gjson.Get(json, "name.last") + println(value.String()) +} + +``` + +类似的,下面是使用 SJSON “设置” JSON 字符串中的值返回设置之后的字符串的示例代码: + +```go +package main + +import "github.com/tidwall/sjson" + +const JSON = `{"name":{"first":"Janet","last":"Prichard"},"age":47}` + +func main() { + value, _ := sjson.Set(json, "name.last", "Anderson") + println(value) +} +``` + +如果 SJSON 和 GJSON 不符合你的口味,还有[一些](https://github.com/pquerna/ffjson)[其他的](https://github.com/mailru/easyjson)[第三方库](https://github.com/Jeffail/gabs),可以用来在 Go 程序中稍微复杂点地处理 JSON。 + +--- + +via: https://alanstorm.com/simplified-json-handling-in-go/ + +作者:[Alan](https://alanstorm.com/about/) +译者:[lxbwolf](https://github.com/lxbwolf) +校对:[polaris](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/A-Malloc-Idiom-in-Go.md b/published/tech/A-Malloc-Idiom-in-Go.md index 31fab46cb..2028514d5 100644 --- a/published/tech/A-Malloc-Idiom-in-Go.md +++ b/published/tech/A-Malloc-Idiom-in-Go.md @@ -4,7 +4,7 @@ 我终于又开始使用 Go 语言编程了。虽然我在前两年多的时间里积极参与这个项目,但从 2012 年起,我就基本没有参加过这个项目。最初,我之所以做出贡献,是因为我是贝尔实验室 [Plan 9](http://9p.io/plan9/)(操作系统) 和 [FreeBSD](https://www.freebsd.org/) 的粉丝。我喜欢可用的、基于 csp 的语言,但是 Go 最初的版本只能在 Linux 和 OS X 上运行。那时候我只有 FreeBSD 系统,因此,我将编译器工具链、运行时和标准库移植到 FreeBSD (有很多调试成果来自 [Russ Cox](https://swtch.com/~rsc/) )。 -然而,过去我的大部分工作都是在低延迟的系统软件上,它们大部分是用 C 语言编写的,自2007年起,我所有的雇主都不再支持 FreeBSD。因为我并没有真正的机会用 Go 语言去编写新的软件,而且我最终也不再对维护一个操作系统的知识感兴趣,我只是为了好玩,所以我对 Go 语言的使用和贡献都被搁置了。 +然而,过去我的大部分工作都是在低延迟的系统软件上,它们大部分是用 C 语言编写的,自 2007 年起,我所有的雇主都不再支持 FreeBSD。因为我并没有真正的机会用 Go 语言去编写新的软件,而且我最终也不再对维护一个操作系统的知识感兴趣,我只是为了好玩,所以我对 Go 语言的使用和贡献都被搁置了。 现在我在谷歌工作,我终于有机会用 Go 语言写代码了。虽然我仍然喜欢这门语言,但有一些[经验报告](https://github.com/golang/go/wiki/ExperienceReports),例如风格那样的东西结果阻止了我在过去的 5~6 年里使用这门语言,而我现在觉得有些麻烦。在一些同事的建议下,我想我应该至少记录下其中的一个。 @@ -14,7 +14,7 @@ T *p = malloc(sizeof *p); ``` -注意,`p (T *)` 的类型只出现一次。这一行代码利用了它的操作数时的大小,并且显示了一个指针 —— 这其实是没有发生的事情,因为操作符 `sizeof` 的结果必须在编译时是可识别的。这种编程语言而不是语法定义的结果是 `sizeof` 产生了指向对象的大小;它不会变成运行时的东西。C 语言的好处是,如果我改变用 `T` 表示的类型,我只改变声明或定义中的类型。在 `site(s)` 中不需要做任何更改,指针对象被分配给内存分配的结果。上面的示例很简单,让我们考虑一个更复杂的情况,结构成员指向某种类型: +注意,`p (T *)` 的类型只出现一次。这一行代码利用了它的操作数时的大小,并且显示了一个指针 —— 这其实是没有发生的事情,因为操作符 `sizeof` 的结果必须 在编译时是可识别的。这种编程语言而不是语法定义的结果是 `sizeof` 产生了指向对象的大小;它不会变成运行时的东西。C 语言的好处是,如果我改变用 `T` 表示的类型,我只改变声明或定义中的类型。在 `site(s)` 中不需要做任何更改,指针对象被分配给内存分配的结果。上面的示例很简单,让我们考虑一个更复杂的情况,结构成员指向某种类型: ```c struct set { @@ -44,7 +44,7 @@ set_create(size_t sz) } ``` -如果以后我们想要更改 `struct set` 来支持除 int 以外的成员,我们可能会将成员更改为一个union,并添加一些 enum 类型来指定一些我们想要添加的字段。我们可以在不改变 `set_create` 中的任何代码的情况下做到这一点。 +如果以后我们想要更改 `struct set` 来支持除 int 以外的成员,我们可能会将成员更改为一个 union,并添加一些 enum 类型来指定一些我们想要添加的字段。我们可以在不改变 `set_create` 中的任何代码的情况下做到这一点。 每次我使用 Go 语言创建了一些结构类型,当需要嵌入一些像 slice 和 map 那样需要分配内存的字段的时候都让我很抓狂。在 Go 中,我们被迫重复表达我们想要分配的东西的类型,尽管编译器熟知这种类型而且类型推断是符合语言习惯的(试想一下如这样的表达式 `a:= b` ),我有时不得不深究一下嵌入字段的类型是什么。让我们来看看在创建一个嵌入了 map 的结构体所涉及的内容: @@ -59,7 +59,7 @@ func NewNamedMap(name string) *NamedMap { } ``` -我们还可以在 `NewNamedMap` 中使用 `make` ,但是仍然保留了`return &NamedMap{name: name, m: make(map[string]string)}` — 再次,重复它的类型。经过深思熟虑的代码,应该只有一个(额外的)地方需要我们指定类型来分配它,但是当类型改变时,这仍然需要多处改动代码。当我在做原型的时候,这就会让我抓狂,而且我还没有充分考虑到我需要保存在 map 中的状态。我发现在很多地方需要自己手动将 `map[string]string` 更改为 `map[string]T`,每次我需要更改多行代码时,它都会使我感到困扰。 +我们还可以在 `NewNamedMap` 中使用 `make` ,但是仍然保留了 `return &NamedMap{name: name, m: make(map[string]string)}` — 再次,重复它的类型。经过深思熟虑的代码,应该只有一个(额外的)地方需要我们指定类型来分配它,但是当类型改变时,这仍然需要多处改动代码。当我在做原型的时候,这就会让我抓狂,而且我还没有充分考虑到我需要保存在 map 中的状态。我发现在很多地方需要自己手动将 `map[string]string` 更改为 `map[string]T`,每次我需要更改多行代码时,它都会使我感到困扰。 有人可能会说,在写代码之前,我应该多考虑一下我需要什么,那样会更好。但我仍然会反驳说,在项目的生命周期中开发额外的状态需求并不少见,比如在上面的例子中。随着时间的推移,系统的约束也可能会发生变化,这样一种最初非常好的类型最终会变得不可用。在 Go 中,set 结构可能是这样的: @@ -75,7 +75,7 @@ func NewSet(sz int) *Set { } ``` -你可能会问为什么我不只是用一个 slice,答案是这是一个演示这个问题的简单例子。不管怎样,我们以后可能想要支持不同类型的 slice,那样我们又遇到了之前的问题。set 中如果有一个 slice,我们可能可以忽略初始化,假设我们在添加后只从 slice 中读取。由于形如 map 和 channel 那样的类型,因为我们必须在使用前进行分配,所以会使得情况更加复杂。那么在某个地方重复输入信息并不罕见。 +你可能会问为什么我不只是用一个 slice,答案是这是一个演示这个问题的简单例子。不管怎样,我们以后可能想要支持不同类型的 slice,那样我们又遇到了之前的问题。set 中如果有一个 slice,我们可能可以忽略初始化,假设我们在添加后只从 slice 中读取。由于形如 map 和 channel 那样的  类型,因为我们必须在使用前进行分配,所以会使得情况更加复杂。那么在某个地方重复输入信息并不罕见。 我不知道该如何解决这个问题。对于复合文本,可能可以添加如下语法: @@ -99,9 +99,9 @@ return &Set{cap: sz, members: make(Set.members)} 我不知道改变新的有相似的行为是有用的还是有价值的;我怀疑它的使用是不寻常的。在任何情况下,我都清楚这将减少重构软件以及编写新的软件的开销。 -这个问题并不是那么糟糕,但修复它肯定会让我觉得更好。在很多情况下,Go已经比 C 和 C++ (我至今无法忍受) 更有表现力了。我认为,如果在语言中添加了对分配类型的推断的支持,那么 Go 语言对 C 系统程序员来说就会更有吸引力,因为除了 GC ,他们还有其他坚持的理由。(就我个人而言,我还希望看到一个关于支持系统级并发的更好的事情,但最好是单独发布。) +这个问题并不是那么糟糕,但修复它肯定会让我觉得更好。在很多情况下,Go 已经比 C 和 C++ (我至今无法忍受) 更有表现力了。我认为,如果在语言中添加了对分配类型的推断的支持,那么 Go 语言对 C 系统程序员来说就会更有吸引力,因为除了 GC ,他们还有其他坚持的理由。(就我个人而言,我还希望看到一个关于支持系统级并发的更好的事情,但最好是单独发布。) -我编辑了这篇文章,以修复 C 示例中的一个错误。当我最初编写这个示例时,`struct set` 没有使用灵活的数组成员。Anmol Sethi 写信询问这个特性,并指出我错误地分配和再次分配给了FAM。我忘记了要删除那些代码。 +我编辑了这篇文章,以修复 C 示例中的一个错误。当我最初编写这个示例时,`struct set` 没有使用灵活的数组成员。Anmol Sethi 写信询问这个特性,并指出我错误地分配和再次分配给了 FAM。我忘记了要删除那些代码。 嗷. diff --git a/published/tech/Beauty-Of-Go.md b/published/tech/Beauty-Of-Go.md index c4f0c2c82..56ed2e84f 100644 --- a/published/tech/Beauty-Of-Go.md +++ b/published/tech/Beauty-Of-Go.md @@ -4,11 +4,11 @@ 最近,我在做兴趣项目的时候开始探索 Go 语言,被 Go 语言的美征服了。 -Go语言的美在于它在灵活使用(常见于一些动态,解释型语言)和安全性能(常见于一些静态,编译语言)之间有一个很好的平衡。 +Go 语言的美在于它在灵活使用(常见于一些动态,解释型语言)和安全性能(常见于一些静态,编译语言)之间有一个很好的平衡。 除此之外,还有另外的两个功能让我觉得 Go 语言非常适合现代的软件开发。我会在之下优势的部分阐述。 -其中之一是 **对语言并发性的一流支持**(通过 `goroutine`,和 `channels` 实现,下面解释)。 并发,通过其设计,使您能够有效地使用您的 CPU 马力。 即使您的处理器只有1个内核,并发的设计也能让您高效地使用该内核。 这就是为什么您通常可以在单台机器上运行数十万个并发 `goroutines`(轻量级线程)的原因。 `channels` 和 `goroutines` 是分布式系统的核心,因为它们抽象了生产者 - 消费者的消息范例。 +其中之一是 **对语言并发性的一流支持**(通过 `goroutine`,和 `channels` 实现,下面解释)。 并发,通过其设计,使您能够有效地使用您的 CPU 马力。 即使您的处理器只有 1 个内核,并发的设计也能让您高效地使用该内核。 这就是为什么您通常可以在单台机器上运行数十万个并发 `goroutines`(轻量级线程)的原因。 `channels` 和 `goroutines` 是分布式系统的核心,因为它们抽象了生产者 - 消费者的消息范例。 我非常喜欢 Go 的另一个特性是接口 `(Interface)` 。 **接口为您的系统提供松耦合或分离组件。** 这意味着你的代码的一部分可以只依赖于接口类型,并不关心谁实现了接口或接口是如何实现的。 然后,您的控制器可以提供一个满足接口(实现接口中的所有功能)的代码的依赖关系。 这也为单元测试提供了一个非常干净的架构(通过依赖注入)。 现在,您的控制器可以注入代码所需的接口的模拟实现,以便能够测试它是否正确地执行其工作。 @@ -17,14 +17,14 @@ Go语言的美在于它在灵活使用(常见于一些动态,解释型语言 在这篇文章中,我将讨论该语言的以下几个方面: a)介绍 -b)为什么需要Go +b)为什么需要 Go c)目标受众 -d)Go的优势 -e)Go的弱点 -f)走向2 -g)Go的设计理念 +d)Go 的优势 +e)Go 的弱点 +f)走向 2 +g)Go 的设计理念 h)如何开始 -i)谁在使用Go +i)谁在使用 Go ## 介绍 @@ -38,7 +38,7 @@ Rob Pike 提到 Go 编程语言的目的: > “因此,Go 的目的不是研究编程语言设计; 它是为了改善设计师和同事的工作环境。 Go 比编程语言研究更关注软件工程。 或者换句话说,就是为软件工程师服务的语言设计。” -**困扰Google软件工程视野的问题**(摘自(https://talks.golang.org/2012/splash.article)): +**困扰 Google 软件工程视野的问题**(摘自(https://talks.golang.org/2012/splash.article)): a)缓慢的构建 - 构建有时需要一个小时才能完成 b)不受控制的依赖 @@ -50,11 +50,11 @@ g)版本歪斜 h)编写自动工具的难度 i)跨语言构建 -**为了成功,Go必须解决这些问题**(摘自(https://talks.golang.org/2012/splash.article)): +**为了成功,Go 必须解决这些问题**(摘自(https://talks.golang.org/2012/splash.article)): -a)Go必须能大规模的使用,用于多人的大组,并且适用于有大量依赖程序的项目。 -b)Go的语法必须是让人熟悉的,大致类C。 谷歌需要在Go中快速提高程序员的效率,这意味着语言的语法的变化不能太激进。 -c)Go必须是现代的。 它应该具有像并发这样的功能,以便程序可以高效地使用多核机器。 它应该有内置的网络和Web服务器库,以便它有助于现代化的发展。 +a)Go 必须能大规模的使用,用于多人的大组,并且适用于有大量依赖程序的项目。 +b)Go 的语法必须是让人熟悉的,大致类 C。 谷歌需要在 Go 中快速提高程序员的效率,这意味着语言的语法的变化不能太激进。 +c)Go 必须是现代的。 它应该具有像并发这样的功能,以便程序可以高效地使用多核机器。 它应该有内置的网络和 Web 服务器库,以便它有助于现代化的发展。 ## 目标听众 @@ -64,9 +64,9 @@ Go 是一种系统编程语言。 对于诸如云系统(网络服务器,缓 **a)静态类型:** Go 是静态类型的。 这意味着您需要在编译时为所有变量和函数参数(以及返回变量)声明类型。 虽然这听起来不方便,但这有一个很大的优势,因为在编译时本身会发现很多错误。 当你的团队规模增加时,这个因素起着非常重要的作用,因为声明的类型使得函数和库更易读,更容易理解。 -**b)编译速度:**Go 代码编译速度**非常快**,因此您无需继续等待代码编译。 实际上,`go run` 命令会很快启动你的Go程序,所以你甚至不会觉得你的代码是先编译好的。 这感觉就像一种解释性语言。 +**b)编译速度:**Go 代码编译速度**非常快**,因此您无需继续等待代码编译。 实际上,`go run` 命令会很快启动你的 Go 程序,所以你甚至不会觉得你的代码是先编译好的。 这感觉就像一种解释性语言。 -**c)执行速度:** 根据操作系统(Linux/Windows/Mac)和代码正在编译的机器的 CPU 指令集体系结构(x86,x86-64,arm等),Go 代码直接编译为机器代码。 所以,它运行速度非常快。 +**c)执行速度:** 根据操作系统(Linux/Windows/Mac)和代码正在编译的机器的 CPU 指令集体系结构(x86,x86-64,arm 等),Go 代码直接编译为机器代码。 所以,它运行速度非常快。 **d)便携式:** 由于代码直接编译为机器码,因此,二进制文件变得便携。 这里的可移植性意味着你可以从你的机器(比如 Linux,x86-64)获取二进制文件并直接在你的服务器上运行(如果你的服务器也在 x86-64 架构上运行 Linux)。 @@ -80,7 +80,7 @@ Go 是一种系统编程语言。 对于诸如云系统(网络服务器,缓 这种方法有两个好处: -1. 初始化时的 `Goroutine` 具有 2KB 的堆栈。与一个一般为 1 MB 的 OS 线程堆栈相比,这非常小巧。当你需要同时运行几十万个不同的 goroutine 时,这个数字很重要。如果你要并行运行数千个 OS 线程,RAM 显然将成为瓶颈。 +1. 初始化时的 `Goroutine` 具有 2KB 的堆栈。与一个一般为 1 MB 的 OS 线程堆栈相比,这非常小巧。当你需要同时运行几十万个不同的 Goroutine 时,这个数字很重要。如果你要并行运行数千个 OS 线程,RAM 显然将成为瓶颈。 2. Go 可以遵循与 Java 等其他语言相同的模型,它支持与 OS 线程相同的线程概念。但是在这种情况下,OS 线程之间的上下文切换成本比不同的 `goroutine` 之间的上下文切换成本要大得多。 @@ -98,7 +98,7 @@ Go 是一种系统编程语言。 对于诸如云系统(网络服务器,缓 **h)不报错异常,自己处理错误:** 我喜欢 Go 没有其他语言具有的标准异常逻辑。去强迫开发人员处理“无法打开文件”等基本错误,而不是让他们将所有代码包装在 try catch 块中。这也迫使开发人员考虑需要采取什么措施来处理这些故障情况。 -**i)惊人的工具**:关于Go的最好方面之一是它的工具。它有如下工具: +**i)惊人的工具**:关于 Go 的最好方面之一是它的工具。它有如下工具: i) [Gofmt](https://blog.golang.org/go-fmt-your-code):它会自动格式化和缩进你的代码,这样你的代码看起来就像这个星球上的每个 Go 开发者一样。这对代码可读性有巨大的影响。 @@ -126,7 +126,7 @@ Go 的开发有很多进展。你可以在[这里](https://github.com/avelino/aw ## 弱点 -**1.泛型的缺乏** - 泛型让我们在稍后指定待指定的类型时设计算法。假设您需要编写一个函数来对整数列表进行排序。稍后,您需要编写另一个函数来排序字符串列表。在那一刻,你意识到代码几乎看起来一样,但你不能使用原始函数,因为函数可以将一个整数类型列表或一个类型字符串列表作为参数。这将需要代码重复。因此,泛型允许您围绕稍后可以指定的类型设计算法。您可以设计一个算法来排序T类型的列表。然后,您可以使用整数/字符串/任何其他类型调用相同的函数,因为存在该类型的排序函数。这意味着编译器可以检查该类型的一个值是否大于该类型的另一个值(因为这是排序所需的) +**1.泛型的缺乏** - 泛型让我们在稍后指定待指定的类型时设计算法。假设您需要编写一个函数来对整数列表进行排序。稍后,您需要编写另一个函数来排序字符串列表。在那一刻,你意识到代码几乎看起来一样,但你不能使用原始函数,因为函数可以将一个整数类型列表或一个类型字符串列表作为参数。这将需要代码重复。因此,泛型允许您围绕稍后可以指定的类型设计算法。您可以设计一个算法来排序 T 类型的列表。然后,您可以使用整数/字符串/任何其他类型调用相同的函数,因为存在该类型的排序函数。这意味着编译器可以检查该类型的一个值是否大于该类型的另一个值(因为这是排序所需的) 通过使用语言的空接口 `(interface {})` 功能,可以在 Go 中实现某种通用机制。但是,这并不理想。 @@ -134,7 +134,7 @@ Go 的开发有很多进展。你可以在[这里](https://github.com/avelino/aw 这就是说,Go 的作者在 Go 中表达了对实施某种泛型机制的开放性。但是,这不仅仅是泛型。泛型只有在语言的所有其他功能都能正常工作时才能实现。让我们拭目以待 Go 2 是否为他们提供了某种解决方案。 -**2.缺乏依赖管理** - Go1 的承诺意味着 Go 语言及其库在 Go 1 的生命周期中不能更改其API。这意味着您的源代码将继续为 Go 1.5 和Go 1.9 编译。因此,大多数第三方 Go 库也遵循相同的承诺。由于从 GitHub 获得第三方库的主要方式是通过 'go get' 工具,因此,当您执行 `go get github.com/vendor/library` 时,您希望他们主分支中的最新代码不会更改库 API。虽然这对临时项目很酷,因为大多数库没有违背承诺,但这对于生产部署并不理想。 +**2.缺乏依赖管理** - Go1 的承诺意味着 Go 语言及其库在 Go 1 的生命周期中不能更改其 API。这意味着您的源代码将继续为 Go 1.5 和 Go 1.9 编译。因此,大多数第三方 Go 库也遵循相同的承诺。由于从 GitHub 获得第三方库的主要方式是通过 'go get' 工具,因此,当您执行 `go get github.com/vendor/library` 时,您希望他们主分支中的最新代码不会更改库 API。虽然这对临时项目很酷,因为大多数库没有违背承诺,但这对于生产部署并不理想。 理想情况下应该有一些依赖版本的方法,这样你就可以简单地在你的依赖文件中包含第三方库的版本号。即使他们的 API 改变了,你也不需要担心,因为新的 API 将带有更新的版本。您稍后可以回头查看所做的更改,然后决定是否升级您的依赖文件中的版本并根据 API 接口中的更改更改您的客户端代码。 @@ -154,7 +154,7 @@ d)可能的话,提出如何解决问题的解决方案 在我看来,语言的两个最紧迫的问题是泛型和依赖管理。依赖管理更多的是发布工程或工具问题。希望我们会看到 [dep](https://github.com/golang/dep)(官方实验)成为解决问题的官方工具。鉴于作者已经表达了对该语言的泛型的开放性,我很好奇他们是如何实现它们的,因为泛型以编译时间或执行时间为代价。 -## Go的设计理念 +## Go 的设计理念 在 Rob Pike 的的 talk [“简单就是复杂”](https://talks.golang.org/2015/simplicity-is-complicated.slide#18)中,他提到的一些设计理念让我觉得很有亮点。 @@ -170,9 +170,9 @@ d)**可读性也意味着可靠性。** 如果一门语言很复杂,你就 ## 如何开始 -您可以下载Go并按照此处的安装说明进行操作。 +您可以下载 Go 并按照此处的安装说明进行操作。 -这里是开始使用Go的[官方指南](https://tour.golang.org/welcome/1)。[Go by example](https://gobyexample.com/)也是一本好书。 +这里是开始使用 Go 的[官方指南](https://tour.golang.org/welcome/1)。[Go by example](https://gobyexample.com/)也是一本好书。 如果你想读一本书,[The Go Programming Language](https://www.amazon.com/Programming-Language-Addison-Wesley-Professional-Computing/dp/013419044)是一个很好的选择。 它的编写方式与传说中的[C 语言白皮书](https://www.amazon.com/Programming-Language-2nd-Brian-Kernighan/dp/0131103628)相似,由[Alan A. A. Donovan](https://www.informit.com/authors/bio/cd7c1e12-138d-4bf9-b609-e12e5a7fa866)和[Brian W. Kernighan](https://en.wikipedia.org/wiki/Brian_Kernighan)撰写。 @@ -226,7 +226,7 @@ SendGrid - [如何说服您的公司与 Golang 一起使用](https://sendgrid.co Shopify - [Twitter](https://twitter.com/burkelibbey/status/312328030670450688) -SoundCloud - [进入SoundCloud](https://developers.soundcloud.com/blog/go-at-soundcloud) +SoundCloud - [进入 SoundCloud](https://developers.soundcloud.com/blog/go-at-soundcloud) SourceGraph - [YouTube](https://www.youtube.com/watch?v=-DpKaoPz8l8) diff --git a/published/tech/Blocks-in-Go.md b/published/tech/Blocks-in-Go.md index e2654f23a..091e0cc10 100644 --- a/published/tech/Blocks-in-Go.md +++ b/published/tech/Blocks-in-Go.md @@ -154,7 +154,7 @@ LOOP: via: https://medium.com/golangspec/blocks-in-go-2f68768868f6 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[sunzhaohao](https://github.com/sunzhaohao) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/Clean-Architecture-using-Golang.md b/published/tech/Clean-Architecture-using-Golang.md index f96dbb709..f994daf72 100644 --- a/published/tech/Clean-Architecture-using-Golang.md +++ b/published/tech/Clean-Architecture-using-Golang.md @@ -150,10 +150,10 @@ type Service interface { } ``` -最后一层,我们架构中的 Controller 是在 api 的内容中实现的: +最后一层,我们架构中的 Controller 是在 API 的内容中实现的: ``` -cd api ; tree +cd API ; tree . |____handler | |____company.go diff --git a/published/tech/Composite-literals-in-Go.md b/published/tech/Composite-literals-in-Go.md index 9df79d51a..9c9c7aca2 100644 --- a/published/tech/Composite-literals-in-Go.md +++ b/published/tech/Composite-literals-in-Go.md @@ -4,7 +4,7 @@ ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/composite-literal/1_TM61VTlvvL2YWtI6UUYLOg.png) -在源代码中字面量可以描述像数字,字符串,布尔等类型的固定值。Go 和 JavaScript、Python 语言一样,即便是复合类型(数组,字典,切片,结构体)也允许使用字面量。Golang 的复合字面量表达也很方便、简洁,使用单一语法即可实现。在 JavaScript 中它是这样的: +在源代码中字面量可以描述像数字,字符串,布尔等类型的  固定值。Go 和 JavaScript、Python 语言一样,即便是复合类型(数组 ,字典,切片,结构体)也允许使用  字面量。Golang 的复合字面量表达也  很方便、简洁,使用单一语法即可实现。在 JavaScript 中它  是这样的: ```javascript var numbers = [1, 2, 3, 4] @@ -32,10 +32,10 @@ thing := Thing{"Raspberry Pi", 2, "B"} thing = Thing{name: "Raspberry Pi", generation: 2, model: "B"} ``` -除了字典类型以外的其他类型,键是可选的,便于理解没有歧义: +除了字典类型以外的其他类型,键是可选的, 便于理解没有歧义: -* 对结构体而言,键就是项名称 -* 对于数组或切片而言,键就是索引 +*  对结构体而言,键就是项名称 +* 对于数组  或切片而言,键就是索引 键不是字面量常量,就必须是常量表达式;因此这种写法是错误的: @@ -52,7 +52,7 @@ elements := []string{0: "zero", 1: "one", 4 / 2: "two"} 编译一切顺利。 -重复的键是不允许的: +重复的键是  不允许的: ```go elements := []string{ 0: "zero", @@ -68,16 +68,16 @@ elements := []string{ type S struct { name string } -s := S{name: "Michał", name: "Michael"} +s := S{name: "Micha ł", name: "Michael"} ``` 编译结果是 "duplicate field name in struct literal: name" 的错误。 -相应的字面量必须被赋值给相应的键,元素或结构体的项。更多关于可赋值性的内容可查看 ["Go 语言中的可赋值性"](https://studygolang.com/articles/12381)一文。 +相应的字面量必须被赋值给相应的键,元素或结构体的项。更多关于  可赋值性的内容可查看 ["Go 语言中的可赋值性"](https://studygolang.com/articles/12381)一文。 ## 结构体 -对于结构体类型定义的项,这里有两三个创建实例时的规定。 +对于结构体类型定义的项,这里有两三个创建实例时的  规定。 像下面的代码片段,结构体的定义必须指定内部项的名称,并且如果使用了那些定义以外的名称,那么在编译时会发生错误:"unknown S field ‘name’ in struct literal": @@ -85,10 +85,10 @@ s := S{name: "Michał", name: "Michael"} type S struct { age int8 } -s := S{name: "Michał"} +s := S{name: "Micha ł"} ``` -如果最先的字面量有对应的键,那么之后的字面量也必须有对应的键,下面这样写是不合理的 +如果最先的字面量有对应的键,那么  之后的字面量也必须有对应的键,下面这样写是不合理的 : ```go @@ -96,13 +96,13 @@ type S struct { name string age int8 } -s := S{name: "Michał", 29} +s := S{name: "Micha ł", 29} ``` -像这样,编译器会抛出异常"mixture of field:value and value initializers",可以通过省略结构体中所有元素对应的键来更正它。 +像这样, 编译器会抛出异常"mixture of field:value and value initializers",可以通过省略结构体中所有元素  对应的键来  更正它。 ```go -s := S{"Michał", 29} +s := S{"Micha ł", 29} ``` 但这样又有一个附加限制:字面量的依次顺序必须与结构体定义时各项的顺序保持一致。 @@ -113,24 +113,24 @@ type S struct { name string age int8 } -s := S{name: "Michał"} +s := S{name: "Micha ł"} fmt.Printf("%#v\n", s) ``` 输出: ```go -main.S{name:"Michał", age:0} +main.S{name:"Micha ł", age:0} ``` -只有使用键:值的形式初始化结构体时,才会有默认赋零值的操作: +只有使用键:值的形式初始化结构体时,才会有默认  赋零  值的操作: ```go -s := S{"Michał"} +s := S{"Micha ł"} ``` -这种写法是不能编译通过的,会抛出异常"too few values in struct initializer"。当有新的项被添加到结构体的一个被字面量赋值的项之前时,这种报错的做法对程序员更加安全--如果这个结构体中,一个名为"title"的字符串类型的项被加到了"name"项之前,那么值"Michał"将被认为是一个"title",而这个问题是很难被排查出来的。 -如果结构体字面量是空值,那么结构体内每一项都会被赋于零值: +这种写法是不能编译通过的,会  抛出异常"too few values in struct initializer"。当有新的项被添加到结构体的一个被字面量赋值的项之前时,这种报错的做法对程序员更加安全-- 如果这个结构体中,一个名为"title"的字符串类型的项被加到了"name"项之前,那么值"Micha ł"将被认为是一个"title",而这个问题是很难被排查出来的。 +如果结构体字面量是空值,那么结构体内每一项都会被  赋于零值: ```go type Employee struct { @@ -145,7 +145,7 @@ type S struct { main.S{name:"", age:0, Employee:main.Employee{department:"", position:""}} ``` -最后一个规定,结构体的赋值与[导出标示](https://studygolang.com/articles/12809)有关(简而言之,字面量不允许给非导出项赋值) +最后一个规定,结构体的赋值与 [导出标示](https://studygolang.com/articles/12809)有关(简而言之,字面量不允许  给非导出项赋值) ## 数组和切片 @@ -157,21 +157,21 @@ fmt.Printf("%#v\n", numbers) []string{"a", "b", "", "", "c", "d"} ``` -赋值元素的数量可以小于数组的长度(被忽略的元素将会被赋值为零): +赋值元素的数量可以小于数组的长度( 被忽略的  元素将会被赋值为零): ```go fmt.Printf("%#v\n", [3]string{"foo", "bar"}) [3]string{"foo", "bar", ""} ``` -不允许对超出范围的索引赋值,所以下面几行代码是无效的: +不允许对超出范围的索引赋值,所以下面几行代码  是无效的: ``` [1]string{"foo", "bar"} [2]string{1: "foo", "bar"} ``` -可以通过使用三个点(...)的快捷符号来省去程序员声明数组长度的工作,编译器会通过索引最大值加一的方式获得它: +可以通过使用三个点(...)的快捷符号来  省去程序员声明数组长度的工作, 编译器会通过索引最大值加一的方式获得  它: ```go elements := […]string{2: "foo", 4: "bar"} @@ -184,7 +184,7 @@ fmt.Printf("%#v, length=%d\n", elements, len(elements)) [5]string{"", "", "foo", "", "bar"}, length=5 ``` -切片与之前数组内容基本一致: +切片与之前数组  内容基本一致: ```go els := []string{2: "foo", 4: "bar"} @@ -207,7 +207,7 @@ constants := map[string]float64{"euler": 2.71828, "pi": .1415926535} ## 快捷方式 -如果作为字典键或者数组、切片、字典元素的字面量的类型与键或元素的类型一致,那么为了简洁,这个类型可以省略不写: +如果  作为  字典键或者数组、切片、字典元素的字面量的类型与键或元素的类型一致,那么为了简洁,这个类型可以  省略不写: ```go coords := map[[2]byte]string{{1, 1}: "one one", {2, 1}: "two one"} @@ -215,13 +215,13 @@ type Engineer struct { name string age byte } -engineers := [...]Engineer{{"Michał", 29}, {"John", 25}} +engineers := [...]Engineer{{"Micha ł", 29}, {"John", 25}} ``` -同样,如果键或元素是指针类型的话,&T也可以被省略: +同样,如果  键或元素是指针类型的话,&T 也可以被省略: ```go -engineers := […]*Engineer{{"Michał", 29}, {"John", 25}} +engineers := […]*Engineer{{"Micha ł", 29}, {"John", 25}} fmt.Printf("%#v\n", engineers) ``` @@ -238,7 +238,7 @@ https://golang.org/ref/spec#Composite_literals via: https://medium.com/golangspec/composite-literals-in-go-10dc62eec06a -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[yiyulantian](https://github.com/yiyulantian) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/Debugging-an-evil-Go-runtime-bug.md b/published/tech/Debugging-an-evil-Go-runtime-bug.md index 3739579de..2dee830f3 100644 --- a/published/tech/Debugging-an-evil-Go-runtime-bug.md +++ b/published/tech/Debugging-an-evil-Go-runtime-bug.md @@ -124,9 +124,9 @@ github.com/prometheus/node_exporter/vendor/github.com/prometheus/client_golang/p 但是,我还可以做另外一件事。既然这个问题会因为温度变高而更严重。那如果我加热内存的话会发生什么呢? ![Memtest86+](https://raw.githubusercontent.com/studygolang/gctt-images/master/debug-runtime-bug/badram.jpg) -![A cozy 100℃](https://raw.githubusercontent.com/studygolang/gctt-images/master/debug-runtime-bug/badram_t.jpg) +![A cozy 100 ℃](https://raw.githubusercontent.com/studygolang/gctt-images/master/debug-runtime-bug/badram_t.jpg) -我用一个温度为 130℃ 的加热枪同时加热了两个内存模块(我的笔记本一共有四个 `SODIMM` 插槽,另外两个在背壳后面)。我按照模块顺序来检查,陆续发现了另外三个只能在高温环境下才能检测到的坏比特,它们分布在三个笔记本的三个内存条上。 +我用一个温度为 130 ℃ 的加热枪同时加热了两个内存模块(我的笔记本一共有四个 `SODIMM` 插槽,另外两个在背壳后面)。我按照模块顺序来检查,陆续发现了另外三个只能在高温环境下才能检测到的坏比特,它们分布在三个笔记本的三个内存条上。 我还发现这些错误的地址很一致,即使在我交换这些模块之后。地址的高位比特都是相同的。这是因为内存是交错的,数据会分布在四个内存条上。这很方便,因为我可以把所有可能出错的内存比特地址都划到一个范围内,不用担心我可能在将来交换内存条搞错了掩码。我发现划掉一个相邻的 128KiB 区域就可以覆盖所有已知的损坏比特,为了保险,我最后划掉了相邻的 1MiB。所以我把三个 1MiB 对齐的内存块标记为坏内存块(其中一个包含了两个坏比特,加起来共有四个): @@ -273,20 +273,20 @@ done extra= if [ ! -z "$doit" ]; then sha="$(echo -n "$objfile" | sha1sum - | cut -d" " -f1)" - echo "${sha:0:8} $objfile" >> objs.txt + Echo "${sha:0:8} $objfile" >> objs.txt if [ $((0x${sha:0:8} & (0x80000000 >> $BIT))) = 0 ]; then - echo "[n]" "$objfile" 1>&2 + Echo "[n]" "$objfile" 1>&2 else extra=-DCONFIG_OPTIMIZE_INLINING - echo "[y]" "$objfile" 1>&2 + Echo "[y]" "$objfile" 1>&2 fi fi exec gcc $extra "${args[@]}" ``` -这个脚本使用 `SHA-1` 算法计算每个 object 文件的哈希值,然后在前 32 位比特中任取一位,如果这个比特是 0, 就关闭 `CONFIG_OPTIMIZE_INLINING` 来编译。如果这个比特是 1,就打开`CONFIG_OPTIMIZE_INLINING` 来编译。我观察到现在的内核大概有 685 个 object 文件(我之前的最小化内核工作取得了成效),这写文件可能需要十个比特位来编号。这个方法还有一个好处是:我只需要关注会崩溃的内核,这可比证明一个内核不会崩溃简单多了。 +这个脚本使用 `SHA-1` 算法计算每个 object 文件的哈希值,然后在前 32 位比特中任取一位,如果这个比特是 0, 就关闭 `CONFIG_OPTIMIZE_INLINING` 来编译。如果这个比特是 1,就打开 `CONFIG_OPTIMIZE_INLINING` 来编译。我观察到现在的内核大概有 685 个 object 文件(我之前的最小化内核工作取得了成效),这写文件可能需要十个比特位来编号。这个方法还有一个好处是:我只需要关注会崩溃的内核,这可比证明一个内核不会崩溃简单多了。 -我以 `SHA-1` 哈希串前缀中的每一个比特位为标志位,花29分钟编译了 32 个内核。然后我开始测试他们是否会崩溃,每次我测试出一个会崩溃的内核,我就用一个正则表达式来表达可能的 `SHA-1` 值(在指定位数是 0 的值)。经过八次崩溃之后,我已经能锁定到 4 个 object 文件了。当我测试到第十次崩溃时,就只有一个匹配的 object 文件了。 +我以 `SHA-1` 哈希串前缀中的每一个比特位为标志位,花 29 分钟编译了 32 个内核。然后我开始测试他们是否会崩溃,每次我测试出一个会崩溃的内核,我就用一个正则表达式来表达可能的 `SHA-1` 值(在指定位数是 0 的值)。经过八次崩溃之后,我已经能锁定到 4 个 object 文件了。当我测试到第十次崩溃时,就只有一个匹配的 object 文件了。 ``` $ grep '^[0246][012389ab][0189][014589cd][028a][012389ab][014589cd]' objs_0.txt 6b9cab4f arch/x86/entry/vdso/vclock_gettime.o diff --git a/published/tech/Dynamic-JSON-in-Go.md b/published/tech/Dynamic-JSON-in-Go.md index a4cd1f5df..2990eee78 100644 --- a/published/tech/Dynamic-JSON-in-Go.md +++ b/published/tech/Dynamic-JSON-in-Go.md @@ -4,7 +4,7 @@ Go 语言是静态类型语言,虽然它也可以表现出动态类型,但是使用一个嵌套的 `map[string]interface{}` 在那里乱叫会让代码变得特别丑。通过掌握语言的静态特性,我们可以做的更好。 -通过同一通道交换多种信息的时候,我们经常需要 JSON 具有动态的,或者更合适的参数内容。首先,让我们来讨论一下消息封装(message envelopes),JSON 在这里看起来就像这样: +通过同一通道交换多种信息的时候,我们经常需要 JSON 具有动态的,或者更合适的参数内容。 首先,让我们来讨论一下消息  封装(message envelopes),JSON 在这里看起来就像这样: ```json { @@ -15,7 +15,7 @@ Go 语言是静态类型语言,虽然它也可以表现出动态类型,但 ## 通过不同的消息类型生成 JSON -通过 `interface{}`,我们可以很容易的将数据结构编码成为独立封装的,具有多种类型的消息体的 JSON 数据。为了生成下面的 JSON : + 通过 `interface{}`,我们可以很容易的将数据结构编码成为独立封装的,具有多种类型的消息体的 JSON 数据。为了生成下面的 JSON : ```json { @@ -35,7 +35,7 @@ Go 语言是静态类型语言,虽然它也可以表现出动态类型,但 } } ``` -我们可以使用这些 Go 类型: +我们可以使用  这些 Go 类型: ```go package main @@ -151,9 +151,9 @@ type Envelope { ``` [`json.RawMessage`](http://golang.org/pkg/encoding/json/#RawMessage) 非常有用,它可以让你延迟解析相应的 JSON 数据。它会将未处理的数据存储为 `[]byte`。 -这种方式可以让你显式控制 `Msg` 的解析。从而延迟到获取到 `Type` 的值之后,依据 `Type` 的值进行解析。这种方式不好的地方在于你需要先明确解析 `Msg`,或者你需要单独分为 `EnvelopeIn` 和 `EnvelopeOut` 两种类型,其中 `EnvelopeOut` 仍然有 `Msg interface{}`。 +这种方式可以让你  显式控制 `Msg` 的解析。从而延迟到获取到 `Type` 的值之后,依据 `Type` 的值进行解析。这种方式不好的地方在于你需要先明确解析 `Msg`,或者你需要单独分为 `EnvelopeIn` 和 `EnvelopeOut` 两种类型,其中 `EnvelopeOut` 仍然有 `Msg interface{}`。 -## 结合 `*json.RawMessage` 和 `interface{}` 的优点 +## 结合 `*json.RawMessage`  和 `interface{}` 的  优点 那么如何将上述两者好的一面结合起来呢?通过在 `interface{}` 字段中放入 `*json.RawMessage`! @@ -215,7 +215,7 @@ dynamite ## 如何把所有数据都放在最外层(顶层) -虽然我极其推荐你将动态可变的部分放在一个单独的 key 下面,但是有时你可能需要处理一些预先存在的数据,它们并没有用这样的方式进行格式化。 +虽然我  极其推荐你将动态可变的部分放在一个单独的 key 下面,但是有时你可能需要处理一些预先存在的数据,它们并没有用这样的方式进行格式化。 如果可以的话,请使用文章前面提到的风格。 diff --git a/published/tech/Exploring-vgo.md b/published/tech/Exploring-vgo.md index ff04f7a9d..51f515f30 100644 --- a/published/tech/Exploring-vgo.md +++ b/published/tech/Exploring-vgo.md @@ -2,7 +2,7 @@ # 探索 vgo -昨天,Russ Cox 发布了 [vgo](https://research.swtch.com/vgo),作为一个现有 go 构建命令的继任者,添加了一直缺失的包版本管理功能。虽然它只是一个大胆的尝试,但是在大家都认为 [dep](https://github.com/golang/dep) 将要成为 Go 语言官方正式的包管理工具的时候,它的出现多少让大家有点意外。Russ 写的 [vgo 简介](https://research.swtch.com/vgo-intro) 和一起发布的 [vgo 使用指南](https://research.swtch.com/vgo-tour) 是了解 vgo 很好的参考资料,尽管许多人对文章中的一些观点有些误解,我还是强烈建议第一次接触 vgo 的朋友读一下。(译注:vgo 的系列文章,Go 中文网专栏有中译文,地址:https://studygolang.com/subject/52) +昨天,Russ Cox 发布了 [vgo](https://research.swtch.com/vgo),作为一个现有 Go 构建命令的继任者,添加了一直缺失的包版本管理功能。虽然它只是一个大胆的尝试,但是在大家都认为 [dep](https://github.com/golang/dep) 将要成为 Go 语言官方正式的包管理工具的时候,它的出现多少让大家有点意外。Russ 写的 [vgo 简介](https://research.swtch.com/vgo-intro) 和一起发布的 [vgo 使用指南](https://research.swtch.com/vgo-tour) 是了解 vgo 很好的参考资料,尽管许多人对文章中的一些观点有些误解,我还是强烈建议第一次接触 vgo 的朋友读一下。(译注:vgo 的系列文章,Go 中文网专栏有中译文,地址:https://studygolang.com/subject/52) 第一次读那篇文章的时候,我和大家一样,对文章里提到的一些观点比较困惑,总是觉得哪里有些不对,但是又不能很明确的表达出来。为了更加深入地理解 vgo 是什么,我决定在读 [vgo 使用指南](https://research.swtch.com/vgo-tour) 的同时,下载 vgo 的源码,做一些代码测试。大家可以从 Github 上下载到我在这期间写的一些 [代码](https://github.com/joncalhoun?tab=repositories) @@ -88,13 +88,13 @@ vgo 确定应该使用哪个版本是根据 [版本规范](https://semver.org/) Russ Cox 在 [golang-nuts 邮件列表](https://groups.google.com/forum/#!topic/golang-nuts/jFPz5yZCPcQ) 的一个回复中也提到了这一点 -> 在 [vgo 使用指南](https://research.swtch.com/vgo-tour) 中, 我提到了 「all」 被重新定义了,变得更加实用了。所以在尝试更新依赖包时,它是一个特别好用的工具,运行 go test all 后,如果通过了全部的测试而且你相信你写的测试用例,那么就保存对 go.mod 的修改。一些人甚至会构建一个 bot 来自动做这些事情。我不认为最小版本选择意味着你必须用旧版本,它只是不会自动这样做,除非你手动指定并且准备好去体验新版本的功能有多好。而不是运行了 ge get,然后所有版本一下就都更新了。 +> 在 [vgo 使用指南](https://research.swtch.com/vgo-tour) 中, 我提到了 「all」 被重新定义了,变得更加实用了。所以在尝试更新依赖包时,它是一个特别好用的工具,运行 Go test all 后,如果通过了全部的测试而且你相信你写的测试用例,那么就保存对 go.mod 的修改。一些人甚至会构建一个 bot 来自动做这些事情。我不认为最小版本选择意味着你必须用旧版本,它只是不会自动这样做,除非你手动指定并且准备好去体验新版本的功能有多好。而不是运行了 ge get,然后所有版本一下就都更新了。 ### 包会变得难以维护 问题是这样的:作为一个包管理工具,如果当包有新版本发布时,不能自动更新到这个版本,用户就不能使用新的功能 -作为一种对比,即使包管理工具把依赖包升级到了最新版本,同样会导致一些问题。比如说我们的项目中引用了包 turtle, 而它依赖包 foo,当包 foo 发布了一个不兼容的新版本,这时包管理工具默认更新到了这个版本,这时项目构建一定会出现问题,当使用者遇到这个些问题时 ,肯定会向包 turtle 的作者提出修改意见,但是这只是因为它的依赖包 foo 被错误的更新导致的,包 turtle不应该为此承担任何责任 +作为一种对比,即使包管理工具把依赖包升级到了最新版本,同样会导致一些问题。比如说我们的项目中引用了包 turtle, 而它依赖包 foo,当包 foo 发布了一个不兼容的新版本,这时包管理工具默认更新到了这个版本,这时项目构建一定会出现问题,当使用者遇到这个些问题时 ,肯定会向包 turtle 的作者提出修改意见,但是这只是因为它的依赖包 foo 被错误的更新导致的,包 turtl e不应该为此承担任何责任 这使我不禁要想,对于包的开发者来说哪种方式会带来更多的麻烦,是包管理工具默认把依赖包升级到最新版本,还是按 go.mod 中指定的版本来确定依赖包的版本 @@ -122,7 +122,7 @@ vgo 对这种情况的处理是非常自然的,只有当开发者手动指定 ### 以 HTTP 来下载代码不安全 -一些开发者认为 vgo 获取代码的方式不安全,因为它用的是 HTTP 而不是 HTTPS。不过据我所知,在 vgo 的源码实现里,应该采用的是 HTTPS 的方式, Russ 在文章中用 「HTTP」 可能只是用它来指代完成实际传输任务的 git 或者 bzr. +一些开发者认为 vgo 获取代码的方式不安全,因为它用的是 HTTP 而不是 HTTPS。不过据我所知,在 vgo 的源码实现里,应该采用的是 HTTPS 的方式, Russ 在文章中用 「HTTP」 可能只是用它来指代完成实际传输任务的 Git 或者 bzr. ### API 的限制 @@ -136,11 +136,11 @@ vgo 对这种情况的处理是非常自然的,只有当开发者手动指定 许多优秀的开发者聚在一起,开发了依赖包管理工具 dep,我非常喜欢这个工具,但是在熟练使用它之前需要一个学习的过程 -相比来说,vgo 和现有的 go 命令行工具的使用方式几乎一样,可能唯一需要我们适应的就是 go.mod 这个文件了。但是如果这真的是从 go 过度到 vgo 最大的障碍的话,那么 vgo 就是非常成功的了 +相比来说,vgo 和现有的 Go 命令行工具的使用方式几乎一样,可能唯一需要我们适应的就是 go.mod 这个文件了。但是如果这真的是从 Go 过度到 vgo 最大的障碍的话,那么 vgo 就是非常成功的了 -### go test all 变得更加实用了 +### Go test all 变得更加实用了 -vgo 也让 go test all 命令变得更加实用了,因为现在它只是测试当前模块和它的依赖项。虽然这不是开发 vgo 的主要目标,但这个变化绝对是深思熟虑的结果,我越来越喜欢这个命令了。 +vgo 也让 Go test all 命令变得更加实用了,因为现在它只是测试当前模块和它的依赖项。虽然这不是开发 vgo 的主要目标,但这个变化绝对是深思熟虑的结果,我越来越喜欢这个命令了。 更进一步说,我希望这种不依赖 $GOPATH 的方式,可以让像 gorename 这样的程序 工作得更加一致,通过把作用范围限制在一个单独的模块,就不会像之前一样,只要 $GOPATH/src 目录下有一个不正常的模块就会导致 gorename 运行失败。此外由于单个模块构建时不依赖外部模块,构建失败也很少会发生。 diff --git a/published/tech/ExportIdentifiersInGo.md b/published/tech/ExportIdentifiersInGo.md index b92b27809..2f2dbb720 100644 --- a/published/tech/ExportIdentifiersInGo.md +++ b/published/tech/ExportIdentifiersInGo.md @@ -78,7 +78,7 @@ type record struct { age int8 } func GetRecord() record { - return record{Name: "Michał", age: 29} + return record{Name: "Micha ł", age: 29} } @@ -185,7 +185,7 @@ record.walk undefined (cannot refer to unexported field or method library.Record via: https://medium.com/golangspec/exported-identifiers-in-go-518e93cc98af -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[tingtingr](https://github.com/wentingrohwer) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/For-Range-Semantics.md b/published/tech/For-Range-Semantics.md index a4947f2e2..c8d965202 100644 --- a/published/tech/For-Range-Semantics.md +++ b/published/tech/For-Range-Semantics.md @@ -24,7 +24,7 @@ [play.golang.org](https://play.golang.org/p/_CWCAF6ge3) -**代码清单1** +**代码清单 1** ```go package main @@ -49,11 +49,11 @@ func main() { } } ``` -在代码清单1中,程序声明一个名为 `user` 的类型,创建四个用户值,然后显示关于每个用户的信息。第 18 行的范围循环使用值语义。这是因为在每次迭代中都会在循环内部创建并操作来自切片的原始用户值的副本。实际上,对 `Println` 的调用会创建循环副本的第二个副本。如果要为用户值使用值语义,这就是你想要的。 +在代码清单 1 中,程序声明一个名为 `user` 的类型,创建四个用户值,然后显示关于每个用户的信息。第 18 行的范围循环使用值语义。这是因为在每次迭代中都会在循环内部创建并操作来自切片的原始用户值的副本。实际上,对 `Println` 的调用会创建循环副本的第二个副本。如果要为用户值使用值语义,这就是你想要的。 如果你要使用指针语义,`for range` 循环看起来像这样。 -**代码清单2** +**代码清单 2** ```go for i := range users { @@ -65,7 +65,7 @@ for i := range users { 要解决这个问题,需要再做一次最后的修改。 -**代码清单3** +**代码清单 3** ```go for i := range users { @@ -75,9 +75,9 @@ for i := range users { 现在会一直使用 `user` 的指针语义。 -作为参考,清单4并排显示了值和指针语义。 +作为参考,清单 4 并排显示了值和指针语义。 -**代码清单4** +**代码清单 4** ```go // Value semantics. // Pointer semantics. @@ -92,7 +92,7 @@ for i, u := range users { for i := range users { [https://play.golang.org/p/IlAiEkgs4C](https://play.golang.org/p/IlAiEkgs4C) -**代码清单5** +**代码清单 5** ```go package main @@ -115,7 +115,7 @@ func main() { 这个程序的预期输出是什么? -**清单6** +**清单 6** ``` Bfr[Betty] Aft[Jack] @@ -124,7 +124,7 @@ Aft[Jack] [https://play.golang.org/p/opSsIGtNU1](https://play.golang.org/p/opSsIGtNU1) -**清单7** +**清单 7** ```go package main @@ -147,7 +147,7 @@ func main() { 在循环的每次迭代中,代码再次更改索引 1 处的字符串。此时代码显示索引 1 处的值时,输出不同。 -**清单8** +**清单 8** ``` Bfr[Betty] : v[Betty] ``` @@ -155,7 +155,7 @@ Bfr[Betty] : v[Betty] 当使用值语义形式覆盖切片时,将采用切片标头的副本。 这就是为什么清单 9 中的代码不必惊慌。 -**清单9** +**清单 9** ```go package main @@ -179,11 +179,11 @@ v[Doug] v[Edward] ``` -如果您查看第09行,循环内的切片值会缩减为2,但循环将在切片值的自身副本上进行操作。 这允许循环使用原始长度进行迭代而没有任何问题,因为后备数组仍然是完整的。 +如果您查看第 09 行,循环内的切片值会缩减为 2,但循环将在切片值的自身副本上进行操作。 这允许循环使用原始长度进行迭代而没有任何问题,因为后备数组仍然是完整的。 如果代码使用 `for range` 的指针语义形式,程序就会发生混乱。 -**清单10** +**清单 10** ```go package main @@ -215,7 +215,7 @@ main.main() 这是一个完全糟糕的例子。该代码混合了用户类型定义的语义,并引发了一个 bug。 -**清单11** +**清单 11** ```go package main @@ -251,9 +251,9 @@ func main() { } ``` -这个例子没有那么做作。在第05行,`user` 类型被声明并且选择指针语义来实现为用户类型设置的方法。然后在 `main` 程序中,在 `for range` 循环中使用值语义为每个用户添加一个 like。然后使用第二个循环来再次使用值语义来通知每个 `user`。 +这个例子没有那么做作。在第 05 行,`user` 类型被声明并且选择指针语义来实现为用户类型设置的方法。然后在 `main` 程序中,在 `for range` 循环中使用值语义为每个用户添加一个 like。然后使用第二个循环来再次使用值语义来通知每个 `user`。 -**清单12** +**清单 12** ``` bill has 0 likes @@ -264,7 +264,7 @@ lisa has 0 likes 这是代码应该看起来如何与用户类型的指针语义保持一致。 -**清单13** +**清单 13** ```go package main @@ -306,7 +306,7 @@ lisa has 1 likes ## 结论 -值和指针语义是Go编程语言的重要组成部分,正如我已经展示的那样,集成到了 `for range` 循环中。在使用 `for range` 时,验证你正在迭代的给定类型在使用正确的形式。最后一件事是混合语义,如果你没有注意的话,`for range` 很容易混合使用语义。 +值和指针语义是 Go 编程语言的重要组成部分,正如我已经展示的那样,集成到了 `for range` 循环中。在使用 `for range` 时,验证你正在迭代的给定类型在使用正确的形式。最后一件事是混合语义,如果你没有注意的话,`for range` 很容易混合使用语义。 语言给了你这种选择语义的力量,并且干净而一致地使用它。这是你想要充分利用的东西。 我想让你决定每种类型使用的语义并保持一致。你对一段数据的语义越一致,您的代码库就越好。如果你有一个很好的理由来改变语义,然后广泛地记录下来。 diff --git a/published/tech/Go-Revel-Tutorial.md b/published/tech/Go-Revel-Tutorial.md index 9e3dc6015..349277b88 100644 --- a/published/tech/Go-Revel-Tutorial.md +++ b/published/tech/Go-Revel-Tutorial.md @@ -1,6 +1,6 @@ 已发布:https://studygolang.com/articles/12898 -# Go/Revel教程:在浏览器(使用 PaizaCloud IDE)上,构建 Go web 框架 Revel 的应用程序 +# Go/Revel 教程:在浏览器(使用 PaizaCloud IDE)上,构建 Go Web 框架 Revel 的应用程序 ![gopher](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-revel/20180323134353.png) @@ -10,7 +10,7 @@ Go 语言(golang)的特性有: - 易于编写并发程序。 - 易于管理可执行文件(因为只有一个文件) -由于这些特点,Go 语言在 web 开发中也越发受到欢迎。 +由于这些特点,Go 语言在 Web 开发中也越发受到欢迎。 如下图所示,我们可以在 Google Trends 看到 Go 受关注的程度。 @@ -18,17 +18,17 @@ Go 语言(golang)的特性有: 来自 [google trends](https://trends.google.com/trends/explore?date=2010-02-23%202018-03-23&q=golang) -虽说 Go 本身自带着丰富的标准库,帮助我们构建 web 应用,但是使用 web 应用框架,我们能够更轻松地开发出功能齐全的 web 应用。 +虽说 Go 本身自带着丰富的标准库,帮助我们构建 Web 应用,但是使用 Web 应用框架,我们能够更轻松地开发出功能齐全的 Web 应用。 -Go 的 web 框架有很多:Revel、Echo、Gin、Iris 等,**其中 Revel 是最受欢迎的全栈 web 应用框架之一**。 +Go 的 Web 框架有很多:Revel、Echo、Gin、Iris 等,**其中 Revel 是最受欢迎的全栈 Web 应用框架之一**。 -Go 框架 Revel 的 web 开发功能有:路由、MVC、生成器。按照 Revel 的规则来构建应用,你可以轻而易举地创建可读性强、易扩展的 Web 应用程序。在 Revel 中,你还可以使用 OR 映射库(如 Gorm)。 +Go 框架 Revel 的 Web 开发功能有:路由、MVC、生成器。按照 Revel 的规则来构建应用,你可以轻而易举地创建可读性强、易扩展的 Web 应用程序。在 Revel 中,你还可以使用 OR 映射库(如 Gorm)。 然而,要在实际中开发 Revel 应用,你需要安装和配置 Go、Revel、Gorm 和 数据库。这些安装和设置都很麻烦。仅仅根据安装说明进行,常常会出错,或者因为 OS、版本和软件依赖等原因引起各种错误。 同样,如果你发布这项服务,朋友和其他人的反馈的确会让你动力十足。但是,这项服务还需要“部署”。“部署”同样也很难搞。 -所以,[PaizaCloud](https://paiza.cloud/) 这个 Cloud IDE 应运而生。这是一个基于浏览器的在线 web 和应用开发环境。 +所以,[PaizaCloud](https://paiza.cloud/) 这个 Cloud IDE 应运而生。这是一个基于浏览器的在线 Web 和应用开发环境。 **由于 PaizaCloud 拥有 Go/Revel 应用的开发环境,因此你可以直接在你的浏览器中,开始编写你的 Go/Revel 应用程序**。 @@ -79,10 +79,10 @@ Go 框架 Revel 的 web 开发功能有:路由、MVC、生成器。按照 Reve 我们输入: ```bash -$ go get github.com/revel/revel -$ go get github.com/revel/cmd/revel -$ go get github.com/jinzhu/gorm -$ go get github.com/go-sql-driver/mysql +$ Go get github.com/revel/revel +$ Go get github.com/revel/cmd/revel +$ Go get github.com/jinzhu/gorm +$ Go get github.com/go-sql-driver/mysql ``` ![bash](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-revel/20180323105536.png) @@ -142,7 +142,7 @@ Revel 服务器会在 9000 端口上运行。[PaizaCloud Cloud IDE](https://paiz 点击该按钮,会出现浏览器程序(PaizaClound 中的浏览器应用程序)。现在,你可以看到 Revel 的网页了,这就是你的应用! -![your web page](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-revel/20180323111629.png) +![your Web page](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-revel/20180323111629.png) (尽管 Revel 是作为 HTTP 服务器运行的,但是 PaizaCloud 会把 HTTP 转换为 HTTPS。) @@ -192,7 +192,7 @@ $ sudo systemctl start mysql 接下来,创建这个应用的数据库。在这里,我们使用 `mysql` 命令,创建一个数据库 `mydb`。输入下面命令,可以创建数据库 `mydb`。 ```bash -$ mysql -u root +$ MySQL -u root create database mydb; ``` @@ -530,7 +530,7 @@ func (c Post) RedirectToPosts() revel.Result { 通过 PaizaCloud Cloud IDE,我们在浏览器上创建了一个 Go/Revel 应用,无需安装和设置任何开发环境。我们甚至可以直接在 PaizaCloud 上发布应用。现在,开始构建你自己的 Go/Revel 应用吧! -通过 [PaizaCloud Cloud IDE](https://paiza.cloud/),只需在浏览器上,你就能灵活、轻松地开发和发布 web 应用或服务器应用。 +通过 [PaizaCloud Cloud IDE](https://paiza.cloud/),只需在浏览器上,你就能灵活、轻松地开发和发布 Web 应用或服务器应用。 --- diff --git a/published/tech/Go_execution_tracer.md b/published/tech/Go_execution_tracer.md index 3a4c04067..8b51eec77 100644 --- a/published/tech/Go_execution_tracer.md +++ b/published/tech/Go_execution_tracer.md @@ -4,19 +4,19 @@ ## 概述 -你有没有好奇过 Go 运行时是如何调度 goroutine 的?有没有深入研究过为什么有时候加了并发但是性能没有提高? Go 提供了执行跟踪器,可以帮助你诊断性能问题(如延迟、竞争或低并发等)、解决前面那些疑问。 +你有没有好奇过 Go 运行时是如何调度 Goroutine 的?有没有深入研究过为什么有时候加了并发但是性能没有提高? Go 提供了执行跟踪器,可以帮助你诊断性能问题(如延迟、竞争或低并发等)、解决前面那些疑问。 Go 从 1.5 版本开始有执行跟踪器这么一个工具,原理是:监听 Go 运行时的一些特定的事件,如: -1. goroutine的创建、开始和结束。 -2. 阻塞/解锁goroutine的一些事件(系统调用,channel,锁) -3. 网络I/O相关事件 +1. goroutine 的创建、开始和结束。 +2. 阻塞/解锁 goroutine 的一些事件(系统调用,channel,锁) +3. 网络 I/O 相关事件 4. 系统调用 5. 垃圾回收 追踪器会原原本本地收集这些信息,不做任何聚合或者抽样操作。对于负载高的应用来说,就可能会生成一个比较大的文件,该文件后面可以通过 `go tool trace` 命令来进行解析。 -在执行追踪器之前, Go 已经提供了 pprof 分析器可以用来分析内存和CPU,那么问题来了,为什么还要再添加这么一个官方工具链? CPU 分析器可以很清晰地查看到是哪个函数最占用 CPU 时间,但是你没办法通过它知道是什么原因导致 goroutine 不运行,也没法知道底层如何在操作系统线程上调度 goroutine 的。而这些恰恰是追踪器所擅长的。追踪器的 [设计文档](https://docs.google.com/document/u/1/d/1FP5apqzBgr7ahCCgFO-yoVhk4YZrNIDNf9RybngBc14/pub) 详尽地说明了引入追踪器的原因以及工作原理。 +在执行追踪器之前, Go 已经提供了 pprof 分析器可以用来分析内存和 CPU,那么问题来了,为什么还要再添加这么一个官方工具链? CPU 分析器可以很清晰地查看到是哪个函数最占用 CPU 时间,但是你没办法通过它知道是什么原因导致 Goroutine 不运行,也没法知道底层如何在操作系统线程上调度 Goroutine 的。而这些恰恰是追踪器所擅长的。追踪器的 [设计文档](https://docs.google.com/document/u/1/d/1FP5apqzBgr7ahCCgFO-yoVhk4YZrNIDNf9RybngBc14/pub) 详尽地说明了引入追踪器的原因以及工作原理。 ## 追踪器”之旅“ @@ -46,7 +46,7 @@ func main() { } ``` -本例中,先创建了一个无缓冲 channel,然后初始化一个 goroutine 通过 channel 发送数字 42。主 goroutine 会一直阻塞直到另外的那个 goroutine 通过 channel 发送一个值过来。 +本例中,先创建了一个无缓冲 channel,然后初始化一个 Goroutine 通过 channel 发送数字 42。主 Goroutine 会一直阻塞直到另外的那个 Goroutine 通过 channel 发送一个值过来。 通过运行 `go run main.go 2> trace.out` 命令,将追踪信息输出到 `trace.out` 文件,可以用 `go tool trace trace.out` 命令读取该文件。 @@ -58,7 +58,7 @@ func main() { 1. View trace(查看追踪信息) -提供了一个最复杂、最强大的交互式可视化界面,显示整个程序执行的时间线。举个例子,界面展示每个虚拟处理器上跑了什么以及被阻塞等待重新跑的有哪些。后文中会更详细的介绍这个可视化界面。注意这个界面只支持 chrome 浏览器。 +提供了一个最复杂、最强大的交互式可视化界面,显示整个程序执行的时间线。举个例子,界面展示每个虚拟处理器上跑了什么以及被阻塞等待重新跑的有哪些。后文中会更详细的介绍这个可视化界面。注意这个界面只支持 Chrome 浏览器。 2. Goroutine analysis ( Goroutine 分析) @@ -84,7 +84,7 @@ func main() { 1. Timeline (时间线) -显示执行时的时间,时间单位可以通过导航栏进行调整。用户可以通过键盘快捷键(WASD键,就像游戏里的一样)操纵时间线。 +显示执行时的时间,时间单位可以通过导航栏进行调整。用户可以通过键盘快捷键(WASD 键,就像游戏里的一样)操纵时间线。 2. Heap (堆) @@ -92,7 +92,7 @@ func main() { 3. Goroutines -显示有多少 groroutine 正在运行,以及每个时间点有多少是可运行的(等待被调度)。可运行 goroutine 数量过多可能意味着有调度竞争,例如,当程序创建了过多的 goroutine 会导致调度器忙不过来。 +显示有多少 groroutine 正在运行,以及每个时间点有多少是可运行的(等待被调度)。可运行 Goroutine 数量过多可能意味着有调度竞争,例如,当程序创建了过多的 Goroutine 会导致调度器忙不过来。 4. OS Threads (操作系统线程) @@ -102,13 +102,13 @@ func main() { 每一行显示一个虚拟处理器。虚拟进程的数量受 GOMAXPROCS 环境变量控制(默认为机器核心数量)。 -6.Goroutines and events (goroutine和事件) +6.Goroutines and events (goroutine 和事件) -展示每个虚拟处理器上的 goroutine 跑的内容/跑在哪里。连接 goroutine 的线代表事件。在例图中,我们可以看到: Goroutine “G1 runtime.main”产生出两个不同的 Goroutine:G6 和 G5 (前者负责收集追踪信息,后者是使用“go”关键字产生的)。 +展示每个虚拟处理器上的 Goroutine 跑的内容/跑在哪里。连接 Goroutine 的线代表事件。在例图中,我们可以看到: Goroutine “G1 runtime.main”产生出两个不同的 Goroutine:G6 和 G5 (前者负责收集追踪信息,后者是使用“go”关键字产生的)。 -每个虚拟处理器的第二行可能会显示额外的事件,例如系统调用和运行时事件。同时包括了 goroutine 为运行时做的一些工作(例如,协助垃圾收集)。 +每个虚拟处理器的第二行可能会显示额外的事件,例如系统调用和运行时事件。同时包括了 Goroutine 为运行时做的一些工作(例如,协助垃圾收集)。 -下图显示选择一个 goroutine 时得到的信息。 +下图显示选择一个 Goroutine 时得到的信息。 ![view-goroutine.png](https://raw.githubusercontent.com/studygolang/gctt-images/master/execution-tracer/view-goroutine.png) @@ -119,28 +119,28 @@ func main() { * 持续时间(Wall Duration) * 开始时刻堆栈轨迹 * 结束时刻堆栈轨迹 -* 该 goroutine 生成的事件 +* 该 Goroutine 生成的事件 -我们可以看到:该 goroutine 产生两个事件:生成用于追踪的 goroutine 以及在channel上开始发送数字42的 goroutine。 +我们可以看到:该 Goroutine 产生两个事件:生成用于追踪的 Goroutine 以及在 channel 上开始发送数字 42 的 goroutine。 ![view-event.png](https://raw.githubusercontent.com/studygolang/gctt-images/master/execution-tracer/view-event.png) -通过点击一个特定的事件(图中一条线或者通过点 goroutine 之后选事件),我们可以看到: +通过点击一个特定的事件(图中一条线或者通过点 Goroutine 之后选事件),我们可以看到: * 事件开始时刻的堆栈轨迹 * 事件的持续时间 * 时间包含的 goroutine -可以通过点击这些 goroutine 导航到他们的追踪数据。 +可以通过点击这些 Goroutine 导航到他们的追踪数据。 ## 阻塞分析 -通过追踪还可以得到网络/同步/系统调用阻塞分析的视图。阻塞分析显示的图像视图与 pprof 内存/cpu 分析的视图比较相似。区别在于,这里不显示每个函数分配了多少内存,而是显示每个 goroutine 在特定资源上阻塞了多久。 +通过追踪还可以得到网络/同步/系统调用阻塞分析的视图。阻塞分析显示的图像视图与 pprof 内存/cpu 分析的视图比较相似。区别在于,这里不显示每个函数分配了多少内存,而是显示每个 Goroutine 在特定资源上阻塞了多久。 下图显示对我们示例代码的“同步阻塞分析”。 ![blocking-profile.png](https://raw.githubusercontent.com/studygolang/gctt-images/master/execution-tracer/blocking-profile.png) -该图说明主 goroutine 从 channel 接收数据阻塞时间花费 12.08 微秒。这种类型的图对于多个 goroutine 竞争资源锁,查找锁竞争情况非常有用。 +该图说明主 Goroutine 从 channel 接收数据阻塞时间花费 12.08 微秒。这种类型的图对于多个 Goroutine 竞争资源锁,查找锁竞争情况非常有用。 ## 收集追踪数据 @@ -156,11 +156,11 @@ func main() { 3.使用 debug/pprof/trace 处理器 -是从正在运行 web 应用收集追踪数据的最好方式。 +是从正在运行 Web 应用收集追踪数据的最好方式。 -## 追踪一个 web 应用 +## 追踪一个 Web 应用 -为了能够对 Go 写的正在运行的 web 应用收集追踪信息,需要添加 `/debug/pprof/trace` 处理器。下文示例代码说明对于 `http.DefaultServerMux` 如何做到这一点:通过简单地引入 `net/http/pprof` 包。 +为了能够对 Go 写的正在运行的 Web 应用收集追踪信息,需要添加 `/debug/pprof/trace` 处理器。下文示例代码说明对于 `http.DefaultServerMux` 如何做到这一点:通过简单地引入 `net/http/pprof` 包。 ```go package main @@ -191,9 +191,9 @@ func helloHandler(w http.ResponseWriter, r *http.Request) { $ wrk -c 100 -t 10 -d 60s http://localhost:8181/hello ``` -该命令会在 60 秒内通过 10 个线程使用 100 个连接发起请求。跑`wrk`的同时,我们可以收集 5 s 的追踪数据:`curl localhost:8181/debug/pprof/trace?seconds=5 > trace.out`。该命令会在本人 4 CPU 机器上生成一个 5 MB 的文件(文件大小会随负载加大而快速增加)。 +该命令会在 60 秒内通过 10 个线程使用 100 个连接发起请求。跑 `wrk` 的同时,我们可以收集 5 s 的追踪数据:`curl localhost:8181/debug/pprof/trace?seconds=5 > trace.out`。该命令会在本人 4 CPU 机器上生成一个 5 MB 的文件(文件大小会随负载加大而快速增加)。 -同样的,通过 go 工具 trace 命令打开追踪数据:`go tool trace trace.out`。由于工具要分析整个文件,会比前例耗费更长时间。完成之后,页面看起来稍微有点不一样: +同样的,通过 Go 工具 trace 命令打开追踪数据:`go tool trace trace.out`。由于工具要分析整个文件,会比前例耗费更长时间。完成之后,页面看起来稍微有点不一样: ``` View trace (0s-2.546634537s) @@ -212,24 +212,24 @@ Scheduler latency profile ![trace-web.png](https://raw.githubusercontent.com/studygolang/gctt-images/master/execution-tracer/trace-web.png) -该截图显示 1169 ms ~ 1170 ms 之间开始、1174 ms 之后结束的一个 GC 操作。这段时间内,一个操作系统线程(PROC 1)启动一个 goroutine 专门做 GC,其他的 goroutine 辅助 GC 操作(在 goroutine 行下展示,标示为 MARK ASSIST ),截频的末尾,我们可以看到大部分分配的内存已被 GC 释放掉。 +该截图显示 1169 ms ~ 1170 ms 之间开始、1174 ms 之后结束的一个 GC 操作。这段时间内,一个操作系统线程(PROC 1)启动一个 Goroutine 专门做 GC,其他的 Goroutine 辅助 GC 操作(在 Goroutine 行下展示,标示为 MARK ASSIST ),截频的末尾,我们可以看到大部分分配的内存已被 GC 释放掉。 -另外一个特别有用的信息是:处于“Runnable”状态(选择时显示的是13)的 goroutine 数量,如果该数值随时间变大,就意味着我们需要更多 CPU 来处理负载。 +另外一个特别有用的信息是:处于“Runnable”状态(选择时显示的是 13)的 Goroutine 数量,如果该数值随时间变大,就意味着我们需要更多 CPU 来处理负载。 ## 总结 -追踪器是调试并发问题(如锁竞争和逻辑竞争)的一个强大工具。它不能够解决一切问题:它不是追踪哪块代码最耗费 CPU 时间、内存的最佳工具。用`go tool pprof`更适合这样的场景。 +追踪器是调试并发问题(如锁竞争和逻辑竞争)的一个强大工具。它不能够解决一切问题:它不是追踪哪块代码最耗费 CPU 时间、内存的最佳工具。用 `go tool pprof` 更适合这样的场景。 -该工具对于理解程序不运行时每个 goroutine 在做什么以及按照时间查看程序行为非常有用。收集追踪数据会有一些开销,同时可能会产生较大数据量的数据以供查看。 +该工具对于理解程序不运行时每个 Goroutine 在做什么以及按照时间查看程序行为非常有用。收集追踪数据会有一些开销,同时可能会产生较大数据量的数据以供查看。 不幸的是,官方文档缺少一些实验来让我们试验、理解追踪器显示的信息。这也是个给官方文档、社区(如博客)做贡献的[机会](https://github.com/golang/go/issues/16526)。 -André 是 [Globo.com](http://www.globo.com/) 的高级软件工程师, 开发 [Tsuru](https://tsuru.io/)项目。 twitter 请@andresantostc, 或者 web 留言https://andrestc.com。 +Andr é 是 [Globo.com](http://www.globo.com/) 的高级软件工程师, 开发 [Tsuru](https://tsuru.io/)项目。 Twitter 请@andresantostc, 或者 Web 留言 https://andrestc.com。 ## 参考 1. [Go execution tracer (design doc)](https://docs.google.com/document/u/1/d/1FP5apqzBgr7ahCCgFO-yoVhk4YZrNIDNf9RybngBc14/pub) -2. [Using the go tracer to speed fractal rendering](https://medium.com/justforfunc/using-the-go-execution-tracer-to-speed-up-fractal-rendering-c06bb3760507) +2. [Using the Go tracer to speed fractal rendering](https://medium.com/justforfunc/using-the-go-execution-tracer-to-speed-up-fractal-rendering-c06bb3760507) 3. [Go tool trace](https://making.pusher.com/go-tool-trace/) 4. [Your pprof is showing](http://mmcloughlin.com/posts/your-pprof-is-showing) @@ -237,7 +237,7 @@ André 是 [Globo.com](http://www.globo.com/) 的高级软件工程师, 开发 [ via: https://blog.gopheracademy.com/advent-2017/go-execution-tracer/ -作者:[André Carvalho](https://blog.gopheracademy.com/advent-2017/go-execution-tracer/) +作者:[Andr é Carvalho](https://blog.gopheracademy.com/advent-2017/go-execution-tracer/) 译者:[dongfengkuayue](https://github.com/dongfengkuayue) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/Golang-database-mocks.md b/published/tech/Golang-database-mocks.md index d16539a59..6a81df2cd 100644 --- a/published/tech/Golang-database-mocks.md +++ b/published/tech/Golang-database-mocks.md @@ -6,7 +6,7 @@ 测试我们项目的数据库代码,我通过两个步骤完成。 -通过 database/sql 包,我们有了 sql.DB 结构,它代表一系列mocks的连接,以及包含一系列与这些连接进行交互的方法。在我们的代码库中,我们使用了其中的两个(以及一个返回打开的数据库的函数): +通过 database/sql 包,我们有了 sql.DB 结构,它代表一系列 mocks 的连接,以及包含一系列与这些连接进行交互的方法。在我们的代码库中,我们使用了其中的两个(以及一个返回打开的数据库的函数): ```go func Open(driverName, dataSourceName string) (*DB, error) diff --git a/published/tech/Gotchas-of-Defer-in-Go/20171208-part1.md b/published/tech/Gotchas-of-Defer-in-Go/20171208-part1.md index cfb25c806..8ff3e50a9 100644 --- a/published/tech/Gotchas-of-Defer-in-Go/20171208-part1.md +++ b/published/tech/Gotchas-of-Defer-in-Go/20171208-part1.md @@ -125,7 +125,7 @@ disconnect ### 糟糕的处理方式: -即便这种处理方式很糟,但我还是想告诉你如何不用变量来解决这个问题,因此,我希望你能以此来了解 defer 亦或是 go 语言的运行机制。 +即便这种处理方式很糟,但我还是想告诉你如何不用变量来解决这个问题,因此,我希望你能以此来了解 defer 亦或是 Go 语言的运行机制。 ```go func() { diff --git a/published/tech/Gotchas-of-Defer-in-Go/20171221-part2.md b/published/tech/Gotchas-of-Defer-in-Go/20171221-part2.md index 305f17509..d70499034 100644 --- a/published/tech/Gotchas-of-Defer-in-Go/20171221-part2.md +++ b/published/tech/Gotchas-of-Defer-in-Go/20171221-part2.md @@ -159,7 +159,7 @@ for i := 0; i < 3; i++ { ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/5-gotchas-defer-2/capture_in_the_loop.png) -所有的延迟函数会查看相同的 `i` ,循环结束(值变成了3),因此它们查看到的都是 3 。 +所有的延迟函数会查看相同的 `i` ,循环结束(值变成了 3),因此它们查看到的都是 3 。 ### 解决方案 diff --git a/published/tech/Gotchas-of-Defer-in-Go/20180119-part3.md b/published/tech/Gotchas-of-Defer-in-Go/20180119-part3.md index 0641e1811..4c4838b56 100644 --- a/published/tech/Gotchas-of-Defer-in-Go/20180119-part3.md +++ b/published/tech/Gotchas-of-Defer-in-Go/20180119-part3.md @@ -323,7 +323,7 @@ func errorly() { ### 传入任意类型 -正如你所看到的 *panic* 可以接收 *string* 以及 [error类型](https://golang.org/pkg/builtin/#error) 。这意味着事实上你可以给 panic 传 "任意类型" 的数据并能够在 *defer* 中使用 *recover* 来获取这个数据。 +正如你所看到的 *panic* 可以接收 *string* 以及 [error 类型](https://golang.org/pkg/builtin/#error) 。这意味着事实上你可以给 panic 传 "任意类型" 的数据并能够在 *defer* 中使用 *recover* 来获取这个数据。 ```go type myerror struct {} diff --git a/published/tech/How-to-use-a-Forwarding-Proxy-with-golang.md b/published/tech/How-to-use-a-Forwarding-Proxy-with-golang.md index 7d92bd4a2..df110f79f 100644 --- a/published/tech/How-to-use-a-Forwarding-Proxy-with-golang.md +++ b/published/tech/How-to-use-a-Forwarding-Proxy-with-golang.md @@ -16,11 +16,11 @@ 2015 年 12 月 1 日,有一位用户在 [CircleCI 论坛](https://discuss.circleci.com/t/circleci-source-ip/1202)上提了这个问题,并且问题还未关闭。当然,CircleCI 很棒。我只是举个例子,并非要埋怨他们。 -解决这个问题的一种可行方法是使用正向代理。你可以让一组节点以同一静态IP运转,然后把清单提供给客户即可。 +解决这个问题的一种可行方法是使用正向代理。你可以让一组节点以同一静态 IP 运转,然后把清单提供给客户即可。 几乎所有云服务提供商都是这样做的,比如 DigitalOcean 的浮动 IP(floating IP)、AWS 的弹性 IP(elastic IP)等。 -你可以通过配置自己的应用来把请求转发到这个(代理)池中。这样,终点的服务所取得的IP就是正向代理节点的IP,而不是内部IP。 +你可以通过配置自己的应用来把请求转发到这个(代理)池中。这样,终点的服务所取得的 IP 就是正向代理节点的 IP,而不是内部 IP。 正向代理可以成为你的网络设施的又一安全层,因为你可以在一个中心化的地方极其方便地扫描和控制内部网络发出来的数据包。 @@ -54,7 +54,7 @@ func main() { } ``` -如果用 `GET` 方法访问路径 `/whoyare`,你会得到一个类似下面的 JSON 格式的响应:`{"addr": "34.35.23.54"}`,其中 `34.35.23.54` 就是你的公网地址。如果你使用的是笔记本电脑,那么在终端上发出请求后,你应该会得到 `localhost`的结果。可以用 `curl` 来试一下: +如果用 `GET` 方法访问路径 `/whoyare`,你会得到一个类似下面的 JSON 格式的响应:`{"addr": "34.35.23.54"}`,其中 `34.35.23.54` 就是你的公网地址。如果你使用的是笔记本电脑,那么在终端上发出请求后,你应该会得到 `localhost` 的结果。可以用 `curl` 来试一下: 18:36 $ curl -v http://localhost:8080/whoyare * TCP_NODELAY set @@ -112,7 +112,7 @@ func main() { * whoyare: public ip 188.166.17.88 * privoxy: public ip 167.99.41.79 -Privoxy 是一个易用的正向代理。相比而言,Nginx 和 Haproxy 都不太适合在这种场景下使用,因为它们不支持`CONNECT`方法。 +Privoxy 是一个易用的正向代理。相比而言,Nginx 和 Haproxy 都不太适合在这种场景下使用,因为它们不支持 `CONNECT` 方法。 我在 Docker Hub 上创建了一个 docker 镜像,你可以直接运行它,默认使用端口 8118。 @@ -133,9 +133,9 @@ Privoxy 是一个易用的正向代理。相比而言,Nginx 和 Haproxy 都不 2018-03-18 17:28:05.611 7fbbf41dab88 Info: Listening on port 8118 on IP address 0.0.0.0 -第二步,编译`whoyare`并且把可执行文件用scp传送到服务器,可使用以下命令: +第二步,编译 `whoyare` 并且把可执行文件用 scp 传送到服务器,可使用以下命令: - $ CGO_ENABLED=0 GOOS=linux go build -o bin/server_linux -a ./whoyare + $ CGO_ENABLED=0 GOOS=linux Go build -o bin/server_linux -a ./whoyare 应用运行起来之后,我们就可以用 cURL 来直接或者通过 privoxy 发送请求了。 @@ -143,7 +143,7 @@ Privoxy 是一个易用的正向代理。相比而言,Nginx 和 Haproxy 都不 $ curl -v http://your-ip:8080/whoyare -cURL 使用环境变量`http_proxy`来配置代理进行请求转发: +cURL 使用环境变量 `http_proxy` 来配置代理进行请求转发: $ http_proxy=http://167.99.41.79:8118 curl -v http://188.166.17.88:8080/whoyare * Trying 167.99.41.79... @@ -177,7 +177,7 @@ privoxy 处应该会留下如下的请求日志: 2018/03/18 18:37:59 Target http://188.166.17.88:8080. You are {"addr":"95.248.202.252:38620"} -Go语言的 `HTTP.Client` 包支持一组和代理相关的环境变量,设置这些环境变量可以对运行期间的服务立刻生效,十分灵活。 +Go 语言的 `HTTP.Client` 包支持一组和代理相关的环境变量,设置这些环境变量可以对运行期间的服务立刻生效,十分灵活。 export HTTP_PROXY=http://http_proxy:port/ export HTTPS_PROXY=http://https_proxy:port/ diff --git a/published/tech/How-to-write-concise-tests-Table-Driven-Tests.md b/published/tech/How-to-write-concise-tests-Table-Driven-Tests.md index 64fa596de..f9a965ca9 100644 --- a/published/tech/How-to-write-concise-tests-Table-Driven-Tests.md +++ b/published/tech/How-to-write-concise-tests-Table-Driven-Tests.md @@ -2,7 +2,7 @@ # Go 如何编写简洁测试 -- 表格驱动测试 -表格驱动测试是一种编写易于扩展测试用例的测试方法。表格驱动测试在 Go 语言中很常见(并非唯一),以至于很多标准库[1](#reference)都有使用。表格驱动测试使用匿名结构体。 +表格驱动测试是一种编写易于扩展测试用例的测试方法。表格驱动测试在 Go 语言中很常见(并非唯一),以至于很多标准库 [1](#reference) 都有使用。表格驱动测试使用匿名结构体。 在这篇文章中我会告诉你如何编写表格驱动测试。继续使用 [errline repo](https://github.com/virup/errline) 这个项目,现在我们来为 `Wrap()` 函数添加测试。`Wrap()` 函数用于给一个 `error` 在调用位置添加文件名和行数的修饰。我们尤其需要测试其中计算文件的短名称的逻辑(以粗体表示部分)。最初的 `Wrap()` 函数如下: @@ -123,7 +123,7 @@ func TestShortFilename(t *testing.T) { 代码可以从 [我的 github](https://github.com/virup/errline/tree/master) 获取。 -##

引用

+##

引用

1. 一些 Go 语言标准库的表格驱动测试例子 * [https://github.com/golang/go/blob/master/src/strconv/ftoa_test.go](https://github.com/golang/go/blob/master/src/strconv/ftoa_test.go) diff --git a/published/tech/Implementing-Tail-Follow-In-Go.md b/published/tech/Implementing-Tail-Follow-In-Go.md index 2a0e35faa..67dd6151e 100644 --- a/published/tech/Implementing-Tail-Follow-In-Go.md +++ b/published/tech/Implementing-Tail-Follow-In-Go.md @@ -31,7 +31,7 @@ func follow(file io.Reader) error { 当内容写入文件时,可悲的是没有及时做出反应。Linux 提供了一个 API 来监视文件系统事件:inotify AP。手册页给了你一个很好的介绍。它提供了两个我们感兴趣的函数:`inotify_init` 和 `inotify_add_watch`。`inotify_init` 函数创建一个对象,我们将使用该对象进一步与 API 进行交互。`inotify_add_watch` 函数允许你指定感兴趣的文件事件。API 提供了几个事件,但我们关心的是修改文件时发出的 `IN_MODIFY` 事件。 -由于我们使用Go,不得不列出 `syscall` 包。它为前面提到的功能提供了包装器:`syscall.InotifyInit` 和 `syscall.InotifyAddWatch`。使用 syscall 让我们看看如何实现 follow 函数。为了简洁起见,我省略了错误处理,当你看到一个 `_` 变量被使用时,它是处理返回错误的好地方。 +由于我们使用 Go,不得不列出 `syscall` 包。它为前面提到的功能提供了包装器:`syscall.InotifyInit` 和 `syscall.InotifyAddWatch`。使用 syscall 让我们看看如何实现 follow 函数。为了简洁起见,我省略了错误处理,当你看到一个 `_` 变量被使用时,它是处理返回错误的好地方。 ```go func follow(filename string) error { diff --git a/published/tech/Interface-Values-Are-Valueless.md b/published/tech/Interface-Values-Are-Valueless.md index df709cfae..5a7bd0bfc 100644 --- a/published/tech/Interface-Values-Are-Valueless.md +++ b/published/tech/Interface-Values-Are-Valueless.md @@ -203,7 +203,7 @@ Tom 的观点已经清楚地表明,具体的数据才是设计实现不同行 请注意:你可能注意在接收者的方法中的第 13 行和第 23 行,声明了但没有给一个变量具体的名字。这其实是惯例,如果这个方法不需要使用接收者的任何数据时就可以不给接收者一个具体的名字。 -在代码清单 8,在第 13 行,为类型 file 定义了一个方法,在第23 行,为 pipe 类型定义了一个方法。现在,每种类型都定义了一个名为 read 的方法,它已经实现了 reader 定义的所有方法。由于有了这些方法的定义,接下来我们可以说: +在代码清单 8,在第 13 行,为类型 file 定义了一个方法,在第 23 行,为 pipe 类型定义了一个方法。现在,每种类型都定义了一个名为 read 的方法,它已经实现了 reader 定义的所有方法。由于有了这些方法的定义,接下来我们可以说: “类型 file 和 pipe 现在已经实现了 reader 接口。” diff --git a/published/tech/Labels-in-Go.md b/published/tech/Labels-in-Go.md index c8581716c..a2de60295 100644 --- a/published/tech/Labels-in-Go.md +++ b/published/tech/Labels-in-Go.md @@ -220,7 +220,7 @@ Block: via: https://medium.com/golangspec/labels-in-go-4ffd81932339 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[saberuster](https://github.com/saberuster) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/Language-Mechanics/Language-Mechanics-On-Escape-Analysis.md b/published/tech/Language-Mechanics/Language-Mechanics-On-Escape-Analysis.md index 8550ffb90..b78a0bdc7 100644 --- a/published/tech/Language-Mechanics/Language-Mechanics-On-Escape-Analysis.md +++ b/published/tech/Language-Mechanics/Language-Mechanics-On-Escape-Analysis.md @@ -15,7 +15,7 @@ ## 介绍(Introduction) -在四部分系列的第一部分,我用一个将值共享给 goroutine 栈的例子介绍了指针结构的基础。而我没有说的是值存在栈之上的情况。为了理解这个,你需要学习值存储的另外一个位置:堆。有这个基础,就可以开始学习逃逸分析。 +在四部分系列的第一部分,我用一个将值共享给 Goroutine 栈的例子介绍了指针结构的基础。而我没有说的是值存在栈之上的情况。为了理解这个,你需要学习值存储的另外一个位置:堆。有这个基础,就可以开始学习逃逸分析。 逃逸分析是编译器用来决定你的程序中值的位置的过程。特别地,编译器执行静态代码分析,以确定一个构造体的实例化值是否会逃逸到堆。在 Go 语言中,你没有可用的关键字或者函数,能够直接让编译器做这个决定。只能够通过你写代码的方式来作出这个决定。 @@ -27,7 +27,7 @@ ## 共享栈(Sharing Stacks) -在 Go 语言中,不允许 goroutine 中的指针指向另外一个 goroutine 的栈。这是因为当栈增长或者收缩时,goroutine 中的栈内存会被一块新的内存替换。如果运行时需要追踪指针指向其他的 goroutine 的栈,就会造成非常多需要管理的内存,以至于更新指向那些栈的指针将使 “stop the world” 问题更严重。 +在 Go 语言中,不允许 Goroutine 中的指针指向另外一个 Goroutine 的栈。这是因为当栈增长或者收缩时,goroutine 中的栈内存会被一块新的内存替换。如果运行时需要追踪指针指向其他的 Goroutine 的栈,就会造成非常多需要管理的内存,以至于更新指向那些栈的指针将使 “stop the world” 问题更严重。 这里有一个栈被替换好几次的例子。看输出的第 2 和第 6 行。你会看到 main 函数中的栈的字符串地址值改变了两次。[https://play.golang.org/p/pxn5u4EBSI](https://play.golang.org/p/pxn5u4EBSI) @@ -209,7 +209,7 @@ func createUserV2() *user { 02:跟函数 `json.Unmarshal` 函数共享指针。 03:返回 `u` 的副本给调用者。 -这里并不是很好理解,`user`值被 `json.Unmarshal` 函数创建,并被共享给调用者。 +这里并不是很好理解,`user` 值被 `json.Unmarshal` 函数创建,并被共享给调用者。 如何在构造过程中使用语法语义来改变可读性? @@ -240,7 +240,7 @@ func createUserV2() *user { ### 清单 10 ```shell -$ go build -gcflags "-m -m" +$ Go build -gcflags "-m -m" ./main.go:16: cannot inline createUserV1: marked go:noinline ./main.go:27: cannot inline createUserV2: marked go:noinline ./main.go:8: cannot inline main: non-leaf function diff --git a/published/tech/Language-Mechanics/Language-Mechanics-On-Memory-Profiling.md b/published/tech/Language-Mechanics/Language-Mechanics-On-Memory-Profiling.md index 4f0ca88ff..4057e21cd 100644 --- a/published/tech/Language-Mechanics/Language-Mechanics-On-Memory-Profiling.md +++ b/published/tech/Language-Mechanics/Language-Mechanics-On-Memory-Profiling.md @@ -17,7 +17,7 @@ ## 介绍(Introduction) -在前面的博文中,通过一个共享在 goroutine 的栈上的值的例子讲解了逃逸分析的基础。还有其他没有介绍的造成值逃逸的场景。为了帮助大家理解,我将调试一个分配内存的程序,并使用非常有趣的方法。 +在前面的博文中,通过一个共享在 Goroutine 的栈上的值的例子讲解了逃逸分析的基础。还有其他没有介绍的造成值逃逸的场景。为了帮助大家理解,我将调试一个分配内存的程序,并使用非常有趣的方法。 ## 程序(The Program) @@ -125,7 +125,7 @@ func BenchmarkAlgorithmOne(b *testing.B) { ### 清单 4 ``` -$ go test -run none -bench AlgorithmOne -benchtime 3s -benchmem +$ Go test -run none -bench AlgorithmOne -benchtime 3s -benchmem BenchmarkAlgorithmOne-8 2000000 2522 ns/op 117 B/op 2 allocs/op ``` @@ -138,7 +138,7 @@ BenchmarkAlgorithmOne-8 2000000 2522 ns/op 117 B/op 2 al ### 清单 5 ``` -$ go test -run none -bench AlgorithmOne -benchtime 3s -benchmem -memprofile mem.out +$ Go test -run none -bench AlgorithmOne -benchtime 3s -benchmem -memprofile mem.out BenchmarkAlgorithmOne-8 2000000 2570 ns/op 117 B/op 2 allocs/op ``` @@ -163,7 +163,7 @@ total 9248 ### 清单 7 ``` -$ go tool pprof -alloc_space memcpu.test mem.out +$ Go tool pprof -alloc_space memcpu.test mem.out Entering interactive mode (type "help" for commands) (pprof) _ ``` @@ -402,7 +402,7 @@ func algOne(data []byte, find []byte, repl []byte, output *bytes.Buffer) { ### 清单 19 ``` -$ go test -run none -bench AlgorithmOne -benchtime 3s -benchmem -memprofile mem.out +$ Go test -run none -bench AlgorithmOne -benchtime 3s -benchmem -memprofile mem.out BenchmarkAlgorithmOne-8 2000000 1814 ns/op 5 B/op 1 allocs/op ``` @@ -411,7 +411,7 @@ BenchmarkAlgorithmOne-8 2000000 1814 ns/op 5 B/op 1 allo ### 清单 20 ``` -$ go tool pprof -alloc_space memcpu.test mem.out +$ Go tool pprof -alloc_space memcpu.test mem.out Entering interactive mode (type "help" for commands) (pprof) list algOne Total: 7.50MB @@ -439,7 +439,7 @@ ROUTINE ======================== .../memcpu.BenchmarkAlgorithmOne in code/go/src ### 清单 21 ``` -$ go build -gcflags "-m -m" +$ Go build -gcflags "-m -m" ./stream.go:89: make([]byte, size) escapes to heap ./stream.go:89: from make([]byte, size) (too large for stack) at ./stream.go:89 ``` @@ -461,7 +461,7 @@ $ go build -gcflags "-m -m" ### 清单 23 ``` -$ go test -run none -bench AlgorithmOne -benchtime 3s -benchmem +$ Go test -run none -bench AlgorithmOne -benchtime 3s -benchmem BenchmarkAlgorithmOne-8 3000000 1720 ns/op 0 B/op 0 allocs/op ``` @@ -470,7 +470,7 @@ BenchmarkAlgorithmOne-8 3000000 1720 ns/op 0 B/op 0 allocs ### 清单 24 ``` -$ go build -gcflags "-m -m" +$ Go build -gcflags "-m -m" ./stream.go:83: algOne &bytes.Buffer literal does not escape ./stream.go:89: algOne make([]byte, 5) does not escape ``` diff --git a/published/tech/Language-Mechanics/Language-Mechanics-On-Stacks-And-Pointers.md b/published/tech/Language-Mechanics/Language-Mechanics-On-Stacks-And-Pointers.md index 98d5b8717..8df02d529 100644 --- a/published/tech/Language-Mechanics/Language-Mechanics-On-Stacks-And-Pointers.md +++ b/published/tech/Language-Mechanics/Language-Mechanics-On-Stacks-And-Pointers.md @@ -55,9 +55,9 @@ func increment(inc int) { } ``` -程序启动后,语言运行环境会创建 main goroutine 来执行包含在函数 main 内的所有初始化代码。goroutine 是被放置在操作系统线程上的可执行序列,在 Go 语言的1.8版本中,为每一个 goroutine 分配了 2048 byte 的连续内存作为它的栈空间。这个初始化的内存大小几年来一直在变化,而且未来很有可能继续变化。 +程序启动后,语言运行环境会创建 main Goroutine 来执行包含在函数 main 内的所有初始化代码。goroutine 是被放置在操作系统线程上的可执行序列,在 Go 语言的 1.8 版本中,为每一个 Goroutine 分配了 2048 byte 的连续内存作为它的栈空间。这个初始化的内存大小几年来一直在变化,而且未来很有可能继续变化。 -栈在 Go 语言中是非常重要的,因为它为分配给每个函数的帧边界提供了物理内存空间。main goroutine 在执行表 1 中的代码时,goroutine 的栈看起来像下面这个样子(在一个比较高的语言层次) +栈在 Go 语言中是非常重要的,因为它为分配给每个函数的帧边界提供了物理内存空间。main Goroutine 在执行表 1 中的代码时,goroutine 的栈看起来像下面这个样子(在一个比较高的语言层次) ## 图 1 @@ -93,7 +93,7 @@ func increment(inc int) { 12 increment(count) ``` -函数调用意味着 goroutine 需要在栈空间中创建一个新的栈帧。然而,这里并没有这么简单。要成功的调用一个函数,需要将数据在上下文转换过程中跨栈帧边界传递到新建的栈帧中。特别的,对于 integer 值,在调用过程中需要拷贝并传递过去,在第 18 行对函数 increment 的声明语句中可以看到这一点: +函数调用意味着 Goroutine 需要在栈空间中创建一个新的栈帧。然而,这里并没有这么简单。要成功的调用一个函数,需要将数据在上下文转换过程中跨栈帧边界传递到新建的栈帧中。特别的,对于 integer 值,在调用过程中需要拷贝并传递过去,在第 18 行对函数 increment 的声明语句中可以看到这一点: ### 清单 5 @@ -109,7 +109,7 @@ func increment(inc int) { ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/lang-mechanics/80_figure2.png) -可以看到,现在在栈里有两个栈帧, 一个是函数 main 的,它下面的是函数 increment 的。在函数 increment 栈帧里,有一个变量 inc,它的值是当函数调用时从外面拷贝并传递过来的 10,它的地址是 0x10429f98,因为栈帧是从上往下使用栈空间的,所以它的地址比上面的小,不过这只是一个实现细节,并不保证所有实现都这样。重要的是 goroutine 把函数 main 的栈帧中的变量 count 的值拷贝并传递给了函数 increment 的栈帧中的变量 inc。 +可以看到,现在在栈里有两个栈帧, 一个是函数 main 的,它下面的是函数 increment 的。在函数 increment 栈帧里,有一个变量 inc,它的值是当函数调用时从外面拷贝并传递过来的 10,它的地址是 0x10429f98,因为栈帧是从上往下使用栈空间的,所以它的地址比上面的小,不过这只是一个实现细节,并不保证所有实现都这样。重要的是 Goroutine 把函数 main 的栈帧中的变量 count 的值拷贝并传递给了函数 increment 的栈帧中的变量 inc。 函数 increment 剩下的代码显示了变量 inc 的值和地址: @@ -277,7 +277,7 @@ func increment(inc *int) { 3. 按值传递的优点是可读性好 4. 栈是非常重要的,因为它为分配给每个函数的帧边界提供了可访问的物理内存空间 5. 在活动栈帧以下的栈空间是不可用的,只有活动栈帧和它之上的栈空间是可用的 -6. 函数调用意味着 goroutine 需要在栈上为函数创建一个新的栈帧 +6. 函数调用意味着 Goroutine 需要在栈上为函数创建一个新的栈帧 7. 只有当发生了函数调用 ,栈区块被分配的栈帧占用后,相应栈空间才会被初始化 8. 使用指针是为了和被调用函数共享变量,使被调用函数可以用间接方式访问自己栈帧之外的变量 9. 每一个类型,不管是语言内置的还是用户定义的,都有一个与之对应的指针类型 diff --git a/published/tech/Lesser-Known-Features-of-Go-Test.md b/published/tech/Lesser-Known-Features-of-Go-Test.md index 630235ddd..bbb97c202 100644 --- a/published/tech/Lesser-Known-Features-of-Go-Test.md +++ b/published/tech/Lesser-Known-Features-of-Go-Test.md @@ -76,7 +76,7 @@ func TestCountMallocs(t *testing.T) { if testing.Short() { t.Skip("skipping malloc count in short mode") } - // rest of test... + // REST of test... } ``` @@ -117,7 +117,7 @@ func TestParallel(t *testing.T) { 为了保持可控性,`-p` 标志可以用来指定编译和测试的并发数。当仓库中有多个测试包,并且每个包在不同的子目录中,一个可以执行所有包的命令是 `go test ./...`,这包含当前目录和所有子目录。没有带 `-p` 标志执行时,总的运行时间应该接近于运行时间最长的包的时间(加上编译时间)。运行 `go test -p 1 ./...`,使编译和测试工具只能在一个包中执行时,总的时间应该接近于所有独立的包测试的时间加上编译的时间的总和。你可以自己试试,执行 `go test -p 3 ./...`,看一下对运行时间的影响。 -还有,另外一个可以并行化的地方(你应该测试一下)是在包的代码里面。多亏了 Go 非常棒的并行原语,实际上,除非 GOMAXPROCS 通过环境变量或者在代码中显式设置为 GOMAXPROCS=1,否则,包中一个goroutines 都没有用是不太常见的。想要使用 2 个 CPU,可以执行 `GOMAXPROCS=2 go test`,想要使用 4 个 CPU,可以执行 `GOMAXPROCS=4 go test`,但还有更好的方法:`go test -cpu=1,2,4` 将会执行 3 次,其中 GOMAXPROCS 值分别为 1,2,和 4。 +还有,另外一个可以并行化的地方(你应该测试一下)是在包的代码里面。多亏了 Go 非常棒的并行原语,实际上,除非 GOMAXPROCS 通过环境变量或者在代码中显式设置为 GOMAXPROCS=1,否则,包中一个 goroutines 都没有用是不太常见的。想要使用 2 个 CPU,可以执行 `GOMAXPROCS=2 Go test`,想要使用 4 个 CPU,可以执行 `GOMAXPROCS=4 Go test`,但还有更好的方法:`go test -cpu=1,2,4` 将会执行 3 次,其中 GOMAXPROCS 值分别为 1,2,和 4。 `-cpu` 标志,搭配数据竞争的探测标志 `-race`,简直进入天堂(或者下地狱,取决于它具体怎么运行)。竞争探测是一个很神奇的工具,在以高并发为主的开发中不得不使用这个工具(来防止死锁问题),但对它的讨论已经超过了本文的范围。如果你对此感兴趣,可以阅读 Go 官方博客的 [这篇文章](http://blog.golang.org/race-detector)。 diff --git a/published/tech/Line-of-sight-in-code.md b/published/tech/Line-of-sight-in-code.md index acc2ff09d..fd549a974 100644 --- a/published/tech/Line-of-sight-in-code.md +++ b/published/tech/Line-of-sight-in-code.md @@ -2,9 +2,9 @@ # 代码中的缩进线 -![我在2016年伦敦Golang英国会议上谈论代码缩进线](https://raw.githubusercontent.com/studygolang/gctt-images/master/line-of-sight/1_CBjBs9EzL8q1AL6XvjjpJg.png) +![我在 2016 年伦敦 Golang 英国会议上谈论代码缩进线](https://raw.githubusercontent.com/studygolang/gctt-images/master/line-of-sight/1_CBjBs9EzL8q1AL6XvjjpJg.png) -在近期伦敦举行的 [Golang 英国会议](https://www.youtube.com/watch?v=yeetIgNeIkc) 上,我在[地道的Go 语言窍门](https://www.youtube.com/watch?v=yeetIgNeIkc) 交流([幻灯片](http://go-talks.appspot.com/github.com/matryer/present/idiomatic-go-tricks/main.slide#1))中讲到关于代码中的缩进线, 我想在这里稍微解释一下。 +在近期伦敦举行的 [Golang 英国会议](https://www.youtube.com/watch?v=yeetIgNeIkc) 上,我在[地道的 Go 语言窍门](https://www.youtube.com/watch?v=yeetIgNeIkc) 交流([幻灯片](http://go-talks.appspot.com/github.com/matryer/present/idiomatic-go-tricks/main.slide#1))中讲到关于代码中的缩进线, 我想在这里稍微解释一下。 > 缩进线是“观察者无障碍视线的直线” diff --git a/published/tech/Linked-lists.md b/published/tech/Linked-lists.md index 48c539306..ab391e928 100644 --- a/published/tech/Linked-lists.md +++ b/published/tech/Linked-lists.md @@ -102,9 +102,9 @@ Second: &{Dolor sit amet 1257894000 } 可以看出,当我们在 `Feed` 添加第一个 `Post` 后,它的长度为 `1` 并且第一个 `Post` 拥有一个 `body` 和一个 `publishDate` (作为 Unix 时间戳),与此同时,它的 `next` 值为 `nil` 。 然后,我们将第二个 `Post` 添加到 `Feed` 中,当我们查看两个 `Posts` 时,我们会看到第一个 `Post` 的内容与之前的内容相同,但它的指针指向列表中的下一个 `Post` 。 第二个 `Post` 也有一个 `body` 和一个 `publishDate` ,但是没有指向列表中的下一个 `Post` 的指针。 此外,当我们添加更多的 `Posts` 时,`Feed` 的长度也会增加。 -现在让我们回过头来看 `Append` 函数并解构它,这样我们就能更好地理解如何使用链表。 首先,该函数创建一个指向 `Post` 值的指针,将 `body` 参数作为 `Post`的 `body`,并将 `publishDate` 设置为当前时间的 Unix 时间戳表示。 +现在让我们回过头来看 `Append` 函数并解构它,这样我们就能更好地理解如何使用链表。 首先,该函数创建一个指向 `Post` 值的指针,将 `body` 参数作为 `Post` 的 `body`,并将 `publishDate` 设置为当前时间的 Unix 时间戳表示。 -然后,我们检查 `Feed` 的`length`是否为 `0` — 这意味着它没有 `Post` 。第一个被添加的 `Post` 会被设为起始 `Post`,为方便起见,我们把它命名为 `start`。 +然后,我们检查 `Feed` 的 `length` 是否为 `0` — 这意味着它没有 `Post` 。第一个被添加的 `Post` 会被设为起始 `Post`,为方便起见,我们把它命名为 `start`。 但是,如果 `Feed` 的长度大于 0 ,那么我们的算法就会发生不同的变化。 它将从 `Feed` 的 `start` 开始,它将遍历所有的 `Post`,直到找到一个没有指向 `next` 的指针。 然后,它将把新的 `Post` 附加到列表的最后一个 `Post` 上。 @@ -126,7 +126,7 @@ type Feed struct { } ``` -但是,我们的 `Append` 算法必须被调整,以适应 `Feed` 的新结构。这是使用 `Post`的 `end` 属性的 `Append` 的版本: +但是,我们的 `Append` 算法必须被调整,以适应 `Feed` 的新结构。这是使用 `Post` 的 `end` 属性的 `Append` 的版本: ```go func (f *Feed) Append(newPost *Post) { @@ -149,7 +149,7 @@ func (f *Feed) Append(newPost *Post) { 我们在重新审视一下算法,它做了两件事情:如果 `Feed` 为空,它就会设置一个新的 `Post` 作为 `Feed` 的开头和结尾,反之它会设置一个新的 `Post` 作为 `end` 项并且依附到链表中先前的 `Post` 。很重要的一点是它很简单,并且算法复杂度为 `O(1)` ,也称为常数时间复杂度。这意味着无论 `Feed` 结构的长度如何,`Append` 都将执行相同的操作。 -很简单,对吧?但让我们想象一下,`Feed` 实际上是我们的配置文件中的 `Post`列表。因此,它们是我们的,我们应该能够删除它们。我的意思是,什么样的社交网络不允许用户(至少)删除他们的帖子? +很简单,对吧?但让我们想象一下,`Feed` 实际上是我们的配置文件中的 `Post` 列表。因此,它们是我们的,我们应该能够删除它们。我的意思是,什么样的社交网络不允许用户(至少)删除他们的帖子? ## 移除一个 `Post` diff --git a/published/tech/Making-and-Using-HTTP-Middleware.md b/published/tech/Making-and-Using-HTTP-Middleware.md index a2c318a1b..f4ad0625b 100644 --- a/published/tech/Making-and-Using-HTTP-Middleware.md +++ b/published/tech/Making-and-Using-HTTP-Middleware.md @@ -100,7 +100,7 @@ func main() { 运行这个应用程序并向 `http://localhost:3000` 发出请求。你应该会得到这样的日志输出: ``` -$ go run main.go +$ Go run main.go 2014/10/13 20:27:36 Executing middlewareOne 2014/10/13 20:27:36 Executing middlewareTwo 2014/10/13 20:27:36 Executing finalHandler @@ -205,7 +205,7 @@ OK goji/httpauth 包提供了 HTTP 基本的认证功能。它有一个 [SimpleBasicAuth](https://godoc.org/github.com/goji/httpauth#SimpleBasicAuth) helper,它返回一个带有签名的 `func (http.Handler) http.Handler` 函数。这意味着我们可以像我们定制的中间层一样(的方式)使用它。 ``` -$ go get github.com/goji/httpauth +$ Go get github.com/goji/httpauth ``` ``` @@ -333,7 +333,7 @@ $ cat server.log ## 附加工具(Additional Tools) -[由 Justinas Stankevičius 编写的 Alice](https://github.com/justinas/alice) 是一个非常聪明并且轻量级的包,它为连接中间层处理程序提供了一些语法糖。在最基础的方面,Alice 允许你重写这个: +[由 Justinas Stankevi č ius 编写的 Alice](https://github.com/justinas/alice) 是一个非常聪明并且轻量级的包,它为连接中间层处理程序提供了一些语法糖。在最基础的方面,Alice 允许你重写这个: `http.Handle("/", myLoggingHandler(authHandler(enforceXMLHandler(finalHandler))))` diff --git a/published/tech/Measuring-progress-and-estimating-time-remaining-of-long-running-tasks-in-Go.md b/published/tech/Measuring-progress-and-estimating-time-remaining-of-long-running-tasks-in-Go.md index 80fdc6d09..d5264e9ae 100644 --- a/published/tech/Measuring-progress-and-estimating-time-remaining-of-long-running-tasks-in-Go.md +++ b/published/tech/Measuring-progress-and-estimating-time-remaining-of-long-running-tasks-in-Go.md @@ -43,7 +43,7 @@ func (r *Reader) N() int64 { 试试看你能不在自己写出 Writer 的计数部分,两者很类似。 -由于方法 N 返回( 基于 atomic.LoadInt64 的安全调用)读取到的字节数,我们能在任意时刻使用另一个 goroutine 调用它,从而获取当前状况。 +由于方法 N 返回( 基于 atomic.LoadInt64 的安全调用)读取到的字节数,我们能在任意时刻使用另一个 Goroutine 调用它,从而获取当前状况。 ## 获取总共的字节数 @@ -128,7 +128,7 @@ duration := estimated.Sub(time.Now()) ### 小助手 -我们还添加了一个小助手,它可以给你一个进度上的 go channel 来周期性报告。 你可以开启一个新的 goroutine 并打印进度,或更新进度,这取决于您的用例。 +我们还添加了一个小助手,它可以给你一个进度上的 Go channel 来周期性报告。 你可以开启一个新的 Goroutine 并打印进度,或更新进度,这取决于您的用例。 ```go ctx := context.Background() @@ -138,7 +138,7 @@ s := `Now that's what I call progress` size := len(s) r := progress.NewReader(strings.NewReader(s)) -// 开启一个 goroutine 打印进度 +// 开启一个 Goroutine 打印进度 go func() { progressChan := progress.NewTicker(ctx, r, size, 1*time.Second) for p := range <-progressChan { diff --git a/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-1.md b/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-1.md index aacf691b9..de4969995 100644 --- a/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-1.md +++ b/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-1.md @@ -25,10 +25,10 @@ Golang 中的微服务系列总计十部分,预计每周更新。本系列的 ## 先决条件 -- 掌握 golang 语言和其开发环境 +- 掌握 Golang 语言和其开发环境 - 安装 gRPC / protobuf [查看链接](https://grpc.io/docs/quickstart/go.html) -- 安装 golang [查看链接](https://golang.org/doc/install) -- 按照下列指令,安装 go 的第三方库 +- 安装 Golang [查看链接](https://golang.org/doc/install) +- 按照下列指令,安装 Go 的第三方库 ``` go get -u google.golang.org/grpc @@ -167,7 +167,7 @@ func (repo *Repository) Create(consignment *pb.Consignment) (*pb.Consignment, er } // 服务需要实现所有在 protobuf 里定义的方法。 -// 你可以参考 protobuf 生成的 go 文件中的接口信息。 +// 你可以参考 protobuf 生成的 Go 文件中的接口信息。 type service struct { repo IRepository } @@ -207,7 +207,7 @@ func main() { } ``` -请仔细阅读代码中的注释,有助于你对这个服务的理解。简单来说,这些代码实现的功能是:在 50051 端口创建一个的 gRPC 服务器,通过 protobuf 生成的消息格式,实现 gRPC 接口交互的逻辑。就这样,你完成了一个完整功能的 gRPC 服务!你可以输入指令 `$ go run main.go` 来运行这个程序,不过,目前,从界面上你还看不到任何东西。那如何能直观看到这个 gRPC 服务器正常工作了呢?我们来一起创建个与它对接的客户端吧! +请仔细阅读代码中的注释,有助于你对这个服务的理解。简单来说,这些代码实现的功能是:在 50051 端口创建一个的 gRPC 服务器,通过 protobuf 生成的消息格式,实现 gRPC 接口交互的逻辑。就这样,你完成了一个完整功能的 gRPC 服务!你可以输入指令 `$ Go run main.go` 来运行这个程序,不过,目前,从界面上你还看不到任何东西。那如何能直观看到这个 gRPC 服务器正常工作了呢?我们来一起创建个与它对接的客户端吧! 下面,我们来写一个命令行交互的程序,用来读取一个包含委托信息的 JSON 文件,和我们已创建的 gRPC 服务器交互。 @@ -285,7 +285,7 @@ func main() { } ``` -完成以上步骤后,在 `consignment-service` 下运行 `$ go run main.go`,然后打开一个新的终端界面,运行 `$ go run cli.go`,这时你就能看到一条消息 `Created: true`。不过,如何我们才能确认,这个委托真正地生成了?让我们继续更新我们的服务,添加一个 `GetConsignments` 方法,能够看到所有已创建的委托。 +完成以上步骤后,在 `consignment-service` 下运行 `$ Go run main.go`,然后打开一个新的终端界面,运行 `$ Go run cli.go`,这时你就能看到一条消息 `Created: true`。不过,如何我们才能确认,这个委托真正地生成了?让我们继续更新我们的服务,添加一个 `GetConsignments` 方法,能够看到所有已创建的委托。 首先需要更新我们的 proto 定义(我在修改部分添加了备注) @@ -376,7 +376,7 @@ func (repo *Repository) GetAll() []*pb.Consignment { } // 服务需要实现所有在 protobuf 里定义的方法。 -// 你可以参考 protobuf 生成的 go 文件中的接口信息。 +// 你可以参考 protobuf 生成的 Go 文件中的接口信息。 type service struct { repo IRepository } @@ -428,7 +428,7 @@ func main() { ```go func main() { ... - // ·...`表示和之前代码一致,这里不再重复 + // ·...` 表示和之前代码一致,这里不再重复 getAll, err := client.GetConsignments(context.Background(), &pb.GetRequest{}) if err != nil { @@ -440,7 +440,7 @@ func main() { } ``` -在原先 main 函数中,找到打印 `Created: success` 日志的位置,在这之后添加上述代码,然后运行 `$ go run cli.go`。程序就会创建一个委托,紧接着调用 `GetConsignments`。当你运行次数越多,委托列表就会越来越长。 +在原先 main 函数中,找到打印 `Created: success` 日志的位置,在这之后添加上述代码,然后运行 `$ Go run cli.go`。程序就会创建一个委托,紧接着调用 `GetConsignments`。当你运行次数越多,委托列表就会越来越长。 *注意:为了看起来简洁,我有时会用 `...` 来表示和之前的代码完全一致。之后几行新增的代码,需要手动添加到原代码中* @@ -450,7 +450,7 @@ func main() { 如果对此文有任何 bug、错误或者反馈,请直接邮箱[联系我](ewan.valentine89@gmail.com)。 -本教程所包含的代码库[链接](https://github.com/ewanvalentine/shippy),用 `git` 工具 checkout 分支`tutorial-1` 第一章。第二章也将在近期更新。 +本教程所包含的代码库[链接](https://github.com/ewanvalentine/shippy),用 `git` 工具 checkout 分支 `tutorial-1` 第一章。第二章也将在近期更新。 编写本文花了我很长的时间以及大量的精力。如果你觉得这个系列有帮助,请考虑顺手打赏我(完全取决于你的意愿)。十分感谢![https://monzo.me/ewanvalentine](https://monzo.me/ewanvalentine) diff --git a/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-3.md b/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-3.md index cd44201a6..0e44dbc35 100644 --- a/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-3.md +++ b/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-3.md @@ -1,6 +1,6 @@ 已发布:https://studygolang.com/articles/12452 -# golang 中的微服务 - 第 3 部分 - docker compose 和 datastores +# Golang 中的微服务 - 第 3 部分 - docker compose 和 datastores 在[之前的文章](https://studygolang.com/articles/12094)中,我们介绍了 [go-micro](https://github.com/micro/go-micro) 和 [Docker](https://www.docker.com/) 的一些基础知识。在推出了这两项服务之后我们将在本文介绍 [docker-compose](https://docs.docker.com/compose/)、教大家如何更便捷地在本地运行多个服务,还会列述一些在本系列微服务教程中可以使用的数据库类型,最后引出本系列的第三项服务 —— User service。 @@ -18,7 +18,7 @@ docker-compose 安装完毕之后,我们来介绍一些可用的数据库以 微服务的优点是,你可以为每个服务选择一个不同的数据库。当然许多情况下我们不必这样做。例如生产环境中的小团队完全不必选择多个数据库,这样会增加维护成本。但在某些情况下,一个服务的数据可能并不兼容其他服务的数据库,这时不得不增加一个数据库。微服务使得数据兼容这件事变得更加简单,你完全不必操心不同服务的数据使用不同的数据库带来的额外维护成本。 -本文不会解释如何为你的服务选择“正确的”数据库,这是一个值得深入探究的话题,详情可以借鉴[如何为你的服务选择“正确的”数据库](https://www.infoworld.com/article/3236291/database/how-to-choose-a-database-for-your-microservices.html)。本文示例的数据持久化选择 NoSQL 文档存储解决方案,NoSQL 更适用于处理大量松散且不一致的数据集,例如 json 存储数据更加灵活,我会选择效果良好并且社区服务更加完善的 MongoDB 作为我们的 NoSQL。 +本文不会解释如何为你的服务选择“正确的”数据库,这是一个值得深入探究的话题,详情可以借鉴[如何为你的服务选择“正确的”数据库](https://www.infoworld.com/article/3236291/database/how-to-choose-a-database-for-your-microservices.html)。本文示例的数据持久化选择 NoSQL 文档存储解决方案,NoSQL 更适用于处理大量松散且不一致的数据集,例如 JSON 存储数据更加灵活,我会选择效果良好并且社区服务更加完善的 MongoDB 作为我们的 NoSQL。 如果需要处理的数据是被严格定义并且联系紧密,那么可以使用传统的关系型数据库(RDBMS),但实际上并非一定要这么做。在选择之前一定考虑我们服务的数据结构,它是做的读操作更多还是写操作更多?查询的复杂程度如何?等等。这些才是我们选择使用何种数据库的出发点。由于个人原因,关系数据库我更喜欢使用 Postgres,当然,你也可以使用 MySQL 或者 MariaDB 等等。 @@ -38,7 +38,7 @@ docker-compose 安装完毕之后,我们来介绍一些可用的数据库以 ## docker-compose -上一篇文章我们介绍了 [Docker](https://docker.com/) ,它可以用轻量级的容器运行我们的服务,并拥有自己独立的运行时间和依赖关系。但是服务数量较多的情景下使用单独的 Makefile 运行和管理每个服务太麻烦了。[docker-compose](https://docs.docker.com/compose/) 应运而生,它很好的帮我们解决了这一问题。 Docker-compose 允许我们在 yaml 文件中定义 Docker 容器列表,并指定关于其运行时间的元数据。我们可以从 Docker-compose 中看到一些熟悉的 docker 命令的影子。例如: +上一篇文章我们介绍了 [Docker](https://docker.com/) ,它可以用轻量级的容器运行我们的服务,并拥有自己独立的运行时间和依赖关系。但是服务数量较多的情景下使用单独的 Makefile 运行和管理每个服务太麻烦了。[docker-compose](https://docs.docker.com/compose/) 应运而生,它很好的帮我们解决了这一问题。 Docker-compose 允许我们在 YAML 文件中定义 Docker 容器列表,并指定关于其运行时间的元数据。我们可以从 Docker-compose 中看到一些熟悉的 docker 命令的影子。例如: ``` $ docker run -p 50052:50051 -e MICRO_SERVER_ADDRESS =:50051 -e MICRO_REGISTRY = mdns vessel-service @@ -67,7 +67,7 @@ services: $ touch docker-compose.yml ``` -然后在这个 yml 文件里添加我们的服务: +然后在这个 YAML 文件里添加我们的服务: ``` # docker-compose.yml @@ -99,13 +99,13 @@ services: ``` -这个 yml 文件首先定义了要使用的 docker-compose 的版本号`3.1`,然后定义一个 service 列表。还有其他的根级定义,比如网络和卷。 +这个 YAML 文件首先定义了要使用的 docker-compose 的版本号 `3.1`,然后定义一个 service 列表。还有其他的根级定义,比如网络和卷。 先关注 service ,每个 service 都由其名称定义,然后我们加入了一个 build 路径,这里要存放我们的 Dockerfile 文件,docker-compose 会从这个路径下寻找 Dockerfile 来构建镜像,后文我们会演示如何用 `image` 字段来引用一个构建好的镜像。然后我们还可以定义映射端口和环境变量。 这些是 docker-compose 的基本命令: -* 构建你的docker-compose集: `$ docker-compose build && docker-compose run` +* 构建你的 docker-compose 集: `$ docker-compose build && docker-compose run` * 在后台运行你的容器集: `$ docker-compose up -d` * 查看当前正在运行的容器的列表: `$ docker ps` * 停止正在运行的所有容器: `$ docker stop $(docker ps -qa)` @@ -116,7 +116,7 @@ services: ## 容器实体和 protobufs -本系列前面的文章已经讲过使用 [protobufs](https://www.ibm.com/developerworks/cn/linux/l-cn-gpb/) 作为数据模型的模板。我们用它来定义我们的服务结构和功能函数。因为 protobuf 生成的结构基本上都是正确的数据类型,我们也可以将这些结构重复利用为底层数据库模型。这实际上是相当令人兴奋的。protobufs保证了数据的来源单一性和一致性。 +本系列前面的文章已经讲过使用 [protobufs](https://www.ibm.com/developerworks/cn/linux/l-cn-gpb/) 作为数据模型的模板。我们用它来定义我们的服务结构和功能函数。因为 protobuf 生成的结构基本上都是正确的数据类型,我们也可以将这些结构重复利用为底层数据库模型。这实际上是相当令人兴奋的。protobufs 保证了数据的来源单一性和一致性。 当然这种方法确有其不足之处。有时候,将 protobuf 生成的代码封装到一个有效的数据库实体是非常棘手的。有时数据库很难利用 protobuf 生成的自定义本地数据类型。例如,如何将一个 Mongodb 实体里的 `bson.ObjectId` 类型的 ID 转换成 `string` 类型的 ID 就困扰了我很久。后来通过实验论证,无论如何 bson.ObjectId 实际上只是一个字符串,所以你可以把它们封装在一起。此外,mongodb 的 id 索引在内部被存储为 `_id`,该 `_Id` 字段是无法被执行的,所以必须要将它与 Id 字符串字段绑定在一起。这意味着你要为你的 protobuf 文件定义自定义标签。稍后我们讨论如何做到这一点。 @@ -142,7 +142,7 @@ func (service *Service) (ctx context.Context, req *proto.User, res *proto.Respon 接下来可以开始连接我们的第一个服务,consignment(委托)服务。 -先整理一下代码文件: main.go 文件已经包含了我们所有的逻辑代码。为了使我们的微服务示例代码更加清晰,我在 consignment 服务的根目录下创建了另外几个文件:`handler.go`、`datastore.go`和`respository.go`,而不是将这些代码文件创建为一个新的目录或包。这种方式对于一个小型的微服务来说是完全可行的。插一句题外话,对于开发人员来说,可能会非常喜欢使用这样的目录结构来存放自己的代码文件: +先整理一下代码文件: main.go 文件已经包含了我们所有的逻辑代码。为了使我们的微服务示例代码更加清晰,我在 consignment 服务的根目录下创建了另外几个文件:`handler.go`、`datastore.go` 和 `respository.go`,而不是将这些代码文件创建为一个新的目录或包。这种方式对于一个小型的微服务来说是完全可行的。插一句题外话,对于开发人员来说,可能会非常喜欢使用这样的目录结构来存放自己的代码文件: ``` main.go @@ -155,7 +155,7 @@ services/ auth.go ``` -这是MVC的常见目录结构,但 Golang 并不建议这样做。 就目录结构而言,无论是简单的小项目还是需要处理多个复杂关系的大项目,Golang 的建议是这样的: +这是 MVC 的常见目录结构,但 Golang 并不建议这样做。 就目录结构而言,无论是简单的小项目还是需要处理多个复杂关系的大项目,Golang 的建议是这样的: ``` main.go @@ -176,19 +176,19 @@ containers/ ``` 这里是按函数的定义域分组代码,而不是随意地将代码按其功能分组。 -但是,由于我们正在处理的是一个只需要关注单一简单问题的微服务,所以我们不需要将目录结构考虑得太复杂。实际上,Go的精神就是鼓励简单。 所以我们将从简单的一步开始,把所有简易命名的代码文件都放在我们的服务的根目录中。 +但是,由于我们正在处理的是一个只需要关注单一简单问题的微服务,所以我们不需要将目录结构考虑得太复杂。实际上,Go 的精神就是鼓励简单。 所以我们将从简单的一步开始,把所有简易命名的代码文件都放在我们的服务的根目录中。 -一方面,我们要修改 Dockerfile 文件的内容,因为我们没有将新代码分离出来作为包导入,我们需要在 Dockerfile 文件里告诉go编译器来引入这些新文件并更新构建函数: +一方面,我们要修改 Dockerfile 文件的内容,因为我们没有将新代码分离出来作为包导入,我们需要在 Dockerfile 文件里告诉 go 编译器来引入这些新文件并更新构建函数: ``` -RUN CGO_ENABLED=0 GOOS=linux go build -o consignment-service -a -installsuffix cgo main.go repository.go handler.go datastore.go +RUN CGO_ENABLED=0 GOOS=linux Go build -o consignment-service -a -installsuffix cgo main.go repository.go handler.go datastore.go ``` 这条命令会将我们刚刚创建的新文件导入。 -[Golang 编写的 MongoDB driver 就是这种简单性的一个很好的例子](https://github.com/go-mgo/mgo)。最后,这里有[一篇关于组织Go代码库的文章](https://studygolang.com/articles/11823)推荐给大家学习。 +[Golang 编写的 MongoDB driver 就是这种简单性的一个很好的例子](https://github.com/go-mgo/mgo)。最后,这里有[一篇关于组织 Go 代码库的文章](https://studygolang.com/articles/11823)推荐给大家学习。 -我们可以先删除 main.go 中已经导入的所有仓库代码然后使用 golang 的 mongodb 库 mgo 来重新实现它。我在代码里进行了详细的标注以解释每部分的作用,因此请仔细阅读代码和注释。 尤其是mgo如何处理 sessions 的部分: +我们可以先删除 main.go 中已经导入的所有仓库代码然后使用 Golang 的 MongoDB 库 mgo 来重新实现它。我在代码里进行了详细的标注以解释每部分的作用,因此请仔细阅读代码和注释。 尤其是 mgo 如何处理 sessions 的部分: ```go // consignment-service/repository.go @@ -219,7 +219,7 @@ func (repo *ConsignmentRepository) Create(consignment *pb.Consignment) error { return repo.collection().Insert(consignment) } -// 获取所有的consignments +// 获取所有的 consignments func (repo *ConsignmentRepository) GetAll() ([]*pb.Consignment, error) { var consignments []*pb.Consignment @@ -227,16 +227,16 @@ func (repo *ConsignmentRepository) GetAll() ([]*pb.Consignment, error) { //Find()通常需要一个参数,但如果我们想要返回所有的结果就将参数设为 nil //然后将所有的 consignments 作为参数传递给.All()函数, //.All()函数将所有的 consignments 作为查询的结果返回 - //在这里还可以调用 One()方法来返回一个单一的consignment + //在这里还可以调用 One()方法来返回一个单一的 consignment err := repo.collection().Find(nil).All(&consignments) return consignments, err } -// Close() 负责在每个查询运行结束后关闭数据库session。 +// Close() 负责在每个查询运行结束后关闭数据库 session。 // Mgo 在启动时创建一个主 session ,主 session 会为每个请求创建一个新的 session 。 这意味着每个请求都有自己的数据库 session,这样的机制会使得会话更安全、高效。更底层来讲,每个 session 中都有自己独立的数据库 socket 和错误处理机制。 -//使用一个主数据库 socket 意味着其余请求必须等待主 session 优先使用 cpu 资源。 -// I.e方法使得我们拒绝锁机制而允许多个请求同时处理。这一点很棒!但是...这意味着我们必须确保每个 session 在完成时关闭掉,于此同时你可能会建立大量的连接,以至于达到连接限制。这一点尤其需要注意!! +//使用一个主数据库 socket 意味着其余请求必须等待主 session 优先使用 CPU 资源。 +// I.e 方法使得我们拒绝锁机制而允许多个请求同时处理。这一点很棒!但是...这意味着我们必须确保每个 session 在完成时关闭掉,于此同时你可能会建立大量的连接,以至于达到连接限制。这一点尤其需要注意!! func (repo *ConsignmentRepository) Close() { repo.session.Close() @@ -248,7 +248,7 @@ func (repo *ConsignmentRepository) collection() *mgo.Collection { ``` -接下来需要编写与 Mongodb 数据库交互的代码,创建主会话/连接。 按照如下所示修改`consignment-service/datastore.go`: +接下来需要编写与 Mongodb 数据库交互的代码,创建主会话/连接。 按照如下所示修改 `consignment-service/datastore.go`: ```go // consignment-service/datastore.go @@ -258,7 +258,7 @@ import ( "gopkg.in/mgo.v2" ) -// CreateSession() 创建了连接到 mongodb 的主 session +// CreateSession() 创建了连接到 MongoDB 的主 session func CreateSession(host string) (*mgo.Session, error) { session, err := mgo.Dial(host) if err != nil { @@ -271,7 +271,7 @@ func CreateSession(host string) (*mgo.Session, error) { } ``` -就是这样,非常简单。下一步修改`main.go`文件用以连接 repository,它将一个主机字符串作为参数,返回了一个连接到数据库的 session 和一个可能出现的 error,以便程序启动时可以处理这个 error。 +就是这样,非常简单。下一步修改 `main.go` 文件用以连接 repository,它将一个主机字符串作为参数,返回了一个连接到数据库的 session 和一个可能出现的 error,以便程序启动时可以处理这个 error。 ```go // consignment-service/main.go @@ -305,7 +305,7 @@ func main() { session, err := CreateSession(host) - // Mgo 创建的主 session必须于 main() 函数结束之前关闭 + // Mgo 创建的主 session 必须于 main() 函数结束之前关闭 defer session.Close() if err != nil { @@ -328,10 +328,10 @@ func main() { // srv.Init() - // 注册调度器Register handler + // 注册调度器 Register handler pb.RegisterShippingServiceHandler(srv.Server(), &service{session, vesselClient}) - // 启动server + // 启动 server if err := srv.Run(); err != nil { fmt.Println(err) } @@ -340,11 +340,11 @@ func main() { ## Copy vs Clone -你可能已经注意到使用 mgo Mongodb 库时。我们会创建一个被传递给 handlers 的数据库 session,每出现一个请求,就会调用一个该session的克隆方法。 +你可能已经注意到使用 mgo Mongodb 库时。我们会创建一个被传递给 handlers 的数据库 session,每出现一个请求,就会调用一个该 session 的克隆方法。 实际上,除了建立与数据库的第一次连接之外,我们从不调用“主会话”,每次我们要访问数据存储时都调用 session.Clone() 方法。如代码注释所言,若使用主会话,则必须再次使用相同的套接字。这意味着您的查询可能会被其他查询阻塞,必须等待此套接字的占用被锁释放。在支持并发的语言中这是绝对无法容忍的。 -所以为了避免阻塞请求,mgo 允许你 Copy() 或者 Clone() 一个会话,这样你就可以为每个请求建立一个并发连接。你会注意到我提到了 Copy 和 Clone 方法,这些方法非常相似,但有一个微妙但重要的区别:Clone重新使用和主会话相同的套接字,减少了产生一个新的套接字的开销,十分适用于对写入性能要求更高的代码。但是,在耗时较长的读操作(例如十分复杂的查询或大数据作业等)中其他 Go 协程使用这个相同套接字时可能会引发阻塞。 +所以为了避免阻塞请求,mgo 允许你 Copy() 或者 Clone() 一个会话,这样你就可以为每个请求建立一个并发连接。你会注意到我提到了 Copy 和 Clone 方法,这些方法非常相似,但有一个微妙但重要的区别:Clone 重新使用和主会话相同的套接字,减少了产生一个新的套接字的开销,十分适用于对写入性能要求更高的代码。但是,在耗时较长的读操作(例如十分复杂的查询或大数据作业等)中其他 Go 协程使用这个相同套接字时可能会引发阻塞。 像我们公司这样写入操作更多的业务,使用 Clone() 更合适一些。 @@ -439,7 +439,7 @@ type Repository interface { ``` -改成这样的原因是我认为我们在创建 Consignment 之后不需要再返回一个相同的 Consignment。为了防止可能出现的错误,get 查询还会返回一个 error,最后还要添加一个Close()方法。 +改成这样的原因是我认为我们在创建 Consignment 之后不需要再返回一个相同的 Consignment。为了防止可能出现的错误,get 查询还会返回一个 error,最后还要添加一个 Close()方法。 请对 vessel-service 做相同的修改。这篇文章不再赘述,你应该可以参考我的[代码库](https://github.com/EwanValentine/shippy/tree/tutorial-3)自行完成。 @@ -477,7 +477,7 @@ message Response { ``` -我们在gRPC服务下创建了一个新的Create方法,该方法需要一个 vessel 并返回 generic response。 我已经在 response message 中添加了一个 bool 类型的新字段:`created`。 这是你需要运行`$ make build`来更新这个服务。 现在我们将在`vessel-service / handler.go`中添加一个新的处理程序,并添加一个新的 repository 方法: +我们在 gRPC 服务下创建了一个新的 Create 方法,该方法需要一个 vessel 并返回 generic response。 我已经在 response message 中添加了一个 bool 类型的新字段:`created`。 这是你需要运行 `$ make build` 来更新这个服务。 现在我们将在 `vessel-service / handler.go` 中添加一个新的处理程序,并添加一个新的 repository 方法: ```go // vessel-service/handler.go @@ -509,7 +509,7 @@ func (repo *VesselRepository) Create(vessel *pb.Vessel) error { 终于可以创造 vessels 了! 为了使用新的 Create 方法来存储虚拟数据,我已经更新了 main.go ,[请看这里](https://github.com/EwanValentine/shippy/blob/master/vessel-service/main.go)。 -做完上述内容之后。 我们已经使用Mongodb更新了我们的服务。在尝试运行之前,需要更新我们的 docker-compose 文件来启动一个 Mongodb 容器: +做完上述内容之后。 我们已经使用 Mongodb 更新了我们的服务。在尝试运行之前,需要更新我们的 docker-compose 文件来启动一个 Mongodb 容器: ``` services: @@ -523,7 +523,7 @@ services: 更新两个服务中的环境变量:`DB_HOST:“datastore:27017”`。 -请注意,由于 docker-compose 为我们做了一些内部DNS处理, hostname 被命名为 datastore 而不是示例中的 localhost。 +请注意,由于 docker-compose 为我们做了一些内部 DNS 处理, hostname 被命名为 datastore 而不是示例中的 localhost。 所以最终的 docker-compose 文件应该更新为: @@ -561,11 +561,11 @@ services: - 27017:27017 ``` -重新 build 一下,运行`$ docker-compose build`然后运行`$ docker-compose up`。 请注意,有时候由于 Dockers 的缓存机制,在 build 时需要加一个`--no-cache` 参数来取消缓存。 +重新 build 一下,运行 `$ docker-compose build` 然后运行 `$ docker-compose up`。 请注意,有时候由于 Dockers 的缓存机制,在 build 时需要加一个 `--no-cache` 参数来取消缓存。 ## User service -User service 是我们创建的第三个服务。首先修改`docker-compose.yml`,微服务的概念之一就是将所有的东西全部集中起来服务化,所以我们将 Postgres 添加到 docker 容器集里面用来为我们的 User service 服务。 +User service 是我们创建的第三个服务。首先修改 `docker-compose.yml`,微服务的概念之一就是将所有的东西全部集中起来服务化,所以我们将 Postgres 添加到 docker 容器集里面用来为我们的 User service 服务。 ``` ... @@ -631,9 +631,9 @@ message Error { ``` -现在,确保你已经在根目录下创建了 Makefile 文件,按照前几个服务的 Makefile 文件照猫画虎写一个即可,接下来运行`$ make build`来生成 gRPC 代码。据以往经验,已经自动生成了一些连接的 gRPC 方法的代码。 本文只会讲解其中一小部分 service 的运行,其余 service 的运行详解会在本系列的其余文章给出。在本文我们只介绍 User service 如何创建和获取用户。 在本系列接下来的文章中,我们将讨论认证和 [JWT](https://www.jianshu.com/p/576dbf44b2ae) 的具体实现。阅读过程中请做好相关标记。 +现在,确保你已经在根目录下创建了 Makefile 文件,按照前几个服务的 Makefile 文件照猫画虎写一个即可,接下来运行 `$ make build` 来生成 gRPC 代码。据以往经验,已经自动生成了一些连接的 gRPC 方法的代码。 本文只会讲解其中一小部分 service 的运行,其余 service 的运行详解会在本系列的其余文章给出。在本文我们只介绍 User service 如何创建和获取用户。 在本系列接下来的文章中,我们将讨论认证和 [JWT](https://www.jianshu.com/p/576dbf44b2ae) 的具体实现。阅读过程中请做好相关标记。 -【译者注:本系列是 [Ewan Valentine](http://ewanvalentine.io/author/ewan) 编写的关于 golang 微服务的[长文教程系列](https://ewanvalentine.io/tag/golang/)第三篇,每一篇的讲解都很细致,建议大家结合作者[源码](https://github.com/EwanValentine/shippy.git)仔细将每一篇阅读完毕】 +【译者注:本系列是 [Ewan Valentine](http://ewanvalentine.io/author/ewan) 编写的关于 Golang 微服务的[长文教程系列](https://ewanvalentine.io/tag/golang/)第三篇,每一篇的讲解都很细致,建议大家结合作者[源码](https://github.com/EwanValentine/shippy.git)仔细将每一篇阅读完毕】 你的处理程序应该是这样的: @@ -746,7 +746,7 @@ func (repo *UserRepository) Create(user *pb.User) error { ``` -为了避免使用整数 ID ,我们还需要更改 ORM 行为,以便在创建时生成一个 [UUID](https://baike.baidu.com/item/UUID/5921266?fr=aladdin)。如果您不知道,UUID是一组随机生成的带有连字符的字符串,用作ID或主键。这比使用自动递增的整数 ID 更安全,因为它可以阻止人们猜测或遍历 API 节点。MongoDB 已经使用了 UUID,但我们需要告诉 Postgres 模型使用UUID。 因此,我们需要在`user-service/proto/user`中创建一个名为extensions.go的新文件,编码如下: +为了避免使用整数 ID ,我们还需要更改 ORM 行为,以便在创建时生成一个 [UUID](https://baike.baidu.com/item/UUID/5921266?fr=aladdin)。如果您不知道,UUID 是一组随机生成的带有连字符的字符串,用作 ID 或主键。这比使用自动递增的整数 ID 更安全,因为它可以阻止人们猜测或遍历 API 节点。MongoDB 已经使用了 UUID,但我们需要告诉 Postgres 模型使用 UUID。 因此,我们需要在 `user-service/proto/user` 中创建一个名为 extensions.go 的新文件,编码如下: ```go package go_micro_srv_user @@ -889,4 +889,4 @@ via: https://ewanvalentine.io/microservices-in-golang-part-3/ 译者:[张阳](https://github.com/zhangyang9) 校对:[polaris1119](https://github.com/polaris1119) -本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](Go语言中文网) 荣誉推出 +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](Go 语言中文网) 荣誉推出 diff --git a/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-4-Authentication-with-JWT.md b/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-4-Authentication-with-JWT.md index e0210ac01..9b399881c 100644 --- a/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-4-Authentication-with-JWT.md +++ b/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-4-Authentication-with-JWT.md @@ -68,13 +68,13 @@ func (srv *service) Create(ctx context.Context, req *pb.User, res *pb.Response) 这里没做太多改动,除了增加了密码哈希功能,我们在保存新用户之前把哈希后的内容作为新的密码。同样的,在认证部分,我们会校验密码的哈希值。 -现在我们能够安全的认证数据库里的用户信息,我们需要一个机制,能使用接口和分布式服务来做认证。实现这样的功能有许多方法,但是我发现,能通过服务和 web 使用的最简单的认证方法是用 [JWT](https://jwt.io/)。 +现在我们能够安全的认证数据库里的用户信息,我们需要一个机制,能使用接口和分布式服务来做认证。实现这样的功能有许多方法,但是我发现,能通过服务和 Web 使用的最简单的认证方法是用 [JWT](https://jwt.io/)。 -不过在我们继续下面的内容之前,请查看下我在每个服务中的 Dockfiles 和 Makefiles 做的修改。为了匹配最新的 git 仓库,我也修改了 imports 。 +不过在我们继续下面的内容之前,请查看下我在每个服务中的 Dockfiles 和 Makefiles 做的修改。为了匹配最新的 Git 仓库,我也修改了 imports 。 ## JWT -[JWT](https://jwt.io/) 是 JSON web tokens 的缩写,是一个分布式的安全协议。类似 OAuth。 概念很简单,用算法给用户生成一个唯一的哈希,这个哈希值可以用来比较和校验。不仅如此,token 自身也会包含用户的元数据 (metadata)。也就是说,用户数据本身也可以是 token 的一部分。 我们看一个 JWT 的实例: +[JWT](https://jwt.io/) 是 JSON Web tokens 的缩写,是一个分布式的安全协议。类似 OAuth。 概念很简单,用算法给用户生成一个唯一的哈希,这个哈希值可以用来比较和校验。不仅如此,token 自身也会包含用户的元数据 (metadata)。也就是说,用户数据本身也可以是 token 的一部分。 我们看一个 JWT 的实例: ``` eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ @@ -352,7 +352,7 @@ func AuthWrapper(fn server.HandlerFunc) server.HandlerFunc { } ``` -然后我们执行 consignment-cli工具,cd 到新的 shippy-consignment-cli 仓库,执行 `$make build` 来创建新的 docker 镜像, 现在运行: +然后我们执行 consignment-cli 工具,cd 到新的 shippy-consignment-cli 仓库,执行 `$make build` 来创建新的 docker 镜像, 现在运行: ``` $ make build @@ -362,7 +362,7 @@ $ docker run --net="host" \ ``` -注意下我们在运行 docker 容器的时候使用了 `--net="host"` 标记。用来告诉 Docker 在本地局域网来运行我们的容器,如127.0.0.1 或 localhost,而不是 Docker 的内网。注意,用这个方法不需要使用端口转发。因此只需要用 -p 8080 代替 -p 8080:8080 就可以了。 [更多关于 Docker 网络的内容请看这里](https://docs.docker.com/engine/userguide/networking/)。 +注意下我们在运行 docker 容器的时候使用了 `--net="host"` 标记。用来告诉 Docker 在本地局域网来运行我们的容器,如 127.0.0.1 或 localhost,而不是 Docker 的内网。注意,用这个方法不需要使用端口转发。因此只需要用 -p 8080 代替 -p 8080:8080 就可以了。 [更多关于 Docker 网络的内容请看这里](https://docs.docker.com/engine/userguide/networking/)。 到这一步,你就能看到新的 consignment 被创建了。试着从 token 里删除几个字母,token 就会变无效。会出现错误。 @@ -438,7 +438,7 @@ run: via: https://ewanvalentine.io/microservices-in-golang-part-4/ -作者:[André Carvalho](https://ewanvalentine.io/microservices-in-golang-part-4/) +作者:[Andr é Carvalho](https://ewanvalentine.io/microservices-in-golang-part-4/) 译者:[ArisAries](https://github.com/ArisAries) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-5-Event-brokering-with-Go-Micro.md b/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-5-Event-brokering-with-Go-Micro.md index 05f580801..26edc4347 100644 --- a/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-5-Event-brokering-with-Go-Micro.md +++ b/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-5-Event-brokering-with-Go-Micro.md @@ -183,11 +183,11 @@ pubsub := srv.Server().Options().Broker 就是这样!这是一个简单的例子,因为我们的电子邮件服务隐式地收听单个 `user.created` 事件,但希望你能看到这种方法如何让你编写解耦的服务。 -值得一提的是,使用 JSON over NATS 会比 gRPC 带来更高的性能开销,因为我们已经回到串行化json字符串的领域。但是,对于某些使用情况,这是完全可以接受的。 NATS 非常高效,非常适合消息最多交付一次的事件(fire and forget 有消息最多交付一次的意思,这个[链接](http://www.enterpriseintegrationpatterns.com/patterns/conversation/FireAndForget.html)可以帮助做更深入的理解)。 +值得一提的是,使用 JSON over NATS 会比 gRPC 带来更高的性能开销,因为我们已经回到串行化 json 字符串的领域。但是,对于某些使用情况,这是完全可以接受的。 NATS 非常高效,非常适合消息最多交付一次的事件(fire and forget 有消息最多交付一次的意思,这个[链接](http://www.enterpriseintegrationpatterns.com/patterns/conversation/FireAndForget.html)可以帮助做更深入的理解)。 Go-micro 还支持一些最广泛使用的队列 / pubsub 技术供你使用。[你可以在这里看到它们的列表](https://github.com/micro/go-plugins/tree/master/broker)。你不需要改变你的实现因为 go-micro 为你提供了抽象。你只需要将环境变量从 `MICRO_BROKER=nats` 更改为 `MICRO_BROKER=googlepubsub`,然后将 main.go 的导入从 `_ "github.com/micro/go-plugins/broker/nats"` 更改为 `_ "github.com/micro/go-plugins/broker/googlepubsub"`。 -如果你不使用 go-micro,那么有一个 [NATS go 库](https://github.com/nats-io/go-nats)(NATS 是用 go 写的,所以对 Go 的支持非常稳固)。 +如果你不使用 go-micro,那么有一个 [NATS Go 库](https://github.com/nats-io/go-nats)(NATS 是用 Go 写的,所以对 Go 的支持非常稳固)。 发布一个事件: @@ -278,7 +278,7 @@ func main() { via:[Microservices in Golang - Part 5 - Event brokering with Go Micro](https://ewanvalentine.io/microservices-in-golang-part-5/) -作者:[André Carvalho](https://ewanvalentine.io/) +作者:[Andr é Carvalho](https://ewanvalentine.io/) 译者:[shniu](https://github.com/shniu) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-6-Web-Clients.md b/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-6-Web-Clients.md index 436d05eee..1866d6170 100644 --- a/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-6-Web-Clients.md +++ b/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-6-Web-Clients.md @@ -2,9 +2,9 @@ # Golang 下的微服务 - 第 6 部分 - Web Clients -在之前的文章中,我们看了一些使用 go-micro 和 go 语言的生成的各种事件驱动的方法。 在本篇文章,我们将深入到客户端,探究一下如何创建一个能够与我们之前创建的平台交互的 Web 客户端。 +在之前的文章中,我们看了一些使用 go-micro 和 Go 语言的生成的各种事件驱动的方法。 在本篇文章,我们将深入到客户端,探究一下如何创建一个能够与我们之前创建的平台交互的 Web 客户端。 -这篇文章会介绍如何使用 [micro](https://github.com/micro/micro) 工具包生成 web 客户端从外部代理内部 rpc 方法。 +这篇文章会介绍如何使用 [micro](https://github.com/micro/micro) 工具包生成 Web 客户端从外部代理内部 rpc 方法。 我们会创建一个 user 接口用于生成平台的登录界面、还会创建一个接口用于使用我们的 consignments。该界面包含了创建用户、登录、和创建 consignments 等功能。 本系列的前几篇文章已经介绍过其中的部分代码了,在这篇文章我会带大家深入了解一下。 @@ -14,7 +14,7 @@ REST 已经在网络上服务了很多年了,并且迅速成为管理客户端和服务器之间资源的途径。REST 正在逐渐取代已经过时的 RPC 和 SOAP。曾经必须写一个 wsdl 文件的时代已经过去了。 -REST 向我们承诺了一种实用,简单和标准化的资源管理方法。 REST 使用 http 协议明确了正在执行的具体 web 动作类型。REST 鼓励我们使用 http 错误响应码来更精确地描述服务器的响应状态。而且大多数情况下,这种方法运行良好,并没有问题。但是像所有好东西一样,REST有许多不足和缺点,我不打算在这里详细介绍。大家有兴趣可以参考[这篇文章](https://medium.freecodecamp.org/rest-is-the-new-soap-97ff6c09896d)。 +REST 向我们承诺了一种实用,简单和标准化的资源管理方法。 REST 使用 http 协议明确了正在执行的具体 Web 动作类型。REST 鼓励我们使用 http 错误响应码来更精确地描述服务器的响应状态。而且大多数情况下,这种方法运行良好,并没有问题。但是像所有好东西一样,REST 有许多不足和缺点,我不打算在这里详细介绍。大家有兴趣可以参考[这篇文章](https://medium.freecodecamp.org/rest-is-the-new-soap-97ff6c09896d)。 但是!随着**微服务**的出现,RPC 正在卷土重来。 @@ -31,10 +31,10 @@ API 网关将允许我们将 rpc 调用代理为 Web 友好的 javascriptON rpc 首先要确保安装了 micro 工具包: ``` -$ go get -u github.com/micro/micro +$ Go get -u github.com/micro/micro ``` -Docker环境下使用 Micro 更好的方法还是建议大家使用Docker镜像: +Docker 环境下使用 Micro 更好的方法还是建议大家使用 Docker 镜像: ``` $ docker pull microhq/micro @@ -74,7 +74,7 @@ func main() { // 创建一个新的服务 srv := micro.NewService( - // 这个名字必须于你在protobuf definition定义的包名匹配 + // 这个名字必须于你在 protobuf definition 定义的包名匹配 micro.Name("shippy.auth"), ) @@ -242,12 +242,12 @@ func (srv *service) ValidateToken(ctx context.Context, req *pb.Token, res *pb.To ``` -现在运行 `$ make build && make run`。 然后转到 shippy-email-service 运行`$ make build && make run`。 一旦这两个服务都运行,运行: +现在运行 `$ make build && make run`。 然后转到 shippy-email-service 运行 `$ make build && make run`。 一旦这两个服务都运行,运行: ```shell $ docker run -p 8080:8080 \ -e MICRO_REGISTRY=mdns \ - microhq/micro api \ + microhq/micro API \ --handler=rpc \ --address=:8080 \ --namespace=shippy @@ -300,7 +300,7 @@ $ curl -XPOST -H 'Content-Type: application/javascripton' \ 现在可以使用我们的刚刚创建的新 rpc 节点创建一个用户界面。本文使用了 React,当然如果你喜欢的话可以使用其余的架构。请求都是一样的。本文使用来自 Facebook 的 react-create-app 库: -`$ npm install -g react-create-app` +`$ NPM install -g react-create-app` 安装完成后,执行 `$ react-create-app shippy-ui`。 这将为您创建一个 React 应用程序的框架。 @@ -631,11 +631,11 @@ export default CreateConsignment; ``` -**注意**:我还将 Twitter Bootstrap 添加到 /public/index.html 并更改了一些CSS。 +**注意**:我还将 Twitter Bootstrap 添加到 /public/index.html 并更改了一些 CSS。 -现在运行用户界面 `$ npm start`。 之后应该浏览器会自动打开一个界面。您现在应该可以注册并登录并查看 consignment 表单,您可以在其中创建新 consignments。看看你的开发工具中的 network 选项,然后看看 rpc 方法从我们的不同微服务中触发和获取我们的数据。 +现在运行用户界面 `$ NPM start`。 之后应该浏览器会自动打开一个界面。您现在应该可以注册并登录并查看 consignment 表单,您可以在其中创建新 consignments。看看你的开发工具中的 network 选项,然后看看 rpc 方法从我们的不同微服务中触发和获取我们的数据。 -第6部分到这里就结束了,如果您有任何反馈,请给我发一封[电子邮件](ewan.valentine89@gmail.com),我会尽快答复(有可能不会很及时,敬请见谅)。 +第 6 部分到这里就结束了,如果您有任何反馈,请给我发一封[电子邮件](ewan.valentine89@gmail.com),我会尽快答复(有可能不会很及时,敬请见谅)。 如果你发现这个系列有用,并且你使用了一个广告拦截器。 请考虑为我的时间和努力赞助几块钱。十分感谢!! [https://monzo.me/ewanvalentine](https://monzo.me/ewanvalentine) @@ -643,7 +643,7 @@ export default CreateConsignment; via: https://ewanvalentine.io/microservices-in-golang-part-6/ -作者:[André Carvalho](https://ewanvalentine.io/microservices-in-golang-part-6/) +作者:[Andr é Carvalho](https://ewanvalentine.io/microservices-in-golang-part-6/) 译者:[zhangyang9](https://github.com/zhangyang9) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-7-Terraform-a-Cloud.md b/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-7-Terraform-a-Cloud.md index c40aaad4d..8422552e6 100644 --- a/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-7-Terraform-a-Cloud.md +++ b/published/tech/Microservices-in-Golang/Microservices-in-Golang-Part-7-Terraform-a-Cloud.md @@ -2,7 +2,7 @@ # Golang 下的微服务 - 第 7 部分 - Terraform a Cloud -在之前的文章中,我们简要介绍了用户界面和 Web 客户端以及如何使用微工具包rpc代理与我们新创建的rpc服务进行交互。 +在之前的文章中,我们简要介绍了用户界面和 Web 客户端以及如何使用微工具包 rpc 代理与我们新创建的 rpc 服务进行交互。 本文将讨论如何创建云环境来托管我们的服务。 我们将使用 Terraform 在 Google Cloud 平台上构建我们的云群集。这应该是一篇相当短的文章,但它也很重要。 @@ -10,7 +10,7 @@ 我已经使用了几种不同的云供应解决方案,但对我而言,Hashicorps Terraform 感觉最容易使用并且得到最好的支持。近年来出现了一个术语:'基础设施作为代码'。为什么你想要你的基础设施作为代码?那么,基础设施很复杂,它描述了很多移动部件。跟踪基础架构的变更和版本控制变更也很重要。 -Terraform 完美地做到了这一点。他们实际上已经创建了自己的DSL(域特定语言),它允许您描述您的基础设施应该是什么样子。 +Terraform 完美地做到了这一点。他们实际上已经创建了自己的 DSL(域特定语言),它允许您描述您的基础设施应该是什么样子。 Terraform 还允许您进行原子更改。所以在出现失败的情况下,它会将所有东西都退回来,并将其恢复到原来的状态。 Terraform 甚至允许您通过执行转出计划来预览更改。这将准确描述您的更改将对您的基础架构做什么。这给了你很多的信心,曾经是一个可怕的前景。 @@ -18,13 +18,13 @@ Terraform 还允许您进行原子更改。所以在出现失败的情况下, ## 创建您的云计划 -转到 Google Cloud 并创建一个新项目。 如果您之前没有使用过它,您可能会发现您有300英镑的免费试用版。太好了! 无论如何,你应该看到一个空白的新项目。 现在在你的左边,你应该看到一个IAM&Admin tabb,然后在那里创建一个新的服务密钥。 确保选择“提供新密钥”,然后确保您选择了json类型。 安全保管,我们稍后需要。 这允许程序代表您执行对 Google Cloud API 的更改。 现在我们应该掌握一切我们需要开始的事情。 +转到 Google Cloud 并创建一个新项目。 如果您之前没有使用过它,您可能会发现您有 300 英镑的免费试用版。太好了! 无论如何,你应该看到一个空白的新项目。 现在在你的左边,你应该看到一个 IAM&Admin tabb,然后在那里创建一个新的服务密钥。 确保选择“提供新密钥”,然后确保您选择了 json 类型。 安全保管,我们稍后需要。 这允许程序代表您执行对 Google Cloud API 的更改。 现在我们应该掌握一切我们需要开始的事情。 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-micro/Screen-Shot-2018-02-10-at-10.58.07.png) 所以创建一个新的回购。 我称之为我的 shippy-infrastructure。 ## 描述我们的基础设施 -创建一个名为 variables.tf 的新文件。 这将包含关于我们项目的基本信息。 在这里,我们有我们的项目 ID,我们的地区,我们的项目名称和我们的平台名称。 该地区是我们希望集群运行的数据中心。 该区域是该区域内的可用区域。 项目名称是我们Google项目的项目ID,最后,平台名称是我们的群集的名称。 +创建一个名为 variables.tf 的新文件。 这将包含关于我们项目的基本信息。 在这里,我们有我们的项目 ID,我们的地区,我们的项目名称和我们的平台名称。 该地区是我们希望集群运行的数据中心。 该区域是该区域内的可用区域。 项目名称是我们 Google 项目的项目 ID,最后,平台名称是我们的群集的名称。 ```c variable "gcloud-region" { default = "europe-west2" } @@ -136,7 +136,7 @@ resource "google_dns_record_set" "dev-k8s-endpoint-shippy-freight" { 运行 `$ terraform init` - 这将下载任何缺少的提供 providers/plugins。 这会发现我们正在使用 Google Cloud 模块并自动获取这些依赖关系。 -现在,如果您运行 `$ terraform` 计划,它会向您显示它将做出的更改。 这几乎就像在你的整个基础设施上做 `$ git diff`。 现在很酷! +现在,如果您运行 `$ terraform` 计划,它会向您显示它将做出的更改。 这几乎就像在你的整个基础设施上做 `$ Git diff`。 现在很酷! 浏览部署计划后,我认为很好。 @@ -144,13 +144,13 @@ resource "google_dns_record_set" "dev-k8s-endpoint-shippy-freight" { 注意:您可能会被要求启用一些 API 以完成此操作,没关系,单击链接,确保启用它们并重新运行 `$ terraform apply`。 如果您想节省时间,请转到 Google 云端控制台中的 API 部分,并启用 DNS,Kubernetes 和计算引擎 API。 -这可能需要一段时间,但那是因为发生了很多事情。 但是一旦完成,如果您转到Google云端控制台右侧菜单中的 Kubernetes 引擎或计算引擎细分,则应该能够看到您的新群集。 +这可能需要一段时间,但那是因为发生了很多事情。 但是一旦完成,如果您转到 Google 云端控制台右侧菜单中的 Kubernetes 引擎或计算引擎细分,则应该能够看到您的新群集。 注意:如果你没有使用免费试用期,这将立即开始花费你的钱。 请务必查看实例定价列表。 哦,而且我在开展这些职位的工作之间已经开动了我的力量。 这不会产生费用,因为这些费用是根据资源使用情况收取的。 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-micro/Screen-Shot-2018-02-10-at-12.25.11-1.png) -而已! 我们有一个完全正常运行的cluster / vm,准备好让我们开始部署我们的容器。 本系列的下一部分将介绍Kubernetes,以及如何设置和部署容器到Kubernetes。 +而已! 我们有一个完全正常运行的 cluster / vm,准备好让我们开始部署我们的容器。 本系列的下一部分将介绍 Kubernetes,以及如何设置和部署容器到 Kubernetes。 如果你发现这个系列有用,并且你使用了一个广告拦截器(谁可以责怪你)。 请考虑为我的时间和努力捐赠几块钱。 干杯! https://monzo.me/ewanvalentine diff --git a/published/tech/Microservices-in-Golang/Microservices-in-Golang-part-2-Docker-and-go-micro.md b/published/tech/Microservices-in-Golang/Microservices-in-Golang-part-2-Docker-and-go-micro.md index 9715c0fef..797545f77 100644 --- a/published/tech/Microservices-in-Golang/Microservices-in-Golang-part-2-Docker-and-go-micro.md +++ b/published/tech/Microservices-in-Golang/Microservices-in-Golang-part-2-Docker-and-go-micro.md @@ -6,7 +6,7 @@ **[在上篇文章中](https://studygolang.com/articles/12060)**,我们大致介绍了如何编写一个基于 `gRPC` 的微服务。在这个部分,我们将涵盖 `Docker` 服务的基础知识,我们也将使用 [go-micro](https://github.com/micro/go-micro) 更新我们的服务,并在文本末尾引入第二个服务。 -## Docker简介 +## Docker 简介 随着云计算的到来和微服务的诞生,服务在部署的时候有更多的压力,但是一次一小段代码就产生了一些有趣的新思想和新技术,其中之一就是[容器](https://en.wikipedia.org/wiki/Operating-system-level_virtualization)的概念。 @@ -58,11 +58,11 @@ CMD ["./consignment-service"] ``` build: ... - GOOS=linux GOARCH=amd64 go build + GOOS=linux GOARCH=amd64 Go build docker build -t consignment-service . ``` -我们在这里增加了两个步骤,我想详细解释一下。首先,我们正在构建我们的二进制文件。你会注意到在运行命令 `$ go build` 之前,设置了两个环境变量。GOOS 和 GOARCH 允许您为另一个操作系统交叉编译您的二进制文件,由于我在 Macbook上开发,所以无法编译出二进制文件,让它在 Docker 容器中运行它,而该容器使用的是 Linux。这个二进制在你的 Docker 容器中将是完全没有意义的,它会抛出一个错误。第二步是添加 Docker 构建过程。这将读取你的 Dockerfile 文件,并通过一个名称 `consignment-service` 构建镜像。句号表示一个目录路径,在这里我们只是希望构建过程在当前目录中查找。 +我们在这里增加了两个步骤,我想详细解释一下。首先,我们正在构建我们的二进制文件。你会注意到在运行命令 `$ Go build` 之前,设置了两个环境变量。GOOS 和 GOARCH 允许您为另一个操作系统交叉编译您的二进制文件,由于我在 Macbook 上开发,所以无法编译出二进制文件,让它在 Docker 容器中运行它,而该容器使用的是 Linux。这个二进制在你的 Docker 容器中将是完全没有意义的,它会抛出一个错误。第二步是添加 Docker 构建过程。这将读取你的 Dockerfile 文件,并通过一个名称 `consignment-service` 构建镜像。句号表示一个目录路径,在这里我们只是希望构建过程在当前目录中查找。 我将在我们的 Makefile 中添加一个新条目: @@ -75,7 +75,7 @@ run: [您可以阅读更多关于 Docker 网络如何工作的信息。](https://docs.docker.com/engine/userguide/networking/) -当您运行 `$ docker build` 时,您正在将代码和运行时环境构建到镜像中。Docker 镜像是您的环境及其依赖关系的可移植快照。你可以将它分享到 Docker Hub 来共享你的 Docker 镜像。Docker 镜像就像一个 npm 或 yum repo。当你在你的 Dockerfile 里面定义了 `FROM`,你就告诉了 docker 从 docker hub 下载哪个镜像来作为运行环境。然后,您可以扩展并覆盖该基本文件的某些部分,方法是自行重新定义它们。我们不会公开我们的 Docker 镜像,但是可以随时仔细阅读 Docker hub,并且注意到有多少功能被容器化。一些非常[显著的事情](https://www.youtube.com/watch?v=GsLZz8cZCzc)已经被 Docker 化了。 +当您运行 `$ docker build` 时,您正在将代码和运行时环境构建到镜像中。Docker 镜像是您的环境及其依赖关系的可移植快照。你可以将它分享到 Docker Hub 来共享你的 Docker 镜像。Docker 镜像就像一个 NPM 或 yum repo。当你在你的 Dockerfile 里面定义了 `FROM`,你就告诉了 docker 从 docker hub 下载哪个镜像来作为运行环境。然后,您可以扩展并覆盖该基本文件的某些部分,方法是自行重新定义它们。我们不会公开我们的 Docker 镜像,但是可以随时仔细阅读 Docker hub,并且注意到有多少功能被容器化。一些非常[显著的事情](https://www.youtube.com/watch?v=GsLZz8cZCzc)已经被 Docker 化了。 Dockerfile 中的每个声明在第一次构建时都被缓存。这样可以节省每次更改时重新构建整个运行时的时间。 Docker 非常聪明,可以确定哪些部分发生了变化,哪些部分需要重新构建。这使得构建过程非常快速。 @@ -206,7 +206,7 @@ func main() { 这里的主要变化是我们实例化我们的 gRPC 服务器的方式,它处理注册我们的服务,已经被整齐地抽象到一个 `mico.NewService()` 方法中。最后,处理连接本身的 `service.Run()` 函数。和以前一样,我们注册了我们的实现,但这次使用了一个稍微不同的方法。第二个最大的变化是服务方法本身,参数和响应类型略有变化,把请求和响应结构作为参数,现在只返回一个错误。在我们的方法中,设置了由 `go-micro` 处理响应。 -最后,我们不再对端口进行硬编码。go-micro 应该使用环境变量或命令行参数进行配置。设置地址, 使用 `MICRO_SERVER_ADDRESS=:50051`。我们还需要告诉我们的服务使用 [mdns](https://en.wikipedia.org/wiki/Multicast_DNS)(多播DNS)作为我们本地使用的服务代理。 +最后,我们不再对端口进行硬编码。go-micro 应该使用环境变量或命令行参数进行配置。设置地址, 使用 `MICRO_SERVER_ADDRESS=:50051`。我们还需要告诉我们的服务使用 [mdns](https://en.wikipedia.org/wiki/Multicast_DNS)(多播 DNS)作为我们本地使用的服务代理。 您通常不会在生产环境中使用 [mdns](https://en.wikipedia.org/wiki/Multicast_DNS) 进行服务发现,但我们希望避免在本地运行诸如 Consul 或 etcd 这样的测试。更多我们将在后面介绍。 让我们更新我们的 Makefile 来实现这一点。 @@ -248,7 +248,7 @@ func main() { ``` build: - GOOS=linux GOARCH=amd64 go build + GOOS=linux GOARCH=amd64 Go build docker build -t consignment-cli . run: @@ -271,7 +271,7 @@ ADD consignment-cli /app/consignment-cli CMD ["./consignment-cli"] ``` -它除了引入了我们的 json 数据文件,其余与之前服务的 Dockerfile 非常相似。如果你在你的 `consignment-cli` 目录,运行 `$ make run` 命令,你应该和以前一样,看见 `Created: true`。 +它除了引入了我们的 JSON 数据文件,其余与之前服务的 Dockerfile 非常相似。如果你在你的 `consignment-cli` 目录,运行 `$ make run` 命令,你应该和以前一样,看见 `Created: true`。 之前,我提到那些使用 Linux 的人应该切换到使用 Debian 作为基础映像。现在看起来是一个很好的时机来看看 Docker 的一个新功能:多阶段构建。这使我们可以在一个 Dockerfile 中使用多个 Docker 镜像。 @@ -280,7 +280,7 @@ CMD ["./consignment-cli"] ``` # consignment-service/Dockerfile -# We use the official golang image, which contains all the +# We use the official Golang image, which contains all the # correct build tools and libraries. Notice `as builder`, # this gives this container a name that we can reference later on. FROM golang:1.9.0 as builder @@ -292,9 +292,9 @@ WORKDIR /go/src/github.com/EwanValentine/shippy/consignment-service COPY . . # Here we're pulling in godep, which is a dependency manager tool, -# we're going to use dep instead of go get, to get around a few -# quirks in how go get works with sub-packages. -RUN go get -u github.com/golang/dep/cmd/dep +# we're going to use dep instead of Go get, to get around a few +# quirks in how Go get works with sub-packages. +RUN Go get -u github.com/golang/dep/cmd/dep # Create a dep project, and run `ensure`, which will pull in all # of the dependencies within this directory. @@ -302,7 +302,7 @@ RUN dep init && dep ensure # Build the binary, with a few flags which will allow # us to run this binary in Alpine. -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo . +RUN CGO_ENABLED=0 GOOS=linux Go build -a -installsuffix cgo . # Here we're using a second FROM statement, which is strange, # but this tells Docker to start a new build process with this @@ -330,11 +330,11 @@ CMD ["./consignment-service"] 这种方法的唯一问题,我想回来并在某些时候改善这一点,是 Docker 不能从父目录中读取文件。它只能读取 Dockerfile 所在目录或子目录中的文件。 -这意味着为了运行 `$ dep ensure` 或 `$ go get`,你需要确保你的代码被推到 Git 上,这样它就可以提取 vessel-service。就像其他 Go 包一样。这种方法不理想,但足够满足我们现在的需求。 +这意味着为了运行 `$ dep ensure` 或 `$ Go get`,你需要确保你的代码被推到 Git 上,这样它就可以提取 vessel-service。就像其他 Go 包一样。这种方法不理想,但足够满足我们现在的需求。 现在我将会在其他 Docker 文件中应用这种新方法。 -噢,记住要记得从 Makefiles 中删除 `$ go build`。 +噢,记住要记得从 Makefiles 中删除 `$ Go build`。 [更多的多阶段编译在这里](https://docs.docker.com/engine/userguide/eng-image/multistage-build/#name-your-build-stages) @@ -403,9 +403,9 @@ WORKDIR /go/src/github.com/EwanValentine/shippy/vessel-service COPY . . -RUN go get -u github.com/golang/dep/cmd/dep +RUN Go get -u github.com/golang/dep/cmd/dep RUN dep init && dep ensure -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo . +RUN CGO_ENABLED=0 GOOS=linux Go build -a -installsuffix cgo . FROM alpine:latest diff --git a/published/tech/Microservices-in-Golang/part-8.md b/published/tech/Microservices-in-Golang/part-8.md index 0ef5784d3..50ed52395 100644 --- a/published/tech/Microservices-in-Golang/part-8.md +++ b/published/tech/Microservices-in-Golang/part-8.md @@ -23,13 +23,13 @@ $ gcloud components install kubectl 现在确保你连接到集群,并且认证正确。第一步,我们登录进去,确保已被认证。第二步我们设置下项目配置,确保我们使用正确的项目 ID 和可访问区域。 ``` -$ echo "This command will open a web browser, and will ask you to login +$ Echo "This command will open a Web browser, and will ask you to login $ gcloud auth application-default login $ gcloud config set project shippy-freight $ gcloud config set compute/zone eu-west2-a -$ echo "Now generate a security token and access to your KB cluster" +$ Echo "Now generate a security token and access to your KB cluster" $ gcloud container clusters get-credentials shippy-freight-cluster ``` @@ -40,13 +40,13 @@ $ gcloud container clusters get-credentials shippy-freight-cluster ``` -$ echo "This command will open a web browser, and will ask you to login +$ Echo "This command will open a Web browser, and will ask you to login $ gcloud auth application-default login $ gcloud config set project $ gcloud config set compute/zone -$ echo "Now generate a security token and access to your KB cluster" +$ Echo "Now generate a security token and access to your KB cluster" $ gcloud container clusters get-credentials ``` @@ -67,7 +67,7 @@ $ kubectl get pods ``` -你会看到 ... `No resources found.`。没关系,我们还没有部署任何内容。我们可以想想我们需要实际部署些什么。我们需要一个 Mongodb 实例。一般来说,我们会部署一个 mongodb 实例,或者为了完全分离,将数据库实例和每个服务放一起。但是这个例子里,我们耍点小聪明,就用一个中心化的实例。这是个单一故障点,但是在实际应用场景中,你要考虑下将数据库实例分开部署,和服务保持一致。不过我们这种方法也可以。 +你会看到 ... `No resources found.`。没关系,我们还没有部署任何内容。我们可以想想我们需要实际部署些什么。我们需要一个 Mongodb 实例。一般来说,我们会部署一个 MongoDB 实例,或者为了完全分离,将数据库实例和每个服务放一起。但是这个例子里,我们耍点小聪明,就用一个中心化的实例。这是个单一故障点,但是在实际应用场景中,你要考虑下将数据库实例分开部署,和服务保持一致。不过我们这种方法也可以。 然后我需要部署服务了,vessel 服务,user 服务,consignment 服务和 email 服务。好了,很简单! @@ -189,7 +189,7 @@ stateful set 和 deployment 有相似的地方,除了它会用一些存储方 实际情况里,Mongodb 将数据写入二进制数据存储格式,很多数据库都是这样做的。创建一个可回收的数据库实例,比如 docker 容器。如果容器重启数据会丢失。一般来说你需要在容器启动的时候使用分卷装载数据/文件。 -你可以在 Kubernetes 上做这些部署。但是 StatefulSets,在相关的集群点有一些额外的自动化操作。因此这个对 mongodb 容器天然的合适。 +你可以在 Kubernetes 上做这些部署。但是 StatefulSets,在相关的集群点有一些额外的自动化操作。因此这个对 MongoDB 容器天然的合适。 ## Service @@ -201,11 +201,11 @@ stateful set 和 deployment 有相似的地方,除了它会用一些存储方 load balancer,是一个轮询的负载均衡器,可以给你选的 node 节点创建一个 IP 地址给代理。通过代理把服务暴露给外部。 -node port 将 pod 暴露给上层的网络环境,这样他们可以被其他服务, 内部的pod/实例访问。这样对暴露 node 给其他的 pod 来说是有用的。这就是你能用来允许服务和其他服务通信的方式。这就是服务发现的本质。至少是一部分。 +node port 将 pod 暴露给上层的网络环境,这样他们可以被其他服务, 内部的 pod/实例访问。这样对暴露 node 给其他的 pod 来说是有用的。这就是你能用来允许服务和其他服务通信的方式。这就是服务发现的本质。至少是一部分。 现在我们刚看了一点点 Kubernetes 的内容,我们来再多谈些,再挖掘挖掘。值得注意的是,如果你是个在本机上使用 docker ,比如如果你用的是 mac/windows 上的 docker 的 edge 版本,你可以把 Kubernetes 集群钉在本机上。测试时候很有用。 -那么我们已经创建了三个文件,一个用于存储,一个用于 stateful set,一个用于我们的服务。最后结果是有 mongodb 容器的副本,stateful 存储和通过 pod 保留给数据存储的服务。我们继续看看,创建,按正确的顺序,因为有些操作是需要依赖前面创建的内容。 +那么我们已经创建了三个文件,一个用于存储,一个用于 stateful set,一个用于我们的服务。最后结果是有 MongoDB 容器的副本,stateful 存储和通过 pod 保留给数据存储的服务。我们继续看看,创建,按正确的顺序,因为有些操作是需要依赖前面创建的内容。 ``` echo "shippy-infrastructure" @@ -214,7 +214,7 @@ $ kubectl create -f ./deployments/mongodb-deployment.yml $ kubectl create -f ./deployments/mongodb-service.yml ``` -等几分钟,你可以查下 mongodb 容器的状态,运行: +等几分钟,你可以查下 MongoDB 容器的状态,运行: ``` $ kubectl get pods @@ -538,7 +538,7 @@ $ curl localhost/rpc -XPOST -d '{ ``` -你会看到一个返回 `created: true`。超简洁!这就是你的 gRPC 服务,被代理并且转成了 web 友好的格式,使用了分片的 mongodb 实例。没费多大劲! +你会看到一个返回 `created: true`。超简洁!这就是你的 gRPC 服务,被代理并且转成了 Web 友好的格式,使用了分片的 MongoDB 实例。没费多大劲! ## 部署 UI diff --git a/published/tech/Microservices-in-Golang/part-9.md b/published/tech/Microservices-in-Golang/part-9.md index 60b67fc72..6fddd13ec 100644 --- a/published/tech/Microservices-in-Golang/part-9.md +++ b/published/tech/Microservices-in-Golang/part-9.md @@ -8,7 +8,7 @@ [CircleCI](http://circleci.com/) 是一款不可思议的工具,它有一个非常实用的免费平台。这个平台就是 SaaS, 因此与 Jenkins 不同的是,它是被完全管理的。同时它的配置和建立非常直截了当。此外,[CircleCI](http://circleci.com/) 也使用 Docker 镜像(images),所以你可以在如何管理你的构建上有很多创意。 -确保你已经注册并且创造了账户。首先让我们在 [CircleCI](http://circleci.com/) 中新建一个工程。在左侧菜单中,点击 “add project”。如果你已经将你的 github 账户连接到你的 [CircleCI](http://circleci.com/) 账户,你应该可以看到你的微服务 git 仓库出现在列表中。点击 “follow project”。你将看到一个请求页面,你可以选择你乐于使用的操作系统和语言。确保 Linux 和 Go 被选中。然后点击开始构建。 +确保你已经注册并且创造了账户。首先让我们在 [CircleCI](http://circleci.com/) 中新建一个工程。在左侧菜单中,点击 “add project”。如果你已经将你的 GitHub 账户连接到你的 [CircleCI](http://circleci.com/) 账户,你应该可以看到你的微服务 Git 仓库出现在列表中。点击 “follow project”。你将看到一个请求页面,你可以选择你乐于使用的操作系统和语言。确保 Linux 和 Go 被选中。然后点击开始构建。 这将创造一些默认的配置,但是我们需要在构建能开始正常工作之前,增加我们自己的配置到此代码仓库中。 @@ -57,7 +57,7 @@ jobs: # Then sets the gcloud project name from the environment variables we set above. # Then we set the cluster name, the compute region/zone, then fetch the credentials. command: | - echo $GCLOUD_SERVICE_KEY | base64 --decode -i > ${HOME}/gcloud-service-key.json && \ + Echo $GCLOUD_SERVICE_KEY | base64 --decode -i > ${HOME}/gcloud-service-key.json && \ gcloud auth activate-service-account --key-file ${HOME}/gcloud-service-key.json && \ gcloud config set project $GCLOUD_PROJECT_NAME && \ gcloud --quiet config set container/cluster $GCLOUD_CLUSTER_NAME && \ @@ -73,13 +73,13 @@ jobs: 我们需要谷歌云服务钥匙,正如我们在[第 7 章](https://studygolang.com/articles/12799)创建的那个,然后我们需要将此钥匙加密成 base64 并作为我们构建工程设置中的一个环境变量来存储。 -因此找到你的谷歌云服务钥匙,然后运行 `$ cat .json | base64`,并复制得到的字符串。回到 [CircleCI](http://circleci.com/) 你的项目来,点击右上方的齿轮,然后选择左边栏目中的环境变量。新建一个环境变量,命名为`GCLOUD_SERVICE_KEY`,然后粘贴前面得到的 base64 字符串作为其值,并保存。 +因此找到你的谷歌云服务钥匙,然后运行 `$ cat .json | base64`,并复制得到的字符串。回到 [CircleCI](http://circleci.com/) 你的项目来,点击右上方的齿轮,然后选择左边栏目中的环境变量。新建一个环境变量,命名为 `GCLOUD_SERVICE_KEY`,然后粘贴前面得到的 base64 字符串作为其值,并保存。 上述操作可以在 circleci 内保存任何安全信息,且使代码仓库不熟任何敏感数据影响。它将这些访问密钥保存在操作团队的控制之下,而不仅限于任何可以访问代码仓库的人员。 现在我们的构建配置中,用来对我们组进行身份验证的变量,其内容被解码为一个文件。 -大功告成,相当简单。我们拥有 CI 作为我们的一个服务。作为一个产品服务,在你执行部署步骤之前,你可能会首先运行你的测试用例。查看[这篇文档](https://circleci.com/docs/2.0/)然后看看你能用 circle 做哪些有趣的东西。由于 circle 使用Docker 容器,你甚至可以加一个数据库容器,以便于你运行集成测试。发挥你的创造力,最大限度使用这些特性。 +大功告成,相当简单。我们拥有 CI 作为我们的一个服务。作为一个产品服务,在你执行部署步骤之前,你可能会首先运行你的测试用例。查看[这篇文档](https://circleci.com/docs/2.0/)然后看看你能用 circle 做哪些有趣的东西。由于 circle 使用 Docker 容器,你甚至可以加一个数据库容器,以便于你运行集成测试。发挥你的创造力,最大限度使用这些特性。 如果你觉得这一系列的文章对你有用,且你装了广告拦截器(没有人会责怪你),考虑打赏一下我撰写这些文章所付出的时间和努力吧!谢谢! https://monzo.me/ewanvalentine diff --git a/published/tech/Object-Orientation-In-Go.md b/published/tech/Object-Orientation-In-Go.md index 26ea336cc..481495b09 100644 --- a/published/tech/Object-Orientation-In-Go.md +++ b/published/tech/Object-Orientation-In-Go.md @@ -6,7 +6,7 @@ > 什么是面向对象呢? -这里我试着分享我对 go 语言如何实现面向对象的理解,以及让它看起来比其他传统的面向对象语言更加面向对象。 +这里我试着分享我对 Go 语言如何实现面向对象的理解,以及让它看起来比其他传统的面向对象语言更加面向对象。 在之前的一篇文章中,我探讨了用函数来表达一切的想法,而实际上只是用一种安全的方式来表示一组函数,它总是与相同的闭包一起运行,并且可以在相同的状态下运行。 @@ -77,7 +77,7 @@ func main() { 这就是 Go interfaces(接口)的由来。它提供了一个编译时安全的方式来表现协议,通过适当的函数来消除初始化结构的所有模板。它会为我们初始化结构,甚至对初始化结构优化一次,这在 Go 中被称为 iface 。类似于 C++ 的 vtable 。 -它还允许代码更加的解耦,因为你不需要了解定义这个接口 (interface)并且实现这个接口的包。与相同的编译安全型语言Java、C++比较,在 Go 允许的基础上,Go更加的灵活。 +它还允许代码更加的解耦,因为你不需要了解定义这个接口 (interface)并且实现这个接口的包。与相同的编译安全型语言 Java、C++ 比较,在 Go 允许的基础上,Go 更加的灵活。 让我们重新审视之前的带有接口(interfaces)的文件协议: @@ -123,7 +123,7 @@ func main(){ } ``` -一个关键的不同是, 这段代码使用了接口(interface),现在是安全的。那个`useFileProtocol`不必担心调用函数是否为nil,go编译器将会创建一个结构体,通过一个 `iface`描述符来保持一个指针,该指针具有满足协议的所有功能。它会按类型<->接口的每一个匹配项执行此操作,就像它被使用的那样(它第一次初始化使用的那样)。 +一个关键的不同是, 这段代码使用了接口(interface),现在是安全的。那个 `useFileProtocol` 不必担心调用函数是否为 nil,go 编译器将会创建一个结构体,通过一个 `iface` 描述符来保持一个指针,该指针具有满足协议的所有功能。它会按类型 <-> 接口的每一个匹配项执行此操作,就像它被使用的那样(它第一次初始化使用的那样)。 如果你这样做,仍然会造成一个段错误,如下: @@ -140,7 +140,7 @@ useFileProtocol(func() (ReadCloser, error) { ## 代码扩展超出其最初的目的 -当我尝试为 nash 上的内置函数 exit 编写测试代码时,第一次了解到 go 的 interfaces 是多么的强大。主要的问题是,似乎我们必须为每个平台实现不同的测试,因为在某些平台上,退出状态代码的处理方式不同。我现在不记得所有的细节,但在 plan9 上,退出状态是一个 string 类型,而不是一个 integer 类型。 +当我尝试为 nash 上的内置函数 exit 编写测试代码时,第一次了解到 Go 的 interfaces 是多么的强大。主要的问题是,似乎我们必须为每个平台实现不同的测试,因为在某些平台上,退出状态代码的处理方式不同。我现在不记得所有的细节,但在 plan9 上,退出状态是一个 string 类型,而不是一个 integer 类型。 基本上在一个错误上,我想要的是状态代码,而不仅仅是错误,就像在 Cmd.run 上提供的。(文件 Cmd.run : https://golang.org/pkg/os/exec/#Cmd.Run) @@ -203,7 +203,7 @@ syscall % grep -R ExitStatus . ./syscall_plan9.go:func (w Waitmsg) ExitStatus() int { ``` -看起来像公共协议被足够多的平台所实现,这对我来说至少是足够的(windows + linux + plan9 是足够的。)。现在我们有一个共同的协议所有的平台我们可以这样做: +看起来像公共协议被足够多的平台所实现,这对我来说至少是足够的(windows + Linux + plan9 是足够的。)。现在我们有一个共同的协议所有的平台我们可以这样做: ```go // exitResult is a common interface implemented by @@ -228,7 +228,7 @@ if exiterr, ok := err.(*exec.ExitError); ok { Sys() 方法返回了一个抽象的、更加精确的接口,这将是更容易的想出一个新的接口,这将是一个接口的子集,并且能保证编译时的安全,而不是通过检查运行时得到的运行时安全。 -但即使是一个简单的方法来定义一个新的 interface 并且不改变源代码的情况下执行一个安全的运行时检查,这样实现 interface 是很简洁的。在 Java 或 c++ 语言中,我不能想出一个解决方案,包含相同数量的代码/复杂性,特别因为基于多态性的层次结构的脆性的。检查只被允许这种情况,如果原始代码知道你正在检查的接口,并且明显的是继承自它。为了解决我的问题我不得不改变 go 的核心代码,去了解我的 interface ,在 Go 的 interfaces 中,这个不是必需的( yay 层次结构)。 +但即使是一个简单的方法来定义一个新的 interface 并且不改变源代码的情况下执行一个安全的运行时检查,这样实现 interface 是很简洁的。在 Java 或 c++ 语言中,我不能想出一个解决方案,包含相同数量的代码/复杂性,特别因为基于多态性的层次结构的脆性的。检查只被允许这种情况,如果原始代码知道你正在检查的接口,并且明显的是继承自它。为了解决我的问题我不得不改变 Go 的核心代码,去了解我的 interface ,在 Go 的 interfaces 中,这个不是必需的( yay 层次结构)。 这是非常重要的, 因为它允许开发人员提出简单的对象, 因为它们不必预测对象将来可能使用的每一种方式, 就像接下来可能会有哪些接口有用一样。 @@ -250,7 +250,7 @@ Sys() 方法返回了一个抽象的、更加精确的接口,这将是更容 思考对象的重要部分是封装(不是类型)。他给出了一个很好的例子,那就是细胞,他们有明确的细胞膜,他们允许出去,也允许进入。 -彼此交互的每一个细胞都对彼此内部运作一无所知,他们不需要知道其他细胞类型,他们只需要实现相同的协议,交易相同的蛋白质等(我不擅长生物学=))。重点是化学反应(过程)而不是细胞类型。 +彼此交互的每一个细胞都对彼此内部运作一无所知,他们不需要知道其他细胞类型,他们只需要实现相同的协议,交易相同的蛋白质等(我不擅长生物学 =))。重点是化学反应(过程)而不是细胞类型。 所以我们最终封装和明确的协议作为面向对象应该是什么,如何开发系统和一个伟大的隐喻,模仿生物机制,而是因为有机生命尺度数量级更好。 @@ -271,9 +271,9 @@ think that you know what you are doing ## 结论 -上面我给的例子已经显示了一眼在 Go 中如何做更多,而无需改变任何预先存在的代码使用 协议(Go的interfaces) 的概念代替类。似乎更容易开发根据开闭原则(开闭原则:open closed principle,https://en.wikipedia.org/wiki/Open/closed_principle),因为我可以轻松地扩展其他代码去做事情,这不是最初打算不用改变它。 +上面我给的例子已经显示了一眼在 Go 中如何做更多,而无需改变任何预先存在的代码使用 协议(Go 的 interfaces) 的概念代替类。似乎更容易开发根据开闭原则(开闭原则:open closed principle,https://en.wikipedia.org/wiki/Open/closed_principle),因为我可以轻松地扩展其他代码去做事情,这不是最初打算不用改变它。 -Go 和 Java 都有 interfaces 的概念,这是似乎具有误导性的,因为他们唯一的共同点是他们的名字。在 Java 中接口的创建是一个关系,在 Go 中并不是。它只是定义了一个协议,可用于直接集成的对象不知道对方(他们甚至不需要知道明确的接口)。这似乎更加面向对象,比任何到目前为止我知道的(当然,我并不知道太多=))。 +Go 和 Java 都有 interfaces 的概念,这是似乎具有误导性的,因为他们唯一的共同点是他们的名字。在 Java 中接口的创建是一个关系,在 Go 中并不是。它只是定义了一个协议,可用于直接集成的对象不知道对方(他们甚至不需要知道明确的接口)。这似乎更加面向对象,比任何到目前为止我知道的(当然,我并不知道太多 =))。 ## 鸣谢 diff --git a/published/tech/Optimized-abs-for-int64-in-Go.md b/published/tech/Optimized-abs-for-int64-in-Go.md index 2ca35a8f2..fdaa7c06a 100644 --- a/published/tech/Optimized-abs-for-int64-in-Go.md +++ b/published/tech/Optimized-abs-for-int64-in-Go.md @@ -40,7 +40,7 @@ func WithStdLib(n int64) int64 { 上边的代码中,将 n 先从 `int64` 转成 `float64`,通过 `math.Abs` 取到绝对值后再转回 `int64`,多次转换显然会造成性能开销。可以写一个基准测试来验证一下: ```console -$ go test -bench=. +$ Go test -bench=. goos: darwin goarch: amd64 pkg: github.com/cavaliercoder/abs @@ -52,7 +52,7 @@ ok github.com/cavaliercoder/abs 2.320s 测试结果:0.3 ns/op, `WithBranch` 要快两倍多,它还有一个优势:在将 int64 的大数转化为 IEEE-754 标准的 float64 不会发生截断(丢失超出精度的值) -举个例子:`abs.WithBranch(-9223372036854775807)` 会正确返回 9223372036854775807。但 `WithStdLib(-9223372036854775807)` 则在类型转换区间发生了溢出,返回 -9223372036854775808,在大的正数输入时, `WithStdLib(9223372036854775807)` 也会返回不正确的负数结果。 +举个例子:`abs.WithBranch(-9223372036854775807)` 会正确返回 9223372036854775807。但 `WithStdLib(-9223372036854775807)` 则在类型转换区间发生了溢出,返回 -9223372036854775808,在  大的正数输入时, `WithStdLib(9223372036854775807)` 也会返回不正确的负数结果。 不依赖分支控制的方法取绝对值的方法对有符号整数显然更快更准,不过还有更好的办法吗? @@ -89,7 +89,7 @@ TEXT ·WithASM(SB),$0 `WithASM` 的基准测试结果: ```shell -$ go test -bench=. +$ Go test -bench=. goos: darwin goarch: amd64 pkg: github.com/cavaliercoder/abs @@ -109,7 +109,7 @@ ok github.com/cavaliercoder/abs 6.059s 运行效果: ```console -$ go tool compile -m abs.go +$ Go tool compile -m abs.go # github.com/cavaliercoder/abs ./abs.go:11:6: can inline WithBranch ./abs.go:21:6: can inline WithStdLib @@ -169,14 +169,14 @@ func WithBranch(n int64) int64 { 重新编译,我们会看到编译器优化内容变少了: ```go -$ go tool compile -m abs.go +$ Go tool compile -m abs.go abs.go:22:23: inlining call to math.Abs ``` 基准测试的结果: ```console -$ go test -bench=. +$ Go test -bench=. goos: darwin goarch: amd64 pkg: github.com/cavaliercoder/abs @@ -209,7 +209,7 @@ func WithTwosComplement(n int64) int64 { 编译结果说明我们的方法被内联了: ```shell -$ go tool compile -m abs.go +$ Go tool compile -m abs.go ... abs.go:26:6: can inline WithTwosComplement ``` @@ -217,7 +217,7 @@ abs.go:26:6: can inline WithTwosComplement 但是性能怎么样呢?结果表明:当我们启用函数内联时,性能与 `WithBranch` 很相近了: ```shell -$ go test -bench=. +$ Go test -bench=. goos: darwin goarch: amd64 pkg: github.com/cavaliercoder/abs @@ -234,7 +234,7 @@ ok github.com/cavaliercoder/abs 6.777s 使用 `-S` 参数告诉编译器打印出汇编过程: ```shell -$ go tool compile -S abs.go +$ Go tool compile -S abs.go ... "".WithTwosComplement STEXT nosplit size=24 args=0x10 locals=0x0 0x0000 00000 (abs.go:26) TEXT "".WithTwosComplement(SB), NOSPLIT, $0-16 diff --git a/published/tech/Profiling-Go.md b/published/tech/Profiling-Go.md index 08bf18ee1..884d72022 100644 --- a/published/tech/Profiling-Go.md +++ b/published/tech/Profiling-Go.md @@ -21,8 +21,8 @@ Go 实现的是 _并行的_ [标记-清除垃圾回收器](http://wiki.c2.com/?M - 优点 - 缺点 + 优点 + 缺点 @@ -34,11 +34,11 @@ Go 实现的是 _并行的_ [标记-清除垃圾回收器](http://wiki.c2.com/?M pprof - - 详述CPU和内存使用情况。
+ - 详述 CPU 和内存使用情况。
- 可以远程分析。
- 可以生成图像。 - 需要改代码。
- - 需调用API。 + - 需调用 API。 trace @@ -46,7 +46,7 @@ Go 实现的是 _并行的_ [标记-清除垃圾回收器](http://wiki.c2.com/?M - 强大的调试界面。
- 易实现问题区域的可视化。 - 需要改代码。
- - UI界面复杂。
+ - UI 界面复杂。
- 理解需要一点时间。 @@ -171,7 +171,7 @@ memory comparison... 使用这个工具,有下面几种方式: 1. 开发时在程序中加入指令代码,生成分析用的 `.profile` 文件。 -2. 通过 web server 远程分析程序(不明确生产 `.profile` 文件)。 +2. 通过 Web server 远程分析程序(不明确生产 `.profile` 文件)。 > 注意: 性能概要文件不一定带有 `.profile` 后缀名 (文件类型可以自己定义) @@ -216,7 +216,7 @@ func main() { `go build -o app && time ./app > cpu.profile` -最后,使用 go tool 命令,以交互的方式检查数据: +最后,使用 Go tool 命令,以交互的方式检查数据: `go tool pprof cpu.profile` @@ -238,7 +238,7 @@ Showing nodes accounting for 180ms, 100% of 180ms total 0 0% 100% 180ms 100% runtime.systemstack /.../src/runtime/asm_amd64.s ``` -这表示 `runtime.memclrNoHeapPointers` 占用了最多的CPU时间。 +这表示 `runtime.memclrNoHeapPointers` 占用了最多的 CPU 时间。 我们一行一行的分解程序,可以更准确地观察 CPU 的使用情况。 @@ -331,7 +331,7 @@ Showing nodes accounting for 95.38MB, 100% of 95.38MB total ``` 因为是个简单的示例程序,可以很清晰地看出主要的内存分配发生在 `main.bigBytes` 函数中。 -如果想看一些更详细的数据,可以执行`list main.`: +如果想看一些更详细的数据,可以执行 `list main.`: ``` (pprof) list main. @@ -369,7 +369,7 @@ ROUTINE ======================== main.main in /.../code/go/profiling/main.go 在下面的例子中修改了代码,我们建立了一个 Web 服务,并导入 `"net/http/pprof"` 包 (https://golang.org/pkg/net/http/pprof/),以实现自动分析。 -> 注意:如果你的程序已经使用了 Web 服务器,你不必再新建一个。pprof 包会挂载到 web 服务的多路复用器(multiplexer)。 +> 注意:如果你的程序已经使用了 Web 服务器,你不必再新建一个。pprof 包会挂载到 Web 服务的多路复用器(multiplexer)。 ```go package main @@ -391,7 +391,7 @@ func bigBytes() *[]byte { func main() { var wg sync.WaitGroup - go func() { + Go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() @@ -420,7 +420,7 @@ profiles: 0 mutex 7 threadcreate -full goroutine stack dump +full Goroutine stack dump /debug/pprof/ ``` @@ -430,16 +430,16 @@ full goroutine stack dump 首先,先认识一下这五个分析文件的含义: * **block**: 同步原语引起阻塞的跟踪信息; -* **goroutine**: 所有当前 go 协程的跟踪信息; +* **goroutine**: 所有当前 Go 协程的跟踪信息; * **heap**: 堆内存分配情况; * **mutex**: 竞争互斥的跟踪信息; * **threadcreate**: 创建操作系统线程的跟踪信息; -web 服务器也可以产生 30s 的 CPU 性能分析文件,访问地址http://localhost:6060/debug/pprof/profile(这个文件不能在浏览器中展示,但可以下载到你的本地系统中) +web 服务器也可以产生 30s 的 CPU 性能分析文件,访问地址 http://localhost:6060/debug/pprof/profile(这个文件不能在浏览器中展示,但可以下载到你的本地系统中) 在访问 `/debug/pprof/` 时,看不到 CPU 性能分析的链接。因为做 CPU 性能分析需要调用特殊的 API(也就是,`StartCPUProfile` 和 `StopCPUProfile` 函数),只有调用后才产生输出流,最终下载到你的文件系统。 -web 服务器可以产生“追踪”文件,访问地址http://localhost:6060/debug/pprof/trace?seconds=5(与 CPU 性能分析一样的原因,都没有列出来,调用后才产生输出数据,然后下载到你的文件系统)。这个“追踪”文件需要用 go tool trace 进行解析(后面的章节会讲解 go tool trace )。 +web 服务器可以产生“追踪”文件,访问地址 http://localhost:6060/debug/pprof/trace?seconds=5(与 CPU 性能分析一样的原因,都没有列出来,调用后才产生输出数据,然后下载到你的文件系统)。这个“追踪”文件需要用 Go tool trace 进行解析(后面的章节会讲解 Go tool trace )。 > 注意:pprof 的选项信息可以参考:[golang.org/pkg/net/http/pprof/](https://golang.org/pkg/net/http/pprof/) @@ -582,7 +582,7 @@ ROUTINE ======================== main.bigBytes in /.../go/profiling/main.go ### Web UI 界面 -不久后,pprof 分析会新增一个交互式的 web 界面(大概在 2017 年 11 月份)。 +不久后,pprof 分析会新增一个交互式的 Web 界面(大概在 2017 年 11 月份)。 更多信息请参考[这篇文章](https://rakyll.org/pprof-ui/) @@ -612,7 +612,7 @@ func main() { wg.Add(1) var result []byte - go func() { + Go func() { result = make([]byte, 500000000) log.Println("done here") wg.Done() @@ -624,14 +624,14 @@ func main() { ``` 要用 trace 追踪功能,你只要导入 `"runtime/trace"`,然后调用 `trace.Start` 和 `trace.Stop` 函数。(为了追踪程序的所有内容,在 `trace.Stop` 函数前加入 `defer`) -此外,我们创建了一个 go 协程,在其中创建了一个 500MB 的切片。等待 go 协程执行完成,然后记录 result 的类型。这样做可以看到更多的直观数据。 +此外,我们创建了一个 Go 协程,在其中创建了一个 500MB 的切片。等待 Go 协程执行完成,然后记录 result 的类型。这样做可以看到更多的直观数据。 现在重新编译程序,用 trace 打开产生的追踪数据: ``` -$ go build -o app +$ Go build -o app $ time ./app > app.trace -$ go tool trace app.trace +$ Go tool trace app.trace ``` > 注意:使用 `-pprof` 标签,可以生成 pprof 兼容的文件(比如要动态地检查数据时)。更多信息请参考[go documentation](https://golang.org/cmd/trace/)。 @@ -664,17 +664,17 @@ $ go tool trace app.trace ### Go 协程 -如果图形放大到足够大,你可以看到 “goroutines”(go 协程)这一部分,它由两种颜色构成:浅绿色(可运行 go 协程)和深绿色(正在运行的 go 协程)。如果你点击图形,在屏幕下方的预览中,可以看到样例的明细。有趣的是,在任何特定的时刻,会有多个 go 协程存在,但它们不一定同时运行。 +如果图形放大到足够大,你可以看到 “goroutines”(go 协程)这一部分,它由两种颜色构成:浅绿色(可运行 Go 协程)和深绿色(正在运行的 Go 协程)。如果你点击图形,在屏幕下方的预览中,可以看到样例的明细。有趣的是,在任何特定的时刻,会有多个 Go 协程存在,但它们不一定同时运行。 -在我们的例子中,可以看到程序的变化:从一个准备运行的 go 协程,其并没有真正运行(也就是 “runnable” 可运行状态),继续向前看到有两个 go 协程在运行。(也就是,两个都处在 “running” 运行状态,没有 go 协程处于 “runnable” 状态) +在我们的例子中,可以看到程序的变化:从一个准备运行的 Go 协程,其并没有真正运行(也就是 “runnable” 可运行状态),继续向前看到有两个 Go 协程在运行。(也就是,两个都处在 “running” 运行状态,没有 Go 协程处于 “runnable” 状态) -有趣的是,在图中还可以看到,运行中的 go 协程数量与底层操作系统所创建的线程数量之间的关系。 +有趣的是,在图中还可以看到,运行中的 Go 协程数量与底层操作系统所创建的线程数量之间的关系。 ### 线程 -同样的,放大图片你也可以看到 “threads”(线程)这一部分,它由两种颜色构成:浅紫色(syscalls 系统调用)和深紫色(running threads运行中的线程)。 +同样的,放大图片你也可以看到 “threads”(线程)这一部分,它由两种颜色构成:浅紫色(syscalls 系统调用)和深紫色(running threads 运行中的线程)。 -在界面中 “heap” 部分有意思的内容是,因为 go 垃圾回收是并发执行,所以我们看到应用程序在堆上从未分配超过 100mb 的内存(go 垃圾回收在不同的进程/线程上运行)。程序运行中,无用的内存就被清理掉了。 +在界面中 “heap” 部分有意思的内容是,因为 Go 垃圾回收是并发执行,所以我们看到应用程序在堆上从未分配超过 100mb 的内存(go 垃圾回收在不同的进程/线程上运行)。程序运行中,无用的内存就被清理掉了。 这是可以理解的,因为在我们的程序中分配了 100mb 的内存给变量 `s`,这个变量仅在 loop 循环中有效。 一旦 loop 完成一次迭代,`s` 变量就不引用任何地址,所以 GC(垃圾回收)就可以清理掉这块内存。 @@ -686,13 +686,13 @@ $ go tool trace app.trace ### 进程 -在界面中 “procs” 部分,可以看到,在分配 500mb 内存时,Proc 3(进程 3)上有一个新的 go 协程在运行 `main.main.func1` 函数(在我们的程序中,这个函数负责内存分配工作) +在界面中 “procs” 部分,可以看到,在分配 500mb 内存时,Proc 3(进程 3)上有一个新的 Go 协程在运行 `main.main.func1` 函数(在我们的程序中,这个函数负责内存分配工作) 如果在 “View Options”(查看选项)中选择 “Flow events”(流事件),你可以看到一个箭头从 `main.main` 函数指向 `main.main.func1` 函数,`main.main.func1` 是运行在一个独立的进程/线程上。(箭头不容易看到,但确实有) ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/profiling-go/profiling_go_3.png) -通过图形界面,不但可直观的见到 `main.main.func1` 协程运行与内存分配的对应关系,而且能够看到程序的因果关系(也就是,_什么_ 触发了新的 go 协程的运行) +通过图形界面,不但可直观的见到 `main.main.func1` 协程运行与内存分配的对应关系,而且能够看到程序的因果关系(也就是,_什么_ 触发了新的 Go 协程的运行) ## 结尾 diff --git a/published/tech/Promoted-fields-and-methods-in-Go.md b/published/tech/Promoted-fields-and-methods-in-Go.md index 603603d3c..1972ee23a 100644 --- a/published/tech/Promoted-fields-and-methods-in-Go.md +++ b/published/tech/Promoted-fields-and-methods-in-Go.md @@ -12,8 +12,8 @@ type Person struct { age int32 } func main() { - person := Person{name: "Michał", age: 29} - fmt.Println(person) // {Michał 29} + person := Person{name: "Micha ł", age: 29} + fmt.Println(person) // {Micha ł 29} } ``` @@ -35,7 +35,7 @@ type T struct { 如果编译,编译器将抛出错误: ``` -> go install github.com/mlowicki/sandbox +> Go install github.com/mlowicki/sandbox # github.com/mlowicki/sandbox src/github.com/mlowicki/sandbox/sandbox.go:34: duplicate field Request ``` @@ -62,11 +62,11 @@ type Record struct { } ... record := Record{} -record.name = "Michał" +record.name = "Micha ł" record.age = 29 record.position = "software engineer" -fmt.Println(record) // {{Michał 29} {software engineer}} -fmt.Println(record.name) // Michał +fmt.Println(record) // {{Micha ł 29} {software engineer}} +fmt.Println(record.name) // Micha ł fmt.Println(record.age) // 29 fmt.Println(record.position) // software engineer fmt.Println(record.IsAdult()) // true @@ -79,7 +79,7 @@ fmt.Println(record.IsManager()) // false ```go //record := Record{} -record := Record{name: "Michał", age: 29} +record := Record{name: "Micha ł", age: 29} ``` 它将导致编译器抛出错误: @@ -92,7 +92,7 @@ record := Record{name: "Michał", age: 29} 可以通过创建一个明确的,完整的,嵌入的结构体来达到我们的目的: ```go -Record{Person{name: "Michał", age: 29}, Employee{position: "Software Engineer"}} +Record{Person{name: "Micha ł", age: 29}, Employee{position: "Software Engineer"}} ``` [来源:](https://golang.org/ref/spec#Struct_types) @@ -102,7 +102,7 @@ Record{Person{name: "Michał", age: 29}, Employee{position: "Software Engineer"} via: https://medium.com/golangspec/promoted-fields-and-methods-in-go-4e8d7aefb3e3 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[gogeof](https://github.com/gogeof) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/Quick-intro-to-go-assembly.md b/published/tech/Quick-intro-to-go-assembly.md index 1630fa1c0..4d2ee9171 100644 --- a/published/tech/Quick-intro-to-go-assembly.md +++ b/published/tech/Quick-intro-to-go-assembly.md @@ -30,9 +30,9 @@ Go ASM 和标准的汇编语法( NASM 或 YASM )不太一样,首先你会 - `SB`: 静态基址指针 –全局符号– - `SP`: 栈指针 –栈的顶端–. -这些虚拟寄存器在 Go ASM 中占有了重要地位,并且被广泛使用,其中最重要的就要属 SB 和 FP了。 +这些虚拟寄存器在 Go ASM 中占有了重要地位,并且被广泛使用,其中最重要的就要属 SB 和 FP 了。 -伪寄存器 SB 可以看作是内存的起始地址,所以 foo(SB) 就是 foo 在内存中的地址。语法中有两种修饰符,<> 和 +N (N是一个整数)。第一种情况 foo<>(SB) 代表了一个私有元素,只有在同一个源文件中才可以访问,类似于 Go 里面的小写命名。第二种属于对相对地址加上一个偏移量后得到的地址,所以 foo+8(SB) 就指向 foo 之后 8 个字节处的地址。 +伪寄存器 SB 可以看作是内存的起始地址,所以 foo(SB) 就是 foo 在内存中的地址。语法中有两种修饰符,<> 和 +N (N 是一个整数)。第一种情况 foo<>(SB) 代表了一个私有元素,只有在同一个源文件中才可以访问,类似于 Go 里面的小写命名。第二种属于对相对地址加上一个偏移量后得到的地址,所以 foo+8(SB) 就指向 foo 之后 8 个字节处的地址。 伪寄存器 FP 是一个虚拟帧指针,被用来引用过程参数,这些引用由编译器负责维护,它们将指向从伪寄存器处偏移的栈中参数。在一台 64 位机器上, 0(FP) 是第一个参数, 8(FP) 就是第二个参数。为了引用这些参数,编译器会强制它们的命名使用,这是出于清晰和可读性的考虑。所以 MOVL foo+0(FP), CX 会把虚拟的 FP 寄存器中的第一个参数放入到物理上的 CX 寄存器,以及 MOVL bar+8(FP), DX 会把第二个参数放入到 DX 寄存器中。 @@ -113,13 +113,13 @@ TEXT ·neg(SB), NOSPLIT, $0 - `·neg`: 该过程的包符号和符号。 - `(SB)`: 词法分析器会用到。 - `NOSPLIT`: 使得没有必要定义参数大小。–可以省略不写– -- `$0`: 参数的大小, 如果定义了`NOSPLIT` 就是 `$0` 。 +- `$0`: 参数的大小, 如果定义了 `NOSPLIT` 就是 `$0` 。 -build的步骤仍旧和往常一样,使用 `go build` 命令, Go 编译器会根据文件名–`amd64`–自动链接`.s` 文件。 +build 的步骤仍旧和往常一样,使用 `go build` 命令, Go 编译器会根据文件名–`amd64`–自动链接 `.s` 文件。 还有一份资源可以帮助学习 Go 文件的编译过程,我们可以看下 `go tool build -S ` 生成的 Go ASM 。 -一些类似 `NOSPLIT` 和 `RODATA` 的符号都是在 `textflax` 头文件中定义,因此用`#include textflag.h` 包含 该文件可以有利于完成一次没有报错的完美编译。 +一些类似 `NOSPLIT` 和 `RODATA` 的符号都是在 `textflax` 头文件中定义,因此用 `#include textflag.h` 包含 该文件可以有利于完成一次没有报错的完美编译。 ## MacOS 种的系统调用 @@ -135,7 +135,7 @@ MacOS 中的系统调用需要在加上调用号 `0x2000000` 后才能被调用, 所有的 MacOS 系统调用号列表可以在 [这里](https://opensource.apple.com/source/xnu/xnu-1504.3.12/bsd/kern/syscalls.master) 找到. -参数是通过这些寄存器 `DI`, `SI`, `DX`, `R10`, `R8` 和`R9` 传递给系统调用, 系统调用代码存放在 `AX` 中。 +参数是通过这些寄存器 `DI`, `SI`, `DX`, `R10`, `R8` 和 `R9` 传递给系统调用, 系统调用代码存放在 `AX` 中。 NASM 中的写法类似这样: @@ -170,9 +170,9 @@ section data: foo: db "My random string", 0x00 ``` -可这在 Go 中不行,在我深入研究了我能从网上找到的所有 go ASM 项目后,我还是没能找到一个定义简单字符串的示例。最后我在 Plan9 汇编语言文档中找到了一个例子,它可以说明怎样让目标实现。 +可这在 Go 中不行,在我深入研究了我能从网上找到的所有 Go ASM 项目后,我还是没能找到一个定义简单字符串的示例。最后我在 Plan9 汇编语言文档中找到了一个例子,它可以说明怎样让目标实现。 -Go 和 Plan9 唯一的不同之处是使用双引号而非单引号,并且添加了一个`RODATA` 符号: +Go 和 Plan9 唯一的不同之处是使用双引号而非单引号,并且添加了一个 `RODATA` 符号: ```asm DATA foo<>+0x00(SB)/8, $"My rando" @@ -191,7 +191,7 @@ TEXT ·helloWorld(SB), NOSPLIT, $0 注意,定义字符串时不能放在一起,需要把它们定义在 8 字节( 64 位)的块中。 -现在你可以深入 Go ASM 世界中写下你自己的超级快速和极端优化的代码l,并请记住,去读那些操蛋的手册(微笑脸)。 +现在你可以深入 Go ASM 世界中写下你自己的超级快速和极端优化的代码 l,并请记住,去读那些操蛋的手册(微笑脸)。 ## 在安全领域使用? diff --git a/published/tech/Reusable-barriers-in-Golang.md b/published/tech/Reusable-barriers-in-Golang.md index 418785950..e54fa110f 100644 --- a/published/tech/Reusable-barriers-in-Golang.md +++ b/published/tech/Reusable-barriers-in-Golang.md @@ -6,7 +6,7 @@ ## 问题 -现在假设我们我们有一堆 workers。为了充分发挥 CPU 多核的能力,我们让每个 worker 运行在单独的 goroutine 中: +现在假设我们我们有一堆 workers。为了充分发挥 CPU 多核的能力,我们让每个 worker 运行在单独的 Goroutine 中: ```go for i := 0; i < workers; i++ { @@ -24,7 +24,7 @@ func worker() { } ``` -每次 job 前都需要在所有的 worker 上同步地先进行一次准备 bootstrap 的过程。也就是说,每个 worker 在执行 job 前,需要等待所有其他 worker 都完成 bootstrap 的准备。 +每次 job 前都需要在所有的 worker 上同步地先进行一次准备 Bootstrap 的过程。也就是说,每个 worker 在执行 job 前,需要等待所有其他 worker 都完成 Bootstrap 的准备。 ```go func worker() { @@ -36,7 +36,7 @@ func worker() { } ``` -还有件事。如果至少有一个 worker 仍在执行 job,则所有 worker 的下一次的 bootstrap 都不能开始。换句话说,每次的 bootstrap 都是为紧接着的 job 部分做准备的,所以不能在上一次的 job 尚未结束之前就开始下一次的 bootstrap: +还有件事。如果至少有一个 worker 仍在执行 job,则所有 worker 的下一次的 Bootstrap 都不能开始。换句话说,每次的 Bootstrap 都是为紧接着的 job 部分做准备的,所以不能在上一次的 job 尚未结束之前就开始下一次的 bootstrap: ```go func worker() { @@ -49,7 +49,7 @@ func worker() { } ``` -我们的 bootstrap 部分内容为增长一个共享的计数器。job 部分为等待一段时间并打印计数器的内容: +我们的 Bootstrap 部分内容为增长一个共享的计数器。job 部分为等待一段时间并打印计数器的内容: ```go type counter struct { @@ -396,7 +396,7 @@ func (b *Barrier) After() { via: https://medium.com/golangspec/reusable-barriers-in-golang-156db1f75d0b -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[alfred-zhong](https://github.com/alfred-zhong) 校对:[rxcai](https://github.com/rxcai) diff --git a/published/tech/Seeking-around-in-an-HTTP-object.md b/published/tech/Seeking-around-in-an-HTTP-object.md index 058841549..ce222fdfe 100644 --- a/published/tech/Seeking-around-in-an-HTTP-object.md +++ b/published/tech/Seeking-around-in-an-HTTP-object.md @@ -33,7 +33,7 @@ Content-Type: application/zip 现在我们需要写一个处理 `ZIP` 文件格式的方法,它可以让我们使用具有 `Range` 头部的 `HTTP` 的 `GET` 请求,只读取元数据的方式,实现替换“读取下一个目录头文件”的某个部分。这就是 `Go` 的 [`archive/zip`](https://golang.org/pkg/archive/zip) 和 [`archive/tar`](https://godoc.org/archive/tar) 包的实现! -正如我们前面所说,[zip.NewReader](https://godoc.org/archive/zip#NewReader) 正在琢磨什么位置开始查找。然而,当我们看看 `TAR` 时,我们发现了一个问题。`tar.NewReader` 方法需要一个 `io.Reader`。`io.Reader` 的问题在于,它不会让我们随机访问资源,就像`io.ReaderAt` 一样。它是这样实现的,因为它使 `tar` 包更具适应性。特别是,您可以将 `Go tar` 包直接挂接到 `compress/gzip` 包并读取 `tar.gz` 文件 - 只要您按顺序读取它们,而不是像我们希望的那样跳过。 +正如我们前面所说,[zip.NewReader](https://godoc.org/archive/zip#NewReader) 正在琢磨什么位置开始查找。然而,当我们看看 `TAR` 时,我们发现了一个问题。`tar.NewReader` 方法需要一个 `io.Reader`。`io.Reader` 的问题在于,它不会让我们随机访问资源,就像 `io.ReaderAt` 一样。它是这样实现的,因为它使 `tar` 包更具适应性。特别是,您可以将 `Go tar` 包直接挂接到 `compress/gzip` 包并读取 `tar.gz` 文件 - 只要您按顺序读取它们,而不是像我们希望的那样跳过。 那么该怎么办?使用源码。环顾四周,找找[下一个方法](https://github.com/golang/go/blob/c007ce824d9a4fccb148f9204e04c23ed2984b71/src/archive/tar/reader.go#L88)。这就是我们期望它能够找到下一个元数据的地方。在几行代码内,对于 [`skipUnread`](https://github.com/golang/go/blob/c007ce824d9a4fccb148f9204e04c23ed2984b71/src/archive/tar/reader.go#L407) 函数, 我们发现一个有趣的调用。在那里,我们发现一些非常有趣的东西: @@ -50,7 +50,7 @@ func (tr *Reader) skipUnread() { _, tr.err = io.CopyN(ioutil.Discard, tr.r, nr) } -// Note: This is from Go 1.4, which had a simpler skipUnread than go 1.9 does. +// Note: This is from Go 1.4, which had a simpler skipUnread than Go 1.9 does. ``` 这里表示:”如果 `io.Reader` 实际上也能够搜索,那么我们不是直接读取和丢弃,而是直接找到正确的地方。“找到了!我们只需要将 `tar` 文件传给 `io.Reader`。`NewReader` 也满足 [`io.Seeker`](https://golang.org/pkg/io/#Seeker)的功能(因此,它是一个[`io.ReadSeeker`](https://golang.org/pkg/io/#ReadSeeker))。 @@ -112,7 +112,7 @@ File: 00000001-X009741H/00/000/001/00000001.pdf 你能看到问题吗?这是很多 `HTTP` 事务! `TAR reader` 正在一次一点点地完成 `TAR` 流,发出一小串 `bit`。所有这些短的 `HTTP` 事务在服务器上都很难实现,并且对于吞吐量来说很糟糕,因为每个 `HTTP` 事务都需要多次往返服务器。 -当然,解决方案是缓存。**读取TAR读取器要求的前 512 个字节,而不是读取其中的 10 倍,以便接下来的几个读取将直接从缓存中获取。**如果读取超出了缓存的范围,我们假设其他读取也将进入该区域,并删除整个当前缓存,以便用当前偏移量的 10 倍填充它。 +当然,解决方案是缓存。**读取 TAR 读取器要求的前 512 个字节,而不是读取其中的 10 倍,以便接下来的几个读取将直接从缓存中获取。**如果读取超出了缓存的范围,我们假设其他读取也将进入该区域,并删除整个当前缓存,以便用当前偏移量的 10 倍填充它。 `TAR` 阅读器发送**大量小读数**的事实指出了有关缓冲的一些非常重要的事情。将 [`os.Open`](https://godoc.org/os#Open) 的结果直接发送给 `tar`。`NewReader` 不是很聪明,尤其是如果你打算跳过文件寻找元数据。尽管 `* os.File` 实现了 `io.ReadSeeker`,我们现在知道 `TAR` 将会向内核发出大量的**小系统调用**。该解决方案与上面的解决方案非常相似,可能是使用 [`bufio`](https://godoc.org/bufio) 包来缓冲 `* os.File`,以便 `TAR` 发出的小数据将从 `RAM` 中取出,而不是转到操作系统。但请注意:它真的是解决方案吗?`bufio.Reader` 是否真的实现了 `io`?`ReadSeeker` 和 `io.ReadAt` 就像我们需要的一样? **(破坏者:它没有;也许你们有读者想告诉我们如何使用下一个的替代品 `bufio` 加速 `Go` 的 `tar`?** @@ -125,4 +125,4 @@ via:https://blog.gopheracademy.com/advent-2017/seekable-http/ 译者:[yuhanle](https://github.com/yuhanle) 校对:[Unknwon](https://github.com/Unknwon) -本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go中文网](https://studygolang.com/) 荣誉推出 +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/Singleton-Design-Pattern-in-Go.md b/published/tech/Singleton-Design-Pattern-in-Go.md index b89ad0053..19a4d1887 100644 --- a/published/tech/Singleton-Design-Pattern-in-Go.md +++ b/published/tech/Singleton-Design-Pattern-in-Go.md @@ -19,7 +19,7 @@ 在 Go 中我们可以利用包和类型的作用域和封装规则来实现 Singleton,对于这篇文章,我们将探索我的 straps 包,因为它将给我们一个现实世界的例子。 -straps 包提供了一种机制来将配置选项(straps)存储在XML文档中,并将其读入内存以供应用程序使用。straps 的名称来自配置网络设备的早期阶段。 这些设置被称为 straps,并且这个名字一直伴随着我。 在 MacOS 中,我们有 .plist 文件,在 .Net 中我们有 app.config 文件,在 Go 中有 straps.xml 文件。 +straps 包提供了一种机制来将配置选项(straps)存储在 XML 文档中,并将其读入内存以供应用程序使用。straps 的名称来自配置网络设备的早期阶段。 这些设置被称为 straps,并且这个名字一直伴随着我。 在 MacOS 中,我们有 .plist 文件,在 .Net 中我们有 app.config 文件,在 Go 中有 straps.xml 文件。 以下是我的一个应用程序的示例 straps 文件: diff --git a/published/tech/Small-Functions-considered-Harmful.md b/published/tech/Small-Functions-considered-Harmful.md index 8f6a5a672..9c21f8cd6 100644 --- a/published/tech/Small-Functions-considered-Harmful.md +++ b/published/tech/Small-Functions-considered-Harmful.md @@ -151,7 +151,7 @@ Rubyist Sandi Metz 有一场名为 All The Little Things 的著名演讲,她 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/small-functions-considered-harmful/Small-Functions-considered-Harmful-9.jpg) -通常情况下,程序员只会在代码确定被删除,或者不再使用时,将代码视为 “已死亡”。如果我们开始(以代码将 “死亡”)思考我们的编写的代码,那么每增加一个新的 git commit,我认为我们可能会更加积极地编写易于修改的代码。在思考如何抽象时,认识到我们正在构建的代码可能距离死亡(正在被修改)只有几个小时的事实对于我们很有帮助。因此,为了便于修改代码而进行的优化往往比试图构建 《Clean Code》中提到的自顶向下的设计更好。 +通常情况下,程序员只会在代码确定被删除,或者不再使用时,将代码视为 “已死亡”。如果我们开始(以代码将 “死亡”)思考我们的编写的代码,那么每增加一个新的 Git commit,我认为我们可能会更加积极地编写易于修改的代码。在思考如何抽象时,认识到我们正在构建的代码可能距离死亡(正在被修改)只有几个小时的事实对于我们很有帮助。因此,为了便于修改代码而进行的优化往往比试图构建 《Clean Code》中提到的自顶向下的设计更好。 ## 类污染 diff --git a/published/tech/Some-common-traps-while-using-defer.md b/published/tech/Some-common-traps-while-using-defer.md index c5a0652c6..5f4ab9a4e 100644 --- a/published/tech/Some-common-traps-while-using-defer.md +++ b/published/tech/Some-common-traps-while-using-defer.md @@ -2,7 +2,7 @@ # 使用 defer 时可能遇到的若干陷阱 -go 的 defer 语句对改善代码可读性起了很大作用。但是,某些情况下 defer 的行为很容易引起混淆,并且难以快速理清。尽管作者已经使用 go 两年多了,依然会被 defer 弄得挠头不已。我的计划是把过去曾困惑过我的一系列行为汇总起来,作为对自己的警示。 +go 的 defer 语句对改善代码可读性起了很大作用。但是,某些情况下 defer 的行为很容易引起混淆,并且难以快速理清。尽管作者已经使用 Go 两年多了,依然会被 defer 弄得挠头不已。我的计划是把过去曾困惑过我的一系列行为汇总起来,作为对自己的警示。 ## defer 的作用域是一个函数,不是一个语句块 @@ -136,7 +136,7 @@ Success: false, Latency: 0s ## 结论 -如果你使用 go 的时间足够久,那么这些可能都算不上 “陷阱” 。但对新手来说, defer 语句的很多地方都不符合 [最少吃惊原则](https://en.wikipedia.org/wiki/Principle_of_least_astonishment) 。还有 [更](http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/) [多](https://studygolang.com/articles/12061) [地](https://studygolang.com/articles/12136) [方](https://studygolang.com/articles/12319) 深入研究了使用 go 时可能遇到的常见失误。欢迎阅读。 +如果你使用 Go 的时间足够久,那么这些可能都算不上 “陷阱” 。但对新手来说, defer 语句的很多地方都不符合 [最少吃惊原则](https://en.wikipedia.org/wiki/Principle_of_least_astonishment) 。还有 [更](http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/) [多](https://studygolang.com/articles/12061) [地](https://studygolang.com/articles/12136) [方](https://studygolang.com/articles/12319) 深入研究了使用 Go 时可能遇到的常见失误。欢迎阅读。 --- diff --git a/published/tech/Stacks-and-queues.md b/published/tech/Stacks-and-queues.md index e1fcda766..97aeb46a2 100644 --- a/published/tech/Stacks-and-queues.md +++ b/published/tech/Stacks-and-queues.md @@ -95,7 +95,7 @@ func (history *ActionHistory) Undo() *Action { 这是堆栈的基本行为。堆栈是一种数据结构,您只能在堆栈顶部插入或删除元素。把它想象成一堆文件,或者你厨房抽屉里的一堆盘子。如果你想从那一堆盘子取出最下面的盘子,那是挺难的。但是拿最上面的那个是简单的。堆栈也被认为是 `LIFO` 结构 —— 意思是后进先出,我们前面解释过那是为什么。 -这基本上就是我们的 `Undo` 函数所处理的。如果堆栈(或者说`ActionHistory`)有多个 `Action` ,它将为第二项设置顶部链接。否则,它将清空 `ActionHistory`,将 `top` 元素设置为 `nil`。 +这基本上就是我们的 `Undo` 函数所处理的。如果堆栈(或者说 `ActionHistory`)有多个 `Action` ,它将为第二项设置顶部链接。否则,它将清空 `ActionHistory`,将 `top` 元素设置为 `nil`。 从 `Big-O` 表示法来看,在堆栈中搜索的复杂度是 `O(n)`,但是在堆栈中插入和删除是非常快的复杂度是 `O(1)`。 这是因为遍历整个堆栈,在最坏的情况下,仍然会在其中执行所有的 `n` 项,而插入和删除元素的时间复杂度是常量时间,因为我们总是从堆栈的顶部插入和删除。 @@ -122,7 +122,7 @@ type Luggage struct { 与此同时,我们为 `Luggage` 类型添加简单的构造函数: ```go -func NewLuggage(weight int, passenger string) *Luggage { +func NewLuggage(weight int, Passenger string) *Luggage { l := Luggage{ weight: weight, passenger: passenger, // just as an identifier @@ -148,7 +148,7 @@ func (belt *Belt) Add(newLuggage *Luggage) { ``` 既然 `Belt` 实际上是一个切片,那么我们就可以用 Go 语言内建函数 `append` 将 `newLuggage` 添加到 `Belt` 上。这个实现很奇妙的部分是时间复杂度 -- 因为我们使用了 `append` 这个内建函数,所以插入操作的时间复杂度是 O(1)。 -当然,这有一定的控间浪费,这是一位 go 语言切片的工作原理造成的。 +当然,这有一定的控间浪费,这是一位 Go 语言切片的工作原理造成的。 当 `Belt` 开始运动并且将 `Luggage` 带到 X 光机上,我们需要将行李拿下来并且装进机器进行检查。 鉴于 `Belt` 的自然属性,第一个放到传送带上面的行李是第一个被扫描监测的。 @@ -158,7 +158,7 @@ func (belt *Belt) Add(newLuggage *Luggage) { ```go func (belt *Belt) Take() *Luggage { - first, rest := (*belt)[0], (*belt)[1:] + first, REST := (*belt)[0], (*belt)[1:] *belt = rest return first } @@ -193,7 +193,7 @@ First luggage: &{3 Elmer Fudd} Belt: &[0x1040a0d0 0x1040a0e0 0x1040a100 0x1040a110] Length: 4 ``` -基本上,我们在 `Belt` 上加了5个不同的 `Luggage`,然后我们取出第一个元素,它在屏幕的第二行输出显示了。 +基本上,我们在 `Belt` 上加了 5 个不同的 `Luggage`,然后我们取出第一个元素,它在屏幕的第二行输出显示了。 你可以在[*这里*](https://play.golang.org/p/DTFUkWeZ4H8)使用实例代码。 @@ -214,7 +214,7 @@ type Luggage struct { 当然,我们使用 `newLuggage` 函数创建 `luggage` 的时候会加入 `priority` 作为参数。 ```go -func NewLuggage(weight int, priority int, passenger string) *Luggage { +func NewLuggage(weight int, priority int, Passenger string) *Luggage { l := Luggage{ weight: weight, priority: priority, @@ -224,7 +224,7 @@ func NewLuggage(weight int, priority int, passenger string) *Luggage { } ``` -让我们再想想。基本上,当一个新的 `Luggage` 被放在 `Belt` 上时,我们需要检测它的 `priority`,并根据 `priority` 把它放在 `Belt`的最前面。 +让我们再想想。基本上,当一个新的 `Luggage` 被放在 `Belt` 上时,我们需要检测它的 `priority`,并根据 `priority` 把它放在 `Belt` 的最前面。 我们在修改一下 `Add` 函数: diff --git a/published/tech/Thread-Pooling-in-Go-Programming.md b/published/tech/Thread-Pooling-in-Go-Programming.md index 8becd6b0e..81d8bab79 100644 --- a/published/tech/Thread-Pooling-in-Go-Programming.md +++ b/published/tech/Thread-Pooling-in-Go-Programming.md @@ -2,7 +2,7 @@ # Go 语言中的线程池(Thread Pooling in Go Programming) -用过一段时间的 Go 之后,我学会了如何使用一个不带缓存的 channel 去创建一个 goroutine 池。我喜欢这个实现,这个实现甚至比这篇博文描述的更好。虽然这样说,这篇博文仍然对它所描述的部分有一定的价值。 +用过一段时间的 Go 之后,我学会了如何使用一个不带缓存的 channel 去创建一个 Goroutine 池。我喜欢这个实现,这个实现甚至比这篇博文描述的更好。虽然这样说,这篇博文仍然对它所描述的部分有一定的价值。 [https://github.com/goinggo/work](https://github.com/goinggo/work) @@ -22,11 +22,11 @@ 在 Go 语言中我不创建线程,而是创建协程。协程函数类似于多线程函数,但由 Go 来管理实际上在系统层面运行的线程。了解更多关于 Go 中的并发,查看这个文档:[http://golang.org/doc/effective_go.html#concurrency](http://golang.org/doc/effective_go.html#concurrency)。 -我创建了名为 workpool 和 jobpool 的包。它们通过 channel 和 go 协程来实现池的功能。 +我创建了名为 workpool 和 jobpool 的包。它们通过 channel 和 Go 协程来实现池的功能。 ## 工作池(Workpool) -这个包创建了一个 go 协程池,专门用来处理发布到池子中的工作。一个独立的 Go 协程负责工作的排队处理。协程提供安全的工作排队,跟踪队列中工作量,当队列满时报告错误。 +这个包创建了一个 Go 协程池,专门用来处理发布到池子中的工作。一个独立的 Go 协程负责工作的排队处理。协程提供安全的工作排队,跟踪队列中工作量,当队列满时报告错误。 提交工作到队列中是一个阻塞操作。这样调用者才能知道工作是否已经进入队列。(workpool 也会一直)保持工作队列中活动程序数量的计数。 diff --git a/published/tech/Trying-Clean-Architecture-on-Golang.md b/published/tech/Trying-Clean-Architecture-on-Golang.md index c016f9dc2..fee03af3e 100644 --- a/published/tech/Trying-Clean-Architecture-on-Golang.md +++ b/published/tech/Trying-Clean-Architecture-on-Golang.md @@ -156,7 +156,7 @@ type ArticleUsecase interface { 与用例层相同,因为该层依赖于用例层,意味着该层需要用例层来支持测试。基于之前定义的契约接口, 也需要对用例层进行模拟。 -对于模拟,我使用 vektra 的 golang的模拟库: +对于模拟,我使用 vektra 的 golang 的模拟库: [https://github.com/vektra/mockery](https://github.com/vektra/mockery) ## 仓库层(Repository)测试 @@ -238,7 +238,7 @@ Mockery 将会为我生成一个仓库层模型,我不需要先完成仓库( ## 表现层( Delivery )测试 -表现层测试依赖于你如何传递的数据。如果使用 http REST API, 我们可以使用 golang 中的内置包 httptest。 +表现层测试依赖于你如何传递的数据。如果使用 http REST API, 我们可以使用 Golang 中的内置包 httptest。 因为该层依赖于用例( Usecase )层, 所以 我们需要模拟 Usecase,与仓库层相同,我使用 Mockery 模拟我的 Usecase 来进行表现层( Delivery )的测试。 @@ -369,7 +369,7 @@ func main() { * [https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html](https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html) -* [http://manuel.kiessling.net/2012/09/28/applying-the-clean-architecture-to-go-applications/](http://manuel.kiessling.net/2012/09/28/applying-the-clean-architecture-to-go-applications/)。 这是Golang种另一个版本的简洁架构。 +* [http://manuel.kiessling.net/2012/09/28/applying-the-clean-architecture-to-go-applications/](http://manuel.kiessling.net/2012/09/28/applying-the-clean-architecture-to-go-applications/)。 这是 Golang 种另一个版本的简洁架构。 如果你任何问题,或者需要更多的解释,或者我在这里没有解释清楚的。你可以通过我的[LinkedIn](https://www.linkedin.com/in/imantumorang/)或者[email](iman.tumorang@gmail.com)联系我。谢谢。 diff --git a/published/tech/Type-assertions-in-Go.md b/published/tech/Type-assertions-in-Go.md index 970712a2e..80386aa18 100644 --- a/published/tech/Type-assertions-in-Go.md +++ b/published/tech/Type-assertions-in-Go.md @@ -179,7 +179,7 @@ main.B{name:""} false > 当断言不成立时,第一个值将会作为测试类型的[零值](https://golang.org/ref/spec#The_zero_value) ## 资源: -* [go 编程语言规范- go 编程语言](https://golang.org/ref/spec#TypeAssertion) +* [go 编程语言规范- Go 编程语言](https://golang.org/ref/spec#TypeAssertion) * [Go 是一个通用语言,设计时考虑了系统编程。它是强类型的并且具有垃圾回收机制...](https://golang.org/ref/spec#TypeAssertion) * [golang.org](https://golang.org/ref/spec#TypeAssertion) @@ -187,8 +187,8 @@ main.B{name:""} false via:https://medium.com/golangspec/type-assertions-in-go-e609759c42e1 - 作者:[Michał Łowicki](https://medium.com/@mlowicki) + 作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[fredvence](https://github.com/fredvence) 校对:[rxcai](https://github.com/rxcai) - 本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go中文网](https://studygolang.com/) 荣誉推出 + 本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/Using-Go-as-a-scripting-language-in-Linux.md b/published/tech/Using-Go-as-a-scripting-language-in-Linux.md index f01d96264..d2da699de 100644 --- a/published/tech/Using-Go-as-a-scripting-language-in-Linux.md +++ b/published/tech/Using-Go-as-a-scripting-language-in-Linux.md @@ -14,15 +14,15 @@ * 易于使用的非特权包管理:如果你想在脚本中使用第三方库,你可以简单的使用 `go get` 命令来获取。而且由于拉取的代码将安装在你的 `GOPATH` 中,使用一些第三方库并不需要系统管理员的权限(与其他一些脚本语言不同)。这在大型企业环境中尤其有用。 -* 在早期项目阶段进行快速的代码原型设计:当您进行第一次代码迭代时,通常需要进行大量的编辑,甚至进行编译,而且您必须在 "编辑->构建->检查" 循环中浪费大量的按键。相反,使用 Go,您可以跳过 `build` 部分,并立即执行源文件。 +* 在早期项目阶段进行快速的代码原型设计:当您进行第一次代码迭代时,通常需要进行大量的编辑,甚至进行编译,而且您必须在 "编辑-> 构建-> 检查" 循环中浪费大量的按键。相反,使用 Go,您可以跳过 `build` 部分,并立即执行源文件。 * 强类型的脚本语言:如果你在脚本中的某个地方有个小的输入错误,大多数的脚本语言都会执行到有错误的地方然后停止。这可能会让你的系统处于不一致的状态(因为有些语句的执行会改变数据的状态,从而污染了执行脚本之前的状态)。使用强类型语言时,许多拼写错误可以在编译时被捕获,因此有 bug 的脚本将不会首先运行。 ## Go 脚本的当前状态 -咋一看 Go 脚本貌似很容易实现 Unix 脚本的 shebang(#! ...) 支持。[shebang 行](https://en.wikipedia.org/wiki/Shebang_(Unix))是脚本的第一行,以 `#!` 开头,并指定脚本解释器用于执行脚本(例如,`#!/bin/bash` 或 `#!/usr/bin/env python`),所以无论使用何种编程语言,系统都确切知道如何执行脚本。Go 已经使用 `go run` 命令支持 `.go` 文件的类似于解释器的调用,所以只需要添加适当的 shebang 行(`#!/usr/bin/env go run`)到任何的 `.go` 文件中,设置好文件的可执行状态,然后就可以愉快的玩耍了。 +咋一看 Go 脚本貌似很容易实现 Unix 脚本的 shebang(#! ...) 支持。[shebang 行](https://en.wikipedia.org/wiki/Shebang_(Unix))是脚本的第一行,以 `#!` 开头,并指定脚本解释器用于执行脚本(例如,`#!/bin/bash` 或 `#!/usr/bin/env python`),所以无论使用何种编程语言,系统都确切知道如何执行脚本。Go 已经使用 `go run` 命令支持 `.go` 文件的类似于解释器的调用,所以只需要添加适当的 shebang 行(`#!/usr/bin/env Go run`)到任何的 `.go` 文件中,设置好文件的可执行状态,然后就可以愉快的玩耍了。 -但是,直接使用 go run 还是有问题的。[这篇牛 b 的文章](https://gist.github.com/posener/73ffd326d88483df6b1cb66e8ed1e0bd)详细描述了围绕 `go run` 的所有问题和潜在解决方法,但其要点是: +但是,直接使用 Go run 还是有问题的。[这篇牛 b 的文章](https://gist.github.com/posener/73ffd326d88483df6b1cb66e8ed1e0bd)详细描述了围绕 `go run` 的所有问题和潜在解决方法,但其要点是: * `go run` 不能正确地将脚本错误代码返回给操作系统,这对脚本很重要,因为错误代码是多个脚本之间相互交互和操作系统环境最常见的方式之一。 @@ -42,7 +42,7 @@ helloscript.go:1:1: illegal character U+0023 '#' OK,看起来 shebang 的方法并没有为我们提供全面的解决方案。是否还有其他方式是我们可以使用的?让我们仔细看看 Linux 内核如何执行二进制文件。 当你尝试执行一个二进制/脚本(或任何有可执行位设置的文件)时,你的 shell 最后只会使用 Linux `execve` 系统调用,将它传递给二进制文件系统路径,命令行参数和 当前定义的环境变量。 然后内核负责正确解析文件并用文件中的代码创建一个新进程。 我们中的大多数人都知道 Linux (和许多其他类 Unix 操作系统)为其可执行文件使用 ELF 二进制格式。 -然而,Linux 内核开发的核心原则之一是避免任何子系统的 “vendor/format lock-in”,这是内核的一部分。因此,Linux 实现了一个“可插拔”系统,它允许内核支持任何二进制格式 - 所有你需要做的就是编写一个正确的模块,它可以解析你选择的格式。如果仔细研究内核源代码,你会发现 Linux 支持更多的二进制格式。例如,最近的`4.14` Linux 内核,我们可以看到它至少支持7种二进制格式(用于各种二进制格式的树内模块通常在其名称中具有 `binfmt_` 前缀)。值得注意的是 [binfmt_script](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/tree/fs/binfmt_script.c?h=linux-4.14.y) 模块,它负责解析上面提到的 shebang 行并在目标系统上执行脚本(并不是每个人都知道 shebang 支持实际上是在内核本身而不是在 shell 或其他守护进程/进程中实现的)。 +然而,Linux 内核开发的核心原则之一是避免任何子系统的 “vendor/format lock-in”,这是内核的一部分。因此,Linux 实现了一个“可插拔”系统,它允许内核支持任何二进制格式 - 所有你需要做的就是编写一个正确的模块,它可以解析你选择的格式。如果仔细研究内核源代码,你会发现 Linux 支持更多的二进制格式。例如,最近的 `4.14` Linux 内核,我们可以看到它至少支持 7 种二进制格式(用于各种二进制格式的树内模块通常在其名称中具有 `binfmt_` 前缀)。值得注意的是 [binfmt_script](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git/tree/fs/binfmt_script.c?h=linux-4.14.y) 模块,它负责解析上面提到的 shebang 行并在目标系统上执行脚本(并不是每个人都知道 shebang 支持实际上是在内核本身而不是在 shell 或其他守护进程/进程中实现的)。 ## 从用户空间扩展受支持的二进制格式 @@ -60,13 +60,13 @@ systemd-1 on /proc/sys/fs/binfmt_misc type autofs (rw,relatime,fd=27,pgrp=1,time 接下来,因为我们希望我们的 .go 脚本能够正确地将退出代码传递给操作系统,所以我们需要将定制的 gorun 包装器作为我们的“解释器”: ```shell -$ go get github.com/erning/gorun +$ Go get github.com/erning/gorun $ sudo mv ~/go/bin/gorun /usr/local/bin/ ``` 从技术角度上讲,我们不需要将 gorun 移动到 `/usr/local/bin` 或任何其他系统路径,而无论如何 `binfmt_misc` 都需要解释器的完整路径,但系统可以以任意权限运行此可执行文件,因此从安全视角来看限制文件访问权限是一个好主意。 -在这一点上,让我们来建一个简单的 go 脚本 `helloscript.go` 并验证我们可以成功“解释”它。脚本如下: +在这一点上,让我们来建一个简单的 Go 脚本 `helloscript.go` 并验证我们可以成功“解释”它。脚本如下: ```go package main @@ -97,15 +97,15 @@ func main() { ```shell $ gorun helloscript.go Hello, world! -$ echo $? +$ Echo $? 0 $ gorun helloscript.go gopher Hello, gopher! -$ echo $? +$ Echo $? 0 $ gorun helloscript.go fail Hello, fail! -$ echo $? +$ Echo $? 30 ``` @@ -114,11 +114,11 @@ $ echo $? 让我们注册我们新的 Go 脚本二进制格式: ```shell -$ echo ':golang:E::go::/usr/local/bin/gorun:OC' | sudo tee /proc/sys/fs/binfmt_misc/register +$ Echo ':golang:E::go::/usr/local/bin/gorun:OC' | sudo tee /proc/sys/fs/binfmt_misc/register :golang:E::go::/usr/local/bin/gorun:OC ``` -如果系统成功注册了,则应在 `/proc/sys/fs/binfmt_misc` 目录下显示新的 golang 文件。 最后,我们可以在本地执行我们的 .go 文件: +如果系统成功注册了,则应在 `/proc/sys/fs/binfmt_misc` 目录下显示新的 Golang 文件。 最后,我们可以在本地执行我们的 .go 文件: ```shell $ chmod u+x helloscript.go @@ -128,7 +128,7 @@ $ ./helloscript.go gopher Hello, gopher! $ ./helloscript.go fail Hello, fail! -$ echo $? +$ Echo $? 30 ``` diff --git a/published/tech/Visualizing-Concurrency-in-Go.md b/published/tech/Visualizing-Concurrency-in-Go.md index 4923ce93e..fb17d0bb6 100644 --- a/published/tech/Visualizing-Concurrency-in-Go.md +++ b/published/tech/Visualizing-Concurrency-in-Go.md @@ -24,7 +24,7 @@ func main() { ch := make(chan int) // start new anonymous goroutine - go func() { + Go func() { // send 42 to channel ch <- 42 }() @@ -36,11 +36,11 @@ func main() { ![hello](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/hello.gif) -在这张图中,蓝色的线代表 goroutine 的时间轴。连接 `main` 和 `go#19` 的蓝线是用来标记 goroutine 的起始和终止并且表示父子关系的。红色的箭头代表的是 send/recv 操作。尽管 send/recv 操作是两个独立的操作,但是我试着将它们表示成一个操作 `从 A 发送到 B`。右边蓝线上的 `#19` 是该 goroutine 的内部 ID,可以通过 Scott Mansfield 在 [Goroutine IDs](http://blog.sgmansfield.com/2015/12/goroutine-ids/) 一文中提到的技巧获取。 +在这张图中,蓝色的线代表 Goroutine 的时间轴。连接 `main` 和 `go#19` 的蓝线是用来标记 Goroutine 的起始和终止并且表示父子关系的。红色的箭头代表的是 send/recv 操作。尽管 send/recv 操作是两个独立的操作,但是我试着将它们表示成一个操作 `从 A 发送到 B`。右边蓝线上的 `#19` 是该 Goroutine 的内部 ID,可以通过 Scott Mansfield 在 [Goroutine IDs](http://blog.sgmansfield.com/2015/12/goroutine-ids/) 一文中提到的技巧获取。 ## 计时器(Timers) -事实上,我们可以通过简单的几个步骤编写一个计时器:创建一个 channel,启动一个 goroutine 以给定间隔往 channel 中写数据,将这个 chennel 返回给调用者。调用者阻塞地从 channel 中读,就会得到一个精准的时钟。让我们来试试调用这个程序 24 次并且将过程可视化。 +事实上,我们可以通过简单的几个步骤编写一个计时器:创建一个 channel,启动一个 Goroutine 以给定间隔往 channel 中写数据,将这个 chennel 返回给调用者。调用者阻塞地从 channel 中读,就会得到一个精准的时钟。让我们来试试调用这个程序 24 次并且将过程可视化。 ```go package main @@ -49,7 +49,7 @@ import "time" func timer(d time.Duration) <-chan int { c := make(chan int) - go func() { + Go func() { time.Sleep(d) c <- 1 }() @@ -73,7 +73,7 @@ func main() { 这个例子是我从 Google 员工 Sameer Ajmani 的一次演讲 ["Advanced Go Concurrency Patterns"](https://talks.golang.org/2013/advconc.slide#1) 中找到的。当然,这并不是一个很高阶的并发模型,但是对于 Go 语言并发的新手来说是很有趣的。 -在这个例子中,我们定义了一个 channel 来作为“乒乓桌”。乒乓球是一个整形变量,代码中有两个 goroutine “玩家”通过增加乒乓球的 counter 在“打球”。 +在这个例子中,我们定义了一个 channel 来作为“乒乓桌”。乒乓球是一个整形变量,代码中有两个 Goroutine “玩家”通过增加乒乓球的 counter 在“打球”。 ```go package main @@ -83,8 +83,8 @@ import "time" func main() { var Ball int table := make(chan int) - go player(table) - go player(table) + Go player(table) + Go player(table) table <- Ball time.Sleep(1 * time.Second) @@ -109,28 +109,28 @@ func player(table chan int) { 现在,我们给这个模型添加一个玩家(goroutine)。 ```go - go player(table) - go player(table) - go player(table) + Go player(table) + Go player(table) + Go player(table) ``` [WebGL 动画界面](http://divan.github.io/demos/pingpong3/) ![Ping-pong2](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/pingpong3.gif) -我们可以看到每个 goroutine 都有序地“打到球”,你可能会好奇这个行为的原因。那么,为什么这三个 goroutine 始终按照一定顺序接收到 ball 呢? +我们可以看到每个 Goroutine 都有序地“打到球”,你可能会好奇这个行为的原因。那么,为什么这三个 Goroutine 始终按照一定顺序接收到 ball 呢? -答案很简单,Go 运行时会对每个 channel 的所有接收者维护一个 [FIFO 队列 ](https://github.com/golang/go/blob/master/src/runtime/chan.go#L34)。在我们的例子中,每个 goroutine 会在它将 ball 传给 channel 之后就开始等待 channel,所以它们在队列里的顺序总是一定的。让我们增加 goroutine 的数量,看看顺序是否仍然保持一致。 +答案很简单,Go 运行时会对每个 channel 的所有接收者维护一个 [FIFO 队列 ](https://github.com/golang/go/blob/master/src/runtime/chan.go#L34)。在我们的例子中,每个 Goroutine 会在它将 ball 传给 channel 之后就开始等待 channel,所以它们在队列里的顺序总是一定的。让我们增加 Goroutine 的数量,看看顺序是否仍然保持一致。 ```go for i := 0; i < 100; i++ { - go player(table) + Go player(table) } ``` [WebGL 动画界面](http://divan.github.io/demos/pingpong100/) ![Ping-pong100](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/pingpong100.gif) -很明显,它们的顺序仍然是一定的。我们可以创建一百万个 goroutine 去尝试,但是上面的实验已经足够让我们得出结论了。接下来,让我们来看看一些不一样的东西,比如说通用的消息模型。 +很明显,它们的顺序仍然是一定的。我们可以创建一百万个 Goroutine 去尝试,但是上面的实验已经足够让我们得出结论了。接下来,让我们来看看一些不一样的东西,比如说通用的消息模型。 ## 扇入模式(Fan-In) @@ -162,9 +162,9 @@ func reader(out chan int) { func main() { ch := make(chan int) out := make(chan int) - go producer(ch, 100*time.Millisecond) - go producer(ch, 250*time.Millisecond) - go reader(out) + Go producer(ch, 100*time.Millisecond) + Go producer(ch, 250*time.Millisecond) + Go reader(out) for i := range ch { out <- i } @@ -178,7 +178,7 @@ func main() { ## 工作者模式(Workers) -与扇入模式相反的模式叫做扇出(fan-out)或者工作者(workers)模式。多个 goroutine 可以从相同的 channel 中读数据,利用多核并发完成自身的工作,这就是工作者(workers)模式的由来。在 Go 中,这个模式很容易实现,只需要启动多个以 channel 作为参数的 goroutine,主函数传数据给这个 channel,数据分发和复用会由 Go 运行环境自动完成。 +与扇入模式相反的模式叫做扇出(fan-out)或者工作者(workers)模式。多个 Goroutine 可以从相同的 channel 中读数据,利用多核并发完成自身的工作,这就是工作者(workers)模式的由来。在 Go 中,这个模式很容易实现,只需要启动多个以 channel 作为参数的 goroutine,主函数传数据给这个 channel,数据分发和复用会由 Go 运行环境自动完成。 ```go package main @@ -206,7 +206,7 @@ func pool(wg *sync.WaitGroup, workers, tasks int) { tasksCh := make(chan int) for i := 0; i < workers; i++ { - go worker(tasksCh, wg) + Go worker(tasksCh, wg) } for i := 0; i < tasks; i++ { @@ -219,7 +219,7 @@ func pool(wg *sync.WaitGroup, workers, tasks int) { func main() { var wg sync.WaitGroup wg.Add(36) - go pool(&wg, 36, 50) + Go pool(&wg, 36, 50) wg.Wait() } ``` @@ -227,7 +227,7 @@ func main() { ![Workers](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/workers.gif) -在这里需要提一下并行结构(parallelism)。我们可以看到,动图中所有的 goroutine 都是平行“延伸”,等待 channel 给它们发数据来运行的。我们还可以注意到两个 goroutine 接收数据之间几乎是没有停顿的。不幸的是,这个动画并没有用颜色区分一个 goroutine 是在等数据还是在执行工作,这个动画是在 `GOMAXPROCS=4` 的情况下录制的,所以只有 4 个 goroutine 能够同时运行。我们将会在下文汇总讨论这个主题。 +在这里需要提一下并行结构(parallelism)。我们可以看到,动图中所有的 Goroutine 都是平行“延伸”,等待 channel 给它们发数据来运行的。我们还可以注意到两个 Goroutine 接收数据之间几乎是没有停顿的。不幸的是,这个动画并没有用颜色区分一个 Goroutine 是在等数据还是在执行工作,这个动画是在 `GOMAXPROCS=4` 的情况下录制的,所以只有 4 个 Goroutine 能够同时运行。我们将会在下文汇总讨论这个主题。 现在,我们来写更复杂一点的代码,启动带有子工作者的工作者(subworkers): @@ -268,7 +268,7 @@ func worker(tasks <-chan int, wg *sync.WaitGroup) { subtasks := make(chan int) for i := 0; i < SUBWORKERS; i++ { - go subworker(subtasks) + Go subworker(subtasks) } for i := 0; i < SUBTASKS; i++ { task1 := task * i @@ -284,7 +284,7 @@ func main() { tasks := make(chan int) for i := 0; i < WORKERS; i++ { - go worker(tasks, &wg) + Go worker(tasks, &wg) } for i := 0; i < TASKS; i++ { @@ -305,7 +305,7 @@ Go 中还存在比这更酷的扇出模式,比如动态工作者/子工作者 ## 服务器(Servers) -下一个要说的常用模式和扇出相似,但是它会在短时间内生成多个 goroutine 来完成某些任务。这个模式常被用来实现服务器 -- 创建一个监听器,在循环中运行 accept() 并针对每个接受的连接启动 goroutine 来完成指定任务。这个模式很形象并且它能尽可能地简化服务器 handler 的实现。让我们来看一个简单的例子: +下一个要说的常用模式和扇出相似,但是它会在短时间内生成多个 Goroutine 来完成某些任务。这个模式常被用来实现服务器 -- 创建一个监听器,在循环中运行 accept() 并针对每个接受的连接启动 Goroutine 来完成指定任务。这个模式很形象并且它能尽可能地简化服务器 handler 的实现。让我们来看一个简单的例子: ```go package main @@ -327,7 +327,7 @@ func main() { if err != nil { continue } - go handler(c) + Go handler(c) } } ``` @@ -337,7 +337,7 @@ func main() { 从并发的角度看好像什么事情都没有发生。当然,表面平静,内在其实风起云涌,完成了一系列复杂的操作,只是复杂性都被隐藏了,毕竟 [Simplicity is complicated.](https://www.youtube.com/watch?v=rFejpH_tAHM) -但是让我们回归到并发的角度,给我们的服务器添加一些交互功能。比如说,我们定义一个 logger 以独立的 goroutine 的形式来记日志,每个 handler 想要异步地通过这个 logger 去写数据。 +但是让我们回归到并发的角度,给我们的服务器添加一些交互功能。比如说,我们定义一个 logger 以独立的 Goroutine 的形式来记日志,每个 handler 想要异步地通过这个 logger 去写数据。 ```go package main @@ -366,7 +366,7 @@ func server(l net.Listener, ch chan string) { if err != nil { continue } - go handler(c, ch) + Go handler(c, ch) } } @@ -376,8 +376,8 @@ func main() { panic(err) } ch := make(chan string) - go logger(ch) - go server(l, ch) + Go logger(ch) + Go server(l, ch) time.Sleep(10 * time.Second) } ``` @@ -423,9 +423,9 @@ func pool(ch chan string, n int) { wch := make(chan int) results := make(chan int) for i := 0; i < n; i++ { - go logger(wch, results) + Go logger(wch, results) } - go parse(results) + Go parse(results) for { addr := <-ch l := len(addr) @@ -439,7 +439,7 @@ func server(l net.Listener, ch chan string) { if err != nil { continue } - go handler(c, ch) + Go handler(c, ch) } } @@ -449,8 +449,8 @@ func main() { panic(err) } ch := make(chan string) - go pool(ch, 4) - go server(l, ch) + Go pool(ch, 4) + Go server(l, ch) time.Sleep(10 * time.Second) } ``` @@ -458,13 +458,13 @@ func main() { ![Server3](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/servers3.gif) -在这个例子中,我们把记日志的任务分布到了 4 个 goroutine 中,有效地改善了 logger 模块的吞吐量。但是从动画中仍然可以看出,logger 仍然是系统中最容易出现性能问题的地方。如果上千个连接同时调用 logger 记日志, 现在的 logger 模块仍然可能会出现性能瓶颈。当然,相比于之前的实现,它的阈值已经高了很多。 +在这个例子中,我们把记日志的任务分布到了 4 个 Goroutine 中,有效地改善了 logger 模块的吞吐量。但是从动画中仍然可以看出,logger 仍然是系统中最容易出现性能问题的地方。如果上千个连接同时调用 logger 记日志, 现在的 logger 模块仍然可能会出现性能瓶颈。当然,相比于之前的实现,它的阈值已经高了很多。 ## 并发质数筛选法(Concurrent Prime Sieve) 看够了扇入/扇出模型,我们现在来看看具体的并行算法。让我们来讲讲我最喜欢的并行算法之一:并行质数筛选法。这个算法是我从 [Go Concurrency Patterns](https://talks.golang.org/2012/concurrency.slide) 这个演讲中看到的。质数筛选法(埃拉托斯特尼筛法)是在一个寻找给定范围内最大质数的古老算法。它通过一定的顺序筛掉多个质数的乘积,最终得到想要的最大质数。但是其原始的算法在多核机器上并不高效。 -这个算法的并行版本定义了多个 goroutine,每个 goroutine 代表一个已经找到的质数,同时有多个 channel 用来从 generator 传输数据到 filter。每当找到质数时,这个质数就会被一层层 channel 送到 main 函数来输出。当然,这个算法也不够高效,尤其是当你需要寻找一个很大的质数或者在寻找时间复杂度最低的算法时,但它的思想很优雅。 +这个算法的并行版本定义了多个 goroutine,每个 Goroutine 代表一个已经找到的质数,同时有多个 channel 用来从 generator 传输数据到 filter。每当找到质数时,这个质数就会被一层层 channel 送到 main 函数来输出。当然,这个算法也不够高效,尤其是当你需要寻找一个很大的质数或者在寻找时间复杂度最低的算法时,但它的思想很优雅。 ```go // A concurrent prime sieve @@ -493,12 +493,12 @@ func Filter(in <-chan int, out chan<- int, prime int) { // The prime sieve: Daisy-chain Filter processes. func main() { ch := make(chan int) // Create a new channel. - go Generate(ch) // Launch Generate goroutine. + Go Generate(ch) // Launch Generate goroutine. for i := 0; i < 10; i++ { prime := <-ch fmt.Println(prime) ch1 := make(chan int) - go Filter(ch, ch1, prime) + Go Filter(ch, ch1, prime) ch = ch1 } } @@ -518,15 +518,15 @@ func main() { ``` GOMAXPROCS sets the maximum number of CPUs that can be executing simultaneously. ``` -定义中的 CPU 指的是逻辑 CPU。我之前稍微修改了一下工作者的例子让每个 goroutine 都做一点会占用 CPU 时间的事情,然后我设置了不同的 `GOMAXPROCS` 值,重复运行这个例子,运行环境是一个 2 CPU, 共 24 核的机器,系统是 Linux。 +定义中的 CPU 指的是逻辑 CPU。我之前稍微修改了一下工作者的例子让每个 Goroutine 都做一点会占用 CPU 时间的事情,然后我设置了不同的 `GOMAXPROCS` 值,重复运行这个例子,运行环境是一个 2 CPU, 共 24 核的机器,系统是 Linux。 以下两个图中,第一张是运行在 1 个核上时的动画效果,第二张是运行在 24 核上时的动画效果。 -[WebGL 动画界面 1核](http://divan.github.io/demos/gomaxprocs1/) +[WebGL 动画界面 1 核](http://divan.github.io/demos/gomaxprocs1/) ![1Core-Worker](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/gomaxprocs1.gif) -[WebGL 动画界面 24核](http://divan.github.io/demos/gomaxprocs24/) +[WebGL 动画界面 24 核](http://divan.github.io/demos/gomaxprocs24/) ![24Core-Worker](https://raw.githubusercontent.com/studygolang/gctt-images/master/visualizing-concurrency/gomaxprocs24.gif) @@ -536,9 +536,9 @@ GOMAXPROCS sets the maximum number of CPUs that can be executing simultaneously. ## Goroutine 泄露(Goroutines leak) -Go 并发中还有什么使我们能可视化的呢? goroutine 泄露是我能想到的一个场景。当你启动一个 goroutine 但是它在你的代码外陷入了错误状态,或者是你启动了很多带有死循环的 goroutine 时,goroutine 泄露就发生了。 +Go 并发中还有什么使我们能可视化的呢? Goroutine 泄露是我能想到的一个场景。当你启动一个 Goroutine 但是它在你的代码外陷入了错误状态,或者是你启动了很多带有死循环的 Goroutine 时,goroutine 泄露就发生了。 -我仍然记得我第一次遇到 goroutine 泄露时,我脑子里想象的可怕场景。紧接着的周末,我就写了 expvarmon (一个 Go 应用的资源监控工具)。现在,我可以用 WebGL 来描绘当时在我脑海中的景象了。 +我仍然记得我第一次遇到 Goroutine 泄露时,我脑子里想象的可怕场景。紧接着的周末,我就写了 expvarmon (一个 Go 应用的资源监控工具)。现在,我可以用 WebGL 来描绘当时在我脑海中的景象了。 [WebGL 动画界面](http://divan.github.io/demos/leak/) @@ -593,9 +593,9 @@ JSON 文件的样例如下: 接下来,gothree.js 使用 [Three.js](http://threejs.org/) 这个能够用 WebGL 生成 3D 图像的的库来绘制动画。 -这种方法的使用场景非常有限。我必须精准地选择例子,重命名 channel 和 goroutine 来输出一个正确的 trace。这个方法也无法关联两个 goroutine 中的相同但不同名的 channel,更不用说识别通过 channel 传送的 channel 了。这个方法生成的时间戳也会出现问题,有时候输出信息到标准输出会比传值花费更多的时间,所以我为了得到正确的动画不得不在某些情况下让 goroutine 等待一些时间。 +这种方法的使用场景非常有限。我必须精准地选择例子,重命名 channel 和 Goroutine 来输出一个正确的 trace。这个方法也无法关联两个 Goroutine 中的相同但不同名的 channel,更不用说识别通过 channel 传送的 channel 了。这个方法生成的时间戳也会出现问题,有时候输出信息到标准输出会比传值花费更多的时间,所以我为了得到正确的动画不得不在某些情况下让 Goroutine 等待一些时间。 -这就是我并没有将这份代码开源的原因。我正在尝试使用 Dmitry Vyukov 的 [execution tracer](https://golang.org/cmd/trace/),它看起来能提供足够多的信息,但并不包含 channel 传输的值。也许有更好的方法来实现我的目标。如果你有什么想法,可以通过 twitter 或者在本文下方评论来联系我。如果我们能够把它做成一个帮助开发者调试和记录 Go 程序运行情况的工具的话就更好了。 +这就是我并没有将这份代码开源的原因。我正在尝试使用 Dmitry Vyukov 的 [execution tracer](https://golang.org/cmd/trace/),它看起来能提供足够多的信息,但并不包含 channel 传输的值。也许有更好的方法来实现我的目标。如果你有什么想法,可以通过 Twitter 或者在本文下方评论来联系我。如果我们能够把它做成一个帮助开发者调试和记录 Go 程序运行情况的工具的话就更好了。 如果你想用我的工具看一些算法的动画效果,可以在下方留言,我很乐意提供帮助。 diff --git a/published/tech/Waht-the-most-common-identifier-in-go-stdlib.md b/published/tech/Waht-the-most-common-identifier-in-go-stdlib.md index 2009a812c..06c131779 100644 --- a/published/tech/Waht-the-most-common-identifier-in-go-stdlib.md +++ b/published/tech/Waht-the-most-common-identifier-in-go-stdlib.md @@ -34,7 +34,7 @@ IfStmt = "if" [ SimpleStmt ";" ] Expression Block [ "else" ( IfStmt | Block ) ] 这些规则组成的集合就是语法,你可以在 Go 语言规范中找到它们的详细定义。 -这些规则不是简单的由程序的单个字符定义的,而是有一系列 token 组成。 这些token除了像 **if** 和 **else** 这样的原子 token 外, 还有像整数 42,浮点数 4.2 和字符串 “hello” 这样的复合 token, 以及像 **main** 这样的标识符。 +这些规则不是简单的由程序的单个字符定义的,而是有一系列 token 组成。 这些 token 除了像 **if** 和 **else** 这样的原子 token 外, 还有像整数 42,浮点数 4.2 和字符串 “hello” 这样的复合 token, 以及像 **main** 这样的标识符。 但是,我们是怎么知道 main 是一个标识符,而不是一个数字呢? 原来它也是有专门的规则来定义的。如果你读过 Go 语言规范中的标识符部分,你就会发现如下的规则: ``` @@ -43,7 +43,7 @@ identifier = letter { letter | unicode_digit } . 在这条规则中,letter 和 unicode_digit 不是 token 而是字符。 所以有了这些规则,就可以写一个程序来逐个字符地分析,一旦检测到一组字符匹配到某一条规则,就 “发射”(emits) 出一个 token。 -所以,如果我们以 **fmt.Println** 为例, 它可以产生这些 token:标识符 **fmt**, **“.”**, 以及标识符 **Println**。 这是一个函数调用吗? 在这里我们还无法确定,而且我们也不关心。它的结构就是一个序列,表明 token 出现的顺序。 +所以,如果我们以 **fmt.Println** 为例, 它可以产生这些 token:标识符 **fmt**, **“.”**, 以及标识符 **Println**。 这是一个函数调用吗? 在这里我们还无法确定,而且我们也不关心。它的结构就是一个序列,表明  token 出现的顺序。 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/most-common-identifier/1.png) @@ -157,7 +157,7 @@ for i := 0; i < len(pairs) && i < 5; i++ { 我们来用这个程序分析一下 github.com/golang/go 上的代码: ```bash -$ go install github.com/campoy/justforfunc/24-ast/scanner +$ Go install github.com/campoy/justforfunc/24-ast/scanner $ scanner ~/go/src/**/*.go 82163 v 46584 err @@ -179,7 +179,7 @@ for s, n := range counts { 再来一次: ```bash -$ go install github.com/campoy/justforfunc/24-ast/scanner +$ Go install github.com/campoy/justforfunc/24-ast/scanner $ scanner ~/go/src/**/*.go 46584 err 44681 Args diff --git a/published/tech/Write-a-Kubernetes-ready-service-from-zero-step-by-step.md b/published/tech/Write-a-Kubernetes-ready-service-from-zero-step-by-step.md index 067ff7a45..aa7db41d9 100644 --- a/published/tech/Write-a-Kubernetes-ready-service-from-zero-step-by-step.md +++ b/published/tech/Write-a-Kubernetes-ready-service-from-zero-step-by-step.md @@ -89,7 +89,7 @@ func home(w http.ResponseWriter, _ *http.Request) { } ``` -然后`main.go`中做点小改动: +然后 `main.go` 中做点小改动: ```go package main @@ -101,7 +101,7 @@ import ( "github.com/rumyantseva/advent-2017/handlers" ) -// How to try it: go run main.go +// How to try it: Go run main.go func main() { log.Print("Starting the service...") router := handlers.Router() @@ -156,7 +156,7 @@ func TestRouter(t *testing.T) { } ``` -检查了 `GET` 请求 `/home` 路径是否返回 `200`,而 `POST` 请求该路径应该要返回 `405`。请求不存在的路由期望返回`404`。实际上,这样子测有点太冗余了,`gorilla/mux` 中已经包含类似的测试,所以测试代码可以简化下。 +检查了 `GET` 请求 `/home` 路径是否返回 `200`,而 `POST` 请求该路径应该要返回 `405`。请求不存在的路由期望返回 `404`。实际上,这样子测有点太冗余了,`gorilla/mux` 中已经包含类似的测试,所以测试代码可以简化下。 对于 `home` 来说,检查其返回得 code 和 body 值即可。 @@ -192,10 +192,10 @@ func TestHome(t *testing.T) { } ``` -运行`go test`开始测试。 +运行 `go test` 开始测试。 ``` -$ go test -v ./... +$ Go test -v ./... ? github.com/rumyantseva/advent-2017 [no test files] === RUN TestRouter --- PASS: TestRouter (0.00s) @@ -222,7 +222,7 @@ import ( "github.com/rumyantseva/advent-2017/handlers" ) -// How to try it: PORT=8000 go run main.go +// How to try it: PORT=8000 Go run main.go func main() { log.Print("Starting the service...") @@ -346,7 +346,7 @@ func home(w http.ResponseWriter, _ *http.Request) { ``` RELEASE?=0.0.1 -COMMIT?=$(shell git rev-parse --short HEAD) +COMMIT?=$(shell Git rev-parse --short HEAD) BUILD_TIME?=$(shell date -u '+%Y-%m-%d_%H:%M:%S') ``` @@ -456,7 +456,7 @@ func Router(buildTime, commit, release string) *mux.Router { ## 第十步 添加平滑关闭功能 -关闭服务时,最好是不要立即中断连接、请求或者其他一些操作,而应该平滑关闭。Go 从 1.8 版本支持平滑关闭`http.Server`。下面看看怎么用: +关闭服务时,最好是不要立即中断连接、请求或者其他一些操作,而应该平滑关闭。Go 从 1.8 版本支持平滑关闭 `http.Server`。下面看看怎么用: `main.go` @@ -492,7 +492,7 @@ func main() { ``` 收到 `SIGINT` 或 `SIGTERM` 任意一个系统信号,服务平滑关闭。 -注意:当我在写这段代码的时候,我(作者)尝试去捕获 `SIGKILL` 信号。之前在不同的库中有看到过这种用法,我确认这样是行的通的。但是后来 Sandor Szücs [指出](https://twitter.com/sszuecs/status/941582509565005824) ,不可能获取到 `SIGKILL` 信号。发出 `SIGKILL` 信号后,程序会直接结束。 +注意:当我在写这段代码的时候,我(作者)尝试去捕获 `SIGKILL` 信号。之前在不同的库中有看到过这种用法,我确认这样是行的通的。但是后来 Sandor Sz ü cs [指出](https://twitter.com/sszuecs/status/941582509565005824) ,不可能获取到 `SIGKILL` 信号。发出 `SIGKILL` 信号后,程序会直接结束。 ## 第十一步 添加 Dockerfile @@ -527,7 +527,7 @@ GOARCH?=amd64 ... build: clean - CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} go build \ + CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} Go build \ -ldflags "-s -w -X ${PROJECT}/version.Release=${RELEASE} \ -X ${PROJECT}/version.Commit=${COMMIT} -X ${PROJECT}/version.BuildTime=${BUILD_TIME}" \ -o ${APP} @@ -546,7 +546,7 @@ run: container 添加了 `container` 和 `run` goal,前者构建镜像,后者从容器启动程序。所有改动 [这里](https://github.com/rumyantseva/advent-2017/commit/909fef6d585c85c5e16b5b0e4fdbdf080893b679) 可以找到。 -请尝试运行 `make run`命令,检查所有过程是否正确。 +请尝试运行 `make run` 命令,检查所有过程是否正确。 ## 第十二步 添加 vendor @@ -599,7 +599,7 @@ ee1f0f98199f: Pushed 0.0.1: digest: sha256:fb3a25b19946787e291f32f45931ffd95a933100c7e55ab975e523a02810b04c size: 528 ``` -成功了~!然后可以在[这里找到镜像](https://hub.docker.com/r/webdeva/advent/tags/)。 +成功了 ~!然后可以在[这里找到镜像](https://hub.docker.com/r/webdeva/advent/tags/)。 接下来,定义必要的 Kubernetes 配置(manifest)。通常,一个服务至少需要设置 deployment、service 和 ingress 配置。默认情况,manifest 都是静态的,即其中不能使用任何变量。不过可以通过 [helm 工具](https://github.com/kubernetes/helm) 创建更灵活的配置。 @@ -723,7 +723,7 @@ minikube: push cat $$t | \ gsed -E "s/\{\{(\s*)\.Release(\s*)\}\}/$(RELEASE)/g" | \ gsed -E "s/\{\{(\s*)\.ServiceName(\s*)\}\}/$(APP)/g"; \ - echo ---; \ + Echo ---; \ done > tmp.yaml kubectl apply -f tmp.yaml ``` @@ -762,7 +762,7 @@ Vary: Accept-Encoding {"buildTime":"2017-12-10_11:29:59","commit":"020a181","release":"0.0.5"}% ``` -成功~! +成功 ~! 所有步骤的代码在 [这里](https://github.com/rumyantseva/advent-2017) ,两个版本:[按 commit 划分](https://github.com/rumyantseva/advent-2017/commits/master) 以及 [按步骤划分](https://github.com/rumyantseva/advent-2017/tree/all-steps)。如有任何疑问,请 [提 issue](https://github.com/rumyantseva/advent-2017/issues/new),或者 tweet[@webdeva](https://twitter.com/webdeva),或者在评论区留评论。 diff --git a/published/tech/ardanlabs-modules/20191010-modules-01-why-and-what.md b/published/tech/ardanlabs-modules/20191010-modules-01-why-and-what.md new file mode 100644 index 000000000..5545dc070 --- /dev/null +++ b/published/tech/ardanlabs-modules/20191010-modules-01-why-and-what.md @@ -0,0 +1,157 @@ +首发于:https://studygolang.com/articles/24580 + +# Modules 第 1 部分:为什么和做什么 + +## 引言 + +Module 针对自 Go 语言初版发布以来即成为开发者痛点的三个关键问题提供了完整的解决方案,使得开发者: + +* 能够在 GOPATH 工作区之外使用 Go 代码; +* 能够对依赖包进行版本控制并识别可以使用的最兼容版本; +* 能够使用 Go 原生工具来管理依赖包; + +随着 Go 语言 1.13 版本的发布,这三个问题已经成为了“过去时”。在过去的两年中,Go 语言团队花费了很多精力才让所有人达到这一步。在本文中,我将重点介绍从 GOPATH 到 module 的迁移以及 module 所解决的问题。在此过程中,我将只提供足够的术语,以便您可以更好地了解 module 是如何在较高的层面上起作用的,也许更为重要的是,为什么它以这样的方式起作用。 + +## GOPATH + +使用 GOPATH 在磁盘上为 Go 工作区提供物理位置已经为 Go 语言开发者提供了很好的服务。不幸的是,对于部分非 Go 语言开发者来说,由于他们只是时不时的进行 Go 项目并且可能没有设置 Go 工作区,这可能是个瓶颈。Go 语言团队想要解决的问题之一便是允许将 Go 代码仓库克隆到磁盘上的任何位置(GOPATH 之外),同时 Go 工具能够对其进行定位、构建和测试。 + +图 1 + +![108_figure1.png](https://www.ardanlabs.com/images/goinggo/108_figure1.png) + +图 1 展示的是 [conf](https://github.com/ardanlabs/conf) 包的 GitHub 仓库。这个仓库是一个能为应用程序处理配置信息提供支撑的包。在 module 出现之前,如果您要使用该包,可以通过 `go get` 将这个仓库以其规范名为相对路径克隆到 GOPATH 中,其中包的规范名是远程仓库的根目录和仓库名的组合。 + +例如,如果您运行 `go get github.com/ardanlabs/conf`,那么代码将会被克隆到路径 `$GOPATH/src/github.com/ardanlabs/conf` 下。正是因为有了 GOPATH 以及仓库的规范名称,所以不论开发者选择将工作区置于何处,Go 工具都可以找到代码。 + +## 解析导入 + +清单 1 +[github.com/ardanlabs/conf/blob/master/conf_test.go](https://www.ardanlabs.com/blog/2019/10/github.com/ardanlabs/conf/blob/master/conf_test.go) + +```go +01 package conf_test +02 +03 import ( +... +10 "github.com/ardanlabs/conf" +... +12 ) +``` + +清单 1 展示的是 `conf` 仓库的测试文件 `conf_test.go` 中 import 部分的代码片段。当测试代码在包名中使用 `_test` 这样的约定命名方式时(如您在第 01 行看到的那样),意味着测试代码与被测试的代码存在于不同的包中,并且测试代码必须要像任何外部用户一样导入被测试的包。您可以在第 10 行看到该测试文件是怎样使用仓库的规范名来导入 `conf` 包的。借助 GOPATH 机制,可以将这个导入的包解析到磁盘上的具体位置,然后,Go 工具就可以可以定位、构建和测试代码了。 + +假使 GOPATH 不再存在并且包所处于的文件夹结构与仓库的规范名称也不再一致时,会怎样呢? + +清单 2 +```go +import "github.com/ardanlabs/conf" + +// GOPATH 模式:包在磁盘上的物理位置与 GOPATH +// 和仓库的规范名相匹配。 +$GOPATH/src/github.com/ardanlabs/conf + +// Module 模式:包在磁盘上的物理位置与仓库的规范名称 +// 不一致。 +/users/bill/conf +``` + +清单 2 展示了将 `conf` 仓库克隆到磁盘上任意位置时所遇到的问题。当开发者可以选择将代码克隆到所希望的任何位置时,所有必需的用来将导入的包解析到磁盘上具体物理位置的信息都消失了。 + +解决此问题的方法是使用一个包含仓库规范名的特殊文件。用该文件在磁盘上的位置来代替 GOPATH,无论仓库被克隆到何处,Go 工具都能够利用在其中定义的仓库规范名来解析导入。 + +这个特殊的文件被命名为 [go.mod](https://golang.org/cmd/go/#hdr-The_go_mod_file),而在其中定义的仓库规范名将代表称为 module 的新实体 。 + +清单 3 +[github.com/ardanlabs/conf/blob/v1.1.0/go.mod](https://www.ardanlabs.com/blog/2019/10/github.com/ardanlabs/conf/blob/v1.1.0/go.mod) + +```go +01 module github.com/ardanlabs/conf +02 +... +06 +``` + +清单 3 显示了 `conf` 仓库中 `go.mod` 文件的第一行。该行定义了 module 的名称,开发者可以用这个 module 名来索引该仓库中的任何代码。现在,把仓库克隆任何位置都是没问题的,因为 Go 工具可以使用 module 文件的位置和 module 名来解析任何内部导入,例如导入上述的测试文件。 + +借助 module 的概念,就可以将代码克隆到磁盘上的任何位置了,下一个将要解决的问题是支持将代码捆绑在一起并进行版本控制。 + +## 捆绑和版本控制 + +大多数版本控制系统都允许我们对代码仓库的任意提交点打标签(例如:v1.0.0、v2.3.8 等),这些标签被认为是不可变的,通常被用于发布新功能。 + +图 2 + +![108_figure2.png](https://www.ardanlabs.com/images/goinggo/108_figure2.png) + +图 2 展示 `conf` 包的作者给该仓库标记了三个不同的版本号,这些标签遵循着 [语义化版本号](https://semver.org/) 的格式。 + +借助版本控制工具,开发者可以通过特定标签将对应版本的 `conf` 包克隆到磁盘上。然而,首先我们需要回答几个问题: + +* 应该使用哪个版本的包? +* 怎么知道哪个版本与我正在编写和使用的所有代码都兼容? + +回答完这两个问题后,您还需要回答第三个问题: +* 要将仓库克隆到何处,以便 Go 工具可以找到和访问它? + +然后情况便变得更糟了,您不能在自己的项目中使用某个版本的 `conf` 包,除非您还克隆了所有 `conf` 所依赖包的仓库,这是您的所有项目都会遇到的依赖项传递问题。 + +在 GOPATH 模式下的解决方案是使用 `go get` 来识别并将所有依赖包的仓库克隆到您的 GOPATH 中。但是,这并不是一个完美的解决方案,因为 `go get` 只懂得如何为每个依赖包克隆仓库以及更新仓库 `master` 分支的最新代码。在编写代码初期,从依赖包仓库的 `master` 分支拉取代码或许没什么大碍。但是,在几个月(或几年)后,因为依赖包的独立演进,依赖包仓库的 `master` 分支的最新代码可能与您的项目已不再兼容。这是因为您的项目没有遵循版本标签,因此任何包的升级都可能包含破坏性的变更。 + +在新的 Go module 模式下,使用 `go get` 将所有依赖包的仓库克隆到一个单一的预定义好的工作空间中不再成为首选。另外,你需要一个适用于整个项目的方法,来引用每个依赖包的兼容版本。然后便是支持在你的项目中使用同一个依赖包的不同主版本,以防止你的依赖包正在导入主版本号不同的同一个包。 + +尽管,针对这些问题的若干解决方案已经以社区开发工具的形式存在了(例如:dep、godep、glide 等),但是 Go 语言需要的是一个完整的解决方案。这个解决方案便是复用 module 文件来维护一个版本化的依赖列表,其中包括直接或间接的依赖。然后将任何给定版本的仓库都视为一个不变的代码集合。这个版本化的不可变的代码集合称为 module。 + +## 完整的解决方案 + +图 3 + +![108_figure3.png](https://www.ardanlabs.com/images/goinggo/108_figure3.png) + +图 3 展示了仓库和 module 之间的关系。它表明了 import 指令是怎样引用存储在给定版本 module 内的包。在图 3 中,版本号为 1.1.0 的 module `conf` 中的代码可以从版本号为 0.3.1 的 module `go-cmp` 中导入包 `cmp`。由于依赖项信息已经在 `conf` module 中列出(通过 module 文件),因此 Go 工具可以获取其中任何 module 的特定版本,于是便可以成功构建。 + +一旦有了 module,很多工程机会就会浮现出来: + +* 您可以为构建、保留、认证、验证、获取、缓存和重用 module 提供支持(除了某些例外),以供全世界的 Go 开发者使用。 +* 您可以建一个代理服务器来支持不同的版本控制系统并提供某些上述的支持。 +* 您可以验证一个 module (对于任何给定的版本)始终包含完全相同的代码,而不论它被构建了多少次,以及从何处获取、由谁提供。 + +关于 module 所能完美支持的特性,已经由 Go 语言团队在 Go 1.13 发行版本中提供。 + +## 结论 + +这篇文章试图为理解 module 是什么以及 Go 语言团队如何使用该解决方案奠定基础。当然,仍有许多方面需要讨论,例如: + +* 怎样选用 module 的特定版本? +* module 文件的结构是怎样的,有哪些选项可用于控制对 module 的选择? +* module 是怎样构建、获取和缓存在本地以解决导入问题的? +* module 是怎样验证符合语义化版本号契约的? +* 在您的项目中应当怎样使用 module 以及最佳实践是什么? + +在后续的文章中,我计划提供对这些以及更多其他问题的理解。现在,请确保您已经了解仓库,包和 module 之间的关系。如有任何疑问,请随时在 Slack 上找到我。那里有一个很棒的频道 `#modules`,其中的人们随时可以提供帮助。 + +## Module 文档 + +有许多关于 Go 语言的文档,下面是由 Go 语言团队发布的一些文章和视频。 + +[Modules The Wiki](https://github.com/golang/go/wiki/Modules) + +[1.13 Go Release Notes](https://golang.org/doc/go1.13#modules) + +[Go Blog: Module Mirror and Checksum Database Launched](https://blog.golang.org/module-mirror-launch) + +[Go Blog: Publishing Go Modules](https://blog.golang.org/publishing-go-modules) + +[Proposal: Secure the Public Go Module Ecosystem](https://go.googlesource.com/proposal/+/master/design/25530-sumdb.md) + +[GopherCon 2019: Katie Hockman - Go Module Proxy: Life of a Query](https://www.youtube.com/watch?v=KqTySYYhPUE) + +--- + +via: https://www.ardanlabs.com/blog/2019/10/modules-01-why-and-what.html + +作者:[William Kennedy](https://www.ardanlabs.com/) +译者:[anxk](https://github.com/anxk) +校对:[DingdingZhou](https://github.com/DingdingZhou) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/ardanlabs-modules/20191202-modules-02-projects-dependencies-gopls.md b/published/tech/ardanlabs-modules/20191202-modules-02-projects-dependencies-gopls.md new file mode 100644 index 000000000..8819ecdc3 --- /dev/null +++ b/published/tech/ardanlabs-modules/20191202-modules-02-projects-dependencies-gopls.md @@ -0,0 +1,431 @@ +首发于:https://studygolang.com/articles/35202 + +# Go Module 教程第 2 部分:项目、依赖和 gopls + +## 引言 + +模块是集成到 Go 系统中,为依赖管理提供支持。这意味着模块几乎可以触及任何与源代码相关的内容,包括编辑器支持。为了向编辑器提供模块支持(以及其他原因) ,Go 团队构建了一个名为 [gopls](https://github.com/golang/tools/blob/master/gopls/doc/user.md) 的服务,它实现了语言服务器协议([LSP](https://microsoft.github.io/language-server-protocol/))。LSP 最初是由微软为 VSCode 开发的,现在已经成为一个开放标准。该协议的思想是为编辑器提供对语言特性的支持,比如自动完成、定义和查找所有引用。 + +当你使用模块和 VSCode 时,在编辑器中点击保存将不再直接运行 go build 命令。现在发生的情况是,一个请求被发送给 gopls,gopls 运行适当的 Go 命令和相关的 API 来提供编辑器反馈和支持。Gopls 还可以向编辑器发送信息而不需要请求。有时,由于 LSP 的特性或运行 Go 命令的固有延迟,编辑器似乎滞后或与代码更改不同步。团队正在努力达到一个 1.0 版本的 gopls 来处理这些边缘情况,这样你就可以有最平滑的编辑器体验。 + +在这篇文章中,我将介绍在项目中添加和删除依赖项的基本工作流程。本文使用了 VSCode 编辑器、 gopls 的0.2.0 版本和 Go 的 1.13.3 版本。(GCTT 注:目前 gopls 的版本做了大量的优化,个人感觉已经很好用了) + +## 模块缓存 + +为了帮助加快构建速度并快速更新项目中的依赖关系更改,Go 维护一个缓存,其中包含它在本地计算机上下载的所有模块。该缓存可以在 `$GOPATH/pkg` 中找到。如果没有 GOPATH 设置,默认的 GOPATH 是 `$HOME/go`。 + +注意: 有一个提案建议提供一个环境变量,允许用户控制模块缓存的位置。如果没有更改,`$GOPATH/pkg` 将是默认值。(GCTT 注:现在已经有了这个环境变量:GOMODCACHE) + +清单 1: + +```bash +$HOME/code/go/pkg +$ ls -l +total 0 +drwxr-xr-x 11 bill staff 352 Oct 16 15:53 mod +drwxr-xr-x 3 bill staff 96 Oct 3 16:49 sumdb +``` + +清单 1 显示了我当前 $GOPATH/pkg 文件夹的样子。你可以看到有两个文件夹,mod 和 sumdb。如果你查看 mod 文件夹内部,你可以了解更多关于模块缓存布局的信息。 + +清单 2: + +```bash +$HOME/code/go/pkg +$ ls -l mod/ +total 0 +drwxr-xr-x 5 bill staff 160 Oct 7 10:37 cache +drwxr-xr-x 3 bill staff 96 Oct 3 16:55 contrib.go.opencensus.io +drwxr-xr-x 40 bill staff 1280 Oct 16 15:53 github.com +dr-x------ 26 bill staff 832 Oct 3 16:50 go.opencensus.io@v0.22.1 +drwxr-xr-x 3 bill staff 96 Oct 3 16:56 golang.org +drwxr-xr-x 4 bill staff 128 Oct 7 10:37 google.golang.org +drwxr-xr-x 7 bill staff 224 Oct 16 15:53 gopkg.in +drwxr-xr-x 7 bill staff 224 Oct 16 15:53 k8s.io +drwxr-xr-x 5 bill staff 160 Oct 16 15:53 sigs.k8s.io +``` + +清单 2 显示了当前模块缓存的顶级结构。你可以看到如何将与模块名称关联的 URL 的第一部分用作模块缓存中的顶级文件夹。如果我导航到 github.com/ardanlabs,可以向你展示 2 个实际的模块。 + +清单 3: + +```bash +$HOME/code/go/pkg +$ ls -l mod/github.com/ardanlabs/ +total 0 +dr-x------ 13 bill staff 416 Oct 3 16:49 conf@v1.1.0 +dr-x------ 18 bill staff 576 Oct 12 10:08 service@v0.0.0-20191008203700-49ed4b4f1088 +``` + +清单 3 显示了我正在使用的来自 ArdanLabs 的两个模块及其版本。第一个是 conf 模块,另一个模块与我用来讲解 kubernetes 和服务的服务项目相关联。 + +Gopls 服务器还维护一个保存在内存中的模块缓存。在启动 VSCode 并处于模块模式时,将启动一个 gopls 服务器来支持该编辑器会话。内部 gopls 模块缓存与当前在磁盘上的内容同步。Gopls 使用这个内部模块缓存来处理编辑器请求。 + +在这篇文章中,我将在开始之前清空模块缓存,这样我就有了一个干净的工作环境。我还将在启动 VSCode 编辑器之前设置我的项目。这将允许我向你展示如何处理你需要的模块尚未下载到本地模块缓存或更新到 gopls 内部模块缓存的情况。 + +注意: 在任何正常的工作流中,您都不应该清除模块缓存。 + +清单 4: + +```bash +$ go clean -modcache +``` + +清单 4 显示了如何清除磁盘上的本地模块缓存。清理命令通常用于清理本地的 GOPATH 工作目录和 GOPATH/bin 文件夹。现在使用新的 -mocache 标志,可以使用该命令清理模块缓存。 + +注意: 这个命令不会清除任何正在运行的 gopls 实例的内部缓存。 + +## 新项目 + +我将在 GOPATH 之外开始一个新项目,在编写代码的过程中,我将介绍添加和删除依赖项的基本工作流程。 + +清单 5: + +```bash +$ cd $HOME +$ mkdir service +$ cd service +$ mkdir cmd +$ mkdir cmd/sales-api +$ touch cmd/sales-api/main.go +``` + +清单 5 显示了一些命令,这些命令用于设置工作目录文件、创建初始项目结构并添加 main.go 文件。 + +使用模块时的第一步是初始化项目源树的根。这是通过使用 go mod init 命令完成的。 + +清单 6: + +```bash +$ go mod init github.com/ardanlabs/service +``` + +清单 6 显示了对 go mod init 的调用,将模块的名称作为参数传递。正如在[第一篇文章](https://studygolang.com/articles/24580)中所讨论的,模块的名称允许在模块内部解析内部导入。按照仓库代码的 URL 来命名模块是惯用法。在这篇文章中,我假设这个模块与 Github 中 Ardan Labs 下的 [service repo](https://github.com/ardanlabs/service) 相关联。 + +一旦调用 go mod init 完成,就会在当前工作目录中创建一个 go.mod 文件。这个文件将表示项目的根。 + +清单 7: + +```bash +01 module github.com/ardanlabs/service +02 +03 go 1.13 +``` + +清单 7 显示了这个项目的初始模块文件的内容。有了这些,就可以进行项目编码了。 + +清单 8: + +```bash +$ code . +``` + +清单 8 显示了启动 VSCode 实例的命令。这将反过来启动 gopls 服务器的实例,以支持这个编辑器实例。 + +![图1](https://www.ardanlabs.com/images/goinggo/110_figure1.png) + +图 1 显示了在运行所有命令之后,我的 VSCode 编辑器中的项目是什么样子的。为了确保你使用的设置与我相同,我将列出我当前的 VSCode 设置。 + +清单 9: + +```json +{ + // Important Settings + "go.lintTool": "golint", + "go.goroot": "/usr/local/go", + "go.gopath": "/Users/bill/code/go", + + "go.useLanguageServer": true, + "[go]": { + "editor.snippetSuggestions": "none", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + } + }, + "gopls": { + "usePlaceholders": true, // add parameter placeholders when completing a function + "completeUnimported": true, // autocomplete unimported packages + "deepCompletion": true, // enable deep completion + }, + "go.languageServerFlags": [ + "-rpc.trace", // for more detailed debug logging + ], +} +``` + +清单 9 显示了我当前的 VSCode 设置。如果你一直跟着做,没有看到相同的行为,检查你的这些设置。如果你想查看当前推荐的 VSCode 设置,请点击[这里](https://github.com/golang/tools/blob/master/gopls/doc/vscode.md)。 + +## 应用编码 + +我将从这个应用程序的初始代码开始。 + +清单 10: + +```go +01 package main +02 +03 func main() { +04 if err := run(); err != nil { +05 log.Println("error :", err) +06 os.Exit(1) +07 } +08 } +09 +10 func run() error { +11 return nil +12 } +``` + +清单 10 显示了我添加到 main.go 的前 12 行代码。它为应用程序设置一个单一的退出点,并记录启动或关闭时的任何错误。一旦这 12 行代码被保存到文件中,编辑器将自动地(感谢 gopls)包含从标准库中所需的导入。 + +清单 11: + +```go +03 import ( +04 "log" +05 "os" +06 ) +``` + +清单 11 显示了由于编辑器与 gopls 集成而对第 03 至 06 行的源代码所做的更改。 + +接下来,我将添加对配置的支持。 + +清单 12: + +```go +17 func run() error { +18 var cfg struct { +19 Web struct { +20 APIHost string `conf:"default:0.0.0.0:3000"` +21 DebugHost string `conf:"default:0.0.0.0:4000"` +22 ReadTimeout time.Duration `conf:"default:5s"` +23 WriteTimeout time.Duration `conf:"default:5s"` +24 ShutdownTimeout time.Duration `conf:"default:5s"` +25 } +26 } +27 +28 if err := conf.Parse(os.Args[1:], "SALES", &cfg); err != nil { +29 return fmt.Errorf("parsing config : %w", err) +30 } +``` + +清单 12 显示了添加到第 18 行到第 30 行的 run 函数中以支持配置的代码。当这段代码被添加到源文件中并点击保存时,编辑器会将 fmt 和 time 包正确地包含到导入集中。不幸的是,由于 gopls 目前在其内部模块缓存中没有关于 conf 包的任何信息,所以 gopls 不能指示编辑器为 conf 添加一个导入或向编辑器提供包信息。 + +![图2](https://www.ardanlabs.com/images/goinggo/110_figure2.png) + +图 2 显示了编辑器如何清楚地表明它不能解析与 conf 包相关的任何信息。 + +## 添加一个依赖项 + +为了解析导入,需要检索包含 conf 包的模块。这样做的一种方法是将导入添加到源代码文件的顶部,并让编辑器和 gopls 完成这项工作。 + +清单 13: + +```go +01 package main +02 +03 import ( +04 "fmt" +05 "log" +06 "os" +07 "time" +08 +09 "github.com/ardanlabs/conf" +10 ) +``` + +在清单 13 中,我在第 09 行添加了 conf 包的导入。一旦我点击保存,编辑器就会找到 gopls,然后 gopls 会找到、下载并使用 Go 命令和相关的 API 提取这个包的模块。这些调用还更新 Go 模块文件以反映这一更改。 + +清单 14: + +```bash +~/code/go/pkg/mod/github.com/ardanlabs +$ ls -l +total 0 +drwxr-xr-x 3 bill staff 96B Nov 8 16:02 . +drwxr-xr-x 3 bill staff 96B Nov 8 16:02 .. +dr-x------ 13 bill staff 416B Nov 8 16:02 conf@v1.2.0 +``` + +清单 14 显示了 Go 命令如何完成它的工作,以及如何使用版本 1.2.0 下载 conf 模块。我们需要解析导入的代码现在在我的本地模块缓存中。 + +![图3](https://www.ardanlabs.com/images/goinggo/110_figure3.png) + +图 3 显示了编辑器仍然不能解析有关包的信息。为什么编辑器无法解析此信息?不幸的是,gopls 内部模块缓存与本地模块缓存不同步。Gopls 服务器并不知道 Go 命令刚刚做出的更改。由于 gopls 使用它的内部缓存,所以 gopls 不能向编辑器提供它所需要的信息。 + +注意: 这个缺点目前正在处理中,将在即将发布的版本中修正。你可以在这里追踪这个问题。()。(GCTT 译注:目前版本该问题已经解决了) + +使 gopls 内部模块缓存与本地模块缓存同步的一个快速方法是重新加载 VS Code 编辑器。这将重新启动 gopls 服务器并重置其内部模块缓存。在 VSCode 中,有一个名为 reload window 的特殊命令可以做到这一点。(新版本不需要此步骤了) + +```bash +Ctrl + Shift + P and run > Reload Window +``` + +![图4](https://www.ardanlabs.com/images/goinggo/110_figure4.png) + +图 4 显示了在使用 Ctrl + Shift + P 快捷键 reload 窗口之后在 VS Code 中出现的对话框。 + +运行此快速命令后,将解析与导入相关的任何消息。 + +## 可传递依赖关系 + +从 Go 工具的角度来看,构建这个应用程序所需的所有代码现在都在本地模块缓存中。但是,conf 包的测试依赖于 googlego-cmp 包。 + +清单 15: + +```bash +module github.com/ardanlabs/conf + +go 1.13 + +require github.com/google/go-cmp v0.3.1 +``` + +清单 15 显示了 conf 模块的 1.2.0 版本的模块文件。您可以看到 conf 依赖于 go-cmp 的 0.3.1 版本。此模块未列入服务的模块文件中,因为这样做是冗余的。Go 工具可以按照模块文件的路径来获取构建或测试代码所需的所有模块的完整图像。 + +此时,还没有找到这个传递模块,也没有将其下载并提取到本地模块缓存中。因为在构建代码时不需要这个模块,所以 Go 构建工具还没有发现需要下载它。如果我在命令行上运行 go mod tidy,那么 Go 工具将花费时间将 go-cmp 模块放入本地缓存中。 + +清单 16: + +```bash +$ go mod tidy +go: downloading github.com/google/go-cmp v0.3.1 +go: extracting github.com/google/go-cmp v0.3.1 +``` + +清单 16 显示了如何找到、下载和提取 go-cmp 模块。这个调用 go mod tidy 不会改变项目的模块文件,因为这不是一个直接的依赖项。它将更新 go.sum 文件,以便有模块 hash 的记录,从而维护持久的、可重复的构建。我将在以后的文章中谈论校验和数据库。 + +清单 17: + +```bash +github.com/ardanlabs/conf v1.2.0 h1:2IntiqlEhRk+sYUbc8QAAZdZlpBWIzNoqILQvV6Jofo= +github.com/ardanlabs/conf v1.2.0/go.mod h1:ILsMo9dMqYzCxDjDXTiwMI0IgxOJd0MOiucbQY2wlJw= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +``` + +清单 17 显示了运行 go mod tidy 后校验和文件的样子。每个与项目相关联的模块有两条记录。 + +## 下载模块 + +如果你还没有准备好在代码库中使用某个特定的模块,但是希望将该模块下载到本地模块缓存中,可以选择手动将该模块添加到项目 go.mod 文件中,然后在编辑器外运行 go mod tidy。 + +清单 18: + +```bash +01 module github.com/ardanlabs/service +02 +03 go 1.13 +04 +05 require ( +06 github.com/ardanlabs/conf v1.2.0 +07 github.com/pkg/errors latest +08 ) +``` + +在清单 18 中,你可以看到我如何为最新版本的 errors 模块在模块文件中手动添加第 07 行。手动添加所需模块的重要部分是使用最新的标记。一旦我对这个更改运行 go mod tidy,它会告诉 Go 找到 errors 模块的最新版本并将其下载到缓存中。 + +清单 19: + +```bash +$HOME/service +$ go mod tidy +go: finding github.com/pkg/errors v0.8.1 +``` + +清单 19 显示了如何找到、下载和提取 errors 模块的 0.8.1 版本。一旦命令运行完毕,模块将从模块文件中删除,因为项目不使用该模块。但是,该模块列在校验和文件中。 + +清单 20: + +```bash +github.com/ardanlabs/conf v1.2.0 h1:2IntiqlEhRk+sYUbc8QAAZdZlpBWIzNoqILQvV6Jofo= +github.com/ardanlabs/conf v1.2.0/go.mod h1:ILsMo9dMqYzCxDjDXTiwMI0IgxOJd0MOiucbQY2wlJw= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +``` + +清单 20 显示了如何在校验和文件中列出 errors 模块模块文件的散列。记住校验和文件不是项目使用的所有依赖项的规范记录,这一点很重要。它可以包含更多的模块,这是绝对好的。 + +我喜欢这种通过使用 go get 来下载新模块的方法,因为如果不小心的话,go get 也可以尝试升级项目的依赖关系图(直接和间接)。重要的是要知道什么时候版本升级只是下载你想要的新模块。在以后的文章中,我将讨论使用 go get 来更新现有的模块依赖关系。 + +## 移除依赖 + +如果我决定不再使用 conf 包会发生什么?我可以删除使用包的任何代码。 + +清单 21: + +```go +01 package main +02 +03 import ( +04 "log" +05 "os" +06 ) +07 +08 func main() { +09 if err := run(); err != nil { +10 log.Println("error :", err) +11 os.Exit(1) +12 } +13 } +14 +15 func run() error { +16 return nil +17 } +``` + +清单 21 显示了从 main 函数中删除引用 conf 包的代码。一旦我点击保存,编辑器就会从导入集中删除 conf 的导入。但是,模块文件没有更新以反映更改。 + +清单 22: + +```bash +01 module github.com/ardanlabs/service +02 +03 go 1.13 +04 +05 require github.com/ardanlabs/conf v1.1.0 +``` + +清单 22 显示 conf 包仍然被认为是必需的。为了解决这个问题,我需要离开编辑器,再次运行 go mod tidy。 + +清单 23: + +```bash +$HOME/service +$ go mod tidy +``` + +清单 23 再次显示了 go mod 的运行情况。这次没有输出。一旦这个命令完成,模块文件再次精确。 + +清单 24: + +```bash +$HOME/services/go.mod + +01 module github.com/ardanlabs/service +02 +03 go 1.13 +``` + +清单 24 显示了从模块文件中删除了 conf 模块。这一次,go mod tidy 命令清除了校验和文件,它将是空的。在你对 VCS 进行任何修改之前,确保你的模块文件是正确的,并且与你使用的依赖关系一致,这一点很重要。 + +## 总结 + +在不久的将来,我分享的一些解决方案,比如重新加载窗口,将不再需要。团队意识到了这一点以及当今存在的其他缺陷,他们正在积极地修复这些缺陷。他们非常感谢任何和所有的反馈,所以如果你发现一个问题,请报告它。没有问题是太大或太小。作为一个社区,让我们与 Go 团队一起快速解决这些遗留问题。 + +现在正在进行的一个核心特性是 gopls 能够监视文件系统并自己查看项目更改。这将有助于 gopls 保持其内部模块缓存与磁盘上的本地模块缓存同步。一旦这样做了,重新装载窗口的需求就会消失。计划也在制定中,以提供视觉线索,工作是在背景中进行的。(GCTT 注:目前已经解决) + +总的来说,我对当前的工具集和刷新窗口的工作方式感到满意。我希望你考虑开始使用模块,如果你还没有。模块已经可以使用了,越多的项目开始使用它,Go 生态系统对每个人来说就越好。 + +--- + +via: https://www.ardanlabs.com/blog/2019/12/modules-02-projects-dependencies-gopls.html + +作者:[William Kennedy](https://www.ardanlabs.com/) +译者:[polaris1119](https://github.com/polaris1119) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/ardanlabs-modules/20191218-modules-03-minimal-version-selection.md b/published/tech/ardanlabs-modules/20191218-modules-03-minimal-version-selection.md new file mode 100644 index 000000000..cdffa22e6 --- /dev/null +++ b/published/tech/ardanlabs-modules/20191218-modules-03-minimal-version-selection.md @@ -0,0 +1,498 @@ +首发于:https://studygolang.com/articles/35210 + +# Go Module 教程第 3 部分:最小版本选择 + +前两个教程: + +- [Go Module 教程第 1 部分:为什么和做什么](https://studygolang.com/articles/24580) +- [Go Module 教程第 2 部分:项目、依赖和 gopls](https://studygolang.com/articles/35202) + +> 注意,该教程基于 Go1.13。最新版本可能会有所不同。 + +## 引言 + +每个依赖管理解决方案都必须解决选择依赖版本的问题。目前存在的许多版本选择算法都试图识别任何依赖关系的“最新最大”版本。如果你相信语义版本控制将被正确应用,社会契约将得到尊重,那么这是有意义的。在这些情况下,依赖项的“最新最大”版本应该是最稳定和安全的版本,并且应该与早期版本具有向后兼容性。至少在相同的主版本依赖关系树中应该如此。 + +Go 决定采取一种不同的方法,Russ Cox 花了大量的时间和精力[写作](https://research.swtch.com/vgo)和[讨论](https://www.youtube.com/watch?v=F8nrpe0XWRg) Go 团队的版本选择方法,这种方法被称为最小版本选择或 MVS。实质上,Go 团队相信 MVS 为 Go 程序提供了持久的、可重复的长期构建的最佳机会。我建议读一读[这篇文章](https://research.swtch.com/vgo-principles),理解为什么 Go 团队相信这一点。 + +在本文中,我将尽力解释 MVS 语义,并展示一个实际的 Go 示例和 MVS 算法。 + +## MVS 语义 + +命名 Go 的选择算法“最小版本选择”有点用词不当,但是一旦你了解了它的工作原理,你就会发现它的名字非常接近。正如我之前所说的,许多选择算法选择依赖项的“最新最大”版本。我喜欢把 MVS 看作是一种选择“最新的非最大”版本的算法。并不是 MVS 不能选择“最新的最大”,只是如果项目中的任何依赖项都不需要“最新的最大”,那么就不需要该版本。 + +为了更好地理解这一点,让我们创建这样一种情况: 几个模块(A、B 和 C)依赖于同一个模块(D) ,但每个模块需要不同的版本。 + +![图1](https://www.ardanlabs.com/images/goinggo/111_figure1.png) + +图 1 显示了模块 A、B 和 C 各自独立地需要模块 D,并且每个模块都需要不同版本的模块。 + +如果我启动一个需要模块 A 的项目,那么为了构建代码,我还需要模块 D。模块 D 有很多版本可供选择。例如,想象模块 D 是 sirupsen 中的 logrus 模块。我可以要求 Go 为我提供一个所有已经被标记为模块 D 的版本的列表。 + +**清单 1** + +```bash +$ go list -m -versions github.com/sirupsen/logrus + +github.com/sirupsen/logrus v0.1.0 v0.1.1 v0.2.0 +v0.3.0 v0.4.0 v0.4.1 v0.5.0 v0.5.1 v0.6.0 v0.6.1 +v0.6.2 v0.6.3 v0.6.4 v0.6.5 v0.6.6 v0.7.0 v0.7.1 +v0.7.2 v0.7.3 v0.8.0 v0.8.1 v0.8.2 v0.8.3 v0.8.4 +v0.8.5 v0.8.6 v0.8.7 v0.9.0 v0.10.0 v0.11.0 v0.11.1 +v0.11.2 v0.11.3 v0.11.4 v0.11.5 v1.0.0 v1.0.1 v1.0.3 +v1.0.4 v1.0.5 v1.0.6 v1.1.0 v1.1.1 v1.2.0 v1.3.0 +v1.4.0 v1.4.1 v1.4.2 +``` + +清单 1 显示了模块 D 的所有版本,其中显示了“最新最大”的版本为 v1.4.2。 + +应该为项目选择哪个版本的模块 D?实际上有两种选择。第一个选择是选择“最新最大”的版本(在这一行的主要版本 1 版本中) ,它将是版本 1.4.2。第二个选择是选择模块 A 需要的版本,即 v1.0.6 版本。 + +像 dep 这样的依赖工具会选择 v1.4.2 版本,并在语义版本控制和社会契约得到尊重的前提下工作。然而,根据 Russ 在[该文](https://research.swtch.com/vgo-principles)中定义的原因,Go 将尊重模块 A 的要求,选择 v1.0.6 版本。Go 为项目中需要该模块的所有依赖项选择当前在所需版本集中的“最小”版本。换句话说,现在只有模块 A 需要模块 D,而模块 A 已经指定它需要版本 v1.0.6,因此这将作为模块 D 的版本。 + +如果我引入新的代码,要求项目导入模块 B,会怎么样?一旦模块 B 被导入到项目中,Go 将该项目的模块 D 的版本从 v1.0.6 升级到 v1.2.0。再次为项目中需要模块 D 的所有依赖项(模块 A 和模块 B)选择模块 D 的“最小”版本,该版本目前位于所需版本集(v1.0.6 和 v1.2.0 )中。 + +如果我再次引入需要项目导入模块 C 的新代码会怎么样?然后 Go 将从所需的版本集(v1.0.6、 v1.2.0、 v1.3.2)中选择最新版本(v1.3.2)。请注意,v1.3.2 版本仍然是“最小”版本,而不是模块 D (v1.4.2)的“最新最大”版本。 + +最后,如果我删除刚刚为模块 C 添加的代码会怎样?Go 将把该项目锁定到模块 D 的版本 v1.3.2 中,降级回到版本 v1.2.0 将是一个更大的改变,而且 Go 知道版本 v1.3.2 工作正常且稳定,因此版本 v1.3.2 仍然是该项目模块 D 的“最新非最大”或“最小”版本。另外,模块文件只维护一个快照,而不是日志。没有关于历史撤销或降级的信息。 + +这就是为什么我喜欢将 MVS 看作是一种选择模块的“最新非最大”版本的算法。希望您现在明白为什么 Russ 在命名算法时选择 “minimal”这个名称。 + +## 示例项目 + +有了这个基础,我将以上放在一个项目里,这样你就可以看到 Go 和 MVS 算法起的作用。在这个项目中,模块 D 将表示 logrus 模块,该项目直接依赖 [rethinkdb-go](https://github.com/rethinkdb/rethinkdb-go) (模块 A)和 [golib](https://github.com/Bhinneka/golib) (模块 B)模块。Rethinkdb-go 和 golib 模块直接依赖 logrus 模块,并且每个模块都需要不同的版本,而不是 logrus 的“最新最大”版本。 + +![图2](https://www.ardanlabs.com/images/goinggo/111_figure2.png) + +图 2 显示了这三个模块之间的独立关系。首先,我将创建项目,初始化模块,然后启动 VSCode。 + +**清单 2** + +```bash +$ cd $HOME +$ mkdir app +$ mkdir app/cmd +$ mkdir app/cmd/db +$ touch app/cmd/db/main.go +$ cd app +$ go mod init app +$ code . +``` + +清单 2 显示了要运行的所有命令。 + +![图3](https://www.ardanlabs.com/images/goinggo/111_figure3.png) + +图 3 显示了项目结构和模块文件应该包含的内容。现在可以添加使用 rethinkdb-go 模块的代码了。 + +**清单 3**: + +```go +package main + +import ( + "context" + "log" + + db "gopkg.in/rethinkdb/rethinkdb-go.v5" +) + +func main() { + c, err := db.NewCluster([]db.Host{{Name: "localhost", Port: 3000}}, nil) + if err != nil { + log.Fatalln(err) + } + + if _, err = c.Query(context.Background(), db.Query{}); err != nil { + log.Fatalln(err) + } +} +``` + +清单 3 引入了 rethinkdb-go 模块的主版本 5。添加并保存这段代码之后,Go 查找、下载并提取模块,更新 go.mod 和 go.sum 文件。 + +**清单 4** + +```bash +module app + +go 1.13 + +require gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.1 +``` + +清单 4 显示了 go.mod 文件,该文件要求 rethinkdb-go 模块作为一个直接依赖项,选择 v5.0.1版本,这是该模块的“最新最大”版本。 + +**清单 5** + +```bash +... +github.com/sirupsen/logrus v1.0.6 h1:hcP1GmhGigz/O7h1WVUM5KklBp1JoNS9FggWKdj/j3s= +github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +... +``` + +清单 5 显示了 go.sum 文件中的两行代码,它们是 logrus 模块的 v1.0.6 版本。此时,你可以看到 MVS 算法已经选择了 logrus 模块的“最小”版本,以满足 rethinkdb-go 模块所指定的需求。记住 logrus 模块的最新版本是 1.4.2。 + +注意:go.sum 文件应该被视为不透明的可靠性物件,不应该用它来理解您的依赖关系。我在上面所做的确定版本是错误的,不久我将向您展示确定你的项目使用什么版本的正确方法。 + +![图4](https://www.ardanlabs.com/images/goinggo/111_figure4.png) + +图 4 显示了将使用哪个版本的 logrus 模块 Go 来构建项目中的代码。 + +接下来,我将添加引入 golib 模块依赖项的代码。 + +**清单 6**: + +```go +package main + +import ( + "context" + "log" + + "github.com/Bhinneka/golib" + db "gopkg.in/rethinkdb/rethinkdb-go.v5" +) + +func main() { + c, err := db.NewCluster([]db.Host{{Name: "localhost", Port: 3000}}, nil) + if err != nil { + log.Fatalln(err) + } + + if _, err = c.Query(context.Background(), db.Query{}); err != nil { + log.Fatalln(err) + } + + golib.CreateDBConnection("") +} +``` + +清单 6 为程序添加了第 07 行和第 21 行。一旦 Go 查找、下载并提取 golib 模块,go.mod 文件中将显示以下更改。 + +**清单 7** + +```bash +module app + +go 1.13 + +require ( + github.com/Bhinneka/golib v0.0.0-20191209103129-1dc569916cba + gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.1 +) +``` + +清单 7 显示了 go.mod 文件已经被修改,以包含 golib 模块对该模块的“最新最大”版本的依赖关系,该模块没有语义版本标记。 + +**清单 8** + +```bash +... +github.com/sirupsen/logrus v1.0.6 h1:hcP1GmhGigz/O7h1WVUM5KklBp1JoNS9FggWKdj/j3s= +github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +... +``` + +清单 8 显示了 go.sum 文件中的四行代码,它们现在包含 logrus 模块的 v1.0.6 和 v1.2.0 版本。看到 go.sum 文件中列出的两个版本,就会产生两个疑问: + +1. 为什么两个版本都在 `go.sum` 文件中? +2. 当 Go 执行构建时,会使用哪个版本? + +两个版本都在 go.sum 文件中列出的原因,Go 团队的 Bryan Mills 回答得更好。 + +”go.sum 文件仍然包含旧版本(1.0.6) ,因为它的传递需求可能会影响其他模块的选定版本。我们实际上只需要 go.mod 文件的校验和,因为它声明了那些可传递的需求,但我们最终还是保留了源代码的校验和,因为 go mod tidy 并不像它应该的那样精确。” +$ go mod tidy +``` + +清单 22 显示了可以运行的命令,以允许 MVS 从头再次执行所有选择。在写这篇文章的整个过程中,我一直在这样做,以便重新设置项目并提供文章的列表。 + +## 总结 + +在这篇文章中,我解释了 MVS 语义,并展示了 Go 和 MVS 算法的实际应用示例。我还展示了一些 Go 命令,它们可以在你遇到困难或遇到未知问题时为你提供信息。在向项目添加越来越多的依赖项时,可能会遇到一些边缘情况。这是因为 Go 生态系统已经有 10 年的历史了,所有现有的项目都需要更多的时间才能达到模块兼容。 + +在以后的文章中,我将讨论在同一个项目中使用不同主要版本的依赖关系,以及如何手动检索和锁定依赖关系的特定版本。现在,我希望您能够更多地信任模块和 Go 工具,并且你能够更清楚地了解 MVS 如何随着时间的推移选择版本。 + + +--- + +via: + +作者:[William Kennedy](https://www.ardanlabs.com/) +译者:[polaris1119](https://github.com/polaris1119) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/ardanlabs-modules/20200210-modules-04-mirros-checksums-athens.md b/published/tech/ardanlabs-modules/20200210-modules-04-mirros-checksums-athens.md new file mode 100644 index 000000000..d83e2cf17 --- /dev/null +++ b/published/tech/ardanlabs-modules/20200210-modules-04-mirros-checksums-athens.md @@ -0,0 +1,386 @@ +首发于:https://studygolang.com/articles/35225 + +# Go Module 教程第 4 部分:镜像、校验和以及 Athens + +前三个教程: + +- [Go Module 教程第 1 部分:为什么和做什么](https://studygolang.com/articles/24580) +- [Go Module 教程第 2 部分:项目、依赖和 gopls](https://studygolang.com/articles/35202) +- [Go Module 教程第 3 部分:最小版本选择](https://studygolang.com/articles/35210) + +> 注意,该教程基于 Go1.13。最新版本可能会有所不同。 + +## 引言 + +当我第一次学习模块时遇到的一个长期问题是模块镜像、校验和数据库以及 Athens 是如何工作的。Go 团队已经写了大量关于模块镜像和校验和数据库的内容,但我希望在这里合并最重要的信息。在这篇文章中,我提供了这些系统的用途,你可以控制的不同的配置选项,并用示例程序展示了这些系统的运行情况。 + +## 模块镜像(Mirror) + +[模块镜像](https://blog.golang.org/module-mirror-launch)是 2019 年 8 月发布的,是 Go 版本 1.13 中用于抓取模块的默认系统。模块镜像是作为一个代理服务器实现的,它面向 VCS 环境,帮助加速获取构建应用程序所需的本地模块。代理服务器实现了一个基于 REST 的 API,并且是围绕 Go 工具的需求而设计的。 + +模块镜像缓存模块及其被请求的特定版本,这样可以更快地检索未来的请求。一旦获取代码并将其缓存到模块镜像中,就可以快速地将其提供给世界各地的客户机。模块镜像还允许用户继续获取原始 VCS 位置不再可用的源代码。这可以防止像 Node 开发者在 2016 年[遇到的问题](https://www.theregister.co.uk/2016/03/23/npm_left_pad_chaos/)。 + +## 校验和数据库 + +[校验和数据库](https://go.googlesource.com/proposal/+/master/design/25530-sumdb.md)也于 2019 年 8 月推出,是一个防篡改的模块散列码日志,可用于验证不可信代理或来源。校验和数据库是作为服务实现的,Go 工具使用它来验证模块。它验证了特定版本的任何给定模块的代码是否是相同的,不管是谁、是什么、在哪里以及如何获取。它还解决了其他依赖管理系统尚未解决的其他安全问题(如上面的链接所述)。Google 拥有现存唯一的校验和数据库,但是它可以被私有的模块镜像缓存。 + +## 模块索引 + +该[索引服务](https://index.golang.org/)是为那些希望跟踪添加到 Google 模块镜像的新模块的模块列表的开发人员提供的。像 [pkg.go.dev](https://pkg.go.dev/) 这样的网站使用索引来检索和发布模块的信息。 + +## 你的隐私 + +正如[隐私政策](https://sum.golang.org/privacy)中记录的那样,Go 团队构建这些服务是为了尽可能少地保留关于使用情况的信息,同时仍然确保它们能够检测和修复问题。然而,像 IP 地址这样的个人身份信息可以保存 30 天。如果这对您或您的公司是一个问题,您可能不希望使用 Go 团队的服务来获取和验证模块。 + +## Athens + +[Athens](https://docs.gomods.io/)是一个模块镜像,你可以搭建你的私人环境。使用私有模块镜像的一个原因是允许缓存公共模块镜像无法访问的私有模块。最棒的是 Athens 项目提供了一个 Docker 容器,发布在 Docker Hub 上,所以不需要特殊安装。 + +> GCTT 注:还有 goproxy.io 和 goproxy.cn,这两个都是国人开发的 + +**清单 1** + +```bash +docker run -p '3000:3000' gomods/athens:latest +``` + +清单 1 显示了如何使用 Docker 运行本地 Athens 服务器。稍后我们将使用这个工具来查看 Go 工具的运行情况,并监视所有 Go 工具 Web 调用的 Athens 日志。要知道,Athens Docker 映像默认启动了临时磁盘存储,所以当你关闭运行的容器时,所有内容都将被清空。 + +Athens 有能[代理](https://docs.gomods.io/configuration/sumdb)校验和数据库。当 Go 工具被配置为使用一个像 Athens 一样的私有模块镜像时,当需要从校验和数据库中查找散列码时,Go 工具将尝试使用相同的私有模块镜像。如果你正在使用的私有模块镜像不支持代理校验和数据库,那么将直接访问校验和数据库,除非它被手动关闭。 + +**清单 2** + +```bash +http://localhost:3000/sumdb/sum.golang.org/latest + +go.sum database tree +756113 +k9nFMBuXq8uk+9SQNxs/Vadri2XDkaoo96u4uMa0qE0= + +— sum.golang.org Az3grgIHxiDLRpsKUElIX5vJMlFS79SqfQDSgHQmON922lNdJ5zxF8SSPPcah3jhIkpG8LSKNaWXiy7IldOSDCt4Pwk= +``` + +清单 2 显示了 Athens 如何成为校验和数据库代理。第一行中列出的 URL 要求本地运行的 Athens 服务从校验和数据库中检索有关最新签名树的信息。你可以了解为什么 GOSUMDB 配置为名称而不是 URL。 + +## 环境变量 + +有几个环境变量控制 Go 工具的行为,因为它与模块镜像和校验和数据库相关。需要在每个开发人员或构建环境的计算机级别上设置这些变量。 + +**GOPROXY**:一组指向模块镜像的 URL,用于抓取模块。如果你希望 Go 工具只从 VCS 位置直接获取模块,那么可以将其设置为 direct。如果你将此设置为 off,那么模块将不会被下载。使用 off 可以用在保留 vendoring 或模块缓存的构建环境中。 + +**GOSUMDB**:用于验证给定模块/版本的代码的校验和数据库的名称没有随时间变化。此名称用于形成一个正确的 URL,该 URL 告诉 Go 工具在哪里执行这些校验和数据库查找。这个 URL 可以指向 Google 拥有的校验和数据库,或者指向支持缓存或代理校验和数据库的本地模块镜像。如果你不希望 Go 工具验证添加到 go.sum 文件中的给定模块/版本的哈希代码,也可以将其设置为 off。只有在向 go.sum 文件添加任何新的 go.sum 行之前,才会查询校验和数据库。 + +**GONOPROXY**:模块的一组基于 URL 的模块路径,不应该使用模块镜像获取,而是直接从 VCS 位置获取。 + +**GOPRIVATE**:一个便捷的变量,用于设置具有相同默认值的 GONOPROXY 和 GONOSUMDB。 + +> GCTT 注:这些环境变量的帮助文档可以通过 go help environment 获得 + +## 隐私语义学(Privacy Semantics) + +在考虑隐私和项目所依赖的模块时,需要考虑以下几点。特别是那些你不想让别人知道的私人模块。下面的图表尝试提供隐私选项。同样,需要在每个开发人员或构建环境的计算机级别上设置此配置。 + +**清单 3** + +```bash +Option : Fetch New Modules : Validate New Checksums +----------------------------------------------------------------------------------------- +Complete Privacy : GOPROXY="direct" : GOSUMDB="off" +Internal Privacy : GOPROXY="Private_URL" : GOSUMDB="sum.golang.org" + GONOSUMDB="github.com/mycompany/*,gitlab.com/*" +No Privacy : GOPROXY="Public_URL" : GOSUMDB="sum.golang.org" +``` + +**Complete Privacy**:代码直接从 VCS 服务器获取,没有生成并添加到 go.sum 文件的哈希代码,并且它们也不会从校验和数据库中查找。 + +**Internal Privacy**:代码是通过一个像 [Athens](https://docs.gomods.io/) 这样的私有模块镜像获取的,并且不会在校验和数据库中查找生成和添加到 go.sum 文件中的,GONOSUMDB 下列出的指定 URL 路径的哈希代码。如果需要,将在校验和数据库中查找不属于 GONOSUMDB 中列出的路径的模块。 + +**No Privacy**:代码是通过像 [Google](https://proxy.golang.org/) 或 [Goproxy.CN](https://goproxy.cn/) 公共服务器这样的公共模块镜像获取的。在这种情况下,你所依赖的所有模块都需要是公共模块,并可由你选择的公共模块进行访问。这些公共模块镜像将记录你的请求和其中包含的详细信息。访问谷歌拥有的校验和数据库也将被记录。所记录的信息受各自的隐私策略控制。 + +从来没有理由在校验和数据库中查找私有模块的哈希代码,因为校验和数据库中永远不会有这些模块的列表。公共模块镜像不能访问私有模块,因此不能生成和存储哈希代码。对于私有模块,你需要依靠内部策略和实践来保持给定模块/版本的代码一致。但是,如果私有模块/版本的代码确实发生了更改,那么当第一次在新机器上获取并缓存模块/版本时,Go 工具仍然可以发现差异。 + +任何时候,当模块/版本被添加到机器上的本地缓存中,并且 go.sum 文件中已经有一个条目时,go.sum 文件中的哈希码都会与刚才在缓存中获取的内容进行比较。如果哈希代码不匹配,就说明发生了变化。这个工作流程最好的部分是不需要校验和数据库查找,因此任何给定版本的私有模块仍然可以在不损失隐私的情况下进行验证。显然,这完全取决于你第一次获取私有模块/版本的时间,这对于存储在校验和数据库中的公共模块/版本来说也是同样的问题。 + +当使用 Athens 作为模块镜像,需要考虑 Athens 配置选项。 + +**清单 4** + +```bash +GlobalEndpoint = "https://" +NoSumPatterns = ["github.com/mycompany/*] +``` + +清单 4 中的这些设置来自 Athens 的文档,它们很重要。默认情况下,Athens 将直接从不同的 VCS 服务器获取模块。这将为你的环境保持最高级别的隐私。但是,可以通过将 GlobalEnpoint 设置为该模块镜像的 URL,将 Athens 指向另一个模块镜像。这将使你在获取新的公共模块时获得更好的性能,但是你将失去隐私。 + +另一个设置称为 NoSumPatterns,它有助于验证开发人员和构建环境的正确配置。开发人员向 GONOSUMDB 添加的相同路径集应该添加到 NoSumPatterns 中。当检查和数据库请求访问 Athens 以获取与路径匹配的模块时,它将返回一个状态代码,该状态代码将导致 Go 工具失败。这表明开发人员的设置是错误的。换句话说,如果机器配置正确,那么这个请求从一开始就不应该到达 Athens 。 + +## Vendoring + +我相信每个项目都应该提供他们的依赖关系,或者认为这样做不合理或者不切实际。像 Docker 和 Kubernetes 这样的项目不能提供他们的依赖项,因为依赖项太多了。然而,对于我们大多数人来说,情况并非如此。在 v1.14 版本中,对 vendoring 和模块有很好的支持。我将在另一篇文章中讨论这个问题。 + +我提到 vendoring 有一个重要的原因。我听说有人用 Athens 或者私有模块镜像代替 vendoring。我认为这是个错误。这两者没有任何关系。您可以争辩模块镜像 vendoring 的依赖关系,因为模块的代码是持久化的,但是代码仍然远离依赖它的项目。即使你相信你的模块镜像的弹性,我也相信没有什么可以替代你的项目拥有它所需要的所有源代码,除了项目本身来构建代码之外不依赖其他任何东西。 + +## 工具的使用 + +有了所有这些背景和知识,是时候看看 Go 工具是如何工作的了。为了了解环境变量如何影响 Go 工具,我将运行几个不同的场景。在开始之前,可以通过运行 go env 命令来了解默认值。 + +**清单 5**: + +```bash +$ go env +GONOPROXY="" +GONOSUMDB="" +GOPRIVATE="" +GOPROXY="https://proxy.golang.org,direct" +GOSUMDB="sum.golang.org" +``` + +清单 5 显示了告诉 Go 工具使用 Google 模块镜像和 Google 校验和数据库的默认值。如果你需要的所有代码都可以通过这些 Google 服务访问,那么这是推荐的配置。如果 Google 模块镜像碰巧响应了410(已消失)或404(未找到),那么使用 direct(这是 GOPROXY 配置的一部分)将允许 Go 工具改变方向并直接从 VCS 位置获取模块/版本。任何其他状态代码(比如 500)都会导致 Go 工具失败。 + +如果 Google 模块镜像碰巧对给定模块/版本响应了 410 或 404,那是因为它不在缓存中,可能不能缓存,而私有模块就是这种情况。在这种情况下,校验和数据库中很可能也没有列表。即使 Go 工具可以成功地直接获取模块/版本,但是查找将会失败,而 Go 工具仍然会失败。使用私有模块时需要注意的一些事项。 + +因为我不能显示任何来自 Google 模块镜像的日志,所以我将使用 Athens 运行一个本地模块镜像。这将允许你看到 Go 工具和模块镜像在运行。最后,Athens 实现了相同的语义和工作流。 + +## 项目 + +要创建项目,请启动终端会话并创建项目结构。 + +**清单 6** + +```bash +$ cd $HOME +$ mkdir app +$ mkdir app/cmd +$ mkdir app/cmd/db +$ touch app/cmd/db/main.go +$ cd app +$ go mod init app +$ code . +``` + +清单 6 显示了为在磁盘上创建项目结构、为模块初始化项目和运行 VSCode 而运行的命令。 + +**清单 7** + + + +```go +package main + +import ( + "context" + "log" + + "github.com/Bhinneka/golib" + db "gopkg.in/rethinkdb/rethinkdb-go.v5" +) + +func main() { + c, err := db.NewCluster([]db.Host{{Name: "localhost", Port: 3000}}, nil) + if err != nil { + log.Fatalln(err) + } + + if _, err = c.Query(context.Background(), db.Query{}); err != nil { + log.Fatalln(err) + } + + golib.CreateDBConnection("") +} +``` + +清单 7 显示了 main.go 的代码。随着项目的设置和主要功能的到位,我将在项目中运行三个不同的场景,以更好地理解环境变量和 Go 工具。 + +### 场景 1:Athens 模块镜像 + +在这个场景中,我将使用 Athens 作为私有模块镜像替代 Google 模块镜像。 + +**清单 8** + +```bash +GONOSUMDB="" +GONOPROXY="" +GOSUMDB="sum.golang.org" +GOPROXY="http://localhost:3000,direct" +``` + +清单 8 显示,我要求 Go 工具对模块镜像使用在端口 3000 上本地运行的 Athens 服务。如果模块镜像以 410(已消失)或 404(未找到)响应,则尝试直接拉出模块。默认情况下,如果需要,Go 工具现在将使用 Athens 来访问校验和数据库。 + +接下来,为运行 Athens 启动一个新的终端会话。 + +**清单 9** + +```bash +$ docker run -p '3000:3000' -e ATHENS_LOG_LEVEL=debug -e GO_ENV=development gomods/athens:latest + +INFO[10:15AM]: Exporter not specified. Traces won't be exported +2021-09-05 10:15:08.464666 I | Starting application at port :3000 +``` + +清单 9 在第一行显示了在一个新的终端会话中运行的命令,该命令使用额外的调试日志记录启动并运行 Athens 服务。确保你本机启动了 Docker。一旦 Athens 启动,你应该会看到清单中的输出。 + +要查看 Go 工具使用 Athens 服务的情况,请在用于创建项目的原始终端会话中运行以下命令。 + +**清单 10** + +```bash +$ export GOPROXY="http://localhost:3000,direct" +$ rm go.* +$ go mod init app +$ go mod tidy +``` + +清单 10 显示了将 GOPROXY 变量设置为使用 Athens 服务、删除模块文件和重新初始化应用程序的命令。最后的命令 `go mod tidy` 将使 Go 工具与 Athens 服务通信,以获取构建这个项目所需的模块。 + +**清单 11** + +```bash +handler: GET /github.com/!bhinneka/@v/list [404] +handler: GET /github.com/@v/list [404] +handler: GET /github.com/!bhinneka/golib/@v/list [200] +handler: GET /gopkg.in/@v/list [404] +handler: GET /github.com/!bhinneka/golib/@latest [200] +handler: GET /gopkg.in/rethinkdb/rethinkdb-go.v5/@v/list [200] +handler: GET /github.com/bitly/@v/list [404] +handler: GET /github.com/bmizerany/@v/list [404] +handler: GET /github.com/bmizerany/assert/@v/list [200] +handler: GET /github.com/bitly/go-hostpool/@v/list [200] +handler: GET /github.com/bmizerany/assert/@latest [200] +``` + +清单 11 显示了来自 Athens Service 的重要输出。如果查看 go.mod 和 go.sum 文件,你将看到构建和验证项目所需的所有内容。 + +> GCTT 注:你看到的信息可能会有所不同,因为 Athens 可能升级了,日志输出方式变了 + +### 场景 2:Athens 模块镜像/直接从 GitHub Modules 获取 + +在这个场景中,我不希望从模块镜像获取任何托管在 GitHub 上的模块。我希望这些模块可以直接从 GitHub 获取。 + +**清单 12** + +```bash +$ export GONOPROXY="github.com" +$ export GOPROXY="http://localhost:3000,direct" +$ rm go.* +$ go mod init app +$ go mod tidy +``` + +清单 12 显示了在这个场景中如何设置 GONOPROXY 变量。现在 GONOPROXY 告诉 Go 工具直接获取任何名称以 github. com 开头的模块。不要使用 GOPROXY 变量定义的模块镜像。虽然我使用 GitHub 来展示这一点,但是如果你运行一个像 GitLab 这样的本地 VCS,这个配置是完美的。这将允许你直接获取私有模块。 + +**清单 13** + +```bash +handler: GET /gopkg.in/@v/list [404] +handler: GET /gopkg.in/rethinkdb/rethinkdb-go.v5/@v/list [200] +``` + +清单 13 显示了运行 go mod tidy 之后从 Athens Service 得到的更重要的输出。这次 Athens 只显示对位于 gopk.in 的两个模块的请求。位于 github. com 的模块不再需要 Athens 的服务。 + +### 场景 3:Module Mirror 404 + +在这个场景中,我将使用自己的模块镜像,它将为每个模块请求返回一个 404。当模块镜像返回 410(已消失)或 404(未找到)时,Go 工具将沿着 GOPROXY 变量中列出的逗号分隔的其他镜像集继续。 + +**清单 14** + + + +```go +package main + +import ( + "log" + "net/http" +) + +func main() { + h := func(w http.ResponseWriter, r *http.Request) { + log.Printf("%s %s -> %s\n", r.Method, r.URL.Path, r.RemoteAddr) + w.WriteHeader(http.StatusNotFound) + } + http.ListenAndServe(":3000", http.HandlerFunc(h)) +} +``` + +清单 14 显示了我的模块镜像的代码。它能够记录每个请求的跟踪并返回 `http.StatusNotFound`,即 404。 + +**清单 15** + +```bash +$ unset GONOPROXY +$ export GOPROXY="http://localhost:3000" +$ rm go.* +$ go mod init app +$ go mod tidy +``` + +清单 15 显示了如何将 GONOPROXY 变量恢复为空,以及再次运行 go mod tidy 之前如何从 GOPROXY 中删除 direct。 + +**清单 16** + +```bash +app/cmd/db imports + github.com/Bhinneka/golib: cannot find module providing package github.com/Bhinneka/golib +app/cmd/db imports + gopkg.in/rethinkdb/rethinkdb-go.v5: cannot find module providing package gopkg.in/rethinkdb/rethinkdb-go.v5 +``` + +清单 16 显示了运行 go mod tidy 时来自 Go 工具的输出。你可以看到调用失败,因为 Go 工具找不到模块。 + +如果我将 direct 放回 GOPROXY 变量中会怎样? + +```bash +$ unset GONOPROXY +$ export GOPROXY="http://localhost:3000,direct" +$ rm go.* +$ go mod init app +$ go mod tidy +``` + +清单 17 显示了如何再次将 direct 用于 GOPROXY 变量。 + +**清单 18** + +```bash +go: finding github.com/Bhinneka/golib latest +go: finding gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.1 +go: downloading gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.1 +go: extracting gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.1 +``` + +清单 18 显示了 Go 工具是如何再次工作的,并直接到每个 VCS 系统去获取模块。记住,如果返回任何其他状态代码(在 200、410 或 404 之外),Go 工具将失败。 + +### 其他场景 + +我决定不再继续使用其他只会导致 Go 工具失败的场景。如果你使用的是私有模块,那么你需要一个私有模块镜像,每个开发人员和构建机器上的配置都很重要,并且需要保持一致。私有模块镜像的配置需要与开发人员配置的内容相匹配,构建计算机也是如此。然后使用 GONOPROXY 和 GONOSUMDB 环境变量防止将私有模块的请求发送到任何 Google 服务器。如果你正在使用 Athens,它有特殊的配置选项来查找任何开发人员或构建计算机上的配置差异。 + +## VCS 认证问题 + +在回顾这篇文章的时候,[Erdem Aslan](https://twitter.com/Gladmir) 非常友好地为人们遇到的问题提供了一个解决方案。获取依赖项时的 Go 工具直接期望使用基于 https 的协议。在需要 VCS 认证的环境中,这可能是一个问题。Athens 可以帮助解决这个问题,但是如果你想确保直接调用不会失败,Erdem 为你的全局 git 配置文件提供了这些设置。 + +**清单 19** + +```bash +[url "git@github.com:"] +insteadOf = "https://github.com" + pushInsteadOf = "github:" + pushInsteadOf = "git://github.com/" +``` + +## 总结 + +当你开始在自己的项目中使用模块时,请确保尽早决定使用哪个模块镜像。如果你有一个私有的 VCS 或者如果隐私是一个大问题,那么使用一个私有的模块镜像是你最好的选择。这将提供你需要的所有安全性、更好的抓取模块性能和最高级别的隐私。Athens 是运行私有模块镜像的好选择,因为它提供了模块缓存和校验和数据库代理。 + +如果你想检查 Go 工具是否遵守你的配置,并且所选择的模块镜像是否正确地代理了校验和数据库,那么 Go 工具有一个名为 go mod verify 的命令。此命令检查依赖项在下载后是否未被修改。它将检查本地模块缓存中的内容,在 1.15 版本,该命令可以检查 [vendor 件夹](https://github.com/golang/go/issues/27348)。 + +尝试这些配置,并找到最符合你需要的解决方案。 + +--- + +via: + +作者:[William Kennedy](https://www.ardanlabs.com/) +译者:[polaris1119](https://github.com/polaris1119) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 \ No newline at end of file diff --git a/published/tech/ardanlabs-modules/20200413-modules-05-vendoring.md b/published/tech/ardanlabs-modules/20200413-modules-05-vendoring.md new file mode 100644 index 000000000..8bb311137 --- /dev/null +++ b/published/tech/ardanlabs-modules/20200413-modules-05-vendoring.md @@ -0,0 +1,100 @@ +首发于:https://studygolang.com/articles/35227 + +# Go Module 教程第 5 部分:Vendoring + +前四个教程: + +- [Go Module 教程第 1 部分:为什么和做什么](https://studygolang.com/articles/24580) +- [Go Module 教程第 2 部分:项目、依赖和 gopls](https://studygolang.com/articles/35202) +- [Go Module 教程第 3 部分:最小版本选择](https://studygolang.com/articles/35210) +- [Go Module 教程第 4 部分:镜像、校验和以及 Athens](https://studygolang.com/articles/35225) + +作者是一个 Vendoring 爱好者。觉得它合理和实用,可以用于你的应用程序项目。 + +## 引言 + +我相信 Vendoring 给你的应用程序项目最持久的稳定保证,因为该项目拥有每一行源代码,它需要构建应用程序。如果你想要一个可重复的构建,而不需要依赖外部服务(比如模块镜像),并且不需要连接到网络,那么 vendoring 就是解决方案。 + +Vendoring 的其他好处有: + +- 如果从 VCS 中删除了依赖项,或者代理服务器丢失了模块,Vendoring 同样可以正常工作; +- 通过运行 diff 可以看到升级依赖关系,并维护历史记录; +- 你将能够跟踪和调试依赖项,并在必要时测试更改; + - 一旦你运行 `go mod tidy` 或 `go mod vendor`,更改就会覆盖掉; + +在这篇文章中,我将提供 Go 支持 Vendoring 的历史,以及随着时间的推移默认行为的改变。我还将分享 Go 的工具如何能够维护版本间的向后兼容性。最后,我将分享可能需要(随着时间的推移)手动升级 go.mod 文件中列出的版本,以更改未来 Go 版本的默认行为。 + +## 运行不同版本的 Go + +为了向你展示 Go 1.13 和 Go 1.14 之间默认行为的差异,我需要能够同时在我的机器上运行这两个版本的工具。在我发表这篇文章的时候,我已经在我的机器上安装了 Go 1.14.2,我使用传统的 `go` 访问这个版本。但是对于这篇文章,我还需要运行一个 Go 1.13 环境。那么,如何才能在不破坏我目前的开发环境的情况下做到这一点呢? + +关于这点,「polarisxu」公众号发表两篇相关的文章: + +- [终于找到了一款我喜欢的安装和管理 Go 版本的工具](https://mp.weixin.qq.com/s/yTblk9Js1Zcq5aWVcYGjOA) +- [我这样升级 Go 版本,你呢?](https://mp.weixin.qq.com/s/jEhX5JHAo9L6iD3N54x6aA) + +推荐大家参考使用。 + +## Vendoring 快速参考 + +Go 工具在管理和 vendor 应用程序项目的依赖关系方面做了很好的工作,最小化了对工作流的影响。它有两个子命令: `tidy` 和 `vendor`。 + +`go mod tidy` 命令可以保证项目的依赖准确的列出。有些编辑器(比如 VS Code 和 GoLand)提供了在开发期间更新模块文件的功能,但这并不意味着一旦一切正常工作,模块文件就会变得干净和准确。我建议在提交代码之前运行 `go mod tidy` 命令,并将代码 push 到 VCS。 + +如果你也想 vendor 这些依赖项,那么在 tidy 之后运行 vendor 命令。 + +```bash +go mod vendor +``` + +此命令在项目中创建一个 vendor 文件夹,其中包含项目构建和测试代码所需的所有依赖项(直接和间接)的源代码。在运行 tidy 之后应该运行此命令,以保持 vendor 文件夹与模块文件同步。确保提交并将 vendor 文件夹推送到 VCS。 + +## 版本间的向后兼容性 + +在 Go 1.14中,在模块缓存上默认使用 vendor 文件夹的更改是我希望项目的行为。起初我以为我可以用 Go 1.14 来构建我现有的项目,这就足够了,但是我错了。在我第一次使用 Go 1.14 构建并且没有看到 vendor 文件夹后,我了解到 Go 工具读取 go.mod 文件以获取版本信息,并且保持与列出的版本的向后兼容性。其实在 Go 1.14的发布说明中清楚地表达了这一点。。 + +当主模块包含顶级 vendor 目录并且它的 go.mod 文件指定 Go 1.14 或更高时,Go 命令现在默认为 `-mod=vendor` 来执行接受该标志的操作。 + +为了使用新的默认 vendoring 行为,我需要将 go.mod 文件中的版本信息从 Go 1.13 升级到 Go 1.14。 + +## GOPATH 或 Module 模式 + +在 Go 1.11 中,向 Go 工具添加了一个新的模式,称为“模块模式”。当 Go 工具以模块模式运行时,模块系统用于查找和生成代码。当 Go 工具以 GOPATH 模式运行时,传统的 GOPATH 系统将继续用于查找和构建代码。我在使用 Go 工具时遇到的一个更大的问题是,知道不同版本之间默认使用什么模式。然后知道哪些配置更改和标志需要保持构建的一致性。 + +为了理解 Go 过去 4 个版本的历史和语义变化,最好重温一下这些模式。(截止 2021 年 9 月,已经发布了 Go1.17) + +**Go 1.11** + +引入了一个叫做 GO111MODULE 的新环境变量,它的默认设置是 auto。这个变量将决定 Go 工具是使用模块模式还是 GOPATH 模式,具体取决于代码所在的位置(GOPATH 内部或外部)。若要强制一种模式或另一种模式,您可以将此变量设置为 on 或 off。当涉及到 vendor 文件夹时,模块模式将默认忽略 vendor 文件夹,并建立对模块缓存的依赖关系。 + +**Go 1.12** + +GO111MODULE 的默认设置仍然是 auto,Go 工具继续根据代码所在的位置(GOPATH 内部或外部)确定模块模式或 GOPATH 模式。对于 vendor 文件夹,模块模式在默认情况下仍然会忽略 vendor 文件夹,并依赖于模块缓存。 + +**Go 1.13** + +GO111MODULE 的默认设置仍然是 auto,但是 Go 工具不再对工作目录是否在 GOPATH 中敏感。模块模式在默认情况下仍然会忽略 vendor 文件夹,并依赖模块缓存生成依赖项。 + +**Go 1.14** + +GO111MODULE 的默认设置仍然是 auto,Go 工具不再对 GOPATH 中是否包含工作目录敏感。但是,如果存在 vendor 文件夹,默认情况下将使用它来构建依赖项,而不是构建模块缓存。此外,go 命令验证项目的 vendor/modules.txt 文件与它的 go.mod 文件是否一致。 + +从 Go1.16 起,GO111MODULE 默认值设置为 on,即默认启用 Module 模式。 + +## 总结 + +不知道大家用 Vendor 多不多?如果依赖很多,Vendor 似乎是比较好的选择。像 Kubernetes 使用的就是 Vendor。 + +本文只是简单的介绍了 Vendoring,毕竟使用很简单,可以根据你的情况来决定。 + +没有完全按照文章翻译。 + +--- + +via: + +作者:[William Kennedy](https://www.ardanlabs.com/) +译者:[polaris1119](https://github.com/polaris1119) +校对:[polaris1119](https://github.com/polaris1119) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/aspects-of-a-good-go-library.md b/published/tech/aspects-of-a-good-go-library.md index e3902c07a..521eb18d2 100644 --- a/published/tech/aspects-of-a-good-go-library.md +++ b/published/tech/aspects-of-a-good-go-library.md @@ -10,7 +10,7 @@ ### 加标签的库版本 -使用 git 标签来管理你的库版本。语义版本化是一个合理的系统。如果你对语义版本化的[反对在意](https://news.ycombinator.com/item?id=13378637),那么,你就不是本文的目标读者 :) +使用 Git 标签来管理你的库版本。语义版本化是一个合理的系统。如果你对语义版本化的[反对在意](https://news.ycombinator.com/item?id=13378637),那么,你就不是本文的目标读者 :) ### 没有非标准库依赖 @@ -124,7 +124,7 @@ func (p *PreferThis) WriteTo(w Writer) (n int64, err error) { ... } ### 通过行为暴露错误,而不是类型 -这是以库为中心的 Dave的 _Assert errors for behaviour, not type。_的等价说明。更多信息,看[这里](https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully)。 +这是以库为中心的 Dave 的 _Assert errors for behaviour, not type。_的等价说明。更多信息,看[这里](https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully)。 ### 不要 panic @@ -134,9 +134,9 @@ func (p *PreferThis) WriteTo(w Writer) (n int64, err error) { ... } ### 避免创建 goroutine -这是由 [CodeReviewComments 中的同步函数](https://github.com/golang/go/wiki/CodeReviewComments#synchronous-functions)部分推导出的更明确的规则。同步函数为库用户提供更多的控制权。goroutine 有时在并行化逻辑时是有用的,但是,作为一名库作者,你应该从不使用 goroutine 并且找出使用它们的原因**开始**,而不是先使用 goroutine,然后争论着摆脱它们。 +这是由 [CodeReviewComments 中的同步函数](https://github.com/golang/go/wiki/CodeReviewComments#synchronous-functions)部分推导出的更明确的规则。同步函数为库用户提供更多的控制权。goroutine 有时在并行化逻辑时是有用的,但是,作为一名库作者,你应该从不使用 Goroutine 并且找出使用它们的原因**开始**,而不是先使用 goroutine,然后争论着摆脱它们。 -### 允许后台 goroutine 干净地停止 +### 允许后台 Goroutine 干净地停止 这是 [goroutine 生命周期](https://github.com/golang/go/wiki/CodeReviewComments#goroutine-lifetimes) 反馈的首选限制。应该有一种方式,以一种不会发出虚假错误的方式,结束你的库创建的任意 goroutine。 @@ -181,12 +181,12 @@ Go 简单的语法和优秀的标准库函数允许广泛的静态代码检查 100% 测试覆盖率是极端的,而 0% 测试覆盖率几乎不是什么好事。这是一项难以量化的规则,所以我已经决定“没有任何函数应该具备 0% 的测试覆盖率”是最低限度了。你可以使用 Go 的 cover 工具获取每个函数测试覆盖率。 ```console -# go test -coverprofile=cover.out context +# Go test -coverprofile=cover.out context ok context 2.651s coverage: 97.0% of statements ``` ```console -# go tool cover -func=cover.out +# Go tool cover -func=cover.out context/context.go:162: Error 100.0% context/context.go:163: Timeout 100.0% context/context.go:164: Temporary 100.0% diff --git a/published/tech/call-private-function.md b/published/tech/call-private-function.md index e31cdb94e..32812f5d0 100644 --- a/published/tech/call-private-function.md +++ b/published/tech/call-private-function.md @@ -1,19 +1,19 @@ 已发布:https://studygolang.com/articles/12134 -# 在 golang 中如何调用私有函数(绑定隐藏的标识符) +# 在 Golang 中如何调用私有函数(绑定隐藏的标识符) 2016 年 4 月 28 日 -名字在 golang 中的重要性和在其他任何一种语言是一样的。他们甚至含有语义的作用:在一个包的外部某个名字的可见性是由这个名字首字母是否是大写来决定的。 +名字在 Golang 中的重要性和在其他任何一种语言是一样的。他们甚至含有语义的作用:在一个包的外部某个名字的可见性是由这个名字首字母是否是大写来决定的。 有时为了更好的组织代码或者在其他包使用某些隐藏的函数时需要克服这种限制。 在过去美好的日子,有 2 种实现方式,它们能绕过编译器的检查:不能引用未导出的名称 pkg.symbol : - 旧的方式,现在已经不再使用 - 汇编级隐式连接到所需符号,称为 assembly stubs ,详见 [go runtime, os/signal: use //go:linkname instead of assembly stubs to get access to runtime functions](https://groups.google.com/forum/#!topic/golang-codereviews/J0HK9GLc76M) 。 -- 现行的方式 - go 编译器通过 go:linkname 支持名称重定向,引用于 11.11.14 [ dev.cc code review 169360043: cmd/gc: changes for removing runtime C code (issue 169360043 by r…@golang.org)](https://groups.google.com/forum/#!topic/golang-codereviews/5Ps_El_RpNE) ,在 github.com 的 issue 上有可以找到 [ cmd/compile: “missing function body” error when using the //go:linkname compiler directive #15006](https://github.com/golang/go/issues/15006) 。 +- 现行的方式 - Go 编译器通过 go:linkname 支持名称重定向,引用于 11.11.14 [ dev.cc code review 169360043: cmd/gc: changes for removing runtime C code (issue 169360043 by r…@golang.org)](https://groups.google.com/forum/#!topic/golang-codereviews/5Ps_El_RpNE) ,在 github.com 的 issue 上有可以找到 [ cmd/compile: “missing function body” error when using the //go:linkname compiler directive #15006](https://github.com/golang/go/issues/15006) 。 -用这些技巧我曾设法绑定 golang 运行时调度器相关的函数用以减少过度使用 go 的协程和内部锁机制导致的 gc 停顿。 +用这些技巧我曾设法绑定 Golang 运行时调度器相关的函数用以减少过度使用 Go 的协程和内部锁机制导致的 gc 停顿。 ## 使用 assembly stubs @@ -23,7 +23,7 @@ ```go // Assembly to get into package runtime without using exported symbols. -// +build amd64 amd64p32 arm arm64 386 ppc64 ppc64le +// +build amd64 amd64p32 ARM arm64 386 ppc64 ppc64le #include "textflag.h" @@ -53,7 +53,7 @@ JMP runtime·signal_recv(SB) 而 signal_unix.go 的绑定如下: ```go -// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris windows +// +build darwin dragonfly freebsd Linux nacl netbsd openbsd solaris windows package signal @@ -71,14 +71,14 @@ func signal_recv() uint32 ## 使用 go:linkname -为了使用这种方法,代码中必须 `import _ "unsafe"` 包。为了解决 go 编译器 `-complete` 参数的限制,一种可能的方法是在 main 包目录加一个空的汇编 stub 文件以禁用编译器的检查。 +为了使用这种方法,代码中必须 `import _ "unsafe"` 包。为了解决 Go 编译器 `-complete` 参数的限制,一种可能的方法是在 main 包目录加一个空的汇编 stub 文件以禁用编译器的检查。 详见 os/signal/sig.s: ```go // The runtime package uses //go:linkname to push a few functions into this // package but we still need a .s file so the Go tool does not pass -complete -// to the go tool compile so the latter does not complain about Go functions +// to the Go tool compile so the latter does not complain about Go functions // with no bodies. ``` @@ -154,7 +154,7 @@ import ( // Event types in the trace, args are given in square brackets. const ( - traceEvGoBlock = 20 // goroutine blocks [timestamp, stack] + traceEvGoBlock = 20 // Goroutine blocks [timestamp, stack] ) type mutex struct { diff --git a/published/tech/dont-just-check-errors-handle-them-gracefully.md b/published/tech/dont-just-check-errors-handle-them-gracefully.md index 70d0daad8..31b1ee6d9 100644 --- a/published/tech/dont-just-check-errors-handle-them-gracefully.md +++ b/published/tech/dont-just-check-errors-handle-them-gracefully.md @@ -36,7 +36,7 @@ if err == ErrSomething { … } 这些信息应该在日志文件中或者是显示屏上出现,你不需要通过检查这些信息来改变程序行为。 -我知道有时候这样很难,就像有些人在 twitter 上提到的那样,这条建议在写测试的时候不适用。尽管如此,在我看来,作为一种编码风格,你应该避免比较字符型的错误信息。 +我知道有时候这样很难,就像有些人在 Twitter 上提到的那样,这条建议在写测试的时候不适用。尽管如此,在我看来,作为一种编码风格,你应该避免比较字符型的错误信息。 ### 标记错误成为公开 API 的一部分 @@ -218,7 +218,7 @@ func AuthenticateRequest(r *Request) error { } ``` -就像我们在前面提到的,这个模式不兼容标记错误或者类型断言,因为转换错误值到字符串,再和其他的字符串合并,再使用 fmt.Errorf 转换为error 打破了对等关系,破坏了原始错误的相关信息。 +就像我们在前面提到的,这个模式不兼容标记错误或者类型断言,因为转换错误值到字符串,再和其他的字符串合并,再使用 fmt.Errorf 转换为 error 打破了对等关系,破坏了原始错误的相关信息。 ### 注解错误 diff --git a/published/tech/escape-analysis-flaws.md b/published/tech/escape-analysis-flaws.md index 3f4645549..8ad77593c 100644 --- a/published/tech/escape-analysis-flaws.md +++ b/published/tech/escape-analysis-flaws.md @@ -65,7 +65,7 @@ func BenchmarkAssignmentIndirect(b *testing.B) { **基准测试输出** ```console -$ go test -gcflags "-m -m" -run none -bench . -benchmem -memprofile mem.out +$ Go test -gcflags "-m -m" -run none -bench . -benchmem -memprofile mem.out BenchmarkAssignmentIndirect-8 100000000 14.2 ns/op 8 B/op 1 allocs/op ``` @@ -82,7 +82,7 @@ BenchmarkAssignmentIndirect-8 100000000 14.2 ns/op 8 B/op **Pprof 输出** ``` -$ go tool pprof -alloc_space mem.out +$ Go tool pprof -alloc_space mem.out ROUTINE ======================== 759.51MB 759.51MB (flat, cum) 100% of Total @@ -147,13 +147,13 @@ func foo(p *int, x int) { 在第 07 行,声明了一个类型为 `int`,名字为 `y1` 的变量,这个变量在第 08 行对 `foo` 的函数调用过程中发生了共享。从第 10 行到第 13 行,存在类似的情况。声明了一个类型为 `int` 的变量 `y2`,然后这个变量作为第一个参数共享给一个在第 13 行声明和执行的字面函数。这个字面函数与 `foo` 函数相同。 -最后,在第 15 行到第 17 行之间,`foo`函数被赋给一个名为 `p` 的变量。通过变量 `p`,`foo` 函数被执行,其中,变量 `y3` 被共享。第 17 行的这个函数调用是通过 `p` 变量间接完成的。这与第 13 行的字面函数没有显式函数变量所执行的函数调用方式情况相同。 +最后,在第 15 行到第 17 行之间,`foo` 函数被赋给一个名为 `p` 的变量。通过变量 `p`,`foo` 函数被执行,其中,变量 `y3` 被共享。第 17 行的这个函数调用是通过 `p` 变量间接完成的。这与第 13 行的字面函数没有显式函数变量所执行的函数调用方式情况相同。 以下是运行基准测试的结果,以及一份逃逸分析报告。还包括了 pprof list 命令的输出。 **基准测试输出** ``` -$ go test -gcflags "-m -m" -run none -bench BenchmarkLiteralFunctions -benchmem -memprofile mem.out +$ Go test -gcflags "-m -m" -run none -bench BenchmarkLiteralFunctions -benchmem -memprofile mem.out BenchmarkLiteralFunctions-8 50000000 30.7 ns/op 16 B/op 2 allocs/op ``` @@ -172,7 +172,7 @@ BenchmarkLiteralFunctions-8 50000000 30.7 ns/op 16 B/op **Pprof 输出** ``` -$ go tool pprof -alloc_space mem.out +$ Go tool pprof -alloc_space mem.out ROUTINE ======================== 768.01MB 768.01MB (flat, cum) 100% of Total @@ -196,9 +196,9 @@ ROUTINE ======================== 在逃逸分析报告中,为变量 `y2` 和 `y3` 变量的分配给出的原因是 `(parameter to indirect call)`。pprof 输出很清楚的显示出,`y2` 和 `y3` 被分配在堆上,而 `y1` 不是。 -虽然,我会认为在第 13 行调用的函数字面量的使用是代码异味,但是,第 16 行变量 `p` 的使用并不是。在 Go 中,人们总是会传递函数作为参数。特别是在构建 web 服务的时候。修复这个间接调用缺陷会帮助减少 Go web 服务应用中的许多分配。 +虽然,我会认为在第 13 行调用的函数字面量的使用是代码异味,但是,第 16 行变量 `p` 的使用并不是。在 Go 中,人们总是会传递函数作为参数。特别是在构建 Web 服务的时候。修复这个间接调用缺陷会帮助减少 Go Web 服务应用中的许多分配。 -这里是一个你会在许多 web 服务应用中找到的例子。 +这里是一个你会在许多 Web 服务应用中找到的例子。 **代码清单 2.2** @@ -248,7 +248,7 @@ func wrapHandler(h Handler) Handler { **基准测试输出** ``` -$ go test -gcflags "-m -m" -run none -bench BenchmarkHandler -benchmem -memprofile mem.out +$ Go test -gcflags "-m -m" -run none -bench BenchmarkHandler -benchmem -memprofile mem.out BenchmarkHandler-8 20000000 72.4 ns/op 256 B/op 1 allocs/op ``` @@ -264,7 +264,7 @@ BenchmarkHandler-8 20000000 72.4 ns/op 256 B/op 1 alloc **Pprof 输出** ``` -$ go tool pprof -alloc_space mem.out +$ Go tool pprof -alloc_space mem.out ROUTINE ======================== 5.07GB 5.07GB (flat, cum) 100% of Total @@ -279,7 +279,7 @@ ROUTINE ======================== . . 22:} ``` -在逃逸分析报告中,你可以看到这种分配的原因是 `(parameter to indirect call)`。pprof 报告显示,`r` 变量正在分配。如前所述,这是人们在用 Go 构建 web 服务时编写的常见代码。修复这个缺陷会减少程序中大量的分配。 +在逃逸分析报告中,你可以看到这种分配的原因是 `(parameter to indirect call)`。pprof 报告显示,`r` 变量正在分配。如前所述,这是人们在用 Go 构建 Web 服务时编写的常见代码。修复这个缺陷会减少程序中大量的分配。 ## 切片和 Map 赋值 @@ -314,7 +314,7 @@ func BenchmarkSliceMapAssignment(b *testing.B) { **基准测试输出** ``` -$ go test -gcflags "-m -m" -run none -bench . -benchmem -memprofile mem.out +$ Go test -gcflags "-m -m" -run none -bench . -benchmem -memprofile mem.out BenchmarkSliceMapAssignment-8 10000000 104 ns/op 16 B/op 2 allocs/op ``` @@ -335,7 +335,7 @@ BenchmarkSliceMapAssignment-8 10000000 104 ns/op 16 B/op **Pprof 输出** ``` -$ go tool pprof -alloc_space mem.out +$ Go tool pprof -alloc_space mem.out ROUTINE ======================== 162.50MB 162.50MB (flat, cum) 100% of Total @@ -420,7 +420,7 @@ func foo(i Iface) { **基准测试输出** ``` -$ go test -gcflags "-m -m" -run none -bench . -benchmem -memprofile mem.out +$ Go test -gcflags "-m -m" -run none -bench . -benchmem -memprofile mem.out BenchmarkInterfaces-8 10000000 126 ns/op 64 B/op 4 allocs/op ``` @@ -452,7 +452,7 @@ BenchmarkInterfaces-8 10000000 126 ns/op 64 B/op 4 all **Pprof 输出** ``` -$ go tool pprof -alloc_space mem.out +$ Go tool pprof -alloc_space mem.out ROUTINE ======================== 658.01MB 658.01MB (flat, cum) 100% of Total @@ -476,7 +476,7 @@ ROUTINE ======================== ``` -注意,在基准报告中有四个分配。这是因为代码会复制 `x1` 和 `x2` 变量,这也会产生分配。在第 18 行中使用`x1` 变量进行赋值时,以及在第 25 行中对 `foo` 进行函数调用使用 `x2` 的值时,创建了这些副本。 +注意,在基准报告中有四个分配。这是因为代码会复制 `x1` 和 `x2` 变量,这也会产生分配。在第 18 行中使用 `x1` 变量进行赋值时,以及在第 25 行中对 `foo` 进行函数调用使用 `x2` 的值时,创建了这些副本。 在逃逸分析报告中,为 `x1` 以及 `x1` 的副本逃逸提供的原因是 `(receiver in indirect call)`。这很有趣,因为第 21 和 22 行对 `Method` 的调用才是这个缺陷真正的罪魁祸首。请记住,针对接口的方法调用需要通过 iTable 进行间接调用。正如你之前看到的,间接调用是逃逸分析中的一个缺陷。 @@ -519,7 +519,7 @@ func BenchmarkUnknown(b *testing.B) { **基准测试输出** ``` -$ go test -gcflags "-m -m" -run none -bench . -benchmem -memprofile mem.out +$ Go test -gcflags "-m -m" -run none -bench . -benchmem -memprofile mem.out Benchmark-8 20000000 50.8 ns/op 112 B/op 1 allocs/op ``` @@ -534,7 +534,7 @@ Benchmark-8 20000000 50.8 ns/op 112 B/op 1 allocs/op **Pprof 输出** ``` -$ go tool pprof -alloc_space mem.out +$ Go tool pprof -alloc_space mem.out ROUTINE ======================== 2.19GB 2.19GB (flat, cum) 100% of Total @@ -554,7 +554,7 @@ _这可能与 `Buffer` 类型的引导数组有关。它意味着一种优化, **有这个问题,它与导致这种分配的引导数组有关:** -[cmd/compile, bytes: bootstrap array causes bytes.Buffer to always be heap-allocated](https://github.com/golang/go/issues/7921) +[cmd/compile, bytes: Bootstrap array causes bytes.Buffer to always be heap-allocated](https://github.com/golang/go/issues/7921) **CKS 在 reddit 上发布了此回复:** @@ -564,7 +564,7 @@ _这可能与 `Buffer` 类型的引导数组有关。它意味着一种优化, ./buffer.go:170:46: leaking param content: p ./buffer.go:170:46: from *p (indirection) at ./buffer.go:170:46 ./buffer.go:170:46: from copy(b.buf[m:], p) (copied slice) at ./buffer.go:176:13 -(The line numbers are for the current git tip; they may be slightly off in other copies.) +(The line numbers are for the current Git tip; they may be slightly off in other copies.) ``` 考虑到 copy() 是语言内置函数,似乎编译器应该知道这里,源参数不逃逸。或者有可能编译器在对 copy() 的实际实现做一些十分有趣的事情,以至于源在某些情况下会逃逸。 diff --git a/published/tech/go-functions-overview-anonymous-closures.md b/published/tech/go-functions-overview-anonymous-closures.md index e9f5b8835..a114e3b55 100644 --- a/published/tech/go-functions-overview-anonymous-closures.md +++ b/published/tech/go-functions-overview-anonymous-closures.md @@ -20,7 +20,7 @@ ![named Func](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-functions-overview/named_funcs.png) -

这是一个命名函数:Len 函数接受一个 string 类型的参数并返回一个 int 类型的值

+

这是一个命名函数:Len 函数接受一个 string 类型的参数并返回一个 int 类型的值

--- @@ -59,7 +59,7 @@ func Incr(c Count) int ![Method](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-functions-overview/methods.png) -

原理并不完全如上所示,但你可以像这样来理解

+

原理并不完全如上所示,但你可以像这样来理解

### 值传递 @@ -71,7 +71,7 @@ var c Count; c.Incr(); c.Incr() // output: 1 1 ``` -

c 的值并不会增加,因为 c 是通过值传递的方式传递给方法

+

c 的值并不会增加,因为 c 是通过值传递的方式传递给方法

![Value receiver](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-functions-overview/value_receiver.png) @@ -94,7 +94,7 @@ c.Incr(); c.Incur() [![run the code](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-functions-overview/run_the_code.png)](https://play.golang.org/p/hGVJWPIFZG "receiver") -

在我之前的一些文章中有更多的示例:看这里!这里!

+

在我之前的一些文章中有更多的示例:看 这里! 这里!

--- @@ -140,7 +140,7 @@ onApiHit(&dummyCounter) ![first-class funcs](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-functions-overview/first-class_funcs.png) -

函数可以作为一种值类型和其他的类型配合使用,反之亦然

+

函数可以作为一种值类型和其他的类型配合使用,反之亦然

### 示例 @@ -337,7 +337,7 @@ dog [![run the code](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-functions-overview/run_the_code.png)](https://play.golang.org/p/AI1_5BkO1d "closure") -

再次提示,这里面有更详细的描述哦~

+

再次提示,这里面有更详细的描述哦 ~

--- @@ -384,7 +384,7 @@ main: ends ![concurrent funs](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-functions-overview/concurrent_funcs.png) -

如果 main 函数中没有睡眠等阻塞调用,那么,main 函数会终止,而不会等待并发函数执行完。

+

如果 main 函数中没有睡眠等阻塞调用,那么,main 函数会终止,而不会等待并发函数执行完。

``` main: continues... @@ -418,7 +418,7 @@ Go 语言的链接器会将函数放置到可执行环境中,以便稍后在 ### 外部函数 -如果你省略掉函数体,仅仅进行函数声明,连接器会尝试在任何可能的地方找到这个外部函数。例如:Atan Func在[*这里只进行了声明*](https://github.com/golang/go/blob/dd8dc6f0595ffc2c4951c0ce8ff6b63228effd97/src/pkg/math/atan.go#L54),而后在[*这里进行了实现*](https://github.com/golang/go/blob/dd8dc6f0595ffc2c4951c0ce8ff6b63228effd97/src/pkg/math/atan_386.s)。 +如果你省略掉函数体,仅仅进行函数声明,连接器会尝试在任何可能的地方找到这个外部函数。例如:Atan Func 在[*这里只进行了声明*](https://github.com/golang/go/blob/dd8dc6f0595ffc2c4951c0ce8ff6b63228effd97/src/pkg/math/atan.go#L54),而后在[*这里进行了实现*](https://github.com/golang/go/blob/dd8dc6f0595ffc2c4951c0ce8ff6b63228effd97/src/pkg/math/atan_386.s)。 --- diff --git a/published/tech/go-reflection-creating-objects-from-type/20171121-part-i-primitive-types.md b/published/tech/go-reflection-creating-objects-from-type/20171121-part-i-primitive-types.md index 395454dad..e33db7868 100644 --- a/published/tech/go-reflection-creating-objects-from-type/20171121-part-i-primitive-types.md +++ b/published/tech/go-reflection-creating-objects-from-type/20171121-part-i-primitive-types.md @@ -2,7 +2,7 @@ # Go 反射:根据类型创建对象-第一部分(原始类型) -> 这是关于在 Go 中根据类型创建对象的博客系列两部分的第一部分。这部分讨论原始类型的对象创建 +>  这是关于在 Go 中根据类型创建对象的博客系列两部分的第一部分。这部分讨论原始类型的  对象创建 ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-reflect/cover1.png) @@ -10,11 +10,11 @@ Go 中的 reflect 包提供了根据执行过程中对象的类型来改变程 reflect 包提供了两个重要的结构 - Type 和 Value。 -Type 是一个 Go 中任何类型的代表。换句话说,它可以被用于编码任何 Go 类型(例如:int , string , bool , myCustomType 等等)。Value 是一个 Go 中任何值的代表。换句话说,它可以被用于编码、操作任何 Go 的值。 +Type 是一个 Go 中任何类型的代表。 换句话说,它可以被用于编码任何 Go 类型(例如:int , string , bool , myCustomType 等等)。Value 是一个 Go 中任何  值的代表。换句话说,它可以被用于编码、操作任何 Go 的值。 ## 类型与类别(Types 和 Kinds) -Go 中有一个隐蔽的、鲜为人知的,使得 Type 和 Kind 含义有差别的公约。这种差别可以通过一个例子理解一下。 +Go 中有一个隐蔽的、鲜为人知的 ,使得 Type 和 Kind 含义有差别的公约。这种差别可以通过一个例子理解一下。 看一下这个结构: ```go @@ -24,7 +24,7 @@ type example struct { } ``` -这个结构体的一个对象的 type 应该是 example。而这个对象的 kind 应该是 struct。这里的 Kind 可以被看成一个 Type 的 Type。 + 这个结构体的一个对象的 type 应该是 example。而这个对象的 kind 应该是 struct。这里的 Kind 可以被看成一个 Type 的 Type。 > 在 Go 里所有 structs 都是相同的 kind,但不是相同的 Type @@ -32,15 +32,15 @@ type example struct { 相比之下,像 int、float、string 等等原始类型,并没有产生 type 和 kind 含义上的差别。换句话说,一个 int 变量的 kind 是 int。一个 int 变量的 type 也是 int。 -## 根据类型创建对象 +##  根据类型创建对象 为了根据一个类型标签(type signature)创建一个对象,这个对象的 type 和 kind 都是必要的。从这里开始,当我用到 ‘type signature’ 这个术语时,我的意思就是 Go 里的 reflect.Type 类型的对象。 ### 根据原始类型创建原始对象 -原始对象可以根据它们的 type signature,通过使用它们的 Zero 值被创建。 +原始对象可以根据它们的 type signature,通过使用它们的 Zero 值被  创建。 -> 一个类型的 Zero 值是指一个该类型、没有初始化的对象的值 +> 一个类型的 Zero 值是指一个  该类型、 没有初始化的对象的值 这是一个 Go 中所有原始类型的列表: @@ -73,9 +73,9 @@ func CreatePrimitiveObjects(t reflect.Type) reflect.Value { } ``` -这里将创建一个期望的对象,并返回一个拥有相应 Zero 值的 reflect.Value 对象。为了让这个对象可以使用,需要将它的值提取一下。 +这里将创建一个期望的  对象, 并返回一个拥有相应 Zero 值的 reflect.Value 对象。为了让这个对象可以使用,需要将它的值提取一下。 -对于不同的原始类型,相应对象的值可以采用相应适合的方法来提取。 +对于不同的原始  类型,相应对象的值可以采用相应适合的方法来提取。 ### 提取整数值 @@ -89,9 +89,9 @@ Int32 Int64 ``` -Int 类型表示平台定义的默认整型大小。另外 4 种类型分别是 8,16,32,64(bit单位)大小的整型。 +Int 类型表示平台定义的默认整型大小。另外 4 种类型  分别是 8,16,32,64(bit 单位)大小的整型。 -为了获得每种不同的整数类型,reflect.Value 对象对应的整数需要被转换成相应的整数类型。 +为了获得每种不同的整数类型,reflect.Value 对象对应的  整数需要被转换成相应的整数类型。 这里是如何提取 int32 类型: @@ -110,7 +110,7 @@ func extractInt32(v reflect.Value) (int32, error) { 值得注意的是:reflect.Int() 会返回 int64 类型。这是因为 int64 类型可以编码成其他所有整数类型。 -这里是剩余其他整数类型的提取: +这里是剩余其他  整数类型的提取: ```go // Extract Int64 @@ -160,7 +160,7 @@ func extractInt(v reflect.Value) (int, error) { ### 提取布尔值 -布尔值在 reflect 包中用常量 Bool 表示。 +布尔值在 reflect 包中  用常量 Bool 表示。 它们可以通过使用 Bool() 方法,从 reflect.Value 对象中提取出来: @@ -187,9 +187,9 @@ Uint32 Uint64 ``` -Uint 类型表示平台定义的默认无符号整型大小。另外 4 种类型分别是 8,16,32,64(bit单位)大小的无符号整型。 +Uint 类型表示平台定义的默认无符号整型大小。另外 4 种类型  分别是 8,16,32,64(bit 单位)大小的无符号整型。 -为了获得每种不同的无符号整数类型,reflect.Value 对象对应的无符号整数需要被转换成相应的无符号整数类型。 +为了获得每种不同的无符号整数类型,reflect.Value 对象对应的  无符号整数需要被转换成相应的无符号整数类型。 这里是如何提取 Uint32 类型: @@ -208,7 +208,7 @@ func extractUint32(v reflect.Value) (uint32, error) { 值得注意的是:reflect.Uint() 会返回 uint64 类型。这是因为 uint64 类型可以编码成其他所有整数类型。 -这里是剩余其他无符号整数类型的提取: +这里是剩余其他无符号  整数类型的提取: ```go // Extract Uint64 @@ -268,9 +268,9 @@ Float64 Float32 类型表示 32bit 大小的浮点数。 Float64 类型表示 64bit 大小的浮点数。 -为了获得每种不同的浮点数类型,reflect.Value 对象对应的浮点数需要被转换成相应的浮点数类型。 +为了获得每种不同的浮点数类型,reflect.Value 对象对应的  浮点数需要被转换成相应的浮点数类型。 -这里是如何提取 Float32 类型: +这里是如何  提取 Float32 类型: ```go // Extract Float32 @@ -311,11 +311,11 @@ Complex64 Complex128 ``` -Complex64 类型表示 64bit 大小的复数。Complex128 类型表示 128bit 大小的复数。 +Complex64 类型表示 64bit 大小的  复数。Complex128 类型表示 128bit 大小的复数。 -为了获得每种不同的复数类型,reflect.Value 对象对应的复数需要被转换成相应的复数类型。 +为了获得每种不同的复数类型,reflect.Value 对象对应的  复数需要被转换成相应的复数类型。 -这里是如何提取 Complex64 类型: +这里是如何  提取 Complex64 类型: ```go // Extract Complex64 @@ -353,7 +353,7 @@ func extractComplex128(v reflect.Value) (complex128, error) { 它们可以通过使用 String() 方法,从 reflect.Value 对象中提取出来: -这里是如何提取 String 类型: +这里是如何  提取 String 类型: ```go // Extract String @@ -375,9 +375,9 @@ Uintptr UnsafePointer ``` -Uintptr 和 UnsafePointer 其实是程序内存中代表一个虚拟地址的 uint 值。它可以表示一个变量或函数的位置。 +Uintptr 和 UnsafePointer 其实是程序内存中代表一个虚拟地址的 uint 值。 它可以表示一个变量  或函数的位置。 -Uintptr 和 UnsafePointer 两者间的不同在于:Uintptr 会在 Go 运行时进行类型校验,而 UnsafePointer 不会。UnsafePointer 可以被用于 Go 中任意类型向 Go 中其他任何拥有相同内存结构的类型转换。如果这是你想要探索的,请在下面评论,我会写更多关于它的东西。 +Uintptr 和 UnsafePointer 两者间的不同在于:Uintptr 会在  Go 运行时进行类型校验,而 UnsafePointer 不会。UnsafePointer 可以被用于 Go 中任意类型向 Go 中其他任何拥有相同内存结构的类型转换。如果  这是你想要探索的, 请在下面评论,我会写更多关于它的  东西。 Uintptr 和 UnsafePointer 可以分别通过 Addr() 和 UnsafeAddr() 方法,从 reflect.Value 的对象中提取出来。这是一个展示 Uintptr 提取的例子: @@ -409,13 +409,13 @@ func extractUnsafePointer(v reflect.Value) (unsafe.Pointer, error) { } ``` -值得注意的是:上面 v.UnsafeAddr() 会返回 uintptr 值。它应该在同一行进行类型转换,否则这个 unsafe.Pointer 的值不一定指向预期的位置。 +值得注意的是:上面 v.UnsafeAddr() 会返回 uintptr 值。它应该在同一行进行类型转换, 否则这个 unsafe.Pointer 的值不一定  指向预期的位置。 ## 接下来是什么 -请注意:reflect.Value 结构的所有方法在使用时都需要检验它们的 kind,否则很容易引发 panic。 +请注意:reflect.Value 结构的所有  方法在使用时都需要  检验它们的 kind,否则很容易引发 panic。 -在下一篇博客中,我会写更多像 struct、pointer、chan、map、slice、array 等复杂类型对象的创建。敬请期待! +在下一篇博客中,我会写更多像 struct、pointer、chan、map、slice、array 等复杂类型对象的创建。 敬请期待! --- diff --git a/published/tech/go-reflection-creating-objects-from-type/20171123-part-ii-composite-types.md b/published/tech/go-reflection-creating-objects-from-type/20171123-part-ii-composite-types.md index c195088ef..e67090b11 100644 --- a/published/tech/go-reflection-creating-objects-from-type/20171123-part-ii-composite-types.md +++ b/published/tech/go-reflection-creating-objects-from-type/20171123-part-ii-composite-types.md @@ -6,7 +6,7 @@ ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-reflect/cover1.png) -在前一篇博客中,我解释了 go reflect 包 `type` 和 `kind` 的概念。这篇博客,我将深入探讨这些术语。因为相比原始类型,`type` 和 `kind` 对于复合类型来说含义更多。 +在前一篇博客中,我解释了 Go reflect 包 `type` 和 `kind` 的概念。这篇博客,我将深入探讨这些术语。因为相比原始类型,`type` 和 `kind` 对于复合类型来说含义更多。 ## 类型和种类 @@ -465,4 +465,4 @@ via:https://medium.com/kokster/go-reflection-creating-objects-from-types-part- 译者:[ParadeTo](https://github.com/ParadeTo) 校对:[polaris1119](https://github.com/polaris1119) -本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go中文网](https://studygolang.com/) 荣誉推出 +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/golang-channels-tips-tricks.md b/published/tech/golang-channels-tips-tricks.md index 452779b09..6bb1a48a8 100644 --- a/published/tech/golang-channels-tips-tricks.md +++ b/published/tech/golang-channels-tips-tricks.md @@ -2,9 +2,9 @@ # Go 语言的缓冲通道:提示和技巧 -​Mahadevan Ramachandran • January 15 +​ Mahadevan Ramachandran • January 15 -通道和 goroutine 是 Go 语言基于 CSP( communicating sequential processes ,通信顺序进程)并发机制的核心部分。阅读本文可以学到一些关于channel的提示和技巧,尤其是“缓冲” channel ,在 “生产者-消费者” 情境中广泛使用了缓冲通道作为队列。 +通道和 Goroutine 是 Go 语言基于 CSP( communicating sequential processes ,通信顺序进程)并发机制的核心部分。阅读本文可以学到一些关于 channel 的提示和技巧,尤其是“缓冲” channel ,在 “生产者-消费者” 情境中广泛使用了缓冲通道作为队列。 ## 缓冲通道 = 队列 @@ -39,7 +39,7 @@ select { } // 在这里, "ok" is: // true => 不阻塞的将元素入队 -// false => 元素没有入队, 会因为queue已满而阻塞 +// false => 元素没有入队, 会因为 queue 已满而阻塞 ``` 消费者通常从队列中取出元素并处理它们。如果队列为空并且消费者无事可做,就会发生阻塞,直到生产者放入一个元素。 @@ -60,8 +60,8 @@ select { ok = false } // 在这里, "ok" is: -// true => 从queue中取出元素item (或者queue已经关闭,见下) -// false => 没有取出元素, queue为空而发生阻塞 +// true => 从 queue 中取出元素 item (或者 queue 已经关闭,见下) +// false => 没有取出元素, queue 为空而发生阻塞 ``` ## 关闭缓冲通道 @@ -172,7 +172,7 @@ select { ## 咨询和训练 -需要帮助获得一个使用 Golang 的项目?我们在创建和运行生产级 Go 平台软件解决方案领域拥有丰富经验。我们可以帮助你架构和设计 Go 平台项目,或者为使用 Go 工作的团队提供建议和监督。我们也会为希望开展Go项目的团队提供培训或者提升 Golang 知识。[这里发现更多](https://www.rapidloop.com/training) 或者 [马上联系我们](https://www.rapidloop.com/contact) 来讨论你的需求! +需要帮助获得一个使用 Golang 的项目?我们在创建和运行生产级 Go 平台软件解决方案领域拥有丰富经验。我们可以帮助你架构和设计 Go 平台项目,或者为使用 Go 工作的团队提供建议和监督。我们也会为希望开展 Go 项目的团队提供培训或者提升 Golang 知识。[这里发现更多](https://www.rapidloop.com/training) 或者 [马上联系我们](https://www.rapidloop.com/contact) 来讨论你的需求! **Mahadevan Ramachandran** diff --git a/published/tech/golang-variadic-funcs-how-to-patterns.md b/published/tech/golang-variadic-funcs-how-to-patterns.md index c44f9bcb7..e55f45a28 100644 --- a/published/tech/golang-variadic-funcs-how-to-patterns.md +++ b/published/tech/golang-variadic-funcs-how-to-patterns.md @@ -8,11 +8,11 @@ 可变参数函数即其参数数量是可变的 —— 0 个或多个。声明可变参数函数的方式是在其参数类型前带上省略符(三个点)前缀。 ->译者注:“可变参数函数”在一些翻译中也称“变长函数”,本篇译文中采用“可变参数函数“ +> 译者注:“可变参数函数”在一些翻译中也称“变长函数”,本篇译文中采用“可变参数函数“ ![what is variadic func](https://raw.githubusercontent.com/studygolang/gctt-images/master/variadic-func/what_is_variadic_func.png) -

该语句声明了一个可变参数函数及其以 “names” 命名的字符串类型可变参数

+

该语句声明了一个可变参数函数及其以 “names” 命名的字符串类型可变参数

--- diff --git a/published/tech/goroutine-leak.md b/published/tech/goroutine-leak.md index 14199ace7..1c265c965 100644 --- a/published/tech/goroutine-leak.md +++ b/published/tech/goroutine-leak.md @@ -4,7 +4,7 @@ ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/goroutine-leak/cover.jpg) -Go 中的并发性是以 goroutine(独立活动)和 channel(用于通信)的形式实现的。处理 goroutine 时,程序员需要小心翼翼地避免泄露。如果最终永远堵塞在 I/O 上(例如 channel 通信),或者陷入死循环,那么 goroutine 会发生泄露。即使是阻塞的 goroutine,也会消耗资源,因此,程序可能会使用比实际需要更多的内存,或者最终耗尽内存,从而导致崩溃。让我们来看看几个可能会发生泄露的例子。然后,我们将重点关注如何检测程序是否受到这种问题的影响。 +Go 中的并发性是以 goroutine(独立活动)和 channel(用于通信)的形式实现的。处理 Goroutine 时,程序员需要小心翼翼地避免泄露。如果最终永远堵塞在 I/O 上(例如 channel 通信),或者陷入死循环,那么 Goroutine 会发生泄露。即使是阻塞的 goroutine,也会消耗资源,因此,程序可能会使用比实际需要更多的内存,或者最终耗尽内存,从而导致崩溃。让我们来看看几个可能会发生泄露的例子。然后,我们将重点关注如何检测程序是否受到这种问题的影响。 ## 发送到一个没有接收者的 channel @@ -53,9 +53,9 @@ func main() { #goroutines: 9 ``` -每次调用 _queryAll_ 后,goroutine 的数目会发生增长。问题在于,在接收到第一个响应后,“较慢的” goroutine 将会发送到另一端没有接收者的 channel 中。 +每次调用 _queryAll_ 后,goroutine 的数目会发生增长。问题在于,在接收到第一个响应后,“较慢的” Goroutine 将会发送到另一端没有接收者的 channel 中。 -可能的解决方法是,如果提前知道后端服务器的数量,那么使用缓存 channel。否则,只要至少有一个 goroutine 仍在工作,我们就可以使用另一个 goroutine 来接收来自这个 channel 的数据。其他的解决方案可能是使用 [context](https://golang.org/pkg/context/)([example](http://golang.rakyll.org/leakingctx/)),利用 某些机制来取消其他请求。 +可能的解决方法是,如果提前知道后端服务器的数量,那么使用缓存 channel。否则,只要至少有一个 Goroutine 仍在工作,我们就可以使用另一个 Goroutine 来接收来自这个 channel 的数据。其他的解决方案可能是使用 [context](https://golang.org/pkg/context/)([example](http://golang.rakyll.org/leakingctx/)),利用 某些机制来取消其他请求。 ## 从没有发送者的 channel 中接收数据 @@ -147,11 +147,11 @@ import ( log.Println(http.ListenAndServe("localhost:6060", nil)) ``` -调用 http://localhost:6060/debug/pprof/goroutine?debug=1 ,将会返回带有堆栈跟踪的 goroutine 列表。 +调用 http://localhost:6060/debug/pprof/goroutine?debug=1 ,将会返回带有堆栈跟踪的 Goroutine 列表。 ### runtime/pprof -要将现有的 goroutine 的堆栈跟踪打印到标准输出,请执行以下操作: +要将现有的 Goroutine 的堆栈跟踪打印到标准输出,请执行以下操作: ```go import ( @@ -167,7 +167,7 @@ pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) ### [gops](https://github.com/google/gops) ``` -> go get -u github.com/google/gops +> Go get -u github.com/google/gops ``` 集成到你的程序中: @@ -196,13 +196,13 @@ num CPU: 4 ### [leaktest](https://github.com/fortytw2/leaktest) -这是用测试来自动检测泄露的方法之一。它基本上是在测试的开始和结束的时候,利用 [runtime.Stack](https://golang.org/pkg/runtime/#Stack) 获取活跃 goroutine 的堆栈跟踪。如果在测试完成后还有一些新的 goroutine,那么将其归类为泄露。 +这是用测试来自动检测泄露的方法之一。它基本上是在测试的开始和结束的时候,利用 [runtime.Stack](https://golang.org/pkg/runtime/#Stack) 获取活跃 Goroutine 的堆栈跟踪。如果在测试完成后还有一些新的 goroutine,那么将其归类为泄露。 --- -分析甚至已经在运行的程序的 goroutine 管理,以避免可能会导致内存不足的泄露,这至关重要。代码在生产上运行数日后,这样的问题通常就会出现,因此它可能会造成真正的损害。 +分析甚至已经在运行的程序的 Goroutine 管理,以避免可能会导致内存不足的泄露,这至关重要。代码在生产上运行数日后,这样的问题通常就会出现,因此它可能会造成真正的损害。 -点击原文中的 ❤ 以帮助其他人发现这个问题。如果你想实时获得新的更新,请关注原作者哦~ +点击原文中的 ❤ 以帮助其他人发现这个问题。如果你想实时获得新的更新,请关注原作者哦 ~ ## 资源 @@ -214,19 +214,19 @@ num CPU: 4 gops —— 一个列出和诊断当前运行在你的系统上的 Go 进程的工具。 -* [runtime:检测僵尸 goroutine · 问题 #5308 · golang/go](https://github.com/golang/go/issues/5308) +* [runtime:检测僵尸 Goroutine · 问题 #5308 · golang/go](https://github.com/golang/go/issues/5308) - runtime 可以检测不可达 channel / mutex 等上面的 goroutine 阻塞,然后报告此类问题。这需要一个接口…… + runtime 可以检测不可达 channel / mutex 等上面的 Goroutine 阻塞,然后报告此类问题。这需要一个接口…… * [fortytw2/leaktest](https://github.com/fortytw2/leaktest) - leaktest - goroutine 泄露检测器。 + leaktest - Goroutine 泄露检测器。 --- via: https://medium.com/golangspec/goroutine-leak-400063aef468 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[ictar](https://github.com/ictar) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/goroutines-and-channels-arent-free.md b/published/tech/goroutines-and-channels-arent-free.md index 89401e679..e750572e7 100644 --- a/published/tech/goroutines-and-channels-arent-free.md +++ b/published/tech/goroutines-and-channels-arent-free.md @@ -1,8 +1,8 @@ 已发布:https://studygolang.com/articles/12529 -# goroutine 和 channel 不可滥用 +# Goroutine 和 channel 不可滥用 -我以前觉得使用 goroutine 和 channel 的性能开销是基本忽略不计的--尤其是和 IO 的性能开销相比--但是最近我做了一个实验,实际验证了下。 +我以前觉得使用 Goroutine 和 channel 的性能开销是基本忽略不计的--尤其是和 IO 的性能开销相比--但是最近我做了一个实验,实际验证了下。 我在给[我的课程项目](https://bradfieldcs.com/courses/databases/)做一个[玩具相关的数据库](https://github.com/robot-dreams/zdb2)。一开始,我从 CSV 文件里加载数据表,后来我需要添加一个二进制的表格结构。不幸的是,第一次尝试(加载二进制表格)的效果比加载 CSV 文件差远了。 @@ -13,7 +13,7 @@ $ ./binary_scan_benchmark -path table.bt Done scanning 20000263 records after 27.01220384s ``` -扫描二进制表格的时间慢了两倍!这也太反常了,因为二进制表格结构更简单,不需要字符串转换。 幸好,我用了[几个不错的 go 的性能测试工具](https://blog.golang.org/profiling-go-programs)研究了一下这个问题。 +扫描二进制表格的时间慢了两倍!这也太反常了,因为二进制表格结构更简单,不需要字符串转换。 幸好,我用了[几个不错的 Go 的性能测试工具](https://blog.golang.org/profiling-go-programs)研究了一下这个问题。 扫描 CSV 表格的 CPU profile 看起来比较合理,大部分时间浪费在 IO 相关的系统调用上; @@ -25,7 +25,7 @@ Done scanning 20000263 records after 27.01220384s ![](https://raw.githubusercontent.com/studygolang/gctt-images/master/goroutine-channel/1_w6cWW8qfyDAESzsEEb3vYA.png) > 扫描二进制表格的 CPU profile(第一次实验结果) -原来,这个不合理的 CPU profile ,是由于我使用了 go 的并发原型。 当时我想用 goroutine 和 channel 把生产者和消费者解耦,简化 scanner 的代码结构。 +原来,这个不合理的 CPU profile ,是由于我使用了 Go 的并发原型。 当时我想用 Goroutine 和 channel 把生产者和消费者解耦,简化 scanner 的代码结构。 创建 scanner 启动生产者 goroutine,用来实现 IO 操作/解码,给 channel 返回结果: @@ -57,7 +57,7 @@ func (s *binaryScan) NextRecord() (Record, error) { 可是,从 CPU profile 看出,在这一步,goroutine 花了大量时间阻塞在 channel 操作上,go 在运行时浪费了大量资源在调度/并发原型上。 -我重写了二进制表格扫描代码片段,直接在消费者 goroutine 上做了所有操作: +我重写了二进制表格扫描代码片段,直接在消费者 Goroutine 上做了所有操作: ```go func NewBinaryScan(path string) (*binaryScan, error) { @@ -84,7 +84,7 @@ Done scanning 20000263 records after 8.160765247s 比读取 CSV 文件快 2 倍多,比第一次实验快 3 倍多,这还差不多! -我一直以为合理的使用 goroutine 和 channel 可以使代码简洁,在大部分情况下,不可能是性能问题的根本原因。但是这次实验给我提了个醒,就是 go 的超级并发模型不能随便滥用。 +我一直以为合理的使用 Goroutine 和 channel 可以使代码简洁,在大部分情况下,不可能是性能问题的根本原因。但是这次实验给我提了个醒,就是 Go 的超级并发模型不能随便滥用。 **[校订]** @@ -99,7 +99,7 @@ Done scanning 20000263 records after 8.160765247s 看来我们又学到一点:channel 缓冲大小和 `select` 语句的复杂度都对性能都有很大影响。 -**[校对2]** +**[校对 2]** 感谢 [Stuart Carnie](https://medium.com/@stuartcarnie) 的建议,他建议通过 channel 同时发送批次记录,而不是一次只发送一条!下面是我使用不同的批次大小得到的 benchmark 结果: diff --git a/published/tech/heraldry-generation/20180312-Heraldry-Generation-Pt-1-Basics.md b/published/tech/heraldry-generation/20180312-Heraldry-Generation-Pt-1-Basics.md index ddfda0524..3be458148 100644 --- a/published/tech/heraldry-generation/20180312-Heraldry-Generation-Pt-1-Basics.md +++ b/published/tech/heraldry-generation/20180312-Heraldry-Generation-Pt-1-Basics.md @@ -21,7 +21,7 @@ 纹章的生成坚持必要的原则,盾型的徽章基本上如同现在的路标,目的是清楚地指明方向。于是我为这个构造器假定了一些规则: 1. 如果盾徽的寓意物是金属,那么它的主背景将不会是金属,同样的也不会有颜色; - 2. 只考虑欧洲10世纪-16世纪的背景和寓意物元素; + 2. 只考虑欧洲 10 世纪-16 世纪的背景和寓意物元素; 3. 徽章将包含该时代的色彩、金属、皮草等元素,英格兰的“血色”着色将不会考虑。 目前来说这是指导性的规则,后期我可能会修改它。 diff --git a/published/tech/how-to-add-a-gui-to-your-golang-app.md b/published/tech/how-to-add-a-gui-to-your-golang-app.md index ca8fecf83..63d38eb77 100644 --- a/published/tech/how-to-add-a-gui-to-your-golang-app.md +++ b/published/tech/how-to-add-a-gui-to-your-golang-app.md @@ -4,7 +4,7 @@ 创建一个 Golang app 是一件简单又轻松的事情,但是有时候你想给你的应用锦上添花:创建一个 GUI! -在本篇文章中,我将通过使用 astilectron 工具中的 bootstrap 以及 bundler 给一个简单的 Golang 程序添加 GUI。 +在本篇文章中,我将通过使用 astilectron 工具中的 Bootstrap 以及 bundler 给一个简单的 Golang 程序添加 GUI。 我们的带有 GUI 的 Golang app 能够打开一个文件夹并且展示其中的内容。 @@ -49,7 +49,7 @@ ### Go -首先我们需要在 `main.go` 中导入 [astilectron](https://github.com/asticode/go-astilectron) 的 bootstrap 源码包 : +首先我们需要在 `main.go` 中导入 [astilectron](https://github.com/asticode/go-astilectron) 的 Bootstrap 源码包 : ```go package main @@ -104,16 +104,16 @@ func main() { Width: astilectron.PtrInt(700), }, }); err != nil { - astilog.Fatal(errors.Wrap(err, "running bootstrap failed")) + astilog.Fatal(errors.Wrap(err, "running Bootstrap failed")) } } ``` 2 个全局变量 `AppName` 和 `BuiltAt` 将会通过 [bundler](https://github.com/asticode/go-astilectron-bundler) 打包自动添加进去。 -随后我们将发现我们的主页变成了 `index.html` ,我们将有一个含有 2 个项目( `about` 和 `close` )的菜单并且会出现一个 `700x700` , `中心对齐的` , `#333` 背景色的窗口。 +随后我们将发现我们的主页变成了 `index.html` ,我们将有一个含有 2 个项目( `about` 和 `close` )的菜单并且会出现一个 `700x700` , ` 中心对齐的 ` , `#333` 背景色的窗口。 -我们要在 go 上添加 `debug` 选项,因为我们需要使用 HTML/JS/CSS 调试工具。 +我们要在 Go 上添加 `debug` 选项,因为我们需要使用 HTML/JS/CSS 调试工具。 最后我们将指向 `astilectron.Window` 的指针存入全局变量 `w`,以备后续在使用 `OnWait` 选项时,它包含一个在窗口、菜单及其他所有对象被创建时立即执行的回调函数。 @@ -155,7 +155,7 @@ func main() { ``` -这里没什么特殊的地方,我们声明我们的 `css` 和 `js` 文件,我们设置 html 文件结构并且需要确保我们的 `js` 脚本通过 `index.init()` 进行了初始化 +这里没什么特殊的地方,我们声明我们的 `css` 和 `js` 文件,我们设置 HTML 文件结构并且需要确保我们的 `js` 脚本通过 `index.init()` 进行了初始化 ### CSS @@ -260,7 +260,7 @@ document.addEventListener('astilectron-ready', function() { }) ``` -同时我们在 Go 中监听来自 Javascript 的消息,并且通过 bootstrap 的 `MessageHandler` 给 Javascript 发送消息: +同时我们在 Go 中监听来自 Javascript 的消息,并且通过 Bootstrap 的 `MessageHandler` 给 Javascript 发送消息: ```go func main() { @@ -285,7 +285,7 @@ func handleMessages(_ *astilectron.Window, m bootstrap.MessageIn) (payload inter } ``` -这是一个简单的例子,将在 js 的输出中打印出 `received hello world` 。 +这是一个简单的例子,将在 JS 的输出中打印出 `received hello world` 。 在这种情形中,我们需要更多的逻辑因为我们想要允许打开一个文件夹并且展示其中的内容。 @@ -641,10 +641,10 @@ let index = { 首先我们通过下面命令进行安装: ``` -$ go get -u github.com/asticode/go-astilectron-bundler/... +$ Go get -u github.com/asticode/go-astilectron-bundler/... ``` -然后我们在 `main.go` 中给 bootstrap 添加配置项: +然后我们在 `main.go` 中给 Bootstrap 添加配置项: ```go func main() { @@ -682,7 +682,7 @@ $ astilectron-bundler -v ## 结论 -感谢 astilectron 的 bootstrap 和 bundler ,有了一点点的组织和结构,给你的 Golang 程序添加 GUI 从未如此简单。 +感谢 astilectron 的 Bootstrap 和 bundler ,有了一点点的组织和结构,给你的 Golang 程序添加 GUI 从未如此简单。 需要指出的是这种方法有 2 个主要的缺点: diff --git a/published/tech/how-to-sign-messages-in-java-and-verify.md b/published/tech/how-to-sign-messages-in-java-and-verify.md index 23454d9c0..139292219 100644 --- a/published/tech/how-to-sign-messages-in-java-and-verify.md +++ b/published/tech/how-to-sign-messages-in-java-and-verify.md @@ -4,9 +4,9 @@ 在我的公司中,我们使用 Java 和 Go 作为开发平台,当然有时候这些项目彼此之间会进行交互。在这篇文章中,我想要介绍我们的关于在 Java 端进行消息签名并在 Go 服务程序中进行验证的解决方案。 -首先,我们聊一聊下面这个架构,我们的 Java 应用程序运行在云上新建虚拟机实例中,并且这个基础镜像实例包含了一个小的 Go 服务程序。这个服务程序是我们的配置管理系统的主入口,我们不希望有来自不可信的客户端可以修改节点。在请求中包含签名的双向 SSL 看起来足以信任客户端。但由于这两个组件都是开源的,所以我们在二进制文件中没有任何“秘密”,因此我们选择了RSA非对称秘钥对来生成和验证签名。Java 端拥有私钥,Go 端拥有公钥。 +首先,我们聊一聊下面这个架构,我们的 Java 应用程序运行在云上新建虚拟机实例中,并且这个基础镜像实例包含了一个小的 Go 服务程序。这个服务程序是我们的配置管理系统的主入口,我们不希望有来自不可信的客户端可以修改节点。在请求中包含签名的双向 SSL 看起来足以信任客户端。但由于这两个组件都是开源的,所以我们在二进制文件中没有任何“秘密”,因此我们选择了 RSA 非对称秘钥对来生成和验证签名。Java 端拥有私钥,Go 端拥有公钥。 -Java 是一个古老的平台(个人有多年的Java经验)因此,Java 有很多的库,但是我开始使用Go。我没有第六感,但我认为 Go 应该是支持协议的列表中最弱的。好消息是, Go 有一个内置的 crypto/rsa 软件包,坏消息是,它只支持 PKCS#1。在研究期间,我发现了一个支持 PKCS#8 的第三方库,我们不得不在这个计划点上停下来并重点考察: +Java 是一个古老的平台(个人有多年的 Java 经验)因此,Java 有很多的库,但是我开始使用 Go。我没有第六感,但我认为 Go 应该是支持协议的列表中最弱的。好消息是, Go 有一个内置的 crypto/rsa 软件包,坏消息是,它只支持 PKCS#1。在研究期间,我发现了一个支持 PKCS#8 的第三方库,我们不得不在这个计划点上停下来并重点考察: 1. 使用在较老的标准上建立的,经过良好测试的库 2. 使用在新的标准上的未知的库 @@ -153,7 +153,7 @@ public static String generateSignature(String privateKeyPem, byte[] data) { } private static String clarifyPemKey(String rawPem) { - return "-----BEGIN RSA PRIVATE KEY-----\n" + rawPem.replaceAll("-----(.*)-----|\n", "") + "\n-----END RSA PRIVATE KEY-----"; // PEMParser nem kedveli a sortöréseket + return "-----BEGIN RSA PRIVATE KEY-----\n" + rawPem.replaceAll("-----(.*)-----|\n", "") + "\n-----END RSA PRIVATE KEY-----"; // PEMParser nem kedveli a sort ö r é seket } ``` @@ -165,10 +165,10 @@ ps: 我不为你介绍如何使用 Java 生成秘钥对,因为你可以在 via:https://mhmxs.blogspot.hk/2018/03/how-to-sign-messages-in-java-and-verify.html -作者:[Richárd Kovács](https://mhmxs.blogspot.hk/) +作者:[Rich á rd Kov á cs](https://mhmxs.blogspot.hk/) 译者:[fredvence](https://github.com/fredvence) 校对:[polaris1119](https://github.com/polaris1119) -本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go中文网](https://studygolang.com/) 荣誉推出 +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/https-proxies-support-in-go-1-10.md b/published/tech/https-proxies-support-in-go-1-10.md index a2153dc32..6a217fa3e 100644 --- a/published/tech/https-proxies-support-in-go-1-10.md +++ b/published/tech/https-proxies-support-in-go-1-10.md @@ -6,9 +6,9 @@ Go1.9 出来后 6 个多月的时间,Go1.10 就被[发布](https://blog.golang ## Server -为了验证这一改变,首先请用 golang 启动一个简单的 HTTP(S) 代理服务器。具体做法可以从下面文章了解。 +为了验证这一改变,首先请用 Golang 启动一个简单的 HTTP(S) 代理服务器。具体做法可以从下面文章了解。 -[HTTP(S) Proxy in Golang in less than 100 lines of code](https://medium.com/@mlowicki/http-s-proxy-in-golang-in-less-than-100-lines-of-code-6a51c2f2c38c) +[HTTP(S) Proxy in Golang in Less than 100 lines of code](https://medium.com/@mlowicki/http-s-proxy-in-golang-in-less-than-100-lines-of-code-6a51c2f2c38c) ## Client ```go @@ -68,7 +68,7 @@ panic:Get https://google.com:malformed HTTP response "\x15\x03\x01\x00\x02\x02\x ------------ via: https://medium.com/@mlowicki/https-proxies-support-in-go-1-10-b956fb501d6b -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[zhaohj1118](https://github.com/zhaohj1118) 校对:[rxcai](https://github.com/rxcai) diff --git a/published/tech/job-queues-in-go.md b/published/tech/job-queues-in-go.md index e7a1c195c..d37453217 100644 --- a/published/tech/job-queues-in-go.md +++ b/published/tech/job-queues-in-go.md @@ -4,7 +4,7 @@ 在 [RapidLoop](https://www.rapidloop.com/) 中,我们几乎用 [Go](https://golang.org) 做所有事情,包括我们的服务器,应用服务和监控系统 [OpsDash](https://www.opsdash.com/)。 -Go 十分擅长编写异步程序 - goroutine 和 channel 使用十分简单不容易出错并且和其他语言相比异步/等待模式,语法和功能都更加强大。请继续阅读来瞧瞧围绕任务队列的一些有趣的 Go 代码。 +Go 十分擅长编写异步程序 - Goroutine 和 channel 使用十分简单不容易出错并且和其他语言相比异步/等待模式,语法和功能都更加强大。请继续阅读来瞧瞧围绕任务队列的一些有趣的 Go 代码。 ## 不使用任务队列 @@ -108,7 +108,7 @@ for job := range jobChan {...} ## 等待 worker 处理 -这看起来很容易,不过 `close(jobChan)` 不会等待 goroutine 完成就会退出。因此我们还需使用 sync.WaitGroup: +这看起来很容易,不过 `close(jobChan)` 不会等待 Goroutine 完成就会退出。因此我们还需使用 sync.WaitGroup: ```go // use a WaitGroup @@ -135,11 +135,11 @@ wg.Wait() 这样,我们可以通过关闭 channel 给 worker 发送关闭信号并使用 wg.Wait 会等待 worker 处理完成以后才会退出。 -注意:我们必须在开始 goroutine 之前递增 wait group,并且在 goroutine 结束(不管以何种方式)时递减。 +注意:我们必须在开始 Goroutine 之前递增 wait group,并且在 Goroutine 结束(不管以何种方式)时递减。 ## 附带超时的等待 -`wg.Wait()` 会在 goroutine 退出前一直等待。但是如果我们无法无限期的等待怎么办? +`wg.Wait()` 会在 Goroutine 退出前一直等待。但是如果我们无法无限期的等待怎么办? 如下帮助函数封装了 `wg.Wait()` 增加了超时时间: @@ -177,7 +177,7 @@ WaitTimeout(&wg, 5 * time.Second) // create a context that can be cancelled ctx, cancel := context.WithCancel(context.Background()) -// start the goroutine passing it the context +// start the Goroutine passing it the context go worker(ctx, jobChan) func worker(ctx context.Context, jobChan <-chan Job) { @@ -265,7 +265,7 @@ cancel() // create a cancel channel cancelChan := make(chan struct{}) -// start the goroutine passing it the cancel channel +// start the Goroutine passing it the cancel channel go worker(jobChan, cancelChan) func worker(jobChan <-chan Job, cancelChan <-chan struct{}) { diff --git a/published/tech/learning-go-as-a-nodejs-developer.md b/published/tech/learning-go-as-a-nodejs-developer.md index 37dc2ce5c..f74199e69 100644 --- a/published/tech/learning-go-as-a-nodejs-developer.md +++ b/published/tech/learning-go-as-a-nodejs-developer.md @@ -30,18 +30,18 @@ func main() { ## Go 的依赖管理 如果打算写大量的 JavaScript 代码,首要问题就是依赖管理问题,Go 是怎么处理的呢?有两个办法: -- go get +- Go get - dep -用 npm 的术语来说,你可以把他们看作是:在你需要使用 npm install -g 的时候,使用 go get,然后使用 dep 来管理不同项目的依赖。 +用 NPM 的术语来说,你可以把他们看作是:在你需要使用 NPM install -g 的时候,使用 Go get,然后使用 dep 来管理不同项目的依赖。 -要安装 dep,可以通过 go get 来安装,使用如下命令: +要安装 dep,可以通过 Go get 来安装,使用如下命令: ``` go get -u github.com/golang/dep/cmd/dep ``` -然而,使用 go get 有一个缺点 —— go get 并不处理版本,它仅仅是获取 Github 仓库的最新版本。这就是为什么推荐大家安装并使用 dep。如果是 Mac 系统,安装 dep 也可以通过如下命令: +然而,使用 Go get 有一个缺点 —— Go get 并不处理版本,它仅仅是获取 Github 仓库的最新版本。这就是为什么推荐大家安装并使用 dep。如果是 Mac 系统,安装 dep 也可以通过如下命令: ```go brew install dep @@ -49,11 +49,11 @@ brew upgrade dep ``` (如果是其他操作系统,安装请参见: https://golang.org/doc/install) -一旦安装了 dep,你就可以使用 **dep init** 来初始化项目,就好像使用 **npm init** 初始化 nodejs 项目一样。 +一旦安装了 dep,你就可以使用 **dep init** 来初始化项目,就好像使用 **npm init** 初始化 Node.js 项目一样。 -> 开发Go项目之前,你需要花点时间设置好GOPATH环境变量。—— [官方指导链接](https://golang.org/doc/install) +> 开发 Go 项目之前,你需要花点时间设置好 GOPATH 环境变量。—— [官方指导链接](https://golang.org/doc/install) -dep 会像 npm 一样,创建一个 Node.js 项目中类似 package.json 的文件来描述工程 —— Gopkg.toml。类似 package-lock.json,也会有一个 Gopkg.lock 文件。不同于 nodejs 项目将依赖放入 node modules 文件夹中,dep 将依赖放入一个叫作 vendor 的文件夹中。 +dep 会像 NPM 一样,创建一个 Node.js 项目中类似 package.json 的文件来描述工程 —— Gopkg.toml。类似 package-lock.json,也会有一个 Gopkg.lock 文件。不同于 Node.js 项目将依赖放入 node modules 文件夹中,dep 将依赖放入一个叫作 vendor 的文件夹中。 要添加依赖,你只需要运行 dep ensure -add github.com/pkg/errors 命令。运行结束后,这个依赖就会出现在 lock 和 toml 文件中: @@ -63,7 +63,7 @@ dep 会像 npm 一样,创建一个 Node.js 项目中类似 package.json 的文 version = "0.8.0" ``` -## Go处理异步操作 +## Go 处理异步操作 当用 JavaScript 写异步代码时,我们会用到一些库或者语言特性,比如: - async 库 @@ -122,7 +122,7 @@ func main() { 上面的例子是可以正常运行的,但是读取文件是一个接一个读取。—— 让我们来稍加改进,将它异步化吧! -Go有一个叫作 *goroutines* 的概念来处理多线程。一个 *goroutine* 是一个轻量级的线程,它由 Go runtime 来管理。*goroutine* 使得你可以并发地跑Go的函数。 +Go 有一个叫作 *goroutines* 的概念来处理多线程。一个 *goroutine* 是一个轻量级的线程,它由 Go runtime 来管理。*goroutine* 使得你可以并发地跑 Go 的函数。 我最终使用 *errgroup* 包来管理或者说同步 *goroutines*。这个包提供同步机制,错误传播,以及对同一个由一组 *goroutines* 子任务组成的公共任务提供上下文取消机制。 @@ -183,9 +183,9 @@ import "github.com/gin-gonic/gin" func main() { // 创建默认不带任何中间件的路由 r := gin.New() - // 默认gin的输出为标准输出 + // 默认 gin 的输出为标准输出 r.Use(gin.Logger()) - // Recovery中间件从异常中恢复,并回复500 + // Recovery 中间件从异常中恢复,并回复 500 r.Use(gin.Recovery()) r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ diff --git a/published/tech/making-debugger-for-golang/part1.md b/published/tech/making-debugger-for-golang/part1.md index 42fdcec35..168456d40 100644 --- a/published/tech/making-debugger-for-golang/part1.md +++ b/published/tech/making-debugger-for-golang/part1.md @@ -54,13 +54,13 @@ RUN apt-get update && apt-get install -y tree [这里](https://docs.docker.com/engine/security/seccomp/) 有安全运算模式(seccomp)的相关描述。现在剩下的是在容器里编译这这两个程序。第一个可以这样做: ``` -> go install --gcflags="-N -l" github.com/mlowicki/hello +> Go install --gcflags="-N -l" github.com/mlowicki/hello ``` 标识 --gcflag 用于禁止 [内联函数](https://en.wikipedia.org/wiki/Inline_expansion) (-l),编译优化(-N)可以让调试更容易。调试器如下做编译: ``` -> go install github.com/mlowicki/debugger +> Go install github.com/mlowicki/debugger ``` @@ -196,8 +196,8 @@ func main() { log.Printf("Exited: %v\n", ws.Exited()) log.Printf("Exit status: %v\n", ws.ExitStatus()) } -> go install -gcflags="-N -l" github.com/mlowicki/hello -> go install github.com/mlowicki/debugger +> Go install -gcflags="-N -l" github.com/mlowicki/hello +> Go install github.com/mlowicki/debugger > debugger /go/bin/hello 2017/05/05 20:09:38 State: stop signal: trace/breakpoint trap 2017/05/05 20:09:38 Restarting... @@ -218,7 +218,7 @@ hello world via: https://medium.com/golangspec/making-debugger-for-golang-part-i-53124284b7c8 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[ArisAries](https://github.com/ArisAries) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/making-debugger-for-golang/part2.md b/published/tech/making-debugger-for-golang/part2.md index 7466e2fd2..dfc34fdfa 100644 --- a/published/tech/making-debugger-for-golang/part2.md +++ b/published/tech/making-debugger-for-golang/part2.md @@ -70,8 +70,8 @@ func main() { 构建并运行这个段代码,输出应该像下面这样(每次调用显示的步数可能不一样) ``` -> go install -gcflags="-N -l" github.com/mlowicki/hello -> go install github.com/mlowicki/debugger +> Go install -gcflags="-N -l" github.com/mlowicki/hello +> Go install github.com/mlowicki/debugger > debugger /go/bin/hello 2017/06/09 19:54:42 State: stop signal: trace/breakpoint trap hello world @@ -200,8 +200,8 @@ func main() { 那么输出应该是这样的 ```shell -> go install github.com/mlowicki/linetable -> go install — gcflags=”-N -l” github.com/mlowicki/hello +> Go install github.com/mlowicki/linetable +> Go install — gcflags=”-N -l” github.com/mlowicki/hello > linetable /go/bin/hello 2017/06/30 18:47:38 filename: /go/src/github.com/mlowicki/hello/hello.go 2017/06/30 18:47:38 lineno: 5 @@ -223,7 +223,7 @@ ELF 中包含许多段,我们用到了其中三个:.text、.gopclntab 和 .g via: https://medium.com/golangspec/making-debugger-in-golang-part-ii-d2b8eb2f19e0 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[jettyhan](https://github.com/jettyhan) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/making-debugger-for-golang/part3.md b/published/tech/making-debugger-for-golang/part3.md index 7c3679cad..ec4dcae43 100644 --- a/published/tech/making-debugger-for-golang/part3.md +++ b/published/tech/making-debugger-for-golang/part3.md @@ -39,7 +39,7 @@ mov rdi, 1 INT 3 指令会生成大小为一个字节的特殊操作码(CC),通过它可以调用异常处理函数,(这个操作码非常有用,因为它可以用来替换任何一条指令的第一个字节,使之成为一个断点,然后再加入额外的一个字节,而不影响其它的代码),具体信息参见以下文档 -[Intel® 64 and IA-32 系统软件使用手册](https://software.intel.com/en-us/articles/intel-sdm) +[Intel ® 64 and IA-32 系统软件使用手册](https://software.intel.com/en-us/articles/intel-sdm) 我们用 0xCC 来替换特定指令的头一个字节,使之成为一个断点,一旦这个断点被出发,我们就可以做以下的事情 @@ -177,7 +177,7 @@ func main() { 源文件以一些辅助函数开始,setPC 和 getPC 用来维护 [程序计数器](https://en.wikipedia.org/wiki/Program_counter),寄存器 PC 存放的是下一条要执行的指令。如果程序在没有执行任何指令的时候被暂停,PC 中的值就是程序第一条指令的内存地址。维护断点的函数(setBreakpoint 和 clearBreakpoint)负责在指令中插入或者移除操作码 0xCC,下面是程序的输出: ```shell -> go install github.com/mlowicki/breakpoint +> Go install github.com/mlowicki/breakpoint > breakpoint /go/bin/hello 2017/07/16 21:06:33 State: stop signal: trace/breakpoint trap 2017/07/16 21:06:33 RAX=1, RDI=0 @@ -267,7 +267,7 @@ func main() { via: https://medium.com/golangspec/making-debugger-in-golang-part-iii-5aac8e49f291 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[jettyhan](https://github.com/jettyhan) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/methods-in-go/20161102-part1.md b/published/tech/methods-in-go/20161102-part1.md index 4b57e7066..2547fd62b 100644 --- a/published/tech/methods-in-go/20161102-part1.md +++ b/published/tech/methods-in-go/20161102-part1.md @@ -12,7 +12,7 @@ func (t T) PrintName() { fmt.Println(t.name) } func main() { - t := T{name: "Michał"} + t := T{name: "Micha ł"} t.PrintName() } ``` @@ -34,7 +34,7 @@ func (*T) F() {} 上面的代码无法编译. 第一个 F 方法被绑到 T 上. 第二各方法被邦到 *T 上. 对于单个基类型,方法名必须唯一,所以编译将报错: ``` -> go install github.com/mlowicki/lab && ./spec/bin/lab +> Go install github.com/mlowicki/lab && ./spec/bin/lab # github.com/mlowicki/lab spec/src/github.com/mlowicki/lab/lab.go:103: method redeclared: T.F method(T) func() @@ -218,7 +218,7 @@ G 的声明省略无用标识符。 via: https://medium.com/golangspec/methods-in-go-part-i-a4e575dff860 -作者:[Michał Łowicki](https://medium.com/@mlowicki) +作者:[Micha ł Ł owicki](https://medium.com/@mlowicki) 译者:[themoonbear](https://github.com/themoonbear) 校对:[polaris1119](https://github.com/polaris1119) diff --git a/published/tech/profiling-and-optimizing-go-web-applications.md b/published/tech/profiling-and-optimizing-go-web-applications.md index a5a9db03e..e1e309949 100644 --- a/published/tech/profiling-and-optimizing-go-web-applications.md +++ b/published/tech/profiling-and-optimizing-go-web-applications.md @@ -1,12 +1,12 @@ 已发布:https://studygolang.com/articles/12685 -# 剖析与优化 Go 的 web 应用 +# 剖析与优化 Go 的 Web 应用 原文发表日期: 2017/3/13 关键字: `dev` `go` `golang` `pprof` -Go 语言有一个很强大的内置分析器(profiler),支持CPU、内存、协程 与 阻塞/抢占(block/contention)的分析。 +Go 语言有一个很强大的内置分析器(profiler),支持 CPU、内存、协程 与 阻塞/抢占(block/contention)的分析。 ## 开启分析器(profiler) @@ -32,7 +32,7 @@ func main() { } ``` -如果你的 web 应用使用自定义的 URL 路由,你需要手动注册一些 HTTP 端点(endpoints) 。 +如果你的 Web 应用使用自定义的 URL 路由,你需要手动注册一些 HTTP 端点(endpoints) 。 ```go package main @@ -61,13 +61,13 @@ func main() { } ``` -如上代码那样,开启 web 应用,然后使用 pprof 工具: +如上代码那样,开启 Web 应用,然后使用 pprof 工具: ```shell go tool pprof [binary] http://127.0.0.1:8080/debug/pprof/profile ``` -pprof 的最大的优点之一是它是的性能负载很小,可以在生产环境中使用,不会对 web 请求响应造成明显的性能消耗。 +pprof 的最大的优点之一是它是的性能负载很小,可以在生产环境中使用,不会对 Web 请求响应造成明显的性能消耗。 但是在深入挖掘 pprof 之前,我们需要一个真实案例来展示如何在 GO 应用中检查并解决性能问题。 @@ -108,17 +108,17 @@ Requests per second: 22810.15 [#/sec] (mean) Time per request: 0.042 [ms] (mean, across all concurrent requests) ``` -注:上面的测试结果的执行环境:笔记本 MacBook Pro Late 2013 (2.6 GHz Intel Core i5, 8 GB 1600 MHz DDR3, macOS 10.12.3) , Go编译器版本是1.8 。 +注:上面的测试结果的执行环境:笔记本 MacBook Pro Late 2013 (2.6 GHz Intel Core i5, 8 GB 1600 MHz DDR3, macOS 10.12.3) , Go 编译器版本是 1.8 。 ## CPU 分析(CPU profile) -再次执行 Apache benchmark tool ,但这次使用更高的请求数量(1百万应该足够了),并同时执行 pprof : +再次执行 Apache benchmark tool ,但这次使用更高的请求数量(1 百万应该足够了),并同时执行 pprof : ```shell go tool pprof goprofex http://127.0.0.1:8080/debug/pprof/profile ``` -这个 CPU profiler 默认执行30秒。它使用采样的方式来确定哪些函数花费了大多数的CPU时间。Go runtime 每10毫秒就停止执行过程并记录每一个运行中的协程的当前堆栈信息。 +这个 CPU profiler 默认执行 30 秒。它使用采样的方式来确定哪些函数花费了大多数的 CPU 时间。Go runtime 每 10 毫秒就停止执行过程并记录每一个运行中的协程的当前堆栈信息。 当 pprof 进入交互模式,输入 `top`,这条命令会展示收集样本中最常出现的函数列表。在我们的案例中,是所有 runtime 与标准库函数,这不是很有用。 @@ -317,7 +317,7 @@ go tool pprof goprofex http://127.0.0.1:8080/debug/pprof/goroutine ![](https://github.com/studygolang/gctt-images/raw/master/profiling-and-optimizing-go-web-applications/web-goroutine.png) -上图只有18个活跃中的协程,这是非常小的数字。拥有数千个运行中的协程的情况并不少见,但并不会显著降低性能。 +上图只有 18 个活跃中的协程,这是非常小的数字。拥有数千个运行中的协程的情况并不少见,但并不会显著降低性能。 ## 阻塞分析(Block profile) @@ -381,7 +381,7 @@ BenchmarkStatsD-4 1000000 1516 ns/op 560 B/op ### Logging -让应用运行更快,一个很好又不是经常管用的方法是,让它执行更少的工作。除了 debug 的目的之外,这行代码 `log.Printf("%s request took %v", name, elapsed)` 在 web service 中不需要。所有非必要的 logs 应该在生产环境中被移除代码或者关闭功能。可以使用分级日志(a leveled logger)来解决这个问题,比如这些很棒的 [日志工具库(logging libraries)](https://github.com/avelino/awesome-go#logging) +让应用运行更快,一个很好又不是经常管用的方法是,让它执行更少的工作。除了 debug 的目的之外,这行代码 `log.Printf("%s request took %v", name, elapsed)` 在 Web service 中不需要。所有非必要的 logs 应该在生产环境中被移除代码或者关闭功能。可以使用分级日志(a leveled logger)来解决这个问题,比如这些很棒的 [日志工具库(logging libraries)](https://github.com/avelino/awesome-go#logging) 关于打日志或者其他一般的 I/O 操作,另一个重要的事情是尽可能使用有缓冲的输入输出(buffered input/output),这样可以减少系统调用的次数。通常,并不是每个 logger 调用都需要立即写入文件 —— 使用 [bufio](https://golang.org/pkg/bufio/) package 来实现 buffered I/O 。我们可以使用 `bufio.NewWriter` 或者 `bufio.NewWriterSize` 来简单地封装 `io.Writer` 对象,再传递给 logger : @@ -466,7 +466,7 @@ func (s *StatsD) Send(stat string, kind string, delta float64) { } ``` -这样做,将分配数量(number of allocations)从14减少到1个,并且使 `Send` 运行快了4倍。 +这样做,将分配数量(number of allocations)从 14 减少到 1 个,并且使 `Send` 运行快了 4 倍。 ``` BenchmarkStatsD-4 5000000 381 ns/op 112 B/op 1 allocs/op @@ -508,7 +508,7 @@ Requests per second: 32619.54 [#/sec] (mean) Time per request: 0.030 [ms] (mean, across all concurrent requests) ``` -这个 web 服务现在可以每秒多处理10000个请求!  +这个 Web 服务现在可以每秒多处理 10000 个请求!  ## 优化技巧 diff --git a/published/tech/the-ultimate-guide-to-writing-dockerfile.md b/published/tech/the-ultimate-guide-to-writing-dockerfile.md index 586ed6276..a9dd7ce69 100644 --- a/published/tech/the-ultimate-guide-to-writing-dockerfile.md +++ b/published/tech/the-ultimate-guide-to-writing-dockerfile.md @@ -28,7 +28,7 @@ RUN xz -d -c /usr/local/upx-3.94-amd64_linux.tar.xz | \ tar -xOf - upx-3.94-amd64_linux/upx > /bin/upx && \ chmod a+x /bin/upx # install glide -RUN go get github.com/Masterminds/glide +RUN Go get github.com/Masterminds/glide # setup the working directory WORKDIR /go/src/app ADD glide.yaml glide.yaml @@ -38,8 +38,8 @@ RUN glide install # add source code ADD src src # build the source -RUN go build src/main.go -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main src/main.go +RUN Go build src/main.go +RUN CGO_ENABLED=0 GOOS=linux Go build -a -installsuffix cgo -o main src/main.go # strip and compress the binary RUN strip --strip-unneeded main RUN upx main @@ -77,7 +77,7 @@ ADD src src CMD ["go", "run", "src/main.go"] ``` -我们使用 `debian jessie` 版本的 golang 镜像,因为像 `go get` 这样的命令需要安装有 `git` 等工具。对于生产版本,我们会用更加轻量的版本,如 `alpine`。 +我们使用 `debian jessie` 版本的 Golang 镜像,因为像 `go get` 这样的命令需要安装有 `git` 等工具。对于生产版本,我们会用更加轻量的版本,如 `alpine`。 构建并运行该镜像: @@ -89,7 +89,7 @@ $ docker run --rm -it -p 8080:8080 go-docker-dev 成功后可以通过 `http://localhost:8080` 来访问。按下 `Ctrl+C` 可以中断服务。 -但这并没有多大意义,因为每次修改代码时,我们都必须构建和运行docker 镜像。 +但这并没有多大意义,因为每次修改代码时,我们都必须构建和运行 docker 镜像。 一个更好的版本是将源代码挂载到 docker 容器中,并使用容器内的 shell 来停止和启动 `go run`。 @@ -98,7 +98,7 @@ $ cd go-docker $ docker build -t go-docker-dev . $ docker run --rm -it -p 8080:8080 -v $(pwd):/go/src/app \ go-docker-dev bash -root@id:/go/src/app# go run src/main.go +root@id:/go/src/app# Go run src/main.go ``` 这个命令会提供一个 shell,我们可以在里面执行 `go run src/main.go` 以启动服务。我们可以在宿主机上编辑 `main.go` 并重新运行该命令来查看变化,因为现在源代码已经直接挂载到了容器中。 @@ -121,7 +121,7 @@ $ touch glide.lock ```bash FROM golang:1.8.5-jessie # install glide -RUN go get github.com/Masterminds/glide +RUN Go get github.com/Masterminds/glide # create a working directory WORKDIR /go/src/app # add glide.yaml and glide.lock @@ -168,14 +168,14 @@ root@id:/go/src/app# glide install ``` ## 实时重载 -[codegangsta/gin](https://github.com/codegangsta/gin) 是我最喜欢的实时重载工具。它简直就是为 Go web 服务而生的。我们使用 `go get` 来安装 gin: +[codegangsta/gin](https://github.com/codegangsta/gin) 是我最喜欢的实时重载工具。它简直就是为 Go Web 服务而生的。我们使用 `go get` 来安装 gin: ```bash FROM golang:1.8.5-jessie # install glide -RUN go get github.com/Masterminds/glide +RUN Go get github.com/Masterminds/glide # install gin -RUN go get github.com/codegangsta/gin +RUN Go get github.com/codegangsta/gin # create a working directory WORKDIR /go/src/app # add glide.yaml and glide.lock @@ -189,28 +189,28 @@ ADD src src CMD ["go", "run", "src/main.go"] ``` -构建镜像并运行 gin 以便当我们修改了 `src` 中的源代码时可以自动重新编译: +构建镜像并运行 Gin 以便当我们修改了 `src` 中的源代码时可以自动重新编译: ```bash $ cd go-docker $ docker build -t go-docker-dev . $ docker run --rm -it -p 8080:8080 -v $(pwd):/go/src/app \ go-docker-dev bash -root@id:/go/src/app# gin --path src --port 8080 run main.go +root@id:/go/src/app# Gin --path src --port 8080 run main.go ``` -注意到 web-server 需要一个 `PORT` 的环境变量来监听,因为 gin 会随机设置 `PORT` 变量并代理到该端口的连接。 +注意到 web-server 需要一个 `PORT` 的环境变量来监听,因为 Gin 会随机设置 `PORT` 变量并代理到该端口的连接。 现在,修改 `src` 目录下的内容会触发重新编译,所有更新的内容可以实时在 `http://localhost:8080` 访问到。 -一旦开发完毕,我们可以构建二进制文件并运行它,而不需要使用 `go run` 命令。可以使用相同的镜像来构建,或者也可以使用 Docker 的多阶段构建,即使用 `golang` 镜像来构建并使用迷你 linux 容器如 `alpine` 来运行服务。 +一旦开发完毕,我们可以构建二进制文件并运行它,而不需要使用 `go run` 命令。可以使用相同的镜像来构建,或者也可以使用 Docker 的多阶段构建,即使用 `golang` 镜像来构建并使用迷你 Linux 容器如 `alpine` 来运行服务。 ## 单阶段生产构建 ```bash FROM golang:1.8.5-jessie # install glide -RUN go get github.com/Masterminds/glide +RUN Go get github.com/Masterminds/glide # create a working directory WORKDIR /go/src/app # add glide.yaml and glide.lock @@ -221,7 +221,7 @@ RUN glide install # add source code ADD src src # build main.go -RUN go build src/main.go +RUN Go build src/main.go # run the binary CMD ["./main"] ``` @@ -242,7 +242,7 @@ $ docker run --rm -it -p 8080:8080 go-docker-prod ```bash FROM golang:1.8.5-jessie as builder # install glide -RUN go get github.com/Masterminds/glide +RUN Go get github.com/Masterminds/glide # setup the working directory WORKDIR /go/src/app ADD glide.yaml glide.yaml @@ -252,8 +252,8 @@ RUN glide install # add source code ADD src src # build the source -RUN go build src/main.go -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main src/main.go +RUN Go build src/main.go +RUN CGO_ENABLED=0 GOOS=linux Go build -a -installsuffix cgo -o main src/main.go # use a minimal alpine image FROM alpine:3.7 @@ -273,7 +273,7 @@ CMD ["./main"] ## 福利:使用 UPX 来压缩二进制文件 -在 [Hasura](https://hasura.io/),我们已经在到处使用 [UPX](https://upx.github.io/) 了,压缩后我们的 CLI 二进制文件从 50 MB 左右降到 8 MB左右,大大加快了下载速度。UPX 可以极快地进行原地解压,不需要额外的工具,因为它将解压器嵌入到了二进制文件内部。 +在 [Hasura](https://hasura.io/),我们已经在到处使用 [UPX](https://upx.github.io/) 了,压缩后我们的 CLI 二进制文件从 50 MB 左右降到 8 MB 左右,大大加快了下载速度。UPX 可以极快地进行原地解压,不需要额外的工具,因为它将解压器嵌入到了二进制文件内部。 ```bash FROM golang:1.8.5-jessie as builder @@ -287,7 +287,7 @@ RUN xz -d -c /usr/local/upx-3.94-amd64_linux.tar.xz | \ tar -xOf - upx-3.94-amd64_linux/upx > /bin/upx && \ chmod a+x /bin/upx # install glide -RUN go get github.com/Masterminds/glide +RUN Go get github.com/Masterminds/glide # setup the working directory WORKDIR /go/src/app ADD glide.yaml glide.yaml @@ -297,8 +297,8 @@ RUN glide install # add source code ADD src src # build the source -RUN go build src/main.go -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main src/main.go +RUN Go build src/main.go +RUN CGO_ENABLED=0 GOOS=linux Go build -a -installsuffix cgo -o main src/main.go # strip and compress the binary RUN strip --strip-unneeded main RUN upx main @@ -333,4 +333,4 @@ via:https://blog.hasura.io/the-ultimate-guide-to-writing-dockerfiles-for-go-we 译者:[ParadeTo](https://github.com/ParadeTo) 校对:[polaris1119](https://github.com/polaris1119) -本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go中文网](https://studygolang.com/) 荣誉推出 +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/published/tech/understanding-go-programs-with-go-parser.md b/published/tech/understanding-go-programs-with-go-parser.md index 71ae84c90..06b02c2d3 100644 --- a/published/tech/understanding-go-programs-with-go-parser.md +++ b/published/tech/understanding-go-programs-with-go-parser.md @@ -74,7 +74,7 @@ func main() { 1:1: expected 'package', found 'var' (and 1 more errors) ``` -为了解析这个我们叫做 `ParseFile` 的声明,我们需要给出一个完整的 go 源文件格式(以 package 作为源文件开头)。 +为了解析这个我们叫做 `ParseFile` 的声明,我们需要给出一个完整的 Go 源文件格式(以 package 作为源文件开头)。 > 注意:注释可以写在 package 前面 @@ -105,7 +105,7 @@ func main() { 运行后输出如下: ``` -$ go run main.go +$ Go run main.go &{ 1 main [0xc420054100] scope 0xc42000e210 { var a } @@ -145,7 +145,7 @@ func main() { 重新运行程序我们会得到更加易读的输出: ``` -$ go run main.go +$ Go run main.go (*ast.File)(0xc42009c000)({ Doc: (*ast.CommentGroup)(), Package: (token.Pos) 1, @@ -339,7 +339,7 @@ func (v visitor) Visit(n ast.Node) ast.Visitor { 执行这段代码我们将会得到所有来自命令行参数的文件的 AST。我们可以试试传入刚刚写的 main.go 文件。 ``` -$ go build -o parser main.go && parser main.go +$ Go build -o parser main.go && parser main.go # output removed for brevity ``` diff --git a/published/tech/what-accept-interfaces-return-structs-means-in-go.md b/published/tech/what-accept-interfaces-return-structs-means-in-go.md index 2933c6dcd..6a6fbbaa1 100644 --- a/published/tech/what-accept-interfaces-return-structs-means-in-go.md +++ b/published/tech/what-accept-interfaces-return-structs-means-in-go.md @@ -1,6 +1,6 @@ 已发布:https://studygolang.com/articles/12397 -# 接受 interface 参数,返回 struct 在 go 中意味着什么 +# 接受 interface 参数,返回 struct 在 Go 中意味着什么 ## 注意细节 @@ -13,7 +13,7 @@ > 除了因为太多的迂回方式所造成的问题之外,所有的计算机科学问题都能够通过另一个级别的迂回方式来解决。 > - David J. Wheeler -软件工程师喜欢抽象。个人看法,我从未看到过一个同事参与写代码超过他为了某个事务建立抽象多。Go 语言从结构中抽象出接口,这种处理方式会产生嵌入复杂性。遵循[你并不需要它](http://c2.com/xp/YouArentGonnaNeedIt.html)软件设计理念,如果不需要就没有理由增加复杂性。一个常见的返回接口的理由是让用户把注意力放在函数所提供的 API 上。在 Go 中因为隐含实现了接口,所以这并不需要。返回结构的公共函数就成为那个API。 +软件工程师喜欢抽象。个人看法,我从未看到过一个同事参与写代码超过他为了某个事务建立抽象多。Go 语言从结构中抽象出接口,这种处理方式会产生嵌入复杂性。遵循[你并不需要它](http://c2.com/xp/YouArentGonnaNeedIt.html)软件设计理念,如果不需要就没有理由增加复杂性。一个常见的返回接口的理由是让用户把注意力放在函数所提供的 API 上。在 Go 中因为隐含实现了接口,所以这并不需要。返回结构的公共函数就成为那个 API。 > 永远只有当你真正需要的时候才抽象,不要因为预见可能会需要而抽象 @@ -66,7 +66,7 @@ Dave Cheney 在写[接口隔离原则](https://en.wikipedia.org/wiki/Interface_s > 依照需求描述的结果也就是函数-仅仅是需要可写并且提供相应的功能 -我会按照这个思想,重新考虑上面的函数 addNumbers,很明显不需要参数 s 字符串,函数 NewUser 同样也不需要一个包括 RemoveUser 的Database参数。 +我会按照这个思想,重新考虑上面的函数 addNumbers,很明显不需要参数 s 字符串,函数 NewUser 同样也不需要一个包括 RemoveUser 的 Database 参数。 ## 总结理由和审查例外情况 diff --git a/published/tech/why-cant-i-pass-this-function-as-an-http-handler.md b/published/tech/why-cant-i-pass-this-function-as-an-http-handler.md index 6cb6dcaff..39a62ebb2 100644 --- a/published/tech/why-cant-i-pass-this-function-as-an-http-handler.md +++ b/published/tech/why-cant-i-pass-this-function-as-an-http-handler.md @@ -51,7 +51,7 @@ func main() { } func handler(w http.ResponseWriter, r *http.Request) { - // handler func的实现内容 + // handler func 的实现内容 } ``` diff --git a/published/tech/your-pprof-is-showing.md b/published/tech/your-pprof-is-showing.md index 9054f1b97..ced53adef 100644 --- a/published/tech/your-pprof-is-showing.md +++ b/published/tech/your-pprof-is-showing.md @@ -6,9 +6,9 @@ IPv4 扫描暴露出的 `net/http/pprof` 端点(endpoint) 原文发表日期: 2017/9/27 -Go语言的 [net/http/pprof](https://golang.org/pkg/net/http/pprof/) 包是令人难以置信的强大的,调试正在运行的生产服务器的这个功能微不足道,而在这个调试过程,就很容易不经意间将调试信息暴露给世界。在这篇文章中,我们用 [zmap project](https://github.com/zmap) 作为例子,展示一个现实中真正的问题,并且说明你可以采取的预防措施。 +Go 语言的 [net/http/pprof](https://golang.org/pkg/net/http/pprof/) 包是令人难以置信的强大的,调试正在运行的生产服务器的这个功能微不足道,而在这个调试过程,就很容易不经意间将调试信息暴露给世界。在这篇文章中,我们用 [zmap project](https://github.com/zmap) 作为例子,展示一个现实中真正的问题,并且说明你可以采取的预防措施。 -> 早期版本提出,暴露的端点可能泄露源代码。[Aram Hăvărneanu 指出了这个错误](https://github.com/golang/go/issues/22085#issuecomment-333166626),本文已修正。 +> 早期版本提出,暴露的端点可能泄露源代码。[Aram H ă v ă rneanu 指出了这个错误](https://github.com/golang/go/issues/22085#issuecomment-333166626),本文已修正。 ## 引言 @@ -32,9 +32,9 @@ func main() { } ``` -这个服务不仅会对你说 `Hello World!`,它还会通过uri路径 `/debug/pprof` 返回诊断报告。 +这个服务不仅会对你说 `Hello World!`,它还会通过 uri 路径 `/debug/pprof` 返回诊断报告。 -- `/debug/pprof/profile`: 30秒的CPU状态信息 +- `/debug/pprof/profile`: 30 秒的 CPU 状态信息 - `/debug/pprof/heap`: 内存的堆信息 - `/debug/pprof/goroutine?debug=1`: 所有协程的堆栈踪迹 - `/debug/pprof/trace`: 执行的追踪信息 @@ -43,7 +43,7 @@ func main() { ```shell $ wget -O trace.out http://localhost:8080/debug/pprof/trace -$ go tool trace trace.out +$ Go tool trace trace.out ``` 在几秒钟内,我们以较细的颗粒度地检查服务器 @@ -73,7 +73,7 @@ $ go tool trace trace.out $ zmap -p 6060 | zgrab --port 6060 --http="/debug/pprof/" ``` -[zmap](https://github.com/zmap/zmap) 扫描 IPv4 范围中开启6060端口的服务并调用它,然后 `banner grabber` 的 [zgrab](https://github.com/zmap/zgrab) 采集 HTTP 请求的 `GET /debug pprof` 的响应结果与问题。我们可以认为任意响应为 `200 OK` 的服务器与包含 `goroutine` 的响应体即为命中。下面是我们发现的内容: +[zmap](https://github.com/zmap/zmap) 扫描 IPv4 范围中开启 6060 端口的服务并调用它,然后 `banner grabber` 的 [zgrab](https://github.com/zmap/zgrab) 采集 HTTP 请求的 `GET /debug pprof` 的响应结果与问题。我们可以认为任意响应为 `200 OK` 的服务器与包含 `goroutine` 的响应体即为命中。下面是我们发现的内容: - 至少有 69 个 IP 使用 `pprof` 开启了 6060 端口 - 同上,至少有 70 个 IP 开启了 8080 端口 @@ -90,19 +90,19 @@ $ zmap -p 6060 | zgrab --port 6060 --http="/debug/pprof/" 安全问题: - 显示函数名与文件路径 -- 分析数据可能揭示商业敏感信息(例如,web服务的流量) +- 分析数据可能揭示商业敏感信息(例如,web 服务的流量) - 分析会降低性能,为 DoS 攻击增加助攻 ## 预防 Farsight Security [警告过这个问题,并且提供了建议](https://www.farsightsecurity.com/2016/10/28/cmikk-go-remote-profiling/) -> 一个简单而有效的方式是将pprof http服务器放在本地主机上的一个单独的端口上,与应用程序http服务器分开。 +> 一个简单而有效的方式是将 pprof http 服务器放在本地主机上的一个单独的端口上,与应用程序 http 服务器分开。 -总之,你需要安排两台HTTP服务器。常见的设置是 +总之,你需要安排两台 HTTP 服务器。常见的设置是 -- 应用程序服务将80端口暴露在公网上 -- `pprof`服务监听本地6060端口并且限于本地访问 +- 应用程序服务将 80 端口暴露在公网上 +- `pprof` 服务监听本地 6060 端口并且限于本地访问 原生的写法是不使用全局的 HTTP 方法的情况下构建主应用程序(使用隐藏配置 `http.DefaultServeMux` ),而是用标准的方法启动你的 pprof 服务。 diff --git a/translated/tech/20121211-Analysis-of-the-Go-runtime-scheduler.md b/translated/tech/20121211-Analysis-of-the-Go-runtime-scheduler.md new file mode 100644 index 000000000..0e7d30d7b --- /dev/null +++ b/translated/tech/20121211-Analysis-of-the-Go-runtime-scheduler.md @@ -0,0 +1,110 @@ +# Go 运行时调度器(runtime scheduler)分析 + +## 摘要 + +Go runtime,以及最近改进它的建议,都来自于先前改善其可扩展性和性能的工作。在本篇论文中,我们会探索一些之前的研究,一部分对 Go runtime 有着深远的影响,而一部分被作为设计 Go runtime 的指导准则。另外,基于抢占式的调度技术,他们也提出了一些对于 runtime 的额外扩展。 + +## 1. 序言 + +Go 语言是用的计算模型是基于 C.A.R. Hoare 于 1978[10]. 年发表的一篇关于**Communicating Sequential Processes**的重要论文。Go 是一种高级语言,应用了 Hoare 的论文中提出了许多架构,这在 C 语言家族中没有发现,而且比锁和信号保护共享内存更容易推理。Go 提供了通过 Goroutines 来支持并发,不仅如此,goroutines 相比于线程,更加轻量级,并且可以独立运行。goroutine 相互之间通过一个叫做 channel 的结构来通信,而 channel 本质上是一个**同步消息队列**。channels 通信,加上对 Go 对闭包一流的支持,可以让我们通过一种直接方式,解决复杂的问题。 + +Go 第一个稳定的版本最近发布了,它仍然在开发当中,并且还有很多改进,特别是它的编译器和基础架构。另外还有 Hoare 和他所提出的先于 Go 诞生的 CSP 语言,都对于 Go 提供了巨大的贡献。在我们的研究过程中,我们参考了许多论文,这些论文在 Go 的实现上有一些共同点,也有一些论文在算法的细节上和解决方案上可以很好的应用在 GO 上。基于这些研究,我们提出了一个 Go runtime 的拓展,我们相信这可以改善当前 Dmity Vyukov 的 Go runtime 的实现。 + +在这篇论文中,我们主要探索一下 Go 的 runtime 调度器。我们对 Go runtime 调度器特别感兴趣是因为我们相信一个相对比较小的改动就可以导致很大的性能提升。这篇论文贡献有,Go runtime 调度器的分析,Go runtime 相关研究的总览以及对调度器的一些改进意见。 + +章节 2 是 Go 语言历史的简述。章节 3 我们探索一下 Go runtime 调度器的实现,在章节 4 谈论一下当前实现的局限性。章节 5 我们描述了对于调度器的改进意见及细节。章节 6 描述了数篇应用在 Go runtime 的论文。章节 7 我们讨论一些坚持使用的好的 ideas,并且章节 8 我们提出了 Go runtime 扩展的改进意见。章节 9 总结全文。 + +## 2. Go 语言简史 + +Hoare 的名为 "Communicating Sequential Processes" 的论文,发表于单机多处理器流行之前。许多研究者,包括 Hoare 在内的许多研究人员都看到了这一趋势的前兆,并且解决了在多核处理器无处不在之前需要回答的研究问题。Hoare 看见了多处理器下进程之间存在的潜在问题。当时的通信模型和今天的线程通信模型有着许多相同的原语。即,在锁机制的协助下来修改共享内存。这个模型很难进行推断,并且容易产生 bug 和错误。Hoare 提出来的解决方案包含了一系列的原语来进程之间消息的传递,而不是修改共享呢内存。 + +Go 语言里用到的许多原语都可以在 Hoare 的 CSP 论文中找到出处。例如,Goroutine,channel 通信以及 select 表达式。这片关于 CSP 的论文详述了许多常见的计算机科学和逻辑问题,也包括这些问题的解决方案。论文中讨论的问题包括计算因子,有界缓冲,哲学家就餐及矩阵乘法等经典问题。尽管 Hoare 使用的符号有很大的不同,但是解决方案的实现和 Go 是基本相同的。当时,Hoare 的 CSP 原语是纯理论化的,但是现在技术已经发展了,我们可以看到他对于并行处理的观点在今日仍然非常有价值。 + +在贝尔实验室开发的基于 CSP 的语言中,Newsqueak 是杰出的一个,并且对 Go 语言影响深远。Rob Pike 从事过好几个这些语言的开发,并且 Newsqueak 是第一个有 channel。这样 channel 和函数的优雅组合可以开发出复杂的通信结构。对于 Newsqueak 语言及其继任者,例如 Alef 和 Limbo,提供了许多语言进化的令人迷人的视角,可以让我们去追溯 Go 的优雅结构的历史。 + +## 3. Go Runtime 论述 + +Go Runtime 管理调度,垃圾收集和 Goroutines 的运行时环境等等。我们主要关注调度器,但是为了做到这一点,也会对 runtime 作基本了解。首先,我们将讨论 runtime 是什么,特别是在与底层操作系统以及程序员编写的 Go 代码有关的情况下。 + +Go 程序被 Go 编译器编译成机器码。由于 Go 提供了诸如 Goroutine,channel 和垃圾收集等高层次的构造,所以需要运行时基础结构来支持这些特性。runtime 是在链接阶段静态链接到已编译的用户代码的 C 代码。因此,Go 程序在操作系统的用户空间中显示为独立的可执行文件。尽管如此,我们可以将 God 的执行程序想象为由两个离散的层组成:用户代码层和 runtime 层,他们通过函数调用来管理 Goroutines,channels 以及其他一些高层次的结构。用户代码对操作系统 API 的所有调用都要通过 runtime 层,来方便调度,当然也包括垃圾回收。图 1 展示了 Go 程序,Go runtime 以及底层操作系统之间的关系 + +![屏幕快照 2017-12-18 下午 4.12.49](/Users/coyote/Desktop/ 屏幕快照 2017-12-18 下午 4.12.49.png) + +**图 1:runtime, os 和用户代码之间的关系** + +可以说,Go runtime 最重要的一个方面就是 Goroutine 的调度器。runtime 追踪每一个 Goroutine,并且在线程池中调度它们。goroutine 是运行在线程之上的,并且有效的在线程之上调度 Goroutine 对于 Go 程序的性能来说至关重要。goroutine 可以并行运行,就像线程一样,但是相比较于线程,goroutine 是更加轻量级的。所以一个 Go 可能会创建很多线程,而 Goroutine 的数量又会比线程的要多的多。之所以需要多个线程是因为要保证 Goroutine 阻塞。当一个 Goroutine 进行阻塞调用时,运行它的线程也必须阻塞。所以,runtime 至少要创建一个以上的线程来继续执行其他没有阻塞的 Goroutine。通过设置 GOMAXPROCS[6],开发可以定义线程的最大数量。 + +goroutine 对于操作系统来说是透明的,操作系统并不知道 Goroutine 的存在,对操作系统来讲仅仅是用户进程及其线程。 + +在 Go runtime 里面,主要有三个 C 数据结构来支持 Goroutine 的调度,以及追踪和保存所有的状态信息。 + +### G + +G 代表一个 Goroutine[9]。它保存有其堆栈和当前状态的字段。当然也有运行其代码的引用。见下图 + +![屏幕快照 2017-12-25 下午 10.28.44](/Users/coyote/Desktop/ 屏幕快照 2017-12-25 下午 10.28.44.png) + +**图片 2:G 的相关字段** + +### M + +M 是 Go runtime 对于线程的表示[9]。它有志向全局 G 队列的指针,当前运行的 G,自己的 cache 以及调度器的句柄。见下图 ![屏幕快照 2017-12-25 下午 10.29.35](/Users/coyote/Desktop/ 屏幕快照 2017-12-25 下午 10.29.35.png) + +### SCHED + +Sched 是一个唯一的,全局的数据结构[9]。它保存有不同 G 和 M 队列,以及一些其他的调度器的信息,例如全局的 Sched 锁。另外还有两个 G 队列,一个是 M 可以拿去运行的 G 队列,另外一个是 G 的空闲队列。Sched 里有一个 M 队列,在这个队列里的 M 是空闲的并且随时准备工作。为了修改这些队列,一个全局的 Sched 的锁是必要的。如下图 + +![屏幕快照 2017-12-25 下午 10.47.12](/Users/coyote/Desktop/ 屏幕快照 2017-12-25 下午 10.47.12.png) + +Runtime 开始运行时也会运行多个 G。一个用来管理垃圾回收,一个负责调度,还有一个代表着用的 Go 代码。开始时,一个 M 被创建出来并且唤醒 runtime。随着程序运行,许许多多的 G 会被用户 G 程序创建,所以就许多更多的 M 来运行所有的 G。这时,runtime 可能需要提供额外的线程 ( 取决于 GOMAXPROCS)。因为在任何时候,go 程序最多有 GOMAXPROCS 个活动的 M。 + +因为 M 代表着线程,所以 M 需要来运行 Goroutine。一个空闲的 M 会从全局可运行 G 队列中拿出一个 G 并且运行它。如果 G 需要运行它的 M 阻塞,比如系统调用,那么其他的 M 就会从全局空闲 M 中唤醒。这样做是为了保证 Goroutines 仍然可以运行,而不是因为缺少可运行的 M 而阻塞。 + +系统调用强制调用线程陷入内核,导致它在系统调用执行期间被阻塞。如果与 G 相关的代码进入系统调用,运行它的 M 就不可能再运行其他 G,直到系统调用返回。但是 channel 通信中,M 并不会出现同样的阻塞。操作系统并不知道 channel 通信,channels 的细节完全被 runtime 所掌控,对操作系统透明。如果一个 Goroutine 有一个 channel 调用,它会阻塞,但是运行它的 M 是不会阻塞的。这种情况下,G 的状态会被设置成 waitting,并且运行它 M 会运行其他的 G 直到 channel 调用返回。这时 G 的状态被重新设置为 runnable,并且尽快被 M 运行。 + +## 4. 改进 + +当前的 runtime 调度器是相对简单的。go 本身是年轻的,所以这意味着还没有足够的时间去优化它的实现。当前的调度器可以运行,但是却会导致一些性能问题。Dmitry Vyukov 在他的设计文档里提出了当前调度器的主要问题,并且也提出了改进调度器的建议。 + +一个问题是调度器过度依赖于全局的 Sched 锁。为了安全的修改 M 队列和 G 队列,以及其他的一些全局变量,这个锁是必须的。但是在我们构建大型系统的时候,这就导致了一些新的问题,特别是 " 高吞吐量和并行计算程序 "。 + +更大问题在于 M 数据结构。即使一个 M 没有执行 Go 代码,也必须需要一个超过 2MB 的 MCache,这是没有必要的,特别是它没有运行一个 Goroutine。如果空闲 M 的数量变得太多,就会导致严重的性能问题以及 "poor data locality"[13]。第三个问题是系统调用会导致多余的对于 M 的锁和解锁操作,特别浪费 CPU 时间。最后,当前的设计会导致 M 将 G 互相传递,而不是运行 G,这也会导致不必要的延迟和资源开支。 + +## 5. VYUKOV 的的改进建议 + +Dmitry Vyukov 是 Google 的一名员工。他发布了一份详细介绍当前运行时间调度程序的一些缺陷的文档,并概述了 Go 的运行时调度程序的未来改进[13]。本章节也包括他提出的改进意见的总结。 + +Vyukov 的计划中,有一个是创建一个抽象层。他提出另外一个数据结构 P,来模拟处理器。M 仍然代表 OS 线程,G 仍然用来标会 Goroutine。用 GOMAXPROCS 来表示 P 的数量,M 会需要 P 作为资源,才能运行 Go code。 + +P 会偷走许多原来 M 和 Sched 的字段。例如,MCache 移动到了 P 中,并且每一个 P 会有一个本地可运行 G 队列而不是一个全局的队列。在 P 中建立本地队列,可以解决之前 Sched 全局锁的问题,并且将 cache 中 M 移到 P 中,减少空间的浪费。无论何时一个 G 被创建出来,它就会被放到 P 的队尾,来保证这个 G 最终会被执行到。另外,在 P 的顶层实现了工作窃取算法。当一个 P 的 G 队列为空时,它会随机的选择其他 P 的队尾并偷走一半的 G 给自己。如果要运行一个 G,如果在搜索 G 运行时,M 遇到了一个锁定到空闲 M 的 G,它将唤醒空闲的 M,并将其关联的 G 和 P 交给先前闲置的 M。 + +另外的问题是 Vyukov 提出的 M 连续的阻塞和解阻塞的问题,导致了很多资源开销。Vyukov 想要运用空转来代替锁,以减少资源开销。它提出了两种空转[13]: + +1. 一个空闲的,有 P 的 M 空转,不断寻找新的 G +2. 没有 P 的 M 空转等待可用的 P + +在任何时候,最多有 GOMAXPROCS 空转的 M。 + +而且,任何一个拥有 P 的空闲 M 不能阻塞,当有空闲的 M 没有 P 的时候。有三种情况可以导致 M 暂时不能运行 G 代码。当一个 G 产生时,M 进入系统调用或 M 从空闲转为忙的时候。在变成阻塞状态前,M 必须首先保证至少有一个自旋的 M,除非所有 P 是忙的。这就可以解决连续的 blocking 和 unblocking 导致的问题,并且保证了只要有 G 可以运行,每一个 P 当前都有一个运行的 G。所以,通过运行自旋机制,系统调用导致的开销也相对的减少了。 + +Vyukov 同样建议,除非真的需要,不要将 G 和栈空间分配给一个新的 Goroutine。他指出我们仅需要 6 个字就能创建一个 Goroutine 而不用调用函数或分配内存。这样会大幅度减少内存的开销。另一个改进建议是将 G 的局部性到 P,因为 G 上次运行的 P 将已经使其 MCache 载入。类似的,G 的局部性在 M 中也是有益的,会让 G 可以更容易的运行。我们必须记住 P 是一个 runtime 创建起来的抽象概念,OS 并不知道,而 M 代表内核线程。大多数现代内核将提供线程与物理处理器之间的亲和性。因此,更好的 G 到 M 的局部性会给我们更好的缓存性能。 + +## 6. 相关工作 + +在我们研究的过程中,我们参考了许多我们相信对 Go runtime 有用的论文。本章节对这些论文做一个简单介绍,并且描述这些论文是符合被我们利用的。 + +### 6.1 Co-scheduling + +Scheduling Techniques for Concureent Systems,于 1982[11]由 John K.Ousterhout 发表,介绍了多进程在一个时间片内大规模通信的问题。本文讨论了三种不同的调度进程任务组的算法,调度进程任务组是相互通信频繁的进程组。最容易应用于 Go runtime 的算法是本文的矩阵算法。 + +矩阵算法将多处理器作为一个矩阵,类似于前文提过的 G 中 P 的数组。矩阵创建时,一列关联一个 P。如下图所示。行数在论文中滨给有细说,但是我们可以假设应该有足够的行数来满足算法。当一个进程任务组创建的时候,算法尝试去找到可以将整个任务组放进去的一行,这样每一个进程就可以它自己的 cell 中。如下图所示,任务组 1 和 2 放在了 row 0。任务组 3 由于太大所以放在剩下的 row 0 以及 row1 中。任务组 4 由于太小可以放在 row 0 的末端。算法将进程放在矩阵的合适位置是为了方便可以同时调度整个任务组。假设总共有 n rows,在时间片 k,在 row(k%n) 行的进程就可以调度在相应的处理上。如果一个处理器是空闲的,无论是因为在当前的任务组中,这个处理器没有进程运行,或者是当前运行的进程阻塞了,这时,这个处理器这一列的其他进程就会被调度来执行。 + +![屏幕快照 2018-01-07 下午 2.26.39](/Users/coyote/Desktop/ 屏幕快照 2018-01-07 下午 2.26.39.png) + +如果 G 的 P 数据结构来代替处理器的话,以上算法可以用来调度同时使用一个 channel 的不同 Goroutines。这样可以大幅度减少阻塞 M 和 G 的时间花费,尽管如此,这就按要求 channel 机制的结构有大幅度的修改。 + +### 6.2 抢占式调度 + +#### 6.2.1 缓存冲突和进程 + +运行在同一进程的线程共享着缓存空间。 diff --git a/translated/tech/20191211-go-2-generics-contracts-are-now-boring-and-that's-good/20191211-go-2-generics-contracts-are-now-boring-and-that's-good.md b/translated/tech/20191211-go-2-generics-contracts-are-now-boring-and-that's-good/20191211-go-2-generics-contracts-are-now-boring-and-that's-good.md new file mode 100644 index 000000000..d60718411 --- /dev/null +++ b/translated/tech/20191211-go-2-generics-contracts-are-now-boring-and-that's-good/20191211-go-2-generics-contracts-are-now-boring-and-that's-good.md @@ -0,0 +1,31 @@ +# Go 2 范型:目前的合约很无聊(不过这很棒) + +> August 19, 2019 + +7 月底,Go 团队推出了 [Go 2 范型设计的修订版](https://go.googlesource.com/proposal/+/master/design/go2draft-contracts.md),完全重写了合约,其中明确规定了范型方法等接受的约束(令我惊讶的是,修订版设计尚未链接到 [wiki 反馈页面](https://github.com/golang/go/wiki/Go2GenericsFeedback),但据我所知,该设计完全是官方的)。自从修订版设计问世,我一直在考虑对它的看法,简而言之,我并不想说什么,换一种说法,是因为新的合约设计很无聊。 + +你会对[初版设计](https://go.googlesource.com/proposal/+/master/design/go2draft.md)中的合约有很多评论,但是他们确实并不无聊。尽管合约复用了现有的 Go 语法(用于方法体),它们与 Go 中其他任何内容都存在明显差异,[一种我觉得非常聪明的差异](https://utcc.utoronto.ca/~cks/space/blog/programming/Go2ContractsTooClever)。为了尽量减少新语法,初版合约设计通常[间接的描述事情](https://utcc.utoronto.ca/~cks/space/blog/programming/Go2ContractsMoreReadable),导致了一些问题,例如有许多方式可以表示同一种约束(尽管 [Go 可能要求最少的合约](https://utcc.utoronto.ca/~cks/space/blog/programming/Go2RequireMinimalContracts))。 + +新版合约设计消除了这些问题。合约使用了新语法,尽管它复用了普通 Go 语言中的许多语法元素,新语法基本上是最少的,并且直接表述了它的约束。之前只能通过暗示来表达的一些棘手的事情,现在可以通过字面含义表达,例如,将一个类型限定为基于整型的某种类型。到目前为止,那种情况下的类型约束仍然很难表示,Go 团队引入了一个预先声明的称作 *comparable* 的合约,而非尝试变得更聪明。正如提案本身所说: + +> 总是会有数量有限的预声明类型,以及这些类型支持的数量有限的运算符。将来语言的修改不会从根本上改变这些要素,因此这种方式将持续有效。 + +对于这个问题,这是一种标准的 「Go 语言」 方法,这让我印象深刻。它既不聪明,也不让人兴奋,但它有效(并且清晰明了)。 + +另一个决定 - 范型函数中的所有方法调用都将是指针类型的方法调用,并不优雅,但却解决了一个真实潜在的混乱。关于可寻址的值和指针值的方法的规则[有些令人困惑且晦涩难懂](https://utcc.utoronto.ca/~cks/space/blog/programming/GoAddressableValues),因此 Go 团队决定让它们变得无聊,而非尝试变得聪明。无论这让人觉得有多么不优雅,当我需要处理范型时,它可能会使我的生活更轻松。 + +(该设计还允许你指定特定的方法必须是指针方法,尽管哪种类型满足生成的合约可能有点太聪明,也太晦涩了。) + +修订版的合约设计几乎保留了初版设计中[我喜欢的所有内容](https://utcc.utoronto.ca/~cks/space/blog/programming/Go2ContractsLike)。它放弃了一件事,我并不希望如此,即你不再能够要求类型具有某些特定字段(隐式要求它们是结构类型)。在实践中,内联的 getter 和 setter 方法也可能同样有效,[即便我不喜欢它们](https://utcc.utoronto.ca/~cks/space/blog/programming/GettersSettersDislike),并且总有一些使用场景是不清晰的。 + +我不知道目前的 Go 2 范型以及合约设计是否绝对正确,但我现在确实觉得,和初版方案给人的感觉相对比,这并不是个错误。我确实希望人们尝试使用这种设计来写范型代码,如果它以某种试验形式在 [Go 的某些版本](https://utcc.utoronto.ca/~cks/space/blog/programming/GoAppearanceOfChanges)中提供,或者以其他方式可用,因为我怀疑这是我们找到任何剩余的大致的边界和痛点的主要方式。 + +--- + +via: https://utcc.utoronto.ca/~cks/space/blog/programming/Go2ContractsNowBoring + +作者:[ChrisSiebenmann](https://utcc.utoronto.ca/~cks/space/People/ChrisSiebenmann) +译者:[DoubleLuck](https://github.com/DoubleLuck) +校对:[unknwon](https://github.com/unknwon) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 diff --git a/translated/tech/20210817-How-to-Use-Generic-in-Go-From-1.17.md b/translated/tech/20210817-How-to-Use-Generic-in-Go-From-1.17.md new file mode 100644 index 000000000..5ea88a3f2 --- /dev/null +++ b/translated/tech/20210817-How-to-Use-Generic-in-Go-From-1.17.md @@ -0,0 +1,77 @@ +# 如何在 Go 1.17 中使用范型 + +我们知道,Go 1.18 预计将在今年末或明年初发布时为 Go 语言带来范型。 +但对于那些等不及的人, 可以从 [Go2Go Playground](https://go2goplay.golang.org/) 的特定版本在线尝试范型, +还有一种方法可以让你在本地环境尝试范型,不过有点麻烦, +需要从 [源码](https://go.googlesource.com/go/+/refs/heads/dev.go2go/README.go2go.md) 编译 Go。 + +直到今天 Go 1.17 发布: + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210817-How-to-Use-Generic-in-Go-From-1.17/tweet-01.png) + +除了一些新特性,还有一个特定的标记参数 `-gcflags=-G=3`,在编译或运行的时候加上它就能使用范型。 +我第一次在这里看到这个消息,但是除了这个来源,我还发现一些其他的公共消息。 + +![](https://raw.githubusercontent.com/studygolang/gctt-images2/master/20210817-How-to-Use-Generic-in-Go-From-1.17/tweet-02.png) + +总之,我很高兴确认它可以用!然后要说明的是,我是在 [Go2Go Playground](https://go2goplay.golang.org/) 运行下面的代码: + +```go +package main + +import ( + "fmt" +) + +// The playground now uses square brackets for type parameters. Otherwise, +// the syntax of type parameter lists matches the one of regular parameter +// lists except that all type parameters must have a name, and the type +// parameter list cannot be empty. The predeclared identifier "any" may be +// used in the position of a type parameter constraint (and only there); +// it indicates that there are no constraints. + +func Print[T any](s []T) { + for _, v := range s { + fmt.Print(v) + } +} + +func main() { + Print([]string{"Hello, ", "playground\n"}) +} +``` + +当我尝试用刚刚提到的参数运行的时候,会导致下面的错误: + +```bash +$ go1.17 run -gcflags=-G=3 cmd/generics/main.go + +# command-line-arguments +cmd/generics/main.go:14:6: internal compiler error: Cannot export a generic function (yet): Print +No problem, lets unexport Printfor now, by renaming it to print. +``` + +现在运行相同的命令绝对可以正确执行! + +```go +$ go1.17 run -gcflags=-G=3 cmd/generics/main.go + +Hello, playground +``` + +## 这到底有什么用? + +毫无疑问,这肯定是前进的一步。如果你想尝试范型,你必须从源码编译 Go。 +然而,Go 编译器的实现也只是完成了一半工作,另外一半是工具链的支持。 +根据我有限的测试,似乎只有 `run` 和 `build` 命令支持了这个参数,其他的命令,比如格式化或测试都没有成功。 + +随着 Go 1.18 越来越近,我很期待看到更多工具链的支持。 + +--- +via: https://preslav.me/2021/08/17/how-to-use-generics-in-golang-starting-from-go1-17/ + +作者:[Preslav Rachev](https://preslav.me/author/preslav/) +译者:[h1z3y3](https://www.h1z3y3.me) +校对:[校对者 ID](https://github.com/校对者 ID) + +本文由 [GCTT](https://github.com/studygolang/GCTT) 原创编译,[Go 中文网](https://studygolang.com/) 荣誉推出 \ No newline at end of file diff --git a/wechat.png b/wechat.png new file mode 100644 index 000000000..9bf5d165e Binary files /dev/null and b/wechat.png differ