Full Code of EDDYCJY/blog for AI

master 5d1b9c17c374 cached
260 files
1.1 MB
509.2k tokens
10 symbols
1 requests
Download .txt
Showing preview only (1,785K chars total). Download the full file or copy to clipboard to get everything.
Repository: EDDYCJY/blog
Branch: master
Commit: 5d1b9c17c374
Files: 260
Total size: 1.1 MB

Directory structure:
gitextract_hs1r1ie2/

├── .gitignore
├── README.md
├── archetypes/
│   ├── default.md
│   └── posts.md
├── config.toml
├── content/
│   ├── about.md
│   ├── go-categories.md
│   ├── k8s-categories.md
│   ├── posts/
│   │   ├── 2020-summary.md
│   │   ├── 2020-top100.md
│   │   ├── 2021-ecug.md
│   │   ├── 2021giac.md
│   │   ├── go/
│   │   │   ├── 117-build.md
│   │   │   ├── 117-errorstack.md
│   │   │   ├── 117-generics.md
│   │   │   ├── 117-module-pruning-lazy.md
│   │   │   ├── 117-performance.md
│   │   │   ├── 118-build-info.md
│   │   │   ├── 118-build.md
│   │   │   ├── 118-constraints.md
│   │   │   ├── 118-cut.md
│   │   │   ├── 118-leader-generics.md
│   │   │   ├── 118-module.md
│   │   │   ├── 4errors.md
│   │   │   ├── again-mutex.md
│   │   │   ├── annotation.md
│   │   │   ├── any.md
│   │   │   ├── class-extends.md
│   │   │   ├── crawler/
│   │   │   │   ├── 2018-03-21-douban-top250.md
│   │   │   │   ├── 2018-04-01-cars.md
│   │   │   │   └── 2018-04-28-go2018.md
│   │   │   ├── defer/
│   │   │   │   └── 2019-05-27-defer.md
│   │   │   ├── delve.md
│   │   │   ├── empty-struct.md
│   │   │   ├── enum.md
│   │   │   ├── func-reload.md
│   │   │   ├── fuzzing.md
│   │   │   ├── gdb.md
│   │   │   ├── generics-apis.md
│   │   │   ├── generics-design.md
│   │   │   ├── generics-proposal.md
│   │   │   ├── gin/
│   │   │   │   ├── 2018-02-10-install.md
│   │   │   │   ├── 2018-02-11-api-01.md
│   │   │   │   ├── 2018-02-12-api-02.md
│   │   │   │   ├── 2018-02-13-api-03.md
│   │   │   │   ├── 2018-02-14-jwt.md
│   │   │   │   ├── 2018-02-15-log.md
│   │   │   │   ├── 2018-03-15-reload-http.md
│   │   │   │   ├── 2018-03-18-swagger.md
│   │   │   │   ├── 2018-03-24-golang-docker.md
│   │   │   │   ├── 2018-03-26-cgo.md
│   │   │   │   ├── 2018-04-15-gorm-callback.md
│   │   │   │   ├── 2018-04-29-cron.md
│   │   │   │   ├── 2018-05-27-config-upload.md
│   │   │   │   ├── 2018-06-02-application-redis.md
│   │   │   │   ├── 2018-06-14-excel.md
│   │   │   │   ├── 2018-07-05-image.md
│   │   │   │   ├── 2018-07-07-font.md
│   │   │   │   ├── 2018-08-26-makefile.md
│   │   │   │   └── 2018-09-01-nginx.md
│   │   │   ├── gmp-why-p.md
│   │   │   ├── go-array-slice.md
│   │   │   ├── go-bootstrap.md
│   │   │   ├── go-bootstrap0.md
│   │   │   ├── go-concurrent-lock.md
│   │   │   ├── go-design.md
│   │   │   ├── go-empty-struct.md
│   │   │   ├── go-error2panic.md
│   │   │   ├── go-errors-boom.md
│   │   │   ├── go-golang.md
│   │   │   ├── go-map-access.md
│   │   │   ├── go-moduels/
│   │   │   │   ├── 2019-09-29-goproxy-cn.md
│   │   │   │   └── 2020-02-28-go-modules.md
│   │   │   ├── go-standards.md
│   │   │   ├── go-tips-defer.md
│   │   │   ├── go-tips-gmp-p.md
│   │   │   ├── go-tips-goroutineid.md
│   │   │   ├── go-tips-goroutineloop.md
│   │   │   ├── go-tips-goroutinenums.md
│   │   │   ├── go-tips-interface.md
│   │   │   ├── go-tips-lenstr.md
│   │   │   ├── go-tips-sturct.md
│   │   │   ├── go-tips-timer-memory.md
│   │   │   ├── go-typeparams-master.md
│   │   │   ├── go-why-path.md
│   │   │   ├── go1.16-1.md
│   │   │   ├── go1.16-2.md
│   │   │   ├── go1.16-3.md
│   │   │   ├── go1.16-mod.md
│   │   │   ├── go11.md
│   │   │   ├── go16-preview.md
│   │   │   ├── go2-errors.md
│   │   │   ├── gophercon2020-errors.md
│   │   │   ├── goroutine-27.md
│   │   │   ├── goroutine-errors.md
│   │   │   ├── goroutine-leak.md
│   │   │   ├── grpc/
│   │   │   │   ├── 2018-09-22-install.md
│   │   │   │   ├── 2018-09-23-client-and-server.md
│   │   │   │   ├── 2018-09-24-stream-client-server.md
│   │   │   │   ├── 2018-10-07-grpc-tls.md
│   │   │   │   ├── 2018-10-08-ca-tls.md
│   │   │   │   ├── 2018-10-10-interceptor.md
│   │   │   │   ├── 2018-10-12-grpc-http.md
│   │   │   │   ├── 2018-10-14-per-rpc-credentials.md
│   │   │   │   ├── 2018-10-16-deadlines.md
│   │   │   │   └── 2018-10-20-zipkin.md
│   │   │   ├── grpc-gateway/
│   │   │   │   ├── 2018-02-23-install.md
│   │   │   │   ├── 2018-02-27-hello-world.md
│   │   │   │   ├── 2018-03-04-swagger.md
│   │   │   │   └── 2019-06-22-grpc-gateway-tls.md
│   │   │   ├── import-cyc.md
│   │   │   ├── import-generics.md
│   │   │   ├── len.md
│   │   │   ├── map/
│   │   │   │   ├── 2019-03-05-map-access.md
│   │   │   │   ├── 2019-03-24-map-assign.md
│   │   │   │   └── 2019-04-07-why-map-no-order.md
│   │   │   ├── map-65.md
│   │   │   ├── map-con.md
│   │   │   ├── map-reset.md
│   │   │   ├── map-slice-concurrency.md
│   │   │   ├── memory-model.md
│   │   │   ├── news-slices-maps.md
│   │   │   ├── news115.md
│   │   │   ├── nil-func.md
│   │   │   ├── panic/
│   │   │   │   └── 2019-05-21-panic-and-recover.md
│   │   │   ├── pkg/
│   │   │   │   ├── 2018-09-28-log.md
│   │   │   │   ├── 2018-12-04-fmt.md
│   │   │   │   └── 2018-12-15-unsafe.md
│   │   │   ├── plugin.md
│   │   │   ├── real-context.md
│   │   │   ├── reflect.md
│   │   │   ├── runtimepark.md
│   │   │   ├── rust-php.md
│   │   │   ├── site-history.md
│   │   │   ├── slice/
│   │   │   │   ├── 2018-12-11-slice.md
│   │   │   │   └── 2019-01-06-why-slice-max.md
│   │   │   ├── slice-discuss.md
│   │   │   ├── slice-leak.md
│   │   │   ├── slice-string-header.md
│   │   │   ├── stop-goroutine.md
│   │   │   ├── struct-pointer.md
│   │   │   ├── switch-type.md
│   │   │   ├── sync-map.md
│   │   │   ├── talk/
│   │   │   │   ├── 2018-03-13-golang-relatively-path.md
│   │   │   │   ├── 2018-05-21-go-fake-useragent.md
│   │   │   │   ├── 2018-06-07-go-redis-protocol.md
│   │   │   │   ├── 2018-11-25-gomock.md
│   │   │   │   ├── 2018-12-26-go-memory-align.md
│   │   │   │   ├── 2019-01-20-control-goroutine.md
│   │   │   │   ├── 2019-02-17-for-loop-json-unmarshal.md
│   │   │   │   ├── 2019-03-31-go-ins.md
│   │   │   │   ├── 2019-05-20-stack-heap.md
│   │   │   │   ├── 2019-06-16-defer-loss.md
│   │   │   │   ├── 2019-06-29-talking-grpc.md
│   │   │   │   ├── 2019-09-07-go1.13-defer.md
│   │   │   │   └── 2019-09-24-why-vsz-large.md
│   │   │   ├── ternary-operator.md
│   │   │   ├── throw.md
│   │   │   ├── tools/
│   │   │   │   ├── 2018-09-15-go-tool-pprof.md
│   │   │   │   ├── 2019-07-12-go-tool-trace.md
│   │   │   │   ├── 2019-08-19-godebug-sched.md
│   │   │   │   └── 2019-09-02-godebug-gc.md
│   │   │   ├── type-after.md
│   │   │   ├── unsafe-pointer.md
│   │   │   ├── value-quote.md
│   │   │   ├── var.md
│   │   │   └── when-gc.md
│   │   ├── go-meetup1017.md
│   │   ├── go-programming-tour-book.md
│   │   ├── kubernetes/
│   │   │   ├── 2020-05-01-install.md
│   │   │   ├── 2020-05-03-deployment.md
│   │   │   └── 2020-05-10-api.md
│   │   ├── microservice/
│   │   │   ├── dismantle.md
│   │   │   ├── flowcontrol-circuitbreaker.md
│   │   │   ├── leaky-token-buckets.md
│   │   │   ├── linkage.md
│   │   │   ├── monitor-alarm.md
│   │   │   ├── standardization.md
│   │   │   ├── tests.md
│   │   │   └── tracing.md
│   │   ├── mq-nodus.md
│   │   ├── prometheus/
│   │   │   ├── 2020-05-16-metrics.md
│   │   │   ├── 2020-05-16-pull.md
│   │   │   └── 2020-05-16-startup.md
│   │   ├── reading/
│   │   │   ├── 2020-04-24-book.md
│   │   │   ├── documentary-of-go.md
│   │   │   ├── programmer-accom-base.md
│   │   │   ├── programmer-compile-link.md
│   │   │   └── programmer-linker.md
│   │   ├── reload-man.md
│   │   ├── where-is-proto.md
│   │   ├── why-container-memory-exceed.md
│   │   ├── why-container-memory-exceed2.md
│   │   └── why-mq.md
│   └── prometheus-categories.md
├── layouts/
│   ├── _default/
│   │   ├── baseof.html
│   │   ├── list.html
│   │   └── single.html
│   ├── index.html
│   ├── partials/
│   │   ├── analytics.html
│   │   ├── comments.html
│   │   ├── favicons.html
│   │   ├── footer.html
│   │   ├── header.html
│   │   ├── social-icons.html
│   │   ├── structured-data.html
│   │   └── svg.html
│   └── posts/
│       ├── rss.xml
│       └── single.html
├── resources/
│   └── _gen/
│       └── assets/
│           └── scss/
│               └── scss/
│                   ├── style.scss_c16d144eee185fbddd582cd5e25a4fae.content
│                   └── style.scss_c16d144eee185fbddd582cd5e25a4fae.json
├── static/
│   └── css/
│       └── styles.css
└── themes/
    └── hermit/
        ├── .editorconfig
        ├── .gitattributes
        ├── LICENSE
        ├── README.md
        ├── archetypes/
        │   ├── default.md
        │   └── posts.md
        ├── assets/
        │   ├── js/
        │   │   └── main.js
        │   └── scss/
        │       ├── _animate.scss
        │       ├── _normalize.scss
        │       ├── _predefined.scss
        │       ├── _syntax.scss
        │       └── style.scss
        ├── exampleSite/
        │   ├── config.toml
        │   └── content/
        │       ├── about-hugo.md
        │       └── posts/
        │           ├── creating-a-new-theme.md
        │           ├── goisforlovers.md
        │           ├── hugoisforlovers.md
        │           ├── migrate-from-jekyll.md
        │           ├── post-with-featured-image.md
        │           ├── the-figure-shortcode.md
        │           └── typography.md
        ├── i18n/
        │   ├── en.toml
        │   ├── it.toml
        │   └── zh-hans.toml
        ├── layouts/
        │   ├── 404.html
        │   ├── _default/
        │   │   ├── baseof.html
        │   │   ├── list.html
        │   │   └── single.html
        │   ├── index.html
        │   ├── partials/
        │   │   ├── analytics.html
        │   │   ├── favicons.html
        │   │   ├── footer.html
        │   │   ├── header.html
        │   │   ├── social-icons.html
        │   │   ├── structured-data.html
        │   │   └── svg.html
        │   ├── post/
        │   │   ├── rss.xml
        │   │   └── single.html
        │   └── posts/
        │       ├── rss.xml
        │       └── single.html
        ├── resources/
        │   └── _gen/
        │       └── assets/
        │           ├── js/
        │           │   └── js/
        │           │       ├── main.js_d11fe7b62c27961c87ecd0f2490357b9.content
        │           │       └── main.js_d11fe7b62c27961c87ecd0f2490357b9.json
        │           └── scss/
        │               └── scss/
        │                   ├── style.scss_c16d144eee185fbddd582cd5e25a4fae.content
        │                   └── style.scss_c16d144eee185fbddd582cd5e25a4fae.json
        ├── static/
        │   ├── browserconfig.xml
        │   ├── site.webmanifest
        │   └── utteranc.js
        └── theme.toml

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
.idea/
.DS_Store
_book/
node_modules/
public/


================================================
FILE: README.md
================================================
# 煎鱼的迷之博客

写写代码,喝喝茶,搞搞 Go,一起吧,这是我的项目地址:https://github.com/eddycjy/blog

## 在线阅读

- https://eddycjy.com/

## 我的公众号

所有文章和最新进度,请关注:

![image](https://image.eddycjy.com/7074be90379a121746146bc4229819f8.jpg)

## ?

如果有任何疑问或错误,欢迎在 issues 进行提问或给予修正意见

如果喜欢或对你有所帮助,欢迎 Star,对作者是一种鼓励和推进 😀

## License

所有文章采用[知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议](https://creativecommons.org/licenses/by-nc-sa/3.0/cn/)进行许可


================================================
FILE: archetypes/default.md
================================================
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
comments: false
images:
---



================================================
FILE: archetypes/posts.md
================================================
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
toc: false
images:
tags: 
  - untagged
---



================================================
FILE: config.toml
================================================
baseURL = "https://eddycjy.com"
languageCode = "zh-hans"
defaultContentLanguage = "en"
title = "煎鱼"
theme = "hermit"
# enableGitInfo = true
pygmentsCodefences  = true
pygmentsUseClasses  = true
# hasCJKLanguage = true  # If Chinese/Japanese/Korean is your main content language, enable this to make wordCount works right.
rssLimit = 10  # Maximum number of items in the RSS feed.
copyright = "This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License." # This message is only used by the RSS template.
enableEmoji = true  # Shorthand emojis in content files - https://gohugo.io/functions/emojify/
googleAnalytics = "UA-166045776-1"
# disqusShortname = "yourdiscussshortname"
summarylength = 30

[author]
  name = "煎鱼"

[blackfriday]
  # hrefTargetBlank = true
  # noreferrerLinks = true
  # nofollowLinks = true

[taxonomies]
  tag = "tags"
  # Categories are disabled by default.

[params]
  since = "2018"
  toc = true

  dateform        = "Jan 2, 2006"
  dateformShort   = "Jan 2"
  dateformNum     = "2006-01-02"
  dateformNumTime = "2006-01-02 15:04 -0700"

  # Metadata mostly used in document's head
  description = "煎鱼,博客,go,golang,源码分析,系列教程"
  # images = [""]
  themeColor = "#494f5c"

  mainSections = ["posts"]

  homeSubtitle = "Metrics, Tracing, Logging"
  footerCopyright = ' &#183; <a href="http://www.beian.miit.gov.cn/">粤ICP备19076352号</a>'
  # bgImg = ""  # Homepage background-image URL

  # Prefix of link to the git commit detail page. GitInfo must be enabled.
  gitUrl = "https://github.com/eddycjy/blog/commit/"

  # Toggling this option needs to rebuild SCSS, requires Hugo extended version
  justifyContent = false  # Set "text-align: justify" to `.content`.

  relatedPosts = false  # Add a related content section to all single posts page

  code_copy_button = true # Turn on/off the code-copy-button for code-fields
  
  # Add custom css
  # customCSS = ["css/foo.css", "css/bar.css"]
  customCSS = ["https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css", "css/styles.css"]

  # Social Icons
  # Check https://github.com/Track3/hermit#social-icons for more info.


  [[params.social]]
    name = "github"
    url = "https://github.com/eddycjy"

[menu]

  [[menu.main]]
    name = "文章"
    url = "posts/"
    weight = 10

  [[menu.main]]
    name = "标签"
    url = "tags/"
    weight = 10

  [[menu.main]]
    name = "关于"
    url = "about/"
    weight = 20


  [[menu.nav]]
    name = "文章"
    url = "posts/"
    weight = 10

  [[menu.nav]]
    name = "Go语言编程之旅"
    url = "https://golang2.eddycjy.com/"
    weight = 11

  [[menu.nav]]
    name = "Go语言进阶之旅"
    url = "https://golang1.eddycjy.com/"
    weight = 11

  [[menu.nav]]
    name = "Go语言设计哲学"
    url = "https://golang3.eddycjy.com/"
    weight = 11

  [[menu.nav]]
    name = "Go语言入门系列"
    url = "go-categories/"
    weight = 12

  [[menu.nav]]
    name = "Kubernetes系列"
    url = "k8s-categories/"
    weight = 13





================================================
FILE: content/about.md
================================================
---
title: "关于"
date: "2020-03-15"
---

你好,我是煎鱼,最近沉迷于 Go、Kubernetes、Prometheus 这一生态圈子里的东西(努力学习中)。在工作中,目前主要负责公司的基础架构/组件的建设和业务团队推广, 欢迎大家来找我玩和讨论问题,提建议也随时欢迎。

### 有趣的时间节点:

1. 高一:参加俱乐部,捣鼓 Photoshop 为校学生会做做平面设计。
2. 高一:参加市/省/国赛,捣鼓前端,那是一个 table+css+js 的时代,dw 还很火热。
3. 大一:参加校组织,捣鼓 PHP,还为校组织开发了微信公众号(当时公众号刚出来),结果不错,市场占有率极高。
4. 工作三年(加上实习的时间):在那一天,公司 CTO 突然喊我私聊,希望我去全新的业务组(哥伦布组),我一口答应下来,当时还不知道 Go 语言是什么,自此从主力语言从 PHP 转为了 Go,并写了很多 Go 语言相关的[系列](/category)。
5. 工作四年:2019 年年尾正式转到了公司的架构组,在 2020 年 7 月正式出版 Go 语言图书《[Go 语言编程之旅](https://item.jd.com/12685249.html)》,并在 2020 年的 GopherChina 拿到了 GOP 的荣誉称号。

### 联系方式:

- Github:https://github.com/eddycjy/blog
- 公众号:脑子进煎鱼了
- 邮箱:eddycjy@gmail.com

### 我的公众号

平时喜欢分享 Go 语言、微服务架构和奇怪的系统设计,欢迎关注我的公众号:

![image](https://image.eddycjy.com/7074be90379a121746146bc4229819f8.jpg)




================================================
FILE: content/go-categories.md
================================================
---
title: "《跟煎鱼学 Go》"
date: "2020-04-21"
---

我不怎么喜欢左写写,右写写,因此总是在不知不觉中写了不少的系列教程,希望对你有所帮助,若要催更请关注公众号后私聊催。

- 一:**HTTP 应用**
    - [「连载一」Go 介绍与环境安装](/posts/go/gin/2018-02-10-install/)
    - [「连载二」Gin搭建Blog API's (一)](/posts/go/gin/2018-02-11-api-01/)
    - [「连载三」Gin搭建Blog API's (二)](/posts/go/gin/2018-02-12-api-02/)
    - [「连载四」Gin搭建Blog API's (三)](/posts/go/gin/2018-02-13-api-03/)
    - [「连载五」使用 JWT 进行身份校验](/posts/go/gin/2018-02-14-jwt/)
    - [「连载六」编写一个简单的文件日志](/posts/go/gin/2018-02-15-log/)
    - [「连载七」优雅的重启服务](/posts/go/gin/2018-03-15-reload-http/)
    - [「连载八」为它加上Swagger](/posts/go/gin/2018-03-18-swagger/)
    - [「连载九」将Golang应用部署到Docker](/posts/go/gin/2018-03-24-golang-docker/)
    - [「连载十」定制 GORM Callbacks](/posts/go/gin/2018-04-15-gorm-callback/)
    - [「连载十一」Cron定时任务](/posts/go/gin/2018-04-29-cron/)
    - [「连载十二」优化配置结构及实现图片上传](/posts/go/gin/2018-05-27-config-upload/)
    - [「连载十三」优化你的应用结构和实现Redis缓存](/posts/go/gin/2018-06-02-application-redis/)
    - [「连载十四」实现导出、导入 Excel](/posts/go/gin/2018-06-14-excel/)
    - [「连载十五」生成二维码、合并海报](/posts/go/gin/2018-07-05-image/)
    - [「连载十六」在图片上绘制文字](/posts/go/gin/2018-07-07-font/)
    - [「连载十七」用Nginx部署Go应用](/posts/go/gin/2018-09-01-nginx/)
    - [「番外」Golang 交叉编译](/posts/go/gin/2018-03-26-cgo/)
    - [「番外」请入门 Makefile](/posts/go/gin/2018-08-26-makefile/)
- 二:**gRPC 应用**
    - [「连载一」gRPC及相关介绍](/posts/go/grpc/2018-09-22-install/)
    - [「连载二」gRPC Client and Server](/posts/go/grpc/2018-09-23-client-and-server/)
    - [「连载三」gRPC Streaming, Client and Server](/posts/go/grpc/2018-09-24-stream-client-server/)
    - [「连载四」TLS 证书认证](/posts/go/grpc/2018-10-07-grpc-tls/)
    - [「连载五」基于 CA 的 TLS 证书认证](/posts/go/grpc/2018-10-08-ca-tls/)
    - [「连载六」Unary and Stream interceptor](/posts/go/grpc/2018-10-10-interceptor/)
    - [「连载七」让你的服务同时提供 HTTP 接口](/posts/go/grpc/2018-10-12-grpc-http/)
    - [「连载八」对 RPC 方法做自定义认证](/posts/go/grpc/2018-10-14-per-rpc-credentials/)
    - [「连载九」gRPC Deadlines](/posts/go/grpc/2018-10-16-deadlines/)
    - [「连载十」分布式链路追踪 gRPC + Opentracing + Zipkin](/posts/go/grpc/2018-10-20-zipkin/)
- 三:**grpc+grpc-gateway 应用**
    - [「连载一」gRPC介绍与环境安装](/posts/go/grpc-gateway/2018-02-23-install/)
    - [「连载二」Hello World](/posts/go/grpc-gateway/2018-02-27-hello-world/)
    - [「连载三」Swagger了解一下](/posts/go/grpc-gateway/2018-03-04-swagger/)
    - [「连载四」gRPC+gRPC Gateway 能不能不用证书?](/posts/go/grpc-gateway/2019-06-22-grpc-gateway-tls/)

### 我的公众号

平时喜欢分享 Go 语言、微服务架构和奇怪的系统设计,欢迎关注我的公众号:

![image](https://image.eddycjy.com/7074be90379a121746146bc4229819f8.jpg)

================================================
FILE: content/k8s-categories.md
================================================
---
title: "《跟煎鱼学 Kubernetes/Prometheus》"
date: "2020-05-16"
---

请注意,这不是成品,随时可能会对以前的章节进行修改。那么为什么要放出来呢,当然是为了催更自己。

1. [Kubernetes 本地快速启动(基于 Docker)](/posts/kubernetes/2020-05-01-install)
2. [在 Kubernetes 中部署应用程序](/posts/kubernetes/2020-05-03-deployment)
3. [使用 Go 程序调用 Kubernetes API](/posts/kubernetes/2020-05-10-api)


1. [Prometheus 快速入门](/posts/prometheus/2020-05-16-startup)
2. [Prometheus 四大度量指标的了解和应用](/posts/prometheus/2020-05-16-metrics)
3. [使用 Prometheus 对 Go 程序进行指标采集](/posts/prometheus/2020-05-16-pull)

### 我的公众号

平时喜欢分享 Go 语言、微服务架构和奇怪的系统设计,欢迎关注我的公众号:

![image](https://image.eddycjy.com/7074be90379a121746146bc4229819f8.jpg)

================================================
FILE: content/posts/2020-summary.md
================================================
---
title: "拖更的 2020 年不一样"
date: 2020-12-31T21:29:55+08:00
images:
tags: 
  - 总结
---

大家好,我是煎鱼。

万万没想到...想着写 2020 年总结,结果就到了 2021 年,不愧是只有 7s 记忆的博主 😂。

2020 年并不简单,这也是第一次在公开场合写个人向,并且还是年终总结,感慨颇多。

## 出书

今年(2020年)上半年几乎没有更新博客文章,当时还一口气退了一大堆技术交流的微信群。当时有不少朋友来咨询我怎么了,后面的事大家也就知道了。

蛰伏了将近 9 个月,出了人生第一本图书(简体+繁体):

![](https://image.eddycjy.com/56c805ff0e8c6134e845d5da99d4ab0b.jpg)

本书已印刷三次,和编辑沟通了几次,现在 Go 语言还算小众(与 Java、Python 相比较),销量算是挺不错的了。

有一块比较遗憾,在初印有几个比较致命的印刷问题。后续在第二次印刷中解决了。此次只能说第一次写书经验不足,编辑也是,下次一定。

另外当时出书时与曹大聊过几次,他提到的出书后会遇到的所有烦恼我大多都遇到了,现实太真实,出书很理想。

其次写书并不赚钱,不要见面就问。更没有因此改变我的初心,依然是热爱分享知识的煎鱼。

未曾想过的第一个里程碑完成。

## 博客/公众号

后半年就开始陆续恢复博客的写作了,2020 年博客上共有 32 篇技术文章,加上图书的稿子一年总共写了 70+ 篇文章。

2019 年只写了 19 篇。相比较而言,总产出还算可以:

![](https://image.eddycjy.com/4be8f0069f8b79388c1fe0853ab9a534.jpg)

但理论上还可以更高,因为在写完书后,由于之前过于高度集中,导致下半年出现了几个月的真空休息期。更深刻的认识到了劳逸结合,适当调节非常重要。

同时公众号也在年尾终于往前走了一步,开始接了一些推广,虽然钱不多。但更多的还是激励自己,倒逼自己更多的输入和输出。

最近有很多小伙伴也发现了我公众号的更新频率变高了,这是相辅相成的。

知识吸到就是你的。

## 社区

今年因为分享知识接触到了很多人,又因此认识到了更多的人。其中不乏各行各领域的优秀小伙伴们,间接的给我人生发展上提供了极大的建议和反馈,影响了我做许多事情的决策和思考方向。

同时在 Gopher China 2020 中拿到了 GOP(Go 领域最具观点专家)的荣誉:

![](https://image.eddycjy.com/6106728f2f69a19d5584623326c97363.jpeg)

很可惜的是,当时正在在准备公司内答辩的 PPT 和生病中,因此没能去现场,很遗憾。

不过在线下依然感受到了各路 Gopher 们的反馈和关注,很感谢 GoCN 的认可。

未曾想过的第二个里程碑完成。

## 工作

感觉自己变化太大了,发现今年思考事情的角度、广度、深度以及关联性已然和上一年不一样。

![](https://image.eddycjy.com/82d6bd939ce594c3ad219722380f5957.jpg)

经提醒也发现已入职三年半,已经到了传统定义的 “职业倦怠期” 的时间阶段,如何更好的保持自己的好奇和发展是一个要考虑的大命题。

自己评判自己的工作是很难的,继续努力是大方向。

## 读书

今年看的书挺多,涉及了计算机、产品设计、用户增长、金融理财等各个领域。

阅读时间主要是集中在午睡时间和晚上睡前看。

公司桌面上的书也是越来越多:

![](https://image.eddycjy.com/b114cdba7dc15e5175dedb83d5aa82b9.jpeg)

强烈建议阅读 DDIA,好书。

## 总结

今年经历了许多魔幻的事情,好彩大多都挺了过来并解决了。

同时也认识到了许多的社区朋友,在北京也算吃过烤鸭,冻肿过手指的人了。

最后,我时常能在私聊或朋友圈听到来自读者们的反馈:

![](https://image.eddycjy.com/c23325dd64d5eee7410647cc0e472b94.jpeg)

很感谢大家喜欢我的文字,能从煎鱼身上吸取到自己想要的知识。

希望 2021 年咱们继续努力,把 flag 都给立好了。

![image](https://image.eddycjy.com/0618aba7eb620d6541e2f02154a4ab19.jpeg)





================================================
FILE: content/posts/2020-top100.md
================================================
---
title: "吐血整理 | 快速了解全球软件案例(Top100)"
date: 2020-12-22T21:26:44+08:00
toc: true
images:
tags: 
  - top100
---

前几天,煎鱼去了趟北京,参加了为期三天的全球软件案例研究峰会(TOP 100)。

同时记了一些笔记,整理后分享出来,希望对大家有所帮助,拓展眼界非常重要。

![](https://image.eddycjy.com/a1be9ee345e57e1299f74a3d9e336d13.jpeg)

内容比较多(已经精简过),大家可以挑自己感兴趣的学习,建议三连。

一级目录如下:

1. 百度内部业务 ServieMesh 实践。
2. 云原生开发平台在腾讯游戏运营中的实践。
3. 快狗打车可持续交付实践。
4. 网易数帆从微服务框架到服务网格架构平滑演进及最佳实践。
5. 不破不立:企业级研发效能提升的创新实践。
6. 自如云原生落地最佳实践。
7. 研发效能度量的误区、体系化实践和效能提升案例。
8. 京东 BDP 的全域监控、管控平台搭建实践。
9. 构建发布效率从10分钟到秒级的提升 - 云原生下编程方式的探索和实践。
10. 全面监控体系建设及智能监控的探索实践。
11. 低代码技术在贝壳的实践。

## 百度内部业务 ServieMesh 实践

本场演讲内容主要为微服务到服务网格的过程。其中涉及百度在异构场景下的一系列演进和适配操作。

同时也能得知百度也是自己做了个 bmesh,自此概括几乎全一线互联网大厂,均为自研(或结合)ServieMesh。

### 整体演进

#### 1.0 时代

第一代微服务架构(1.0时代),主体是基于 SDK/开发框架的微服务治理体系。

![](https://image.eddycjy.com/83b5009e040969ee7b60362ad7426573.jpeg)

主要存在以下问题:
- 开发成本高:异构语言的问题,每个语言都要重新开发。
- 升级成本高:框架上线以来业务。
- 管理成本高:服务拓扑和治理没有统一管理(需要治理)。

#### 2.0时代

第二代微服务架构(2.0时代),主体是基于微服务框架到服务网格,也就是把服务治理能力抽取出来,作为一个进程(sidecar),与业务逻辑解耦。

![](https://image.eddycjy.com/ea571676ce9b75b0730a5d56350ae93e.jpeg)

从概念上来讲,主要分为以下两类:
- 数据平面
    - 与业务无关。
    - 与语言无关。
    - 独立的升级(直接升级 sidecar 的进程),能够解耦。
- 控制平面
    - 能够统一的管控。

### 百度现状

各语言在内部平分秋色,没有谁强谁弱。各自都有框架,且有可能有多个框架,可自行脑补一下在公司内部一种语言有 N 种框架,且多种协议(含私有协议)的情况:

![](https://image.eddycjy.com/182845aceb39c9e413e28fd549058cf8.jpeg)

存在以下问题:
- 多个语言开发。
- 多个框架改造。
- 多个通讯协议。

简单来讲就是 “异构系统”,传统的微服务框架无法满足了,成本非常高,甚至不可行。只能通过服务网关的方式来实现微服务治理。

### 上服务网格的困难

- 改造成本:
    - 各种内部框架的支持。
    - 各种通讯协议的支持。
- 性能问题:
    - 通讯延迟,有些敏感业务无法接受,例如:搜索。
    - 资源开源,数十万机器,每个服务都加边车,成本极大。
- 规模问题:
    - 随着接入的节点越多,规模越大,控制平面下发配置的速度越慢,甚至无法工作。
    
### 百度的解决方案(整体架构)

![](https://image.eddycjy.com/9679ccb5a92f650b83fcf29e0a6a6775.jpeg)

在开源的技术栈上进行了自己的拓展,用的是 istio+envoy。

并且在 istio 之上做了一层抽象,实现了 Mesh 的管理界面。

另外实际上在调参时,是需要很多实际经验的,例如:超时的值到底怎么配,因此又基于此在平台上提供了智能调参系统。

与目前所知的一线互联网大厂改造比较类似,区别在于还做了很多自有平台。

### 遇到的问题(大规模落地三步走)

![](https://image.eddycjy.com/ddf9c9a45551e218c4018d5c53e9f6bb.jpeg)

#### 解决接入问题
- 流量劫持方案:
    - 社区自有的方案无法修改服务治理的参数(例如:导致原有的超时重试变成了对边车重试)。
    - iptables 存在性能问题。
    - 无法在 mesh 和 非 mesh 下切换:不能完全信任,挂掉后怎么处理,流量怎么切。解决方案是劫持服务发现的过程(边车劫持的是的服务地址),就能够解决流量劫持的自由问题。
- 自有协议代理:有二十多种协议,解决方案是抽了一层公共 Proxy,实现 codec 就可以了。但也没法解决全部问题,因为 Mesh 对协议有要求,例如加字段。但有的老协议是没法扩展的。最后解决方案是做协议转换,业务代码就不需要修改了。
- 多框架支持:网格对框架有基本要求(治理行为可拓展,透传 Trace 信息),但有的老框架没法支持。通过探针逻辑修改框架行为(探针感知逻辑,修改框架行为)。

#### 解决性能问题
- 网络性能优化:
    - envoy 的最大问题是性能不怎么好,拓展性是第一,性能是他的第二位。
    - envoy 的多线程的竞争是非常厉害的。最后是采取 envoy+brpc 的融合方案(难度很大,组件替换,逐步替换)解决,整体延迟和 CPU 使用率明显低于原生版本。做了开关,能在原生和融合后版本切换。
- 网络控制面优化:
    - istio 原生的配置是全量下发的,非常不科学。
    - 改造为通过获取关系按需下发。服务发现改为由控制面下发到数据面。简单来讲就是原生 istio 控制面在大规模下有很多问题,为此改了很多逻辑。
- 网络性能优化:
    - istio 原生为 both side 模式,要转换 2 次,损耗 2 次网络开销,2次性能开源,内部认为不可接受。改为 client side 的模式(架构上的折中,有的业务很敏感,不推荐为主流方式)。

#### 享受网格的红利
- 流量可操控:
    - 所有的流量都在自己的手中,可以去做很多事情。例如做很多动态的事情,全局流控,根据延迟分配请求到不同的下游等,带来的新的问题就是太多配置,太吃经验,因此做了哥全局智能调参。
    - 结合高级治理,配合自愈和剔除,可用性的修复时间大大的提高,提高了可用性。
- 服务可观测:
    - 结合框架透传 Trace 信息,Sidecar 负责上报监控,即可形成追踪。
- 自动止损
    - 结合监控平台采集实例、集群信息,发现异常,上报给控制平面。然后就可以屏蔽掉某个实例、集群。实现自动止损。
- 异常注入
    - 混沌平台负责配置、评估,服务网格负责实施异常注入。
- 容量管理
    - 传统需要做压测,对整个系统进行很长时间的压测(想压到极限,要构造大量的数据和流量),非常麻烦。
    - 通过服务网格可以在局部构造出极限的压力。

### 落地情况

百度目前正在逐渐落地,已接入实例数万。通过高级的服务治理(需要自实现)带来了很大的收益。

但需要注意,如果只是单纯接入服务网格,并不能带来上述所说的收益。他的收益更多的是面向后续的一些高级治理等高级场景。

### 总结

- 服务网格不是微服务治理的银弹:
    - 完全无侵入支持所有空间和治理策略的 Mesh 方案是不存在的。
    - 大规模落地一定会涉及已有治理的兼容升级和改造。
- 服务网格确实实现了业务逻辑和服务治理架构的解耦。
- 服务网格的开始是最难的,落地服务网格会非常困难和艰辛。

### QA

- 第一个:
    - 新产品可以上服务网格,但要有一个现成成熟的服务网格,自研工作量是非常之大的。
- 第二个:
    - 和开源社区结合的问题,会不定期更新 envoy,istio 的版本。
    - 服务网格不能只看节省的成本,如果只是说框架的治理,那是非常小的,最大的收益是把所有的流量都汇总到一起后收益非常大。网格一开始会非常痛苦,需要把流量真正的拦截上去。
    - 边车的定位是服务于服务之间的通讯代理,与业务逻辑比较紧耦合是不适合放进去的。
    - 推广的目标不是全站覆盖,核心链路上到就可以,因为有的应用对这类没什么诉求。
- 第三个:
    - 是否 SSL 看企业场景。
- 第四个:
    - bmesh 可以从全局角度监控,现有的监控模式可能你会是自己有问题。


## 云原生开发平台在腾讯游戏运营中的实践

- 平台的期望:提高研发效能,业务落地成长。

- 业务背景:营销活动,上线活动很多是不报备的,因此伸缩性要求高,日活很容易上千万。活动多(每天新增 50 个以上),数量量幅度大,服务质量要求高。

- 实际成效:这么大的规模,现在只需要 3 个 专职运维,晚上 6 点就下班了。

### 本质需求

- 频繁发布:版本发布更新。
- 动态伸缩:数据量大。
- 持续高可用:经常变更。

### 运维的任务

![](https://image.eddycjy.com/aba4c12c0307ac56aedf5e7b2dadf69b.jpeg)

以往都是开发提交给运维的,问题是有极多个开发对接有限的运维。面临的问题:

- 面临的环境,部署的东西,都是不是固定的。
- 开发需要转移知识给运维。
- 实际经常会出现意外情况。
- 对部署的风险:运维本能排斥变化。

呈现的结果是运维忙于救火,开发提个需求,就只能排队,线上总不稳定。

### 解决办法

- 持续交付:
    - 控制产品发布节奏,需求尽快上线,不积压。
- 打造部署安全网:
    - 微服务、并行部署,完善的监控。
- 实现可重复性:
    - 控制环境和部署的构建,自动化, 保证输入输出的一样的。
- 变化是必然的:
    - 以故障是常态去设计。

### 碎片的解决方案

要解决上述所提到的问题,基本目前在业界都有基本的解决方案,但其都是 “碎片化” 的,如下:

![](https://image.eddycjy.com/4f84f02beb6427bc9a6d8d09d2376746.jpeg)

“碎片化” 指的是不同的组件都分别是解决特定的问题。这就导致了下述问题:

- 各方面的学习成本高。
- 系统自动化程度低。
- 有经验开发人员有限:
    - 人员招聘成本高,限制发展规模。
- 无生命周期:
    - 整体的上线到下线没有管理。

### 云原生开发平台的设计

真正的完整解决方案就是做一个 “云原生开发平台”,提供一整套的服务来支持软件的开发和运维,实现各种云原生软件的诉求。

从设计角度上来讲:

![](https://image.eddycjy.com/02519bfb266773f243fdef49420313d1.jpeg)

运维不暴露太多的基础建设,只是开放角度去做。开发人员只需要关注应用程序,不需要关注底层的基础设施。

不需要让业务关注你用的是 k8s、envoy、gitlab 什么基础设施,不用去学习一些特定的知识体系。

### 资源评估中心化的运维

![](https://image.eddycjy.com/549cfc258b5b09317e51edf0d640cf8d.jpeg)

开发需要申请资源,运维需要评估,分配,再审核执行。最快是小时级,是平台的瓶颈。

![](https://image.eddycjy.com/f7f163af78812e58c4d3c47b4e396ae6.jpeg)

解决方案是把资源分片,实现团队自治,开发就可能承担了部分运维以往的工作。

此时又会带来由于运维部分工作转移到开发后的成本问题,这部分就需要由平台来解决,提供下述全流程:

1. 智能的容量评估
2. 自动化提单/审批
3. 自动下线(通过日志、性能、调用来识别)
4. 自治并释放资源。

最终的成效是目前所有的审批都是业务团队自己去做,运维只负责供应资源。

#### 微服务下的依赖问题

思考:服务 A 流量上涨,依赖的服务如何扩容?

![](https://image.eddycjy.com/d077e4317cde1e70737c7d5616929159.jpeg)

利用 istio 做全链路分析,实现全自动化,精准识别出入流量,计算差异部分,就能知道改变的服务链路和所需放大的倍数。

### 实现可重复性

应用跑不起来,肯定是有东西改变了:

- 环境控制:镜像。
- 构件控制:全自动化流水线,例如:代码版本、编译命令等。
- 运行时配置:提供配置管理,实现版本控制。

控制可运行时容器,就可以做一系列工作。系统提供默认安全,应用也就安全。

### 变化是必然的

面向故障设计:
- 多可用区可用(异地多活?).
- 主机故障自愈/剔除能力。
- 实例守护剔除能力。
- 配置恢复能力。

### 开发阶段如何提升效率

做了个服务市场,业务应用。沉淀利用现有的能力。

### 总结

基础设施,团队自治,统一且自动化的交付。开发运维一体化,转移到开发的成本,要用平台来解决。

### QA

- 第一个:
    - 故障定位,看基础设施,若是软件业务逻辑,软件业务逻辑是通过平台来提供工具来支持业务排查,帮助定位,大部分还是靠业务团队做的,若是基础设施基本都是大面积的。
- 第二个
    - 版本一致性,必然存在,发布有先后顺序,可以走蓝绿,业务逻辑也要做一些兼容处理。
- 第三个
    - 去抖动下发的策略,会持续监听 IP 并收集,配置下发有最小的间隔,例如十秒间隔的统一下发(存在一定延迟)。又或是到了一定数量级也会下发。不会来一发触发一条。
- 第四个
    - 开发要对利用率关注,压测,让平台上能够自动化去帮他实现,帮助他认识,自动生成出建议的数量和规模。同时服务的流量是分高峰,平峰的,平台要有提供自动扩缩容的机制(例如:也可以做定时策略)。支持自定义指标的扩缩容,要有一个超卖的策略。
- 第五个
    - 研发和运维的分界线,版本上线的告警目前都是由业务自己做的,代码是业务自己写的,暗含的逻辑运维必然不知道。开发要做全生命周期的跟踪。
    - 统一网关不包含业务逻辑,更多的是支持公有云私有云,私有协议,一些登陆。业务网关更多的是跟业务相关的。
- 第六个
    - 长连接如何做无损发布,超过 30s 后 ipvs 感知不到连接断开,流量还是会分过来。存在局限性,要感知协议类型(内部做了针对性的自动化判定),判断流量是否结束,若结束则转发到新的。四层还是需要业务自己实现的。

## 网易数帆从微服务框架到服务网格架构平滑演进及最佳实践

介绍微服务相关的理念和服务网格的演进,前半部分非常高关注率,听众大量拍照。

后半部分主要是网易轻舟的产品和技术介绍,主要是偏向 Java 方向,因此主要是在思想上进行参考,有一定的价值意义。

从 2017 年开始进行 ServieMesh 的研究,不断进行打磨,直到 2020 年正式释出网易轻舟这一个技术产品。

### 为什么要从微服务框架演进至服务网关

在 2012 年正式提出微服务,介绍了微服务的发展史:

![](https://image.eddycjy.com/e02b5f50d064103233b3adee3b96a510.jpeg)

- 1.0时代:2011-2017,以框架为代表的微服务框架。
- 2.0时代:2017-至今,以服务网关为代表,业务与治理解耦。

### 微服务框架存在的问题

1. 适用范围。
2. 升级成本。
3. 侵入性。
4. 治理能力有限。
5. 框架负担。
6. 架构演进与云原生相冲突(注册中心部分)。

### 服务网格的定义和优势

服务网格定义为服务间通讯的基础设施层,通过轻量的网络代理进行拦截和处理。堪称异构语言的救星。

### 服务网格的技术升级改造成本

思考:我真的需要服务网格吗?

![](https://image.eddycjy.com/dcf816fe7054b0a74c353321030b73ce.jpeg)

1. 业务诉求是否只能用服务网格解决?
2. 业务现状是否满足网格接入条件?
3. 业务团队是否能够驾驭得了服务网格?
4. 是否有开发配套的基础设施?

### 演进的成本问题

ROI 策略,做成本分析。

![](https://image.eddycjy.com/b06e00b230527202d3420d52eb4760e1.jpeg)

### 接入诉求

![](https://image.eddycjy.com/74cde9d40b00ceea6d10db88ce9a1512.jpeg)

要关注业务期望收益是什么?

### 微服务框架和服务网格共存

- 微服务框架:面向应用内的服务治理。
- 服务网格:面向服务间的服务治理。

微服务框架和服务网格两者存在重合的区域,但又无法完全替代:

![](https://image.eddycjy.com/46bf4496d08a7627f25dbf5588cfbfd4.jpeg)

网易轻舟的平滑演进主要是针对 Java 系,对此 JavaAgent 做了一些调整(如下业务迁移),以此来实现平滑演进。

### 业务迁移

微服务框架 =》 融合(过度)期 :存在流量管理的能力冲突 =》 服务网格

逐步分离,缓慢实现技术升级。方案分为:
- 通过 JavaAgent 迁移。
- 通过网关做灰度流量迁移。

### 服务网格与真实的使用场景差异

设计上比较理想化,很难直接拿来给业务直接用,业务真正使用都要做定制化改造:

![](https://image.eddycjy.com/8478ec28fa40c5f169aa51aa9b461b1d.jpeg)

为了解决这些问题,网易轻舟做了以下增强:
- 通讯协议增强支持(例如:Dubbo、Thrift)。
- sidecar 的管理问题:每次的升级问题,社区方案每次都要重启 pod。网易轻舟是实现了热升级。
- 多场景的限流方案:社区方案的性能常被人吐槽,并且场景支持不充足。
- 基于服务网格的能力拓展:例如:监控。

提供微服务框架和服务网格的一体化的控制台,简单来讲就是通过平台将用户的业务改造成本和学习成本和运维成本大幅度降低了。

因此平台化是解决服务网格 “成本” 的一个出路。

## 未来

- 中间件 Mesh
- 排障体系建设
- 故障演练体系

## 总结

认为落地过程一定是曲折的,服务网格未来一定会是光明的。

细节会是魔鬼。

## QA

- 第一个:
    - 微服务框架到服务网格的最大难点:解决服务发现的过度问题,如何把注册中心打通。
- 第二个:
    - 2017 年开始关注投入,不断打磨,到 2020 年才出现网易轻舟。因为官方也不断在演进,他们也在不断演进。
- 第三个:
    - 中间件 Mesh,性能影响?目前主要还是偏向监测,访问成功,错误率,流量,使用的,偏向监控方面的设计。
- 第四个:
    - 现在遇到的业务诉求是否一定要用服务网格去解决?以及内部是否认同服务网格?


## 不破不立:企业级研发效能提升的创新实践

会场全场站满,讲师很有趣,经历丰厚,是研发效能的出品人。其介绍了许多研发效能和度量相关的知识和理念。

同时驳斥了现在业界很多的一些基本的理念,让人深思。

### 为什么研发效能火了

为什么以前研发效能都没法塞满一个会场,为什么现在出现如此盛况?总的来讲是时代变了,商业逻辑变了,大家对研发效能有了更大的理解:

![](https://image.eddycjy.com/679db6b81a9bf927f70c81bd1418fcff.jpeg)

靠信息不对称,对称后如何在研发这一侧如何快速的交付,同时要高质量,既要又要。

### 研发效能的五大 “灵魂拷问”

概括研发领域的现象,如下图:

![](https://image.eddycjy.com/3cac50b8521ddc22bcf9624ec7ee5693.jpeg)

拉车的人(代指:老板)没空看轮子,不知道轮子是不是方的,而推轮子的人是我们工程师。你知道不知道轮子是什么形状?

#### 第一问:研发团队的忙碌能够代表高效率吗?

![](https://image.eddycjy.com/ac6a5cba95ed68e30ff314a0f028d4c9.jpeg)

例如:凌晨半夜三点修 BUG 的人一定是最好的吗?BUG 很有可能是他埋的,他不修谁修?

建议方向:
- 架构的长期规划。
- 中台的持续沉淀。

#### 第二问:敏捷是研发效能提升的银弹吗?

![](https://image.eddycjy.com/2ddb582a11acd6f750cc3f46aaa54520.jpeg)

敏捷指的是能更快速的尝试,有问题的话马上调头。敏捷是要做小船。

#### 第三问:自动化测试真的提升软件质量了吗?

![](https://image.eddycjy.com/86c11968fd3789c7f65b488447106dae.jpeg)

如果卡死自动化测试的覆盖率没意义,最后会变成覆盖率很高,走的很慢,因为让覆盖率变高有非常多种方法。

而卡死自动化测试,就会导致没有精力去做探索性测试,更多的测试。需求变了,自动化测试又变了,又要花时间继续做新的自动化测试。。

自动化测试只是个手段,不是目标。新功能不应该做自动化,**功能本身趋向稳定了,才应该去做自动化测试,成本才低,成就感才高**。

#### 第四问:没有度量就没有改进,这是真的吗?

![](https://image.eddycjy.com/b49cb46586a53de77a562fcba314527e.jpeg)

研发效能很难在真正意义上度量,软件研发是创造性的劳动,不同的人来做是不一样的,硬要做,就会变成你度量什么,工程师就做什么。

你度量钉子,那你得到的就是钉子。你度量什么,就一定会得到什么。

不要用来考量 KPI,否则千行就会变成万行,要慎重。

#### 第五问:研发效能的提升一定是由技术驱动的吗?

![](https://image.eddycjy.com/358b682fb20ba6b54769b3a0c1186138.jpeg)

不要陷入局部思维,真正的问题不是单点的问题。

例如:看医生,真正挂号多久,但你真正花时间的是排队,看完医生1分钟,又开单验血,又等。因此等待时间是最大的。

在软件领域中,也是在等待时常上花的时间最久的。是部门墙,信息不对称导致的问题。

### 研发效能到底是什么?

![](https://image.eddycjy.com/df4963c29b6fa4cf6d452093b43ddf00.jpeg)

先有的现象,再有的结果,定义。

### 研发效能提升的案例

- 前端代码的自动化生成。
    - 工程师在白板上画 UI,自动识别并生成出代码和界面(利用了 AI 的相关技术)。
- 临界参数下的 API 测试
    - 自动的测试数据生成。
- 微服务架构下的环境困局。
    - 公共基础环境的问题,高效的方法是做公共基础环境,也就是现在的云端环境。每天和生产环境同步。

### 研发效能的第一性原理

![](https://image.eddycjy.com/a74a6f1aa5e19ac991402597853ed1d2.jpeg)

顺畅,高质量地持续交付有效价值的闭环。

- 做有价值的东西,做用户需要的,不要做用户不要的。

- 凡事做的事情能让这五个 “持续” 提高,就算研发效能。

- 所有的过程改进要用数据说话,但不要用来考核。

### “研发效能” 的点点滴滴

![](https://image.eddycjy.com/d0abd507792ff6c98882b5887ddfe2a0.jpeg)

研发效能的点点滴滴,做这些都能提高,针对性举例:

- 例如:云 IDE,非常方便。

- 例如:(举例 sonar)sonar 的机制为时已晚,没什么意义。可以在本地就去跑 linter,走得快质量还高。

- 例如:代码复杂度,最终呈现的就是软件和架构的腐化。研发工程师就开始复制粘贴改,最后没几年就废了。

- 例如:代码递交规范,你会把需求id带进去吗?不带的话,后面所有的度量都没法做,追踪都没法做,拿不到需求 id 都没做。

- 例如:分布式编译,各个模块分散到分布式去编译,从十分钟变 10 秒。

### 研发效能提升的一些经验和实践

![](https://image.eddycjy.com/57a3b325f949fc24b03ab1db8b221f89.jpeg)

推荐看书,用 MVP 思想做,和做通用工具完全不一样。

研发效能要先发现钉子,再去找锤子。和做通用工具不同,工具是拿锤子找钉子。

研发效能一般采用逐渐扎小孔,一层层做的模式。每次给的功能点足够小,但每一次给的都有价值。

![](https://image.eddycjy.com/6b574fba0ab6c360e3a2b5440f7450e1.jpeg)

做 MVP 不是横切一刀,是包含各方面的斜切。

![](https://image.eddycjy.com/c7cd61c07e131800a9febaa8ba675b65.jpeg)

这部分内容太太太多了,讲师也没有讲完。下方为根据讲了的部分整理:

- 从痛点入手:
    - 测试数据的搭建统一到测试数据中台去做。
    - 如研发度量的数据获取是最重要得,例如由工具自动触发状态的改变,而不需要研发工程师去调整,且获得的数据是真实有效的。
- 从全局切入:
    - 例如:一个 BUG,真正修复的时间是多少?
- 用户获益:
    - 让用户获益,是研发效能的核心
    - 不要你以为业务团队要,业务团队关心的是温饱问题。例如:你就看看业务团队自己有没有在搞,他们有在搞,这就说明是真的需求。
    - 结构很重要,如果设计的体制要每个人都大公无私是必然失败。每个人越自私越自利越能成功。举了和尚分粥的例子。
    - 谁接入了多少不是最重要的,是业务得到了什么。
    - 服务意识,早期保姆式服务,沉淀后,就是双赢,
- 持续改进
    - 例如:GitHook 直接调 JIRA API 不是最好的方案,没有版本管理,规模大了绝对不是好方法。
    - 应该走消息队列,揭藕。平台化。
- 全局优化
    - 下层提出,上层认可。
- 杜绝掩耳盗铃
    - 虚荣性指标 vs 可执行指标
    - 例如 sonar 接了多少个项目,就是虚荣性指标。应该考察可执行指标,例如严重 BUG 存在了多久。
- 吃自己的狗粮:
    - 自己的产品你都不用,别人更不可能。

### 研发效能的未来

![](https://image.eddycjy.com/3e02bf64de89130bdc29fe5d5a69cb60.jpeg)

表达核心观点:“敏态” 和 “稳态” 一定是齐头并进的。

## 快狗打车可持续交付实践

主要面向测试环境治理的演讲,Devops 不是单纯的技术问题,整体来看 Devops 是一个复杂的混合问题。

### 理想与现实

快狗打车前期是存在固定的多套测试环境,测试环境相互影响。测试同学 A 用完环境,第二天给了测试同学 B,B 同学发现有问题,又找回 A。A 同学又认为不是自己的问题:

![](https://image.eddycjy.com/a1935c4d63890b97e5639c5723f49f4e.jpeg)

### 测试环境V1

早期测试环境的具体表现,主体为稳定环境全量部署,下分四套环境,根据需求部署:

![](https://image.eddycjy.com/d8ccd7a945a1389126d4c13771ba79bc.jpeg)

早期几十个集群还没什么问题,等到规模变大后几千个集群后问题就会很严重。同时测试人人都有管理权限,第二套变更后,会同步到稳定环境,那么其他几套环境的同步由谁负责(他们早期是手动维护)。

![](https://image.eddycjy.com/14361c5532b6b7ae6df55890b90788f7.jpeg)

另外并行需求多了就会发现固定的测试环境不够用。呈现的结果是投入产出比差异过大,各配置互相影响,稳定性很差,最终造成的测试结果也不稳定。

### 理想的测试环境是怎么样的?

- 即点即用
    - 任何时间都可以部署,并不需要排队。
- 自动隔离
    - 任何环境都是相互隔离的。
- 依赖关系
    - 系统自动解析,根据配置自动部署依赖上下游,使用者无需关注。
- 缩放自如。
    - 资源池管理,资源弹性伸缩。
- 独立闭环
    - 独立部署,无需跨部门沟通(不用找运维要资源)。

### 第一轮的优化实践

核心要点:规范、格式、自动。

![](https://image.eddycjy.com/d05babd370f63589d17cd767370d2f7a.jpg)

针对各服务做依赖关系的自动解析。细则如下:
- 制定了配置文件的格式规范,用于扫描上下游依赖,层层扫描,最终得出整体的依赖图。
- 规范要符合公司现状,绝大部分都能适配,只有让小部分的人要改。
- 服务按照类型优先级部署,这里结合了应用信息(上层应用服务,底层应用、数据库、Redis 等)。

### 测试环境 V2

只有一套稳定环境,剩余的都可以按照需求来拉环境。但存在服务直连的情况,导致出现流量拦截和调动有问题。

属于企业内部自身的债务问题了,不展开赘述。

### 测试环境 V3

结合基础架构组,最后实现按需部署,谁开发谁部署,不改动不部署。

属于企业内部自身的历史债务问题了,不展开赘述。

### 总结

在治理优化实践上一共做了如下:

- 测试环境的服务按需部署。
- 依赖环境的自动解析。
- 部署资源池管理:评估部署所需资源,再通过资源管理平台,统一资源的调度。
- 自动流转与执行:Nginx(vhost、location)、域名(独立域名、泛解析)、MQ、堡垒机等的自动申请、审核。
- 资源自动回收:需求完成上线后,需求所关联的都自动释放,又或是回收到资源池。

整体上来讲,技术不是难点,最难的是人与人的沟通,例如:跨部门的沟通,最重要的是方向、坚持、执行力。

### QA

- 第一个:
    - 测试环境数据库也没有采用按需,因为测试环境数据库不是主要痛点(矛盾点),主要权衡的是投入产出比,因为并不是建起数据库就能解决的,还要造数据,各种成本很高,结合权衡没往这个方向做。
- 第二个:
    - 资源低于 60%,则针对于已经完成上线的机器,并不是资源池内的。
- 第三个:
    - 部署的互相依赖和网状结构,通过打标签的方式,若已经部署了则不会再部署了。
- 第四个:
    - 资源平台优化策略,目前正在转向 K8S 这类云平台,后续不再自行关注。
- 第五个:
    - 公共组件是否也是重新部署,目前 redis 是重新部的,主要是针对 mysql、redis。kafka 这类是没有独立部署的。
- 第六个:
    - 最大的困难是什么,是 “人”,人的认知,达成全员的共识,为什么做这件事情,讲清楚,比做什么更重要。

## 自如云原生落地最佳实践

主要演讲内容是自如的云原生演进之路,如下:

![](https://image.eddycjy.com/6ec794425d71b36638c5d967256baa76.jpg)

当时存在大量的问题,进行了调研。结果提示低 NPS(-44%),也就是 100 个里有 52 个人对 CI/CD 系统不满意。

具体的痛点:
- 运维人肉运维。
- 生产、测试环境不一样。
- 分支合并漏发代码、漏发配置。
- 上线发布一台台点。
- kvm CPU 使用率低。

进过调研和选型,发现开源的均有缺点,最终选择自研一站式研发平台。主体功能结构:

![](https://image.eddycjy.com/c80ee685792aa265366b5cea0d3f91d9.jpg)
- 上层的平台服务:面向开发同学。
- 下层的 K8S 等:面向 Ops 同学。

平台在整体的设计边界和原则上如下:
- 边界:只做无状态应用的容器化。
- 原则:能放到平台的操作坚决不用人。

### 容器化后遇到的问题

容器化后一堆新的知识 pod、ingress、service,网络模式也变了,开发同学都不懂,产生了大量的成本(学习、运维等)。

因此就决定了做应用平台,也就上面提到的平台服务。流程图如下:

![](https://image.eddycjy.com/66886bf86b9217dc0fcb92fb7a5d6ff2.png)

### CI/CD

- 定规范,统一环境。
- 定分支,统一分支模型。
    - 在各个 feature 分支上进行开发。
    - release 分支环境用于集成和发布。
- Dcoker/Deployment 零配置
    - 根据创建应用所填写的信息自动配置,研发不需要关心。
- 工具-跳板机
    - 在平台上做跳板机,不需要关心 IP,也不用登陆。

### 总结

- 云原生平台化,运维 0 参与。公司标准化(环境、分支等)。

- 不要闭门造车,统一思想,走 MVP(步子不要迈的太大)、持续运营、持续关注 NPS。

### QA

- 第一个
  -  流量染色,是为了动态的的调控服务调用。
- 第二个
   - 数据库污染,利用账户体系来做。同时注意 mq 这类需要隔离。
- 第三个
   - webshell 创建 bash 不要太多,超过 32 个会有问题。产生僵尸进程。
- 第四个
   - 微服务到云原生的成本,学习成本必然,把 dev 和 ops 放到一起。
- 第五个
    - 目前自如是让业务在平台上配一个专门的探活接口,再去探活。
- 第六个
    - 最大的阻力,就是人,CTO 把基础架构把运维放到了一起,形成了互补。组织结构要先调整。


## 研发效能度量的误区、体系化实践和效能提升案例

Devops 专题的出品人,会场火爆,全部站满。开局表示现在已经不再是讨论要不要 Devops,而是讨论怎么去做。

讲的很好,会场人员认可度高。

### 研发效能的情况

- 你的研发效率在业界属于什么水平?与竞争对手差距?
- 敏捷转 Devops 的转型有没有效果?是否可以量化评估吗?

### 软件交付效能的度量指标

![](https://image.eddycjy.com/f4b2db7fb494a5eaea9fdd5d79f8409a.jpg)

- 部署频率。
- 变更前置时间。
- 服务恢复时间。
- 变更失败率。

### 研发效能评估(愿景)

#### 阿里(211)

- 需求 2 周内交付。
- 变更 1 小时内完成发布。
- 需求 1 周内开发完毕。

#### 腾讯

- 项目团队规模扩张控制在 20 人以下
- 迭代周期在 1 周内

#### 研发效能度量的原则

- 结果指标 > 过程指标。
- 全局指标 > 局部指标。
- 定量指标 > 定性指标。
- 团队指标 > 个人指标。
- 指导性,可牵引行动。
- 全面性,可互相制约。
- 动态性,按阶段调整。

### 工具链网络

![](https://image.eddycjy.com/eefc72a3c3125e995459d7737016ee56.jpg)

- Devops 工具链网络:强调 “网络”,工具与工具的关联,代码与需求,代码与合并,与编译,各种信息能不能追溯到下面所有的环节(把工具串联集成起来)。而不是单单管理某一个领域。
    - 项目协作域。
    - 开发域。
    - 测试域。
- 价值流的交付模型:要从用户、客户的视角去看,从端到端的角度去看,而不是开发、测试的角度。要从完整的一个用户需求提上来每一步的具体步骤。
    - 工作流。
    - 生命周期。
    - 流动效率(不是资源的占用率)。
- 效能度量分析模型:软件研发效果,最终思考的是组织效能、业务结构。
    - 交付效率:需求前置时间、产研交付周期、需求吞吐量。
    - 交付质量:变更成功率、线上缺陷密度、故障恢复速度。
    - 交付能力:变更前置时间、部署频率。


给出了分析模型里的大量的度量考察指标,并表示企业内部有更多,几百个。但要注意度量指标不在于多,在于精。不同阶段也不一样,要有北极星指标。

![](https://image.eddycjy.com/a8925088b7aeecaabf6e90c843f90a4b.png)

你做的实践不一定代表有用,但要不断地思考和实践并改善。例如单元覆盖率,有个公司永远在 80%+,后面发现是对 KPI 战法,甚至单元测试里没有断言(多个讲师提到)。

不仅要关注创造价值的工作,还要关注保护价值的工作:
- 业务需求。
- 产品需求。
- 研发需求。

### 企业内部实践

展示了京东内部的研发度量系统,看上去非常完善,可以进行多层次的下钻(事业部 -> 项目组 -> 研发人员):

![](https://image.eddycjy.com/b36d995cc4f347bb6b7114fe4e515eda.jpg)

### 总结和避坑

- 成本问题:看板里的数据,如何让度量更准,那就是标准,那就需要大量培训。让需求和代码有关联,自动触发变更状态。自动化。
- 避免平均值陷阱:类似长尾问题,尽量用分位数。
- 度量不是为了控制,而是指导改进:如果是 KPI,你度量什么就会得到什么,只是不是以你所希望的方式得到的(古德哈特法则)。

总结:**那些不懂数字的人是糟糕的,而那些只看数字的人是最最糟糕的。应该走下去看具体是什么达成的,走到工作现场,看看是否真的有改进**。

## 京东 BDP 的全域监控、管控平台搭建实践

### 基本介绍

基于 Prometheus 生态进行了大量的改造:
- 采集端改造:PushGateway 会推数据到 Kafka,再另外消费。
- 模块拆解,作为不同的角色,读写分离,便于扩展。
    - 数据采集。
    - 预计算。
    - 数据分析。
    - 监控告警,
- 多级缓存:监控数据的数据是短时间内不会变的,会进行缓存(不同业务可配不同)。
- kongming 服务:基于不同的 promql 决定执行什么策略,例如:实时作业、离线任务、集群调度等。相当于是一个拓展了,高级监控治理了。

### 监控实践

- 单点监控:常见面板,
- 组监控:业务提供黄金指标,自动生成对应的组监控,可以做到千人前面。
- 关系链监控:父级节点和大表盘。

### 平台实践

平台提供让业务选择,业务不需要关注底层的表达式:

![](https://image.eddycjy.com/e9c2893dad1a97389131018582d6fdeb.jpg)

在更具体的实践上:

- 告警通知:支持父子节点的通知。

- 告警通知:支持告警人的动态通知,支持业务在上报指标时指定的。

- 高级治理:利用所拓展的 kongming 模块,做了许多基于历史数据和现状的干预,例如:实时作业干预、智能调度(削峰、自愈等)。也就是相当于 “人工智能” 的那一块相关内容了。

总体来讲,做自动化也会是类似的思路,要拓展出一个模块。这样子你做什么都可以,只要基于 Prometheus 的一些表达式和数据源就可以了。

### 总结

监控系统不仅要能发现,还要哪能解决问题,监只是手段,控才是目标。

解决各种人力问题。

## 淘宝系 - 云原生下编程方式的探索和实践

### 淘宝现状

![](https://image.eddycjy.com/2e85139d64d801a8b20cfe1439262689.jpg)

- 中心化:微服务化。

- 去中心化:FatSDK,以 SDK 的方式提供能力。能够保障稳定性和性能。淘系绝大部分都是采取的第二种模式。出问题的话,就只有你自己的服务有问题,不会影响其他依赖方。

在 SDK 上他们只提供 Java,其他你自己想办法,常见的可以做个代理。

### 通用能力下沉

把原有 SDK 的能力剥离到独立的进程,而不是像原本那样沉淀在应用中:

![](https://image.eddycjy.com/e92806927597cbe146488d698ef02eea.jpg)

利用云原生的容器,提供运维能力容器,业务能力容器,解决中心化的问题。在此书其实是参照了 ServiceMesh 的方式,走 Sidecar 就好了。

### 解决多语言问题

加多一层,提供 GRPC API 来进行对接。对于业务来讲只需要面对标准化的 API 就可以了:

![](https://image.eddycjy.com/767257e017a6a496a0d4f116e8cd5277.jpg)

业务不会直接对外对接暴露,所有东西都是通过对外/对内的中间层来进行衔接:

![](https://image.eddycjy.com/2709782851a3590f94711996866a4022.jpg)

### 开发市场

做了一个应用市场,业务开发可以提供组件能力上去,避免重复造轮子:

![image](https://image.eddycjy.com/4737ce38878a622436193d91f931d245.jpg)

## 全面监控体系建设及智能监控的探索实践

PPT 内容比较抽象,简单来讲:AIOps = 大数据+算法+运维场景。

通过各项能力,建设了对于历史数据的分析,做了各种分析和告警合并等场景。每个模块都大致讲了,但均没有深入讲,只能大概听个响,与原有所知的监控思路基本一致。

智能运维业界目前没有开源方案,都是一线大厂分享的经验。放一张 PPT 图:

![](https://image.eddycjy.com/a52c0e553978fd959973f9c56081b963.jpg)

按分层自下往上看,基本是基于数据进行智能化的预示来达到效果。

## 低代码技术在贝壳的实践

![](https://image.eddycjy.com/f0f5c4b8cd9416272e2d3fa4b72c2cf4.jpg)

更具体的演示效果可看平台方发的视频(期望/现状)。


### 提效

![image](https://image.eddycjy.com/c94415916dbd8d3c1ede248b9b5ef4ca.jpg)

能覆盖到的场景基本把前端功效给去掉了,但同时后端的工作量会加大。

### 现状

- 河图1.0:已开源。定制化需求,一开始想让客户自己开发插件化,效果不行。最终决定走向智能化。

- 河图2.0:智能化。
    - 期望:自动识别设计稿,国外有,支持多端。
    - 目前:
        - 贝壳现在支持的是上传 sketch 设计稿,支持不同的 iOS,安卓,Flutter 以及自己的小程序。
        - 支持后台管理增删改查,小程序,中后台等。
    - 未来:
        - 后期会引入智能化,将会持续开源。

### 投入的人力

- 第一期:最初是3个人,做了两年,从 2019.1 做到 2020.12。
- 第二期:目前投了 10 个人。

### 复杂场景

- 第一类通用类:
    - 目前可以解决。
    - 例如配置系统可以用。
- 第二类定制化:
    - 要靠智能识别。
    - 目前的确只能解决一些通用常见的问题,复杂问题还需要人来解决。

## 总结

目前业界中其实存在着大量的方案和思路,很多大厂其实更多的会根据目前公司内的实际情况进行选型和设计。听这类会议/培训,一定要关注的是其解决思路和途中的思考,是为什么这么做,踩过什么坑,比较重要。

希望大家在看完后都能有进入心流的深度思考时间,那样你才能消化到知识,并转换到你实际的工作中去。


================================================
FILE: content/posts/2021-ecug.md
================================================
---
title: "推荐一个牛逼的技术社区!"
date: 2021-01-05T21:26:50+08:00
toc: false
images:
tags: 
  - ecug
---

相信我的读者中不少是 Go 语言的爱好者,又或是正在伺机而动。

今天要给大家所介绍的这个技术社区,就是由与 Go 语言有很浓厚的缘分的人所创办的。

他有如下几个业界标签:

- 早期的国内 Go 语言布道师。
- 早期在公司内大规模的推广和使用 Go 语言。
- 早期编写了一本 Go 语言图书:《Go 语言编程》。
- 现在是一家公司的 CEO。
- 近期在大力推广 Go+。
- ...

他还有非常多的标签,通过上述这几点,你是否猜到是谁了呢?

![](https://image.eddycjy.com/0c8ac8602cca4e19c8caca30ac991305.jpeg)

没错,他就是七牛云的 CEO 许式伟。

## 与 Go 语言的渊源

许式伟在早年离开盛大创新院。创办七牛云的时候,选择了 Go 这门还未发布正式版的语言。因为小众,许式伟开始有意识地培养 Go 中国社区。

他们做了很多工作。具体有:

- 2012 年 2 月,许式伟首次在公开场合说:Go 会超过 C、Java,成为最流行的语言。讲得最多的一个 PPT 是《Go,Next C》这篇。
- 2012 年 8 月,正式出版 Go 图书,书名为《Go 语言编程》。
- 2020 年下半年,正式对 Go+ 进行了宣传和推广,对大数据科学的领域进行了增强。

## 神秘的技术社区

虽然许式伟已经是七牛云 CEO,但依然在技术领域和咱们的 Go 领域发光发热,并没有因此而停下脚步。许式伟早在 2007 年就成立了一个技术社区。

![](https://image.eddycjy.com/f04e24b25f48c3d2293e64390d22888f.jpeg)

这个社区名字叫 ECUG,ECUG 全称为 Effective Cloud User Group(实效云计算用户组)。

其成立于 2007 年的 CN Erlounge II,由许式伟发起,是科技领域不可或缺的高端前沿团体。作为行业技术进步的一扇窗口,ECUG 汇聚众多技术人,关注当下热点技术与尖端实践,共同引领行业技术的变革。

![](https://image.eddycjy.com/1cba758a30621c7b4db7c92ae9e739d0.jpeg)

截止到 2020 年 ECUG Con 已成功举办了 13 届,ECUG Con 的技术主题主要涉及:云计算、数据、区块链方向。

**今年第 14 届 ECUG 大会将于 2021 年 1 月 16 - 17 日在上海举行,为期两天**。作为 Gopher,身处云原生时代,这样的盛宴不容错过。

购票地址:

![](https://image.eddycjy.com/017bf73c7eaa90dc99d793324b347e88.png)

大会议程如下:

![](https://image.eddycjy.com/1b3475f904becad56e9450aff88d9402.jpg)

大会嘉宾:

![](https://image.eddycjy.com/177b353f31903dcde292755a84af5e73.png)

## 各种福利

这次大会煎鱼**为大家申请到了 15 个免费名额,我会从留言者中随机选 15 位送出**。


同时本次大会还有技术嘉年华的环节,会送出各种礼品:

![](https://image.eddycjy.com/da791fb34cb3a8bbd353bb9171b8c180.jpeg)

有兴趣的鱼粉们赶紧留言,报名参加 ECUG 吧!

================================================
FILE: content/posts/2021giac.md
================================================
---
title: "我周末参加了个架构师大会!"
date: 2021-12-31T12:54:57+08:00
toc: true
images:
tags: 
  - giac
---

大家好,我是煎鱼。

前两天 GIAC 全球互联网架构大会在深圳举办了,总算是有个长年在深圳举办的大会了,愉快参加了大部分的场次,面基了不少社区网友。

分享一些我听了觉得有意义的记录给大家。希望能和大家一起学习进步。本文分别涉及如下几个议题:
- 《hits for microservices desgin》
- 《在企业中的个人成长》
- 《大规模任务调度在 AfterShip 的高可用实践》
- 《快手前端实时性能监控和稳定性度量》
- 《快手中间件 mesh 化实践》

## hits for microservices desgin

一开始先介绍了为什么叫 ”hits“。叫 ”hits“ 的主要原因,是**业务架构没有技术架构那么明确,没有明确的对与错,是个人的工作经验和总结**。

### 微服务解决什么问题

业内常常说到,微服务,微服务。总归期望微服务解决什么问题。

演讲的作者做了如下的调研:

![](https://files.mdnice.com/user/3610/903dfcbb-a0b6-481f-a40e-3476b8ac8b64.png)

从调研结果来看,占比最大的就是 ”独立自治,只关注自己的模块“。这和绝大部分既有业务的公司做微服务的初衷一致。

许多就是被单体的巨石应用折腾的不行,纷纷希望通过拆分微服务来实现业务模块的独立自治。

### 微服务的现状

主要是播放了动图,配合口述。现在大多数服务拆分后的现状,很多就是**改哪影响哪完全不清楚,和水管漏水似的**:

![](https://files.mdnice.com/user/3610/f871b4de-c8b6-47b4-a5e7-5b9f64b73aea.jpg)

(自行脑补一拧水管,堵哪,别的地方就漏)


### 衡量微服务拆分的标准

理想中的微服务拆分,希望要有灵活的组装能力。但拆分后遇到的新问题,实际的情况,拆分后与期望的不一样,拆着拆着就变成了一大坨,但只是说隔开了,与现在企业中微服务运行的现状很贴合。

拆分后如有如下几个痛点:

![](https://files.mdnice.com/user/3610/0890ca8a-4fa1-4173-8334-92a167a47a19.png)

举了几个案例。分别是:
- 订单的例子。
- 报价的系统。
- 数仓的例子。

#### 订单

举的是订单的例子,订单团队非常忙,因为信息都存在订单里,系统其他有任何业务上的变更诉求,都要找订单的团队。

为此,在拆分上需要优化成订单业务只保存订单 ID:

![](https://files.mdnice.com/user/3610/24a64dd9-98ae-419f-afef-aa125513dbd2.png)

各系统存订单ID,各团队自治,实现业务解耦,订单团队就不用因其他业务变更天天加班了。


#### 报价

举的是报价的系统,要是报价团队,针对各个子业务项都要自己实现一般,会非常的辛苦,经常要加班。

我们只需要在报价系统提供接口标准,各系统自己实现,再对接:

![](https://files.mdnice.com/user/3610/92f5b5eb-e5de-4bfd-bab6-f3e9ab2d7a6d.png)

报价团队就不需要每次都重新开会,再对接。报价系统自己只做业务流程的编排,瞬间变轻松了。

#### 数仓

举的是数仓的例子,业务改一个字段,数仓系统要改一个月,否则就会出现问题,因此要求业务有任何 schema 改变都必须要通知数仓团队:

![](https://files.mdnice.com/user/3610/b1a09935-9663-449a-8646-b55ef3e08b7c.png)

很现实的是,基本通知不过来,所以很多公司把他作为绩效,定期考核,出问题定期批评。

建议的是:通过 RPC 的方式提供维护,把数据维护交给业务团队自己维护,数仓团队应该只做具体的跨团队的数据互联。

#### 好的标准的定义

分层,都可以独立变更,可以自己搞自己,只需要保证这一层提供的能力是稳定的就好(全部改一遍的另当别论),不需要了解上下游,只需要维护好 interface。

具体几点:
- 不同模块间完全没影响
- 只共享不可变数据
- 共享可变数据,但接口不变。
- 大部分情况变化的是实现,变化的不是接口,接口的变更次数很少。

参照乐高,关注接口的稳定性,而不是拆的越细越好。

评价的标准是:看不同系统不同模块的互相影响程度,就知道各系统怎么样了。

### 小结

1. 不要在一开始就使用有意义的名词,例如:交易中心,支付中心。大家会根据名字来设计的架构,建议最后再起名字。
2. 复用不是目的,是手段。例如业务中台真的是复用吗,不是。只是互联互通。
3. 好的架构,是要控制复杂度,在一定的规范下尽量自治。
4. 先划分清楚业务模块,划分清楚了,再去设计你的技术架构。

提问时也有涉及到 ”分布式事务“,这简直就是微服务相关议题的必问话题。演讲者表示:倾向持续交付。尽可能不让他有分布式事务。

## 在企业中的个人成长

毛老师的演讲,据说全场综合评分最高,内容是分享了自己在企业成长的三个阶段:
- 阶段一:加入 Startup 公司。
- 阶段二:轮岗。
- 阶段三:重新出发。

分享比较有明确的时间线,我是直接按点来记录的,刚好十条,非常经典。

### 十条纲要

1. 15 年,也不知道什么是 B 站,也找不到人。从以前认识的一些熟人,从别的公司挖回来。**小公司,亲自带。自己熟悉业务,请教老员工文化**。

2. 团队到 40~50 就要考虑做人才梯队了。要保证每个月有 1v1 的简单聊一聊,或是每天现场聊聊。**稳定性问题和管理有关系**,有没有研发红线,例如发布变更,生产无小事。

3. 改变一个人性格非常困难,只能告诉他,给一两次机会。**不合适就放弃,心慈刀快,尽快解决掉**。合适的人,给过两三次机会,自己早就转变过来了,两三次还不行,肯定是难以改变的了。

4. 要做核心的事情,不要什么需要都做。**不要用技术实现去挑战老板的战略考虑,要用业务**。

5. 不要害怕空降,**引入新的人,有竞争压力,有进步**,要做减法,聚焦最重要的事情。向他们学习,看他们的优势,择机成长。

6. 要换位思考,站在平台方,成就业务。**最好的团建是一起拿成果**。从老板的角度考虑,绝对能折腾,绝对能将就,就像基础架构部门,有时候要业务优先。

7. 影响力,技术辐射范围足够广,不同团队落地,自然而然就有了影响力。公司外的影响力,多分享,多参加 meetup。

8. 哪里有需要就去哪里。

9. OKR 要足够的透明,足够的明确。下级要知道,要清楚,甚至是自下而上的 OKR。

10. **团队的会模仿你,看别人的缺点,修正自己**。自己在团队中要做榜样(举例:早点下班的问题)。

### 小结

在职业生涯发展上的小伙伴,建议可以看看毛老师的分享 PPT,看看大佬从 0 到千亿万身家的个人成长发展史。

就如总结所说的:”当你很忙碌的时候,你的管理工作一定出了问题“,值得思考。

## 大规模任务调度在 AfterShip 的高可用实践

初始的业务需求是有优先级调度的需求,用户的调度比物流包裹的优先级高。

任务调度的量比较大,要能运行千万级的优先级任务调度。

### 老版本

老版本是根据 15 分钟划分一个 Topic,可以理解为分区,一天 96 个。

采取的是轮询策略,还没执行的放进延迟队列。实现了 15 分钟粒度级别的任务级别(没法做到 1 分钟,5分钟的这类纬度)。

![](https://files.mdnice.com/user/3610/d78359d3-5c59-4535-a0ed-2214ade04302.png)

存在以下问题:
- 会导致出现波峰,资源浪费。
- 设计过于复杂,导致系统脆弱。
- 链路过长,定位困难。
- 错误的 FIFO 实现。

### 新版本

![](https://files.mdnice.com/user/3610/36b6fce2-c23a-451a-a538-4df3cac19be5.png)

解决思路: 
- 通过 LMSTFY 任务队列,解决延迟和优先级功能,解耦业务。
- 基于 Redis,正在做二级存储,冷热数据隔离。
- 通过指定多个队列,来实现多个优先级调度。取多个队列,A队列有内容,则优先消费A。
- 通过实现接口来队列多个数据存储。
- 借助组件的特性和系统优化,简化了架构。

### 小结

相当于是看了一个任务调度系统的设计发展史,理论上设计的存储和方案不一定得选 Redis 或是 LMSTFY。

不过考虑到演讲者的背景,因此趋向了这个技术方案,也可以从中看到后续任务调度系统规模更大后,可能出现的问题。

## 快手前端实时性能监控和稳定性度量

公司内也有类似的系统,与快手的前端 APM 是一模一样的定位,可以借此看看成熟的系统的选型和发展情况。

### 前端的数据

需要有统一性能指标,用一个指标代表:

- 早期:DCL,Onload,来判断前端页面的性能。页面依赖接口数据容易被绕过,长页面不能反映用户真实感知。

- 设想指标的作用:希望获取白屏时间,代表白屏到非白屏的时间。

但发现也无法保证,计算白屏可能会导致客户端崩溃。不能代表页面核心内容的时间:

![](https://files.mdnice.com/user/3610/0afbfd6c-e9ca-4357-8ddb-c45f26f2c1f7.png)

转为目的是拿到主要内容渲染的时间,业内常见用 FMP、LCP:
- FMP(First Meaningful Paint):没有标准实现,对页面细微变化或于敏感。某一个 DOM 内部有很多个节点,可能会造成误差。
- LCP(Largest Contentful Paint):页面内最大内容的渲染时间,最大元素不一定是最重要的元素,例如新闻内的图片。并且浏览器的支持率只有 70%(致命),因为快手很多低端机是不支持的。

### 真正要的指标

考虑业务需要的指标是什么,本质上业务的真正内容是**从接口异步获取回来的**。

因此采取了自定义 FMP,API 响应数据渲染到页面后的时间,代表页面的性能:

![](https://files.mdnice.com/user/3610/5a6f62f8-2ef7-4500-b7a5-b47c62d526ba.png)

这个自定义方案要业务自己计算的,会调用提供的统一方法来计算。虽然有一定侵入,但准确性最高,最有效。

主要有以下三种统一度量指标: 
- API 性能/异常。
- 资源性能/异常。
- 脚本异常。

也做了容器的性能数据,例如:webview 的启动时间比较慢。提前缓存,可以大幅度提升性能。

### 小结

大篇幅主要是介绍了前端指标的定义和摸索过程,这是一个平台的基石数据了。

后续的平台能力拓展如下:

- 在排查问题上:他们做了根因定位,分析了许多前端的具体指标,基本达到出现异常 5 分钟内可通知到业务,10 分钟内给出解决方案建议。
- 在大数据量上:动态采样,批量上报,数据上报保障,异常场景兜底(例如:异常上报不上来,会在恢复后上报异常退出了页面,会记录堆栈信息)
- 在性能拓展上,做了性能周报,主动给业务推,便于他们实时的跟踪自己的情况。也做了数据大屏做全局视角的分析。

## 快手中间件 mesh 化实践

分享是介绍中间件相关的 mesh,比较有意思的是定义了目前业内 mesh 的三代阶段,这倒是没怎么听过。

具体的定义如下:

![](https://files.mdnice.com/user/3610/be285750-bacf-42cd-bcdf-12507849b108.png)

快手中间件采取的是:第三代中间件 mesh,轻 sdk 的方式。

### 选型考虑

在以往,重 sdk 下,整体开发维护成本,升级都比较繁琐,代理业务协议的流量。

如果是以 Istio、Linkerd2 为代表,主要是处理常见的通讯协议(例如:HTTP、gRPC 协议),又对中间件 mesh 不大合适。

最终采取的是 mesh 加轻 SDK 的方式:

![](https://files.mdnice.com/user/3610/89749180-c082-4d7e-8072-55ceec5c385d.png)

在此 mesh 的定位是能力下沉,sdk 的定位是标准化的定义。

### 新的挑战

做过微服务,基础设施的小伙伴都知道,要达到 A,将会在 BCD 付出更多,这是常态。

要上 mesh 非常简单,K8s 里几条命令的事。但后面要处理的事,可就是要切切实实的成本了。

主要面临了如下挑战:
- 成本问题:复杂环境下的统一部署与运维。
- 复杂度问题:规模大、性能要求高、策略复杂。
- 落地推广:对业务来说不是强需求。

### 解决方案

针对以上三点,又配套了做了如下解决方案和措施:

1. 统一运维:对自己,网关的运维平台。对业务,定位排查,可观察性的。

2. 规模大问题:对 Enovy 等做了二开。只传输变更的数据、按需获取,解决单实例服务数过多的问题。

3. 性能要求高:协议栈的优化、序列化优化等,做了大量底层的优化。

4. 实施了面向失败设计,SDK fallback,可以 fallback 切换为直连模式。如果是新功能,没有老的,会切换到 Proxy 模式。

### 业务推广

业务推广这点要单独拧出来讲,因为 mesh 一般的直接收益不是业务,是基础设施,对业务不是强需求会遇到:
- 业务对性能,稳定性敏感。
- 业务很难配合人力配合架构升级。
- 对业务侧的收益并不明显。

据闻现在业内大规模落地的只有字节和蚂蚁,且都是有很多背景因素的,和组织上的人有直接关系。

关于业务推广,演讲者也给出了一些建议,例如:稳定性很重要,搭便车,业务共建,选型有明显业务收益等。

### 小结

近几年各家都纷纷出来分享 mesh,其实基本上现状和应用情况都比较清晰了。

像本次快手 mesh 的分享主要是面向中间件(gRPC、Kafka、RocketMQ、ZK、Mongo、Redis、MySQL)等。

但能明显感觉到业务推广的苦恼,以及可预测的运维投入巨大,这也是所有出来分享 mesh 的核心痛点分享。

其余的技术细节,大多通过二开等方案解决了。


================================================
FILE: content/posts/go/117-build.md
================================================
---
title: "时隔 3 年,Go1.17 增强构建约束!"
date: 2021-12-31T12:54:56+08:00
toc: true
images:
tags: 
  - go
  - go1.17
---

大家好,我是煎鱼。

Go1.17rc1 在前几天终于正式发布了:

![](https://files.mdnice.com/user/3610/a9dd1134-e4f8-4d9c-9c3c-2608728ddf69.png)

看到 Go1.17 增加了一个新特性,是面向 Go 构建时的构建约束的增强。认真一看,是一个时隔 3 年的提案了,原本还在 Go2 和 Go1 之间左右摇摆,这下在 6 月底 Russ Cox 就输出了新草案:《Bug-resistant build constraints — Draft Design》。紧接着直接计划在 Go1.17 发布了。

一气呵成,真实版高效能了。

如下图:

![](https://files.mdnice.com/user/3610/10037273-887d-45c0-9eca-6ef36d7c4d72.png)

之前小咸鱼有遇到好几个朋友,在报错时压根不知道 Go 有这个约束语法,以为只是个单纯的注释,直接不明所以然,感觉科普之路任重道远。

今天这篇文章煎鱼就来讲讲构建约束这事。

注:下个月 Go1.17 就会正式发布,距离 Go1.18 泛型出山只差一点点距离了,值得期待!

## 构建约束的背景

简单来讲,在真实环境中,可能需要为不同的编译环境编写不同的 Go 代码,所以需要做构建约束。

划重点,Go 语言对这一问题的解决方案是**在文件层面进行有条件的编译:每个文件要么在编译中,要么不在**。

也就是,假设不符合构建约束的场景。那么会直接不编译这个文件,因为他不在编译范围内。那在程序想运行时就会报错,表示找不到文件。因此有许多的同学看着报错信息,经常找不着北...

## 现有的构建约束

既然是叫 “增强”。说明现有就有构建约束。最早的构建约束是在 2011 年 9 月引入的构建约束。

我们平时常见的构建约束(build constraint),也叫做构建标记(build tag),构建约束必须出现在 `package` 之前。

平时会在 Go 工程的文件中的最开始会看到如下行注解:

```
// +build
```
为了将构建约束与包文档区分开来,构建约束后必须跟一个空行。

```
// +build linux,386 darwin,!cgo
```

又或是:

```
// +build linux darwin
// +build amd64
```

还可以根据 Go 版本来约束:

```
// +build go1.9
```

其主要支持如下几种:
- 指定编译的操作系统,例如:windows、linux 等,对应 `runtime.GOOS` 的值。
- 指定编译的计算机架构,例如:amd64、386,对应 `runtime.GOARCH` 的值。
- 指定使用的编译器,例如:gccgo、gc。
- 指定 Go 版本,例如:go1.9、go1.10 等。
- 指定自定义的标签,例如:编译时通过指定 `-tags` 传入的值。
- ...

## 有什么问题

既要用动他,本着 Go team 的 less is more 原则。想必是现有的构建约束,存在着什么问题,才需要调整他。

### 对语法困惑

从 issues 的反馈来看,是太复杂,如下:

```
// +build linux,386 darwin,!cgo
```

他表达的构建约束是:(linux AND 386) OR (darwin AND (NOT cgo)) 。

感觉可以像三元运算符一样玩出花,可参见《Go 凭什么不支持三元运算符?》,这更夸张,没常见的逻辑符。

也可以更复杂一些:

```
// +build 386 !gccgo,amd64 !gccgo,amd64p32 !gccgo
```

会导致会混淆用户的认知,如果能够这样写更好:

```
// +build 386 amd64 amd64p32
// +build !gccgo
```

### 对布局困惑

现在的 `// +build` 有硬性的使用规则:
- 必须出现在文件顶部附近,前面只能有空行和其他行注释。这些规则意味着在 Go 文件中,构建约束必须出现在 package 子句之前。
- 为了将构建约束与包文档区分开来,一系列构建约束后必须跟一个空行。

像是以下失败案例:

```golang
package main

// +build linux
```

又或是:

```golang
/*
Copyright ...
*/

// +build linux

package main
```

整体来看,官方在 2020 年 3 月对 `// +build` 注解的使用情况进行了分析,得出以下几种常见情况:
- 忽略了`/* */`注释后的构建约束,通常是版权声明。
- 忽略了文档注释中的构建约束。
- 忽略了包声明后的构建约束。

这些都是实际项目中出现的,也就是这个构建约束的布局约束并不好,造成了很多意外和反馈。

像是版权声明的统一写入,基本都是脚本统一打上去的。会造成大量的隐藏版本 BUG。

## 增强后的构建约束

增强,就是优化,主要的目标之一是解决语法、布局困惑。

设计的核心思想:用新的 `//go:build` 取代目前用于构建标签选择的 `//+build`,并且使用更广为熟悉的布尔表达式。

设计的关键:平滑过渡,避免破坏 Go 代码。

以前老的注解:

```
// +build linux
// +build 386
```

新的注解:

```
//go:build linux && 386
```

新的语法主体为 Go spec 的 EBNF 标记:

```
BuildLine      = "//go:build" Expr
Expr           = OrExpr
OrExpr         = AndExpr   { "||" AndExpr }
AndExpr        = UnaryExpr { "&&" UnaryExpr }
UnaryExpr      = "!" UnaryExpr | "(" Expr ")" | tag
tag            = tag_letter { tag_letter }
tag_letter     = unicode_letter | unicode_digit | "_" | "."
```

也就是说,构建标记的语法与其当前形式保持不变,但构建标记的组合现在使用 Go 的 ||、&& 和 ! 运算符和括号完成。

另外一个文件只能有一行构建语句,也就是一个文件有多行 `//go:build` 是错误的,如此设计的目的是为了消除关于多行是隐式 AND 还是 OR 在一起的混淆。

## 过渡阶段

在过渡阶段,也就是 Go1.17 起。官方的 gofmt 工具会自动根据旧语法转换新版的语法,以保证兼容性。

例如:

```
// +build !windows,!plan9
```

会转变为:

```
//go:build !windows && !plan9
// +build !windows,!plan9
```

后面是计划把 `//+build` 给完全下线的。

![](https://files.mdnice.com/user/3610/6fcaf62d-bd78-475b-85f1-614d5bc51437.png)

常规 Go 工程基本用不到,因此就不进一步展开描述了。

对过渡阶段感兴趣的可以看看 《Bug-resistant build constraints — Draft Design》的 Transition 部分,比较长,正常来讲是不需要我们关注的。

## 总结

Go 1.17 构建约束的增强,一下子让整个语法明确了起来。统一为 `//go:build`,至少不会有人看到 `//+build` 又以为是普通注释了。

**你是否有在工作中遇到构建的版本、环境约束等场景呢,欢迎大家在评论区留言交流**!

================================================
FILE: content/posts/go/117-errorstack.md
================================================
---
title: "Go1.17 新特性,优化抛出的错误堆栈"
date: 2021-12-31T12:55:05+08:00
toc: true
images:
tags: 
  - go
---

大家好,我是煎鱼。

平时在日常工程中,我们常常会用到异常恐慌(panic)的记录和追踪。最常见的就是,线上 panic 了之后,我们总想从中找到一些蛛丝马迹。

我们很多人是看 panic 是看他的调用堆栈。然后就开始猜,看代码。猜测是不是哪里写的有问题,就想知道 panic 是由什么参数引起的?

因为知道了诱发的参数,排查问题就非常方便了。为此**在 Go1.17,官方对这块的调用堆栈信息展示进行了优化**,使其可读性更友好。

## 案例

结合我们平时所使用的 panic 案例。如下:

```golang
func main() {
	example(make([]string, 1, 2), "煎鱼", 3)
}

//go:noinline
func example(slice []string, str string, i int) error {
	panic("脑子进煎鱼了")
}
```

运行结果:

```
$ go run main.go
panic: 脑子进煎鱼了

goroutine 1 [running]:
main.example(0xc000032758, 0x1, 0x2, 0x1073d11, 0x6, 0x3, 0xc000102058, 0x1013201)
	/Users/eddycjy/go-application/awesomeProject/main.go:9 +0x39
main.main()
	/Users/eddycjy/go-application/awesomeProject/main.go:4 +0x68
exit status 2
```

我们函数的入参是:`[]string、string、int`,核心关注到 `main.example` 方法的调用堆栈信息:

```golang
main.example(
    0xc000032758, 
    0x1, 
    0x2, 
    0x1073d11, 
    0x6, 
    0x3, 
    0xc000102058, 
    0x1013201
)
```
明明只是函数三个参数,却输出了一堆,对应起来非常的不清晰。

其实际对应是:

- slice:0xc000032758、0x1、0x2。
- string:0x1073d11、0x6。
- int:0x3。

这里存在的问题是,看调用堆栈的人,还得必须了解基本数据结构(例如:slice、string、int 等),他才知道每个函数入参他对应拥有几个字段,才能知道其内存布局的结构,有一点麻烦。

并且从程序运行的角度来讲,这么水平平铺的方式,并不直观和准确。因为不同类型他是多个字段组合成结构才能代表一个类型。这不得还要人为估测?

## 优化

终于,这一块的调用堆栈查看在 Go1.17 做了正式的改善。如下:

```golang
$ go1.17 run main.go 
panic: 脑子进煎鱼了

goroutine 1 [running]:
main.example({0x0, 0xc0000001a0, 0xc000034770}, {0x1004319, 0x60}, 0x0)
	/Users/eddycjy/go-application/awesomeProject/main.go:9 +0x27
main.main()
	/Users/eddycjy/go-application/awesomeProject/main.go:4 +0x47
exit status 2
```

新版本的调用堆栈的信息改变:

```golang
main.example(
    {0x0, 0xc0000001a0, 0xc000034770}, 
    {0x1004319, 0x60}, 
    0x0
)
```

在 Go 语言以前的版本中,调用堆栈中的函数参数被打印成基于内存布局的十六进制值的形式,比较难以读取。
 
在 **Go1.17 后,每个函数的参数都会被单独打印,并且以 “,” 隔开**,复合数据类型(例如:结构体、数组、切片等)的参数会用大括号包裹起来,整体更易读。

其实际对应如下:

- slice:0x0, 0xc0000001a0, 0xc000034770。
- string:0x1004319, 0x60。
- int:0x0。

这里也有一块细节要注意,你会发现 Go1.17 的函数参数的数量和以往的版本相比,少了。是因为函数的返回值存在于寄存器中,而不会存储到内存中。

因此函数返回值可能会是不准确的,所以也在新版本中也就不再打印了。

## 总结

在 Go1.17 的新版本中,调用堆栈的函数参数的可读性得到了进一步的优化和调整,在后续的使用上可能能够带来一定的排错效率的提高。

你平时在借助调用堆栈排查问题呢,希望还获得什么辅助呢?

## 参考
- [GoTip: New Stack Trace Output Wrong](https://github.com/golang/go/issues/46708)
- [cmd/compile: bad traceback arguments](https://github.com/golang/go/issues/45728)
- [Go 1.17新特性详解:使用基于寄存器的调用惯例](https://mp.weixin.qq.com/s/AkJoXLlpSmw5vMZDpXoq5w)
- [doc/go1.17: reword "results" in stack trace printing](https://groups.google.com/g/golang-codereviews/c/JkhaLqHFReM?pli=1)

================================================
FILE: content/posts/go/117-generics.md
================================================
---
title: "Go 1.17 支持泛型了?具体怎么用"
date: 2021-12-31T12:55:02+08:00
toc: true
images:
tags: 
  - go
---

大家好,我是煎鱼。

千呼万唤的,Go1.17 前几天终于发布了:

![](https://files.mdnice.com/user/3610/45fe4f8a-ef8e-41a0-abba-ecdc05fe61c5.png)

先前我写了几篇 Go1.17 新特性的文章,有兴趣的小伙伴可以看看:

- [一个新细节,Go 1.17 将允许切片转换为数组指针!](https://mp.weixin.qq.com/s/v1czjzlUsaSQTpAOG9Ub3w)
- [我要提高 Go 程序健壮性,Fuzzing 来了!](https://mp.weixin.qq.com/s/zdsrmlwVR0bP1Q_Xg_VlpQ)
- [提了 3 年,Go1.17 终于增强构建约束!](https://mp.weixin.qq.com/s/5kLFIuI0UJl_o8vMmZNfoA)

今天的主题是泛型,众所皆知 Go1.18 泛型就会正式释出,都很期待,毕竟大更新,所有配套都会陆续有来!
其实,**在 Go1.17 的此刻其实可以使用泛型了**,泛型代码已合入 master 分支。

咱们只需要一点点操作,就能提前过上 Go 泛型的实验生活了。

## 升级 Go1.17 

你需要先升级 Go1.17,如下图:

![](https://files.mdnice.com/user/3610/a3260cf0-f9bc-4c86-aafd-4d08480143b9.png)

安装后查看版本信息是否正常输出:

```
go1.17 version
go version go1.17 darwin/amd64
```

## 使用泛型

接着写入一个基本的泛型 Demo:

```golang
import (
	"fmt"
)

func Print[T any](s []T) {
	for _, v := range s {
		fmt.Print(v)
	}
}

func main() {
	Print([]string{"你好, ", "脑子进了煎鱼\n"})
	Print([]int64{1, 2, 3})
}
```

只需要在 run 和 build 的命令执行时指定 `-G` 标识就好了。不过有的小伙伴可能会疑惑,为什么要这么干?

其实这类提前放入主版本的操作,在 Go 语言中并不少见。像是现在所见的 `GO111MODULE`,早期的 `GO15VENDOREXPERIMENT` 都有些这么个味道。都是逐步入场,分阶段使用,等确定成熟、完善后再渐渐去掉。

本次泛型也采取了这种方法,按照提案,目前使用的是 `-G` 标识做为泛型的开关。

运行的命令如下:

```
go1.17 run -gcflags=-G=3 xxx
``` 

就可以运行带有泛型的代码。

查看输出结果:

```
$ go1.17 run -gcflags=-G=3 generics.go
# command-line-arguments
./generics.go:7:6: internal compiler error: Cannot export a generic function (yet): Print

Please file a bug report including a short program that triggers the error.
https://golang.org/issue/new
```

竟然报错了,煎鱼你翻车了是吧...

根据错误提示可得知,是还没实现导出一个通用函数的功能。那样我们只需要把 `Print` 方法改为 `print`,再执行就可以了。

再次执行后的输出结果:

```
你好, 脑子进了煎鱼
123
```
成功输出了不同类型的值。

## 更多的案例

在 GitHub 有个小伙伴 mattn 整理了完整的泛型使用案例后开源了,可以实际下载使用看看:

![github.com/mattn/go-generics-example](https://files.mdnice.com/user/3610/a3e768b4-ca68-448d-b4b8-98a3fce447da.png)

大家根据上面的介绍来实际使用就可以达到运行泛型的效果了,GitHub 地址是:github.com/mattn/go-generics-example。

## 总结

经过多年的折腾,Go 语言在发布的 1.17 版本中已经包含了泛型的功能。将会在 Go1.18 正式宣发泛型,我们将会是Go 历史新阶段的见证者。

为什么?因为随着 Go1.18 的逼近,我们将会将会见到越来越多的新工具支持和变更,甚至会改变不少 Go 工程的写法。

欢迎大家在评论区分享你的看法!

================================================
FILE: content/posts/go/117-module-pruning-lazy.md
================================================
---
title: "Go1.17 新特性:对 Go 依赖管理的一把大剪刀"
date: 2021-12-31T12:55:03+08:00
toc: true
images:
tags: 
  - go
---

大家好,我是煎鱼。

不得不说。我可是个经历过 Go 依赖管理群魔乱舞,Go modules 迁移一堆 BUG 的人儿,难顶...
为此当年我写了不少技术文章,希望给大家避坑。

如下:
- [Go Modules 终极入门](https://mp.weixin.qq.com/s/6gJkSyGAFR0v6kow2uVklA)
- [干货满满的 Go Modules 知识分享](https://mp.weixin.qq.com/s/uUNTH06_s6yzy5urtjPMsg)
- [Go1.16 新特性:Go mod 的后悔药,仅需这一招](https://mp.weixin.qq.com/s/0g89yj9sc1oIz9kS9ZIAEA)

在近期 Go1.17 发布后,Go modules 带来了两大更新,煎鱼摩拳擦掌,他们分别是:
- 模块依赖图裁剪(module graph pruning)
- 延时模块加载(lazy module loading)

今天带大家一起来了解这两块内容,争取了解其为何物,背景又是什么。

## 背景

在日常的 Go 工程开发中,不知道你有没有遇到过 Go modules 的一个奇怪的点。大家没说,就以为是正确的,默认就接受了。

引用官方的 [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") 的案例来说,就是假设我们在代码中:
- main module 是 lazy。其导入了 module A 的 package x,package x 又导入了 module B。
- main module lazy 也相当于同时导入了 module A 的 package y。
- module A 的 package y 又导入了 module C 的 package z。

关联如下图所示:

![module 关联图示](https://image.eddycjy.com/d535e2d41648a346c08c41ac38eff6b9.jpg)

这个 Go 程序如果运行起来,会发生什么情况呢?**在 Go1.17 以前,如果你不存在 module C 的 package z**,程序在编译构建的时候就会报错,提示找不到。

实际上 module C 的 package z 并没有**对你主程序有任何建设意义**,俗话来讲就是 “占着茅坑不拉屎”。

他只是因为 main module 在导入 module A 时,也被 “间接” 导入了 package y 的依赖,也就是我们常看到的 go.mod 文件中的 “indirect” 标识,他们会导致构建失败,让人直呼无奈。

## Go1.17 module 改进

显然,社区反馈希望避免看到 “不相关” 的传递依赖等,也因此有了 Go1.17 的 module 改造。

![](https://files.mdnice.com/user/3610/e69f5155-3334-462f-b021-bee477c62c49.png)

接下来的 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")》的内容、案例来进行说明和介绍。

### module graph pruning

第一个改进就是模块依赖图裁剪(module graph pruning),这是这个版本 module 优化的基础。

在 Go1.17 以前,只要该项目的 go.mod 文件分析出来你存在间接的依赖,如果你没有安装过该依赖,就会出现报错。

错误提示如下:

```golang
$ go build
go: example.com/a@v0.1.0 requires
 example.com/c@v0.1.0: missing go.sum entry; to add it:
 go mod download example.com/c
```

这个时候我们都会默默地去安装一遍...没想过这是间接依赖,和我们的程序没一点直接的代码关系。

在 Go1.17 及之后就变了,go.mod 文件如下,会存在 2 块 require 代码块:

```golang
module example.com/lazy

go 1.17

require example.com/a v0.1.0

require example.com/b v0.1.0 // indirect
...
```

这就是区别,第一块的 require 我们眼熟,那分拆出来的第二块 require 的是什么呢?

这就是那些模块的间接依赖(常见到的 indirect 标识依赖)。可以理解为像是其他语言的 xxx.lock 文件一样的存在。

此处分析出来的间接依赖,将会不会像以前一样阻碍编译构建,只会真正有使用到的才会进行识别。

### lazy module loading

第二个改进是延时模块加载(lazy module loading),是基于模块依赖图裁剪(module graph pruning)的场景上的进一步优化。

也就是以往那些没被使用到的,但又间接依赖的模块。在 Go1.17 及以后不会被 Go 命令读取和加载,只有真正需要的时候才会加载。

### 副作用

Go module 依赖图裁剪也带来了一个副作用,那就是 go.mod 文件 size 会变大。

在 Go 1.17 版本之后,每次 go mod tidy(当go.mod中的go版本为1.17时),Go 命令都会对 main module 的依赖做一次深度扫描(deepening scan)。

该操作将 main module 的所有直接和间接依赖都记录在 go.mod 中,考虑到内容较多,Go 1.17 将直接依赖和间接依赖分别放在两个不同的 require 代码块中。

也就是上文所见到的内容。

## 总结

自从 Go 语言推出 Go modules 依赖,module 一直不断地在优化和改进。虽然看上去已经越来越好用了,但依然似乎存在不少问题。

就拿本次变更来讲,我也是在好朋友的 Go 微信群中看到提问,才思考了起来。因为大家看到第二块 require 时,虽然知道是间接依赖的包,但更明确,为什么要单独出来?

大家其实是不大理解的,本次变更也可能存在语义不清,不够明确的情况。但无论如何,后续我们可以继续观察。

================================================
FILE: content/posts/go/117-performance.md
================================================
---
title: "Go1.17 新特性,凭什么提速 5~10%?"
date: 2021-12-31T12:55:04+08:00
toc: true
images:
tags: 
  - go
---

大家好,我是煎鱼。

在 Go1.17 发布后,我们惊喜的发现 Go 语言他又又又优化了,编译器改进后产生了约 5% 的性能提升,也没有什么破坏性修改,保证了向前兼容。

![](https://files.mdnice.com/user/3610/c39ffb33-4afd-4537-a172-4919be7975a4.png)

他做了些什么呢,好像没怎么看到有人提起。为此今天煎鱼带大家来解读两新提案:
- 《[Proposal: Register-based Go calling convention](https://go.googlesource.com/proposal/+/master/design/40724-register-calling.md "Proposal: Register-based Go calling convention")》
- 《[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")》

本文会基于提案讲解和拆解,毕竟分享新知识肯定要从官方资料作为事实基准出发。

## 背景

在以往的 Go 版本中,Go 的调用约定简单且几乎跨平台通用,其原因在于选用了基于 Plan9 ABI 的堆栈调用约定,也就是**函数的参数和返回值都是通过堆栈上来进行传递**。

这里我们一共提到了 Plan9 和 ABI,这是两个很关键的理念:
- Plan9:Go 语言所使用的汇编器,Rob Pike 是贝尔实验室的猛人。
- ABI:Application Binary Interface(应用程序二进制接口),ABI 包含了应用程序在操作系统下运行时必须遵守的编程约定(例如:二进制接口)。

该方案的优缺点如下:
- 优点:实现简单,简化了实现成本。
- 缺点:性能方面付出了不少的代价。

按我理解,在 Go 语言初创时期,采取先简单实现,跑起来再说。也合理,性能倒不是一个 TOP1 需求。

## Go1.17 优化

### 什么是调用惯例

在新版本的优化中,提到了调用惯例(calling convention)的概念,指的是**调用方和被调用方对函数调用的共识约定**。

这些共识包含:函数的参数、返回值、参数传递顺序、传递方式等。

双方都必须遵循这个约定时,程序的函数才能正常的运行起来。如果不遵循,那么该函数是没法运行起来的。

### 优化是什么

在 Go1.17 起,正式将把 Go 内部 ABI 规范(在 Go 函数之间使用)从基于堆栈的函数参数和结果传递的方式**改为基于寄存器的函数参数和结果传递**。

本次修改涉及到的项非常多,该优化是持续的,原本预计是 Go1.16 实现,不过拖到了 Go1.17。

![](https://files.mdnice.com/user/3610/41e7a047-cac2-4b8a-9006-762fd73ee2a4.png)

目前实现了 amd64 和 arm64 架构的支持。还有不少的更多的支持会持续在 Go1.18 中完成,具体进度可见 [issues #40724](https://github.com/golang/go/issues/40724 "issues #40724")。

### 性能如何

在 [Go1.17 Release Notes](https://golang.org/doc/go1.17 "Go1.17 Release Notes") 中明确指出,用一组有代表性的 Go 包和程序的基准测试。

官方数据显示:
- Go 程序的运行性能提高了约 5%。
- Go 所编译出的二进制大小的减少约 2%。

在民间数据来看,在 [twitter](https://twitter.com/__Achille__/status/1431014148800802819 "twitter") 看到 @Achille 表示从 Go1.15.7 升级到 Go1.17 后显示。在一个大规模的数据处理系统上进行的 Go1.17 升级产生了惊人的效果,我们来看看他的真实数据。

CPU、Malloc 调用时间减少了约15%:

![图来自 @Achille](https://files.mdnice.com/user/3610/0c98d5c9-4691-4c39-92af-fe25fd41de25.png)


![图来自 @Achille](https://files.mdnice.com/user/3610/2ea79a65-297f-49aa-a97f-15d02dbf5c9b.png)


RSS 大小更接近于堆的大小:

![图来自 @Achille](https://files.mdnice.com/user/3610/14b1886f-7bd7-4ea2-9967-5272f5b09c79.png)

从原本的 1.6GB 降至 1GB。

结合官方和民间数据来看,优化效果是明确且有效的。有兴趣的小伙伴也可以自己测一测。

## 总结

在 Go1.17 这一个新版本中,只需要简单的升一升 Go 版本,我们就能得到一定的性能优化,这是非常不错的。

从以往的基于堆栈的函数参数和结果传递的方式改为 Go1.17~Go1.18 基于寄存器的函数参数和结果传递,Go 语言正在一步步走的更好!

你觉得呢?

================================================
FILE: content/posts/go/118-build-info.md
================================================
---
title: "Go1.18 新特性:编译后的二进制文件,将包含更多信息"
date: 2022-02-05T16:02:45+08:00
toc: true
images:
tags: 
  - go
---

大家好,我是煎鱼。

我有一个朋友,,开开心心入职,想着施展拳脚,第一个任务就是对老旧的二进制文件进行研究。

他一看,这文件,不知道是编译器用什么参数怎么打出来的,环境不知道是什么,更不知道来自什么代码分支?

这除了是项目流程上的问题外,Go 在这块也有类似的小问题,处理起来比较麻烦。

## 背景

日常中很难从 Go 二进制文件中检索元信息,要么是信息完全缺失,要么提取需要对二进制文件进行大量解析。

包含的元信息如下:

|  元信息   | 提取处  |
|  ----  | ----  |
| Go 构建版本  | 符号表,通过全局变量 `runtime.buildVersion` 来获取 |
| 构建信息,例如:模块和版本 | 符号表,通过全局变量 `runtime/debug.modinfo` 来获取 |
| 编译器选项,例如:构建模式、编译器、gcflags、ldflags 等 | 无法获取 |
| 用户定义的自定义数据,例如:应用程序版本等 | 需在编译时设置全局字符串变量,才可以获取 |

关注到编译器选项,也就是参数等都是无法得知的,也就是会提高获取如何编译出来的难度。

## 新提案

Michael Obermüller 提出了一个新的提案《[cmd/go: add compiler flags, relevant env vars to 'go version -m' output](https://github.com/golang/go/issues/35667)》用于解决上述问题。

在提案中想要的是 JSON 格式的结构输出:

```json
{
    "version": "go1.13.4",
    "compileropts": {
        "compiler": "gc",
        "mode": "pie",
        "os": "linux",
        ...
    },
    "buildinfo": {
        "path": "脑子进煎鱼了",
        "main": {
            "path": "HelloWorld",
            "version": "(devel)",
        },
        "deps": []
    },
    "user": {
        "customkey": "customval",
        ...
    }
}
```

Russ Cox 表示由于编译信息已有既有格式,并且默认使用 JSON 只会让二进制文件变得更大。好处少,没必要,改为了选项化的支持。

新的 Go1.18 版本中,可以通过既有的:

```
go version -m
``` 

查看到提案所提到的信息。

例如:

```go
$ gotip version
go version devel go1.18-eba0e866fa Mon Oct 18 22:56:07 2021 +0000 darwin/amd64
$ gotip build ./
$ gotip version -m ko
...
	build	compiler	gc
	build	tags	goexperiment.regabiwrappers,goexperiment.regabireflect,goexperiment.regabiargs
	build	CGO_ENABLED	true
	build	CGO_CPPFLAGS	
	build	CGO_CFLAGS	
	build	CGO_CXXFLAGS	
	build	CGO_LDFLAGS	
	build	gitrevision	6447264ff8b5d48aff64000f81bb0847aefc7bac
	build	gituncommitted	true
```

若需要输出 JSON 格式,也可以通过指定 `go version -json` 达到一样的效果。

在上面的输出中,现有的编译器选项等都会包含在内,能够让大家对整体编译后的二进制文件溯源有一个更好的认知。

## 总结

在今天这篇文章中,给大家介绍了 Go1.18 的一个新的变化。

新版本中,编译器选项/参数、相关环境变量等,将会包含在编译后的二进制文件中,能够更便于后人排查和查看信息。

================================================
FILE: content/posts/go/118-build.md
================================================
---
title: "泛型是双刃剑?Go1.18 编译会慢近 20%"
date: 2021-12-31T12:55:18+08:00
toc: true
images:
tags: 
  - go
---

大家好,我是煎鱼。

目前 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)》。

作者表示在 Go1.18 有了泛型后,编译速度将会变慢,虽然不意外,说明副作用还是有的,升级需谨慎。

以下为修整后概括的原文信息。

## 性能分析

这个测试主要是测试 Go 泛型对 Go 编译器带来的影响,并没有输入大量的测试用例,是最简单的比较,仅代表大部分的差异。

比较的内容是 Go 泛型的 -G=0 和 -G=3 模式下的编译时间。

分别代表以下含义:
- -G=0 模式:默认不打开泛型的模式。
- -G=3 模式:打开泛型的模式。

Go 1.18 中的 -G=0 模式和 Go 1.17 模式的比较显示,由于非泛型的变化,编译器的速度可能降低了~1%(因为 -G=0 模式不支持泛型)。

Go 1.18 的编译时间可能比 Go 1.17 慢 15-18%,这主要是由于实现泛型所带来的变化,也就是 Go1.18 开启泛型下,编译时间会变慢。

## 差异在哪

大部分的差异是由于新的编译器前端处理,因为 SSA 后端对于泛型完全没有变化。

- 在 -G=0 模式下(用于 Go 1.18 之前的所有编译器):有一个语法分析器,创建 ir.Node 节点树的 noder 阶段,以及标准类型检查器。
- 在 -G=3 模式下:有相同的语法分析器,但程序首先由 types2(支持泛型)进行类型检查。

在通过 -G=3 模式打开泛型后,会有一个 noder2 阶段,使用语法信息和 types2 类型检查器的类型信息创建 ir.Node 节点树。在一次运行中,noder+ types1-typechecking 的开销总和约为 4%,而 types2-typechecker+noder2 的总和为 14%。

可以看到大部分的速度下降是由于改变了编译前端处理(并不意外)。

## 总结

可以明确的是,在打开泛型后,Go1.18 编译时间可能会慢 15-18%,Go 官方将计划在 Go 1.19 中减少这种额外的开销。

泛型的双刃剑初见,后续不管是编译时间、执行时间(预计不会减缓)、泛型的滥用、最佳实践等,都值得我们去讨论和关注。

欢迎大家在评论区讨论和交流:)

================================================
FILE: content/posts/go/118-constraints.md
================================================
---
title: "Go 新语法挺丑?最新的泛型类型约束介绍"
date: 2021-12-31T12:55:19+08:00
toc: true
images:
tags: 
  - go
---

大家好,我是煎鱼。

近期我们在分享《[3 件与 Go 开发者有关的大小事](https://mp.weixin.qq.com/s/22TeQOUjf_glPX3QLPX8yw)》时,里面有一部分有提到 Go 泛型的约束类型语法又又又变了。

在评论区里看到不少的读者朋友大呼泛型的新类型约束语法挺丑,不如原本的好...

如下:

![](https://files.mdnice.com/user/3610/8e734a8f-612c-41c3-a0cd-8a5fe6eea483.png)

为此,今天煎鱼就带大家来看看,为什么会发生泛型的新语法会这种改变?

## 问题背景

原本 @Ian Lance Taylor 设计的的泛型类型关键字如下:

```golang
type T interface {
 type int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, complex64, complex128, string
}
```
看起来好像非常 “顺眼”。但在《[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")》中社区进行了热烈的讨论。

认为该类型约束的关键字,过于 “模棱两可”。像是 @Damien Neil 所提出的以下两个例子。

结构体的例子:

```golang
package p
type mustIncludeDefaultCase struct{}
type MySum interface {
  type int, float64, mustIncludeDefaultCase
}
```

不明确的点之一,如果类型列表包含一个未导出的类型,那又应该是如何处理呢?

接口的例子:

```golang
type T interface { type int16, int32 }
func main() {
  var x T
  switch x.(type) {
  case int16:
  case int32:
  } 
}
```

你认为程序会跑进哪个 switch-case 的代码块里呢,是 int16,还是 int32?

不,都不会,变量 x 是 nil,如此迷惑。

在社区讨论中,发现设计与真实场景一结合,发现这个类型规则在普通的接口类型、在约束中使用也太微妙了。

用类型列表嵌入接口时的行为也很奇怪。认为可以做的更好,那就是 “更显式”。

## 新提案

为此,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")》。

其包含三个新的、更简单的想法来取代泛型提案中定义的类型列表。

### 关键名词

新语法在泛型处增加一个新概念:接口元素(interface elements),用作约束条件的接口类型,或者被嵌入约束条件的接口类型,允许嵌入一些额外的构造。

被嵌入的可以是:
- 任何类型,而不仅仅是一个接口类型。
- 一个新的句法结构,称为近似元素。
- 一个新的句法结构,称为联合元素。

重点名词,我们继续展开讲解,分别是:
- 嵌入约束。
- 近似元素。
- 联合元素。
- 接口类型。

### 联合元素

原先的语法中,类型约束会用逗号分隔的方式来展示。

如下:
```golang
type int, int8, int16, int32, int64
``` 

在新语法中,结合定义为 union element(联合元素),写成一系列由竖线 ”|“ 分隔的类型或近似元素。

如下:

```golang
type PredeclaredSignedInteger interface {
	int | int8 | int16 | int32 | int64
}
```

常常会和下面讲到的近似元素一起使用。

### 近似元素

新语法,他的标识符是 “~”,完整用法是 `~T`。`~T` 是指底层类型为 T 的所有类型的集合。例如:

```golang
type AnyInt interface{ ~int }
```

他的类型集是 `~int`,也就是所有类型为 int 的类型(如:int、int8、int16、int32、int64)都能够满足这个类型约束的条件。

再结合以上的分隔来使用,用法为:

```golang
type SignedInteger interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64
}
```

相当于泛型提案中使用的以下类型:

```golang
interface {
	type int, int8, int16, int32, int64
}
```

新语法只需借助近似标识符 `~int` 来表达就可以了,更明确的表示了近似匹配,而不是存在隐式理解。

### 嵌入约束

一个类型约束可以嵌入另一个约束,联合元素可以包括约束。

例如:

```golang
// Signed is a constraint whose type set is any signed integer type.
type Signed interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64
}

// Unsigned is a constraint whose type set is any unsigned integer type.
type Unsigned interface {
	~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}

// Float is a constraint whose type set is any floating point type.
type Float interface {
	~float32 | ~float64
}

// Ordered is a constraint whose type set is any ordered type.
// That is, any type that supports the < operator.
type Ordered interface {
	Signed | Unsigned | Float | ~string
}
```

这个很好理解,就是正式支持嵌入约束了。

### 接口类型(联合约束元素)

在联合元素中,使用接口类型的话。将会把该类型集添加到联合中。

例如:

```golang
type Stringish interface {
	string | fmt.Stringer
}
```

Stringish 的类型集将是字符串类型和所有实现 `fmt.Stringer` 的类型,任何这些类型(包括 `fmt.Stringer` 本身)将被允许作为这个约束的类型参数。
 
也就是针对接口类型做了特殊的处理。

## 总结

今天这篇文章,主要讲解了 Go 泛型的新语法,其实本质上还是为了解决引入 A 后,出现了 BCD 新问题,又继续引入新的语法和模式来解决。

整体就是三个观点:

- 显式匹配:使用**明确的 "~" 近似元素,澄清了何时在底层类型上进行匹配**。
- 明确含义:**使用 “|” 而不是 “,” 强调这是一个元素的联合**。
- 嵌套优化:通过允许约束嵌入非界面元素,类型关键字可以被省略。

这就是用这两个新语法符号的原因,被嫌丑的新语法标识符 “|” 和 “~” ,其实也是在 issues 的大范围讨论中,由社区贡献出来的。

算是有利有弊?**你的看法如何,欢迎在评论区留言**:)


================================================
FILE: content/posts/go/118-cut.md
================================================
---
title: "Go1.18 新特性:新增好用的 Cut 方法"
date: 2022-02-05T16:03:31+08:00
toc: true
images:
tags: 
  - go
---

大家好,我是煎鱼。

在各种写业务代码的时候,大家会常常要处理字符串的内容。常见的像是用邮箱登陆账号,如果是:eddycjy@gmail.com,那就得根据 @ 来切割,分别取出前和后,来识别用户名和邮箱地址。

这种需求,在 Go 里写起来方便吗?今天就由煎鱼带大家了解。

## 背景

### 重复代码

无独有偶,Ainar Garipov 在许多项目中遇到了前面我们所提的切割需求。

例如:

```go
idx = strings.Index(username, "@")
if idx != -1 {
  name = username[:idx]
} else {
  name = username
}  
```

又或是:

```go
idx = strings.LastIndex(address, "@")
if idx != -1 {
  host = address[idx+1:]
} else {
  host = address
}
```

经常要反复写一些繁琐的代码,提案提出者表示不愉快。

## 新提案

### 实施内容

建议新增 Cut 方法到 strings 标准库:

```go
func Cut(s, sep string) (before, after string, found bool) {
	if i := Index(s, sep); i >= 0 {
		return s[:i], s[i+len(sep):], true
	}
	return s, "", false
}
```

同步也要在 bytes 标准库:  

```go
func Cut(s, sep []byte) (before, after []byte, found bool)
```

这样一来,就可以从原本的:

```go
	eq := strings.IndexByte(rec, '=')
	if eq == -1 {
		return "", "", s, ErrHeader
	}
	k, v = rec[:eq], rec[eq+1:]
```

变成:

```go
	k, v, ok = strings.Cut(rec, "=")
	if !ok {
		return "", "", s, ErrHeader
	}
```

写法上会更优雅,在复杂的场景下会更具可读性和抽象级别。

### 接受原因

可能就有小伙伴会吐槽了,Go 居然只为了节省 1 行代码,就搞了个新函数,这还是大道至简吗?

实际上,在官方团队(Russ Cox)介入后,他对 Go 主仓库进行了分析,搜索了相关类似函数的使用:
- strings.Index。
- strings.IndexByte。
- strings.IndexRune。

统计后,转换为了可以使用 `strings.Cut` 的用法,在例子和测试数据之外有 311 个索引调用。

排除了一些确实不需要的,剩下 285 个调用。在这些调用中,有 221 次是最好写成 Cut 方法的,能更优雅。

![](https://files.mdnice.com/user/3610/a1a61fd3-1ca0-448a-b503-551433635992.png)

也就是说,有现有的 Go 代码中,有 77% 可以用新增的 Cut 函数写得更清楚,可读性和抽象可以做得更好。

Go 主仓库确实存在如此重复的代码,他认为这也是非常不可思议的!

## 总结

Go1.18 的新特性中,Cut 虽然只是新增了一个方法,看上去无伤大雅。

但类似 Cut 方法的用法,在 Go 的主版本中其实已经被发明了两次。

该新方法的出现,可以同时取代并简化四个不同的标准库函数:Index、IndexByte、IndexRune 和 SplitN 中的绝大部分用法。
 
由于这些原因,最终将 Cut 添加到标准库中。

你觉得怎么样?:)

## 参考
- [bytes, strings: add Cut](https://github.com/golang/go/issues/46336)

================================================
FILE: content/posts/go/118-leader-generics.md
================================================
---
title: "回归现实:Go Leader 对 1.18 泛型的期望"
date: 2021-12-31T12:55:16+08:00
toc: true
images:
tags: 
  - go
---

大家好,我是煎鱼。

前段时间根据 Go 泛型的最新动态,我写了一篇《[出泛型后 API 怎么办?Go 开发者要注意了](https://mp.weixin.qq.com/s/yWEM2EAwv80ZUFjbKGtpNA)》文章,引发了不少小伙伴的热议。

Go 核心开发团队的现任 Leader 
@Russ Cox 在 golang-dev 中正式发表《[expectations for generics in Go 1.18](https://groups.google.com/g/golang-dev/c/iuB22_G9Kbo)》对 Go 泛型给出了 “期待”,可以认为是后续的计划了。


![](https://files.mdnice.com/user/3610/e4693268-eff4-4dd3-ab6b-e4c41d8728cc.png)


如果不出现严重的问题,Go 1.18 将会包括对泛型的支持,并且这次泛型的支持将会是有史以来最大的一次语言变化,对以下几点有顾虑:
- 最佳实践.
- 生产经验
- 兼容性承诺。

接下来,煎鱼带大家一起了解 Russ Cox 发表的 Go 泛型进程,知悉官方一手消息。

## 最佳实践

Go 团队表示不知道使用泛型的最佳实践是什么,所以给出的官方文档将无法就何时使用泛型和何时不使用泛型给出精确、明确的答案,只可以给出粗略的指导。

此处可以参考《Effective Go》的最初版本,是在不间断地写了一整年的 Go 代码后,才正式输出的。

![](https://files.mdnice.com/user/3610/034ab5db-f125-450d-bc97-9ea42ba94022.png)

按照现有的计划,官方只会提供关于如何使用泛型的文档,暂时无法提供任何关于风格、最佳实践的规定性文档。

在提供的标准库上,先是已经通过提案的 maps 和 slices库会先放到 golang.org/x/exp 中作为实验,不会保证向后兼容。待成熟后,再推广到标准库中。

![](https://files.mdnice.com/user/3610/b1d967a2-2ac7-4077-be2e-4d1c4d3565cb.png)

可以明确,Go 泛型出来后,社区就会陆续开始百花齐放,接着有官方输出推荐方法了,历史是如此的相似。

## 生产经验

目前 Go 团队没有关于泛型的生产经验,因此会在文档中给出明确提示,让大家在生产中使用泛型的时候应该适当谨慎。

泛型出来后,会陆续涉及到大量的重写工作,但是由于现在处于中间阶段。正在重写的 Go 1.18 工具链去同时适配泛型、非泛型代码是需要时间和经验的,有一定的风险。

因此泛型出来后,可能会出现一些意想不到的问题,仅在生产发现(教训)。

## 兼容性承诺

Go1.18 会和其他 Go1.x 版本一样,保证向后兼容的承诺:不会破坏用 Go 1.18 构建的代码,包括使用泛型的代码。

如果是最坏的情况,如果发现 Go 1.18 的语义有一些致命的问题,并需要改变它们(例如:在Go 1.19 中)。

将会使用 go.mod 文件的 go 行来确定该模块中的源文件是期待 Go 1.18 还是 Go 1.19+ 语义,以此实现版本控制。但目前来看,不需要这样做。

也建议急于使用 Go 泛型的开源库作者,做好泛型和非泛型版本代表的支持和隔离,这样对用户会更加的友好。

## 总结

可以明确的是,Go 泛型的整体推进方案,在这篇文章中均已说明。Go 官方团队也与许多第三方工具的作者进行沟通。

第三方工具可能不会在 Go 1.18 发布时就完全支持泛型,这会由各作者自行根据自己的时间表来更新。

煎鱼猜测推进节奏就是:
1. 支持泛型语法。
2. 观察。
3. 推进标准/工具库。
4. 逐步替换。
5. 修 BUG。
6. 观察、优化
7. 生产可用。

大概需要 2~3 个 Go 版本,需要 1~2 年,Go 泛型的各类配套组件就会基本完善,可用,转为持续优化了。

**你对 Go 官方的推进计划此怎么看呢**,欢迎在评论区留言和交流!

================================================
FILE: content/posts/go/118-module.md
================================================
---
title: "Go1.18 新特性:多 Module 工作区模式"
date: 2022-02-05T16:00:00+08:00
toc: true
images:
tags: 
  - go
  - go1.18
---

大家好,我是煎鱼。

Go 的依赖管理,也就是 Go Module。从推出到现在,也已经有了一定的年头了,吐槽一直很多,官方也不断地在进行完善。

Go1.18 将会推出一个新特性:Multi-Module Workspaces,用于支持 Module 多工作区,能解决以往的一系列问题。

今天将由煎鱼带大家一起深入学习。

## 背景

在日常使用 Go 工程时,总会遇到 2 个经典问题,特别的折腾人。

如下:
1. 依赖本地 replace module。
2. 依赖本地未发布的 module。

### replace module

第一个场景:像是平时在 Go 工程中,我们为了解决一些本地依赖,或是定制化代码。会在 go.mod 文件中使用 replace 做替换。

如下代码:

```go
replace golang.org/x/net => /Users/eddycjy/go/awesomeProject
```

这样就可以实现本地开发联调时的准确性。

问题就在这里:
- 本地路径:所设定的 replace 本质上转换的是本地的路径,也就是每个人都不一样。
- 仓库依赖:文件修改是会上传到 Git 仓库的,不小心传上去了,影响到其他开发同学,又或是每次上传都得重新改回去。

用户体验非常差,很折腾人。

### 未发布的 module

第二个场景:在做本地的 Go 项目开发时,可能会在本地同时开发多个库(项目库、工具库、第三方库)等。

如下代码:

```go
package main

import (
    "github.com/eddycjy/pkgutil"
)

func main() {
    pkgutil.PrintFish()
}
```

如果这个时候运行 `go run` 或是 `go mod tidy`,都不行,会运行失败。

报如下类似错误:

```
fatal: repository 'https://github.com/eddycjy/pkgutil/' not found
```

这个问题报错是因为 `github.com/eddycjy/pkgutil` 这个库,在 GitHub 是没有的,自然也就拉取不到。

解决方法:在 Go1.18 以前,我们会通过 replace(会遇到背景一的问题),又或是直接上传到 Github 上,自然也就能被 Go 工具链拉取到依赖了。

许多同学会发出灵魂质疑:Go 的依赖都必须要上传到 GitHub 吗,强绑定?

对新入门的同学非常不友好,很要命。

## 工作区模式

在社区的多轮反馈下,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 正式落地。

新提案的一个核心概念,就是增加了 `go work` 工作区的概念,针对的是 Go Module 的依赖管理模式。

其能够在本地项目的 go.work 文件中,通过设置一系列依赖的模块本地路径,再将**路径下的模块组成一个当前的工作区**,他的读取优先级是最高的。

我们可以通过 `go help` 来查看,如下:

```
$ go1.18beta1 help work
Usage:

	go work <command> [arguments]

The commands are:

	edit        edit go.work from tools or scripts
	init        initialize workspace file
	sync        sync workspace build list to modules
	use         add modules to workspace file

Use "go help work <command>" for more information about a command.
```

只要执行 `go work init` 就可以初始化一个新的工作区,后面跟的参数就是要生成的具体子模块 mod。

命令如下:

```go
go work init ./mod ./tools
```

项目目录如下:

```
awesomeProject
├── mod
│   ├── go.mod      // 子模块
│   └── main.go
├── go.work         // 工作区
└── tools
    ├── fish.go
    └── go.mod      // 子模块
```

生成的 go.work 文件内容:

```
go 1.18

use (
    ./mod 
    ./tools
)
```

新的 go.work 与 go.mod 语法一致,也可以使用 replace 语法:

```
go 1.18

use (...)

replace golang.org/x/net => example.com/fork/net v1.4.5
```

go.work 文件内共支持三个指令:
- go:声明 go 版本号,主要用于后续新语义的版本控制。
- use:声明应用所依赖模块的具体文件路径,路径可以是绝对路径或相对路径,可以在应用命目录外均可。
- replace:声明替换某个模块依赖的导入路径,优先级高级 go.mod 中的 replace 指令。

若想要禁用工作区模式,可以通过 `-workfile=off` 指令来指定。

也就是在运行时执行如下命令:

```
go run -workfile=off main.go

go build -workfile=off
```

go.work 文件是不需要提交到 Git 仓库上的,否则就比较折腾了。

只要你在 Go 项目中设置了 go.work 文件,那么在运行和编译时就会进入到工作区模式,会优先以工作区的配置为最高优先级,来适配本地开发的诉求。

至此,工作区的核心知识就介绍完毕了。

## 总结

今天给大家介绍了 Go1.18 的新特性:多 Module 工作区模式。其本质上还是为了解决本地开发的诉求。

由于 go.mod 文件是与项目强关联的,基本都会上传到 Git 仓库中,很难在这上面动刀子。直接就造了个 go.work 出来,纯本地使用,方便快捷。

使用新的 go.work,就可以在完全是本地文件上各种捣鼓了,不会对其他成员开发有其他影响。

你觉得怎么样呢?:)


================================================
FILE: content/posts/go/4errors.md
================================================
---
title: "大家对 Go 错误处理的 4 个误解!"
date: 2021-12-31T12:55:04+08:00
draft: true
images:
tags: 
  - go
---

大家好,我是煎鱼。

Go 语言中错误处理的机制一直是各大 Gopher 热议的问题。甚至一直有人寄望 Go 支持 `throw` 和 `catch` 关键字,实现与其他语言类似的特性。

今天煎鱼带大家了解几个 Go 语言的错误处理中,大家最关心的问题:
1. 为什么不支持 try-catch?
2. 为什么不支持全局捕获的机制?
3. 为什么要这么设计错误处理?
4. 未来的错误处理机制会怎么样?

## 落寞的 try-catch

在 Go1 时,大家知道基本不可能支持。于是打起了 Go2 的主意。为什么 Go 就不能支持 try-catch 组合拳?

上一年宣发了 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")》的提案。

下面来自该提案的演示,Go1 的错误处理: 

```golang
type data struct {}

func (d data) bar() (string, error) {
    return "", errors.New("err")
}

func foo() (data, error) {
    return data{}, errors.New("err")
}

func do () (string, error) {
    d, err := foo()
    if err != nil {
        return "", err
    }

    s, err := d.bar()
    if err != nil {
        return "", err
    }

    return s, nil
}
```

新提案所改造的方式:

```golang
type data struct {}

func (d data) bar() string {
    throw "", errors.New("err")
}

func foo() (d data) {
    throw errors.New("err")
    return
}

func do () (string, error) {
    catch err {
        return "", err 
    }

    s := foo().bar()
    return s, nil
}
```

不过答复非常明确,@davecheney 在底下回复“以最强烈的措辞,不(In the strongest possible terms, no)”。这可让人懵了圈,为什么这么硬呢?

其实 Go 官方早在《[Error Handling — Problem Overview](https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md "Error Handling — Problem Overview")》提案早已明确提过,Go 官方在**设计上会有意识地选择使用显式错误结果和显式错误检查**。

结合《[language: Go 2: error handling meta issue](https://github.com/golang/go/issues/40432 "language: Go 2: error handling meta issue")》可得知,要拒绝 try-catch 关键字的主要原因是:
- 会**涉及到额外的流程控制**,因为使用 try 的复杂表达式,会导致函数意外返回。
- 在表达式层面上没有流程控制结构,只有 panic 关键字,它不只是从一个函数返回。

说白了,就是设计理念不合,加之实现上也不大合理。在以往的多轮讨论中早已被 Go 团队拒绝了。

反之 Go 团队倒是一遍遍在回答这个问题,已经不大耐烦了,直接都整理了 issues 版的 FAQ 了。

## 想捕获所有 panic

在 Go 语言中,有一个点,很多新同学会不一样碰到的。那就是在 goroutine 中如果 panic 了,没有加 recover 关键字(有时候也会忘记),就会导致程序崩溃。

又或是以为加了 recover 就能保障一个 goroutine 下所派生出来的 goroutine 所产生的 panic,一劳永逸。

但现实总是会让人迷惑,我经常会看到有同学提出类似的疑惑:

![来自 Go 读者交流群](https://files.mdnice.com/user/3610/8e3127ef-7cbe-4952-8780-f8812f921f45.png)

这时候,有其他语言经验的同学中,又有想到了一个利器。能不能设置一个全局的错误处理 handler。

像是 PHP 语言也可以有类似的方法:

```
set_error_handler();
set_exception_handler();
register_shutdown_function();
```

显然,Go 语言中并没有类似的东西。归类一下,我们聚焦以下两个问题:
1. 为什么 recover 不能捕获更上层的 panic?
2. 为什么 Go 没有全局的错误处理方法?

### 源码层面

如果是讲设计的话,其实只是通过 Go 的 GMP 模型和 defer+panic+recver 的源码剖析就能知道了。

![](https://files.mdnice.com/user/3610/42998ba0-84cc-4fe5-b88d-b9400dd1698b.png)

本质上 defer+panic 都是挂载在 G 上的,可查看我以前写的《[深入理解 Go panic and recover](https://eddycjy.com/posts/go/panic/2019-05-21-panic-and-recover/ "深入理解 Go panic and recover")》,你会有更多深入的理解。

### 设计思想

在本文中我们不能仅限于源码,需要更深挖,Go 设计者他的思想是什么,为什么就是不支持?

在 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") 》分别的针对性探讨过上述问题。

Go 团队的大当家 @Russ Cox 给出了明确的答复:**Go 语言的设计立场是错误恢复应该在本地完成,或者完全在一个单独的进程中完成**。

![](https://files.mdnice.com/user/3610/cb6fac34-ba12-40fd-98d3-f177e00ee39f.png)

这就是为什么 Go 语言不能跨 goroutines 从 panic 中恢复,也不能从 throw 中恢复的根本原因,是**语言设计层面的思想所决定**。

在源码剖析时,你所看到的整套 GMP+defer+panic+recover 的机制机制,就是跟随着这个设计思想去编写和发展的。

设计思想决定源码实现。

### 建议方式

从 Go 语言层面去动摇这个设计思想,目前来看可能性很低。至少 2021 年的现在没有看到改观。

整体上会建议提供公共的 Go 方法去规避这种情况。参考 issues 所提供的范例如下:

```golang
recovery.SafeGo(logger, func() {
              method(all parameters)
	})

func SafeGo(logger logging.ILogger, f func()) {
	go func() {
		defer func() {
			if panicMessage := recover(); panicMessage != nil {
				...
			}
		}()

		f()
	}()
}
```

是不是感觉似曾相识?

每家公司的内部库都应该有这么一个工具方法,规避偶尔忘记的 goroutine recover 所引发的奇奇怪怪问题。

也可以参照建议,利用一个单独的进程(Go 语言中是 goroutine)去统一处理这些 panic,不过这比较麻烦,较少见。

## 未来会如何

Go 社区对 Go 语言未来的错误处理机制非常关心,因为 Go1 已经米已成炊,希望在 Go2 上解决错误处理机制的问题。

期望 Go2 核心要处理的包含如下几点(#40432):

1. 对于 Go2,我们希望使错误检查更加轻便,减少专门用于错误检查的 Go 程序代码的数量。我们还想让写错误处理更方便,减少程序员花时间写错误处理的可能性。
2. 错误检查和错误处理都必须保持明确,即在程序文本中可见。我们不希望重复异常处理的陷阱。
3. 现有的代码必须继续工作,并保持与现在一样的有效性。任何改变都必须与现有的代码相互配合。

为此,许多人提过不少新的提案...很可惜,截止 2021.08 月底为止,有许多人试图改变语言层面以达到这些目标,但没有一个新的提案被接受。

现在也有许多变更并入 Go2 提案,主要是 error-handling 方面的优化。

大家有兴趣可以看看我之前写的:《[先睹为快,Go2 Error 的挣扎之路](https://mp.weixin.qq.com/s/XILveKzh07BOQnqxYDKQsA)》,相信能给你带来不少新知识。

## 总结

看到这里,我们不由得想到。为什么,为什么在 21 世纪前者已经有了这么多优秀的语言,Go 语言的错误处理机制依然这么的难抉择?

显然 Go 语言的开发团队是有自己的设计哲学和思想的,否则 “less is more” 也不会如此广泛流传。

这存在着一系列既要也要的问题。欢迎大家关注煎鱼,后续我们也可以面向 Go 后续的错误处理持续的关注和讨论!

================================================
FILE: content/posts/go/again-mutex.md
================================================
---
title: "Go 为什么不支持可重入锁?"
date: 2021-12-31T12:55:24+08:00
toc: true
images:
tags: 
  - go
---

大家好,我是煎鱼。

程序里的锁,是很多小伙伴在写分布式应用时用的最多的一个利器之一。

使用 Go 的同学里,绝大部分都有其他语言的经验,就会对其中一点有疑惑,那就是 **Go 里的锁,竟然不支持可重入**?

为此,今天煎鱼带大家一起来了解这里的设计考量,看看为什么。

## 可重入锁

如果对已经上锁的普通互斥锁进行 “加锁” 操作,其结果要么失败,要么会阻塞至解锁。

锁的场景如下:
- 在加锁上:如果是可重入互斥锁,当前尝试加锁的线程如果就是持有该锁的线程时,加锁操作就会成功。
- 在解锁上:可重入互斥锁一般都会记录被加锁的次数,只有执行相同次数的解锁操作才会真正解锁。

简单来讲,可重入互斥锁是互斥锁的一种,同一线程对其多次加锁不会产生死锁,又或是导致阻塞。

不同语言间实现可能或多或少有些区别,但大体意思差不多。

请你想一下,Go 是怎么样的呢?

## Go 支持情况

我们看到以下这个 Go 互斥锁例子:

```golang
var mu sync.Mutex

func main() {
	mu.Lock()
	mu.Lock()
}
```

这段 Go 程序会阻塞吗?不会,会报以下错误:

```
fatal error: all goroutines are asleep - deadlock!
```

Go 显然是不支持可重入互斥锁的。

## 官方回复

### Go 设计原则

在工程中使用互斥的根本原因是:为了保护不变量,也可以用于保护内、外部的不变量。

基于此,Go 在互斥锁设计上会遵守这几个原则。如下:
- 在调用 `mutex.Lock` 方法时,要保证这些变量的不变性保持,不会在后续的过程中被破坏。
- 在调用 `mu.Unlock` 方法时,要保证:
    - 程序不再需要依赖那些不变量。
    - 如果程序在互斥锁加锁期间破坏了它们,则需要确保已经恢复了它们。

### 不支持的原因

讲了 Go 自己的设计原则后,那为什么不支持可重入呢?

其实 Russ Cox 于 2010 年在《[Experimenting with GO](https://groups.google.com/g/golang-nuts/c/XqW1qcuZgKg/m/Ui3nQkeLV80J "Experimenting with GO")》就给出了答复,认为递归(又称:重入)互斥是个坏主意,这个设计并不好。

我们可以结合官方的例子来理解。

如下:

```golang
func F() {
        mu.Lock()
        ... do some stuff ...
        G()
        ... do some more stuff ...
        mu.Unlock()
}

func G() {
        mu.Lock()
        ... do some stuff ...
        mu.Unlock()
}
```

在上述代码中,我们在 `F` 方法中调用 `mu.Lock` 方法加上了锁。如果支持可重入锁,接着就会进入到 `G` 方法中。

此时就会有一个致命的问题,你**不知道 `F` 和 `G` 方法加锁后是不是做了什么事情**,从而导致破坏了不变量,毕竟随手起几个协程做点坏事,也是完全可能的。

这对于 Go 是无法接受的,可重入的设计**违反了前面所提到的设计理念**,也就是:“要保证这些变量的不变性保持,不会在后续的过程中被破坏”。

基于上述原因,Go 官方团队选择了没有支持该项特性。

## 总结

Go 互斥锁没有支持可重入锁的设计,也是喜欢的大道至简的思路了,可能的干扰比较多,不如直接简单的来。

你在工作过程中有没有类似的疑惑呢,欢迎大家在评论区留言和交流:)

================================================
FILE: content/posts/go/annotation.md
================================================
---
title: "Go:我有注解,Java:不,你没有!"
date: 2021-12-31T12:55:11+08:00
toc: true
images:
tags: 
  - go
  - 为什么
---

大家好,我是煎鱼。

作为一位 Go 程序员,你会发现身边的同事大多都拥有其他语言的编写经验。那势必就会遇到一点,要把新学到的知识和以前的知识建立连接。

![图来自网络](https://files.mdnice.com/user/3610/050a9802-1ca2-4634-859d-325a09d418c5.png)

特殊在于,Go 有些特性是其他语言有,他没有的。最经典的就是 N 位 Java 同学寻找 Go 语言的注解在哪里,总要解释。

为此,今天煎鱼就带大家了解一下 Go 语言的注解的使用和情况。

## 什么是注解

### 了解历史

注解(Annotation)最早出现自何处,翻了一圈并没有找到。但可以明确,在注解的使用中,Java 注解最为经典,为了便于理解,因此我们基于 Java 做初步的注解理解。

![](https://files.mdnice.com/user/3610/3c9e2434-c5f4-4d7a-bd2d-a8b582b570c8.png)

在 2002 年,JSR-175 提出了 《[A Metadata Facility for the Java Programming Language](https://jcp.org/en/jsr/detail?id=175)》,也就是为 Java 编程语言提供元数据工具。

这就是现在使用最广泛地注解(Annotation)的来源。
示例如下:

```golang
// @annotation1
// @annotation2
func Hello() string {
        return ""
}
```

在格式上均以 “@” 作为注解标识来使用。

### 注解例子

摘抄自 @wikipedia 的一个注解例子:

```java
  //等同于 @Edible(value = true)
  @Edible(true)
  Item item = new Carrot();

  public @interface Edible {
    boolean value() default false;
  }

  @Author(first = "Oompah", last = "Loompah")
  Book book = new Book();

  public @interface Author {
    String first();
    String last();
  }
  
  // 该标注可以在运行时通过反射访问。
  @Retention(RetentionPolicy.RUNTIME) 
  // 该标注只用于类内方法。
  @Target({ElementType.METHOD})
  public @interface Tweezable {
  }

```

在上述例子中,通过注解去做了一系列的定义、声明、赋值等。若是对语言既有注解不熟,或是做的比较复杂的注解,就会有一定的理解成本。

在业内也常常会说,**注解就是 “在源码上进行编码”**,注解的存在,有着明确的优缺点。你觉得呢?

## 注解的作用

在注解的的作用上,分为如下几点:
1. 为编译器提供信息:注释可以被编译器用来检测错误或支持警告。
2. 编译时和部署时处理:软件工具可以处理注释信息以生成代码、XML文件等。
3. 运行时处理:有些注解可以在运行时检查,并用于其他用途。

## Go 注解在哪里

### 现状 

Go 语言本身并没有原生支持强大的注解,仅限于以下两种:
- 编译时生成:go:generate
- 编译时约束:go:build

但这先按不足以作为一个函数注解来使用,也无法形成像 Python 那样的装饰器行为。

### 为什么不支持

Go issues 上有人提过类似的提案:

![](https://files.mdnice.com/user/3610/9695f163-65e8-456a-8dce-bb8551739016.png)

Go Contributor @ianlancetaylor 给出了明确的答复,Go在设计上更倾向于明确的、显式的编程风格。

思考的优缺点如下:
- 优势:不知道 Go 能从添加装饰器中得到什么好处,没能在 issues 上明确论证。
- 缺点:是明确的,会存在意外设置的情况。

因如下原因,没有接受注解:
- 对比现有代码方法,这种装饰器的新的方法没有提供比现有方法更多的优势,大到足矣推翻原有的设计思路。
- 社区内的投票,支持的也很少(基于表情符号的投票),用户反馈不多。

可能有小伙伴会说了,有注解做装饰器了,代码会简洁不少。

对此 Go 团队的态度很明确:

![](https://files.mdnice.com/user/3610/bb357e12-9b15-4729-9381-977d164b6b04.png)

Go 认为**可读性更重要**,如果只是额外多写一点代码,在权衡后,还是可以接受的。

## 用 Go 实现注解

虽然 Go 语言官方没有原生的完整支持,但开源社区中也有小伙伴已经放出了大招,借助各项周边工具和库来实现特定的函数注解功能。


GitHub 项目分别如下:
- [MarcGrol/golangAnnotations](github.com/MarcGrol/golangAnnotations)
- [u2takey/go-annotation](https://github.com/u2takey/go-annotation)

使用示例如下:

```golang
package tourdefrance

//go:generate golangAnnotations -input-dir .

// @RestService( path = "/api/tour" )
type TourService struct{}

type EtappeResult struct{ ... }

// @RestOperation( method = "PUT", path = "/{year}/etappe/{etappeUid}" )
func (ts *TourService) addEtappeResults(c context.Context, year int, etappeUid string, results EtappeResult) error {
	return nil
}
```

对 Go 注解的使用感兴趣的小伙伴可以自行查阅使用手册。

我们更多的关心,Go 原生都没支持,那么开源库都是如何实现的呢?在此我们借助 [MarcGrol/golangAnnotations](github.com/MarcGrol/golangAnnotations) 项目所提供的思路来讲解。

分为三个步骤:
1. 解析代码。
2. 模板处理。
3. 生成代码。

### 解析 AST

首先,我们需要用用 go/ast 标准库获取代码所生成的 AST Tree 中需要的内容和结构。

示例代码如下:

```shell
parsedSources := ParsedSources{
    PackageName: "tourdefrance",
    Structs:     []model.Struct{
        {
      	     DocLines:   []string{"// @RestService( path = "/api/tour" )"},
      	     Name:       "TourService",
      	     Operations: []model.Operation{
                {
              	    DocLines:   []string{"// @RestOperation( method = "PUT", path = "/{year}/etappe/{etappeUid}"},
              	    ...
                },
            },
        },
    },
}
```
我们可以看到,在 AST Tree 中能够获取到在示例代码中所定义的注解内容,我们就可以依据此去做很多奇奇怪怪的事情了。

### 模板生成

紧接着,在知道了注解的输入是什么后,我们只需要根据实际情况,编写对应的模板生成器 code-generator 就可以了。

我们会基于 text/template 标准库来实现,比较经典的像是 [kubernetes/code-generator](https://github.com/kubernetes/code-generator) 是一个可以参考的实现。

代码实现完毕后,将其编译成 go plugin,便于我们在下一步调用就可以了。

### 代码生成

最后,万事俱备只欠东风。差的就是告诉工具,哪些 Go 文件中包含注解,需要我们去生成的。

这时候我们可以使用 `//go:generate` 在 Go 文件声明。就像前面的项目中所说的:

```
//go:generate golangAnnotations -input-dir .
```

声明该 Go 文件需要生成,并调用前面编写好的 golangAnnotations 二进制文件,就可以实现基本的 Go 注解生成了。

## 总结

今天在这篇文章中,我们介绍了注解(Annotation)的历史背景。同时我们针对 Go 语言目前原生的注解支持情况进行了说明。

也面向为什么 Go 没有像 Java 那样支持强大的注解进行了基于 Go 官方团队的原因解释。如果希望在 Go 实现注解的,也提供了相应的开源技术方案。

**你觉得 Go 语言是否需要强大的注解支持**呢,欢迎你在评论区留言和讨论!

================================================
FILE: content/posts/go/any.md
================================================
---
title: "Go 新关键字 any,interface 会成历史吗?"
date: 2021-12-31T12:55:21+08:00
toc: true
images:
tags: 
  - go
---

大家好,我是煎鱼。

大家在看 Go1.18 泛型的代码时,不知道是否有留意到一个新的关键字 any。

例子如下:

```golang
func Print[T any](s []T) {}
```

之前没有专门提过,但有没有小伙伴以为这个关键字,是泛型代码专属的?

其实不是...在这次新的 Go1.18 更新中,any 是作为一个新的关键字出现,**any 有一个真身,本质上是 interface{} 的别名**:

```golang
type any = interface{}
```

也就是,在常规代码中,也可以直接使用:

```golang
func f(a any) {
	switch a.(type) {
	case int:
		fmt.Println("进脑子煎鱼了")
	case float64:
		fmt.Println("煎鱼进脑子了")
	case string:
		fmt.Println("脑子进煎鱼了")
	}
}

func main() {
	f(2)
	f(3.1415)
	f("煎鱼好!")
}
```

从使用层面来讲,新的关键字 any 会比 interface{} 方便不少,毕竟少打了好多个词,更快了,其实也是参照现有 rune 类型的做法。

增加新关键字后的对比如下:

| 长声明    |  短声明   |
| --- | --- |
|  func f[T interface{}](s []T) []T	   |  func f[T any](s []T) []T
  func f(a interface{})	 | func f(a any)
 |  var a interface{}	   |  var a any   |

我们在了解他的便利性后,再从代码一致性和可读性来讲,是有些问题的,会造成一定的疑惑。

因此前两天有人提出了《[all: rewrite interface{} to any](https://github.com/golang/go/issues/49884)》的需求,打算把内部所有的代码都重写一遍。

![](https://files.mdnice.com/user/3610/06687ddd-e224-4499-892b-3869ee1fc1d0.png)

你可能会以为是人肉手工改?那肯定不是,Go 官方发起了 CL 进行批量修改。

我们在日常的工程中,也可以和他们一样,直接借用 Go 工具链来实现替换。

如下:

```golang
gofmt -w -r 'interface{} -> any' ./...
```

听到这个消息时,我的朋友咸鱼就大惊了,在想 interface{} 会不会成为历史,被新的关键字 any 完全替代?

![](https://files.mdnice.com/user/3610/41a70c0c-284c-44c7-9b15-a1ee37545424.png)

显然,答案是不会的。因为 **Go1 有兼容性的保证**,肯定不会在现阶段删除。不过后续会出现一个现象,就是我们的 Go 工程中,有人用 any,有人用 interface{},会在代码可读性上比较伤人。

不过我们也可以学 Go 官方,在 linter 流程中借助 gofmt 工具来强行把所有 interface{} 都替换成 any 来实现代码的一致性。 

这次变更,感觉是个**美学**的问题,你对此怎么看呢?有没有也希望哪些东西有别名,**欢迎大家在评论区留言和交流**:)

================================================
FILE: content/posts/go/class-extends.md
================================================
---
title: "Go 为什么不支持类和继承?"
date: 2021-12-31T12:55:22+08:00
toc: true
images:
tags: 
  - go
---

大家好,我是煎鱼。

大家在早期学习 Go 时,一旦跨过语法的阶段后。马上就会进入到一个新的纠结点,Go 不支持面向对象吗?

![](https://files.mdnice.com/user/3610/a299d98d-e46c-4a6d-8362-02f957e86b10.png)


这门编程语言里没有类(class)、继承(extends),~~没法一把搜了,面试问啥面向对象(OOP)~~?

今天煎鱼就带大家一起来了解这之中的思考,Go 真的不支持吗?

## 类和继承

### 类是什么

类(class)在面向对象编程中是一种面向对象计算机编程语言的构造,是创建对象的蓝图,描述了所创建的对象共同的特性和方法(via @维基百科)。

例子如下:

```php
class SimpleClass
{
    // 声明属性
    public $var = '脑子进煎鱼了';

    // 声明方法
    public function displayVar() {
        echo $this->var;
    }
}
```

每个类的定义都以关键字 class 开头,后面跟着类名,后面跟着一对花括号,里面包含有类的属性与方法的定义。

### 继承是什么

继承是面向对象软件技术当中的一个概念,如果一个类别 B “继承自”另一个类别 A,就把这个 B 称为 “A的子类”,而把 A 称为 “B的父类别” 也可以称 “A 是 B 的超类”(via @维基百科)。

例子如下:

```php
// 父类
class Foo
{
    public function printItem($string)
    {
        echo '煎鱼1: ' . $string . PHP_EOL;
    }
    
    public function printPHP()
    {
        echo 'PHP is great.' . PHP_EOL;
    }
}

// 子类
class Bar extends Foo
{
    public function printItem($string)
    {
        echo '煎鱼2: ' . $string . PHP_EOL;
    }
}
```

继承有如下两个特性:
- 子类具有父类别的各种属性和方法,不需要再次编写相同的代码。
- 子类别继承父类时,可以重新定义某些属性,并重写某些方法,使其获得与父类别不同的功能。

## 结构和组合

在 Go 里就比较 ”特别“ 了,因为没有传统的类,也没有继承。

取而代之的是结构和组合的方式。这也是业内对 Go 是否 OOP 争议最大的地方。

### 结构体

我们可以在 Go 中通过结构体的方式来组织代码,达到类似类的方式。

例子如下:

```golang
package main

import "fmt"

type person struct {
    name string
    age  int
}

func(p *person) hello(){}

func newPerson(name string) *person {
    p := person{name: name}
    p.age = 42
    return &p
}

func main() {
    fmt.Println(person{"煎鱼1", 22})
    fmt.Println(person{name: "煎鱼2", age: 33})
    ...
}
```

在上述代码中,我们可以定义结构体内的属性,也可以针对结构体这些类型定义只属于他们的方法。

在声明实例上,可以配合 `newXXX` 的初始化方法来生成,这是 Go 里约定俗成的方式。

### 组合

类的声明采取结构体的方式取代后,也可以配套使用 ”组合“ 来达到类似继承的效果。

例子如下:

```golang
type man struct {
	name string
}

func (m *man) hello1() {}

type person struct {
	man
	name string
}

func (p *person) hello2() {}

func newPerson(name string) *person {
	p := person{name: name}
	return &p
}

func main() {
	p := newPerson("脑子进煎鱼了")
	p.hello1()
}
```

在上述代码中,我们分别定义了 man 和 person 两个结构体,并将 man 嵌入到 person 中,形成组合。

你可以在 main 方法中能够看到,person 实例是可以使用和调用 man 实例的一些公开属性和方法的。

在简单的使用效果上会与继承有些接近。

## Go 是面向对象的语言吗

“Go 语言是否一门面向对象的语言?”,这是一个日经话题。官方 FAQ 给出的答复是:

![](https://files.mdnice.com/user/3610/159601a9-b428-4958-9f85-2214aea30127.png)

是的,也不是。原因是:

- Go 有类型和方法,并且允许面向对象的编程风格,但没有类型层次。
- Go 中的 "接口 "概念提供了一种不同的方法,我们认为这种方法易于使用,而且在某些方面更加通用。还有一些方法可以将类型嵌入到其他类型中,以提供类似的东西,但不等同于子类。
- Go 中的方法比 C++ 或 Java 中的方法更通用:它们可以为任何类型的数据定义,甚至是内置类型,如普通的、"未装箱的 "整数。它们并不局限于结构(类)。
- Go 由于缺乏类型层次,Go 中的 "对象 "比 C++ 或 Java 等语言更轻巧。

## 为什么不支持类和继承

有的人认为类和继承是面向对象的必要特性,必须要有,才能是面向对象的语言,但其实也并非如此。

面向对象(OOP)有不同的含义和解读,许多概念也可以通过结构体、组合和接口等方式进行表达,说是不支持传统的 OOP。

其实真相是 Go 是选择了另外一条路,也就是 ”**组合优于继承**“。我们所提到的类和继承并不是定义 OOP 的一种准则,只是协助完成 OOP 的方法之一。

不要本末倒置了,不让工具来定义 OOP 的理念。

## 总结

在今天这篇文章中,我们介绍了常说的类和继承的业内定义和使用案例。同时面向 Go 读者群里的疑惑,进行了解答。

实质上,Go 是 OOP,也不是 OOP。类和继承只是实现 OOP 的一种方式,但并不是没有这两者,他就不是 OOP 了。

不支持的原因也很明确,Go 在设计上,选择了组合优于继承的编程设计模式,它不是传统那种面向类型的范式。

你觉得呢,欢迎大家在评论区留言和交流:)
 
## 参考

- 为什么人们声称 Go 不是面向对象的,我是不是误读了什么?:https://www.reddit.com/r/golang/comments/a9rn6n/why_people_claim_that_go_is_not_object_oriented/
- 组合大于继承:https://en.wikipedia.org/wiki/Composition_over_inheritance
- Go 是面向对象的吗?:https://flaviocopes.com/golang-is-go-object-oriented/
- 类 vs 新型 + 接收器?:https://www.reddit.com/r/golang/comments/a8zgvm/class_vs_new_type_receiver/
- 结构而不是类:https://golangbot.com/structs-instead-of-classes/

================================================
FILE: content/posts/go/crawler/2018-03-21-douban-top250.md
================================================
---

title:      "爬取豆瓣电影 Top250"
date:       2018-03-21 12:30:00
author:     "煎鱼"
toc: true
tags:
    - go
    - 数据分析
---

爬虫是标配了,看数据那一刻很有趣。第一个就从最最最简单最基础的爬虫开始写起吧!

项目地址:https://github.com/go-crawler/douban-movie

## 目标

我们的目标站点是 [豆瓣电影 Top250](https://movie.douban.com/top250),估计大家都很眼熟了

本次爬取 8 个字段,用于简单的概括分析。具体的字段如下:

![image](https://i.loli.net/2018/03/20/5ab11596b8810.png)

简单的分析一下目标源

- 一页共 25 条
- 含分页(共 10 页)且分页规则是正常的
- 每一项的数据字段排序都是规则且不变

## 开始

由于量不大,我们的爬取步骤如下

- 分析页面,获取所有的分页
- 分析页面,循环爬取所有页面的电影信息
- 爬取的电影信息入库

### 安装

```
$ go get -u github.com/PuerkitoBio/goquery
```

### 运行

```
$ go run main.go
```

### 代码片段

#### 1、获取所有分页

```go
func ParsePages(doc *goquery.Document) (pages []Page) {
	pages = append(pages, Page{Page: 1, Url: ""})
	doc.Find("#content > div > div.article > div.paginator > a").Each(func(i int, s *goquery.Selection) {
		page, _ := strconv.Atoi(s.Text())
		url, _ := s.Attr("href")

		pages = append(pages, Page{
			Page: page,
			Url:  url,
		})
	})

	return pages
}
```

#### 2、分析豆瓣电影信息

```go
func ParseMovies(doc *goquery.Document) (movies []Movie) {
	doc.Find("#content > div > div.article > ol > li").Each(func(i int, s *goquery.Selection) {
		title := s.Find(".hd a span").Eq(0).Text()

		...

		movieDesc := strings.Split(DescInfo[1], "/")
		year := strings.TrimSpace(movieDesc[0])
		area := strings.TrimSpace(movieDesc[1])
		tag := strings.TrimSpace(movieDesc[2])

		star := s.Find(".bd .star .rating_num").Text()

		comment := strings.TrimSpace(s.Find(".bd .star span").Eq(3).Text())
		compile := regexp.MustCompile("[0-9]")
		comment = strings.Join(compile.FindAllString(comment, -1), "")

		quote := s.Find(".quote .inq").Text()

		...

		log.Printf("i: %d, movie: %v", i, movie)

		movies = append(movies, movie)
	})

	return movies
}
```

### 数据

![image](https://i.loli.net/2018/03/21/5ab1309594741.png)

![image](https://i.loli.net/2018/03/21/5ab131ca582f8.png)

![image](https://i.loli.net/2018/03/21/5ab130d3a00d9.png)

看到这些数据,你有什么想法呢,真是好奇 :=)


================================================
FILE: content/posts/go/crawler/2018-04-01-cars.md
================================================
---

title:      "爬取汽车之家 二手车产品库"
date:       2018-04-01 12:30:00
author:     "煎鱼"
toc: true
tags:
    - go
    - 数据分析
---

项目地址:https://github.com/go-crawler/car-prices

## 目标

最近经常有人在耳边提起汽车之家,也好奇二手车在国内的价格是怎么样的,因此本次的目标站点是 [汽车之家](https://car.autohome.com.cn/2sc/440399/index.html) 的二手车产品库

![image](https://i.loli.net/2018/03/30/5abe47f82a01f.png)

分析目标源:

- 一页共 24 条
- 含分页,但这个老产品库,在 100 页后会存在问题,因此我们爬取 99 页
- 可以获取全部城市
- 共可爬取 19w+ 数据

## 开始

爬取步骤

- 获取全部的城市
- 拼装全部城市 URL 入队列
- 解析二手车页面结构
- 下一页 URL 入队列
- 循环拉取所有分页的二手车数据
- 循环拉取队列中城市的二手车数据
- 等待,确定队列中无新的 URL
- 爬取的二手车数据入库

### 获取城市

![image](https://i.loli.net/2018/03/31/5abeff11ef583.png)

通过页面查看,可发现在城市筛选区可得到全部的二手车城市列表,但是你仔细查阅代码。会发现它是 JS 加载进来的,城市也统一放在了一个变量中

![image](https://i.loli.net/2018/03/31/5abf056389cf0.png)

有两种提取方法

- 分析 JS 变量,提取出来
- 直接将 `areaJson` 复制出来作为变量解析

在这里我们直接将其复制粘贴出来即可,因为这是比较少变动的值

### 获取分页

![image](https://i.loli.net/2018/03/31/5abf08ec812e2.png)

通过分析页面可以得知分页链接是有一定规律的,例如:`/2sc/hangzhou/a0_0msdgscncgpi1ltocsp2exb4/`,可以发现 `sp%d`,`sp` 后面为页码

按照常理,可以通过预测所有分页链接,推入队列后 `go routine` 一波 即可快速拉取

但是在这老产品库存在一个问题,在超过 100 页后,下一页永远是 101 页

![image](https://i.loli.net/2018/03/31/5abf0e1e623ec.png)

因此我们采取比较传统的做法,通过拉取下一页的链接去访问,以便适应可能的分页链接改变; 100 页以后的分页展示也很奇怪,先忽视

### 获取二手车数据

页面结构较为固定,常规的清洗 HTML 即可

```go
func GetCars(doc *goquery.Document) (cars []QcCar) {
	cityName := GetCityName(doc)
	doc.Find(".piclist ul li:not(.line)").Each(func(i int, selection *goquery.Selection) {
		title := selection.Find(".title a").Text()
		price := selection.Find(".detail .detail-r").Find(".colf8").Text()
		kilometer := selection.Find(".detail .detail-l").Find("p").Eq(0).Text()
		year := selection.Find(".detail .detail-l").Find("p").Eq(1).Text()

		kilometer = strings.Join(compileNumber.FindAllString(kilometer, -1), "")
		year = strings.Join(compileNumber.FindAllString(strings.TrimSpace(year), -1), "")
		priceS, _ := strconv.ParseFloat(price, 64)
		kilometerS, _ := strconv.ParseFloat(kilometer, 64)
		yearS, _ := strconv.Atoi(year)

		cars = append(cars, QcCar{
			CityName: cityName,
			Title: title,
			Price: priceS,
			Kilometer: kilometerS,
			Year: yearS,
		})
	})

	return cars
}
```

## 数据

![image](https://i.loli.net/2018/03/31/5abf1d8042196.png)

![image](https://i.loli.net/2018/04/01/5abfbaa14b09c.png)

在各城市的平均价格对比中,我们可以发现北上广深里的北京、上海、深圳都在榜单上,而近年势头较猛的杭州直接占领了榜首,且后几名都有一些距离

而其他城市大致都是梯级下降的趋势,看来一线城市的二手车也是不便宜了,当然这只是均价

![image](https://i.loli.net/2018/03/31/5abf1dbc665f2.png)

我们可以看到价格和公里数的对比,上海、成都、郑州的等比差异是有点大,感觉有需求的话可以在价格和公里数上做一个衡量

![image](https://i.loli.net/2018/03/31/5abf1e1434edc.png)

这图有点儿有趣,粗略的统计了一下总公里数。在前几张图里,平均价格排名较高的统统没有出现在这里,反倒是呼和浩特、大庆、中山等出现在了榜首

是否侧面反应了一线城市的车辆更新换代较快,而较后的城市的车辆倒是换代较慢,公里数基本都杠杠的

![image](https://i.loli.net/2018/03/31/5abf1e4936640.png)

通过对标题的分析,可以得知车辆产品库的命名基本都是品牌名称+自动/手动+XXXX 款+属性,看标题就能知道个概况了

## 参考

### 爬虫项目地址

- https://github.com/go-crawler/car-prices




================================================
FILE: content/posts/go/crawler/2018-04-28-go2018.md
================================================
---

title:      "了解一下Golang的市场行情"
date:       2018-04-28 12:30:00
author:     "煎鱼"
toc: true
tags:
    - go
    - 数据分析
---

项目地址:https://github.com/go-crawler/lagou_jobs

如果对你有所帮助,欢迎 Star,给文章来波赞,这样可以让更多的人看见  :)

## 目标

在工作中 Golang 已是一份子,想让大家了解一下 Golang 的市场行情,也想让更多的人熟悉它。因此主要是展示数据分析的结果

目标站点是 [某招聘网站](https://www.lagou.com/) 的职位数据抓取和分析,爬取城市分别为 北京、上海、广州、深圳、杭州、成都,再得出一个结论

### 分析

首先需要进行页面分析,找到我们的抓取方向

![image](https://i.loli.net/2018/04/26/5ae1e28a3412a.jpeg)

搜索 golang 关键字,打开页面 F12 就能看到它发送了四个请求,留意 positionAjax.json 这个请求

![image](https://i.loli.net/2018/04/26/5ae1efe538791.jpeg)

我们仔细研判这个接口的入参和出参

### 入参

1、Query String Param

- city:请求的城市

- needAddtionalResult:是否需要补充额外的参数,这里默认 false

2、Form Data

- first:是否首页
- pn:页码
- kd:关键字

### 出参

![image](https://i.loli.net/2018/04/26/5ae1f4c9920a9.jpeg)

就是它了,从返回结果可得出许多有用的信息

- companyFullName:公司全称
- companyLabelList:公司标签
- companyShortName:公司简称
- companySize:公司规模
- education:学历要求
- financeStage:融资阶段

等等~

### 分页

在上面两张图中,可以发现在 content 节点中包含 pageNo、pageSize 字段,content.positionResult 节点有 totalCount 字段,可以得知当前是第几页,每页显示多少条,当前的职位总条数

需要注意一下,分页的计算是要向上取整的

## 模拟浏览器头

User-Agent 可以用 [fake-useragent](https://github.com/EDDYCJY/fake-useragent) 这个项目来随机生成 UA 头 😄

## 数据

### 一、分布图

不同工作、工种,自然也会遍布在不同的工作区域,我们先了解一下各个城市的 Golang 工程师都主要在哪个区上班,心里留个底

#### 北京

![image](https://i.loli.net/2018/04/27/5ae291859667c.jpeg)

#### 上海

![image](https://i.loli.net/2018/04/27/5ae290856b774.jpeg)

#### 广州

![image](https://i.loli.net/2018/04/27/5ae28f1ab3e0c.jpeg)

#### 深圳

![image](https://i.loli.net/2018/04/27/5ae1fbebb1784.jpeg)

#### 杭州

![image](https://i.loli.net/2018/04/27/5ae29218c91dc.jpeg)

#### 成都

![image](https://i.loli.net/2018/04/27/5ae295b1059ed.jpeg)

### 二、招聘与职位数量对比

![image](https://i.loli.net/2018/04/27/5ae296b750dd8.png)

通过分析图中的数据,我们可以得知各城市的招聘职位数量

- 北京:348
- 上海:145
- 广州:37
- 成都:49
- 杭州:45
- 深圳:108

总共招聘的职位数量为 732 个,数量顺序分别为 北京 > 上海 > 深圳 > 成都 > 杭州 > 广州

还有另外一个关注点,就是招聘公司数量与职位的数量对比,可以看到 北京 招聘的职位数量为 348 个,而招聘的公司数量为 191 个,约为 1.82 的比例,也就是一家公司能提供两个 Golang 职位,它可能类别不同、(中级、中高级、高级)级别不同,具有一定可能性。而在广州,为 31 对比 37,虽然差额不大,但仍然存在这种现象

可以得出结果,Golang 在市场上具有一定的伸缩空间,也就是具有上升空间,一家公司会将 Golang 应用在多个不同的应用场景,也就是方向不同,需要的级别人才也就不同了

但是需要注意的是,Golang 的市场招聘人数目前份额还是较低,六个城市总数仅为 732 个,与其他大热语言相差有一定距离,需要谨慎

同时,面试 Golang 的人与其他大热语言相比会少些,职位的争夺是否小点呢?

### 三、招聘公司规模

![image](https://i.loli.net/2018/04/27/5ae2ab2babbd9.png)

通过查看招聘 Golang 工程师的公司规模,可以很直观的发现,微型公司使用 Golang 较少,其他类别的规模都有一定程度的应用,且差距不大。在 2000 人以上、50 - 150 人的公司规模中最受青睐

为什么呢,我认为有以下可能

- 大型公司结合场景,想通过 Golang 的特性来解决一些痛点问题
- 在小型公司 Golang 这颗新星实施起来更便捷,有一定的应用场景

你觉得呢,是不是应该有更多的选择它的原因?

### 四、学历要求

![image](https://s2.ax1x.com/2020/02/27/3dbTdP.png)

在招聘市场上,Golang 的招聘者更希望你是本科学历,大专和不限也有一定的份额,但市场份额相差较大

硕士学历要求的为两个,可以得出,在市场上 Golang 招聘者们对高学历的需求并不高,或者并不强制高学历

### 五、行业领域

![image](https://s2.ax1x.com/2020/02/27/3dqPiT.png)

在这里,重点关注 Golang 工程师的招聘公司都分别在什么行业领域,大头移动互联网是不容置疑的了,还可以惊喜的发现

- 数据服务
- 电子商务
- 金融
- 企业服务
- 游戏

Golang 在这几个方面都有所应用,说明了在市场上,Golang 的路子是比较广阔的,前景不错

同时,如果可以涉及多个领域的内容,想必身为工程师的你,肯定很激动

### 六、职位诱惑

![image](https://s2.ax1x.com/2020/02/27/3dqEQJ.png)

职位诱惑是投简历时必看的一点了,可以看到高频词条基本都是 IT 从业者关心的话题了,这里你懂的...

重点,我看到了一个 “免费三餐” 的词条命中 7 次,分别来自北京的海淀区、东城区、朝阳区,上海的黄浦区的七家不同的公司,辛苦了

### 七、行业、职位标签

![image](https://s2.ax1x.com/2020/02/27/3dqlWD.png)

在招聘 JD 中,描述和标签常用于给求职者了解这一职业的具体工作内容和其关联性

在图中你可以看到 Golang 常常和什么内容搭上边,这点很有意义哦

1、语言

- Java
- Python
- C/C++
- PHP

在图中可以看出,Golang 与以上四种语言有一定关联性,而 Java 和 Python 分别第一、第二名,可以说明市场上对复合型人才的渴望度更高,也许你不懂也行,但你懂了就最好(加分项)。需要你自身有多语言的经验,也便于和其他人对接

同时 Golang 目前存在许多内部转语言写的情况,所以这一点可以参考

2、职称

- 高级
- 资深
- 中级

特意将职称放在第二位,可以发现在市场上 Golang 标签的需求是 高级 > 资深 > 中级,关联第一项 “语言关联” 不难得出这个结论,因为语言只是解决问题的工具,到了中级及以上的工程师都是懂多门语言的居多,再采取不同的方案去解决应用场景上的问题

可得出结论,市场目前对 Golang 更期望是中高、高级、资深的人才,而中级的反而少一点点

大家可以努力再往上冲击冲击

3、组件

- Linux
- Redis
- Mysql

4、行业

- 云计算
- 信息安全
- 大数据
- 金融
- 软件开发

#### 八、薪资与工作年限

![image](https://s2.ax1x.com/2020/02/27/3dq3Se.jpg)

1、1-3 年

一个(成长)特殊的阶段,有个位数也有双位数的,大头可以到 15-30k,20-40k,而初级的也有 8-16k

2、3-5 年

厚积待发的阶段,薪酬范畴的跨度是较大,10-60k 的薪酬都有,这充分说明能力决定你的上下

3、5-10 年

核心,招聘网站上的招聘数量反而少,都会走内推或猎头,不需要特别介绍了

##### 小结

这一部分,相信是很多人关注的地方

在有的文章中会看到,他们的薪资部分是以平均值来展示的。我就很纳闷,因为对平均值并不是很关心,**重点是无法体现薪资幅度**。因此这里我会尽可能的把数据展现给你们看

(正文)从图表来看,Golang 当前的薪酬水平还是很不错的,市场能根据不同阶段(水平)的人给出一个好的价位

(题外话)看完之后希望你能知道以下内容

- 你当前工作年限的最高、最低薪资范畴
- 你的下一阶段的薪资范畴
- 为什么有的人高,有的人低
- 在大头部队还是小头,为什么
- 不要满足于平均值

### 九、融资阶段

![image](https://s2.ax1x.com/2020/02/27/3dqteI.png)

选用 Golang 的公司大多数都较为稳定,有一部分比较刺激 :)

#### 融资阶段与薪资范畴对比

##### 不需要融资

![image](https://s2.ax1x.com/2020/02/27/3dqyOs.png)

##### 上市公司

![image](https://s2.ax1x.com/2020/02/27/3dqWkV.png)

##### A 轮

![image](https://s2.ax1x.com/2020/02/27/3dqfYT.png)

##### B 轮

![image](https://s2.ax1x.com/2020/02/27/3dqTX9.png)

##### C 轮

![image](https://s2.ax1x.com/2020/02/27/3dqOk6.png)

##### D 轮以上

![image](https://s2.ax1x.com/2020/02/27/3dqz1e.png)

### 十、附近的地铁

Golang 工程师都驻扎在什么地铁站附近呢

经常在地铁上看到同行在看代码,来了解一下都分布在哪 :)

#### 北京

![image](https://s2.ax1x.com/2020/02/27/3dLCnA.png)

#### 上海

![image](https://s2.ax1x.com/2020/02/27/3dLkAP.png)

#### 广州

![image](https://s2.ax1x.com/2020/02/27/3dLAtf.png)

#### 深圳

![image](https://s2.ax1x.com/2020/02/27/3dLZ9S.png)

#### 杭州

![image](https://s2.ax1x.com/2020/02/27/3dLKns.png)

#### 成都

![image](https://s2.ax1x.com/2020/02/27/3dL3NV.png)

## 结论

如同官方所说 "Go has been on an amazing journey over the last 8+ years",作为一门新生语言,一直在不断地发展,**缺点肯定是有的,你要去识别它**

### 从数量来看

单从这个招聘网站上来看,数量方面,与大热语言的招聘职位数量仍然有一定的差距,但 Golang 存在许多内部转语言开发的情况,当前展现出来的数据,**招聘数量不多,但质量不错**

### 从分布图来看

一线城市基本都有 Golang 的职位,虽然其他城市较少,但对于新语言来说是需要持续关注的过程,不能一刀切

### 从职称级别来看

Golang 中高、高级、资深仍然是占大头,给的薪资也基本符合市场行情

### 从方向来看

Golang 涉及的行业领域广泛,移动互联网、数据服务、电子商务、金融、企业服务、云计算等都是它的战场之一

### 从开源项目来看

docker、k8s、etcd、consul 都挺稳

---

总的来说,Golang 处于一个发展的阶段,市场行情也还行、应用场景较广,不过招聘数量不多,你又怎么看呢?

最后放上今天新发布的 Logo :)

![image](https://s2.ax1x.com/2020/02/27/3dLY3F.jpg)

如果对你有所帮助,欢迎 Star,给文章点个赞,这样可以让更多的人看见这篇文章

## 参考

- 项目地址:https://github.com/go-crawler/lagou_jobs


================================================
FILE: content/posts/go/defer/2019-05-27-defer.md
================================================
---

title:      "深入理解 Go defer"
date:       2019-05-27 12:30:00
author:     "煎鱼"
toc: true
tags:
    - go
    - 源码分析
---

在上一章节 《深入理解 Go panic and recover》中,我们发现了 `defer` 与其关联性极大,还是觉得非常有必要深入一下。希望通过本章节大家可以对 `defer` 关键字有一个深刻的理解,那么我们开始吧。你先等等,请排好队,我们这儿采取后进先出 LIFO 的出站方式...

## 特性

我们简单的过一下 `defer` 关键字的基础使用,让大家先有一个基础的认知

### 一、延迟调用

```go
func main() {
	defer log.Println("EDDYCJY.")

	log.Println("end.")
}
```

输出结果:

```
$ go run main.go
2019/05/19 21:15:02 end.
2019/05/19 21:15:02 EDDYCJY.
```

### 二、后进先出

```go
func main() {
	for i := 0; i < 6; i++ {
		defer log.Println("EDDYCJY" + strconv.Itoa(i) + ".")
	}


	log.Println("end.")
}
```

输出结果:

```
$ go run main.go
2019/05/19 21:19:17 end.
2019/05/19 21:19:17 EDDYCJY5.
2019/05/19 21:19:17 EDDYCJY4.
2019/05/19 21:19:17 EDDYCJY3.
2019/05/19 21:19:17 EDDYCJY2.
2019/05/19 21:19:17 EDDYCJY1.
2019/05/19 21:19:17 EDDYCJY0.
```

### 三、运行时间点

```go
func main() {
	func() {
		 defer log.Println("defer.EDDYCJY.")
	}()

	log.Println("main.EDDYCJY.")
}
```

输出结果:

```
$ go run main.go
2019/05/22 23:30:27 defer.EDDYCJY.
2019/05/22 23:30:27 main.EDDYCJY.
```

### 四、异常处理

```go
func main() {
	defer func() {
		if e := recover(); e != nil {
			log.Println("EDDYCJY.")
		}
	}()

	panic("end.")
}
```

输出结果:

```
$ go run main.go
2019/05/20 22:22:57 EDDYCJY.
```

## 源码剖析

```
$ go tool compile -S main.go
"".main STEXT size=163 args=0x0 locals=0x40
	...
	0x0059 00089 (main.go:6)	MOVQ	AX, 16(SP)
	0x005e 00094 (main.go:6)	MOVQ	$1, 24(SP)
	0x0067 00103 (main.go:6)	MOVQ	$1, 32(SP)
	0x0070 00112 (main.go:6)	CALL	runtime.deferproc(SB)
	0x0075 00117 (main.go:6)	TESTL	AX, AX
	0x0077 00119 (main.go:6)	JNE	137
	0x0079 00121 (main.go:7)	XCHGL	AX, AX
	0x007a 00122 (main.go:7)	CALL	runtime.deferreturn(SB)
	0x007f 00127 (main.go:7)	MOVQ	56(SP), BP
	0x0084 00132 (main.go:7)	ADDQ	$64, SP
	0x0088 00136 (main.go:7)	RET
	0x0089 00137 (main.go:6)	XCHGL	AX, AX
	0x008a 00138 (main.go:6)	CALL	runtime.deferreturn(SB)
	0x008f 00143 (main.go:6)	MOVQ	56(SP), BP
	0x0094 00148 (main.go:6)	ADDQ	$64, SP
	0x0098 00152 (main.go:6)	RET
	...
```

首先我们需要找到它,找到它实际对应什么执行代码。通过汇编代码,可得知涉及如下方法:

- runtime.deferproc
- runtime.deferreturn

很显然是运行时的方法,是对的人。我们继续往下走看看都分别承担了什么行为

### 数据结构

在开始前我们需要先介绍一下 `defer` 的基础单元 `_defer` 结构体,如下:

```go
type _defer struct {
	siz     int32
	started bool
	sp      uintptr // sp at time of defer
	pc      uintptr
	fn      *funcval
	_panic  *_panic // panic that is running defer
	link    *_defer
}

...
type funcval struct {
	fn uintptr
	// variable-size, fn-specific data here
}
```

- siz:所有传入参数的总大小
- started:该 `defer` 是否已经执行过
- sp:函数栈指针寄存器,一般指向当前函数栈的栈顶
- pc:程序计数器,有时称为指令指针(IP),线程利用它来跟踪下一个要执行的指令。在大多数处理器中,PC 指向的是下一条指令,而不是当前指令
- fn:指向传入的函数地址和参数
- \_panic:指向 `_panic` 链表
- link:指向 `_defer` 链表

![image](https://s2.ax1x.com/2020/02/27/3dLNjJ.png)

### deferproc

```go
func deferproc(siz int32, fn *funcval) {
    ...
	sp := getcallersp()
	argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
	callerpc := getcallerpc()

	d := newdefer(siz)
    ...
	d.fn = fn
	d.pc = callerpc
	d.sp = sp
	switch siz {
	case 0:
		// Do nothing.
	case sys.PtrSize:
		*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
	default:
		memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
	}

	return0()
}
```

- 获取调用 `defer` 函数的函数栈指针、传入函数的参数具体地址以及 PC (程序计数器),也就是下一个要执行的指令。这些相当于是预备参数,便于后续的流转控制
- 创建一个新的 `defer` 最小单元 `_defer`,填入先前准备的参数
- 调用 `memmove` 将传入的参数存储到新 `_defer` (当前使用)中去,便于后续的使用
- 最后调用 `return0` 进行返回,这个函数非常重要。能够避免在 `deferproc` 中又因为返回 `return`,而诱发 `deferreturn` 方法的调用。其根本原因是一个停止 `panic` 的延迟方法会使 `deferproc` 返回 1,但在机制中如果 `deferproc` 返回不等于 0,将会总是检查返回值并跳转到函数的末尾。而 `return0` 返回的就是 0,因此可以防止重复调用

#### 小结

在**这个函数中会为新的 `_defer` 设置一些基础属性,并将调用函数的参数集传入。最后通过特殊的返回方法结束函数调用**。另外这一块与先前 [《深入理解 Go panic and recover》](https://segmentfault.com/a/1190000019251478#articleHeader9) 的处理逻辑有一定关联性,其实就是 `gp.sched.ret` 返回 0 还是 1 会分流至不同处理方式

### newdefer

```go
func newdefer(siz int32) *_defer {
	var d *_defer
	sc := deferclass(uintptr(siz))
	gp := getg()
	if sc < uintptr(len(p{}.deferpool)) {
		pp := gp.m.p.ptr()
		if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
			...
			lock(&sched.deferlock)
			d := sched.deferpool[sc]
			unlock(&sched.deferlock)
		}
		...
	}
	if d == nil {
		systemstack(func() {
			total := roundupsize(totaldefersize(uintptr(siz)))
			d = (*_defer)(mallocgc(total, deferType, true))
		})
		...
	}
	d.siz = siz
	d.link = gp._defer
	gp._defer = d
	return d
}
```

- 从池中获取可以使用的 `_defer`,则复用作为新的基础单元
- 若在池中没有获取到可用的,则调用 `mallocgc` 重新申请一个新的
- 设置 `defer` 的基础属性,最后修改当前 `Goroutine` 的 `_defer` 指向

通过这个方法我们可以注意到两点,如下:

- `defer` 与 `Goroutine(g)` 有直接关系,所以讨论 `defer` 时基本离不开 `g` 的关联
- 新的 `defer` 总是会在现有的链表中的最前面,也就是 `defer` 的特性后进先出

#### 小结

这个函数主要承担了获取新的 `_defer` 的作用,它有可能是从 `deferpool` 中获取的,也有可能是重新申请的

### deferreturn

```go
func deferreturn(arg0 uintptr) {
	gp := getg()
	d := gp._defer
	if d == nil {
		return
	}
	sp := getcallersp()
	if d.sp != sp {
		return
	}

	switch d.siz {
	case 0:
		// Do nothing.
	case sys.PtrSize:
		*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
	default:
		memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
	}
	fn := d.fn
	d.fn = nil
	gp._defer = d.link
	freedefer(d)
	jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
```

如果在一个方法中调用过 `defer` 关键字,那么编译器将会在结尾处插入 `deferreturn` 方法的调用。而该方法中主要做了如下事项:

- 清空当前节点 `_defer` 被调用的函数调用信息
- 释放当前节点的 `_defer` 的存储信息并放回池中(便于复用)
- 跳转到调用 `defer` 关键字的调用函数处

在这段代码中,跳转方法 `jmpdefer` 格外重要。因为它显式的控制了流转,代码如下:

```
// asm_amd64.s
TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
	MOVQ	fv+0(FP), DX	// fn
	MOVQ	argp+8(FP), BX	// caller sp
	LEAQ	-8(BX), SP	// caller sp after CALL
	MOVQ	-8(SP), BP	// restore BP as if deferreturn returned (harmless if framepointers not in use)
	SUBQ	$5, (SP)	// return to CALL again
	MOVQ	0(DX), BX
	JMP	BX	// but first run the deferred function
```

通过源码的分析,我们发现它做了两个很 “奇怪” 又很重要的事,如下:

- MOVQ -8(SP), BP:`-8(BX)` 这个位置保存的是 `deferreturn` 执行完毕后的地址
- SUBQ \$5, (SP):`SP` 的地址减 5 ,其减掉的长度就恰好是 `runtime.deferreturn` 的长度

你可能会问,为什么是 5?好吧。翻了半天最后看了一下汇编代码...嗯,相减的确是 5 没毛病,如下:

```
	0x007a 00122 (main.go:7)	CALL	runtime.deferreturn(SB)
	0x007f 00127 (main.go:7)	MOVQ	56(SP), BP
```

我们整理一下思绪,照上述逻辑的话,那 `deferreturn` 就是一个 “递归” 了哦。每次都会重新回到 `deferreturn` 函数,那它在什么时候才会结束呢,如下:

```go
func deferreturn(arg0 uintptr) {
	gp := getg()
	d := gp._defer
	if d == nil {
		return
	}
	...
}
```

也就是会不断地进入 `deferreturn` 函数,判断链表中是否还存着 `_defer`。若已经不存在了,则返回,结束掉它。简单来讲,就是处理完全部 `defer` 才允许你真的离开它。果真如此吗?我们再看看上面的汇编代码,如下:

```
    。..
	0x0070 00112 (main.go:6)	CALL	runtime.deferproc(SB)
	0x0075 00117 (main.go:6)	TESTL	AX, AX
	0x0077 00119 (main.go:6)	JNE	137
	0x0079 00121 (main.go:7)	XCHGL	AX, AX
	0x007a 00122 (main.go:7)	CALL	runtime.deferreturn(SB)
	0x007f 00127 (main.go:7)	MOVQ	56(SP), BP
	0x0084 00132 (main.go:7)	ADDQ	$64, SP
	0x0088 00136 (main.go:7)	RET
	0x0089 00137 (main.go:6)	XCHGL	AX, AX
	0x008a 00138 (main.go:6)	CALL	runtime.deferreturn(SB)
	...
```

的确如上述流程所分析一致,验证完毕

#### 小结

这个函数主要承担了清空已使用的 `defer` 和跳转到调用 `defer` 关键字的函数处,非常重要

## 总结

我们有提到 `defer` 关键字涉及两个核心的函数,分别是 `deferproc` 和 `deferreturn` 函数。而 `deferreturn` 函数比较特殊,是当应用函数调用 `defer` 关键字时,编译器会在其结尾处插入 `deferreturn` 的调用,它们俩一般都是成对出现的

但是当一个 `Goroutine` 上存在着多次 `defer` 行为(也就是多个 `_defer`)时,编译器会进行利用一些小技巧, 重新回到 `deferreturn` 函数去消耗 `_defer` 链表,直到一个不剩才允许真正的结束

而新增的基础单元 `_defer`,有可能是被复用的,也有可能是全新申请的。它最后都会被追加到 `_defer` 链表的表头,从而设定了后进先出的调用特性

## 关联

- [深入理解 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)

## 参考

- [Scheduling In Go](https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part1.html)
- [Dive into stack and defer/panic/recover in go](http://hustcat.github.io/dive-into-stack-defer-panic-recover-in-go/)
- [golang-notes](https://github.com/cch123/golang-notes/blob/master/defer.md)


================================================
FILE: content/posts/go/delve.md
================================================
---
title: "一个 Demo 学会使用 Go Delve 调试"
date: 2021-12-31T12:54:57+08:00
toc: true
images:
tags: 
  - go
---

大家好,我是煎鱼。

在 Go 语言中,除了 go tool 工具链中的 pprof、trace 等剖析工具的大利器外。常常还会有小伙伴问,有没有更好用,更精细的,

大家总嫌弃 pprof、trace 等工具,不够细,没法一口气看到根因,或者具体变量...希望能够最好能追到代码级别调试的,看到具体变量的值是怎么样的,随意想怎么看怎么看的那种。

为此今天给大家介绍 Go 语言强大的 Delve (dlv)调试工具,来更深入问题剖析。

## 安装

我们需要先安装 Go delve,若是 Go1.16 及以后的版本,可以直接执行下述命令安装:

```
$ go install github.com/go-delve/delve/cmd/dlv@latest
```

也可以通过 git clone 的方式安装:

```
$ git clone https://github.com/go-delve/delve
$ cd delve
$ go install github.com/go-delve/delve/cmd/dlv
```

在安装完毕后,我们执行 `dlv version` 命令,查看安装情况:

```
$ dlv version
Delve Debugger
Version: 1.7.0
Build: $Id: e353a65161e6ed74952b96bbb62ebfc56090832b $
```

可以明确看到我们所安装的版本是 v1.7.0。

## 演示程序

我们计划用一个反转字符串的演示程序来进行 Go 程序的调试。第一部分先是完成 `stringer` 包的 `Reverse` 方法。

代码如下:

```golang
package stringer

func Reverse(s string) string {
	r := []rune(s)
	for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
		r[i], r[j] = r[j], r[i]
	}
	return string(r)
}
```

再在具体的 `main` 启动函数中进行调用。代码如下:

```golang
import (
	"fmt"

	"github.com/eddycjy/awesome-project/stringer"
)

func main() {
	fmt.Println(stringer.Reverse("脑子进煎鱼了!"))
}
```

输出结果:

```
!了鱼煎进子脑
```

## 进行调试

Delve 是 Go 程序的源代码级调试器。Delve 使您能够通过控制流程的执行与您的程序进行交互,查看变量,提供线程、goroutine、CPU 状态等信息。

其一共支持如下 11 个子命令:

```
Available Commands:
  attach      Attach to running process and begin debugging.
  connect     Connect to a headless debug server.
  core        Examine a core dump.
  dap         [EXPERIMENTAL] Starts a TCP server communicating via Debug Adaptor Protocol (DAP).
  debug       Compile and begin debugging main package in current directory, or the package specified.
  exec        Execute a precompiled binary, and begin a debug session.
  help        Help about any command
  run         Deprecated command. Use 'debug' instead.
  test        Compile test binary and begin debugging program.
  trace       Compile and begin tracing program.
  version     Prints version.
```

我们今天主要用到的是 debug 命令,他能够编译并开始调试当前目录下的主包,或指定的包,是最常用的功能之一。

接下来我们利用这个演示程序来进行 dlv 的深入调试和应用。

执行如下命令:

```
➜  awesomeProject dlv debug .
Type 'help' for list of commands.
(dlv) 
```

我们先在演示程序根目录下执行了 debug,进入了 dlv 的交互模式。

再使用关键字 `b`(break 的缩写)对 `main.main` 方法设置断点:

```
(dlv) b main.main
Breakpoint 1 (enabled) set at 0x10cbab3 for main.main() ./main.go:9
(dlv) 
```

设置完毕后,我们可以看到方法对应的文件名、行数。接着我们可以执行关键字 `c`(continue 的缩写)跳转到下一个断点处:

```
(dlv) c
> main.main() ./main.go:9 (hits goroutine(1):1 total:1) (PC: 0x10cbab3)
     4:		"fmt"
     5:	
     6:		"github.com/eddycjy/awesome-project/stringer"
     7:	)
     8:	
=>   9:	func main() {
    10:		fmt.Println(stringer.Reverse("脑子进煎鱼了!"))
    11:	}
(dlv) 
```

在断点处,我看可以看到具体的代码块、goroutine、CPU 寄存器地址等运行时信息。

紧接着执行关键字 `n`(next 的缩写)单步执行程序的下一步:

```
(dlv) n
> main.main() ./main.go:10 (PC: 0x10cbac1)
     5:	
     6:		"github.com/eddycjy/awesome-project/stringer"
     7:	)
     8:	
     9:	func main() {
=>  10:		fmt.Println(stringer.Reverse("脑子进煎鱼了!"))
    11:	}
```

我们可以看到程序走到了 main.go 文件中的第 10 行中,并且调用了 `stringer.Reverse` 方法去处理。

此时我们可以执行关键字 `s`(step 的关键字)进入到这个函数中去继续调试:

```
(dlv) s
> github.com/eddycjy/awesome-project/stringer.Reverse() ./stringer/string.go:3 (PC: 0x10cb87b)
     1:	package stringer
     2:	
=>   3:	func Reverse(s string) string {
     4:		r := []rune(s)
     5:		for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
     6:			r[i], r[j] = r[j], r[i]
     7:		}
     8:		return string(r)
```

输入后,调试的光标会到 `Reverse` 方法上,此时我们可以调用关键字 `p`(print 的缩写)传出所传入的变量的值:

```
(dlv) p s
"脑子进煎鱼了!"
```

此处函数的形参变量是 s,输出了 “脑子进煎鱼了!”,与我们所传入的是一致的。

但故事一般没有这么的简单,会用到 Delve 来调试,说明是比较细致、隐患的 BUG。为此我们大多需要更进一步的深入。

我们继续围观 `Reverse` 方法:

```
     5:		for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
     6:			r[i], r[j] = r[j], r[i]
     7:		}
```

从表现来看,我们常常会怀疑是第 6 行可能是问题的所在。这时可以针对性的对第 6 行进行断点查看:

```
(dlv) b 6
Breakpoint 2 (enabled) set at 0x10cb92c for github.com/eddycjy/awesome-project/stringer.Reverse() ./stringer/string.go:6
```

设置完断点后,我们只需要执行关键字 `c`,继续下一步:

```
(dlv) c
> github.com/eddycjy/awesome-project/stringer.Reverse() ./stringer/string.go:6 (hits goroutine(1):1 total:1) (PC: 0x10cb92c)
     1:	package stringer
     2:	
     3:	func Reverse(s string) string {
     4:		r := []rune(s)
     5:		for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
=>   6:			r[i], r[j] = r[j], r[i]
     7:		}
     8:		return string(r)
     9:	}
```

走到对应的代码片段后,执行关键字 `locals`:

```
(dlv) locals
r = []int32 len: 7, cap: 32, [...]
j = 6
i = 0
```

我们就可以看到对应的变量 r, i, j 的值是多少,可以根据此来分析程序流转是否与我们预想的一致。

另外也可以调用关键字 `set` 去针对特定变量设置期望的值:

```
(dlv) set i = 1
(dlv) locals
r = []int32 len: 7, cap: 32, [...]
j = 6
i = 1
```

设置后,若还需要继续排查,可以继续调用关键字 `c` 去定位,这种常用于特定变量的特定值的异常,这样一设置一调试基本就能排查出来了。

在排查完毕后,我们可以执行关键字 `r`(reset 的缩写):

```
(dlv)  r
Process restarted with PID 56614
```

执行完毕后,整个调试就会重置,像是前面在打断点时所设置的变量值就会恢复。

若要查看设置的断点情况,也可以执行关键字 `bp` 查看:

```
(dlv) bp
Breakpoint runtime-fatal-throw (enabled) at 0x1038fc0 for runtime.fatalthrow() /usr/local/Cellar/go/1.16.2/libexec/src/runtime/panic.go:1163 (0)
Breakpoint unrecovered-panic (enabled) at 0x1039040 for runtime.fatalpanic() /usr/local/Cellar/go/1.16.2/libexec/src/runtime/panic.go:1190 (0)
	print runtime.curg._panic.arg
Breakpoint 1 (enabled) at 0x10cbab3 for main.main() ./main.go:9 (0)
Breakpoint 2 (enabled) at 0x10cb92c for github.com/eddycjy/awesome-project/stringer.Reverse() ./stringer/string.go:6 (0)
```

查看断点情况后,若有部分已经排除了,可以调用关键字 `clearall` 对一些断点清除:

```
(dlv) clearall main.main
Breakpoint 1 (enabled) cleared at 0x10cbab3 for main.main() ./main.go:9
```

若不指点断点,则会默认清除全部断点。

在日常的 Go 工程中,若都从 main 方法进入就太繁琐了。我们可以直接借助函数名进行调式定位:

```
(dlv) funcs Reverse
github.com/eddycjy/awesome-project/stringer.Reverse
(dlv) b stringer.Reverse
Breakpoint 3 (enabled) set at 0x10cb87b for github.com/eddycjy/awesome-project/stringer.Reverse() ./stringer/string.go:3
(dlv) c
> github.com/eddycjy/awesome-project/stringer.Reverse() ./stringer/string.go:3 (hits goroutine(1):1 total:1) (PC: 0x10cb87b)
     1:	package stringer
     2:	
=>   3:	func Reverse(s string) string {
     4:		r := []rune(s)
     5:		for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
     6:			r[i], r[j] = r[j], r[i]
     7:		}
     8:		return string(r)
```

紧接着其他步骤都与先前的一样,进行具体的调试就好了。我们也可以借助 Go 语言的公共函数进行计算:

```
(dlv) p len(r)-1
6
```

也可以借助关键字 `vars` 查看某个包下的所有全局变量的值,例如:`vars main`。这种方式对于查看全局变量的情况非常有帮助。

排查完毕后,执行关键字 `exit` 就可以愉快的退出了:

```
(dlv) exit
```

解决完问题,可以下班了 :)

## 总结

在 Go 语言中,Delve 调试工具是与 Go 语言亲和度最高的,因为 Delve 是 Go 语言实现的。其在我们日常工作中,非常常用。

像是假设程序的 for 循环运行到第 N 次才出现 BUG 时,我们就可以通过断点对应的方法和代码块,再设置变量的值,进行具体的查看,就可以解决。



================================================
FILE: content/posts/go/empty-struct.md
================================================
---
title: "详解 Go 空结构体的 3 种使用场景"
date: 2021-12-31T12:54:51+08:00
toc: true
images:
tags: 
  - go
---

大家好,我是煎鱼。

在大家初识 Go 语言时,总会拿其他语言的基本特性来类比 Go 语言,说白了就是老知识和新知识产生关联,实现更高的学习效率。

![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/87c66858abf04b848643b6fa03f4bdab~tplv-k3u1fbpfcp-zoom-1.image)

最常见的类比,就是 “Go 语言如何实现面向对象?”,进一步展开就是 Go 语言如何实现面向对象特性中的继承。

这不仅在学习中才用到类比,在业内的 Go 面试中也有非常多的面试官喜欢问:

![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d1cc9c2b2e174fc4a82506b368591a70~tplv-k3u1fbpfcp-zoom-1.image)

来自读者微信群

在今天这篇文章中,煎鱼带大家具体展开了解这块的知识。一起愉快地开始吸鱼之路。

什么是面向对象
-------

在了解 Go 语言是不是面向对象(简称:OOP) 之前,我们必须先知道 OOP 是啥,得先给他 “下定义”。

根据 Wikipedia 的定义,我们梳理出 OOP 的几个基本认知:

*   面向对象编程(OOP)是一种基于 "对象" 概念的编程范式,它可以包含数据和代码:数据以字段的形式存在(通常称为属性或属性),代码以程序的形式存在(通常称为方法)。
    
*   对象自己的程序可以访问并经常修改自己的数据字段。
    
*   对象经常被定义为类的一个实例。
    
*   对象利用属性和方法的私有/受保护/公共可见性,对象的内部状态受到保护,不受外界影响(被封装)。
    

基于这几个基本认知进行一步延伸出,面向对象的三大基本特性:

*   封装。
    
*   继承。
    
*   多态。
    

至此对面向对象的基本概念讲解结束,想更进一步了解的可自行网上冲浪。

Go 是面向对象的语言吗
------------

“Go 语言是否一门面向对象的语言?”,这是一个日经话题。官方 FAQ 给出的答复是:

![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bb8296e58a6d4c7c882984dcceaaf9c0~tplv-k3u1fbpfcp-zoom-1.image)

是的,也不是。原因是:

*   Go 有类型和方法,并且允许面向对象的编程风格,但没有类型层次。
    
*   Go 中的 "接口 "概念提供了一种不同的方法,我们认为这种方法易于使用,而且在某些方面更加通用。还有一些方法可以将类型嵌入到其他类型中,以提供类似的东西,但不等同于子类。
    
*   Go 中的方法比 C++ 或 Java 中的方法更通用:它们可以为任何类型的数据定义,甚至是内置类型,如普通的、"未装箱的 "整数。它们并不局限于结构(类)。
    
*   Go 由于缺乏类型层次,Go 中的 "对象 "比 C++ 或 Java 等语言更轻巧。
    

Go 实现面向对象编程
-----------

### 封装

面向对象中的 “封装” 指的是可以隐藏对象的内部属性和实现细节,仅对外提供公开接口调用,这样子用户就不需要关注你内部是怎么实现的。

在 Go 语言中的属性访问权限,通过首字母大小写来控制:

*   首字母大写,代表是公共的、可被外部访问的。
    
*   首字母小写,代表是私有的,不可以被外部访问。
    

Go 语言的例子如下:

```
type Animal struct {
 name string
}

func NewAnimal() *Animal {
 return &Animal{}
}

func (p *Animal) SetName(name string) {
 p.name = name
}

func (p *Animal) GetName() string {
 return p.name
}

```

在上述例子中,我们声明了一个结构体 `Animal`,其属性 `name` 为小写。没法通过外部方法,在配套上存在 Setter 和 Getter 的方法,用于统一的访问和设置控制。

以此实现在 Go 语言中的基本封装。

### 继承

面向对象中的 “继承” 指的是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。

![图片](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ec1cf116a1bf404981cb624009945157~tplv-k3u1fbpfcp-zoom-1.image)

图来自网络

从实际的例子来看,就是动物是一个大父类,下面又能细分为 “食草动物”、“食肉动物”,这两者会包含 “动物” 这个父类的基本定义。

在 Go 语言中,是没有类似 `extends` 关键字的这种继承的方式,在语言设计上采取的是组合的方式:

```
type Animal struct {
 Name string
}

type Cat struct {
 Animal
 FeatureA string
}

type Dog struct {
 Animal
 FeatureB string
}

```

在上述例子中,我们声明了 `Cat` 和 `Dog` 结构体,其在内部匿名组合了 `Animal` 结构体。因此 `Cat` 和 `Dog` 的实例都可以调用 `Animal` 结构体的方法:

```
func main() {
 p := NewAnimal()
 p.SetName("煎鱼,记得点赞~")

 dog := Dog{Animal: *p}
 fmt.Println(dog.GetName())
}

```

同时 `Cat` 和 `Dog` 的实例可以拥有自己的方法:

```
func (dog *Dog) HelloWorld() {
 fmt.Println("脑子进煎鱼了")
}

func (cat *Cat) HelloWorld() {
 fmt.Println("煎鱼进脑子了")
}

```

上述例子能够正常包含调用 `Animal` 的相关属性和方法,也能够拥有自己的独立属性和方法,在 Go 语言中达到了类似继承的效果。

### 多态

面向对象中的 “多态” 指的同一个行为具有多种不同表现形式或形态的能力,具体是指一个类实例(对象)的相同方法在不同情形有不同表现形式。

多态也使得不同内部结构的对象可以共享相同的外部接口,也就是都是一套外部模板,内部实际是什么,只要符合规格就可以。

在 Go 语言中,多态是通过接口来实现的:

```
type AnimalSounder interface {
 MakeDNA()
}

func MakeSomeDNA(animalSounder AnimalSounder) {
 animalSounder.MakeDNA()
}

```

在上述例子中,我们声明了一个接口类型 `AnimalSounder`,配套一个 `MakeSomeDNA` 方法,其接受 `AnimalSounder` 接口类型作为入参。

因此在 Go 语言中。只要配套的 `Cat` 和 `Dog` 的实例也实现了 `MakeSomeDNA` 方法,那么我们就可以认为他是 `AnimalSounder` 接口类型:

```
type AnimalSounder interface {
 MakeDNA()
}

func MakeSomeDNA(animalSounder AnimalSounder) {
 animalSounder.MakeDNA()
}

func (c *Cat) MakeDNA() {
 fmt.Println("煎鱼是煎鱼")
}

func (c *Dog) MakeDNA() {
 fmt.Println("煎鱼其实不是煎鱼")
}

func main() {
 MakeSomeDNA(&Cat{})
 MakeSomeDNA(&Dog{})
}

```

当 `Cat` 和 `Dog` 的实例实现了 `AnimalSounder` 接口类型的约束后,就意味着满足了条件,他们在 Go 语言中就是一个东西。能够作为入参传入 `MakeSomeDNA` 方法中,再根据不同的实例实现多态行为。

总结
--

通过今天这篇文章,我们基本了解了面向对象的定义和 Go 官方对面向对象这一件事的看法,同时针对面向对象的三大特性:“封装、继承、多态” 在 Go 语言中的实现方法就进行了一一讲解。

在日常工作中,基本了解这些概念就可以了。若是面试,可以针对三大特性:“封装、继承、多态” 和 五大原则 “单一职责原则(SRP)、开放封闭原则(OCP)、里氏替换原则(LSP)、依赖倒置原则(DIP)、接口隔离原则(ISP)” 进行深入理解和说明。

在说明后针对上述提到的概念。再在 Go 语言中讲解其具体的实现和利用到的基本原理,互相结合讲解,就能得到一个不错的效果了。

## 鼓励

若有任何疑问欢迎评论区反馈和交流,**最好的关系是互相成就**,各位的**点赞**就是[煎鱼](https://github.com/eddycjy)创作的最大动力,感谢支持。

> 文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 **GitHub** [github.com/eddycjy/blog](https://github.com/eddycjy/blog) 已收录,欢迎 Star 催更。


参考
--

*   Is Go an Object Oriented language?
    
*   面向对象的三大基本特征,五大基本原则
    
*   Go 面向对象编程(译)

================================================
FILE: content/posts/go/enum.md
================================================
---
title: "小技巧分享:在 Go 如何实现枚举?"
date: 2021-12-31T12:54:50+08:00
toc: true
images:
tags: 
  - go
---

大家好,我是煎鱼。

在日常的业务工程开发中,我们常常会有使用枚举值的诉求,枚举控的好,测试值边界一遍过...

有的小伙伴会说,在 Go 语言不是有 `iota` 类型做枚举吗,那煎鱼你这篇文章还讲什么?

讲道理,Go 语言并没有 enum 关键字,有用过 Protobuf 等的小伙伴知道,Go 语言只是 ”有限的枚举“ 支持,我们也会用常量来定义,枚举值也需要有字面意思的映射。

示例
--

在一些业务场景下,是没法达到我们的诉求的。示例如下:

```
type FishType int

const (
 A FishType = iota
 B
 C
 D
)

func main() {
 fmt.Println(A, B, C, D)
}
```

输出结果为:“0 1 2 3”。这时候就一脸懵逼了...枚举值,应该除了键以外,还得有个对应的值。也就是这个 “0 1 2 3” 分别对应着什么含义,是不是应该输出 ”A B C D“

但 Go 语言这块就没有直接的支撑了,因此这不是一个完整的枚举类型的实现。

同时假设我们传入超过 `FishType` 类型声明范围的枚举值,在 Go 语言中默认也不会有任何控制,是正常输出的。

上述这种 Go 枚举实现,在某种情况下是不完全的,严格意义上不能成为 enum(枚举)。

使用 String 做枚举
-------------

如果要支持枚举值的对应输出的话,我们可以通过如下方式:

```
type FishType int

const (
 A FishType = iota
 B
 C
 D
)

func (f FishType) String() string {
 return [...]string{"A", "B", "C", "D"}[f]
}
```

运行程序:

```
func main() {
 var f FishType = A
 fmt.Println(f)
 switch f {
 case A:
  fmt.Println("脑子进煎鱼了")
 case B:
  fmt.Println("记得点赞")
 default:
  fmt.Println("别别别...")
 }
}
```

输出结果:

```
A
脑子进煎鱼了
```

我们可以借助 Go 中 `String` 方法的默认约定,针对于定义了 `String` 方法的类型,默认输出的时候会调用该方法。

这样就可以达到获得枚举值的同时,也能拿到其映射的字面意思。

自动生成 String
-----------

但每次手动编写还是比较麻烦的。在这一块,我们可以利用官方提供的 `cmd/string` 来快速实现。

我们安装如下命令:

```
go install golang.org/x/tools/cmd/stringer
```

在所需枚举值上设置 `go:generate` 指令:

```
//go:generate stringer -type=FishType
type FishType int
```

在项目根目录执行:

```
go generate ./...

```

会在根目录生成 fishtype\_string.go 文件:

```
.
├── fishtype_string.go
├── go.mod
├── go.sum
└── main.go
```

fishtype\_string 文件内容:

```
package main

import "strconv"

const _FishType_name = "ABCD"

var _FishType_index = [...]uint8{0, 1, 2, 3, 4}

func (i FishType) String() string {
 if i < 0 || i >= FishType(len(_FishType_index)-1) {
  return "FishType(" + strconv.FormatInt(int64(i), 10) + ")"
 }
 return _FishType_name[_FishType_index[i]:_FishType_index[i+1]]
}
```

所生成出来的文件,主要是根据枚举值和映射值做了个映射,且针对超出枚举值的场景进行了判断:

```
func main() {
 var f1 FishType = A
 fmt.Println(f1)
 var f2 FishType = E
 fmt.Println(f2)
}
```

执行 `go run .` 查看程序运行结果:

```
$ go run .
A
FishType(4)
```

总结
--

在今天这篇文章中,我们介绍了如何在 Go 语言实现标准的枚举值,虽然有些繁琐,但整体不会太难。

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/05d5614987b04e0183e20fb5ac5249d4~tplv-k3u1fbpfcp-zoom-1.image)

也有小伙伴已经在社区中提出了 ”proposal: spec: add typed enum support“ 的提案,相信未来有机会能看到 Go 自身支持 enum(枚举)的那一天。

你平时会怎么在业务代码中实现枚举呢,欢迎大家一起留言交流:)


**欢迎大家在评论区留言和交流 :)**

## 鼓励

若有任何疑问欢迎评论区反馈和交流,**最好的关系是互相成就**,各位的**点赞**就是[煎鱼](https://github.com/eddycjy)创作的最大动力,感谢支持。

> 文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 **GitHub** [github.com/eddycjy/blog](https://github.com/eddycjy/blog) 已收录,学习 Go 语言可以看 [Go 学习地图和路线](https://github.com/eddycjy/go-developer-roadmap),欢迎 Star 催更。

================================================
FILE: content/posts/go/func-reload.md
================================================
---
title: "Go 为什么不支持函数重载和参数默认值?"
date: 2021-12-31T12:55:16+08:00
toc: true
images:
tags: 
  - go
  - 为什么
---

大家好,我是煎鱼。

大家在初学习 Go 语言时,带着其他语言的习惯,总是会有些不习惯,感觉非常不能理解,直打问号。

其中一点就是 Go 语言不支持函数重载和参数默认值,觉得使用起来很不方便。

为此,在这篇文章中煎鱼就和大家一起来了解为什么,有又会怎么样。

## 函数重载

函数重载(function overloading),也叫方法重载。是某些编程语言(如 C++、C#、Java、Swift、Kotlin 等)具有的一项特性。

该特性**允许创建多个具有不同实现的同名函数**,对重载函数的调用会运行其适用于调用上下文的具体实现。

从功能上来讲,就是允许一个函数调用根据上下文执行不同的方法,达到调用同一个函数名,执行不同的方法。

一个简单的例子:

```c++
#include <iostream>

int Volume(int s) {  // 立方体的体积。
  return s * s * s;
}

double Volume(double r, int h) {  // 圆柱体的体积。
  return 3.1415926 * r * r * static_cast<double>(h);
}

long Volume(long l, int b, int h) {  // 长方体的体积。
  return l * b * h;
}

int main() {
  std::cout << Volume(10);
  std::cout << Volume(2.5, 8);
  std::cout << Volume(100l, 75, 15);
}
```

在上述例子中,实现了 3 个同名的 `Volume` 函数,但是 3 个函数的入参个数、类型均不一样,也代表了不同的实现目的。

在主函数 `main` 中,传入了不同的入参,编译器或运行时再进行内部处理,从程序上来看达到了调用不同函数的目的。

这就是函数重载,一函数多形态。

## 参数默认值

参数默认值,又叫缺省参数。指的是允许程序员设定缺省参数并指定默认值,**当调用该函数并未指定值时,该缺省参数将为缺省值来使用**。

一个简单的例子:

```c++
 int my_func(int a, int b, int c=12);
```

在上述例子中,函数 `my_func` 一共有 3 个变量,分别是:a、b、c。变量 c 设置了缺省值,也就是 12。

其调用方式可以为:

```c++
 // 第一种调用方式
 result = my_func(1, 2, 3);
 // 第二种调用方式
 result = my_func(1, 2);
```

在第一种方式中,就会正常的传入所有参数。在第二种方式,由于第三个参数 c 并没有传递,因此会直接使用缺省值 12。

这就是参数默认值,也叫缺省参数。

## 为什么不支持

### 美好

从上述的功能特性介绍来看,似乎非常的不错,能够节省很多功夫。像是 Go 语言的 context 库中的这些方法:

```golang
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
```

要是有函数重载,直接就 WithXXX 就好了,只需要关注传入的参数类型,也不用 “记” 那么多个方法名了。

有同学说,有参数默认值。那就可以直接设置在上面,作为 “最佳实践” 给到使用函数的人,岂不美哉。那怎么 Go 语言就不支持呢?

### 细思

其实这和设计理念,和对程序的理解有关系。说白了,就是你喜欢 “显式”,还是 “隐喻”。

函数重载和参数默认值,其实是不好的行为。调用者只看函数名字,可能没法知道,你这个默认值,又或是入参不同,会调用的东西,会产生怎么样的后果?

你可以观察一下自己的行为。大部分人都会潜意识的追进去看代码,看看会调到哪,缺省值的作用是什么,以确保可控。

### 敲定

这细思的可能,在 Go 语言中是不被允许的。Go 语言的**设计理念就是 “显式大于隐喻”,追求明确,显式**。

在 Go FAQ 《Why does Go not support overloading of methods and operators?》有相关的解释。

如下图:

![](https://files.mdnice.com/user/3610/582eac4e-ecd1-4fb5-bde7-b2cbcac7f809.png)

官方有明确提到两个观点:
- 函数重载:拥有各种同名但不同签名的方法有时是很有用的,但在实践中也可能是混乱和脆弱的。
- 参数默认值:操作符重载,似乎更像是一种便利,不是绝对的要求。没有它,程序会更简单。

这就是为什么 Go 语言不支持的原因。

## 总结

在这篇文章中,我们介绍了业内常见的编程语言的函数重载和参数默认值的概念和使用方法。也结合了 Go 语言自身的设计理念,说明了为什么不支持的原因。

你会希望 Go 语言支持这几个特性功能吗,欢迎在评论区留言讨论和交流:)

## 参考

- 维基百科(函数重载和缺省值定义)
- Frequently Asked Questions (FAQ)

================================================
FILE: content/posts/go/fuzzing.md
================================================
---
title: "提高 Go 程序健壮性,Fuzzing 要来了!"
date: 2021-12-31T12:54:50+08:00
toc: true
images:
tags: 
  - go
---

大家好,我是煎鱼。

就在前几天,Go1.17 beta1 正式发布:

![](https://files.mdnice.com/user/3610/fb4ceb3d-ef1c-4c32-a7c3-a4188a0d74b0.png)

兴冲冲本想着看一下当初在 Go1.17 的计划中,预计会支持的新特性:模糊测试(Fuzzing)。不过没想到...计划赶不上变化,官方正式宣告 Fuzzing 不会出现在 Go1.17 的新功能中。

煎鱼在悲伤之际,发现 Go 在 dev.fuzz 分支上提供了该功能的 Beta 测试,因此今天带大家一起来深入该特性。

## 什么是 Fuzzing

Fuzzing 是一种自动测试技术,包括向计算机程序提供随机数据作为输入。然后监测程序是否出现恐慌、断言失败、无限循环等。

Fuzzing 不是使用一个小的、预先定义好的手动创建的输入集(如单元测试),而是用新的案例不断地测试代码,以努力 ”锻炼“ 有关软件的所有方面。

这听起来很 ”难“。但在过去的几年里,Fuzzing 的技术水平有了很大的提高。Fuzzing 不再是需要专业知识才能成功使用的东西,现代模糊测试策略能更快、更有效地找到有用的输入。

在应用程序中,就是你只要引入一个 package,对着 API 一顿用就可以了。

## 为什么要做 Fuzzing

可能会有小伙伴说,测试?直接人工测试,再把测试数据准备一下,配套 YAPI 等接口管理平台,把自动化接口测试一弄就好了。还需要 Fuzzing 吗?

其实 Fuzzing 是对其他形式的测试、代码审查和静态分析的补充,它通过生成一个随机测试用例去覆盖人为测不到的各种复杂场景。而这些输入几乎不可能人为去构造,总会被传统测试所遗漏。

## 发生在身边的 Fuzzing

实际上 Go-fuzz 对 Go 标准库进行过测试,依然这这之中发现了 200  多个 bug:

![](https://files.mdnice.com/user/3610/9b00972c-2231-4406-84b8-2e8e44f57d6d.png)

这还是建立在标准库已经比较成熟,且由非常有经验的开发者编写,在生产中使用多年的情况下,依然有如此多的问题。

## 快速上手

我们需要在本地执行如下命令,需开启 GO111MODULE 和天梯:

```golang
$ go get golang.org/dl/gotip
$ gotip download dev.fuzz
```

执行完毕后会从 dev.fuzz 分支构建 Go 工具链,同时 gotip 可以作为 go 命令的替代者命令,也就是可以运行 Fuzzing 的相关代码了。

```golang
// +build gofuzzbeta

package tests

import (
	"net/url"
	"reflect"
	"testing"
)

func FuzzParseQuery(f *testing.F) {
	f.Add("x=1&y=2")
	f.Fuzz(func(t *testing.T, queryStr string) {
		query, err := url.ParseQuery(queryStr)
		if err != nil {
			t.Skip()
		}
		queryStr2 := query.Encode()
		query2, err := url.ParseQuery(queryStr2)
		if err != nil {
			t.Fatalf("ParseQuery failed to decode a valid encoded query %s: %v", queryStr2, err)
		}
		if !reflect.DeepEqual(query, query2) {
			t.Errorf("ParseQuery gave different query after being encoded\nbefore: %v\nafter: %v", query, query2)
		}
	})
}
```

在相应的目录下执行 `gotip test -fuzz=FuzzParseQuery` 命令,输出结果:

```golang
fuzzing, elapsed: 3.0s, execs: 319 (106/sec), workers: 4, interesting: 15
fuzzing, elapsed: 6.0s, execs: 665 (111/sec), workers: 4, interesting: 15
fuzzing, elapsed: 9.0s, execs: 1019 (113/sec), workers: 4, interesting: 15
fuzzing, elapsed: 12.0s, execs: 1400 (117/sec), workers: 4, interesting: 15
...
```

需要注意的是:
- Fuzzing 会消耗大量的内存,在运行时会影响到机器的性能(一运行,小风扇就转了起来)。
- Fuzzing 会默认使用 `GOMAXPROCS`相同的核数,可以通过执行 `-parallel` 标识来控制数量。
- Fuzzing 会默认在运行时,将扩大测试范围的数值写入 `$GOCACHE/fuzz` 内的模糊缓存目录,目前是没有限制的,可以通过运行 `gotip clean -fuzzcache` 来清除。

## 总结

在今天这篇文章中,我们介绍了 Fuzzing 是什么。
简单而言,模糊测试(Fuzzing)在真实环境已经被验证了其有效性,其可以随机生成测试用例去覆盖人为测不到的各种复杂场景,带来很大的收益。

在接下来中,除了依赖开源的 go-fuzz 库外,Go 语言也正式的在支持 Fuzzing,虽然他放了 Go1.17 的鸽子...

这会对构建 Go 程序健壮性的又一强心剂!

================================================
FILE: content/posts/go/gdb.md
================================================
---
title: "学会使用 GDB 调试 Go 代码"
date: 2021-12-31T12:54:57+08:00
toc: true
images:
tags: 
  - go
---

大家好,我是煎鱼。

上一篇文章《一个 Demo 学会使用 Go Delve 调试》我们详细介绍了 Go 语言如何使用 Delve 进行排查和调试,对于问题的解决非常的有帮助。

但调试工具肯定不止只有 Delve,今天我们来介绍第二个神器,那就是:GDB。

## GDB 是什么

GDB 是一个类 UNIX 系统下的程序调试工具,允许你看到另一个程序在执行时 "内部 "发生了什么,或者程序在崩溃时正在做什么。

![GDB Logo](https://files.mdnice.com/user/3610/35fc6475-ec0d-44a0-ae8c-920121130edb.png)

主要可以做四类事情:

1. 启动你的程序,指定任何可能影响其行为的东西。
2. 使你的程序在指定的条件下停止。
3. 检查当你的程序停止时发生了什么。
4. 改变你程序中的东西,这样你就可以试验纠正一个错误的影响,并继续了解另一个错误。

## 安装

如果是在 MacOS 上的话,可以直接使用 brew 安装:

```
brew install gdb
```

如果是在 Linux ,则使用自带的包管理工具进行安装即可,但需要注意安装完毕后需要在 HOME 目录进行相关配置。

安装完毕后,执行 `gdb` 就可以看到:

```
$ gdb
GNU gdb (GDB) 10.2
...
(gdb) 
```

写此文时最新的 gdb 版本已经是 10.2 了,我也升级了上去。问题不大,还多了不少功能。

## 编译

我们还是使用先前的演示程序来进行调试。但由于 Go 语言的不少编译优化,因此在编译运行程序时,有以下几点需要注意:

- go build 编译时需要增加 `-gcflags=all="-N -l"` 指令来关闭内联优化,方便接下来的调试。

- 若是 MacOS,在 go build 编译时需要增加 `-ldflags='-compressdwarf=false'` 指令。
  - 若不禁止,则会出现 `No symbol table is loaded. Use the "file" command.` 的错误。
  - Go 编译默认为了减少二进制大小会默认压缩 DWARF 调试信息,但这会影响 gdb 的调试,因此需要将其关闭。
  
编译的命令是:

```
$ go build -gcflags=all="-N -l" -ldflags='-compressdwarf=false' .
```

输出结果:

```
!了鱼煎进子脑
```
  
## 尝试 gdb

GDB 有两种调试模式,分别是文本用户界面(Text User Interface,简称 tui)和默认的命令行模式:

```
// 调试界面
$ gdb -tui ./awesome-project

// 命令行模式
$ gdb ./awesome-project
```

接下来我们使用 gdb tui 的调试模式来给大家演示功能。

我们在执行命令 `gdb -tui ./awesome-project` 后,窗口会切换为如下:


![gdb tui 初始样子](https://files.mdnice.com/user/3610/2a7096f4-addd-404f-9a47-4a97cbe9d595.png)

你会发现中间提示 “No Source Available”,此时你需要继续回车两次,他就会自动加载插件支持,提示:“Loading Go Runtime support.”。

我们就可以看到具体的代码块内容,如下:

![](https://files.mdnice.com/user/3610/351571a3-b89e-42f2-941e-24e734bfae26.png)

用 MacOS 的同学需要注意,如果你在断点时发现发现了如下错误:

```
(gdb) b main.main
Breakpoint 1 at 0x10a2ea0: file /Users/eddycjy/go-application/awesomeProject/main.go, line 15.
(gdb) r
Starting program: /Users/eddycjy/go-application/awesomeProject/hello
Unable to find Mach task port for process-id 64212: (os/kern) failure (0x5).
 (please check gdb is codesigned - see taskgated(8))
```

也就是 “please check gdb is codesigned - see taskgated(8)”,则需要重新处理证书认证和授权,是 MacOS 使用上的一个问题,具体可参考:《[Codesign gdb on OSX](https://gist.github.com/hlissner/898b7dfc0a3b63824a70e15cd0180154)》。

解决后,咱们的 gdb 就算是能够正确的运行起来了!

## 常用 gdb 命令

在 gdb 中,和 dlv 一样有常用的关键字命令。当然了,gdb 的 help all 输出非常多:

```
(gdb) help all

Command class: aliases
Command class: breakpoints

awatch -- Set a watchpoint for an expression.
break, brea, bre, br, b -- Set breakpoint at specified location.
break-range -- Set a breakpoint for an address range.
catch -- Set catchpoints to catch events.
...
```

常用的关键字如下:
- b:break 的缩写,作用是打断点,例如:main.main,可带代码行数。
- r:run 的缩写,作用是运行程序到下一个断点处。
- c:continue 的缩写,作用是继续执行到下一个断点。
- s:step 的缩写,作用是单步执行,如果有所调用的方法,将会进入该方法。
- l:list 的缩写,作用是查看对应的源码。
- n:next 的缩写,作用是单步执行,不会进入所调用的方法,。
- q:quit 的缩写,作用是退出。
- info breakpoints:作用是查看所有设置的断点信息。
- info locals:作用是查看变量信息。
- info args:作用是查看函数的入参和出参的具体值。
- info goroutines:作用是查看 goroutines 的信息。
- goroutine 1 bt:作用是查看指定序号的 goroutine 调用堆栈。

## 进行调试

在调试上与 dlv 差不多,也是先执行关键字 `b` 打断点:

```
(gdb) b main.main
Breakpoint 1 at 0x10cbaa0: file /Users/eddycjy/go-application/awesomeProject/main.go, line 9.
```

也可以先执行关键字 `l` 查看对应的代码情况再进行做决定:

```
(gdb) l main.main
4		"fmt"
5	
6		"github.com/eddycjy/awesome-project/stringer"
7	)
8	
9	func main() {
10		fmt.Println(stringer.Reverse("脑子进煎鱼了!"))
11	}
```

查看对应 goroutines 正在运行的函数情况:

```
(gdb) info goroutines
  1  waiting runtime.gosched
* 13  running runtime.goexit
```

根据 pprof 等所得到的 goroutine 序号进行进一步的分析:

```
(gdb) goroutine 1 bt
#0  0x000000000040facb in runtime.gosched () at /home/user/go/src/runtime/proc.c:873
#1  0x00000000004031c9 in runtime.chanrecv (c=void, ep=void, selected=void, received=void)
 at  /home/user/go/src/runtime/chan.c:342
#2  0x0000000000403299 in runtime.chanrecv1 (t=void, c=void) at/home/user/go/src/runtime/chan.c:423
#3  0x000000000043075b in testing.RunTests (matchString...
```

注意一个细节,gdb 调试是可以看到并对 runtime 包内容的代码进行断点和分析的。

也可以和 dlv 一样执行 p 关键字输出相应的值的类型、值内容:

```
(gdb) p re
(gdb) p t
$1 = (struct testing.T *) 0xf840688b60
(gdb) p t
$1 = (struct testing.T *) 0xf840688b60
(gdb) p *t
$2 = {errors = "", failed = false, ch = 0xf8406f5690}
(gdb) p *t->ch
$3 = struct hchan<*testing.T>
```

与 dlv 大同小异。

## 总结

总体上来讲,MacOS 上使用 gdb 还是挺麻烦的,在 Linux 环境下使用 gdb 还是更方便些。

由于 **dlv 和 gdb 在大致的调试上不会差距的太远**,因此本文就没有过于展开。

若是对业务代码进行分析,更建议使用 dlv,也就是我们上一篇文章所讲的内容。**若有 runtime 库的调试需求的话,推荐使用 gdb 来作为首要调试工具**,若无这方面诉求,建议使用 dlv。

================================================
FILE: content/posts/go/generics-apis.md
================================================
---
title: "出泛型后 API 怎么办?Go 开发者要注意了"
date: 2021-12-31T12:55:14+08:00
toc: true
images:
tags: 
  - go
---

大家好,我是煎鱼。

前段时间社区里一下子就爆了,主要是各大媒体引用了 Go 语言之父 
Rob Pike 所提的《go: don't change the libraries in 1.18》。

![](https://files.mdnice.com/user/3610/08e47a87-cf63-4dec-b0b5-fc178a494c4c.png)

很多社交媒体都做了跟进,认为 Rob Pike 是硬性的反对 Go 泛型的 API 改造!

如果读者只看了标题,有可能会产生一些误解实际上其表达的意思和近期 Go 社区讨论的事项是有关联的,要一起综合来看。

为此,今天煎鱼就和大家一起来理一理,看看 Go 泛型 API 的改造工程,是个怎么一回事?

## 现状

马上就是 2021.11 月,连深圳都变冷了...根据 Go 语言的发布周期,Go1.18 版本的发布,那就是 2022.02 月左右。

![](https://files.mdnice.com/user/3610/f849c76d-e84d-43d6-96c5-b30df77f1dd9.png)

现在给到 Ian Lance Taylor、
Robert Griesemer 等大佬仅剩 3 个月的时间给大家讨论泛型细节,进一步完善实现,达到生产可用。

抛出 Go 泛型的实现进度不说,现在遇到了一个比较大的问题。那就是**实现泛型后 ”如何更新泛型的 API“**。

这之中包含好几个方面,分别是:既有标准库、开源库,新标准库等。不同库之间是不同的人在维护。

但这里存在一个大问题,如下图:

![](https://files.mdnice.com/user/3610/033083ef-d0ba-40f7-93d4-006e65310cf9.png)

Russ Cox 在 9 月就提出了 ”how to update APIs for generics“ 的疑惑,当时显然这一块还没有共识。在 11 月的现在,从讨论的记录来看,**怎么做还没有达成一个最终的明确共识**(初步已有,未正式答复)。

但存在一个问题,Go 社区对于泛型的迫切度,热情非常高,各种泛型化的标准库的提案都提出来了,推着设计者往前走。

## 争议

结合来看 Rob Pike,更多是:建议和提醒 Go 社区和核心开发团队,**要 ”悠着点“**,Go1.18 想支持泛型,做完成库的改造,还得代价小,毕竟细节很多。

引用其理由,核心论据是:
- 在一个版本中,做泛型、标准库等,要做的事情太多,很可能会弄错。
- 没有在 Go 中使用新类型的经验,无法为其设计提供有力的依据。
- Go1 兼容性的承诺,在任何细节上出错的代价都很高,要等待、观察和学习。

和一句谚语很接近:”**不要一口气吃胖子**“,何况没有相关的经验,都只是详细的推理、预演,需要晋升。

在 Go issues 中也有人吐槽,1.18 空有泛型的实现。其他配套的标准库等都没有,那这个 Go1.18 出来的泛型意义是?

## 后续

虽然还没有最终拍板,但是根据讨论的过程和社区赞同数(👍)来看,如下:

![](https://files.mdnice.com/user/3610/4856e2c4-7ae4-421c-812b-561ea6cf1cef.png)

后续仍然会设计、构建、测试和使用用于切片(Slice)、地图(Map)、通道(Channel)等的新库。

这些库并没有生产可用,会把他们**放在 golang/x/exp 仓库**中,可以使用,仅作为现阶段的实验性的库,没有兼容性保障。


![](https://files.mdnice.com/user/3610/47b8cae4-4ef8-42a3-af6c-686dd8acfb7a.png)


该实验库会在一两个周期内会改变、调整和发展。能够让 Go 社区的开发者们尝试一下使用,以便接受更多的意见。

再根据使用者的反馈通过经验和分析进行更新,就会把它们移到主仓库中,才达到正式生产可用的级别。

## 总结

在今天这篇文章中,我们针对 Rob Pike 为什么会要调整 Go 泛型后的标准库 API 等的提议进行了分析。

为此我们了解到 Go 核心团队对 ”how to update APIs for generics“ 的顾虑,以及现有社区的激情,综合来看,给出的逐步演进的泛型方案建议。

以此可知,Go 完整泛型(含配套库)的生产可用,可能还要经历几个 Go 版本,让不少人望穿秋水了...


================================================
FILE: content/posts/go/generics-design.md
================================================
---
title: "Go 泛型的 3 个核心设计,你学会了吗?"
date: 2022-02-05T15:52:46+08:00
toc: true
images:
tags: 
  - go
---

大家好,我是煎鱼。

Go1.18 的泛型是闹得沸沸扬扬,虽然之前写过很多篇针对泛型的一些设计和思考。但因为泛型的提案之前一直还没定型,所以就没有写完整介绍。

如今已经基本成型,就由煎鱼带大家一起摸透 Go 泛型。本文内容主要涉及泛型的 3 大概念,非常值得大家深入了解。

如下:
- 类型参数。
- 类型约束。
- 类型推导。

## 类型参数

类型参数,这个名词。不熟悉的小伙伴咋一看就懵逼了。

泛型代码是使用抽象的数据类型编写的,我们将其称之为类型参数。当程序运行通用代码时,类型参数就会被类型参数所取代。也就是**类型参数是泛型的抽象数据类型**。

简单的泛型例子:

```go

func Print(s []T) {
	for _, v := range s {
		fmt.Println(v)
	}
}
```

代码有一个 `Print` 函数,它打印出一个片断的每个元素,其中片断的元素类型,这里称为 T,是未知的。

这里引出了一个要做泛型语法设计的点,那就是:T 的**泛型类型参数,应该如何定义**?

在现有的设计中,分为两个部分:
- 类型参数列表:**类型参数列表将会出现在常规参数的前面**。为了区分类型参数列表和常规参数列表,类型参数列表**使用方括号**而不是小括号。
- 类型参数约束:如同常规参数有类型一样,类型参数也有元类型,被称为约束(后面会进一步介绍)。

结合完整的例子如下:

```go
// Print 可以打印任何片断的元素。
// Print 有一个类型参数 T,并有一个单一的(非类型)的 s,它是该类型参数的一个片断。
func Print[T any](s []T) {
	// do something...
}
```

在上述代码中,我们声明了一个函数 `Print`,其有一个类型参数 T,类型约束为 `any`,表示为任意的类型,作用与 `interface{}` 一样。他的入参变量 `s` 是类型 T 的切片。

函数声明完了,在函数调用时,我们需要指定类型参数的类型。如下:

```go
	Print[int]([]int{1, 2, 3})
```

在上述代码中,我们指定了传入的类型参数为 int,并传入了 `[]int{1, 2, 3}` 作为参数。

其他类型,例如 float64:

```go
	Print[float64]([]float64{0.1, 0.2, 0.3})
```

也是类似的声明方式,照着套就好了。

## 类型约束

说完类型参数,我们再说说 “约束”。在所有的类型参数中都要指定类型约束,才能叫做完整的泛型。

以下分为两个部分来具体展开讲解:
- 定义函数约束。
- 定义运算符越苏

### 为什么要有类型约束

为了**确保调用方能够满足接受方的程序诉求**,保证程序中所应用的函数、运算符等特性能够正常运行。

泛型的类型参数,类型约束,相辅相成。

### 定义函数约束

#### 问题点

我们看看 Go 官方所提供的例子:

```go
func Stringify[T any](s []T) (ret []string) {
	for _, v := range s {
		ret = append(ret, v.String()) // INVALID
	}
	return ret
}
```

该方法的实现目的是:任何类型的切片都能转换成对应的字符串切片。但程序逻辑里有一个问题,那就是他的入参 T 是 `any` 类型,是任意类型都可以传入。

其内部又调用了 `String` 方法,自然也就会报错,因为只像是 int、float64 等类型,就可能没有实现该方法。

你说要定义有效的类型约束,那像是上面的例子,在泛型中如何实现呢?

要求传入方要有内置方法,就得定义一个 `interface` 来约束他。

#### 单个类型

例子如下:

```go
type Stringer interface {
	String() string
}
```

在泛型方法中应用:

```go
func Stringify[T Stringer](s []T) (ret []string) {
	for _, v := range s {
		ret = append(ret, v.String())
	}
	return ret
}
```

再将 `Stringer` 类型放到原有的 `any` 类型处,就可以实现程序所需的诉求了。

#### 多个类型

如果是多个类型约束。例子如下:

```go
type Stringer interface {
	String() string
}

type Plusser interface {
	Plus(string) string
}

func ConcatTo[S Stringer, P Plusser](s []S, p []P) []string {
	r := make([]string, len(s))
	for i, v := range s {
		r[i] = p[i].Plus(v.String())
	}
	return r
}
```

与常规的入参、出参类型声明一样的规则。

### 定义运算符约束

完成了函数约束的定义后,剩下一个要啃的大骨头就是 “运算符” 的约束了。

#### 问题点

我们看看 Go 官方的例子:

```go
func Smallest[T any](s []T) T {
	r := s[0] // panic if slice is empty
	for _, v := range s[1:] {
		if v < r { // INVALID
			r = v
		}
	}
	return r
}
```

经过上面的函数例子,我们很快能意识到这个程序根本无法运行成功。

其入参是 `any` 类型,程序内部是按 slice 类型来获取值,且在内部又进行运算符比较,那如果真是 slice,内部就可能每个值类型都不一样。

如果一个是 slice,一个是 int 类型,又如何进行运算符的值对比?

#### 近似元素

可能有的同学想到了重载运算符,但...想太多了,Go 语言没有支持的计划。为此做了一个新的设计,那就是允许限制类型参数的类型范围。

语法如下:

```go
InterfaceType  = "interface" "{" {(MethodSpec | InterfaceTypeName | ConstraintElem) ";" } "}" .
ConstraintElem = ConstraintTerm { "|" ConstraintTerm } .
ConstraintTerm = ["~"] Type .
```

例子如下:

```go
type AnyInt interface{ ~int }
```

上述声明的类型集是 `~int`,也就是所有类型为 int 的类型(如:int、int8、int16、int32、int64)都能够满足这个类型约束的条件。

包括底层类型是 int8 类型的,例如:

```go
type AnyInt8 int8
```

也就是在该匹配范围内的。

#### 联合元素

如果希望进一步缩小限定类型,可以结合分隔符来使用,用法为:

```go
type AnyInt interface{
 ~int8 | ~int64
}
```

就可以将类型集限定在 int8 和 int64 之中。

#### 实现运算符约束

基于新的语法,结合新的概念联合和近似元素,可以把程序改造一下,实现在泛型中的运算符的匹配。

类型约束的声明,如下:

```go
type Ordered interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 |
		~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
		~float32 | ~float64 |
		~string
}
```

应用的程序如下:

```go
func Smallest[T Ordered](s []T) T {
	r := s[0] // panics if slice is empty
	for _, v := range s[1:] {
		if v < r {
			r = v
		}
	}
	return r
}
```

确保了值均为基础数据类型后,程序就可以正常运行了。

## 类型推导

程序员写代码,一定程度的偷懒是必然的。

在一定的场景下,可以通过类型推导来避免明确地写出一些或所有的类型参数,编译器会进行自动识别。

建议复杂函数和参数能明确是最好的,否则读代码的同学会比较麻烦,可读性和可维护性的保证也是工作中重要的一点。

### 参数推导

函数例子。如下:

```go
func Map[F, T any](s []F, f func(F) T) []T { ... }
```

公共代码片段。如下:

```go
var s []int
f := func(i int) int64 { return int64(i) }
var r []int64
```
明确指定两个类型参数。如下:

```go
r = Map[int, int64](s, f)
```

只指定第一个类型参数,变量 f 被推断出来。如下:

```go
r = Map[int](s, f)
```

不指定任何类型参数,让两者都被推断出来。如下:

```go
r = Map(s, f)
```

### 约束推导

神奇的在于,类型推导不仅限与此,连约束都可以推导。

函数例子,如下:

```go
func Double[E constraints.Number](s []E) []E {
	r := make([]E, len(s))
	for i, v := range s {
		r[i] = v + v
	}
	return r
}
```

基于此的推导案例,如下:

```go
type MySlice []int

var V1 = Double(MySlice{1})
```

MySlice 是一个 int 的切片类型别名。变量 V1 的类型编译器推导后 []int 类型,并不是 MySlice。

原因在于编译器在比较两者的类型时,会将 MySlice 类型识别为 []int,也就是 int 类型。

要实现 “正确” 的推导,需要如下定义:

```go
type SC[E any] interface {
	[]E 
}

func DoubleDefined[S SC[E], E constraints.Number](s S) S {
	r := make(S, len(s))
	for i, v := range s {
		r[i] = v + v
	}
	return r
}
```

基于此的推导案例。如下:

```go
var V2 = DoubleDefined[MySlice, int](MySlice{1})
```

只要定义显式类型参数,就可以获得正确的类型,变量 V2 的类型会是 MySlice。

那如果不声明约束呢?如下:

```go
var V3 = DoubleDefined(MySlice{1})
```

编译器通过函数参数进行推导,也可以明确变量 V3 类型是 MySlice。

## 总结

今天我们在文章中给大家介绍了泛型的三个重要概念,分别是:
- 类型参数:泛型的抽象数据类型。
- 类型约束:确保调用方能够满足接受方的程序诉求。
- 类型推导:避免明确地写出一些或所有的类型参数。

在内容中也涉及到了联合元素、近似元素、函数约束、运算符约束等新概念。本质上都是基于三个大概念延伸出来的新解决方法,一环扣一环。

你学会 Go 泛型了吗,设计的如何,欢迎一起和煎鱼讨论:)

## 参考
- [Type Parameters Proposal](https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md)
- [Summary of Go Generics Discussions](https://docs.google.com/document/d/1vrAy9gMpMoS3uaVphB32uVXX4pi-HnNjkMEgyAHX4N4/)

================================================
FILE: content/posts/go/generics-proposal.md
================================================
---
title: "快报:正式提案将泛型特性加入 Go 语言"
date: 2021-01-13T21:11:44+08:00
toc: true
images:
tags: 
  - 泛型
---

经历九九八十一难,多年的不断探讨和 Go 语言爱好者们在社区中的强烈关注,且 Go 官方在 2020 年不断放出消息。

![image](https://image.eddycjy.com/1d0e5a264c65e37659f142bc2ee55805.jpg)

总算在 2021 年 1 月 12 日。官方正式提出将泛型特性加入 Go 语言的 proposal 了,且最新的草案设计已经更新。


基本语法如下:

```
func Print[T any](s []T) {
	// same as above
}
```

其大体的概述如下:

- 函数可以具有使用方括号的其他类型参数列表,但其他情况下看起来像普通的参数列表:`func F[T any](p T) { ... }`。
- 类型也可以具有类型参数列表:`type MySlice[T any] []T`。
- 每个类型参数都有一个类型约束,就像每个普通参数都有一个类型:`func F[T Constraint](p T) { ... }`。
- 类型约束是接口类型。
- 新的预声明名称 `any` 是允许任何类型的类型约束。
- 用作类型约束的接口类型可以具有预先声明的类型的列表。只有与那些类型之一匹配的类型参数才能满足约束条件。
- 泛型函数只能使用其类型约束所允许的操作。
- 使用泛型函数或类型需要传递类型实参。
- 在通常情况下,类型推断允许省略函数调用的类型参数。

根据官方博客的消息,如果该提案被正式接受。那么将会在 2021 年底之前完成一个基本可用的泛型特性使用,又或是会作为 Go1.18beta 的一部分。

这是 Go 泛型特性的又一步前进。若大家有兴趣进一步了解或想提出意见,可查看下述传送门:

- A Proposal for Adding Generics to Go:https://blog.golang.org/generics-proposal。
- proposal: spec: add generic programming using type parameters:https://github.com/golang/go/issues/43651。

今年年底或 Go1.18beta 到底能不能看到泛型的正式完整可用版本呢,值得期待。

================================================
FILE: content/posts/go/gin/2018-02-10-install.md
================================================
---

title:      "「连载一」Go 介绍与环境安装"
date:       2018-02-10 12:00:00
author:     "煎鱼"
toc: true
tags:
    - go
    - gin
---

## 本文目标

- 学会安装 Go。
- 知道什么是 Go。
- 知道什么是 Go modules。
- 了解 Go modules 的小历史。
- 学会简单的使用 Go modules。
- 了解 Gin,并简单跑起一个 Demo。

## 准备环节

### 安装 Go

#### Centos

首先,根据对应的操作系统选择安装包 [下载](https://studygolang.com/dl),在这里我使用的是 Centos 64 位系统,如下:

```sh
$ wget https://studygolang.com/dl/golang/go1.13.1.linux-amd64.tar.gz

$ tar -zxvf go1.13.1.linux-amd64.tar.gz

$ mv go/ /usr/local/
```

配置 /etc/profile

```sh
vi /etc/profile
```

添加环境变量 GOROOT 和将 GOBIN 添加到 PATH 中

```sh
export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin
```

配置完毕后,执行命令令其生效

```sh
source /etc/profile
```

在控制台输入`go version`,若输出版本号则**安装成功**,如下:

```
$ go version
go version go1.13.1 linux/amd64
```

#### MacOS

在 MacOS 上安装 Go 最方便的办法就是使用 brew,安装如下:

```
$ brew install go
```

升级命令如下:

```
$ brew upgrade go
```

注:升级命令你不需要执行,但我想未来你有一天会用到的。

同样在控制台输入`go version`,若输出版本号则**安装成功**。

### 了解 Go

#### 是什么

> Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.

上述为官方说明,如果简单来讲,大致为如下几点:

- Go 是编程语言。
- 谷歌爸爸撑腰。
- 语言级高并发。
- 上手快,入门简单。
- 简洁,很有特色。
- 国内使用人群逐年增多。

#### 谁在用

![image](https://image.eddycjy.com/6d278b22a4c0bf29c6b89ece99cd6c88.jpg)

#### 有什么

那么大家会有些疑问,纠结 `Go` 本身有什么东西,我们刚刚设置的环境变量又有什么用呢,甚至作为一名老粉,你会纠结 GOPATH 去哪里了,我们一起接着往下看。

##### 目录结构

首先,我们在解压的时候会得到一个名为 `go` 的文件夹,其中包括了所有 `Go` 语言相关的一些文件,如下:

```
$ tree -L 1 go
go
├── api
├── bin
├── doc
├── lib
├── misc
├── pkg
├── src
├── test
└── ...
```

在这之中包含了很多文件夹和文件,我们来简单说明其中主要文件夹的作用:

- api:用于存放依照 `Go` 版本顺序的 API 增量列表文件。这里所说的 API 包含公开的变量、常量、函数等。这些 API 增量列表文件用于 `Go` 语言 API 检查
- bin:用于存放主要的标准命令文件(可执行文件),包含`go`、`godoc`、`gofmt`
- blog:用于存放官方博客中的所有文章
- doc:用于存放标准库的 HTML 格式的程序文档。我们可以通过`godoc`命令启动一个 Web 程序展示这些文档
- lib:用于存放一些特殊的库文件
- misc:用于存放一些辅助类的说明和工具
- pkg:用于存放安装`Go`标准库后的所有归档文件(以`.a`结尾的文件)。注意,你会发现其中有名称为`linux_amd64`的文件夹,我们称为平台相关目录。这类文件夹的名称由对应的操作系统和计算架构的名称组合而成。通过`go install`命令,`Go`程序会被编译成平台相关的归档文件存放到其中
- src:用于存放 `Go`自身、`Go` 标准工具以及标准库的所有源码文件
- test:存放用来测试和验证`Go`本身的所有相关文件

##### 环境变量

你可能会疑惑刚刚设置的环境变量是什么,如下:

- GOROOT:`Go`的根目录。
- PATH 下增加 `$GOROOT/bin`:`Go`的 `bin`下会存放可执行文件,我们把他加入 `$PATH` 后,未来拉下来并编译后的二进制文件就可以直接在命令行使用。

那在什么东西都不下载的情况下,`$GOBIN` 下面有什么呢,如下:

```
bin/ $ls
go  gofmt
```

- go:`Go` 二进制本身。
- gofmt:代码格式化工具。

因此我们刚刚把 `$GOBIN` 加入到 `$PATH` 后,你执行 `go version` 命令后就可以查看到对应的输出结果。

注:MacOS 用 brew 安装的话就不需要。

#### 放在哪

你现在知道 Go 是什么了,也知道 Go 的源码摆在哪了,你肯定会想,那我应用代码放哪呢,答案是在 **Go1.11+ 和开启 Go Modules 的情况下摆哪都行**。

### 了解 Go Modules

#### 了解历史

在过去,Go 的依赖包管理在工具上混乱且不统一,有 dep,有 glide,有 govendor...甚至还有因为外网的问题,频频导致拉不下来包,很多人苦不堪言,盼着官方给出一个大一统做出表率。

而在 Go modules 正式出来之前还有一个叫 dep 的项目,我们在上面有提到,它是 Go 的一个官方实验性项目,目的也是为了解决 Go 在依赖管理方面的问题,当时社区里面几乎所有的人都认为 dep 肯定就是未来 Go 官方的依赖管理解决方案了。

但是万万没想到,半路杀出个程咬金,Russ Cox 义无反顾地推出了 Go modules,这瞬间导致一石激起千层浪,让社区炸了锅。大家一致认为 Go team 实在是太霸道、太独裁了,连个招呼都不打一声。我记得当时有很多人在网上跟 Russ Cox 口水战,各种依赖管理解决方案的专家都冒出来发表意见,讨论范围甚至一度超出了 Go 语言的圈子触及到了其他语言的领域。

当然,最后,推成功了,Go modules 已经进入官方工具链中,与 Go 深深结合,以前常说的 GOPATH 终将会失去它原有的作用,而且它还提供了 GOPROXY 间接解决了国内访问外网的问题。

#### 了解 Russ Cox

在上文中提到的 Russ Cox 是谁呢,他是 Go 这个项目目前代码提交量最多的人,甚至是第二名的两倍还要多(从 2019 年 09 月 30 日前来看)。

Russ Cox 还是 Go 现在的掌舵人(大家应该知道之前 Go 的掌舵人是 Rob Pike,但是听说由于他本人不喜欢特朗普执政所以离开了美国,然后他岁数也挺大的了,所以也正在逐渐交权,不过现在还是在参与 Go 的发展)。

Russ Cox 的个人能力相当强,看问题的角度也很独特,这也就是为什么他刚一提出 Go modules 的概念就能引起那么大范围的响应。虽然是被强推的,但事实也证明当下的 Go modules 表现得确实很优秀,所以这表明一定程度上的 “独裁” 还是可以接受的,至少可以保证一个项目能更加专一地朝着一个方向发展。

#### 初始化行为

在前面我们已经了解到 Go 依赖包管理的历史情况,接下来我们将正式的进入使用,首先你需要有一个你喜欢的目录,例如:`$ mkdir ~/go-application && cd ~/go-application`,然后执行如下命令:

```
$ mkdir go-gin-example && cd go-gin-example

$ go env -w GO111MODULE=on

$ go env -w GOPROXY=https://goproxy.cn,direct

$ go mod init github.com/EDDYCJY/go-gin-example
go: creating new go.mod: module github.com/EDDYCJY/go-gin-example

$ ls
go.mod
```

- `mkdir xxx && cd xxx`:创建并切换到项目目录里去。
- `go env -w GO111MODULE=on`:打开 Go modules 开关(目前在 Go1.13 中默认值为 `auto`)。
- `go env -w GOPROXY=...`:设置 GOPROXY 代理,这里主要涉及到两个值,第一个是 `https://goproxy.cn`,它是由七牛云背书的一个强大稳定的 Go 模块代理,可以有效地解决你的外网问题;第二个是 `direct`,它是一个特殊的 fallback 选项,它的作用是用于指示 Go 在拉取模块时遇到错误会回源到模块版本的源地址去抓取(比如 GitHub 等)。
- `go mod init [MODULE_PATH]`:初始化 Go modules,它将会生成 go.mod 文件,需要注意的是 `MODULE_PATH` 填写的是模块引入路径,你可以根据自己的情况修改路径。

在执行了上述步骤后,初始化工作已完成,我们打开 `go.mod` 文件看看,如下:

```
module github.com/EDDYCJY/go-gin-example

go 1.13
```

默认的 `go.mod` 文件里主要是两块内容,一个是当前的模块路径和预期的 Go 语言版本。

#### 基础使用

- 用 `go get` 拉取新的依赖
  - 拉取最新的版本(优先择取 tag):`go get golang.org/x/text@latest`
  - 拉取 `master` 分支的最新 commit:`go get golang.org/x/text@master`
  - 拉取 tag 为 v0.3.2 的 commit:`go get golang.org/x/text@v0.3.2`
  - 拉取 hash 为 342b231 的 commit,最终会被转换为 v0.3.2:`go get golang.org/x/text@342b2e`
  - 用 `go get -u` 更新现有的依赖
  - 用 `go mod download` 下载 go.mod 文件中指明的所有依赖
  - 用 `go mod tidy` 整理现有的依赖
  - 用 `go mod graph` 查看现有的依赖结构
  - 用 `go mod init` 生成 go.mod 文件 (Go 1.13 中唯一一个可以生成 go.mod 文件的子命令)
- 用 `go mod edit` 编辑 go.mod 文件
- 用 `go mod vendor` 导出现有的所有依赖 (事实上 Go modules 正在淡化 Vendor 的概念)
- 用 `go mod verify` 校验一个模块是否被篡改过

这一小节主要是针对 Go modules 的基础使用讲解,还没具体的使用,是希望你能够留个印象,因为在后面章节会不断夹杂 Go modules 的知识点。

注:建议阅读官方文档 [wiki/Modules](https://github.com/golang/go/wiki/Modules)。

## 开始 Gin 之旅

### 是什么

> 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.

Gin 是用 Go 开发的一个微框架,类似 Martinier 的 API,重点是小巧、易用、性能好很多,也因为 [httprouter](https://github.com/julienschmidt/httprouter) 的性能提高了 40 倍。

### 安装

我们回到刚刚创建的 `go-gin-example` 目录下,在命令行下执行如下命令:

```sh
$ go get -u github.com/gin-gonic/gin
go: downloading golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223
go: extracting golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223
go: finding github.com/gin-contrib/sse v0.1.0
go: finding github.com/ugorji/go v1.1.7
go: finding gopkg.in/yaml.v2 v2.2.3
go: finding golang.org/x/sys latest
go: finding github.com/mattn/go-isatty v0.0.9
go: finding github.com/modern-go/concurrent latest
...
```

#### go.sum

这时候你再检查一下该目录下,会发现多个了个 `go.sum` 文件,如下:

```
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW...
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW...
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2...
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO...
...
```

`go.sum` 文件详细罗列了当前项目直接或间接依赖的所有模块版本,并写明了那些模块版本的 SHA-256 哈希值以备 Go 在今后的操作中保证项目所依赖的那些模块版本不会被篡改。

#### go.mod

既然我们下载了依赖包,`go.mod` 文件会不会有所改变呢,我们再去看看,如下:

```
module github.com/EDDYCJY/go-gin-example

go 1.13

require (
        github.com/gin-contrib/sse v0.1.0 // indirect
        github.com/gin-gonic/gin v1.4.0 // indirect
        github.com/golang/protobuf v1.3.2 // indirect
        github.com/json-iterator/go v1.1.7 // indirect
        github.com/mattn/go-isatty v0.0.9 // indirect
        github.com/ugorji/go v1.1.7 // indirect
        golang.org/x/sys v0.0.0-20190927073244-c990c680b611 // indirect
        gopkg.in/yaml.v2 v2.2.3 // indirect
)
```

确确实实发生了改变,那多出来的东西又是什么呢,`go.mod` 文件又保存了什么信息呢,实际上 `go.mod` 文件是启用了 Go modules 的项目所必须的最重要的文件,因为它描述了当前项目(也就是当前模块)的元信息,每一行都以一个动词开头,目前有以下 5 个动词:

- module:用于定义当前项目的模块路径。
- go:用于设置预期的 Go 版本。
- require:用于设置一个特定的模块版本。
- exclude:用于从使用中排除一个特定的模块版本。
- replace:用于将一个模块版本替换为另外一个模块版本。

你可能还会疑惑 `indirect` 是什么东西,`indirect` 的意思是传递依赖,也就是非直接依赖。

### 测试

编写一个`test.go`文件

```go
package main

import "github.com/gin-gonic/gin"

func main() {
  r := gin.Default()
  r.GET("/ping", func(c *gin.Context) {
    c.JSON(200, gin.H{
      "message": "pong",
    })
  })
  r.Run() // listen and serve on 0.0.0.0:8080
}
```

执行`test.go`

```sh
$ go run test.go
...
[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
```

访问 `$HOST:8080/ping`,若返回`{"message":"pong"}`则正确

```sh
curl 127.0.0.1:8080/ping
```

至此,我们的环境安装和初步运行都基本完成了。

## 再想一想

刚刚在执行了命令 `$ go get -u github.com/gin-gonic/gin` 后,我们查看了 `go.mod` 文件,如下:

```
...
require (
        github.com/gin-contrib/sse v0.1.0 // indirect
        github.com/gin-gonic/gin v1.4.0 // indirect
        ...
)
```

你会发现 `go.mod` 里的 `github.com/gin-gonic/gin` 是 `indirect` 模式,这显然不对啊,因为我们的应用程序已经实际的编写了 gin server 代码了,我就想把它调对,怎么办呢,在应用根目录下执行如下命令:

```
$ go mod tidy
```

该命令主要的作用是整理现有的依赖,非常的常用,执行后 `go.mod` 文件内容为:

```
...
require (
        github.com/gin-contrib/sse v0.1.0 // indirect
        github.com/gin-gonic/gin v1.4.0
        ...
)
```

可以看到 `github.com/gin-gonic/gin` 已经变成了直接依赖,调整完毕。

## 参考

### 本系列示例代码

- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)

### 相关文档

- [Gin](https://github.com/gin-gonic/gin)
- [Gin Web Framework](https://gin-gonic.github.io/gin/)
- [干货满满的 Go Modules 和 goproxy.cn](https://book.eddycjy.com/golang/talk/goproxy-cn.html)

## 关于

### 修改记录

- 第一版:2018 年 02 月 16 日发布文章
- 第二版:2019 年 10 月 01 日修改文章

## ?

如果有任何疑问或错误,欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见,如果喜欢或对你有所帮助,欢迎 Star,对作者是一种鼓励和推进。

### 我的公众号

![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)

================================================
FILE: content/posts/go/gin/2018-02-11-api-01.md
================================================
---

title:      "「连载二」Gin搭建Blog API's (一)"
date:       2018-02-11 12:00:00
author:     "煎鱼"
toc: true
tags:
    - go
    - gin
---

## 思考

首先,在一个初始项目开始前,大家都要思考一下

- 程序的文本配置写在代码中,好吗?

- API 的错误码硬编码在程序中,合适吗?

- db 句柄谁都去`Open`,没有统一管理,好吗?

- 获取分页等公共参数,谁都自己写一套逻辑,好吗?

显然在较正规的项目中,这些问题的答案都是**不可以**,为了解决这些问题,我们挑选一款读写配置文件的库,目前比较火的有 [viper](https://github.com/spf13/viper),有兴趣你未来可以简单了解一下,没兴趣的话等以后接触到再说。

但是本系列选用 [go-ini/ini](https://github.com/go-ini/ini) ,它的 [中文文档](https://ini.unknwon.io/)。大家是必须需要要简单阅读它的文档,再接着完成后面的内容。

## 本文目标

- 编写一个简单的 API 错误码包。
- 完成一个 Demo 示例。
- 讲解 Demo 所涉及的知识点。

## 介绍和初始化项目

### 初始化项目目录

在前一章节中,我们初始化了一个 `go-gin-example` 项目,接下来我们需要继续新增如下目录结构:

```
go-gin-example/
├── conf
├── middleware
├── models
├── pkg
├── routers
└── runtime
```

- conf:用于存储配置文件
- middleware:应用中间件
- models:应用数据库模型
- pkg:第三方包
- routers 路由逻辑处理
- runtime:应用运行时数据

### 添加 Go Modules Replace

打开 `go.mod` 文件,新增 `replace` 配置项,如下:

```
module github.com/EDDYCJY/go-gin-example

go 1.13

require (...)

replace (
		github.com/EDDYCJY/go-gin-example/pkg/setting => ~/go-application/go-gin-example/pkg/setting
		github.com/EDDYCJY/go-gin-example/conf    	  => ~/go-application/go-gin-example/pkg/conf
		github.com/EDDYCJY/go-gin-example/middleware  => ~/go-application/go-gin-example/middleware
		github.com/EDDYCJY/go-gin-example/models 	  => ~/go-application/go-gin-example/models
		github.com/EDDYCJY/go-gin-example/routers 	  => ~/go-application/go-gin-example/routers
)
```

可能你会不理解为什么要特意跑来加 `replace` 配置项,首先你要看到我们使用的是完整的外部模块引用路径(`github.com/EDDYCJY/go-gin-example/xxx`),而这个模块还没推送到远程,是没有办法下载下来的,因此需要用 `replace` 将其指定读取本地的模块路径,这样子就可以解决本地模块读取的问题。

**注:后续每新增一个本地应用目录,你都需要主动去 go.mod 文件里新增一条 replace(我不会提醒你),如果你漏了,那么编译时会出现报错,找不到那个模块。**

### 初始项目数据库

新建 `blog` 数据库,编码为`utf8_general_ci`,在 `blog` 数据库下,新建以下表

**1、 标签表**

```sql
CREATE TABLE `blog_tag` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(100) DEFAULT '' COMMENT '标签名称',
  `created_on` int(10) unsigned DEFAULT '0' COMMENT '创建时间',
  `created_by` varchar(100) DEFAULT '' COMMENT '创建人',
  `modified_on` int(10) unsigned DEFAULT '0' COMMENT '修改时间',
  `modified_by` varchar(100) DEFAULT '' COMMENT '修改人',
  `deleted_on` int(10) unsigned DEFAULT '0',
  `state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0为禁用、1为启用',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文章标签管理';
```

**2、 文章表**

```sql
CREATE TABLE `blog_article` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `tag_id` int(10) unsigned DEFAULT '0' COMMENT '标签ID',
  `title` varchar(100) DEFAULT '' COMMENT '文章标题',
  `desc` varchar(255) DEFAULT '' COMMENT '简述',
  `content` text,
  `created_on` int(11) DEFAULT NULL,
  `created_by` varchar(100) DEFAULT '' COMMENT '创建人',
  `modified_on` int(10) unsigned DEFAULT '0' COMMENT '修改时间',
  `modified_by` varchar(255) DEFAULT '' COMMENT '修改人',
  `deleted_on` int(10) unsigned DEFAULT '0',
  `state` tinyint(3) unsigned DEFAULT '1' COMMENT '状态 0为禁用1为启用',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='文章管理';
```

**3、 认证表**

```sql
CREATE TABLE `blog_auth` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(50) DEFAULT '' COMMENT '账号',
  `password` varchar(50) DEFAULT '' COMMENT '密码',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `blog`.`blog_auth` (`id`, `username`, `password`) VALUES (null, 'test', 'test123456');

```

## 编写项目配置包

在 `go-gin-example` 应用目录下,拉取 `go-ini/ini` 的依赖包,如下:

```
$ go get -u github.com/go-ini/ini
go: finding github.com/go-ini/ini v1.48.0
go: downloading github.com/go-ini/ini v1.48.0
go: extracting github.com/go-ini/ini v1.48.0
```

接下来我们需要编写基础的应用配置文件,在 `go-gin-example` 的`conf`目录下新建`app.ini`文件,写入内容:

```ini
#debug or release
RUN_MODE = debug

[app]
PAGE_SIZE = 10
JWT_SECRET = 23347$040412

[server]
HTTP_PORT = 8000
READ_TIMEOUT = 60
WRITE_TIMEOUT = 60

[database]
TYPE = mysql
USER = 数据库账号
PASSWORD = 数据库密码
#127.0.0.1:3306
HOST = 数据库IP:数据库端口号
NAME = blog
TABLE_PREFIX = blog_
```

建立调用配置的`setting`模块,在`go-gin-example`的`pkg`目录下新建`setting`目录(注意新增 replace 配置),新建 `setting.go` 文件,写入内容:

```go
package setting

import (
	"log"
	"time"

	"github.com/go-ini/ini"
)

var (
	Cfg *ini.File

	RunMode string

	HTTPPort int
	ReadTimeout time.Duration
	WriteTimeout time.Duration

	PageSize int
	JwtSecret string
)

func init() {
	var err error
	Cfg, err = ini.Load("conf/app.ini")
	if err != nil {
		log.Fatalf("Fail to parse 'conf/app.ini': %v", err)
	}

	LoadBase()
	LoadServer()
	LoadApp()
}

func LoadBase() {
	RunMode = Cfg.Section("").Key("RUN_MODE").MustString("debug")
}

func LoadServer() {
	sec, err := Cfg.GetSection("server")
	if err != nil {
		log.Fatalf("Fail to get section 'server': %v", err)
	}

	HTTPPort = sec.Key("HTTP_PORT").MustInt(8000)
	ReadTimeout = time.Duration(sec.Key("READ_TIMEOUT").MustInt(60)) * time.Second
	WriteTimeout =  time.Duration(sec.Key("WRITE_TIMEOUT").MustInt(60)) * time.Second
}

func LoadApp() {
	sec, err := Cfg.GetSection("app")
	if err != nil {
		log.Fatalf("Fail to get section 'app': %v", err)
	}

	JwtSecret = sec.Key("JWT_SECRET").MustString("!@)*#)!@U#@*!@!)")
	PageSize = sec.Key("PAGE_SIZE").MustInt(10)
}
```

当前的目录结构:

```
go-gin-example
├── conf
│   └── app.ini
├── go.mod
├── go.sum
├── middleware
├── models
├── pkg
│   └── setting.go
├── routers
└── runtime
```

## 编写 API 错误码包

建立错误码的`e`模块,在`go-gin-example`的`pkg`目录下新建`e`目录(注意新增 replace 配置),新建`code.go`和`msg.go`文件,写入内容:

**1、 code.go:**

```go
package e

const (
	SUCCESS = 200
	ERROR = 500
	INVALID_PARAMS = 400

	ERROR_EXIST_TAG = 10001
	ERROR_NOT_EXIST_TAG = 10002
	ERROR_NOT_EXIST_ARTICLE = 10003

	ERROR_AUTH_CHECK_TOKEN_FAIL = 20001
	ERROR_AUTH_CHECK_TOKEN_TIMEOUT = 20002
	ERROR_AUTH_TOKEN = 20003
	ERROR_AUTH = 20004
)
```

**2、 msg.go:**

```go
package e

var MsgFlags = map[int]string {
	SUCCESS : "ok",
	ERROR : "fail",
	INVALID_PARAMS : "请求参数错误",
	ERROR_EXIST_TAG : "已存在该标签名称",
	ERROR_NOT_EXIST_TAG : "该标签不存在",
	ERROR_NOT_EXIST_ARTICLE : "该文章不存在",
	ERROR_AUTH_CHECK_TOKEN_FAIL : "Token鉴权失败",
	ERROR_AUTH_CHECK_TOKEN_TIMEOUT : "Token已超时",
	ERROR_AUTH_TOKEN : "Token生成失败",
	ERROR_AUTH : "Token错误",
}

func GetMsg(code int) string {
	msg, ok := MsgFlags[code]
	if ok {
		return msg
	}

	return MsgFlags[ERROR]
}
```

## 编写工具包

在`go-gin-example`的`pkg`目录下新建`util`目录(注意新增 replace 配置),并拉取`com`的依赖包,如下:

```
$ go get -u github.com/unknwon/com
```

### 编写分页页码的获取方法

在`util`目录下新建`pagination.go`,写入内容:

```go
package util

import (
	"github.com/gin-gonic/gin"
	"github.com/unknwon/com"

	"github.com/EDDYCJY/go-gin-example/pkg/setting"
)

func GetPage(c *gin.Context) int {
	result := 0
	page, _ := com.StrTo(c.Query("page")).Int()
    if page > 0 {
        result = (page - 1) * setting.PageSize
    }

    return result
}
```

## 编写 models init

拉取`gorm`的依赖包,如下:

```
$ go get -u github.com/jinzhu/gorm
```

拉取`mysql`驱动的依赖包,如下:

```
$ go get -u github.com/go-sql-driver/mysql
```

完成后,在`go-gin-example`的`models`目录下新建`models.go`,用于`models`的初始化使用

```go
package models

import (
	"log"
	"fmt"

	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"

	"github.com/EDDYCJY/go-gin-example/pkg/setting"
)

var db *gorm.DB

type Model struct {
	ID int `gorm:"primary_key" json:"id"`
	CreatedOn int `json:"created_on"`
	ModifiedOn int `json:"modified_on"`
}

func init() {
	var (
		err error
		dbType, dbName, user, password, host, tablePrefix string
	)

	sec, err := setting.Cfg.GetSection("database")
	if err != nil {
		log.Fatal(2, "Fail to get section 'database': %v", err)
	}

	dbType = sec.Key("TYPE").String()
	dbName = sec.Key("NAME").String()
	user = sec.Key("USER").String()
	password = sec.Key("PASSWORD").String()
	host = sec.Key("HOST").String()
	tablePrefix = sec.Key("TABLE_PREFIX").String()

	db, err = gorm.Open(dbType, fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local",
		user,
		password,
		host,
		dbName))

	if err != nil {
		log.Println(err)
	}

	gorm.DefaultTableNameHandler = func (db *gorm.DB, defaultTableName string) string  {
	    return tablePrefix + defaultTableName;
	}

	db.SingularTable(true)
	db.LogMode(true)
	db.DB().SetMaxIdleConns(10)
	db.DB().SetMaxOpenConns(100)
}

func CloseDB() {
	defer db.Close()
}
```

## 编写项目启动、路由文件

最基础的准备工作完成啦,让我们开始编写 Demo 吧!

### 编写 Demo

在`go-gin-example`下建立`main.go`作为启动文件(也就是`main`包),我们先写个**Demo**,帮助大家理解,写入文件内容:

```go
package main

import (
    "fmt"
    "net/http"

    "github.com/gin-gonic/gin"

    "github.com/EDDYCJY/go-gin-example/pkg/setting"
)

func main() {
    router := gin.Default()
    router.GET("/test", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "test",
		})
	})

	s := &http.Server{
		Addr:           fmt.Sprintf(":%d", setting.HTTPPort),
		Handler:        router,
		ReadTimeout:    setting.ReadTimeout,
		WriteTimeout:   setting.WriteTimeout,
		MaxHeaderBytes: 1 << 20,
	}

	s.ListenAndServe()
}
```

执行`go run main.go`,查看命令行是否显示

```
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /test                     --> main.main.func1 (3 handlers)
```

在本机执行`curl 127.0.0.1:8000/test`,检查是否返回`{"message":"test"}`。

### 知识点

**那么,我们来延伸一下 Demo 所涉及的知识点!**

##### 标准库

- [fmt](https://golang.org/pkg/fmt/):实现了类似 C 语言 printf 和 scanf 的格式化 I/O。格式化动作('verb')源自 C 语言但更简单
- [net/http](https://golang.org/pkg/net/http/):提供了 HTTP 客户端和服务端的实现

##### **Gin**

- [gin.Default()](https://gowalker.org/github.com/gin-gonic/gin#Default):返回 Gin 的`type Engine struct{...}`,里面包含`RouterGroup`,相当于创建一个路由`Handlers`,可以后期绑定各类的路由规则和函数、中间件等
- [router.GET(...){...}](https://gowalker.org/github.com/gin-gonic/gin#IRoutes):创建不同的 HTTP 方法绑定到`Handlers`中,也支持 POST、PUT、DELETE、PATCH、OPTIONS、HEAD 等常用的 Restful 方法
- [gin.H{...}](https://gowalker.org/github.com/gin-gonic/gin#H):就是一个`map[string]interface{}`
- [gin.Context](https://gowalker.org/github.com/gin-gonic/gin#Context):`Context`是`gin`中的上下文,它允许我们在中间件之间传递变量、管理流、验证 JSON 请求、响应 JSON 请求等,在`gin`中包含大量`Context`的方法,例如我们常用的`DefaultQuery`、`Query`、`DefaultPostForm`、`PostForm`等等

##### &http.Server 和 ListenAndServe?

1、http.Server:

```go
type Server struct {
    Addr    string
    Handler Handler
    TLSConfig *tls.Config
    ReadTimeout time.Duration
    ReadHeaderTimeout time.Duration
    WriteTimeout time.Duration
    IdleTimeout time.Duration
    MaxHeaderBytes int
    ConnState func(net.Conn, ConnState)
    ErrorLog *log.Logger
}
```

- Addr:监听的 TCP 地址,格式为`:8000`
- Handler:http 句柄,实质为`ServeHTTP`,用于处理程序响应 HTTP 请求
- TLSConfig:安全传输层协议(TLS)的配置
- ReadTimeout:允许读取的最大时间
- ReadHeaderTimeout:允许读取请求头的最大时间
- WriteTimeout:允许写入的最大时间
- IdleTimeout:等待的最大时间
- MaxHeaderBytes:请求头的最大字节数
- ConnState:指定一个可选的回调函数,当客户端连接发生变化时调用
- ErrorLog:指定一个可选的日志记录器,用于接收程序的意外行为和底层系统错误;如果未设置或为`nil`则默认以日志包的标准日志记录器完成(也就是在控制台输出)

2、 ListenAndServe:

```go
func (srv *Server) ListenAndServe() error {
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    }
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    }
    return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
```

开始监听服务,监听 TCP 网络地址,Addr 和调用应用程序处理连接上的请求。

我们在源码中看到`Addr`是调用我们在`&http.Server`中设置的参数,因此我们在设置时要用`&`,我们要改变参数的值,因为我们`ListenAndServe`和其他一些方法需要用到`&http.Server`中的参数,他们是相互影响的。

3、 `http.ListenAndServe`和 [连载一](https://segmentfault.com/a/1190000013297625#articleHeader5) 的`r.Run()`有区别吗?

我们看看`r.Run`的实现:

```go
func (engine *Engine) Run(addr ...string) (err error) {
    defer func() { debugPrintError(err) }()

    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    err = http.ListenAndServe(address, engine)
    return
}
```

通过分析源码,得知**本质上没有区别**,同时也得知了启动`gin`时的监听 debug 信息在这里输出。

4、 为什么 Demo 里会有`WARNING`?

首先我们可以看下`Default()`的实现

```go
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}
```

大家可以看到默认情况下,已经附加了日志、恢复中间件的引擎实例。并且在开头调用了`debugPrintWARNINGDefault()`,而它的实现就是输出该行日志

```go
func debugPrintWARNINGDefault() {
	debugPrint(`[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
`)
}
```

而另外一个`Running in "debug" mode. Switch to "release" mode in production.`,是运行模式原因,并不难理解,已在配置文件的管控下 :-),运维人员随时就可以修改它的配置。

5、 Demo 的`router.GET`等路由规则可以不写在`main`包中吗?

我们发现`router.GET`等路由规则,在 Demo 中被编写在了`main`包中,感觉很奇怪,我们去抽离这部分逻辑!

在`go-gin-example`下`routers`目录新建`router.go`文件,写入内容:

```go
package routers

import (
    "github.com/gin-gonic/gin"

    "github.com/EDDYCJY/go-gin-example/pkg/setting"
)

func InitRouter() *gin.Engine {
    r := gin.New()

    r.Use(gin.Logger())

    r.Use(gin.Recovery())

    gin.SetMode(setting.RunMode)

    r.GET("/test", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "test",
        })
    })

    return r
}
```

修改`main.go`的文件内容:

```go
package main

import (
	"fmt"
	"net/http"

	"github.com/EDDYCJY/go-gin-example/routers"
	"github.com/EDDYCJY/go-gin-example/pkg/setting"
)

func main() {
	router := routers.InitRouter()

	s := &http.Server{
		Addr:           fmt.Sprintf(":%d", setting.HTTPPort),
		Handler:        router,
		ReadTimeout:    setting.ReadTimeout,
		WriteTimeout:   setting.WriteTimeout,
		MaxHeaderBytes: 1 << 20,
	}

	s.ListenAndServe()
}
```

当前目录结构:

```
go-gin-example/
├── conf
│   └── app.ini
├── main.go
├── middleware
├── models
│   └── models.go
├── pkg
│   ├── e
│   │   ├── code.go
│   │   └── msg.go
│   ├── setting
│   │   └── setting.go
│   └── util
│       └── pagination.go
├── routers
│   └── router.go
├── runtime
```

重启服务,执行 `curl 127.0.0.1:8000/test`查看是否正确返回。

下一节,我们将以我们的 Demo 为起点进行修改,开始编码!

## 参考

### 本系列示例代码

- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)

## 关于

### 修改记录

- 第一版:2018 年 02 月 16 日发布文章
- 第二版:2019 年 10 月 01 日修改文章

## ?

如果有任何疑问或错误,欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见,如果喜欢或对你有所帮助,欢迎 Star,对作者是一种鼓励和推进。

### 我的公众号

![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)

================================================
FILE: content/posts/go/gin/2018-02-12-api-02.md
================================================
---

title:      "「连载三」Gin搭建Blog API's (二)"
date:       2018-02-12 12:00:00
author:     "煎鱼"
toc: true
tags:
    - go
    - gin
---

## 涉及知识点

- [Gin](https://github.com/gin-gonic/gin):Golang 的一个微框架,性能极佳。
- [beego-validation](https://github.com/astaxie/beego/tree/master/validation):本节采用的 beego 的表单验证库,[中文文档](https://beego.me/docs/mvc/controller/validation.md)。
- [gorm](https://github.com/jinzhu/gorm),对开发人员友好的 ORM 框架,[英文文档](http://gorm.io/docs/)
- [com](https://github.com/Unknwon/com),一个小而美的工具包。

## 本文目标

- 完成博客的标签类接口定义和编写

## 定义接口

本节正是编写标签的逻辑,我们想一想,一般接口为增删改查是基础的,那么我们定义一下接口吧!

- 获取标签列表:GET("/tags")
- 新建标签:POST("/tags")
- 更新指定标签:PUT("/tags/:id")
- 删除指定标签:DELETE("/tags/:id")

---

## 编写路由空壳

开始编写路由文件逻辑,在`routers`下新建`api`目录,我们当前是第一个 API 大版本,因此在`api`下新建`v1`目录,再新建`tag.go`文件,写入内容:

```go
package v1

import (
    "github.com/gin-gonic/gin"
)

//获取多个文章标签
func GetTags(c *gin.Context) {
}

//新增文章标签
func AddTag(c *gin.Context) {
}

//修改文章标签
func EditTag(c *gin.Context) {
}

//删除文章标签
func DeleteTag(c *gin.Context) {
}
```

## 注册路由

我们打开`routers`下的`router.go`文件,修改文件内容为:

```go
package routers

import (
    "github.com/gin-gonic/gin"

    "gin-blog/routers/api/v1"
    "gin-blog/pkg/setting"
)

func InitRouter() *gin.Engine {
    r := gin.New()

    r.Use(gin.Logger())

    r.Use(gin.Recovery())

    gin.SetMode(setting.RunMode)

    apiv1 := r.Group("/api/v1")
    {
        //获取标签列表
        apiv1.GET("/tags", v1.GetTags)
        //新建标签
        apiv1.POST("/tags", v1.AddTag)
        //更新指定标签
        apiv1.PUT("/tags/:id", v1.EditTag)
        //删除指定标签
        apiv1.DELETE("/tags/:id", v1.DeleteTag)
    }

    return r
}
```

当前目录结构:

```
gin-blog/
├── conf
│   └── app.ini
├── main.go
├── middleware
├── models
│   └── models.go
├── pkg
│   ├── e
│   │   ├── code.go
│   │   └── msg.go
│   ├── setting
│   │   └── setting.go
│   └── util
│       └── pagination.go
├── routers
│   ├── api
│   │   └── v1
│   │       └── tag.go
│   └── router.go
├── runtime
```

## 检验路由是否注册成功

回到命令行,执行`go run main.go`,检查路由规则是否注册成功。

```
$ go run main.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /api/v1/tags              --> gin-blog/routers/api/v1.GetTags (3 handlers)
[GIN-debug] POST   /api/v1/tags              --> gin-blog/routers/api/v1.AddTag (3 handlers)
[GIN-debug] PUT    /api/v1/tags/:id          --> gin-blog/routers/api/v1.EditTag (3 handlers)
[GIN-debug] DELETE /api/v1/tags/:id          --> gin-blog/routers/api/v1.DeleteTag (3 handlers)
```

运行成功,那么我们愉快的**开始编写我们的接口**吧!

## 下载依赖包

---

首先我们要拉取`validation`的依赖包,在后面的接口里会使用到表单验证

```
$ go get -u github.com/astaxie/beego/validation
```

## 编写标签列表的 models 逻辑

创建`models`目录下的`tag.go`,写入文件内容:

```go
package models

type Tag struct {
    Model

    Name string `json:"name"`
    CreatedBy string `json:"created_by"`
    ModifiedBy string `json:"modified_by"`
    State int `json:"state"`
}

func GetTags(pageNum int, pageSize int, maps interface {}) (tags []Tag) {
    db.Where(maps).Offset(pageNum).Limit(pageSize).Find(&tags)

    return
}

func GetTagTotal(maps interface {}) (count int){
    db.Model(&Tag{}).Where(maps).Count(&count)

    return
}
```

1. 我们创建了一个`Tag struct{}`,用于`Gorm`的使用。并给予了附属属性`json`,这样子在`c.JSON`的时候就会自动转换格式,非常的便利

2. 可能会有的初学者看到`return`,而后面没有跟着变量,会不理解;其实你可以看到在函数末端,我们已经显示声明了返回值,这个变量在函数体内也可以直接使用,因为他在一开始就被声明了

3. 有人会疑惑`db`是哪里来的;因为在同个`models`包下,因此`db *gorm.DB`是可以直接使用的

## 编写标签列表的路由逻辑

打开`routers`目录下 v1 版本的`tag.go`,第一我们先编写**获取标签列表的接口**

修改文件内容:

```go
package v1

import (
    "net/http"

    "github.com/gin-gonic/gin"
    //"github.com/astaxie/beego/validation"
    "github.com/Unknwon/com"

    "gin-blog/pkg/e"
    "gin-blog/models"
    "gin-blog/pkg/util"
    "gin-blog/pkg/setting"
)

//获取多个文章标签
func GetTags(c *gin.Context) {
    name := c.Query("name")

    maps := make(map[string]interface{})
    data := make(map[string]interface{})

    if name != "" {
        maps["name"] = name
    }

    var state int = -1
    if arg := c.Query("state"); arg != "" {
        state = com.StrTo(arg).MustInt()
        maps["state"] = state
    }

    code := e.SUCCESS

    data["lists"] = models.GetTags(util.GetPage(c), setting.PageSize, maps)
    data["total"] = models.GetTagTotal(maps)

    c.JSON(http.StatusOK, gin.H{
        "code" : code,
        "msg" : e.GetMsg(code),
        "data" : data,
    })
}

//新增文章标签
func AddTag(c *gin.Context) {
}

//修改文章标签
func EditTag(c *gin.Context) {
}

//删除文章标签
func DeleteTag(c *gin.Context) {
}
```

1. `c.Query`可用于获取`?name=test&state=1`这类 URL 参数,而`c.DefaultQuery`则支持设置一个默认值
2. `code`变量使用了`e`模块的错误编码,这正是先前规划好的错误码,方便排错和识别记录
3. `util.GetPage`保证了各接口的`page`处理是一致的
4. `c *gin.Context`是`Gin`很重要的组成部分,可以理解为上下文,它允许我们在中间件之间传递变量、管理流、验证请求的 JSON 和呈现 JSON 响应

在本机执行`curl 127.0.0.1:8000/api/v1/tags`,正确的返回值为`{"code":200,"data":{"lists":[],"total":0},"msg":"ok"}`,若存在问题请结合 gin 结果进行拍错。

在获取标签列表接口中,我们可以根据`name`、`state`、`page`来筛选查询条件,分页的步长可通过`app.ini`进行配置,以`lists`、`total`的组合返回达到分页效果。

## 编写新增标签的 models 逻辑

接下来我们编写**新增标签**的接口

打开`models`目录下的`tag.go`,修改文件(增加 2 个方法):

```go
...
func ExistTagByName(name string) bool {
    var tag Tag
    db.Select("id").Where("name = ?", name).First(&tag)
    if tag.ID > 0 {
        return true
    }

    return false
}

func AddTag(name string, state int, createdBy string) bool{
    db.Create(&Tag {
        Name : name,
        State : state,
        CreatedBy : createdBy,
    })

    return true
}
...
```

## 编写新增标签的路由逻辑

打开`routers`目录下的`tag.go`,修改文件(变动 AddTag 方法):

```go
package v1

import (
    "log"
    "net/http"

    "github.com/gin-gonic/gin"
    "github.com/astaxie/beego/validation"
    "github.com/Unknwon/com"

    "gin-blog/pkg/e"
    "gin-blog/models"
    "gin-blog/pkg/util"
    "gin-blog/pkg/setting"
)

...

//新增文章标签
func AddTag(c *gin.Context) {
    name := c.Query("name")
    state := com.StrTo(c.DefaultQuery("state", "0")).MustInt()
    createdBy := c.Query("created_by")

    valid := validation.Validation{}
    valid.Required(name, "name").Message("名称不能为空")
    valid.MaxSize(name, 100, "name").Message("名称最长为100字符")
    valid.Required(createdBy, "created_by").Message("创建人不能为空")
    valid.MaxSize(createdBy, 100, "created_by").Message("创建人最长为100字符")
    valid.Range(state, 0, 1, "state").Message("状态只允许0或1")

    code := e.INVALID_PARAMS
    if ! valid.HasErrors() {
        if ! models.ExistTagByName(name) {
            code = e.SUCCESS
            models.AddTag(name, state, createdBy)
        } else {
            code = e.ERROR_EXIST_TAG
        }
    }

    c.JSON(http.StatusOK, gin.H{
        "code" : code,
        "msg" : e.GetMsg(code),
        "data" : make(map[string]string),
    })
}
...
```

用`Postman`用 POST 访问`http://127.0.0.1:8000/api/v1/tags?name=1&state=1&created_by=test`,查看`code`是否返回`200`及`blog_tag`表中是否有值,有值则正确。

## 编写 models callbacks

但是这个时候大家会发现,我明明新增了标签,但`created_on`居然没有值,那做修改标签的时候`modified_on`会不会也存在这个问题?

为了解决这个问题,我们需要打开`models`目录下的`tag.go`文件,修改文件内容(修改包引用和增加 2 个方法):

```go
package models

import (
    "time"

    "github.com/jinzhu/gorm"
)

...

func (tag *Tag) BeforeCreate(scope *gorm.Scope) error {
    scope.SetColumn("CreatedOn", time.Now().Unix())

    return nil
}

func (tag *Tag) BeforeUpdate(scope *gorm.Scope) error {
    scope.SetColumn("ModifiedOn", time.Now().Unix())

    return nil
}
```

重启服务,再在用`Postman`用 POST 访问`http://127.0.0.1:8000/api/v1/tags?name=2&state=1&created_by=test`,发现`created_on`已经有值了!

**在这几段代码中,涉及到知识点:**

这属于`gorm`的`Callbacks`,可以将回调方法定义为模型结构的指针,在创建、更新、查询、删除时将被调用,如果任何回调返回错误,gorm 将停止未来操作并回滚所有更改。

`gorm`所支持的回调方法:

- 创建:BeforeSave、BeforeCreate、AfterCreate、AfterSave
- 更新:BeforeSave、BeforeUpdate、AfterUpdate、AfterSave
- 删除:BeforeDelete、AfterDelete
- 查询:AfterFind

---

## 编写其余接口的路由逻辑

接下来,我们一口气把剩余的两个接口(EditTag、DeleteTag)完成吧

打开`routers`目录下 v1 版本的`tag.go`文件,修改内容:

```go
...

//修改文章标签
func EditTag(c *gin.Context) {
    id := com.StrTo(c.Param("id")).MustInt()
    name := c.Query("name")
    modifiedBy := c.Query("modified_by")

    valid := validation.Validation{}

    var state int = -1
    if arg := c.Query("state"); arg != "" {
        state = com.StrTo(arg).MustInt()
        valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
    }

    valid.Required(id, "id").Message("ID不能为空")
    valid.Required(modifiedBy, "modified_by").Message("修改人不能为空")
    valid.MaxSize(modifiedBy, 100, "modified_by").Message("修改人最长为100字符")
    valid.MaxSize(name, 100, "name").Message("名称最长为100字符")

    code := e.INVALID_PARAMS
    if ! valid.HasErrors() {
        code = e.SUCCESS
        if models.ExistTagByID(id) {
            data := make(map[string]interface{})
            data["modified_by"] = modifiedBy
            if name != "" {
                data["name"] = name
            }
            if state != -1 {
                data["state"] = state
            }

            models.EditTag(id, data)
        } else {
            code = e.ERROR_NOT_EXIST_TAG
        }
    }

    c.JSON(http.StatusOK, gin.H{
        "code" : code,
        "msg" : e.GetMsg(code),
        "data" : make(map[string]string),
    })
}

//删除文章标签
func DeleteTag(c *gin.Context) {
    id := com.StrTo(c.Param("id")).MustInt()

    valid := validation.Validation{}
    valid.Min(id, 1, "id").Message("ID必须大于0")

    code := e.INVALID_PARAMS
    if ! valid.HasErrors() {
        code = e.SUCCESS
        if models.ExistTagByID(id) {
            models.DeleteTag(id)
        } else {
            code = e.ERROR_NOT_EXIST_TAG
        }
    }

    c.JSON(http.StatusOK, gin.H{
        "code" : code,
        "msg" : e.GetMsg(code),
        "data" : make(map[string]string),
    })
}
```

## 编写其余接口的 models 逻辑

打开`models`下的`tag.go`,修改文件内容:

```go
...

func ExistTagByID(id int) bool {
    var tag Tag
    db.Select("id").Where("id = ?", id).First(&tag)
    if tag.ID > 0 {
        return true
    }

    return false
}

func DeleteTag(id int) bool {
    db.Where("id = ?", id).Delete(&Tag{})

    return true
}

func EditTag(id int, data interface {}) bool {
    db.Model(&Tag{}).Where("id = ?", id).Updates(data)

    return true
}
...
```

## 验证功能

重启服务,用 Postman

- PUT 访问 http://127.0.0.1:8000/api/v1/tags/1?name=edit1&state=0&modified_by=edit1 ,查看 code 是否返回 200
- DELETE 访问 http://127.0.0.1:8000/api/v1/tags/1 ,查看 code 是否返回 200

至此,Tag 的 API's 完成,下一节我们将开始 Article 的 API's 编写!

## 参考

### 本系列示例代码

- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)

## 关于

### 修改记录

- 第一版:2018 年 02 月 16 日发布文章
- 第二版:2019 年 10 月 01 日修改文章

## ?

如果有任何疑问或错误,欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见,如果喜欢或对你有所帮助,欢迎 Star,对作者是一种鼓励和推进。

### 我的公众号

![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)

================================================
FILE: content/posts/go/gin/2018-02-13-api-03.md
================================================
---

title:      "「连载四」Gin搭建Blog API's (三)"
date:       2018-02-13 12:00:00
author:     "煎鱼"
toc: true
tags:
    - go
    - gin
---

## 涉及知识点

- [Gin](https://github.com/gin-gonic/gin):Golang 的一个微框架,性能极佳。
- [beego-validation](https://github.com/astaxie/beego/tree/master/validation):本节采用的 beego 的表单验证库,[中文文档](https://beego.me/docs/mvc/controller/validation.md)。
- [gorm](https://github.com/jinzhu/gorm),对开发人员友好的 ORM 框架,[英文文档](http://gorm.io/docs/)
- [com](https://github.com/Unknwon/com),一个小而美的工具包。

## 本文目标

- 完成博客的文章类接口定义和编写

## 定义接口

本节编写文章的逻辑,我们定义一下接口吧!

- 获取文章列表:GET("/articles")
- 获取指定文章:POST("/articles/:id")
- 新建文章:POST("/articles")
- 更新指定文章:PUT("/articles/:id")
- 删除指定文章:DELETE("/articles/:id")

## 编写路由逻辑

在`routers`的 v1 版本下,新建`article.go`文件,写入内容:

```go
package v1

import (
    "github.com/gin-gonic/gin"
)

//获取单个文章
func GetArticle(c *gin.Context) {
}

//获取多个文章
func GetArticles(c *gin.Context) {
}

//新增文章
func AddArticle(c *gin.Context) {
}

//修改文章
func EditArticle(c *gin.Context) {
}

//删除文章
func DeleteArticle(c *gin.Context) {
}
```

我们打开`routers`下的`router.go`文件,修改文件内容为:

```go
package routers

import (
    "github.com/gin-gonic/gin"

    "github.com/EDDYCJY/go-gin-example/routers/api/v1"
    "github.com/EDDYCJY/go-gin-example/pkg/setting"
)

func InitRouter() *gin.Engine {
    ...
    apiv1 := r.Group("/api/v1")
    {
        ...
        //获取文章列表
        apiv1.GET("/articles", v1.GetArticles)
        //获取指定文章
        apiv1.GET("/articles/:id", v1.GetArticle)
        //新建文章
        apiv1.POST("/articles", v1.AddArticle)
        //更新指定文章
        apiv1.PUT("/articles/:id", v1.EditArticle)
        //删除指定文章
        apiv1.DELETE("/articles/:id", v1.DeleteArticle)
    }

    return r
}
```

当前目录结构:

```
go-gin-example/
├── conf
│   └── app.ini
├── main.go
├── middleware
├── models
│   ├── models.go
│   └── tag.go
├── pkg
│   ├── e
│   │   ├── code.go
│   │   └── msg.go
│   ├── setting
│   │   └── setting.go
│   └── util
│       └── pagination.go
├── routers
│   ├── api
│   │   └── v1
│   │       ├── article.go
│   │       └── tag.go
│   └── router.go
├── runtime

```

在基础的路由规则配置结束后,我们**开始编写我们的接口**吧!

---

##编写 models 逻辑
创建`models`目录下的`article.go`,写入文件内容:

```go
package models

import (
    "github.com/jinzhu/gorm"

    "time"
)

type Article struct {
    Model

    TagID int `json:"tag_id" gorm:"index"`
    Tag   Tag `json:"tag"`

    Title string `json:"title"`
    Desc string `json:"desc"`
    Content string `json:"content"`
    CreatedBy string `json:"created_by"`
    ModifiedBy string `json:"modified_by"`
    State int `json:"state"`
}


func (article *Article) BeforeCreate(scope *gorm.Scope) error {
    scope.SetColumn("CreatedOn", time.Now().Unix())

    return nil
}

func (article *Article) BeforeUpdate(scope *gorm.Scope) error {
    scope.SetColumn("ModifiedOn", time.Now().Unix())

    return nil
}
```

我们创建了一个`Article struct {}`,与`Tag`不同的是,`Article`多了几项,如下:

1. `gorm:index`,用于声明这个字段为索引,如果你使用了自动迁移功能则会有所影响,在不使用则无影响
2. `Tag`字段,实际是一个嵌套的`struct`,它利用`TagID`与`Tag`模型相互关联,在执行查询的时候,能够达到`Article`、`Tag`关联查询的功能
3. `time.Now().Unix()` 返回当前的时间戳

接下来,请确保已对上一章节的内容通读且了解,由于逻辑偏差不会太远,我们本节直接编写这五个接口

---

打开`models`目录下的`article.go`,修改文件内容:

```go
package models

import (
    "time"

    "github.com/jinzhu/gorm"
)

type Article struct {
    Model

    TagID int `json:"tag_id" gorm:"index"`
    Tag   Tag `json:"tag"`

    Title string `json:"title"`
    Desc string `json:"desc"`
    Content string `json:"content"`
    CreatedBy string `json:"created_by"`
    ModifiedBy string `json:"modified_by"`
    State int `json:"state"`
}


func ExistArticleByID(id int) bool {
    var article Article
    db.Select("id").Where("id = ?", id).First(&article)

    if article.ID > 0 {
        return true
    }

    return false
}

func GetArticleTotal(maps interface {}) (count int){
    db.Model(&Article{}).Where(maps).Count(&count)

    return
}

func GetArticles(pageNum int, pageSize int, maps interface {}) (articles []Article) {
    db.Preload("Tag").Where(maps).Offset(pageNum).Limit(pageSize).Find(&articles)

    return
}

func GetArticle(id int) (article Article) {
    db.Where("id = ?", id).First(&article)
    db.Model(&article).Related(&article.Tag)

    return
}

func EditArticle(id int, data interface {}) bool {
    db.Model(&Article{}).Where("id = ?", id).Updates(data)

    return true
}

func AddArticle(data map[string]interface {}) bool {
    db.Create(&Article {
        TagID : data["tag_id"].(int),
        Title : data["title"].(string),
        Desc : data["desc"].(string),
        Content : data["content"].(string),
        CreatedBy : data["created_by"].(string),
        State : data["state"].(int),
    })

    return true
}

func DeleteArticle(id int) bool {
    db.Where("id = ?", id).Delete(Article{})

    return true
}

func (article *Article) BeforeCreate(scope *gorm.Scope) error {
    scope.SetColumn("CreatedOn", time.Now().Unix())

    return nil
}

func (article *Article) BeforeUpdate(scope *gorm.Scope) error {
    scope.SetColumn("ModifiedOn", time.Now().Unix())

    return nil
}
```

在这里,我们拿出三点不同来讲,如下:

**1、 我们的`Article`是如何关联到`Tag`?**

```go
func GetArticle(id int) (article Article) {
    db.Where("id = ?", id).First(&article)
    db.Model(&article).Related(&article.Tag)

    return
}
```

能够达到关联,首先是`gorm`本身做了大量的约定俗成

- `Article`有一个结构体成员是`TagID`,就是外键。`gorm`会通过类名+ID 的方式去找到这两个类之间的关联关系
- `Article`有一个结构体成员是`Tag`,就是我们嵌套在`Article`里的`Tag`结构体,我们可以通过`Related`进行关联查询

**2、 `Preload`是什么东西,为什么查询可以得出每一项的关联`Tag`?**

```go
func GetArticles(pageNum int, pageSize int, maps interface {}) (articles []Article) {
    db.Preload("Tag").Where(maps).Offset(pageNum).Limit(pageSize).Find(&articles)

    return
}
```

`Preload`就是一个预加载器,它会执行两条 SQL,分别是`SELECT * FROM blog_articles;`和`SELECT * FROM blog_tag WHERE id IN (1,2,3,4);`,那么在查询出结构后,`gorm`内部处理对应的映射逻辑,将其填充到`Article`的`Tag`中,会特别方便,并且避免了循环查询

那么有没有别的办法呢,大致是两种

- `gorm`的`Join`
- 循环`Related`

综合之下,还是`Preload`更好,如果你有更优的方案,欢迎说一下 :)

**3、 `v.(I)` 是什么?**

`v`表示一个接口值,`I`表示接口类型。这个实际就是 Golang 中的**类型断言**,用于判断一个接口值的实际类型是否为某个类型,或一个非接口值的类型是否实现了某个接口类型

---

打开`routers`目录下 v1 版本的`article.go`文件,修改文件内容:

```go
package v1

import (
    "net/http"
    "log"

    "github.com/gin-gonic/gin"
    "github.com/astaxie/beego/validation"
    "github.com/unknwon/com"

    "github.com/EDDYCJY/go-gin-example/models"
    "github.com/EDDYCJY/go-gin-example/pkg/e"
    "github.com/EDDYCJY/go-gin-example/pkg/setting"
    "github.com/EDDYCJY/go-gin-example/pkg/util"
)

//获取单个文章
func GetArticle(c *gin.Context) {
    id := com.StrTo(c.Param("id")).MustInt()

    valid := validation.Validation{}
    valid.Min(id, 1, "id").Message("ID必须大于0")

    code := e.INVALID_PARAMS
    var data interface {}
    if ! valid.HasErrors() {
        if models.ExistArticleByID(id) {
            data = models.GetArticle(id)
            code = e.SUCCESS
        } else {
            code = e.ERROR_NOT_EXIST_ARTICLE
        }
    } else {
        for _, err := range valid.Errors {
            log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
        }
    }

    c.JSON(http.StatusOK, gin.H{
        "code" : code,
        "msg" : e.GetMsg(code),
        "data" : data,
    })
}

//获取多个文章
func GetArticles(c *gin.Context) {
    data := make(map[string]interface{})
    maps := make(map[string]interface{})
    valid := validation.Validation{}

    var state int = -1
    if arg := c.Query("state"); arg != "" {
        state = com.StrTo(arg).MustInt()
        maps["state"] = state

        valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
    }

    var tagId int = -1
    if arg := c.Query("tag_id"); arg != "" {
        tagId = com.StrTo(arg).MustInt()
        maps["tag_id"] = tagId

        valid.Min(tagId, 1, "tag_id").Message("标签ID必须大于0")
    }

    code := e.INVALID_PARAMS
    if ! valid.HasErrors() {
        code = e.SUCCESS

        data["lists"] = models.GetArticles(util.GetPage(c), setting.PageSize, maps)
        data["total"] = models.GetArticleTotal(maps)

    } else {
        for _, err := range valid.Errors {
            log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
        }
    }

    c.JSON(http.StatusOK, gin.H{
        "code" : code,
        "msg" : e.GetMsg(code),
        "data" : data,
    })
}

//新增文章
func AddArticle(c *gin.Context) {
    tagId := com.StrTo(c.Query("tag_id")).MustInt()
    title := c.Query("title")
    desc := c.Query("desc")
    content := c.Query("content")
    createdBy := c.Query("created_by")
    state := com.StrTo(c.DefaultQuery("state", "0")).MustInt()

    valid := validation.Validation{}
    valid.Min(tagId, 1, "tag_id").Message("标签ID必须大于0")
    valid.Required(title, "title").Message("标题不能为空")
    valid.Required(desc, "desc").Message("简述不能为空")
    valid.Required(content, "content").Message("内容不能为空")
    valid.Required(createdBy, "created_by").Message("创建人不能为空")
    valid.Range(state, 0, 1, "state").Message("状态只允许0或1")

    code := e.INVALID_PARAMS
    if ! valid.HasErrors() {
        if models.ExistTagByID(tagId) {
            data := make(map[string]interface {})
            data["tag_id"] = tagId
            data["title"] = title
            data["desc"] = desc
            data["content"] = content
            data["created_by"] = createdBy
            data["state"] = state

            models.AddArticle(data)
            code = e.SUCCESS
        } else {
            code = e.ERROR_NOT_EXIST_TAG
        }
    } else {
        for _, err := range valid.Errors {
            log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
        }
    }

    c.JSON(http.StatusOK, gin.H{
        "code" : code,
        "msg" : e.GetMsg(code),
        "data" : make(map[string]interface{}),
    })
}

//修改文章
func EditArticle(c *gin.Context) {
    valid := validation.Validation{}

    id := com.StrTo(c.Param("id")).MustInt()
    tagId := com.StrTo(c.Query("tag_id")).MustInt()
    title := c.Query("title")
    desc := c.Query("desc")
    content := c.Query("content")
    modifiedBy := c.Query("modified_by")

    var state int = -1
    if arg := c.Query("state"); arg != "" {
        state = com.StrTo(arg).MustInt()
        valid.Range(state, 0, 1, "state").Message("状态只允许0或1")
    }

    valid.Min(id, 1, "id").Message("ID必须大于0")
    valid.MaxSize(title, 100, "title").Message("标题最长为100字符")
    valid.MaxSize(desc, 255, "desc").Message("简述最长为255字符")
    valid.MaxSize(content, 65535, "content").Message("内容最长为65535字符")
    valid.Required(modifiedBy, "modified_by").Message("修改人不能为空")
    valid.MaxSize(modifiedBy, 100, "modified_by").Message("修改人最长为100字符")

    code := e.INVALID_PARAMS
    if ! valid.HasErrors() {
        if models.ExistArticleByID(id) {
            if models.ExistTagByID(tagId) {
                data := make(map[string]interface {})
                if tagId > 0 {
                    data["tag_id"] = tagId
                }
                if title != "" {
                    data["title"] = title
                }
                if desc != "" {
                    data["desc"] = desc
                }
                if content != "" {
                    data["content"] = content
                }

                data["modified_by"] = modifiedBy

                models.EditArticle(id, data)
                code = e.SUCCESS
            } else {
                code = e.ERROR_NOT_EXIST_TAG
            }
        } else {
            code = e.ERROR_NOT_EXIST_ARTICLE
        }
    } else {
        for _, err := range valid.Errors {
            log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
        }
    }

    c.JSON(http.StatusOK, gin.H{
        "code" : code,
        "msg" : e.GetMsg(code),
        "data" : make(map[string]string),
    })
}

//删除文章
func DeleteArticle(c *gin.Context) {
    id := com.StrTo(c.Param("id")).MustInt()

    valid := validation.Validation{}
    valid.Min(id, 1, "id").Message("ID必须大于0")

    code := e.INVALID_PARAMS
    if ! valid.HasErrors() {
        if models.ExistArticleByID(id) {
            models.DeleteArticle(id)
            code = e.SUCCESS
        } else {
            code = e.ERROR_NOT_EXIST_ARTICLE
        }
    } else {
        for _, err := range valid.Errors {
            log.Printf("err.key: %s, err.message: %s", err.Key, err.Message)
        }
    }

    c.JSON(http.StatusOK, gin.H{
        "code" : code,
        "msg" : e.GetMsg(code),
        "data" : make(map[string]string),
    })
}
```

当前目录结构:

```
go-gin-example/
├── conf
│   └── app.ini
├── main.go
├── middleware
├── models
│   ├── article.go
│   ├── models.go
│   └── tag.go
├── pkg
│   ├── e
│   │   ├── code.go
│   │   └── msg.go
│   ├── setting
│   │   └── setting.go
│   └── util
│       └── pagination.go
├── routers
│   ├── api
│   │   └── v1
│   │       ├── article.go
│   │       └── tag.go
│   └── router.go
├── runtime
```

## 验证功能

我们重启服务,执行`go run main.go`,检查控制台输出结果

```
$ go run main.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /api/v1/tags              --> gin-blog/routers/api/v1.GetTags (3 handlers)
[GIN-debug] POST   /api/v1/tags              --> gin-blog/routers/api/v1.AddTag (3 handlers)
[GIN-debug] PUT    /api/v1/tags/:id          --> gin-blog/routers/api/v1.EditTag (3 handlers)
[GIN-debug] DELETE /api/v1/tags/:id          --> gin-blog/routers/api/v1.DeleteTag (3 handlers)
[GIN-debug] GET    /api/v1/articles          --> gin-blog/routers/api/v1.GetArticles (3 handlers)
[GIN-debug] GET    /api/v1/articles/:id      --> gin-blog/routers/api/v1.GetArticle (3 handlers)
[GIN-debug] POST   /api/v1/articles          --> gin-blog/routers/api/v1.AddArticle (3 handlers)
[GIN-debug] PUT    /api/v1/articles/:id      --> gin-blog/routers/api/v1.EditArticle (3 handlers)
[GIN-debug] DELETE /api/v1/articles/:id      --> gin-blog/routers/api/v1.DeleteArticle (3 handlers)
```

使用`Postman`检验接口是否正常,在这里大家可以选用合适的参数传递方式,此处为了方便展示我选用了 GET/Param 传参的方式,而后期会改为 POST。

- 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
- GET:http://127.0.0.1:8000/api/v1/articles
- GET:http://127.0.0.1:8000/api/v1/articles/1
- 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
- DELETE:http://127.0.0.1:8000/api/v1/articles/1

至此,我们的 API's 编写就到这里,下一节我们将介绍另外的一些技巧!

## 参考

### 本系列示例代码

- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)

## 关于

### 修改记录

- 第一版:2018 年 02 月 16 日发布文章
- 第二版:2019 年 10 月 01 日修改文章

## ?

如果有任何疑问或错误,欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见,如果喜欢或对你有所帮助,欢迎 Star,对作者是一种鼓励和推进。

### 我的公众号

![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)

================================================
FILE: content/posts/go/gin/2018-02-14-jwt.md
================================================
---

title:      "「连载五」使用 JWT 进行身份校验"
date:       2018-02-14 12:00:00
author:     "煎鱼"
toc: true
tags:
    - go
    - gin
---

## 涉及知识点

- JWT

## 本文目标

在前面几节中,我们已经基本的完成了 API's 的编写,但是,还存在一些非常严重的问题,例如,我们现在的 API 是可以随意调用的,这显然还不安全全,在本文中我们通过 [jwt-go](https://github.com/dgrijalva/jwt-go) ([GoDoc](https://godoc.org/github.com/dgrijalva/jwt-go))的方式来简单解决这个问题。

## 下载依赖包

首先,我们下载 jwt-go 的依赖包,如下:

```
go get -u github.com/dgrijalva/jwt-go
```

## 编写 jwt 工具包

我们需要编写一个`jwt`的工具包,我们在`pkg`下的`util`目录新建`jwt.go`,写入文件内容:

```go
package util

import (
	"time"

	jwt "github.com/dgrijalva/jwt-go"

	"github.com/EDDYCJY/go-gin-example/pkg/setting"
)

var jwtSecret = []byte(setting.JwtSecret)

type Claims struct {
	Username string `json:"username"`
	Password string `json:"password"`
	jwt.StandardClaims
}

func GenerateToken(username, password string) (string, error) {
	nowTime := time.Now()
	expireTime := nowTime.Add(3 * time.Hour)

	claims := Claims{
		username,
		password,
		jwt.StandardClaims {
			ExpiresAt : expireTime.Unix(),
			Issuer : "gin-blog",
		},
	}

	tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	token, err := tokenClaims.SignedString(jwtSecret)

	return token, err
}

func ParseToken(token string) (*Claims, error) {
	tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
		return jwtSecret, nil
	})

	if tokenClaims != nil {
		if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid {
			return claims, nil
		}
	}

	return nil, err
}
```

在这个工具包,我们涉及到

- `NewWithClaims(method SigningMethod, claims Claims)`,`method`对应着`SigningMethodHMAC struct{}`,其包含`SigningMethodHS256`、`SigningMethodHS384`、`SigningMethodHS512`三种`crypto.Hash`方案
- `func (t *Token) SignedString(key interface{})` 该方法内部生成签名字符串,再用于获取完整、已签名的`token`
- `func (p *Parser) ParseWithClaims` 用于解析鉴权的声明,[方法内部](https://gowalker.org/github.com/dgrijalva/jwt-go#Parser_ParseWithClaims)主要是具体的解码和校验的过程,最终返回`*Token`
- `func (m MapClaims) Valid()` 验证基于时间的声明`exp, iat, nbf`,注意如果没有任何声明在令牌中,仍然会被认为是有效的。并且对于时区偏差没有计算方法

有了`jwt`工具包,接下来我们要编写要用于`Gin`的中间件,我们在`middleware`下新建`jwt`目录,新建`jwt.go`文件,写入内容:

```go
package jwt

import (
	"time"
	"net/http"

	"github.com/gin-gonic/gin"

	"github.com/EDDYCJY/go-gin-example/pkg/util"
	"github.com/EDDYCJY/go-gin-example/pkg/e"
)

func JWT() gin.HandlerFunc {
	return func(c *gin.Context) {
		var code int
		var data interface{}

		code = e.SUCCESS
		token := c.Query("token")
		if token == "" {
			code = e.INVALID_PARAMS
		} else {
			claims, err := util.ParseToken(token)
			if err != nil {
				code = e.ERROR_AUTH_CHECK_TOKEN_FAIL
			} else if time.Now().Unix() > claims.ExpiresAt {
				code = e.ERROR_AUTH_CHECK_TOKEN_TIMEOUT
			}
		}

		if code != e.SUCCESS {
			c.JSON(http.StatusUnauthorized, gin.H{
		        "code" : code,
		        "msg" : e.GetMsg(code),
		        "data" : data,
		    })

		    c.Abort()
		    return
		}

		c.Next()
	}
}
```

## 如何获取`Token`

那么我们如何调用它呢,我们还要获取`Token`呢?

1、 我们要新增一个获取`Token`的 API

在`models`下新建`auth.go`文件,写入内容:

```go
package models

type Auth struct {
	ID int `gorm:"primary_key" json:"id"`
	Username string `json:"username"`
	Password string `json:"password"`
}

func CheckAuth(username, password string) bool {
	var auth Auth
	db.Select("id").Where(Auth{Username : username, Password : password}).First(&auth)
	if auth.ID > 0 {
		return true
	}

	return false
}
```

在`routers`下的`api`目录新建`auth.go`文件,写入内容:

```go
package api

import (
	"log"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/astaxie/beego/validation"

	"github.com/EDDYCJY/go-gin-example/pkg/e"
	"github.com/EDDYCJY/go-gin-example/pkg/util"
	"github.com/EDDYCJY/go-gin-example/models"
)

type auth struct {
	Username string `valid:"Required; MaxSize(50)"`
	Password string `valid:"Required; MaxSize(50)"`
}

func GetAuth(c *gin.Context) {
	username := c.Query("username")
	password := c.Query("password")

	valid := validation.Validation{}
	a := auth{Username: username, Password: password}
	ok, _ := valid.Valid(&a)

	data := make(map[string]interface{})
	code := e.INVALID_PARAMS
	if ok {
		isExist := models.CheckAuth(username, password)
		if isExist {
			token, err := util.GenerateToken(username, password)
			if err != nil {
				code = e.ERROR_AUTH_TOKEN
			} else {
				data["token"] = token

				code = e.SUCCESS
			}

		} else {
			code = e.ERROR_AUTH
		}
	} else {
		for _, err := range valid.Errors {
            log.Println(err.Key, err.Message)
        }
	}

	c.JSON(http.StatusOK, gin.H{
        "code" : code,
        "msg" : e.GetMsg(code),
        "data" : data,
    })
}
```

我们打开`routers`目录下的`router.go`文件,修改文件内容(新增获取 token 的方法):

```go
package routers

import (
    "github.com/gin-gonic/gin"

    "github.com/EDDYCJY/go-gin-example/routers/api"
    "github.com/EDDYCJY/go-gin-example/routers/api/v1"
    "github.com/EDDYCJY/go-gin-example/pkg/setting"
)

func InitRouter() *gin.Engine {
    r := gin.New()

    r.Use(gin.Logger())

    r.Use(gin.Recovery())

    gin.SetMode(setting.RunMode)

    r.GET("/auth", api.GetAuth)

    apiv1 := r.Group("/api/v1")
    {
        ...
    }

    return r
}
```

## 验证`Token`

获取`token`的 API 方法就到这里啦,让我们来测试下是否可以正常使用吧!

重启服务后,用`GET`方式访问`http://127.0.0.1:8000/auth?username=test&password=test123456`,查看返回值是否正确

```json
{
  "code": 200,
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJwYXNzd29yZCI6InRlc3QxMjM0NTYiLCJleHAiOjE1MTg3MjAwMzcsImlzcyI6Imdpbi1ibG9nIn0.-kK0V9E06qTHOzupQM_gHXAGDB3EJtJS4H5TTCyWwW8"
  },
  "msg": "ok"
}
```

我们有了`token`的 API,也调用成功了

## 将中间件接入`Gin`

2、 接下来我们将中间件接入到`Gin`的访问流程中

我们打开`routers`目录下的`router.go`文件,修改文件内容(新增引用包和中间件引用)

```go
package routers

import (
    "github.com/gin-gonic/gin"

    "github.com/EDDYCJY/go-gin-example/routers/api"
    "github.com/EDDYCJY/go-gin-example/routers/api/v1"
    "github.com/EDDYCJY/go-gin-example/pkg/setting"
    "github.com/EDDYCJY/go-gin-example/middleware/jwt"
)

func InitRouter() *gin.Engine {
    r := gin.New()

    r.Use(gin.Logger())

    r.Use(gin.Recovery())

    gin.SetMode(setting.RunMode)

    r.GET("/auth", api.GetAuth)

    apiv1 := r.Group("/api/v1")
    apiv1.Use(jwt.JWT())
    {
        ...
    }

    return r
}
```

当前目录结构:

```
go-gin-example/
├── conf
│   └── app.ini
├── main.go
├── middleware
│   └── jwt
│       └── jwt.go
├── models
│   ├── article.go
│   ├── auth.go
│   ├── models.go
│   └── tag.go
├── pkg
│   ├── e
│   │   ├── code.go
│   │   └── msg.go
│   ├── setting
│   │   └── setting.go
│   └── util
│       ├── jwt.go
│       └── pagination.go
├── routers
│   ├── api
│   │   ├── auth.go
│   │   └── v1
│   │       ├── article.go
│   │       └── tag.go
│   └── router.go
├── runtime
```

到这里,我们的`JWT`编写就完成啦!

## 验证功能

我们来测试一下,再次访问

- http://127.0.0.1:8000/api/v1/articles
- http://127.0.0.1:8000/api/v1/articles?token=23131

正确的反馈应该是

```json
{
  "code": 400,
  "data": null,
  "msg": "请求参数错误"
}

{
  "code": 20001,
  "data": null,
  "msg": "Token鉴权失败"
}

```

我们需要访问`http://127.0.0.1:8000/auth?username=test&password=test123456`,得到`token`

```json
{
  "code": 200,
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InRlc3QiLCJwYXNzd29yZCI6InRlc3QxMjM0NTYiLCJleHAiOjE1MTg3MjQ2OTMsImlzcyI6Imdpbi1ibG9nIn0.KSBY6TeavV_30kfmP7HWLRYKP5TPEDgHtABe9HCsic4"
  },
  "msg": "ok"
}
```

再用包含`token`的 URL 参数去访问我们的应用 API,

访问`http://127.0.0.1:8000/api/v1/articles?token=eyJhbGci...`,检查接口返回值

```json
{
  "code": 200,
  "data": {
    "lists": [
      {
        "id": 2,
        "created_on": 1518700920,
        "modified_on": 0,
        "tag_id": 1,
        "tag": {
          "id": 1,
          "created_on": 1518684200,
          "modified_on": 0,
          "name": "tag1",
          "created_by": "",
          "modified_by": "",
          "state": 0
        },
        "content": "test-content",
        "created_by": "test-created",
        "modified_by": "",
        "state": 0
      }
    ],
    "total": 1
  },
  "msg": "ok"
}
```

返回正确,至此我们的`jwt-go`在`Gin`中的验证就完成了!

## 参考

### 本系列示例代码

- [go-gin-example](https://github.com/EDDYCJY/go-gin-example)

## 关于

### 修改记录

- 第一版:2018 年 02 月 16 日发布文章
- 第二版:2019 年 10 月 01 日修改文章

## ?

如果有任何疑问或错误,欢迎在 [issues](https://github.com/EDDYCJY/blog) 进行提问或给予修正意见,如果喜欢或对你有所帮助,欢迎 Star,对作者是一种鼓励和推进。

### 我的公众号

![image](https://image.eddycjy.com/8d0b0c3a11e74efd5fdfd7910257e70b.jpg)

================================================
FILE: content/posts/go/gin/2018-02-15-log.md
================================================
---

title:      "「连载六」编写一个简单的文件日志"
date:       2018-02-15 12:00:00
author:     "煎鱼"
toc: true
tags:
    - go
    - gin
---

## 涉及知识点

- 自定义 log。

## 本文目标

在上一节中,我们解决了 API's 可以任意访问的问题,那么我们现在还有一个问题,就是我们的日志,都是输出到控制台上的,这显然对于一个项目来说是不合理的,因此我们这一节简单封装`log`库,使其支持简单的文件日志!

## 新建`logging`包

我们在`pkg`下新建`logging`目录,新建`file.go`和`log.go`文件,写入内容:

### 编写`file`文件

**1、 file.go:**

```go
package logging

import (
	"os"
	"time"
	"fmt"
	"log"
)

var (
	LogSavePath = "runtime/logs/"
	LogSaveName = "log"
	LogFileExt = "log"
	TimeFormat = "20060102"
)

func getLogFilePath() string {
	return fmt.Sprintf("%s", LogSavePath)
}

func getLogFileFullPath() string {
	prefixPath := getLogFilePath()
	suffixPath := fmt.Sprintf("%s%s.%s", LogSaveName, time.Now().Format(TimeFormat), LogFileExt)

	return fmt.Sprintf("%s%s", prefixPath, suffixPath)
}

func openLogFile(filePath string) *os.File {
	_, err := os.Stat(filePath)
	switch {
		case os.IsNotExist(err):
			mkDir()
		case os.IsPermission(err):
			log.Fatalf("Permission :%v", err)
	}

	handle, err := os.OpenFile(filePath, os.O_APPEND | os.O_CREATE | os.O_WRONLY, 0644)
	if err != nil {
		log.Fatalf("Fail to OpenFile :%v", err)
	}

	return handle
}

func mkDir() {
	dir, _ := os.Getwd()
	err := os.MkdirAll(dir + "/" + getLogFilePath(), os.ModePerm)
	if err != nil {
		panic(err)
	}
}
```

- `os.Stat`:返回文件信息结构描述文件。如果出现错误,会返回`*PathError`

```go
type PathError struct {
    Op   string
    Path string
    Err  error
}
```

- `os.IsNotExist`:能够接受`ErrNotExist`、`syscall`的一些错误,它会返回一个布尔值,能够得知文件不存在或目录不存在
- `os.IsPermission`:能够接受`ErrPermission`、`syscall`的一些错误,它会返回一个布尔值,能够得知权限是否满足
- `os.OpenFile`:调用文件,支持传入文件名称、指定的模式调用文件、文件权限,返回的文件的方法可以用于 I/O。如果出现错误,则为`*PathError`。

```go
const (
    // Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified.
    O_RDONLY int = syscall.O_RDONLY // 以只读模式打开文件
    O_WRONLY int = syscall.O_WRONLY // 以只写模式打开文件
    O_RDWR   int = syscall.O_RDWR   // 以读写模式打开文件
    // The remaining values may be or'ed in to control behavior.
    O_APPEND int = syscall.O_APPEND // 在写入时将数据追加到文件中
    O_CREATE int = syscall.O_CREAT  // 如果不存在,则创建一个新文件
    O_EXCL   int = syscall.O_EXCL   // 使用O_CREATE时,文件必须不存在
    O_SYNC   int = syscall.O_SYNC   // 同步IO
    O_TRUNC  int = syscall.O_TRUNC  // 如果可以,打开时
)
```

- `os.Getwd`:返回与当前目录对应的根路径名
- `os.MkdirAll`:创建对应的目录以及所需的子目录,若成功则返回`nil`,否则返回`error`
- `os.ModePerm`:`const`定义`ModePerm FileMode = 0777`

### 编写`log`文件

**2、log.go**

```go
package logging

import (
	"log"
	"os"
	"runtime"
	"path/filepath"
	"fmt"
)

type Level int

var (
	F *os.File

	DefaultPrefix = ""
	DefaultCallerDepth = 2

	logger *log.Logger
	logPrefix = ""
	levelFlags = []string{"DEBUG", "INFO", "WARN", "ERROR", "FATAL"}
)

const (
	DEBUG Level = iota
	INFO
	WARNING
	ERROR
	FATAL
)

func init() {
	filePath := getLogFileFullPath()
	F = openLogFile(filePath)

	logger = log.New(F, DefaultPrefix, log.LstdFlags)
}

func Debug(v ...interface{}) {
	setPrefix(DEBUG)
	logger.Println(v)
}

func Info(v ...interface{}) {
	setPrefix(INFO)
	logger.Println(v)
}

func Warn(v ...interface{}) {
	setPrefix(WARNING)
	logger.Println(v)
}

func Error(v ...interface{}) {
	setPrefix(ERROR)
	logger.Println(v)
}

func Fatal(v ...interface{}) {
	setPrefix(FATAL)
	logger.Fatalln(v)
}

func setPrefix(level Level) {
	_, file, line, ok := runtime.Caller(DefaultCallerDepth)
	if ok {
		logPrefix = fmt.Sprintf("[%s][%s:%d]", levelFlags[level], filepath.Base(file), line)
	} else {
		logPrefix = fmt.Sprintf("[%s]", levelFlags[level])
	}

	logger.SetPrefix(logPrefix)
}

```

- `log.New`:创建一个新的日志记录器。`out`定义要写入日志数据的`IO`句柄。`prefix`定义每个生成的日志行的开头。`flag`定义了日志记录属性

```go
func New(out io.Writer, prefix string, flag int) *Logger {
    return &Logger{out: out, prefix: prefix, flag: flag}
}
```

- `log.LstdFlags`:日志记录的格式属性之一,其余的选项如下

```go
const (
    Ldate         = 1 << iota     // the date in the local time zone: 2009/01/23
    Ltime                         // the time in the local time zone: 01:23:23
    Lmicroseconds                 // microsecond resolution: 01:23:23.123123.  assumes Ltime.
    Llongfile                     // full file name and line number: /a/b/c/d.go:23
    Lshortfile                    // final file name element and line number: d.go:23. overrides Llongfile
    LUTC                          // if Ldate or Ltime is set, use UTC rather than the local time zone
    LstdFlags     = Ldate | Ltime // initial values for the standard logger
)
```

当前目录结构:

```
gin-blog/
├── conf
│   └── app.ini
├── main.go
├── middleware
│   └── jwt
│       └── jwt.go
├── models
│   ├── article.go
│   ├── auth.go
│   ├── models.go
│   └── tag.go
├── pkg
│   ├── e
│   │   ├── code.go
│   │   └── msg.go
│   ├── logging
│   │   ├── file.go
│   │   └── log.go
│   ├── setting
│   │   └── setting.go
│   └── util
│       ├── jwt.go
│       └── pagination.go
├── routers
│   ├── api
│   │   ├── auth.go
│   │   └── v1
│   │       ├── article.go
│   │       └── tag.go
│   └── router.go
├── runtime

```

我们自定义的`logging`包,已经基本完成了,接下来让它接入到我们的项目之中吧。我们打开先前包含`log`包的代码,如下:

1. 打开`routers`目录下的`article.go`、`tag.go`、`auth.go`。
2. 将`log`包的引用删除,修改引用我们自己的日志包为`github.com/EDDYCJY/go-gin-example/pkg/logging`。
3. 将原本的`log.Println(...)`改为`logging.Info(...)`。

例如`auth.go`文件的修改内容:

```go
package api

import (
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/astaxie/beego/validation"

	"github.com/EDDYCJY/go-gin-example/pkg/e"
	"github.com/EDDYCJY/go-gin-example/pkg/util"
	"github.com/EDDYCJY/go-gin-example/models"
	"github.com/EDDYCJY/go-gin-example/pkg/logging"
)
...
func GetAuth(c *gin.Context) {
	...
	code := e.INVALID_PARAMS
	if ok {
		...
	} else {
	    for _, err := range valid.Errors {
                logging.Info(err.Key, err.Message)
            }
	}

	c.JSON(http.StatusOK, gin.H{
        "code" : code,
     
Download .txt
gitextract_hs1r1ie2/

├── .gitignore
├── README.md
├── archetypes/
│   ├── default.md
│   └── posts.md
├── config.toml
├── content/
│   ├── about.md
│   ├── go-categories.md
│   ├── k8s-categories.md
│   ├── posts/
│   │   ├── 2020-summary.md
│   │   ├── 2020-top100.md
│   │   ├── 2021-ecug.md
│   │   ├── 2021giac.md
│   │   ├── go/
│   │   │   ├── 117-build.md
│   │   │   ├── 117-errorstack.md
│   │   │   ├── 117-generics.md
│   │   │   ├── 117-module-pruning-lazy.md
│   │   │   ├── 117-performance.md
│   │   │   ├── 118-build-info.md
│   │   │   ├── 118-build.md
│   │   │   ├── 118-constraints.md
│   │   │   ├── 118-cut.md
│   │   │   ├── 118-leader-generics.md
│   │   │   ├── 118-module.md
│   │   │   ├── 4errors.md
│   │   │   ├── again-mutex.md
│   │   │   ├── annotation.md
│   │   │   ├── any.md
│   │   │   ├── class-extends.md
│   │   │   ├── crawler/
│   │   │   │   ├── 2018-03-21-douban-top250.md
│   │   │   │   ├── 2018-04-01-cars.md
│   │   │   │   └── 2018-04-28-go2018.md
│   │   │   ├── defer/
│   │   │   │   └── 2019-05-27-defer.md
│   │   │   ├── delve.md
│   │   │   ├── empty-struct.md
│   │   │   ├── enum.md
│   │   │   ├── func-reload.md
│   │   │   ├── fuzzing.md
│   │   │   ├── gdb.md
│   │   │   ├── generics-apis.md
│   │   │   ├── generics-design.md
│   │   │   ├── generics-proposal.md
│   │   │   ├── gin/
│   │   │   │   ├── 2018-02-10-install.md
│   │   │   │   ├── 2018-02-11-api-01.md
│   │   │   │   ├── 2018-02-12-api-02.md
│   │   │   │   ├── 2018-02-13-api-03.md
│   │   │   │   ├── 2018-02-14-jwt.md
│   │   │   │   ├── 2018-02-15-log.md
│   │   │   │   ├── 2018-03-15-reload-http.md
│   │   │   │   ├── 2018-03-18-swagger.md
│   │   │   │   ├── 2018-03-24-golang-docker.md
│   │   │   │   ├── 2018-03-26-cgo.md
│   │   │   │   ├── 2018-04-15-gorm-callback.md
│   │   │   │   ├── 2018-04-29-cron.md
│   │   │   │   ├── 2018-05-27-config-upload.md
│   │   │   │   ├── 2018-06-02-application-redis.md
│   │   │   │   ├── 2018-06-14-excel.md
│   │   │   │   ├── 2018-07-05-image.md
│   │   │   │   ├── 2018-07-07-font.md
│   │   │   │   ├── 2018-08-26-makefile.md
│   │   │   │   └── 2018-09-01-nginx.md
│   │   │   ├── gmp-why-p.md
│   │   │   ├── go-array-slice.md
│   │   │   ├── go-bootstrap.md
│   │   │   ├── go-bootstrap0.md
│   │   │   ├── go-concurrent-lock.md
│   │   │   ├── go-design.md
│   │   │   ├── go-empty-struct.md
│   │   │   ├── go-error2panic.md
│   │   │   ├── go-errors-boom.md
│   │   │   ├── go-golang.md
│   │   │   ├── go-map-access.md
│   │   │   ├── go-moduels/
│   │   │   │   ├── 2019-09-29-goproxy-cn.md
│   │   │   │   └── 2020-02-28-go-modules.md
│   │   │   ├── go-standards.md
│   │   │   ├── go-tips-defer.md
│   │   │   ├── go-tips-gmp-p.md
│   │   │   ├── go-tips-goroutineid.md
│   │   │   ├── go-tips-goroutineloop.md
│   │   │   ├── go-tips-goroutinenums.md
│   │   │   ├── go-tips-interface.md
│   │   │   ├── go-tips-lenstr.md
│   │   │   ├── go-tips-sturct.md
│   │   │   ├── go-tips-timer-memory.md
│   │   │   ├── go-typeparams-master.md
│   │   │   ├── go-why-path.md
│   │   │   ├── go1.16-1.md
│   │   │   ├── go1.16-2.md
│   │   │   ├── go1.16-3.md
│   │   │   ├── go1.16-mod.md
│   │   │   ├── go11.md
│   │   │   ├── go16-preview.md
│   │   │   ├── go2-errors.md
│   │   │   ├── gophercon2020-errors.md
│   │   │   ├── goroutine-27.md
│   │   │   ├── goroutine-errors.md
│   │   │   ├── goroutine-leak.md
│   │   │   ├── grpc/
│   │   │   │   ├── 2018-09-22-install.md
│   │   │   │   ├── 2018-09-23-client-and-server.md
│   │   │   │   ├── 2018-09-24-stream-client-server.md
│   │   │   │   ├── 2018-10-07-grpc-tls.md
│   │   │   │   ├── 2018-10-08-ca-tls.md
│   │   │   │   ├── 2018-10-10-interceptor.md
│   │   │   │   ├── 2018-10-12-grpc-http.md
│   │   │   │   ├── 2018-10-14-per-rpc-credentials.md
│   │   │   │   ├── 2018-10-16-deadlines.md
│   │   │   │   └── 2018-10-20-zipkin.md
│   │   │   ├── grpc-gateway/
│   │   │   │   ├── 2018-02-23-install.md
│   │   │   │   ├── 2018-02-27-hello-world.md
│   │   │   │   ├── 2018-03-04-swagger.md
│   │   │   │   └── 2019-06-22-grpc-gateway-tls.md
│   │   │   ├── import-cyc.md
│   │   │   ├── import-generics.md
│   │   │   ├── len.md
│   │   │   ├── map/
│   │   │   │   ├── 2019-03-05-map-access.md
│   │   │   │   ├── 2019-03-24-map-assign.md
│   │   │   │   └── 2019-04-07-why-map-no-order.md
│   │   │   ├── map-65.md
│   │   │   ├── map-con.md
│   │   │   ├── map-reset.md
│   │   │   ├── map-slice-concurrency.md
│   │   │   ├── memory-model.md
│   │   │   ├── news-slices-maps.md
│   │   │   ├── news115.md
│   │   │   ├── nil-func.md
│   │   │   ├── panic/
│   │   │   │   └── 2019-05-21-panic-and-recover.md
│   │   │   ├── pkg/
│   │   │   │   ├── 2018-09-28-log.md
│   │   │   │   ├── 2018-12-04-fmt.md
│   │   │   │   └── 2018-12-15-unsafe.md
│   │   │   ├── plugin.md
│   │   │   ├── real-context.md
│   │   │   ├── reflect.md
│   │   │   ├── runtimepark.md
│   │   │   ├── rust-php.md
│   │   │   ├── site-history.md
│   │   │   ├── slice/
│   │   │   │   ├── 2018-12-11-slice.md
│   │   │   │   └── 2019-01-06-why-slice-max.md
│   │   │   ├── slice-discuss.md
│   │   │   ├── slice-leak.md
│   │   │   ├── slice-string-header.md
│   │   │   ├── stop-goroutine.md
│   │   │   ├── struct-pointer.md
│   │   │   ├── switch-type.md
│   │   │   ├── sync-map.md
│   │   │   ├── talk/
│   │   │   │   ├── 2018-03-13-golang-relatively-path.md
│   │   │   │   ├── 2018-05-21-go-fake-useragent.md
│   │   │   │   ├── 2018-06-07-go-redis-protocol.md
│   │   │   │   ├── 2018-11-25-gomock.md
│   │   │   │   ├── 2018-12-26-go-memory-align.md
│   │   │   │   ├── 2019-01-20-control-goroutine.md
│   │   │   │   ├── 2019-02-17-for-loop-json-unmarshal.md
│   │   │   │   ├── 2019-03-31-go-ins.md
│   │   │   │   ├── 2019-05-20-stack-heap.md
│   │   │   │   ├── 2019-06-16-defer-loss.md
│   │   │   │   ├── 2019-06-29-talking-grpc.md
│   │   │   │   ├── 2019-09-07-go1.13-defer.md
│   │   │   │   └── 2019-09-24-why-vsz-large.md
│   │   │   ├── ternary-operator.md
│   │   │   ├── throw.md
│   │   │   ├── tools/
│   │   │   │   ├── 2018-09-15-go-tool-pprof.md
│   │   │   │   ├── 2019-07-12-go-tool-trace.md
│   │   │   │   ├── 2019-08-19-godebug-sched.md
│   │   │   │   └── 2019-09-02-godebug-gc.md
│   │   │   ├── type-after.md
│   │   │   ├── unsafe-pointer.md
│   │   │   ├── value-quote.md
│   │   │   ├── var.md
│   │   │   └── when-gc.md
│   │   ├── go-meetup1017.md
│   │   ├── go-programming-tour-book.md
│   │   ├── kubernetes/
│   │   │   ├── 2020-05-01-install.md
│   │   │   ├── 2020-05-03-deployment.md
│   │   │   └── 2020-05-10-api.md
│   │   ├── microservice/
│   │   │   ├── dismantle.md
│   │   │   ├── flowcontrol-circuitbreaker.md
│   │   │   ├── leaky-token-buckets.md
│   │   │   ├── linkage.md
│   │   │   ├── monitor-alarm.md
│   │   │   ├── standardization.md
│   │   │   ├── tests.md
│   │   │   └── tracing.md
│   │   ├── mq-nodus.md
│   │   ├── prometheus/
│   │   │   ├── 2020-05-16-metrics.md
│   │   │   ├── 2020-05-16-pull.md
│   │   │   └── 2020-05-16-startup.md
│   │   ├── reading/
│   │   │   ├── 2020-04-24-book.md
│   │   │   ├── documentary-of-go.md
│   │   │   ├── programmer-accom-base.md
│   │   │   ├── programmer-compile-link.md
│   │   │   └── programmer-linker.md
│   │   ├── reload-man.md
│   │   ├── where-is-proto.md
│   │   ├── why-container-memory-exceed.md
│   │   ├── why-container-memory-exceed2.md
│   │   └── why-mq.md
│   └── prometheus-categories.md
├── layouts/
│   ├── _default/
│   │   ├── baseof.html
│   │   ├── list.html
│   │   └── single.html
│   ├── index.html
│   ├── partials/
│   │   ├── analytics.html
│   │   ├── comments.html
│   │   ├── favicons.html
│   │   ├── footer.html
│   │   ├── header.html
│   │   ├── social-icons.html
│   │   ├── structured-data.html
│   │   └── svg.html
│   └── posts/
│       ├── rss.xml
│       └── single.html
├── resources/
│   └── _gen/
│       └── assets/
│           └── scss/
│               └── scss/
│                   ├── style.scss_c16d144eee185fbddd582cd5e25a4fae.content
│                   └── style.scss_c16d144eee185fbddd582cd5e25a4fae.json
├── static/
│   └── css/
│       └── styles.css
└── themes/
    └── hermit/
        ├── .editorconfig
        ├── .gitattributes
        ├── LICENSE
        ├── README.md
        ├── archetypes/
        │   ├── default.md
        │   └── posts.md
        ├── assets/
        │   ├── js/
        │   │   └── main.js
        │   └── scss/
        │       ├── _animate.scss
        │       ├── _normalize.scss
        │       ├── _predefined.scss
        │       ├── _syntax.scss
        │       └── style.scss
        ├── exampleSite/
        │   ├── config.toml
        │   └── content/
        │       ├── about-hugo.md
        │       └── posts/
        │           ├── creating-a-new-theme.md
        │           ├── goisforlovers.md
        │           ├── hugoisforlovers.md
        │           ├── migrate-from-jekyll.md
        │           ├── post-with-featured-image.md
        │           ├── the-figure-shortcode.md
        │           └── typography.md
        ├── i18n/
        │   ├── en.toml
        │   ├── it.toml
        │   └── zh-hans.toml
        ├── layouts/
        │   ├── 404.html
        │   ├── _default/
        │   │   ├── baseof.html
        │   │   ├── list.html
        │   │   └── single.html
        │   ├── index.html
        │   ├── partials/
        │   │   ├── analytics.html
        │   │   ├── favicons.html
        │   │   ├── footer.html
        │   │   ├── header.html
        │   │   ├── social-icons.html
        │   │   ├── structured-data.html
        │   │   └── svg.html
        │   ├── post/
        │   │   ├── rss.xml
        │   │   └── single.html
        │   └── posts/
        │       ├── rss.xml
        │       └── single.html
        ├── resources/
        │   └── _gen/
        │       └── assets/
        │           ├── js/
        │           │   └── js/
        │           │       ├── main.js_d11fe7b62c27961c87ecd0f2490357b9.content
        │           │       └── main.js_d11fe7b62c27961c87ecd0f2490357b9.json
        │           └── scss/
        │               └── scss/
        │                   ├── style.scss_c16d144eee185fbddd582cd5e25a4fae.content
        │                   └── style.scss_c16d144eee185fbddd582cd5e25a4fae.json
        ├── static/
        │   ├── browserconfig.xml
        │   ├── site.webmanifest
        │   └── utteranc.js
        └── theme.toml
Download .txt
SYMBOL INDEX (10 symbols across 1 files)

FILE: themes/hermit/static/utteranc.js
  function u (line 1) | function u(e,u){if(e in i)return i[e];var t="function"==typeof parcelReq...
  function q (line 1) | function q(e){for(var r,o=/\+/g,n=/([^&=]+)=?([^&]*)/g,p=function(e){ret...
  function h (line 1) | function h(e){var r=[];for(var o in e)e.hasOwnProperty(o)&&r.push(encode...
  function i (line 1) | function i(e){try{l(n.next(e))}catch(t){a(t)}}
  function $ (line 1) | function $(e){try{l(n.throw(e))}catch(t){a(t)}}
  function l (line 1) | function l(e){e.done?o(e.value):new r(function(t){t(e.value)}).then(i,$)}
  function $ (line 1) | function $(a){return function($){return function(a){if(r)throw new TypeE...
  function i (line 1) | function i(e){return f+"/authorize?"+h({redirect_uri:e})}
  function p (line 1) | function p(){return r(this,void 0,Promise,function(){var e,t,r;return s(...
  function v (line 1) | function v(e){addEventListener("click",function(t){if(t.target instanceo...
Condensed preview — 260 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,819K chars).
[
  {
    "path": ".gitignore",
    "chars": 46,
    "preview": ".idea/\n.DS_Store\n_book/\nnode_modules/\npublic/\n"
  },
  {
    "path": "README.md",
    "chars": 394,
    "preview": "# 煎鱼的迷之博客\n\n写写代码,喝喝茶,搞搞 Go,一起吧,这是我的项目地址:https://github.com/eddycjy/blog\n\n## 在线阅读\n\n- https://eddycjy.com/\n\n## 我的公众号\n\n所有文章和"
  },
  {
    "path": "archetypes/default.md",
    "chars": 108,
    "preview": "---\ntitle: \"{{ replace .Name \"-\" \" \" | title }}\"\ndate: {{ .Date }}\ndraft: true\ncomments: false\nimages:\n---\n\n"
  },
  {
    "path": "archetypes/posts.md",
    "chars": 123,
    "preview": "---\ntitle: \"{{ replace .Name \"-\" \" \" | title }}\"\ndate: {{ .Date }}\ndraft: true\ntoc: false\nimages:\ntags: \n  - untagged\n--"
  },
  {
    "path": "config.toml",
    "chars": 2968,
    "preview": "baseURL = \"https://eddycjy.com\"\nlanguageCode = \"zh-hans\"\ndefaultContentLanguage = \"en\"\ntitle = \"煎鱼\"\ntheme = \"hermit\"\n# e"
  },
  {
    "path": "content/about.md",
    "chars": 797,
    "preview": "---\ntitle: \"关于\"\ndate: \"2020-03-15\"\n---\n\n你好,我是煎鱼,最近沉迷于 Go、Kubernetes、Prometheus 这一生态圈子里的东西(努力学习中)。在工作中,目前主要负责公司的基础架构/组件的建"
  },
  {
    "path": "content/go-categories.md",
    "chars": 2524,
    "preview": "---\ntitle: \"《跟煎鱼学 Go》\"\ndate: \"2020-04-21\"\n---\n\n我不怎么喜欢左写写,右写写,因此总是在不知不觉中写了不少的系列教程,希望对你有所帮助,若要催更请关注公众号后私聊催。\n\n- 一:**HTTP 应用"
  },
  {
    "path": "content/k8s-categories.md",
    "chars": 638,
    "preview": "---\ntitle: \"《跟煎鱼学 Kubernetes/Prometheus》\"\ndate: \"2020-05-16\"\n---\n\n请注意,这不是成品,随时可能会对以前的章节进行修改。那么为什么要放出来呢,当然是为了催更自己。\n\n1. [K"
  },
  {
    "path": "content/posts/2020-summary.md",
    "chars": 1960,
    "preview": "---\ntitle: \"拖更的 2020 年不一样\"\ndate: 2020-12-31T21:29:55+08:00\nimages:\ntags: \n  - 总结\n---\n\n大家好,我是煎鱼。\n\n万万没想到...想着写 2020 年总结,结果"
  },
  {
    "path": "content/posts/2020-top100.md",
    "chars": 18988,
    "preview": "---\ntitle: \"吐血整理 | 快速了解全球软件案例(Top100)\"\ndate: 2020-12-22T21:26:44+08:00\ntoc: true\nimages:\ntags: \n  - top100\n---\n\n前几天,煎鱼去了"
  },
  {
    "path": "content/posts/2021-ecug.md",
    "chars": 1631,
    "preview": "---\ntitle: \"推荐一个牛逼的技术社区!\"\ndate: 2021-01-05T21:26:50+08:00\ntoc: false\nimages:\ntags: \n  - ecug\n---\n\n相信我的读者中不少是 Go 语言的爱好者,又"
  },
  {
    "path": "content/posts/2021giac.md",
    "chars": 6231,
    "preview": "---\ntitle: \"我周末参加了个架构师大会!\"\ndate: 2021-12-31T12:54:57+08:00\ntoc: true\nimages:\ntags: \n  - giac\n---\n\n大家好,我是煎鱼。\n\n前两天 GIAC 全球"
  },
  {
    "path": "content/posts/go/117-build.md",
    "chars": 3548,
    "preview": "---\ntitle: \"时隔 3 年,Go1.17 增强构建约束!\"\ndate: 2021-12-31T12:54:56+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - go1.17\n---\n\n大家好,我"
  },
  {
    "path": "content/posts/go/117-errorstack.md",
    "chars": 2563,
    "preview": "---\ntitle: \"Go1.17 新特性,优化抛出的错误堆栈\"\ndate: 2021-12-31T12:55:05+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n平时在日常工"
  },
  {
    "path": "content/posts/go/117-generics.md",
    "chars": 2145,
    "preview": "---\ntitle: \"Go 1.17 支持泛型了?具体怎么用\"\ndate: 2021-12-31T12:55:02+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n千呼万唤的,G"
  },
  {
    "path": "content/posts/go/117-module-pruning-lazy.md",
    "chars": 3327,
    "preview": "---\ntitle: \"Go1.17 新特性:对 Go 依赖管理的一把大剪刀\"\ndate: 2021-12-31T12:55:03+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n"
  },
  {
    "path": "content/posts/go/117-performance.md",
    "chars": 2520,
    "preview": "---\ntitle: \"Go1.17 新特性,凭什么提速 5~10%?\"\ndate: 2021-12-31T12:55:04+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n在 G"
  },
  {
    "path": "content/posts/go/118-build-info.md",
    "chars": 1983,
    "preview": "---\ntitle: \"Go1.18 新特性:编译后的二进制文件,将包含更多信息\"\ndate: 2022-02-05T16:02:45+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。"
  },
  {
    "path": "content/posts/go/118-build.md",
    "chars": 1194,
    "preview": "---\ntitle: \"泛型是双刃剑?Go1.18 编译会慢近 20%\"\ndate: 2021-12-31T12:55:18+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n目前 "
  },
  {
    "path": "content/posts/go/118-constraints.md",
    "chars": 3701,
    "preview": "---\ntitle: \"Go 新语法挺丑?最新的泛型类型约束介绍\"\ndate: 2021-12-31T12:55:19+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n近期我们在分"
  },
  {
    "path": "content/posts/go/118-cut.md",
    "chars": 1864,
    "preview": "---\ntitle: \"Go1.18 新特性:新增好用的 Cut 方法\"\ndate: 2022-02-05T16:03:31+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n在各种"
  },
  {
    "path": "content/posts/go/118-leader-generics.md",
    "chars": 1867,
    "preview": "---\ntitle: \"回归现实:Go Leader 对 1.18 泛型的期望\"\ndate: 2021-12-31T12:55:16+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n"
  },
  {
    "path": "content/posts/go/118-module.md",
    "chars": 3029,
    "preview": "---\ntitle: \"Go1.18 新特性:多 Module 工作区模式\"\ndate: 2022-02-05T16:00:00+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - go1.18\n---\n\n大"
  },
  {
    "path": "content/posts/go/4errors.md",
    "chars": 4710,
    "preview": "---\ntitle: \"大家对 Go 错误处理的 4 个误解!\"\ndate: 2021-12-31T12:55:04+08:00\ndraft: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\nGo 语言"
  },
  {
    "path": "content/posts/go/again-mutex.md",
    "chars": 1724,
    "preview": "---\ntitle: \"Go 为什么不支持可重入锁?\"\ndate: 2021-12-31T12:55:24+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n程序里的锁,是很多小伙伴"
  },
  {
    "path": "content/posts/go/annotation.md",
    "chars": 4338,
    "preview": "---\ntitle: \"Go:我有注解,Java:不,你没有!\"\ndate: 2021-12-31T12:55:11+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - 为什么\n---\n\n大家好,我是煎鱼。\n"
  },
  {
    "path": "content/posts/go/any.md",
    "chars": 1630,
    "preview": "---\ntitle: \"Go 新关键字 any,interface 会成历史吗?\"\ndate: 2021-12-31T12:55:21+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。"
  },
  {
    "path": "content/posts/go/class-extends.md",
    "chars": 3486,
    "preview": "---\ntitle: \"Go 为什么不支持类和继承?\"\ndate: 2021-12-31T12:55:22+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n大家在早期学习 Go 时"
  },
  {
    "path": "content/posts/go/crawler/2018-03-21-douban-top250.md",
    "chars": 1991,
    "preview": "---\n\ntitle:      \"爬取豆瓣电影 Top250\"\ndate:       2018-03-21 12:30:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - 数据分析\n--"
  },
  {
    "path": "content/posts/go/crawler/2018-04-01-cars.md",
    "chars": 2859,
    "preview": "---\n\ntitle:      \"爬取汽车之家 二手车产品库\"\ndate:       2018-04-01 12:30:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - 数据分析\n--"
  },
  {
    "path": "content/posts/go/crawler/2018-04-28-go2018.md",
    "chars": 5890,
    "preview": "---\n\ntitle:      \"了解一下Golang的市场行情\"\ndate:       2018-04-28 12:30:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - 数据分析\n"
  },
  {
    "path": "content/posts/go/defer/2019-05-27-defer.md",
    "chars": 7802,
    "preview": "---\n\ntitle:      \"深入理解 Go defer\"\ndate:       2019-05-27 12:30:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - 源码分析\n--"
  },
  {
    "path": "content/posts/go/delve.md",
    "chars": 6623,
    "preview": "---\ntitle: \"一个 Demo 学会使用 Go Delve 调试\"\ndate: 2021-12-31T12:54:57+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n在 "
  },
  {
    "path": "content/posts/go/empty-struct.md",
    "chars": 4387,
    "preview": "---\ntitle: \"详解 Go 空结构体的 3 种使用场景\"\ndate: 2021-12-31T12:54:51+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n在大家初识 G"
  },
  {
    "path": "content/posts/go/enum.md",
    "chars": 2741,
    "preview": "---\ntitle: \"小技巧分享:在 Go 如何实现枚举?\"\ndate: 2021-12-31T12:54:50+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n在日常的业务工程"
  },
  {
    "path": "content/posts/go/func-reload.md",
    "chars": 2476,
    "preview": "---\ntitle: \"Go 为什么不支持函数重载和参数默认值?\"\ndate: 2021-12-31T12:55:16+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - 为什么\n---\n\n大家好,我是煎鱼。"
  },
  {
    "path": "content/posts/go/fuzzing.md",
    "chars": 2663,
    "preview": "---\ntitle: \"提高 Go 程序健壮性,Fuzzing 要来了!\"\ndate: 2021-12-31T12:54:50+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n就在"
  },
  {
    "path": "content/posts/go/gdb.md",
    "chars": 4512,
    "preview": "---\ntitle: \"学会使用 GDB 调试 Go 代码\"\ndate: 2021-12-31T12:54:57+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n上一篇文章《一个 "
  },
  {
    "path": "content/posts/go/generics-apis.md",
    "chars": 2037,
    "preview": "---\ntitle: \"出泛型后 API 怎么办?Go 开发者要注意了\"\ndate: 2021-12-31T12:55:14+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n前段时"
  },
  {
    "path": "content/posts/go/generics-design.md",
    "chars": 5454,
    "preview": "---\ntitle: \"Go 泛型的 3 个核心设计,你学会了吗?\"\ndate: 2022-02-05T15:52:46+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\nGo1.1"
  },
  {
    "path": "content/posts/go/generics-proposal.md",
    "chars": 1090,
    "preview": "---\ntitle: \"快报:正式提案将泛型特性加入 Go 语言\"\ndate: 2021-01-13T21:11:44+08:00\ntoc: true\nimages:\ntags: \n  - 泛型\n---\n\n经历九九八十一难,多年的不断探讨和"
  },
  {
    "path": "content/posts/go/gin/2018-02-10-install.md",
    "chars": 9170,
    "preview": "---\n\ntitle:      \"「连载一」Go 介绍与环境安装\"\ndate:       2018-02-10 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n-"
  },
  {
    "path": "content/posts/go/gin/2018-02-11-api-01.md",
    "chars": 14159,
    "preview": "---\n\ntitle:      \"「连载二」Gin搭建Blog API's (一)\"\ndate:       2018-02-11 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n  "
  },
  {
    "path": "content/posts/go/gin/2018-02-12-api-02.md",
    "chars": 10693,
    "preview": "---\n\ntitle:      \"「连载三」Gin搭建Blog API's (二)\"\ndate:       2018-02-12 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n  "
  },
  {
    "path": "content/posts/go/gin/2018-02-13-api-03.md",
    "chars": 14829,
    "preview": "---\n\ntitle:      \"「连载四」Gin搭建Blog API's (三)\"\ndate:       2018-02-13 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n  "
  },
  {
    "path": "content/posts/go/gin/2018-02-14-jwt.md",
    "chars": 8340,
    "preview": "---\n\ntitle:      \"「连载五」使用 JWT 进行身份校验\"\ndate:       2018-02-14 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gi"
  },
  {
    "path": "content/posts/go/gin/2018-02-15-log.md",
    "chars": 6832,
    "preview": "---\n\ntitle:      \"「连载六」编写一个简单的文件日志\"\ndate:       2018-02-15 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n"
  },
  {
    "path": "content/posts/go/gin/2018-03-15-reload-http.md",
    "chars": 7577,
    "preview": "---\n\ntitle:      \"「连载七」优雅的重启服务\"\ndate:       2018-03-15 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n---\n"
  },
  {
    "path": "content/posts/go/gin/2018-03-18-swagger.md",
    "chars": 3279,
    "preview": "---\n\ntitle:      \"「连载八」为它加上Swagger\"\ndate:       2018-03-18 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n"
  },
  {
    "path": "content/posts/go/gin/2018-03-24-golang-docker.md",
    "chars": 10009,
    "preview": "---\n\ntitle:      \"「连载九」将Golang应用部署到Docker\"\ndate:       2018-03-24 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n   "
  },
  {
    "path": "content/posts/go/gin/2018-03-26-cgo.md",
    "chars": 4710,
    "preview": "---\n\ntitle:      \"「番外」Golang 交叉编译\"\ndate:       2018-03-26 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n-"
  },
  {
    "path": "content/posts/go/gin/2018-04-15-gorm-callback.md",
    "chars": 6865,
    "preview": "---\n\ntitle:      \"「连载十」定制 GORM Callbacks\"\ndate:       2018-04-15 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    "
  },
  {
    "path": "content/posts/go/gin/2018-04-29-cron.md",
    "chars": 5903,
    "preview": "---\n\ntitle:      \"「连载十一」Cron定时任务\"\ndate:       2018-04-29 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n--"
  },
  {
    "path": "content/posts/go/gin/2018-05-27-config-upload.md",
    "chars": 17374,
    "preview": "---\n\ntitle:      \"「连载十二」优化配置结构及实现图片上传\"\ndate:       2018-05-27 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - g"
  },
  {
    "path": "content/posts/go/gin/2018-06-02-application-redis.md",
    "chars": 8814,
    "preview": "---\n\ntitle:      \"「连载十三」优化你的应用结构和实现Redis缓存\"\ndate:       2018-06-02 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n  "
  },
  {
    "path": "content/posts/go/gin/2018-06-14-excel.md",
    "chars": 6389,
    "preview": "---\n\ntitle:      \"「连载十四」实现导出、导入 Excel\"\ndate:       2018-06-14 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - g"
  },
  {
    "path": "content/posts/go/gin/2018-07-05-image.md",
    "chars": 9116,
    "preview": "---\n\ntitle:      \"「连载十五」生成二维码、合并海报\"\ndate:       2018-07-05 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n"
  },
  {
    "path": "content/posts/go/gin/2018-07-07-font.md",
    "chars": 3992,
    "preview": "---\n\ntitle:      \"「连载十六」在图片上绘制文字\"\ndate:       2018-07-07 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n--"
  },
  {
    "path": "content/posts/go/gin/2018-08-26-makefile.md",
    "chars": 2685,
    "preview": "---\n\ntitle:      \"「番外」请入门 Makefile\"\ndate:       2018-08-26 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gin\n"
  },
  {
    "path": "content/posts/go/gin/2018-09-01-nginx.md",
    "chars": 5271,
    "preview": "---\n\ntitle:      \"「连载十七」用Nginx部署Go应用\"\ndate:       2018-09-01 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - gi"
  },
  {
    "path": "content/posts/go/gmp-why-p.md",
    "chars": 4941,
    "preview": "---\ntitle: \"经典面试题:你觉得 Go 在什么时候会抢占 P?\"\ndate: 2021-06-24T12:42:05+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好,我是煎鱼。\n\n前几天我"
  },
  {
    "path": "content/posts/go/go-array-slice.md",
    "chars": 5021,
    "preview": "---\ntitle: \"Go 数组比切片好在哪?\"\ndate: 2021-09-17T12:43:08+08:00\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n前段时间有播放一条快讯,就是 Go1.17 会正"
  },
  {
    "path": "content/posts/go/go-bootstrap.md",
    "chars": 3560,
    "preview": "---\ntitle: \"Go 应用程序是怎么运行起来的?\"\ndate: 2020-10-08T15:57:18+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n自古应用程序均从 Hello World "
  },
  {
    "path": "content/posts/go/go-bootstrap0.md",
    "chars": 6429,
    "preview": "---\ntitle: \"详解 Go 程序的启动流程,你知道 g0,m0 是什么吗?\"\ndate: 2021-06-17T12:42:42+08:00\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n自古应用程序均"
  },
  {
    "path": "content/posts/go/go-concurrent-lock.md",
    "chars": 3702,
    "preview": "---\ntitle: \"Go 并发:一些有趣的现象和要避开的 “坑”\"\ndate: 2020-12-10T00:25:59+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n最近在看"
  },
  {
    "path": "content/posts/go/go-design.md",
    "chars": 6895,
    "preview": "---\ntitle: \"上帝视角:Go 语言设计失误,缺乏远见?\"\ndate: 2021-12-31T12:55:13+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n前段时间我有"
  },
  {
    "path": "content/posts/go/go-empty-struct.md",
    "chars": 4081,
    "preview": "---\ntitle: \"用 Go struct 不能犯的一个低级错误!\"\ndate: 2021-06-17T12:44:27+08:00\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n前段时间我分享了 《手撕 "
  },
  {
    "path": "content/posts/go/go-error2panic.md",
    "chars": 2270,
    "preview": "---\ntitle: \"Go 错误处理:用 panic 取代 err != nil 的模式\"\ndate: 2020-12-12T17:21:42+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n前段时间"
  },
  {
    "path": "content/posts/go/go-errors-boom.md",
    "chars": 2000,
    "preview": "---\ntitle: \"生产环境遇到一个 Go 问题,整组人都懵逼了...\"\ndate: 2021-07-07T12:43:49+08:00\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n前段时间正在疯狂写代码"
  },
  {
    "path": "content/posts/go/go-golang.md",
    "chars": 2577,
    "preview": "---\ntitle: \"Go 和 Golang 有什么关系?\"\ndate: 2021-12-31T12:55:12+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n最近天气也冷了,"
  },
  {
    "path": "content/posts/go/go-map-access.md",
    "chars": 4722,
    "preview": "---\ntitle: \"用 Go map 要注意这个细节,避免依赖他!\"\ndate: 2021-09-12T17:47:29+08:00\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n最近又有同学问我这个日经话"
  },
  {
    "path": "content/posts/go/go-moduels/2019-09-29-goproxy-cn.md",
    "chars": 14342,
    "preview": "---\n\ntitle:      \"干货满满的 Go Modules 和 goproxy.cn\"\ndate:       2019-09-29 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - "
  },
  {
    "path": "content/posts/go/go-moduels/2020-02-28-go-modules.md",
    "chars": 16251,
    "preview": "---\ntitle:      \"Go Modules 终极入门\"\ndate:       2020-02-28 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - go-mod"
  },
  {
    "path": "content/posts/go/go-standards.md",
    "chars": 3837,
    "preview": "---\ntitle: \"上帝视角看 “Go 项目标准布局” 之争\"\ndate: 2021-09-13T23:34:23+08:00\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n前段时间 Go 语言社区有一件事"
  },
  {
    "path": "content/posts/go/go-tips-defer.md",
    "chars": 1709,
    "preview": "---\ntitle: \"Go 群友提问:学习 defer 时很懵逼,这道不会做!\"\ndate: 2021-04-05T16:10:51+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好,我是煎鱼。\n\n"
  },
  {
    "path": "content/posts/go/go-tips-gmp-p.md",
    "chars": 3480,
    "preview": "---\ntitle: \"Go 面试官:GMP 模型,为什么要有 P?\"\ndate: 2021-04-05T16:15:20+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好,我是煎鱼。\n\n最近金三银四"
  },
  {
    "path": "content/posts/go/go-tips-goroutineid.md",
    "chars": 3900,
    "preview": "---\ntitle: \"Go 群友提问:进程、线程都有 ID,为什么 Goroutine 没有 ID?\"\ndate: 2021-04-05T16:14:14+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n"
  },
  {
    "path": "content/posts/go/go-tips-goroutineloop.md",
    "chars": 3371,
    "preview": "---\ntitle: \"Go 面试官:单核 CPU,开两个 Goroutine,其中一个死循环,会怎么样?\"\ndate: 2021-04-05T16:17:23+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---"
  },
  {
    "path": "content/posts/go/go-tips-goroutinenums.md",
    "chars": 4044,
    "preview": "---\ntitle: \"Go 群友提问:Goroutine 数量控制在多少合适,会影响 GC 和调度?\"\ndate: 2021-04-05T16:08:18+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n"
  },
  {
    "path": "content/posts/go/go-tips-interface.md",
    "chars": 2285,
    "preview": "---\ntitle: \"Go 面试官:Go interface 的一个 “坑” 及原理分析\"\ndate: 2021-04-05T16:12:59+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好,我是"
  },
  {
    "path": "content/posts/go/go-tips-lenstr.md",
    "chars": 2112,
    "preview": "---\ntitle: \"问个 Go 问题,字符串 len 为 0 和 字符串为空 ,有啥区别?\"\ndate: 2021-04-05T16:09:14+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好,"
  },
  {
    "path": "content/posts/go/go-tips-sturct.md",
    "chars": 4223,
    "preview": "---\ntitle: \"Go 面试官:Go 结构体是否可以比较,为什么?\"\ndate: 2021-04-05T16:16:00+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好,我是煎鱼。\n\n最近金三"
  },
  {
    "path": "content/posts/go/go-tips-timer-memory.md",
    "chars": 3542,
    "preview": "---\ntitle: \"Go 内存泄露之痛,这篇把 Go timer.After 问题根因讲透了!\"\ndate: 2021-04-05T16:16:47+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家"
  },
  {
    "path": "content/posts/go/go-typeparams-master.md",
    "chars": 3248,
    "preview": "---\ntitle: \"令人激动!Go 泛型代码合入 master(附尝鲜方法)\"\ndate: 2021-04-05T16:06:50+08:00\nimages:\ntags: \n  - go\n---\n\n大家好,我是慢一拍的后方记者煎鱼。\n\n"
  },
  {
    "path": "content/posts/go/go-why-path.md",
    "chars": 2024,
    "preview": "---\ntitle: \"意见征集:Go1 要不要移除 GOPATH?\"\ndate: 2021-04-05T16:02:34+08:00\nimages:\ntags: \n  - go\n---\n\n大家好,我是在打自己脸的后方记者煎鱼。\n\n前几天我"
  },
  {
    "path": "content/posts/go/go1.16-1.md",
    "chars": 3913,
    "preview": "---\ntitle: \"Go1.16 即将正式发布,以下变更你需要知道\"\ndate: 2021-02-11T16:13:15+08:00\nimages:\ntags: \n  - go\n---\n\n大家好,我是正在努力学习的煎鱼。\n\n在前几天,G"
  },
  {
    "path": "content/posts/go/go1.16-2.md",
    "chars": 2751,
    "preview": "---\ntitle: \"Go1.16 新特性:快速上手 Go embed\"\ndate: 2021-02-11T16:13:19+08:00\nimages:\ntags: \n  - go\n---\n\n在以前,很多从其他语言转过来 Go 语言的同学"
  },
  {
    "path": "content/posts/go/go1.16-3.md",
    "chars": 2998,
    "preview": "---\ntitle: \"Go1.16 新特性:详解内存管理机制的变更,你需要了解\"\ndate: 2021-02-11T16:13:20+08:00\nimages:\ntags: \n  - go\n---\n\n\n大家好,我是正在学习如何蒸鱼的煎鱼。"
  },
  {
    "path": "content/posts/go/go1.16-mod.md",
    "chars": 2740,
    "preview": "---\ntitle: \"Go1.16 新特性:Go mod 的后悔药,仅需这一招\"\ndate: 2021-04-05T16:00:13+08:00\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n\n前几天 Go "
  },
  {
    "path": "content/posts/go/go11.md",
    "chars": 3558,
    "preview": "---\ntitle: \"Go 语言今年 11 岁,何去何从,现状到底如何?\"\ndate: 2020-11-11T21:21:58+08:00\ntoc: false\nimages:\ntags: \n  - go\n---\n\n不说不知道,一说下一跳"
  },
  {
    "path": "content/posts/go/go16-preview.md",
    "chars": 5902,
    "preview": "---\ntitle: \"为什么 Go 的泛型一拖再拖?\"\ndate: 2020-11-12T23:47:16+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。前段时间 Go 语言的泛型"
  },
  {
    "path": "content/posts/go/go2-errors.md",
    "chars": 8018,
    "preview": "---\ntitle: \"先睹为快,Go2 Error 的挣扎之路\"\ndate: 2020-12-03T20:56:47+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n自从 Go "
  },
  {
    "path": "content/posts/go/gophercon2020-errors.md",
    "chars": 1846,
    "preview": "---\ntitle: \"重磅:Go errors 将不会有任何进一步的改进计划\"\ndate: 2020-11-14T16:48:33+08:00\nimages:\ntags: \n  - go\n---\n\n今天在 Gophercon2020 上,"
  },
  {
    "path": "content/posts/go/goroutine-27.md",
    "chars": 3968,
    "preview": "---\ntitle: \"会诱发 Goroutine 挂起的 27 个原因\"\ndate: 2021-12-31T12:55:06+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n上个"
  },
  {
    "path": "content/posts/go/goroutine-errors.md",
    "chars": 3766,
    "preview": "---\ntitle: \"多 Goroutine 如何优雅处理错误?\"\ndate: 2021-12-31T12:54:50+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n在 Go "
  },
  {
    "path": "content/posts/go/goroutine-leak.md",
    "chars": 5582,
    "preview": "---\ntitle: \"跟面试官聊 Goroutine 泄露的 6 种方法,真刺激!\"\ndate: 2021-06-11T12:54:49+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好,我是煎鱼。"
  },
  {
    "path": "content/posts/go/grpc/2018-09-22-install.md",
    "chars": 5511,
    "preview": "---\n\ntitle:      \"「连载一」gRPC及相关介绍\"\ndate:       2018-09-22 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - grpc\n-"
  },
  {
    "path": "content/posts/go/grpc/2018-09-23-client-and-server.md",
    "chars": 6376,
    "preview": "---\n\ntitle:      \"「连载二」gRPC Client and Server\"\ndate:       2018-09-23 12:30:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go"
  },
  {
    "path": "content/posts/go/grpc/2018-09-24-stream-client-server.md",
    "chars": 11181,
    "preview": "---\n\ntitle:      \"「连载三」gRPC Streaming, Client and Server\"\ndate:       2018-09-24 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntag"
  },
  {
    "path": "content/posts/go/grpc/2018-10-07-grpc-tls.md",
    "chars": 6064,
    "preview": "---\n\ntitle:      \"「连载四」TLS 证书认证\"\ndate:       2018-10-07 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - grpc\n--"
  },
  {
    "path": "content/posts/go/grpc/2018-10-08-ca-tls.md",
    "chars": 5846,
    "preview": "---\n\ntitle:      \"「连载五」基于 CA 的 TLS 证书认证\"\ndate:       2018-10-08 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    -"
  },
  {
    "path": "content/posts/go/grpc/2018-10-10-interceptor.md",
    "chars": 4337,
    "preview": "---\n\ntitle:      \"「连载六」Unary and Stream interceptor\"\ndate:       2018-10-10 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n  "
  },
  {
    "path": "content/posts/go/grpc/2018-10-12-grpc-http.md",
    "chars": 4195,
    "preview": "---\n\ntitle:      \"「连载七」让你的服务同时提供 HTTP 接口\"\ndate:       2018-10-12 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    "
  },
  {
    "path": "content/posts/go/grpc/2018-10-14-per-rpc-credentials.md",
    "chars": 4241,
    "preview": "---\n\ntitle:      \"「连载八」对 RPC 方法做自定义认证\"\ndate:       2018-10-14 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - g"
  },
  {
    "path": "content/posts/go/grpc/2018-10-16-deadlines.md",
    "chars": 3082,
    "preview": "---\n\ntitle:      \"「连载九」gRPC Deadlines\"\ndate:       2018-10-16 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - g"
  },
  {
    "path": "content/posts/go/grpc/2018-10-20-zipkin.md",
    "chars": 5453,
    "preview": "---\n\ntitle:      \"「连载十」分布式链路追踪 gRPC + Opentracing + Zipkin\"\ndate:       2018-10-20 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\nt"
  },
  {
    "path": "content/posts/go/grpc-gateway/2018-02-23-install.md",
    "chars": 5971,
    "preview": "---\n\ntitle:      \"「连载一」gRPC介绍与环境安装\"\ndate:       2018-02-23 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - grpc"
  },
  {
    "path": "content/posts/go/grpc-gateway/2018-02-27-hello-world.md",
    "chars": 22723,
    "preview": "---\n\ntitle:      \"「连载二」Hello World\"\ndate:       2018-02-27 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - grpc"
  },
  {
    "path": "content/posts/go/grpc-gateway/2018-03-04-swagger.md",
    "chars": 8481,
    "preview": "---\n\ntitle:      \"「连载三」Swagger了解一下\"\ndate:       2018-03-04 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - grpc"
  },
  {
    "path": "content/posts/go/grpc-gateway/2019-06-22-grpc-gateway-tls.md",
    "chars": 3813,
    "preview": "---\n\ntitle:      \"「连载四」gRPC+gRPC Gateway 能不能不用证书?\"\ndate:       2019-06-22 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    "
  },
  {
    "path": "content/posts/go/import-cyc.md",
    "chars": 1906,
    "preview": "---\ntitle: \"为什么 Go 不支持循环引用?\"\ndate: 2021-12-31T12:55:15+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - 为什么\n---\n\n大家好,我是煎鱼。\n\n近年来"
  },
  {
    "path": "content/posts/go/import-generics.md",
    "chars": 2331,
    "preview": "---\ntitle: \"长达 12 年,Go 才引入泛型,是政治,还是技术问题?\"\ndate: 2021-12-31T12:55:25+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。"
  },
  {
    "path": "content/posts/go/len.md",
    "chars": 4194,
    "preview": "---\ntitle: \"迷惑了,Go len() 是怎么计算出来的?\"\ndate: 2021-12-31T12:54:58+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n最近看到"
  },
  {
    "path": "content/posts/go/map/2019-03-05-map-access.md",
    "chars": 7150,
    "preview": "---\n\ntitle:      \"深入理解 Go map:初始化和访问元素\"\ndate:       2019-03-05 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - "
  },
  {
    "path": "content/posts/go/map/2019-03-24-map-assign.md",
    "chars": 13025,
    "preview": "---\n\ntitle:      \"深入理解 Go map:赋值和扩容迁移\"\ndate:       2019-03-24 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - 源"
  },
  {
    "path": "content/posts/go/map/2019-04-07-why-map-no-order.md",
    "chars": 4710,
    "preview": "---\n\ntitle:      \"为什么遍历 Go map 是无序的\"\ndate:       2019-04-07 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - 源码分"
  },
  {
    "path": "content/posts/go/map-65.md",
    "chars": 3503,
    "preview": "---\ntitle: \"面试官:为什么 Go 的负载因子是 6.5?\"\ndate: 2021-12-31T12:55:07+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好,我是煎"
  },
  {
    "path": "content/posts/go/map-con.md",
    "chars": 1330,
    "preview": "---\ntitle: \"Go 为什么不在语言层面支持 map 并发?\"\ndate: 2022-02-05T15:55:22+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n很多小伙"
  },
  {
    "path": "content/posts/go/map-reset.md",
    "chars": 2843,
    "preview": "---\ntitle: \"Go map 如何缩容?\"\ndate: 2021-12-31T12:55:07+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好,我是煎鱼。\n\n前几天看到 "
  },
  {
    "path": "content/posts/go/map-slice-concurrency.md",
    "chars": 5430,
    "preview": "---\ntitle: \"为什么 Go map 和 slice 是非线程安全的?\"\ndate: 2021-12-31T12:54:49+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家"
  },
  {
    "path": "content/posts/go/memory-model.md",
    "chars": 4745,
    "preview": "---\ntitle: \"Go 内存模型:happens-before 原则\"\ndate: 2021-12-31T12:54:54+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n在"
  },
  {
    "path": "content/posts/go/news-slices-maps.md",
    "chars": 3246,
    "preview": "---\ntitle: \"Go 提案:增加泛型版 slices 和 maps 新包\"\ndate: 2021-12-31T12:54:57+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。"
  },
  {
    "path": "content/posts/go/news115.md",
    "chars": 3739,
    "preview": "---\ntitle: \"分享 Go 最近的几件周边大小事\"\ndate: 2021-12-31T12:55:17+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n最近可能是因为 Q4"
  },
  {
    "path": "content/posts/go/nil-func.md",
    "chars": 1332,
    "preview": "---\ntitle: \"Go 读者提问:值为 nil 也能调用函数,太神奇了吧?\"\ndate: 2021-12-31T12:55:27+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。"
  },
  {
    "path": "content/posts/go/panic/2019-05-21-panic-and-recover.md",
    "chars": 9915,
    "preview": "---\n\ntitle:      \"深入理解 Go panic and recover\"\ndate:       2019-05-21 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n "
  },
  {
    "path": "content/posts/go/pkg/2018-09-28-log.md",
    "chars": 5240,
    "preview": "---\n\ntitle:      \"log 标准库\"\ndate:       2018-09-28 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - 源码分析\n---\n\n## "
  },
  {
    "path": "content/posts/go/pkg/2018-12-04-fmt.md",
    "chars": 9914,
    "preview": "---\n\ntitle:      \"fmt 标准库 --- Print* 是怎么样输出的?\"\ndate:       2018-12-04 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go"
  },
  {
    "path": "content/posts/go/pkg/2018-12-15-unsafe.md",
    "chars": 3063,
    "preview": "---\n\ntitle:      \"有点不安全却又一亮的 Go unsafe.Pointer\"\ndate:       2018-12-15 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - g"
  },
  {
    "path": "content/posts/go/plugin.md",
    "chars": 3773,
    "preview": "---\ntitle: \"Go 插件系统,一个凉了快半截的特性?\"\ndate: 2021-12-31T12:54:59+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n在 Go 语言"
  },
  {
    "path": "content/posts/go/real-context.md",
    "chars": 5124,
    "preview": "---\ntitle: \"分享 Go 使用 Context 的正式姿势\"\ndate: 2021-12-31T12:54:54+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n在 Go"
  },
  {
    "path": "content/posts/go/reflect.md",
    "chars": 8201,
    "preview": "---\ntitle: \"解密 Go 语言之反射 reflect\"\ndate: 2020-11-07T15:01:51+08:00\ndraft: false\ntoc: true\nimages:\ntags: \n  - go\n  - 深度解密\n-"
  },
  {
    "path": "content/posts/go/runtimepark.md",
    "chars": 2432,
    "preview": "---\ntitle: \"Goroutine 一泄露就看到他,这是个什么?\"\ndate: 2021-12-31T12:55:01+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n作为"
  },
  {
    "path": "content/posts/go/rust-php.md",
    "chars": 1818,
    "preview": "---\ntitle: \"Rust 内讧,PHP 主力淡出?Go:好好放假\"\ndate: 2021-12-31T12:55:20+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n现在"
  },
  {
    "path": "content/posts/go/site-history.md",
    "chars": 1280,
    "preview": "---\ntitle: \"分久必合,golang.org 将成为历史!\"\ndate: 2021-12-31T12:55:03+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n这两天看"
  },
  {
    "path": "content/posts/go/slice/2018-12-11-slice.md",
    "chars": 8973,
    "preview": "---\n\ntitle:      \"深入理解 Go Slice\"\ndate:       2018-12-11 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - 源码分析\n--"
  },
  {
    "path": "content/posts/go/slice/2019-01-06-why-slice-max.md",
    "chars": 4249,
    "preview": "---\n\ntitle:      \"Go Slice 最大容量大小是怎么来的\"\ndate:       2019-01-06 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n    - "
  },
  {
    "path": "content/posts/go/slice-discuss.md",
    "chars": 3312,
    "preview": "---\ntitle: \"Go 切片这道题,吵了一个下午!\"\ndate: 2021-12-31T12:55:06+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n前几天听到咱 Go "
  },
  {
    "path": "content/posts/go/slice-leak.md",
    "chars": 2703,
    "preview": "---\ntitle: \"Go 切片导致内存泄露,被坑两次了!\"\ndate: 2021-12-31T12:55:09+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n前段时间在我的 "
  },
  {
    "path": "content/posts/go/slice-string-header.md",
    "chars": 4902,
    "preview": "---\ntitle: \"Go SliceHeader 和 StringHeader,你知道吗?\"\ndate: 2021-12-31T12:54:53+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家"
  },
  {
    "path": "content/posts/go/stop-goroutine.md",
    "chars": 3736,
    "preview": "---\ntitle: \"回答我,停止 Goroutine 有几种方法?\"\ndate: 2021-12-31T12:54:50+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n协程("
  },
  {
    "path": "content/posts/go/struct-pointer.md",
    "chars": 2399,
    "preview": "---\ntitle: \"你知道 Go 结构体和结构体指针调用有什么区别吗?\"\ndate: 2021-06-06T12:21:30+08:00\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好,我是煎鱼。\n\n前几天"
  },
  {
    "path": "content/posts/go/switch-type.md",
    "chars": 2702,
    "preview": "---\ntitle: \"Go 泛型玩出花,详解新提案 switch type!\"\ndate: 2021-12-31T12:55:23+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n"
  },
  {
    "path": "content/posts/go/sync-map.md",
    "chars": 8710,
    "preview": "---\ntitle: \"Go 并发读写 sync.map 的强大之处\"\ndate: 2021-12-31T12:54:50+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好,我是煎"
  },
  {
    "path": "content/posts/go/talk/2018-03-13-golang-relatively-path.md",
    "chars": 3915,
    "preview": "---\n\ntitle:      \"聊一聊,Go 的相对路径问题\"\ndate:       2018-03-13 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n## 前言\n\n"
  },
  {
    "path": "content/posts/go/talk/2018-05-21-go-fake-useragent.md",
    "chars": 3851,
    "preview": "---\n\ntitle:      \"Go 的 fake-useragent 了解一下\"\ndate:       2018-05-21 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n--"
  },
  {
    "path": "content/posts/go/talk/2018-06-07-go-redis-protocol.md",
    "chars": 4382,
    "preview": "---\n\ntitle:      \"用 Go 来了解一下 Redis 通讯协议\"\ndate:       2018-06-07 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n"
  },
  {
    "path": "content/posts/go/talk/2018-11-25-gomock.md",
    "chars": 5897,
    "preview": "---\n\ntitle:      \"使用 Gomock 进行单元测试\"\ndate:       2018-11-25 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n在实际项目"
  },
  {
    "path": "content/posts/go/talk/2018-12-26-go-memory-align.md",
    "chars": 6967,
    "preview": "---\n\ntitle:      \"在 Go 中恰到好处的内存对齐\"\ndate:       2018-12-26 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n![imag"
  },
  {
    "path": "content/posts/go/talk/2019-01-20-control-goroutine.md",
    "chars": 6711,
    "preview": "---\n\ntitle:      \"来,控制一下 goroutine 的并发数量\"\ndate:       2019-01-20 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n"
  },
  {
    "path": "content/posts/go/talk/2019-02-17-for-loop-json-unmarshal.md",
    "chars": 7151,
    "preview": "---\n\ntitle:      \"for-loop 与 json.Unmarshal 性能分析概要\"\ndate:       2019-02-17 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n   "
  },
  {
    "path": "content/posts/go/talk/2019-03-31-go-ins.md",
    "chars": 3416,
    "preview": "---\n\ntitle:      \"简单围观一下有趣的 //go: 指令\"\ndate:       2019-03-31 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n![i"
  },
  {
    "path": "content/posts/go/talk/2019-05-20-stack-heap.md",
    "chars": 5807,
    "preview": "---\n\ntitle:      \"我要在栈上。不,你应该在堆上\"\ndate:       2019-05-20 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n![image"
  },
  {
    "path": "content/posts/go/talk/2019-06-16-defer-loss.md",
    "chars": 3523,
    "preview": "---\n\ntitle:      \"Go1.12 defer 会有性能损耗,尽量不要用?\"\ndate:       2019-06-16 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n"
  },
  {
    "path": "content/posts/go/talk/2019-06-29-talking-grpc.md",
    "chars": 21301,
    "preview": "---\n\ntitle:      \"从实践到原理,带你参透 gRPC\"\ndate:       2019-06-29 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n![ima"
  },
  {
    "path": "content/posts/go/talk/2019-09-07-go1.13-defer.md",
    "chars": 5666,
    "preview": "---\n\ntitle:      \"Go1.13 defer 的性能是如何提高的\"\ndate:       2019-09-07 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n"
  },
  {
    "path": "content/posts/go/talk/2019-09-24-why-vsz-large.md",
    "chars": 14108,
    "preview": "---\n\ntitle:      \"Go 应用内存占用太多,让排查?(VSZ篇)\"\ndate:       2019-09-24 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n"
  },
  {
    "path": "content/posts/go/ternary-operator.md",
    "chars": 2958,
    "preview": "---\ntitle: \"Go 凭什么不支持三元运算符?\"\ndate: 2021-12-31T12:54:50+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - 为什么\n---\n\n大家好,我是煎鱼。\n\n这是一"
  },
  {
    "path": "content/posts/go/throw.md",
    "chars": 3546,
    "preview": "---\ntitle: \"Go 有哪些无法恢复的致命场景?\"\ndate: 2021-12-31T12:55:26+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n有一次事故现场,在紧"
  },
  {
    "path": "content/posts/go/tools/2018-09-15-go-tool-pprof.md",
    "chars": 6386,
    "preview": "---\n\ntitle:      \"Go 大杀器之性能剖析 PProf\"\ndate:       2018-09-15 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n## 前"
  },
  {
    "path": "content/posts/go/tools/2019-07-12-go-tool-trace.md",
    "chars": 5298,
    "preview": "---\n\ntitle:      \"Go 大杀器之跟踪剖析 trace\"\ndate:       2019-07-12 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n![im"
  },
  {
    "path": "content/posts/go/tools/2019-08-19-godebug-sched.md",
    "chars": 10302,
    "preview": "---\n\ntitle:      \"用 GODEBUG 看调度跟踪\"\ndate:       2019-08-19 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n![imag"
  },
  {
    "path": "content/posts/go/tools/2019-09-02-godebug-gc.md",
    "chars": 4739,
    "preview": "---\n\ntitle:      \"用 GODEBUG 看 GC\"\ndate:       2019-09-02 12:00:00\nauthor:     \"煎鱼\"\ntoc: true\ntags:\n    - go\n---\n\n![image"
  },
  {
    "path": "content/posts/go/type-after.md",
    "chars": 2720,
    "preview": "---\ntitle: \"为什么 Go 语言把类型放在后面?\"\ndate: 2021-12-31T12:55:10+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n前段时间看到大家在"
  },
  {
    "path": "content/posts/go/unsafe-pointer.md",
    "chars": 3385,
    "preview": "---\ntitle: \"详解 Go 团队不建议用的 unsafe.Pointer\"\ndate: 2021-12-31T12:54:54+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。"
  },
  {
    "path": "content/posts/go/value-quote.md",
    "chars": 5274,
    "preview": "---\ntitle: \"群里又吵起来了,Go 是传值还是传引用?\"\ndate: 2021-12-31T12:54:52+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。\n\n前几天在咱们"
  },
  {
    "path": "content/posts/go/var.md",
    "chars": 3032,
    "preview": "---\ntitle: \"为什么 Go 有两种声明变量的方式,有什么区别,哪种好?\"\ndate: 2022-02-05T15:56:48+08:00\ntoc: true\nimages:\ntags: \n  - go\n---\n\n大家好,我是煎鱼。"
  },
  {
    "path": "content/posts/go/when-gc.md",
    "chars": 4827,
    "preview": "---\ntitle: \"Go 什么时候会触发 GC?\"\ndate: 2021-12-31T12:55:08+08:00\ntoc: true\nimages:\ntags: \n  - go\n  - 面试题\n---\n\n大家好,我是煎鱼。\n\nGo 语"
  },
  {
    "path": "content/posts/go-meetup1017.md",
    "chars": 2807,
    "preview": "---\ntitle: \"快速了解 2020 Gopher Meetup 深圳站\"\ndate: 2020-10-18T01:03:25+08:00\ntoc: true\nimages:\ntags: \n  - meetup\n---\n\n昨天(202"
  },
  {
    "path": "content/posts/go-programming-tour-book.md",
    "chars": 3027,
    "preview": "---\ntitle: \"新书《Go语言编程之旅:一起用Go做项目》出版啦!\"\ndate: 2020-07-03T21:06:33+08:00\ntoc: true\ntags: \n  - go\n---\n\n从我开始写技术文章起,不知不觉近三年过去"
  },
  {
    "path": "content/posts/kubernetes/2020-05-01-install.md",
    "chars": 5112,
    "preview": "---\ntitle: \"Kubernetes 本地快速启动(基于 Docker)\"\ndate: 2020-05-01T11:25:52+08:00\ntoc: true\ntags: \n  - kubernetes\n---\n\nKubernete"
  },
  {
    "path": "content/posts/kubernetes/2020-05-03-deployment.md",
    "chars": 7013,
    "preview": "---\ntitle: \"在 Kubernetes 中部署应用程序\"\ndate: 2020-05-03T11:05:00+08:00\ntoc: true\ntags: \n  - kubernetes\n---\n\n在完成了本地 Kubernetes"
  },
  {
    "path": "content/posts/kubernetes/2020-05-10-api.md",
    "chars": 4606,
    "preview": "---\ntitle: \"使用 Go 程序调用 Kubernetes API\"\ndate: 2020-05-10T21:20:26+08:00\ntoc: true\ntags: \n  - kubernetes\n---\n\n在前面的章节中,我们介绍"
  },
  {
    "path": "content/posts/microservice/dismantle.md",
    "chars": 4996,
    "preview": "---\ntitle: \"微服务的战争:按什么维度拆分服务\"\ndate: 2020-08-19T20:56:55+08:00\nimages:\ntoc: true\ntags: \n  - 微服务\n---\n\n> “微服务的战争” 是一个关于微服务设"
  },
  {
    "path": "content/posts/microservice/flowcontrol-circuitbreaker.md",
    "chars": 4141,
    "preview": "---\ntitle: \"限流熔断是什么,怎么做,不做行不行?\"\ndate: 2020-10-05T13:24:16+08:00\ntoc: true\nimages:\ntags: \n  - 微服务\n  - 服务治理\n---\n\n“在分布式应用中,"
  },
  {
    "path": "content/posts/microservice/leaky-token-buckets.md",
    "chars": 2760,
    "preview": "---\ntitle: \"带你快速了解:限流中的漏桶和令牌桶算法\"\ndate: 2020-10-06T12:44:10+08:00\ntoc: true\nimages:\ntags: \n  - 微服务\n  - 服务治理\n---\n\n在前文 《限流熔"
  },
  {
    "path": "content/posts/microservice/linkage.md",
    "chars": 1656,
    "preview": "---\ntitle: \"微服务的战争:级联故障和雪崩\"\ndate: 2020-08-25T21:08:39+08:00\ntoc: true\nimages:\ntags: \n  - 微服务\n---\n\n> “微服务的战争” 是一个关于微服务设计思"
  },
  {
    "path": "content/posts/microservice/monitor-alarm.md",
    "chars": 5414,
    "preview": "---\ntitle: \"想要4个9?本文告诉你监控告警如何做\"\ndate: 2020-09-13T18:42:17+08:00\ntoc: true\nimages:\ntags: \n  - 微服务\n  - 服务治理\n---\n\n“你说说,没有仪表"
  },
  {
    "path": "content/posts/microservice/standardization.md",
    "chars": 2837,
    "preview": "---\ntitle: \"微服务的战争:统一且标准化\"\ndate: 2020-08-22T21:56:14+08:00\ntoc: true\nimages:\ntags: \n  - 微服务\n---\n\n> “微服务的战争” 是一个关于微服务设计思考"
  },
  {
    "path": "content/posts/microservice/tests.md",
    "chars": 2010,
    "preview": "---\ntitle: \"微服务的灾难:端到端测试的痛苦\"\ndate: 2020-09-10T19:54:59+08:00\ntoc: true\nimages:\ntags: \n  - 微服务\n---\n\n大家好,我是煎鱼。\n\n小咸鱼经过前文所提到"
  },
  {
    "path": "content/posts/microservice/tracing.md",
    "chars": 3757,
    "preview": "---\ntitle: \"微服务的战争:选型?分布式链路追踪\"\ndate: 2020-09-10T19:53:59+08:00\ntoc: true\nimages:\ntags: \n  - 微服务\n---\n\n\n> “微服务的战争” 是一个关于微服"
  },
  {
    "path": "content/posts/mq-nodus.md",
    "chars": 2584,
    "preview": "---\ntitle: \"《漫谈 MQ》设计 MQ 的 3 个难点\"\ndate: 2021-12-31T12:54:50+08:00\ntoc: true\nimages:\ntags: \n  - mq\n---\n\n\n大家好,我是煎鱼。\n\n前段时间我"
  },
  {
    "path": "content/posts/prometheus/2020-05-16-metrics.md",
    "chars": 9956,
    "preview": "---\ntitle: \"Prometheus 四大度量指标的了解和应用\"\ndate: 2020-05-16T15:08:51+08:00\ntoc: true\ntags: \n  - prometheus\n---\n\n在上一个章节中我们完成了 P"
  },
  {
    "path": "content/posts/prometheus/2020-05-16-pull.md",
    "chars": 2472,
    "preview": "---\ntitle: \"使用 Prometheus 对 Go 程序进行指标采集\"\ndate: 2020-05-17T17:52:37+08:00\n---\n\n在前面的章节中,已经知道了如何对应用程序进行 Prometheus metrics "
  },
  {
    "path": "content/posts/prometheus/2020-05-16-startup.md",
    "chars": 3860,
    "preview": "---\ntitle: \"Prometheus 快速入门\"\ndate: 2020-05-16T12:05:58+08:00\ntoc: true\ntags: \n  - prometheus\n---\n\n一般我们说 Prometheus,有两种理解"
  },
  {
    "path": "content/posts/reading/2020-04-24-book.md",
    "chars": 664,
    "preview": "---\ntitle: \"2020年下半年:读书清单\"\ndate: 2020-04-24T22:00:45+08:00\ntoc: false\nimages:\ntags: \n  - 读书清单\n---\n\n2020 年的上半年因为一些事情耽搁了整体"
  },
  {
    "path": "content/posts/reading/documentary-of-go.md",
    "chars": 1452,
    "preview": "---\ntitle: \"Go: A Documentary 发布!\"\ndate: 2020-09-11T20:46:38+08:00\ntoc: false\nimages:\ntags: \n  - untagged\n---\n\n以前经常有读者问我"
  },
  {
    "path": "content/posts/reading/programmer-accom-base.md",
    "chars": 5952,
    "preview": "---\ntitle: \"必知必会!计算机里一些基本又重要的概念\"\ndate: 2020-10-17T00:25:59+08:00\ntoc: true\nimages:\ntags: \n  - 程序员的自我修养\n---\n\n最近在翻阅文章时,看到全"
  },
  {
    "path": "content/posts/reading/programmer-compile-link.md",
    "chars": 7687,
    "preview": "---\ntitle: \"应用编译,计算机中那些一定要掌握的知识细节\"\ndate: 2020-10-28T20:52:52+08:00\ntoc: true\ntags: \n  - 程序员的自我修养\n---\n\n”Hello World“ 程序几乎"
  },
  {
    "path": "content/posts/reading/programmer-linker.md",
    "chars": 416,
    "preview": "---\ntitle: \"链接器,应用编译的最后一公里路\"\ndate: 2020-11-05T21:15:46+08:00\ntoc: true\ndraft: true\nimages:\ntags: \n  - 程序员的自我修养\n---\n\n在《应用"
  },
  {
    "path": "content/posts/reload-man.md",
    "chars": 2488,
    "preview": "---\ntitle: \"为什么鼓励可以重塑一个职场人?\"\ndate: 2021-12-31T12:54:56+08:00\ntoc: true\nimages:\ntags: \n  - 职场成长\n---\n\n大家好,我是煎鱼。\n\n最近在看书的时候看"
  },
  {
    "path": "content/posts/where-is-proto.md",
    "chars": 2149,
    "preview": "---\ntitle: \"Proto 代码到底放哪里?\"\ndate: 2020-05-23T15:07:37+08:00\ndraft: false\ntoc: true\nimages:\ntags: \n  - protobuf\n---\n\n虽然公司"
  },
  {
    "path": "content/posts/why-container-memory-exceed.md",
    "chars": 5358,
    "preview": "---\ntitle: \"为什么容器内存占用居高不下,频频 OOM\"\ndate: 2020-06-07T14:52:19+08:00\ntoc: true\ntags: \n  - go\n---\n\n\n最近我在回顾思考(写 PPT),整理了现状,发现"
  },
  {
    "path": "content/posts/why-container-memory-exceed2.md",
    "chars": 5017,
    "preview": "---\ntitle: \"为什么容器内存占用居高不下,频频 OOM(续)\"\ndate: 2020-06-19T21:29:08+08:00\ntoc: true\ntags: \n  - go\n---\n\n在上周的文章[《为什么容器内存占用居高不下,"
  },
  {
    "path": "content/posts/why-mq.md",
    "chars": 3360,
    "preview": "---\ntitle: \"《漫谈 MQ》要消息队列(MQ)有什么用?\"\ndate: 2021-12-31T12:54:50+08:00\ntoc: true\nimages:\ntags: \n  - mq\n---\n\n大家好,我是煎鱼。想是问题,做是"
  },
  {
    "path": "content/prometheus-categories.md",
    "chars": 421,
    "preview": "---\ntitle: \"《跟煎鱼学 Prometheus》\"\ndate: \"2020-05-16\"\n---\n\n请注意,这不是成品,随时可能会对以前的章节进行修改。那么为什么要放出来呢,当然是为了催更自己。\n\n1. [Prometheus 快"
  },
  {
    "path": "layouts/_default/baseof.html",
    "chars": 1474,
    "preview": "<!DOCTYPE html>\n<html lang=\"{{.Site.LanguageCode}}\">\n\n<head>\n\t<meta charset=\"UTF-8\">\n\t<meta name=\"viewport\" content=\"wid"
  },
  {
    "path": "layouts/_default/list.html",
    "chars": 793,
    "preview": "{{ define \"header\" }}\n{{ partialCached \"header.html\" . }}\n{{ end }}\n\n{{ define \"main\" }}\n\t<main class=\"site-main section"
  },
  {
    "path": "layouts/_default/single.html",
    "chars": 1219,
    "preview": "{{ define \"head\" }}\n\t{{ if .Params.featuredImg -}}\n\t<style>.bg-img {background-image: url('{{.Params.featuredImg}}');}</"
  },
  {
    "path": "layouts/index.html",
    "chars": 1467,
    "preview": "{{ define \"header\" }}\n{{ partialCached \"header.html\" . }}\n{{ end }}\n\n{{ define \"main\" }}\n\t\t<main class=\"site-main sectio"
  },
  {
    "path": "layouts/partials/analytics.html",
    "chars": 57,
    "preview": "{{ template \"_internal/google_analytics_async.html\" . }}\n"
  }
]

// ... and 60 more files (download for full content)

About this extraction

This page contains the full source code of the EDDYCJY/blog GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 260 files (1.1 MB), approximately 509.2k tokens, and a symbol index with 10 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!