[
  {
    "path": ".gitignore",
    "content": ".idea/\n.DS_Store\n_book/\nnode_modules/\npublic/\n"
  },
  {
    "path": "README.md",
    "content": "# 煎鱼的迷之博客\n\n写写代码，喝喝茶，搞搞 Go，一起吧，这是我的项目地址：https://github.com/eddycjy/blog\n\n## 在线阅读\n\n- https://eddycjy.com/\n\n## 我的公众号\n\n所有文章和最新进度，请关注：\n\n![image](https://image.eddycjy.com/7074be90379a121746146bc4229819f8.jpg)\n\n## ？\n\n如果有任何疑问或错误，欢迎在 issues 进行提问或给予修正意见\n\n如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进 😀\n\n## License\n\n所有文章采用[知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议](https://creativecommons.org/licenses/by-nc-sa/3.0/cn/)进行许可\n"
  },
  {
    "path": "archetypes/default.md",
    "content": "---\ntitle: \"{{ replace .Name \"-\" \" \" | title }}\"\ndate: {{ .Date }}\ndraft: true\ncomments: false\nimages:\n---\n\n"
  },
  {
    "path": "archetypes/posts.md",
    "content": "---\ntitle: \"{{ replace .Name \"-\" \" \" | title }}\"\ndate: {{ .Date }}\ndraft: true\ntoc: false\nimages:\ntags: \n  - untagged\n---\n\n"
  },
  {
    "path": "config.toml",
    "content": "baseURL = \"https://eddycjy.com\"\nlanguageCode = \"zh-hans\"\ndefaultContentLanguage = \"en\"\ntitle = \"煎鱼\"\ntheme = \"hermit\"\n# enableGitInfo = true\npygmentsCodefences  = true\npygmentsUseClasses  = true\n# hasCJKLanguage = true  # If Chinese/Japanese/Korean is your main content language, enable this to make wordCount works right.\nrssLimit = 10  # Maximum number of items in the RSS feed.\ncopyright = \"This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.\" # This message is only used by the RSS template.\nenableEmoji = true  # Shorthand emojis in content files - https://gohugo.io/functions/emojify/\ngoogleAnalytics = \"UA-166045776-1\"\n# disqusShortname = \"yourdiscussshortname\"\nsummarylength = 30\n\n[author]\n  name = \"煎鱼\"\n\n[blackfriday]\n  # hrefTargetBlank = true\n  # noreferrerLinks = true\n  # nofollowLinks = true\n\n[taxonomies]\n  tag = \"tags\"\n  # Categories are disabled by default.\n\n[params]\n  since = \"2018\"\n  toc = true\n\n  dateform        = \"Jan 2, 2006\"\n  dateformShort   = \"Jan 2\"\n  dateformNum     = \"2006-01-02\"\n  dateformNumTime = \"2006-01-02 15:04 -0700\"\n\n  # Metadata mostly used in document's head\n  description = \"煎鱼,博客,go,golang,源码分析,系列教程\"\n  # images = [\"\"]\n  themeColor = \"#494f5c\"\n\n  mainSections = [\"posts\"]\n\n  homeSubtitle = \"Metrics, Tracing, Logging\"\n  footerCopyright = ' &#183; <a href=\"http://www.beian.miit.gov.cn/\">粤ICP备19076352号</a>'\n  # bgImg = \"\"  # Homepage background-image URL\n\n  # Prefix of link to the git commit detail page. GitInfo must be enabled.\n  gitUrl = \"https://github.com/eddycjy/blog/commit/\"\n\n  # Toggling this option needs to rebuild SCSS, requires Hugo extended version\n  justifyContent = false  # Set \"text-align: justify\" to `.content`.\n\n  relatedPosts = false  # Add a related content section to all single posts page\n\n  code_copy_button = true # Turn on/off the code-copy-button for code-fields\n  \n  # Add custom css\n  # customCSS = [\"css/foo.css\", \"css/bar.css\"]\n  customCSS = [\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css\", \"css/styles.css\"]\n\n  # Social Icons\n  # Check https://github.com/Track3/hermit#social-icons for more info.\n\n\n  [[params.social]]\n    name = \"github\"\n    url = \"https://github.com/eddycjy\"\n\n[menu]\n\n  [[menu.main]]\n    name = \"文章\"\n    url = \"posts/\"\n    weight = 10\n\n  [[menu.main]]\n    name = \"标签\"\n    url = \"tags/\"\n    weight = 10\n\n  [[menu.main]]\n    name = \"关于\"\n    url = \"about/\"\n    weight = 20\n\n\n  [[menu.nav]]\n    name = \"文章\"\n    url = \"posts/\"\n    weight = 10\n\n  [[menu.nav]]\n    name = \"Go语言编程之旅\"\n    url = \"https://golang2.eddycjy.com/\"\n    weight = 11\n\n  [[menu.nav]]\n    name = \"Go语言进阶之旅\"\n    url = \"https://golang1.eddycjy.com/\"\n    weight = 11\n\n  [[menu.nav]]\n    name = \"Go语言设计哲学\"\n    url = \"https://golang3.eddycjy.com/\"\n    weight = 11\n\n  [[menu.nav]]\n    name = \"Go语言入门系列\"\n    url = \"go-categories/\"\n    weight = 12\n\n  [[menu.nav]]\n    name = \"Kubernetes系列\"\n    url = \"k8s-categories/\"\n    weight = 13\n\n\n\n"
  },
  {
    "path": "content/about.md",
    "content": "---\ntitle: \"关于\"\ndate: \"2020-03-15\"\n---\n\n你好，我是煎鱼，最近沉迷于 Go、Kubernetes、Prometheus 这一生态圈子里的东西（努力学习中）。在工作中，目前主要负责公司的基础架构/组件的建设和业务团队推广， 欢迎大家来找我玩和讨论问题，提建议也随时欢迎。\n\n### 有趣的时间节点：\n\n1. 高一：参加俱乐部，捣鼓 Photoshop 为校学生会做做平面设计。\n2. 高一：参加市/省/国赛，捣鼓前端，那是一个 table+css+js 的时代，dw 还很火热。\n3. 大一：参加校组织，捣鼓 PHP，还为校组织开发了微信公众号（当时公众号刚出来），结果不错，市场占有率极高。\n4. 工作三年（加上实习的时间）：在那一天，公司 CTO 突然喊我私聊，希望我去全新的业务组（哥伦布组），我一口答应下来，当时还不知道 Go 语言是什么，自此从主力语言从 PHP 转为了 Go，并写了很多 Go 语言相关的[系列](/category)。\n5. 工作四年：2019 年年尾正式转到了公司的架构组，在 2020 年 7 月正式出版 Go 语言图书《[Go 语言编程之旅](https://item.jd.com/12685249.html)》，并在 2020 年的 GopherChina 拿到了 GOP 的荣誉称号。\n\n### 联系方式：\n\n- Github：https://github.com/eddycjy/blog\n- 公众号：脑子进煎鱼了\n- 邮箱：eddycjy@gmail.com\n\n### 我的公众号\n\n平时喜欢分享 Go 语言、微服务架构和奇怪的系统设计，欢迎关注我的公众号：\n\n![image](https://image.eddycjy.com/7074be90379a121746146bc4229819f8.jpg)\n\n\n"
  },
  {
    "path": "content/go-categories.md",
    "content": "---\ntitle: \"《跟煎鱼学 Go》\"\ndate: \"2020-04-21\"\n---\n\n我不怎么喜欢左写写，右写写，因此总是在不知不觉中写了不少的系列教程，希望对你有所帮助，若要催更请关注公众号后私聊催。\n\n- 一：**HTTP 应用**\n    - [「连载一」Go 介绍与环境安装](/posts/go/gin/2018-02-10-install/)\n    - [「连载二」Gin搭建Blog API's （一）](/posts/go/gin/2018-02-11-api-01/)\n    - [「连载三」Gin搭建Blog API's （二）](/posts/go/gin/2018-02-12-api-02/)\n    - [「连载四」Gin搭建Blog API's （三）](/posts/go/gin/2018-02-13-api-03/)\n    - [「连载五」使用 JWT 进行身份校验](/posts/go/gin/2018-02-14-jwt/)\n    - [「连载六」编写一个简单的文件日志](/posts/go/gin/2018-02-15-log/)\n    - [「连载七」优雅的重启服务](/posts/go/gin/2018-03-15-reload-http/)\n    - [「连载八」为它加上Swagger](/posts/go/gin/2018-03-18-swagger/)\n    - [「连载九」将Golang应用部署到Docker](/posts/go/gin/2018-03-24-golang-docker/)\n    - [「连载十」定制 GORM Callbacks](/posts/go/gin/2018-04-15-gorm-callback/)\n    - [「连载十一」Cron定时任务](/posts/go/gin/2018-04-29-cron/)\n    - [「连载十二」优化配置结构及实现图片上传](/posts/go/gin/2018-05-27-config-upload/)\n    - [「连载十三」优化你的应用结构和实现Redis缓存](/posts/go/gin/2018-06-02-application-redis/)\n    - [「连载十四」实现导出、导入 Excel](/posts/go/gin/2018-06-14-excel/)\n    - [「连载十五」生成二维码、合并海报](/posts/go/gin/2018-07-05-image/)\n    - [「连载十六」在图片上绘制文字](/posts/go/gin/2018-07-07-font/)\n    - [「连载十七」用Nginx部署Go应用](/posts/go/gin/2018-09-01-nginx/)\n    - [「番外」Golang 交叉编译](/posts/go/gin/2018-03-26-cgo/)\n    - [「番外」请入门 Makefile](/posts/go/gin/2018-08-26-makefile/)\n- 二：**gRPC 应用**\n    - [「连载一」gRPC及相关介绍](/posts/go/grpc/2018-09-22-install/)\n    - [「连载二」gRPC Client and Server](/posts/go/grpc/2018-09-23-client-and-server/)\n    - [「连载三」gRPC Streaming, Client and Server](/posts/go/grpc/2018-09-24-stream-client-server/)\n    - [「连载四」TLS 证书认证](/posts/go/grpc/2018-10-07-grpc-tls/)\n    - [「连载五」基于 CA 的 TLS 证书认证](/posts/go/grpc/2018-10-08-ca-tls/)\n    - [「连载六」Unary and Stream interceptor](/posts/go/grpc/2018-10-10-interceptor/)\n    - [「连载七」让你的服务同时提供 HTTP 接口](/posts/go/grpc/2018-10-12-grpc-http/)\n    - [「连载八」对 RPC 方法做自定义认证](/posts/go/grpc/2018-10-14-per-rpc-credentials/)\n    - [「连载九」gRPC Deadlines](/posts/go/grpc/2018-10-16-deadlines/)\n    - [「连载十」分布式链路追踪 gRPC + Opentracing + Zipkin](/posts/go/grpc/2018-10-20-zipkin/)\n- 三：**grpc+grpc-gateway 应用**\n    - [「连载一」gRPC介绍与环境安装](/posts/go/grpc-gateway/2018-02-23-install/)\n    - [「连载二」Hello World](/posts/go/grpc-gateway/2018-02-27-hello-world/)\n    - [「连载三」Swagger了解一下](/posts/go/grpc-gateway/2018-03-04-swagger/)\n    - [「连载四」gRPC+gRPC Gateway 能不能不用证书？](/posts/go/grpc-gateway/2019-06-22-grpc-gateway-tls/)\n\n### 我的公众号\n\n平时喜欢分享 Go 语言、微服务架构和奇怪的系统设计，欢迎关注我的公众号：\n\n![image](https://image.eddycjy.com/7074be90379a121746146bc4229819f8.jpg)"
  },
  {
    "path": "content/k8s-categories.md",
    "content": "---\ntitle: \"《跟煎鱼学 Kubernetes/Prometheus》\"\ndate: \"2020-05-16\"\n---\n\n请注意，这不是成品，随时可能会对以前的章节进行修改。那么为什么要放出来呢，当然是为了催更自己。\n\n1. [Kubernetes 本地快速启动（基于 Docker）](/posts/kubernetes/2020-05-01-install)\n2. [在 Kubernetes 中部署应用程序](/posts/kubernetes/2020-05-03-deployment)\n3. [使用 Go 程序调用 Kubernetes API](/posts/kubernetes/2020-05-10-api)\n\n\n1. [Prometheus 快速入门](/posts/prometheus/2020-05-16-startup)\n2. [Prometheus 四大度量指标的了解和应用](/posts/prometheus/2020-05-16-metrics)\n3. [使用 Prometheus 对 Go 程序进行指标采集](/posts/prometheus/2020-05-16-pull)\n\n### 我的公众号\n\n平时喜欢分享 Go 语言、微服务架构和奇怪的系统设计，欢迎关注我的公众号：\n\n![image](https://image.eddycjy.com/7074be90379a121746146bc4229819f8.jpg)"
  },
  {
    "path": "content/posts/2020-summary.md",
    "content": "---\ntitle: \"拖更的 2020 年不一样\"\ndate: 2020-12-31T21:29:55+08:00\nimages:\ntags: \n  - 总结\n---\n\n大家好，我是煎鱼。\n\n万万没想到...想着写 2020 年总结，结果就到了 2021 年，不愧是只有 7s 记忆的博主 😂。\n\n2020 年并不简单，这也是第一次在公开场合写个人向，并且还是年终总结，感慨颇多。\n\n## 出书\n\n今年（2020年）上半年几乎没有更新博客文章，当时还一口气退了一大堆技术交流的微信群。当时有不少朋友来咨询我怎么了，后面的事大家也就知道了。\n\n蛰伏了将近 9 个月，出了人生第一本图书（简体+繁体）：\n\n![](https://image.eddycjy.com/56c805ff0e8c6134e845d5da99d4ab0b.jpg)\n\n本书已印刷三次，和编辑沟通了几次，现在 Go 语言还算小众（与 Java、Python 相比较），销量算是挺不错的了。\n\n有一块比较遗憾，在初印有几个比较致命的印刷问题。后续在第二次印刷中解决了。此次只能说第一次写书经验不足，编辑也是，下次一定。\n\n另外当时出书时与曹大聊过几次，他提到的出书后会遇到的所有烦恼我大多都遇到了，现实太真实，出书很理想。\n\n其次写书并不赚钱，不要见面就问。更没有因此改变我的初心，依然是热爱分享知识的煎鱼。\n\n未曾想过的第一个里程碑完成。\n\n## 博客/公众号\n\n后半年就开始陆续恢复博客的写作了，2020 年博客上共有 32 篇技术文章，加上图书的稿子一年总共写了 70+ 篇文章。\n\n2019 年只写了 19 篇。相比较而言，总产出还算可以：\n\n![](https://image.eddycjy.com/4be8f0069f8b79388c1fe0853ab9a534.jpg)\n\n但理论上还可以更高，因为在写完书后，由于之前过于高度集中，导致下半年出现了几个月的真空休息期。更深刻的认识到了劳逸结合，适当调节非常重要。\n\n同时公众号也在年尾终于往前走了一步，开始接了一些推广，虽然钱不多。但更多的还是激励自己，倒逼自己更多的输入和输出。\n\n最近有很多小伙伴也发现了我公众号的更新频率变高了，这是相辅相成的。\n\n知识吸到就是你的。\n\n## 社区\n\n今年因为分享知识接触到了很多人，又因此认识到了更多的人。其中不乏各行各领域的优秀小伙伴们，间接的给我人生发展上提供了极大的建议和反馈，影响了我做许多事情的决策和思考方向。\n\n同时在 Gopher China 2020 中拿到了 GOP（Go 领域最具观点专家）的荣誉：\n\n![](https://image.eddycjy.com/6106728f2f69a19d5584623326c97363.jpeg)\n\n很可惜的是，当时正在在准备公司内答辩的 PPT 和生病中，因此没能去现场，很遗憾。\n\n不过在线下依然感受到了各路 Gopher 们的反馈和关注，很感谢 GoCN 的认可。\n\n未曾想过的第二个里程碑完成。\n\n## 工作\n\n感觉自己变化太大了，发现今年思考事情的角度、广度、深度以及关联性已然和上一年不一样。\n\n![](https://image.eddycjy.com/82d6bd939ce594c3ad219722380f5957.jpg)\n\n经提醒也发现已入职三年半，已经到了传统定义的 “职业倦怠期” 的时间阶段，如何更好的保持自己的好奇和发展是一个要考虑的大命题。\n\n自己评判自己的工作是很难的，继续努力是大方向。\n\n## 读书\n\n今年看的书挺多，涉及了计算机、产品设计、用户增长、金融理财等各个领域。\n\n阅读时间主要是集中在午睡时间和晚上睡前看。\n\n公司桌面上的书也是越来越多：\n\n![](https://image.eddycjy.com/b114cdba7dc15e5175dedb83d5aa82b9.jpeg)\n\n强烈建议阅读 DDIA，好书。\n\n## 总结\n\n今年经历了许多魔幻的事情，好彩大多都挺了过来并解决了。\n\n同时也认识到了许多的社区朋友，在北京也算吃过烤鸭，冻肿过手指的人了。\n\n最后，我时常能在私聊或朋友圈听到来自读者们的反馈：\n\n![](https://image.eddycjy.com/c23325dd64d5eee7410647cc0e472b94.jpeg)\n\n很感谢大家喜欢我的文字，能从煎鱼身上吸取到自己想要的知识。\n\n希望 2021 年咱们继续努力，把 flag 都给立好了。\n\n![image](https://image.eddycjy.com/0618aba7eb620d6541e2f02154a4ab19.jpeg)\n\n\n\n"
  },
  {
    "path": "content/posts/2020-top100.md",
    "content": "---\ntitle: \"吐血整理 | 快速了解全球软件案例（Top100）\"\ndate: 2020-12-22T21:26:44+08:00\ntoc: true\nimages:\ntags: \n  - top100\n---\n\n前几天，煎鱼去了趟北京，参加了为期三天的全球软件案例研究峰会（TOP 100）。\n\n同时记了一些笔记，整理后分享出来，希望对大家有所帮助，拓展眼界非常重要。\n\n![](https://image.eddycjy.com/a1be9ee345e57e1299f74a3d9e336d13.jpeg)\n\n内容比较多（已经精简过），大家可以挑自己感兴趣的学习，建议三连。\n\n一级目录如下：\n\n1. 百度内部业务 ServieMesh 实践。\n2. 云原生开发平台在腾讯游戏运营中的实践。\n3. 快狗打车可持续交付实践。\n4. 网易数帆从微服务框架到服务网格架构平滑演进及最佳实践。\n5. 不破不立：企业级研发效能提升的创新实践。\n6. 自如云原生落地最佳实践。\n7. 研发效能度量的误区、体系化实践和效能提升案例。\n8. 京东 BDP 的全域监控、管控平台搭建实践。\n9. 构建发布效率从10分钟到秒级的提升 - 云原生下编程方式的探索和实践。\n10. 全面监控体系建设及智能监控的探索实践。\n11. 低代码技术在贝壳的实践。\n\n## 百度内部业务 ServieMesh 实践\n\n本场演讲内容主要为微服务到服务网格的过程。其中涉及百度在异构场景下的一系列演进和适配操作。\n\n同时也能得知百度也是自己做了个 bmesh，自此概括几乎全一线互联网大厂，均为自研（或结合）ServieMesh。\n\n### 整体演进\n\n#### 1.0 时代\n\n第一代微服务架构（1.0时代），主体是基于 SDK/开发框架的微服务治理体系。\n\n![](https://image.eddycjy.com/83b5009e040969ee7b60362ad7426573.jpeg)\n\n主要存在以下问题：\n- 开发成本高：异构语言的问题，每个语言都要重新开发。\n- 升级成本高：框架上线以来业务。\n- 管理成本高：服务拓扑和治理没有统一管理（需要治理）。\n\n#### 2.0时代\n\n第二代微服务架构（2.0时代），主体是基于微服务框架到服务网格，也就是把服务治理能力抽取出来，作为一个进程（sidecar），与业务逻辑解耦。\n\n![](https://image.eddycjy.com/ea571676ce9b75b0730a5d56350ae93e.jpeg)\n\n从概念上来讲，主要分为以下两类：\n- 数据平面\n    - 与业务无关。\n    - 与语言无关。\n    - 独立的升级（直接升级 sidecar 的进程），能够解耦。\n- 控制平面\n    - 能够统一的管控。\n\n### 百度现状\n\n各语言在内部平分秋色，没有谁强谁弱。各自都有框架，且有可能有多个框架，可自行脑补一下在公司内部一种语言有 N 种框架，且多种协议（含私有协议）的情况：\n\n![](https://image.eddycjy.com/182845aceb39c9e413e28fd549058cf8.jpeg)\n\n存在以下问题：\n- 多个语言开发。\n- 多个框架改造。\n- 多个通讯协议。\n\n简单来讲就是 “异构系统”，传统的微服务框架无法满足了，成本非常高，甚至不可行。只能通过服务网关的方式来实现微服务治理。\n\n### 上服务网格的困难\n\n- 改造成本：\n    - 各种内部框架的支持。\n    - 各种通讯协议的支持。\n- 性能问题：\n    - 通讯延迟，有些敏感业务无法接受，例如：搜索。\n    - 资源开源，数十万机器，每个服务都加边车，成本极大。\n- 规模问题：\n    - 随着接入的节点越多，规模越大，控制平面下发配置的速度越慢，甚至无法工作。\n    \n### 百度的解决方案（整体架构）\n\n![](https://image.eddycjy.com/9679ccb5a92f650b83fcf29e0a6a6775.jpeg)\n\n在开源的技术栈上进行了自己的拓展，用的是 istio+envoy。\n\n并且在 istio 之上做了一层抽象，实现了 Mesh 的管理界面。\n\n另外实际上在调参时，是需要很多实际经验的，例如：超时的值到底怎么配，因此又基于此在平台上提供了智能调参系统。\n\n与目前所知的一线互联网大厂改造比较类似，区别在于还做了很多自有平台。\n\n### 遇到的问题（大规模落地三步走）\n\n![](https://image.eddycjy.com/ddf9c9a45551e218c4018d5c53e9f6bb.jpeg)\n\n#### 解决接入问题\n- 流量劫持方案：\n    - 社区自有的方案无法修改服务治理的参数（例如：导致原有的超时重试变成了对边车重试）。\n    - iptables 存在性能问题。\n    - 无法在 mesh 和 非 mesh 下切换：不能完全信任，挂掉后怎么处理，流量怎么切。解决方案是劫持服务发现的过程（边车劫持的是的服务地址），就能够解决流量劫持的自由问题。\n- 自有协议代理：有二十多种协议，解决方案是抽了一层公共 Proxy，实现 codec 就可以了。但也没法解决全部问题，因为 Mesh 对协议有要求，例如加字段。但有的老协议是没法扩展的。最后解决方案是做协议转换，业务代码就不需要修改了。\n- 多框架支持：网格对框架有基本要求（治理行为可拓展，透传 Trace 信息），但有的老框架没法支持。通过探针逻辑修改框架行为（探针感知逻辑，修改框架行为）。\n\n#### 解决性能问题\n- 网络性能优化：\n    - envoy 的最大问题是性能不怎么好，拓展性是第一，性能是他的第二位。\n    - envoy 的多线程的竞争是非常厉害的。最后是采取 envoy+brpc 的融合方案（难度很大，组件替换，逐步替换）解决，整体延迟和 CPU 使用率明显低于原生版本。做了开关，能在原生和融合后版本切换。\n- 网络控制面优化：\n    - istio 原生的配置是全量下发的，非常不科学。\n    - 改造为通过获取关系按需下发。服务发现改为由控制面下发到数据面。简单来讲就是原生 istio 控制面在大规模下有很多问题，为此改了很多逻辑。\n- 网络性能优化：\n    - istio 原生为 both side 模式，要转换 2 次，损耗 2 次网络开销，2次性能开源，内部认为不可接受。改为 client side 的模式（架构上的折中，有的业务很敏感，不推荐为主流方式）。\n\n#### 享受网格的红利\n- 流量可操控：\n    - 所有的流量都在自己的手中，可以去做很多事情。例如做很多动态的事情，全局流控，根据延迟分配请求到不同的下游等，带来的新的问题就是太多配置，太吃经验，因此做了哥全局智能调参。\n    - 结合高级治理，配合自愈和剔除，可用性的修复时间大大的提高，提高了可用性。\n- 服务可观测：\n    - 结合框架透传 Trace 信息，Sidecar 负责上报监控，即可形成追踪。\n- 自动止损\n    - 结合监控平台采集实例、集群信息，发现异常，上报给控制平面。然后就可以屏蔽掉某个实例、集群。实现自动止损。\n- 异常注入\n    - 混沌平台负责配置、评估，服务网格负责实施异常注入。\n- 容量管理\n    - 传统需要做压测，对整个系统进行很长时间的压测（想压到极限，要构造大量的数据和流量），非常麻烦。\n    - 通过服务网格可以在局部构造出极限的压力。\n\n### 落地情况\n\n百度目前正在逐渐落地，已接入实例数万。通过高级的服务治理（需要自实现）带来了很大的收益。\n\n但需要注意，如果只是单纯接入服务网格，并不能带来上述所说的收益。他的收益更多的是面向后续的一些高级治理等高级场景。\n\n### 总结\n\n- 服务网格不是微服务治理的银弹：\n    - 完全无侵入支持所有空间和治理策略的 Mesh 方案是不存在的。\n    - 大规模落地一定会涉及已有治理的兼容升级和改造。\n- 服务网格确实实现了业务逻辑和服务治理架构的解耦。\n- 服务网格的开始是最难的，落地服务网格会非常困难和艰辛。\n\n### QA\n\n- 第一个：\n    - 新产品可以上服务网格，但要有一个现成成熟的服务网格，自研工作量是非常之大的。\n- 第二个：\n    - 和开源社区结合的问题，会不定期更新 envoy，istio 的版本。\n    - 服务网格不能只看节省的成本，如果只是说框架的治理，那是非常小的，最大的收益是把所有的流量都汇总到一起后收益非常大。网格一开始会非常痛苦，需要把流量真正的拦截上去。\n    - 边车的定位是服务于服务之间的通讯代理，与业务逻辑比较紧耦合是不适合放进去的。\n    - 推广的目标不是全站覆盖，核心链路上到就可以，因为有的应用对这类没什么诉求。\n- 第三个：\n    - 是否 SSL 看企业场景。\n- 第四个：\n    - bmesh 可以从全局角度监控，现有的监控模式可能你会是自己有问题。\n\n\n## 云原生开发平台在腾讯游戏运营中的实践\n\n- 平台的期望：提高研发效能，业务落地成长。\n\n- 业务背景：营销活动，上线活动很多是不报备的，因此伸缩性要求高，日活很容易上千万。活动多（每天新增 50 个以上），数量量幅度大，服务质量要求高。\n\n- 实际成效：这么大的规模，现在只需要 3 个 专职运维，晚上 6 点就下班了。\n\n### 本质需求\n\n- 频繁发布：版本发布更新。\n- 动态伸缩：数据量大。\n- 持续高可用：经常变更。\n\n### 运维的任务\n\n![](https://image.eddycjy.com/aba4c12c0307ac56aedf5e7b2dadf69b.jpeg)\n\n以往都是开发提交给运维的，问题是有极多个开发对接有限的运维。面临的问题：\n\n- 面临的环境，部署的东西，都是不是固定的。\n- 开发需要转移知识给运维。\n- 实际经常会出现意外情况。\n- 对部署的风险：运维本能排斥变化。\n\n呈现的结果是运维忙于救火，开发提个需求，就只能排队，线上总不稳定。\n\n### 解决办法\n\n- 持续交付：\n    - 控制产品发布节奏，需求尽快上线，不积压。\n- 打造部署安全网：\n    - 微服务、并行部署，完善的监控。\n- 实现可重复性：\n    - 控制环境和部署的构建，自动化， 保证输入输出的一样的。\n- 变化是必然的：\n    - 以故障是常态去设计。\n\n### 碎片的解决方案\n\n要解决上述所提到的问题，基本目前在业界都有基本的解决方案，但其都是 “碎片化” 的，如下：\n\n![](https://image.eddycjy.com/4f84f02beb6427bc9a6d8d09d2376746.jpeg)\n\n“碎片化” 指的是不同的组件都分别是解决特定的问题。这就导致了下述问题：\n\n- 各方面的学习成本高。\n- 系统自动化程度低。\n- 有经验开发人员有限：\n    - 人员招聘成本高，限制发展规模。\n- 无生命周期：\n    - 整体的上线到下线没有管理。\n\n### 云原生开发平台的设计\n\n真正的完整解决方案就是做一个 “云原生开发平台”，提供一整套的服务来支持软件的开发和运维，实现各种云原生软件的诉求。\n\n从设计角度上来讲：\n\n![](https://image.eddycjy.com/02519bfb266773f243fdef49420313d1.jpeg)\n\n运维不暴露太多的基础建设，只是开放角度去做。开发人员只需要关注应用程序，不需要关注底层的基础设施。\n\n不需要让业务关注你用的是 k8s、envoy、gitlab 什么基础设施，不用去学习一些特定的知识体系。\n\n### 资源评估中心化的运维\n\n![](https://image.eddycjy.com/549cfc258b5b09317e51edf0d640cf8d.jpeg)\n\n开发需要申请资源，运维需要评估，分配，再审核执行。最快是小时级，是平台的瓶颈。\n\n![](https://image.eddycjy.com/f7f163af78812e58c4d3c47b4e396ae6.jpeg)\n\n解决方案是把资源分片，实现团队自治，开发就可能承担了部分运维以往的工作。\n\n此时又会带来由于运维部分工作转移到开发后的成本问题，这部分就需要由平台来解决，提供下述全流程：\n\n1. 智能的容量评估\n2. 自动化提单/审批\n3. 自动下线（通过日志、性能、调用来识别）\n4. 自治并释放资源。\n\n最终的成效是目前所有的审批都是业务团队自己去做，运维只负责供应资源。\n\n#### 微服务下的依赖问题\n\n思考：服务 A 流量上涨，依赖的服务如何扩容？\n\n![](https://image.eddycjy.com/d077e4317cde1e70737c7d5616929159.jpeg)\n\n利用 istio 做全链路分析，实现全自动化，精准识别出入流量，计算差异部分，就能知道改变的服务链路和所需放大的倍数。\n\n### 实现可重复性\n\n应用跑不起来，肯定是有东西改变了：\n\n- 环境控制：镜像。\n- 构件控制：全自动化流水线，例如：代码版本、编译命令等。\n- 运行时配置：提供配置管理，实现版本控制。\n\n控制可运行时容器，就可以做一系列工作。系统提供默认安全，应用也就安全。\n\n### 变化是必然的\n\n面向故障设计：\n- 多可用区可用（异地多活？）.\n- 主机故障自愈/剔除能力。\n- 实例守护剔除能力。\n- 配置恢复能力。\n\n### 开发阶段如何提升效率\n\n做了个服务市场，业务应用。沉淀利用现有的能力。\n\n### 总结\n\n基础设施，团队自治，统一且自动化的交付。开发运维一体化，转移到开发的成本，要用平台来解决。\n\n### QA\n\n- 第一个：\n    - 故障定位，看基础设施，若是软件业务逻辑，软件业务逻辑是通过平台来提供工具来支持业务排查，帮助定位，大部分还是靠业务团队做的，若是基础设施基本都是大面积的。\n- 第二个\n    - 版本一致性，必然存在，发布有先后顺序，可以走蓝绿，业务逻辑也要做一些兼容处理。\n- 第三个\n    - 去抖动下发的策略，会持续监听 IP 并收集，配置下发有最小的间隔，例如十秒间隔的统一下发（存在一定延迟）。又或是到了一定数量级也会下发。不会来一发触发一条。\n- 第四个\n    - 开发要对利用率关注，压测，让平台上能够自动化去帮他实现，帮助他认识，自动生成出建议的数量和规模。同时服务的流量是分高峰，平峰的，平台要有提供自动扩缩容的机制（例如：也可以做定时策略）。支持自定义指标的扩缩容，要有一个超卖的策略。\n- 第五个\n    - 研发和运维的分界线，版本上线的告警目前都是由业务自己做的，代码是业务自己写的，暗含的逻辑运维必然不知道。开发要做全生命周期的跟踪。\n    - 统一网关不包含业务逻辑，更多的是支持公有云私有云，私有协议，一些登陆。业务网关更多的是跟业务相关的。\n- 第六个\n    - 长连接如何做无损发布，超过 30s 后 ipvs 感知不到连接断开，流量还是会分过来。存在局限性，要感知协议类型（内部做了针对性的自动化判定），判断流量是否结束，若结束则转发到新的。四层还是需要业务自己实现的。\n\n## 网易数帆从微服务框架到服务网格架构平滑演进及最佳实践\n\n介绍微服务相关的理念和服务网格的演进，前半部分非常高关注率，听众大量拍照。\n\n后半部分主要是网易轻舟的产品和技术介绍，主要是偏向 Java 方向，因此主要是在思想上进行参考，有一定的价值意义。\n\n从 2017 年开始进行 ServieMesh 的研究，不断进行打磨，直到 2020 年正式释出网易轻舟这一个技术产品。\n\n### 为什么要从微服务框架演进至服务网关\n\n在 2012 年正式提出微服务，介绍了微服务的发展史：\n\n![](https://image.eddycjy.com/e02b5f50d064103233b3adee3b96a510.jpeg)\n\n- 1.0时代：2011-2017，以框架为代表的微服务框架。\n- 2.0时代：2017-至今，以服务网关为代表，业务与治理解耦。\n\n### 微服务框架存在的问题\n\n1. 适用范围。\n2. 升级成本。\n3. 侵入性。\n4. 治理能力有限。\n5. 框架负担。\n6. 架构演进与云原生相冲突（注册中心部分）。\n\n### 服务网格的定义和优势\n\n服务网格定义为服务间通讯的基础设施层，通过轻量的网络代理进行拦截和处理。堪称异构语言的救星。\n\n### 服务网格的技术升级改造成本\n\n思考：我真的需要服务网格吗？\n\n![](https://image.eddycjy.com/dcf816fe7054b0a74c353321030b73ce.jpeg)\n\n1. 业务诉求是否只能用服务网格解决？\n2. 业务现状是否满足网格接入条件？\n3. 业务团队是否能够驾驭得了服务网格？\n4. 是否有开发配套的基础设施？\n\n### 演进的成本问题\n\nROI 策略，做成本分析。\n\n![](https://image.eddycjy.com/b06e00b230527202d3420d52eb4760e1.jpeg)\n\n### 接入诉求\n\n![](https://image.eddycjy.com/74cde9d40b00ceea6d10db88ce9a1512.jpeg)\n\n要关注业务期望收益是什么？\n\n### 微服务框架和服务网格共存\n\n- 微服务框架：面向应用内的服务治理。\n- 服务网格：面向服务间的服务治理。\n\n微服务框架和服务网格两者存在重合的区域，但又无法完全替代：\n\n![](https://image.eddycjy.com/46bf4496d08a7627f25dbf5588cfbfd4.jpeg)\n\n网易轻舟的平滑演进主要是针对 Java 系，对此 JavaAgent 做了一些调整（如下业务迁移），以此来实现平滑演进。\n\n### 业务迁移\n\n微服务框架 =》 融合（过度）期 ：存在流量管理的能力冲突 =》 服务网格\n\n逐步分离，缓慢实现技术升级。方案分为：\n- 通过 JavaAgent 迁移。\n- 通过网关做灰度流量迁移。\n\n### 服务网格与真实的使用场景差异\n\n设计上比较理想化，很难直接拿来给业务直接用，业务真正使用都要做定制化改造：\n\n![](https://image.eddycjy.com/8478ec28fa40c5f169aa51aa9b461b1d.jpeg)\n\n为了解决这些问题，网易轻舟做了以下增强：\n- 通讯协议增强支持（例如：Dubbo、Thrift）。\n- sidecar 的管理问题：每次的升级问题，社区方案每次都要重启 pod。网易轻舟是实现了热升级。\n- 多场景的限流方案：社区方案的性能常被人吐槽，并且场景支持不充足。\n- 基于服务网格的能力拓展：例如：监控。\n\n提供微服务框架和服务网格的一体化的控制台，简单来讲就是通过平台将用户的业务改造成本和学习成本和运维成本大幅度降低了。\n\n因此平台化是解决服务网格 “成本” 的一个出路。\n\n## 未来\n\n- 中间件 Mesh\n- 排障体系建设\n- 故障演练体系\n\n## 总结\n\n认为落地过程一定是曲折的，服务网格未来一定会是光明的。\n\n细节会是魔鬼。\n\n## QA\n\n- 第一个：\n    - 微服务框架到服务网格的最大难点：解决服务发现的过度问题，如何把注册中心打通。\n- 第二个：\n    - 2017 年开始关注投入，不断打磨，到 2020 年才出现网易轻舟。因为官方也不断在演进，他们也在不断演进。\n- 第三个：\n    - 中间件 Mesh，性能影响？目前主要还是偏向监测，访问成功，错误率，流量，使用的，偏向监控方面的设计。\n- 第四个：\n    - 现在遇到的业务诉求是否一定要用服务网格去解决？以及内部是否认同服务网格？\n\n\n## 不破不立：企业级研发效能提升的创新实践\n\n会场全场站满，讲师很有趣，经历丰厚，是研发效能的出品人。其介绍了许多研发效能和度量相关的知识和理念。\n\n同时驳斥了现在业界很多的一些基本的理念，让人深思。\n\n### 为什么研发效能火了\n\n为什么以前研发效能都没法塞满一个会场，为什么现在出现如此盛况？总的来讲是时代变了，商业逻辑变了，大家对研发效能有了更大的理解：\n\n![](https://image.eddycjy.com/679db6b81a9bf927f70c81bd1418fcff.jpeg)\n\n靠信息不对称，对称后如何在研发这一侧如何快速的交付，同时要高质量，既要又要。\n\n### 研发效能的五大 “灵魂拷问”\n\n概括研发领域的现象，如下图：\n\n![](https://image.eddycjy.com/3cac50b8521ddc22bcf9624ec7ee5693.jpeg)\n\n拉车的人（代指：老板）没空看轮子，不知道轮子是不是方的，而推轮子的人是我们工程师。你知道不知道轮子是什么形状？\n\n#### 第一问：研发团队的忙碌能够代表高效率吗？\n\n![](https://image.eddycjy.com/ac6a5cba95ed68e30ff314a0f028d4c9.jpeg)\n\n例如：凌晨半夜三点修 BUG 的人一定是最好的吗？BUG 很有可能是他埋的，他不修谁修？\n\n建议方向：\n- 架构的长期规划。\n- 中台的持续沉淀。\n\n#### 第二问：敏捷是研发效能提升的银弹吗？\n\n![](https://image.eddycjy.com/2ddb582a11acd6f750cc3f46aaa54520.jpeg)\n\n敏捷指的是能更快速的尝试，有问题的话马上调头。敏捷是要做小船。\n\n#### 第三问：自动化测试真的提升软件质量了吗？\n\n![](https://image.eddycjy.com/86c11968fd3789c7f65b488447106dae.jpeg)\n\n如果卡死自动化测试的覆盖率没意义，最后会变成覆盖率很高，走的很慢，因为让覆盖率变高有非常多种方法。\n\n而卡死自动化测试，就会导致没有精力去做探索性测试，更多的测试。需求变了，自动化测试又变了，又要花时间继续做新的自动化测试。。\n\n自动化测试只是个手段，不是目标。新功能不应该做自动化，**功能本身趋向稳定了，才应该去做自动化测试，成本才低，成就感才高**。\n\n#### 第四问：没有度量就没有改进，这是真的吗？\n\n![](https://image.eddycjy.com/b49cb46586a53de77a562fcba314527e.jpeg)\n\n研发效能很难在真正意义上度量，软件研发是创造性的劳动，不同的人来做是不一样的，硬要做，就会变成你度量什么，工程师就做什么。\n\n你度量钉子，那你得到的就是钉子。你度量什么，就一定会得到什么。\n\n不要用来考量 KPI，否则千行就会变成万行，要慎重。\n\n#### 第五问：研发效能的提升一定是由技术驱动的吗？\n\n![](https://image.eddycjy.com/358b682fb20ba6b54769b3a0c1186138.jpeg)\n\n不要陷入局部思维，真正的问题不是单点的问题。\n\n例如：看医生，真正挂号多久，但你真正花时间的是排队，看完医生1分钟，又开单验血，又等。因此等待时间是最大的。\n\n在软件领域中，也是在等待时常上花的时间最久的。是部门墙，信息不对称导致的问题。\n\n### 研发效能到底是什么？\n\n![](https://image.eddycjy.com/df4963c29b6fa4cf6d452093b43ddf00.jpeg)\n\n先有的现象，再有的结果，定义。\n\n### 研发效能提升的案例\n\n- 前端代码的自动化生成。\n    - 工程师在白板上画 UI，自动识别并生成出代码和界面（利用了 AI 的相关技术）。\n- 临界参数下的 API 测试\n    - 自动的测试数据生成。\n- 微服务架构下的环境困局。\n    - 公共基础环境的问题，高效的方法是做公共基础环境，也就是现在的云端环境。每天和生产环境同步。\n\n### 研发效能的第一性原理\n\n![](https://image.eddycjy.com/a74a6f1aa5e19ac991402597853ed1d2.jpeg)\n\n顺畅，高质量地持续交付有效价值的闭环。\n\n- 做有价值的东西，做用户需要的，不要做用户不要的。\n\n- 凡事做的事情能让这五个 “持续” 提高，就算研发效能。\n\n- 所有的过程改进要用数据说话，但不要用来考核。\n\n### “研发效能” 的点点滴滴\n\n![](https://image.eddycjy.com/d0abd507792ff6c98882b5887ddfe2a0.jpeg)\n\n研发效能的点点滴滴，做这些都能提高，针对性举例：\n\n- 例如：云 IDE，非常方便。\n\n- 例如：（举例 sonar）sonar 的机制为时已晚，没什么意义。可以在本地就去跑 linter，走得快质量还高。\n\n- 例如：代码复杂度，最终呈现的就是软件和架构的腐化。研发工程师就开始复制粘贴改，最后没几年就废了。\n\n- 例如：代码递交规范，你会把需求id带进去吗？不带的话，后面所有的度量都没法做，追踪都没法做，拿不到需求 id 都没做。\n\n- 例如：分布式编译，各个模块分散到分布式去编译，从十分钟变 10 秒。\n\n### 研发效能提升的一些经验和实践\n\n![](https://image.eddycjy.com/57a3b325f949fc24b03ab1db8b221f89.jpeg)\n\n推荐看书，用 MVP 思想做，和做通用工具完全不一样。\n\n研发效能要先发现钉子，再去找锤子。和做通用工具不同，工具是拿锤子找钉子。\n\n研发效能一般采用逐渐扎小孔，一层层做的模式。每次给的功能点足够小，但每一次给的都有价值。\n\n![](https://image.eddycjy.com/6b574fba0ab6c360e3a2b5440f7450e1.jpeg)\n\n做 MVP 不是横切一刀，是包含各方面的斜切。\n\n![](https://image.eddycjy.com/c7cd61c07e131800a9febaa8ba675b65.jpeg)\n\n这部分内容太太太多了，讲师也没有讲完。下方为根据讲了的部分整理：\n\n- 从痛点入手：\n    - 测试数据的搭建统一到测试数据中台去做。\n    - 如研发度量的数据获取是最重要得，例如由工具自动触发状态的改变，而不需要研发工程师去调整，且获得的数据是真实有效的。\n- 从全局切入：\n    - 例如：一个 BUG，真正修复的时间是多少？\n- 用户获益：\n    - 让用户获益，是研发效能的核心\n    - 不要你以为业务团队要，业务团队关心的是温饱问题。例如：你就看看业务团队自己有没有在搞，他们有在搞，这就说明是真的需求。\n    - 结构很重要，如果设计的体制要每个人都大公无私是必然失败。每个人越自私越自利越能成功。举了和尚分粥的例子。\n    - 谁接入了多少不是最重要的，是业务得到了什么。\n    - 服务意识，早期保姆式服务，沉淀后，就是双赢，\n- 持续改进\n    - 例如：GitHook 直接调 JIRA API 不是最好的方案，没有版本管理，规模大了绝对不是好方法。\n    - 应该走消息队列，揭藕。平台化。\n- 全局优化\n    - 下层提出，上层认可。\n- 杜绝掩耳盗铃\n    - 虚荣性指标 vs 可执行指标\n    - 例如 sonar 接了多少个项目，就是虚荣性指标。应该考察可执行指标，例如严重 BUG 存在了多久。\n- 吃自己的狗粮：\n    - 自己的产品你都不用，别人更不可能。\n\n### 研发效能的未来\n\n![](https://image.eddycjy.com/3e02bf64de89130bdc29fe5d5a69cb60.jpeg)\n\n表达核心观点：“敏态” 和 “稳态” 一定是齐头并进的。\n\n## 快狗打车可持续交付实践\n\n主要面向测试环境治理的演讲，Devops 不是单纯的技术问题，整体来看 Devops 是一个复杂的混合问题。\n\n### 理想与现实\n\n快狗打车前期是存在固定的多套测试环境，测试环境相互影响。测试同学 A 用完环境，第二天给了测试同学 B，B 同学发现有问题，又找回 A。A 同学又认为不是自己的问题：\n\n![](https://image.eddycjy.com/a1935c4d63890b97e5639c5723f49f4e.jpeg)\n\n### 测试环境V1\n\n早期测试环境的具体表现，主体为稳定环境全量部署，下分四套环境，根据需求部署：\n\n![](https://image.eddycjy.com/d8ccd7a945a1389126d4c13771ba79bc.jpeg)\n\n早期几十个集群还没什么问题，等到规模变大后几千个集群后问题就会很严重。同时测试人人都有管理权限，第二套变更后，会同步到稳定环境，那么其他几套环境的同步由谁负责（他们早期是手动维护）。\n\n![](https://image.eddycjy.com/14361c5532b6b7ae6df55890b90788f7.jpeg)\n\n另外并行需求多了就会发现固定的测试环境不够用。呈现的结果是投入产出比差异过大，各配置互相影响，稳定性很差，最终造成的测试结果也不稳定。\n\n### 理想的测试环境是怎么样的？\n\n- 即点即用\n    - 任何时间都可以部署，并不需要排队。\n- 自动隔离\n    - 任何环境都是相互隔离的。\n- 依赖关系\n    - 系统自动解析，根据配置自动部署依赖上下游，使用者无需关注。\n- 缩放自如。\n    - 资源池管理，资源弹性伸缩。\n- 独立闭环\n    - 独立部署，无需跨部门沟通（不用找运维要资源）。\n\n### 第一轮的优化实践\n\n核心要点：规范、格式、自动。\n\n![](https://image.eddycjy.com/d05babd370f63589d17cd767370d2f7a.jpg)\n\n针对各服务做依赖关系的自动解析。细则如下：\n- 制定了配置文件的格式规范，用于扫描上下游依赖，层层扫描，最终得出整体的依赖图。\n- 规范要符合公司现状，绝大部分都能适配，只有让小部分的人要改。\n- 服务按照类型优先级部署，这里结合了应用信息（上层应用服务，底层应用、数据库、Redis 等）。\n\n### 测试环境 V2\n\n只有一套稳定环境，剩余的都可以按照需求来拉环境。但存在服务直连的情况，导致出现流量拦截和调动有问题。\n\n属于企业内部自身的债务问题了，不展开赘述。\n\n### 测试环境 V3\n\n结合基础架构组，最后实现按需部署，谁开发谁部署，不改动不部署。\n\n属于企业内部自身的历史债务问题了，不展开赘述。\n\n### 总结\n\n在治理优化实践上一共做了如下：\n\n- 测试环境的服务按需部署。\n- 依赖环境的自动解析。\n- 部署资源池管理：评估部署所需资源，再通过资源管理平台，统一资源的调度。\n- 自动流转与执行：Nginx（vhost、location）、域名（独立域名、泛解析）、MQ、堡垒机等的自动申请、审核。\n- 资源自动回收：需求完成上线后，需求所关联的都自动释放，又或是回收到资源池。\n\n整体上来讲，技术不是难点，最难的是人与人的沟通，例如：跨部门的沟通，最重要的是方向、坚持、执行力。\n\n### QA\n\n- 第一个：\n    - 测试环境数据库也没有采用按需，因为测试环境数据库不是主要痛点（矛盾点），主要权衡的是投入产出比，因为并不是建起数据库就能解决的，还要造数据，各种成本很高，结合权衡没往这个方向做。\n- 第二个：\n    - 资源低于 60%，则针对于已经完成上线的机器，并不是资源池内的。\n- 第三个：\n    - 部署的互相依赖和网状结构，通过打标签的方式，若已经部署了则不会再部署了。\n- 第四个：\n    - 资源平台优化策略，目前正在转向 K8S 这类云平台，后续不再自行关注。\n- 第五个：\n    - 公共组件是否也是重新部署，目前 redis 是重新部的，主要是针对 mysql、redis。kafka 这类是没有独立部署的。\n- 第六个：\n    - 最大的困难是什么，是 “人”，人的认知，达成全员的共识，为什么做这件事情，讲清楚，比做什么更重要。\n\n## 自如云原生落地最佳实践\n\n主要演讲内容是自如的云原生演进之路，如下：\n\n![](https://image.eddycjy.com/6ec794425d71b36638c5d967256baa76.jpg)\n\n当时存在大量的问题，进行了调研。结果提示低 NPS（-44%），也就是 100 个里有 52 个人对 CI/CD 系统不满意。\n\n具体的痛点：\n- 运维人肉运维。\n- 生产、测试环境不一样。\n- 分支合并漏发代码、漏发配置。\n- 上线发布一台台点。\n- kvm CPU 使用率低。\n\n进过调研和选型，发现开源的均有缺点，最终选择自研一站式研发平台。主体功能结构：\n\n![](https://image.eddycjy.com/c80ee685792aa265366b5cea0d3f91d9.jpg)\n- 上层的平台服务：面向开发同学。\n- 下层的 K8S 等：面向 Ops 同学。\n\n平台在整体的设计边界和原则上如下：\n- 边界：只做无状态应用的容器化。\n- 原则：能放到平台的操作坚决不用人。\n\n### 容器化后遇到的问题\n\n容器化后一堆新的知识 pod、ingress、service，网络模式也变了，开发同学都不懂，产生了大量的成本（学习、运维等）。\n\n因此就决定了做应用平台，也就上面提到的平台服务。流程图如下：\n\n![](https://image.eddycjy.com/66886bf86b9217dc0fcb92fb7a5d6ff2.png)\n\n### CI/CD\n\n- 定规范，统一环境。\n- 定分支，统一分支模型。\n    - 在各个 feature 分支上进行开发。\n    - release 分支环境用于集成和发布。\n- Dcoker/Deployment 零配置\n    - 根据创建应用所填写的信息自动配置，研发不需要关心。\n- 工具-跳板机\n    - 在平台上做跳板机，不需要关心 IP，也不用登陆。\n\n### 总结\n\n- 云原生平台化，运维 0 参与。公司标准化（环境、分支等）。\n\n- 不要闭门造车，统一思想，走 MVP（步子不要迈的太大）、持续运营、持续关注 NPS。\n\n### QA\n\n- 第一个\n  -  流量染色，是为了动态的的调控服务调用。\n- 第二个\n   - 数据库污染，利用账户体系来做。同时注意 mq 这类需要隔离。\n- 第三个\n   - webshell 创建 bash 不要太多，超过 32 个会有问题。产生僵尸进程。\n- 第四个\n   - 微服务到云原生的成本，学习成本必然，把 dev 和 ops 放到一起。\n- 第五个\n    - 目前自如是让业务在平台上配一个专门的探活接口，再去探活。\n- 第六个\n    - 最大的阻力，就是人，CTO 把基础架构把运维放到了一起，形成了互补。组织结构要先调整。\n\n\n## 研发效能度量的误区、体系化实践和效能提升案例\n\nDevops 专题的出品人，会场火爆，全部站满。开局表示现在已经不再是讨论要不要 Devops，而是讨论怎么去做。\n\n讲的很好，会场人员认可度高。\n\n### 研发效能的情况\n\n- 你的研发效率在业界属于什么水平？与竞争对手差距？\n- 敏捷转 Devops 的转型有没有效果？是否可以量化评估吗？\n\n### 软件交付效能的度量指标\n\n![](https://image.eddycjy.com/f4b2db7fb494a5eaea9fdd5d79f8409a.jpg)\n\n- 部署频率。\n- 变更前置时间。\n- 服务恢复时间。\n- 变更失败率。\n\n### 研发效能评估（愿景）\n\n#### 阿里（211）\n\n- 需求 2 周内交付。\n- 变更 1 小时内完成发布。\n- 需求 1 周内开发完毕。\n\n#### 腾讯\n\n- 项目团队规模扩张控制在 20 人以下\n- 迭代周期在 1 周内\n\n#### 研发效能度量的原则\n\n- 结果指标 > 过程指标。\n- 全局指标 > 局部指标。\n- 定量指标 > 定性指标。\n- 团队指标 > 个人指标。\n- 指导性，可牵引行动。\n- 全面性，可互相制约。\n- 动态性，按阶段调整。\n\n### 工具链网络\n\n![](https://image.eddycjy.com/eefc72a3c3125e995459d7737016ee56.jpg)\n\n- Devops 工具链网络：强调 “网络”，工具与工具的关联，代码与需求，代码与合并，与编译，各种信息能不能追溯到下面所有的环节（把工具串联集成起来）。而不是单单管理某一个领域。\n    - 项目协作域。\n    - 开发域。\n    - 测试域。\n- 价值流的交付模型：要从用户、客户的视角去看，从端到端的角度去看，而不是开发、测试的角度。要从完整的一个用户需求提上来每一步的具体步骤。\n    - 工作流。\n    - 生命周期。\n    - 流动效率（不是资源的占用率）。\n- 效能度量分析模型：软件研发效果，最终思考的是组织效能、业务结构。\n    - 交付效率：需求前置时间、产研交付周期、需求吞吐量。\n    - 交付质量：变更成功率、线上缺陷密度、故障恢复速度。\n    - 交付能力：变更前置时间、部署频率。\n\n\n给出了分析模型里的大量的度量考察指标，并表示企业内部有更多，几百个。但要注意度量指标不在于多，在于精。不同阶段也不一样，要有北极星指标。\n\n![](https://image.eddycjy.com/a8925088b7aeecaabf6e90c843f90a4b.png)\n\n你做的实践不一定代表有用，但要不断地思考和实践并改善。例如单元覆盖率，有个公司永远在 80%+，后面发现是对 KPI 战法，甚至单元测试里没有断言（多个讲师提到）。\n\n不仅要关注创造价值的工作，还要关注保护价值的工作：\n- 业务需求。\n- 产品需求。\n- 研发需求。\n\n### 企业内部实践\n\n展示了京东内部的研发度量系统，看上去非常完善，可以进行多层次的下钻（事业部 -> 项目组 -> 研发人员）：\n\n![](https://image.eddycjy.com/b36d995cc4f347bb6b7114fe4e515eda.jpg)\n\n### 总结和避坑\n\n- 成本问题：看板里的数据，如何让度量更准，那就是标准，那就需要大量培训。让需求和代码有关联，自动触发变更状态。自动化。\n- 避免平均值陷阱：类似长尾问题，尽量用分位数。\n- 度量不是为了控制，而是指导改进：如果是 KPI，你度量什么就会得到什么，只是不是以你所希望的方式得到的（古德哈特法则）。\n\n总结：**那些不懂数字的人是糟糕的，而那些只看数字的人是最最糟糕的。应该走下去看具体是什么达成的，走到工作现场，看看是否真的有改进**。\n\n## 京东 BDP 的全域监控、管控平台搭建实践\n\n### 基本介绍\n\n基于 Prometheus 生态进行了大量的改造：\n- 采集端改造：PushGateway 会推数据到 Kafka，再另外消费。\n- 模块拆解，作为不同的角色，读写分离，便于扩展。\n    - 数据采集。\n    - 预计算。\n    - 数据分析。\n    - 监控告警，\n- 多级缓存：监控数据的数据是短时间内不会变的，会进行缓存（不同业务可配不同）。\n- kongming 服务：基于不同的 promql 决定执行什么策略，例如：实时作业、离线任务、集群调度等。相当于是一个拓展了，高级监控治理了。\n\n### 监控实践\n\n- 单点监控：常见面板，\n- 组监控：业务提供黄金指标，自动生成对应的组监控，可以做到千人前面。\n- 关系链监控：父级节点和大表盘。\n\n### 平台实践\n\n平台提供让业务选择，业务不需要关注底层的表达式：\n\n![](https://image.eddycjy.com/e9c2893dad1a97389131018582d6fdeb.jpg)\n\n在更具体的实践上：\n\n- 告警通知：支持父子节点的通知。\n\n- 告警通知：支持告警人的动态通知，支持业务在上报指标时指定的。\n\n- 高级治理：利用所拓展的 kongming 模块，做了许多基于历史数据和现状的干预，例如：实时作业干预、智能调度（削峰、自愈等）。也就是相当于 “人工智能” 的那一块相关内容了。\n\n总体来讲，做自动化也会是类似的思路，要拓展出一个模块。这样子你做什么都可以，只要基于 Prometheus 的一些表达式和数据源就可以了。\n\n### 总结\n\n监控系统不仅要能发现，还要哪能解决问题，监只是手段，控才是目标。\n\n解决各种人力问题。\n\n## 淘宝系 - 云原生下编程方式的探索和实践\n\n### 淘宝现状\n\n![](https://image.eddycjy.com/2e85139d64d801a8b20cfe1439262689.jpg)\n\n- 中心化：微服务化。\n\n- 去中心化：FatSDK，以 SDK 的方式提供能力。能够保障稳定性和性能。淘系绝大部分都是采取的第二种模式。出问题的话，就只有你自己的服务有问题，不会影响其他依赖方。\n\n在 SDK 上他们只提供 Java，其他你自己想办法，常见的可以做个代理。\n\n### 通用能力下沉\n\n把原有 SDK 的能力剥离到独立的进程，而不是像原本那样沉淀在应用中：\n\n![](https://image.eddycjy.com/e92806927597cbe146488d698ef02eea.jpg)\n\n利用云原生的容器，提供运维能力容器，业务能力容器，解决中心化的问题。在此书其实是参照了 ServiceMesh 的方式，走 Sidecar 就好了。\n\n### 解决多语言问题\n\n加多一层，提供 GRPC API 来进行对接。对于业务来讲只需要面对标准化的 API 就可以了：\n\n![](https://image.eddycjy.com/767257e017a6a496a0d4f116e8cd5277.jpg)\n\n业务不会直接对外对接暴露，所有东西都是通过对外/对内的中间层来进行衔接：\n\n![](https://image.eddycjy.com/2709782851a3590f94711996866a4022.jpg)\n\n### 开发市场\n\n做了一个应用市场，业务开发可以提供组件能力上去，避免重复造轮子：\n\n![image](https://image.eddycjy.com/4737ce38878a622436193d91f931d245.jpg)\n\n## 全面监控体系建设及智能监控的探索实践\n\nPPT 内容比较抽象，简单来讲：AIOps = 大数据+算法+运维场景。\n\n通过各项能力，建设了对于历史数据的分析，做了各种分析和告警合并等场景。每个模块都大致讲了，但均没有深入讲，只能大概听个响，与原有所知的监控思路基本一致。\n\n智能运维业界目前没有开源方案，都是一线大厂分享的经验。放一张 PPT 图：\n\n![](https://image.eddycjy.com/a52c0e553978fd959973f9c56081b963.jpg)\n\n按分层自下往上看，基本是基于数据进行智能化的预示来达到效果。\n\n## 低代码技术在贝壳的实践\n\n![](https://image.eddycjy.com/f0f5c4b8cd9416272e2d3fa4b72c2cf4.jpg)\n\n更具体的演示效果可看平台方发的视频（期望/现状）。\n\n\n### 提效\n\n![image](https://image.eddycjy.com/c94415916dbd8d3c1ede248b9b5ef4ca.jpg)\n\n能覆盖到的场景基本把前端功效给去掉了，但同时后端的工作量会加大。\n\n### 现状\n\n- 河图1.0：已开源。定制化需求，一开始想让客户自己开发插件化，效果不行。最终决定走向智能化。\n\n- 河图2.0：智能化。\n    - 期望：自动识别设计稿，国外有，支持多端。\n    - 目前：\n        - 贝壳现在支持的是上传 sketch 设计稿，支持不同的 iOS，安卓，Flutter 以及自己的小程序。\n        - 支持后台管理增删改查，小程序，中后台等。\n    - 未来：\n        - 后期会引入智能化，将会持续开源。\n\n### 投入的人力\n\n- 第一期：最初是3个人，做了两年，从 2019.1 做到 2020.12。\n- 第二期：目前投了 10 个人。\n\n### 复杂场景\n\n- 第一类通用类：\n    - 目前可以解决。\n    - 例如配置系统可以用。\n- 第二类定制化：\n    - 要靠智能识别。\n    - 目前的确只能解决一些通用常见的问题，复杂问题还需要人来解决。\n\n## 总结\n\n目前业界中其实存在着大量的方案和思路，很多大厂其实更多的会根据目前公司内的实际情况进行选型和设计。听这类会议/培训，一定要关注的是其解决思路和途中的思考，是为什么这么做，踩过什么坑，比较重要。\n\n希望大家在看完后都能有进入心流的深度思考时间，那样你才能消化到知识，并转换到你实际的工作中去。\n"
  },
  {
    "path": "content/posts/2021-ecug.md",
    "content": "---\ntitle: \"推荐一个牛逼的技术社区！\"\ndate: 2021-01-05T21:26:50+08:00\ntoc: false\nimages:\ntags: \n  - ecug\n---\n\n相信我的读者中不少是 Go 语言的爱好者，又或是正在伺机而动。\n\n今天要给大家所介绍的这个技术社区，就是由与 Go 语言有很浓厚的缘分的人所创办的。\n\n他有如下几个业界标签：\n\n- 早期的国内 Go 语言布道师。\n- 早期在公司内大规模的推广和使用 Go 语言。\n- 早期编写了一本 Go 语言图书：《Go 语言编程》。\n- 现在是一家公司的 CEO。\n- 近期在大力推广 Go+。\n- ...\n\n他还有非常多的标签，通过上述这几点，你是否猜到是谁了呢？\n\n![](https://image.eddycjy.com/0c8ac8602cca4e19c8caca30ac991305.jpeg)\n\n没错，他就是七牛云的 CEO 许式伟。\n\n## 与 Go 语言的渊源\n\n许式伟在早年离开盛大创新院。创办七牛云的时候，选择了 Go 这门还未发布正式版的语言。因为小众，许式伟开始有意识地培养 Go 中国社区。\n\n他们做了很多工作。具体有：\n\n- 2012 年 2 月，许式伟首次在公开场合说：Go 会超过 C、Java，成为最流行的语言。讲得最多的一个 PPT 是《Go，Next C》这篇。\n- 2012 年 8 月，正式出版 Go 图书，书名为《Go 语言编程》。\n- 2020 年下半年，正式对 Go+ 进行了宣传和推广，对大数据科学的领域进行了增强。\n\n## 神秘的技术社区\n\n虽然许式伟已经是七牛云 CEO，但依然在技术领域和咱们的 Go 领域发光发热，并没有因此而停下脚步。许式伟早在 2007 年就成立了一个技术社区。\n\n![](https://image.eddycjy.com/f04e24b25f48c3d2293e64390d22888f.jpeg)\n\n这个社区名字叫 ECUG，ECUG 全称为 Effective Cloud User Group（实效云计算用户组）。\n\n其成立于 2007 年的 CN Erlounge II，由许式伟发起，是科技领域不可或缺的高端前沿团体。作为行业技术进步的一扇窗口，ECUG 汇聚众多技术人，关注当下热点技术与尖端实践，共同引领行业技术的变革。\n\n![](https://image.eddycjy.com/1cba758a30621c7b4db7c92ae9e739d0.jpeg)\n\n截止到 2020 年 ECUG Con 已成功举办了 13 届，ECUG Con 的技术主题主要涉及：云计算、数据、区块链方向。\n\n**今年第 14 届 ECUG 大会将于 2021 年 1 月 16 - 17 日在上海举行，为期两天**。作为 Gopher，身处云原生时代，这样的盛宴不容错过。\n\n购票地址：\n\n![](https://image.eddycjy.com/017bf73c7eaa90dc99d793324b347e88.png)\n\n大会议程如下：\n\n![](https://image.eddycjy.com/1b3475f904becad56e9450aff88d9402.jpg)\n\n大会嘉宾：\n\n![](https://image.eddycjy.com/177b353f31903dcde292755a84af5e73.png)\n\n## 各种福利\n\n这次大会煎鱼**为大家申请到了 15 个免费名额，我会从留言者中随机选 15 位送出**。\n\n\n同时本次大会还有技术嘉年华的环节，会送出各种礼品：\n\n![](https://image.eddycjy.com/da791fb34cb3a8bbd353bb9171b8c180.jpeg)\n\n有兴趣的鱼粉们赶紧留言，报名参加 ECUG 吧！"
  },
  {
    "path": "content/posts/2021giac.md",
    "content": "---\ntitle: \"我周末参加了个架构师大会！\"\ndate: 2021-12-31T12:54:57+08:00\ntoc: true\nimages:\ntags: \n  - giac\n---\n\n大家好，我是煎鱼。\n\n前两天 GIAC 全球互联网架构大会在深圳举办了，总算是有个长年在深圳举办的大会了，愉快参加了大部分的场次，面基了不少社区网友。\n\n分享一些我听了觉得有意义的记录给大家。希望能和大家一起学习进步。本文分别涉及如下几个议题：\n- 《hits for microservices desgin》\n- 《在企业中的个人成长》\n- 《大规模任务调度在 AfterShip 的高可用实践》\n- 《快手前端实时性能监控和稳定性度量》\n- 《快手中间件 mesh 化实践》\n\n## hits for microservices desgin\n\n一开始先介绍了为什么叫 ”hits“。叫 ”hits“ 的主要原因，是**业务架构没有技术架构那么明确，没有明确的对与错，是个人的工作经验和总结**。\n\n### 微服务解决什么问题\n\n业内常常说到，微服务，微服务。总归期望微服务解决什么问题。\n\n演讲的作者做了如下的调研：\n\n![](https://files.mdnice.com/user/3610/903dfcbb-a0b6-481f-a40e-3476b8ac8b64.png)\n\n从调研结果来看，占比最大的就是 ”独立自治，只关注自己的模块“。这和绝大部分既有业务的公司做微服务的初衷一致。\n\n许多就是被单体的巨石应用折腾的不行，纷纷希望通过拆分微服务来实现业务模块的独立自治。\n\n### 微服务的现状\n\n主要是播放了动图，配合口述。现在大多数服务拆分后的现状，很多就是**改哪影响哪完全不清楚，和水管漏水似的**：\n\n![](https://files.mdnice.com/user/3610/f871b4de-c8b6-47b4-a5e7-5b9f64b73aea.jpg)\n\n(自行脑补一拧水管，堵哪，别的地方就漏)\n\n\n### 衡量微服务拆分的标准\n\n理想中的微服务拆分，希望要有灵活的组装能力。但拆分后遇到的新问题，实际的情况，拆分后与期望的不一样，拆着拆着就变成了一大坨，但只是说隔开了，与现在企业中微服务运行的现状很贴合。\n\n拆分后如有如下几个痛点：\n\n![](https://files.mdnice.com/user/3610/0890ca8a-4fa1-4173-8334-92a167a47a19.png)\n\n举了几个案例。分别是：\n- 订单的例子。\n- 报价的系统。\n- 数仓的例子。\n\n#### 订单\n\n举的是订单的例子，订单团队非常忙，因为信息都存在订单里，系统其他有任何业务上的变更诉求，都要找订单的团队。\n\n为此，在拆分上需要优化成订单业务只保存订单 ID：\n\n![](https://files.mdnice.com/user/3610/24a64dd9-98ae-419f-afef-aa125513dbd2.png)\n\n各系统存订单ID，各团队自治，实现业务解耦，订单团队就不用因其他业务变更天天加班了。\n\n\n#### 报价\n\n举的是报价的系统，要是报价团队，针对各个子业务项都要自己实现一般，会非常的辛苦，经常要加班。\n\n我们只需要在报价系统提供接口标准，各系统自己实现，再对接：\n\n![](https://files.mdnice.com/user/3610/92f5b5eb-e5de-4bfd-bab6-f3e9ab2d7a6d.png)\n\n报价团队就不需要每次都重新开会，再对接。报价系统自己只做业务流程的编排，瞬间变轻松了。\n\n#### 数仓\n\n举的是数仓的例子，业务改一个字段，数仓系统要改一个月，否则就会出现问题，因此要求业务有任何 schema 改变都必须要通知数仓团队：\n\n![](https://files.mdnice.com/user/3610/b1a09935-9663-449a-8646-b55ef3e08b7c.png)\n\n很现实的是，基本通知不过来，所以很多公司把他作为绩效，定期考核，出问题定期批评。\n\n建议的是：通过 RPC 的方式提供维护，把数据维护交给业务团队自己维护，数仓团队应该只做具体的跨团队的数据互联。\n\n#### 好的标准的定义\n\n分层，都可以独立变更，可以自己搞自己，只需要保证这一层提供的能力是稳定的就好（全部改一遍的另当别论），不需要了解上下游，只需要维护好 interface。\n\n具体几点：\n- 不同模块间完全没影响\n- 只共享不可变数据\n- 共享可变数据，但接口不变。\n- 大部分情况变化的是实现，变化的不是接口，接口的变更次数很少。\n\n参照乐高，关注接口的稳定性，而不是拆的越细越好。\n\n评价的标准是：看不同系统不同模块的互相影响程度，就知道各系统怎么样了。\n\n### 小结\n\n1. 不要在一开始就使用有意义的名词，例如：交易中心，支付中心。大家会根据名字来设计的架构，建议最后再起名字。\n2. 复用不是目的，是手段。例如业务中台真的是复用吗，不是。只是互联互通。\n3. 好的架构，是要控制复杂度，在一定的规范下尽量自治。\n4. 先划分清楚业务模块，划分清楚了，再去设计你的技术架构。\n\n提问时也有涉及到 ”分布式事务“，这简直就是微服务相关议题的必问话题。演讲者表示：倾向持续交付。尽可能不让他有分布式事务。\n\n## 在企业中的个人成长\n\n毛老师的演讲，据说全场综合评分最高，内容是分享了自己在企业成长的三个阶段：\n- 阶段一：加入 Startup 公司。\n- 阶段二：轮岗。\n- 阶段三：重新出发。\n\n分享比较有明确的时间线，我是直接按点来记录的，刚好十条，非常经典。\n\n### 十条纲要\n\n1. 15 年，也不知道什么是 B 站，也找不到人。从以前认识的一些熟人，从别的公司挖回来。**小公司，亲自带。自己熟悉业务，请教老员工文化**。\n\n2. 团队到 40~50 就要考虑做人才梯队了。要保证每个月有 1v1 的简单聊一聊，或是每天现场聊聊。**稳定性问题和管理有关系**，有没有研发红线，例如发布变更，生产无小事。\n\n3. 改变一个人性格非常困难，只能告诉他，给一两次机会。**不合适就放弃，心慈刀快，尽快解决掉**。合适的人，给过两三次机会，自己早就转变过来了，两三次还不行，肯定是难以改变的了。\n\n4. 要做核心的事情，不要什么需要都做。**不要用技术实现去挑战老板的战略考虑，要用业务**。\n\n5. 不要害怕空降，**引入新的人，有竞争压力，有进步**，要做减法，聚焦最重要的事情。向他们学习，看他们的优势，择机成长。\n\n6. 要换位思考，站在平台方，成就业务。**最好的团建是一起拿成果**。从老板的角度考虑，绝对能折腾，绝对能将就，就像基础架构部门，有时候要业务优先。\n\n7. 影响力，技术辐射范围足够广，不同团队落地，自然而然就有了影响力。公司外的影响力，多分享，多参加 meetup。\n\n8. 哪里有需要就去哪里。\n\n9. OKR 要足够的透明，足够的明确。下级要知道，要清楚，甚至是自下而上的 OKR。\n\n10. **团队的会模仿你，看别人的缺点，修正自己**。自己在团队中要做榜样（举例：早点下班的问题）。\n\n### 小结\n\n在职业生涯发展上的小伙伴，建议可以看看毛老师的分享 PPT，看看大佬从 0 到千亿万身家的个人成长发展史。\n\n就如总结所说的：”当你很忙碌的时候，你的管理工作一定出了问题“，值得思考。\n\n## 大规模任务调度在 AfterShip 的高可用实践\n\n初始的业务需求是有优先级调度的需求，用户的调度比物流包裹的优先级高。\n\n任务调度的量比较大，要能运行千万级的优先级任务调度。\n\n### 老版本\n\n老版本是根据 15 分钟划分一个 Topic，可以理解为分区，一天 96 个。\n\n采取的是轮询策略，还没执行的放进延迟队列。实现了 15 分钟粒度级别的任务级别(没法做到 1 分钟，5分钟的这类纬度)。\n\n![](https://files.mdnice.com/user/3610/d78359d3-5c59-4535-a0ed-2214ade04302.png)\n\n存在以下问题：\n- 会导致出现波峰，资源浪费。\n- 设计过于复杂，导致系统脆弱。\n- 链路过长，定位困难。\n- 错误的 FIFO 实现。\n\n### 新版本\n\n![](https://files.mdnice.com/user/3610/36b6fce2-c23a-451a-a538-4df3cac19be5.png)\n\n解决思路: \n- 通过 LMSTFY 任务队列，解决延迟和优先级功能，解耦业务。\n- 基于 Redis，正在做二级存储，冷热数据隔离。\n- 通过指定多个队列，来实现多个优先级调度。取多个队列，A队列有内容，则优先消费A。\n- 通过实现接口来队列多个数据存储。\n- 借助组件的特性和系统优化，简化了架构。\n\n### 小结\n\n相当于是看了一个任务调度系统的设计发展史，理论上设计的存储和方案不一定得选 Redis 或是 LMSTFY。\n\n不过考虑到演讲者的背景，因此趋向了这个技术方案，也可以从中看到后续任务调度系统规模更大后，可能出现的问题。\n\n## 快手前端实时性能监控和稳定性度量\n\n公司内也有类似的系统，与快手的前端 APM 是一模一样的定位，可以借此看看成熟的系统的选型和发展情况。\n\n### 前端的数据\n\n需要有统一性能指标，用一个指标代表:\n\n- 早期：DCL，Onload，来判断前端页面的性能。页面依赖接口数据容易被绕过，长页面不能反映用户真实感知。\n\n- 设想指标的作用：希望获取白屏时间，代表白屏到非白屏的时间。\n\n但发现也无法保证，计算白屏可能会导致客户端崩溃。不能代表页面核心内容的时间：\n\n![](https://files.mdnice.com/user/3610/0afbfd6c-e9ca-4357-8ddb-c45f26f2c1f7.png)\n\n转为目的是拿到主要内容渲染的时间，业内常见用 FMP、LCP：\n- FMP（First Meaningful Paint）：没有标准实现，对页面细微变化或于敏感。某一个 DOM 内部有很多个节点，可能会造成误差。\n- LCP（Largest Contentful Paint）：页面内最大内容的渲染时间，最大元素不一定是最重要的元素，例如新闻内的图片。并且浏览器的支持率只有 70%（致命），因为快手很多低端机是不支持的。\n\n### 真正要的指标\n\n考虑业务需要的指标是什么，本质上业务的真正内容是**从接口异步获取回来的**。\n\n因此采取了自定义 FMP，API 响应数据渲染到页面后的时间，代表页面的性能：\n\n![](https://files.mdnice.com/user/3610/5a6f62f8-2ef7-4500-b7a5-b47c62d526ba.png)\n\n这个自定义方案要业务自己计算的，会调用提供的统一方法来计算。虽然有一定侵入，但准确性最高，最有效。\n\n主要有以下三种统一度量指标: \n- API 性能/异常。\n- 资源性能/异常。\n- 脚本异常。\n\n也做了容器的性能数据，例如：webview 的启动时间比较慢。提前缓存，可以大幅度提升性能。\n\n### 小结\n\n大篇幅主要是介绍了前端指标的定义和摸索过程，这是一个平台的基石数据了。\n\n后续的平台能力拓展如下：\n\n- 在排查问题上：他们做了根因定位，分析了许多前端的具体指标，基本达到出现异常 5 分钟内可通知到业务，10 分钟内给出解决方案建议。\n- 在大数据量上：动态采样，批量上报，数据上报保障，异常场景兜底（例如：异常上报不上来，会在恢复后上报异常退出了页面，会记录堆栈信息）\n- 在性能拓展上，做了性能周报，主动给业务推，便于他们实时的跟踪自己的情况。也做了数据大屏做全局视角的分析。\n\n## 快手中间件 mesh 化实践\n\n分享是介绍中间件相关的 mesh，比较有意思的是定义了目前业内 mesh 的三代阶段，这倒是没怎么听过。\n\n具体的定义如下：\n\n![](https://files.mdnice.com/user/3610/be285750-bacf-42cd-bcdf-12507849b108.png)\n\n快手中间件采取的是：第三代中间件 mesh，轻 sdk 的方式。\n\n### 选型考虑\n\n在以往，重 sdk 下，整体开发维护成本，升级都比较繁琐，代理业务协议的流量。\n\n如果是以 Istio、Linkerd2 为代表，主要是处理常见的通讯协议（例如：HTTP、gRPC 协议），又对中间件 mesh 不大合适。\n\n最终采取的是 mesh 加轻 SDK 的方式：\n\n![](https://files.mdnice.com/user/3610/89749180-c082-4d7e-8072-55ceec5c385d.png)\n\n在此 mesh 的定位是能力下沉，sdk 的定位是标准化的定义。\n\n### 新的挑战\n\n做过微服务，基础设施的小伙伴都知道，要达到 A，将会在 BCD 付出更多，这是常态。\n\n要上 mesh 非常简单，K8s 里几条命令的事。但后面要处理的事，可就是要切切实实的成本了。\n\n主要面临了如下挑战：\n- 成本问题：复杂环境下的统一部署与运维。\n- 复杂度问题：规模大、性能要求高、策略复杂。\n- 落地推广：对业务来说不是强需求。\n\n### 解决方案\n\n针对以上三点，又配套了做了如下解决方案和措施：\n\n1. 统一运维：对自己，网关的运维平台。对业务，定位排查，可观察性的。\n\n2. 规模大问题：对 Enovy 等做了二开。只传输变更的数据、按需获取，解决单实例服务数过多的问题。\n\n3. 性能要求高：协议栈的优化、序列化优化等，做了大量底层的优化。\n\n4. 实施了面向失败设计，SDK fallback，可以 fallback 切换为直连模式。如果是新功能，没有老的，会切换到 Proxy 模式。\n\n### 业务推广\n\n业务推广这点要单独拧出来讲，因为 mesh 一般的直接收益不是业务，是基础设施，对业务不是强需求会遇到：\n- 业务对性能，稳定性敏感。\n- 业务很难配合人力配合架构升级。\n- 对业务侧的收益并不明显。\n\n据闻现在业内大规模落地的只有字节和蚂蚁，且都是有很多背景因素的，和组织上的人有直接关系。\n\n关于业务推广，演讲者也给出了一些建议，例如：稳定性很重要，搭便车，业务共建，选型有明显业务收益等。\n\n### 小结\n\n近几年各家都纷纷出来分享 mesh，其实基本上现状和应用情况都比较清晰了。\n\n像本次快手 mesh 的分享主要是面向中间件（gRPC、Kafka、RocketMQ、ZK、Mongo、Redis、MySQL）等。\n\n但能明显感觉到业务推广的苦恼，以及可预测的运维投入巨大，这也是所有出来分享 mesh 的核心痛点分享。\n\n其余的技术细节，大多通过二开等方案解决了。\n"
  },
  {
    "path": "content/posts/go/117-build.md",
    "content": "---\ntitle: \"时隔 3 年，Go1.17 增强构建约束！\"\ndate: 2021-12-31T12:54:56+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - go1.17\n---\n\n大家好，我是煎鱼。\n\nGo1.17rc1 在前几天终于正式发布了：\n\n![](https://files.mdnice.com/user/3610/a9dd1134-e4f8-4d9c-9c3c-2608728ddf69.png)\n\n看到 Go1.17 增加了一个新特性，是面向 Go 构建时的构建约束的增强。认真一看，是一个时隔 3 年的提案了，原本还在 Go2 和 Go1 之间左右摇摆，这下在 6 月底 Russ Cox 就输出了新草案：《Bug-resistant build constraints — Draft Design》。紧接着直接计划在 Go1.17 发布了。\n\n一气呵成，真实版高效能了。\n\n如下图：\n\n![](https://files.mdnice.com/user/3610/10037273-887d-45c0-9eca-6ef36d7c4d72.png)\n\n之前小咸鱼有遇到好几个朋友，在报错时压根不知道 Go 有这个约束语法，以为只是个单纯的注释，直接不明所以然，感觉科普之路任重道远。\n\n今天这篇文章煎鱼就来讲讲构建约束这事。\n\n注：下个月 Go1.17 就会正式发布，距离 Go1.18 泛型出山只差一点点距离了，值得期待！\n\n## 构建约束的背景\n\n简单来讲，在真实环境中，可能需要为不同的编译环境编写不同的 Go 代码，所以需要做构建约束。\n\n划重点，Go 语言对这一问题的解决方案是**在文件层面进行有条件的编译：每个文件要么在编译中，要么不在**。\n\n也就是，假设不符合构建约束的场景。那么会直接不编译这个文件，因为他不在编译范围内。那在程序想运行时就会报错，表示找不到文件。因此有许多的同学看着报错信息，经常找不着北...\n\n## 现有的构建约束\n\n既然是叫 “增强”。说明现有就有构建约束。最早的构建约束是在 2011 年 9 月引入的构建约束。\n\n我们平时常见的构建约束（build constraint），也叫做构建标记（build tag），构建约束必须出现在 `package` 之前。\n\n平时会在 Go 工程的文件中的最开始会看到如下行注解：\n\n```\n// +build\n```\n为了将构建约束与包文档区分开来，构建约束后必须跟一个空行。\n\n```\n// +build linux,386 darwin,!cgo\n```\n\n又或是：\n\n```\n// +build linux darwin\n// +build amd64\n```\n\n还可以根据 Go 版本来约束：\n\n```\n// +build go1.9\n```\n\n其主要支持如下几种：\n- 指定编译的操作系统，例如：windows、linux 等，对应 `runtime.GOOS` 的值。\n- 指定编译的计算机架构，例如：amd64、386，对应 `runtime.GOARCH` 的值。\n- 指定使用的编译器，例如：gccgo、gc。\n- 指定 Go 版本，例如：go1.9、go1.10 等。\n- 指定自定义的标签，例如：编译时通过指定 `-tags` 传入的值。\n- ...\n\n## 有什么问题\n\n既要用动他，本着 Go team 的 less is more 原则。想必是现有的构建约束，存在着什么问题，才需要调整他。\n\n### 对语法困惑\n\n从 issues 的反馈来看，是太复杂，如下：\n\n```\n// +build linux,386 darwin,!cgo\n```\n\n他表达的构建约束是：(linux AND 386) OR (darwin AND (NOT cgo)) 。\n\n感觉可以像三元运算符一样玩出花，可参见《Go 凭什么不支持三元运算符？》，这更夸张，没常见的逻辑符。\n\n也可以更复杂一些：\n\n```\n// +build 386 !gccgo,amd64 !gccgo,amd64p32 !gccgo\n```\n\n会导致会混淆用户的认知，如果能够这样写更好：\n\n```\n// +build 386 amd64 amd64p32\n// +build !gccgo\n```\n\n### 对布局困惑\n\n现在的 `// +build` 有硬性的使用规则：\n- 必须出现在文件顶部附近，前面只能有空行和其他行注释。这些规则意味着在 Go 文件中，构建约束必须出现在 package 子句之前。\n- 为了将构建约束与包文档区分开来，一系列构建约束后必须跟一个空行。\n\n像是以下失败案例：\n\n```golang\npackage main\n\n// +build linux\n```\n\n又或是：\n\n```golang\n/*\nCopyright ...\n*/\n\n// +build linux\n\npackage main\n```\n\n整体来看，官方在 2020 年 3 月对 `// +build` 注解的使用情况进行了分析，得出以下几种常见情况：\n- 忽略了`/* */`注释后的构建约束，通常是版权声明。\n- 忽略了文档注释中的构建约束。\n- 忽略了包声明后的构建约束。\n\n这些都是实际项目中出现的，也就是这个构建约束的布局约束并不好，造成了很多意外和反馈。\n\n像是版权声明的统一写入，基本都是脚本统一打上去的。会造成大量的隐藏版本 BUG。\n\n## 增强后的构建约束\n\n增强，就是优化，主要的目标之一是解决语法、布局困惑。\n\n设计的核心思想：用新的 `//go:build` 取代目前用于构建标签选择的 `//+build`，并且使用更广为熟悉的布尔表达式。\n\n设计的关键：平滑过渡，避免破坏 Go 代码。\n\n以前老的注解：\n\n```\n// +build linux\n// +build 386\n```\n\n新的注解：\n\n```\n//go:build linux && 386\n```\n\n新的语法主体为 Go spec 的 EBNF 标记：\n\n```\nBuildLine      = \"//go:build\" Expr\nExpr           = OrExpr\nOrExpr         = AndExpr   { \"||\" AndExpr }\nAndExpr        = UnaryExpr { \"&&\" UnaryExpr }\nUnaryExpr      = \"!\" UnaryExpr | \"(\" Expr \")\" | tag\ntag            = tag_letter { tag_letter }\ntag_letter     = unicode_letter | unicode_digit | \"_\" | \".\"\n```\n\n也就是说，构建标记的语法与其当前形式保持不变，但构建标记的组合现在使用 Go 的 ||、&& 和 ! 运算符和括号完成。\n\n另外一个文件只能有一行构建语句，也就是一个文件有多行 `//go:build` 是错误的，如此设计的目的是为了消除关于多行是隐式 AND 还是 OR 在一起的混淆。\n\n## 过渡阶段\n\n在过渡阶段，也就是 Go1.17 起。官方的 gofmt 工具会自动根据旧语法转换新版的语法，以保证兼容性。\n\n例如：\n\n```\n// +build !windows,!plan9\n```\n\n会转变为：\n\n```\n//go:build !windows && !plan9\n// +build !windows,!plan9\n```\n\n后面是计划把 `//+build` 给完全下线的。\n\n![](https://files.mdnice.com/user/3610/6fcaf62d-bd78-475b-85f1-614d5bc51437.png)\n\n常规 Go 工程基本用不到，因此就不进一步展开描述了。\n\n对过渡阶段感兴趣的可以看看 《Bug-resistant build constraints — Draft Design》的 Transition 部分，比较长，正常来讲是不需要我们关注的。\n\n## 总结\n\nGo 1.17 构建约束的增强，一下子让整个语法明确了起来。统一为 `//go:build`，至少不会有人看到 `//+build` 又以为是普通注释了。\n\n**你是否有在工作中遇到构建的版本、环境约束等场景呢，欢迎大家在评论区留言交流**！"
  },
  {
    "path": "content/posts/go/117-errorstack.md",
    "content": "---\ntitle: \"Go1.17 新特性，优化抛出的错误堆栈\"\ndate: 2021-12-31T12:55:05+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n平时在日常工程中，我们常常会用到异常恐慌（panic）的记录和追踪。最常见的就是，线上 panic 了之后，我们总想从中找到一些蛛丝马迹。\n\n我们很多人是看 panic 是看他的调用堆栈。然后就开始猜，看代码。猜测是不是哪里写的有问题，就想知道 panic 是由什么参数引起的？\n\n因为知道了诱发的参数，排查问题就非常方便了。为此**在 Go1.17，官方对这块的调用堆栈信息展示进行了优化**，使其可读性更友好。\n\n## 案例\n\n结合我们平时所使用的 panic 案例。如下：\n\n```golang\nfunc main() {\n\texample(make([]string, 1, 2), \"煎鱼\", 3)\n}\n\n//go:noinline\nfunc example(slice []string, str string, i int) error {\n\tpanic(\"脑子进煎鱼了\")\n}\n```\n\n运行结果：\n\n```\n$ go run main.go\npanic: 脑子进煎鱼了\n\ngoroutine 1 [running]:\nmain.example(0xc000032758, 0x1, 0x2, 0x1073d11, 0x6, 0x3, 0xc000102058, 0x1013201)\n\t/Users/eddycjy/go-application/awesomeProject/main.go:9 +0x39\nmain.main()\n\t/Users/eddycjy/go-application/awesomeProject/main.go:4 +0x68\nexit status 2\n```\n\n我们函数的入参是：`[]string、string、int`，核心关注到 `main.example` 方法的调用堆栈信息：\n\n```golang\nmain.example(\n    0xc000032758, \n    0x1, \n    0x2, \n    0x1073d11, \n    0x6, \n    0x3, \n    0xc000102058, \n    0x1013201\n)\n```\n明明只是函数三个参数，却输出了一堆，对应起来非常的不清晰。\n\n其实际对应是：\n\n- slice：0xc000032758、0x1、0x2。\n- string：0x1073d11、0x6。\n- int：0x3。\n\n这里存在的问题是，看调用堆栈的人，还得必须了解基本数据结构（例如：slice、string、int 等），他才知道每个函数入参他对应拥有几个字段，才能知道其内存布局的结构，有一点麻烦。\n\n并且从程序运行的角度来讲，这么水平平铺的方式，并不直观和准确。因为不同类型他是多个字段组合成结构才能代表一个类型。这不得还要人为估测？\n\n## 优化\n\n终于，这一块的调用堆栈查看在 Go1.17 做了正式的改善。如下：\n\n```golang\n$ go1.17 run main.go \npanic: 脑子进煎鱼了\n\ngoroutine 1 [running]:\nmain.example({0x0, 0xc0000001a0, 0xc000034770}, {0x1004319, 0x60}, 0x0)\n\t/Users/eddycjy/go-application/awesomeProject/main.go:9 +0x27\nmain.main()\n\t/Users/eddycjy/go-application/awesomeProject/main.go:4 +0x47\nexit status 2\n```\n\n新版本的调用堆栈的信息改变：\n\n```golang\nmain.example(\n    {0x0, 0xc0000001a0, 0xc000034770}, \n    {0x1004319, 0x60}, \n    0x0\n)\n```\n\n在 Go 语言以前的版本中，调用堆栈中的函数参数被打印成基于内存布局的十六进制值的形式，比较难以读取。\n \n在 **Go1.17 后，每个函数的参数都会被单独打印，并且以 “，” 隔开**，复合数据类型（例如：结构体、数组、切片等）的参数会用大括号包裹起来，整体更易读。\n\n其实际对应如下：\n\n- slice：0x0, 0xc0000001a0, 0xc000034770。\n- string：0x1004319, 0x60。\n- int：0x0。\n\n这里也有一块细节要注意，你会发现 Go1.17 的函数参数的数量和以往的版本相比，少了。是因为函数的返回值存在于寄存器中，而不会存储到内存中。\n\n因此函数返回值可能会是不准确的，所以也在新版本中也就不再打印了。\n\n## 总结\n\n在 Go1.17 的新版本中，调用堆栈的函数参数的可读性得到了进一步的优化和调整，在后续的使用上可能能够带来一定的排错效率的提高。\n\n你平时在借助调用堆栈排查问题呢，希望还获得什么辅助呢？\n\n## 参考\n- [GoTip: New Stack Trace Output Wrong](https://github.com/golang/go/issues/46708)\n- [cmd/compile: bad traceback arguments](https://github.com/golang/go/issues/45728)\n- [Go 1.17新特性详解：使用基于寄存器的调用惯例](https://mp.weixin.qq.com/s/AkJoXLlpSmw5vMZDpXoq5w)\n- [doc/go1.17: reword \"results\" in stack trace printing](https://groups.google.com/g/golang-codereviews/c/JkhaLqHFReM?pli=1)"
  },
  {
    "path": "content/posts/go/117-generics.md",
    "content": "---\ntitle: \"Go 1.17 支持泛型了？具体怎么用\"\ndate: 2021-12-31T12:55:02+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n千呼万唤的，Go1.17 前几天终于发布了：\n\n![](https://files.mdnice.com/user/3610/45fe4f8a-ef8e-41a0-abba-ecdc05fe61c5.png)\n\n先前我写了几篇 Go1.17 新特性的文章，有兴趣的小伙伴可以看看：\n\n- [一个新细节，Go 1.17 将允许切片转换为数组指针！](https://mp.weixin.qq.com/s/v1czjzlUsaSQTpAOG9Ub3w)\n- [我要提高 Go 程序健壮性，Fuzzing 来了！](https://mp.weixin.qq.com/s/zdsrmlwVR0bP1Q_Xg_VlpQ)\n- [提了 3 年，Go1.17 终于增强构建约束！](https://mp.weixin.qq.com/s/5kLFIuI0UJl_o8vMmZNfoA)\n\n今天的主题是泛型，众所皆知 Go1.18 泛型就会正式释出，都很期待，毕竟大更新，所有配套都会陆续有来！\n其实，**在 Go1.17 的此刻其实可以使用泛型了**，泛型代码已合入 master 分支。\n\n咱们只需要一点点操作，就能提前过上 Go 泛型的实验生活了。\n\n## 升级 Go1.17 \n\n你需要先升级 Go1.17，如下图：\n\n![](https://files.mdnice.com/user/3610/a3260cf0-f9bc-4c86-aafd-4d08480143b9.png)\n\n安装后查看版本信息是否正常输出：\n\n```\ngo1.17 version\ngo version go1.17 darwin/amd64\n```\n\n## 使用泛型\n\n接着写入一个基本的泛型 Demo：\n\n```golang\nimport (\n\t\"fmt\"\n)\n\nfunc Print[T any](s []T) {\n\tfor _, v := range s {\n\t\tfmt.Print(v)\n\t}\n}\n\nfunc main() {\n\tPrint([]string{\"你好, \", \"脑子进了煎鱼\\n\"})\n\tPrint([]int64{1, 2, 3})\n}\n```\n\n只需要在 run 和 build 的命令执行时指定 `-G` 标识就好了。不过有的小伙伴可能会疑惑，为什么要这么干？\n\n其实这类提前放入主版本的操作，在 Go 语言中并不少见。像是现在所见的 `GO111MODULE`，早期的 `GO15VENDOREXPERIMENT` 都有些这么个味道。都是逐步入场，分阶段使用，等确定成熟、完善后再渐渐去掉。\n\n本次泛型也采取了这种方法，按照提案，目前使用的是 `-G` 标识做为泛型的开关。\n\n运行的命令如下：\n\n```\ngo1.17 run -gcflags=-G=3 xxx\n``` \n\n就可以运行带有泛型的代码。\n\n查看输出结果：\n\n```\n$ go1.17 run -gcflags=-G=3 generics.go\n# command-line-arguments\n./generics.go:7:6: internal compiler error: Cannot export a generic function (yet): Print\n\nPlease file a bug report including a short program that triggers the error.\nhttps://golang.org/issue/new\n```\n\n竟然报错了，煎鱼你翻车了是吧...\n\n根据错误提示可得知，是还没实现导出一个通用函数的功能。那样我们只需要把 `Print` 方法改为 `print`，再执行就可以了。\n\n再次执行后的输出结果：\n\n```\n你好, 脑子进了煎鱼\n123\n```\n成功输出了不同类型的值。\n\n## 更多的案例\n\n在 GitHub 有个小伙伴 mattn 整理了完整的泛型使用案例后开源了，可以实际下载使用看看：\n\n![github.com/mattn/go-generics-example](https://files.mdnice.com/user/3610/a3e768b4-ca68-448d-b4b8-98a3fce447da.png)\n\n大家根据上面的介绍来实际使用就可以达到运行泛型的效果了，GitHub 地址是：github.com/mattn/go-generics-example。\n\n## 总结\n\n经过多年的折腾，Go 语言在发布的 1.17 版本中已经包含了泛型的功能。将会在 Go1.18 正式宣发泛型，我们将会是Go 历史新阶段的见证者。\n\n为什么？因为随着 Go1.18 的逼近，我们将会将会见到越来越多的新工具支持和变更，甚至会改变不少 Go 工程的写法。\n\n欢迎大家在评论区分享你的看法！"
  },
  {
    "path": "content/posts/go/117-module-pruning-lazy.md",
    "content": "---\ntitle: \"Go1.17 新特性：对 Go 依赖管理的一把大剪刀\"\ndate: 2021-12-31T12:55:03+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n不得不说。我可是个经历过 Go 依赖管理群魔乱舞，Go modules 迁移一堆 BUG 的人儿，难顶...\n为此当年我写了不少技术文章，希望给大家避坑。\n\n如下：\n- [Go Modules 终极入门](https://mp.weixin.qq.com/s/6gJkSyGAFR0v6kow2uVklA)\n- [干货满满的 Go Modules 知识分享](https://mp.weixin.qq.com/s/uUNTH06_s6yzy5urtjPMsg)\n- [Go1.16 新特性：Go mod 的后悔药，仅需这一招](https://mp.weixin.qq.com/s/0g89yj9sc1oIz9kS9ZIAEA)\n\n在近期 Go1.17 发布后，Go modules 带来了两大更新，煎鱼摩拳擦掌，他们分别是：\n- 模块依赖图裁剪（module graph pruning）\n- 延时模块加载（lazy module loading）\n\n今天带大家一起来了解这两块内容，争取了解其为何物，背景又是什么。\n\n## 背景\n\n在日常的 Go 工程开发中，不知道你有没有遇到过 Go modules 的一个奇怪的点。大家没说，就以为是正确的，默认就接受了。\n\n引用官方的 [mod_lazy_new_import.txt](https://github.com/golang/go/blob/4012fea822763ef3aa66dd949fa95b9f8d89450a/src/cmd/go/testdata/script/mod_lazy_new_import.txt \"mod_lazy_new_import.txt\") 的案例来说，就是假设我们在代码中：\n- main module 是 lazy。其导入了 module A 的 package x，package x 又导入了 module B。\n- main module lazy 也相当于同时导入了 module A 的 package y。\n- module A 的 package y 又导入了 module C 的 package z。\n\n关联如下图所示：\n\n![module 关联图示](https://image.eddycjy.com/d535e2d41648a346c08c41ac38eff6b9.jpg)\n\n这个 Go 程序如果运行起来，会发生什么情况呢？**在 Go1.17 以前，如果你不存在 module C 的 package z**，程序在编译构建的时候就会报错，提示找不到。\n\n实际上 module C 的 package z 并没有**对你主程序有任何建设意义**，俗话来讲就是 “占着茅坑不拉屎”。\n\n他只是因为 main module 在导入 module A 时，也被 “间接” 导入了 package y 的依赖，也就是我们常看到的 go.mod 文件中的 “indirect” 标识，他们会导致构建失败，让人直呼无奈。\n\n## Go1.17 module 改进\n\n显然，社区反馈希望避免看到 “不相关” 的传递依赖等，也因此有了 Go1.17 的 module 改造。\n\n![](https://files.mdnice.com/user/3610/e69f5155-3334-462f-b021-bee477c62c49.png)\n\n接下来的 module 例子我们将会结合提案 [《Proposal: Lazy Module Loading》](https://go.googlesource.com/proposal/+/master/design/36460-lazy-module-loading.md \"Proposal: Lazy Module Loading\") 、[《cmd/go: module graph pruning and lazy module loading》](https://github.com/golang/go/issues/36460 \"cmd/go: module graph pruning and lazy module loading\") 以及 《[Module graph pruning](https://golang.google.cn/ref/mod#graph-pruning \"Module graph pruning\")》的内容、案例来进行说明和介绍。\n\n### module graph pruning\n\n第一个改进就是模块依赖图裁剪（module graph pruning），这是这个版本 module 优化的基础。\n\n在 Go1.17 以前，只要该项目的 go.mod 文件分析出来你存在间接的依赖，如果你没有安装过该依赖，就会出现报错。\n\n错误提示如下：\n\n```golang\n$ go build\ngo: example.com/a@v0.1.0 requires\n example.com/c@v0.1.0: missing go.sum entry; to add it:\n go mod download example.com/c\n```\n\n这个时候我们都会默默地去安装一遍...没想过这是间接依赖，和我们的程序没一点直接的代码关系。\n\n在 Go1.17 及之后就变了，go.mod 文件如下，会存在 2 块 require 代码块：\n\n```golang\nmodule example.com/lazy\n\ngo 1.17\n\nrequire example.com/a v0.1.0\n\nrequire example.com/b v0.1.0 // indirect\n...\n```\n\n这就是区别，第一块的 require 我们眼熟，那分拆出来的第二块 require 的是什么呢？\n\n这就是那些模块的间接依赖（常见到的 indirect 标识依赖）。可以理解为像是其他语言的 xxx.lock 文件一样的存在。\n\n此处分析出来的间接依赖，将会不会像以前一样阻碍编译构建，只会真正有使用到的才会进行识别。\n\n### lazy module loading\n\n第二个改进是延时模块加载（lazy module loading），是基于模块依赖图裁剪（module graph pruning）的场景上的进一步优化。\n\n也就是以往那些没被使用到的，但又间接依赖的模块。在 Go1.17 及以后不会被 Go 命令读取和加载，只有真正需要的时候才会加载。\n\n### 副作用\n\nGo module 依赖图裁剪也带来了一个副作用，那就是 go.mod 文件 size 会变大。\n\n在 Go 1.17 版本之后，每次 go mod tidy（当go.mod中的go版本为1.17时），Go 命令都会对 main module 的依赖做一次深度扫描（deepening scan）。\n\n该操作将 main module 的所有直接和间接依赖都记录在 go.mod 中，考虑到内容较多，Go 1.17 将直接依赖和间接依赖分别放在两个不同的 require 代码块中。\n\n也就是上文所见到的内容。\n\n## 总结\n\n自从 Go 语言推出 Go modules 依赖，module 一直不断地在优化和改进。虽然看上去已经越来越好用了，但依然似乎存在不少问题。\n\n就拿本次变更来讲，我也是在好朋友的 Go 微信群中看到提问，才思考了起来。因为大家看到第二块 require 时，虽然知道是间接依赖的包，但更明确，为什么要单独出来？\n\n大家其实是不大理解的，本次变更也可能存在语义不清，不够明确的情况。但无论如何，后续我们可以继续观察。"
  },
  {
    "path": "content/posts/go/117-performance.md",
    "content": "---\ntitle: \"Go1.17 新特性，凭什么提速 5~10%？\"\ndate: 2021-12-31T12:55:04+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n在 Go1.17 发布后，我们惊喜的发现 Go 语言他又又又优化了，编译器改进后产生了约 5% 的性能提升，也没有什么破坏性修改，保证了向前兼容。\n\n![](https://files.mdnice.com/user/3610/c39ffb33-4afd-4537-a172-4919be7975a4.png)\n\n他做了些什么呢，好像没怎么看到有人提起。为此今天煎鱼带大家来解读两新提案：\n- 《[Proposal: Register-based Go calling convention](https://go.googlesource.com/proposal/+/master/design/40724-register-calling.md \"Proposal: Register-based Go calling convention\")》\n- 《[Proposal: Create an undefined internal calling convention](https://go.googlesource.com/proposal/+/master/design/27539-internal-abi.md \"Proposal: Create an undefined internal calling convention\")》\n\n本文会基于提案讲解和拆解，毕竟分享新知识肯定要从官方资料作为事实基准出发。\n\n## 背景\n\n在以往的 Go 版本中，Go 的调用约定简单且几乎跨平台通用，其原因在于选用了基于 Plan9 ABI 的堆栈调用约定，也就是**函数的参数和返回值都是通过堆栈上来进行传递**。\n\n这里我们一共提到了 Plan9 和 ABI，这是两个很关键的理念：\n- Plan9：Go 语言所使用的汇编器，Rob Pike 是贝尔实验室的猛人。\n- ABI：Application Binary Interface（应用程序二进制接口），ABI 包含了应用程序在操作系统下运行时必须遵守的编程约定（例如：二进制接口）。\n\n该方案的优缺点如下：\n- 优点：实现简单，简化了实现成本。\n- 缺点：性能方面付出了不少的代价。\n\n按我理解，在 Go 语言初创时期，采取先简单实现，跑起来再说。也合理，性能倒不是一个 TOP1 需求。\n\n## Go1.17 优化\n\n### 什么是调用惯例\n\n在新版本的优化中，提到了调用惯例（calling convention）的概念，指的是**调用方和被调用方对函数调用的共识约定**。\n\n这些共识包含：函数的参数、返回值、参数传递顺序、传递方式等。\n\n双方都必须遵循这个约定时，程序的函数才能正常的运行起来。如果不遵循，那么该函数是没法运行起来的。\n\n### 优化是什么\n\n在 Go1.17 起，正式将把 Go 内部 ABI 规范（在 Go 函数之间使用）从基于堆栈的函数参数和结果传递的方式**改为基于寄存器的函数参数和结果传递**。\n\n本次修改涉及到的项非常多，该优化是持续的，原本预计是 Go1.16 实现，不过拖到了 Go1.17。\n\n![](https://files.mdnice.com/user/3610/41e7a047-cac2-4b8a-9006-762fd73ee2a4.png)\n\n目前实现了 amd64 和 arm64 架构的支持。还有不少的更多的支持会持续在 Go1.18 中完成，具体进度可见 [issues #40724](https://github.com/golang/go/issues/40724 \"issues #40724\")。\n\n### 性能如何\n\n在 [Go1.17 Release Notes](https://golang.org/doc/go1.17 \"Go1.17 Release Notes\") 中明确指出，用一组有代表性的 Go 包和程序的基准测试。\n\n官方数据显示：\n- Go 程序的运行性能提高了约 5%。\n- Go 所编译出的二进制大小的减少约 2%。\n\n在民间数据来看，在 [twitter](https://twitter.com/__Achille__/status/1431014148800802819 \"twitter\") 看到 @Achille 表示从 Go1.15.7 升级到 Go1.17 后显示。在一个大规模的数据处理系统上进行的 Go1.17 升级产生了惊人的效果，我们来看看他的真实数据。\n\nCPU、Malloc 调用时间减少了约15%：\n\n![图来自 @Achille](https://files.mdnice.com/user/3610/0c98d5c9-4691-4c39-92af-fe25fd41de25.png)\n\n\n![图来自 @Achille](https://files.mdnice.com/user/3610/2ea79a65-297f-49aa-a97f-15d02dbf5c9b.png)\n\n\nRSS 大小更接近于堆的大小：\n\n![图来自 @Achille](https://files.mdnice.com/user/3610/14b1886f-7bd7-4ea2-9967-5272f5b09c79.png)\n\n从原本的 1.6GB 降至 1GB。\n\n结合官方和民间数据来看，优化效果是明确且有效的。有兴趣的小伙伴也可以自己测一测。\n\n## 总结\n\n在 Go1.17 这一个新版本中，只需要简单的升一升 Go 版本，我们就能得到一定的性能优化，这是非常不错的。\n\n从以往的基于堆栈的函数参数和结果传递的方式改为 Go1.17~Go1.18 基于寄存器的函数参数和结果传递，Go 语言正在一步步走的更好！\n\n你觉得呢？"
  },
  {
    "path": "content/posts/go/118-build-info.md",
    "content": "---\ntitle: \"Go1.18 新特性：编译后的二进制文件，将包含更多信息\"\ndate: 2022-02-05T16:02:45+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n我有一个朋友，，开开心心入职，想着施展拳脚，第一个任务就是对老旧的二进制文件进行研究。\n\n他一看，这文件，不知道是编译器用什么参数怎么打出来的，环境不知道是什么，更不知道来自什么代码分支？\n\n这除了是项目流程上的问题外，Go 在这块也有类似的小问题，处理起来比较麻烦。\n\n## 背景\n\n日常中很难从 Go 二进制文件中检索元信息，要么是信息完全缺失，要么提取需要对二进制文件进行大量解析。\n\n包含的元信息如下：\n\n|  元信息   | 提取处  |\n|  ----  | ----  |\n| Go 构建版本  | 符号表，通过全局变量 `runtime.buildVersion` 来获取 |\n| 构建信息，例如：模块和版本 | 符号表，通过全局变量 `runtime/debug.modinfo` 来获取 |\n| 编译器选项，例如：构建模式、编译器、gcflags、ldflags 等 | 无法获取 |\n| 用户定义的自定义数据，例如：应用程序版本等 | 需在编译时设置全局字符串变量，才可以获取 |\n\n关注到编译器选项，也就是参数等都是无法得知的，也就是会提高获取如何编译出来的难度。\n\n## 新提案\n\nMichael Obermüller 提出了一个新的提案《[cmd/go: add compiler flags, relevant env vars to 'go version -m' output](https://github.com/golang/go/issues/35667)》用于解决上述问题。\n\n在提案中想要的是 JSON 格式的结构输出：\n\n```json\n{\n    \"version\": \"go1.13.4\",\n    \"compileropts\": {\n        \"compiler\": \"gc\",\n        \"mode\": \"pie\",\n        \"os\": \"linux\",\n        ...\n    },\n    \"buildinfo\": {\n        \"path\": \"脑子进煎鱼了\",\n        \"main\": {\n            \"path\": \"HelloWorld\",\n            \"version\": \"(devel)\",\n        },\n        \"deps\": []\n    },\n    \"user\": {\n        \"customkey\": \"customval\",\n        ...\n    }\n}\n```\n\nRuss Cox 表示由于编译信息已有既有格式，并且默认使用 JSON 只会让二进制文件变得更大。好处少，没必要，改为了选项化的支持。\n\n新的 Go1.18 版本中，可以通过既有的：\n\n```\ngo version -m\n``` \n\n查看到提案所提到的信息。\n\n例如：\n\n```go\n$ gotip version\ngo version devel go1.18-eba0e866fa Mon Oct 18 22:56:07 2021 +0000 darwin/amd64\n$ gotip build ./\n$ gotip version -m ko\n...\n\tbuild\tcompiler\tgc\n\tbuild\ttags\tgoexperiment.regabiwrappers,goexperiment.regabireflect,goexperiment.regabiargs\n\tbuild\tCGO_ENABLED\ttrue\n\tbuild\tCGO_CPPFLAGS\t\n\tbuild\tCGO_CFLAGS\t\n\tbuild\tCGO_CXXFLAGS\t\n\tbuild\tCGO_LDFLAGS\t\n\tbuild\tgitrevision\t6447264ff8b5d48aff64000f81bb0847aefc7bac\n\tbuild\tgituncommitted\ttrue\n```\n\n若需要输出 JSON 格式，也可以通过指定 `go version -json` 达到一样的效果。\n\n在上面的输出中，现有的编译器选项等都会包含在内，能够让大家对整体编译后的二进制文件溯源有一个更好的认知。\n\n## 总结\n\n在今天这篇文章中，给大家介绍了 Go1.18 的一个新的变化。\n\n新版本中，编译器选项/参数、相关环境变量等，将会包含在编译后的二进制文件中，能够更便于后人排查和查看信息。"
  },
  {
    "path": "content/posts/go/118-build.md",
    "content": "---\ntitle: \"泛型是双刃剑？Go1.18 编译会慢近 20%\"\ndate: 2021-12-31T12:55:18+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n目前 Go 的泛型已经在稳定推进的过程，在 Go1.18 将会释出正式的第一版。不过前两天我看到 @danscales 提出的《cmd/compile: Go 1.18 compile time may be about 18% slower than Go.17 (largely from changes due to generics)》。\n\n作者表示在 Go1.18 有了泛型后，编译速度将会变慢，虽然不意外，说明副作用还是有的，升级需谨慎。\n\n以下为修整后概括的原文信息。\n\n## 性能分析\n\n这个测试主要是测试 Go 泛型对 Go 编译器带来的影响，并没有输入大量的测试用例，是最简单的比较，仅代表大部分的差异。\n\n比较的内容是 Go 泛型的 -G=0 和 -G=3 模式下的编译时间。\n\n分别代表以下含义：\n- -G=0 模式：默认不打开泛型的模式。\n- -G=3 模式：打开泛型的模式。\n\nGo 1.18 中的 -G=0 模式和 Go 1.17 模式的比较显示，由于非泛型的变化，编译器的速度可能降低了~1%（因为 -G=0 模式不支持泛型）。\n\nGo 1.18 的编译时间可能比 Go 1.17 慢 15-18%，这主要是由于实现泛型所带来的变化，也就是 Go1.18 开启泛型下，编译时间会变慢。\n\n## 差异在哪\n\n大部分的差异是由于新的编译器前端处理，因为 SSA 后端对于泛型完全没有变化。\n\n- 在 -G=0 模式下（用于 Go 1.18 之前的所有编译器）：有一个语法分析器，创建 ir.Node 节点树的 noder 阶段，以及标准类型检查器。\n- 在 -G=3 模式下：有相同的语法分析器，但程序首先由 types2（支持泛型）进行类型检查。\n\n在通过 -G=3 模式打开泛型后，会有一个 noder2 阶段，使用语法信息和 types2 类型检查器的类型信息创建 ir.Node 节点树。在一次运行中，noder+ types1-typechecking 的开销总和约为 4%，而 types2-typechecker+noder2 的总和为 14%。\n\n可以看到大部分的速度下降是由于改变了编译前端处理（并不意外）。\n\n## 总结\n\n可以明确的是，在打开泛型后，Go1.18 编译时间可能会慢 15-18%，Go 官方将计划在 Go 1.19 中减少这种额外的开销。\n\n泛型的双刃剑初见，后续不管是编译时间、执行时间（预计不会减缓）、泛型的滥用、最佳实践等，都值得我们去讨论和关注。\n\n欢迎大家在评论区讨论和交流：）"
  },
  {
    "path": "content/posts/go/118-constraints.md",
    "content": "---\ntitle: \"Go 新语法挺丑？最新的泛型类型约束介绍\"\ndate: 2021-12-31T12:55:19+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n近期我们在分享《[3 件与 Go 开发者有关的大小事](https://mp.weixin.qq.com/s/22TeQOUjf_glPX3QLPX8yw)》时，里面有一部分有提到 Go 泛型的约束类型语法又又又变了。\n\n在评论区里看到不少的读者朋友大呼泛型的新类型约束语法挺丑，不如原本的好...\n\n如下：\n\n![](https://files.mdnice.com/user/3610/8e734a8f-612c-41c3-a0cd-8a5fe6eea483.png)\n\n为此，今天煎鱼就带大家来看看，为什么会发生泛型的新语法会这种改变？\n\n## 问题背景\n\n原本 @Ian Lance Taylor 设计的的泛型类型关键字如下：\n\n```golang\ntype T interface {\n type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, complex64, complex128, string\n}\n```\n看起来好像非常 “顺眼”。但在《[proposal: Go 2: sum types using interface type lists](https://github.com/golang/go/issues/41716 \"proposal: Go 2: sum types using interface type lists\")》中社区进行了热烈的讨论。\n\n认为该类型约束的关键字，过于 “模棱两可”。像是 @Damien Neil 所提出的以下两个例子。\n\n结构体的例子：\n\n```golang\npackage p\ntype mustIncludeDefaultCase struct{}\ntype MySum interface {\n  type int, float64, mustIncludeDefaultCase\n}\n```\n\n不明确的点之一，如果类型列表包含一个未导出的类型，那又应该是如何处理呢？\n\n接口的例子：\n\n```golang\ntype T interface { type int16, int32 }\nfunc main() {\n  var x T\n  switch x.(type) {\n  case int16:\n  case int32:\n  } \n}\n```\n\n你认为程序会跑进哪个 switch-case 的代码块里呢，是 int16，还是 int32？\n\n不，都不会，变量 x 是 nil，如此迷惑。\n\n在社区讨论中，发现设计与真实场景一结合，发现这个类型规则在普通的接口类型、在约束中使用也太微妙了。\n\n用类型列表嵌入接口时的行为也很奇怪。认为可以做的更好，那就是 “更显式”。\n\n## 新提案\n\n为此，Go 泛型的设计者 @Ian Lance Taylor 提出了一个新的提案《[spec: generics: use type sets to remove type keyword in constraints](https://github.com/golang/go/issues/45346 \"spec: generics: use type sets to remove type keyword in constraints\")》。\n\n其包含三个新的、更简单的想法来取代泛型提案中定义的类型列表。\n\n### 关键名词\n\n新语法在泛型处增加一个新概念：接口元素（interface elements），用作约束条件的接口类型，或者被嵌入约束条件的接口类型，允许嵌入一些额外的构造。\n\n被嵌入的可以是：\n- 任何类型，而不仅仅是一个接口类型。\n- 一个新的句法结构，称为近似元素。\n- 一个新的句法结构，称为联合元素。\n\n重点名词，我们继续展开讲解，分别是：\n- 嵌入约束。\n- 近似元素。\n- 联合元素。\n- 接口类型。\n\n### 联合元素\n\n原先的语法中，类型约束会用逗号分隔的方式来展示。\n\n如下：\n```golang\ntype int, int8, int16, int32, int64\n``` \n\n在新语法中，结合定义为 union element（联合元素），写成一系列由竖线 ”|“ 分隔的类型或近似元素。\n\n如下：\n\n```golang\ntype PredeclaredSignedInteger interface {\n\tint | int8 | int16 | int32 | int64\n}\n```\n\n常常会和下面讲到的近似元素一起使用。\n\n### 近似元素\n\n新语法，他的标识符是 “~”，完整用法是 `~T`。`~T` 是指底层类型为 T 的所有类型的集合。例如：\n\n```golang\ntype AnyInt interface{ ~int }\n```\n\n他的类型集是 `~int`，也就是所有类型为 int 的类型（如：int、int8、int16、int32、int64）都能够满足这个类型约束的条件。\n\n再结合以上的分隔来使用，用法为：\n\n```golang\ntype SignedInteger interface {\n\t~int | ~int8 | ~int16 | ~int32 | ~int64\n}\n```\n\n相当于泛型提案中使用的以下类型：\n\n```golang\ninterface {\n\ttype int, int8, int16, int32, int64\n}\n```\n\n新语法只需借助近似标识符 `~int` 来表达就可以了，更明确的表示了近似匹配，而不是存在隐式理解。\n\n### 嵌入约束\n\n一个类型约束可以嵌入另一个约束，联合元素可以包括约束。\n\n例如：\n\n```golang\n// Signed is a constraint whose type set is any signed integer type.\ntype Signed interface {\n\t~int | ~int8 | ~int16 | ~int32 | ~int64\n}\n\n// Unsigned is a constraint whose type set is any unsigned integer type.\ntype Unsigned interface {\n\t~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr\n}\n\n// Float is a constraint whose type set is any floating point type.\ntype Float interface {\n\t~float32 | ~float64\n}\n\n// Ordered is a constraint whose type set is any ordered type.\n// That is, any type that supports the < operator.\ntype Ordered interface {\n\tSigned | Unsigned | Float | ~string\n}\n```\n\n这个很好理解，就是正式支持嵌入约束了。\n\n### 接口类型（联合约束元素）\n\n在联合元素中，使用接口类型的话。将会把该类型集添加到联合中。\n\n例如：\n\n```golang\ntype Stringish interface {\n\tstring | fmt.Stringer\n}\n```\n\nStringish 的类型集将是字符串类型和所有实现 `fmt.Stringer` 的类型，任何这些类型（包括 `fmt.Stringer` 本身）将被允许作为这个约束的类型参数。\n \n也就是针对接口类型做了特殊的处理。\n\n## 总结\n\n今天这篇文章，主要讲解了 Go 泛型的新语法，其实本质上还是为了解决引入 A 后，出现了 BCD 新问题，又继续引入新的语法和模式来解决。\n\n整体就是三个观点：\n\n- 显式匹配：使用**明确的 \"~\" 近似元素，澄清了何时在底层类型上进行匹配**。\n- 明确含义：**使用 “|” 而不是 “,” 强调这是一个元素的联合**。\n- 嵌套优化：通过允许约束嵌入非界面元素，类型关键字可以被省略。\n\n这就是用这两个新语法符号的原因，被嫌丑的新语法标识符 “|” 和 “~” ，其实也是在 issues 的大范围讨论中，由社区贡献出来的。\n\n算是有利有弊？**你的看法如何，欢迎在评论区留言**：）\n"
  },
  {
    "path": "content/posts/go/118-cut.md",
    "content": "---\ntitle: \"Go1.18 新特性：新增好用的 Cut 方法\"\ndate: 2022-02-05T16:03:31+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n在各种写业务代码的时候，大家会常常要处理字符串的内容。常见的像是用邮箱登陆账号，如果是：eddycjy@gmail.com，那就得根据 @ 来切割，分别取出前和后，来识别用户名和邮箱地址。\n\n这种需求，在 Go 里写起来方便吗？今天就由煎鱼带大家了解。\n\n## 背景\n\n### 重复代码\n\n无独有偶，Ainar Garipov 在许多项目中遇到了前面我们所提的切割需求。\n\n例如：\n\n```go\nidx = strings.Index(username, \"@\")\nif idx != -1 {\n  name = username[:idx]\n} else {\n  name = username\n}  \n```\n\n又或是：\n\n```go\nidx = strings.LastIndex(address, \"@\")\nif idx != -1 {\n  host = address[idx+1:]\n} else {\n  host = address\n}\n```\n\n经常要反复写一些繁琐的代码，提案提出者表示不愉快。\n\n## 新提案\n\n### 实施内容\n\n建议新增 Cut 方法到 strings 标准库：\n\n```go\nfunc Cut(s, sep string) (before, after string, found bool) {\n\tif i := Index(s, sep); i >= 0 {\n\t\treturn s[:i], s[i+len(sep):], true\n\t}\n\treturn s, \"\", false\n}\n```\n\n同步也要在 bytes 标准库：  \n\n```go\nfunc Cut(s, sep []byte) (before, after []byte, found bool)\n```\n\n这样一来，就可以从原本的：\n\n```go\n\teq := strings.IndexByte(rec, '=')\n\tif eq == -1 {\n\t\treturn \"\", \"\", s, ErrHeader\n\t}\n\tk, v = rec[:eq], rec[eq+1:]\n```\n\n变成：\n\n```go\n\tk, v, ok = strings.Cut(rec, \"=\")\n\tif !ok {\n\t\treturn \"\", \"\", s, ErrHeader\n\t}\n```\n\n写法上会更优雅，在复杂的场景下会更具可读性和抽象级别。\n\n### 接受原因\n\n可能就有小伙伴会吐槽了，Go 居然只为了节省 1 行代码，就搞了个新函数，这还是大道至简吗？\n\n实际上，在官方团队（Russ Cox）介入后，他对 Go 主仓库进行了分析，搜索了相关类似函数的使用：\n- strings.Index。\n- strings.IndexByte。\n- strings.IndexRune。\n\n统计后，转换为了可以使用 `strings.Cut` 的用法，在例子和测试数据之外有 311 个索引调用。\n\n排除了一些确实不需要的，剩下 285 个调用。在这些调用中，有 221 次是最好写成 Cut 方法的，能更优雅。\n\n![](https://files.mdnice.com/user/3610/a1a61fd3-1ca0-448a-b503-551433635992.png)\n\n也就是说，有现有的 Go 代码中，有 77% 可以用新增的 Cut 函数写得更清楚，可读性和抽象可以做得更好。\n\nGo 主仓库确实存在如此重复的代码，他认为这也是非常不可思议的！\n\n## 总结\n\nGo1.18 的新特性中，Cut 虽然只是新增了一个方法，看上去无伤大雅。\n\n但类似 Cut 方法的用法，在 Go 的主版本中其实已经被发明了两次。\n\n该新方法的出现，可以同时取代并简化四个不同的标准库函数：Index、IndexByte、IndexRune 和 SplitN 中的绝大部分用法。\n \n由于这些原因，最终将 Cut 添加到标准库中。\n\n你觉得怎么样？：）\n\n## 参考\n- [bytes, strings: add Cut](https://github.com/golang/go/issues/46336)"
  },
  {
    "path": "content/posts/go/118-leader-generics.md",
    "content": "---\ntitle: \"回归现实：Go Leader 对 1.18 泛型的期望\"\ndate: 2021-12-31T12:55:16+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n前段时间根据 Go 泛型的最新动态，我写了一篇《[出泛型后 API 怎么办？Go 开发者要注意了](https://mp.weixin.qq.com/s/yWEM2EAwv80ZUFjbKGtpNA)》文章，引发了不少小伙伴的热议。\n\nGo 核心开发团队的现任 Leader \n@Russ Cox 在 golang-dev 中正式发表《[expectations for generics in Go 1.18](https://groups.google.com/g/golang-dev/c/iuB22_G9Kbo)》对 Go 泛型给出了 “期待”，可以认为是后续的计划了。\n\n\n![](https://files.mdnice.com/user/3610/e4693268-eff4-4dd3-ab6b-e4c41d8728cc.png)\n\n\n如果不出现严重的问题，Go 1.18 将会包括对泛型的支持，并且这次泛型的支持将会是有史以来最大的一次语言变化，对以下几点有顾虑：\n- 最佳实践.\n- 生产经验\n- 兼容性承诺。\n\n接下来，煎鱼带大家一起了解 Russ Cox 发表的 Go 泛型进程，知悉官方一手消息。\n\n## 最佳实践\n\nGo 团队表示不知道使用泛型的最佳实践是什么，所以给出的官方文档将无法就何时使用泛型和何时不使用泛型给出精确、明确的答案，只可以给出粗略的指导。\n\n此处可以参考《Effective Go》的最初版本，是在不间断地写了一整年的 Go 代码后，才正式输出的。\n\n![](https://files.mdnice.com/user/3610/034ab5db-f125-450d-bc97-9ea42ba94022.png)\n\n按照现有的计划，官方只会提供关于如何使用泛型的文档，暂时无法提供任何关于风格、最佳实践的规定性文档。\n\n在提供的标准库上，先是已经通过提案的 maps 和 slices库会先放到 golang.org/x/exp 中作为实验，不会保证向后兼容。待成熟后，再推广到标准库中。\n\n![](https://files.mdnice.com/user/3610/b1d967a2-2ac7-4077-be2e-4d1c4d3565cb.png)\n\n可以明确，Go 泛型出来后，社区就会陆续开始百花齐放，接着有官方输出推荐方法了，历史是如此的相似。\n\n## 生产经验\n\n目前 Go 团队没有关于泛型的生产经验，因此会在文档中给出明确提示，让大家在生产中使用泛型的时候应该适当谨慎。\n\n泛型出来后，会陆续涉及到大量的重写工作，但是由于现在处于中间阶段。正在重写的 Go 1.18 工具链去同时适配泛型、非泛型代码是需要时间和经验的，有一定的风险。\n\n因此泛型出来后，可能会出现一些意想不到的问题，仅在生产发现（教训）。\n\n## 兼容性承诺\n\nGo1.18 会和其他 Go1.x 版本一样，保证向后兼容的承诺：不会破坏用 Go 1.18 构建的代码，包括使用泛型的代码。\n\n如果是最坏的情况，如果发现 Go 1.18 的语义有一些致命的问题，并需要改变它们（例如：在Go 1.19 中）。\n\n将会使用 go.mod 文件的 go 行来确定该模块中的源文件是期待 Go 1.18 还是 Go 1.19+ 语义，以此实现版本控制。但目前来看，不需要这样做。\n\n也建议急于使用 Go 泛型的开源库作者，做好泛型和非泛型版本代表的支持和隔离，这样对用户会更加的友好。\n\n## 总结\n\n可以明确的是，Go 泛型的整体推进方案，在这篇文章中均已说明。Go 官方团队也与许多第三方工具的作者进行沟通。\n\n第三方工具可能不会在 Go 1.18 发布时就完全支持泛型，这会由各作者自行根据自己的时间表来更新。\n\n煎鱼猜测推进节奏就是：\n1. 支持泛型语法。\n2. 观察。\n3. 推进标准/工具库。\n4. 逐步替换。\n5. 修 BUG。\n6. 观察、优化\n7. 生产可用。\n\n大概需要 2~3 个 Go 版本，需要 1~2 年，Go 泛型的各类配套组件就会基本完善，可用，转为持续优化了。\n\n**你对 Go 官方的推进计划此怎么看呢**，欢迎在评论区留言和交流！"
  },
  {
    "path": "content/posts/go/118-module.md",
    "content": "---\ntitle: \"Go1.18 新特性：多 Module 工作区模式\"\ndate: 2022-02-05T16:00:00+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - go1.18\n---\n\n大家好，我是煎鱼。\n\nGo 的依赖管理，也就是 Go Module。从推出到现在，也已经有了一定的年头了，吐槽一直很多，官方也不断地在进行完善。\n\nGo1.18 将会推出一个新特性：Multi-Module Workspaces，用于支持 Module 多工作区，能解决以往的一系列问题。\n\n今天将由煎鱼带大家一起深入学习。\n\n## 背景\n\n在日常使用 Go 工程时，总会遇到 2 个经典问题，特别的折腾人。\n\n如下：\n1. 依赖本地 replace module。\n2. 依赖本地未发布的 module。\n\n### replace module\n\n第一个场景：像是平时在 Go 工程中，我们为了解决一些本地依赖，或是定制化代码。会在 go.mod 文件中使用 replace 做替换。\n\n如下代码：\n\n```go\nreplace golang.org/x/net => /Users/eddycjy/go/awesomeProject\n```\n\n这样就可以实现本地开发联调时的准确性。\n\n问题就在这里：\n- 本地路径：所设定的 replace 本质上转换的是本地的路径，也就是每个人都不一样。\n- 仓库依赖：文件修改是会上传到 Git 仓库的，不小心传上去了，影响到其他开发同学，又或是每次上传都得重新改回去。\n\n用户体验非常差，很折腾人。\n\n### 未发布的 module\n\n第二个场景：在做本地的 Go 项目开发时，可能会在本地同时开发多个库（项目库、工具库、第三方库）等。\n\n如下代码：\n\n```go\npackage main\n\nimport (\n    \"github.com/eddycjy/pkgutil\"\n)\n\nfunc main() {\n    pkgutil.PrintFish()\n}\n```\n\n如果这个时候运行 `go run` 或是 `go mod tidy`，都不行，会运行失败。\n\n报如下类似错误：\n\n```\nfatal: repository 'https://github.com/eddycjy/pkgutil/' not found\n```\n\n这个问题报错是因为 `github.com/eddycjy/pkgutil` 这个库，在 GitHub 是没有的，自然也就拉取不到。\n\n解决方法：在 Go1.18 以前，我们会通过 replace（会遇到背景一的问题），又或是直接上传到 Github 上，自然也就能被 Go 工具链拉取到依赖了。\n\n许多同学会发出灵魂质疑：Go 的依赖都必须要上传到 GitHub 吗，强绑定？\n\n对新入门的同学非常不友好，很要命。\n\n## 工作区模式\n\n在社区的多轮反馈下，Michael Matloob 提出了提案《[Proposal: Multi-Module Workspaces in cmd/go](https://go.googlesource.com/proposal/+/master/design/45713-workspace.md \"Proposal: Multi-Module Workspaces in cmd/go\")》进行了大量的讨论和实施，在 Go1.18 正式落地。\n\n新提案的一个核心概念，就是增加了 `go work` 工作区的概念，针对的是 Go Module 的依赖管理模式。\n\n其能够在本地项目的 go.work 文件中，通过设置一系列依赖的模块本地路径，再将**路径下的模块组成一个当前的工作区**，他的读取优先级是最高的。\n\n我们可以通过 `go help` 来查看，如下：\n\n```\n$ go1.18beta1 help work\nUsage:\n\n\tgo work <command> [arguments]\n\nThe commands are:\n\n\tedit        edit go.work from tools or scripts\n\tinit        initialize workspace file\n\tsync        sync workspace build list to modules\n\tuse         add modules to workspace file\n\nUse \"go help work <command>\" for more information about a command.\n```\n\n只要执行 `go work init` 就可以初始化一个新的工作区，后面跟的参数就是要生成的具体子模块 mod。\n\n命令如下：\n\n```go\ngo work init ./mod ./tools\n```\n\n项目目录如下：\n\n```\nawesomeProject\n├── mod\n│   ├── go.mod      // 子模块\n│   └── main.go\n├── go.work         // 工作区\n└── tools\n    ├── fish.go\n    └── go.mod      // 子模块\n```\n\n生成的 go.work 文件内容：\n\n```\ngo 1.18\n\nuse (\n    ./mod \n    ./tools\n)\n```\n\n新的 go.work 与 go.mod 语法一致，也可以使用 replace 语法：\n\n```\ngo 1.18\n\nuse (...)\n\nreplace golang.org/x/net => example.com/fork/net v1.4.5\n```\n\ngo.work 文件内共支持三个指令：\n- go：声明 go 版本号，主要用于后续新语义的版本控制。\n- use：声明应用所依赖模块的具体文件路径，路径可以是绝对路径或相对路径，可以在应用命目录外均可。\n- replace：声明替换某个模块依赖的导入路径，优先级高级 go.mod 中的 replace 指令。\n\n若想要禁用工作区模式，可以通过 `-workfile=off` 指令来指定。\n\n也就是在运行时执行如下命令：\n\n```\ngo run -workfile=off main.go\n\ngo build -workfile=off\n```\n\ngo.work 文件是不需要提交到 Git 仓库上的，否则就比较折腾了。\n\n只要你在 Go 项目中设置了 go.work 文件，那么在运行和编译时就会进入到工作区模式，会优先以工作区的配置为最高优先级，来适配本地开发的诉求。\n\n至此，工作区的核心知识就介绍完毕了。\n\n## 总结\n\n今天给大家介绍了 Go1.18 的新特性：多 Module 工作区模式。其本质上还是为了解决本地开发的诉求。\n\n由于 go.mod 文件是与项目强关联的，基本都会上传到 Git 仓库中，很难在这上面动刀子。直接就造了个 go.work 出来，纯本地使用，方便快捷。\n\n使用新的 go.work，就可以在完全是本地文件上各种捣鼓了，不会对其他成员开发有其他影响。\n\n你觉得怎么样呢？：）\n"
  },
  {
    "path": "content/posts/go/4errors.md",
    "content": "---\ntitle: \"大家对 Go 错误处理的 4 个误解！\"\ndate: 2021-12-31T12:55:04+08:00\ndraft: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\nGo 语言中错误处理的机制一直是各大 Gopher 热议的问题。甚至一直有人寄望 Go 支持 `throw` 和 `catch` 关键字，实现与其他语言类似的特性。\n\n今天煎鱼带大家了解几个 Go 语言的错误处理中，大家最关心的问题：\n1. 为什么不支持 try-catch？\n2. 为什么不支持全局捕获的机制？\n3. 为什么要这么设计错误处理？\n4. 未来的错误处理机制会怎么样？\n\n## 落寞的 try-catch\n\n在 Go1 时，大家知道基本不可能支持。于是打起了 Go2 的主意。为什么 Go 就不能支持 try-catch 组合拳？\n\n上一年宣发了 Go2 的构想，所以在 2020 年就有小伙伴乘机提出过类似 《[proposal: Go 2: use keywords throw, catch, and guard to handle errors](https://github.com/golang/go/issues/40583 \"proposal: Go 2: use keywords throw, catch, and guard to handle errors\")》的提案。\n\n下面来自该提案的演示，Go1 的错误处理： \n\n```golang\ntype data struct {}\n\nfunc (d data) bar() (string, error) {\n    return \"\", errors.New(\"err\")\n}\n\nfunc foo() (data, error) {\n    return data{}, errors.New(\"err\")\n}\n\nfunc do () (string, error) {\n    d, err := foo()\n    if err != nil {\n        return \"\", err\n    }\n\n    s, err := d.bar()\n    if err != nil {\n        return \"\", err\n    }\n\n    return s, nil\n}\n```\n\n新提案所改造的方式：\n\n```golang\ntype data struct {}\n\nfunc (d data) bar() string {\n    throw \"\", errors.New(\"err\")\n}\n\nfunc foo() (d data) {\n    throw errors.New(\"err\")\n    return\n}\n\nfunc do () (string, error) {\n    catch err {\n        return \"\", err \n    }\n\n    s := foo().bar()\n    return s, nil\n}\n```\n\n不过答复非常明确，@davecheney 在底下回复“以最强烈的措辞，不（In the strongest possible terms, no）”。这可让人懵了圈，为什么这么硬呢？\n\n其实 Go 官方早在《[Error Handling — Problem Overview](https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md \"Error Handling — Problem Overview\")》提案早已明确提过，Go 官方在**设计上会有意识地选择使用显式错误结果和显式错误检查**。\n\n结合《[language: Go 2: error handling meta issue](https://github.com/golang/go/issues/40432 \"language: Go 2: error handling meta issue\")》可得知，要拒绝 try-catch 关键字的主要原因是：\n- 会**涉及到额外的流程控制**，因为使用 try 的复杂表达式，会导致函数意外返回。\n- 在表达式层面上没有流程控制结构，只有 panic 关键字，它不只是从一个函数返回。\n\n说白了，就是设计理念不合，加之实现上也不大合理。在以往的多轮讨论中早已被 Go 团队拒绝了。\n\n反之 Go 团队倒是一遍遍在回答这个问题，已经不大耐烦了，直接都整理了 issues 版的 FAQ 了。\n\n## 想捕获所有 panic\n\n在 Go 语言中，有一个点，很多新同学会不一样碰到的。那就是在 goroutine 中如果 panic 了，没有加 recover 关键字（有时候也会忘记），就会导致程序崩溃。\n\n又或是以为加了 recover 就能保障一个 goroutine 下所派生出来的 goroutine 所产生的 panic，一劳永逸。\n\n但现实总是会让人迷惑，我经常会看到有同学提出类似的疑惑：\n\n![来自 Go 读者交流群](https://files.mdnice.com/user/3610/8e3127ef-7cbe-4952-8780-f8812f921f45.png)\n\n这时候，有其他语言经验的同学中，又有想到了一个利器。能不能设置一个全局的错误处理 handler。\n\n像是 PHP 语言也可以有类似的方法：\n\n```\nset_error_handler();\nset_exception_handler();\nregister_shutdown_function();\n```\n\n显然，Go 语言中并没有类似的东西。归类一下，我们聚焦以下两个问题：\n1. 为什么 recover 不能捕获更上层的 panic？\n2. 为什么 Go 没有全局的错误处理方法？\n\n### 源码层面\n\n如果是讲设计的话，其实只是通过 Go 的 GMP 模型和 defer+panic+recver 的源码剖析就能知道了。\n\n![](https://files.mdnice.com/user/3610/42998ba0-84cc-4fe5-b88d-b9400dd1698b.png)\n\n本质上 defer+panic 都是挂载在 G 上的，可查看我以前写的《[深入理解 Go panic and recover](https://eddycjy.com/posts/go/panic/2019-05-21-panic-and-recover/ \"深入理解 Go panic and recover\")》，你会有更多深入的理解。\n\n### 设计思想\n\n在本文中我们不能仅限于源码，需要更深挖，Go 设计者他的思想是什么，为什么就是不支持？\n\n在 Go issues 中《[proposal: spec: allow fatal panic handler](https://github.com/golang/go/issues/32333 \"proposal: spec: allow fatal panic handler\")》、《[No way to catch errors from goroutines automatically](https://github.com/golang/go/issues/20161 \"No way to catch errors from goroutines automatically\") 》分别的针对性探讨过上述问题。\n\nGo 团队的大当家 @Russ Cox 给出了明确的答复：**Go 语言的设计立场是错误恢复应该在本地完成，或者完全在一个单独的进程中完成**。\n\n![](https://files.mdnice.com/user/3610/cb6fac34-ba12-40fd-98d3-f177e00ee39f.png)\n\n这就是为什么 Go 语言不能跨 goroutines 从 panic 中恢复，也不能从 throw 中恢复的根本原因，是**语言设计层面的思想所决定**。\n\n在源码剖析时，你所看到的整套 GMP+defer+panic+recover 的机制机制，就是跟随着这个设计思想去编写和发展的。\n\n设计思想决定源码实现。\n\n### 建议方式\n\n从 Go 语言层面去动摇这个设计思想，目前来看可能性很低。至少 2021 年的现在没有看到改观。\n\n整体上会建议提供公共的 Go 方法去规避这种情况。参考 issues 所提供的范例如下：\n\n```golang\nrecovery.SafeGo(logger, func() {\n              method(all parameters)\n\t})\n\nfunc SafeGo(logger logging.ILogger, f func()) {\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif panicMessage := recover(); panicMessage != nil {\n\t\t\t\t...\n\t\t\t}\n\t\t}()\n\n\t\tf()\n\t}()\n}\n```\n\n是不是感觉似曾相识？\n\n每家公司的内部库都应该有这么一个工具方法，规避偶尔忘记的 goroutine recover 所引发的奇奇怪怪问题。\n\n也可以参照建议，利用一个单独的进程（Go 语言中是 goroutine）去统一处理这些 panic，不过这比较麻烦，较少见。\n\n## 未来会如何\n\nGo 社区对 Go 语言未来的错误处理机制非常关心，因为 Go1 已经米已成炊，希望在 Go2 上解决错误处理机制的问题。\n\n期望 Go2 核心要处理的包含如下几点（#40432）：\n\n1. 对于 Go2，我们希望使错误检查更加轻便，减少专门用于错误检查的 Go 程序代码的数量。我们还想让写错误处理更方便，减少程序员花时间写错误处理的可能性。\n2. 错误检查和错误处理都必须保持明确，即在程序文本中可见。我们不希望重复异常处理的陷阱。\n3. 现有的代码必须继续工作，并保持与现在一样的有效性。任何改变都必须与现有的代码相互配合。\n\n为此，许多人提过不少新的提案...很可惜，截止 2021.08 月底为止，有许多人试图改变语言层面以达到这些目标，但没有一个新的提案被接受。\n\n现在也有许多变更并入 Go2 提案，主要是 error-handling 方面的优化。\n\n大家有兴趣可以看看我之前写的：《[先睹为快，Go2 Error 的挣扎之路](https://mp.weixin.qq.com/s/XILveKzh07BOQnqxYDKQsA)》，相信能给你带来不少新知识。\n\n## 总结\n\n看到这里，我们不由得想到。为什么，为什么在 21 世纪前者已经有了这么多优秀的语言，Go 语言的错误处理机制依然这么的难抉择？\n\n显然 Go 语言的开发团队是有自己的设计哲学和思想的，否则 “less is more” 也不会如此广泛流传。\n\n这存在着一系列既要也要的问题。欢迎大家关注煎鱼，后续我们也可以面向 Go 后续的错误处理持续的关注和讨论！"
  },
  {
    "path": "content/posts/go/again-mutex.md",
    "content": "---\ntitle: \"Go 为什么不支持可重入锁？\"\ndate: 2021-12-31T12:55:24+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n程序里的锁，是很多小伙伴在写分布式应用时用的最多的一个利器之一。\n\n使用 Go 的同学里，绝大部分都有其他语言的经验，就会对其中一点有疑惑，那就是 **Go 里的锁，竟然不支持可重入**？\n\n为此，今天煎鱼带大家一起来了解这里的设计考量，看看为什么。\n\n## 可重入锁\n\n如果对已经上锁的普通互斥锁进行 “加锁” 操作，其结果要么失败，要么会阻塞至解锁。\n\n锁的场景如下：\n- 在加锁上：如果是可重入互斥锁，当前尝试加锁的线程如果就是持有该锁的线程时，加锁操作就会成功。\n- 在解锁上：可重入互斥锁一般都会记录被加锁的次数，只有执行相同次数的解锁操作才会真正解锁。\n\n简单来讲，可重入互斥锁是互斥锁的一种，同一线程对其多次加锁不会产生死锁，又或是导致阻塞。\n\n不同语言间实现可能或多或少有些区别，但大体意思差不多。\n\n请你想一下，Go 是怎么样的呢？\n\n## Go 支持情况\n\n我们看到以下这个 Go 互斥锁例子：\n\n```golang\nvar mu sync.Mutex\n\nfunc main() {\n\tmu.Lock()\n\tmu.Lock()\n}\n```\n\n这段 Go 程序会阻塞吗？不会，会报以下错误：\n\n```\nfatal error: all goroutines are asleep - deadlock!\n```\n\nGo 显然是不支持可重入互斥锁的。\n\n## 官方回复\n\n### Go 设计原则\n\n在工程中使用互斥的根本原因是：为了保护不变量，也可以用于保护内、外部的不变量。\n\n基于此，Go 在互斥锁设计上会遵守这几个原则。如下：\n- 在调用 `mutex.Lock` 方法时，要保证这些变量的不变性保持，不会在后续的过程中被破坏。\n- 在调用 `mu.Unlock` 方法时，要保证：\n    - 程序不再需要依赖那些不变量。\n    - 如果程序在互斥锁加锁期间破坏了它们，则需要确保已经恢复了它们。\n\n### 不支持的原因\n\n讲了 Go 自己的设计原则后，那为什么不支持可重入呢？\n\n其实 Russ Cox 于 2010 年在《[Experimenting with GO](https://groups.google.com/g/golang-nuts/c/XqW1qcuZgKg/m/Ui3nQkeLV80J \"Experimenting with GO\")》就给出了答复，认为递归（又称：重入）互斥是个坏主意，这个设计并不好。\n\n我们可以结合官方的例子来理解。\n\n如下：\n\n```golang\nfunc F() {\n        mu.Lock()\n        ... do some stuff ...\n        G()\n        ... do some more stuff ...\n        mu.Unlock()\n}\n\nfunc G() {\n        mu.Lock()\n        ... do some stuff ...\n        mu.Unlock()\n}\n```\n\n在上述代码中，我们在 `F` 方法中调用 `mu.Lock` 方法加上了锁。如果支持可重入锁，接着就会进入到 `G` 方法中。\n\n此时就会有一个致命的问题，你**不知道 `F` 和 `G` 方法加锁后是不是做了什么事情**，从而导致破坏了不变量，毕竟随手起几个协程做点坏事，也是完全可能的。\n\n这对于 Go 是无法接受的，可重入的设计**违反了前面所提到的设计理念**，也就是：“要保证这些变量的不变性保持，不会在后续的过程中被破坏”。\n\n基于上述原因，Go 官方团队选择了没有支持该项特性。\n\n## 总结\n\nGo 互斥锁没有支持可重入锁的设计，也是喜欢的大道至简的思路了，可能的干扰比较多，不如直接简单的来。\n\n你在工作过程中有没有类似的疑惑呢，欢迎大家在评论区留言和交流：）"
  },
  {
    "path": "content/posts/go/annotation.md",
    "content": "---\ntitle: \"Go：我有注解，Java：不，你没有！\"\ndate: 2021-12-31T12:55:11+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - 为什么\n---\n\n大家好，我是煎鱼。\n\n作为一位 Go 程序员，你会发现身边的同事大多都拥有其他语言的编写经验。那势必就会遇到一点，要把新学到的知识和以前的知识建立连接。\n\n![图来自网络](https://files.mdnice.com/user/3610/050a9802-1ca2-4634-859d-325a09d418c5.png)\n\n特殊在于，Go 有些特性是其他语言有，他没有的。最经典的就是 N 位 Java 同学寻找 Go 语言的注解在哪里，总要解释。\n\n为此，今天煎鱼就带大家了解一下 Go 语言的注解的使用和情况。\n\n## 什么是注解\n\n### 了解历史\n\n注解（Annotation）最早出现自何处，翻了一圈并没有找到。但可以明确，在注解的使用中，Java 注解最为经典，为了便于理解，因此我们基于 Java 做初步的注解理解。\n\n![](https://files.mdnice.com/user/3610/3c9e2434-c5f4-4d7a-bd2d-a8b582b570c8.png)\n\n在 2002 年，JSR-175 提出了 《[A Metadata Facility for the Java Programming Language](https://jcp.org/en/jsr/detail?id=175)》，也就是为 Java 编程语言提供元数据工具。\n\n这就是现在使用最广泛地注解（Annotation）的来源。\n示例如下：\n\n```golang\n// @annotation1\n// @annotation2\nfunc Hello() string {\n        return \"\"\n}\n```\n\n在格式上均以 “@” 作为注解标识来使用。\n\n### 注解例子\n\n摘抄自 @wikipedia 的一个注解例子：\n\n```java\n  //等同于 @Edible(value = true)\n  @Edible(true)\n  Item item = new Carrot();\n\n  public @interface Edible {\n    boolean value() default false;\n  }\n\n  @Author(first = \"Oompah\", last = \"Loompah\")\n  Book book = new Book();\n\n  public @interface Author {\n    String first();\n    String last();\n  }\n  \n  // 该标注可以在运行时通过反射访问。\n  @Retention(RetentionPolicy.RUNTIME) \n  // 该标注只用于类内方法。\n  @Target({ElementType.METHOD})\n  public @interface Tweezable {\n  }\n\n```\n\n在上述例子中，通过注解去做了一系列的定义、声明、赋值等。若是对语言既有注解不熟，或是做的比较复杂的注解，就会有一定的理解成本。\n\n在业内也常常会说，**注解就是 “在源码上进行编码”**，注解的存在，有着明确的优缺点。你觉得呢？\n\n## 注解的作用\n\n在注解的的作用上，分为如下几点：\n1. 为编译器提供信息：注释可以被编译器用来检测错误或支持警告。\n2. 编译时和部署时处理：软件工具可以处理注释信息以生成代码、XML文件等。\n3. 运行时处理：有些注解可以在运行时检查，并用于其他用途。\n\n## Go 注解在哪里\n\n### 现状 \n\nGo 语言本身并没有原生支持强大的注解，仅限于以下两种：\n- 编译时生成：go:generate\n- 编译时约束：go:build\n\n但这先按不足以作为一个函数注解来使用，也无法形成像 Python 那样的装饰器行为。\n\n### 为什么不支持\n\nGo issues 上有人提过类似的提案：\n\n![](https://files.mdnice.com/user/3610/9695f163-65e8-456a-8dce-bb8551739016.png)\n\nGo Contributor @ianlancetaylor 给出了明确的答复，Go在设计上更倾向于明确的、显式的编程风格。\n\n思考的优缺点如下：\n- 优势：不知道 Go 能从添加装饰器中得到什么好处，没能在 issues 上明确论证。\n- 缺点：是明确的，会存在意外设置的情况。\n\n因如下原因，没有接受注解：\n- 对比现有代码方法，这种装饰器的新的方法没有提供比现有方法更多的优势，大到足矣推翻原有的设计思路。\n- 社区内的投票，支持的也很少（基于表情符号的投票），用户反馈不多。\n\n可能有小伙伴会说了，有注解做装饰器了，代码会简洁不少。\n\n对此 Go 团队的态度很明确：\n\n![](https://files.mdnice.com/user/3610/bb357e12-9b15-4729-9381-977d164b6b04.png)\n\nGo 认为**可读性更重要**，如果只是额外多写一点代码，在权衡后，还是可以接受的。\n\n## 用 Go 实现注解\n\n虽然 Go 语言官方没有原生的完整支持，但开源社区中也有小伙伴已经放出了大招，借助各项周边工具和库来实现特定的函数注解功能。\n\n\nGitHub 项目分别如下：\n- [MarcGrol/golangAnnotations](github.com/MarcGrol/golangAnnotations)\n- [u2takey/go-annotation](https://github.com/u2takey/go-annotation)\n\n使用示例如下：\n\n```golang\npackage tourdefrance\n\n//go:generate golangAnnotations -input-dir .\n\n// @RestService( path = \"/api/tour\" )\ntype TourService struct{}\n\ntype EtappeResult struct{ ... }\n\n// @RestOperation( method = \"PUT\", path = \"/{year}/etappe/{etappeUid}\" )\nfunc (ts *TourService) addEtappeResults(c context.Context, year int, etappeUid string, results EtappeResult) error {\n\treturn nil\n}\n```\n\n对 Go 注解的使用感兴趣的小伙伴可以自行查阅使用手册。\n\n我们更多的关心，Go 原生都没支持，那么开源库都是如何实现的呢？在此我们借助 [MarcGrol/golangAnnotations](github.com/MarcGrol/golangAnnotations) 项目所提供的思路来讲解。\n\n分为三个步骤：\n1. 解析代码。\n2. 模板处理。\n3. 生成代码。\n\n### 解析 AST\n\n首先，我们需要用用 go/ast 标准库获取代码所生成的 AST Tree 中需要的内容和结构。\n\n示例代码如下：\n\n```shell\nparsedSources := ParsedSources{\n    PackageName: \"tourdefrance\",\n    Structs:     []model.Struct{\n        {\n      \t     DocLines:   []string{\"// @RestService( path = \"/api/tour\" )\"},\n      \t     Name:       \"TourService\",\n      \t     Operations: []model.Operation{\n                {\n              \t    DocLines:   []string{\"// @RestOperation( method = \"PUT\", path = \"/{year}/etappe/{etappeUid}\"},\n              \t    ...\n                },\n            },\n        },\n    },\n}\n```\n我们可以看到，在 AST Tree 中能够获取到在示例代码中所定义的注解内容，我们就可以依据此去做很多奇奇怪怪的事情了。\n\n### 模板生成\n\n紧接着，在知道了注解的输入是什么后，我们只需要根据实际情况，编写对应的模板生成器 code-generator 就可以了。\n\n我们会基于 text/template 标准库来实现，比较经典的像是 [kubernetes/code-generator](https://github.com/kubernetes/code-generator) 是一个可以参考的实现。\n\n代码实现完毕后，将其编译成 go plugin，便于我们在下一步调用就可以了。\n\n### 代码生成\n\n最后，万事俱备只欠东风。差的就是告诉工具，哪些 Go 文件中包含注解，需要我们去生成的。\n\n这时候我们可以使用 `//go:generate` 在 Go 文件声明。就像前面的项目中所说的：\n\n```\n//go:generate golangAnnotations -input-dir .\n```\n\n声明该 Go 文件需要生成，并调用前面编写好的 golangAnnotations 二进制文件，就可以实现基本的 Go 注解生成了。\n\n## 总结\n\n今天在这篇文章中，我们介绍了注解（Annotation）的历史背景。同时我们针对 Go 语言目前原生的注解支持情况进行了说明。\n\n也面向为什么 Go 没有像 Java 那样支持强大的注解进行了基于 Go 官方团队的原因解释。如果希望在 Go 实现注解的，也提供了相应的开源技术方案。\n\n**你觉得 Go 语言是否需要强大的注解支持**呢，欢迎你在评论区留言和讨论！"
  },
  {
    "path": "content/posts/go/any.md",
    "content": "---\ntitle: \"Go 新关键字 any，interface 会成历史吗？\"\ndate: 2021-12-31T12:55:21+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n大家在看 Go1.18 泛型的代码时，不知道是否有留意到一个新的关键字 any。\n\n例子如下：\n\n```golang\nfunc Print[T any](s []T) {}\n```\n\n之前没有专门提过，但有没有小伙伴以为这个关键字，是泛型代码专属的？\n\n其实不是...在这次新的 Go1.18 更新中，any 是作为一个新的关键字出现，**any 有一个真身，本质上是 interface{} 的别名**：\n\n```golang\ntype any = interface{}\n```\n\n也就是，在常规代码中，也可以直接使用：\n\n```golang\nfunc f(a any) {\n\tswitch a.(type) {\n\tcase int:\n\t\tfmt.Println(\"进脑子煎鱼了\")\n\tcase float64:\n\t\tfmt.Println(\"煎鱼进脑子了\")\n\tcase string:\n\t\tfmt.Println(\"脑子进煎鱼了\")\n\t}\n}\n\nfunc main() {\n\tf(2)\n\tf(3.1415)\n\tf(\"煎鱼好！\")\n}\n```\n\n从使用层面来讲，新的关键字 any 会比 interface{} 方便不少，毕竟少打了好多个词，更快了，其实也是参照现有 rune 类型的做法。\n\n增加新关键字后的对比如下：\n\n| 长声明    |  短声明   |\n| --- | --- |\n|  func f[T interface{}](s []T) []T\t   |  func f[T any](s []T) []T\n  func f(a interface{})\t | func f(a any)\n |  var a interface{}\t   |  var a any   |\n\n我们在了解他的便利性后，再从代码一致性和可读性来讲，是有些问题的，会造成一定的疑惑。\n\n因此前两天有人提出了《[all: rewrite interface{} to any](https://github.com/golang/go/issues/49884)》的需求，打算把内部所有的代码都重写一遍。\n\n![](https://files.mdnice.com/user/3610/06687ddd-e224-4499-892b-3869ee1fc1d0.png)\n\n你可能会以为是人肉手工改？那肯定不是，Go 官方发起了 CL 进行批量修改。\n\n我们在日常的工程中，也可以和他们一样，直接借用 Go 工具链来实现替换。\n\n如下：\n\n```golang\ngofmt -w -r 'interface{} -> any' ./...\n```\n\n听到这个消息时，我的朋友咸鱼就大惊了，在想 interface{} 会不会成为历史，被新的关键字 any 完全替代？\n\n![](https://files.mdnice.com/user/3610/41a70c0c-284c-44c7-9b15-a1ee37545424.png)\n\n显然，答案是不会的。因为 **Go1 有兼容性的保证**，肯定不会在现阶段删除。不过后续会出现一个现象，就是我们的 Go 工程中，有人用 any，有人用 interface{}，会在代码可读性上比较伤人。\n\n不过我们也可以学 Go 官方，在 linter 流程中借助 gofmt 工具来强行把所有 interface{} 都替换成 any 来实现代码的一致性。 \n\n这次变更，感觉是个**美学**的问题，你对此怎么看呢？有没有也希望哪些东西有别名，**欢迎大家在评论区留言和交流**：）"
  },
  {
    "path": "content/posts/go/class-extends.md",
    "content": "---\ntitle: \"Go 为什么不支持类和继承？\"\ndate: 2021-12-31T12:55:22+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n大家在早期学习 Go 时，一旦跨过语法的阶段后。马上就会进入到一个新的纠结点，Go 不支持面向对象吗？\n\n![](https://files.mdnice.com/user/3610/a299d98d-e46c-4a6d-8362-02f957e86b10.png)\n\n\n这门编程语言里没有类（class）、继承（extends），~~没法一把搜了，面试问啥面向对象（OOP）~~？\n\n今天煎鱼就带大家一起来了解这之中的思考，Go 真的不支持吗？\n\n## 类和继承\n\n### 类是什么\n\n类（class）在面向对象编程中是一种面向对象计算机编程语言的构造，是创建对象的蓝图，描述了所创建的对象共同的特性和方法（via @维基百科）。\n\n例子如下：\n\n```php\nclass SimpleClass\n{\n    // 声明属性\n    public $var = '脑子进煎鱼了';\n\n    // 声明方法\n    public function displayVar() {\n        echo $this->var;\n    }\n}\n```\n\n每个类的定义都以关键字 class 开头，后面跟着类名，后面跟着一对花括号，里面包含有类的属性与方法的定义。\n\n### 继承是什么\n\n继承是面向对象软件技术当中的一个概念，如果一个类别 B “继承自”另一个类别 A，就把这个 B 称为 “A的子类”，而把 A 称为 “B的父类别” 也可以称 “A 是 B 的超类”（via @维基百科）。\n\n例子如下：\n\n```php\n// 父类\nclass Foo\n{\n    public function printItem($string)\n    {\n        echo '煎鱼1: ' . $string . PHP_EOL;\n    }\n    \n    public function printPHP()\n    {\n        echo 'PHP is great.' . PHP_EOL;\n    }\n}\n\n// 子类\nclass Bar extends Foo\n{\n    public function printItem($string)\n    {\n        echo '煎鱼2: ' . $string . PHP_EOL;\n    }\n}\n```\n\n继承有如下两个特性：\n- 子类具有父类别的各种属性和方法，不需要再次编写相同的代码。\n- 子类别继承父类时，可以重新定义某些属性，并重写某些方法，使其获得与父类别不同的功能。\n\n## 结构和组合\n\n在 Go 里就比较 ”特别“ 了，因为没有传统的类，也没有继承。\n\n取而代之的是结构和组合的方式。这也是业内对 Go 是否 OOP 争议最大的地方。\n\n### 结构体\n\n我们可以在 Go 中通过结构体的方式来组织代码，达到类似类的方式。\n\n例子如下：\n\n```golang\npackage main\n\nimport \"fmt\"\n\ntype person struct {\n    name string\n    age  int\n}\n\nfunc(p *person) hello(){}\n\nfunc newPerson(name string) *person {\n    p := person{name: name}\n    p.age = 42\n    return &p\n}\n\nfunc main() {\n    fmt.Println(person{\"煎鱼1\", 22})\n    fmt.Println(person{name: \"煎鱼2\", age: 33})\n    ...\n}\n```\n\n在上述代码中，我们可以定义结构体内的属性，也可以针对结构体这些类型定义只属于他们的方法。\n\n在声明实例上，可以配合 `newXXX` 的初始化方法来生成，这是 Go 里约定俗成的方式。\n\n### 组合\n\n类的声明采取结构体的方式取代后，也可以配套使用 ”组合“ 来达到类似继承的效果。\n\n例子如下：\n\n```golang\ntype man struct {\n\tname string\n}\n\nfunc (m *man) hello1() {}\n\ntype person struct {\n\tman\n\tname string\n}\n\nfunc (p *person) hello2() {}\n\nfunc newPerson(name string) *person {\n\tp := person{name: name}\n\treturn &p\n}\n\nfunc main() {\n\tp := newPerson(\"脑子进煎鱼了\")\n\tp.hello1()\n}\n```\n\n在上述代码中，我们分别定义了 man 和 person 两个结构体，并将 man 嵌入到 person 中，形成组合。\n\n你可以在 main 方法中能够看到，person 实例是可以使用和调用 man 实例的一些公开属性和方法的。\n\n在简单的使用效果上会与继承有些接近。\n\n## Go 是面向对象的语言吗\n\n“Go 语言是否一门面向对象的语言？”，这是一个日经话题。官方 FAQ 给出的答复是：\n\n![](https://files.mdnice.com/user/3610/159601a9-b428-4958-9f85-2214aea30127.png)\n\n是的，也不是。原因是：\n\n- Go 有类型和方法，并且允许面向对象的编程风格，但没有类型层次。\n- Go 中的 \"接口 \"概念提供了一种不同的方法，我们认为这种方法易于使用，而且在某些方面更加通用。还有一些方法可以将类型嵌入到其他类型中，以提供类似的东西，但不等同于子类。\n- Go 中的方法比 C++ 或 Java 中的方法更通用：它们可以为任何类型的数据定义，甚至是内置类型，如普通的、\"未装箱的 \"整数。它们并不局限于结构（类）。\n- Go 由于缺乏类型层次，Go 中的 \"对象 \"比 C++ 或 Java 等语言更轻巧。\n\n## 为什么不支持类和继承\n\n有的人认为类和继承是面向对象的必要特性，必须要有，才能是面向对象的语言，但其实也并非如此。\n\n面向对象（OOP）有不同的含义和解读，许多概念也可以通过结构体、组合和接口等方式进行表达，说是不支持传统的 OOP。\n\n其实真相是 Go 是选择了另外一条路，也就是 ”**组合优于继承**“。我们所提到的类和继承并不是定义 OOP 的一种准则，只是协助完成 OOP 的方法之一。\n\n不要本末倒置了，不让工具来定义 OOP 的理念。\n\n## 总结\n\n在今天这篇文章中，我们介绍了常说的类和继承的业内定义和使用案例。同时面向 Go 读者群里的疑惑，进行了解答。\n\n实质上，Go 是 OOP，也不是 OOP。类和继承只是实现 OOP 的一种方式，但并不是没有这两者，他就不是 OOP 了。\n\n不支持的原因也很明确，Go 在设计上，选择了组合优于继承的编程设计模式，它不是传统那种面向类型的范式。\n\n你觉得呢，欢迎大家在评论区留言和交流：）\n \n## 参考\n\n- 为什么人们声称 Go 不是面向对象的，我是不是误读了什么？：https://www.reddit.com/r/golang/comments/a9rn6n/why_people_claim_that_go_is_not_object_oriented/\n- 组合大于继承：https://en.wikipedia.org/wiki/Composition_over_inheritance\n- Go 是面向对象的吗？：https://flaviocopes.com/golang-is-go-object-oriented/\n- 类 vs 新型 + 接收器？：https://www.reddit.com/r/golang/comments/a8zgvm/class_vs_new_type_receiver/\n- 结构而不是类：https://golangbot.com/structs-instead-of-classes/"
  },
  {
    "path": "content/posts/go/crawler/2018-03-21-douban-top250.md",
    "content": "---\n\ntitle:      \"爬取豆瓣电影 Top250\"\ndate:       2018-03-21 12:30:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - 数据分析\n---\n\n爬虫是标配了，看数据那一刻很有趣。第一个就从最最最简单最基础的爬虫开始写起吧！\n\n项目地址：https://github.com/go-crawler/douban-movie\n\n## 目标\n\n我们的目标站点是 [豆瓣电影 Top250](https://movie.douban.com/top250)，估计大家都很眼熟了\n\n本次爬取 8 个字段，用于简单的概括分析。具体的字段如下：\n\n![image](https://i.loli.net/2018/03/20/5ab11596b8810.png)\n\n简单的分析一下目标源\n\n- 一页共 25 条\n- 含分页（共 10 页）且分页规则是正常的\n- 每一项的数据字段排序都是规则且不变\n\n## 开始\n\n由于量不大，我们的爬取步骤如下\n\n- 分析页面，获取所有的分页\n- 分析页面，循环爬取所有页面的电影信息\n- 爬取的电影信息入库\n\n### 安装\n\n```\n$ go get -u github.com/PuerkitoBio/goquery\n```\n\n### 运行\n\n```\n$ go run main.go\n```\n\n### 代码片段\n\n#### 1、获取所有分页\n\n```go\nfunc ParsePages(doc *goquery.Document) (pages []Page) {\n\tpages = append(pages, Page{Page: 1, Url: \"\"})\n\tdoc.Find(\"#content > div > div.article > div.paginator > a\").Each(func(i int, s *goquery.Selection) {\n\t\tpage, _ := strconv.Atoi(s.Text())\n\t\turl, _ := s.Attr(\"href\")\n\n\t\tpages = append(pages, Page{\n\t\t\tPage: page,\n\t\t\tUrl:  url,\n\t\t})\n\t})\n\n\treturn pages\n}\n```\n\n#### 2、分析豆瓣电影信息\n\n```go\nfunc ParseMovies(doc *goquery.Document) (movies []Movie) {\n\tdoc.Find(\"#content > div > div.article > ol > li\").Each(func(i int, s *goquery.Selection) {\n\t\ttitle := s.Find(\".hd a span\").Eq(0).Text()\n\n\t\t...\n\n\t\tmovieDesc := strings.Split(DescInfo[1], \"/\")\n\t\tyear := strings.TrimSpace(movieDesc[0])\n\t\tarea := strings.TrimSpace(movieDesc[1])\n\t\ttag := strings.TrimSpace(movieDesc[2])\n\n\t\tstar := s.Find(\".bd .star .rating_num\").Text()\n\n\t\tcomment := strings.TrimSpace(s.Find(\".bd .star span\").Eq(3).Text())\n\t\tcompile := regexp.MustCompile(\"[0-9]\")\n\t\tcomment = strings.Join(compile.FindAllString(comment, -1), \"\")\n\n\t\tquote := s.Find(\".quote .inq\").Text()\n\n\t\t...\n\n\t\tlog.Printf(\"i: %d, movie: %v\", i, movie)\n\n\t\tmovies = append(movies, movie)\n\t})\n\n\treturn movies\n}\n```\n\n### 数据\n\n![image](https://i.loli.net/2018/03/21/5ab1309594741.png)\n\n![image](https://i.loli.net/2018/03/21/5ab131ca582f8.png)\n\n![image](https://i.loli.net/2018/03/21/5ab130d3a00d9.png)\n\n看到这些数据，你有什么想法呢，真是好奇 :=)\n"
  },
  {
    "path": "content/posts/go/crawler/2018-04-01-cars.md",
    "content": "---\n\ntitle:      \"爬取汽车之家 二手车产品库\"\ndate:       2018-04-01 12:30:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - 数据分析\n---\n\n项目地址：https://github.com/go-crawler/car-prices\n\n## 目标\n\n最近经常有人在耳边提起汽车之家，也好奇二手车在国内的价格是怎么样的，因此本次的目标站点是 [汽车之家](https://car.autohome.com.cn/2sc/440399/index.html) 的二手车产品库\n\n![image](https://i.loli.net/2018/03/30/5abe47f82a01f.png)\n\n分析目标源：\n\n- 一页共 24 条\n- 含分页，但这个老产品库，在 100 页后会存在问题，因此我们爬取 99 页\n- 可以获取全部城市\n- 共可爬取 19w+ 数据\n\n## 开始\n\n爬取步骤\n\n- 获取全部的城市\n- 拼装全部城市 URL 入队列\n- 解析二手车页面结构\n- 下一页 URL 入队列\n- 循环拉取所有分页的二手车数据\n- 循环拉取队列中城市的二手车数据\n- 等待，确定队列中无新的 URL\n- 爬取的二手车数据入库\n\n### 获取城市\n\n![image](https://i.loli.net/2018/03/31/5abeff11ef583.png)\n\n通过页面查看，可发现在城市筛选区可得到全部的二手车城市列表，但是你仔细查阅代码。会发现它是 JS 加载进来的，城市也统一放在了一个变量中\n\n![image](https://i.loli.net/2018/03/31/5abf056389cf0.png)\n\n有两种提取方法\n\n- 分析 JS 变量，提取出来\n- 直接将 `areaJson` 复制出来作为变量解析\n\n在这里我们直接将其复制粘贴出来即可，因为这是比较少变动的值\n\n### 获取分页\n\n![image](https://i.loli.net/2018/03/31/5abf08ec812e2.png)\n\n通过分析页面可以得知分页链接是有一定规律的，例如：`/2sc/hangzhou/a0_0msdgscncgpi1ltocsp2exb4/`，可以发现 `sp%d`，`sp` 后面为页码\n\n按照常理，可以通过预测所有分页链接，推入队列后 `go routine` 一波 即可快速拉取\n\n但是在这老产品库存在一个问题，在超过 100 页后，下一页永远是 101 页\n\n![image](https://i.loli.net/2018/03/31/5abf0e1e623ec.png)\n\n因此我们采取比较传统的做法，通过拉取下一页的链接去访问，以便适应可能的分页链接改变； 100 页以后的分页展示也很奇怪，先忽视\n\n### 获取二手车数据\n\n页面结构较为固定，常规的清洗 HTML 即可\n\n```go\nfunc GetCars(doc *goquery.Document) (cars []QcCar) {\n\tcityName := GetCityName(doc)\n\tdoc.Find(\".piclist ul li:not(.line)\").Each(func(i int, selection *goquery.Selection) {\n\t\ttitle := selection.Find(\".title a\").Text()\n\t\tprice := selection.Find(\".detail .detail-r\").Find(\".colf8\").Text()\n\t\tkilometer := selection.Find(\".detail .detail-l\").Find(\"p\").Eq(0).Text()\n\t\tyear := selection.Find(\".detail .detail-l\").Find(\"p\").Eq(1).Text()\n\n\t\tkilometer = strings.Join(compileNumber.FindAllString(kilometer, -1), \"\")\n\t\tyear = strings.Join(compileNumber.FindAllString(strings.TrimSpace(year), -1), \"\")\n\t\tpriceS, _ := strconv.ParseFloat(price, 64)\n\t\tkilometerS, _ := strconv.ParseFloat(kilometer, 64)\n\t\tyearS, _ := strconv.Atoi(year)\n\n\t\tcars = append(cars, QcCar{\n\t\t\tCityName: cityName,\n\t\t\tTitle: title,\n\t\t\tPrice: priceS,\n\t\t\tKilometer: kilometerS,\n\t\t\tYear: yearS,\n\t\t})\n\t})\n\n\treturn cars\n}\n```\n\n## 数据\n\n![image](https://i.loli.net/2018/03/31/5abf1d8042196.png)\n\n![image](https://i.loli.net/2018/04/01/5abfbaa14b09c.png)\n\n在各城市的平均价格对比中，我们可以发现北上广深里的北京、上海、深圳都在榜单上，而近年势头较猛的杭州直接占领了榜首，且后几名都有一些距离\n\n而其他城市大致都是梯级下降的趋势，看来一线城市的二手车也是不便宜了，当然这只是均价\n\n![image](https://i.loli.net/2018/03/31/5abf1dbc665f2.png)\n\n我们可以看到价格和公里数的对比，上海、成都、郑州的等比差异是有点大，感觉有需求的话可以在价格和公里数上做一个衡量\n\n![image](https://i.loli.net/2018/03/31/5abf1e1434edc.png)\n\n这图有点儿有趣，粗略的统计了一下总公里数。在前几张图里，平均价格排名较高的统统没有出现在这里，反倒是呼和浩特、大庆、中山等出现在了榜首\n\n是否侧面反应了一线城市的车辆更新换代较快，而较后的城市的车辆倒是换代较慢，公里数基本都杠杠的\n\n![image](https://i.loli.net/2018/03/31/5abf1e4936640.png)\n\n通过对标题的分析，可以得知车辆产品库的命名基本都是品牌名称+自动/手动+XXXX 款+属性，看标题就能知道个概况了\n\n## 参考\n\n### 爬虫项目地址\n\n- https://github.com/go-crawler/car-prices\n\n\n"
  },
  {
    "path": "content/posts/go/crawler/2018-04-28-go2018.md",
    "content": "---\n\ntitle:      \"了解一下Golang的市场行情\"\ndate:       2018-04-28 12:30:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - 数据分析\n---\n\n项目地址：https://github.com/go-crawler/lagou_jobs\n\n如果对你有所帮助，欢迎 Star，给文章来波赞，这样可以让更多的人看见  :)\n\n## 目标\n\n在工作中 Golang 已是一份子，想让大家了解一下 Golang 的市场行情，也想让更多的人熟悉它。因此主要是展示数据分析的结果\n\n目标站点是 [某招聘网站](https://www.lagou.com/) 的职位数据抓取和分析，爬取城市分别为 北京、上海、广州、深圳、杭州、成都，再得出一个结论\n\n### 分析\n\n首先需要进行页面分析，找到我们的抓取方向\n\n![image](https://i.loli.net/2018/04/26/5ae1e28a3412a.jpeg)\n\n搜索 golang 关键字，打开页面 F12 就能看到它发送了四个请求，留意 positionAjax.json 这个请求\n\n![image](https://i.loli.net/2018/04/26/5ae1efe538791.jpeg)\n\n我们仔细研判这个接口的入参和出参\n\n### 入参\n\n1、Query String Param\n\n- city：请求的城市\n\n- needAddtionalResult：是否需要补充额外的参数，这里默认 false\n\n2、Form Data\n\n- first：是否首页\n- pn：页码\n- kd：关键字\n\n### 出参\n\n![image](https://i.loli.net/2018/04/26/5ae1f4c9920a9.jpeg)\n\n就是它了，从返回结果可得出许多有用的信息\n\n- companyFullName：公司全称\n- companyLabelList：公司标签\n- companyShortName：公司简称\n- companySize：公司规模\n- education：学历要求\n- financeStage：融资阶段\n\n等等~\n\n### 分页\n\n在上面两张图中，可以发现在 content 节点中包含 pageNo、pageSize 字段，content.positionResult 节点有 totalCount 字段，可以得知当前是第几页，每页显示多少条，当前的职位总条数\n\n需要注意一下，分页的计算是要向上取整的\n\n## 模拟浏览器头\n\nUser-Agent 可以用 [fake-useragent](https://github.com/EDDYCJY/fake-useragent) 这个项目来随机生成 UA 头 😄\n\n## 数据\n\n### 一、分布图\n\n不同工作、工种，自然也会遍布在不同的工作区域，我们先了解一下各个城市的 Golang 工程师都主要在哪个区上班，心里留个底\n\n#### 北京\n\n![image](https://i.loli.net/2018/04/27/5ae291859667c.jpeg)\n\n#### 上海\n\n![image](https://i.loli.net/2018/04/27/5ae290856b774.jpeg)\n\n#### 广州\n\n![image](https://i.loli.net/2018/04/27/5ae28f1ab3e0c.jpeg)\n\n#### 深圳\n\n![image](https://i.loli.net/2018/04/27/5ae1fbebb1784.jpeg)\n\n#### 杭州\n\n![image](https://i.loli.net/2018/04/27/5ae29218c91dc.jpeg)\n\n#### 成都\n\n![image](https://i.loli.net/2018/04/27/5ae295b1059ed.jpeg)\n\n### 二、招聘与职位数量对比\n\n![image](https://i.loli.net/2018/04/27/5ae296b750dd8.png)\n\n通过分析图中的数据，我们可以得知各城市的招聘职位数量\n\n- 北京：348\n- 上海：145\n- 广州：37\n- 成都：49\n- 杭州：45\n- 深圳：108\n\n总共招聘的职位数量为 732 个，数量顺序分别为 北京 > 上海 > 深圳 > 成都 > 杭州 > 广州\n\n还有另外一个关注点，就是招聘公司数量与职位的数量对比，可以看到 北京 招聘的职位数量为 348 个，而招聘的公司数量为 191 个，约为 1.82 的比例，也就是一家公司能提供两个 Golang 职位，它可能类别不同、（中级、中高级、高级）级别不同，具有一定可能性。而在广州，为 31 对比 37，虽然差额不大，但仍然存在这种现象\n\n可以得出结果，Golang 在市场上具有一定的伸缩空间，也就是具有上升空间，一家公司会将 Golang 应用在多个不同的应用场景，也就是方向不同，需要的级别人才也就不同了\n\n但是需要注意的是，Golang 的市场招聘人数目前份额还是较低，六个城市总数仅为 732 个，与其他大热语言相差有一定距离，需要谨慎\n\n同时，面试 Golang 的人与其他大热语言相比会少些，职位的争夺是否小点呢？\n\n### 三、招聘公司规模\n\n![image](https://i.loli.net/2018/04/27/5ae2ab2babbd9.png)\n\n通过查看招聘 Golang 工程师的公司规模，可以很直观的发现，微型公司使用 Golang 较少，其他类别的规模都有一定程度的应用，且差距不大。在 2000 人以上、50 - 150 人的公司规模中最受青睐\n\n为什么呢，我认为有以下可能\n\n- 大型公司结合场景，想通过 Golang 的特性来解决一些痛点问题\n- 在小型公司 Golang 这颗新星实施起来更便捷，有一定的应用场景\n\n你觉得呢，是不是应该有更多的选择它的原因？\n\n### 四、学历要求\n\n![image](https://s2.ax1x.com/2020/02/27/3dbTdP.png)\n\n在招聘市场上，Golang 的招聘者更希望你是本科学历，大专和不限也有一定的份额，但市场份额相差较大\n\n硕士学历要求的为两个，可以得出，在市场上 Golang 招聘者们对高学历的需求并不高，或者并不强制高学历\n\n### 五、行业领域\n\n![image](https://s2.ax1x.com/2020/02/27/3dqPiT.png)\n\n在这里，重点关注 Golang 工程师的招聘公司都分别在什么行业领域，大头移动互联网是不容置疑的了，还可以惊喜的发现\n\n- 数据服务\n- 电子商务\n- 金融\n- 企业服务\n- 游戏\n\nGolang 在这几个方面都有所应用，说明了在市场上，Golang 的路子是比较广阔的，前景不错\n\n同时，如果可以涉及多个领域的内容，想必身为工程师的你，肯定很激动\n\n### 六、职位诱惑\n\n![image](https://s2.ax1x.com/2020/02/27/3dqEQJ.png)\n\n职位诱惑是投简历时必看的一点了，可以看到高频词条基本都是 IT 从业者关心的话题了，这里你懂的...\n\n重点，我看到了一个 “免费三餐” 的词条命中 7 次，分别来自北京的海淀区、东城区、朝阳区，上海的黄浦区的七家不同的公司，辛苦了\n\n### 七、行业、职位标签\n\n![image](https://s2.ax1x.com/2020/02/27/3dqlWD.png)\n\n在招聘 JD 中，描述和标签常用于给求职者了解这一职业的具体工作内容和其关联性\n\n在图中你可以看到 Golang 常常和什么内容搭上边，这点很有意义哦\n\n1、语言\n\n- Java\n- Python\n- C/C++\n- PHP\n\n在图中可以看出，Golang 与以上四种语言有一定关联性，而 Java 和 Python 分别第一、第二名，可以说明市场上对复合型人才的渴望度更高，也许你不懂也行，但你懂了就最好（加分项）。需要你自身有多语言的经验，也便于和其他人对接\n\n同时 Golang 目前存在许多内部转语言写的情况，所以这一点可以参考\n\n2、职称\n\n- 高级\n- 资深\n- 中级\n\n特意将职称放在第二位，可以发现在市场上 Golang 标签的需求是 高级 > 资深 > 中级，关联第一项 “语言关联” 不难得出这个结论，因为语言只是解决问题的工具，到了中级及以上的工程师都是懂多门语言的居多，再采取不同的方案去解决应用场景上的问题\n\n可得出结论，市场目前对 Golang 更期望是中高、高级、资深的人才，而中级的反而少一点点\n\n大家可以努力再往上冲击冲击\n\n3、组件\n\n- Linux\n- Redis\n- Mysql\n\n4、行业\n\n- 云计算\n- 信息安全\n- 大数据\n- 金融\n- 软件开发\n\n#### 八、薪资与工作年限\n\n![image](https://s2.ax1x.com/2020/02/27/3dq3Se.jpg)\n\n1、1-3 年\n\n一个（成长）特殊的阶段，有个位数也有双位数的，大头可以到 15-30k，20-40k，而初级的也有 8-16k\n\n2、3-5 年\n\n厚积待发的阶段，薪酬范畴的跨度是较大，10-60k 的薪酬都有，这充分说明能力决定你的上下\n\n3、5-10 年\n\n核心，招聘网站上的招聘数量反而少，都会走内推或猎头，不需要特别介绍了\n\n##### 小结\n\n这一部分，相信是很多人关注的地方\n\n在有的文章中会看到，他们的薪资部分是以平均值来展示的。我就很纳闷，因为对平均值并不是很关心，**重点是无法体现薪资幅度**。因此这里我会尽可能的把数据展现给你们看\n\n（正文）从图表来看，Golang 当前的薪酬水平还是很不错的，市场能根据不同阶段（水平）的人给出一个好的价位\n\n（题外话）看完之后希望你能知道以下内容\n\n- 你当前工作年限的最高、最低薪资范畴\n- 你的下一阶段的薪资范畴\n- 为什么有的人高，有的人低\n- 在大头部队还是小头，为什么\n- 不要满足于平均值\n\n### 九、融资阶段\n\n![image](https://s2.ax1x.com/2020/02/27/3dqteI.png)\n\n选用 Golang 的公司大多数都较为稳定，有一部分比较刺激 :)\n\n#### 融资阶段与薪资范畴对比\n\n##### 不需要融资\n\n![image](https://s2.ax1x.com/2020/02/27/3dqyOs.png)\n\n##### 上市公司\n\n![image](https://s2.ax1x.com/2020/02/27/3dqWkV.png)\n\n##### A 轮\n\n![image](https://s2.ax1x.com/2020/02/27/3dqfYT.png)\n\n##### B 轮\n\n![image](https://s2.ax1x.com/2020/02/27/3dqTX9.png)\n\n##### C 轮\n\n![image](https://s2.ax1x.com/2020/02/27/3dqOk6.png)\n\n##### D 轮以上\n\n![image](https://s2.ax1x.com/2020/02/27/3dqz1e.png)\n\n### 十、附近的地铁\n\nGolang 工程师都驻扎在什么地铁站附近呢\n\n经常在地铁上看到同行在看代码，来了解一下都分布在哪 :)\n\n#### 北京\n\n![image](https://s2.ax1x.com/2020/02/27/3dLCnA.png)\n\n#### 上海\n\n![image](https://s2.ax1x.com/2020/02/27/3dLkAP.png)\n\n#### 广州\n\n![image](https://s2.ax1x.com/2020/02/27/3dLAtf.png)\n\n#### 深圳\n\n![image](https://s2.ax1x.com/2020/02/27/3dLZ9S.png)\n\n#### 杭州\n\n![image](https://s2.ax1x.com/2020/02/27/3dLKns.png)\n\n#### 成都\n\n![image](https://s2.ax1x.com/2020/02/27/3dL3NV.png)\n\n## 结论\n\n如同官方所说 \"Go has been on an amazing journey over the last 8+ years\"，作为一门新生语言，一直在不断地发展，**缺点肯定是有的，你要去识别它**\n\n### 从数量来看\n\n单从这个招聘网站上来看，数量方面，与大热语言的招聘职位数量仍然有一定的差距，但 Golang 存在许多内部转语言开发的情况，当前展现出来的数据，**招聘数量不多，但质量不错**\n\n### 从分布图来看\n\n一线城市基本都有 Golang 的职位，虽然其他城市较少，但对于新语言来说是需要持续关注的过程，不能一刀切\n\n### 从职称级别来看\n\nGolang 中高、高级、资深仍然是占大头，给的薪资也基本符合市场行情\n\n### 从方向来看\n\nGolang 涉及的行业领域广泛，移动互联网、数据服务、电子商务、金融、企业服务、云计算等都是它的战场之一\n\n### 从开源项目来看\n\ndocker、k8s、etcd、consul 都挺稳\n\n---\n\n总的来说，Golang 处于一个发展的阶段，市场行情也还行、应用场景较广，不过招聘数量不多，你又怎么看呢？\n\n最后放上今天新发布的 Logo :)\n\n![image](https://s2.ax1x.com/2020/02/27/3dLY3F.jpg)\n\n如果对你有所帮助，欢迎 Star，给文章点个赞，这样可以让更多的人看见这篇文章\n\n## 参考\n\n- 项目地址：https://github.com/go-crawler/lagou_jobs\n"
  },
  {
    "path": "content/posts/go/defer/2019-05-27-defer.md",
    "content": "---\n\ntitle:      \"深入理解 Go defer\"\ndate:       2019-05-27 12:30:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - 源码分析\n---\n\n在上一章节 《深入理解 Go panic and recover》中，我们发现了 `defer` 与其关联性极大，还是觉得非常有必要深入一下。希望通过本章节大家可以对 `defer` 关键字有一个深刻的理解，那么我们开始吧。你先等等，请排好队，我们这儿采取后进先出 LIFO 的出站方式...\n\n## 特性\n\n我们简单的过一下 `defer` 关键字的基础使用，让大家先有一个基础的认知\n\n### 一、延迟调用\n\n```go\nfunc main() {\n\tdefer log.Println(\"EDDYCJY.\")\n\n\tlog.Println(\"end.\")\n}\n```\n\n输出结果：\n\n```\n$ go run main.go\n2019/05/19 21:15:02 end.\n2019/05/19 21:15:02 EDDYCJY.\n```\n\n### 二、后进先出\n\n```go\nfunc main() {\n\tfor i := 0; i < 6; i++ {\n\t\tdefer log.Println(\"EDDYCJY\" + strconv.Itoa(i) + \".\")\n\t}\n\n\n\tlog.Println(\"end.\")\n}\n```\n\n输出结果：\n\n```\n$ go run main.go\n2019/05/19 21:19:17 end.\n2019/05/19 21:19:17 EDDYCJY5.\n2019/05/19 21:19:17 EDDYCJY4.\n2019/05/19 21:19:17 EDDYCJY3.\n2019/05/19 21:19:17 EDDYCJY2.\n2019/05/19 21:19:17 EDDYCJY1.\n2019/05/19 21:19:17 EDDYCJY0.\n```\n\n### 三、运行时间点\n\n```go\nfunc main() {\n\tfunc() {\n\t\t defer log.Println(\"defer.EDDYCJY.\")\n\t}()\n\n\tlog.Println(\"main.EDDYCJY.\")\n}\n```\n\n输出结果：\n\n```\n$ go run main.go\n2019/05/22 23:30:27 defer.EDDYCJY.\n2019/05/22 23:30:27 main.EDDYCJY.\n```\n\n### 四、异常处理\n\n```go\nfunc main() {\n\tdefer func() {\n\t\tif e := recover(); e != nil {\n\t\t\tlog.Println(\"EDDYCJY.\")\n\t\t}\n\t}()\n\n\tpanic(\"end.\")\n}\n```\n\n输出结果：\n\n```\n$ go run main.go\n2019/05/20 22:22:57 EDDYCJY.\n```\n\n## 源码剖析\n\n```\n$ go tool compile -S main.go\n\"\".main STEXT size=163 args=0x0 locals=0x40\n\t...\n\t0x0059 00089 (main.go:6)\tMOVQ\tAX, 16(SP)\n\t0x005e 00094 (main.go:6)\tMOVQ\t$1, 24(SP)\n\t0x0067 00103 (main.go:6)\tMOVQ\t$1, 32(SP)\n\t0x0070 00112 (main.go:6)\tCALL\truntime.deferproc(SB)\n\t0x0075 00117 (main.go:6)\tTESTL\tAX, AX\n\t0x0077 00119 (main.go:6)\tJNE\t137\n\t0x0079 00121 (main.go:7)\tXCHGL\tAX, AX\n\t0x007a 00122 (main.go:7)\tCALL\truntime.deferreturn(SB)\n\t0x007f 00127 (main.go:7)\tMOVQ\t56(SP), BP\n\t0x0084 00132 (main.go:7)\tADDQ\t$64, SP\n\t0x0088 00136 (main.go:7)\tRET\n\t0x0089 00137 (main.go:6)\tXCHGL\tAX, AX\n\t0x008a 00138 (main.go:6)\tCALL\truntime.deferreturn(SB)\n\t0x008f 00143 (main.go:6)\tMOVQ\t56(SP), BP\n\t0x0094 00148 (main.go:6)\tADDQ\t$64, SP\n\t0x0098 00152 (main.go:6)\tRET\n\t...\n```\n\n首先我们需要找到它，找到它实际对应什么执行代码。通过汇编代码，可得知涉及如下方法：\n\n- runtime.deferproc\n- runtime.deferreturn\n\n很显然是运行时的方法，是对的人。我们继续往下走看看都分别承担了什么行为\n\n### 数据结构\n\n在开始前我们需要先介绍一下 `defer` 的基础单元 `_defer` 结构体，如下：\n\n```go\ntype _defer struct {\n\tsiz     int32\n\tstarted bool\n\tsp      uintptr // sp at time of defer\n\tpc      uintptr\n\tfn      *funcval\n\t_panic  *_panic // panic that is running defer\n\tlink    *_defer\n}\n\n...\ntype funcval struct {\n\tfn uintptr\n\t// variable-size, fn-specific data here\n}\n```\n\n- siz：所有传入参数的总大小\n- started：该 `defer` 是否已经执行过\n- sp：函数栈指针寄存器，一般指向当前函数栈的栈顶\n- pc：程序计数器，有时称为指令指针(IP)，线程利用它来跟踪下一个要执行的指令。在大多数处理器中，PC 指向的是下一条指令，而不是当前指令\n- fn：指向传入的函数地址和参数\n- \\_panic：指向 `_panic` 链表\n- link：指向 `_defer` 链表\n\n![image](https://s2.ax1x.com/2020/02/27/3dLNjJ.png)\n\n### deferproc\n\n```go\nfunc deferproc(siz int32, fn *funcval) {\n    ...\n\tsp := getcallersp()\n\targp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)\n\tcallerpc := getcallerpc()\n\n\td := newdefer(siz)\n    ...\n\td.fn = fn\n\td.pc = callerpc\n\td.sp = sp\n\tswitch siz {\n\tcase 0:\n\t\t// Do nothing.\n\tcase sys.PtrSize:\n\t\t*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))\n\tdefault:\n\t\tmemmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))\n\t}\n\n\treturn0()\n}\n```\n\n- 获取调用 `defer` 函数的函数栈指针、传入函数的参数具体地址以及 PC （程序计数器），也就是下一个要执行的指令。这些相当于是预备参数，便于后续的流转控制\n- 创建一个新的 `defer` 最小单元 `_defer`，填入先前准备的参数\n- 调用 `memmove` 将传入的参数存储到新 `_defer` （当前使用）中去，便于后续的使用\n- 最后调用 `return0` 进行返回，这个函数非常重要。能够避免在 `deferproc` 中又因为返回 `return`，而诱发 `deferreturn` 方法的调用。其根本原因是一个停止 `panic` 的延迟方法会使 `deferproc` 返回 1，但在机制中如果 `deferproc` 返回不等于 0，将会总是检查返回值并跳转到函数的末尾。而 `return0` 返回的就是 0，因此可以防止重复调用\n\n#### 小结\n\n在**这个函数中会为新的 `_defer` 设置一些基础属性，并将调用函数的参数集传入。最后通过特殊的返回方法结束函数调用**。另外这一块与先前 [《深入理解 Go panic and recover》](https://segmentfault.com/a/1190000019251478#articleHeader9) 的处理逻辑有一定关联性，其实就是 `gp.sched.ret` 返回 0 还是 1 会分流至不同处理方式\n\n### newdefer\n\n```go\nfunc newdefer(siz int32) *_defer {\n\tvar d *_defer\n\tsc := deferclass(uintptr(siz))\n\tgp := getg()\n\tif sc < uintptr(len(p{}.deferpool)) {\n\t\tpp := gp.m.p.ptr()\n\t\tif len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {\n\t\t\t...\n\t\t\tlock(&sched.deferlock)\n\t\t\td := sched.deferpool[sc]\n\t\t\tunlock(&sched.deferlock)\n\t\t}\n\t\t...\n\t}\n\tif d == nil {\n\t\tsystemstack(func() {\n\t\t\ttotal := roundupsize(totaldefersize(uintptr(siz)))\n\t\t\td = (*_defer)(mallocgc(total, deferType, true))\n\t\t})\n\t\t...\n\t}\n\td.siz = siz\n\td.link = gp._defer\n\tgp._defer = d\n\treturn d\n}\n```\n\n- 从池中获取可以使用的 `_defer`，则复用作为新的基础单元\n- 若在池中没有获取到可用的，则调用 `mallocgc` 重新申请一个新的\n- 设置 `defer` 的基础属性，最后修改当前 `Goroutine` 的 `_defer` 指向\n\n通过这个方法我们可以注意到两点，如下：\n\n- `defer` 与 `Goroutine(g)` 有直接关系，所以讨论 `defer` 时基本离不开 `g` 的关联\n- 新的 `defer` 总是会在现有的链表中的最前面，也就是 `defer` 的特性后进先出\n\n#### 小结\n\n这个函数主要承担了获取新的 `_defer` 的作用，它有可能是从 `deferpool` 中获取的，也有可能是重新申请的\n\n### deferreturn\n\n```go\nfunc deferreturn(arg0 uintptr) {\n\tgp := getg()\n\td := gp._defer\n\tif d == nil {\n\t\treturn\n\t}\n\tsp := getcallersp()\n\tif d.sp != sp {\n\t\treturn\n\t}\n\n\tswitch d.siz {\n\tcase 0:\n\t\t// Do nothing.\n\tcase sys.PtrSize:\n\t\t*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))\n\tdefault:\n\t\tmemmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))\n\t}\n\tfn := d.fn\n\td.fn = nil\n\tgp._defer = d.link\n\tfreedefer(d)\n\tjmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))\n}\n```\n\n如果在一个方法中调用过 `defer` 关键字，那么编译器将会在结尾处插入 `deferreturn` 方法的调用。而该方法中主要做了如下事项：\n\n- 清空当前节点 `_defer` 被调用的函数调用信息\n- 释放当前节点的 `_defer` 的存储信息并放回池中（便于复用）\n- 跳转到调用 `defer` 关键字的调用函数处\n\n在这段代码中，跳转方法 `jmpdefer` 格外重要。因为它显式的控制了流转，代码如下：\n\n```\n// asm_amd64.s\nTEXT runtime·jmpdefer(SB), NOSPLIT, $0-16\n\tMOVQ\tfv+0(FP), DX\t// fn\n\tMOVQ\targp+8(FP), BX\t// caller sp\n\tLEAQ\t-8(BX), SP\t// caller sp after CALL\n\tMOVQ\t-8(SP), BP\t// restore BP as if deferreturn returned (harmless if framepointers not in use)\n\tSUBQ\t$5, (SP)\t// return to CALL again\n\tMOVQ\t0(DX), BX\n\tJMP\tBX\t// but first run the deferred function\n```\n\n通过源码的分析，我们发现它做了两个很 “奇怪” 又很重要的事，如下：\n\n- MOVQ -8(SP), BP：`-8(BX)` 这个位置保存的是 `deferreturn` 执行完毕后的地址\n- SUBQ \\$5, (SP)：`SP` 的地址减 5 ，其减掉的长度就恰好是 `runtime.deferreturn` 的长度\n\n你可能会问，为什么是 5？好吧。翻了半天最后看了一下汇编代码...嗯，相减的确是 5 没毛病，如下：\n\n```\n\t0x007a 00122 (main.go:7)\tCALL\truntime.deferreturn(SB)\n\t0x007f 00127 (main.go:7)\tMOVQ\t56(SP), BP\n```\n\n我们整理一下思绪，照上述逻辑的话，那 `deferreturn` 就是一个 “递归” 了哦。每次都会重新回到 `deferreturn` 函数，那它在什么时候才会结束呢，如下：\n\n```go\nfunc deferreturn(arg0 uintptr) {\n\tgp := getg()\n\td := gp._defer\n\tif d == nil {\n\t\treturn\n\t}\n\t...\n}\n```\n\n也就是会不断地进入 `deferreturn` 函数，判断链表中是否还存着 `_defer`。若已经不存在了，则返回，结束掉它。简单来讲，就是处理完全部 `defer` 才允许你真的离开它。果真如此吗？我们再看看上面的汇编代码，如下：\n\n```\n    。..\n\t0x0070 00112 (main.go:6)\tCALL\truntime.deferproc(SB)\n\t0x0075 00117 (main.go:6)\tTESTL\tAX, AX\n\t0x0077 00119 (main.go:6)\tJNE\t137\n\t0x0079 00121 (main.go:7)\tXCHGL\tAX, AX\n\t0x007a 00122 (main.go:7)\tCALL\truntime.deferreturn(SB)\n\t0x007f 00127 (main.go:7)\tMOVQ\t56(SP), BP\n\t0x0084 00132 (main.go:7)\tADDQ\t$64, SP\n\t0x0088 00136 (main.go:7)\tRET\n\t0x0089 00137 (main.go:6)\tXCHGL\tAX, AX\n\t0x008a 00138 (main.go:6)\tCALL\truntime.deferreturn(SB)\n\t...\n```\n\n的确如上述流程所分析一致，验证完毕\n\n#### 小结\n\n这个函数主要承担了清空已使用的 `defer` 和跳转到调用 `defer` 关键字的函数处，非常重要\n\n## 总结\n\n我们有提到 `defer` 关键字涉及两个核心的函数，分别是 `deferproc` 和 `deferreturn` 函数。而 `deferreturn` 函数比较特殊，是当应用函数调用 `defer` 关键字时，编译器会在其结尾处插入 `deferreturn` 的调用，它们俩一般都是成对出现的\n\n但是当一个 `Goroutine` 上存在着多次 `defer` 行为（也就是多个 `_defer`）时，编译器会进行利用一些小技巧， 重新回到 `deferreturn` 函数去消耗 `_defer` 链表，直到一个不剩才允许真正的结束\n\n而新增的基础单元 `_defer`，有可能是被复用的，也有可能是全新申请的。它最后都会被追加到 `_defer` 链表的表头，从而设定了后进先出的调用特性\n\n## 关联\n\n- [深入理解 Go panic and recover](https://github.com/EDDYCJY/blog/blob/master/golang/pkg/2019-05-18-%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Go-panic-and-recover.md)\n\n## 参考\n\n- [Scheduling In Go](https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part1.html)\n- [Dive into stack and defer/panic/recover in go](http://hustcat.github.io/dive-into-stack-defer-panic-recover-in-go/)\n- [golang-notes](https://github.com/cch123/golang-notes/blob/master/defer.md)\n"
  },
  {
    "path": "content/posts/go/delve.md",
    "content": "---\ntitle: \"一个 Demo 学会使用 Go Delve 调试\"\ndate: 2021-12-31T12:54:57+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n在 Go 语言中，除了 go tool 工具链中的 pprof、trace 等剖析工具的大利器外。常常还会有小伙伴问，有没有更好用，更精细的，\n\n大家总嫌弃 pprof、trace 等工具，不够细，没法一口气看到根因，或者具体变量...希望能够最好能追到代码级别调试的，看到具体变量的值是怎么样的，随意想怎么看怎么看的那种。\n\n为此今天给大家介绍 Go 语言强大的 Delve （dlv）调试工具，来更深入问题剖析。\n\n## 安装\n\n我们需要先安装 Go delve，若是 Go1.16 及以后的版本，可以直接执行下述命令安装：\n\n```\n$ go install github.com/go-delve/delve/cmd/dlv@latest\n```\n\n也可以通过 git clone 的方式安装：\n\n```\n$ git clone https://github.com/go-delve/delve\n$ cd delve\n$ go install github.com/go-delve/delve/cmd/dlv\n```\n\n在安装完毕后，我们执行 `dlv version` 命令，查看安装情况：\n\n```\n$ dlv version\nDelve Debugger\nVersion: 1.7.0\nBuild: $Id: e353a65161e6ed74952b96bbb62ebfc56090832b $\n```\n\n可以明确看到我们所安装的版本是 v1.7.0。\n\n## 演示程序\n\n我们计划用一个反转字符串的演示程序来进行 Go 程序的调试。第一部分先是完成 `stringer` 包的 `Reverse` 方法。\n\n代码如下：\n\n```golang\npackage stringer\n\nfunc Reverse(s string) string {\n\tr := []rune(s)\n\tfor i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {\n\t\tr[i], r[j] = r[j], r[i]\n\t}\n\treturn string(r)\n}\n```\n\n再在具体的 `main` 启动函数中进行调用。代码如下：\n\n```golang\nimport (\n\t\"fmt\"\n\n\t\"github.com/eddycjy/awesome-project/stringer\"\n)\n\nfunc main() {\n\tfmt.Println(stringer.Reverse(\"脑子进煎鱼了！\"))\n}\n```\n\n输出结果：\n\n```\n！了鱼煎进子脑\n```\n\n## 进行调试\n\nDelve 是 Go 程序的源代码级调试器。Delve 使您能够通过控制流程的执行与您的程序进行交互，查看变量，提供线程、goroutine、CPU 状态等信息。\n\n其一共支持如下 11 个子命令：\n\n```\nAvailable Commands:\n  attach      Attach to running process and begin debugging.\n  connect     Connect to a headless debug server.\n  core        Examine a core dump.\n  dap         [EXPERIMENTAL] Starts a TCP server communicating via Debug Adaptor Protocol (DAP).\n  debug       Compile and begin debugging main package in current directory, or the package specified.\n  exec        Execute a precompiled binary, and begin a debug session.\n  help        Help about any command\n  run         Deprecated command. Use 'debug' instead.\n  test        Compile test binary and begin debugging program.\n  trace       Compile and begin tracing program.\n  version     Prints version.\n```\n\n我们今天主要用到的是 debug 命令，他能够编译并开始调试当前目录下的主包，或指定的包，是最常用的功能之一。\n\n接下来我们利用这个演示程序来进行 dlv 的深入调试和应用。\n\n执行如下命令：\n\n```\n➜  awesomeProject dlv debug .\nType 'help' for list of commands.\n(dlv) \n```\n\n我们先在演示程序根目录下执行了 debug，进入了 dlv 的交互模式。\n\n再使用关键字 `b`（break 的缩写）对 `main.main` 方法设置断点：\n\n```\n(dlv) b main.main\nBreakpoint 1 (enabled) set at 0x10cbab3 for main.main() ./main.go:9\n(dlv) \n```\n\n设置完毕后，我们可以看到方法对应的文件名、行数。接着我们可以执行关键字 `c`（continue 的缩写）跳转到下一个断点处：\n\n```\n(dlv) c\n> main.main() ./main.go:9 (hits goroutine(1):1 total:1) (PC: 0x10cbab3)\n     4:\t\t\"fmt\"\n     5:\t\n     6:\t\t\"github.com/eddycjy/awesome-project/stringer\"\n     7:\t)\n     8:\t\n=>   9:\tfunc main() {\n    10:\t\tfmt.Println(stringer.Reverse(\"脑子进煎鱼了！\"))\n    11:\t}\n(dlv) \n```\n\n在断点处，我看可以看到具体的代码块、goroutine、CPU 寄存器地址等运行时信息。\n\n紧接着执行关键字 `n`（next 的缩写）单步执行程序的下一步：\n\n```\n(dlv) n\n> main.main() ./main.go:10 (PC: 0x10cbac1)\n     5:\t\n     6:\t\t\"github.com/eddycjy/awesome-project/stringer\"\n     7:\t)\n     8:\t\n     9:\tfunc main() {\n=>  10:\t\tfmt.Println(stringer.Reverse(\"脑子进煎鱼了！\"))\n    11:\t}\n```\n\n我们可以看到程序走到了 main.go 文件中的第 10 行中，并且调用了 `stringer.Reverse` 方法去处理。\n\n此时我们可以执行关键字 `s`（step 的关键字）进入到这个函数中去继续调试：\n\n```\n(dlv) s\n> github.com/eddycjy/awesome-project/stringer.Reverse() ./stringer/string.go:3 (PC: 0x10cb87b)\n     1:\tpackage stringer\n     2:\t\n=>   3:\tfunc Reverse(s string) string {\n     4:\t\tr := []rune(s)\n     5:\t\tfor i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {\n     6:\t\t\tr[i], r[j] = r[j], r[i]\n     7:\t\t}\n     8:\t\treturn string(r)\n```\n\n输入后，调试的光标会到 `Reverse` 方法上，此时我们可以调用关键字 `p`（print 的缩写）传出所传入的变量的值：\n\n```\n(dlv) p s\n\"脑子进煎鱼了！\"\n```\n\n此处函数的形参变量是 s，输出了 “脑子进煎鱼了！”，与我们所传入的是一致的。\n\n但故事一般没有这么的简单，会用到 Delve 来调试，说明是比较细致、隐患的 BUG。为此我们大多需要更进一步的深入。\n\n我们继续围观 `Reverse` 方法：\n\n```\n     5:\t\tfor i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {\n     6:\t\t\tr[i], r[j] = r[j], r[i]\n     7:\t\t}\n```\n\n从表现来看，我们常常会怀疑是第 6 行可能是问题的所在。这时可以针对性的对第 6 行进行断点查看：\n\n```\n(dlv) b 6\nBreakpoint 2 (enabled) set at 0x10cb92c for github.com/eddycjy/awesome-project/stringer.Reverse() ./stringer/string.go:6\n```\n\n设置完断点后，我们只需要执行关键字 `c`，继续下一步：\n\n```\n(dlv) c\n> github.com/eddycjy/awesome-project/stringer.Reverse() ./stringer/string.go:6 (hits goroutine(1):1 total:1) (PC: 0x10cb92c)\n     1:\tpackage stringer\n     2:\t\n     3:\tfunc Reverse(s string) string {\n     4:\t\tr := []rune(s)\n     5:\t\tfor i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {\n=>   6:\t\t\tr[i], r[j] = r[j], r[i]\n     7:\t\t}\n     8:\t\treturn string(r)\n     9:\t}\n```\n\n走到对应的代码片段后，执行关键字 `locals`：\n\n```\n(dlv) locals\nr = []int32 len: 7, cap: 32, [...]\nj = 6\ni = 0\n```\n\n我们就可以看到对应的变量 r, i, j 的值是多少，可以根据此来分析程序流转是否与我们预想的一致。\n\n另外也可以调用关键字 `set` 去针对特定变量设置期望的值：\n\n```\n(dlv) set i = 1\n(dlv) locals\nr = []int32 len: 7, cap: 32, [...]\nj = 6\ni = 1\n```\n\n设置后，若还需要继续排查，可以继续调用关键字 `c` 去定位，这种常用于特定变量的特定值的异常，这样一设置一调试基本就能排查出来了。\n\n在排查完毕后，我们可以执行关键字 `r`（reset 的缩写）：\n\n```\n(dlv)  r\nProcess restarted with PID 56614\n```\n\n执行完毕后，整个调试就会重置，像是前面在打断点时所设置的变量值就会恢复。\n\n若要查看设置的断点情况，也可以执行关键字 `bp` 查看：\n\n```\n(dlv) bp\nBreakpoint runtime-fatal-throw (enabled) at 0x1038fc0 for runtime.fatalthrow() /usr/local/Cellar/go/1.16.2/libexec/src/runtime/panic.go:1163 (0)\nBreakpoint unrecovered-panic (enabled) at 0x1039040 for runtime.fatalpanic() /usr/local/Cellar/go/1.16.2/libexec/src/runtime/panic.go:1190 (0)\n\tprint runtime.curg._panic.arg\nBreakpoint 1 (enabled) at 0x10cbab3 for main.main() ./main.go:9 (0)\nBreakpoint 2 (enabled) at 0x10cb92c for github.com/eddycjy/awesome-project/stringer.Reverse() ./stringer/string.go:6 (0)\n```\n\n查看断点情况后，若有部分已经排除了，可以调用关键字 `clearall` 对一些断点清除：\n\n```\n(dlv) clearall main.main\nBreakpoint 1 (enabled) cleared at 0x10cbab3 for main.main() ./main.go:9\n```\n\n若不指点断点，则会默认清除全部断点。\n\n在日常的 Go 工程中，若都从 main 方法进入就太繁琐了。我们可以直接借助函数名进行调式定位：\n\n```\n(dlv) funcs Reverse\ngithub.com/eddycjy/awesome-project/stringer.Reverse\n(dlv) b stringer.Reverse\nBreakpoint 3 (enabled) set at 0x10cb87b for github.com/eddycjy/awesome-project/stringer.Reverse() ./stringer/string.go:3\n(dlv) c\n> github.com/eddycjy/awesome-project/stringer.Reverse() ./stringer/string.go:3 (hits goroutine(1):1 total:1) (PC: 0x10cb87b)\n     1:\tpackage stringer\n     2:\t\n=>   3:\tfunc Reverse(s string) string {\n     4:\t\tr := []rune(s)\n     5:\t\tfor i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {\n     6:\t\t\tr[i], r[j] = r[j], r[i]\n     7:\t\t}\n     8:\t\treturn string(r)\n```\n\n紧接着其他步骤都与先前的一样，进行具体的调试就好了。我们也可以借助 Go 语言的公共函数进行计算：\n\n```\n(dlv) p len(r)-1\n6\n```\n\n也可以借助关键字 `vars` 查看某个包下的所有全局变量的值，例如：`vars main`。这种方式对于查看全局变量的情况非常有帮助。\n\n排查完毕后，执行关键字 `exit` 就可以愉快的退出了：\n\n```\n(dlv) exit\n```\n\n解决完问题，可以下班了 ：）\n\n## 总结\n\n在 Go 语言中，Delve 调试工具是与 Go 语言亲和度最高的，因为 Delve 是 Go 语言实现的。其在我们日常工作中，非常常用。\n\n像是假设程序的 for 循环运行到第 N 次才出现 BUG 时，我们就可以通过断点对应的方法和代码块，再设置变量的值，进行具体的查看，就可以解决。\n\n"
  },
  {
    "path": "content/posts/go/empty-struct.md",
    "content": "---\ntitle: \"详解 Go 空结构体的 3 种使用场景\"\ndate: 2021-12-31T12:54:51+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n在大家初识 Go 语言时，总会拿其他语言的基本特性来类比 Go 语言，说白了就是老知识和新知识产生关联，实现更高的学习效率。\n\n![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/87c66858abf04b848643b6fa03f4bdab~tplv-k3u1fbpfcp-zoom-1.image)\n\n最常见的类比，就是 “Go 语言如何实现面向对象？”，进一步展开就是 Go 语言如何实现面向对象特性中的继承。\n\n这不仅在学习中才用到类比，在业内的 Go 面试中也有非常多的面试官喜欢问：\n\n![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d1cc9c2b2e174fc4a82506b368591a70~tplv-k3u1fbpfcp-zoom-1.image)\n\n来自读者微信群\n\n在今天这篇文章中，煎鱼带大家具体展开了解这块的知识。一起愉快地开始吸鱼之路。\n\n什么是面向对象\n-------\n\n在了解 Go 语言是不是面向对象（简称：OOP） 之前，我们必须先知道 OOP 是啥，得先给他 “下定义”。\n\n根据 Wikipedia 的定义，我们梳理出 OOP 的几个基本认知：\n\n*   面向对象编程（OOP）是一种基于 \"对象\" 概念的编程范式，它可以包含数据和代码：数据以字段的形式存在（通常称为属性或属性），代码以程序的形式存在（通常称为方法）。\n    \n*   对象自己的程序可以访问并经常修改自己的数据字段。\n    \n*   对象经常被定义为类的一个实例。\n    \n*   对象利用属性和方法的私有/受保护/公共可见性，对象的内部状态受到保护，不受外界影响（被封装）。\n    \n\n基于这几个基本认知进行一步延伸出，面向对象的三大基本特性：\n\n*   封装。\n    \n*   继承。\n    \n*   多态。\n    \n\n至此对面向对象的基本概念讲解结束，想更进一步了解的可自行网上冲浪。\n\nGo 是面向对象的语言吗\n------------\n\n“Go 语言是否一门面向对象的语言？”，这是一个日经话题。官方 FAQ 给出的答复是：\n\n![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bb8296e58a6d4c7c882984dcceaaf9c0~tplv-k3u1fbpfcp-zoom-1.image)\n\n是的，也不是。原因是：\n\n*   Go 有类型和方法，并且允许面向对象的编程风格，但没有类型层次。\n    \n*   Go 中的 \"接口 \"概念提供了一种不同的方法，我们认为这种方法易于使用，而且在某些方面更加通用。还有一些方法可以将类型嵌入到其他类型中，以提供类似的东西，但不等同于子类。\n    \n*   Go 中的方法比 C++ 或 Java 中的方法更通用：它们可以为任何类型的数据定义，甚至是内置类型，如普通的、\"未装箱的 \"整数。它们并不局限于结构（类）。\n    \n*   Go 由于缺乏类型层次，Go 中的 \"对象 \"比 C++ 或 Java 等语言更轻巧。\n    \n\nGo 实现面向对象编程\n-----------\n\n### 封装\n\n面向对象中的 “封装” 指的是可以隐藏对象的内部属性和实现细节，仅对外提供公开接口调用，这样子用户就不需要关注你内部是怎么实现的。\n\n在 Go 语言中的属性访问权限，通过首字母大小写来控制：\n\n*   首字母大写，代表是公共的、可被外部访问的。\n    \n*   首字母小写，代表是私有的，不可以被外部访问。\n    \n\nGo 语言的例子如下：\n\n```\ntype Animal struct {\n name string\n}\n\nfunc NewAnimal() *Animal {\n return &Animal{}\n}\n\nfunc (p *Animal) SetName(name string) {\n p.name = name\n}\n\nfunc (p *Animal) GetName() string {\n return p.name\n}\n\n```\n\n在上述例子中，我们声明了一个结构体 `Animal`，其属性 `name` 为小写。没法通过外部方法，在配套上存在 Setter 和 Getter 的方法，用于统一的访问和设置控制。\n\n以此实现在 Go 语言中的基本封装。\n\n### 继承\n\n面向对象中的 “继承” 指的是子类继承父类的特征和行为，使得子类对象（实例）具有父类的实例域和方法，或子类从父类继承方法，使得子类具有父类相同的行为。\n\n![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ec1cf116a1bf404981cb624009945157~tplv-k3u1fbpfcp-zoom-1.image)\n\n图来自网络\n\n从实际的例子来看，就是动物是一个大父类，下面又能细分为 “食草动物”、“食肉动物”，这两者会包含 “动物” 这个父类的基本定义。\n\n在 Go 语言中，是没有类似 `extends` 关键字的这种继承的方式，在语言设计上采取的是组合的方式：\n\n```\ntype Animal struct {\n Name string\n}\n\ntype Cat struct {\n Animal\n FeatureA string\n}\n\ntype Dog struct {\n Animal\n FeatureB string\n}\n\n```\n\n在上述例子中，我们声明了 `Cat` 和 `Dog` 结构体，其在内部匿名组合了 `Animal` 结构体。因此 `Cat` 和 `Dog` 的实例都可以调用 `Animal` 结构体的方法：\n\n```\nfunc main() {\n p := NewAnimal()\n p.SetName(\"煎鱼，记得点赞~\")\n\n dog := Dog{Animal: *p}\n fmt.Println(dog.GetName())\n}\n\n```\n\n同时 `Cat` 和 `Dog` 的实例可以拥有自己的方法：\n\n```\nfunc (dog *Dog) HelloWorld() {\n fmt.Println(\"脑子进煎鱼了\")\n}\n\nfunc (cat *Cat) HelloWorld() {\n fmt.Println(\"煎鱼进脑子了\")\n}\n\n```\n\n上述例子能够正常包含调用 `Animal` 的相关属性和方法，也能够拥有自己的独立属性和方法，在 Go 语言中达到了类似继承的效果。\n\n### 多态\n\n面向对象中的 “多态” 指的同一个行为具有多种不同表现形式或形态的能力，具体是指一个类实例（对象）的相同方法在不同情形有不同表现形式。\n\n多态也使得不同内部结构的对象可以共享相同的外部接口，也就是都是一套外部模板，内部实际是什么，只要符合规格就可以。\n\n在 Go 语言中，多态是通过接口来实现的：\n\n```\ntype AnimalSounder interface {\n MakeDNA()\n}\n\nfunc MakeSomeDNA(animalSounder AnimalSounder) {\n animalSounder.MakeDNA()\n}\n\n```\n\n在上述例子中，我们声明了一个接口类型 `AnimalSounder`，配套一个 `MakeSomeDNA` 方法，其接受 `AnimalSounder` 接口类型作为入参。\n\n因此在 Go 语言中。只要配套的 `Cat` 和 `Dog` 的实例也实现了 `MakeSomeDNA` 方法，那么我们就可以认为他是 `AnimalSounder` 接口类型：\n\n```\ntype AnimalSounder interface {\n MakeDNA()\n}\n\nfunc MakeSomeDNA(animalSounder AnimalSounder) {\n animalSounder.MakeDNA()\n}\n\nfunc (c *Cat) MakeDNA() {\n fmt.Println(\"煎鱼是煎鱼\")\n}\n\nfunc (c *Dog) MakeDNA() {\n fmt.Println(\"煎鱼其实不是煎鱼\")\n}\n\nfunc main() {\n MakeSomeDNA(&Cat{})\n MakeSomeDNA(&Dog{})\n}\n\n```\n\n当 `Cat` 和 `Dog` 的实例实现了 `AnimalSounder` 接口类型的约束后，就意味着满足了条件，他们在 Go 语言中就是一个东西。能够作为入参传入 `MakeSomeDNA` 方法中，再根据不同的实例实现多态行为。\n\n总结\n--\n\n通过今天这篇文章，我们基本了解了面向对象的定义和 Go 官方对面向对象这一件事的看法，同时针对面向对象的三大特性：“封装、继承、多态” 在 Go 语言中的实现方法就进行了一一讲解。\n\n在日常工作中，基本了解这些概念就可以了。若是面试，可以针对三大特性：“封装、继承、多态” 和 五大原则 “单一职责原则（SRP）、开放封闭原则（OCP）、里氏替换原则（LSP）、依赖倒置原则（DIP）、接口隔离原则（ISP）” 进行深入理解和说明。\n\n在说明后针对上述提到的概念。再在 Go 语言中讲解其具体的实现和利用到的基本原理，互相结合讲解，就能得到一个不错的效果了。\n\n## 鼓励\n\n若有任何疑问欢迎评论区反馈和交流，**最好的关系是互相成就**，各位的**点赞**就是[煎鱼](https://github.com/eddycjy)创作的最大动力，感谢支持。\n\n> 文章持续更新，可以微信搜【脑子进煎鱼了】阅读，本文 **GitHub** [github.com/eddycjy/blog](https://github.com/eddycjy/blog) 已收录，欢迎 Star 催更。\n\n\n参考\n--\n\n*   Is Go an Object Oriented language?\n    \n*   面向对象的三大基本特征，五大基本原则\n    \n*   Go 面向对象编程（译）"
  },
  {
    "path": "content/posts/go/enum.md",
    "content": "---\ntitle: \"小技巧分享：在 Go 如何实现枚举？\"\ndate: 2021-12-31T12:54:50+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n在日常的业务工程开发中，我们常常会有使用枚举值的诉求，枚举控的好，测试值边界一遍过...\n\n有的小伙伴会说，在 Go 语言不是有 `iota` 类型做枚举吗，那煎鱼你这篇文章还讲什么？\n\n讲道理，Go 语言并没有 enum 关键字，有用过 Protobuf 等的小伙伴知道，Go 语言只是 ”有限的枚举“ 支持，我们也会用常量来定义，枚举值也需要有字面意思的映射。\n\n示例\n--\n\n在一些业务场景下，是没法达到我们的诉求的。示例如下：\n\n```\ntype FishType int\n\nconst (\n A FishType = iota\n B\n C\n D\n)\n\nfunc main() {\n fmt.Println(A, B, C, D)\n}\n```\n\n输出结果为：“0 1 2 3”。这时候就一脸懵逼了...枚举值，应该除了键以外，还得有个对应的值。也就是这个 “0 1 2 3” 分别对应着什么含义，是不是应该输出 ”A B C D“\n\n但 Go 语言这块就没有直接的支撑了，因此这不是一个完整的枚举类型的实现。\n\n同时假设我们传入超过 `FishType` 类型声明范围的枚举值，在 Go 语言中默认也不会有任何控制，是正常输出的。\n\n上述这种 Go 枚举实现，在某种情况下是不完全的，严格意义上不能成为 enum（枚举）。\n\n使用 String 做枚举\n-------------\n\n如果要支持枚举值的对应输出的话，我们可以通过如下方式：\n\n```\ntype FishType int\n\nconst (\n A FishType = iota\n B\n C\n D\n)\n\nfunc (f FishType) String() string {\n return [...]string{\"A\", \"B\", \"C\", \"D\"}[f]\n}\n```\n\n运行程序：\n\n```\nfunc main() {\n var f FishType = A\n fmt.Println(f)\n switch f {\n case A:\n  fmt.Println(\"脑子进煎鱼了\")\n case B:\n  fmt.Println(\"记得点赞\")\n default:\n  fmt.Println(\"别别别...\")\n }\n}\n```\n\n输出结果：\n\n```\nA\n脑子进煎鱼了\n```\n\n我们可以借助 Go 中 `String` 方法的默认约定，针对于定义了 `String` 方法的类型，默认输出的时候会调用该方法。\n\n这样就可以达到获得枚举值的同时，也能拿到其映射的字面意思。\n\n自动生成 String\n-----------\n\n但每次手动编写还是比较麻烦的。在这一块，我们可以利用官方提供的 `cmd/string` 来快速实现。\n\n我们安装如下命令：\n\n```\ngo install golang.org/x/tools/cmd/stringer\n```\n\n在所需枚举值上设置 `go:generate` 指令：\n\n```\n//go:generate stringer -type=FishType\ntype FishType int\n```\n\n在项目根目录执行：\n\n```\ngo generate ./...\n\n```\n\n会在根目录生成 fishtype\\_string.go 文件：\n\n```\n.\n├── fishtype_string.go\n├── go.mod\n├── go.sum\n└── main.go\n```\n\nfishtype\\_string 文件内容：\n\n```\npackage main\n\nimport \"strconv\"\n\nconst _FishType_name = \"ABCD\"\n\nvar _FishType_index = [...]uint8{0, 1, 2, 3, 4}\n\nfunc (i FishType) String() string {\n if i < 0 || i >= FishType(len(_FishType_index)-1) {\n  return \"FishType(\" + strconv.FormatInt(int64(i), 10) + \")\"\n }\n return _FishType_name[_FishType_index[i]:_FishType_index[i+1]]\n}\n```\n\n所生成出来的文件，主要是根据枚举值和映射值做了个映射，且针对超出枚举值的场景进行了判断：\n\n```\nfunc main() {\n var f1 FishType = A\n fmt.Println(f1)\n var f2 FishType = E\n fmt.Println(f2)\n}\n```\n\n执行 `go run .` 查看程序运行结果：\n\n```\n$ go run .\nA\nFishType(4)\n```\n\n总结\n--\n\n在今天这篇文章中，我们介绍了如何在 Go 语言实现标准的枚举值，虽然有些繁琐，但整体不会太难。\n\n![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/05d5614987b04e0183e20fb5ac5249d4~tplv-k3u1fbpfcp-zoom-1.image)\n\n也有小伙伴已经在社区中提出了 ”proposal: spec: add typed enum support“ 的提案，相信未来有机会能看到 Go 自身支持 enum（枚举）的那一天。\n\n你平时会怎么在业务代码中实现枚举呢，欢迎大家一起留言交流：）\n\n\n**欢迎大家在评论区留言和交流 ：）**\n\n## 鼓励\n\n若有任何疑问欢迎评论区反馈和交流，**最好的关系是互相成就**，各位的**点赞**就是[煎鱼](https://github.com/eddycjy)创作的最大动力，感谢支持。\n\n> 文章持续更新，可以微信搜【脑子进煎鱼了】阅读，本文 **GitHub** [github.com/eddycjy/blog](https://github.com/eddycjy/blog) 已收录，学习 Go 语言可以看 [Go 学习地图和路线](https://github.com/eddycjy/go-developer-roadmap)，欢迎 Star 催更。"
  },
  {
    "path": "content/posts/go/func-reload.md",
    "content": "---\ntitle: \"Go 为什么不支持函数重载和参数默认值？\"\ndate: 2021-12-31T12:55:16+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - 为什么\n---\n\n大家好，我是煎鱼。\n\n大家在初学习 Go 语言时，带着其他语言的习惯，总是会有些不习惯，感觉非常不能理解，直打问号。\n\n其中一点就是 Go 语言不支持函数重载和参数默认值，觉得使用起来很不方便。\n\n为此，在这篇文章中煎鱼就和大家一起来了解为什么，有又会怎么样。\n\n## 函数重载\n\n函数重载（function overloading），也叫方法重载。是某些编程语言（如 C++、C#、Java、Swift、Kotlin 等）具有的一项特性。\n\n该特性**允许创建多个具有不同实现的同名函数**，对重载函数的调用会运行其适用于调用上下文的具体实现。\n\n从功能上来讲，就是允许一个函数调用根据上下文执行不同的方法，达到调用同一个函数名，执行不同的方法。\n\n一个简单的例子：\n\n```c++\n#include <iostream>\n\nint Volume(int s) {  // 立方体的体积。\n  return s * s * s;\n}\n\ndouble Volume(double r, int h) {  // 圆柱体的体积。\n  return 3.1415926 * r * r * static_cast<double>(h);\n}\n\nlong Volume(long l, int b, int h) {  // 长方体的体积。\n  return l * b * h;\n}\n\nint main() {\n  std::cout << Volume(10);\n  std::cout << Volume(2.5, 8);\n  std::cout << Volume(100l, 75, 15);\n}\n```\n\n在上述例子中，实现了 3 个同名的 `Volume` 函数，但是 3 个函数的入参个数、类型均不一样，也代表了不同的实现目的。\n\n在主函数 `main` 中，传入了不同的入参，编译器或运行时再进行内部处理，从程序上来看达到了调用不同函数的目的。\n\n这就是函数重载，一函数多形态。\n\n## 参数默认值\n\n参数默认值，又叫缺省参数。指的是允许程序员设定缺省参数并指定默认值，**当调用该函数并未指定值时，该缺省参数将为缺省值来使用**。\n\n一个简单的例子：\n\n```c++\n int my_func(int a, int b, int c=12);\n```\n\n在上述例子中，函数 `my_func` 一共有 3 个变量，分别是：a、b、c。变量 c 设置了缺省值，也就是 12。\n\n其调用方式可以为：\n\n```c++\n // 第一种调用方式\n result = my_func(1, 2, 3);\n // 第二种调用方式\n result = my_func(1, 2);\n```\n\n在第一种方式中，就会正常的传入所有参数。在第二种方式，由于第三个参数 c 并没有传递，因此会直接使用缺省值 12。\n\n这就是参数默认值，也叫缺省参数。\n\n## 为什么不支持\n\n### 美好\n\n从上述的功能特性介绍来看，似乎非常的不错，能够节省很多功夫。像是 Go 语言的 context 库中的这些方法：\n\n```golang\nfunc WithCancel(parent Context) (ctx Context, cancel CancelFunc)\nfunc WithDeadline(parent Context, d time.Time) (Context, CancelFunc)\nfunc WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)\n```\n\n要是有函数重载，直接就 WithXXX 就好了，只需要关注传入的参数类型，也不用 “记” 那么多个方法名了。\n\n有同学说，有参数默认值。那就可以直接设置在上面，作为 “最佳实践” 给到使用函数的人，岂不美哉。那怎么 Go 语言就不支持呢？\n\n### 细思\n\n其实这和设计理念，和对程序的理解有关系。说白了，就是你喜欢 “显式”，还是 “隐喻”。\n\n函数重载和参数默认值，其实是不好的行为。调用者只看函数名字，可能没法知道，你这个默认值，又或是入参不同，会调用的东西，会产生怎么样的后果？\n\n你可以观察一下自己的行为。大部分人都会潜意识的追进去看代码，看看会调到哪，缺省值的作用是什么，以确保可控。\n\n### 敲定\n\n这细思的可能，在 Go 语言中是不被允许的。Go 语言的**设计理念就是 “显式大于隐喻”，追求明确，显式**。\n\n在 Go FAQ 《Why does Go not support overloading of methods and operators?》有相关的解释。\n\n如下图：\n\n![](https://files.mdnice.com/user/3610/582eac4e-ecd1-4fb5-bde7-b2cbcac7f809.png)\n\n官方有明确提到两个观点：\n- 函数重载：拥有各种同名但不同签名的方法有时是很有用的，但在实践中也可能是混乱和脆弱的。\n- 参数默认值：操作符重载，似乎更像是一种便利，不是绝对的要求。没有它，程序会更简单。\n\n这就是为什么 Go 语言不支持的原因。\n\n## 总结\n\n在这篇文章中，我们介绍了业内常见的编程语言的函数重载和参数默认值的概念和使用方法。也结合了 Go 语言自身的设计理念，说明了为什么不支持的原因。\n\n你会希望 Go 语言支持这几个特性功能吗，欢迎在评论区留言讨论和交流：）\n\n## 参考\n\n- 维基百科（函数重载和缺省值定义）\n- Frequently Asked Questions (FAQ)"
  },
  {
    "path": "content/posts/go/fuzzing.md",
    "content": "---\ntitle: \"提高 Go 程序健壮性，Fuzzing 要来了！\"\ndate: 2021-12-31T12:54:50+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n就在前几天，Go1.17 beta1 正式发布：\n\n![](https://files.mdnice.com/user/3610/fb4ceb3d-ef1c-4c32-a7c3-a4188a0d74b0.png)\n\n兴冲冲本想着看一下当初在 Go1.17 的计划中，预计会支持的新特性：模糊测试（Fuzzing）。不过没想到...计划赶不上变化，官方正式宣告 Fuzzing 不会出现在 Go1.17 的新功能中。\n\n煎鱼在悲伤之际，发现 Go 在 dev.fuzz 分支上提供了该功能的 Beta 测试，因此今天带大家一起来深入该特性。\n\n## 什么是 Fuzzing\n\nFuzzing 是一种自动测试技术，包括向计算机程序提供随机数据作为输入。然后监测程序是否出现恐慌、断言失败、无限循环等。\n\nFuzzing 不是使用一个小的、预先定义好的手动创建的输入集（如单元测试），而是用新的案例不断地测试代码，以努力 ”锻炼“ 有关软件的所有方面。\n\n这听起来很 ”难“。但在过去的几年里，Fuzzing 的技术水平有了很大的提高。Fuzzing 不再是需要专业知识才能成功使用的东西，现代模糊测试策略能更快、更有效地找到有用的输入。\n\n在应用程序中，就是你只要引入一个 package，对着 API 一顿用就可以了。\n\n## 为什么要做 Fuzzing\n\n可能会有小伙伴说，测试？直接人工测试，再把测试数据准备一下，配套 YAPI 等接口管理平台，把自动化接口测试一弄就好了。还需要 Fuzzing 吗？\n\n其实 Fuzzing 是对其他形式的测试、代码审查和静态分析的补充，它通过生成一个随机测试用例去覆盖人为测不到的各种复杂场景。而这些输入几乎不可能人为去构造，总会被传统测试所遗漏。\n\n## 发生在身边的 Fuzzing\n\n实际上 Go-fuzz 对 Go 标准库进行过测试，依然这这之中发现了 200  多个 bug：\n\n![](https://files.mdnice.com/user/3610/9b00972c-2231-4406-84b8-2e8e44f57d6d.png)\n\n这还是建立在标准库已经比较成熟，且由非常有经验的开发者编写，在生产中使用多年的情况下，依然有如此多的问题。\n\n## 快速上手\n\n我们需要在本地执行如下命令，需开启 GO111MODULE 和天梯：\n\n```golang\n$ go get golang.org/dl/gotip\n$ gotip download dev.fuzz\n```\n\n执行完毕后会从 dev.fuzz 分支构建 Go 工具链，同时 gotip 可以作为 go 命令的替代者命令，也就是可以运行 Fuzzing 的相关代码了。\n\n```golang\n// +build gofuzzbeta\n\npackage tests\n\nimport (\n\t\"net/url\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc FuzzParseQuery(f *testing.F) {\n\tf.Add(\"x=1&y=2\")\n\tf.Fuzz(func(t *testing.T, queryStr string) {\n\t\tquery, err := url.ParseQuery(queryStr)\n\t\tif err != nil {\n\t\t\tt.Skip()\n\t\t}\n\t\tqueryStr2 := query.Encode()\n\t\tquery2, err := url.ParseQuery(queryStr2)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"ParseQuery failed to decode a valid encoded query %s: %v\", queryStr2, err)\n\t\t}\n\t\tif !reflect.DeepEqual(query, query2) {\n\t\t\tt.Errorf(\"ParseQuery gave different query after being encoded\\nbefore: %v\\nafter: %v\", query, query2)\n\t\t}\n\t})\n}\n```\n\n在相应的目录下执行 `gotip test -fuzz=FuzzParseQuery` 命令，输出结果：\n\n```golang\nfuzzing, elapsed: 3.0s, execs: 319 (106/sec), workers: 4, interesting: 15\nfuzzing, elapsed: 6.0s, execs: 665 (111/sec), workers: 4, interesting: 15\nfuzzing, elapsed: 9.0s, execs: 1019 (113/sec), workers: 4, interesting: 15\nfuzzing, elapsed: 12.0s, execs: 1400 (117/sec), workers: 4, interesting: 15\n...\n```\n\n需要注意的是：\n- Fuzzing 会消耗大量的内存，在运行时会影响到机器的性能（一运行，小风扇就转了起来）。\n- Fuzzing 会默认使用 `GOMAXPROCS`相同的核数，可以通过执行 `-parallel` 标识来控制数量。\n- Fuzzing 会默认在运行时，将扩大测试范围的数值写入 `$GOCACHE/fuzz` 内的模糊缓存目录，目前是没有限制的，可以通过运行 `gotip clean -fuzzcache` 来清除。\n\n## 总结\n\n在今天这篇文章中，我们介绍了 Fuzzing 是什么。\n简单而言，模糊测试（Fuzzing）在真实环境已经被验证了其有效性，其可以随机生成测试用例去覆盖人为测不到的各种复杂场景，带来很大的收益。\n\n在接下来中，除了依赖开源的 go-fuzz 库外，Go 语言也正式的在支持 Fuzzing，虽然他放了 Go1.17 的鸽子...\n\n这会对构建 Go 程序健壮性的又一强心剂！"
  },
  {
    "path": "content/posts/go/gdb.md",
    "content": "---\ntitle: \"学会使用 GDB 调试 Go 代码\"\ndate: 2021-12-31T12:54:57+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n上一篇文章《一个 Demo 学会使用 Go Delve 调试》我们详细介绍了 Go 语言如何使用 Delve 进行排查和调试，对于问题的解决非常的有帮助。\n\n但调试工具肯定不止只有 Delve，今天我们来介绍第二个神器，那就是：GDB。\n\n## GDB 是什么\n\nGDB 是一个类 UNIX 系统下的程序调试工具，允许你看到另一个程序在执行时 \"内部 \"发生了什么，或者程序在崩溃时正在做什么。\n\n![GDB Logo](https://files.mdnice.com/user/3610/35fc6475-ec0d-44a0-ae8c-920121130edb.png)\n\n主要可以做四类事情：\n\n1. 启动你的程序，指定任何可能影响其行为的东西。\n2. 使你的程序在指定的条件下停止。\n3. 检查当你的程序停止时发生了什么。\n4. 改变你程序中的东西，这样你就可以试验纠正一个错误的影响，并继续了解另一个错误。\n\n## 安装\n\n如果是在 MacOS 上的话，可以直接使用 brew 安装：\n\n```\nbrew install gdb\n```\n\n如果是在 Linux ，则使用自带的包管理工具进行安装即可，但需要注意安装完毕后需要在 HOME 目录进行相关配置。\n\n安装完毕后，执行 `gdb` 就可以看到：\n\n```\n$ gdb\nGNU gdb (GDB) 10.2\n...\n(gdb) \n```\n\n写此文时最新的 gdb 版本已经是 10.2 了，我也升级了上去。问题不大，还多了不少功能。\n\n## 编译\n\n我们还是使用先前的演示程序来进行调试。但由于 Go 语言的不少编译优化，因此在编译运行程序时，有以下几点需要注意：\n\n- go build 编译时需要增加 `-gcflags=all=\"-N -l\"` 指令来关闭内联优化，方便接下来的调试。\n\n- 若是 MacOS，在 go build 编译时需要增加 `-ldflags='-compressdwarf=false'` 指令。\n  - 若不禁止，则会出现 `No symbol table is loaded. Use the \"file\" command.` 的错误。\n  - Go 编译默认为了减少二进制大小会默认压缩 DWARF 调试信息，但这会影响 gdb 的调试，因此需要将其关闭。\n  \n编译的命令是：\n\n```\n$ go build -gcflags=all=\"-N -l\" -ldflags='-compressdwarf=false' .\n```\n\n输出结果：\n\n```\n！了鱼煎进子脑\n```\n  \n## 尝试 gdb\n\nGDB 有两种调试模式，分别是文本用户界面（Text User Interface，简称 tui）和默认的命令行模式：\n\n```\n// 调试界面\n$ gdb -tui ./awesome-project\n\n// 命令行模式\n$ gdb ./awesome-project\n```\n\n接下来我们使用 gdb tui 的调试模式来给大家演示功能。\n\n我们在执行命令 `gdb -tui ./awesome-project` 后，窗口会切换为如下：\n\n\n![gdb tui 初始样子](https://files.mdnice.com/user/3610/2a7096f4-addd-404f-9a47-4a97cbe9d595.png)\n\n你会发现中间提示 “No Source Available”，此时你需要继续回车两次，他就会自动加载插件支持，提示：“Loading Go Runtime support.”。\n\n我们就可以看到具体的代码块内容，如下：\n\n![](https://files.mdnice.com/user/3610/351571a3-b89e-42f2-941e-24e734bfae26.png)\n\n用 MacOS 的同学需要注意，如果你在断点时发现发现了如下错误：\n\n```\n(gdb) b main.main\nBreakpoint 1 at 0x10a2ea0: file /Users/eddycjy/go-application/awesomeProject/main.go, line 15.\n(gdb) r\nStarting program: /Users/eddycjy/go-application/awesomeProject/hello\nUnable to find Mach task port for process-id 64212: (os/kern) failure (0x5).\n (please check gdb is codesigned - see taskgated(8))\n```\n\n也就是 “please check gdb is codesigned - see taskgated(8)”，则需要重新处理证书认证和授权，是 MacOS 使用上的一个问题，具体可参考：《[Codesign gdb on OSX](https://gist.github.com/hlissner/898b7dfc0a3b63824a70e15cd0180154)》。\n\n解决后，咱们的 gdb 就算是能够正确的运行起来了！\n\n## 常用 gdb 命令\n\n在 gdb 中，和 dlv 一样有常用的关键字命令。当然了，gdb 的 help all 输出非常多：\n\n```\n(gdb) help all\n\nCommand class: aliases\nCommand class: breakpoints\n\nawatch -- Set a watchpoint for an expression.\nbreak, brea, bre, br, b -- Set breakpoint at specified location.\nbreak-range -- Set a breakpoint for an address range.\ncatch -- Set catchpoints to catch events.\n...\n```\n\n常用的关键字如下：\n- b：break 的缩写，作用是打断点，例如：main.main，可带代码行数。\n- r：run 的缩写，作用是运行程序到下一个断点处。\n- c：continue 的缩写，作用是继续执行到下一个断点。\n- s：step 的缩写，作用是单步执行，如果有所调用的方法，将会进入该方法。\n- l：list 的缩写，作用是查看对应的源码。\n- n：next 的缩写，作用是单步执行，不会进入所调用的方法，。\n- q：quit 的缩写，作用是退出。\n- info breakpoints：作用是查看所有设置的断点信息。\n- info locals：作用是查看变量信息。\n- info args：作用是查看函数的入参和出参的具体值。\n- info goroutines：作用是查看 goroutines 的信息。\n- goroutine 1 bt：作用是查看指定序号的 goroutine 调用堆栈。\n\n## 进行调试\n\n在调试上与 dlv 差不多，也是先执行关键字 `b` 打断点：\n\n```\n(gdb) b main.main\nBreakpoint 1 at 0x10cbaa0: file /Users/eddycjy/go-application/awesomeProject/main.go, line 9.\n```\n\n也可以先执行关键字 `l` 查看对应的代码情况再进行做决定：\n\n```\n(gdb) l main.main\n4\t\t\"fmt\"\n5\t\n6\t\t\"github.com/eddycjy/awesome-project/stringer\"\n7\t)\n8\t\n9\tfunc main() {\n10\t\tfmt.Println(stringer.Reverse(\"脑子进煎鱼了！\"))\n11\t}\n```\n\n查看对应 goroutines 正在运行的函数情况：\n\n```\n(gdb) info goroutines\n  1  waiting runtime.gosched\n* 13  running runtime.goexit\n```\n\n根据 pprof 等所得到的 goroutine 序号进行进一步的分析：\n\n```\n(gdb) goroutine 1 bt\n#0  0x000000000040facb in runtime.gosched () at /home/user/go/src/runtime/proc.c:873\n#1  0x00000000004031c9 in runtime.chanrecv (c=void, ep=void, selected=void, received=void)\n at  /home/user/go/src/runtime/chan.c:342\n#2  0x0000000000403299 in runtime.chanrecv1 (t=void, c=void) at/home/user/go/src/runtime/chan.c:423\n#3  0x000000000043075b in testing.RunTests (matchString...\n```\n\n注意一个细节，gdb 调试是可以看到并对 runtime 包内容的代码进行断点和分析的。\n\n也可以和 dlv 一样执行 p 关键字输出相应的值的类型、值内容：\n\n```\n(gdb) p re\n(gdb) p t\n$1 = (struct testing.T *) 0xf840688b60\n(gdb) p t\n$1 = (struct testing.T *) 0xf840688b60\n(gdb) p *t\n$2 = {errors = \"\", failed = false, ch = 0xf8406f5690}\n(gdb) p *t->ch\n$3 = struct hchan<*testing.T>\n```\n\n与 dlv 大同小异。\n\n## 总结\n\n总体上来讲，MacOS 上使用 gdb 还是挺麻烦的，在 Linux 环境下使用 gdb 还是更方便些。\n\n由于 **dlv 和 gdb 在大致的调试上不会差距的太远**，因此本文就没有过于展开。\n\n若是对业务代码进行分析，更建议使用 dlv，也就是我们上一篇文章所讲的内容。**若有 runtime 库的调试需求的话，推荐使用 gdb 来作为首要调试工具**，若无这方面诉求，建议使用 dlv。"
  },
  {
    "path": "content/posts/go/generics-apis.md",
    "content": "---\ntitle: \"出泛型后 API 怎么办？Go 开发者要注意了\"\ndate: 2021-12-31T12:55:14+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n前段时间社区里一下子就爆了，主要是各大媒体引用了 Go 语言之父 \nRob Pike 所提的《go: don't change the libraries in 1.18》。\n\n![](https://files.mdnice.com/user/3610/08e47a87-cf63-4dec-b0b5-fc178a494c4c.png)\n\n很多社交媒体都做了跟进，认为 Rob Pike 是硬性的反对 Go 泛型的 API 改造！\n\n如果读者只看了标题，有可能会产生一些误解实际上其表达的意思和近期 Go 社区讨论的事项是有关联的，要一起综合来看。\n\n为此，今天煎鱼就和大家一起来理一理，看看 Go 泛型 API 的改造工程，是个怎么一回事？\n\n## 现状\n\n马上就是 2021.11 月，连深圳都变冷了...根据 Go 语言的发布周期，Go1.18 版本的发布，那就是 2022.02 月左右。\n\n![](https://files.mdnice.com/user/3610/f849c76d-e84d-43d6-96c5-b30df77f1dd9.png)\n\n现在给到 Ian Lance Taylor、\nRobert Griesemer 等大佬仅剩 3 个月的时间给大家讨论泛型细节，进一步完善实现，达到生产可用。\n\n抛出 Go 泛型的实现进度不说，现在遇到了一个比较大的问题。那就是**实现泛型后 ”如何更新泛型的 API“**。\n\n这之中包含好几个方面，分别是：既有标准库、开源库，新标准库等。不同库之间是不同的人在维护。\n\n但这里存在一个大问题，如下图：\n\n![](https://files.mdnice.com/user/3610/033083ef-d0ba-40f7-93d4-006e65310cf9.png)\n\nRuss Cox 在 9 月就提出了 ”how to update APIs for generics“ 的疑惑，当时显然这一块还没有共识。在 11 月的现在，从讨论的记录来看，**怎么做还没有达成一个最终的明确共识**（初步已有，未正式答复）。\n\n但存在一个问题，Go 社区对于泛型的迫切度，热情非常高，各种泛型化的标准库的提案都提出来了，推着设计者往前走。\n\n## 争议\n\n结合来看 Rob Pike，更多是：建议和提醒 Go 社区和核心开发团队，**要 ”悠着点“**，Go1.18 想支持泛型，做完成库的改造，还得代价小，毕竟细节很多。\n\n引用其理由，核心论据是：\n- 在一个版本中，做泛型、标准库等，要做的事情太多，很可能会弄错。\n- 没有在 Go 中使用新类型的经验，无法为其设计提供有力的依据。\n- Go1 兼容性的承诺，在任何细节上出错的代价都很高，要等待、观察和学习。\n\n和一句谚语很接近：”**不要一口气吃胖子**“，何况没有相关的经验，都只是详细的推理、预演，需要晋升。\n\n在 Go issues 中也有人吐槽，1.18 空有泛型的实现。其他配套的标准库等都没有，那这个 Go1.18 出来的泛型意义是？\n\n## 后续\n\n虽然还没有最终拍板，但是根据讨论的过程和社区赞同数（👍）来看，如下：\n\n![](https://files.mdnice.com/user/3610/4856e2c4-7ae4-421c-812b-561ea6cf1cef.png)\n\n后续仍然会设计、构建、测试和使用用于切片（Slice）、地图（Map）、通道（Channel）等的新库。\n\n这些库并没有生产可用，会把他们**放在 golang/x/exp 仓库**中，可以使用，仅作为现阶段的实验性的库，没有兼容性保障。\n\n\n![](https://files.mdnice.com/user/3610/47b8cae4-4ef8-42a3-af6c-686dd8acfb7a.png)\n\n\n该实验库会在一两个周期内会改变、调整和发展。能够让 Go 社区的开发者们尝试一下使用，以便接受更多的意见。\n\n再根据使用者的反馈通过经验和分析进行更新，就会把它们移到主仓库中，才达到正式生产可用的级别。\n\n## 总结\n\n在今天这篇文章中，我们针对 Rob Pike 为什么会要调整 Go 泛型后的标准库 API 等的提议进行了分析。\n\n为此我们了解到 Go 核心团队对 ”how to update APIs for generics“ 的顾虑，以及现有社区的激情，综合来看，给出的逐步演进的泛型方案建议。\n\n以此可知，Go 完整泛型（含配套库）的生产可用，可能还要经历几个 Go 版本，让不少人望穿秋水了...\n"
  },
  {
    "path": "content/posts/go/generics-design.md",
    "content": "---\ntitle: \"Go 泛型的 3 个核心设计，你学会了吗？\"\ndate: 2022-02-05T15:52:46+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\nGo1.18 的泛型是闹得沸沸扬扬，虽然之前写过很多篇针对泛型的一些设计和思考。但因为泛型的提案之前一直还没定型，所以就没有写完整介绍。\n\n如今已经基本成型，就由煎鱼带大家一起摸透 Go 泛型。本文内容主要涉及泛型的 3 大概念，非常值得大家深入了解。\n\n如下：\n- 类型参数。\n- 类型约束。\n- 类型推导。\n\n## 类型参数\n\n类型参数，这个名词。不熟悉的小伙伴咋一看就懵逼了。\n\n泛型代码是使用抽象的数据类型编写的，我们将其称之为类型参数。当程序运行通用代码时，类型参数就会被类型参数所取代。也就是**类型参数是泛型的抽象数据类型**。\n\n简单的泛型例子：\n\n```go\n\nfunc Print(s []T) {\n\tfor _, v := range s {\n\t\tfmt.Println(v)\n\t}\n}\n```\n\n代码有一个 `Print` 函数，它打印出一个片断的每个元素，其中片断的元素类型，这里称为 T，是未知的。\n\n这里引出了一个要做泛型语法设计的点，那就是：T 的**泛型类型参数，应该如何定义**？\n\n在现有的设计中，分为两个部分：\n- 类型参数列表：**类型参数列表将会出现在常规参数的前面**。为了区分类型参数列表和常规参数列表，类型参数列表**使用方括号**而不是小括号。\n- 类型参数约束：如同常规参数有类型一样，类型参数也有元类型，被称为约束（后面会进一步介绍）。\n\n结合完整的例子如下：\n\n```go\n// Print 可以打印任何片断的元素。\n// Print 有一个类型参数 T，并有一个单一的（非类型）的 s，它是该类型参数的一个片断。\nfunc Print[T any](s []T) {\n\t// do something...\n}\n```\n\n在上述代码中，我们声明了一个函数 `Print`，其有一个类型参数 T，类型约束为 `any`，表示为任意的类型，作用与 `interface{}` 一样。他的入参变量 `s` 是类型 T 的切片。\n\n函数声明完了，在函数调用时，我们需要指定类型参数的类型。如下：\n\n```go\n\tPrint[int]([]int{1, 2, 3})\n```\n\n在上述代码中，我们指定了传入的类型参数为 int，并传入了 `[]int{1, 2, 3}` 作为参数。\n\n其他类型，例如 float64:\n\n```go\n\tPrint[float64]([]float64{0.1, 0.2, 0.3})\n```\n\n也是类似的声明方式，照着套就好了。\n\n## 类型约束\n\n说完类型参数，我们再说说 “约束”。在所有的类型参数中都要指定类型约束，才能叫做完整的泛型。\n\n以下分为两个部分来具体展开讲解：\n- 定义函数约束。\n- 定义运算符越苏\n\n### 为什么要有类型约束\n\n为了**确保调用方能够满足接受方的程序诉求**，保证程序中所应用的函数、运算符等特性能够正常运行。\n\n泛型的类型参数，类型约束，相辅相成。\n\n### 定义函数约束\n\n#### 问题点\n\n我们看看 Go 官方所提供的例子：\n\n```go\nfunc Stringify[T any](s []T) (ret []string) {\n\tfor _, v := range s {\n\t\tret = append(ret, v.String()) // INVALID\n\t}\n\treturn ret\n}\n```\n\n该方法的实现目的是：任何类型的切片都能转换成对应的字符串切片。但程序逻辑里有一个问题，那就是他的入参 T 是 `any` 类型，是任意类型都可以传入。\n\n其内部又调用了 `String` 方法，自然也就会报错，因为只像是 int、float64 等类型，就可能没有实现该方法。\n\n你说要定义有效的类型约束，那像是上面的例子，在泛型中如何实现呢？\n\n要求传入方要有内置方法，就得定义一个 `interface` 来约束他。\n\n#### 单个类型\n\n例子如下：\n\n```go\ntype Stringer interface {\n\tString() string\n}\n```\n\n在泛型方法中应用：\n\n```go\nfunc Stringify[T Stringer](s []T) (ret []string) {\n\tfor _, v := range s {\n\t\tret = append(ret, v.String())\n\t}\n\treturn ret\n}\n```\n\n再将 `Stringer` 类型放到原有的 `any` 类型处，就可以实现程序所需的诉求了。\n\n#### 多个类型\n\n如果是多个类型约束。例子如下：\n\n```go\ntype Stringer interface {\n\tString() string\n}\n\ntype Plusser interface {\n\tPlus(string) string\n}\n\nfunc ConcatTo[S Stringer, P Plusser](s []S, p []P) []string {\n\tr := make([]string, len(s))\n\tfor i, v := range s {\n\t\tr[i] = p[i].Plus(v.String())\n\t}\n\treturn r\n}\n```\n\n与常规的入参、出参类型声明一样的规则。\n\n### 定义运算符约束\n\n完成了函数约束的定义后，剩下一个要啃的大骨头就是 “运算符” 的约束了。\n\n#### 问题点\n\n我们看看 Go 官方的例子：\n\n```go\nfunc Smallest[T any](s []T) T {\n\tr := s[0] // panic if slice is empty\n\tfor _, v := range s[1:] {\n\t\tif v < r { // INVALID\n\t\t\tr = v\n\t\t}\n\t}\n\treturn r\n}\n```\n\n经过上面的函数例子，我们很快能意识到这个程序根本无法运行成功。\n\n其入参是 `any` 类型，程序内部是按 slice 类型来获取值，且在内部又进行运算符比较，那如果真是 slice，内部就可能每个值类型都不一样。\n\n如果一个是 slice，一个是 int 类型，又如何进行运算符的值对比？\n\n#### 近似元素\n\n可能有的同学想到了重载运算符，但...想太多了，Go 语言没有支持的计划。为此做了一个新的设计，那就是允许限制类型参数的类型范围。\n\n语法如下：\n\n```go\nInterfaceType  = \"interface\" \"{\" {(MethodSpec | InterfaceTypeName | ConstraintElem) \";\" } \"}\" .\nConstraintElem = ConstraintTerm { \"|\" ConstraintTerm } .\nConstraintTerm = [\"~\"] Type .\n```\n\n例子如下：\n\n```go\ntype AnyInt interface{ ~int }\n```\n\n上述声明的类型集是 `~int`，也就是所有类型为 int 的类型（如：int、int8、int16、int32、int64）都能够满足这个类型约束的条件。\n\n包括底层类型是 int8 类型的，例如：\n\n```go\ntype AnyInt8 int8\n```\n\n也就是在该匹配范围内的。\n\n#### 联合元素\n\n如果希望进一步缩小限定类型，可以结合分隔符来使用，用法为：\n\n```go\ntype AnyInt interface{\n ~int8 | ~int64\n}\n```\n\n就可以将类型集限定在 int8 和 int64 之中。\n\n#### 实现运算符约束\n\n基于新的语法，结合新的概念联合和近似元素，可以把程序改造一下，实现在泛型中的运算符的匹配。\n\n类型约束的声明，如下：\n\n```go\ntype Ordered interface {\n\t~int | ~int8 | ~int16 | ~int32 | ~int64 |\n\t\t~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |\n\t\t~float32 | ~float64 |\n\t\t~string\n}\n```\n\n应用的程序如下：\n\n```go\nfunc Smallest[T Ordered](s []T) T {\n\tr := s[0] // panics if slice is empty\n\tfor _, v := range s[1:] {\n\t\tif v < r {\n\t\t\tr = v\n\t\t}\n\t}\n\treturn r\n}\n```\n\n确保了值均为基础数据类型后，程序就可以正常运行了。\n\n## 类型推导\n\n程序员写代码，一定程度的偷懒是必然的。\n\n在一定的场景下，可以通过类型推导来避免明确地写出一些或所有的类型参数，编译器会进行自动识别。\n\n建议复杂函数和参数能明确是最好的，否则读代码的同学会比较麻烦，可读性和可维护性的保证也是工作中重要的一点。\n\n### 参数推导\n\n函数例子。如下：\n\n```go\nfunc Map[F, T any](s []F, f func(F) T) []T { ... }\n```\n\n公共代码片段。如下：\n\n```go\nvar s []int\nf := func(i int) int64 { return int64(i) }\nvar r []int64\n```\n明确指定两个类型参数。如下：\n\n```go\nr = Map[int, int64](s, f)\n```\n\n只指定第一个类型参数，变量 f 被推断出来。如下：\n\n```go\nr = Map[int](s, f)\n```\n\n不指定任何类型参数，让两者都被推断出来。如下：\n\n```go\nr = Map(s, f)\n```\n\n### 约束推导\n\n神奇的在于，类型推导不仅限与此，连约束都可以推导。\n\n函数例子，如下：\n\n```go\nfunc Double[E constraints.Number](s []E) []E {\n\tr := make([]E, len(s))\n\tfor i, v := range s {\n\t\tr[i] = v + v\n\t}\n\treturn r\n}\n```\n\n基于此的推导案例，如下：\n\n```go\ntype MySlice []int\n\nvar V1 = Double(MySlice{1})\n```\n\nMySlice 是一个 int 的切片类型别名。变量 V1 的类型编译器推导后 []int 类型，并不是 MySlice。\n\n原因在于编译器在比较两者的类型时，会将 MySlice 类型识别为 []int，也就是 int 类型。\n\n要实现 “正确” 的推导，需要如下定义：\n\n```go\ntype SC[E any] interface {\n\t[]E \n}\n\nfunc DoubleDefined[S SC[E], E constraints.Number](s S) S {\n\tr := make(S, len(s))\n\tfor i, v := range s {\n\t\tr[i] = v + v\n\t}\n\treturn r\n}\n```\n\n基于此的推导案例。如下：\n\n```go\nvar V2 = DoubleDefined[MySlice, int](MySlice{1})\n```\n\n只要定义显式类型参数，就可以获得正确的类型，变量 V2 的类型会是 MySlice。\n\n那如果不声明约束呢？如下：\n\n```go\nvar V3 = DoubleDefined(MySlice{1})\n```\n\n编译器通过函数参数进行推导，也可以明确变量 V3 类型是 MySlice。\n\n## 总结\n\n今天我们在文章中给大家介绍了泛型的三个重要概念，分别是：\n- 类型参数：泛型的抽象数据类型。\n- 类型约束：确保调用方能够满足接受方的程序诉求。\n- 类型推导：避免明确地写出一些或所有的类型参数。\n\n在内容中也涉及到了联合元素、近似元素、函数约束、运算符约束等新概念。本质上都是基于三个大概念延伸出来的新解决方法，一环扣一环。\n\n你学会 Go 泛型了吗，设计的如何，欢迎一起和煎鱼讨论：）\n\n## 参考\n- [Type Parameters Proposal](https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md)\n- [Summary of Go Generics Discussions](https://docs.google.com/document/d/1vrAy9gMpMoS3uaVphB32uVXX4pi-HnNjkMEgyAHX4N4/)"
  },
  {
    "path": "content/posts/go/generics-proposal.md",
    "content": "---\ntitle: \"快报：正式提案将泛型特性加入 Go 语言\"\ndate: 2021-01-13T21:11:44+08:00\ntoc: true\nimages:\ntags: \n  - 泛型\n---\n\n经历九九八十一难，多年的不断探讨和 Go 语言爱好者们在社区中的强烈关注，且 Go 官方在 2020 年不断放出消息。\n\n![image](https://image.eddycjy.com/1d0e5a264c65e37659f142bc2ee55805.jpg)\n\n总算在 2021 年 1 月 12 日。官方正式提出将泛型特性加入 Go 语言的 proposal 了，且最新的草案设计已经更新。\n\n\n基本语法如下：\n\n```\nfunc Print[T any](s []T) {\n\t// same as above\n}\n```\n\n其大体的概述如下：\n\n- 函数可以具有使用方括号的其他类型参数列表，但其他情况下看起来像普通的参数列表：`func F[T any](p T) { ... }`。\n- 类型也可以具有类型参数列表：`type MySlice[T any] []T`。\n- 每个类型参数都有一个类型约束，就像每个普通参数都有一个类型：`func F[T Constraint](p T) { ... }`。\n- 类型约束是接口类型。\n- 新的预声明名称 `any` 是允许任何类型的类型约束。\n- 用作类型约束的接口类型可以具有预先声明的类型的列表。只有与那些类型之一匹配的类型参数才能满足约束条件。\n- 泛型函数只能使用其类型约束所允许的操作。\n- 使用泛型函数或类型需要传递类型实参。\n- 在通常情况下，类型推断允许省略函数调用的类型参数。\n\n根据官方博客的消息，如果该提案被正式接受。那么将会在 2021 年底之前完成一个基本可用的泛型特性使用，又或是会作为 Go1.18beta 的一部分。\n\n这是 Go 泛型特性的又一步前进。若大家有兴趣进一步了解或想提出意见，可查看下述传送门：\n\n- A Proposal for Adding Generics to Go：https://blog.golang.org/generics-proposal。\n- proposal: spec: add generic programming using type parameters：https://github.com/golang/go/issues/43651。\n\n今年年底或 Go1.18beta 到底能不能看到泛型的正式完整可用版本呢，值得期待。"
  },
  {
    "path": "content/posts/go/gin/2018-02-10-install.md",
    "content": "---\n\ntitle:      \"「连载一」Go 介绍与环境安装\"\ndate:       2018-02-10 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n---\n\n## 本文目标\n\n- 学会安装 Go。\n- 知道什么是 Go。\n- 知道什么是 Go modules。\n- 了解 Go modules 的小历史。\n- 学会简单的使用 Go modules。\n- 了解 Gin，并简单跑起一个 Demo。\n\n## 准备环节\n\n### 安装 Go\n\n#### Centos\n\n首先，根据对应的操作系统选择安装包 [下载](https://studygolang.com/dl)，在这里我使用的是 Centos 64 位系统，如下：\n\n```sh\n$ wget https://studygolang.com/dl/golang/go1.13.1.linux-amd64.tar.gz\n\n$ tar -zxvf go1.13.1.linux-amd64.tar.gz\n\n$ mv go/ /usr/local/\n```\n\n配置 /etc/profile\n\n```sh\nvi /etc/profile\n```\n\n添加环境变量 GOROOT 和将 GOBIN 添加到 PATH 中\n\n```sh\nexport GOROOT=/usr/local/go\nexport PATH=$PATH:$GOROOT/bin\n```\n\n配置完毕后，执行命令令其生效\n\n```sh\nsource /etc/profile\n```\n\n在控制台输入`go version`，若输出版本号则**安装成功**，如下：\n\n```\n$ go version\ngo version go1.13.1 linux/amd64\n```\n\n#### MacOS\n\n在 MacOS 上安装 Go 最方便的办法就是使用 brew，安装如下：\n\n```\n$ brew install go\n```\n\n升级命令如下：\n\n```\n$ brew upgrade go\n```\n\n注：升级命令你不需要执行，但我想未来你有一天会用到的。\n\n同样在控制台输入`go version`，若输出版本号则**安装成功**。\n\n### 了解 Go\n\n#### 是什么\n\n> Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.\n\n上述为官方说明，如果简单来讲，大致为如下几点：\n\n- Go 是编程语言。\n- 谷歌爸爸撑腰。\n- 语言级高并发。\n- 上手快，入门简单。\n- 简洁，很有特色。\n- 国内使用人群逐年增多。\n\n#### 谁在用\n\n![image](https://image.eddycjy.com/6d278b22a4c0bf29c6b89ece99cd6c88.jpg)\n\n#### 有什么\n\n那么大家会有些疑问，纠结 `Go` 本身有什么东西，我们刚刚设置的环境变量又有什么用呢，甚至作为一名老粉，你会纠结 GOPATH 去哪里了，我们一起接着往下看。\n\n##### 目录结构\n\n首先，我们在解压的时候会得到一个名为 `go` 的文件夹，其中包括了所有 `Go` 语言相关的一些文件，如下：\n\n```\n$ tree -L 1 go\ngo\n├── api\n├── bin\n├── doc\n├── lib\n├── misc\n├── pkg\n├── src\n├── test\n└── ...\n```\n\n在这之中包含了很多文件夹和文件，我们来简单说明其中主要文件夹的作用：\n\n- api：用于存放依照 `Go` 版本顺序的 API 增量列表文件。这里所说的 API 包含公开的变量、常量、函数等。这些 API 增量列表文件用于 `Go` 语言 API 检查\n- bin：用于存放主要的标准命令文件（可执行文件），包含`go`、`godoc`、`gofmt`\n- blog：用于存放官方博客中的所有文章\n- doc：用于存放标准库的 HTML 格式的程序文档。我们可以通过`godoc`命令启动一个 Web 程序展示这些文档\n- lib：用于存放一些特殊的库文件\n- misc：用于存放一些辅助类的说明和工具\n- pkg：用于存放安装`Go`标准库后的所有归档文件（以`.a`结尾的文件）。注意，你会发现其中有名称为`linux_amd64`的文件夹，我们称为平台相关目录。这类文件夹的名称由对应的操作系统和计算架构的名称组合而成。通过`go install`命令，`Go`程序会被编译成平台相关的归档文件存放到其中\n- src：用于存放 `Go`自身、`Go` 标准工具以及标准库的所有源码文件\n- test：存放用来测试和验证`Go`本身的所有相关文件\n\n##### 环境变量\n\n你可能会疑惑刚刚设置的环境变量是什么，如下：\n\n- GOROOT：`Go`的根目录。\n- PATH 下增加 `$GOROOT/bin`：`Go`的 `bin`下会存放可执行文件，我们把他加入 `$PATH` 后，未来拉下来并编译后的二进制文件就可以直接在命令行使用。\n\n那在什么东西都不下载的情况下，`$GOBIN` 下面有什么呢，如下：\n\n```\nbin/ $ls\ngo  gofmt\n```\n\n- go：`Go` 二进制本身。\n- gofmt：代码格式化工具。\n\n因此我们刚刚把 `$GOBIN` 加入到 `$PATH` 后，你执行 `go version` 命令后就可以查看到对应的输出结果。\n\n注：MacOS 用 brew 安装的话就不需要。\n\n#### 放在哪\n\n你现在知道 Go 是什么了，也知道 Go 的源码摆在哪了，你肯定会想，那我应用代码放哪呢，答案是在 **Go1.11+ 和开启 Go Modules 的情况下摆哪都行**。\n\n### 了解 Go Modules\n\n#### 了解历史\n\n在过去，Go 的依赖包管理在工具上混乱且不统一，有 dep，有 glide，有 govendor...甚至还有因为外网的问题，频频导致拉不下来包，很多人苦不堪言，盼着官方给出一个大一统做出表率。\n\n而在 Go modules 正式出来之前还有一个叫 dep 的项目，我们在上面有提到，它是 Go 的一个官方实验性项目，目的也是为了解决 Go 在依赖管理方面的问题，当时社区里面几乎所有的人都认为 dep 肯定就是未来 Go 官方的依赖管理解决方案了。\n\n但是万万没想到，半路杀出个程咬金，Russ Cox 义无反顾地推出了 Go modules，这瞬间导致一石激起千层浪，让社区炸了锅。大家一致认为 Go team 实在是太霸道、太独裁了，连个招呼都不打一声。我记得当时有很多人在网上跟 Russ Cox 口水战，各种依赖管理解决方案的专家都冒出来发表意见，讨论范围甚至一度超出了 Go 语言的圈子触及到了其他语言的领域。\n\n当然，最后，推成功了，Go modules 已经进入官方工具链中，与 Go 深深结合，以前常说的 GOPATH 终将会失去它原有的作用，而且它还提供了 GOPROXY 间接解决了国内访问外网的问题。\n\n#### 了解 Russ Cox\n\n在上文中提到的 Russ Cox 是谁呢，他是 Go 这个项目目前代码提交量最多的人，甚至是第二名的两倍还要多（从 2019 年 09 月 30 日前来看）。\n\nRuss Cox 还是 Go 现在的掌舵人（大家应该知道之前 Go 的掌舵人是 Rob Pike，但是听说由于他本人不喜欢特朗普执政所以离开了美国，然后他岁数也挺大的了，所以也正在逐渐交权，不过现在还是在参与 Go 的发展）。\n\nRuss Cox 的个人能力相当强，看问题的角度也很独特，这也就是为什么他刚一提出 Go modules 的概念就能引起那么大范围的响应。虽然是被强推的，但事实也证明当下的 Go modules 表现得确实很优秀，所以这表明一定程度上的 “独裁” 还是可以接受的，至少可以保证一个项目能更加专一地朝着一个方向发展。\n\n#### 初始化行为\n\n在前面我们已经了解到 Go 依赖包管理的历史情况，接下来我们将正式的进入使用，首先你需要有一个你喜欢的目录，例如：`$ mkdir ~/go-application && cd ~/go-application`，然后执行如下命令：\n\n```\n$ mkdir go-gin-example && cd go-gin-example\n\n$ go env -w GO111MODULE=on\n\n$ go env -w GOPROXY=https://goproxy.cn,direct\n\n$ go mod init github.com/EDDYCJY/go-gin-example\ngo: creating new go.mod: module github.com/EDDYCJY/go-gin-example\n\n$ ls\ngo.mod\n```\n\n- `mkdir xxx && cd xxx`：创建并切换到项目目录里去。\n- `go env -w GO111MODULE=on`：打开 Go modules 开关（目前在 Go1.13 中默认值为 `auto`）。\n- `go env -w GOPROXY=...`：设置 GOPROXY 代理，这里主要涉及到两个值，第一个是 `https://goproxy.cn`，它是由七牛云背书的一个强大稳定的 Go 模块代理，可以有效地解决你的外网问题；第二个是 `direct`，它是一个特殊的 fallback 选项，它的作用是用于指示 Go 在拉取模块时遇到错误会回源到模块版本的源地址去抓取（比如 GitHub 等）。\n- `go mod init [MODULE_PATH]`：初始化 Go modules，它将会生成 go.mod 文件，需要注意的是 `MODULE_PATH` 填写的是模块引入路径，你可以根据自己的情况修改路径。\n\n在执行了上述步骤后，初始化工作已完成，我们打开 `go.mod` 文件看看，如下：\n\n```\nmodule github.com/EDDYCJY/go-gin-example\n\ngo 1.13\n```\n\n默认的 `go.mod` 文件里主要是两块内容，一个是当前的模块路径和预期的 Go 语言版本。\n\n#### 基础使用\n\n- 用 `go get` 拉取新的依赖\n  - 拉取最新的版本(优先择取 tag)：`go get golang.org/x/text@latest`\n  - 拉取 `master` 分支的最新 commit：`go get golang.org/x/text@master`\n  - 拉取 tag 为 v0.3.2 的 commit：`go get golang.org/x/text@v0.3.2`\n  - 拉取 hash 为 342b231 的 commit，最终会被转换为 v0.3.2：`go get golang.org/x/text@342b2e`\n  - 用 `go get -u` 更新现有的依赖\n  - 用 `go mod download` 下载 go.mod 文件中指明的所有依赖\n  - 用 `go mod tidy` 整理现有的依赖\n  - 用 `go mod graph` 查看现有的依赖结构\n  - 用 `go mod init` 生成 go.mod 文件 (Go 1.13 中唯一一个可以生成 go.mod 文件的子命令)\n- 用 `go mod edit` 编辑 go.mod 文件\n- 用 `go mod vendor` 导出现有的所有依赖 (事实上 Go modules 正在淡化 Vendor 的概念)\n- 用 `go mod verify` 校验一个模块是否被篡改过\n\n这一小节主要是针对 Go modules 的基础使用讲解，还没具体的使用，是希望你能够留个印象，因为在后面章节会不断夹杂 Go modules 的知识点。\n\n注：建议阅读官方文档 [wiki/Modules](https://github.com/golang/go/wiki/Modules)。\n\n## 开始 Gin 之旅\n\n### 是什么\n\n> Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance -- up to 40 times faster. If you need smashing performance, get yourself some Gin.\n\nGin 是用 Go 开发的一个微框架，类似 Martinier 的 API，重点是小巧、易用、性能好很多，也因为 [httprouter](https://github.com/julienschmidt/httprouter) 的性能提高了 40 倍。\n\n### 安装\n\n我们回到刚刚创建的 `go-gin-example` 目录下，在命令行下执行如下命令：\n\n```sh\n$ go get -u github.com/gin-gonic/gin\ngo: downloading golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223\ngo: extracting golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223\ngo: finding github.com/gin-contrib/sse v0.1.0\ngo: finding github.com/ugorji/go v1.1.7\ngo: finding gopkg.in/yaml.v2 v2.2.3\ngo: finding golang.org/x/sys latest\ngo: finding github.com/mattn/go-isatty v0.0.9\ngo: finding github.com/modern-go/concurrent latest\n...\n```\n\n#### go.sum\n\n这时候你再检查一下该目录下，会发现多个了个 `go.sum` 文件，如下：\n\n```\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW...\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW...\ngithub.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g=\ngithub.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2...\ngithub.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO...\n...\n```\n\n`go.sum` 文件详细罗列了当前项目直接或间接依赖的所有模块版本，并写明了那些模块版本的 SHA-256 哈希值以备 Go 在今后的操作中保证项目所依赖的那些模块版本不会被篡改。\n\n#### go.mod\n\n既然我们下载了依赖包，`go.mod` 文件会不会有所改变呢，我们再去看看，如下：\n\n```\nmodule github.com/EDDYCJY/go-gin-example\n\ngo 1.13\n\nrequire (\n        github.com/gin-contrib/sse v0.1.0 // indirect\n        github.com/gin-gonic/gin v1.4.0 // indirect\n        github.com/golang/protobuf v1.3.2 // indirect\n        github.com/json-iterator/go v1.1.7 // indirect\n        github.com/mattn/go-isatty v0.0.9 // indirect\n        github.com/ugorji/go v1.1.7 // indirect\n        golang.org/x/sys v0.0.0-20190927073244-c990c680b611 // indirect\n        gopkg.in/yaml.v2 v2.2.3 // indirect\n)\n```\n\n确确实实发生了改变，那多出来的东西又是什么呢，`go.mod` 文件又保存了什么信息呢，实际上 `go.mod` 文件是启用了 Go modules 的项目所必须的最重要的文件，因为它描述了当前项目（也就是当前模块）的元信息，每一行都以一个动词开头，目前有以下 5 个动词:\n\n- module：用于定义当前项目的模块路径。\n- go：用于设置预期的 Go 版本。\n- require：用于设置一个特定的模块版本。\n- exclude：用于从使用中排除一个特定的模块版本。\n- replace：用于将一个模块版本替换为另外一个模块版本。\n\n你可能还会疑惑 `indirect` 是什么东西，`indirect` 的意思是传递依赖，也就是非直接依赖。\n\n### 测试\n\n编写一个`test.go`文件\n\n```go\npackage main\n\nimport \"github.com/gin-gonic/gin\"\n\nfunc main() {\n  r := gin.Default()\n  r.GET(\"/ping\", func(c *gin.Context) {\n    c.JSON(200, gin.H{\n      \"message\": \"pong\",\n    })\n  })\n  r.Run() // listen and serve on 0.0.0.0:8080\n}\n```\n\n执行`test.go`\n\n```sh\n$ go run test.go\n...\n[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)\n[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default\n[GIN-debug] Listening and serving HTTP on :8080\n```\n\n访问 `$HOST:8080/ping`，若返回`{\"message\":\"pong\"}`则正确\n\n```sh\ncurl 127.0.0.1:8080/ping\n```\n\n至此，我们的环境安装和初步运行都基本完成了。\n\n## 再想一想\n\n刚刚在执行了命令 `$ go get -u github.com/gin-gonic/gin` 后，我们查看了 `go.mod` 文件，如下：\n\n```\n...\nrequire (\n        github.com/gin-contrib/sse v0.1.0 // indirect\n        github.com/gin-gonic/gin v1.4.0 // indirect\n        ...\n)\n```\n\n你会发现 `go.mod` 里的 `github.com/gin-gonic/gin` 是 `indirect` 模式，这显然不对啊，因为我们的应用程序已经实际的编写了 gin server 代码了，我就想把它调对，怎么办呢，在应用根目录下执行如下命令：\n\n```\n$ go mod tidy\n```\n\n该命令主要的作用是整理现有的依赖，非常的常用，执行后 `go.mod` 文件内容为：\n\n```\n...\nrequire (\n        github.com/gin-contrib/sse v0.1.0 // indirect\n        github.com/gin-gonic/gin v1.4.0\n        ...\n)\n```\n\n可以看到 `github.com/gin-gonic/gin` 已经变成了直接依赖，调整完毕。\n\n## 参考\n\n### 本系列示例代码\n\n- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)\n\n### 相关文档\n\n- [Gin](https://github.com/gin-gonic/gin)\n- [Gin Web Framework](https://gin-gonic.github.io/gin/)\n- [干货满满的 Go Modules 和 goproxy.cn](https://book.eddycjy.com/golang/talk/goproxy-cn.html)\n\n## 关于\n\n### 修改记录\n\n- 第一版：2018 年 02 月 16 日发布文章\n- 第二版：2019 年 10 月 01 日修改文章\n\n## ？\n\n如果有任何疑问或错误，欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见，如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进。\n\n### 我的公众号\n\n![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)"
  },
  {
    "path": "content/posts/go/gin/2018-02-11-api-01.md",
    "content": "---\n\ntitle:      \"「连载二」Gin搭建Blog API's （一）\"\ndate:       2018-02-11 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n---\n\n## 思考\n\n首先，在一个初始项目开始前，大家都要思考一下\n\n- 程序的文本配置写在代码中，好吗？\n\n- API 的错误码硬编码在程序中，合适吗？\n\n- db 句柄谁都去`Open`，没有统一管理，好吗？\n\n- 获取分页等公共参数，谁都自己写一套逻辑，好吗？\n\n显然在较正规的项目中，这些问题的答案都是**不可以**，为了解决这些问题，我们挑选一款读写配置文件的库，目前比较火的有 [viper](https://github.com/spf13/viper)，有兴趣你未来可以简单了解一下，没兴趣的话等以后接触到再说。\n\n但是本系列选用 [go-ini/ini](https://github.com/go-ini/ini) ，它的 [中文文档](https://ini.unknwon.io/)。大家是必须需要要简单阅读它的文档，再接着完成后面的内容。\n\n## 本文目标\n\n- 编写一个简单的 API 错误码包。\n- 完成一个 Demo 示例。\n- 讲解 Demo 所涉及的知识点。\n\n## 介绍和初始化项目\n\n### 初始化项目目录\n\n在前一章节中，我们初始化了一个 `go-gin-example` 项目，接下来我们需要继续新增如下目录结构：\n\n```\ngo-gin-example/\n├── conf\n├── middleware\n├── models\n├── pkg\n├── routers\n└── runtime\n```\n\n- conf：用于存储配置文件\n- middleware：应用中间件\n- models：应用数据库模型\n- pkg：第三方包\n- routers 路由逻辑处理\n- runtime：应用运行时数据\n\n### 添加 Go Modules Replace\n\n打开 `go.mod` 文件，新增 `replace` 配置项，如下：\n\n```\nmodule github.com/EDDYCJY/go-gin-example\n\ngo 1.13\n\nrequire (...)\n\nreplace (\n\t\tgithub.com/EDDYCJY/go-gin-example/pkg/setting => ~/go-application/go-gin-example/pkg/setting\n\t\tgithub.com/EDDYCJY/go-gin-example/conf    \t  => ~/go-application/go-gin-example/pkg/conf\n\t\tgithub.com/EDDYCJY/go-gin-example/middleware  => ~/go-application/go-gin-example/middleware\n\t\tgithub.com/EDDYCJY/go-gin-example/models \t  => ~/go-application/go-gin-example/models\n\t\tgithub.com/EDDYCJY/go-gin-example/routers \t  => ~/go-application/go-gin-example/routers\n)\n```\n\n可能你会不理解为什么要特意跑来加 `replace` 配置项，首先你要看到我们使用的是完整的外部模块引用路径（`github.com/EDDYCJY/go-gin-example/xxx`），而这个模块还没推送到远程，是没有办法下载下来的，因此需要用 `replace` 将其指定读取本地的模块路径，这样子就可以解决本地模块读取的问题。\n\n**注：后续每新增一个本地应用目录，你都需要主动去 go.mod 文件里新增一条 replace（我不会提醒你），如果你漏了，那么编译时会出现报错，找不到那个模块。**\n\n### 初始项目数据库\n\n新建 `blog` 数据库，编码为`utf8_general_ci`，在 `blog` 数据库下，新建以下表\n\n**1、 标签表**\n\n```sql\nCREATE TABLE `blog_tag` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `name` varchar(100) DEFAULT '' COMMENT '标签名称',\n  `created_on` int(10) unsigned DEFAULT '0' COMMENT '创建时间',\n  `created_by` varchar(100) DEFAULT '' COMMENT '创建人',\n  `modified_on` int(10) unsigned DEFAULT '0' COMMENT '修改时间',\n  `modified_by` varchar(100) DEFAULT '' COMMENT '修改人',\n  `deleted_on` int(10) unsigned DEFAULT '0',\n  `state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0为禁用、1为启用',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文章标签管理';\n```\n\n**2、 文章表**\n\n```sql\nCREATE TABLE `blog_article` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `tag_id` int(10) unsigned DEFAULT '0' COMMENT '标签ID',\n  `title` varchar(100) DEFAULT '' COMMENT '文章标题',\n  `desc` varchar(255) DEFAULT '' COMMENT '简述',\n  `content` text,\n  `created_on` int(11) DEFAULT NULL,\n  `created_by` varchar(100) DEFAULT '' COMMENT '创建人',\n  `modified_on` int(10) unsigned DEFAULT '0' COMMENT '修改时间',\n  `modified_by` varchar(255) DEFAULT '' COMMENT '修改人',\n  `deleted_on` int(10) unsigned DEFAULT '0',\n  `state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0为禁用1为启用',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文章管理';\n```\n\n**3、 认证表**\n\n```sql\nCREATE TABLE `blog_auth` (\n  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n  `username` varchar(50) DEFAULT '' COMMENT '账号',\n  `password` varchar(50) DEFAULT '' COMMENT '密码',\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\n\nINSERT INTO `blog`.`blog_auth` (`id`, `username`, `password`) VALUES (null, 'test', 'test123456');\n\n```\n\n## 编写项目配置包\n\n在 `go-gin-example` 应用目录下，拉取 `go-ini/ini` 的依赖包，如下：\n\n```\n$ go get -u github.com/go-ini/ini\ngo: finding github.com/go-ini/ini v1.48.0\ngo: downloading github.com/go-ini/ini v1.48.0\ngo: extracting github.com/go-ini/ini v1.48.0\n```\n\n接下来我们需要编写基础的应用配置文件，在 `go-gin-example` 的`conf`目录下新建`app.ini`文件，写入内容：\n\n```ini\n#debug or release\nRUN_MODE = debug\n\n[app]\nPAGE_SIZE = 10\nJWT_SECRET = 23347$040412\n\n[server]\nHTTP_PORT = 8000\nREAD_TIMEOUT = 60\nWRITE_TIMEOUT = 60\n\n[database]\nTYPE = mysql\nUSER = 数据库账号\nPASSWORD = 数据库密码\n#127.0.0.1:3306\nHOST = 数据库IP:数据库端口号\nNAME = blog\nTABLE_PREFIX = blog_\n```\n\n建立调用配置的`setting`模块，在`go-gin-example`的`pkg`目录下新建`setting`目录（注意新增 replace 配置），新建 `setting.go` 文件，写入内容：\n\n```go\npackage setting\n\nimport (\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/go-ini/ini\"\n)\n\nvar (\n\tCfg *ini.File\n\n\tRunMode string\n\n\tHTTPPort int\n\tReadTimeout time.Duration\n\tWriteTimeout time.Duration\n\n\tPageSize int\n\tJwtSecret string\n)\n\nfunc init() {\n\tvar err error\n\tCfg, err = ini.Load(\"conf/app.ini\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Fail to parse 'conf/app.ini': %v\", err)\n\t}\n\n\tLoadBase()\n\tLoadServer()\n\tLoadApp()\n}\n\nfunc LoadBase() {\n\tRunMode = Cfg.Section(\"\").Key(\"RUN_MODE\").MustString(\"debug\")\n}\n\nfunc LoadServer() {\n\tsec, err := Cfg.GetSection(\"server\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Fail to get section 'server': %v\", err)\n\t}\n\n\tHTTPPort = sec.Key(\"HTTP_PORT\").MustInt(8000)\n\tReadTimeout = time.Duration(sec.Key(\"READ_TIMEOUT\").MustInt(60)) * time.Second\n\tWriteTimeout =  time.Duration(sec.Key(\"WRITE_TIMEOUT\").MustInt(60)) * time.Second\n}\n\nfunc LoadApp() {\n\tsec, err := Cfg.GetSection(\"app\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Fail to get section 'app': %v\", err)\n\t}\n\n\tJwtSecret = sec.Key(\"JWT_SECRET\").MustString(\"!@)*#)!@U#@*!@!)\")\n\tPageSize = sec.Key(\"PAGE_SIZE\").MustInt(10)\n}\n```\n\n当前的目录结构：\n\n```\ngo-gin-example\n├── conf\n│   └── app.ini\n├── go.mod\n├── go.sum\n├── middleware\n├── models\n├── pkg\n│   └── setting.go\n├── routers\n└── runtime\n```\n\n## 编写 API 错误码包\n\n建立错误码的`e`模块，在`go-gin-example`的`pkg`目录下新建`e`目录（注意新增 replace 配置），新建`code.go`和`msg.go`文件，写入内容：\n\n**1、 code.go：**\n\n```go\npackage e\n\nconst (\n\tSUCCESS = 200\n\tERROR = 500\n\tINVALID_PARAMS = 400\n\n\tERROR_EXIST_TAG = 10001\n\tERROR_NOT_EXIST_TAG = 10002\n\tERROR_NOT_EXIST_ARTICLE = 10003\n\n\tERROR_AUTH_CHECK_TOKEN_FAIL = 20001\n\tERROR_AUTH_CHECK_TOKEN_TIMEOUT = 20002\n\tERROR_AUTH_TOKEN = 20003\n\tERROR_AUTH = 20004\n)\n```\n\n**2、 msg.go：**\n\n```go\npackage e\n\nvar MsgFlags = map[int]string {\n\tSUCCESS : \"ok\",\n\tERROR : \"fail\",\n\tINVALID_PARAMS : \"请求参数错误\",\n\tERROR_EXIST_TAG : \"已存在该标签名称\",\n\tERROR_NOT_EXIST_TAG : \"该标签不存在\",\n\tERROR_NOT_EXIST_ARTICLE : \"该文章不存在\",\n\tERROR_AUTH_CHECK_TOKEN_FAIL : \"Token鉴权失败\",\n\tERROR_AUTH_CHECK_TOKEN_TIMEOUT : \"Token已超时\",\n\tERROR_AUTH_TOKEN : \"Token生成失败\",\n\tERROR_AUTH : \"Token错误\",\n}\n\nfunc GetMsg(code int) string {\n\tmsg, ok := MsgFlags[code]\n\tif ok {\n\t\treturn msg\n\t}\n\n\treturn MsgFlags[ERROR]\n}\n```\n\n## 编写工具包\n\n在`go-gin-example`的`pkg`目录下新建`util`目录（注意新增 replace 配置），并拉取`com`的依赖包，如下：\n\n```\n$ go get -u github.com/unknwon/com\n```\n\n### 编写分页页码的获取方法\n\n在`util`目录下新建`pagination.go`，写入内容：\n\n```go\npackage util\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/unknwon/com\"\n\n\t\"github.com/EDDYCJY/go-gin-example/pkg/setting\"\n)\n\nfunc GetPage(c *gin.Context) int {\n\tresult := 0\n\tpage, _ := com.StrTo(c.Query(\"page\")).Int()\n    if page > 0 {\n        result = (page - 1) * setting.PageSize\n    }\n\n    return result\n}\n```\n\n## 编写 models init\n\n拉取`gorm`的依赖包，如下：\n\n```\n$ go get -u github.com/jinzhu/gorm\n```\n\n拉取`mysql`驱动的依赖包，如下：\n\n```\n$ go get -u github.com/go-sql-driver/mysql\n```\n\n完成后，在`go-gin-example`的`models`目录下新建`models.go`，用于`models`的初始化使用\n\n```go\npackage models\n\nimport (\n\t\"log\"\n\t\"fmt\"\n\n\t\"github.com/jinzhu/gorm\"\n\t_ \"github.com/jinzhu/gorm/dialects/mysql\"\n\n\t\"github.com/EDDYCJY/go-gin-example/pkg/setting\"\n)\n\nvar db *gorm.DB\n\ntype Model struct {\n\tID int `gorm:\"primary_key\" json:\"id\"`\n\tCreatedOn int `json:\"created_on\"`\n\tModifiedOn int `json:\"modified_on\"`\n}\n\nfunc init() {\n\tvar (\n\t\terr error\n\t\tdbType, dbName, user, password, host, tablePrefix string\n\t)\n\n\tsec, err := setting.Cfg.GetSection(\"database\")\n\tif err != nil {\n\t\tlog.Fatal(2, \"Fail to get section 'database': %v\", err)\n\t}\n\n\tdbType = sec.Key(\"TYPE\").String()\n\tdbName = sec.Key(\"NAME\").String()\n\tuser = sec.Key(\"USER\").String()\n\tpassword = sec.Key(\"PASSWORD\").String()\n\thost = sec.Key(\"HOST\").String()\n\ttablePrefix = sec.Key(\"TABLE_PREFIX\").String()\n\n\tdb, err = gorm.Open(dbType, fmt.Sprintf(\"%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local\",\n\t\tuser,\n\t\tpassword,\n\t\thost,\n\t\tdbName))\n\n\tif err != nil {\n\t\tlog.Println(err)\n\t}\n\n\tgorm.DefaultTableNameHandler = func (db *gorm.DB, defaultTableName string) string  {\n\t    return tablePrefix + defaultTableName;\n\t}\n\n\tdb.SingularTable(true)\n\tdb.LogMode(true)\n\tdb.DB().SetMaxIdleConns(10)\n\tdb.DB().SetMaxOpenConns(100)\n}\n\nfunc CloseDB() {\n\tdefer db.Close()\n}\n```\n\n## 编写项目启动、路由文件\n\n最基础的准备工作完成啦，让我们开始编写 Demo 吧！\n\n### 编写 Demo\n\n在`go-gin-example`下建立`main.go`作为启动文件（也就是`main`包），我们先写个**Demo**，帮助大家理解，写入文件内容：\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"net/http\"\n\n    \"github.com/gin-gonic/gin\"\n\n    \"github.com/EDDYCJY/go-gin-example/pkg/setting\"\n)\n\nfunc main() {\n    router := gin.Default()\n    router.GET(\"/test\", func(c *gin.Context) {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"message\": \"test\",\n\t\t})\n\t})\n\n\ts := &http.Server{\n\t\tAddr:           fmt.Sprintf(\":%d\", setting.HTTPPort),\n\t\tHandler:        router,\n\t\tReadTimeout:    setting.ReadTimeout,\n\t\tWriteTimeout:   setting.WriteTimeout,\n\t\tMaxHeaderBytes: 1 << 20,\n\t}\n\n\ts.ListenAndServe()\n}\n```\n\n执行`go run main.go`，查看命令行是否显示\n\n```\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:\texport GIN_MODE=release\n - using code:\tgin.SetMode(gin.ReleaseMode)\n\n[GIN-debug] GET    /test                     --> main.main.func1 (3 handlers)\n```\n\n在本机执行`curl 127.0.0.1:8000/test`，检查是否返回`{\"message\":\"test\"}`。\n\n### 知识点\n\n**那么，我们来延伸一下 Demo 所涉及的知识点！**\n\n##### 标准库\n\n- [fmt](https://golang.org/pkg/fmt/)：实现了类似 C 语言 printf 和 scanf 的格式化 I/O。格式化动作（'verb'）源自 C 语言但更简单\n- [net/http](https://golang.org/pkg/net/http/)：提供了 HTTP 客户端和服务端的实现\n\n##### **Gin**\n\n- [gin.Default()](https://gowalker.org/github.com/gin-gonic/gin#Default)：返回 Gin 的`type Engine struct{...}`，里面包含`RouterGroup`，相当于创建一个路由`Handlers`，可以后期绑定各类的路由规则和函数、中间件等\n- [router.GET(...){...}](https://gowalker.org/github.com/gin-gonic/gin#IRoutes)：创建不同的 HTTP 方法绑定到`Handlers`中，也支持 POST、PUT、DELETE、PATCH、OPTIONS、HEAD 等常用的 Restful 方法\n- [gin.H{...}](https://gowalker.org/github.com/gin-gonic/gin#H)：就是一个`map[string]interface{}`\n- [gin.Context](https://gowalker.org/github.com/gin-gonic/gin#Context)：`Context`是`gin`中的上下文，它允许我们在中间件之间传递变量、管理流、验证 JSON 请求、响应 JSON 请求等，在`gin`中包含大量`Context`的方法，例如我们常用的`DefaultQuery`、`Query`、`DefaultPostForm`、`PostForm`等等\n\n##### &http.Server 和 ListenAndServe？\n\n1、http.Server：\n\n```go\ntype Server struct {\n    Addr    string\n    Handler Handler\n    TLSConfig *tls.Config\n    ReadTimeout time.Duration\n    ReadHeaderTimeout time.Duration\n    WriteTimeout time.Duration\n    IdleTimeout time.Duration\n    MaxHeaderBytes int\n    ConnState func(net.Conn, ConnState)\n    ErrorLog *log.Logger\n}\n```\n\n- Addr：监听的 TCP 地址，格式为`:8000`\n- Handler：http 句柄，实质为`ServeHTTP`，用于处理程序响应 HTTP 请求\n- TLSConfig：安全传输层协议（TLS）的配置\n- ReadTimeout：允许读取的最大时间\n- ReadHeaderTimeout：允许读取请求头的最大时间\n- WriteTimeout：允许写入的最大时间\n- IdleTimeout：等待的最大时间\n- MaxHeaderBytes：请求头的最大字节数\n- ConnState：指定一个可选的回调函数，当客户端连接发生变化时调用\n- ErrorLog：指定一个可选的日志记录器，用于接收程序的意外行为和底层系统错误；如果未设置或为`nil`则默认以日志包的标准日志记录器完成（也就是在控制台输出）\n\n2、 ListenAndServe：\n\n```go\nfunc (srv *Server) ListenAndServe() error {\n    addr := srv.Addr\n    if addr == \"\" {\n        addr = \":http\"\n    }\n    ln, err := net.Listen(\"tcp\", addr)\n    if err != nil {\n        return err\n    }\n    return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})\n}\n```\n\n开始监听服务，监听 TCP 网络地址，Addr 和调用应用程序处理连接上的请求。\n\n我们在源码中看到`Addr`是调用我们在`&http.Server`中设置的参数，因此我们在设置时要用`&`，我们要改变参数的值，因为我们`ListenAndServe`和其他一些方法需要用到`&http.Server`中的参数，他们是相互影响的。\n\n3、 `http.ListenAndServe`和 [连载一](https://segmentfault.com/a/1190000013297625#articleHeader5) 的`r.Run()`有区别吗？\n\n我们看看`r.Run`的实现：\n\n```go\nfunc (engine *Engine) Run(addr ...string) (err error) {\n    defer func() { debugPrintError(err) }()\n\n    address := resolveAddress(addr)\n    debugPrint(\"Listening and serving HTTP on %s\\n\", address)\n    err = http.ListenAndServe(address, engine)\n    return\n}\n```\n\n通过分析源码，得知**本质上没有区别**，同时也得知了启动`gin`时的监听 debug 信息在这里输出。\n\n4、 为什么 Demo 里会有`WARNING`？\n\n首先我们可以看下`Default()`的实现\n\n```go\n// Default returns an Engine instance with the Logger and Recovery middleware already attached.\nfunc Default() *Engine {\n\tdebugPrintWARNINGDefault()\n\tengine := New()\n\tengine.Use(Logger(), Recovery())\n\treturn engine\n}\n```\n\n大家可以看到默认情况下，已经附加了日志、恢复中间件的引擎实例。并且在开头调用了`debugPrintWARNINGDefault()`，而它的实现就是输出该行日志\n\n```go\nfunc debugPrintWARNINGDefault() {\n\tdebugPrint(`[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n`)\n}\n```\n\n而另外一个`Running in \"debug\" mode. Switch to \"release\" mode in production.`，是运行模式原因，并不难理解，已在配置文件的管控下 :-)，运维人员随时就可以修改它的配置。\n\n5、 Demo 的`router.GET`等路由规则可以不写在`main`包中吗？\n\n我们发现`router.GET`等路由规则，在 Demo 中被编写在了`main`包中，感觉很奇怪，我们去抽离这部分逻辑！\n\n在`go-gin-example`下`routers`目录新建`router.go`文件，写入内容：\n\n```go\npackage routers\n\nimport (\n    \"github.com/gin-gonic/gin\"\n\n    \"github.com/EDDYCJY/go-gin-example/pkg/setting\"\n)\n\nfunc InitRouter() *gin.Engine {\n    r := gin.New()\n\n    r.Use(gin.Logger())\n\n    r.Use(gin.Recovery())\n\n    gin.SetMode(setting.RunMode)\n\n    r.GET(\"/test\", func(c *gin.Context) {\n        c.JSON(200, gin.H{\n            \"message\": \"test\",\n        })\n    })\n\n    return r\n}\n```\n\n修改`main.go`的文件内容：\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/EDDYCJY/go-gin-example/routers\"\n\t\"github.com/EDDYCJY/go-gin-example/pkg/setting\"\n)\n\nfunc main() {\n\trouter := routers.InitRouter()\n\n\ts := &http.Server{\n\t\tAddr:           fmt.Sprintf(\":%d\", setting.HTTPPort),\n\t\tHandler:        router,\n\t\tReadTimeout:    setting.ReadTimeout,\n\t\tWriteTimeout:   setting.WriteTimeout,\n\t\tMaxHeaderBytes: 1 << 20,\n\t}\n\n\ts.ListenAndServe()\n}\n```\n\n当前目录结构：\n\n```\ngo-gin-example/\n├── conf\n│   └── app.ini\n├── main.go\n├── middleware\n├── models\n│   └── models.go\n├── pkg\n│   ├── e\n│   │   ├── code.go\n│   │   └── msg.go\n│   ├── setting\n│   │   └── setting.go\n│   └── util\n│       └── pagination.go\n├── routers\n│   └── router.go\n├── runtime\n```\n\n重启服务，执行 `curl 127.0.0.1:8000/test`查看是否正确返回。\n\n下一节，我们将以我们的 Demo 为起点进行修改，开始编码！\n\n## 参考\n\n### 本系列示例代码\n\n- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)\n\n## 关于\n\n### 修改记录\n\n- 第一版：2018 年 02 月 16 日发布文章\n- 第二版：2019 年 10 月 01 日修改文章\n\n## ？\n\n如果有任何疑问或错误，欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见，如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进。\n\n### 我的公众号\n\n![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)"
  },
  {
    "path": "content/posts/go/gin/2018-02-12-api-02.md",
    "content": "---\n\ntitle:      \"「连载三」Gin搭建Blog API's （二）\"\ndate:       2018-02-12 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n---\n\n## 涉及知识点\n\n- [Gin](https://github.com/gin-gonic/gin)：Golang 的一个微框架，性能极佳。\n- [beego-validation](https://github.com/astaxie/beego/tree/master/validation)：本节采用的 beego 的表单验证库，[中文文档](https://beego.me/docs/mvc/controller/validation.md)。\n- [gorm](https://github.com/jinzhu/gorm)，对开发人员友好的 ORM 框架，[英文文档](http://gorm.io/docs/)\n- [com](https://github.com/Unknwon/com)，一个小而美的工具包。\n\n## 本文目标\n\n- 完成博客的标签类接口定义和编写\n\n## 定义接口\n\n本节正是编写标签的逻辑，我们想一想，一般接口为增删改查是基础的，那么我们定义一下接口吧！\n\n- 获取标签列表：GET(\"/tags\")\n- 新建标签：POST(\"/tags\")\n- 更新指定标签：PUT(\"/tags/:id\")\n- 删除指定标签：DELETE(\"/tags/:id\")\n\n---\n\n## 编写路由空壳\n\n开始编写路由文件逻辑，在`routers`下新建`api`目录，我们当前是第一个 API 大版本，因此在`api`下新建`v1`目录，再新建`tag.go`文件，写入内容：\n\n```go\npackage v1\n\nimport (\n    \"github.com/gin-gonic/gin\"\n)\n\n//获取多个文章标签\nfunc GetTags(c *gin.Context) {\n}\n\n//新增文章标签\nfunc AddTag(c *gin.Context) {\n}\n\n//修改文章标签\nfunc EditTag(c *gin.Context) {\n}\n\n//删除文章标签\nfunc DeleteTag(c *gin.Context) {\n}\n```\n\n## 注册路由\n\n我们打开`routers`下的`router.go`文件，修改文件内容为：\n\n```go\npackage routers\n\nimport (\n    \"github.com/gin-gonic/gin\"\n\n    \"gin-blog/routers/api/v1\"\n    \"gin-blog/pkg/setting\"\n)\n\nfunc InitRouter() *gin.Engine {\n    r := gin.New()\n\n    r.Use(gin.Logger())\n\n    r.Use(gin.Recovery())\n\n    gin.SetMode(setting.RunMode)\n\n    apiv1 := r.Group(\"/api/v1\")\n    {\n        //获取标签列表\n        apiv1.GET(\"/tags\", v1.GetTags)\n        //新建标签\n        apiv1.POST(\"/tags\", v1.AddTag)\n        //更新指定标签\n        apiv1.PUT(\"/tags/:id\", v1.EditTag)\n        //删除指定标签\n        apiv1.DELETE(\"/tags/:id\", v1.DeleteTag)\n    }\n\n    return r\n}\n```\n\n当前目录结构：\n\n```\ngin-blog/\n├── conf\n│   └── app.ini\n├── main.go\n├── middleware\n├── models\n│   └── models.go\n├── pkg\n│   ├── e\n│   │   ├── code.go\n│   │   └── msg.go\n│   ├── setting\n│   │   └── setting.go\n│   └── util\n│       └── pagination.go\n├── routers\n│   ├── api\n│   │   └── v1\n│   │       └── tag.go\n│   └── router.go\n├── runtime\n```\n\n## 检验路由是否注册成功\n\n回到命令行，执行`go run main.go`，检查路由规则是否注册成功。\n\n```\n$ go run main.go\n[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:   export GIN_MODE=release\n - using code:  gin.SetMode(gin.ReleaseMode)\n\n[GIN-debug] GET    /api/v1/tags              --> gin-blog/routers/api/v1.GetTags (3 handlers)\n[GIN-debug] POST   /api/v1/tags              --> gin-blog/routers/api/v1.AddTag (3 handlers)\n[GIN-debug] PUT    /api/v1/tags/:id          --> gin-blog/routers/api/v1.EditTag (3 handlers)\n[GIN-debug] DELETE /api/v1/tags/:id          --> gin-blog/routers/api/v1.DeleteTag (3 handlers)\n```\n\n运行成功，那么我们愉快的**开始编写我们的接口**吧！\n\n## 下载依赖包\n\n---\n\n首先我们要拉取`validation`的依赖包，在后面的接口里会使用到表单验证\n\n```\n$ go get -u github.com/astaxie/beego/validation\n```\n\n## 编写标签列表的 models 逻辑\n\n创建`models`目录下的`tag.go`，写入文件内容：\n\n```go\npackage models\n\ntype Tag struct {\n    Model\n\n    Name string `json:\"name\"`\n    CreatedBy string `json:\"created_by\"`\n    ModifiedBy string `json:\"modified_by\"`\n    State int `json:\"state\"`\n}\n\nfunc GetTags(pageNum int, pageSize int, maps interface {}) (tags []Tag) {\n    db.Where(maps).Offset(pageNum).Limit(pageSize).Find(&tags)\n\n    return\n}\n\nfunc GetTagTotal(maps interface {}) (count int){\n    db.Model(&Tag{}).Where(maps).Count(&count)\n\n    return\n}\n```\n\n1. 我们创建了一个`Tag struct{}`，用于`Gorm`的使用。并给予了附属属性`json`，这样子在`c.JSON`的时候就会自动转换格式，非常的便利\n\n2. 可能会有的初学者看到`return`，而后面没有跟着变量，会不理解；其实你可以看到在函数末端，我们已经显示声明了返回值，这个变量在函数体内也可以直接使用，因为他在一开始就被声明了\n\n3. 有人会疑惑`db`是哪里来的；因为在同个`models`包下，因此`db *gorm.DB`是可以直接使用的\n\n## 编写标签列表的路由逻辑\n\n打开`routers`目录下 v1 版本的`tag.go`，第一我们先编写**获取标签列表的接口**\n\n修改文件内容：\n\n```go\npackage v1\n\nimport (\n    \"net/http\"\n\n    \"github.com/gin-gonic/gin\"\n    //\"github.com/astaxie/beego/validation\"\n    \"github.com/Unknwon/com\"\n\n    \"gin-blog/pkg/e\"\n    \"gin-blog/models\"\n    \"gin-blog/pkg/util\"\n    \"gin-blog/pkg/setting\"\n)\n\n//获取多个文章标签\nfunc GetTags(c *gin.Context) {\n    name := c.Query(\"name\")\n\n    maps := make(map[string]interface{})\n    data := make(map[string]interface{})\n\n    if name != \"\" {\n        maps[\"name\"] = name\n    }\n\n    var state int = -1\n    if arg := c.Query(\"state\"); arg != \"\" {\n        state = com.StrTo(arg).MustInt()\n        maps[\"state\"] = state\n    }\n\n    code := e.SUCCESS\n\n    data[\"lists\"] = models.GetTags(util.GetPage(c), setting.PageSize, maps)\n    data[\"total\"] = models.GetTagTotal(maps)\n\n    c.JSON(http.StatusOK, gin.H{\n        \"code\" : code,\n        \"msg\" : e.GetMsg(code),\n        \"data\" : data,\n    })\n}\n\n//新增文章标签\nfunc AddTag(c *gin.Context) {\n}\n\n//修改文章标签\nfunc EditTag(c *gin.Context) {\n}\n\n//删除文章标签\nfunc DeleteTag(c *gin.Context) {\n}\n```\n\n1. `c.Query`可用于获取`?name=test&state=1`这类 URL 参数，而`c.DefaultQuery`则支持设置一个默认值\n2. `code`变量使用了`e`模块的错误编码，这正是先前规划好的错误码，方便排错和识别记录\n3. `util.GetPage`保证了各接口的`page`处理是一致的\n4. `c *gin.Context`是`Gin`很重要的组成部分，可以理解为上下文，它允许我们在中间件之间传递变量、管理流、验证请求的 JSON 和呈现 JSON 响应\n\n在本机执行`curl 127.0.0.1:8000/api/v1/tags`，正确的返回值为`{\"code\":200,\"data\":{\"lists\":[],\"total\":0},\"msg\":\"ok\"}`，若存在问题请结合 gin 结果进行拍错。\n\n在获取标签列表接口中，我们可以根据`name`、`state`、`page`来筛选查询条件，分页的步长可通过`app.ini`进行配置，以`lists`、`total`的组合返回达到分页效果。\n\n## 编写新增标签的 models 逻辑\n\n接下来我们编写**新增标签**的接口\n\n打开`models`目录下的`tag.go`，修改文件（增加 2 个方法）：\n\n```go\n...\nfunc ExistTagByName(name string) bool {\n    var tag Tag\n    db.Select(\"id\").Where(\"name = ?\", name).First(&tag)\n    if tag.ID > 0 {\n        return true\n    }\n\n    return false\n}\n\nfunc AddTag(name string, state int, createdBy string) bool{\n    db.Create(&Tag {\n        Name : name,\n        State : state,\n        CreatedBy : createdBy,\n    })\n\n    return true\n}\n...\n```\n\n## 编写新增标签的路由逻辑\n\n打开`routers`目录下的`tag.go`，修改文件（变动 AddTag 方法）：\n\n```go\npackage v1\n\nimport (\n    \"log\"\n    \"net/http\"\n\n    \"github.com/gin-gonic/gin\"\n    \"github.com/astaxie/beego/validation\"\n    \"github.com/Unknwon/com\"\n\n    \"gin-blog/pkg/e\"\n    \"gin-blog/models\"\n    \"gin-blog/pkg/util\"\n    \"gin-blog/pkg/setting\"\n)\n\n...\n\n//新增文章标签\nfunc AddTag(c *gin.Context) {\n    name := c.Query(\"name\")\n    state := com.StrTo(c.DefaultQuery(\"state\", \"0\")).MustInt()\n    createdBy := c.Query(\"created_by\")\n\n    valid := validation.Validation{}\n    valid.Required(name, \"name\").Message(\"名称不能为空\")\n    valid.MaxSize(name, 100, \"name\").Message(\"名称最长为100字符\")\n    valid.Required(createdBy, \"created_by\").Message(\"创建人不能为空\")\n    valid.MaxSize(createdBy, 100, \"created_by\").Message(\"创建人最长为100字符\")\n    valid.Range(state, 0, 1, \"state\").Message(\"状态只允许0或1\")\n\n    code := e.INVALID_PARAMS\n    if ! valid.HasErrors() {\n        if ! models.ExistTagByName(name) {\n            code = e.SUCCESS\n            models.AddTag(name, state, createdBy)\n        } else {\n            code = e.ERROR_EXIST_TAG\n        }\n    }\n\n    c.JSON(http.StatusOK, gin.H{\n        \"code\" : code,\n        \"msg\" : e.GetMsg(code),\n        \"data\" : make(map[string]string),\n    })\n}\n...\n```\n\n用`Postman`用 POST 访问`http://127.0.0.1:8000/api/v1/tags?name=1&state=1&created_by=test`，查看`code`是否返回`200`及`blog_tag`表中是否有值，有值则正确。\n\n## 编写 models callbacks\n\n但是这个时候大家会发现，我明明新增了标签，但`created_on`居然没有值，那做修改标签的时候`modified_on`会不会也存在这个问题？\n\n为了解决这个问题，我们需要打开`models`目录下的`tag.go`文件，修改文件内容（修改包引用和增加 2 个方法）：\n\n```go\npackage models\n\nimport (\n    \"time\"\n\n    \"github.com/jinzhu/gorm\"\n)\n\n...\n\nfunc (tag *Tag) BeforeCreate(scope *gorm.Scope) error {\n    scope.SetColumn(\"CreatedOn\", time.Now().Unix())\n\n    return nil\n}\n\nfunc (tag *Tag) BeforeUpdate(scope *gorm.Scope) error {\n    scope.SetColumn(\"ModifiedOn\", time.Now().Unix())\n\n    return nil\n}\n```\n\n重启服务，再在用`Postman`用 POST 访问`http://127.0.0.1:8000/api/v1/tags?name=2&state=1&created_by=test`，发现`created_on`已经有值了！\n\n**在这几段代码中，涉及到知识点：**\n\n这属于`gorm`的`Callbacks`，可以将回调方法定义为模型结构的指针，在创建、更新、查询、删除时将被调用，如果任何回调返回错误，gorm 将停止未来操作并回滚所有更改。\n\n`gorm`所支持的回调方法：\n\n- 创建：BeforeSave、BeforeCreate、AfterCreate、AfterSave\n- 更新：BeforeSave、BeforeUpdate、AfterUpdate、AfterSave\n- 删除：BeforeDelete、AfterDelete\n- 查询：AfterFind\n\n---\n\n## 编写其余接口的路由逻辑\n\n接下来，我们一口气把剩余的两个接口（EditTag、DeleteTag）完成吧\n\n打开`routers`目录下 v1 版本的`tag.go`文件，修改内容：\n\n```go\n...\n\n//修改文章标签\nfunc EditTag(c *gin.Context) {\n    id := com.StrTo(c.Param(\"id\")).MustInt()\n    name := c.Query(\"name\")\n    modifiedBy := c.Query(\"modified_by\")\n\n    valid := validation.Validation{}\n\n    var state int = -1\n    if arg := c.Query(\"state\"); arg != \"\" {\n        state = com.StrTo(arg).MustInt()\n        valid.Range(state, 0, 1, \"state\").Message(\"状态只允许0或1\")\n    }\n\n    valid.Required(id, \"id\").Message(\"ID不能为空\")\n    valid.Required(modifiedBy, \"modified_by\").Message(\"修改人不能为空\")\n    valid.MaxSize(modifiedBy, 100, \"modified_by\").Message(\"修改人最长为100字符\")\n    valid.MaxSize(name, 100, \"name\").Message(\"名称最长为100字符\")\n\n    code := e.INVALID_PARAMS\n    if ! valid.HasErrors() {\n        code = e.SUCCESS\n        if models.ExistTagByID(id) {\n            data := make(map[string]interface{})\n            data[\"modified_by\"] = modifiedBy\n            if name != \"\" {\n                data[\"name\"] = name\n            }\n            if state != -1 {\n                data[\"state\"] = state\n            }\n\n            models.EditTag(id, data)\n        } else {\n            code = e.ERROR_NOT_EXIST_TAG\n        }\n    }\n\n    c.JSON(http.StatusOK, gin.H{\n        \"code\" : code,\n        \"msg\" : e.GetMsg(code),\n        \"data\" : make(map[string]string),\n    })\n}\n\n//删除文章标签\nfunc DeleteTag(c *gin.Context) {\n    id := com.StrTo(c.Param(\"id\")).MustInt()\n\n    valid := validation.Validation{}\n    valid.Min(id, 1, \"id\").Message(\"ID必须大于0\")\n\n    code := e.INVALID_PARAMS\n    if ! valid.HasErrors() {\n        code = e.SUCCESS\n        if models.ExistTagByID(id) {\n            models.DeleteTag(id)\n        } else {\n            code = e.ERROR_NOT_EXIST_TAG\n        }\n    }\n\n    c.JSON(http.StatusOK, gin.H{\n        \"code\" : code,\n        \"msg\" : e.GetMsg(code),\n        \"data\" : make(map[string]string),\n    })\n}\n```\n\n## 编写其余接口的 models 逻辑\n\n打开`models`下的`tag.go`，修改文件内容：\n\n```go\n...\n\nfunc ExistTagByID(id int) bool {\n    var tag Tag\n    db.Select(\"id\").Where(\"id = ?\", id).First(&tag)\n    if tag.ID > 0 {\n        return true\n    }\n\n    return false\n}\n\nfunc DeleteTag(id int) bool {\n    db.Where(\"id = ?\", id).Delete(&Tag{})\n\n    return true\n}\n\nfunc EditTag(id int, data interface {}) bool {\n    db.Model(&Tag{}).Where(\"id = ?\", id).Updates(data)\n\n    return true\n}\n...\n```\n\n## 验证功能\n\n重启服务，用 Postman\n\n- PUT 访问 http://127.0.0.1:8000/api/v1/tags/1?name=edit1&state=0&modified_by=edit1 ，查看 code 是否返回 200\n- DELETE 访问 http://127.0.0.1:8000/api/v1/tags/1 ，查看 code 是否返回 200\n\n至此，Tag 的 API's 完成，下一节我们将开始 Article 的 API's 编写！\n\n## 参考\n\n### 本系列示例代码\n\n- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)\n\n## 关于\n\n### 修改记录\n\n- 第一版：2018 年 02 月 16 日发布文章\n- 第二版：2019 年 10 月 01 日修改文章\n\n## ？\n\n如果有任何疑问或错误，欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见，如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进。\n\n### 我的公众号\n\n![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)"
  },
  {
    "path": "content/posts/go/gin/2018-02-13-api-03.md",
    "content": "---\n\ntitle:      \"「连载四」Gin搭建Blog API's （三）\"\ndate:       2018-02-13 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n---\n\n## 涉及知识点\n\n- [Gin](https://github.com/gin-gonic/gin)：Golang 的一个微框架，性能极佳。\n- [beego-validation](https://github.com/astaxie/beego/tree/master/validation)：本节采用的 beego 的表单验证库，[中文文档](https://beego.me/docs/mvc/controller/validation.md)。\n- [gorm](https://github.com/jinzhu/gorm)，对开发人员友好的 ORM 框架，[英文文档](http://gorm.io/docs/)\n- [com](https://github.com/Unknwon/com)，一个小而美的工具包。\n\n## 本文目标\n\n- 完成博客的文章类接口定义和编写\n\n## 定义接口\n\n本节编写文章的逻辑，我们定义一下接口吧！\n\n- 获取文章列表：GET(\"/articles\")\n- 获取指定文章：POST(\"/articles/:id\")\n- 新建文章：POST(\"/articles\")\n- 更新指定文章：PUT(\"/articles/:id\")\n- 删除指定文章：DELETE(\"/articles/:id\")\n\n## 编写路由逻辑\n\n在`routers`的 v1 版本下，新建`article.go`文件，写入内容：\n\n```go\npackage v1\n\nimport (\n    \"github.com/gin-gonic/gin\"\n)\n\n//获取单个文章\nfunc GetArticle(c *gin.Context) {\n}\n\n//获取多个文章\nfunc GetArticles(c *gin.Context) {\n}\n\n//新增文章\nfunc AddArticle(c *gin.Context) {\n}\n\n//修改文章\nfunc EditArticle(c *gin.Context) {\n}\n\n//删除文章\nfunc DeleteArticle(c *gin.Context) {\n}\n```\n\n我们打开`routers`下的`router.go`文件，修改文件内容为：\n\n```go\npackage routers\n\nimport (\n    \"github.com/gin-gonic/gin\"\n\n    \"github.com/EDDYCJY/go-gin-example/routers/api/v1\"\n    \"github.com/EDDYCJY/go-gin-example/pkg/setting\"\n)\n\nfunc InitRouter() *gin.Engine {\n    ...\n    apiv1 := r.Group(\"/api/v1\")\n    {\n        ...\n        //获取文章列表\n        apiv1.GET(\"/articles\", v1.GetArticles)\n        //获取指定文章\n        apiv1.GET(\"/articles/:id\", v1.GetArticle)\n        //新建文章\n        apiv1.POST(\"/articles\", v1.AddArticle)\n        //更新指定文章\n        apiv1.PUT(\"/articles/:id\", v1.EditArticle)\n        //删除指定文章\n        apiv1.DELETE(\"/articles/:id\", v1.DeleteArticle)\n    }\n\n    return r\n}\n```\n\n当前目录结构：\n\n```\ngo-gin-example/\n├── conf\n│   └── app.ini\n├── main.go\n├── middleware\n├── models\n│   ├── models.go\n│   └── tag.go\n├── pkg\n│   ├── e\n│   │   ├── code.go\n│   │   └── msg.go\n│   ├── setting\n│   │   └── setting.go\n│   └── util\n│       └── pagination.go\n├── routers\n│   ├── api\n│   │   └── v1\n│   │       ├── article.go\n│   │       └── tag.go\n│   └── router.go\n├── runtime\n\n```\n\n在基础的路由规则配置结束后，我们**开始编写我们的接口**吧！\n\n---\n\n##编写 models 逻辑\n创建`models`目录下的`article.go`，写入文件内容：\n\n```go\npackage models\n\nimport (\n    \"github.com/jinzhu/gorm\"\n\n    \"time\"\n)\n\ntype Article struct {\n    Model\n\n    TagID int `json:\"tag_id\" gorm:\"index\"`\n    Tag   Tag `json:\"tag\"`\n\n    Title string `json:\"title\"`\n    Desc string `json:\"desc\"`\n    Content string `json:\"content\"`\n    CreatedBy string `json:\"created_by\"`\n    ModifiedBy string `json:\"modified_by\"`\n    State int `json:\"state\"`\n}\n\n\nfunc (article *Article) BeforeCreate(scope *gorm.Scope) error {\n    scope.SetColumn(\"CreatedOn\", time.Now().Unix())\n\n    return nil\n}\n\nfunc (article *Article) BeforeUpdate(scope *gorm.Scope) error {\n    scope.SetColumn(\"ModifiedOn\", time.Now().Unix())\n\n    return nil\n}\n```\n\n我们创建了一个`Article struct {}`，与`Tag`不同的是，`Article`多了几项，如下：\n\n1. `gorm:index`，用于声明这个字段为索引，如果你使用了自动迁移功能则会有所影响，在不使用则无影响\n2. `Tag`字段，实际是一个嵌套的`struct`，它利用`TagID`与`Tag`模型相互关联，在执行查询的时候，能够达到`Article`、`Tag`关联查询的功能\n3. `time.Now().Unix()` 返回当前的时间戳\n\n接下来，请确保已对上一章节的内容通读且了解，由于逻辑偏差不会太远，我们本节直接编写这五个接口\n\n---\n\n打开`models`目录下的`article.go`，修改文件内容：\n\n```go\npackage models\n\nimport (\n    \"time\"\n\n    \"github.com/jinzhu/gorm\"\n)\n\ntype Article struct {\n    Model\n\n    TagID int `json:\"tag_id\" gorm:\"index\"`\n    Tag   Tag `json:\"tag\"`\n\n    Title string `json:\"title\"`\n    Desc string `json:\"desc\"`\n    Content string `json:\"content\"`\n    CreatedBy string `json:\"created_by\"`\n    ModifiedBy string `json:\"modified_by\"`\n    State int `json:\"state\"`\n}\n\n\nfunc ExistArticleByID(id int) bool {\n    var article Article\n    db.Select(\"id\").Where(\"id = ?\", id).First(&article)\n\n    if article.ID > 0 {\n        return true\n    }\n\n    return false\n}\n\nfunc GetArticleTotal(maps interface {}) (count int){\n    db.Model(&Article{}).Where(maps).Count(&count)\n\n    return\n}\n\nfunc GetArticles(pageNum int, pageSize int, maps interface {}) (articles []Article) {\n    db.Preload(\"Tag\").Where(maps).Offset(pageNum).Limit(pageSize).Find(&articles)\n\n    return\n}\n\nfunc GetArticle(id int) (article Article) {\n    db.Where(\"id = ?\", id).First(&article)\n    db.Model(&article).Related(&article.Tag)\n\n    return\n}\n\nfunc EditArticle(id int, data interface {}) bool {\n    db.Model(&Article{}).Where(\"id = ?\", id).Updates(data)\n\n    return true\n}\n\nfunc AddArticle(data map[string]interface {}) bool {\n    db.Create(&Article {\n        TagID : data[\"tag_id\"].(int),\n        Title : data[\"title\"].(string),\n        Desc : data[\"desc\"].(string),\n        Content : data[\"content\"].(string),\n        CreatedBy : data[\"created_by\"].(string),\n        State : data[\"state\"].(int),\n    })\n\n    return true\n}\n\nfunc DeleteArticle(id int) bool {\n    db.Where(\"id = ?\", id).Delete(Article{})\n\n    return true\n}\n\nfunc (article *Article) BeforeCreate(scope *gorm.Scope) error {\n    scope.SetColumn(\"CreatedOn\", time.Now().Unix())\n\n    return nil\n}\n\nfunc (article *Article) BeforeUpdate(scope *gorm.Scope) error {\n    scope.SetColumn(\"ModifiedOn\", time.Now().Unix())\n\n    return nil\n}\n```\n\n在这里，我们拿出三点不同来讲，如下：\n\n**1、 我们的`Article`是如何关联到`Tag`？**\n\n```go\nfunc GetArticle(id int) (article Article) {\n    db.Where(\"id = ?\", id).First(&article)\n    db.Model(&article).Related(&article.Tag)\n\n    return\n}\n```\n\n能够达到关联，首先是`gorm`本身做了大量的约定俗成\n\n- `Article`有一个结构体成员是`TagID`，就是外键。`gorm`会通过类名+ID 的方式去找到这两个类之间的关联关系\n- `Article`有一个结构体成员是`Tag`，就是我们嵌套在`Article`里的`Tag`结构体，我们可以通过`Related`进行关联查询\n\n**2、 `Preload`是什么东西，为什么查询可以得出每一项的关联`Tag`？**\n\n```go\nfunc GetArticles(pageNum int, pageSize int, maps interface {}) (articles []Article) {\n    db.Preload(\"Tag\").Where(maps).Offset(pageNum).Limit(pageSize).Find(&articles)\n\n    return\n}\n```\n\n`Preload`就是一个预加载器，它会执行两条 SQL，分别是`SELECT * FROM blog_articles;`和`SELECT * FROM blog_tag WHERE id IN (1,2,3,4);`，那么在查询出结构后，`gorm`内部处理对应的映射逻辑，将其填充到`Article`的`Tag`中，会特别方便，并且避免了循环查询\n\n那么有没有别的办法呢，大致是两种\n\n- `gorm`的`Join`\n- 循环`Related`\n\n综合之下，还是`Preload`更好，如果你有更优的方案，欢迎说一下 :)\n\n**3、 `v.(I)` 是什么？**\n\n`v`表示一个接口值，`I`表示接口类型。这个实际就是 Golang 中的**类型断言**，用于判断一个接口值的实际类型是否为某个类型，或一个非接口值的类型是否实现了某个接口类型\n\n---\n\n打开`routers`目录下 v1 版本的`article.go`文件，修改文件内容：\n\n```go\npackage v1\n\nimport (\n    \"net/http\"\n    \"log\"\n\n    \"github.com/gin-gonic/gin\"\n    \"github.com/astaxie/beego/validation\"\n    \"github.com/unknwon/com\"\n\n    \"github.com/EDDYCJY/go-gin-example/models\"\n    \"github.com/EDDYCJY/go-gin-example/pkg/e\"\n    \"github.com/EDDYCJY/go-gin-example/pkg/setting\"\n    \"github.com/EDDYCJY/go-gin-example/pkg/util\"\n)\n\n//获取单个文章\nfunc GetArticle(c *gin.Context) {\n    id := com.StrTo(c.Param(\"id\")).MustInt()\n\n    valid := validation.Validation{}\n    valid.Min(id, 1, \"id\").Message(\"ID必须大于0\")\n\n    code := e.INVALID_PARAMS\n    var data interface {}\n    if ! valid.HasErrors() {\n        if models.ExistArticleByID(id) {\n            data = models.GetArticle(id)\n            code = e.SUCCESS\n        } else {\n            code = e.ERROR_NOT_EXIST_ARTICLE\n        }\n    } else {\n        for _, err := range valid.Errors {\n            log.Printf(\"err.key: %s, err.message: %s\", err.Key, err.Message)\n        }\n    }\n\n    c.JSON(http.StatusOK, gin.H{\n        \"code\" : code,\n        \"msg\" : e.GetMsg(code),\n        \"data\" : data,\n    })\n}\n\n//获取多个文章\nfunc GetArticles(c *gin.Context) {\n    data := make(map[string]interface{})\n    maps := make(map[string]interface{})\n    valid := validation.Validation{}\n\n    var state int = -1\n    if arg := c.Query(\"state\"); arg != \"\" {\n        state = com.StrTo(arg).MustInt()\n        maps[\"state\"] = state\n\n        valid.Range(state, 0, 1, \"state\").Message(\"状态只允许0或1\")\n    }\n\n    var tagId int = -1\n    if arg := c.Query(\"tag_id\"); arg != \"\" {\n        tagId = com.StrTo(arg).MustInt()\n        maps[\"tag_id\"] = tagId\n\n        valid.Min(tagId, 1, \"tag_id\").Message(\"标签ID必须大于0\")\n    }\n\n    code := e.INVALID_PARAMS\n    if ! valid.HasErrors() {\n        code = e.SUCCESS\n\n        data[\"lists\"] = models.GetArticles(util.GetPage(c), setting.PageSize, maps)\n        data[\"total\"] = models.GetArticleTotal(maps)\n\n    } else {\n        for _, err := range valid.Errors {\n            log.Printf(\"err.key: %s, err.message: %s\", err.Key, err.Message)\n        }\n    }\n\n    c.JSON(http.StatusOK, gin.H{\n        \"code\" : code,\n        \"msg\" : e.GetMsg(code),\n        \"data\" : data,\n    })\n}\n\n//新增文章\nfunc AddArticle(c *gin.Context) {\n    tagId := com.StrTo(c.Query(\"tag_id\")).MustInt()\n    title := c.Query(\"title\")\n    desc := c.Query(\"desc\")\n    content := c.Query(\"content\")\n    createdBy := c.Query(\"created_by\")\n    state := com.StrTo(c.DefaultQuery(\"state\", \"0\")).MustInt()\n\n    valid := validation.Validation{}\n    valid.Min(tagId, 1, \"tag_id\").Message(\"标签ID必须大于0\")\n    valid.Required(title, \"title\").Message(\"标题不能为空\")\n    valid.Required(desc, \"desc\").Message(\"简述不能为空\")\n    valid.Required(content, \"content\").Message(\"内容不能为空\")\n    valid.Required(createdBy, \"created_by\").Message(\"创建人不能为空\")\n    valid.Range(state, 0, 1, \"state\").Message(\"状态只允许0或1\")\n\n    code := e.INVALID_PARAMS\n    if ! valid.HasErrors() {\n        if models.ExistTagByID(tagId) {\n            data := make(map[string]interface {})\n            data[\"tag_id\"] = tagId\n            data[\"title\"] = title\n            data[\"desc\"] = desc\n            data[\"content\"] = content\n            data[\"created_by\"] = createdBy\n            data[\"state\"] = state\n\n            models.AddArticle(data)\n            code = e.SUCCESS\n        } else {\n            code = e.ERROR_NOT_EXIST_TAG\n        }\n    } else {\n        for _, err := range valid.Errors {\n            log.Printf(\"err.key: %s, err.message: %s\", err.Key, err.Message)\n        }\n    }\n\n    c.JSON(http.StatusOK, gin.H{\n        \"code\" : code,\n        \"msg\" : e.GetMsg(code),\n        \"data\" : make(map[string]interface{}),\n    })\n}\n\n//修改文章\nfunc EditArticle(c *gin.Context) {\n    valid := validation.Validation{}\n\n    id := com.StrTo(c.Param(\"id\")).MustInt()\n    tagId := com.StrTo(c.Query(\"tag_id\")).MustInt()\n    title := c.Query(\"title\")\n    desc := c.Query(\"desc\")\n    content := c.Query(\"content\")\n    modifiedBy := c.Query(\"modified_by\")\n\n    var state int = -1\n    if arg := c.Query(\"state\"); arg != \"\" {\n        state = com.StrTo(arg).MustInt()\n        valid.Range(state, 0, 1, \"state\").Message(\"状态只允许0或1\")\n    }\n\n    valid.Min(id, 1, \"id\").Message(\"ID必须大于0\")\n    valid.MaxSize(title, 100, \"title\").Message(\"标题最长为100字符\")\n    valid.MaxSize(desc, 255, \"desc\").Message(\"简述最长为255字符\")\n    valid.MaxSize(content, 65535, \"content\").Message(\"内容最长为65535字符\")\n    valid.Required(modifiedBy, \"modified_by\").Message(\"修改人不能为空\")\n    valid.MaxSize(modifiedBy, 100, \"modified_by\").Message(\"修改人最长为100字符\")\n\n    code := e.INVALID_PARAMS\n    if ! valid.HasErrors() {\n        if models.ExistArticleByID(id) {\n            if models.ExistTagByID(tagId) {\n                data := make(map[string]interface {})\n                if tagId > 0 {\n                    data[\"tag_id\"] = tagId\n                }\n                if title != \"\" {\n                    data[\"title\"] = title\n                }\n                if desc != \"\" {\n                    data[\"desc\"] = desc\n                }\n                if content != \"\" {\n                    data[\"content\"] = content\n                }\n\n                data[\"modified_by\"] = modifiedBy\n\n                models.EditArticle(id, data)\n                code = e.SUCCESS\n            } else {\n                code = e.ERROR_NOT_EXIST_TAG\n            }\n        } else {\n            code = e.ERROR_NOT_EXIST_ARTICLE\n        }\n    } else {\n        for _, err := range valid.Errors {\n            log.Printf(\"err.key: %s, err.message: %s\", err.Key, err.Message)\n        }\n    }\n\n    c.JSON(http.StatusOK, gin.H{\n        \"code\" : code,\n        \"msg\" : e.GetMsg(code),\n        \"data\" : make(map[string]string),\n    })\n}\n\n//删除文章\nfunc DeleteArticle(c *gin.Context) {\n    id := com.StrTo(c.Param(\"id\")).MustInt()\n\n    valid := validation.Validation{}\n    valid.Min(id, 1, \"id\").Message(\"ID必须大于0\")\n\n    code := e.INVALID_PARAMS\n    if ! valid.HasErrors() {\n        if models.ExistArticleByID(id) {\n            models.DeleteArticle(id)\n            code = e.SUCCESS\n        } else {\n            code = e.ERROR_NOT_EXIST_ARTICLE\n        }\n    } else {\n        for _, err := range valid.Errors {\n            log.Printf(\"err.key: %s, err.message: %s\", err.Key, err.Message)\n        }\n    }\n\n    c.JSON(http.StatusOK, gin.H{\n        \"code\" : code,\n        \"msg\" : e.GetMsg(code),\n        \"data\" : make(map[string]string),\n    })\n}\n```\n\n当前目录结构：\n\n```\ngo-gin-example/\n├── conf\n│   └── app.ini\n├── main.go\n├── middleware\n├── models\n│   ├── article.go\n│   ├── models.go\n│   └── tag.go\n├── pkg\n│   ├── e\n│   │   ├── code.go\n│   │   └── msg.go\n│   ├── setting\n│   │   └── setting.go\n│   └── util\n│       └── pagination.go\n├── routers\n│   ├── api\n│   │   └── v1\n│   │       ├── article.go\n│   │       └── tag.go\n│   └── router.go\n├── runtime\n```\n\n## 验证功能\n\n我们重启服务，执行`go run main.go`，检查控制台输出结果\n\n```\n$ go run main.go\n[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:   export GIN_MODE=release\n - using code:  gin.SetMode(gin.ReleaseMode)\n\n[GIN-debug] GET    /api/v1/tags              --> gin-blog/routers/api/v1.GetTags (3 handlers)\n[GIN-debug] POST   /api/v1/tags              --> gin-blog/routers/api/v1.AddTag (3 handlers)\n[GIN-debug] PUT    /api/v1/tags/:id          --> gin-blog/routers/api/v1.EditTag (3 handlers)\n[GIN-debug] DELETE /api/v1/tags/:id          --> gin-blog/routers/api/v1.DeleteTag (3 handlers)\n[GIN-debug] GET    /api/v1/articles          --> gin-blog/routers/api/v1.GetArticles (3 handlers)\n[GIN-debug] GET    /api/v1/articles/:id      --> gin-blog/routers/api/v1.GetArticle (3 handlers)\n[GIN-debug] POST   /api/v1/articles          --> gin-blog/routers/api/v1.AddArticle (3 handlers)\n[GIN-debug] PUT    /api/v1/articles/:id      --> gin-blog/routers/api/v1.EditArticle (3 handlers)\n[GIN-debug] DELETE /api/v1/articles/:id      --> gin-blog/routers/api/v1.DeleteArticle (3 handlers)\n```\n\n使用`Postman`检验接口是否正常，在这里大家可以选用合适的参数传递方式，此处为了方便展示我选用了 GET/Param 传参的方式，而后期会改为 POST。\n\n- POST：http://127.0.0.1:8000/api/v1/articles?tag_id=1&title=test1&desc=test-desc&content=test-content&created_by=test-created&state=1\n- GET：http://127.0.0.1:8000/api/v1/articles\n- GET：http://127.0.0.1:8000/api/v1/articles/1\n- PUT：http://127.0.0.1:8000/api/v1/articles/1?tag_id=1&title=test-edit1&desc=test-desc-edit&content=test-content-edit&modified_by=test-created-edit&state=0\n- DELETE：http://127.0.0.1:8000/api/v1/articles/1\n\n至此，我们的 API's 编写就到这里，下一节我们将介绍另外的一些技巧！\n\n## 参考\n\n### 本系列示例代码\n\n- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)\n\n## 关于\n\n### 修改记录\n\n- 第一版：2018 年 02 月 16 日发布文章\n- 第二版：2019 年 10 月 01 日修改文章\n\n## ？\n\n如果有任何疑问或错误，欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见，如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进。\n\n### 我的公众号\n\n![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)"
  },
  {
    "path": "content/posts/go/gin/2018-02-14-jwt.md",
    "content": "---\n\ntitle:      \"「连载五」使用 JWT 进行身份校验\"\ndate:       2018-02-14 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n---\n\n## 涉及知识点\n\n- JWT\n\n## 本文目标\n\n在前面几节中，我们已经基本的完成了 API's 的编写，但是，还存在一些非常严重的问题，例如，我们现在的 API 是可以随意调用的，这显然还不安全全，在本文中我们通过 [jwt-go](https://github.com/dgrijalva/jwt-go) （[GoDoc](https://godoc.org/github.com/dgrijalva/jwt-go)）的方式来简单解决这个问题。\n\n## 下载依赖包\n\n首先，我们下载 jwt-go 的依赖包，如下：\n\n```\ngo get -u github.com/dgrijalva/jwt-go\n```\n\n## 编写 jwt 工具包\n\n我们需要编写一个`jwt`的工具包，我们在`pkg`下的`util`目录新建`jwt.go`，写入文件内容：\n\n```go\npackage util\n\nimport (\n\t\"time\"\n\n\tjwt \"github.com/dgrijalva/jwt-go\"\n\n\t\"github.com/EDDYCJY/go-gin-example/pkg/setting\"\n)\n\nvar jwtSecret = []byte(setting.JwtSecret)\n\ntype Claims struct {\n\tUsername string `json:\"username\"`\n\tPassword string `json:\"password\"`\n\tjwt.StandardClaims\n}\n\nfunc GenerateToken(username, password string) (string, error) {\n\tnowTime := time.Now()\n\texpireTime := nowTime.Add(3 * time.Hour)\n\n\tclaims := Claims{\n\t\tusername,\n\t\tpassword,\n\t\tjwt.StandardClaims {\n\t\t\tExpiresAt : expireTime.Unix(),\n\t\t\tIssuer : \"gin-blog\",\n\t\t},\n\t}\n\n\ttokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)\n\ttoken, err := tokenClaims.SignedString(jwtSecret)\n\n\treturn token, err\n}\n\nfunc ParseToken(token string) (*Claims, error) {\n\ttokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {\n\t\treturn jwtSecret, nil\n\t})\n\n\tif tokenClaims != nil {\n\t\tif claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid {\n\t\t\treturn claims, nil\n\t\t}\n\t}\n\n\treturn nil, err\n}\n```\n\n在这个工具包，我们涉及到\n\n- `NewWithClaims(method SigningMethod, claims Claims)`，`method`对应着`SigningMethodHMAC struct{}`，其包含`SigningMethodHS256`、`SigningMethodHS384`、`SigningMethodHS512`三种`crypto.Hash`方案\n- `func (t *Token) SignedString(key interface{})` 该方法内部生成签名字符串，再用于获取完整、已签名的`token`\n- `func (p *Parser) ParseWithClaims` 用于解析鉴权的声明，[方法内部](https://gowalker.org/github.com/dgrijalva/jwt-go#Parser_ParseWithClaims)主要是具体的解码和校验的过程，最终返回`*Token`\n- `func (m MapClaims) Valid()` 验证基于时间的声明`exp, iat, nbf`，注意如果没有任何声明在令牌中，仍然会被认为是有效的。并且对于时区偏差没有计算方法\n\n有了`jwt`工具包，接下来我们要编写要用于`Gin`的中间件，我们在`middleware`下新建`jwt`目录，新建`jwt.go`文件，写入内容：\n\n```go\npackage jwt\n\nimport (\n\t\"time\"\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\n\t\"github.com/EDDYCJY/go-gin-example/pkg/util\"\n\t\"github.com/EDDYCJY/go-gin-example/pkg/e\"\n)\n\nfunc JWT() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tvar code int\n\t\tvar data interface{}\n\n\t\tcode = e.SUCCESS\n\t\ttoken := c.Query(\"token\")\n\t\tif token == \"\" {\n\t\t\tcode = e.INVALID_PARAMS\n\t\t} else {\n\t\t\tclaims, err := util.ParseToken(token)\n\t\t\tif err != nil {\n\t\t\t\tcode = e.ERROR_AUTH_CHECK_TOKEN_FAIL\n\t\t\t} else if time.Now().Unix() > claims.ExpiresAt {\n\t\t\t\tcode = e.ERROR_AUTH_CHECK_TOKEN_TIMEOUT\n\t\t\t}\n\t\t}\n\n\t\tif code != e.SUCCESS {\n\t\t\tc.JSON(http.StatusUnauthorized, gin.H{\n\t\t        \"code\" : code,\n\t\t        \"msg\" : e.GetMsg(code),\n\t\t        \"data\" : data,\n\t\t    })\n\n\t\t    c.Abort()\n\t\t    return\n\t\t}\n\n\t\tc.Next()\n\t}\n}\n```\n\n## 如何获取`Token`\n\n那么我们如何调用它呢，我们还要获取`Token`呢？\n\n1、 我们要新增一个获取`Token`的 API\n\n在`models`下新建`auth.go`文件，写入内容：\n\n```go\npackage models\n\ntype Auth struct {\n\tID int `gorm:\"primary_key\" json:\"id\"`\n\tUsername string `json:\"username\"`\n\tPassword string `json:\"password\"`\n}\n\nfunc CheckAuth(username, password string) bool {\n\tvar auth Auth\n\tdb.Select(\"id\").Where(Auth{Username : username, Password : password}).First(&auth)\n\tif auth.ID > 0 {\n\t\treturn true\n\t}\n\n\treturn false\n}\n```\n\n在`routers`下的`api`目录新建`auth.go`文件，写入内容：\n\n```go\npackage api\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/astaxie/beego/validation\"\n\n\t\"github.com/EDDYCJY/go-gin-example/pkg/e\"\n\t\"github.com/EDDYCJY/go-gin-example/pkg/util\"\n\t\"github.com/EDDYCJY/go-gin-example/models\"\n)\n\ntype auth struct {\n\tUsername string `valid:\"Required; MaxSize(50)\"`\n\tPassword string `valid:\"Required; MaxSize(50)\"`\n}\n\nfunc GetAuth(c *gin.Context) {\n\tusername := c.Query(\"username\")\n\tpassword := c.Query(\"password\")\n\n\tvalid := validation.Validation{}\n\ta := auth{Username: username, Password: password}\n\tok, _ := valid.Valid(&a)\n\n\tdata := make(map[string]interface{})\n\tcode := e.INVALID_PARAMS\n\tif ok {\n\t\tisExist := models.CheckAuth(username, password)\n\t\tif isExist {\n\t\t\ttoken, err := util.GenerateToken(username, password)\n\t\t\tif err != nil {\n\t\t\t\tcode = e.ERROR_AUTH_TOKEN\n\t\t\t} else {\n\t\t\t\tdata[\"token\"] = token\n\n\t\t\t\tcode = e.SUCCESS\n\t\t\t}\n\n\t\t} else {\n\t\t\tcode = e.ERROR_AUTH\n\t\t}\n\t} else {\n\t\tfor _, err := range valid.Errors {\n            log.Println(err.Key, err.Message)\n        }\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n        \"code\" : code,\n        \"msg\" : e.GetMsg(code),\n        \"data\" : data,\n    })\n}\n```\n\n我们打开`routers`目录下的`router.go`文件，修改文件内容（新增获取 token 的方法）：\n\n```go\npackage routers\n\nimport (\n    \"github.com/gin-gonic/gin\"\n\n    \"github.com/EDDYCJY/go-gin-example/routers/api\"\n    \"github.com/EDDYCJY/go-gin-example/routers/api/v1\"\n    \"github.com/EDDYCJY/go-gin-example/pkg/setting\"\n)\n\nfunc InitRouter() *gin.Engine {\n    r := gin.New()\n\n    r.Use(gin.Logger())\n\n    r.Use(gin.Recovery())\n\n    gin.SetMode(setting.RunMode)\n\n    r.GET(\"/auth\", api.GetAuth)\n\n    apiv1 := r.Group(\"/api/v1\")\n    {\n        ...\n    }\n\n    return r\n}\n```\n\n## 验证`Token`\n\n获取`token`的 API 方法就到这里啦，让我们来测试下是否可以正常使用吧！\n\n重启服务后，用`GET`方式访问`http://127.0.0.1:8000/auth?username=test&password=test123456`，查看返回值是否正确\n\n```json\n{\n  \"code\": 200,\n  \"data\": {\n    \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJwYXNzd29yZCI6InRlc3QxMjM0NTYiLCJleHAiOjE1MTg3MjAwMzcsImlzcyI6Imdpbi1ibG9nIn0.-kK0V9E06qTHOzupQM_gHXAGDB3EJtJS4H5TTCyWwW8\"\n  },\n  \"msg\": \"ok\"\n}\n```\n\n我们有了`token`的 API，也调用成功了\n\n## 将中间件接入`Gin`\n\n2、 接下来我们将中间件接入到`Gin`的访问流程中\n\n我们打开`routers`目录下的`router.go`文件，修改文件内容（新增引用包和中间件引用）\n\n```go\npackage routers\n\nimport (\n    \"github.com/gin-gonic/gin\"\n\n    \"github.com/EDDYCJY/go-gin-example/routers/api\"\n    \"github.com/EDDYCJY/go-gin-example/routers/api/v1\"\n    \"github.com/EDDYCJY/go-gin-example/pkg/setting\"\n    \"github.com/EDDYCJY/go-gin-example/middleware/jwt\"\n)\n\nfunc InitRouter() *gin.Engine {\n    r := gin.New()\n\n    r.Use(gin.Logger())\n\n    r.Use(gin.Recovery())\n\n    gin.SetMode(setting.RunMode)\n\n    r.GET(\"/auth\", api.GetAuth)\n\n    apiv1 := r.Group(\"/api/v1\")\n    apiv1.Use(jwt.JWT())\n    {\n        ...\n    }\n\n    return r\n}\n```\n\n当前目录结构：\n\n```\ngo-gin-example/\n├── conf\n│   └── app.ini\n├── main.go\n├── middleware\n│   └── jwt\n│       └── jwt.go\n├── models\n│   ├── article.go\n│   ├── auth.go\n│   ├── models.go\n│   └── tag.go\n├── pkg\n│   ├── e\n│   │   ├── code.go\n│   │   └── msg.go\n│   ├── setting\n│   │   └── setting.go\n│   └── util\n│       ├── jwt.go\n│       └── pagination.go\n├── routers\n│   ├── api\n│   │   ├── auth.go\n│   │   └── v1\n│   │       ├── article.go\n│   │       └── tag.go\n│   └── router.go\n├── runtime\n```\n\n到这里，我们的`JWT`编写就完成啦！\n\n## 验证功能\n\n我们来测试一下，再次访问\n\n- http://127.0.0.1:8000/api/v1/articles\n- http://127.0.0.1:8000/api/v1/articles?token=23131\n\n正确的反馈应该是\n\n```json\n{\n  \"code\": 400,\n  \"data\": null,\n  \"msg\": \"请求参数错误\"\n}\n\n{\n  \"code\": 20001,\n  \"data\": null,\n  \"msg\": \"Token鉴权失败\"\n}\n\n```\n\n我们需要访问`http://127.0.0.1:8000/auth?username=test&password=test123456`，得到`token`\n\n```json\n{\n  \"code\": 200,\n  \"data\": {\n    \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJwYXNzd29yZCI6InRlc3QxMjM0NTYiLCJleHAiOjE1MTg3MjQ2OTMsImlzcyI6Imdpbi1ibG9nIn0.KSBY6TeavV_30kfmP7HWLRYKP5TPEDgHtABe9HCsic4\"\n  },\n  \"msg\": \"ok\"\n}\n```\n\n再用包含`token`的 URL 参数去访问我们的应用 API，\n\n访问`http://127.0.0.1:8000/api/v1/articles?token=eyJhbGci...`，检查接口返回值\n\n```json\n{\n  \"code\": 200,\n  \"data\": {\n    \"lists\": [\n      {\n        \"id\": 2,\n        \"created_on\": 1518700920,\n        \"modified_on\": 0,\n        \"tag_id\": 1,\n        \"tag\": {\n          \"id\": 1,\n          \"created_on\": 1518684200,\n          \"modified_on\": 0,\n          \"name\": \"tag1\",\n          \"created_by\": \"\",\n          \"modified_by\": \"\",\n          \"state\": 0\n        },\n        \"content\": \"test-content\",\n        \"created_by\": \"test-created\",\n        \"modified_by\": \"\",\n        \"state\": 0\n      }\n    ],\n    \"total\": 1\n  },\n  \"msg\": \"ok\"\n}\n```\n\n返回正确，至此我们的`jwt-go`在`Gin`中的验证就完成了！\n\n## 参考\n\n### 本系列示例代码\n\n- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)\n\n## 关于\n\n### 修改记录\n\n- 第一版：2018 年 02 月 16 日发布文章\n- 第二版：2019 年 10 月 01 日修改文章\n\n## ？\n\n如果有任何疑问或错误，欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见，如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进。\n\n### 我的公众号\n\n![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)"
  },
  {
    "path": "content/posts/go/gin/2018-02-15-log.md",
    "content": "---\n\ntitle:      \"「连载六」编写一个简单的文件日志\"\ndate:       2018-02-15 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n---\n\n## 涉及知识点\n\n- 自定义 log。\n\n## 本文目标\n\n在上一节中，我们解决了 API's 可以任意访问的问题，那么我们现在还有一个问题，就是我们的日志，都是输出到控制台上的，这显然对于一个项目来说是不合理的，因此我们这一节简单封装`log`库，使其支持简单的文件日志！\n\n## 新建`logging`包\n\n我们在`pkg`下新建`logging`目录，新建`file.go`和`log.go`文件，写入内容：\n\n### 编写`file`文件\n\n**1、 file.go：**\n\n```go\npackage logging\n\nimport (\n\t\"os\"\n\t\"time\"\n\t\"fmt\"\n\t\"log\"\n)\n\nvar (\n\tLogSavePath = \"runtime/logs/\"\n\tLogSaveName = \"log\"\n\tLogFileExt = \"log\"\n\tTimeFormat = \"20060102\"\n)\n\nfunc getLogFilePath() string {\n\treturn fmt.Sprintf(\"%s\", LogSavePath)\n}\n\nfunc getLogFileFullPath() string {\n\tprefixPath := getLogFilePath()\n\tsuffixPath := fmt.Sprintf(\"%s%s.%s\", LogSaveName, time.Now().Format(TimeFormat), LogFileExt)\n\n\treturn fmt.Sprintf(\"%s%s\", prefixPath, suffixPath)\n}\n\nfunc openLogFile(filePath string) *os.File {\n\t_, err := os.Stat(filePath)\n\tswitch {\n\t\tcase os.IsNotExist(err):\n\t\t\tmkDir()\n\t\tcase os.IsPermission(err):\n\t\t\tlog.Fatalf(\"Permission :%v\", err)\n\t}\n\n\thandle, err := os.OpenFile(filePath, os.O_APPEND | os.O_CREATE | os.O_WRONLY, 0644)\n\tif err != nil {\n\t\tlog.Fatalf(\"Fail to OpenFile :%v\", err)\n\t}\n\n\treturn handle\n}\n\nfunc mkDir() {\n\tdir, _ := os.Getwd()\n\terr := os.MkdirAll(dir + \"/\" + getLogFilePath(), os.ModePerm)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n```\n\n- `os.Stat`：返回文件信息结构描述文件。如果出现错误，会返回`*PathError`\n\n```go\ntype PathError struct {\n    Op   string\n    Path string\n    Err  error\n}\n```\n\n- `os.IsNotExist`：能够接受`ErrNotExist`、`syscall`的一些错误，它会返回一个布尔值，能够得知文件不存在或目录不存在\n- `os.IsPermission`：能够接受`ErrPermission`、`syscall`的一些错误，它会返回一个布尔值，能够得知权限是否满足\n- `os.OpenFile`：调用文件，支持传入文件名称、指定的模式调用文件、文件权限，返回的文件的方法可以用于 I/O。如果出现错误，则为`*PathError`。\n\n```go\nconst (\n    // Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified.\n    O_RDONLY int = syscall.O_RDONLY // 以只读模式打开文件\n    O_WRONLY int = syscall.O_WRONLY // 以只写模式打开文件\n    O_RDWR   int = syscall.O_RDWR   // 以读写模式打开文件\n    // The remaining values may be or'ed in to control behavior.\n    O_APPEND int = syscall.O_APPEND // 在写入时将数据追加到文件中\n    O_CREATE int = syscall.O_CREAT  // 如果不存在，则创建一个新文件\n    O_EXCL   int = syscall.O_EXCL   // 使用O_CREATE时，文件必须不存在\n    O_SYNC   int = syscall.O_SYNC   // 同步IO\n    O_TRUNC  int = syscall.O_TRUNC  // 如果可以，打开时\n)\n```\n\n- `os.Getwd`：返回与当前目录对应的根路径名\n- `os.MkdirAll`：创建对应的目录以及所需的子目录，若成功则返回`nil`，否则返回`error`\n- `os.ModePerm`：`const`定义`ModePerm FileMode = 0777`\n\n### 编写`log`文件\n\n**2、log.go**\n\n```go\npackage logging\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"runtime\"\n\t\"path/filepath\"\n\t\"fmt\"\n)\n\ntype Level int\n\nvar (\n\tF *os.File\n\n\tDefaultPrefix = \"\"\n\tDefaultCallerDepth = 2\n\n\tlogger *log.Logger\n\tlogPrefix = \"\"\n\tlevelFlags = []string{\"DEBUG\", \"INFO\", \"WARN\", \"ERROR\", \"FATAL\"}\n)\n\nconst (\n\tDEBUG Level = iota\n\tINFO\n\tWARNING\n\tERROR\n\tFATAL\n)\n\nfunc init() {\n\tfilePath := getLogFileFullPath()\n\tF = openLogFile(filePath)\n\n\tlogger = log.New(F, DefaultPrefix, log.LstdFlags)\n}\n\nfunc Debug(v ...interface{}) {\n\tsetPrefix(DEBUG)\n\tlogger.Println(v)\n}\n\nfunc Info(v ...interface{}) {\n\tsetPrefix(INFO)\n\tlogger.Println(v)\n}\n\nfunc Warn(v ...interface{}) {\n\tsetPrefix(WARNING)\n\tlogger.Println(v)\n}\n\nfunc Error(v ...interface{}) {\n\tsetPrefix(ERROR)\n\tlogger.Println(v)\n}\n\nfunc Fatal(v ...interface{}) {\n\tsetPrefix(FATAL)\n\tlogger.Fatalln(v)\n}\n\nfunc setPrefix(level Level) {\n\t_, file, line, ok := runtime.Caller(DefaultCallerDepth)\n\tif ok {\n\t\tlogPrefix = fmt.Sprintf(\"[%s][%s:%d]\", levelFlags[level], filepath.Base(file), line)\n\t} else {\n\t\tlogPrefix = fmt.Sprintf(\"[%s]\", levelFlags[level])\n\t}\n\n\tlogger.SetPrefix(logPrefix)\n}\n\n```\n\n- `log.New`：创建一个新的日志记录器。`out`定义要写入日志数据的`IO`句柄。`prefix`定义每个生成的日志行的开头。`flag`定义了日志记录属性\n\n```go\nfunc New(out io.Writer, prefix string, flag int) *Logger {\n    return &Logger{out: out, prefix: prefix, flag: flag}\n}\n```\n\n- `log.LstdFlags`：日志记录的格式属性之一，其余的选项如下\n\n```go\nconst (\n    Ldate         = 1 << iota     // the date in the local time zone: 2009/01/23\n    Ltime                         // the time in the local time zone: 01:23:23\n    Lmicroseconds                 // microsecond resolution: 01:23:23.123123.  assumes Ltime.\n    Llongfile                     // full file name and line number: /a/b/c/d.go:23\n    Lshortfile                    // final file name element and line number: d.go:23. overrides Llongfile\n    LUTC                          // if Ldate or Ltime is set, use UTC rather than the local time zone\n    LstdFlags     = Ldate | Ltime // initial values for the standard logger\n)\n```\n\n当前目录结构：\n\n```\ngin-blog/\n├── conf\n│   └── app.ini\n├── main.go\n├── middleware\n│   └── jwt\n│       └── jwt.go\n├── models\n│   ├── article.go\n│   ├── auth.go\n│   ├── models.go\n│   └── tag.go\n├── pkg\n│   ├── e\n│   │   ├── code.go\n│   │   └── msg.go\n│   ├── logging\n│   │   ├── file.go\n│   │   └── log.go\n│   ├── setting\n│   │   └── setting.go\n│   └── util\n│       ├── jwt.go\n│       └── pagination.go\n├── routers\n│   ├── api\n│   │   ├── auth.go\n│   │   └── v1\n│   │       ├── article.go\n│   │       └── tag.go\n│   └── router.go\n├── runtime\n\n```\n\n我们自定义的`logging`包，已经基本完成了，接下来让它接入到我们的项目之中吧。我们打开先前包含`log`包的代码，如下：\n\n1. 打开`routers`目录下的`article.go`、`tag.go`、`auth.go`。\n2. 将`log`包的引用删除，修改引用我们自己的日志包为`github.com/EDDYCJY/go-gin-example/pkg/logging`。\n3. 将原本的`log.Println(...)`改为`logging.Info(...)`。\n\n例如`auth.go`文件的修改内容：\n\n```go\npackage api\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/astaxie/beego/validation\"\n\n\t\"github.com/EDDYCJY/go-gin-example/pkg/e\"\n\t\"github.com/EDDYCJY/go-gin-example/pkg/util\"\n\t\"github.com/EDDYCJY/go-gin-example/models\"\n\t\"github.com/EDDYCJY/go-gin-example/pkg/logging\"\n)\n...\nfunc GetAuth(c *gin.Context) {\n\t...\n\tcode := e.INVALID_PARAMS\n\tif ok {\n\t\t...\n\t} else {\n\t    for _, err := range valid.Errors {\n                logging.Info(err.Key, err.Message)\n            }\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n        \"code\" : code,\n        \"msg\" : e.GetMsg(code),\n        \"data\" : data,\n    })\n}\n```\n\n## 验证功能\n\n修改文件后，重启服务，我们来试试吧！\n\n获取到 API 的 Token 后，我们故意传错误 URL 参数给接口，如：`http://127.0.0.1:8000/api/v1/articles?tag_id=0&state=9999999&token=eyJhbG..`\n\n然后我们到`$GOPATH/gin-blog/runtime/logs`查看日志：\n\n```\n$ tail -f log20180216.log\n[INFO][article.go:79]2018/02/16 18:33:12 [state 状态只允许0或1]\n[INFO][article.go:79]2018/02/16 18:33:42 [state 状态只允许0或1]\n[INFO][article.go:79]2018/02/16 18:33:42 [tag_id 标签ID必须大于0]\n[INFO][article.go:79]2018/02/16 18:38:39 [state 状态只允许0或1]\n[INFO][article.go:79]2018/02/16 18:38:39 [tag_id 标签ID必须大于0]\n```\n\n日志结构一切正常，我们的记录模式都为`Info`，因此前缀是对的，并且我们是入参有问题，也把错误记录下来了，这样排错就很方便了！\n\n至此，本节就完成了，这只是一个简单的扩展，实际上我们线上项目要使用的文件日志，是更复杂一些，开动你的大脑 举一反三吧！\n\n## 参考\n\n### 本系列示例代码\n\n- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)\n\n## 关于\n\n### 修改记录\n\n- 第一版：2018 年 02 月 16 日发布文章\n- 第二版：2019 年 10 月 01 日修改文章\n\n## ？\n\n如果有任何疑问或错误，欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见，如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进。\n\n### 我的公众号\n\n![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)"
  },
  {
    "path": "content/posts/go/gin/2018-03-15-reload-http.md",
    "content": "---\n\ntitle:      \"「连载七」优雅的重启服务\"\ndate:       2018-03-15 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n---\n\n## 知识点\n\n- 信号量的了解。\n- 应用热更新。\n\n## 本文目标\n\n在前面编写案例代码时，我相信你会想到，每次更新完代码，更新完配置文件后，就直接这么 `ctrl+c` 真的没问题吗，`ctrl+c`到底做了些什么事情呢？\n\n在这一节中我们简单讲述 `ctrl+c` 背后的**信号**以及如何在`Gin`中**优雅的重启服务**，也就是对 `HTTP` 服务进行热更新。\n\n## ctrl + c\n\n> 内核在某些情况下发送信号，比如在进程往一个已经关闭的管道写数据时会产生`SIGPIPE`信号\n\n在终端执行特定的组合键可以使系统发送特定的信号给此进程，完成一系列的动作\n\n| 命令      | 信号    | 含义                                                                                                    |\n| --------- | ------- | ------------------------------------------------------------------------------------------------------- |\n| ctrl + c  | SIGINT  | 强制进程结束                                                                                            |\n| ctrl + z  | SIGTSTP | 任务中断，进程挂起                                                                                      |\n| ctrl + \\  | SIGQUIT | 进程结束 和 `dump core`                                                                                 |\n| ctrl + d  |         | EOF                                                                                                     |\n|           | SIGHUP  | 终止收到该信号的进程。若程序中没有捕捉该信号，当收到该信号时，进程就会退出（常用于 重启、重新加载进程） |\n\n因此在我们执行`ctrl + c`关闭`gin`服务端时，**会强制进程结束，导致正在访问的用户等出现问题**\n\n常见的 `kill -9 pid` 会发送 `SIGKILL` 信号给进程，也是类似的结果\n\n### 信号\n\n本段中反复出现**信号**是什么呢？\n\n信号是 `Unix` 、类 `Unix` 以及其他 `POSIX` 兼容的操作系统中进程间通讯的一种有限制的方式\n\n它是一种异步的通知机制，用来提醒进程一个事件（硬件异常、程序执行异常、外部发出信号）已经发生。当一个信号发送给一个进程，操作系统中断了进程正常的控制流程。此时，任何非原子操作都将被中断。如果进程定义了信号的处理函数，那么它将被执行，否则就执行默认的处理函数\n\n### 所有信号\n\n```\n$ kill -l\n 1) SIGHUP   2) SIGINT   3) SIGQUIT  4) SIGILL   5) SIGTRAP\n 6) SIGABRT  7) SIGBUS   8) SIGFPE   9) SIGKILL 10) SIGUSR1\n11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM\n16) SIGSTKFLT   17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP\n21) SIGTTIN 22) SIGTTOU 23) SIGURG  24) SIGXCPU 25) SIGXFSZ\n26) SIGVTALRM   27) SIGPROF 28) SIGWINCH    29) SIGIO   30) SIGPWR\n31) SIGSYS  34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3\n38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8\n43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13\n48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12\n53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7\n58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2\n63) SIGRTMAX-1  64) SIGRTMAX\n```\n\n## 怎样算优雅\n\n### 目的\n\n- 不关闭现有连接（正在运行中的程序）\n- 新的进程启动并替代旧进程\n- 新的进程接管新的连接\n- 连接要随时响应用户的请求，当用户仍在请求旧进程时要保持连接，新用户应请求新进程，不可以出现拒绝请求的情况\n\n### 流程\n\n1、替换可执行文件或修改配置文件\n\n2、发送信号量 `SIGHUP`\n\n3、拒绝新连接请求旧进程，但要保证已有连接正常\n\n4、启动新的子进程\n\n5、新的子进程开始 `Accet`\n\n6、系统将新的请求转交新的子进程\n\n7、旧进程处理完所有旧连接后正常结束\n\n## 实现优雅重启\n\n### endless\n\n> Zero downtime restarts for golang HTTP and HTTPS servers. (for golang 1.3+)\n\n我们借助 [fvbock/endless](https://github.com/fvbock/endless) 来实现 `Golang HTTP/HTTPS` 服务重新启动的零停机\n\n`endless server` 监听以下几种信号量：\n\n- syscall.SIGHUP：触发 `fork` 子进程和重新启动\n- syscall.SIGUSR1/syscall.SIGTSTP：被监听，但不会触发任何动作\n- syscall.SIGUSR2：触发 `hammerTime`\n- syscall.SIGINT/syscall.SIGTERM：触发服务器关闭（会完成正在运行的请求）\n\n`endless` 正正是依靠监听这些**信号量**，完成管控的一系列动作\n\n#### 安装\n\n```\ngo get -u github.com/fvbock/endless\n```\n\n#### 编写\n\n打开 [gin-blog](https://github.com/EDDYCJY/go-gin-example) 的 `main.go`文件，修改文件：\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"log\"\n    \"syscall\"\n\n    \"github.com/fvbock/endless\"\n\n    \"gin-blog/routers\"\n    \"gin-blog/pkg/setting\"\n)\n\nfunc main() {\n    endless.DefaultReadTimeOut = setting.ReadTimeout\n    endless.DefaultWriteTimeOut = setting.WriteTimeout\n    endless.DefaultMaxHeaderBytes = 1 << 20\n    endPoint := fmt.Sprintf(\":%d\", setting.HTTPPort)\n\n    server := endless.NewServer(endPoint, routers.InitRouter())\n    server.BeforeBegin = func(add string) {\n        log.Printf(\"Actual pid is %d\", syscall.Getpid())\n    }\n\n    err := server.ListenAndServe()\n    if err != nil {\n        log.Printf(\"Server err: %v\", err)\n    }\n}\n```\n\n`endless.NewServer` 返回一个初始化的 `endlessServer` 对象，在 `BeforeBegin` 时输出当前进程的 `pid`，调用 `ListenAndServe` 将实际“启动”服务\n\n#### 验证\n\n##### **编译**\n\n```\n$ go build main.go\n```\n\n##### **执行**\n\n```\n$ ./main\n[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n...\nActual pid is 48601\n```\n\n启动成功后，输出了`pid`为 48601；在另外一个终端执行 `kill -1 48601` ，检验先前服务的终端效果\n\n```\n[root@localhost go-gin-example]# ./main\n[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:   export GIN_MODE=release\n - using code:  gin.SetMode(gin.ReleaseMode)\n\n[GIN-debug] GET    /auth                     --> ...\n[GIN-debug] GET    /api/v1/tags              --> ...\n...\n\nActual pid is 48601\n\n...\n\nActual pid is 48755\n48601 Received SIGTERM.\n48601 [::]:8000 Listener closed.\n48601 Waiting for connections to finish...\n48601 Serve() returning...\nServer err: accept tcp [::]:8000: use of closed network connection\n```\n\n可以看到该命令已经挂起，并且 `fork` 了新的子进程 `pid` 为 `48755`\n\n```\n48601 Received SIGTERM.\n48601 [::]:8000 Listener closed.\n48601 Waiting for connections to finish...\n48601 Serve() returning...\nServer err: accept tcp [::]:8000: use of closed network connection\n```\n\n大致意思为主进程（`pid`为 48601）接受到 `SIGTERM` 信号量，关闭主进程的监听并且等待正在执行的请求完成；这与我们先前的描述一致\n\n##### **唤醒**\n\n这时候在 `postman` 上再次访问我们的接口，你可以惊喜的发现，他“复活”了！\n\n```\nActual pid is 48755\n48601 Received SIGTERM.\n48601 [::]:8000 Listener closed.\n48601 Waiting for connections to finish...\n48601 Serve() returning...\nServer err: accept tcp [::]:8000: use of closed network connection\n\n\n$ [GIN] 2018/03/15 - 13:00:16 | 200 |     188.096µs |   192.168.111.1 | GET      /api/v1/tags...\n\n```\n\n这就完成了一次正向的流转了\n\n你想想，每次更新发布、或者修改配置文件等，只需要给该进程发送**SIGTERM 信号**，而不需要强制结束应用，是多么便捷又安全的事！\n\n#### 问题\n\n`endless` 热更新是采取创建子进程后，将原进程退出的方式，这点不符合守护进程的要求\n\n### http.Server - Shutdown()\n\n如果你的`Golang >= 1.8`，也可以考虑使用 `http.Server` 的 [Shutdown](https://golang.org/pkg/net/http/#Server.Shutdown) 方法\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"net/http\"\n    \"context\"\n    \"log\"\n    \"os\"\n    \"os/signal\"\n    \"time\"\n\n\n    \"gin-blog/routers\"\n    \"gin-blog/pkg/setting\"\n)\n\nfunc main() {\n    router := routers.InitRouter()\n\n    s := &http.Server{\n        Addr:           fmt.Sprintf(\":%d\", setting.HTTPPort),\n        Handler:        router,\n        ReadTimeout:    setting.ReadTimeout,\n        WriteTimeout:   setting.WriteTimeout,\n        MaxHeaderBytes: 1 << 20,\n    }\n\n    go func() {\n        if err := s.ListenAndServe(); err != nil {\n            log.Printf(\"Listen: %s\\n\", err)\n        }\n    }()\n\n    quit := make(chan os.Signal)\n    signal.Notify(quit, os.Interrupt)\n    <- quit\n\n    log.Println(\"Shutdown Server ...\")\n\n    ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)\n    defer cancel()\n    if err := s.Shutdown(ctx); err != nil {\n        log.Fatal(\"Server Shutdown:\", err)\n    }\n\n    log.Println(\"Server exiting\")\n}\n```\n\n## 小结\n\n在日常的服务中，优雅的重启（热更新）是非常重要的一环。而 `Golang` 在 `HTTP` 服务方面的热更新也有不少方案了，我们应该根据实际应用场景挑选最合适的\n\n## 参考\n\n### 本系列示例代码\n\n- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)\n\n### 拓展阅读\n\n- [manners](https://github.com/braintree/manners)\n- [graceful](https://github.com/tylerb/graceful)\n- [grace](https://github.com/facebookgo/grace)\n- [plugin: new package for loading plugins · golang/go@0cbb12f · GitHub](https://github.com/golang/go/commit/0cbb12f0bbaeb3893b3d011fdb1a270291747ab0)\n\n## 关于\n\n### 修改记录\n\n- 第一版：2018 年 02 月 16 日发布文章\n- 第二版：2019 年 10 月 01 日修改文章\n\n## ？\n\n如果有任何疑问或错误，欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见，如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进。\n\n### 我的公众号\n\n![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)"
  },
  {
    "path": "content/posts/go/gin/2018-03-18-swagger.md",
    "content": "---\n\ntitle:      \"「连载八」为它加上Swagger\"\ndate:       2018-03-18 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n---\n\n## 涉及知识点\n\n- Swagger\n\n## 本文目标\n\n一个好的 `API's`，必然离不开一个好的`API`文档，如果要开发纯手写 `API` 文档，不存在的（很难持续维护），因此我们要自动生成接口文档。\n\n## 安装 swag\n\n```\n$ go get -u github.com/swaggo/swag/cmd/swag@v1.6.5\n```\n\n若 `$GOROOT/bin` 没有加入`$PATH`中，你需要执行将其可执行文件移动到`$GOBIN`下\n\n```\nmv $GOPATH/bin/swag /usr/local/go/bin\n```\n\n### 验证是否安装成功\n\n检查 \\$GOBIN 下是否有 swag 文件，如下：\n\n```\n$ swag -v\nswag version v1.6.5\n```\n\n## 安装 gin-swagger\n\n```\n$ go get -u github.com/swaggo/gin-swagger@v1.2.0 \n$ go get -u github.com/swaggo/files\n$ go get -u github.com/alecthomas/template\n```\n\n注：若无科学上网，请务必配置 Go modules proxy。\n\n## 初始化\n\n### 编写 API 注释\n\n`Swagger` 中需要将相应的注释或注解编写到方法上，再利用生成器自动生成说明文件\n\n`gin-swagger` 给出的范例：\n\n```go\n// @Summary Add a new pet to the store\n// @Description get string by ID\n// @Accept  json\n// @Produce  json\n// @Param   some_id     path    int     true        \"Some ID\"\n// @Success 200 {string} string\t\"ok\"\n// @Failure 400 {object} web.APIError \"We need ID!!\"\n// @Failure 404 {object} web.APIError \"Can not find ID\"\n// @Router /testapi/get-string-by-int/{some_id} [get]\n```\n\n我们可以参照 `Swagger` 的注解规范和范例去编写\n\n```go\n// @Summary 新增文章标签\n// @Produce  json\n// @Param name query string true \"Name\"\n// @Param state query int false \"State\"\n// @Param created_by query int false \"CreatedBy\"\n// @Success 200 {string} json \"{\"code\":200,\"data\":{},\"msg\":\"ok\"}\"\n// @Router /api/v1/tags [post]\nfunc AddTag(c *gin.Context) {\n```\n\n```go\n// @Summary 修改文章标签\n// @Produce  json\n// @Param id path int true \"ID\"\n// @Param name query string true \"ID\"\n// @Param state query int false \"State\"\n// @Param modified_by query string true \"ModifiedBy\"\n// @Success 200 {string} json \"{\"code\":200,\"data\":{},\"msg\":\"ok\"}\"\n// @Router /api/v1/tags/{id} [put]\nfunc EditTag(c *gin.Context) {\n```\n\n参考的注解请参见 [go-gin-example](https://github.com/EDDYCJY/go-gin-example)。以确保获取最新的 swag 语法\n\n### 路由\n\n在完成了注解的编写后，我们需要针对 swagger 新增初始化动作和对应的路由规则，才可以使用。打开 routers/router.go 文件，新增内容如下：\n\n```go\npackage routers\n\nimport (\n\t...\n\n\t_ \"github.com/EDDYCJY/go-gin-example/docs\"\n\n\t...\n)\n\n// InitRouter initialize routing information\nfunc InitRouter() *gin.Engine {\n\t...\n\tr.GET(\"/swagger/*any\", ginSwagger.WrapHandler(swaggerFiles.Handler))\n\t...\n\tapiv1 := r.Group(\"/api/v1\")\n\tapiv1.Use(jwt.JWT())\n\t{\n\t\t...\n\t}\n\n\treturn r\n}\n```\n\n### 生成\n\n我们进入到`gin-blog`的项目根目录中，执行初始化命令\n\n```\n[$ gin-blog]# swag init\n2018/03/13 23:32:10 Generate swagger docs....\n2018/03/13 23:32:10 Generate general API Info\n2018/03/13 23:32:10 create docs.go at  docs/docs.go\n\n```\n\n完毕后会在项目根目录下生成`docs`\n\n```\ndocs/\n├── docs.go\n└── swagger\n    ├── swagger.json\n    └── swagger.yaml\n\n```\n\n我们可以检查 `docs.go` 文件中的 `doc` 变量，详细记载中我们文件中所编写的注解和说明\n![image](https://image.eddycjy.com/37ae10e1714c63899a55d49c19af0860.png)\n\n### 验证\n\n大功告成，访问一下 `http://127.0.0.1:8000/swagger/index.html`， 查看 `API` 文档生成是否正确\n\n![image](https://image.eddycjy.com/703b677c6756129c33b5308c1655a35c.png)\n\n## 参考\n\n### 本系列示例代码\n\n- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)\n\n## 关于\n\n### 修改记录\n\n- 第一版：2018 年 02 月 16 日发布文章\n- 第二版：2019 年 10 月 01 日修改文章\n\n## ？\n\n如果有任何疑问或错误，欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见，如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进。\n\n### 我的公众号\n\n![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)"
  },
  {
    "path": "content/posts/go/gin/2018-03-24-golang-docker.md",
    "content": "---\n\ntitle:      \"「连载九」将Golang应用部署到Docker\"\ndate:       2018-03-24 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n---\n\n## 涉及知识点\n\n- Go + Docker\n\n## 本文目标\n\n将我们的 `go-gin-example` 应用部署到一个 Docker 里，你需要先准备好如下东西：\n\n- 你需要安装好 `docker`。\n- 如果上外网比较吃力，需要配好镜像源。\n\n## Docker\n\n在这里简单介绍下 Docker，建议深入学习\n\n![image](https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1521800047226&di=28b2764fccca8a943aea7d79ad8aed98&imgtype=0&src=http%3A%2F%2Fwww.cww.net.cn%2FupLoadFile%2F2014%2F6%2F13%2F201461382247734.png)\n\nDocker 是一个开源的轻量级容器技术，让开发者可以打包他们的应用以及应用运行的上下文环境到一个可移植的镜像中，然后发布到任何支持 Docker 的系统上运行。 通过容器技术，在几乎没有性能开销的情况下，Docker 为应用提供了一个隔离运行环境\n\n- 简化配置\n- 代码流水线管理\n- 提高开发效率\n- 隔离应用\n- 快速、持续部署\n\n---\n\n接下来我们正式开始对项目进行 `docker` 的所需处理和编写，每一个大标题为步骤大纲\n\n## Golang\n\n### 一、编写 Dockerfile\n\n在 `go-gin-example` 项目根目录创建 Dockerfile 文件，写入内容\n\n```\nFROM golang:latest\n\nENV GOPROXY https://goproxy.cn,direct\nWORKDIR $GOPATH/src/github.com/EDDYCJY/go-gin-example\nCOPY . $GOPATH/src/github.com/EDDYCJY/go-gin-example\nRUN go build .\n\nEXPOSE 8000\nENTRYPOINT [\"./go-gin-example\"]\n```\n\n#### 作用\n\n`golang:latest` 镜像为基础镜像，将工作目录设置为 `$GOPATH/src/go-gin-example`，并将当前上下文目录的内容复制到 `$GOPATH/src/go-gin-example` 中\n\n在进行 `go build` 编译完毕后，将容器启动程序设置为 `./go-gin-example`，也就是我们所编译的可执行文件\n\n注意 `go-gin-example` 在 `docker` 容器里编译，并没有在宿主机现场编译\n\n#### 说明\n\nDockerfile 文件是用于定义 Docker 镜像生成流程的配置文件，文件内容是一条条指令，每一条指令构建一层，因此每一条指令的内容，就是描述该层应当如何构建；这些指令应用于基础镜像并最终创建一个新的镜像\n\n你可以认为用于快速创建自定义的 Docker 镜像\n\n**1、 FROM**\n\n指定基础镜像（必须有的指令，并且必须是第一条指令）\n\n**2、 WORKDIR**\n\n格式为 `WORKDIR` <工作目录路径>\n\n使用 `WORKDIR` 指令可以来**指定工作目录**（或者称为当前目录），以后各层的当前目录就被改为指定的目录，如果目录不存在，`WORKDIR` 会帮你建立目录\n\n**3、COPY**\n\n格式：\n\n    COPY <源路径>... <目标路径>\n    COPY [\"<源路径1>\",... \"<目标路径>\"]\n\n`COPY` 指令将从构建上下文目录中 <源路径> 的文件/目录**复制**到新的一层的镜像内的 <目标路径> 位置\n\n**4、RUN**\n\n用于执行命令行命令\n\n格式：`RUN` <命令>\n\n**5、EXPOSE**\n\n格式为 `EXPOSE` <端口 1> [<端口 2>...]\n\n`EXPOSE` 指令是**声明运行时容器提供服务端口，这只是一个声明**，在运行时并不会因为这个声明应用就会开启这个端口的服务\n\n在 Dockerfile 中写入这样的声明有两个好处\n\n- 帮助镜像使用者理解这个镜像服务的守护端口，以方便配置映射\n- 运行时使用随机端口映射时，也就是 `docker run -P` 时，会自动随机映射 `EXPOSE` 的端口\n\n**6、ENTRYPOINT**\n\n`ENTRYPOINT` 的格式和 `RUN` 指令格式一样，分为两种格式\n\n- `exec` 格式：\n\n```\n<ENTRYPOINT> \"<CMD>\"\n```\n\n- `shell` 格式：\n\n```\nENTRYPOINT [ \"curl\", \"-s\", \"http://ip.cn\" ]\n```\n\n`ENTRYPOINT` 指令是**指定容器启动程序及参数**\n\n### 二、构建镜像\n\n`go-gin-example` 的项目根目录下**执行** `docker build -t gin-blog-docker .`\n\n该命令作用是创建/构建镜像，`-t` 指定名称为 `gin-blog-docker`，`.` 构建内容为当前上下文目录\n\n```\n$ docker build -t gin-blog-docker .\nSending build context to Docker daemon 96.39 MB\nStep 1/6 : FROM golang:latest\n ---> d632bbfe5767\nStep 2/6 : WORKDIR $GOPATH/src/github.com/EDDYCJY/go-gin-example\n ---> 56294f978c5d\nRemoving intermediate container e112997b995d\nStep 3/6 : COPY . $GOPATH/src/github.com/EDDYCJY/go-gin-example\n ---> 3b60960120cf\nRemoving intermediate container 63e310b3f60c\nStep 4/6 : RUN go build .\n ---> Running in 52648a431450\ngo: downloading github.com/gin-gonic/gin v1.3.0\ngo: downloading github.com/go-ini/ini v1.32.1-0.20180214101753-32e4be5f41bb\ngo: downloading github.com/swaggo/gin-swagger v1.0.1-0.20190110070702-0c6fcfd3c7f3\n...\n ---> 7bfbeb301fea\nRemoving intermediate container 52648a431450\nStep 5/6 : EXPOSE 8000\n ---> Running in 98f5b387d1bb\n ---> b65bd4076c65\nRemoving intermediate container 98f5b387d1bb\nStep 6/6 : ENTRYPOINT ./go-gin-example\n ---> Running in c4f6cdeb667b\n ---> d8a109c7697c\nRemoving intermediate container c4f6cdeb667b\nSuccessfully built d8a109c7697c\n```\n\n### 三、验证镜像\n\n查看所有的镜像，确定刚刚构建的 `gin-blog-docker` 镜像是否存在\n\n```\n$ docker images\nREPOSITORY              TAG                 IMAGE ID            CREATED              SIZE\ngin-blog-docker         latest              d8a109c7697c        About a minute ago   946 MB\ndocker.io/golang        latest              d632bbfe5767        8 days ago           779 MB\n...\n```\n\n### 四、创建并运行一个新容器\n\n执行命令 `docker run -p 8000:8000 gin-blog-docker`\n\n```\n$ docker run -p 8000:8000 gin-blog-docker\ndial tcp 127.0.0.1:3306: connect: connection refused\n[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:\texport GIN_MODE=release\n - using code:\tgin.SetMode(gin.ReleaseMode)\n\n...\nActual pid is 1\n\n```\n\n运行成功，你以为大功告成了吗？\n\n你想太多了，仔细看看控制台的输出了一条错误 `dial tcp 127.0.0.1:3306: connect: connection refused`\n\n我们研判一下，发现是 `Mysql` 的问题，接下来第二项我们将解决这个问题\n\n## Mysql\n\n### 一、拉取镜像\n\n从 `Docker` 的公共仓库 `Dockerhub` 下载 `MySQL` 镜像（国内建议配个镜像）\n\n```\n$ docker pull mysql\n```\n\n### 二、创建并运行一个新容器\n\n运行 `Mysql` 容器，并设置执行成功后返回容器 ID\n\n```\n$ docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=rootroot -d mysql\n8c86ac986da4922492934b6fe074254c9165b8ee3e184d29865921b0fef29e64\n```\n\n#### 连接 Mysql\n\n初始化的 `Mysql` 应该如图\n\n![image](https://i.loli.net/2018/03/23/5ab4caab04cf1.png)\n\n## Golang + Mysql\n\n### 一、删除镜像\n\n由于原本的镜像存在问题，我们需要删除它，此处有几种做法\n\n- 删除原本有问题的镜像，重新构建一个新镜像\n- 重新构建一个不同 `name`、`tag` 的新镜像\n\n删除原本的有问题的镜像，`-f` 是强制删除及其关联状态\n\n若不执行 `-f`，你需要执行 `docker ps -a` 查到所关联的容器，将其 `rm` 解除两者依赖关系\n\n```\n$ docker rmi -f gin-blog-docker\nUntagged: gin-blog-docker:latest\nDeleted: sha256:d8a109c7697c3c2d9b4de7dbb49669d10106902122817b6467a031706bc52ab4\nDeleted: sha256:b65bd4076c65a3c24029ca4def3b3f37001ff7c9eca09e2590c4d29e1e23dce5\nDeleted: sha256:7bfbeb301fea9d8912a4b7c43e4bb8b69bdc57f0b416b372bfb6510e476a7dee\nDeleted: sha256:3b60960120cf619181c1762cdc1b8ce318b8c815e056659809252dd321bcb642\nDeleted: sha256:56294f978c5dfcfa4afa8ad033fd76b755b7ecb5237c6829550741a4d2ce10bc\n```\n\n### 二、修改配置文件\n\n将项目的配置文件 `conf/app.ini`，内容修改为\n\n```ini\n#debug or release\nRUN_MODE = debug\n\n[app]\nPAGE_SIZE = 10\nJWT_SECRET = 233\n\n[server]\nHTTP_PORT = 8000\nREAD_TIMEOUT = 60\nWRITE_TIMEOUT = 60\n\n[database]\nTYPE = mysql\nUSER = root\nPASSWORD = rootroot\nHOST = mysql:3306\nNAME = blog\nTABLE_PREFIX = blog_\n\n```\n\n### 三、重新构建镜像\n\n重复先前的步骤，回到 `gin-blog` 的项目根目录下**执行** `docker build -t gin-blog-docker .`\n\n### 四、创建并运行一个新容器\n\n## 关联\n\nQ：我们需要将 `Golang` 容器和 `Mysql` 容器关联起来，那么我们需要怎么做呢？\n\nA：增加命令 `--link mysql:mysql` 让 `Golang` 容器与 `Mysql` 容器互联；通过 `--link`，**可以在容器内直接使用其关联的容器别名进行访问**，而不通过 IP，但是`--link`只能解决单机容器间的关联，在分布式多机的情况下，需要通过别的方式进行连接\n\n## 运行\n\n执行命令 `docker run --link mysql:mysql -p 8000:8000 gin-blog-docker`\n\n```\n$ docker run --link mysql:mysql -p 8000:8000 gin-blog-docker\n[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:\texport GIN_MODE=release\n - using code:\tgin.SetMode(gin.ReleaseMode)\n...\nActual pid is 1\n```\n\n## 结果\n\n检查启动输出、接口测试、数据库内数据，均正常；我们的 `Golang` 容器和 `Mysql` 容器成功关联运行，大功告成 :)\n\n---\n\n## Review\n\n### 思考\n\n虽然应用已经能够跑起来了\n\n但如果对 `Golang` 和 `Docker` 有一定的了解，我希望你能够想到至少 2 个问题\n\n- 为什么 `gin-blog-docker` 占用空间这么大？（可用 `docker ps -as | grep gin-blog-docker` 查看）\n- `Mysql` 容器直接这么使用，数据存储到哪里去了？\n\n### 创建超小的 Golang 镜像\n\nQ：第一个问题，为什么这么镜像体积这么大？\n\nA：`FROM golang:latest` 拉取的是官方 `golang` 镜像，包含 Golang 的编译和运行环境，外加一堆 GCC、build 工具，相当齐全\n\n这是有问题的，**我们可以不在 Golang 容器中现场编译的**，压根用不到那些东西，我们只需要一个能够运行可执行文件的环境即可\n\n#### 构建 Scratch 镜像\n\nScratch 镜像，简洁、小巧，基本是个空镜像\n\n##### 一、修改 Dockerfile\n\n```\nFROM scratch\n\nWORKDIR $GOPATH/src/github.com/EDDYCJY/go-gin-example\nCOPY . $GOPATH/src/github.com/EDDYCJY/go-gin-example\n\nEXPOSE 8000\nCMD [\"./go-gin-example\"]\n```\n\n##### 二、编译可执行文件\n\n```\nCGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o go-gin-example .\n```\n\n编译所生成的可执行文件会依赖一些库，并且是动态链接。在这里因为使用的是 `scratch` 镜像，它是空镜像，因此我们需要将生成的可执行文件静态链接所依赖的库\n\n##### 三、构建镜像\n\n```\n$ docker build -t gin-blog-docker-scratch .\nSending build context to Docker daemon 133.1 MB\nStep 1/5 : FROM scratch\n --->\nStep 2/5 : WORKDIR $GOPATH/src/github.com/EDDYCJY/go-gin-example\n ---> Using cache\n ---> ee07e166a638\nStep 3/5 : COPY . $GOPATH/src/github.com/EDDYCJY/go-gin-example\n ---> 1489a0693d51\nRemoving intermediate container e3e9efc0fe4d\nStep 4/5 : EXPOSE 8000\n ---> Running in b5630de5544a\n ---> 6993e9f8c944\nRemoving intermediate container b5630de5544a\nStep 5/5 : CMD ./go-gin-example\n ---> Running in eebc0d8628ae\n ---> 5310bebeb86a\nRemoving intermediate container eebc0d8628ae\nSuccessfully built 5310bebeb86a\n```\n\n注意，假设你的 Golang 应用没有依赖任何的配置等文件，是可以直接把可执行文件给拷贝进去即可，其他都不必关心\n\n这里可以有好几种解决方案\n\n- 依赖文件统一管理挂载\n- go-bindata 一下\n\n...\n\n因此这里如果**解决了文件依赖的问题**后，就不需要把目录给 `COPY` 进去了\n\n##### 四、运行\n\n```\n$ docker run --link mysql:mysql -p 8000:8000 gin-blog-docker-scratch\n[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:\texport GIN_MODE=release\n - using code:\tgin.SetMode(gin.ReleaseMode)\n\n[GIN-debug] GET    /auth                     --> github.com/EDDYCJY/go-gin-example/routers/api.GetAuth (3 handlers)\n...\n```\n\n成功运行，程序也正常接收请求\n\n接下来我们再看看占用大小，执行 `docker ps -as` 命令\n\n```\n$ docker ps -as\nCONTAINER ID        IMAGE                     COMMAND                  ...         SIZE\n9ebdba5a8445        gin-blog-docker-scratch   \"./go-gin-example\"       ...     0 B (virtual 132 MB)\n427ee79e6857        gin-blog-docker           \"./go-gin-example\"       ...     0 B (virtual 946 MB)\n```\n\n从结果而言，占用大小以`Scratch`镜像为基础的容器完胜，完成目标\n\n### Mysql 挂载数据卷\n\n倘若不做任何干涉，在每次启动一个 `Mysql` 容器时，数据库都是空的。另外容器删除之后，数据就丢失了（还有各类意外情况），非常糟糕！\n\n#### 数据卷\n\n数据卷 是被设计用来持久化数据的，它的生命周期独立于容器，Docker 不会在容器被删除后自动删除 数据卷，并且也不存在垃圾回收这样的机制来处理没有任何容器引用的 数据卷。如果需要在删除容器的同时移除数据卷。可以在删除容器的时候使用 `docker rm -v` 这个命令\n\n数据卷 是一个可供一个或多个容器使用的特殊目录，它绕过 UFS，可以提供很多有用的特性：\n\n- 数据卷 可以在容器之间共享和重用\n\n- 对 数据卷 的修改会立马生效\n\n- 对 数据卷 的更新，不会影响镜像\n\n- 数据卷 默认会一直存在，即使容器被删除\n\n> 注意：数据卷 的使用，类似于 Linux 下对目录或文件进行 mount，镜像中的被指定为挂载点的目录中的文件会隐藏掉，能显示看的是挂载的 数据卷。\n\n#### 如何挂载\n\n首先创建一个目录用于存放数据卷；示例目录 `/data/docker-mysql`，注意 `--name` 原本名称为 `mysql` 的容器，需要将其删除 `docker rm`\n\n```\n$ docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=rootroot -v /data/docker-mysql:/var/lib/mysql -d mysql\n54611dbcd62eca33fb320f3f624c7941f15697d998f40b24ee535a1acf93ae72\n```\n\n创建成功，检查目录 `/data/docker-mysql`，下面多了不少数据库文件\n\n#### 验证\n\n接下来交由你进行验证，目标是创建一些测试表和数据，然后删除当前容器，重新创建的容器，数据库数据也依然存在（当然了数据卷指向要一致）\n\n我已验证完毕，你呢？\n\n## 参考\n\n### 本系列示例代码\n\n- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)\n\n### 书籍\n\n- [Docker —— 从入门到实践](https://www.gitbook.com/book/yeasy/docker_practice/details)\n\n## 关于\n\n### 修改记录\n\n- 第一版：2018 年 02 月 16 日发布文章\n- 第二版：2019 年 10 月 01 日修改文章\n\n## ？\n\n如果有任何疑问或错误，欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见，如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进。\n\n### 我的公众号\n\n![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)"
  },
  {
    "path": "content/posts/go/gin/2018-03-26-cgo.md",
    "content": "---\n\ntitle:      \"「番外」Golang 交叉编译\"\ndate:       2018-03-26 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n---\n\n## 知识点\n\n- 跨平台编译\n\n## 本文目标\n\n在 [连载九](https://segmentfault.com/a/1190000013960558) 讲解**构建 Scratch 镜像**时，我们编译可执行文件用了另外一个形式的命令，如下：\n\n```\n$ CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o go-gin-example .\n```\n\n我想你可能会有疑问，今天本文会针对这块进行讲解。\n\n## 说明\n\n我们将讲解命令各个参数的作用，希望你在阅读时，将每一项串联起来，你会发现这就是**交叉编译相关的小知识**\n\n也就是 `Golang` 令人心动的特性之一**跨平台编译**\n\n### 一、CGO_ENABLED\n\n**作用：**\n\n用于标识（声明） `cgo` 工具是否可用\n\n**意义：**\n\n存在交叉编译的情况时，`cgo` 工具是不可用的。在标准 go 命令的上下文环境中，交叉编译意味着程序构建环境的目标计算架构的标识与程序运行环境的目标计算架构的标识不同，或者程序构建环境的目标操作系统的标识与程序运行环境的目标操作系统的标识不同\n\n**小结：**\n\n结合案例来说，我们是在宿主机编译的可执行文件，而在 `Scratch` 镜像运行的可执行文件；显然两者的计算机架构、运行环境标识你无法确定它是否一致（毕竟构建的 `docker` 镜像还可以给他人使用），那么我们就要进行交叉编译，而交叉编译不支持 `cgo`，因此这里要禁用掉它\n\n关闭 `cgo` 后，在构建过程中会忽略 `cgo` 并静态链接所有的依赖库，而开启 `cgo` 后，方式将转为动态链接\n\n**补充：**\n\n`golang` 是默认开启 `cgo` 工具的，可执行 `go env` 命令查看\n\n```\n$ go env\nGOARCH=\"amd64\"\nGOBIN=\"\"\nGOCACHE=\"/root/.cache/go-build\"\nGOEXE=\"\"\nGOHOSTARCH=\"amd64\"\nGOHOSTOS=\"linux\"\nGOOS=\"linux\"\n...\nGCCGO=\"gccgo\"\nCC=\"gcc\"\nCXX=\"g++\"\nCGO_ENABLED=\"1\"\n...\n```\n\n### 二、GOOS\n\n用于标识（声明）程序构建环境的目标操作系统\n\n如：\n\n- linux\n- windows\n\n### 三、GOARCH\n\n用于标识（声明）程序构建环境的目标计算架构\n\n若不设置，默认值与程序运行环境的目标计算架构一致（案例就是采用的默认值）\n\n如：\n\n- amd64\n- 386\n\n| 系统          | GOOS    | GOARCH |\n| ------------- | ------- | ------ |\n| Windows 32 位 | windows | 386    |\n| Windows 64 位 | windows | amd64  |\n| OS X 32 位    | darwin  | 386    |\n| OS X 64 位    | darwin  | amd64  |\n| Linux 32 位   | linux   | 386    |\n| Linux 64 位   | linux   | amd64  |\n\n### 四、GOHOSTOS\n\n用于标识（声明）程序运行环境的目标操作系统\n\n### 五、GOHOSTARCH\n\n用于标识（声明）程序运行环境的目标计算架构\n\n### 六、go build\n\n#### -a\n\n强制重新编译，简单来说，就是不利用缓存或已编译好的部分文件，直接所有包都是最新的代码重新编译和关联\n\n#### -installsuffix\n\n**作用：**\n\n在软件包安装的目录中**增加后缀标识**，以保持输出与默认版本分开\n\n**补充：**\n\n如果使用 `-race` 标识，则后缀就会默认设置为 `-race` 标识，用于区别 `race` 和普通的版本\n\n#### -o\n\n指定编译后的可执行文件名称\n\n### 小结\n\n大部分参数指令，都有一定关联性，且与交叉编译的知识点相关，可以好好品味一下\n\n最后可以看看 `go build help` 加深了解\n\n```\n$ go help build\nusage: go build [-o output] [-i] [build flags] [packages]\n...\n\t-a\n\t\tforce rebuilding of packages that are already up-to-date.\n\t-n\n\t\tprint the commands but do not run them.\n\t-p n\n\t\tthe number of programs, such as build commands or\n\t\ttest binaries, that can be run in parallel.\n\t\tThe default is the number of CPUs available.\n\t-race\n\t\tenable data race detection.\n\t\tSupported only on linux/amd64, freebsd/amd64, darwin/amd64 and windows/amd64.\n\t-msan\n\t\tenable interoperation with memory sanitizer.\n\t\tSupported only on linux/amd64,\n\t\tand only with Clang/LLVM as the host C compiler.\n\t-v\n\t\tprint the names of packages as they are compiled.\n\t-work\n\t\tprint the name of the temporary work directory and\n\t\tdo not delete it when exiting.\n\t-x\n\t\tprint the commands.\n\n\t-asmflags '[pattern=]arg list'\n\t\targuments to pass on each go tool asm invocation.\n\t-buildmode mode\n\t\tbuild mode to use. See 'go help buildmode' for more.\n\t-compiler name\n\t\tname of compiler to use, as in runtime.Compiler (gccgo or gc).\n\t-gccgoflags '[pattern=]arg list'\n\t\targuments to pass on each gccgo compiler/linker invocation.\n\t-gcflags '[pattern=]arg list'\n\t\targuments to pass on each go tool compile invocation.\n\t-installsuffix suffix\n\t\ta suffix to use in the name of the package installation directory,\n\t\tin order to keep output separate from default builds.\n\t\tIf using the -race flag, the install suffix is automatically set to race\n\t\tor, if set explicitly, has _race appended to it. Likewise for the -msan\n\t\tflag. Using a -buildmode option that requires non-default compile flags\n\t\thas a similar effect.\n\t-ldflags '[pattern=]arg list'\n\t\targuments to pass on each go tool link invocation.\n\t-linkshared\n\t\tlink against shared libraries previously created with\n\t\t-buildmode=shared.\n\t-pkgdir dir\n\t\tinstall and load all packages from dir instead of the usual locations.\n\t\tFor example, when building with a non-standard configuration,\n\t\tuse -pkgdir to keep generated packages in a separate location.\n\t-tags 'tag list'\n\t\ta space-separated list of build tags to consider satisfied during the\n\t\tbuild. For more information about build tags, see the description of\n\t\tbuild constraints in the documentation for the go/build package.\n\t-toolexec 'cmd args'\n\t\ta program to use to invoke toolchain programs like vet and asm.\n\t\tFor example, instead of running asm, the go command will run\n\t\t'cmd args /path/to/asm <arguments for asm>'.\n...\n```\n\n## 参考\n\n### 本系列示例代码\n\n- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)\n\n### 书籍\n\n- Go 并发编程实战 第二版\n\n## 关于\n\n### 修改记录\n\n- 第一版：2018 年 02 月 16 日发布文章\n- 第二版：2019 年 10 月 01 日修改文章\n\n## ？\n\n如果有任何疑问或错误，欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见，如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进。\n\n### 我的公众号\n\n![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)"
  },
  {
    "path": "content/posts/go/gin/2018-04-15-gorm-callback.md",
    "content": "---\n\ntitle:      \"「连载十」定制 GORM Callbacks\"\ndate:       2018-04-15 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n---\n\n## 涉及知识点\n\n- GORM\n\n## 本文目标\n\n> GORM itself is powered by Callbacks, so you could fully customize GORM as you want\n\nGORM 本身是由回调驱动的，所以我们可以根据需要完全定制 GORM，以此达到我们的目的，如下：\n\n- 注册一个新的回调\n- 删除现有的回调\n- 替换现有的回调\n- 注册回调的顺序\n\n在 GORM 中包含以上四类 Callbacks，我们结合项目选用 “替换现有的回调” 来解决一个小痛点。\n\n## 问题\n\n在 models 目录下，我们包含 tag.go 和 article.go 两个文件，他们有一个问题，就是 BeforeCreate、BeforeUpdate 重复出现了，那难道 100 个文件，就要写一百次吗？\n\n1、tag.go\n\n![image](https://i.loli.net/2018/04/14/5ad20efdba409.jpg)\n\n2、article.go\n\n![image](https://i.loli.net/2018/04/14/5ad20ebacc4c9.jpg)\n\n显然这是不可能的，如果先前你已经意识到这个问题，那挺 OK，但没有的话，现在开始就要改\n\n### 解决\n\n在这里我们通过 Callbacks 来实现功能，不需要一个个文件去编写\n\n### 实现 Callbacks\n\n打开 models 目录下的 models.go 文件，实现以下两个方法：\n\n1、updateTimeStampForCreateCallback\n\n```go\n// updateTimeStampForCreateCallback will set `CreatedOn`, `ModifiedOn` when creating\nfunc updateTimeStampForCreateCallback(scope *gorm.Scope) {\n    if !scope.HasError() {\n        nowTime := time.Now().Unix()\n        if createTimeField, ok := scope.FieldByName(\"CreatedOn\"); ok {\n            if createTimeField.IsBlank {\n                createTimeField.Set(nowTime)\n            }\n        }\n\n        if modifyTimeField, ok := scope.FieldByName(\"ModifiedOn\"); ok {\n            if modifyTimeField.IsBlank {\n                modifyTimeField.Set(nowTime)\n            }\n        }\n    }\n}\n```\n\n在这段方法中，会完成以下功能\n\n- 检查是否有含有错误（db.Error）\n- `scope.FieldByName` 通过 `scope.Fields()` 获取所有字段，判断当前是否包含所需字段\n\n```go\nfor _, field := range scope.Fields() {\n    if field.Name == name || field.DBName == name {\n        return field, true\n    }\n    if field.DBName == dbName {\n        mostMatchedField = field\n    }\n}\n```\n\n- `field.IsBlank` 可判断该字段的值是否为空\n\n```go\nfunc isBlank(value reflect.Value) bool {\n    switch value.Kind() {\n    case reflect.String:\n        return value.Len() == 0\n    case reflect.Bool:\n        return !value.Bool()\n    case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:\n        return value.Int() == 0\n    case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:\n        return value.Uint() == 0\n    case reflect.Float32, reflect.Float64:\n        return value.Float() == 0\n    case reflect.Interface, reflect.Ptr:\n        return value.IsNil()\n    }\n\n    return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface())\n}\n```\n\n- 若为空则 `field.Set` 用于给该字段设置值，参数为 `interface{}`\n\n2、updateTimeStampForUpdateCallback\n\n```go\n// updateTimeStampForUpdateCallback will set `ModifyTime` when updating\nfunc updateTimeStampForUpdateCallback(scope *gorm.Scope) {\n    if _, ok := scope.Get(\"gorm:update_column\"); !ok {\n        scope.SetColumn(\"ModifiedOn\", time.Now().Unix())\n    }\n}\n```\n\n- `scope.Get(...)` 根据入参获取设置了字面值的参数，例如本文中是 `gorm:update_column` ，它会去查找含这个字面值的字段属性\n- `scope.SetColumn(...)` 假设没有指定 `update_column` 的字段，我们默认在更新回调设置 `ModifiedOn` 的值\n\n### 注册 Callbacks\n\n在上面小节我已经把回调方法编写好了，接下来需要将其注册进 GORM 的钩子里，但其本身自带 Create 和 Update 回调，因此调用替换即可\n\n在 models.go 的 init 函数中，增加以下语句\n\n```go\ndb.Callback().Create().Replace(\"gorm:update_time_stamp\", updateTimeStampForCreateCallback)\ndb.Callback().Update().Replace(\"gorm:update_time_stamp\", updateTimeStampForUpdateCallback)\n```\n\n### 验证\n\n访问 AddTag 接口，成功后检查数据库，可发现 `created_on` 和 `modified_on` 字段都为当前执行时间\n\n访问 EditTag 接口，可发现 `modified_on` 为最后一次执行更新的时间\n\n## 拓展\n\n我们想到，在实际项目中硬删除是较少存在的，那么是否可以通过 Callbacks 来完成这个功能呢？\n\n答案是可以的，我们在先前 `Model struct` 增加 `DeletedOn` 变量\n\n```go\ntype Model struct {\n    ID int `gorm:\"primary_key\" json:\"id\"`\n    CreatedOn int `json:\"created_on\"`\n    ModifiedOn int `json:\"modified_on\"`\n    DeletedOn int `json:\"deleted_on\"`\n}\n```\n\n### 实现 Callbacks\n\n打开 models 目录下的 models.go 文件，实现以下方法：\n\n```go\nfunc deleteCallback(scope *gorm.Scope) {\n    if !scope.HasError() {\n        var extraOption string\n        if str, ok := scope.Get(\"gorm:delete_option\"); ok {\n            extraOption = fmt.Sprint(str)\n        }\n\n        deletedOnField, hasDeletedOnField := scope.FieldByName(\"DeletedOn\")\n\n        if !scope.Search.Unscoped && hasDeletedOnField {\n            scope.Raw(fmt.Sprintf(\n                \"UPDATE %v SET %v=%v%v%v\",\n                scope.QuotedTableName(),\n                scope.Quote(deletedOnField.DBName),\n                scope.AddToVars(time.Now().Unix()),\n                addExtraSpaceIfExist(scope.CombinedConditionSql()),\n                addExtraSpaceIfExist(extraOption),\n            )).Exec()\n        } else {\n            scope.Raw(fmt.Sprintf(\n                \"DELETE FROM %v%v%v\",\n                scope.QuotedTableName(),\n                addExtraSpaceIfExist(scope.CombinedConditionSql()),\n                addExtraSpaceIfExist(extraOption),\n            )).Exec()\n        }\n    }\n}\n\nfunc addExtraSpaceIfExist(str string) string {\n    if str != \"\" {\n        return \" \" + str\n    }\n    return \"\"\n}\n```\n\n- `scope.Get(\"gorm:delete_option\")` 检查是否手动指定了 delete_option\n- `scope.FieldByName(\"DeletedOn\")` 获取我们约定的删除字段，若存在则 `UPDATE` 软删除，若不存在则 `DELETE` 硬删除\n- `scope.QuotedTableName()` 返回引用的表名，这个方法 GORM 会根据自身逻辑对表名进行一些处理\n- `scope.CombinedConditionSql()` 返回组合好的条件 SQL，看一下方法原型很明了\n\n```go\nfunc (scope *Scope) CombinedConditionSql() string {\n    joinSQL := scope.joinsSQL()\n    whereSQL := scope.whereSQL()\n    if scope.Search.raw {\n        whereSQL = strings.TrimSuffix(strings.TrimPrefix(whereSQL, \"WHERE (\"), \")\")\n    }\n    return joinSQL + whereSQL + scope.groupSQL() +\n        scope.havingSQL() + scope.orderSQL() + scope.limitAndOffsetSQL()\n}\n```\n\n- `scope.AddToVars` 该方法可以添加值作为 SQL 的参数，也可用于防范 SQL 注入\n\n```go\nfunc (scope *Scope) AddToVars(value interface{}) string {\n    _, skipBindVar := scope.InstanceGet(\"skip_bindvar\")\n\n    if expr, ok := value.(*expr); ok {\n        exp := expr.expr\n        for _, arg := range expr.args {\n            if skipBindVar {\n                scope.AddToVars(arg)\n            } else {\n                exp = strings.Replace(exp, \"?\", scope.AddToVars(arg), 1)\n            }\n        }\n        return exp\n    }\n\n    scope.SQLVars = append(scope.SQLVars, value)\n\n    if skipBindVar {\n        return \"?\"\n    }\n    return scope.Dialect().BindVar(len(scope.SQLVars))\n}\n```\n\n### 注册 Callbacks\n\n在 models.go 的 init 函数中，增加以下删除的回调\n\n```go\ndb.Callback().Delete().Replace(\"gorm:delete\", deleteCallback)\n```\n\n### 验证\n\n重启服务，访问 DeleteTag 接口，成功后即可发现 deleted_on 字段有值\n\n## 小结\n\n在这一章节中，我们结合 GORM 完成了新增、更新、查询的 Callbacks，在实际项目中常常也是这么使用\n\n毕竟，一个钩子的事，就没有必要自己手写过多不必要的代码了\n\n（注意，增加了软删除后，先前的代码需要增加 `deleted_on` 的判断）\n\n## 参考\n\n### 本系列示例代码\n\n- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)\n\n### 文档\n\n- [gorm](http://gorm.io/docs/write_plugins.html)\n\n## 关于\n\n### 修改记录\n\n- 第一版：2018 年 02 月 16 日发布文章\n- 第二版：2019 年 10 月 01 日修改文章\n\n## ？\n\n如果有任何疑问或错误，欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见，如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进。\n\n### 我的公众号\n\n![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)"
  },
  {
    "path": "content/posts/go/gin/2018-04-29-cron.md",
    "content": "---\n\ntitle:      \"「连载十一」Cron定时任务\"\ndate:       2018-04-29 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n---\n\n## 知识点\n\n- 完成定时任务的功能\n\n## 本文目标\n\n在实际的应用项目中，定时任务的使用是很常见的。你是否有过 Golang 如何做定时任务的疑问，莫非是轮询，在本文中我们将结合我们的项目讲述 Cron。\n\n## 介绍\n\n我们将使用 [cron](https://github.com/robfig/cron) 这个包，它实现了 cron 规范解析器和任务运行器，简单来讲就是包含了定时任务所需的功能\n\n### Cron 表达式格式\n\n| 字段名                         | 是否必填 | 允许的值        | 允许的特殊字符 |\n| ------------------------------ | -------- | --------------- | -------------- |\n| 秒（Seconds）                  | Yes      | 0-59            | \\* / , -       |\n| 分（Minutes）                  | Yes      | 0-59            | \\* / , -       |\n| 时（Hours）                    | Yes      | 0-23            | \\* / , -       |\n| 一个月中的某天（Day of month） | Yes      | 1-31            | \\* / , - ?     |\n| 月（Month）                    | Yes      | 1-12 or JAN-DEC | \\* / , -       |\n| 星期几（Day of week）          | Yes      | 0-6 or SUN-SAT  | \\* / , - ?     |\n\nCron 表达式表示一组时间，使用 6 个空格分隔的字段\n\n可以留意到 Golang 的 Cron 比 Crontab 多了一个秒级，以后遇到秒级要求的时候就省事了\n\n### Cron 特殊字符\n\n1、星号 ( \\* )\n\n星号表示将匹配字段的所有值\n\n2、斜线 ( / )\n\n斜线用户 描述范围的增量，表现为 “N-MAX/x”，first-last/x 的形式，例如 3-59/15 表示此时的第三分钟和此后的每 15 分钟，到 59 分钟为止。即从 N 开始，使用增量直到该特定范围结束。它不会重复\n\n3、逗号 ( , )\n\n逗号用于分隔列表中的项目。例如，在 Day of week 使用“MON，WED，FRI”将意味着星期一，星期三和星期五\n\n4、连字符 ( - )\n\n连字符用于定义范围。例如，9 - 17 表示从上午 9 点到下午 5 点的每个小时\n\n5、问号 ( ? )\n\n不指定值，用于代替 “ \\* ”，类似 “ \\_ ” 的存在，不难理解\n\n### 预定义的 Cron 时间表\n\n| 输入                   | 简述                                   | 相当于          |\n| ---------------------- | -------------------------------------- | --------------- |\n| @yearly (or @annually) | 1 月 1 日午夜运行一次                  | 0 0 0 1 1 \\*    |\n| @monthly               | 每个月的午夜，每个月的第一个月运行一次 | 0 0 0 1 \\* \\*   |\n| @weekly                | 每周一次，周日午夜运行一次             | 0 0 0 \\* \\* 0   |\n| @daily (or @midnight)  | 每天午夜运行一次                       | 0 0 0 \\* \\* \\*  |\n| @hourly                | 每小时运行一次                         | 0 0 \\* \\* \\* \\* |\n\n## 安装\n\n```\n$ go get -u github.com/robfig/cron\n```\n\n## 实践\n\n在上一章节 [Gin 实践 连载十 定制 GORM Callbacks](https://segmentfault.com/a/1190000014393602) 中，我们使用了 GORM 的回调实现了软删除，同时也引入了另外一个问题\n\n就是我怎么硬删除，我什么时候硬删除？这个往往与业务场景有关系，大致为\n\n- 另外有一套硬删除接口\n- 定时任务清理（或转移、backup）无效数据\n\n在这里我们选用第二种解决方案来进行实践\n\n### 编写硬删除代码\n\n打开 models 目录下的 tag.go、article.go 文件，分别添加以下代码\n\n1、tag.go\n\n```go\nfunc CleanAllTag() bool {\n\tdb.Unscoped().Where(\"deleted_on != ? \", 0).Delete(&Tag{})\n\n\treturn true\n}\n```\n\n2、article.go\n\n```go\nfunc CleanAllArticle() bool {\n\tdb.Unscoped().Where(\"deleted_on != ? \", 0).Delete(&Article{})\n\n\treturn true\n}\n\n```\n\n注意硬删除要使用 `Unscoped()`，这是 GORM 的约定\n\n### 编写 Cron\n\n在 项目根目录下新建 cron.go 文件，用于编写定时任务的代码，写入文件内容\n\n```go\npackage main\n\nimport (\n\t\"time\"\n\t\"log\"\n\n\t\"github.com/robfig/cron\"\n\n\t\"github.com/EDDYCJY/go-gin-example/models\"\n)\n\nfunc main() {\n\tlog.Println(\"Starting...\")\n\n\tc := cron.New()\n\tc.AddFunc(\"* * * * * *\", func() {\n\t\tlog.Println(\"Run models.CleanAllTag...\")\n\t\tmodels.CleanAllTag()\n\t})\n\tc.AddFunc(\"* * * * * *\", func() {\n\t\tlog.Println(\"Run models.CleanAllArticle...\")\n\t\tmodels.CleanAllArticle()\n\t})\n\n\tc.Start()\n\n\tt1 := time.NewTimer(time.Second * 10)\n\tfor {\n\t\tselect {\n\t\tcase <-t1.C:\n\t\t\tt1.Reset(time.Second * 10)\n\t\t}\n\t}\n}\n```\n\n在这段程序中，我们做了如下的事情\n\n#### cron.New()\n\n会根据本地时间创建一个新（空白）的 Cron job runner\n\n```go\nfunc New() *Cron {\n\treturn NewWithLocation(time.Now().Location())\n}\n\n// NewWithLocation returns a new Cron job runner.\nfunc NewWithLocation(location *time.Location) *Cron {\n\treturn &Cron{\n\t\tentries:  nil,\n\t\tadd:      make(chan *Entry),\n\t\tstop:     make(chan struct{}),\n\t\tsnapshot: make(chan []*Entry),\n\t\trunning:  false,\n\t\tErrorLog: nil,\n\t\tlocation: location,\n\t}\n}\n```\n\n#### c.AddFunc()\n\nAddFunc 会向 Cron job runner 添加一个 func ，以按给定的时间表运行\n\n```go\nfunc (c *Cron) AddJob(spec string, cmd Job) error {\n\tschedule, err := Parse(spec)\n\tif err != nil {\n\t\treturn err\n\t}\n\tc.Schedule(schedule, cmd)\n\treturn nil\n}\n```\n\n会首先解析时间表，如果填写有问题会直接 err，无误则将 func 添加到 Schedule 队列中等待执行\n\n```go\nfunc (c *Cron) Schedule(schedule Schedule, cmd Job) {\n\tentry := &Entry{\n\t\tSchedule: schedule,\n\t\tJob:      cmd,\n\t}\n\tif !c.running {\n\t\tc.entries = append(c.entries, entry)\n\t\treturn\n\t}\n\n\tc.add <- entry\n}\n```\n\n3、c.Start()\n\n在当前执行的程序中启动 Cron 调度程序。其实这里的主体是 goroutine + for + select + timer 的调度控制哦\n\n```go\nfunc (c *Cron) Run() {\n\tif c.running {\n\t\treturn\n\t}\n\tc.running = true\n\tc.run()\n}\n```\n\n#### time.NewTimer + for + select + t1.Reset\n\n如果你是初学者，大概会有疑问，这是干嘛用的？\n\n**（1）time.NewTimer **\n\n会创建一个新的定时器，持续你设定的时间 d 后发送一个 channel 消息\n\n**（2）for + select**\n\n阻塞 select 等待 channel\n\n**（3）t1.Reset**\n\n会重置定时器，让它重新开始计时\n\n注：本文适用于 “t.C 已经取走，可直接使用 Reset”。\n\n---\n\n总的来说，这段程序是为了阻塞主程序而编写的，希望你带着疑问来想，有没有别的办法呢？\n\n有的，你直接 `select{}` 也可以完成这个需求 :)\n\n## 验证\n\n```\n$ go run cron.go\n2018/04/29 17:03:34 [info] replacing callback `gorm:update_time_stamp` from /Users/eddycjy/go/src/github.com/EDDYCJY/go-gin-example/models/models.go:56\n2018/04/29 17:03:34 [info] replacing callback `gorm:update_time_stamp` from /Users/eddycjy/go/src/github.com/EDDYCJY/go-gin-example/models/models.go:57\n2018/04/29 17:03:34 [info] replacing callback `gorm:delete` from /Users/eddycjy/go/src/github.com/EDDYCJY/go-gin-example/models/models.go:58\n2018/04/29 17:03:34 Starting...\n2018/04/29 17:03:35 Run models.CleanAllArticle...\n2018/04/29 17:03:35 Run models.CleanAllTag...\n2018/04/29 17:03:36 Run models.CleanAllArticle...\n2018/04/29 17:03:36 Run models.CleanAllTag...\n2018/04/29 17:03:37 Run models.CleanAllTag...\n2018/04/29 17:03:37 Run models.CleanAllArticle...\n```\n\n检查输出日志正常，模拟已软删除的数据，定时任务工作 OK\n\n## 小结\n\n定时任务很常见，希望你通过本文能够熟知 Golang 怎么实现一个简单的定时任务调度管理\n\n可以不依赖系统的 Crontab 设置，指不定哪一天就用上了呢\n\n## 问题\n\n如果你手动修改计算机的系统时间，是会导致定时任务错乱的，所以一般不要乱来。\n\n## 参考\n\n### 本系列示例代码\n\n- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)\n\n## 关于\n\n### 修改记录\n\n- 第一版：2018 年 02 月 16 日发布文章\n- 第二版：2019 年 10 月 02 日修改文章\n\n## ？\n\n如果有任何疑问或错误，欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见，如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进。\n\n### 我的公众号\n\n![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)\n"
  },
  {
    "path": "content/posts/go/gin/2018-05-27-config-upload.md",
    "content": "---\n\ntitle:      \"「连载十二」优化配置结构及实现图片上传\"\ndate:       2018-05-27 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n---\n\n## 知识点\n\n- 重构、调整结构\n\n## 本文目标\n\n这个应用程序跑了那么久了，越来越大，越来越壮，仿佛我们的产品一样，现在它需要进行小范围重构了，以便于后续的使用，这非常重要。\n\n## 前言\n\n一天，产品经理突然跟你说文章列表，没有封面图，不够美观，！）&￥*！&）#&￥*！加一个吧，几分钟的事\n\n你打开你的程序，分析了一波写了个清单：\n\n- 优化配置结构（因为配置项越来越多）\n- 抽离 原 logging 的 File 便于公用（logging、upload 各保有一份并不合适）\n- 实现上传图片接口（需限制文件格式、大小）\n- 修改文章接口（需支持封面地址参数）\n- 增加 blog_article （文章）的数据库字段\n- 实现 http.FileServer\n\n嗯，你发现要较优的话，需要调整部分的应用程序结构，因为功能越来越多，原本的设计也要跟上节奏\n\n也就是在适当的时候，及时优化\n\n## 优化配置结构\n\n### 一、讲解\n\n在先前章节中，采用了直接读取 KEY 的方式去存储配置项，而本次需求中，需要增加图片的配置项，总体就有些冗余了\n\n我们采用以下解决方法：\n\n- 映射结构体：使用 MapTo 来设置配置参数\n- 配置统管：所有的配置项统管到 setting 中\n\n#### 映射结构体（示例）\n\n在 go-ini 中可以采用 MapTo 的方式来映射结构体，例如：\n\n```go\ntype Server struct {\n\tRunMode string\n\tHttpPort int\n\tReadTimeout time.Duration\n\tWriteTimeout time.Duration\n}\n\nvar ServerSetting = &Server{}\n\nfunc main() {\n    Cfg, err := ini.Load(\"conf/app.ini\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Fail to parse 'conf/app.ini': %v\", err)\n\t}\n\n\terr = Cfg.Section(\"server\").MapTo(ServerSetting)\n\tif err != nil {\n\t\tlog.Fatalf(\"Cfg.MapTo ServerSetting err: %v\", err)\n\t}\n}\n```\n\n在这段代码中，可以注意 ServerSetting 取了地址，为什么 MapTo 必须地址入参呢？\n\n```go\n// MapTo maps section to given struct.\nfunc (s *Section) MapTo(v interface{}) error {\n\ttyp := reflect.TypeOf(v)\n\tval := reflect.ValueOf(v)\n\tif typ.Kind() == reflect.Ptr {\n\t\ttyp = typ.Elem()\n\t\tval = val.Elem()\n\t} else {\n\t\treturn errors.New(\"cannot map to non-pointer struct\")\n\t}\n\n\treturn s.mapTo(val, false)\n}\n```\n\n在 MapTo 中 `typ.Kind() == reflect.Ptr` 约束了必须使用指针，否则会返回 `cannot map to non-pointer struct` 的错误。这个是表面原因\n\n更往内探究，可以认为是 `field.Set` 的原因，当执行 `val := reflect.ValueOf(v)` ，函数通过传递 `v` 拷贝创建了 `val`，但是 `val` 的改变并不能更改原始的 `v`，要想 `val` 的更改能作用到 `v`，则必须传递 `v` 的地址\n\n显然 go-ini 里也是包含修改原始值这一项功能的，你觉得是什么原因呢？\n\n#### 配置统管\n\n在先前的版本中，models 和 file 的配置是在自己的文件中解析的，而其他在 setting.go 中，因此我们需要将其在 setting 中统一接管\n\n你可能会想，直接把两者的配置项复制粘贴到 setting.go 的 init 中，一下子就完事了，搞那么麻烦？\n\n但你在想想，先前的代码中存在多个 init 函数，执行顺序存在问题，无法达到我们的要求，你可以试试\n\n（此处是一个基础知识点）\n\n在 Go 中，当存在多个 init 函数时，执行顺序为：\n\n- 相同包下的 init 函数：按照源文件编译顺序决定执行顺序（默认按文件名排序）\n- 不同包下的 init 函数：按照包导入的依赖关系决定先后顺序\n\n所以要避免多 init 的情况，**尽量由程序把控初始化的先后顺序**\n\n### 二、落实\n\n#### 修改配置文件\n\n打开 conf/app.ini 将配置文件修改为大驼峰命名，另外我们增加了 5 个配置项用于上传图片的功能，4 个文件日志方面的配置项\n\n```ini\n[app]\nPageSize = 10\nJwtSecret = 233\n\nRuntimeRootPath = runtime/\n\nImagePrefixUrl = http://127.0.0.1:8000\nImageSavePath = upload/images/\n# MB\nImageMaxSize = 5\nImageAllowExts = .jpg,.jpeg,.png\n\nLogSavePath = logs/\nLogSaveName = log\nLogFileExt = log\nTimeFormat = 20060102\n\n[server]\n#debug or release\nRunMode = debug\nHttpPort = 8000\nReadTimeout = 60\nWriteTimeout = 60\n\n[database]\nType = mysql\nUser = root\nPassword = rootroot\nHost = 127.0.0.1:3306\nName = blog\nTablePrefix = blog_\n```\n\n#### 优化配置读取及设置初始化顺序\n\n##### 第一步\n\n将散落在其他文件里的配置都删掉，**统一在 setting 中处理**以及**修改 init 函数为 Setup 方法**\n\n打开 pkg/setting/setting.go 文件，修改如下：\n\n```go\npackage setting\n\nimport (\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/go-ini/ini\"\n)\n\ntype App struct {\n\tJwtSecret string\n\tPageSize int\n\tRuntimeRootPath string\n\n\tImagePrefixUrl string\n\tImageSavePath string\n\tImageMaxSize int\n\tImageAllowExts []string\n\n\tLogSavePath string\n\tLogSaveName string\n\tLogFileExt string\n\tTimeFormat string\n}\n\nvar AppSetting = &App{}\n\ntype Server struct {\n\tRunMode string\n\tHttpPort int\n\tReadTimeout time.Duration\n\tWriteTimeout time.Duration\n}\n\nvar ServerSetting = &Server{}\n\ntype Database struct {\n\tType string\n\tUser string\n\tPassword string\n\tHost string\n\tName string\n\tTablePrefix string\n}\n\nvar DatabaseSetting = &Database{}\n\nfunc Setup() {\n\tCfg, err := ini.Load(\"conf/app.ini\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Fail to parse 'conf/app.ini': %v\", err)\n\t}\n\n\terr = Cfg.Section(\"app\").MapTo(AppSetting)\n\tif err != nil {\n\t\tlog.Fatalf(\"Cfg.MapTo AppSetting err: %v\", err)\n\t}\n\n\tAppSetting.ImageMaxSize = AppSetting.ImageMaxSize * 1024 * 1024\n\n\terr = Cfg.Section(\"server\").MapTo(ServerSetting)\n\tif err != nil {\n\t\tlog.Fatalf(\"Cfg.MapTo ServerSetting err: %v\", err)\n\t}\n\n\tServerSetting.ReadTimeout = ServerSetting.ReadTimeout * time.Second\n\tServerSetting.WriteTimeout = ServerSetting.WriteTimeout * time.Second\n\n\terr = Cfg.Section(\"database\").MapTo(DatabaseSetting)\n\tif err != nil {\n\t\tlog.Fatalf(\"Cfg.MapTo DatabaseSetting err: %v\", err)\n\t}\n}\n```\n\n在这里，我们做了如下几件事：\n\n- 编写与配置项保持一致的结构体（App、Server、Database）\n- 使用 MapTo 将配置项映射到结构体上\n- 对一些需特殊设置的配置项进行再赋值\n\n**需要你去做的事：**\n\n- 将 [models.go](https://github.com/EDDYCJY/go-gin-example/blob/a338ddec103c9506b4c7ed16d9f5386040d99b4b/models/models.go#L23)、[setting.go](https://github.com/EDDYCJY/go-gin-example/blob/a338ddec103c9506b4c7ed16d9f5386040d99b4b/pkg/setting/setting.go#L23)、[pkg/logging/log.go](https://github.com/EDDYCJY/go-gin-example/blob/a338ddec103c9506b4c7ed16d9f5386040d99b4b/pkg/logging/log.go#L32-L37) 的 init 函数修改为 Setup 方法\n- 将 [models/models.go](https://github.com/EDDYCJY/go-gin-example/blob/a338ddec103c9506b4c7ed16d9f5386040d99b4b/models/models.go#L23-L39) 独立读取的 DB 配置项删除，改为统一读取 setting\n- 将 [pkg/logging/file](https://github.com/EDDYCJY/go-gin-example/blob/a338ddec103c9506b4c7ed16d9f5386040d99b4b/pkg/logging/file.go#L10-L15) 独立的 LOG 配置项删除，改为统一读取 setting\n\n这几项比较基础，并没有贴出来，我希望你可以自己动手，有问题的话可右拐 [项目地址](https://github.com/EDDYCJY/go-gin-example)\n\n##### 第二步\n\n在这一步我们要设置初始化的流程，打开 main.go 文件，修改内容：\n\n```go\nfunc main() {\n\tsetting.Setup()\n\tmodels.Setup()\n\tlogging.Setup()\n\n\tendless.DefaultReadTimeOut = setting.ServerSetting.ReadTimeout\n\tendless.DefaultWriteTimeOut = setting.ServerSetting.WriteTimeout\n\tendless.DefaultMaxHeaderBytes = 1 << 20\n\tendPoint := fmt.Sprintf(\":%d\", setting.ServerSetting.HttpPort)\n\n\tserver := endless.NewServer(endPoint, routers.InitRouter())\n\tserver.BeforeBegin = func(add string) {\n\t\tlog.Printf(\"Actual pid is %d\", syscall.Getpid())\n\t}\n\n\terr := server.ListenAndServe()\n\tif err != nil {\n\t\tlog.Printf(\"Server err: %v\", err)\n\t}\n}\n```\n\n修改完毕后，就成功将多模块的初始化函数放到启动流程中了（先后顺序也可以控制）\n\n##### 验证\n\n在这里为止，针对本需求的配置优化就完毕了，你需要执行 `go run main.go` 验证一下你的功能是否正常哦\n\n顺带留个基础问题，大家可以思考下\n\n```go\nServerSetting.ReadTimeout = ServerSetting.ReadTimeout * time.Second\nServerSetting.WriteTimeout = ServerSetting.ReadTimeout * time.Second\n```\n\n若将 setting.go 文件中的这两行删除，会出现什么问题，为什么呢？\n\n## 抽离 File\n\n在先前版本中，在 [logging/file.go](https://github.com/EDDYCJY/go-gin-example/blob/a338ddec103c9506b4c7ed16d9f5386040d99b4b/pkg/logging/file.go) 中使用到了 os 的一些方法，我们通过前期规划发现，这部分在上传图片功能中可以复用\n\n### 第一步\n\n在 pkg 目录下新建 file/file.go ，写入文件内容如下：\n\n```go\npackage file\n\nimport (\n\t\"os\"\n\t\"path\"\n\t\"mime/multipart\"\n\t\"io/ioutil\"\n)\n\nfunc GetSize(f multipart.File) (int, error) {\n\tcontent, err := ioutil.ReadAll(f)\n\n\treturn len(content), err\n}\n\nfunc GetExt(fileName string) string {\n\treturn path.Ext(fileName)\n}\n\nfunc CheckNotExist(src string) bool {\n\t_, err := os.Stat(src)\n\n\treturn os.IsNotExist(err)\n}\n\nfunc CheckPermission(src string) bool {\n\t_, err := os.Stat(src)\n\n\treturn os.IsPermission(err)\n}\n\nfunc IsNotExistMkDir(src string) error {\n\tif notExist := CheckNotExist(src); notExist == true {\n\t\tif err := MkDir(src); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc MkDir(src string) error {\n\terr := os.MkdirAll(src, os.ModePerm)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc Open(name string, flag int, perm os.FileMode) (*os.File, error) {\n\tf, err := os.OpenFile(name, flag, perm)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn f, nil\n}\n```\n\n在这里我们一共封装了 7 个 方法\n\n- GetSize：获取文件大小\n- GetExt：获取文件后缀\n- CheckNotExist：检查文件是否存在\n- CheckPermission：检查文件权限\n- IsNotExistMkDir：如果不存在则新建文件夹\n- MkDir：新建文件夹\n- Open：打开文件\n\n在这里我们用到了 `mime/multipart` 包，它主要实现了 MIME 的 multipart 解析，主要适用于 [HTTP](https://tools.ietf.org/html/rfc2388) 和常见浏览器生成的 multipart 主体\n\nmultipart 又是什么，[rfc2388](https://tools.ietf.org/html/rfc2388) 的 multipart/form-data 了解一下\n\n### 第二步\n\n我们在第一步已经将 file 重新封装了一层，在这一步我们将原先 logging 包的方法都修改掉\n\n1、打开 pkg/logging/file.go 文件，修改文件内容：\n\n```go\npackage logging\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/EDDYCJY/go-gin-example/pkg/setting\"\n\t\"github.com/EDDYCJY/go-gin-example/pkg/file\"\n)\n\nfunc getLogFilePath() string {\n\treturn fmt.Sprintf(\"%s%s\", setting.AppSetting.RuntimeRootPath, setting.AppSetting.LogSavePath)\n}\n\nfunc getLogFileName() string {\n\treturn fmt.Sprintf(\"%s%s.%s\",\n\t\tsetting.AppSetting.LogSaveName,\n\t\ttime.Now().Format(setting.AppSetting.TimeFormat),\n\t\tsetting.AppSetting.LogFileExt,\n\t)\n}\n\nfunc openLogFile(fileName, filePath string) (*os.File, error) {\n\tdir, err := os.Getwd()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"os.Getwd err: %v\", err)\n\t}\n\n\tsrc := dir + \"/\" + filePath\n\tperm := file.CheckPermission(src)\n\tif perm == true {\n\t\treturn nil, fmt.Errorf(\"file.CheckPermission Permission denied src: %s\", src)\n\t}\n\n\terr = file.IsNotExistMkDir(src)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"file.IsNotExistMkDir src: %s, err: %v\", src, err)\n\t}\n\n\tf, err := file.Open(src + fileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"Fail to OpenFile :%v\", err)\n\t}\n\n\treturn f, nil\n}\n```\n\n我们将引用都改为了 file/file.go 包里的方法\n\n2、打开 pkg/logging/log.go 文件，修改文件内容:\n\n```go\npackage logging\n\n...\n\nfunc Setup() {\n\tvar err error\n\tfilePath := getLogFilePath()\n\tfileName := getLogFileName()\n\tF, err = openLogFile(fileName, filePath)\n\tif err != nil {\n\t\tlog.Fatalln(err)\n\t}\n\n\tlogger = log.New(F, DefaultPrefix, log.LstdFlags)\n}\n\n...\n```\n\n由于原方法形参改变了，因此 openLogFile 也需要调整\n\n## 实现上传图片接口\n\n这一小节，我们开始实现上次图片相关的一些方法和功能\n\n首先需要在 blog_article 中增加字段 `cover_image_url`，格式为 `varchar(255) DEFAULT '' COMMENT '封面图片地址'`\n\n### 第零步\n\n一般不会直接将上传的图片名暴露出来，因此我们对图片名进行 MD5 来达到这个效果\n\n在 util 目录下新建 md5.go，写入文件内容：\n\n```go\npackage util\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n)\n\nfunc EncodeMD5(value string) string {\n\tm := md5.New()\n\tm.Write([]byte(value))\n\n\treturn hex.EncodeToString(m.Sum(nil))\n}\n\n```\n\n### 第一步\n\n在先前我们已经把底层方法给封装好了，实质这一步为封装 image 的处理逻辑\n\n在 pkg 目录下新建 upload/image.go 文件，写入文件内容：\n\n```go\npackage upload\n\nimport (\n\t\"os\"\n\t\"path\"\n\t\"log\"\n\t\"fmt\"\n\t\"strings\"\n\t\"mime/multipart\"\n\n\t\"github.com/EDDYCJY/go-gin-example/pkg/file\"\n\t\"github.com/EDDYCJY/go-gin-example/pkg/setting\"\n\t\"github.com/EDDYCJY/go-gin-example/pkg/logging\"\n\t\"github.com/EDDYCJY/go-gin-example/pkg/util\"\n)\n\nfunc GetImageFullUrl(name string) string {\n\treturn setting.AppSetting.ImagePrefixUrl + \"/\" + GetImagePath() + name\n}\n\nfunc GetImageName(name string) string {\n\text := path.Ext(name)\n\tfileName := strings.TrimSuffix(name, ext)\n\tfileName = util.EncodeMD5(fileName)\n\n\treturn fileName + ext\n}\n\nfunc GetImagePath() string {\n\treturn setting.AppSetting.ImageSavePath\n}\n\nfunc GetImageFullPath() string {\n\treturn setting.AppSetting.RuntimeRootPath + GetImagePath()\n}\n\nfunc CheckImageExt(fileName string) bool {\n\text := file.GetExt(fileName)\n\tfor _, allowExt := range setting.AppSetting.ImageAllowExts {\n\t\tif strings.ToUpper(allowExt) == strings.ToUpper(ext) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc CheckImageSize(f multipart.File) bool {\n\tsize, err := file.GetSize(f)\n\tif err != nil {\n\t\tlog.Println(err)\n\t\tlogging.Warn(err)\n\t\treturn false\n\t}\n\n\treturn size <= setting.AppSetting.ImageMaxSize\n}\n\nfunc CheckImage(src string) error {\n\tdir, err := os.Getwd()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"os.Getwd err: %v\", err)\n\t}\n\n\terr = file.IsNotExistMkDir(dir + \"/\" + src)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"file.IsNotExistMkDir err: %v\", err)\n\t}\n\n\tperm := file.CheckPermission(src)\n\tif perm == true {\n\t\treturn fmt.Errorf(\"file.CheckPermission Permission denied src: %s\", src)\n\t}\n\n\treturn nil\n}\n```\n\n在这里我们实现了 7 个方法，如下：\n\n- GetImageFullUrl：获取图片完整访问 URL\n- GetImageName：获取图片名称\n- GetImagePath：获取图片路径\n- GetImageFullPath：获取图片完整路径\n- CheckImageExt：检查图片后缀\n- CheckImageSize：检查图片大小\n- CheckImage：检查图片\n\n这里基本是对底层代码的二次封装，为了更灵活的处理一些图片特有的逻辑，并且方便修改，不直接对外暴露下层\n\n### 第二步\n\n这一步将编写上传图片的业务逻辑，在 routers/api 目录下 新建 upload.go 文件，写入文件内容:\n\n```go\npackage api\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\n\t\"github.com/EDDYCJY/go-gin-example/pkg/e\"\n\t\"github.com/EDDYCJY/go-gin-example/pkg/logging\"\n\t\"github.com/EDDYCJY/go-gin-example/pkg/upload\"\n)\n\nfunc UploadImage(c *gin.Context) {\n\tcode := e.SUCCESS\n\tdata := make(map[string]string)\n\n\tfile, image, err := c.Request.FormFile(\"image\")\n\tif err != nil {\n\t\tlogging.Warn(err)\n\t\tcode = e.ERROR\n\t\tc.JSON(http.StatusOK, gin.H{\n\t\t\t\"code\": code,\n\t\t\t\"msg\":  e.GetMsg(code),\n\t\t\t\"data\": data,\n\t\t})\n\t}\n\n\tif image == nil {\n\t\tcode = e.INVALID_PARAMS\n\t} else {\n\t\timageName := upload.GetImageName(image.Filename)\n\t\tfullPath := upload.GetImageFullPath()\n\t\tsavePath := upload.GetImagePath()\n\n\t\tsrc := fullPath + imageName\n\t\tif ! upload.CheckImageExt(imageName) || ! upload.CheckImageSize(file) {\n\t\t\tcode = e.ERROR_UPLOAD_CHECK_IMAGE_FORMAT\n\t\t} else {\n\t\t\terr := upload.CheckImage(fullPath)\n\t\t\tif err != nil {\n\t\t\t\tlogging.Warn(err)\n\t\t\t\tcode = e.ERROR_UPLOAD_CHECK_IMAGE_FAIL\n\t\t\t} else if err := c.SaveUploadedFile(image, src); err != nil {\n\t\t\t\tlogging.Warn(err)\n\t\t\t\tcode = e.ERROR_UPLOAD_SAVE_IMAGE_FAIL\n\t\t\t} else {\n\t\t\t\tdata[\"image_url\"] = upload.GetImageFullUrl(imageName)\n\t\t\t\tdata[\"image_save_url\"] = savePath + imageName\n\t\t\t}\n\t\t}\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"code\": code,\n\t\t\"msg\":  e.GetMsg(code),\n\t\t\"data\": data,\n\t})\n}\n```\n\n所涉及的错误码（需在 pkg/e/code.go、msg.go 添加）：\n\n```go\n// 保存图片失败\nERROR_UPLOAD_SAVE_IMAGE_FAIL = 30001\n// 检查图片失败\nERROR_UPLOAD_CHECK_IMAGE_FAIL = 30002\n// 校验图片错误，图片格式或大小有问题\nERROR_UPLOAD_CHECK_IMAGE_FORMAT = 30003\n```\n\n在这一大段的业务逻辑中，我们做了如下事情：\n\n- c.Request.FormFile：获取上传的图片（返回提供的表单键的第一个文件）\n- CheckImageExt、CheckImageSize 检查图片大小，检查图片后缀\n- CheckImage：检查上传图片所需（权限、文件夹）\n- SaveUploadedFile：保存图片\n\n总的来说，就是 入参 -> 检查 -》 保存 的应用流程\n\n### 第三步\n\n打开 routers/router.go 文件，增加路由 `r.POST(\"/upload\", api.UploadImage)` ，如：\n\n```go\nfunc InitRouter() *gin.Engine {\n\tr := gin.New()\n    ...\n\tr.GET(\"/auth\", api.GetAuth)\n\tr.GET(\"/swagger/*any\", ginSwagger.WrapHandler(swaggerFiles.Handler))\n\tr.POST(\"/upload\", api.UploadImage)\n\n\tapiv1 := r.Group(\"/api/v1\")\n\tapiv1.Use(jwt.JWT())\n\t{\n\t\t...\n\t}\n\n\treturn r\n}\n```\n\n### 验证\n\n最后我们请求一下上传图片的接口，测试所编写的功能\n\n![image](https://s2.ax1x.com/2020/02/15/1xumb8.jpg)\n\n检查目录下是否含文件（注意权限问题）\n\n```\n$ pwd\n$GOPATH/src/github.com/EDDYCJY/go-gin-example/runtime/upload/images\n\n$ ll\n... 96a3be3cf272e017046d1b2674a52bd3.jpg\n... c39fa784216313cf2faa7c98739fc367.jpeg\n```\n\n在这里我们一共返回了 2 个参数，一个是完整的访问 URL，另一个为保存路径\n\n## 实现 http.FileServer\n\n在完成了上一小节后，我们还需要让前端能够访问到图片，一般是如下：\n\n- CDN\n- http.FileSystem\n\n在公司的话，CDN 或自建分布式文件系统居多，也不需要过多关注。而在实践里的话肯定是本地搭建了，Go 本身对此就有很好的支持，而 Gin 更是再封装了一层，只需要在路由增加一行代码即可\n\n### r.StaticFS\n\n打开 routers/router.go 文件，增加路由 `r.StaticFS(\"/upload/images\", http.Dir(upload.GetImageFullPath()))`，如：\n\n```go\nfunc InitRouter() *gin.Engine {\n    ...\n\tr.StaticFS(\"/upload/images\", http.Dir(upload.GetImageFullPath()))\n\n\tr.GET(\"/auth\", api.GetAuth)\n\tr.GET(\"/swagger/*any\", ginSwagger.WrapHandler(swaggerFiles.Handler))\n\tr.POST(\"/upload\", api.UploadImage)\n    ...\n}\n```\n\n### 它做了什么\n\n当访问 $HOST/upload/images 时，将会读取到 $GOPATH/src/github.com/EDDYCJY/go-gin-example/runtime/upload/images 下的文件\n\n而这行代码又做了什么事呢，我们来看看方法原型\n\n```go\n// StaticFS works just like `Static()` but a custom `http.FileSystem` can be used instead.\n// Gin by default user: gin.Dir()\nfunc (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes {\n\tif strings.Contains(relativePath, \":\") || strings.Contains(relativePath, \"*\") {\n\t\tpanic(\"URL parameters can not be used when serving a static folder\")\n\t}\n\thandler := group.createStaticHandler(relativePath, fs)\n\turlPattern := path.Join(relativePath, \"/*filepath\")\n\n\t// Register GET and HEAD handlers\n\tgroup.GET(urlPattern, handler)\n\tgroup.HEAD(urlPattern, handler)\n\treturn group.returnObj()\n}\n```\n\n首先在暴露的 URL 中禁止了 \\* 和 : 符号的使用，通过 `createStaticHandler` 创建了静态文件服务，实质最终调用的还是 `fileServer.ServeHTTP` 和一些处理逻辑了\n\n```go\nfunc (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc {\n\tabsolutePath := group.calculateAbsolutePath(relativePath)\n\tfileServer := http.StripPrefix(absolutePath, http.FileServer(fs))\n\t_, nolisting := fs.(*onlyfilesFS)\n\treturn func(c *Context) {\n\t\tif nolisting {\n\t\t\tc.Writer.WriteHeader(404)\n\t\t}\n\t\tfileServer.ServeHTTP(c.Writer, c.Request)\n\t}\n}\n```\n\n#### http.StripPrefix\n\n我们可以留意下 `fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))` 这段语句，在静态文件服务中很常见，它有什么作用呢？\n\n`http.StripPrefix` 主要作用是从请求 URL 的路径中删除给定的前缀，最终返回一个 `Handler`\n\n通常 http.FileServer 要与 http.StripPrefix 相结合使用，否则当你运行：\n\n```go\nhttp.Handle(\"/upload/images\", http.FileServer(http.Dir(\"upload/images\")))\n```\n\n会无法正确的访问到文件目录，因为 `/upload/images` 也包含在了 URL 路径中，必须使用：\n\n```go\nhttp.Handle(\"/upload/images\", http.StripPrefix(\"upload/images\", http.FileServer(http.Dir(\"upload/images\"))))\n```\n\n#### /\\*filepath\n\n到下面可以看到 `urlPattern := path.Join(relativePath, \"/*filepath\")`，`/*filepath` 你是谁，你在这里有什么用，你是 Gin 的产物吗?\n\n通过语义可得知是路由的处理逻辑，而 Gin 的路由是基于 httprouter 的，通过查阅文档可得到以下信息\n\n```\nPattern: /src/*filepath\n\n /src/                     match\n /src/somefile.go          match\n /src/subdir/somefile.go   match\n```\n\n`*filepath` 将匹配所有文件路径，并且 `*filepath` 必须在 Pattern 的最后\n\n### 验证\n\n重新执行 `go run main.go` ，去访问刚刚在 upload 接口得到的图片地址，检查 http.FileSystem 是否正常\n\n![image](https://s2.ax1x.com/2020/02/15/1xu4Gd.jpg)\n\n## 修改文章接口\n\n接下来，需要你修改 routers/api/v1/article.go 的 AddArticle、EditArticle 两个接口\n\n- 新增、更新文章接口：支持入参 cover_image_url\n- 新增、更新文章接口：增加对 cover_image_url 的非空、最长长度校验\n\n这块前面文章讲过，如果有问题可以参考项目的代码 👌\n\n## 总结\n\n在这章节中，我们简单的分析了下需求，对应用做出了一个小规划并实施\n\n完成了清单中的功能点和优化，在实际项目中也是常见的场景，希望你能够细细品尝并针对一些点进行深入学习\n\n## 参考\n\n### 本系列示例代码\n\n- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)\n\n## 关于\n\n### 修改记录\n\n- 第一版：2018 年 02 月 16 日发布文章\n- 第二版：2019 年 10 月 02 日修改文章\n\n## ？\n\n如果有任何疑问或错误，欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见，如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进。\n\n### 我的公众号\n\n![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)"
  },
  {
    "path": "content/posts/go/gin/2018-06-02-application-redis.md",
    "content": "---\n\ntitle:      \"「连载十三」优化你的应用结构和实现Redis缓存\"\ndate:       2018-06-02 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n---\n\n## 前言\n\n之前就在想，不少教程或示例的代码设计都是一步到位的（也没问题）\n\n但实际操作的读者真的能够理解透彻为什么吗？左思右想，有了今天这一章的内容，我认为实际经历过一遍印象会更加深刻\n\n## 本文目标\n\n在本章节，将介绍以下功能的整理：\n\n- 抽离、分层业务逻辑：减轻 routers.go 内的 api 方法的逻辑（但本文暂不分层 repository，这块逻辑还不重）。\n- 增加容错性：对 gorm 的错误进行判断。\n- Redis 缓存：对获取数据类的接口增加缓存设置。\n- 减少重复冗余代码。\n\n## 问题在哪？\n\n在规划阶段我们发现了一个问题，这是目前的伪代码：\n\n```go\nif ! HasErrors() {\n\tif ExistArticleByID(id) {\n\t\tDeleteArticle(id)\n\t\tcode = e.SUCCESS\n\t} else {\n\t\tcode = e.ERROR_NOT_EXIST_ARTICLE\n\t}\n} else {\n\tfor _, err := range valid.Errors {\n\t\tlogging.Info(err.Key, err.Message)\n\t}\n}\n\nc.JSON(http.StatusOK, gin.H{\n\t\"code\": code,\n\t\"msg\":  e.GetMsg(code),\n\t\"data\": make(map[string]string),\n})\n```\n\n如果加上规划内的功能逻辑呢，伪代码会变成：\n\n```go\nif ! HasErrors() {\n    exists, err := ExistArticleByID(id)\n    if err == nil {\n        if exists {\n    \t\terr = DeleteArticle(id)\n    \t\tif err == nil {\n    \t\t    code = e.SUCCESS\n    \t\t} else {\n    \t\t    code = e.ERROR_XXX\n    \t\t}\n    \t} else {\n    \t\tcode = e.ERROR_NOT_EXIST_ARTICLE\n    \t}\n    } else {\n        code = e.ERROR_XXX\n    }\n} else {\n\tfor _, err := range valid.Errors {\n\t\tlogging.Info(err.Key, err.Message)\n\t}\n}\n\nc.JSON(http.StatusOK, gin.H{\n\t\"code\": code,\n\t\"msg\":  e.GetMsg(code),\n\t\"data\": make(map[string]string),\n})\n```\n\n如果缓存的逻辑也加进来，后面慢慢不断的迭代，岂不是会变成如下图一样？\n\n![image](https://coolshell.cn/wp-content/uploads/2017/04/IMG_7411.jpg)\n\n现在我们发现了问题，应及时解决这个代码结构问题，同时把代码写的清晰、漂亮、易读易改也是一个重要指标\n\n## 如何改？\n\n在左耳朵耗子的文章中，这类代码被称为 “箭头型” 代码，有如下几个问题：\n\n1、我的显示器不够宽，箭头型代码缩进太狠了，需要我来回拉水平滚动条，这让我在读代码的时候，相当的不舒服\n\n2、除了宽度外还有长度，有的代码的 if-else 里的 if-else 里的 if-else 的代码太多，读到中间你都不知道中间的代码是经过了什么样的层层检查才来到这里的\n\n总而言之，“箭头型代码”如果嵌套太多，代码太长的话，会相当容易让维护代码的人（包括自己）迷失在代码中，因为看到最内层的代码时，你已经不知道前面的那一层一层的条件判断是什么样的，代码是怎么运行到这里的，所以，箭头型代码是非常难以维护和 Debug 的。\n\n简单的来说，就是**让出错的代码先返回，前面把所有的错误判断全判断掉，然后就剩下的就是正常的代码了**\n\n（注意：本段引用自耗子哥的 [如何重构“箭头型”代码](https://coolshell.cn/articles/17757.html)，建议细细品尝）\n\n## 落实\n\n本项目将对既有代码进行优化和实现缓存，希望你习得方法并对其他地方也进行优化\n\n第一步：完成 Redis 的基础设施建设（需要你先装好 Redis）\n\n第二步：对现有代码进行拆解、分层（不会贴上具体步骤的代码，希望你能够实操一波，加深理解 🤔）\n\n### Redis\n\n#### 一、配置\n\n打开 conf/app.ini 文件，新增配置：\n\n```ini\n...\n[redis]\nHost = 127.0.0.1:6379\nPassword =\nMaxIdle = 30\nMaxActive = 30\nIdleTimeout = 200\n```\n\n#### 二、缓存 Prefix\n\n打开 pkg/e 目录，新建 cache.go，写入内容：\n\n```go\npackage e\n\nconst (\n\tCACHE_ARTICLE = \"ARTICLE\"\n\tCACHE_TAG     = \"TAG\"\n)\n```\n\n#### 三、缓存 Key\n\n（1）、打开 service 目录，新建 cache_service/article.go\n\n写入内容：[传送门](https://github.com/EDDYCJY/go-gin-example/blob/master/service/cache_service/article.go)\n\n（2）、打开 service 目录，新建 cache_service/tag.go\n\n写入内容：[传送门](https://github.com/EDDYCJY/go-gin-example/blob/master/service/cache_service/tag.go)\n\n这一部分主要是编写获取缓存 KEY 的方法，直接参考传送门即可\n\n#### 四、Redis 工具包\n\n打开 pkg 目录，新建 gredis/redis.go，写入内容：\n\n```go\npackage gredis\n\nimport (\n\t\"encoding/json\"\n\t\"time\"\n\n\t\"github.com/gomodule/redigo/redis\"\n\n\t\"github.com/EDDYCJY/go-gin-example/pkg/setting\"\n)\n\nvar RedisConn *redis.Pool\n\nfunc Setup() error {\n\tRedisConn = &redis.Pool{\n\t\tMaxIdle:     setting.RedisSetting.MaxIdle,\n\t\tMaxActive:   setting.RedisSetting.MaxActive,\n\t\tIdleTimeout: setting.RedisSetting.IdleTimeout,\n\t\tDial: func() (redis.Conn, error) {\n\t\t\tc, err := redis.Dial(\"tcp\", setting.RedisSetting.Host)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif setting.RedisSetting.Password != \"\" {\n\t\t\t\tif _, err := c.Do(\"AUTH\", setting.RedisSetting.Password); err != nil {\n\t\t\t\t\tc.Close()\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn c, err\n\t\t},\n\t\tTestOnBorrow: func(c redis.Conn, t time.Time) error {\n\t\t\t_, err := c.Do(\"PING\")\n\t\t\treturn err\n\t\t},\n\t}\n\n\treturn nil\n}\n\nfunc Set(key string, data interface{}, time int) error {\n\tconn := RedisConn.Get()\n\tdefer conn.Close()\n\n\tvalue, err := json.Marshal(data)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = conn.Do(\"SET\", key, value)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = conn.Do(\"EXPIRE\", key, time)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc Exists(key string) bool {\n\tconn := RedisConn.Get()\n\tdefer conn.Close()\n\n\texists, err := redis.Bool(conn.Do(\"EXISTS\", key))\n\tif err != nil {\n\t\treturn false\n\t}\n\n\treturn exists\n}\n\nfunc Get(key string) ([]byte, error) {\n\tconn := RedisConn.Get()\n\tdefer conn.Close()\n\n\treply, err := redis.Bytes(conn.Do(\"GET\", key))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn reply, nil\n}\n\nfunc Delete(key string) (bool, error) {\n\tconn := RedisConn.Get()\n\tdefer conn.Close()\n\n\treturn redis.Bool(conn.Do(\"DEL\", key))\n}\n\nfunc LikeDeletes(key string) error {\n\tconn := RedisConn.Get()\n\tdefer conn.Close()\n\n\tkeys, err := redis.Strings(conn.Do(\"KEYS\", \"*\"+key+\"*\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, key := range keys {\n\t\t_, err = Delete(key)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n```\n\n在这里我们做了一些基础功能封装\n\n1、设置 RedisConn 为 redis.Pool（连接池）并配置了它的一些参数：\n\n- Dial：提供创建和配置应用程序连接的一个函数\n\n- TestOnBorrow：可选的应用程序检查健康功能\n\n- MaxIdle：最大空闲连接数\n\n- MaxActive：在给定时间内，允许分配的最大连接数（当为零时，没有限制）\n\n- IdleTimeout：在给定时间内将会保持空闲状态，若到达时间限制则关闭连接（当为零时，没有限制）\n\n2、封装基础方法\n\n文件内包含 Set、Exists、Get、Delete、LikeDeletes 用于支撑目前的业务逻辑，而在里面涉及到了如方法：\n\n（1）`RedisConn.Get()`：在连接池中获取一个活跃连接\n\n（2）`conn.Do(commandName string, args ...interface{})`：向 Redis 服务器发送命令并返回收到的答复\n\n（3）`redis.Bool(reply interface{}, err error)`：将命令返回转为布尔值\n\n（4）`redis.Bytes(reply interface{}, err error)`：将命令返回转为 Bytes\n\n（5）`redis.Strings(reply interface{}, err error)`：将命令返回转为 []string\n\n在 [redigo](https://godoc.org/github.com/gomodule/redigo/redis) 中包含大量类似的方法，万变不离其宗，建议熟悉其使用规则和 [Redis 命令](http://doc.redisfans.com/index.html) 即可\n\n到这里为止，Redis 就可以愉快的调用啦。另外受篇幅限制，这块的深入讲解会另外开设！\n\n### 拆解、分层\n\n在先前规划中，引出几个方法去优化我们的应用结构\n\n- 错误提前返回\n- 统一返回方法\n- 抽离 Service，减轻 routers/api 的逻辑，进行分层\n- 增加 gorm 错误判断，让错误提示更明确（增加内部错误码）\n\n#### 编写返回方法\n\n要让错误提前返回，c.JSON 的侵入是不可避免的，但是可以让其更具可变性，指不定哪天就变 XML 了呢？\n\n1、打开 pkg 目录，新建 app/request.go，写入文件内容：\n\n```go\npackage app\n\nimport (\n\t\"github.com/astaxie/beego/validation\"\n\n\t\"github.com/EDDYCJY/go-gin-example/pkg/logging\"\n)\n\nfunc MarkErrors(errors []*validation.Error) {\n\tfor _, err := range errors {\n\t\tlogging.Info(err.Key, err.Message)\n\t}\n\n\treturn\n}\n```\n\n2、打开 pkg 目录，新建 app/response.go，写入文件内容：\n\n```go\npackage app\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n\n\t\"github.com/EDDYCJY/go-gin-example/pkg/e\"\n)\n\ntype Gin struct {\n\tC *gin.Context\n}\n\nfunc (g *Gin) Response(httpCode, errCode int, data interface{}) {\n\tg.C.JSON(httpCode, gin.H{\n\t\t\"code\": errCode,\n\t\t\"msg\":  e.GetMsg(errCode),\n\t\t\"data\": data,\n\t})\n\n\treturn\n}\n```\n\n这样子以后如果要变动，直接改动 app 包内的方法即可\n\n#### 修改既有逻辑\n\n打开 routers/api/v1/article.go，查看修改 GetArticle 方法后的代码为：\n\n```go\nfunc GetArticle(c *gin.Context) {\n\tappG := app.Gin{c}\n\tid := com.StrTo(c.Param(\"id\")).MustInt()\n\tvalid := validation.Validation{}\n\tvalid.Min(id, 1, \"id\").Message(\"ID必须大于0\")\n\n\tif valid.HasErrors() {\n\t\tapp.MarkErrors(valid.Errors)\n\t\tappG.Response(http.StatusOK, e.INVALID_PARAMS, nil)\n\t\treturn\n\t}\n\n\tarticleService := article_service.Article{ID: id}\n\texists, err := articleService.ExistByID()\n\tif err != nil {\n\t\tappG.Response(http.StatusOK, e.ERROR_CHECK_EXIST_ARTICLE_FAIL, nil)\n\t\treturn\n\t}\n\tif !exists {\n\t\tappG.Response(http.StatusOK, e.ERROR_NOT_EXIST_ARTICLE, nil)\n\t\treturn\n\t}\n\n\tarticle, err := articleService.Get()\n\tif err != nil {\n\t\tappG.Response(http.StatusOK, e.ERROR_GET_ARTICLE_FAIL, nil)\n\t\treturn\n\t}\n\n\tappG.Response(http.StatusOK, e.SUCCESS, article)\n}\n```\n\n这里有几个值得变动点，主要是在内部增加了错误返回，如果存在错误则直接返回。另外进行了分层，业务逻辑内聚到了 service 层中去，而 routers/api（controller）显著减轻，代码会更加的直观\n\n例如 service/article_service 下的 `articleService.Get()` 方法：\n\n```go\nfunc (a *Article) Get() (*models.Article, error) {\n\tvar cacheArticle *models.Article\n\n\tcache := cache_service.Article{ID: a.ID}\n\tkey := cache.GetArticleKey()\n\tif gredis.Exists(key) {\n\t\tdata, err := gredis.Get(key)\n\t\tif err != nil {\n\t\t\tlogging.Info(err)\n\t\t} else {\n\t\t\tjson.Unmarshal(data, &cacheArticle)\n\t\t\treturn cacheArticle, nil\n\t\t}\n\t}\n\n\tarticle, err := models.GetArticle(a.ID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tgredis.Set(key, article, 3600)\n\treturn article, nil\n}\n```\n\n而对于 gorm 的 错误返回设置，只需要修改 models/article.go 如下:\n\n```go\nfunc GetArticle(id int) (*Article, error) {\n\tvar article Article\n\terr := db.Where(\"id = ? AND deleted_on = ? \", id, 0).First(&article).Related(&article.Tag).Error\n\tif err != nil && err != gorm.ErrRecordNotFound {\n\t\treturn nil, err\n\t}\n\n\treturn &article, nil\n}\n```\n\n习惯性增加 .Error，把控绝大部分的错误。另外需要注意一点，在 gorm 中，查找不到记录也算一种 “错误” 哦\n\n## 最后\n\n显然，本章节并不是你跟着我敲系列。我给你的课题是 “实现 Redis 缓存并优化既有的业务逻辑代码”\n\n让其能够不断地适应业务的发展，让代码更清晰易读，且呈层级和结构性\n\n如果有疑惑，可以到 [go-gin-example](https://github.com/EDDYCJY/go-gin-example) 看看我是怎么写的，你是怎么写的，又分别有什么优势、劣势，取长补短一波？\n\n## 参考\n\n### 本系列示例代码\n\n- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)\n\n### 推荐阅读\n\n- [如何重构“箭头型”代码](https://coolshell.cn/articles/17757.html)\n\n## 关于\n\n### 修改记录\n\n- 第一版：2018 年 02 月 16 日发布文章\n- 第二版：2019 年 10 月 01 日修改文章\n\n## ？\n\n如果有任何疑问或错误，欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见，如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进。\n\n### 我的公众号\n\n![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)"
  },
  {
    "path": "content/posts/go/gin/2018-06-14-excel.md",
    "content": "---\n\ntitle:      \"「连载十四」实现导出、导入 Excel\"\ndate:       2018-06-14 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n---\n\n## 知识点\n\n- 导出功能的实现\n\n## 本文目标\n\n在本节，我们将实现对标签信息的导出、导入功能，这是很标配功能了，希望你掌握基础的使用方式。\n\n另外在本文我们使用了 2 个 Excel 的包，excelize 最初的 XML 格式文件的一些结构，是通过 tealeg/xlsx 格式文件结构演化而来的，因此特意在此都展示了，你可以根据自己的场景和喜爱去使用。\n\n## 配置\n\n首先要指定导出的 Excel 文件的存储路径，在 app.ini 中增加配置：\n\n```ini\n[app]\n...\n\nExportSavePath = export/\n```\n\n修改 setting.go 的 App struct：\n\n```go\ntype App struct {\n\tJwtSecret       string\n\tPageSize        int\n\tPrefixUrl       string\n\n\tRuntimeRootPath string\n\n\tImageSavePath  string\n\tImageMaxSize   int\n\tImageAllowExts []string\n\n\tExportSavePath string\n\n\tLogSavePath string\n\tLogSaveName string\n\tLogFileExt  string\n\tTimeFormat  string\n}\n```\n\n在这里需增加 ExportSavePath 配置项，另外将先前 ImagePrefixUrl 改为 PrefixUrl 用于支撑两者的 HOST 获取\n\n（注意修改 image.go 的 GetImageFullUrl 方法）\n\n## pkg\n\n新建 pkg/export/excel.go 文件，如下：\n\n```go\npackage export\n\nimport \"github.com/EDDYCJY/go-gin-example/pkg/setting\"\n\nfunc GetExcelFullUrl(name string) string {\n\treturn setting.AppSetting.PrefixUrl + \"/\" + GetExcelPath() + name\n}\n\nfunc GetExcelPath() string {\n\treturn setting.AppSetting.ExportSavePath\n}\n\nfunc GetExcelFullPath() string {\n\treturn setting.AppSetting.RuntimeRootPath + GetExcelPath()\n}\n```\n\n这里编写了一些常用的方法，以后取值方式如果有变动，直接改内部代码即可，对外不可见\n\n## 尝试一下标准库\n\n```go\nf, err := os.Create(export.GetExcelFullPath() + \"test.csv\")\nif err != nil {\n\tpanic(err)\n}\ndefer f.Close()\n\nf.WriteString(\"\\xEF\\xBB\\xBF\")\n\nw := csv.NewWriter(f)\ndata := [][]string{\n\t{\"1\", \"test1\", \"test1-1\"},\n\t{\"2\", \"test2\", \"test2-1\"},\n\t{\"3\", \"test3\", \"test3-1\"},\n}\n\nw.WriteAll(data)\n```\n\n在 Go 提供的标准库 encoding/csv 中，天然的支持 csv 文件的读取和处理，在本段代码中，做了如下工作：\n\n1、os.Create：\n\n创建了一个 test.csv 文件\n\n2、f.WriteString(\"\\xEF\\xBB\\xBF\")：\n\n`\\xEF\\xBB\\xBF` 是 UTF-8 BOM 的 16 进制格式，在这里的用处是标识文件的编码格式，通常会出现在文件的开头，因此第一步就要将其写入。如果不标识 UTF-8 的编码格式的话，写入的汉字会显示为乱码\n\n3、csv.NewWriter：\n\n```go\nfunc NewWriter(w io.Writer) *Writer {\n\treturn &Writer{\n\t\tComma: ',',\n\t\tw:     bufio.NewWriter(w),\n\t}\n}\n```\n\n4、w.WriteAll：\n\n```go\nfunc (w *Writer) WriteAll(records [][]string) error {\n\tfor _, record := range records {\n\t\terr := w.Write(record)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn w.w.Flush()\n}\n```\n\nWriteAll 实际是对 Write 的封装，需要注意在最后调用了 `w.w.Flush()`，这充分了说明了 WriteAll 的使用场景，你可以想想作者的设计用意\n\n## 导出\n\n### Service 方法\n\n打开 service/tag.go，增加 Export 方法，如下：\n\n```go\nfunc (t *Tag) Export() (string, error) {\n\ttags, err := t.GetAll()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfile := xlsx.NewFile()\n\tsheet, err := file.AddSheet(\"标签信息\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\ttitles := []string{\"ID\", \"名称\", \"创建人\", \"创建时间\", \"修改人\", \"修改时间\"}\n\trow := sheet.AddRow()\n\n\tvar cell *xlsx.Cell\n\tfor _, title := range titles {\n\t\tcell = row.AddCell()\n\t\tcell.Value = title\n\t}\n\n\tfor _, v := range tags {\n\t\tvalues := []string{\n\t\t\tstrconv.Itoa(v.ID),\n\t\t\tv.Name,\n\t\t\tv.CreatedBy,\n\t\t\tstrconv.Itoa(v.CreatedOn),\n\t\t\tv.ModifiedBy,\n\t\t\tstrconv.Itoa(v.ModifiedOn),\n\t\t}\n\n\t\trow = sheet.AddRow()\n\t\tfor _, value := range values {\n\t\t\tcell = row.AddCell()\n\t\t\tcell.Value = value\n\t\t}\n\t}\n\n\ttime := strconv.Itoa(int(time.Now().Unix()))\n\tfilename := \"tags-\" + time + \".xlsx\"\n\n\tfullPath := export.GetExcelFullPath() + filename\n\terr = file.Save(fullPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn filename, nil\n}\n```\n\n## routers 入口\n\n打开 routers/api/v1/tag.go，增加如下方法：\n\n```go\nfunc ExportTag(c *gin.Context) {\n\tappG := app.Gin{C: c}\n\tname := c.PostForm(\"name\")\n\tstate := -1\n\tif arg := c.PostForm(\"state\"); arg != \"\" {\n\t\tstate = com.StrTo(arg).MustInt()\n\t}\n\n\ttagService := tag_service.Tag{\n\t\tName:  name,\n\t\tState: state,\n\t}\n\n\tfilename, err := tagService.Export()\n\tif err != nil {\n\t\tappG.Response(http.StatusOK, e.ERROR_EXPORT_TAG_FAIL, nil)\n\t\treturn\n\t}\n\n\tappG.Response(http.StatusOK, e.SUCCESS, map[string]string{\n\t\t\"export_url\":      export.GetExcelFullUrl(filename),\n\t\t\"export_save_url\": export.GetExcelPath() + filename,\n\t})\n}\n```\n\n### 路由\n\n在 routers/router.go 文件中增加路由方法，如下\n\n```go\napiv1 := r.Group(\"/api/v1\")\napiv1.Use(jwt.JWT())\n{\n\t...\n\t//导出标签\n\tr.POST(\"/tags/export\", v1.ExportTag)\n}\n```\n\n### 验证接口\n\n访问 `http://127.0.0.1:8000/tags/export`，结果如下：\n\n```json\n{\n  \"code\": 200,\n  \"data\": {\n    \"export_save_url\": \"export/tags-1528903393.xlsx\",\n    \"export_url\": \"http://127.0.0.1:8000/export/tags-1528903393.xlsx\"\n  },\n  \"msg\": \"ok\"\n}\n```\n\n最终通过接口返回了导出文件的地址和保存地址\n\n### StaticFS\n\n那你想想，现在直接访问地址肯定是无法下载文件的，那么该如何做呢？\n\n打开 router.go 文件，增加代码如下：\n\n```go\nr.StaticFS(\"/export\", http.Dir(export.GetExcelFullPath()))\n```\n\n若你不理解，强烈建议温习下前面的章节，举一反三\n\n## 验证下载\n\n再次访问上面的 export_url ，如：`http://127.0.0.1:8000/export/tags-1528903393.xlsx`，是不是成功了呢？\n\n## 导入\n\n### Service 方法\n\n打开 service/tag.go，增加 Import 方法，如下：\n\n```go\nfunc (t *Tag) Import(r io.Reader) error {\n\txlsx, err := excelize.OpenReader(r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trows := xlsx.GetRows(\"标签信息\")\n\tfor irow, row := range rows {\n\t\tif irow > 0 {\n\t\t\tvar data []string\n\t\t\tfor _, cell := range row {\n\t\t\t\tdata = append(data, cell)\n\t\t\t}\n\n\t\t\tmodels.AddTag(data[1], 1, data[2])\n\t\t}\n\t}\n\n\treturn nil\n}\n```\n\n## routers 入口\n\n打开 routers/api/v1/tag.go，增加如下方法：\n\n```go\nfunc ImportTag(c *gin.Context) {\n\tappG := app.Gin{C: c}\n\n\tfile, _, err := c.Request.FormFile(\"file\")\n\tif err != nil {\n\t\tlogging.Warn(err)\n\t\tappG.Response(http.StatusOK, e.ERROR, nil)\n\t\treturn\n\t}\n\n\ttagService := tag_service.Tag{}\n\terr = tagService.Import(file)\n\tif err != nil {\n\t\tlogging.Warn(err)\n\t\tappG.Response(http.StatusOK, e.ERROR_IMPORT_TAG_FAIL, nil)\n\t\treturn\n\t}\n\n\tappG.Response(http.StatusOK, e.SUCCESS, nil)\n}\n```\n\n### 路由\n\n在 routers/router.go 文件中增加路由方法，如下\n\n```go\napiv1 := r.Group(\"/api/v1\")\napiv1.Use(jwt.JWT())\n{\n\t...\n\t//导入标签\n\tr.POST(\"/tags/import\", v1.ImportTag)\n}\n```\n\n### 验证\n\n![image](https://s2.ax1x.com/2020/02/15/1xKtSA.jpg)\n\n在这里我们将先前导出的 Excel 文件作为入参，访问 `http://127.0.0.01:8000/tags/import`，检查返回和数据是否正确入库\n\n## 总结\n\n在本文中，简单介绍了 Excel 的导入、导出的使用方式，使用了以下 2 个包：\n\n- [tealeg/xlsx](https://github.com/tealeg/xlsx)\n- [360EntSecGroup-Skylar/excelize](https://github.com/360EntSecGroup-Skylar/excelize)\n\n你可以细细阅读一下它的实现和使用方式，对你的把控更有帮助 🤔\n\n## 课外\n\n- tag：导出使用 excelize 的方式去实现（可能你会发现更简单哦）\n- tag：导入去重功能实现\n- artice ：导入、导出功能实现\n\n也不失为你很好的练手机会，如果有兴趣，可以试试\n\n## 参考\n\n### 本系列示例代码\n\n- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)\n\n## 关于\n\n### 修改记录\n\n- 第一版：2018 年 02 月 16 日发布文章\n- 第二版：2019 年 10 月 02 日修改文章\n\n## ？\n\n如果有任何疑问或错误，欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见，如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进。\n\n### 我的公众号\n\n![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)\n\n"
  },
  {
    "path": "content/posts/go/gin/2018-07-05-image.md",
    "content": "---\n\ntitle:      \"「连载十五」生成二维码、合并海报\"\ndate:       2018-07-05 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n---\n\n## 知识点\n\n- 图片生成\n- 二维码生成\n\n## 本文目标\n\n在文章的详情页中，我们常常会需要去宣传它，而目前最常见的就是发海报了，今天我们将实现如下功能：\n\n- 生成二维码\n\n- 合并海报（背景图 + 二维码）\n\n## 实现\n\n首先，你需要在 App 配置项中增加二维码及其海报的存储路径，我们约定配置项名称为 `QrCodeSavePath`，值为 `qrcode/`，经过多节连载的你应该能够完成，若有不懂可参照 [go-gin-example](https://github.com/EDDYCJY/go-gin-example/blob/master/conf/app.ini#L14)。\n\n## 生成二维码\n\n### 安装\n\n```\n$ go get -u github.com/boombuler/barcode\n```\n\n### 工具包\n\n考虑生成二维码这一动作贴合工具包的定义，且有公用的可能性，新建 pkg/qrcode/qrcode.go 文件，写入内容：\n\n```go\npackage qrcode\n\nimport (\n\t\"image/jpeg\"\n\n\t\"github.com/boombuler/barcode\"\n\t\"github.com/boombuler/barcode/qr\"\n\n\t\"github.com/EDDYCJY/go-gin-example/pkg/file\"\n\t\"github.com/EDDYCJY/go-gin-example/pkg/setting\"\n\t\"github.com/EDDYCJY/go-gin-example/pkg/util\"\n)\n\ntype QrCode struct {\n\tURL    string\n\tWidth  int\n\tHeight int\n\tExt    string\n\tLevel  qr.ErrorCorrectionLevel\n\tMode   qr.Encoding\n}\n\nconst (\n\tEXT_JPG = \".jpg\"\n)\n\nfunc NewQrCode(url string, width, height int, level qr.ErrorCorrectionLevel, mode qr.Encoding) *QrCode {\n\treturn &QrCode{\n\t\tURL:    url,\n\t\tWidth:  width,\n\t\tHeight: height,\n\t\tLevel:  level,\n\t\tMode:   mode,\n\t\tExt:    EXT_JPG,\n\t}\n}\n\nfunc GetQrCodePath() string {\n\treturn setting.AppSetting.QrCodeSavePath\n}\n\nfunc GetQrCodeFullPath() string {\n\treturn setting.AppSetting.RuntimeRootPath + setting.AppSetting.QrCodeSavePath\n}\n\nfunc GetQrCodeFullUrl(name string) string {\n\treturn setting.AppSetting.PrefixUrl + \"/\" + GetQrCodePath() + name\n}\n\nfunc GetQrCodeFileName(value string) string {\n\treturn util.EncodeMD5(value)\n}\n\nfunc (q *QrCode) GetQrCodeExt() string {\n\treturn q.Ext\n}\n\nfunc (q *QrCode) CheckEncode(path string) bool {\n\tsrc := path + GetQrCodeFileName(q.URL) + q.GetQrCodeExt()\n\tif file.CheckNotExist(src) == true {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc (q *QrCode) Encode(path string) (string, string, error) {\n\tname := GetQrCodeFileName(q.URL) + q.GetQrCodeExt()\n\tsrc := path + name\n\tif file.CheckNotExist(src) == true {\n\t\tcode, err := qr.Encode(q.URL, q.Level, q.Mode)\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", err\n\t\t}\n\n\t\tcode, err = barcode.Scale(code, q.Width, q.Height)\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", err\n\t\t}\n\n\t\tf, err := file.MustOpen(name, path)\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", err\n\t\t}\n\t\tdefer f.Close()\n\n\t\terr = jpeg.Encode(f, code, nil)\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", err\n\t\t}\n\t}\n\n\treturn name, path, nil\n}\n```\n\n这里主要聚焦 `func (q *QrCode) Encode` 方法，做了如下事情：\n\n- 获取二维码生成路径\n- 创建二维码\n- 缩放二维码到指定大小\n- 新建存放二维码图片的文件\n- 将图像（二维码）以 JPEG 4：2：0 基线格式写入文件\n\n另外在 `jpeg.Encode(f, code, nil)` 中，第三个参数可设置其图像质量，默认值为 75\n\n```go\n// DefaultQuality is the default quality encoding parameter.\nconst DefaultQuality = 75\n\n// Options are the encoding parameters.\n// Quality ranges from 1 to 100 inclusive, higher is better.\ntype Options struct {\n\tQuality int\n}\n```\n\n### 路由方法\n\n1、第一步\n\n在 routers/api/v1/article.go 新增 GenerateArticlePoster 方法用于接口开发\n\n2、第二步\n\n在 routers/router.go 的 apiv1 中新增 `apiv1.POST(\"/articles/poster/generate\", v1.GenerateArticlePoster)` 路由\n\n3、第三步\n\n修改 GenerateArticlePoster 方法，编写对应的生成逻辑，如下：\n\n```go\nconst (\n\tQRCODE_URL = \"https://github.com/EDDYCJY/blog#gin%E7%B3%BB%E5%88%97%E7%9B%AE%E5%BD%95\"\n)\n\nfunc GenerateArticlePoster(c *gin.Context) {\n\tappG := app.Gin{c}\n\tqrc := qrcode.NewQrCode(QRCODE_URL, 300, 300, qr.M, qr.Auto)\n\tpath := qrcode.GetQrCodeFullPath()\n\t_, _, err := qrc.Encode(path)\n\tif err != nil {\n\t\tappG.Response(http.StatusOK, e.ERROR, nil)\n\t\treturn\n\t}\n\n\tappG.Response(http.StatusOK, e.SUCCESS, nil)\n}\n```\n\n### 验证\n\n通过 POST 方法访问 `http://127.0.0.1:8000/api/v1/articles/poster/generate?token=$token`（注意 \\$token）\n\n![image](https://s2.ax1x.com/2020/02/15/1xQmb6.jpg)\n\n通过检查两个点确定功能是否正常，如下：\n\n1、访问结果是否 200\n\n2、本地目录是否成功生成二维码图片\n\n![image](https://s2.ax1x.com/2020/02/15/1xQCUU.jpg)\n\n## 合并海报\n\n在这一节，将实现二维码图片与背景图合并成新的一张图，可用于常见的宣传海报等业务场景\n\n### 背景图\n\n![image](https://s2.ax1x.com/2020/02/15/1xMXgs.jpg)\n\n将背景图另存为 runtime/qrcode/bg.jpg（实际应用，可存在 OSS 或其他地方）\n\n### service 方法\n\n打开 service/article_service 目录，新建 article_poster.go 文件，写入内容：\n\n```go\npackage article_service\n\nimport (\n\t\"image\"\n\t\"image/draw\"\n\t\"image/jpeg\"\n\t\"os\"\n\n\t\"github.com/EDDYCJY/go-gin-example/pkg/file\"\n\t\"github.com/EDDYCJY/go-gin-example/pkg/qrcode\"\n)\n\ntype ArticlePoster struct {\n\tPosterName string\n\t*Article\n\tQr *qrcode.QrCode\n}\n\nfunc NewArticlePoster(posterName string, article *Article, qr *qrcode.QrCode) *ArticlePoster {\n\treturn &ArticlePoster{\n\t\tPosterName: posterName,\n\t\tArticle:    article,\n\t\tQr:         qr,\n\t}\n}\n\nfunc GetPosterFlag() string {\n\treturn \"poster\"\n}\n\nfunc (a *ArticlePoster) CheckMergedImage(path string) bool {\n\tif file.CheckNotExist(path+a.PosterName) == true {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc (a *ArticlePoster) OpenMergedImage(path string) (*os.File, error) {\n\tf, err := file.MustOpen(a.PosterName, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn f, nil\n}\n\ntype ArticlePosterBg struct {\n\tName string\n\t*ArticlePoster\n\t*Rect\n\t*Pt\n}\n\ntype Rect struct {\n\tName string\n\tX0   int\n\tY0   int\n\tX1   int\n\tY1   int\n}\n\ntype Pt struct {\n\tX int\n\tY int\n}\n\nfunc NewArticlePosterBg(name string, ap *ArticlePoster, rect *Rect, pt *Pt) *ArticlePosterBg {\n\treturn &ArticlePosterBg{\n\t\tName:          name,\n\t\tArticlePoster: ap,\n\t\tRect:          rect,\n\t\tPt:            pt,\n\t}\n}\n\nfunc (a *ArticlePosterBg) Generate() (string, string, error) {\n\tfullPath := qrcode.GetQrCodeFullPath()\n\tfileName, path, err := a.Qr.Encode(fullPath)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\n\tif !a.CheckMergedImage(path) {\n\t\tmergedF, err := a.OpenMergedImage(path)\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", err\n\t\t}\n\t\tdefer mergedF.Close()\n\n\t\tbgF, err := file.MustOpen(a.Name, path)\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", err\n\t\t}\n\t\tdefer bgF.Close()\n\n\t\tqrF, err := file.MustOpen(fileName, path)\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", err\n\t\t}\n\t\tdefer qrF.Close()\n\n\t\tbgImage, err := jpeg.Decode(bgF)\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", err\n\t\t}\n\t\tqrImage, err := jpeg.Decode(qrF)\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", err\n\t\t}\n\n\t\tjpg := image.NewRGBA(image.Rect(a.Rect.X0, a.Rect.Y0, a.Rect.X1, a.Rect.Y1))\n\n\t\tdraw.Draw(jpg, jpg.Bounds(), bgImage, bgImage.Bounds().Min, draw.Over)\n\t\tdraw.Draw(jpg, jpg.Bounds(), qrImage, qrImage.Bounds().Min.Sub(image.Pt(a.Pt.X, a.Pt.Y)), draw.Over)\n\n\t\tjpeg.Encode(mergedF, jpg, nil)\n\t}\n\n\treturn fileName, path, nil\n}\n```\n\n这里重点留意 `func (a *ArticlePosterBg) Generate()` 方法，做了如下事情：\n\n- 获取二维码存储路径\n- 生成二维码图像\n- 检查合并后图像（指的是存放合并后的海报）是否存在\n- 若不存在，则生成待合并的图像 mergedF\n- 打开事先存放的背景图 bgF\n- 打开生成的二维码图像 qrF\n- 解码 bgF 和 qrF 返回 image.Image\n- 创建一个新的 RGBA 图像\n- 在 RGBA 图像上绘制 背景图（bgF）\n- 在已绘制背景图的 RGBA 图像上，在指定 Point 上绘制二维码图像（qrF）\n- 将绘制好的 RGBA 图像以 JPEG 4：2：0 基线格式写入合并后的图像文件（mergedF）\n\n### 错误码\n\n新增 [错误码](https://github.com/EDDYCJY/go-gin-example/blob/master/pkg/e/code.go#L27)，[错误提示](https://github.com/EDDYCJY/go-gin-example/blob/master/pkg/e/msg.go#L25)\n\n### 路由方法\n\n打开 routers/api/v1/article.go 文件，修改 GenerateArticlePoster 方法，编写最终的业务逻辑（含生成二维码及合并海报），如下：\n\n```go\nconst (\n\tQRCODE_URL = \"https://github.com/EDDYCJY/blog#gin%E7%B3%BB%E5%88%97%E7%9B%AE%E5%BD%95\"\n)\n\nfunc GenerateArticlePoster(c *gin.Context) {\n\tappG := app.Gin{c}\n\tarticle := &article_service.Article{}\n\tqr := qrcode.NewQrCode(QRCODE_URL, 300, 300, qr.M, qr.Auto) // 目前写死 gin 系列路径，可自行增加业务逻辑\n\tposterName := article_service.GetPosterFlag() + \"-\" + qrcode.GetQrCodeFileName(qr.URL) + qr.GetQrCodeExt()\n\tarticlePoster := article_service.NewArticlePoster(posterName, article, qr)\n\tarticlePosterBgService := article_service.NewArticlePosterBg(\n\t\t\"bg.jpg\",\n\t\tarticlePoster,\n\t\t&article_service.Rect{\n\t\t\tX0: 0,\n\t\t\tY0: 0,\n\t\t\tX1: 550,\n\t\t\tY1: 700,\n\t\t},\n\t\t&article_service.Pt{\n\t\t\tX: 125,\n\t\t\tY: 298,\n\t\t},\n\t)\n\n\t_, filePath, err := articlePosterBgService.Generate()\n\tif err != nil {\n\t\tappG.Response(http.StatusOK, e.ERROR_GEN_ARTICLE_POSTER_FAIL, nil)\n\t\treturn\n\t}\n\n\tappG.Response(http.StatusOK, e.SUCCESS, map[string]string{\n\t\t\"poster_url\":      qrcode.GetQrCodeFullUrl(posterName),\n\t\t\"poster_save_url\": filePath + posterName,\n\t})\n}\n```\n\n这块涉及到大量知识，强烈建议阅读下，如下：\n\n- [image.Rect](https://golang.org/pkg/image/#Rect)\n- [image.Pt](https://golang.org/pkg/image/#Pt)\n- [image.NewRGBA](https://golang.org/pkg/image/#NewRGBA)\n- [jpeg.Encode](https://golang.org/pkg/image/jpeg/#Encode)\n- [jpeg.Decode](https://golang.org/pkg/image/jpeg/#Decode)\n- [draw.Op](https://golang.org/pkg/image/draw/#Op)\n- [draw.Draw](https://golang.org/pkg/image/draw/#Draw)\n- [go-imagedraw-package](https://blog.golang.org/go-imagedraw-package)\n\n其所涉及、关联的库都建议研究一下\n\n### StaticFS\n\n在 routers/router.go 文件，增加如下代码:\n\n```go\nr.StaticFS(\"/qrcode\", http.Dir(qrcode.GetQrCodeFullPath()))\n```\n\n### 验证\n\n![image](https://s2.ax1x.com/2020/02/15/1xMLCQ.jpg)\n\n访问完整的 URL 路径，返回合成后的海报并扫除二维码成功则正确 🤓\n\n![image](https://s2.ax1x.com/2020/02/15/1xMhjI.jpg)\n\n## 总结\n\n在本章节实现了两个很常见的业务功能，分别是生成二维码和合并海报。希望你能够仔细阅读我给出的链接，这块的知识量不少，想要用好图像处理的功能，必须理解对应的思路，举一反三\n\n最后希望对你有所帮助 👌\n\n## 参考\n\n### 本系列示例代码\n\n- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)\n\n## 关于\n\n### 修改记录\n\n- 第一版：2018 年 02 月 16 日发布文章\n- 第二版：2019 年 10 月 02 日修改文章\n\n## ？\n\n如果有任何疑问或错误，欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见，如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进。\n\n### 我的公众号\n\n![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)"
  },
  {
    "path": "content/posts/go/gin/2018-07-07-font.md",
    "content": "---\n\ntitle:      \"「连载十六」在图片上绘制文字\"\ndate:       2018-07-07 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n---\n\n## 知识点\n\n- 字体库使用\n- 图片合成\n\n## 本文目标\n\n主要实现**合并后的海报上绘制文字**的功能（这个需求也是常见的很了），内容比较简单。\n\n## 实现\n\n这里使用的是 [微软雅黑](https://github.com/EDDYCJY/go-gin-example/blob/master/runtime/fonts/msyhbd.ttc) 的字体，请点击进行下载并**存放到 runtime/fonts 目录**下（字体文件占 16 MB 大小）\n\n### 安装\n\n```\n$ go get -u github.com/golang/freetype\n```\n\n### 绘制文字\n\n打开 service/article_service/article_poster.go 文件，增加绘制文字的业务逻辑，如下：\n\n```go\ntype DrawText struct {\n\tJPG    draw.Image\n\tMerged *os.File\n\n\tTitle string\n\tX0    int\n\tY0    int\n\tSize0 float64\n\n\tSubTitle string\n\tX1       int\n\tY1       int\n\tSize1    float64\n}\n\nfunc (a *ArticlePosterBg) DrawPoster(d *DrawText, fontName string) error {\n\tfontSource := setting.AppSetting.RuntimeRootPath + setting.AppSetting.FontSavePath + fontName\n\tfontSourceBytes, err := ioutil.ReadFile(fontSource)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttrueTypeFont, err := freetype.ParseFont(fontSourceBytes)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfc := freetype.NewContext()\n\tfc.SetDPI(72)\n\tfc.SetFont(trueTypeFont)\n\tfc.SetFontSize(d.Size0)\n\tfc.SetClip(d.JPG.Bounds())\n\tfc.SetDst(d.JPG)\n\tfc.SetSrc(image.Black)\n\n\tpt := freetype.Pt(d.X0, d.Y0)\n\t_, err = fc.DrawString(d.Title, pt)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfc.SetFontSize(d.Size1)\n\t_, err = fc.DrawString(d.SubTitle, freetype.Pt(d.X1, d.Y1))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = jpeg.Encode(d.Merged, d.JPG, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n```\n\n这里主要使用了 freetype 包，分别涉及如下细项：\n\n1、freetype.NewContext：创建一个新的 Context，会对其设置一些默认值\n\n```go\nfunc NewContext() *Context {\n\treturn &Context{\n\t\tr:        raster.NewRasterizer(0, 0),\n\t\tfontSize: 12,\n\t\tdpi:      72,\n\t\tscale:    12 << 6,\n\t}\n}\n```\n\n2、fc.SetDPI：设置屏幕每英寸的分辨率\n\n3、fc.SetFont：设置用于绘制文本的字体\n\n4、fc.SetFontSize：以磅为单位设置字体大小\n\n5、fc.SetClip：设置剪裁矩形以进行绘制\n\n6、fc.SetDst：设置目标图像\n\n7、fc.SetSrc：设置绘制操作的源图像，通常为 [image.Uniform](https://golang.org/pkg/image/#Uniform)\n\n```go\nvar (\n        // Black is an opaque black uniform image.\n        Black = NewUniform(color.Black)\n        // White is an opaque white uniform image.\n        White = NewUniform(color.White)\n        // Transparent is a fully transparent uniform image.\n        Transparent = NewUniform(color.Transparent)\n        // Opaque is a fully opaque uniform image.\n        Opaque = NewUniform(color.Opaque)\n)\n```\n\n8、fc.DrawString：根据 Pt 的坐标值绘制给定的文本内容\n\n### 业务逻辑\n\n打开 service/article_service/article_poster.go 方法，在 Generate 方法增加绘制文字的代码逻辑，如下：\n\n```go\nfunc (a *ArticlePosterBg) Generate() (string, string, error) {\n\tfullPath := qrcode.GetQrCodeFullPath()\n\tfileName, path, err := a.Qr.Encode(fullPath)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\n\tif !a.CheckMergedImage(path) {\n\t\t...\n\n\t\tdraw.Draw(jpg, jpg.Bounds(), bgImage, bgImage.Bounds().Min, draw.Over)\n\t\tdraw.Draw(jpg, jpg.Bounds(), qrImage, qrImage.Bounds().Min.Sub(image.Pt(a.Pt.X, a.Pt.Y)), draw.Over)\n\n\t\terr = a.DrawPoster(&DrawText{\n\t\t\tJPG:    jpg,\n\t\t\tMerged: mergedF,\n\n\t\t\tTitle: \"Golang Gin 系列文章\",\n\t\t\tX0:    80,\n\t\t\tY0:    160,\n\t\t\tSize0: 42,\n\n\t\t\tSubTitle: \"---煎鱼\",\n\t\t\tX1:       320,\n\t\t\tY1:       220,\n\t\t\tSize1:    36,\n\t\t}, \"msyhbd.ttc\")\n\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", err\n\t\t}\n\t}\n\n\treturn fileName, path, nil\n}\n```\n\n## 验证\n\n访问生成文章海报的接口 `$HOST/api/v1/articles/poster/generate?token=$token`，检查其生成结果，如下图\n\n![image](https://s2.ax1x.com/2020/02/15/1xKBTS.jpg)\n\n## 总结\n\n在本章节在 [连载十五](https://github.com/EDDYCJY/blog/blob/master/golang/gin/2018-07-04-Gin%E5%AE%9E%E8%B7%B5-%E8%BF%9E%E8%BD%BD%E5%8D%81%E4%BA%94-%E7%94%9F%E6%88%90%E4%BA%8C%E7%BB%B4%E7%A0%81-%E5%90%88%E5%B9%B6%E6%B5%B7%E6%8A%A5.md) 的基础上增加了绘制文字，在实现上并不困难，而这两块需求一般会同时出现，大家可以多加练习，了解里面的逻辑和其他 API 😁\n\n## 参考\n\n### 本系列示例代码\n\n- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)\n\n## 关于\n\n### 修改记录\n\n- 第一版：2018 年 02 月 16 日发布文章\n- 第二版：2019 年 10 月 02 日修改文章\n\n## ？\n\n如果有任何疑问或错误，欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见，如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进。\n\n### 我的公众号\n\n![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)"
  },
  {
    "path": "content/posts/go/gin/2018-08-26-makefile.md",
    "content": "---\n\ntitle:      \"「番外」请入门 Makefile\"\ndate:       2018-08-26 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n--- \n\n## 知识点\n\n- 写一个 Makefile\n\n## 本文目标\n\n含一定复杂度的软件工程，基本上都是先编译 A，再依赖 B，再编译 C...，最后才执行构建。如果每次都人为编排，又或是每新来一个同事就问你项目 D 怎么构建、重新构建需要注意什么...等等情况，岂不是要崩溃？\n\n我们常常会在开源项目中发现 Makefile，你是否有过疑问？\n\n本章节会简单介绍 Makefile 的使用方式，最后建议深入学习。\n\n## 怎么解决\n\n对于构建编排，Docker 有 Dockerfile ，在 Unix 中有神器 [Make](https://en.wikipedia.org/wiki/Make_%28software%29) ....\n\n## Make\n\n### 是什么\n\nMake 是一个构建自动化工具，会在当前目录下寻找 Makefile 或 makefile 文件。如果存在，会依据 Makefile 的**构建规则**去完成构建\n\n当然了，实际上 Makefile 内都是你根据 make 语法规则，自己编写的特定 Shell 命令等\n\n它是一个工具，规则也很简单。在支持的范围内，编译 A， 依赖 B，再编译 C，完全没问题\n\n### 规则\n\nMakefile 由多条规则组成，每条规则都以一个 target（目标）开头，后跟一个 : 冒号，冒号后是这一个目标的 prerequisites（前置条件）\n\n紧接着新的一行，必须以一个 tab 作为开头，后面跟随 command（命令），也就是你希望这一个 target 所执行的构建命令\n\n```\n[target] ... : [prerequisites] ...\n<tab>[command]\n    ...\n    ...\n```\n\n- target：一个目标代表一条规则，可以是一个或多个文件名。也可以是某个操作的名字（标签），称为**伪目标（phony）**\n- prerequisites：前置条件，这一项是**可选参数**。通常是多个文件名、伪目标。它的作用是 target 是否需要重新构建的标准，如果前置条件不存在或有过更新（文件的最后一次修改时间）则认为 target 需要重新构建\n- command：构建这一个 target 的具体命令集\n\n### 简单的例子\n\n本文将以 [go-gin-example](https://github.com/EDDYCJY/go-gin-example) 去编写 Makefile 文件，请跨入 make 的大门\n\n#### 分析\n\n在编写 Makefile 前，需要先分析构建先后顺序、依赖项，需要解决的问题等\n\n#### 编写\n\n```makefile\n.PHONY: build clean tool lint help\n\nall: build\n\nbuild:\n\tgo build -v .\n\ntool:\n\tgo tool vet . |& grep -v vendor; true\n\tgofmt -w .\n\nlint:\n\tgolint ./...\n\nclean:\n\trm -rf go-gin-example\n\tgo clean -i .\n\nhelp:\n\t@echo \"make: compile packages and dependencies\"\n\t@echo \"make tool: run specified go tool\"\n\t@echo \"make lint: golint ./...\"\n\t@echo \"make clean: remove object files and cached files\"\n```\n\n1、在上述文件中，使用了 `.PHONY`，其作用是声明 build / clean / tool / lint / help 为**伪目标**，声明为伪目标会怎么样呢？\n\n- 声明为伪目标后：在执行对应的命令时，make 就不会去检查是否存在 build / clean / tool / lint / help 其对应的文件，而是每次都会运行标签对应的命令\n\n- 若不声明：恰好存在对应的文件，则 make 将会认为 xx 文件已存在，没有重新构建的必要了\n\n2、这块比较简单，在命令行执行即可看见效果，实现了以下功能：\n\n1. make: make 就是 make all\n2. make build: 编译当前项目的包和依赖项\n3. make tool: 运行指定的 Go 工具集\n4. make lint: golint 一下\n5. make clean: 删除对象文件和缓存文件\n6. make help: help\n\n#### 为什么会打印执行的命令\n\n如果你实际操作过，可能会有疑问。明明只是执行命令，为什么会打印到标准输出上了？\n\n##### 原因\n\nmake 默认会打印每条命令，再执行。这个行为被定义为**回声**\n\n##### 解决\n\n可以在对应命令前加上 @，可指定该命令不被打印到标准输出上\n\n```makefile\nbuild:\n\t@go build -v .\n```\n\n那么还有其他的特殊符号吗？有的，请课后去了解下 +、- 的用途 🤩\n\n## 小结\n\n这是一篇比较简洁的文章，希望可以让您对 Makefile 有一个基本了解。\n\n## 参考\n\n### 本系列示例代码\n\n- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)\n\n## 关于\n\n### 修改记录\n\n- 第一版：2018 年 02 月 16 日发布文章\n- 第二版：2019 年 10 月 01 日修改文章\n\n## ？\n\n如果有任何疑问或错误，欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见，如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进。\n\n### 我的公众号\n\n![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)"
  },
  {
    "path": "content/posts/go/gin/2018-09-01-nginx.md",
    "content": "---\n\ntitle:      \"「连载十七」用Nginx部署Go应用\"\ndate:       2018-09-01 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n--- \n\n## 知识点\n\n- Nginx。\n- 反向代理。\n\n## 本文目标\n\n简单部署后端服务。\n\n## 做什么\n\n在本章节，我们将简单介绍 Nginx 以及使用 Nginx 来完成对 [go-gin-example](https://github.com/EDDYCJY/go-gin-example) 的部署，会实现反向代理和简单负载均衡的功能。\n\n## Nginx\n\n### 是什么\n\nNginx 是一个 Web Server，可以用作反向代理、负载均衡、邮件代理、TCP / UDP、HTTP 服务器等等，它拥有很多吸引人的特性，例如：\n\n- 以较低的内存占用率处理 10,000 多个并发连接（每 10k 非活动 HTTP 保持活动连接约 2.5 MB ）\n- 静态服务器（处理静态文件）\n- 正向、反向代理\n- 负载均衡\n- 通过 OpenSSL 对 TLS / SSL 与 SNI 和 OCSP 支持\n- FastCGI、SCGI、uWSGI 的支持\n- WebSockets、HTTP/1.1 的支持\n- Nginx + Lua\n\n### 安装\n\n请右拐谷歌或百度，安装好 Nginx 以备接下来的使用\n\n### 简单讲解\n\n#### 常用命令\n\n- nginx：启动 Nginx\n- nginx -s stop：立刻停止 Nginx 服务\n- nginx -s reload：重新加载配置文件\n- nginx -s quit：平滑停止 Nginx 服务\n- nginx -t：测试配置文件是否正确\n- nginx -v：显示 Nginx 版本信息\n- nginx -V：显示 Nginx 版本信息、编译器和配置参数的信息\n\n#### 涉及配置\n\n1、 proxy_pass：配置**反向代理的路径**。需要注意的是如果 proxy_pass 的 url 最后为\n/，则表示绝对路径。否则（不含变量下）表示相对路径，所有的路径都会被代理过去\n\n2、 upstream：配置**负载均衡**，upstream 默认是以轮询的方式进行负载，另外还支持**四种模式**，分别是：\n\n（1）weight：权重，指定轮询的概率，weight 与访问概率成正比\n\n（2）ip_hash：按照访问 IP 的 hash 结果值分配\n\n（3）fair：按后端服务器响应时间进行分配，响应时间越短优先级别越高\n\n（4）url_hash：按照访问 URL 的 hash 结果值分配\n\n## 部署\n\n在这里需要对 nginx.conf 进行配置，如果你不知道对应的配置文件是哪个，可执行 `nginx -t` 看一下\n\n```\n$ nginx -t\nnginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok\nnginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful\n```\n\n显然，我的配置文件在 `/usr/local/etc/nginx/` 目录下，并且测试通过\n\n### 反向代理\n\n反向代理是指以代理服务器来接受网络上的连接请求，然后将请求转发给内部网络上的服务器，并将从服务器上得到的结果返回给请求连接的客户端，此时代理服务器对外就表现为一个反向代理服务器。（来自[百科](https://baike.baidu.com/item/%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86/7793488?fr=aladdin)）\n\n![image](https://s2.ax1x.com/2020/02/15/1xlQe0.png)\n\n#### 配置 hosts\n\n由于需要用本机作为演示，因此先把映射配上去，打开 `/etc/hosts`，增加内容：\n\n```\n127.0.0.1       api.blog.com\n```\n\n#### 配置 nginx.conf\n\n打开 nginx 的配置文件 nginx.conf（我的是 /usr/local/etc/nginx/nginx.conf），我们做了如下事情：\n\n增加 server 片段的内容，设置 server_name 为 api.blog.com 并且监听 8081 端口，将所有路径转发到 `http://127.0.0.1:8000/` 下\n\n```\nworker_processes  1;\n\nevents {\n    worker_connections  1024;\n}\n\n\nhttp {\n    include       mime.types;\n    default_type  application/octet-stream;\n\n    sendfile        on;\n    keepalive_timeout  65;\n\n    server {\n        listen       8081;\n        server_name  api.blog.com;\n\n        location / {\n            proxy_pass http://127.0.0.1:8000/;\n        }\n    }\n}\n```\n\n#### 验证\n\n##### 启动 go-gin-example\n\n回到 [go-gin-example](github.com/EDDYCJY/go-gin-example) 的项目下，执行 make，再运行 ./go-gin-exmaple\n\n```sh\n$ make\ngithub.com/EDDYCJY/go-gin-example\n$ ls\nLICENSE        README.md      conf           go-gin-example middleware     pkg            runtime        vendor\nMakefile       README_ZH.md   docs           main.go        models         routers        service\n$ ./go-gin-example\n...\n[GIN-debug] DELETE /api/v1/articles/:id      --> github.com/EDDYCJY/go-gin-example/routers/api/v1.DeleteArticle (4 handlers)\n[GIN-debug] POST   /api/v1/articles/poster/generate --> github.com/EDDYCJY/go-gin-example/routers/api/v1.GenerateArticlePoster (4 handlers)\nActual pid is 14672\n```\n\n##### 重启 nginx\n\n```sh\n$ nginx -t\nnginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok\nnginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful\n$ nginx -s reload\n```\n\n##### 访问接口\n\n![image](https://s2.ax1x.com/2020/02/15/1xlEFS.jpg)\n\n如此，就实现了一个简单的反向代理了，是不是很简单呢\n\n### 负载均衡\n\n负载均衡，英文名称为 Load Balance（常称 LB），其意思就是分摊到多个操作单元上进行执行（来自百科）\n\n你能从运维口中经常听见，XXX 负载怎么突然那么高。 那么它到底是什么呢？\n\n其背后一般有多台 server，系统会根据配置的策略（例如 Nginx 有提供四种选择）来进行动态调整，尽可能的达到各节点均衡，从而提高系统整体的吞吐量和快速响应\n\n#### 如何演示\n\n前提条件为多个后端服务，那么势必需要多个 [go-gin-example](https://github.com/EDDYCJY/go-gin-example)，为了演示我们可以启动多个端口，达到模拟的效果\n\n为了便于演示，分别在启动前将 conf/app.ini 的应用端口修改为 8001 和 8002（也可以做成传入参数的模式），达到启动 2 个监听 8001 和 8002 的后端服务\n\n#### 配置 nginx.conf\n\n回到 nginx.conf 的老地方，增加负载均衡所需的配置。新增 upstream 节点，设置其对应的 2 个后端服务，最后修改了 proxy_pass 指向（格式为 http:// + upstream 的节点名称）\n\n```\nworker_processes  1;\n\nevents {\n    worker_connections  1024;\n}\n\n\nhttp {\n    include       mime.types;\n    default_type  application/octet-stream;\n\n    sendfile        on;\n    keepalive_timeout  65;\n\n    upstream api.blog.com {\n        server 127.0.0.1:8001;\n        server 127.0.0.1:8002;\n    }\n\n    server {\n        listen       8081;\n        server_name  api.blog.com;\n\n        location / {\n            proxy_pass http://api.blog.com/;\n        }\n    }\n}\n```\n\n##### 重启 nginx\n\n```sh\n$ nginx -t\nnginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok\nnginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful\n$ nginx -s reload\n```\n\n#### 验证\n\n再重复访问 `http://api.blog.com:8081/auth?username={USER_NAME}}&password={PASSWORD}`，多访问几次便于查看效果\n\n目前 Nginx 没有进行特殊配置，那么它是轮询策略，而 go-gin-example 默认开着 debug 模式，看看请求 log 就明白了\n\n![image](https://s2.ax1x.com/2020/02/15/1xlZWQ.jpg)\n\n![image](https://s2.ax1x.com/2020/02/15/1xlnQs.jpg)\n\n## 总结\n\n在本章节，希望您能够简单习得日常使用的 Web Server 背后都是一些什么逻辑，Nginx 是什么？反向代理？负载均衡？\n\n怎么简单部署，知道了吧。\n\n## 参考\n\n### 本系列示例代码\n\n- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)\n\n## 关于\n\n### 修改记录\n\n- 第一版：2018 年 02 月 16 日发布文章\n- 第二版：2019 年 10 月 01 日修改文章\n\n## ？\n\n如果有任何疑问或错误，欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见，如果喜欢或对你有所帮助，欢迎 Star，对作者是一种鼓励和推进。\n\n### 我的公众号\n\n![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)"
  },
  {
    "path": "content/posts/go/gmp-why-p.md",
    "content": "---\ntitle: \"经典面试题：你觉得 Go 在什么时候会抢占 P？\"\ndate: 2021-06-24T12:42:05+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好，我是煎鱼。\n\n前几天我们有聊到《单核 CPU，开两个 Goroutine，其中一个死循环，会怎么样？》的问题，我们在一个细节部分有提到：\n\n![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6d9cdfb09590496daeca3675aae08611~tplv-k3u1fbpfcp-zoom-1.image)\n\n有新的小伙伴会产生更多的疑问，那就是在 Go 语言中，是如何抢占 P 的呢，这里面是怎么做的？\n\n今天这篇文章我们就来解密抢占 P。\n\n## 调度器的发展史\n\n在 Go 语言中，Goroutine 早期是没有设计成抢占式的，早期 Goroutine 只有读写、主动让出、锁等操作时才会触发调度切换。\n\n这样有一个严重的问题，就是垃圾回收器进行 STW 时，如果有一个 Goroutine 一直都在阻塞调用，垃圾回收器就会一直等待他，不知道等到什么时候...\n\n这种情况下就需要抢占式调度来解决问题。如果一个 Goroutine 运行时间过久，就需要进行抢占来解决。\n\n这块 Go 语言在 Go1.2 起开始实现抢占式调度器，不断完善直至今日：\n- Go0.x：基于单线程的程调度器。\n- Go1.0：基于多线程的调度器。\n- Go1.1：基于任务窃取的调度器。\n- Go1.2 - Go1.13：基于协作的抢占式调度器。\n- Go1.14：基于信号的抢占式调度器。\n\n调度器的新提案：非均匀存储器访问调度（Non-uniform memory access，NUMA），\n但由于实现过于复杂，优先级也不够高，因此迟迟未提上日程。\n\n有兴趣的小伙伴可以详见 Dmitry Vyukov, dvyukov 所提出的 [NUMA-aware scheduler for Go](https://docs.google.com/document/u/0/d/1d3iI2QWURgDIsSR6G2275vMeQ_X7w-qxM2Vp7iGwwuM/pub)。\n\n## 为什么要抢占 P\n\n为什么会要想去抢占 P 呢，说白了就是不抢，就没机会运行，会 hang 死。又或是资源分配不均了，\n\n这在调度器设计中显然是不合理的。\n\n跟这个例子一样：\n\n```golang\n// Main Goroutine \nfunc main() {\n    // 模拟单核 CPU\n    runtime.GOMAXPROCS(1)\n    \n    // 模拟 Goroutine 死循环\n    go func() {\n        for {\n        }\n    }()\n\n    time.Sleep(time.Millisecond)\n    fmt.Println(\"脑子进煎鱼了\")\n}\n```\n\n这个例子在老版本的 Go 语言中，就会一直阻塞，没法重见天日，是一个需要做抢占的场景。\n\n但可能会有小伙伴问，抢占了，会不会有新问题。因为原本正在使用 P 的 M 就凉凉了（M 会与 P 进行绑定），没了 P 也就没法继续执行了。\n\n这其实没有问题，因为该 Goroutine 已经阻塞在了系统调用上，暂时是不会有后续的执行新诉求。\n\n但万一代码是在运行了好一段时间后又能够运行了（业务上也允许长等待），也就是该 Goroutine 从阻塞状态中恢复了，期望继续运行，没了 P 怎么办？\n\n这时候该 Goroutine 可以和其他 Goroutine 一样，先检查自身所在的 M 是否仍然绑定着 P：\n- 若是有 P，则可以调整状态，继续运行。\n- 若是没有 P，可以重新抢 P，再占有并绑定 P，为自己所用。\n\n也就是抢占 P，本身就是一个双向行为，你抢了我的 P，我也可以去抢别人的 P 来继续运行。\n\n## 怎么抢占 P\n\n讲解了为什么要抢占 P 的原因后，我们进一步深挖，“他” 是怎么抢占到具体的 P 的呢？\n\n这就涉及到前文所提到的 `runtime.retake` 方法了，其处理以下两种场景：\n- 抢占阻塞在系统调用上的 P。\n- 抢占运行时间过长的 G。\n\n在此主要针对抢占 P 的场景，分析如下：\n\n```golang\nfunc retake(now int64) uint32 {\n\tn := 0\n\t// 防止发生变更，对所有 P 加锁\n\tlock(&allpLock)\n\t// 走入主逻辑，对所有 P 开始循环处理\n\tfor i := 0; i < len(allp); i++ {\n\t\t_p_ := allp[i]\n\t\tpd := &_p_.sysmontick\n\t\ts := _p_.status\n\t\tsysretake := false\n\t\t...\n\t\tif s == _Psyscall {\n\t\t\t// 判断是否超过 1 个 sysmon tick 周期\n\t\t\tt := int64(_p_.syscalltick)\n\t\t\tif !sysretake && int64(pd.syscalltick) != t {\n\t\t\t\tpd.syscalltick = uint32(t)\n\t\t\t\tpd.syscallwhen = now\n\t\t\t\tcontinue\n\t\t\t}\n      \n\t\t\t...\n\t\t}\n\t}\n\tunlock(&allpLock)\n\treturn uint32(n)\n}\n```\n\n该方法会先对 `allpLock` 上锁，这个变量含义如其名，`allpLock` 可以防止该数组发生变化。\n\n其会保护 `allp`、`idlepMask` 和 `timerpMask` 属性的无 `P` 读取和大小变化，以及对 `allp` 的所有写入操作，可以避免影响后续的操作。\n\n### 场景一\n\n前置处理完毕后，进入主逻辑，会使用万能的 `for` 循环对所有的 P（allp）进行一个个处理。\n\n```golang\n\t\t\tt := int64(_p_.syscalltick)\n\t\t\tif !sysretake && int64(pd.syscalltick) != t {\n\t\t\t\tpd.syscalltick = uint32(t)\n\t\t\t\tpd.syscallwhen = now\n\t\t\t\tcontinue\n\t\t\t}\n```\n\n第一个场景是：会对 `syscalltick` 进行判定，如果在系统调用（syscall）中存在超过 1 个 sysmon tick 周期（至少 20us）的任务，则会从系统调用中抢占 P，否则跳过。\n\n### 场景二\n\n如果未满足会继续往下，走到如下逻辑：\n\n```golang\nfunc retake(now int64) uint32 {\n\tfor i := 0; i < len(allp); i++ {\n\t\t...\n\t\tif s == _Psyscall {\n\t\t\t// 从此处开始分析\n\t\t\tif runqempty(_p_) && \n      atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) > 0 && \n      pd.syscallwhen+10*1000*1000 > now {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t...\n\t\t}\n\t}\n\tunlock(&allpLock)\n\treturn uint32(n)\n}\n```\n第二个场景，聚焦到这一长串的判断中：\n- `runqempty(_p_) == true` 方法会判断任务队列 P 是否为空，以此来检测有没有其他任务需要执行。\n- `atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) > 0` 会判断是否存在空闲 P 和正在进行调度窃取 G 的 P。\n- `pd.syscallwhen+10*1000*1000 > now` 会判断系统调用时间是否超过了 10ms。\n\n这里奇怪的是 `runqempty` 方法明明已经判断了没有其他任务，这就代表了没有任务需要执行，是不需要抢夺 P 的。\n\n但实际情况是，由于可能会阻止 sysmon 线程的深度睡眠，最终还是希望继续占有 P。\n\n在完成上述判断后，进入到抢夺 P 的阶段：\n\n```golang\nfunc retake(now int64) uint32 {\n\tfor i := 0; i < len(allp); i++ {\n\t\t...\n\t\tif s == _Psyscall {\n\t\t\t// 承接上半部分\n\t\t\tunlock(&allpLock)\n\t\t\tincidlelocked(-1)\n\t\t\tif atomic.Cas(&_p_.status, s, _Pidle) {\n\t\t\t\tif trace.enabled {\n\t\t\t\t\ttraceGoSysBlock(_p_)\n\t\t\t\t\ttraceProcStop(_p_)\n\t\t\t\t}\n\t\t\t\tn++\n\t\t\t\t_p_.syscalltick++\n\t\t\t\thandoffp(_p_)\n\t\t\t}\n\t\t\tincidlelocked(1)\n\t\t\tlock(&allpLock)\n\t\t}\n\t}\n\tunlock(&allpLock)\n\treturn uint32(n)\n}\n```\n\n- 解锁相关属性：需要调用 `unlock` 方法解锁 `allpLock`，从而实现获取 `sched.lock`，以便继续下一步。\n- 减少闲置 M：需要在原子操作（CAS）之前减少闲置 M 的数量（假设有一个正在运行）。 否则在发生抢夺 M 时可能会退出系统调用，递增 nmidle 并报告死锁事件。\n- 修改 P 状态：调用 `atomic.Cas` 方法将所抢夺的 P 状态设为 idle，以便于交于其他 M 使用。\n- 抢夺 P 和调控 M：调用 `handoffp` 方法从系统调用或锁定的 M 中抢夺 P，会由新的 M 接管这个 P。\n\n## 总结\n\n至此完成了抢占 P 的基本流程，我们可得出满足以下条件：\n1. 如果存在系统调用超时：存在超过 1 个 sysmon tick 周期（至少 20us）的任务，则会从系统调用中抢占 P。\n2. 如果没有空闲的 P：所有的 P 都已经与 M 绑定。需要抢占当前正处于系统调用之，而实际上系统调用并不需要的这个 P 的情况，会将其分配给其它 M 去调度其它 G。\n3. 如果 P 的运行队列里面有等待运行的 G，为了保证 P 的本地队列中的 G 得到及时调度。而自己本身的 P 又忙于系统调用，无暇管理。此时会寻找另外一个 M 来接管 P，从而实现继续调度 G 的目的。\n\n## 参考\n\n- [NUMA-aware scheduler for Go](https://docs.google.com/document/u/0/d/1d3iI2QWURgDIsSR6G2275vMeQ_X7w-qxM2Vp7iGwwuM/pub)\n- [go-under-the-hood](https://golang.design/under-the-hood/zh-cn/part2runtime/ch06sched/preemption/#p-)\n- [深入解析 Go-抢占式调度](https://tiancaiamao.gitbooks.io/go-internals/content/zh/05.5.html)\n- [Go语言调度器源代码情景分析](https://cloud.tencent.com/developer/article/1450290)"
  },
  {
    "path": "content/posts/go/go-array-slice.md",
    "content": "---\ntitle: \"Go 数组比切片好在哪？\"\ndate: 2021-09-17T12:43:08+08:00\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n前段时间有播放一条快讯，就是 Go1.17 会正式支持切片（Slice）转换到数据（Array），不再需要用以前那种骚办法了，安全了许多。\n\n但是也有同学提出了新的疑惑，在 Go 语言中，数组其实是用的相对较少的，甚至会有同学认为在 Go 里可以把数组给去掉。\n\n![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9764cd832d1e4875953b66434beab8e4~tplv-k3u1fbpfcp-zoom-1.image)\n\n数组相较切片到底有什么优势，我们又应该在什么场景下使用呢？\n\n这是一个我们需要深究的问题，因此今天就跟大家一起来一探究竟，本文会先简单介绍数组和切片是什么，再进一步对数组的使用场景剖析。\n\n一起愉快地开始吸鱼之路。\n\n数组是什么\n-----\n\nGo 语言中有一种基本数据类型，叫数组。其格式为：`[n]T`。是一个包含 N 个类型 T 的值的数组。\n\n基本声明格式为：\n\n```\nvar a [10]int\n\n```\n\n代表的是声明了一个变量 a 是一个包含 10 个整数的数组。数组的长度是其类型的一部分，所以数组不能被随意调整大小。\n\n在使用例子上：\n\n```\nfunc main() {\n var a [2]string\n a[0] = \"脑子进\"\n a[1] = \"煎鱼了\"\n fmt.Println(a[0], a[1])\n fmt.Println(a)\n\n primes := [6]int{2, 3, 5, 7, 11, 13}\n fmt.Println(primes)\n}\n\n```\n\n输出结果：\n\n```\n脑子进 煎鱼了\n[脑子进 煎鱼了]\n[2 3 5 7 11 13]\n\n```\n\n在赋值和访问上，数组可以针对不同的索引，进行单独操作。在内存布局上，数组的索引 0 和 1...是会在相邻区域，可直接访问。\n\n切片是什么\n-----\n\n为什么数组在业务代码似乎用的很少。因为 Go 语言有一个切片的数据类型：\n\n基本声明格式为：\n\n```\nvar a []T\n\n```\n\n代表的是变量 a 是带有类型元素的切片T。通过指定两个索引（下限和上限）并用冒号隔开来形成切片：\n\n```\na[low : high]\n\n```\n\n在使用例子上：\n\n```\nfunc main() {\n primes := [3]string{\"煎鱼\", \"搞\", \"Go\"}\n\n var s []string = primes[1:3]\n fmt.Println(s)\n}\n\n```\n\n输出结果：\n\n```\n[搞 Go]\n\n```\n\n切片支持动态的扩缩容，不需要用户侧去关注，非常便利。更重要的一点是，切片的底层数据结构中本身就包含了数组：\n\n```\ntype slice struct {\n array unsafe.Pointer\n len   int\n cap   int\n}\n\n```\n\n也就很多人笑称：**在 Go 语言中数组已经可以下岗了，用切片就完事了**...\n\n你怎么看待这个说法的呢，快速思考你心中的答案。\n\n数组的优势\n-----\n\n在风尘仆仆介绍完数组和切片的基本场景后，在数组的优势方面，先了解一下官方的自述：\n\n> > Arrays are useful when planning the detailed layout of memory and sometimes can help avoid allocation, but primarily they are a building block for slices.\n\n非常粗暴间接：在规划内存的详细布局时，数组是很有用的，有时可以帮助避免分配，但主要是它们是分片的构建块。\n\n我们再进一步解读，看看官方这股 “密文” 具体指的是什么，我们将该密文解读为以下内容进行讲解：\n\n*   可比较。\n    \n*   编译安全。\n    \n*   长度是类型。\n    \n*   规划内存布局。\n    \n*   访问速度。\n    \n\n### 可比较\n\n数组是固定长度的，它们之间是可以进行比较的，数组是值对象（不是引用或指针类型），你不会遇到 interface 等比较的误判：\n\n```\nfunc main() {\n a1 := [3]string{\"脑子\", \"进\", \"煎鱼了\"}\n a2 := [3]string{\"煎鱼\", \"进\", \"脑子了\"}\n a3 := [3]string{\"脑子\", \"进\", \"煎鱼了\"}\n\n fmt.Println(a1 == a2, a1 == a3)\n}\n\n```\n\n输出结果：\n\n```\nfalse true\n\n```\n\n另一方面，切片不可以直接比较，也不能用于判断：\n\n```\nfunc main() {\n a1 := []string{\"脑子\", \"进\", \"煎鱼了\"}\n a2 := []string{\"煎鱼\", \"进\", \"脑子了\"}\n a3 := []string{\"脑子\", \"进\", \"煎鱼了\"}\n\n fmt.Println(a1 == a2, a1 == a3)\n}\n\n```\n\n输出结果：\n\n```\n# command-line-arguments\n./main.go:10:17: invalid operation: a1 == a2 (slice can only be compared to nil)\n./main.go:10:27: invalid operation: a1 == a3 (slice can only be compared to nil)\n\n```\n\n同时数组可以作为 map 的 k（键），而切片不行，切片并没有实现平等运算符（equality operator），需要考虑的问题有非常多，例如：\n\n*   涉及浅层与深层比较。\n    \n*   指针与值比较。\n    \n*   如何处理递归类型。\n    \n\n平等是为结构体和数组定义的，所以这类类型可以作为 map 键使用。切片没有平等的定义，有着非常根本的差距。\n\n数组的可比较和平等，切片做不到。\n\n### 编译安全\n\n数组可以提供更高的编译时安全，可以在编译时检查索引范围。如下：\n\n```\ns := make([]int, 3)\ns[3] = 3 // \"Only\" a runtime panic: runtime error: index out of range\n\na := [3]int{}\na[3] = 3 // Compile-time error: invalid array index 3 (out of bounds for 3-element array)\n\n```\n\n这个编译检查的帮助虽 “小”，但其实非常有意义。我是日常看到各大切片越界的告警，感觉都能背下来了...\n\n万一这个越界是在 hot path 上，影响大量用户，分分钟背个事故，再来个 3.25，岂不梦中惊醒？\n\n数组的编译安全，切片做不到。\n\n### 长度是类型\n\n数组的长度是数组类型声明的一部分，因此**长度不同的数组是不同的类型**，两个就不是一个 “东西”。\n\n当然，这是一把双刃剑。其优势在于：可用于显式指定所需数组的长度。\n\n例如：你在业务代码中想编写一个使用 IPv4 地址的函数。可以声明 `type [4]byte`。使用数组有以下意识：\n\n*   有了编译时的保证，也就是达到传递给你的函数的值将恰好具有4个字节，不多也不少的效果。\n    \n*   如果长度不对，也就可以认为是无效的 IPv4 地址，非常方便。\n    \n\n同时数组的长度，也可以用做记录目的：\n\n*   MD5 类型，在 `crypto/md5`包中，`md5.Sum` 方法返回类型为的值，`[Size]byte` 其中 `md5.Size` 一个常量为16：MD5 校验和的长度。\n    \n*   IPv4 类型，所声明的 `[4]byte` 正确记录了有 4 个字节。\n    \n*   RGB 类型，所声明的 `[3]byte` 告诉有对每个颜色成分 1 个字节。\n    \n\n在特定业务场景上，使用数组更好。\n\n### 规划内存布局\n\n数组可以更好地控制内存布局，因为不能直接在带有切片的结构中分配空间，所以可以使用数组来解决。\n\n例如：\n\n```\ntype Foo struct {\n    buf [64]byte\n}\n\n```\n\n不知道你是否有在一些 Go 图形库上见过这种不明所以的操作，例子如下：\n\n```\ntype TGIHeader struct {\n    _        uint16 // Reserved\n    _        uint16 // Reserved\n    Width    uint32\n    Height   uint32\n    _        [15]uint32 // 15 \"don't care\" dwords\n    SaveTime int64\n}\n\n```\n\n因为业务需求，我们需要实现一个格式，其中格式是 \"TGI\"（理论上的Go Image），头包含这样的字段：\n\n*   有 2 个保留字（每个16位）。\n    \n*   有 1 个字的图像宽度。\n    \n*   有 1 个字的图像高度。\n    \n*   有 15 个业务 \"不在乎 \"的字节。\n    \n*   有 1 个保存时间，图像的保存时间为8字节，是自1970年1月1日UTC以来的纳秒数。\n    \n\n这么一看，也就不难理解数组的在这个场景下的优势了。定长，可控的内存，在计划内存布局时非常有用。\n\n### 访问速度\n\n使用数组时，其访问（单个）数组元素比访问切片元素更高效，时间复杂度是 O（1）。例如：\n\n```\n var a [2]string\n a[0] = \"脑子进\"\n a[1] = \"煎鱼了\"\n fmt.Println(a[0], a[1])\n\n```\n\n切片就没那么方便了，访问某个位置上的索引值，需要：\n\n```\n var a []int{0, 1, 2, 3, 4, 5}  \n  number := numbers[1:3]\n\n```\n\n相对复杂些的，删除指定索引位上的值，可能还有小伙伴纠结半天，甚至在找第三方开源库想快速实现。\n\n无论在访问速度和开发效率上，数组都占一定的优势，这是切片所无法直接对比的。\n\n总结\n--\n\n经过一轮的探讨，我们对 Go 语言的数组有了更深入的理解。总结如下：\n\n*   数组是值对象，可以进行比较，可以将数组用作 map 的映射键。而这些，切片都不可以，不能比较，无法作为 map 的映射键。\n    \n*   数组有编译安全的检查，可以在早起就避免越界行为。切片是在运行时会出现越界的 panic，阶段不同。\n    \n*   数组可以更好地控制内存布局，若拿切片替换，会发现不能直接在带有切片的结构中分配空间，数组可以。\n    \n*   数组在访问单个元素时，性能比切片好。\n    \n*   数组的长度，是类型的一部分。在特定场景下具有一定的意义。\n    \n*   数组是切片的基础，每个数组都可以是一个切片，但并非每个切片都可以是一个数组。如果值是固定大小，可以通过使用数组来获得较小的性能提升（至少节省 slice 头占用的空间）。\n    \n\n与你心目中的数组的优势是否一致呢，欢迎大家在评论区进行讨论和交流。\n\n我是煎鱼，咱们下期再见：）\n\n\n参考\n--\n\n*   In GO programming language what are the benefits of using Arrays over Slices?\n    \n*   Why have arrays in Go?"
  },
  {
    "path": "content/posts/go/go-bootstrap.md",
    "content": "---\ntitle: \"Go 应用程序是怎么运行起来的？\"\ndate: 2020-10-08T15:57:18+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n自古应用程序均从 Hello World 开始，你我所写的 Go 语言亦然：\n\n```\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Println(\"hello world.\")\n}\n```\n\n这段程序的输出结果为 `hello world.`，就是这么的简单又直接。但这时候又不禁思考了起来，这个 `hello world.` 是怎么输出来，经历了什么过程。\n\n真是非常的好奇，今天我们就通过这篇文章来一探究竟。\n\n## 引导阶段\n\n### 查找入口\n\n开始剖析之路，首先编译上文提到的示例程序：\n\n```shell\n$ GOFLAGS=\"-ldflags=-compressdwarf=false\" go build \n```\n\n在命令中指定了 GOFLAGS 参数，这是因为在 Go1.11 起，为了减少二进制文件大小，调试信息会被压缩。导致在 MacOS 上使用 gdb 时无法理解压缩的 DWARF 的含义是什么（而我恰恰就是用的 MacOS）。\n\n因此需要在本次调试中将其关闭，再使用 gdb 进行调试，以此达到观察的目的：\n\n```shell\n$ gdb awesomeProject \n(gdb) info files\nSymbols from \"/Users/eddycjy/go-application/awesomeProject/awesomeProject\".\nLocal exec file:\n\t`/Users/eddycjy/go-application/awesomeProject/awesomeProject', file type mach-o-x86-64.\n\tEntry point: 0x1063c80\n\t0x0000000001001000 - 0x00000000010a6aca is .text\n\t...\n(gdb) b *0x1063c80\nBreakpoint 1 at 0x1063c80: file /usr/local/Cellar/go/1.15/libexec/src/runtime/rt0_darwin_amd64.s, line 8.\n```\n\n通过 Entry point 的调试，可看到真正的程序入口在 runtime 包中，不同的计算机架构指向不同，例如：MacOS 在 `src/runtime/rt0_darwin_amd64.s`，Linux 在 `src/runtime/rt0_linux_amd64.s`。\n\n```\nBreakpoint 1 at 0x1063c80: file /usr/local/Cellar/go/1.15/libexec/src/runtime/rt0_darwin_amd64.s, line 8.\n```\n\n其最终指向了 rt0_darwin_amd64.s 文件，这个文件名称非常的直观，rt0 代表 runtime0 的缩写，指代运行时的创世，超级奶爸；darwin 代表目标操作系统（GOOS），amd64 代表目标操作系统架构（GOHOSTARCH）。\n\n同时 Go 语言还支持更多的目标系统架构，例如：AMD64、AMR、MIPS、WASM 等：\n\n![image](https://image.eddycjy.com/981720dfbce750bec26fc394e97d9ff7.jpg)\n\n若有兴趣可到 `src/runtime` 目录下进一步查看。\n\n### 入口方法\n\n在 rt0_linux_amd64.s 文件中，可发现 `_rt0_amd64_darwin` JMP 跳转到了 `_rt0_amd64` 方法：\n\n```\nTEXT _rt0_amd64_darwin(SB),NOSPLIT,$-8\n\tJMP\t_rt0_amd64(SB)\n...\n```\n\n紧接着又跳转到 `runtime·rt0_go` 方法：\n\n```\nTEXT _rt0_amd64(SB),NOSPLIT,$-8\n\tMOVQ\t0(SP), DI\t// argc\n\tLEAQ\t8(SP), SI\t// argv\n\tJMP\truntime·rt0_go(SB)\n```\n\n该方法将程序输入的 argc 和 argv 从内存移动到寄存器中。栈指针（SP）的前两个值分别是 argc 和 argv，其对应参数的数量和具体各参数的值。\n\n### 开启主线\n\n程序参数准备就绪后，正式初始化的方法落在 `runtime·rt0_go` 方法中：\n\n```\nTEXT runtime·rt0_go(SB),NOSPLIT,$0\n\t...\n\tCALL\truntime·check(SB)\n\tMOVL\t16(SP), AX\t\t// copy argc\n\tMOVL\tAX, 0(SP)\n\tMOVQ\t24(SP), AX\t\t// copy argv\n\tMOVQ\tAX, 8(SP)\n\tCALL\truntime·args(SB)\n\tCALL\truntime·osinit(SB)\n\tCALL\truntime·schedinit(SB)\n\n\t// create a new goroutine to start program\n\tMOVQ\t$runtime·mainPC(SB), AX\t\t// entry\n\tPUSHQ\tAX\n\tPUSHQ\t$0\t\t\t// arg size\n\tCALL\truntime·newproc(SB)\n\tPOPQ\tAX\n\tPOPQ\tAX\n\n\t// start this M\n\tCALL\truntime·mstart(SB)\n\t...\n```\n\n\n- runtime.check：运行时类型检查，主要是校验编译器的翻译工作是否正确，是否有 “坑”。基本代码均为检查 `int8` 在 `unsafe.Sizeof` 方法下是否等于 1 这类动作。\n\n- runtime.args：系统参数传递，主要是将系统参数转换传递给程序使用。\n\n- runtime.osinit：系统基本参数设置，主要是获取 CPU 核心数和内存物理页大小。\n\n- runtime.schedinit：进行各种运行时组件的初始化，包含调度器、内存分配器、堆、栈、GC 等一大堆初始化工作。是后续的重点关注对象。\n\n- runtime·main：主要工作是运行 main goroutine，虽然在`runtime·rt0_go` 中指向的是`$runtime·mainPC` ，但实质指向的是 `runtime.main`。\n\n- runtime.newproc：创建一个新的 goroutine 将其放入 g 的等待运行队列中去。且绑定 `runtime.main` 方法，也就是应用程序中的入口 main 方法。\n\n- runtime.mstart：调度器开始进行循环调度。\n\n在 `runtime·rt0_go` 方法中，其主要是完成各类运行时的检查，系统参数设置和获取，并进行大量的 Go 基础组件初始化。初始化完毕后进行 main goroutine 的运行，并放入等待队列（GMP），最后调度器开始进行循环调度。\n\n## 总结\n\n根据上述源码剖析，可以得出如下 Go 应用程序引导的流程图：\n\n![image](https://image.eddycjy.com/057c1ccb06c16e8c5f38ff5800e3fa63.jpg)\n\n在 Go 语言中，实际的运行入口并不是用户日常所写的 `main func`，更不是 `runtime.main` 方法，而是从 `rt0_*_amd64.s` 开始，最终再一路 JMP 到 `runtime·rt0_go` 里去，再在该方法里完成一系列 Go 自身所需要完成的绝大部分初始化动作。\n\n其中包括运行时类型检查、系统参数传递、CPU 核数获取及设置、运行时组件的初始化（调度器、内存分配器、堆、栈、GC 等）、运行 main goroutine 和相应的 GMP 等大量缺省行为，还会涉及到调度器相关的大量知识。\n\n后续将会继续剖析将进一步剖析 `runtime·rt0_go` 里的爱与恨，尤其像是 `runtime.main`、`runtime.schedinit` 等调度方法，都有非常大的学习价值，有兴趣的小伙伴可以持续关注。\n"
  },
  {
    "path": "content/posts/go/go-bootstrap0.md",
    "content": "---\ntitle: \"详解 Go 程序的启动流程，你知道 g0，m0 是什么吗？\"\ndate: 2021-06-17T12:42:42+08:00\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n自古应用程序均从 Hello World 开始，你我所写的 Go 语言亦然：\n\n```\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Println(\"hello world.\")\n}\n```\n\n这段程序的输出结果为 `hello world.`，就是这么的简单又直接。但这时候又不禁思考了起来，这个 `hello world.` 是怎么输出来，经历了什么过程。\n\n真是非常的好奇，今天我们就一起来探一探 Go 程序的启动流程。\n其中涉及到 Go Runtime 的调度器启动，g0，m0 又是什么？\n\n车门焊死，正式开始吸鱼之路。\n\n## Go 引导阶段\n\n### 查找入口\n\n首先编译上文提到的示例程序：\n\n```shell\n$ GOFLAGS=\"-ldflags=-compressdwarf=false\" go build \n```\n\n在命令中指定了 GOFLAGS 参数，这是因为在 Go1.11 起，为了减少二进制文件大小，调试信息会被压缩。导致在 MacOS 上使用 gdb 时无法理解压缩的 DWARF 的含义是什么（而我恰恰就是用的 MacOS）。\n\n因此需要在本次调试中将其关闭，再使用 gdb 进行调试，以此达到观察的目的：\n\n```shell\n$ gdb awesomeProject \n(gdb) info files\nSymbols from \"/Users/eddycjy/go-application/awesomeProject/awesomeProject\".\nLocal exec file:\n\t`/Users/eddycjy/go-application/awesomeProject/awesomeProject', file type mach-o-x86-64.\n\tEntry point: 0x1063c80\n\t0x0000000001001000 - 0x00000000010a6aca is .text\n\t...\n(gdb) b *0x1063c80\nBreakpoint 1 at 0x1063c80: file /usr/local/Cellar/go/1.15/libexec/src/runtime/rt0_darwin_amd64.s, line 8.\n```\n\n通过 Entry point 的调试，可看到真正的程序入口在 runtime 包中，不同的计算机架构指向不同。例如：\n- MacOS 在 `src/runtime/rt0_darwin_amd64.s`。\n- Linux 在 `src/runtime/rt0_linux_amd64.s`。\n\n其最终指向了 rt0_darwin_amd64.s 文件，这个文件名称非常的直观：\n\n```\nBreakpoint 1 at 0x1063c80: file /usr/local/Cellar/go/1.15/libexec/src/runtime/rt0_darwin_amd64.s, line 8.\n```\n\nrt0 代表 runtime0 的缩写，指代运行时的创世，超级奶爸：\n- darwin 代表目标操作系统（GOOS）。\n- amd64 代表目标操作系统架构（GOHOSTARCH）。\n\n同时 Go 语言还支持更多的目标系统架构，例如：AMD64、AMR、MIPS、WASM 等：\n\n![源码目录](https://image.eddycjy.com/981720dfbce750bec26fc394e97d9ff7.jpg)\n\n若有兴趣可到 `src/runtime` 目录下进一步查看，这里就不一一介绍了。\n\n### 入口方法\n\n在 rt0_linux_amd64.s 文件中，可发现 `_rt0_amd64_darwin` JMP 跳转到了 `_rt0_amd64` 方法：\n\n```\nTEXT _rt0_amd64_darwin(SB),NOSPLIT,$-8\n\tJMP\t_rt0_amd64(SB)\n...\n```\n\n紧接着又跳转到 `runtime·rt0_go` 方法：\n\n```\nTEXT _rt0_amd64(SB),NOSPLIT,$-8\n\tMOVQ\t0(SP), DI\t// argc\n\tLEAQ\t8(SP), SI\t// argv\n\tJMP\truntime·rt0_go(SB)\n```\n\n该方法将程序输入的 argc 和 argv 从内存移动到寄存器中。\n\n栈指针（SP）的前两个值分别是 argc 和 argv，其对应参数的数量和具体各参数的值。\n\n### 开启主线\n\n程序参数准备就绪后，正式初始化的方法落在 `runtime·rt0_go` 方法中：\n\n```\nTEXT runtime·rt0_go(SB),NOSPLIT,$0\n\t...\n\tCALL\truntime·check(SB)\n\tMOVL\t16(SP), AX\t\t// copy argc\n\tMOVL\tAX, 0(SP)\n\tMOVQ\t24(SP), AX\t\t// copy argv\n\tMOVQ\tAX, 8(SP)\n\tCALL\truntime·args(SB)\n\tCALL\truntime·osinit(SB)\n\tCALL\truntime·schedinit(SB)\n\n\t// create a new goroutine to start program\n\tMOVQ\t$runtime·mainPC(SB), AX\t\t// entry\n\tPUSHQ\tAX\n\tPUSHQ\t$0\t\t\t// arg size\n\tCALL\truntime·newproc(SB)\n\tPOPQ\tAX\n\tPOPQ\tAX\n\n\t// start this M\n\tCALL\truntime·mstart(SB)\n\t...\n```\n\n- runtime.check：运行时类型检查，主要是校验编译器的翻译工作是否正确，是否有 “坑”。基本代码均为检查 `int8` 在 `unsafe.Sizeof` 方法下是否等于 1 这类动作。\n- runtime.args：系统参数传递，主要是将系统参数转换传递给程序使用。\n- runtime.osinit：系统基本参数设置，主要是获取 CPU 核心数和内存物理页大小。\n- runtime.schedinit：进行各种运行时组件的初始化，包含调度器、内存分配器、堆、栈、GC 等一大堆初始化工作。会进行 p 的初始化，并将 m0 和某一个 p 进行绑定。\n- runtime.main：主要工作是运行 main goroutine，虽然在`runtime·rt0_go` 中指向的是`$runtime·mainPC`，但实质指向的是 `runtime.main`。\n- runtime.newproc：创建一个新的 goroutine，且绑定 `runtime.main` 方法（也就是应用程序中的入口 main 方法）。并将其放入 m0 绑定的p的本地队列中去，以便后续调度。\n- runtime.mstart：启动 m，调度器开始进行循环调度。\n\n在 `runtime·rt0_go` 方法中，其主要是完成各类运行时的检查，系统参数设置和获取，并进行大量的 Go 基础组件初始化。\n\n初始化完毕后进行主协程（main goroutine）的运行，并放入等待队列（GMP 模型），最后调度器开始进行循环调度。\n\n### 小结\n\n根据上述源码剖析，可以得出如下 Go 应用程序引导的流程图：\n\n![Go 程序引导过程](https://image.eddycjy.com/057c1ccb06c16e8c5f38ff5800e3fa63.jpg)\n\n在 Go 语言中，实际的运行入口并不是用户日常所写的 `main func`，更不是 `runtime.main` 方法，而是从 `rt0_*_amd64.s` 开始，最终再一路 JMP 到 `runtime·rt0_go` 里去，再在该方法里完成一系列 Go 自身所需要完成的绝大部分初始化动作。\n\n其中整体包括：\n- 运行时类型检查、系统参数传递、CPU 核数获取及设置、运行时组件的初始化（调度器、内存分配器、堆、栈、GC 等）。\n- 运行 main goroutine。\n- 运行相应的 GMP 等大量缺省行为。\n- 涉及到调度器相关的大量知识。\n\n后续将会继续剖析将进一步剖析 `runtime·rt0_go` 里的爱与恨，尤其像是 `runtime.main`、`runtime.schedinit` 等调度方法，都有非常大的学习价值，有兴趣的小伙伴可以持续关注。\n\n## Go 调度器初始化\n\n知道了 Go 程序是怎么引导起来的之后，我们需要了解 Go Runtime 中调度器是怎么流转的。\n\n### runtime.mstart\n\n这里主要关注 `runtime.mstart` 方法：\n\n```golang\nfunc mstart() {\n\t// 获取 g0\n\t_g_ := getg()\n\n\t// 确定栈边界\n\tosStack := _g_.stack.lo == 0\n\tif osStack {\n\t\tsize := _g_.stack.hi\n\t\tif size == 0 {\n\t\t\tsize = 8192 * sys.StackGuardMultiplier\n\t\t}\n\t\t_g_.stack.hi = uintptr(noescape(unsafe.Pointer(&size)))\n\t\t_g_.stack.lo = _g_.stack.hi - size + 1024\n\t}\n\t_g_.stackguard0 = _g_.stack.lo + _StackGuard\n\t_g_.stackguard1 = _g_.stackguard0\n  \n  // 启动 m，进行调度器循环调度\n\tmstart1()\n\n\t// 退出线程\n\tif mStackIsSystemAllocated() {\n\t\tosStack = true\n\t}\n\tmexit(osStack)\n}\n```\n\n- 调用 `getg` 方法获取 GMP 模型中的 g，此处获取的是 g0。\n- 通过检查 g 的执行栈 `_g_.stack` 的边界（堆栈的边界正好是 lo, hi）来确定是否为系统栈。若是，则根据系统栈初始化 g 执行栈的边界。\n- 调用 `mstart1` 方法启动系统线程 m，进行调度器循环调度。\n- 调用 `mexit` 方法退出系统线程 m。\n\n### runtime.mstart1\n\n这么看来其实质逻辑在 `mstart1` 方法，我们继续往下剖析：\n\n```golang\nfunc mstart1() {\n\t// 获取 g，并判断是否为 g0\n\t_g_ := getg()\n\tif _g_ != _g_.m.g0 {\n\t\tthrow(\"bad runtime·mstart\")\n\t}\n\n\t// 初始化 m 并记录调用方 pc、sp\n\tsave(getcallerpc(), getcallersp())\n\tasminit()\n\tminit()\n\n\t// 设置信号 handler\n\tif _g_.m == &m0 {\n\t\tmstartm0()\n\t}\n\t// 运行启动函数\n\tif fn := _g_.m.mstartfn; fn != nil {\n\t\tfn()\n\t}\n\n\tif _g_.m != &m0 {\n\t\tacquirep(_g_.m.nextp.ptr())\n\t\t_g_.m.nextp = 0\n\t}\n\tschedule()\n}\n```\n\n- 调用 `getg` 方法获取 g。并且通过前面绑定的 `_g_.m.g0` 判断所获取的 g 是否 g0。若不是，则直接抛出致命错误。因为调度器仅在 g0 上运行。\n- 调用 `minit` 方法初始化 m，并记录调用方的 PC、SP，便于后续 schedule 阶段时的复用。\n- 若确定当前的 g 所绑定的 m 是 m0，则调用 `mstartm0` 方法，设置信号 handler。该动作必须在 `minit` 方法之后，这样 `minit` 方法可以提前准备好线程，以便能够处理信号。\n- 若当前 g 所绑定的 m 有启动函数，则运行。否则跳过。\n- 若当前 g 所绑定的 m 不是 m0，则需要调用 `acquirep` 方法获取并绑定 p，也就是 m 与 p 绑定。\n- 调用 `schedule` 方法进行正式调度。\n\n忙活了一大圈，终于进入到开题的主菜了，原来潜伏的很深的 `schedule` 方法才是真正做调度的方法，其他都是前置处理和准备数据。\n\n由于篇幅问题，`schedule` 方法会放到下篇再继续剖析，我们先聚焦本篇的一些细节点。\n\n## 问题深剖\n\n不过到这里篇幅也已经比较长了，积累了不少问题。我们针对在 Runtime 中出镜率最高的两个元素进行剖析：\n1. `m0` 是什么，作用是？\n2. `g0` 是什么，作用是？\n\n### m0\n\nm0 是 Go Runtime 所创建的第一个系统线程，一个 Go 进程只有一个 m0，也叫主线程。\n\n从多个方面来看：\n- 数据结构：m0 和其他创建的 m 没有任何区别。\n- 创建过程：m0 是进程在启动时应该汇编直接复制给 m0 的，其他后续的 m 则都是 Go Runtime 内自行创建的。\n- 变量声明：m0 和常规 m 一样，m0 的定义就是 `var m0 m`，没什么特别之处。\n\n### g0\n\ng 一般分为三种，分别是：\n- 执行用户任务的叫做 g。\n- 执行 `runtime.main` 的 main goroutine。\n- 执行调度任务的叫 g0。。\n\ng0 比较特殊，每一个 m 都只有一个 g0（仅此只有一个 g0），且每个 m 都只会绑定一个 g0。在 g0 的赋值上也是通过汇编赋值的，其余后续所创建的都是常规的 g。\n\n从多个方面来看：\n- 数据结构：g0 和其他创建的 g 在数据结构上是一样的，但是存在栈的差别。在 g0 上的栈分配的是系统栈，在 Linux 上栈大小默认固定 8MB，不能扩缩容。 而常规的 g 起始只有 2KB，可扩容。\n- 运行状态：g0 和常规的 g 不一样，没有那么多种运行状态，也不会被调度程序抢占，调度本身就是在 g0 上运行的。\n- 变量声明：g0 和常规 g，g0 的定义就是 `var g0 g`，没什么特别之处。\n\n### 小结\n\n在本章节中我们讲解了 Go 调度器初始化的一个过程，分别涉及：\n- runtime.mstart。\n- runtime.mstart1。\n\n基于此也了解到了在调度器初始化过程中，需要准备什么，初始化什么。另外针对调度过程中最常提到的 m0、g0 的概念我们进行了梳理和说明。\n\n## 总结\n\n在今天这篇文章中，我们详细的介绍了 Go 语言的引导启动过程中的所有流程和初始化动作。\n\n同时针对调度器的初始化进行了初步分析，详细介绍了 m0、g0 的用途和区别。\n在下一篇文章中我们将进一步对真正调度的 `schedule` 方法进行详解，这块也是个硬骨头了。"
  },
  {
    "path": "content/posts/go/go-concurrent-lock.md",
    "content": "---\ntitle: \"Go 并发：一些有趣的现象和要避开的 “坑”\"\ndate: 2020-12-10T00:25:59+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n最近在看 Go 并发相关的内容，发现还是有不少细节容易让人迷迷糊糊的，一个不小心就踏入深坑里，且指不定要在上线跑了一些数据后才能发现，那可真是太人崩溃了。\n\n今天来分享几个案例，希望大家在编码时能够避开这几个 “坑”。\n\n## 案例一\n\n### 演示代码\n\n第一个案例来自 @鸟窝 大佬在极客时间的分享，代码如下：\n\n```\nfunc main() {\n\tcount := 0\n\twg := sync.WaitGroup{}\n\twg.Add(10)\n\tfor i := 0; i < 10; i++ {\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tfor j := 0; j < 100000; j++ {\n\t\t\t\tcount++\n\t\t\t}\n\t\t}()\n\t}\n\twg.Wait()\n\n\tfmt.Println(count)\n}\n```\n\n思考一下，最后输出的 `count` 变量的值是多少？是不是一百万？\n\n### 输出结果\n\n在上述代码中，我们通过 `for-loop ` 循环起 `goroutine` 进行自增，并使用了 `sync.WaitGroup` 来保证所有的 goroutine 都执行完毕才输出最终的结果值。\n\n最终的输出结果如下：\n\n```\n// 第一次执行\n638853\n\n// 第二次执行\n654473\n\n// 第三次执行\n786193\n```\n\n输出的结果值不是恒定的，也就是每次输出的都不一样，且基本不会达到想象中的一百万。\n\n### 分析原因\n\n其原因在于 `count++` 并不是一个原子操作，在汇编上就包含了好几个动作，如下：\n\n```\nMOVQ \"\".count(SB), AX \nLEAQ 1(AX), CX \nMOVQ CX, \"\".count(SB)\n```\n\n因为可能会同时存在多个 goroutine 同时读取到 `count` 的值为 1212，并各自自增 1，再将其写回。\n\n与此同时也会有其他的 goroutine 可能也在其自增时读到了值，形成了互相覆盖的情况，这是一种并发访问共享数据的错误。\n\n### 发现问题\n\n这类竞争问题可以通过 Go 语言所提供的的 race 检测（[Go race detector](https://blog.golang.org/race-detector)）来进行分析和发现：\n\n```\n$ go run -race main.go \n==================\nWARNING: DATA RACE\nRead at 0x00c0000c6008 by goroutine 13:\n  main.main.func1()\n      /Users/eddycjy/go-application/awesomeProject/main.go:28 +0x78\n\nPrevious write at 0x00c0000c6008 by goroutine 7:\n  main.main.func1()\n      /Users/eddycjy/go-application/awesomeProject/main.go:28 +0x91\n\nGoroutine 13 (running) created at:\n  main.main()\n      /Users/eddycjy/go-application/awesomeProject/main.go:25 +0xe4\n\nGoroutine 7 (running) created at:\n  main.main()\n      /Users/eddycjy/go-application/awesomeProject/main.go:25 +0xe4\n==================\n...\n489194\nFound 3 data race(s)\nexit status 66\n```\n\n编译器会通过探测所有的内存访问，监听其内存地址的访问（读或写）。在应用运行时就能够发现对共享变量的访问和操作，进而发现问题并打印出相关的警告信息。\n\n需要注意的一点是，`go run -race` 是运行时检测，并不是编译时。且 race 存在明确的性能开销，通常是正常程序的十倍，因此不要想不开在生产环境打开这个配置，很容易翻车。\n\n## 案例二\n\n### 演示代码\n\n第二个案例来自煎鱼在脑子的分享，代码如下：\n\n```\nfunc main() {\n\twg := sync.WaitGroup{}\n\twg.Add(5)\n\tfor i := 0; i < 5; i++ {\n\t\tgo func(i int) {\n\t\t\tdefer wg.Done()\n\t\t\tfmt.Println(i)\n\t\t}(i)\n\t}\n\twg.Wait()\n}\n```\n\n思考一下，最后输出的结果是什么？值都是 4 吗？输出是稳定有序的吗？\n\n### 输出结果\n\n在上述代码中，我们通过 `for-loop` 循环起了多个 `goroutine`，并将变量 `i` 作为形参传递给了 `goroutine`，最后在 `goroutine` 内输出了变量 `i`。\n\n最终的输出结果如下：\n\n```\n// 第一次输出\n0\n1\n2\n4\n3\n\n// 第二次输出\n4\n0\n1\n2\n3\n```\n\n显然，从结果上来看，输出的值都是无序且不稳定的，值更不是 4。这到底是为什么？\n\n### 分析原因\n\n其原因在于，即使所有的 `goroutine` 都创建完了，但 `goroutine` 不一定已经开始运行了。\n\n也就是等到 `goroutine` 真正去执行输出时，变量 `i` （值拷贝）可能已经不是创建时的值了。\n\n其整个程序扭转实质上分为了多个阶段，也就是各自运行的时间线并不同，可以其拆分为：\n\n- 先创建：`for-loop` 循环创建 `goroutine`。\n\n- 再调度：协程`goroutine` 开始调度执行。\n\n- 才执行：开始执行 `goroutine` 内的输出。\n\n同时 `goroutine` 的调度存在一定的随机性（建议了解一下 GMP 模型），那么其输出的结果就势必是无序且不稳定的。\n\n### 发现问题\n\n这时候你可能会想，那前面提到的 `go run -race` 能不能发现这个问题呢。如下：\n\n```\n$ go run -race main.go\n0\n1\n2\n3\n4\n```\n\n没有出现警告，显然是不能的，因为其本质上并不是并发访问共享数据的错误，且会导致程序变成了串行，从而蒙蔽了你的双眼。\n\n## 案例三\n\n### 演示代码\n\n第三个案例来自煎鱼在梦里的分享，代码如下：\n\n```\nfunc main() {\n\twg := sync.WaitGroup{}\n\twg.Add(5)\n\tfor i := 0; i < 5; i++ {\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tfmt.Println(i)\n\t\t}()\n\t}\n\twg.Wait()\n}\n```\n\n思考一下，最后输出的结果是什么？值都是 4 吗？会像案例二一样乱窜吗？\n\n### 输出结果\n\n在上述代码中，与案例二大体没有区别，主要是变量 `i` 没有作为形参传入。\n\n最终的输出结果如下：\n\n```\n// 第一次输出\n5\n5\n5\n5\n5\n```\n\n初步从输出的结果上来看都是 5，这时候就会有人迷糊了，为什么不是 4 呢？\n\n不少人会因不是 4 而陷入了迷惑，但千万不要被一两次的输出迷惑了心智，认为铁定就是 5 了。可以再动手多输出几次，如下：\n\n```\n// 多输出几次\n5\n3\n5\n5\n5\n```\n\n最终会发现...输出结果存在随机性，输出结果并不是 100% 都是 5，更不用提 4 了。这到底是为什么呢？\n\n### 分析原因\n\n其原因与案例二其实非常接近，理论上理解了案例二也就能解决案例三。\n\n其本质还是创建 `goroutine` 与真正执行 `fmt.Println` 并不同步。因此很有可能在你执行 `fmt.Println` 时，循环 `for-loop` 已经运行完毕，因此变量 `i` 的值最终变成了 5。\n\n那么相反，其也有可能没运行完，存在随机性。写个 test case 就能发现明显的不同。\n\n## 总结\n\n在本文中，我分享了几个近期看到次数最频繁的一些并发上的小 “坑”，希望对你有所帮助。同时你也可以回想一下，在你编写 Go 并发程序有没有也遇到过什么问题？\n\n同时你也可以回想一下，在你编写 Go 并发程序有没有也遇到过什么问题？\n\n欢迎大家一起讨论交流。"
  },
  {
    "path": "content/posts/go/go-design.md",
    "content": "---\ntitle: \"上帝视角：Go 语言设计失误，缺乏远见？\"\ndate: 2021-12-31T12:55:13+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n前段时间我有一个朋友在某乎上摸鱼时，给我甩来一个主题为《golang 设计者是如何偿还技术债的》链接。\n\n说是让我学习、围观一下社区观点，早日好修成正果，本鱼表示满脸问号。\n\n原回答如下图：\n\n![](https://files.mdnice.com/user/3610/4bbdfe91-b98e-4ae1-bcc6-aa5fc50cd184.png)\n\n主要是以极短的话语表述 Go 语言的 “泛型、异常、channel、annotation、模块依赖” 的设计是失误的。\n\n说是没有向各种编程语言的 “最佳实践” 各取所需。\n\n## 那些故事\n\n刚好煎鱼也入门 Go 没几天，偶尔翻过 issues 和 proposal，看了一点点历史事件。\n\n![图来自 Introduction to Golang](https://files.mdnice.com/user/3610/3213b564-1a53-4dcf-9b69-4f4359bd50db.png)\n\n也从我的观点来围观一下 Go 官方这些年为特性挣扎过的那些事。\n\n涉及：\n1. 泛型。\n2. 错误处理。\n3. 依赖管理。\n4. 注解。\n\n### 泛型\n\n**为什么 Go 语言这么久都没有泛型**，是不是 Go 官方不够 “聪明”，抄作业都不会抄。这显然是不对的。\n\n有如下几点原因：\n1. 泛型本质上并不是绝对的必需品。\n2. 泛型不是 Go 语言的早期目标。\n3. 其他 feature 更重要，把精力放在这些上面，Go 团队人力很有限的。\n\n#### 历史尝试\n\n在以往的尝试中，Go 团队有人进行过不少的泛型 proposal 试验。基本时间线（via @changkun）如下：\n\n|  简述   | 时间  | 作者 |\n|  ----  | ----  | --- |\n| Type Functions  | 2010年 | Ian Lance Taylor |\n| Generalized Types  | 2011年 | Ian Lance Taylor |\n| Generalized Types v2  | 2013年 | Ian Lance Taylor |\n| Type Parameters  | 2013年 | Ian Lance Taylor |\n| go:generate  | 2014年 | Rob Pike |\n| First Class Types  | 2015年 | Bryan C.Mills |\n| Contracts  | 2018年 | Ian Lance Taylor, Robert Griesemer |\n| Contracts  | 2019年 | Ian Lance Taylor, Robert Griesemer  |\n| Redundancy in Contracts(2019)'s Design  | 2019年 | Ian Lance Taylor, Robert Griesemer |\n| Constrained Type Parameters(2020, v1)  | 2020年 | Ian Lance Taylor, Robert Griesemer |\n| Constrained Type Parameters(2020, v2)  | 2020年 | Ian Lance Taylor, Robert Griesemer |\n| Constrained Type Parameters(2020, v3)  | 2020年 | Ian Lance Taylor, Robert Griesemer |\n\n我们观察一下，10 年过去了，Ian Lance Taylor 依然在开展泛型提案，持续地在思考着 Go 泛型。\n\n坚持思考，这一点值得我们学习。\n\n对 Go 泛型历史有兴趣的读者可以看看《[为什么 Go 的泛型一拖再拖？](https://mp.weixin.qq.com/s/ftmuA9g7QPAwSwiswRuSuw)》，给了明确完整的内容介绍和过程描述了。\n\n### 下一步计划\n\n在 2021 年尾巴的我们，明年（2022年） **Go1.18 左右就可以见到 Go 泛型，基本跑不了。**\n\n想想就激动，如下图（此刻是 4 个月后）：\n\n![图来自网上](https://files.mdnice.com/user/3610/934613fa-2564-4f26-bd40-bb309bd48dcf.png)\n\n在出来前可以看看《[Go 1.17 支持泛型了？具体怎么用](https://mp.weixin.qq.com/s/Pf7YuFpwbldSB60DDCBtlA)》，可以作为玩具用了。\n\n接下来可以预见泛型出来后，一堆工具库和数据结构很大可能会被逐步改写，像是《[Go 提案：增加泛型版 slices 和 maps 新包](https://mp.weixin.qq.com/s/D7u7nxixctoFIL-Pch0zvw)》，早已摩拳擦掌。\n\n届时 Go 源码类别的书的部分内容也会失时效，需要关注 Go 版本的时效性。\n\n### 错误处理\n\n在日常工程中，我们写的、看到最多的可能就是这一段标志性 Go 代码：\n\n```golang\nfunc main() {\n x, err := foo()\n if err != nil {\n   // handle error\n }\n y, err := foo()\n if err != nil {\n   // handle error\n }\n z, err := foo()\n if err != nil {\n   // handle error\n }\n s, err := foo()\n if err != nil {\n   // handle error\n }\n}\n```\n\n这是在业内被吐槽的最多的，甚至都可以用来作为 Gopher 的互认。\n\n#### 设计方向\n\n那 Go 是瞎设计的吗，就粗制滥造，搞个错误 err 的返回约定惯例。像是：\n\n```golang\nfunc foo() err {\n    return nil\n}\n```\n\n其实并不是，Go 团队在设计上有意识地选择了**显式**的设计方向，如下：\n- 使用显式错误结果。\n- 使用显式错误检查。\n\n这和其他语言不一样 ，是由于 Go 团队也认识到了异常处理的不可见错误检查所带来的问题。\n\n设计草案有一部分是受到了这些问题的启发。如下：\n\n![](https://files.mdnice.com/user/3610/9c308232-631e-4aa3-b265-85daf2b9909d.png)\n\n目前 Go 官方也没有打算去掉 “显式” 这一做法，新版 Go2 错误处理的核心目标是：“**错误检查更加轻便，减少专门用于错误检查的 Go 程序代码的数量和所花费的时间**。”。\n\n从 Go2 的趋势来看，主要是增加关键字和修饰来解决这个问题，相当于是堆积木了，而不是直接把他干掉的。\n\n这在 Go 核心团队内是非常明确的。\n\n#### 进一步深入\n\n对 Go 语言错误处理还想进一步深入的，推荐看下面这几篇文章：\n\n- 《[先睹为快，Go2 Error 的挣扎之路](https://mp.weixin.qq.com/s/XILveKzh07BOQnqxYDKQsA)》\n- 《[Go errors 不会有进一步的改进计划](https://mp.weixin.qq.com/s/ixBMcAgqW51I0r_hkw5l5A)》\n- 《[你对 Go 错误处理的 4 个误解！](https://mp.weixin.qq.com/s/Ey-yqIq__wpaLTlBAOHjxg)》\n\n### 依赖管理\n\nGo 语言在一开始是完全基于 GOPATH 作为依赖管理的模式，当时也闹了不少的争议出来。有以下核心问题：\n1. 依赖要手动拉取和下载，没有强版本化的概念，开发者很难受（例如：不兼容升级、要拉取同一份）。\n2. 依赖和工程代码必须在 GOPATH 下才能运行，不能任意摆放。\n\n所以在 Go1.0~Go1.11 中，各路神仙发招，社区出现了各种诸如 dep、glide、godep 等依赖包管理工具。\n\n#### 时间线\n\n后续 Go 团队在 Russ Cox 的强势推进下，力排众议，推动 Go modules 的发展：\n\n![](https://files.mdnice.com/user/3610/66291d6e-b10b-42db-9242-d5e16eb94237.png)\n\n时间线如下：\n- Go1.11 起开始推进 Go modules（前身 vgo）。\n- Go1.13 起不再推荐使用 GOPATH 的使用模式。\n- Go1.14 表示已经准备好，可以用在生产上（ready for production）了。\n\n#### 为什么这么晚\n\n为什么 Go modules 这么晚才诞生，这是不是就是 Go 团队的设计失误呢？\n\n我认为，是也不是。\n\nGo 的诞生一开始是为了解决 Google 几位大佬自己的痛点。\n\n在 Google 的依赖管理上，本身是大仓库（Monorepo）的模式，企业内部有自己一整套工具和流程，设计之初没有这块的强诉求。\n\n如下：\n\n![图来自 Mono Repo vs Multi Repo](https://files.mdnice.com/user/3610/c589a0af-e0c1-4e9b-83c2-899369c9fa8e.jpg)\n\n有兴趣的读者详细可阅读《[Why Google Stores Billions of Lines of Code in a Single Repository](https://dl.acm.org/doi/epdf/10.1145/2854146)》，\n\nGo 在社区开源后，大规模使用后这个问题就爆发了，社区自行释出了方案。可惜，五花八门，也都没有解决好。官方队伍就自己上手了。\n\n要知道，没有技术方案是完美的。Go modules 也被不少人所吐槽，存在争议。\n\n#### 进一步深入\n\n想更进一步深入 Go modules 的小伙伴，可以看看下述文章：\n- 《[Go Modules 终极入门和历史](https://mp.weixin.qq.com/s/6gJkSyGAFR0v6kow2uVklA)》\n- 《[干货满满的 Go Modules 和 goproxy.cn](https://mp.weixin.qq.com/s/jpp7vs3Fdg4m15P1SHt1yA)》\n\n### 注解\n\nGo 开发者中有大部分同学都有其他语言的使用经验。在其他语言中，注解是一个强大的工具，没得用会很不习惯。\n\n![图片来自网络](https://files.mdnice.com/user/3610/9061ff09-9776-4a74-9550-fc5a4f17f824.png)\n\n甚至有听过没有注解，就自嘲不会 “写” 代码了，所以一上来就找 Go 语言的注解怎么用了。\n\n#### 一些疑惑\n\n我有一个朋友，经常会听到如下疑惑，甚至无奈的发问：\n\n- “怎么样在函数前声明，直接开启事务？”\n- \"为什么 Java 可以完美注解，Go 就不行，难以理解，我无法接受...\"\n- “那 Go 支持什么程度的注解？”\n\nGo 的 “注解” 支撑的非常有限，基本都是 `//go build`、`go:generate` 这类辅助，达不到标准的装饰器的作用。\n\n#### 为什么不支持\n\n没有全面的支持注解来做装饰器，显然不算 Go 的设计失误，这是刻意为之，这是与错误处理的设计理念相关联。\n\nGo issues 上有人提过类似的提案：\n\n![](https://files.mdnice.com/user/3610/9695f163-65e8-456a-8dce-bb8551739016.png)\n\nGo Contributor @ianlancetaylor 给出了明确的答复，Go在设计上更倾向于明确的、显式的编程风格。\n\n优缺点如下：\n- 优势：不知道 Go 能从添加装饰器中得到什么好处，没能在 issues 上明确论证。\n- 缺点：是明确的，会存在意外设置的情况。\n\n因如下原因，没有接受注解：\n- 对比现有代码方法，这种装饰器的新的方法没有提供比现有方法更多的优势，大到足矣推翻原有的设计思路。\n- 社区内的投票，支持的也很少（基于表情符号的投票），用户反馈不多。\n\n可能有小伙伴会说了，有注解做装饰器了，代码会简洁不少。\n\n但其实 Go 团队的态度很明确：\n\n![](https://files.mdnice.com/user/3610/bb357e12-9b15-4729-9381-977d164b6b04.png)\n\nGo 认为**可读性更重要**，如果只是额外多写一点代码，在权衡后，还是可以接受的。\n\n如果想自己在 Go 中实现完整注解的，可以详细阅读《[Go：我有注解，Java：不，你没有！](https://mp.weixin.qq.com/s/hrsagmDtjt6r9fJKf8SUcQ)》，可以给到你一些思路。\n\n## 偿还的过程\n\n如果是在职场中工作多年的小伙伴，其实不难发现 Go 的发展史和业务的发展节奏是类似的。\n\n在社区中吐槽的主要是两块，如下：\n- 为什么这个功能不如此设计？\n- 这个功能为什么没有支持？\n\n### 不如此设计\n\n为什么 Go 语言不如此设计？经典的像是 Go 的错误处理（error），很多小伙伴会**先入为主**，以其他语言的最佳实践，要教 Go 团队设计，要 throw，要 catch！\n\n其实想一下，我们做一个业务，这个业务就是 Go 语言。我们需要先做业务建模，确定 Go 的核心思想，才能持续的迭代和设计。\n\nGo 语言的设计定义很明显是：**既要简单、还要显式，不能有隐式、要避免复杂**，所以社区传递的是 “**less is more**” 的设计理念。\n\n这么想，很多提案的落地，被拒等，都能了解到 Go 语言的设计哲学和团队理念。\n\n### 还没有支持\n\n为什么 Go 语言的 XXX 功能没有支持？经典的像是 Go 的泛型、注解等功能。\n\n还没有支持的可能性有三点，如下：\n1. 还没有想清楚。\n2. 早就被拒绝了。\n3. 优先级不够高。\n\n实际上和我们业务迭代一样，Go 团队的人力资源有限，**做事会有优先级**。前文所提到的 Russ Cox 就是现在 Go 团队 Leader，每年也会开相关的会议讨论事项。\n\n像是 Go 泛型，显然没有，也不会影响到 Go 在业务初期的短期发展，国内依然存有一定的占用率。2011 年没有想清楚，也就一直持续思考和尝试了...\n\n而注解，或是你们想到的。很多在 go issues 其实早就被拒绝过多次，自然还没有支持，也是因为他不大可能直接出现了。\n\n### 推进的模式\n\nGo 在推进或偿还新技术改进时，现在采取的模式都是一样的。会先设计一个编译时可以指定的 “变量”。\n\n例如：\n- 泛型的 G 变量。\n- Modules 的 GO111MODULE 变量。\n\n再在 Go 的不断迭代中，推进使用和反馈，再推进变量的默认开启，逐渐去除。\n\n可以参考 GO111MODULE 的过程。\n\n## 总结\n\n我们在学习很多语言、技能时，会以既有的知识去认知，再对新的对象建立新的认知树，很容易会有**先入为主的认知行为**。\n\n但若没有及时思考，就很容易产生偏见。**认为 XXX 是 XXX，你 Go 语言就应该是 XXX，这样是有失偏颇的**。\n\n就像我们行业经常讨论的，网上的 A 同学，35 岁被裁员了。那你我，35 岁就 100% 会下岗吗？\n\n相反，Go 语言这 10+ 年来，基于自己的设计理念。保持了大致一贯的 less is more 设计理念，是值得赞许的。\n\n我们要知道软件设计，是**没有银弹**的。Go 语言的设计理念，有好有坏，**社区也有不少人对大道至简的理念嗤之以鼻**。\n\n**你又是怎么看待 Go 语言的呢**，欢迎点赞、留言，一起来交流和讨论：）"
  },
  {
    "path": "content/posts/go/go-empty-struct.md",
    "content": "---\ntitle: \"用 Go struct 不能犯的一个低级错误！\"\ndate: 2021-06-17T12:44:27+08:00\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n前段时间我分享了 《手撕 Go 面试官：Go 结构体是否可以比较，为什么？》的文章，把基本 Go struct 的比较依据研究了一番。这不，最近有一位读者，遇到了一个关于 struct 的新问题，不得解。\n\n大家一起来看看，建议大家在看到代码例子后先思考一下答案，再往下看。\n\n独立思考很重要。\n\n## 疑惑的例子\n\n其给出的例子一如下：\n\n```golang\ntype People struct {}\n\nfunc main() {\n a := &People{}\n b := &People{}\n fmt.Println(a == b)\n}\n```\n\n你认为输出结果是什么呢？\n\n输出结果是：false。\n\n再稍加改造一下，例子二如下：\n\n```golang\ntype People struct {}\n\nfunc main() {\n a := &People{}\n b := &People{}\n fmt.Printf(\"%p\\n\", a)\n fmt.Printf(\"%p\\n\", b)\n fmt.Println(a == b)\n}\n```\n输出结果是：true。\n\n他的问题是 \"**为什么第一个返回 false 第二个返回 true，是什么原因导致的**？\n\n煎鱼进一步的精简这个例子，得到最小示例：\n\n```golang\nfunc main() {\n\ta := new(struct{})\n\tb := new(struct{})\n\tprintln(a, b, a == b)\n\n\tc := new(struct{})\n\td := new(struct{})\n\tfmt.Println(c, d)\n\tprintln(c, d, c == d)\n}\n```\n\n输出结果：\n\n```bash\n// a, b; a == b\n0xc00005cf57 0xc00005cf57 false\n\n// c, d\n&{} &{}\n// c, d, c == d\n0x118c370 0x118c370 true\n```\n\n第一段代码的结果是 false，第二段的结果是 true，且可以看到内存地址指向的完全一样，也就是排除了输出后变量内存指向改变导致的原因。\n\n进一步来看，似乎是 `fmt.Print` 方法导致的，但一个标准库里的输出方法，会导致这种奇怪的问题？\n\n## 问题剖析\n\n如果之前有被这个 “坑” 过，或有看过源码的同学。可能能够快速的意识到，导致这个输出是**逃逸分析**所致的结果。\n\n我们对例子进行逃逸分析：\n\n```\n// 源代码结构\n$ cat -n main.go\n     5\tfunc main() {\n     6\t\ta := new(struct{})\n     7\t\tb := new(struct{})\n     8\t\tprintln(a, b, a == b)\n     9\t\n    10\t\tc := new(struct{})\n    11\t\td := new(struct{})\n    12\t\tfmt.Println(c, d)\n    13\t\tprintln(c, d, c == d)\n    14\t}\n\n// 进行逃逸分析\n$ go run -gcflags=\"-m -l\" main.go\n# command-line-arguments\n./main.go:6:10: a does not escape\n./main.go:7:10: b does not escape\n./main.go:10:10: c escapes to heap\n./main.go:11:10: d escapes to heap\n./main.go:12:13: ... argument does not escape\n```\n\n通过分析可得知变量 a, b 均是分配在栈中，而变量 c, d 分配在堆中。\n\n其关键原因是因为调用了 `fmt.Println` 方法，该方法内部是涉及到大量的反射相关方法的调用，会造成逃逸行为，也就是分配到堆上。\n\n### 为什么逃逸后相等\n\n关注第一个细节，就是 “为什么逃逸后，两个空 struct 会是相等的？”。\n\n这里主要与 Go runtime 的一个优化细节有关，如下：\n\n```golang\n// runtime/malloc.go\nvar zerobase uintptr\n```\n\n变量 `zerobase` 是所有 0 字节分配的基础地址。更进一步来讲，就是空（0字节）的在进行了逃逸分析后，往堆分配的都会指向 `zerobase` 这一个地址。\n\n所以空 struct 在逃逸后本质上指向了 `zerobase`，其两者比较就是相等的，返回了 true。\n\n### 为什么没逃逸不相等\n\n关注第二个细节，就是 “为什么没逃逸前，两个空 struct 比较不相等？”。\n\n![Go spec](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dc05fb027a9a481f83053f08b1ee2868~tplv-k3u1fbpfcp-zoom-1.image)\n\n从 Go spec 来看，这是 Go 团队刻意而为之的设计，不希望大家依赖这一个来做判断依据。如下：\n\n>> This is an intentional language choice to give implementations flexibility in how they handle pointers to zero-sized objects. If every pointer to a zero-sized object were required to be different, then each allocation of a zero-sized object would have to allocate at least one byte. If every pointer to a zero-sized object were required to be the same, it would be different to handle taking the address of a zero-sized field within a larger struct.\n\n还说了一句很经典的，细品：\n\n>> Pointers to distinct zero-size variables may or may not be equal.\n\n另外空 struct 在实际使用中的场景是比较少的，常见的是：\n- 设置 context，传递时作为 key 时用到。\n- 设置空 struct 业务场景中临时用到。\n\n但业务场景的情况下，也大多数会随着业务发展而不断改变，假设有个远古时代的 Go 代码，依赖了空 struct 的直接判断，岂不是事故上身？\n\n#### 不可直接依赖 \n\n因此 Go 团队这番操作，与 Go map 的随机性如出一辙，避免大家对这类逻辑的直接依赖，是值得思考的。\n\n而在没逃逸的场景下，两个空 struct 的比较动作，你以为是真的在比较。实际上已经在代码优化阶段被直接优化掉，转为了 false。\n\n因此，虽然在代码上看上去是 == 在做比较，实际上结果是 a == b 时就直接转为了 false，比都不需要比了。\n\n你说妙不？\n\n#### 没逃逸让他相等\n\n既然我们知道了他是在代码优化阶段被优化的，那么相对的，知道了原理的我们也可以借助在 go 编译运行时的 gcflags 指令，让他不优化。\n\n在运行前面的例子时，执行 `-gcflags=\"-N -l\"` 指令：\n\n```\n$ go run -gcflags=\"-N -l\" main.go \n0xc000092f06 0xc000092f06 true\n&{} &{}\n0x118c370 0x118c370 true\n```\n\n你看，两个比较的结果都是 true 了。\n\n## 总结\n\n在今天这篇文章中，我们针对 Go 语言中的空结构体（struct）的比较场景进行了进一步的补全。经过这两篇文章的洗礼，你会更好的理解 Go 结构体为什么叫既可比较又不可比较了。\n\n而空结构比较的奇妙，主要原因如下：\n- 若逃逸到堆上，空结构体则默认分配的是 `runtime.zerobase` 变量，是专门用于分配到堆上的 0 字节基础地址。因此两个空结构体，都是 `runtime.zerobase`，一比较当然就是 true 了。\n- 若没有发生逃逸，也就分配到栈上。在 Go 编译器的代码优化阶段，会对其进行优化，直接返回 false。并不是传统意义上的，真的去比较了。\n\n不会有人拿来出面试题，不会吧，为什么 Go 结构体说可比较又不可比较？\n\n若有任何疑问欢迎评论区反馈和交流，**最好的关系是互相成就**，各位的**点赞**就是[煎鱼](https://github.com/eddycjy)创作的最大动力，感谢支持。\n\n> 文章持续更新，可以微信搜【脑子进煎鱼了】阅读，回复【**000**】有我准备的一线大厂面试算法题解和资料；本文 **GitHub** [github.com/eddycjy/blog](https://github.com/eddycjy/blog) 已收录，欢迎 Star 催更。\n\n## 参考\n\n- 欧神的微信交流\n- 曹大的一个空 struct 的“坑”\n\n"
  },
  {
    "path": "content/posts/go/go-error2panic.md",
    "content": "---\ntitle: \"Go 错误处理：用 panic 取代 err != nil 的模式\"\ndate: 2020-12-12T17:21:42+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n前段时间我分享了文章 《先睹为快，Go2 Error 的挣扎之路》后，和一位朋友进行了一次深度交流，他给我分享了他们项目组对于 Go 错误处理的方式调整。\n\n简单来讲，就是在业务代码中使用 `panic` 的方式来替代 “永无止境” 的 `if err != nil`。这就是今天本文的重点内容，我们一起来看看是怎么做，又有什么优缺点。\n\n## 为什么想替换\n\n在 Go 语言中 `if err != nil` 写的太多，还要管方法声明各种，嫌麻烦又不方便：\n\n```\nerr := foo()\nif err != nil {\n     //do something..\n     return err\n}\n\nerr := foo()\nif err != nil {\n     //do something..\n     return err\n}\n\nerr := foo()\nif err != nil {\n     //do something..\n     return err\n}\n\nerr := foo()\nif err != nil {\n     //do something..\n     return err\n}\n```\n\n上述还是示例代码，比较直面。若是在工程实践，还得各种 package 跳来跳去加 `if err != nil`，总的来讲比较繁琐，要去关心整体的上下游。更具体的就不赘述了，可以翻看我先前的文章。\n\n## 怎么替换 err != nil\n\n不想写 `if err != nil` 的代码，方式之一就是用 `panic` 来替代他。示例代码如下：\n\n```\nfunc GetFish(db *sql.DB, name string) []string {\n\trows, err := db.Query(\"select name from users where `name` = ?\", name)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer rows.Close()\n\n\tvar names []string\n\tfor rows.Next() {\n\t\tvar name string\n\t\terr := rows.Scan(&name)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tnames = append(names, name)\n\t}\n\n\terr = rows.Err()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn names\n}\n``` \n\n在上述业务代码中，我们通过 `panic` 的方式取代了 `return err` 的函数返回，自然其所关联的下游业务代码也就不需要编写 `if err != nil` 的代码：\n\n```\nfunc main() {\n\tfish1 := GetFish(db, \"煎鱼\")\n\tfish2 := GetFish(db, \"咸鱼\")\n\tfish3 := GetFish(db, \"摸鱼\")\n\t...\n}\n```\n\n同时在转换为使用 `panic` 模式的错误机制后，我们必须要在外层增加 `recover` 方法：\n\n```\nfunc AppRecovery() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tdefer func() {\n\t\t\tif err := recover(); err != nil {\n\t\t\t\tif _, ok := err.(AppErr); ok {\n\t\t\t\t\t// do something...\n\t\t\t\t} else {\n\t\t\t\t\tpanic(err)\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n}\n```\n\n每次 `panic` 后根据其抛出的错误进行断言，识别是否定制的 `AppErr` 错误类型，若是则可以进行一系列的处理动作。否则可继续向上 `panic` 抛出给顶级的 `Recovery` 方法进行处理。\n\n这就是一个相对完整的 `panic` 错误链路处理了。\n\n## 优缺点\n\n- 从优点上来讲：\n\n    - 整体代码结构看起来更加的简洁，仅专注于实现逻辑即可。\n\n    - 不需要关注和编写冗杂的 `if err != nil` 的错误处理代码。\n\n- 从缺点上来讲：\n\n    - 认知负担的增加，需要参加项目的每一个新老同学都清楚该模式，要做一个基本规范或培训。\n\n    - 存在一定的性能开销，每次 `panic` 都存在用户态的上下文切换。\n\n    - 存在一定的风险性，一旦 `panic` 没有 `recover` 住，就会导致事故。\n\n    - Go 官方并不推荐，与 `panic` 本身的定义相违背，也就是 `panic` 与 `error` 的概念混淆。\n\n## 总结\n\n在今天这篇文章给大家分享了如何使用 `panic` 的方式来处理 Go 的错误，其必然有利必有有弊，需要做一个权衡了。\n\n你们团队有没有为了 Go 错误处理做过什么新的调整呢？欢迎在留言区交流和分享。"
  },
  {
    "path": "content/posts/go/go-errors-boom.md",
    "content": "---\ntitle: \"生产环境遇到一个 Go 问题，整组人都懵逼了...\"\ndate: 2021-07-07T12:43:49+08:00\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n前段时间正在疯狂写代码的时候，突然有一个读者给我提了一个问题，让我有了一定的兴趣：\n\n![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1011e66fe104d228dcaa2083b554fc8~tplv-k3u1fbpfcp-watermark.image)\n\n我还是比较感兴趣的，因为是生产环境、有代码，且整组人都懵逼的问题。\n\n在征求了小伙伴的意见后，今天分享出来，大家也思考一下原因，一起规避这个 “坑”。\n\n## 案例一\n\n代码示例如下：\n\n```golang\ntype MyErr struct {\n    Msg string\n}\n\nfunc main() {\n    var e error\n    e = GetErr()\n    log.Println(e == nil)\n}\n\nfunc GetErr() *MyErr {\n    return nil\n}\n\nfunc (m *MyErr) Error() string {\n    return \"脑子进煎鱼了\"\n}\n```\n\n请思考一下，**这段程序的输出结果是什么**？\n\n该程序所调用的 `GetErr` 方法所返回的是 `nil`，而外部判断是 `e == nil`，因此最终的输出结果是 true，对吗？ \n\n输出结果如下：\n\n```\n2021/04/04 08:39:04 false\n```\n\n答案是：false。\n\n## 案例二\n\n代码示例如下：\n\n```golang\ntype Base interface {\n    do()\n}\n\ntype App struct {\n}\n\nfunc main() {\n    var base Base\n    base = GetApp()\n    \n    log.Println(base)\n    log.Println(base == nil)\n}\n\nfunc GetApp() *App {\n    return nil\n}\nfunc (a *App) do() {}\n```\n\n请思考一下，**这段程序的输出结果是什么**？\n\n该程序调用了 `GetApp` 方法，该方法返回的是 `nil`，因此其赋值的 `base` 也是 `nil`。因此判断 `base == nil` 的最终输出结果是 `<nil>` 和 `true`，对吗？\n\n输出结果如下：\n\n```\n2021/04/04 08:59:00 <nil>\n2021/04/04 08:59:00 false\n```\n\n答案是：`<nil>` 和 false。\n\n## 为什么\n\n为什么，这两段 Go 程序是怎么回事...也太反直觉了？其背后的原因本质上还是对 Go 语言中 interface 的基本原理的理解。\n\n在案例一中，虽然 `GetErr` 方法确实是返回了 `nil`，返回的类型也是具体的 `*MyErr` 类型。但是其接收的变量却不是具体的结构类型，而是 `error` 类型：\n\n```golang\nvar e error\ne = GetErr()\n```\n\n在 Go 语言中， `error` 类型本质上是 interface：\n\n```golang\ntype error interface {\n    Error() string\n}\n```\n\n因此兜兜转转又回到了 interface 类型的问题，interface 不是单纯的值，而是**分为类型和值**。\n\n所以传统认知的此 nil 并非彼 nil，**必须得类型和值同时都为 nil 的情况下，interface 的 nil 判断才会为 true**。\n\n在案例一中，结合代码逻辑，更符合场景的是：\n\n```golang\nvar e *MyErr\ne = GetErr()\nlog.Println(e == nil)\n```\n\n输出结果就会是 true。\n\n在案例二中，也是一样的结果，原因也是 interface。不管是 `error` 接口（interface），还是自定义的接口，背后原理一致，自然也就结果一致了。\n\n## 总结\n\n今天这篇文章，相当于是《Go 面试题：Go interface 的一个 “坑” 及原理分析》的变形了，毕竟是生产环境的代码改造而来，更贴合真实的实际场景。\n\n下意识的直觉有时候不是绝对正确的，我们要正确的理解 Go 语言中的那些知识点，才能更好地实现早下班的理想和愿景。\n\n"
  },
  {
    "path": "content/posts/go/go-golang.md",
    "content": "---\ntitle: \"Go 和 Golang 有什么关系？\"\ndate: 2021-12-31T12:55:12+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n最近天气也冷了，掐指一算是招聘季了，无论是校招、社招、HR、面试官们都蠢蠢欲动。这不，我有一个朋友的 HR 朋友都有起名困难了，一看 Go 语言的工作说明（Job Description），发现各有不同。\n\n如下图：\n\n![来自某招聘网站](https://files.mdnice.com/user/3610/629d4aaa-bb01-4e2b-9a43-d671200da099.png)\n\n仔细一看，有叫 Go 的，也有叫 Golang，还有叫 GO 的。好家伙，Go 语言有这么多个别名，甚至某乎都讨论了起来。到底叫什么是正确的？\n\n为此，今天就由煎鱼带大家理一理，了解这背后的关系。\n\n## Go 官方定义\n\n从网上的资料来看，大家对 Go 的名字还是比较关注的，对于 Go 团队来讲，仿佛经常被问。例如：\n\n- “Go 和 Golang 的关系是什么？”\n- “Go、Golang、GO 哪个对？”\n\n甚至在之前探讨 Go2 草案时，也有人开始起 Go2 的名字了，纠结是要叫 “golang2”，还是 “go2lang”：\n\n![](https://files.mdnice.com/user/3610/7608f5a1-9e77-45b2-a552-a7b7c32ad100.png)\n\n其实这是错误的。在 Go FAQ 中有明确的回答这个问题：\n\n![](https://files.mdnice.com/user/3610/bdb43d48-f5c1-41bb-8495-7bbfc2fc60a6.png)\n\n这一门语言称为 “Go”，不叫 “Golang”，也不叫 “GO”。“golang” 只是网站的地址，而不是语言的名称。\n\n同时 “GO” 的语言名称叫法也是错误的，虽然官方上的 Logo 是 “GO”：\n\n![](https://files.mdnice.com/user/3610/67b022a9-49de-4349-861e-77dd25367ac6.png)\n\n但这显然只是设计师层面的美观考量，并不是这一门语言的标准定义。\n\n因此**这一门语言叫做 “Go” 语言**，这是正确的，也得到官方认证的，也不曾改变过。\n\n## 为什么会有 Golang\n\n但可能又有小伙伴疑惑了，那为什么 “Golang” 这个别名，如此之火。到底是为什么？\n\n这里一共有三点原因，分别是：站点地址（Go FAQ 提到）、搜索引擎、社区和论坛、语言重名。\n\n### Go 站点地址\n\nGo 团队所期望的 https://go.org 早就被注册，从网站的底部标识来看，2008 年起建站：\n\n![](https://files.mdnice.com/user/3610/d40e96a2-122c-44fa-8134-2a8b88eb7690.png)\n\n所以 Go 语言只能使用 https://golang.org，你也会 https://pkg.go.dev 和 https://golang.org、https://godoc.org，存在多个域名，并不统一。\n\n因此作为 Go 开发者所常用官方站点，自然而然 golang 这一个语言标识就深深地被记住了，一直沿用至今。\n\n同时域名为 “golang” 关键字，自然会大幅度的影响到 Go 资料搜索引擎的收录，是一个非常重要的因素。\n\n### 搜索引擎\n\n在早年 Go 语言还不知名时，用 go 关键字去搜索资料会非常的困难。这是各大搜索引擎早年的一个槽点（reddit 很多吐槽）。\n\n因为单一的 go 关键字过于广泛了，很多人会直接用 golang 关键字来搜资料，反而会更能看到一些与 Go 真正相关的。\n\n![](https://files.mdnice.com/user/3610/fe21dc19-26d5-4e14-88c5-fa077f149e00.png)\n\n这一点在近年来有明确改善，得益于 Go 语言的崛起，现在也能搜到了。\n\n### 社区和论坛\n\n在社区、论坛等，也有类似的问题。因为占位、重名、认知等原因。像是 segmentfault、twitter 叫 golang。掘金叫 Go，各有不同。\n\n![](https://files.mdnice.com/user/3610/07ee2a59-901f-44ff-bd65-7de95a9878c6.png)\n\n这点难以改善，毕竟各家都是不同企业的。所以难受的点是用户，搜了 Go，可能搜不到，又跑去搜 Golang 才可以。\n\n再看看国外的论坛，在 Google 群组 golang-nuts 和 golang-dev 也有类似偏差。\n\n基本可以明确 **“Golang” 更多会被用在搜索和标签上**，能够保证搜索和标签查询的结果。\n\n### 语言重名 \n\n实际上在 Go 语言出现前，已经存在一门 “Go!” 的编程语言了。有网友表示这也是 Go 官方纠结的一点。\n\n![](https://files.mdnice.com/user/3610/8c2197f0-5376-49c1-845a-b2564606d1a7.png)\n\n不过实际上编程语言重名并不少见，但由于真实性有待考量，建议仅是了解即可。\n\n至少现在已经没有这门语言的命名之争。\n\n## 总结\n\n可以明确，官方诠释的正确名称为 Go。\n\n但由于 go.org 域名的原因，\n因此在 Go Programming Language 的通俗称呼下，采取了 golang 来作为 Go 站点、Google 群组的域名/组别等的建立。\n\nGo 资料肯定都集中在官方站点、论坛，自然而然，大家用 “go” 关键字也就很难搜索到了，都得用 “golang” 关键字。\n\n可以明确，**Go 是这一门编程语言的名字，Golang 更多是在搜索和标签上的使用**。\n\n这看上去，是搜索引擎的胜利，你觉得呢？ ：）"
  },
  {
    "path": "content/posts/go/go-map-access.md",
    "content": "---\ntitle: \"用 Go map 要注意这个细节，避免依赖他！\"\ndate: 2021-09-12T17:47:29+08:00\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n最近又有同学问我这个日经话题，想转他文章时，结果发现我的公众号竟然没有发过，因此今天我再唠叨两句，好让大家避开这个 “坑”。\n\n有的小伙伴没留意过 Go map 输出、遍历顺序，**以为它是稳定的有序的，会在业务程序中直接依赖这个结果集顺序，结果栽了个大跟头**，吃了线上 BUG。\n\n有的小伙伴知道是无序的，但却不知道为什么,有的却理解错误？\n\n![奇怪的输出结果](http://wx2.sinaimg.cn/large/006fVPCvly1g1s1ah84k8j30k70dvaac.jpg)\n\n今天通过本文，我们将揭开 `for range map` 输出的 “神秘” 面纱，看看它内部实现到底是怎么样的，顺序到底是怎么样？\n\n开始吸鱼之路。\n\n## 前言\n\n例子如下：\n\n```go\nfunc main() {\n\tm := make(map[int32]string)\n\tm[0] = \"EDDYCJY1\"\n\tm[1] = \"EDDYCJY2\"\n\tm[2] = \"EDDYCJY3\"\n\tm[3] = \"EDDYCJY4\"\n\tm[4] = \"EDDYCJY5\"\n\n\tfor k, v := range m {\n\t\tlog.Printf(\"k: %v, v: %v\", k, v)\n\t}\n}\n```\n\n假设运行这段代码，输出的结果是怎么样？是有序，还是无序输出呢？\n\n```\nk: 3, v: EDDYCJY4\nk: 4, v: EDDYCJY5\nk: 0, v: EDDYCJY1\nk: 1, v: EDDYCJY2\nk: 2, v: EDDYCJY3\n```\n\n从输出结果上来讲，是非固定顺序输出的，也就是每次都不一样。但这是为什么呢？\n\n首先**建议你先自己想想原因**。其次我在面试时听过一些说法。有人说因为是哈希的所以就是无（乱）序等等说法。当时我是有点 ？？？\n\n这也是这篇文章出现的原因，希望大家可以一起研讨一下，理清这个问题 ：）\n\n## 看一下汇编\n\n```\n    ...\n\t0x009b 00155 (main.go:11)\tLEAQ\ttype.map[int32]string(SB), AX\n\t0x00a2 00162 (main.go:11)\tPCDATA\t$2, $0\n\t0x00a2 00162 (main.go:11)\tMOVQ\tAX, (SP)\n\t0x00a6 00166 (main.go:11)\tPCDATA\t$2, $2\n\t0x00a6 00166 (main.go:11)\tLEAQ\t\"\"..autotmp_3+24(SP), AX\n\t0x00ab 00171 (main.go:11)\tPCDATA\t$2, $0\n\t0x00ab 00171 (main.go:11)\tMOVQ\tAX, 8(SP)\n\t0x00b0 00176 (main.go:11)\tPCDATA\t$2, $2\n\t0x00b0 00176 (main.go:11)\tLEAQ\t\"\"..autotmp_2+72(SP), AX\n\t0x00b5 00181 (main.go:11)\tPCDATA\t$2, $0\n\t0x00b5 00181 (main.go:11)\tMOVQ\tAX, 16(SP)\n\t0x00ba 00186 (main.go:11)\tCALL\truntime.mapiterinit(SB)\n\t0x00bf 00191 (main.go:11)\tJMP\t207\n\t0x00c1 00193 (main.go:11)\tPCDATA\t$2, $2\n\t0x00c1 00193 (main.go:11)\tLEAQ\t\"\"..autotmp_2+72(SP), AX\n\t0x00c6 00198 (main.go:11)\tPCDATA\t$2, $0\n\t0x00c6 00198 (main.go:11)\tMOVQ\tAX, (SP)\n\t0x00ca 00202 (main.go:11)\tCALL\truntime.mapiternext(SB)\n\t0x00cf 00207 (main.go:11)\tCMPQ\t\"\"..autotmp_2+72(SP), $0\n\t0x00d5 00213 (main.go:11)\tJNE\t193\n\t...\n```\n\n我们大致看一下整体过程，重点处理 Go map 循环迭代的是两个 runtime 方法，如下：\n\n- runtime.mapiterinit\n- runtime.mapiternext\n\n但你可能会想，明明用的是 `for range` 进行循环迭代，怎么出现了这两个函数，怎么回事？\n\n## 看一下转换后\n\n```go\nvar hiter map_iteration_struct\nfor mapiterinit(type, range, &hiter); hiter.key != nil; mapiternext(&hiter) {\n    index_temp = *hiter.key\n    value_temp = *hiter.val\n    index = index_temp\n    value = value_temp\n    original body\n}\n```\n\n实际上编译器对于 slice 和 map 的循环迭代有不同的实现方式，并不是 `for` 一扔就完事了，还做了一些附加动作进行处理。而上述代码就是 `for range map` 在编译器展开后的伪实现\n\n## 看一下源码\n\n### runtime.mapiterinit\n\n```go\nfunc mapiterinit(t *maptype, h *hmap, it *hiter) {\n\t...\n\tit.t = t\n\tit.h = h\n\tit.B = h.B\n\tit.buckets = h.buckets\n\tif t.bucket.kind&kindNoPointers != 0 {\n\t\th.createOverflow()\n\t\tit.overflow = h.extra.overflow\n\t\tit.oldoverflow = h.extra.oldoverflow\n\t}\n\n\tr := uintptr(fastrand())\n\tif h.B > 31-bucketCntBits {\n\t\tr += uintptr(fastrand()) << 31\n\t}\n\tit.startBucket = r & bucketMask(h.B)\n\tit.offset = uint8(r >> h.B & (bucketCnt - 1))\n\tit.bucket = it.startBucket\n    ...\n\n\tmapiternext(it)\n}\n```\n\n通过对 `mapiterinit` 方法阅读，可得知其主要用途是在 map 进行遍历迭代时**进行初始化动作**。共有三个形参，用于读取当前哈希表的类型信息、当前哈希表的存储信息和当前遍历迭代的数据\n\n#### 为什么\n\n咱们关注到源码中 `fastrand` 的部分，这个方法名，是不是迷之眼熟。没错，它是一个生成随机数的方法。再看看上下文：\n\n```go\n...\n// decide where to start\nr := uintptr(fastrand())\nif h.B > 31-bucketCntBits {\n\tr += uintptr(fastrand()) << 31\n}\nit.startBucket = r & bucketMask(h.B)\nit.offset = uint8(r >> h.B & (bucketCnt - 1))\n\n// iterator state\nit.bucket = it.startBucket\n```\n\n在这段代码中，它生成了随机数。用于决定从哪里开始循环迭代。更具体的话就是根据随机数，选择一个桶位置作为起始点进行遍历迭代\n\n因此每次重新 `for range map`，你见到的结果都是不一样的。那是因为它的起始位置根本就不固定！\n\n### runtime.mapiternext\n\n```go\nfunc mapiternext(it *hiter) {\n    ...\n    for ; i < bucketCnt; i++ {\n\t\t...\n\t\tk := add(unsafe.Pointer(b), dataOffset+uintptr(offi)*uintptr(t.keysize))\n\t\tv := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+uintptr(offi)*uintptr(t.valuesize))\n\t\t...\n\t\tif (b.tophash[offi] != evacuatedX && b.tophash[offi] != evacuatedY) ||\n\t\t\t!(t.reflexivekey || alg.equal(k, k)) {\n\t\t\t...\n\t\t\tit.key = k\n\t\t\tit.value = v\n\t\t} else {\n\t\t\trk, rv := mapaccessK(t, h, k)\n\t\t\tif rk == nil {\n\t\t\t\tcontinue // key has been deleted\n\t\t\t}\n\t\t\tit.key = rk\n\t\t\tit.value = rv\n\t\t}\n\t\tit.bucket = bucket\n\t\tif it.bptr != b {\n\t\t\tit.bptr = b\n\t\t}\n\t\tit.i = i + 1\n\t\tit.checkBucket = checkBucket\n\t\treturn\n\t}\n\tb = b.overflow(t)\n\ti = 0\n\tgoto next\n}\n```\n\n在上小节中，咱们已经选定了起始桶的位置。接下来就是通过 `mapiternext` 进行**具体的循环遍历动作**。该方法主要涉及如下：\n\n- 从已选定的桶中开始进行遍历，寻找桶中的下一个元素进行处理\n- 如果桶已经遍历完，则对溢出桶 `overflow buckets` 进行遍历处理\n\n通过对本方法的阅读，可得知其对 buckets 的**遍历规则**以及对于扩容的一些处理（这不是本文重点。因此没有具体展开）\n\n## 总结\n\n在本文开始，咱们先提出核心讨论点：“为什么 Go map 遍历输出是不固定顺序？”。\n\n经过这一番分析，原因也很简单明了。就是 `for range map` 在开始处理循环逻辑的时候，就做了随机播种...\n\n你想问为什么要这么做？\n\n当然是官方有意为之，因为 Go 在早期（1.0）的时候，虽是稳定迭代的，但从结果来讲，其实是无法保证每个 Go 版本迭代遍历规则都是一样的。而这将会导致可移植性问题。\n\n因此，改之。也请不要依赖...\n\n## 参考\n\n- [Go maps in action](https://blog.golang.org/go-maps-in-action)"
  },
  {
    "path": "content/posts/go/go-moduels/2019-09-29-goproxy-cn.md",
    "content": "---\n\ntitle:      \"干货满满的 Go Modules 和 goproxy.cn\"\ndate:       2019-09-29 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - go-modules\n---\n\n大家好，我是一只普通的煎鱼，周四晚上很有幸邀请到 goproxy.cn 的作者 @盛傲飞（@aofei） 到 Go 夜读给我们进行第 61 期 《Go Modules、Go Module Proxy 和 goproxy.cn》的技术分享。\n\n本次 @盛傲飞 的夜读分享，是对 Go Modules 的一次很好的解读，比较贴近工程实践，我必然希望把这块的知识更多的分享给大家，因此有了今天本篇文章，同时大家也可以多关注 Go 夜读，每周会通过 zoom 在线直播的方式分享 Go 相关的技术话题，希望对大家有所帮助。\n\n## 前言\n\nGo 1.11 推出的模块（Modules）为 Go 语言开发者打开了一扇新的大门，理想化的依赖管理解决方案使得 Go 语言朝着计算机编程史上的第一个依赖乌托邦（Deptopia）迈进。随着模块一起推出的还有模块代理协议（Module proxy protocol），通过这个协议我们可以实现 Go 模块代理（Go module proxy），也就是依赖镜像。\n\nGo 1.13 的发布为模块带来了大量的改进，所以模块的扶正就是这次 Go 1.13 发布中开发者能直接感觉到的最大变化。而问题在于，Go 1.13 中的 GOPROXY 环境变量拥有了一个在中国大陆无法访问到的默认值 `proxy.golang.org`，经过大家在 golang/go#31755 中激烈的讨论（有些人甚至将话提上升到了“自由世界”的层次），最终 Go 核心团队仍然无法为中国开发者提供一个可在中国大陆访问的官方模块代理。\n\n为了今后中国的 Go 语言开发者能更好地进行开发，七牛云推出了非营利性项目 `goproxy.cn`，其目标是为中国和世界上其他地方的 Gopher 们提供一个免费的、可靠的、持续在线的且经过 CDN 加速的模块代理。可以预见未来是属于模块化的，所以 Go 语言开发者能越早切入模块就能越早进入未来。\n\n如果说 Go 1.11 和 Go 1.12 时由于模块的不完善你不愿意切入，那么 Go 1.13 你则可以大胆地开始放心使用。本次分享将讨论如何使用模块和模块代理，以及在它们的使用中会常遇见的坑，还会讲解如何快速搭建自己的私有模块代理，并简单地介绍一下七牛云推出的 `goproxy.cn` 以及它的出现对于中国 Go 语言开发者来说重要在何处。\n\n## 目录\n\n- Go Modules 简介\n- 快速迁移项目至 Go Modules\n- 使用 Go Modules 时常遇见的坑\n    - 坑 1:判断项目是否启用了 Go Modules\n    - 坑 2:管理 Go 的环境变量\n    - 坑 3:从 dep、glide 等迁移至 Go Modules\n    - 坑 4:拉取私有模块\n    - 坑 5:更新现有的模块 \n    - 坑 6:主版本号 \n- Go Module Proxy 简介\n- Goproxy 中国(goproxy.cn)\n\n## Go Modules 简介\n\n![image](https://image.eddycjy.com/765e3c7525bede127297a66e03cf3506.jpg)\n\nGo modules (前身 vgo) 是 Go team (Russ Cox) **强推**的一个**理想化**的**类语言级**依赖管理解决方案，它是和 Go1.11 一同发布的，在 Go1.13 做了大量的优化和调整，目前已经变得比较不错，如果你想用 Go modules，但还停留在 1.11/1.12 版本的话，强烈建议升级。\n\n### 三个关键字\n\n#### 强推\n首先这并不是乱说的，因为 Go modules 确实是被强推出来的，如下：\n- 之前：大家都知道在 Go modules 之前还有一个叫 dep 的项目，它也是 Go 的一个官方的实验性项目，目的同样也是为了解决 Go 在依赖管理方面的短板。在 Russ Cox 还没有提出 Go modules 的时候，社区里面几乎所有的人都认为 dep 肯定就是未来 Go 官方的依赖管理解决方案了。\n- 后来：谁都没想到半路杀出个程咬金，Russ Cox 义无反顾地推出了 Go modules，这瞬间导致一石激起千层浪，让社区炸了锅。大家一致认为 Go team 实在是太霸道、太独裁了，连个招呼都不打一声。我记得当时有很多人在网上跟 Russ Cox 口水战，各种依赖管理解决方案的专家都冒出来发表意见，讨论范围甚至一度超出了 Go 语言的圈子触及到了其他语言的领域。\n\n#### 理想化\n从他强制要求使用语义化版本控制这一点来说就很理想化了，如下：\n- Go modules 狠到如果你的 Tag 没有遵循语义化版本控制那么它就会忽略你的 Tag，然后根据你的 Commit 时间和哈希值再为你生成一个假定的符合语义化版本控制的版本号。\n- Go modules 还默认认为，只要你的主版本号不变，那这个模块版本肯定就不包含 Breaking changes，因为语义化版本控制就是这么规定的啊。是不是很理想化。\n\n#### 类语言级：\n这个关键词其实是我自己瞎编的，我只是单纯地个人认为 Go modules 在设计上就像个语言级特性一样，比如如果你的主版本号发生变更，那么你的代码里的 import path 也得跟着变，它认为主版本号不同的两个模块版本是完全不同的两个模块。此外，Go moduels 在设计上跟 go 整个命令都结合得相当紧密，无处不在，所以我才说它是一个有点儿像语言级的特性，虽然不是太严谨。\n\n### 推 Go Modules 的人是谁\n\n那么在上文中提到的 Russ Cox 何许人也呢，很多人应该都知道他，他是 Go 这个项目目前代码提交量最多的人，甚至是第二名的两倍还要多。\n\nRuss Cox 还是 Go 现在的掌舵人（大家应该知道之前 Go 的掌舵人是 Rob Pike，但是听说由于他本人不喜欢特朗普执政所以离开了美国，然后他岁数也挺大的了，所以也正在逐渐交权，不过现在还是在参与 Go 的发展）。\n\nRuss Cox 的个人能力相当强，看问题的角度也很独特，这也就是为什么他刚一提出 Go modules 的概念就能引起那么大范围的响应。虽然是被强推的，但事实也证明当下的 Go modules 表现得确实很优秀，所以这表明一定程度上的 “独裁” 还是可以接受的，至少可以保证一个项目能更加专一地朝着一个方向发展。\n\n总之，无论如何 Go modules 现在都成了 Go 语言的一个密不可分的组件。\n\n### GOPATH\n\nGo modules 出现的目的之一就是为了解决 GOPATH 的问题，也就相当于是抛弃 GOPATH 了。\n\n### Opt-in\n\nGo modules 还处于 Opt-in 阶段，就是你想用就用，不用就不用，不强制你。但是未来很有可能 Go2 就强制使用了。\n\n### \"module\" != \"package\"\n\n有一点需要纠正，就是“模块”和“包”，也就是 “module” 和 “package” 这两个术语并不是等价的，是 “集合” 跟 “元素” 的关系，“模块” 包含 “包”，“包” 属于 “模块”，一个 “模块” 是零个、一个或多个 “包” 的集合。\n\n## Go Modules 相关属性\n\n![image](https://image.eddycjy.com/6d9f959fbdf96cc4c8a064b08287e7bc.jpg)\n\n### go.mod\n\n```\nmodule example.com/foobar\n\ngo 1.13\n\nrequire (\n    example.com/apple v0.1.2\n    example.com/banana v1.2.3\n    example.com/banana/v2 v2.3.4\n    example.com/pineapple v0.0.0-20190924185754-1b0db40df49a\n)\n\nexclude example.com/banana v1.2.4\nreplace example.com/apple v0.1.2 => example.com/rda v0.1.0 \nreplace example.com/banana => example.com/hugebanana\n```\n\n go.mod 是启用了 Go moduels 的项目所必须的最重要的文件，它描述了当前项目（也就是当前模块）的元信息，每一行都以一个动词开头，目前有以下 5 个动词:\n- module：用于定义当前项目的模块路径。\n- go：用于设置预期的 Go 版本。\n- require：用于设置一个特定的模块版本。\n- exclude：用于从使用中排除一个特定的模块版本。\n- replace：用于将一个模块版本替换为另外一个模块版本。\n\n这里的填写格式基本为包引用路径+版本号，另外比较特殊的是 `go $version`，目前从 Go1.13 的代码里来看，还只是个标识作用，暂时未知未来是否有更大的作用。\n\n### go.sum\n\ngo.sum 是类似于比如 dep 的 Gopkg.lock 的一类文件，它详细罗列了当前项目直接或间接依赖的所有模块版本，并写明了那些模块版本的 SHA-256 哈希值以备 Go 在今后的操作中保证项目所依赖的那些模块版本不会被篡改。\n\n```\nexample.com/apple v0.1.2 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= \nexample.com/apple v0.1.2/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= example.com/banana v1.2.3 h1:qHgHjyoNFV7jgucU8QZUuU4gcdhfs8QW1kw68OD2Lag= \nexample.com/banana v1.2.3/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= example.com/banana/v2 v2.3.4 h1:zl/OfRA6nftbBK9qTohYBJ5xvw6C/oNKizR7cZGl3cI= example.com/banana/v2 v2.3.4/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= \n...\n```\n\n我们可以看到一个模块路径可能有如下两种：\n\n```\nexample.com/apple v0.1.2 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= \nexample.com/apple v0.1.2/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\n```\n\n前者为 Go modules 打包整个模块包文件 zip 后再进行 hash 值，而后者为针对 go.mod 的 hash 值。他们两者，要不就是同时存在，要不就是只存在 go.mod hash。\n\n那什么情况下会不存在 zip hash 呢，就是当 Go 认为肯定用不到某个模块版本的时候就会省略它的 zip hash，就会出现不存在 zip hash，只存在 go.mod hash 的情况。\n\n### GO111MODULE\n\n这个环境变量主要是 Go modules 的开关，主要有以下参数：\n\n- auto：只在项目包含了 go.mod 文件时启用 Go modules，在 Go 1.13 中仍然是默认值，详见\n  ：golang.org/issue/31857。\n\n- on：无脑启用 Go modules，推荐设置，未来版本中的默认值，让 GOPATH 从此成为历史。\n\n- off：禁用 Go modules。\n\n### GOPROXY\n\n这个环境变量主要是用于设置 Go 模块代理，主要如下：\n\n- 它的值是一个以英文逗号 “,” 分割的 Go module proxy 列表（稍后讲解）\n\n  - 作用：用于使 Go 在后续拉取模块版本时能够脱离传统的 VCS 方式从镜像站点快速拉取。它拥有一个默认：`https://proxy.golang.org,direct`，但很可惜 `proxy.golang.org` 在中国无法访问，故而建议使用 `goproxy.cn` 作为替代，可以执行语句：`go env -w GOPROXY=https://goproxy.cn,direct`。\n\n  - 设置为 “off” ：禁止 Go 在后续操作中使用任 何 Go module proxy。\n\n刚刚在上面，我们可以发现值列表中有 “direct” ，它又有什么作用呢。其实值列表中的 “direct” 为特殊指示符，用于指示 Go 回源到模块版本的源地址去抓取(比如 GitHub 等)，当值列表中上一个 Go module proxy 返回 404 或 410 错误时，Go 自动尝试列表中的下一个，遇见 “direct” 时回源，遇见 EOF 时终止并抛出类似 “invalid version: unknown revision...” 的错误。\n\n### GOSUMDB\n\n它的值是一个 Go checksum database，用于使 Go 在拉取模块版本时(无论是从源站拉取还是通过 Go module proxy 拉取)保证拉取到的模块版本数据未经篡改，也可以是“off”即禁止 Go 在后续操作中校验模块版本\n- 格式 1：`<SUMDB_NAME>+<PUBLIC_KEY>`。\n- 格式 2：`<SUMDB_NAME>+<PUBLIC_KEY> <SUMDB_URL>`。\n\n- 拥有默认值：`sum.golang.org` (之所以没有按照上面的格式是因为 Go 对默认值做了特殊处理)。\n- 可被 Go module proxy 代理 (详见：Proxying a Checksum Database)。\n- `sum.golang.org` 在中国无法访问，故而更加建议将 GOPROXY 设置为 `goproxy.cn`，因为 `goproxy.cn` 支持代理 `sum.golang.org`。\n\n\n### Go Checksum Database\n\nGo checksum database 主要用于保护 Go 不会从任何源头拉到被篡改过的非法 Go 模块版本，其作用（左）和工作机制（右）如下图：\n\n![image](https://image.eddycjy.com/8a74a6aa59f5706c5c25836451538a12.jpg)\n\n如果有兴趣的小伙伴可以看看 [Proposal: Secure the Public Go Module Ecosystem](https://go.googlesource.com/proposal/+/master/design/25530-sumdb.md#proxying-a-checksum-database)，有详细介绍其算法机制，如果想简单一点，查看 `go help module-auth` 也是一个不错的选择。\n\n\n### GONOPROXY/GONOSUMDB/GOPRIVATE\n\n这三个环境变量都是用在当前项目依赖了私有模块，也就是依赖了由 GOPROXY 指定的 Go module proxy 或由 GOSUMDB 指定 Go checksum database 无法访问到的模块时的场景\n\n- 它们三个的值都是一个以英文逗号 “,” 分割的模块路径前缀，匹配规则同 path.Match。\n- 其中 GOPRIVATE 较为特殊，它的值将作为 GONOPROXY 和 GONOSUMDB 的默认值，所以建议的最佳姿势是只是用 GOPRIVATE。\n\n在使用上来讲，比如 `GOPRIVATE=*.corp.example.com` 表示所有模块路径以 `corp.example.com` 的下一级域名 (如 `team1.corp.example.com`) 为前缀的模块版本都将不经过 Go module proxy 和 Go checksum database，需要注意的是不包括 `corp.example.com` 本身。\n\n### Global Caching\n\n这个主要是针对 Go modules 的全局缓存数据说明，如下：\n\n- 同一个模块版本的数据只缓存一份，所有其他模块共享使用。\n- 目前所有模块版本数据均缓存在 `$GOPATH/pkg/mod`和 ​`$GOPATH/pkg/sum` 下，未来或将移至 `$GOCACHE/mod `和`$GOCACHE/sum` 下( 可能会在当 `$GOPATH` 被淘汰后)。\n- 可以使用 `go clean -modcache` 清理所有已缓存的模块版本数据。\n\n另外在 Go1.11 之后 GOCACHE 已经不允许设置为 off 了，我想着这也是为了模块数据缓存移动位置做准备，因此大家应该尽快做好适配。\n\n\n## 快速迁移项目至 Go Modules\n\n- 第一步: 升级到 Go 1.13。\n- 第二步: 让 GOPATH 从你的脑海中完全消失，早一步踏入未来。\n    - 修改 GOBIN 路径（可选）：`go env -w GOBIN=$HOME/bin`。\n    - 打开 Go modules：`go env -w GO111MODULE=on`。\n    - 设置 GOPROXY：`go env -w GOPROXY=https://goproxy.cn,direct` # 在中国是必须的，因为它的默认值被墙了。\n- 第三步(可选): 按照你喜欢的目录结构重新组织你的所有项目。\n- 第四步: 在你项目的根目录下执行 `go mod init <OPTIONAL_MODULE_PATH>` 以生成 go.mod 文件。\n- 第五步: 想办法说服你身边所有的人都去走一下前四步。\n\n\n## 迁移后 go get 行为的改变\n\n- 用 `go help module-get` 和 `go help gopath-get`分别去了解 Go modules 启用和未启用两种状态下的 go get 的行为\n- 用 `go get` 拉取新的依赖\n   - 拉取最新的版本(优先择取 tag)：`go get golang.org/x/text@latest`\n   - 拉取 `master` 分支的最新 commit：`go get golang.org/x/text@master`\n   - 拉取 tag 为 v0.3.2 的 commit：`go get golang.org/x/text@v0.3.2`\n   - 拉取 hash 为 342b231 的 commit，最终会被转换为 v0.3.2：`go get golang.org/x/text@342b2e`\n   - 用 `go get -u` 更新现有的依赖\n   - 用 `go mod download` 下载 go.mod 文件中指明的所有依赖\n   - 用 `go mod tidy` 整理现有的依赖\n   - 用 `go mod graph` 查看现有的依赖结构\n   - 用 `go mod init` 生成 go.mod 文件 (Go 1.13 中唯一一个可以生成 go.mod 文件的子命令)\n- 用 `go mod edit` 编辑 go.mod 文件\n- 用 `go mod vendor` 导出现有的所有依赖 (事实上 Go modules 正在淡化 Vendor 的概念)\n- 用 `go mod verify` 校验一个模块是否被篡改过\n\n这里我们注意到有两点比较特别，分别是：\n\n- 第一点：为什么 “拉取 hash 为 342b231 的 commit，最终会被转换为 v0.3.2” 呢。这是因为虽然我们设置了拉取 @342b2e commit，但是因为 Go modules 会与 tag 进行对比，若发现对应的 commit 与 tag 有关联，则进行转换。\n- 第二点：为什么不建议使用 `go mod vendor`，因为 Go modules 正在淡化 Vendor 的概念，很有可能 Go2 就去掉了。\n\n## 使用 Go Modules 时常遇见的坑\n\n### 坑 1: 判断项目是否启用了 Go Modules\n\n![image](https://image.eddycjy.com/0dda1c26b7aa3f9e8655c8e366f49116.jpg)\n\n\n\n### 坑 2: 管理 Go 的环境变量\n\n![image](https://image.eddycjy.com/78a93176b5e24dfde88327aebe63fe9c.jpg)\n\n这里主要是提到 Go1.13 新增了 `go env -w` 用于写入环境变量，而写入的地方是 `os.UserConfigDir` 所返回的路径，需要注意的是 `go env -w` 不会覆写。\n\n### 坑 3: 从 dep、glide 等迁移至 Go Modules\n\n![image](https://image.eddycjy.com/67c83f5d4a3d936449a705921fcfe492.jpg)\n\n这里主要是指从旧有的依赖包管理工具（dep/glide 等）进行迁移时，因为 BUG 的原因会导致不经过 GOPROXY 的代理，解决方法有如下两个：\n\n- 手动创建一个 go.mod 文件，再执行 go mod tidy 进行补充。\n- 上代理，相当于不使用 GOPROXY 了。\n\n### 坑 4:拉取私有模块\n\n![image](https://image.eddycjy.com/075bdc3d3552c000981c9d4fdd8d0f3f.jpg)\n\n这里主要想涉及两块知识点，如下：\n\n- GOPROXY 是无权访问到任何人的私有模块的，所以你放心，安全性没问题。\n- GOPROXY 除了设置模块代理的地址以外，还需要增加 “direct” 特殊标识才可以成功拉取私有库。\n\n### 坑 5:更新现有的模块\n\n![image](https://image.eddycjy.com/d35e9f465d82a14c53fcca3ff5ebc557.jpg)\n\n### 坑 6:主版本号 \n\n![image](https://image.eddycjy.com/75778deb206803598e48693f6fea60b8.jpg)\n\n\n\n## Go Module Proxy 简介\n\n![image](https://image.eddycjy.com/20cb4e449ab50de36a880e3b22e1e8d8.jpg)\n\n在这里再次强调了 Go Module Proxy 的作用（图左），以及其对应的协议交互流程（图右），有兴趣的小伙伴可以认真看一下。\n\n## Goproxy 中国(goproxy.cn)\n\n在这块主要介绍了  Goproxy 的实践操作以及 goproxy.cn 的一些 Q&A 和 近况，如下：\n\n### Q&A\n\n**Q：如果中国 Go 语言社区没有咱们自己家的 Go Module Proxy 会怎么样？**\n\n**A：**在 Go 1.13 中 GOPROXY 和 GOSUMDB 这两个环境变量都有了在中国无法 访问的默认值，尽管我在 golang.org/issue/31755 里努力尝 试过，但最终仍然无法为咱们中国的 Go 语言开发者谋得一个完美的解决方案。所以从今以后咱 们中国的所有 Go 语言开发者，只要是 使用了 Go modules 的，那么都必须先修改 GOPROXY 和 GOSUMDB 才能正常使用 Go 做开发，否则可能连一个最简单的程序都跑不起 来(只要它有依 赖第三方模 块)。\n\n\n\n**Q： 我创建 Goproxy 中国(goproxy.cn)的主要原因？**\n\n**A：**其实更早的时候，也就是今年年初我也曾 试图在 golang.org/issue/31020 中请求 Go team 能想办法避免那时的 GOPROXY 即将拥有的默认值可以在中国正常访问，但 Go team 似乎也无能为力，为此我才坚定了创建 goproxy.cn 的信念。既然别人没法儿帮忙，那咱们就 得自己动手，不为别的，就为了让大家以后能够更愉快地使用 Go 语言配合 Go modules 做开发。\n\n最初我先是和七牛云的 许叔(七牛云的 创始人兼 CEO 许式伟)提出了我打算 创建 goproxy.cn 的想法，本是抱着 试试看的目的，但没想 到 许叔几乎是没有超过一分钟的考虑便认可了我的想法并表示愿意一起推 动。那一阵子刚好赶上我在写毕业论文，所以项目开发完后就 一直没和七牛云做交接，一直跑在我的个人服 务器上。直到有一次 goproxy.cn 被攻击了，一下午的功夫 烧了我一百多美元，然后我才 意识到这种项目真不能个人来做。个人来做不靠 谱，万一依赖这个项目的人多了，项目再出什么事儿，那就会给大家􏰁成不必要的损 失。所以我赶紧和七牛云做了交接，把 goproxy.cn 完全交给了七牛云，甚至连域名都过户了去。\n\n\n\n### 近况\n\n![image](https://image.eddycjy.com/7bf56751651d56edb989f7cfd64c0006.png)\n\n- Goproxy 中国 (goproxy.cn) 是目前中国最可靠的 Go module proxy (真不是在自卖自夸)。\n- 为中国 Go 语言开发者量身打􏰁，支持代理 GOSUMDB 的默认值，经过全球 CDN 加速，高可用，可 应用进公司复杂的开发环境中，亦可用作上游代理。\n- 由中国倍受信赖的云服务提供商七牛云无偿提供基础设施支持的开源的非营利性项目。\n- 目标是为中国乃至全世界的 Go 语言开发者提供一个免 费的、可靠的、持 续在线的且经过 CDN 加􏰀的 Go module proxy。\n- 域名已由七牛云进行了备案 (沪ICP备11037377号-56)。\n\n### 情况\n\n![image](https://image.eddycjy.com/aa517d9e93aff49762de76f601702eb1.jpg)\n\n此处呈现的是存储大小，主要是针对模块包代码，而一般来讲代码并不会有多大，0-10MB，10-50MB 占最大头，也是能够理解，但是大于 100MB 的模块包代码就比较夸张了。\n\n![image](https://image.eddycjy.com/94bbc93b83f87b43b254f5f15ff995e7.jpg)\n\n此时主要是展示了一下近期 goproxy.cn 的网络数据情况，我相信未来是会越来越高的，值得期待。\n\n\n\n## Q&A\n\n\n**Q：如何解决 Go 1.13 在从 GitLab 拉取模块版本时遇到的，Go 错误地按照非期望值的路径寻找目标模块版本结果致使最终目标模块拉取失败的问题？**\n\n**A：**GitLab 中配合 goget 而设置的 `<meta>` 存在些许问题，导致 Go 1.13 错误地识别了模块的具体路径，这是个 Bug，据说在 GitLab 的新版本中已经被修复了，详细内容可以看 https://github.com/golang/go/issues/34094 这个 Issue。然后目前的解决办法的话除了升级 GitLab 的版本外，还可以参考 https://github.com/developer-learning/night-reading-go/issues/468#issuecomment-535850154 这条回复。\n\n**Q：使用 Go modules 时可以同时依赖同一个模块的不同的两个或者多个小版本（修订版本号不同）吗？**\n\n**A：**不可以的，Go modules 只可以同时依赖一个模块的不同的两个或者多个大版本（主版本号不同）。比如可以同时依赖 example.com/foobar@v1.2.3 和 example.com/foobar/v2@v2.3.4，因为他们的模块路径（module path）不同，Go modules 规定主版本号不是 v0 或者 v1 时，那么主版本号必须显式地出现在模块路径的尾部。但是，同时依赖两个或者多个小版本是不支持的。比如如果模块 A 同时直接依赖了模块 B 和模块 C，且模块 A 直接依赖的是模块 C 的 v1.0.0 版本，然后模块 B 直接依赖的是模块 C 的 v1.0.1 版本，那么最终 Go modules 会为模块 A 选用模块 C 的 v1.0.1 版本而不是模块 A 的 go.mod 文件中指明的 v1.0.0 版本。\n\n这是因为 Go modules 认为只要主版本号不变，那么剩下的都可以直接升级采用最新的。但是如果采用了最新的结果导致项目 Break 掉了，那么 Go modules 就会 Fallback 到上一个老的版本，比如在前面的例子中就会 Fallback 到 v1.0.0 版本。\n\n**Q：在 go.sum 文件中的一个模块版本的 Hash 校验数据什么情况下会成对出现，什么情况下只会存在一行？**\n\n**A：**通常情况下，在 go.sum 文件中的一个模块版本的 Hash 校验数据会有两行，前一行是该模块的 ZIP 文件的 Hash 校验数据，后一行是该模块的 go.mod 文件的 Hash 校验数据。但是也有些情况下只会出现一行该模块的 go.mod 文件的 Hash 校验数据，而不包含该模块的 ZIP 文件本身的 Hash 校验数据，这个情况发生在 Go modules 判定为你当前这个项目完全用不到该模块，根本也不会下载该模块的 ZIP 文件，所以就没必要对其作出 Hash 校验保证，只需要对该模块的 go.mod 文件作出 Hash 校验保证即可，因为 go.mod 文件是用得着的，在深入挖取项目依赖的时候要用。\n\n**Q：能不能更详细地讲解一下 go.mod 文件中的 replace 动词的行为以及用法？**\n\n**A：**这个 replace 动词的作用是把一个“模块版本”替换为另外一个“模块版本”，这是“模块版本”和“模块版本（module path）”之间的替换，“=>”标识符前面的内容是待替换的“模块版本”的“模块路径”，后面的内容是要替换的目标“模块版本”的所在地，即路径，这个路径可以是一个本地磁盘的相对路径，也可以是一个本地磁盘的绝对路径，还可以是一个网络路径，但是这个目标路径并不会在今后你的项目代码中作为你“导入路径（import path）”出现，代码里的“导入路径”还是得以你替换成的这个目标“模块版本”的“模块路径”作为前缀。\n\n另外需要注意，Go modules 是不支持在 “导入路径” 里写相对路径的。举个例子，如果项目 A 依赖了模块 B，比如模块 B 的“模块路径”是 example.com/b，然后它在的磁盘路径是 ~/b，在项目 A 里的 go.mod 文件中你有一行 replace example.com/b=>~/b，然后在项目 A 里的代码中的“导入路基”就是 import\"example.com/b\"，而不是 import\"~/b\"，剩下的工作是 Go modules 帮你自动完成了的。\n\n然后就是我在分享中也提到了， exclude 和 replace 这两个动词只作用于当前主模块，也就是当前项目，它所依赖的那些其他模块版本中如果出现了你待替换的那个模块版本的话，Go modules 还是会为你依赖的那个模块版本去拉取你的这个待替换的模块版本。\n\n举个例子，比如项目 A 直接依赖了模块 B 和模块 C，然后模块 B 也直接依赖了模块 C，那么你在项目 A 中的 go.mod 文件里的 replace c=>~/some/path/c 是只会影响项目 A 里写的代码中，而模块 B 所用到的还是你 replace 之前的那个 c，并不是你替换成的 ~/some/path/c 这个。\n\n\n## 总结\n\n在 Go1.13 发布后，接触 Go modules 和 Go module proxy 的人越来越多，经常在各种群看到各种小伙伴在咨询，包括我自己也贡献了好几枚 “坑”，因此我觉得傲飞的这一次 《Go Modules、Go Module Proxy 和 goproxy.cn》的技术分享，非常的有实践意义。如果后续大家还有什么建议或问题，欢迎随时来讨论。\n\n最后，感谢 goproxy.cn 背后的人们（@七牛云 和 @盛傲飞）对中国 Go 语言社区的无私贡献和奉献。\n\n\n## 进一步阅读\n\n- [night-reading-go/issues/468](https://github.com/developer-learning/night-reading-go/issues/468)\n- [B站：【Go 夜读】第 61 期 Go Modules、Go Module Proxy 和 goproxy.cn](https://www.bilibili.com/video/av69111199?from=search&seid=14251207475086319821)\n- [youtube：【Go 夜读】第 61 期 Go Modules、Go Module Proxy 和 goproxy.cn](https://www.youtube.com/watch?v=H3LVVwZ9zNY)\n\n\n"
  },
  {
    "path": "content/posts/go/go-moduels/2020-02-28-go-modules.md",
    "content": "---\ntitle:      \"Go Modules 终极入门\"\ndate:       2020-02-28 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - go-modules\n---\n\nGo modules 是 Go 语言中正式官宣的项目依赖解决方案，Go modules（前身为vgo）于 Go1.11 正式发布，在 Go1.14 已经准备好，并且可以用在生产上（ready for production）了，Go官方也鼓励所有用户从其他依赖项管理工具迁移到 Go modules。\n\n而 Go1.14，在近期也终于正式发布，Go 官方亲自 “喊” 你来用：\n\n![image](https://image.eddycjy.com/243fb2cca64972b2f36827f59b26d840.jpeg)\n\n\n因此在今天这篇文章中，我将给大家带来 Go modules 的 “终极入门”，欢迎大家一起共同探讨。\n\nGo modules 是 Go 语言中正式官宣的项目依赖管理工具，Go modules（前身为vgo）于 Go1.11 正式发布，在 Go1.14 已经准备好，并且可以用在生产上（ready for production）了，鼓励所有用户从其他依赖项管理工具迁移到 Go modules。\n\n## 什么是Go Modules\n\nGo modules 是 Go 语言的依赖解决方案，发布于 Go1.11，成长于 Go1.12，丰富于 Go1.13，正式于 Go1.14 推荐在生产上使用。\n\nGo moudles 目前集成在 Go 的工具链中，只要安装了 Go，自然而然也就可以使用 Go moudles 了，而 Go modules 的出现也解决了在 Go1.11 前的几个常见争议问题：\n\n1. Go 语言长久以来的依赖管理问题。\n2. “淘汰”现有的 GOPATH 的使用模式。\n3. 统一社区中的其它的依赖管理工具（提供迁移功能）。\n\n## GOPATH的那些点点滴滴\n\n我们有提到 Go modules 的解决的问题之一就是“淘汰”掉 GOPATH，但是 GOPATH 又是什么呢，为什么在 Go1.11 前就使用 GOPATH，而 Go1.11 后就开始逐步建议使用 Go modules，不再推荐 GOPATH 的模式了呢？\n\n### GOPATH是什么\n\n我们先看看第一个问题，GOPATH 是什么，我们可以输入如下命令查看：\n\n```\n$ go env\nGOPATH=\"/Users/eddycjy/go\"\n...\n```\n\n我们输入`go env`命令行后可以查看到 GOPATH 变量的结果，我们进入到该目录下进行查看，如下：\n\n```\ngo\n├── bin\n├── pkg\n└── src\n    ├── github.com\n    ├── golang.org\n    ├── google.golang.org\n    ├── gopkg.in\n    ....\n```\n\nGOPATH目录下一共包含了三个子目录，分别是：\n\n- bin：存储所编译生成的二进制文件。\n- pkg：存储预编译的目标文件，以加快程序的后续编译速度。\n- src：存储所有`.go`文件或源代码。在编写 Go 应用程序，程序包和库时，一般会以`$GOPATH/src/github.com/foo/bar`的路径进行存放。\n\n因此在使用 GOPATH 模式下，我们需要将应用代码存放在固定的`$GOPATH/src`目录下，并且如果执行`go get`来拉取外部依赖会自动下载并安装到`$GOPATH`目录下。\n\n### 为什么弃用GOPATH模式\n\n在 GOPATH 的 `$GOPATH/src` 下进行 `.go` 文件或源代码的存储，我们可以称其为 GOPATH 的模式，这个模式，看起来好像没有什么问题，那么为什么我们要弃用呢，参见如下原因：\n\n- GOPATH 模式下没有版本控制的概念，具有致命的缺陷，至少会造成以下问题：\n  - 在执行`go get`的时候，你无法传达任何的版本信息的期望，也就是说你也无法知道自己当前更新的是哪一个版本，也无法通过指定来拉取自己所期望的具体版本。\n  - 在运行Go应用程序的时候，你无法保证其它人与你所期望依赖的第三方库是相同的版本，也就是说在项目依赖库的管理上，你无法保证所有人的依赖版本都一致。\n  - 你没办法处理 v1、v2、v3 等等不同版本的引用问题，因为 GOPATH 模式下的导入路径都是一样的，都是`github.com/foo/bar`。\n- Go 语言官方从 Go1.11 起开始推进 Go modules（前身vgo），Go1.13 起不再推荐使用 GOPATH 的使用模式，Go modules 也渐趋稳定，因此新项目也没有必要继续使用GOPATH模式。\n\n### 在GOPATH模式下的产物\n\nGo1 在 2012 年 03 月 28 日发布，而 Go1.11 是在 2018 年 08 月 25 日才正式发布（数据来源：Github Tag），在这个空档的时间内，并没有 Go modules 这一个东西，最早期可能还好说，因为刚发布，用的人不多，所以没有明显暴露，但是后期 Go 语言使用的人越来越多了，那怎么办？\n\n这时候社区中逐渐的涌现出了大量的依赖解决方案，百花齐放，让人难以挑选，其中包括我们所熟知的 vendor 目录的模式，以及曾经一度被认为是“官宣”的 dep 的这类依赖管理工具。\n\n但为什么 dep 没有正在成为官宣呢，其实是因为随着 Russ Cox 与 Go 团队中的其他成员不断深入地讨论，发现dep 的一些细节似乎越来越不适合 Go，因此官方采取了另起 proposal 的方式来推进，其方案的结果一开始先是释出 vgo（Go modules的前身，知道即可，不需要深入了解），最终演变为我们现在所见到的 Go modules，也在 Go1.11 正式进入了 Go 的工具链。\n\n因此与其说是 “在GOPATH模式下的产物”，不如说是历史为当前提供了重要的教训，因此出现了 Go modules。\n\n## Go Modules基本使用\n\n在初步了解了 Go modules 的前世今生后，我们正式进入到 Go modules 的使用，首先我们将从头开始创建一个 Go modules 的项目（原则上所创建的目录应该不要放在 GOPATH 之中）。\n\n### 所提供的命令\n\n在 Go modules 中，我们能够使用如下命令进行操作：\n\n| 命令            | 作用                             |\n| --------------- | -------------------------------- |\n| go mod init     | 生成 go.mod 文件                 |\n| go mod download | 下载 go.mod 文件中指明的所有依赖 |\n| go mod tidy     | 整理现有的依赖                   |\n| go mod graph    | 查看现有的依赖结构               |\n| go mod edit     | 编辑 go.mod 文件                 |\n| go mod vendor   | 导出项目所有的依赖到vendor目录   |\n| go mod verify   | 校验一个模块是否被篡改过         |\n| go mod why      | 查看为什么需要依赖某模块         |\n\n### 所提供的环境变量\n\n在 Go modules 中有如下常用环境变量，我们可以通过 `go env` 命令来进行查看，如下：\n\n```\n$ go env\nGO111MODULE=\"auto\"\nGOPROXY=\"https://proxy.golang.org,direct\"\nGONOPROXY=\"\"\nGOSUMDB=\"sum.golang.org\"\nGONOSUMDB=\"\"\nGOPRIVATE=\"\"\n...\n```\n\n#### GO111MODULE\n\nGo语言提供了 GO111MODULE 这个环境变量来作为 Go modules 的开关，其允许设置以下参数：\n\n- auto：只要项目包含了 go.mod 文件的话启用 Go modules，目前在 Go1.11 至 Go1.14 中仍然是默认值。\n- on：启用 Go modules，推荐设置，将会是未来版本中的默认值。\n- off：禁用 Go modules，不推荐设置。\n\n##### GO111MODULE的小历史\n\n你可能会留意到 GO111MODULE 这个名字比较“奇特”，实际上在 Go 语言中经常会有这类阶段性的变量， GO111MODULE 这个命名代表着Go语言在 1.11 版本添加的，针对 Module 的变量。\n\n像是在 Go1.5 版本的时候，也发布了一个系统环境变量 GO15VENDOREXPERIMENT，作用是用于开启 vendor 目录的支持，当时其默认值也不是开启，仅仅作为 experimental。其随后在 Go1.6 版本时也将默认值改为了开启，并且最后作为了official，GO15VENDOREXPERIMENT 系统变量就退出了历史舞台。\n\n而未来 GO111MODULE 这一个系统环境变量也会面临这个问题，也会先调整为默认值为 on（曾经在Go1.13想想改为 on，并且已经合并了 PR，但最后因为种种原因改回了 auto），然后再把 GO111MODULE 的支持给去掉，我们猜测应该会在 Go2 将 GO111MODULE 给去掉，因为如果直接去掉 GO111MODULE 的支持，会存在兼容性问题。\n\n#### GOPROXY\n\n这个环境变量主要是用于设置 Go 模块代理（Go module proxy），其作用是用于使 Go 在后续拉取模块版本时能够脱离传统的 VCS 方式，直接通过镜像站点来快速拉取。\n\nGOPROXY 的默认值是：`https://proxy.golang.org,direct`，这有一个很严重的问题，就是 `proxy.golang.org` 在国内是无法访问的，因此这会直接卡住你的第一步，所以你必须在开启 Go modules 的时，同时设置国内的 Go 模块代理，执行如下命令：\n\n```\n$ go env -w GOPROXY=https://goproxy.cn,direct\n```\n\nGOPROXY的值是一个以英文逗号 “,” 分割的 Go 模块代理列表，允许设置多个模块代理，假设你不想使用，也可以将其设置为 “off” ，这将会禁止 Go 在后续操作中使用任何 Go 模块代理。\n\n##### direct是什么\n\n而在刚刚设置的值中，我们可以发现值列表中有 “direct” 标识，它又有什么作用呢？\n\n实际上 “direct” 是一个特殊指示符，用于指示 Go 回源到模块版本的源地址去抓取（比如 GitHub 等），场景如下：当值列表中上一个 Go 模块代理返回 404 或 410 错误时，Go 自动尝试列表中的下一个，遇见 “direct” 时回源，也就是回到源地址去抓取，而遇见 EOF 时终止并抛出类似 “invalid version: unknown revision...” 的错误。\n\n#### GOSUMDB\n\n它的值是一个 Go checksum database，用于在拉取模块版本时（无论是从源站拉取还是通过 Go module proxy 拉取）保证拉取到的模块版本数据未经过篡改，若发现不一致，也就是可能存在篡改，将会立即中止。\n\nGOSUMDB的默认值为：`sum.golang.org`，在国内也是无法访问的，但是 GOSUMDB 可以被 Go 模块代理所代理（详见：Proxying a Checksum Database）。\n\n因此我们可以通过设置 GOPROXY 来解决，而先前我们所设置的模块代理 `goproxy.cn` 就能支持代理 `sum.golang.org`，所以这一个问题在设置 GOPROXY 后，你可以不需要过度关心。\n\n另外若对 GOSUMDB 的值有自定义需求，其支持如下格式：\n\n- 格式 1：`<SUMDB_NAME>+<PUBLIC_KEY>`。\n- 格式 2：`<SUMDB_NAME>+<PUBLIC_KEY> <SUMDB_URL>`。\n\n也可以将其设置为“off”，也就是禁止 Go 在后续操作中校验模块版本。\n\n#### GONOPROXY/GONOSUMDB/GOPRIVATE\n\n这三个环境变量都是用在当前项目依赖了私有模块，例如像是你公司的私有 git 仓库，又或是 github 中的私有库，都是属于私有模块，都是要进行设置的，否则会拉取失败。\n\n更细致来讲，就是依赖了由 GOPROXY 指定的 Go 模块代理或由 GOSUMDB 指定 Go checksum database 都无法访问到的模块时的场景。\n\n而一般建议直接设置 GOPRIVATE，它的值将作为 GONOPROXY 和 GONOSUMDB 的默认值，所以建议的最佳姿势是直接使用 GOPRIVATE。\n\n并且它们的值都是一个以英文逗号 “,” 分割的模块路径前缀，也就是可以设置多个，例如：\n\n```\n$ go env -w GOPRIVATE=\"git.example.com,github.com/eddycjy/mquote\"\n```\n\n设置后，前缀为 git.xxx.com 和 github.com/eddycjy/mquote 的模块都会被认为是私有模块。\n\n如果不想每次都重新设置，我们也可以利用通配符，例如：\n\n```\n$ go env -w GOPRIVATE=\"*.example.com\"\n```\n\n这样子设置的话，所有模块路径为 example.com 的子域名（例如：git.example.com）都将不经过 Go module proxy 和 Go checksum database，需要注意的是不包括 example.com 本身。\n\n### 开启Go Modules\n\n目前Go modules并不是默认开启，因此Go语言提供了GO111MODULE这个环境变量来作为Go modules的开关，其允许设置以下参数：\n\n- auto：只要项目包含了go.mod文件的话启用 Go modules，目前在Go1.11至Go1.14中仍然是默认值。\n- on：启用 Go modules，推荐设置，将会是未来版本中的默认值。\n- off：禁用 Go modules，不推荐设置。\n\n如果你不确定你当前的值是什么，可以执行`go env`命令，查看结果：\n\n```\n$ go env\nGO111MODULE=\"off\"\n...\n```\n\n如果需要对GO111MODULE的值进行变更，推荐通过`go env`命令进行设置：\n\n```\n $ go env -w GO111MODULE=on\n```\n\n但是需要注意的是如果对应的系统环境变量有值了（进行过设置），会出现如下警告信息：`warning: go env -w GO111MODULE=... does not override conflicting OS environment variable`。\n\n又或是可以通过直接设置系统环境变量（写入对应的.bash_profile文件亦可）来实现这个目的：\n\n```\n$ export GO111MODULE=on\n```\n\n### 初始化项目\n\n在完成 Go modules 的开启后，我们需要创建一个示例项目来进行演示，执行如下命令：\n\n```\n$ mkdir -p $HOME/eddycjy/module-repo \n$ cd $HOME/eddycjy/module-repo\n```\n\n然后进行Go modules的初始化，如下：\n\n```\n$ go mod init github.com/eddycjy/module-repo\ngo: creating new go.mod: module github.com/eddycjy/module-repo\n```\n\n在执行 `go mod init` 命令时，我们指定了模块导入路径为 `github.com/eddycjy/module-repo`。接下来我们在该项目根目录下创建 main.go 文件，如下：\n\n```\npackage main\n\nimport (\n    \"fmt\"\n    \"github.com/eddycjy/mquote\"\n)\n\nfunc main() {\n\tfmt.Println(mquote.GetHello())\n}\n```\n\n然后在项目根目录执行 `go get github.com/eddycjy/mquote` 命令，如下：\n\n```\n$ go get github.com/eddycjy/mquote \ngo: finding github.com/eddycjy/mquote latest\ngo: downloading github.com/eddycjy/mquote v0.0.0-20200220041913-e066a990ce6f\ngo: extracting github.com/eddycjy/mquote v0.0.0-20200220041913-e066a990ce6f\n```\n\n### 查看go.mod 文件\n\n在初始化项目时，会生成一个 go.mod 文件，是启用了 Go modules 项目所必须的最重要的标识，同时也是GO111MODULE 值为 auto 时的识别标识，它描述了当前项目（也就是当前模块）的元信息，每一行都以一个动词开头。\n\n在我们刚刚进行了初始化和简单拉取后，我们再次查看go.mod文件，基本内容如下：\n\n```\nmodule github.com/eddycjy/module-repo\n\ngo 1.13\n\nrequire (\n\tgithub.com/eddycjy/mquote v0.0.0-20200220041913-e066a990ce6f\n)\n```\n\n为了更进一步的讲解，我们模拟引用如下：\n\n```\nmodule github.com/eddycjy/module-repo\n\ngo 1.13\n\nrequire (\n    example.com/apple v0.1.2\n    example.com/banana v1.2.3\n    example.com/banana/v2 v2.3.4\n    example.com/pear // indirect\n    example.com/strawberry // incompatible\n)\n\nexclude example.com/banana v1.2.4\nreplace example.com/apple v0.1.2 => example.com/fried v0.1.0 \nreplace example.com/banana => example.com/fish\n```\n\n- module：用于定义当前项目的模块路径。\n- go：用于标识当前模块的 Go 语言版本，值为初始化模块时的版本，目前来看还只是个标识作用。\n- require：用于设置一个特定的模块版本。\n- exclude：用于从使用中排除一个特定的模块版本。\n- replace：用于将一个模块版本替换为另外一个模块版本。\n\n另外你会发现 `example.com/pear` 的后面会有一个 indirect 标识，indirect 标识表示该模块为间接依赖，也就是在当前应用程序中的 import 语句中，并没有发现这个模块的明确引用，有可能是你先手动 `go get` 拉取下来的，也有可能是你所依赖的模块所依赖的，情况有好几种。\n\n### 查看go.sum文件\n\n在第一次拉取模块依赖后，会发现多出了一个 go.sum 文件，其详细罗列了当前项目直接或间接依赖的所有模块版本，并写明了那些模块版本的 SHA-256 哈希值以备 Go 在今后的操作中保证项目所依赖的那些模块版本不会被篡改。\n\n```\ngithub.com/eddycjy/mquote v0.0.1 h1:4QHXKo7J8a6J/k8UA6CiHhswJQs0sm2foAQQUq8GFHM=\ngithub.com/eddycjy/mquote v0.0.1/go.mod h1:ZtlkDs7Mriynl7wsDQ4cU23okEtVYqHwl7F1eDh4qPg=\ngithub.com/eddycjy/mquote/module/tour v0.0.1 h1:cc+pgV0LnR8Fhou0zNHughT7IbSnLvfUZ+X3fvshrv8=\ngithub.com/eddycjy/mquote/module/tour v0.0.1/go.mod h1:8uL1FOiQJZ4/1hzqQ5mv4Sm7nJcwYu41F3nZmkiWx5I=\n...\n```\n\n我们可以看到一个模块路径可能有如下两种：\n\n```\ngithub.com/eddycjy/mquote v0.0.1 h1:4QHXKo7J8a6J/k8UA6CiHhswJQs0sm2foAQQUq8GFHM=\ngithub.com/eddycjy/mquote v0.0.1/go.mod h1:ZtlkDs7Mriynl7wsDQ4cU23okEtVYqHwl7F1eDh4qPg=\n```\n\nh1 hash 是 Go modules 将目标模块版本的 zip 文件开包后，针对所有包内文件依次进行 hash，然后再把它们的 hash 结果按照固定格式和算法组成总的 hash 值。\n\n而 h1 hash 和 go.mod hash 两者，要不就是同时存在，要不就是只存在 go.mod hash。那什么情况下会不存在 h1 hash 呢，就是当 Go 认为肯定用不到某个模块版本的时候就会省略它的 h1 hash，就会出现不存在 h1 hash，只存在 go.mod hash 的情况。\n\n### 查看全局缓存\n\n我们刚刚成功的将 `github.com/eddycjy/mquote` 模块拉取了下来，其拉取的结果缓存在  `$GOPATH/pkg/mod`和 `$GOPATH/pkg/sumdb` 目录下，而在`mod`目录下会以 `github.com/foo/bar` 的格式进行存放，如下：\n\n```\nmod\n├── cache\n├── github.com\n├── golang.org\n├── google.golang.org\n├── gopkg.in\n...\n```\n\n需要注意的是同一个模块版本的数据只缓存一份，所有其它模块共享使用。如果你希望清理所有已缓存的模块版本数据，可以执行 `go clean -modcache` 命令。\n\n## Go Modules下的go get行为\n\n在拉取项目依赖时，你会发现拉取的过程总共分为了三大步，分别是 finding（发现）、downloading（下载）以及 extracting（提取）， 并且在拉取信息上一共分为了三段内容：\n\n![image](https://image.eddycjy.com/a78b16231e7c0164e0acccb7abdd01be.jpg)\n\n需要注意的是，所拉取版本的 commit 时间是以UTC时区为准，而并非本地时区，同时我们会发现我们 `go get` 命令所拉取到的版本是 v0.0.0，这是因为我们是直接执行 `go get -u` 获取的，并没有指定任何的版本信息，由 Go modules 自行按照内部规则进行选择。\n\n### go get的拉取行为\n\n刚刚我们用 `go get` 命令拉取了新的依赖，那么 `go get` 又提供了哪些功能呢，常用的拉取命令如下：\n\n| 命令               | 作用                                                         |\n| ------------------ | ------------------------------------------------------------ |\n| go get             | 拉取依赖，会进行指定性拉取（更新），并不会更新所依赖的其它模块。 |\n| go get -u          | 更新现有的依赖，会强制更新它所依赖的其它全部模块，不包括自身。 |\n| go get -u -t ./... | 更新所有直接依赖和间接依赖的模块版本，包括单元测试中用到的。 |\n\n那么我想选择具体版本应当如何执行呢，如下：\n\n| 命令                            | 作用                                                    |\n| ------------------------------- | ------------------------------------------------------- |\n| go get golang.org/x/text@latest | 拉取最新的版本，若存在tag，则优先使用。                 |\n| go get golang.org/x/text@master | 拉取 master 分支的最新 commit。                         |\n| go get golang.org/x/text@v0.3.2 | 拉取 tag 为 v0.3.2 的 commit。                          |\n| go get golang.org/x/text@342b2e | 拉取 hash 为 342b231 的 commit，最终会被转换为 v0.3.2。 |\n\n### go get的版本选择\n\n我们回顾一下我们拉取的 `go get github.com/eddycjy/mquote`，其结果是 `v0.0.0-20200220041913-e066a990ce6f`，对照着上面所提到的 `go get` 行为来看，你可能还会有一些疑惑，那就是在 `go get` 没有指定任何版本的情况下，它的版本选择规则是怎么样的，也就是为什么 `go get` 拉取的是 `v0.0.0`，它什么时候会拉取正常带版本号的 tags 呢。实际上这需要区分两种情况，如下：\n\n1. 所拉取的模块有发布 tags：\n   - 如果只有单个模块，那么就取主版本号最大的那个tag。\n   - 如果有多个模块，则推算相应的模块路径，取主版本号最大的那个tag（子模块的tag的模块路径会有前缀要求）\n2. 所拉取的模块没有发布过 tags：\n   - 默认取主分支最新一次 commit 的 commithash。\n\n#### 没有发布过 tags\n\n那么为什么会拉取的是 `v0.0.0` 呢，是因为 `github.com/eddycjy/mquote` 没有发布任何的tag，如下：\n\n![image](https://image.eddycjy.com/25989c9757d0dfba50789a1bb327edab.jpg)\n\n因此它默认取的是主分支最新一次 commit 的 commit 时间和 commithash，也就是 `20200220041913-e066a990ce6f`，属于第二种情况。\n\n#### 有发布 tags\n\n在项目有发布 tags 的情况下，还存在着多种模式，也就是只有单个模块和多个模块，我们统一以多个模块来进行展示，因为多个模块的情况下就已经包含了单个模块的使用了，如下图：\n\n![image](https://image.eddycjy.com/5e9cd4d15161f478e797c800e29cf2fd.jpg)\n\n在这个项目中，我们一共打了两个tag，分别是：v0.0.1 和 module/tour/v0.0.1。这时候你可能会奇怪，为什么要打 `module/tour/v0.0.1` 这么“奇怪”的tag，这有什么用意吗？\n\n其实是 Go modules 在同一个项目下多个模块的tag表现方式，其主要目录结构为：\n\n```\nmquote\n├── go.mod\n├── module\n│   └── tour\n│       ├── go.mod\n│       └── tour.go\n└── quote.go\n```\n\n可以看到在 `mquote` 这个项目的根目录有一个 go.mod 文件，而在 `module/tour` 目录下也有一个 go.mod 文件，其模块导入和版本信息的对应关系如下：\n\n| tag               | 模块导入路径                          | 含义                                             |\n| ----------------- | ------------------------------------- | ------------------------------------------------ |\n| v0.0.1            | github.com/eddycjy/mquote             | mquote 项目的v 0.0.1 版本                        |\n| module/tour/v0.01 | github.com/eddycjy/mquote/module/tour | mquote 项目下的子模块 module/tour 的 v0.0.1 版本 |\n\n#### 导入主模块和子模块\n\n结合上述内容，拉取主模块的话，还是照旧执行如下命令：\n\n```\n$ go get github.com/eddycjy/mquote@v0.0.1\ngo: finding github.com/eddycjy/mquote v0.0.1\ngo: downloading github.com/eddycjy/mquote v0.0.1\ngo: extracting github.com/eddycjy/mquote v0.0.1\n```\n\n如果是想拉取子模块，执行如下命令：\n\n```\n$ go get github.com/eddycjy/mquote/module/tour@v0.0.1\ngo: finding github.com/eddycjy/mquote/module v0.0.1\ngo: finding github.com/eddycjy/mquote/module/tour v0.0.1\ngo: downloading github.com/eddycjy/mquote/module/tour v0.0.1\ngo: extracting github.com/eddycjy/mquote/module/tour v0.0.1\n```\n\n我们将主模块和子模块的拉取进行对比，你会发现子模块的拉取会多出一步，它会先发现 `github.com/eddycjy/mquote/module`，再继续推算，最终拉取到 `module/tour`。\n\n## Go Modules的导入路径说明\n\n### 不同版本的导入路径\n\n在前面的模块拉取和引用中，你会发现我们的模块导入路径就是 `github.com/eddycjy/mquote` 和  `github.com/eddycjy/mquote/module/tour`，似乎并没有什么特殊的。\n\n其实不然，实际上 Go modules 在主版本号为 v0 和 v1 的情况下省略了版本号，而在主版本号为v2及以上则需要明确指定出主版本号，否则会出现冲突，其tag与模块导入路径的大致对应关系如下：\n\n| tag    | 模块导入路径                 |\n| ------ | ---------------------------- |\n| v0.0.0 | github.com/eddycjy/mquote    |\n| v1.0.0 | github.com/eddycjy/mquote    |\n| v2.0.0 | github.com/eddycjy/mquote/v2 |\n| v3.0.0 | github.com/eddycjy/mquote/v3 |\n\n简单来讲，就是主版本号为 v0 和 v1 时，不需要在模块导入路径包含主版本的信息，而在 v1 版本以后，也就是 v2 起，必须要在模块的导入路径末尾加上主版本号，引用时就需要调整为如下格式：\n\n```\nimport (\n    \"github.com/eddycjy/mquote/v2/example\"\n)\n```\n\n另外忽略主版本号 v0 和 v1 是强制性的（不是可选项），因此每个软件包只有一个明确且规范的导入路径。\n\n### 为什么忽略v0和v1的主版本号\n\n1. 导入路径中忽略 v1 版本的原因是：考虑到许多开发人员创建一旦到达 v1 版本便永不改变的软件包，这是官方所鼓励的，不认为所有这些开发人员在无意发布 v2 版时都应被迫拥有明确的 v1 版本尾缀，这将导致 v1 版本变成“噪音”且无意义。\n\n2. 导入路径中忽略了 v0 版本的原因是：根据语义化版本规范，v0的这些版本完全没有兼容性保证。需要一个显式的 v0 版本的标识对确保兼容性没有多大帮助。\n\n## Go Modules的语义化版本控制\n\n我们不断地在 Go Modules 的使用中提到版本号，其实质上被称为“语义化版本”，假设我们的版本号是 v1.2.3，如下：\n\n![image](https://image.eddycjy.com/6e556b628df36b1fd3800fb9d91a0d16.jpg)\n\n其版本格式为“主版本号.次版本号.修订号”，版本号的递增规则如下：\n\n1. 主版本号：当你做了不兼容的 API 修改。\n2. 次版本号：当你做了向下兼容的功能性新增。\n3. 修订号：当你做了向下兼容的问题修正。\n\n假设你是先行版本号或特殊情况，可以将版本信息追加到“主版本号.次版本号.修订号”的后面，作为延伸，如下：\n\n![image](https://image.eddycjy.com/b45438512cbb44015402da1a98190ac0.jpg)\n\n至此我们介绍了 Go modules 所支持的两类版本号方式，在我们发布新版本打 tag 的时候，需要注意遵循，否则不遵循语义化版本规则的版本号都是无法进行拉取的。\n\n## Go Modules的最小版本选择\n\n现在我们已经有一个模块，也有发布的 tag，但是一个模块往往依赖着许多其它许许多多的模块，并且不同的模块在依赖时很有可能会出现依赖同一个模块的不同版本，如下图（来自Russ Cox）：\n\n![image](https://image.eddycjy.com/7d509e8945fa31b7986369986c58e6f4.jpg)\n\n在上述依赖中，模块 A 依赖了模块 B 和模块 C，而模块 B 依赖了模块 D，模块 C 依赖了模块 D 和 F，模块 D 又依赖了模块 E，而且同模块的不同版本还依赖了对应模块的不同版本。那么这个时候 Go modules 怎么选择版本，选择的是哪一个版本呢？\n\n我们根据 proposal 可得知，Go modules 会把每个模块的依赖版本清单都整理出来，最终得到一个构建清单，如下图（来自Russ Cox）：\n\n![image](https://image.eddycjy.com/2bd0bed89d9300c0aac24c7bc72a6307.jpg)\n\n我们看到 rough list 和 final list，两者的区别在于重复引用的模块 D（v1.3、v1.4），其最终清单选用了模块 D 的 v1.4 版本，主要原因：\n\n1. 语义化版本的控制：因为模块 D 的 v1.3 和 v1.4 版本变更，都属于次版本号的变更，而在语义化版本的约束下，v1.4 必须是要向下兼容 v1.3 版本，因此认为不存在破坏性变更，也就是兼容的。\n\n2. 模块导入路径的规范：主版本号不同，模块的导入路径不一样，因此若出现不兼容的情况，其主版本号会改变，模块的导入路径自然也就改变了，因此不会与第一点的基础相冲突。\n\n## go.sum文件要不要提交\n\n理论上 go.mod 和 go.sum 文件都应该提交到你的 Git 仓库中去。\n\n假设我们不上传 go.sum 文件，就会造成每个人执行 Go modules 相关命令，又会生成新的一份 go.sum，也就是会重新到上游拉取，再拉取时有可能就是被篡改过的了，会有很大的安全隐患，失去了与基准版本（第一个所提交的人，所期望的版本）的校验内容，因此 go.sum文件是需要提交。\n\n## 总结\n\n至此我们介绍了 Go modules 的前世今生、基本使用和在 Go modules 模式下 `go get` 命令的行为转换，同时我们对常见的多版本导入路径、语义化版本控制以及多模块的最小版本选择规则进行了大致的介绍。\n\nGo modules 的成长和发展经历了一定的过程，如果你是刚接触的读者，直接基于 Go modules 的项目开始即可，如果既有老项目，那么是时候考虑切换过来了，Go1.14起已经准备就绪，并推荐你使用。\n\n## 我的公众号\n\n![image](https://image.eddycjy.com/25549b3f68cac5e89e92e1943d0babc2.jpeg)\n\n## 参考\n\n- [wiki/Modules](https://github.com/golang/go/wiki/Modules)\n- [wiki/vgo](https://github.com/golang/go/wiki/vgo)\n- [proposal](https://github.com/golang/go/issues/24301)\n- [干货满满的 Go Modules 和 goproxy.cn](https://book.eddycjy.com/golang/talk/goproxy-cn.html)"
  },
  {
    "path": "content/posts/go/go-standards.md",
    "content": "---\ntitle: \"上帝视角看 “Go 项目标准布局” 之争\"\ndate: 2021-09-13T23:34:23+08:00\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n前段时间 Go 语言社区有一件事情引爆了热议，那就是 `golang-standards/project-layout` 项目的 “Go 项目的标准布局” 之争。\n\n没想到，五一假期，认真一看，这个 issues 已经提出将近一个月了，仍然在热议阶段，我想，咱们需要好好的聊聊这个话题。\n\n煎鱼带你了解下的前因后果，再分享我的看法和业务真实情况。\n\n背景\n--\n\n### 问题发生地\n\n在 GitHub 上有一个项目 Spaghetti（github.com/adonovan/spaghetti），是 Go 软件包的一个依赖性分析工具。\n\n该项目的目录结构如下：\n\n![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/11b7f74aa3944e509c89bcbfc51a2c99~tplv-k3u1fbpfcp-zoom-1.image)\n\n看上去并不复杂，代码量不多，文件平铺也不超过一屏，就是一个布局比较简单的项目。\n\n有一位老哥提出了一个 PR，明确的期望该项目按照 `golang-standards/project-layout` 项目给出的 “标准” 布局来调整。：\n\n![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e0da59130ce94ee190d51491396d6919~tplv-k3u1fbpfcp-zoom-1.image)\n\n我猜测该项目可能是因为把 Go、HTML、JS、PNG 和 go.mod 文件等摆在了一起，引起了该同学的一丝丝纠结，觉得比较乱？\n\n### “标准布局“ 长什么样子\n\n在 `golang-standards/project-layout` 项目中，其自称：\n\n![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/55511ceb43cb48c59d0ce302783eec59~tplv-k3u1fbpfcp-zoom-1.image)\n\n项目的组织名也是 \"golang-standards\"，其提供了一个基本的 Go 项目布局，精简展示如下：\n\n```\nproject-layout\n├── api\n├── cmd\n├── configs\n├── docs\n├── go.mod\n├── init\n├── internal\n├── pkg\n├── scripts\n├── vendor\n├── ...\n\n```\n\n*   /cmd：项目主要的应用程序。\n    \n*   /internal：私有的应用程序代码库，这些是不希望被其他人导入的代码。\n    \n\n*   应用程序实际的代码可以放在 /internal/app 目录（如：internal/app/myapp）。\n    \n*   应用程序的共享代码放在 /internal/pkg 目录（如：internal/pkg/myprivlib）中。\n    \n\n*   /pkg：外部应用程序可以使用的库代码（如：/pkg/mypubliclib）。其他项目将会导入这些库来保证项目可以正常运行。\n    \n*   /vendor：应用程序的依赖关系，可通过执行 `go mod vendor` 执行得到。\n    \n*   /configs：配置文件模板或默认配置。\n    \n*   /init：系统初始化（systemd、upstart、sysv）和进程管理（runit、supervisord）配置。\n    \n*   /scripts:：用于执行各种构建，安装，分析等操作的脚本。\n    \n\n更具体的布局介绍，大家可以参见 project-layout 项目的 README，其基本把方方面面的目录都考虑到了（人多力量大）。\n\n由于内容过于长，因此就不一一展示了。\n\n### Russ Cox 现身原因\n\n不过很巧，该项目的作者是前 Google 员工，是 `gopl.io` 项目（5.1k stars）的作者。\n\n在仅仅过去 23 分钟后，作为 GoTeam Leader 的 Russ Cox（@rsc）就现身，并提出新的 issue 表达出了反对意见：\n\n![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f72fcd999116477193c22430eb33c22f~tplv-k3u1fbpfcp-zoom-1.image)\n\n在 `golang-standards/project-layout` 项目的 README 中有明确指出这不是官方的标准，有如下声称：\n\n> > it is a set of common historical and emerging project layout patterns in the Go ecosystem.\n\nRuss Cox 主要是对声称 \"这是一套 Go 生态系统中常见的历史和新兴的项目布局模式\" 这一说法表示了 “不准确” 的意见。\n\n例如：Go 生态系统中的绝大多数包都不会将可导入的包放在 pkg 子目录中。更广泛地说，这里描述的只是非常复杂的工程项目，而 Go 的仓库往往要简单得多。\n\n另外，不幸的是，这套项目布局在组织名字上被称作 \"golang-standards\"（Golang 标准） 提出来，实际上并非真的是官方标准，有误导的情况存在。\n\n### Russ Cox 反对原因\n\n在了解 project-layout 项目所提供的 “标准“ 项目布局和 Russ Cox 提出 issues 的背景后。\n\n我们进一步了解 Russ Cox 认为**这不对**的根本考虑。project-layout 这个项目有两个问题：\n\n*   它声称是 Go 标准（Go standards）的主办方，但实际上并非如此，因为这些标准绝非 Go 官方标准。\n    \n*   它提出的项目布局标准过于复杂，不是一个合理的标准。\n    \n\nGo 项目布局的标准是什么\n-------------\n\n提出这个 issues 后，出现了一大堆人追问 Russ Cox，到底何为 Go 项目的布局标准？\n\nRuss Cox 给出了正式回应，一个可导入的 Go repo 的最小标准布局是：\n\n*   在你的根目录下放一个 LICENSE 文件。\n    \n*   在你的根目录下放一个 go.mod 文件。\n    \n*   将 Go 代码放在 repo 中，放在根目录中，或者按照你认为合适的方式组织成一个目录树。\n    \n\n就这样了，这就是 \"标准\"，没有那么复杂。不需要像 project-layout 项目一样的布局。像是 Go 官方的 `golang.org/x` 仓库打破了 project-layout 所说的这些 \"规则 \"中的每一条。\n\nGo 提案\n-----\n\n在经历了长时间的口水战后，已经有人在 Go 官方仓库提出希望释出相关的提案（proposal）：\n\n![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ada96a8647e04b38a340995ccf764156~tplv-k3u1fbpfcp-zoom-1.image)\n\n猜测可能会有如下几种可能：\n\n*   GitHub 项目 golang-standards/project-layout 愿意更名，不再自称 ”golang-standards“，不过可能性比较低，因为已经已多人提出，但作者没什么表示。\n    \n*   Go 官方正式提供 Go 标准项目布局的说明。\n    \n*   Go 官方不做约束，仅做表态，可能输出文章。\n    \n\n后续大家继续关注该提案，就可以知道发展了，传送门：issues #45861。\n\n按照惯例，我猜测第三种可能性最大，因为很难有人可以提供所有开发者认可的标准，每个事业部、团队的喜好都可能有所不同。\n\n总结\n--\n\n实际上，任何东西自称 “XX 标准”，在名气大后，都会带来一些问题。就像本文提到的 golang-standards/project-layout 项目一样。\n\n换位思考一下，若你是某个项目的 Leader，某一天你的同事，被人拿着 “标准” 来建议修改时，说这是这个项目的 “标准”，会不会很奇妙？\n\n无独有偶，我有一个朋友，他们公司早年只有一套 DDD 标准，本想统一。结果后面每一个介入 DDD 的业务同学，都认为前人不标准，每个人都自创了一套 DDD 标准。\n\n总是会有小伙伴**想让定义绝对的 “标准”，又或是 “最佳实践”**。其实是难以定义的，最好的就是能够一个团队内形成基本共识，这里面牵扯到的不单单只有技术...\n\n你对此有什么看法呢，**欢迎在评论区留言和大家一起交流**！"
  },
  {
    "path": "content/posts/go/go-tips-defer.md",
    "content": "---\ntitle: \"Go 群友提问：学习 defer 时很懵逼，这道不会做！\"\ndate: 2021-04-05T16:10:51+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好，我是煎鱼。\n\n前几天在读者交流群里看到一位小伙伴，在向大家咨询 Go 相关的技术问题。\n疑问是：“**各位大佬，我在学习 defer 遇到闭包的时候很懵逼，谁比较明白，能指点？**”\n\n![](https://image.eddycjy.com/36e2b86b536909f265b84db24dcd80c6.jpg)\n\n## 疑问\n\n他的疑问是下面这道 Go 语言的 defer 题目，大家一起看看：\n\n```\nfunc main() {\n\tvar whatever [6]struct{}\n\tfor i := range whatever {\n\t\tdefer func() {\n\t\t\tfmt.Println(i)\n\t\t}()\n\t}\n}\n```\n请自己先想一下输出的结果答案是什么。\n\n这位小伙伴按自己的理解后，认为应当输出 xx。但最终的输出结果，可能与其思考的有所偏差，一时想不通。\n\n### 解惑\n\n这段程序的输出结果是：\n\n```\n5\n5\n5\n5\n5\n5\n```\n\n为什么全是 5，为什么不是 0, 1, 2, 3, 4, 5 这样的输出结果呢？\n\n其根本原因是**闭包**所导致的，有两点原因：\n- 在 `for` 循环结束后，局部变量 `i` 的值已经是 5 了，并且 `defer `的闭包是直接引用变量的 i。\n- 结合`defer` 关键字的特性，可得知会在 `main` 方法主体结束后再执行。\n\n结合上述，最终输出的结果是已经自增完毕的 5。\n\n### 进一步思考\n\n既然了解了为什么，我们再变形一下。再看看另外一种情况，代码如下：\n\n```\nfunc main() {\n\tvar whatever [6]struct{}\n\tfor i := range whatever {\n\t\tdefer func(i int) {\n\t\t\tfmt.Println(i)\n\t\t}(i)\n\t}\n}\n```\n\n与第一个案例不同，我们这回把变量 `i` 传了进去。那么他的输出结果是什么呢？\n\n这段程序的输出结果是：\n\n```\n5\n4\n3\n2\n1\n0\n```\n\n为什么是 5, 4, 3, 2, 1, 0 呢，为什么不是 0, 1, 2, 3, 4, 5？（难道煎鱼敲错了吗？）\n\n其根本原因在于两点：\n- 在 `for` 循环时，局部变量 `i` 已经传入进 `defer func` 中 ，属于值传递。其值在 `defer` 语句声明时的时候就已经确定下来了。\n- 结合 `defer` 关键字的特性，是按**先进后出**的顺序来执行的。\n\n结合上述，最终输出的结果是 5, 4, 3, 2, 1, 0。\n\n## 下一个疑问\n\n没过一会，这位小伙伴又有了新的感悟。抛出了新的示例问题，如下：\n\n```\nfunc f1() (r int) {\n   defer func() {\n      r++\n   }()\n   return 0\n}\n\nfunc f2() (r int) {\n   t := 5\n   defer func() {\n      t = t + 5\n   }()\n   return t\n}\n\nfunc f3() (r int) {\n   defer func(r int) {\n      r = r + 5\n   }(r)\n   return 1\n}\n```\n\n主函数：\n\n```\nfunc main() {\n\tprintln(f1())\n\tprintln(f2())\n\tprintln(f3())\n}\n```\n\n请自己先想一下输出的结果答案是什么。\n\n\n这段程序的输出结果是：\n\n```\n1\n5\n1\n```\n\n为什么是 1, 5, 1 呢，而不是 0, 10, 5，又或是其他答案？\n\n欢迎大家在**下方评论区留言讨论和分享解题的思路**，一起思考和进步。"
  },
  {
    "path": "content/posts/go/go-tips-gmp-p.md",
    "content": "---\ntitle: \"Go 面试官：GMP 模型，为什么要有 P？\"\ndate: 2021-04-05T16:15:20+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好，我是煎鱼。\n\n最近金三银四，是面试的季节。在我的 Go 读者交流群里出现了许多小伙伴在讨论自己面试过程中所遇到的一些 Go 面试题。\n\n今天的主角，是 Go 面试的万能题 GMP 模型的延伸题（疑问），那就是 ”**GMP 模型，为什么要有 P**？“\n\n进一步推敲问题的背后，其实这个面试题本质是想问：”**GMP 模型，为什么不是 G 和 M 直接绑定就完了，还要搞多一个 P 出来，那么麻烦，为的是什么，是要解决什么问题吗**？“\n\n这篇文章煎鱼就带你一同探索，GM、GMP 模型的变迁是因为什么原因。\n\n## GM 模型\n\n在 Go1.1 之前 Go 的调度模型其实就是 GM 模型，也就是没有 P。\n\n今天带大家一起回顾过去的设计。\n\n### 解密 Go1.0 源码\n\n我们了解一个东西的办法之一就是看源码，和煎鱼一起看看 Go1.0.1 的[调度器源码](https://github.com/golang/go/blob/go1.0.1/src/pkg/runtime/proc.c)的核心关键步骤：\n\n```c\nstatic void\nschedule(G *gp)\n{\n\t...\n\tschedlock();\n\tif(gp != nil) {\n\t\t...\n\t\tswitch(gp->status){\n\t\tcase Grunnable:\n\t\tcase Gdead:\n\t\t\t// Shouldn't have been running!\n\t\t\truntime·throw(\"bad gp->status in sched\");\n\t\tcase Grunning:\n\t\t\tgp->status = Grunnable;\n\t\t\tgput(gp);\n\t\t\tbreak;\n\t\t}\n\n\tgp = nextgandunlock();\n\tgp->readyonstop = 0;\n\tgp->status = Grunning;\n\tm->curg = gp;\n\tgp->m = m;\n\t...\n\truntime·gogo(&gp->sched, 0);\n}\n```\n\n- 调用 `schedlock` 方法来获取全局锁。\n- 获取全局锁成功后，将当前 Goroutine 状态从 Running（正在被调度） 状态修改为 Runnable（可以被调度）状态。\n- 调用 `gput` 方法来保存当前 Goroutine 的运行状态等信息，以便于后续的使用；\n- 调用 `nextgandunlock` 方法来寻找下一个可运行 Goroutine，并且释放全局锁给其他调度使用。\n- 获取到下一个待运行的 Goroutine 后，将其的运行状态修改为 Running。\n- 调用 `runtime·gogo` 方法，将刚刚所获取到的下一个待执行的 Goroutine 运行起来。\n\n### 思考 GM 模型\n\n通过对 Go1.0.1 的调度器源码剖析，我们可以发现一个比较有趣的点。那就是调度器本身（schedule 方法），在正常流程下，是不会返回的，也就是不会结束主流程。\n\n![G-M模型简图](https://image.eddycjy.com/89f9533b4d59bacaa8fcadc47a690059.jpg)\n\n他会不断地运行调度流程，GoroutineA 完成了，就开始寻找 GoroutineB，寻找到 B 了，就把已经完成的 A 的调度权交给 B，让 GoroutineB 开始被调度，也就是运行。\n\n当然了，也有被正在阻塞（Blocked）的 G。假设 G 正在做一些系统、网络调用，那么就会导致 G 停滞。这时候 M（系统线程）就会被会重新放内核队列中，等待新的一轮唤醒。\n\n### GM 模型的缺点\n\n这么表面的看起来，GM 模型似乎牢不可破，毫无缺陷。但为什么要改呢？\n\n在 2012 年时 Dmitry Vyukov 发表了文章《[Scalable Go Scheduler Design Doc](https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw/edit)》，目前也依然成为各大研究 Go 调度器文章的主要对象，其在文章内讲述了整体的原因和考虑，下述内容将引用该文章。\n\n当前（代指 Go1.0 的 GM 模型）的 Goroutine 调度器限制了用 Go 编写的并发程序的可扩展性，尤其是高吞吐量服务器和并行计算程序。\n\n实现有如下的问题：\n- 存在单一的全局 mutex（Sched.Lock）和集中状态管理：\n    - mutex 需要保护所有与 goroutine 相关的操作（创建、完成、重排等），导致锁竞争严重。\n- Goroutine 传递的问题：\n    - goroutine（G）交接（G.nextg）：工作者线程（M's）之间会经常交接可运行的 goroutine。\n    - 上述可能会导致延迟增加和额外的开销。每个 M 必须能够执行任何可运行的 G，特别是刚刚创建 G 的 M。\n- 每个 M 都需要做内存缓存（M.mcache）：\n    - 会导致资源消耗过大（每个 mcache 可以吸纳到 2M 的内存缓存和其他缓存），数据局部性差。\n- 频繁的线程阻塞/解阻塞：\n    - 在存在 syscalls 的情况下，线程经常被阻塞和解阻塞。这增加了很多额外的性能开销。\n\n## GMP 模型\n\n为了解决 GM 模型的以上诸多问题，在 Go1.1 时，Dmitry Vyukov 在 GM 模型的基础上，新增了一个 P（Processor）组件。并且实现了 Work Stealing 算法来解决一些新产生的问题。\n\n![](https://image.eddycjy.com/fb4c6c92c93af3bc2dfc4f13dc167cdf.png)\n\nGMP 模型，在上一篇文章《Go 群友提问：Goroutine 数量控制在多少合适，会影响 GC 和调度？》中已经讲解过了。\n\n觉得不错的小伙伴可以关注一下，这里就不再复述了。\n\n### 带来什么改变\n\n加了 P 之后会带来什么改变呢？我们再更显式的讲一下。\n\n- 每个 P 有自己的本地队列，大幅度的减轻了对全局队列的直接依赖，所带来的效果就是锁竞争的减少。而 GM 模型的性能开销大头就是锁竞争。\n\n- 每个 P 相对的平衡上，在 GMP 模型中也实现了 Work Stealing 算法，如果 P 的本地队列为空，则会从全局队列或其他 P 的本地队列中窃取可运行的 G 来运行，减少空转，提高了资源利用率。\n\n### 为什么要有 P\n\n这时候就有小伙伴会疑惑了，如果是想实现本地队列、Work Stealing 算法，那为什么不直接在 M 上加呢，M 也照样可以实现类似的组件。为什么又再加多一个 P 组件？\n\n结合 M（系统线程） 的定位来看，若这么做，有以下问题：\n\n- 一般来讲，M 的数量都会多于 P。像在 Go 中，M 的数量默认是 10000，P 的默认数量的 CPU 核数。另外由于 M 的属性，也就是如果存在系统阻塞调用，阻塞了 M，又不够用的情况下，M 会不断增加。\n\n- M 不断增加的话，如果本地队列挂载在 M 上，那就意味着本地队列也会随之增加。这显然是不合理的，因为本地队列的管理会变得复杂，且 Work Stealing 性能会大幅度下降。\n\n-  M 被系统调用阻塞后，我们是期望把他既有未执行的任务分配给其他继续运行的，而不是一阻塞就导致全部停止。\n\n因此使用 M 是不合理的，那么引入新的组件 P，把本地队列关联到 P 上，就能很好的解决这个问题。\n\n## 总结\n\n今天这篇文章结合了整个 Go 语言调度器的一些历史情况、原因分析以及解决方案说明。\n\n”GMP 模型，为什么要有 P“ 这个问题就像是一道系统设计了解，因为现在很多人为了应对面试，会硬背 GMP 模型，或者是泡面式过了一遍。而理解其中真正背后的原因，才是我们要去学的要去理解。\n\n知其然知其所以然，才可破局。"
  },
  {
    "path": "content/posts/go/go-tips-goroutineid.md",
    "content": "---\ntitle: \"Go 群友提问：进程、线程都有 ID，为什么 Goroutine 没有 ID？\"\ndate: 2021-04-05T16:14:14+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好，我是煎鱼。\n\n最近金三银四，是面试的季节。在我的 Go 读者交流群里出现了许多小伙伴在讨论自己面试过程中所遇到的一些 Go 面试题。\n\n今天的主角，是大家在既有语言基础的情况下，学 Goroutine 时会容易纠结的一点。就是 “**进程、线程都有 ID，为什么 Goroutine 没有 GoroutineID？**”。\n\n这是为什么呢，怎么做那些跨协程处理呢？\n\n## GoroutineID 是什么\n\n我们要知道，为什么大家会下意识的想去要 GoroutineID，下面引用 Go 语言圣经中的表述：\n\n>> 在大多数支持多线程的操作系统和程序语言中，当前的线程都有一个独特的身份（ID），并且这个身份信息可以以一个普通值的形式被很容易地获取到，典型的可以是一个 integer 或者指针值。这种情况下我们做一个抽象化的 thread-local storage（线程本地存储，多线程编程中不希望其它线程访问的内容）就很容易，只需要以线程的 ID 作为 key 的一个 map 就可以解决问题，每一个线程以其 ID 就能从中获取到值，且和其它线程互不冲突。\n\n也就在常规的进程、线程中都有其 ID 的概念，我们可以在程序中通过 ID 来获取其他进程、线程中的数据，甚至是传输数据。就像一把钥匙一样，有了他干啥都可以。\n\nGoroutineID 的概念也是类似的，也就是协程的 ID。我们下意识的就期望通过协程 ID 来进行跨协程的操作。\n\n但，在 Go 语言中 GoroutineID 并没有显式获取的办法，这就要打个大大的疑惑了。\n\n## 为什么没有 GoroutineID\n\n为什么在 Go 语言中没有 GoroutineID 呢，是从一开始就没有的，还是，这样子设计的原因是什么呢？\n\n其实 Go 语言在以前是有暴露方法去获取 GoroutineID 的，但在 Go1.4 后就把该方法给隐藏起来了，不建议大家使用。\n\n也就是明面上没有 GoroutineID，是一个有意而为之的行为。原因是：**根据以往的经验，认为 thread-local storage 存在被滥用的可能性，且带来许多不必要的复杂度**。\n\n简单来讲，Andrew Gerrand 的回答是 ”**thread-local storage 的成本远远超过了它们的收益。它们只是不适合 Go 语言**。”\n\n### 潜在的问题\n\n- 当 Goroutine 消失时：\n    - 它的 Goroutine 本地存储将不会被 GC 化。 (你可以得到 goid 的当前的 Goroutine，但你不能得到所有运行的 Goroutine 的列表)\n\n- 如果处理程序自己产生了新的 Goroutine 怎么办？ \n    - 新的 Goroutine 失去了对既有的 Goroutine 本地存储。虽然你可以保证自己的代码不会产生其他的 Goroutine。\n    - 一般来说，你不能确保标准库或任何第三方代码不会这样做。\n- Go 应用程序的复杂度和心智负担等上升。\n\n### 滥用的场景\n\n有一个对外提供 HTTP 服务的 Go 应用，也就是 Web Server。Go HTTP Server 都是采取每次请求新起一个协程的方式。\n\n\n假设可以通过 GoroutineID 进行跨协程操纵，那么就有可能出现我的 Goroutine，不一定是由 “我” 自己决定的。可能其他正在处理的 GoroutineB 悄悄摸摸的改了我这个 GoroutineA 的行为。\n\n这就有可能导致一个灾难问题，就是出问题时，你不知道是谁动了你的奶酪。查起问题来简直就是一个灾难。\n\n若是自己维护的模块清楚还起码知道这事，假设你的前同事刚好离职了，你又在熟悉代码，一出问题。这锅那是死死的扣在了你的头上了。\n\n## 如何获取 GoroutineID\n\n刚刚我们提到是在明面上把 GoroutineID 给隐藏了，那暗面呢，是不是有其他办法可以获取到？\n\n答案是：可以的。\n\n通过骇客代码的方式可以获取到。在 Go 语言的标准库 [http/2 的 gotrack ](https://github.com/golang/net/blob/master/http2/gotrack.go) 中，就有提供如下获取方法：\n\n```golang\nfunc main() {\n    go func() {\n        fmt.Println(\"脑子进煎鱼了的 GoroutineID：\", curGoroutineID())\n    }()\n\n    time.Sleep(time.Second)\n}\n\nfunc curGoroutineID() uint64 {\n    bp := littleBuf.Get().(*[]byte)\n    defer littleBuf.Put(bp)\n    b := *bp\n    b = b[:runtime.Stack(b, false)]\n    // Parse the 4707 out of \"goroutine 4707 [\"\n    b = bytes.TrimPrefix(b, goroutineSpace)\n    i := bytes.IndexByte(b, ' ')\n    if i < 0 {\n        panic(fmt.Sprintf(\"No space found in %q\", b))\n    }\n    b = b[:i]\n    n, err := parseUintBytes(b, 10, 64)\n    if err != nil {\n        panic(fmt.Sprintf(\"Failed to parse goroutine ID out of %q: %v\", b, err))\n    }\n    return n\n}\n\nvar littleBuf = sync.Pool{\n    New: func() interface{} {\n        buf := make([]byte, 64)\n        return &buf\n    },\n}\n\nvar goroutineSpace = []byte(\"goroutine \")\n```\n\n输出结果为：\n\n```\n脑子进煎鱼了的 GoroutineID： 18\n```\n\n结合 `curGoroutineID` 方法来看，可以通过对 Go 运行时的分析，也就是 `runtime.Stack` 从而得到 GoroutineID。\n\n其作用，更多的是对进行跟踪和调试作用居多。因为官方并没有根据 GoroutineID 提供一系列跨协程操纵的方法。\n\n也有如下开源库可以用于获取 GoroutineID（不过均多年未维护了）：\n\n- [davecheney/junk](github.com/davecheney/junk)\n- [jtolio/gls](https://github.com/jtolio/gls)\n- [tylerstillwater/gls](https://github.com/tylerstillwater/gls)\n\nGo 团队的 Dave Cheney 对其所开源的 GoroutineID 库，评价：“If you use this package, you will go straight to hell.”：\n\n![davecheney/junk](https://image.eddycjy.com/5f67767b9f8d0be030294d8a2ffb8b83.jpg)\n\n也就是 “如果你使用这个包，你会直接下地狱。“，非常猛了，深深地劝退大家使用。\n\n## 日常在哪里常见\n\n如果大家经常做救火队长，去排查 Go 工程中的问题，例如：错误堆栈信息、PProf 性能分析等调试信息。\n\n因此经常看到 GoroutineID，也就是 “`goroutine ####` […]”。\n\n我们所看到的 `####` 就是真实的 GoroutineID，剩余的信息就是一些堆栈跟踪和错误描述了。\n\n## 应该使用 GoroutineID 吗？\n\n从结果来看，肯定是不推荐使用 GoroutineID 了。毕竟没有什么特别的好处，Go 团队也是反对的。\n\n所以一般都会直接回答 ”无法获取 GoroutineID“，应当跟从语言设计理念，使用 [Share Memory By Communicating](https://blog.golang.org/codelab-share) 来实现跨协程的操纵会更合理。\n\n## 总结\n\n今天这篇文章我们根据 GoroutineID 的历史，作用，原因，骇客方法进行了逐一梳理，摸索了下里面究竟为何物。\n\n进程、线程、协程的对比是一个面试中常被拿出来问的话题，而 GoroutineID 就是其中一点，这涉及到整个全局上的设计考虑。\n\n你又是否遇到过 GoroutineID 使用和疑问的场景呢，欢迎大家一起留言讨论。"
  },
  {
    "path": "content/posts/go/go-tips-goroutineloop.md",
    "content": "---\ntitle: \"Go 面试官：单核 CPU，开两个 Goroutine，其中一个死循环，会怎么样？\"\ndate: 2021-04-05T16:17:23+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好，我是煎鱼。\n\n最近金三银四，是面试的季节。在我的 Go 读者交流群里出现了许多小伙伴在讨论自己面试过程中所遇到的一些 Go 面试题。\n\n今天的男主角，是与 Go 工程师有调度相关的知识，那就是 “**单核 CPU，开两个 Goroutine，其中一个死循环，会怎么样？**”\n\n**请在此处默念自己心目中的答案**，再往和煎鱼一起研讨一波 Go 的技术哲学。\n\n## 问题定义\n\n针对这个问题，我们需要把问题剖开来看看，其具有以下几个元素：\n- 运行 Go 程序的计算机只有一个单核 CPU。\n- 两个 Goroutine 在运行。\n- 一个 Goroutine 死循环。\n\n根据这道题的题意，可大致理解其想要问的是 Go 调度相关的一些知识理解。\n\n### 单核 CPU\n\n第一个要点，就是要明确 “计算机只有一个单核 CPU” 这一个变量定义，对 Go 程序会产生什么影响，否则很难继续展开。\n\n既然明确涉及 Goroutine，这里就会考察到你对 Go 的调度模型 GMP 的基本理解了。\n\n从单核 CPU 来看，最大的影响就是 GMP 模型中的 P，因为 P 的数量默认是与 CPU 核数（GOMAXPROCS）保持一致的。\n\n- G：Goroutine，实际上我们每次调用 `go func` 就是生成了一个 G。\n- P：Processor，处理器，一般 P 的数量就是处理器的核数，可以通过 `GOMAXPROCS` 进行修改。\n- M：Machine，系统线程。\n\n这三者交互实际来源于 Go 的 M: N 调度模型。也就是 M 必须与 P 进行绑定，然后不断地在 M 上循环寻找可运行的 G 来执行相应的任务。\n\n### Goroutine 受限\n\n第二个要点，就是 Goroutine 的数量和运行模式都是受限的。有两个 Goroutine，一个 Goroutine 在死循环，另外一个在正常运行。\n\n这可以理解为 Main Goroutine + 起了一个新 Goroutine 跑着死循环，因为本身 main 函数就是一个主协程在运行着，没毛病。\n\n需要注意的是，Goroutine 里跑着死循环，也就是时时刻刻在运行着 “业务逻辑”。这块需要与单核 CPU 关联起来，**考虑是否会一直阻塞住，把整个 Go 进程运行给 hang 住了**？\n\n注： 但若是在现场面试，可以先枚举出这种场景，诠释清楚后。再补充提问面试官，是否这类场景？\n\n### Go 版本的问题\n\n第三个要点，是一个隐性的拓展点。如果你是一个老 Go 粉，经常关注 Go 版本的更新情况（至少大版本），则应该会知道 Go 的调度是会变动的（会在后面的小节讲解）。\n\n因此**本文这个问题，在不同的 Go 语言版本中，结果可能会是不一样**的。但是面试官并没有指出，这里就需要考虑到：\n1. 面试官故意不指出，等着你指出。\n2. 面试官没留意到这块，没想那么多。\n3. 面试官自己都不知道这块的 “新” 知识，他的知识可能还是老的。\n\n如果你注意到了，是一个小亮点，说明你在这块有一定的知识积累。\n\n## 实战演练\n\n在刚刚过去的 3s 中，你已经把上面的考量都在大脑中过了一遍。接下来我们正式进入实战演练，构造一个例子：\n\n```golang\n// Main Goroutine \nfunc main() {\n    // 模拟单核 CPU\n    runtime.GOMAXPROCS(1)\n    \n    // 模拟 Goroutine 死循环\n    go func() {\n        for {\n        }\n    }()\n\n    time.Sleep(time.Millisecond)\n    fmt.Println(\"脑子进煎鱼了\")\n}\n```\n\n在上面的例子中，我们通过以下方式达到了面试题所需的目的：\n- 设置 `runtime.GOMAXPROCS` 方法模拟了单核 CPU 下只有一个 P 的场景。\n- 运行一个 Goroutine，内部跑一个 for 死循环，达到阻塞运行的目的。\n- 运行一个 Goroutine，主函数（main）本身就是一个 Main Goroutine。\n\n思考一下：**这段程序是否会输出 ”脑子进煎鱼了“ 呢，为什么**？\n\n答案是：\n- 在 Go1.14 前，不会输出任何结果。\n- 在 Go1.14 及之后，能够正常输出结果。\n\n## 为什么\n\n这是怎么回事呢，这两种情况分别对应了什么原因和标准，Go 版本的变更有带来了什么影响？\n\n### 不会输出任何结果\n\n显然，这段程序是有一个 Goroutine 是正在执行死循环，也就是说他肯定无法被抢占。\n\n这段程序中更没有涉及主动放弃执行权的调用（runtime.Gosched），又或是其他调用（可能会导致执行权转移）的行为。\n因此这个 Goroutine 是没机会溜号的，只能一直打工...\n\n那为什么主协程（Main Goroutine）会无法运行呢，其实原因是会优先调用休眠，但由于单核 CPU，其只有唯一的 P。唯一的 P 又一直在打工不愿意下班（执行 for 死循环，被迫无限加班）。\n\n因此主协程永远没有机会呗调度，所以这个 Go 程序自然也就一直阻塞在了执行死循环的 Goroutine 中，永远无法下班（执行完毕，退出程序）。\n\n### 正常输出结果\n\n那为什么 Go1.14 及以后的版本，又能正常输出了呢？\n\n主要还是**在 Go1.14 实现了基于信号的抢占式调度**，以此来解决上述一些仍然无法被抢占解决的场景。\n\n主要原理是Go 程序在启动时，会在 `runtime.sighandler` 方法注册并且绑定 `SIGURG` 信号：\n\n```golang\nfunc mstartm0() {\n\t...\n\tinitsig(false)\n}\n\nfunc initsig(preinit bool) {\n\tfor i := uint32(0); i < _NSIG; i++ {\n\t\t...\n\t\tsetsig(i, funcPC(sighandler))\n\t}\n}\n```\n\n绑定相应的 `runtime.doSigPreempt` 抢占方法：\n\n```golang\nfunc sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) {\n    ...\n    if sig == sigPreempt && debug.asyncpreemptoff == 0 {\n        // 执行抢占\n        doSigPreempt(gp, c)\n    }\n}\n```\n\n同时在调度的 `runtime.sysmon` 方法会调用 `retake` 方法处理一下两种场景：\n- 抢占阻塞在系统调用上的 P。\n- 抢占运行时间过长的 G。\n\n该方法会检测符合场景的 P，当满足上述两个场景之一。就会发送信号给 M， M 收到信号后将会休眠正在阻塞的 Goroutine，调用绑定的信号方法，并进行重新调度。以此来解决这个问题。\n\n注：在 Go 语言中，sysmon 会用于检测抢占。sysmon 是 Go 的 Runtime 的系统检测器，sysmon 可进行 forcegc、netpoll、retake 等一系列骚操作（via @xiaorui）。\n\n## 总结\n\n在这篇文章中，我们针对 ”单核 CPU，开两个 Goroutine，其中一个死循环，会怎么样？“ 这个问题进行了展开剖析。\n\n针对不同 Go 语言版本，不同程序逻辑的表现形式都不同，但背后的基本原理都是与 Go 调度模型和抢占有关。\n\n你是否有在这一块遇到问题呢，欢迎大家在留言区评论和交流。"
  },
  {
    "path": "content/posts/go/go-tips-goroutinenums.md",
    "content": "---\ntitle: \"Go 群友提问：Goroutine 数量控制在多少合适，会影响 GC 和调度？\"\ndate: 2021-04-05T16:08:18+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好，我是煎鱼。\n\n前几天在读者交流群里看到一位小伙伴，发出了一个致命提问，那就是：“**单机的 goroutine 数量控制在多少比较合适？**”。\n\n也许你和群内小伙伴第一反应一样，会答复 “控制多少，我觉得没有定论”。\n\n紧接着延伸出了更进一步的疑惑：“**goroutine 太多了会影响 gc 和调度吧，主要是怎么预算这个数是合理的呢？**”\n\n这是本文要进行探讨的主体，因此本文的结构会是先探索基础知识，再一步步揭开，深入理解这个问题。\n\n## Goroutine 是什么\n\nGo 语言作为一个新生编程语言，其令人喜爱的特性之一就是 goroutine。Goroutine 是一个由 Go 运行时管理的轻量级线程，一般称其为 “协程”。\n\n```\ngo f(x, y, z)\n```\n\n操作系统本身是无法明确感知到 Goroutine 的存在的，Goroutine 的操作和切换归属于 “用户态” 中。\n\nGoroutine 由特定的调度模式来控制，以 “多路复用” 的形式运行在操作系统为 Go 程序分配的几个系统线程上。\n\n同时创建 Goroutine 的开销很小，初始只需要 2-4k 的栈空间。Goroutine 本身会根据实际使用情况进行自伸缩，非常轻量。\n\n```\nfunc say(s string) {\n\tfor i := 0; i < 9999999; i++ {\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\tfmt.Println(s)\n\t}\n}\n\nfunc main() {\n\tgo say(\"煎鱼\")\n\tsay(\"你好\")\n}\n\n```\n\n人称可以开几百几千万个的协程小霸王，是 Go 语言的得意之作之一。\n\n## 调度是什么\n\n既然有了用户态的代表 Goroutine，操作系统又看不到他。必然需要有某个东西去管理他，才能更好的运作起来。\n\n这指的就是 Go 语言中的调度，最常见、面试最爱问的 GMP 模型。因此接下来将会给大家介绍一下 Go 调度的基础知识和流程。\n\n下述内容摘自煎鱼和 p 神写的《Go 语言编程之旅》中的章节内容。\n\n### 调度基础知识\n\nGo scheduler 的主要功能是针对在处理器上运行的 OS 线程分发可运行的 Goroutine，而我们一提到调度器，就离不开三个经常被提到的缩写，分别是：\n\n- G：Goroutine，实际上我们每次调用 `go func` 就是生成了一个 G。\n- P：Processor，处理器，一般 P 的数量就是处理器的核数，可以通过 `GOMAXPROCS` 进行修改。\n- M：Machine，系统线程。\n\n这三者交互实际来源于 Go 的 M: N 调度模型。也就是 M 必须与 P 进行绑定，然后不断地在 M 上循环寻找可运行的 G 来执行相应的任务。\n\n### 调度流程\n\n我们以 GMP 模型的工作流程图进行简单分析，官方图如下:\n\n![](https://image.eddycjy.com/fb4c6c92c93af3bc2dfc4f13dc167cdf.png)\n\n1. 当我们执行 `go func()` 时，实际上就是创建一个全新的 Goroutine，我们称它为 G。\n2. 新创建的 G 会被放入 P 的本地队列（Local Queue）或全局队列（Global Queue）中，准备下一步的动作。需要注意的一点，这里的 P 指的是创建 G 的 P。\n3. 唤醒或创建 M 以便执行 G。\n4. 不断地进行事件循环\n5. 寻找在可用状态下的 G 进行执行任务\n6. 清除后，重新进入事件循环\n\n在描述中有提到全局和本地这两类队列，其实在功能上来讲都是用于存放正在等待运行的 G，但是不同点在于，本地队列有数量限制，不允许超过 256 个。\n\n并且在新建 G 时，会优先选择 P 的本地队列，如果本地队列满了，则将 P 的本地队列的一半的 G 移动到全局队列。\n\n这可以理解为调度资源的共享和再平衡。\n\n### 窃取行为\n\n我们可以看到图上有 steal 行为，这是用来做什么的呢，我们都知道当你创建新的 G 或者 G 变成可运行状态时，它会被推送加入到当前 P 的本地队列中。\n\n其实当 P 执行 G 完毕后，它也会 “干活”，它会将其从本地队列中弹出 G，同时会检查当前本地队列是否为空，如果为空会随机的从其他 P 的本地队列中尝试窃取一半可运行的 G 到自己的名下。\n\n官方图如下：\n\n![](https://image.eddycjy.com/6fde3925fd4172a1c89938018bd2d7b5.png)\n\n在这个例子中，P2 在本地队列中找不到可以运行的 G，它会执行 `work-stealing` 调度算法，随机选择其它的处理器 P1，并从 P1 的本地队列中窃取了三个 G 到它自己的本地队列中去。\n\n至此，P1、P2 都拥有了可运行的 G，P1 多余的 G 也不会被浪费，调度资源将会更加平均的在多个处理器中流转。\n\n## 有没有什么限制\n\n在前面的内容中，我们针对 Go 的调度模型和 Goroutine 做了一个基本介绍和分享。\n\n接下来我们回到主题，思考 “goroutine 太多了，会不会有什么影响”。\n\n在了解 GMP 的基础知识后，我们要知道**在协程的运行过程中，真正干活的 GPM 又分别被什么约束**？\n\n煎鱼带大家分别从 GMP 来逐步分析。\n\n### M 的限制\n\n第一，要知道**在协程的执行中，真正干活的是 GPM 中的哪一个**？\n\n那势必是 M（系统线程） 了，因为 G 是用户态上的东西，最终执行都是得映射，对应到 M 这一个系统线程上去运行。\n\n那么 M 有没有限制呢？\n\n答案是：有的。在 Go 语言中，**M 的默认数量限制是 10000**，如果超出则会报错：\n\n```\nGO: runtime: program exceeds 10000-thread limit\n```\n\n通常只有在 Goroutine 出现阻塞操作的情况下，才会遇到这种情况。这可能也预示着你的程序有问题。\n\n若确切是需要那么多，还可以通过 `debug.SetMaxThreads` 方法进行设置。\n\n### G 的限制\n\n第二，那 G 呢，Goroutine 的创建数量是否有限制？\n\n答案是：没有。但**理论上会受内存的影响**，假设一个 Goroutine 创建需要 4k（via @GoWKH）：\n\n- 4k * 80,000 = 320,000k ≈ 0.3G内存\n- 4k * 1,000,000 = 4,000,000k ≈ 4G内存\n\n以此就可以相对计算出来一台单机在通俗情况下，所能够创建 Goroutine 的大概数量级别。\n\n注：Goroutine 创建所需申请的 2-4k 是需要连续的内存块。\n\n### P 的限制\n\n第三，那 P 呢，P 的数量是否有限制，受什么影响？\n\n答案是：有限制。**P 的数量受环境变量 `GOMAXPROCS` 的直接影响**。\n\n环境变量 `GOMAXPROCS` 又是什么？\n在 Go 语言中，通过设置 `GOMAXPROCS`，用户可以调整调度中中 P（Processor）的数量。\n\n另一个重点在于，与 P 相关联的的 M（系统线程），是需要绑定 P 才能进行具体的任务执行的，因此 P 的多少会影响到 Go 程序的运行表现。\n\nP 的数量基本是受本机的核数影响，没必要太过度纠结他。\n\n那 P 的数量是否会影响 Goroutine 的数量创建呢？\n\n答案是：不影响。且 Goroutine 多了少了，P 也该干嘛干嘛，不会带来灾难性问题。\n\n## 何为之合理\n\n在介绍完 GMP 各自的限制后，我们回到一个重点，就是 “Goroutine 数量怎么预算，才叫合理？”。\n\n“合理” 这个词，是需要看具体场景来定义的，可结合上述对 GPM 的学习和了解。\n得出：\n- M：有限制，默认数量限制是 10000，可调整。\n- G：没限制，但受内存影响。\n- P：受本机的核数影响，可大可小，不影响 G 的数量创建。\n\nGoroutine 数量在 MG 的可控限额以下，多个把个、几十个，少几个其实没有什么影响，就可以称其为 “合理”。\n\n## 真实情况\n\n在真实的应用场景中，没法如此简单的定义。如果你 Goroutine：\n- 在频繁请求 HTTP，MySQL，打开文件等，那假设短时间内有几十万个协程在跑，那肯定就不大合理了（可能会导致  too many files open）。\n- 常见的 Goroutine 泄露所导致的 CPU、Memory 上涨等，还是得看你的 Goroutine 里具体在跑什么东西。\n\n还是得看 Goroutine 里面跑的是什么东西。\n\n## 总结\n\n在这篇文章中，分别介绍了 Goroutine、GMP、调度模型的基本知识，针对如下问题进行了展开：\n- 单机的 goroutine 数量控制在多少比较合适？\n- goroutine 太多了会影响 gc 和调度吧，主要是怎么预算这个数是合理的呢？\n\n单机的 goroutine 数量只要控制在限额以下的，都可以认为是 “合理”。\n\n真实场景得看具体里面跑的是什么，跑的如果是 “资源怪兽”，只运行几个 Goroutine 都可能可以跑死。因此想定义 “预算”，就得看跑的什么了。\n\n\n\n\n"
  },
  {
    "path": "content/posts/go/go-tips-interface.md",
    "content": "---\ntitle: \"Go 面试官：Go interface 的一个 “坑” 及原理分析\"\ndate: 2021-04-05T16:12:59+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好，我是煎鱼。\n\n前几天在读者交流群里看到一位小伙伴，针对 interface 的使用有了比较大的疑惑。\n\n无独有偶，我也在网上看到有小伙伴在 Go 面试的时候被问到了：\n\n\n![来自网上博客的截图](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/36d7ecb0da9e4b32a493dedce6ebc691~tplv-k3u1fbpfcp-watermark.image)\n\n今天特意分享出来让大家避开这个坑。\n\n## 例子一\n\n第一个例子，如下代码：\n\n\n```golang\nfunc main() {\n    var v interface{}\n    v = (*int)(nil)\n    fmt.Println(v == nil)\n}\n```\n\n你觉得输出结果是什么呢？\n\n答案是：\n\n```\nfalse\n```\n为什么不是 `true`。明明都已经强行置为 `nil` 了。是不是 Go 编译器有问题？\n\n## 例子二\n\n第二个例子，如下代码：\n\n```golang\nfunc main() {\n    var data *byte\n    var in interface{}\n\n    fmt.Println(data, data == nil)\n    fmt.Println(in, in == nil)\n\n    in = data\n    fmt.Println(in, in == nil)\n}\n```\n\n你觉得输出结果是什么呢？\n\n答案是：\n\n```\n<nil> true\n<nil> true\n<nil> false\n```\n\n这可就更奇怪了，为什么刚刚声明出来的 `data` 和 `in` 变量，确实是输出结果是 `nil`，判断结果也是 `true`。\n\n怎么把变量 `data` 一赋予给变量 `in`，世界就变了？输出结果依然是 `nil`，但判定却变成了 `false`。\n\n和上面的第一个例子结果类似，真是神奇。\n\n## 原因\n\ninterface 判断与想象中不一样的根本原因是，interface 并不是一个指针类型，虽然他看起来很像，以至于误导了不少人。\n\n我们钻下去 interface，interface 共有两类数据结构：\n\n![](https://imgkr2.cn-bj.ufileos.com/560dcab5-e436-4a2c-bfee-6eba6faee1d2.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=HF1wka8F9MTc%252FVCTrMjhASpeeE4%253D&Expires=1615900476)\n\n- `runtime.eface` 结构体：表示不包含任何方法的空接口，也称为 empty interface。\n- `runtime.iface` 结构体：表示包含方法的接口。\n\n看看这两者相应的底层数据结构：\n\n```golang\ntype eface struct {\n    _type *_type\n    data  unsafe.Pointer\n}\n\ntype iface struct {\n    tab  *itab\n    data unsafe.Pointer\n}\n```\n\n你会发现 interface 不是单纯的值，而是**分为类型和值**。\n\n所以传统认知的此 nil 并非彼 nil，**必须得类型和值同时都为 nil 的情况下，interface 的 nil 判断才会为 true**。\n\n## 解决办法\n\n与其说是解决方法，不如说是委婉的破局之道。在不改变类型的情况下，方法之一是利用反射（reflect），如下代码：\n\n```golang\nfunc main() {\n    var data *byte\n    var in interface{}\n\n    in = data\n    fmt.Println(IsNil(in))\n}\n\nfunc IsNil(i interface{}) bool {\n    vi := reflect.ValueOf(i)\n    if vi.Kind() == reflect.Ptr {\n        return vi.IsNil()\n    }\n    return false\n}\n```\n\n利用反射来做 nil 的值判断，在反射中会有针对 interface 类型的特殊处理，最终输出结果是：true，达到效果。\n\n其他方法的话，就是改变原有的程序逻辑，例如：\n- 对值进行 nil 判断，再返回给 interface 设置。\n- 返回具体的值类型，而不是返回 interface。\n\n## 总结\n\nGo interface 是 Go 语言中最常用的类型之一，大家用惯了 `if err != nil` 就很容易顺手就踩进去了。\n\n建议大家要多留个心眼，如果对 interface 想要有更进一步的了解，可以看看我的这篇深入解析的文章：《一文吃透 Go 语言解密之接口 interface》。\n"
  },
  {
    "path": "content/posts/go/go-tips-lenstr.md",
    "content": "---\ntitle: \"问个 Go 问题，字符串 len 为 0 和 字符串为空 ，有啥区别？\"\ndate: 2021-04-05T16:09:14+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好，我是煎鱼。\n\n前几天在微信群看到几位大佬在讨论一个问题： ”**字符串 len == 0 和 字符串 == \"\" ，有啥区别**？“\n\n这是一个比较小的细节点，同时也勾起了我的好奇心，因此今天这篇文章就和大家一起研究一下他们两者有没有区别，谁的性能更好一些？\n\n## 测试方法\n\n在测试的方法中，我们分别声明了 `Test1` 和 `Test2` 方法：\n\n```\nfunc Test1() bool {\n\tvar v string\n\tif v == \"\" {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc Test2() bool {\n\tvar v string\n\tif len(v) == 0 {\n\t\treturn true\n\t}\n\treturn false\n}\n```\n\n在方法内部仅做了简单的变量类型声明，分别以 字符串 == \"\" 和 字符串 len == 0 为判断依据。\n\n## 测试用例\n\n编写两个方法的 Benchmark，用于后续的性能测试：\n\n```\nfunc BenchmarkTest1(b *testing.B) {\n\tfor i := 0; i < b.N; i++ {\n\t\tTest1()\n\t}\n}\n\nfunc BenchmarkTest2(b *testing.B) {\n\tfor i := 0; i < b.N; i++ {\n\t\tTest2()\n\t}\n}\n```\n\n## 结果分析\n\n```\n$ go test --bench=. -benchmem\ngoos: darwin\ngoarch: amd64\nBenchmarkTest1-4   \t1000000000\t         0.305 ns/op\t       0 B/op\t       0 allocs/op\nBenchmarkTest2-4   \t1000000000\t         0.305 ns/op\t       0 B/op\t       0 allocs/op\nPASS\nok  \t_/Users/eddycjy/go-application/awesomeProject/tests\t0.688s\n```\n\n从多次测试的结果来看，两者比较：\n- 性能几乎没有区别，甚至可以出现一模一样的情况。\n- 均不涉及内存申请和操作，均为 0/op。说明变量并不是声明了，就有初始化动作的，这块 Go 编译器有做优化。\n\n结果上居然是一样的。根据曹大的提示，我们可以进一步看一下两者的汇编代码，看看具体区别在哪里：\n\n```\n$ go tool compile -S main.go\n\"\".main STEXT nosplit size=1 args=0x0 locals=0x0\n\t0x0000 00000 (main.go:3)\tTEXT\t\"\".main(SB), NOSPLIT|ABIInternal, $0-0\n\t0x0000 00000 (main.go:3)\tFUNCDATA\t$0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)\n\t0x0000 00000 (main.go:3)\tFUNCDATA\t$1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)\n\t0x0000 00000 (main.go:5)\tRET\n\t0x0000 c3                                               .\ngo.cuinfo.packagename. SDWARFINFO dupok size=0\n\t0x0000 6d 61 69 6e                                      main\n\"\"..inittask SNOPTRDATA size=24\n\t0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................\n\t0x0010 00 00 00 00 00 00 00 00                          ........\ngclocals·33cdeccccebe80329f1fdbee7f5874cb SRODATA dupok size=8\n\t0x0000 01 00 00 00 00 00 00 00   \n```\n\n无论是 `len(v) == 0`，又或是 `v == \"\"` 的判断，其编译出来的汇编代码都是完全一致的。可以明确 Go 编译器在这块做了明确的优化，大概率是直接比对了。\n\n大家有没有其他的看法和拓展呢，欢迎一起来学习和交流。\n\n\n"
  },
  {
    "path": "content/posts/go/go-tips-sturct.md",
    "content": "---\ntitle: \"Go 面试官：Go 结构体是否可以比较，为什么？\"\ndate: 2021-04-05T16:16:00+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好，我是煎鱼。\n\n最近金三银四，是面试的季节。在我的 Go 读者交流群里出现了许多小伙伴在讨论自己面试过程中所遇到的一些 Go 面试题。\n\n今天的男主角，是 Go 工程师的必修技能，也是极容易踩坑的地方，就是 “**Go 面试题：Go 结构体（struct）是否可以比较？**”\n\n如果可以比较，是为什么？如果不可以比较，又是为什么？\n\n**请在此处默念自己心目中的答案**，再往和煎鱼一起研讨一波 Go 的技术哲学。\n\n## 结构体是什么\n\n在 Go 语言中有个基本类型，开发者们称之为结构体（struct）。是 Go 语言中非常常用的，基本定义：\n\n```golang\ntype struct_variable_type struct {\n    member definition\n    member definition\n    ...\n    member definition\n}\n```\n\n简单示例：\n\n```golang\npackage main\n\nimport \"fmt\"\n\ntype Vertex struct {\n    Name1 string\n    Name2 string\n}\n\nfunc main() {\n    v := Vertex{\"脑子进了\", \"煎鱼\"}\n    v.Name2 = \"蒸鱼\"\n    fmt.Println(v.Name2)\n}\n```\n\n输出结果：\n\n```\n蒸鱼\n```\n\n这部分属于基础知识，因此不再过多解释。如果看不懂，建议重学 Go 语言语法基础。\n\n## 比较两下\n\n### 例子一\n\n接下来正式开始研讨 Go 结构体比较的问题，第一个例子如下：\n\n```golang\ntype Value struct {\n    Name   string\n    Gender string\n}\n\nfunc main() {\n    v1 := Value{Name: \"煎鱼\", Gender: \"男\"}\n    v2 := Value{Name: \"煎鱼\", Gender: \"男\"}\n    if v1 == v2 {\n        fmt.Println(\"脑子进煎鱼了\")\n        return\n    }\n\n    fmt.Println(\"脑子没进煎鱼\")\n}\n```\n\n我们声明了两个变量，分别是 v1 和 v2。其都是 `Value` 结构体的实例化，是同一个结构体的两个实例。\n\n他们的比较结果是什么呢，是输出 ”脑子进煎鱼了“，还是 ”脑子没进煎鱼“？\n\n输出结果：\n\n```\n脑子进煎鱼了\n```\n\n最终输出结果是 ”脑子进煎鱼了“，初步的结论是可以结构体间比较的。皆大欢喜，那这篇文章是不是就要结束了？\n\n当然不是...很多人都会踩到这个 Go 语言的坑，**真实情况是结构体是可比较，也不可比较的**，不要误入歧途了，这是一个非常 \"有趣\" 的现象。\n\n### 例子二\n\n接下来继续改造上面的例子，我们在原本的结构体中增加了指针类型的引用。\n\n第二个例子如下：\n\n```golang\ntype Value struct {\n    Name   string\n    Gender *string\n}\n\nfunc main() {\n    v1 := Value{Name: \"煎鱼\", Gender: new(string)}\n    v2 := Value{Name: \"煎鱼\", Gender: new(string)}\n    if v1 == v2 {\n        fmt.Println(\"脑子进煎鱼了\")\n        return\n    }\n\n    fmt.Println(\"脑子没进煎鱼\")\n}\n```\n\n这段程序输出结果是什么呢，我们猜测一下，变量依然是同一结构体的两个实例，值的赋值方式和内容都是一样的，是否应当输出 “脑子进煎鱼了”？\n\n答案是：脑子没进煎鱼。\n\n### 例子三\n\n我们继续不信邪，试试另外的基本类型，看看结果是不是还是相等的。\n\n第三个例子如下：\n\n```golang\ntype Value struct {\n    Name   string\n    GoodAt []string\n}\n\nfunc main() {\n    v1 := Value{Name: \"煎鱼\", GoodAt: []string{\"炸\", \"煎\", \"蒸\"}}\n    v2 := Value{Name: \"煎鱼\", GoodAt: []string{\"炸\", \"煎\", \"蒸\"}}\n    if v1 == v2 {\n        fmt.Println(\"脑子进煎鱼了\")\n        return\n    }\n\n    fmt.Println(\"脑子没进煎鱼\")\n}\n```\n\n这段程序输出结果是什么呢？\n\n答案是：\n\n```\n# command-line-arguments\n./main.go:15:8: invalid operation: v1 == v2 (struct containing []string cannot be compared)\n```\n程序运行就直接报错，IDE 也提示错误，一只煎鱼都没能输出出来。\n\n### 例子四\n\n那不同结构体，相同的值内容呢，能否进行比较？\n\n第四个例子：\n\n```golang\ntype Value1 struct {\n    Name string\n}\n\ntype Value2 struct {\n    Name string\n}\n\nfunc main() {\n    v1 := Value1{Name: \"煎鱼\"}\n    v2 := Value2{Name: \"煎鱼\"}\n    if v1 == v2 {\n        fmt.Println(\"脑子进煎鱼了\")\n        return\n    }\n\n    fmt.Println(\"脑子没进煎鱼\")\n}\n```\n\n显然，会直接报错：\n\n```\n# command-line-arguments\n./main.go:18:8: invalid operation: v1 == v2 (mismatched types Value1 and Value2)\n```\n\n那是不是就完全没法比较了呢？并不，我们可以借助强制转换来实现：\n\n```golang\n\tif v1 == Value1(v2) {\n\t\tfmt.Println(\"脑子进煎鱼了\")\n\t\treturn\n\t}\n```\n\n这样程序就会正常运行，且输出 “脑子进煎鱼了”。当然，若是不可比较类型，依然是不行的。\n\n## 为什么\n\n为什么 Go 结构体有的比较就是正常，有的就不行，甚至还直接报错了。难道是有什么 “潜规则” 吗？\n\n在 Go 语言中，Go 结构体有时候并不能直接比较，当其基本类型包含：slice、map、function 时，是不能比较的。若强行比较，就会导致出现例子中的直接报错的情况。\n\n而指针引用，其虽然都是 `new(string)`，从表象来看是一个东西，但其具体返回的地址是不一样的。\n\n因此若要比较，则需改为：\n\n```golang\nfunc main() {\n    gender := new(string)\n    v1 := Value{Name: \"煎鱼\", Gender: gender}\n    v2 := Value{Name: \"煎鱼\", Gender: gender}\n    ...\n}\n```\n\n这样就可以保证两者的比较。如果我们被迫无奈，被要求一定要用结构体比较怎么办？\n\n这时候可以使用反射方法 `reflect.DeepEqual`，如下：\n\n```golang\nfunc main() {\n    v1 := Value{Name: \"煎鱼\", GoodAt: []string{\"炸\", \"煎\", \"蒸\"}}\n    v2 := Value{Name: \"煎鱼\", GoodAt: []string{\"炸\", \"煎\", \"蒸\"}}\n    if reflect.DeepEqual(v1, v2) {\n        fmt.Println(\"脑子进煎鱼了\")\n        return\n    }\n\n    fmt.Println(\"脑子没进煎鱼\")\n}\n```\n\n这样子就能够正确的比较，输出结果为 “脑子进煎鱼了”。\n\n例子中所用到的反射比较方法 `reflect.DeepEqual` 常用于判定两个值是否深度一致，其规则如下：\n\n- 相同类型的值是深度相等的，不同类型的值永远不会深度相等。\n- 当数组值（array）的对应元素深度相等时，数组值是深度相等的。\n- 当结构体（struct）值如果其对应的字段（包括导出和未导出的字段）都是深度相等的，则该值是深度相等的。\n- 当函数（func）值如果都是零，则是深度相等；否则就不是深度相等。\n- 当接口（interface）值如果持有深度相等的具体值，则深度相等。\n- ...\n\n更具体的大家可到 `golang.org/pkg/reflect/#DeepEqual` 进行详细查看：\n\n![reflect.DeepEqual 完整说明](https://image.eddycjy.com/aefb19ae4bce5bbbada39244141bfd68.jpg)\n\n该方法对 Go 语言中的各种类型都进行了兼容处理和判别，由于这不是本文的重点，因此就不进一步展开了。\n\n## 总结\n\n在本文中，我们针对 Go 语言的结构体（struct）是否能够比较进行了具体例子的展开和说明。\n\n其本质上还是对 Go 语言基本数据类型的理解问题，算是变形到结构体中的具体进一步拓展。\n\n不知道你有没有在 Go 结构体吃过什么亏呢，欢迎在下方评论区留言和我们一起交流和讨论。"
  },
  {
    "path": "content/posts/go/go-tips-timer-memory.md",
    "content": "---\ntitle: \"Go 内存泄露之痛，这篇把 Go timer.After 问题根因讲透了！\"\ndate: 2021-04-05T16:16:47+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好，我是煎鱼。\n\n前几天在公众号分享了一篇 Go timer 源码解析的文章《难以驾驭的 Go timer，一文带你参透计时器的奥秘》。\n\n在评论区有小伙伴提到了经典的 `timer.After` 泄露问题，希望我能聊聊，这是一个不能不知的一个大 “坑”。\n\n今天这篇文章煎鱼就带大家来研讨一下这个问题。\n\n## timer.After\n\n今天是男主角是Go 标准库 time 所提供的 `After` 方法。函数签名如下：\n\n```golang\nfunc After(d Duration) <-chan Time \n```\n\n该方法可以在一定时间（根据所传入的 Duration）后主动返回 `time.Time` 类型的 channel 消息。\n\n在常见的场景下，我们会基于此方法做一些计时器相关的功能开发，例子如下：\n\n```golang\nfunc main() {\n    ch := make(chan string)\n    go func() {\n        time.Sleep(time.Second * 3)\n        ch <- \"脑子进煎鱼了\"\n    }()\n\n    select {\n    case _ = <-ch:\n    case <-time.After(time.Second * 1):\n        fmt.Println(\"煎鱼出去了，超时了！！！\")\n    }\n}\n```\n\n在运行 1 秒钟后，输出结果：\n\n```\n煎鱼出去了，超时了！！！\n```\n\n上述程序在在运行 1 秒钟后将触发 `time.After` 方法的定时消息返回，输出了超时的结果。\n\n## 坑在哪里\n\n从例子来看似乎非常正常，也没什么 “坑” 的样子。难道是 `timer.After` 方法的虚晃一枪？\n\n我们再看一个不像是有问题例子，这在 Go 工程中经常能看见，只是大家都没怎么关注。\n\n代码如下：\n\n```golang\nfunc main() {\n    ch := make(chan int, 10)\n    go func() {\n        in := 1\n        for {\n            in++\n            ch <- in\n        }\n    }()\n    \n    for {\n        select {\n        case _ = <-ch:\n            // do something...\n            continue\n        case <-time.After(3 * time.Minute):\n            fmt.Printf(\"现在是：%d，我脑子进煎鱼了！\", time.Now().Unix())\n        }\n    }\n}\n```\n\n在上述代码中，我们构造了一个 `for+select+channel` 的一个经典的处理模式。\n\n同时在 `select+case` 中调用了 `time.After` 方法做超时控制，避免在 `channel` 等待时阻塞过久，引发其他问题。\n\n看上去都没什么问题，但是细心一看。在运行了一段时间后，粗暴的利用 `top` 命令一看：\n\n![运行了一会后，10+GB](https://image.eddycjy.com/4ad756d034cbbe7e7a19d9b4eb0c4843.jpg)\n\n我的 Go 工程的内存占用竟然已经达到了 10+GB 之高，并且还在持续增长，非常可怕。\n\n在所设置的超时时间到达后，Go 工程的内存占用似乎一时半会也没有要回退下去的样子，这，到底发生了什么事？\n\n## 为什么\n\n抱着一脸懵逼的煎鱼，我默默的掏出我早已埋好的 PProf，这是 Go 语言中最强的性能分析剖析工具，在我出版的 《Go 语言编程之旅》特意有花量章节的篇幅大面积将讲解过。\n\n在 Go 语言中，PProf 是用于可视化和分析性能分析数据的工具，PProf 以 profile.proto 读取分析样本的集合，并生成报告以可视化并帮助分析数据（支持文本和图形报告）。\n\n我们直接用 `go tool pprof` 分析 Go 工程中函数内存申请情况，如下图：\n\n![PProf](https://image.eddycjy.com/c9552708ee112bceef4ac80f1ead50bd.jpg)\n\n从图来分析，可以发现是不断地在调用 `time.After`，从而导致计时器 `time.NerTimer` 的不断创建和内存申请。\n\n这就非常奇怪了，因为我们的 Go 工程里只有几行代码与 `time` 相关联：\n\n```golang\nfunc main() {\n    ...\n    for {\n        select {\n        ...\n        case <-time.After(3 * time.Minute):\n            fmt.Printf(\"现在是：%d，我脑子进煎鱼了！\", time.Now().Unix())\n        }\n    }\n}\n```\n\n由于 Demo 足够的小，我们相信这就是问题代码，但原因是什么呢？\n\n原因在于 `for`+`select`，再加上 `time.After` 的组合会导致内存泄露。因为 `for`在循环时，就会调用都 `select` 语句，因此在每次进行 `select` 时，都会重新初始化一个全新的计时器（Timer）。\n\n我们这个计时器，是在 3 分钟后才会被触发去执行某些事，但重点在于计时器激活后，却又发现和 `select` 之间没有引用关系了，因此很合理的也就被 GC 给清理掉了，因为没有人需要 “我” 了。\n\n![](https://image.eddycjy.com/b84d7d95fc2ca3d9688ae56461449512.jpg)\n\n要命的还在后头，被抛弃的 `time.After` 的定时任务还是在时间堆中等待触发，在定时任务未到期之前，是不会被 GC 清除的。\n\n![](https://image.eddycjy.com/8c2b0ebbce7d8e0d4432bb7c81a50c6e.jpg)\n\n但很可惜，他 “永远” 不会到期了，也就是为什么我们的 Go 工程内存会不断飙高，其实是 `time.After` 产生的内存孤儿们导致了泄露。\n\n## 解决办法\n\n既然我们知道了问题的根因代码是不断的重复创建 `time.After`，又没法完整的走完释放的闭环，那解决办法也就有了。\n\n改进后的代码如下：\n\n```golang\nfunc main() {\n    timer := time.NewTimer(3 * time.Minute)\n    defer timer.Stop()\n    \n    ...\n    for {\n        select {\n        ...\n        case <-timer.C:\n            fmt.Printf(\"现在是：%d，我脑子进煎鱼了！\", time.Now().Unix())\n        }\n    }\n}\n```\n\n经过一段时间的摸鱼后，再使用 PProf 进行采集和查看：\n\n![PProf](https://image.eddycjy.com/bed36b48fb6e75d690208e1b1b149369.jpg)\n\nGo 进程的各项指标正常，完好的解决了这个内存泄露的问题。\n\n## 总结\n\n在今天这篇文章中，我们介绍了标准库 `time` 的基本常规使用，同时针对 Go 小伙伴所提出的 `time.After` 方法的使用不当，所导致的内存泄露进行了重现和问题解析。\n\n其根因就在于 Go 语言时间堆的处理机制和常规 `for`+`select`+`time.After` 组合的下意识写法所导致的泄露。\n\n突然想起我有一个朋友在公司里有看到过类似的代码...\n\n不知道你在日常工作中有没有遇到过相似的问题呢，欢迎留言区评论和交流。\n\n"
  },
  {
    "path": "content/posts/go/go-typeparams-master.md",
    "content": "---\ntitle: \"令人激动！Go 泛型代码合入 master（附尝鲜方法）\"\ndate: 2021-04-05T16:06:50+08:00\nimages:\ntags: \n  - go\n---\n\n大家好，我是慢一拍的后方记者煎鱼。\n\n按照先前官方和文章的说法，Go 泛型预计是在 Go1.18 正式释出。\n\n![](https://imgkr2.cn-bj.ufileos.com/8971e01e-75f8-47c2-b9d3-f6b0ebd85d0d.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=8eKquOL0aM7GzJXegZUzGkOmLPg%253D&Expires=1614491040)\n\n\n在 GopherCon 2020 Go Team AMA 时，要在今年底要能有生产环境的试用版上线，这是 rsc 所提出的一个管理目标。\n\n## 转折点\n\n近期出现了一个新的转折点，能够让大家在主干分支（master）上就能享受到泛型的功能。\n\n![](https://imgkr2.cn-bj.ufileos.com/3acc09a4-d5a2-49e7-b272-b2fa1571f589.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=zXcEIWPbc%252BUFBDmTyYrRVywwKuI%253D&Expires=1614491656)\n\n而 master 分支对应了 Go1.17 的版本。因此未来将可以在 Go1.17 使用到泛型，这是一个比较惊喜的事情。\n\n## 原因\n\n这件事情为什么会突然发生呢？一切都得从背景说起。原本 Go 泛型是一直在 [dev.typeparams](https://github.com/golang/go/tree/dev.typeparams) 分支上进行研讨和开发。\n\n由于泛型不是简单的一两个模块的代码变更，而是涉及大量的代码变更。\n\n因此需要经常保持与 master 分支的代码同步（近两个月共 20+ 次），会涉及代码冲突/合并的处理，且对于一些冲突的模块他们也不熟悉，所以期望迁移到 master 分支上进行开发。\n\n## 如何不影响既有功能\n\n这类提前放入主版本的操作，在 Go 语言中并不少见。像是现在所见的 `GO111MODULE`，早期的 `GO15VENDOREXPERIMENT` 都有些这么个味道。都是逐步入场，分阶段使用，等确定成熟、完善后再渐渐去掉。\n\n因此本次泛型也采取了这种方法，按照提案，目前使用的是 `-G` 标识做为泛型的开关。\n\n计划如下：\n\n- `-G=0`：继续使用传统的类型检查器。\n- `-G=1`：使用 type2，但不支持泛型。\n- `-G=2`：使用 type2，支持泛型。\n\n在完成 types2 的错误和现有的错误的开发协调后，计划在 Go 1.17 将 `-G=1` 设置为默认值。\n\n未来也许可以在 Go 1.18 中放弃对 `-G=0` 的支持，这样后续在默认启用 `-G=2` 上会变得更容易。\n\n## 在 Go1.17 尝鲜\n\n在 Go1.17 尝鲜，也就意味着需要拉取 Go 语言的 master 分支的代码，Go1.17 现在正处于开发阶段：\n\n![](https://imgkr2.cn-bj.ufileos.com/8690e590-08d0-416b-b7c0-76a3cd9fbd2b.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=YDlj2rmds9pT8iLgaho3cITFCuw%253D&Expires=1614498690)\n\n我们可以通过 `gotip` 来达到下载 master 分支代码的目的：\n\n```\n$ go get golang.org/dl/gotip\n$ gotip download\nFrom https://go.googlesource.com/go\n * branch            master     -> FETCH_HEAD\n   44361140c0..d9fd38e68b master     -> origin/master\nPrevious HEAD position was 44361140c0 embed: update docs for proposal tweaks\n...\n```\n\n在拉取完毕后可以执行 `gotip version` 查看所拉取的版本（commit-id）：\n\n```\n$ gotip version\ngo version devel +d9fd38e68b Sat Feb 27 03:03:29 2021 +0000 darwin/amd64\n```\n\n在确定 `gotip` 正常后，我们就可以编写泛型的示例代码了，如下：\n\n```\nfunc Print[T any](s []T) {\n\tfor _, v := range s {\n\t\tfmt.Print(v)\n\t}\n}\n\nfunc main() {\n\tPrint([]string{\"脑子进, \", \"煎鱼了\\n\"})\n}\n```\n\n如果执行像往常那样执行，是会直接提示无法识别泛型的一些标识符：\n\n```\n$ gotip run main.go \n# command-line-arguments\n./main.go:7:6: missing function body\n./main.go:7:11: syntax error: unexpected [, expecting (\n```\n\n结合上文的解析，我们需要指定 `-G` 标识，就可以运行了。如下：\n\n```\n$ gotip run -gcflags=all=-G=3 main.go \n# command-line-arguments\n./main.go:7:6: internal compiler error: Cannot export a generic function (yet): Print\n```\n\n显然，正确的走进泛型的逻辑里去了，虽然愉快的报错了，但 Matthew Dempsky 表示这很正常，毕竟 Go 泛型还在开发阶段。\n\n可能会有的小伙伴注意到，`-G` 指定的是 3，与前文所述不符。这与早期的编码有关：\n\n![](https://imgkr2.cn-bj.ufileos.com/4599daf1-36ef-4c4a-bdf7-33bcbe7ac1cb.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=S9zQAMxyQpdxryZN%252FrwkYOmqDI4%253D&Expires=1614499432)\n\n已经提了 CL 变更，只是代码冲突了，待解决。\n\n## 总结\n\nGo 语言的泛型开发计划已经比较明确。首先合入 master 分支，再逐步完成开发，逐步开放。\n\n再进行 `-G` 默认值的调整，最后在泛型完善后就默认开启，把 `-G` 标识彻底去掉。\n\n细品，与 Go modules 的方向是不是差不多。一开始 `GO111MODULE` 需要手动开启 `on`（也就是默认 `off`），再到 Go1.16 `GO111MODULE` 默认为 `on`。\n\n以此完成了一个正反馈的循环，逐步开放，接受社区反馈和开发调整。\n\n结论，**Go 泛型指日可待了**。\n"
  },
  {
    "path": "content/posts/go/go-why-path.md",
    "content": "---\ntitle: \"意见征集：Go1 要不要移除 GOPATH？\"\ndate: 2021-04-05T16:02:34+08:00\nimages:\ntags: \n  - go\n---\n\n大家好，我是在打自己脸的后方记者煎鱼。\n\n前几天我发表了文章《意见征集：Go1 要不要移除 GOPATH？》。\n本次投票共有 592 人参与了投票。\n结果如下：\n\n![](https://static01.imgkr.com/temp/d10e1354bc9d43c68439fb71d8935270.png)\n\n绝大部分人支持移除 GOPATH，但建议保留的人依然占相当一部分。\n\n## 最新进展\n\n在近几天，在 go issues 上有人提出了新的提案：《proposal: cmd/go: maintain 'classic' vendor behaviour》。\n\n该提案正式向 Go 官方提出了我们上一篇文章有提到的两点：\n1. Go 历史项目的维护问题。\n2. Go1 兼容性保证的许诺。\n\n摘选一部分核心观点给大家看看。\n\n## 新的提案\n\n从 Go1.17 开始，Go1.17 就将 GOPATH 从编译器工具链中移除 `GO111MODULE` 标识。\n\n其依据如下：\n\n![](https://static01.imgkr.com/temp/bf314e0979b7481681c6293ee60715c3.png)\n\n重点在于执行 `-mod=vendor` 命令时，其会忽略主模块根目录以外位置的软件目录，**这意味着在 go mod 之前设计和编写的应用程序不能再编译（指的历史项目问题）**。\n\n提案本身并不是要求要保持 GOPATH 代码解析 1:1 的说法，而是希望能够允许项目代码通过其他的方式在移除 GOPATH 后也能正常运行，有其他方式能够导入（不需要转成 Go mod）。\n\n另外最后提到删除 GOPATH 这样的代码解析能力，**与下面这段摘自 go1compat 文档的精神不一致**：\n\n>> It is intended that programs written to the Go 1 specification will continue to compile and run correctly, unchanged, over the lifetime of that specification. At some indefinite point, a Go 2 specification may arise, but until that time, Go programs that work today should continue to work even as future \"point\" releases of Go 1 arise (Go 1.1, Go 1.2, etc.).\n\n指出直接移除 GOPATH 可能违反 Go1 兼容性保证的精神，带来历史项目的工作量。\n\n## 相爱相杀\n\n无独有偶，前几天我和欧神（@changkun）就讨论过 Go1 兼容性保证（go1compat）的问题，我也是认为违反了该精神保证，这次行为不是很合理。\n\n但...欧神重读 《Go 1 and the Future of Go Programs》后，也就是我们俗称的 Go1 兼容性保证。发现了以下亮点：\n\n![](https://static01.imgkr.com/temp/aa47a80786814576b5a5b091d8244127.png)\n\n在工具链中提到，**Go 语言的工具链（编译器、链接器、构建工具等）仍然正在积极开发中，可能会改变行为，也就是不在兼容性保护的范围内**。\n\n我们回到主角身上，GOPATH 是什么：GOPATH 定义的是软件的构建形式而不是编译的条件，归属于工具链里。\n\n因此其实**移除 GOPATH 并不违反 Go1 兼容性保证**，因为他不在保障范围内。\n\n注：感谢欧神的探讨和释疑，在此推荐欧神的大作《[Go 语言原本](https://golang.design/under-the-hood/ \"Go 语言原本\")》。\n\n## 总结\n\n目前该[提案](https://github.com/golang/go/issues/44519 \"提案\")已经进入 proposal review meeting 日程了，很快就会有决策了，在这里我们就不进行过多的新方案探讨。\n\n现实场景中依赖 GOPATH 的历史项目确实存在，使用什么方法更低成本、合理的让既有项目能够正常运行将会是一个讨论重点。\n\n接下来煎鱼将会跟踪这个提案，新的消息会继续同步。毕竟我是利益相关者（有依赖 GOPATH 的项目)...\n\n欢迎大家一起来讨论各种可能性！"
  },
  {
    "path": "content/posts/go/go1.16-1.md",
    "content": "---\ntitle: \"Go1.16 即将正式发布，以下变更你需要知道\"\ndate: 2021-02-11T16:13:15+08:00\nimages:\ntags: \n  - go\n---\n\n大家好，我是正在努力学习的煎鱼。\n\n在前几天，Go1.16rc1 抢先发布了。结合常规的 28 发布规律，其将会在 2021.02 月份左右发布正式版本。\n\n![](https://imgkr2.cn-bj.ufileos.com/417219ea-578e-48f0-8a83-84544000a698.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=mbLHd49lXRyOoY%252FlyamD5YnGagw%253D&Expires=1612064951)\n\n这次 Go1.16 也带来了一些新特性或变更。那么作为一个 Gopher，想必不能错过这次的更新。\n\n![](https://imgkr2.cn-bj.ufileos.com/4161e3cd-9f66-4f3f-8f68-91f3ba496c18.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=CwpOhSJnk70gEkTF8ITIxC58PoM%253D&Expires=1612074386)\n\n今天这篇文章将会带大家了解一下 Go1.16 的几个需要关注的特性。\n\n### 废弃 io/ioutil\n\nGo 官方认为 io/ioutil 这个包的定义不明确且难以理解。所以 Russ Cox 在 2020.10.17 提出了废弃 io/ioutil 的提案。\n\n大致变更如下：\n- Discard => io.Discard\n- NopCloser => io.NopCloser\n- ReadAll => io.ReadAll\n- ReadDir => os.ReadDir\n- ReadFile => os.ReadFile\n- TempDir => os.MkdirTemp\n- TempFile => os.CreateTemp\n- WriteFile => os.WriteFile\n\n与此同时大家也不需要担心存在破坏性变更，因为有 Go1 兼容性的保证，在 Go1 中 io/ioutil 还会存在，只变更内部的方法调用：\n\n```\nfunc ReadAll(r io.Reader) ([]byte, error) {\n    return io.ReadAll(r)\n}\n\nfunc ReadFile(filename string) ([]byte, error) {\n    return os.ReadFile(filename)\n}\n```\n\n大家在后续也可以改改调用习惯。\n\n### 支持静态资源嵌入\n\n如果我们希望把静态文件编译进 Go 的二进制文件的话，在以往需要借助 go-bindata/go-bindata 这类第三方开源库来实现。\n\n而从 Go1.16 起，通过 `go:embed` 就可以快速实现这个功能：\n\n```\nimport _ \"embed\"\n\n//go:embed hello.txt\nvar s string\nprint(s)\n```\n\n通过对变量 `s` 声明 `go:embed` 指令，使其在编译时读取当前目录下的 `hello.txt` 文件。\n\n最终变量 `s` 就会输出 `hello.txt` 文件中的字符串内容。 \n\n### 新增 io/fs 的支持 \n\n新增了标准库 io/fs，正式将文件系统相关的基础接口抽象到了该标准库中。\n\n以前的话大多是在 `os` 标准库中，这一步抽离更进一步的抽象了文件树的接口。在后续的版本中，大家可以优先考虑使用 `io/fs` 标准库。\n\n### 调整切片扩容策略\n\nGo1.16 以前的 slice 的扩容条件是 `len`，在最新的代码中，已经改为了以 `cap` 属性作为基准：\n\n```\n  // src/runtime/slice.go\n\tif cap > doublecap {\n\t\tnewcap = cap\n\t} else {\n\t\t// 这是以前的代码：if old.len < 1024 {\n\t\t// 下面是 Go1.16rc1 的代码\n\t\tif old.cap < 1024 {\n\t\t\tnewcap = doublecap\n\t\t}\n```\n\n以官方的 test case 为例：\n\n```\nfunc main() {\n\tconst N = 1024\n\tvar a [N]int\n\tx := cap(append(a[:N-1:N], 9, 9))\n\ty := cap(append(a[:N:N], 9))\n\tprintln(cap(x), cap(y))\n}\n```\n\n在 Go1.16 以前输出 2048, 1280。在 Go1.16 及以后输出 1280, 1280，保证了两种的一致。\n\n### 支持 Apple Silicon M1\n\n众所周知，最新版本的 Mac 采用了新的 64 位 ARM 架构，因此在 Go1.16 后正式支持了 `GOOS=darwin` 和 `GOARCH=arm64`。\n\n而相应的先前用于 iOS 端口的，将改为 `GOOS=ios` 和 `GOARCH=arm64`。\n\n同时 Apple M1 能不能很好的跑好 Go 语言程序也是各大微信群爱讨论的问题，在 GoLand 上：\n\n![图来自网络，路过微信群看见](https://static01.imgkr.com/temp/960978d546694a5c94ca2af70e44c83c.png)\n\n需要注意，GoLand 的一些给你要到后续的新版本才可以使用。\n\n### 调整 Go modules 策略\n\n从 Go1.16 起，Go modules 的环境变量 `GO111MODULE `默认开关将为 `on`，不再是之前是 `auto` 了。\n\n还在使用 GOPATH，或 Go modules 没切全的同学这一块需要特别注意。\n\n### 新增 GODEBUG inittrace\n\nGODEBUG 新增 inittrace 指令，可以用于 `init` 方法的排查：\n\n```\n$ GODEBUG=inittrace=1 go run main.go \n```\n\n输出结果：\n\n```\ninit internal/bytealg @0.008 ms, 0 ms clock, 0 bytes, 0 allocs\ninit runtime @0.059 ms, 0.026 ms clock, 0 bytes, 0 allocs\ninit math @0.19 ms, 0.001 ms clock, 0 bytes, 0 allocs\ninit errors @0.22 ms, 0.004 ms clock, 0 bytes, 0 allocs\ninit strconv @0.24 ms, 0.002 ms clock, 32 bytes, 2 allocs\ninit sync @0.28 ms, 0.003 ms clock, 16 bytes, 1 allocs\ninit unicode @0.44 ms, 0.11 ms clock, 23328 bytes, 24 allocs\n...\n```\n\n主要作用是 init 函数跟踪的支持，以用于 init 调试和启动时间的概要分析，算是一个 GODEBUG 的补充功能点。\n\n## 简化结构体标签\n\n在 Go 语言的结构体中，我们常常会因为各种库的诉求，需要对结构体的 `tag` 设置标识。\n\n如果像是以前，量比较多就会变成：\n\n```\ntype MyStruct struct {\n  Field1 string `json:\"field_1,omitempty\" bson:\"field_1,omitempty\" xml:\"field_1,omitempty\" form:\"field_1,omitempty\" other:\"value\"`\n}\n```\n\n但在 Go1.16 及以后，就可以通过合并的方式：\n\n```\ntype MyStruct struct {\n  Field1 string `json,bson,xml,form:\"field_1,omitempty\" other:\"value\"`\n}\n```\n\n方便和简洁了不少。\n\n## 总结\n\n在本次 Go1.16 中带来了不少小优化和新的特性支持。离 Go1.18 的泛型又近了一步。\n\n另外在本次新版本中，像是 `template` 支持跨行：\n\n```\n{{\"hello\" |\n   printf}}\n```\n\n又或是 Linux 的默认内存管理策略下又从 MADV_FREE 改回了 MADV_DONTNEED 策略，大家在新版本中不再需要设置：\n\n```\nGODEBUG=madvdontneed=1\n```\n\n大家若有需求都可以进一步去了解，现在新版本的功能特性已经锁定，基本尘埃落定。\n\n传送门：https://tip.golang.org/doc/go1.16。\n\n\n\n"
  },
  {
    "path": "content/posts/go/go1.16-2.md",
    "content": "---\ntitle: \"Go1.16 新特性：快速上手 Go embed\"\ndate: 2021-02-11T16:13:19+08:00\nimages:\ntags: \n  - go\n---\n\n在以前，很多从其他语言转过来 Go 语言的同学会问到，或是踩到一个坑。就是以为 Go 语言所打包的二进制文件中会包含配置文件的联同编译和打包。\n\n![](https://imgkr2.cn-bj.ufileos.com/f59a06c7-2fa1-41f4-901c-990f7dd7d715.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=werBc5OkLhKUh0JhpYpcAtl3etA%253D&Expires=1612147306)\n\n\n结果往往一把二进制文件挪来挪去，就无法把应用程序运行起来了。因为无法读取到静态文件的资源。\n\n无法将静态资源编译打包进二进制文件的话，通常会有两种解决方法：\n- 第一种是识别这类静态资源，是否需要跟着程序走。\n- 第二种就是考虑将其打包进二进制文件中。\n\n第二种情况的话，Go 以前是不支持的，大家就会去借助各种花式的开源库，例如：go-bindata/go-bindata 来实现。\n\n但从在 Go1.16 起，Go 语言自身正式支持了该项特性，今天我们将通过这篇文章快速了解和学习这项特性。\n\n## 基本使用\n\n演示代码：\n\n```\nimport _ \"embed\"\n\n//go:embed hello.txt\nvar s string\n\nfunc main() {\n\tprint(s)\n}\n```\n我们首先在对应的目录下创建了 `hello.txt` 文件，并且写入文本内容 “吃煎鱼”。\n\n在代码中编写了最为核心的 `//go:embed hello.txt` 注解。注解的格式很简单，就是 `go:embed` 指令声明，外加读取的内容的地址，可支持相对和绝对路径。\n\n输出结果：\n\n```\n吃煎鱼\n```\n\n读取到静态文件中的内容后自动赋值给了变量 `s`，并且在主函数中成功输出。\n\n而针对其他的基础类型，Go embed 也是支持的：\n\n```\n//go:embed hello.txt\nvar s string\n\n//go:embed hello.txt\nvar b []byte\n\n//go:embed hello.txt\nvar f embed.FS\n\nfunc main() {\n\tprint(s)\n\tprint(string(b))\n\n\tdata, _ := f.ReadFile(\"hello.txt\")\n\tprint(string(data))\n}\n```\n\n输出结果：\n\n```\n吃煎鱼\n吃煎鱼\n吃煎鱼\n```\n\n我们同时在一个代码文件中进行了多个 embed 的注解声明。\n\n并且针对 string、slice、byte、fs 等多种类型进行了打包，也不需要过多的处理，非常便利。\n\n## 拓展用法\n\n除去基本用法完，embed 本身在指令上也支持多种变形：\n\n```\n//go:embed hello1.txt hello2.txt\nvar f embed.FS\n\nfunc main() {\n\tdata1, _ := f.ReadFile(\"hello1.txt\")\n\tfmt.Println(string(data1))\n\n\tdata2, _ := f.ReadFile(\"hello2.txt\")\n\tfmt.Println(string(data2))\n}\n```\n在指定 `go:embed` 注解时可以一次性多个文件来读取，并且也可以一个变量多行注解：\n\n```\n//go:embed hello1.txt \n//go:embed hello2.txt\nvar f embed.FS\n```\n\n也可以通过在注解中指定目录 `helloworld`，再对应读取文件：\n\n```\n//go:embed helloworld\nvar f embed.FS\n\nfunc main() {\n\tdata1, _ := f.ReadFile(\"helloworld/hello1.txt\")\n\tfmt.Println(string(data1))\n\n\tdata2, _ := f.ReadFile(\"helloworld/hello2.txt\")\n\tfmt.Println(string(data2))\n}\n```\n\n同时既然能够支持目录读取，也能支持贪婪模式的匹配：\n\n```\n//go:embed helloworld/*\nvar f embed.FS\n```\n\n可能会有小伙伴注意到，`embed.FS` 也能调各类文件系统的接口，其实本质是 `embed.FS` 实现了 `io/fs` 接口。\n\n## 只读属性\n\n在 embed 所提供的 FS 中，我们可以发现其都是打开和只读方法：\n\n```\ntype FS\n    func (f FS) Open(name string) (fs.File, error)\n    func (f FS) ReadDir(name string) ([]fs.DirEntry, error)\n    func (f FS) ReadFile(name string) ([]byte, error)\n```\n\n根据此也可以确定 embed 所打包进二进制文件的内容只允许读取，不允许变更。\n\n更抽象来讲就是在编译期就确定了 embed 的内容，在运行时不允许修改，保证了一致性。\n\n## 总结\n\n通过 Go1.16 正式提供的 embed 特性，可以实现原生就支持静态资源文件的嵌入。整体如下：\n\n- 在功能上：能够将静态资源嵌入二进制文件中，在运行时可以打开和读取相关的打包后的静态文件。\n- 在安全上：是在编译期编译嵌入，在运行时不支持修改。\n- 在使用上：\n    - 支持单文件读取：`go:embed hello.txt`。\n    - 支持多文件读取：`go:embed hello1.txt`、`go:embed hello2.txt`。\n    - 支持目录读取：`go:embed helloworld`。\n    - 支持贪婪匹配：`go:embed helloworld/*`。\n\n总的来讲，Go1.16 embed 特性很好的填补了 Go 语言在打包静态文件资源的一块原生空白领域。同时也说明了 Go 官方的确在不断地吸收社区的一些良好的想法和经验。"
  },
  {
    "path": "content/posts/go/go1.16-3.md",
    "content": "---\ntitle: \"Go1.16 新特性：详解内存管理机制的变更，你需要了解\"\ndate: 2021-02-11T16:13:20+08:00\nimages:\ntags: \n  - go\n---\n\n\n大家好，我是正在学习如何蒸鱼的煎鱼。\n\n在前面 Go1.16 特性介绍的文章中我们有提到，从 v1.16 起，Go 在 Linux 下的默认内存管理策略会从`MADV_FREE` 改回 `MADV_DONTNEED` 策略。\n\n这时候可能至少分两拨小伙伴，分别是：\n\n- 知道是什么，被这个问题 “折磨“ 过的，瞬间眼前一亮。\n- 不知道是什么，出现了各种疑惑了，这说的都是些什么。\n\n## 灵魂拷问\n\n你有没有以下的疑问，或者是否清楚：\n\n- 文中所说的 `MADV_FREE` 是什么？\n- 文中所说的 `MADV_DONTNEED` 是什么？\n- 为什么特指 Go 语言的 Linux 环境？\n- 为什么是说从 `MADV_FREE`改回 `MADV_DONTNEED`？\n\n在今天这篇文章中我们都将进一步的展开和说明，让我们一同来了解这个改来改去的内存机制到底是何物。\n\n## madvise 爱与恨\n\n在 Linux 系统中，在 Go Runtime 中通过系统调用 `madvise(addr, length, advise)` 方法，能够告诉内核如何处理从 addr 开始的 length 字节。\n\n重点之一就是 ”如何处理“，在 Linux 下 Go 语言中目前支持两种策略，分别是：\n\n- MADV_FREE：内核会在进程的页表中将这些页标记为“未分配”，从而进程的 RSS 就会变小。OS 后续可以将对应的物理页分配给其他进程。\n- MADV_DONTNEED：内核只会在页表中将这些进程页面标记为可回收，在需要的时候才回收这些页面。\n\n## 所带来的影响\n\nGo 语言官方恰好就在 2019 年的 Go1.12 做了如下调整。\n\n- Go1.12 以前。\n- Go.12-Go1.15.\n\n\n### Go1.12 以前\n\nGo Runtime 在 Linux 上默认使用的是 `MADV_DONTNEED` 策略。\n\n```\n  // 没有任何奇奇怪怪的判断\n\tmadvise(v, n, _MADV_DONTNEED)\n```\n\n从整体效果来看，进程 RSS 可以下降的比较快，但从性能效率上来看差点。\n\n\n### Go1.12-Go1.15\n\n当前 Linux 内核版本 >=4.5 时，Go Runtime 在 Linux 上默认使用了性能更为高效的 MADV_FREE 策略。\n\n```\n\tvar advise uint32\n\tif debug.madvdontneed != 0 {\n\t\tadvise = _MADV_DONTNEED\n\t} else {\n\t\tadvise = atomic.Load(&adviseUnused)\n\t}\n\tif errno := madvise(v, n, int32(advise)); advise == _MADV_FREE && errno != 0 {\n\t\t// MADV_FREE was added in Linux 4.5. Fall back to MADV_DONTNEED if it is\n\t\t// not supported.\n\t\tatomic.Store(&adviseUnused, _MADV_DONTNEED)\n\t\tmadvise(v, n, _MADV_DONTNEED)\n\t}\n```\n\n从整体效果来看，进程RSS 不会立刻下降，要等到系统有内存压力了才会释放占用，RSS 才会下降。\n\n## 带来的副作用\n\n故事往往不是那么的美好，显然在 Go1.12 起针对 `madvise` 的 `MADV_FREE` 策略的调整非常 “片面”。\n\n![来自社区小伙伴](https://static01.imgkr.com/temp/5b82f6e181bd406db94e31bca3a4b2ab.png)\n\n结合社区里所遇到的案例可得知，该次调整带来了许多问题：\n- **引发用户体验的问题**：Go issues 上总是出现以为内存泄露，但其实只是未满足条件，内存没有马上释放的案例。\n- **混淆统计信息和监控工具的情况**：在 Grafana 等监控上，发现容器进程内存较高，释放很慢，告警了，很慌。\n- **导致与内存使用有关联的个别管理系统集成不良**：例如 Kubernetes HPA ，或者自定义了扩缩容策略这类模式，难以评估。\n- **挤压同主机上的其他应用资源**：并不是所有的 Go 程序都一定独立跑在单一主机中，自然就会导致同一台主机上的其他应用受到挤压，这是难以评估的。\n\n从社区反馈来看是问题多多，弊大于利。\n\n官方本想着想着性能更好一些，但是在现实场景中引发了不少的新问题，甚至有提到和 Android 流程管理不兼容的情况。\n\n有种 “捡了芝麻，丢了西瓜” 的感觉。\n\n\n## Go1.16：峰回路转\n\n既然社区反馈的问题何其多，有没有人提呢？有，超级多。\n\n\n![](https://imgkr2.cn-bj.ufileos.com/75ee9ea5-a12c-4de1-806f-eeb85f80e61f.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=%252BqN8BOgO5kZXIUhuHy1QyxOOZ30%253D&Expires=1612673513)\n\n\n多到提出修改回 `MADV_DONTNEED` 的 issues 仅花了 1-2 天的时间就讨论完毕。\n\n很快得出结论且合并 CL 关闭 issues 了。\n\nGo1.16 修改内容如下：\n\n```\nfunc parsedebugvars() {\n\t// defaults\n\tdebug.cgocheck = 1\n\tdebug.invalidptr = 1\n\tif GOOS == \"linux\" {\n\t\tdebug.madvdontneed = 1\n\t}\n  ...\n}\n```\n\n直接指定回了 `debug.madvdontneed = 1`，简单粗暴。\n\n## 总结\n\n在本篇文章中，我们针对 Go 语言在 Linux 下的 `madvise` 方法的策略调整进行了历史介绍和说明，同时针对其调整所带来的的副作用及应对措施进行了一一介绍。\n\n本次变更很好的印证了，牵一发动全身的说法。大家在后续应用这块时也要多加注意。\n\n## 参考\n\n- [runtime: default to MADV_DONTNEED on Linux](https://github.com/golang/go/issues/42330)\n- [踩坑记： go 服务内存暴涨](https://www.v2ex.com/t/666257?p=1)\n- [Go 1.12 关于内存释放的一个改进](https://ms2008.github.io/2019/06/30/golang-madvfree/)"
  },
  {
    "path": "content/posts/go/go1.16-mod.md",
    "content": "---\ntitle: \"Go1.16 新特性：Go mod 的后悔药，仅需这一招\"\ndate: 2021-04-05T16:00:13+08:00\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n\n前几天 Go 官方正式发布了 1.16 版本。**从这个版本起，环境变量 GO111MODULE 的默认值正式修改为 on**。\n\n![](https://imgkr2.cn-bj.ufileos.com/64ac411e-6361-4b23-b437-78123e16ae5a.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=NWVlX2CGAsMjMs%252BJH54ZKW1s1fM%253D&Expires=1613623986)\n\n这也意味着 Go modules 将更进一步推进其业务覆盖面，有新老项目共存的小伙伴建议手动将 GO111MODULE 调整为 auto。\n\n**Go1.16 针对 Go modules 放出了一个新特性，能够让维护 Go mod 库的开发者拥有反复吃 “后悔药” 的权力，提醒开发者已发布的 “脏” 版本存在问题**。\n\n这个新特性，对于许多维护和使用公共库（开源、企业等）的小伙伴简直是一个小福音，建议大家都应该要了解这个知识点。\n\n在接下来文章中将进行详细说明和介绍。\n\n## 后悔药：Go mod retract\n\nGo1.16 起可以在 go.mod 文件中使用 `retract` 指令来声明该第三方模块的某些发行版本不能被其他模块使用。\n\n在使用场景上：在发现严重问题或无意发布某些版本后，模块的维护作者可以撤回该版本，支持撤回单个或多个版本。\n\n以前没有办法解决，因此一旦出现就非常麻烦。对应两者的操作如下：\n\n- 维护者：\n    - 删除有问题版本的 tag。\n    - 重新打一个新版本的 tag。\n- 使用者：\n    - 发现有问题的版本 tag 丢失，需手动介入。\n    - 不知道有问题，由于其他库依赖，因此被动升级而踩坑。\n\n因此在本次 Go1.16 发布后，就拥有了一个**半止损**的新手段了，也可以作为 Go mod 自动更新的大坑的补全办法之一。\n\n## 实战演练\n\n为了方便演示，首先创建一个 Demo 项目（github.com/eddycjy/go-retract-demo），其含有一个基础方法：\n\n```\npackage go_retract_demo\n\nfunc HelloWorld() string {\n\treturn \"001：脑子进煎鱼了！\"\n}\n```\n另外有一个应用工程依赖了该第三方库，代码如下：\n\n```\nfunc main() {\n  // import demo \"github.com/eddycjy/go-retract-demo\"\n\ts := demo.HelloWorld()\n\tfmt.Println(s)\n}\n```\n\n对应的 go.mod 文件如下：\n\n```\nmodule github.com/eddycjy/awesomeProject\n\ngo 1.16\n\nrequire github.com/eddycjy/go-retract-demo v0.0.1\n```\n\n### retract 特性演示\n\n但随着时间不断推移，第三方开源库 `eddycjy/go-retract-demo` 即将迭代到 `v0.3.0` 时，发现以往的 `v0.2.0` 是有 BUG 的。\n\n需要紧急的在`v0.3.0` 版本把这个 BUG 修复并提醒出去。此时可以在 `go.mod` 文件中写入 `retract` 指令：\n\n```\nmodule github.com/eddycjy/go-retract-demo\n\ngo 1.16\n\n// 因为煎鱼不小心敲错了...\nretract v0.2.0\n```\n指令上面为撤回的原因，后面是具体的版本。如果涉及多版本，可以如下编写：\n\n```\nretract (\n  v0.1.0\n  v0.2.0\n)\n```\n\n### retract 特性效果\n\n成功发布最新版本 `v0.3.0` 版本并指定 `retract` 后。\n所有引用了该库的工程应用，执行 `go list` 就可以看到如下提醒：\n\n```\n$ go1.16 list -m -u all\ngithub.com/eddycjy/awesomeProject\ngithub.com/eddycjy/go-retract-demo v0.2.0 (retracted) [v0.3.0]\n```\n\n结合该命令，我们日常使用的 IDE（例如：GoLand），其在保存时是会默认执行 `go list` 命令的。**在后续 IDE 支持后，就可以在编码时就快速发现有问题的版本和提示**。\n\n在手动执行 `go get` 时也会出现 `warning` 提示，会把 go.mod 文件上的原因注释显示出来：\n\n```\n$ go1.16 get github.com/eddycjy/go-retract-demo@v0.2.0\ngo: warning: github.com/eddycjy/go-retract-demo@v0.2.0: retracted by module author: 因为煎鱼不小心敲错了...\ngo: to switch to the latest unretracted version, run:\n\tgo get github.com/eddycjy/go-retract-demo@latest\n```\n这样就能看到是哪个模块依赖，因为什么原因要求撤回了，非常直观。\n\n## 总结\n\n以往在出问题后每个个体需要跑去问维护者或者看 GitHub Commits，那样总归非常麻烦，很可能一来一回半个钟就没了。\n\n新特性给予了 Go modules 软撤回版本的一个方法，能够把问题更直观的反馈到开发者的手中，再结合日常开发工具的话更是美哉。\n\n但这个特性的完全应用目前也是有一定的阻碍的：\n\n- ~~国内模块代理：需要国内的模块代理也支持 retract ，否则即使你更新了版本也没有提示处理~~。\n- IDE：IDE 针对 retract 做一些支持，例如：文字颜色标红、黄等，能够便于开发者更好的识别。\n\n你对 Go modules 的 retract 特性怎么看，欢迎一起留言讨论！"
  },
  {
    "path": "content/posts/go/go11.md",
    "content": "---\ntitle: \"Go 语言今年 11 岁，何去何从，现状到底如何？\"\ndate: 2020-11-11T21:21:58+08:00\ntoc: false\nimages:\ntags: \n  - go\n---\n\n不说不知道，一说下一跳。Go 语言已经开源 11 周年了，感觉是一路高歌，Release History （polarisxu 整理）如下：\n\n- 2011 年 3 月 16 日，Go 语言的第一个稳定版本 r56 发布；\n- 2012 年 3 月  28 日，Go 语言的第一个正式版本 Go1 发布，并承诺 1.x 的 兼容性；\n- 2013 年  5 月 13 日，Go1.1 正式版才发布。\n- 2013 年 12 月 1 日，Go1.2 正式发布；\n- 2014 年 6 月 18 日，Go1.3 正式发布；\n- 2014 年 12 月 10 日，Go1.4 正式发布；\n- 2015 年 8 月 19 日，Go1.5 正式发布。该版本实现了自举，即移除了 C 代码，使用 Go 开发 Go 语言；\n- 2016 年 2 月 17 日，Go1.6 正式发布；\n- 2016 年 8 月 15 日，Go1.7 正式发布；引入 context 包；\n- 2017 年 2 月 17 日，Go1.8 正式发布；\n- 2017 年 8 月 24 日，Go1.9 正式发布；引入别名；\n- 2018 年 2 月 16 日，Go1.10 正式发布；\n- 2018 年 8 月 25 日，Go1.11 正式发布。开始强势支持 Go modules；\n- 2019 年 3 月 1 日，Go1.12 正式发布；\n- 2019 年 9 月 3 日，Go1.13 正式发布；\n- 2020 年 2 月 25 日，Go1.14 正式发布；goroutine 支持异步抢占调度；\n- 2020 年 8 月 11 日，Go1.15 正式发布；\n- 2021 年 2 月，预计 Go1.16 正式发布；将包含新的文件系统接口和支持在构建时的静态文件嵌入，以及链接器的重写，且正式对 Apple Silicon（GOARCH=arm64）Mac 进行支持。\n\n## 目视现在\n\n现在的 Go 语言在国内已经掀起了一浪又一浪的热潮，炒的非常火热。各大平台极客时间、拉勾教育、掘金小册、慕课网等纷纷出现了大量 Go 语言相关的付费专栏/视频。\n\n在现实工作层面，字节跳动、腾讯向 Go 语言侧偏，以及其它各大一二线厂均出现了不少 Go 的岗位，也正预示着当前已经到了一个比较好风口。\n\n在开源项目层面，Kubernetes、Etcd、Prometheus、Docker 等大量的云原生相关组件均以 Go 语言开发，懂一门 Go 语言，排查问题也更方便了。\n\n与企业开发层面，出现了大量其他语言的开发者向 Go 语言转型，在企业的软件开发中出现，新项目用 Go 语言，老项目保留，形成同时维护新老系统，再渐迁的绞杀者模式：\n\n![绞杀者模式](https://image.eddycjy.com/e3789c8026e3e2684f640309f119213a.png)\n\n在面试中比较常见的是 PHP、C++ 语言，就会在企业中形成了 Go+PHP（新+老系统）的局面，又或是 Go 调 CGO 的运行模式。这也得益于 Go 语言的易用性和一定的胶水特性。\n\n在培训机构层面，各大机构都多少曾经向 Go 语言发起过进攻，但目前 Go 语言大多以中高级人才为主，也就是有过其他语言经验的软件开发从业者为主。因此培训机构的市场行情相对较差。\n\n在社会招聘和岗位层面，狭义上来看，与 2018 年我写的 《带你了解一下Golang的市场行情》基本情况仍保持一致：\n\n![image](https://camo.githubusercontent.com/710fc8e25ba15c8b3802d7a33673f798ee0e28abb51ce49743d7348ad8ebb062/68747470733a2f2f692e6c6f6c692e6e65742f323031382f30342f32372f356165323936623735306464382e706e67)\n\n以下为 GoCN 所收集的 “2020 中国Go 开发者调查报告” 的地域分布：\n\n![image](https://static.gocn.vip/photo/2020/16c7f28a-280f-4c30-acd1-81d9b74c3e85.png?x-oss-process=image/resize,w_1920)\n\n目前 Go 语言的大热门地区依然是：北京、上海、深圳，主体集中在一线城市，机会这里最多。\n\n## 看看数据：TIOBE\n\n从 TIOBE 的编程语言排行榜来看，整体上 Go 语言的热门程度并不会特别高（与老牌语言相比），但作为一门编程语言在短短 11 年内已有很不错的表现：\n\n![图来自 GoCN](https://image.eddycjy.com/24b3917d52a3549b598b72932c9d34c9.jpg)\n\n同时业内时常说 Go 语言要干掉 PHP、C++、Java 等，目前来看短期内不现实，官方也没有这方面打算，因为合适的场景选择合适的语言就好了。\n\nTIOBE 提示本月的排名在第 13 名，且最高排名出现在 2020 年 5 月，在第 10 名，近期基本稳定在这个位数附近。至少近年是干不掉老大哥们的，但最近接触的一个运营大佬称其为 “准备霸占未来语言半壁江山” 的语言，你觉得呢？\n\n## 展望未来\n\nGo1 目前一如既往的遵守了 [Go1 兼容性承诺](https://tip.golang.org/doc/go1compat)，这给不少正在使用 Go 语言的企业带来了一注强心针。但给 Go 语言也带来了一些 “麻烦”。那就是存在破坏性变更的变动无法在 Go1 中实现。\n\n因此为了解决一些 ”问题“，也想达到更好的特性目标。2018 年时释出了 Go2 的计划，详细可参见[Go 2, here we come!](https://blog.golang.org/go2-here-we-come)，其中包含了大量的功能特性。\n\n从目前的基本论调和实际情况来看，可兼容实现的，都会在 Go1 实现，例如大家最期待的功能之一 ”泛型“，预计最早会在 Go1.17 会释出，样例：\n\n```\n// Print prints the elements of any slice.\n// Print has a type parameter T and has a single (non-type)\n// parameter s which is a slice of that type parameter.\nfunc Print[T any](s []T) {\n\t// same as above\n}\n```\n\n其在 6 月下旬发布了最新的设计草稿，若对泛型有更进一步需求可关注 [design/go2draft-type-parameters](https://github.com/golang/proposal/blob/master/design/go2draft-type-parameters.md)，而一些不兼容的修改，若确切评估后无法直接实现的，将会到 Go2 的 计划中去：\n\n![image](https://image.eddycjy.com/eb173beda6d6f989c65d28b0129edd1c.jpg)\n\n并且相信 Go2 发布时，肯定也不是 `go run xxx` 了，估计会变更命令集，以示区分。\n\n## 总结\n\n11 岁，Go 语言目前在国内已经火起来了，但现阶段的 ”成功“ 并不代表后续一定持续强劲，背后离不开所有开发者在社区开源的努力。我们一起思考如下问题：\n\n- 你最喜欢 Go 语言哪些方面？\n\n- 你认为 Go 语言目前还有哪些问题呢？ 期望他解决哪一块内容？\n\n- 如果 Go 语言想继续占领更多的语言市场，需要在什么领域发力？\n\n以更具现化的 TLOBE Index for Go 趋势图来看：\n\n![image](https://image.eddycjy.com/a8f3d88382c1473df54458a5aef80eaa.jpg)\n\n你认为 2020 年后 Go 语言的走向会是怎么样，现在适合 “抄底” 吗？\n"
  },
  {
    "path": "content/posts/go/go16-preview.md",
    "content": "---\ntitle: \"为什么 Go 的泛型一拖再拖？\"\ndate: 2020-11-12T23:47:16+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。前段时间 Go 语言的泛型讨论频频出现在各微信群，且又冲上了国内外各大文章的 “头条”：\n\n![来自 p 神公众号的截图](https://image.eddycjy.com/c9c48e9479c7036f7d5a33b6ab49e855.jpg)\n\n信息汇总来看，Go 泛型这几年会出，但大体来讲现在 Go 泛型又又又推迟了，好家伙。我最早了解到时是考虑 Go1.16 释出，后面又推到了 Go1.17，接着现在又延期到了 Go1.18 了（2021 年底）。\n\n看到了信息的表象后，再想想为什么泛型 “这件事情” 突然醒目起来了，其原因之一是由官方 [Go，11 岁](https://blog.golang.org/11years) 的博文所引爆的。\n\n同时近日举办的 GopherCon2020 大会，Robert Griesemer 分享的 Typing [Generic] Go。更正式的让 Go 泛型更面向了大众，也侧面的说明官方认为其已经到达了一个新的阶段了，进入最终实现阶段。\n\n事不宜迟，既然官方都已经摩拳擦掌了，我们的学习之路也得跟上，因此本文将会介绍 Go 泛型现在的情况，并通过在介绍过程中不断思考最后得出一个为什么。\n\n## 什么是泛型\n\n泛型程序设计（generic programming）是程序设计语言的一种风格或范式。泛型允许程序员在强类型语言中编写代码时，使用一些以后才确定的类型，其在真正实例化时才会为这些参数指确定类型。另外各语言和其编译器、运行环境对泛型的支持均不一样，因此需要针对来辩证。\n\n简单来讲，泛型就是参数化多态。其可根据实参类型生成不同的版本，支持任意数量的调用：\n\n```\nfunc F(a, b T) T{ return a+b }\n\n// T 为 int\nF(1, 2)\n\n// T 为 string\nF(\"1\", \"2\")\n```\n\n在编译时期编译器便确定其 T 的入参类型。这也是 Go 泛型实现的要求之一 “编译时类型安全”。\n\n## 为什么需要泛型\n\n这时候可能会有人说，没有泛型也可以啊...感觉写业务代码没什么影响，与其搞泛型不如搞好 errors（具体新消息可参见：重磅：Go errors 将不会有任何进一步的改进计划）。\n\n但泛型是有其所需的场景，最常见的是像基础库在处理获取配置中心数据时，就要处理类型，时常遇到下述场景：\n\n![image](https://image.eddycjy.com/4d630c956a58bd4b88a4a6e0cddbb845.gif)\n\n如果使用接口（interface）类型来做，也得 `switch.(type)` 枚举出所有的基础类型。这显然并不合理，也没法做太复杂的逻辑，而且所支持的类型还泄露。\n\n另外同时单从语言层面来讲，泛型支持是一个必然事件了，因为泛型的存在对解决特定领域的问题存在一定的意义。\n\n## 接口和泛型有什么区别\n\n在上面我们有提到接口（interface）类型，这时候就出现了泛型的第二个经典问题。那就是 “接口和泛型有什么区别？”，为什么不用接口来实现 “泛型”：\n\n```\ntype T interface { ... }\nfunc F(a, b T) T { return a+b }\n```\n\n也像这么一回事，但在这里存在一个致命的缺陷。那就是接口的入参和出参均可以在运行时表现为不同的类型：\n\n```\nF(\"煎鱼\", 233)\n```\n\n要做好，还得依靠内部去对参数进行断言，否则作为 string 类型的煎鱼又如何和 int 类型的 233 相加呢，那是必然报错的。\n\n而反过来看真 “泛型” 的实际使用，编译器会保证泛型函数的入参和出参必须为同一类型，有强制性的检验：\n\n```\n// 报错：type checking failed for main\nF(\"煎鱼\", 233)\n\n// 必须为同一类型，才能正常运行\nF(666, 233)\n```\n\n两者存在本质上的区别，泛型会更安全，能够保证编译早期就发现错误，而不是等到运行时（并且可能会存在隐性的 BUG）。\n\n总体来讲，泛型相较接口有如下优点：\n\n- 更安全：编译早期就能发现错误。\n\n- 性能好：静态类型。\n\n## 过去：为什么那么久都没有泛型\n\n前几段在社区的微信群看到一位小伙伴吐槽 “Go 语言居然没有泛型？”，变相来看，可能其会认为 ”Go 都已经 11 岁了，2020 年了居然还没有泛型？”。\n\n这显然是不对的，因为泛型本质上并不是绝对的必需品，更不是 Go 语言的早期目标，因此在过往的发展阶段没有过多重视这一点，而是把精力放在了其他 feature 上。\n\n另外 Go 语言在以往其实进行过大量的泛型 proposal 试验，基本时间线（via @changkun）如下：\n\n|  简述   | 时间  | 作者 |\n|  ----  | ----  | --- |\n| [Type Functions]  | 2010年 | Ian Lance Taylor |\n| Generalized Types  | 2011年 | Ian Lance Taylor |\n| Generalized Types v2  | 2013年 | Ian Lance Taylor |\n| Type Parameters  | 2013年 | Ian Lance Taylor |\n| go:generate  | 2014年 | Rob Pike |\n| First Class Types  | 2015年 | Bryan C.Mills |\n| Contracts  | 2018年 | Ian Lance Taylor, Robert Griesemer |\n| Contracts  | 2019年 | Ian Lance Taylor, Robert Griesemer  |\n| Redundancy in Contracts(2019)'s Design  | 2019年 | Ian Lance Taylor, Robert Griesemer |\n| Constrained Type Parameters(2020, v1)  | 2020年 | Ian Lance Taylor, Robert Griesemer |\n| Constrained Type Parameters(2020, v2)  | 2020年 | Ian Lance Taylor, Robert Griesemer |\n| Constrained Type Parameters(2020, v3)  | 2020年 | Ian Lance Taylor, Robert Griesemer |\n\n虽然偶有中断，但仔细一看，2010 年就尝试过，现在 2020 年了，也是很励志了，显然官方也是在寻路和尝试的过程中，但一直没有找到相较好的方案，争端过多了。\n\n## 现在：Go 泛型\n\n泛型尝鲜的方式有两种方式。线上 Ian Lance Taylor 提供了一个在线编译的 [go2go](https://go2goplay.golang.org/)：\n\n![image](https://image.eddycjy.com/0609310f0a775b57fe017f56c1e50195.jpg)\n\n另外一种是线下，也就在本地安装 Go 的特定分支版本：\n\n```\n$ git clone https://github.com/golang/go\n$ git checkout dev.go2go\n$ cd src && ./all.bash\n```\n\n不过这种本地安装的方法会耗时比较久，初步尝试的话建议使用 go2go 就可以了。而在尝鲜时，可以看到在代码块中声明了一个 `Print` 方法，其函数签名主体分为三部分：\n\n![image](https://image.eddycjy.com/5fc715bb226563645dfc6bb4da210c84.jpg)\n\n咋一看，变量 T 的这个关键字 `any` 是什么？早期泛型你可能有听说合约（Contract），难道这就是合约。其实严格意义上来讲并不是，因为为了更一步简化语法，合约在 2020.06.07 已经正式移除。\n\n其已改头换面，现在只需要写参数化的 interface。而上述的 `any` 关键字是一个预定义的类型约束，声明后将允许任何类型用作类型实参，并且允许函数使用用于任何类型的操作。\n\n从语法分析的角度来讲，`Print` 方法一共包含了如下属性（从左到右）：\n\n- type list：声明了入参的类型列表为一个 `T` 变量，其可以传任意类型的参数。\n\n- parameter list：声明了入参的参数列表为 `T` 变量的切片，且形参为 `s`。\n\n- return type list：声明了函数的返回参数列表。\n\n上述函数签名便是一个 Go 泛型的基本样子，由于本文并不是 CRUD 泛型，便不展开案例，若大家有兴趣可以详细阅读提案：[Type Parameters - Draft Design](https://github.com/golang/proposal/blob/master/design/go2draft-type-parameters.md)。\n\n## 泛型的战争\n\n### 为什么不用尖括号\n\n在社区中很多同学在讨论的一个问题，那就是 “为什么 Go 泛型不像 C++ 和 Java 那样使用尖括号？，也出现了 “Go 一直标榜业界工程实践类的榜样，为什么就是不用尖括号” 的言论？\n\n思考问题我们不只看表面，官方说不行，那么我们可以倒推来看，看看 Go 语言就用尖括号：\n\n```\nfunc print<type T>(list []T) {\n\nprint<int>(numbers)\nprint<string>(strings)\nprint<float64>(floats)\n```\n\n普通的函数声明看上去似乎结构清晰，没有什么大问题的。接着往下看：\n\n```\na := w < x\nb := y > (z)\n```\n\n我们继续把代码演进一下，简洁一点：\n\n```\na, b := w < x, y > (z)\n```\n\n这时候就犯难了，不仅编译器难以解析，人也很难判别，到底指的是：\n\n```\na := w < x\nb := y > (z)\n```\n\n又或是：\n\n```\na, b := w<x, y>(z)\n```\n\n从上述代码来看，使用尖括号难以分别，因为没有类型信息，就无法确定赋值的右侧是一对表达式 `w < x和y > (z)`，还是返回两个结果值 `w<x, y>(z)` 的泛型函数实例化和调用，其存在歧义。\n\n要解决还要引入新的约束，会破坏 Go1 的兼容性承诺，这显然是不合理的。\n\n### 为什么不用括号\n\n其实最早 Go 泛型的版本是使用了括号的模式，虽然能用，但是用括号会引入新的解析歧义。例如：\n\n```\nvar f func(x(T))\n```\n\n从语法上来讲，你无法识别他是未命名参数的 `x(T)` 函数，还是类型名为参数的 `(T)` 函数。同时 Go 语言还存在强制类型转换这一语法，假设代码是 `[]T(v1)` 和 `[]T(v2){}` ，那么你在开括号处，就无法得知其是否代表类型转换。\n\n更甚至在函数的完整声明上，我们都会感到困惑：\n\n```\nfunc F(T any)(v T)(r1, r2 T)\n```\n\n函数入参、泛型、返回值声明均都是括号，造成了语义不清，这显然也是不合理的。\n\n### 为什么不用书名号（«»）\n\n想的美，并不想使用非 ASCII，未来更没打算支持。\n\n## 总结\n\n在本文中我们从多个维度介绍了 Go 泛型的相关内容，既了解到了上段时间 Go 泛型再度火爆的信息来源是什么。也知道了 Go 泛型是什么，与接口的区别。\n\n同时我们还针对业界常见的一些疑问，例如接口和泛型的区别，泛型的历史，泛型的尖括号/括号/书名号之争进行了解释和说明。\n\n最后我们回答一下最开始的疑问，”为什么 Go 的泛型一拖再拖“，主要如下：\n\n- Go 语言的早期目标（工作重点）并不是泛型。\n\n- Go 语言在 2010-2020 年都有间断在做 Go 泛型的 proposal，但总是 ”失败“，在不断地吸收经验。\n\n- Go 语言社区的意见反馈是真的多，单用什么符号表示泛型，不想要泛型都争论不休。\n\n- Go 语言的泛型现在还不成熟，很多​细节其实并没有支持好。\n\n很显然，在保证 Go1 向后兼容性的同时，Go 官方也不想直接妥协出一个随便的方案，因此总是不断地在改进。随着 Go 语言的不断应用，泛型也和 errors 一样被推上风头浪尖。\n\n## 到底拖到什么时候\n\n那 Go 泛型到底什么时候出呢？\n\n前段时间也向欧神（@changkun）了解到在 GopherCon 2020 Go Team AMA，russ cox 有聊到相关问题，表示在明年年底要能有生产环境的试用版上线，这是一个管理目标。\n\n但具体真正的时间线肯定是要看泛型的实现者：robert 和 keith，可以多多关注他们，就能拿到一手信息，且可以确定的是 Go 泛型明年二月之前是不会有生产可用的试用版。\n\n**灵魂拷问：你对 Go 语言的泛型又有什么想法和意见呢，一起留言讨论吧。**\n\n## 推荐阅读\n\n- [欧神：第 80 期 2020-03-18 带你提前玩 Go 2 新特性：泛型](https://talkgo.org/t/topic/99)\n- [提案：Type Parameters - Draft Design](https://github.com/golang/proposal/blob/master/design/go2draft-type-parameters.md)\n"
  },
  {
    "path": "content/posts/go/go2-errors.md",
    "content": "---\ntitle: \"先睹为快，Go2 Error 的挣扎之路\"\ndate: 2020-12-03T20:56:47+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n自从 Go 语言在国内火热以来，除去泛型，其次最具槽点的就是 Go 对错误的处理方式，一句经典的 `if err != nil`  暗号就能认出你是一个 Go 语言爱好者。\n\n![image](https://image.eddycjy.com/381fcb5e85923479666f5be14de3782c.jpeg)\n\n自然，大家对 Go error 的关注度更是高涨，Go team 也是，因此在 [Go 2 Draft Designs](https://github.com/golang/proposal/blob/master/design/go2draft.md) 中正式提到了 error handling（错误处理）的相关草案，希望能够在未来正式的解决这个问题。\n\n在今天这篇文章中，我们将一同跟踪 Go2 error，看看他是怎么 “挣扎” 的，能不能破局？\n\n## 为什么要吐槽 Go1 \n\n要吐槽 Go1 error，就得先知道为什么大家到底是在喷 Error 哪里处理的不好。在 Go 语言中，error 其实本质上只是个 Error 的 `interface`：\n\n```\ntype error interface {\n    Error() string\n}\n```\n\n实际的应用场景如下：\n\n```\nfunc main() {\n\tx, err := foo()\n\tif err != nil {\n\t\t // handle error\n\t}\n}\n```\n\n单纯的看这个例子似乎没什么问题，但工程大了后呢？显然 `if err != nil` 的逻辑是会堆积在工程代码中，Go 代码里的 `if err != nil` 甚至会达到工程代码量的 30% 以上：\n\n```\nfunc main() {\n\tx, err := foo()\n\tif err != nil {\n\t\t // handle error\n\t}\n\ty, err := foo()\n\tif err != nil {\n\t\t // handle error\n\t}\n\tz, err := foo()\n\tif err != nil {\n\t\t // handle error\n\t}\n\ts, err := foo()\n\tif err != nil {\n\t\t // handle error\n\t}\n}\n```\n\n暴力的对比一下，就发现四行函数调用，十二行错误，还要苦练且精通 IDE 的快速折叠功能，还是比较麻烦的。\n\n另外既然是错误处理，那肯定不单单是一个 `return err` 了。在工程实践中，项目代码都是层层嵌套的，如果直接写成：\n\n```\nif err != nil {\n\treturn err\n}\n```\n\n在实际工程中肯定是不行。你怎么知道具体是哪里抛出来的错误信息，实际出错时只能瞎猜。大家又想出了 PlanB，那就是加各种描述信息：\n\n```\nif err != nil {\n\tlogger.Errorf(\"煎鱼报错 err：%v\", err)\n\treturn err\n}\n```\n虽然看上去人模人样的，在实际出错时，也会遇到新的问题，因为你要去查这个错误是从哪里抛出来的，单纯几句错误描述是难以定位的。这时候就会发展成**到处打错误日志**：\n\n```\nfunc main() {\n\terr := bar()\n\tif err != nil {\n\t\tlogger.Errorf(\"bar err：%v\", err)\n\t}\n\t...\n}\n\nfunc bar() error {\n\t_, err := foo()\n\tif err != nil {\n\t\tlogger.Errorf(\"foo err：%v\", err)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc foo() ([]byte, error) {\n\ts, err := json.Marshal(\"hello world.\")\n\tif err != nil {\n\t\tlogger.Errorf(\"json.Marshal err：%v\", err)\n\t\treturn nil, err\n\t}\n\n\treturn s, nil\n}\n```\n\n虽然到处打了日志，就会变成错误日志非常多，一旦出问题，人肉可能短时间内识别不出来。且最常见的就是到 IDE 上 `ctrl + f` 搜索是在哪出错，同时在我们常常会自定义一些错误类型，而在 Go 则需要各种判断和处理：\n\n```\nif err := dec.Decode(&val); err != nil {\n    if serr, ok := err.(*json.SyntaxError); ok {\n       ...\n    }\n    return err\n}\n```\n\n首先你得判断不等于 `nil`，还得对自定义的错误类型进行断言，整体来讲比较繁琐。\n\n汇总来讲，Go1 错误处理的问题至少有：\n\n- 在工程实践中，`if err != nil` 写的烦，代码中一大堆错误处理的判断，占了相当的比例，不够优雅。\n\n- 在排查问题时，Go 的 `err` 并没有其他堆栈信息，只能自己增加描述信息，层层叠加，打一大堆日志，排查很麻烦。\n\n- 在验证和测试错误时，要自定义错误（各种判断和断言）或者被迫用字符串校验。\n\n## Go1.13 的挽尊\n\n在 2019 年 09 月，Go1.13 正式发布。其中两个比较大的两个关注点分别是包依赖管理 Go modules 的转正，以及错误处理 errors 标准库的改进：\n\n![image](https://image.eddycjy.com/51485fae58cbf9bd92aa19686caf5a27.jpg)\n\n在本次改进中，errors 标准库引入了 Wrapping Error 的概念，并增加了 Is/As/Unwarp 三个方法，用于对所返回的错误进行二次处理和识别。同时也是将 Go2 error 预规划中没有破坏 Go1 兼容性的相关功能提前实现了。\n\n简单来讲，Go1.13 后 Go 的 error 就可以嵌套了，并提供了三个配套的方法。例子：\n\n```\nfunc main() {\n\te := errors.New(\"脑子进煎鱼了\")\n\tw := fmt.Errorf(\"快抓住：%w\", e)\n\tfmt.Println(w)\n\tfmt.Println(errors.Unwrap(w))\n}\n```\n\n输出结果：\n\n```\n$ go run main.go\n快抓住：脑子进煎鱼了\n脑子进煎鱼了\n```\n\n在上述代码中，变量 `w` 就是一个嵌套一层的 error。最外层是 “快抓住：”，此处调用 `%w` 意味着 Wrapping Error 的嵌套生成。因此最终输出了 “快抓住：脑子进煎鱼了”。\n\n需要注意的是，Go 并没有提供 `Warp` 方法，而是直接扩展了 `fmt.Errorf` 方法。而下方的输出由于直接调用了 `errors.Unwarp` 方法，因此将 “取” 出一层嵌套，最终直接输出 “脑子进煎鱼了”。\n\n对 Wrapping Error 有了基本理解后，我们简单介绍一下三个配套方法：\n\n```\nfunc Is(err, target error) bool\nfunc As(err error, target interface{}) bool\nfunc Unwrap(err error) error\n```\n\n### errors.Is\n\n方法签名：\n\n```\nfunc Is(err, target error) bool\n```\n\n方法例子：\n\n```\nfunc main() {\n\tif _, err := os.Open(\"non-existing\"); err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\tfmt.Println(\"file does not exist\")\n\t\t} else {\n\t\t\tfmt.Println(err)\n\t\t}\n\t}\n\n}\n```\n\n`errors.Is` 方法的作用是判断所传入的 err 和 target 是否同一类型，如果是则返回 true。\n\n### errors.As\n\n方法签名：\n\n```\nfunc As(err error, target interface{}) bool\n```\n\n方法例子：\n\n```\nfunc main() {\n\tif _, err := os.Open(\"non-existing\"); err != nil {\n\t\tvar pathError *os.PathError\n\t\tif errors.As(err, &pathError) {\n\t\t\tfmt.Println(\"Failed at path:\", pathError.Path)\n\t\t} else {\n\t\t\tfmt.Println(err)\n\t\t}\n\t}\n\n}\n```\n\n`errors.As` 方法的作用是从 err 错误链中识别和 target 相同的类型，如果可以赋值，则返回 true。\n\n### errors.Unwarp\n\n方法签名：\n\n```\nfunc Unwrap(err error) error\n```\n\n方法例子：\n\n```\nfunc main() {\n\te := errors.New(\"脑子进煎鱼了\")\n\tw := fmt.Errorf(\"快抓住：%w\", e)\n\tfmt.Println(w)\n\tfmt.Println(errors.Unwrap(w))\n}\n```\n\n该方法的作用是将嵌套的 error 解析出来，若存在多级嵌套则需要调用多次 Unwarp 方法。\n\n## 民间自救 pkg/errors\n\nGo1 的 error 处理固然存在许多问题，因此在 Go1.13 前，早已有 “民间” 发现没有上下文调试信息在实际工程应用中存在严重的体感问题。因此 `github.com/pkg/errors` 在 2016 年诞生了，目前该库也已经受到了极大的关注。\n\n官方例子如下：\n\n```\ntype stackTracer interface {\n    StackTrace() errors.StackTrace\n}\n\nerr, ok := errors.Cause(fn()).(stackTracer)\nif !ok {\n    panic(\"oops, err does not implement stackTracer\")\n}\n\nst := err.StackTrace()\nfmt.Printf(\"%+v\", st[0:2]) // top two frames\n\n// Example output:\n// github.com/pkg/errors_test.fn\n//\t/home/dfc/src/github.com/pkg/errors/example_test.go:47\n// github.com/pkg/errors_test.Example_stackTrace\n//\t/home/dfc/src/github.com/pkg/errors/example_test.go:127\n```\n\n简单来讲，就是对 Go1 error 的上下文处理进行了优化和处理，例如类型断言、调用堆栈等。若有兴趣的小伙伴可以自行到 `github.com/pkg/errors` 进行学习。\n\n另外你可能会发现 Go1.13 新增的 Wrapping Error 体系与 `pkg/errors` 有些相像。你并没有体会错，Go team 接纳了相关的意见，对 Go1 进行了调整，但调用堆栈这块因综合原因暂时没有纳入。\n\n## Go2 error 要解决什么问题\n\n在前面我们聊了 Go1 error 的许多问题，以及 Go1.13 和 `pkg/errors` 的自救和融合。你可能会疑惑，那...Go2 error 还有出场的机会吗？即使 Go1 做了这些事情，Go1 error 还有问题吗？\n\n并没有解决，`if err != nil` 依旧一把梭，目前社区声音依然认为 Go 语言的错误处理要改进。\n\n## Go2 error proposal\n\n在 2018 年 8 月，官方正式公布了 [Go 2 Draft Designs](https://go.googlesource.com/proposal/+/master/design/go2draft.md)，其中包含泛型和错误处理机制改进的初步草案：\n\n![image](https://image.eddycjy.com/48b07b14442b1832c09eb6e2bc35fb6b.jpg)\n\n注：Go1.13 正式将一些不破坏 Go1 兼容性的 Error 特性加入到了 main branch，也就是前面提到的 Wrapping Error。\n\n### 错误处理（Error Handling）\n\n第一个要解决的问题就是大量 `if err != nil` 的问题，针对此提出了 [Go2 error handling](https://github.com/golang/proposal/blob/master/design/go2draft-error-handling-overview.md) 的草案设计。\n\n简单例子：\n\n```\nif err != nil {\n\treturn err\n}\n```\n\n优化后的方案如下：\n\n```\nfunc CopyFile(src, dst string) error {\n\thandle err {\n\t\treturn fmt.Errorf(\"copy %s %s: %v\", src, dst, err)\n\t}\n\n\tr := check os.Open(src)\n\tdefer r.Close()\n\n\tw := check os.Create(dst)\n\thandle err {\n\t\tw.Close()\n\t\tos.Remove(dst) // (only if a check fails)\n\t}\n\n\tcheck io.Copy(w, r)\n\tcheck w.Close()\n\treturn nil\n}\n```\n\n主函数：\n\n```\nfunc main() {\n\thandle err {\n\t\tlog.Fatal(err)\n\t}\n\n\thex := check ioutil.ReadAll(os.Stdin)\n\tdata := check parseHexdump(string(hex))\n\tos.Stdout.Write(data)\n}\n```\n\n该提案引入了两种新的语法形式，首先是 `check` 关键字，其可以选中一个表达式 `check f(x, y, z)` 或 `check err`，其将会标识这是一个显式的错误检查。\n\n其次引入了 `handle` 关键字，用于定义错误处理程序流转，逐级上抛，依此类推，直到处理程序执行 `return` 语句，才正式结束。\n\n### 错误值打印（Error Printing）\n\n第二个要解决的问题是错误值（Error Values）、错误检查（Error Inspection）的问题，其引申出错误值打印（Error Printing）的问题，也可以认为是错误格式化的不便利。\n\n官方针对此提出了提出了 [Error Values](https://github.com/golang/proposal/blob/master/design/go2draft-error-values-overview.md) 和 [Error Printing](https://github.com/golang/proposal/blob/master/design/go2draft-error-printing.md) 的草案设计。\n\n简单例子如下：\n\n```\nif err != nil {\n\treturn fmt.Errorf(\"write users database: %v\", err)\n}\n```\n\n优化后的方案如下：\n\n```\npackage errors\n\ntype Wrapper interface {\n\tUnwrap() error\n}\n\nfunc Is(err, target error) bool\nfunc As(type E)(err error) (e E, ok bool)\n```\n\n该提案增加了错误链的 Wrapping Error 概念，并同时增加 `errors.Is` 和 `errors.As` 的方法，与前面说到的 Go1.13 的改进一致，不再赘述。\n\n需要留意的是，Go1.13 并没有实现 `%+v` 输出调用堆栈的需求，因为此举会破坏 Go1 兼容性和产生一些性能问题，大概会在 Go2 加入。\n\n## try-catch 不香吗\n\n社区中另外一股声音就是直指 Go 语言反人类不用 `try-catch` 的机制，在社区内也产生了大量的探讨，具体可以看看相关的提案 [Proposal: A built-in Go error check function, \"try\"](https://github.com/golang/go/issues/32437)。\n\n目前该提案已被拒绝，具体可参见 [go/issues/32437#issuecomment-512035919](https://github.com/golang/go/issues/32437#issuecomment-512035919) 和 [Why does Go not have exceptions](https://golang.org/doc/faq#exceptions)。\n\n## 总结\n\n在这篇文章中，我们介绍了目前 Go1 Error 的现状，概括了大家对 Go 语言错误处理的常见问题和意见。同时还介绍了在这几年间，Go team 针对 Go2、Go1.13 Error 的持续优化和探索。\n\n如果是你，你会怎么去优化目前 Go 语言的错误处理机制呢，现在 Go2 error proposal 你又是否认可？\n\n## 参考\n\n- [Golang error 的突围](https://qcrao.com/2019/09/18/golang-error-break-through)\n\n- [为什么 Go 语言的 Error Handling 是一个败笔](https://www.zhihu.com/question/330263279)\n\n- [Go语言(golang)新发布的1.13中的Error Wrapping深度分析](https://www.flysnow.org/2019/09/06/go1.13-error-wrapping.html)"
  },
  {
    "path": "content/posts/go/gophercon2020-errors.md",
    "content": "---\ntitle: \"重磅：Go errors 将不会有任何进一步的改进计划\"\ndate: 2020-11-14T16:48:33+08:00\nimages:\ntags: \n  - go\n---\n\n今天在 Gophercon2020 上，**Go 1.13 错误提案的作者事后提及他对目前错误格式化的缺失表示遗憾，而且在未来很长的好几年内都不会有任何进一步改进计划**。\n\n对此他本人给出的原因之一是：对于错误处理这一领域特定的问题，在他的能力范围内实在是无法给出一个令所有人都满意的方案。\n\n尽管如此，在他演讲的最后，还是给出了一些关于错误嵌套的建议，即实现 `fmt.Formatter`，图中给出了一个简单的例子，大家可以参考如下代码：\n\n```\ntype DetailError struct {\n\tmsg, detail string\n\terr         error\n}\n\nfunc (e *DetailError) Unwrap() error { return e.err }\n\nfunc (e *DetailError) Error() string {\n\tif e.err == nil {\n\t\treturn e.msg\n\t}\n\treturn e.msg + \": \" + e.err.Error()\n}\n\nfunc (e *DetailError) Format(s fmt.State, c rune) {\n\tif s.Flag('#') && c == 'v' {\n\t\ttype nomethod DetailError\n\t\tfmt.Fprintf(s, \"%#v\", (*nomethod)(e))\n\t\treturn\n\t}\n\tif !s.Flag('+') || c != 'v' {\n\t\tfmt.Fprintf(s, spec(s, c), e.Error())\n\t\treturn\n\t}\n\tfmt.Fprintln(s, e.msg)\n\tif e.detail != \"\" {\n\t\tio.WriteString(s, \"\\t\")\n\t\tfmt.Fprintln(s, e.detail)\n\t}\n\tif e.err != nil {\n\t\tif ferr, ok := e.err.(fmt.Formatter); ok {\n\t\t\tferr.Format(s, c)\n\t\t} else {\n\t\t\tfmt.Fprintf(s, spec(s, c), e.err)\n\t\t\tio.WriteString(s, \"\\n\")\n\t\t}\n\t}\n}\n\nfunc spec(s fmt.State, c rune) string {\n\tbuf := []byte{'%'}\n\tfor _, f := range []int{'+', '-', '#', ' ', '0'} {\n\t\tif s.Flag(f) {\n\t\t\tbuf = append(buf, byte(f))\n\t\t}\n\t}\n\tif w, ok := s.Width(); ok {\n\t\tbuf = strconv.AppendInt(buf, int64(w), 10)\n\t}\n\tif p, ok := s.Precision(); ok {\n\t\tbuf = append(buf, '.')\n\t\tbuf = strconv.AppendInt(buf, int64(p), 10)\n\t}\n\tbuf = append(buf, byte(c))\n\treturn string(buf)\n}\n```\n\n此处的内容来源于欧神（@changkun）在知识星球里的线上分享，作为 Go 夜读 SIG 成员的一员，借此也安利下咱们《Go 夜读》的星球，欢迎大家一起来学习和分享：\n\n![image](https://image.eddycjy.com/f474866fbaed634f83fa2e3228cfbec6.jpeg)\n\n## 讨论\n\nGo 语言的错误处理机制一直饱受争议，前段时间在 issues 中还长期争吵过一段时间，因此还是维持了目前 `if err != nil` 的方式，也没有什么大改动。\n\n我们再想想，像其他语言的 `try catch` 是一定好吗？毕竟 `try catch` 的方式也有很多人不看好。抛个砖，**如果是你，你会想怎么设计 Go 语言的错误机制？或者说你觉得怎么的处理才算好**？"
  },
  {
    "path": "content/posts/go/goroutine-27.md",
    "content": "---\ntitle: \"会诱发 Goroutine 挂起的 27 个原因\"\ndate: 2021-12-31T12:55:06+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n上个月面向读者的提问，我们针对 goroutine 泄露中都会看到的大头 runtime.gopark 函数进行了学习和了解，输出了 《[Goroutine 一泄露就看到他，这是个什么？](https://mp.weixin.qq.com/s/x6Kzn7VA1wUz7g8txcBX7A)》。\n\n有小伙伴提到，虽然我们知道了 runtime.gopark 函数的缘起和内在，但其实没有提到 **runtime.gopark 的诱发因素，这是我们日常编码中需要关注的**。\n\n今天这篇文章就和大家一起围观 gopark 的 27 个诱发场景。为了方便阅读，我们会根据分类进行说明。\n\n### 第一部分\n\n|  标识   |  含义   |\n| --- | --- |\n|   waitReasonZero  |   无  |\n|   waitReasonGCAssistMarking  |   GC assist marking  |\n|   waitReasonIOWait  |   IO wait |\n\n- waitReasonZero：无正式解释，从使用情况来看。主要在 sleep 和 lock 的 2 个场景中使用。\n- waitReasonGCAssistMarking：GC 辅助标记阶段会使得阻塞等待。\n- waitReasonIOWait：IO 阻塞等待时，例如：网络请求等。\n\n### 第二部分\n\n|  标识   |  含义   |\n| --- | --- |\n|   waitReasonChanReceiveNilChan  |   chan receive (nil chan)  |\n|   waitReasonChanSendNilChan  |   chan send (nil chan)  |\n\n- waitReasonChanReceiveNilChan：对未初始化的 channel 进行读操作。\n- waitReasonChanSendNilChan：对未初始化的 channel 进行写操作。\n\n### 第三部分\n\n|  标识   |  含义   |\n| --- | --- |\n|   waitReasonDumpingHeap  |  dumping heap |\n|   waitReasonGarbageCollection  |   garbage collection  |\n|   waitReasonGarbageCollectionScan  |   garbage collection scan  |\n\n- waitReasonDumpingHeap：对 Go Heap 堆 dump 时，这个的使用场景仅在 runtime.debug 时，也就是常见的 pprof 这一类采集时阻塞。\n- waitReasonGarbageCollection：在垃圾回收时，主要场景是 GC 标记终止（GC Mark Termination）阶段时触发。\n- waitReasonGarbageCollectionScan：在垃圾回收扫描时，主要场景是 GC 标记（GC Mark）扫描 Root 阶段时触发。\n\n### 第四部分\n\n|  标识   |  含义   |\n| --- | --- |\n|   waitReasonPanicWait  |  panicwait |\n|   waitReasonSelect  |   select  |\n|   waitReasonSelectNoCases  |   select (no cases)  |\n\n- waitReasonPanicWait：在 main goroutine 发生 panic 时，会触发。\n- waitReasonSelect：在调用关键字 select 时会触发。\n- waitReasonSelectNoCases：在调用关键字 select 时，若一个 case 都没有，会直接触发。\n\n### 第五部分\n\n|  标识   |  含义   |\n| --- | --- |\n|   waitReasonGCAssistWait  |  GC assist wait |\n|   waitReasonGCSweepWait  |   GC sweep wait  |\n|   waitReasonGCScavengeWait  |   GC scavenge wait  |\n\n- waitReasonGCAssistWait：GC 辅助标记阶段中的结束行为，会触发。\n- waitReasonGCSweepWait：GC 清扫阶段中的结束行为，会触发。\n- waitReasonGCScavengeWait：GC scavenge 阶段的结束行为，会触发。GC Scavenge 主要是新空间的垃圾回收，是一种经常运行、快速的 GC，负责从新空间中清理较小的对象。\n\n### 第六部分\n\n|  标识   |  含义   |\n| --- | --- |\n|   waitReasonChanReceive  |  chan receive |\n|   waitReasonChanSend  |   chan send  |\n|   waitReasonFinalizerWait  |   finalizer wait  |\n\n- waitReasonChanReceive：在 channel 进行读操作，会触发。\n- waitReasonChanSend：在 channel 进行写操作，会触发。\n- waitReasonFinalizerWait：在 finalizer 结束的阶段，会触发。在 Go 程序中，可以通过调用 `runtime.SetFinalizer` 函数来为一个对象设置一个终结者函数。这个行为对应着结束阶段造成的回收。\n\n### 第七部分\n\n|  标识   |  含义   |\n| --- | --- |\n|   waitReasonForceGCIdle  |  force gc (idle) |\n|   waitReasonSemacquire  |   semacquire  |\n|   waitReasonSleep  |   sleep  |\n\n- waitReasonForceGCIdle：强制 GC（空闲时间）结束时，会触发。\n- waitReasonSemacquire：信号量处理结束时，会触发。\n- waitReasonSleep：经典的 sleep 行为，会触发。\n\n### 第八部分\n\n|  标识   |  含义   |\n| --- | --- |\n|   waitReasonSyncCondWait  |  sync.Cond.Wait |\n|   waitReasonTimerGoroutineIdle  |   timer goroutine (idle)  |\n|   waitReasonTraceReaderBlocked  |   trace reader (blocked)  |\n\n- waitReasonSyncCondWait：结合 `sync.Cond` 用法能知道，是在调用 `sync.Wait` 方法时所触发。\n- waitReasonTimerGoroutineIdle：与 Timer 相关，在没有定时器需要执行任务时，会触发。\n- waitReasonTraceReaderBlocked：与 Trace 相关，ReadTrace会返回二进制跟踪数据，将会阻塞直到数据可用。\n\n### 第九部分\n\n|  标识   |  含义   |\n| --- | --- |\n|   waitReasonWaitForGCCycle  |  wait for GC cycle |\n|   waitReasonGCWorkerIdle  |   GC worker (idle)  |\n|   waitReasonPreempted  |   preempted  |\n|   waitReasonDebugCall  |   debug call  |\n\n- waitReasonWaitForGCCycle：等待 GC 周期，会休眠造成阻塞。\n- waitReasonGCWorkerIdle：GC Worker 空闲时，会休眠造成阻塞。\n- waitReasonPreempted：发生循环调用抢占时，会会休眠等待调度。\n- waitReasonDebugCall：调用 GODEBUG 时，会触发。\n\n## 总结\n\n今天这篇文章是对开头 runtime.gopark 函数的详解文章的一个补充，我们能够对此了解到其诱发的因素。\n\n主要场景为：\n1. 通道（Channel）。\n2. 垃圾回收（GC）。\n3. 休眠（Sleep）。\n4. 锁等待（Lock）。\n5. 抢占（Preempted）。\n6. IO 阻塞（IO Wait）\n7. 其他，例如：panic、finalizer、select 等。\n\n我们可以根据这些特性，去拆解可能会造成阻塞的原因。其实也就没必要记了，他们会导致阻塞肯定是由于存在影响控制流的因素，才会导致 gopark 的调用。\n\n活学活用：）"
  },
  {
    "path": "content/posts/go/goroutine-errors.md",
    "content": "---\ntitle: \"多 Goroutine 如何优雅处理错误？\"\ndate: 2021-12-31T12:54:50+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n在 Go 语言中，goroutine 的使用是非常频繁的，因此在日常编码的时候我们会遇到一个问题，那就是 goroutine 里面的错误处理，怎么做比较好？\n\n![](https://files.mdnice.com/user/3610/72758b15-f9b7-4437-ba17-b37a36f285ae.png)\n\n\n这是来自我读者群的问题。作为一个宠粉煎鱼，我默默记下了这个技术话题。今天煎鱼就大家来看看多 goroutine 的错误处理机制也有哪些！\n\n一般来讲，我们的业务代码会是：\n\n```golang\nfunc main() {\n\tvar wg sync.WaitGroup\n\twg.Add(2)\n\tgo func() {\n\t\tlog.Println(\"脑子进煎鱼了\")\n\t\twg.Done()\n\t}()\n\tgo func() {\n\t\tlog.Println(\"煎鱼想报错...\")\n\t\twg.Done()\n\t}()\n\n\ttime.Sleep(time.Second)\n}\n```\n\n在上述代码中，我们运行了多个 goroutine。但我想抛出 error 的错误信息出来，似乎没什么好办法...\n\n## 通过错误日志记录\n\n为此，业务代码中常见的第一种方法：通过把错误记录写入日志文件中，再结合相关的 logtail 进行采集和梳理。\n\n但这又会引入新的问题，那就是调用错误日志的方法写的到处都是。代码结构也比较乱，不直观。\n\n最重要的是无法针对 error 做特定的逻辑处理和流转。\n\n## 利用 channel 传输\n\n这时候大家可能会想到 Go 的经典哲学：**不要通过共享内存来通信，而是通过通信来实现内存共享**（Do not communicate by sharing memory; instead, share memory by communicating）。\n\n第二种的方法：利用 channel 来传输多个 goroutine 中的 errors：\n\n```golang\nfunc main() {\n\tgerrors := make(chan error)\n\twgDone := make(chan bool)\n\n\tvar wg sync.WaitGroup\n\twg.Add(2)\n\n\tgo func() {\n\t\twg.Done()\n\t}()\n\tgo func() {\n\t\terr := returnError()\n\t\tif err != nil {\n\t\t\tgerrors <- err\n\t\t}\n\t\twg.Done()\n\t}()\n\n\tgo func() {\n\t\twg.Wait()\n\t\tclose(wgDone)\n\t}()\n\n\tselect {\n\tcase <-wgDone:\n\t\tbreak\n\tcase err := <-gerrors:\n\t\tclose(gerrors)\n\t\tfmt.Println(err)\n\t}\n\n\ttime.Sleep(time.Second)\n}\n\nfunc returnError() error {\n\treturn errors.New(\"煎鱼报错了...\")\n}\n```\n\n输出结果：\n\n```golang\n煎鱼报错了...\n```\n\n虽然使用 channel 后已经方便了不少。但自己编写 channel 总是需要关心一些非业务向的逻辑。\n\n## 借助 sync/errgroup\n\n因此第三种方法，就是使用官方提供的 `sync/errgroup` 标准库：\n\n```golang\ntype Group\n    func WithContext(ctx context.Context) (*Group, context.Context)\n    func (g *Group) Go(f func() error)\n    func (g *Group) Wait() error\n```\n\n- Go：启动一个协程，在新的 goroutine 中调用给定的函数。\n- Wait：等待协程结束，直到来自 Go 方法的所有函数调用都返回，然后返回其中的第一个非零错误（如果有的话）。\n\n结合其特性能够非常便捷的针对多 goroutine 进行错误处理：\n\n```golang\nfunc main() {\n\tg := new(errgroup.Group)\n\tvar urls = []string{\n\t\t\"http://www.golang.org/\",\n\t\t\"https://golang2.eddycjy.com/\",\n\t\t\"https://eddycjy.com/\",\n\t}\n\tfor _, url := range urls {\n\t\turl := url\n\t\tg.Go(func() error {\n\t\t\tresp, err := http.Get(url)\n\t\t\tif err == nil {\n\t\t\t\tresp.Body.Close()\n\t\t\t}\n\t\t\treturn err\n\t\t})\n\t}\n\tif err := g.Wait(); err == nil {\n\t\tfmt.Println(\"Successfully fetched all URLs.\")\n\t} else {\n\t\tfmt.Printf(\"Errors: %+v\", err)\n\t}\n}\n```\n\n在上述代码中，其表现的是爬虫的案例。每一个计划新起的 goroutine 都直接使用 `Group.Go` 方法。在等待和错误上，直接调用 `Group.Wait` 方法就可以了。\n\n使用标准库 `sync/errgroup` 这种方法的好处就是不需要关注非业务逻辑的控制代码，比较省心省力。\n\n## 进阶使用\n\n在真实的工程代码中，我们还可以基于 `sync/errgroup` 实现一个 http server 的启动和关闭 ，以及 linux signal 信号的注册和处理。以此保证能够实现一个 http server 退出，全部注销退出。\n\n参考代码（@via 毛老师）如下：\n\n```golang\nfunc main() {\n\tg, ctx := errgroup.WithContext(context.Background())\n\tsvr := http.NewServer()\n\t// http server\n\tg.Go(func() error {\n\t\tfmt.Println(\"http\")\n\t\tgo func() {\n\t\t\t<-ctx.Done()\n\t\t\tfmt.Println(\"http ctx done\")\n\t\t\tsvr.Shutdown(context.TODO())\n\t\t}()\n\t\treturn svr.Start()\n\t})\n\n\t// signal\n\tg.Go(func() error {\n\t\texitSignals := []os.Signal{os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT} // SIGTERM is POSIX specific\n\t\tsig := make(chan os.Signal, len(exitSignals))\n\t\tsignal.Notify(sig, exitSignals...)\n\t\tfor {\n\t\t\tfmt.Println(\"signal\")\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tfmt.Println(\"signal ctx done\")\n\t\t\t\treturn ctx.Err()\n\t\t\tcase <-sig:\n\t\t\t\t// do something\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t})\n\n\t// inject error\n\tg.Go(func() error {\n\t\tfmt.Println(\"inject\")\n\t\ttime.Sleep(time.Second)\n\t\tfmt.Println(\"inject finish\")\n\t\treturn errors.New(\"inject error\")\n\t})\n\n\terr := g.Wait() // first error return\n\tfmt.Println(err)\n}\n```\n\n内部基础框架有非常有这种代码，有兴趣的可以自己模仿着写一遍，收货会很多。\n\n## 总结\n\n在 Go 语言中 goroutine 是非常常用的一种方法，为此我们需要更了解 goroutine 配套的上下游（像是 context、error 处理等），应该如何用什么来保证。\n\n再在团队中形成一定的共识和规范，这么工程代码阅读起来就会比较的舒适，一些很坑的隐藏 BUG 也会少很多 ：）"
  },
  {
    "path": "content/posts/go/goroutine-leak.md",
    "content": "---\ntitle: \"跟面试官聊 Goroutine 泄露的 6 种方法，真刺激！\"\ndate: 2021-06-11T12:54:49+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好，我是煎鱼。\n\n前几天分享 Go 群友提问的文章时，有读者在朋友圈下提到，希望我能够针对 Goroutine 泄露这块进行讲解，他在面试的时候经常被问到。\n\n今天的男主角，就是 Go 语言的著名品牌标识 Goroutine，一个随随便便就能开几十万个快车进车道的大杀器。\n\n```golang\n    for {\n        go func() {}()\n    }\n```\n本文会聚焦于 Goroutine 泄露的 N 种方法，进行详解和说明。\n\n## 为什么要问\n\n面试官为啥会问 Goroutine（协程）泄露这种奇特的问题呢？\n\n可以猜测是：\n\n- Goroutine 实在是使用门槛实在是太低了，随手就一个就能起，出现了不少滥用的情况。例如：并发 map。\n- Goroutine 本身在 Go 语言的标准库、复合类型、底层源码中应用广泛。例如：HTTP Server 对每一个请求的处理就是一个协程去运行。\n\n很多 Go 工程在线上出事故时，基本 Goroutine 的关联，大家都会作为救火队长，风风火火的跑去看指标、看日志，通过 PProf 采集 Goroutine 运行情况等。\n\n自然他也就是最受瞩目的那颗 “星” 了，所以在日常面试中，被问几率也就极高了。\n\n## Goroutine 泄露\n\n了解清楚大家爱问的原因后，我们开始对 Goroutine 泄露的 N 种方法进行研究，希望通过前人留下的 “坑”，了解其原理和避开这些问题。\n\n泄露的原因大多集中在：\n- Goroutine 内正在进行 channel/mutex 等读写操作，但由于逻辑问题，某些情况下会被一直阻塞。\n- Goroutine 内的业务逻辑进入死循环，资源一直无法释放。\n- Goroutine 内的业务逻辑进入长时间等待，有不断新增的 Goroutine 进入等待。\n\n接下来我会引用在网上冲浪收集到的一些 Goroutine 泄露例子（会在文末参考注明出处）。\n\n### channel 使用不当\n\nGoroutine+Channel 是最经典的组合，因此不少泄露都出现于此。\n\n最经典的就是上面提到的 channel 进行读写操作时的逻辑问题。\n\n#### 发送不接收\n\n第一个例子：\n\n```golang\nfunc main() {\n    for i := 0; i < 4; i++ {\n        queryAll()\n        fmt.Printf(\"goroutines: %d\\n\", runtime.NumGoroutine())\n    }\n}\n\nfunc queryAll() int {\n    ch := make(chan int)\n    for i := 0; i < 3; i++ {\n        go func() { ch <- query() }()\n\t    }\n    return <-ch\n}\n\nfunc query() int {\n    n := rand.Intn(100)\n    time.Sleep(time.Duration(n) * time.Millisecond)\n    return n\n}\n```\n\n输出结果：\n\n```\ngoroutines: 3\ngoroutines: 5\ngoroutines: 7\ngoroutines: 9\n```\n\n在这个例子中，我们调用了多次 `queryAll` 方法，并在 `for` 循环中利用 Goroutine 调用了 `query` 方法。其重点在于调用 `query` 方法后的结果会写入 `ch` 变量中，接收成功后再返回 `ch` 变量。 \n\n最后可看到输出的 goroutines 数量是在不断增加的，每次多 2 个。也就是每调用一次，都会泄露 Goroutine。\n\n原因在于 channel 均已经发送了（每次发送 3 个），但是在接收端并没有接收完全（只返回 1 个 ch），所诱发的 Goroutine 泄露。\n\n#### 接收不发送\n\n第二个例子：\n\n```golang\nfunc main() {\n    defer func() {\n        fmt.Println(\"goroutines: \", runtime.NumGoroutine())\n    }()\n\n    var ch chan struct{}\n    go func() {\n        ch <- struct{}{}\n    }()\n    \n    time.Sleep(time.Second)\n}\n```\n\n输出结果：\n\n```\ngoroutines:  2\n```\n\n在这个例子中，与 “发送不接收” 两者是相对的，channel 接收了值，但是不发送的话，同样会造成阻塞。\n\n但在实际业务场景中，一般更复杂。基本是一大堆业务逻辑里，有一个 channel 的读写操作出现了问题，自然就阻塞了。\n\n#### nil channel\n\n第三个例子：\n\n```golang\nfunc main() {\n    defer func() {\n        fmt.Println(\"goroutines: \", runtime.NumGoroutine())\n    }()\n\n    var ch chan int\n    go func() {\n        <-ch\n    }()\n    \n    time.Sleep(time.Second)\n}\n```\n\n输出结果：\n\n```\ngoroutines:  2\n```\n\n在这个例子中，可以得知 channel 如果忘记初始化，那么无论你是读，还是写操作，都会造成阻塞。\n\n正常的初始化姿势是：\n\n```golang\n    ch := make(chan int)\n    go func() {\n        <-ch\n    }()\n    ch <- 0\n    time.Sleep(time.Second)\n```\n\n调用 `make` 函数进行初始化。\n\n### 奇怪的慢等待\n\n第四个例子：\n\n```golang\nfunc main() {\n    for {\n        go func() {\n            _, err := http.Get(\"https://www.xxx.com/\")\n            if err != nil {\n                fmt.Printf(\"http.Get err: %v\\n\", err)\n            }\n            // do something...\n    }()\n\n    time.Sleep(time.Second * 1)\n    fmt.Println(\"goroutines: \", runtime.NumGoroutine())\n\t}\n}\n```\n\n输出结果：\n\n```\ngoroutines:  5\ngoroutines:  9\ngoroutines:  13\ngoroutines:  17\ngoroutines:  21\ngoroutines:  25\n...\n```\n\n在这个例子中，展示了一个 Go 语言中经典的事故场景。也就是一般我们会在应用程序中去调用第三方服务的接口。\n\n但是第三方接口，有时候会很慢，久久不返回响应结果。恰好，Go 语言中默认的 `http.Client` 是没有设置超时时间的。\n\n因此就会导致一直阻塞，一直阻塞就一直爽，Goroutine 自然也就持续暴涨，不断泄露，最终占满资源，导致事故。\n\n在 Go 工程中，我们一般建议至少对 `http.Client` 设置超时时间：\n\n```golang\n    httpClient := http.Client{\n        Timeout: time.Second * 15,\n    }\n```\n并且要做限流、熔断等措施，以防突发流量造成依赖崩塌，依然吃 P0。\n\n### 互斥锁忘记解锁\n\n第五个例子：\n\n```golang\nfunc main() {\n    total := 0\n    defer func() {\n        time.Sleep(time.Second)\n        fmt.Println(\"total: \", total)\n        fmt.Println(\"goroutines: \", runtime.NumGoroutine())\n\t}()\n\n    var mutex sync.Mutex\n    for i := 0; i < 10; i++ {\n        go func() {\n            mutex.Lock()\n            total += 1\n        }()\n    }\n}\n```\n\n输出结果：\n\n```\ntotal:  1\ngoroutines:  10\n```\n\n在这个例子中，第一个互斥锁 `sync.Mutex` 加锁了，但是他可能在处理业务逻辑，又或是忘记 `Unlock` 了。\n\n因此导致后面的所有 `sync.Mutex` 想加锁，却因未释放又都阻塞住了。一般在 Go 工程中，我们建议如下写法：\n\n```golang\n    var mutex sync.Mutex\n    for i := 0; i < 10; i++ {\n        go func() {\n            mutex.Lock()\n            defer mutex.Unlock()\n            total += 1\n    }()\n    }\n```\n\n### 同步锁使用不当\n\n第六个例子：\n\n```golang\nfunc handle(v int) {\n    var wg sync.WaitGroup\n    wg.Add(5)\n    for i := 0; i < v; i++ {\n        fmt.Println(\"脑子进煎鱼了\")\n        wg.Done()\n    }\n    wg.Wait()\n}\n\nfunc main() {\n    defer func() {\n        fmt.Println(\"goroutines: \", runtime.NumGoroutine())\n    }()\n\n    go handle(3)\n    time.Sleep(time.Second)\n}\n```\n\n在这个例子中，我们调用了同步编排 `sync.WaitGroup`，模拟了一遍我们会从外部传入循环遍历的控制变量。\n\n但由于 `wg.Add` 的数量与 `wg.Done` 数量并不匹配，因此在调用 `wg.Wait` 方法后一直阻塞等待。\n\n在 Go 工程中使用，我们会建议如下写法：\n\n```golang\n    var wg sync.WaitGroup\n    for i := 0; i < v; i++ {\n        wg.Add(1)\n        defer wg.Done()\n        fmt.Println(\"脑子进煎鱼了\")\n    }\n    wg.Wait()\n```\n\n## 排查方法\n\n我们可以调用 `runtime.NumGoroutine` 方法来获取 Goroutine 的运行数量，进行前后一比较，就能知道有没有泄露了。\n\n但在业务服务的运行场景中，Goroutine 内导致的泄露，大多数处于生产、测试环境，因此更多的是使用 PProf：\n\n```golang\nimport (\n    \"net/http\"\n     _ \"net/http/pprof\"\n)\n\nhttp.ListenAndServe(\"localhost:6060\", nil))\n```\n\n只要我们调用 `http://localhost:6060/debug/pprof/goroutine?debug=1`，PProf 会返回所有带有堆栈跟踪的 Goroutine 列表。\n\n也可以利用 PProf 的其他特性进行综合查看和分析，这块参考我之前写的《Go 大杀器之性能剖析 PProf》，基本是全村最全的教程了。\n\n## 总结\n\n在今天这篇文章中，我们针对 Goroutine 泄露的 N 种常见的方式方法进行了一一分析，虽说看起来都是比较基础的场景。\n\n但结合在实际业务代码中，就是一大坨中的某个细节导致全盘皆输了，希望上面几个案例能够给大家带来警惕。\n\n而面试官爱问，怕不是自己踩过许多坑，也希望进来的同僚，也是身经百战了。\n\n靠谱的工程师，而非只是八股工程师。\n\n## 参考\n\n- 波罗学大佬的《[Go 笔记之如何防止 goroutine 泄露](https://zhuanlan.zhihu.com/p/74090074)》\n- 二斗斗的《[怎么看待Goroutine 泄露](https://zhuanlan.zhihu.com/p/139689803)》"
  },
  {
    "path": "content/posts/go/grpc/2018-09-22-install.md",
    "content": "---\n\ntitle:      \"「连载一」gRPC及相关介绍\"\ndate:       2018-09-22 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - grpc\n---\n\n项目地址：https://github.com/EDDYCJY/go-grpc-example\n\n作为开篇章，将会介绍 gRPC 相关的一些知识。简单来讲 gRPC 是一个 基于 HTTP/2 协议设计的 RPC 框架，它采用了 Protobuf 作为 IDL\n\n你是否有过疑惑，它们都是些什么？本文将会介绍一些常用的知识和概念，更详细的会给出手册地址去深入\n\n## 一、RPC\n\n### 什么是 RPC\n\nRPC 代指远程过程调用（Remote Procedure Call），它的调用包含了传输协议和编码（对象序列号）协议等等。允许运行于一台计算机的程序调用另一台计算机的子程序，而开发人员无需额外地为这个交互作用编程\n\n#### 实际场景：\n\n有两台服务器，分别是 A、B。在 A 上的应用 C 想要调用 B 服务器上的应用 D，它们可以直接本地调用吗？  \n答案是不能的，但走 RPC 的话，十分方便。因此常有人称使用 RPC，就跟本地调用一个函数一样简单\n\n### RPC 框架\n\n我认为，一个完整的 RPC 框架，应包含负载均衡、服务注册和发现、服务治理等功能，并具有可拓展性便于流量监控系统等接入  \n那么它才算完整的，当然了。有些较单一的 RPC 框架，通过组合多组件也能达到这个标准\n\n你认为呢？\n\n### 常见 RPC 框架\n\n- [gRPC](https://grpc.io/)\n- [Thrift](https://github.com/apache/thrift)\n- [Rpcx](https://github.com/smallnest/rpcx)\n- [Dubbo](https://github.com/apache/incubator-dubbo)\n\n### 比较一下\n\n| \\      | 跨语言 | 多 IDL | 服务治理 | 注册中心 | 服务管理 |\n| ------ | ------ | ------ | -------- | -------- | -------- |\n| gRPC   | √      | ×      | ×        | ×        | ×        |\n| Thrift | √      | ×      | ×        | ×        | ×        |\n| Rpcx   | ×      | √      | √        | √        | √        |\n| Dubbo  | ×      | √      | √        | √        | √        |\n\n### 为什么要 RPC\n\n简单、通用、安全、效率\n\n### RPC 可以基于 HTTP 吗\n\nRPC 是代指远程过程调用，是可以基于 HTTP 协议的\n\n肯定会有人说效率优势，我可以告诉你，那是基于 HTTP/1.1 来讲的，HTTP/2 优化了许多问题（当然也存在新的问题），所以你看到了本文的主题 gRPC\n\n## 二、Protobuf\n\n### 介绍\n\nProtocol Buffers 是一种与语言、平台无关，可扩展的序列化结构化数据的方法，常用于通信协议，数据存储等等。相较于 JSON、XML，它更小、更快、更简单，因此也更受开发人员的青眯\n\n### 语法\n\n```go\nsyntax = \"proto3\";\n\nservice SearchService {\n    rpc Search (SearchRequest) returns (SearchResponse);\n}\n\nmessage SearchRequest {\n  string query = 1;\n  int32 page_number = 2;\n  int32 result_per_page = 3;\n}\n\nmessage SearchResponse {\n    ...\n}\n```\n\n1、第一行（非空的非注释行）声明使用 `proto3` 语法。如果不声明，将默认使用 `proto2` 语法。同时我建议用 v2 还是 v3，都应当声明其使用的版本\n\n2、定义 `SearchService` RPC 服务，其包含 RPC 方法 `Search`，入参为 `SearchRequest` 消息，出参为 `SearchResponse` 消息\n\n3、定义 `SearchRequest`、`SearchResponse` 消息，前者定义了三个字段，每一个字段包含三个属性：类型、字段名称、字段编号\n\n4、Protobuf 编译器会根据选择的语言不同，生成相应语言的 Service Interface Code 和 Stubs\n\n最后，这里只是简单的语法介绍，详细的请右拐 [\nLanguage Guide (proto3)](https://developers.google.com/protocol-buffers/docs/proto3)\n\n### 数据类型\n\n| .proto Type | C++ Type | Java Type  | Go Type | PHP Type       |\n| ----------- | -------- | ---------- | ------- | -------------- |\n| double      | double   | double     | float64 | float          |\n| float       | float    | float      | float32 | float          |\n| int32       | int32    | int        | int32   | integer        |\n| int64       | int64    | long       | int64   | integer/string |\n| uint32      | uint32   | int        | uint32  | integer        |\n| uint64      | uint64   | long       | uint64  | integer/string |\n| sint32      | int32    | int        | int32   | integer        |\n| sint64      | int64    | long       | int64   | integer/string |\n| fixed32     | uint32   | int        | uint32  | integer        |\n| fixed64     | uint64   | long       | uint64  | integer/string |\n| sfixed32    | int32    | int        | int32   | integer        |\n| sfixed64    | int64    | long       | int64   | integer/string |\n| bool        | bool     | boolean    | bool    | boolean        |\n| string      | string   | String     | string  | string         |\n| bytes       | string   | ByteString | []byte  | string         |\n\n### v2 和 v3 主要区别\n\n- 删除原始值字段的字段存在逻辑\n- 删除 required 字段\n- 删除 optional 字段，默认就是\n- 删除 default 字段\n- 删除扩展特性，新增 Any 类型来替代它\n- 删除 unknown 字段的支持\n- 新增 [JSON Mapping](https://developers.google.com/protocol-buffers/docs/proto3#json)\n- 新增 Map 类型的支持\n- 修复 enum 的 unknown 类型\n- repeated 默认使用 packed 编码\n- 引入了新的语言实现（C＃，JavaScript，Ruby，Objective-C）\n\n以上是日常涉及的常见功能，如果还想详细了解可阅读 [Protobuf Version 3.0.0](https://github.com/protocolbuffers/protobuf/releases?after=v3.2.1)\n\n### 相较 Protobuf，为什么不使用 XML？\n\n- 更简单\n- 数据描述文件只需原来的 1/10 至 1/3\n- 解析速度是原来的 20 倍至 100 倍\n- 减少了二义性\n- 生成了更易使用的数据访问类\n\n## 三、gRPC\n\n### 介绍\n\ngRPC 是一个高性能、开源和通用的 RPC 框架，面向移动和 HTTP/2 设计\n\n#### 多语言\n\n- C++\n- C#\n- Dart\n- Go\n- Java\n- Node.js\n- Objective-C\n- PHP\n- Python\n- Ruby\n\n#### 特点\n\n1、HTTP/2\n\n2、Protobuf\n\n3、客户端、服务端基于同一份 IDL\n\n4、移动网络的良好支持\n\n5、支持多语言\n\n### 概览\n\n![image](https://image.eddycjy.com/7dcac5be0a34636c699025368242d3f3.png)\n\n### 讲解\n\n1、客户端（gRPC Sub）调用 A 方法，发起 RPC 调用\n\n2、对请求信息使用 Protobuf 进行对象序列化压缩（IDL）\n\n3、服务端（gRPC Server）接收到请求后，解码请求体，进行业务逻辑处理并返回\n\n4、对响应结果使用 Protobuf 进行对象序列化压缩（IDL）\n\n5、客户端接受到服务端响应，解码请求体。回调被调用的 A 方法，唤醒正在等待响应（阻塞）的客户端调用并返回响应结果\n\n### 示例\n\n在这一小节，将简单的给大家展示 gRPC 的客户端和服务端的示例代码，希望大家先有一个基础的印象，将会在下一章节详细介绍 🤔\n\n#### 构建和启动服务端\n\n```go\nlis, err := net.Listen(\"tcp\", fmt.Sprintf(\":%d\", *port))\nif err != nil {\n        log.Fatalf(\"failed to listen: %v\", err)\n}\n\ngrpcServer := grpc.NewServer()\n...\npb.RegisterSearchServer(grpcServer, &SearchServer{})\ngrpcServer.Serve(lis)\n```\n\n1、监听指定 TCP 端口，用于接受客户端请求\n\n2、创建 gRPC Server 的实例对象\n\n3、gRPC Server 内部服务和路由的注册\n\n4、Serve() 调用服务器以执行阻塞等待，直到进程被终止或被 Stop() 调用\n\n#### 创建客户端\n\n```go\nvar opts []grpc.DialOption\n...\nconn, err := grpc.Dial(*serverAddr, opts...)\nif err != nil {\n    log.Fatalf(\"fail to dial: %v\", err)\n}\n\ndefer conn.Close()\nclient := pb.NewSearchClient(conn)\n...\n```\n\n1、创建 gRPC Channel 与 gRPC Server 进行通信（需服务器地址和端口作为参数）\n\n2、设置 DialOptions 凭证（例如，TLS，GCE 凭据，JWT 凭证）\n\n3、创建 Search Client Stub\n\n4、调用对应的服务方法\n\n## 思考题\n\n1、什么场景下不适合使用 Protobuf，而适合使用 JSON、XML？\n\n2、Protobuf 一节中提到的 packed 编码，是什么？\n\n## 总结\n\n在开篇内容中，我利用了尽量简短的描述给你介绍了接下来所必须、必要的知识点\n希望你能够有所收获，建议能到我给的参考资料处进行深入学习，是最好的了\n\n## 参考资料\n\n- [Protocol Buffers](https://developers.google.com/protocol-buffers/docs/proto3)\n- [gRPC](https://grpc.io/docs/)\n"
  },
  {
    "path": "content/posts/go/grpc/2018-09-23-client-and-server.md",
    "content": "---\n\ntitle:      \"「连载二」gRPC Client and Server\"\ndate:       2018-09-23 12:30:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - grpc\n---\n\n## 前言\n\n本章节将使用 Go 来编写 gRPC Server 和 Client，让其互相通讯。在此之上会使用到如下库：\n\n- google.golang.org/grpc\n- github.com/golang/protobuf/protoc-gen-go\n\n## 安装\n\n### gRPC\n\n```\ngo get -u google.golang.org/grpc\n```\n\n### Protocol Buffers v3\n\n```\nwget https://github.com/google/protobuf/releases/download/v3.5.1/protobuf-all-3.5.1.zip\nunzip protobuf-all-3.5.1.zip\ncd protobuf-3.5.1/\n./configure\nmake\nmake install\n```\n\n检查是否安装成功\n\n```\nprotoc --version\n```\n\n若出现以下错误，执行 `ldconfig` 命名就能解决这问题\n\n```\nprotoc: error while loading shared libraries: libprotobuf.so.15: cannot open shared object file: No such file or directory\n```\n\n### Protoc Plugin\n\n```\ngo get -u github.com/golang/protobuf/protoc-gen-go\n```\n\n安装环境若有问题，可参考我先前的文章 [《介绍与环境安装》](https://segmentfault.com/a/1190000013339403) 内有详细介绍，不再赘述\n\n## gRPC\n\n本小节开始正式编写 gRPC 相关的程序，一起上车吧 😄\n\n### 图示\n\n![image](https://image.eddycjy.com/415d9544fce1e774e1095ab99b6cc015.png)\n\n### 目录结构\n\n```\n$ tree go-grpc-example\ngo-grpc-example\n├── client\n├── proto\n│   └── search.proto\n└── server.go\n```\n\n### IDL\n\n#### 编写\n\n在 proto 文件夹下的 search.proto 文件中，写入如下内容：\n\n```\nsyntax = \"proto3\";\n\npackage proto;\n\nservice SearchService {\n    rpc Search(SearchRequest) returns (SearchResponse) {}\n}\n\nmessage SearchRequest {\n    string request = 1;\n}\n\nmessage SearchResponse {\n    string response = 1;\n}\n```\n\n#### 生成\n\n在 proto 文件夹下执行如下命令：\n\n```\n$ protoc --go_out=plugins=grpc:. *.proto\n```\n\n- plugins=plugin1+plugin2：指定要加载的子插件列表\n\n我们定义的 proto 文件是涉及了 RPC 服务的，而默认是不会生成 RPC 代码的，因此需要给出 `plugins` 参数传递给 `protoc-gen-go`，告诉它，请支持 RPC（这里指定了 gRPC）\n\n- --go_out=.：设置 Go 代码输出的目录\n\n该指令会加载 protoc-gen-go 插件达到生成 Go 代码的目的，生成的文件以 .pb.go 为文件后缀\n\n- : （冒号）\n\n冒号充当分隔符的作用，后跟所需要的参数集。如果这处不涉及 RPC，命令可简化为：\n\n```\n$ protoc --go_out=. *.proto\n```\n\n注：建议你看看两条命令生成的 .pb.go 文件，分别有什么区别\n\n#### 生成后\n\n执行完毕命令后，将得到一个 .pb.go 文件，文件内容如下：\n\n```go\ntype SearchRequest struct {\n\tRequest              string   `protobuf:\"bytes,1,opt,name=request\" json:\"request,omitempty\"`\n\tXXX_NoUnkeyedLiteral struct{} `json:\"-\"`\n\tXXX_unrecognized     []byte   `json:\"-\"`\n\tXXX_sizecache        int32    `json:\"-\"`\n}\n\nfunc (m *SearchRequest) Reset()         { *m = SearchRequest{} }\nfunc (m *SearchRequest) String() string { return proto.CompactTextString(m) }\nfunc (*SearchRequest) ProtoMessage()    {}\nfunc (*SearchRequest) Descriptor() ([]byte, []int) {\n\treturn fileDescriptor_search_8b45f79ee13ff6a3, []int{0}\n}\n\nfunc (m *SearchRequest) GetRequest() string {\n\tif m != nil {\n\t\treturn m.Request\n\t}\n\treturn \"\"\n}\n```\n\n通过阅读这一部分代码，可以知道主要涉及如下方面：\n\n- 字段名称从小写下划线转换为大写驼峰模式（字段导出）\n- 生成一组 Getters 方法，能便于处理一些空指针取值的情况\n- ProtoMessage 方法实现 proto.Message 的接口\n- 生成 Rest 方法，便于将 Protobuf 结构体恢复为零值\n- Repeated 转换为切片\n\n```go\ntype SearchRequest struct {\n\tRequest              string   `protobuf:\"bytes,1,opt,name=request\" json:\"request,omitempty\"`\n}\n\nfunc (*SearchRequest) Descriptor() ([]byte, []int) {\n\treturn fileDescriptor_search_8b45f79ee13ff6a3, []int{0}\n}\n\ntype SearchResponse struct {\n\tResponse             string   `protobuf:\"bytes,1,opt,name=response\" json:\"response,omitempty\"`\n}\n\nfunc (*SearchResponse) Descriptor() ([]byte, []int) {\n\treturn fileDescriptor_search_8b45f79ee13ff6a3, []int{1}\n}\n\n...\n\nfunc init() { proto.RegisterFile(\"search.proto\", fileDescriptor_search_8b45f79ee13ff6a3) }\n\nvar fileDescriptor_search_8b45f79ee13ff6a3 = []byte{\n\t// 131 bytes of a gzipped FileDescriptorProto\n\t0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x29, 0x4e, 0x4d, 0x2c,\n\t0x4a, 0xce, 0xd0, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x05, 0x53, 0x4a, 0x9a, 0x5c, 0xbc,\n\t0xc1, 0x60, 0xe1, 0xa0, 0xd4, 0xc2, 0xd2, 0xd4, 0xe2, 0x12, 0x21, 0x09, 0x2e, 0xf6, 0x22, 0x08,\n\t0x53, 0x82, 0x51, 0x81, 0x51, 0x83, 0x33, 0x08, 0xc6, 0x55, 0xd2, 0xe1, 0xe2, 0x83, 0x29, 0x2d,\n\t0x2e, 0xc8, 0xcf, 0x2b, 0x4e, 0x15, 0x92, 0xe2, 0xe2, 0x28, 0x82, 0xb2, 0xa1, 0x8a, 0xe1, 0x7c,\n\t0x23, 0x0f, 0x98, 0xc1, 0xc1, 0xa9, 0x45, 0x65, 0x99, 0xc9, 0xa9, 0x42, 0xe6, 0x5c, 0x6c, 0x10,\n\t0x01, 0x21, 0x11, 0x88, 0x13, 0xf4, 0x50, 0x2c, 0x96, 0x12, 0x45, 0x13, 0x85, 0x98, 0xa3, 0xc4,\n\t0x90, 0xc4, 0x06, 0x16, 0x37, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xf3, 0xba, 0x74, 0x95, 0xc0,\n\t0x00, 0x00, 0x00,\n}\n```\n\n而这一部分代码主要是围绕 `fileDescriptor` 进行，在这里 `fileDescriptor_search_8b45f79ee13ff6a3` 表示一个编译后的 proto 文件，而每一个方法都包含 Descriptor 方法，代表着这一个方法在 `fileDescriptor` 中具体的 Message Field\n\n### Server\n\n这一小节将编写 gRPC Server 的基础模板，完成一个方法的调用。对 server.go 写入如下内容：\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"net\"\n\n\t\"google.golang.org/grpc\"\n\n\tpb \"github.com/EDDYCJY/go-grpc-example/proto\"\n)\n\ntype SearchService struct{}\n\nfunc (s *SearchService) Search(ctx context.Context, r *pb.SearchRequest) (*pb.SearchResponse, error) {\n\treturn &pb.SearchResponse{Response: r.GetRequest() + \" Server\"}, nil\n}\n\nconst PORT = \"9001\"\n\nfunc main() {\n\tserver := grpc.NewServer()\n\tpb.RegisterSearchServiceServer(server, &SearchService{})\n\n\tlis, err := net.Listen(\"tcp\", \":\"+PORT)\n\tif err != nil {\n\t\tlog.Fatalf(\"net.Listen err: %v\", err)\n\t}\n\n\tserver.Serve(lis)\n}\n```\n\n- 创建 gRPC Server 对象，你可以理解为它是 Server 端的抽象对象\n- 将 SearchService（其包含需要被调用的服务端接口）注册到 gRPC Server 的内部注册中心。这样可以在接受到请求时，通过内部的服务发现，发现该服务端接口并转接进行逻辑处理\n- 创建 Listen，监听 TCP 端口\n- gRPC Server 开始 lis.Accept，直到 Stop 或 GracefulStop\n\n### Client\n\n接下来编写 gRPC Go Client 的基础模板，打开 client/client.go 文件，写入以下内容：\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\n\t\"google.golang.org/grpc\"\n\n\tpb \"github.com/EDDYCJY/go-grpc-example/proto\"\n)\n\nconst PORT = \"9001\"\n\nfunc main() {\n\tconn, err := grpc.Dial(\":\"+PORT, grpc.WithInsecure())\n\tif err != nil {\n\t\tlog.Fatalf(\"grpc.Dial err: %v\", err)\n\t}\n\tdefer conn.Close()\n\n\tclient := pb.NewSearchServiceClient(conn)\n\tresp, err := client.Search(context.Background(), &pb.SearchRequest{\n\t\tRequest: \"gRPC\",\n\t})\n\tif err != nil {\n\t\tlog.Fatalf(\"client.Search err: %v\", err)\n\t}\n\n\tlog.Printf(\"resp: %s\", resp.GetResponse())\n}\n```\n\n- 创建与给定目标（服务端）的连接交互\n- 创建 SearchService 的客户端对象\n- 发送 RPC 请求，等待同步响应，得到回调后返回响应结果\n- 输出响应结果\n\n## 验证\n\n### 启动 Server\n\n```sh\n$ pwd\n$GOPATH/github.com/EDDYCJY/go-grpc-example\n$ go run server.go\n```\n\n### 启动 Client\n\n```sh\n$ pwd\n$GOPATH/github.com/EDDYCJY/go-grpc-example/client\n$ go run client.go\n2018/09/23 11:06:23 resp: gRPC Server\n```\n\n## 总结\n\n在本章节，我们对 Protobuf、gRPC Client/Server 分别都进行了介绍。希望你结合文中讲述内容再写一个 Demo 进行深入了解，肯定会更棒 🤔\n\n## 参考\n\n### 本系列示例代码\n\n- [go-grpc-example](https://github.com/EDDYCJY/go-grpc-example)"
  },
  {
    "path": "content/posts/go/grpc/2018-09-24-stream-client-server.md",
    "content": "---\n\ntitle:      \"「连载三」gRPC Streaming, Client and Server\"\ndate:       2018-09-24 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - grpc\n---\n\n## 前言\n\n本章节将介绍 gRPC 的流式，分为三种类型：\n\n- Server-side streaming RPC：服务器端流式 RPC\n- Client-side streaming RPC：客户端流式 RPC\n- Bidirectional streaming RPC：双向流式 RPC\n\n## 流\n\n任何技术，因为有痛点，所以才有了存在的必要性。如果您想要了解 gRPC 的流式调用，请继续\n\n### 图\n\n![image](https://image.eddycjy.com/8812038d20ffece377c0e4901c9a9231.png)\n\ngRPC Streaming 是基于 HTTP/2 的，后续章节再进行详细讲解\n\n### 为什么不用 Simple RPC\n\n流式为什么要存在呢，是 Simple RPC 有什么问题吗？通过模拟业务场景，可得知在使用 Simple RPC 时，有如下问题：\n\n- 数据包过大造成的瞬时压力\n- 接收数据包时，需要所有数据包都接受成功且正确后，才能够回调响应，进行业务处理（无法客户端边发送，服务端边处理）\n\n### 为什么用 Streaming RPC\n\n- 大规模数据包\n- 实时场景\n\n#### 模拟场景\n\n每天早上 6 点，都有一批百万级别的数据集要同从 A 同步到 B，在同步的时候，会做一系列操作（归档、数据分析、画像、日志等）。这一次性涉及的数据量确实大\n\n在同步完成后，也有人马上会去查阅数据，为了新的一天筹备。也符合实时性。\n\n两者相较下，这个场景下更适合使用 Streaming RPC\n\n## gRPC\n\n在讲解具体的 gRPC 流式代码时，会**着重在第一节讲解**，因为三种模式其实是不同的组合。希望你能够注重理解，举一反三，其实都是一样的知识点 👍\n\n### 目录结构\n\n```\n$ tree go-grpc-example\ngo-grpc-example\n├── client\n│   ├── simple_client\n│   │   └── client.go\n│   └── stream_client\n│       └── client.go\n├── proto\n│   ├── search.proto\n│   └── stream.proto\n└── server\n    ├── simple_server\n    │   └── server.go\n    └── stream_server\n        └── server.go\n```\n\n增加 stream_server、stream_client 存放服务端和客户端文件，proto/stream.proto 用于编写 IDL\n\n### IDL\n\n在 proto 文件夹下的 stream.proto 文件中，写入如下内容：\n\n```\nsyntax = \"proto3\";\n\npackage proto;\n\nservice StreamService {\n    rpc List(StreamRequest) returns (stream StreamResponse) {};\n\n    rpc Record(stream StreamRequest) returns (StreamResponse) {};\n\n    rpc Route(stream StreamRequest) returns (stream StreamResponse) {};\n}\n\n\nmessage StreamPoint {\n  string name = 1;\n  int32 value = 2;\n}\n\nmessage StreamRequest {\n  StreamPoint pt = 1;\n}\n\nmessage StreamResponse {\n  StreamPoint pt = 1;\n}\n```\n\n注意关键字 stream，声明其为一个流方法。这里共涉及三个方法，对应关系为\n\n- List：服务器端流式 RPC\n- Record：客户端流式 RPC\n- Route：双向流式 RPC\n\n### 基础模板 + 空定义\n\n#### Server\n\n```go\npackage main\n\nimport (\n\t\"log\"\n\t\"net\"\n\n\t\"google.golang.org/grpc\"\n\n\tpb \"github.com/EDDYCJY/go-grpc-example/proto\"\n\n)\n\ntype StreamService struct{}\n\nconst (\n\tPORT = \"9002\"\n)\n\nfunc main() {\n\tserver := grpc.NewServer()\n\tpb.RegisterStreamServiceServer(server, &StreamService{})\n\n\tlis, err := net.Listen(\"tcp\", \":\"+PORT)\n\tif err != nil {\n\t\tlog.Fatalf(\"net.Listen err: %v\", err)\n\t}\n\n\tserver.Serve(lis)\n}\n\nfunc (s *StreamService) List(r *pb.StreamRequest, stream pb.StreamService_ListServer) error {\n\treturn nil\n}\n\nfunc (s *StreamService) Record(stream pb.StreamService_RecordServer) error {\n\treturn nil\n}\n\nfunc (s *StreamService) Route(stream pb.StreamService_RouteServer) error {\n\treturn nil\n}\n```\n\n写代码前，建议先将 gRPC Server 的基础模板和接口给空定义出来。若有不清楚可参见上一章节的知识点\n\n#### Client\n\n```go\npackage main\n\nimport (\n    \"log\"\n\n\t\"google.golang.org/grpc\"\n\n\tpb \"github.com/EDDYCJY/go-grpc-example/proto\"\n)\n\nconst (\n\tPORT = \"9002\"\n)\n\nfunc main() {\n\tconn, err := grpc.Dial(\":\"+PORT, grpc.WithInsecure())\n\tif err != nil {\n\t\tlog.Fatalf(\"grpc.Dial err: %v\", err)\n\t}\n\n\tdefer conn.Close()\n\n\tclient := pb.NewStreamServiceClient(conn)\n\n\terr = printLists(client, &pb.StreamRequest{Pt: &pb.StreamPoint{Name: \"gRPC Stream Client: List\", Value: 2018}})\n\tif err != nil {\n\t\tlog.Fatalf(\"printLists.err: %v\", err)\n\t}\n\n\terr = printRecord(client, &pb.StreamRequest{Pt: &pb.StreamPoint{Name: \"gRPC Stream Client: Record\", Value: 2018}})\n\tif err != nil {\n\t\tlog.Fatalf(\"printRecord.err: %v\", err)\n\t}\n\n\terr = printRoute(client, &pb.StreamRequest{Pt: &pb.StreamPoint{Name: \"gRPC Stream Client: Route\", Value: 2018}})\n\tif err != nil {\n\t\tlog.Fatalf(\"printRoute.err: %v\", err)\n\t}\n}\n\nfunc printLists(client pb.StreamServiceClient, r *pb.StreamRequest) error {\n\treturn nil\n}\n\nfunc printRecord(client pb.StreamServiceClient, r *pb.StreamRequest) error {\n\treturn nil\n}\n\nfunc printRoute(client pb.StreamServiceClient, r *pb.StreamRequest) error {\n\treturn nil\n}\n```\n\n### 一、Server-side streaming RPC：服务器端流式 RPC\n\n服务器端流式 RPC，显然是单向流，并代指 Server 为 Stream 而 Client 为普通 RPC 请求\n\n简单来讲就是客户端发起一次普通的 RPC 请求，服务端通过流式响应多次发送数据集，客户端 Recv 接收数据集。大致如图：\n\n![image](https://image.eddycjy.com/b25a47e2f2fb2a8c352a547f7612808b.png)\n\n#### Server\n\n```go\nfunc (s *StreamService) List(r *pb.StreamRequest, stream pb.StreamService_ListServer) error {\n\tfor n := 0; n <= 6; n++ {\n\t\terr := stream.Send(&pb.StreamResponse{\n\t\t\tPt: &pb.StreamPoint{\n\t\t\t\tName:  r.Pt.Name,\n\t\t\t\tValue: r.Pt.Value + int32(n),\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n```\n\n在 Server，主要留意 `stream.Send` 方法。它看上去能发送 N 次？有没有大小限制？\n\n```go\ntype StreamService_ListServer interface {\n\tSend(*StreamResponse) error\n\tgrpc.ServerStream\n}\n\nfunc (x *streamServiceListServer) Send(m *StreamResponse) error {\n\treturn x.ServerStream.SendMsg(m)\n}\n```\n\n通过阅读源码，可得知是 protoc 在生成时，根据定义生成了各式各样符合标准的接口方法。最终再统一调度内部的 `SendMsg` 方法，该方法涉及以下过程:\n\n- 消息体（对象）序列化\n- 压缩序列化后的消息体\n- 对正在传输的消息体增加 5 个字节的 header\n- 判断压缩+序列化后的消息体总字节长度是否大于预设的 maxSendMessageSize（预设值为 `math.MaxInt32`），若超出则提示错误\n- 写入给流的数据集\n\n#### Client\n\n```go\nfunc printLists(client pb.StreamServiceClient, r *pb.StreamRequest) error {\n\tstream, err := client.List(context.Background(), r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor {\n\t\tresp, err := stream.Recv()\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tlog.Printf(\"resp: pj.name: %s, pt.value: %d\", resp.Pt.Name, resp.Pt.Value)\n\t}\n\n\treturn nil\n}\n```\n\n在 Client，主要留意 `stream.Recv()` 方法。什么情况下 `io.EOF` ？什么情况下存在错误信息呢?\n\n```go\ntype StreamService_ListClient interface {\n\tRecv() (*StreamResponse, error)\n\tgrpc.ClientStream\n}\n\nfunc (x *streamServiceListClient) Recv() (*StreamResponse, error) {\n\tm := new(StreamResponse)\n\tif err := x.ClientStream.RecvMsg(m); err != nil {\n\t\treturn nil, err\n\t}\n\treturn m, nil\n}\n```\n\nRecvMsg 会从流中读取完整的 gRPC 消息体，另外通过阅读源码可得知：\n\n（1）RecvMsg 是阻塞等待的\n\n（2）RecvMsg 当流成功/结束（调用了 Close）时，会返回 `io.EOF`\n\n（3）RecvMsg 当流出现任何错误时，流会被中止，错误信息会包含 RPC 错误码。而在 RecvMsg 中可能出现如下错误：\n\n- io.EOF\n- io.ErrUnexpectedEOF\n- transport.ConnectionError\n- google.golang.org/grpc/codes\n\n同时需要注意，默认的 MaxReceiveMessageSize 值为 1024 _ 1024 _ 4，建议不要超出\n\n#### 验证\n\n运行 stream_server/server.go：\n\n```\n$ go run server.go\n```\n\n运行 stream_client/client.go：\n\n```\n$ go run client.go\n2018/09/24 16:18:25 resp: pj.name: gRPC Stream Client: List, pt.value: 2018\n2018/09/24 16:18:25 resp: pj.name: gRPC Stream Client: List, pt.value: 2019\n2018/09/24 16:18:25 resp: pj.name: gRPC Stream Client: List, pt.value: 2020\n2018/09/24 16:18:25 resp: pj.name: gRPC Stream Client: List, pt.value: 2021\n2018/09/24 16:18:25 resp: pj.name: gRPC Stream Client: List, pt.value: 2022\n2018/09/24 16:18:25 resp: pj.name: gRPC Stream Client: List, pt.value: 2023\n2018/09/24 16:18:25 resp: pj.name: gRPC Stream Client: List, pt.value: 2024\n```\n\n### 二、Client-side streaming RPC：客户端流式 RPC\n\n客户端流式 RPC，单向流，客户端通过流式发起**多次** RPC 请求给服务端，服务端发起**一次**响应给客户端，大致如图：\n\n![image](https://image.eddycjy.com/97473884d939ec91d6cdf53090bef92e.png)\n\n#### Server\n\n```go\nfunc (s *StreamService) Record(stream pb.StreamService_RecordServer) error {\n\tfor {\n\t\tr, err := stream.Recv()\n\t\tif err == io.EOF {\n\t\t\treturn stream.SendAndClose(&pb.StreamResponse{Pt: &pb.StreamPoint{Name: \"gRPC Stream Server: Record\", Value: 1}})\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tlog.Printf(\"stream.Recv pt.name: %s, pt.value: %d\", r.Pt.Name, r.Pt.Value)\n\t}\n\n\treturn nil\n}\n```\n\n多了一个从未见过的方法 `stream.SendAndClose`，它是做什么用的呢？\n\n在这段程序中，我们对每一个 Recv 都进行了处理，当发现 `io.EOF` (流关闭) 后，需要将最终的响应结果发送给客户端，同时关闭正在另外一侧等待的 Recv\n\n#### Client\n\n```go\nfunc printRecord(client pb.StreamServiceClient, r *pb.StreamRequest) error {\n\tstream, err := client.Record(context.Background())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor n := 0; n < 6; n++ {\n\t\terr := stream.Send(r)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tresp, err := stream.CloseAndRecv()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlog.Printf(\"resp: pj.name: %s, pt.value: %d\", resp.Pt.Name, resp.Pt.Value)\n\n\treturn nil\n}\n```\n\n`stream.CloseAndRecv` 和 `stream.SendAndClose` 是配套使用的流方法，相信聪明的你已经秒懂它的作用了\n\n#### 验证\n\n重启 stream_server/server.go，再次运行 stream_client/client.go：\n\n##### stream_client：\n\n```\n$ go run client.go\n2018/09/24 16:23:03 resp: pj.name: gRPC Stream Server: Record, pt.value: 1\n```\n\n##### stream_server：\n\n```\n$ go run server.go\n2018/09/24 16:23:03 stream.Recv pt.name: gRPC Stream Client: Record, pt.value: 2018\n2018/09/24 16:23:03 stream.Recv pt.name: gRPC Stream Client: Record, pt.value: 2018\n2018/09/24 16:23:03 stream.Recv pt.name: gRPC Stream Client: Record, pt.value: 2018\n2018/09/24 16:23:03 stream.Recv pt.name: gRPC Stream Client: Record, pt.value: 2018\n2018/09/24 16:23:03 stream.Recv pt.name: gRPC Stream Client: Record, pt.value: 2018\n2018/09/24 16:23:03 stream.Recv pt.name: gRPC Stream Client: Record, pt.value: 2018\n```\n\n### 三、Bidirectional streaming RPC：双向流式 RPC\n\n双向流式 RPC，顾名思义是双向流。由客户端以流式的方式发起请求，服务端同样以流式的方式响应请求\n\n首个请求一定是 Client 发起，但具体交互方式（谁先谁后、一次发多少、响应多少、什么时候关闭）根据程序编写的方式来确定（可以结合协程）\n\n假设该双向流是**按顺序发送**的话，大致如图：\n\n![image](https://image.eddycjy.com/ab80297cd6715048a235e0c9b0f36091.png)\n\n还是要强调，双向流变化很大，因程序编写的不同而不同。**双向流图示无法适用不同的场景**\n\n#### Server\n\n```go\nfunc (s *StreamService) Route(stream pb.StreamService_RouteServer) error {\n\tn := 0\n\tfor {\n\t\terr := stream.Send(&pb.StreamResponse{\n\t\t\tPt: &pb.StreamPoint{\n\t\t\t\tName:  \"gPRC Stream Client: Route\",\n\t\t\t\tValue: int32(n),\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tr, err := stream.Recv()\n\t\tif err == io.EOF {\n\t\t\treturn nil\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tn++\n\n\t\tlog.Printf(\"stream.Recv pt.name: %s, pt.value: %d\", r.Pt.Name, r.Pt.Value)\n\t}\n\n\treturn nil\n}\n```\n\n#### Client\n\n```go\nfunc printRoute(client pb.StreamServiceClient, r *pb.StreamRequest) error {\n\tstream, err := client.Route(context.Background())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor n := 0; n <= 6; n++ {\n\t\terr = stream.Send(r)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tresp, err := stream.Recv()\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tlog.Printf(\"resp: pj.name: %s, pt.value: %d\", resp.Pt.Name, resp.Pt.Value)\n\t}\n\n\tstream.CloseSend()\n\n\treturn nil\n}\n```\n\n#### 验证\n\n重启 stream_server/server.go，再次运行 stream_client/client.go：\n\n##### stream_server\n\n```\n$ go run server.go\n2018/09/24 16:29:43 stream.Recv pt.name: gRPC Stream Client: Route, pt.value: 2018\n2018/09/24 16:29:43 stream.Recv pt.name: gRPC Stream Client: Route, pt.value: 2018\n2018/09/24 16:29:43 stream.Recv pt.name: gRPC Stream Client: Route, pt.value: 2018\n2018/09/24 16:29:43 stream.Recv pt.name: gRPC Stream Client: Route, pt.value: 2018\n2018/09/24 16:29:43 stream.Recv pt.name: gRPC Stream Client: Route, pt.value: 2018\n2018/09/24 16:29:43 stream.Recv pt.name: gRPC Stream Client: Route, pt.value: 2018\n```\n\n##### stream_client\n\n```\n$ go run client.go\n2018/09/24 16:29:43 resp: pj.name: gPRC Stream Client: Route, pt.value: 0\n2018/09/24 16:29:43 resp: pj.name: gPRC Stream Client: Route, pt.value: 1\n2018/09/24 16:29:43 resp: pj.name: gPRC Stream Client: Route, pt.value: 2\n2018/09/24 16:29:43 resp: pj.name: gPRC Stream Client: Route, pt.value: 3\n2018/09/24 16:29:43 resp: pj.name: gPRC Stream Client: Route, pt.value: 4\n2018/09/24 16:29:43 resp: pj.name: gPRC Stream Client: Route, pt.value: 5\n2018/09/24 16:29:43 resp: pj.name: gPRC Stream Client: Route, pt.value: 6\n```\n\n## 总结\n\n在本文共介绍了三类流的交互方式，可以根据实际的业务场景去选择合适的方式。会事半功倍哦 🎑\n\n## 参考\n\n### 本系列示例代码\n\n- [go-grpc-example](https://github.com/EDDYCJY/go-grpc-example)"
  },
  {
    "path": "content/posts/go/grpc/2018-10-07-grpc-tls.md",
    "content": "---\n\ntitle:      \"「连载四」TLS 证书认证\"\ndate:       2018-10-07 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - grpc\n---\n\n## 前言\n\n在前面的章节里，我们介绍了 gRPC 的四种 API 使用方式。是不是很简单呢 😀\n\n此时存在一个安全问题，先前的例子中 gRPC Client/Server 都是明文传输的，会不会有被窃听的风险呢？\n\n从结论上来讲，是有的。在明文通讯的情况下，你的请求就是裸奔的，有可能被第三方恶意篡改或者伪造为“非法”的数据\n\n## 抓个包\n\n![image](https://image.eddycjy.com/15e68df2ba9aa7cace3e26e35c79f200.jpg)\n\n![image](https://image.eddycjy.com/ebebd3ea7d306ad2fcd311f1d8b46cc0.jpg)\n\n嗯，明文传输无误。这是有问题的，接下将改造我们的 gRPC，以便于解决这个问题 😤\n\n## 证书生成\n\n### 私钥\n\n```\nopenssl ecparam -genkey -name secp384r1 -out server.key\n```\n\n### 自签公钥\n\n```\nopenssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650\n```\n\n#### 填写信息\n\n```\nCountry Name (2 letter code) []:\nState or Province Name (full name) []:\nLocality Name (eg, city) []:\nOrganization Name (eg, company) []:\nOrganizational Unit Name (eg, section) []:\nCommon Name (eg, fully qualified host name) []:go-grpc-example\nEmail Address []:\n```\n\n### 生成完毕\n\n生成证书结束后，将证书相关文件放到 conf/ 下，目录结构：\n\n```\n$ tree go-grpc-example\ngo-grpc-example\n├── client\n├── conf\n│   ├── server.key\n│   └── server.pem\n├── proto\n└── server\n    ├── simple_server\n    └── stream_server\n```\n\n由于本文偏向 gRPC，详解可参见 [《制作证书》](https://segmentfault.com/a/1190000013408485#articleHeader3)。后续番外可能会展开细节描述 👌\n\n## 为什么之前不需要证书\n\n在 simple_server 中，为什么“啥事都没干”就能在不需要证书的情况下运行呢？\n\n### Server\n\n```go\ngrpc.NewServer()\n```\n\n在服务端显然没有传入任何 DialOptions\n\n### Client\n\n```go\nconn, err := grpc.Dial(\":\"+PORT, grpc.WithInsecure())\n```\n\n在客户端留意到 `grpc.WithInsecure()` 方法\n\n```go\nfunc WithInsecure() DialOption {\n\treturn newFuncDialOption(func(o *dialOptions) {\n\t\to.insecure = true\n\t})\n}\n```\n\n在方法内可以看到 `WithInsecure` 返回一个 `DialOption`，并且它最终会通过读取设置的值来禁用安全传输\n\n那么它“最终”又是在哪里处理的呢，我们把视线移到 `grpc.Dial()` 方法内\n\n```go\nfunc DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) {\n    ...\n\n    for _, opt := range opts {\n\t\topt.apply(&cc.dopts)\n\t}\n    ...\n\n    if !cc.dopts.insecure {\n\t\tif cc.dopts.copts.TransportCredentials == nil {\n\t\t\treturn nil, errNoTransportSecurity\n\t\t}\n\t} else {\n\t\tif cc.dopts.copts.TransportCredentials != nil {\n\t\t\treturn nil, errCredentialsConflict\n\t\t}\n\t\tfor _, cd := range cc.dopts.copts.PerRPCCredentials {\n\t\t\tif cd.RequireTransportSecurity() {\n\t\t\t\treturn nil, errTransportCredentialsMissing\n\t\t\t}\n\t\t}\n\t}\n\t...\n\n\tcreds := cc.dopts.copts.TransportCredentials\n\tif creds != nil && creds.Info().ServerName != \"\" {\n\t\tcc.authority = creds.Info().ServerName\n\t} else if cc.dopts.insecure && cc.dopts.authority != \"\" {\n\t\tcc.authority = cc.dopts.authority\n\t} else {\n\t\t// Use endpoint from \"scheme://authority/endpoint\" as the default\n\t\t// authority for ClientConn.\n\t\tcc.authority = cc.parsedTarget.Endpoint\n\t}\n\t...\n}\n```\n\n## gRPC\n\n接下来我们将正式开始编码，在 gRPC Client/Server 上实现 TLS 证书认证的支持 🤔\n\n### TLS Server\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"net\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials\"\n\n\tpb \"github.com/EDDYCJY/go-grpc-example/proto\"\n)\n\n...\n\nconst PORT = \"9001\"\n\nfunc main() {\n\tc, err := credentials.NewServerTLSFromFile(\"../../conf/server.pem\", \"../../conf/server.key\")\n\tif err != nil {\n\t\tlog.Fatalf(\"credentials.NewServerTLSFromFile err: %v\", err)\n\t}\n\n\tserver := grpc.NewServer(grpc.Creds(c))\n\tpb.RegisterSearchServiceServer(server, &SearchService{})\n\n\tlis, err := net.Listen(\"tcp\", \":\"+PORT)\n\tif err != nil {\n\t\tlog.Fatalf(\"net.Listen err: %v\", err)\n\t}\n\n\tserver.Serve(lis)\n}\n```\n\n- credentials.NewServerTLSFromFile：根据服务端输入的证书文件和密钥构造 TLS 凭证\n\n```go\nfunc NewServerTLSFromFile(certFile, keyFile string) (TransportCredentials, error) {\n\tcert, err := tls.LoadX509KeyPair(certFile, keyFile)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn NewTLS(&tls.Config{Certificates: []tls.Certificate{cert}}), nil\n}\n```\n\n- grpc.Creds()：返回一个 ServerOption，用于设置服务器连接的凭据。用于 `grpc.NewServer(opt ...ServerOption)` 为 gRPC Server 设置连接选项\n\n```go\nfunc Creds(c credentials.TransportCredentials) ServerOption {\n\treturn func(o *options) {\n\t\to.creds = c\n\t}\n}\n```\n\n经过以上两个简单步骤，gRPC Server 就建立起需证书认证的服务啦 🤔\n\n### TLS Client\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials\"\n\n\tpb \"github.com/EDDYCJY/go-grpc-example/proto\"\n)\n\nconst PORT = \"9001\"\n\nfunc main() {\n\tc, err := credentials.NewClientTLSFromFile(\"../../conf/server.pem\", \"go-grpc-example\")\n\tif err != nil {\n\t\tlog.Fatalf(\"credentials.NewClientTLSFromFile err: %v\", err)\n\t}\n\n\tconn, err := grpc.Dial(\":\"+PORT, grpc.WithTransportCredentials(c))\n\tif err != nil {\n\t\tlog.Fatalf(\"grpc.Dial err: %v\", err)\n\t}\n\tdefer conn.Close()\n\n\tclient := pb.NewSearchServiceClient(conn)\n\tresp, err := client.Search(context.Background(), &pb.SearchRequest{\n\t\tRequest: \"gRPC\",\n\t})\n\tif err != nil {\n\t\tlog.Fatalf(\"client.Search err: %v\", err)\n\t}\n\n\tlog.Printf(\"resp: %s\", resp.GetResponse())\n}\n```\n\n- credentials.NewClientTLSFromFile()：根据客户端输入的证书文件和密钥构造 TLS 凭证。serverNameOverride 为服务名称\n\n```go\nfunc NewClientTLSFromFile(certFile, serverNameOverride string) (TransportCredentials, error) {\n\tb, err := ioutil.ReadFile(certFile)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcp := x509.NewCertPool()\n\tif !cp.AppendCertsFromPEM(b) {\n\t\treturn nil, fmt.Errorf(\"credentials: failed to append certificates\")\n\t}\n\treturn NewTLS(&tls.Config{ServerName: serverNameOverride, RootCAs: cp}), nil\n}\n```\n\n- grpc.WithTransportCredentials()：返回一个配置连接的 DialOption 选项。用于 `grpc.Dial(target string, opts ...DialOption)` 设置连接选项\n\n```go\nfunc WithTransportCredentials(creds credentials.TransportCredentials) DialOption {\n\treturn newFuncDialOption(func(o *dialOptions) {\n\t\to.copts.TransportCredentials = creds\n\t})\n}\n```\n\n## 验证\n\n### 请求\n\n重新启动 server.go 和执行 client.go，得到响应结果\n\n```\n$ go run client.go\n2018/09/30 20:00:21 resp: gRPC Server\n```\n\n### 抓个包\n\n![image](https://image.eddycjy.com/c8ad6edf1f7d084883b847b3eee29dd2.jpg)\n\n成功。\n\n## 总结\n\n在本章节我们实现了 gRPC TLS Client/Servert，你以为大功告成了吗？我不 😤\n\n## 问题\n\n你仔细再看看，Client 是基于 Server 端的证书和服务名称来建立请求的。这样的话，你就需要将 Server 的证书通过各种手段给到 Client 端，否则是无法完成这项任务的\n\n问题也就来了，你无法保证你的“各种手段”是安全的，毕竟现在的网络环境是很危险的，万一被...\n\n我们将在下一章节解决这个问题，保证其可靠性 🙂\n\n## 参考\n\n### 本系列示例代码\n\n- [go-grpc-example](https://github.com/EDDYCJY/go-grpc-example)"
  },
  {
    "path": "content/posts/go/grpc/2018-10-08-ca-tls.md",
    "content": "---\n\ntitle:      \"「连载五」基于 CA 的 TLS 证书认证\"\ndate:       2018-10-08 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - grpc\n---\n\n## 前言\n\n在上一章节中，我们提出了一个问题。就是如何保证证书的可靠性和有效性？你如何确定你 Server、Client 的证书是对的呢？\n\n## CA\n\n为了保证证书的可靠性和有效性，在这里可引入 CA 颁发的根证书的概念。其遵守 X.509 标准\n\n### 根证书\n\n根证书（root certificate）是属于根证书颁发机构（CA）的公钥证书。我们可以通过验证 CA 的签名从而信任 CA ，任何人都可以得到 CA 的证书（含公钥），用以验证它所签发的证书（客户端、服务端）\n\n\n它包含的文件如下：\n\n- 公钥\n- 密钥\n\n### 生成 Key\n\n```\nopenssl genrsa -out ca.key 2048\n```\n\n### 生成密钥\n\n```\nopenssl req -new -x509 -days 7200 -key ca.key -out ca.pem\n```\n\n#### 填写信息\n\n```\nCountry Name (2 letter code) []:\nState or Province Name (full name) []:\nLocality Name (eg, city) []:\nOrganization Name (eg, company) []:\nOrganizational Unit Name (eg, section) []:\nCommon Name (eg, fully qualified host name) []:go-grpc-example\nEmail Address []:\n```\n\n### Server\n\n#### 生成 CSR\n\n```\nopenssl req -new -key server.key -out server.csr\n```\n\n##### 填写信息\n\n```\nCountry Name (2 letter code) []:\nState or Province Name (full name) []:\nLocality Name (eg, city) []:\nOrganization Name (eg, company) []:\nOrganizational Unit Name (eg, section) []:\nCommon Name (eg, fully qualified host name) []:go-grpc-example\nEmail Address []:\n\nPlease enter the following 'extra' attributes\nto be sent with your certificate request\nA challenge password []:\n```\n\nCSR 是 Cerificate Signing Request 的英文缩写，为证书请求文件。主要作用是 CA 会利用 CSR 文件进行签名使得攻击者无法伪装或篡改原有证书\n\n#### 基于 CA 签发\n\n```\nopenssl x509 -req -sha256 -CA ca.pem -CAkey ca.key -CAcreateserial -days 3650 -in server.csr -out server.pem\n```\n\n### Client\n\n### 生成 Key\n\n```\nopenssl ecparam -genkey -name secp384r1 -out client.key\n```\n\n### 生成 CSR\n\n```\nopenssl req -new -key client.key -out client.csr\n```\n\n#### 基于 CA 签发\n\n```\nopenssl x509 -req -sha256 -CA ca.pem -CAkey ca.key -CAcreateserial -days 3650 -in client.csr -out client.pem\n```\n\n### 整理目录\n\n至此我们生成了一堆文件，请按照以下目录结构存放：\n\n```\n$ tree conf \nconf\n├── ca.key\n├── ca.pem\n├── ca.srl\n├── client\n│   ├── client.csr\n│   ├── client.key\n│   └── client.pem\n└── server\n    ├── server.csr\n    ├── server.key\n    └── server.pem\n```\n\n另外有一些文件是不应该出现在仓库内，应当保密或删除的。但为了真实演示所以保留着（敲黑板）\n\n## gRPC\n\n接下来将正式开始针对 gRPC 进行编码，改造上一章节的代码。目标是基于 CA 进行 TLS 认证 🤫\n\n### Server\n\n```\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"net\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"io/ioutil\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials\"\n\n\tpb \"github.com/EDDYCJY/go-grpc-example/proto\"\n)\n\n...\n\nconst PORT = \"9001\"\n\nfunc main() {\n\tcert, err := tls.LoadX509KeyPair(\"../../conf/server/server.pem\", \"../../conf/server/server.key\")\n\tif err != nil {\n\t\tlog.Fatalf(\"tls.LoadX509KeyPair err: %v\", err)\n\t}\n\n\tcertPool := x509.NewCertPool()\n\tca, err := ioutil.ReadFile(\"../../conf/ca.pem\")\n\tif err != nil {\n\t\tlog.Fatalf(\"ioutil.ReadFile err: %v\", err)\n\t}\n\n\tif ok := certPool.AppendCertsFromPEM(ca); !ok {\n\t\tlog.Fatalf(\"certPool.AppendCertsFromPEM err\")\n\t}\n\n\tc := credentials.NewTLS(&tls.Config{\n\t\tCertificates: []tls.Certificate{cert},\n\t\tClientAuth:   tls.RequireAndVerifyClientCert,\n\t\tClientCAs:    certPool,\n\t})\n\n\tserver := grpc.NewServer(grpc.Creds(c))\n\tpb.RegisterSearchServiceServer(server, &SearchService{})\n\n\tlis, err := net.Listen(\"tcp\", \":\"+PORT)\n\tif err != nil {\n\t\tlog.Fatalf(\"net.Listen err: %v\", err)\n\t}\n\n\tserver.Serve(lis)\n}\n```\n\n- tls.LoadX509KeyPair()：从证书相关文件中**读取**和**解析**信息，得到证书公钥、密钥对\n\n```\nfunc LoadX509KeyPair(certFile, keyFile string) (Certificate, error) {\n\tcertPEMBlock, err := ioutil.ReadFile(certFile)\n\tif err != nil {\n\t\treturn Certificate{}, err\n\t}\n\tkeyPEMBlock, err := ioutil.ReadFile(keyFile)\n\tif err != nil {\n\t\treturn Certificate{}, err\n\t}\n\treturn X509KeyPair(certPEMBlock, keyPEMBlock)\n}\n```\n\n- x509.NewCertPool()：创建一个新的、空的 CertPool\n- certPool.AppendCertsFromPEM()：尝试解析所传入的 PEM 编码的证书。如果解析成功会将其加到 CertPool 中，便于后面的使用\n- credentials.NewTLS：构建基于 TLS 的 TransportCredentials 选项\n- tls.Config：Config 结构用于配置 TLS 客户端或服务器\n\n在 Server，共使用了三个 Config 配置项：\n\n（1）Certificates：设置证书链，允许包含一个或多个\n\n（2）ClientAuth：要求必须校验客户端的证书。可以根据实际情况选用以下参数：\n\n```\nconst (\n\tNoClientCert ClientAuthType = iota\n\tRequestClientCert\n\tRequireAnyClientCert\n\tVerifyClientCertIfGiven\n\tRequireAndVerifyClientCert\n)\n```\n\n（3）ClientCAs：设置根证书的集合，校验方式使用 ClientAuth 中设定的模式\n\n### Client\n\n```\npackage main\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"io/ioutil\"\n\t\"log\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials\"\n\n\tpb \"github.com/EDDYCJY/go-grpc-example/proto\"\n)\n\nconst PORT = \"9001\"\n\nfunc main() {\n\tcert, err := tls.LoadX509KeyPair(\"../../conf/client/client.pem\", \"../../conf/client/client.key\")\n\tif err != nil {\n\t\tlog.Fatalf(\"tls.LoadX509KeyPair err: %v\", err)\n\t}\n\n\tcertPool := x509.NewCertPool()\n\tca, err := ioutil.ReadFile(\"../../conf/ca.pem\")\n\tif err != nil {\n\t\tlog.Fatalf(\"ioutil.ReadFile err: %v\", err)\n\t}\n\n\tif ok := certPool.AppendCertsFromPEM(ca); !ok {\n\t\tlog.Fatalf(\"certPool.AppendCertsFromPEM err\")\n\t}\n\n\tc := credentials.NewTLS(&tls.Config{\n\t\tCertificates: []tls.Certificate{cert},\n\t\tServerName:   \"go-grpc-example\",\n\t\tRootCAs:      certPool,\n\t})\n\n\tconn, err := grpc.Dial(\":\"+PORT, grpc.WithTransportCredentials(c))\n\tif err != nil {\n\t\tlog.Fatalf(\"grpc.Dial err: %v\", err)\n\t}\n\tdefer conn.Close()\n\n\tclient := pb.NewSearchServiceClient(conn)\n\tresp, err := client.Search(context.Background(), &pb.SearchRequest{\n\t\tRequest: \"gRPC\",\n\t})\n\tif err != nil {\n\t\tlog.Fatalf(\"client.Search err: %v\", err)\n\t}\n\n\tlog.Printf(\"resp: %s\", resp.GetResponse())\n}\n```\n\n在 Client 中绝大部分与 Server 一致，不同点的地方是，在 Client 请求 Server 端时，Client 端会使用根证书和 ServerName 去对 Server 端进行校验\n\n简单流程大致如下：\n\n1. Client 通过请求得到 Server 端的证书\n2. 使用 CA 认证的根证书对 Server 端的证书进行可靠性、有效性等校验\n3. 校验 ServerName 是否可用、有效\n\n当然了，在设置了 `tls.RequireAndVerifyClientCert` 模式的情况下，Server 也会使用 CA 认证的根证书对 Client 端的证书进行可靠性、有效性等校验。也就是两边都会进行校验，极大的保证了安全性 👍\n\n### 验证\n\n重新启动 server.go 和执行 client.go，查看响应结果是否正常\n\n## 总结\n\n在本章节，我们使用 CA 颁发的根证书对客户端、服务端的证书进行了签发。进一步的提高了两者的通讯安全 \n\n这回是真的大功告成了！\n\n## 参考\n\n### 本系列示例代码\n\n- [go-grpc-example](https://github.com/EDDYCJY/go-grpc-example)"
  },
  {
    "path": "content/posts/go/grpc/2018-10-10-interceptor.md",
    "content": "---\n\ntitle:      \"「连载六」Unary and Stream interceptor\"\ndate:       2018-10-10 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - grpc\n---\n\n## 前言\n\n我想在每个 RPC 方法的前或后做某些事情，怎么做？\n\n本章节将要介绍的拦截器（interceptor），就能帮你在合适的地方实现这些功能。\n\n## 有几种方法\n\n在 gRPC 中，大类可分为两种 RPC 方法，与拦截器的对应关系是：\n\n- 普通方法：一元拦截器（grpc.UnaryInterceptor）\n- 流方法：流拦截器（grpc.StreamInterceptor）\n\n\n## 看一看\n\n### grpc.UnaryInterceptor\n\n```\nfunc UnaryInterceptor(i UnaryServerInterceptor) ServerOption {\n\treturn func(o *options) {\n\t\tif o.unaryInt != nil {\n\t\t\tpanic(\"The unary server interceptor was already set and may not be reset.\")\n\t\t}\n\t\to.unaryInt = i\n\t}\n}\n```\n函数原型：\n```\ntype UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)\n```\n\n通过查看源码可得知，要完成一个拦截器需要实现 `UnaryServerInterceptor` 方法。形参如下：\n\n- ctx context.Context：请求上下文\n- req interface{}：RPC 方法的请求参数\n- info *UnaryServerInfo：RPC 方法的所有信息\n- handler UnaryHandler：RPC 方法本身\n\n### grpc.StreamInterceptor\n\n```\nfunc StreamInterceptor(i StreamServerInterceptor) ServerOption\n```\n函数原型：\n```\ntype StreamServerInterceptor func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error\n```\n\nStreamServerInterceptor 与 UnaryServerInterceptor 形参的意义是一样，不再赘述\n\n### 如何实现多个拦截器\n\n另外，可以发现 gRPC 本身居然只能设置一个拦截器，难道所有的逻辑都只能写在一起？\n\n关于这一点，你可以放心。采用开源项目 [go-grpc-middleware](https://github.com/grpc-ecosystem/go-grpc-middleware) 就可以解决这个问题，本章也会使用它。\n\n```\nimport \"github.com/grpc-ecosystem/go-grpc-middleware\"\n\nmyServer := grpc.NewServer(\n    grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(\n        ...\n    )),\n    grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(\n       ...\n    )),\n)\n```\n\n## gRPC\n\n从本节开始编写 gRPC interceptor 的代码，我们会将实现以下拦截器：\n\n- logging：RPC 方法的入参出参的日志输出\n- recover：RPC 方法的异常保护和日志输出\n\n### 实现 interceptor\n\n#### logging\n\n```\nfunc LoggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {\n\tlog.Printf(\"gRPC method: %s, %v\", info.FullMethod, req)\n\tresp, err := handler(ctx, req)\n\tlog.Printf(\"gRPC method: %s, %v\", info.FullMethod, resp)\n\treturn resp, err\n}\n```\n\n#### recover\n\n```\nfunc RecoveryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {\n\tdefer func() {\n\t\tif e := recover(); e != nil {\n\t\t\tdebug.PrintStack()\n\t\t\terr = status.Errorf(codes.Internal, \"Panic err: %v\", e)\n\t\t}\n\t}()\n\n\treturn handler(ctx, req)\n}\n```\n\n### Server\n\n```\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net\"\n\t\"runtime/debug\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials\"\n\t\"google.golang.org/grpc/status\"\n\t\"google.golang.org/grpc/codes\"\n\t\"github.com/grpc-ecosystem/go-grpc-middleware\"\n\n\tpb \"github.com/EDDYCJY/go-grpc-example/proto\"\n)\n\n...\n\nfunc main() {\n\tc, err := GetTLSCredentialsByCA()\n\tif err != nil {\n\t\tlog.Fatalf(\"GetTLSCredentialsByCA err: %v\", err)\n\t}\n\n\topts := []grpc.ServerOption{\n\t\tgrpc.Creds(c),\n\t\tgrpc_middleware.WithUnaryServerChain(\n\t\t\tRecoveryInterceptor,\n\t\t\tLoggingInterceptor,\n\t\t),\n\t}\n\n\tserver := grpc.NewServer(opts...)\n\tpb.RegisterSearchServiceServer(server, &SearchService{})\n\n\tlis, err := net.Listen(\"tcp\", \":\"+PORT)\n\tif err != nil {\n\t\tlog.Fatalf(\"net.Listen err: %v\", err)\n\t}\n\n\tserver.Serve(lis)\n}\n```\n\n## 验证\n\n### logging\n\n启动 simple_server/server.go，执行 simple_client/client.go 发起请求，得到结果：\n\n```\n$ go run server.go\n2018/10/02 13:46:35 gRPC method: /proto.SearchService/Search, request:\"gRPC\" \n2018/10/02 13:46:35 gRPC method: /proto.SearchService/Search, response:\"gRPC Server\"\n```\n\n### recover\n\n在 RPC 方法中人为地制造运行时错误，再重复启动 server/client.go，得到结果：\n\n#### client\n\n```\n$ go run client.go\n2018/10/02 13:19:03 client.Search err: rpc error: code = Internal desc = Panic err: assignment to entry in nil map\nexit status 1\n```\n\n#### server\n\n```\n$ go run server.go\ngoroutine 23 [running]:\nruntime/debug.Stack(0xc420223588, 0x1033da9, 0xc420001980)\n\t/usr/local/Cellar/go/1.10.1/libexec/src/runtime/debug/stack.go:24 +0xa7\nruntime/debug.PrintStack()\n\t/usr/local/Cellar/go/1.10.1/libexec/src/runtime/debug/stack.go:16 +0x22\nmain.RecoveryInterceptor.func1(0xc420223a10)\n...\n```\n\n检查服务是否仍然运行，即可知道 Recovery 是否成功生效\n\n## 总结\n\n通过本章节，你可以学会最常见的拦截器使用方法。接下来其它“新”需求只要举一反三即可。\n\n## 参考\n\n### 本系列示例代码\n\n- [go-grpc-example](https://github.com/EDDYCJY/go-grpc-example)"
  },
  {
    "path": "content/posts/go/grpc/2018-10-12-grpc-http.md",
    "content": "---\n\ntitle:      \"「连载七」让你的服务同时提供 HTTP 接口\"\ndate:       2018-10-12 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - grpc\n---\n\n## 前言\n\n- 接口需要提供给其他业务组访问，但是 RPC 协议不同无法内调，对方问能否走 HTTP 接口，怎么办？\n\n- 微信（公众号、小程序）等第三方回调接口只支持 HTTP 接口，怎么办\n\n我相信你在实际工作中都会遇到如上问题，在 gRPC 中都是有解决方案的，本章节将会进行介绍 🤔 \n\n## 为什么可以同时提供 HTTP 接口\n\n关键一点，gRPC 的协议是基于 HTTP/2 的，因此应用程序能够在单个 TCP 端口上提供 HTTP/1.1 和 gRPC 接口服务（两种不同的流量）\n\n## 怎么同时提供 HTTP 接口\n\n### 检测协议\n\n```\nif r.ProtoMajor == 2 && strings.Contains(r.Header.Get(\"Content-Type\"), \"application/grpc\") {\n    server.ServeHTTP(w, r)\n} else {\n    mux.ServeHTTP(w, r)\n}\n```\n\n### 流程\n\n1. 检测请求协议是否为 HTTP/2\n2. 判断 Content-Type 是否为 application/grpc（gRPC 的默认标识位）\n3. 根据协议的不同转发到不同的服务处理\n\n## gRPC\n\n### TLS\n\n在前面的章节，为了便于展示因此没有简单封装\n\n在本节需复用代码，重新封装了，可详见：[go-grpc-example](https://github.com/EDDYCJY/go-grpc-example/tree/master/pkg/gtls)\n\n### 目录结构\n\n新建 simple_http_client、simple_http_server 目录，目录结构如下：\n\n```\ngo-grpc-example\n├── client\n│   ├── simple_client\n│   ├── simple_http_client\n│   └── stream_client\n├── conf\n├── pkg\n│   └── gtls\n├── proto\n├── server\n│   ├── simple_http_server\n│   ├── simple_server\n│   └── stream_server\n```\n\n### Server\n\n在 simple_http_server 目录下新建 server.go，写入文件内容：\n\n```\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/EDDYCJY/go-grpc-example/pkg/gtls\"\n\tpb \"github.com/EDDYCJY/go-grpc-example/proto\"\n\n\t\"google.golang.org/grpc\"\n)\n\ntype SearchService struct{}\n\nfunc (s *SearchService) Search(ctx context.Context, r *pb.SearchRequest) (*pb.SearchResponse, error) {\n\treturn &pb.SearchResponse{Response: r.GetRequest() + \" HTTP Server\"}, nil\n}\n\nconst PORT = \"9003\"\n\nfunc main() {\n\tcertFile := \"../../conf/server/server.pem\"\n\tkeyFile := \"../../conf/server/server.key\"\n\ttlsServer := gtls.Server{\n\t\tCertFile: certFile,\n\t\tKeyFile:  keyFile,\n\t}\n\n\tc, err := tlsServer.GetTLSCredentials()\n\tif err != nil {\n\t\tlog.Fatalf(\"tlsServer.GetTLSCredentials err: %v\", err)\n\t}\n\n\tmux := GetHTTPServeMux()\n\n\tserver := grpc.NewServer(grpc.Creds(c))\n\tpb.RegisterSearchServiceServer(server, &SearchService{})\n\n\thttp.ListenAndServeTLS(\":\"+PORT,\n\t\tcertFile,\n\t\tkeyFile,\n\t\thttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tif r.ProtoMajor == 2 && strings.Contains(r.Header.Get(\"Content-Type\"), \"application/grpc\") {\n\t\t\t\tserver.ServeHTTP(w, r)\n\t\t\t} else {\n\t\t\t\tmux.ServeHTTP(w, r)\n\t\t\t}\n\n\t\t\treturn\n\t\t}),\n\t)\n}\n\nfunc GetHTTPServeMux() *http.ServeMux {\n\tmux := http.NewServeMux()\n\tmux.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Write([]byte(\"eddycjy: go-grpc-example\"))\n\t})\n\n\treturn mux\n}\n```\n\n- http.NewServeMux：创建一个新的 ServeMux，ServeMux 本质上是一个路由表。它默认实现了 ServeHTTP，因此返回 Handler 后可直接通过 HandleFunc 注册 pattern 和处理逻辑的方法\n- http.ListenAndServeTLS：可简单的理解为提供监听 HTTPS 服务的方法，重点的协议判断转发，也在这里面\n\n其实，你理解后就会觉得很简单，核心步骤：判断 -> 转发 -> 响应。我们改变了前两步的默认逻辑，仅此而已\n\n### Client\n\n在 simple_http_server 目录下新建 client.go，写入文件内容：\n\n```\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\n\t\"google.golang.org/grpc\"\n\n\t\"github.com/EDDYCJY/go-grpc-example/pkg/gtls\"\n\tpb \"github.com/EDDYCJY/go-grpc-example/proto\"\n)\n\nconst PORT = \"9003\"\n\nfunc main() {\n\ttlsClient := gtls.Client{\n\t\tServerName: \"go-grpc-example\",\n\t\tCertFile:   \"../../conf/server/server.pem\",\n\t}\n\tc, err := tlsClient.GetTLSCredentials()\n\tif err != nil {\n\t\tlog.Fatalf(\"tlsClient.GetTLSCredentials err: %v\", err)\n\t}\n\n\tconn, err := grpc.Dial(\":\"+PORT, grpc.WithTransportCredentials(c))\n\tif err != nil {\n\t\tlog.Fatalf(\"grpc.Dial err: %v\", err)\n\t}\n\tdefer conn.Close()\n\n\tclient := pb.NewSearchServiceClient(conn)\n\tresp, err := client.Search(context.Background(), &pb.SearchRequest{\n\t\tRequest: \"gRPC\",\n\t})\n\tif err != nil {\n\t\tlog.Fatalf(\"client.Search err: %v\", err)\n\t}\n\n\tlog.Printf(\"resp: %s\", resp.GetResponse())\n}\n```\n\n## 验证\n\n### gRPC Client\n\n```\n$ go run client.go \n2018/10/04 14:56:56 resp: gRPC HTTP Server\n```\n\n### HTTP/1.1 访问\n\n![image](https://image.eddycjy.com/1d92cb9e949e32eef7f8a64a6a77deb9.jpg)\n\n## 总结\n\n通过本章节，表面上完成了同端口提供双服务的功能，但实际上，应该是加深了 HTTP/2 的理解和使用，这才是本质\n\n## 拓展\n\n如果你有一个需求，是要**同时提供** RPC 和 RESTful JSON API 两种接口的，不要犹豫，点进去：[gRPC + gRPC Gateway 实践](https://segmentfault.com/a/1190000013339403)\n\n## 问题\n\n你以为这个方案就万能了吗，不。Envoy Proxy 的支持就不完美，无法同时监听一个端口的两种流量 😤\n\n## 参考\n\n### 本系列示例代码\n\n- [go-grpc-example](https://github.com/EDDYCJY/go-grpc-example)"
  },
  {
    "path": "content/posts/go/grpc/2018-10-14-per-rpc-credentials.md",
    "content": "---\n\ntitle:      \"「连载八」对 RPC 方法做自定义认证\"\ndate:       2018-10-14 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - grpc\n---\n\n## 前言\n\n在前面的章节中，我们介绍了两种（证书算一种）可全局认证的方法：\n\n1. [TLS 证书认证](https://github.com/EDDYCJY/blog/blob/master/grpc/grpc-tls.md)\n2. [基于 CA 的 TLS 证书认证](https://github.com/EDDYCJY/blog/blob/master/grpc/ca-tls.md)\n3. [Unary and Stream interceptor](https://github.com/EDDYCJY/blog/blob/master/grpc/interceptor.md)\n\n\n而在实际需求中，常常会对某些模块的 RPC 方法做特殊认证或校验。今天将会讲解、实现这块的功能点\n\n## 课前知识\n\n```\ntype PerRPCCredentials interface {\n    GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)\n    RequireTransportSecurity() bool\n}\n```\n\n在 gRPC 中默认定义了 PerRPCCredentials，它就是本章节的主角，是 gRPC 默认提供用于自定义认证的接口，它的作用是将所需的安全认证信息添加到每个 RPC 方法的上下文中。其包含 2 个方法：\n\n- GetRequestMetadata：获取当前请求认证所需的元数据（metadata）\n- RequireTransportSecurity：是否需要基于 TLS 认证进行安全传输\n\n## 目录结构\n\n新建 simple_token_server/server.go 和 simple_token_client/client.go，目录结构如下：\n\n```\ngo-grpc-example\n├── client\n│   ├── simple_client\n│   ├── simple_http_client\n│   ├── simple_token_client\n│   └── stream_client\n├── conf\n├── pkg\n├── proto\n├── server\n│   ├── simple_http_server\n│   ├── simple_server\n│   ├── simple_token_server\n│   └── stream_server\n└── vendor\n```\n\n## gRPC\n\n### Client\n\n```\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\n\t\"google.golang.org/grpc\"\n\n\t\"github.com/EDDYCJY/go-grpc-example/pkg/gtls\"\n\tpb \"github.com/EDDYCJY/go-grpc-example/proto\"\n)\n\nconst PORT = \"9004\"\n\ntype Auth struct {\n\tAppKey    string\n\tAppSecret string\n}\n\nfunc (a *Auth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {\n\treturn map[string]string{\"app_key\": a.AppKey, \"app_secret\": a.AppSecret}, nil\n}\n\nfunc (a *Auth) RequireTransportSecurity() bool {\n\treturn true\n}\n\nfunc main() {\n\ttlsClient := gtls.Client{\n\t\tServerName: \"go-grpc-example\",\n\t\tCertFile:   \"../../conf/server/server.pem\",\n\t}\n\tc, err := tlsClient.GetTLSCredentials()\n\tif err != nil {\n\t\tlog.Fatalf(\"tlsClient.GetTLSCredentials err: %v\", err)\n\t}\n\n\tauth := Auth{\n\t\tAppKey:    \"eddycjy\",\n\t\tAppSecret: \"20181005\",\n\t}\n\tconn, err := grpc.Dial(\":\"+PORT, grpc.WithTransportCredentials(c), grpc.WithPerRPCCredentials(&auth))\n\t...\n}\n```\n\n在 Client 端，重点实现 `type PerRPCCredentials interface` 所需的方法，关注两点即可：\n\n- struct Auth：GetRequestMetadata、RequireTransportSecurity\n- grpc.WithPerRPCCredentials\n\n### Server\n\n```\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"net\"\n\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/codes\"\n\t\"google.golang.org/grpc/metadata\"\n\t\"google.golang.org/grpc/status\"\n\n\t\"github.com/EDDYCJY/go-grpc-example/pkg/gtls\"\n\tpb \"github.com/EDDYCJY/go-grpc-example/proto\"\n)\n\ntype SearchService struct {\n\tauth *Auth\n}\n\nfunc (s *SearchService) Search(ctx context.Context, r *pb.SearchRequest) (*pb.SearchResponse, error) {\n\tif err := s.auth.Check(ctx); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &pb.SearchResponse{Response: r.GetRequest() + \" Token Server\"}, nil\n}\n\nconst PORT = \"9004\"\n\nfunc main() {\n\t...\n}\n\ntype Auth struct {\n\tappKey    string\n\tappSecret string\n}\n\nfunc (a *Auth) Check(ctx context.Context) error {\n\tmd, ok := metadata.FromIncomingContext(ctx)\n\tif !ok {\n\t\treturn status.Errorf(codes.Unauthenticated, \"自定义认证 Token 失败\")\n\t}\n\n\tvar (\n\t\tappKey    string\n\t\tappSecret string\n\t)\n\tif value, ok := md[\"app_key\"]; ok {\n\t\tappKey = value[0]\n\t}\n\tif value, ok := md[\"app_secret\"]; ok {\n\t\tappSecret = value[0]\n\t}\n\n\tif appKey != a.GetAppKey() || appSecret != a.GetAppSecret() {\n\t\treturn status.Errorf(codes.Unauthenticated, \"自定义认证 Token 无效\")\n\t}\n\n\treturn nil\n}\n\nfunc (a *Auth) GetAppKey() string {\n\treturn \"eddycjy\"\n}\n\nfunc (a *Auth) GetAppSecret() string {\n\treturn \"20181005\"\n}\n```\n\n在 Server 端就更简单了，实际就是调用 `metadata.FromIncomingContext` 从上下文中获取 metadata，再在不同的 RPC 方法中进行认证检查\n\n### 验证\n\n重新启动 server.go 和 client.go，得到以下结果：\n\n```\n$ go run client.go\n2018/10/05 20:59:58 resp: gRPC Token Server\n```\n\n修改 client.go 的值，制造两者不一致，得到无效结果：\n\n```\n$ go run client.go\n2018/10/05 21:00:05 client.Search err: rpc error: code = Unauthenticated desc = invalid token\nexit status 1\n```\n\n### 一个个加太麻烦\n\n我相信你肯定会问一个个加，也太麻烦了吧？有这个想法的你，应当把 `type PerRPCCredentials interface` 做成一个拦截器（interceptor）\n\n## 总结\n\n本章节比较简单，主要是针对 RPC 方法的自定义认证进行了介绍，如果是想做全局的，建议是举一反三从拦截器下手哦。\n\n## 参考\n\n### 本系列示例代码\n\n- [go-grpc-example](https://github.com/EDDYCJY/go-grpc-example)\n"
  },
  {
    "path": "content/posts/go/grpc/2018-10-16-deadlines.md",
    "content": "---\n\ntitle:      \"「连载九」gRPC Deadlines\"\ndate:       2018-10-16 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - grpc\n---\n\n## 前言\n\n在前面的章节中，已经介绍了 gRPC 的基本用法。那你想想，让它这么裸跑真的没问题吗？\n\n那么，肯定是有问题了。今天将介绍 gRPC Deadlines 的用法，这一个必备技巧。内容也比较简单\n\n## Deadlines\n\nDeadlines 意指截止时间，在 gRPC 中强调 TL;DR（Too long, Don't read）并建议**始终设定截止日期**，为什么呢？\n\n### 为什么要设置\n\n当未设置 Deadlines 时，将采用默认的 DEADLINE_EXCEEDED（这个时间非常大）\n\n如果产生了阻塞等待，就会造成大量正在进行的请求都会被保留，并且所有请求都有可能达到最大超时\n\n这会使服务面临资源耗尽的风险，例如内存，这会增加服务的延迟，或者在最坏的情况下可能导致整个进程崩溃\n\n## gRPC\n\n### Client\n\n```\nfunc main() {\n    ...\n\tctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Duration(5 * time.Second)))\n\tdefer cancel()\n\n\tclient := pb.NewSearchServiceClient(conn)\n\tresp, err := client.Search(ctx, &pb.SearchRequest{\n\t\tRequest: \"gRPC\",\n\t})\n\tif err != nil {\n\t\tstatusErr, ok := status.FromError(err)\n\t\tif ok {\n\t\t\tif statusErr.Code() == codes.DeadlineExceeded {\n\t\t\t\tlog.Fatalln(\"client.Search err: deadline\")\n\t\t\t}\n\t\t}\n\n\t\tlog.Fatalf(\"client.Search err: %v\", err)\n\t}\n\n\tlog.Printf(\"resp: %s\", resp.GetResponse())\n}\n```\n\n- context.WithDeadline：会返回最终上下文截止时间。第一个形参为父上下文，第二个形参为调整的截止时间。若父级时间早于子级时间，则以父级时间为准，否则以子级时间为最终截止时间\n\n```\nfunc WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {\n\tif cur, ok := parent.Deadline(); ok && cur.Before(d) {\n\t\t// The current deadline is already sooner than the new one.\n\t\treturn WithCancel(parent)\n\t}\n\tc := &timerCtx{\n\t\tcancelCtx: newCancelCtx(parent),\n\t\tdeadline:  d,\n\t}\n\tpropagateCancel(parent, c)\n\tdur := time.Until(d)\n\tif dur <= 0 {\n\t\tc.cancel(true, DeadlineExceeded) // deadline has already passed\n\t\treturn c, func() { c.cancel(true, Canceled) }\n\t}\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\tif c.err == nil {\n\t\tc.timer = time.AfterFunc(dur, func() {\n\t\t\tc.cancel(true, DeadlineExceeded)\n\t\t})\n\t}\n\treturn c, func() { c.cancel(true, Canceled) }\n}\n```\n\n- context.WithTimeout：很常见的另外一个方法，是便捷操作。实际上是对于 WithDeadline 的封装\n\n```\nfunc WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {\n\treturn WithDeadline(parent, time.Now().Add(timeout))\n}\n```\n\n- status.FromError：返回 GRPCStatus 的具体错误码，若为非法，则直接返回 `codes.Unknown`\n\n### Server\n\n```\ntype SearchService struct{}\n\nfunc (s *SearchService) Search(ctx context.Context, r *pb.SearchRequest) (*pb.SearchResponse, error) {\n\tfor i := 0; i < 5; i++  {\n\t\tif ctx.Err() == context.Canceled {\n\t\t\treturn nil, status.Errorf(codes.Canceled, \"SearchService.Search canceled\")\n\t\t}\n\n\t\ttime.Sleep(1 * time.Second)\n\t}\n\n\treturn &pb.SearchResponse{Response: r.GetRequest() + \" Server\"}, nil\n}\n\nfunc main() {\n\t...\n}\n```\n\n而在 Server 端，由于 Client 已经设置了截止时间。Server 势必要去检测它\n\n否则如果 Client 已经结束掉了，Server 还傻傻的在那执行，这对资源是一种极大的浪费\n\n因此在这里需要用 `ctx.Err() == context.Canceled` 进行判断，为了模拟场景我们加了循环和睡眠 🤔\n\n### 验证\n\n重新启动 server.go 和 client.go，得到结果：\n\n```\n$ go run client.go\n2018/10/06 17:45:55 client.Search err: deadline\nexit status 1\n```\n\n## 总结\n\n本章节比较简单，你需要知道以下知识点：\n\n- 怎么设置 Deadlines\n- 为什么要设置 Deadlines\n\n你要清楚地明白到，gRPC Deadlines 是很重要的，否则这小小的功能点就会要了你生产的命 🤫\n\n## 参考\n\n### 本系列示例代码\n\n- [go-grpc-example](https://github.com/EDDYCJY/go-grpc-example)\n\n### 资料\n\n- [gRPC and Deadlines](https://grpc.io/blog/deadlines)\n"
  },
  {
    "path": "content/posts/go/grpc/2018-10-20-zipkin.md",
    "content": "---\n\ntitle:      \"「连载十」分布式链路追踪 gRPC + Opentracing + Zipkin\"\ndate:       2018-10-20 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - grpc\n---\n\n在实际应用中，你做了那么多 Server 端，写了 N 个 RPC 方法。想看看方法的指标，却无处下手？\n\n本文将通过 gRPC + Opentracing + Zipkin 搭建一个**分布式链路追踪系统**来实现查看整个系统的链路、性能等指标。\n\n## Opentracing\n\n### 是什么\n\nOpenTracing 通过提供平台无关、厂商无关的API，使得开发人员能够方便的添加（或更换）追踪系统的实现\n\n不过 OpenTracing 并不是标准。因为 CNCF 不是官方标准机构，但是它的目标是致力为分布式追踪创建更标准的 API 和工具\n\n### 名词解释\n\n#### Trace\n\n一个 trace 代表了一个事务或者流程在（分布式）系统中的执行过程\n\n#### Span\n\n一个 span 代表在分布式系统中完成的单个工作单元。也包含其他 span 的 “引用”，这允许将多个 spans 组合成一个完整的 Trace\n\n每个 span 根据 OpenTracing 规范封装以下内容：\n\n- 操作名称\n- 开始时间和结束时间\n- key:value span Tags\n- key:value span Logs\n- SpanContext\n\n#### Tags\n\nSpan tags（跨度标签）可以理解为用户自定义的 Span 注释。便于查询、过滤和理解跟踪数据\n\n#### Logs\n\nSpan logs（跨度日志）可以记录 Span 内特定时间或事件的日志信息。主要用于捕获特定 Span 的日志信息以及应用程序本身的其他调试或信息输出\n\n#### SpanContext\n\nSpanContext 代表跨越进程边界，传递到子级 Span 的状态。常在追踪示意图中创建上下文时使用\n\n#### Baggage Items\n\nBaggage Items 可以理解为 trace 全局运行中额外传输的数据集合\n\n### 一个案例\n\n![image](https://image.eddycjy.com/c7912244434f56f32be37ac66ad164ab.png)\n\n图中可以看到以下内容：\n\n- 执行时间的上下文\n- 服务间的层次关系\n- 服务间串行或并行调用链\n\n结合以上信息，在实际场景中我们可以通过整个系统的调用链的上下文、性能等指标信息，一下子就能够发现系统的痛点在哪儿 \n\n## Zipkin\n\n![image](https://image.eddycjy.com/f82f883ce74801abfece12c775f45c6c.png)\n\n### 是什么\n\nZipkin 是分布式追踪系统。它的作用是收集解决微服务架构中的延迟问题所需的时序数据。它管理这些数据的收集和查找\n\nZipkin 的设计基于 [Google Dapper](http://research.google.com/pubs/pub36356.html) 论文。\n\n### 运行\n\n```\ndocker run -d -p 9411:9411 openzipkin/zipkin\n```\n\n其他方法安装参见：https://github.com/openzipkin/zipkin\n\n### 验证\n\n访问 http://127.0.0.1:9411/zipkin/ 检查 Zipkin 是否运行正常\n\n![image](https://image.eddycjy.com/f22ea6012f6ce4adea9f29d36f1017c7.jpg)\n\n## gRPC + Opentracing + Zipkin\n\n在前面的小节中，我们做了以下准备工作：\n\n- 了解 Opentracing 是什么\n- 搭建 Zipkin 提供分布式追踪系统的功能\n\n接下来实现 gRPC 通过 Opentracing 标准 API 对接 Zipkin，再通过 Zipkin 去查看数据\n\n### 目录结构\n\n新建 simple_zipkin_client、simple_zipkin_server 目录，目录结构如下：\n\n```\ngo-grpc-example\n├── LICENSE\n├── README.md\n├── client\n│   ├── ...\n│   ├── simple_zipkin_client\n├── conf\n├── pkg\n├── proto\n├── server\n│   ├── ...\n│   ├── simple_zipkin_server\n└── vendor\n```\n\n### 安装\n\n```\n$ go get -u github.com/openzipkin/zipkin-go-opentracing\n$ go get -u github.com/grpc-ecosystem/grpc-opentracing/go/otgrpc\n```\n\n### gRPC\n\n#### Server\n\n```\npackage main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"net\"\n\n\t\"github.com/grpc-ecosystem/go-grpc-middleware\"\n\t\"github.com/grpc-ecosystem/grpc-opentracing/go/otgrpc\"\n\tzipkin \"github.com/openzipkin/zipkin-go-opentracing\"\n\t\"google.golang.org/grpc\"\n\n\t\"github.com/EDDYCJY/go-grpc-example/pkg/gtls\"\n\tpb \"github.com/EDDYCJY/go-grpc-example/proto\"\n)\n\ntype SearchService struct{}\n\nfunc (s *SearchService) Search(ctx context.Context, r *pb.SearchRequest) (*pb.SearchResponse, error) {\n\treturn &pb.SearchResponse{Response: r.GetRequest() + \" Server\"}, nil\n}\n\nconst (\n\tPORT = \"9005\"\n\n\tSERVICE_NAME              = \"simple_zipkin_server\"\n\tZIPKIN_HTTP_ENDPOINT      = \"http://127.0.0.1:9411/api/v1/spans\"\n\tZIPKIN_RECORDER_HOST_PORT = \"127.0.0.1:9000\"\n)\n\nfunc main() {\n\tcollector, err := zipkin.NewHTTPCollector(ZIPKIN_HTTP_ENDPOINT)\n\tif err != nil {\n\t\tlog.Fatalf(\"zipkin.NewHTTPCollector err: %v\", err)\n\t}\n\n\trecorder := zipkin.NewRecorder(collector, true, ZIPKIN_RECORDER_HOST_PORT, SERVICE_NAME)\n\n\ttracer, err := zipkin.NewTracer(\n\t\trecorder, zipkin.ClientServerSameSpan(false),\n\t)\n\tif err != nil {\n\t\tlog.Fatalf(\"zipkin.NewTracer err: %v\", err)\n\t}\n\n\ttlsServer := gtls.Server{\n\t\tCaFile:   \"../../conf/ca.pem\",\n\t\tCertFile: \"../../conf/server/server.pem\",\n\t\tKeyFile:  \"../../conf/server/server.key\",\n\t}\n\tc, err := tlsServer.GetCredentialsByCA()\n\tif err != nil {\n\t\tlog.Fatalf(\"GetTLSCredentialsByCA err: %v\", err)\n\t}\n\n\topts := []grpc.ServerOption{\n\t\tgrpc.Creds(c),\n\t\tgrpc_middleware.WithUnaryServerChain(\n\t\t\totgrpc.OpenTracingServerInterceptor(tracer, otgrpc.LogPayloads()),\n\t\t),\n\t}\n    ...\n}\n```\n\n- zipkin.NewHTTPCollector：创建一个 Zipkin HTTP 后端收集器  \n- zipkin.NewRecorder：创建一个基于 Zipkin 收集器的记录器\n- zipkin.NewTracer：创建一个 OpenTracing 跟踪器（兼容 Zipkin Tracer）\n- otgrpc.OpenTracingClientInterceptor：返回 grpc.UnaryServerInterceptor，不同点在于该拦截器会在 gRPC Metadata 中查找 OpenTracing SpanContext。如果找到则为该服务的 Span Context 的子节点 \n- otgrpc.LogPayloads：设置并返回 Option。作用是让 OpenTracing 在双向方向上记录应用程序的有效载荷（payload）\n\n总的来讲，就是初始化 Zipkin，其又包含收集器、记录器、跟踪器。再利用拦截器在 Server 端实现 SpanContext、Payload 的双向读取和管理\n\n#### Client\n\n```\nfunc main() {\n\t// the same as zipkin server\n\t// ...\n\tconn, err := grpc.Dial(\":\"+PORT, grpc.WithTransportCredentials(c),\n\t\tgrpc.WithUnaryInterceptor(\n\t\t\totgrpc.OpenTracingClientInterceptor(tracer, otgrpc.LogPayloads()),\n\t\t))\n\t...\n}\n```\n\n- otgrpc.OpenTracingClientInterceptor：返回 grpc.UnaryClientInterceptor。该拦截器的核心功能在于：\n\n（1）OpenTracing SpanContext 注入 gRPC Metadata \n\n（2）查看 context.Context 中的上下文关系，若存在父级 Span 则创建一个 ChildOf 引用，得到一个子 Span\n\n其他方面，与 Server 端是一致的，先初始化 Zipkin，再增加 Client 端特需的拦截器。就可以完成基础工作啦\n\n### 验证\n\n启动 Server.go，执行 Client.go。查看 http://127.0.0.1:9411/zipkin/ 的示意图：\n\n![image](https://image.eddycjy.com/35c586cc15b28496d5c227e03cde7e67.jpg)\n\n![image](https://image.eddycjy.com/8c17c36d87764237e75b4d7c4739fdf4.jpg)\n\n## 复杂点\n\n![image](https://image.eddycjy.com/d33c339e872ceab76c906e2da1a450c3.jpg)\n\n![image](https://image.eddycjy.com/dc3fc3ec49276d3b56c0c2d22e6a5ad4.jpg)\n\n来，自己实践一下\n\n## 总结\n\n在多服务下的架构下，串行、并行、服务套服务是一个非常常见的情况，用常规的方案往往很难发现问题在哪里（成本太大）。而这种情况就是**分布式追踪系统**大展拳脚的机会了\n\n希望你通过本章节的介绍和学习，能够了解其概念和搭建且应用一个追踪系统。\n\n## 参考\n\n### 本系列示例代码\n\n- [go-grpc-example](https://github.com/EDDYCJY/go-grpc-example)\n\n### 资料\n\n- [opentracing](https://opentracing.io/)\n- [zipkin](https://zipkin.io)\n\n"
  },
  {
    "path": "content/posts/go/grpc-gateway/2018-02-23-install.md",
    "content": "---\n\ntitle:      \"「连载一」gRPC介绍与环境安装\"\ndate:       2018-02-23 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - grpc-gateway\n---\n\n假定我们有一个项目需求，希望用`Rpc`作为内部`API`的通讯，同时也想对外提供`Restful Api`，写两套又太繁琐不符合\n\n于是我们想到了`Grpc`以及`Grpc Gateway`，这就是我们所需要的\n\n![image](https://camo.githubusercontent.com/e75a8b46b078a3c1df0ed9966a16c24add9ccb83/68747470733a2f2f646f63732e676f6f676c652e636f6d2f64726177696e67732f642f3132687034435071724e5046686174744c5f63496f4a707446766c41716d35774c513067677149356d6b43672f7075623f773d37343926683d333730)\n\n## 准备环节\n在正式开始我们的`Grpc`+`Grpc Gateway`实践前，我们需要先配置好我们的开发环境\n- Grpc\n- Protoc Plugin\n- Protocol Buffers\n- Grpc-gateway\n\n## Grpc\n\n### 是什么\nGoogle对`Grpc`的定义：\n> A high performance, open-source universal RPC framework\n\n也就是`Grpc`是一个高性能、开源的通用RPC框架，具有以下特性：\n- 强大的`IDL`，使用`Protocol Buffers`作为数据交换的格式，支持`v2`、`v3`（推荐`v3`）\n- 跨语言、跨平台，也就是`Grpc`支持多种平台和语言\n- **支持HTTP2**，双向传输、多路复用、认证等\n\n\n### 安装\n1、官方推荐（需科学上网）\n```\ngo get -u google.golang.org/grpc\n```\n2、通过`github.com`\n\n\n进入到第一个$GOPATH目录（因为`go get` 会默认安装在第一个下）下，新建`google.golang.org`目录，拉取`golang`在`github`上的镜像库：\n```\ncd /usr/local/go/path/src   \n\nmkdir google.golang.org\n\ncd google.golang.org/\n\ngit clone https://github.com/grpc/grpc-go\n\nmv grpc-go/ grpc/\n```\n\n目录结构：\n```\ngoogle.golang.org/\n└── grpc\n    ...\n```\n\n而在`grpc`下有许多常用的包，例如：\n- [metadata](https://gowalker.org/google.golang.org/grpc/metadata)：定义了`grpc`所支持的元数据结构，包中方法可以对`MD`进行获取和处理\n- [credentials](https://gowalker.org/google.golang.org/grpc/credentials)：实现了`grpc`所支持的各种认证凭据，封装了客户端对服务端进行身份验证所需要的所有状态，并做出各种断言\n- [codes](https://gowalker.org/google.golang.org/grpc/codes)：定义了`grpc`使用的标准错误码，可通用\n\n## Protoc Plugin\n\n### 是什么\n编译器插件\n\n### 安装\n```\ngo get -u github.com/golang/protobuf/protoc-gen-go\n```\n将`Protoc Plugin`的可执行文件从$GOPATH中移动到$GOBIN下\n```\nmv /usr/local/go/path/bin/protoc-gen-go /usr/local/go/bin/\n```\n\n## Protocol Buffers v3\n### 是什么\n>Protocol buffers are a flexible, efficient, automated mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages. You can even update your data structure without breaking deployed programs that are compiled against the \"old\" format.\n\n`Protocol Buffers`是`Google`推出的一种数据描述语言，支持多语言、多平台，它是一种二进制的格式，总得来说就是更小、更快、更简单、更灵活，目前分别有`v2`、`v3`的版本，我们推荐使用`v3`\n\n- [proto2 文档地址](https://developers.google.com/protocol-buffers/docs/proto)\n- [proto3 文档地址](https://developers.google.com/protocol-buffers/docs/proto3)\n\n建议可以阅读下官方文档的介绍，本系列会在使用时简单介绍所涉及的内容\n\n### 安装\n\n```\nwget https://github.com/google/protobuf/releases/download/v3.5.1/protobuf-all-3.5.1.zip\nunzip protobuf-all-3.5.1.zip\ncd protobuf-3.5.1/\n./configure\nmake\nmake install\n```\n\n检查是否安装成功\n```\nprotoc --version\n```\n\n如果出现报错\n```\nprotoc: error while loading shared libraries: libprotobuf.so.15: cannot open shared object file: No such file or directory\n```\n则执行`ldconfig`后，再次运行即可成功\n\n#### 为什么要执行`ldconfig`\n我们通过控制台输出的信息可以知道，`Protocol Buffers Libraries`的默认安装路径在`/usr/local/lib`\n\n```\nLibraries have been installed in:\n   /usr/local/lib\n\nIf you ever happen to want to link against installed libraries\nin a given directory, LIBDIR, you must either use libtool, and\nspecify the full pathname of the library, or use the `-LLIBDIR'\nflag during linking and do at least one of the following:\n   - add LIBDIR to the `LD_LIBRARY_PATH' environment variable\n     during execution\n   - add LIBDIR to the `LD_RUN_PATH' environment variable\n     during linking\n   - use the `-Wl,-rpath -Wl,LIBDIR' linker flag\n   - have your system administrator add LIBDIR to `/etc/ld.so.conf'\n\nSee any operating system documentation about shared libraries for\nmore information, such as the ld(1) and ld.so(8) manual pages.\n```\n\n而我们安装了一个新的动态链接库，`ldconfig`一般在系统启动时运行，所以现在会找不到这个`lib`，因此我们要手动执行`ldconfig`，**让动态链接库为系统所共享，它是一个动态链接库管理命令**，这就是`ldconfig`命令的作用\n\n### protoc使用\n我们按照惯例执行`protoc --help`（查看帮助文档），我们抽出几个常用的命令进行讲解\n\n1、`-IPATH, --proto_path=PATH`：指定`import`搜索的目录，可指定多个，如果不指定则默认当前工作目录\n\n2、`--go_out`：生成`golang`源文件\n\n#### 参数\n若要将额外的参数传递给插件，可使用从输出目录中分离出来的逗号分隔的参数列表:\n```\nprotoc --go_out=plugins=grpc,import_path=mypackage:. *.proto\n```\n\n- `import_prefix=xxx`：将指定前缀添加到所有`import`路径的开头\n- `import_path=foo/bar`：如果文件没有声明`go_package`，则用作包。如果它包含斜杠，那么最右边的斜杠将被忽略。\n- `plugins=plugin1+plugin2`：指定要加载的子插件列表（我们所下载的repo中唯一的插件是grpc）\n- `Mfoo/bar.proto=quux/shme`： `M`参数，指定`.proto`文件编译后的包名（`foo/bar.proto`编译后为包名为`quux/shme`）\n\n#### Grpc支持\n如果`proto`文件指定了`RPC`服务，`protoc-gen-go`可以生成与`grpc`相兼容的代码，我们仅需要将`plugins=grpc`参数传递给`--go_out`，就可以达到这个目的\n```\nprotoc --go_out=plugins=grpc:. *.proto\n```\n\n## Grpc-gateway\n\n### 是什么\n> grpc-gateway is a plugin of protoc. It reads gRPC service definition, and generates a reverse-proxy server which translates a RESTful JSON API into gRPC. This server is generated according to custom options in your gRPC definition.\n\n[grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway)是protoc的一个插件。它读取gRPC服务定义，并生成一个反向代理服务器，将RESTful JSON API转换为gRPC。此服务器是根据gRPC定义中的自定义选项生成的。\n\n### 安装\n```\ngo get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway\n```\n\n如果出现以下报错，我们分析错误提示可得知是连接超时（大概是被墙了）\n```\npackage google.golang.org/genproto/googleapis/api/annotations: unrecognized import path \"google.golang.org/genproto/googleapis/api/annotations\" (https fetch: Get https://google.golang.org/genproto/googleapis/api/annotations?go-get=1: dial tcp 216.239.37.1:443: getsockopt: connection timed out)\n```\n\n有两种解决方法，\n\n1、科学上网\n\n2、通过`github.com`\n\n进入到第一个$GOTPATH目录的`google.golang.org`目录下，拉取`genproto`在`github`上的`go-genproto`镜像库：\n```\ncd /usr/local/go/path/src/google.golang.org\n\ngit clone https://github.com/google/go-genproto.git\n\nmv go-genproto/ genproto/\n```\n\n\n在安装完毕后，我们将`grpc-gateway`的可执行文件从$GOPATH中移动到$GOBIN\n```\nmv /usr/local/go/path/bin/protoc-gen-grpc-gateway /usr/local/go/bin/\n```\n\n到这里我们这节就基本完成了，建议多反复看几遍加深对各个组件的理解！\n\n## 参考\n### 示例代码\n- [grpc-hello-world](https://github.com/EDDYCJY/grpc-hello-world)\n\n"
  },
  {
    "path": "content/posts/go/grpc-gateway/2018-02-27-hello-world.md",
    "content": "---\n\ntitle:      \"「连载二」Hello World\"\ndate:       2018-02-27 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - grpc-gateway\n---\n\n这节将开始编写一个复杂的Hello World，涉及到许多的知识，建议大家认真思考其中的概念\n\n## 需求\n由于本实践偏向`Grpc`+`Grpc Gateway`的方面，我们的需求是**同一个服务端支持`Rpc`和`Restful Api`**，那么就意味着`http2`、`TLS`等等的应用，功能方面就是一个服务端能够接受来自`grpc`和`Restful Api`的请求并响应\n\n## 一、初始化目录\n\n我们先在$GOPATH中新建`grpc-hello-world`文件夹，我们项目的初始目录目录如下：\n```\ngrpc-hello-world/\n├── certs\n├── client\n├── cmd\n├── pkg\n├── proto\n│   ├── google\n│   │   └── api\n└── server\n```\n- `certs`：证书凭证\n- `client`：客户端\n- `cmd`：命令行\n- `pkg`：第三方公共模块\n- `proto`：`protobuf`的一些相关文件（含`.proto`、`pb.go`、`.pb.gw.go`)，`google/api`中用于存放`annotations.proto`、`http.proto`\n- `server`：服务端\n\n## 二、制作证书\n\n在服务端支持`Rpc`和`Restful Api`，需要用到`TLS`，因此我们要先制作证书\n\n进入`certs`目录，生成`TLS`所需的公钥密钥文件\n\n### 私钥\n```\nopenssl genrsa -out server.key 2048\n\nopenssl ecparam -genkey -name secp384r1 -out server.key\n```\n- `openssl genrsa`：生成`RSA`私钥，命令的最后一个参数，将指定生成密钥的位数，如果没有指定，默认512\n- `openssl ecparam`：生成`ECC`私钥，命令为椭圆曲线密钥参数生成及操作，本文中`ECC`曲线选择的是`secp384r1`\n\n### 自签名公钥\n```\nopenssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650\n```\n- `openssl req`：生成自签名证书，`-new`指生成证书请求、`-sha256`指使用`sha256`加密、`-key`指定私钥文件、`-x509`指输出证书、`-days 3650`为有效期，此后则输入证书拥有者信息\n\n### 填写信息\n```\nCountry Name (2 letter code) [XX]:\nState or Province Name (full name) []:\nLocality Name (eg, city) [Default City]:\nOrganization Name (eg, company) [Default Company Ltd]:\nOrganizational Unit Name (eg, section) []:\nCommon Name (eg, your name or your server's hostname) []:grpc server name\nEmail Address []:\n```\n## 三、`proto`\n### 编写\n\n1、 `google.api`\n\n我们看到`proto`目录中有`google/api`目录，它用到了`google`官方提供的两个`api`描述文件，主要是针对`grpc-gateway`的`http`转换提供支持，定义了`Protocol Buffer`所扩展的`HTTP Option`\n\n`annotations.proto`文件：\n```\n// Copyright (c) 2015, Google Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nsyntax = \"proto3\";\n\npackage google.api;\n\nimport \"google/api/http.proto\";\nimport \"google/protobuf/descriptor.proto\";\n\noption java_multiple_files = true;\noption java_outer_classname = \"AnnotationsProto\";\noption java_package = \"com.google.api\";\n\nextend google.protobuf.MethodOptions {\n  // See `HttpRule`.\n  HttpRule http = 72295728;\n}\n\n```\n\n`http.proto`文件：\n```\n// Copyright 2016 Google Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n//     http://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n\nsyntax = \"proto3\";\n\npackage google.api;\n\noption cc_enable_arenas = true;\noption java_multiple_files = true;\noption java_outer_classname = \"HttpProto\";\noption java_package = \"com.google.api\";\n\n\n// Defines the HTTP configuration for a service. It contains a list of\n// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method\n// to one or more HTTP REST API methods.\nmessage Http {\n  // A list of HTTP rules for configuring the HTTP REST API methods.\n  repeated HttpRule rules = 1;\n}\n\n// Use CustomHttpPattern to specify any HTTP method that is not included in the\n// `pattern` field, such as HEAD, or \"*\" to leave the HTTP method unspecified for\n// a given URL path rule. The wild-card rule is useful for services that provide\n// content to Web (HTML) clients.\nmessage HttpRule {\n  // Selects methods to which this rule applies.\n  //\n  // Refer to [selector][google.api.DocumentationRule.selector] for syntax details.\n  string selector = 1;\n\n  // Determines the URL pattern is matched by this rules. This pattern can be\n  // used with any of the {get|put|post|delete|patch} methods. A custom method\n  // can be defined using the 'custom' field.\n  oneof pattern {\n    // Used for listing and getting information about resources.\n    string get = 2;\n\n    // Used for updating a resource.\n    string put = 3;\n\n    // Used for creating a resource.\n    string post = 4;\n\n    // Used for deleting a resource.\n    string delete = 5;\n\n    // Used for updating a resource.\n    string patch = 6;\n\n    // Custom pattern is used for defining custom verbs.\n    CustomHttpPattern custom = 8;\n  }\n\n  // The name of the request field whose value is mapped to the HTTP body, or\n  // `*` for mapping all fields not captured by the path pattern to the HTTP\n  // body. NOTE: the referred field must not be a repeated field.\n  string body = 7;\n\n  // Additional HTTP bindings for the selector. Nested bindings must\n  // not contain an `additional_bindings` field themselves (that is,\n  // the nesting may only be one level deep).\n  repeated HttpRule additional_bindings = 11;\n}\n\n// A custom pattern is used for defining custom HTTP verb.\nmessage CustomHttpPattern {\n  // The name of this custom HTTP verb.\n  string kind = 1;\n\n  // The path matched by this custom verb.\n  string path = 2;\n}\n\n```\n\n2. `hello.proto`\n\n这一小节将编写`Demo`的`.proto`文件，我们在`proto`目录下新建`hello.proto`文件，写入文件内容：\n```\nsyntax = \"proto3\";\n\npackage proto;\n\nimport \"google/api/annotations.proto\";\n\nservice HelloWorld {\n    rpc SayHelloWorld(HelloWorldRequest) returns (HelloWorldResponse) {\n        option (google.api.http) = {\n            post: \"/hello_world\"\n            body: \"*\"\n        };\n    }\n}\n\nmessage HelloWorldRequest {\n    string referer = 1;\n}\n\nmessage HelloWorldResponse {\n    string message = 1;\n}\n```\n\n在`hello.proto`文件中，引用了`google/api/annotations.proto`，达到支持`HTTP Option`的效果\n\n- 定义了一个`service`RPC服务`HelloWorld`，在其内部定义了一个`HTTP Option`的`POST`方法，`HTTP`响应路径为`/hello_world`\n- 定义`message`类型`HelloWorldRequest`、`HelloWorldResponse`，用于响应请求和返回结果\n\n### 编译\n\n在编写完`.proto`文件后，我们需要对其进行编译，就能够在`server`中使用\n\n进入`proto`目录，执行以下命令\n\n```\n# 编译google.api\nprotoc -I . --go_out=plugins=grpc,Mgoogle/protobuf/descriptor.proto=github.com/golang/protobuf/protoc-gen-go/descriptor:. google/api/*.proto\n\n#编译hello_http.proto为hello_http.pb.proto\nprotoc -I . --go_out=plugins=grpc,Mgoogle/api/annotations.proto=grpc-hello-world/proto/google/api:. ./hello.proto\n\n#编译hello_http.proto为hello_http.pb.gw.proto\nprotoc --grpc-gateway_out=logtostderr=true:. ./hello.proto\n```\n\n执行完毕后将生成`hello.pb.go`和`hello.gw.pb.go`，分别针对`grpc`和`grpc-gateway`的功能支持\n\n\n## 四、命令行模块 `cmd`\n### 介绍\n这一小节我们编写命令行模块，为什么要独立出来呢，是为了将`cmd`和`server`两者解耦，避免混淆在一起。\n\n我们采用 [Cobra](https://github.com/spf13/cobra) 来完成这项功能，`Cobra`既是创建强大的现代CLI应用程序的库，也是生成应用程序和命令文件的程序。提供了以下功能：\n\n- 简易的子命令行模式\n- 完全兼容posix的命令行模式(包括短和长版本)\n- 嵌套的子命令\n- 全局、本地和级联`flags`\n- 使用`Cobra`很容易的生成应用程序和命令，使用`cobra create appname`和`cobra add cmdname`\n- 智能提示\n- 自动生成commands和flags的帮助信息\n- 自动生成详细的help信息`-h`，`--help`等等\n- 自动生成的bash自动完成功能\n- 为应用程序自动生成手册\n- 命令别名\n- 定义您自己的帮助、用法等的灵活性。\n- 可选与[viper](https://github.com/spf13/viper)紧密集成的apps\n\n### 编写`server`\n\n在编写`cmd`时需要先用`server`进行测试关联，因此这一步我们先写`server.go`用于测试\n\n在`server`模块下 新建`server.go`文件，写入测试内容：\n```\npackage server\n\nimport (\n    \"log\"\n)\n\nvar (\n    ServerPort string\n    CertName string\n    CertPemPath string\n    CertKeyPath string\n)\n\nfunc Serve() (err error){\n    log.Println(ServerPort)\n    \n    log.Println(CertName)\n    \n    log.Println(CertPemPath)\n    \n    log.Println(CertKeyPath)\n    \n    return nil\n}\n\n```\n### 编写`cmd`\n\n在`cmd`模块下 新建`root.go`文件，写入内容：\n```\npackage cmd\n\nimport (\n    \"fmt\"\n    \"os\"\n\n    \"github.com/spf13/cobra\"\n)\n\nvar rootCmd = &cobra.Command{\n    Use:   \"grpc\",\n    Short: \"Run the gRPC hello-world server\",\n}\n\nfunc Execute() {\n    if err := rootCmd.Execute(); err != nil {\n        fmt.Println(err)\n        os.Exit(-1)\n    }\n}\n```\n新建`server.go`文件，写入内容：\n```\npackage cmd\n\nimport (\n\t\"log\"\n\n\t\"github.com/spf13/cobra\"\n\t\n\t\"grpc-hello-world/server\"\n)\n\nvar serverCmd = &cobra.Command{\n\tUse:   \"server\",\n\tShort: \"Run the gRPC hello-world server\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tdefer func() {\n\t\t\tif err := recover(); err != nil {\n\t\t\t\tlog.Println(\"Recover error : %v\", err)\n\t\t\t}\n\t\t}()\n\t\t\n\t\tserver.Serve()\n\t},\n}\n\nfunc init() {\n\tserverCmd.Flags().StringVarP(&server.ServerPort, \"port\", \"p\", \"50052\", \"server port\")\n\tserverCmd.Flags().StringVarP(&server.CertPemPath, \"cert-pem\", \"\", \"./certs/server.pem\", \"cert pem path\")\n\tserverCmd.Flags().StringVarP(&server.CertKeyPath, \"cert-key\", \"\", \"./certs/server.key\", \"cert key path\")\n\tserverCmd.Flags().StringVarP(&server.CertName, \"cert-name\", \"\", \"grpc server name\", \"server's hostname\")\n\trootCmd.AddCommand(serverCmd)\n}\n```\n\n我们在`grpc-hello-world/`目录下，新建文件`main.go`，写入内容：\n```\npackage main\n\nimport (\n\t\"grpc-hello-world/cmd\"\n)\n\nfunc main() {\n\tcmd.Execute()\n}\n```\n\n### 讲解\n\n要使用`Cobra`，按照`Cobra`标准要创建`main.go`和一个`rootCmd`文件，另外我们有子命令`server`\n\n1、`rootCmd`：\n`rootCmd`表示在没有任何子命令的情况下的基本命令\n\n2、`&cobra.Command`：\n- `Use`：`Command`的用法，`Use`是一个行用法消息\n- `Short`：`Short`是`help`命令输出中显示的简短描述\n- `Run`：运行:典型的实际工作功能。大多数命令只会实现这一点；另外还有`PreRun`、`PreRunE`、`PostRun`、`PostRunE`等等不同时期的运行命令，但比较少用，具体使用时再查看亦可\n\n3、`rootCmd.AddCommand`：`AddCommand`向这父命令（`rootCmd`）添加一个或多个命令\n\n4、`serverCmd.Flags().StringVarP()`：\n\n一般来说，我们需要在`init()`函数中定义`flags`和处理配置，以`serverCmd.Flags().StringVarP(&server.ServerPort, \"port\", \"p\", \"50052\", \"server port\")`为例，我们定义了一个`flag`，值存储在`&server.ServerPort`中，长命令为`--port`，短命令为`-p`，，默认值为`50052`，命令的描述为`server port`。这一种调用方式成为`Local Flags`\n\n我们延伸一下，如果觉得每一个子命令都要设一遍觉得很麻烦，我们可以采用`Persistent Flags`：\n\n`rootCmd.PersistentFlags().BoolVarP(&Verbose, \"verbose\", \"v\", false, \"verbose output\")`\n\n作用：\n\n`flag`是可以持久的，这意味着该`flag`将被分配给它所分配的命令以及该命令下的每个命令。对于全局标记，将标记作为根上的持久标志。\n\n另外还有`Local Flag on Parent Commands`、`Bind Flags with Config`、`Required flags`等等，使用到再 [传送](https://github.com/spf13/cobra#local-flag-on-parent-commands) 了解即可\n\n\n### 测试\n回到`grpc-hello-world/`目录下执行`go run main.go server`，查看输出是否为（此时应为默认值）：\n```\n2018/02/25 23:23:21 50052\n2018/02/25 23:23:21 dev\n2018/02/25 23:23:21 ./certs/server.pem\n2018/02/25 23:23:21 ./certs/server.key\n```\n\n执行`go run main.go server --port=8000 --cert-pem=test-pem --cert-key=test-key --cert-name=test-name`，检验命令行参数是否正确：\n```\n2018/02/25 23:24:56 8000\n2018/02/25 23:24:56 test-name\n2018/02/25 23:24:56 test-pem\n2018/02/25 23:24:56 test-key\n```\n\n若都无误，那么恭喜你`cmd`模块的编写正确了，下一部分开始我们的重点章节！\n\n## 五、服务端模块 `server`\n\n\n### 编写`hello.go`\n在`server`目录下新建文件`hello.go`，写入文件内容：\n```\npackage server\n\nimport (\n\t\"golang.org/x/net/context\"\n\n\tpb \"grpc-hello-world/proto\"\n)\n\ntype helloService struct{}\n\nfunc NewHelloService() *helloService {\n\treturn &helloService{}\n}\n\nfunc (h helloService) SayHelloWorld(ctx context.Context, r *pb.HelloWorldRequest) (*pb.HelloWorldResponse, error) {\n\treturn &pb.HelloWorldResponse{\n\t\tMessage : \"test\",\n\t}, nil\n}\n```\n\n我们创建了`helloService`及其方法`SayHelloWorld`，对应`.proto`的`rpc SayHelloWorld`，这个方法需要有2个参数：`ctx context.Context`用于接受上下文参数、`r *pb.HelloWorldRequest`用于接受`protobuf`的`Request`参数（对应`.proto`的`message HelloWorldRequest`）\n\n\n### *编写`server.go`\n这一小章节，我们编写最为重要的服务端程序部分，涉及到大量的`grpc`、`grpc-gateway`及一些网络知识的应用\n\n1、在`pkg`下新建`util`目录，新建`grpc.go`文件，写入内容：\n```\npackage util\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n\n\t\"google.golang.org/grpc\"\n)\n\nfunc GrpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {\n    if otherHandler == nil {\n        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n            grpcServer.ServeHTTP(w, r)\n        })\n    }\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        if r.ProtoMajor == 2 && strings.Contains(r.Header.Get(\"Content-Type\"), \"application/grpc\") {\n            grpcServer.ServeHTTP(w, r)\n        } else {\n            otherHandler.ServeHTTP(w, r)\n        }\n    })\n}\n```\n\n`GrpcHandlerFunc`函数是用于判断请求是来源于`Rpc`客户端还是`Restful Api`的请求，根据不同的请求注册不同的`ServeHTTP`服务；`r.ProtoMajor == 2`也代表着请求必须基于`HTTP/2`\n\n2、在`pkg`下的`util`目录下，新建`tls.go`文件，写入内容：\n```\npackage util\n\nimport (\n\t\"crypto/tls\"\n    \"io/ioutil\"\n    \"log\"\n\n    \"golang.org/x/net/http2\"\n)\n\nfunc GetTLSConfig(certPemPath, certKeyPath string) *tls.Config {\n    var certKeyPair *tls.Certificate\n    cert, _ := ioutil.ReadFile(certPemPath)\n    key, _ := ioutil.ReadFile(certKeyPath)\n    \n    pair, err := tls.X509KeyPair(cert, key)\n    if err != nil {\n        log.Println(\"TLS KeyPair err: %v\\n\", err)\n    }\n    \n    certKeyPair = &pair\n\n    return &tls.Config{\n        Certificates: []tls.Certificate{*certKeyPair},\n        NextProtos:   []string{http2.NextProtoTLS},\n    }\n}\n```\n\n`GetTLSConfig`函数是用于获取`TLS`配置，在内部，我们读取了`server.key`和`server.pem`这类证书凭证文件\n- `tls.X509KeyPair`：从一对`PEM`编码的数据中解析公钥/私钥对。成功则返回公钥/私钥对\n- `http2.NextProtoTLS`：`NextProtoTLS`是谈判期间的`NPN/ALPN`协议，用于**HTTP/2的TLS设置**\n- `tls.Certificate`：返回一个或多个证书，实质我们解析`PEM`调用的`X509KeyPair`的函数声明就是`func X509KeyPair(certPEMBlock, keyPEMBlock []byte) (Certificate, error)`，返回值就是`Certificate`\n\n总的来说该函数是用于处理从证书凭证文件（PEM），最终获取`tls.Config`作为`HTTP2`的使用参数\n\n3、修改`server`目录下的`server.go`文件，该文件是我们服务里的核心文件，写入内容：\n```\npackage server\n\nimport (\n    \"crypto/tls\"\n    \"net\"\n    \"net/http\"\n    \"log\"\n\n    \"golang.org/x/net/context\"\n    \"google.golang.org/grpc\"\n    \"google.golang.org/grpc/credentials\"\n    \"github.com/grpc-ecosystem/grpc-gateway/runtime\"\n    \n    pb \"grpc-hello-world/proto\"\n    \"grpc-hello-world/pkg/util\"\n)\n\nvar (\n    ServerPort string\n    CertName string\n    CertPemPath string\n    CertKeyPath string\n    EndPoint string\n)\n\nfunc Serve() (err error){\n    EndPoint = \":\" + ServerPort\n    conn, err := net.Listen(\"tcp\", EndPoint)\n    if err != nil {\n        log.Printf(\"TCP Listen err:%v\\n\", err)\n    }\n\n    tlsConfig := util.GetTLSConfig(CertPemPath, CertKeyPath)\n    srv := createInternalServer(conn, tlsConfig)\n\n    log.Printf(\"gRPC and https listen on: %s\\n\", ServerPort)\n\n    if err = srv.Serve(tls.NewListener(conn, tlsConfig)); err != nil {\n        log.Printf(\"ListenAndServe: %v\\n\", err)\n    }\n\n    return err\n}\n\nfunc createInternalServer(conn net.Listener, tlsConfig *tls.Config) (*http.Server) {\n    var opts []grpc.ServerOption\n\n    // grpc server\n    creds, err := credentials.NewServerTLSFromFile(CertPemPath, CertKeyPath)\n    if err != nil {\n        log.Printf(\"Failed to create server TLS credentials %v\", err)\n    }\n\n    opts = append(opts, grpc.Creds(creds))\n    grpcServer := grpc.NewServer(opts...)\n\n    // register grpc pb\n    pb.RegisterHelloWorldServer(grpcServer, NewHelloService())\n\n    // gw server\n    ctx := context.Background()\n    dcreds, err := credentials.NewClientTLSFromFile(CertPemPath, CertName)\n    if err != nil {\n        log.Printf(\"Failed to create client TLS credentials %v\", err)\n    }\n    dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}\n    gwmux := runtime.NewServeMux()\n\n    // register grpc-gateway pb\n    if err := pb.RegisterHelloWorldHandlerFromEndpoint(ctx, gwmux, EndPoint, dopts); err != nil {\n        log.Printf(\"Failed to register gw server: %v\\n\", err)\n    }\n\n    // http服务\n    mux := http.NewServeMux()\n    mux.Handle(\"/\", gwmux)\n\n    return &http.Server{\n        Addr:      EndPoint,\n        Handler:   util.GrpcHandlerFunc(grpcServer, mux),\n        TLSConfig: tlsConfig,\n    }\n}\n```\n\n#### `server`流程剖析\n\n我们将这一大块代码，分成以下几个部分来理解\n\n##### 一、启动监听\n\n`net.Listen(\"tcp\", EndPoint)`用于监听本地的网络地址通知，它的函数原型`func Listen(network, address string) (Listener, error)`\n\n参数：`network`必须传入`tcp`、`tcp4`、`tcp6`、`unix`、`unixpacket`，若`address`为空或为0则会自动选择一个端口号\n返回值：通过查看源码我们可以得知其返回值为`Listener`，结构体原型：\n```\ntype Listener interface {\n    Accept() (Conn, error)\n    Close() error\n    Addr() Addr\n}\n```\n\n通过分析得知，**最后`net.Listen`会返回一个监听器的结构体，返回给接下来的动作，让其执行下一步的操作**，它可以执行三类操作\n- `Accept`：接受等待并将下一个连接返回给`Listener`\n- `Close`：关闭`Listener`\n- `Addr`：返回`Listener`的网络地址\n\n##### 二、获取`TLS`\n\n通过`util.GetTLSConfig`解析得到`tls.Config`，传达给`http.Server`服务的`TLSConfig`配置项使用\n\n##### 三、创建内部服务\n\n`createInternalServer`函数，是整个服务端的核心流转部分\n\n程序采用的是`HTT2`、`HTTPS`也就是需要支持`TLS`，因此在启动`grpc.NewServer`前，我们要将认证的中间件注册进去\n\n\n而前面所获取的`tlsConfig`仅能给`HTTP`使用，因此**第一步**我们要创建`grpc`的`TLS`认证凭证\n\n**1、创建`grpc`的`TLS`认证凭证**\n\n新增引用`google.golang.org/grpc/credentials`的第三方包，它实现了`grpc`库支持的各种凭证，该凭证封装了客户机需要的所有状态，以便与服务器进行身份验证并进行各种断言，例如关于客户机的身份，角色或是否授权进行特定的呼叫\n\n我们调用`NewServerTLSFromFile`来达到我们的目的，它能够从输入证书文件和服务器的密钥文件**构造TLS证书凭证**\n```\nfunc NewServerTLSFromFile(certFile, keyFile string) (TransportCredentials, error) {\n    //LoadX509KeyPair读取并解析来自一对文件的公钥/私钥对\n    cert, err := tls.LoadX509KeyPair(certFile, keyFile)\n    if err != nil {\n        return nil, err\n    }\n    //NewTLS使用tls.Config来构建基于TLS的TransportCredentials\n    return NewTLS(&tls.Config{Certificates: []tls.Certificate{cert}}), nil\n}\n```\n\n**2、设置`grpc ServerOption`**\n\n以`grpc.Creds(creds)`为例，其原型为`func Creds(c credentials.TransportCredentials) ServerOption`，该函数返回`ServerOption`，它为服务器连接设置凭据\n\n\n**3、创建`grpc`服务端**\n\n函数原型：\n```\nfunc NewServer(opt ...ServerOption) *Server\n```\n我们在此处创建了一个没有注册服务的`grpc`服务端，还没有开始接受请求\n```\ngrpcServer := grpc.NewServer(opts...)\n```\n**4、注册`grpc`服务**\n```\npb.RegisterHelloWorldServer(grpcServer, NewHelloService())\n```\n\n**5、创建`grpc-gateway`关联组件**\n```\nctx := context.Background()\ndcreds, err := credentials.NewClientTLSFromFile(CertPemPath, CertName)\nif err != nil {\n    log.Println(\"Failed to create client TLS credentials %v\", err)\n}\ndopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}\n```\n- `context.Background`：返回一个非空的空上下文。它没有被注销，没有值，没有过期时间。它通常由主函数、初始化和测试使用，并作为传入请求的**顶级上下文**\n- `credentials.NewClientTLSFromFile`：从客户机的输入证书文件构造TLS凭证\n- `grpc.WithTransportCredentials`：配置一个连接级别的安全凭据(例：`TLS`、`SSL`)，返回值为`type DialOption`\n- `grpc.DialOption`：`DialOption`选项配置我们如何设置连接（其内部具体由多个的`DialOption`组成，决定其设置连接的内容）\n\n**6、创建`HTTP NewServeMux`及注册`grpc-gateway`逻辑**\n```\ngwmux := runtime.NewServeMux()\n\n// register grpc-gateway pb\nif err := pb.RegisterHelloWorldHandlerFromEndpoint(ctx, gwmux, EndPoint, dopts); err != nil {\n    log.Println(\"Failed to register gw server: %v\\n\", err)\n}\n\n// http服务\nmux := http.NewServeMux()\nmux.Handle(\"/\", gwmux)\n```\n\n- `runtime.NewServeMux`：返回一个新的`ServeMux`，它的内部映射是空的；`ServeMux`是`grpc-gateway`的一个请求多路复用器。它将`http`请求与模式匹配，并调用相应的处理程序\n- `RegisterHelloWorldHandlerFromEndpoint`：如函数名，注册`HelloWorld`服务的`HTTP Handle`到`grpc`端点\n- `http.NewServeMux`：`分配并返回一个新的ServeMux`\n- `mux.Handle`：为给定模式注册处理程序\n\n（带着疑问去看程序）为什么`gwmux`可以放入`mux.Handle`中？\n\n首先我们看看它们的原型是怎么样的\n\n（1）`http.NewServeMux()`\n```\nfunc NewServeMux() *ServeMux {\n        return new(ServeMux) \n}\n```\n```\ntype Handler interface {\n    ServeHTTP(ResponseWriter, *Request)\n}\n```\n（2）`runtime.NewServeMux`？\n```\nfunc NewServeMux(opts ...ServeMuxOption) *ServeMux {\n    serveMux := &ServeMux{\n        handlers:               make(map[string][]handler),\n        forwardResponseOptions: make([]func(context.Context, http.ResponseWriter, proto.Message) error, 0),\n        marshalers:             makeMarshalerMIMERegistry(),\n    }\n    ...\n    return serveMux\n}\n```\n（3）`http.NewServeMux()`的`Handle`方法\n```\nfunc (mux *ServeMux) Handle(pattern string, handler Handler)\n```\n\n通过分析可得知，两者`NewServeMux`都是最终返回`serveMux`，`Handler`中导出的方法仅有`ServeHTTP`，功能是用于响应HTTP请求\n\n我们回到`Handle interface`中，可以得出结论就是任何结构体，只要实现了`ServeHTTP`方法，这个结构就可以称为`Handle`，`ServeMux`会使用该`Handler`调用`ServeHTTP`方法处理请求，这也就是**自定义`Handler`**\n\n而我们这里正是将`grpc-gateway`中注册好的`HTTP Handler`无缝的植入到`net/http`的`Handle`方法中\n\n\n**补充：在`go`中任何结构体只要实现了与接口相同的方法，就等同于实现了接口**\n\n**7、注册具体服务**\n```\nif err := pb.RegisterHelloWorldHandlerFromEndpoint(ctx, gwmux, EndPoint, dopts); err != nil {\n    log.Println(\"Failed to register gw server: %v\\n\", err)\n}\n```\n在这段代码中，我们利用了前几小节的\n\n- 上下文\n- `gateway-grpc`的请求多路复用器\n- 服务网络地址\n- 配置好的安全凭据\n\n注册了`HelloWorld`这一个服务\n\n##### 四、创建`tls.NewListener`\n```\nfunc NewListener(inner net.Listener, config *Config) net.Listener {\n    l := new(listener)\n    l.Listener = inner\n    l.config = config\n    return l\n}\n```\n`NewListener`将会创建一个`Listener`，它接受两个参数，第一个是来自内部`Listener`的监听器，第二个参数是`tls.Config`（必须包含至少一个证书）\n\n##### 五、服务开始接受请求\n在最后我们调用`srv.Serve(tls.NewListener(conn, tlsConfig))`，可以得知它是`http.Server`的方法，并且需要一个`Listener`作为参数，那么`Serve`内部做了些什么事呢？\n```\nfunc (srv *Server) Serve(l net.Listener) error {\n    defer l.Close()\n    ...\n\n    baseCtx := context.Background() // base is always background, per Issue 16220\n    ctx := context.WithValue(baseCtx, ServerContextKey, srv)\n    for {\n        rw, e := l.Accept()\n        ...\n        c := srv.newConn(rw)\n        c.setState(c.rwc, StateNew) // before Serve can return\n        go c.serve(ctx)\n    }\n}\n```\n\n粗略的看，它创建了一个`context.Background()`上下文对象，并调用`Listener`的`Accept`方法开始接受外部请求，在获取到连接数据后使用`newConn`创建连接对象，在最后使用`goroutine`的方式处理连接请求，达到其目的\n\n**补充：对于`HTTP/2`支持，在调用`Serve`之前，应将`srv.TLSConfig`初始化为提供的`Listener`的TLS配置。如果`srv.TLSConfig`非零，并且在`Config.NextProtos`中不包含字符串`h2`，则不启用`HTTP/2`支持**\n\n## 六、验证功能\n\n### 编写测试客户端\n在`grpc-hello-world/`下新建目录`client`，新建`client.go`文件，新增内容：\n```\npackage main\n\nimport (\n\t\"log\"\n\n\t\"golang.org/x/net/context\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials\"\n\n\tpb \"grpc-hello-world/proto\"\n)\n\nfunc main() {\n\tcreds, err := credentials.NewClientTLSFromFile(\"../certs/server.pem\", \"dev\")\n\tif err != nil {\n\t\tlog.Println(\"Failed to create TLS credentials %v\", err)\n\t}\n\tconn, err := grpc.Dial(\":50052\", grpc.WithTransportCredentials(creds))\n\tdefer conn.Close()\n\n\tif err != nil {\n\t\tlog.Println(err)\n\t}\n\n\tc := pb.NewHelloWorldClient(conn)\n\tcontext := context.Background()\n\tbody := &pb.HelloWorldRequest{\n\t\tReferer : \"Grpc\",\n\t}\n\n\tr, err := c.SayHelloWorld(context, body)\n\tif err != nil {\n\t\tlog.Println(err)\n\t}\n\n\tlog.Println(r.Message)\n}\n```\n由于客户端只是展示测试用，就简单的来了，原本它理应归类到`cobra`的管控下，配置管理等等都应可控化\n\n在看这篇文章的你，可以试试将测试客户端归类好\n\n### 启动服务端\n\n回到`grpc-hello-world/`目录下，启动服务端`go run main.go server`，成功则仅返回\n```\n2018/02/26 17:19:36 gRPC and https listen on: 50052\n```\n\n### 执行测试客户端\n\n回到`client`目录下，启动客户端`go run client.go`，成功则返回\n```\n2018/02/26 17:22:57 Grpc\n```\n\n### 执行测试Restful Api\n```\ncurl -X POST -k https://localhost:50052/hello_world -d '{\"referer\": \"restful_api\"}'\n```\n\n成功则返回`{\"message\":\"restful_api\"}`\n\n---\n\n## 最终目录结构\n```\ngrpc-hello-world\n├── certs\n│   ├── server.key\n│   └── server.pem\n├── client\n│   └── client.go\n├── cmd\n│   ├── root.go\n│   └── server.go\n├── main.go\n├── pkg\n│   └── util\n│       ├── grpc.go\n│       └── tls.go\n├── proto\n│   ├── google\n│   │   └── api\n│   │       ├── annotations.pb.go\n│   │       ├── annotations.proto\n│   │       ├── http.pb.go\n│   │       └── http.proto\n│   ├── hello.pb.go\n│   ├── hello.pb.gw.go\n│   └── hello.proto\n└── server\n    ├── hello.go\n    └── server.go\n```\n\n至此本节就结束了，推荐一下`jergoo`的文章，大家有时间可以看看\n\n另外本节涉及了许多组件间的知识，值得大家细细的回味，非常有意义！\n\n## 参考\n### 示例代码\n- [grpc-hello-world](https://github.com/EDDYCJY/grpc-hello-world)\n\n"
  },
  {
    "path": "content/posts/go/grpc-gateway/2018-03-04-swagger.md",
    "content": "---\n\ntitle:      \"「连载三」Swagger了解一下\"\ndate:       2018-03-04 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - grpc-gateway\n---\n\n在[上一节](https://segmentfault.com/a/1190000013408485)，我们完成了一个服务端同时支持`Rpc`和`RESTful Api`后，你以为自己大功告成了，结果突然发现要写`Api`文档和前端同事对接= = 。。。\n\n你寻思有没有什么组件能够自动化生成`Api`文档来解决这个问题，就在这时你发现了`Swagger`，一起了解一下吧！\n\n## 介绍\n### Swagger\n`Swagger`是全球最大的`OpenAPI`规范（OAS）API开发工具框架，支持从设计和文档到测试和部署的整个API生命周期的开发\n\n`Swagger`是目前最受欢迎的`RESTful Api`文档生成工具之一，主要的原因如下\n- 跨平台、跨语言的支持\n- 强大的社区\n- 生态圈 Swagger Tools（[Swagger Editor](https://github.com/swagger-api/swagger-editor)、[Swagger Codegen](https://github.com/swagger-api/swagger-codegen)、[Swagger UI](https://github.com/swagger-api/swagger-ui) ...）\n- 强大的控制台\n\n同时`grpc-gateway`也支持`Swagger`\n\n[image]\n\n### `OpenAPI`规范\n`OpenAPI`规范是`Linux`基金会的一个项目，试图通过定义一种用来描述API格式或API定义的语言，来规范`RESTful`服务开发过程。`OpenAPI`规范帮助我们描述一个API的基本信息，比如：\n- 有关该API的一般性描述\n- 可用路径（/资源）\n- 在每个路径上的可用操作（获取/提交...）\n- 每个操作的输入/输出格式\n\n目前V2.0版本的[OpenAPI规范](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md)（也就是SwaggerV2.0规范）已经发布并开源在github上。该文档写的非常好，结构清晰，方便随时查阅。\n\n注：`OpenAPI`规范的介绍引用自[原文](https://huangwenchao.gitbooks.io/swagger/content/)\n\n## 使用\n\n### 生成`Swagger`的说明文件\n\n**第一**，我们需要检查$GOBIN下是否包含`protoc-gen-swagger`可执行文件\n\n若不存在则需要执行：\n```\ngo get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger\n```\n等待执行完毕后，可在`$GOPATH/bin`下发现该执行文件，将其移动到`$GOBIN`下即可\n\n**第二**，回到`$GOPATH/src/grpc-hello-world/proto`下，执行命令\n```\nprotoc -I/usr/local/include -I. -I$GOPATH/src/grpc-hello-world/proto/google/api --swagger_out=logtostderr=true:. ./hello.proto\n```\n成功后执行`ls`即可看到`hello.swagger.json`文件\n\n\n### 下载`Swagger UI`文件\n`Swagger`提供可视化的`API`管理平台，就是[Swagger UI](https://github.com/swagger-api/swagger-ui)\n\n我们将其源码下载下来，并将其`dist`目录下的所有文件拷贝到我们项目中的`$GOPATH/src/grpc-hello-world/third_party/swagger-ui`去\n\n### 将`Swagger UI`转换为`Go`源代码\n\n在这里我们使用的转换工具是[go-bindata](https://github.com/jteeuwen/go-bindata)\n\n它支持将任何文件转换为可管理的`Go`源代码。用于将二进制数据嵌入到`Go`程序中。并且在将文件数据转换为原始字节片之前，可以选择压缩文件数据\n\n#### 安装\n```\ngo get -u github.com/jteeuwen/go-bindata/...\n```\n完成后，将`$GOPATH/bin`下的`go-bindata`移动到`$GOBIN`下\n\n#### 转换\n在项目下新建`pkg/ui/data/swagger`目录，回到`$GOPATH/src/grpc-hello-world/third_party/swagger-ui`下，执行命令\n```\ngo-bindata --nocompress -pkg swagger -o pkg/ui/data/swagger/datafile.go third_party/swagger-ui/...\n```\n\n#### 检查\n回到`pkg/ui/data/swagger`目录，检查是否存在`datafile.go`文件\n\n### `Swagger UI`文件服务器（对外提供服务）\n在这一步，我们需要使用与其配套的[go-bindata-assetfs](https://github.com/elazarl/go-bindata-assetfs/)\n\n它能够使用`go-bindata`所生成`Swagger UI`的`Go`代码，结合`net/http`对外提供服务\n\n#### 安装\n```\ngo get github.com/elazarl/go-bindata-assetfs/...\n```\n\n#### 编写\n\n通过分析，我们得知生成的文件提供了一个`assetFS`函数，该函数返回一个封装了嵌入文件的`http.Filesystem`，可以用其来提供一个`HTTP`服务\n\n那么我们来编写`Swagger UI`的代码吧，主要是两个部分，一个是`swagger.json`，另外一个是`swagger-ui`的响应\n\n##### serveSwaggerFile\n引用包`strings`、`path`\n```\nfunc serveSwaggerFile(w http.ResponseWriter, r *http.Request) {\n      if ! strings.HasSuffix(r.URL.Path, \"swagger.json\") {\n        log.Printf(\"Not Found: %s\", r.URL.Path)\n        http.NotFound(w, r)\n        return\n    }\n\n    p := strings.TrimPrefix(r.URL.Path, \"/swagger/\")\n    p = path.Join(\"proto\", p)\n\n    log.Printf(\"Serving swagger-file: %s\", p)\n\n    http.ServeFile(w, r, p)\n}\n```\n在函数中，我们利用`r.URL.Path`进行路径后缀判断\n\n主要做了对`swagger.json`的文件访问支持（提供`https://127.0.0.1:50052/swagger/hello.swagger.json`的访问）\n\n##### serveSwaggerUI\n引用包`github.com/elazarl/go-bindata-assetfs`、`grpc-hello-world/pkg/ui/data/swagger`\n```\nfunc serveSwaggerUI(mux *http.ServeMux) {\n    fileServer := http.FileServer(&assetfs.AssetFS{\n        Asset:    swagger.Asset,\n        AssetDir: swagger.AssetDir,\n        Prefix:   \"third_party/swagger-ui\",\n    })\n    prefix := \"/swagger-ui/\"\n    mux.Handle(prefix, http.StripPrefix(prefix, fileServer))\n}\n```\n\n在函数中，我们使用了[go-bindata-assetfs](https://github.com/elazarl/go-bindata-assetfs/)来调度先前生成的`datafile.go`，结合`net/http`来对外提供`swagger-ui`的服务\n\n#### 结合\n在完成功能后，我们发现`path.Join(\"proto\", p)`是写死参数的，这样显然不对，我们应该将其导出成外部参数，那么我们来最终改造一番\n\n首先我们在`server.go`新增包全局变量`SwaggerDir`，修改`cmd/server.go`文件：\n```\npackage cmd\n\nimport (\n\t\"log\"\n\n\t\"github.com/spf13/cobra\"\n\t\n\t\"grpc-hello-world/server\"\n)\n\nvar serverCmd = &cobra.Command{\n\tUse:   \"server\",\n\tShort: \"Run the gRPC hello-world server\",\n\tRun: func(cmd *cobra.Command, args []string) {\n\t\tdefer func() {\n\t\t\tif err := recover(); err != nil {\n\t\t\t\tlog.Println(\"Recover error : %v\", err)\n\t\t\t}\n\t\t}()\n\t\t\n\t\tserver.Run()\n\t},\n}\n\nfunc init() {\n\tserverCmd.Flags().StringVarP(&server.ServerPort, \"port\", \"p\", \"50052\", \"server port\")\n\tserverCmd.Flags().StringVarP(&server.CertPemPath, \"cert-pem\", \"\", \"./conf/certs/server.pem\", \"cert-pem path\")\n\tserverCmd.Flags().StringVarP(&server.CertKeyPath, \"cert-key\", \"\", \"./conf/certs/server.key\", \"cert-key path\")\n\tserverCmd.Flags().StringVarP(&server.CertServerName, \"cert-server-name\", \"\", \"grpc server name\", \"server's hostname\")\n\tserverCmd.Flags().StringVarP(&server.SwaggerDir, \"swagger-dir\", \"\", \"proto\", \"path to the directory which contains swagger definitions\")\n\t\n\trootCmd.AddCommand(serverCmd)\n}\n```\n\n修改`path.Join(\"proto\", p)`为`path.Join(SwaggerDir, p)`，这样的话我们`swagger.json`的文件路径就可以根据外部情况去修改它\n\n最终`server.go`文件内容：\n```\npackage server\n\nimport (\n    \"crypto/tls\"\n    \"net\"\n    \"net/http\"\n    \"log\"\n    \"strings\"\n    \"path\"\n\n    \"golang.org/x/net/context\"\n    \"google.golang.org/grpc\"\n    \"google.golang.org/grpc/credentials\"\n    \"github.com/grpc-ecosystem/grpc-gateway/runtime\"\n    \"github.com/elazarl/go-bindata-assetfs\"\n    \n    pb \"grpc-hello-world/proto\"\n    \"grpc-hello-world/pkg/util\"\n    \"grpc-hello-world/pkg/ui/data/swagger\"\n)\n\nvar (\n    ServerPort string\n    CertServerName string\n    CertPemPath string\n    CertKeyPath string\n    SwaggerDir string\n    EndPoint string\n\n    tlsConfig *tls.Config\n)\n\nfunc Run() (err error) {\n    EndPoint = \":\" + ServerPort\n    tlsConfig = util.GetTLSConfig(CertPemPath, CertKeyPath)\n\n    conn, err := net.Listen(\"tcp\", EndPoint)\n    if err != nil {\n        log.Printf(\"TCP Listen err:%v\\n\", err)\n    }\n\n    srv := newServer(conn)\n\n    log.Printf(\"gRPC and https listen on: %s\\n\", ServerPort)\n\n    if err = srv.Serve(util.NewTLSListener(conn, tlsConfig)); err != nil {\n        log.Printf(\"ListenAndServe: %v\\n\", err)\n    }\n\n    return err\n}\n \nfunc newServer(conn net.Listener) (*http.Server) {\n    grpcServer := newGrpc()\n    gwmux, err := newGateway()\n    if err != nil {\n        panic(err)\n    }\n\n    mux := http.NewServeMux()\n    mux.Handle(\"/\", gwmux)\n    mux.HandleFunc(\"/swagger/\", serveSwaggerFile)\n    serveSwaggerUI(mux)\n\n    return &http.Server{\n        Addr:      EndPoint,\n        Handler:   util.GrpcHandlerFunc(grpcServer, mux),\n        TLSConfig: tlsConfig,\n    }\n}\n\nfunc newGrpc() *grpc.Server {\n    creds, err := credentials.NewServerTLSFromFile(CertPemPath, CertKeyPath)\n    if err != nil {\n        panic(err)\n    }\n\n    opts := []grpc.ServerOption{\n        grpc.Creds(creds),\n    }\n    server := grpc.NewServer(opts...)\n\n    pb.RegisterHelloWorldServer(server, NewHelloService())\n\n    return server\n}\n\nfunc newGateway() (http.Handler, error) {\n    ctx := context.Background()\n    dcreds, err := credentials.NewClientTLSFromFile(CertPemPath, CertServerName)\n    if err != nil {\n        return nil, err\n    }\n    dopts := []grpc.DialOption{grpc.WithTransportCredentials(dcreds)}\n    \n    gwmux := runtime.NewServeMux()\n    if err := pb.RegisterHelloWorldHandlerFromEndpoint(ctx, gwmux, EndPoint, dopts); err != nil {\n        return nil, err\n    }\n\n    return gwmux, nil\n}\n\nfunc serveSwaggerFile(w http.ResponseWriter, r *http.Request) {\n      if ! strings.HasSuffix(r.URL.Path, \"swagger.json\") {\n        log.Printf(\"Not Found: %s\", r.URL.Path)\n        http.NotFound(w, r)\n        return\n    }\n\n    p := strings.TrimPrefix(r.URL.Path, \"/swagger/\")\n    p = path.Join(SwaggerDir, p)\n\n    log.Printf(\"Serving swagger-file: %s\", p)\n\n    http.ServeFile(w, r, p)\n}\n\nfunc serveSwaggerUI(mux *http.ServeMux) {\n    fileServer := http.FileServer(&assetfs.AssetFS{\n        Asset:    swagger.Asset,\n        AssetDir: swagger.AssetDir,\n        Prefix:   \"third_party/swagger-ui\",\n    })\n    prefix := \"/swagger-ui/\"\n    mux.Handle(prefix, http.StripPrefix(prefix, fileServer))\n}\n```\n\n## 测试\n访问路径`https://127.0.0.1:50052/swagger/hello.swagger.json`，查看输出内容是否为`hello.swagger.json`的内容，例如：\n[image]\n\n访问路径`https://127.0.0.1:50052/swagger-ui/`，查看内容\n[image]\n\n## 小结\n\n至此我们这一章节就完毕了，`Swagger`和其生态圈十分的丰富，有兴趣研究的小伙伴可以到其[官网](https://swagger.io/)认真研究\n\n而目前完成的程度也满足了日常工作的需求了，可较自动化的生成`RESTful Api`文档，完成与接口对接\n\n\n## 参考\n### 示例代码\n- [grpc-hello-world](https://github.com/EDDYCJY/grpc-hello-world)\n\n"
  },
  {
    "path": "content/posts/go/grpc-gateway/2019-06-22-grpc-gateway-tls.md",
    "content": "---\n\ntitle:      \"「连载四」gRPC+gRPC Gateway 能不能不用证书？\"\ndate:       2019-06-22 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - grpc-gateway\n---\n\n如果你以前有涉猎过 gRPC+gRPC Gateway 这两个组件，你肯定会遇到这个问题，就是 **“为什么非得开 TLS，才能够实现同端口双流量，能不能不开？”** 又或是 **“我不想用证书就实现这些功能，行不行？”**。我被无数的人问过无数次这些问题，也说服过很多人，但说服归说服，不代表放弃。前年不行，不代表今年不行，在今天我希望分享来龙去脉和具体的实现方式给你。\n\n![image](https://s2.ax1x.com/2020/02/27/3dLBAx.png)\n\n## 过去\n\n### 为什么 h2 不行\n\n因为 `net/http2` 仅支持 \"h2\" 标识，而 \"h2\" 标识 HTTP/2 必须使用传输层安全性（TLS）的协议，此标识符用于 TLS 应用层协议协商字段以及识别 HTTP/2 over TLS。\n\n简单来讲，也就 `net/http2` 必须使用 TLS 来交互。通俗来讲就要用证书，那么理所当然，也就无法支持非 TLS 的情况了。\n\n### 寻找 h2c\n\n那这条路不行，我们再想想别的路？那就是 HTTP/2 规范中的 \"h2c\" 标识了，\"h2c\" 标识允许通过明文 TCP 运行 HTTP/2 的协议，此标识符用于 HTTP/1.1 升级标头字段以及标识 HTTP/2 over TCP。\n\n但是这条路，早在 2015 年就已经有在 [issue](https://github.com/golang/go/issues/13128#issuecomment-153193762) 中进行讨论，当时 @bradfitz 明确表示 “不打算支持 h2c，对仅支持 TLS 的情况非常满意，一年后再问我一次”，原文回复如下：\n\n> We do not plan to support h2c. I don't want to receive bug reports from users who get bitten by transparent proxies messing with h2c. Also, until there's widespread browser support, it's not interesting. I am also not interested in being the chicken or the egg to get browser support going. I'm very happy with the TLS-only situation, and things like https://LetsEncrypt.org/ will make TLS much easier (and automatic) soon.\n\n> Ask me again in one year.\n\n### 琢磨其他方式\n\n#### 使用 cmux\n\n基于多路复用器 [soheilhy/cmux](https://github.com/soheilhy/cmux) 的另类实现 [Stoakes/grpc-gateway-example](https://github.com/Stoakes/grpc-gateway-example)。若对 `cmux` 的实现方式感兴趣，还可以看看 [《Golang: Run multiple services on one port》](https://blog.dgraph.io/post/cmux/)。\n\n#### 使用第三方 h2\n\n- [veqryn/h2c](https://github.com/veqryn/h2c)\n\n这种属于自己实现了 h2c 的逻辑，以此达到效果。\n\n## 现在\n\n经过社区的不断讨论，最后在 2018 年 6 月，代表 \"h2c\" 标志的 `golang.org/x/net/http2/h2c` 标准库正式合并进来，自此我们就可以使用官方标准库（h2c），这个标准库实现了 HTTP/2 的未加密模式，因此我们就可以利用该标准库在同个端口上既提供 HTTP/1.1 又提供 HTTP/2 的功能了。\n\n### 使用标准库 h2c \n\n```\nimport (\n\t...\n\n\t\"golang.org/x/net/http2\"\n\t\"golang.org/x/net/http2/h2c\"\n\t\"google.golang.org/grpc\"\n\n\t\"github.com/grpc-ecosystem/grpc-gateway/runtime\"\n\n\tpb \"github.com/EDDYCJY/go-grpc-example/proto\"\n)\n\n...\n\nfunc grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {\n\treturn h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.ProtoMajor == 2 && strings.Contains(r.Header.Get(\"Content-Type\"), \"application/grpc\") {\n\t\t\tgrpcServer.ServeHTTP(w, r)\n\t\t} else {\n\t\t\totherHandler.ServeHTTP(w, r)\n\t\t}\n\t}), &http2.Server{})\n}\n\nfunc main() {\n\tserver := grpc.NewServer()\n\n\tpb.RegisterSearchServiceServer(server, &SearchService{})\n\n\tmux := http.NewServeMux()\n\tgwmux := runtime.NewServeMux()\n\tdopts := []grpc.DialOption{grpc.WithInsecure()}\n\n\terr := pb.RegisterSearchServiceHandlerFromEndpoint(context.Background(), gwmux, \"localhost:\"+PORT, dopts)\n\t...\n\tmux.Handle(\"/\", gwmux)\n\thttp.ListenAndServe(\":\"+PORT, grpcHandlerFunc(server, mux))\n}\n```\n\n我们可以看到关键之处在于调用了 `h2c.NewHandler` 方法进行了特殊处理，`h2c.NewHandler` 会返回一个 `http.handler`，主要的内部逻辑是拦截了所有 `h2c` 流量，然后根据不同的请求流量类型将其劫持并重定向到相应的 `Hander` 中去处理。\n\n### 验证\n\n#### HTTP/1.1\n\n```\n$ curl -X GET 'http://127.0.0.1:9005/search?request=EDDYCJY'\n{\"response\":\"EDDYCJY\"}\n```\n\n#### HTTP/2(gRPC)\n\n```\n...\nfunc main() {\n\tconn, err := grpc.Dial(\":\"+PORT, grpc.WithInsecure())\n\t...\n\tclient := pb.NewSearchServiceClient(conn)\n\tresp, err := client.Search(context.Background(), &pb.SearchRequest{\n\t\tRequest: \"gRPC\",\n\t})\n}\n```\n输出结果：\n\n```\n$ go run main.go\n2019/06/21 20:04:09 resp: gRPC h2c Server\n```\n\n## 总结\n\n在本文中我介绍了大致的前因后果，且介绍了几种解决方法，我建议你选择官方的 `h2c` 标准库去实现这个功能，也简单。在最后，不管你是否曾经为这个问题烦恼过许久，又或者正在纠结，都希望这篇文章能够帮到你。\n\n## 参考\n\n- https://github.com/golang/go/issues/13128\n- https://github.com/golang/go/issues/14141\n- https://github.com/golang/net/commit/c4299a1a0d8524c11563db160fbf9bddbceadb21\n- https://go-review.googlesource.com/c/net/+/112997/\n"
  },
  {
    "path": "content/posts/go/import-cyc.md",
    "content": "---\ntitle: \"为什么 Go 不支持循环引用？\"\ndate: 2021-12-31T12:55:15+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - 为什么\n---\n\n大家好，我是煎鱼。\n\n近年来开始学习 Go 语言的开发者越来越多了。很多小伙伴在使用时，就会遇到种种不理解的问题。\n\n其中一点就是循环引入的报错：\n\n```shell\npackage command-line-arguments\n\timports github.com/eddycjy/awesome-project/a\n\timports github.com/eddycjy/awesome-project/b\n\timports github.com/eddycjy/awesome-project/a: import cycle not allowed\n```\n\n为什么 Go 不支持循环引用呢，这就很不解了，难道还影响性能了？\n\n\n![图来自网络](https://files.mdnice.com/user/3610/4596a6d1-9286-40b2-8074-cc43f9bbd9f4.png)\n\n今天煎鱼将和大家一起了解背后的原因。\n\n## 案例演示\n\n做一个基本的案例 Demo，便于没接触过的同学建立初步认知。我们的程序分别有 2 个 package。\n\npackage a 的代码如下：\n\n```golang\nimport (\n\t\"github.com/eddycjy/awesome-project/b\"\n)\n\nfunc Hello(s string) {\n\tb.Print(s)\n}\n```\n\npackage b 的代码如下：\n\n```golang\nimport (\n\t\"fmt\"\n\n\t\"github.com/eddycjy/awesome-project/a\"\n)\n\nfunc Hello() {\n\ta.Hello(\"脑子进煎鱼了\")\n}\n\nfunc Print(s string) {\n\tfmt.Println(s)\n}\n```\n\n再在 main.go 的文件中调用 `a.Hello(\"脑子进煎鱼了\")` 方法。\n\n一运行，就会出现如下错误提示：\n\n```shell\npackage command-line-arguments\n\timports github.com/eddycjy/awesome-project/a\n\timports github.com/eddycjy/awesome-project/b\n\timports github.com/eddycjy/awesome-project/a: import cycle not allowed\n```\n\n错误的本质原因是 package a 引用了 package b，而 package b 又引用了 package a，造成了循环引用。\n\n这在 Go 语言中是明令禁止的，在编译时就会中断程序，导致编译失败。\n\n## 原因分析\n\n根据现在 Go 官方的统一意见来看，package 循环导入几乎不可能出现，即使是 Go2。\n\n因为 Go2 可能是很多核心问题的破变的关键节点，有许多人提了类似《[proposal: Go 2: allow import cycle](https://github.com/golang/go/issues/30247)》的提案，希望解决循环引入的问题。\n\nGo 语言之父 Rob Pike 亲自回答了这个问题，原因如下：\n\n- 没有支持循环导入，目的是迫使 Go 程序员更多地考虑程序的依赖关系。\n    - 保持依赖关系图的简洁。\n    - 快速的程序构建。\n- 如果支持循环导入，很容易会造成懒惰、不良的依赖性管理和缓慢的构建。这是设计者不希望看见的。\n    - 混乱的依赖关系。\n    - 缓慢的程序构建\n\n简单拿来就，就是在 Go 工程中出现循环引用，这会对构建性能和依赖关系的解决非常不利。\n\n为此，考虑一开始就保持图的正确 DAG，认为这是一个值得预先简化的领域。导入循环可能很方便，但其实背后的代价可能是灾难性的，所以在 Go 中被明确禁止支持。\n\n## 总结\n\n在程序中，如果我们频繁的出现模块与模块之间的循环引用，这时候我们是不是应该考虑一下，是不是设计的有些问题，要不要考虑调整？\n\n但也并非所有的事都是二极管，Go 源码可能或多或少都有自己循环引用的案例，最重要的是想清楚。\n\n**你对此支持循环引用怎么看**，欢迎在评论区留言交流：）\n"
  },
  {
    "path": "content/posts/go/import-generics.md",
    "content": "---\ntitle: \"长达 12 年，Go 才引入泛型，是政治，还是技术问题？\"\ndate: 2021-12-31T12:55:25+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n前两天 Go1.18 beta1 已经发布，距离正式发布 Go1.18 的生产可用还有 2 个月，也就是泛型即将正式面世。\n\n最近正在收集泛型的一些资料，看到在 2015 年有人在 Hacker News 上的《[Go 1.5 max procs default](https://news.ycombinator.com/item?id=9622417 \"Go 1.5 max procs default\")》吐槽 Go 不支持泛型是 “政治” 原因...\n\n看了还是有些意义的，**与现在的矛盾点基本一致**，为此分享给大家。\n\n## 网友吐槽\n\n网友 @aikah 认为 Go 团队不太可能在语言中加入泛型，这显然是一个政治问题而不是技术问题。错误处理也是如此。\n\n和许多人一样，该网友**认为 Go 在极简主义和功能之间没有取得正确的平衡**。反对泛型的人赞成用编译时类型检查（总是安全的）换取运行时类型断言（可能失败）。\n\n他们拒绝承认这一事实。这就是他们反对泛型的论点，并将最终损害语言的任何潜在增长。他们基本上是在违背自己的利益。\n\n## 官方回复\n\nRuss Cox 做了正式的回复：很抱歉，但不是：**泛型是一个技术问题**，不是一个政治问题。\n\nGo 团队并**不反对泛型本身**，只是反对做那些没有被很好理解或不能很好地与 Go 配合的事情。\n\n这就是核心观点和矛盾点，也从 2009 年，延续到了现在。\n\n### 会遇到的问题\n\nGo 团队认为要将泛型的概念融入 Go，并与系统的其他部分很好地配合，必须解决一些深层次的技术问题，而我们并没有解决这些问题的办法。\n\n关于这些问题，在几年前就在博客上写过一篇《[The Generic Dilemma](https://research.swtch.com/generic \"The Generic Dilemma\")》：\n\n![](https://files.mdnice.com/user/3610/8ed3923a-d5fc-482f-bb6d-6fff7a19fcaa.png)\n\n即使克服了那一页上的问题，也有其他问题，接下来你会遇到的问题是：”如何让程序员以一种有用的、易于解释的方式省略类型注释“。\n\n也就是如何更人性、更易于的表达泛型的类型参数。\n\n#### 泛型例子\n\n举个例子，C++ 允许你写 `make_pair(1, \"foo\")`，而不是 `make_pair<int, string>(1, \"foo\")`。\n\n为了达到这种效果，推断注释背后的逻辑需要几页几页的规范，这并不是一个特别容易理解的编程模型，当事情出错时，编译器也不能轻易解释。\n\n在这之后肯定还有更多的新问题在这里面。\n\n#### 和专家沟通\n\nGo 团队和一些真正的 Java 泛型专家谈过，他们每个人都说了大致相同的话：要非常小心，它不像看起来那么容易，而且你会被你犯的所有错误困住。\n\n作为一个 Java 示范，可以浏览一下《[Java Generics FAQs - Frequently Asked Questions](http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html \"Java Generics FAQs - Frequently Asked Questions\")》的大部分内容：\n\n![](https://files.mdnice.com/user/3610/6e188c16-f31c-4d6b-9d1b-216aa702d548.png)\n\n看看过了多久你会开始思考 \"这真的是最好的方法吗？\"。\n\n在泛型过程中会遇到许多问题，像是《[How do I decrypt Enum<E extends Enum<E>>](http://www.angelikalanger.com/GenericsFAQ/FAQSections/TypeParameters.html#FAQ106\" \"How do I decrypt Enum<E extends Enum<E>>\")》：\n\n![](https://files.mdnice.com/user/3610/2cbd8083-cee6-48f7-a0bd-d6faf8d27c40.png)\n\n为此，Go 团队在泛型的推动上非常谨慎。\n  \n## 承认缺点\n\nGo 团队说得很清楚，承认这个事实：**没有泛型是有一定的缺点的**。\n  \n你要么使用 `interface{}` 而放弃编译时检查，要么写代码生成器而使你的构建过程复杂化。\n\n现有语言中实现的泛型也有明确的缺点，而且今天不妥协有一个非常大的好处：它使明天**采用更好的解决方案变得更加容易**。\n\n## 总结\n  \n今天给大家分享了过去在国外社区针对 Go 泛型的各种争议和探讨，其实泛型的核心观点很明确：”Go 团队不反对泛型本身“。\n  \n一直没能把泛型做起来，也是因为顾虑很多，做泛型要和 Go 多个部分，要解决很多深层次的问题，还要解决类型参数可读性等问题。所以一直拖到了现在。\n  \n回到即将 2022 年的现在，都预言对了。社区都在扯做泛型后的多个关联组件，以及泛型可读性和结构...\n\n显然，**泛型就是双刃剑？你怎么看**。"
  },
  {
    "path": "content/posts/go/len.md",
    "content": "---\ntitle: \"迷惑了，Go len() 是怎么计算出来的？\"\ndate: 2021-12-31T12:54:58+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n最近看到了一个很有意思的话题，我们平时常常会用 Go 的内置函数 `len` 去获取各种 map、slice 的长度，那他是怎么实现的呢？\n\n正当我想去看看 `len` 的具体实现时，一展身手，却发现竟然是个空方法：\n\n```golang\nfunc len(v Type) int\n```\n\n看注解也没有 link 到其他 runtime 函数，那么 len 函数是如何被调用的呢？\n\n先前看国外讨论 Go 计算 len 的文章时做了一些翻译和笔记（底下有参考链接），在此分享给大家，共同进步。\n\n## 谜底\n\n今天就由煎鱼带大家一同解开这个谜底。既然是谜底，那就一开始就揭开。\n\n其实 Go 语言中并没有 len 函数的具体实现代码，他其实**是 Go 编译器的 \"魔法\" ，不是实际的函数调用**。\n\n接下来将展开这部分，我们可以更深入地了解 Go 编译器的内部工作原理。\n\n## 编译器\n\n在 Go 编译器编译时会解析命令行参数中指定的标志和 Go 源文件，对解析后的 Go 包进行类型检查，将函数编译为机器代码。代码，最后将编译后的包定义写到磁盘上。\n\n内部定义基本类型、内置函数和操作函数的阶段是在 types/universe.go 当中。同时会进行内置函数和具体的操作符匹配，可以明确知道内置函数 len 对应的是 OLEN：\n\n```golang\nvar builtinFuncs = [...]struct {\n\tname string\n\top   Op\n}{\n\t{\"append\", OAPPEND},\n\t{\"cap\", OCAP},\n\t{\"close\", OCLOSE},\n\t{\"complex\", OCOMPLEX},\n\t{\"copy\", OCOPY},\n\t{\"delete\", ODELETE},\n\t{\"imag\", OIMAG},\n\t{\"len\", OLEN},\n\t...\n}\n```\n\n在编译时，上分为五个阶段进行类型检查：\n- 第一阶段：常量、类型、以及函数的名称和类型。\n- 第二阶段：变量赋值、接口赋值、别名声明。\n- 第三阶段：类型检查函数体。\n- 第四阶段：检查外部声明。\n- 第五阶段：检查类型的地图键，未使用的导入。\n\n如果最后一个类型检查阶段遇到 len 函数，就会转换为 UnaryExpr 类型，一个 UnaryExpr 节点代表一个单数表达式，也最终就是不会成为函数调用：\n\n```golang\nfunc typecheck1(n ir.Node, top int) ir.Node {\n\tif n, ok := n.(*ir.Name); ok {\n\t\ttypecheckdef(n)\n\t}\n\n\tswitch n.Op() {\n\t...\n\tcase ir.OCAP, ir.OLEN:\n\t\tn := n.(*ir.UnaryExpr)\n\t\treturn tcLenCap(n)\n\t}\n}\n```\n\n在调用 `*ir.UnaryExpr` 转换完毕后，会调用 `tcLenCap`，也就是 typecheck，使用 okforlen 数组来验证参数的合法性或发出相关错误信息：\n\n```golang\nfunc tcLenCap(n *ir.UnaryExpr) ir.Node {\n\tn.X = Expr(n.X)\n\tn.X = DefaultLit(n.X, nil)\n\tn.X = implicitstar(n.X)\n\t...\n\tvar ok bool\n\tif n.Op() == ir.OLEN {\n\t\tok = okforlen[t.Kind()]\n\t} else {\n\t\tok = okforcap[t.Kind()]\n\t}\n  \n\t...\n\tn.SetType(types.Types[types.TINT])\n\treturn n\n}\n```\n\n经历过上面的步骤后在对所有内容进行类型检查后，所有函数都将排队等待编译：\n\n```golang\n\tbase.Timer.Start(\"be\", \"compilefuncs\")\n\tfcount := int64(0)\n\tfor i := 0; i < len(typecheck.Target.Decls); i++ {\n\t\tif fn, ok := typecheck.Target.Decls[i].(*ir.Func); ok {\n\t\t\tenqueueFunc(fn)\n\t\t\tfcount++\n\t\t}\n\t}\n\tbase.Timer.AddEvent(fcount, \"funcs\")\n\n\tcompileFunctions()\n```\n\n在经过在 buildssa 和 genssa 之后，再深入几层，就会将 AST 树中的 len 表达式转换为 SSA。接着我们就可以看到 Go 语言中的每种类型的长度是怎么获取的。\n\n这块的处理对应 internal/ssagen/ssa.go 的 expr 方法，如下：\n\n```golang\n\tcase ir.OLEN, ir.OCAP:\n\t\tn := n.(*ir.UnaryExpr)\n\t\tswitch {\n\t\tcase n.X.Type().IsSlice():\n\t\t\top := ssa.OpSliceLen\n\t\t\tif n.Op() == ir.OCAP {\n\t\t\t\top = ssa.OpSliceCap\n\t\t\t}\n\t\t\treturn s.newValue1(op, types.Types[types.TINT], s.expr(n.X))\n\t\tcase n.X.Type().IsString(): // string; not reachable for OCAP\n\t\t\treturn s.newValue1(ssa.OpStringLen, types.Types[types.TINT], s.expr(n.X))\n\t\tcase n.X.Type().IsMap(), n.X.Type().IsChan():\n\t\t\treturn s.referenceTypeBuiltin(n, s.expr(n.X))\n\t\tdefault: // array\n\t\t\treturn s.constInt(types.Types[types.TINT], n.X.Type().NumElem())\n\t\t}\n```\n\n若是数组（array）类型，则会调用 `NumElem` 方法来获取长度值：\n\n```golang\ntype Array struct {\n\tElem  *Type \n\tBound int64 \n}\n\nfunc (t *Type) NumElem() int64 {\n\tt.wantEtype(TARRAY)\n\treturn t.Extra.(*Array).Bound\n}\n```\n\n若是字典（map）类型或通道（channel），将会调用 `referenceTypeBuiltin` 方法：\n\n```golang\nfunc (s *state) referenceTypeBuiltin(n *ir.UnaryExpr, x *ssa.Value) *ssa.Value {\n\tlenType := n.Type()\n\tnilValue := s.constNil(types.Types[types.TUINTPTR])\n\tcmp := s.newValue2(ssa.OpEqPtr, types.Types[types.TBOOL], x, nilValue)\n\tb := s.endBlock()\n\tb.Kind = ssa.BlockIf\n\tb.SetControl(cmp)\n\tb.Likely = ssa.BranchUnlikely\n\n\tbThen := s.f.NewBlock(ssa.BlockPlain)\n\tbElse := s.f.NewBlock(ssa.BlockPlain)\n\tbAfter := s.f.NewBlock(ssa.BlockPlain)\n\t...\n\tswitch n.Op() {\n\tcase ir.OLEN:\n\t\ts.vars[n] = s.load(lenType, x)\n\t...\n\treturn s.variable(n, lenType)\n}\n```\n\n该函数的作用是是获取 map 或chan 的内存地址，并以零偏移量引用其结构布局，就像 `unsafe.Pointer(uintptr(unsafe.Pointer(s))` 一样，返回第一个字面字段的值。\n\n那为什么要获取结构体的第一个字段的值呢，应该是和 map 和 chan 的基础数据结构有关：\n\n```golang\ntype hmap struct {\n\tcount     int \n  ...\n}\n\ntype hchan struct {\n\tqcount   uint    \n\t...\n}\n```\n\n是因为 map 和 chan 的基础数据结构的第一个字段就表示长度，自然也就通过计算偏移值来获取了。\n\n其他的数据类型，大家可以继续深入代码，再细看就好了。主要还是枚举多同类的数据类型，接着调用相应的方法。\n\n## 总结\n\n每次我们看到内置函数时，总会下意识的以为是在 runtime 内实现的。看不到 runtime 内的实现方法，又会以为是通过注解 link 的方式来解决的。\n\n但需要注意，其实还有像 len 内置函数这种直接编译器转换的，这也是一种不错的优化方式。\n\n## 参考\n- https://tpaschalis.github.io/golang-len\n- https://stackoverflow.com/questions/28204831/how-do-os-len-and-make-functions-work"
  },
  {
    "path": "content/posts/go/map/2019-03-05-map-access.md",
    "content": "---\n\ntitle:      \"深入理解 Go map：初始化和访问元素\"\ndate:       2019-03-05 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - 源码分析\n---\n\n从本文开始咱们一起探索 Go map 里面的奥妙吧，看看它的内在是怎么构成的，又分别有什么值得留意的地方？\n\n第一篇将探讨**初始化和访问元素**相关板块，咱们带着疑问去学习，例如：\n\n- 初始化的时候会马上分配内存吗？\n- 底层数据是如何存储的？\n- 底层是如何使用 key 去寻找数据的？\n- 底层是用什么方式解决哈希冲突的？\n- 数据类型那么多，底层又是怎么处理的呢？\n\n...\n\n## 数据结构\n\n首先我们一起看看 Go map 的基础数据结构，先有一个大致的印象\n\n![image](https://s2.ax1x.com/2020/02/27/3dLgjH.png)\n\n### hmap\n\n```go\ntype hmap struct {\n\tcount     int\n\tflags     uint8\n\tB         uint8\n\tnoverflow uint16\n\thash0     uint32\n\tbuckets    unsafe.Pointer\n\toldbuckets unsafe.Pointer\n\tnevacuate  uintptr\n\textra *mapextra\n}\n\ntype mapextra struct {\n\toverflow    *[]*bmap\n\toldoverflow *[]*bmap\n\tnextOverflow *bmap\n}\n```\n\n- count：map 的大小，也就是 len() 的值。代指 map 中的键值对个数\n- flags：状态标识，主要是 goroutine 写入和扩容机制的相关状态控制。并发读写的判断条件之一就是该值\n- B：桶，最大可容纳的元素数量，值为 **负载因子（默认 6.5） \\* 2 ^ B**，是 2 的指数\n- noverflow：溢出桶的数量\n- hash0：哈希因子\n- buckets：保存当前桶数据的指针地址（指向一段连续的内存地址，主要存储键值对数据）\n- oldbuckets，保存旧桶的指针地址\n- nevacuate：迁移进度\n- extra：原有 buckets 满载后，会发生扩容动作，在 Go 的机制中使用了增量扩容，如下为细项：\n  - `overflow` 为 `hmap.buckets` （当前）溢出桶的指针地址\n  - `oldoverflow` 为 `hmap.oldbuckets` （旧）溢出桶的指针地址\n  - `nextOverflow` 为空闲溢出桶的指针地址\n\n在这里我们要注意几点，如下：\n\n1. 如果 keys 和 values 都不包含指针并且允许内联的情况下。会将 bucket 标识为不包含指针，使用 extra 存储溢出桶就可以避免 GC 扫描整个 map，节省不必要的开销\n2. 在前面有提到，Go 用了增量扩容。而 `buckets` 和 `oldbuckets` 也是与扩容相关的载体，一般情况下只使用 `buckets`，`oldbuckets` 是为空的。但如果正在扩容的话，`oldbuckets` 便不为空，`buckets` 的大小也会改变\n3. 当 `hint` 大于 8 时，就会使用 `*mapextra` 做溢出桶。若小于 8，则存储在 buckets 桶中\n\n### bmap\n\n![image](https://s2.ax1x.com/2020/02/27/3dLz5V.png)\n\n```go\nbucketCntBits = 3\nbucketCnt     = 1 << bucketCntBits\n...\ntype bmap struct {\n\ttophash [bucketCnt]uint8\n}\n```\n\n- tophash：key 的 hash 值高 8 位\n- keys：8 个 key\n- values：8 个 value\n- overflow：下一个溢出桶的指针地址（当 hash 冲突发生时）\n\n实际 bmap 就是 buckets 中的 bucket，一个 bucket 最多存储 8 个键值对\n\n#### tophash\n\ntophash 是个长度为 8 的数组，代指桶最大可容纳的键值对为 8。\n\n存储每个元素 hash 值的高 8 位，如果 `tophash [0] <minTopHash`，则 `tophash [0]` 表示为迁移进度\n\n#### keys 和 values\n\n在这里我们留意到，存储 k 和 v 的载体并不是用 `k/v/k/v/k/v/k/v` 的模式，而是 `k/k/k/k/v/v/v/v` 的形式去存储。这是为什么呢？\n\n```go\nmap[int64]int8\n```\n\n在这个例子中，如果按照 `k/v/k/v/k/v/k/v` 的形式存放的话，虽然每个键值对的值都只占用 1 个字节。但是却需要 7 个填充字节来补齐内存空间。最终就会造成大量的内存 “浪费”\n\n![image](https://s2.ax1x.com/2020/02/27/3dOK2D.png)\n\n但是如果以 `k/k/k/k/v/v/v/v` 的形式存放的话，就能够解决因对齐所 \"浪费\" 的内存空间\n\n因此这部分的拆分主要是考虑到内存对齐的问题，虽然相对会复杂一点，但依然值得如此设计\n\n![image](https://s2.ax1x.com/2020/02/27/3dODqs.png)\n\n#### overflow\n\n可能会有同学疑惑为什么会有溢出桶这个东西？实际上在不存在哈希冲突的情况下，去掉溢出桶，也就是只需要桶、哈希因子、哈希算法。也能实现一个简单的 hash table。但是哈希冲突（碰撞）是不可避免的...\n\n而在 Go map 中当 `hmap.buckets` 满了后，就会使用溢出桶接着存储。我们结合分析可确定 Go 采用的是数组 + 链地址法解决哈希冲突\n\n![image](https://s2.ax1x.com/2020/02/27/3dO7Ix.png)\n\n## 初始化\n\n### 用法\n\n```go\nm := make(map[int32]int32)\n```\n\n### 函数原型\n\n通过阅读源码可得知，初始化方法有好几种。函数原型如下：\n\n```go\nfunc makemap_small() *hmap\nfunc makemap64(t *maptype, hint int64, h *hmap) *hmap\nfunc makemap(t *maptype, hint int, h *hmap) *hmap\n```\n\n- makemap_small：当 `hint` 小于 8 时，会调用 `makemap_small` 来初始化 hmap。主要差异在于是否会马上初始化 hash table\n- makemap64：当 `hint` 类型为 int64 时的特殊转换及校验处理，后续实质调用 `makemap`\n- makemap：实现了标准的 map 初始化动作\n\n### 源码\n\n```go\nfunc makemap(t *maptype, hint int, h *hmap) *hmap {\n\tif hint < 0 || hint > int(maxSliceCap(t.bucket.size)) {\n\t\thint = 0\n\t}\n\n\tif h == nil {\n\t\th = new(hmap)\n\t}\n\th.hash0 = fastrand()\n\n\tB := uint8(0)\n\tfor overLoadFactor(hint, B) {\n\t\tB++\n\t}\n\th.B = B\n\n\tif h.B != 0 {\n\t\tvar nextOverflow *bmap\n\t\th.buckets, nextOverflow = makeBucketArray(t, h.B, nil)\n\t\tif nextOverflow != nil {\n\t\t\th.extra = new(mapextra)\n\t\t\th.extra.nextOverflow = nextOverflow\n\t\t}\n\t}\n\n\treturn h\n}\n```\n\n- 根据传入的 `bucket` 类型，获取其类型能够申请的最大容量大小。并对其长度 `make(map[k]v, hint)` 进行边界值检验\n- 初始化 hmap\n- 初始化哈希因子\n- 根据传入的 `hint`，计算一个可以放下 `hint` 个元素的桶 `B` 的最小值\n- 分配并初始化 hash table。如果 `B` 为 0 将在后续懒惰分配桶，大于 0 则会马上进行分配\n- 返回初始化完毕的 hmap\n\n在这里可以注意到，（当 `hint` 大于等于 8 ）第一次初始化 map 时，就会通过调用 `makeBucketArray` 对 buckets 进行分配。因此我们常常会说，在初始化时指定一个适当大小的容量。能够提升性能。\n\n若该容量过少，而新增的键值对又很多。就会导致频繁的分配 buckets，进行扩容迁移等 rehash 动作。最终结果就是性能直接的下降（敲黑板）\n\n而当 `hint` 小于 8 时，这种问题**相对**就不会凸显的太明显，如下：\n\n```go\nfunc makemap_small() *hmap {\n\th := new(hmap)\n\th.hash0 = fastrand()\n\treturn h\n}\n```\n\n### 图示\n\n![image](https://s2.ax1x.com/2020/02/27/3dOLRO.png)\n\n## 访问\n\n### 用法\n\n```\nv := m[i]\nv, ok := m[i]\n```\n\n### 函数原型\n\n在实现 map 元素访问上有好几种方法，主要是包含针对 32/64 位、string 类型的特殊处理，总的函数原型如下：\n\n```go\nmapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer\nmapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool)\n\nmapaccessK(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, unsafe.Pointer)\n\nmapaccess1_fat(t *maptype, h *hmap, key, zero unsafe.Pointer) unsafe.Pointer\nmapaccess2_fat(t *maptype, h *hmap, key, zero unsafe.Pointer) (unsafe.Pointer, bool)\n\nmapaccess1_fast32(t *maptype, h *hmap, key uint32) unsafe.Pointer\nmapaccess2_fast32(t *maptype, h *hmap, key uint32) (unsafe.Pointer, bool)\nmapassign_fast32(t *maptype, h *hmap, key uint32) unsafe.Pointer\nmapassign_fast32ptr(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer\n\nmapaccess1_fast64(t *maptype, h *hmap, key uint64) unsafe.Pointer\n...\n\nmapaccess1_faststr(t *maptype, h *hmap, ky string) unsafe.Pointer\n...\n```\n\n- mapaccess1：返回 `h[key]` 的指针地址，如果键不在 `map` 中，将返回对应类型的零值\n- mapaccess2：返回 `h[key]` 的指针地址，如果键不在 `map` 中，将返回零值和布尔值用于判断\n\n### 源码\n\n```go\nfunc mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {\n\t...\n\tif h == nil || h.count == 0 {\n\t\treturn unsafe.Pointer(&zeroVal[0])\n\t}\n\tif h.flags&hashWriting != 0 {\n\t\tthrow(\"concurrent map read and map write\")\n\t}\n\talg := t.key.alg\n\thash := alg.hash(key, uintptr(h.hash0))\n\tm := bucketMask(h.B)\n\tb := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))\n\tif c := h.oldbuckets; c != nil {\n\t\tif !h.sameSizeGrow() {\n\t\t\t// There used to be half as many buckets; mask down one more power of two.\n\t\t\tm >>= 1\n\t\t}\n\t\toldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))\n\t\tif !evacuated(oldb) {\n\t\t\tb = oldb\n\t\t}\n\t}\n\ttop := tophash(hash)\n\tfor ; b != nil; b = b.overflow(t) {\n\t\tfor i := uintptr(0); i < bucketCnt; i++ {\n\t\t\tif b.tophash[i] != top {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tk := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))\n\t\t\tif t.indirectkey {\n\t\t\t\tk = *((*unsafe.Pointer)(k))\n\t\t\t}\n\t\t\tif alg.equal(key, k) {\n\t\t\t\tv := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))\n\t\t\t\tif t.indirectvalue {\n\t\t\t\t\tv = *((*unsafe.Pointer)(v))\n\t\t\t\t}\n\t\t\t\treturn v\n\t\t\t}\n\t\t}\n\t}\n\treturn unsafe.Pointer(&zeroVal[0])\n}\n```\n\n- 判断 map 是否为 nil，长度是否为 0。若是则返回零值\n- 判断当前是否并发读写 map，若是则抛出异常\n- 根据 key 的不同类型调用不同的 hash 方法计算得出 hash 值\n- 确定 key 在哪一个 bucket 中，并得到其位置\n- 判断是否正在发生扩容（h.oldbuckets 是否为 nil），若正在扩容，则到老的 buckets 中查找（因为 buckets 中可能还没有值，搬迁未完成），若该 bucket 已经搬迁完毕。则到 buckets 中继续查找\n- 计算 hash 的 tophash 值（高八位）\n- 根据计算出来的 tophash，依次循环对比 buckets 的 tophash 值（快速试错）\n- 如果 tophash 匹配成功，则计算 key 的所在位置，正式完整的对比两个 key 是否一致\n- 若查找成功并返回，若不存在，则返回零值\n\n在上述步骤三中，提到了根据不同的类型计算出 hash 值，另外会计算出 hash 值的高八位和低八位。低八位会作为 bucket index，作用是用于找到 key 所在的 bucket。而高八位会存储在 bmap tophash 中\n\n其主要作用是在上述步骤七中进行迭代快速定位。这样子可以提高性能，而不是一开始就直接用 key 进行一致性对比\n\n### 图示\n\n![image](https://s2.ax1x.com/2020/02/27/3dOOzD.png)\n\n## 总结\n\n在本章节，我们介绍了 map 类型的以下知识点：\n\n- map 的基础数据结构\n- 初始化 map\n- 访问 map\n\n从阅读源码中，得知 Go 本身**对于一些不同大小、不同类型的属性，包括哈希方法都有编写特定方法**去运行。总的来说，这块的设计隐含较多的思路，有不少点值得细细品尝 :)\n\n注：本文基于 Go 1.11.5\n"
  },
  {
    "path": "content/posts/go/map/2019-03-24-map-assign.md",
    "content": "---\n\ntitle:      \"深入理解 Go map：赋值和扩容迁移\"\ndate:       2019-03-24 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - 源码分析\n---\n\n## 概要\n\n在 [上一章节](https://book.eddycjy.com/golang/map/map-access.html) 中，数据结构小节里讲解了大量基础字段，可能你会疑惑需要 #&（！……#（！￥！ 来干嘛？接下来我们一起简单了解一下基础概念。再开始研讨今天文章的重点内容。我相信这样你能更好的读懂这篇文章\n\n### 哈希函数\n\n哈希函数，又称散列算法、散列函数。主要作用是通过特定算法将数据根据一定规则组合重新生成得到一个**散列值**\n\n而在哈希表中，其生成的散列值常用于寻找其键映射到哪一个桶上。而一个好的哈希函数，应当尽量少的出现哈希冲突，以此保证操作哈希表的时间复杂度（但是哈希冲突在目前来讲，是无法避免的。我们需要 “解决” 它）\n\n![image](http://wx3.sinaimg.cn/large/006fVPCvly1g161h7r7hgj30is0dmjro.jpg)\n\n### 链地址法\n\n在哈希操作中，相当核心的一个处理动作就是 “哈希冲突” 的解决。而在 Go map 中采用的就是 \"链地址法 \" 去解决哈希冲突，又称 \"拉链法\"。其主要做法是数组 + 链表的数据结构，其溢出节点的存储内存都是动态申请的，因此相对更灵活。而每一个元素都是一个链表。如下图：\n\n![image](http://wx4.sinaimg.cn/large/006fVPCvly1g1dw2b8t0ej30e60cy747.jpg)\n\n### 桶/溢出桶\n\n```go\ntype hmap struct {\n\t...\n\tbuckets    unsafe.Pointer\n    ...\n\textra *mapextra\n}\n\ntype mapextra struct {\n\toverflow    *[]*bmap\n\toldoverflow *[]*bmap\n\tnextOverflow *bmap\n}\n```\n\n在上章节中，我们介绍了 Go map 中的桶和溢出桶的概念，在其桶中只能存储 8 个键值对元素。当超过 8 个时，将会使用溢出桶进行存储或进行扩容\n\n你可能会有疑问，hint 大于 8 又会怎么样？答案很明显，性能问题，其时间复杂度改变（也就是执行效率出现问题）\n\n## 前言\n\n概要复习的差不多后，接下来我们将一同研讨 Go map 的另外三个核心行为：赋值、扩容、迁移。正式开始我们的研讨之旅吧 ：）\n\n## 赋值\n\n```go\nm := make(map[int32]string)\nm[0] = \"EDDYCJY\"\n```\n\n### 函数原型\n\n在 map 的赋值动作中，依旧是针对 32/64 位、string、pointer 类型有不同的转换处理，总的函数原型如下：\n\n```go\nfunc mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer\nfunc mapaccess1_fast32(t *maptype, h *hmap, key uint32) unsafe.Pointer\nfunc mapaccess2_fast32(t *maptype, h *hmap, key uint32) (unsafe.Pointer, bool)\nfunc mapassign_fast32(t *maptype, h *hmap, key uint32) unsafe.Pointer\nfunc mapassign_fast32ptr(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer\n\nfunc mapaccess1_fast64(t *maptype, h *hmap, key uint64) unsafe.Pointer\nfunc mapaccess2_fast64(t *maptype, h *hmap, key uint64) (unsafe.Pointer, bool)\nfunc mapassign_fast64(t *maptype, h *hmap, key uint64) unsafe.Pointer\nfunc mapassign_fast64ptr(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer\nfunc mapaccess1_faststr(t *maptype, h *hmap, ky string) unsafe.Pointer\nfunc mapaccess2_faststr(t *maptype, h *hmap, ky string) (unsafe.Pointer, bool)\nfunc mapassign_faststr(t *maptype, h *hmap, s string) unsafe.Pointer\n...\n```\n\n接下来我们将分成几个部分去看看底层在赋值的时候，都做了些什么处理？\n\n### 源码\n\n#### 第一阶段：校验和初始化\n\n```go\nfunc mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {\n\tif h == nil {\n\t\tpanic(plainError(\"assignment to entry in nil map\"))\n\t}\n\t...\n\tif h.flags&hashWriting != 0 {\n\t\tthrow(\"concurrent map writes\")\n\t}\n\talg := t.key.alg\n\thash := alg.hash(key, uintptr(h.hash0))\n\n\th.flags |= hashWriting\n\n\tif h.buckets == nil {\n\t\th.buckets = newobject(t.bucket) // newarray(t.bucket, 1)\n\t}\n    ...\n}\n```\n\n- 判断 hmap 是否已经初始化（是否为 nil）\n- 判断是否并发读写 map，若是则抛出异常\n- 根据 key 的不同类型调用不同的 hash 方法计算得出 hash 值\n- 设置 flags 标志位，表示有一个 goroutine 正在写入数据。因为 `alg.hash` 有可能出现 `panic` 导致异常\n- 判断 buckets 是否为 nil，若是则调用 `newobject` 根据当前 bucket 大小进行分配（例如：上章节提到的 `makemap_small` 方法，就在初始化时没有初始 buckets，那么它在第一次赋值时就会对 buckets 分配）\n\n#### 第二阶段：寻找可插入位和更新既有值\n\n```go\n...\nagain:\n\tbucket := hash & bucketMask(h.B)\n\tif h.growing() {\n\t\tgrowWork(t, h, bucket)\n\t}\n\tb := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize)))\n\ttop := tophash(hash)\n\n\tvar inserti *uint8\n\tvar insertk unsafe.Pointer\n\tvar val unsafe.Pointer\n\tfor {\n\t\tfor i := uintptr(0); i < bucketCnt; i++ {\n\t\t\tif b.tophash[i] != top {\n\t\t\t\tif b.tophash[i] == empty && inserti == nil {\n\t\t\t\t\tinserti = &b.tophash[i]\n\t\t\t\t\tinsertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))\n\t\t\t\t\tval = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tk := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))\n\t\t\tif t.indirectkey {\n\t\t\t\tk = *((*unsafe.Pointer)(k))\n\t\t\t}\n\t\t\tif !alg.equal(key, k) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// already have a mapping for key. Update it.\n\t\t\tif t.needkeyupdate {\n\t\t\t\ttypedmemmove(t.key, k, key)\n\t\t\t}\n\t\t\tval = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))\n\t\t\tgoto done\n\t\t}\n\t\tovf := b.overflow(t)\n\t\tif ovf == nil {\n\t\t\tbreak\n\t\t}\n\t\tb = ovf\n\t}\n\n\tif !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {\n\t\thashGrow(t, h)\n\t\tgoto again // Growing the table invalidates everything, so try again\n\t}\n\t...\n```\n\n- 根据低八位计算得到 bucket 的内存地址，并判断是否正在扩容，若正在扩容中则先迁移再接着处理\n- 计算并得到 bucket 的 bmap 指针地址，计算 key hash 高八位用于查找 Key\n- 迭代 buckets 中的每一个 bucket（共 8 个），对比 `bucket.tophash` 与 top（高八位）是否一致\n- 若不一致，判断是否为空槽。若是空槽（有两种情况，第一种是**没有插入过**。第二种是**插入后被删除**），则把该位置标识为可插入 tophash 位置。注意，这里就是第一个可以插入数据的地方\n- 若 key 与当前 k 不匹配则跳过。但若是匹配（也就是原本已经存在），则进行更新。最后跳出并返回 value 的内存地址\n- 判断是否迭代完毕，若是则结束迭代 buckets 并更新当前桶位置\n- 若满足三个条件：触发最大 `LoadFactor` 、存在过多溢出桶 `overflow buckets`、没有正在进行扩容。就会进行扩容动作（以确保后续的动作）\n\n总的来讲，这一块逻辑做了两件大事，第一是**寻找空位，将位置其记录在案，用于后续的插入动作**。第二是**判断 Key 是否已经存在哈希表中，存在则进行更新**。而若是第二种场景，更新完毕后就会进行收尾动作，第一种将继续执行下述的代码\n\n#### 第三阶段：申请新的插入位和插入新值\n\n```go\n    ...\n\tif inserti == nil {\n\t\tnewb := h.newoverflow(t, b)\n\t\tinserti = &newb.tophash[0]\n\t\tinsertk = add(unsafe.Pointer(newb), dataOffset)\n\t\tval = add(insertk, bucketCnt*uintptr(t.keysize))\n\t}\n\n\tif t.indirectkey {\n\t\tkmem := newobject(t.key)\n\t\t*(*unsafe.Pointer)(insertk) = kmem\n\t\tinsertk = kmem\n\t}\n\tif t.indirectvalue {\n\t\tvmem := newobject(t.elem)\n\t\t*(*unsafe.Pointer)(val) = vmem\n\t}\n\ttypedmemmove(t.key, insertk, key)\n\t*inserti = top\n\th.count++\n\ndone:\n\t...\n\treturn val\n\n```\n\n经过前面迭代寻找动作，若没有找到可插入的位置，意味着当前的所有桶都满了，将重新分配一个新溢出桶用于插入动作。最后再在上一步申请的新插入位置，存储键值对，返回该值的内存地址\n\n#### 第四阶段：写入\n\n但是这里又疑惑了？最后为什么是返回内存地址。这是因为隐藏的最后一步写入动作（将值拷贝到指定内存区域）是通过底层汇编配合来完成的，在 runtime 中只完成了绝大部分的动作\n\n```go\nfunc main() {\n\tm := make(map[int32]int32)\n\tm[0] = 6666666\n}\n```\n\n对应的汇编部分：\n\n```\n...\n0x0099 00153 (test.go:6)\tCALL\truntime.mapassign_fast32(SB)\n0x009e 00158 (test.go:6)\tPCDATA\t$2, $2\n0x009e 00158 (test.go:6)\tMOVQ\t24(SP), AX\n0x00a3 00163 (test.go:6)\tPCDATA\t$2, $0\n0x00a3 00163 (test.go:6)\tMOVL\t$6666666, (AX)\n```\n\n这里分为了几个部位，主要是调用 `mapassign` 函数和拿到值存放的内存地址，再将 6666666 这个值存放进该内存地址中。另外我们看到 `PCDATA` 指令，主要是包含一些垃圾回收的信息，由编译器产生\n\n### 小结\n\n通过前面几个阶段的分析，我们可梳理出一些要点。例如：\n\n- 不同类型对应哈希函数不一样\n- 高八位用于定位 bucket\n- 低八位用于定位 key，快速试错后再进行完整对比\n- buckets/overflow buckets 遍历\n- 可插入位的处理\n- 最终写入动作与底层汇编的交互\n\n## 扩容\n\n在所有动作中，扩容规则是大家较关注的点，也是赋值里非常重要的一环。因此咱们将这节拉出来，对这块细节进行研讨\n\n### 什么时候扩容\n\n```go\nif !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {\n\thashGrow(t, h)\n\tgoto again\n}\n```\n\n在特定条件的情况下且当前没有正在进行扩容动作（以判断 `hmap.oldbuckets != nil` 为基准）。哈希表在赋值、删除的动作下会触发扩容行为，条件如下：\n\n- 触发 `load factor` 的最大值，负载因子已达到当前界限\n- 溢出桶 `overflow buckets` 过多\n\n### 什么时候受影响\n\n那么什么情况下会对这两个 “值” 有影响呢？如下：\n\n1. 负载因子 `load factor`，用途是评估哈希表当前的时间复杂度，其与哈希表当前包含的键值对数、桶数量等相关。如果负载因子越大，则说明空间使用率越高，但产生哈希冲突的可能性更高。而负载因子越小，说明空间使用率低，产生哈希冲突的可能性更低\n2. 溢出桶 `overflow buckets` 的判定与 buckets 总数和 overflow buckets 总数相关联\n\n### 因子关系\n\n| loadFactor | %overflow | bytes/entry | hitprobe | missprobe |\n| ---------- | --------- | ----------- | -------- | --------- |\n| 4.00       | 2.13      | 20.77       | 3.00     | 4.00      |\n| 4.50       | 4.05      | 17.30       | 3.25     | 4.50      |\n| 5.00       | 6.85      | 14.77       | 3.50     | 5.00      |\n| 5.50       | 10.55     | 12.94       | 3.75     | 5.50      |\n| 6.00       | 15.27     | 11.67       | 4.00     | 6.00      |\n| 6.50       | 20.90     | 10.79       | 4.25     | 6.50      |\n| 7.00       | 27.14     | 10.15       | 4.50     | 7.00      |\n\n- loadFactor：负载因子\n- %overflow：溢出率，具有溢出桶 `overflow buckets` 的桶的百分比\n- bytes/entry：每个键值对所的字节数开销\n- hitprobe：查找存在的 key 时，平均需要检索的条目数量\n- missprobe：查找不存在的 key 时，平均需要检索的条目数量\n\n这一组数据能够体现出不同的负载因子会给哈希表的动作带来怎么样的影响。而在上一章节我们有提到默认的负载因子是 6.5 (loadFactorNum/loadFactorDen)，可以看出来是经过测试后取出的一个比较合理的因子。能够较好的影响哈希表的扩容动作的时机\n\n### 源码剖析\n\n```go\nfunc hashGrow(t *maptype, h *hmap) {\n\tbigger := uint8(1)\n\tif !overLoadFactor(h.count+1, h.B) {\n\t\tbigger = 0\n\t\th.flags |= sameSizeGrow\n\t}\n\toldbuckets := h.buckets\n\tnewbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)\n    ...\n\th.oldbuckets = oldbuckets\n\th.buckets = newbuckets\n\th.nevacuate = 0\n\th.noverflow = 0\n\n\tif h.extra != nil && h.extra.overflow != nil {\n\t\tif h.extra.oldoverflow != nil {\n\t\t\tthrow(\"oldoverflow is not nil\")\n\t\t}\n\t\th.extra.oldoverflow = h.extra.overflow\n\t\th.extra.overflow = nil\n\t}\n\tif nextOverflow != nil {\n\t\tif h.extra == nil {\n\t\t\th.extra = new(mapextra)\n\t\t}\n\t\th.extra.nextOverflow = nextOverflow\n\t}\n\n\t// the actual copying of the hash table data is done incrementally\n\t// by growWork() and evacuate().\n}\n```\n\n#### 第一阶段：确定扩容容量规则\n\n在上小节有讲到扩容的依据有两种，在 `hashGrow` 开头就进行了划分。如下：\n\n```go\nif !overLoadFactor(h.count+1, h.B) {\n\tbigger = 0\n\th.flags |= sameSizeGrow\n}\n```\n\n若不是负载因子 `load factor` 超过当前界限，也就是属于溢出桶 `overflow buckets` 过多的情况。因此本次扩容规则将是 `sameSizeGrow`，即是**不改变大小的扩容动作**。那要是前者的情况呢？\n\n```go\nbigger := uint8(1)\n...\nnewbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)\n```\n\n结合代码分析可得出，若是负载因子 `load factor` 达到当前界限，将会动态扩容**当前大小的两倍**作为其新容量大小\n\n#### 第二阶段：初始化、交换新旧 桶/溢出桶\n\n主要是针对扩容的相关数据**前置处理**，涉及 buckets/oldbuckets、overflow/oldoverflow 之类与存储相关的字段\n\n```go\n...\noldbuckets := h.buckets\nnewbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)\n\nflags := h.flags &^ (iterator | oldIterator)\nif h.flags&iterator != 0 {\n\tflags |= oldIterator\n}\n\nh.B += bigger\n...\nh.noverflow = 0\n\nif h.extra != nil && h.extra.overflow != nil {\n\t...\n\th.extra.oldoverflow = h.extra.overflow\n\th.extra.overflow = nil\n}\nif nextOverflow != nil {\n\t...\n\th.extra.nextOverflow = nextOverflow\n}\n```\n\n这里注意到这段代码： `newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)`。第一反应是扩容的时候就马上申请并初始化内存了吗？假设涉及大量的内存分配，那挺耗费性能的...\n\n然而并不，内部只会先进行预分配，当使用的时候才会真正的去初始化\n\n#### 第三阶段：扩容\n\n在源码中，发现第三阶段的流转并没有显式展示。这是因为流转由底层去做控制了。但通过分析代码和注释，可得知由第三阶段涉及 `growWork` 和 `evacuate` 方法。如下：\n\n```go\nfunc growWork(t *maptype, h *hmap, bucket uintptr) {\n\tevacuate(t, h, bucket&h.oldbucketmask())\n\n\tif h.growing() {\n\t\tevacuate(t, h, h.nevacuate)\n\t}\n}\n```\n\n在该方法中，主要是两个 `evacuate` 函数的调用。他们在调用上又分别有什么区别呢？如下：\n\n- evacuate(t, h, bucket&h.oldbucketmask()): 将 oldbucket 中的元素迁移 rehash 到扩容后的新 bucket\n- evacuate(t, h, h.nevacuate): 如果当前正在进行扩容，则再进行多一次迁移\n\n另外，在执行扩容动作的时候，可以发现都是以 bucket/oldbucket 为单位的，而不是传统的 buckets/oldbuckets。再结合代码分析，可得知在 Go map 中**扩容是采取增量扩容的方式，并非一步到位**\n\n##### 为什么是增量扩容？\n\n如果是全量扩容的话，那问题就来了。假设当前 hmap 的容量比较大，直接全量扩容的话，就会导致扩容要花费大量的时间和内存，导致系统卡顿，最直观的表现就是慢。显然，不能这么做\n\n而增量扩容，就可以解决这个问题。它通过每一次的 map 操作行为去分摊总的一次性动作。因此有了 buckets/oldbuckets 的设计，它是逐步完成的，并且会在扩容完毕后才进行清空\n\n### 小结\n\n通过前面三个阶段的分析，可以得知扩容的大致过程。我们阶段性总结一下。主要如下：\n\n- 根据需扩容的原因不同（overLoadFactor/tooManyOverflowBuckets），分为两类容量规则方向，为等量扩容（不改变原有大小）或双倍扩容\n- 新申请的扩容空间（newbuckets/newoverflow）都是预分配，等真正使用的时候才会初始化\n- 扩容完毕后（预分配），不会马上就进行迁移。而是采取**增量扩容**的方式，当有访问到具体 bukcet 时，才会逐渐的进行迁移（将 oldbucket 迁移到 bucket）\n\n这时候又想到，既然迁移是逐步进行的。那如果在途中又要扩容了，怎么办？\n\n```go\nagain:\n\tbucket := hash & bucketMask(h.B)\n    ...\n\tif !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {\n\t\thashGrow(t, h)\n\t\tgoto again\n\t}\n```\n\n在这里注意到 `goto again` 语句，结合上下文可得若正在进行扩容，就会不断地进行迁移。待迁移完毕后才会开始进行下一次的扩容动作\n\n## 迁移\n\n在扩容的完整闭环中，包含着迁移的动作，又称 “搬迁”。因此我们继续深入研究 `evacuate` 函数。接下来一起打开迁移世界的大门。如下：\n\n```go\ntype evacDst struct {\n\tb *bmap\n\ti int\n\tk unsafe.Pointer\n\tv unsafe.Pointer\n}\n```\n\n`evacDst` 是迁移中的基础数据结构，其包含如下字段：\n\n- b: 当前目标桶\n- i: 当前目标桶存储的键值对数量\n- k: 指向当前 key 的内存地址\n- v: 指向当前 value 的内存地址\n\n```go\nfunc evacuate(t *maptype, h *hmap, oldbucket uintptr) {\n\tb := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))\n\tnewbit := h.noldbuckets()\n\tif !evacuated(b) {\n\t\tvar xy [2]evacDst\n\t\tx := &xy[0]\n\t\tx.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.bucketsize)))\n\t\tx.k = add(unsafe.Pointer(x.b), dataOffset)\n\t\tx.v = add(x.k, bucketCnt*uintptr(t.keysize))\n\n\t\tif !h.sameSizeGrow() {\n\t\t\ty := &xy[1]\n\t\t\ty.b = (*bmap)(add(h.buckets, (oldbucket+newbit)*uintptr(t.bucketsize)))\n\t\t\ty.k = add(unsafe.Pointer(y.b), dataOffset)\n\t\t\ty.v = add(y.k, bucketCnt*uintptr(t.keysize))\n\t\t}\n\n\t\tfor ; b != nil; b = b.overflow(t) {\n            ...\n\t\t}\n\n\t\tif h.flags&oldIterator == 0 && t.bucket.kind&kindNoPointers == 0 {\n\t\t\tb := add(h.oldbuckets, oldbucket*uintptr(t.bucketsize))\n\t\t\tptr := add(b, dataOffset)\n\t\t\tn := uintptr(t.bucketsize) - dataOffset\n\t\t\tmemclrHasPointers(ptr, n)\n\t\t}\n\t}\n\n\tif oldbucket == h.nevacuate {\n\t\tadvanceEvacuationMark(h, t, newbit)\n\t}\n}\n```\n\n- 计算并得到 oldbucket 的 bmap 指针地址\n- 计算 hmap 在增长之前的桶数量\n- 判断当前的迁移（搬迁）状态，以便流转后续的操作。若没有正在进行迁移 `!evacuated(b)` ，则根据扩容的规则的不同，当规则为等量扩容 `sameSizeGrow` 时，只使用一个 `evacDst` 桶用于分流。而为双倍扩容时，就会使用两个 `evacDst` 进行分流操作\n- 当分流完毕后，需要迁移的数据都会通过 `typedmemmove` 函数迁移到指定的目标桶上\n- 若当前不存在 flags 使用标志、使用 oldbucket 迭代器、bucket 不为指针类型。则取消链接溢出桶、清除键值\n- 在最后 `advanceEvacuationMark` 函数中会对迁移进度 `hmap.nevacuate` 进行累积计数，并调用 `bucketEvacuated` 对旧桶 oldbuckets 进行不断的迁移。直至全部迁移完毕。那么也就表示扩容完毕了，会对 `hmap.oldbuckets` 和 `h.extra.oldoverflow` 进行清空\n\n总的来讲，就是计算得到所需数据的位置。再根据当前的迁移状态、扩容规则进行数据分流迁移。结束后进行清理，促进 GC 的回收\n\n## 总结\n\n在本章节我们主要研讨了 Go map 的几个核心动作，分别是：“赋值、扩容、迁移” 。而通过本次的阅读，我们能够更进一步的认识到一些要点，例如：\n\n- 赋值的时候会触发扩容吗？\n- 负载因子是什么？过高会带来什么问题？它的变动会对哈希表操作带来什么影响吗？\n- 溢出桶越多会带来什么问题？\n- 是否要扩容的基准条件是什么？\n- 扩容的容量规则是怎么样的？\n- 扩容的步骤是怎么样的？涉及到了哪些数据结构？\n- 扩容是一次性扩容还是增量扩容？\n- 正在扩容的时候又要扩容怎么办？\n- 扩容时的迁移分流动作是怎么样的？\n- 在扩容动作中，底层汇编承担了什么角色？做了什么事？\n- 在 buckets/overflow buckets 中寻找时，是如何 “快速” 定位值的？低八位、高八位的用途？\n- 空槽有可能出现在任意位置吗？假设已经没有空槽了，但是又有新值要插入，底层会怎么处理\n\n最后希望你通过本文的阅读，能更清楚地了解到 Go map 是怎么样运作的 ：）\n"
  },
  {
    "path": "content/posts/go/map/2019-04-07-why-map-no-order.md",
    "content": "---\n\ntitle:      \"为什么遍历 Go map 是无序的\"\ndate:       2019-04-07 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - 源码分析\n---\n\n![image](http://wx2.sinaimg.cn/large/006fVPCvly1g1s1ah84k8j30k70dvaac.jpg)\n\n有的小伙伴没留意过 Go map 输出顺序，以为它是稳定的有序的；有的小伙伴知道是无序的，但却不知道为什么？有的却理解错误？今天我们将通过本文，揭开 `for range map` 的 “神秘” 面纱，看看它内部实现到底是怎么样的，输出顺序到底是怎么样？\n\n## 前言\n\n```go\nfunc main() {\n\tm := make(map[int32]string)\n\tm[0] = \"EDDYCJY1\"\n\tm[1] = \"EDDYCJY2\"\n\tm[2] = \"EDDYCJY3\"\n\tm[3] = \"EDDYCJY4\"\n\tm[4] = \"EDDYCJY5\"\n\n\tfor k, v := range m {\n\t\tlog.Printf(\"k: %v, v: %v\", k, v)\n\t}\n}\n```\n\n假设运行这段代码，输出结果是按顺序？还是无序输出呢？\n\n```\n2019/04/03 23:27:29 k: 3, v: EDDYCJY4\n2019/04/03 23:27:29 k: 4, v: EDDYCJY5\n2019/04/03 23:27:29 k: 0, v: EDDYCJY1\n2019/04/03 23:27:29 k: 1, v: EDDYCJY2\n2019/04/03 23:27:29 k: 2, v: EDDYCJY3\n```\n\n从输出结果上来讲，是非固定顺序输出的，也就是每次都不一样（标题也讲了）。但这是为什么呢？\n\n首先**建议你先自己想想原因**。其次我在面试时听过一些说法。有人说因为是哈希的所以就是无（乱）序等等说法。当时我是有点 ？？？\n\n这也是这篇文章出现的原因，希望大家可以一起研讨一下，理清这个问题 ：）\n\n## 看一下汇编\n\n```\n    ...\n\t0x009b 00155 (main.go:11)\tLEAQ\ttype.map[int32]string(SB), AX\n\t0x00a2 00162 (main.go:11)\tPCDATA\t$2, $0\n\t0x00a2 00162 (main.go:11)\tMOVQ\tAX, (SP)\n\t0x00a6 00166 (main.go:11)\tPCDATA\t$2, $2\n\t0x00a6 00166 (main.go:11)\tLEAQ\t\"\"..autotmp_3+24(SP), AX\n\t0x00ab 00171 (main.go:11)\tPCDATA\t$2, $0\n\t0x00ab 00171 (main.go:11)\tMOVQ\tAX, 8(SP)\n\t0x00b0 00176 (main.go:11)\tPCDATA\t$2, $2\n\t0x00b0 00176 (main.go:11)\tLEAQ\t\"\"..autotmp_2+72(SP), AX\n\t0x00b5 00181 (main.go:11)\tPCDATA\t$2, $0\n\t0x00b5 00181 (main.go:11)\tMOVQ\tAX, 16(SP)\n\t0x00ba 00186 (main.go:11)\tCALL\truntime.mapiterinit(SB)\n\t0x00bf 00191 (main.go:11)\tJMP\t207\n\t0x00c1 00193 (main.go:11)\tPCDATA\t$2, $2\n\t0x00c1 00193 (main.go:11)\tLEAQ\t\"\"..autotmp_2+72(SP), AX\n\t0x00c6 00198 (main.go:11)\tPCDATA\t$2, $0\n\t0x00c6 00198 (main.go:11)\tMOVQ\tAX, (SP)\n\t0x00ca 00202 (main.go:11)\tCALL\truntime.mapiternext(SB)\n\t0x00cf 00207 (main.go:11)\tCMPQ\t\"\"..autotmp_2+72(SP), $0\n\t0x00d5 00213 (main.go:11)\tJNE\t193\n\t...\n```\n\n我们大致看一下整体过程，重点处理 Go map 循环迭代的是两个 runtime 方法，如下：\n\n- runtime.mapiterinit\n- runtime.mapiternext\n\n但你可能会想，明明用的是 `for range` 进行循环迭代，怎么出现了这两个函数，怎么回事？\n\n## 看一下转换后\n\n```go\nvar hiter map_iteration_struct\nfor mapiterinit(type, range, &hiter); hiter.key != nil; mapiternext(&hiter) {\n    index_temp = *hiter.key\n    value_temp = *hiter.val\n    index = index_temp\n    value = value_temp\n    original body\n}\n```\n\n实际上编译器对于 slice 和 map 的循环迭代有不同的实现方式，并不是 `for` 一扔就完事了，还做了一些附加动作进行处理。而上述代码就是 `for range map` 在编译器展开后的伪实现\n\n## 看一下源码\n\n### runtime.mapiterinit\n\n```go\nfunc mapiterinit(t *maptype, h *hmap, it *hiter) {\n\t...\n\tit.t = t\n\tit.h = h\n\tit.B = h.B\n\tit.buckets = h.buckets\n\tif t.bucket.kind&kindNoPointers != 0 {\n\t\th.createOverflow()\n\t\tit.overflow = h.extra.overflow\n\t\tit.oldoverflow = h.extra.oldoverflow\n\t}\n\n\tr := uintptr(fastrand())\n\tif h.B > 31-bucketCntBits {\n\t\tr += uintptr(fastrand()) << 31\n\t}\n\tit.startBucket = r & bucketMask(h.B)\n\tit.offset = uint8(r >> h.B & (bucketCnt - 1))\n\tit.bucket = it.startBucket\n    ...\n\n\tmapiternext(it)\n}\n```\n\n通过对 `mapiterinit` 方法阅读，可得知其主要用途是在 map 进行遍历迭代时**进行初始化动作**。共有三个形参，用于读取当前哈希表的类型信息、当前哈希表的存储信息和当前遍历迭代的数据\n\n#### 为什么\n\n咱们关注到源码中 `fastrand` 的部分，这个方法名，是不是迷之眼熟。没错，它是一个生成随机数的方法。再看看上下文：\n\n```go\n...\n// decide where to start\nr := uintptr(fastrand())\nif h.B > 31-bucketCntBits {\n\tr += uintptr(fastrand()) << 31\n}\nit.startBucket = r & bucketMask(h.B)\nit.offset = uint8(r >> h.B & (bucketCnt - 1))\n\n// iterator state\nit.bucket = it.startBucket\n```\n\n在这段代码中，它生成了随机数。用于决定从哪里开始循环迭代。更具体的话就是根据随机数，选择一个桶位置作为起始点进行遍历迭代\n\n因此每次重新 `for range map`，你见到的结果都是不一样的。那是因为它的起始位置根本就不固定！\n\n### runtime.mapiternext\n\n```go\nfunc mapiternext(it *hiter) {\n    ...\n    for ; i < bucketCnt; i++ {\n\t\t...\n\t\tk := add(unsafe.Pointer(b), dataOffset+uintptr(offi)*uintptr(t.keysize))\n\t\tv := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+uintptr(offi)*uintptr(t.valuesize))\n\t\t...\n\t\tif (b.tophash[offi] != evacuatedX && b.tophash[offi] != evacuatedY) ||\n\t\t\t!(t.reflexivekey || alg.equal(k, k)) {\n\t\t\t...\n\t\t\tit.key = k\n\t\t\tit.value = v\n\t\t} else {\n\t\t\trk, rv := mapaccessK(t, h, k)\n\t\t\tif rk == nil {\n\t\t\t\tcontinue // key has been deleted\n\t\t\t}\n\t\t\tit.key = rk\n\t\t\tit.value = rv\n\t\t}\n\t\tit.bucket = bucket\n\t\tif it.bptr != b {\n\t\t\tit.bptr = b\n\t\t}\n\t\tit.i = i + 1\n\t\tit.checkBucket = checkBucket\n\t\treturn\n\t}\n\tb = b.overflow(t)\n\ti = 0\n\tgoto next\n}\n```\n\n在上小节中，咱们已经选定了起始桶的位置。接下来就是通过 `mapiternext` 进行**具体的循环遍历动作**。该方法主要涉及如下：\n\n- 从已选定的桶中开始进行遍历，寻找桶中的下一个元素进行处理\n- 如果桶已经遍历完，则对溢出桶 `overflow buckets` 进行遍历处理\n\n通过对本方法的阅读，可得知其对 buckets 的**遍历规则**以及对于扩容的一些处理（这不是本文重点。因此没有具体展开）\n\n## 总结\n\n在本文开始，咱们先提出核心讨论点：“为什么 Go map 遍历输出是不固定顺序？”。而通过这一番分析，原因也很简单明了。就是 `for range map` 在开始处理循环逻辑的时候，就做了随机播种...\n\n你想问为什么要这么做？当然是官方有意为之，因为 Go 在早期（1.0）的时候，虽是稳定迭代的，但从结果来讲，其实是无法保证每个 Go 版本迭代遍历规则都是一样的。而这将会导致可移植性问题。因此，改之。也请不要依赖...\n\n## 参考\n\n- [Go maps in action](https://blog.golang.org/go-maps-in-action)\n"
  },
  {
    "path": "content/posts/go/map-65.md",
    "content": "---\ntitle: \"面试官：为什么 Go 的负载因子是 6.5？\"\ndate: 2021-12-31T12:55:07+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好，我是煎鱼。\n\n最近我有一个朋友，在网上看到一个有趣的段子，引发了我一些兴趣。\n\n如下图：\n\n![](https://files.mdnice.com/user/3610/e81547d8-8de7-4310-a62f-e59ce4c0def2.png)\n\n听说是在最后的闲聊、吹水、聊人生、乱扯环节了，不是在技术环节了，所以大家也不用太在意什么技术评估法则（别杠）。\n\n煎鱼作为一名技术号主，看到这里的 6.5，就想给大家挖一挖，这到底是何物，和大家一同学习和增长知识！\n\n## 6.5 是什么\n\n实际上在 Go 语言中，就存在 6.5 这一概念，与 map 存在直接关系，因此我们需要先了解 map 的基本数据结构，再介绍 6.5 的背景和由来。\n\n开始学习 6.5 吧！\n\n### 了解 map 底层\n\n我以前在写《[深入理解 Go map：初始化和访问元素](https://eddycjy.com/posts/go/map/2019-03-05-map-access/)》时有介绍过 map 的基础数据结构。\n\n基本结构如下图：\n\n![map 基本数据结构](https://files.mdnice.com/user/3610/8679a84f-0d21-485d-b55f-dc9d70d5ddd1.png)\n\n其中重要的一个基本单位是 hmap：\n\n```golang\ntype hmap struct {\n\tcount     int\n\tflags     uint8\n\tB         uint8\n\tnoverflow uint16\n\thash0     uint32\n\tbuckets    unsafe.Pointer\n\toldbuckets unsafe.Pointer\n\tnevacuate  uintptr\n\textra *mapextra\n}\n\ntype mapextra struct {\n\toverflow    *[]*bmap\n\toldoverflow *[]*bmap\n\tnextOverflow *bmap\n}\n```\n\n- count：map 的大小，也就是 len() 的值，代指 map 中的键值对个数。\n- flags：状态标识，主要是 goroutine 写入和扩容机制的相关状态控制。并发读写的判断条件之一就是该值。\n- B：**桶，最大可容纳的元素数量，值为 负载因子（默认 6.5） * 2 ^ B，是 2 的指数**。\n- noverflow：溢出桶的数量。\n- hash0：哈希因子。\n- buckets：保存当前桶数据的指针地址（指向一段连续的内存地址，主要存储键值对数据）。\n- oldbuckets，保存旧桶的指针地址。\n- nevacuate：迁移进度。\n- extra：原有 buckets 满载后，会发生扩容动作，在 Go 的机制中使用了增量扩容，如下为细项：\n    - overflow 为 hmap.buckets （当前）溢出桶的指针地址。\n    - oldoverflow 为 hmap.oldbuckets （旧）溢出桶的指针地址。\n    - nextOverflow 为空闲溢出桶的指针地址。\n\n我们关注到 hmap 的 B 字段，其值就是 6.5，他就是我们在苦苦寻找的 6.5，但他又是什么呢？\n\n### 什么是负载因子\n\nB 值，这里就涉及到一个概念：**负载因子（load factor），用于衡量当前哈希表中空间占用率的核心指标**，也就是每个 bucket 桶存储的平均元素个数。\n\n另外负载因子**与扩容、迁移**等重新散列（rehash）行为有直接关系：\n- 在程序运行时，会不断地进行插入、删除等，会导致 bucket 不均，内存利用率低，需要迁移。\n- 在程序运行时，出现负载因子过大，需要做扩容，解决 bucket 过大的问题。\n\n负载因子是哈希表中的一个重要指标，在各种版本的哈希表实现中都有类似的东西，主要目的是**为了平衡 buckets 的存储空间大小和查找元素时的性能高低**。\n\n在接触各种哈希表时都可以关注一下，做不同的对比，看看各家的考量。\n\n## 为什么是 6.5\n\n了解是什么后，我们进一步深挖。\n\n为什么 Go 语言中哈希表的负载因子是 6.5，为什么不是 8 ，也不是 1。这里面有可靠的数据支撑吗？\n\n### 测试报告\n\n实际上这是 Go 官方的经过认真的测试得出的数字，一起来看看官方的这份测试报告。\n\n报告中共包含 4 个关键指标，如下：\n\n| loadFactor | %overflow  |  bytes/entry | hitprobe |    missprobe |\n| --- | --- | --- | --- | --- |\n|  4.00   |  2.13   |  20.77   |  3.00   |  4.00   |\n|  4.50   |  4.05  |  17.30   |  3.25   |  4.50   |\n|  5.00   |  6.85   | 14.77    | 3.50    |  5.00   |\n|  5.50   |  10.55    | 12.94    | 3.75    | 5.50    |\n|  6.00   |  15.27   | 11.67    | 4.00    | 6.00    |\n|  6.50   |  20.90   | 10.79    | 4.25    | 6.50    |\n|  7.00   |  27.14   | 10.15    | 4.50    | 7.00    |\n|  7.50   |  34.03   | 9.73    | 4.75    | 7.50    |\n|  8.00   |  41.10   | 9.40    |  5.00   | 8.00    |\n\n- loadFactor：负载因子，也有叫装载因子。\n- %overflow：溢出率，有溢出 bukcet 的百分比。\n- bytes/entry：每对 key/elem 的开销字节数.\n- hitprobe：查找一个存在的 key 时，要查找的平均个数。\n- missprobe：查找一个不存在的 key 时，要查找的平均个数。\n\n### 选择数值\n\n结合测试报告一看，好家伙，不测不知道，一测吓一跳，有依据了。\n\nGo 官方发现：**负载因子太大了，会有很多溢出的桶。太小了，就会浪费很多空间**（too large and we have lots of overflow buckets, too small and we waste a lot of space）。\n\n![来自 Go 官方源码说明](https://files.mdnice.com/user/3610/93b87f8d-a0ad-4503-b4f9-e3cf040468df.png)\n\n根据这份测试结果和讨论，Go 官方把 Go 中的 map 的负载因子硬编码为 6.5，这就是 6.5 的选择缘由。\n\n这意味着在 Go 语言中，**当 B（bucket）平均每个存储的元素大于或等于 6.5 时，就会触发扩容行为**，这是作为我们用户对这个数值最近的接触。\n\n## 总结\n\n在今天这篇文章中，我们先快速了解了 Go 语言中 map 的基本数据结构和设计，这和我们要解释的问题紧密相关。\n\n紧接着针对开头所提出的 6.5，进行了介绍和说明，这其实是 map 中的负载因子。其数值的确定来源于 Go 官方的测试。\n\n为什么是 6.5，你懂了吗？\n\n## 参考\n- [src/runtime/map.go](https://golang.org/src/runtime/map.go)\n- [深度解析golang map](https://juejin.cn/post/6954707500151078919)\n- [golang中map底层B值的计算逻辑](https://zhuanlan.zhihu.com/p/366472077)"
  },
  {
    "path": "content/posts/go/map-con.md",
    "content": "---\ntitle: \"Go 为什么不在语言层面支持 map 并发？\"\ndate: 2022-02-05T15:55:22+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n很多小伙伴学习 Go 语言的语法时，可能只是轻轻地看到过这个问题，结果一旦上手，多多少少一个组内总会碰到过几次。\n\n甚至会发现有一定年限的程序员也会遇到。有小伙伴疑惑了，这么折腾，为什么 Go 不直接在语言层面就支持 map 并发，那得有多香？\n\n## 为什么原生不支持\n\n凭什么 Go 官方还不支持，难不成太复杂了，性能太差了，到底是为什么？\n\n官方答复原因如下（via @go faq）：\n\n- 典型使用场景：map 的典型使用场景是不需要从多个 goroutine 中进行安全访问。\n- 非典型场景（需要原子操作）：map 可能是一些更大的数据结构或已经同步的计算的一部分。\n- 性能场景考虑：若是只是为少数程序增加安全性，导致 map 所有的操作都要处理 mutex，将会降低大多数程序的性能。\n\n核心来讲就是：Go 团队在经过了长时间的讨论后，认为原生 map 更应适配典型使用场景。\n\n如果为了小部分情况，将**会导致大部分程序付出性能代价**，决定了不支持原生的并发 map 读写。且在 Go1.6 起，**增加了检测机制**，并发的话会导致异常。\n\n## 为什么要崩溃\n\n前面有提到一点，在 Go1.6 起会进行原生 map 的并发检测，这是一些人的 “噩梦”。\n\n在此有人吐槽到：“明明给我抛个错就好了，凭什么要让我的 Go 进程直接崩溃掉，分分钟给我背个 P0”。\n\n### 场景枚举\n\n这里我们假设一下，如果并发读写 map 是以下两种场景：\n\n1. 产生 panic：程序 panic -> 默认走进 recover -> 没有对并发 map 进行处理 -> map 存在脏数据 -> 程序使用脏数据 -> 产生**未知((影响。\n2. 产生 crash：程序 crash -> 直接崩溃 -> 保全数据（数据正常）-> 产生**明确((风险。\n\n你会选择哪一种方案呢？Go 官方在两者的风险衡量中选择了第二种。\n\n无论是编程，还是人生。如何在随机性中掌握确定性的部分，也是一门极大的哲学了。\n\n### let it crash\n\nGo 官方团队选择的方式是业内经典的 “let it crash” 行为，很多编程语言中，都会将其奉行为设计哲学。\n\n**let it crash 是指工程师不必过分担心未知的错误，而去进行面面俱到的防御性编码**。\n\n这块理念最经典的就是 erlang 了。\n\n## 总结\n\n在今天这篇文章中，我们介绍了 Go 语言为什么不支持原生支持 map 并发，核心原因是大部分场景都不需要，从性能考虑上做的考虑。\n\n直接让并发读写 map 的原因，是从 “let it crash” 去考虑。这块如果你想在自己的工程中避免这个情况，可以在 linter 等工具链加入竞态检测（-race），也可以避免这类风险。\n\n你觉得 Go 这块的设计考虑怎么样呢？欢迎在评论区留言和交流：）"
  },
  {
    "path": "content/posts/go/map-reset.md",
    "content": "---\ntitle: \"Go map 如何缩容？\"\ndate: 2021-12-31T12:55:07+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好，我是煎鱼。\n\n前几天看到 Go 圈子的著名股神（不是我...），在归类中简单的提到了 Go 语言中 map 的缩容的描述，这让我对其产生了兴趣，想要来一探究竟。\n\n我们常常喊扩缩容，扩缩容，但社区里都是清一色分析扩容机制，Go 面试官也都是卷 Go 语言 map 的扩容机制...\n\n在 **Go 语言中的 map 缩容机制是怎么做的**呢，今天就由煎鱼带大家一起研讨围观一轮。\n\n## 基本分析\n\n在 Go 底层源码 src/runtime/map.go 中，扩缩容的处理方法是 grow 为前缀的方法来处理的。\n\n其中扩缩容涉及到的是插入元素的操作，对应 mapassign 方法：\n\n```golang\nfunc mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {\n  ...\n\tif !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {\n\t\thashGrow(t, h)\n\t\tgoto again\n\t}\n  ...\n}\n\nfunc (h *hmap) growing() bool {\n\treturn h.oldbuckets != nil\n}\n\nfunc overLoadFactor(count int, B uint8) bool {\n\treturn count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)\n}\n\nfunc tooManyOverflowBuckets(noverflow uint16, B uint8) bool {\n\tif B > 15 {\n\t\tB = 15\n\t}\n  \n\treturn noverflow >= uint16(1)<<(B&15)\n}\n```\n\n核心看到针对扩缩容的判断逻辑：\n- 当前没有在扩容：条件为 oldbuckets 不为 nil。\n- 是否可以进行扩容：条件为 `hmap.count`> hash 桶数量 `(2^B)*6.5`。其中 `hmap.count` 指的是map 的数据数目， `2^B` 仅指 hash 数组的大小，不包含溢出桶。\n- 是否可以进行缩容：条件为溢出桶（noverflow）的数量 >= 32768（1<<15）。\n\n而我们可以关注到，无论是扩容还是缩容，其都是由 `hashGrow` 方法进行处理：\n\n```golang\nfunc hashGrow(t *maptype, h *hmap) {\n\tbigger := uint8(1)\n\tif !overLoadFactor(h.count+1, h.B) {\n\t\tbigger = 0\n\t\th.flags |= sameSizeGrow\n\t}\n  ...\n}\n```\n\n若是扩容，则 bigger 为 1，也就是 B+1。代表 hash 表容量扩大 1 倍。不满足就是缩容，也就是 hash 表容量不变。\n\n可以得出结论：map 的**扩缩容的主要区别在于 hmap.B 的容量大小改变**。而缩容由于 hmap.B 压根没变，内存空间的占用也是没有变化的。\n\n## 带来的隐患\n\n这种方式其实是存在运行隐患的，也就是**导致在删除元素时，并不会释放内存，使得分配的总内存不断增加**。如果一个不小心，拿 map 来做大 key/value 的存储，也不注意管理，很容易就内存爆了。\n\n也就是 Go 语言的 map 目前实现的是 ”伪缩容“，仅针对溢出桶过多的情况。若是触发缩容，hash 数组的占用的内存大小不变。\n\n若要实现 ”真缩容“，Go Contributor @josharian 表示目前**唯一可用的解决方法是：创建一个新的 map 并从旧的 map 中复制元素**。\n\n示例如下：\n\n```golang\nold := make(map[int]int, 9999999)\nnew := make(map[int]int, len(old))\nfor k, v := range old {\n    new[k] = v\n}\nold = new\n...\n```\n\n无比的像，复制粘贴，大的往小的挪动，再删掉大的就可以了。\n\n果然程序员的解决方案都是相似的。\n\n## 为什么不支持\n\n下述内容会主要基于如下两个 issues 和 proposal 来分析：\n1. 《[runtime: shrink map as elements are deleted](https://github.com/golang/go/issues/20135 \"runtime: shrink map as elements are deleted\")》\n2. 《[proposal: runtime: add way to clear and reuse a map's working storage](https://github.com/golang/go/issues/45328 \"proposal: runtime: add way to clear and reuse a map's working storage\")》\n\n目前 map 的缩容处理起来比较棘手，最早的 issues 是 2016 年提出的，也有人提过一些提案，但都因为种种原因被拒绝了。\n\n简单来讲，就是没有找到一个很好的方法实现，存在明确的实现成本问题，没法很方便的 ”告诉“ Go 运行时，我要：\n1. 记得保留存储空间，我要立即重用 map。\n2. 赶紧释放存储空间，map 从现在开始会小很多。\n\n抽象来看症结是：**需要保证增长结果在下一个开始之前完成**，此处的增长指的是 ”从小到大，从一个大小到相同大小，从大到小“ 的复杂过程。\n\n这属于一个多重 case，从而导致也就一直拖着，慢慢想。\n\n## 总结\n\nGo 语言中 map 的扩容机制是大家经常思考和学习的，但是缩容方面现在也是一个大 ”坑“。虽然不误用就没问题。\n\n但是一旦新同学来了，不知道，一塞，就会出问题。我有一个朋友，之前面试时，就听闻有人会塞几个 GB 的数据进 map，可想而知还是很危险的。\n\n现有 map 的缩容机制，是存在短板的，背后有着比较多重的纠结，不知道你**有没有什么好的建议或想法呢，欢迎大家一起来讨论**！"
  },
  {
    "path": "content/posts/go/map-slice-concurrency.md",
    "content": "---\ntitle: \"为什么 Go map 和 slice 是非线程安全的？\"\ndate: 2021-12-31T12:54:49+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好，我是煎鱼。\n\n初入 Go 语言的大门，有不少的小伙伴会快速的 3 天精通 Go，5 天上手项目，14 天上线业务迭代，21 天排查、定位问题，顺带捎个反省报告。\n\n其中最常见的初级错误，Go 面试较最爱问的问题之一：\n\n![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8d1a0506617a4729abe35927d87508d0~tplv-k3u1fbpfcp-zoom-1.image)\n（来自读者提问）\n\n为什么在 Go 语言里，map 和 slice 不支持并发读写，也就是是非线程安全的，为什么不支持？\n\n见招拆招后，紧接着就会开始讨论如何让他们俩 ”冤家“ 支持并发读写？\n\n今天我们这篇文章就来理一理，了解其前因后果，一起吸鱼学懂 Go 语言。\n\n非线程安全的例子\n--------\n\n### slice\n\n我们使用多个 goroutine 对类型为 slice 的变量进行操作，看看结果会变的怎么样。\n\n如下：\n\n```\nfunc main() {\n var s []string\n for i := 0; i < 9999; i++ {\n  go func() {\n   s = append(s, \"脑子进煎鱼了\")\n  }()\n }\n\n fmt.Printf(\"进了 %d 只煎鱼\", len(s))\n}\n\n```\n\n输出结果：\n\n```\n// 第一次执行\n进了 5790 只煎鱼\n// 第二次执行\n进了 7370 只煎鱼\n// 第三次执行\n进了 6792 只煎鱼\n\n```\n\n你会发现无论你执行多少次，每次输出的值大概率都不会一样。也就是追加进 slice 的值，出现了覆盖的情况。\n\n因此在循环中所追加的数量，与最终的值并不相等。且这种情况，是不会报错的，是一个出现率不算高的隐式问题。\n\n这个产生的主要原因是程序逻辑本身就有问题，同时读取到相同索引位，自然也就会产生覆盖的写入了。\n\n### map\n\n同样针对 map 也如法炮制一下。重复针对类型为 map 的变量进行写入。\n\n如下：\n\n```\nfunc main() {\n s := make(map[string]string)\n for i := 0; i < 99; i++ {\n  go func() {\n   s[\"煎鱼\"] = \"吸鱼\"\n  }()\n }\n\n fmt.Printf(\"进了 %d 只煎鱼\", len(s))\n}\n\n```\n\n输出结果：\n\n```\nfatal error: concurrent map writes\n\ngoroutine 18 [running]:\nruntime.throw(0x10cb861, 0x15)\n        /usr/local/Cellar/go/1.16.2/libexec/src/runtime/panic.go:1117 +0x72 fp=0xc00002e738 sp=0xc00002e708 pc=0x1032472\nruntime.mapassign_faststr(0x10b3360, 0xc0000a2180, 0x10c91da, 0x6, 0x0)\n        /usr/local/Cellar/go/1.16.2/libexec/src/runtime/map_faststr.go:211 +0x3f1 fp=0xc00002e7a0 sp=0xc00002e738 pc=0x1011a71\nmain.main.func1(0xc0000a2180)\n        /Users/eddycjy/go-application/awesomeProject/main.go:9 +0x4c fp=0xc00002e7d8 sp=0xc00002e7a0 pc=0x10a474c\nruntime.goexit()\n        /usr/local/Cellar/go/1.16.2/libexec/src/runtime/asm_amd64.s:1371 +0x1 fp=0xc00002e7e0 sp=0xc00002e7d8 pc=0x1063fe1\ncreated by main.main\n        /Users/eddycjy/go-application/awesomeProject/main.go:8 +0x55\n\n```\n\n好家伙，程序运行会直接报错。并且是 Go 源码调用 `throw` 方法所导致的致命错误，也就是说 Go 进程会中断。\n\n不得不说，这个并发写 map 导致的 `fatal error: concurrent map writes` 错误提示。我有一个朋友，已经看过少说几十次了，不同组，不同人...\n\n是个日经的隐式问题。\n\n如何支持并发读写\n--------\n\n### 对 map 上锁\n\n实际上我们仍然存在并发读写 map 的诉求（程序逻辑决定），因为 Go 语言中的 goroutine 实在是太方便了。\n\n像是一般写爬虫任务时，基本会用到多个 goroutine，获取到数据后再写入到 map 或者 slice 中去。\n\nGo 官方在 Go maps in action 中提供了一种简单又便利的方式来实现：\n\n```\nvar counter = struct{\n    sync.RWMutex\n    m map[string]int\n}{m: make(map[string]int)}\n\n```\n\n这条语句声明了一个变量，它是一个匿名结构（struct）体，包含一个原生和一个嵌入读写锁 `sync.RWMutex`。\n\n要想从变量中中读出数据，则调用读锁：\n\n```\ncounter.RLock()\nn := counter.m[\"煎鱼\"]\ncounter.RUnlock()\nfmt.Println(\"煎鱼:\", n)\n\n```\n\n要往变量中写数据，则调用写锁：\n\n```\ncounter.Lock()\ncounter.m[\"煎鱼\"]++\ncounter.Unlock()\n\n```\n\n这就是一个最常见的 Map 支持并发读写的方式了。\n\n### sync.Map\n\n#### 前言\n\n虽然有了 Map+Mutex 的极简方案，但是也仍然存在一定问题。那就是在 map 的数据量非常大时，只有一把锁（Mutex）就非常可怕了，一把锁会导致大量的争夺锁，导致各种冲突和性能低下。\n\n常见的解决方案是分片化，将一个大 map 分成多个区间，各区间使用多个锁，这样子锁的粒度就大大降低了。不过该方案实现起来很复杂，很容易出错。因此 Go 团队到比较为止暂无推荐，而是采取了其他方案。\n\n该方案就是在 Go1.9 起支持的 `sync.Map`，其支持并发读写 map，起到一个补充的作用。\n\n#### 具体介绍\n\nGo 语言的 `sync.Map` 支持并发读写 map，采取了 “空间换时间” 的机制，冗余了两个数据结构，分别是：read 和 dirty，减少加锁对性能的影响：\n\n```\ntype Map struct {\n mu Mutex\n read atomic.Value // readOnly\n dirty map[interface{}]*entry\n misses int\n}\n\n```\n\n其是专门为 `append-only` 场景设计的，也就是适合读多写少的场景。这是他的优点之一。\n\n若出现写多/并发多的场景，会导致 read map 缓存失效，需要加锁，冲突变多，性能急剧下降。这是他的重大缺点。\n\n提供了以下常用方法：\n\n```\nfunc (m *Map) Delete(key interface{})\nfunc (m *Map) Load(key interface{}) (value interface{}, ok bool)\nfunc (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool)\nfunc (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)\nfunc (m *Map) Range(f func(key, value interface{}) bool)\nfunc (m *Map) Store(key, value interface{})\n\n```\n\n*   Delete：删除某一个键的值。\n    \n*   Load：返回存储在 map 中的键的值，如果没有值，则返回 nil。ok 结果表示是否在 map 中找到了值。\n    \n*   LoadAndDelete：删除一个键的值，如果有的话返回之前的值。\n    \n*   LoadOrStore：如果存在的话，则返回键的现有值。否则，它存储并返回给定的值。如果值被加载，加载的结果为 true，如果被存储，则为 false。\n    \n*   Range：递归调用，对 map 中存在的每个键和值依次调用闭包函数 `f`。如果 `f` 返回 false 就停止迭代。\n    \n*   Store：存储并设置一个键的值。\n    \n\n实际运行例子如下：\n\n```\nvar m sync.Map\n\nfunc main() {\n //写入\n data := []string{\"煎鱼\", \"咸鱼\", \"烤鱼\", \"蒸鱼\"}\n for i := 0; i < 4; i++ {\n  go func(i int) {\n   m.Store(i, data[i])\n  }(i)\n }\n time.Sleep(time.Second)\n\n //读取\n v, ok := m.Load(0)\n fmt.Printf(\"Load: %v, %v\\n\", v, ok)\n\n //删除\n m.Delete(1)\n\n //读或写\n v, ok = m.LoadOrStore(1, \"吸鱼\")\n fmt.Printf(\"LoadOrStore: %v, %v\\n\", v, ok)\n\n //遍历\n m.Range(func(key, value interface{}) bool {\n  fmt.Printf(\"Range: %v, %v\\n\", key, value)\n  return true\n })\n}\n\n```\n\n输出结果：\n\n```\nLoad: 煎鱼, true\nLoadOrStore: 吸鱼, false\nRange: 0, 煎鱼\nRange: 1, 吸鱼\nRange: 3, 蒸鱼\nRange: 2, 烤鱼\n\n```\n\n为什么不支持\n------\n\nGo Slice 的话，主要还是索引位覆写问题，这个就不需要纠结了，势必是程序逻辑在编写上有明显缺陷，自行改之就好。\n\n但 Go map 就不大一样了，很多人以为是默认支持的，一个不小心就翻车，这么的常见。那凭什么 Go 官方还不支持，难不成太复杂了，性能太差了，到底是为什么？\n\n原因如下（via @go faq）：\n\n*   典型使用场景：map 的典型使用场景是不需要从多个 goroutine 中进行安全访问。\n    \n*   非典型场景（需要原子操作）：map 可能是一些更大的数据结构或已经同步的计算的一部分。\n    \n*   性能场景考虑：若是只是为少数程序增加安全性，导致 map 所有的操作都要处理 mutex，将会降低大多数程序的性能。\n    \n\n汇总来讲，就是 Go 官方在经过了长时间的讨论后，认为 Go map 更应适配典型使用场景，而不是为了小部分情况，导致大部分程序付出代价（性能），决定了不支持。\n\n\n## 鼓励\n\n若有任何疑问欢迎评论区反馈和交流，**最好的关系是互相成就**，各位的**点赞**就是[煎鱼](https://github.com/eddycjy)创作的最大动力，感谢支持。\n\n> 文章持续更新，可以微信搜【脑子进煎鱼了】阅读，本文 **GitHub** [github.com/eddycjy/blog](https://github.com/eddycjy/blog) 已收录，欢迎 Star 催更。\n\n\n总结\n--\n\n在今天这篇文章中，我们针对 Go 语言中的 map 和 slice 进行了基本的介绍，也对不支持并发读者的场景进行了模拟展示。\n\n同时也针对业内常见的支持并发读写的方式进行了讲述，最后分析了不支持的原因，让我们对整个前因后果有了一个完整的了解。\n\n不知道你**在日常是否有遇到过 Go 语言中非线性安全的问题呢，欢迎你在评论区留言和大家一起交流**！\n\n"
  },
  {
    "path": "content/posts/go/memory-model.md",
    "content": "---\ntitle: \"Go 内存模型：happens-before 原则\"\ndate: 2021-12-31T12:54:54+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n在日常工作中，如果我们能够了解 Go 语言内存模型，那会带来非常大的作用。这样在看一些极端情况，又或是变态面试题的时候，就能够明白程序运行表现下的很多根本原因了。\n\n当然，靠一篇普通文章讲完 Go 内存模型，不可能。因此今天这篇文章，把重点划在给大家**讲解 Go 语言的 happens-before 原则**这 1 个细节。\n\n开吸，和煎鱼揭开他的神秘面纱！\n\n内存模型定义是什么\n---------\n\n既然要了解 happens-before 原则，我们得先知道 The Go Memory Model（Go 内存模型）定义的是什么，官方解释如下：\n\n> The Go memory model specifies the conditions under which reads of a variable in one goroutine can be guaranteed to observe values produced by writes to the same variable in a different goroutine.\n\n在 Go 内存模型规定：“在一个 goroutine 中读取一个变量时，可以保证观察到不同 goroutine 中对同一变量的写入所产生的值” 的条件。\n\n这是学习后续知识的一个大前提。\n\nhappens-before 是什么\n------------------\n\nHappens Before 是一个专业术语，与 Go 语言没有直接关系，也就是并非是特有的。用大白话来讲，其定义是：\n\n>  在一个多线程程序中，假设存在 A 和 B 两个操作，如果 A 操作在 B 操作之前发生（A happens-before B），那么 A 操作对内存的影响将会对执行 B 的线程可见。\n\nA 不一定 happens-before B\n----------------------\n\n从 happens-before 定义来看，我们可以反过来想。那就是：\n\n>  在同一个（相同）线程中，如果都执行 A 和 B 操作，并且 A 的声明一定在 B 之前，那么 A 一定先于（happens-before）B 发生。\n\n以下述 Go 代码例子：\n\n```\nvar A int\nvar B int\n\nfunc main() {\n A = B + 1  (1)\n B = 1      (2)\n}\n```\n\n该代码是在同一个 main goroutine，全局变量 A 在变量 B 之前声明。\n\n在 main 函数中，代码行 (1)，也在代码行 (2) 之前。因此我们可以得出 (1) 一定会在 (2) 前执行，对吗？\n\n答案是：错误的，因为 A happens-before B 并不意味着 A 操作一定会在 B 操作之前发生。\n\n实际上在编译器中，上述代码在汇编的真正执行顺序如下：\n\n```\n 0x0000 00000 (main.go:7) MOVQ \"\".B(SB), AX\n 0x0007 00007 (main.go:7) INCQ AX\n 0x000a 00010 (main.go:7) MOVQ AX, \"\".A(SB)\n 0x0011 00017 (main.go:8) MOVQ $1, \"\".B(SB)\n```\n\n*   (2)：加载 B 到寄存器 AX。\n    \n*   (2)：进行 B = 1 赋值，在代码中执行为 INCQ 自增。\n    \n*   (1)：将寄存器 AX 中值加上 1 后赋值给 A。\n    \n\n通过上述分析，我们可以得知。在代码行 (1) 在 (2) 之前，但确实 (2) 比 (1) 更早执行。\n\n那么这是不是意味着违反了 happens-before 的设计原则，毕竟这可是同个线程里的操作，Go 编译器有 BUG？\n\n其实不然，因为对 A 的赋值实质上对 B 的赋值没有影响。所以并没有违反 happens-before 的设计原则。\n\nGo 语言中的 happens-before\n----------------------\n\n在 《The Go Memory Model》 中，给出了 Go 语言中 Happens Before 的明确语言定义。\n\n以下术语将会在介绍中用到：\n\n*   变量 v：一个指代性的变量，用于示例演示。\n    \n*   读 r：代表读操作。\n    \n*   写 w：代表写操作。\n    \n\n### 定义\n\n在满足如下两点条件下，允许对变量 v 的读 r 观察对 v 的写 w：\n\n1.  r 在 w 之前没有发生。\n    \n2.  没有其他写到 v 的 w' 发生在 w 之后但在 r 之前。\n    \n\n为了保证变量 v 的读 r 观察到对 v 的特定写 w，确保 w 是唯一允许 r 观察的写。\n\n因此如果以下两点都成立，就能保证 r 能观察到 w ：\n\n1.  w 发生在 r 之前。\n    \n2.  对共享变量 v 的任何其他写入都发生在 w 之前或 r 之后。\n    \n\n这看起来比较生涩，接下来我们以《The Go Memory Model》 中具体的 channel 例子来进行进一步说明，会更好理解一些。\n\nGo Channel 实例\n-------------\n\n在 Go 语言中提倡不要通过共享内存来进行通讯；相反，应当通过通讯来共享内存：\n\n> Do not communicate by sharing memory; instead, share memory by communicating.\n\n因此在 Go 工程中，Channel 是一个非常常用的语法。在原则上其需要遵守：\n\n1.  一个 channel 上的发送是在该 channel 的相应接收完成之前发生的。\n    \n2.  channel 的关闭发生在接收之前，因为通道被关闭而返回一个零值。\n    \n3.  一个无缓冲 channel 的接收发生在该 channel 的发送完成之前。\n    \n4.  一个容量为 C 的 channel 上，第 k 次接收发生在该 channel 的第 k+C 次发送完成之前。\n    \n\n接下来根据这四条原则，我们逐一给出例子，用于学习和理解。\n\n例子 1\n----\n\nGo channel 例子 1，你认为输出的结果是什么。如下：\n\n```\nvar c = make(chan int, 10)\nvar a string\n\nfunc f() {\n a = \"炸煎鱼\"   (1)\n c <- 0        (2)\n}\n\nfunc main() {\n go f()\n <-c           (3)\n print(a)      (4)\n}\n\n```\n\n答案是空字符串吗？\n\n程序最终结果是正常输出 “炸煎鱼” 的，原因如下：\n\n*   (1) happens-before (2) 。\n    \n*   (4) happens-after (3)。\n    \n\n当然，最后 (1) 写入变量 a 的操作，必然 happens-before 于 (4) print 方法，因此正确的输出了 “炸煎鱼”。\n\n能够满足 “一个 channel 上的发送是在该 channel 的相应接收完成之前发生的”。\n\n例子 2\n----\n\n主要是确保了关闭管道时的行为。只需要在前面的例子中，替换 `c <- 0` 成 `close(c)` 就能够产生具有相同的行为保证的程序。\n\n能够满足 “channel 的关闭发生在接收之前，因为通道被关闭而返回一个零值”。\n\n例子 3\n----\n\nGo channel 例子 3，你认为输出的结果是什么。如下：\n\n```\nvar c = make(chan int)\nvar a string\n\nfunc f() {\n a = \"煎鱼进脑子了\"    (1)\n <-c                 (2)\n}\n\nfunc main() {\n go f()\n c <- 0              (3)\n print(a)            (4)\n}\n\n```\n\n答案是空字符串吗？\n\n程序最终结果是正常输出 “煎鱼进脑子了” 的，原因如下：\n\n*   (2) happens-before (3)。\n    \n*   (1) happens-before (4)。\n    \n\n能够满足 “一个无缓冲 channel 的接收发生在该 channel 的发送完成之前”。\n\n如果我们把无缓冲改为 `make(chan int, 1)`，也就是带缓冲的 channel，则无法保证正常的输出 “煎鱼进脑子了”。\n\n例子 4\n----\n\nGo channel 例子 4，这个程序为工作列表中的每个条目启动一个 goroutine，但 goroutine 使用 channel 进行协调，以确保每次最多只有三个工作函数在运行。\n\n代码如下：\n\n```\nvar limit = make(chan int, 3)\n\nfunc main() {\n for _, w := range work {\n  go func(w func()) {\n   limit <- 1\n   w()\n   <-limit\n  }(w)\n }\n select{}\n}\n```\n\n能够满足 “一个容量为 C 的 channel 上，第 k 次接收发生在该 channel 的第 k+C 次发送完成之前”。\n\n总结\n--\n\n在本文中，我们针对 happens-before 原则进行了基本的说明。同时结合 Go 语言中实际的 happens-before 和 happens-after 的场景进了展示和讲解。\n\n实际上，在日常的开发工作中，happens-before 原则基本已经深入到潜意识中，就跟设计模式一样。会不知觉就应用到，但是若我们希望更进一步的对 Go 语言等内存模型就行研究和理解，就必须对这个基本理念有所认知。\n\n你平时有没有注意到这块的问题呢，欢迎大家留言和讨论！\n\n## 鼓励\n\n若有任何疑问欢迎评论区反馈和交流，**最好的关系是互相成就**，各位的**点赞**就是[煎鱼](https://github.com/eddycjy)创作的最大动力，感谢支持。\n\n> 文章持续更新，可以微信搜【脑子进煎鱼了】阅读，本文 **GitHub** [github.com/eddycjy/blog](https://github.com/eddycjy/blog) 已收录，学习 Go 语言可以看 [Go 学习地图和路线](https://github.com/eddycjy/go-developer-roadmap)，欢迎 Star 催更。\n\n参考\n--\n\n*   The Go Memory Model\n    \n*   Go内存模型&Happen-Before（一）\n    \n*   GoLang 内存模型\n    \n*   Golang happens before & channel\n    \n*   Go 内存模型\n"
  },
  {
    "path": "content/posts/go/news-slices-maps.md",
    "content": "---\ntitle: \"Go 提案：增加泛型版 slices 和 maps 新包\"\ndate: 2021-12-31T12:54:57+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n现在是 2021 年 8 月份了，根据 Go 语言发布周期的 2，8 原则。Go 1.17 即将发布，在写这篇文章时，现在已经进行到了 rc2：\n\n![](https://files.mdnice.com/user/3610/ced4caa3-98ac-4c8a-8077-c72e11f69b56.png)\n\n这意味着离 Go1.18 释出泛型的正式支持又近了一点点，社区中讨论泛型相关的周边功能的声音又多了起来。\n\n今天要讨论的泛型版功能支持也是如此，分别包含：map（#47330）、slice（#45955）、container/set（#47331） 三种通用类型的支持。\n\n我们主要展开 maps 和 slices，其余的都大同小异，理解核心思想就好。\n\n## maps\n\n该提案建议定义一个新的包 maps，它将提供可用于任何类型的 map 的函数：\n\n![](https://files.mdnice.com/user/3610/fc067e0f-0972-4f92-898e-d64404b5bfa4.png)\n\n下面的描述侧重于描述 API 的提供：\n\n```golang\npackage maps\n\nfunc Keys[K comparable, V any](m map[K]V) []K\n\nfunc Values[K comparable, V any](m map[K]V) []V\n\nfunc Equal[K, V comparable](m1, m2 map[K]V) bool\n\nfunc EqualFunc[K comparable, V1, V2 any](m1 map[K]V1, m2 map[K]V2, cmp func(V1, V2) bool) bool\n\nfunc Clear[K comparable, V any](m map[K]V)\n\nfunc Clone[K comparable, V any](m map[K]V) map[K]V\n\nfunc Add[K comparable, V any](dst, src map[K]V)\n\nfunc Filter[K comparable, V any](m map[K]V, keep func(K, V) bool)\n```\n- Keys：返回 map 的键值。这些键将是一个不确定的顺序。\n- Values：返回 map 值。这些值将以不确定的顺序出现。\n- Equal：匹配两个 map 是否包含相同的键/值对。\n- EqualFunc：EqualFunc 与 Equal 类似，但使用 cmp 进行数值比较。\n- Clear：从 map 中清除所有条目，使其为空。\n- Clone：返回一个 map 的副本，这是一个浅层克隆。新的键和值使用普通的赋值来设置。\n- Add：将 src 中的所有键/值对添加到 dst 中。当 src 中的一个键已经存在于 dst 中时，dst 中的值将被 src 中的键相关的值覆盖。\n- Filter：过滤器从 map 中删除任何 keep 返回结果为 false 的键/值对。\n\n## slice\n\n该提案建议定义一个新的包 slices，它将提供可用于任何类型的 slice 的函数：\n\n![](https://files.mdnice.com/user/3610/8086fdea-63ee-4c73-aa4b-95b3c647d8e6.png)\n\n下面的描述侧重于描述 API 的提供：\n\n```golang\npackage slices\n\nimport \"constraint\"\n\nfunc Equal[T comparable](s1, s2 []T) bool\n\nfunc EqualFunc[T1, T2 any](s1 []T1, s2 []T2, eq func(T1, T2) bool) bool\n\nfunc Compare[T constraints.Ordered](s1, s2 []T) int\n\nfunc CompareFunc[T any](s1, s2 []T, cmp func(T, T) int) int\n\nfunc Index[T comparable](s []T, v T) int\n\nfunc IndexFunc[T any](s []T, f func(T) bool) int\n\nfunc Contains[T comparable](s []T, v T) bool\n\nfunc Insert[S constraints.Slice[T], T any](s S, i int, v ...T) S\n\nfunc Delete[S constraints.Slice[T], T any](s S, i, j int) S\n\nfunc Clone[S constraints.Slice[T], T any](s S) S\n```\n- Equal：检查两个切片是否相等，以长度和元素值来比较。\n- EqualFunc：检查两个切片是否相等，以所传入的匹配函数来比较。\n- Compare/CompareFunc：\n    - Compare 比较两个切片 s1 和 s2 的元素。\n    - CompareFunc 与 Compare 类似，在每一对元素上使用所传入的比较函数。\n- Index：\n    - Index 返回值 v 在切片 s 中第一次出现的索引，如果不存在，则返回-1。\n    - IndexFunc 返回满足f(c)的第一个元素在s中的索引，如果没有则返回-1。\n- Contains：检查值 v 是否存在于切片 s 中。\n\n插入、删除、克隆的 API 比较常见，这里我就不展开了。在通用类型的切片有一些比较特殊的 API：\n\n```golang\nfunc Compact[S constraints.Slice[T], T comparable](s S) S\n\nfunc CompactFunc[S constraints.Slice[T], T any](s S, cmp func(T, T) bool) S\n\nfunc Grow[S constraints.Slice[T], T any](s S, n int) S\n\nfunc Clip[S constraints.Slice[T], T any](s S) S\n```\n- Compact/CompactFunc：\n    - Compact 将连续运行的相等元素替换为单一的副本。这就像 Unix 中的 uniq 命令。会直接修改切片 s 的内容，不会重新创建一个。\n    - CompactFunc 与 Compact 方法类似，但是使用一个比较函数来匹配。\n- Grow：如果有必要，Grow 会增长切片的容量，以保证另外 n 个元素的空间。在 Grow(n) 之后，至少有 n 个元素可以被添加到分片中而不需要再分配。如果 n 是负数或者太大，无法分配内存，Grow 将陷入恐慌。\n- Clip：从切片中移除未使用的容量，返回s[:len(s):len(s)]。\n\n## 总结\n\n如果这些提议被接受，这几个新包将被包含在实现泛型后的第一个Go版本中（我们目前预计将是Go 1.18）。\n\n从 issues 的讨论来看，通用类型的新包支持很大概率会实现，主要争议在实现细节，例如：性能、命名、规范等。\n\n实现后值得期待，又是一次生产力的优化！"
  },
  {
    "path": "content/posts/go/news115.md",
    "content": "---\ntitle: \"分享 Go 最近的几件周边大小事\"\ndate: 2021-12-31T12:55:17+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n最近可能是因为 Q4 了，又恰逢 Go1.18 快要发布，各路 Go 语言的新消息层出不穷。\n\n今天煎鱼带大家一起来了解下最近社区发生的几件大小事，当然，我只讲一些核心的内容。\n\n![](https://files.mdnice.com/user/3610/d6f8bfde-141c-405b-8ec5-779d31b0283d.png)\n\n## Go 诞生 12 年\n\n在 2021 年 11 月 10 日，是 Go 语言开源版本的 12 岁生日。Go 官方博客发表《[Twelve Years of Go](https://go.dev/blog/12years \"Twelve Years of Go\")》。\n\n主体内容分为三块介绍：\n- 回顾过去一年的核心变更。\n- 展望明年的特性计划。\n- 介绍今年做的 Go 相关分享。\n\n### 回顾过去\n\n是对过去一年的版本更新进行说明：\n- Go1.16：默认启用 Go modules，增加 MacOS ARM64 的支持，新支持文件系统接口和嵌入文件的特性。\n- Go1.17：Go 函数改为基于寄存器的调用规范（提高了 5~15% 的性能），增加 Windows ARM64 支持，还引入了模块裁剪等功能。\n- Go1.18：预计支持模糊测试（Go fuzzing）、泛型等强大的新特性。\n\n核心总结：今年大力推动 Go modules，提高了 Go 函数的性能，增加了更多的计算机架构支持，以及若干改进和优化（例如：TLS）。\n\n也为了推广 Go 语言，做出了更多的努力和资料培训（Gin 也因此更上了一层楼）。\n\n### 后续安排\n\n我们核心关注泛型方面的消息，泛型将会是 2022 年的核心重点之一。规划如下：\n- Go 1.18 中的初始版本只是泛型的开始，将会在此版本使用泛型并学习哪些有效、哪些无效。\n- 在确定泛型的 “实践” 后，会输出 “最佳实践”，并决定何时追加泛型实现到标准库和第三方库中。\n- 期待在 **Go1.19**（也就是 2022.08）及更高版本将**进一步完善泛型**的设计和实现，并将它们进一步整合到整体的 Go 体验中（也就是工具链等）。\n\n核心总结：明年要继续大力推进泛型，先尝鲜，再出最佳实践，进而融合进 Go 体系中，路还比较远。\n\n## 分享知识\n\n今年 Go 团队为了推广 Go 语言的配套知识体系，还发布了一堆教程：\n- 《[guide to databases in Go](https://golang.org/doc/database/ \"guide to databases in Go\")》：Go 数据库指南。\n- 《[guide to developing modules](https://golang.org/doc/#developing-modules \"guide to developing modules\")》：Go 开发模块指南。\n- 《[用 Go 和 Gin 开发 RESTful API](https://golang.org/doc/tutorial/web-service-gin \"用 Go 和 Gin 开发 RESTful API\")》：用 Go 和 Gin 开发 RESTful API，有种把 Gin 作为推荐框架的感觉了。\n\n这两天 Go 团队在 Google Open Source Live 举办了第二次年度 Go 日：\n\n![来源于 youtube，还自带字幕翻译](https://files.mdnice.com/user/3610/ba469b39-4395-4f3c-9e82-37e4a1b8812e.png)  \n\n分享了如下话题：\n- 《[Using Generics in Go](https://www.youtube.com/watch?v=nr8EpUO9jhw \"Using Generics in Go\")》：介绍了泛型以及如何有效地使用它们。\n- 《[Modern Enterprise Applications](https://www.youtube.com/watch?v=5fgG1qZaV4w \"Modern Enterprise Applications\")》：展示了 Go 在企业现代化中所扮演的角色。\n- 《[Building Better Projects with the Go Editor](https://www.youtube.com/watch?v=jMyzsp2E_0U \"Building Better Projects with the Go Editor\")》：展示了VS Code Go的集成工具如何帮助你浏览代码、调试测试等。\n- 《[From Proof of Concept to Production](https://www.youtube.com/watch?v=e7PtBOsTpXE \"From Proof of Concept to Production\")》：介绍美国运通公司如何在其支付和奖励平台中使用 Go。\n\n有个别 Go 话题还是很不错的，尤其是泛型的快速了解。这在该视频网站上还有比较顺畅的字幕翻译，阅读基本没问题。\n\n可自行选择食用。\n\n## 泛型新语法和 Playground\n\n### GoTip Playground\n\n在之前我们是通过 `go2goplay.golang.org` 来进行一些在线泛型的例子玩耍。\n\n在经历了一定时间的迭代后，泛型的新特性多了不少，由新版的 Playground（gotipplay.golang.org）来替换使用：\n\n![https://gotipplay.golang.org](https://files.mdnice.com/user/3610/53d4f09e-689b-445f-862c-f18b91a359f8.png)\n\n主要特点：所运行的代码**基于 tip 版本**，再也不用自己拉代码了。在 select 控件上也多了不少例子的展示。\n\n后续大概率会增加泛型的实践。\n\n### 约束语法\n\n需要注意的一点，最新的 Go 泛型约束语法又又又变了。从原本的 “,” 改为了 \"|\"。\n\n![Type Parameters Proposal](https://files.mdnice.com/user/3610/5f1f20c6-0d81-43a1-a1c3-334213546fc7.png)\n\n原本如下：\n\n```golang\ntype T interface {\n type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, complex64, complex128, string\n}\n```\n\n新的如下：\n\n```golang\ntype T interface {\n\t~int | ~int8 | ~int16 | ~int32 | ~int64 |\n\t\t~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |\n\t\t~float32 | ~float64 |\n\t\t~string\n}\n```\n\n## 新搜索体验\n\n近期官方对文档站 `pkg.go.dev` 做了一轮搜索优化。主要分为如下：\n- 按包搜索。\n- 按符号搜索。\n\n若是按包搜索，将会对相关包的搜索结果进行分组，优化后不再是流水式展示，而是聚类，根据归类后分组展示。\n\n如下图：\n\n![按包搜索](https://files.mdnice.com/user/3610/5e12b6ee-995a-4dd4-af4d-8aec6f57f580.png)\n\n若是按符号搜索，将可以对包内的 “符号” 实现更精准的搜索，例如：常量、变量、函数、类型、字段或方法。\n\n如下图：\n\n![按符号搜索](https://files.mdnice.com/user/3610/52697a9c-00f1-46d8-9068-976d85dcb6d0.png)\n\n搜索效率是提高了不少的。以后会不会向完整的代码搜索和关联的方向发展，也是个值得思考的问题。\n\n## 总结\n\n今天给大家介绍了 Go 语言社区最近发生的好几件大小事，每一个展开都是一篇新的文章。\n例如：可以了解为什么泛型要从 “,” 改为 “|” 的格式，肯定是有原因的。\n\n这些内容（以及 Go1.18 的新特性），我会在后续的文章继续深入展开和介绍。\n\n欢迎大家关注我，获取一手的 Go 社区快讯和知识：）"
  },
  {
    "path": "content/posts/go/nil-func.md",
    "content": "---\ntitle: \"Go 读者提问：值为 nil 也能调用函数，太神奇了吧？\"\ndate: 2021-12-31T12:55:27+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n最近在我们 Go 的技术交流群里，有一个小伙伴提了一个程序方面的问题，还挺有意思的，分享给大家。\n\n![](https://files.mdnice.com/user/3610/0774f643-81a2-4458-b557-05c81a9572ef.png)\n\n## 示例\n\n示例程序如下：\n\n```golang\ntype T struct{}\n\nfunc (t *T) Hello() string {\n\tif t == nil {\n\t\tfmt.Println(\"脑子进煎鱼了\")\n\t\treturn \"\"\n\t}\n\n\treturn \"煎鱼进脑子了\"\n}\n\nfunc main() {\n\tvar t *T\n\tt.Hello()\n```\n\n这段程序的运行结果是什么？\n\n从程序的分析来看，变量 `t` 并没有初始化，只是声明了类型。然后就直接调用了 `Hello` 方法，像是 nil 调用函数，理论上应该出现恐慌（panic）。\n\n运行结果是：\n\n```\npanic: runtime error: invalid memory address or nil pointer dereference\n```\n\n对不对呢？\n\n显然，真正的运行结果是：\n\n```\n脑子进煎鱼了\n```\n\n请你思考一下，想想这是为什么？\n\n## 为什么\n\n问题的原因是：很多小伙伴认为变量 `t` 的值都是 nil 了，不应该还能调用到才对。\n\n更抽象化来讲，就是 ”程序是如何检查对象指针来寻找和调度所需函数“。\n\n实际上，在 Go 中，表达式 `Expression.Name` 的语法，所调用的函数完全由 `Expression` 的类型决定。\n\n其调用函数的指向不是由该表达式的特定运行时值来决定，包括我们前面所提到的 nil。\n\n具体如下：\n\n```golang\nfunc (p *Sometype) Somemethod (firstArg int) {}\n```\n\n本质上是：\n\n```golang\nfunc SometypeSomemethod(p *Sometype, firstArg int) {}\n```\n\n这么一看，其实大家应该都明白了。\n\n上述入参 `p *Sometype` 是有具体上下文类型的，自然而然也就能调用到相应的方法。如果是没有任何上下文类型的，例如：`nil.Somemethod` 方法来调用，那肯定就是无法运行的。\n\n与值是不是 nil，是什么，没有太多直接的影响。只要有预期类型的上下文就可以了。\n\n## 总结\n\n今天给大家分享了一个 Go 语言里面的一个小细节，平时可能很多人没注意到，毕竟 IDE 也会标黄，会避开这个问题点。\n\n在理解 Go 的设计和思考上，我们是需要清晰其背后的原因和逻辑的，也就是类型决定其调用，而不是值（容易误判）。\n\n你有没有遇到过其它的细节问题呢，欢迎交流：）"
  },
  {
    "path": "content/posts/go/panic/2019-05-21-panic-and-recover.md",
    "content": "---\n\ntitle:      \"深入理解 Go panic and recover\"\ndate:       2019-05-21 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - 源码分析\n---\n\n作为一个 gophper，我相信你对于 `panic` 和 `recover` 肯定不陌生，但是你有没有想过。当我们执行了这两条语句之后。底层到底发生了什么事呢？前几天和同事刚好聊到相关的话题，发现其实大家对这块理解还是比较模糊的。希望这篇文章能够从更深入的角度告诉你为什么，它到底做了什么事？\n\n## 思考\n\n### 一、为什么会中止运行\n\n```\nfunc main() {\n\tpanic(\"EDDYCJY.\")\n}\n```\n\n输出结果：\n\n```\n$ go run main.go\npanic: EDDYCJY.\n\ngoroutine 1 [running]:\nmain.main()\n\t/Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:4 +0x39\nexit status 2\n```\n\n请思考一下，为什么执行 `panic` 后会导致应用程序运行中止？（而不是单单说执行了 `panic` 所以就结束了这么含糊）\n\n### 二、为什么不会中止运行\n\n```\nfunc main() {\n\tdefer func() {\n\t\tif err := recover(); err != nil {\n\t\t\tlog.Printf(\"recover: %v\", err)\n\t\t}\n\t}()\n\n\tpanic(\"EDDYCJY.\")\n}\n```\n\n输出结果：\n\n```\n$ go run main.go \n2019/05/11 23:39:47 recover: EDDYCJY.\n```\n\n请思考一下，为什么加上 `defer` + `recover` 组合就可以保护应用程序？\n\n### 三、不设置 defer 行不\n\n上面问题二是 `defer` + `recover` 组合，那我去掉 `defer` 是不是也可以呢？如下：\n\n```\nfunc main() {\n\tif err := recover(); err != nil {\n\t\tlog.Printf(\"recover: %v\", err)\n\t}\n\n\tpanic(\"EDDYCJY.\")\n}\n```\n\n输出结果：\n\n```\n$ go run main.go\npanic: EDDYCJY.\n\ngoroutine 1 [running]:\nmain.main()\n\t/Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:10 +0xa1\nexit status 2\n```\n\n竟然不行，啊呀毕竟入门教程都写的 `defer` + `recover` 组合 “万能” 捕获。但是为什么呢。去掉 `defer` 后为什么就无法捕获了？\n\n请思考一下，为什么需要设置 `defer` 后 `recover` 才能起作用？\n\n同时你还需要仔细想想，我们设置 `defer` + `recover` 组合后就能无忧无虑了吗，各种 “乱” 写了吗？\n\n### 四、为什么起个 goroutine 就不行\n\n```\nfunc main() {\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif err := recover(); err != nil {\n\t\t\t\tlog.Printf(\"recover: %v\", err)\n\t\t\t}\n\t\t}()\n\t}()\n\n\tpanic(\"EDDYCJY.\")\n}\n```\n\n输出结果：\n\n```\n$ go run main.go \npanic: EDDYCJY.\n\ngoroutine 1 [running]:\nmain.main()\n\t/Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:14 +0x51\nexit status 2\n```\n\n请思考一下，为什么新起了一个 `Goroutine` 就无法捕获到异常了？到底发生了什么事...\n\n## 源码\n\n接下来我们将带着上述 4+1 个小思考题，开始对源码的剖析和分析，尝试从阅读源码中找到思考题的答案和更多为什么\n\n### 数据结构\n\n```\ntype _panic struct {\n\targp      unsafe.Pointer\n\targ       interface{} \n\tlink      *_panic \n\trecovered bool\n\taborted   bool \n}\n```\n\n在 `panic` 中是使用 `_panic` 作为其基础单元的，每执行一次 `panic` 语句，都会创建一个 `_panic`。它包含了一些基础的字段用于存储当前的 `panic` 调用情况，涉及的字段如下：\n\n- argp：指向 `defer` 延迟调用的参数的指针\n- arg：`panic` 的原因，也就是调用 `panic` 时传入的参数\n- link：指向上一个调用的 `_panic`\n- recovered：`panic` 是否已经被处理，也就是是否被 `recover`\n- aborted：`panic` 是否被中止\n\n另外通过查看 `link` 字段，可得知其是一个链表的数据结构，如下图：\n\n![image](http://wx3.sinaimg.cn/large/006fVPCvly1g2muc73jp1j30hc099q2x.jpg)\n\n### 恐慌 panic\n\n```\nfunc main() {\n\tpanic(\"EDDYCJY.\")\n}\n```\n\n输出结果：\n\n```\n$ go run main.go\npanic: EDDYCJY.\n\ngoroutine 1 [running]:\nmain.main()\n\t/Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:4 +0x39\nexit status 2\n```\n\n我们去反查一下 `panic` 处理具体逻辑的地方在哪，如下：\n\n```\n$ go tool compile -S main.go\n\"\".main STEXT size=66 args=0x0 locals=0x18\n\t0x0000 00000 (main.go:23)\tTEXT\t\"\".main(SB), ABIInternal, $24-0\n\t0x0000 00000 (main.go:23)\tMOVQ\t(TLS), CX\n\t0x0009 00009 (main.go:23)\tCMPQ\tSP, 16(CX)\n\t...\n\t0x002f 00047 (main.go:24)\tPCDATA\t$2, $0\n\t0x002f 00047 (main.go:24)\tMOVQ\tAX, 8(SP)\n\t0x0034 00052 (main.go:24)\tCALL\truntime.gopanic(SB)\n```\n\n显然汇编代码直指内部实现是 `runtime.gopanic`，我们一起来看看这个方法做了什么事，如下（省略了部分）：\n\n```\nfunc gopanic(e interface{}) {\n\tgp := getg()\n\t...\n\tvar p _panic\n\tp.arg = e\n\tp.link = gp._panic\n\tgp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))\n    \n\tfor {\n\t\td := gp._defer\n\t\tif d == nil {\n\t\t\tbreak\n\t\t}\n\n\t\t// defer...\n\t\t...\n\t\td._panic = (*_panic)(noescape(unsafe.Pointer(&p)))\n\n\t\tp.argp = unsafe.Pointer(getargp(0))\n\t\treflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))\n\t\tp.argp = nil\n\n\t\t// recover...\n\t\tif p.recovered {\n\t\t\t...\n\t\t\tmcall(recovery)\n\t\t\tthrow(\"recovery failed\") // mcall should not return\n\t\t}\n\t}\n\n\tpreprintpanics(gp._panic)\n\n\tfatalpanic(gp._panic) // should not return\n\t*(*int)(nil) = 0      // not reached\n}\n```\n\n- 获取指向当前 `Goroutine` 的指针\n- 初始化一个 `panic` 的基本单位 `_panic` 用作后续的操作\n- 获取当前 `Goroutine` 上挂载的 `_defer`（数据结构也是链表）\n- 若当前存在 `defer` 调用，则调用 `reflectcall` 方法去执行先前 `defer` 中延迟执行的代码，若在执行过程中需要运行 `recover` 将会调用 `gorecover` 方法\n- 结束前，使用 `preprintpanics` 方法打印出所涉及的 `panic` 消息\n- 最后调用 `fatalpanic` 中止应用程序，实际是执行 `exit(2)` 进行最终退出行为的\n\n通过对上述代码的执行分析，可得知 `panic` 方法实际上就是处理当前 `Goroutine(g)` 上所挂载的 `._panic` 链表（所以无法对其他 `Goroutine` 的异常事件响应），然后对其所属的 `defer` 链表和 `recover` 进行检测并处理，最后调用退出命令中止应用程序\n\n### 无法恢复的恐慌 fatalpanic\n\n```\nfunc fatalpanic(msgs *_panic) {\n\tpc := getcallerpc()\n\tsp := getcallersp()\n\tgp := getg()\n\tvar docrash bool\n\n\tsystemstack(func() {\n\t\tif startpanic_m() && msgs != nil {\n\t\t    ...\n\t\t\tprintpanics(msgs)\n\t\t}\n\n\t\tdocrash = dopanic_m(gp, pc, sp)\n\t})\n\n\tsystemstack(func() {\n\t\texit(2)\n\t})\n\n\t*(*int)(nil) = 0\n}\n```\n\n我们看到在异常处理的最后会执行该方法，似乎它承担了所有收尾工作。实际呢，它是在最后对程序执行 `exit` 指令来达到中止运行的作用，但在结束前它会通过 `printpanics` 递归输出所有的异常消息及参数。代码如下：\n\n```\nfunc printpanics(p *_panic) {\n\tif p.link != nil {\n\t\tprintpanics(p.link)\n\t\tprint(\"\\t\")\n\t}\n\tprint(\"panic: \")\n\tprintany(p.arg)\n\tif p.recovered {\n\t\tprint(\" [recovered]\")\n\t}\n\tprint(\"\\n\")\n}\n```\n\n所以不要以为所有的异常都能够被 `recover` 到，实际上像 `fatal error` 和 `runtime.throw` 都是无法被 `recover` 到的，甚至是 oom 也是直接中止程序的，也有反手就给你来个 `exit(2)` 教做人。因此在写代码时你应该要相对注意些，“恐慌” 是存在无法恢复的场景的\n\n### 恢复 recover\n\n```\nfunc main() {\n\tdefer func() {\n\t\tif err := recover(); err != nil {\n\t\t\tlog.Printf(\"recover: %v\", err)\n\t\t}\n\t}()\n\n\tpanic(\"EDDYCJY.\")\n}\n```\n\n输出结果：\n\n```\n$ go run main.go \n2019/05/11 23:39:47 recover: EDDYCJY.\n```\n\n和预期一致，成功捕获到了异常。但是 `recover` 是怎么恢复 `panic` 的呢？再看看汇编代码，如下：\n\n```\n$ go tool compile -S main.go\n\"\".main STEXT size=110 args=0x0 locals=0x18\n\t0x0000 00000 (main.go:5)\tTEXT\t\"\".main(SB), ABIInternal, $24-0\n\t...\n\t0x0024 00036 (main.go:6)\tLEAQ\t\"\".main.func1·f(SB), AX\n\t0x002b 00043 (main.go:6)\tPCDATA\t$2, $0\n\t0x002b 00043 (main.go:6)\tMOVQ\tAX, 8(SP)\n\t0x0030 00048 (main.go:6)\tCALL\truntime.deferproc(SB)\n\t...\n\t0x0050 00080 (main.go:12)\tCALL\truntime.gopanic(SB)\n\t0x0055 00085 (main.go:12)\tUNDEF\n\t0x0057 00087 (main.go:6)\tXCHGL\tAX, AX\n\t0x0058 00088 (main.go:6)\tCALL\truntime.deferreturn(SB)\n\t...\n\t0x0022 00034 (main.go:7)\tMOVQ\tAX, (SP)\n\t0x0026 00038 (main.go:7)\tCALL\truntime.gorecover(SB)\n\t0x002b 00043 (main.go:7)\tPCDATA\t$2, $1\n\t0x002b 00043 (main.go:7)\tMOVQ\t16(SP), AX\n\t0x0030 00048 (main.go:7)\tMOVQ\t8(SP), CX\n\t...\n\t0x0056 00086 (main.go:8)\tLEAQ\tgo.string.\"recover: %v\"(SB), AX\n\t...\n\t0x0086 00134 (main.go:8)\tCALL\tlog.Printf(SB)\n\t...\n```\n\n通过分析底层调用，可得知主要是如下几个方法：\n- runtime.deferproc\n- runtime.gopanic\n- runtime.deferreturn\n- runtime.gorecover\n\n在上小节中，我们讲述了简单的流程，`gopanic` 方法会调用当前 `Goroutine` 下的 `defer` 链表，若 `reflectcall` 执行中遇到 `recover` 就会调用 `gorecover` 进行处理，该方法代码如下：\n\n\n```\nfunc gorecover(argp uintptr) interface{} {\n\tgp := getg()\n\tp := gp._panic\n\tif p != nil && !p.recovered && argp == uintptr(p.argp) {\n\t\tp.recovered = true\n\t\treturn p.arg\n\t}\n\treturn nil\n}\n```\n\n这代码，看上去挺简单的，核心就是修改 `recovered` 字段。该字段是用于标识当前 `panic` 是否已经被 `recover` 处理。但是这和我们想象的并不一样啊，程序是怎么从 `panic` 流转回去的呢？是不是在核心方法里处理了呢？我们再看看 `gopanic` 的代码，如下：\n\n\n```\nfunc gopanic(e interface{}) {\n\t...\n\tfor {\n\t\t// defer...\n\t\t...\n\t\tpc := d.pc\n\t\tsp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy\n\t\tfreedefer(d)\n\t\t\n\t\t// recover...\n\t\tif p.recovered {\n\t\t\tatomic.Xadd(&runningPanicDefers, -1)\n\n\t\t\tgp._panic = p.link\n\t\t\tfor gp._panic != nil && gp._panic.aborted {\n\t\t\t\tgp._panic = gp._panic.link\n\t\t\t}\n\t\t\tif gp._panic == nil { \n\t\t\t\tgp.sig = 0\n\t\t\t}\n\n\t\t\tgp.sigcode0 = uintptr(sp)\n\t\t\tgp.sigcode1 = pc\n\t\t\tmcall(recovery)\n\t\t\tthrow(\"recovery failed\") \n\t\t}\n\t}\n    ...\n}\n```\n\n我们回到 `gopanic` 方法中再仔细看看，发现实际上是包含对 `recover` 流转的处理代码的。恢复流程如下：\n\n- 判断当前 `_panic` 中的 `recover` 是否已标注为处理\n- 从 `_panic` 链表中删除已标注中止的 `panic` 事件，也就是删除已经被恢复的 `panic` 事件\n- 将相关需要恢复的栈帧信息传递给 `recovery` 方法的 `gp` 参数（每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量）\n- 执行 `recovery` 进行恢复动作\n\n从流程来看，最核心的是 `recovery` 方法。它承担了异常流转控制的职责。代码如下：\n\n```\nfunc recovery(gp *g) {\n\tsp := gp.sigcode0\n\tpc := gp.sigcode1\n\n\tif sp != 0 && (sp < gp.stack.lo || gp.stack.hi < sp) {\n\t\tprint(\"recover: \", hex(sp), \" not in [\", hex(gp.stack.lo), \", \", hex(gp.stack.hi), \"]\\n\")\n\t\tthrow(\"bad recovery\")\n\t}\n\n\tgp.sched.sp = sp\n\tgp.sched.pc = pc\n\tgp.sched.lr = 0\n\tgp.sched.ret = 1\n\tgogo(&gp.sched)\n}\n```\n\n粗略一看，似乎就是很简单的设置了一些值？但实际上设置的是编译器中伪寄存器的值，常常被用于维护上下文等。在这里我们需要结合 `gopanic` 方法一同观察 `recovery` 方法。它所使用的栈指针 `sp` 和程序计数器 `pc` 是由当前 `defer` 在调用流程中的 `deferproc` 传递下来的，因此实际上最后是通过 `gogo` 方法跳回了 `deferproc` 方法。另外我们注意到：\n\n```\ngp.sched.ret = 1\n```\n\n在底层中程序将 `gp.sched.ret` 设置为了 1，也就是**没有实际调用** `deferproc` 方法，直接修改了其返回值。意味着默认它已经处理完成。直接转移到 `deferproc` 方法的下一条指令去。至此为止，异常状态的流转控制就已经结束了。接下来就是继续走 `defer` 的流程了\n\n\n为了验证这个想法，我们可以看一下核心的跳转方法 `gogo` ，代码如下：\n\n```\n// void gogo(Gobuf*)\n// restore state from Gobuf; longjmp\nTEXT runtime·gogo(SB),NOSPLIT,$8-4\n\tMOVW\tbuf+0(FP), R1\n\tMOVW\tgobuf_g(R1), R0\n\tBL\tsetg<>(SB)\n\n\tMOVW\tgobuf_sp(R1), R13\t// restore SP==R13\n\tMOVW\tgobuf_lr(R1), LR\n\tMOVW\tgobuf_ret(R1), R0\n\tMOVW\tgobuf_ctxt(R1), R7\n\tMOVW\t$0, R11\n\tMOVW\tR11, gobuf_sp(R1)\t// clear to help garbage collector\n\tMOVW\tR11, gobuf_ret(R1)\n\tMOVW\tR11, gobuf_lr(R1)\n\tMOVW\tR11, gobuf_ctxt(R1)\n\tMOVW\tgobuf_pc(R1), R11\n\tCMP\tR11, R11 // set condition codes for == test, needed by stack split\n\tB\t(R11)\n```\n\n通过查看代码可得知其主要作用是从 `Gobuf` 恢复状态。简单来讲就是将寄存器的值修改为对应 `Goroutine(g)` 的值，而在文中讲了很多次的 `Gobuf`，如下：\n\n```\ntype gobuf struct {\n\tsp   uintptr\n\tpc   uintptr\n\tg    guintptr\n\tctxt unsafe.Pointer\n\tret  sys.Uintreg\n\tlr   uintptr\n\tbp   uintptr\n}\n```\n\n讲道理，其实它存储的就是 `Goroutine` 切换上下文时所需要的一些东西\n\n## 拓展\n\n```\nconst(\n\tOPANIC       // panic(Left)\n\tORECOVER     // recover()\n\t...\n)\n...\nfunc walkexpr(n *Node, init *Nodes) *Node {\n    ...\n\tswitch n.Op {\n\tdefault:\n\t\tDump(\"walk\", n)\n\t\tFatalf(\"walkexpr: switch 1 unknown op %+S\", n)\n\n\tcase ONONAME, OINDREGSP, OEMPTY, OGETG:\n\tcase OTYPE, ONAME, OLITERAL:\n\t    ...\n\tcase OPANIC:\n\t\tn = mkcall(\"gopanic\", nil, init, n.Left)\n\n\tcase ORECOVER:\n\t\tn = mkcall(\"gorecover\", n.Type, init, nod(OADDR, nodfp, nil))\n\t...\n}\n```\n\n实际上在调用 `panic` 和 `recover` 关键字时，是在编译阶段先转换为相应的 OPCODE 后，再由编译器转换为对应的运行时方法。并不是你所想像那样一步到位，有兴趣的小伙伴可以研究一下\n\n## 总结\n\n本文主要针对 `panic` 和 `recover` 关键字进行了深入源码的剖析，而开头的 4+1 个思考题，就是希望您能够带着疑问去学习，达到事半功倍的功效\n\n另外本文和 `defer` 有一定的关联性，因此需要有一定的基础知识。若刚刚看的时候这部分不理解，学习后可以再读一遍加深印象\n\n在最后，现在的你可以回答这几个思考题了吗？说出来了才是真的懂 ：）\n"
  },
  {
    "path": "content/posts/go/pkg/2018-09-28-log.md",
    "content": "---\n\ntitle:      \"log 标准库\"\ndate:       2018-09-28 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - 源码分析\n---\n\n## 日志\n\n### 输出\n\n```\n2018/09/28 20:03:08 EDDYCJY Blog...\n```\n\n### 构成\n\n[日期]<空格>[时分秒]<空格>[内容]<\\n>\n\n## 源码剖析\n\n### Logger\n\n```\ntype Logger struct {\n\tmu     sync.Mutex \n\tprefix string\n\tflag   int\n\tout    io.Writer\n\tbuf    []byte\n}\n```\n\n1. mu：互斥锁，用于确保原子的写入\n2. prefix：每行需写入的日志前缀内容\n3. flag：设置日志辅助信息（时间、文件名、行号）的写入。可选如下标识位：\n\n```\nconst (\n\tLdate         = 1 << iota       // value: 1\n\tLtime                           // value: 2\n\tLmicroseconds                   // value: 4\n\tLlongfile                       // value: 8\n\tLshortfile                      // value: 16\n\tLUTC                            // value: 32\n\tLstdFlags     = Ldate | Ltime   // value: 3\n)\n```\n\n- Ldate：当地时区的格式化日期：2009/01/23\n- Ltime：当地时区的格式化时间：01:23:23\n- Lmicroseconds：在 Ltime 的基础上，增加微秒的时间数值显示\n- Llongfile：完整的文件名和行号：/a/b/c/d.go:23\n- Lshortfile：当前文件名和行号：d.go：23，会覆盖 Llongfile 标识\n- LUTC：如果设置 Ldate 或 Ltime，且设置 LUTC，则优先使用 UTC 时区而不是本地时区\n- LstdFlags：Logger 的默认初始值（Ldate 和 Ltime）\n\n4. out：io.Writer\n5. buf：用于存储将要写入的日志内容\n\n### New\n\n```\nfunc New(out io.Writer, prefix string, flag int) *Logger {\n\treturn &Logger{out: out, prefix: prefix, flag: flag}\n}\n\nvar std = New(os.Stderr, \"\", LstdFlags)\n```\n\nNew 方法用于初始化 Logger，接受三个初始参数，可以定制化而在 log 包内默认会初始一个 std，它指向标准输入流。而默认的标准输出、标准错误就是显示器（输出到屏幕上），标准输入就是键盘。辅助的时间信息默认为 `Ldate | Ltime`，也就是 `2009/01/23 01:23:23`\n\n```\n// os\nvar (\n\tStdin  = NewFile(uintptr(syscall.Stdin), \"/dev/stdin\")\n\tStdout = NewFile(uintptr(syscall.Stdout), \"/dev/stdout\")\n\tStderr = NewFile(uintptr(syscall.Stderr), \"/dev/stderr\")\n)\n```\n\n- Stdin：标准输入\n- Stdout：标准输出\n- Stderr：标准错误\n\n### Getter\n\n- Flags\n- Prefix\n\n### Setter\n\n- SetFlags\n- SetPrefix\n- SetOutput\n\n### Print*, Fatal*, Panic*\n\n```\nfunc Print(v ...interface{}) {\n\tstd.Output(2, fmt.Sprint(v...))\n}\n\nfunc Printf(format string, v ...interface{}) {\n\tstd.Output(2, fmt.Sprintf(format, v...))\n}\n\nfunc Println(v ...interface{}) {\n\tstd.Output(2, fmt.Sprintln(v...))\n}\n\nfunc Fatal(v ...interface{}) {\n\tstd.Output(2, fmt.Sprint(v...))\n\tos.Exit(1)\n}\n\nfunc Panic(v ...interface{}) {\n\ts := fmt.Sprint(v...)\n\tstd.Output(2, s)\n\tpanic(s)\n}\n\n...\n```\n\n这一部分介绍最常用的日志写入方法，从源码可得知 `Xrintln`、`Xrintf` 函数 **换行**、**可变参数**都是通过 `fmt` 标准库的方法去实现的\n\n`Fatal` 和 `Panic` 是通过 `os.Exit(1)`、`panic(s)` 集成实现的。而具体的组装逻辑是通过 `Output` 方法实现的\n\n#### Logger.Output\n\n```\nfunc (l *Logger) Output(calldepth int, s string) error {\n\tnow := time.Now() // get this early.\n\tvar file string\n\tvar line int\n\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n\tif l.flag&(Lshortfile|Llongfile) != 0 {\n\t\t// Release lock while getting caller info - it's expensive.\n\t\tl.mu.Unlock()\n\t\tvar ok bool\n\t\t_, file, line, ok = runtime.Caller(calldepth)\n\t\tif !ok {\n\t\t\tfile = \"???\"\n\t\t\tline = 0\n\t\t}\n\t\tl.mu.Lock()\n\t}\n\tl.buf = l.buf[:0]\n\tl.formatHeader(&l.buf, now, file, line)\n\tl.buf = append(l.buf, s...)\n\tif len(s) == 0 || s[len(s)-1] != '\\n' {\n\t\tl.buf = append(l.buf, '\\n')\n\t}\n\t_, err := l.out.Write(l.buf)\n\treturn err\n}\n```\n\nOutput 方法，简单来讲就是将写入的日志事件信息组装并输出，它会根据 flag 标识位的不同来使用 `runtime.Caller` 去获取当前 goroutine 所执行的函数文件、行号等调用信息（log 标准库中默认深度为 2）。另外如果结尾不是换行符 `\\n`，将自动补全一个换行\n\n#### Logger.formatHeader\n\n```\nfunc (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int) {\n\t*buf = append(*buf, l.prefix...)\n\tif l.flag&(Ldate|Ltime|Lmicroseconds) != 0 {\n\t\tif l.flag&LUTC != 0 {\n\t\t\tt = t.UTC()\n\t\t}\n\t\tif l.flag&Ldate != 0 {\n\t\t\tyear, month, day := t.Date()\n\t\t\titoa(buf, year, 4)\n\t\t\t*buf = append(*buf, '/')\n\t\t\titoa(buf, int(month), 2)\n\t\t\t*buf = append(*buf, '/')\n\t\t\titoa(buf, day, 2)\n\t\t\t*buf = append(*buf, ' ')\n\t\t}\n\t\tif l.flag&(Ltime|Lmicroseconds) != 0 {\n\t\t\thour, min, sec := t.Clock()\n\t\t\titoa(buf, hour, 2)\n\t\t\t*buf = append(*buf, ':')\n\t\t\titoa(buf, min, 2)\n\t\t\t*buf = append(*buf, ':')\n\t\t\titoa(buf, sec, 2)\n\t\t\tif l.flag&Lmicroseconds != 0 {\n\t\t\t\t*buf = append(*buf, '.')\n\t\t\t\titoa(buf, t.Nanosecond()/1e3, 6)\n\t\t\t}\n\t\t\t*buf = append(*buf, ' ')\n\t\t}\n\t}\n\tif l.flag&(Lshortfile|Llongfile) != 0 {\n\t\tif l.flag&Lshortfile != 0 {\n\t\t\tshort := file\n\t\t\tfor i := len(file) - 1; i > 0; i-- {\n\t\t\t\tif file[i] == '/' {\n\t\t\t\t\tshort = file[i+1:]\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tfile = short\n\t\t}\n\t\t*buf = append(*buf, file...)\n\t\t*buf = append(*buf, ':')\n\t\titoa(buf, line, -1)\n\t\t*buf = append(*buf, \": \"...)\n\t}\n}\n```\n\n该方法主要是用于格式化日志头（前缀），根据入参不同的标识位，添加分隔符和对应的值到日志信息中。执行流程如下：\n\n（1）如果不是空值，则将 prefix 写入 buf\n\n（2）如果设置 `Ldate`、`Ltime`、`Lmicroseconds`，则对应将日期和时间写入 buf\n\n（3）如果设置 `Lshortfile`、`Llongfile`，则对应将文件和行号信息写入 buf\n\n#### Logger.itoa\n\n```\nfunc itoa(buf *[]byte, i int, wid int) {\n\t// Assemble decimal in reverse order.\n\tvar b [20]byte\n\tbp := len(b) - 1\n\tfor i >= 10 || wid > 1 {\n\t\twid--\n\t\tq := i / 10\n\t\tb[bp] = byte('0' + i - q*10)\n\t\tbp--\n\t\ti = q\n\t}\n\t// i < 10\n\tb[bp] = byte('0' + i)\n\t*buf = append(*buf, b[bp:]...)\n}\n```\n\n该方法主要用于将整数转换为定长的十进制 ASCII，同时给出负数宽度避免左侧补 0。另外会以相反的顺序组合十进制 \n\n### 如何定制化 Logger\n\n在标准库内，可通过其开放的 New 方法来实现各种各样的自定义 Logger 组件，但是为什么也可以直接 `log.Print*` 等方法呢？\n\n```\nfunc New(out io.Writer, prefix string, flag int) *Logger\n```\n\n其实是在标准库内，如果你刚刚细心的看了前面的小节，不难发现其默认实现了一个 Logger 组件\n\n```\nvar std = New(os.Stderr, \"\", LstdFlags)\n```\n\n这也是一个小小的精妙之处 ⭕️\n\n## 总结\n\n通过查阅 log 标准库的源码，可得知最简单的一个日志包应该如何编写。另外 log 包是在所有涉及到 Logger 的地方都对 `sync.Mutex` 进行操作（以此解决原子问题），其余逻辑均为组装日志信息和转换数值格式，该包较为经典，可以多读几遍 😄\n\n## 问题\n\n为什么在调用 `runtime.Caller` 前要先解锁，后再加锁呢?\n"
  },
  {
    "path": "content/posts/go/pkg/2018-12-04-fmt.md",
    "content": "---\n\ntitle:      \"fmt 标准库 --- Print* 是怎么样输出的？\"\ndate:       2018-12-04 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - 源码分析\n---\n\n## 前言\n\n```\npackage main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hello World!\")\n}\n```\n\n标准开场见多了，那内部标准库又是怎么输出这段英文的呢？今天一起来围观下源码吧 🤭\n\n## 原型\n\n\n```\nfunc Print(a ...interface{}) (n int, err error) {\n\treturn Fprint(os.Stdout, a...)\n}\n\nfunc Println(a ...interface{}) (n int, err error) {\n\treturn Fprintln(os.Stdout, a...)\n}\n\nfunc Printf(format string, a ...interface{}) (n int, err error) {\n\treturn Fprintf(os.Stdout, format, a...)\n}\n```\n\n- Print：使用默认格式说明符打印格式并写入标准输出。当两者都不是字符串时，在操作数之间添加空格\n- Println：同上，不同的地方是始终在操作数之间添加空格，并附加换行符\n- Printf：根据格式说明符进行格式化并写入标准输出\n\n以上三类就是最常见的格式化 I/O 的方法，我们将基于此去进行拆解描述\n\n## 执行流程\n\n### 案例一：Print\n\n在这里我们使用 `Print` 方法做一个分析，便于后面的加深理解 😄\n\n```\nfunc Print(a ...interface{}) (n int, err error) {\n\treturn Fprint(os.Stdout, a...)\n}\n```\n\n`Print` 使用默认格式说明符打印格式并写入标准输出。另外当两者都为非空字符串时将插入一个空格\n\n#### 原型\n\n```\nfunc Fprint(w io.Writer, a ...interface{}) (n int, err error) {\n\tp := newPrinter()\n\tp.doPrint(a)\n\tn, err = w.Write(p.buf)\n\tp.free()\n\treturn\n}\n```\n\n该函数一共有两个形参：\n\n- w：输出流，只要实现 io.Writer 就可以（抽象）为流的写入\n- a：任意类型的多个值\n\n#### 分析主干流程\n\n1、 p := newPrinter(): 申请一个临时对象池（sync.Pool）\n\n```\nvar ppFree = sync.Pool{\n\tNew: func() interface{} { return new(pp) },\n}\n\nfunc newPrinter() *pp {\n\tp := ppFree.Get().(*pp)\n\tp.panicking = false\n\tp.erroring = false\n\tp.fmt.init(&p.buf)\n\treturn p\n}\n```\n\n- ppFree.Get()：基于 sync.Pool 实现 *pp 的临时对象池，每次获取一定会返回一个新的 pp 对象用于接下来的处理\n- *pp.panicking：用于解决无限递归的 panic、recover 问题，会根据该参数在 catchPanic 及时掐断\n- *pp.erroring：用于表示正在处理错误无效的 verb 标识符，主要作用是防止调用 handleMethods 方法\n- *pp.fmt.init(&p.buf)：初始化 fmt 配置，会设置 buf 并且清空 fmtFlags 标志位\n\n2、 p.doPrint(a): 执行约定的格式化动作（参数间增加一个空格、最后一个参数增加换行符）\n\n```\nfunc (p *pp) doPrint(a []interface{}) {\n\tprevString := false\n\tfor argNum, arg := range a {\n\t    true && false\n\t\tisString := arg != nil && reflect.TypeOf(arg).Kind() == reflect.String\n\t\t// Add a space between two non-string arguments.\n\t\tif argNum > 0 && !isString && !prevString {\n\t\t\tp.buf.WriteByte(' ')\n\t\t}\n\t\tp.printArg(arg, 'v')\n\t\tprevString = isString\n\t}\n}\n```\n\n可以看到底层通过判断该入参，**同时**满足以下条件就会添加分隔符（空格）：\n\n- 当前入参为多个参数（例如：Slice）\n- 当前入参不为 nil 且不为字符串（通过反射确定）\n- 当前入参不为首项或上一个入参不为字符串\n\n而在 `Print` 方法中，不需要指定格式符。实际上在该方法内直接指定为 `v`。也就是默认格式的值\n\n```\np.printArg(arg, 'v')\n```\n\n3. w.Write(p.buf): 写入标准输出（io.Writer）\n\n4. *pp.free(): 释放已缓存的内容。在使用完临时对象后，会将 buf、arg、value 清空再重新存放到 ppFree 中。以便于后面再取出重用（利用 sync.Pool 的临时对象特性）\n\n### 案例二：Printf\n\n#### 标识符\n\n##### Verbs\n\n```\n%v\tthe value in a default format\n\twhen printing structs, the plus flag (%+v) adds field names\n%#v\ta Go-syntax representation of the value\n%T\ta Go-syntax representation of the type of the value\n%%\ta literal percent sign; consumes no value\n%t\tthe word true or false\n```\n\n##### Flags\n\n```\n+\talways print a sign for numeric values;\n\tguarantee ASCII-only output for %q (%+q)\n-\tpad with spaces on the right rather than the left (left-justify the field)\n#\talternate format: add leading 0 for octal (%#o), 0x for hex (%#x);\n\t0X for hex (%#X); suppress 0x for %p (%#p);\n\tfor %q, print a raw (backquoted) string if strconv.CanBackquote\n\treturns true;\n\talways print a decimal point for %e, %E, %f, %F, %g and %G;\n\tdo not remove trailing zeros for %g and %G;\n\twrite e.g. U+0078 'x' if the character is printable for %U (%#U).\n' '\t(space) leave a space for elided sign in numbers (% d);\n\tput spaces between bytes printing strings or slices in hex (% x, % X)\n0\tpad with leading zeros rather than spaces;\n\tfor numbers, this moves the padding after the sign\n```\n\n详细建议参见 [Godoc](https://golang.org/pkg/fmt/#hdr-Printing)\n\n#### 原型\n\n```\nfunc Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {\n\tp := newPrinter()\n\tp.doPrintf(format, a)\n\tn, err = w.Write(p.buf)\n\tp.free()\n\treturn\n}\n```\n\n与 Print 相比，最大的不同就是 doPrintf 方法了。在这里我们来详细看看其代码，如下：\n\n```\nfunc (p *pp) doPrintf(format string, a []interface{}) {\n\tend := len(format)\n\targNum := 0         // we process one argument per non-trivial format\n\tafterIndex := false // previous item in format was an index like [3].\n\tp.reordered = false\nformatLoop:\n\tfor i := 0; i < end; {\n\t\tp.goodArgNum = true\n\t\tlasti := i\n\t\tfor i < end && format[i] != '%' {\n\t\t\ti++\n\t\t}\n\t\tif i > lasti {\n\t\t\tp.buf.WriteString(format[lasti:i])\n\t\t}\n\t\tif i >= end {\n\t\t\t// done processing format string\n\t\t\tbreak\n\t\t}\n\n\t\t// Process one verb\n\t\ti++\n\n\t\t// Do we have flags?\n\t\tp.fmt.clearflags()\n\tsimpleFormat:\n\t\tfor ; i < end; i++ {\n\t\t\tc := format[i]\n\t\t\tswitch c {\n\t\t\tcase '#':   //'#'、'0'、'+'、'-'、' '\n\t\t\t\t...\n\t\t\tdefault:\n\t\t\t\tif 'a' <= c && c <= 'z' && argNum < len(a) {\n\t\t\t\t\t...\n\t\t\t\t\tp.printArg(a[argNum], rune(c))\n\t\t\t\t\targNum++\n\t\t\t\t\ti++\n\t\t\t\t\tcontinue formatLoop\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tbreak simpleFormat\n\t\t\t}\n\t\t}\n\n\t\t// Do we have an explicit argument index?\n\t\targNum, i, afterIndex = p.argNumber(argNum, format, i, len(a))\n\n\t\t// Do we have width?\n\t\tif i < end && format[i] == '*' {\n\t\t\t...\n\t\t}\n\n\t\t// Do we have precision?\n\t\tif i+1 < end && format[i] == '.' {\n\t\t\t...\n\t\t}\n\n\t\tif !afterIndex {\n\t\t\targNum, i, afterIndex = p.argNumber(argNum, format, i, len(a))\n\t\t}\n\n\t\tif i >= end {\n\t\t\tp.buf.WriteString(noVerbString)\n\t\t\tbreak\n\t\t}\n\n\t\t...\n\n\t\tswitch {\n\t\tcase verb == '%': // Percent does not absorb operands and ignores f.wid and f.prec.\n\t\t\tp.buf.WriteByte('%')\n\t\tcase !p.goodArgNum:\n\t\t\tp.badArgNum(verb)\n\t\tcase argNum >= len(a): // No argument left over to print for the current verb.\n\t\t\tp.missingArg(verb)\n\t\tcase verb == 'v':\n\t\t\t...\n\t\t\tfallthrough\n\t\tdefault:\n\t\t\tp.printArg(a[argNum], verb)\n\t\t\targNum++\n\t\t}\n\t}\n\n\tif !p.reordered && argNum < len(a) {\n\t\t...\n\t}\n}\n```\n\n#### 分析主干流程\n\n1. 写入 % 之前的字符内容\n2. 如果所有标志位处理完毕（到达字符尾部），则跳出处理逻辑\n3. （往后移）跳过 % ，开始处理其他 verb 标志位\n4. 清空（重新初始化） fmt 配置\n5. 处理一些基础的 verb 标识符（simpleFormat）。如：'#'、'0'、'+'、'-'、' ' 以及**简单的 verbs 标识符（不包含精度、宽度和参数索引）。需要注意的是，若当前字符为简单 verb 标识符。则直接进行处理。完成后会直接后移到下一个字符**。其余标志位则变更 fmt 配置项，便于后续处理\n6. 处理参数索引（argument index）\n7. 处理参数宽度（width）\n8. 处理参数精度（precision）\n9. % 之后若不存在 verbs 标识符则返回 `noVerbString`。值为 %!(NOVERB)\n10. 处理特殊 verbs 标识符（如：'%%'、'%#v'、'%+v'）、错误情况（如：参数索引指定错误、参数集个数与 verbs 标识符数量不匹配）或进行格式化参数集\n11. 常规流程处理完毕\n\n在特殊情况下，若提供的参数集比 verb 标识符多。fmt 将会贪婪检查下去，将多出的参数集以特定的格式输出，如下：\n\n```\nfmt.Printf(\"%d\", 1, 2, 3)\n// 1%!(EXTRA int=2, int=3)\n```\n\n- 约定前缀额外标志：%!(EXTRA\n- 当前参数的类型\n- 约定格式符：=\n- 当前参数的值（默认以 %v 格式化）\n- 约定格式符：)\n\n值得注意的是，当指定了参数索引或实际处理的参数小于入参的参数集时，就不会进行贪婪匹配来展示\n\n### 案例三：Println\n\n#### 原型\n\n```\nfunc Fprintln(w io.Writer, a ...interface{}) (n int, err error) {\n\tp := newPrinter()\n\tp.doPrintln(a)\n\tn, err = w.Write(p.buf)\n\tp.free()\n\treturn\n}\n```\n\n在这个方法中，最大的区别就是 doPrintln，我们一起来看看，如下：\n\n```\nfunc (p *pp) doPrintln(a []interface{}) {\n\tfor argNum, arg := range a {\n\t\tif argNum > 0 {\n\t\t\tp.buf.WriteByte(' ')\n\t\t}\n\t\tp.printArg(arg, 'v')\n\t}\n\tp.buf.WriteByte('\\n')\n}\n```\n\n#### 分析主干流程\n\n- 循环入参的参数集，并以空格分隔\n- 格式化当前参数，默认以 `%v` 对参数进行格式化\n- 在结尾添加 `\\n` 字符\n\n\n## 如何格式化参数\n\n在上例的执行流程分析中，可以看到格式化参数这一步是在 `p.printArg(arg, verb)` 执行的，我们一起来看看它都做了些什么？\n\n```\nfunc (p *pp) printArg(arg interface{}, verb rune) {\n\tp.arg = arg\n\tp.value = reflect.Value{}\n\n\tif arg == nil {\n\t\tswitch verb {\n\t\tcase 'T', 'v':\n\t\t\tp.fmt.padString(nilAngleString)\n\t\tdefault:\n\t\t\tp.badVerb(verb)\n\t\t}\n\t\treturn\n\t}\n\n\tswitch verb {\n\tcase 'T':\n\t\tp.fmt.fmt_s(reflect.TypeOf(arg).String())\n\t\treturn\n\tcase 'p':\n\t\tp.fmtPointer(reflect.ValueOf(arg), 'p')\n\t\treturn\n\t}\n\n\t// Some types can be done without reflection.\n\tswitch f := arg.(type) {\n\tcase bool:\n\t\tp.fmtBool(f, verb)\n\tcase float32:\n\t\tp.fmtFloat(float64(f), 32, verb)\n\t...\n\tcase reflect.Value:\n\t\tif f.IsValid() && f.CanInterface() {\n\t\t\tp.arg = f.Interface()\n\t\t\tif p.handleMethods(verb) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tp.printValue(f, verb, 0)\n\tdefault:\n\t\tif !p.handleMethods(verb) {\n\t\t\tp.printValue(reflect.ValueOf(f), verb, 0)\n\t\t}\n\t}\n}\n```\n\n在小节代码中可以看见，fmt 本身对不同的类型做了不同的处理。这样子就避免了通过反射确定。相对的提高了性能\n\n其中有两个特殊的方法，分别是 `handleMethods` 和 `badVerb`，接下来分别来看看他们的作用是什么\n\n1、badVerb\n\n它主要用于格式化并处理错误的行为。我们可以一起来看看，代码如下：\n\n```\nfunc (p *pp) badVerb(verb rune) {\n\tp.erroring = true\n\tp.buf.WriteString(percentBangString)\n\tp.buf.WriteRune(verb)\n\tp.buf.WriteByte('(')\n\tswitch {\n\tcase p.arg != nil:\n\t\tp.buf.WriteString(reflect.TypeOf(p.arg).String())\n\t\tp.buf.WriteByte('=')\n\t\tp.printArg(p.arg, 'v')\n\t...\n\tdefault:\n\t\tp.buf.WriteString(nilAngleString)\n\t}\n\tp.buf.WriteByte(')')\n\tp.erroring = false\n}\n```\n\n在处理错误格式化时，我们可以对比以下例子：\n\n```\nfmt.Printf(\"%s\", []int64{1, 2, 3})\n// [%!s(int64=1) %!s(int64=2) %!s(int64=3)]%\n```\n\n在 badVerb 中可以看到错误字符串的处理主要分为以下部分：\n\n- 约定前缀错误标志：%!\n- 当前的格式化操作符\n- 约定格式符：(\n- 当前参数的类型\n- 约定格式符：=\n- 当前参数的值（默认以 %v 格式化）\n- 约定格式符：)\n\n2、handleMethods\n\n```\nfunc (p *pp) handleMethods(verb rune) (handled bool) {\n\tif p.erroring {\n\t\treturn\n\t}\n\t// Is it a Formatter?\n\tif formatter, ok := p.arg.(Formatter); ok {\n\t\thandled = true\n\t\tdefer p.catchPanic(p.arg, verb)\n\t\tformatter.Format(p, verb)\n\t\treturn\n\t}\n\n\t// If we're doing Go syntax and the argument knows how to supply it, take care of it now.\n\t...\n\t\n\treturn false\n}\n```\n\n这个方法比较特殊，一般在自定义结构体和未知情况下进行调用。主要流程是：\n\n- 若当前参数为错误 verb 标识符，则直接返回\n- 判断是否实现了 Formatter \n- 实现，则利用自定义 Formatter 格式化参数\n- 未实现，则最大程度的利用 Go syntax 默认规则去格式化参数\n\n## 拓展\n\n在 fmt 标准库中可以通过自定义结构体来实现方法的自定义，大致如下几种\n\n### fmt.State\n\n```\ntype State interface {\n\tWrite(b []byte) (n int, err error)\n\n\tWidth() (wid int, ok bool)\n\n\tPrecision() (prec int, ok bool)\n\n\tFlag(c int) bool\n}\n```\n\nState 用于获取标志位的状态值，涉及如下：\n\n- Write：将格式化完毕的字符写入缓冲区中，等待下一步处理\n- Width：返回宽度信息和是否被设置\n- Precision：返回精度信息和是否被设置\n- Flag：返回特殊标志符（'#'、'0'、'+'、'-'、' '）是否被设置\n\n### fmt.Formatter\n\n```\ntype Formatter interface {\n\tFormat(f State, c rune)\n}\n```\n\nFormatter 用于实现**自定义格式化方法**。可通过在自定义结构体中实现 Format 方法来实现这个目的\n\n另外，可以通过 f 获取到当前标识符的宽度、精度等状态值。c 为 verb 标识符，可以得到其动作是什么\n\n### fmt.Stringer\n\n```\ntype Stringer interface {\n\tString() string\n}\n```\n\n当该对象为 String、Array、Slice 等类型时，将会调用 `String()` 方法对类字符串进行格式化\n\n### fmt.GoStringer\n\n```\ntype GoStringer interface {\n\tGoString() string\n}\n```\n\n当格式化特定 verb 标识符（%v）时，将调用 `GoString()` 方法对其进行格式化\n\n\n## 总结\n\n通过本文对 fmt 标准库的分析，可以发现它有以下特点：\n\n- 在拓展性方面，可以自定义格式化方法等\n- 在完整度方面，尽可能的贪婪匹配，输出参数集\n- 在性能方面，每种不同的参数类型，都实现了不同的格式化处理操作\n- 在性能方面，尽可能的最短匹配，格式化参数集\n\n总的来说，fmt 标准库有许多值得推敲的细节，希望你能够在本文学到 😄\n"
  },
  {
    "path": "content/posts/go/pkg/2018-12-15-unsafe.md",
    "content": "---\n\ntitle:      \"有点不安全却又一亮的 Go unsafe.Pointer\"\ndate:       2018-12-15 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - 源码分析\n---\n\n\n在上一篇文章《深入理解 Go Slice》中，大家会发现其底层数据结构使用了 `unsafe.Pointer`。因此想着再介绍一下其关联知识\n\n## 前言\n\n在大家学习 Go 的时候，肯定都学过 “Go 的指针是不支持指针运算和转换” 这个知识点。为什么呢？\n\n首先，Go 是一门静态语言，所有的变量都必须为标量类型。不同的类型不能够进行赋值、计算等跨类型的操作。那么指针也对应着相对的类型，也在 Compile 的静态类型检查的范围内。同时静态语言，也称为强类型。也就是一旦定义了，就不能再改变它\n\n## 错误示例\n\n```\nfunc main(){\n\tnum := 5\n\tnumPointer := &num\n\n\tflnum := (*float32)(numPointer)\n\tfmt.Println(flnum)\n}\n```\n\n输出结果：\n\n```\n# command-line-arguments\n...: cannot convert numPointer (type *int) to type *float32\n```\n\n在示例中，我们创建了一个 `num` 变量，值为 5，类型为 `int`。取了其对于的指针地址后，试图强制转换为 `*float32`，结果失败...\n\n## unsafe\n\n针对刚刚的 “错误示例”，我们可以采用今天的男主角 `unsafe` 标准库来解决。它是一个神奇的包，在官方的诠释中，有如下概述：\n\n- 围绕 Go 程序内存安全及类型的操作\n- 很可能会是不可移植的\n- 不受 Go 1 兼容性指南的保护\n\n简单来讲就是，不怎么推荐你使用。因为它是 unsafe（不安全的），但是在特殊的场景下，使用了它。可以打破 Go 的类型和内存安全机制，让你获得眼前一亮的惊喜效果 😄\n\n### Pointer\n\n为了解决这个问题，需要用到 `unsafe.Pointer`。它表示任意类型且可寻址的指针值，可以在不同的指针类型之间进行转换（类似 C 语言的 void * 的用途）\n\n其包含四种核心操作：\n\n- 任何类型的指针值都可以转换为 Pointer\n- Pointer 可以转换为任何类型的指针值\n- uintptr 可以转换为 Pointer\n- Pointer 可以转换为 uintptr\n\n在这一部分，重点看第一点、第二点。你再想想怎么修改 “错误示例” 让它运行起来？\n\n```\nfunc main(){\n\tnum := 5\n\tnumPointer := &num\n\n\tflnum := (*float32)(unsafe.Pointer(numPointer))\n\tfmt.Println(flnum)\n}\n```\n\n输出结果：\n\n```\n0xc4200140b0\n```\n\n在上述代码中，我们小加改动。通过 `unsafe.Pointer` 的特性对该指针变量进行了修改，就可以完成任意类型（*T）的指针转换\n\n需要注意的是，这时还无法对变量进行操作或访问。因为不知道该指针地址指向的东西具体是什么类型。不知道是什么类型，又如何进行解析呢。无法解析也就自然无法对其变更了\n\n### Offsetof\n\n在上小节中，我们对普通的指针变量进行了修改。那么它是否能做更复杂一点的事呢？\n\n```\ntype Num struct{\n\ti string\n\tj int64\n}\n\nfunc main(){\n\tn := Num{i: \"EDDYCJY\", j: 1}\n\tnPointer := unsafe.Pointer(&n)\n\n\tniPointer := (*string)(unsafe.Pointer(nPointer))\n\t*niPointer = \"煎鱼\"\n\n\tnjPointer := (*int64)(unsafe.Pointer(uintptr(nPointer) + unsafe.Offsetof(n.j)))\n\t*njPointer = 2\n\n\tfmt.Printf(\"n.i: %s, n.j: %d\", n.i, n.j)\n}\n\n```\n\n输出结果：\n\n```\nn.i: 煎鱼, n.j: 2\n```\n\n在剖析这段代码做了什么事之前，我们需要了解结构体的一些基本概念：\n\n- 结构体的成员变量在内存存储上是一段连续的内存\n- 结构体的初始地址就是第一个成员变量的内存地址\n- 基于结构体的成员地址去计算偏移量。就能够得出其他成员变量的内存地址\n\n再回来看看上述代码，得出执行流程：\n\n- 修改 `n.i` 值：`i` 为第一个成员变量。因此不需要进行偏移量计算，直接取出指针后转换为 `Pointer`，再强制转换为字符串类型的指针值即可\n\n- 修改 `n.j` 值：`j` 为第二个成员变量。需要进行偏移量计算，才可以对其内存地址进行修改。在进行了偏移运算后，当前地址已经指向第二个成员变量。接着重复转换赋值即可\n\n\n需要注意的是，这里使用了如下方法（来完成偏移计算的目标）：\n\n1、uintptr：`uintptr` 是 Go 的内置类型。返回无符号整数，可存储一个完整的地址。后续常用于指针运算\n\n```\ntype uintptr uintptr\n```\n\n2、unsafe.Offsetof：返回成员变量 x 在结构体当中的偏移量。更具体的讲，就是返回结构体初始位置到 x 之间的字节数。需要注意的是入参 `ArbitraryType` 表示任意类型，并非定义的 `int`。它实际作用是一个占位符\n\n```\nfunc Offsetof(x ArbitraryType) uintptr\n```\n\n在这一部分，其实就是巧用了 `Pointer` 的第三、第四点特性。这时候就已经可以对变量进行操作了 😄\n\n### 错误示例\n\n```\nfunc main(){\n\tn := Num{i: \"EDDYCJY\", j: 1}\n\tnPointer := unsafe.Pointer(&n)\n    ...\n\n\tptr := uintptr(nPointer)\n\tnjPointer := (*int64)(unsafe.Pointer(ptr + unsafe.Offsetof(n.j)))\n\t...\n}\n```\n\n这里存在一个问题，`uintptr` 类型是不能存储在临时变量中的。因为从 GC 的角度来看，`uintptr` 类型的临时变量只是一个无符号整数，并不知道它是一个指针地址\n\n因此当满足一定条件后，`ptr` 这个临时变量是可能被垃圾回收掉的，那么接下来的内存操作，岂不成迷？\n\n## 总结\n\n简洁回顾两个知识点。第一是 `unsafe.Pointer` 可以让你的变量在不同的指针类型转来转去，也就是表示为任意可寻址的指针类型。第二是 `uintptr` 常用于与 `unsafe.Pointer` 打配合，用于做指针运算，巧妙地很\n\n最后还是那句，没有特殊必要的话。是不建议使用 `unsafe` 标准库，它并不安全。虽然它常常能让你眼前一亮 👌\n"
  },
  {
    "path": "content/posts/go/plugin.md",
    "content": "---\ntitle: \"Go 插件系统，一个凉了快半截的特性？\"\ndate: 2021-12-31T12:54:59+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n在 Go 语言中，有一个好像很好用，但却比较少人提及的功能，那就是 Go Plugin。目前在 Go 工程中普遍还没广泛的使用起来，覆盖率不高。\n\n前段时间小咸鱼的同事问了他这玩具怎么用，他正想甩出一个链接，但发现...煎鱼竟然没写过，这不，Go 知识板块的文章地图得补全。\n\n今天煎鱼就大家一起学习 Go Plugin！\n\n## 是什么\n\nGo Team 最早在 Go1.7 实验，在 Go1.8 正式引入了 Go Plugin 的机制。于 2016 年发布，一开始仅支持 Linux 实现：\n\n![](https://files.mdnice.com/user/3610/9a791ced-39db-4f09-a027-60569ccc95dd.png)\n\nGo Plugin 机制实现了 Go 插件的加载和符号解析，能够支持将我们所编写的 Go 包编译为共享库（.so）。\n\n这样 Go 工程就可以加载所编译好的 Go Plugin（已经变成了共享库文件），在程序中调用共享库中的函数、常量、变量等使用。也称其为 Go 语言中的热插拔的插件系统。\n\n截止 Go1.17 为止，Go Plugin 仅支持在 Linux、FreeBSD 和 MacOS 上运行，还不支持 Windows。\n\n## 为什么需要\n\nGo 语言是静态语言，正常我们写一个程序，分如下两个角度来看：\n- 从代码编写的角度来看：我们在程序编写的时候就已经把所有的功能实现给确定了，不会发生什么根本性的变化。\n- 从程序的角度来看：在 Go 进行编译时，就已经把所有引用的标准库、第三方库等都编译打包好进二进制文件了，因此也就无法在运行时去动态加载，所以没法有其它的可能性。\n\n那么为什么需要 Go Plugin 呢，原因如下：\n- 可插拔的插件：程序能够随时的安装插件，也能够卸载他，获得更多运行时的自定义能力。\n- 可动态加载运行时模块：随时安装了插件，自然也就需要可自行决定运行哪个插件的模块了。\n- 可独立开发插件、模块：主系统和子插件，可能由不同的团队开发和提供，也更有价值。\n\n其实本质上还是**希望程序能够在运行时实现动态的外部加载**，根据不同的条件、场景加载不同的插件功能。\n\n## 使用方法\n\n### 通用概念\n\nGo 官方给出的例子非常简单，只需要在 Go 编译时指定为插件就可以了。\n\n编译的命令例子如下：\n\n```golang\ngo build -buildmode=plugin\n```\n\n当一个插件初次被打开时，所有尚未成为程序一部分的包的init函数被调用。不过主函数不被运行。需要注意**一个插件只会被初始化一次，插件不能被关闭**。\n\n其共有如下几个 API：\n\n```golang\ntype Plugin\n    func Open(path string) (*Plugin, error)\n    func (p *Plugin) Lookup(symName string) (Symbol, error)\ntype Symbol\n```\n- Plugin.Open：开启一个 Go 插件。如果一个路径已经被打开，那么将返回现有的 `*Plugin`。\n- Plugin.Lookup：在插件中搜索名所传入的符号，符号是任何导出的变量或函数。如果没有找到该符号，它会报告一个错误。\n\n主要就是细分为插件和符号，符号（Symbol）本身是一个 interface，在调用 Plugin 相关方法后还是需要进一步断言才能使用。\n\n### 实际编写\n\n了解基本定义后，我们定义一个插件，一般我们会有个 plugins/ 的目录，作为主程序的附属插件集。\n\n插件的代码如下：\n\n```golang\npackage main\n\nimport \"fmt\"\n\nvar V int\n\nfunc F() {\n\tfmt.Printf(\"脑子进了 %d 次煎鱼 \\n\", V)\n}\n```\n\n包名必须为 main，在该插件根目录运行： \n\n```golang\ngo build -buildmode=plugin -o plugin.so main.go\n```\n\n就可以看到在编译的目录下多出了 `plugin.so` 文件，这就是这个插件经过编译后的动态库 .so 文件。\n\n随后只需在主程序加载这个插件就可以了，如下：\n\n```golang\nimport (\n\t\"plugin\"\n)\n\nfunc main() {\n\tp, err := plugin.Open(\"plugin.so\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tv, err := p.Lookup(\"V\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tf, err := p.Lookup(\"F\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t*v.(*int) = 999\n\tf.(func())()\n}\n```\n\n输出结果：\n\n```\n脑子进了 999 次煎鱼 \n```\n\n在程序中，我们先调用了 `plugin.Open` 方法打开了前面所编译的 `plugin.so` 动态库。\n\n紧接着调用 `plugin.Lookup` 方法，定位到了变量 V 和 方法 F，但由于其返回值都是 Symbol（interface），因此我们需要对其进行类型断言，随时才可以调用和使用。\n\n至此完成了一个插件的基本使用。\n\n## 为什么不被需要\n\n在前面我们提到了大量 Go Plugin 的优点，也演示了其 Plugin 代码编写起来有多么的简单和方便。\n\n但，**为什么 Go Plugin 已经发布了 4 年依然没有被大规模应用**，甚至对于不少业务开发来讲是不被需要的呢，或是压根不知道有这东西？\n\n究其原因，我个人认为一个东西的广泛应用要至少符合以下三大点：\n- 基数：需要的场景多。\n- 上手：方便且易用。\n- 质量：没有大问题。\n\n比较折腾的人的是，Go Plugin 这三大点都欠一些火候，综合导致了该功能的没有大规模应用。\n\n像是要应用 Go Plugin 有诸如下约束：\n\n- 环境问题：不支持 Windows 等（暂无计划，#19282），MacOS 有些问题，一开始只支持 Linux，其他的也是后面慢慢增加的支持。\n- Go 版本问题：Plugin 构建环境和第三方包的依赖版本需要保持一致。\n- 特性问题：Plugin 特性的缺失，例如不支持插件的关闭，暂时无新计划支持（#20461）。\n\n## 总结\n\n在 Go issues 中畅游时，能看到许多小伙伴在以往 4 年踩过的坑和无奈。甚至有一个高赞回答（#19282）表示：**插件功能主要是一个技术演示，由于一些不道德的原因，被作为语言的稳定功能发布**（The plugin feature is mostly a tech demo that for some unholy reason got released as a stable feature of the language.）。\n\n目前 Go Plugin 并不是 Go Team 的优先事项，在 Windows/Mac 的支持存在问题。GOPATH 有问题，不同 GO 版本也有问题。更是建议如果您想要插件，请走较慢的 grpc 路线，因为它们是有效的插件。\n\n也可以参考为数不多的一些 Go Plugin 用户的方案，例如：tidb。\n\n但如果要正式使用，是需要慎重考虑，又或是再等等...等更完善的那一天？\n\n## 参考\n\n- [Go Package plugin](https://golang.org/pkg/plugin/)\n- [Why is there no windows support for plugins?](https://www.reddit.com/r/golang/comments/bsoa4e/why_is_there_no_windows_support_for_plugins/)\n- [plugin: add Windows support](https://github.com/golang/go/issues/19282)\n- [plugin: Add support for closing plugins](https://github.com/golang/go/issues/20461)\n- [如何评价 Go 标准库中新增的 plugin 包？](https://www.zhihu.com/question/51650593)\n- [一文搞懂Go语言的plugin](https://mp.weixin.qq.com/s/yBosg0q0V_-wYk9G2GiVTw)"
  },
  {
    "path": "content/posts/go/real-context.md",
    "content": "---\ntitle: \"分享 Go 使用 Context 的正式姿势\"\ndate: 2021-12-31T12:54:54+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n在 Go 语言中，Goroutine（协程），也就是关键字 `go` 是一个家喻户晓的高级用法。这起的非常妙，说到 Go，就会想到这一门语言，想到 goroutine 这一关键字，而与之关联最深的就是 context。\n\n![](https://files.mdnice.com/user/3610/2f26bd64-e9bd-4c89-a9a0-326b8c2b9d00.png)\n\n## 背景\n\n平时在 Go 工程中开发中，几乎所有服务端（例如：HTTP Server）的默认实现，都在处理请求时新起 goroutine 进行处理。\n\n但一开始存在一个问题，那就是当一个请求被取消或超时时，所有在该请求上工作的 goroutines 应该迅速退出，以便系统可以回收他们正在使用的任何资源。\n\n当年可没有 context 标准库。很折腾。因此 Go 官方在 2014 年正式宣发了 context 标准库，形成一个完整的闭环。\n\n但有了 context 标准库，Go 爱好者们又奇怪了，前段时间我就被问到了：“Go context 的正确使用姿势是怎么样的”？\n\n（一张忘记在哪里被问的隐形截图）\n\n今天这篇文章就由煎鱼带你看看。\n\n## Context 用法\n\n在 Go context 用法中，我们常常将其与 select 关键字结合使用，用于监听其是否结束、取消等。\n\n代码如下：\n\n```golang\nconst shortDuration = 1 * time.Millisecond\n\nfunc main() {\n\tctx, cancel := context.WithTimeout(context.Background(), shortDuration)\n\tdefer cancel()\n\n\tselect {\n\tcase <-time.After(1 * time.Second):\n\t\tfmt.Println(\"脑子进煎鱼了\")\n\tcase <-ctx.Done():\n\t\tfmt.Println(ctx.Err())\n\t}\n}\n```\n\n输出结果：\n\n```\ncontext deadline exceeded\n```\n\n如果是更进一步结合 goroutine 的话，常见的例子是：\n\n```golang\n\tfunc(ctx context.Context) <-chan int {\n\t\tdst := make(chan int)\n\t\tn := 1\n\t\tgo func() {\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\treturn\n\t\t\t\tcase dst <- n:\n\t\t\t\t\tn++\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t\treturn dst\n\t}\n```\n\n我们平时工程中会起很多的 goroutine，这时候会在 goroutine 内结合 for+select，针对 context 的事件进行处理，达到跨 goroutine 控制的目的。\n\n## 正确的使用姿势\n\n### 对第三方调用要传入 context\n\n在 Go 语言中，Context 的默认支持已经是约定俗称的规范了。因此在我们对第三方有调用诉求的时候，要传入 context：\n\n```golang\nfunc main() {\n\treq, err := http.NewRequest(\"GET\", \"https://eddycjy.com/\", nil)\n\tif err != nil {\n\t\tfmt.Printf(\"http.NewRequest err: %+v\", err)\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithTimeout(req.Context(), 50*time.Millisecond)\n\tdefer cancel()\n\n\treq = req.WithContext(ctx)\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\tfmt.Printf(\"http.DefaultClient.Do err: %+v\", err)\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n}\n```\n\n这样子由于第三方开源库已经实现了根据 context 的超时控制，那么当你所传入的时间到达时，将会中断调用。\n\n若你发现第三方开源库没支持 context，那建议赶紧跑，换一个。免得在微服务体系下出现级联故障，还没有简单的手段控制，那就很麻烦了。\n\n### 不要将上下文存储在结构类型中\n\n大家会发现，在 Go 语言中，所有的第三方开源库，业务代码。清一色的都会将 context 放在方法的一个入参参数，作为首位形参。\n\n例如：\n\n![](https://files.mdnice.com/user/3610/f79303b3-2070-4d2b-ac6b-840c413d03fd.png)\n\n标准要求：每个方法的第一个参数都将 context 作为第一个参数，并使用 ctx 变量名惯用语。\n\n当然，我们也不能一杆子打死所有情况。确实存在极少数是把 context 放在结构体中的。基本常见于：\n- 底层基础库。\n- DDD 结构。\n\n每个请求都是独立的，context 自然每个都不一样，想清楚自己的应用使用场景很重要，否则遵循 Go 基本规范就好。\n\n\n在真实案例来看，有的 Leader 会单纯为了不想频繁传 context 而设计成结构体，结果导致一线 RD 就得天天 NewXXX，甚至有时候忘记了，还得背个小锅。\n\n### 函数调用链必须传播上下文\n\n我们会把 context 作为方法首位，本质目的是为了传播 context，自行完整调用链路上的各类控制：\n\n```golang\nfunc List(ctx context.Context, db *sqlx.DB) ([]User, error) {\n\tctx, span := trace.StartSpan(ctx, \"internal.user.List\")\n\tdefer span.End()\n\n\tusers := []User{}\n\tconst q = `SELECT * FROM users`\n\n\tif err := db.SelectContext(ctx, &users, q); err != nil {\n\t\treturn nil, errors.Wrap(err, \"selecting users\")\n\t}\n\n\treturn users, nil\n}\n```\n\n像在上述例子中，我们会把所传入方法的 context 一层层的传进去下一级方法。这里就是将外部的 context 传入 List 方法，再传入 SQL 执行的方法，解决了 SQL 执行语句的时间问题。\n\n### context 的继承和派生\n\n在 Go 标准库 context 中具有以下派生 context 的标准方法：\n\n```golang\nfunc WithCancel(parent Context) (ctx Context, cancel CancelFunc)\nfunc WithDeadline(parent Context, d time.Time) (Context, CancelFunc)\nfunc WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)\n```\n\n代码例子如下：\n\n```golang\nfunc handle(w http.ResponseWriter, req *http.Request) {\n  // parent context\n\ttimeout, _ := time.ParseDuration(req.FormValue(\"timeout\"))\n\tctx, cancel := context.WithTimeout(context.Background(), timeout)\n\n  // chidren context\n\tnewCtx, cancel := context.WithCancel(ctx)\n\tdefer cancel()\n\t// do something...\n}\n```\n\n一般会有父级 context 和子级 context 的区别，我们要保证在程序的行为中上下文对于多个 goroutine 同时使用是安全的。并且存在父子级别关系，父级 context 关闭或超时，可以继而影响到子级 context 的程序。\n\n### 不传递 nil context\n\n很多时候我们在创建 context 时，还不知道其具体的作用和下一步用途是什么。\n\n这种时候大家可能会直接使用 `context.Background` 方法：\n\n```golang\nvar (\n   background = new(emptyCtx)\n   todo       = new(emptyCtx)\n)\n\nfunc Background() Context {\n   return background\n}\n\nfunc TODO() Context {\n   return todo\n}\n```\n\n但在实际的 context 建议中，我们会建议使用 `context.TODO` 方法来创建顶级的 context，直到弄清楚实际 Context 的下一步用途，再进行变更。\n\n### context 仅传递必要的值\n\n我们在使用 context 作为上下文时，经常有信息传递的诉求。像是在 gRPC 中就会有 metadata 的概念，而在 gin 中就会自己封装 context 作为参数管理。\n\nGo 标准库 context 也有提供相关的方法：\n\n```golang\ntype Context\n    func WithValue(parent Context, key, val interface{}) Context\n```\n\n代码例子如下：\n\n```golang\nfunc main() {\n\ttype favContextKey string\n\tf := func(ctx context.Context, k favContextKey) {\n\t\tif v := ctx.Value(k); v != nil {\n\t\t\tfmt.Println(\"found value:\", v)\n\t\t\treturn\n\t\t}\n\t\tfmt.Println(\"key not found:\", k)\n\t}\n\n\tk := favContextKey(\"脑子进\")\n\tctx := context.WithValue(context.Background(), k, \"煎鱼\")\n\n\tf(ctx, k)\n\tf(ctx, favContextKey(\"小咸鱼\"))\n}\n```\n\n输出结果：\n\n```\nfound value: 煎鱼\nkey not found: 小咸鱼\n```\n\n在规范中，我们建议 context 在传递时，仅携带必要的参数给予其他的方法，或是 goroutine。甚至在 gRPC 中会做严格的出、入上下文参数的控制。\n\n在业务场景上，context 传值适用于传必要的业务核心属性，例如：租户号、小程序ID 等。不要将可选参数放到 context 中，否则可能会一团糟。\n\n## 总结\n\n- 对第三方调用要传入 context，用于控制远程调用。\n- 不要将上下文存储在结构类型中，尽可能的作为函数第一位形参传入。\n- 函数调用链必须传播上下文，实现完整链路上的控制。\n- context 的继承和派生，保证父、子级 context 的联动。\n- 不传递 nil context，不确定的 context 应当使用 TODO。\n- context 仅传递必要的值，不要让可选参数揉在一起。"
  },
  {
    "path": "content/posts/go/reflect.md",
    "content": "---\ntitle: \"解密 Go 语言之反射 reflect\"\ndate: 2020-11-07T15:01:51+08:00\ndraft: false\ntoc: true\nimages:\ntags: \n  - go\n  - 深度解密\n---\n\n大家好，我是煎鱼。\n\n在所有的语言中，反射这一功能基本属于必不可少的模块。虽说 “反射” 这个词让人根深蒂固，但更多的还是 WHY。反射到底是什么，反射又是基于什么法则实现的？\n\n![image](https://image.eddycjy.com/47976eb32b9cb5bdbe1869123fefb92b.jpg)\n\n今天我们通过这篇文章来一一揭晓，以 Go 语言为例，了解反射到底为何物，其底层又是如何实现的。\n\n## 反射是什么\n\n在计算机学中，反射是指计算机程序在运行时（runtime）可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说，反射就是程序在运行的时候能够 “观察” 并且修改自己的行为（来自维基百科）。\n\n![image](https://image.eddycjy.com/96394a2bb7b1dd964b5197837781e348.jpg)\n\n简单来讲就是，应用程序能够在运行时观察到变量的值，并且能够修改他。\n\n## 一个例子\n\n最常见的 reflect 标准库例子，如下：\n\n```\nimport (\n\t\"fmt\"\n\t\"reflect\"\n)\n\nfunc main() {\n\trv := []interface{}{\"hi\", 42, func() {}}\n\tfor _, v := range rv {\n\t\tswitch v := reflect.ValueOf(v); v.Kind() {\n\t\tcase reflect.String:\n\t\t\tfmt.Println(v.String())\n\t\tcase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:\n\t\t\tfmt.Println(v.Int())\n\t\tdefault:\n\t\t\tfmt.Printf(\"unhandled kind %s\", v.Kind())\n\t\t}\n\t}\n}\n```\n\n输出结果：\n\n```\nhi\n42\nunhandled kind func\n```\n\n在程序中主要是声明了 rv 变量，变量类型为 `interface{}`，其包含 3 个不同类型的值，分别是字符串、数字、闭包。一般 `interface{}` 的使用常见于不知道入参者具体的基本类型是什么，那么就会用 `interface{}` 类型来做一个伪 “泛型”。\n\n这时候又会引出一个新的问题，既然入参是 `interface{}`，那么出参时呢？ Go 语言是强类型语言，入参是 `interface{}`，出参也肯定是跑不了的，因此必然离不开类型的判断，这时候就要用到反射，也就是 reflect 标准库。反射过后又再进行 `(type)` 的类型断言。\n\n![image](https://image.eddycjy.com/f14d599ca1763d6e33e98179474929ac.jpg)\n\n这就是我们在编写程序时最常遇见的一个反射使用场景。\n\n## Go reflect\n\nreflect 标准库中，最核心的莫过于 `reflect.Type` 和 `reflect.Value` 类型。而在反射中所使用的方法都围绕着这两者进行，其方法主要含义如下：\n\n![image](https://image.eddycjy.com/da4e21e579da2a049598c5e209209269.jpg)\n\n- `TypeOf` 方法：用于提取入参值的**类型信息**。\n\n- `ValueOf` 方法：用于提取存储的变量的**值信息**。\n\n### reflect.TypeOf\n\n演示程序：\n\n```\nfunc main() {\n\tblog := Blog{\"煎鱼\"}\n\ttypeof := reflect.TypeOf(blog)\n\tfmt.Println(typeof.String())\n}\n```\n\n输出结果：\n\n```\nmain.Blog\n```\n\n从输出结果中可得出 `reflect.TypeOf` 成功解析出 `blog` 变量的类型是 `main.Blog`，也就是连 package 都知道了。通过人识别的角度来看似乎很正常，但程序就不是这样了。\n\n他是怎么知道 “他” 是哪个 package 下的什么呢？我们一起追一下源码看看：\n\n```\nfunc TypeOf(i interface{}) Type {\n\teface := *(*emptyInterface)(unsafe.Pointer(&i))\n\treturn toType(eface.typ)\n}\n```\n\n从源码层面来看，`TypeOf` 方法中主要涉及三块操作，分别如下：\n\n1. 使用 `unsafe.Pointer` 方法获取任意类型且可寻址的指针值。\n\n2. 利用 `emptyInterface` 类型进行强制的 `interface` 类型转换。\n\n3. 调用 `toType` 方法转换为可供外部使用的 `Type` 类型。\n\n而这之中信息量最大的是 `emptyInterface` 结构体中的 `rtype` 类型：\n\n```\ntype rtype struct {\n\tsize       uintptr\n\tptrdata    uintptr \n\thash       uint32 \n\ttflag      tflag \n\talign      uint8  \n\tfieldAlign uint8  \n\tkind       uint8   \n\tequal     func(unsafe.Pointer, unsafe.Pointer) bool\n\tgcdata    *byte  \n\tstr       nameOff \n\tptrToThis typeOff \n}\n```\n\n在使用上最重要的是 `rtype` 类型，其实现了 `Type` 类型的所有接口方法，因此他可以直接作为 `Type` 类型返回，而 `Type` 实际上是一个接口实现，其包含了获取一个类型所必要的所有方法：\n\n```\ntype Type interface {\n\t// 适用于所有类型\n\t// 返回该类型内存对齐后所占用的字节数\n\tAlign() int\n\n\t// 仅作用于 strcut 类型\n\t// 返回该类型内存对齐后所占用的字节数\n\tFieldAlign() int\n\n\t// 返回该类型的方法集中的第 i 个方法\n\tMethod(int) Method\n\n\t// 根据方法名获取对应方法集中的方法\n\tMethodByName(string) (Method, bool)\n\n\t// 返回该类型的方法集中导出的方法的数量。\n\tNumMethod() int\n\n\t// 返回该类型的名称\n\tName() string\n\t...\n}\n```\n\n`Type` 接口的方法是真的多，建议大致过一遍，了解清楚有哪些方法，再针对向看就好。\n\n主体思想是给自己大脑建立一个索引，便于后续快速到 pkg.go.dev 上查询。\n\n### reflect.ValueOf\n\n演示程序：\n\n```\nfunc main() {\n\tvar x float64 = 3.4\n\tfmt.Println(\"value:\", reflect.ValueOf(x))\n}\n```\n\n输出结果：\n\n```\nvalue: 3.4\n```\n\n从输出结果中可得知通过 `reflect.ValueOf` 成功获取到了变量 `x` 的值为 3.4。与 `reflect.TypeOf` 形成一个相匹配，一个负责获取类型，一个负责获取值。\n\n那么 `reflect.ValueOf` 是怎么获取到值的呢，核心源码如下：\n\n```\nfunc ValueOf(i interface{}) Value {\n\tif i == nil {\n\t\treturn Value{}\n\t}\n\n\tescapes(i)\n\n\treturn unpackEface(i)\n}\n\nfunc unpackEface(i interface{}) Value {\n\te := (*emptyInterface)(unsafe.Pointer(&i))\n\tt := e.typ\n\tif t == nil {\n\t\treturn Value{}\n\t}\n\tf := flag(t.Kind())\n\tif ifaceIndir(t) {\n\t\tf |= flagIndir\n\t}\n\treturn Value{t, e.word, f}\n}\n```\n\n从源码层面来看，`ValueOf` 方法中主要涉及如下几个操作：\n\n1. 调用 `escapes` 让变量 `i` 逃逸到堆上。\n\n2. 将变量 `i` 强制转换为 `emptyInterface` 类型。\n\n3. 将所需的信息（其中包含值的具体类型和指针）组装成 `reflect.Value` 类型后返回。\n\n#### 何时类型转换\n\n在调用 `reflect` 进行一系列反射行为时，Go 又是在什么时候进行的类型转换呢。毕竟我们传入的是 `float64`，而函数如参数是 `inetrface` 类型。\n\n查看汇编如下:\n\n```\n$ go tool compile -S main.go                         \n\t...\n\t0x0058 00088 ($GOROOT/src/reflect/value.go:2817)\tLEAQ\ttype.float64(SB), CX\n\t0x005f 00095 ($GOROOT/src/reflect/value.go:2817)\tMOVQ\tCX, reflect.dummy+8(SB)\n\t0x0066 00102 ($GOROOT/src/reflect/value.go:2817)\tPCDATA\t$0, $-2\n\t0x0066 00102 ($GOROOT/src/reflect/value.go:2817)\tCMPL\truntime.writeBarrier(SB), $0\n\t0x006d 00109 ($GOROOT/src/reflect/value.go:2817)\tJNE\t357\n\t0x0073 00115 ($GOROOT/src/reflect/value.go:2817)\tMOVQ\tAX, reflect.dummy+16(SB)\n\t0x007a 00122 ($GOROOT/src/reflect/value.go:2348)\tPCDATA\t$0, $-1\n\t0x007a 00122 ($GOROOT/src/reflect/value.go:2348)\tMOVQ\tCX, reflect.i+64(SP)\n\t0x007f 00127 ($GOROOT/src/reflect/value.go:2348)\tMOVQ\tAX, reflect.i+72(SP)\n\t...\n```\n\n显然，Go 语言会在编译阶段就会完成分析，且进行类型转换。这样子 `reflect` 真正所使用的就是 `interface` 类型了。\n\n### reflect.Set\n\n演示程序：\n\n```\nfunc main() {\n\ti := 2.33\n\tv := reflect.ValueOf(&i)\n\tv.Elem().SetFloat(6.66)\n\tlog.Println(\"value: \", i)\n}\n```\n\n输出结果：\n\n```\nvalue:  6.66\n\n```\n\n从输出结果中，我们可得知在调用 `reflect.ValueOf` 方法后，我们利用 `SetFloat` 方法进行了值变更。核心的方法之一就是 Setter 相关的方法，我们可以一起看看其源码是怎么实现的：\n\n```\nfunc (v Value) Set(x Value) {\n\tv.mustBeAssignable()\n\tx.mustBeExported() // do not let unexported x leak\n\tvar target unsafe.Pointer\n\tif v.kind() == Interface {\n\t\ttarget = v.ptr\n\t}\n\tx = x.assignTo(\"reflect.Set\", v.typ, target)\n\tif x.flag&flagIndir != 0 {\n\t\ttypedmemmove(v.typ, v.ptr, x.ptr)\n\t} else {\n\t\t*(*unsafe.Pointer)(v.ptr) = x.ptr\n\t}\n}\n```\n\n1. 检查反射对象及其字段是否可以被设置。\n\n2. 检查反射对象及其字段是否导出（对外公开）。\n\n3. 调用 `assignTo` 方法创建一个新的反射对象并对原本的反射对象进行覆盖。\n\n4. 根据 `assignTo` 方法所返回的指针值，对当前反射对象的指针进行值的修改。\n\n简单来讲就是，检查是否可以设置，接着创建一个新的对象，最后对其修改。是一个非常标准的赋值流程。\n\n## 反射三大定律\n\nGo 语言中的反射，其归根究底都是在实现三大定律：\n\n1. Reflection goes from interface value to reflection object.\n\n2. Reflection goes from reflection object to interface value.\n\n3. To modify a reflection object, the value must be settable.\n\n我们将针对这核心的三大定律进行介绍和说明，以此来理解 Go 反射里的各种方法是基于什么理念实现的。\n\n### 第一定律 \n\n反射的第一定律是：“反射可以从接口值（interface）得到反射对象”。\n\n示例代码：\n\n\n```\nfunc main() {\n\tvar x float64 = 3.4\n\tfmt.Println(\"type:\", reflect.TypeOf(x))\n}\n```\n\n输出结果：\n\n```\ntype: float64\n```\n\n可能有读者就迷糊了，我明明在代码中传入的变量 `x`，他的类型是 `float64`。怎么就成从接口值得到反射对象了。\n\n其实不然，虽然在代码中我们所传入的变量基本类型是 `float64`，但是 `reflect.TypeOf` 方法入参是 `interface{}`，本质上 Go 语言内部对其是做了类型转换的。这一块会在后面会进一步展开说明。\n\n### 第二定律\n\n反射的第二定律是：“可以从反射对象得到接口值（interface）”。其与第一条定律是相反的定律，可以是互相补充了。\n\n示例代码：\n\n```\nfunc main() {\n\tvo := reflect.ValueOf(3.4)\n\tvf := vo.Interface().(float64)\n\tlog.Println(\"value:\", vf)\n}\n```\n\n输出结果：\n\n```\nvalue: 3.4\n```\n\n可以看到在示例代码中，变量 `vo` 已经是反射对象，然后我们可以利用其所提供的的 `Interface` 方法获取到接口值（interface），并最后强制转换回我们原始的变量类型。\n\n### 第三定律\n\n反射的第三定律是：“要修改反射对象，该值必须可以修改”。第三条定律看上去与第一、第二条均无直接关联，但却是必不可少的，因为反射在工程实践中，目的一就是可以获取到值和类型，其二就是要能够修改他的值。\n\n否则反射出来只能看，不能动，就会造成这个反射很鸡肋。例如：应用程序中的配置热更新，必然会涉及配置项相关的变量变动，大多会使用到反射来变动初始值。\n\n示例代码：\n\n```\nfunc main() {\n\ti := 2.33\n\tv := reflect.ValueOf(&i)\n\tv.Elem().SetFloat(6.66)\n\tlog.Println(\"value: \", i)\n}\n```\n\n输出结果：\n\n```\nvalue:  6.66\n```\n\n单从结果来看，变量 `i` 的值确实从 `2.33` 变成了 `6.66`，似乎非常完美。\n\n但是单看代码，似乎有些 “问题”，怎么设置一个反射值这么 ”麻烦“：\n\n1. 为什么必须传入变量 `i` 的指针引用？\n\n2. 为什么变量 `v` 在设置前还需要 `Elem` 一下？\n\n本叛逆的 Gophper 表示我就不这么设置，行不行呢，会不会出现什么问题：\n\n```\nfunc main() {\n\ti := 2.33\n\treflect.ValueOf(i).SetFloat(6.66)\n\tlog.Println(\"value: \", i)\n}\n```\n\n报错信息：\n\n```\npanic: reflect: reflect.Value.SetFloat using unaddressable value\n\ngoroutine 1 [running]:\nreflect.flag.mustBeAssignableSlow(0x8e)\n        /usr/local/Cellar/go/1.15/libexec/src/reflect/value.go:259 +0x138\nreflect.flag.mustBeAssignable(...)\n        /usr/local/Cellar/go/1.15/libexec/src/reflect/value.go:246\nreflect.Value.SetFloat(0x10b2980, 0xc00001a0b0, 0x8e, 0x401aa3d70a3d70a4)\n        /usr/local/Cellar/go/1.15/libexec/src/reflect/value.go:1609 +0x37\nmain.main()\n        /Users/eddycjy/go-application/awesomeProject/main.go:10 +0xc5\n```\n\n根据上述提示可知，由于使用 “使用不可寻址的值”，因此示例程序无法正常的运作下去。并且这是一个 `reflect` 标准库本身就加以防范了的硬性要求。\n\n这么做的原因在于，Go 语言的函数调用的传递都是值拷贝的，因此若不传指针引用，单纯值传递，那么肯定是无法变动反射对象的源值的。因此 Go 标准库就对其进行了逻辑判断，避免出现问题。\n\n因此期望变更反射对象的源值时，我们必须主动传入对应变量的指针引用，并且调用 `reflect` 标准库的 `Elem` 方法来获取指针所指向的源变量，并且最后调用 `Set` 相关方法来进行设置。 \n\n## 总结\n\n通过本文我们学习并了解了 Go 反射是如何使用，又是基于什么定律设计的。另外我们稍加关注，不难发现 Go 的反射都是基于接口（interface）来实现的，更进一步来讲，Go 语言中运行时的功能很多都是基于接口来实现的。\n\n整体来讲，Go 反射是围绕着三者进行的，分别是 Type、Value 以及 Interface，三者相辅相成，而反射本质上与 Interface​ 存在直接关系，Interface​ 这一块的内容我们也将在后续的文章进行进一步的剖析。\n"
  },
  {
    "path": "content/posts/go/runtimepark.md",
    "content": "---\ntitle: \"Goroutine 一泄露就看到他，这是个什么？\"\ndate: 2021-12-31T12:55:01+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n作为一个 Go 语言的使用大户，常常就有人冷不丁的，一下就泄露了...泄露了啥？\n\n表象来看当然是 goroutine 泄露了，这时候就会有小伙伴开始跑去拉取 PProf。就会看到类似下面这张图：\n\n![](https://files.mdnice.com/user/3610/78c8dadf-4f5c-4dfe-be36-d39bfa9d13d3.png)\n\n重点会看到 `runtime.gopark` 这个函数，在所有的 goroutine 泄露中都会看到有，并且都会是大头。\n\n既然是大头，也就会有许多朋友以为他是泄漏点，在那一顿猛查，那这个函数到底是什么，作用是？ \n\n## runtime.gopark 是何物\n\n想要知道 `runtime.gopark` 函数是作用，最快的办法就是看源码了。其实现细节在 src/runtime/proc.go 文件中。\n\n源代码如下：\n\n```golang\nfunc gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {\n\tmp := acquirem()\n\tgp := mp.curg\n\tstatus := readgstatus(gp)\n\tmp.waitlock = lock\n\tmp.waitunlockf = unlockf\n\tgp.waitreason = reason\n\tmp.waittraceev = traceEv\n\tmp.waittraceskip = traceskip\n\treleasem(mp)\n\t\n\tmcall(park_m)\n}\n```\n\n该函数主要作用有三大点：\n- 调用 `acquirem` 函数：\n    - 获取当前 goroutine 所绑定的 m，设置各类所需数据。\n    - 调用 `releasem` 函数**将当前 goroutine 和其 m 的绑定关系解除**。\n- 调用 `park_m` 函数：\n    - 将当前 goroutine 的状态从 `_Grunning` 切换为 `_Gwaiting`，也就是等待状态。\n    - 删除 m 和当前 goroutine m->curg（简称gp）之间的关联。\n- 调用 `mcall` 函数，仅会在需要进行 goroutiine 切换时会被调用：\n    - 切换当前线程的堆栈，从 g 的堆栈切换到 g0 的堆栈并调用 fn(g) 函数。\n    - 将 g 的当前 PC/SP 保存在 g->sched 中，以便后续调用 goready 函数时可以恢复运行现场。\n\n熟读了其源码后，我们可得知该函数的关键作用就是**将当前的 goroutine 放入等待状态**，这意味着 goroutine 被暂时被搁置了，也就是被运行时调度器暂停了。\n\n## 缘由\n\n回到最初的问题，之所以 goroutine 泄露，你就会看到大量的 `runtime.gopark` 函数，这是因为 goroutine 泄露一般不会单单只是一个 goroutine，肯定是会有多个的。\n\n同时这些 goroutine 在调用了 `runtime.gopark` 函数后都被暂停了，也就是进入休眠状态，自然而然也就停留在此。\n\n直至满足条件后再被 `runtime.goready` 函数唤醒，该函数会将已准备就绪的 goroutine 切换状态，再加入运行队列，等待调度器的新一轮调度。\n\n## 思考\n\n前几天就有读者在我的 Go 读者群中咨询了下述问题，也和  `runtime.gopark` 函数有关。问题如下：\n\n![](https://files.mdnice.com/user/3610/eb5d0483-8193-4adf-881e-490825d137fe.png)\n\n经过上述的分析，显然 `runtime.gopark` 不是 goroutine 的一种状态，导致 goroutine 状态变更只是他的执行过程中所涉及到，产生的一个结果。\n\n而 goroutine 的状态一共有 9 种，有兴趣的小伙伴可以了解。如下：\n\n状态  | 含义\n---|---| ---\n_Gidle  | 刚刚被分配，还没有进行初始化。\n_Grunnable  | 已经在运行队列中，还没有执行用户代码。\n_Grunning | 不在运行队列里中，已经可以执行用户代码，此时已经分配了 M 和 P。\n_Gsyscall | 正在执行系统调用，此时分配了 M。\n_Gwaiting | 在运行时被阻止，没有执行用户代码，也不在运行队列中，此时它正在某处阻塞等待中。\n_Gmoribund_unused | 尚未使用，但是在 gdb 中进行了硬编码。\n_Gdead | 尚未使用，这个状态可能是刚退出或是刚被初始化，此时它并没有执行用户代码，有可能有也有可能没有分配堆栈。\n_Genqueue_unused | 尚未使用。\n_Gcopystack | 正在复制堆栈，并没有执行用户代码，也不在运行队列中。\n\n## 总结\n\n在今天这篇文章中，我们介绍了大家最常碰到的 goroutine 泄露，而在泄露后最关心的 `runtime.gopark` 函数的意义，我们从源码再到作用进行了一轮剖析。\n\n下次如果再有人问你 `runtime.gopark` 是干嘛用的，就可以愉快的把这篇文章甩给他，分享你的知识啦 ：）"
  },
  {
    "path": "content/posts/go/rust-php.md",
    "content": "---\ntitle: \"Rust 内讧，PHP 主力淡出？Go：好好放假\"\ndate: 2021-12-31T12:55:20+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n现在已经是 2021 年的 Q4 季度了，许多职场人都忙的飞起，被 PPT 各种轰炸。\n\n在上周，看到几门语言的社区都发生了一些大事，煎鱼表示大受震撼，来说几句我的看法。\n\n## PHP 主力淡出\n\n在 11 月 23 日，看到 PHP 的主力开发 [Nikita Popov](https://twitter.com/nikita_ppv \"Nikita Popov\") 在论坛上发文宣布将**不再以专业身份从事 PHP 工作**，投入到 PHP 开发中的时间将会大幅度减少。\n\n![](https://files.mdnice.com/user/3610/82ba0eaf-fa69-4bee-9cbc-ec385d3b9793.png)\n\n根据 Jetbrains 分享的消息来看，可得知 Nikita Popov 在高中（2011 年）时就开始参与 PHP 开发，截止现在已有 10 年经验了。\n\n![](https://files.mdnice.com/user/3610/93d9d4c9-9863-42fe-9d2c-f5ada62210c2.png)\n\n他离开的原因，我看了一遍帖子，众说纷纭，业内猜测有以下两个观点：\n- 迫于生活压力，过多精力投入维护开源项目收入不高。\n- PHP 新版本特性受阻等原因，把精力从 PHP 转到 LLVM。\n\n也因此，PHP 社区加速宣布成立 PHP 基金会《[The New Life of PHP – The PHP Foundation](https://blog.jetbrains.com/phpstorm/2021/11/the-php-foundation/ \"The New Life of PHP – The PHP Foundation\")》。基金会所募集的资金，将会用于资助开发者在 PHP 上工作。\n\n基金会的宣发上，讲的很清楚，为的就是**避免再发生失去 PHP 的主要贡献者**的事情发生，这影响是非常之巨大的。\n\n果然，面包和理想，还是要有权衡，Typora 都开始收费了。\n\n## Rust 社区内讧\n\n在 11 月 22 日，Rust 社区的审核团队集体辞职（Moderation Team Resignation），立即生效。宣布用的 pr 如下图：\n\n![](https://files.mdnice.com/user/3610/a4359504-a128-43e1-9b36-b855307b8825.png)\n\n团队辞职的原因是：**为了抗议 Rust 核心团队将自己置于对任何人都不负责任的地位，只对自己负责**。\n\n一时间，吃瓜众说纷纭。但没有人再给出官方解释了。业内猜测有以下两个观点：\n- 在 Rust 运作上：认为与亚马逊正在试图侵蚀 Rust 有关。包括：雇佣了语言团队、编译器团队负责人等。\n- 在 Rust 基金会上：亚马逊决定不设立 Rust 基金会 ED，主席将在 Rust 基金会中拥有巨大的权力。\n\n截止 11 月 27 号，这件事感觉已经见不到 “真相” 了。因为 [pr](https://github.com/rust-lang/team/pull/671 \"pr\") 和 [reddit](https://www.reddit.com/r/rust/comments/qzme1z/moderation_team_resignation/ \"reddit\") 上的讨论帖均已锁定，没有正式回复。\n\n总感觉有种黑幕的感觉？\n\n## 总结\n\n看了这两起社区重大异常后，再对比看 Go 社区，似乎又比较的温和。毕竟 Go 一开始的诞生，就来源于 Google 大佬们在职期间对既有使用语言的不满。\n\n这么多年了，他们也一直没有离职。Google 也提供了不少的时间和资金给 Go 核心团队做宣传和维护社区，相对安全，还**能定期休假 2 周**（静默期），真是太羡慕了。\n\n但在反面来看，也有很多人嫌弃 Go 背后的靠山是 Google，你怎么看呢？\n\n欢迎在评论区留言和交流：）"
  },
  {
    "path": "content/posts/go/site-history.md",
    "content": "---\ntitle: \"分久必合，golang.org 将成为历史！\"\ndate: 2021-12-31T12:55:03+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n这两天看到官方博客的《[Tidying up the Go web experience](https://go.dev/blog/tidy-web \"Tidying up the Go web experience\")》，已经明确了优化 Go 站点的计划和安排了，为此今天和大家分享这一个好消息。\n\n在之前 Go 官方推出了新的站点 go.dev，一个新的 Go 开发者中心：\n\n![go.dev](https://files.mdnice.com/user/3610/c70f1330-64da-4b65-8988-f9dbe6b1df05.png)\n\n以及提供给开发者查询 Go 包（package）和模块（module）信息的配套网站 pkg.go.dev：\n\n![pkg.go.dev](https://files.mdnice.com/user/3610/41df22df-78e2-4b15-83d7-c8e38fcaba54.png)\n\n看上去似乎美好，但其实原有的 golang.org 在继续提供 Go 发行版下载、文档和标准库的软件包的资料。\n\n而其他 Go 网站，例如：blog.golang.org、play.golang.org、talks.golang.org 以及tour.golang.org，又都拥有额外的 Go 资料。这一切都有些零散和混乱。\n\n在软件发展来讲，这种被称之为 ”绞杀者“ 模式：\n\n![图来自网络](https://files.mdnice.com/user/3610/4645e874-771a-452a-b4ba-e41d8ffe5673.png)\n\n也就是旧系统不断地被新系统取代，在两边都还存在流量时，会保持两边的运行。随着时间的推移，新系统不断地取代老系统，最终完全取代。\n\nGo 官方**计划在接下来的一两个月里，将把 golang.org 网站合并成一个统一的网站**，也就是在 go.dev 上。\n\n所有的现有 URL（例如：Go 标准库、Go 博客等）都会重定向到新的 URL 地址中，不会出现破坏性修改（无法访问），一切按计划都是兼容的。\n\n\n![](https://files.mdnice.com/user/3610/0708fcf4-90b2-4451-8a3d-5bf5ce2a1dfa.png)\n\n\n咱们将会有一个更统一的 Go 网站，部分 Go 初学者也不再需要辛辛苦苦再翻山倒海先找个天梯了，因为是**可以直接访问的**，这是一种利好！\n\n不得不感叹语句，Russ Cox 推动事情的能力真是杠杠的，虽然也会引来不少争议和讨论，但我们依然可以从中学习到好的部分！\n\n大家**有没有什么也想让 Go 团队优化的呢，欢迎在评论区交流和讨论**！"
  },
  {
    "path": "content/posts/go/slice/2018-12-11-slice.md",
    "content": "---\n\ntitle:      \"深入理解 Go Slice\"\ndate:       2018-12-11 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - 源码分析\n---\n\n![image](https://s2.ax1x.com/2020/02/27/3dXSeA.png)\n\n## 是什么\n\n在 Go 中，Slice（切片）是抽象在 Array（数组）之上的特殊类型。为了更好地了解 Slice，第一步需要先对 Array 进行理解。深刻了解 Slice 与 Array 之间的区别后，就能更好的对其底层一番摸索 😄\n\n## 用法\n\n### Array\n\n```go\nfunc main() {\n\tnums := [3]int{}\n\tnums[0] = 1\n\n\tn := nums[0]\n\tn = 2\n\n\tfmt.Printf(\"nums: %v\\n\", nums)\n\tfmt.Printf(\"n: %d\\n\", n)\n}\n```\n\n我们可得知在 Go 中，数组类型需要指定长度和元素类型。在上述代码中，可得知 `[3]int{}` 表示 3 个整数的数组，并进行了初始化。底层数据存储为一段连续的内存空间，通过固定的索引值（下标）进行检索\n\n![image](https://s2.ax1x.com/2020/02/27/3dXASS.png)\n\n数组在声明后，其元素的初始值（也就是零值）为 0。并且该变量可以直接使用，不需要特殊操作\n\n同时数组的长度是固定的，它的长度是类型的一部分，因此 `[3]int` 和 `[4]int` 在类型上是不同的，不能称为 “一个东西”\n\n#### 输出结果\n\n```\nnums: [1 0 0]\nn: 2\n```\n\n### Slice\n\n```go\nfunc main() {\n\tnums := [3]int{}\n\tnums[0] = 1\n\n\tdnums := nums[:]\n\n\tfmt.Printf(\"dnums: %v\", dnums)\n}\n```\n\nSlice 是对 Array 的抽象，类型为 `[]T`。在上述代码中，`dnums` 变量通过 `nums[:]` 进行赋值。需要注意的是，Slice 和 Array 不一样，它不需要指定长度。也更加的灵活，能够自动扩容\n\n## 数据结构\n\n![image](https://s2.ax1x.com/2020/02/27/3wmr3F.png)\n\n```go\ntype slice struct {\n\tarray unsafe.Pointer\n\tlen   int\n\tcap   int\n}\n```\n\nSlice 的底层数据结构共分为三部分，如下：\n\n- array：指向所引用的数组指针（`unsafe.Pointer` 可以表示任何可寻址的值的指针）\n- len：长度，当前引用切片的元素个数\n- cap：容量，当前引用切片的容量（底层数组的元素总数）\n\n在实际使用中，cap 一定是大于或等于 len 的。否则会导致 panic\n\n### 示例\n\n为了更好的理解，我们回顾上小节的代码便于演示，如下：\n\n```go\nfunc main() {\n\tnums := [3]int{}\n\tnums[0] = 1\n\n\tdnums := nums[:]\n\n\tfmt.Printf(\"dnums: %v\", dnums)\n}\n```\n\n![image](https://s2.ax1x.com/2020/02/27/3wmoge.png)\n\n在代码中，可观察到 `dnums := nums[:]`，这段代码确定了 Slice 的 Pointer 指向数组，且 len 和 cap 都为数组的基础属性。与图示表达一致\n\n### len、cap 不同\n\n```go\nfunc main() {\n\tnums := [3]int{}\n\tnums[0] = 1\n\n\tdnums := nums[0:2]\n\n\tfmt.Printf(\"dnums: %v, len: %d, cap: %d\", dnums, len(dnums), cap(dnums))\n}\n```\n\n![image](https://s2.ax1x.com/2020/02/27/3wmxC8.png)\n\n#### 输出结果\n\n```\ndnums: [1 0], len: 2, cap: 3\n```\n\n显然，在这里指定了 `Slice[0:2]`，因此 len 为所引用元素的个数，cap 为所引用的数组元素总个数。与期待一致 😄\n\n## 创建\n\nSlice 的创建有两种方式，如下：\n\n- `var []T` 或 `[]T{}`\n- `func make（[] T，len，cap）[] T`\n\n可以留意 make 函数，我们都知道 Slice 需要指向一个 Array。那 make 是怎么做的呢？\n\n它会在调用 make 的时候，分配一个数组并返回引用该数组的 Slice\n\n```go\nfunc makeslice(et *_type, len, cap int) slice {\n\tmaxElements := maxSliceCap(et.size)\n\tif len < 0 || uintptr(len) > maxElements {\n\t\tpanic(errorString(\"makeslice: len out of range\"))\n\t}\n\n\tif cap < len || uintptr(cap) > maxElements {\n\t\tpanic(errorString(\"makeslice: cap out of range\"))\n\t}\n\n\tp := mallocgc(et.size*uintptr(cap), et, true)\n\treturn slice{p, len, cap}\n}\n```\n\n- 根据传入的 Slice 类型，获取其类型能够申请的最大容量大小\n- 判断 len 是否合规，检查是否在 0 < x < maxElements 范围内\n- 判断 cap 是否合规，检查是否在 len < x < maxElements 范围内\n- 申请 Slice 所需的内存空间对象。若为大型对象（大于 32 KB）则直接从堆中分配\n- 返回申请成功的 Slice 内存地址和相关属性（默认返回申请到的内存起始地址）\n\n## 扩容\n\n当使用 Slice 时，若存储的元素不断增长（例如通过 append）。当条件满足扩容的策略时，将会触发自动扩容\n\n那么分别是什么规则呢？让我们一起看看源码是怎么说的 😄\n\n### zerobase\n\n```go\nfunc growslice(et *_type, old slice, cap int) slice {\n\t...\n\tif et.size == 0 {\n\t\tif cap < old.cap {\n\t\t\tpanic(errorString(\"growslice: cap out of range\"))\n\t\t}\n\n\t\treturn slice{unsafe.Pointer(&zerobase), old.len, cap}\n\t}\n    ...\n}\n```\n\n当 Slice size 为 0 时，若将要扩容的容量比原本的容量小，则抛出异常（也就是不支持缩容操作）。否则，将重新生成一个新的 Slice 返回，其 Pointer 指向一个 0 byte 地址（不会保留老的 Array 指向）\n\n### 扩容 - 计算策略\n\n```go\nfunc growslice(et *_type, old slice, cap int) slice {\n    ...\n    newcap := old.cap\n\tdoublecap := newcap + newcap\n\tif cap > doublecap {\n\t\tnewcap = cap\n\t} else {\n\t\tif old.len < 1024 {\n\t\t\tnewcap = doublecap\n\t\t} else {\n\t\t\tfor 0 < newcap && newcap < cap {\n\t\t\t\tnewcap += newcap / 4\n\t\t\t}\n\t\t\t...\n\t\t}\n\t}\n\t...\n}\n```\n\n- 若 Slice cap 大于 doublecap，则扩容后容量大小为 新 Slice 的容量（超了基准值，我就只给你需要的容量大小）\n- 若 Slice len 小于 1024 个，在扩容时，增长因子为 1（也就是 3 个变 6 个）\n- 若 Slice len 大于 1024 个，在扩容时，增长因子为 0.25（原本容量的四分之一）\n\n注：也就是小于 1024 个时，增长 2 倍。大于 1024 个时，增长 1.25 倍\n\n### 扩容 - 内存策略\n\n```go\nfunc growslice(et *_type, old slice, cap int) slice {\n    ...\n    var overflow bool\n\tvar lenmem, newlenmem, capmem uintptr\n\tconst ptrSize = unsafe.Sizeof((*byte)(nil))\n\tswitch et.size {\n\tcase 1:\n\t\tlenmem = uintptr(old.len)\n\t\tnewlenmem = uintptr(cap)\n\t\tcapmem = roundupsize(uintptr(newcap))\n\t\toverflow = uintptr(newcap) > _MaxMem\n\t\tnewcap = int(capmem)\n\t    ...\n\t}\n\n\tif cap < old.cap || overflow || capmem > _MaxMem {\n\t\tpanic(errorString(\"growslice: cap out of range\"))\n\t}\n\n\tvar p unsafe.Pointer\n\tif et.kind&kindNoPointers != 0 {\n\t\tp = mallocgc(capmem, nil, false)\n\t\tmemmove(p, old.array, lenmem)\n\t\tmemclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)\n\t} else {\n\t\tp = mallocgc(capmem, et, true)\n\t\tif !writeBarrier.enabled {\n\t\t\tmemmove(p, old.array, lenmem)\n\t\t} else {\n\t\t\tfor i := uintptr(0); i < lenmem; i += et.size {\n\t\t\t\ttypedmemmove(et, add(p, i), add(old.array, i))\n\t\t\t}\n\t\t}\n\t}\n\t...\n}\n```\n\n1、获取老 Slice 长度和计算假定扩容后的新 Slice 元素长度、容量大小以及指针地址（用于后续操作内存的一系列操作）\n\n2、确定新 Slice 容量大于老 Sice，并且新容量内存小于指定的最大内存、没有溢出。否则抛出异常\n\n3、若元素类型为 `kindNoPointers`，也就是**非指针**类型。则在老 Slice 后继续扩容\n\n- 第一步：根据先前计算的 `capmem`，在老 Slice cap 后继续申请内存空间，其后用于扩容\n- 第二步：将 old.array 上的 n 个 bytes（根据 lenmem）拷贝到新的内存空间上\n- 第三步：新内存空间（p）加上新 Slice cap 的容量地址。最终得到完整的新 Slice cap 内存地址 `add(p, newlenmem)` （ptr）\n- 第四步：从 ptr 开始重新初始化 n 个 bytes（capmem-newlenmem）\n\n注：那么问题来了，为什么要重新初始化这块内存呢？这是因为 ptr 是未初始化的内存（例如：可重用的内存，一般用于新的内存分配），其可能包含 “垃圾”。因此在这里应当进行 “清理”。便于后面实际使用（扩容）\n\n4、不满足 3 的情况下，重新申请并初始化一块内存给新 Slice 用于存储 Array\n\n5、检测当前是否正在执行 GC，也就是当前是否启用 Write Barrier（写屏障），若**启用**则通过 `typedmemmove` 方法，利用指针运算**循环拷贝**。否则通过 `memmove` 方法采取**整体拷贝**的方式将 lenmem 个字节从 old.array 拷贝到 ptr，以此达到更高的效率\n\n注：一般会在 GC 标记阶段启用 Write Barrier，并且 Write Barrier 只针对指针启用。那么在第 5 点中，你就不难理解为什么会有两种截然不同的处理方式了\n\n#### 小结\n\n这里需要注意的是，扩容时的内存管理的选择项，如下：\n\n- 翻新扩展：当前元素为 `kindNoPointers`，将在老 Slice cap 的地址后继续申请空间用于扩容\n- 举家搬迁：重新申请一块内存地址，整体迁移并扩容\n\n### 两个小 “陷阱”\n\n#### 一、同根\n\n```go\nfunc main() {\n\tnums := [3]int{}\n\tnums[0] = 1\n\n\tfmt.Printf(\"nums: %v , len: %d, cap: %d\\n\", nums, len(nums), cap(nums))\n\n\tdnums := nums[0:2]\n\tdnums[0] = 5\n\n\tfmt.Printf(\"nums: %v ,len: %d, cap: %d\\n\", nums, len(nums), cap(nums))\n\tfmt.Printf(\"dnums: %v, len: %d, cap: %d\\n\", dnums, len(dnums), cap(dnums))\n}\n```\n\n输出结果：\n\n```\nnums: [1 0 0] , len: 3, cap: 3\nnums: [5 0 0] ,len: 3, cap: 3\ndnums: [5 0], len: 2, cap: 3\n```\n\n在**未扩容前**，Slice array 指向所引用的 Array。因此在 Slice 上的变更。会直接修改到原始 Array 上（两者所引用的是同一个）\n\n![image](https://s2.ax1x.com/2020/02/27/3wnibn.png)\n\n#### 二、时过境迁\n\n随着 Slice 不断 append，内在的元素越来越多，终于触发了扩容。如下代码：\n\n```go\nfunc main() {\n\tnums := [3]int{}\n\tnums[0] = 1\n\n\tfmt.Printf(\"nums: %v , len: %d, cap: %d\\n\", nums, len(nums), cap(nums))\n\n\tdnums := nums[0:2]\n\tdnums = append(dnums, []int{2, 3}...)\n\tdnums[1] = 1\n\n\tfmt.Printf(\"nums: %v ,len: %d, cap: %d\\n\", nums, len(nums), cap(nums))\n\tfmt.Printf(\"dnums: %v, len: %d, cap: %d\\n\", dnums, len(dnums), cap(dnums))\n}\n```\n\n输出结果：\n\n```\nnums: [1 0 0] , len: 3, cap: 3\nnums: [1 0 0] ,len: 3, cap: 3\ndnums: [1 1 2 3], len: 4, cap: 6\n```\n\n往 Slice append 元素时，若满足扩容策略，也就是假设插入后，原本数组的容量就超过最大值了\n\n这时候内部就会重新申请一块内存空间，将原本的元素**拷贝**一份到新的内存空间上。此时其与原本的数组就没有任何关联关系了，**再进行修改值也不会变动到原始数组**。这是需要注意的\n\n![image](https://s2.ax1x.com/2020/02/27/3wnAU0.png)\n\n## 复制\n\n### 原型\n\n```go\nfunc copy（dst，src [] T）int\n```\n\ncopy 函数将数据从**源 Slice**复制到**目标 Slice**。它返回复制的元素数。\n\n### 示例\n\n```go\nfunc main() {\n\tdst := []int{1, 2, 3}\n\tsrc := []int{4, 5, 6, 7, 8}\n\tn := copy(dst, src)\n\n\tfmt.Printf(\"dst: %v, n: %d\", dst, n)\n}\n```\n\ncopy 函数支持在不同长度的 Slice 之间进行复制，若出现长度不一致，在复制时会按照最少的 Slice 元素个数进行复制\n\n那么在源码中是如何完成复制这一个行为的呢？我们来一起看看源码的实现，如下：\n\n```go\nfunc slicecopy(to, fm slice, width uintptr) int {\n\tif fm.len == 0 || to.len == 0 {\n\t\treturn 0\n\t}\n\n\tn := fm.len\n\tif to.len < n {\n\t\tn = to.len\n\t}\n\n\tif width == 0 {\n\t\treturn n\n\t}\n\n\t...\n\n\tsize := uintptr(n) * width\n\tif size == 1 {\n\t\t*(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer\n\t} else {\n\t\tmemmove(to.array, fm.array, size)\n\t}\n\treturn n\n}\n```\n\n- 若源 Slice 或目标 Slice 存在长度为 0 的情况，则直接返回 0（因为压根不需要执行复制行为）\n- 通过对比两个 Slice，获取最小的 Slice 长度。便于后续操作\n- 若 Slice 只有一个元素，则直接利用指针的特性进行转换\n- 若 Slice 大于一个元素，则从 `fm.array` 复制 `size` 个字节到 `to.array` 的地址处（会覆盖原有的值）\n\n## \"奇特\"的初始化\n\n在 Slice 中流传着两个传说，分别是 Empty 和 Nil Slice，接下来让我们看看它们的小区别 🤓\n\n### Empty\n\n```go\nfunc main() {\n\tnums := []int{}\n\trenums := make([]int, 0)\n\n\tfmt.Printf(\"nums: %v, len: %d, cap: %d\\n\", nums, len(nums), cap(nums))\n\tfmt.Printf(\"renums: %v, len: %d, cap: %d\\n\", renums, len(renums), cap(renums))\n}\n```\n\n输出结果：\n\n```\nnums: [], len: 0, cap: 0\nrenums: [], len: 0, cap: 0\n```\n\n### Nil\n\n```go\nfunc main() {\n    var nums []int\n}\n```\n\n输出结果：\n\n```\nnums: [], len: 0, cap: 0\n```\n\n### 想一想\n\n乍一看，Empty Slice 和 Nil Slice 好像一模一样？不管是 len，还是 cap 都为 0。好像没区别？我们再看看如下代码：\n\n```go\nfunc main() {\n\tvar nums []int\n\trenums := make([]int, 0)\n\tif nums == nil {\n\t\tfmt.Println(\"nums is nil.\")\n\t}\n\tif renums == nil {\n\t\tfmt.Println(\"renums is nil.\")\n\t}\n}\n```\n\n你觉得输出结果是什么呢？你可能已经想到了，最终的输出结果：\n\n```\nnums is nil.\n```\n\n#### 为什么\n\n##### Empty\n\n![image](https://s2.ax1x.com/2020/02/27/3wncRS.png)\n\n##### Nil\n\n![image](https://s2.ax1x.com/2020/02/27/3wn5aq.png)\n\n从图示中可以看出来，两者有本质上的区别。其底层数组的指向指针是不一样的，Nil Slice 指向的是 nil，Empty Slice 指向的是实际存在的空数组地址\n\n你可以认为，Nil Slice 代指不存在的 Slice，Empty Slice 代指空集合。两者所代表的意义是完全不同的\n\n## 总结\n\n通过本文，可得知 Go Slice 相当灵活。不需要你手动扩容，也不需要你关注加多少减多少。对 Array 是动态引用，是 Go 类型的一个极大的补充，也因此在应用中使用的更多、更便捷\n\n虽然有个别要注意的 “坑”，但其实是合理的。你觉得呢？😄\n"
  },
  {
    "path": "content/posts/go/slice/2019-01-06-why-slice-max.md",
    "content": "---\n\ntitle:      \"Go Slice 最大容量大小是怎么来的\"\ndate:       2019-01-06 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - 源码分析\n---\n\n![image](https://s2.ax1x.com/2020/02/27/3wnHRU.png)\n\n## 前言\n\n在《深入理解 Go Slice》中，我们提到了 “根据其类型大小去获取能够申请的最大容量大小” 的处理逻辑。今天我们将更深入地去探究一下，底层到底做了什么东西，涉及什么知识点？\n\nGo Slice 对应代码如下：\n\n```go\nfunc makeslice(et *_type, len, cap int) slice {\n\tmaxElements := maxSliceCap(et.size)\n\tif len < 0 || uintptr(len) > maxElements {\n\t\t...\n\t}\n\n\tif cap < len || uintptr(cap) > maxElements {\n\t\t...\n\t}\n\n\tp := mallocgc(et.size*uintptr(cap), et, true)\n\treturn slice{p, len, cap}\n}\n```\n\n根据想要追寻的逻辑，定位到了 `maxSliceCap` 方法，它会根据**当前类型的大小获取到了所允许的最大容量大小**来进行阈值判断，也就是安全检查。这是浅层的了解，我们继续追下去看看还做了些什么？\n\n## maxSliceCap\n\n```go\nfunc maxSliceCap(elemsize uintptr) uintptr {\n\tif elemsize < uintptr(len(maxElems)) {\n\t\treturn maxElems[elemsize]\n\t}\n\treturn maxAlloc / elemsize\n}\n```\n\n## maxElems\n\n```go\nvar maxElems = [...]uintptr{\n\t^uintptr(0),\n\tmaxAlloc / 1, maxAlloc / 2, maxAlloc / 3, maxAlloc / 4,\n\tmaxAlloc / 5, maxAlloc / 6, maxAlloc / 7, maxAlloc / 8,\n\tmaxAlloc / 9, maxAlloc / 10, maxAlloc / 11, maxAlloc / 12,\n\tmaxAlloc / 13, maxAlloc / 14, maxAlloc / 15, maxAlloc / 16,\n\tmaxAlloc / 17, maxAlloc / 18, maxAlloc / 19, maxAlloc / 20,\n\tmaxAlloc / 21, maxAlloc / 22, maxAlloc / 23, maxAlloc / 24,\n\tmaxAlloc / 25, maxAlloc / 26, maxAlloc / 27, maxAlloc / 28,\n\tmaxAlloc / 29, maxAlloc / 30, maxAlloc / 31, maxAlloc / 32,\n}\n```\n\n`maxElems` 是包含一些预定义的切片最大容量值的查找表，索引是切片元素的类型大小。而值看起来 “奇奇怪怪” 不大眼熟，都是些什么呢。主要是以下三个核心点：\n\n- ^uintptr(0)\n- maxAlloc\n- maxAlloc / typeSize\n\n### ^uintptr(0)\n\n```go\nfunc main() {\n\tlog.Printf(\"uintptr: %v\\n\", uintptr(0))\n\tlog.Printf(\"^uintptr: %v\\n\", ^uintptr(0))\n}\n```\n\n输出结果：\n\n```\n2019/01/05 17:51:52 uintptr: 0\n2019/01/05 17:51:52 ^uintptr: 18446744073709551615\n```\n\n我们留意一下输出结果，比较神奇。取反之后为什么是 18446744073709551615 呢？\n\n### uintptr 是什么\n\n在分析之前，我们要知道 uintptr 的本质（真面目），也就是它的类型是什么，如下：\n\n```go\ntype uintptr uintptr\n```\n\nuintptr 的类型是自定义类型，接着找它的真面目，如下：\n\n```\n#ifdef _64BIT\ntypedef\tuint64\t\tuintptr;\n#else\ntypedef\tuint32\t\tuintptr;\n#endif\n```\n\n通过对以上代码的分析，可得出以下结论：\n\n- 在 32 位系统下，uintptr 为 uint32 类型，占用大小为 4 个字节\n- 在 64 位系统下，uintptr 为 uint64 类型，占用大小为 8 个字节\n\n### ^uintptr 做了什么事\n\n^ 位运算符的作用是**按位异或**，如下：\n\n```go\nfunc main() {\n\tlog.Println(^1)\n\tlog.Println(^uint64(0))\n}\n```\n\n输出结果：\n\n```\n2019/01/05 20:44:49 -2\n2019/01/05 20:44:49 18446744073709551615\n```\n\n接下来我们分析一下，这两段代码都做了什么事情呢\n\n#### ^1\n\n二进制：0001\n\n按位取反：1110\n\n该数为有符号整数，最高位为符号位。低三位为表示数值。按位取反后为 1110，根据先前的说明，最高位为 1，因此表示为 -。取反后 110 对应十进制 -2\n\n#### ^uint64(0)\n\n二进制：0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000\n\n按位取反：1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111\n\n该数为无符号整数，该位取反后得到十进制值为：18446744073709551615\n\n这个值是不是看起来很眼熟呢？没错，就是 `^uintptr(0)` 的值。也印证了其底层数据类型为 uint64 的事实 （本机为 64 位）。同时它又代表如下：\n\n- math.MaxUint64\n- 2 的 64 次方减 1\n\n### maxAlloc\n\n```go\nconst GoarchMips = 0\nconst GoarchMipsle = 0\nconst GoarchWasm = 0\n\n...\n\n_64bit = 1 << (^uintptr(0) >> 63) / 2\n\nheapAddrBits = (_64bit*(1-sys.GoarchWasm))*48 + (1-_64bit+sys.GoarchWasm)*(32-(sys.GoarchMips+sys.GoarchMipsle))\n\nmaxAlloc = (1 << heapAddrBits) - (1-_64bit)*1\n```\n\n`maxAlloc` 是**允许用户分配的最大虚拟内存空间**。在 64 位，理论上可分配最大 `1 << heapAddrBits` 字节。在 32 位，最大可分配小于 `1 << 32` 字节\n\n在本文，仅需了解它承载的是什么就好了。具体的在以后内存管理的文章再讲述\n\n注：该变量在 go 10.1 为 `_MaxMem`，go 11.4 已改为 `maxAlloc`。相关的 `heapAddrBits` 计算方式也有所改变\n\n### maxAlloc / typeSize\n\n我们再次回顾 `maxSliceCap` 的逻辑代码，这次重点放在控制逻辑，如下：\n\n```go\n// func makeslice\nmaxElements := maxSliceCap(et.size)\n\n...\n\n// func maxSliceCap\nif elemsize < uintptr(len(maxElems)) {\n\treturn maxElems[elemsize]\n}\nreturn maxAlloc / elemsize\n```\n\n通过这段代码和 Slice 上下文逻辑，可得知在想得到该类型的最大容量大小时。会根据对应的类型大小去查找表查找索引（索引为类型大小，摆放顺序是有考虑原因的）。“迫不得已的情况下” 才会手动的计算它的值，最终计算得到的内存字节大小都为该类型大小的整数倍\n\n查找表的设置，更像是一个优化逻辑。减少常用的计算开销 :)\n\n## 总结\n\n通过本文的分析，可得出 Slice 所允许申请的最大容量大小，与当前**值类型**和当前**平台位数**有直接关系\n\n## 最后\n\n本文与[《有点不安全却又一亮的 Go unsafe.Pointer》](https://github.com/EDDYCJY/blog/blob/master/golang/pkg/2018-12-15-%E6%9C%89%E7%82%B9%E4%B8%8D%E5%AE%89%E5%85%A8%E5%8D%B4%E5%8F%88%E4%B8%80%E4%BA%AE%E7%9A%84Go-unsafe-Pointer.md)一同属于[《深入理解 Go Slice》](https://github.com/EDDYCJY/blog/blob/master/golang/pkg/2018-12-11-%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Go-Slice.md)的关联章节。如果你在阅读源码时，对这些片段有疑惑。记得想尽办法深究下去，搞懂它\n\n短短的一句话其实蕴含着不少知识点，希望这篇文章恰恰好可以帮你解惑\n\n注：本文 Go 代码基于版本 11.4\n"
  },
  {
    "path": "content/posts/go/slice-discuss.md",
    "content": "---\ntitle: \"Go 切片这道题，吵了一个下午！\"\ndate: 2021-12-31T12:55:06+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n前几天听到咱 Go 读者交流群里的小伙伴私聊我，表示他们在群里一直在讨论一个问题 slice 相关的问题，众说纷纭。\n\n![来自煎鱼的聊天记录](https://files.mdnice.com/user/3610/f2cedcdf-8f6e-42f9-9009-6f1ee0f74f91.png)\n\n今天和各位小伙伴们一起来研究一下，避免后续又踩一遍坑，共同进步！\n\n## 问题代码\n\n引起群内大范围讨论的代码如下：\n\n```golang\nfunc main() {\n\tsl := make([]int, 0, 10)\n\tvar appenFunc = func(s []int) {\n\t\ts = append(s, 10, 20, 30)\n\t\tfmt.Println(s)\n\t}\n\tfmt.Println(sl)\n\tappenFunc(sl)\n\tfmt.Println(sl)\n\tfmt.Println(sl[:10])\n}\n```\n\n你认为程序的输出结果是什么？\n\n是如下的答案：\n\n```\n[]\n[10 20 30]\n[]\n[]\n```\n\n对吗？\n\n看上去很有道理，但错了。正确的结果是：\n\n```\n[]\n[10 20 30]\n[]\n[10 20 30 0 0 0 0 0 0 0]\n```\n\n这下可把大家整懵了，为什么输出 `sl` 和 `sl[:10]` 的结果差别这么大，这与预期的输出结果不一致。\n\n群内小伙伴的问题更明确了，疑惑点是：\n\n```golang\n\tfmt.Println(sl)     \n\tfmt.Println(sl[:10]) \n```\n\n上述代码中，**为什么第一个 `sl` 打印结果是空的，第二个 `sl` 给索引位置就能打印出来**？\n\n也有小伙伴不断在尝试 `sl[:10]` 以外的输出，有没有因为一些边界值改变而导致不行。\n\n例如：\n\n```golang\nfmt.Println(sl[:])\n```\n\n你认为这个对应的输出结果是什么？\n\n是如下的答案：\n\n```\n[10 20 30 0 0 0 0 0 0 0]\n```\n\n对吗？\n\n看上去很有道理，但错了。正确的结果是：\n\n```\n[]\n```\n\n是没有任何元素输出，这下大家更懵了。为什么 `sl[:]` 的输出结果为空？\n\n再看看变量 `sl` 的长度和容量：\n\n```\nfmt.Println(len(sl), cap(sl))\n```\n\n输出结果：\n\n```\n0 10\n```\n\n长度竟然是 0 ...迷了？\n\n## 挖掘原因\n\n### 三个问题\n在研究了问题代码的表象后，我们要进一步的挖掘问题的原因。\n\n请思考如下三个问题：\n1. 为什么打印 `sl[:10]` 时，结果包含了 10 个元素，还包含了函数闭包中插入的 10, 20, 30，之间有什么关系？\n2. 为什么打印 `sl` 变量时，结果为空？\n3. 为什么打印 `sl[:]` 时，结果为空。但打印 `sl[:10]` 就正常输出？\n\n### 了解底层\n\n要分析起源，我们就必须要再提到 slice（切片）的底层实现，slice 底层存储的数据结构指向了一个 array（数组）。\n\n如下：\n\n![slice 和 array 的友谊小船](https://files.mdnice.com/user/3610/6a025aba-38f0-4ac5-92bb-515d05adeb68.png)\n\n对应的 Slice 在运行时的表现是 SliceHeader 结构体，定义如下：\n\n```golang\ntype SliceHeader struct {\n Data uintptr\n Len  int\n Cap  int\n}\n```\n- Data：指向具体的底层数组。\n- Len：代表切片的长度。\n- Cap：代表切片的容量。\n\n核心要记住的是：slice 真正存储数据的地方，是一个数组。slice 的结构中**存储的是指向所引用的数组指针地址**。\n\n### 分析原因\n\n在了解 slice 的底层后，我们需要来分析问题的起源，也就是那段 Go 程序。\n\n我们关注到 `appenFunc` 变量，他其实是一个函数，并且结果中我们所看到的 10, 20, 30，也只有这里有插入的动作。因此这是需要分析的。\n\n如下：\n\n```golang\nfunc main() {\n\tsl := make([]int, 0, 10)\n\tvar appenFunc = func(s []int) {\n\t\ts = append(s, 10, 20, 30)\n\t}\n\tappenFunc(sl)\n\tfmt.Println(sl)\n\tfmt.Println(sl[:10])\n}\n```\n\n但为什么在 `appenFunc` 函数中所插入的 10, 20, 30 元素，就跑到外面的切片 `sl` 中去了呢？\n\n这其实结合 slice 的底层设计和函数传递就明白了，**在 Go 语言中，只有值传递**：\n\n![](https://files.mdnice.com/user/3610/3d2999da-e405-4a42-ba37-858487052c3b.png)\n\n具体可详见我之前写的《[又吵起来了，Go 是传值还是传引用？](https://mp.weixin.qq.com/s/qsxvfiyZfRCtgTymO9LBZQ)》，有明确分析和说明。\n\n实质上在调用 `appenFunc(sl)` 函数时，**实际上修改了底层所指向的数组**，自然也就会发生变化，也就不难理解为什么 10, 20, 30 元素会出现了。\n\n那为什么 `sl` 变量的长度是 0，甚至有人猜测是不是扩容了，这其实和上面的问题还是一样，因为是值传递，自然也就不会发生变化。\n\n要记住一个关键点：**如果传过去的值是指向内存空间的地址，是可以对这块内存空间做修改的**。反之，你也改不了。\n\n至此，也就解决了我们的第一个大问题。\n\n### 切片小优化\n\n还剩下两个大问题，这似乎用上面的结论没法完整解释。虽说程序是诱因，但这块最直接的影响是和切片访问的小优化有关。\n\n常用的访问切片我们会用：\n\n```\ns[low : high]\n```\n\n注意这里是：low、high。可没有用 len、cap 这种定性的词语，也就代表着这里取的值是可变的。\n\n当是切片（slice）时，表达式 `s[low : high]` 中的 high，**最大的取值范围对应着切片的容量（cap），不是单纯的长度（len）**。因此调用 `fmt.Println(sl[:10])` 时可以输出容量范围内的值，不会出现越界。\n\n相对的 `fmt.Println(sl)` 因为该切片 len 值为 0，没有指定最大索引值，high 则取 len 值，导致输出结果为空。\n\n至此，第二和第三个大问题就解决了。\n\n注：访问元素的定位在 Go 编译期就确定的了，相关逻辑可以在 compile 相关的代码中看到。\n\n## 总结\n\n在今天这篇文章中，我们结合了 Go 语言中切片的基本底层原理、值传递、边界值取值等进行了多轮探讨。\n\n我们要牢记：**如果传过去的值是指向内存空间的地址，是可以对这块内存空间做修改的**。这在多种应用场景下都是适用的。\n\n**所谓的最大取值范围**，除非官方给你写定 len 或 cap，否则不要过于主观的认为，因为他**会根据访问的数据类型和访问定位等改变**。\n\n注：欢迎大家一起多多讨论，加我微信号：cJY0728，备注：加群。我拉你进读者交流群，和大家一起捣鼓技术，突破自己。\n\n## 参考\n- 来自读者 1v1 私聊\n- 来自 Go 读者群"
  },
  {
    "path": "content/posts/go/slice-leak.md",
    "content": "---\ntitle: \"Go 切片导致内存泄露，被坑两次了！\"\ndate: 2021-12-31T12:55:09+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n前段时间在我的 Go 读者群里，有小伙伴们在纠结切片（slice）的问题，我写了这篇《[Go 切片这道题，吵了一个下午！](https://mp.weixin.qq.com/s/kEQI74ge6VhvNEr1d3JW-Q)》，引起了一拨各种讨论，还是比较欣慰的。\n\n这不，有小伙伴给我提出了新的题材：\n\n![来自读者微信提问](https://files.mdnice.com/user/3610/a97e5e6d-b58d-44f2-b858-b9ca12780180.png)\n\n提出的是 Go 中很容易踩坑的切片内存泄露问题。作为宠粉的煎鱼肯定不会放过，争取让大家都避开这个 “坑”。\n\n今天这篇文章，就由煎鱼带大家来了解这个问题：Go 切片可能可以怎么泄露法？\n\n## 切片泄露的可能\n\n在业务代码的编写上，我们经常会接受来自外部的接口数据，再把他插入到对应的数据结构中去，再进行下一步的业务聚合、裁剪、封装、处理。\n\n像在 PHP 语言，常常会放到数组（array）中。在 Go 语言，会放到切片（slice）中。因此在 Go 的切片处理逻辑中，常常会涉及到如下类似的动作。\n\n示例代码如下：\n\n```golang\nvar a []int\n\nfunc f(b []int) []int {\n\ta = b[:2]\n\treturn a\n}\n\nfunc main() {\n    ...\n}\n```\n\n仔细想想，**这段程序有没有问题**，是否存在内存泄露的风险？\n\n答案是：有的。有明确的切片内存泄露的可能性和风险。\n\n## 切片底层结构\n\n可能有些小伙伴会疑惑，怎么就有问题了，是哪里有问题？\n\n这里就得复习一下切片的底层基本数据结构了，切片在运行时的表现是 SliceHeader 结构体，定义如下：\n\n```golang\ntype SliceHeader struct {\n Data uintptr\n Len  int\n Cap  int\n}\n```\n- Data：指向具体的底层数组。\n- Len：代表切片的长度。\n- Cap：代表切片的容量。\n\n要点是：切片真正存储数据的地方，是一个数组。切片的 Data 属性中**存储的是指向所引用的数组指针地址**。\n\n## 背后的原因\n\n在上述案例中，我们有一个包全局变量 a，共有 2 个切片 a 和 b，截取了 b 的一部分赋值给了 a，两者存在着关联。\n\n从程序的直面来看，截取了 b 的一部分赋值给了 a，结构似乎是如下图：\n\n![](https://files.mdnice.com/user/3610/856aff0a-14bb-4dca-9324-4b852f25dd12.png)\n\n但我们进一步打开程序底层来看，他应该是如下图所示：\n\n![](https://files.mdnice.com/user/3610/c79008dd-771d-475b-8c41-6b654216feff.png)\n\n切片 a 和 b 都共享着同一个底层数组（共享内存块），sliceB 包含全部所引用的字符。sliceA 只包含了 [:2]，也就是 0 和 1 两个索引位的字符。\n\n那他们泄露在哪里了？\n\n## 泄露的点\n\n泄露的点，就在于虽然切片 b 已经在函数内结束了他的使命了，不再使用了。但切片 a 还在使用，切片 a 和 切片 b 引用的是同一块底层数组（共享内存块）。\n\n关键点：**切片 a 引用了底层数组中的一段**。\n\n![](https://files.mdnice.com/user/3610/0a4353e0-e793-41b5-a2dc-a6a25a39a519.png)\n\n虽然切片 a 只有底层数组中 0 和 1 两个索引位正在被使用，其余未使用的底层数组空间毫无作用。但由于正在被引用，他们也不会被 GC，因此造成了泄露。\n\n## 解决办法\n\n解决的办法，就是利用切片的特性。当切片的容量空间不足时，会**重新申请一个新的底层数组来存储，让两者彻底分手**。\n\n示例代码如下：\n\n```golang\nvar a []int\nvar c []int    // 第三者\n\nfunc f(b []int) []int {\n\ta = b[:2]\n  \n  // 新的切片 append 导致切片扩容\n\tc = append(c, b[:2]...)\n\tfmt.Printf(\"a: %p\\nc: %p\\nb: %p\\n\", &a[0], &c[0], &b[0])\n  \n\treturn a\n}\n```\n\n输出结果：\n\n```\na: 0xc000102060\nc: 0xc000124010\nb: 0xc000102060\n```\n\n这段程序，新增了一个变量 c，他容量为 0。此时将期望的数据，追加过去。自然而然他就会遇到容量空间不足的情况，也就能实现申请新底层数据。\n\n我们再将原本的切片置为 nil，就能成功实现两者分手的目标了。\n\n## 总结\n\n在今天这篇文章中，我们介绍了 Go 切片的一种常见的内存泄露方式。虽然我们在日常使用的时候可能没关注到。\n\n主要原因还是由于切片的大多数使用场景，体量都比较小。又或是不知不觉就自己扩容了，就变成暂时性泄露了。\n\n这依然是存在风险的，在编写 Go 代码时需要谨慎。毕竟这可是 **Go 语言官方自己都踩过坑的 “坑”**。\n\n## 参考\n- [An interesting way to leak memory with Go slices](https://utcc.utoronto.ca/~cks/space/blog/programming/GoSlicesMemoryLeak)\n- [internal/poll: avoid memory leak in Writev](https://github.com/golang/go/pull/32138/files)\n- [slice 类型内存泄露的逻辑](https://xargin.com/logic-of-slice-memory-leak/)\n- [golang slice内存泄露回收](https://zhuanlan.zhihu.com/p/149381458)"
  },
  {
    "path": "content/posts/go/slice-string-header.md",
    "content": "---\ntitle: \"Go SliceHeader 和 StringHeader，你知道吗？\"\ndate: 2021-12-31T12:54:53+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n在 Go 语言中总是有一些看上去奇奇怪怪的东西，咋一眼一看感觉很熟悉，但又不理解其在 Go 代码中的实际意义，面试官却爱问...\n\n今天要给大家介绍的是 SliceHeader 和 StringHeader 结构体，了解清楚他到底是什么，又有什么用，并且会在最后给大家介绍 0 拷贝转换的内容。\n\n一起愉快地开始吸鱼之路。\n\nSliceHeader\n-----------\n\nSliceHeader 如其名，Slice + Header，看上去很直观，实际上是 Go Slice（切片）的运行时表现。\n\nSliceHeader 的定义如下：\n\n```\ntype SliceHeader struct {\n Data uintptr\n Len  int\n Cap  int\n}\n```\n\n*   Data：指向具体的底层数组。\n    \n*   Len：代表切片的长度。\n    \n*   Cap：代表切片的容量。\n    \n\n既然知道了切片的运行时表现，那是不是就意味着我们可以自己造一个？\n\n在日常程序中，可以利用标准库 `reflect` 提供的 `SliceHeader` 结构体造一个：\n\n```\nfunc main() {\n  // 初始化底层数组\n s := [4]string{\"脑子\", \"进\", \"煎鱼\", \"了\"}\n s1 := s[0:1]\n s2 := s[:]\n\n  // 构造 SliceHeader\n sh1 := (*reflect.SliceHeader)(unsafe.Pointer(&s1))\n sh2 := (*reflect.SliceHeader)(unsafe.Pointer(&s2))\n fmt.Println(sh1.Len, sh1.Cap, sh1.Data)\n fmt.Println(sh2.Len, sh2.Cap, sh2.Data)\n}\n```\n\n你认为输出结果是什么，这两个新切片会指向同一个底层数组的内存地址吗？\n\n输出结果：\n\n```\n1 4 824634330936\n4 4 824634330936\n```\n\n两个切片的 Data 属性所指向的底层数组是一致的，Len 属性的值不一样，sh1 和 sh2 分别是两个切片。\n\n### 疑问\n\n为什么两个新切片所指向的 Data 是同一个地址的呢？\n\n这其实是 Go 语言本身为了减少内存占用，提高整体的性能才这么设计的。\n\n将切片复制到任意函数的时候，对底层数组大小都不会影响。复制时只会复制切片本身（值传递），不会涉及底层数组。\n\n也就是在函数间传递切片，其只拷贝 24 个字节（指针字段 8 个字节，长度和容量分别需要 8 个字节），效率很高。\n\n### 坑\n\n这种设计也引出了新的问题，在平时通过 `s[i:j]` 所生成的新切片，两个切片底层指向的是同一个底层数组。\n\n假设**在没有超过容量（cap）的情况下，对第二个切片操作会影响第一个切片**。\n\n这是很多 Go 开发常会碰到的一个大 “坑”，不清楚的排查了很久的都不得而终。\n\nStringHeader\n------------\n\n除了 SliceHeader 外，Go 语言中还有一个典型代表，那就是字符串（string）的运行时表现。\n\nStringHeader 的定义如下：\n\n```\ntype StringHeader struct {\n   Data uintptr\n   Len  int\n}\n```\n\n*   Data：存放指针，其指向具体的存储数据的内存区域。\n    \n*   Len：字符串的长度。\n    \n\n可得知 “Hello” 字符串的底层数据如下：\n\n```\nvar data = [...]byte{\n    'h', 'e', 'l', 'l', 'o',\n}\n```\n\n底层的存储示意图如下：\n\n![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/30ace7491055413eadaf97c05de85b9c~tplv-k3u1fbpfcp-zoom-1.image)\n\n图来自网络\n\n真实演示例子如下：\n\n```\nfunc main() {\n s := \"脑子进煎鱼了\"\n s1 := \"脑子进煎鱼了\"\n s2 := \"脑子进煎鱼了\"[7:]\n\n fmt.Printf(\"%d \\n\", (*reflect.StringHeader)(unsafe.Pointer(&s)).Data)\n fmt.Printf(\"%d \\n\", (*reflect.StringHeader)(unsafe.Pointer(&s1)).Data)\n fmt.Printf(\"%d \\n\", (*reflect.StringHeader)(unsafe.Pointer(&s2)).Data)\n}\n```\n\n你认为输出结果是什么，变量 s 和 s1、s2 会指向同一个底层内存空间吗？\n\n输出结果：\n\n```\n17608227 \n17608227 \n17608234 \n```\n\n从输出结果来看，变量 s 和 s1 指向同一个内存地址。变量 s2 虽稍有偏差，但本质上也是指向同一块。\n\n因为其是字符串的切片操作，是从第 7 位索引开始，因此正好的 17608234-17608227 = 7。也就是三个变量都是指向同一块内存空间，这是为什么呢？\n\n这是因为在 Go 语言中，**字符串都是只读的，为了节省内存，相同字面量的字符串通常对应于同一字符串常量，因此指向同一个底层数组**。\n\n0 拷贝转换\n------\n\n为什么会有人关注到 SliceHeader、StringHeader 这类运行时细节呢，一大部分原因是业内会有开发者，**希望利用其实现零拷贝的 string 到 bytes 的转换**。\n\n常见转换代码如下：\n\n```\nfunc string2bytes(s string) []byte {\n stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))\n\n bh := reflect.SliceHeader{\n  Data: stringHeader.Data,\n  Len:  stringHeader.Len,\n  Cap:  stringHeader.Len,\n }\n\n return *(*[]byte)(unsafe.Pointer(&bh))\n}\n```\n\n但这其实是错误的，官方明确表示：\n\n>  the Data field is not sufficient to guarantee the data it references will not be garbage collected, so programs must keep a separate, correctly typed pointer to the underlying data.\n\nSliceHeader、StringHeader 的 Data 字段是一个 `uintptr` 类型。由于 Go 语言只有值传递。\n\n因此在上述代码中会出现将 `Data` 作为值拷贝的情况，这就会导致**无法保证它所引用的数据不会被垃圾回收（GC）**。\n\n应该使用如下转换方式：\n\n```\nfunc main() {\n s := \"脑子进煎鱼了\"\n v := string2bytes1(s)\n fmt.Println(v)\n}\n\nfunc string2bytes1(s string) []byte {\n stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))\n\n var b []byte\n pbytes := (*reflect.SliceHeader)(unsafe.Pointer(&b))\n pbytes.Data = stringHeader.Data\n pbytes.Len = stringHeader.Len\n pbytes.Cap = stringHeader.Len\n\n return b\n}\n```\n\n在程序必须保留一个单独的、正确类型的指向底层数据的指针。\n\n在性能方面，若只是期望单纯的转换，对容量（cap）等字段值不敏感，也可以使用以下方式：\n\n```\nfunc string2bytes2(s string) []byte {\n return *(*[]byte)(unsafe.Pointer(&s))\n}\n```\n\n性能对比：\n\n```\nstring2bytes1-1000-4   3.746 ns/op  0 allocs/op\nstring2bytes1-1000-4   3.713 ns/op  0 allocs/op\nstring2bytes1-1000-4   3.969 ns/op  0 allocs/op\n\nstring2bytes2-1000-4   2.445 ns/op  0 allocs/op\nstring2bytes2-1000-4   2.451 ns/op  0 allocs/op\nstring2bytes2-1000-4   2.455 ns/op  0 allocs/op\n```\n\n会相当标准的转换性能会稍快一些，这种强转也会导致一个小问题。\n\n代码如下：\n\n```\nfunc main() {\n s := \"脑子进煎鱼了\"\n v := string2bytes2(s)\n println(len(v), cap(v))\n}\nfunc string2bytes2(s string) []byte {\n return *(*[]byte)(unsafe.Pointer(&s))\n}\n```\n\n输出结果：\n\n```\n18 824633927632\n\n```\n\n这种强转其会导致 byte 的切片容量非常大，需要特别注意。一般还是推荐使用标准的 SliceHeader、StringHeader 方式就好了，也便于后来的维护者理解。\n\n总结\n--\n\n在这篇文章中，我们介绍了字符串（string）和切片（slice）的两个运行时表现，分别是 StringHeader 和 SliceHeader。\n\n同时了解到其运行时表现后，我们还针对其两者的地址指向，常见坑进行了说明。\n\n最后我们进一步深入，面向 0 拷贝转换的场景进行了介绍和性能分析。\n\n你平时有没有遇到过这块的疑惑或问题呢，欢迎大家一起讨论！\n\n## 鼓励\n\n若有任何疑问欢迎评论区反馈和交流，**最好的关系是互相成就**，各位的**点赞**就是[煎鱼](https://github.com/eddycjy)创作的最大动力，感谢支持。\n\n> 文章持续更新，可以微信搜【脑子进煎鱼了】阅读，本文 **GitHub** [github.com/eddycjy/blog](https://github.com/eddycjy/blog) 已收录，学习 Go 语言可以看 [Go 学习地图和路线](https://github.com/eddycjy/go-developer-roadmap)，欢迎 Star 催更。\n\n\n参考\n--\n\n*   Go语言slice的本质-SliceHeader\n    \n*   数组、字符串和切片\n    \n*   零拷贝实现string 和bytes的转换疑问\n"
  },
  {
    "path": "content/posts/go/stop-goroutine.md",
    "content": "---\ntitle: \"回答我，停止 Goroutine 有几种方法？\"\ndate: 2021-12-31T12:54:50+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n协程（goroutine）作为 Go 语言的扛把子，经常在各种 Go 工程项目中频繁露面，甚至有人会为了用 goroutine 而强行用他。\n\n在 Go 工程师的面试中，也绕不开他，会有人问 ”如何停止一个 goroutine？”，一下子就把话题范围扩大了，这是一个涉及多个知识点的话题，能进一步深入问。\n\n为此，今天煎鱼就带大家了解一下停止 goroutine 的方法！\n\n## goroutine 案例\n\n在日常的工作中，我们常会有这样的 Go 代码，go 关键字一把搜起一个 goroutine：\n\n```golang\nfunc main() { \n\tch := make(chan string, 6)\n\tgo func() {\n\t\tfor {\n\t\t\tch <- \"脑子进煎鱼了\"\n\t\t}\n\t}()\n}\n```\n\n初入 goroutine 大门的开发者可能就完事了，但跑一段时间后，他就可能会遇到一些问题，苦苦排查...\n\n像是：当 goroutine 内的任务，运行的太久，又或是卡死了...就会一直阻塞在系统中，变成 goroutine 泄露，或是间接造成资源暴涨，会带来许多的问题。\n\n如何在停止 goroutine，就成了一门必修技能了，不懂就没法用好 goroutine。\n\n## 关闭 channel\n\n第一种方法，就是借助 channel 的 close 机制来完成对 goroutine 的精确控制。\n\n代码如下：\n\n```golang\nfunc main() {\n\tch := make(chan string, 6)\n\tgo func() {\n\t\tfor {\n\t\t\tv, ok := <-ch\n\t\t\tif !ok {\n\t\t\t\tfmt.Println(\"结束\")\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfmt.Println(v)\n\t\t}\n\t}()\n\n\tch <- \"煎鱼还没进锅里...\"\n\tch <- \"煎鱼进脑子里了！\"\n\tclose(ch)\n\ttime.Sleep(time.Second)\n}\n```\n\n在 Go 语言的 channel 中，channel 接受数据有两种方法：\n\n```golang\nmsg := <-ch\nmsg, ok := <-ch\n```\n\n这两种方式对应着不同的 runtime 方法，我们可以利用其第二个参数进行判别，当关闭 channel 时，就根据其返回结果跳出。\n\n另外我们也可以利用 `for range` 的特性：\n\n```golang\n\tgo func() {\n\t\tfor {\n\t\t\tfor v := range ch {\n\t\t\t\tfmt.Println(v)\n\t\t\t}\n\t\t}\n\t}()\n```\n\n其会一直循环遍历通道 `ch`，直到其关闭为止，是颇为常见的一种用法。\n\n## 定期轮询 channel\n\n第二种方法，是更为精细的方法，其结合了第一种方法和类似信号量的处理方式。\n\n代码如下：\n\n```golang\nfunc main() {\n\tch := make(chan string, 6)\n\tdone := make(chan struct{})\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase ch <- \"脑子进煎鱼了\":\n\t\t\tcase <-done:\n\t\t\t\tclose(ch)\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\ttime.Sleep(3 * time.Second)\n\t\tdone <- struct{}{}\n\t}()\n\n\tfor i := range ch {\n\t\tfmt.Println(\"接收到的值: \", i)\n\t}\n\n\tfmt.Println(\"结束\")\n}\n```\n\n在上述代码中，我们声明了变量 `done`，其类型为 channel，用于作为信号量处理 goroutine 的关闭。\n\n而 goroutine 的关闭是不知道什么时候发生的，因此在 Go 语言中会利用 `for-loop` 结合 `select` 关键字进行监听，再进行完毕相关的业务处理后，再调用 `close` 方法正式关闭 channel。\n\n若程序逻辑比较简单结构化，也可以不调用 `close` 方法，因为 goroutine 会自然结束，也就不需要手动关闭了。\n\n## 使用 context\n\n第三种方法，可以借助 Go 语言的上下文（context）来做 goroutine 的控制和关闭。\n\n代码如下：\n\n```golang\nfunc main() {\n\tch := make(chan struct{})\n\tctx, cancel := context.WithCancel(context.Background())\n\n\tgo func(ctx context.Context) {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tch <- struct{}{}\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t\tfmt.Println(\"煎鱼还没到锅里...\")\n\t\t\t}\n\n\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t}\n\t}(ctx)\n\n\tgo func() {\n\t\ttime.Sleep(3 * time.Second)\n\t\tcancel()\n\t}()\n\n\t<-ch\n\tfmt.Println(\"结束\")\n}\n```\n\n在 context 中，我们可以借助 `ctx.Done` 获取一个只读的 channel，类型为结构体。可用于识别当前 channel 是否已经被关闭，其原因可能是到期，也可能是被取消了。\n\n因此 context 对于跨 goroutine 控制有自己的灵活之处，可以调用 `context.WithTimeout` 来根据时间控制，也可以自己主动地调用 `cancel` 方法来手动关闭。\n\n## 干掉另外一个 goroutine\n\n在了解了停止 goroutine 的 3 种经典方法后，又有小伙伴提出了新的想法。就是 “**我想在 goroutineA 里去停止 goroutineB，有办法吗？**”\n\n答案是不能，因为在 Go 语言中，goroutine 只能自己主动退出，一般通过 channel 来控制，不能被外界的其他 goroutine 关闭或干掉，也没有 goroutine 句柄的显式概念。\n\n![go/issues/32610](https://files.mdnice.com/user/3610/6c9da671-cc06-4eef-8911-915fd470375f.png)\n\n在 Go issues 中也有人提过类似问题，Dave Cheney 给出了一些思考：\n\n- 如果一个 goroutine 被强行停止了，它所拥有的资源会发生什么？堆栈被解开了吗？defer 是否被执行？\n  - 如果执行 defer，该 goroutine 可能可以继续无限期地生存下去。\n  - 如果不执行 defer，该 goroutine 原本的应用程序系统设计逻辑将会被破坏，这肯定不合理。\n- 如果允许强制停止 goroutine，是要释放所有东西，还是直接把它从调度器中踢出去，你想通过此解决什么问题？\n\n这都是值得深思的，另外一旦放开这种限制。作为程序员，你维护代码。很有可能就不知道 goroutine 的句柄被传到了哪里，又是在何时何地被人莫名其妙关闭，非常糟糕...\n\n## 总结\n\n在今天这篇文章中，我们介绍了在 Go 语言中停止 goroutine 的三大经典方法（channel、context，channel+context）和其背后的使用原理。\n\n同时针对 goroutine 不可以跨 goroutine 强制停止的原因进行了分析。其实 goroutine 的设计就是这样的，包括像 goroutine+panic+recover 的设计也是遵循这个原理，因此也有的 Go 开发者总是会误以为跨 goroutine 能有 recover 接住...\n\n记住，在 Go 语言中**每一个 goroutine 都需要自己承担自己的任何责任**，这是基本原则。\n\n（你已经是一个成熟的 goroutine 了...）\n\n## 参考\n- [How to stop a goroutine](https://stackoverflow.com/questions/6807590/how-to-stop-a-goroutine)"
  },
  {
    "path": "content/posts/go/struct-pointer.md",
    "content": "---\ntitle: \"你知道 Go 结构体和结构体指针调用有什么区别吗？\"\ndate: 2021-06-06T12:21:30+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好，我是煎鱼。\n\n前几天在分享《Go 结构体是否可以比较，为什么？》时，有小伙伴提出了新的问题：\n\n![来自文章评论区](https://image.eddycjy.com/a4d34c5312339b9909e482c18f0cdf4a.png)\n\n虽然大家提问题的速度已经超出了本鱼写文章的速度...不过作为宠粉狂鱼，在此刻清明假期时还是写下了这篇文章。\n\n我在网上冲浪时搜索了相关问题，发现 6 年前就有 Go 开发者有一模一样的疑问，真是困扰了一代又一代的小伙伴。\n\n![来自 stackoverflow.com](https://image.eddycjy.com/dbe910917c86e103648e314f79896a81.png)\n\n\n本期的男主角是《**Go 结构体和结构体指针调用有什么区别**》，希望对大家有所帮助，带来一些思考。\n\n**请在此处默念自己心目中的答案**，再和煎鱼一同研讨一波 Go 的技术哲学。\n\n## 结构体是什么\n\n在 Go 语言中有个基本类型，开发者们称之为结构体（struct）。是 Go 语言中非常常用的，基本定义：\n\n```golang\ntype struct_variable_type struct {\n    member definition\n    member definition\n    ...\n    member definition\n}\n```\n\n简单示例：\n\n```golang\npackage main\n\nimport \"fmt\"\n\ntype Vertex struct {\n    Name1 string\n    Name2 string\n}\n\nfunc main() {\n    v := Vertex{\"脑子进了\", \"煎鱼\"}\n    v.Name2 = \"蒸鱼\"\n    fmt.Println(v.Name2)\n}\n```\n\n输出结果：\n\n```\n蒸鱼\n```\n\n这部分属于基础知识，因此不再过多解释。如果看不懂，建议重学 Go 语言语法基础。\n\n## 结构体和指针调用\n\n讲解前置概要后，直接进入本文主题。如下例子：\n\n```golang\ntype MyStruct struct {\n    Name string\n}\n\nfunc (s MyStruct) SetName1(name string) {\n    s.Name = name\n}\n\nfunc (s *MyStruct) SetName2(name string) {\n    s.Name = name\n}\n```\n\n该程序声明了一个 `User` 结构体，其包含两个结构体方法，分别是 `SetName1` 和 `SetName2` 方法，两者之间的差异就是**引用的方式不同**。\n\n进一步延伸，这两者有什么区别，什么情况下用哪种，有没有什么注意事项？\n\n注：很巧，我有一个朋友，当年刚上手 Go 语言时，就纠结过这个问题。\n\n## 两者区别\n\n从许多小伙伴的反馈来看，这两个例子之间的区别可能会让人感到困惑，经常会有人纠结要不要使用 “指针”，又担心 GC 什么的。\n\n实际上情况没那么复杂，看看下面的例子：\n\n```golang\nfunc (s MyStruct) SetName1(name string) \nfunc (s *MyStruct) SetName2(name string)\n```\n当在一个类型上定义一个方法时，接收器（在上面的例子中是 s）的行为就像它是方法的一个参数一样。其相当于：\n\n```golang\n func SetName1(s MyStruct, name string){\n    u.Name = name\n }\n\n func SetName2(s *MyStruct,name string){\n    u.Name = name\n }\n```\n\n因此结构体方法是要将接收器定义成值，还是指针。这本质上与函数参数应该是值还是指针是同一个问题。\n\n## 如何选择\n\n整体有以下几个考虑因素，按重要程度顺序排列：\n\n1. 在使用上的考虑：方法是否需要修改接收器？如果需要，接收器必须是一个指针。\n\n2. 在效率上的考虑：如果接收器很大，比如：一个大的结构体，使用指针接收器会好很多。\n\n3. 在一致性上的考虑：如果类型的某些方法必须有指针接收器，那么其余的方法也应该有指针接收器，所以无论类型如何使用，方法集都是一致的。\n\n回到上面的例子中，从功能使用角度来看：\n- 如果 `SetName2` 方法修改了 s 的字段，调用者是可以看到这些字段值变更的，因为其是指针引用，本质上是同一份。\n- 相对 `SetName1` 方法来讲，该方法是用调用者参数的副本来调用的，本质上是值传递，它所做的任何字段变更对调用者来说是看不见的。\n\n另外对于基本类型、切片和小结构等类型，值接收器是非常廉价的。\n\n因此除非方法的语义需要指针，那么值接收器是最高效和清晰的。在 GC 方面，也不需要过度关注。出现时再解决就好了。\n\n## 总结\n\n在本文中，我们针对 Go 结构体和结构体指针调用有什么区别，这个问题进行了深入浅出的分析和说明。\n\n而在本文中所介绍的部分内容，来自于官方 FAQ 的 “Should I define methods on values or pointers?”，可以认为是官方给出的基本解答了（问的人是真的多）。\n\n谁疑惑这个问题，转发这篇文章，吸就完了。"
  },
  {
    "path": "content/posts/go/switch-type.md",
    "content": "---\ntitle: \"Go 泛型玩出花，详解新提案 switch type！\"\ndate: 2021-12-31T12:55:23+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n前面写过好几篇 Go 泛型的语法、案例介绍，新的一手 Go 消息。实际上，随着一些提案被接受，新的提案也逐渐冒出。\n\n这不，我发现有了泛型后，大家可以更进一步玩出花来了。看到了一个 ”新“ 提案《[proposal: spec: generics: type switch on parametric types](https://github.com/golang/go/issues/45380 \"proposal: spec: generics: type switch on parametric types\")》，讲的就是增加泛型后的参数类型上的类型开关诉求。\n\n跟着煎鱼一起掌握新的 Go 知识吧！\n\n## 新提案\n\n新的提案内容是希望增加一个新的变种语句，允许在 switch 语句中使用泛型时，能够进一步便捷的约束其类型参数。\n\n例如：\n\n```go\nswitch type T {\ncase A1:\ncase A2, A3:\n   ...\n}\n```\n\n也就是 switch-type 语句的 **T 类型可以是一个泛型的类型参**，case 所对应的的类型可以是任何类型，包括泛型的约束类型。\n\n假设类型 T 的类型有可能是以下：\n\n```go\ninterface{\n    C\n    A\n}\n```\n\n可以借助泛型的近似元素来约束：\n\n```go\n    interface{\n        C\n        A1 | A2 | ... | An\n    }\n```\n\n甚至还可以在 case 上有新的写法：\n\n```go\ncase interface {~T}:\n```\n\n在支持泛型后，**switch 在 type 和 case 上会存在很多种可能性**，需要进行具体的特性支持，这个提案就是为此出现。\n\n## 实际案例\n\n### 案例一：多类型元素\n\n```go\ntype Stringish interface {\n\tstring | fmt.Stringer\n}\n\nfunc Concat[S Stringish](x []S \"S Stringish\") string {\n    switch type S {\n    case string:\n        ...\n    case fmt.Stringer:\n        ...\n    }\n }\n```\n\n类型 S 能够支持 string 和 fmt.Stringer 类型，case 配套对应实现。\n\n### 案例二：近似元素\n\n```go\ntype Constraint interface {\n    ~int | ~int8 | ~string\n}\n\nfunc ThisSyntax[T Constraint]( \"T Constraint\") {\n    switch type T {\n    case ~int | ~int8:\n        ...\n    case ~string:\n        ...\n    }\n}\n\nfunc IsClearerThanThisSyntax[T Constraint]( \"T Constraint\") {\n    switch type T {\n    case interface{~int | ~int8 }:\n        ...\n    case interface{ ~string }:\n        ...\n    }\n}\n```\n\n类型 T 可能有很多类型，程序中用到了近似元素，也就是基础类型是 int、int8、string，这些类型中的任何一种都能够满足这个约束。\n\n为此，switch-type 支持了，case 也要配套支持该特性。\n\n## 争议点\n\n看到这里可能大家也想到了，这个味道很似曾相识，好像某个语法能够支持。因此，这个提案下最有争议的，就是与原有的类型断言的重复。\n\n原有的类型断言如下：\n\n```go\nswitch T.(type) {\ncase string:\n   ...\ndefault:\n   ...\n}\n```\n\n新的类型判别如下：\n\n```go\nswitch type T {\ncase A1:\ncase A2, A3:\n   ...\n}\n```\n\n这么咋一看，其实类型断言的完全可以取代新的，那岂不是重复建设，造轮子了？\n\n其实是没有完全取代的。差异点如下：\n\n```go\ntype ApproxString interface { ~string }\n\nfunc F[T ApproxString](v T \"T ApproxString\") {\n    switch (interface{})(v).(type) {\n    case string:\n        fmt.Println(v)\n    default:\n        panic(\"脑子没进煎鱼\")\n    }\n}\n\ntype MyString string\n\nfunc main() {\n    F(MyString(\"脑子进煎鱼了\"))\n}\n```\n\n看出来差别在哪了吗，答案是什么？\n\n\n答案是：会抛出恐慌（panic）。\n\n你可能纠结了，问题出在哪里？这传入的 ”脑子进煎鱼了“ 的类型是 `MyString`，他的基础类型是 `string` 类型，也满足 `ApproxString` 类型的近似类型 `~string` 的要求，怎么就不行了...\n\n根本原因是因为他的类型是 interface，而非 string 类型。所以走到了 defalut 分支的恐慌。\n\n\n## 总结\n\n今天给大家介绍了 Go 泛型的最新消息，在上一个提案被合并后，该提案也有一些新的动静。不过 Go 官方表态，会等熟练掌握泛型实践后，再继续推动该提案。\n\n我相信，原有的 `switch.(type)` 和 `switch type` 很大概率在 Go 底层会变成同一个逻辑块处理，再逐渐过渡。\n\n这个提案的目的还是**为了解决若干引入泛型后，所带入的 BUG/需求**，正正是需要新的语法结构来解决的。\n\n**你对此有什么看法呢**，欢迎在评论区留言和交流：）"
  },
  {
    "path": "content/posts/go/sync-map.md",
    "content": "---\ntitle: \"Go 并发读写 sync.map 的强大之处\"\ndate: 2021-12-31T12:54:50+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好，我是煎鱼。\n\n在之前的 《[为什么 Go map 和 slice 是非线程安全的？](http://mp.weixin.qq.com/s?__biz=MzUxMDI4MDc1NA==&mid=2247489045&idx=1&sn=197bda427246e16907c7b471a5dc0572&chksm=f9040348ce738a5ebf541954a4de29ce746238ab7f6e5a2af8a1765c5383ad4208f43b2bac4f&scene=21#wechat_redirect)》 文章中，我们讨论了 Go 语言的 map 和 slice 非线程安全的问题，基于此引申出了 map 的两种目前在业界使用的最多的并发支持的模式。\n\n分别是：\n\n*   原生 map + 互斥锁或读写锁 mutex。\n    \n*   标准库 sync.Map（Go1.9及以后）。\n    \n\n有了选择，总是有选择困难症的，这**两种到底怎么选，谁的性能更加的好**？我有一个朋友说 标准库 sync.Map 性能菜的很，不要用。我到底听谁的...\n\n今天煎鱼就带你揭秘 Go sync.map，我们先会了解清楚什么场景下，Go map 的多种类型怎么用，谁的性能最好！\n\n接着根据各 map 性能分析的结果，针对性的对 sync.map 进行源码解剖，了解 WHY。\n\n一起愉快地开始吸鱼之路。\n\nsync.Map 优势\n-----------\n\n在 Go 官方文档中明确指出 Map 类型的一些建议：\n\n![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6500ee41a08a415ca681fa427f5032b3~tplv-k3u1fbpfcp-zoom-1.image)\n\n*   多个 goroutine 的并发使用是安全的，不需要额外的锁定或协调控制。\n    \n*   大多数代码应该使用原生的 map，而不是单独的锁定或协调控制，以获得更好的类型安全性和维护性。\n    \n\n同时 Map 类型，还针对以下场景进行了性能优化：\n\n*   当一个给定的键的条目只被写入一次但被多次读取时。例如在仅会增长的缓存中，就会有这种业务场景。\n    \n*   当多个 goroutines 读取、写入和覆盖不相干的键集合的条目时。\n    \n\n这两种情况与 Go map 搭配单独的 Mutex 或 RWMutex 相比较，使用 Map 类型可以大大减少锁的争夺。\n\n性能测试\n----\n\n听官方文档介绍了一堆好处后，他并没有讲到缺点，所说的性能优化后的优势又是否真实可信。我们一起来验证一下。\n\n首先我们定义基本的数据结构：\n\n```\n// 代表互斥锁\ntype FooMap struct {\n sync.Mutex\n data map[int]int\n}\n\n// 代表读写锁\ntype BarRwMap struct {\n sync.RWMutex\n data map[int]int\n}\n\nvar fooMap *FooMap\nvar barRwMap *BarRwMap\nvar syncMap *sync.Map\n\n// 初始化基本数据结构\nfunc init() {\n fooMap = &FooMap{data: make(map[int]int, 100)}\n barRwMap = &BarRwMap{data: make(map[int]int, 100)}\n syncMap = &sync.Map{}\n}\n\n```\n\n在配套方法上，常见的增删改查动作我们都编写了相应的方法。用于后续的压测（只展示部分代码）：\n\n```\nfunc builtinRwMapStore(k, v int) {\n barRwMap.Lock()\n defer barRwMap.Unlock()\n barRwMap.data[k] = v\n}\n\nfunc builtinRwMapLookup(k int) int {\n barRwMap.RLock()\n defer barRwMap.RUnlock()\n if v, ok := barRwMap.data[k]; !ok {\n  return -1\n } else {\n  return v\n }\n}\n\nfunc builtinRwMapDelete(k int) {\n barRwMap.Lock()\n defer barRwMap.Unlock()\n if _, ok := barRwMap.data[k]; !ok {\n  return\n } else {\n  delete(barRwMap.data, k)\n }\n}\n\n```\n\n其余的类型方法基本类似，考虑重复篇幅问题因此就不在此展示了。\n\n压测方法基本代码如下：\n\n```\nfunc BenchmarkBuiltinRwMapDeleteParalell(b *testing.B) {\n b.RunParallel(func(pb *testing.PB) {\n  r := rand.New(rand.NewSource(time.Now().Unix()))\n  for pb.Next() {\n   k := r.Intn(100000000)\n   builtinRwMapDelete(k)\n  }\n })\n}\n\n```\n\n这块主要就是增删改查的代码和压测方法的准备，压测代码直接复用的是大白大佬的 go19-examples/benchmark-for-map 项目。\n\n也可以使用 Go 官方提供的 map\\_bench\\_test.go，有兴趣的小伙伴可以自己拉下来运行试一下。\n\n### 压测结果\n\n1）写入：\n\n| 方法名 | 含义 | 压测结果 |\n| --- | --- | --- |\n| BenchmarkBuiltinMapStoreParalell-4 | map+mutex 写入元素 | 237.1 ns/op |\n| BenchmarkSyncMapStoreParalell-4 | sync.map 写入元素 | 509.3 ns/op |\n| BenchmarkBuiltinRwMapStoreParalell-4 | map+rwmutex 写入元素 | 207.8 ns/op |\n\n在写入元素上，最慢的是 `sync.map` 类型，其次是原生 map+互斥锁（Mutex），最快的是原生 map+读写锁（RwMutex）。\n\n总体的排序（从慢到快）为：SyncMapStore < MapStore < RwMapStore。\n\n2）查找：\n\n| 方法名 | 含义 | 压测结果 |\n| --- | --- | --- |\n| BenchmarkBuiltinMapLookupParalell-4 | map+mutex 查找元素 | 166.7 ns/op |\n| BenchmarkBuiltinRwMapLookupParalell-4 | map+rwmutex 查找元素 | 60.49 ns/op |\n| BenchmarkSyncMapLookupParalell-4 | sync.map 查找元素 | 53.39 ns/op |\n\n在查找元素上，最慢的是原生 map+互斥锁，其次是原生 map+读写锁。最快的是 `sync.map` 类型。\n\n总体的排序为：MapLookup < RwMapLookup < SyncMapLookup。\n\n3）删除：\n\n| 方法名 | 含义 | 压测结果 |\n| --- | --- | --- |\n| BenchmarkBuiltinMapDeleteParalell-4 | map+mutex 删除元素 | 168.3 ns/op |\n| BenchmarkBuiltinRwMapDeleteParalell-4 | map+rwmutex 删除元素 | 188.5 ns/op |\n| BenchmarkSyncMapDeleteParalell-4 | sync.map 删除元素 | 41.54 ns/op |\n\n在删除元素上，最慢的是原生 map+读写锁，其次是原生 map+互斥锁，最快的是 `sync.map` 类型。\n\n总体的排序为：RwMapDelete < MapDelete < SyncMapDelete。\n\n### 场景分析\n\n根据上述的压测结果，我们可以得出 `sync.Map` 类型：\n\n*   在读和删场景上的性能是最佳的，领先一倍有多。\n    \n*   在写入场景上的性能非常差，落后原生 map+锁整整有一倍之多。\n    \n\n因此在实际的业务场景中。假设是读多写少的场景，会更建议使用 `sync.Map` 类型。\n\n但若是那种写多的场景，例如多 goroutine 批量的循环写入，那就建议另辟途径了，性能不忍直视（无性能要求另当别论）。\n\nsync.Map 剖析\n-----------\n\n清楚如何测试，测试的结果后。我们需要进一步深挖，知其所以然。\n\n为什么 `sync.Map` 类型的测试结果这么的 “偏科”，为什么读操作性能这么高，写操作性能低的可怕，他是怎么设计的？\n\n### 数据结构\n\n`sync.Map` 类型的底层数据结构如下：\n\n```\ntype Map struct {\n mu Mutex\n read atomic.Value // readOnly\n dirty map[interface{}]*entry\n misses int\n}\n\n// Map.read 属性实际存储的是 readOnly。\ntype readOnly struct {\n m       map[interface{}]*entry\n amended bool\n}\n\n```\n\n*   mu：互斥锁，用于保护 read 和 dirty。\n    \n*   read：只读数据，支持并发读取（atomic.Value 类型）。如果涉及到更新操作，则只需要加锁来保证数据安全。\n    \n\n*   read 实际存储的是 readOnly 结构体，内部也是一个原生 map，amended 属性用于标记 read 和 dirty 的数据是否一致。\n    \n\n*   dirty：读写数据，是一个原生 map，也就是非线程安全。操作 dirty 需要加锁来保证数据安全。\n    \n*   misses：统计有多少次读取 read 没有命中。每次 read 中读取失败后，misses 的计数值都会加 1。\n    \n\n在 read 和 dirty 中，都有涉及到的结构体：\n\n```\ntype entry struct {\n p unsafe.Pointer // *interface{}\n}\n\n```\n\n其包含一个指针 p, 用于指向用户存储的元素（key）所指向的 value 值。\n\n在此建议你必须搞懂 read、dirty、entry，再往下看，食用效果会更佳，后续会围绕着这几个概念流转。\n\n### 查找过程\n\n划重点，Map 类型本质上是有两个 “map”。一个叫 read、一个叫 dirty，长的也差不多：\n\n![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3a34f8cbe34f428c93bddfa7d302a2bb~tplv-k3u1fbpfcp-zoom-1.image)\n\nsync.Map 的 2 个 map\n\n当我们从 sync.Map 类型中读取数据时，其会先查看 read 中是否包含所需的元素：\n\n*   若有，则通过 atomic 原子操作读取数据并返回。\n    \n*   若无，则会判断 `read.readOnly` 中的 amended 属性，他会告诉程序 dirty 是否包含 `read.readOnly.m` 中没有的数据；因此若存在，也就是 amended 为 true，将会进一步到 dirty 中查找数据。\n    \n\nsync.Map 的读操作性能如此之高的原因，就在于存在 read 这一巧妙的设计，其作为一个缓存层，提供了快路径（fast path）的查找。\n\n同时其结合 amended 属性，配套解决了每次读取都涉及锁的问题，实现了读这一个使用场景的高性能。\n\n### 写入过程\n\n我们直接关注 `sync.Map` 类型的 Store 方法，该方法的作用是新增或更新一个元素。\n\n源码如下：\n\n```\nfunc (m *Map) Store(key, value interface{}) {\n read, _ := m.read.Load().(readOnly)\n if e, ok := read.m[key]; ok && e.tryStore(&value) {\n  return\n }\n  ...\n}\n\n```\n\n调用 `Load` 方法检查 `m.read` 中是否存在这个元素。若存在，且没有被标记为删除状态，则尝试存储。\n\n若该元素不存在或已经被标记为删除状态，则继续走到下面流程：\n\n```\nfunc (m *Map) Store(key, value interface{}) {\n ...\n m.mu.Lock()\n read, _ = m.read.Load().(readOnly)\n if e, ok := read.m[key]; ok {\n  if e.unexpungeLocked() {\n   m.dirty[key] = e\n  }\n  e.storeLocked(&value)\n } else if e, ok := m.dirty[key]; ok {\n  e.storeLocked(&value)\n } else {\n  if !read.amended {\n   m.dirtyLocked()\n   m.read.Store(readOnly{m: read.m, amended: true})\n  }\n  m.dirty[key] = newEntry(value)\n }\n m.mu.Unlock()\n}\n\n```\n\n由于已经走到了 dirty 的流程，因此开头就直接调用了 `Lock` 方法**上互斥锁**，保证数据安全，也是凸显**性能变差的第一幕**。\n\n其分为以下三个处理分支：\n\n*   若发现 read 中存在该元素，但已经被标记为已删除（expunged），则说明 dirty 不等于 nil（dirty 中肯定不存在该元素）。其将会执行如下操作。\n    \n\n*   将元素状态从已删除（expunged）更改为 nil。\n    \n*   将元素插入 dirty 中。\n    \n\n*   若发现 read 中不存在该元素，但 dirty 中存在该元素，则直接写入更新 entry 的指向。\n    \n*   若发现 read 和 dirty 都不存在该元素，则从 read 中复制未被标记删除的数据，并向 dirty 中插入该元素，赋予元素值 entry 的指向。\n    \n\n我们理一理，写入过程的整体流程就是：\n\n*   查 read，read 上没有，或者已标记删除状态。\n    \n*   上互斥锁（Mutex）。\n    \n*   操作 dirty，根据各种数据情况和状态进行处理。\n    \n\n回到最初的话题，为什么他写入性能差那么多。究其原因：\n\n*   写入一定要会经过 read，无论如何都比别人多一层，后续还要查数据情况和状态，性能开销相较更大。\n    \n*   （第三个处理分支）当初始化或者 dirty 被提升后，会从 read 中复制全量的数据，若 read 中数据量大，则会影响性能。\n    \n\n可得知 `sync.Map` 类型不适合写多的场景，读多写少是比较好的。\n\n若有大数据量的场景，则需要考虑 read 复制数据时的偶然性能抖动是否能够接受。\n\n### 删除过程\n\n这时候可能有小伙伴在想了。写入过程，理论上和删除不会差太远。怎么 `sync.Map` 类型的删除的性能似乎还行，这里面有什么猫腻？\n\n源码如下：\n\n```\nfunc (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {\n read, _ := m.read.Load().(readOnly)\n e, ok := read.m[key]\n ...\n  if ok {\n  return e.delete()\n }\n}\n\n```\n\n删除是标准的开场，依然先到 read 检查该元素是否存在。\n\n若存在，则调用 `delete` 标记为 expunged（删除状态），非常高效。可以明确在 read 中的元素，被删除，性能是非常好的。\n\n若不存在，也就是走到 dirty 流程中：\n\n```\nfunc (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {\n ...\n if !ok && read.amended {\n  m.mu.Lock()\n  read, _ = m.read.Load().(readOnly)\n  e, ok = read.m[key]\n  if !ok && read.amended {\n   e, ok = m.dirty[key]\n   delete(m.dirty, key)\n   m.missLocked()\n  }\n  m.mu.Unlock()\n }\n ...\n return nil, false\n}\n\n```\n\n若 read 中不存在该元素，dirty 不为空，read 与 dirty 不一致（利用 amended 判别），则表明要操作 dirty，上互斥锁。\n\n再重复进行双重检查，若 read 仍然不存在该元素。则调用 delete 方法从 dirty 中标记该元素的删除。\n\n需要注意，出现频率较高的 delete 方法：\n\n```\nfunc (e *entry) delete() (value interface{}, ok bool) {\n for {\n  p := atomic.LoadPointer(&e.p)\n  if p == nil || p == expunged {\n   return nil, false\n  }\n  if atomic.CompareAndSwapPointer(&e.p, p, nil) {\n   return *(*interface{})(p), true\n  }\n }\n}\n\n```\n\n该方法都是将 entry.p 置为 nil，并且标记为 expunged（删除状态），而**不是真真正正的删除**。\n\n注：不要误用 `sync.Map`，前段时间从字节大佬分享的案例来看，他们将一个连接作为 key 放了进去，于是和这个连接相关的，例如：buffer 的内存就永远无法释放了...\n\n总结\n--\n\n通过阅读本文，我们明确了 `sync.Map` 和原生 map +互斥锁/读写锁之间的性能情况。\n\n标准库 `sync.Map` 虽说支持并发读写 map，但更适用于读多写少的场景，因为他写入的性能比较差，使用时要考虑清楚这一点。\n\n另外我们针对 `sync.Map` 的性能差异，进行了深入的源码剖析，了解到了其背后快、慢的原因，实现了知其然知其所以然。\n\n经常看到并发读写 map 导致致命错误，实在是令人忧心。大家觉得如果本文不错，欢迎分享给更多的 Go 爱好者 ：）\n\n## 鼓励\n\n若有任何疑问欢迎评论区反馈和交流，**最好的关系是互相成就**，各位的**点赞**就是[煎鱼](https://github.com/eddycjy)创作的最大动力，感谢支持。\n\n> 文章持续更新，可以微信搜【脑子进煎鱼了】阅读，本文 **GitHub** [github.com/eddycjy/blog](https://github.com/eddycjy/blog) 已收录，欢迎 Star 催更。\n\n\n参考\n--\n\n*   Package sync\n    \n*   踩了 Golang sync.Map 的一个坑\n    \n*   go19-examples/benchmark-for-map\n    \n*   通过实例深入理解sync.Map的工作原理\n    "
  },
  {
    "path": "content/posts/go/talk/2018-03-13-golang-relatively-path.md",
    "content": "---\n\ntitle:      \"聊一聊，Go 的相对路径问题\"\ndate:       2018-03-13 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n## 前言\n\n`Golang` 中存在各种运行方式，如何**正确的引用文件路径**成为一个值得商议的问题\n\n以 [gin-blog](https://github.com/EDDYCJY/go-gin-example) 为例，当我们在项目根目录下，执行 `go run main.go` 时能够正常运行（`go build`也是正常的）\n```\n[$ gin-blog]# go run main.go\n[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:    export GIN_MODE=release\n - using code:    gin.SetMode(gin.ReleaseMode)\n\n[GIN-debug] GET    /api/v1/tags              --> gin-blog/routers/api/v1.GetTags (3 handlers)\n...\n```\n\n\n那么在不同的目录层级下，不同的方式运行，又是怎么样的呢，带着我们的疑问去学习\n\n## 问题\n1、 go run\n我们上移目录层级，到 `$GOPATH/src` 下，执行 `go run gin-blog/main.go`\n```\n[$ src]# go run gin-blog/main.go\n2018/03/12 16:06:13 Fail to parse 'conf/app.ini': open conf/app.ini: no such file or directory\nexit status 1\n```\n\n2、 go build，执行 `./gin-blog/main`\n```\n[$ src]# ./gin-blog/main\n2018/03/12 16:49:35 Fail to parse 'conf/app.ini': open conf/app.ini: no such file or directory\n```\n\n这时候你要打一个大大的问号，就是我的程序读取到什么地方去了\n\n--- \n\n我们通过分析得知，**`Golang`的相对路径是相对于执行命令时的目录**；自然也就读取不到了\n\n\n## 思考\n\n既然已经知道问题的所在点，我们就可以寻思做点什么 : )\n\n我们想到相对路径是相对执行命令的目录，那么我们获取可执行文件的地址，拼接起来不就好了吗？\n\n## 实践\n\n我们编写**获取当前可执行文件路径的方法**\n```\nimport (\n\t\"path/filepath\"\n\t\"os\"\n\t\"os/exec\"\n\t\"string\"\n)\n\nfunc GetAppPath() string {\n    file, _ := exec.LookPath(os.Args[0])\n    path, _ := filepath.Abs(file)\n    index := strings.LastIndex(path, string(os.PathSeparator))\n\n    return path[:index]\n}\n```\n将其放到启动代码处查看路径\n```\nlog.Println(GetAppPath())\n```\n\n我们分别执行以下两个命令，查看输出结果\n1、 go run \n```\n$ go run main.go\n2018/03/12 18:45:40 /tmp/go-build962610262/b001/exe\n```\n2、 go build\n```\n$ ./main\n2018/03/12 18:49:44 $GOPATH/src/gin-blog\n\n```\n\n## 剖析\n\n我们聚焦在 `go run` 的输出结果上，发现它是一个临时文件的地址，这是为什么呢？\n\n在`go help run`中，我们可以看到\n```\nRun compiles and runs the main package comprising the named Go source files.\nA Go source file is defined to be a file ending in a literal \".go\" suffix.\n```\n\n也就是 `go run` 执行时会将文件放到 `/tmp/go-build...` 目录下，编译并运行\n\n因此`go run main.go`出现`/tmp/go-build962610262/b001/exe`结果也不奇怪了，因为它已经跑到临时目录下去执行可执行文件了\n\n---\n\n这就已经很清楚了，那么我们想想，会出现哪些问题呢\n\n- 依赖相对路径的文件，出现路径出错的问题\n- `go run` 和 `go build` 不一样，一个到临时目录下执行，一个可手动在编译后的目录下执行，路径的处理方式会不同\n- 不断`go run`，不断产生新的临时文件\n\n\n这其实就是**根本原因**了，因为 `go run` 和 `go build` 的编译文件执行路径并不同，执行的层级也有可能不一样，自然而然就出现各种读取不到的奇怪问题了\n\n\n## 解决方案\n\n**一、获取编译后的可执行文件路径**\n\n1、 将配置文件的相对路径与`GetAppPath()`的结果相拼接，可解决`go build main.go`的可执行文件跨目录执行的问题（如：`./src/gin-blog/main`）\n```\nimport (\n\t\"path/filepath\"\n\t\"os\"\n\t\"os/exec\"\n\t\"string\"\n)\n\nfunc GetAppPath() string {\n    file, _ := exec.LookPath(os.Args[0])\n    path, _ := filepath.Abs(file)\n    index := strings.LastIndex(path, string(os.PathSeparator))\n\n    return path[:index]\n}\n```\n\n但是这种方式，对于`go run`依旧无效，这时候就需要2来补救\n\n2、 通过传递参数指定路径，可解决`go run`的问题\n```\npackage main\n\nimport (\n    \"flag\"\n    \"fmt\"\n)\n\nfunc main() {\n    var appPath string\n    flag.StringVar(&appPath, \"app-path\", \"app-path\")\n    flag.Parse()\n    fmt.Printf(\"App path: %s\", appPath)\n}\n```\n运行\n```\ngo run main.go --app-path \"Your project address\"\n```\n\n**二、增加`os.Getwd()`进行多层判断**\n\n参见 [beego](https://github.com/astaxie/beego/blob/master/config.go#L133-L146) 读取 `app.conf` 的代码\n\n该写法可兼容 `go build` 和在项目根目录执行 `go run` ，但是若跨目录执行 `go run` 就不行\n\n\n**三、配置全局系统变量**\n\n我们可以通过`os.Getenv`来获取系统全局变量，然后与相对路径进行拼接\n\n1、 设置项目工作区\n\n简单来说，就是设置项目（应用）的工作路径，然后与配置文件、日志文件等相对路径进行拼接，达到相对的绝对路径来保证路径一致\n\n参见 [gogs](https://github.com/gogits/gogs/blob/master/pkg/setting/setting.go#L351) 读取`GOGS_WORK_DIR`进行拼接的代码\n\n2、 利用系统自带变量\n\n简单来说就是通过系统自带的全局变量，例如`$HOME`等，将配置文件存放在`$HOME/conf`或`/etc/conf`下\n\n这样子就能更加固定的存放配置文件，**不需要额外去设置一个环境变量**\n\n（这点今早与一位SFer讨论了一波，感谢）\n\n## 拓展\n\n`go test` 在一些场景下也会遇到路径问题，因为`go test`只能够在当前目录执行，所以在执行测试用例的时候，你的执行目录已经是测试目录了\n\n需要注意的是，如果采用获取外部参数的办法，用 `os.args` 时，`go test -args` 和 `go run`、`go build` 会有命令行参数位置的不一致问题\n\n## 小结\n\n这三种解决方案，在目前可见的开源项目或介绍中都能找到这些的身影\n\n优缺点也是显而易见的，我认为应在**不同项目选定合适的解决方案**即可\n\n建议大家不要强依赖读取配置文件的模块，应当将其“堆积木”化，**需要什么配置才去注册什么配置变量**，可以解决一部分的问题\n\n大家又有什么想法呢，一起讨论一波？\n\n"
  },
  {
    "path": "content/posts/go/talk/2018-05-21-go-fake-useragent.md",
    "content": "---\n\ntitle:      \"Go 的 fake-useragent 了解一下\"\ndate:       2018-05-21 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n有的网站会根据 User-Agent 的不同，跳转到不同（PC、M）的站点，也有根据版本的不同给出不一样的提示等等，而 User-Agent 的变化更是爬虫里的基础姿势\n\n使用 Go 编写网络爬虫或需要模拟浏览器头（User-Agent）的时候，你是否会觉得很麻烦，获取请求头（Request Headers）的 User-Agent 还得找来找去，挺繁琐。先前我也遇到了这个问题，因此有了这个项目 [fake-useragent](https://github.com/EDDYCJY/fake-useragent)，用来解决你我的痛点\n\n项目地址：https://github.com/EDDYCJY/fake-useragent\n\n## 支持\n\n- All User-Agent Random\n- Chrome\n- InternetExplorer (IE)\n- Firefox\n- Safari\n- Android\n- MacOSX\n- IOS\n- Linux\n- IPhone\n- IPad\n- Computer\n- Mobile\n\n## 安装\n\n```\n$ go get github.com/EDDYCJY/fake-useragent\n```\n\n## 用法\n\n``` go\npackage main\n\nimport (\n\t\"log\"\n\n\t\"github.com/EDDYCJY/fake-useragent\"\n)\n\nfunc main() {\n\t// 推荐使用\n\trandom := browser.Random()\n\tlog.Printf(\"Random: %s\", random)\n\n\tchrome := browser.Chrome()\n\tlog.Printf(\"Chrome: %s\", chrome)\n\n\tinternetExplorer := browser.InternetExplorer()\n\tlog.Printf(\"IE: %s\", internetExplorer)\n\n\tfirefox := browser.Firefox()\n\tlog.Printf(\"Firefox: %s\", firefox)\n\n\tsafari := browser.Safari()\n\tlog.Printf(\"Safari: %s\", safari)\n\n\tandroid := browser.Android()\n\tlog.Printf(\"Android: %s\", android)\n\n\tmacOSX := browser.MacOSX()\n\tlog.Printf(\"MacOSX: %s\", macOSX)\n\n\tios := browser.IOS()\n\tlog.Printf(\"IOS: %s\", ios)\n\n\tlinux := browser.Linux()\n\tlog.Printf(\"Linux: %s\", linux)\n\n\tiphone := browser.IPhone()\n\tlog.Printf(\"IPhone: %s\", iphone)\n\n\tipad := browser.IPad()\n\tlog.Printf(\"IPad: %s\", ipad)\n\n\tcomputer := browser.Computer()\n\tlog.Printf(\"Computer: %s\", computer)\n\n\tmobile := browser.Mobile()\n\tlog.Printf(\"Mobile: %s\", mobile)\n}\n```\n\n### 定制\n\n你可以调整抓取数据源的最大页数、时间间隔以及最大超时时间。 如果不填写，则为默认值。\n\n``` go\nclient := browser.Client{\n\tMaxPage: 3,\n\tDelay: 200 * time.Millisecond,\n\tTimeout: 10 * time.Second,\n}\ncache := browser.Cache{}\nb := browser.NewBrowser(client, cache)\n\nrandom := b.Random()\n```\n\n更新浏览器头的临时文件缓存\n\n``` go\nclient := browser.Client{}\ncache := browser.Cache{\n\tUpdateFile: true,\n}\nb := browser.NewBrowser(client, cache)\n```\n**最后，建议常规用法就好，默认参数能够满足日常需求**\n\n## 输出\n\n``` sh\nRandom: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36\n\nChrome: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36\n\nIE: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)\n\nFirefox: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:41.0) Gecko/20100101 Firefox/41.0\n\nSafari: Mozilla/5.0 (iPhone; CPU iPhone OS 11_2_5 like Mac OS X) AppleWebKit/604.5.6 (KHTML, like Gecko) Version/11.0 Mobile/15D60 Safari/604.1\n\nAndroid: Mozilla/5.0 (Linux; Android 6.0; MYA-L22 Build/HUAWEIMYA-L22) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36\n\nMacOSX: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/602.2.14 (KHTML, like Gecko) Version/10.0.1 Safari/602.2.14\n\nIOS: Mozilla/5.0 (iPhone; CPU iPhone OS 10_1 like Mac OS X) AppleWebKit/602.2.14 (KHTML, like Gecko) Version/10.0 Mobile/14B72 Safari/602.1\n\nLinux: Mozilla/5.0 (X11; Linux x86_64; rv:42.0) Gecko/20100101 Firefox/42.0\n\nIPhone: Mozilla/5.0 (iPhone; CPU iPhone OS 10_2 like Mac OS X) AppleWebKit/602.3.12 (KHTML, like Gecko) Version/10.0 Mobile/14C92 Safari/602.1\n\nIPad: Mozilla/5.0 (iPad; CPU OS 5_0_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A405 Safari/7534.48.3\n\nComputer: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:54.0) Gecko/20100101 Firefox/54.0\n\nMobile: Mozilla/5.0 (Linux; Android 7.0; Redmi Note 4 Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.111 Mobile Safari/537.36\n```\n\n## 注意\n\n如果第一次使用，[fake-useragent](https://github.com/EDDYCJY/fake-useragent) 将收集数据并在临时目录中创建一个文件作为文件缓存，请耐心等待几秒钟\n\n## 最后\n\n如果在项目中发现了什么问题，欢迎提交 PR 或者 issue。希望你能够喜欢这个项目，根本目的还是为了解决痛点，欢迎 Star！😁\n\n---\n\n项目地址：https://github.com/EDDYCJY/fake-useragent\n\n"
  },
  {
    "path": "content/posts/go/talk/2018-06-07-go-redis-protocol.md",
    "content": "---\n\ntitle:      \"用 Go 来了解一下 Redis 通讯协议\"\ndate:       2018-06-07 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\nGo、PHP、Java... 都有那么多包来支撑你使用 Redis，那你是否有想过\n\n有了服务端，有了客户端，他们俩是怎样通讯，又是基于什么通讯协议做出交互的呢？\n\n## 介绍\n\n基于我们的目的，本文主要讲解和实践 Redis 的通讯协议\n\nRedis 的客户端和服务端是通过 TCP 连接来进行数据交互， 服务器默认的端口号为 6379\n\n客户端和服务器发送的命令或数据一律以 \\r\\n （CRLF）结尾（这是一条约定）\n\n## 协议\n\n在 Redis 中分为**请求**和**回复**，而请求协议又分为新版和旧版，新版统一请求协议在 Redis 1.2 版本中引入，最终在 Redis 2.0 版本成为 Redis 服务器通信的标准方式\n\n本文是基于新版协议来实现功能，不建议使用旧版（1.2 挺老旧了）。如下是新协议的各种范例：\n\n### 请求协议\n\n1、 格式示例\n\n```\n*<参数数量> CR LF\n$<参数 1 的字节数量> CR LF\n<参数 1 的数据> CR LF\n...\n$<参数 N 的字节数量> CR LF\n<参数 N 的数据> CR LF\n```\n\n在该协议下所有发送至 Redis 服务器的参数都是二进制安全（binary safe）的\n\n2、打印示例\n\n```\n*3\n$3\nSET\n$5\nmykey\n$7\nmyvalue\n```\n\n3、实际协议值\n\n```\n\"*3\\r\\n$3\\r\\nSET\\r\\n$5\\r\\nmykey\\r\\n$7\\r\\nmyvalue\\r\\n\"\n```\n\n这就是 Redis 的请求协议规范，按照范例1编写客户端逻辑，最终发送的是范例3，相信你已经有大致的概念了，Redis 的协议非常的简洁易懂，这也是好上手的原因之一，你可以想想协议这么定义的好处在哪？\n\n### 回复\n\nRedis 会根据你请求协议的不同（执行的命令结果也不同），返回多种不同类型的回复。在这个回复“协议”中，可以通过检查第一个字节，确定这个回复是什么类型，如下：\n\n- 状态回复（status reply）的第一个字节是 \"+\"\n- 错误回复（error reply）的第一个字节是 \"-\"\n- 整数回复（integer reply）的第一个字节是 \":\"\n- 批量回复（bulk reply）的第一个字节是 \"$\"\n- 多条批量回复（multi bulk reply）的第一个字节是 \"*\"\n\n\n有了回复的头部标识，结尾的 CRLF，你可以大致猜想出回复“协议”是怎么样的，但是实践才能得出真理，斎知道怕是你很快就忘记了 😀\n\n\n## 实践\n\n### 与 Redis 服务器交互\n\n```\npackage main\n\nimport (\n\t\"log\"\n\t\"net\"\n\t\"os\"\n\n\t\"github.com/EDDYCJY/redis-protocol-example/protocol\"\n)\n\nconst (\n\tAddress = \"127.0.0.1:6379\"\n\tNetwork = \"tcp\"\n)\n\nfunc Conn(network, address string) (net.Conn, error) {\n\tconn, err := net.Dial(network, address)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn conn, nil\n}\n\nfunc main() {\n        // 读取入参\n\targs := os.Args[1:]\n\tif len(args) <= 0 {\n\t\tlog.Fatalf(\"Os.Args <= 0\")\n\t}\n    \n        // 获取请求协议\n\treqCommand := protocol.GetRequest(args)\n\t\n\t// 连接 Redis 服务器\n\tredisConn, err := Conn(Network, Address)\n\tif err != nil {\n\t\tlog.Fatalf(\"Conn err: %v\", err)\n\t}\n\tdefer redisConn.Close()\n    \n        // 写入请求内容\n\t_, err = redisConn.Write(reqCommand)\n\tif err != nil {\n\t\tlog.Fatalf(\"Conn Write err: %v\", err)\n\t}\n    \n        // 读取回复\n\tcommand := make([]byte, 1024)\n\tn, err := redisConn.Read(command)\n\tif err != nil {\n\t\tlog.Fatalf(\"Conn Read err: %v\", err)\n\t}\n    \n        // 处理回复\n\treply, err := protocol.GetReply(command[:n])\n\tif err != nil {\n\t\tlog.Fatalf(\"protocol.GetReply err: %v\", err)\n\t}\n    \n        // 处理后的回复内容\n\tlog.Printf(\"Reply: %v\", reply)\n\t// 原始的回复内容\n\tlog.Printf(\"Command: %v\", string(command[:n]))\n}\n```\n\n在这里我们完成了整个 Redis 客户端和服务端交互的流程，分别如下：\n\n1、读取命令行参数：获取执行的 Redis 命令\n\n2、获取请求协议参数\n\n3、连接 Redis 服务器，获取连接句柄\n\n4、将请求协议参数写入连接：发送请求的命令行参数\n\n5、从连接中读取返回的数据：读取先前请求的回复数据\n\n6、根据回复“协议”内容，处理回复的数据集\n\n7、输出处理后的回复内容及原始回复内容\n\n### 请求\n\n```\nfunc GetRequest(args []string) []byte {\n\treq := []string{\n\t\t\"*\" + strconv.Itoa(len(args)),\n\t}\n\n\tfor _, arg := range args {\n\t\treq = append(req, \"$\"+strconv.Itoa(len(arg)))\n\t\treq = append(req, arg)\n\t}\n\n\tstr := strings.Join(req, \"\\r\\n\")\n\treturn []byte(str + \"\\r\\n\")\n}\n```\n\n通过对 Redis 的请求协议的分析，可得出它的规律，先加上标志位，计算参数总数量，再循环合并各个参数的字节数量、值就可以了\n\n### 回复\n\n```\nfunc GetReply(reply []byte) (interface{}, error) {\n\treplyType := reply[0]\n\tswitch replyType {\n\tcase StatusReply:\n\t\treturn doStatusReply(reply[1:])\n\tcase ErrorReply:\n\t\treturn doErrorReply(reply[1:])\n\tcase IntegerReply:\n\t\treturn doIntegerReply(reply[1:])\n\tcase BulkReply:\n\t\treturn doBulkReply(reply[1:])\n\tcase MultiBulkReply:\n\t\treturn doMultiBulkReply(reply[1:])\n\tdefault:\n\t\treturn nil, nil\n\t}\n}\n\nfunc doStatusReply(reply []byte) (string, error) {\n\tif len(reply) == 3 && reply[1] == 'O' && reply[2] == 'K' {\n\t\treturn OkReply, nil\n\t}\n\n\tif len(reply) == 5 && reply[1] == 'P' && reply[2] == 'O' && reply[3] == 'N' && reply[4] == 'G' {\n\t\treturn PongReply, nil\n\t}\n\n\treturn string(reply), nil\n}\n\nfunc doErrorReply(reply []byte) (string, error) {\n\treturn string(reply), nil\n}\n\nfunc doIntegerReply(reply []byte) (int, error) {\n\tpos := getFlagPos('\\r', reply)\n\tresult, err := strconv.Atoi(string(reply[:pos]))\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn result, nil\n}\n\n...\n```\n\n在这里我们对所有回复类型进行了分发，不同的回复标志位对应不同的处理方式，在这里需求注意几项问题，如下：\n\n1、当请求的值不存在，会将特殊值 -1 用作回复\n\n2、服务器发送的所有字符串都由 CRLF 结尾\n\n3、多条批量回复是可基于批量回复的，要注意理解\n\n4、无内容的多条批量回复是存在的\n\n最重要的是，对不同回复的规则的把控，能够让你更好的理解 Redis 的请求、回复的交互过程 👌\n\n## 小结\n\n写这篇文章的起因，是因为常常在使用 Redis 时，只是用，你不知道它是基于什么样的通讯协议来通讯，这样的感觉是十分难受的\n\n通过本文的讲解，我相信你已经大致了解 Redis 客户端是怎么样和服务端交互，也清楚了其所用的通讯原理，希望能够对你有所帮助！\n\n最后，如果想详细查看代码，右拐项目地址：https://github.com/EDDYCJY/redis-protocol-example\n\n如果对你有所帮助，欢迎点个 Star 👍\n\n## 参考\n\n- [通信协议](http://doc.redisfans.com/topic/protocol.html)\n"
  },
  {
    "path": "content/posts/go/talk/2018-11-25-gomock.md",
    "content": "---\n\ntitle:      \"使用 Gomock 进行单元测试\"\ndate:       2018-11-25 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n在实际项目中，需要进行单元测试的时候。却往往发现有一大堆依赖项。这时候就是 [Gomock](https://github.com/golang/mock) 大显身手的时候了\n\nGomock 是 Go 语言的一个 mock 框架，官方的那种 🤪\n\n## 安装\n\n```\n$ go get -u github.com/golang/mock/gomock\n$ go install github.com/golang/mock/mockgen\n```\n\n1. 第一步：我们将安装 gomock 第三方库和 mock 代码的生成工具 mockgen。而后者可以大大的节省我们的工作量。只需要了解其使用方式就可以\n\n2. 第二步：输入 `mockgen` 验证代码生成工具是否安装正确。若无法正常响应，请检查 `bin` 目录下是否包含该二进制文件\n\n### 用法\n\n在 `mockgen` 命令中，支持两种生成模式：\n\n1. source：从源文件生成 mock 接口（通过 -source 启用）\n\n```\nmockgen -source=foo.go [other options]\n```\n\n2. reflect：通过使用反射程序来生成 mock 接口。它通过传递两个非标志参数来启用：导入路径和逗号分隔的接口列表\n\n```\nmockgen database/sql/driver Conn,Driver\n```\n\n从本质上来讲，两种方式生成的 mock 代码并没有什么区别。因此选择合适的就可以了\n\n## 写测试用例\n\n在本文将模拟一个简单 Demo 来编写测试用例，熟悉整体的测试流程\n\n### 步骤\n\n1. 想清楚整体逻辑\n2. 定义想要（模拟）依赖项的 interface（接口）\n3. 使用 `mockgen` 命令对所需 mock 的 interface 生成 mock 文件\n4. 编写单元测试的逻辑，在测试中使用 mock\n5. 进行单元测试的验证\n\n### 目录\n\n```\n├── mock\n├── person\n│   └── male.go\n└── user\n    ├── user.go\n    └── user_test.go\n```\n\n### 编写\n\n#### interface 方法\n\n打开 person/male.go 文件，写入以下内容：\n\n```go\npackage person\n\ntype Male interface {\n\tGet(id int64) error\n}\n```\n\n#### 调用方法\n\n打开 user/user.go 文件，写入以下内容：\n\n```go\npackage user\n\nimport \"github.com/EDDYCJY/mockd/person\"\n\ntype User struct {\n\tPerson person.Male\n}\n\nfunc NewUser(p person.Male) *User {\n\treturn &User{Person: p}\n}\n\nfunc (u *User) GetUserInfo(id int64) error {\n\treturn u.Person.Get(id)\n}\n```\n\n#### 生成 mock 文件\n\n回到 `mockd/` 的根目录下，执行以下命令\n\n```\n$ mockgen -source=./person/male.go -destination=./mock/male_mock.go -package=mock\n```\n\n在执行完毕后，可以发现 `mock/` 目录下多出了 male_mock.go 文件，这就是 mock 文件。那么命令中的指令又分别有什么用呢？如下：\n\n- -source：设置需要模拟（mock）的接口文件\n- -destination：设置 mock 文件输出的地方，若不设置则打印到标准输出中\n- -package：设置 mock 文件的包名，若不设置则为 `mock_` 前缀加上文件名（如本文的包名会为 mock_person）\n\n想了解更多的指令符，可参见 [官方文档](https://github.com/golang/mock#running-mockgen)\n\n##### 输出的 mock 文件\n\n```go\n// Code generated by MockGen. DO NOT EDIT.\n// Source: ./person/male.go\n\n// Package mock is a generated GoMock package.\npackage mock\n\nimport (\n\tgomock \"github.com/golang/mock/gomock\"\n\treflect \"reflect\"\n)\n\n// MockMale is a mock of Male interface\ntype MockMale struct {\n\tctrl     *gomock.Controller\n\trecorder *MockMaleMockRecorder\n}\n\n// MockMaleMockRecorder is the mock recorder for MockMale\ntype MockMaleMockRecorder struct {\n\tmock *MockMale\n}\n\n// NewMockMale creates a new mock instance\nfunc NewMockMale(ctrl *gomock.Controller) *MockMale {\n\tmock := &MockMale{ctrl: ctrl}\n\tmock.recorder = &MockMaleMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use\nfunc (m *MockMale) EXPECT() *MockMaleMockRecorder {\n\treturn m.recorder\n}\n\n// Get mocks base method\nfunc (m *MockMale) Get(id int64) error {\n\tret := m.ctrl.Call(m, \"Get\", id)\n\tret0, _ := ret[0].(error)\n\treturn ret0\n}\n\n// Get indicates an expected call of Get\nfunc (mr *MockMaleMockRecorder) Get(id interface{}) *gomock.Call {\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Get\", reflect.TypeOf((*MockMale)(nil).Get), id)\n}\n```\n\n#### 测试用例\n\n打开 user/user_test.go 文件，写入以下内容：\n\n```go\npackage user\n\nimport (\n\t\"testing\"\n\n\t\"github.com/EDDYCJY/mockd/mock\"\n\n\t\"github.com/golang/mock/gomock\"\n)\n\nfunc TestUser_GetUserInfo(t *testing.T) {\n\tctl := gomock.NewController(t)\n\tdefer ctl.Finish()\n\n\tvar id int64 = 1\n\tmockMale := mock.NewMockMale(ctl)\n\tgomock.InOrder(\n\t\tmockMale.EXPECT().Get(id).Return(nil),\n\t)\n\n\tuser := NewUser(mockMale)\n\terr := user.GetUserInfo(id)\n\tif err != nil {\n\t\tt.Errorf(\"user.GetUserInfo err: %v\", err)\n\t}\n}\n```\n\n1. gomock.NewController：返回 `gomock.Controller`，它代表 mock 生态系统中的顶级控件。定义了 mock 对象的范围、生命周期和期待值。另外它在多个 goroutine 中是安全的\n2. mock.NewMockMale：创建一个新的 mock 实例\n3. gomock.InOrder：声明给定的调用应按顺序进行（是对 gomock.After 的二次封装）\n4. mockMale.EXPECT().Get(id).Return(nil)：这里有三个步骤，`EXPECT()`返回一个允许调用者设置**期望**和**返回值**的对象。`Get(id)` 是设置入参并调用 mock 实例中的方法。`Return(nil)` 是设置先前调用的方法出参。简单来说，就是设置入参并调用，最后设置返回值\n\n5. NewUser(mockMale)：创建 User 实例，值得注意的是，在这里**注入了 mock 对象**，因此实际在随后的 `user.GetUserInfo(id)` 调用（入参：id 为 1）中。它调用的是我们事先模拟好的 mock 方法\n6. ctl.Finish()：进行 mock 用例的期望值断言，一般会使用 `defer` 延迟执行，以防止我们忘记这一操作\n\n### 测试\n\n回到 `mockd/` 的根目录下，执行以下命令\n\n```\n$ go test ./user\nok  \tgithub.com/EDDYCJY/mockd/user\n```\n\n看到这样的结果，就大功告成啦！你可以自己调整一下 `Return()` 的返回值，以此得到不一样的测试结果哦 😄\n\n## 查看测试情况\n\n### 测试覆盖率\n\n```\n$ go test -cover ./user\nok  \tgithub.com/EDDYCJY/mockd/user\t(cached)\tcoverage: 100.0% of statements\n```\n\n可通过设置 `-cover` 标志符来开启覆盖率的统计，展示内容为 `coverage: 100.0%`。\n\n### 可视化界面\n\n1. 生成测试覆盖率的 profile 文件\n\n```\n$ go test ./... -coverprofile=cover.out\n```\n\n2. 利用 profile 文件生成可视化界面\n\n```\n$ go tool cover -html=cover.out\n```\n\n3. 查看可视化界面，分析覆盖情况\n\n![image](https://s2.ax1x.com/2020/02/27/3wKu7R.jpg)\n\n## 更多\n\n### 一、常用 mock 方法\n\n#### 调用方法\n\n- Call.Do()：声明在匹配时要运行的操作\n- Call.DoAndReturn()：声明在匹配调用时要运行的操作，并且模拟返回该函数的返回值\n- Call.MaxTimes()：设置最大的调用次数为 n 次\n- Call.MinTimes()：设置最小的调用次数为 n 次\n- Call.AnyTimes()：允许调用次数为 0 次或更多次\n- Call.Times()：设置调用次数为 n 次\n\n#### 参数匹配\n\n- gomock.Any()：匹配任意值\n- gomock.Eq()：通过反射匹配到指定的类型值，而不需要手动设置\n- gomock.Nil()：返回 nil\n\n建议更多的方法可参见 [官方文档](https://godoc.org/github.com/golang/mock/gomock#pkg-index)\n\n### 二、生成多个 mock 文件\n\n你可能会想一条条命令生成 mock 文件，岂不得崩溃？\n\n当然，官方提供了更方便的方式，我们可以利用 `go:generate` 来完成批量处理的功能\n\n```\ngo generate [-run regexp] [-n] [-v] [-x] [build flags] [file.go... | packages]\n```\n\n#### 修改 interface 方法\n\n打开 person/male.go 文件，修改为以下内容：\n\n```go\npackage person\n\n//go:generate mockgen -destination=../mock/male_mock.go -package=mock github.com/EDDYCJY/mockd/person Male\n\ntype Male interface {\n\tGet(id int64) error\n}\n```\n\n我们关注到 `go:generate` 这条语句，可分为以下部分：\n\n1. 声明 `//go:generate` （注意不要留空格）\n2. 使用 `mockgen` 命令\n3. 定义 `-destination`\n4. 定义 `-package`\n5. 定义 `source`，此处为 person 的包路径\n6. 定义 `interfaces`，此处为 `Male`\n\n#### 重新生成 mock 文件\n\n回到 `mockd/` 的根目录下，执行以下命令\n\n```\n$ go generate ./...\n```\n\n再检查 `mock/` 发现也已经正确生成了，在多个文件时是不是很方便呢 🤩\n\n## 总结\n\n在单元测试这一环，gomock 给我们提供了极大的便利。能够 mock 掉许许多多的依赖项\n\n其中还有很多的使用方式和功能。你可以 mark 住后详细阅读下官方文档，记忆会更深刻"
  },
  {
    "path": "content/posts/go/talk/2018-12-26-go-memory-align.md",
    "content": "---\n\ntitle:      \"在 Go 中恰到好处的内存对齐\"\ndate:       2018-12-26 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n![image](https://s2.ax1x.com/2020/02/27/3wuT0A.png)\n\n## 问题\n\n```go\ntype Part1 struct {\n\ta bool\n\tb int32\n\tc int8\n\td int64\n\te byte\n}\n```\n\n在开始之前，希望你计算一下 `Part1` 共占用的大小是多少呢？\n\n```go\nfunc main() {\n\tfmt.Printf(\"bool size: %d\\n\", unsafe.Sizeof(bool(true)))\n\tfmt.Printf(\"int32 size: %d\\n\", unsafe.Sizeof(int32(0)))\n\tfmt.Printf(\"int8 size: %d\\n\", unsafe.Sizeof(int8(0)))\n\tfmt.Printf(\"int64 size: %d\\n\", unsafe.Sizeof(int64(0)))\n\tfmt.Printf(\"byte size: %d\\n\", unsafe.Sizeof(byte(0)))\n\tfmt.Printf(\"string size: %d\\n\", unsafe.Sizeof(\"EDDYCJY\"))\n}\n```\n\n输出结果：\n\n```\nbool size: 1\nint32 size: 4\nint8 size: 1\nint64 size: 8\nbyte size: 1\nstring size: 16\n```\n\n这么一算，`Part1` 这一个结构体的占用内存大小为 1+4+1+8+1 = 15 个字节。相信有的小伙伴是这么算的，看上去也没什么毛病\n\n真实情况是怎么样的呢？我们实际调用看看，如下：\n\n```go\ntype Part1 struct {\n\ta bool\n\tb int32\n\tc int8\n\td int64\n\te byte\n}\n\nfunc main() {\n\tpart1 := Part1{}\n\n\tfmt.Printf(\"part1 size: %d, align: %d\\n\", unsafe.Sizeof(part1), unsafe.Alignof(part1))\n}\n```\n\n输出结果：\n\n```\npart1 size: 32, align: 8\n```\n\n最终输出为占用 32 个字节。这与前面所预期的结果完全不一样。这充分地说明了先前的计算方式是错误的。为什么呢？\n\n在这里要提到 “内存对齐” 这一概念，才能够用正确的姿势去计算，接下来我们详细的讲讲它是什么\n\n## 内存对齐\n\n有的小伙伴可能会认为内存读取，就是一个简单的字节数组摆放\n\n![image](https://s2.ax1x.com/2020/02/27/3wuLff.png)\n\n上图表示一个坑一个萝卜的内存读取方式。但实际上 CPU 并不会以一个一个字节去读取和写入内存。相反 CPU 读取内存是**一块一块读取**的，块的大小可以为 2、4、6、8、16 字节等大小。块大小我们称其为**内存访问粒度**。如下图：\n\n![image](https://s2.ax1x.com/2020/02/27/3wKSmj.png)\n\n在样例中，假设访问粒度为 4。 CPU 是以每 4 个字节大小的访问粒度去读取和写入内存的。这才是正确的姿势\n\n### 为什么要关心对齐\n\n- 你正在编写的代码在性能（CPU、Memory）方面有一定的要求\n- 你正在处理向量方面的指令\n- 某些硬件平台（ARM）体系不支持未对齐的内存访问\n\n另外作为一个工程师，你也很有必要学习这块知识点哦 :)\n\n### 为什么要做对齐\n\n- 平台（移植性）原因：不是所有的硬件平台都能够访问任意地址上的任意数据。例如：特定的硬件平台只允许在特定地址获取特定类型的数据，否则会导致异常情况\n- 性能原因：若访问未对齐的内存，将会导致 CPU 进行两次内存访问，并且要花费额外的时钟周期来处理对齐及运算。而本身就对齐的内存仅需要一次访问就可以完成读取动作\n\n![image](https://s2.ax1x.com/2020/02/27/3wKApT.png)\n\n在上图中，假设从 Index 1 开始读取，将会出现很崩溃的问题。因为它的内存访问边界是不对齐的。因此 CPU 会做一些额外的处理工作。如下：\n\n1. CPU **首次**读取未对齐地址的第一个内存块，读取 0-3 字节。并移除不需要的字节 0\n2. CPU **再次**读取未对齐地址的第二个内存块，读取 4-7 字节。并移除不需要的字节 5、6、7 字节\n3. 合并 1-4 字节的数据\n4. 合并后放入寄存器\n\n从上述流程可得出，不做 “内存对齐” 是一件有点 \"麻烦\" 的事。因为它会增加许多耗费时间的动作\n\n而假设做了内存对齐，从 Index 0 开始读取 4 个字节，只需要读取一次，也不需要额外的运算。这显然高效很多，是标准的**空间换时间**做法\n\n### 默认系数\n\n在不同平台上的编译器都有自己默认的 “对齐系数”，可通过预编译命令 `#pragma pack(n)` 进行变更，n 就是代指 “对齐系数”。一般来讲，我们常用的平台的系数如下：\n\n- 32 位：4\n- 64 位：8\n\n另外要注意，不同硬件平台占用的大小和对齐值都可能是不一样的。因此本文的值不是唯一的，调试的时候需按本机的实际情况考虑\n\n### 成员对齐\n\n```go\nfunc main() {\n\tfmt.Printf(\"bool align: %d\\n\", unsafe.Alignof(bool(true)))\n\tfmt.Printf(\"int32 align: %d\\n\", unsafe.Alignof(int32(0)))\n\tfmt.Printf(\"int8 align: %d\\n\", unsafe.Alignof(int8(0)))\n\tfmt.Printf(\"int64 align: %d\\n\", unsafe.Alignof(int64(0)))\n\tfmt.Printf(\"byte align: %d\\n\", unsafe.Alignof(byte(0)))\n\tfmt.Printf(\"string align: %d\\n\", unsafe.Alignof(\"EDDYCJY\"))\n\tfmt.Printf(\"map align: %d\\n\", unsafe.Alignof(map[string]string{}))\n}\n```\n\n输出结果：\n\n```\nbool align: 1\nint32 align: 4\nint8 align: 1\nint64 align: 8\nbyte align: 1\nstring align: 8\nmap align: 8\n```\n\n在 Go 中可以调用 `unsafe.Alignof` 来返回相应类型的对齐系数。通过观察输出结果，可得知基本都是 `2^n`，最大也不会超过 8。这是因为我手提（64 位）编译器默认对齐系数是 8，因此最大值不会超过这个数\n\n### 整体对齐\n\n在上小节中，提到了结构体中的成员变量要做字节对齐。那么想当然身为最终结果的结构体，也是需要做字节对齐的\n\n### 对齐规则\n\n- 结构体的成员变量，第一个成员变量的偏移量为 0。往后的每个成员变量的对齐值必须为**编译器默认对齐长度**（`#pragma pack(n)`）或**当前成员变量类型的长度**（`unsafe.Sizeof`），取**最小值作为当前类型的对齐值**。其偏移量必须为对齐值的整数倍\n- 结构体本身，对齐值必须为**编译器默认对齐长度**（`#pragma pack(n)`）或**结构体的所有成员变量类型中的最大长度**，取**最大数的最小整数倍**作为对齐值\n- 结合以上两点，可得知若**编译器默认对齐长度**（`#pragma pack(n)`）超过结构体内成员变量的类型最大长度时，默认对齐长度是没有任何意义的\n\n## 分析流程\n\n接下来我们一起分析一下，“它” 到底经历了些什么，影响了 “预期” 结果\n\n| 成员变量   | 类型  | 偏移量 | 自身占用 |\n| ---------- | ----- | ------ | -------- |\n| a          | bool  | 0      | 1        |\n| 字节对齐   | 无    | 1      | 3        |\n| b          | int32 | 4      | 4        |\n| c          | int8  | 8      | 1        |\n| 字节对齐   | 无    | 9      | 7        |\n| d          | int64 | 16     | 8        |\n| e          | byte  | 24     | 1        |\n| 字节对齐   | 无    | 25     | 7        |\n| 总占用大小 | -     | -      | 32       |\n\n### 成员对齐\n\n- 第一个成员 a\n  - 类型为 bool\n  - 大小/对齐值为 1 字节\n  - 初始地址，偏移量为 0。占用了第 1 位\n- 第二个成员 b\n  - 类型为 int32\n  - 大小/对齐值为 4 字节\n  - 根据规则 1，其偏移量必须为 4 的整数倍。确定偏移量为 4，因此 2-4 位为 Padding。而当前数值从第 5 位开始填充，到第 8 位。如下：axxx|bbbb\n- 第三个成员 c\n  - 类型为 int8\n  - 大小/对齐值为 1 字节\n  - 根据规则 1，其偏移量必须为 1 的整数倍。当前偏移量为 8。不需要额外对齐，填充 1 个字节到第 9 位。如下：axxx|bbbb|c...\n- 第四个成员 d\n  - 类型为 int64\n  - 大小/对齐值为 8 字节\n  - 根据规则 1，其偏移量必须为 8 的整数倍。确定偏移量为 16，因此 9-16 位为 Padding。而当前数值从第 17 位开始写入，到第 24 位。如下：axxx|bbbb|cxxx|xxxx|dddd|dddd\n- 第五个成员 e\n  - 类型为 byte\n  - 大小/对齐值为 1 字节\n  - 根据规则 1，其偏移量必须为 1 的整数倍。当前偏移量为 24。不需要额外对齐，填充 1 个字节到第 25 位。如下：axxx|bbbb|cxxx|xxxx|dddd|dddd|e...\n\n### 整体对齐\n\n在每个成员变量进行对齐后，根据规则 2，整个结构体本身也要进行字节对齐，因为可发现它可能并不是 `2^n`，不是偶数倍。显然不符合对齐的规则\n\n根据规则 2，可得出对齐值为 8。现在的偏移量为 25，不是 8 的整倍数。因此确定偏移量为 32。对结构体进行对齐\n\n### 结果\n\nPart1 内存布局：axxx|bbbb|cxxx|xxxx|dddd|dddd|exxx|xxxx\n\n### 小结\n\n通过本节的分析，可得知先前的 “推算” 为什么错误？\n\n是因为实际内存管理并非 “一个萝卜一个坑” 的思想。而是一块一块。通过空间换时间（效率）的思想来完成这块读取、写入。另外也需要兼顾不同平台的内存操作情况\n\n## 巧妙的结构体\n\n在上一小节，可得知根据成员变量的类型不同，其结构体的内存会产生对齐等动作。那假设字段顺序不同，会不会有什么变化呢？我们一起来试试吧 :-)\n\n```go\ntype Part1 struct {\n\ta bool\n\tb int32\n\tc int8\n\td int64\n\te byte\n}\n\ntype Part2 struct {\n\te byte\n\tc int8\n\ta bool\n\tb int32\n\td int64\n}\n\nfunc main() {\n\tpart1 := Part1{}\n\tpart2 := Part2{}\n\n\tfmt.Printf(\"part1 size: %d, align: %d\\n\", unsafe.Sizeof(part1), unsafe.Alignof(part1))\n\tfmt.Printf(\"part2 size: %d, align: %d\\n\", unsafe.Sizeof(part2), unsafe.Alignof(part2))\n}\n```\n\n输出结果：\n\n```\npart1 size: 32, align: 8\npart2 size: 16, align: 8\n```\n\n通过结果可以惊喜的发现，只是 “简单” 对成员变量的字段顺序进行改变，就改变了结构体占用大小\n\n接下来我们一起剖析一下 `Part2`，看看它的内部到底和上一位之间有什么区别，才导致了这样的结果？\n\n### 分析流程\n\n| 成员变量   | 类型  | 偏移量 | 自身占用 |\n| ---------- | ----- | ------ | -------- |\n| e          | byte  | 0      | 1        |\n| c          | int8  | 1      | 1        |\n| a          | bool  | 2      | 1        |\n| 字节对齐   | 无    | 3      | 1        |\n| b          | int32 | 4      | 4        |\n| d          | int64 | 8      | 8        |\n| 总占用大小 | -     | -      | 16       |\n\n#### 成员对齐\n\n- 第一个成员 e\n  - 类型为 byte\n  - 大小/对齐值为 1 字节\n  - 初始地址，偏移量为 0。占用了第 1 位\n- 第二个成员 c\n  - 类型为 int8\n  - 大小/对齐值为 1 字节\n  - 根据规则 1，其偏移量必须为 1 的整数倍。当前偏移量为 2。不需要额外对齐\n- 第三个成员 a\n  - 类型为 bool\n  - 大小/对齐值为 1 字节\n  - 根据规则 1，其偏移量必须为 1 的整数倍。当前偏移量为 3。不需要额外对齐\n- 第四个成员 b\n  - 类型为 int32\n  - 大小/对齐值为 4 字节\n  - 根据规则 1，其偏移量必须为 4 的整数倍。确定偏移量为 4，因此第 3 位为 Padding。而当前数值从第 4 位开始填充，到第 8 位。如下：ecax|bbbb\n- 第五个成员 d\n  - 类型为 int64\n  - 大小/对齐值为 8 字节\n  - 根据规则 1，其偏移量必须为 8 的整数倍。当前偏移量为 8。不需要额外对齐，从 9-16 位填充 8 个字节。如下：ecax|bbbb|dddd|dddd\n\n#### 整体对齐\n\n符合规则 2，不需要额外对齐\n\n#### 结果\n\nPart2 内存布局：ecax|bbbb|dddd|dddd\n\n## 总结\n\n通过对比 `Part1` 和 `Part2` 的内存布局，你会发现两者有很大的不同。如下：\n\n- Part1：axxx|bbbb|cxxx|xxxx|dddd|dddd|exxx|xxxx\n\n- Part2：ecax|bbbb|dddd|dddd\n\n仔细一看，`Part1` 存在许多 Padding。显然它占据了不少空间，那么 Padding 是怎么出现的呢？\n\n通过本文的介绍，可得知是由于不同类型导致需要进行字节对齐，以此保证内存的访问边界\n\n那么也不难理解，为什么**调整结构体内成员变量的字段顺序**就能达到缩小结构体占用大小的疑问了，是因为巧妙地减少了 Padding 的存在。让它们更 “紧凑” 了。这一点对于加深 Go 的内存布局印象和大对象的优化非常有帮\n\n当然了，没什么特殊问题，你可以不关注这一块。但你要知道这块知识点 😄\n\n## 参考\n\n- [Data structure alignment](https://en.wikipedia.org/wiki/Data_structure_alignment)\n- [Data alignment](https://www.ibm.com/developerworks/library/pa-dalign/)\n"
  },
  {
    "path": "content/posts/go/talk/2019-01-20-control-goroutine.md",
    "content": "---\n\ntitle:      \"来，控制一下 goroutine 的并发数量\"\ndate:       2019-01-20 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n![image](https://s2.ax1x.com/2020/02/27/3wnOsJ.jpg)\n\n## 问题\n\n```go\nfunc main() {\n\tuserCount := math.MaxInt64\n\tfor i := 0; i < userCount; i++ {\n\t\tgo func(i int) {\n\t\t    // 做一些各种各样的业务逻辑处理\n\t\t\tfmt.Printf(\"go func: %d\\n\", i)\n\t\t\ttime.Sleep(time.Second)\n\t\t}(i)\n\t}\n}\n```\n\n在这里，假设 `userCount` 是一个外部传入的参数（不可预测，有可能值非常大），有人会全部丢进去循环。想着全部都并发 goroutine 去同时做某一件事。觉得这样子会效率会更高，对不对！\n\n那么，你觉得这里有没有什么问题？\n\n## 噩梦般的开始\n\n当然，在**特定场景下**，问题可大了。因为在本文被丢进去同时并发的可是一个极端值。我们可以一起观察下图的指标分析，看看情况有多 “崩溃”。下图是上述代码的表现：\n\n### 输出结果\n\n```\n...\ngo func: 5839\ngo func: 5840\ngo func: 5841\ngo func: 5842\ngo func: 5915\ngo func: 5524\ngo func: 5916\ngo func: 8209\ngo func: 8264\nsignal: killed\n```\n\n如果你自己执行过代码，在 “输出结果” 上你会遇到如下问题：\n\n- 系统资源占用率不断上涨\n- 输出一定数量后：控制台就不再刷新输出最新的值了\n- 信号量：signal: killed\n\n### 系统负载\n\n![image](https://s2.ax1x.com/2020/02/27/3wnxd1.jpg)\n\n### CPU\n\n![image](https://s2.ax1x.com/2020/02/27/3wuKW8.jpg)\n\n短时间内系统负载暴增\n\n### 虚拟内存\n\n![image](https://s2.ax1x.com/2020/02/27/3wu1yQ.jpg)\n\n短时间内占用的虚拟内存暴增\n\n### top\n\n```\nPID    COMMAND      %CPU  TIME     #TH   #WQ  #PORT MEM    PURG   CMPRS  PGRP  PPID  STATE    BOOSTS\n...\n73414  test         100.2 01:59.50 9/1   0    18    6801M+ 0B     114G+  73403 73403 running  *0[1]\n```\n\n### 小结\n\n如果仔细看过监控工具的示意图，就可以知道其实我间隔的执行了两次，能看到系统间的使用率幅度非常大。当进程被杀掉后，整体又恢复为正常值\n\n在这里，我们回到主题，就是在**不控制并发的 goroutine 数量** 会发生什么问题？大致如下：\n\n- CPU 使用率浮动上涨\n- Memory 占用不断上涨。也可以看看 CMPRS，它表示进程的压缩数据的字节数。已经到达 114G+ 了\n- 主进程崩溃（被杀掉了）\n\n简单来说，“崩溃” 的原因就是对系统资源的占用过大。常见的比如：打开文件数（too many files open）、内存占用等等\n\n### 危害\n\n对该台服务器产生非常大的影响，影响自身及相关联的应用。很有可能导致不可用或响应缓慢，另外启动了复数 “失控” 的 goroutine，导致程序流转混乱\n\n## 解决方案\n\n在前面花了大量篇幅，渲染了在存在大量并发 goroutine 数量时，不控制的话会出现 “严重” 的问题，接下来一起思考下解决方案。如下：\n\n1. 控制/限制 goroutine 同时并发运行的数量\n2. 改变应用程序的逻辑写法（避免大规模的使用系统资源和等待）\n3. ~~调整服务的硬件配置、最大打开数、内存等阈值~~\n\n## 控制 goroutine 并发数量\n\n接下来正式的开始解决这个问题，希望你认真阅读的同时加以思考，因为这个问题在实际项目中真的是太常见了！\n\n问题已经抛出来了，你需要做的是**想想有什么办法**解决这个问题。建议你自行思考一下技术方案。再接着往下看 :-)\n\n### 尝试 chan\n\n```go\nfunc main() {\n\tuserCount := 10\n\tch := make(chan bool, 2)\n\tfor i := 0; i < userCount; i++ {\n\t\tch <- true\n\t\tgo Read(ch, i)\n\t}\n\n\t//time.Sleep(time.Second)\n}\n\nfunc Read(ch chan bool, i int) {\n\tfmt.Printf(\"go func: %d\\n\", i)\n\t<- ch\n}\n```\n\n输出结果：\n\n```\ngo func: 1\ngo func: 2\ngo func: 3\ngo func: 4\ngo func: 5\ngo func: 6\ngo func: 7\ngo func: 8\ngo func: 0\n```\n\n嗯，我们似乎很好的控制了 2 个 2 个的 “顺序” 执行多个 goroutine。但是，问题出现了。你仔细数一下输出结果，才 9 个值？\n\n这明显就不对。原因出在当主协程结束时，子协程也是会被终止掉的。因此剩余的 goroutine 没来及把值输出，就被送上路了（不信你把 `time.Sleep` 打开看看，看看输出数量）\n\n### 尝试 sync\n\n```go\n...\nvar wg = sync.WaitGroup{}\n\nfunc main() {\n\tuserCount := 10\n\tfor i := 0; i < userCount; i++ {\n\t\twg.Add(1)\n\t\tgo Read(i)\n\t}\n\n\twg.Wait()\n}\n\nfunc Read(i int) {\n\tdefer wg.Done()\n\tfmt.Printf(\"go func: %d\\n\", i)\n}\n```\n\n嗯，单纯的使用 `sync.WaitGroup` 也不行。没有控制到同时并发的 goroutine 数量（代指达不到本文所要求的目标）\n\n#### 小结\n\n单纯**简单**使用 channel 或 sync 都有明显缺陷，不行。我们再看看组件配合能不能实现\n\n### 尝试 chan + sync\n\n```go\n...\nvar wg = sync.WaitGroup{}\n\nfunc main() {\n\tuserCount := 10\n\tch := make(chan bool, 2)\n\tfor i := 0; i < userCount; i++ {\n\t\twg.Add(1)\n\t\tgo Read(ch, i)\n\t}\n\n\twg.Wait()\n}\n\nfunc Read(ch chan bool, i int) {\n\tdefer wg.Done()\n\n\tch <- true\n\tfmt.Printf(\"go func: %d, time: %d\\n\", i, time.Now().Unix())\n\ttime.Sleep(time.Second)\n\t<-ch\n}\n```\n\n输出结果：\n\n```\ngo func: 9, time: 1547911938\ngo func: 1, time: 1547911938\ngo func: 6, time: 1547911939\ngo func: 7, time: 1547911939\ngo func: 8, time: 1547911940\ngo func: 0, time: 1547911940\ngo func: 3, time: 1547911941\ngo func: 2, time: 1547911941\ngo func: 4, time: 1547911942\ngo func: 5, time: 1547911942\n```\n\n从输出结果来看，确实实现了控制 goroutine 以 2 个 2 个的数量去执行我们的 “业务逻辑”，当然结果集也理所应当的是乱序输出\n\n### 方案一：简单 Semaphore\n\n在确立了简单使用 chan + sync 的方案是可行后，我们重新将流转逻辑封装为 [gsema](https://github.com/EDDYCJY/gsema)，主程序变成如下：\n\n```go\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/EDDYCJY/gsema\"\n)\n\nvar sema = gsema.NewSemaphore(3)\n\nfunc main() {\n\tuserCount := 10\n\tfor i := 0; i < userCount; i++ {\n\t\tgo Read(i)\n\t}\n\n\tsema.Wait()\n}\n\nfunc Read(i int) {\n\tdefer sema.Done()\n\tsema.Add(1)\n\n\tfmt.Printf(\"go func: %d, time: %d\\n\", i, time.Now().Unix())\n\ttime.Sleep(time.Second)\n}\n```\n\n### 分析方案\n\n在上述代码中，程序执行流程如下：\n\n- 设置允许的并发数目为 3 个\n- 循环 10 次，每次启动一个 goroutine 来执行任务\n- 每一个 goroutine 在内部利用 `sema` 进行调控是否阻塞\n- 按允许并发数逐渐释出 goroutine，最后结束任务\n\n看上去人模人样，没什么严重问题。但却有一个 “大” 坑，认真看到第二点 “每次启动一个 goroutine” 这句话。这里**有点问题**，提前产生那么多的 goroutine 会不会有什么问题，接下来一起分析下利弊，如下：\n\n#### 利\n\n- 适合**量不大、复杂度低**的使用场景\n  - 几百几千个、几十万个也是可以接受的（看具体业务场景）\n  - 实际业务逻辑在运行前就已经被阻塞等待了（因为并发数受限），基本实际业务逻辑损耗的性能比 goroutine 本身大\n  - goroutine 本身很轻便，仅损耗极少许的内存空间和调度。这种等待响应的情况都是躺好了，等待任务唤醒\n- Semaphore 操作复杂度低且流转简单，容易控制\n\n#### 弊\n\n- 不适合**量很大、复杂度高**的使用场景\n  - 有几百万、几千万个 goroutine 的话，就浪费了大量调度 goroutine 和内存空间。恰好你的服务器也接受不了的话\n- Semaphore 操作复杂度提高，要管理更多的状态\n\n### 小结\n\n- 基于什么业务场景，就用什么方案去做事\n- 有足够的时间，允许你去追求更优秀、极致的方案（用第三方库也行）\n\n用哪种方案，我认为主要基于以上两点去思考，都是 OK 的。没有对错，只有当前业务场景能不能接受，这个预先启动的 goroutine 数量你的系统是否能够接受\n\n当然了，常见/简单的 Go 应用采用这类技术方案，基本就能解决问题了。因为像本文第一节 “问题” 如此超巨大数量的情况，情况很少。其并不存在那些 “特殊性”。因此用这个方案基本 OK\n\n## 灵活控制 goroutine 并发数量\n\n小手一紧。隔壁老王发现了新的问题。“方案一” 中，在**输入输出一体**的情况下，在常见的业务场景中确实可以\n\n但，这次新的业务场景比较特殊，要控制输入的数量，以此达到**改变允许并发运行 goroutine 的数量**。我们仔细想想，要做出如下改变：\n\n- 输入/输出要抽离，才可以分别控制\n- 输入/输出要可变，理所应当在 for-loop 中（可设置数值的地方）\n- 允许改变 goroutine 并发数量，但它也必须有一个**最大值**（因为允许改变是相对）\n\n### 方案二：灵活 chan + sync\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n)\n\nvar wg sync.WaitGroup\n\nfunc main() {\n\tuserCount := 10\n\tch := make(chan int, 5)\n\tfor i := 0; i < userCount; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tfor d := range ch {\n\t\t\t\tfmt.Printf(\"go func: %d, time: %d\\n\", d, time.Now().Unix())\n\t\t\t\ttime.Sleep(time.Second * time.Duration(d))\n\t\t\t}\n\t\t}()\n\t}\n\n\tfor i := 0; i < 10; i++ {\n\t\tch <- 1\n\t\tch <- 2\n\t\t//time.Sleep(time.Second)\n\t}\n\n\tclose(ch)\n\twg.Wait()\n}\n```\n\n输出结果：\n\n```\n...\ngo func: 1, time: 1547950567\ngo func: 3, time: 1547950567\ngo func: 1, time: 1547950567\ngo func: 2, time: 1547950567\ngo func: 2, time: 1547950567\ngo func: 3, time: 1547950567\ngo func: 1, time: 1547950568\ngo func: 2, time: 1547950568\ngo func: 3, time: 1547950568\ngo func: 1, time: 1547950568\ngo func: 3, time: 1547950569\ngo func: 2, time: 1547950569\n```\n\n在 “方案二” 中，我们可以随时随地的根据新的业务需求，做如下事情：\n\n- 变更 channel 的输入数量\n- 能够根据特殊情况，变更 channel 的循环值\n- 变更最大允许并发的 goroutine 数量\n\n总的来说，就是可控空间都尽量放开了，是不是更加灵活了呢 :-)\n\n### 方案三：第三方库\n\n- [go-playground/pool](https://github.com/go-playground/pool)\n- [nozzle/throttler](https://github.com/nozzle/throttler)\n- [Jeffail/tunny](https://github.com/Jeffail/tunny)\n- [panjf2000/ants](https://github.com/panjf2000/ants)\n\n比较成熟的第三方库也不少，基本都是以生成和管理 goroutine 为目标的池工具。我简单列了几个，具体建议大家阅读下源码或者多找找，原理相似\n\n## 总结\n\n在本文的开头，我花了大力气（极端数量），告诉你**同时并发过多的 goroutine 数量会导致系统占用资源不断上涨。最终该服务崩盘的极端情况**。为的是希望你今后避免这种问题，给你留下深刻的印象\n\n接下来我们以 “控制 goroutine 并发数量” 为主题，展开了一番分析。分别给出了三种方案。在我看来，各具优缺点，我建议你**挑选合适自身场景的技术方案**就可以了\n\n因为，有不同类型的技术方案也能解决这个问题，千人千面。本文推荐的是较常见的解决方案，也欢迎大家在评论区继续补充 :-)\n"
  },
  {
    "path": "content/posts/go/talk/2019-02-17-for-loop-json-unmarshal.md",
    "content": "---\n\ntitle:      \"for-loop 与 json.Unmarshal 性能分析概要\"\ndate:       2019-02-17 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n在项目中，常常会遇到循环交换赋值的数据处理场景，尤其是 RPC，数据交互格式要转为 Protobuf，赋值是无法避免的。一般会有如下几种做法：\n\n- for\n- for range\n- json.Marshal/Unmarshal\n\n这时候又面临 “选择困难症”，用哪个好？又想代码量少，又担心性能有没有影响啊...\n\n为了弄清楚这个疑惑，接下来将分别编写三种使用场景。来简单看看它们的性能情况，看看谁更 “好”\n\n## 功能代码\n\n```go\n...\ntype Person struct {\n\tName   string `json:\"name\"`\n\tAge    int    `json:\"age\"`\n\tAvatar string `json:\"avatar\"`\n\tType   string `json:\"type\"`\n}\n\ntype AgainPerson struct {\n\tName   string `json:\"name\"`\n\tAge    int    `json:\"age\"`\n\tAvatar string `json:\"avatar\"`\n\tType   string `json:\"type\"`\n}\n\nconst MAX = 10000\n\nfunc InitPerson() []Person {\n\tvar persons []Person\n\tfor i := 0; i < MAX; i++ {\n\t\tpersons = append(persons, Person{\n\t\t\tName:   \"EDDYCJY\",\n\t\t\tAge:    i,\n\t\t\tAvatar: \"https://github.com/EDDYCJY\",\n\t\t\tType:   \"Person\",\n\t\t})\n\t}\n\n\treturn persons\n}\n\nfunc ForStruct(p []Person, count int) {\n\tfor i := 0; i < count; i++ {\n\t\t_, _ = i, p[i]\n\t}\n}\n\nfunc ForRangeStruct(p []Person) {\n\tfor i, v := range p {\n\t\t_, _ = i, v\n\t}\n}\n\nfunc JsonToStruct(data []byte, againPerson []AgainPerson) ([]AgainPerson, error) {\n\terr := json.Unmarshal(data, &againPerson)\n\treturn againPerson, err\n}\n\nfunc JsonIteratorToStruct(data []byte, againPerson []AgainPerson) ([]AgainPerson, error) {\n\tvar jsonIter = jsoniter.ConfigCompatibleWithStandardLibrary\n\terr := jsonIter.Unmarshal(data, &againPerson)\n\treturn againPerson, err\n}\n```\n\n## 测试代码\n\n```go\n...\nfunc BenchmarkForStruct(b *testing.B) {\n\tperson := InitPerson()\n\tcount := len(person)\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tForStruct(person, count)\n\t}\n}\n\nfunc BenchmarkForRangeStruct(b *testing.B) {\n\tperson := InitPerson()\n\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tForRangeStruct(person)\n\t}\n}\n\nfunc BenchmarkJsonToStruct(b *testing.B) {\n\tvar (\n\t\tperson = InitPerson()\n\t\tagainPersons []AgainPerson\n\t)\n\tdata, err := json.Marshal(person)\n\tif err != nil {\n\t\tb.Fatalf(\"json.Marshal err: %v\", err)\n\t}\n\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tJsonToStruct(data, againPersons)\n\t}\n}\n\nfunc BenchmarkJsonIteratorToStruct(b *testing.B) {\n\tvar (\n\t\tperson = InitPerson()\n\t\tagainPersons []AgainPerson\n\t)\n\tdata, err := json.Marshal(person)\n\tif err != nil {\n\t\tb.Fatalf(\"json.Marshal err: %v\", err)\n\t}\n\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tJsonIteratorToStruct(data, againPersons)\n\t}\n}\n```\n\n## 测试结果\n\n```\nBenchmarkForStruct-4              \t  500000\t      3289 ns/op\t       0 B/op\t       0 allocs/op\nBenchmarkForRangeStruct-4         \t  200000\t      9178 ns/op\t       0 B/op\t       0 allocs/op\nBenchmarkJsonToStruct-4           \t     100\t  19173117 ns/op\t 2618509 B/op\t   40036 allocs/op\nBenchmarkJsonIteratorToStruct-4   \t     300\t   4116491 ns/op\t 3694017 B/op\t   30047 allocs/op\n```\n\n从测试结果来看，性能排名为：for < for range < json-iterator < encoding/json。接下来我们看看是什么原因导致了这样子的排名？\n\n## 性能对比\n\n![image](https://s2.ax1x.com/2020/02/27/3wuywR.png)\n\n### for-loop\n\n在测试结果中，`for range` 在性能上相较 `for` 差。这是为什么呢？在这里我们可以参见 `for range` 的 [实现](https://github.com/gcc-mirror/gcc/blob/master/gcc/go/gofrontend/statements.cc)，伪实现如下：\n\n```go\nfor_temp := range\nlen_temp := len(for_temp)\nfor index_temp = 0; index_temp < len_temp; index_temp++ {\n    value_temp = for_temp[index_temp]\n    index = index_temp\n    value = value_temp\n    original body\n}\n```\n\n通过分析伪实现，可得知 `for range` 相较 `for` 多做了如下事项\n\n#### Expression\n\n```\nRangeClause = [ ExpressionList \"=\" | IdentifierList \":=\" ] \"range\" Expression .\n```\n\n在循环开始之前会对范围表达式进行求值，多做了 “解” 表达式的动作，得到了最终的范围值\n\n#### Copy\n\n```go\n...\nvalue_temp = for_temp[index_temp]\nindex = index_temp\nvalue = value_temp\n...\n```\n\n从伪实现上可以得出，`for range` 始终使用**值拷贝**的方式来生成循环变量。通俗来讲，就是在每次循环时，都会对循环变量重新分配\n\n#### 小结\n\n通过上述的分析，可得知其比 `for` 慢的原因是 `for range` 有额外的性能开销，主要为**值拷贝的动作**导致的性能下降。这是它慢的原因\n\n那么其实在 `for range` 中，我们可以使用 `_` 和 `T[i]` 也能达到和 `for` 差不多的性能。但这可能不是 `for range` 的设计本意了\n\n### json.Marshal/Unmarshal\n\n#### encoding/json\n\njson 互转是在三种方案中最慢的，这是为什么呢？\n\n众所皆知，官方的 `encoding/json` 标准库，是通过大量反射来实现的。那么 “慢”，也是必然的。可参见下述代码：\n\n```go\n...\nfunc newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {\n    ...\n\tswitch t.Kind() {\n\tcase reflect.Bool:\n\t\treturn boolEncoder\n\tcase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:\n\t\treturn intEncoder\n\tcase reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:\n\t\treturn uintEncoder\n\tcase reflect.Float32:\n\t\treturn float32Encoder\n\tcase reflect.Float64:\n\t\treturn float64Encoder\n\tcase reflect.String:\n\t\treturn stringEncoder\n\tcase reflect.Interface:\n\t\treturn interfaceEncoder\n\tcase reflect.Struct:\n\t\treturn newStructEncoder(t)\n\tcase reflect.Map:\n\t\treturn newMapEncoder(t)\n\tcase reflect.Slice:\n\t\treturn newSliceEncoder(t)\n\tcase reflect.Array:\n\t\treturn newArrayEncoder(t)\n\tcase reflect.Ptr:\n\t\treturn newPtrEncoder(t)\n\tdefault:\n\t\treturn unsupportedTypeEncoder\n\t}\n}\n```\n\n既然官方的标准库存在一定的 “问题”，那么有没有其他解决方法呢？目前在社区里，大多为两类方案。如下：\n\n- 预编译生成代码（提前确定类型），可以解决运行时的反射带来的性能开销。缺点是增加了预生成的步骤\n- 优化序列化的逻辑，性能达到最大化\n\n接下来的实验，我们用第二种方案的库来测试，看看有没有改变。另外也推荐大家了解如下项目：\n\n- [json-iterator/go](https://github.com/json-iterator/go)\n- [mailru/easyjson](https://github.com/mailru/easyjson)\n- [pquerna/ffjson](https://github.com/pquerna/ffjson)\n\n#### json-iterator/go\n\n目前社区较常用的是 json-iterator/go，我们在测试代码中用到了它\n\n它的用法与标准库 100% 兼容，并且性能有较大提升。我们一起粗略的看下是怎么做到的，如下：\n\n##### reflect2\n\n利用 [modern-go/reflect2](https://github.com/modern-go/reflect2) 减少运行时调度开销\n\n```go\n...\ntype StructDescriptor struct {\n\tType   reflect2.Type\n\tFields []*Binding\n}\n\n...\ntype Binding struct {\n\tlevels    []int\n\tField     reflect2.StructField\n\tFromNames []string\n\tToNames   []string\n\tEncoder   ValEncoder\n\tDecoder   ValDecoder\n}\n\ntype Extension interface {\n\tUpdateStructDescriptor(structDescriptor *StructDescriptor)\n\tCreateMapKeyDecoder(typ reflect2.Type) ValDecoder\n\tCreateMapKeyEncoder(typ reflect2.Type) ValEncoder\n\tCreateDecoder(typ reflect2.Type) ValDecoder\n\tCreateEncoder(typ reflect2.Type) ValEncoder\n\tDecorateDecoder(typ reflect2.Type, decoder ValDecoder) ValDecoder\n\tDecorateEncoder(typ reflect2.Type, encoder ValEncoder) ValEncoder\n}\n```\n\n##### struct Encoder/Decoder Cache\n\n类型为 struct 时，只需要反射一次 Name 和 Type，会缓存 struct Encoder 和 Decoder\n\n```go\nvar typeDecoders = map[string]ValDecoder{}\nvar fieldDecoders = map[string]ValDecoder{}\nvar typeEncoders = map[string]ValEncoder{}\nvar fieldEncoders = map[string]ValEncoder{}\nvar extensions = []Extension{}\n\n....\n\nfieldNames := calcFieldNames(field.Name(), tagParts[0], tag)\nfieldCacheKey := fmt.Sprintf(\"%s/%s\", typ.String(), field.Name())\ndecoder := fieldDecoders[fieldCacheKey]\nif decoder == nil {\n\tdecoder = decoderOfType(ctx.append(field.Name()), field.Type())\n}\nencoder := fieldEncoders[fieldCacheKey]\nif encoder == nil {\n\tencoder = encoderOfType(ctx.append(field.Name()), field.Type())\n}\n```\n\n##### 文本解析优化\n\n#### 小结\n\n相较于官方标准库，第三方库 `json-iterator/go` 在运行时上做的更好。这是它快的原因\n\n有个需要注意的点，在 Go1.10 后 `map` 类型与标准库的已经没有太大的性能差异。但是，例如 `struct` 类型等仍然有较大的性能提高\n\n## 总结\n\n在本文中，我们首先进行了性能测试，再分析了不同方案，得知为什么了快慢的原因。那么最终在选择方案时，可以根据不同的应用场景去抉择：\n\n- 对性能开销有较高要求：选用 `for`，开销最小\n- 中规中矩：选用 `for range`，大对象慎用\n- 量小、占用小、数量可控：选用 `json.Marshal/Unmarshal` 的方案也可以。其**重复代码**少，但开销最大\n\n在绝大多数场景中，使用哪种并没有太大的影响。但作为工程师你应当清楚其利弊。以上就是不同的方案**分析概要**，希望对你有所帮助 :)\n"
  },
  {
    "path": "content/posts/go/talk/2019-03-31-go-ins.md",
    "content": "---\n\ntitle:      \"简单围观一下有趣的 //go: 指令\"\ndate:       2019-03-31 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n![image](http://wx2.sinaimg.cn/large/006fVPCvly1g1m1bplu3mj30xc0m8myg.jpg)\n\n## 前言\n\n如果你平时有翻看源码的习惯，你肯定会发现。咦，怎么有的方法上面总是写着 `//go:`  这类指令呢。他们到底是干嘛用的？\n\n今天我们一同揭开他们的面纱，我将简单给你介绍一下，它们都负责些什么\n\n## go:linkname\n\n```\n//go:linkname localname importpath.name\n```\n\n该指令指示编译器使用 `importpath.name` 作为源代码中声明为 `localname` 的变量或函数的目标文件符号名称。但是由于这个伪指令，可以破坏类型系统和包模块化。因此只有引用了 unsafe 包才可以使用\n\n简单来讲，就是 `importpath.name` 是 `localname` 的符号别名，编译器实际上会调用 `localname` 。但前提是使用了 `unsafe` 包才能使用\n\n### 案例\n\n#### time/time.go\n```\n...\nfunc now() (sec int64, nsec int32, mono int64)\n```\n\n#### runtime/timestub.go\n```\nimport _ \"unsafe\" // for go:linkname\n\n//go:linkname time_now time.now\nfunc time_now() (sec int64, nsec int32, mono int64) {\n\tsec, nsec = walltime()\n\treturn sec, nsec, nanotime() - startNano\n}\n```\n\n在这个案例中可以看到 `time.now`，它并没有具体的实现。如果你初看可能会懵逼。这时候建议你全局搜索一下源码，你就会发现其实现在 `runtime.time_now` 中\n\n配合先前的用法解释，可得知在 runtime 包中，我们声明了 `time_now` 方法是 `time.now` 的符号别名。并且在文件头引入了 `unsafe` 达成前提条件\n\n## go:noescape\n\n```\n//go:noescape\n```\n\n该指令指定下一个有声明但没有主体（意味着实现有可能不是 Go）的函数，不允许编译器对其做逃逸分析\n\n一般情况下，该指令用于内存分配优化。因为编译器默认会进行逃逸分析，会通过规则判定一个变量是分配到堆上还是栈上。但凡事有意外，一些函数虽然逃逸分析其是存放到堆上。但是对于我们来说，它是特别的。我们就可以使用 `go:noescape` 指令强制要求编译器将其分配到函数栈上\n\n### 案例\n\n```\n// memmove copies n bytes from \"from\" to \"to\".\n// in memmove_*.s\n//go:noescape\nfunc memmove(to, from unsafe.Pointer, n uintptr)\n```\n\n我们观察一下这个案例，它满足了该指令的常见特性。如下：\n\n- memmove_*.s：只有声明，没有主体。其主体是由底层汇编实现的\n- memmove：函数功能，在栈上处理性能会更好\n\n## go:nosplit\n\n```\n//go:nosplit\n```\n\n该指令指定文件中声明的下一个函数不得包含堆栈溢出检查。简单来讲，就是这个函数跳过堆栈溢出的检查\n\n\n### 案例\n\n```\n//go:nosplit\nfunc key32(p *uintptr) *uint32 {\n\treturn (*uint32)(unsafe.Pointer(p))\n}\n```\n\n## go:nowritebarrierrec\n\n```\n//go:nowritebarrierrec\n```\n\n该指令表示编译器遇到写屏障时就会产生一个错误，并且允许递归。也就是这个函数调用的其他函数如果有写屏障也会报错。简单来讲，就是针对写屏障的处理，防止其死循环\n\n### 案例\n\n```\n//go:nowritebarrierrec\nfunc gcFlushBgCredit(scanWork int64) {\n    ...\n}\n```\n\n## go:yeswritebarrierrec\n\n```\n//go:yeswritebarrierrec\n```\n\n该指令与 `go:nowritebarrierrec` 相对，在标注 `go:nowritebarrierrec` 指令的函数上，遇到写屏障会产生错误。而当编译器遇到 `go:yeswritebarrierrec` 指令时将会停止\n\n### 案例\n\n```\n//go:yeswritebarrierrec\nfunc gchelper() {\n\t...\n}\n```\n\n## go:noinline\n\n该指令表示该函数禁止进行内联\n\n### 案例\n\n```\n//go:noinline\nfunc unexportedPanicForTesting(b []byte, i int) byte {\n\treturn b[i]\n}\n```\n\n我们观察一下这个案例，是直接通过索引取值，逻辑比较简单。如果不加上 `go:noinline` 的话，就会出现编译器对其进行内联优化\n\n显然，内联有好有坏。该指令就是提供这一特殊处理\n\n## go:norace\n\n```\n//go:norace\n```\n\n该指令表示禁止进行竞态检测。而另外一种常见的形式就是在启动时执行 `go run -race`，能够检测应用程序中是否存在双向的数据竞争。非常有用\n\n### 案例\n\n```\n//go:norace\nfunc forkAndExecInChild(argv0 *byte, argv, envv []*byte, chroot, dir *byte, attr *ProcAttr, sys *SysProcAttr, pipe int) (pid int, err Errno) {\n    ...\n}\n```\n\n## go:notinheap\n\n```\n//go:notinheap\n```\n\n该指令常用于类型声明，它表示这个类型不允许从 GC 堆上进行申请内存。在运行时中常用其来做较低层次的内部结构，避免调度器和内存分配中的写屏障。能够提高性能\n\n### 案例\n\n```\n// notInHeap is off-heap memory allocated by a lower-level allocator\n// like sysAlloc or persistentAlloc.\n//\n// In general, it's better to use real types marked as go:notinheap,\n// but this serves as a generic type for situations where that isn't\n// possible (like in the allocators).\n//\n//go:notinheap\ntype notInHeap struct{}\n```\n\n## 总结\n\n在本文我们简单介绍了一些常见的指令集，我建议仅供了解。一般我们是用不到的，因为你的瓶颈可能更多的在自身应用上\n\n但是了解这一些，对你了解底层源码和运行机制会更有帮助。如果想再深入些，可阅读我给出的参考链接 ：）\n\n## 参考\n\n- [HACKING](https://github.com/golang/go/blob/master/src/runtime/HACKING.md)\n- [Command compile](https://golang.org/cmd/compile/)\n"
  },
  {
    "path": "content/posts/go/talk/2019-05-20-stack-heap.md",
    "content": "---\n\ntitle:      \"我要在栈上。不，你应该在堆上\"\ndate:       2019-05-20 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n![image](https://s2.ax1x.com/2020/02/27/3wK39K.jpg)\n\n我们在写代码的时候，有时候会想这个变量到底分配到哪里了？这时候可能会有人说，在栈上，在堆上。信我准没错...\n\n但从结果上来讲你还是一知半解，这可不行，万一被人懵了呢。今天我们一起来深挖下 Go 在这块的奥妙，自己动手丰衣足食\n\n## 问题\n\n```go\ntype User struct {\n\tID     int64\n\tName   string\n\tAvatar string\n}\n\nfunc GetUserInfo() *User {\n\treturn &User{ID: 13746731, Name: \"EDDYCJY\", Avatar: \"https://avatars0.githubusercontent.com/u/13746731\"}\n}\n\nfunc main() {\n\t_ = GetUserInfo()\n}\n```\n\n开局就是一把问号，带着问题进行学习。请问 main 调用 `GetUserInfo` 后返回的 `&User{...}`。这个变量是分配到栈上了呢，还是分配到堆上了？\n\n## 什么是堆/栈\n\n在这里并不打算详细介绍堆栈，仅简单介绍本文所需的基础知识。如下：\n\n- 堆（Heap）：一般来讲是人为手动进行管理，手动申请、分配、释放。一般所涉及的内存大小并不定，一般会存放较大的对象。另外其分配相对慢，涉及到的指令动作也相对多\n- 栈（Stack）：由编译器进行管理，自动申请、分配、释放。一般不会太大，我们常见的函数参数（不同平台允许存放的数量不同），局部变量等等都会存放在栈上\n\n今天我们介绍的 Go 语言，它的堆栈分配是通过 Compiler 进行分析，GC 去管理的，而对其的分析选择动作就是今天探讨的重点\n\n## 什么是逃逸分析\n\n在编译程序优化理论中，逃逸分析是一种确定指针动态范围的方法，简单来说就是分析在程序的哪些地方可以访问到该指针\n\n通俗地讲，逃逸分析就是确定一个变量要放堆上还是栈上，规则如下：\n\n1. 是否有在其他地方（非局部）被引用。只要**有可能**被引用了，那么它**一定**分配到堆上。否则分配到栈上\n2. 即使没有被外部引用，但对象过大，无法存放在栈区上。依然有可能分配到堆上\n\n对此你可以理解为，逃逸分析是编译器用于决定变量分配到堆上还是栈上的一种行为\n\n## 在什么阶段确立逃逸\n\n在编译阶段确立逃逸，注意并不是在运行时\n\n## 为什么需要逃逸\n\n这个问题我们可以反过来想，如果变量都分配到堆上了会出现什么事情？例如：\n\n- 垃圾回收（GC）的压力不断增大\n- 申请、分配、回收内存的系统开销增大（相对于栈）\n- 动态分配产生一定量的内存碎片\n\n其实总的来说，就是频繁申请、分配堆内存是有一定 “代价” 的。会影响应用程序运行的效率，间接影响到整体系统。因此 “按需分配” 最大限度的灵活利用资源，才是正确的治理之道。这就是为什么需要逃逸分析的原因，你觉得呢？\n\n## 怎么确定是否逃逸\n\n第一，通过编译器命令，就可以看到详细的逃逸分析过程。而指令集 `-gcflags` 用于将标识参数传递给 Go 编译器，涉及如下：\n\n- `-m` 会打印出逃逸分析的优化策略，实际上最多总共可以用 4 个 `-m`，但是信息量较大，一般用 1 个就可以了\n\n- `-l` 会禁用函数内联，在这里禁用掉 inline 能更好的观察逃逸情况，减少干扰\n\n```\n$ go build -gcflags '-m -l' main.go\n```\n\n第二，通过反编译命令查看\n\n```\n$ go tool compile -S main.go\n```\n\n注：可以通过 `go tool compile -help` 查看所有允许传递给编译器的标识参数\n\n## 逃逸案例\n\n### 案例一：指针\n\n第一个案例是一开始抛出的问题，现在你再看看，想想，如下：\n\n```go\ntype User struct {\n\tID     int64\n\tName   string\n\tAvatar string\n}\n\nfunc GetUserInfo() *User {\n\treturn &User{ID: 13746731, Name: \"EDDYCJY\", Avatar: \"https://avatars0.githubusercontent.com/u/13746731\"}\n}\n\nfunc main() {\n\t_ = GetUserInfo()\n}\n```\n\n执行命令观察一下，如下：\n\n```\n$ go build -gcflags '-m -l' main.go\n# command-line-arguments\n./main.go:10:54: &User literal escapes to heap\n```\n\n通过查看分析结果，可得知 `&User` 逃到了堆里，也就是分配到堆上了。这是不是有问题啊...再看看汇编代码确定一下，如下：\n\n```\n$ go tool compile -S main.go\n\"\".GetUserInfo STEXT size=190 args=0x8 locals=0x18\n\t0x0000 00000 (main.go:9)\tTEXT\t\"\".GetUserInfo(SB), $24-8\n\t...\n\t0x0028 00040 (main.go:10)\tMOVQ\tAX, (SP)\n\t0x002c 00044 (main.go:10)\tCALL\truntime.newobject(SB)\n\t0x0031 00049 (main.go:10)\tPCDATA\t$2, $1\n\t0x0031 00049 (main.go:10)\tMOVQ\t8(SP), AX\n\t0x0036 00054 (main.go:10)\tMOVQ\t$13746731, (AX)\n\t0x003d 00061 (main.go:10)\tMOVQ\t$7, 16(AX)\n\t0x0045 00069 (main.go:10)\tPCDATA\t$2, $-2\n\t0x0045 00069 (main.go:10)\tPCDATA\t$0, $-2\n\t0x0045 00069 (main.go:10)\tCMPL\truntime.writeBarrier(SB), $0\n\t0x004c 00076 (main.go:10)\tJNE\t156\n\t0x004e 00078 (main.go:10)\tLEAQ\tgo.string.\"EDDYCJY\"(SB), CX\n    ...\n```\n\n我们将目光集中到 CALL 指令，发现其执行了 `runtime.newobject` 方法，也就是确实是分配到了堆上。这是为什么呢？\n\n#### 分析结果\n\n这是因为 `GetUserInfo()` 返回的是指针对象，引用被返回到了方法之外了。因此编译器会把该对象分配到堆上，而不是栈上。否则方法结束之后，局部变量就被回收了，岂不是翻车。所以最终分配到堆上是理所当然的\n\n#### 再想想\n\n那你可能会想，那就是所有指针对象，都应该在堆上？并不。如下：\n\n```go\nfunc main() {\n\tstr := new(string)\n\t*str = \"EDDYCJY\"\n}\n```\n\n你想想这个对象会分配到哪里？如下：\n\n```\n$ go build -gcflags '-m -l' main.go\n# command-line-arguments\n./main.go:4:12: main new(string) does not escape\n```\n\n显然，该对象分配到栈上了。很核心的一点就是它有没有被作用域之外所引用，而这里作用域仍然保留在 `main` 中，因此它没有发生逃逸\n\n### 案例二：未确定类型\n\n```go\nfunc main() {\n\tstr := new(string)\n\t*str = \"EDDYCJY\"\n\n\tfmt.Println(str)\n}\n```\n\n执行命令观察一下，如下：\n\n```\n$ go build -gcflags '-m -l' main.go\n# command-line-arguments\n./main.go:9:13: str escapes to heap\n./main.go:6:12: new(string) escapes to heap\n./main.go:9:13: main ... argument does not escape\n```\n\n通过查看分析结果，可得知 `str` 变量逃到了堆上，也就是该对象在堆上分配。但上个案例时它还在栈上，我们也就 `fmt` 输出了它而已。这...到底发生了什么事？\n\n#### 分析结果\n\n相对案例一，案例二只加了一行代码 `fmt.Println(str)`，问题肯定出在它身上。其原型：\n\n```go\nfunc Println(a ...interface{}) (n int, err error)\n```\n\n通过对其分析，可得知当形参为 `interface` 类型时，在编译阶段编译器无法确定其具体的类型。因此会产生逃逸，最终分配到堆上\n\n如果你有兴趣追源码的话，可以看下内部的 `reflect.TypeOf(arg).Kind()` 语句，其会造成堆逃逸，而表象就是 `interface` 类型会导致该对象分配到堆上\n\n### 案例三、泄露参数\n\n```go\ntype User struct {\n\tID     int64\n\tName   string\n\tAvatar string\n}\n\nfunc GetUserInfo(u *User) *User {\n\treturn u\n}\n\nfunc main() {\n\t_ = GetUserInfo(&User{ID: 13746731, Name: \"EDDYCJY\", Avatar: \"https://avatars0.githubusercontent.com/u/13746731\"})\n}\n```\n\n执行命令观察一下，如下：\n\n```\n$ go build -gcflags '-m -l' main.go\n# command-line-arguments\n./main.go:9:18: leaking param: u to result ~r1 level=0\n./main.go:14:63: main &User literal does not escape\n```\n\n我们注意到 `leaking param` 的表述，它说明了变量 `u` 是一个泄露参数。结合代码可得知其传给 `GetUserInfo` 方法后，没有做任何引用之类的涉及变量的动作，直接就把这个变量返回出去了。因此这个变量实际上并没有逃逸，它的作用域还在 `main()` 之中，所以分配在栈上\n\n#### 再想想\n\n那你再想想怎么样才能让它分配到堆上？结合案例一，举一反三。修改如下：\n\n```go\ntype User struct {\n\tID     int64\n\tName   string\n\tAvatar string\n}\n\nfunc GetUserInfo(u User) *User {\n\treturn &u\n}\n\nfunc main() {\n\t_ = GetUserInfo(User{ID: 13746731, Name: \"EDDYCJY\", Avatar: \"https://avatars0.githubusercontent.com/u/13746731\"})\n}\n```\n\n执行命令观察一下，如下：\n\n```\n$ go build -gcflags '-m -l' main.go\n# command-line-arguments\n./main.go:10:9: &u escapes to heap\n./main.go:9:18: moved to heap: u\n```\n\n只要一小改，它就考虑会被外部所引用，因此妥妥的分配到堆上了\n\n## 总结\n\n在本文我给你介绍了逃逸分析的概念和规则，并列举了一些例子加深理解。但实际肯定远远不止这些案例，你需要做到的是掌握方法，遇到再看就好了。除此之外你还需要注意：\n\n- 静态分配到栈上，性能一定比动态分配到堆上好\n- 底层分配到堆，还是栈。实际上对你来说是透明的，不需要过度关心\n- 每个 Go 版本的逃逸分析都会有所不同（会改变，会优化）\n- 直接通过 `go build -gcflags '-m -l'` 就可以看到逃逸分析的过程和结果\n- 到处都用指针传递并不一定是最好的，要用对\n\n之前就有想过要不要写 “逃逸分析” 相关的文章，直到最近看到在夜读里有人问，还是有写的必要。对于这块的知识点。我的建议是适当了解，但没必要硬记。靠基础知识点加命令调试观察就好了。像是曹大之前讲的 “你琢磨半天逃逸分析，一压测，瓶颈在锁上”，完全没必要过度在意...\n\n## 参考\n\n- [Golang escape analysis](http://www.agardner.me/golang/garbage/collection/gc/escape/analysis/2015/10/18/go-escape-analysis.html)\n- [FAQ](https://golang.org/doc/faq#stack_or_heap)\n- [逃逸分析](https://zh.wikipedia.org/wiki/%E9%80%83%E9%80%B8%E5%88%86%E6%9E%90)\n"
  },
  {
    "path": "content/posts/go/talk/2019-06-16-defer-loss.md",
    "content": "---\n\ntitle:      \"Go1.12 defer 会有性能损耗，尽量不要用？\"\ndate:       2019-06-16 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n![image](https://s2.ax1x.com/2020/02/27/3wuUYV.jpg)\n\n上个月在 @polaris @轩脉刃 的全栈技术群里看到一个小伙伴问 **“说 defer 在栈退出时执行，会有性能损耗，尽量不要用，这个怎么解？”**。\n\n恰好前段时间写了一篇 [《深入理解 Go defer》](https://segmentfault.com/a/1190000019303572) 去详细剖析 `defer` 关键字。那么这一次简单结合前文对这个问题进行探讨一波，希望对你有所帮助，但在此之前希望你花几分钟，自己思考一下答案，再继续往下看。\n\n## 测试\n\n```go\nfunc DoDefer(key, value string) {\n    defer func(key, value string) {\n        _ = key + value\n    }(key, value)\n}\n\nfunc DoNotDefer(key, value string) {\n    _ = key + value\n}\n```\n\n基准测试：\n\n```go\nfunc BenchmarkDoDefer(b *testing.B) {\n    for i := 0; i < b.N; i++ {\n        DoDefer(\"煎鱼\", \"https://github.com/EDDYCJY/blog\")\n    }\n}\n\nfunc BenchmarkDoNotDefer(b *testing.B) {\n    for i := 0; i < b.N; i++ {\n        DoNotDefer(\"煎鱼\", \"https://github.com/EDDYCJY/blog\")\n    }\n}\n```\n\n输出结果：\n\n```\n$ go test -bench=. -benchmem -run=none\ngoos: darwin\ngoarch: amd64\npkg: github.com/EDDYCJY/awesomeDefer\nBenchmarkDoDefer-4          20000000            91.4 ns/op        48 B/op          1 allocs/op\nBenchmarkDoNotDefer-4       30000000            41.6 ns/op        48 B/op          1 allocs/op\nPASS\nok      github.com/EDDYCJY/awesomeDefer 3.234s\n```\n\n从结果上来，使用 `defer` 后的函数开销确实比没使用高了不少，这损耗用到哪里去了呢？\n\n## 想一下\n\n```\n$ go tool compile -S main.go\n\"\".main STEXT size=163 args=0x0 locals=0x40\n    ...\n    0x0059 00089 (main.go:6)    MOVQ    AX, 16(SP)\n    0x005e 00094 (main.go:6)    MOVQ    $1, 24(SP)\n    0x0067 00103 (main.go:6)    MOVQ    $1, 32(SP)\n    0x0070 00112 (main.go:6)    CALL    runtime.deferproc(SB)\n    0x0075 00117 (main.go:6)    TESTL    AX, AX\n    0x0077 00119 (main.go:6)    JNE    137\n    0x0079 00121 (main.go:7)    XCHGL    AX, AX\n    0x007a 00122 (main.go:7)    CALL    runtime.deferreturn(SB)\n    0x007f 00127 (main.go:7)    MOVQ    56(SP), BP\n    0x0084 00132 (main.go:7)    ADDQ    $64, SP\n    0x0088 00136 (main.go:7)    RET\n    0x0089 00137 (main.go:6)    XCHGL    AX, AX\n    0x008a 00138 (main.go:6)    CALL    runtime.deferreturn(SB)\n    0x008f 00143 (main.go:6)    MOVQ    56(SP), BP\n    0x0094 00148 (main.go:6)    ADDQ    $64, SP\n    0x0098 00152 (main.go:6)    RET\n    ...\n```\n\n我们在前文提到 `defer` 关键字其实涉及了一系列的连锁调用，内部 `runtime` 函数的调用就至少多了三步，分别是 `runtime.deferproc` 一次和 `runtime.deferreturn` 两次。\n\n而这还只是在运行时的显式动作，另外编译器做的事也不少，例如：\n\n- 在 `deferproc` 阶段（注册延迟调用），还得获取/传入目标函数地址、函数参数等等。\n- 在 `deferreturn` 阶段，需要在函数调用结尾处插入该方法的调用，同时若有被 `defer` 的函数，还需要使用 `runtime·jmpdefer` 进行跳转以便于后续调用。\n\n这一些动作途中还要涉及最小单元 `_defer` 的获取/生成， `defer` 和 `recover` 链表的逻辑处理和消耗等动作。\n\n## Q&A\n\n最后讨论的时候有提到 **“问题指的是本来就是用来执行 close() 一些操作的，然后说尽量不能用，例子就把 defer db.close() 前面的 defer 删去了”** 这个疑问。\n\n这是一个比较类似 “教科书” 式的说法，在一些入门教程中会潜移默化的告诉你在资源控制后加个 `defer` 延迟关闭一下。例如：\n\n```go\nresp, err := http.Get(...)\nif err != nil {\n    return err\n}\ndefer resp.Body.Close()\n```\n\n但是一定得这么写吗？其实并不，很多人给出的理由都是 “怕你忘记” 这种说辞，这没有毛病。但需要认清场景，假设我的应用场景如下：\n\n```go\nresp, err := http.Get(...)\nif err != nil {\n    return err\n}\ndefer resp.Body.Close()\n// do something\ntime.Sleep(time.Second * 60)\n```\n\n嗯，一个请求当然没问题，流量、并发一下子大了呢，那可能就是个灾难了。你想想为什么？从常见的 `defer` + `close` 的使用组合来讲，用之前建议先看清楚应用场景，在保证无异常的情况下确保尽早关闭才是首选。如果只是小范围调用很快就返回的话，偷个懒直接一套组合拳出去也未尝不可。\n\n## 结论\n\n一个 `defer` 关键字实际上包含了不少的动作和处理，和你单纯调用一个函数一条指令是没法比的。而与对照物相比，它确确实实是有性能损耗，目前延迟调用的全部开销大约在 50ns，但 `defer` 所提供的作用远远大于此，你从全局来看，它的损耗非常小，并且官方还不断地在优化中。\n\n因此，对于 “Go defer 会有性能损耗，尽量不能用？” 这个问题，我认为**该用就用，应该及时关闭就不要延迟，在 hot paths 用时一定要想清楚场景**。\n\n## 补充\n\n最后补充上柴大的回复：**“不是性能问题，defer 最大的功能是 Panic 后依然有效。如果没有 defer，Panic 后就会导致 unlock 丢失，从而导致死锁了”**，非常经典。\n"
  },
  {
    "path": "content/posts/go/talk/2019-06-29-talking-grpc.md",
    "content": "---\n\ntitle:      \"从实践到原理，带你参透 gRPC\"\ndate:       2019-06-29 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n![image](https://image.eddycjy.com/4a47a0db6e60853dedfcfdf08a5ca249.png)\n\ngRPC 在 Go 语言中大放异彩，越来越多的小伙伴在使用，最近也在公司安利了一波，希望这一篇文章能带你一览 gRPC 的巧妙之处，本文篇幅比较长，请做好阅读准备。本文目录如下：\n\n![image](https://image.eddycjy.com/156005c5baf40ff51a327f1c34f2975b.jpg)\n\n## 简述\n\ngRPC 是一个高性能、开源和通用的 RPC 框架，面向移动和 HTTP/2 设计。目前提供 C、Java 和 Go 语言版本，分别是：grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支持。\n\ngRPC 基于 HTTP/2 标准设计，带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特性。这些特性使得其在移动设备上表现更好，更省电和节省空间占用。\n\n## 调用模型\n\n![image](https://image.eddycjy.com/10fb15c77258a991b0028080a64fb42d.png)\n\n1、客户端（gRPC Stub）调用 A 方法，发起 RPC 调用。\n\n2、对请求信息使用 Protobuf 进行对象序列化压缩（IDL）。\n\n3、服务端（gRPC Server）接收到请求后，解码请求体，进行业务逻辑处理并返回。\n\n4、对响应结果使用 Protobuf 进行对象序列化压缩（IDL）。\n\n5、客户端接受到服务端响应，解码请求体。回调被调用的 A 方法，唤醒正在等待响应（阻塞）的客户端调用并返回响应结果。\n\n## 调用方式\n\n### 一、Unary RPC：一元 RPC\n\n![image](https://image.eddycjy.com/09dd8c2662b96ce14928333f055c5580.png)\n\n#### Server\n\n```go\ntype SearchService struct{}\n\nfunc (s *SearchService) Search(ctx context.Context, r *pb.SearchRequest) (*pb.SearchResponse, error) {\n    return &pb.SearchResponse{Response: r.GetRequest() + \" Server\"}, nil\n}\n\nconst PORT = \"9001\"\n\nfunc main() {\n    server := grpc.NewServer()\n    pb.RegisterSearchServiceServer(server, &SearchService{})\n\n    lis, err := net.Listen(\"tcp\", \":\"+PORT)\n    ...\n\n    server.Serve(lis)\n}\n```\n\n- 创建 gRPC Server 对象，你可以理解为它是 Server 端的抽象对象。\n- 将 SearchService（其包含需要被调用的服务端接口）注册到 gRPC Server。 的内部注册中心。这样可以在接受到请求时，通过内部的 “服务发现”，发现该服务端接口并转接进行逻辑处理。\n- 创建 Listen，监听 TCP 端口。\n- gRPC Server 开始 lis.Accept，直到 Stop 或 GracefulStop。\n\n#### Client\n\n```go\nfunc main() {\n    conn, err := grpc.Dial(\":\"+PORT, grpc.WithInsecure())\n    ...\n    defer conn.Close()\n\n    client := pb.NewSearchServiceClient(conn)\n    resp, err := client.Search(context.Background(), &pb.SearchRequest{\n        Request: \"gRPC\",\n    })\n    ...\n}\n```\n\n- 创建与给定目标（服务端）的连接句柄。\n- 创建 SearchService 的客户端对象。\n- 发送 RPC 请求，等待同步响应，得到回调后返回响应结果。\n\n### 二、Server-side streaming RPC：服务端流式 RPC\n\n![image](https://image.eddycjy.com/8266e4bfeda1bd42d8f9794eb4ea0a13.png)\n\n#### Server\n\n```go\nfunc (s *StreamService) List(r *pb.StreamRequest, stream pb.StreamService_ListServer) error {\n    for n := 0; n <= 6; n++ {\n        stream.Send(&pb.StreamResponse{\n            Pt: &pb.StreamPoint{\n                ...\n            },\n        })\n    }\n\n    return nil\n}\n```\n\n#### Client\n\n```go\nfunc printLists(client pb.StreamServiceClient, r *pb.StreamRequest) error {\n    stream, err := client.List(context.Background(), r)\n    ...\n\n    for {\n        resp, err := stream.Recv()\n        if err == io.EOF {\n            break\n        }\n        ...\n    }\n\n    return nil\n}\n```\n\n### 三、Client-side streaming RPC：客户端流式 RPC\n\n![image](https://image.eddycjy.com/f19c9085129709ee14d013be869df69b.png)\n\n#### Server\n\n```go\nfunc (s *StreamService) Record(stream pb.StreamService_RecordServer) error {\n    for {\n        r, err := stream.Recv()\n        if err == io.EOF {\n            return stream.SendAndClose(&pb.StreamResponse{Pt: &pb.StreamPoint{...}})\n        }\n        ...\n\n    }\n\n    return nil\n}\n```\n\n#### Client\n\n```go\nfunc printRecord(client pb.StreamServiceClient, r *pb.StreamRequest) error {\n    stream, err := client.Record(context.Background())\n    ...\n\n    for n := 0; n < 6; n++ {\n        stream.Send(r)\n    }\n\n    resp, err := stream.CloseAndRecv()\n    ...\n\n    return nil\n}\n```\n\n### 四、Bidirectional streaming RPC：双向流式 RPC\n\n![image](https://image.eddycjy.com/9eb9cd58b9ea5e04c890326b5c1f471f.png)\n\n#### Server\n\n```go\nfunc (s *StreamService) Route(stream pb.StreamService_RouteServer) error {\n    for {\n        stream.Send(&pb.StreamResponse{...})\n        r, err := stream.Recv()\n        if err == io.EOF {\n            return nil\n        }\n        ...\n    }\n\n    return nil\n}\n```\n\n#### Client\n\n```go\nfunc printRoute(client pb.StreamServiceClient, r *pb.StreamRequest) error {\n    stream, err := client.Route(context.Background())\n    ...\n\n    for n := 0; n <= 6; n++ {\n        stream.Send(r)\n        resp, err := stream.Recv()\n        if err == io.EOF {\n            break\n        }\n        ...\n    }\n\n    stream.CloseSend()\n\n    return nil\n}\n```\n\n## 客户端与服务端是如何交互的\n\n在开始分析之前，我们要先 gRPC 的调用有一个初始印象。那么最简单的就是对 Client 端调用 Server 端进行抓包去剖析，看看整个过程中它都做了些什么事。如下图：\n\n![image](https://image.eddycjy.com/8cda81fc7ad906927144235dda5fdf15.jpg)\n\n- Magic\n- SETTINGS\n- HEADERS\n- DATA\n- SETTINGS\n- WINDOW_UPDATE\n- PING\n- HEADERS\n- DATA\n- HEADERS\n- WINDOW_UPDATE\n- PING\n\n我们略加整理发现共有十二个行为，是比较重要的。在开始分析之前，建议你自己先想一下，它们的作用都是什么？大胆猜测一下，带着疑问去学习效果更佳。\n\n### 行为分析\n\n#### Magic\n\n![image](https://image.eddycjy.com/30e62fddc14c05988b44e7c02788e187.jpg)\n\nMagic 帧的主要作用是建立 HTTP/2 请求的前言。在 HTTP/2 中，要求两端都要发送一个连接前言，作为对所使用协议的最终确认，并确定 HTTP/2 连接的初始设置，客户端和服务端各自发送不同的连接前言。\n\n而上图中的 Magic 帧是客户端的前言之一，内容为 `PRI * HTTP/2.0\\r\\n\\r\\nSM\\r\\n\\r\\n`，以确定启用 HTTP/2 连接。\n\n#### SETTINGS\n\n![image](https://image.eddycjy.com/ae566253288191ce5d879e51dae1d8c3.jpg)\n\n![image](https://image.eddycjy.com/62bf1edb36141f114521ec4bb4175579.jpg)\n\nSETTINGS 帧的主要作用是设置这一个连接的参数，作用域是整个连接而并非单一的流。\n\n而上图的 SETTINGS 帧都是空 SETTINGS 帧，图一是客户端连接的前言（Magic 和 SETTINGS 帧分别组成连接前言）。图二是服务端的。另外我们从图中可以看到多个 SETTINGS 帧，这是为什么呢？是因为发送完连接前言后，客户端和服务端还需要有一步互动确认的动作。对应的就是带有 ACK 标识 SETTINGS 帧。\n\n#### HEADERS\n\n![image](https://image.eddycjy.com/8df7b73a7820f4aef47864f2a6c5fccf.jpg)\n\nHEADERS 帧的主要作用是存储和传播 HTTP 的标头信息。我们关注到 HEADERS 里有一些眼熟的信息，分别如下：\n\n- method：POST\n- scheme：http\n- path：/proto.SearchService/Search\n- authority：:10001\n- content-type：application/grpc\n- user-agent：grpc-go/1.20.0-dev\n\n你会发现这些东西非常眼熟，其实都是 gRPC 的基础属性，实际上远远不止这些，只是设置了多少展示多少。例如像平时常见的 `grpc-timeout`、`grpc-encoding` 也是在这里设置的。\n\n#### DATA\n\n![image](https://image.eddycjy.com/9414a8f5b810972c3c9a0e2860c07532.jpg)\n\nDATA 帧的主要作用是装填主体信息，是数据帧。而在上图中，可以很明显看到我们的请求参数 gRPC 存储在里面。只需要了解到这一点就可以了。\n\n#### HEADERS, DATA, HEADERS\n\n![image](https://image.eddycjy.com/edab7ba7e203cd7576d1200465194ea8.jpg)\n\n在上图中 HEADERS 帧比较简单，就是告诉我们 HTTP 响应状态和响应的内容格式。\n\n![imgae](https://image.eddycjy.com/db3a17f7bcac837ecc1fe2bc630a5473.jpg)\n\n在上图中 DATA 帧主要承载了响应结果的数据集，图中的 gRPC Server 就是我们 RPC 方法的响应结果。\n\n![image](https://image.eddycjy.com/85b6f89b41cae26786ac72365fff771b.jpg)\n\n在上图中 HEADERS 帧主要承载了 gRPC 状态 和 gRPC 状态消息，图中的 `grpc-status` 和 `grpc-message` 就是我们的 gRPC 调用状态的结果。\n\n### 其它步骤\n\n#### WINDOW_UPDATE\n\n主要作用是管理和流的窗口控制。通常情况下打开一个连接后，服务器和客户端会立即交换 SETTINGS 帧来确定流控制窗口的大小。默认情况下，该大小设置为约 65 KB，但可通过发出一个 WINDOW_UPDATE 帧为流控制设置不同的大小。\n\n![image](https://image.eddycjy.com/a269962fe1424e1ca3e68c328b9fed61.jpg)\n\n#### PING/PONG\n\n主要作用是判断当前连接是否仍然可用，也常用于计算往返时间。其实也就是 PING/PONG，大家对此应该很熟。\n\n### 小结\n\n![image](https://image.eddycjy.com/ba6beb7ae28ef0a97d7a0a038feb5060.png)\n\n- 在建立连接之前，客户端/服务端都会发送**连接前言**（Magic+SETTINGS），确立协议和配置项。\n- 在传输数据时，是会涉及滑动窗口（WINDOW_UPDATE）等流控策略的。\n- 传播 gRPC 附加信息时，是基于 HEADERS 帧进行传播和设置；而具体的请求/响应数据是存储的 DATA 帧中的。\n- 请求/响应结果会分为 HTTP 和 gRPC 状态响应两种类型。\n- 客户端发起 PING，服务端就会回应 PONG，反之亦可。\n\n这块 gRPC 的基础使用，你可以看看我另外的 [《gRPC 入门系列》](https://github.com/EDDYCJY/blog#grpc%E7%B3%BB%E5%88%97%E7%9B%AE%E5%BD%95)，相信对你一定有帮助。\n\n## 浅谈理解\n\n### 服务端\n\n![image](https://image.eddycjy.com/7134f8f5aced525d1c11d229063305e7.png)\n\n为什么四行代码，就能够起一个 gRPC Server，内部做了什么逻辑。你有想过吗？接下来我们一步步剖析，看看里面到底是何方神圣。\n\n### 一、初始化\n\n```go\n// grpc.NewServer()\nfunc NewServer(opt ...ServerOption) *Server {\n\topts := defaultServerOptions\n\tfor _, o := range opt {\n\t\to(&opts)\n\t}\n\ts := &Server{\n\t\tlis:    make(map[net.Listener]bool),\n\t\topts:   opts,\n\t\tconns:  make(map[io.Closer]bool),\n\t\tm:      make(map[string]*service),\n\t\tquit:   make(chan struct{}),\n\t\tdone:   make(chan struct{}),\n\t\tczData: new(channelzData),\n\t}\n\ts.cv = sync.NewCond(&s.mu)\n\t...\n\n\treturn s\n}\n```\n\n这块比较简单，主要是实例 grpc.Server 并进行初始化动作。涉及如下：\n\n- lis：监听地址列表。\n- opts：服务选项，这块包含 Credentials、Interceptor 以及一些基础配置。\n- conns：客户端连接句柄列表。\n- m：服务信息映射。\n- quit：退出信号。\n- done：完成信号。\n- czData：用于存储 ClientConn，addrConn 和 Server 的 channelz 相关数据。\n- cv：当优雅退出时，会等待这个信号量，直到所有 RPC 请求都处理并断开才会继续处理。\n\n### 二、注册\n\n```go\npb.RegisterSearchServiceServer(server, &SearchService{})\n```\n\n#### 步骤一：Service API interface\n\n```go\n// search.pb.go\ntype SearchServiceServer interface {\n\tSearch(context.Context, *SearchRequest) (*SearchResponse, error)\n}\n\nfunc RegisterSearchServiceServer(s *grpc.Server, srv SearchServiceServer) {\n\ts.RegisterService(&_SearchService_serviceDesc, srv)\n}\n```\n\n还记得我们平时编写的 Protobuf 吗？在生成出来的 `.pb.go` 文件中，会定义出 Service APIs interface 的具体实现约束。而我们在 gRPC Server 进行注册时，会传入应用 Service 的功能接口实现，此时生成的 `RegisterServer` 方法就会保证两者之间的一致性。\n\n#### 步骤二：Service API IDL\n\n你想乱传糊弄一下？不可能的，请乖乖定义与 Protobuf 一致的接口方法。但是那个 `&_SearchService_serviceDesc` 又有什么作用呢？代码如下：\n\n```go\n// search.pb.go\nvar _SearchService_serviceDesc = grpc.ServiceDesc{\n\tServiceName: \"proto.SearchService\",\n\tHandlerType: (*SearchServiceServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"Search\",\n\t\t\tHandler:    _SearchService_Search_Handler,\n\t\t},\n\t},\n\tStreams:  []grpc.StreamDesc{},\n\tMetadata: \"search.proto\",\n}\n```\n\n这看上去像服务的描述代码，用来向内部表述 “我” 都有什么。涉及如下:\n\n- ServiceName：服务名称\n- HandlerType：服务接口，用于检查用户提供的实现是否满足接口要求\n- Methods：一元方法集，注意结构内的 `Handler` 方法，其对应最终的 RPC 处理方法，在执行 RPC 方法的阶段会使用。\n- Streams：流式方法集\n- Metadata：元数据，是一个描述数据属性的东西。在这里主要是描述 `SearchServiceServer` 服务\n\n#### 步骤三：Register Service\n\n```go\nfunc (s *Server) register(sd *ServiceDesc, ss interface{}) {\n    ...\n\tsrv := &service{\n\t\tserver: ss,\n\t\tmd:     make(map[string]*MethodDesc),\n\t\tsd:     make(map[string]*StreamDesc),\n\t\tmdata:  sd.Metadata,\n\t}\n\tfor i := range sd.Methods {\n\t\td := &sd.Methods[i]\n\t\tsrv.md[d.MethodName] = d\n\t}\n\tfor i := range sd.Streams {\n\t\t...\n\t}\n\ts.m[sd.ServiceName] = srv\n}\n```\n\n在最后一步中，我们会将先前的服务接口信息、服务描述信息给注册到内部 `service` 去，以便于后续实际调用的使用。涉及如下：\n\n- server：服务的接口信息\n- md：一元服务的 RPC 方法集\n- sd：流式服务的 RPC 方法集\n- mdata：metadata，元数据\n\n#### 小结\n\n在这一章节中，主要介绍的是 gRPC Server 在启动前的整理和注册行为，看上去很简单，但其实一切都是为了后续的实际运行的预先准备。因此我们整理一下思路，将其串联起来看看，如下：\n\n![image](https://image.eddycjy.com/75c168b671d4ce827fca23907d85f114.png)\n\n### 三、监听\n\n接下来到了整个流程中，最重要也是大家最关注的监听/处理阶段，核心代码如下：\n\n```go\nfunc (s *Server) Serve(lis net.Listener) error {\n\t...\n\tvar tempDelay time.Duration\n\tfor {\n\t\trawConn, err := lis.Accept()\n\t\tif err != nil {\n\t\t\tif ne, ok := err.(interface {\n\t\t\t\tTemporary() bool\n\t\t\t}); ok && ne.Temporary() {\n\t\t\t\tif tempDelay == 0 {\n\t\t\t\t\ttempDelay = 5 * time.Millisecond\n\t\t\t\t} else {\n\t\t\t\t\ttempDelay *= 2\n\t\t\t\t}\n\t\t\t\tif max := 1 * time.Second; tempDelay > max {\n\t\t\t\t\ttempDelay = max\n\t\t\t\t}\n\t\t\t\t...\n\t\t\t\ttimer := time.NewTimer(tempDelay)\n\t\t\t\tselect {\n\t\t\t\tcase <-timer.C:\n\t\t\t\tcase <-s.quit:\n\t\t\t\t\ttimer.Stop()\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t...\n\t\t\treturn err\n\t\t}\n\t\ttempDelay = 0\n\n\t\ts.serveWG.Add(1)\n\t\tgo func() {\n\t\t\ts.handleRawConn(rawConn)\n\t\t\ts.serveWG.Done()\n\t\t}()\n\t}\n}\n```\n\nServe 会根据外部传入的 Listener 不同而调用不同的监听模式，这也是 `net.Listener` 的魅力，灵活性和扩展性会比较高。而在 gRPC Server 中最常用的就是 `TCPConn`，基于 TCP Listener 去做。接下来我们一起看看具体的处理逻辑，如下：\n\n![image](https://image.eddycjy.com/7ae5e99a8c2f19cd25f44313293553aa.png)\n\n- 循环处理连接，通过 `lis.Accept` 取出连接，如果队列中没有需处理的连接时，会形成阻塞等待。\n- 若 `lis.Accept` 失败，则触发休眠机制，若为第一次失败那么休眠 5ms，否则翻倍，再次失败则不断翻倍直至上限休眠时间 1s，而休眠完毕后就会尝试去取下一个 “它”。\n- 若 `lis.Accept` 成功，则重置休眠的时间计数和启动一个新的 goroutine 调用 `handleRawConn` 方法去执行/处理新的请求，也就是大家很喜欢说的 “每一个请求都是不同的 goroutine 在处理”。\n- 在循环过程中，包含了 “退出” 服务的场景，主要是硬关闭和优雅重启服务两种情况。\n\n## 客户端\n\n![image](https://image.eddycjy.com/2484a7df36877a14689574eebda6dd7c.png)\n\n### 一、创建拨号连接\n\n```go\n// grpc.Dial(\":\"+PORT, grpc.WithInsecure())\nfunc DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) {\n\tcc := &ClientConn{\n\t\ttarget:            target,\n\t\tcsMgr:             &connectivityStateManager{},\n\t\tconns:             make(map[*addrConn]struct{}),\n\t\tdopts:             defaultDialOptions(),\n\t\tblockingpicker:    newPickerWrapper(),\n\t\tczData:            new(channelzData),\n\t\tfirstResolveEvent: grpcsync.NewEvent(),\n\t}\n\t...\n\tchainUnaryClientInterceptors(cc)\n\tchainStreamClientInterceptors(cc)\n\n\t...\n}\n```\n\n`grpc.Dial` 方法实际上是对于 `grpc.DialContext` 的封装，区别在于 `ctx` 是直接传入 `context.Background`。其主要功能是**创建**与给定目标的客户端连接，其承担了以下职责：\n\n- 初始化 ClientConn\n- 初始化（基于进程 LB）负载均衡配置\n- 初始化 channelz\n- 初始化重试规则和客户端一元/流式拦截器\n- 初始化协议栈上的基础信息\n- 相关 context 的超时控制\n- 初始化并解析地址信息\n- 创建与服务端之间的连接\n\n#### 连没连\n\n之前听到有的人说调用 `grpc.Dial` 后客户端就已经与服务端建立起了连接，但这对不对呢？我们先鸟瞰全貌，看看正在跑的 goroutine。如下：\n\n![image](https://image.eddycjy.com/cf5793938b321b67b3b667655b375703.jpg)\n\n我们可以有几个核心方法一直在等待/处理信号，通过分析底层源码可得知。涉及如下：\n\n```go\nfunc (ac *addrConn) connect()\nfunc (ac *addrConn) resetTransport()\nfunc (ac *addrConn) createTransport(addr resolver.Address, copts transport.ConnectOptions, connectDeadline time.Time)\nfunc (ac *addrConn) getReadyTransport()\n```\n\n在这里主要分析 goroutine 提示的 `resetTransport` 方法，看看都做了啥。核心代码如下：\n\n```go\nfunc (ac *addrConn) resetTransport() {\n\tfor i := 0; ; i++ {\n\t\tif ac.state == connectivity.Shutdown {\n\t\t\treturn\n\t\t}\n\t\t...\n\t\tconnectDeadline := time.Now().Add(dialDuration)\n\t\tac.updateConnectivityState(connectivity.Connecting)\n\t\tnewTr, addr, reconnect, err := ac.tryAllAddrs(addrs, connectDeadline)\n\t\tif err != nil {\n\t\t\tif ac.state == connectivity.Shutdown {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tac.updateConnectivityState(connectivity.TransientFailure)\n\t\t\ttimer := time.NewTimer(backoffFor)\n\t\t\tselect {\n\t\t\tcase <-timer.C:\n\t\t\t\t...\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif ac.state == connectivity.Shutdown {\n\t\t\tnewTr.Close()\n\t\t\treturn\n\t\t}\n\t\t...\n\t\tif !healthcheckManagingState {\n\t\t\tac.updateConnectivityState(connectivity.Ready)\n\t\t}\n\t\t...\n\n\t\tif ac.state == connectivity.Shutdown {\n\t\t\treturn\n\t\t}\n\t\tac.updateConnectivityState(connectivity.TransientFailure)\n\t}\n}\n```\n\n在该方法中会不断地去尝试创建连接，若成功则结束。否则不断地根据 `Backoff` 算法的重试机制去尝试创建连接，直到成功为止。从结论上来讲，单纯调用 `DialContext` 是异步建立连接的，也就是并不是马上生效，处于 `Connecting` 状态，而正式下要到达 `Ready` 状态才可用。\n\n#### 真的连了吗\n\n![image](https://image.eddycjy.com/eb935669c45405844c35aafbd5fe43d7.jpg)\n\n在抓包工具上提示一个包都没有，那么这算真正连接了吗？我认为这是一个表述问题，我们应该尽可能的严谨。如果你真的想通过 `DialContext` 方法就打通与服务端的连接，则需要调用 `WithBlock` 方法，虽然会导致阻塞等待，但最终连接会到达 `Ready` 状态（握手成功）。如下图：\n\n![image](https://image.eddycjy.com/e0e28452229af52e70f87dd03c3a30c2.jpg)\n\n### 二、实例化 Service API\n\n```go\ntype SearchServiceClient interface {\n\tSearch(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error)\n}\n\ntype searchServiceClient struct {\n\tcc *grpc.ClientConn\n}\n\nfunc NewSearchServiceClient(cc *grpc.ClientConn) SearchServiceClient {\n\treturn &searchServiceClient{cc}\n}\n```\n\n这块就是实例 Service API interface，比较简单。\n\n### 三、调用\n\n```go\n// search.pb.go\nfunc (c *searchServiceClient) Search(ctx context.Context, in *SearchRequest, opts ...grpc.CallOption) (*SearchResponse, error) {\n\tout := new(SearchResponse)\n\terr := c.cc.Invoke(ctx, \"/proto.SearchService/Search\", in, out, opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n```\n\nproto 生成的 RPC 方法更像是一个包装盒，把需要的东西放进去，而实际上调用的还是 `grpc.invoke` 方法。如下：\n\n```go\nfunc invoke(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, opts ...CallOption) error {\n\tcs, err := newClientStream(ctx, unaryStreamDesc, cc, method, opts...)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := cs.SendMsg(req); err != nil {\n\t\treturn err\n\t}\n\treturn cs.RecvMsg(reply)\n}\n```\n\n通过概览，可以关注到三块调用。如下：\n\n- newClientStream：获取传输层 Trasport 并组合封装到 ClientStream 中返回，在这块会涉及负载均衡、超时控制、 Encoding、 Stream 的动作，与服务端基本一致的行为。\n- cs.SendMsg：发送 RPC 请求出去，但其并不承担等待响应的功能。\n- cs.RecvMsg：阻塞等待接受到的 RPC 方法响应结果。\n\n#### 连接\n\n```go\n// clientconn.go\nfunc (cc *ClientConn) getTransport(ctx context.Context, failfast bool, method string) (transport.ClientTransport, func(balancer.DoneInfo), error) {\n\tt, done, err := cc.blockingpicker.pick(ctx, failfast, balancer.PickOptions{\n\t\tFullMethodName: method,\n\t})\n\tif err != nil {\n\t\treturn nil, nil, toRPCErr(err)\n\t}\n\treturn t, done, nil\n}\n```\n\n在 `newClientStream` 方法中，我们通过 `getTransport` 方法获取了 Transport 层中抽象出来的 ClientTransport 和 ServerTransport，实际上就是获取一个连接给后续 RPC 调用传输使用。\n\n### 四、关闭连接\n\n```go\n// conn.Close()\nfunc (cc *ClientConn) Close() error {\n\tdefer cc.cancel()\n    ...\n\tcc.csMgr.updateState(connectivity.Shutdown)\n    ...\n\tcc.blockingpicker.close()\n\tif rWrapper != nil {\n\t\trWrapper.close()\n\t}\n\tif bWrapper != nil {\n\t\tbWrapper.close()\n\t}\n\n\tfor ac := range conns {\n\t\tac.tearDown(ErrClientConnClosing)\n\t}\n\tif channelz.IsOn() {\n\t\t...\n\t\tchannelz.AddTraceEvent(cc.channelzID, ted)\n\t\tchannelz.RemoveEntry(cc.channelzID)\n\t}\n\treturn nil\n}\n```\n\n该方法会取消 ClientConn 上下文，同时关闭所有底层传输。涉及如下：\n\n- Context Cancel\n- 清空并关闭客户端连接\n- 清空并关闭解析器连接\n- 清空并关闭负载均衡连接\n- 添加跟踪引用\n- 移除当前通道信息\n\n## Q&A\n\n### 1. gRPC Metadata 是通过什么传输？\n\n![image](https://image.eddycjy.com/129e458698c4745a32d44582161b51d8.jpg)\n\n### 2. 调用 grpc.Dial 会真正的去连接服务端吗？\n\n会，但是是异步连接的，连接状态为正在连接。但如果你设置了 `grpc.WithBlock` 选项，就会阻塞等待（等待握手成功）。另外你需要注意，当未设置 `grpc.WithBlock` 时，ctx 超时控制对其无任何效果。\n\n### 3. 调用 ClientConn 不 Close 会导致泄露吗？\n\n会，除非你的客户端不是常驻进程，那么在应用结束时会被动地回收资源。但如果是常驻进程，你又真的忘记执行 `Close` 语句，会造成的泄露。如下图：\n\n**3.1. 客户端**\n\n![image](https://image.eddycjy.com/e25418821200a0f7c8f9f81b22d21691.jpg)\n\n**3.2. 服务端**\n\n![image](https://image.eddycjy.com/19ee203f0229aae4b91567bff25442e5.png)\n\n**3.3. TCP**\n\n![image](https://image.eddycjy.com/f0d0b070be593820651230120b0374be.jpg)\n\n### 4. 不控制超时调用的话，会出现什么问题？\n\n短时间内不会出现问题，但是会不断积蓄泄露，积蓄到最后当然就是服务无法提供响应了。如下图：\n\n![image](https://image.eddycjy.com/853b031a43495200d111d6f5239398a3.jpg)\n\n### 5. 为什么默认的拦截器不可以传多个？\n\n```go\nfunc chainUnaryClientInterceptors(cc *ClientConn) {\n\tinterceptors := cc.dopts.chainUnaryInts\n\tif cc.dopts.unaryInt != nil {\n\t\tinterceptors = append([]UnaryClientInterceptor{cc.dopts.unaryInt}, interceptors...)\n\t}\n\tvar chainedInt UnaryClientInterceptor\n\tif len(interceptors) == 0 {\n\t\tchainedInt = nil\n\t} else if len(interceptors) == 1 {\n\t\tchainedInt = interceptors[0]\n\t} else {\n\t\tchainedInt = func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error {\n\t\t\treturn interceptors[0](ctx, method, req, reply, cc, getChainUnaryInvoker(interceptors, 0, invoker), opts...)\n\t\t}\n\t}\n\tcc.dopts.unaryInt = chainedInt\n}\n```\n\n当存在多个拦截器时，取的就是第一个拦截器。因此结论是允许传多个，但并没有用。\n\n### 6. 真的需要用到多个拦截器的话，怎么办？\n\n可以使用 [go-grpc-middleware](https://github.com/grpc-ecosystem/go-grpc-middleware) 提供的 `grpc.UnaryInterceptor` 和 `grpc.StreamInterceptor` 链式方法，方便快捷省心。\n\n单单会用还不行，我们再深剖一下，看看它是怎么实现的。核心代码如下：\n\n```go\nfunc ChainUnaryClient(interceptors ...grpc.UnaryClientInterceptor) grpc.UnaryClientInterceptor {\n\tn := len(interceptors)\n\tif n > 1 {\n\t\tlastI := n - 1\n\t\treturn func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {\n\t\t\tvar (\n\t\t\t\tchainHandler grpc.UnaryInvoker\n\t\t\t\tcurI         int\n\t\t\t)\n\n\t\t\tchainHandler = func(currentCtx context.Context, currentMethod string, currentReq, currentRepl interface{}, currentConn *grpc.ClientConn, currentOpts ...grpc.CallOption) error {\n\t\t\t\tif curI == lastI {\n\t\t\t\t\treturn invoker(currentCtx, currentMethod, currentReq, currentRepl, currentConn, currentOpts...)\n\t\t\t\t}\n\t\t\t\tcurI++\n\t\t\t\terr := interceptors[curI](currentCtx, currentMethod, currentReq, currentRepl, currentConn, chainHandler, currentOpts...)\n\t\t\t\tcurI--\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn interceptors[0](ctx, method, req, reply, cc, chainHandler, opts...)\n\t\t}\n\t}\n    ...\n}\n```\n\n当拦截器数量大于 1 时，从 `interceptors[1]` 开始递归，每一个递归的拦截器 `interceptors[i]` 会不断地执行，最后才真正的去执行 `handler` 方法。同时也经常有人会问拦截器的执行顺序是什么，通过这段代码你得出结论了吗？\n\n### 7. 频繁创建 ClientConn 有什么问题？\n\n这个问题我们可以反向验证一下，假设不公用 ClientConn 看看会怎么样？如下:\n\n```go\nfunc BenchmarkSearch(b *testing.B) {\n\tfor i := 0; i < b.N; i++ {\n\t\tconn, err := GetClientConn()\n\t\tif err != nil {\n\t\t\tb.Errorf(\"GetClientConn err: %v\", err)\n\t\t}\n\t\t_, err = Search(context.Background(), conn)\n\t\tif err != nil {\n\t\t\tb.Errorf(\"Search err: %v\", err)\n\t\t}\n\t}\n}\n```\n\n输出结果：\n\n```\n    ... connection error: desc = \"transport: Error while dialing dial tcp :10001: socket: too many open files\"\n    ... connection error: desc = \"transport: Error while dialing dial tcp :10001: socket: too many open files\"\n    ... connection error: desc = \"transport: Error while dialing dial tcp :10001: socket: too many open files\"\n    ... connection error: desc = \"transport: Error while dialing dial tcp :10001: socket: too many open files\"\nFAIL\nexit status 1\n```\n\n当你的应用场景是存在高频次同时生成/调用 ClientConn 时，可能会导致系统的文件句柄占用过多。这种情况下你可以变更应用程序生成/调用 ClientConn 的模式，又或是池化它，这块可以参考 [grpc-go-pool](github.com/processout/grpc-go-pool) 项目。\n\n### 8. 客户端请求失败后会默认重试吗？\n\n会不断地进行重试，直到上下文取消。而重试时间方面采用 backoff 算法作为的重连机制，默认的最大重试时间间隔是 120s。\n\n### 9. 为什么要用 HTTP/2 作为传输协议？\n\n许多客户端要通过 HTTP 代理来访问网络，gRPC 全部用 HTTP/2 实现，等到代理开始支持 HTTP/2 就能透明转发 gRPC 的数据。不光如此，负责负载均衡、访问控制等等的反向代理都能无缝兼容 gRPC，比起自己设计 wire protocol 的 Thrift，这样做科学不少。@ctiller @滕亦飞\n\n### 10. 在 Kubernetes 中 gRPC 负载均衡有问题？\n\ngRPC 的 RPC 协议是基于 HTTP/2 标准实现的，HTTP/2 的一大特性就是不需要像 HTTP/1.1 一样，每次发出请求都要重新建立一个新连接，而是会复用原有的连接。\n\n所以这将导致 kube-proxy 只有在连接建立时才会做负载均衡，而在这之后的每一次 RPC 请求都会利用原本的连接，那么实际上后续的每一次的 RPC 请求都跑到了同一个地方。\n\n注：使用 k8s service 做负载均衡的情况下\n\n## 总结\n\n- gRPC 基于 HTTP/2 + Protobuf。\n- gRPC 有四种调用方式，分别是一元、服务端/客户端流式、双向流式。\n- gRPC 的附加信息都会体现在 HEADERS 帧，数据在 DATA 帧上。\n- Client 请求若使用 grpc.Dial 默认是异步建立连接，当时状态为 Connecting。\n- Client 请求若需要同步则调用 WithBlock()，完成状态为 Ready。\n- Server 监听是循环等待连接，若没有则休眠，最大休眠时间 1s；若接收到新请求则起一个新的 goroutine 去处理。\n- grpc.ClientConn 不关闭连接，会导致 goroutine 和 Memory 等泄露。\n- 任何内/外调用如果不加超时控制，会出现泄漏和客户端不断重试。\n- 特定场景下，如果不对 grpc.ClientConn 加以调控，会影响调用。\n- 拦截器如果不用 go-grpc-middleware 链式处理，会覆盖。\n- 在选择 gRPC 的负载均衡模式时，需要谨慎。\n\n## 参考\n\n- http://doc.oschina.net/grpc\n- https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md\n- https://juejin.im/post/5b88a4f56fb9a01a0b31a67e\n- https://www.ibm.com/developerworks/cn/web/wa-http2-under-the-hood/index.html\n- https://github.com/grpc/grpc-go/issues/1953\n- https://www.zhihu.com/question/52670041\n"
  },
  {
    "path": "content/posts/go/talk/2019-09-07-go1.13-defer.md",
    "content": "---\n\ntitle:      \"Go1.13 defer 的性能是如何提高的\"\ndate:       2019-09-07 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n最近 Go1.13 终于发布了，其中一个值得关注的特性就是 **defer 在大部分的场景下性能提升了30%**，但是官方并没有具体写是怎么提升的，这让大家非常的疑惑。而我因为之前写过[《深入理解 Go defer》](https://book.eddycjy.com/golang/defer/defer.html) 和 [《Go defer 会有性能损耗，尽量不要用？》](https://book.eddycjy.com/golang/talk/defer-loss.html) 这类文章，因此我挺感兴趣它是做了什么改变才能得到这样子的结果，所以今天和大家一起探索其中奥妙。\n\n## 一、测试\n\n### Go1.12\n\n```\n$ go test -bench=. -benchmem -run=none\ngoos: darwin\ngoarch: amd64\npkg: github.com/EDDYCJY/awesomeDefer\nBenchmarkDoDefer-4      \t20000000\t        91.4 ns/op\t      48 B/op\t       1 allocs/op\nBenchmarkDoNotDefer-4   \t30000000\t        41.6 ns/op\t      48 B/op\t       1 allocs/op\nPASS\nok  \tgithub.com/EDDYCJY/awesomeDefer\t3.234s\n```\n\n### Go1.13\n\n```\n$ go test -bench=. -benchmem -run=none\ngoos: darwin\ngoarch: amd64\npkg: github.com/EDDYCJY/awesomeDefer\nBenchmarkDoDefer-4      \t15986062\t        74.7 ns/op\t      48 B/op\t       1 allocs/op\nBenchmarkDoNotDefer-4   \t29231842\t        40.3 ns/op\t      48 B/op\t       1 allocs/op\nPASS\nok  \tgithub.com/EDDYCJY/awesomeDefer\t3.444s\n```\n\n在开场，我先以不标准的测试基准验证了先前的测试用例，确确实实在这两个版本中，`defer` 的性能得到了提高，但是看上去似乎不是百分百提高 30 %。\n\n## 二、看一下\n\n### 之前（Go1.12）\n\n```\n    0x0070 00112 (main.go:6)    CALL    runtime.deferproc(SB)\n    0x0075 00117 (main.go:6)    TESTL    AX, AX\n    0x0077 00119 (main.go:6)    JNE    137\n    0x0079 00121 (main.go:7)    XCHGL    AX, AX\n    0x007a 00122 (main.go:7)    CALL    runtime.deferreturn(SB)\n    0x007f 00127 (main.go:7)    MOVQ    56(SP), BP\n```\n\n### 现在（Go1.13）\n\n```\n\t0x006e 00110 (main.go:4)\tMOVQ\tAX, (SP)\n\t0x0072 00114 (main.go:4)\tCALL\truntime.deferprocStack(SB)\n\t0x0077 00119 (main.go:4)\tTESTL\tAX, AX\n\t0x0079 00121 (main.go:4)\tJNE\t139\n\t0x007b 00123 (main.go:7)\tXCHGL\tAX, AX\n\t0x007c 00124 (main.go:7)\tCALL\truntime.deferreturn(SB)\n\t0x0081 00129 (main.go:7)\tMOVQ\t112(SP), BP\n```\n\n从汇编的角度来看，像是 `runtime.deferproc` 改成了 `runtime.deferprocStack` 调用，难道是做了什么优化，我们**抱着疑问**继续看下去。\n\n## 三、观察源码\n\n### _defer\n\n```\ntype _defer struct {\n\tsiz     int32\n\tsiz     int32 // includes both arguments and results\n\tstarted bool\n\theap    bool\n\tsp      uintptr // sp at time of defer\n\tpc      uintptr\n\tfn      *funcval\n\t...\n```\n\n相较于以前的版本，最小单元的 `_defer` 结构体主要是新增了 `heap` 字段，用于标识这个 `_defer` 是在堆上，还是在栈上进行分配，其余字段并没有明确变更，那我们可以把聚焦点放在 `defer` 的堆栈分配上了，看看是做了什么事。\n\n### deferprocStack\n\n```\nfunc deferprocStack(d *_defer) {\n\tgp := getg()\n\tif gp.m.curg != gp {\n\t\tthrow(\"defer on system stack\")\n\t}\n\t\n\td.started = false\n\td.heap = false\n\td.sp = getcallersp()\n\td.pc = getcallerpc()\n\n\t*(*uintptr)(unsafe.Pointer(&d._panic)) = 0\n\t*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))\n\t*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))\n\n\treturn0()\n}\n```\n\n这一块代码挺常规的，主要是获取调用 `defer` 函数的函数栈指针、传入函数的参数具体地址以及PC（程序计数器），这块在前文 [《深入理解 Go defer》](https://book.eddycjy.com/golang/defer/defer.html) 有详细介绍过，这里就不再赘述了。\n\n那这个 `deferprocStack` 特殊在哪呢，我们可以看到它把 `d.heap` 设置为了 `false`，也就是代表 `deferprocStack` 方法是针对将 `_defer` 分配在栈上的应用场景的。\n\n### deferproc\n\n那么问题来了，它又在哪里处理分配到堆上的应用场景呢？\n\n```\nfunc newdefer(siz int32) *_defer {\n\t...\n\td.heap = true\n\td.link = gp._defer\n\tgp._defer = d\n\treturn d\n}\n```\n\n那么 `newdefer` 是在哪里调用的呢，如下：\n\n```\nfunc deferproc(siz int32, fn *funcval) { // arguments of fn follow fn\n\t...\n\tsp := getcallersp()\n\targp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)\n\tcallerpc := getcallerpc()\n\n\td := newdefer(siz)\n\t...\n}\n```\n\n非常明确，先前的版本中调用的 `deferproc` 方法，现在被用于对应分配到堆上的场景了。\n\n### 小结\n\n- 第一点：可以确定的是 `deferproc` 并没有被去掉，而是流程被优化了。\n- 第二点：编译器会根据应用场景去选择使用 `deferproc` 还是 `deferprocStack` 方法，他们分别是针对分配在堆上和栈上的使用场景。\n\n## 四、编译器如何选择\n\n### esc\n\n```\n// src/cmd/compile/internal/gc/esc.go\ncase ODEFER:\n\tif e.loopdepth == 1 { // top level\n\t\tn.Esc = EscNever // force stack allocation of defer record (see ssa.go)\n\t\tbreak\n\t}\n```\n\n### ssa\n\n```\n// src/cmd/compile/internal/gc/ssa.go\ncase ODEFER:\n\td := callDefer\n\tif n.Esc == EscNever {\n\t\td = callDeferStack\n\t}\n    s.call(n.Left, d)\n```\n\n### 小结\n\n这块结合来看，核心就是当 `e.loopdepth == 1` 时，会将逃逸分析结果 `n.Esc` 设置为 `EscNever`，也就是将 `_defer` 分配到栈上，那这个 `e.loopdepth` 到底又是何方神圣呢，我们再详细看看代码，如下：\n\n```\n// src/cmd/compile/internal/gc/esc.go\ntype NodeEscState struct {\n\tCurfn             *Node\n\tFlowsrc           []EscStep \n\tRetval            Nodes    \n\tLoopdepth         int32  \n\tLevel             Level\n\tWalkgen           uint32\n\tMaxextraloopdepth int32\n}\n```\n\n这里重点查看 `Loopdepth` 字段，目前它共有三个值标识，分别是:\n\n- -1：全局。\n- 0：返回变量。\n- 1：顶级函数，又或是内部函数的不断增长值。\n\n这个读起来有点绕，结合我们上述 `e.loopdepth == 1` 的表述来看，也就是当 `defer func` 是顶级函数时，将会分配到栈上。但是若在  `defer func` 外层出现显式的迭代循环，又或是出现隐式迭代，将会分配到堆上。其实深层表示的还是迭代深度的意思，我们可以来证实一下刚刚说的方向，显式迭代的代码如下：\n\n```\nfunc main() {\n\tfor p := 0; p < 10; p++ {\n\t\tdefer func() {\n\t\t\tfor i := 0; i < 20; i++ {\n\t\t\t\tlog.Println(\"EDDYCJY\")\n\t\t\t}\n\t\t}()\n\t}\n}\n```\n\n查看汇编情况：\n\n```\n$ go tool compile -S main.go\n\"\".main STEXT size=122 args=0x0 locals=0x20\n\t0x0000 00000 (main.go:15)\tTEXT\t\"\".main(SB), ABIInternal, $32-0\n\t...\n\t0x0048 00072 (main.go:17)\tCALL\truntime.deferproc(SB)\n\t0x004d 00077 (main.go:17)\tTESTL\tAX, AX\n\t0x004f 00079 (main.go:17)\tJNE\t83\n\t0x0051 00081 (main.go:17)\tJMP\t33\n\t0x0053 00083 (main.go:17)\tXCHGL\tAX, AX\n\t0x0054 00084 (main.go:17)\tCALL\truntime.deferreturn(SB)\n\t...\n```\n\n显然，最终 `defer` 调用的是 `runtime.deferproc` 方法，也就是分配到堆上了，没毛病。而隐式迭代的话，你可以借助 `goto` 语句去实现这个功能，再自己验证一遍，这里就不再赘述了。\n\n## 总结\n\n从分析的结果上来看，官方说明的 Go1.13 defer 性能提高 30%，主要来源于其延迟对象的堆栈分配规则的改变，措施是由编译器通过对 `defer` 的 `for-loop` 迭代深度进行分析，如果 `loopdepth` 为 1，则设置逃逸分析的结果，将分配到栈上，否则分配到堆上。\n\n的确，我个人觉得对大部分的使用场景来讲，是优化了不少，也解决了一些人吐槽 `defer` 性能 “差” 的问题。另外，我想从 Go1.13 起，你也需要稍微了解一下它这块的机制，别随随便便就来个狂野版嵌套迭代 `defer`，可能没法效能最大化。\n\n如果你还想了解更多细节，可以看看 `defer` 这块的的[提交内容](https://github.com/golang/go/commit/fff4f599fe1c21e411a99de5c9b3777d06ce0ce6)，官方的测试用例也包含在里面。\n"
  },
  {
    "path": "content/posts/go/talk/2019-09-24-why-vsz-large.md",
    "content": "---\n\ntitle:      \"Go 应用内存占用太多，让排查？（VSZ篇）\"\ndate:       2019-09-24 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n前段时间，某同学说某服务的容器因为超出内存限制，不断地重启，问我们是不是有内存泄露，赶紧排查，然后解决掉，省的出问题。我们大为震惊，赶紧查看监控+报警系统和性能分析，发现应用指标压根就不高，不像有泄露的样子。\n\n那么问题是出在哪里了呢，我们进入某个容器里查看了 `top` 的系统指标，结果如下：\n\n```shell\nPID       VSZ    RSS   ... COMMAND\n67459     2007m  136m  ... ./eddycjy-server\n```\n\n从结果上来看，也没什么大开销的东西，主要就一个 Go 进程，一看，某同学就说 VSZ 那么高，而某云上的容器内存指标居然恰好和 VSZ 的值相接近，因此某同学就怀疑是不是 VSZ 所导致的，觉得存在一定的关联关系。\n\n而从最终的结论上来讲，上述的表述是不全对的，那么在今天，本篇文章将**主要围绕 Go 进程的 VSZ 来进行剖析**，看看到底它为什么那么 \"高\"，而在正式开始分析前，第一节为前置的补充知识，大家可按顺序阅读。\n\n## 基础知识\n\n### 什么是 VSZ \n\nVSZ 是该进程所能使用的虚拟内存总大小，它包括进程可以访问的所有内存，其中包括了被换出的内存（Swap）、已分配但未使用的内存以及来自共享库的内存。\n\n### 为什么要虚拟内存\n\n在前面我们有了解到 VSZ 其实就是该进程的虚拟内存总大小，那**如果我们想了解 VSZ 的话，那我们得先了解 “为什么要虚拟内存？”**。\n\n本质上来讲，在一个系统中的进程是与其他进程共享 CPU 和主存资源的，而在现代的操作系统中，多进程的使用非常的常见，那么如果太多的进程需要太多的内存，那么在没有虚拟内存的情况下，物理内存很可能会不够用，就会导致其中有些任务无法运行，更甚至会出现一些很奇怪的现象，例如 “某一个进程不小心写了另一个进程使用的内存”，就会造成内存破坏，因此虚拟内存是非常重要的一个媒介。\n\n### 虚拟内存包含了什么\n\n![image](https://image.eddycjy.com/3062dec8cd187490adadbdbcf50c17d4.jpg)\n\n而虚拟内存，又分为内核虚拟内存和进程虚拟内存，每一个进程的虚拟内存都是独立的， 呈现如上图所示。\n\n这里也补充说明一下，在内核虚拟内存中，是包含了内核中的代码和数据结构，而内核虚拟内存中的某些区域会被映射到所有进程共享的物理页面中去，因此你会看到 ”内核虚拟内存“ 中实际上是包含了 ”物理内存“ 的，它们两者存在映射关系。而在应用场景上来讲，每个进程也会去共享内核的代码和全局数据结构，因此就会被映射到所有进程的物理页面中去。\n\n![image](https://image.eddycjy.com/3ba1352075ace855104f4bd57752a2ad.jpg)\n\n### 虚拟内存的重要能力\n\n为了更有效地管理内存并且减少出错，现代系统提供了一种对主存的抽象概念，也就是今天的主角，叫做虚拟内存（VM），虚拟内存是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件交互的地方，它为每个进程提供了一个大的、一致的和私有的地址空间，虚拟内存提供了三个重要的能力：\n\n1. 它将主存看成是一个存储在磁盘上的地址空间的高速缓存，在主存中只保存活动区域，并根据需要在磁盘和主存之间来回传送数据，通过这种方式，它高效地使用了主存。\n2. 它为每个进程提供了一致的地址空间，从而简化了内存管理。\n3. 它保护了每个进程的地址空间不被其他进程破坏。\n\n### 小结\n\n上面发散的可能比较多，简单来讲，对于本文我们重点关注这些知识点，如下：\n\n- 虚拟内存它是有各式各样内存交互的地方，它包含的不仅仅是 \"自己\"，**而在本文中，我们只需要关注 VSZ，也就是进程虚拟内存，它包含了你的代码、数据、堆、栈段和共享库**。\n- 虚拟内存作为内存保护的工具，能够保证进程之间的内存空间独立，不受其他进程的影响，因此每一个进程的 VSZ 大小都不一样，互不影响。\n- 虚拟内存的存在，系统给各进程分配的内存之和是可以大于实际可用的物理内存的，因此你也会发现你进程的物理内存总是比虚拟内存低的多的多。\n\n\n\n## 排查问题\n\n在了解了基础知识后，我们正式开始排查问题，第一步我们先编写一个测试程序，看看没有什么业务逻辑的 Go 程序，它初始的 VSZ 是怎么样的。\n\n### 测试\n\n应用代码：\n\n```go\nfunc main() {\n\tr := gin.Default()\n\tr.GET(\"/ping\", func(c *gin.Context) {\n\t\tc.JSON(200, gin.H{\n\t\t\t\"message\": \"pong\",\n\t\t})\n\t})\n\tr.Run(\":8001\")\n}\n```\n\n查看进程情况：\n\n```shell\n$ ps aux 67459\nUSER      PID  %CPU %MEM      VSZ    RSS   ...\neddycjy 67459   0.0  0.0  4297048    960   ...\n```\n\n从结果上来看，VSZ 为 4297048K，也就是 4G 左右，咋一眼看过去还是挺吓人的，明明没有什么业务逻辑，但是为什么那么高呢，真是令人感到好奇。\n\n### 确认有没有泄露\n\n在未知的情况下，我们可以首先看下 `runtime.MemStats` 和 `pprof`，确定应用到底有没有泄露。不过我们这块是演示程序，什么业务逻辑都没有，因此可以确定和应用没有直接关系。\n\n```\n# runtime.MemStats\n# Alloc = 1298568\n# TotalAlloc = 1298568\n# Sys = 71893240\n# Lookups = 0\n# Mallocs = 10013\n# Frees = 834\n# HeapAlloc = 1298568\n# HeapSys = 66551808\n# HeapIdle = 64012288\n# HeapInuse = 2539520\n# HeapReleased = 64012288\n# HeapObjects = 9179\n...\n```\n\n### Go FAQ\n\n接着我第一反应是去翻了 Go FAQ（因为看到过，有印象），其问题为 \"Why does my Go process use so much virtual memory?\"，回答如下：\n\n> The Go memory allocator reserves a large region of virtual memory as an arena for allocations. This virtual memory is local to the specific Go process; the reservation does not deprive other processes of memory.\n>\n> To find the amount of actual memory allocated to a Go process, use the Unix top command and consult the RES (Linux) or RSIZE (macOS) columns.\n\n这个 FAQ 是在 2012 年 10 月 [提交](https://github.com/golang/go/commit/2100947d4a25dcf875be1941d0e3a409ea85051e) 的，这么多年了也没有更进一步的说明，再翻了 issues 和 forum，一些关闭掉的 issue 都指向了 FAQ，这显然无法满足我的求知欲，因此我继续往下探索，看看里面到底都摆了些什么。\n\n### 查看内存映射\n\n在上图中，我们有提到进程虚拟内存，主要包含了你的代码、数据、堆、栈段和共享库，那初步怀疑是不是进程做了什么内存映射，导致了大量的内存空间被保留呢，为了确定这一点，我们通过如下命令去排查：\n\n```powershell\n$ vmmap --wide 67459\n...\n==== Non-writable regions for process 67459\nREGION TYPE                      START - END             [ VSIZE  RSDNT  DIRTY   SWAP] PRT/MAX SHRMOD PURGE    REGION DETAIL\n__TEXT                 00000001065ff000-000000010667b000 [  496K   492K     0K     0K] r-x/rwx SM=COW          /bin/zsh\n__LINKEDIT             0000000106687000-0000000106699000 [   72K    44K     0K     0K] r--/rwx SM=COW          /bin/zsh\nMALLOC metadata        000000010669b000-000000010669c000 [    4K     4K     4K     0K] r--/rwx SM=COW          DefaultMallocZone_0x10669b000 zone structure\n...\n__TEXT                 00007fff76c31000-00007fff76c5f000 [  184K   168K     0K     0K] r-x/r-x SM=COW          /usr/lib/system/libxpc.dylib\n__LINKEDIT             00007fffe7232000-00007ffff32cb000 [192.6M  17.4M     0K     0K] r--/r-- SM=COW          dyld shared cache combined __LINKEDIT\n...        \n\n==== Writable regions for process 67459\nREGION TYPE                      START - END             [ VSIZE  RSDNT  DIRTY   SWAP] PRT/MAX SHRMOD PURGE    REGION DETAIL\n__DATA                 000000010667b000-0000000106682000 [   28K    28K    28K     0K] rw-/rwx SM=COW          /bin/zsh\n...   \n__DATA                 0000000106716000-000000010671e000 [   32K    28K    28K     4K] rw-/rwx SM=COW          /usr/lib/zsh/5.3/zsh/zle.so\n__DATA                 000000010671e000-000000010671f000 [    4K     4K     4K     0K] rw-/rwx SM=COW          /usr/lib/zsh/5.3/zsh/zle.so\n__DATA                 0000000106745000-0000000106747000 [    8K     8K     8K     0K] rw-/rwx SM=COW          /usr/lib/zsh/5.3/zsh/complete.so\n__DATA                 000000010675a000-000000010675b000 [    4K     4K     4K     0K] rw-\n...\n```\n\n这块主要是利用 macOS 的 `vmmap` 命令去查看内存映射情况，这样就可以知道这个进程的内存映射情况，从输出分析来看，**这些关联共享库占用的空间并不大，导致 VSZ 过高的根本原因不在共享库和二进制文件上，但是并没有发现大量保留内存空间的行为，这是一个问题点**。\n\n\n\n注：若是 Linux 系统，可使用 `cat /proc/PID/maps` 或 `cat /proc/PID/smaps` 查看。\n\n### 查看系统调用\n\n既然在内存映射中，我们没有明确的看到保留内存空间的行为，那我们接下来看看该进程的系统调用，确定一下它是否存在内存操作的行为，如下：\n\n```\n$ sudo dtruss -a ./awesomeProject\n...\n 4374/0x206a2:     15620       6      3 mprotect(0x1BC4000, 0x1000, 0x0)\t\t = 0 0\n...\n 4374/0x206a2:     15781       9      4 sysctl([CTL_HW, 3, 0, 0, 0, 0] (2), 0x7FFEEFBFFA64, 0x7FFEEFBFFA68, 0x0, 0x0)\t\t = 0 0\n 4374/0x206a2:     15783       3      1 sysctl([CTL_HW, 7, 0, 0, 0, 0] (2), 0x7FFEEFBFFA64, 0x7FFEEFBFFA68, 0x0, 0x0)\t\t = 0 0\n 4374/0x206a2:     15899       7      2 mmap(0x0, 0x40000, 0x3, 0x1002, 0xFFFFFFFFFFFFFFFF, 0x0)\t\t = 0x4000000 0\n 4374/0x206a2:     15930       3      1 mmap(0xC000000000, 0x4000000, 0x0, 0x1002, 0xFFFFFFFFFFFFFFFF, 0x0)\t\t = 0xC000000000 0\n 4374/0x206a2:     15934       4      2 mmap(0xC000000000, 0x4000000, 0x3, 0x1012, 0xFFFFFFFFFFFFFFFF, 0x0)\t\t = 0xC000000000 0\n 4374/0x206a2:     15936       2      0 mmap(0x0, 0x2000000, 0x3, 0x1002, 0xFFFFFFFFFFFFFFFF, 0x0)\t\t = 0x59B7000 0\n 4374/0x206a2:     15942       2      0 mmap(0x0, 0x210800, 0x3, 0x1002, 0xFFFFFFFFFFFFFFFF, 0x0)\t\t = 0x4040000 0\n 4374/0x206a2:     15947       2      0 mmap(0x0, 0x10000, 0x3, 0x1002, 0xFFFFFFFFFFFFFFFF, 0x0)\t\t = 0x1BD0000 0\n 4374/0x206a2:     15993       3      0 madvise(0xC000000000, 0x2000, 0x8)\t\t = 0 0\n 4374/0x206a2:     16004       2      0 mmap(0x0, 0x10000, 0x3, 0x1002, 0xFFFFFFFFFFFFFFFF, 0x0)\t\t = 0x1BE0000 0\n...\n```\n\n在这小节中，我们通过 macOS 的 `dtruss` 命令监听并查看了运行这个程序所进行的所有系统调用，发现了与内存管理有一定关系的方法如下：\n\n- mmap：创建一个新的虚拟内存区域，但这里需要注意，**就是当系统调用 mmap 时，它只是从虚拟内存中申请了一段空间出来，并不会去分配和映射真实的物理内存，而当你访问这段空间的时候，才会在当前时间真正的去分配物理内存**。那么对应到我们实际应用的进程中，那就是 VSZ 的增长后，而该内存空间又未正式使用的话，物理内存是不会有增长的。\n- madvise：提供有关使用内存的建议，例如：MADV_NORMAL、MADV_RANDOM、MADV_SEQUENTIAL、MADV_WILLNEED、MADV_DONTNEED 等等。\n- mprotect：设置内存区域的保护情况，例如：PROT_NONE、PROT_READ、PROT_WRITE、PROT_EXEC、PROT_SEM、PROT_SAO、PROT_GROWSUP、PROT_GROWSDOWN 等等。\n- sysctl：在内核运行时动态地修改内核的运行参数。\n\n在此比较可疑的是 `mmap` 方法，它在 `dtruss` 的最终统计中一共调用了 10 余次，我们可以相信它在 Go Runtime 的时候进行了大量的虚拟内存申请，我们再接着往下看，看看到底是在什么阶段进行了虚拟内存空间的申请。\n\n\n\n注：若是 Linux 系统，可使用 `strace` 命令。\n\n### 查看 Go Runtime\n\n#### 启动流程\n\n通过上述的分析，我们可以知道在 Go 程序启动的时候 VSZ 就已经不低了，并且确定不是共享库等的原因，且程序在启动时系统调用确实存在 `mmap` 等方法的调用，那么我们可以充分怀疑 Go 在初始化阶段就保留了该内存空间。那我们第一步要做的就是查看一下 Go 的引导启动流程，看看是在哪里申请的，引导过程如下：\n\n```\ngraph TD\nA(rt0_darwin_amd64.s:8<br/>_rt0_amd64_darwin) -->|JMP| B(asm_amd64.s:15<br/>_rt0_amd64)\nB --> |JMP|C(asm_amd64.s:87<br/>runtime-rt0_go)\nC --> D(runtime1.go:60<br/>runtime-args)\nD --> E(os_darwin.go:50<br/>runtime-osinit)\nE --> F(proc.go:472<br/>runtime-schedinit)\nF --> G(proc.go:3236<br/>runtime-newproc)\nG --> H(proc.go:1170<br/>runtime-mstart)\nH --> I(在新创建的 p 和 m 上运行 runtime-main)\n```\n\n- runtime-osinit：获取 CPU 核心数。\n- runtime-schedinit：初始化程序运行环境（包括栈、内存分配器、垃圾回收、P等）。\n- runtime-newproc：创建一个新的 G 和 绑定 runtime.main。\n- runtime-mstart：启动线程 M。\n\n注：来自@曹大的 《Go 程序的启动流程》和@全成的 《Go 程序是怎样跑起来的》，推荐大家阅读。\n\n#### 初始化运行环境\n\n显然，我们要研究的是 runtime 里的 `schedinit` 方法，如下：\n\n```\nfunc schedinit() {\n\t...\n\tstackinit()\n\tmallocinit()\n\tmcommoninit(_g_.m)\n\tcpuinit()       // must run before alginit\n\talginit()       // maps must not be used before this call\n\tmodulesinit()   // provides activeModules\n\ttypelinksinit() // uses maps, activeModules\n\titabsinit()     // uses activeModules\n\n\tmsigsave(_g_.m)\n\tinitSigmask = _g_.m.sigmask\n\n\tgoargs()\n\tgoenvs()\n\tparsedebugvars()\n\tgcinit()\n  ...\n}\n```\n\n从用途来看，非常明显， `mallocinit` 方法会进行内存分配器的初始化，我们继续往下看。\n\n#### 初始化内存分配器\n\n##### mallocinit\n\n接下来我们正式的分析一下 `mallocinit` 方法，在引导流程中， `mallocinit` 主要承担 Go 程序的内存分配器的初始化动作，而今天主要是针对虚拟内存地址这块进行拆解，如下：\n\n```\nfunc mallocinit() {\n\t...\n\tif sys.PtrSize == 8 {\n\t\tfor i := 0x7f; i >= 0; i-- {\n\t\t\tvar p uintptr\n\t\t\tswitch {\n\t\t\tcase GOARCH == \"arm64\" && GOOS == \"darwin\":\n\t\t\t\tp = uintptr(i)<<40 | uintptrMask&(0x0013<<28)\n\t\t\tcase GOARCH == \"arm64\":\n\t\t\t\tp = uintptr(i)<<40 | uintptrMask&(0x0040<<32)\n\t\t\tcase GOOS == \"aix\":\n\t\t\t\tif i == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tp = uintptr(i)<<40 | uintptrMask&(0xa0<<52)\n\t\t\tcase raceenabled:\n\t\t\t\t...\n\t\t\tdefault:\n\t\t\t\tp = uintptr(i)<<40 | uintptrMask&(0x00c0<<32)\n\t\t\t}\n\t\t\thint := (*arenaHint)(mheap_.arenaHintAlloc.alloc())\n\t\t\thint.addr = p\n\t\t\thint.next, mheap_.arenaHints = mheap_.arenaHints, hint\n\t\t}\n\t} else {\n      ...\n\t}\n}\n```\n\n- 判断当前是 64 位还是 32 位的系统。\n- 从 0x7fc000000000~0x1c000000000 开始设置保留地址。\n- 判断当前 `GOARCH`、`GOOS` 或是否开启了竞态检查，根据不同的情况申请不同大小的连续内存地址，而这里的 `p` 是即将要要申请的连续内存地址的开始地址。\n- 保存刚刚计算的 arena 的信息到 `arenaHint` 中。\n\n可能会有小伙伴问，为什么要判断是 32 位还是 64 位的系统，这是因为不同位数的虚拟内存的寻址范围是不同的，因此要进行区分，否则会出现高位的虚拟内存映射问题。而在申请保留空间时，我们会经常提到 `arenaHint` 结构体，它是 `arenaHints `链表里的一个节点，结构如下：\n\n```\ntype arenaHint struct {\n\taddr uintptr\n\tdown bool\n\tnext *arenaHint\n}\n```\n\n- addr：`arena` 的起始地址\n- down：是否最后一个 `arena`\n- next：下一个 `arenaHint` 的指针地址\n\n那么这里疯狂提到的 `arena` 又是什么东西呢，这其实是 Go 的内存管理中的概念，Go Runtime 会把申请的虚拟内存分为三个大块，如下：\n\n![image](https://image.eddycjy.com/c415cfea1db7a60b33d99084f9f32ad1.jpg)\n\n- spans：记录 arena 区域页号和 mspan 的映射关系。\n- bitmap：标识 arena 的使用情况，在功能上来讲，会用于标识 arena 的哪些空间地址已经保存了对象。\n- arean：arean 其实就是 Go 的堆区，是由 mheap 进行管理的，它的 MaxMem 是 512GB-1。而在功能上来讲，Go 会在初始化的时候申请一段连续的虚拟内存空间地址到 arean 保留下来，在真正需要申请堆上的空间时再从 arean 中取出来处理，这时候就会转变为物理内存了。\n\n在这里的话，你需要理解 arean 区域在 Go 内存里的作用就可以了。\n\n##### mmap \n\n我们刚刚通过上述的分析，已经知道 `mallocinit` 的用途了，但是你可能还是会有疑惑，就是我们之前所看到的 `mmap` 系统调用，和它又有什么关系呢，怎么就关联到一起了，接下来我们先一起来看看更下层的代码，如下：\n\n```\nfunc sysAlloc(n uintptr, sysStat *uint64) unsafe.Pointer {\n\tp, err := mmap(nil, n, _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_PRIVATE, -1, 0)\n\t...\n\tmSysStatInc(sysStat, n)\n\treturn p\n}\n\nfunc sysReserve(v unsafe.Pointer, n uintptr) unsafe.Pointer {\n\tp, err := mmap(v, n, _PROT_NONE, _MAP_ANON|_MAP_PRIVATE, -1, 0)\n\t...\n}\n\nfunc sysMap(v unsafe.Pointer, n uintptr, sysStat *uint64) {\n\t...\n\tmunmap(v, n)\n\tp, err := mmap(v, n, _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_FIXED|_MAP_PRIVATE, -1, 0)\n  ...\n}\n```\n\n在 Go Runtime 中存在着一系列的系统级内存调用方法，本文涉及的主要如下：\n\n- sysAlloc：从 OS 系统上申请清零后的内存空间，调用参数是 `_PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_PRIVATE`，得到的结果需进行内存对齐。\n- sysReserve：从 OS 系统中保留内存的地址空间，这时候还没有分配物理内存，调用参数是 `_PROT_NONE, _MAP_ANON|_MAP_PRIVATE`，得到的结果需进行内存对齐。\n- sysMap：通知 OS 系统我们要使用已经保留了的内存空间，调用参数是 `_PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_FIXED|_MAP_PRIVATE`。\n\n看上去好像很有道理的样子，但是 `mallocinit` 方法在初始化时，到底是在哪里涉及了 `mmap` 方法呢，表面看不出来，如下：\n\n```\nfor i := 0x7f; i >= 0; i-- {\n\t...\n\thint := (*arenaHint)(mheap_.arenaHintAlloc.alloc())\n\thint.addr = p\n\thint.next, mheap_.arenaHints = mheap_.arenaHints, hint\n}\n```\n\n实际上在调用 `mheap_.arenaHintAlloc.alloc()` 时，调用的是 `mheap`  下的 `sysAlloc` 方法，而 `sysAlloc` 又会与 `mmap` 方法产生调用关系，并且这个方法与常规的 `sysAlloc` 还不大一样，如下：\n\n```\nvar mheap_ mheap\n...\nfunc (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) {\n\t...\n\tfor h.arenaHints != nil {\n\t\thint := h.arenaHints\n\t\tp := hint.addr\n\t\tif hint.down {\n\t\t\tp -= n\n\t\t}\n\t\tif p+n < p {\n\t\t\tv = nil\n\t\t} else if arenaIndex(p+n-1) >= 1<<arenaBits {\n\t\t\tv = nil\n\t\t} else {\n\t\t\tv = sysReserve(unsafe.Pointer(p), n)\n\t\t}\n\t\t...\n}\n```\n\n你可以惊喜的发现 `mheap.sysAlloc` 里其实有调用 `sysReserve` 方法，而 `sysReserve` 方法又正正是从 OS 系统中保留内存的地址空间的特定方法，是不是很惊喜，一切似乎都串起来了。\n\n#### 小结\n\n在本节中，我们先写了一个测试程序，然后根据非常规的排查思路进行了一步步的跟踪怀疑，整体流程如下：\n\n- 通过 `top` 或 `ps` 等命令，查看进程运行情况，分析基础指标。\n- 通过 `pprof` 或 `runtime.MemStats ` 等工具链查看应用运行情况，分析应用层面是否有泄露或者哪儿高。\n- 通过 `vmmap` 命令，查看进程的内存映射情况，分析是不是进程虚拟空间内的某个区域比较高，例如：共享库等。\n- 通过 `dtruss` 命令，查看程序的系统调用情况，分析可能出现的一些特殊行为，例如：在分析中我们发现  `mmap` 方法调用的比例是比较高的，那我们有充分的理由怀疑 Go 在启动时就进行了大量的内存空间保留。\n- 通过上述的分析，确定可能是在哪个环节申请了那么多的内存空间后，再到 Go Runtime 中去做进一步的源码分析，因为源码面前，了无秘密，没必要靠猜。\n\n从结论上而言，VSZ（进程虚拟内存大小）与共享库等没有太大的关系，主要与 Go Runtime 存在直接关联，也就是在前图中表示的运行时堆（malloc）。转换到 Go Runtime 里，就是在 `mallocinit`  这个内存分配器的初始化阶段里进行了一定量的虚拟空间的保留。\n\n而保留虚拟内存空间时，受什么影响，又是一个哲学问题。从源码上来看，主要如下：\n\n- 受不同的 OS 系统架构（GOARCH/GOOS）和位数（32/64 位）的影响。\n- 受内存对齐的影响，计算回来的内存空间大小是需要经过对齐才会进行保留。\n\n## 总结\n\n我们通过一步步地分析，讲解了 Go 会在哪里，又会受什么因素，去调用了什么方法保留了那么多的虚拟内存空间，但是我们肯定会忧心进程虚拟内存（VSZ）高，会不会存在问题呢，我分析如下：\n\n- VSZ 并不意味着你真正使用了那些物理内存，因此是不需要担心的。\n- VSZ 并不会给 GC 带来压力，GC 管理的是进程实际使用的物理内存，而 VSZ 在你实际使用它之前，它并没有过多的代价。\n- VSZ 基本都是不可访问的内存映射，也就是它并没有内存的访问权限（不允许读、写和执行）。\n\n看到这里舒一口气，因为 Go VSZ 的高，并不会对我们产生什么非常实质性的问题，但是又仔细一想，为什么 Go 要申请那么多的虚拟内存呢，到底有啥用呢，考虑如下：Go 的设计是考虑到 `arena` 和  `bitmap` 的后续使用，先提早保留了整个内存地址空间。 然后随着 Go Runtime 和应用的逐步使用，肯定也会开始实际的申请和使用内存，这时候 `arena` 和 `bitmap` 的内存分配器就只需要将事先申请好的内存地址空间保留更改为实际可用的物理内存就好了，这样子可以极大的提高效能。\n\n## 参考\n\n- [曹大的 Go 程序的启动流程](http://xargin.com/go-bootstrap/)\n- [全成的 Go 程序是怎样跑起来的](https://www.cnblogs.com/qcrao-2018/p/11124360.html)\n- [推荐阅读 欧神的 go-under-the-hood](https://github.com/changkun/go-under-the-hood/blob/master/book/zh-cn/part2runtime/ch07alloc/readme.md)\n- [High virtual memory allocation by golang](https://forum.golangbridge.org/t/high-virtual-memory-allocation-by-golang/6716)\n- [GO MEMORY MANAGEMENT](https://povilasv.me/go-memory-management/)\n- [GoBigVirtualSize](https://utcc.utoronto.ca/~cks/space/blog/programming/GoBigVirtualSize)\n- [GoProgramMemoryUse](https://utcc.utoronto.ca/~cks/space/blog/programming/GoProgramMemoryUse)\n\n"
  },
  {
    "path": "content/posts/go/ternary-operator.md",
    "content": "---\ntitle: \"Go 凭什么不支持三元运算符？\"\ndate: 2021-12-31T12:54:50+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - 为什么\n---\n\n大家好，我是煎鱼。\n\n这是一个很多其他语言工程师转 Go 语言的时间节点，这就难免不论一番比较。其中一个经典的运算上的就是 “三元运算符”：\n\n![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/05766eebaf454d7fb91b60ab1c851cf4~tplv-k3u1fbpfcp-zoom-1.image)\n\n为什么 Go 语言不支持三元运算符，Go 不支持三元运算符就是设计的不好，是历史在开倒车吗？\n\n今天就由煎鱼来和大家一起摸索为什么。\n\n三元运算符是什么\n--------\n\n三元运算符，在典型的数学意义上，或者从解析器的角度来看，是一个需要三个参数的运算符。而我们日常中，最常见的是二元运算符：\n\n```\nx + y\nx / y\nx * y\n```\n\n还有一元运算符：\n\n```\n-a\n~b\n!c\n\n```\n\n以及今天的男主角 “三元运算符”。在 C/C++ 等多种语言中，我们可以根据条件声明和初始化变量的习惯来选择性使用三元条件运算符：\n\n```\nint index = val > 0 ? val : -val\n```\n\nGo 使用三元运算符\n----------\n\n想在 Go 语言里也使用三元运算符时，发现居然没有...想要实现与上面相同的代码段的方式似乎只能：\n\n```\nvar index int\n\nif val > 0 {\n    index = val\n} else {\n    index = -val\n}\n```\n\n看上去十分的冗余，不够简洁。\n\n为什么 Go 没有三元运算符\n--------------\n\n为什么 Go 没有 `?:` 操作符，没有的话，官方推荐的方式是怎么样的。\n\n通过 Go FAQ 我们可以得知：\n\n![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9ca7870161614747ae7decb88b4ce427~tplv-k3u1fbpfcp-zoom-1.image)\n\nGo 官方就是推荐我们使用前面提到的方式来替代，并且明确了如下态度：\n\n*   Go 中没有 `?:` 的原因是语言的设计者看到这个操作经常被用来创建难以理解的复杂表达式。\n    \n*   在替代方案上，if-else 形式虽然较长，但无疑是更清晰的。一门语言只需要一个条件控制流结构。\n    \n\n整体来讲，Go 语言的设计者是为了考虑**可读性**拒绝了实现三元运算符，\"less is more.\" 也是标榜台词了。\n\n社区争议\n----\n\nGo 语言的一些点与众不同，基本是大家皆知的。无论是 if err != nil，又或是本次的三元运算符，要大家用 if-else 替代：\n\n```\nif expr {\n    n = trueVal\n} else {\n    n = falseVal\n}\n```\n\n### 反对和同意\n\n#### 反对\n\n因此有社区小伙伴给出了反对，基本分为如下几类：\n\n1.  认为 if-else 也有以类似情况能被滥用，设计者的理由不够充分，认为是 “借口”。\n    \n2.  认为三元运算符的 “丑陋” 问题，是开发者的编码问题，而不是语言问题。三元在各种语言中很常见，它们是正常的，Go 语言也应该要有。\n    \n3.  认为用 if-else 替代三元运算符也很麻烦，让开发者多读了 3-4 行和额外的缩进级别。\n    \n\n#### 同意\n\n认可这个决策的也有不少，为此给出了大量的真实工程案例。\n\n一般来讲，我们用三元运算符是希望这么用：\n\n```\ncond ? true_value : false_value\n```\n\n你可能见过这么用：\n\n```\ncond ? value_a + value_b : value_c * value_d\n```\n\n还见过这样：\n\n```\n(((cond_a ? val_one) : cond_b) ? val_two) : val_three\n\ncond_a ? (val_one : (cond_b ? (val_two : val_three)))\n```\n\n还能嵌套三元运算符：\n\n```\nint a = cond_a ? val_one :\n    cond_b ? val_two :\n    cond_c ? val_three : val_four;\n```\n\n也能出现可读性更差的：\n\n```\nvoid rgb_to_lightness_(\n  const double re, const double gr, const double bl, double &li)\n{\n  li=((re < gr) ? ((gr < bl) ? bl : gr) : ((re < bl) ? bl : re) +\n                            (gr < re)\n                          ? ((bl < gr) ? bl : gr)\n                          : ((bl < re) ? bl : re)) / 2.0;\n}\n```\n\n说白了就是真实的代码工程中，大家见到过大量三元运算符滥用的场景，纷纷给出了大量的难理解的例子，让大家困扰不堪。\n\n总结\n--\n\n在这篇文章中，首先针对 “三元运算符” 做了基本的介绍。紧接着根据 Go 语言不支持三元的态度进行了说明，且面向社区的争议我们分为了正反方面的基本诠释。\n\n实际上一个简单的 `?:` 既整洁又实用，但是没有很好又高效的办法方法可以防止丑陋的嵌套，也就是排除可读性的问题。\n\n在真实的业务工程中，常常能看到一个三元运算符，**一开始只是很简单。后面嵌套越加越深，逻辑越写越复杂，从而带来了许多维护上的问题**。\n\n给大家抛出如下问题：\n\n*   你认为 Go 语言是否要有三元运算符呢？\n    \n*   如果要有，复杂嵌套的三元运算符又如何考虑呢？\n    \n\n**欢迎大家在评论区留言和交流 ：）**\n\n## 鼓励\n\n\n若有任何疑问欢迎评论区反馈和交流，**最好的关系是互相成就**，各位的**点赞**就是[煎鱼](https://github.com/eddycjy)创作的最大动力，感谢支持。\n\n> 文章持续更新，可以微信搜【脑子进煎鱼了】阅读，本文 **GitHub** [github.com/eddycjy/blog](https://github.com/eddycjy/blog) 已收录，学习 Go 语言可以看 [Go 学习地图和路线](https://github.com/eddycjy/go-developer-roadmap)，欢迎 Star 催更。"
  },
  {
    "path": "content/posts/go/throw.md",
    "content": "---\ntitle: \"Go 有哪些无法恢复的致命场景？\"\ndate: 2021-12-31T12:55:26+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n有一次事故现场，在紧急恢复后，他正在排查代码，查了好一会。我回头一看，这错误提醒很明显就是致命错误，较好定位。\n\n但此时，他竟然在查 panic-recover 是不是哪里漏了，我表示大受震惊...\n\n今天就由煎鱼给大家分享一下错误类型有哪几种，又在什么场景下会触发。\n\n## 错误类型\n\n### error\n\n第一种是 Go 中最标准的 error 错误，其真身是一个 interface{}。\n\n如下：\n\n```go\ntype error interface {\n    Error() string\n}\n```\n\n在日常工程中，我们只需要创建任意结构体，实现了 Error 方法，就可以认为是 error 错误类型。\n\n如下：\n\n```go\ntype errorString struct {\n    s string\n}\n\nfunc (e *errorString) Error() string {\n    return e.s\n}\n```\n\n在外部调用标准库 API，一般如下：\n\n```go\nf, err := os.Open(\"filename.ext\")\nif err != nil {\n    log.Fatal(err)\n}\n// do something with the open *File f\n```\n\n我们会约定最后一个参数为 error 类型，一般常见于第二个参数，可以有个约定俗成的习惯。\n\n### panic\n\n第二种是 Go 中的异常处理 panic，能够产生异常错误，结合 panic+recover 可以扭转程序的运行状态。\n\n如下：\n\n```go\npackage main\n\nimport \"os\"\n\nfunc main() {\n    panic(\"a problem\")\n\n    _, err := os.Create(\"/tmp/file\")\n    if err != nil {\n        panic(err)\n    }\n}\n```\n\n输出结果：\n\n```go\n$ go run panic.go\npanic: a problem\ngoroutine 1 [running]:\nmain.main()\n    /.../panic.go:12 +0x47\n...\nexit status 2\n```\n\n如果没有使用 recover 作为捕获，就会导致程序中断。也因此经常被人误以为程序中断，就 100% 是 panic 导致的。\n\n这是一个误区。\n\n### throw\n\n第三种是 Go 初学者经常踩坑，也不知道的错误类型，那就是致命错误 throw。\n\n这个错误类型，在用户侧是没法主动调用的，均为 Go 底层自行调用的，像是大家常见的 map 并发读写，就是由此触发。\n\n其源码如下：\n\n```go\nfunc throw(s string) {\n\tsystemstack(func() {\n\t\tprint(\"fatal error: \", s, \"\\n\")\n\t})\n\tgp := getg()\n\tif gp.m.throwing == 0 {\n\t\tgp.m.throwing = 1\n\t}\n\tfatalthrow()\n\t*(*int)(nil) = 0 // not reached\n}\n```\n\n根据上述程序，会获取当前 G 的实例，并设置其 M 的 throwing 状态为 1。\n\n状态设置好后，会调用 `fatalthrow` 方法进行真正的 crash 相关操作：\n\n```go\nfunc fatalthrow() {\n\tpc := getcallerpc()\n\tsp := getcallersp()\n\tgp := getg()\n\t\n\tsystemstack(func() {\n\t\tstartpanic_m()\n\t\tif dopanic_m(gp, pc, sp) {\n\t\t\tcrash()\n\t\t}\n\n\t\texit(2)\n\t})\n\n\t*(*int)(nil) = 0 // not reached\n}\n```\n\n主体逻辑是发送 `_SIGABRT` 信号量，最后调用 `exit` 方法退出，所以你会发现这是拦也拦不住的 “致命” 错误。\n\n## 致命场景\n\n为此，作为一名 “成熟” 的 Go 工程师，除了保障自己程序的健壮性外，我也在网上收集了一些致命的错误场景，分享给大家。\n\n一起学习和规避这些致命场景，年底争取拿个 A，不要背上 P0 事故。\n\n### 并发读写 map\n\n```go\nfunc foo() {\n\tm := map[string]int{}\n\tgo func() {\n\t\tfor {\n\t\t\tm[\"煎鱼1\"] = 1\n\t\t}\n\t}()\n\tfor {\n\t\t_ = m[\"煎鱼2\"]\n\t}\n}\n```\n\n输出结果：\n\n```\nfatal error: concurrent map read and map write\n\ngoroutine 1 [running]:\nruntime.throw(0x1078103, 0x21)\n...\n```\n\n### 堆栈内存耗尽\n\n```go\nfunc foo() {\n\tvar f func(a [1000]int64)\n\tf = func(a [1000]int64) {\n\t\tf(a)\n\t}\n\tf([1000]int64{})\n}\n```\n\n输出结果：\n\n```\nruntime: goroutine stack exceeds 1000000000-byte limit\nruntime: sp=0xc0200e1bf0 stack=[0xc0200e0000, 0xc0400e0000]\nfatal error: stack overflow\n\nruntime stack:\nruntime.throw(0x1074ba3, 0xe)\n        /usr/local/Cellar/go/1.16.6/libexec/src/runtime/panic.go:1117 +0x72\nruntime.newstack()\n...\n```\n\n### 将 nil 函数作为 goroutine 启动\n\n```go\nfunc foo() {\n\tvar f func()\n\tgo f()\n}\n```\n\n输出结果： \n\n```\nfatal error: go of nil func value\n\ngoroutine 1 [running]:\nmain.foo()\n...\n```\n\n### goroutines 死锁\n\n```go\nfunc foo() {\n\tselect {}\n}\n```\n\n输出结果：\n\n```\nfatal error: all goroutines are asleep - deadlock!\n\ngoroutine 1 [select (no cases)]:\nmain.foo()\n...\n```\n\n### 线程限制耗尽\n\n如果你的 goroutines 被 IO 操作阻塞了，新的线程可能会被启动来执行你的其他 goroutines。\n\nGo 的最大的线程数是有默认限制的，如果达到了这个限制，你的应用程序就会崩溃。\n\n会出现如下输出结果：\n\n```\nfatal error: thread exhaustion\n...\n```\n\n可以通过调用 `runtime.SetMaxThreads` 方法增大线程数，不过也需要考量是否程序有问题。\n\n### 超出可用内存\n\n如果你执行的操作，例如：下载大文件等。导致应用程序占用内存过大，程序上涨，导致 OOM。\n\n会出现如下输出结果：\n\n```\nfatal error: runtime: out of memory\n...\n```\n\n建议处理掉一些程序，或者换新电脑了。\n\n## 总结\n\n在今天这篇文章中，我们介绍了 Go 语言的三种错误类型。其中针对大家最少见，但一碰到就很容易翻车的致命错误 fatal error 进行了介绍，给出了一些经典案例。\n\n希望大家后续能够规避，**你有没有遇到过其中的场景**？\n\n欢迎在评论区交流和留言：）\n\n## 参考\n\n- Are all runtime errors recoverable in Go?"
  },
  {
    "path": "content/posts/go/tools/2018-09-15-go-tool-pprof.md",
    "content": "---\n\ntitle:      \"Go 大杀器之性能剖析 PProf\"\ndate:       2018-09-15 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n## 前言\n\n写了几吨代码，实现了几百个接口。功能测试也通过了，终于成功的部署上线了\n\n结果，性能不佳，什么鬼？😭\n\n## 想做性能分析\n\n### PProf\n\n想要进行性能优化，首先瞩目在 Go 自身提供的工具链来作为分析依据，本文将带你学习、使用 Go 后花园，涉及如下：\n\n- runtime/pprof：采集程序（非 Server）的运行数据进行分析\n- net/http/pprof：采集 HTTP Server 的运行时数据进行分析\n\n### 是什么\n\npprof 是用于可视化和分析性能分析数据的工具\n\npprof 以 [profile.proto](https://github.com/google/pprof/blob/master/proto/profile.proto) 读取分析样本的集合，并生成报告以可视化并帮助分析数据（支持文本和图形报告）\n\nprofile.proto 是一个 Protocol Buffer v3 的描述文件，它描述了一组 callstack 和 symbolization 信息， 作用是表示统计分析的一组采样的调用栈，是很常见的 stacktrace 配置文件格式\n\n### 支持什么使用模式\n\n- Report generation：报告生成\n- Interactive terminal use：交互式终端使用\n- Web interface：Web 界面\n\n### 可以做什么\n\n- CPU Profiling：CPU 分析，按照一定的频率采集所监听的应用程序 CPU（含寄存器）的使用情况，可确定应用程序在主动消耗 CPU 周期时花费时间的位置\n- Memory Profiling：内存分析，在应用程序进行堆分配时记录堆栈跟踪，用于监视当前和历史内存使用情况，以及检查内存泄漏\n- Block Profiling：阻塞分析，记录 goroutine 阻塞等待同步（包括定时器通道）的位置\n- Mutex Profiling：互斥锁分析，报告互斥锁的竞争情况\n\n## 一个简单的例子\n\n我们将编写一个简单且有点问题的例子，用于基本的程序初步分析\n\n### 编写 demo 文件\n\n（1）demo.go，文件内容：\n\n```go\npackage main\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\t_ \"net/http/pprof\"\n\t\"github.com/EDDYCJY/go-pprof-example/data\"\n)\n\nfunc main() {\n\tgo func() {\n\t\tfor {\n\t\t\tlog.Println(data.Add(\"https://github.com/EDDYCJY\"))\n\t\t}\n\t}()\n\n\thttp.ListenAndServe(\"0.0.0.0:6060\", nil)\n}\n```\n\n（2）data/d.go，文件内容：\n\n```go\npackage data\n\nvar datas []string\n\nfunc Add(str string) string {\n\tdata := []byte(str)\n\tsData := string(data)\n\tdatas = append(datas, sData)\n\n\treturn sData\n}\n\n```\n\n运行这个文件，你的 HTTP 服务会多出 /debug/pprof 的 endpoint 可用于观察应用程序的情况\n\n### 分析\n\n#### 一、通过 Web 界面\n\n查看当前总览：访问 `http://127.0.0.1:6060/debug/pprof/`\n\n```\n/debug/pprof/\n\nprofiles:\n0\tblock\n5\tgoroutine\n3\theap\n0\tmutex\n9\tthreadcreate\n\nfull goroutine stack dump\n```\n\n这个页面中有许多子页面，咱们继续深究下去，看看可以得到什么？\n\n- cpu（CPU Profiling）: `$HOST/debug/pprof/profile`，默认进行 30s 的 CPU Profiling，得到一个分析用的 profile 文件\n- block（Block Profiling）：`$HOST/debug/pprof/block`，查看导致阻塞同步的堆栈跟踪\n- goroutine：`$HOST/debug/pprof/goroutine`，查看当前所有运行的 goroutines 堆栈跟踪\n- heap（Memory Profiling）: `$HOST/debug/pprof/heap`，查看活动对象的内存分配情况\n- mutex（Mutex Profiling）：`$HOST/debug/pprof/mutex`，查看导致互斥锁的竞争持有者的堆栈跟踪\n- threadcreate：`$HOST/debug/pprof/threadcreate`，查看创建新 OS 线程的堆栈跟踪\n\n#### 二、通过交互式终端使用\n\n（1）go tool pprof http://localhost:6060/debug/pprof/profile?seconds=60\n\n```sh\n$ go tool pprof http://localhost:6060/debug/pprof/profile\\?seconds\\=60\n\nFetching profile over HTTP from http://localhost:6060/debug/pprof/profile?seconds=60\nSaved profile in /Users/eddycjy/pprof/pprof.samples.cpu.007.pb.gz\nType: cpu\nDuration: 1mins, Total samples = 26.55s (44.15%)\nEntering interactive mode (type \"help\" for commands, \"o\" for options)\n(pprof)\n```\n\n执行该命令后，需等待 60 秒（可调整 seconds 的值），pprof 会进行 CPU Profiling。结束后将默认进入 pprof 的交互式命令模式，可以对分析的结果进行查看或导出。具体可执行 `pprof help` 查看命令说明\n\n```sh\n(pprof) top10\nShowing nodes accounting for 25.92s, 97.63% of 26.55s total\nDropped 85 nodes (cum <= 0.13s)\nShowing top 10 nodes out of 21\n      flat  flat%   sum%        cum   cum%\n    23.28s 87.68% 87.68%     23.29s 87.72%  syscall.Syscall\n     0.77s  2.90% 90.58%      0.77s  2.90%  runtime.memmove\n     0.58s  2.18% 92.77%      0.58s  2.18%  runtime.freedefer\n     0.53s  2.00% 94.76%      1.42s  5.35%  runtime.scanobject\n     0.36s  1.36% 96.12%      0.39s  1.47%  runtime.heapBitsForObject\n     0.35s  1.32% 97.44%      0.45s  1.69%  runtime.greyobject\n     0.02s 0.075% 97.51%     24.96s 94.01%  main.main.func1\n     0.01s 0.038% 97.55%     23.91s 90.06%  os.(*File).Write\n     0.01s 0.038% 97.59%      0.19s  0.72%  runtime.mallocgc\n     0.01s 0.038% 97.63%     23.30s 87.76%  syscall.Write\n```\n\n- flat：给定函数上运行耗时\n- flat%：同上的 CPU 运行耗时总比例\n- sum%：给定函数累积使用 CPU 总比例\n- cum：当前函数加上它之上的调用运行总耗时\n- cum%：同上的 CPU 运行耗时总比例\n\n最后一列为函数名称，在大多数的情况下，我们可以通过这五列得出一个应用程序的运行情况，加以优化 🤔\n\n（2）go tool pprof http://localhost:6060/debug/pprof/heap\n\n```sh\n$ go tool pprof http://localhost:6060/debug/pprof/heap\nFetching profile over HTTP from http://localhost:6060/debug/pprof/heap\nSaved profile in /Users/eddycjy/pprof/pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.008.pb.gz\nType: inuse_space\nEntering interactive mode (type \"help\" for commands, \"o\" for options)\n(pprof) top\nShowing nodes accounting for 837.48MB, 100% of 837.48MB total\n      flat  flat%   sum%        cum   cum%\n  837.48MB   100%   100%   837.48MB   100%  main.main.func1\n```\n\n- -inuse_space：分析应用程序的常驻内存占用情况\n\n- -alloc_objects：分析应用程序的内存临时分配情况\n\n（3） go tool pprof http://localhost:6060/debug/pprof/block\n\n（4） go tool pprof http://localhost:6060/debug/pprof/mutex\n\n#### 三、PProf 可视化界面\n\n这是令人期待的一小节。在这之前，我们需要简单的编写好测试用例来跑一下\n\n##### 编写测试用例\n\n（1）新建 data/d_test.go，文件内容：\n\n```go\npackage data\n\nimport \"testing\"\n\nconst url = \"https://github.com/EDDYCJY\"\n\nfunc TestAdd(t *testing.T) {\n\ts := Add(url)\n\tif s == \"\" {\n\t\tt.Errorf(\"Test.Add error!\")\n\t}\n}\n\nfunc BenchmarkAdd(b *testing.B) {\n\tfor i := 0; i < b.N; i++ {\n\t\tAdd(url)\n\t}\n}\n```\n\n（2）执行测试用例\n\n```\n$ go test -bench=. -cpuprofile=cpu.prof\npkg: github.com/EDDYCJY/go-pprof-example/data\nBenchmarkAdd-4   \t10000000\t       187 ns/op\nPASS\nok  \tgithub.com/EDDYCJY/go-pprof-example/data\t2.300s\n```\n\n-memprofile 也可以了解一下\n\n##### 启动 PProf 可视化界面\n\n###### 方法一：\n\n```\n$ go tool pprof -http=:8080 cpu.prof\n```\n\n###### 方法二：\n\n```\n$ go tool pprof cpu.prof\n$ (pprof) web\n```\n\n如果出现 `Could not execute dot; may need to install graphviz.`，就是提示你要安装 `graphviz` 了 （请右拐谷歌）\n\n##### 查看 PProf 可视化界面\n\n（1）Top\n\n![image](https://s2.ax1x.com/2020/02/15/1xlsYD.jpg)\n\n（2）Graph\n\n![image](https://s2.ax1x.com/2020/02/15/1xlgld.jpg)\n\n框越大，线越粗代表它占用的时间越大哦\n\n（3）Peek\n\n![image](https://s2.ax1x.com/2020/02/15/1xlROI.jpg)\n\n（4）Source\n\n![image](https://s2.ax1x.com/2020/02/15/1xl4Tf.jpg)\n\n通过 PProf 的可视化界面，我们能够更方便、更直观的看到 Go 应用程序的调用链、使用情况等，并且在 View 菜单栏中，还支持如上多种方式的切换\n\n你想想，在烦恼不知道什么问题的时候，能用这些辅助工具来检测问题，是不是瞬间效率翻倍了呢 👌\n\n#### 四、PProf 火焰图\n\n另一种可视化数据的方法是火焰图，需手动安装原生 PProf 工具：\n\n（1） 安装 PProf\n\n```\n$ go get -u github.com/google/pprof\n```\n\n（2） 启动 PProf 可视化界面:\n\n```\n$ pprof -http=:8080 cpu.prof\n```\n\n（3） 查看 PProf 可视化界面\n\n打开 PProf 的可视化界面时，你会明显发现比官方工具链的 PProf 精致一些，并且多了 Flame Graph（火焰图）\n\n它就是本次的目标之一，它的最大优点是动态的。调用顺序由上到下（A -> B -> C -> D），每一块代表一个函数，越大代表占用 CPU 的时间更长。同时它也支持点击块深入进行分析！\n\n![image](https://s2.ax1x.com/2020/02/15/1xlj00.jpg)\n\n## 总结\n\n在本章节，粗略地介绍了 Go 的性能利器 PProf。在特定的场景中，PProf 给定位、剖析问题带了极大的帮助\n\n希望本文对你有所帮助，另外建议能够自己实际操作一遍，最好是可以深入琢磨一下，内含大量的用法、知识点 🤓\n\n## 思考题\n\n你很优秀的看到了最后，那么有两道简单的思考题，希望拓展你的思路\n\n（1）flat 一定大于 cum 吗，为什么？什么场景下 cum 会比 flat 大？\n\n（2）本章节的 demo 代码，有什么性能问题？怎么解决它？\n"
  },
  {
    "path": "content/posts/go/tools/2019-07-12-go-tool-trace.md",
    "content": "---\n\ntitle:      \"Go 大杀器之跟踪剖析 trace\"\ndate:       2019-07-12 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n![image](https://s2.ax1x.com/2020/02/15/1x1phF.png)\n\n在 Go 中有许许多多的分析工具，在之前我有写过一篇 《Golang 大杀器之性能剖析 PProf》 来介绍 PProf，如果有小伙伴感兴趣可以去我博客看看。\n\n但单单使用 PProf 有时候不一定足够完整，因为在真实的程序中还包含许多的隐藏动作，例如 Goroutine 在执行时会做哪些操作？执行/阻塞了多长时间？在什么时候阻止？在哪里被阻止的？谁又锁/解锁了它们？GC 是怎么影响到 Goroutine 的执行的？这些东西用 PProf 是很难分析出来的，但如果你又想知道上述的答案的话，你可以用本文的主角 `go tool trace` 来打开新世界的大门。目录如下：\n\n![image](https://s2.ax1x.com/2020/02/15/1x1P1J.png)\n\n## 初步了解\n\n```go\nimport (\n\t\"os\"\n\t\"runtime/trace\"\n)\n\nfunc main() {\n\ttrace.Start(os.Stderr)\n\tdefer trace.Stop()\n\n\tch := make(chan string)\n\tgo func() {\n\t\tch <- \"EDDYCJY\"\n\t}()\n\n\t<-ch\n}\n```\n\n生成跟踪文件：\n\n```\n$ go run main.go 2> trace.out\n```\n\n启动可视化界面：\n\n```\n$ go tool trace trace.out\n2019/06/22 16:14:52 Parsing trace...\n2019/06/22 16:14:52 Splitting trace...\n2019/06/22 16:14:52 Opening browser. Trace viewer is listening on http://127.0.0.1:57321\n```\n\n查看可视化界面：\n\n![image](https://s2.ax1x.com/2020/02/15/1x1FXR.png)\n\n- View trace：查看跟踪\n- Goroutine analysis：Goroutine 分析\n- Network blocking profile：网络阻塞概况\n- Synchronization blocking profile：同步阻塞概况\n- Syscall blocking profile：系统调用阻塞概况\n- Scheduler latency profile：调度延迟概况\n- User defined tasks：用户自定义任务\n- User defined regions：用户自定义区域\n- Minimum mutator utilization：最低 Mutator 利用率\n\n### Scheduler latency profile\n\n在刚开始查看问题时，除非是很明显的现象，否则不应该一开始就陷入细节，因此我们一般先查看 “Scheduler latency profile”，我们能通过 Graph 看到整体的调用开销情况，如下：\n\n![image](https://s2.ax1x.com/2020/02/15/1x1K9e.png)\n\n演示程序比较简单，因此这里就两块，一个是 `trace` 本身，另外一个是 `channel` 的收发。\n\n### Goroutine analysis\n\n第二步看 “Goroutine analysis”，我们能通过这个功能看到整个运行过程中，每个函数块有多少个有 Goroutine 在跑，并且观察每个的 Goroutine 的运行开销都花费在哪个阶段。如下：\n\n![image](https://s2.ax1x.com/2020/02/15/1x1ljA.png)\n\n通过上图我们可以看到共有 3 个 goroutine，分别是 `runtime.main`、`runtime/trace.Start.func1`、`main.main.func1`，那么它都做了些什么事呢，接下来我们可以通过点击具体细项去观察。如下：\n\n![image](https://s2.ax1x.com/2020/02/15/1x18Bt.jpg)\n\n同时也可以看到当前 Goroutine 在整个调用耗时中的占比，以及 GC 清扫和 GC 暂停等待的一些开销。如果你觉得还不够，可以把图表下载下来分析，相当于把整个 Goroutine 运行时掰开来看了，这块能够很好的帮助我们**对 Goroutine 运行阶段做一个的剖析，可以得知到底慢哪，然后再决定下一步的排查方向**。如下：\n\n| 名称                  | 含义         | 耗时   |\n| --------------------- | ------------ | ------ |\n| Execution Time        | 执行时间     | 3140ns |\n| Network Wait Time     | 网络等待时间 | 0ns    |\n| Sync Block Time       | 同步阻塞时间 | 0ns    |\n| Blocking Syscall Time | 调用阻塞时间 | 0ns    |\n| Scheduler Wait Time   | 调度等待时间 | 14ns   |\n| GC Sweeping           | GC 清扫      | 0ns    |\n| GC Pause              | GC 暂停      | 0ns    |\n\n### View trace\n\n在对当前程序的 Goroutine 运行分布有了初步了解后，我们再通过 “查看跟踪” 看看之间的关联性，如下：\n\n![image](https://s2.ax1x.com/2020/02/15/1x1GHP.png)\n\n这个跟踪图粗略一看，相信有的小伙伴会比较懵逼，我们可以依据注解一块块查看，如下：\n\n1. 时间线：显示执行的时间单元，根据时间维度的不同可以调整区间，具体可执行 `shift` + `?` 查看帮助手册。\n2. 堆：显示执行期间的内存分配和释放情况。\n3. 协程：显示在执行期间的每个 Goroutine 运行阶段有多少个协程在运行，其包含 GC 等待（GCWaiting）、可运行（Runnable）、运行中（Running）这三种状态。\n4. OS 线程：显示在执行期间有多少个线程在运行，其包含正在调用 Syscall（InSyscall）、运行中（Running）这两种状态。\n5. 虚拟处理器：每个虚拟处理器显示一行，虚拟处理器的数量一般默认为系统内核数。\n6. 协程和事件：显示在每个虚拟处理器上有什么 Goroutine 正在运行，而连线行为代表事件关联。\n\n![image](https://s2.ax1x.com/2020/02/15/1x1YAf.jpg)\n\n点击具体的 Goroutine 行为后可以看到其相关联的详细信息，这块很简单，大家实际操作一下就懂了。文字解释如下：\n\n- Start：开始时间\n- Wall Duration：持续时间\n- Self Time：执行时间\n- Start Stack Trace：开始时的堆栈信息\n- End Stack Trace：结束时的堆栈信息\n- Incoming flow：输入流\n- Outgoing flow：输出流\n- Preceding events：之前的事件\n- Following events：之后的事件\n- All connected：所有连接的事件\n\n### View Events\n\n我们可以通过点击 View Options-Flow events、Following events 等方式，查看我们应用运行中的事件流情况。如下：\n\n![image](https://s2.ax1x.com/2020/02/15/1x1d3Q.png)\n\n通过分析图上的事件流，我们可得知这程序从 `G1 runtime.main` 开始运行，在运行时创建了 2 个 Goroutine，先是创建 `G18 runtime/trace.Start.func1`，然后再是 `G19 main.main.func1` 。而同时我们可以通过其 Goroutine Name 去了解它的调用类型，如：`runtime/trace.Start.func1` 就是程序中在 `main.main` 调用了 `runtime/trace.Start` 方法，然后该方法又利用协程创建了一个闭包 `func1` 去进行调用。\n\n![image](https://s2.ax1x.com/2020/02/15/1x1Dun.png)\n\n在这里我们结合开头的代码去看的话，很明显就是 `ch` 的输入输出的过程了。\n\n## 结合实战\n\n今天生产环境突然出现了问题，机智的你早已埋好 `_ \"net/http/pprof\"` 这个神奇的工具，你麻利的执行了如下命令：\n\n- curl http://127.0.0.1:6060/debug/pprof/trace\\?seconds\\=20 > trace.out\n- go tool trace trace.out\n\n### View trace\n\n你很快的看到了熟悉的 List 界面，然后不信邪点开了 View trace 界面，如下：\n\n![image](https://s2.ax1x.com/2020/02/15/1x1cNT.jpg)\n\n完全看懵的你，稳住，对着合适的区域执行快捷键 `W` 不断地放大时间线，如下：\n\n![image](https://s2.ax1x.com/2020/02/15/1x1ID1.jpg)\n\n经过初步排查，你发现上述绝大部分的 G 竟然都和 `google.golang.org/grpc.(*Server).Serve.func` 有关，关联的一大串也是 `Serve` 所触发的相关动作。\n\n![image](https://s2.ax1x.com/2020/02/16/3pNw9I.jpg)\n\n这时候有经验的你心里已经有了初步结论，你可以继续追踪 View trace 深入进去，不过我建议先鸟瞰全貌，因此我们再往下看 “Network blocking profile” 和 “Syscall blocking profile” 所提供的信息，如下：\n\n### Network blocking profile\n\n![image](https://s2.ax1x.com/2020/02/16/3pNfCn.jpg)\n\n### Syscall blocking profile\n\n![image](https://s2.ax1x.com/2020/02/16/3pN7bF.jpg)\n\n通过对以上三项的跟踪分析，加上这个泄露，这个阻塞的耗时，这个涉及的内部方法名，很明显就是哪位又忘记关闭客户端连接了，赶紧改改改。\n\n## 总结\n\n通过本文我们习得了 `go tool trace` 的武林秘籍，它能够跟踪捕获各种执行中的事件，例如 Goroutine 的创建/阻塞/解除阻塞，Syscall 的进入/退出/阻止，GC 事件，Heap 的大小改变，Processor 启动/停止等等。\n\n希望你能够用好 Go 的两大杀器 pprof + trace 组合，此乃排查好搭档，谁用谁清楚，即使他并不万能。\n\n## 参考\n\n- https://about.sourcegraph.com/go/an-introduction-to-go-tool-trace-rhys-hiltner\n- https://www.itcodemonkey.com/article/5419.html\n- https://making.pusher.com/go-tool-trace/\n- https://golang.org/cmd/trace/\n- https://docs.google.com/document/d/1FP5apqzBgr7ahCCgFO-yoVhk4YZrNIDNf9RybngBc14/pub\n- https://godoc.org/runtime/trace"
  },
  {
    "path": "content/posts/go/tools/2019-08-19-godebug-sched.md",
    "content": "---\n\ntitle:      \"用 GODEBUG 看调度跟踪\"\ndate:       2019-08-19 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n![image](https://image.eddycjy.com/b01c2ce25e34f80d499f0488d034b00b.png)\n\n让 Go 更强大的原因之一莫过于它的 GODEBUG 工具，GODEBUG 的设置可以让 Go 程序在运行时输出调试信息，可以根据你的要求很直观的看到你想要的调度器或垃圾回收等详细信息，并且还不需要加装其它的插件，非常方便，今天我们将先讲解 GODEBUG 的调度器相关内容，希望对你有所帮助。\n\n不过在开始前，没接触过的小伙伴得先补补如下前置知识，便于更好的了解调试器输出的信息内容。\n\n## 前置知识\n\nGo scheduler 的主要功能是针对在处理器上运行的 OS 线程分发可运行的 Goroutine，而我们一提到调度器，就离不开三个经常被提到的缩写，分别是：\n\n- G：Goroutine，实际上我们每次调用 `go func` 就是生成了一个 G。\n- P：处理器，一般为处理器的核数，可以通过 `GOMAXPROCS` 进行修改。\n- M：OS 线程\n\n这三者交互实际来源于 Go 的 M: N 调度模型，也就是 M 必须与 P 进行绑定，然后不断地在 M 上循环寻找可运行的 G 来执行相应的任务，如果想具体了解可以详细阅读 [《Go Runtime Scheduler》](https://speakerdeck.com/retervision/go-runtime-scheduler)，我们抽其中的工作流程图进行简单分析，如下:\n\n![image](https://image.eddycjy.com/fb4c6c92c93af3bc2dfc4f13dc167cdf.png)\n\n1. 当我们执行 `go func()` 时，实际上就是创建一个全新的 Goroutine，我们称它为 G。\n2. 新创建的 G 会被放入 P 的本地队列（Local Queue）或全局队列（Global Queue）中，准备下一步的动作。\n3. 唤醒或创建 M 以便执行 G。\n4. 不断地进行事件循环\n5. 寻找在可用状态下的 G 进行执行任务\n6. 清除后，重新进入事件循环\n\n而在描述中有提到全局和本地这两类队列，其实在功能上来讲都是用于存放正在等待运行的 G，但是不同点在于，本地队列有数量限制，不允许超过 256 个。并且在新建 G 时，会优先选择 P 的本地队列，如果本地队列满了，则将 P 的本地队列的一半的 G 移动到全局队列，这其实可以理解为调度资源的共享和再平衡。\n\n另外我们可以看到图上有 steal 行为，这是用来做什么的呢，我们都知道当你创建新的 G 或者 G 变成可运行状态时，它会被推送加入到当前 P 的本地队列中。但其实当 P 执行 G 完毕后，它也会 “干活”，它会将其从本地队列中弹出 G，同时会检查当前本地队列是否为空，如果为空会随机的从其他 P 的本地队列中尝试窃取一半可运行的 G 到自己的名下。例子如下：\n\n![image](https://image.eddycjy.com/e7ca8f212466d8c15ec0f60b69a1ce4d.png)\n\n在这个例子中，P2 在本地队列中找不到可以运行的 G，它会执行 `work-stealing` 调度算法，随机选择其它的处理器 P1，并从 P1 的本地队列中窃取了三个 G 到它自己的本地队列中去。至此，P1、P2 都拥有了可运行的 G，P1 多余的 G 也不会被浪费，调度资源将会更加平均的在多个处理器中流转。\n\n## GODEBUG\n\nGODEBUG 变量可以控制运行时内的调试变量，参数以逗号分隔，格式为：`name=val`。本文着重点在调度器观察上，将会使用如下两个参数：\n\n- schedtrace：设置 `schedtrace=X` 参数可以使运行时在每 X 毫秒发出一行调度器的摘要信息到标准 err 输出中。\n- scheddetail：设置 `schedtrace=X` 和 `scheddetail=1` 可以使运行时在每 X 毫秒发出一次详细的多行信息，信息内容主要包括调度程序、处理器、OS 线程 和 Goroutine 的状态。\n\n### 演示代码\n\n```\nfunc main() {\n\twg := sync.WaitGroup{}\n\twg.Add(10)\n\tfor i := 0; i < 10; i++ {\n\t\tgo func(wg *sync.WaitGroup) {\n\t\t\tvar counter int\n\t\t\tfor i := 0; i < 1e10; i++ {\n\t\t\t\tcounter++\n\t\t\t}\n\t\t\twg.Done()\n\t\t}(&wg)\n\t}\n\n\twg.Wait()\n}\n```\n\n### schedtrace\n\n```\n$ GODEBUG=schedtrace=1000 ./awesomeProject \nSCHED 0ms: gomaxprocs=4 idleprocs=1 threads=5 spinningthreads=1 idlethreads=0 runqueue=0 [0 0 0 0]\nSCHED 1000ms: gomaxprocs=4 idleprocs=0 threads=5 spinningthreads=0 idlethreads=0 runqueue=0 [1 2 2 1]\nSCHED 2000ms: gomaxprocs=4 idleprocs=0 threads=5 spinningthreads=0 idlethreads=0 runqueue=0 [1 2 2 1]\nSCHED 3001ms: gomaxprocs=4 idleprocs=0 threads=5 spinningthreads=0 idlethreads=0 runqueue=0 [1 2 2 1]\nSCHED 4010ms: gomaxprocs=4 idleprocs=0 threads=5 spinningthreads=0 idlethreads=0 runqueue=0 [1 2 2 1]\nSCHED 5011ms: gomaxprocs=4 idleprocs=0 threads=5 spinningthreads=0 idlethreads=0 runqueue=0 [1 2 2 1]\nSCHED 6012ms: gomaxprocs=4 idleprocs=0 threads=5 spinningthreads=0 idlethreads=0 runqueue=0 [1 2 2 1]\nSCHED 7021ms: gomaxprocs=4 idleprocs=0 threads=5 spinningthreads=0 idlethreads=0 runqueue=4 [0 1 1 0]\nSCHED 8023ms: gomaxprocs=4 idleprocs=0 threads=5 spinningthreads=0 idlethreads=0 runqueue=4 [0 1 1 0]\nSCHED 9031ms: gomaxprocs=4 idleprocs=0 threads=5 spinningthreads=0 idlethreads=0 runqueue=4 [0 1 1 0]\nSCHED 10033ms: gomaxprocs=4 idleprocs=0 threads=5 spinningthreads=0 idlethreads=0 runqueue=4 [0 1 1 0]\nSCHED 11038ms: gomaxprocs=4 idleprocs=0 threads=5 spinningthreads=0 idlethreads=0 runqueue=4 [0 1 1 0]\nSCHED 12044ms: gomaxprocs=4 idleprocs=0 threads=5 spinningthreads=0 idlethreads=0 runqueue=4 [0 1 1 0]\nSCHED 13051ms: gomaxprocs=4 idleprocs=0 threads=5 spinningthreads=0 idlethreads=0 runqueue=4 [0 1 1 0]\nSCHED 14052ms: gomaxprocs=4 idleprocs=2 threads=5 \n...\n```\n- sched：每一行都代表调度器的调试信息，后面提示的毫秒数表示启动到现在的运行时间，输出的时间间隔受 `schedtrace` 的值影响。\n- gomaxprocs：当前的 CPU 核心数（GOMAXPROCS 的当前值）。\n- idleprocs：空闲的处理器数量，后面的数字表示当前的空闲数量。\n- threads：OS 线程数量，后面的数字表示当前正在运行的线程数量。\n- spinningthreads：自旋状态的 OS 线程数量。\n- idlethreads：空闲的线程数量。\n- runqueue：全局队列中中的 Goroutine 数量，而后面的 [0 0 1 1] 则分别代表这 4 个 P 的本地队列正在运行的 Goroutine 数量。\n\n在上面我们有提到 “自旋线程” 这个概念，如果你之前没有了解过相关概念，一听 “自旋” 肯定会比较懵，我们引用 《Head First of Golang Scheduler》 的内容来说明：\n \n > 自旋线程的这个说法，是因为 Go Scheduler 的设计者在考虑了 “OS 的资源利用率” 以及 “频繁的线程抢占给 OS 带来的负载” 之后，提出了 “Spinning Thread” 的概念。也就是当 “自旋线程” 没有找到可供其调度执行的 Goroutine 时，并不会销毁该线程 ，而是采取 “自旋” 的操作保存了下来。虽然看起来这是浪费了一些资源，但是考虑一下 syscall 的情景就可以知道，比起 “自旋\"，线程间频繁的抢占以及频繁的创建和销毁操作可能带来的危害会更大。\n\n### scheddetail\n\n如果我们想要更详细的看到调度器的完整信息时，我们可以增加 `scheddetail` 参数，就能够更进一步的查看调度的细节逻辑，如下：\n\n```\n$ GODEBUG=scheddetail=1,schedtrace=1000 ./awesomeProject\nSCHED 1000ms: gomaxprocs=4 idleprocs=0 threads=5 spinningthreads=0 idlethreads=0 runqueue=0 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0\n  P0: status=1 schedtick=2 syscalltick=0 m=3 runqsize=3 gfreecnt=0\n  P1: status=1 schedtick=2 syscalltick=0 m=4 runqsize=1 gfreecnt=0\n  P2: status=1 schedtick=2 syscalltick=0 m=0 runqsize=1 gfreecnt=0\n  P3: status=1 schedtick=1 syscalltick=0 m=2 runqsize=1 gfreecnt=0\n  M4: p=1 curg=18 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1\n  M3: p=0 curg=22 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1\n  M2: p=3 curg=24 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1\n  M1: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 spinning=false blocked=false lockedg=-1\n  M0: p=2 curg=26 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1\n  G1: status=4(semacquire) m=-1 lockedm=-1\n  G2: status=4(force gc (idle)) m=-1 lockedm=-1\n  G3: status=4(GC sweep wait) m=-1 lockedm=-1\n  G17: status=1() m=-1 lockedm=-1\n  G18: status=2() m=4 lockedm=-1\n  G19: status=1() m=-1 lockedm=-1\n  G20: status=1() m=-1 lockedm=-1\n  G21: status=1() m=-1 lockedm=-1\n  G22: status=2() m=3 lockedm=-1\n  G23: status=1() m=-1 lockedm=-1\n  G24: status=2() m=2 lockedm=-1\n  G25: status=1() m=-1 lockedm=-1\n  G26: status=2() m=0 lockedm=-1\n```\n\n在这里我们抽取了 1000ms 时的调试信息来查看，信息量比较大，我们先从每一个字段开始了解。如下：\n\n#### G\n\n- status：G 的运行状态。\n- m：隶属哪一个 M。\n- lockedm：是否有锁定 M。\n\n在第一点中我们有提到 G 的运行状态，这对于分析内部流转非常的有用，共涉及如下 9 种状态：\n\n状态 | 值 | 含义\n---|---| ---\n_Gidle | 0 | 刚刚被分配，还没有进行初始化。\n_Grunnable | 1 | 已经在运行队列中，还没有执行用户代码。\n_Grunning | 2 | 不在运行队列里中，已经可以执行用户代码，此时已经分配了 M 和 P。\n_Gsyscall | 3 | 正在执行系统调用，此时分配了 M。\n_Gwaiting | 4 | 在运行时被阻止，没有执行用户代码，也不在运行队列中，此时它正在某处阻塞等待中。\n_Gmoribund_unused | 5 | 尚未使用，但是在 gdb 中进行了硬编码。\n_Gdead | 6 | 尚未使用，这个状态可能是刚退出或是刚被初始化，此时它并没有执行用户代码，有可能有也有可能没有分配堆栈。\n_Genqueue_unused | 7 | 尚未使用。\n_Gcopystack | 8 | 正在复制堆栈，并没有执行用户代码，也不在运行队列中。\n\n在理解了各类的状态的意思后，我们结合上述案例看看，如下：\n\n```\nG1: status=4(semacquire) m=-1 lockedm=-1\nG2: status=4(force gc (idle)) m=-1 lockedm=-1\nG3: status=4(GC sweep wait) m=-1 lockedm=-1\nG17: status=1() m=-1 lockedm=-1\nG18: status=2() m=4 lockedm=-1\n```\n\n在这个片段中，G1 的运行状态为 `_Gwaiting`，并没有分配 M 和锁定。这时候你可能好奇在片段中括号里的是什么东西呢，其实是因为该 `status=4` 是表示 `Goroutine` 在**运行时时被阻止**，而阻止它的事件就是 `semacquire` 事件，是因为 `semacquire` 会检查信号量的情况，在合适的时机就调用 `goparkunlock` 函数，把当前 `Goroutine` 放进等待队列，并把它设为 `_Gwaiting` 状态。\n\n那么在实际运行中还有什么原因会导致这种现象呢，我们一起看看，如下：\n\n```\n\twaitReasonZero                                    // \"\"\n\twaitReasonGCAssistMarking                         // \"GC assist marking\"\n\twaitReasonIOWait                                  // \"IO wait\"\n\twaitReasonChanReceiveNilChan                      // \"chan receive (nil chan)\"\n\twaitReasonChanSendNilChan                         // \"chan send (nil chan)\"\n\twaitReasonDumpingHeap                             // \"dumping heap\"\n\twaitReasonGarbageCollection                       // \"garbage collection\"\n\twaitReasonGarbageCollectionScan                   // \"garbage collection scan\"\n\twaitReasonPanicWait                               // \"panicwait\"\n\twaitReasonSelect                                  // \"select\"\n\twaitReasonSelectNoCases                           // \"select (no cases)\"\n\twaitReasonGCAssistWait                            // \"GC assist wait\"\n\twaitReasonGCSweepWait                             // \"GC sweep wait\"\n\twaitReasonChanReceive                             // \"chan receive\"\n\twaitReasonChanSend                                // \"chan send\"\n\twaitReasonFinalizerWait                           // \"finalizer wait\"\n\twaitReasonForceGGIdle                             // \"force gc (idle)\"\n\twaitReasonSemacquire                              // \"semacquire\"\n\twaitReasonSleep                                   // \"sleep\"\n\twaitReasonSyncCondWait                            // \"sync.Cond.Wait\"\n\twaitReasonTimerGoroutineIdle                      // \"timer goroutine (idle)\"\n\twaitReasonTraceReaderBlocked                      // \"trace reader (blocked)\"\n\twaitReasonWaitForGCCycle                          // \"wait for GC cycle\"\n\twaitReasonGCWorkerIdle                            // \"GC worker (idle)\"\n```\n\n我们通过以上 `waitReason` 可以了解到 `Goroutine` 会被暂停运行的原因要素，也就是会出现在括号中的事件。\n\n#### M\n\n- p：隶属哪一个 P。\n- curg：当前正在使用哪个 G。\n- runqsize：运行队列中的 G 数量。\n- gfreecnt：可用的G（状态为 Gdead）。\n- mallocing：是否正在分配内存。\n- throwing：是否抛出异常。\n- preemptoff：不等于空字符串的话，保持 curg 在这个 m 上运行。\n\n#### P\n\n- status：P 的运行状态。\n- schedtick：P 的调度次数。\n- syscalltick：P 的系统调用次数。\n- m：隶属哪一个 M。\n- runqsize：运行队列中的 G 数量。\n- gfreecnt：可用的G（状态为 Gdead）。\n\n状态 | 值 | 含义\n---|---| ---\n_Pidle | 0 | 刚刚被分配，还没有进行进行初始化。\n_Prunning | 1 | 当 M 与 P 绑定调用 acquirep 时，P 的状态会改变为 _Prunning。\n_Psyscall | 2 | 正在执行系统调用。\n_Pgcstop | 3 | 暂停运行，此时系统正在进行 GC，直至 GC 结束后才会转变到下一个状态阶段。\n_Pdead | 4 | 废弃，不再使用。\n\n## 总结\n\n通过本文我们学习到了调度的一些基础知识，再通过神奇的 GODEBUG 掌握了观察调度器的方式方法，你想想，是不是可以和我上一篇文章的 `go tool trace` 来结合使用呢，在实际的使用中，类似的办法有很多，组合巧用是重点。\n\n## 参考\n\n- [Debugging performance issues in Go programs](https://software.intel.com/en-us/blogs/2014/05/10/debugging-performance-issues-in-go-programs)\n- [A whirlwind tour of Go’s runtime environment variables](https://dave.cheney.net/tag/godebug)\n- [Go调度器系列（2）宏观看调度器](https://mp.weixin.qq.com/s?__biz=Mzg3MTA0NDQ1OQ==&mid=2247483907&idx=2&sn=c955372683bc0078e14227702ab0a35e&chksm=ce85c607f9f24f116158043f63f7ca11dc88cd519393ba182261f0d7fc328c7b6a94fef4e416&scene=38#wechat_redirect)\n- [Go's work-stealing scheduler](https://rakyll.org/scheduler/)\n- [Scheduler Tracing In Go](https://www.ardanlabs.com/blog/2015/02/scheduler-tracing-in-go.html)\n- [Head First of Golang Scheduler](https://zhuanlan.zhihu.com/p/42057783)\n- [goroutine 的状态切换](http://xargin.com/state-of-goroutine/)\n- [Environment_Variables](https://golang.org/pkg/runtime/#hdr-Environment_Variables)"
  },
  {
    "path": "content/posts/go/tools/2019-09-02-godebug-gc.md",
    "content": "---\n\ntitle:      \"用 GODEBUG 看 GC\"\ndate:       2019-09-02 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n![image](https://image.eddycjy.com/b07f55c7fd136392763729b9782f7776.png)\n\n## 什么是 GC\n\n在计算机科学中，垃圾回收（GC）是一种自动管理内存的机制，垃圾回收器会去尝试回收程序不再使用的对象及其占用的内存。而最早 John McCarthy 在 1959 年左右发明了垃圾回收，以简化 Lisp 中的手动内存管理的机制（来自 wikipedia）。\n\n## 为什么要 GC\n\n手动管理内存挺麻烦，管错或者管漏内存也很糟糕，将会直接导致程序不稳定（持续泄露）甚至直接崩溃。\n\n## GC 带来的问题\n\n硬要说会带来什么问题的话，也就数大家最关注的 Stop The World（STW），STW 代指在执行某个垃圾回收算法的某个阶段时，需要将整个应用程序暂停去处理 GC 相关的工作事项。例如：\n\n行为 | 会不会 STW | 为什么\n---|---|---\n标记开始 | 会 | 在开始标记时，准备根对象的扫描，会打开写屏障（Write Barrier） 和 辅助GC（mutator assist），而回收器和应用程序是并发运行的，因此会暂停当前正在运行的所有 Goroutine。\n并发标记中 | 不会 | 标记阶段，主要目的是标记堆内存中仍在使用的值。 \n标记结束 | 会 | 在完成标记任务后，将重新扫描部分根对象，这时候会禁用写屏障（Write Barrier）和辅助GC（mutator assist），而标记阶段和应用程序是并发运行的，所以在标记阶段可能会有新的对象产生，因此在重新扫描时需要进行 STW。\n\n## 如何调整 GC 频率\n\n可以通过 GOGC 变量设置初始垃圾收集器的目标百分比值，对比的规则为当新分配的数值与上一次收集后剩余的实时数值的比例达到设置的目标百分比时，就会触发 GC，默认值为 GOGC=100。如果将其设置为 GOGC=off 可以完全禁用垃圾回收器，要不试试？\n\n简单来讲就是，GOGC 的值设置的越大，GC 的频率越低，但每次最终所触发到 GC 的堆内存也会更大。\n\n## 各版本 GC 情况\n\n版本| GC 算法 | STW 时间\n---|---|---\nGo 1.0 | STW（强依赖 tcmalloc） | 百ms到秒级别\nGo 1.3 | Mark STW, Sweep 并行 | 百ms级别\nGo 1.5 | 三色标记法, 并发标记清除。同时运行时从 C 和少量汇编，改为 Go 和少量汇编实现 | 10-50ms级别\nGo 1.6 | 1.5 中一些与并发 GC 不协调的地方更改，集中式的 GC 协调协程，改为状态机实现 | 5ms级别\nGo 1.7 | GC 时由 mark 栈收缩改为并发，span 对象分配机制由 freelist 改为 bitmap 模式，SSA引入 | ms级别\nGo 1.8 | 混合写屏障（hybrid write barrier）, 消除 re-scanning stack | sub ms\nGo 1.12 | Mark Termination 流程优化 | sub ms, 但几乎减少一半 \n\n注：资料来源于 @boya 在深圳 Gopher Meetup 的分享。\n\n## GODEBUG\n\nGODEBUG 变量可以控制运行时内的调试变量，参数以逗号分隔，格式为：`name=val`。本文着重点在 GC 的观察上，主要涉及 gctrace 参数，我们通过设置 `gctrace=1` 后就可以使得垃圾收集器向标准错误流发出 GC 运行信息。\n\n## 涉及术语\n\n- mark：标记阶段。\n- markTermination：标记结束阶段。\n- mutator assist：辅助 GC，是指在 GC 过程中 mutator 线程会并发运行，而 mutator assist 机制会协助 GC 做一部分的工作。\n- heap_live：在 Go 的内存管理中，span 是内存页的基本单元，每页大小为 8kb，同时 Go 会根据对象的大小不同而分配不同页数的 span，而 heap_live 就代表着所有 span 的总大小。\n- dedicated / fractional / idle：在标记阶段会分为三种不同的 mark worker 模式，分别是 dedicated、fractional 和 idle，它们代表着不同的专注程度，其中 dedicated 模式最专注，是完整的 GC 回收行为，fractional 只会干部分的 GC 行为，idle 最轻松。这里你只需要了解它是不同专注程度的 mark worker 就好了，详细介绍我们可以等后续的文章。\n\n## 演示代码\n\n```\nfunc main() {\n    wg := sync.WaitGroup{}\n    wg.Add(10)\n    for i := 0; i < 10; i++ {\n        go func(wg *sync.WaitGroup) {\n            var counter int\n            for i := 0; i < 1e10; i++ {\n                counter++\n            }\n            wg.Done()\n        }(&wg)\n    }\n\n    wg.Wait()\n}\n```\n\n## gctrace\n\n```\n$ GODEBUG=gctrace=1 go run main.go    \ngc 1 @0.032s 0%: 0.019+0.45+0.003 ms clock, 0.076+0.22/0.40/0.80+0.012 ms cpu, 4->4->0 MB, 5 MB goal, 4 P\ngc 2 @0.046s 0%: 0.004+0.40+0.008 ms clock, 0.017+0.32/0.25/0.81+0.034 ms cpu, 4->4->0 MB, 5 MB goal, 4 P\ngc 3 @0.063s 0%: 0.004+0.40+0.008 ms clock, 0.018+0.056/0.32/0.64+0.033 ms cpu, 4->4->0 MB, 5 MB goal, 4 P\ngc 4 @0.080s 0%: 0.004+0.45+0.016 ms clock, 0.018+0.15/0.34/0.77+0.065 ms cpu, 4->4->1 MB, 5 MB goal, 4 P\ngc 5 @0.095s 0%: 0.015+0.87+0.005 ms clock, 0.061+0.27/0.74/1.8+0.023 ms cpu, 4->4->1 MB, 5 MB goal, 4 P\ngc 6 @0.113s 0%: 0.014+0.69+0.002 ms clock, 0.056+0.23/0.48/1.4+0.011 ms cpu, 4->4->1 MB, 5 MB goal, 4 P\ngc 7 @0.140s 1%: 0.031+2.0+0.042 ms clock, 0.12+0.43/1.8/0.049+0.17 ms cpu, 4->4->1 MB, 5 MB goal, 4 P\n...\n```\n\n### 格式\n\n```\ngc # @#s #%: #+#+# ms clock, #+#/#/#+# ms cpu, #->#-># MB, # MB goal, # P\n```\n\n### 含义\n\n- `gc#`：GC 执行次数的编号，每次叠加。\n- `@#s`：自程序启动后到当前的具体秒数。\n- `#%`：自程序启动以来在GC中花费的时间百分比。\n- `#+...+#`：GC 的标记工作共使用的 CPU 时间占总 CPU 时间的百分比。\n- `#->#-># MB`：分别表示 GC 启动时, GC 结束时, GC 活动时的堆大小.\n- `#MB goal`：下一次触发 GC 的内存占用阈值。\n- `#P`：当前使用的处理器 P 的数量。\n\n### 案例\n\n```\ngc 7 @0.140s 1%: 0.031+2.0+0.042 ms clock, 0.12+0.43/1.8/0.049+0.17 ms cpu, 4->4->1 MB, 5 MB goal, 4 P\n```\n- gc 7：第 7 次 GC。\n- @0.140s：当前是程序启动后的 0.140s。\n- 1%：程序启动后到现在共花费 1% 的时间在 GC 上。\n- 0.031+2.0+0.042 ms clock：\n   - 0.031：表示单个 P 在 mark 阶段的 STW 时间。\n   - 2.0：表示所有 P 的 mark concurrent（并发标记）所使用的时间。\n   - 0.042：表示单个 P 的 markTermination 阶段的 STW 时间。\n- 0.12+0.43/1.8/0.049+0.17 ms cpu：\n   - 0.12：表示整个进程在 mark 阶段 STW 停顿的时间。\n   - 0.43/1.8/0.049：0.43 表示 mutator assist 占用的时间，1.8 表示 dedicated + fractional 占用的时间，0.049 表示 idle 占用的时间。\n   - 0.17ms：0.17 表示整个进程在 markTermination 阶段 STW 时间。\n- 4->4->1 MB：\n   - 4：表示开始 mark 阶段前的 heap_live 大小。\n   - 4：表示开始 markTermination 阶段前的 heap_live 大小。\n   - 1：表示被标记对象的大小。\n- 5 MB goal：表示下一次触发 GC 回收的阈值是 5 MB。\n- 4 P：本次 GC 一共涉及多少个 P。\n\n## 总结\n\n通过本章节我们掌握了使用 GODEBUG 查看应用程序 GC 运行情况的方法，只要用这种方法我们就可以观测不同情况下 GC 的情况了，甚至可以做出非常直观的对比图，大家不妨尝试一下。\n\n## 关联文章\n\n- [用 GODEBUG 看调度跟踪](https://mp.weixin.qq.com/s/Brby6D7d1szUIBjcD_8kfg)\n\n## 参考\n\n- [Go GC打印出来的这些信息都是什么含义？](https://gocn.vip/question/310)\n- [GODEBUG之gctrace解析](http://cbsheng.github.io/posts/godebug%E4%B9%8Bgctrace%E8%A7%A3%E6%9E%90/)\n- [关于Golang GC的一些误解--真的比Java GC更领先吗？](https://zhuanlan.zhihu.com/p/77943973)\n- @boya 深入浅出Golang Runtime PPT\n"
  },
  {
    "path": "content/posts/go/type-after.md",
    "content": "---\ntitle: \"为什么 Go 语言把类型放在后面？\"\ndate: 2021-12-31T12:55:10+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n前段时间看到大家在吵一个话题，那就是 Go 语言的类型声明，抠知识抠的非常细了，就是为什么他要放在后面。\n\n示例代码如下：\n\n```golang\nvar a []string\nvar b []string\n```\n\n其实在早年 Go 官方估计已经被问烦了，写过一篇《[Go's Declaration Syntax](https://go.dev/blog/declaration-syntax \"Go's Declaration Syntax\")》来具体介绍和说明情况。\n\n为此煎鱼将参考并结合这篇官方资料，带大家一起了解为什么 Go 如此的 “与众不同” ，为什么要把类型放在后面。\n\n## 类型前置\n\n在业内目前有不少知名语言，也采取的是在声明变量类型时，把类型定义在名字前面。像是 C、C++、C#、Java 等：\n\n```c\nint x;\nint x = 100;\n```\n\n基本的格式定义：<data_type> <variable_list>;。\n\n上面的声明是一个简单的例子，如果更复杂一些，Go 官方还给出了著名的函数指针的例子：\n\n```c\nint (*fp)(int a, int b);\n```\n\n更进一步，如果返回值也是个函数指针类型，就会变成：\n\n```c\nint (*(*fp)(int (*)(int, int), int))(int, int)\n```\n\n这已经很难看出来是个 fp 的声明了。\n\n## 类型后置\n\n前面所举例的类型前置的编程语言，很多都是 C 系列中的一者。类型后置的代表，分别有：Go、Rust、Scala、Kotlin 等。\n\n其实在很多类型后置的编程语言种，会采取变量名+冒号+类型的方式出现。就像 Rust 一样：\n\n```rust\nlet x: i32;\n```\n\n基本的格式定义：\n\n```\nx: int\np: pointer to int\na: array[3] of int\n```\n\nGo 官方参照了这类类型后置的设计，并且为了简洁，进一步去掉了冒号和一些关键字，变成：\n\n```golang\nvar a []string\n```\n\n我们再看回前面 fp 的声明的例子：\n\n```c\nint (*(*fp)(int (*)(int, int), int))(int, int)\n```\n\n再对比 Go 语言中就变成了：\n\n```golang\nf func(func(int,int) int, int) func(int, int) int\n```\n\n两者一对比，Go 语言代码可读性确实更高一些。\n\n## 思考\n\n### 后置类别\n\n在类型声明上，实际上分为：变量类型后置、函数返回值后置。两者共同构建了前置还是后置，总不能一个前置，一个后置吧，那得多么的难受。\n\n上方 C 语言和 Go 语言函数指针的例子，所对比带来的代码可读性提高，其实本质上是由**函数返回值后置**所带来的。\n\n和类型前置、后置没太多直接关系。\n\n### 核心思想\n\n在类型后置上来讲，Go 官方核心思想是：**这种声明方式（从左到右的风格）的一个优点是，当类型变得更加复杂时，它的效果非常好**（One merit of this left-to-right style is how well it works as the types become more complex）。\n\nGo 的变量名总是在前，在人的代码阅读上可以**保持从左到右阅读**，不需要像 C 语言一样在一大堆声明中用技巧找变量名对应的类型。\n\n![The Clockwise/Spiral Rule](https://files.mdnice.com/user/3610/b025d274-ae83-404c-8072-2776cd790708.png)\n\n为此甚至有人写了篇 C 语言的顺时针读法《[The Clockwise/Spiral Rule](http://c-faq.com/decl/spiral.anderson.html \"The Clockwise/Spiral Rule\")》，有兴趣可以阅读。\n\n如此一对比，Go 语言的类型后置在复杂场景下与 C 语言的对比确实更好一些。\n\n### 其他因素\n\n#### 类型推导\n\n诸如在类型推导的形式上也会更直观：\n\n```golang\nfunc main() {\n    var s1 := \"脑子进煎鱼了\"\n    var s2 string\n}\n```\n\n也是一个可读性提高的问题。\n\n#### 类型和名字谁更重要\n\n不同设计者对谁更重要的理解也不一样。是类型更重要，还是名字更重要呢？\n\n有的人认为是类型，有的人认为是名字。这就真的是千人千面，众口难调了。\n\n## C# 的后悔\n\n我们看看其他语言，C# 设计组成员之一，其实在《[Sharp Regrets: Top 10 Worst C# Features](https://www.informit.com/articles/article.aspx?p=2425867 \"Sharp Regrets: Top 10 Worst C# Features\")》中的第五点表达了个人对类型前置、后置的设计教训。\n\n\n![](https://files.mdnice.com/user/3610/6d5665b7-125f-4202-8742-c11c433566d7.png)\n\n核心观点是：从编程和数学两方面来看，都有一个约定，即计算的结果在右侧表示，所以在类 C 语言中，类型在左侧是很奇怪的。\n\n在设计时，C# 本来计划把类型注释放在右边。但考虑到类 C 语言，因此遵循了其他语言的惯例。\n\n\n## 总结\n\n实际上该问题的研讨，在 2021 年的现在，大部分 case 都一一被反驳了。类型后置也不是一个与众不同的设计，很多语言都是如此。但既然要讨论 Go 语言，那更多的是站在设计者的角度去考虑。\n\n结合 Go 所提供的官方资料，在当年的目的更多的是为了**在遇到复杂类型定义时，能保持一定的代码可读性**。\n\n当然，这不可否认肯定包含 Go 开发团队的主观意识。有兴趣的可以具体挖挖背后的信息。\n\n如果是你，**你会把类型放在前面，还是后面呢，为什么**？"
  },
  {
    "path": "content/posts/go/unsafe-pointer.md",
    "content": "---\ntitle: \"详解 Go 团队不建议用的 unsafe.Pointer\"\ndate: 2021-12-31T12:54:54+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n大家在学习 Go 的时候，肯定都学过 “Go 的指针是不支持指针运算和转换” 这个知识点。为什么呢？\n\n首先，Go 是一门静态语言，所有的变量都必须为标量类型。不同的类型不能够进行赋值、计算等跨类型的操作。\n\n那么指针也对应着相对的类型，也在 Compile 的静态类型检查的范围内。同时静态语言，也称为强类型。也就是一旦定义了，就不能再改变它。\n\n错误的示例\n-----\n\n```\nfunc main(){\n num := 5\n numPointer := &num\n\n flnum := (*float32)(numPointer)\n fmt.Println(flnum)\n}\n```\n\n输出结果：\n\n```\n# command-line-arguments\n...: cannot convert numPointer (type *int) to type *float32\n```\n\n在示例中，我们创建了一个 `num` 变量，值为 5，类型为 `int`，准备干一番大事。\n\n接下来我们取了其对于的指针地址后，试图强制转换为 `*float32`，结果失败...\n\n万能的破壁 unsafe\n------------\n\n针对刚刚的 “错误示例”，我们可以采用今天的男主角 `unsafe` 标准库来解决。它是一个神奇的包，在官方的诠释中，有如下概述：\n\n*   围绕 Go 程序内存安全及类型的操作。\n    \n*   很可能会是不可移植的。\n    \n*   不受 Go 1 兼容性指南的保护。\n    \n\n简单来讲就是，不怎么推荐你使用，因为它是 unsafe（不安全的）。\n\n但是在特殊的场景下，使用了它，可以打破 Go 的类型和内存安全机制，让你获得眼前一亮的惊喜效果。\n\n### unsafe.Pointer\n\n为了解决这个问题，需要用到 `unsafe.Pointer`。它表示任意类型且可寻址的指针值，可以在不同的指针类型之间进行转换（类似 C 语言的 void \\* 的用途）。\n\n其包含四种核心操作：\n\n*   任何类型的指针值都可以转换为 Pointer。\n    \n*   Pointer 可以转换为任何类型的指针值。\n    \n*   uintptr 可以转换为 Pointer。\n    \n*   Pointer 可以转换为 uintptr。\n    \n\n在这一部分，重点看第一点、第二点。你再想想怎么修改 “错误的例子” 让它运行起来？\n\n修改如下：  \n\n```\nfunc main(){\n num := 5\n numPointer := &num\n\n flnum := (*float32)(unsafe.Pointer(numPointer))\n fmt.Println(flnum)\n}\n```\n\n输出结果：\n\n```\n0xc4200140b0\n```\n\n在上述代码中，我们小加改动。通过 `unsafe.Pointer` 的特性对该指针变量进行了修改，就可以完成任意类型（\\*T）的指针转换。\n\n需要注意的是，这时还无法对变量进行操作或访问，因为不知道该指针地址指向的东西具体是什么类型。不知道是什么类型，又如何进行解析呢？\n\n无法解析也就自然无法对其变更了。\n\n### unsafe.Offsetof\n\n在上小节中，我们对普通的指针变量进行了修改。那么它是否能做更复杂一点的事呢？\n\n```\ntype Num struct{\n i string\n j int64\n}\n\nfunc main(){\n n := Num{i: \"EDDYCJY\", j: 1}\n nPointer := unsafe.Pointer(&n)\n\n niPointer := (*string)(unsafe.Pointer(nPointer))\n *niPointer = \"煎鱼\"\n\n njPointer := (*int64)(unsafe.Pointer(uintptr(nPointer) + unsafe.Offsetof(n.j)))\n *njPointer = 2\n\n fmt.Printf(\"n.i: %s, n.j: %d\", n.i, n.j)\n}\n```\n\n输出结果：\n\n```\nn.i: 煎鱼, n.j: 2\n```\n\n在剖析这段代码做了什么事之前，我们需要了解结构体的一些基本概念：\n\n*   结构体的成员变量在内存存储上是一段连续的内存。\n    \n*   结构体的初始地址就是第一个成员变量的内存地址。\n    \n*   基于结构体的成员地址去计算偏移量。就能够得出其他成员变量的内存地址。\n    \n\n再回来看看上述代码，得出执行流程：\n\n*   修改 `n.i` 值：`i` 为第一个成员变量。因此不需要进行偏移量计算，直接取出指针后转换为 `Pointer`，再强制转换为字符串类型的指针值即可。\n    \n*   修改 `n.j` 值：`j` 为第二个成员变量。需要进行偏移量计算，才可以对其内存地址进行修改。在进行了偏移运算后，当前地址已经指向第二个成员变量。接着重复转换赋值即可。\n    \n\n### 细节分析\n\n需要注意的是，这里使用了如下方法（来完成偏移计算的目标）：\n\n1、uintptr：`uintptr` 是 Go 的内置类型。返回无符号整数，可存储一个完整的地址。后续常用于指针运算\n\n```\ntype uintptr uintptr\n```\n\n2、unsafe.Offsetof：返回成员变量 x 在结构体当中的偏移量。更具体的讲，就是返回结构体初始位置到 x 之间的字节数。需要注意的是入参 `ArbitraryType` 表示任意类型，并非定义的 `int`。它实际作用是一个占位符\n\n```\nfunc Offsetof(x ArbitraryType) uintptr\n```\n\n在这一部分，其实就是巧用了 `Pointer` 的第三、第四点特性。这时候就已经可以对变量进行操作了。\n\n### 糟糕的例子\n\n```\nfunc main(){\n n := Num{i: \"EDDYCJY\", j: 1}\n nPointer := unsafe.Pointer(&n)\n ...\n\n ptr := uintptr(nPointer)\n njPointer := (*int64)(unsafe.Pointer(ptr + unsafe.Offsetof(n.j)))\n ...\n}\n```\n\n这里存在一个问题，`uintptr` 类型是不能存储在临时变量中的。因为从 GC 的角度来看，`uintptr` 类型的临时变量只是一个无符号整数，并不知道它是一个指针地址。\n\n因此当满足一定条件后，`ptr` 这个临时变量是可能被垃圾回收掉的，那么接下来的内存操作，岂不成迷？\n\n## 鼓励\n\n若有任何疑问欢迎评论区反馈和交流，**最好的关系是互相成就**，各位的**点赞**就是[煎鱼](https://github.com/eddycjy)创作的最大动力，感谢支持。\n\n> 文章持续更新，可以微信搜【脑子进煎鱼了】阅读，本文 **GitHub** [github.com/eddycjy/blog](https://github.com/eddycjy/blog) 已收录，学习 Go 语言可以看 [Go 学习地图和路线](https://github.com/eddycjy/go-developer-roadmap)，欢迎 Star 催更。\n\n总结\n--\n\n简洁回顾两个知识点，如下：\n\n*   第一是 `unsafe.Pointer` 可以让你的变量在不同的指针类型转来转去，也就是表示为任意可寻址的指针类型。\n    \n*   第二是 `uintptr` 常用于与 `unsafe.Pointer` 打配合，用于做指针运算，巧妙地很。"
  },
  {
    "path": "content/posts/go/value-quote.md",
    "content": "---\ntitle: \"群里又吵起来了，Go 是传值还是传引用？\"\ndate: 2021-12-31T12:54:52+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n前几天在咱们的 Go 交流群里，有一个小伙伴问了 “xxx 是不是引用类型？” 这个问题，引发了将近 5 小时的讨论：\n\n![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9be6b8a7ae844766afa6c2a79c3666d5~tplv-k3u1fbpfcp-zoom-1.image)\n\n兜兜转转回到了日经的问题，几乎每个月都要有人因此吵一架。就是 **Go 语言到底是传值（值传递），还是传引用（引用传递）**？\n\nGo 官方的定义\n--------\n\n本部分引用 Go 官方 FAQ 的 “When are function parameters passed by value?”，内容如下。\n\n如同 C 系列的所有语言一样，**Go 语言中的所有东西都是以值传递的**。也就是说，一个函数总是得到一个被传递的东西的副本，就像有一个赋值语句将值赋给参数一样。\n\n![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4dc92546d5494e5b9a2a2a5083aa0a5b~tplv-k3u1fbpfcp-zoom-1.image)\n\n例如：\n\n*   向一个函数传递一个 int 值，就会得到 int 的副本。\n    \n    而传递一个指针值就会得到指针的副本，但不会得到它所指向的数据。\n    \n*   map 和 slice 的行为类似于指针：它们是包含指向底层 map 或 slice 数据的指针的描述符。\n    \n\n*   复制一个 map 或 slice 值并不会复制它所指向的数据。\n    \n*   复制一个接口值会复制存储在接口值中的东西。\n    \n*   如果接口值持有一个结构，复制接口值就会复制该结构。如果接口值持有一个指针，复制接口值会复制该指针，但同样不会复制它所指向的数据。\n    \n\n划重点，Go 语言中一切都是值传递，没有引用传递。不要直接把其他概念硬套上来，会犯先入为主的错误的。\n\n传值和传引用\n------\n\n### 传值\n\n传值，也叫做值传递（pass by value）。其**指的是在调用函数时将实际参数复制一份传递到函数中**，这样在函数中如果对参数进行修改，将不会影响到实际参数。\n\n简单来讲，值传递，所传递的是该参数的副本，是复制了一份的，本质上不能认为是一个东西，指向的不是一个内存地址。\n\n案例一如下：\n\n```\nfunc main() {\n s := \"脑子进煎鱼了\"\n fmt.Printf(\"main 内存地址：%p\\n\", &s)\n hello(&s)\n}\n\nfunc hello(s *string) {\n fmt.Printf(\"hello 内存地址：%p\\n\", &s)\n}\n\n```\n\n输出结果：\n\n```\nmain 内存地址：0xc000116220\nhello 内存地址：0xc000132020\n\n```\n\n我们可以看到在 main 函数中的变量 s 所指向的内存地址是 `0xc000116220`。在经过 hello 函数的参数传递后，其在内部所输出的内存地址是 `0xc000132020`，两者发生了改变。\n\n![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d1610690706d432e9af21835d0d65ed3~tplv-k3u1fbpfcp-zoom-1.image)\n\n据此我们可以得出结论，在 Go 语言确实都是值传递。那是不是在函数内修改值，就不会影响到 main 函数呢？\n\n案例二如下：\n\n```\nfunc main() {\n s := \"脑子进煎鱼了\"\n fmt.Printf(\"main 内存地址：%p\\n\", &s)\n hello(&s)\n fmt.Println(s)\n}\n\nfunc hello(s *string) {\n fmt.Printf(\"hello 内存地址：%p\\n\", &s)\n *s = \"煎鱼进脑子了\"\n}\n\n```\n\n我们在 hello 函数中修改了变量 s 的值，那么最后在 main 函数中我们所输出的变量 s 的值是什么呢。是 “脑子进煎鱼了”，还是 \"煎鱼进脑子了\"？\n\n输出结果：\n\n```\nmain 内存地址：0xc000010240\nhello 内存地址：0xc00000e030\n煎鱼进脑子了\n\n```\n\n输出的结果是 “煎鱼进脑子了”。这时候大家可能又犯嘀咕了，煎鱼前面明明说的是 Go 语言只有值传递，也验证了两者的内存地址，都是不一样的，怎么他这下他的值就改变了，这是为什么？\n\n因为 “如果传过去的值是指向内存空间的地址，那么是可以对这块内存空间做修改的”。\n\n也就是这两个内存地址，其实是指针的指针，其根源都指向着同一个指针，也就是指向着变量 s。因此我们进一步修改变量 s，得到输出 “煎鱼进脑子了” 的结果。\n\n### 传引用\n\n传引用，也叫做引用传递（pass by reference），**指在调用函数时将实际参数的地址直接传递到函数中**，那么在函数中对参数所进行的修改，将影响到实际参数。\n\n在 Go 语言中，官方已经明确了没有传引用，也就是没有引用传递这一情况。\n\n因此借用文字简单描述，像是例子中，即使你将参数传入，最终所输出的内存地址都是一样的。\n\n争议最大的 map 和 slice\n-----------------\n\n这时候又有小伙伴疑惑了，你看 Go 语言中的 map 和 slice 类型，能直接修改，难道不是同个内存地址，不是引用了？\n\n其实在 FAQ 中有一句提醒很重要：“map 和 slice 的行为类似于指针，它们是包含指向底层 map 或 slice 数据的指针的描述符”。\n\n### map\n\n针对 map 类型，进一步展开来看看例子：\n\n```\nfunc main() {\n m := make(map[string]string)\n m[\"脑子进煎鱼了\"] = \"这次一定！\"\n fmt.Printf(\"main 内存地址：%p\\n\", &m)\n hello(m)\n\n fmt.Printf(\"%v\", m)\n}\n\nfunc hello(p map[string]string) {\n fmt.Printf(\"hello 内存地址：%p\\n\", &p)\n p[\"脑子进煎鱼了\"] = \"记得点赞！\"\n}\n\n```\n\n输出结果：\n\n```\nmain 内存地址：0xc00000e028\nhello 内存地址：0xc00000e038\n```\n\n确实是值传递，那修改后的 map 的结果应该是什么。既然是值传递，那肯定就是 \"这次一定！\"，对吗？\n\n输出结果：\n\n```\nmap[脑子进煎鱼了:记得点赞！]\n```\n\n结果是修改成功，输出了 “记得点赞！”。这下就尴尬了，为什么是值传递，又还能做到类似引用的效果，能修改到源值呢？\n\n这里的小窍门是：\n\n```\nfunc makemap(t *maptype, hint int, h *hmap) *hmap {}\n```\n\n这是创建 map 类型的底层 runtime 方法，注意其返回的是 `*hmap` 类型，是一个指针。也就是 Go 语言通过对 map 类型的相关方法进行封装，达到了用户需要关注指针传递的作用。\n\n就是说当我们在调用 `hello` 方法时，其相当于是在传入一个指针参数 `hello(*hmap)`，与前面的值类型的案例二类似。\n\n这类情况我们称其为 “引用类型”，但 “引用类型” 不等同于就是传引用，又或是引用传递了，还是有比较明确的区别的。\n\n在 Go 语言中与 map 类型类似的还有 chan 类型：\n\n```\nfunc makechan(t *chantype, size int) *hchan {}\n```\n\n一样的效果。\n\n### slice\n\n针对 slice 类型，进一步展开来看看例子：\n\n```\nfunc main() {\n s := []string{\"烤鱼\", \"咸鱼\", \"摸鱼\"}\n fmt.Printf(\"main 内存地址：%p\\n\", s)\n hello(s)\n fmt.Println(s)\n}\n\nfunc hello(s []string) {\n fmt.Printf(\"hello 内存地址：%p\\n\", s)\n s[0] = \"煎鱼\"\n}\n```\n\n输出结果：\n\n```\nmain 内存地址：0xc000098180\nhello 内存地址：0xc000098180\n[煎鱼 咸鱼 摸鱼]\n```\n\n从结果来看，两者的内存地址一样，也成功的变更到了变量 s 的值。这难道不是引用传递吗，煎鱼翻车了？\n\n关注两个细节：\n\n*   没有用 `&` 来取地址。\n    \n*   可以直接用 `%p` 来打印。\n    \n\n之所以可以同时做到上面这两件事，是因为标准库 `fmt` 针对在这一块做了优化：  \n\n```\nfunc (p *pp) fmtPointer(value reflect.Value, verb rune) {\n var u uintptr\n switch value.Kind() {\n case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer:\n  u = value.Pointer()\n default:\n  p.badVerb(verb)\n  return\n }\n```\n\n留意到代码 `value.Pointer`，标准库进行了特殊处理，直接对应的值的指针地址，当然就不需要取地址符了。\n\n标准库 `fmt` 能够输出 slice 类型对应的值的原因也在此：\n\n```\nfunc (v Value) Pointer() uintptr {\n ...\n case Slice:\n  return (*SliceHeader)(v.ptr).Data\n }\n}\n\ntype SliceHeader struct {\n Data uintptr\n Len  int\n Cap  int\n}\n```\n\n其在内部转换的 `Data` 属性，正正是 Go 语言中 slice 类型的运行时表现 SliceHeader。我们在调用 `%p` 输出时，是在输出 slice 的底层存储数组元素的地址。\n\n下一个问题是：为什么 slice 类型可以直接修改源数据的值呢。\n\n其实和输出的原理是一样的，在 Go 语言运行时，传递的也是相应 slice 类型的底层数组的指针，但需要注意，其使用的是指针的副本。严格意义是引用类型，依旧是值传递。\n\n妙不妙？\n\n总结\n--\n\n在今天这篇文章中，我们针对 Go 语言的日经问题：“Go 语言到底是传值（值传递），还是传引用（引用传递）” 进行了基本的讲解和分析。\n\n另外在业内中，最多人犯迷糊的就是 slice、map、chan 等类型，都会认为是 “引用传递”，从而认为 Go 语言的 xxx 就是引用传递，我们对此也进行了案例演示。\n\n这实则是不大对的认知，因为：“如果传过去的值是指向内存空间的地址，是可以对这块内存空间做修改的”。\n\n其确实复制了一个副本，但他也借由各手段（其实就是传指针），达到了能修改源数据的效果，是引用类型。\n\n石锤，Go 语言只有值传递，\n\n## 鼓励\n\n若有任何疑问欢迎评论区反馈和交流，**最好的关系是互相成就**，各位的**点赞**就是[煎鱼](https://github.com/eddycjy)创作的最大动力，感谢支持。\n\n> 文章持续更新，可以微信搜【脑子进煎鱼了】阅读，本文 **GitHub** [github.com/eddycjy/blog](https://github.com/eddycjy/blog) 已收录，Go 学习地图，欢迎 Star 催更。\n\n\n参考\n--\n\n*   Go 读者交流群\n    \n*   When are function parameters passed by value?\n    \n*   Java 到底是值传递还是引用传递？\n    \n*   Go语言参数传递是传值还是传引用\n"
  },
  {
    "path": "content/posts/go/var.md",
    "content": "---\ntitle: \"为什么 Go 有两种声明变量的方式，有什么区别，哪种好？\"\ndate: 2022-02-05T15:56:48+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好，我是煎鱼。\n\n有一个读者刚入门 Go ，提了一个很有意思的问题：Go 有几种种声明变量的方式，作为初学者，到底用哪种，有什么区别，又为什么要有多种声明方式呢？\n\n为此，煎鱼将和大家一起探索这个问题。\n\n## 变量声明\n\n在 Go 中，一共有 2 种变量声明的方式，各有不同的使用场景。\n\n分别是：\n- 标准变量声明（Variable declarations）。\n- 简短变量声明（Short variable declarations）\n\n### 标准声明\n\n变量声明创建了一个或多个变量，为它们绑定了相应的标识符，并给每个变量一个类型和初始值。\n\n使用语法：\n\n```go\nVarDecl     = \"var\" ( VarSpec | \"(\" { VarSpec \";\" } \")\" ) .\nVarSpec     = IdentifierList ( Type [ \"=\" ExpressionList ] | \"=\" ExpressionList ) .\n```\n\n案例代码：\n\n```go\nvar i int\nvar U, V, W float64\nvar k = 0\nvar x, y float32 = -1, -2\nvar (\n\ti       int\n\tu, v, s = 1.0, 2.0, \"脑子进煎鱼了\"\n)\n```\n\n### 简短声明\n\n一个短变量声明。使用语法：\n\n```\nShortVarDecl = IdentifierList \":=\" ExpressionList .\n```\n\n案例代码：\n\n```go\ns := \"煎鱼进脑子了\"\ni, j := 0, 10\nf := func() int { return 7 }\nch := make(chan int)\nr, w, _ := os.Pipe()\n_, y, _ := coord(p) \n```\n\n## 网友疑惑\n\n在我们群里的 Go 读者提了这问题后，我也搜了搜相关资料。发现在 stackoverflow 上也有人提出了类似的疑惑：\n\n![](https://files.mdnice.com/user/3610/ff663b2b-d65a-4969-b5bb-03bc81a247b5.png)\n\n问题是：使用哪一种声明方式，令人困惑。\n\n题主纠结的原因在于：\n- 如果一个只是另一个的速记方式，为什么它们的行为会不同？\n- Go 的作者出于什么考虑，让两种方式来声明一个变量（为什么不把它们合并成一种方式）？只是为了迷惑我们？\n- 有没有其他方面需要我在使用时留心的，以防掉进坑里？\n\n下面我们结合 stackoverflow 的这个提问内容和回答，进一步展开。\n\n思考一下：标准声明和短声明，这两者的区别的在哪那里，又或是凭喜好随意使用？\n\n\n\n## 区别在哪\n\n### 代码块的分组声明\n\n使用包含关键字 var 的声明语法时，和其他 package、import、const、type、var 等关键字一样，是可以进行分组的代码块声明的。\n\n例如：\n\n```go\nvar (\n\ti       int\n\tu, v, s = 1.0, 2.0, \"脑子进煎鱼了\"\n)\n```\n\n而短声明，是不支持的。\n\n### 变量的初始值指定\n\n使用标准的变量定义时，我们可以只声明，不主动地定义该变量的初始值（缺省会给零值）。\n\n例如：\n\n```go\nvar (\n\ti    int\n\ts    string\n)\n```\n\n而短声明则不行，必须要在程序中主动地去对变量定义一个值。\n\n例如：\n\n```go\ns := \"脑子进煎鱼了\"\n```\n\n此处即使是定义的空字符串，那也属于是用户侧主动定义的，而非缺省的零值。\n\n### 局部变量，区分作用域\n\n在编写程序时，我们经常会有一些局部变量声明，且作用域是有限的。\n\n可以看看自己的代码，这种时候，我们都会采取短声明的方式。\n\n例如：\n\n```go\nfor idx, value := range array {\n    // Do something with index and value\n}\n\nif num := runtime.NumCPU(); num > 1 {\n    fmt.Println(\"Multicore CPU, cores:\", num)\n}\n```\n\n短声明在这类场景下有明确的优势，标准的变量声明在这类场景不讨好。\n\n### 重新声明变量\n\n在 Go 语言规范中有明确提到，短变量声明是可以重新声明变量的，这是一个高频重新声明的覆盖动作。\n\n如下：\n\n```go\nvar name = \"煎鱼.txt\"\n\nfi, err := os.Stat(name)\nif err != nil {\n    log.Fatal(err)\n}\n\ndata, err := ioutil.ReadFile(name)\nif err != nil {\n    log.Fatal(err)\n}\n...\n```\n\n上述代码中，err 变量就是不断地被反复定义。在 `if err != nil` 猖狂的现在，短变量在此处的优势，简直是大杀器了。\n\n## 总结\n\n相信很多小伙伴初入门时都为此纠结过一下，又或是很多教程压根就没有说清楚两者变量声明的区别。\n\n在今天这篇文章中，我们介绍了 Go 的两种变量声明放水。并且针对短声明存在的场景进行了说明。\n\n主要是：\n- 代码块的分组声明。\n- 变量的初始值指定。\n- 局部变量，区分作用域。\n- 重新声明变量。\n\n你觉得变量声明上，还有没有别的优缺点呢，欢迎在评论区交流：）\n\n## 参考\n\n- [GoLang Variable Declaration](https://www.tutorialandexample.com/golang-variable-declaration/)\n- [Why there are two ways of declaring variables in Go, what's the difference and which to use?](https://stackoverflow.com/questions/27919359/why-there-are-two-ways-of-declaring-variables-in-go-whats-the-difference-and-w)\n- [What is the best practice when declaring variables in go (golang)? E.G. should I use \"var x int = 1\" or just \"x := 1\"?](https://www.quora.com/What-is-the-best-practice-when-declaring-variables-in-go-golang-E-G-should-I-use-var-x-int-1-or-just-x-1)"
  },
  {
    "path": "content/posts/go/when-gc.md",
    "content": "---\ntitle: \"Go 什么时候会触发 GC？\"\ndate: 2021-12-31T12:55:08+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好，我是煎鱼。\n\nGo 语言作为一门新语言，在早期经常遭到唾弃的就是在垃圾回收（下称：GC）机制中 STW（Stop-The-World）的时间过长。\n\n那么这个时候，我们又会好奇一点，作为 STW 的起始，Go 语言中什么时候才会触发 GC 呢？\n\n今天就由煎鱼带大家一起来学习研讨一轮。\n\n## 什么是 GC\n\n在计算机科学中，垃圾回收（GC）是一种自动管理内存的机制，垃圾回收器会去尝试回收程序不再使用的对象及其占用的内存。\n\n最早 John McCarthy 在 1959 年左右发明了垃圾回收，以简化 Lisp 中的手动内存管理的机制（来自 @wikipedia）。\n\n![图来自网络](https://files.mdnice.com/user/3610/09425faf-f521-43a9-a7fa-1db3c5163914.png)\n\n## 为什么要 GC\n\n手动管理内存挺麻烦，管错或者管漏内存也很糟糕，将会直接导致程序不稳定（持续泄露）甚至直接崩溃。\n\n## GC 触发场景\n\nGC 触发的场景主要分为两大类，分别是：\n1. 系统触发：运行时自行根据内置的条件，检查、发现到，则进行 GC 处理，维护整个应用程序的可用性。\n2. 手动触发：开发者在业务代码中自行调用 ` runtime.GC` 方法来触发 GC 行为。\n\n### 系统触发\n\n在系统触发的场景中，Go 源码的 `src/runtime/mgc.go` 文件，明确标识了 GC 系统触发的三种场景，分别如下：\n\n```golang\nconst (\n\tgcTriggerHeap gcTriggerKind = iota\n\tgcTriggerTime\n\tgcTriggerCycle\n)\n```\n- gcTriggerHeap：当所分配的堆大小达到阈值（由控制器计算的触发堆的大小）时，将会触发。\n- gcTriggerTime：当距离上一个 GC 周期的时间超过一定时间时，将会触发。\n    -时间周期以 `runtime.forcegcperiod` 变量为准，默认 2 分钟。\n- gcTriggerCycle：如果没有开启 GC，则启动 GC。\n    - 在手动触发的 `runtime.GC` 方法中涉及。\n\n### 手动触发\n\n在手动触发的场景下，Go 语言中仅有 `runtime.GC` 方法可以触发，也就没什么额外的分类的。\n\n![](https://files.mdnice.com/user/3610/8da69a96-6445-4e20-9234-a0c74ba2b5de.png)\n\n但我们要思考的是，一般我们在什么业务场景中，要涉及到手动干涉 GC，强制触发他呢？\n\n需要手动强制触发的场景极其少见，可能会是在某些业务方法执行完后，因其占用了过多的内存，需要人为释放。又或是 debug 程序所需。\n\n## 基本流程\n\n在了解到 Go 语言会触发 GC 的场景后，我们进一步看看触发 GC 的流程代码是怎么样的，我们可以借助手动触发的 `runtime.GC` 方法来作为突破口。\n\n核心代码如下：\n\n```golang\nfunc GC() {\n\tn := atomic.Load(&work.cycles)\n\tgcWaitOnMark(n)\n\n\tgcStart(gcTrigger{kind: gcTriggerCycle, n: n + 1})\n  \n\tgcWaitOnMark(n + 1)\n\n\tfor atomic.Load(&work.cycles) == n+1 && sweepone() != ^uintptr(0) {\n\t\tsweep.nbgsweep++\n\t\tGosched()\n\t}\n  \n\tfor atomic.Load(&work.cycles) == n+1 && atomic.Load(&mheap_.sweepers) != 0 {\n\t\tGosched()\n\t}\n  \n\tmp := acquirem()\n\tcycle := atomic.Load(&work.cycles)\n\tif cycle == n+1 || (gcphase == _GCmark && cycle == n+2) {\n\t\tmProf_PostSweep()\n\t}\n\treleasem(mp)\n}\n```\n1. 在开始新的一轮 GC 周期前，需要调用 `gcWaitOnMark` 方法上一轮 GC 的标记结束（含扫描终止、标记、或标记终止等）。\n2. 开始新的一轮 GC 周期，调用 `gcStart` 方法触发 GC 行为，开始扫描标记阶段。\n3. 需要调用 `gcWaitOnMark` 方法等待，直到当前 GC 周期的扫描、标记、标记终止完成。\n4. 需要调用 `sweepone` 方法，扫描未扫除的堆跨度，并持续扫除，保证清理完成。在等待扫除完毕前的阻塞时间，会调用 `Gosched` 让出。\n5. 在本轮 GC 已经基本完成后，会调用 `mProf_PostSweep` 方法。以此记录最后一次标记终止时的堆配置文件快照。\n6. 结束，释放 M。\n\n## 在哪触发\n\n看完 GC 的基本流程后，我们有了一个基本的了解。但可能又有小伙伴有疑惑了？\n\n本文的标题是 “GC 什么时候会触发 GC”，虽然我们前面知道了触发的时机。但是....Go 是哪里实现的触发的机制，似乎在流程中完全没有看到？\n\n### 监控线程\n\n实质上在 Go 运行时（runtime）初始化时，会启动一个 goroutine，用于处理 GC 机制的相关事项。\n\n代码如下：\n\n```golang\nfunc init() {\n\tgo forcegchelper()\n}\n\nfunc forcegchelper() {\n\tforcegc.g = getg()\n\tlockInit(&forcegc.lock, lockRankForcegc)\n\tfor {\n\t\tlock(&forcegc.lock)\n\t\tif forcegc.idle != 0 {\n\t\t\tthrow(\"forcegc: phase error\")\n\t\t}\n\t\tatomic.Store(&forcegc.idle, 1)\n\t\tgoparkunlock(&forcegc.lock, waitReasonForceGCIdle, traceEvGoBlock, 1)\n    // this goroutine is explicitly resumed by sysmon\n\t\tif debug.gctrace > 0 {\n\t\t\tprintln(\"GC forced\")\n\t\t}\n\n\t\tgcStart(gcTrigger{kind: gcTriggerTime, now: nanotime()})\n\t}\n}\n```\n\n在这段程序中，需要特别关注的是在 `forcegchelper` 方法中，会调用 `goparkunlock` 方法让该 goroutine 陷入休眠等待状态，以减少不必要的资源开销。\n\n在休眠后，会由 `sysmon` 这一个系统监控线程来进行监控、唤醒等行为：\n\n```golang\nfunc sysmon() {\n\t...\n\tfor {\n\t\t...\n\t\t// check if we need to force a GC\n\t\tif t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && atomic.Load(&forcegc.idle) != 0 {\n\t\t\tlock(&forcegc.lock)\n\t\t\tforcegc.idle = 0\n\t\t\tvar list gList\n\t\t\tlist.push(forcegc.g)\n\t\t\tinjectglist(&list)\n\t\t\tunlock(&forcegc.lock)\n\t\t}\n\t\tif debug.schedtrace > 0 && lasttrace+int64(debug.schedtrace)*1000000 <= now {\n\t\t\tlasttrace = now\n\t\t\tschedtrace(debug.scheddetail > 0)\n\t\t}\n\t\tunlock(&sched.sysmonlock)\n\t}\n}\n```\n\n这段代码核心的行为就是不断地在 for 循环中，对 `gcTriggerTime` 和 `now` 变量进行比较，判断是否达到一定的时间（默认为 2 分钟）。\n\n若达到意味着满足条件，会将 `forcegc.g` 放到全局队列中接受新的一轮调度，再进行对上面 `forcegchelper` 的唤醒。\n\n### 堆内存申请\n\n在了解定时触发的机制后，另外一个场景就是分配的堆空间的时候，那么我们要看的地方就非常明确了。\n\n那就是运行时申请堆内存的 `mallocgc` 方法。核心代码如下：\n\n```golang\nfunc mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {\n\tshouldhelpgc := false\n\t...\n\tif size <= maxSmallSize {\n\t\tif noscan && size < maxTinySize {\n\t\t\t...\n\t\t\t// Allocate a new maxTinySize block.\n\t\t\tspan = c.alloc[tinySpanClass]\n\t\t\tv := nextFreeFast(span)\n\t\t\tif v == 0 {\n\t\t\t\tv, span, shouldhelpgc = c.nextFree(tinySpanClass)\n\t\t\t}\n\t\t\t...\n\t\t\tspc := makeSpanClass(sizeclass, noscan)\n\t\t\tspan = c.alloc[spc]\n\t\t\tv := nextFreeFast(span)\n\t\t\tif v == 0 {\n\t\t\t\tv, span, shouldhelpgc = c.nextFree(spc)\n\t\t\t}\n\t\t\t...\n\t\t}\n\t} else {\n\t\tshouldhelpgc = true\n\t\tspan = c.allocLarge(size, needzero, noscan)\n\t\t...\n\t}\n\n\tif shouldhelpgc {\n\t\tif t := (gcTrigger{kind: gcTriggerHeap}); t.test() {\n\t\t\tgcStart(t)\n\t\t}\n\t}\n\n\treturn x\n}\n```\n\n- 小对象：如果申请小对象时，发现当前内存空间不存在空闲跨度时，将会需要调用 `nextFree` 方法获取新的可用的对象，可能会触发 GC 行为。\n- 大对象：如果申请大于 32k 以上的大对象时，可能会触发 GC 行为。\n\n## 总结\n\n在这篇文章中，我们介绍了 Go 语言触发 GC 的两大类场景，并分别基于大类中的细分场景进行了一一说明。\n\n一般来讲，我们对其了解大概就可以了。若小伙伴们对其内部具体实现感兴趣，也可以以文章中的代码具体再打开看。\n\n但需要注意，很有可能 Go 版本一升级，可能又变了，学思想要紧 ：）\n"
  },
  {
    "path": "content/posts/go-meetup1017.md",
    "content": "---\ntitle: \"快速了解 2020 Gopher Meetup 深圳站\"\ndate: 2020-10-18T01:03:25+08:00\ntoc: true\nimages:\ntags: \n  - meetup\n---\n\n昨天（20201017）很有幸的参加了 GoCN 的 2020 Gopher Meetup 深圳站，在台下听各位大佬分享各自的知识和案例。恰好也是我第一次参加这类 Meetup。因此希望也能够让没来的小伙伴对本次分享内容有一定的了解。\n\n按过往对其观察的惯例，一般在下周后官方就会陆续释出 Meetup PPT 和推文，在此引个主线。\n\n![会场外的展板，图来自 GoCN](https://image.eddycjy.com/08910eccd6b1b0c16c0f60571cd7745a.jpeg)\n\n本次 Meetup 主要的方向是云原生，包含四位讲师分享，分享的主题如下：\n\n1. 华为云的 go 语言云原生实践。\n2. go 云上微服务模式解构。\n3. 服务网格在边缘计算领域的实践与探索。\n4. 腾讯大规模 etcd 集群治理与优化实践。\n\n## 华为云的 go 语言云原生实践\n\n讲述华为云在早起使用 Go 语言时，当时 Go 语言的整体生态圈还比较薄弱，因此很多第三方基础/工具库并不全：\n\n![](https://image.eddycjy.com/4adaf1b0b465a3197a9ba9d714dda51d.jpeg)\n\n围绕此整体做一系列的东西，主要从统一框架开始做，提供各种插件，组件，基本涵盖了常用的所有组件。其所带来的的价值/效益：能够直接提高研发效能，让其他业务能够简单使用，不需要太重复造轮子：\n\n![](https://image.eddycjy.com/8c0afff2cb0bb6d2901d1e4d7f8296f2.jpeg)\n\n再往后就介绍了其使用了 Mesh 去做整块的流量标识，金丝雀等流量控制方案。同时还支持了市面上常见的框架和协议。是一个比较完整很常见的整体解决方案，有真实的参考意义。\n\n若在企业内部有建设过这类基础应用可能会感触比较深，且各家多多少少都有类似的东西，需要必需品。社区的小伙伴可以多看看，结合自己的实际情况进行选型或融合。\n\n同时该类基础规范，最难的可能还是如何在企业内部达到大一统，做推广，拿规范，遵守则。\n\n## go 云上微服务模式解构\n\n详细介绍了云原生的定义，主体讲解了 k8s 的基本网络模型，核心在于传统微服务模式和云原生模式下的各类优缺点和对比。\n\n### Edge Proxy\n\n![](https://image.eddycjy.com/0275954e8196ac7c687a1c58e1b4be23.jpeg)\n\n### ServiceMesh\n\n![](https://image.eddycjy.com/794182e59306fec93128765f1854a353.jpeg)\n\n需要听讲者有一定的基础，整体语速相当快，口述内容也比较多，因此这块没有过多详细记述。\n\n演讲内容主体对应云原生下的几种部署模式，线上的话在网上查阅资料学习即可，可能会更高效些。\n\n印象比较深的是，讲师表述目前也没用 ServiceMesh，四年前也预演过，但问题不少，近期打算重新启动。这块我司也多次尝试，多多少少都有些问题，期待 Istio 更稳定成熟的一天。\n\n## 服务网格在边缘计算领域的实践与探索\n\n主体介绍边缘计算相关的 KubeEdge、IEF 等技术体系：\n\n![](https://image.eddycjy.com/ddab54efbf97b478c4ea06c8a602dcef.jpeg)\n\n这块不是我的技术领域内，隔壁的小哥也没听懂，稍微有些乱，因此不过多的介绍。\n\n但发现讲师刚毕业一年多，年轻有为，潜力无限。\n\n## 腾讯大规模 etcd 集群治理与优化实践\n\n腾讯云近期推出了 etcd 的云服务，先前关注了一番。恰好这次的分享者就是相关人士。\n\n分享内容主要分为两个部分：\n\n1. etcd 本身的基本知识\n\n2. etcd 云服务的介绍\n\n### etcd 知识\n\n第一部分是 etcd 的基本知识，以及抽出 kubectl 查询作为案例进行逻辑分析：\n\n![](https://image.eddycjy.com/515c7ea827f0d19b0cedab48897ddc62.jpeg)\n\n再更一步介绍了 etcd 读请求分析，软件分层，以及一些内部逻辑，流转模型等：\n\n![](https://image.eddycjy.com/fe56478c26fcbcf2b7441e27156f125b.jpeg)\n\n若有兴趣的小伙伴可以结合 PPT 再去追一遍源码，会比较的有意思。\n\n因为其讲述的具体的操作模式涉及了 etcd 里的基本理念，大部分情形下都会展开讨论。\n\n### etcd 云服务\n\n第二部分是讲解腾讯云自身在做 etcd 云服务时，遇到的一些 etcd 自身的 BUG，利用了 Chaos 来制造混沌，以此来更好的发现问题。并在后半部分讲述了 etcd 云服务的大体设计和内部模块结构：\n\n![](https://image.eddycjy.com/f864e44559ea61f3a666ce676bf2d5f3.jpeg)\n\n后半部分感觉比较贴近产品介绍，因为每个模块都能单独拿出来再做一次分享，有限的时间能也很难讲深。\n\n印象最深的还是 “为什么 kubernetes 会选型 etcd？”，讲师给出的答案是：Watch 机制、高可用、商业原因。\n\n各位可以细品一下。\n\n## 总结\n\n整体来讲，个人感觉本次 Meetup 以技术的半解决方案和理念介绍居多。\n\n一个技术的实际应用，普遍分三部分来看：\n\n1. 在价值上：为什么要这么做，做了对公司，对团队，对个人的利弊，外部/内部价值是什么。\n\n2. 在技术上：技术攻坚，这个大家接触的多。\n\n3. 在推广上：，如何规范，推广，是行政命令，还是深抓用户痛点，怎么落的地，是非常重要的。\n\n技术类 Meetup 一般以技术角度居多，因此在与会前，建议提前了解一些基本知识才能在会议上更好的听懂、发散以及思考，否则很难碰撞出火花出来。\n\n但问题又来了，如果已经有了基本知识，肯定会做知识拓展，因此直接网上查阅资料和与业界朋友沟通能达到更佳的目的，更高的时间效率比。因此这是一个矛盾和定位。\n\n其实每一次 Meetup 的背后，组织方和分享讲师其实都会付出大量的精力，都不容易。\n\n抛出一个问题，**如果你是讲师，你怎么在 40-60 分钟内把一份 PPT 讲好？把知识/价值传达到位？**"
  },
  {
    "path": "content/posts/go-programming-tour-book.md",
    "content": "---\ntitle: \"新书《Go语言编程之旅：一起用Go做项目》出版啦！\"\ndate: 2020-07-03T21:06:33+08:00\ntoc: true\ntags: \n  - go\n---\n\n从我开始写技术文章起，不知不觉近三年过去了，咨询和催我出书和读者逐年递增，在 2019 年算是达到一个高峰。当然，综合考虑下我也是一直拒绝的，觉得火候还不够。\n\n直至 2019.09 月，polaris 主动找到了我，说有事情想找我商量，本着 “如果你在纠结一件事情做还是不做，不如先做了看看结果，至少不会后悔” 的想法，更何况是长期被 Ping，因此我一口答应下来，故事自此开始了。\n\n![image](https://image.eddycjy.com/04737f7b3e5567224fd2bc93f352203d.jpeg)\n\n## 本书定位\n\n本书不直接介绍 Go 语言的语法基础，内容将面向项目实践，同时会针对核心细节进行分析。而在实际项目迭代中，常常会出现或多或少的事故，因此本书也针对 Go 语言的大杀器（分析工具）以及常见问题进行了全面讲解。\n\n本书适合已经大致学习了 Go 语言的基础语法后，想要跨越到下一个阶段的开发人员，可以填补该阶段的空白和进一步拓展你的思维方向。\n\n## 读者定位\n\n- 基本了解 Go 语言的语法和使用方式的开发人员。\n- 想要进行 Go 相关项目实践和进一步摸索的开发人员。\n- 希望熟悉 Go 常用分析工具的开发人员。\n\n## 本书大纲\n\n本书针对常见的项目类型，主要细分为 5 + 1 板块，分别是命令行、HTTP、RPC、Websocket 应用、进程内缓存以及 Go 中的大杀器。\n\n同时我们在项目开发、细节分析、运行时分析等方方面面都进行了较深入的介绍和说明，能够为 Go 语言开发者提供相对完整的项目实践经验，而如果深入阅读第六章的章节，更能够为未来各类问题出现时的问题排查提供一份强大的知识板块。\n\n如下为本书的思维导图概览：\n\n![image](https://image.eddycjy.com/e5eafb17140fdc06830b838eb7fb0468.png)\n\n## 如何阅读这本书\n\n常规的列目录未免太无趣。我想不如说说从我个人的角度，所看到读者们在近 3 年来是如何阅读/实践我的实践系列文章的，其面向的读者群体是完全一致的。希望能够从另外一个角度告诉你，应当如何阅读这本书，尽可能的效益最大化。\n\n首先，图书，买来要读，而与实战结合的图书，势必需要实践，实践最常见又分为脑内思考和上机实践：\n\n![image](https://image.eddycjy.com/a6faa89061d62be755b715607e2563b8.jpg)\n\n而在持续的交流中，可以发现至少会延伸出以下几类深入层次的不同：\n\n![image](https://image.eddycjy.com/e3b17b0867e66bda4b5c6fb24ddcebc9.jpg)\n\n- **第一层**：只阅读，留有印象，需要时再唤醒，也行。\n\n- **第二层**：阅读并实践，实打实的完成项目实践，收获丰满。\n\n- **第三层**：实践的过程中，**一定会遇到或大或小的问题**，有的人会放弃，这就是分叉点。但有的读者会持续排查，其提升了个人能力（排错能力很重要）。\n\n- **第四层**：实践完毕后，有自己的想法，认为某某地方还可以这样，也可以再实现更多的功能，举一反三，进一步拓展，并对项目提 issues 或进行 pr。\n\n- **第五层**：完成整体项目后，抽离业务代码，标准化框架，实现框架的应用脚手架，并有的读者会进一步开源。\n\n- **第六层**：形成脚手架后，在自己业务组开始落地，实际在项目中使用，由业务学习转化为企业实践。\n\n- **第七层**：在内部落地实践稳妥后，开始在其它业务组开始推广该框架脚手架，进一步标准化，拓展思路。\n\n通过上图中 “七层金字塔” 的理解，我们不难发现其对于实践项目的理解和应用已经不再是单单这个项目，而是有了更深远的意义，抽象一下，对照着著名的 “学习效率金字塔” 来看：\n\n![image](https://image.eddycjy.com/a35394d0ab562efaac8367c3eeff4b07.jpg)\n\n在单纯的 “阅读” 时，其基本处于 “被动学习” 的阶段，而单进入阅读并实践时，已经转为了 “主动学习”，且绝大部分的读者做完实践后，表示 “嗯，实践完，挺好的，有所得”。\n\n这时候就会进入到一个新的阶段（分叉点），绝大部分读者在做完后，会纠结 ”接下来要做什么“：\n\n![image](https://image.eddycjy.com/16c678d883fe3b4e1db5fa99dfd0b302.jpg)\n\n有部分读者会停滞，也有部分读者会转入 “转教别人/立即应用” 的阶段，也就是普遍的在企业内部进行标准化的使用，又或是开源项目，据此得到更一步的深入实践和提高，更大的吸收差距也在于此。\n\n当然，这一切都要基于前面的 “1”，你得先买了书，读了书，接着就是你的选择和创建机遇的能力了，不同的路线效益自然不一样。\n\n## 广告时间\n\n在《Go语言编程之旅：一起用Go做项目》写作中后期，作为 2020 年的煎鱼，我回顾了 2018、2019 年的煎鱼所写的文章，在现在看来发现多多少少都有些瑕疵。再对比本书，在同类主题下，写出的内容更具知识结构化和实战意义，且能做出更优的选题抉择，确实变化了。\n\n因此我也在这里正式向你推荐本书，希望能够给所有 Go 语言爱好者带来更大的技术价值和切切实实的项目实践经验。\n\n后续有任何问题或建议也欢迎随时来交流。\n\n![image](https://image.eddycjy.com/2b7e6446c9eaeeef1658b595bd58512a.jpeg)\n\n## 关于写书\n\n有关注我的小伙伴应该会发现，我之前突然退了很多个微信群，并且停止了博客的更新，也较少在社区里冒泡了。其实本质上是为了给写书让路，希望尽可能的把业余时间都聚焦在写书上。\n\n这时候又会有另外一个问题，那就是写书，是一件非常长耗时的事情，没有任何的外界反馈，因此我严格做了一系列的 todolist 和时间节点的管理，围绕着自己的生活作息设置了一系列闹钟作为信号量提醒自己。\n\n基本是吃饭、睡前构思结构、想灵感，下班回到家一坐下就开始写内容。当然，我也经常走火入魔一想到好的灵感就激动的掏出手机记在工具上，免得第二天大脑重置后丢失了数据，那就很可惜。\n\n最终在长期的坚持下自然而然也就完成了这本书的写作。\n\n## 感谢你们\n\n非常感谢 polaris，在艰难的情况下依旧完成了本书的编写。感谢博文视点的编辑安娜，基本从不催更。感谢曹大、无闻、杨文、傲飞、大彬、晓东的推荐词或 Review.\n\n我还记得当时曹大的书出版时，因为种种原因，我还立下过 ”绝不写书” 的 flag，和晓东在深圳湾一号吃自助餐时立过 “绝对不会放弃，一定会写完” 的 flag，果然计划赶不上变化，flag 该折折。\n\n当然，最该感谢的还是我司的研发负责人，当年把我从个小角落里筛了出来，否则也不会有这一切的开端了。"
  },
  {
    "path": "content/posts/kubernetes/2020-05-01-install.md",
    "content": "---\ntitle: \"Kubernetes 本地快速启动（基于 Docker）\"\ndate: 2020-05-01T11:25:52+08:00\ntoc: true\ntags: \n  - kubernetes\n---\n\nKubernetes 在容器编排大战结束后已经在云原生中占据了明确的一席，最近几年越来越火热，目前搜索趋势：\n\n![image](https://image.eddycjy.com/2583d073cc05df561c735564bb3e9e81.jpg)\n\nKubernetes 的热度很明显是不断地在上涨，因此学习和使用 Kubernetes 是一件相对正确的事，同时公司大多都在往容器化上接近，在拥抱 Kubernetes，所以我们所开发的应用也总是跑在容器环境中。更甚的是，需要对接 Kubernetes API 来做一些功能的开发。\n\n这个时候，我们就需要一个 Kubernetes 环境来进行开发和调试，但你准备开始时，又遇到了一个问题，虽然在 2020 年的现在，Kubernetes 的安装已经有了极大的简化，教程也满地跑，但 Kubernetes 的安装和运行依然有一定的要求，像我，就遇到了如下问题：\n\n![image](https://image.eddycjy.com/9ee9dd19241bfc9099603abcc455787d.jpg)\n\n显然，我的小水管 Mac 承受不起，但是又需要对 Kubernetes 进行学习和使用，除了买云服务器，又或是再在台式机上搭虚拟机，还有没有什么办法呢。\n\n非运维开发的情况下，入门级中最简单的方式就是采用 Docker 所提供的 Kubernetes 支持。\n\n## Docker for Mac/Windows with Kubernetes\n\nDocker 在 17.12.ce 起就提供了 Mac 版本的 [Kubernetes Beta](https://www.docker.com/blog/docker-mac-kubernetes/) 支持，在初始使用上来说非常的方便。首先我们检查 Docker 的版本，点击 Docker -> Check for Updates 确保你的 Docker 在最新版本。\n\n## 快速安装 Kubernetes\n\n在升级完成后，我们可以点击 Docker -> Preferences -> Kubernetes，如下图：\n\n![image](https://image.eddycjy.com/85e953f790c6b6955aa307445a8cf67a.jpg)\n\n你会发现存在三个选项，分别是：Enable Kubernete、Deploy Docker Stacks to Kubernetes by default、Show system containers (advanced)。\n\n一般我们只勾选 “Enable Kubernetes” 选项，如果你还想通过 `docker ps` 查看到 Kubernetes 的相关容器信息，那么还可以勾选 “Show system containers” 选项，在勾选完毕后点击右下角的 “Apply” 按钮就可以了。\n\n这个时候 Docker Preferences 界面上的 Kubernetes 选项将会进入 `kubernetes is starting...` 状态，也就是在拉取各类镜像，需要一定的时间。\n\n这一步有一点需要注意，Kubernetes 大多数的镜像都在国外，如果不翻墙你是无法正常下载的，就会导致一直阻塞在 `kubernetes is starting...`，等半天也没有响应，这种情况下你可以把镜像源改为国内，又或是参考 [k8s-docker-desktop-for-mac](https://github.com/gotok8s/k8s-docker-desktop-for-mac) 项目来安装。\n\n最后在安装完毕后，你可以检查 Docker Preferences 界面左下角的 Kubernetes 状态是否正常就可以了，如下图：\n\n![image](https://image.eddycjy.com/2e626b97af3549deff1f693800bd1275.jpg)\n\n## 安装 Dashboard\n\n在完成 Kubernetes 的安装后，我们需要安装 Dashboard，执行如下命令：\n\n```bash\n$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v1.10.1/src/deploy/recommended/kubernetes-dashboard.yaml\n```\n\n该 Dashboard 对应 [kubernetes/dashboard](https://github.com/kubernetes/dashboard) 项目，而所选择的版本号（兼容性问题），大家可以根据 Releases 中的 Compatibility 来进行选择，但一般不需要太在意，因为 Kubernetes 在 Dashboard 上的建设重心已经逐渐偏向 Promethues 体系了，因此在这一块是比较滞后的，如果想特别依赖 Dashboard 来进行分析，也可以自行选择一些成熟的开源产品。\n\n在完成 `apply` 后，进行代理，执行如下命令：\n\n```\n// 默认 8001 端口，若有需要可通过 --port=8080 进行指定\n$ kubectl proxy\n```\n\n执行完毕完毕后，我们可以直接在通过浏览器访问 `http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/#!/login` 地址，如下图：\n\n![image](https://image.eddycjy.com/905246b8d5150f90282b2b56eaf6a5c8.jpg)\n\n## 创建 ServiceAccount\n\n我们在本地创建一个 k8s-admin.yaml 文件，创建一个 ServiceAccount 和角色绑定关系，写入如下文件内容：\n\n```\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: dashboard-admin\n  namespace: kube-system\n---\nkind: ClusterRoleBinding\napiVersion: rbac.authorization.k8s.io/v1beta1\nmetadata:\n  name: dashboard-admin\nsubjects:\n  - kind: ServiceAccount\n    name: dashboard-admin\n    namespace: kube-system\nroleRef:\n  kind: ClusterRole\n  name: cluster-admin\n  apiGroup: rbac.authorization.k8s.io\n```\n\n获取管理员角色的 `secret` 名称：\n\n```\n$ kubectl get secrets -n kube-system | grep dashboard-admin | awk '{print $1}'\ndashboard-admin-token-dknqx\n```\n\n获取对应的管理员的 `token` 值：\n\n```\n$ kubectl describe secret dashboard-admin-token-dknqx -n kube-system\nName:         dashboard-admin-token-dknqx\nNamespace:    kube-system\nLabels:       <none>\nAnnotations:  kubernetes.io/service-account.name: dashboard-admin\n              kubernetes.io/service-account.uid: 2f817ddd-5802-4e8b-8c38-f4affc16a6fe\n\nType:  kubernetes.io/service-account-token\n\nData\n====\nca.crt:     1025 bytes\nnamespace:  11 bytes\ntoken:      eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJkYXNoYm9hcmQtYWRtaW4tdG9rZW4tZGtucXgiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGFzaGJvYXJkLWFkbWluIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiMmY4MTdkZGQtNTgwMi00ZThiLThjMzgtZjRhZmZjMTZhNmZlIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50Omt1YmUtc3lzdGVtOmRhc2hib2FyZC1hZG1pbiJ9.NKa8OESUsvrolyxezo8w_auKr7jC94gmCm2ZyvQda5X1wOImYYqnH482sDPsQ5Y_V-RH2UD-4eBIuZP6gh0p50nxz-gPqEEPdRln_7osbRMCgcGzqajVo3bx7UOLTJC9ka9S-0rv5HYbn3yeVi6Pt4sVW5GF6KInDcbyyYgse5B_nySIpw4AjdWXNG0npLjneBCQWrVKAQSYYw2mLPZAPPjw6yjXnBvqZmtH1wyvCsKAXbZqBtTp7ddIVvtmveeBuELsX5yIzWqD7qhcpZz4v07FrsfqK0_QJ18BBDBGYMJaaoaK0h2pl_E9sIlAXoBCJ6ol_wwUzfuOshPo9adqww\n```\n\n如果已经熟悉了，可以直接通过组合命令直接获取 token 值：\n\n```\n$ kubectl describe secret dashboard-admin-token-dknqx -n kube-system | grep -E '^token' | awk '{print $2}'\n```\n\n## 登陆 Dashboard\n\n最后我们将 `token` 保存并复制到 Kubernetes Dashboard 的仪表盘并登陆，我们就可以看到如下界面：\n\n![image](https://image.eddycjy.com/980b8975a010b6321de4e87cb29fb009.jpg)\n\n## 小结\n\n我们又回到最初的问题，要学习和使用 Kubernetes，最快最正确的方式，那就是尽快的进行实践，因为本质上我们不是运维开发人员，部署环境的 Kubernetes 也大多不是由你亲自搭建，因为中小微会直接用某云厂商的 Kubernetes，大厂有专职的人员，也不愁这个问题。\n\n因此尽快行动，完成迭代中的需求是我们的目的，等完成后，再回过头来一步步手动搭建 Kubernetes 也未尝不可，所以我认为 Docker for Mac/Windows with Kubernetes 在初级入门阶段是一个很好的安装和使用方法。\n"
  },
  {
    "path": "content/posts/kubernetes/2020-05-03-deployment.md",
    "content": "---\ntitle: \"在 Kubernetes 中部署应用程序\"\ndate: 2020-05-03T11:05:00+08:00\ntoc: true\ntags: \n  - kubernetes\n---\n\n在完成了本地 Kubernetes 的快速搭建（基于 Docker）后，我们已经可以正式的使用它了。对于我们平时最常见的需求，那就是往 Kubernetes 里部署应用程序，如果你没有看过 Kubernetes 相关的知识，这时候你可能会六神无主，但问题不大，我们就可以使用最经典的 Nginx 来小试身手。\n\n\n## 创建 Deployment\n\n创建 nginx-deployment.yaml 文件：\n\n```yaml\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: nginx-deployment\n  labels:\n    app: nginx\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: nginx\n  template:\n    metadata:\n      labels:\n        app: nginx\n    spec:\n      containers:\n      - name: nginx\n        image: nginx:1.18.0\n```\n\n应用 nginx-deployment.yaml 文件：\n\n```shell\n$ kubectl apply -f nginx-deployment.yaml\ndeployment.apps/nginx-deployment created\n```\n\n## 查看运行状态\n\n查看 Pod 运行情况：\n\n```shell\n$ kubectl get pods\nNAME                               READY   STATUS    RESTARTS   AGE\nnginx-deployment-9fbc65d67-9j68x   1/1     Running   0          1m\nnginx-deployment-9fbc65d67-nwbhj   1/1     Running   0          1m\n```\n\n查看 Deployment 部署情况：\n\n```shell\n$ kubectl get deployment\nNAME               READY   UP-TO-DATE   AVAILABLE   AGE\nnginx-deployment   2/2     2            2           29m\n```\n\n我们也可以通过 describe 命令进行查看\n\n```shell\n$ kubectl describe pod nginx-deployment-9fbc65d67-9j68x\nName:           nginx-deployment-9fbc65d67-9j68x\nNamespace:      default\nPriority:       0\nNode:           docker-desktop/192.168.65.3\nStart Time:     Fri, 01 May 2020 17:36:12 +0800\nLabels:         app=nginx\n                pod-template-hash=9fbc65d67\n...\nEvents:\n  Type    Reason     Age   From                     Message\n  ----    ------     ----  ----                     -------\n  Normal  Scheduled  45m   default-scheduler        Successfully assigned default/nginx-deployment-9fbc65d67-9j68x to docker-desktop\n  Normal  Pulling    45m   kubelet, docker-desktop  Pulling image \"nginx:1.18.0\"\n  Normal  Pulled     44m   kubelet, docker-desktop  Successfully pulled image \"nginx:1.18.0\"\n  Normal  Created    44m   kubelet, docker-desktop  Created container nginx\n  Normal  Started    44m   kubelet, docker-desktop  Started container nginx\n```\n\n## 查看 Dashboard\n\n在应用了 Nginx 的 Deployment 后，我们可以查看上一章节中我们所搭建的 Dashboard：\n\n![image](https://image.eddycjy.com/8e08a407d9333759e99a5f09f18e6e8c.jpg)\n\n可能你在想，我只是执行了一条命令，怎么就把 Nginx 跑起来了，这时候你可以去查看容器组中的事件，就能够看到这个容器在运行时做涉及到的事件：\n\n![image](https://image.eddycjy.com/8d96979504a96c5d5b60fad3eeb35060.jpg)\n\n## 部署 Nginx\n\n### 创建 Nginx Service\n\n```shell\napiVersion: v1\nkind: Service\nmetadata:\n  name: nginx-service\n  labels:\n    app: nginx\nspec:\n  selector:\n    app: nginx\n  ports:\n  - name: nginx-port\n    protocol: TCP\n    port: 80\n```\n\n应用 nginx-service.yaml 文件：\n\n```shell\n$ kubectl apply -f nginx-service.yaml\n```\n\n查看应用的运行情况：\n\n```shell\n$ kubectl get services -o wide\n```\n\n但这时候是无法访问到 Nginx 的，我们可以通过 Kubernetes 的 NodePort 的方式对外提供访问：\n\n\n```\napiVersion: v1\nkind: Service\nmetadata:\n  name: nginx-service\n  labels:\n    app: nginx\nspec:\n  selector:\n    app: nginx\n  ports:\n  - name: nginx-port\n    protocol: TCP\n    port: 80\n    nodePort: 30001\n    targetPort: 80\n  type: NodePort\n```\n\n然后再进行访问：\n\n```shell\n$ curl http://127.0.0.1:30001\n<!DOCTYPE html>\n<html>\n<head>\n<title>Welcome to nginx!</title>\n...\n\n\n```\n\n至此我们已经打通了和 Nginx 之间的访问。\n\n## 部署 Go 程序\n\n在部署环境中常常需要将应用程序部署上去，然后对外进行提供服务，我们模拟一个 Go 程序：\n\n```go\nfunc main() {\n    r := gin.Default()\n    r.GET(\"/ping\", func(c *gin.Context) {\n        c.String(http.StatusOK, \"pong\")\n    })\n    err := r.Run(\":9001\")\n    if err != nil {\n        log.Fatalf(\"r.Run err: %v\", err)\n    }\n}\n```\n\n### 编写和编译 Dockerfile\n\n在项目根目录创建 Dockerfile 文件，进行编写：\n\n```shell\nFROM golang:latest\n\nENV GOPROXY https://goproxy.cn,direct\nWORKDIR $GOPATH/src/github.com/eddycjy/awesome-project\nCOPY . $GOPATH/src/github.com/eddycjy/awesome-project\nRUN go build .\n\nEXPOSE 8000\nENTRYPOINT [\"./awesome-project\"]\n```\n\n编译并打标签：\n\n```shell\n$ docker build -t eddycjy/awesome-project:v0.0.1 .\n...\nSuccessfully built b53cef4d2967\nSuccessfully tagged eddycjy/awesome-project:v0.0.1\n```\n\n验证打包进 Docker 中的程序是否正常运行：\n\n```shell\n$ docker run -p 10001:9001 awesome-project\n[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)\n[GIN-debug] Listening and serving HTTP on :9001\n[GIN] 2020/05/03 - 01:51:40 | 200 |        16.9µs |      172.17.0.1 | GET      \"/ping\"\n```\n\n### 上传到 Dockerhub\n\n登陆并推送镜像到 Dockerhub：\n\n```shell\n$ docker login  \n$ docker push eddycjy/awesome-project:v0.0.1      \nThe push refers to repository [docker.io/eddycjy/awesome-project]\n8192ac09ffeb: Pushed \n9eb17b90d619: Pushed \nb04d698ea69d: Pushed \n31561785c3fc: Mounted from library/golang \n4486631650dc: Mounted from library/golang \n5e28718a7d23: Mounted from library/golang \nea1227feeccb: Mounted from library/golang \n9cae1895156d: Mounted from library/golang \n52dba9daa22c: Mounted from library/golang \n78c1b9419976: Mounted from library/golang \nv0.0.1: digest: sha256:a1ef61e899db75eb2171652356be15559f1991b94a971306fb79ceccea8dd515 size: 2422\n```\n\n这时候你在 hub.docker.com 上就能看的你刚刚所上传的镜像内容：\n\n![image](https://image.eddycjy.com/605dca784a33f0466c655d4418818154.jpg)\n\n## 编写 Kubernetes 配置\n\n接下来我们需要针对刚刚所打包的 Go 程序创建 Deployment，编写 go-deployment.yaml 文件：\n\n```shell\napiVersion: extensions/v1beta1\nkind: Deployment\nmetadata:\n  name: awesome-project\n  labels:\n    app: awesome-project\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: awesome-project\n  template:\n    metadata:\n      labels:\n        app: awesome-project\n    spec:\n      containers:\n      - name: awesome-project\n        image: eddycjy/awesome-project:v0.0.1\n```\n\n创建 Service，编写 go-service.yaml：\n\n```\napiVersion: v1\nkind: Service\nmetadata:\n  name: awesome-project-svc\n  labels:\n    app: awesome-project\nspec:\n  ports:\n  - port: 9001\n  type: ClusterIP\n  selector:\n    app: awesome-project\n```\n\n## 部署 Ingress\n\n### Ingress Controller\n\n我们采用 Docker for Mac 特定提供的 Ingress Controller 部署脚本：\n\n```\n$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-0.32.0/deploy/static/provider/cloud/deploy.yaml\n```\n\n其会所有命名空间监视 Ingress 对象，并配置 RBAC 权限，否则你有可能会遇到 403 Forbidden 的问题。\n\n### Nginx Ingress\n\n在完成了 Ingress Controller 等相关部署后，我们可以正式的部署属于自己业务的 Nginx Ingress 对象：\n\n```\napiVersion: extensions/v1beta1\nkind: Ingress\nmetadata:\n  name: test-ingress\n  annotations:\n    nginx.ingress.kubernetes.io/use-regex: \"true\"\nspec:\n  rules:\n  - host: website-ingress.local\n    http:\n      paths:\n      - backend:\n          serviceName: awesome-project-svc\n          servicePort: 9001\n```\n\n`kubectl apply -f` 应用刚刚所编写的配置文件，然后查看运行情况：\n\n```\n$ kubectl get ingresses.\nNAME           HOSTS                   ADDRESS     PORTS   AGE\ntest-ingress   website-ingress.local   localhost   80      8h\n```\n\n如何发现 ADDRESS 为空，则存在问题，需要进行排查（可能性有很多）。在确定 ADDRESS 属性正常后，我们需要打开 `/etc/hosts` 并配置 HOST `127.0.0.1 awesome-project.local` ，并进行验证：\n\n```shell\n$ curl http://awesome-project.local/ping\npong\n```\n\n至此，我们完成了一个简单的 Go 程序的部署和外部调用。\n\n## 小结\n\n在本章中，我们通过部署 Nginx、Ingress、Go 程序的方式，直接实践了 Kubernetes 的基本流程，达到了将自己的简单程序部署在 Kubernetes 的一个小目标，接下来在后续的章节中我们将进一步针对文中所使用到的相关属性和内容进行详细说明。\n\n毕竟在实践过后，就要去了解为什么，这样子才能做到融会贯通。\n"
  },
  {
    "path": "content/posts/kubernetes/2020-05-10-api.md",
    "content": "---\ntitle: \"使用 Go 程序调用 Kubernetes API\"\ndate: 2020-05-10T21:20:26+08:00\ntoc: true\ntags: \n  - kubernetes\n---\n\n在前面的章节中，我们介绍了快速部署 Kubernetes 和应用程序的方法，接下来在本章节中我们将对 Kubernetes 的 API 进行了解，并且进行调用，这是开发人员最关注的一环之一。\n\n因为不论是 DevOps、基础架构，又或是自愈，都需要与 Kubernetes API 直接/间接接触，因此即使在你不懂 Kubernetes 的情况下，Kubernetes API 的知识点仍然属于必知必会，API 总得会调。\n\n## 查看 Kubernetes API\n\n### kube-apiserver 架构图\n\n![image](https://d33wubrfki0l68.cloudfront.net/7016517375d10c702489167e704dcb99e570df85/7bb53/images/docs/components-of-kubernetes.png)\n\n（图来自 kubernetes.io）\n\n在 Kubernetes 的架构中，由 kube-apiserver 组件在主节点上提供 Kubernetes API 服务，kube-apiserver 是 Kubernetes 所有控制的前端，对外提供大量的 RESTful API。\n\n最常见的就是 kubelet 命令，实际上也是在调用 kube-apiserver 所提供的的 API。\n\n###  访问 API 和查看列表\n\n在了解 Kubernetes 的基本架构和提供 API 的方式后，接下来我们需要知道 Kubernetes 到底提供了哪些 API。为了方便调试，首先我们需要在本地运行 `kubectl proxy` 命令，kube-apiserver 就会在本地的 8001 端口上进行监听，也就是提供了一个 Kubernetes API 服务的 HTTP 代理。\n\n这个时候我们可以访问：\n\n```shell\n$ curl http://localhost:8001/api/v1/\n```\n\n查看所提供的对应 API‘s：\n\n```shell\n{\n  \"kind\": \"APIResourceList\",\n  \"groupVersion\": \"v1\",\n  \"resources\": [\n    {\n      \"name\": \"bindings\",\n      \"singularName\": \"\",\n      \"namespaced\": true,\n      \"kind\": \"Binding\",\n      \"verbs\": [\n        \"create\"\n      ]\n    },\n    {\n      \"name\": \"componentstatuses\",\n      \"singularName\": \"\",\n      \"namespaced\": false,\n      \"kind\": \"ComponentStatus\",\n      \"verbs\": [\n        \"get\",\n        \"list\"\n      ],\n      \"shortNames\": [\n        \"cs\"\n      ]\n    },\n    ...\n  ]\n}\n```\n\n访问 `api/v1/pods` 路径，获取所有 Pods\n```shell\n$ curl http://127.0.0.1:8001/api/v1/pods\n```\n\n访问结果：\n\n```\n{\n  \"kind\": \"PodList\",\n  \"apiVersion\": \"v1\",\n  \"metadata\": {\n    \"selfLink\": \"/api/v1/pods\",\n    \"resourceVersion\": \"614376\"\n  },\n  \"items\": [\n    {\n      \"metadata\": {\n        \"name\": \"awesome-project-76788db95b-7ztwr\",\n        \"generateName\": \"awesome-project-76788db95b-\",\n        \"namespace\": \"default\",\n        \"selfLink\": \"/api/v1/namespaces/default/pods/awesome-project-76788db95b-7ztwr\",\n        \"uid\": \"4fdb6661-edbd-4fc6-bf71-1d2dadb3ffc1\",\n        \"resourceVersion\": \"608545\",\n        \"creationTimestamp\": \"2020-05-03T02:29:32Z\",\n        \"labels\": {\n          \"app\": \"awesome-project\",\n          \"pod-template-hash\": \"76788db95b\"\n        },\n        ...\n        ]\n      },\n    ]\n```\n\n更多的 API 列表和介绍可查看[官方文档](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/)。\n\n## Go 程序调用 Kubernetes API\n\n接下来进入在本章的重点，也就是在程序中调用 Kubernetes API，Kubernetes 官方提供了 Go 语言的 Client SDK，也就是[kubernetes/client-go](https://github.com/kubernetes/client-go)，我们借助上一章节的 Go 程序，对其进行改造。\n\n### Demo\n\n首先写入如下方法：\n\n```go\nfunc NewK8sInCluster() error {\n  config, err := rest.InClusterConfig()\n  if err != nil {\n    return err\n  }\n\n  k8sClient, err = kubernetes.NewForConfig(config)\n  if err != nil {\n    return err\n  }\n\n  return nil\n}\n```\n\n编写获取 K8S Pod 列表的方法：\n\n```go\nfunc GetPodList(pod Pod) ([]v1.Pod, error) {\n  podList, err := k8sClient.CoreV1().Pods(pod.Namespace).List(metav1.ListOptions{})\n  if err != nil {\n    return nil, err\n  }\n  pods := podList.Items\n\n  if pod.Name != \"\" {\n    filterPods := make([]v1.Pod, 0, len(pods))\n    for _, p := range pods {\n      if strings.HasPrefix(p.Name, pod.Name) {\n        filterPods = append(filterPods, p)\n      }\n    }\n    pods = filterPods\n  }\n\n  return pods, nil\n}\n```\n\n修改 main 方法中的路由：\n\n```go\nfunc main() {\n  r := gin.Default()\n  r.GET(\"/ping\", func(c *gin.Context) {\n    c.String(http.StatusOK, \"pong\")\n  })\n  r.GET(\"/k8s/pods\", func(c *gin.Context) {\n    err := NewK8sInCluster()\n    if err != nil {\n      c.String(http.StatusInternalServerError, \"NewK8sInCluster err: %v\", err)\n    }\n    pods, err := GetPodList(Pod{Namespace: \"default\"})\n    if err != nil {\n      c.String(http.StatusInternalServerError, \"GetPodList err: %v\", err)\n    }\n\n    c.JSON(http.StatusOK, pods)\n  })\n  err := r.Run(\":9001\")\n  if err != nil {\n    log.Fatalf(\"r.Run err: %v\", err)\n  }\n}\n```\n\n在确定程序正常后，我们重新编译并打标签：\n\n```\n$ docker build -t eddycjy/awesome-project:v0.0.2 .\n$ docker login  \n$ docker push eddycjy/awesome-project:v0.0.2\n```\n\n### 部署并验证\n\n修改 go-deployment.yaml 文件：\n\n```\n      containers:\n      - name: awesome-project\n        image: eddycjy/awesome-project:v0.0.2\n```\n\n将其应用到 Kubernetes：\n\n```\n$ kubectl apply -f go-deployment.yaml \ndeployment.extensions/awesome-project configured\n```\n\n访问刚刚所编写的接口，如下：\n\n```\n$ curl http://website-ingress.local/k8s/pods\n[{\"metadata\":{\"name\":\"awesome-project-64979bcbd9-rm957\",\"generateName\":\"awesome-project-64979bcbd9-\",\"namespace\":\"default\",\"selfLink\":\"/api/v1/namespaces/default/pods/awesome-project-64979bcbd9-rm957\",\"uid\":\"b0a83787-c547-4d74-9bc4-c930b2188e84\",\"resourceVersion\":\"...\n```\n"
  },
  {
    "path": "content/posts/microservice/dismantle.md",
    "content": "---\ntitle: \"微服务的战争：按什么维度拆分服务\"\ndate: 2020-08-19T20:56:55+08:00\nimages:\ntoc: true\ntags: \n  - 微服务\n---\n\n> “微服务的战争” 是一个关于微服务设计思考的系列题材，主要是针对在微服务化后所出现的一些矛盾/冲突点，不涉及具体某一个知识点深入。如果你有任何问题或建议，欢迎随时交流。\n\n微服务，这三个字正在席卷着目前的互联网软件行业，尤其在近几年云原生迸发后，似乎人人都对微服务有了更广泛的使用和理解，张口就是各种各样的问号，有着强大的好奇心。\n\n无独有偶，我有一个朋友鲤鱼在内部微服务的早期（每个业务组起步）就经常遇到下述的对话：\n\n1. **张三**：为什么要拆现在的代码？\n\n2. **鲤鱼**：因为 ！@*）&&#@*！）&#！&）@！&！ 的原因。\n\n3. **张三**：那即将要做的 “微服务” 是按照什么维度去拆分的服务？\n\n4. **鲤鱼**：常见的一般根据 ！@#*@！#&！（@&！@）#@ 的方式来拆分。\n\n5. **张三**：照你这么说好像也不大对，我看每个业务组拆分的维度似乎都不大一样？\n\n6. **鲤鱼**：嗯，每个业务组还有自己的见解，不会完全相同。\n\n7. **张三**：。。。所以微服务的拆分维度到底是什么？\n\n## 为什么想拆\n\n为什么张三会有这个疑问呢，实际上是因为研发内部希望从原先的大单体，大仓库向微服务体系拆分转换，其原先大单体仓库结构，类 Monorepo：\n\n![image](https://image.eddycjy.com/b61faf600a2648ecce258abfa7d57ce8.jpg)\n\n但类 Monorepo 又有不少的问题，像是：\n\n1. **单个 Repo 体积过大**：导致 Git 无法直接拉取。当你设置完再拉取时，在网速慢时还能去泡杯咖啡，并且在开发机性能不佳的情况下，IDE 会比较卡，代码运行起来也慢。\n\n2. **单个 Repo 存在公共函数/SDK**：在代码仓库中，必然存在公共依赖。因此在解决代码冲突时，若遗留了冲突符，且在动态语言中，不涉及便运行正常。但其实在上线后却又影响到其他业务，可真是糟糕透顶，分分钟被迫抱着事故。\n\n3. **单个 Repo 模块职责/边界不清**：在实际的软件开发中，涉及数十个业务组同时在一个大 Repo 下进行开发，没有强控边界的情况下，往往会逐渐模糊，即使在设计时管得住自己，你也不一定能 100% 防止别人模糊你的边界。\n\n4. **单个 Repo 包含了所有的源码**：出现公司源代码泄露时，会导致整个 Repo 外泄，相当的刺激和具有教育意义。因为虽然开放和协同了，不属于你们组的业务代码你也有权限查看了。\n\n当然，Monorepo 是否又完全不可行呢？实际上国外 Google，Facebook，Twitter 等公司都有在使用 Monorepo，并取得了一定的收益。\n\n其实做 Monorepo 是需要相应的大量工具支撑，若单纯只是一个 Repo 塞多个模块，基本都做不好，甚至引火烧身。还不如早早拆开，至少能确保各业务线服务的相对独立性。\n\n## 拆成什么样\n\n张三在明白了拆的原因后，就出现了第二个问题，那就是 “微服务” 要按照什么样的维度去拆分服务？\n\n张三公司内部对于这块的知识处于模糊不清的阶段，因此需要进行深入了解，便于后续的团队共识和方法论建立，理所当然，十万个为什么也就出现了。\n\n### 大单体变独立服务\n\n最常见的拆分的方式是按照业务模块进行服务的拆解，像是前文所提到的业务模块，在设计上边界非常清晰，这种情况直接拆成各个服务就可以了：\n\n![image](https://image.eddycjy.com/9fe68c7e07a732686240243376da5fcb.jpg)\n\n而在拆分后，又会遇到一个新的问题，也就是张三问第三个问题 “每个业务组拆分的维度似乎都不大一样？”。\n\n因为在实际的执行过程中，严谨一些会由 SM 与 RD 一同开会探讨/规范初版的服务划分，而在持续的快速的迭代中，往往新服务的拆分都是由一线 RD 亲自操刀。\n\n即使是架构师亲自操刀，在相对复杂的业务模型下，不同架构师划分出来的也有可能不完全一致，因此无论是哪种情况，你都会发现每个业务组拆分的维度多多少少都不一样了，毕竟人与人的思想都是不一样的，一千个人有一个千个哈姆雷特，因此张三的疑惑是正常的。\n\n就像下图，核心是定义一只鱼，在不同人的眼中能演化出各种奇奇怪怪的鱼：\n\n![image](https://image.eddycjy.com/53ba3e13d2d58affbd3ee75ea622e7d3.jpg)\n\n### 大数据库变独立数据库\n\n在以前早期的大单体快速迭代中，往往是一个大数据库包含所有的业务数据库（甚至数据库账号都不分），这种时候就会带来各种问题。\n\n像是某一天，你所负责的业务模块数据库莫名其妙出现了一些奇奇怪怪的值，你可能就要抓破脑袋去各种代码和 binlog 查了。更甚还有被网络攻击后，数据库配置被获取，直接跳板一拖直接整个脱裤，那可是糟糕透顶了。\n\n![image](https://image.eddycjy.com/8f3e30b271d218c946dbed5acbd1be5a.jpg)\n\n因此在常见的应用设计中，应用程序在连接数据库时会指定连接特定的域名（例如：eddycjy-user），方便未来迁移。并且每个业务服务分别给予独立的数据库只读权限，进行软隔离。而在业务量上来后，也会对业务数据库进行硬隔离，分配特定的 RDS 实例，就不会互相影响了。\n\n### 环境隔离，独立\n\n在服务拆分后，大多会采取独立部署的方式，将两者之间的环境隔离开来，互不干扰，互不影响：\n\n![image](https://image.eddycjy.com/9bf89f92f766e60c48860cd23439fc75.jpg)\n\n像在云原生中，常见于在 Kubernetes 将一个业务服务作为一个 Service 部署发布，再根据实际的资源和调度情况进行 Pod 的扩缩容就可以了，资源也不会有直接干扰，且外部/内部调用都是有统一的入口管理。\n\n## 拆分的阵痛\n\n### 业务接口聚合\n\n在服务拆分的过程中，总是会有阵痛出现。例如在服务需要获取 “项目” 和 “房源” 信息时，到底是由谁来聚合这两个服务的信息。是不是应该由 BFF 来聚合：\n\n![image](https://image.eddycjy.com/4fc07d0fb4a52ca1a0a4336fdd5e4db8.jpg)\n\n或是应该新写一个胶水服务，用于聚合“项目” 和 “房源” 信息，保证其聚合性，减轻 BFF 的负担：\n\n![image](https://image.eddycjy.com/7dc0d0f258e4e6cda6869455695ae050.jpg)\n\n又或是在量级越来越多的情况下，是不是要怀疑一下，这两个服务拆分是不是有问题，“项目” 和 “房源” 在当前业务模型下是否应是一家：\n\n![image](https://image.eddycjy.com/574dfe3c1294b2589cc0a640cd737cf4.jpg)\n\n显然在鲤鱼的经历中，这三种类型他都见过，不同的人总会在不同的思想和业务模型下选择了不同的解决方案，还真的没有绝对准确的准则。\n\n### 分久必合，合久必分\n\n随着对服务化的进程推进，常见的会遇到两种情况：\n\n- **刚接触服务化时**：服务一个没有，偶尔会有一个新的小业务，居然能拆出好好几个微型服务，并扬言要把剩余业务直接抄底重构了，都拆掉，怎么劝都劝不回头。\n\n- **随着业务的不断发展**：快速迭代，服务越来越多，工期压缩，多个 RD 交叉背好几个业务服务，有点力不从心，发现拆的好像有点问题，从最新的情况来看，某某几个服务似乎应该合在一起。\n\n- **业务阶段性稳定**：。。。这，以前这块好像有点问题，也太难拓展了，不应该这么拆，谁调了我，我的上下游是谁。\n\n大多数的情况都是第二和第三者，但在实际操作中也不见得会合并服务，大多数 RD 会选择吞进心里，因为服务变迁所带来的工期延长和影响面无法直接预估（且存在历史代码，人员可能已经离职多年）。即使是服务拓扑也只能查看到一定时间内的服务调用，不会看到全部，因此上下游均无法 100% 确定。因此综合来看，弊大于利。\n\n在解决方案上，更多的是在下次新服务规划时控制划分变量（因为已经有更成熟的经验了）。\n\n实在不行了，才有可能会新起聚合服务将原本的多个服务聚合，又或是采取版本号等方式进行新老分流。甚至下定决心，蚂蚁搬家，起新服务一个个板块重构，一个个挪，持续灰度，“彻底” 解决历史包袱，完成转化。\n\n## 拆分准则\n\n张三又发话了，你说的我都懂，内部微服务都发展好几年了，作为已经有丰富研发经验的人，能不能释出一套微服务拆分的准则呢，否则每一个人都要经历一遍，怎么办，有没有什么基本准则可以遵守呢，你看现在 DDD 那么火，能不能 DDD 一下，让核心一致呢？\n\n机智的鲤鱼掐指一算，张三肯定想的是让所有业务组的拆分，都能依据拆分的核心准则走，实现你中有我，我中有你，看哪哪都有影子，核心不跑偏就行，建立一套完美的方法核心论：\n\n![image](https://image.eddycjy.com/ffd2a0d1e81011ea1a37b61e0f3b0513.jpg)\n\n这种建议右拐 Google “微服务如何拆分”，网上有超级多的指导资料，建议先培养在团队内的共识。毕竟在每次拆服务时让每一个人都对照着那一长串的 “微服务拆分准则” 是一件很不科学的事情，更多的工程师会依据自身的经验进行当前其认为的最合理拆解。\n\n而准则，你认为的核心 A，在他人眼里并不一定是正确，他可能认为是 B，因此在事业部，业务团队中达成共识并把拆分思想融合进每位 RD 思想中，长期的共同分析现在的拆分情况，且让大家基本认同才是最重要的。\n\n同时让全公司都依据一个准则来做，在服务拆分这种无法利用工具流程强控制的情况，本身就是一个伪命题，更多的会是人与人之间的妥协，基本上会变成一个少有人看的 “指导” 文档。\n\n## 总结\n\n在微服务中，服务的拆分总是能让人如此细细品味，本文并不是具体的讲某几个知识点，更多的是阐述在服务化发展的历程中的 “冲突点” 又或是 “矛盾点”，不同的人总有不一样的理解，希望能够给大家带来一些思考。\n\n且在阅读微服务相关指南时，更建议看企业实践后拆分的经验分享，否则单纯看 “指南” 没有过多的意义，要看具体的公司/团队情况和业务模型。\n\n## 推荐阅读\n\nMonorepo：\n\n- [Why Google Stores Billions of Lines of Code in a Single Repository](https://cacm.acm.org/magazines/2016/7/204032-why-google-stores-billions-of-lines-of-code-in-a-single-repository/fulltext)\n\n- [Monorepos: Please don’t!](https://medium.com/@mattklein123/monorepos-please-dont-e9a279be011b)\n\n- [Why might a project/company use a monorepo?](https://dev.to/david_ojeda/comment/77k5)\n\nMicroservices：\n\n- [Nginx Refactoring a Monolith into Microservices](https://www.nginx.com/blog/introduction-to-microservices/)"
  },
  {
    "path": "content/posts/microservice/flowcontrol-circuitbreaker.md",
    "content": "---\ntitle: \"限流熔断是什么，怎么做，不做行不行？\"\ndate: 2020-10-05T13:24:16+08:00\ntoc: true\nimages:\ntags: \n  - 微服务\n  - 服务治理\n---\n\n“在分布式应用中，最常见的问题是什么呢？”\n\n“一个分布式应用部署上去后，还要关注什么？”\n\n![image](https://image.eddycjy.com/8c42e87dd4f7f1c59431c6185f608699.png)\n\n“这服务的远程调用依赖似乎有点多...”\n\n## 前言\n\n在 [《微服务的战争：级联故障和雪崩》](https://eddycjy.com/posts/microservice/linkage/)中有提到，在一个分布式应用中，最常见，最有危险性之一的点就是级联故障所造成的雪崩，而其对应的解决方案为**根据特定的规则/规律进行流量控制和熔断降级**，避免请求发生堆积，保护自身应用，也防止服务提供方进一步过载。\n\n![image](https://image.eddycjy.com/79a7f8870edffd331432272cf5db2c46.jpg)\n\n简单来讲就是，要控制访问量的流量，要防各类调用的强/弱依赖，才能保护好应用程序的底线。\n\n## 诉求，期望\n\n1. 诉求：作为一个业务，肯定是希望自家的应用程序，能够全年无休，最低限度也要有个 4 个 9，一出突发性大流量，在资源贫乏的窗口期，就马上能够自动恢复。\n\n2. 期望：万丈高楼平地起，我们需要对应用程序进行流量控制、熔断降级。确保在特定的规则下，系统能够进行容错，只处理自己力所能及的请求。若有更一步诉求，再自动扩缩容，提高系统资源上限。\n\n## 解决方案\n\n要如何解决这个问题呢，可以关注到问题的核心点是 “系统没有任何的保护的情况下”，因此核心就是让系统，让你的应用程序有流量控制的保护。一般含以下几个方面：\n\n- 来自端控制：在业务/流量网关处内置流量控制、熔断降级的外部插件，起到端控制的效果。\n\n- 来自集群/节点控制：在统一框架中内建流量控制、熔断降级的处理逻辑，起到集群/节点控制的效果。\n\n- 来自 Mesh 控制：通过 ServiceMesh 来实现流量控制、熔断降级。侵入性小，能带来多种控制模式，但有利有弊。\n\n以上的多种方式均可与内部的治理平台打通，且流量控制、熔断降级是不止面试应用程序的，就看资源埋点上如何设计、注入。常见有如下几种角度：\n\n- 资源的调用关系：例如远程调用，像是面向 HTTP、SQL、Redis、RPC 等调用均，另外针对文件句柄控制也可以。\n\n- 运行指标：例如 QPS、线程池、系统负载等。\n\n## 流量控制\n\n在资源不变的情况下，系统所能提供的处理能力是有限的。而系统所面对的请求所到来的时间和量级往往是随机且不可控的。因此就会存在可能出现突发性流量，而在系统没有任何的保护的情况下，系统就会在数分钟内无法提供正常服务，常见过程为先是出现调用延迟，接着持续出现饱和度上升，最终假死。\n\n![image](https://image.eddycjy.com/6a58406bb7c90355c82c5cb50f417f9a.jpg)\n\n流量控制一般常见的有两种方式，分别是：基于 QPS、基于并发隔离。\n\n### 基于 QPS\n\n最常用的流量控制场景，就是基于 QPS 来做流控，在一定的时间窗口内按照特定的规则达到所设定的阈值则进行调控：\n\n![image](https://image.eddycjy.com/58f0d6fe0043963f6e40c1d73c8e019e.jpg)\n\n#### 案例\n\n在本文中借助 [sentinel-golang](https://github.com/alibaba/sentinel-golang) 来实现案例所需的诉求，代码如下：\n\n```\nimport (\n\t...\n\tsentinel \"github.com/alibaba/sentinel-golang/api\"\n\t\"github.com/alibaba/sentinel-golang/core/base\"\n\t\"github.com/alibaba/sentinel-golang/core/flow\"\n\t\"github.com/alibaba/sentinel-golang/util\"\n)\n\nfunc main() {\n\t_ = sentinel.InitDefault()\n\t_, _ = flow.LoadRules([]*flow.Rule{\n\t\t{\n\t\t\tResource:               \"控制吃煎鱼的速度\",\n\t\t\tThreshold:              60,\n\t\t\tControlBehavior:        flow.Reject,\n\t\t},\n\t})\n\n\t...\n\te, b := sentinel.Entry(\"控制吃煎鱼的速度\", sentinel.WithTrafficType(base.Inbound))\n\tif b != nil {\n\t    // Blocked\n\t} else {\n\t    // Passed\n\t    e.Exit()\n\t}\n}\n```\n\n总的来讲，上述规则结果就是 1s 内允许通过 60 个请求，超出的请求的处理策略为直接拒绝（Reject）。\n\n首先我们初始化了 Sentinel 并定义资源（Resource）为 “控制吃煎鱼的速度”。其 Threshold 配置为 3，也就是 QPS 的阈值为 3，统计窗口未设置默认值为 1s，ControlBehavior 控制的行为为直接拒绝。\n\n而在满足阈值条件后，常见的处理策略还有匀速排队（Throttling），匀速排队方式会严格控制请求通过的间隔时间，也就是让请求以均匀的速度通过。\n\n### 基于并发隔离\n\n基于资源访问的并发协程数来控制对资源的访问数量，主要是控制对资源访问的最大协程数，避免因为资源的异常导致协程耗尽。\n\n![image](https://image.eddycjy.com/f255b874409c8a894b7254558b0828ca.jpg)\n\n这类情况，Go 语言在设计上常常可以使用协程池来进行控制，但设计总是赶不上计划的，且不同场景情况可能不同，因此作为一个日常功能也是非常有存在的必要性。\n\n## 熔断降级\n\n在分布式应用中，随着不断地业务拆分，远程调用逐渐变得越来越多。且在微服务盛兴的情况下，一个小业务拆出七八个服务的也常有。\n\n此时就会出现一个经典的问题，那就是客户端的一个普通调用，很有可能就要经过好几个服务，而一个服务又有可能远程调用外部 HTTP、SQL、Redis、RPC 等，调用链会特别的长。\n\n若其中一个调用流程出现了问题，且没有进行调控，就会出现级联故障，最终导致系统雪崩：\n\n![image](https://image.eddycjy.com/2e7b0d94c32f3bd8f3f7168046671d2b.jpg)\n\n\n服务 D 所依赖的外部接口出现了故障，而他并没有做任何的控制，因此扩散到了所有调用到他的服务，自然也就包含服务 B，因此最终出现系统雪崩。\n\n这种最经典的是出现在默认 Go http client 调用没有设置 Timeout，从而只要出现一次故障，就足矣让记住这类 “坑”，毕竟崩的 ”慢“，错误日志还多。（via: 《微服务的战争：级联故障和雪崩》）\n\n### 目的和措施\n\n为了解决上述问题所带来的灾难，在分布式应用中常需要对服务依赖进行熔断降级。在存在问题时，暂时切断内部调用，避免局部不稳定因素导致整个分布式系统的雪崩。\n\n而熔断降级作为保护服务自身的手段，通常是在客户端进行规则配置和熔断识别：\n\n![image](https://image.eddycjy.com/0fc0b2f9367d384d4ef0a6398054a8c6.jpg)\n\n常见的有三种熔断降级措施：慢调用比例策略、错误比例策略、错误计数策略。\n\n### 慢调用比例\n\n在所设定的时间窗口内，慢调用的比例大于所设置的阈值，则对接下来访问的请求进行自动熔断。\n\n### 错误比例\n\n在所设定的时间窗口内，调用的访问错误比例大于所设置的阈值，则对接下来访问的请求进行自动熔断。\n\n### 错误计数\n\n在所设定的时间窗口内，调用的访问错误次数大于所设置的阈值，则对接下来访问的请求进行自动熔断。\n\n## 实践案例\n\n知道流量控制、熔断降级的基本概念和功能后，在现实环境中应该如何结合项目进行使用呢。最常见的场景是可针对业务服务的 HTTP 路由进行流量控制，以 HTTP 路由作为资源埋点，这样子就可以实现接口级的调控了。\n\n![image](https://image.eddycjy.com/00c7676ca398bf4c5cc8725e69ce77b0.jpg)\n\n还可以增强其功能特性，针对参数也进行多重匹配。常会有这种限流诉求：针对 `HTTP GET /eddycjy/info` 且 language 为 go 的情况下进行限流。另外还可以针对 HTTP 调用封装统一方法，进行默认的熔断注入，实现多重保障。\n\n而结合系统负载、服务 QPS 等，可以对限流熔断的规则数据源进行实时调控，再结合 Watch 机制，就能够比较平滑的实现自适应限流熔断的接入。\n\n## 总结\n\n在分布式应用中，限流熔断是非常重要的一环，越早开始做越有益处。但需要注意的是，不同公司的业务模型多多少少有些不一样，所针对的匹配维度多少有些不同，因此需要提前进行业务调研。\n\n且在做业务的限流熔断时，注意把度量指标的打点做上，这样子后续就能够结合 Prometheus+Grafana+Alertmanager 做一系列的趋势图，熔断告警，自动扩缩容等相关工作了，会是一个很好的助力。"
  },
  {
    "path": "content/posts/microservice/leaky-token-buckets.md",
    "content": "---\ntitle: \"带你快速了解：限流中的漏桶和令牌桶算法\"\ndate: 2020-10-06T12:44:10+08:00\ntoc: true\nimages:\ntags: \n  - 微服务\n  - 服务治理\n---\n\n在前文 《限流熔断是什么，怎么做，不做行不行？》中针对 “限流” 动作，有提到流量控制其内部对应着两种常用的限流算法，分别是漏桶算法和令牌桶算法。因此会有的读者会好奇，这都是些啥？\n\n为了更进一步的了解 WHY，本文来快速探索一下，看看限流下的一些 “算法” 们到底有何作用，是为何成为流量控制的基石的？\n\n![image](https://image.eddycjy.com/815f7524b5367149cc9c6725ee28cd12.jpg)\n\n## 前言\n\n理论上每一个对外/内提供功能的资源点，都需要进行一定的流量控制，否则在业务的持续迭代中，是有可能出现突发性流量的场景（就像年初所带来的一些行业突发转变，导致业务流量突然暴增）：\n\n![image](https://image.eddycjy.com/c29272e53f9e125dbbfba133b3eea7c4.jpg)\n\n若没有进行限流，就会出现一些奇奇怪怪的问题点，实则就是系统无法承受这波流量，逐渐崩溃，走向系统假死。\n\n## 现实场景\n\n最常见的现实场景就是日常随处可见的排插、插座，其内置的保险丝，也被称为电流保险丝，其主要是起过载保护作用，保险丝会在电流异常升高到一定的高度和热度的时候，自身熔断切断电流，从而起到保护电路安全运行的作用。\n\n因此真实世界中有许多与软件工程中的限流熔断的场景有异曲同工之处，也是为了控制量，超量就切断。你也可以想想现实生活中是否有遇到其他类似的例子呢？\n\n![image](https://image.eddycjy.com/23af65486fc991e1bd976c37626ccf18.jpg)\n\n## 漏桶算法（Leaky Bucket）\n\n漏桶算法（Leaky Bucket）是网络中流量整形（Traffic Shaping）或速率限制（Rate Limiting）时常用的一种算法，它的主要目的是控制数据注入到网络的速率，平滑网络上的突发流量。\n\n漏桶算法通过其算法调控了流量访问，使得突发流量可以被整形，去毛刺，变成一个相对缓和，以便为网络提供一个稳定的流量。\n\n漏桶算法的存储桶主要由三个参数定义，分别是：桶的容量、水从桶中流出的速率、桶的初始充满度。\n\n其核心理念就如字面意思：一个会漏水的桶。\n\n![图片来自 geeksforgeeks](https://image.eddycjy.com/386bdd6a907a2130f5bfa74696817221.jpg)\n\n### Bursty Flow\n\n在上图中，水龙头代表着突发流量（Bursty Flow）。当网络中存在突发流量，且无任何调控时，就会出现像 Bursty Data 处类似的场景。主机以 12 Mbps 的速率发送数据，时间持续 2s，总计 24 Mbits 数据。随后主机暂停发送 5s，然后再以 2 Mbps 的速率发送数据 3s，最终总共发送了 6 Mbits 的数据。\n\n因此主机在 10s 内总共发送了 30 Mbits 的数据。但这里存在一个问题，就是数据的发送并不是平滑的，存在一个较大的波峰。若所有流量都是如此的传输方式，将会 “旱的旱死涝的涝死”，对系统并不是特别的友好。\n\n### Fixed Flow\n\n为了解决 Bursty Flow 场景的问题。漏桶（Leaky Bucket）出现了，漏桶具有固定的流出速率、固定的容量大小。\n\n在上图中，漏桶在相同的 10s 内以 3 Mbps 的速率持续发送数据来平滑流量。若水（流量）来的过猛，但水流（漏水）不够快时，其最终结果就是导致水直接溢出，呈现出来就是拒绝请求/排队等待的表现。另外当 Buckets 空时，是会出现一次性倒入达到 Bucket 容量限制的水的可能性，此时也可能会出现波峰。\n\n简单来讲就是，一个漏桶，水流进来，但漏桶只有固定的流速来流出水，若容量满即拒绝，否则将持续保持流量流出。\n\n## 令牌桶算法\n\n令牌桶算法也是网络中流量整形或速率限制时常用的一种算法，它的主要目的是控制发送到网络上的数据的数目，并允许突发数据的发送。\n\n令牌桶算法会以一个恒定的速率向桶里放入令牌，如果有新的请求进来希望进行处理，则必须要先从桶内拿到一个可用的令牌，才能继续被处理。若桶内无令牌可取时，则拒绝请求/排队等待。\n\n![图片来自 gateoverflow](https://image.eddycjy.com/07c0861b3a2e900ea4ca6fc8f7aeaa1a.jpg)\n\n1. 每 1/r 秒新增一个 token 到 buckets 中。\n\n2. buckets 中最多可容纳 b 个令牌。如果桶已满，将丢弃这个新增的 token（也就是不需要新的 token）。\n\n3. 当主机传输 n bytes packets 時，buckets 中如果有 n 个令牌，则取到所需令牌，成功传输 n bytes。\n\n4. 当可用的 token 小于 n bytes 时，不会从 buckets 中取到任何 token，本次请求将被拒绝/排队等待。\n\n## 漏桶 vs 令牌桶\n\n漏桶算法和令牌桶算法本质上都是为了做流量整形（Traffic Shaping）或速率限制（Rate Limiting），避免系统因为大流量而被打崩，但两者核心差异在于限流的方向是相反的。\n\n令牌桶限制的是流量的平均流入速率，并且允许一定程度的突然性流量，最大速率为桶的容量和生成 token 的速率。而漏桶限制的是流量的流出速率，是相对固定的。\n\n因此也会相对的带来一个问题，在某些场景中，漏桶算法并不能有效的使用网络资源，因为漏桶的漏出速率是相对固定的，所以在网络情况比较好，没有拥塞的状态下，漏桶依然是限制住的，并没有办法放开量。而令牌桶算法则不同，其能够是限制平均速率的同时支持一定程度的突发流量。\n\n## 总结\n\n在软件系统中，限流常常所代表的就是流量整形、速率限制，是一个非常常见的调控手段。一般我们会将其在初期集成到统一框架、网关、Mesh 中去。因此建议接触业务的同学，都要对这一块进行考量，便于后续的快速使用/接入，毕竟业务的流量爆发总是来的比较突然，甚至可能是恶意攻击。\n\n而本文所提到的漏桶，令牌桶都是非常常见的手段，虽然两者独立出来分析了。但从软件开发的角度来讲，你认为两者是否可以融合，结合其优势呢？"
  },
  {
    "path": "content/posts/microservice/linkage.md",
    "content": "---\ntitle: \"微服务的战争：级联故障和雪崩\"\ndate: 2020-08-25T21:08:39+08:00\ntoc: true\nimages:\ntags: \n  - 微服务\n---\n\n> “微服务的战争” 是一个关于微服务设计思考的系列题材，主要是针对在微服务化后所出现的一些矛盾/冲突点，不涉及具体某一个知识点深入。如果你有任何问题或建议，欢迎随时交流。\n\n在 [微服务的战争：统一且标准化](https://eddycjy.com/posts/microservice/standardization/) 中，经过好几周与不同业务组不同事业部的跨部门讨论后，终于把初始的标准化方案给定下来了，大家欢快的使用起了内部的统一框架，疯狂的创建起了新服务，没隔多久服务调用链就变成了下图：\n\n![image](https://image.eddycjy.com/97128d96a1a4b8ba1f765bf30134f529.jpg)\n\n服务间存在多次内部调用，服务 A =》服务 B =》服务 C =》服务D，而 服务 E =》 服务 B，服务 F =》服务 E，也就是存在着多个流量入口，且依赖相同的服务。\n\n## 背景\n\n服务与服务中，总存在业务服务，公共服务，基础服务等类型。但在某一个夜晚，突然发现 BFF 调用后端服务开始逐渐不正常，客户给你截图反馈问题，你发现有点问题：\n\n![image](https://image.eddycjy.com/844d0d5a730a4c692d7d96912e1d710c.jpg)\n\n单从表现来看，你发现是 BFF 调用服务 A 极度缓慢，也不知道怎么了...正当以为是服务 A 出问题，想着万能重启一下时。你在日志平台和链路追踪系统一看，发现了大量的错误日志和缓慢，让你略微震惊，一时间不知道从何下手。\n\n这可怎么办？\n\n## 级联故障和雪崩\n\n实际上这是一次很经典的级联故障，最终导致系统雪崩的情景再现。单从上述拓扑来看，问题点之一在于服务 B：\n\n![image](https://image.eddycjy.com/51b18e7c80833fbe1dc13b3a3290940b.jpg)\n\n服务 B 本身作为服务 A 和服务 F 的两个流量入口必经之处，想必至少是一个公共服务，但他也依赖了其他多个服务。因此若服务 C 和服务 D 其中一个有问题，在没有熔断措施的情况下，就出现级联故障，系统逐渐崩盘，最后雪崩：\n\n![image](https://image.eddycjy.com/2e7b0d94c32f3bd8f3f7168046671d2b.jpg)\n\n服务 D 所依赖的外部接口出现了故障，而他并没有做任何的控制，因此扩散到了所有调用到他的服务，自然也就包含服务 B，因此最终出现系统雪崩。\n\n这种最经典的是出现在默认 Go http client 调用没有设置 Timeout，从而只要出现一次故障，就足矣让记住这类 “坑”，毕竟崩的 ”慢“，错误日志还多。\n\n## 解决方法\n\n常见的方式是**根据特定的规则/规律进行熔断和降级**，避免请求发生堆积：\n\n- 超时时间控制。\n\n- 慢调用比例。\n\n- 错误比例。\n\n- 自适应（例如：负载情况等）。\n\n当然，这也只是壮士断腕，后续措施还包含监控告警，通知对应的开发人员来处理。且需提前对被降级的模块进行业务逻辑进行处理等等，这样才能够比较柔和且快速地度过这一次危机。\n\n## 总结\n\n在分布式应用中，级联故障和雪崩是非常常见的，一些开发同学在模块设计时可能并没有意识到这块的问题，在微服务化后会一个不留神就碰到，因为其调用链变得特别的长且多。因此建议配套设施和限流熔断措施都应该及时跟上，否则面对一大堆的错误日志还是很无奈的。\n\n同时，监控告警的建设也要做，因为在危机出现时，有一个 HTTP 调用的 P95/P99 告警出现，那就比较舒心了，直接 root cause。"
  },
  {
    "path": "content/posts/microservice/monitor-alarm.md",
    "content": "---\ntitle: \"想要4个9？本文告诉你监控告警如何做\"\ndate: 2020-09-13T18:42:17+08:00\ntoc: true\nimages:\ntags: \n  - 微服务\n  - 服务治理\n---\n\n“你说说，没有仪表盘的车，你敢开吗？”\n\n“没有仪表盘的车开在路上，你怎么知道现在是什么情况？”\n\n![image](https://image.eddycjy.com/6e2774d84ddcdb2d73918e06575a07b7.jpeg)\n\n“客户说你这车又崩了，咋知道什么时候好的？​啥时候出的问题？”\n\n## 前言\n\n将思考转换到现实的软件系统中，可想而知没有监控系统的情况下，也就是没有 ”仪表盘“ 的情况下实在是太可怕了。\n\n你的故障永远都是你的客户告诉你的，而...在什么时候发生的，你也无法确定，只能通过客户的反馈倒推时间节点，最后从错误日志中得到相对完整的日志信息。\n\n## 问题\n\n更要命的是你无法掌握主动权，错误日志有可能会有人漏记录，平均修复时间（MTTR）更不用想了，需要从 0.1 开始定位，先看 APP 是哪个模块报错，再猜测是哪个服务导致，再打开链路追踪系统，或是日志平台等。\n\n稍微复杂些的，排查来来往往基本都是半小时、一小时以上，那 4 个 9 肯定是达不到的了，以此几次 P0 几小时怕不是业务绩效也凉凉，因为故障修复的速度实在是太慢了。\n\n那归根到底，想破局怎么办，核心第一步就是要把监控告警的整个生态圈给建设好。\n\n## 监控定义\n\n常说监控监控，监控的定义就是监测和控制，检测某些事物的变化，以便于进行控制。在常见的软件系统中，大多分为三大观察类别：\n\n![image](https://image.eddycjy.com/b567a71921b7e54d703d9e47f491d6c0.jpg)\n\n- 业务逻辑：项目所对应的服务其承担的业务逻辑，通常需要对其进行度量。例如：每秒的下单数等。\n\n- 应用程序：应用程序。例如：统一的基础框架。\n\n- 硬件资源：服务器资源情况等。例如：Kubernetes 中的 Cadvisor 组件便会提供大量的资源指标。\n\n从软件系统来讲，监控的定义就是收集、处理、汇总，显示关于某个系统的实时量化数据，例如：请求的数量和类型，错误的数量和类型，以及各类调用/处理的耗时，应用服务的存活时间等。\n\n## 监控目标\n\n知道了监控的定义，了解了监控的作用和具体的实施指标后。我们需要明确的知道，做监控的目标是什么：\n\n![image](https://image.eddycjy.com/bab6275c4b7c73deec2d9f9919f967c7.jpg)\n\n从现实层面出发，做监控的初衷，就是希望能够及时的发现线上环境的各种各样奇奇怪怪的问题，为业务的正常运转保驾护航。因此整体分为上图四项：\n\n- 预测故障：故障还没出现，但存在异常。监控系统根据流量模型、数据分析、度量趋势来推算应用程序的异常趋势，推算可能出现故障的问题点。\n\n- 发现故障：故障已经出现，客户还没反馈到一线人员。监控系统根据真实的度量趋势来计算既有的告警规则，发现已经出现故障的问题点。\n\n- 定位故障：故障已经出现，需要监控系统协助快速定位问题，也就是根因定位（root cause）。此时是需要协调公司内生态圈的多个组件的，例如：链路追踪系统、日志平台、监控系统、治理平台（限流熔断等），根据监控系统所告警出来的问题作为起始锚点，对其进行有特定方向的分析，再形成 ”线索“ 报告，就可以大力的协助开发人员快速的定位问题，发现故障点。\n\n- 故障恢复：故障已经出现，但自动恢复了，又或是通过自动化自愈了。这种情况大多出现在告警规则的阈值配置的不够妥当，又或是第三方依赖恰好恢复了的场景。\n\n而更值得探讨的的是监控告警的后半段闭环，故障自愈，通过上述三点 “预测故障、发现故障、定位故障”，已经定位到故障了，就可以配合内部组件，实现自动化的 ”自愈“，减少人工介入，提高 MTTR。\n\n![image](https://image.eddycjy.com/437e9d859ee86916f5d2d0bc409ab9f5.jpg)\n\n因此做监控系统的目标很明确，就是发现问题，解决问题，最好自愈，达到愉快休假，业务安心的目的。\n\n## 4 个黄金指标\n\n有定义，有目标，那指导呢。实际上 “业务逻辑、应用程序、硬件资源” 已经成为了一个监控系统所要监控构建的首要目标，绝大部分的监控场景都可以归类进来。且针对这三大项，《Google SRE 运维解密》 也总结出了 4 个黄金指标，在业界广为流传和借鉴：\n\n- 延迟：服务处理某个请求所需要的时间。\n    - 区分成功和失败请求很重要，例如：某个由于数据库连接丢失或者其他后端问题造成的 HTTP 500 错误可能延迟很低。因此在计算整体延迟时，如果将 500 回复的延迟也计算在内，可能会产生误导性的结果。\n    - “慢” 错误要比 “快” 错误更糟糕。\n\n- 流量：使用系统中的某个高层次的指标针对系统负载需求所进行的度量。\n    - 对 Web 服务器来讲，该指标通常是每秒 HTTP 请求数量，同时可能按请求类型分类（静态请求与动态请求）。\n    - 针对音频流媒体系统来说，指标可能是网络 I/O 速率，或者并发会话数量。\n    - 针对键值对存储系统来说，指标可能是每秒交易数量，或每秒的读者操作数量。\n\n- 错误：请求失败的速率。\n    - 显式失败（例如：HTTP 500）。\n    - 隐式失败（例如：HTTP 200 回复中包含了错误内容）。\n    - 策略原因导致的失败（例如：如果要求回复在 1s 内发出，任何超过 1s 的请求就都是失败请求）。\n\n- 饱和度：服务容量有多 “满”，通常是系统中目前最为受限的某种资源的某个具体指标的度量，例如：在内存受限的系统中，即为内存；在 I/O 受限的系统中，即为 I/O。\n    - 很多系统在达到 100% 利用率之前性能会严重下降，因此可以考虑增加一个利用率目标。\n    - 延迟增加是饱和度的前导现象，99% 的请求延迟（在某一个小的时间范围内，例如一分钟）可以作为一个饱和度早期预警的指标。\n    - 饱和度需要进行预测，例如 “看起来数据库会在 4 小时内填满硬盘”。\n\n如果已经成功度量了这四个黄金指标，且在某个指标出现故障时能够发出告警（或者快要发生故障），那么在服务的监控层面来讲，基本也就满足了初步的监控诉求。\n\n也就是可以做到知道了是什么出问题，问题出在哪里，单这一步就已经提高了不少定位问题的时间效率，是一个从 0 到 1 的起步阶段。\n\n## 实践案例\n\n知道是什么（定义），为什么要做（目标），做的时候需要什么（4 个黄金指标）后，还缺乏的是一个承载这些基础应用、业务思考的平台，让架构+运维+业务共同在上面施展拳脚。\n\n公司内部至少需要有一个监控告警管理平台。\n\n### 平台搭建\n\n在目前云原生火热的情况下，Kubernetes 生态中大多惯用 Prometheus，因此 Prometheus+Grafana+AlertManger 成为了一大首选，业内占比也越来越高，其基本架构如下：\n\n![image](https://image.eddycjy.com/d6c347759dbe7eb9c14c544b6bbab468.jpg)\n\n- Prometheus Server：用于收集指标和存储时间序列数据，并提供一系列的查询和设置接口。\n- Grafana：用于展示各类趋势图，通过 PromQL 从 Prometheus 服务端查询并构建图表。\n- Alertmanager：用于处理告警事件，从 Prometheus 服务端接收到 alerts 后，会进行去重，分组，然后路由到对应的Receiver，发出报警。\n\n这块具体的基本知识学习和搭建可详见我写的 [Prometheus 系列](https://eddycjy.com/prometheus-categories/)，本文不再赘述。\n\n### 监控指标\n\n在平台搭建完毕后，常要做的第一步，那就是规划你整个系统的度量指标，结合 Google SRE 的 4 个黄金指标，可以初步划分出如下几种常用类型：\n\n- 系统层面：Kubernetes Node、Container 等指标，这块大多 Cadvisor 已采集上报，也可以安装 kube-state-metrics 加强，这样子就能够对 Kubernetes 和应用程序的运行情况有一个较好的观察和告警。\n\n- 系统层面：针对全链路上的所有基础组件（例如：MySQL、Redis 等）安装 exporter，进行采集，对相关基础组件进行监控和告警。\n\n- 业务服务：RPC 方法等的 QPS 记录。可以保证对业务服务的流量情况把控，且后续可以做预测/预警的一系列动作，面对突发性流量的自动化扩缩容有一定的参考意义。\n\n- 业务服务：RPC 方法等的错误情况。能够发现应用程序、业务的常见异常情况，但需要在状态/错误码规划合理的情况下，能够起到较大的作用，有一定困难，要在一开始就做对，否则后面很难扭转。\n\n- 应用程序：各类远程调用（例如：RPC、SQL、HTTP、Redis）的调用开销记录。最万金油的度量指标之一，能够在很多方面提供精确的定位和分析，Web 应用程序标配。常见于使用 P99/95/90。\n\n- 语言级别：内部分析记录，例如：Goroutines 数量、Panic 情况等，常常能发现一些意想不到的泄露情况和空指针调用。没有这类监控的话，很有可能一直都不会被发现。\n\n### 指标落地\n\n第一步完成了整个系统的度量指标规划后，第二步就是需要确确实实的把指标落地了。\n\n无论是统一基础框架的打点，系统组件的 exporter，大多涉及了公司级的跨多部门协作，这时候需要更多的耐心和长期主义和不断地对方向纠错，才能尝到体系建设后的果实。\n\n### 告警体系\n\n在完成监控指标和体系的建设后，告警如何做，成为了一大难题，再好的监控体系，闭环做不好，就无法发挥出很大的作用。因此我们给告警定义一些准则：\n\n1. 告警不要太多，否则会导致“狼来了”。\n\n2. 告警出现时，应当要具体操作某些事情，是亟待解决的。\n\n3. 告警出现时，应当要进行某些智力分析，不应该是机械行为。\n\n4. 不需要人工响应/处理的告警规则，应当直接删除。\n\n5. 告警出现时，你下意识要再观察观察的告警，要直接进行调整。\n\n6. 告警应当足够的简单，直观，不需要猜。\n\n简单来讲就是告警要少，事件需要解决，处理要人工介入。否则右拐自动化自愈恢复可能更香。\n\n#### 告警给谁？\n\n另外一个难题就是：\n谁诱发处理的告警，要通知给谁？\n\n这是一个很需要斟酌的问题，在告警的规范上，尽可能遵循最小原则，再逐级上报。也就是先告警给 on-call 人，若超出 X 分钟，再逐级上报到全业务组，再及其负责人，一级级跟踪，实现渐进式告警。\n\n![image](https://image.eddycjy.com/6745789c92ca3f7bc0fd86909046ce45.jpg)\n\n逐级上报，响应即跟踪，明确问题点的责任人。而逐级上报的数据来源，可通过员工管理系统来获取，在员工管理系统中有完整的上下级关系（类似 OA 审批上看到的流程节点），但如果该系统没有开放 API 之类的，那可能你只能通过其他方式来获取了。\n\n例如像是通过企业微信获取部门关系和人员列表，再手动设置上下级关联关系，也可以达到目的，且在现实世界中，有可能存在定制化的诉求。\n\n### 规范建立\n\n即使所以监控体系、指标落地、告警体系都建立起来了，也不能掉以轻心。实际上在成为事实标准后，你仍然需要尽快为告警后奔跑，将整个闭环搭建起来，也就是故障管理。\n\n与公司内部的流程管理的同学或 QA，一起设立研发底线的规范，进行细致的告警分级识别，告警后的汇总运营分析，形成一个真正意义上的故障管理规范。\n\n否则最后可能会疲于奔命，人的时间精力总是有限的，而面对整个公司的监控告警的搭建，体系上与业务组的共建，督促告警响应，极有可能最后会疲于奔命，即使真的有一定用处，在杂乱无人收敛的告警中最后流于形式。\n\n## 总结\n\n监控告警的体系生态做来有意义吗？\n\n这是必然的，成熟且规范的监控告警的体系生态是具有极大意义，可以提前发现问题，定位问题，解决问题。甚至这个问题的说不定还不需要你自己处理，做多组件的闭环后，直接实施自动化的服务自愈就可以了，安心又快快乐乐的过国庆节，是很香的。\n\n而故障管理的闭环实施后，就可以分析业务服务的告警情况，结合 CI/CD 系统等基础平台，每季度自动化分析实施运营报表，帮助业务发现更多的问题，提供其特有的价值。\n\n但，想真正做到上述所说的成熟且规范，业务共建，有难度，需要多方面认同和公司规范支撑才能最佳实现。因此共同认可，求同存异，多做用户反馈分析也非常重要。"
  },
  {
    "path": "content/posts/microservice/standardization.md",
    "content": "---\ntitle: \"微服务的战争：统一且标准化\"\ndate: 2020-08-22T21:56:14+08:00\ntoc: true\nimages:\ntags: \n  - 微服务\n---\n\n> “微服务的战争” 是一个关于微服务设计思考的系列题材，主要是针对在微服务化后所出现的一些矛盾/冲突点，不涉及具体某一个知识点深入。如果你有任何问题或建议，欢迎随时交流。\n\n![image](https://image.eddycjy.com/b70c37b40768191e5a6812096703a63f.jpg)\n\n## 开天辟地\n\n在远古开天辟地时，大单体转换成微服务化后，服务的数量越来越多。每起一个新的服务，就得把项目的目录结构，基础代码重新整理一遍，并且很有可能都是从最初的 template 上 ctrl+c，ctrl+v 复制出来的产物，如下：\n\n![image](https://image.eddycjy.com/7b4642c2bb7af1e5656be047bc269fb6.jpg)\n\n但是基于 template 的模式，很快就会遇到各种各样的新问题：\n\n![image](https://image.eddycjy.com/3c91559190480a348aeacd609199c2bb.jpg)\n\n随着跨事业部/业务组的使用增多，你根本不知道框架的 template 是什么时间节点被复制粘贴出去的，也不知道所对应的 commit-id 是什么，更不知道先前的 BUG 修复了没，也不知道有没有其他开发人员私下改过被复制走的 template。\n\n简单来讲，就是不具备可维护性，相对独立，BUG 可能一样，但却没有版本可规管。这时候，就可以选择做一个内部基础框架和对应的内部工具（已经有用户市场了），形成一个脚手架闭环：\n\n![image](https://image.eddycjy.com/3787e41616779a91a3a052467b3acdee.jpg)\n\n通过基础工具+基础接口的方式，就可以解决项目A、B、C...的基础框架版本管理和公共维护的问题，且在遇到框架 BUG 时，只需要直接 upgrade 就好了。而在框架维护者层面，还能通过注册机制知道目前基础框架的使用情况（例如：版本分布），便于后续的迭代和规划。\n\n同时若内部微服务依赖复杂，可以将脚手架直接 “升级”，再做多一层基础平台，通过 CI/CD 平台等关联创建应用，选择应用类型等基本信息，然后关联创建对应的应用模板、构建工具、网关、数据库、接口平台、初始化自动化用例等：\n\n![image](https://image.eddycjy.com/0997fb71080b8f35fe0ccd68acaebc94.jpg)\n\n至此，就可以通过结合基础平台（例如：CI/CD）实现流程上的标准化控制，成为一个提效好帮手。\n\n## 大众创新\n\n但，一切都有 “开天辟地” 那么顺利吗。实际上并不，在很多的公司中，大多数是在不同的时间阶段在不同的团队同时进行了多个开天辟地。\n\n更具现化来讲，就是在一家公司内，不同的团队里做出了多种基础工具和基础框架。更要命的是，他们几家的规范可能还不大一样。例如：框架在 gRPC 错误码的规范处理上的差异：\n\n- 业务错误码放在 grpc.status.details 中。\n\n- 业务错误码放在 grpc-status 中。\n\n- 业务错误码放在 grpc-message 中。\n\n又或是 HTTP 状态码的差异：\n\n- HTTP Status Code 为金标准，不在主体定义业务错误码。\n\n- HTTP Status Code 都为 200 OK（除宕机导致的 500，503 等），业务错误码由主体另外定义。\n\n粗略一看，单单在应用错误码/状态码这一件事情上，就能够玩出花样。而这件事又会导致各种问题，例如在监控平台上，因为不同团队所定义的状态码规范不一样，就会导致连基本的监控可用性都会有问题。\n\n像是有的小伙伴会把业务错误码放在 grpc-status 属性中，而在标准 gRPC 的规范中 grpc-status 是和 HTTP Status Code 一样有特定状态码映射的。这时候就会让监控告警系统十分难做，通用的告警规则到底是以哪份状态码为准？\n\n![image](https://image.eddycjy.com/34c3c8c97f1f6164d94fb7ce7ada8262.jpg)\n\n往往最终演进的路线与企业的组织结构有关，也就是[康威定律](https://zh.wikipedia.org/wiki/%E5%BA%B7%E5%A8%81%E5%AE%9A%E5%BE%8B)，一个系统的技术边界反映组织的结构。业界常见的是两种情况：\n\n1. A 吞并 B，B 与 A 一致，从例子上来讲就是基本公用一套（维度为公司/事业部/业务组级别，与企业情况有关）。\n\n2. A，B 均独立发展，从例子上来讲就是均独立搭建，各管各，偶尔互相触碰边界，又或是在公开分享暗中切磋。\n\n显然，这其中利与弊就要各自判断了，多少厂内部有多少个框架，也有血汗厂基本一统江湖的，可能做基础架构适配的小伙伴会比较有感触，不同框架的 Header 规范不一样，这样子即使是 Mesh 也避免不了一顿 if else。\n\n更甚的是，在类似服务发现/注册、限流熔断、基础拦截器，各类 SDK 同个厂的每个内部框架都重现实现一遍。美其名曰框架支持了这些，就允许让他上，但这样子怕是在未来又造成了新的一波技术债务。\n\n同时框架维护者，是有可能离职跳槽到别家去的，这在前端届也层出不穷，带着修炼好的真经走了，留下一个没有人维护的组内框架，这时候只能硬着头皮找 B/C 角来接受，顶上来的人指不定思想还不一样。\n\n这单从公司层面来讲，是一个巨大的伤害，长远来看着实是灾难。\n\n## 总结\n\n在本文中，主体分为了 “开天辟地” 和 “大众创新” 两块内容，理想是丰满的，而现实怕是很骨感。微服务是一把双刃剑，带来好处的同时往往也带来了反面，架构的复杂度很难预知，因此本质上需要一个基架团队不忘初心，持续发现，持续解决问题。\n\n但不论如何，及早的把主力语言、基本技术栈均基本统一起来，做好产品闭环，会是一个很好的方向。\n\n如果具体到要做的事情，需要有一个明事理的上级，清晰的意识到同个子公司内的技术体系最好是基本一致的，由基础架构组或相关领头人拉齐核心 Leader，定义基本规范，构建统一框架，融合 CI/CD 平台。\n\n![image](https://image.eddycjy.com/3b2653c1e548206f5c3893c82765ce29.jpg)\n\n当然，反之倒推也是可以的（野蛮生长），就是步骤更多了，更难了。\n\n\n"
  },
  {
    "path": "content/posts/microservice/tests.md",
    "content": "---\ntitle: \"微服务的灾难：端到端测试的痛苦\"\ndate: 2020-09-10T19:54:59+08:00\ntoc: true\nimages:\ntags: \n  - 微服务\n---\n\n大家好，我是煎鱼。\n\n小咸鱼经过前文所提到的折磨人的 “微服务拆分、微服务环境” 问题后，终于顺顺利利的上到了测试环境进行测试。\n\n这时候开发、测试同学又闹新的头疼了，测了一轮下来。发现好好的。结果发现一上生产就有一些地方有问题，发现没测到。\n\n这到底是为什么呢？\n\n## 背景\n\n在以往，小咸鱼他们团队都是传统的大单体应用。也就是一体化应用，包含了前端、后端等模块，具备天然的协调性：\n- 测试同学能够很方便的就直接测到前后端接口。\n- 测试能够直接对系统本身进行集成测试。\n\n但现在，做了微服务化（雏形）后，小咸鱼他们就翻车了，为什么呢？\n\n因为考虑到微服务，微服务就是向往单拎一个服务出来，都可以独立修改，独立发布。于是小咸鱼提交了一个迭代的几个服务变更，想着实现一把 “敏捷” 发布。\n\n结果一上线就炸了，一大堆的 BUG，光荣加班到晚上 12 点。\n\n这实质上是缺乏端到端测试的一个问题，单服务，无法明确系统正在正常运行。\n\n## 端到端测试\n\n在测试的质量保障上，我们要站在用户视角去验证这个系统，保障整体的系统可用性，而不是单纯的前端 BFF，又或是后端 Server 的某些接口能够正常运行。\n\n在定义上**端到端测试（End-to-end Test）是一种用于测试整个应用程序的流程是否符合预期的测试技术**。测试同学会模拟用户真实的使用场景，通过用户界面测试应用程序。\n\n如下图：\n\n![端到端测试](https://image.eddycjy.com/5ca5b971d8e25cf41c648401489d47b4.jpg)\n\n与小咸鱼团队那种单纯只测接口的方式不同，端到端测试是面向业务的。\n\n其目的是**验证应用程序系统整体上是否符合业务诉求**，主要通过 GUI 测试，也会有人称其为集成测试、系统测试，黑盒测试。不少公司会将这几种混在一起。\n\n实则在细节定义上各有不同：\n\n![图来自网络](https://files.mdnice.com/user/3610/820d7fbb-e3aa-4e99-8898-d69fa242887b.png)\n\n本文不是测试方向文章，因此不深究。\n\n## 问题症结\n\n那么小咸鱼他们团队主要是缺乏端到端的这类集成测试的校验。直接在迭代中，把几个微服务一改，接口跑几下，以为就是合理通过的了。\n\n真实情况：\n- 一上到生产，发现压根不是这么回事。因为多个变更结合在一起，很有可能会导致系统原有的行为发生改变。\n- 即使是你单个服务接口没违背，也不一定能保证其他在同个时段上的服务没问题。\n- 在业内执行情况来看，业务迭代的非常快，接口自动化大多比较缺乏。又或是以请外包人员的方式来做，大多是面向存量补接口。\n\n我们可以知道单纯验证接口，不走端到端类别的集成测试，是非常风骚的。设身处地的想想如下场景：\n\n有没有见过一些开发，他在本地测好接口后，一和前端集成上到测试环境。测试人员，一点一个报错，正向流程压根跑不通。测试同学苦不堪言，开发同学一下身背数十 BUG，齐齐加班。\n\n但开发同学大呼我在本地的接口测试的完全没问题。归根结底，小咸鱼团队的问题，还是因为缺乏端到端测试，缺乏齐全的接口自动化用例导致的。\n\n## 解决思路\n\n在每个迭代中，实际上每个团队都会专注于系统中所使用的所有服务中的某个服务。\n\n系统中存在的大量微服务和子系统的功能和较窄的测试空间，有可能会导致没有发现系统或服务中存在的隐患。\n\n这样测试，问题的出现，甚至是必然的。\n\n在解决思路上常见于：\n\n1. 新增预发布环境，做类似端到端测试的集成测试，确保系统集成后会是可用的。\n2. 尝试更高覆盖率的接口自动化测试，大多数公司会针对新的，做增量或存量的自动化测试用例的补全。\n3. 借助线上、线下数据在 CI/CD 时进行自动化测试，实现更全面真实的测试用例。\n\n业内基本是数种思路齐头并进，最常见的是第一种方式。最有效，但开销也是最大的，并且会导致预发布环境的一定阻塞。\n\n随后第二第三种大多都会紧接着跟进，具体程度会根据公司的软硬实力（例如：行政手段、基础设施等）不同而做的深度不同。\n\n甚至前几天听小咸鱼说，面试时还听到不少公司延伸了外包岗位专门做一块的内容。\n\n## 总结\n\n虽说这个问题并不是 “微服务” 架构所独有的。但是显然微服务化后放大了测试的深坑问题。\n\n很多公司的流程和措施都是为了保障一些东西，像小咸鱼团队这样，被网上布道师例举的优点遮蔽了双眼，后面又被迫把端到端测试加回来的不在少数。\n\n**你们的团队又是如何高效解决这个问题的呢，欢迎在评论区留言和交流**！"
  },
  {
    "path": "content/posts/microservice/tracing.md",
    "content": "---\ntitle: \"微服务的战争：选型？分布式链路追踪\"\ndate: 2020-09-10T19:53:59+08:00\ntoc: true\nimages:\ntags: \n  - 微服务\n---\n\n\n> “微服务的战争” 是一个关于微服务设计思考的系列题材，主要是针对在微服务化后所出现的一些矛盾/冲突点，不涉及具体某一个知识点深入。如果你有任何问题或建议，欢迎随时交流。\n\n## 背景\n\n在经历 [微服务的战争：级联故障和雪崩](https://eddycjy.com/posts/microservice/linkage/) 的 P0 级别事件后，你小手一摊便葛优躺了。开始进行自我复盘，想起这次排查经历，由于现在什么基础设施都还没有，因此在接收到客户反馈后，你是通过错误日志进行问题检查的。\n\n但在级联错误中，错误日志产生的实在是太多了，不同的服务不同的链路几乎都挤在一起，修复时间都主要用在了翻日志上，翻了好几页才找到了相对有效的错误信息。\n\n如果下一次在出现类似的问题，可不得了，MTTR 太久了，4 个 9 很快就会用完。这时候你想到了业界里经常被提起的一个利器，那就是 “分布式链路追踪系统”。粗略来讲，能够看到各种应用的调用依赖：\n\n![image](https://image.eddycjy.com/e233f218a90b7a00b94b7f533a98c0a2.png)\n\n其中最著名的是 [Google Dapper](https://storage.googleapis.com/pub-tools-public-publication-data/pdf/36356.pdf) 论文所介绍的 Dapper。源于 Google 为了解决可能由不同团队，不同语言，不同模块，部署在不同服务器，不同数据中心的所带来的软件复杂性（很难去分析，无法做定位），构建了一个的分布式跟踪系统：\n\n![image](https://image.eddycjy.com/64214cb247989300859b98b61a844c2e.png)\n\n自此就开启了业界在分布式链路的启发/启蒙之路，很多现在出名的分布式链路追踪系统都是基于 Google Dapper 论文发展而来，基本原理和架构都大同小异。若对此有兴趣的可具体查看 [Google Dapper](https://storage.googleapis.com/pub-tools-public-publication-data/pdf/36356.pdf)，非常有意思。\n\n![image](https://image.eddycjy.com/65bd2c9b931f057d7307dfaaa8d5c433.png)\n\n（Google Dapper 中存在跟踪树和 Span 的概念）\n\n## 选型？有哪些\n\n想做链路追踪，那必然要挑选一款开源产品作为你的分布式链路追踪系统，不大可能再造一个全新的，先实现业务目的最重要。因此在网上一搜，发现如下大量产品：\n\n- Twitter：Zipkin。\n- Uber：Jaeger。\n- Elastic Stack：Elastic APM。\n- Apache：SkyWalking（国内开源爱好者吴晟开源）。\n- Naver：Pinpoint（韩国公司开发）。\n- 阿里：鹰眼。\n- 大众点评：Cat。\n- 京东：Hydra。\n\n随手一搜就发现这类产品特别的多，并且据闻各大公司都有自己的一套内部链路追踪系统，这下你可犯了大难。他们之间都是基于 Google Dapper 演进出来的，那本质上到底有什么区别，怎么延伸出这么多的新产品？\n\n### Jaeger\n\n首先看看由 Uber 开发的 Jaeger，Jaeger 目前由 Cloud Native Computing Foundation（CNCF）托管，是 CNCF 的第七个顶级项目（于 2019 年 10 月毕业）：\n\n![image](https://image.eddycjy.com/1a672c2972602f1f154c1666c94e860a.png)\n\n- Jaeger Client：Jaeger 客户端，是 Jaeger 针对 OpenTracing API 的特定语言实现，可用于手动或通过与 OpenTracing 集成的各种现有开源框架（例如Flask，Dropwizard，gRPC等）来检测应用程序以进行分布式跟踪。\n\n- Jaeger Agent：Jaeger 客户端代理，在 UDP 端口上监听所接受的跨度并将其分批发送给 Collector。\n\n- Jaeger Collector：Jaeger 收集器，顾名思义是面向 Agent，用于收集/管理链路的追踪信息。\n\n- Jaeger Query：数据查询与前端界面展示。\n\n- Jaeger Ingester：可从 Kafka 读取数据并写入其他的存储介质（Cassandra，Elasticsearch）。\n\n在了解 Jaeger 的各组件功能后，主要关注其整体的整体架构上的数据流转：\n\n![image](https://image.eddycjy.com/3d954d769e4e21c998c31336996d1a00.jpg)\n\nJaeger 是一个很经典的架构，由客户端主动发送链路信息到 Agent，Agent 上报给 Collector，再经由队列，最终落地到存储。再由另外的可视化管理后台进行查看和分析。\n\n更具现化就是 上报 =》收集 =》存储 =》分析的标准化流程。并且你会发现 Jaeger 与 Zipkin 在架构上差不多：\n\n![image](https://image.eddycjy.com/d694663f68fc1ed6bd8a467b2e49d958.png)\n\n- Zipkin Collector：Zipkin 收集器，用于收集/管理链路的追踪信息。\n\n- Storage：Zipkin 数据存储，支持 Cassandra、ElasticSearch 和 MySQL 等第三方存储。\n\n- Zipkin Query Service：数据存储并建立索引后，用于查找和检索跟踪信息。\n\n- Web UI：数据查询与前端界面展示。\n\n从时间上来看 Jaeger 比 Zipkin 晚四年，莫非是重复造轮子。经过翻阅，可得知做 Jaeger 的主要原因是：\n> 当时将跨度发送到 Zipkin 的唯一方法是通过 Scribe，而 Zipkin 支持的唯一高性能数据存储是 Cassandra。当时 Uber 对这两种技术都没有经验，因此选择了自己构建一个后端，该后端将一些自定义组件与 Zipkin UI 结合在一起，形成了一个完整的跟踪系统。\n\n更详细可阅读 [Evolving Distributed Tracing at Uber Engineering](https://eng.uber.com/distributed-tracing/)，可以了解很多细节。\n\n### 阿里鹰眼\n\n链路追踪系统的另一代表，基于日志和流式计算去做的居多，像是阿里的鹰眼，滴滴的 traces，如下图：\n\n![image](http://5b0988e595225.cdn.sohucs.com/images/20171007/2ba764f2df1e453998ae58ac852483ee.jpeg)\n\n更具体可见[《阿里巴巴鹰眼技术解密》](https://myslide.cn/slides/696) 和 [《异构系统链路追踪——滴滴 trace 实践》](https://www.itdks.com/Home/Course/detail?id=3658) 在大会上的分享，这里就不再赘述了，推荐好奇或忧愁链路追踪落地的小伙伴们阅读。\n\n## 总结\n\n大多数在初始选型时都会选择亲和性比较强的追踪系统，就像是 Jaeger 属于 Go，Zipkin、Skywalking 是 Java 系居多，三者都完全兼容 OpenTracing，只是架构上多少有些不同，且都是基于 Google Dapper 发散，因此所支持的基本功能和查询页面优雅与否很重要。\n\n而本来就有原始的 N 个系统，如果想接入直接新的链路追踪系统，还是非常麻烦的。因为原意想接入，必然是想解决原有系统的排查/定位问题，而不单单是为了新系统，因此单从接入的角度来讲，大多不会就不会使用既有开源追踪系统（除非历史债务不大），且数据量可能极大。\n\n因此基于既有方法去改造来清洗数据再做成链路追踪的模式也挺常见的，这之中日志常常是一个比较好的下手点，也就是去清洗某某数据，形成新的分析系统，再造一个内部轮子。\n\n另外近两年基于 ServiceMesh 的 ”无” 侵入式链路追踪也广受欢迎，似乎是一个被看好的方向，其代表作之一 Istio 便是使用 CNCF 出身的 Jaeger，且 Jaeger 还兼容 Zipkin，在这点上 Jaeger 完胜。"
  },
  {
    "path": "content/posts/mq-nodus.md",
    "content": "---\ntitle: \"《漫谈 MQ》设计 MQ 的 3 个难点\"\ndate: 2021-12-31T12:54:50+08:00\ntoc: true\nimages:\ntags: \n  - mq\n---\n\n\n大家好，我是煎鱼。\n\n前段时间我们分享了漫谈 MQ 的第一期《要消息队列（MQ）有什么用？》，感觉打开了一个新的世界。\n\n但很快就有小伙伴意识到了不妙，既然 MQ 承接了多个系统，那岂不是该有的问题，他都有，又或是更甚。如下：\n\n![](https://files.mdnice.com/user/3610/45b00332-9a06-49b6-a666-675890409c94.png)\n\n今天我们就进一步讲讲，**设计 MQ 时很有可能会遇到的几个大难点**，在业内又配套用了什么解决方案去处理。\n\n## 几个难点\n\n从结论上来看，设计 MQ 这一个存在。会至少引发三大难点。堪称互联网经典的，也是面试官们最爱问的：\n- 高可用：代表系统的可用性程度，高可用性通常通过提高系统的容错能力来实现，从而减少系统宕机时间。\n- 高并发：代表通过设计保证系统能够同时并行处理很多请求，在同一个时间点，有很多用户同时访问同一系统、API、URL。\n- 高可靠：代表能够满足预计条件的一个系统或组件（例如：备份、故障处理、数据存储以及访问），比较经典的是 4 个9 等标准。\n\n### 高可用\n\n像前面评论区留言的兄弟截图表述的一样。\n\n虽然请求不直接找系统 A、B、C、D 了。但是请求都实打实的通过异步的方式打到了 MQ 上，就可以不断往 MQ 塞，变成了多个系统都在请求 MQ，可以认为压力比单系统同步调用大了不止一倍。\n\n同时 MQ 还要去做消费关系的维护，存储既有和新增的大量消息。是一个既要也要还要的典型场景。\n\n这样一来，新的一轮问题就出现了。就是要保证 MQ 的高可用，否则他轻轻松松就会被压到宕机，或是负载过高，出现一些匪夷所思的延迟。\n\n如何保证 MQ 的高可用，是一个大问题。\n\n### 高并发\n\n在高并发上的诉求上，其实是和高可用的场景是一样的。既然各业务系统都是异步的了，自然他也就不会像同步阻塞一样 “等” 你。\n\n像是我有一个朋友，他们喜欢批量清洗多租户的数据。业务程序也不怎么节制，几十、几百、上千万数据，利用 Go 语言写的，抄起 for-loop+go func 就是一把梭。刷刷刷一下子就就给打进 MQ 里。\n\n再多来几个业务系统这么干，这 MQ 并发就比较高了，单单维护就是头疼。很有可能事故背着背着，年底就 3.25 了。因为 MQ，在业务中的依赖非常重，是标准的核心基础设施。\n\n如何保证 MQ 能够承受高并发，是一个大问题。\n\n### 高可靠\n\n对 MQ 来讲，高可靠性的诉求，又分为好几个角度去理解。如下：\n- 消息要靠谱：“我” 发的消息要能够可靠的到达 MQ，MQ 要能够正确的让消费者能够接收到推送或拉取。\n- 存储要靠谱：“我” 发的消息，还在 MQ 上时要存储好，不能发到 MQ 上就因为大量数据，丢了。又或是查询很慢。\n- 处理要靠谱：发了消息，可能会出现异常。发了消息，可能网络抖动，没有接收到。\n\n上述我们列了三点 “要靠谱” 的内容。实质上，对于 MQ 来讲，其每一块领域都要保证其可靠性，否则查起问题来，真的是会非常崩溃。\n\n甚至更往上，还会对 “高性能” 会有要求，不过这一块我们就不进一步展开了。\n\n## 解决方案\n\n### 核心流程\n\n在清楚了设计 MQ 会遇到的三大难点后。我们需要先了解一下现代 MQ 的基础应用架构会是怎么样的。\n\nMQ 包含如下三类角色：\n- 生产者（Producer）：负责生产消息。\n- 消费者（Consumer）：负责消费消息。\n- 服务端（Broker）：负责存储和处理消息，是 MQ 的核心部分。由队列（Queue）延伸而来，因为功能已经不仅仅局限于队列属性了。\n\n其核心流程如下：\n\n![核心流程](https://image.eddycjy.com/7b729b7d25ab9f52205008b3ee63dae7.jpg)\n\n1. 生产者（Producer）发送消息到达服务端（Broker），服务端进行消息存储，核心逻辑处理等。\n2. 再根据先前注册消费的关系（例如：订阅），进行消息的推送或被拉取。也就是消费消息了。\n3. 在完成消费消息后再返回确认（ACK）给服务端。若出现一定时间内未收到 ACK，则会触发服务端的重试机制。\n4. 服务端确定消息处理完毕，删除消息和进行记录。\n\n### 对三高下手\n\n#### 设计高可用\n\n在高可用上，主要要针对服务端（Broker）来做。目前常见的是保证服务端可以进行水平扩展，能够做跨集群的部署。\n\n因此相应上得配套做服务的注册和发现机制，负载均衡（确保服务端压力均衡）。以此来构成 MQ 高可用的基本维持。\n\n#### 设计高并发\n\n在高并发上，服务端必然包含队列（Queue），会起到缓冲的作用。但仍然可能会出现单点流量过大。\n\n因此通常会结合像是 RocketMQ 的 Topic，Kafka 的 Partition 等做队列划分，起到分而治之的作用。\n\n#### 设计高可靠\n\n在高可靠上，主要是针对消息发送、存储消息、处理消息这三块进行展开。\n\n消息发送上，会结合 SDK 和服务端两者，发送和消费消息的确认（ACK）机制、重试机制等来实现消息的可靠性。\n\n存储消息上，常见分为：分布式缓存、分布式文件系统、数据库方案等。目前主流的话，会采取落盘的方式，也就是将消息主体追加写入到日志文件，再配合索引文件来做快速的消息查找。\n\n和 MySQL 数据库的存储模式是有一定的神似之处。\n\n## 总结\n\n在今天这篇文章中，我们面向设计 MQ 中常见的 3 大难点（其实还有更多，以后再介绍...）进行了逐一介绍和说明。同时也针对业内常见的解决方案进行了剖析。\n\n在我们了解了这些细节后，在真正应用 MQ 时，就不会感到那么的无奈。因为常常你所遇到的，消息丢失，又或是消息重试导致裂变所导致宕机。\n\n往往都来自于你所忽略的这些设计细节之中。即使对到用户端上只是几个简单的配置，你也应当理解这些知识 ：）"
  },
  {
    "path": "content/posts/prometheus/2020-05-16-metrics.md",
    "content": "---\ntitle: \"Prometheus 四大度量指标的了解和应用\"\ndate: 2020-05-16T15:08:51+08:00\ntoc: true\ntags: \n  - prometheus\n---\n\n在上一个章节中我们完成了 Prometheus 的基本概念了解和安装，由于考虑到看我博客的估计是开发向的小伙伴居多，因此没有再更深入。而今天本章节将介绍我们开发用的最多的度量指标，并结合实战对 Metrics 进行使用和细节分析。\n\n## 什么是度量指标\n\n> 来自维基百科\n\n度量是指对于一个物体或是事件的某个性质给予一个数字，使其可以和其他物体或是事件的相同性质比较。度量可以是对一物理量（如长度、尺寸或容量等）的估计或测定，也可以是其他较抽象的特质。\n\n简单来讲，也就是数据的量化，形成对应的数据指标。\n\n## Prometheus 的指标格式\n\n在 Prometheus 中，我们的指标表示格式如下：\n\n```\n<metric name>{<label name>=<label value>, ...}\n```\n\n主体为指标名称和标签组成：\n\n```\napi_http_requests_total{method=\"POST\", handler=\"/eddycjy\"}\n```\n\n## 对外提供 metrics 服务\n\n首先创建一个示例项目：\n\n```go\nfunc main() {\n    engine := gin.New()\n    engine.GET(\"/hello\", func(c *gin.Context) {\n        c.String(http.StatusOK, \"煎鱼\")\n    })\n    engine.Run(\":10001\")\n}\n```\n\n接下我们需要安装 Prometheus Client SDK，在 Go 语言中对应 [prometheus/client_golang](https://github.com/prometheus/client_golang) 库：\n\n```shell\n$ go get github.com/prometheus/client_golang\n```\n\n然后调用 `promhttp.Handler` 方法创建对应的 metrics：\n\n```go\nfunc main() {\n    ...\n    engine.GET(\"/metrics\", gin.WrapH(promhttp.Handler()))\n    engine.Run(\":10001\")\n}\n```\n\n重新启动程序，并访问 `http://127.0.0.1:10001/metrics`：\n\n```shell\n# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.\n# TYPE go_gc_duration_seconds summary\ngo_gc_duration_seconds{quantile=\"0\"} 0\ngo_gc_duration_seconds{quantile=\"0.25\"} 0\ngo_gc_duration_seconds{quantile=\"0.5\"} 0\ngo_gc_duration_seconds{quantile=\"0.75\"} 0\ngo_gc_duration_seconds{quantile=\"1\"} 0\ngo_gc_duration_seconds_sum 0\ngo_gc_duration_seconds_count 0\n# HELP go_goroutines Number of goroutines that currently exist.\n# TYPE go_goroutines gauge\ngo_goroutines 8\n# HELP go_info Information about the Go environment.\n# TYPE go_info gauge\ngo_info{version=\"go1.14.2\"} 1\n# HELP go_memstats_alloc_bytes Number of bytes allocated and still in use.\n# TYPE go_memstats_alloc_bytes gauge\ngo_memstats_alloc_bytes 2.563056e+06\n...\n```\n\n我们可以聚焦其中一个指标：\n\n```\n# HELP go_goroutines Number of goroutines that currently exist.\n# TYPE go_goroutines gauge\ngo_goroutines 8\n```\n\n你会发现其具有固定的表示格式，分别是指标的含义、指标的类型、指标的具体字段和数值。而在 `promhttp.Handler` 方法所暴露出来的 metrics 数值，虽然看似很多，但你认真看一下，可以主体为两块：\n\n1. go_memstats 开头的指标都是 [runtime.MemStats](https://golang.org/pkg/runtime/#MemStats) 的格式化数值。\n\n2. promhttp_metric 开头的指标是 HTTP 服务的状态码统计。\n\n\n## Prometheus 四大度量指标的了解和应用\n\n### Counter（计数器）\n\nCounter 类型代表一个累积的指标数据，其单调递增，只增不减。在应用场景中，像是请求次数、错误数量等等，就非常适合用 Counter 来做指标类型，另外 Counter 类型，只有在被采集端重新启动时才会归零。\n\nCounter 类型一共包含两个常规方法，如下：\n\n方法名 | 作用\n---|---\nInc | 将计数器递增 1。\nAdd(float64) | 将给定值添加到计数器，如果设置的值 < 0，则发生错误。\n\n#### 实战演练\n\nCounter 类型是单纯的累积类计数，最基础的就是在访问请求的时候进行分类统计，在上文的示例项目中继续添加代码：\n\n```go\nvar AccessCounter = prometheus.NewCounterVec(\n    prometheus.CounterOpts{\n        Name: \"api_requests_total\",\n    },\n    []string{\"method\", \"path\"},\n)\n\nfunc init() {\n    prometheus.MustRegister(AccessCounter)\n}\n\nfunc main() {\n    ...\n    engine.GET(\"/counter\", func(c *gin.Context) {\n        purl, _ := url.Parse(c.Request.RequestURI)\n        AccessCounter.With(prometheus.Labels{\n            \"method\": c.Request.Method,\n            \"path\":   purl.Path,\n        }).Add(1)\n    })\n    engine.GET(\"/metrics\", gin.WrapH(promhttp.Handler()))\n    engine.Run(\":10001\")\n}\n```\n\n这时候我们访问 `http://127.0.0.1:10001/counter`，就可以发现 metrics +1：\n\n```\n# HELP api_requests_total \n# TYPE api_requests_total counter\napi_requests_total{method=\"GET\",path=\"/counter\"} 1\n```\n\n如果希望对全部请求进行记录和统计，我们可以利用拦截器来实现，但是在添加 Labels 时需要注意一点，就是你所定义的指标 Labels 和实际写入时的 Labels 要对应，否则会造成 panic：\n\n```\n2020/05/17 11:01:06 http: panic serving 127.0.0.1:53393: inconsistent label cardinality: expected 3 label values but got 2 in prometheus.Labels{\"method\":\"GET\", \"path\":\"/hello\"}\ngoroutine 51 [running]:\nnet/http.(*conn).serve.func1(0xc0000ee000)\n        /usr/local/Cellar/go/1.14.2_1/libexec/src/net/http/server.go:1772 +0x139\npanic(0x16272a0, 0xc00009c130)\n        /usr/local/Cellar/go/1.14.2_1/libexec/src/runtime/panic.go:975 +0x3e3\ngithub.com/prometheus/client_golang/prometheus.(*CounterVec).With(0xc0001347e0, 0xc00009a4e0, 0x16ea903, 0x4)\n        /Users/eddycjy/go/pkg/mod/github.com/prometheus/client_golang@v1.6.0/prometheus/counter.go:259 +0xc2\n\n```\n\n### Gauge（仪表盘）\n\nGauge 类型代表一个可以任意变化的指标数据，其可增可减。在应用场景中，像是 Go 应用程序运行时的 Goroutine 的数量就可以用该类型来表示，因为其是浮动的数值，并非固定的，侧重于反馈当前的情况。\n\nGauge 类型一共包含六个常规方法，如下：\n\n方法名 | 作用\n---|---\nSet(float64) | 将仪表设置为任意值。\nInc() | 将仪表增加 1。\nDec() | 将仪表减少 1。\nAdd(float64) | 将给定值添加到仪表，该值如果为负数，那么将导致仪表值减少。\nSub(float64) | 从仪表中减去给定值，该值如果为负数，那么将导致仪表值增加。\nSetToCurrentTime() | 将仪表设置为当前Unix时间（以秒为单位）。\n\n#### 实战演练\n\nGauge 类型是每次都重新设置的统计类型，在系统中统计 CPU、Memory 等等时很常见，而在业务场景中，业务队列的数量也可以用 Gauge 来统计，实时观察队列数量，及时发现堆积情况：\n\n```go\nvar QueueGauge = prometheus.NewGaugeVec(\n    prometheus.GaugeOpts{\n        Name: \"queue_num_total\",\n    },\n\t[]string{\"name\"},\n)\n\nfunc init() {\n    prometheus.MustRegister(AccessCounter)\n}\n\nfunc main() {\n    ...\n    engine.GET(\"/queue\", func(c *gin.Context) {\n        num := c.Query(\"num\")\n        fnum, _ := strconv.ParseFloat(num, 32)\n        QueueGauge.With(prometheus.Labels{\"name\": \"queue_eddycjy\"}).Set(fnum)\n    })\n    engine.GET(\"/metrics\", gin.WrapH(promhttp.Handler()))\n    engine.Run(\":10001\")\n}\n```\n\n访问 `http://127.0.0.1:10001/queue?num=5` 后，再查看 metrics 结果：\n\n```\n# HELP queue_num_total \n# TYPE queue_num_total gauge\nqueue_num_total{name=\"queue_eddycjy\"} 5\n```\n\n另外 Gauge 类型也支持各种增减方法，大家根据实际情况调用即可。 \n\n### Histogram（累积直方图）\n\nHistogram 类型将会在一段时间范围内对数据进行采样（通常是请求持续时间或响应大小等等），并将其计入可配置的存储桶（bucket）中，后续可通过指定区间筛选样本，也可以统计样本总数。\n\n简单来讲，也就是在配置 Histogram 类型时，我们会设置分组区间，例如要分析请求的响应时间，我们可以分为 0-100ms，100-500ms，500-1000ms 等等区间段，那么在 metrics 的上报接口中，将会分为多个维度显示统计情况。\n\nHistogram 类型一共包含一个常规方法，如下：\n\n方法名 | 作用\n---|---\nObserve(float64) | 将一个观察值添加到直方图。\n\n#### 实战演练\n\nHistogram 类型在应用场景中非常的常用，因为其代表的就是分组区间的统计，而在分布式场景盛行的现在，链路追踪系统是必不可少的，那么针对不同的链路的分析统计就非常的有必要，例如像是对 RPC、SQL、HTTP、Redis 的 P90、P95、P99 进行计算统计，并且更进一步的做告警，就能够及时的发现应用链路缓慢，进而发现和减少第三方系统的影响。\n\n我们模仿记录 HTTP 调用响应时间的应用场景：\n\n```go\nvar HttpDurationsHistogram = prometheus.NewHistogramVec(\n    prometheus.HistogramOpts{\n        Name:    \"http_durations_histogram_seconds\",\n        Buckets: []float64{0.2, 0.5, 1, 2, 5, 10, 30},\n    },\n    []string{\"path\"},\n)\n\nfunc init() {\n    prometheus.MustRegister(HttpDurationsHistogram)\n}\n\nfunc main() {\n\t...\n    engine.GET(\"/histogram\", func(c *gin.Context) {\n        purl, _ := url.Parse(c.Request.RequestURI)\n        HttpDurationsHistogram.With(prometheus.Labels{\"path\": purl.Path}).Observe(float64(rand.Intn(30)))\n    })\n    engine.GET(\"/metrics\", gin.WrapH(promhttp.Handler()))\n    engine.Run(\":10001\")\n}\n```\n\n多次调用 `http://127.0.0.1:10001/histogram`，查看 metrics：\n\n```\n# HELP http_durations_histogram_seconds \n# TYPE http_durations_histogram_seconds histogram\nhttp_durations_histogram_seconds_bucket{path=\"/histogram\",le=\"0.2\"} 1\nhttp_durations_histogram_seconds_bucket{path=\"/histogram\",le=\"0.5\"} 1\nhttp_durations_histogram_seconds_bucket{path=\"/histogram\",le=\"1\"} 3\nhttp_durations_histogram_seconds_bucket{path=\"/histogram\",le=\"2\"} 3\nhttp_durations_histogram_seconds_bucket{path=\"/histogram\",le=\"5\"} 3\nhttp_durations_histogram_seconds_bucket{path=\"/histogram\",le=\"10\"} 3\nhttp_durations_histogram_seconds_bucket{path=\"/histogram\",le=\"30\"} 13\nhttp_durations_histogram_seconds_bucket{path=\"/histogram\",le=\"+Inf\"} 13\nhttp_durations_histogram_seconds_sum{path=\"/histogram\"} 191\nhttp_durations_histogram_seconds_count{path=\"/histogram\"} 13\n```\n\n我们结合 histogram metrics 的结果来看，可以发现其分为了三个部分：\n\n1. http_durations_histogram_seconds_bucket：在 Buckets 中你可以发现一共包含 8 个值，分别代表：0-0.2s、0.2-0.5s、0.5-1s、1-2s、2-5s、5-10s、10-30s 以及大于 30s（+Inf），这是我们在 `HistogramOpts.Buckets` 中所定义的区间值。\n\n2. http_durations_histogram_seconds_sum：调用的总耗时。\n\n3. http_durations_histogram_seconds_count：调用总次数。\n\nHistogram 是一个比较精巧类型，首先 Buckets 的分布区间要根据你的实际应用情况，合理的设置，否则就会出现不均，自然而然 PXX（P95、P99 等）计算也就会有问题，同时在 Grafana 上的绘图也会出现偏差，因此需要在理论上多多理解，然后再进行具体的设置，否则后期改来改去会比较麻烦\n\n同时我们也可以利用 http_durations_histogram_seconds_sum 和 http_durations_histogram_seconds_count 相除得出平均耗时，一举多得。\n\n### Summary（摘要）\n\nSummary 类型将会在一段时间范围内对数据进行采样，但是与 Histogram 类型不同的是 Summary 类型将会存储分位数（在客户端进行计算），而不像 Histogram 类型，根据所设置的区间情况统计存储。 \n\nSummary 类型在采样计算后，一共提供三种摘要指标，如下：\n\n- 样本值的分位数分布情况。\n- 所有样本值的大小总和。\n- 样本总数。\n\nSummary 类型一共包含一个常规方法，如下：\n\n方法名 | 作用\n---|---\nObserve(float64) | 将一个观察值添加到摘要。\n\n#### 实战演练\n\nSummary 类型主要是\n\n```go\nvar HttpDurations = prometheus.NewSummaryVec(\n    prometheus.SummaryOpts{\n        Name:       \"http_durations_seconds\",\n        Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},\n    },\n    []string{\"path\"},\n)\n\nfunc init() {\n    prometheus.MustRegister(HttpDurations)\n}\n\nfunc main() {\n    ...\n    engine.GET(\"/summary\", func(c *gin.Context) {\n        purl, _ := url.Parse(c.Request.RequestURI)\n        HttpDurations.With(prometheus.Labels{\"path\": purl.Path}).Observe(float64(rand.Intn(30)))\n    })\n    engine.GET(\"/metrics\", gin.WrapH(promhttp.Handler()))\n    engine.Run(\":10001\")\n}\n```\n\n多次调用 `http://127.0.0.1:10001/summary`，查看 metrics：\n\n```\n# HELP http_durations_seconds \n# TYPE http_durations_seconds summary\nhttp_durations_seconds{path=\"/summary\",quantile=\"0.5\"} 17\nhttp_durations_seconds{path=\"/summary\",quantile=\"0.9\"} 29\nhttp_durations_seconds{path=\"/summary\",quantile=\"0.99\"} 29\nhttp_durations_seconds_sum{path=\"/summary\"} 85\nhttp_durations_seconds_count{path=\"/summary\"} 5\n```\n\n结合 summary metrics 来看，同样分为了三个部分：\n\n1. http_durations_seconds：分别是中位数（0.5），9 分位数（0.9）以及 99 分位数（0.99），对应 `SummaryOpts.Objectives` 中我们所定义的中位数，而各自的意义代表着中位数（0.5）的耗时为 17s，9 分位数为 29s，99 分位数为 29s。\n\n2. http_durations_seconds_sum：调用总耗时。\n\n3. http_durations_seconds_count：调用总次数。\n\n## 小结\n\n在本章节中我们介绍并实操了 Prometheus 的四种度量指标类型 Counter、Gauge、Histogram、Summary，这四种度量类型都极具代表性：Counter 是单调递增的计数器，Gauge 是可任意调整数值的仪表盘，Histogram 是分组区间统计，Summary 是中位数统计。\n\n其中 Histogram 和 Summary 具有一定的 “相似” 度，因为在 Histogram 指标中我们可以通过 `histogram_quantile` 函数计算出分位值，而 Summary 也可以计算分位值，两者区别就在于 Histogram 是在服务端计算的，而 Summary 是在客户端就进行了计算，其一个计算好了再推上去，一个直接推上去，数据维度不一样，可以做的事情也不一样，有利有弊，具体可以根据指标的实际情况做衡量。\n\n\n另外针对度量指标的命名，这是一个非常多人问的问题，因为命名是一个难题，在这里大家可以参照官方的[文档](https://prometheus.io/docs/practices/naming/)建议去针对指标命名就可以了。\n\n\n"
  },
  {
    "path": "content/posts/prometheus/2020-05-16-pull.md",
    "content": "---\ntitle: \"使用 Prometheus 对 Go 程序进行指标采集\"\ndate: 2020-05-17T17:52:37+08:00\n---\n\n在前面的章节中，已经知道了如何对应用程序进行 Prometheus metrics 的注册和暴露，那么接下来如何让 Prometheus 对应用程序进行采集呢。\n\n## 设置采集配置\n\n首先打开先前所安装的 prometheus 软件目录：\n\n```sh\n$ ls\nLICENSE                data                   promtool\nNOTICE                 prometheus             rules\nconsole_libraries      prometheus.yml         tsdb\nconsoles               prometheus.yml.default\n```\n\n打开并修改 prometheus.yml 文件，查看到 [scrape_configs](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config) 配置选项，进行如下调整：\n\n```sh\n...\nscrape_configs:\n  - job_name: 'test01'\n    static_configs:\n    - targets: ['127.0.0.1:10001']\n    scheme: http\n    tls_config:\n        insecure_skip_verify: false\n```\n\n先前所启动的应用程序是在本地，且端口号为 10001，协议为 http，对应的配置简述：\n\n- job_name：采集的任务名。\n\n- static_configs.targets：设置要采集的目标对象列表。\n\n- scheme：采集的目标协议（例如：http、https）。\n\n- tls_config.insecure_skip_verify：是否跳过证书校验。\n\n常用的配置项如上几个，其默认的 metrics path 为 `/metrics`，若需要调整则增加 metrics_path 配置项进行调整就可以了，接着重新启动 prometheus 就可以了。\n\n## 其他配置项\n\n再返回来看看 prometheus.yaml 文件：\n\n```\nglobal:\n  scrape_interval:     15s \n  evaluation_interval: 15s\n\n\nalerting:\n  alertmanagers:\n  - static_configs:\n    - targets:\n       - localhost:9093\n\nrule_files:\n   - ../rules/*.yaml\n\nscrape_configs:\n  - job_name: 'test01'\n    static_configs:\n    - targets: ['127.0.0.1:10001']\n    scheme: http\n    tls_config:\n        insecure_skip_verify: false\n```\n\n- scrape_interval：采集间隔（频率）。\n\n- evaluation_interval：规则计算间隔（频率）。\n\n- alerting.alertmanagers：此 prometheus 所计算的 rules 所对应的 alertmanager。\n\n- rule_files：此 prometheus 所对应的告警规则（rules）。\n\n这里聚焦 rule_files 和 alertmanagers，其分别对应着后续章节所要学的告警规则编写和 alertmanager 的告警平台对接和使用，留个心眼即可。\n\n## 重新启动 Prometheus\n\n在启动 prometheus 时，可以根据自己的情况进行一些启动项的调整：\n\n```\n$ ./prometheus --web.listen-address=\"0.0.0.0:9091\" --config.file=\"prometheus.yml\" \n```\n\n在上述语句中，`--web.listen-address` 调整启动的监听 HOST+PORT，`--config.file` 调整 prometheus 启动时所读取的配置文件地址，更多的可以执行 `./prometheus --help` 命令进行查看。\n\n## 查看采集情况\n\n在完成配置后，在经过 scrape_interval 秒后，将会自动采集到 prometheus 上，就可以在 Console 上进行查看：\n\n![image](https://image.eddycjy.com/958dd2950b64c2a10a73b03864b0e153.jpg)\n\n若想要简单查看趋势，也可以通过 Graph 进行趋势分析：\n\n![image](https://image.eddycjy.com/13e83b57dde665ffbbea8456efd0b146.jpg)\n\n但长久来看，还是建议使用 Grafana（后续会进行讲解）。\n\n## 总结\n\n至此就完成了一个 Prometheus 对 Go 应用程序的基本采集，本章节主要是熟悉 Prometheus 拉模式的采集机制，以及对应的 Promtheus 规则编写和使用即可，虽然比较很简单，但是这块内容的知识体系的了解程度是后面所有内容的基石，建议根据[官方文档](https://prometheus.io/docs/introduction/overview/)再进一步的学习。\n\n\n"
  },
  {
    "path": "content/posts/prometheus/2020-05-16-startup.md",
    "content": "---\ntitle: \"Prometheus 快速入门\"\ndate: 2020-05-16T12:05:58+08:00\ntoc: true\ntags: \n  - prometheus\n---\n\n一般我们说 Prometheus，有两种理解，我们平时需要注意识别的，其含义有两种，一是指的 Prometheus 自身，是一个时序数据库；另外一种是指 Prometheus 生态圈，指的是是整体的监控报警的生态圈和解决方案（Prometheus+Grafana+Alertmanager）。\n\nPrometheus 在 2016年加入了 CNCF（Cloud Native Computing Foundation），是继 Kubernetes 之后的第二个托管项目，目前已经毕业，其主要的特点如下：\n\n- 多维度的数据模型：由指标名称和键/值对标签标识的时间序列数据来组成多维的数据模型。\n- 灵活的查询语言：在 Prometheus 中使用强大的查询语言 PromSQL 来进行查询。\n- 不依赖分布式存储，Prometheus 单个节点也可以直接工作，支持本地存储（TSDB）和远程存储的模式。\n- 服务端采集数据：Prometheus 基于 HTTP pull 方式去对不同的端采集时间序列数据。\n- 客户端主动推送：支持通过 PushGateway 组件主动推送时间序列数据。\n\n## Prometheus 生态组件\n\nPrometheus 生态由多个组件共同组成，其中许多组件是可根据实际情况选择的，并且绝大部分由 Go 语言编写，在部署和构建上比较方便，如下：\n\n- Prometheus Server：[Prometheus 服务端](https://github.com/prometheus/prometheus)，用于收集指标和存储时间序列数据，并提供一系列的查询和设置接口。\n- Client Libraries：[客户端库](https://prometheus.io/docs/instrumenting/clientlibs/)，用于帮助需要监控采集的服务暴露 metrics handler 给 Prometheus server。例如像 [例子](https://github.com/prometheus/client_golang/blob/master/examples/simple/main.go) 中直接调用 promhttp 暴露了一个 metrics 接口。\n- Push Gateway：推送网关，Prometheus 服务端仅支持 HTTP pull 的采集方式，而有一些指存在的时间短，Prometheus 来 pull 前就结束了。又或是该类指标，就是要客户端自行上报的，这时候就可以采用 Push Gateway 的方式，客户端将指标 push 到 Push Gateway，再由 Prometheus Server 从 Pushgateway 上 pull。\n- Exporters：用于暴露已有的第三方服务（HAProxy，StatsD，Graphite）的 metrics 给 Prometheus Server。\n- Alertmanager：用于处理告警，从 Prometheus server 端接收到 alerts 后，会进行去重，分组，然后路由到对应的Receiver，发出报警。\n- Support Tools：各种支持工具。\n\n## Prometheus 整体流程图\n\nPrometheus 的整体架构和生态组件组成，如下图所示：\n\n![image](https://prometheus.io/assets/architecture.png)\n\nPrometheus Server 通过从监控目标中或者间接通过推送网关来拉取监控指标，它在本地存储所有抓取到的样本数据，并对此数据执行一系列规则，以汇总和记录现有数据的新时间序列或生成告警。可以通过 Grafana 或者其他工具来实现监控数据的可视化。\n\n## Prometheus 采集的数据存到哪里\n\nPrometheus 所有采集的指标数据在默认的情况下，都保存在本地所内置的时间序列数据库（TSDB）当中，如果需要另外调整，再将 Prometheus 的存储指向改为远程存储即可。\n\nPrometheus 采用在默认情况下使用本地存储，能够一定的便利性，例如：开箱即用、运维方便（不用管）等等。但是也有不少缺点，像是海量数据无法持久化等等问题，因此强烈建议在上到企业级的海量应用时，一定对其进行研讨，适当考虑远程存储。\n\n### 时序数据库是什么\n\nPrometheus 是时序数据库（Time Series Database），又简称 TSDB。目前在行业中比较出名，流行度较高的时序数据库如下：\n\n![image](https://image.eddycjy.com/899042452628900ef32fe11f8d7a4b1e.jpg)\n\n时序数据库简单来讲，就是将数据按照时间顺序排列，它具有唯一性和可排序性，因此在 Prometheus 的 Metrics 中即使只添加了一个新标签，也会造成破坏，也就是它不再是原本的那个时序数据了。\n\n注：时序数据库是一个比较大的话题，后续会单独开一篇文章讲解，此处仅概要。\n\n### Prometheus 远程存储的方案\n\n- AppOptics: write\n- Azure Data Explorer: read and write\n- Chronix: write\n- Cortex: read and write\n- CrateDB: read and write\n- Elasticsearch: write\n- Gnocchi: write\n- Graphite: write\n- InfluxDB: read and write\n- IRONdb: read and write\n- Kafka: write\n- M3DB: read and write\n- OpenTSDB: write\n- PostgreSQL/TimescaleDB: read and write\n- QuasarDB: read and write\n- SignalFx: write\n- Splunk: read and write\n- TiKV: read and write\n- Thanos: write\n- VictoriaMetrics: write\n- Wavefront: write\n\n### 数据持久化的意义\n\n- 数据存储与服务提供应当隔离。\n- 事故、问题的度量指标回溯。\n- 数据挖掘的考虑。\n- 提供给外部的自定义平台进行数据查询等等\n- ...\n\n\n## 安装 Prometheus\n\n[Prometheus](https://github.com/prometheus/prometheus) 是由 Go 语言编写的，因此安装非常的方便，只需要在 [DOWNLOAD](https://prometheus.io/download/) 中下载对应的 tar.gz 文件。进行如下解压：\n\n```\n$ tar xvfz prometheus-*.tar.gz\n```\n\n就可以看到下述目录：\n\n```\nprometheus\n├── LICENSE\n├── NOTICE\n├── console_libraries\n├── consoles\n├── data\n├── prometheus\n├── prometheus.yml\n├── prometheus.yml.default\n├── promtool\n├── rules\n└── tsdb\n```\n\n启动 Prometheus：\n\n```\n$ ./prometheus \n...\nlevel=info ts=2020-05-16T07:33:34.138Z caller=main.go:661 msg=\"Starting TSDB ...\"\nlevel=info ts=2020-05-16T07:33:34.139Z caller=web.go:508 component=web msg=\"Start listening for connections\" address=0.0.0.0:9090\n```\n\n默认监听 9090 端口：\n\n![image](https://image.eddycjy.com/f39b6b6f1c195973285d2bfd690425f3.jpg)\n\n至此我们就完成了 Prometheus 的基本启动了。\n\n## 小结\n\n在本章节中，我们快速了解 Prometheus 的基本概念和整体的生态概要，在下一章节起我们将进一步的实操，知行合一。\n\n## 参考资料\n\n- https://ryanyang.gitbook.io/prometheus/di-yi-zhang-jie-shao/overview\n- https://www.cnblogs.com/xiangsikai/p/11288632.html\n- https://www.cnblogs.com/charlieroro/p/8670959.html"
  },
  {
    "path": "content/posts/reading/2020-04-24-book.md",
    "content": "---\ntitle: \"2020年下半年：读书清单\"\ndate: 2020-04-24T22:00:45+08:00\ntoc: false\nimages:\ntags: \n  - 读书清单\n---\n\n2020 年的上半年因为一些事情耽搁了整体的进程，不过最近也快要到 Deadline 了，因此又愉快的能空出手了。\n\n回顾这几年在个人技能上，我随着公司的大泥球应用再到微服务化的飞速发展，经历了很多，突破了更多，有了不少新的感悟。因此今年打算再修修内功，认识一下自己的弱小，所以读书清单会比较偏基础方向，如下：\n\n## 必看\n\n1. 《图解 TCP/IP》\n2. 《TCP/IP 详解 卷1：协议》\n3. 《深入理解计算机系统》\n4. 《大话数据结构》\n5. 《算法（第4版）》\n6. 《剑指 Offer》\n7. [《操作系统_清华大学》](https://www.bilibili.com/video/BV1js411b7vg)\n\n## 可选\n\n1. 《Unix 环境高级编程（第3版）》\n2. 《SRE Google 运维解密》：进行中，已经了看了不少。\n3. 《Google 工作法》：进行中，已经了看了不少。\n\n所有的目标都需要有一个验收的标准和奖罚，否则所谓的目标意义不会特别大。我想，**验收标准就是 [《重读 CS》](https://github.com/eddycjy/reread-cs-notes) 的基本成型**，其它不在该集合类的属于第二梯队，考虑释出读书笔记。\n\n惩罚的话，发红包还是送书呢，先想想。\n"
  },
  {
    "path": "content/posts/reading/documentary-of-go.md",
    "content": "---\ntitle: \"Go: A Documentary 发布！\"\ndate: 2020-09-11T20:46:38+08:00\ntoc: false\nimages:\ntags: \n  - untagged\n---\n\n以前经常有读者问我，哪儿可以找到 Go 语言的前世今生，这种时候我们往往会告诉他去看 issues 和 proposals。但资料有点分散，且没有索引体系。因此不少人新入门的读者读着读着就跑偏了，又或是在第一步找资料上就被拦住了。\n\n最近欧神（@changkun）低调的发布了 《Go: A Documentary》，这个文档收集了 Go 开发过程中许多有趣（公开可见的）的问题，讨论，提案，CL 和演讲，其目的是为 Go 历史提供全面的参考。\n\n个人认为这份资料非常的有价值，相当于欧神把资料索引整理好了，强烈推荐对 Go 语言感兴趣的读者进行阅读：\n\n![image](https://image.eddycjy.com/0643ae29cdb8fb99d5a83bf67b443a9a.jpg)\n\n内容索引主要分为：\n\n- Sources\n- Committers\n    - Core Authors\n    - Compiler/Runtime Team\n    - Library/Tools/Security/Community\n    - Group Interviews\n- Timeline\n- Language Design\n    - Misc\n    - Slice\n    - Package Management (1.4, 1.5, 1.7)\n    - Type alias (1.9)\n    - Defer (1.13)\n    - Error values (1.13)\n    - Channel/Select\n    - Generics\n- Compiler Toolchain\n    - Compiler\n    - Linker\n    - Debugger\n    - Tracer\n    - Builder\n    - Modules\n    - gopls\n    - Testing\n- Runtime Core\n    - Statistics\n    - Scheduler\n    - Execution Stack\n    - Memory Allocator\n    - Garbage Collector\n    - Memory model\n    - ABI\n- Standard Library\n    - syscall\n    - io\n    - go/*\n    - sync\n    - Pool\n    - Mutex\n    - atomic\n    - time\n    - context\n    - encoding\n    - image, x/image\n    - misc\n- Unclassified But Relevant Links\n- Fun Facts\n- Acknowledgements\n\n《Go: A Documentary》 的访问地址是 `https://golang.design/history/`，GitHub 仓库地址：`https://github.com/golang-design/history`，大家也可以通过 “阅读原文” 进入。\n"
  },
  {
    "path": "content/posts/reading/programmer-accom-base.md",
    "content": "---\ntitle: \"必知必会！计算机里一些基本又重要的概念\"\ndate: 2020-10-17T00:25:59+08:00\ntoc: true\nimages:\ntags: \n  - 程序员的自我修养\n---\n\n最近在翻阅文章时，看到全成推荐的《程序员的自我修养》，这是一本讲链接、装载与库的计算机图书，看了下目录后觉得挺有意思。\n\n因此决定每读一章就将其读书笔记整理记录下来，分享给大家。\n\n目录：\n\n![image](https://image.eddycjy.com/bf202dccad8c3f8efb3726854b72e850.jpg)\n\n## 不要让 CPU 打盹\n\n在计算机发展早期，CPU 资源十分昂贵。如果一个 CPU 只能运行一个程序，那么当程序在读写磁盘（进行 I/O 操作）时，CPU 就空闲下来了。这在当时简直就是巨大的浪费。\n\n![image](https://image.eddycjy.com/df419c04c95fc7d58cc6f6f34a2fb7fd.jpeg)\n\nCPU 只能和一个程序A “聊天“，其他来再多的程序BCD，都没有任何操作的空间。就像早年的手机，打电话和上网（语音/数据）只能二选一，作为 CPU 的你，并不能多线程操作。\n\n因此机智的人们很快就编写了一些监控程序，希望来解决这个问题。\n\n### 多道程序（Multiprogramming）\n\n多道程序起，操作系统正式具有同时运行多个程序的能力。\n\n其是让 CPU 一次读取多个程序放入内存中。当某个程序暂时无须使用 CPU 时，监控程序就把另外的正在等待 CPU 资源的程序启动，以此使得 CPU 能够充分地利用起来。这种策略的确大大的提高了 CPU 资源的利用率。\n\n#### 真实场景\n\n你在 Windows 上点击鼠标 10 分钟以后系统才有反应，那是多么无奈的事情。因为没有优先级区分，自然一路排下来也就不知道要等到什么时候了，相当于半饿死。\n\n#### 存在的问题\n\n核心问题在于程序之间的调度策略太粗糙。对于多道程序来删，程序之间部分轻重缓急，也就是说不存在优先级的区分。因此如果有些程序急需使用 CPU 来完成一些任务，那么很有可能会很长时间后才有机会被分配到 CPU，才得以继续往下运行。\n\n### 分时系统（Time-Sharing System）\n\n程序运行模式改为协作的模式，在原有的多道程序继续升级改造，即每个程序运行一段时间以后都主动让出 CPU 给其他程序，使得一段时间内每个程序都有机会运行一小段。\n\n#### 真实场景\n\n比如你点击一下鼠标或按下一个键盘按键后，他会相较前者能够更快的得到响应，因为他好歹是存在切换的可能性。\n\n#### 存在问题\n\n这时候的监控程序已经比原有多道程序的模式已经复杂了不少，完整的操作系统雏形已经基本形成，很早期的 Windows（Windows 95 和 Windows NT 之前），MacOS X 之前的 MacOS 版本都是采用这种分时系统的方式来进行程序调度。\n\n其仍然存在问题，核心在于若一个程序一直在进行一个耗时计算，便会一直霸占着 CPU 不放，那么操作系统也没有不放，就会导致其他程序都只能无限等待，相当于就是系统假死了。\n\n### 多任务系统（Multi-tasking）\n\n#### 背景\n\n在分时系统中，一个程序死循环就会导致系统假死，并且其运行效率并不高，只能解决当时的交互式环境。\n\n放在现在来讲，已经完全没法很好的运行。因此当时业界也在研究更为先进的操作系统模式，也就是现在最为流行也是最熟悉的多任务系统。\n\n#### 解决方案\n\n在多任务系统中，所有的应用程序都以进行（Process）的方式运行，其有以下特点：\n\n- 每个进程都有自己独立的地址空间，因此各进程之间相互隔离。\n- 每个 CPU 都由操作系统统一进行分配。\n- 每个进程根据其优先级的高低都有机会得到 CPU。\n\n但需要注意的是，若是进程运行超出了指定的时间，操作系统就会暂停该进程，将 CPU 资源分配给其他等待运行的进程。这种 CPU 的分配方式一般称作抢占式（Preemptive）。\n\n通过这种方式，操作系统就可以强制剥夺 CPU 资源并且分配给它认为目前最需要资源的进程，如果分配给每个进程的时间都很短，即 CPU 在多个进程间快速切换，就可以造成多个进程同时在运行的假象。\n\n## 内存不够用怎么办\n\n在早期的计算机中，程序是直接运行在物理内存上的，访问的内存地址都是物理地址。假设只是一个进程在跑，可能内存资源还够用，但实际上为了更有效地利用硬件资源，我们必须运行多个程序，CPU 的利用率才会比较高。这时候就会遇到一个严重的问题，那就是如何将计算机上有限的物理内存分配给多个程序使用？\n\n![image](https://image.eddycjy.com/e029f8694bf3d0f79b9fe3e30451f141.jpg)\n\n就像上图，每个程序他都想申请 1GB 的内容，而计算机本身只有 1GB 的物理内存，根本没有办法真正的执行。\n\n### 真实场景和问题\n\n可能会有小伙伴想，煎鱼你举的例子太极端了，我们举个 ”正常“ 点的例子。假设计算机有 128MB 内存，程序 A 运行需要 10MB，程序 B 需要 100MB，程序 C 需要 20 MB。假设该几个程序运行时，我们按照其想要的一分配，不就好了吗？\n\n但现实并不是这样，这种简单的内存分配策略存在许多的问题：\n\n- 地址空间不隔离：所有程序都直接访问物理地址，各程序所使用的内存空间并不是相互独立，很容易改写到其他程序的内存地址。\n\n- 内存使用效率低：没有有效的内存管理机制，一会运行程序 A，一会运行程序 B，就需要经常要将大量数据换出换入，效率十分低下。\n\n- 程序运行的地址不确定：每次程序运行时，都需要从内存中给其分配一块足够大的空闲区域，但这些内存区域位置是不确定的，给程序编写造成了一定的麻烦。\n\n### 解决方法\n\n解决上述问题的解决思路，就是万能的法宝：增加中间层，即使用一种间接的地址访问方法。把程序给出的地址看作是一种虚拟地址（Virtual Address），然后通过某些映射的方法，将这个虚拟地址最终转换成实际的物理地址。\n\n上述提到了两个非常重要的内存概念：\n\n- 物理地址空间：是实实在在存在的，存在于计算机中，且对每一台计算机来说只有唯一的一个，你可以将其想象为物理内存。\n\n- 虚拟地址空间：是指虚拟的、人们想象出来的地址空间。其实它并不存在，每个进程都有自己的独立虚拟空间，且每个进程只能访问自己的地址空间。\n\n如此一来，操作系统只需要控制虚拟地址到物理地址的映射过程，就可以保证任意一个程序锁你访问的物理内存区域和另外一个程序不重叠，以达到地址空间隔离的效果。\n\n![进程虚拟空间、物理空间和磁盘之间的页映射关系](https://image.eddycjy.com/fb70d293e18832881e96330c2388614f.jpg)\n\n另外需要清楚虚拟存储的实现需要依靠硬件的支持，对于不同的 CPU 来说不同。但大多采用 MMU（Memory Management Unit）的部件来进行页映射：\n\n![虚拟地址到物理地址的转变](https://image.eddycjy.com/d1be0cd17fc6b36c54c9d33c95baae92.jpg)\n\nCPU 发出的是虚拟地址（Virtual Address），也就是日常程序中所看到的是虚拟地址。经过 MMU 转换后就会变成物理地址（Physical Address）。\n\n目前常见的 MMU 均已集成在 CPU 内部了，不会再以独立部件存在。\n\n## 线程的那些事\n\n线程（Thread），有时候被称为轻量级进程，是程序执行流程的最小单元。一个标准的线程由线程 ID、当前指令指针（PC）、寄存器集合和堆栈组成。\n\n通常一个进程由一个到多个线程组成，各个线程之间共享程序的内存空间（包括代码段、数据段、堆等）及一些进程级的资源（如打开文件和信号）。\n\n![image](https://image.eddycjy.com/1b00bbf5766fd7fb6f2c3fa68bdf7b38.jpg)\n\n### 为什么需要多线程\n\n- 某个操作可能会陷入长时间等待，等待的线程会进入睡眠状态，无法继续执行。多线程执行可以有效利用等待的时间。典型的例子是等待网络响应，这时候就可以切换了。\n\n- 某个操作会消耗大量的时间，如果只有一个线程，程序和用户之间的交互会中断。多线程的情况下，可以让一个线程负责交互，另外一个线程负责计算。\n\n- 程序逻辑本身就要求并发操作，例如一个多端下载软件。\n\n- 多 CPU 或多核计算机，其本身就具备同时执行多个线程的能力。\n\n- 相对于多进程应用，多线程在数据共享方面效率会高很多。\n\n### 线程的访问权限\n\n线程可以访问进程内存里的所有数据，甚至在知道堆栈地址的情况下，可以访问其他线程里的堆栈信息。其私有存储空间主要分为：栈、线程局部存储（Thread Local Storage，TLS）、寄存器（包括 PC 寄存器）。\n\n### 线程调度和时间片\n\n在单处理器对应多线程的情况下，并发是一种模拟处理的状态。操作系统会让这些多线程程序轮流执行，每次仅执行一小段时间（通常是几十到几百毫秒），这样子线程就 “看起来” 在同时执行。\n\n不断在处理器上切换不同的线程行为称之为线程调度（Thread Schedule），通常拥有至少三种状态，分别是：\n\n- 运行（Running）：此时线程正在执行。\n\n- 就绪（Ready）：此时线程可以立刻运行，但 CPU 已经被占用，暂时无法分配。\n\n- 等待（Waiting）：此时线程正在等待某一事件（例如：I/O 事件）发生，无法执行。\n\n处于运行状态中的线程都会拥有一段可以执行的时间，这段时间段称为时间片（Time Slice）。其基本流转：\n\n- 当时间片用尽的时候，进程进入就绪状态。\n\n- 当时间片用尽之前，进程若开始等待某个事件，那么它将进入等待状态。\n\n每当一个线程离开运行状态时，调度系统就会选择一个当前是就绪状态的线程继续执行。而一个处于等待状态的线程在完成所等待的事件后，就会进入就绪状态。\n\n![image](https://image.eddycjy.com/b7cec2f55d30a4ed9d46f2d0e50387a7.jpg)\n\n### 线程优先级\n\n在 Windows 和 Linux 中，线程的优先级可以通过用户手动设置，系统也会根据线程的表现自动调整优先级，以使得调度更有效率。常见的一般有两类线程：\n\n- IO 密集型线程（IO Bound Thread）：频繁等待，像是网络调用。\n\n- CPU 密集型线程（CPU Bound Thread）：很少等待，主要是计算为主。\n\n常见的线程调度方式如下：\n\n- 轮转法：让各个线程轮流执行一小段时间，这也决定了线程之间存在交错执行的特点。\n\n- 优先级调度：在具有优先级调度的系统中，线程都拥有各自的线程优先级，具有高优先级的线程会更早的执行，低优先级的线程常常要等待到系统中已经没有高优先级的可执行线程时才可以执行。\n\nIO 密集型线程总是会比 CPU 密集型线程容易得到优先级的提升。但在优先级调度下，存在一种线程饿死的现象。一个线程被饿死，是说它的优先级比较低，在它执行之前，总是有较高优先级的线程要执行。因此低优先级线程始终无法执行。\n\n为了避免饿死现象，调度系统会逐步提升那些等待了过长时间的得不到执行的线程优先级。这样的方式，一个线程只要等待足够长的时间，其优先级最终一定会提高到足够让他执行的程度。线程优先级改变一般有三种方式：\n\n- 用户指定优先级。\n\n- 根据进入等待状态的频繁程度提升或降低优先级。\n\n- 长时间得不到执行而被提升优先级。\n\n### 可抢占线程和不可抢占线程\n\n线程在用尽时间片之后会被强制剥夺继续执行的权利，而进入就绪状态，这个过程叫做抢占（Preemption），即之后执行的别的线程抢占了当前线程。\n\n目前以可抢占式线程居多，非抢占式线程在今日已经十分少见。\n\n### 三种线程模型\n\n日常在程序中使用的线程其实并不是内核线程，而是存在于用户态的用户线程。用户态并不一定在操作系统内核中对应同等数量的内核线程。接下来将介绍三种常见的用户态多线程库的实现方式。\n\n#### 一对一模型\n\n一对一模型指的是一个用户使用的线程就唯一对应一个内核使用的线程。\n\n![一对一线程模型](https://image.eddycjy.com/af87eb0e79d373560bdbf4e7ed8ca630.jpg)\n\n优点：\n\n- 线程之间的并发是真正的并发。\n\n- 线程阻塞时，其他线程执行不会受到影响。\n\n缺点：\n\n- 操作系统限制了内核线程的数量，因此一对一线程会让用户的线程数受限。\n\n- 内核线程调度时，上下文切换的开销较大，导致用户线程的执行效率下降。\n\n#### 多对一模型\n\n多对一模型指的是多个用户线程映射到一个内核线程上，线程之间的切换由用户态的代码来进行。\n\n![多对一模型](https://image.eddycjy.com/daf4659747a2f7edcc317493ec7ebf96.jpg)\n\n优点：\n\n- 线程切换相对于一对一模型来说高效许多。\n\n缺点：\n\n- 如果一个用户线程阻塞，那么所有的线程豆浆无法执行，因为内核线程也被阻塞住了。\n\n\n#### 多对多模型\n\n多对多模型指的是将多个用户线程映射到少数但不止一个内核线程上。\n\n![多对多模型](https://image.eddycjy.com/25cdc9b99b8543ca33a6494b88e2d482.jpg)\n\n优点：\n\n- 一个用户线程阻塞，并不会导致其它用户线程也阻塞。\n\n缺点：\n\n- 性能提升不如一对一模型高。\n\n## 总结\n\n本文主要涉及到 CPU、内存、线程。我们能够从其的一些关注点知道为什么 CPU 调度会这样子发展，又经历了什么东西。内存为什么会出现虚拟内存，物理内存，其之间又是如何相互转换的。\n\n另外还了解到线程的基本分类和常见调度方式等，这些都是计算机基本的软硬件知识，非常值得大家仔细思考。\n"
  },
  {
    "path": "content/posts/reading/programmer-compile-link.md",
    "content": "---\ntitle: \"应用编译，计算机中那些一定要掌握的知识细节\"\ndate: 2020-10-28T20:52:52+08:00\ntoc: true\ntags: \n  - 程序员的自我修养\n---\n\n”Hello World“ 程序几乎是每个程序员入门和开发环境测试的基本标准。代码如下：\n\n```\n#inclue <stdio.h>\n\nint main()\n{\n\tprintf(\"Hello Wolrd\\n\");\n\treturn 0;\n}\n```\n\n编译该程序，再运行，就基本完成了所有新手的第一个程序。表面看起来轻轻松松，毫无悬念。但是实际上单纯这几下操作，就已经包含了不少暗操作。本着追根溯源的目的，我们将进一步对其流程进行分析。\n\n![image](https://image.eddycjy.com/a8040c0fc18257d2891d4b570b02c44d.jpg)\n\n其内部主要包含 4 个步骤，分别是：预处理、编译、汇编以及链接。由于篇幅问题本文主要涉及前三部分，链接部分将会放到下一篇文章来讲解。\n\n## 预编译\n\n程序编译的第一步是 “预编译” 环境。主要作用是处理源代码文件中以 ”#“ 开始的预编译指令，例如：`#include`、`#define` 等。\n\n常见的处理规则是：\n\n- 将所有 `#define` 删除，并且展开所有的宏定义。\n\n- 处理所有条件预编译指令，比如 `if`、`ifdef`、`elif`、`else`、`endif`。\n\n- 处理 `#include` 预编译指令，将所包含的文件插入到该预编译指令的位置（可递归处理子级引入）。\n\n- 删除所有的注释。\n\n- 添加行号和文件名标识，以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时显示行号。\n\n- 保留所有的 `#pragma` 编译器指令，后续编译器将会使用。\n\n在预编译后，文件中将不包含宏定义或引入。因为在预编译后将会全部展开，相应的代码段均已被插入文件中。像 Go 语言中的话，主要是 `go generate` 命令会涉及到相关的预编译处理。\n\n## 编译\n\n第二步正式进入到 \"编译\" 环境。主要作用是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件。该部分通常是整个程序构建的核心部分，也是最复杂的部分之一。\n\n执行编译操作的工具，一般称其为 “编译器”。编译器是将高级语言翻译成机器语言的一个工具。例如我们平时用 Go 语言写的程序，编译器就可以将其编译成机器可以执行的指令及数据。那么我们就不需要再去关心相关的底层细节，因为使用机器指令或汇编语言编写程序是一件十分费时及乏味的事情。\n\n且高级语言能够使得程序员更关注程序逻辑的本身，不再需要过多的关注计算机本身的限制，具有更高的平台可移植性，能够在多种计算机架构下运行。\n\n### 编译过程\n\n编译过程一般分为 6 步：扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化。整个过程如下：\n\n![image](https://image.eddycjy.com/c1e4902df20b68df654229d9618b9d58.jpg)\n\n我们结合上图的源代码（Source Code）到最终目标代码（Final Target Code）的过程，以一段最简单的 Go 语言程序的代理例子来复现和讲述整个过程，如下：\n\n```\npackage main\n\nimport (\n\t\"fmt\"\n)\n\nfunc main() {\n\tfmt.Println(\"Hello World.\")\n}\n```\n\n### 词法分析\n\n首先 Go 程序会被输入到扫描器中，可以理解为所有解析程序的第一步，都是读取源代码。而扫描器的任务很简单，就是利用有限状态机对源代码的字符序列进行分割，最终变成一系列的记号（Token）。\n\n如下 Hello World 利用 go/scanner 进行处理：\n\n```\n1:1     package \"package\"\n1:9     IDENT   \"main\"\n1:13    ;       \"\\n\"\n3:1     import  \"import\"\n3:8     (       \"\"\n4:2     STRING  \"\\\"fmt\\\"\"\n4:7     ;       \"\\n\"\n5:1     )       \"\"\n5:2     ;       \"\\n\"\n7:1     func    \"func\"\n7:6     IDENT   \"main\"\n7:10    (       \"\"\n7:11    )       \"\"\n7:13    {       \"\"\n8:2     IDENT   \"fmt\"\n8:5     .       \"\"\n8:6     IDENT   \"Println\"\n8:13    (       \"\"\n8:14    STRING  \"\\\"Hello World.\\\"\"\n8:28    )       \"\"\n8:29    ;       \"\\n\"\n9:1     }       \"\"\n9:2     ;       \"\\n\"\n```\n\n在经过扫描器的扫描后，可以看到输出了一大堆的 Token。如果没有前置知识的情况下，第一眼可能会非常懵逼。在此可以初步了解一下 Go 所主要包含的标识符和基本类型，如下：\n\n```\n\t// Special tokens\n\tILLEGAL Token = iota\n\tEOF\n\tCOMMENT\n\n\t// Identifiers and basic type literals\n\t// (these tokens stand for classes of literals)\n\tIDENT  // main\n\tINT    // 12345\n\tFLOAT  // 123.45\n\tIMAG   // 123.45i\n\tCHAR   // 'a'\n\tSTRING // \"abc\"\n\tliteral_end\n```\n\n再根据所输出的 Token 稍加思考，做对比，就可得知其仅是单纯的利用扫描器翻译和输出。而实质上在识别记号时，扫描器也会完成其他工作，例如把标识符放到符号表，将数字、字符串常量存放到文字表等。\n\n词法分析产生的记号一般可以分为如下几类：\n\n- 关键字。\n\n- 标识符。\n\n- 字面量（包含数字、字符串等）。\n\n- 特殊符合（如加号、等号）\n\n### 语法分析/语义分析\n\n#### 语法分析器\n\n语法分析器（Grammar Parser）将对扫描器所产生的记号进行语法分析，从而产生语法树（Syntax Tree），也称抽象语法树（Abstract Syntax Tree，AST）。\n\n常见的分析方式是自顶向下或者自底向上，以及采取[上下文无关语法](https://en.wikipedia.org/wiki/Context-free_grammar)（Context-free Grammer）作为分析手段。这块可参考一些计算机理论的资料，涉及的比较广。\n\n但语法分析仅完成了对表达式的语法层面的分析，但并不清楚这个语句是否真正有意义，还需要一步语义分析。\n\n#### 语义分析器\n\n语义分析器（Semantic Analyzer）将会对对语法分析器所生成的语法树上的表达式标识具体的类型。主要分为两类：\n\n- 静态语义：在编译器就可以确定的语义。\n\n- 动态语义：在运行期才可以确定的语义。\n\n在经过语义分析阶段后，整个语法树的表达式都会被标识上类型，如果有些类型需要进行隐式转换，语义分析程序将会在语法书中插入相应的转换点，成为有更具体含义的语义。\n\n#### 实战演练\n\n语法分析器生成的语法树，本质上就是以表达式（Expression）为节点的树。在 Go 语言中可通过 go/token、go/parser、go/ast 等相关方法生成语法树，代码如下：\n\n```\nfunc main() {\n\tsrc := []byte(\"package main\\n\\nimport (\\n\\t\\\"fmt\\\"\\n)\\n\\nfunc main() {\\n\\tfmt.Println(\\\"Hello World.\\\")\\n}\")\n\tfset := token.NewFileSet() // positions are relative to fset\n\tf, err := parser.ParseFile(fset, \"\", src, 0)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tast.Print(fset, f)\n}\n```\n\n其经过语法分析器（自顶下向）分析后会所输出的结果如下：\n\n```\n     0  *ast.File {\n     1  .  Package: 1:1\n     2  .  Name: *ast.Ident {\n     3  .  .  NamePos: 1:9\n     4  .  .  Name: \"main\"\n     5  .  }\n     6  .  Decls: []ast.Decl (len = 2) {\n     7  .  .  0: *ast.GenDecl {\n     8  .  .  .  TokPos: 3:1\n     9  .  .  .  Tok: import\n    10  .  .  .  Lparen: 3:8\n    11  .  .  .  Specs: []ast.Spec (len = 1) {\n    12  .  .  .  .  0: *ast.ImportSpec {\n    13  .  .  .  .  .  Path: *ast.BasicLit {\n    14  .  .  .  .  .  .  ValuePos: 4:2\n    15  .  .  .  .  .  .  Kind: STRING\n    16  .  .  .  .  .  .  Value: \"\\\"fmt\\\"\"\n    17  .  .  .  .  .  }\n    18  .  .  .  .  .  EndPos: -\n    19  .  .  .  .  }\n    20  .  .  .  }\n    21  .  .  .  Rparen: 5:1\n    22  .  .  }\n    23  .  .  ...\n    71  .  }\n    72  .  Scope: *ast.Scope {\n    73  .  .  Objects: map[string]*ast.Object (len = 1) {\n    74  .  .  .  \"main\": *(obj @ 27)\n    75  .  .  }\n    76  .  }\n    77  .  Imports: []*ast.ImportSpec (len = 1) {\n    78  .  .  0: *(obj @ 12)\n    79  .  }\n    80  .  Unresolved: []*ast.Ident (len = 1) {\n    81  .  .  0: *(obj @ 46)\n    82  .  }\n    83  }\n```\n\n- Package：解析出 package 关键字的位置，1:1 指的是位置在第一行的第一个。\n\n- Name：解析出 package name 的名称，类型是 `*ast.Ident`，1:9 指的是位置在第一行的第九个。\n\n- Decls：节点的顶层声明，其对应 BadDecl（Bad Declaration）、GenDecl（Generic Declaration）、FuncDecl（Function Declaration）。\n\n- Scope：在此文件中的函数作用域，以及作用域对应的对象。\n\n- Imports：在此文件中所导入的模块。\n\n- Unresolved：在此文件中未解析的标识符。\n\n- Comments：在此文件中的所有注释内容。\n\n可视化后的语法树如下：\n\n![image](https://image.eddycjy.com/552b1d6ea65da7470449eac084195f3f.jpg)\n\n在上文中，主要涉及语法分析和语义分析部分，其归属于编译器前端，最终结果是得到了语法树，也就是常说是抽象语法树（AST）。有兴趣可以亲自试试 [yuroyoro/goast-viewer](http://goast.yuroyoro.net/)，会对语法树的理解更加的清晰。\n\n### 中间语言生成\n\n现代的编译器有这多个层次的优化，通常源代码级别会有一个优化过程。例如单纯的 1+2 的表达式就可以被优化。而在 Go 语言中，中间语言则会涉及[静态单赋值](https://en.wikipedia.org/wiki/Static_single_assignment_form)（Static Single Assignment，SSA）的特性。\n\n假定有一个很简单的 SayHelloWorld 方法，如下：\n\n```\npackage helloworld\n\nfunc SayHelloWorld(a int) int {\n    c := a + 2\n    return c\n}\n```\n\n想看到源代码到中间语言，再到 SSA 的话，可通过 `GOSSAFUNC` 编译源代码并查看：\n\n```\n$ GOSSAFUNC=SayHelloWorld go build helloworld.go\n# command-line-arguments\ndumped SSA to ./ssa.html\n```\n\n打开 ssa.html，可看到这个文件源代码所对应的语法树，好几个版本的中间代码以及最终所生成的 SSA。\n\n![image](https://image.eddycjy.com/85cf8d94d2d3a4b1cbca13755feca46d.jpg)\n\n从左往右依次为：Sources（源代码）、AST（抽象语法树），其次最右边第一栏起就是第一轮中间语言（代码），后面还有十几轮。\n\n### 目标代码生成与优化\n\n在中间语言生成完毕后，还不能直接使用。因为机器真正能执行的是机器码。这时候就到了编译器后端的工作了。在源代码级优化器产生中间代码时，则标志着接下来的过程都属于编译器后端。\n\n编译器后端主要包括如下两类，作用如下：：\n\n- 代码生成器（Code Generator）：代码生成器将中间代码转换成目标机器代码。\n\n- 目标代码优化器（Target Code Optimizer）：针对代码生成器所转换出的目标机器代码进行优化。\n\n在 Go 语言中，以上行为包含在前面所提到的十几轮 SSA 优化降级中，有兴趣可自行研究 SSA，最后在 genssa 中可看见最终的中间代码：\n\n![image](https://image.eddycjy.com/191f907078d18683f6cae856d4b42fcb.jpg)\n\n此时的代码已经降级的与最终的汇编代码比较接近，但还没经过正式的转换。\n\n## 汇编\n\n完成程序编译后，第三步将是 ”汇编“，汇编器会将汇编代码转变成机器可执行的指令，每一个汇编语句几乎都对应着一条机器指令。基本逻辑就是根据汇编指令和机器指令的对照表一一翻译。\n\n在 Go 语言中，genssa 所生成的目标代码已经完成了优化降级，接下来会调用 `src/cmd/internal/obj` 包中的汇编器将 SSA 中间代码生成为机器码。我们可通过 `go tool compile -S` 的方式进行查看：\n\n```\n$ go tool compile -S helloworld.go \n\"\".SayHelloWorld STEXT nosplit size=15 args=0x10 locals=0x0\n    0x0000 00000 (helloworld.go:3)  TEXT    \"\".SayHelloWorld(SB), NOSPLIT|ABIInternal, $0-16\n    0x0000 00000 (helloworld.go:3)  FUNCDATA    $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)\n    0x0000 00000 (helloworld.go:3)  FUNCDATA    $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)\n    0x0000 00000 (helloworld.go:4)  MOVQ    \"\".a+8(SP), AX\n    0x0005 00005 (helloworld.go:4)  ADDQ    $2, AX\n    0x0009 00009 (helloworld.go:5)  MOVQ    AX, \"\".~r1+16(SP)\n    0x000e 00014 (helloworld.go:5)  RET\n    0x0000 48 8b 44 24 08 48 83 c0 02 48 89 44 24 10 c3     H.D$.H...H.D$..\ngo.cuinfo.packagename. SDWARFINFO dupok size=0\n    0x0000 68 65 6c 6c 6f 77 6f 72 6c 64                    helloworld\ngclocals·33cdeccccebe80329f1fdbee7f5874cb SRODATA dupok size=8\n    0x0000 01 00 00 00 00 00 00 00                          ........\n```\n\n至此就完成了一个高级语言再到计算机所能理解的机器码转换的完整流程了。\n\n## 总结\n\n在本文中，我们基本了解了一个应用程序是怎么从源代码编译到最终的机器码，其中每一步展开都是一块非常大的计算机基础知识。若有读者对其感兴趣，可根据文中的实操步骤进行深入的剖析和了解。\n\n在下一篇文章中，将会进一步针对最后的一个步骤链接来进行分析和了解其最后一公里。"
  },
  {
    "path": "content/posts/reading/programmer-linker.md",
    "content": "---\ntitle: \"链接器，应用编译的最后一公里路\"\ndate: 2020-11-05T21:15:46+08:00\ntoc: true\ndraft: true\nimages:\ntags: \n  - 程序员的自我修养\n---\n\n在《应用编译，计算机中那些一定要掌握的知识细节》中，我们完成了对 ”预处理、编译、汇编“ 三个基本步骤的了解。接下来剩余完整编译输出的最后一公里路，那就是 ”链接“。\n\n编译器在经过扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化后，源代码终于被编译成了目标代码。但又遇到了新的问题，那就是 “目标代码中有变量定义在其他模块怎么办？”\n\n而其他模块的全局变量和函数的绝对地址是在最终**链接阶段**才会确定，这是很重要的一步，有了模块串联才能让目标程序跑起来。\n\n## 静态链接\n\n### 地址分配\n\n### 空间分配\n\n### 符合决议\n\n### 重定位\n\n## 动态链接\n\n## 总结"
  },
  {
    "path": "content/posts/reload-man.md",
    "content": "---\ntitle: \"为什么鼓励可以重塑一个职场人？\"\ndate: 2021-12-31T12:54:56+08:00\ntoc: true\nimages:\ntags: \n  - 职场成长\n---\n\n大家好，我是煎鱼。\n\n最近在看书的时候看到了一句很经典的话：“**重塑一个人行为最好的方式是鼓励**”。平时我在多个领域的不同图书、不同面谈场合都能听到过不同的诠释。\n\n![](https://files.mdnice.com/user/3610/9a4b75a6-b512-47b8-9c12-28d9b8e7a068.jpg)\n\n可惜的是作者大多都没有指出是 “心理学” 的范畴，都只是表示应该要这么做，没有表示为什么，没有讲解来源，传播的信息非常的 3.5 手。\n\n为此特意咨询了身边的一个心理学科班大佬，自打我认识她以来，每次开会都能看到她点头表示肯定，在我的记忆中非常深刻，非常有意思。\n\n与她咨询过后，得出对应的是理论是 “**皮格马利翁效应**”。今天就进一步展开来简单学习、思考一下这为何物。\n\n## 背景\n\n皮格马利翁效应，又称毕马龙效应、罗森塔尔效应或期待效应，是指人（通常是指孩童或学生）在被赋予更高期望以后，他们会表现的更好的一种现象。\n\n![图来自网络](https://files.mdnice.com/user/3610/e27b3d98-18c0-4cfc-bd4d-5900ac772351.png)\n\n美国心理学家罗伯特·罗森塔尔和雷诺尔·雅各布森对此进行了研究。在他们的研究中发现，假设**老师对学生的期望加强，学生的表现也会相对加强**。\n\n## 实验\n\n在罗森塔尔的实验中，他们从 1~6 年级各选了 3 个班，对这 18 个班的学生进行了 “未来发展趋势测验”。并将一份 “最有发展前途者” 名单（实际上是随便抽取出来的）交给了校长和相关老师。\n\n![图来自网络](https://files.mdnice.com/user/3610/6a09db03-5811-4ff7-b6b5-7a2cb2cc36e4.png)\n\n在 8 个月后，罗森塔尔和助手们对那 18 个班级的学生进行复试，结果发现：**被期望的学生表现出更有适应能力、更有魅力、求知欲更强、智力更活跃等倾向**。\n\n![图来自网络](https://files.mdnice.com/user/3610/c59f2415-812a-4d74-bc93-03f360944b79.png)\n\n通过各种研究和试验得出结论：**认为教师的期望会传递给被期望的学生并产生鼓励效应，进而产生改变**。\n\n也正是因为罗森塔尔对该效应在小学教学上予以验证提出，皮格马利翁效应也被称为“罗森塔尔效应”。\n\n## 思考\n\n回到最初的话题，为什么作者会说 ”重塑一个人行为最好的方式是鼓励”，这让我想起了非常著名两类思考模式：\n\n![图来自网络](https://files.mdnice.com/user/3610/db4d42cc-9519-4c03-8961-57b3deaf9a65.png)\n\n- 成长型思考模式：认为学习不在于天赋，在于努力，只要努力用功，什么都能学会。\n- 固定型思考模式：特别相信天赋的作用，擅长的东西就是擅长，要是不擅长怎么学都没有用。\n\n这其实就直接和 “鼓励” 有关系，我们接下来结合 “否定” 和 “鼓励” 来具体展开说说。\n\n看看长期对一个人进行 “否定”，又或是进行 “鼓励” 可能会发生怎么样的事情，会不会改变一个人？\n\n### 否定\n\n如果在职场中，你**对一个人一直是否定、不认可，这对他是不好的**。你无论做什么，那个人总是挑刺，冷嘲热讽，在公开场合批判你。\n\n可能会带来如下结果：\n- 若他是一张白纸：极有可能就会认为自己不行，认为自己怎么学都没有用，无限的进行自我否定，很难走出来。\n- 若他不是白纸：明确知道自己的底线，一直否定、不认可，也会反噬你团队，又或是对你有极大的意见。\n\n这还是明确是否白纸的场景，但**更多的人不是非黑即白，也就是在灰色，处于两者之间**，不少人会怀疑人生。\n\n逐渐才发现是**大家常说的职场 PUA**，这在勾心斗角的某些地方很常见。\n\n### 鼓励\n\n如果我们加之鼓励，多鼓励职场中的同事，在不同的人生阶段能带来不同的结果：\n- 从小培养：埋下成长型思维模式的种子，他会主动把每一项任务都当成成长的机会，主动愿意花更长的时间钻研难题，主动选择更困难的任务。\n- 成年发展：知道自己不应该被局限、固定的思维定式所控制，慢慢把 “固定型思考” 转变成 “成长型思考”，就会有较大的突破。\n\n在职场中受到鼓励后，就**会朝着这块持续不断地突破，获得自我和周遭的成就和认同感，进而更进一步的拓展，得到更多的发展空间**，这就是 “鼓励” 的力量。\n\n## 职场和工作\n\n无论是哪种形式：爱鼓励、爱批评、爱否定。这世界上都大有人在，总是包罗万象，走 “二极管” 是错误的，单一的某种方式总是会带来极端。\n- 若是永远只有 “鼓励”：那社会上人是千人千面的，习惯性被鼓励，没人鼓励，就动不了了。\n- 若是只有 “批判”、“否定”：那就更糟糕了。一个只会骂人的 Leader，你愿意跟吗？我相信肯定不愿意。\n\n在职场中，除了要多鼓励他人、接受他人鼓励外，我们要多和公司内、同行、同业圈的小伙伴聊聊，及时找到自己的模糊、问题、错误的点。\n\n再针对性释出自己的解决方案，和朋友共同探索，寻找新的解决思路或方法，持续优化下去。\n\n这样就能够有一定的突破和增长，想永远都是问题，想后做了，才是答案，才能改进。\n\n## 最后\n\n如果是你，孩子考试考得不错，作业写得好，如何表扬孩子聪明？\n- A：“这题你都会做？我儿子太聪明了！”\n- B：“不错啊！这次做的很好，看来你下了很大功夫！下次继续！只要你努力！什么事都能做成！”\n\n选择 A，还是 B 呢？\n\n**欢迎大家在评论区留言和交流，说出你的答案和我们在思考**！\n"
  },
  {
    "path": "content/posts/where-is-proto.md",
    "content": "---\ntitle: \"Proto 代码到底放哪里？\"\ndate: 2020-05-23T15:07:37+08:00\ndraft: false\ntoc: true\nimages:\ntags: \n  - protobuf\n---\n\n虽然公司已经从大单体切换为微服务化有一定的年头了，但一些细节方面的处理总会有不同的人有不同的看法，这其中一个讨论点，就是 Proto 这个 IDL 的代码到底放在哪里？\n\n目前来看，一共有如下方案， 我们一起来探讨一下 Proto 的存储方式和对应带来的优缺点。\n\n## 方案一：存放在代码仓库\n\n直接将项目所依赖到的所有 Proto 文件都存放在 `proto/` 目录下，不经过开发工具的自动拉取和发布：\n\n![image](https://image.eddycjy.com/7e95a76d548197cf73e7238ee5df27e6.jpg)\n\n### 缺点\n\n1. 项目所有依赖的 Proto 都存储在代码仓库下，因此所有依赖 Proto 都需要人工的向其它业务组 “要” 来，再放到 `proto/` 目录下，人工介入极度麻烦。\n\n2. Proto 升级和变更，经常要重复第一步，沟通成本高。\n\n### 优点\n\n1. 项目所有依赖的 Proto 都存储在代码仓库下，因此不涉及个人开仓库权限的问题。\n\n2. 多 Proto 的切换开销减少，因为都在代码仓库下，不需要看这看那。\n\n## 方案二：独立仓库\n\n独立仓库存储是我们最早采取的方式，也就是每个服务对应配套一个 Proto 仓库：\n\n![image](https://image.eddycjy.com/a7575628ff1ae9179f022d477aba9df1.png)\n\n这个方案的好处就是可以独立管理所有 Proto 仓库，并且权限划分清晰。但最大的优点也是最大的缺点，因为一个服务会依赖多个 Proto 仓库，并且存在跨业务组调用的情况：\n\n![image](https://image.eddycjy.com/31c5a5b5652f44750124c15e1c0ef397.jpg)\n\n如上图所示，svc-user 服务分别依赖了三块 Proto 仓库，分别是自己组的、业务组 A、业务组 B 总共的 6 个 Proto 仓库。\n\n### 缺点\n\n1. 假设你是一个新入职的开发人员，那么你就需要找不同的业务组申请不同的仓库权限，非常麻烦。如果没有批量赋权工具，也没有管理者权限，那么就需要一个个赋权，非常麻烦。\n2. 在运行服务的时候，你需要将所有相关联的 Proto 仓库拉取下来，如果没有工具做半自动化的支持，麻烦程度无法忍受。\n\n### 优点\n\n1. 使得安全性较高（但 IDL 本身没有太多的秘密）。\n\n2. 按需拉取，不需要关注其余的服务 Proto。\n\n## 方案三：集中仓库\n\n集中仓库也是一些公司考虑的方式之一，是按公司或大事业部的维度进行 Proto 代码的存储，这样子只需要拉取一个仓库，就可以获取到所有所需的 IDL：\n\n![image](https://image.eddycjy.com/9f0c7821aa2d42857685e04d9df25740.jpg)\n\n### 缺点\n\n1. 安全性下降，因为其它业务组的 IDL 也全都 “泄露” 了。\n\n2. 非按需拉取，在查看原始文件时，需要关注一些多余的。\n\n### 优点\n\n1. 只需要拉取一次 Proto 仓库就可以轻松把一个服务所需的 IDL 集齐。\n\n2. 仓库权限管理的复杂度下降。\n\n## 方案四：镜像仓库\n\n结合上面几种方案的特点，我们也可以得出镜像仓库的方式，也就是自己服务的 Proto 文件存放在代码仓库的 proto 文件中，在本次 feature 提交或发布后，自动同步到镜像仓库去。\n\n而你所依赖的其他服务 Proto 则直接通过读取集中的镜像仓库的方式获取：\n\n![image](https://image.eddycjy.com/d90a0040923aacced41549ea902e6cde.jpg)\n\n这样子的话，通过开发工具的配合，开发人员在开发时就只需要关注自己项目的 Proto，集中的镜像仓库用于构建和部署时就可以了，大幅度降低了多 Proto 的关注和切换开销。\n\n## 方案五：其他\n\n本质上上述的所有方案多多少少都有一些利弊存在，并且都需要开发工具来进行支持，否则实操起来还是非常麻烦。\n\n如果想一劳永逸，可以通过云开发环境来解决，因为在分配云开发机时，你已经有了身份认证，你能够拥有什么权限，不能拥有什么权限，基本都是明确的，并且一般在组内、跨组联调时，也可以直接调度，不需要像其它方案那样进行过多的关注，甚至在自己本地运行一套微服务。\n\n但这需要大量的工具/资源支持。\n\n## 小结\n\n在本文中我介绍了比较常见的 5 种 Proto 代码的管理方式，其各有利弊，不同公司不同人的理解或适配度都不一样，大家可以根据实际环境进行选用，并且建议拉上核心的人员进行讨论和选型，因为 Proto 代码涉略面还是比较广的，多多少少都有人有不一样的看法。"
  },
  {
    "path": "content/posts/why-container-memory-exceed.md",
    "content": "---\ntitle: \"为什么容器内存占用居高不下，频频 OOM\"\ndate: 2020-06-07T14:52:19+08:00\ntoc: true\ntags: \n  - go\n---\n\n\n最近我在回顾思考（写 PPT），整理了现状，发现了这个问题存在多时，经过一番波折，最终确定了元凶和相对可行的解决方案，因此也在这里分享一下排查历程。\n\n时间线：\n\n- 在上 Kubernetes 的前半年，只是用 Kubernetes，开发没有权限，业务服务极少，忙着写新业务，风平浪静。\n\n- 在上 Kubernetes 的后半年，业务服务较少，偶尔会阶段性被运维唤醒，问之 “**为什么你们的服务内存占用这么高，赶紧查**”。此时大家还在为新业务冲刺，猜测也许是业务代码问题，但没有调整代码去尝试解决。\n\n- 在上 Kubernetes 的第二年，业务服务逐渐增多，普遍增加了容器限额 Limits，出现了好几个业务服务是内存小怪兽，因此如果不限制的话，服务过度占用会导致驱逐，因此反馈语也就变成了：“**为什么你们的服务内存占用这么高，老被 OOM Kill，赶紧查**”。据闻也有几个业务大佬有去排查（因为 OOM 反馈），似乎没得出最终解决方案。\n\n不禁让我们思考，为什么个别 Go 业务服务，Memory 总是提示这么高，经常达到容器限额，以至于被动 OOM Kill，是不是有什么安全隐患？\n\n## 现象\n\n### 内存居高不下\n\n发现个别业务服务内存占用挺高，触发告警，且通过 Grafana 发现在凌晨（没有什么流量）的情况下，内存占用量依然拉平，没有打算下降的样子，高峰更是不得了，像是个内存炸弹：\n\n![image](https://image.eddycjy.com/2e6c8c153836b29175dff7623ec67a0a.png)\n\n并且我所观测的这个服务，早年还只是 100MB。现在随着业务迭代和上升，目前已经稳步 4GB，容器限额 Limits 纷纷给它开道，但我想总不能是无休止的增加资源吧，这是一个大问题。\n\n### 进入重启怪圈\n\n有的业务服务，业务量小，自然也就没有调整容器限额，因此得不到内存资源，又超过额度，就会进入疯狂的重启怪圈：\n\n![image](https://image.eddycjy.com/95644633a2d55cb2f2684a23d3f3f189.png)\n\n重启将近 300 次，非常不正常了，更不用提所接受到的告警通知。\n\n## 排查\n\n### 猜想一：频繁申请重复对象\n\n出现问题的个别业务服务都有几个特点，那就是基本为图片处理类的功能，例如：图片解压缩、批量生成二维码、PDF 生成等，因此就怀疑是否在量大时频繁申请重复对象，而 Go 本身又没有及时释放内存，因此导致持续占用。\n\n#### sync.Pool\n\n基本上想解决 “频繁申请重复对象”，我们大多会采用多级内存池的方式，也可以用最常见的 sync.Pool，这里可参考全成所借述的《Go 夜读》上关于 sync.Pool 的分享，关于这类情况的场景：\n\n> 当多个 goroutine 都需要创建同⼀个对象的时候，如果 goroutine 数过多，导致对象的创建数⽬剧增，进⽽导致 GC 压⼒增大。形成 “并发⼤－占⽤内存⼤－GC 缓慢－处理并发能⼒降低－并发更⼤”这样的恶性循环。\n\n#### 验证场景\n\n在描述中关注到几个关键字，分别是并发大，Goroutine 数过多，GC 压力增大，GC 缓慢。也就是需要满足上述几个硬性条件，才可以认为是符合猜想的。\n\n通过拉取 PProf goroutine，可得知 Goroutine 数并不高：\n\n![image](https://image.eddycjy.com/4adaadade78389230318c41d006de4ef.png)\n\n另外在凌晨长达 6 小时，没有什么流量的情况下，也不符合并发大，Goroutine 数过多的情况，若要更进一步确认，可通过 Grafana 落实其量的高低。\n\n从结论上来讲，我认为与其没有特别直接的关系，但猜想其所对应的业务功能到导致的间接关系应当存在。\n\n### 猜想二：不知名内存泄露\n\n内存居高不下，其中一个反应就是猜测是否存在泄露，而我们的容器中目前只跑着一个 Go 进程，因此首要看看该 Go 应用是否有问题。这时候可以借助 PProf heap（可以使用 base -diff）：\n\n![image](https://image.eddycjy.com/877545ab97b21e68b580567ccf38e08b.png)\n\n显然其提示的内存使用不高，那会不会是 PProf 出现了 BUG 呢。接下通过命令也可确定 Go 进程的 RSS 并不高，但 VSZ 却相对 “高” 的惊人，我在 19 年针对此写过一篇[《Go 应用内存占用太多，让排查？（VSZ篇）》](https://eddycjy.com/posts/go/talk/2019-09-24-why-vsz-large) ，这次 VSZ 过高也给我留下了一个念想。\n\n从结论上来讲，也不像 Go 进程内存泄露的问题，因此也将其排除。\n\n### 猜想三：madvise 策略变更\n\n- 在 Go1.12 以前，Go Runtime 在 Linux 上使用的是 `MADV_DONTNEED` 策略，可以让 RSS 下降的比较快，就是效率差点。\n\n- 在 Go1.12 及以后，Go Runtime 专门针对其进行了优化，使用了更为高效的 `MADV_FREE` 策略。但这样子所带来的副作用就是 RSS 不会立刻下降，要等到系统有内存压力了才会释放占用，RSS 才会下降。\n\n查看容器的 Linux 内核版本：\n\n```\n$ uname -a\nLinux xxx-xxx-99bd5776f-k9t8z 3.10.0-693.2.2.el7.x86_64 \n```\n\n但 `MADV_FREE` 的策略改变，需要 Linux 内核在 4.5 及以上（详细可见 [go/issues/23687](https://github.com/golang/go/issues/23687)），显然不符合，因此也将其从猜测中排除。\n\n### 猜想四：监控/判别条件有问题\n\n会不会是 Grafana 的图表错了，Kubernetes OOM Kill 的判别标准也错了呢，显然不大可能，毕竟我们拥抱云，阿里云 Kubernetes 也运行了好几年。\n\n![image](https://image.eddycjy.com/ad7ca63b33af2b856a8efcf7ab36dbd4.jpg)\n\n但在这次怀疑中，我了解到 OOM 的判断标准是 container_memory_working_set_bytes 指标，因此有了下一步猜想。\n\n### 猜想五：容器环境的机制\n\n既然不是业务代码影响，也不是 Go Runtime 的直接影响，那是否与环境本身有关呢，我们可以得知容器 OOM 的判别标准是 container_memory_working_set_bytes（当前工作集）。\n\n而 container_memory_working_set_bytes  是由 cadvisor 提供的，对应下述指标：\n\n![image](https://image.eddycjy.com/288361fd15c915e6ff1bb6d21f943939.jpg)\n\n从结论上来讲，Memory 换算过来是 4GB+，石锤。接下来的问题就是 Memory 是怎么计算出来的呢，显然和 RSS 不对标。\n\n## 原因\n\n从 [cadvisor/issues/638](https://github.com/google/cadvisor/issues/638) 可得知 container_memory_working_set_bytes 指标的组成实际上是 RSS + Cache。而 Cache 高的情况，常见于进程有大量文件 IO，占用 Cache 可能就会比较高，猜测也与 Go 版本、Linux 内核版本的 Cache 释放、回收方式有较大关系。\n\n![image](https://image.eddycjy.com/36d4ccc1cf705be334d53766f4ea8dc2.jpg)\n\n而各业务模块常见功能，如：\n\n- 批量图片解压缩。\n- 批量二维码生成。 \n- 批量上传渲染后图片。\n- 批量 PDF 生成。\n- ...\n\n只要是涉及有大量文件 IO 的服务，基本上是这个问题的老常客了，写这类服务基本写一个中一个，因为这是一个混合问题，像其它单纯操作为主的业务服务就很 “正常”，不会出现内存居高不下。\n\n## 解决方案\n\n在本场景中 cadvisor 所提供的判别标准 container_memory_working_set_bytes 是不可变更的，也就是无法把判别标准改为 RSS，因此我们只能考虑掌握主动权。\n\n### 开发角度\n\n使用类 sync.Pool 做多级内存池管理，防止申请到 “不合适”的内存空间，常见的例子： ioutil.ReadAll：\n\n```\nfunc (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {\n    …\n    for {\n        if free := cap(b.buf) - len(b.buf); free < MinRead {\n            newBuf := b.buf\n            if b.off+free < MinRead {\n                    newBuf = makeSlice(2*cap(b.buf) + MinRead)  // 扩充双倍空间\n                    copy(newBuf, b.buf[b.off:])\n            }\n        }\n    }\n}\n\n```\n\n核心是做好做多级内存池管理，因为使用多级内存池，就会预先定义多个 Pool，比如大小 100，200，300的 Pool 池，当你要 150 的时候，分配200，就可以避免部分的内存碎片和内存碎块。\n\n但从另外一个角度来看这存在着一定的难度，因为你怎么知道什么时候在哪个集群上会突然出现这类型的服务，何况开发人员的预期情况参差不齐，写多级内存池写出 BUG 也是有可能的。\n\n让业务服务无限重启，也是不现实的，**被动重启，没有控制，且告警，存在风险**。\n\n### 运维角度\n\n为了掌握主动权，我们可以在部署环境可以配合脚本做 “手动” HPA，当容器内存指标超过约定限制后，起一个新的容器替换，再将原先的容器给释放掉，就可以在预期内替换且业务稳定了。\n\n![image](https://image.eddycjy.com/2b449204373cd0d7c8e8501a326061a7.jpg)\n\n## 总结\n\n根据上述排查和分析结果，原因如下：\n\n- 应用程序行为：文件处理型服务，导致 Cache 占用高。\n- Linux 内核版本：版本比较低（BUG?），不同 Cache 回收机制。\n- 内存分配机制：在达到 cgroup limits 前会尝试释放，但可能内存碎片化，也可能是一次性索要太多，无法分配到足够的连续内存，最终导致 cgroup oom。\n\n![image](https://image.eddycjy.com/391085a138e86866b78c960e4de516c7.jpg)\n\n从根本上来讲，应用程序需要去优化其内存使用和分配策略，又或是将其抽离为独立的特殊服务去处理。并不能以目前这样简单未经多级内存池控制的方式去使用，否则会导致内存使用量越来越大。\n\n而从服务提供的角度来讲，我们并不知道这类服务会在什么地方出现又何时会成长起来，因此我们需要主动去控制容器的 OOM，让其实现优雅退出，保证业务稳定和可控。\n\n因此如果可以，升级 Linux 内核版本走 cgroup v2 极有可能可以解决问题。\n\n## 回顾\n\n虽然这问题时间跨度比较长，整体来讲都是阶段性排查，本质上可以说是对 Kubernetes 的不熟悉有关。但综合来讲这个问题涉及范围比较大，因为内存居高不下的可能性有很多种，要一个个排查，开发权限有限，费时费力。\n\n基本排查思路就是：\n\n1. 怀疑业务代码（PProf）。\n2. 怀疑其它代码（PProf）。\n3. 怀疑 Go Runtime 。\n4. 怀疑工具。\n5. 怀疑环境。\n\n非常感谢在这大段时间内被我咨询的各位大佬们，感觉就是隔了一层纱，捅穿了就很快就定位到了，大家如果有其它解决方案也欢迎随时沟通。\n"
  },
  {
    "path": "content/posts/why-container-memory-exceed2.md",
    "content": "---\ntitle: \"为什么容器内存占用居高不下，频频 OOM（续）\"\ndate: 2020-06-19T21:29:08+08:00\ntoc: true\ntags: \n  - go\n---\n\n在上周的文章[《为什么容器内存占用居高不下，频频 OOM》](/posts/why-container-memory-exceed/) 中，我根据现状进行了分析和说明，收到了很多读者的建议和疑惑，因此有了这一篇文章，包含更进一步的说明和排查。\n\n## 疑问\n\n一般系统内存过高的情况下，可以通过 `free -m` 查看当前系统的内存使用情况：\n\n![image](https://image.eddycjy.com/daf2a1d53f4bf0f21e315d2333e08159.png)\n\n在发现是系统内存占用高后，就会有读者会提到，为什么不 “手动清理 Cache”，因为 Cache 高的话，可以通过 drop_caches 的方式来清理：\n\n1. 清理 page cache：\n\n```\n$ echo 1 > /proc/sys/vm/drop_caches\n```\n\n2. 清理 dentries 和 inodes：\n\n```\n$ echo 2 > /proc/sys/vm/drop_caches\n```\n\n3. 清理 page cache、dentries 和 inodes：\n\n```shell\n$ echo 3 > /proc/sys/vm/drop_caches\n```\n\n但新问题又出现了，因为我们的命题是在容器中，在 Kubernetes 中，若执行 drop_caches 相关命令，将会对 Node 节点上的所有其他应用程序产生影响，尤其是那些占用大量 IO 并由于缓冲区高速缓存而获得更好性能的应用程序，可能会产生 “负面” 后果。\n\n我想这并不是一个好办法。\n\n## 表象\n\n回归原始，那就是为什么要排查这个问题，本质原因就是容器设置了 Memory Limits，而容器在运行中达到了 Limits 上限，被 OOM 掉了，所以我们想知道为什么会出现这个情况。\n\n在前文中我们针对了五大类情况进行了猜想：\n\n- 频繁申请重复对象。\n- 不知名内存泄露。\n- madvise 策略变更。\n- 监控/判别条件有问题。\n- 容器环境的机制。\n\n在逐一排除后，后续发现容器的 Memory OOM 判定标准是 container_memory_working_set_bytes 指标，其实际组成为 RSS + Cache（最近访问的内存、脏内存和内核内存）。\n\n在排除进程内存泄露的情况下，我们肯定是希望知道 Cache 中有什么，为什么占用了那么大的空间，此时我们可以通过 Linux pmap 来查看该容器进程的内存映射情况：\n\n![image](https://image.eddycjy.com/0bb82eabe1fcc1a5a65f4382932a6d2c.jpg)\n\n在上图中，我们发现了大量的 mapping 为 anon 的内存映射，最终 totals 确实达到了容器 Memory 相当的量，那么 anon 又是什么呢。实质上 anon 行表示在磁盘上没有对应的文件，也就是没有实际存在的载体，是 anonymous。\n\n## 思考\n\n既然存在如此多的 anon，结合先前的考虑，我们知道出现这种情况的服务都是文件处理型服务，包含大量的批量生成图片、生成 PDF 等资源消耗型的任务，也就是会瞬间申请大量的内存，使得系统的空闲内存触及全局最低水位线（global wmark_min），而在触及全局最低水位线后，会尝试进行回收，实在不行才会触发 cgroup OOM 的行为。\n\n那么更进一步思考的是两个问题，一个是 cgroup 达到 Limits 前的尝试释放仍然不足以支撑所需申请的连续内存段，而另外一个问题就是为什么 Cache 并没有释放：\n\n![image](https://image.eddycjy.com/2e6c8c153836b29175dff7623ec67a0a.png)\n\n通过上图，可以肯定该服务在凌晨 00：00-06：00 是没有什么流量的，但是 container_memory_working_set_bytes 指标依旧稳定不变，排除 RSS 的原因，那配合指标的查看基本确定是该 cgroup 的 Cache 没有释放。\n\n而 Cache 的占用高，主要考虑是由于其频繁操作文件导致，因为在 Linux 中，在第一次读取文件时会将一份放到系统 Cache，另外一份则放入进程内存中使用。关键点在于当进程运行完毕关闭后，系统 Cache 是不会马上回收的，需要经过系统的内存管理后再适时释放。\n\n但我们发现 Cache 的持续不释放，进程也没外部流量，RSS 也低的可怜，Cache 不像被进程占用住了的样子（这一步的排除很重要），最终就考虑到是否 Linux 内核在这块内存管理上存在 BUG 呢？\n\n## 根因\n\n### 问题版本\n\n该服务所使用的 Kubernetes 是 1.11.5 版本，Linux 内核版本为 3.10.x，时间为 2017 年 9 月：\n\n```\n$ uname -a\nLinux xxxxx-xxx-99bd5776f-k9t8z 3.10.0-693.2.2.el7.x86_64 #1 SMP Tue Sep 12 22:26:13 UTC 2017 x86_64 Linux\n```\n\n都算是有一定年代的老版本了。\n\n### 原因分析\n\nmemcg 是 Linux 内核中管理 cgroup 内存的模块，但实际上在 Linux 3.10.x 的低内核版本中存在不少实现上的 BUG，其中最具代表性的是 memory cgroup 中 kmem accounting 相关的问题（在低版本中属于 alpha 特性）：\n\n- slab 泄露：具体可详见该文章 [SLUB: Unable to allocate memory on node -1](https://pingcap.com/blog/try-to-fix-two-linux-kernel-bugs-while-testing-tidb-operator-in-k8s/#bug-1-unstable-kmem-accounting) 中的介绍和说明。\n\n- memory cgroup 泄露：在删除容器后没有回收完全，而 Linux 内核对 memory cgroup 的总数限制是 65535 个，若频繁创建删除开启了 kmem 的 cgroup，就会导致无法再创建新的 memory cgroup。\n\n当然，为什么出现问题后绝大多数是由 Kubernetes、Docker 的相关使用者发现的呢（从 issues 时间上来看），这与云原生的兴起，这类问题与内部容器化的机制相互影响，最终开发者 “发现” 了这类应用频繁出现 OOM，于是开始进行排查。\n\n## 解决方案\n\n### 调整内核参数\n\n关闭 kmem accounting：\n\n```\ncgroup.memory=nokmem\n```\n\n也可以通过 kubelet 的 nokmem Build Tags 来编译解决：\n\n```\n$ kubelet GOFLAGS=\"-tags=nokmem\"\n```\n\n但需要注意，kubelet 版本需要在 v1.14 及以上。\n\n### 升级内核版本\n\n升级 Linux 内核至 kernel-3.10.0-1075.el7 及以上就可以修复这个问题，详细可见 [slab leak causing a crash when using kmem control group](https://bugzilla.redhat.com/show_bug.cgi?id=1507149#c101)，其在发行版中 CentOS 7.8 已经发布。\n\n## 总结\n\n经过内部讨论，由于种种原因（例如：Linux、Kubernetes 太低），我们选择了升级 Linux 版本，也就是 CentOS 8，这样子其内核版本就会到达至 4.x（cgroup 已经健壮了许多，且在 4.5 cgroup v2 已经 GA），相关问题已经修复，并同步设置 `cgroup.memory=nokmem` 即可解决/避免相关问题。\n\n而在写下这篇文章时，我们可以看到 kmem accounting 的不少问题都已经被修复或提上日程，这对本次排查提供了相当大的便利，在确定问题的所在后根据 cgroup leak 沿着排查下去，基本都能看到大量的前人所经历过的 “挣扎”，大家若有兴趣，也可以跟着参考所提供的的链接做更一进步的深入了解。\n\n但事实上，不管哪个 Linux 内核版本，都存在着或多或少的问题，需要做好适当的心理准备，否则就会遇到 “没上容器时好好的” 的窘境，查起问题更麻烦。\n\n## 后话\n\n现在生产集群已经迁移完毕多日，通过近期的观察，已经确定了这个问题的修复和解决。这是原本的情况：\n\n![image](https://image.eddycjy.com/2e6c8c153836b29175dff7623ec67a0a.png)\n\n新生产集群，经过数日后：\n\n![image](https://image.eddycjy.com/c6ae131d437aae460e6fe70c9cf076b7.png)\n\n通过对比，可以明确的看到，在原本的趋势图中，其在达到当时的内存高位后，即使在凌晨没有流量的情况下，容器内存也依然居高不下，纹丝不动，不会下降。\n\n再反观最新的趋势图，在没有流量打入的情况下，容器内存开始下降，说明 Cahce 的自动回收已经正常的在运行了。\n\n而自动回收的标准，一般常见于接近或达到全局内存水位的情况，系统会尽最大可能进行 Cache 的回收，以确保系统的正常运行：\n\n![image](https://image.eddycjy.com/800df66be75520f982e650b6303bf9e8.jpg)\n\n至此，也就达到了修复这个问题的目的，解决了这一个长达两年的迷之内存漩涡。\n\n## 参考\n\n- [show_bug.cgi?id=1507149#c101](https://bugzilla.redhat.com/show_bug.cgi?id=1507149#c101)\n- [unstable-kmem-accounting](https://pingcap.com/blog/try-to-fix-two-linux-kernel-bugs-while-testing-tidb-operator-in-k8s/#bug-1-unstable-kmem-accounting)\n- [kmem accounting 导致的 cgroup 泄漏问题](https://blog.witd.in/2019/12/09/kmem-accounting%E5%AF%BC%E8%87%B4%E7%9A%84cgroup%E6%B3%84%E6%BC%8F%E9%97%AE%E9%A2%98/)\n- [crash due to k8s 1.9.x](https://github.com/kubernetes/kubernetes/issues/61937)\n- [记一次k8s cgroup内存泄露问题修复](http://www.iceyao.com.cn/2020/01/04/%E8%AE%B0%E4%B8%80%E6%AC%A1k8s-cgroup%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E9%97%AE%E9%A2%98%E8%A7%A3%E5%86%B3/)\n- [Cgroup 泄漏--潜藏在你的集群中](https://tencentcloudcontainerteam.github.io/2018/12/29/cgroup-leaking/)\n"
  },
  {
    "path": "content/posts/why-mq.md",
    "content": "---\ntitle: \"《漫谈 MQ》要消息队列（MQ）有什么用？\"\ndate: 2021-12-31T12:54:50+08:00\ntoc: true\nimages:\ntags: \n  - mq\n---\n\n大家好，我是煎鱼。想是问题，做是答案。\n\n最近我有一个朋友公司踩了不少消息队列（MQ）的坑，让人无奈不已。因此计划写 MQ 系列的技术文章，来科普更多这块的知识。\n\n目前 MQ 也是互联网应用中非常常用的基础组件了，面试特爱问。基本有一定规模的系统都能看见他的踪影。\n\n无论是 RocketMQ、Kafka、RabbitMQ 等，都围绕着根本的设计出发产生不同的高级功能，甚至可能是雷同的设计有 N 个名字。\n\n## 什么是 MQ\n\nMQ 一般代指消息队列（Message Queue）。它是一个抽象层，允许多个进程（可能在不同的机器上）通过各种模式（例如：点对点，发布订阅等）进行通信。\n\n也可以根据不同的实现，它可以被配置为保证可靠性、错误报告、安全、发现、性能等。\n\n## 为什么需要 MQ\n\n在当下 MQ 的必要场景，比较经典的说辞就是 “异步、削峰、解耦”。是各类秒杀系统的设计核心，甚至会作为不少云厂商的卖点，每家都有自己的生态圈。\n\n核心分为三个要点：\n- 解耦。\n- 削峰。\n- 异步。\n\n### 解耦\n\n在业务系统设计中，我们常常会与一个平台系统 A，他汇聚了许许多多的系统的对接。例如，系统 A 作为平台拥有大量用户操作，自然就有非常多的用户行为。\n\n虽然他自己可能不大需要，但是其他子系统就不同了，会要系统 A 来调用他们提供的接口，传输各种行为数据。\n\n其链路依赖如下图：\n\n![多重依赖](https://image.eddycjy.com/84478d2799861f81519ede0ba4ceb91d.jpg)\n\n这时候作为平台方的系统 A 就烦死了，来一个要对接一个，就得安排一个人排工期和迭代。对方还有可能出现系统不稳定，还得关注他们的稳定性和诉求。\n\n但用了 MQ 后就不一样了，如下图：\n\n![MQ 解耦直接依赖](https://image.eddycjy.com/b6c79dbe5c848ed32e347de4042cd9b6.jpg)\n\n系统 A 只需要将消息放到 MQ 中去，不管以后是对接系统 B、C、D、E...，他都不需要太关心，不用一个个对接。用业务同学的话来讲，就是：“自己看文档，去 MQ 里拿，别来骚扰我”。\n\n以此 MQ 达到了系统间解耦的目的。\n\n### 削峰\n\n在 2C 类别的业务系统中，常常会有活动的概念，要面向用户做促销，像我们常见的双 11、618 都是这类营销，也是营销场景。\n\n但这种场景之下，会对系统产生较大的冲击。类似八二原则，也就是 80% 的流量集中在某个时间冲击进来，形成了流量尖峰（高 QPS），系统会因为承受不了如此大的压力，从而宕机。\n\n如下图：\n\n![用户直接并发访问](https://image.eddycjy.com/e396606bb1aec70b43a7016933880f09.jpg)\n\n用户的请求会经由系统 A 直击数据库。当然，在活动场景下的大流量，数据库自然也就撑不住了。\n\n我们可以利用 MQ 做削峰，系统 A 直接把消息写入 MQ，再让系统 B、C、D 自己主动地根据自身情况来 MQ 拉取消息，又或是接受消息的推送：\n\n![利用 MQ 削峰](https://image.eddycjy.com/126ec27324c95ce6311078a8e8849d54.jpg)\n\n这样一来，MQ 作为一个 “转发器”，流量不会直接打到底层，也保证了各业务系统可根据自己的实际负载来决定消费消息的速度，起到流量削峰放缓的作用。\n\n### 异步\n\n在没有引入 MQ 组件的时候，我们系统 A 因业务需求要调好几个接口时，都可能需要专门的写个异步操作，否则就会导致阻塞等待响应过久。\n\n但是使用 MQ 后，系统只需要快速地将消息写入 MQ 中，接着就可以返回了：\n\n![MQ 异步操作](https://image.eddycjy.com/40efbfcc5453aff5b58e85d11fb3d291.jpg)\n\n也就是真正的业务操作，被异步化了。“内部” 的区域是业务系统自身需要关注的，而经由 MQ 的 “外部” 操作与系统 A 无关，自然异步处理也问题不大。\n\n## MQ 三要素\n\nMQ 一共有三个基本角色，分别是：\n- 生产者（Producer）：负责生产消息。\n- 消费者（Consumer）：负责消费消息。\n- 队列（Queue）：负责存储消息。\n\n这么一看，MQ 就是一个很基础的东西，基本就是一发一存一消费的模型：\n\n![MQ 三要素](https://image.eddycjy.com/60a3665e5c14ec8b18a0ec14c31b91b4.jpg)\n\n1. 在 MQ 中传来传去的内容：就是 “消息”，消息是我们业务要传输的数据内容。内容格式为自定义，只要两边商议清楚能解析出来即可。比较常见是像是：JSON、Protobuf 等。\n\n2. 消息在 MQ 的队列组件：承载着传输消息的作用，队列是先进先出的数据结构，生产者的消息入队就是发消息，消费者消费消息，也就是队列出队。\n\n## 消息模型\n\n随着消息的不断规模使用和应用，目前业内常见的有两种模型：\n- 点对点模型。\n- 发布/订阅模型。\n\n### 点对点模型\n\n点对点是 MQ 最初的结构，也就是 “生产者-队列-消费者” 的模型：\n\n![点对点模型](https://image.eddycjy.com/597c3a216c23124f0a0c855b5777a85d.jpg)\n\n生产者将消息写入队列，消费者再从队列中读取出消息出来，完成一个标准动作。\n\n当然，在真实环境中，是允许有多个生产者和多个消费者的，也可以根据不同的业务诉求做队列的隔离。\n\n### 发布/订阅模型\n\n发布/订阅是 MQ 逐渐延伸出来的诉求，因为在实际业务场景中，我们需要将一份消息分发给多个消费者。\n\n多个消费者都要针对这份数据做一些自己的业务处理，那么点对点的就不合适了，除非点对点的开通 N 个队列，但消息的体量可也不少，也不高效，这显然很浪费。\n\n从业务场景来讲，就像平时系统 A 有一份用户行为数据，下游有 N 个系统需要，那咋办。最适合的就是 “发布者-Topic-订阅者” 的模型：\n\n![发布/订阅模型](https://image.eddycjy.com/a64d93b952cb9953af355cf2e56ee1a4.jpg)\n\n在该模型中，“生产者” 变成 “发布者”，“消费者” 变成了 “订阅者”，以往的 “队列” 也改变了他单一的属性，拆成了更多的组件。一般就是 “主题” 了，也就是 Topic 来做消息存储等。\n\n订阅者只需要订阅 Topic，就可以收到发布者每次发布的这个 Topic 的全量消息，以此达到诉求。\n\n## 总结\n\n在今天这篇文章中，我们介绍了消息队列（MQ）的相关核心内容：\n- MQ 基本介绍和说明。\n- MQ 常见的三要素和三大特性进行了分析说明\n- MQ 常见的 “点对点” 和 “发布/订阅” 模型。\n\nMQ 总是有利有弊的，在初步了解后，接下来的文章我们将会持续剖析，欢迎关注煎鱼，继续学习 ：）\n\n## 参考\n- [What is an MQ and why do I want to use it?](https://stackoverflow.com/questions/2868800/what-is-an-mq-and-why-do-i-want-to-use-it)\n- [消息队列（mq）是什么？](https://www.zhihu.com/question/54152397)\n- [MQ 消息队列的解耦、接口异步处理、削峰](https://blog.csdn.net/maihilton/article/details/81628152)"
  },
  {
    "path": "content/prometheus-categories.md",
    "content": "---\ntitle: \"《跟煎鱼学 Prometheus》\"\ndate: \"2020-05-16\"\n---\n\n请注意，这不是成品，随时可能会对以前的章节进行修改。那么为什么要放出来呢，当然是为了催更自己。\n\n1. [Prometheus 快速入门](/posts/prometheus/2020-05-16-startup)\n2. [Prometheus 四大度量指标的了解和应用](/posts/prometheus/2020-05-16-metrics)\n3. [使用 Prometheus 对 Go 程序进行指标采集](/posts/prometheus/2020-05-16-pull)\n\n### 我的公众号\n\n平时喜欢分享 Go 语言、微服务架构和奇怪的系统设计，欢迎关注我的公众号：\n\n![image](https://image.eddycjy.com/7074be90379a121746146bc4229819f8.jpg)"
  },
  {
    "path": "layouts/_default/baseof.html",
    "content": "<!DOCTYPE html>\n<html lang=\"{{.Site.LanguageCode}}\">\n\n<head>\n\t<meta charset=\"UTF-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\n\t{{- with .Site.Params.themeColor }}\n\t<meta name=\"theme-color\" content=\"{{.}}\">\n\t<meta name=\"msapplication-TileColor\" content=\"{{.}}\">\n\t{{- end }}\n\t{{- partial \"structured-data.html\" . }}\n\t{{- partial \"favicons.html\" }}\n\t<title>{{.Title}}</title>\n\t{{ range .AlternativeOutputFormats -}}\n\t\t{{ printf `<link rel=\"%s\" type=\"%s+%s\" href=\"%s\" title=\"%s\" />` .Rel .MediaType.Type .MediaType.Suffix .Permalink $.Site.Title | safeHTML }}\n\t{{ end -}}\n\t{{ $style := resources.Get \"scss/style.scss\" | resources.ExecuteAsTemplate \"css/style.css\" . | toCSS | minify | fingerprint -}}\n\t<link rel=\"stylesheet\" href=\"{{ $style.Permalink }}\">\n\t{{- block \"head\" . -}}{{- end }}\n\t{{- range .Site.Params.customCSS }}\n\t<link rel=\"stylesheet\" href=\"{{ . | absURL }}\">\n\t{{- end }}\n\t{{- if templates.Exists \"partials/extra-head.html\" -}}\n\t{{ partial \"extra-head.html\" . }}\n\t{{- end }}\n</head>\n\n<body id=\"page\">\n\t{{ block \"header\" . -}}{{ end -}}\n\t{{ block \"main\" . -}}{{ end -}}\n\t{{ block \"footer\" . -}}{{ end }}\n\t{{ $script := resources.Get \"js/main.js\" | minify | fingerprint -}}\n\t<script src=\"{{ $script.Permalink }}\"></script>\n\t{{- partial \"analytics.html\" . }}\n\t{{- if templates.Exists \"partials/extra-foot.html\" -}}\n\t{{ partial \"extra-foot.html\" . }}\n\t{{- end }}\n</body>\n\n</html>\n"
  },
  {
    "path": "layouts/_default/list.html",
    "content": "{{ define \"header\" }}\n{{ partialCached \"header.html\" . }}\n{{ end }}\n\n{{ define \"main\" }}\n\t<main class=\"site-main section-inner thin animated fadeIn faster\">\n\t\t<h1>{{ .Title }}</h1>\n\t\t{{- if .Content }}\n\t\t<div class=\"content\">\n\t\t\t{{ .Content }}\n\t\t</div>\n\t\t{{- end }}\n\t\t{{- range .Data.Pages.GroupByDate \"2006\" }}\n\t\t<div class=\"posts-group\">\n\t\t\t<div class=\"post-year\" id=\"{{ .Key }}\">{{ .Key }}</div>\n\t\t\t<ul class=\"posts-list\">\n\t\t\t\t{{- range .Pages }}\n\t\t\t\t<li class=\"post-item\">\n\t\t\t\t\t<a href=\"{{.Permalink}}\">\n\t\t\t\t\t\t<span class=\"post-title\">{{.Title}}</span>\n\t\t\t\t\t\t<span class=\"post-day\">{{ .Date.Format .Site.Params.dateformShort }}</span>\n\t\t\t\t\t</a>\n\t\t\t\t</li>\n\t\t\t\t{{- end }}\n\t\t\t</ul>\n\t\t</div>\n\t\t{{- end }}\n\t</main>\n{{ end }}\n\n{{ define \"footer\" }}\n{{ partialCached \"footer.html\" . }}\n{{ end }}\n"
  },
  {
    "path": "layouts/_default/single.html",
    "content": "{{ define \"head\" }}\n\t{{ if .Params.featuredImg -}}\n\t<style>.bg-img {background-image: url('{{.Params.featuredImg}}');}</style>\n\t{{- else if .Params.images -}}\n\t\t{{- range first 1 .Params.images -}}\n\t\t<style>.bg-img {background-image: url('{{. | absURL}}');}</style>\n\t\t{{- end -}}\n\t{{- end -}}\n{{ end }}\n\n{{ define \"header\" }}\n{{ partial \"header.html\" . }}\n{{ end }}\n\n{{ define \"main\" }}\n\t{{- if (or .Params.images .Params.featuredImg) }}\n\t<div class=\"bg-img\"></div>\n\t{{- end }}\n\t<main class=\"site-main section-inner thin animated fadeIn faster\">\n\t\t<h1>{{ .Title }}</h1>\n\t\t<div class=\"content\">\n\t\t\t{{ .Content | replaceRE \"(<h[1-6] id=\\\"([^\\\"]+)\\\".+)(</h[1-6]+>)\" `${1}<a href=\"#${2}\" class=\"anchor\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3\"></path><line x1=\"8\" y1=\"12\" x2=\"16\" y2=\"12\"></line></svg></a>${3}` | safeHTML }}\n\t\t</div>\n\t\t<div id=\"comments\" class=\"thin\">\n\t\t\t{{ partial \"comments.html\" . }}\n\t\t</div>\n\t</main>\n{{ end }}\n\n{{ define \"footer\" }}\n{{ partialCached \"footer.html\" . }}\n{{ end }}\n"
  },
  {
    "path": "layouts/index.html",
    "content": "{{ define \"header\" }}\n{{ partialCached \"header.html\" . }}\n{{ end }}\n\n{{ define \"main\" }}\n\t\t<main class=\"site-main section-inner thin animated fadeIn faster\">\n    {{ $paginator := .Paginate (where .Site.RegularPages \"Type\" \"in\" .Site.Params.mainSections) }}\n\t\t{{ range $paginator.Pages }}\n        <div class=\"post animated fadeInDown\">\n            <div class=\"post-title\">\n                <h2><a href=\"{{ .RelPermalink }}\">{{ .Title }}</a>\n                </h2>\n            </div>\n            <div class=\"post-content\">\n                <div class=\"p_part\"><p>{{ .Summary }}</p></div>\n            </div>\n            <div class=\"post-footer\">\n                <div class=\"meta\">\n                    <div class=\"info\"><em class=\"fas fa-calendar-day\"></em><span\n                                class=\"date\">{{ .Date.Format .Site.Params.dateformNum }}</span>\n                        {{ with .Params.tags }}\n                            {{- range $index, $el := . -}}\n                                <a class=\"tag\"\n                                   href=\"{{ ( printf \"tags/%s/\" ( . | urlize ) ) | relLangURL }}\">{{ . }}</a>\n                            {{- end -}}\n                        {{ end }}\n                    </div>\n                </div>\n            </div>\n\t\t\t\t</div>\n    {{ end }}\n    <div class=\"pagination\">\n        {{ template \"_internal/pagination.html\" . }}\n    </div>\n\t\t</main>\n{{ end }}\n\n\n{{ define \"footer\" }}\n{{ partialCached \"footer.html\" . }}\n{{ end }}\n"
  },
  {
    "path": "layouts/partials/analytics.html",
    "content": "{{ template \"_internal/google_analytics_async.html\" . }}\n"
  },
  {
    "path": "layouts/partials/comments.html",
    "content": "\t\t\t<script src=\"https://utteranc.es/client.js\"\n\t\t\t\t\t\t\trepo=\"eddycjy/blog\"\n\t\t\t\t\t\t\tissue-term=\"pathname\"\n\t\t\t\t\t\t\ttheme=\"github-light\"\n\t\t\t\t\t\t\tcrossorigin=\"anonymous\"\n\t\t\t\t\t\t\tasync>\n\t\t\t</script>"
  },
  {
    "path": "layouts/partials/favicons.html",
    "content": "\t<link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"{{\"apple-touch-icon.png\" | relURL}}\">\n\t<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"{{\"favicon-32x32.png\" | relURL}}\">\n\t<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"{{\"favicon-16x16.png\" | relURL}}\">\n\t<link rel=\"manifest\" href=\"{{\"site.webmanifest\" | relURL}}\">\n\t<link rel=\"mask-icon\" href=\"{{\"safari-pinned-tab.svg\" | relURL}}\" color=\"{{.Site.Params.themeColor}}\">\n\t<link rel=\"shortcut icon\" href=\"{{\"favicon.ico\" | relURL}}\">\n"
  },
  {
    "path": "layouts/partials/footer.html",
    "content": "\t<footer id=\"site-footer\" class=\"section-inner thin animated fadeIn faster\">\n\t\t<p>&copy; 2018 - {{ now.Format \"2006\" }} <a href=\"{{ .Site.BaseURL }}\">{{ .Site.Author.name }}</a>{{ .Site.Params.footerCopyright | safeHTML }}</p>\n\t\t<p>\n\t\t\tMade with <a href=\"https://gohugo.io/\" target=\"_blank\" rel=\"noopener\">Hugo</a> &#183; Theme <a href=\"https://github.com/Track3/hermit\" target=\"_blank\" rel=\"noopener\">Hermit</a>\n\t\t\t{{- with (not (in (.Site.Language.Get \"disableKinds\") \"RSS\")) }} &#183; <a href=\"{{ \"posts/index.xml\" | absURL }}\" target=\"_blank\" title=\"rss\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-rss\"><path d=\"M4 11a9 9 0 0 1 9 9\"></path><path d=\"M4 4a16 16 0 0 1 16 16\"></path><circle cx=\"5\" cy=\"19\" r=\"1\"></circle></svg></a>{{ end }}\n\t\t</p>\n\t</footer>\n\n<script src=\"https://readmore.openwrite.cn/js/readmore.js\" type=\"text/javascript\"></script>\n<script>\n    const btw = new BTWPlugin();\n    btw.init({\n        id: 'articles',\n        blogId: '15881-1622044386133-596',\n        name: '脑子进煎鱼了',\n        qrcode: 'https://image.eddycjy.com/7074be90379a121746146bc4229819f8.jpg',\n        keyword: '666',\n    });\n</script>"
  },
  {
    "path": "layouts/partials/header.html",
    "content": "\t<header id=\"site-header\" class=\"animated slideInUp faster\">\n\t\t<div class=\"hdr-wrapper section-inner\">\n\t\t\t<div class=\"hdr-left\">\n\t\t\t\t<div class=\"site-branding\">\n\t\t\t\t\t<a href=\"{{.Site.BaseURL}}\">{{ .Site.Title }}</a>\n\t\t\t\t</div>\n\t\t\t\t<nav class=\"site-nav hide-in-mobile\">\n\t\t\t\t\t{{- range .Site.Menus.nav }}\n\t\t\t\t\t<a href=\"{{ .URL | absLangURL}}\">{{ .Name }}</a>\n\t\t\t\t\t{{- end }}\n\t\t\t\t</nav>\n\t\t\t</div>\n\t\t\t<div class=\"hdr-right hdr-icons\">\n\t\t\t\t{{ if (or .Params.images .Params.featuredImg) -}}\n\t\t\t\t<button id=\"img-btn\" class=\"hdr-btn\" title=\"{{i18n \"featuredImage\"}}\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-image\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\"></rect><circle cx=\"8.5\" cy=\"8.5\" r=\"1.5\"></circle><polyline points=\"21 15 16 10 5 21\"></polyline></svg></button>\n\t\t\t\t{{- end }}\n\t\t\t\t{{- with .Params.toc -}}\n\t\t\t\t<button id=\"toc-btn\" class=\"hdr-btn desktop-only-ib\" title=\"{{i18n \"tableOfContents\"}}\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-list\"><line x1=\"8\" y1=\"6\" x2=\"21\" y2=\"6\"></line><line x1=\"8\" y1=\"12\" x2=\"21\" y2=\"12\"></line><line x1=\"8\" y1=\"18\" x2=\"21\" y2=\"18\"></line><line x1=\"3\" y1=\"6\" x2=\"3\" y2=\"6\"></line><line x1=\"3\" y1=\"12\" x2=\"3\" y2=\"12\"></line><line x1=\"3\" y1=\"18\" x2=\"3\" y2=\"18\"></line></svg></button>\n\t\t\t\t{{- end }}\n\t\t\t\t{{- with .Site.Params.social -}}\n\t\t\t\t<span class=\"hdr-social hide-in-mobile\">{{ partialCached \"social-icons.html\" . }}</span>\n\t\t\t\t{{- end -}}\n\t\t\t\t<button id=\"menu-btn\" class=\"hdr-btn\" title=\"{{i18n \"menu\"}}\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-menu\"><line x1=\"3\" y1=\"12\" x2=\"21\" y2=\"12\"></line><line x1=\"3\" y1=\"6\" x2=\"21\" y2=\"6\"></line><line x1=\"3\" y1=\"18\" x2=\"21\" y2=\"18\"></line></svg></button>\n\t\t\t</div>\n\t\t</div>\n\t</header>\n\t<div id=\"mobile-menu\" class=\"animated fast\">\n\t\t<ul>\n\t\t\t{{- range .Site.Menus.main }}\n\t\t\t<li><a href=\"{{ .URL | absLangURL }}\">{{ .Name }}</a></li>\n\t\t\t{{- end }}\n\t\t</ul>\n\t</div>\n"
  },
  {
    "path": "layouts/partials/social-icons.html",
    "content": "{{ range . -}}\n<a href=\"{{ .url }}\" target=\"_blank\" rel=\"noopener me\" title=\"{{ .name | humanize }}\">{{ partial \"svg.html\" . }}</a>\n{{- end -}}\n"
  },
  {
    "path": "layouts/partials/structured-data.html",
    "content": "{{/* We use some Hugo built-in templates, you can find their source here: */}}\n{{/* https://github.com/gohugoio/hugo/tree/master/tpl/tplimpl/embedded/templates */}}\n\n{{- template \"_internal/schema.html\" . }}\n{{- template \"_internal/opengraph.html\" . }}\n{{- template \"_internal/twitter_cards.html\" . }}\n"
  },
  {
    "path": "layouts/partials/svg.html",
    "content": "{{- if (eq .name \"codepen\") -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-codepen\"><polygon points=\"12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5 12 2\"></polygon><line x1=\"12\" y1=\"22\" x2=\"12\" y2=\"15.5\"></line><polyline points=\"22 8.5 12 15.5 2 8.5\"></polyline><polyline points=\"2 15.5 12 8.5 22 15.5\"></polyline><line x1=\"12\" y1=\"2\" x2=\"12\" y2=\"8.5\"></line></svg>\n{{- else if (eq .name \"facebook\") -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-facebook\"><path d=\"M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z\"></path></svg>\n{{- else if (eq .name \"github\") -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-github\"><path d=\"M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22\"></path></svg>\n{{- else if (eq .name \"gitlab\") -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-gitlab\"><path d=\"M22.65 14.39L12 22.13 1.35 14.39a.84.84 0 0 1-.3-.94l1.22-3.78 2.44-7.51A.42.42 0 0 1 4.82 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.49h8.1l2.44-7.51A.42.42 0 0 1 18.6 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.51L23 13.45a.84.84 0 0 1-.35.94z\"></path></svg>\n{{- else if (eq .name \"instagram\") -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-instagram\"><rect x=\"2\" y=\"2\" width=\"20\" height=\"20\" rx=\"5\" ry=\"5\"></rect><path d=\"M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z\"></path><line x1=\"17.5\" y1=\"6.5\" x2=\"17.5\" y2=\"6.5\"></line></svg>\n{{- else if (eq .name \"linkedin\") -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-linkedin\"><path d=\"M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z\"></path><rect x=\"2\" y=\"9\" width=\"4\" height=\"12\"></rect><circle cx=\"4\" cy=\"4\" r=\"2\"></circle></svg>\n{{- else if (eq .name \"slack\") -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-slack\"><path d=\"M22.08 9C19.81 1.41 16.54-.35 9 1.92S-.35 7.46 1.92 15 7.46 24.35 15 22.08 24.35 16.54 22.08 9z\"></path><line x1=\"12.57\" y1=\"5.99\" x2=\"16.15\" y2=\"16.39\"></line><line x1=\"7.85\" y1=\"7.61\" x2=\"11.43\" y2=\"18.01\"></line><line x1=\"16.39\" y1=\"7.85\" x2=\"5.99\" y2=\"11.43\"></line><line x1=\"18.01\" y1=\"12.57\" x2=\"7.61\" y2=\"16.15\"></line></svg>\n{{- else if (eq .name \"telegram\") -}}\n<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" aria-hidden=\"true\" class=\"feather\"><path d=\"m 22.05,1.577 c -0.393,-0.016 -0.784,0.08 -1.117,0.235 -0.484,0.186 -4.92,1.902 -9.41,3.64 C 9.263,6.325 7.005,7.198 5.267,7.867 3.53,8.537 2.222,9.035 2.153,9.059 c -0.46,0.16 -1.082,0.362 -1.61,0.984 -0.79581202,1.058365 0.21077405,1.964825 1.004,2.499 1.76,0.564 3.58,1.102 5.087,1.608 0.556,1.96 1.09,3.927 1.618,5.89 0.174,0.394 0.553,0.54 0.944,0.544 l -0.002,0.02 c 0,0 0.307,0.03 0.606,-0.042 0.3,-0.07 0.677,-0.244 1.02,-0.565 0.377,-0.354 1.4,-1.36 1.98,-1.928 l 4.37,3.226 0.035,0.02 c 0,0 0.484,0.34 1.192,0.388 0.354,0.024 0.82,-0.044 1.22,-0.337 0.403,-0.294 0.67,-0.767 0.795,-1.307 0.374,-1.63 2.853,-13.427 3.276,-15.38 L 23.676,4.725 C 23.972,3.625 23.863,2.617 23.18,2.02 22.838,1.723 22.444,1.593 22.05,1.576 Z\"></path></svg>\n{{- else if (eq .name \"twitter\") -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-twitter\"><path d=\"M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z\"></path></svg>\n{{- else if (eq .name \"youtube\") -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-youtube\"><path d=\"M22.54 6.42a2.78 2.78 0 0 0-1.94-2C18.88 4 12 4 12 4s-6.88 0-8.6.46a2.78 2.78 0 0 0-1.94 2A29 29 0 0 0 1 11.75a29 29 0 0 0 .46 5.33A2.78 2.78 0 0 0 3.4 19c1.72.46 8.6.46 8.6.46s6.88 0 8.6-.46a2.78 2.78 0 0 0 1.94-2 29 29 0 0 0 .46-5.25 29 29 0 0 0-.46-5.33z\"></path><polygon points=\"9.75 15.02 15.5 11.75 9.75 8.48 9.75 15.02\"></polygon></svg>\n{{- else if (eq .name \"email\") -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-mail\"><path d=\"M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z\"></path><polyline points=\"22,6 12,13 2,6\"></polyline></svg>\n{{- else -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-link\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"></path><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"></path></svg>\n{{- end -}}\n"
  },
  {
    "path": "layouts/posts/rss.xml",
    "content": "<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\">\n\t<channel>\n\t\t<title>{{ if eq  .Title  .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }}</title>\n\t\t<link>{{ .Permalink }}</link>\n\t\t<description>Recent content {{ if ne  .Title  .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }}</description>\n\t\t<generator>Hugo -- gohugo.io</generator>{{ with .Site.LanguageCode }}\n\t\t<language>{{.}}</language>{{end}}{{ with .Site.Author.email }}\n\t\t<managingEditor>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</managingEditor>{{end}}{{ with .Site.Author.email }}\n\t\t<webMaster>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</webMaster>{{end}}{{ with .Site.Copyright }}\n\t\t<copyright>{{.}}</copyright>{{end}}{{ if not .Date.IsZero }}\n\t\t<lastBuildDate>{{ .Date.Format \"Mon, 02 Jan 2006 15:04:05 -0700\" | safeHTML }}</lastBuildDate>{{ end }}\n\t\t{{ with .OutputFormats.Get \"RSS\" -}}\n\t\t\t{{ printf \"<atom:link href=%q rel=\\\"self\\\" type=%q />\" .Permalink .MediaType | safeHTML }}\n\t\t{{ end -}}\n\t\t{{ range .Pages }}\n\t\t<item>\n\t\t\t<title>{{ .Title }}</title>\n\t\t\t<link>{{ .Permalink }}</link>\n\t\t\t<pubDate>{{ .Date.Format \"Mon, 02 Jan 2006 15:04:05 -0700\" | safeHTML }}</pubDate>\n\t\t\t{{ with .Site.Author.email }}<author>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</author>{{end}}\n\t\t\t<guid>{{ .Permalink }}</guid>\n\t\t\t<description>{{ .Summary | html }}</description>\n\t\t\t<content type=\"html\">{{ printf `<![CDATA[%s]]>` .Content | safeHTML }}</content>\n\t\t</item>\n\t\t{{ end }}\n\t</channel>\n</rss>\n"
  },
  {
    "path": "layouts/posts/single.html",
    "content": "{{ define \"head\" }}\n\t{{ if .Params.featuredImg -}}\n\t<style>.bg-img {background-image: url('{{.Params.featuredImg}}');}</style>\n\t{{- else if .Params.images -}}\n\t\t{{- range first 1 .Params.images -}}\n\t\t<style>.bg-img {background-image: url('{{. | absURL}}');}</style>\n\t\t{{- end -}}\n\t{{- end -}}\n{{ end }}\n\n{{ define \"header\" }}\n{{ partial \"header.html\" . }}\n{{ end }}\n\n{{ define \"main\" }}\n\t{{- if (or .Params.images .Params.featuredImg) }}\n\t<div class=\"bg-img\"></div>\n\t{{- end }}\n\t<main class=\"site-main section-inner animated fadeIn faster\">\n\t\t<article id=\"articles\" class=\"thin\">\n\t\t\t<header class=\"post-header\">\n\t\t\t\t<div class=\"post-meta\"><span>{{ .Date.Format .Site.Params.dateform }}</span></div>\n\t\t\t\t<h1>{{ .Title }}</h1>\n\t\t\t</header>\n\t\t\t<div class=\"content\">\n\t\t\t\t{{ .Content | replaceRE \"(<h[1-6] id=\\\"([^\\\"]+)\\\".+)(</h[1-6]+>)\" `${1}<a href=\"#${2}\" class=\"anchor\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3\"></path><line x1=\"8\" y1=\"12\" x2=\"16\" y2=\"12\"></line></svg></a>${3}` | safeHTML }}\n\t\t\t</div>\n\t\t\t<div class=\"content\"><br><img src=\"https://image.eddycjy.com/8c23cb407498f8446af59e43fb95ac62.png\"></div>\n\t\t\t<hr class=\"post-end\">\n\t\t\t<footer class=\"post-info\">\n\t\t\t\t{{- with .Params.tags }}\n\t\t\t\t<p>\n\t\t\t\t\t<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-tag meta-icon\"><path d=\"M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z\"></path><line x1=\"7\" y1=\"7\" x2=\"7\" y2=\"7\"></line></svg>\n\t\t\t\t\t{{- range . -}}\n\t\t\t\t\t<span class=\"tag\"><a href=\"{{ \"tags/\" | absURL }}{{ . | urlize }}\">{{.}}</a></span>\n\t\t\t\t\t{{- end }}\n\t\t\t\t</p>\n\t\t\t\t{{- end }}\n\t\t\t\t<p><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-file-text\"><path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"></path><polyline points=\"14 2 14 8 20 8\"></polyline><line x1=\"16\" y1=\"13\" x2=\"8\" y2=\"13\"></line><line x1=\"16\" y1=\"17\" x2=\"8\" y2=\"17\"></line><polyline points=\"10 9 9 9 8 9\"></polyline></svg>{{ i18n \"wordCount\" . }}</p>\n\t\t\t\t<p><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-calendar\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\"></rect><line x1=\"16\" y1=\"2\" x2=\"16\" y2=\"6\"></line><line x1=\"8\" y1=\"2\" x2=\"8\" y2=\"6\"></line><line x1=\"3\" y1=\"10\" x2=\"21\" y2=\"10\"></line></svg>{{ dateFormat .Site.Params.dateformNumTime .Date.Local }}</p>\n\t\t\t\t{{- if and .GitInfo .Site.Params.gitUrl }}\n\t\t\t\t<p><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-git-commit\"><circle cx=\"12\" cy=\"12\" r=\"4\"></circle><line x1=\"1.05\" y1=\"12\" x2=\"7\" y2=\"12\"></line><line x1=\"17.01\" y1=\"12\" x2=\"22.96\" y2=\"12\"></line></svg><a href=\"{{ .Site.Params.gitUrl -}}{{ .GitInfo.Hash }}\" target=\"_blank\" rel=\"noopener\">{{ .GitInfo.AbbreviatedHash }}</a> @ {{ dateFormat .Site.Params.dateformNum .GitInfo.AuthorDate.Local }}</p>\n\t\t\t\t{{- end }}\n\t\t\t</footer>\n\t\t</article>\n\t\t{{- if .Params.toc }}\n\t\t<aside id=\"toc\" class=\"show-toc\">\n\t\t\t<div class=\"toc-title\">{{ i18n \"tableOfContents\" }}</div>\n\t\t\t{{ .TableOfContents }}\n\t\t</aside>\n\t\t{{- end }}\n\t\t<div class=\"post-nav thin\">\n\t\t\t{{- with .NextInSection }}\n\t\t\t<a class=\"next-post\" href=\"{{ .Permalink }}\">\n\t\t\t\t<span class=\"post-nav-label\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-arrow-left\"><line x1=\"19\" y1=\"12\" x2=\"5\" y2=\"12\"></line><polyline points=\"12 19 5 12 12 5\"></polyline></svg>&nbsp;{{ i18n \"newer\" }}</span><br><span>{{ .Title }}</span>\n\t\t\t</a>\n\t\t\t{{- end }}\n\t\t\t{{- with .PrevInSection }}\n\t\t\t<a class=\"prev-post\" href=\"{{ .Permalink }}\">\n\t\t\t\t<span class=\"post-nav-label\">{{ i18n \"older\" }}&nbsp;<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-arrow-right\"><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"></line><polyline points=\"12 5 19 12 12 19\"></polyline></svg></span><br><span>{{ .Title }}</span>\n\t\t\t</a>\n\t\t\t{{- end }}\n\t\t</div>\n\t\t<div id=\"comments\" class=\"thin\">\n\t\t\t{{ partialCached \"comments.html\" . }}\n\t\t</div>\n\t</main>\n{{ end }}\n\n{{ define \"footer\" }}\n{{ partialCached \"footer.html\" . }}\n{{ end }}\n"
  },
  {
    "path": "resources/_gen/assets/scss/scss/style.scss_c16d144eee185fbddd582cd5e25a4fae.content",
    "content": "@charset \"UTF-8\";/*!normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css*/html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}.chroma{color:#eee;background-color:#2c3e50}.chroma .err{color:#960050;background-color:#1e0010}.chroma .lntd{vertical-align:top;padding:0;margin:0;border:0}.chroma .lntable{border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block}.chroma .hl{display:block;width:100%;background-color:#ffc}.chroma .lnt{margin-right:.4em;padding:0 .4em}.chroma .ln{margin-right:.4em;padding:0 .4em}.chroma .k{color:#66d9ef}.chroma .kc{color:#66d9ef}.chroma .kd{color:#66d9ef}.chroma .kn{color:#f92672}.chroma .kp{color:#66d9ef}.chroma .kr{color:#66d9ef}.chroma .kt{color:#66d9ef}.chroma .na{color:#a6e22e}.chroma .nc{color:#a6e22e}.chroma .no{color:#66d9ef}.chroma .nd{color:#a6e22e}.chroma .ne{color:#a6e22e}.chroma .nf{color:#a6e22e}.chroma .nx{color:#a6e22e}.chroma .nt{color:#f92672}.chroma .l{color:#ae81ff}.chroma .ld{color:#e6db74}.chroma .s{color:#e6db74}.chroma .sa{color:#e6db74}.chroma .sb{color:#e6db74}.chroma .sc{color:#e6db74}.chroma .dl{color:#e6db74}.chroma .sd{color:#e6db74}.chroma .s2{color:#e6db74}.chroma .se{color:#ae81ff}.chroma .sh{color:#e6db74}.chroma .si{color:#e6db74}.chroma .sx{color:#e6db74}.chroma .sr{color:#e6db74}.chroma .s1{color:#e6db74}.chroma .ss{color:#e6db74}.chroma .m{color:#ae81ff}.chroma .mb{color:#ae81ff}.chroma .mf{color:#ae81ff}.chroma .mh{color:#ae81ff}.chroma .mi{color:#ae81ff}.chroma .il{color:#ae81ff}.chroma .mo{color:#ae81ff}.chroma .o{color:#f92672}.chroma .ow{color:#f92672}.chroma .c{color:#75715e}.chroma .ch{color:#75715e}.chroma .cm{color:#75715e}.chroma .c1{color:#75715e}.chroma .cs{color:#75715e}.chroma .cp{color:#75715e}.chroma .cpf{color:#75715e}.chroma .gd{color:#f92672}.chroma .ge{font-style:italic}.chroma .gi{color:#a6e22e}.chroma .gs{font-weight:700}.chroma .gu{color:#75715e}/*!* animate.css -https://daneden.github.io/animate.css/\n* Version - 3.7.0\n* Licensed under the MIT license - http://opensource.org/licenses/MIT\n*\n* Copyright (c) 2019 Daniel Eden*/@-webkit-keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}@keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}.flash{-webkit-animation-name:flash;animation-name:flash}@-webkit-keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(0.215,0.61,0.355,1);animation-timing-function:cubic-bezier(0.215,0.61,0.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(0.215,0.61,0.355,1);animation-timing-function:cubic-bezier(0.215,0.61,0.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.bounceInRight{-webkit-animation-name:bounceInRight;animation-name:bounceInRight}@-webkit-keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.bounceOutRight{-webkit-animation-name:bounceOutRight;animation-name:bounceOutRight}@-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInUp{-webkit-animation-name:slideInUp;animation-name:slideInUp}@-webkit-keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.slideOutDown{-webkit-animation-name:slideOutDown;animation-name:slideOutDown}.animated{-webkit-animation-fill-mode:both;animation-fill-mode:both}.animated.infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.animated.delay-1s{-webkit-animation-delay:1s;animation-delay:1s}.animated.delay-2s{-webkit-animation-delay:2s;animation-delay:2s}.animated.delay-3s{-webkit-animation-delay:3s;animation-delay:3s}.animated.delay-4s{-webkit-animation-delay:4s;animation-delay:4s}.animated.delay-5s{-webkit-animation-delay:5s;animation-delay:5s}.animated.fast{-webkit-animation-duration:.8s;animation-duration:.8s}.animated.slow{-webkit-animation-duration:2s;animation-duration:2s}.animated.slower{-webkit-animation-duration:3s;animation-duration:3s}@media(prefers-reduced-motion),(print){.animated{-webkit-animation:unset!important;animation:unset!important;-webkit-transition:none!important;transition:none!important}}::-webkit-scrollbar{width:8px;height:8px;background:#2c3e50}::-webkit-scrollbar-thumb{background:#888}::-webkit-scrollbar-thumb:hover{background:#3b3e48}html{background:#fcfcfc;line-height:1.6;letter-spacing:.05em;scroll-behavior:smooth}body,button,input,select,textarea{color:#3b3e48;font-family:trebuchet ms,Verdana,verdana ref,segoe ui,Candara,lucida grande,lucida sans unicode,lucida sans,Tahoma,sans-serif}pre,code,pre tt{font-family:Consolas,andale mono wt,andale mono,Menlo,Monaco,lucida console,lucida sans typewriter,dejavu sans mono,bitstream vera sans mono,liberation mono,nimbus mono l,courier new,Courier,yahei consolas hybrid,monospace,segoe ui emoji,pingfang sc,microsoft yahei}pre{padding:.7em 1.1em;overflow:auto;font-size:.9em;line-height:1.5;letter-spacing:normal;white-space:pre;color:#eee;background:#2c3e50;border-radius:4px}pre code{color:#eee;padding:0;margin:0;background:#2c3e50}code{color:#2c3e50;background:#eee;border-radius:3px;padding:0 3px;margin:0 4px;word-wrap:break-word;letter-spacing:normal}blockquote{border-left:.25em solid;margin:1em;padding:0 1em;font-style:italic}blockquote cite{font-weight:700;font-style:normal}blockquote cite::before{content:\"—— \"}a{color:#3b3e48;text-decoration:none;border:none;transition-property:color;transition-duration:.4s;transition-timing-function:ease-out}a:hover{color:#000}hr{opacity:.2;border-width:0 0 5px;border-style:dashed;background:0 0;width:50%;margin:1.8em auto}table{border-collapse:collapse;border-spacing:0;empty-cells:show;width:100%;max-width:100%}table th,table td{padding:1.5%;border:1px solid}table th{font-weight:700;vertical-align:bottom}.section-inner{margin:0 auto;max-width:1200px;width:93%}.thin{max-width:900px;margin:auto}.feather{display:inline-block;vertical-align:-.125em;width:1em;height:1em}.desktop-only,.desktop-only-ib{display:none}.screen-reader-text{border:0;clip:rect(1px,1px,1px,1px);clip-path:inset(50%);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute!important;width:1px;word-wrap:normal!important}.screen-reader-text:focus{background-color:#f1f1f1;border-radius:3px;box-shadow:0 0 2px 2px rgba(0,0,0,.6);clip:auto!important;clip-path:none;color:#21759b;display:block;font-size:14px;font-size:.875rem;font-weight:700;height:auto;left:5px;line-height:normal;padding:15px 23px 14px;text-decoration:none;top:5px;width:auto;z-index:100000}#site-header{z-index:1;width:100%;box-sizing:border-box;box-shadow:-1px 2px 3px rgba(0,0,0,.45);background-color:#f9f9f9}.hdr-wrapper{display:flex;justify-content:space-between;align-items:center;padding:.5em 0;font-size:1.2rem}.hdr-wrapper .site-branding{display:inline-block;margin-right:.8em;font-size:1.2em}.hdr-wrapper .site-nav{display:inline-block;font-size:1.1em;opacity:.8}.hdr-wrapper .site-nav a{margin-left:.8em}.hdr-icons{font-size:1.2em}.hdr-social{display:inline-block;margin-left:.6em}.hdr-social>a{margin-left:.4em}.hdr-btn{border:none;background:0 0;padding:0;margin-left:.4em;cursor:pointer}#menu-btn{display:none;margin-left:.6em;cursor:pointer}#mobile-menu{position:fixed;bottom:4.8em;right:1.5em;display:none;padding:.6em 1.8em;z-index:1;box-sizing:border-box;box-shadow:-1px -2px 3px 0 rgba(0,0,0,.45);background-color:#f9f9f9}#mobile-menu ul{list-style:none;margin:0;padding:0;line-height:2;font-size:1.2em}#site-footer{text-align:center;font-size:.9em;margin-bottom:96px;margin-top:64px}#site-footer p{margin:0}#spotlight{display:flex;height:100vh;flex-direction:column;align-items:center;justify-content:center;max-width:93%;margin:auto;font-size:1.5rem}#spotlight.error-404{flex-direction:row;line-height:normal}p.img-404{margin:0}p.img-404 svg{width:180px;max-width:100%;height:auto}.banner-404{margin-left:2em}.banner-404 h1{font-size:3em;margin:.5rem 0}.banner-404 p{margin-top:0}.banner-404 .btn-404{font-size:.8em}.banner-404 .btn-404 a{display:inline-block;border:2px solid #3b3e48;border-radius:5px;padding:5px;transition-property:color,border-color}.banner-404 .btn-404 a:first-child{margin-right:1em}.banner-404 .btn-404 a:hover{border-color:#000}.banner-404 .btn-404 a svg{margin-right:.5em}#home-center{display:flex;flex-grow:1;flex-direction:column;justify-content:center}#home-title{margin:0;text-align:center}#home-subtitle{margin-top:0;margin-bottom:1.5em;text-align:center;line-height:normal;font-size:.7em;font-style:italic;opacity:.9}#home-social{font-size:1.4em;text-align:center;opacity:.8}#home-social a{margin:0 .2em}#home-nav{opacity:.8}#home-nav a{display:block;text-align:center;margin-top:.5em}#home-footer{text-align:center;font-size:.6em;line-height:normal;opacity:.6}#home-footer p{margin-top:0}.posts-group{display:flex;margin-bottom:1.9em;line-height:normal}.posts-group .post-year{padding-top:6px;margin-right:1.8em;font-size:1.6em;opacity:.6}.posts-group .post-year:hover{text-decoration:underline;cursor:pointer}.posts-group .posts-list{flex-grow:1;margin:0;padding:0;list-style:none}.posts-group .post-item{border-bottom:1px #7d828a dashed}.posts-group .post-item a{display:flex;justify-content:space-between;align-items:baseline;padding:12px 0}.posts-group .post-day{flex-shrink:0;margin-left:1em;opacity:.6}.bg-img{width:100vw;height:100vh;opacity:.03;z-index:-1;position:fixed;top:0;background-attachment:fixed;background-repeat:no-repeat;background-size:cover;background-position:50%;transition:opacity .5s}.show-bg-img{z-index:100;opacity:1;cursor:pointer}.post-header{margin-top:1.2em;line-height:normal}.post-header .post-meta{font-size:.9em;letter-spacing:normal;opacity:.6}.post-header h1{margin-top:.1em}hr.post-end{width:50%;margin-top:1.6em;margin-bottom:.8em;margin-left:0;border-style:solid;border-bottom-width:4px}.content a{word-wrap:break-word;border:none;box-shadow:inset 0 -4px 0 #1ad1a3;transition-property:background-color}.content a:hover{background-color:#1ad1a3}.content figure{max-width:100%;height:auto;margin:0;text-align:center}.content figure p{font-size:.8em;font-style:italic;opacity:.6}.content figure.left{float:left;margin-right:1.5em;max-width:50%}.content figure.right{float:right;margin-left:1.5em;max-width:50%}.content figure.big{max-width:100vw}.content img{display:block;max-width:100%;height:auto;margin:auto;border-radius:4px}.content ul,.content ol{padding:0;margin-left:1.8em}.content a.anchor{float:left;margin-left:-20px;padding-right:6px;box-shadow:none;opacity:.8}.content a.anchor:hover{background:0 0;color:#1ad1a3;opacity:1}.content a.anchor svg{display:inline-block;width:14px;height:14px;vertical-align:baseline;visibility:hidden}.content a.anchor:focus svg{visibility:visible}.content h1:hover a.anchor svg,.content h2:hover a.anchor svg,.content h3:hover a.anchor svg,.content h4:hover a.anchor svg,.content h5:hover a.anchor svg,.content h6:hover a.anchor svg{visibility:visible}.footnotes{font-size:.85em}.footnotes a{box-shadow:none;text-decoration:underline;transition-property:color}.footnotes a:hover{background:0 0}.footnotes a.footnote-return{text-decoration:none}.footnotes ol{line-height:1.8}.footnote-ref a{box-shadow:none;text-decoration:none;padding:2px;border-radius:2px;background-color:#2c3e50}.post-info{font-size:.8rem;line-height:normal;opacity:.6}.post-info p{margin:.8em 0}.post-info a:hover{border-bottom:1px solid #1ad1a3}.post-info svg{margin-right:.8em}.post-info .tag{margin-right:.5em}.post-info .tag::before{content:\"#\"}#toc{position:fixed;left:50%;top:0;display:none}.toc-title{margin-left:1em;margin-bottom:.5em;font-size:.8em;font-weight:700}#TableOfContents{font-size:.8em;opacity:.6}#TableOfContents ul{padding-left:1em;margin:0}#TableOfContents>ul{list-style-type:none}#TableOfContents>ul ul ul{font-size:.9em}#TableOfContents a:hover{border-bottom:#1ad1a3 1px solid}.post-nav{display:flex;justify-content:space-between;margin-top:1.5em;margin-bottom:2.5em;font-size:1.2em}.post-nav a{flex-basis:50%;flex-grow:1}.post-nav .next-post{text-align:left;padding-right:5px}.post-nav .prev-post{text-align:right;padding-left:5px}.post-nav .post-nav-label{font-size:.8em;opacity:.8;text-transform:uppercase}@media(min-width:800px){.site-main{margin-top:3em}hr.post-end{width:40%}}@media(min-width:960px){.site-main{margin-top:6em}}@media(min-width:1300px){.site-main{margin-top:8em}.desktop-only,#toc.show-toc{display:block}.desktop-only-ib{display:inline-block}figure.left{margin-left:-240px}figure.left p{text-align:left}figure.right{margin-right:-240px}figure.right p{text-align:right}figure.big{width:1200px;margin-left:-240px}hr.post-end{width:30%}#toc{top:13em;margin-left:460px;max-width:220px}}@media(min-width:1800px){.site-main{margin-top:10em}.section-inner{max-width:1600px}.thin{max-width:960px}figure.left{max-width:75%;margin-left:-320px}figure.right{max-width:75%;margin-right:-320px}figure.big{width:1600px;margin-left:-320px}hr.post-end{width:30%}#toc{top:15em;margin-left:490px;max-width:300px}}@media(max-width:760px){.hide-in-mobile,.site-nav.hide-in-mobile{display:none}#menu-btn{display:inline-block}.posts-group{display:block}.posts-group .post-year{margin:-6px 0 4px}#spotlight.error-404{flex-direction:column;text-align:center}#spotlight.error-404 .banner-404{margin:0}}@media(max-width:520px){.content figure.left,.content figure.right{float:unset;max-width:100%;margin:0}hr.post-end{width:60%}#mobile-menu{right:1.2em}}.utterances{max-width:900px}"
  },
  {
    "path": "resources/_gen/assets/scss/scss/style.scss_c16d144eee185fbddd582cd5e25a4fae.json",
    "content": "{\"Target\":\"css/style.min.d3141168199607bf3a517216ce3c263814eecdbc8fca72a9a88700799a838219.css\",\"MediaType\":\"text/css\",\"Data\":{\"Integrity\":\"sha256-0xQRaBmWB786UXIWzjwmOBTuzbyPynKpqIcAeZqDghk=\"}}"
  },
  {
    "path": "static/css/styles.css",
    "content": ":root {\n    --bg-color: #fff;\n    --secondary-bg-color: #eeeeee;\n    --heading-color: #5f5f5f;\n    --body-color: rgba(0, 0, 0, 0.7);\n    --post-color: rgba(0, 0, 0, 0.44);\n    --border-color: rgba(0, 0, 0, 0.15);\n    --pre-bg-color: #f9f9fd;\n    --nav-text-color: #5a5a5a;\n    --tag-color: #424242;\n    --blockquote-text-color: #858585;\n    --blockquote-border-color: #dfe2e5;\n    scroll-padding-top: 100px;\n}\n\n.post .post-footer {\n    padding: 0 0 10px 0;\n    border-bottom: 1px solid var(--border-color);\n}\n\n.post .post-footer .meta {\n    max-width: 100%;\n    display: flex;\n    color: #bbbbbb;\n}\n\n.post .post-footer .meta .info {\n    float: left;\n    font-size: 12px;\n    margin-bottom: 1em;\n    color: var(--body-color);\n}\n\n.post .post-footer .info .separator a {\n    margin-right: 0.2em;\n}\n\n.post .post-footer .meta .info .date {\n    margin-right: 10px;\n    margin-left: 5px\n}\n\n.info {\n    margin: 1em;\n}\n\n.post .post-footer .meta a {\n    text-decoration: none;\n    color: var(--body-color);\n    padding-right: 10px;\n}\n\n.post .post-footer .meta a:hover {\n    color: #2660ab;\n}\n\n.post .post-footer .meta i {\n    margin-right: 6px;\n}\n\n.post .post-footer .tags {\n    padding-bottom: 15px;\n    font-size: 13px;\n}\n\n.post .post-footer .tags ul {\n    list-style-type: none;\n    display: inline;\n    margin: 0;\n    padding: 0;\n}\n\n.post .post-footer .tags ul li {\n    list-style-type: none;\n    margin: 0;\n    padding-right: 5px;\n    display: inline;\n}\n\n.post .post-footer .tags a {\n    text-decoration: none;\n    color: var(--post-color);\n    font-weight: 400;\n}\n\n.post .post-footer .tags a:hover {\n    text-decoration: none;\n}\n\n.pagination {\n    margin: 30px;\n    padding: 0px 0 56px 0;\n    text-align: center;\n}\n\n.pagination ul {\n    list-style: none;\n    margin: 0;\n    padding: 0;\n    height: 13px;\n}\n\n.pagination ul li {\n    margin: 0 2px 0 2px;\n    display: inline;\n    line-height: 1;\n}\n\n.pagination ul li a {\n    text-decoration: none;\n    color: var(--body-color);\n}\n\n.pagination .pre {\n    float: left;\n}\n\n.pagination .next {\n    float: right;\n}\n\n.tag::before {\n    content: \"#\";\n    opacity: .5;\n}\n\n.tag {\n    display: inline-block;\n    font-size: 15px;\n    line-height: 1;\n    margin: 5px 8px 5px 0;\n}\n"
  },
  {
    "path": "themes/hermit/.editorconfig",
    "content": "# editorconfig.org\n\nroot = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.{html,xml}]\nindent_style = tab\n\n[*.md]\nindent_style = unset\nindent_size = unset\ninsert_final_newline = unset\ntrim_trailing_whitespace = unset\n"
  },
  {
    "path": "themes/hermit/.gitattributes",
    "content": "* text eol=lf\n*.png binary\n"
  },
  {
    "path": "themes/hermit/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2018 Track3\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "themes/hermit/README.md",
    "content": "# Hugo theme Hermit\n\n[![Netlify Status](https://api.netlify.com/api/v1/badges/01a2e2de-d57d-4d89-8322-95685000e60f/deploy-status)](https://app.netlify.com/sites/hugo-theme-hermit/deploys)\n\nHermit is a minimal and fast theme for Hugo. It's built for bloggers who want a simple and focused website.\n\n![](https://github.com/Track3/hermit/raw/master/images/screenshot.png)\n\n## Features\n\n* A single-column layout and carefully crafted typography offers a great reading experience.\n* Navigations and functions are placed in the bottom bar which will hide when you scroll down.\n* Featured image is supported. It will be displayed as a dimmed background of the page.\n* Displays all of your posts on a single page, with one section per year, simple and compact.\n* Extremely lightweight and load fast. No third party framework, no unnecessary code.\n* Responsive & Retina Ready. Scales gracefully from a big screen all the way down to the smallest mobile phone. Assets in vector format ensures that it looks sharp on high-resolution screens.\n\n**[Theme Demo](https://hugo-theme-hermit.netlify.com/)** (uses contents and config from the `exampleSite` folder)\n\n![](https://github.com/Track3/hermit/raw/master/images/hermit.png)\n\n## Getting started\n\n### Installation\n\nRun this command from the root of your Hugo directory:\n\n```bash\n$ git clone https://github.com/Track3/hermit.git themes/hermit\n```\n\nOr, if your Hugo site is already in git, you can include this repository as a [git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules). This makes it easier to update this theme. For this you need to run:\n\n```bash\n$ git submodule add https://github.com/Track3/hermit.git themes/hermit\n```\n\nAlternatively, if you are not familiar with git, you can download the theme as a `.zip` file, unzip the theme contents, and then move the unzipped source into your `themes` directory.\n\nFor more information, read the official [documentation](https://gohugo.io/themes/installing-and-using-themes/) of Hugo.\n\n### Configuration\n\nThe example config file can be found in the theme's `exampleSite` folder. You can just copy the `config.toml` to the root directory of your Hugo site. There are instructions in the example config file, feel free to change strings as you like to customize your website.\n\n#### Favicon\n\nUse [RealFaviconGenerator](https://realfavicongenerator.net/) to generate these files, put them into your site's `static` folder:\n\n* android-chrome-192x192.png\n* android-chrome-512x512.png\n* apple-touch-icon.png\n* favicon-16x16.png\n* favicon-32x32.png\n* favicon.ico\n* mstile-150x150.png\n* safari-pinned-tab.svg\n* site.webmanifest\n\n#### Social icons\n\nThe following icons are supported, please make sure the `name` filed is exactly one of these:\n\n* codepen\n* facebook\n* github\n* gitlab\n* instagram\n* linkedin\n* slack\n* telegram\n* twitter\n* youtube\n* email\n\nIf that's not enough, you can see [Overriding templates](#overriding-templates) section.\n\n### Manage content\n\n* Keep your regular pages in the `content` folder. To create a new page, run `hugo new page-title.md`\n* Keep your blog posts in the `content/posts` folder. To create a new post, run `hugo new posts/post-title.md`\n\n### More customizations\n\n#### Overriding templates\n\nIn Hugo, layouts can live in either the project’s (root) or the themes’ layout folders, any template inside the root layout folder will override theme's layout that relative to it, for example: `layouts/_default/baseof.html` will override `themes/hermit/layouts/_default/baseof.html`. So, you can easily customize the theme without edit it directly, which makes updating the theme easier. Here's some common customizations:\n\n##### Customize social icons\nYou can modify or add any svg icons in site's `layouts/partials/svg.html`.\n\n##### Customize comment system\nWe only have built-in support for Disqus at the moment, if that doesn't fit your needs, you can just add html to site's `layouts/partials/comments.html`.\n\n##### Add custom analytics\nIf you prefer to use different analytics system other than google analytics, then add them inside `layouts/partials/analytics.html`.\n\n#### Customize CSS\n\nIf you'd like to customize theme color or fonts, you can simply override `assets/scss/_predefined.scss`, by simply copy it to site's root (keep the same relative path) then edit those variables. But keep in mind, you'll need **Hugo extended version** which has the ability to rebuild SCSS. You don't have to use extended version in production but in this case it's necessary to make sure the `resources` folder is committed and \"up to date\" (by running `hugo` or `hugo server` locally using the extended version). But anyway, always use the extended version if you can.\n\nFor adding other custom CSS to the theme, you can assign an array of references in `config.toml` like following:\n```\n[params]\n  customCSS = [\"css/foo.css\", \"css/bar.css\"]\n```\nYou may reference as many stylesheets as you want. Their paths need to be relative to the `static` folder or it can be a full URL for external resources.\n\n#### Code injection\n\nYou can inject any html code to every page's document head or right above the closing body tag. This makes it easier to add any html meta data, custom css/js, dns-prefetch etc. To do this you simply need to create a file at site's `layouts/partials/extra-head.html` or `layouts/partials/extra-foot.html`, code inside will be injected to every page.\n\n## Acknowledgments\n\n* [normalize.css](https://necolas.github.io/normalize.css/) - [MIT](https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n* [animate.css](https://daneden.github.io/animate.css/) - [MIT](https://github.com/daneden/animate.css/blob/master/LICENSE)\n* [feather](https://feathericons.com/) - [MIT](https://github.com/feathericons/feather/blob/master/LICENSE)\n\nThanks!\n"
  },
  {
    "path": "themes/hermit/archetypes/default.md",
    "content": "---\ntitle: \"{{ replace .Name \"-\" \" \" | title }}\"\ndate: {{ .Date }}\ndraft: true\ncomments: false\nimages:\n---\n\n"
  },
  {
    "path": "themes/hermit/archetypes/posts.md",
    "content": "---\ntitle: \"{{ replace .Name \"-\" \" \" | title }}\"\ndate: {{ .Date }}\ndraft: true\ntoc: false\nimages:\ntags: \n  - untagged\n---\n\n"
  },
  {
    "path": "themes/hermit/assets/js/main.js",
    "content": "/**\n * Utils\n */\n\n// Throttle\n//\nconst throttle = (callback, limit) => {\n  let timeoutHandler = null;\n  return () => {\n    if (timeoutHandler == null) {\n      timeoutHandler = setTimeout(() => {\n        callback();\n        timeoutHandler = null;\n      }, limit);\n    }\n  };\n};\n\n// addEventListener Helper\n//\nconst listen = (ele, e, callback) => {\n  if (document.querySelector(ele) !== null) {\n    document.querySelector(ele).addEventListener(e, callback);\n  }\n}\n\n/**\n * Functions\n */\n\n// Auto Hide Header\n//\nlet header = document.getElementById('site-header');\nlet lastScrollPosition = window.pageYOffset;\n\nconst autoHideHeader = () => {\n  let currentScrollPosition = window.pageYOffset;\n  if (currentScrollPosition > lastScrollPosition) {\n    header.classList.remove('slideInUp');\n    header.classList.add('slideOutDown');\n  } else {\n    header.classList.remove('slideOutDown');\n    header.classList.add('slideInUp');\n  }\n  lastScrollPosition = currentScrollPosition;\n}\n\n// Mobile Menu Toggle\n//\nlet mobileMenuVisible = false;\n\nconst toggleMobileMenu = () => {\n  let mobileMenu = document.getElementById('mobile-menu');\n  if (mobileMenuVisible == false) {\n    mobileMenu.style.animationName = 'bounceInRight';\n    mobileMenu.style.webkitAnimationName = 'bounceInRight';\n    mobileMenu.style.display = 'block';\n    mobileMenuVisible = true;\n  } else {\n    mobileMenu.style.animationName = 'bounceOutRight';\n    mobileMenu.style.webkitAnimationName = 'bounceOutRight'\n    mobileMenuVisible = false;\n  }\n}\n\n// Featured Image Toggle\n//\nconst showImg = () => {\n  document.querySelector('.bg-img').classList.add('show-bg-img');\n}\n\nconst hideImg = () => {\n  document.querySelector('.bg-img').classList.remove('show-bg-img');\n}\n\n// ToC Toggle\n//\nconst toggleToc = () => {\n  document.getElementById('toc').classList.toggle('show-toc');\n}\n\n\nif (header !== null) {\n  listen('#menu-btn', \"click\", toggleMobileMenu);\n  listen('#toc-btn', \"click\", toggleToc);\n  listen('#img-btn', \"click\", showImg);\n  listen('.bg-img', \"click\", hideImg);\n\n  document.querySelectorAll('.post-year').forEach((ele)=> {\n    ele.addEventListener('click', () => {\n      window.location.hash = '#' + ele.id;\n    });\n  });\n\n  window.addEventListener('scroll', throttle(() => {\n    autoHideHeader();\n\n    if (mobileMenuVisible == true) {\n      toggleMobileMenu();\n    }\n  }, 250));\n}\n"
  },
  {
    "path": "themes/hermit/assets/scss/_animate.scss",
    "content": "@charset \"UTF-8\";\n\n/*!\n * animate.css -https://daneden.github.io/animate.css/\n * Version - 3.7.0\n * Licensed under the MIT license - http://opensource.org/licenses/MIT\n *\n * Copyright (c) 2019 Daniel Eden\n */\n\n@-webkit-keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}@keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}.flash{-webkit-animation-name:flash;animation-name:flash}@-webkit-keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.bounceInRight{-webkit-animation-name:bounceInRight;animation-name:bounceInRight}@-webkit-keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.bounceOutRight{-webkit-animation-name:bounceOutRight;animation-name:bounceOutRight}@-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInUp{-webkit-animation-name:slideInUp;animation-name:slideInUp}@-webkit-keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.slideOutDown{-webkit-animation-name:slideOutDown;animation-name:slideOutDown}.animated{-webkit-animation-fill-mode:both;animation-fill-mode:both}.animated.infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.animated.delay-1s{-webkit-animation-delay:1s;animation-delay:1s}.animated.delay-2s{-webkit-animation-delay:2s;animation-delay:2s}.animated.delay-3s{-webkit-animation-delay:3s;animation-delay:3s}.animated.delay-4s{-webkit-animation-delay:4s;animation-delay:4s}.animated.delay-5s{-webkit-animation-delay:5s;animation-delay:5s}.animated.fast{-webkit-animation-duration:.8s;animation-duration:.8s}.animated.faster{}.animated.slow{-webkit-animation-duration:2s;animation-duration:2s}.animated.slower{-webkit-animation-duration:3s;animation-duration:3s}@media (prefers-reduced-motion),(print){.animated{-webkit-animation:unset!important;animation:unset!important;-webkit-transition:none!important;transition:none!important}}"
  },
  {
    "path": "themes/hermit/assets/scss/_normalize.scss",
    "content": "/*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */\n\n/* Document\n   ========================================================================== */\n\n/**\n * 1. Correct the line height in all browsers.\n * 2. Prevent adjustments of font size after orientation changes in iOS.\n */\n\nhtml {\n  line-height: 1.15; /* 1 */\n  -webkit-text-size-adjust: 100%; /* 2 */\n}\n\n/* Sections\n   ========================================================================== */\n\n/**\n * Remove the margin in all browsers.\n */\n\nbody {\n  margin: 0;\n}\n\n/**\n * Correct the font size and margin on `h1` elements within `section` and\n * `article` contexts in Chrome, Firefox, and Safari.\n */\n\nh1 {\n  font-size: 2em;\n  margin: 0.67em 0;\n}\n\n/* Grouping content\n   ========================================================================== */\n\n/**\n * 1. Add the correct box sizing in Firefox.\n * 2. Show the overflow in Edge and IE.\n */\n\nhr {\n  box-sizing: content-box; /* 1 */\n  height: 0; /* 1 */\n  overflow: visible; /* 2 */\n}\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\npre {\n  font-family: monospace, monospace; /* 1 */\n  font-size: 1em; /* 2 */\n}\n\n/* Text-level semantics\n   ========================================================================== */\n\n/**\n * Remove the gray background on active links in IE 10.\n */\n\na {\n  background-color: transparent;\n}\n\n/**\n * 1. Remove the bottom border in Chrome 57-\n * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n */\n\nabbr[title] {\n  border-bottom: none; /* 1 */\n  text-decoration: underline; /* 2 */\n  text-decoration: underline dotted; /* 2 */\n}\n\n/**\n * Add the correct font weight in Chrome, Edge, and Safari.\n */\n\nb,\nstrong {\n  font-weight: bolder;\n}\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\ncode,\nkbd,\nsamp {\n  font-family: monospace, monospace; /* 1 */\n  font-size: 1em; /* 2 */\n}\n\n/**\n * Add the correct font size in all browsers.\n */\n\nsmall {\n  font-size: 80%;\n}\n\n/**\n * Prevent `sub` and `sup` elements from affecting the line height in\n * all browsers.\n */\n\nsub,\nsup {\n  font-size: 75%;\n  line-height: 0;\n  position: relative;\n  vertical-align: baseline;\n}\n\nsub {\n  bottom: -0.25em;\n}\n\nsup {\n  top: -0.5em;\n}\n\n/* Embedded content\n   ========================================================================== */\n\n/**\n * Remove the border on images inside links in IE 10.\n */\n\nimg {\n  border-style: none;\n}\n\n/* Forms\n   ========================================================================== */\n\n/**\n * 1. Change the font styles in all browsers.\n * 2. Remove the margin in Firefox and Safari.\n */\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n  font-family: inherit; /* 1 */\n  font-size: 100%; /* 1 */\n  line-height: 1.15; /* 1 */\n  margin: 0; /* 2 */\n}\n\n/**\n * Show the overflow in IE.\n * 1. Show the overflow in Edge.\n */\n\nbutton,\ninput { /* 1 */\n  overflow: visible;\n}\n\n/**\n * Remove the inheritance of text transform in Edge, Firefox, and IE.\n * 1. Remove the inheritance of text transform in Firefox.\n */\n\nbutton,\nselect { /* 1 */\n  text-transform: none;\n}\n\n/**\n * Correct the inability to style clickable types in iOS and Safari.\n */\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n  -webkit-appearance: button;\n}\n\n/**\n * Remove the inner border and padding in Firefox.\n */\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n  border-style: none;\n  padding: 0;\n}\n\n/**\n * Restore the focus styles unset by the previous rule.\n */\n\nbutton:-moz-focusring,\n[type=\"button\"]:-moz-focusring,\n[type=\"reset\"]:-moz-focusring,\n[type=\"submit\"]:-moz-focusring {\n  outline: 1px dotted ButtonText;\n}\n\n/**\n * Correct the padding in Firefox.\n */\n\nfieldset {\n  padding: 0.35em 0.75em 0.625em;\n}\n\n/**\n * 1. Correct the text wrapping in Edge and IE.\n * 2. Correct the color inheritance from `fieldset` elements in IE.\n * 3. Remove the padding so developers are not caught out when they zero out\n *    `fieldset` elements in all browsers.\n */\n\nlegend {\n  box-sizing: border-box; /* 1 */\n  color: inherit; /* 2 */\n  display: table; /* 1 */\n  max-width: 100%; /* 1 */\n  padding: 0; /* 3 */\n  white-space: normal; /* 1 */\n}\n\n/**\n * Add the correct vertical alignment in Chrome, Firefox, and Opera.\n */\n\nprogress {\n  vertical-align: baseline;\n}\n\n/**\n * Remove the default vertical scrollbar in IE 10+.\n */\n\ntextarea {\n  overflow: auto;\n}\n\n/**\n * 1. Add the correct box sizing in IE 10.\n * 2. Remove the padding in IE 10.\n */\n\n[type=\"checkbox\"],\n[type=\"radio\"] {\n  box-sizing: border-box; /* 1 */\n  padding: 0; /* 2 */\n}\n\n/**\n * Correct the cursor style of increment and decrement buttons in Chrome.\n */\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n  height: auto;\n}\n\n/**\n * 1. Correct the odd appearance in Chrome and Safari.\n * 2. Correct the outline style in Safari.\n */\n\n[type=\"search\"] {\n  -webkit-appearance: textfield; /* 1 */\n  outline-offset: -2px; /* 2 */\n}\n\n/**\n * Remove the inner padding in Chrome and Safari on macOS.\n */\n\n[type=\"search\"]::-webkit-search-decoration {\n  -webkit-appearance: none;\n}\n\n/**\n * 1. Correct the inability to style clickable types in iOS and Safari.\n * 2. Change font properties to `inherit` in Safari.\n */\n\n::-webkit-file-upload-button {\n  -webkit-appearance: button; /* 1 */\n  font: inherit; /* 2 */\n}\n\n/* Interactive\n   ========================================================================== */\n\n/*\n * Add the correct display in Edge, IE 10+, and Firefox.\n */\n\ndetails {\n  display: block;\n}\n\n/*\n * Add the correct display in all browsers.\n */\n\nsummary {\n  display: list-item;\n}\n\n/* Misc\n   ========================================================================== */\n\n/**\n * Add the correct display in IE 10+.\n */\n\ntemplate {\n  display: none;\n}\n\n/**\n * Add the correct display in IE 10.\n */\n\n[hidden] {\n  display: none;\n}"
  },
  {
    "path": "themes/hermit/assets/scss/_predefined.scss",
    "content": "// Colors\n//\n// Dark theme\n//$theme: #018574;\n//$text: #c6cddb;\n//$light-grey: #494f5c;\n//$dark-grey: #3B3E48;\n//$highlight-grey: #7d828a;\n//$midnightblue: #2c3e50;\n\n$theme: #1ad1a3;\n$text: #3b3e48;\n$light-grey: #fcfcfc;\n$dark-grey: #f9f9f9;\n$highlight-grey: #7d828a;\n$midnightblue: #2c3e50;\n\n// Fonts\n//\n$fonts: \"Trebuchet MS\", Verdana, \"Verdana Ref\", \"Segoe UI\", Candara, \"Lucida Grande\", \"Lucida Sans Unicode\", \"Lucida Sans\", Tahoma, sans-serif;\n$code-fonts: Consolas, \"Andale Mono WT\", \"Andale Mono\", Menlo, Monaco, \"Lucida Console\", \"Lucida Sans Typewriter\", \"DejaVu Sans Mono\", \"Bitstream Vera Sans Mono\", \"Liberation Mono\", \"Nimbus Mono L\", \"Courier New\", Courier, \"YaHei Consolas Hybrid\", monospace, \"Segoe UI Emoji\", \"PingFang SC\", \"Microsoft YaHei\";\n\n// Mixins\n//\n@mixin dimmed {\n  opacity: .6;\n}\n\n@mixin aTag {\n  a {\n    word-wrap: break-word;\n    border: none;\n    box-shadow: inset 0 -4px 0 $theme;\n    transition-property: background-color;\n\n    &:hover {\n      background-color: $theme;\n    }\n  }\n}\n"
  },
  {
    "path": "themes/hermit/assets/scss/_syntax.scss",
    "content": "/* Background */ .chroma { color: #eee; background-color: $midnightblue }\n/* Error */ .chroma .err { color: #960050; background-color: #1e0010 }\n/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }\n/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; width: auto; overflow: auto; display: block; }\n/* LineHighlight */ .chroma .hl { display: block; width: 100%;background-color: #ffffcc }\n/* LineNumbersTable */ .chroma .lnt { margin-right: 0.4em; padding: 0 0.4em 0 0.4em; }\n/* LineNumbers */ .chroma .ln { margin-right: 0.4em; padding: 0 0.4em 0 0.4em; }\n/* Keyword */ .chroma .k { color: #66d9ef }\n/* KeywordConstant */ .chroma .kc { color: #66d9ef }\n/* KeywordDeclaration */ .chroma .kd { color: #66d9ef }\n/* KeywordNamespace */ .chroma .kn { color: #f92672 }\n/* KeywordPseudo */ .chroma .kp { color: #66d9ef }\n/* KeywordReserved */ .chroma .kr { color: #66d9ef }\n/* KeywordType */ .chroma .kt { color: #66d9ef }\n/* NameAttribute */ .chroma .na { color: #a6e22e }\n/* NameClass */ .chroma .nc { color: #a6e22e }\n/* NameConstant */ .chroma .no { color: #66d9ef }\n/* NameDecorator */ .chroma .nd { color: #a6e22e }\n/* NameException */ .chroma .ne { color: #a6e22e }\n/* NameFunction */ .chroma .nf { color: #a6e22e }\n/* NameOther */ .chroma .nx { color: #a6e22e }\n/* NameTag */ .chroma .nt { color: #f92672 }\n/* Literal */ .chroma .l { color: #ae81ff }\n/* LiteralDate */ .chroma .ld { color: #e6db74 }\n/* LiteralString */ .chroma .s { color: #e6db74 }\n/* LiteralStringAffix */ .chroma .sa { color: #e6db74 }\n/* LiteralStringBacktick */ .chroma .sb { color: #e6db74 }\n/* LiteralStringChar */ .chroma .sc { color: #e6db74 }\n/* LiteralStringDelimiter */ .chroma .dl { color: #e6db74 }\n/* LiteralStringDoc */ .chroma .sd { color: #e6db74 }\n/* LiteralStringDouble */ .chroma .s2 { color: #e6db74 }\n/* LiteralStringEscape */ .chroma .se { color: #ae81ff }\n/* LiteralStringHeredoc */ .chroma .sh { color: #e6db74 }\n/* LiteralStringInterpol */ .chroma .si { color: #e6db74 }\n/* LiteralStringOther */ .chroma .sx { color: #e6db74 }\n/* LiteralStringRegex */ .chroma .sr { color: #e6db74 }\n/* LiteralStringSingle */ .chroma .s1 { color: #e6db74 }\n/* LiteralStringSymbol */ .chroma .ss { color: #e6db74 }\n/* LiteralNumber */ .chroma .m { color: #ae81ff }\n/* LiteralNumberBin */ .chroma .mb { color: #ae81ff }\n/* LiteralNumberFloat */ .chroma .mf { color: #ae81ff }\n/* LiteralNumberHex */ .chroma .mh { color: #ae81ff }\n/* LiteralNumberInteger */ .chroma .mi { color: #ae81ff }\n/* LiteralNumberIntegerLong */ .chroma .il { color: #ae81ff }\n/* LiteralNumberOct */ .chroma .mo { color: #ae81ff }\n/* Operator */ .chroma .o { color: #f92672 }\n/* OperatorWord */ .chroma .ow { color: #f92672 }\n/* Comment */ .chroma .c { color: #75715e }\n/* CommentHashbang */ .chroma .ch { color: #75715e }\n/* CommentMultiline */ .chroma .cm { color: #75715e }\n/* CommentSingle */ .chroma .c1 { color: #75715e }\n/* CommentSpecial */ .chroma .cs { color: #75715e }\n/* CommentPreproc */ .chroma .cp { color: #75715e }\n/* CommentPreprocFile */ .chroma .cpf { color: #75715e }\n/* GenericDeleted */ .chroma .gd { color: #f92672 }\n/* GenericEmph */ .chroma .ge { font-style: italic }\n/* GenericInserted */ .chroma .gi { color: #a6e22e }\n/* GenericStrong */ .chroma .gs { font-weight: bold }\n/* GenericSubheading */ .chroma .gu { color: #75715e }\n"
  },
  {
    "path": "themes/hermit/assets/scss/style.scss",
    "content": "@import \"predefined.scss\";\n@import \"normalize.scss\";\n@import \"syntax.scss\";\n@import \"animate.scss\";\n\n/* Webkit Scrollbar Customize */\n::-webkit-scrollbar {\n  width: 8px;\n  height: 8px;\n  background: $midnightblue;\n}\n\n::-webkit-scrollbar-thumb {\n  background: #888;\n\n  &:hover {\n    background: $text;\n  }\n}\n\nhtml {\n  background: $light-grey;\n  line-height: 1.6;\n  letter-spacing: .05em;\n  scroll-behavior: smooth;\n}\n\nbody,\nbutton,\ninput,\nselect,\ntextarea {\n  color: $text;\n  font-family: $fonts;\n}\n\npre,\ncode,\npre tt {\n  font-family: $code-fonts;\n}\n\npre {\n  padding: .7em 1.1em;\n  overflow: auto;\n  font-size: .9em;\n  line-height: 1.5;\n  letter-spacing: normal;\n  white-space: pre;\n  color: #eee;\n  background: $midnightblue;\n  border-radius: 4px;\n  // -webkit-overflow-scrolling: touch;\n\n  code {\n    color: #eee;\n    padding: 0;\n    margin: 0;\n    background: $midnightblue;\n  }\n}\n\ncode {\n  color: $midnightblue;\n  background: #eee;\n  border-radius: 3px;\n  padding: 0 3px;\n  margin: 0 4px;\n  word-wrap: break-word;\n  letter-spacing: normal;\n}\n\nblockquote {\n  border-left: .25em solid;\n  margin: 1em;\n  padding: 0 1em;\n  font-style: italic;\n\n  cite {\n    font-weight: bold;\n    font-style: normal;\n\n    &::before {\n      content: \"—— \";\n    }\n  }\n}\n\na {\n  color: $text;\n  text-decoration: none;\n  border: none;\n  transition-property: color;\n  transition-duration: .4s;\n  transition-timing-function: ease-out;\n\n  &:hover {\n    color: #000000;\n  }\n}\n\nhr {\n  opacity: .2;\n  border-width: 0 0 5px 0;\n  border-style: dashed;\n  background: transparent;\n  width: 50%;\n  margin: 1.8em auto;\n}\n\ntable {\n  border-collapse: collapse;\n  border-spacing: 0;\n  empty-cells: show;\n  width: 100%;\n  max-width: 100%;\n\n  th,\n  td {\n    padding: 1.5%;\n    border: 1px solid;\n  }\n\n  th {\n    font-weight: 700;\n    vertical-align: bottom;\n  }\n}\n\n.section-inner {\n  margin: 0 auto;\n  max-width: 1200px;\n  width: 93%;\n}\n\n.thin {\n  max-width: 900px;\n  margin: auto;\n}\n\n.feather {\n  display: inline-block;\n  vertical-align: -.125em;\n  width: 1em;\n  height: 1em;\n}\n\n.desktop-only, .desktop-only-ib {\n  display: none;\n}\n\n// Accessibility\n//\n.screen-reader-text {\n\tborder: 0;\n\tclip: rect(1px, 1px, 1px, 1px);\n\tclip-path: inset(50%);\n\theight: 1px;\n\tmargin: -1px;\n\toverflow: hidden;\n\tpadding: 0;\n\tposition: absolute !important;\n\twidth: 1px;\n\tword-wrap: normal !important;\n}\n\n.screen-reader-text:focus {\n\tbackground-color: #f1f1f1;\n\tborder-radius: 3px;\n\tbox-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.6);\n\tclip: auto !important;\n\tclip-path: none;\n\tcolor: #21759b;\n\tdisplay: block;\n\tfont-size: 14px;\n\tfont-size: 0.875rem;\n\tfont-weight: bold;\n\theight: auto;\n\tleft: 5px;\n\tline-height: normal;\n\tpadding: 15px 23px 14px;\n\ttext-decoration: none;\n\ttop: 5px;\n\twidth: auto;\n\tz-index: 100000;\n}\n\n// Header & Footer\n//\n#site-header {\n  //position: fixed;\n  z-index: 1;\n  //bottom: 0;\n  width: 100%;\n  box-sizing: border-box;\n  box-shadow: -1px 2px 3px rgba(0, 0, 0, 0.45);\n  background-color: $dark-grey;\n}\n\n.hdr-wrapper {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: .5em 0;\n  font-size: 1.2rem;\n\n  .site-branding {\n    display: inline-block;\n    margin-right: .8em;\n    font-size: 1.2em;\n  }\n\n  .site-nav {\n    display: inline-block;\n    font-size: 1.1em;\n    opacity: .8;\n\n    a {\n      margin-left: .8em;\n    }\n  }\n}\n\n.hdr-icons {\n  font-size: 1.2em;\n}\n\n.hdr-social {\n  display: inline-block;\n  margin-left: .6em;\n\n  &>a {\n    margin-left: .4em;\n  }\n}\n\n.hdr-btn {\n  border: none;\n  background: none;\n  padding: 0;\n  margin-left: .4em;\n  cursor: pointer;\n}\n\n#menu-btn {\n  display: none;\n  margin-left: .6em;\n  cursor: pointer;\n}\n\n#mobile-menu {\n  position: fixed;\n  bottom: 4.8em;\n  right: 1.5em;\n  display: none;\n  padding: .6em 1.8em;\n  z-index: 1;\n  box-sizing: border-box;\n  box-shadow: -1px -2px 3px 0px rgba(0, 0, 0, 0.45);\n  background-color: $dark-grey;\n\n  ul {\n    list-style: none;\n    margin: 0;\n    padding: 0;\n    line-height: 2;\n    font-size: 1.2em;\n  }\n}\n\n#site-footer {\n  text-align: center;\n  font-size: .9em;\n  margin-bottom: 96px;\n  margin-top: 64px;\n\n  p {\n    margin: 0;\n  }\n}\n\n// Spotlight\n//\n#spotlight {\n  display: flex;\n  height: 100vh;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  max-width: 93%;\n  margin: auto;\n  font-size: 1.5rem;\n\n  &.error-404 {\n    flex-direction: row;\n    line-height: normal;\n  }\n}\n\np.img-404 {\n  margin: 0;\n\n  svg {\n    width: 180px;\n    max-width: 100%;\n    height: auto;\n  }\n}\n\n.banner-404 {\n  margin-left: 2em;\n\n  h1 {\n    font-size: 3em;\n    margin: .5rem 0;\n  }\n\n  p {\n    margin-top: 0;\n  }\n\n  .btn-404 {\n    font-size: .8em;\n\n    a {\n      display: inline-block;\n      border: 2px solid $text;\n      border-radius: 5px;\n      padding: 5px;\n      transition-property: color, border-color;\n\n      &:first-child {\n        margin-right: 1em;\n      }\n\n      &:hover {\n        border-color: #000000;\n      }\n\n      svg {\n        margin-right: .5em;\n      }\n    }\n  }\n}\n\n#home-center {\n  display: flex;\n  flex-grow: 1;\n  flex-direction: column;\n  justify-content: center;\n}\n\n#home-title {\n  margin: 0;\n  text-align: center;\n}\n\n#home-subtitle {\n  margin-top: 0;\n  margin-bottom: 1.5em;\n  text-align: center;\n  line-height: normal;\n  font-size: .7em;\n  font-style: italic;\n  opacity: .9;\n}\n\n#home-social {\n  font-size: 1.4em;\n  text-align: center;\n  opacity: .8;\n\n  a {\n    margin: 0 .2em;\n  }\n}\n\n#home-nav {\n  opacity: .8;\n\n  a {\n    display: block;\n    text-align: center;\n    margin-top: .5em;\n  }\n}\n\n#home-footer {\n  text-align: center;\n  font-size: .6em;\n  line-height: normal;\n  @include dimmed;\n\n  p {\n    margin-top: 0;\n  }\n}\n\n// list.html\n//\n.posts-group {\n  display: flex;\n  margin-bottom: 1.9em;\n  line-height: normal;\n\n  .post-year {\n    padding-top: 6px;\n    margin-right: 1.8em;\n    font-size: 1.6em;\n    @include dimmed;\n\n    &:hover {\n      text-decoration: underline;\n      cursor: pointer;\n    }\n  }\n\n  .posts-list {\n    flex-grow: 1;\n    margin: 0;\n    padding: 0;\n    list-style: none;\n  }\n\n  .post-item {\n    border-bottom: 1px $highlight-grey dashed;\n\n    a {\n      display: flex;\n      justify-content: space-between;\n      align-items: baseline;\n      padding: 12px 0;\n    }\n  }\n\n  .post-day {\n    flex-shrink: 0;\n    margin-left: 1em;\n    @include dimmed;\n  }\n}\n\n// single.html\n//\n.bg-img {\n  width: 100vw;\n  height: 100vh;\n  opacity: .03;\n  z-index: -1;\n  position: fixed;\n  top: 0;\n  background-attachment: fixed;\n  background-repeat: no-repeat;\n  background-size: cover;\n  background-position: center;\n  transition: opacity .5s;\n}\n\n.show-bg-img {\n  z-index: 100;\n  opacity: 1;\n  cursor: pointer;\n}\n\n.post-header {\n  margin-top: 1.2em;\n  line-height: normal;\n\n  .post-meta {\n    font-size: .9em;\n    letter-spacing: normal;\n    @include dimmed;\n  }\n\n  h1 {\n    margin-top: .1em;\n  }\n}\n\nhr.post-end {\n  width: 50%;\n  margin-top: 1.6em;\n  margin-bottom: .8em;\n  margin-left: 0;\n  border-style: solid;\n  border-bottom-width: 4px;\n}\n\n.content {\n  {{- with .Site.Params.justifyContent }}\n  text-align: justify;\n  text-justify: inter-ideograph; //For IE/Edge\n  {{- end }}\n\n  @include aTag;\n\n  figure {\n    max-width: 100%;\n    height: auto;\n    margin: 0;\n    text-align: center;\n\n    p {\n      font-size: .8em;\n      font-style: italic;\n      @include dimmed;\n    }\n  }\n\n  figure.left {\n    float: left;\n    margin-right: 1.5em;\n    max-width: 50%;\n  }\n\n  figure.right {\n    float: right;\n    margin-left: 1.5em;\n    max-width: 50%;\n  }\n\n  figure.big {\n    max-width: 100vw;\n  }\n\n  img {\n    display: block;\n    max-width: 100%;\n    height: auto;\n    margin: auto;\n    border-radius: 4px;\n  }\n\n  ul,\n  ol {\n    padding: 0;\n    margin-left: 1.8em;\n  }\n\n  a.anchor {\n    float: left;\n    margin-left: -20px;\n    padding-right: 6px;\n    box-shadow: none;\n    opacity: .8;\n    &:hover {\n      background: none;\n      color: $theme;\n      opacity: 1;\n    }\n\n    svg {\n      display: inline-block;\n      width: 14px;\n      height: 14px;\n      vertical-align: baseline;\n      visibility: hidden;\n    }\n    &:focus svg {\n      visibility: visible;\n    }\n  }\n\n  h1:hover a.anchor svg,\n  h2:hover a.anchor svg,\n  h3:hover a.anchor svg,\n  h4:hover a.anchor svg,\n  h5:hover a.anchor svg,\n  h6:hover a.anchor svg {\n    visibility: visible;\n  }\n}\n\n.footnotes {\n  font-size: .85em;\n\n  a {\n    box-shadow: none;\n    text-decoration: underline;\n    transition-property: color;\n\n    &:hover {\n      background: transparent;\n    }\n\n    &.footnote-return {\n      text-decoration: none;\n    }\n  }\n\n  ol {\n    line-height: 1.8;\n  }\n}\n\n.footnote-ref a {\n  box-shadow: none;\n  text-decoration: none;\n  padding: 2px;\n  border-radius: 2px;\n  background-color: $midnightblue;\n}\n\n.post-info {\n  font-size: .8rem;\n  line-height: normal;\n  @include dimmed;\n\n  p {\n    margin: .8em 0;\n  }\n\n  a:hover {\n    border-bottom: 1px solid $theme;\n  }\n\n  svg {\n    margin-right: .8em;\n  }\n\n  .tag {\n    margin-right: .5em;\n\n    &::before {\n      content: \"#\"\n    }\n  }\n}\n\n#toc {\n  position: fixed;\n  left: 50%;\n  top: 0;\n  display: none;\n}\n\n.toc-title {\n  margin-left: 1em;\n  margin-bottom: .5em;\n  font-size: .8em;\n  font-weight: bold;\n}\n\n#TableOfContents {\n  font-size: .8em;\n  @include dimmed;\n\n  ul {\n    padding-left: 1em;\n    margin: 0;\n  }\n\n  &>ul {\n    list-style-type: none;\n\n    ul ul {\n      font-size: .9em;\n    }\n  }\n\n  a:hover {\n    border-bottom: $theme 1px solid;\n  }\n}\n\n\n.post-nav {\n  display: flex;\n  justify-content: space-between;\n  margin-top: 1.5em;\n  margin-bottom: 2.5em;\n  font-size: 1.2em;\n\n  a {\n    flex-basis: 50%;\n    flex-grow: 1;\n  }\n\n  .next-post {text-align: left; padding-right: 5px;}\n  .prev-post {text-align: right; padding-left: 5px;}\n\n  .post-nav-label {\n    font-size: .8em;\n    opacity: .8;\n    text-transform: uppercase;\n  }\n}\n\n// Media Queries\n//\n@media (min-width: 800px) {\n  .site-main {\n    margin-top: 3em;\n  }\n\n  hr.post-end {\n    width: 40%;\n  }\n}\n\n@media (min-width: 960px) {\n  .site-main {\n    margin-top: 6em;\n  }\n}\n\n@media (min-width: 1300px) {\n  .site-main {\n    margin-top: 8em;\n  }\n\n  .desktop-only,\n  #toc.show-toc {\n    display: block;\n  }\n\n  .desktop-only-ib {\n    display: inline-block;\n  }\n\n  figure.left {\n    margin-left: -240px;\n    p {\n      text-align: left;\n    }\n  }\n\n  figure.right {\n    margin-right: -240px;\n    p {\n      text-align: right;\n    }\n  }\n\n  figure.big {\n    width: 1200px;\n    margin-left: -240px;\n  }\n\n  hr.post-end {\n    width: 30%;\n  }\n\n  #toc {\n    top: 13em;\n    margin-left: 460px;\n    max-width: 220px;\n  }\n}\n\n@media (min-width: 1800px) {\n  .site-main {\n    margin-top: 10em;\n  }\n\n  .section-inner {\n    max-width: 1600px;\n  }\n\n  .thin {\n    max-width: 960px;\n  }\n\n  figure.left {\n    max-width: 75%;\n    margin-left: -320px;\n  }\n\n  figure.right {\n    max-width: 75%;\n    margin-right: -320px;\n  }\n\n  figure.big {\n    width: 1600px;\n    margin-left: -320px;\n  }\n\n  hr.post-end {\n    width: 30%;\n  }\n\n  #toc {\n    top: 15em;\n    margin-left: 490px;\n    max-width: 300px;\n  }\n}\n\n@media (max-width: 760px) {\n\n  .hide-in-mobile,\n  .site-nav.hide-in-mobile {\n    display: none;\n  }\n\n  #menu-btn {\n    display: inline-block;\n  }\n\n  .posts-group {\n    display: block;\n\n    .post-year {\n      margin: -6px 0 4px;\n    }\n  }\n\n  #spotlight.error-404 {\n    flex-direction: column;\n    text-align: center;\n\n    .banner-404 {\n      margin: 0;\n    }\n  }\n}\n\n@media (max-width: 520px) {\n\n  .content figure.left,\n  .content figure.right {\n    float: unset;\n    max-width: 100%;\n    margin: 0;\n  }\n\n  hr.post-end {\n    width: 60%;\n  }\n\n  #mobile-menu {\n    right: 1.2em;\n  }\n}\n\n.utterances {\n  max-width: 900px;\n}\n"
  },
  {
    "path": "themes/hermit/exampleSite/config.toml",
    "content": "baseURL = \"https://example.com\"\nlanguageCode = \"en-us\"\ndefaultContentLanguage = \"en\"\ntitle = \"Hugo Hermit\"\ntheme = \"hermit\"\n# enableGitInfo = true\npygmentsCodefences  = true\npygmentsUseClasses  = true\n# hasCJKLanguage = true  # If Chinese/Japanese/Korean is your main content language, enable this to make wordCount works right.\nrssLimit = 10  # Maximum number of items in the RSS feed.\ncopyright = \"This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.\" # This message is only used by the RSS template.\nenableEmoji = true  # Shorthand emojis in content files - https://gohugo.io/functions/emojify/\n# googleAnalytics = \"UA-123-45\"\n# disqusShortname = \"yourdiscussshortname\"\n\n[author]\n  name = \"John Doe\"\n\n[blackfriday]\n  # hrefTargetBlank = true\n  # noreferrerLinks = true\n  # nofollowLinks = true\n\n[taxonomies]\n  tag = \"tags\"\n  # Categories are disabled by default.\n\n[params]\n  dateform        = \"Jan 2, 2006\"\n  dateformShort   = \"Jan 2\"\n  dateformNum     = \"2006-01-02\"\n  dateformNumTime = \"2006-01-02 15:04 -0700\"\n\n  # Metadata mostly used in document's head\n  # description = \"\"\n  # images = [\"\"]\n  themeColor = \"#494f5c\"\n\n  homeSubtitle = \"A minimal and fast theme for Hugo.\"\n  footerCopyright = ' &#183; <a href=\"https://creativecommons.org/licenses/by-nc/4.0/\" target=\"_blank\" rel=\"noopener\">CC BY-NC 4.0</a>'\n  # bgImg = \"\"  # Homepage background-image URL\n\n  # Prefix of link to the git commit detail page. GitInfo must be enabled.\n  # gitUrl = \"https://github.com/username/repository/commit/\"\n\n  # Toggling this option needs to rebuild SCSS, requires Hugo extended version\n  justifyContent = false  # Set \"text-align: justify\" to `.content`.\n\n  # Add custom css\n  # customCSS = [\"css/foo.css\", \"css/bar.css\"]\n\n  # Social Icons\n  # Check https://github.com/Track3/hermit#social-icons for more info.\n  [[params.social]]\n    name = \"twitter\"\n    url = \"https://twitter.com/\"\n\n  [[params.social]]\n    name = \"instagram\"\n    url = \"https://instagram.com/\"\n\n  [[params.social]]\n    name = \"github\"\n    url = \"https://github.com/\"\n\n[menu]\n\n  [[menu.main]]\n    name = \"Posts\"\n    url = \"posts/\"\n    weight = 10\n\n  [[menu.main]]\n    name = \"About\"\n    url = \"about-hugo/\"\n    weight = 20\n"
  },
  {
    "path": "themes/hermit/exampleSite/content/about-hugo.md",
    "content": "+++\ntitle = \"About Hugo\"\ndate = \"2014-04-09\"\n+++\n\nHugo is the **world’s fastest framework for building websites**. It is written in Go.\n\nIt makes use of a variety of open source projects including:\n\n* https://github.com/russross/blackfriday\n* https://github.com/alecthomas/chroma\n* https://github.com/muesli/smartcrop\n* https://github.com/spf13/cobra\n* https://github.com/spf13/viper\n\nLearn more and contribute on [GitHub](https://github.com/gohugoio).\n\n"
  },
  {
    "path": "themes/hermit/exampleSite/content/posts/creating-a-new-theme.md",
    "content": "---\nauthor: \"Michael Henderson\"\ndate: 2014-09-28\ntitle: Creating a New Theme\n---\n\n## Introduction\n\nThis tutorial will show you how to create a simple theme in Hugo. I assume that you are familiar with HTML, the bash command line, and that you are comfortable using Markdown to format content. I'll explain how Hugo uses templates and how you can organize your templates to create a theme. I won't cover using CSS to style your theme.\n\nWe'll start with creating a new site with a very basic template. Then we'll add in a few pages and posts. With small variations on that, you will be able to create many different types of web sites.\n\nIn this tutorial, commands that you enter will start with the \"$\" prompt. The output will follow. Lines that start with \"#\" are comments that I've added to explain a point. When I show updates to a file, the \":wq\" on the last line means to save the file.\n\nHere's an example:\n\n```\n## this is a comment\n$ echo this is a command\nthis is a command\n\n## edit the file\n$ vi foo.md\n+++\ndate = \"2014-09-28\"\ntitle = \"creating a new theme\"\n+++\n\nbah and humbug\n:wq\n\n## show it\n$ cat foo.md\n+++\ndate = \"2014-09-28\"\ntitle = \"creating a new theme\"\n+++\n\nbah and humbug\n$\n```\n\n\n## Some Definitions\n\nThere are a few concepts that you need to understand before creating a theme.\n\n### Skins\n\nSkins are the files responsible for the look and feel of your site. It’s the CSS that controls colors and fonts, it’s the Javascript that determines actions and reactions. It’s also the rules that Hugo uses to transform your content into the HTML that the site will serve to visitors.\n\nYou have two ways to create a skin. The simplest way is to create it in the ```layouts/``` directory. If you do, then you don’t have to worry about configuring Hugo to recognize it. The first place that Hugo will look for rules and files is in the ```layouts/``` directory so it will always find the skin.\n\nYour second choice is to create it in a sub-directory of the ```themes/``` directory. If you do, then you must always tell Hugo where to search for the skin. It’s extra work, though, so why bother with it?\n\nThe difference between creating a skin in ```layouts/``` and creating it in ```themes/``` is very subtle. A skin in ```layouts/``` can’t be customized without updating the templates and static files that it is built from. A skin created in ```themes/```, on the other hand, can be and that makes it easier for other people to use it.\n\nThe rest of this tutorial will call a skin created in the ```themes/``` directory a theme.\n\nNote that you can use this tutorial to create a skin in the ```layouts/``` directory if you wish to. The main difference will be that you won’t need to update the site’s configuration file to use a theme.\n\n### The Home Page\n\nThe home page, or landing page, is the first page that many visitors to a site see. It is the index.html file in the root directory of the web site. Since Hugo writes files to the public/ directory, our home page is public/index.html.\n\n### Site Configuration File\n\nWhen Hugo runs, it looks for a configuration file that contains settings that override default values for the entire site. The file can use TOML, YAML, or JSON. I prefer to use TOML for my configuration files. If you prefer to use JSON or YAML, you’ll need to translate my examples. You’ll also need to change the name of the file since Hugo uses the extension to determine how to process it.\n\nHugo translates Markdown files into HTML. By default, Hugo expects to find Markdown files in your ```content/``` directory and template files in your ```themes/``` directory. It will create HTML files in your ```public/``` directory. You can change this by specifying alternate locations in the configuration file.\n\n### Content\n\nContent is stored in text files that contain two sections. The first section is the “front matter,” which is the meta-information on the content. The second section contains Markdown that will be converted to HTML.\n\n#### Front Matter\n\nThe front matter is information about the content. Like the configuration file, it can be written in TOML, YAML, or JSON. Unlike the configuration file, Hugo doesn’t use the file’s extension to know the format. It looks for markers to signal the type. TOML is surrounded by “`+++`”, YAML by “`---`”, and JSON is enclosed in curly braces. I prefer to use TOML, so you’ll need to translate my examples if you prefer YAML or JSON.\n\nThe information in the front matter is passed into the template before the content is rendered into HTML.\n\n#### Markdown\n\nContent is written in Markdown which makes it easier to create the content. Hugo runs the content through a Markdown engine to create the HTML which will be written to the output file.\n\n### Template Files\n\nHugo uses template files to render content into HTML. Template files are a bridge between the content and presentation. Rules in the template define what content is published, where it's published to, and how it will rendered to the HTML file. The template guides the presentation by specifying the style to use.\n\nThere are three types of templates: single, list, and partial. Each type takes a bit of content as input and transforms it based on the commands in the template.\n\nHugo uses its knowledge of the content to find the template file used to render the content. If it can’t find a template that is an exact match for the content, it will shift up a level and search from there. It will continue to do so until it finds a matching template or runs out of templates to try. If it can’t find a template, it will use the default template for the site.\n\nPlease note that you can use the front matter to influence Hugo’s choice of templates.\n\n#### Single Template\n\nA single template is used to render a single piece of content. For example, an article or post would be a single piece of content and use a single template.\n\n#### List Template\n\nA list template renders a group of related content. That could be a summary of recent postings or all articles in a category. List templates can contain multiple groups.\n\nThe homepage template is a special type of list template. Hugo assumes that the home page of your site will act as the portal for the rest of the content in the site.\n\n#### Partial Template\n\nA partial template is a template that can be included in other templates. Partial templates must be called using the “partial” template command. They are very handy for rolling up common behavior. For example, your site may have a banner that all pages use. Instead of copying the text of the banner into every single and list template, you could create a partial with the banner in it. That way if you decide to change the banner, you only have to change the partial template.\n\n## Create a New Site\n\nLet's use Hugo to create a new web site. I'm a Mac user, so I'll create mine in my home directory, in the Sites folder. If you're using Linux, you might have to create the folder first.\n\nThe \"new site\" command will create a skeleton of a site. It will give you the basic directory structure and a useable configuration file.\n\n```\n$ hugo new site ~/Sites/zafta\n$ cd ~/Sites/zafta\n$ ls -l\ntotal 8\ndrwxr-xr-x  7 quoha  staff  238 Sep 29 16:49 .\ndrwxr-xr-x  3 quoha  staff  102 Sep 29 16:49 ..\ndrwxr-xr-x  2 quoha  staff   68 Sep 29 16:49 archetypes\n-rw-r--r--  1 quoha  staff   82 Sep 29 16:49 config.toml\ndrwxr-xr-x  2 quoha  staff   68 Sep 29 16:49 content\ndrwxr-xr-x  2 quoha  staff   68 Sep 29 16:49 layouts\ndrwxr-xr-x  2 quoha  staff   68 Sep 29 16:49 static\n$\n```\n\nTake a look in the content/ directory to confirm that it is empty.\n\nThe other directories (archetypes/, layouts/, and static/) are used when customizing a theme. That's a topic for a different tutorial, so please ignore them for now.\n\n### Generate the HTML For the New Site\n\nRunning the `hugo` command with no options will read all the available content and generate the HTML files. It will also copy all static files (that's everything that's not content). Since we have an empty site, it won't do much, but it will do it very quickly.\n\n```\n$ hugo --verbose\nINFO: 2014/09/29 Using config file: config.toml\nINFO: 2014/09/29 syncing from /Users/quoha/Sites/zafta/static/ to /Users/quoha/Sites/zafta/public/\nWARN: 2014/09/29 Unable to locate layout: [index.html _default/list.html _default/single.html]\nWARN: 2014/09/29 Unable to locate layout: [404.html]\n0 draft content \n0 future content \n0 pages created \n0 tags created\n0 categories created\nin 2 ms\n$ \n```\n\nThe \"`--verbose`\" flag gives extra information that will be helpful when we build the template. Every line of the output that starts with \"INFO:\" or \"WARN:\" is present because we used that flag. The lines that start with \"WARN:\" are warning messages. We'll go over them later.\n\nWe can verify that the command worked by looking at the directory again.\n\n```\n$ ls -l\ntotal 8\ndrwxr-xr-x  2 quoha  staff   68 Sep 29 16:49 archetypes\n-rw-r--r--  1 quoha  staff   82 Sep 29 16:49 config.toml\ndrwxr-xr-x  2 quoha  staff   68 Sep 29 16:49 content\ndrwxr-xr-x  2 quoha  staff   68 Sep 29 16:49 layouts\ndrwxr-xr-x  4 quoha  staff  136 Sep 29 17:02 public\ndrwxr-xr-x  2 quoha  staff   68 Sep 29 16:49 static\n$\n```\n\nSee that new public/ directory? Hugo placed all generated content there. When you're ready to publish your web site, that's the place to start. For now, though, let's just confirm that we have what we'd expect from a site with no content.\n\n```\n$ ls -l public\ntotal 16\n-rw-r--r--  1 quoha  staff  416 Sep 29 17:02 index.xml\n-rw-r--r--  1 quoha  staff  262 Sep 29 17:02 sitemap.xml\n$ \n```\n\nHugo created two XML files, which is standard, but there are no HTML files.\n\n\n\n### Test the New Site\n\nVerify that you can run the built-in web server. It will dramatically shorten your development cycle if you do. Start it by running the \"server\" command. If it is successful, you will see output similar to the following:\n\n```\n$ hugo server --verbose\nINFO: 2014/09/29 Using config file: /Users/quoha/Sites/zafta/config.toml\nINFO: 2014/09/29 syncing from /Users/quoha/Sites/zafta/static/ to /Users/quoha/Sites/zafta/public/\nWARN: 2014/09/29 Unable to locate layout: [index.html _default/list.html _default/single.html]\nWARN: 2014/09/29 Unable to locate layout: [404.html]\n0 draft content \n0 future content \n0 pages created \n0 tags created\n0 categories created\nin 2 ms\nServing pages from /Users/quoha/Sites/zafta/public\nWeb Server is available at http://localhost:1313\nPress Ctrl+C to stop\n```\n\nConnect to the listed URL (it's on the line that starts with \"Web Server\"). If everything is working correctly, you should get a page that shows the following:\n\n```\nindex.xml\nsitemap.xml\n```\n\nThat's a listing of your public/ directory. Hugo didn't create a home page because our site has no content. When there's no index.html file in a directory, the server lists the files in the directory, which is what you should see in your browser.\n\nLet’s go back and look at those warnings again.\n\n```\nWARN: 2014/09/29 Unable to locate layout: [index.html _default/list.html _default/single.html]\nWARN: 2014/09/29 Unable to locate layout: [404.html]\n```\n\nThat second warning is easier to explain. We haven’t created a template to be used to generate “page not found errors.” The 404 message is a topic for a separate tutorial.\n\nNow for the first warning. It is for the home page. You can tell because the first layout that it looked for was “index.html.” That’s only used by the home page.\n\nI like that the verbose flag causes Hugo to list the files that it's searching for. For the home page, they are index.html, _default/list.html, and _default/single.html. There are some rules that we'll cover later that explain the names and paths. For now, just remember that Hugo couldn't find a template for the home page and it told you so.\n\nAt this point, you've got a working installation and site that we can build upon. All that’s left is to add some content and a theme to display it.\n\n## Create a New Theme\n\nHugo doesn't ship with a default theme. There are a few available (I counted a dozen when I first installed Hugo) and Hugo comes with a command to create new themes.\n\nWe're going to create a new theme called \"zafta.\" Since the goal of this tutorial is to show you how to fill out the files to pull in your content, the theme will not contain any CSS. In other words, ugly but functional.\n\nAll themes have opinions on content and layout. For example, Zafta uses \"post\" over \"blog\". Strong opinions make for simpler templates but differing opinions make it tougher to use themes. When you build a theme, consider using the terms that other themes do.\n\n\n### Create a Skeleton\n\nUse the hugo \"new\" command to create the skeleton of a theme. This creates the directory structure and places empty files for you to fill out.\n\n```\n$ hugo new theme zafta\n\n$ ls -l\ntotal 8\ndrwxr-xr-x  2 quoha  staff   68 Sep 29 16:49 archetypes\n-rw-r--r--  1 quoha  staff   82 Sep 29 16:49 config.toml\ndrwxr-xr-x  2 quoha  staff   68 Sep 29 16:49 content\ndrwxr-xr-x  2 quoha  staff   68 Sep 29 16:49 layouts\ndrwxr-xr-x  4 quoha  staff  136 Sep 29 17:02 public\ndrwxr-xr-x  2 quoha  staff   68 Sep 29 16:49 static\ndrwxr-xr-x  3 quoha  staff  102 Sep 29 17:31 themes\n\n$ find themes -type f | xargs ls -l\n-rw-r--r--  1 quoha  staff  1081 Sep 29 17:31 themes/zafta/LICENSE.md\n-rw-r--r--  1 quoha  staff     0 Sep 29 17:31 themes/zafta/archetypes/default.md\n-rw-r--r--  1 quoha  staff     0 Sep 29 17:31 themes/zafta/layouts/_default/list.html\n-rw-r--r--  1 quoha  staff     0 Sep 29 17:31 themes/zafta/layouts/_default/single.html\n-rw-r--r--  1 quoha  staff     0 Sep 29 17:31 themes/zafta/layouts/index.html\n-rw-r--r--  1 quoha  staff     0 Sep 29 17:31 themes/zafta/layouts/partials/footer.html\n-rw-r--r--  1 quoha  staff     0 Sep 29 17:31 themes/zafta/layouts/partials/header.html\n-rw-r--r--  1 quoha  staff    93 Sep 29 17:31 themes/zafta/theme.toml\n$ \n```\n\nThe skeleton includes templates (the files ending in .html), license file, a description of your theme (the theme.toml file), and an empty archetype.\n\nPlease take a minute to fill out the theme.toml and LICENSE.md files. They're optional, but if you're going to be distributing your theme, it tells the world who to praise (or blame). It's also nice to declare the license so that people will know how they can use the theme.\n\n```\n$ vi themes/zafta/theme.toml\nauthor = \"michael d henderson\"\ndescription = \"a minimal working template\"\nlicense = \"MIT\"\nname = \"zafta\"\nsource_repo = \"\"\ntags = [\"tags\", \"categories\"]\n:wq\n\n## also edit themes/zafta/LICENSE.md and change\n## the bit that says \"YOUR_NAME_HERE\"\n```\n\nNote that the the skeleton's template files are empty. Don't worry, we'll be changing that shortly.\n\n```\n$ find themes/zafta -name '*.html' | xargs ls -l\n-rw-r--r--  1 quoha  staff  0 Sep 29 17:31 themes/zafta/layouts/_default/list.html\n-rw-r--r--  1 quoha  staff  0 Sep 29 17:31 themes/zafta/layouts/_default/single.html\n-rw-r--r--  1 quoha  staff  0 Sep 29 17:31 themes/zafta/layouts/index.html\n-rw-r--r--  1 quoha  staff  0 Sep 29 17:31 themes/zafta/layouts/partials/footer.html\n-rw-r--r--  1 quoha  staff  0 Sep 29 17:31 themes/zafta/layouts/partials/header.html\n$\n```\n\n\n\n### Update the Configuration File to Use the Theme\n\nNow that we've got a theme to work with, it's a good idea to add the theme name to the configuration file. This is optional, because you can always add \"-t zafta\" on all your commands. I like to put it the configuration file because I like shorter command lines. If you don't put it in the configuration file or specify it on the command line, you won't use the template that you're expecting to.\n\nEdit the file to add the theme, add a title for the site, and specify that all of our content will use the TOML format.\n\n```\n$ vi config.toml\ntheme = \"zafta\"\nbaseurl = \"\"\nlanguageCode = \"en-us\"\ntitle = \"zafta - totally refreshing\"\nMetaDataFormat = \"toml\"\n:wq\n\n$\n```\n\n### Generate the Site\n\nNow that we have an empty theme, let's generate the site again.\n\n```\n$ hugo --verbose\nINFO: 2014/09/29 Using config file: /Users/quoha/Sites/zafta/config.toml\nINFO: 2014/09/29 syncing from /Users/quoha/Sites/zafta/themes/zafta/static/ to /Users/quoha/Sites/zafta/public/\nINFO: 2014/09/29 syncing from /Users/quoha/Sites/zafta/static/ to /Users/quoha/Sites/zafta/public/\nWARN: 2014/09/29 Unable to locate layout: [404.html theme/404.html]\n0 draft content \n0 future content \n0 pages created \n0 tags created\n0 categories created\nin 2 ms\n$\n```\n\nDid you notice that the output is different? The warning message for the home page has disappeared and we have an additional information line saying that Hugo is syncing from the theme's directory.\n\nLet's check the public/ directory to see what Hugo's created.\n\n```\n$ ls -l public\ntotal 16\ndrwxr-xr-x  2 quoha  staff   68 Sep 29 17:56 css\n-rw-r--r--  1 quoha  staff    0 Sep 29 17:56 index.html\n-rw-r--r--  1 quoha  staff  407 Sep 29 17:56 index.xml\ndrwxr-xr-x  2 quoha  staff   68 Sep 29 17:56 js\n-rw-r--r--  1 quoha  staff  243 Sep 29 17:56 sitemap.xml\n$\n```\n\nNotice four things:\n\n1. Hugo created a home page. This is the file public/index.html.\n2. Hugo created a css/ directory.\n3. Hugo created a js/ directory.\n4. Hugo claimed that it created 0 pages. It created a file and copied over static files, but didn't create any pages. That's because it considers a \"page\" to be a file created directly from a content file. It doesn't count things like the index.html files that it creates automatically.\n\n#### The Home Page\n\nHugo supports many different types of templates. The home page is special because it gets its own type of template and its own template file. The file, layouts/index.html, is used to generate the HTML for the home page. The Hugo documentation says that this is the only required template, but that depends. Hugo's warning message shows that it looks for three different templates:\n\n```\nWARN: 2014/09/29 Unable to locate layout: [index.html _default/list.html _default/single.html]\n```\n\nIf it can't find any of these, it completely skips creating the home page. We noticed that when we built the site without having a theme installed.\n\nWhen Hugo created our theme, it created an empty home page template. Now, when we build the site, Hugo finds the template and uses it to generate the HTML for the home page. Since the template file is empty, the HTML file is empty, too. If the template had any rules in it, then Hugo would have used them to generate the home page.\n\n```\n$ find . -name index.html | xargs ls -l\n-rw-r--r--  1 quoha  staff  0 Sep 29 20:21 ./public/index.html\n-rw-r--r--  1 quoha  staff  0 Sep 29 17:31 ./themes/zafta/layouts/index.html\n$ \n```\n\n#### The Magic of Static\n\nHugo does two things when generating the site. It uses templates to transform content into HTML and it copies static files into the site. Unlike content, static files are not transformed. They are copied exactly as they are.\n\nHugo assumes that your site will use both CSS and JavaScript, so it creates directories in your theme to hold them. Remember opinions? Well, Hugo's opinion is that you'll store your CSS in a directory named css/ and your JavaScript in a directory named js/. If you don't like that, you can change the directory names in your theme directory or even delete them completely. Hugo's nice enough to offer its opinion, then behave nicely if you disagree.\n\n```\n$ find themes/zafta -type d | xargs ls -ld\ndrwxr-xr-x  7 quoha  staff  238 Sep 29 17:38 themes/zafta\ndrwxr-xr-x  3 quoha  staff  102 Sep 29 17:31 themes/zafta/archetypes\ndrwxr-xr-x  5 quoha  staff  170 Sep 29 17:31 themes/zafta/layouts\ndrwxr-xr-x  4 quoha  staff  136 Sep 29 17:31 themes/zafta/layouts/_default\ndrwxr-xr-x  4 quoha  staff  136 Sep 29 17:31 themes/zafta/layouts/partials\ndrwxr-xr-x  4 quoha  staff  136 Sep 29 17:31 themes/zafta/static\ndrwxr-xr-x  2 quoha  staff   68 Sep 29 17:31 themes/zafta/static/css\ndrwxr-xr-x  2 quoha  staff   68 Sep 29 17:31 themes/zafta/static/js\n$ \n```\n\n## The Theme Development Cycle\n\nWhen you're working on a theme, you will make changes in the theme's directory, rebuild the site, and check your changes in the browser. Hugo makes this very easy:\n\n1. Purge the public/ directory.\n2. Run the built in web server in watch mode.\n3. Open your site in a browser.\n4. Update the theme.\n5. Glance at your browser window to see changes.\n6. Return to step 4.\n\nI’ll throw in one more opinion: never work on a theme on a live site. Always work on a copy of your site. Make changes to your theme, test them, then copy them up to your site. For added safety, use a tool like Git to keep a revision history of your content and your theme. Believe me when I say that it is too easy to lose both your mind and your changes.\n\nCheck the main Hugo site for information on using Git with Hugo.\n\n### Purge the public/ Directory\n\nWhen generating the site, Hugo will create new files and update existing ones in the ```public/``` directory. It will not delete files that are no longer used. For example, files that were created in the wrong directory or with the wrong title will remain. If you leave them, you might get confused by them later. I recommend cleaning out your site prior to generating it.\n\nNote: If you're building on an SSD, you should ignore this. Churning on a SSD can be costly.\n\n### Hugo's Watch Option\n\nHugo's \"`--watch`\" option will monitor the content/ and your theme directories for changes and rebuild the site automatically.\n\n### Live Reload\n\nHugo's built in web server supports live reload. As pages are saved on the server, the browser is told to refresh the page. Usually, this happens faster than you can say, \"Wow, that's totally amazing.\"\n\n### Development Commands\n\nUse the following commands as the basis for your workflow.\n\n```\n## purge old files. hugo will recreate the public directory.\n##\n$ rm -rf public\n##\n## run hugo in watch mode\n##\n$ hugo server --watch --verbose\n```\n\nHere's sample output showing Hugo detecting a change to the template for the home page. Once generated, the web browser automatically reloaded the page. I've said this before, it's amazing.\n\n\n```\n$ rm -rf public\n$ hugo server --watch --verbose\nINFO: 2014/09/29 Using config file: /Users/quoha/Sites/zafta/config.toml\nINFO: 2014/09/29 syncing from /Users/quoha/Sites/zafta/themes/zafta/static/ to /Users/quoha/Sites/zafta/public/\nINFO: 2014/09/29 syncing from /Users/quoha/Sites/zafta/static/ to /Users/quoha/Sites/zafta/public/\nWARN: 2014/09/29 Unable to locate layout: [404.html theme/404.html]\n0 draft content \n0 future content \n0 pages created \n0 tags created\n0 categories created\nin 2 ms\nWatching for changes in /Users/quoha/Sites/zafta/content\nServing pages from /Users/quoha/Sites/zafta/public\nWeb Server is available at http://localhost:1313\nPress Ctrl+C to stop\nINFO: 2014/09/29 File System Event: [\"/Users/quoha/Sites/zafta/themes/zafta/layouts/index.html\": MODIFY|ATTRIB]\nChange detected, rebuilding site\n\nWARN: 2014/09/29 Unable to locate layout: [404.html theme/404.html]\n0 draft content \n0 future content \n0 pages created \n0 tags created\n0 categories created\nin 1 ms\n```\n\n## Update the Home Page Template\n\nThe home page is one of a few special pages that Hugo creates automatically. As mentioned earlier, it looks for one of three files in the theme's layout/ directory:\n\n1. index.html\n2. _default/list.html\n3. _default/single.html\n\nWe could update one of the default templates, but a good design decision is to update the most specific template available. That's not a hard and fast rule (in fact, we'll break it a few times in this tutorial), but it is a good generalization.\n\n### Make a Static Home Page\n\nRight now, that page is empty because we don't have any content and we don't have any logic in the template. Let's change that by adding some text to the template.\n\n```\n$ vi themes/zafta/layouts/index.html\n<!DOCTYPE html> \n<html> \n<body> \n  <p>hugo says hello!</p> \n</body> \n</html> \n:wq\n\n$\n```\n\nBuild the web site and then verify the results.\n\n```\n$ hugo --verbose\nINFO: 2014/09/29 Using config file: /Users/quoha/Sites/zafta/config.toml\nINFO: 2014/09/29 syncing from /Users/quoha/Sites/zafta/themes/zafta/static/ to /Users/quoha/Sites/zafta/public/\nINFO: 2014/09/29 syncing from /Users/quoha/Sites/zafta/static/ to /Users/quoha/Sites/zafta/public/\nWARN: 2014/09/29 Unable to locate layout: [404.html theme/404.html]\n0 draft content \n0 future content \n0 pages created \n0 tags created\n0 categories created\nin 2 ms\n\n$ find public -type f -name '*.html' | xargs ls -l\n-rw-r--r--  1 quoha  staff  78 Sep 29 21:26 public/index.html\n\n$ cat public/index.html \n<!DOCTYPE html> \n<html> \n<body> \n  <p>hugo says hello!</p> \n</html>\n```\n\n#### Live Reload\n\nNote: If you're running the server with the `--watch` option, you'll see different content in the file:\n\n```\n$ cat public/index.html \n<!DOCTYPE html> \n<html> \n<body> \n  <p>hugo says hello!</p> \n<script>document.write('<script src=\"http://' \n        + (location.host || 'localhost').split(':')[0] \n    + ':1313/livereload.js?mindelay=10\"></' \n        + 'script>')</script></body> \n</html>\n```\n\nWhen you use `--watch`, the Live Reload script is added by Hugo. Look for live reload in the documentation to see what it does and how to disable it.\n\n### Build a \"Dynamic\" Home Page\n\n\"Dynamic home page?\" Hugo's a static web site generator, so this seems an odd thing to say. I mean let's have the home page automatically reflect the content in the site every time Hugo builds it. We'll use iteration in the template to do that.\n\n#### Create New Posts\n\nNow that we have the home page generating static content, let's add some content to the site. We'll display these posts as a list on the home page and on their own page, too.\n\nHugo has a command to generate a skeleton post, just like it does for sites and themes.\n\n```\n$ hugo --verbose new post/first.md\nINFO: 2014/09/29 Using config file: /Users/quoha/Sites/zafta/config.toml\nINFO: 2014/09/29 attempting to create  post/first.md of post\nINFO: 2014/09/29 curpath: /Users/quoha/Sites/zafta/themes/zafta/archetypes/default.md\nERROR: 2014/09/29 Unable to Cast <nil> to map[string]interface{}\n\n$ \n```\n\nThat wasn't very nice, was it?\n\nThe \"new\" command uses an archetype to create the post file. Hugo created an empty default archetype file, but that causes an error when there's a theme. For me, the workaround was to create an archetypes file specifically for the post type.\n\n```\n$ vi themes/zafta/archetypes/post.md\n+++\nDescription = \"\"\nTags = []\nCategories = []\n+++\n:wq\n\n$ find themes/zafta/archetypes -type f | xargs ls -l\n-rw-r--r--  1 quoha  staff   0 Sep 29 21:53 themes/zafta/archetypes/default.md\n-rw-r--r--  1 quoha  staff  51 Sep 29 21:54 themes/zafta/archetypes/post.md\n\n$ hugo --verbose new post/first.md\nINFO: 2014/09/29 Using config file: /Users/quoha/Sites/zafta/config.toml\nINFO: 2014/09/29 attempting to create  post/first.md of post\nINFO: 2014/09/29 curpath: /Users/quoha/Sites/zafta/themes/zafta/archetypes/post.md\nINFO: 2014/09/29 creating /Users/quoha/Sites/zafta/content/post/first.md\n/Users/quoha/Sites/zafta/content/post/first.md created\n\n$ hugo --verbose new post/second.md\nINFO: 2014/09/29 Using config file: /Users/quoha/Sites/zafta/config.toml\nINFO: 2014/09/29 attempting to create  post/second.md of post\nINFO: 2014/09/29 curpath: /Users/quoha/Sites/zafta/themes/zafta/archetypes/post.md\nINFO: 2014/09/29 creating /Users/quoha/Sites/zafta/content/post/second.md\n/Users/quoha/Sites/zafta/content/post/second.md created\n\n$ ls -l content/post\ntotal 16\n-rw-r--r--  1 quoha  staff  104 Sep 29 21:54 first.md\n-rw-r--r--  1 quoha  staff  105 Sep 29 21:57 second.md\n\n$ cat content/post/first.md \n+++\nCategories = []\nDescription = \"\"\nTags = []\ndate = \"2014-09-29T21:54:53-05:00\"\ntitle = \"first\"\n\n+++\nmy first post\n\n$ cat content/post/second.md \n+++\nCategories = []\nDescription = \"\"\nTags = []\ndate = \"2014-09-29T21:57:09-05:00\"\ntitle = \"second\"\n\n+++\nmy second post\n\n$ \n```\n\nBuild the web site and then verify the results.\n\n```\n$ rm -rf public\n$ hugo --verbose\nINFO: 2014/09/29 Using config file: /Users/quoha/Sites/zafta/config.toml\nINFO: 2014/09/29 syncing from /Users/quoha/Sites/zafta/themes/zafta/static/ to /Users/quoha/Sites/zafta/public/\nINFO: 2014/09/29 syncing from /Users/quoha/Sites/zafta/static/ to /Users/quoha/Sites/zafta/public/\nINFO: 2014/09/29 found taxonomies: map[string]string{\"category\":\"categories\", \"tag\":\"tags\"}\nWARN: 2014/09/29 Unable to locate layout: [404.html theme/404.html]\n0 draft content \n0 future content \n2 pages created \n0 tags created\n0 categories created\nin 4 ms\n$\n```\n\nThe output says that it created 2 pages. Those are our new posts:\n\n```\n$ find public -type f -name '*.html' | xargs ls -l\n-rw-r--r--  1 quoha  staff  78 Sep 29 22:13 public/index.html\n-rw-r--r--  1 quoha  staff   0 Sep 29 22:13 public/post/first/index.html\n-rw-r--r--  1 quoha  staff   0 Sep 29 22:13 public/post/index.html\n-rw-r--r--  1 quoha  staff   0 Sep 29 22:13 public/post/second/index.html\n$\n```\n\nThe new files are empty because because the templates used to generate the content are empty. The homepage doesn't show the new content, either. We have to update the templates to add the posts.\n\n### List and Single Templates\n\nIn Hugo, we have three major kinds of templates. There's the home page template that we updated previously. It is used only by the home page. We also have \"single\" templates which are used to generate output for a single content file. We also have \"list\" templates that are used to group multiple pieces of content before generating output.\n\nGenerally speaking, list templates are named \"list.html\" and single templates are named \"single.html.\"\n\nThere are three other types of templates: partials, content views, and terms. We will not go into much detail on these.\n\n### Add Content to the Homepage\n\nThe home page will contain a list of posts. Let's update its template to add the posts that we just created. The logic in the template will run every time we build the site.\n\n```\n$ vi themes/zafta/layouts/index.html \n<!DOCTYPE html>\n<html>\n<body>\n  {{ range first 10 .Data.Pages }}\n    <h1>{{ .Title }}</h1>\n  {{ end }}\n</body>\n</html>\n:wq\n\n$\n```\n\nHugo uses the Go template engine. That engine scans the template files for commands which are enclosed between \"{{\" and \"}}\". In our template, the commands are:\n\n1. range\n2. .Title\n3. end\n\nThe \"range\" command is an iterator. We're going to use it to go through the first ten pages. Every HTML file that Hugo creates is treated as a page, so looping through the list of pages will look at every file that will be created.\n\nThe \".Title\" command prints the value of the \"title\" variable. Hugo pulls it from the front matter in the Markdown file.\n\nThe \"end\" command signals the end of the range iterator. The engine loops back to the top of the iteration when it finds \"end.\" Everything between the \"range\" and \"end\" is evaluated every time the engine goes through the iteration. In this file, that would cause the title from the first ten pages to be output as heading level one.\n\nIt's helpful to remember that some variables, like .Data, are created before any output files. Hugo loads every content file into the variable and then gives the template a chance to process before creating the HTML files.\n\nBuild the web site and then verify the results.\n\n```\n$ rm -rf public\n$ hugo --verbose\nINFO: 2014/09/29 Using config file: /Users/quoha/Sites/zafta/config.toml\nINFO: 2014/09/29 syncing from /Users/quoha/Sites/zafta/themes/zafta/static/ to /Users/quoha/Sites/zafta/public/\nINFO: 2014/09/29 syncing from /Users/quoha/Sites/zafta/static/ to /Users/quoha/Sites/zafta/public/\nINFO: 2014/09/29 found taxonomies: map[string]string{\"tag\":\"tags\", \"category\":\"categories\"}\nWARN: 2014/09/29 Unable to locate layout: [404.html theme/404.html]\n0 draft content \n0 future content \n2 pages created \n0 tags created\n0 categories created\nin 4 ms\n$ find public -type f -name '*.html' | xargs ls -l \n-rw-r--r--  1 quoha  staff  94 Sep 29 22:23 public/index.html\n-rw-r--r--  1 quoha  staff   0 Sep 29 22:23 public/post/first/index.html\n-rw-r--r--  1 quoha  staff   0 Sep 29 22:23 public/post/index.html\n-rw-r--r--  1 quoha  staff   0 Sep 29 22:23 public/post/second/index.html\n$ cat public/index.html \n<!DOCTYPE html>\n<html>\n<body>\n  \n    <h1>second</h1>\n  \n    <h1>first</h1>\n  \n</body>\n</html>\n$\n```\n\nCongratulations, the home page shows the title of the two posts. The posts themselves are still empty, but let's take a moment to appreciate what we've done. Your template now generates output dynamically. Believe it or not, by inserting the range command inside of those curly braces, you've learned everything you need to know to build a theme. All that's really left is understanding which template will be used to generate each content file and becoming familiar with the commands for the template engine.\n\nAnd, if that were entirely true, this tutorial would be much shorter. There are a few things to know that will make creating a new template much easier. Don't worry, though, that's all to come.\n\n### Add Content to the Posts\n\nWe're working with posts, which are in the content/post/ directory. That means that their section is \"post\" (and if we don't do something weird, their type is also \"post\").\n\nHugo uses the section and type to find the template file for every piece of content. Hugo will first look for a template file that matches the section or type name. If it can't find one, then it will look in the _default/ directory. There are some twists that we'll cover when we get to categories and tags, but for now we can assume that Hugo will try post/single.html, then _default/single.html.\n\nNow that we know the search rule, let's see what we actually have available:\n\n```\n$ find themes/zafta -name single.html | xargs ls -l\n-rw-r--r--  1 quoha  staff  132 Sep 29 17:31 themes/zafta/layouts/_default/single.html\n```\n\nWe could create a new template, post/single.html, or change the default. Since we don't know of any other content types, let's start with updating the default.\n\nRemember, any content that we haven't created a template for will end up using this template. That can be good or bad. Bad because I know that we're going to be adding different types of content and we're going to end up undoing some of the changes we've made. It's good because we'll be able to see immediate results. It's also good to start here because we can start to build the basic layout for the site. As we add more content types, we'll refactor this file and move logic around. Hugo makes that fairly painless, so we'll accept the cost and proceed.\n\nPlease see the Hugo documentation on template rendering for all the details on determining which template to use. And, as the docs mention, if you're building a single page application (SPA) web site, you can delete all of the other templates and work with just the default single page. That's a refreshing amount of joy right there.\n\n#### Update the Template File\n\n```\n$ vi themes/zafta/layouts/_default/single.html \n<!DOCTYPE html>\n<html>\n<head>\n  <title>{{ .Title }}</title>\n</head>\n<body>\n  <h1>{{ .Title }}</h1>\n  {{ .Content }}\n</body>\n</html>\n:wq\n\n$\n```\n\nBuild the web site and verify the results.\n\n```\n$ rm -rf public\n$ hugo --verbose\nINFO: 2014/09/29 Using config file: /Users/quoha/Sites/zafta/config.toml\nINFO: 2014/09/29 syncing from /Users/quoha/Sites/zafta/themes/zafta/static/ to /Users/quoha/Sites/zafta/public/\nINFO: 2014/09/29 syncing from /Users/quoha/Sites/zafta/static/ to /Users/quoha/Sites/zafta/public/\nINFO: 2014/09/29 found taxonomies: map[string]string{\"tag\":\"tags\", \"category\":\"categories\"}\nWARN: 2014/09/29 Unable to locate layout: [404.html theme/404.html]\n0 draft content \n0 future content \n2 pages created \n0 tags created\n0 categories created\nin 4 ms\n\n$ find public -type f -name '*.html' | xargs ls -l\n-rw-r--r--  1 quoha  staff   94 Sep 29 22:40 public/index.html\n-rw-r--r--  1 quoha  staff  125 Sep 29 22:40 public/post/first/index.html\n-rw-r--r--  1 quoha  staff    0 Sep 29 22:40 public/post/index.html\n-rw-r--r--  1 quoha  staff  128 Sep 29 22:40 public/post/second/index.html\n\n$ cat public/post/first/index.html \n<!DOCTYPE html>\n<html>\n<head>\n  <title>first</title>\n</head>\n<body>\n  <h1>first</h1>\n  <p>my first post</p>\n\n</body>\n</html>\n\n$ cat public/post/second/index.html \n<!DOCTYPE html>\n<html>\n<head>\n  <title>second</title>\n</head>\n<body>\n  <h1>second</h1>\n  <p>my second post</p>\n\n</body>\n</html>\n$\n```\n\nNotice that the posts now have content. You can go to localhost:1313/post/first to verify.\n\n### Linking to Content\n\nThe posts are on the home page. Let's add a link from there to the post. Since this is the home page, we'll update its template.\n\n```\n$ vi themes/zafta/layouts/index.html\n<!DOCTYPE html>\n<html>\n<body>\n  {{ range first 10 .Data.Pages }}\n    <h1><a href=\"{{ .Permalink }}\">{{ .Title }}</a></h1>\n  {{ end }}\n</body>\n</html>\n```\n\nBuild the web site and verify the results.\n\n```\n$ rm -rf public\n$ hugo --verbose\nINFO: 2014/09/29 Using config file: /Users/quoha/Sites/zafta/config.toml\nINFO: 2014/09/29 syncing from /Users/quoha/Sites/zafta/themes/zafta/static/ to /Users/quoha/Sites/zafta/public/\nINFO: 2014/09/29 syncing from /Users/quoha/Sites/zafta/static/ to /Users/quoha/Sites/zafta/public/\nINFO: 2014/09/29 found taxonomies: map[string]string{\"tag\":\"tags\", \"category\":\"categories\"}\nWARN: 2014/09/29 Unable to locate layout: [404.html theme/404.html]\n0 draft content \n0 future content \n2 pages created \n0 tags created\n0 categories created\nin 4 ms\n\n$ find public -type f -name '*.html' | xargs ls -l\n-rw-r--r--  1 quoha  staff  149 Sep 29 22:44 public/index.html\n-rw-r--r--  1 quoha  staff  125 Sep 29 22:44 public/post/first/index.html\n-rw-r--r--  1 quoha  staff    0 Sep 29 22:44 public/post/index.html\n-rw-r--r--  1 quoha  staff  128 Sep 29 22:44 public/post/second/index.html\n\n$ cat public/index.html \n<!DOCTYPE html>\n<html>\n<body>\n  \n    <h1><a href=\"/post/second/\">second</a></h1>\n  \n    <h1><a href=\"/post/first/\">first</a></h1>\n  \n</body>\n</html>\n\n$\n```\n\n### Create a Post Listing\n\nWe have the posts displaying on the home page and on their own page. We also have a file public/post/index.html that is empty. Let's make it show a list of all posts (not just the first ten).\n\nWe need to decide which template to update. This will be a listing, so it should be a list template. Let's take a quick look and see which list templates are available.\n\n```\n$ find themes/zafta -name list.html | xargs ls -l\n-rw-r--r--  1 quoha  staff  0 Sep 29 17:31 themes/zafta/layouts/_default/list.html\n```\n\nAs with the single post, we have to decide to update _default/list.html or create post/list.html. We still don't have multiple content types, so let's stay consistent and update the default list template.\n\n## Creating Top Level Pages\n\nLet's add an \"about\" page and display it at the top level (as opposed to a sub-level like we did with posts).\n\nThe default in Hugo is to use the directory structure of the content/ directory to guide the location of the generated html in the public/ directory. Let's verify that by creating an \"about\" page at the top level:\n\n```\n$ vi content/about.md \n+++\ntitle = \"about\"\ndescription = \"about this site\"\ndate = \"2014-09-27\"\nslug = \"about time\"\n+++\n\n## about us\n\ni'm speechless\n:wq\n```\n\nGenerate the web site and verify the results.\n\n```\n$ find public -name '*.html' | xargs ls -l\n-rw-rw-r--  1 mdhender  staff   334 Sep 27 15:08 public/about-time/index.html\n-rw-rw-r--  1 mdhender  staff   527 Sep 27 15:08 public/index.html\n-rw-rw-r--  1 mdhender  staff   358 Sep 27 15:08 public/post/first-post/index.html\n-rw-rw-r--  1 mdhender  staff     0 Sep 27 15:08 public/post/index.html\n-rw-rw-r--  1 mdhender  staff   342 Sep 27 15:08 public/post/second-post/index.html\n```\n\nNotice that the page wasn't created at the top level. It was created in a sub-directory named 'about-time/'. That name came from our slug. Hugo will use the slug to name the generated content. It's a reasonable default, by the way, but we can learn a few things by fighting it for this file.\n\nOne other thing. Take a look at the home page.\n\n```\n$ cat public/index.html\n<!DOCTYPE html>\n<html>\n<body>\n    <h1><a href=\"http://localhost:1313/post/theme/\">creating a new theme</a></h1>\n    <h1><a href=\"http://localhost:1313/about-time/\">about</a></h1>\n    <h1><a href=\"http://localhost:1313/post/second-post/\">second</a></h1>\n    <h1><a href=\"http://localhost:1313/post/first-post/\">first</a></h1>\n<script>document.write('<script src=\"http://'\n        + (location.host || 'localhost').split(':')[0]\n\t\t+ ':1313/livereload.js?mindelay=10\"></'\n        + 'script>')</script></body>\n</html>\n```\n\nNotice that the \"about\" link is listed with the posts? That's not desirable, so let's change that first.\n\n```\n$ vi themes/zafta/layouts/index.html\n<!DOCTYPE html>\n<html>\n<body>\n  <h1>posts</h1>\n  {{ range first 10 .Data.Pages }}\n    {{ if eq .Type \"post\"}}\n      <h2><a href=\"{{ .Permalink }}\">{{ .Title }}</a></h2>\n    {{ end }}\n  {{ end }}\n\n  <h1>pages</h1>\n  {{ range .Data.Pages }}\n    {{ if eq .Type \"page\" }}\n      <h2><a href=\"{{ .Permalink }}\">{{ .Title }}</a></h2>\n    {{ end }}\n  {{ end }}\n</body>\n</html>\n:wq\n```\n\nGenerate the web site and verify the results. The home page has two sections, posts and pages, and each section has the right set of headings and links in it.\n\nBut, that about page still renders to about-time/index.html.\n\n```\n$ find public -name '*.html' | xargs ls -l\n-rw-rw-r--  1 mdhender  staff    334 Sep 27 15:33 public/about-time/index.html\n-rw-rw-r--  1 mdhender  staff    645 Sep 27 15:33 public/index.html\n-rw-rw-r--  1 mdhender  staff    358 Sep 27 15:33 public/post/first-post/index.html\n-rw-rw-r--  1 mdhender  staff      0 Sep 27 15:33 public/post/index.html\n-rw-rw-r--  1 mdhender  staff    342 Sep 27 15:33 public/post/second-post/index.html\n```\n\nKnowing that hugo is using the slug to generate the file name, the simplest solution is to change the slug. Let's do it the hard way and change the permalink in the configuration file.\n\n```\n$ vi config.toml\n[permalinks]\n\tpage = \"/:title/\"\n\tabout = \"/:filename/\"\n```\n\nGenerate the web site and verify that this didn't work. Hugo lets \"slug\" or \"URL\" override the permalinks setting in the configuration file. Go ahead and comment out the slug in content/about.md, then generate the web site to get it to be created in the right place.\n\n## Sharing Templates\n\nIf you've been following along, you probably noticed that posts have titles in the browser and the home page doesn't. That's because we didn't put the title in the home page's template (layouts/index.html). That's an easy thing to do, but let's look at a different option.\n\nWe can put the common bits into a shared template that's stored in the themes/zafta/layouts/partials/ directory.\n\n### Create the Header and Footer Partials\n\nIn Hugo, a partial is a sugar-coated template. Normally a template reference has a path specified. Partials are different. Hugo searches for them along a TODO defined search path. This makes it easier for end-users to override the theme's presentation.\n\n```\n$ vi themes/zafta/layouts/partials/header.html\n<!DOCTYPE html>\n<html>\n<head>\n\t<title>{{ .Title }}</title>\n</head>\n<body>\n:wq\n\n$ vi themes/zafta/layouts/partials/footer.html\n</body>\n</html>\n:wq\n```\n\n### Update the Home Page Template to Use the Partials\n\nThe most noticeable difference between a template call and a partials call is the lack of path:\n\n```\n{{ template \"theme/partials/header.html\" . }}\n```\nversus\n```\n{{ partial \"header.html\" . }}\n```\nBoth pass in the context.\n\nLet's change the home page template to use these new partials.\n\n```\n$ vi themes/zafta/layouts/index.html\n{{ partial \"header.html\" . }}\n\n  <h1>posts</h1>\n  {{ range first 10 .Data.Pages }}\n    {{ if eq .Type \"post\"}}\n      <h2><a href=\"{{ .Permalink }}\">{{ .Title }}</a></h2>\n    {{ end }}\n  {{ end }}\n\n  <h1>pages</h1>\n  {{ range .Data.Pages }}\n    {{ if or (eq .Type \"page\") (eq .Type \"about\") }}\n      <h2><a href=\"{{ .Permalink }}\">{{ .Type }} - {{ .Title }} - {{ .RelPermalink }}</a></h2>\n    {{ end }}\n  {{ end }}\n\n{{ partial \"footer.html\" . }}\n:wq\n```\n\nGenerate the web site and verify the results. The title on the home page is now \"your title here\", which comes from the \"title\" variable in the config.toml file.\n\n### Update the Default Single Template to Use the Partials\n\n```\n$ vi themes/zafta/layouts/_default/single.html\n{{ partial \"header.html\" . }}\n\n  <h1>{{ .Title }}</h1>\n  {{ .Content }}\n\n{{ partial \"footer.html\" . }}\n:wq\n```\n\nGenerate the web site and verify the results. The title on the posts and the about page should both reflect the value in the markdown file.\n\n## Add “Date Published” to Posts\n\nIt's common to have posts display the date that they were written or published, so let's add that. The front matter of our posts has a variable named \"date.\" It's usually the date the content was created, but let's pretend that's the value we want to display.\n\n### Add “Date Published” to the Template\n\nWe'll start by updating the template used to render the posts. The template code will look like:\n\n```\n{{ .Date.Format \"Mon, Jan 2, 2006\" }}\n```\n\nPosts use the default single template, so we'll change that file.\n\n```\n$ vi themes/zafta/layouts/_default/single.html\n{{ partial \"header.html\" . }}\n\n  <h1>{{ .Title }}</h1>\n  <h2>{{ .Date.Format \"Mon, Jan 2, 2006\" }}</h2>\n  {{ .Content }}\n\n{{ partial \"footer.html\" . }}\n:wq\n```\n\nGenerate the web site and verify the results. The posts now have the date displayed in them. There's a problem, though. The \"about\" page also has the date displayed.\n\nAs usual, there are a couple of ways to make the date display only on posts. We could do an \"if\" statement like we did on the home page. Another way would be to create a separate template for posts.\n\nThe \"if\" solution works for sites that have just a couple of content types. It aligns with the principle of \"code for today,\" too.\n\nLet's assume, though, that we've made our site so complex that we feel we have to create a new template type. In Hugo-speak, we're going to create a section template.\n\nLet's restore the default single template before we forget.\n\n```\n$ mkdir themes/zafta/layouts/post\n$ vi themes/zafta/layouts/_default/single.html\n{{ partial \"header.html\" . }}\n\n  <h1>{{ .Title }}</h1>\n  {{ .Content }}\n\n{{ partial \"footer.html\" . }}\n:wq\n```\n\nNow we'll update the post's version of the single template. If you remember Hugo's rules, the template engine will use this version over the default.\n\n```\n$ vi themes/zafta/layouts/post/single.html\n{{ partial \"header.html\" . }}\n\n  <h1>{{ .Title }}</h1>\n  <h2>{{ .Date.Format \"Mon, Jan 2, 2006\" }}</h2>\n  {{ .Content }}\n\n{{ partial \"footer.html\" . }}\n:wq\n\n```\n\nNote that we removed the date logic from the default template and put it in the post template. Generate the web site and verify the results. Posts have dates and the about page doesn't.\n\n### Don't Repeat Yourself\n\nDRY is a good design goal and Hugo does a great job supporting it. Part of the art of a good template is knowing when to add a new template and when to update an existing one. While you're figuring that out, accept that you'll be doing some refactoring. Hugo makes that easy and fast, so it's okay to delay splitting up a template.\n"
  },
  {
    "path": "themes/hermit/exampleSite/content/posts/goisforlovers.md",
    "content": "+++\ntitle = \"(Hu)go Template Primer\"\ntags = [\n    \"go\",\n    \"golang\",\n    \"templates\",\n    \"themes\",\n    \"development\",\n]\ndate = \"2014-04-02\"\ntoc = true\n+++\n\nHugo uses the excellent [Go][] [html/template][gohtmltemplate] library for\nits template engine. It is an extremely lightweight engine that provides a very\nsmall amount of logic. In our experience that it is just the right amount of\nlogic to be able to create a good static website. If you have used other\ntemplate systems from different languages or frameworks you will find a lot of\nsimilarities in Go templates.\n\nThis document is a brief primer on using Go templates. The [Go docs][gohtmltemplate]\nprovide more details.\n\n## Introduction to Go Templates\n\nGo templates provide an extremely simple template language. It adheres to the\nbelief that only the most basic of logic belongs in the template or view layer.\nOne consequence of this simplicity is that Go templates parse very quickly.\n\nA unique characteristic of Go templates is they are content aware. Variables and\ncontent will be sanitized depending on the context of where they are used. More\ndetails can be found in the [Go docs][gohtmltemplate].\n\n## Basic Syntax\n\nGolang templates are HTML files with the addition of variables and\nfunctions.\n\n**Go variables and functions are accessible within {{ }}**\n\nAccessing a predefined variable \"foo\":\n\n    {{ foo }}\n\n**Parameters are separated using spaces**\n\nCalling the add function with input of 1, 2:\n\n    {{ add 1 2 }}\n\n**Methods and fields are accessed via dot notation**\n\nAccessing the Page Parameter \"bar\"\n\n    {{ .Params.bar }}\n\n**Parentheses can be used to group items together**\n\n    {{ if or (isset .Params \"alt\") (isset .Params \"caption\") }} Caption {{ end }}\n\n\n## Variables\n\nEach Go template has a struct (object) made available to it. In hugo each\ntemplate is passed either a page or a node struct depending on which type of\npage you are rendering. More details are available on the\n[variables](/layout/variables) page.\n\nA variable is accessed by referencing the variable name.\n\n    <title>{{ .Title }}</title>\n\nVariables can also be defined and referenced.\n\n    {{ $address := \"123 Main St.\"}}\n    {{ $address }}\n\n\n## Functions\n\nGo template ship with a few functions which provide basic functionality. The Go\ntemplate system also provides a mechanism for applications to extend the\navailable functions with their own. [Hugo template\nfunctions](/layout/functions) provide some additional functionality we believe\nare useful for building websites. Functions are called by using their name\nfollowed by the required parameters separated by spaces. Template\nfunctions cannot be added without recompiling hugo.\n\n**Example:**\n\n    {{ add 1 2 }}\n\n## Includes\n\nWhen including another template you will pass to it the data it will be\nable to access. To pass along the current context please remember to\ninclude a trailing dot. The templates location will always be starting at\nthe /layout/ directory within Hugo.\n\n**Example:**\n\n    {{ template \"chrome/header.html\" . }}\n\n\n## Logic\n\nGo templates provide the most basic iteration and conditional logic.\n\n### Iteration\n\nJust like in Go, the Go templates make heavy use of range to iterate over\na map, array or slice. The following are different examples of how to use\nrange.\n\n**Example 1: Using Context**\n\n    {{ range array }}\n        {{ . }}\n    {{ end }}\n\n**Example 2: Declaring value variable name**\n\n    {{range $element := array}}\n        {{ $element }}\n    {{ end }}\n\n**Example 2: Declaring key and value variable name**\n\n    {{range $index, $element := array}}\n        {{ $index }}\n        {{ $element }}\n    {{ end }}\n\n### Conditionals\n\nIf, else, with, or, & and provide the framework for handling conditional\nlogic in Go Templates. Like range, each statement is closed with `end`.\n\n\nGo Templates treat the following values as false:\n\n* false\n* 0\n* any array, slice, map, or string of length zero\n\n**Example 1: If**\n\n    {{ if isset .Params \"title\" }}<h4>{{ index .Params \"title\" }}</h4>{{ end }}\n\n**Example 2: If -> Else**\n\n    {{ if isset .Params \"alt\" }}\n        {{ index .Params \"alt\" }}\n    {{else}}\n        {{ index .Params \"caption\" }}\n    {{ end }}\n\n**Example 3: And & Or**\n\n    {{ if and (or (isset .Params \"title\") (isset .Params \"caption\")) (isset .Params \"attr\")}}\n\n**Example 4: With**\n\nAn alternative way of writing \"if\" and then referencing the same value\nis to use \"with\" instead. With rebinds the context `.` within its scope,\nand skips the block if the variable is absent.\n\nThe first example above could be simplified as:\n\n    {{ with .Params.title }}<h4>{{ . }}</h4>{{ end }}\n\n**Example 5: If -> Else If**\n\n    {{ if isset .Params \"alt\" }}\n        {{ index .Params \"alt\" }}\n    {{ else if isset .Params \"caption\" }}\n        {{ index .Params \"caption\" }}\n    {{ end }}\n\n## Pipes\n\nOne of the most powerful components of Go templates is the ability to\nstack actions one after another. This is done by using pipes. Borrowed\nfrom unix pipes, the concept is simple, each pipeline's output becomes the\ninput of the following pipe.\n\nBecause of the very simple syntax of Go templates, the pipe is essential\nto being able to chain together function calls. One limitation of the\npipes is that they only can work with a single value and that value\nbecomes the last parameter of the next pipeline.\n\nA few simple examples should help convey how to use the pipe.\n\n**Example 1 :**\n\n    {{ if eq 1 1 }} Same {{ end }}\n\nis the same as\n\n    {{ eq 1 1 | if }} Same {{ end }}\n\nIt does look odd to place the if at the end, but it does provide a good\nillustration of how to use the pipes.\n\n**Example 2 :**\n\n    {{ index .Params \"disqus_url\" | html }}\n\nAccess the page parameter called \"disqus_url\" and escape the HTML.\n\n**Example 3 :**\n\n    {{ if or (or (isset .Params \"title\") (isset .Params \"caption\")) (isset .Params \"attr\")}}\n    Stuff Here\n    {{ end }}\n\nCould be rewritten as\n\n    {{  isset .Params \"caption\" | or isset .Params \"title\" | or isset .Params \"attr\" | if }}\n    Stuff Here\n    {{ end }}\n\n\n## Context (aka. the dot)\n\nThe most easily overlooked concept to understand about Go templates is that {{ . }}\nalways refers to the current context. In the top level of your template this\nwill be the data set made available to it. Inside of a iteration it will have\nthe value of the current item. When inside of a loop the context has changed. .\nwill no longer refer to the data available to the entire page. If you need to\naccess this from within the loop you will likely want to set it to a variable\ninstead of depending on the context.\n\n**Example:**\n\n      {{ $title := .Site.Title }}\n      {{ range .Params.tags }}\n        <li> <a href=\"{{ $baseurl }}/tags/{{ . | urlize }}\">{{ . }}</a> - {{ $title }} </li>\n      {{ end }}\n\nNotice how once we have entered the loop the value of {{ . }} has changed. We\nhave defined a variable outside of the loop so we have access to it from within\nthe loop.\n\n# Hugo Parameters\n\nHugo provides the option of passing values to the template language\nthrough the site configuration (for sitewide values), or through the meta\ndata of each specific piece of content. You can define any values of any\ntype (supported by your front matter/config format) and use them however\nyou want to inside of your templates.\n\n\n## Using Content (page) Parameters\n\nIn each piece of content you can provide variables to be used by the\ntemplates. This happens in the [front matter](/content/front-matter).\n\nAn example of this is used in this documentation site. Most of the pages\nbenefit from having the table of contents provided. Sometimes the TOC just\ndoesn't make a lot of sense. We've defined a variable in our front matter\nof some pages to turn off the TOC from being displayed.\n\nHere is the example front matter:\n\n```\n---\ntitle: \"Permalinks\"\ndate: \"2013-11-18\"\naliases:\n  - \"/doc/permalinks/\"\ngroups: [\"extras\"]\ngroups_weight: 30\nnotoc: true\n---\n```\n\nHere is the corresponding code inside of the template:\n\n      {{ if not .Params.notoc }}\n        <div id=\"toc\" class=\"well col-md-4 col-sm-6\">\n        {{ .TableOfContents }}\n        </div>\n      {{ end }}\n\n\n\n## Using Site (config) Parameters\nIn your top-level configuration file (eg, `config.yaml`) you can define site\nparameters, which are values which will be available to you in chrome.\n\nFor instance, you might declare:\n\n```yaml\nparams:\n  CopyrightHTML: \"Copyright &#xA9; 2013 John Doe. All Rights Reserved.\"\n  TwitterUser: \"spf13\"\n  SidebarRecentLimit: 5\n```\n\nWithin a footer layout, you might then declare a `<footer>` which is only\nprovided if the `CopyrightHTML` parameter is provided, and if it is given,\nyou would declare it to be HTML-safe, so that the HTML entity is not escaped\nagain.  This would let you easily update just your top-level config file each\nJanuary 1st, instead of hunting through your templates.\n\n```\n{{if .Site.Params.CopyrightHTML}}<footer>\n<div class=\"text-center\">{{.Site.Params.CopyrightHTML | safeHtml}}</div>\n</footer>{{end}}\n```\n\nAn alternative way of writing the \"if\" and then referencing the same value\nis to use \"with\" instead. With rebinds the context `.` within its scope,\nand skips the block if the variable is absent:\n\n```\n{{with .Site.Params.TwitterUser}}<span class=\"twitter\">\n<a href=\"https://twitter.com/{{.}}\" rel=\"author\">\n<img src=\"/images/twitter.png\" width=\"48\" height=\"48\" title=\"Twitter: {{.}}\"\n alt=\"Twitter\"></a>\n</span>{{end}}\n```\n\nFinally, if you want to pull \"magic constants\" out of your layouts, you can do\nso, such as in this example:\n\n```\n<nav class=\"recent\">\n  <h1>Recent Posts</h1>\n  <ul>{{range first .Site.Params.SidebarRecentLimit .Site.Recent}}\n    <li><a href=\"{{.RelPermalink}}\">{{.Title}}</a></li>\n  {{end}}</ul>\n</nav>\n```\n\n\n[go]: https://golang.org/\n[gohtmltemplate]: https://golang.org/pkg/html/template/\n"
  },
  {
    "path": "themes/hermit/exampleSite/content/posts/hugoisforlovers.md",
    "content": "+++\ntitle = \"Getting Started with Hugo\"\ntags = [\n    \"go\",\n    \"golang\",\n    \"hugo\",\n    \"development\",\n]\ndate = \"2014-04-02\"\ntoc = true\n+++\n\n## Step 1. Install Hugo\n\nGo to [Hugo releases](https://github.com/spf13/hugo/releases) and download the\nappropriate version for your OS and architecture.\n\nSave it somewhere specific as we will be using it in the next step.\n\nMore complete instructions are available at [Install Hugo](https://gohugo.io/getting-started/installing/)\n\n## Step 2. Build the Docs\n\nHugo has its own example site which happens to also be the documentation site\nyou are reading right now.\n\nFollow the following steps:\n\n 1. Clone the [Hugo repository](http://github.com/spf13/hugo)\n 2. Go into the repo\n 3. Run hugo in server mode and build the docs\n 4. Open your browser to http://localhost:1313\n\nCorresponding pseudo commands:\n\n    git clone https://github.com/spf13/hugo\n    cd hugo\n    /path/to/where/you/installed/hugo server --source=./docs\n    > 29 pages created\n    > 0 tags index created\n    > in 27 ms\n    > Web Server is available at http://localhost:1313\n    > Press ctrl+c to stop\n\nOnce you've gotten here, follow along the rest of this page on your local build.\n\n## Step 3. Change the docs site\n\nStop the Hugo process by hitting Ctrl+C.\n\nNow we are going to run hugo again, but this time with hugo in watch mode.\n\n    /path/to/hugo/from/step/1/hugo server --source=./docs --watch\n    > 29 pages created\n    > 0 tags index created\n    > in 27 ms\n    > Web Server is available at http://localhost:1313\n    > Watching for changes in /Users/spf13/Code/hugo/docs/content\n    > Press ctrl+c to stop\n\n\nOpen your [favorite editor](http://vim.spf13.com) and change one of the source\ncontent pages. How about changing this very file to *fix the typo*. How about changing this very file to *fix the typo*.\n\nContent files are found in `docs/content/`. Unless otherwise specified, files\nare located at the same relative location as the url, in our case\n`docs/content/overview/quickstart.md`.\n\nChange and save this file.. Notice what happened in your terminal.\n\n    > Change detected, rebuilding site\n\n    > 29 pages created\n    > 0 tags index created\n    > in 26 ms\n\nRefresh the browser and observe that the typo is now fixed.\n\nNotice how quick that was. Try to refresh the site before it's finished building. I double dare you.\nHaving nearly instant feedback enables you to have your creativity flow without waiting for long builds.\n\n## Step 4. Have fun\n\nThe best way to learn something is to play with it.\n"
  },
  {
    "path": "themes/hermit/exampleSite/content/posts/migrate-from-jekyll.md",
    "content": "---\ndate: 2014-03-10\ntitle: Migrate to Hugo from Jekyll\ntoc: true\n---\n\n## Move static content to `static`\nJekyll has a rule that any directory not starting with `_` will be copied as-is to the `_site` output. Hugo keeps all static content under `static`. You should therefore move it all there.\nWith Jekyll, something that looked like\n\n    ▾ <root>/\n        ▾ images/\n            logo.png\n\nshould become\n\n    ▾ <root>/\n        ▾ static/\n            ▾ images/\n                logo.png\n\nAdditionally, you'll want any files that should reside at the root (such as `CNAME`) to be moved to `static`.\n\n## Create your Hugo configuration file\nHugo can read your configuration as JSON, YAML or TOML. Hugo supports parameters custom configuration too. Refer to the [Hugo configuration documentation](/overview/configuration/) for details.\n\n## Set your configuration publish folder to `_site`\nThe default is for Jekyll to publish to `_site` and for Hugo to publish to `public`. If, like me, you have [`_site` mapped to a git submodule on the `gh-pages` branch](http://blog.blindgaenger.net/generate_github_pages_in_a_submodule.html), you'll want to do one of two alternatives:\n\n1. Change your submodule to point to map `gh-pages` to public instead of `_site` (recommended).\n\n        git submodule deinit _site\n        git rm _site\n        git submodule add -b gh-pages git@github.com:your-username/your-repo.git public\n\n2. Or, change the Hugo configuration to use `_site` instead of `public`.\n\n        {\n            ..\n            \"publishdir\": \"_site\",\n            ..\n        }\n\n## Convert Jekyll templates to Hugo templates\nThat's the bulk of the work right here. The documentation is your friend. You should refer to [Jekyll's template documentation](http://jekyllrb.com/docs/templates/) if you need to refresh your memory on how you built your blog and [Hugo's template](/layout/templates/) to learn Hugo's way.\n\nAs a single reference data point, converting my templates for [heyitsalex.net](http://heyitsalex.net/) took me no more than a few hours.\n\n## Convert Jekyll plugins to Hugo shortcodes\nJekyll has [plugins](http://jekyllrb.com/docs/plugins/); Hugo has [shortcodes](/doc/shortcodes/). It's fairly trivial to do a port.\n\n### Implementation\nAs an example, I was using a custom [`image_tag`](https://github.com/alexandre-normand/alexandre-normand/blob/74bb12036a71334fdb7dba84e073382fc06908ec/_plugins/image_tag.rb) plugin to generate figures with caption when running Jekyll. As I read about shortcodes, I found Hugo had a nice built-in shortcode that does exactly the same thing.\n\nJekyll's plugin:\n\n    module Jekyll\n      class ImageTag < Liquid::Tag\n        @url = nil\n        @caption = nil\n        @class = nil\n        @link = nil\n        // Patterns\n        IMAGE_URL_WITH_CLASS_AND_CAPTION =\n        IMAGE_URL_WITH_CLASS_AND_CAPTION_AND_LINK = /(\\w+)(\\s+)((https?:\\/\\/|\\/)(\\S+))(\\s+)\"(.*?)\"(\\s+)->((https?:\\/\\/|\\/)(\\S+))(\\s*)/i\n        IMAGE_URL_WITH_CAPTION = /((https?:\\/\\/|\\/)(\\S+))(\\s+)\"(.*?)\"/i\n        IMAGE_URL_WITH_CLASS = /(\\w+)(\\s+)((https?:\\/\\/|\\/)(\\S+))/i\n        IMAGE_URL = /((https?:\\/\\/|\\/)(\\S+))/i\n        def initialize(tag_name, markup, tokens)\n          super\n          if markup =~ IMAGE_URL_WITH_CLASS_AND_CAPTION_AND_LINK\n            @class   = $1\n            @url     = $3\n            @caption = $7\n            @link = $9\n          elsif markup =~ IMAGE_URL_WITH_CLASS_AND_CAPTION\n            @class   = $1\n            @url     = $3\n            @caption = $7\n          elsif markup =~ IMAGE_URL_WITH_CAPTION\n            @url     = $1\n            @caption = $5\n          elsif markup =~ IMAGE_URL_WITH_CLASS\n            @class = $1\n            @url   = $3\n          elsif markup =~ IMAGE_URL\n            @url = $1\n          end\n        end\n        def render(context)\n          if @class\n            source = \"<figure class='#{@class}'>\"\n          else\n            source = \"<figure>\"\n          end\n          if @link\n            source += \"<a href=\\\"#{@link}\\\">\"\n          end\n          source += \"<img src=\\\"#{@url}\\\">\"\n          if @link\n            source += \"</a>\"\n          end\n          source += \"<figcaption>#{@caption}</figcaption>\" if @caption\n          source += \"</figure>\"\n          source\n        end\n      end\n    end\n    Liquid::Template.register_tag('image', Jekyll::ImageTag)\n\nis written as this Hugo shortcode:\n\n    <!-- image -->\n    <figure {{ with .Get \"class\" }}class=\"{{.}}\"{{ end }}>\n        {{ with .Get \"link\"}}<a href=\"{{.}}\">{{ end }}\n            <img src=\"{{ .Get \"src\" }}\" {{ if or (.Get \"alt\") (.Get \"caption\") }}alt=\"{{ with .Get \"alt\"}}{{.}}{{else}}{{ .Get \"caption\" }}{{ end }}\"{{ end }} />\n        {{ if .Get \"link\"}}</a>{{ end }}\n        {{ if or (or (.Get \"title\") (.Get \"caption\")) (.Get \"attr\")}}\n        <figcaption>{{ if isset .Params \"title\" }}\n            {{ .Get \"title\" }}{{ end }}\n            {{ if or (.Get \"caption\") (.Get \"attr\")}}<p>\n            {{ .Get \"caption\" }}\n            {{ with .Get \"attrlink\"}}<a href=\"{{.}}\"> {{ end }}\n                {{ .Get \"attr\" }}\n            {{ if .Get \"attrlink\"}}</a> {{ end }}\n            </p> {{ end }}\n        </figcaption>\n        {{ end }}\n    </figure>\n    <!-- image -->\n\n### Usage\nI simply changed:\n\n    {% image full http://farm5.staticflickr.com/4136/4829260124_57712e570a_o_d.jpg \"One of my favorite touristy-type photos. I secretly waited for the good light while we were \"having fun\" and took this. Only regret: a stupid pole in the top-left corner of the frame I had to clumsily get rid of at post-processing.\" ->http://www.flickr.com/photos/alexnormand/4829260124/in/set-72157624547713078/ %}\n\nto this (this example uses a slightly extended version named `fig`, different than the built-in `figure`):\n\n    {{%/* fig class=\"full\" src=\"http://farm5.staticflickr.com/4136/4829260124_57712e570a_o_d.jpg\" title=\"One of my favorite touristy-type photos. I secretly waited for the good light while we were having fun and took this. Only regret: a stupid pole in the top-left corner of the frame I had to clumsily get rid of at post-processing.\" link=\"http://www.flickr.com/photos/alexnormand/4829260124/in/set-72157624547713078/\" */%}}\n\nAs a bonus, the shortcode named parameters are, arguably, more readable.\n\n## Finishing touches\n### Fix content\nDepending on the amount of customization that was done with each post with Jekyll, this step will require more or less effort. There are no hard and fast rules here except that `hugo server --watch` is your friend. Test your changes and fix errors as needed.\n\n### Clean up\nYou'll want to remove the Jekyll configuration at this point. If you have anything else that isn't used, delete it.\n\n## A practical example in a diff\n[Hey, it's Alex](http://heyitsalex.net/) was migrated in less than a _father-with-kids day_ from Jekyll to Hugo. You can see all the changes (and screw-ups) by looking at this [diff](https://github.com/alexandre-normand/alexandre-normand/compare/869d69435bd2665c3fbf5b5c78d4c22759d7613a...b7f6605b1265e83b4b81495423294208cc74d610).\n"
  },
  {
    "path": "themes/hermit/exampleSite/content/posts/post-with-featured-image.md",
    "content": "---\ntitle: \"Post With Featured Image\"\ndate: 2018-10-01T16:15:09+08:00\ndraft: false\nimages: \n  - https://picsum.photos/1024/768/?random\ntags: \n  - Demo\n  - Image\n---\n\nJust define the image URL in the content’s front matter, the featured image will be displayed as the background. \n\nFor example:\n\n```yaml\n---\nimages:\n  - https://picsum.photos/1024/768/?random\n---\n```\n\nThis is an array, you can set multiple urls, only the first url will be used. These images is also used in [Twitter Cards](https://developer.twitter.com/en/docs/tweets/optimize-with-cards/guides/getting-started.html) and the [Open Graph](http://ogp.me/) metadata.\n"
  },
  {
    "path": "themes/hermit/exampleSite/content/posts/the-figure-shortcode.md",
    "content": "---\ntitle: 'The \"figure\" Shortcode'\ndate: 2018-12-24T12:29:41+08:00\ndraft: false\nfeaturedImg: \"\"\ntags: \n  - demo\n  - image\n---\n\nHugo has `figure` shortcode built in, so you can easily add figcaptions or hyperlink rel attributes to images. Documentations can be found here:\n\nhttps://gohugo.io/content-management/shortcodes/#figure\n\nThis theme has 3 CSS classes made for figure elements:\n\n* `big`: images will break the width limit of main content area.\n* `left`: images will float to the left.\n* `right`: images will float to the right.\n\nIf a figure has no class set, the image will behave just like a normal markdown image: `![]()`.\n\nHere's some examples, please be aware that these styles only take effect when the page width is over 1300px.\n\n{{< figure src=\"https://via.placeholder.com/1600x800\" alt=\"image\" caption=\"figure-normal (without any classes)\" >}}\n\nPellentesque posuere sem nec nunc varius, id hendrerit arcu consequat. Maecenas commodo, sapien ut gravida porttitor, dolor risus facilisis enim, eget pharetra nibh nisl porttitor sapien. Proin finibus elementum ligula sit amet hendrerit. Praesent et erat sodales ante accumsan pharetra non eu nulla. Sed vehicula consequat lorem, a fermentum ante faucibus quis. Aliquam erat volutpat. In vitae tincidunt dui. Proin sit amet ligula sodales, elementum tortor et, venenatis sem. Maecenas non nisl erat. Curabitur nec velit eros. Ut cursus lacus nisi, non pretium libero euismod et. Fusce luctus in nisi quis sollicitudin. Aenean nec blandit ligula. Duis ac felis lorem. Proin tellus tellus, dictum nec tempus sit amet, venenatis ac felis. Sed in pharetra nulla, non mollis sem.\n\n{{< figure src=\"https://via.placeholder.com/1600x800\" alt=\"image\" caption=\"figure-big\" class=\"big\" >}}\n\nSuspendisse fringilla malesuada massa, in malesuada orci lacinia a. Praesent dapibus faucibus nisl, id volutpat elit bibendum eu. Nulla vitae laoreet nibh, eu hendrerit lacus. Donec lacinia auctor ligula, vel interdum ipsum malesuada vitae. Donec placerat a justo eu gravida. Aenean ultricies imperdiet convallis. Pellentesque accumsan non ex sed euismod. Proin bibendum lectus nec enim faucibus feugiat. Donec hendrerit nisi viverra ornare luctus. Nullam non viverra nisl. Nam vel tellus et tortor elementum volutpat sit amet et erat. Aliquam a libero quis libero porta consectetur. Etiam aliquam felis vel nulla mattis finibus. Mauris laoreet lacus arcu, sed rhoncus odio condimentum sed. Aenean in dui rutrum elit faucibus faucibus nec fringilla augue. Fusce non ornare mauris.\n\n{{< figure src=\"https://via.placeholder.com/400x280\" alt=\"image\" caption=\"figure-left\" class=\"left\" >}}\n\nIn a libero varius, luctus ligula et, bibendum tortor. Sed sit amet dui malesuada, mattis justo id, ultricies enim. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam sollicitudin cursus feugiat. Vivamus suscipit ipsum eget lobortis sollicitudin. Fusce vehicula neque tellus. Integer eu posuere quam, id laoreet tortor. Mauris sit amet turpis urna. Donec venenatis tempor dolor, nec laoreet orci aliquet et. Sed condimentum elit eu tristique aliquam. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nunc luctus ipsum sit amet nisl maximus pellentesque.\n\n{{< figure src=\"https://via.placeholder.com/400x280\" alt=\"image\" caption=\"figure-right\" class=\"right\" >}}\n\nPellentesque eu consequat nunc. Vivamus eu eros ut nulla dapibus molestie in id tortor. Cras viverra ligula erat, tincidunt hendrerit diam blandit nec. Cras id urna vel dolor dictum mattis. Vestibulum congue erat ac eros molestie accumsan. Maecenas lorem nibh, maximus vel justo eget, facilisis egestas lectus. Mauris eu est ut odio blandit consequat id feugiat eros. Fusce id suscipit mi, et lacinia lectus. Mauris a arcu placerat dolor iaculis feugiat nec non mi. Ut porttitor elit tortor, eget tempus velit mollis eu. Aliquam sem nulla, dictum cursus mauris ac, semper ullamcorper leo.\n\nDonec nec tincidunt est. Sed id metus in erat fringilla mattis at id turpis. Aliquam tempor vehicula faucibus. Phasellus consequat aliquam odio. Morbi a ex vitae sapien porta auctor. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec sit amet nulla arcu. Praesent ut tortor purus. Praesent id eros diam. Pellentesque vitae dolor at nibh ultrices accumsan eu id urna. Aliquam finibus interdum orci in varius. Pellentesque a enim condimentum, condimentum felis id, vehicula augue. Vivamus cursus commodo eros nec lacinia.\n"
  },
  {
    "path": "themes/hermit/exampleSite/content/posts/typography.md",
    "content": "---\ntitle: \"Typography\"\ndate: 2018-09-29T11:36:33+08:00\ndraft: false\nfeaturedImg: \"\"\ntags: \n  - Demo\n  - Typography\n---\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\n> An apple is a sweet, edible fruit produced by an apple tree (Malus pumila). Apple trees are cultivated worldwide, and are the most widely grown species in the genus Malus. The tree originated in Central Asia, where its wild ancestor, Malus sieversii, is still found today. Apples have been grown for thousands of years in Asia and Europe, and were brought to North America by European colonists. Apples have religious and mythological significance in many cultures, including Norse, Greek and European Christian traditions.[^1]\n\n---\n\nInline styles：\n\n**strong**, *emphasis*, ***strong and emphasis***,`code`, <u>underline</u>, ~~strikethrough~~, :joy:🤣, $\\LaTeX$, X^2^, H~2~O, ==highlight==, [Link](https://example.com), and image:\n\n![img](https://picsum.photos/600/400/?random)\n\n---\n\nHeadings:\n\n# Heading 1\n\n## Heading 2\n\n### Heading 3\n\n#### Heading 4\n\n##### Heading 5\n\n###### Heading 6\n\nTable:\n\n| Left-Aligned  | Center Aligned  | Right Aligned |\n| :------------ | :-------------: | ------------: |\n| col 3 is      | some wordy text |         $1600 |\n| col 2 is      |    centered     |           $12 |\n| zebra stripes |    are neat     |            $1 |\n\nLists:\n\n* Unordered list item 1.\n* Unordered list item 2.\n\n1. ordered list item 1.\n2. ordered list item 2.\n   + sub-unordered list item 1.\n   + sub-unordered list item 2.\n     + [x] something is DONE.\n     + [ ] something is NOT DONE.\n\nSyntax Highlighting:\n\n```javascript\nvar num1, num2, sum\nnum1 = prompt(\"Enter first number\")\nnum2 = prompt(\"Enter second number\")\nsum = parseInt(num1) + parseInt(num2) // \"+\" means \"add\"\nalert(\"Sum = \" + sum)  // \"+\" means combine into a string\n```\n\n[^1]: From https://en.wikipedia.org/wiki/Apple"
  },
  {
    "path": "themes/hermit/i18n/en.toml",
    "content": "# Translations for English\n# https://gohugo.io/content-management/multilingual/#translation-of-strings\n\n# 404.html\n#\n[notFound]\nother = \"Oops, page not found…\"\n\n[home]\nother = \"Home\"\n\n[archives]\nother = \"Archives\"\n\n# posts/single.html\n#\n[wordCount]\nother = \"{{ .WordCount }} Words\"\n\n[tableOfContents]\nother = \"Table of Contents\"\n\n[newer]\nother = \"Newer\"\n\n[older]\nother = \"Older\"\n\n# partials/header.html\n#\n[menu]\nother = \"Menu\"\n\n[featuredImage]\nother = \"Featured Image\"\n"
  },
  {
    "path": "themes/hermit/i18n/it.toml",
    "content": "# Translations for Italian\n# https://gohugo.io/content-management/multilingual/#translation-of-strings\n\n# 404.html\n#\n[notFound]\nother = \"Oops, pagina non trovata…\"\n\n[home]\nother = \"Home\"\n\n[archives]\nother = \"Archivi\"\n\n# posts/single.html\n#\n[wordCount]\nother = \"{{ .WordCount }} Parole\"\n\n[tableOfContents]\nother = \"Sommario\"\n\n[newer]\nother = \"Prossimo\"\n\n[older]\nother = \"Precedente\"\n\n# partials/header.html\n#\n[menu]\nother = \"Menu\"\n\n[featuredImage]\nother = \"Immagine in primo piano\"\n"
  },
  {
    "path": "themes/hermit/i18n/zh-hans.toml",
    "content": "# Translations for Simplified Chinese - 简体中文\n# https://gohugo.io/content-management/multilingual/#translation-of-strings\n\n# 404.html\n#\n[notFound]\nother = \"糟糕，您要访问的页面不存在……\"\n\n[home]\nother = \"主页\"\n\n[archives]\nother = \"归档\"\n\n# posts/single.html\n#\n[wordCount]\nother = \"{{ .WordCount }} 字\"\n\n[tableOfContents]\nother = \"目录\"\n\n[newer]\nother = \"新\"\n\n[older]\nother = \"旧\"\n\n# partials/header.html\n#\n[menu]\nother = \"菜单\"\n\n[featuredImage]\nother = \"特色图片\"\n"
  },
  {
    "path": "themes/hermit/layouts/404.html",
    "content": "{{ define \"main\" }}\n\t<div id=\"spotlight\" class=\"error-404 animated fadeIn\">\n\t\t<p class=\"img-404\">\n\t\t\t<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 167.8 163.4\" fill=\"currentColor\"><title>404-lighthouse</title><path d=\"M83,27.5c.5-8.4,12.5-8.4,13,0,.2,3.2,5.2,3.2,5,0C100.7,21.3,96,16,89.5,16S78.3,21.3,78,27.5c-.2,3.2,4.8,3.2,5,0Z\" transform=\"translate(-6.6 -6.6)\"/><path d=\"M92,18V9c0-3.2-5-3.2-5,0v9c0,3.2,5,3.2,5,0Z\" transform=\"translate(-6.6 -6.6)\"/><path d=\"M78,44.5l-7.9,86.7L69,143.5c-.3,3.2,4.7,3.2,5,0l7.9-86.7L83,44.5c.3-3.2-4.7-3.2-5,0Z\" transform=\"translate(-6.6 -6.6)\"/><path d=\"M96,44.5l7.9,86.7,1.1,12.3c.3,3.2,5.3,3.2,5,0l-7.9-86.7L101,44.5c-.3-3.2-5.3-3.2-5,0Z\" transform=\"translate(-6.6 -6.6)\"/><path d=\"M88.5,26.5v18a1,1,0,0,0,2,0v-18a1,1,0,0,0-2,0Z\" transform=\"translate(-6.6 -6.6)\"/><path d=\"M79.1,69.6l21.2-12.2a1.5,1.5,0,0,0-1.5-2.6L77.6,67a1.5,1.5,0,0,0,1.5,2.6Z\" transform=\"translate(-6.6 -6.6)\"/><path d=\"M76.4,99.2,102.7,84a1.5,1.5,0,0,0-1.5-2.6L74.9,96.6a1.5,1.5,0,0,0,1.5,2.6Z\" transform=\"translate(-6.6 -6.6)\"/><path d=\"M73.7,128.7l31.4-18.1a1.5,1.5,0,0,0-1.5-2.6L72.2,126.1a1.5,1.5,0,0,0,1.5,2.6Z\" transform=\"translate(-6.6 -6.6)\"/><path d=\"M98.5,42h-18L83,44.5v-18L80.5,29h18L96,26.5v18c0,3.2,5,3.2,5,0v-18A2.5,2.5,0,0,0,98.5,24h-18A2.5,2.5,0,0,0,78,26.5v18A2.5,2.5,0,0,0,80.5,47h18C101.7,47,101.7,42,98.5,42Z\" transform=\"translate(-6.6 -6.6)\"/><path d=\"M172,165c-5.8-.3-9.5-4.7-15.8-3.8-2.6.4-4.4,1.6-6.7,2.7s-6.9,1.3-10.2-.5-9.5-3.2-14.3-1c-3.3,1.5-5.6,3.3-9.5,2.4-2.4-.5-4.3-2.3-6.7-3.1a15.5,15.5,0,0,0-8.3-.3c-2.5.6-4.3,2.2-6.7,3.1-6.2,2.2-10.8-3.5-16.9-3.5s-10.7,5.6-17,3.5c-2.3-.8-4.2-2.5-6.7-3.1a15.4,15.4,0,0,0-8.3.3c-3.1,1-5.4,3.3-8.9,3.3s-5.8-2.2-8.9-3.3a15.4,15.4,0,0,0-8.8-.2c-3.4,1-5.7,3.3-9.5,3.5s-3.2,5.2,0,5c6-.3,10.9-5.5,17-3.5,2.4.8,4.2,2.5,6.7,3.1a15.4,15.4,0,0,0,8.3-.3c2.3-.8,4.2-2.5,6.7-3.1s6.3.9,9.5,2.4c4.8,2.3,9.8,1.5,14.3-1s6.7-2.2,10.2-.5,4.1,2.3,6.7,2.7a14.9,14.9,0,0,0,7.9-1c2.7-1.2,4.8-2.9,7.9-2.9s5.2,1.7,7.9,2.9a14.9,14.9,0,0,0,7.9,1c2.6-.4,4.4-1.6,6.7-2.7s6.9-1.3,10.2.5a15.9,15.9,0,0,0,16.1,0c7.3-3.9,11.9,2,19.1,2.3,3.2.2,3.2-4.8,0-5Z\" transform=\"translate(-6.6 -6.6)\"/><path d=\"M46.3,165.8l9.6-9.3c4.9-4.6,9.7-11.1,17.2-9.2,4.9,1.2,9.2,5.5,13,8.5s8,6.5,12.1,9.7c2.6,2,5-2.4,2.5-4.3-5-3.8-9.7-7.9-14.7-11.7s-8.7-7-14.6-7.6-11.2,3.6-15.9,8S47,158,42.7,162.2c-2.3,2.3,1.2,5.8,3.5,3.5Z\" transform=\"translate(-6.6 -6.6)\"/><path d=\"M84.8,152.8c8.3-3.7,16.7-8.3,26.1-5.6s15.5,9,19.4,16.6c1.5,2.9,5.8.3,4.3-2.5-4.5-8.7-12.9-16.2-22.4-18.9s-20.3,1.7-29.9,6.1c-2.9,1.3-.4,5.6,2.5,4.3Z\" transform=\"translate(-6.6 -6.6)\"/><g class=\"animated flash infinite slower\" ><path d=\"M62.5,34h-23a1.5,1.5,0,0,0,0,3h23a1.5,1.5,0,0,0,0-3Z\" transform=\"translate(-6.6 -6.6)\"/><path d=\"M63.3,25.2l-18-9c-1.7-.9-3.2,1.7-1.5,2.6l18,9c1.7.9,3.2-1.7,1.5-2.6Z\" transform=\"translate(-6.6 -6.6)\"/><path d=\"M61.7,43.2l-18,9c-1.7.9-.2,3.5,1.5,2.6l18-9c1.7-.9.2-3.5-1.5-2.6Z\" transform=\"translate(-6.6 -6.6)\"/><path d=\"M116.5,37h23a1.5,1.5,0,0,0,0-3h-23a1.5,1.5,0,0,0,0,3Z\" transform=\"translate(-6.6 -6.6)\"/><path d=\"M117.3,27.8l18-9c1.7-.9.2-3.5-1.5-2.6l-18,9c-1.7.9-.2,3.5,1.5,2.6Z\" transform=\"translate(-6.6 -6.6)\"/><path d=\"M115.7,45.8l18,9c1.7.9,3.2-1.7,1.5-2.6l-18-9c-1.7-.9-3.2,1.7-1.5,2.6Z\" transform=\"translate(-6.6 -6.6)\"/></g></svg>\n\t\t</p>\n\t\t<div class=\"banner-404\">\n\t\t\t<h1>404</h1>\n\t\t\t<p>{{ i18n \"notFound\" }}</p>\n\t\t\t<p class=\"btn-404\">\n\t\t\t\t<a href=\"{{.Site.BaseURL}}\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-home\"><path d=\"M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\"></path><polyline points=\"9 22 9 12 15 12 15 22\"></polyline></svg>{{ i18n \"home\" }}</a>\n\t\t\t\t<a href=\"{{ \"post\" | absURL }}\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-archive\"><polyline points=\"21 8 21 21 3 21 3 8\"></polyline><rect x=\"1\" y=\"3\" width=\"22\" height=\"5\"></rect><line x1=\"10\" y1=\"12\" x2=\"14\" y2=\"12\"></line></svg>{{ i18n \"archives\" }}</a>\n\t\t\t</p>\n\t\t</div>\n\t</div>\n{{ end }}\n"
  },
  {
    "path": "themes/hermit/layouts/_default/baseof.html",
    "content": "<!DOCTYPE html>\n<html lang=\"{{.Site.LanguageCode}}\">\n\n<head>\n\t<meta charset=\"UTF-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\n\t{{- with .Site.Params.themeColor }}\n\t<meta name=\"theme-color\" content=\"{{.}}\">\n\t<meta name=\"msapplication-TileColor\" content=\"{{.}}\">\n\t{{- end }}\n\t{{- partial \"structured-data.html\" . }}\n\t{{- partial \"favicons.html\" }}\n\t<title>{{.Title}}</title>\n\t{{ range .AlternativeOutputFormats -}}\n\t\t{{ printf `<link rel=\"%s\" type=\"%s+%s\" href=\"%s\" title=\"%s\" />` .Rel .MediaType.Type .MediaType.Suffix .Permalink $.Site.Title | safeHTML }}\n\t{{ end -}}\n\t{{ $style := resources.Get \"scss/style.scss\" | resources.ExecuteAsTemplate \"css/style.css\" . | toCSS | minify | fingerprint -}}\n\t<link rel=\"stylesheet\" href=\"{{ $style.Permalink }}\">\n\t{{- block \"head\" . -}}{{- end }}\n\t{{- range .Site.Params.customCSS }}\n\t<link rel=\"stylesheet\" href=\"{{ . | absURL }}\">\n\t{{- end }}\n\t{{- if templates.Exists \"partials/extra-head.html\" -}}\n\t{{ partial \"extra-head.html\" . }}\n\t{{- end }}\n</head>\n\n<body id=\"page\">\n\t{{ block \"header\" . -}}{{ end -}}\n\t{{ block \"main\" . -}}{{ end -}}\n\t{{ block \"footer\" . -}}{{ end }}\n\t{{ $script := resources.Get \"js/main.js\" | minify | fingerprint -}}\n\t<script src=\"{{ $script.Permalink }}\"></script>\n\t{{- partial \"analytics.html\" . }}\n\t{{- if templates.Exists \"partials/extra-foot.html\" -}}\n\t{{ partial \"extra-foot.html\" . }}\n\t{{- end }}\n</body>\n\n</html>\n"
  },
  {
    "path": "themes/hermit/layouts/_default/list.html",
    "content": "{{ define \"header\" }}\n{{ partialCached \"header.html\" . }}\n{{ end }}\n\n{{ define \"main\" }}\n\t<main class=\"site-main section-inner thin animated fadeIn faster\">\n\t\t<h1>{{ .Title }}</h1>\n\t\t{{- if .Content }}\n\t\t<div class=\"content\">\n\t\t\t{{ .Content }}\n\t\t</div>\n\t\t{{- end }}\n\t\t{{- range .Data.Pages.GroupByDate \"2006\" }}\n\t\t<div class=\"posts-group\">\n\t\t\t<div class=\"post-year\" id=\"{{ .Key }}\">{{ .Key }}</div>\n\t\t\t<ul class=\"posts-list\">\n\t\t\t\t{{- range .Pages }}\n\t\t\t\t<li class=\"post-item\">\n\t\t\t\t\t<a href=\"{{.Permalink}}\">\n\t\t\t\t\t\t<span class=\"post-title\">{{.Title}}</span>\n\t\t\t\t\t\t<span class=\"post-day\">{{ .Date.Format .Site.Params.dateformShort }}</span>\n\t\t\t\t\t</a>\n\t\t\t\t</li>\n\t\t\t\t{{- end }}\n\t\t\t</ul>\n\t\t</div>\n\t\t{{- end }}\n\t</main>\n{{ end }}\n\n{{ define \"footer\" }}\n{{ partialCached \"footer.html\" . }}\n{{ end }}\n"
  },
  {
    "path": "themes/hermit/layouts/_default/single.html",
    "content": "{{ define \"head\" }}\n\t{{ if .Params.featuredImg -}}\n\t<style>.bg-img {background-image: url('{{.Params.featuredImg}}');}</style>\n\t{{- else if .Params.images -}}\n\t\t{{- range first 1 .Params.images -}}\n\t\t<style>.bg-img {background-image: url('{{. | absURL}}');}</style>\n\t\t{{- end -}}\n\t{{- end -}}\n{{ end }}\n\n{{ define \"header\" }}\n{{ partial \"header.html\" . }}\n{{ end }}\n\n{{ define \"main\" }}\n\t{{- if (or .Params.images .Params.featuredImg) }}\n\t<div class=\"bg-img\"></div>\n\t{{- end }}\n\t<main class=\"site-main section-inner thin animated fadeIn faster\">\n\t\t<h1>{{ .Title }}</h1>\n\t\t<div class=\"content\">\n\t\t\t{{ .Content | replaceRE \"(<h[1-6] id=\\\"([^\\\"]+)\\\".+)(</h[1-6]+>)\" `${1}<a href=\"#${2}\" class=\"anchor\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3\"></path><line x1=\"8\" y1=\"12\" x2=\"16\" y2=\"12\"></line></svg></a>${3}` | safeHTML }}\n\t\t</div>\n\t\t{{- if .Params.comments }}\n\t\t<div id=\"comments\" class=\"thin\">\n\t\t\t{{ partial \"comments.html\" . }}\n\t\t</div>\n\t\t{{- end }}\n\t</main>\n{{ end }}\n\n{{ define \"footer\" }}\n{{ partialCached \"footer.html\" . }}\n{{ end }}\n"
  },
  {
    "path": "themes/hermit/layouts/index.html",
    "content": "{{ define \"head\" }}\n\t{{ if .Site.Params.bgImg -}}\n\t<style>.bg-img {background-image: url('{{.Site.Params.bgImg}}');}</style>\n\t{{- else if .Site.Params.images -}}\n\t\t{{- range first 1 .Site.Params.images -}}\n\t\t<style>.bg-img {background-image: url('{{. | absURL}}');}</style>\n\t\t{{- end -}}\n\t{{- end -}}\n{{ end }}\n\n{{ define \"main\" }}\n\t{{- if (or .Site.Params.images .Site.Params.bgImg) }}\n\t<div class=\"bg-img\"></div>\n\t{{- end }}\n\t<div id=\"spotlight\" class=\"animated fadeIn\">\n\t\t<div id=\"home-center\">\n\t\t\t<h1 id=\"home-title\">{{ .Site.Title }}</h1>\n\t\t\t{{- with .Site.Params.homeSubtitle }}\n\t\t\t<p id=\"home-subtitle\">{{.}}</p>\n\t\t\t{{- end }}\n\t\t\t{{- with .Site.Params.social }}\n\t\t\t<div id=\"home-social\">\n\t\t\t\t{{ partialCached \"social-icons.html\" . }}\n\t\t\t</div>\n\t\t\t{{- end }}\n\t\t\t<nav id=\"home-nav\" class=\"site-nav\">\n\t\t\t\t{{- range .Site.Menus.main }}\n\t\t\t\t<a href=\"{{ .URL | absLangURL }}\">{{ .Name }}</a>\n\t\t\t\t{{- end }}\n\t\t\t</nav>\n\t\t</div>\n\t\t<div id=\"home-footer\">\n\t\t\t<p>\n\t\t\t\t&copy; {{ now.Format \"2006\" }} <a href=\"{{ .Site.BaseURL }}\">{{ .Site.Author.name }}</a>{{ .Site.Params.footerCopyright | safeHTML }}\n\t\t\t\t{{- with (not (in (.Site.Language.Get \"disableKinds\") \"RSS\")) }} &#183; <a href=\"{{ \"post/index.xml\" | absURL }}\" target=\"_blank\" title=\"rss\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-rss\"><path d=\"M4 11a9 9 0 0 1 9 9\"></path><path d=\"M4 4a16 16 0 0 1 16 16\"></path><circle cx=\"5\" cy=\"19\" r=\"1\"></circle></svg></a>{{ end }}\n\t\t\t</p>\n\t\t</div>\n\t</div>\n{{ end }}\n"
  },
  {
    "path": "themes/hermit/layouts/partials/analytics.html",
    "content": "{{ template \"_internal/google_analytics_async.html\" . }}\n"
  },
  {
    "path": "themes/hermit/layouts/partials/favicons.html",
    "content": "\t<link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"{{\"apple-touch-icon.png\" | relURL}}\">\n\t<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"{{\"favicon-32x32.png\" | relURL}}\">\n\t<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"{{\"favicon-16x16.png\" | relURL}}\">\n\t<link rel=\"manifest\" href=\"{{\"site.webmanifest\" | relURL}}\">\n\t<link rel=\"mask-icon\" href=\"{{\"safari-pinned-tab.svg\" | relURL}}\" color=\"{{.Site.Params.themeColor}}\">\n\t<link rel=\"shortcut icon\" href=\"{{\"favicon.ico\" | relURL}}\">\n"
  },
  {
    "path": "themes/hermit/layouts/partials/footer.html",
    "content": "\t<footer id=\"site-footer\" class=\"section-inner thin animated fadeIn faster\">\n\t\t<p>&copy; {{ now.Format \"2006\" }} <a href=\"{{ .Site.BaseURL }}\">{{ .Site.Author.name }}</a>{{ .Site.Params.footerCopyright | safeHTML }}</p>\n\t\t<p>\n\t\t\tMade with <a href=\"https://gohugo.io/\" target=\"_blank\" rel=\"noopener\">Hugo</a> &#183; Theme <a href=\"https://github.com/Track3/hermit\" target=\"_blank\" rel=\"noopener\">Hermit</a>\n\t\t\t{{- with (not (in (.Site.Language.Get \"disableKinds\") \"RSS\")) }} &#183; <a href=\"{{ \"post/index.xml\" | absURL }}\" target=\"_blank\" title=\"rss\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-rss\"><path d=\"M4 11a9 9 0 0 1 9 9\"></path><path d=\"M4 4a16 16 0 0 1 16 16\"></path><circle cx=\"5\" cy=\"19\" r=\"1\"></circle></svg></a>{{ end }}\n\t\t</p>\n\t</footer>\n"
  },
  {
    "path": "themes/hermit/layouts/partials/header.html",
    "content": "\t<header id=\"site-header\" class=\"animated slideInUp faster\">\n\t\t<div class=\"hdr-wrapper section-inner\">\n\t\t\t<div class=\"hdr-left\">\n\t\t\t\t<div class=\"site-branding\">\n\t\t\t\t\t<a href=\"{{.Site.BaseURL}}\">{{ .Site.Title }}</a>\n\t\t\t\t</div>\n\t\t\t\t<nav class=\"site-nav hide-in-mobile\">\n\t\t\t\t\t{{- range .Site.Menus.main }}\n\t\t\t\t\t<a href=\"{{ .URL | absLangURL}}\">{{ .Name }}</a>\n\t\t\t\t\t{{- end }}\n\t\t\t\t</nav>\n\t\t\t</div>\n\t\t\t<div class=\"hdr-right hdr-icons\">\n\t\t\t\t{{ if (or .Params.images .Params.featuredImg) -}}\n\t\t\t\t<button id=\"img-btn\" class=\"hdr-btn\" title=\"{{i18n \"featuredImage\"}}\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-image\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\"></rect><circle cx=\"8.5\" cy=\"8.5\" r=\"1.5\"></circle><polyline points=\"21 15 16 10 5 21\"></polyline></svg></button>\n\t\t\t\t{{- end }}\n\t\t\t\t{{- with .Params.toc -}}\n\t\t\t\t<button id=\"toc-btn\" class=\"hdr-btn desktop-only-ib\" title=\"{{i18n \"tableOfContents\"}}\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-list\"><line x1=\"8\" y1=\"6\" x2=\"21\" y2=\"6\"></line><line x1=\"8\" y1=\"12\" x2=\"21\" y2=\"12\"></line><line x1=\"8\" y1=\"18\" x2=\"21\" y2=\"18\"></line><line x1=\"3\" y1=\"6\" x2=\"3\" y2=\"6\"></line><line x1=\"3\" y1=\"12\" x2=\"3\" y2=\"12\"></line><line x1=\"3\" y1=\"18\" x2=\"3\" y2=\"18\"></line></svg></button>\n\t\t\t\t{{- end }}\n\t\t\t\t{{- with .Site.Params.social -}}\n\t\t\t\t<span class=\"hdr-social hide-in-mobile\">{{ partialCached \"social-icons.html\" . }}</span>\n\t\t\t\t{{- end -}}\n\t\t\t\t<button id=\"menu-btn\" class=\"hdr-btn\" title=\"{{i18n \"menu\"}}\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-menu\"><line x1=\"3\" y1=\"12\" x2=\"21\" y2=\"12\"></line><line x1=\"3\" y1=\"6\" x2=\"21\" y2=\"6\"></line><line x1=\"3\" y1=\"18\" x2=\"21\" y2=\"18\"></line></svg></button>\n\t\t\t</div>\n\t\t</div>\n\t</header>\n\t<div id=\"mobile-menu\" class=\"animated fast\">\n\t\t<ul>\n\t\t\t{{- range .Site.Menus.main }}\n\t\t\t<li><a href=\"{{ .URL | absLangURL }}\">{{ .Name }}</a></li>\n\t\t\t{{- end }}\n\t\t</ul>\n\t</div>\n"
  },
  {
    "path": "themes/hermit/layouts/partials/social-icons.html",
    "content": "{{ range . -}}\n<a href=\"{{ .url }}\" target=\"_blank\" rel=\"noopener me\" title=\"{{ .name | humanize }}\">{{ partial \"svg.html\" . }}</a>\n{{- end -}}\n"
  },
  {
    "path": "themes/hermit/layouts/partials/structured-data.html",
    "content": "{{/* We use some Hugo built-in templates, you can find their source here: */}}\n{{/* https://github.com/gohugoio/hugo/tree/master/tpl/tplimpl/embedded/templates */}}\n\n{{- template \"_internal/schema.html\" . }}\n{{- template \"_internal/opengraph.html\" . }}\n{{- template \"_internal/twitter_cards.html\" . }}\n"
  },
  {
    "path": "themes/hermit/layouts/partials/svg.html",
    "content": "{{- if (eq .name \"codepen\") -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-codepen\"><polygon points=\"12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5 12 2\"></polygon><line x1=\"12\" y1=\"22\" x2=\"12\" y2=\"15.5\"></line><polyline points=\"22 8.5 12 15.5 2 8.5\"></polyline><polyline points=\"2 15.5 12 8.5 22 15.5\"></polyline><line x1=\"12\" y1=\"2\" x2=\"12\" y2=\"8.5\"></line></svg>\n{{- else if (eq .name \"facebook\") -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-facebook\"><path d=\"M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z\"></path></svg>\n{{- else if (eq .name \"github\") -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-github\"><path d=\"M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22\"></path></svg>\n{{- else if (eq .name \"gitlab\") -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-gitlab\"><path d=\"M22.65 14.39L12 22.13 1.35 14.39a.84.84 0 0 1-.3-.94l1.22-3.78 2.44-7.51A.42.42 0 0 1 4.82 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.49h8.1l2.44-7.51A.42.42 0 0 1 18.6 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.51L23 13.45a.84.84 0 0 1-.35.94z\"></path></svg>\n{{- else if (eq .name \"instagram\") -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-instagram\"><rect x=\"2\" y=\"2\" width=\"20\" height=\"20\" rx=\"5\" ry=\"5\"></rect><path d=\"M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z\"></path><line x1=\"17.5\" y1=\"6.5\" x2=\"17.5\" y2=\"6.5\"></line></svg>\n{{- else if (eq .name \"linkedin\") -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-linkedin\"><path d=\"M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z\"></path><rect x=\"2\" y=\"9\" width=\"4\" height=\"12\"></rect><circle cx=\"4\" cy=\"4\" r=\"2\"></circle></svg>\n{{- else if (eq .name \"slack\") -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-slack\"><path d=\"M22.08 9C19.81 1.41 16.54-.35 9 1.92S-.35 7.46 1.92 15 7.46 24.35 15 22.08 24.35 16.54 22.08 9z\"></path><line x1=\"12.57\" y1=\"5.99\" x2=\"16.15\" y2=\"16.39\"></line><line x1=\"7.85\" y1=\"7.61\" x2=\"11.43\" y2=\"18.01\"></line><line x1=\"16.39\" y1=\"7.85\" x2=\"5.99\" y2=\"11.43\"></line><line x1=\"18.01\" y1=\"12.57\" x2=\"7.61\" y2=\"16.15\"></line></svg>\n{{- else if (eq .name \"telegram\") -}}\n<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" aria-hidden=\"true\" class=\"feather\"><path d=\"m 22.05,1.577 c -0.393,-0.016 -0.784,0.08 -1.117,0.235 -0.484,0.186 -4.92,1.902 -9.41,3.64 C 9.263,6.325 7.005,7.198 5.267,7.867 3.53,8.537 2.222,9.035 2.153,9.059 c -0.46,0.16 -1.082,0.362 -1.61,0.984 -0.79581202,1.058365 0.21077405,1.964825 1.004,2.499 1.76,0.564 3.58,1.102 5.087,1.608 0.556,1.96 1.09,3.927 1.618,5.89 0.174,0.394 0.553,0.54 0.944,0.544 l -0.002,0.02 c 0,0 0.307,0.03 0.606,-0.042 0.3,-0.07 0.677,-0.244 1.02,-0.565 0.377,-0.354 1.4,-1.36 1.98,-1.928 l 4.37,3.226 0.035,0.02 c 0,0 0.484,0.34 1.192,0.388 0.354,0.024 0.82,-0.044 1.22,-0.337 0.403,-0.294 0.67,-0.767 0.795,-1.307 0.374,-1.63 2.853,-13.427 3.276,-15.38 L 23.676,4.725 C 23.972,3.625 23.863,2.617 23.18,2.02 22.838,1.723 22.444,1.593 22.05,1.576 Z\"></path></svg>\n{{- else if (eq .name \"twitter\") -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-twitter\"><path d=\"M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z\"></path></svg>\n{{- else if (eq .name \"youtube\") -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-youtube\"><path d=\"M22.54 6.42a2.78 2.78 0 0 0-1.94-2C18.88 4 12 4 12 4s-6.88 0-8.6.46a2.78 2.78 0 0 0-1.94 2A29 29 0 0 0 1 11.75a29 29 0 0 0 .46 5.33A2.78 2.78 0 0 0 3.4 19c1.72.46 8.6.46 8.6.46s6.88 0 8.6-.46a2.78 2.78 0 0 0 1.94-2 29 29 0 0 0 .46-5.25 29 29 0 0 0-.46-5.33z\"></path><polygon points=\"9.75 15.02 15.5 11.75 9.75 8.48 9.75 15.02\"></polygon></svg>\n{{- else if (eq .name \"email\") -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-mail\"><path d=\"M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z\"></path><polyline points=\"22,6 12,13 2,6\"></polyline></svg>\n{{- else -}}\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-link\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"></path><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"></path></svg>\n{{- end -}}\n"
  },
  {
    "path": "themes/hermit/layouts/post/rss.xml",
    "content": "<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\">\n\t<channel>\n\t\t<title>{{ if eq  .Title  .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }}</title>\n\t\t<link>{{ .Permalink }}</link>\n\t\t<description>Recent content {{ if ne  .Title  .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }}</description>\n\t\t<generator>Hugo -- gohugo.io</generator>{{ with .Site.LanguageCode }}\n\t\t<language>{{.}}</language>{{end}}{{ with .Site.Author.email }}\n\t\t<managingEditor>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</managingEditor>{{end}}{{ with .Site.Author.email }}\n\t\t<webMaster>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</webMaster>{{end}}{{ with .Site.Copyright }}\n\t\t<copyright>{{.}}</copyright>{{end}}{{ if not .Date.IsZero }}\n\t\t<lastBuildDate>{{ .Date.Format \"Mon, 02 Jan 2006 15:04:05 -0700\" | safeHTML }}</lastBuildDate>{{ end }}\n\t\t{{ with .OutputFormats.Get \"RSS\" -}}\n\t\t\t{{ printf \"<atom:link href=%q rel=\\\"self\\\" type=%q />\" .Permalink .MediaType | safeHTML }}\n\t\t{{ end -}}\n\t\t{{ range .Pages }}\n\t\t<item>\n\t\t\t<title>{{ .Title }}</title>\n\t\t\t<link>{{ .Permalink }}</link>\n\t\t\t<pubDate>{{ .Date.Format \"Mon, 02 Jan 2006 15:04:05 -0700\" | safeHTML }}</pubDate>\n\t\t\t{{ with .Site.Author.email }}<author>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</author>{{end}}\n\t\t\t<guid>{{ .Permalink }}</guid>\n\t\t\t<description>{{ .Summary | html }}</description>\n\t\t\t<content type=\"html\">{{ printf `<![CDATA[%s]]>` .Content | safeHTML }}</content>\n\t\t</item>\n\t\t{{ end }}\n\t</channel>\n</rss>\n"
  },
  {
    "path": "themes/hermit/layouts/post/single.html",
    "content": "{{ define \"head\" }}\n\t{{ if .Params.featuredImg -}}\n\t<style>.bg-img {background-image: url('{{.Params.featuredImg}}');}</style>\n\t{{- else if .Params.images -}}\n\t\t{{- range first 1 .Params.images -}}\n\t\t<style>.bg-img {background-image: url('{{. | absURL}}');}</style>\n\t\t{{- end -}}\n\t{{- end -}}\n{{ end }}\n\n{{ define \"header\" }}\n{{ partial \"header.html\" . }}\n{{ end }}\n\n{{ define \"main\" }}\n\t{{- if (or .Params.images .Params.featuredImg) }}\n\t<div class=\"bg-img\"></div>\n\t{{- end }}\n\t<main class=\"site-main section-inner animated fadeIn faster\">\n\t\t<article class=\"thin\">\n\t\t\t<header class=\"post-header\">\n\t\t\t\t<div class=\"post-meta\"><span>{{ .Date.Format .Site.Params.dateform }}</span></div>\n\t\t\t\t<h1>{{ .Title }}</h1>\n\t\t\t</header>\n\t\t\t<div class=\"content\">\n\t\t\t\t{{ .Content | replaceRE \"(<h[1-6] id=\\\"([^\\\"]+)\\\".+)(</h[1-6]+>)\" `${1}<a href=\"#${2}\" class=\"anchor\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3\"></path><line x1=\"8\" y1=\"12\" x2=\"16\" y2=\"12\"></line></svg></a>${3}` | safeHTML }}\n\t\t\t</div>\n\t\t\t<hr class=\"post-end\">\n\t\t\t<footer class=\"post-info\">\n\t\t\t\t{{- with .Params.tags }}\n\t\t\t\t<p>\n\t\t\t\t\t<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-tag meta-icon\"><path d=\"M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z\"></path><line x1=\"7\" y1=\"7\" x2=\"7\" y2=\"7\"></line></svg>\n\t\t\t\t\t{{- range . -}}\n\t\t\t\t\t<span class=\"tag\"><a href=\"{{ \"tags/\" | absURL }}{{ . | urlize }}\">{{.}}</a></span>\n\t\t\t\t\t{{- end }}\n\t\t\t\t</p>\n\t\t\t\t{{- end }}\n\t\t\t\t<p><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-file-text\"><path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"></path><polyline points=\"14 2 14 8 20 8\"></polyline><line x1=\"16\" y1=\"13\" x2=\"8\" y2=\"13\"></line><line x1=\"16\" y1=\"17\" x2=\"8\" y2=\"17\"></line><polyline points=\"10 9 9 9 8 9\"></polyline></svg>{{ i18n \"wordCount\" . }}</p>\n\t\t\t\t<p><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-calendar\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\"></rect><line x1=\"16\" y1=\"2\" x2=\"16\" y2=\"6\"></line><line x1=\"8\" y1=\"2\" x2=\"8\" y2=\"6\"></line><line x1=\"3\" y1=\"10\" x2=\"21\" y2=\"10\"></line></svg>{{ dateFormat .Site.Params.dateformNumTime .Date.Local }}</p>\n\t\t\t\t{{- if and .GitInfo .Site.Params.gitUrl }}\n\t\t\t\t<p><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-git-commit\"><circle cx=\"12\" cy=\"12\" r=\"4\"></circle><line x1=\"1.05\" y1=\"12\" x2=\"7\" y2=\"12\"></line><line x1=\"17.01\" y1=\"12\" x2=\"22.96\" y2=\"12\"></line></svg><a href=\"{{ .Site.Params.gitUrl -}}{{ .GitInfo.Hash }}\" target=\"_blank\" rel=\"noopener\">{{ .GitInfo.AbbreviatedHash }}</a> @ {{ dateFormat .Site.Params.dateformNum .GitInfo.AuthorDate.Local }}</p>\n\t\t\t\t{{- end }}\n\t\t\t</footer>\n\t\t</article>\n\t\t{{- if .Params.toc }}\n\t\t<aside id=\"toc\" class=\"show-toc\">\n\t\t\t<div class=\"toc-title\">{{ i18n \"tableOfContents\" }}</div>\n\t\t\t{{ .TableOfContents }}\n\t\t</aside>\n\t\t{{- end }}\n\t\t<div class=\"post-nav thin\">\n\t\t\t{{- with .NextInSection }}\n\t\t\t<a class=\"next-post\" href=\"{{ .Permalink }}\">\n\t\t\t\t<span class=\"post-nav-label\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-arrow-left\"><line x1=\"19\" y1=\"12\" x2=\"5\" y2=\"12\"></line><polyline points=\"12 19 5 12 12 5\"></polyline></svg>&nbsp;{{ i18n \"newer\" }}</span><br><span>{{ .Title }}</span>\n\t\t\t</a>\n\t\t\t{{- end }}\n\t\t\t{{- with .PrevInSection }}\n\t\t\t<a class=\"prev-post\" href=\"{{ .Permalink }}\">\n\t\t\t\t<span class=\"post-nav-label\">{{ i18n \"older\" }}&nbsp;<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-arrow-right\"><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"></line><polyline points=\"12 5 19 12 12 19\"></polyline></svg></span><br><span>{{ .Title }}</span>\n\t\t\t</a>\n\t\t\t{{- end }}\n\t\t</div>\n\t\t<div id=\"comments\" class=\"thin\">\n\t\t\t<script src=\"https://utteranc.es/client.js\"\n\t\t\t\t\t\t\trepo=\"aylei/blog\"\n\t\t\t\t\t\t\tissue-term=\"pathname\"\n\t\t\t\t\t\t\ttheme=\"github-light\"\n\t\t\t\t\t\t\tcrossorigin=\"anonymous\"\n\t\t\t\t\t\t\tasync>\n\t\t\t</script>\n\t\t</div>\n\t</main>\n{{ end }}\n\n{{ define \"footer\" }}\n{{ partialCached \"footer.html\" . }}\n{{ end }}\n"
  },
  {
    "path": "themes/hermit/layouts/posts/rss.xml",
    "content": "<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\">\n\t<channel>\n\t\t<title>{{ if eq  .Title  .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }}</title>\n\t\t<link>{{ .Permalink }}</link>\n\t\t<description>Recent content {{ if ne  .Title  .Site.Title }}{{ with .Title }}in {{.}} {{ end }}{{ end }}on {{ .Site.Title }}</description>\n\t\t<generator>Hugo -- gohugo.io</generator>{{ with .Site.LanguageCode }}\n\t\t<language>{{.}}</language>{{end}}{{ with .Site.Author.email }}\n\t\t<managingEditor>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</managingEditor>{{end}}{{ with .Site.Author.email }}\n\t\t<webMaster>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</webMaster>{{end}}{{ with .Site.Copyright }}\n\t\t<copyright>{{.}}</copyright>{{end}}{{ if not .Date.IsZero }}\n\t\t<lastBuildDate>{{ .Date.Format \"Mon, 02 Jan 2006 15:04:05 -0700\" | safeHTML }}</lastBuildDate>{{ end }}\n\t\t{{ with .OutputFormats.Get \"RSS\" -}}\n\t\t\t{{ printf \"<atom:link href=%q rel=\\\"self\\\" type=%q />\" .Permalink .MediaType | safeHTML }}\n\t\t{{ end -}}\n\t\t{{ range .Pages }}\n\t\t<item>\n\t\t\t<title>{{ .Title }}</title>\n\t\t\t<link>{{ .Permalink }}</link>\n\t\t\t<pubDate>{{ .Date.Format \"Mon, 02 Jan 2006 15:04:05 -0700\" | safeHTML }}</pubDate>\n\t\t\t{{ with .Site.Author.email }}<author>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</author>{{end}}\n\t\t\t<guid>{{ .Permalink }}</guid>\n\t\t\t<description>{{ .Summary | html }}</description>\n\t\t\t<content type=\"html\">{{ printf `<![CDATA[%s]]>` .Content | safeHTML }}</content>\n\t\t</item>\n\t\t{{ end }}\n\t</channel>\n</rss>\n"
  },
  {
    "path": "themes/hermit/layouts/posts/single.html",
    "content": "{{ define \"head\" }}\n\t{{ if .Params.featuredImg -}}\n\t<style>.bg-img {background-image: url('{{.Params.featuredImg}}');}</style>\n\t{{- else if .Params.images -}}\n\t\t{{- range first 1 .Params.images -}}\n\t\t<style>.bg-img {background-image: url('{{. | absURL}}');}</style>\n\t\t{{- end -}}\n\t{{- end -}}\n{{ end }}\n\n{{ define \"header\" }}\n{{ partial \"header.html\" . }}\n{{ end }}\n\n{{ define \"main\" }}\n\t{{- if (or .Params.images .Params.featuredImg) }}\n\t<div class=\"bg-img\"></div>\n\t{{- end }}\n\t<main class=\"site-main section-inner animated fadeIn faster\">\n\t\t<article class=\"thin\">\n\t\t\t<header class=\"post-header\">\n\t\t\t\t<div class=\"post-meta\"><span>{{ .Date.Format .Site.Params.dateform }}</span></div>\n\t\t\t\t<h1>{{ .Title }}</h1>\n\t\t\t</header>\n\t\t\t<div class=\"content\">\n\t\t\t\t{{ .Content | replaceRE \"(<h[1-6] id=\\\"([^\\\"]+)\\\".+)(</h[1-6]+>)\" `${1}<a href=\"#${2}\" class=\"anchor\" aria-hidden=\"true\"><svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3\"></path><line x1=\"8\" y1=\"12\" x2=\"16\" y2=\"12\"></line></svg></a>${3}` | safeHTML }}\n\t\t\t</div>\n\t\t\t<hr class=\"post-end\">\n\t\t\t<footer class=\"post-info\">\n\t\t\t\t{{- with .Params.tags }}\n\t\t\t\t<p>\n\t\t\t\t\t<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-tag meta-icon\"><path d=\"M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z\"></path><line x1=\"7\" y1=\"7\" x2=\"7\" y2=\"7\"></line></svg>\n\t\t\t\t\t{{- range . -}}\n\t\t\t\t\t<span class=\"tag\"><a href=\"{{ \"tags/\" | absURL }}{{ . | urlize }}\">{{.}}</a></span>\n\t\t\t\t\t{{- end }}\n\t\t\t\t</p>\n\t\t\t\t{{- end }}\n\t\t\t\t<p><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-file-text\"><path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"></path><polyline points=\"14 2 14 8 20 8\"></polyline><line x1=\"16\" y1=\"13\" x2=\"8\" y2=\"13\"></line><line x1=\"16\" y1=\"17\" x2=\"8\" y2=\"17\"></line><polyline points=\"10 9 9 9 8 9\"></polyline></svg>{{ i18n \"wordCount\" . }}</p>\n\t\t\t\t<p><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-calendar\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\"></rect><line x1=\"16\" y1=\"2\" x2=\"16\" y2=\"6\"></line><line x1=\"8\" y1=\"2\" x2=\"8\" y2=\"6\"></line><line x1=\"3\" y1=\"10\" x2=\"21\" y2=\"10\"></line></svg>{{ dateFormat .Site.Params.dateformNumTime .Date.Local }}</p>\n\t\t\t\t{{- if and .GitInfo .Site.Params.gitUrl }}\n\t\t\t\t<p><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-git-commit\"><circle cx=\"12\" cy=\"12\" r=\"4\"></circle><line x1=\"1.05\" y1=\"12\" x2=\"7\" y2=\"12\"></line><line x1=\"17.01\" y1=\"12\" x2=\"22.96\" y2=\"12\"></line></svg><a href=\"{{ .Site.Params.gitUrl -}}{{ .GitInfo.Hash }}\" target=\"_blank\" rel=\"noopener\">{{ .GitInfo.AbbreviatedHash }}</a> @ {{ dateFormat .Site.Params.dateformNum .GitInfo.AuthorDate.Local }}</p>\n\t\t\t\t{{- end }}\n\t\t\t</footer>\n\t\t</article>\n\t\t{{- if .Params.toc }}\n\t\t<aside id=\"toc\">\n\t\t\t<div class=\"toc-title\">{{ i18n \"tableOfContents\" }}</div>\n\t\t\t{{ .TableOfContents }}\n\t\t</aside>\n\t\t{{- end }}\n\t\t<div class=\"post-nav thin\">\n\t\t\t{{- with .NextInSection }}\n\t\t\t<a class=\"next-post\" href=\"{{ .Permalink }}\">\n\t\t\t\t<span class=\"post-nav-label\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-arrow-left\"><line x1=\"19\" y1=\"12\" x2=\"5\" y2=\"12\"></line><polyline points=\"12 19 5 12 12 5\"></polyline></svg>&nbsp;{{ i18n \"newer\" }}</span><br><span>{{ .Title }}</span>\n\t\t\t</a>\n\t\t\t{{- end }}\n\t\t\t{{- with .PrevInSection }}\n\t\t\t<a class=\"prev-post\" href=\"{{ .Permalink }}\">\n\t\t\t\t<span class=\"post-nav-label\">{{ i18n \"older\" }}&nbsp;<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-arrow-right\"><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"></line><polyline points=\"12 5 19 12 12 19\"></polyline></svg></span><br><span>{{ .Title }}</span>\n\t\t\t</a>\n\t\t\t{{- end }}\n\t\t</div>\n\t\t<div id=\"comments\" class=\"thin\">\n\t\t{{- partial \"comments.html\" . -}}\n\t\t</div>\n\t</main>\n{{ end }}\n\n{{ define \"footer\" }}\n{{ partialCached \"footer.html\" . }}\n{{ end }}\n"
  },
  {
    "path": "themes/hermit/resources/_gen/assets/js/js/main.js_d11fe7b62c27961c87ecd0f2490357b9.content",
    "content": "const throttle=(callback,limit)=>{let timeoutHandler=null;return()=>{if(timeoutHandler==null){timeoutHandler=setTimeout(()=>{callback();timeoutHandler=null;},limit);}};};const listen=(ele,e,callback)=>{if(document.querySelector(ele)!==null){document.querySelector(ele).addEventListener(e,callback);}}\nlet header=document.getElementById('site-header');let lastScrollPosition=window.pageYOffset;const autoHideHeader=()=>{let currentScrollPosition=window.pageYOffset;if(currentScrollPosition>lastScrollPosition){header.classList.remove('slideInUp');header.classList.add('slideOutDown');}else{header.classList.remove('slideOutDown');header.classList.add('slideInUp');}\nlastScrollPosition=currentScrollPosition;}\nlet mobileMenuVisible=false;const toggleMobileMenu=()=>{let mobileMenu=document.getElementById('mobile-menu');if(mobileMenuVisible==false){mobileMenu.style.animationName='bounceInRight';mobileMenu.style.webkitAnimationName='bounceInRight';mobileMenu.style.display='block';mobileMenuVisible=true;}else{mobileMenu.style.animationName='bounceOutRight';mobileMenu.style.webkitAnimationName='bounceOutRight'\nmobileMenuVisible=false;}}\nconst showImg=()=>{document.querySelector('.bg-img').classList.add('show-bg-img');}\nconst hideImg=()=>{document.querySelector('.bg-img').classList.remove('show-bg-img');}\nconst toggleToc=()=>{document.getElementById('toc').classList.toggle('show-toc');}\nif(header!==null){listen('#menu-btn',\"click\",toggleMobileMenu);listen('#toc-btn',\"click\",toggleToc);listen('#img-btn',\"click\",showImg);listen('.bg-img',\"click\",hideImg);document.querySelectorAll('.post-year').forEach((ele)=>{ele.addEventListener('click',()=>{window.location.hash='#'+ele.id;});});window.addEventListener('scroll',throttle(()=>{autoHideHeader();if(mobileMenuVisible==true){toggleMobileMenu();}},250));}"
  },
  {
    "path": "themes/hermit/resources/_gen/assets/js/js/main.js_d11fe7b62c27961c87ecd0f2490357b9.json",
    "content": "{\"Target\":\"js/main.min.784417f5847151f848c339cf0acb13a06cbb648b1483435a28ed4556c4ead69b.js\",\"MediaType\":\"application/javascript\",\"Data\":{\"Integrity\":\"sha256-eEQX9YRxUfhIwznPCssToGy7ZIsUg0NaKO1FVsTq1ps=\"}}"
  },
  {
    "path": "themes/hermit/resources/_gen/assets/scss/scss/style.scss_c16d144eee185fbddd582cd5e25a4fae.content",
    "content": "@charset \"UTF-8\";/*!normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css*/html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}.chroma{color:#eee;background-color:#2c3e50}.chroma .err{color:#960050;background-color:#1e0010}.chroma .lntd{vertical-align:top;padding:0;margin:0;border:0}.chroma .lntable{border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block}.chroma .hl{display:block;width:100%;background-color:#ffc}.chroma .lnt{margin-right:.4em;padding:0 .4em}.chroma .ln{margin-right:.4em;padding:0 .4em}.chroma .k{color:#66d9ef}.chroma .kc{color:#66d9ef}.chroma .kd{color:#66d9ef}.chroma .kn{color:#f92672}.chroma .kp{color:#66d9ef}.chroma .kr{color:#66d9ef}.chroma .kt{color:#66d9ef}.chroma .na{color:#a6e22e}.chroma .nc{color:#a6e22e}.chroma .no{color:#66d9ef}.chroma .nd{color:#a6e22e}.chroma .ne{color:#a6e22e}.chroma .nf{color:#a6e22e}.chroma .nx{color:#a6e22e}.chroma .nt{color:#f92672}.chroma .l{color:#ae81ff}.chroma .ld{color:#e6db74}.chroma .s{color:#e6db74}.chroma .sa{color:#e6db74}.chroma .sb{color:#e6db74}.chroma .sc{color:#e6db74}.chroma .dl{color:#e6db74}.chroma .sd{color:#e6db74}.chroma .s2{color:#e6db74}.chroma .se{color:#ae81ff}.chroma .sh{color:#e6db74}.chroma .si{color:#e6db74}.chroma .sx{color:#e6db74}.chroma .sr{color:#e6db74}.chroma .s1{color:#e6db74}.chroma .ss{color:#e6db74}.chroma .m{color:#ae81ff}.chroma .mb{color:#ae81ff}.chroma .mf{color:#ae81ff}.chroma .mh{color:#ae81ff}.chroma .mi{color:#ae81ff}.chroma .il{color:#ae81ff}.chroma .mo{color:#ae81ff}.chroma .o{color:#f92672}.chroma .ow{color:#f92672}.chroma .c{color:#75715e}.chroma .ch{color:#75715e}.chroma .cm{color:#75715e}.chroma .c1{color:#75715e}.chroma .cs{color:#75715e}.chroma .cp{color:#75715e}.chroma .cpf{color:#75715e}.chroma .gd{color:#f92672}.chroma .ge{font-style:italic}.chroma .gi{color:#a6e22e}.chroma .gs{font-weight:700}.chroma .gu{color:#75715e}/*!* animate.css -https://daneden.github.io/animate.css/\n* Version - 3.7.0\n* Licensed under the MIT license - http://opensource.org/licenses/MIT\n*\n* Copyright (c) 2019 Daniel Eden*/@-webkit-keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}@keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}.flash{-webkit-animation-name:flash;animation-name:flash}@-webkit-keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(0.215,0.61,0.355,1);animation-timing-function:cubic-bezier(0.215,0.61,0.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(0.215,0.61,0.355,1);animation-timing-function:cubic-bezier(0.215,0.61,0.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.bounceInRight{-webkit-animation-name:bounceInRight;animation-name:bounceInRight}@-webkit-keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.bounceOutRight{-webkit-animation-name:bounceOutRight;animation-name:bounceOutRight}@-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.slideInUp{-webkit-animation-name:slideInUp;animation-name:slideInUp}@-webkit-keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.slideOutDown{-webkit-animation-name:slideOutDown;animation-name:slideOutDown}.animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.animated.infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.animated.delay-1s{-webkit-animation-delay:1s;animation-delay:1s}.animated.delay-2s{-webkit-animation-delay:2s;animation-delay:2s}.animated.delay-3s{-webkit-animation-delay:3s;animation-delay:3s}.animated.delay-4s{-webkit-animation-delay:4s;animation-delay:4s}.animated.delay-5s{-webkit-animation-delay:5s;animation-delay:5s}.animated.fast{-webkit-animation-duration:.8s;animation-duration:.8s}.animated.faster{-webkit-animation-duration:.5s;animation-duration:.5s}.animated.slow{-webkit-animation-duration:2s;animation-duration:2s}.animated.slower{-webkit-animation-duration:3s;animation-duration:3s}@media(prefers-reduced-motion),(print){.animated{-webkit-animation:unset!important;animation:unset!important;-webkit-transition:none!important;transition:none!important}}::-webkit-scrollbar{width:8px;height:8px;background:#2c3e50}::-webkit-scrollbar-thumb{background:#888}::-webkit-scrollbar-thumb:hover{background:#c6cddb}html{background:#494f5c;line-height:1.6;letter-spacing:.06em;scroll-behavior:smooth}body,button,input,select,textarea{color:#c6cddb;font-family:trebuchet ms,Verdana,verdana ref,segoe ui,Candara,lucida grande,lucida sans unicode,lucida sans,Tahoma,sans-serif}pre,code,pre tt{font-family:Consolas,andale mono wt,andale mono,Menlo,Monaco,lucida console,lucida sans typewriter,dejavu sans mono,bitstream vera sans mono,liberation mono,nimbus mono l,courier new,Courier,yahei consolas hybrid,monospace,segoe ui emoji,pingfang sc,microsoft yahei}pre{padding:.7em 1.1em;overflow:auto;font-size:.9em;line-height:1.5;letter-spacing:normal;white-space:pre;color:#eee;background:#2c3e50;border-radius:4px}pre code{padding:0;margin:0;background:#2c3e50}code{color:#eee;background:#7d828a;border-radius:3px;padding:0 3px;margin:0 4px;word-wrap:break-word;letter-spacing:normal}blockquote{border-left:.25em solid;margin:1em;padding:0 1em;font-style:italic}blockquote cite{font-weight:700;font-style:normal}blockquote cite::before{content:\"—— \"}a{color:#c6cddb;text-decoration:none;border:none;transition-property:color;transition-duration:.4s;transition-timing-function:ease-out}a:hover{color:#fff}hr{opacity:.2;border-width:0 0 5px;border-style:dashed;background:transparent;width:50%;margin:1.8em auto}table{border-collapse:collapse;border-spacing:0;empty-cells:show;width:100%;max-width:100%}table th,table td{padding:1.5%;border:1px solid}table th{font-weight:700;vertical-align:bottom}.section-inner{margin:0 auto;max-width:1200px;width:93%}.thin{max-width:720px;margin:auto}.feather{display:inline-block;vertical-align:-.125em;width:1em;height:1em}.desktop-only,.desktop-only-ib{display:none}.screen-reader-text{border:0;clip:rect(1px,1px,1px,1px);clip-path:inset(50%);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute!important;width:1px;word-wrap:normal!important}.screen-reader-text:focus{background-color:#f1f1f1;border-radius:3px;box-shadow:0 0 2px 2px rgba(0,0,0,.6);clip:auto!important;clip-path:none;color:#21759b;display:block;font-size:14px;font-size:.875rem;font-weight:700;height:auto;left:5px;line-height:normal;padding:15px 23px 14px;text-decoration:none;top:5px;width:auto;z-index:100000}#site-header{position:fixed;z-index:1;bottom:0;width:100%;box-sizing:border-box;box-shadow:-1px -2px 3px rgba(0,0,0,.45);background-color:#3b3e48}.hdr-wrapper{display:flex;justify-content:space-between;align-items:center;padding:.5em 0;font-size:1.2rem}.hdr-wrapper .site-branding{display:inline-block;margin-right:.8em;font-size:1.2em}.hdr-wrapper .site-nav{display:inline-block;font-size:1.1em;opacity:.8}.hdr-wrapper .site-nav a{margin-left:.8em}.hdr-icons{font-size:1.2em}.hdr-social{display:inline-block;margin-left:.6em}.hdr-social>a{margin-left:.4em}.hdr-btn{border:none;background:0 0;padding:0;margin-left:.4em;cursor:pointer}#menu-btn{display:none;margin-left:.6em;cursor:pointer}#mobile-menu{position:fixed;bottom:4.8em;right:1.5em;display:none;padding:.6em 1.8em;z-index:1;box-sizing:border-box;box-shadow:-1px -2px 3px 0 rgba(0,0,0,.45);background-color:#3b3e48}#mobile-menu ul{list-style:none;margin:0;padding:0;line-height:2;font-size:1.2em}#site-footer{text-align:center;font-size:.9em;margin-bottom:96px;margin-top:64px}#site-footer p{margin:0}#spotlight{display:flex;height:100vh;flex-direction:column;align-items:center;justify-content:center;max-width:93%;margin:auto;font-size:1.5rem}#spotlight.error-404{flex-direction:row;line-height:normal}p.img-404{margin:0}p.img-404 svg{width:180px;max-width:100%;height:auto}.banner-404{margin-left:2em}.banner-404 h1{font-size:3em;margin:.5rem 0}.banner-404 p{margin-top:0}.banner-404 .btn-404{font-size:.8em}.banner-404 .btn-404 a{display:inline-block;border:2px solid #c6cddb;border-radius:5px;padding:5px;transition-property:color,border-color}.banner-404 .btn-404 a:first-child{margin-right:1em}.banner-404 .btn-404 a:hover{border-color:#fff}.banner-404 .btn-404 a svg{margin-right:.5em}#home-center{display:flex;flex-grow:1;flex-direction:column;justify-content:center}#home-title{margin:0;text-align:center}#home-subtitle{margin-top:0;margin-bottom:1.5em;text-align:center;line-height:normal;font-size:.7em;font-style:italic;opacity:.9}#home-social{font-size:1.4em;text-align:center;opacity:.8}#home-social a{margin:0 .2em}#home-nav{opacity:.8}#home-nav a{display:block;text-align:center;margin-top:.5em}#home-footer{text-align:center;font-size:.6em;line-height:normal;opacity:.6}#home-footer p{margin-top:0}.posts-group{display:flex;margin-bottom:1.9em;line-height:normal}.posts-group .post-year{padding-top:6px;margin-right:1.8em;font-size:1.6em;opacity:.6}.posts-group .post-year:hover{text-decoration:underline;cursor:pointer}.posts-group .posts-list{flex-grow:1;margin:0;padding:0;list-style:none}.posts-group .post-item{border-bottom:1px #7d828a dashed}.posts-group .post-item a{display:flex;justify-content:space-between;align-items:baseline;padding:12px 0}.posts-group .post-day{flex-shrink:0;margin-left:1em;opacity:.6}.bg-img{width:100vw;height:100vh;opacity:.03;z-index:-1;position:fixed;top:0;background-attachment:fixed;background-repeat:no-repeat;background-size:cover;background-position:50%;transition:opacity .5s}.show-bg-img{z-index:100;opacity:1;cursor:pointer}.post-header{margin-top:1.2em;line-height:normal}.post-header .post-meta{font-size:.9em;letter-spacing:normal;opacity:.6}.post-header h1{margin-top:.1em}hr.post-end{width:50%;margin-top:1.6em;margin-bottom:.8em;margin-left:0;border-style:solid;border-bottom-width:4px}.content a{word-wrap:break-word;border:none;box-shadow:inset 0 -4px 0 #018574;transition-property:background-color}.content a:hover{background-color:#018574}.content figure{max-width:100%;height:auto;margin:0;text-align:center}.content figure p{font-size:.8em;font-style:italic;opacity:.6}.content figure.left{float:left;margin-right:1.5em;max-width:50%}.content figure.right{float:right;margin-left:1.5em;max-width:50%}.content figure.big{max-width:100vw}.content img{display:block;max-width:100%;height:auto;margin:auto;border-radius:4px}.content ul,.content ol{padding:0;margin-left:1.8em}.content a.anchor{float:left;margin-left:-20px;padding-right:6px;box-shadow:none;opacity:.8}.content a.anchor:hover{background:0 0;color:#018574;opacity:1}.content a.anchor svg{display:inline-block;width:14px;height:14px;vertical-align:baseline;visibility:hidden}.content a.anchor:focus svg{visibility:visible}.content h1:hover a.anchor svg,.content h2:hover a.anchor svg,.content h3:hover a.anchor svg,.content h4:hover a.anchor svg,.content h5:hover a.anchor svg,.content h6:hover a.anchor svg{visibility:visible}.footnotes{font-size:.85em}.footnotes a{box-shadow:none;text-decoration:underline;transition-property:color}.footnotes a:hover{background:transparent}.footnotes a.footnote-return{text-decoration:none}.footnotes ol{line-height:1.8}.footnote-ref a{box-shadow:none;text-decoration:none;padding:2px;border-radius:2px;background-color:#2c3e50}.post-info{font-size:.8rem;line-height:normal;opacity:.6}.post-info p{margin:.8em 0}.post-info a:hover{border-bottom:1px solid #018574}.post-info svg{margin-right:.8em}.post-info .tag{margin-right:.5em}.post-info .tag::before{content:\"#\"}#toc{position:fixed;left:50%;top:0;display:none}.toc-title{margin-left:1em;margin-bottom:.5em;font-size:.8em;font-weight:700}#TableOfContents{font-size:.8em;opacity:.6}#TableOfContents ul{padding-left:1em;margin:0}#TableOfContents>ul{list-style-type:none}#TableOfContents>ul ul ul{font-size:.9em}#TableOfContents a:hover{border-bottom:#018574 1px solid}.post-nav{display:flex;justify-content:space-between;margin-top:1.5em;margin-bottom:2.5em;font-size:1.2em}.post-nav a{flex-basis:50%;flex-grow:1}.post-nav .next-post{text-align:left;padding-right:5px}.post-nav .prev-post{text-align:right;padding-left:5px}.post-nav .post-nav-label{font-size:.8em;opacity:.8;text-transform:uppercase}@media(min-width:800px){.site-main{margin-top:3em}hr.post-end{width:40%}}@media(min-width:960px){.site-main{margin-top:6em}}@media(min-width:1300px){.site-main{margin-top:8em}.desktop-only,#toc.show-toc{display:block}.desktop-only-ib{display:inline-block}figure.left{margin-left:-240px}figure.left p{text-align:left}figure.right{margin-right:-240px}figure.right p{text-align:right}figure.big{width:1200px;margin-left:-240px}hr.post-end{width:30%}#toc{top:13em;margin-left:370px;max-width:220px}}@media(min-width:1800px){.site-main{margin-top:10em}.section-inner{max-width:1600px}.thin{max-width:960px}figure.left{max-width:75%;margin-left:-320px}figure.right{max-width:75%;margin-right:-320px}figure.big{width:1600px;margin-left:-320px}hr.post-end{width:30%}#toc{top:15em;margin-left:490px;max-width:300px}}@media(max-width:760px){.hide-in-mobile,.site-nav.hide-in-mobile{display:none}#menu-btn{display:inline-block}.posts-group{display:block}.posts-group .post-year{margin:-6px 0 4px}#spotlight.error-404{flex-direction:column;text-align:center}#spotlight.error-404 .banner-404{margin:0}}@media(max-width:520px){.content figure.left,.content figure.right{float:unset;max-width:100%;margin:0}hr.post-end{width:60%}#mobile-menu{right:1.2em}}"
  },
  {
    "path": "themes/hermit/resources/_gen/assets/scss/scss/style.scss_c16d144eee185fbddd582cd5e25a4fae.json",
    "content": "{\"Target\":\"css/style.min.31706917653d2b9e8410abd431f30ec4359a88a94fc87a63654779d87329edec.css\",\"MediaType\":\"text/css\",\"Data\":{\"Integrity\":\"sha256-MXBpF2U9K56EEKvUMfMOxDWaiKlPyHpjZUd52HMp7ew=\"}}"
  },
  {
    "path": "themes/hermit/static/browserconfig.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<browserconfig>\n    <msapplication>\n        <tile>\n            <square150x150logo src=\"/mstile-150x150.png\"/>\n            <TileColor>#00aba9</TileColor>\n        </tile>\n    </msapplication>\n</browserconfig>\n"
  },
  {
    "path": "themes/hermit/static/site.webmanifest",
    "content": "{\n    \"name\": \"\",\n    \"short_name\": \"\",\n    \"icons\": [\n        {\n            \"src\": \"/android-chrome-192x192.png\",\n            \"sizes\": \"192x192\",\n            \"type\": \"image/png\"\n        },\n        {\n            \"src\": \"/android-chrome-384x384.png\",\n            \"sizes\": \"384x384\",\n            \"type\": \"image/png\"\n        }\n    ],\n    \"theme_color\": \"#ffffff\",\n    \"background_color\": \"#ffffff\",\n    \"display\": \"standalone\"\n}\n"
  },
  {
    "path": "themes/hermit/static/utteranc.js",
    "content": "parcelRequire=function(e){var r=\"function\"==typeof parcelRequire&&parcelRequire,n=\"function\"==typeof require&&require,i={};function u(e,u){if(e in i)return i[e];var t=\"function\"==typeof parcelRequire&&parcelRequire;if(!u&&t)return t(e,!0);if(r)return r(e,!0);if(n&&\"string\"==typeof e)return n(e);var o=new Error(\"Cannot find module '\"+e+\"'\");throw o.code=\"MODULE_NOT_FOUND\",o}return u.register=function(e,r){i[e]=r},i=e(u),u.modules=i,u}(function (require) {var d={};function q(e){for(var r,o=/\\+/g,n=/([^&=]+)=?([^&]*)/g,p=function(e){return decodeURIComponent(e.replace(o,\" \"))},a={};r=n.exec(e);)a[p(r[1])]=p(r[2]);return a}function h(e){var r=[];for(var o in e)e.hasOwnProperty(o)&&r.push(encodeURIComponent(o)+\"=\"+encodeURIComponent(e[o]));return r.join(\"&\")}d.deparam=q,d.param=h;var a={};var g={},f=\"https://api.utteranc.es\";g.UTTERANCES_API=f;var r=a&&a.__awaiter||function(e,t,r,n){return new(r||(r=Promise))(function(o,a){function i(e){try{l(n.next(e))}catch(t){a(t)}}function $(e){try{l(n.throw(e))}catch(t){a(t)}}function l(e){e.done?o(e.value):new r(function(t){t(e.value)}).then(i,$)}l((n=n.apply(e,t||[])).next())})},s=a&&a.__generator||function(e,t){var r,n,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:$(0),throw:$(1),return:$(2)},\"function\"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function $(a){return function($){return function(a){if(r)throw new TypeError(\"Generator is already executing.\");for(;i;)try{if(r=1,n&&(o=2&a[0]?n.return:a[0]?n.throw||((o=n.return)&&o.call(n),0):n.next)&&!(o=o.call(n,a[1])).done)return o;switch(n=0,o&&(a=[2&a[0],o.value]),a[0]){case 0:case 1:o=a;break;case 4:return i.label++,{value:a[1],done:!1};case 5:i.label++,n=a[1],a=[0];continue;case 7:a=i.ops.pop(),i.trys.pop();continue;default:if(!(o=(o=i.trys).length>0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]<o[3])){i.label=a[1];break}if(6===a[0]&&i.label<o[1]){i.label=o[1],o=a;break}if(o&&i.label<o[2]){i.label=o[2],i.ops.push(a);break}o[2]&&i.ops.pop(),i.trys.pop();continue;}a=t.call(e,i)}catch($){a=[6,$],n=0}finally{r=o=0}if(5&a[0])throw a[1];return{value:a[0]?a[1]:void 0,done:!0}}([a,$])}}},j={value:null};function i(e){return f+\"/authorize?\"+h({redirect_uri:e})}function p(){return r(this,void 0,Promise,function(){var e,t,r;return s(this,function(n){switch(n.label){case 0:return e=f+\"/token\",[4,fetch(e,{method:\"POST\",mode:\"cors\",credentials:\"include\"})];case 1:return(t=n.sent()).ok?[4,t.json()]:[3,3];case 2:return r=n.sent(),j.value=r,[2,r];case 3:return[2,null];}})})}function v(e){addEventListener(\"click\",function(t){if(t.target instanceof Element&&t.target.closest(\"[sign-in]\")){t.preventDefault();parent.postMessage({type:\"sign-in\"},e)}})}a.token=j,a.getLoginUrl=i,a.loadToken=p,a.listenForSignIn=v;var b=document.currentScript;void 0===b&&(b=document.querySelector(\"script[src^=\\\"https://utteranc.es/client.js\\\"],script[src^=\\\"http://localhost:4000/client.js\\\"]\"));for(var c={},e=0;e<b.attributes.length;e++){var k=b.attributes.item(e);c[k.name.replace(/^data-/,\"\")]=k.value}var l=document.querySelector(\"link[rel='canonical']\");c.url=l?l.href:location.origin+location.pathname+location.search,c.origin=location.origin,c.pathname=location.pathname.length<2?\"index\":location.pathname.substr(1).replace(/\\.\\w+$/,\"\"),c.title=document.title;var m=document.querySelector(\"meta[name='description']\");c.description=m?m.content:\"\";var n=document.querySelector(\"meta[property='og:title'],meta[name='og:title']\");c[\"og:title\"]=n?n.content:\"\",document.head.insertAdjacentHTML(\"afterbegin\",\"<style>\\n    .utterances {\\n      position: relative;\\n      box-sizing: border-box;\\n      width: 100%;\\n      max-width: 760px;\\n      margin-left: auto;\\n      margin-right: auto;\\n    }\\n    .utterances-frame {\\n      position: absolute;\\n      left: 0;\\n      right: 0;\\n      width: 1px;\\n      min-width: 100%;\\n      max-width: 100%;\\n      height: 100%;\\n      border: 0;\\n    }\\n  </style>\");var o=b.src.match(/^https:\\/\\/utteranc\\.es|http:\\/\\/localhost:\\d+/)[0],t=o+\"/utterances.html\";b.insertAdjacentHTML(\"afterend\",\"<div class=\\\"utterances\\\">\\n    <iframe class=\\\"utterances-frame\\\" title=\\\"Comments\\\" scrolling=\\\"no\\\" src=\\\"\"+t+\"?\"+h(c)+\"\\\"></iframe>\\n  </div>\");var u=b.nextElementSibling;b.parentElement.removeChild(b),addEventListener(\"message\",function(t){if(t.origin===o){var r=t.data;r&&\"resize\"===r.type&&r.height?u.style.height=r.height+\"px\":r&&\"sign-in\"===r.type&&(location.href=i(location.href))}});d.__esModule=true;a.__esModule=true;g.__esModule=true;return{\"D53L\":{},\"ieWq\":d,\"5+Ph\":a,\"BYTq\":g};});\n"
  },
  {
    "path": "themes/hermit/theme.toml",
    "content": "name = \"Hermit\"\nlicense = \"MIT\"\nlicenselink = \"https://github.com/Track3/hermit/blob/master/LICENSE\"\ndescription = \"A minimal and fast hugo theme for bloggers.\"\nhomepage = \"https://github.com/Track3/hermit\"\ntags = [\"blog\", \"minimal\", \"dark\", \"responsive\"]\nfeatures = [\n  \"single column\",\n  \"featured image\",\n  \"social icons\",\n  \"google analytics\",\n  \"disqus\"\n]\nmin_version = 0.43\n\n[author]\n  name = \"Track3\"\n  homepage = \"https://www.xxxlbox.com\"\n"
  }
]