万字长文解读 pkg.go.dev 的设计和实现
https://mp.weixin.qq.com/s/btX53JVCgfOfxDy2ynQa_A
文章较长,建议收藏,抽完整时间阅读。觉得不错帮转发下!
北京时间 2020 年 6 月 15 日 22 点左右,Go 官方发博文(https://blg.golang.org/pkgsite)宣布,pkg.go.dev 开源了。开源代码托管在 Google 自有仓库 https://go.googlesource.com/pkgsite,不过在 GitHub 上提供了镜像:https://github.com/golang/pkgsite。同时,对于该项目的任何 issue,通过 Go 主仓库进行管理,即 https://github.com/golang/go/labels/go.dev。
Go 作者们可能也没有想到,经过这么多年的发展,Go 被使用最广的竟然是 Web 开发,这可能得益于一开始 Go 就对 http 有很好的支持,也因此涌现出大量的 Web 框架,其中知名的有 Gin、Echo、Beego 等。
运营 Go 语言中文网和 Go 社区 8 年有余,发现广大 Gopher 们都苦于没有实战项目可以练手,很多新手学习完 Go 语法后,因为工作中没有用到,不知道该怎么进行项目实战或用 Go 做点什么。这期间也有很多人问我有无适合新手学习的开源项目。也是在去年初,我因此还创建了《Go项目实战》知识星球。现在,Go 官方开源了 pkg.go.dev 这个 Web 项目,爱学习的你,不应该只是看到了这个消息就完事了,应该做点什么,学点什么。
原计划,我是希望通过这个项目,让大家能够很好的学习 Go 是如何进行实际项目开发的。当我深入研究 pkg.go.dev 源码后,我失望了,无论是设计还是实现,水平都很一般。本想着放弃这一系列,但想想还是继续,一方面尝试指出问题,给出认为正确的做法;另一方面,毕竟是官方的项目,开源了相信它会变得更好。
本文包括的内容(并非大纲):

项目架构
先上一张官方的架构图:

包含了三个核心组件:
Frontend:这是一个面向用户的前端组件,处理用户请求,获取数据并展示给用户;
Worker:一个后台程序,负责将新模块的信息写入数据库。新模块的数据来自 Go Module Index[1],同时这些模块的内容是从 Go Module Mirror[2] 中下载的;
Database:数据库,用于存储站点上提供的所有信息。
图中其他的部分包括:Frontend 组件使用的 Redis 缓存、任务队列和 Scheduler。
Frontend 组件
这是一个简单的 HTTP 服务,它从数据库中获取数据,渲染模板,最后生成 HTML 页面。而搜索功能,是通过 Postgres 的全文搜索实现的,因此没有引入另外的搜索组件,比如:Solr、ElasticSearch。
目前前端做的事情比较简单:从 Postgres 数据库中获取 modules 和 packages 等数据,而 Redis 用于缓存这些数据。
细心的读者会发现这样的设计存在一个问题:更新不及时。前端数据依赖 Worker 组件写入 DB。我临时创建一个 Go 包,用于验证该问题:github.com/polaris1119/testpkg,在 godoc.org 上看到的信息如下:

但 pkg.go.dev 上看到的却是:

虽然这个 404 页面告诉你:如果你认为这是一个有效的包路径,可以通过这里的说明[3]尝试获取该包。那这个说明是什么?
因为 pkg.go.dev 的数据是从 proxy.golang.org 下载的。我们会定期监控 Go Module Index[4],以查找要添加到 pkg.go.dev 的新包。如果在 pkg.go.dev 上没有看到你要的包,则可以通过以下任一操作将其添加:
向 proxy.golang.org 请求模块版本,可以请求模块代理协议(Module Proxy Protocol)[5]指定的任何端点,例如:https://proxy.golang.org/github.com/polaris1119/testpkg/@v/list;
通过 go 命令下载相应的包。例如:GOPROXY=https://proxy.golang.org GO111MODULE=on go get github.com/polaris1119/testpkg
可见,这个说明是告诉你怎么将包提交给 proxy.golang.org。即使这样做了,很可能 pkg.go.dev 上还是没有,因为 Frontend 只负责从 DB 获取数据。(几分钟后不出意外应该有了)

然而这样的设计大家肯定接受不了,因此出现了几个这样的 issue:#36811[6], #37002[7], #37106[8] 等。为了应对这种情况,所以出现了架构图中的 Frontend Task Queue,用于获取数据库中还不存在的包并将包 master 分支的信息显示给用户,不过截止目前还未实现。不知道到时候以及会如何实现。
这里不得不吐槽了:
这样的问题在设计之初就应该考虑。一个新的系统,不说比之前的 (godoc.org)好,至少不能差。godoc.org 在遇到包没有时会实时获取,而且还支持用户手动刷新;
在访问某个不存在的包时,让用户按照说明操作下,以便 proxy.golang.org 上有这个包。这个过程完全可以程序自动做;不仅如此,还可以考虑自动触发 Worker 进行工作,拉取包数据;
Worker 组件
Worker 的主要工作是下载发现的新模块,进行处理,然后将信息写入数据库以供 Frontend 使用。它提取 README 文件,许可证文件(license)和文档,并将它们写入数据库。它还将与搜索相关的数据写入表(search_documents)。除了直接在模块 zip 中可用的搜索信息外,它还计算每个包的导入者数量。(imports_unique 表保存了每个包导入的其他包)
为了简化处理新模块的工作并利用上限速和重试功能,Worker 使用 Google Cloud Tasks[9] 队列来管理要处理的模块列表。当 Worker 程序在索引(index)中找到新模块时,它会将任务添加到队列中。队列以固定的最大速率将任务推送给 Worker。
文档提到:由于 Worker 必须是无状态 HTTP 服务器,因此无法运行后台任务。因此使用 Google Cloud Scheduler[10] 来定期执行任务。这些任务通常每分钟运行一次,它们是:
轮询索引(index)以使新模块进入队列;
对于暂时处理失败的模块,重新进入队列;
更新每个包的导入者数量;
从架构图可以看到,Worker 从 index 获取新模块(默认是 index.golang.org),从 proxy 获取 module 的 zip 文件(默认是 proxy.golang.org),最后将信息、数据写入 postgres 数据库。
吐槽:
为什么 Worker 必须是无状态的 HTTP 服务,还无法运行后台任务?这里完全可以通过类似 https://github.com/robfig/cron 这样的库来处理定时任务。竟然设计成启动一个 HTTP 服务,由一个外部定时任务调度器(Google Cloud Scheduler)来调用它提供的接口。
数据库
数据库方面,主要看看表的设计,同时学习下是如何做迁移管理的。
该项目没有使用配置文件,而是通过环境变量来控制。比如 postgres 数据库相关配置信息通过如下环境变量控制:
GO_DISCOVERY_DATABASE_USER
(default: postgres)GO_DISCOVERY_DATABASE_PASSWORD
(default: '')GO_DISCOVERY_DATABASE_HOST
(default: localhost)GO_DISCOVERY_DATABASE_NAME
(default: discovery-db)
这些配置信息在 internal/config/config.go 文件中。
表的设计是项目很重要的一个环节。为了看到该项目的表设计,安装好 postgres 后,执行如下脚本(类 Unix 系统)创建 discovery-db 数据库:
之后执行迁移操作。
数据库迁移
很多人可能对迁移不了解,这里简单介绍下。
这里说的数据库迁移,主要是指数据库 schema 的变更(当然也包括不同数据源往某个数据库迁移)。我们知道,代码的变更可以通过 Git 进行管理,通过 Git 可以很容易的实现代码的回滚。于是有人就想,代码变更时,很可能数据表的结构也变了,那有没有可能很方便的对数据库的变化进行回滚呢(升降版本)?于是有了 database migrate。就开源项目而言,对数据库自动升降级很有帮助。
然而数据库迁移并没有标准,依赖于具体的工具实现。迁移工具一般分为两种:1)独立的迁移软件,如 Liquibase[11];2)依附于具体语言的库,比如 Go 语言的 migrate[12],不过这个库也可以作为独立的软件使用。
每一次 schema 的变更,有时包括初始化数据,通常会记录在一个单独的脚本文件中。通常每一次数据库变更,应该生成一个对应的文件。我们通过 migrate 这个库具体学习下迁移的操作。
migrate 学习
该项目是 Go 语言实现的数据库迁移工具,支持 CLI 方式使用,也支持作为库导入使用。Migrate 从源读取迁移,并将迁移以正确的顺序应用于数据库。
目前该工具支持如下数据库:
PostgreSQL[13]
Redshift[14]
Ql[15]
Cassandra[16]
SQLite[17] (todo #165[18])
MySQL/ MariaDB[19]
Neo4j[20]
MongoDB[21]
CrateDB[22] (todo #170[23])
Shell[24] (todo #171[25])
Google Cloud Spanner[26]
CockroachDB[27]
ClickHouse[28]
Firebird[29]
MS SQL Server[30]
迁移来源支持如下几种:
Filesystem[31] - 从文件系统读取
Go-Bindata[32] - 从内嵌的二进制数据读取
Github[33] - 从远程 GitHub 仓库读取
Github Enterprise[34] - 从远程 Github 企业仓库读取
Gitlab[35] - 从远程 Gitlab 仓库读取
AWS S3[36] - 从 Amazon Web Services S3 读取
Google Cloud Storage[37] - 从 Google Cloud Platform Storage 读取
先安装,以 MacOS 为例:
为了方便演示,以上文创建的 https://github.com/polaris1119/testpkg 为例,clone 下来后,在 testpkg 目录下执行如下命令,创建一个迁移:
成功后会在 migrations 目录下生成两个文件:
这里有两个基本概念需要清楚:up 和 down,上面两个文件中有包含。
up 表示升级到当前版本;
down 表示回退到上一版本;
所以,我们在上面两个文件中填上如下内容:
up 文件的内容是创建表 gopher;
down 文件的内容是删除表 gopher;
之后就可以进行迁移操作了(这里假定你本地已经装上 postgres,密码是 123456,同时创建了 testpkg 数据库):
这时会发现数据库中多了两个表:gopher 和 schema_migrations。其中 schema_migrations 的内容如下:
versiondirty1FALSE
如果这时再执行如下命令进行“回滚”:
为了安全,会如下提示:
选择 y,成功后再看看数据库的变化,发现 gopher 表不见了,schema_migrations 表的内容也清空了,因为上次是版本 1 ,这次回退了。
为了进一步了解细节,我们再创建一个迁移,增加一个表:
会生成两个文件,现在有 4 个文件了:
注意到没?文件名前缀自动变为了 00002。同样,我们在新生成的两个文件中填上如下内容:
之后执行迁移命令:
输出:
查看数据库,发现 gopher 和 article 表都有了,schema_migrations 表中 version 字段值是 2,符合预期。
明白迁移是怎么回事了吗?现在回过头来看看 pkgsite 的迁移和数据表。
##pkgsite 的数据表
pkgsite 提供了几个脚本,方便使用。比如上文提到的创建数据库的脚本。创建迁移和执行迁移的脚本分别是:devtools/create_migration.sh 和 devtools/migrate_db.sh。我们是研究 pkgsite,自然是执行迁移:(默认 migrate_db.sh 认为 postgres 数据库账号的密码是空,因为我本地设置了密码是 123456,因此需要在 migrate_db.sh 中加上密码)
输出:
一共 21 个版本。这时在 discovery-db 数据库中生成了一系列表。我们拿其中一个的表:packages ,看看它的设计:
我又要吐槽了:
借用火丁笔记老王的评价:这是应届生设计的吧
表名一般建议使用单数形式;
字符串类型竟然全部是 text,类型选择很随意,没有任何讲究;
主键竟然是三个 text 类型的联合;
。。。
总体看,设计者应该没有经历过大项目,或没有参与过因为量大而遇到性能问题的项目。
设计数据表的建议
这里给出当初在 360 时,公司 DBA 对创建数据表的一些建议或要求:
表名、列名长度不超过 16
每个字段都必须有
NOT NULL DEFAULT ''
, 如果是 int 则 default 0主键必须为 int/bigint 类型
如果是 int 类型,且不会存负数,则标记为 UNSIGNED INT
如果 int 类型是 10 以下的几个可枚举值,则使用 TINYINT 类型
varchar 长度小于 3000
text 字段个数不超过 3 个
每个字段增加 COMMENT 注释,说明字段用途
索引不能有重复
索引个数不能大于 5 个(包括主键)
索引字段必须为 not null,并且有 default 值
请不要使用 MySQL 保留字
以上虽然是针对 MySQL 的,但基本上 Postgres 也是适用的。
代码组织
看看 pkgsite 项目的目录结构:
cmd:该目录几乎是 Go 圈约定俗成的,是 Go 官方以及开源界推荐的方式,用于存放 main.main。它包含 4 个子目录,也就是项目可以生成 4 个可执行文件:
fronted:对应前文介绍的 Frontend 组件;
woker:对应前文介绍的 Worker 组件;
prober:探测器,定期探测 frontend,并导出 frontend 的度量指标,以便监控报警和性能追踪;
teeproxy:用于处理 godoc.org 上的链接跳转到 pkg.go.dev;
internal:除了 cmd 中部分的 Go 代码,其他代码全部在该目录下。目前开源项目习惯包含一个 pkg 包,将一些通用的代码放在该包下。我们知道 internal 包,对其他项目是不可见的。所以,该项目任何包,其他项目都没法直接使用。(上面省略了很多子包)
其他目录说明见上面对应的注释。
关于 main.main 放在哪的问题
上面说,main.main 放在 cmd 目录下几乎是约定俗成的,但针对这个问题还是需要进一步说明一下,毕竟这不是标准或规范。
那关于 main.main,即包含 main 包 和 main 函数的文件(一般是 main.go)放在哪里,目前一般有两种做法:
1)放在项目根目录下。这样放有一个好处,那就是可以方便的通过 go get 进行安装。比如 github.com/polaris1119/golangclub ,按这样的方式安装:
成功后在 $GOBIN
(未设置时取 $GOPATH[0]/bin
)目录下会找到 golangclub 可执行文件。但如果你的项目不止一个可执行文件,也就是会存在多个 main.go,这种方式显然没法满足需求。
目前有一些开源项目是这么做的,比如 cobra 生成的框架也是采用的这种方式。
2)创建一个 cmd 目录,专门放置 main.main,有些项目可能会直接将 main.go 放在 cmd 下,但这又回到了上面的方式,而且还没上面的方式方便。一般建议项目存在多个可执行文件时,在 cmd 下创建对应的目录。因为 pkgsite 存在多个可执行文件,因此采用了这种方式。像知名的 Kubernetes 也是采用的这种方式。对于这种方式,通过 go get 可以这样安装:
这样会将项目所有的可执行文件都生成,你也可以指定生成某一个:
本地搭建 pkgsite
为了更好了解这个项目,我们尝试在本地搭建一个。这里假定你在本地安装好了 postgres,同时设置了 postgres 账号的密码为 123456,并执行了上文提到的迁移程序,创建好了需要的数据表。
本地搭建可以基于 Docker,本文采用非 Docker 的方式。
Frontend
最简单的启动 Frontend 的方式是直接执行如下命令:
发现报错,提示 postgres 需要密码。
我们先不处理这个问题,看看 frontend 程序有无 flag 可以设置。
输出如下:
不一一解释,英文说明很清楚。这里着重看 -direct_proxy
和 -proxy_url
,通过 direct_proxy 似乎可以绕开 postgres,但由于默认的 proxy(proxy.golang.org)国内不可访问,因此同时设置一个 proxy:
输出如下:
打开浏览器访问 localhost:8080:

一切似乎很顺利。打开某个库,第一次稍微慢点,之后很快,从控制台输出的日志也能看到。
但访问标准库,如 database/sql,却报错:(同时页面显示 500)
很显然,在去获取 Go 仓库信息时,因为 go.googlesource.com 在国内无法访问导致错误。然而我科学上网后并没有解决问题:
我们先不细究这个问题。用上数据库试试。
现在解决数据库密码的问题。从启动时输出的 config 可以猜到,数据库信息在 config 中。前文提到,pkgsite 并没有用到配置文件,所有的配置通过环境变量来设置。
在 internal/config/config.go 文件中找到了这行代码:
因此这样启动 Frontend:
这回成功了,访问 localhost:8080 也正常显示了。然而查看包都返回 404,包括标准库和第三方包。

前文讲解过,pkgsite 就是这么设计的,数据库现在是空的,需要通过 Worker 往里面填充数据。
Worker
同样的采用最简单的方式运行:
注意上面 postgres 密码的问题
启动后,发现监听 8000 端口:
看看长什么样:

顶部的一些链接,都是 google cloud 的,本地没有意义。
根据前面讲解,Worker 数据最终是从 Module Index 和 Module Proxy 来的,而且 Worker 不会自己触发,必须外界调用它提供的接口。上面界面中可以看到,通过点击上面的几个按钮可以执行拉取:
Enqueue From Module Index:从 Module Index 入队列以便处理;
Requeue Failed Versions:对失败的版本重入队列;
Reprocess Versions:重新处理;
Populate Standard Library:填充标准库数据;
很惨的是,Enqueue From Module Index 和 Populate Standard Library 都失败。

吐槽一句:在国内做技术真难!
除了这种方式,还有一种手动获取某个版本的办法,比如在浏览器请求 http://localhost:8000/fetch/github.com/gin-gonic/gin/@v/v1.6.3,发现页面报 500,终端出现如下错误:
这是通过 proxy.golang.org 获取包模块信息。针对 Frontend,我们可以修改 proxy,Worker 可以修改吗?
可惜的是,Worker 并没有提供相关 flag 可以修改,但在 config.go 中发现了环境变量:GO_MODULE_PROXY_URL 和 GO_MODULE_INDEX_URL,默认分别是:https://proxy.golang.org 和 https://index.golang.org/index。proxy 我们可以设置为国内的 https://goproxy.cn,index 没有国内的可用。但对于手动获取某一个版本,设置 proxy 即可。通过如下方式重启 Worker:
再次访问 http://localhost:8000/fetch/github.com/gin-gonic/gin/@v/v1.6.3,显示成功:
回过头看看 http://localhost:8080/github.com/gin-gonic/gin,发现正常显示了,数据库中也有相应的数据了。但这种方式一次只能搞定一个包的一个版本。
福利:国内访问 pkg.go.dev 比较慢,有时甚至无法访问,因此我搭建了一个镜像:https://pkg.golangclub.com。
源码研究
通过上面的分析我们知道,Frontend 和 Worker 都是 HTTP 服务,因此先分析下这块是怎么实现的。
基于 net/http 构建
pkgsite 没有使用 Gin、Echo 之类的 Web 框架,而是基于 net/http 构建。Frontend 和 Worker 在这块的代码类似,这里以 Frontend 的代码为例。先看看 main.main 的代码(去除了部分认为不重要的代码),加上了注释。
先附一张调用图,再看代码:

这里用到了 OpenCensus 进行统计跟踪,这块内容本文不展开,有机会后面专门讲解
上面代码我们从后往前看:http.ListenAndServe 的第二个参数是一个 http.Handler,因此 mw(router) 必然返回的是一个 http.Handler。通常我们基于 net/http 创建 HTTP 服务,是这么做的:
之所以第二个参数传递 nil,是因为我们使用 http 包默认的 Handler。而该项目传递了 Handler,说明没有使用默认的。通常为了方便扩展,会实现自己的 Handler,这里主要是为了集成 OpenCensus。
我们看看 pkgsite 项目中间件定义:
中间件是一个接收 http.Handler 类型参数,同时返回 http.Handler 类型的函数;
Chain 将多个中间件串起来并返回一个新的中间件;
h = middlewares[len(middlewares)-1-i](h "len(middlewares)-1-i")
做到了Chain(m1, m2)(handler) = m1(m2(handler))
,做到了后进先出;
看一个具体的中间件 Quota:
pkg.go.dev 采用了基于 IP 的方式限流;
中间件可以在执行 h.ServeHTTP 方法前后加上一些逻辑,也可以在其之后加上一些逻辑;
通过 http.HandleFunc 转换一个函数为 http.Handler;
如果中间件比较复杂,可以自定义类型,实现 http.Handler,即实现其 ServeHTTP 方法,如 RequestLog 中间件;
接着看看 Router,根据要求,它应该是一个 http.Handler。
http.Handler 是一个接口,Router 应该实现它,但它却内嵌了一个 http.Handler?原来通过这种方式,Router 不需要显示实现 http.Handler 接口的方法 ServeHTTP,因为内嵌,默认就实现了 http.Handler 接口。
所以关键在于实例化 Router:
原来通过 http.NewServeMux() 得到一个实现了 http.Handler 接口的实例,然后赋值给 Router 的内嵌字段 http.Handler (忽略 ochttp,它属于 OpenCensus)。
最后是 server.Install(router.Handle, cacheClient)
,安装具体的路由,其实就是调用 router.Handle 方法。
最简代码实现
为了你更好的理解,这里用尽可能少的代码实现上面的主要功能:
代码放在 https://github.com/polaris1119/testpkg,go run main.go 运行后,打开浏览器访问 localhost:2020,终端有如下输出:
符合预期。
具体库如何获取数据
问:在 pkg.go.dev 首页上的 Popular Packages 和 Featured Packages 是如何实现的?

一看代码,我惊住了:竟然是 HTML 中写死的~
pkgsite 如何区分标准库和第三方库的?具体库的信息都是由 handle("/", detailHandler)
处理的,具体是 internal/frontend/details.go 文件中的 serveDetails 方法:
进一步查看 stdlib.Contains 函数:
第三方库和标准库的区别就是包路径中是否包含句点(.
)。然而获取他们信息时,读取的库是一样的。
具体看看请求 github.com/gin-gonic/gin 这个包都进行了什么处理?

继续吐槽:
默认访问某个包时,tab 必然为空,竟然做了前面一大堆事情才判断 tab,进行重定向。这里不应该要么提前重定向,要么给 tab 默认值,不进行重定向吗?
包的组织比较随意,也没有使用 MVC 模式,代码层次、可读性较差;
有一个好点的设计值得提一下:
DataSource 接口。支持从 proxy 和 postgres 获取数据。
Worker 核心逻辑
Frontend 页面展示,相对逻辑较简单,基本是查库、展示。而 Worker 涉及到写库,本应该也较简单,但这里涉及到 Module 的一些知识,我们借此了解下。
上面介绍了,Worker 会循环从 Module Index 发现新模块并写入队列,然后从 Module Proxy 获取模块的具体内容。
index.golang.org:一个索引,用于为 proxy.golang.org 提供可用的新模块版本的摘要。可以在 https://index.golang.org/index 上查看摘要。摘要是以新行分隔的 JSON 形式提供,包括了模块路径(Path),模块版本(Version)以及 proxy.golang.org 首次缓存它的时间(Timestamp)。该列表按时间升序排序。支持两个可选参数:
since:返回列表中模块版本的最早允许时间戳(RFC3339 格式)。默认是 index 开始的时间,例如 https://index.golang.org/index?since=2019-04-10T19:08:52.997264Z
limit:返回列表的最大长度。默认值 = 2000,最大值 = 2000,例如 https://index.golang.org/index?limit=10
返回的内容示例:
我们看看 pkgsite 是怎么从 index.golang.org 获取数据的。
因为 Worker 也是一个 HTTP 服务,main.main 中的代码和 Frontend 类似,我们直接找到拉取 index 的地方:
这是一个由 Cloud Scheduler 调用的端点(endpoint),在 Worker 的后台界面的 “Enqueue From Module Index” 也是调用的这个接口。处理逻辑是:
从 module_version_states 表中根据 index_timestamp 字段降序,拿到最大的一个时间戳,作为 since 参数的值;
使用 since 和 limit 参数请求 index.golang.org/index,获取新模块;
将获取的数据写入 module_version_states 表;
将获取的数据放入队列,方便获取包的具体信息;
本地运行时(非 GAE 上),队列使用的内存队列(满足定义的 Queue 接口),具体使用的是 Channel,默认容量是 1000:
因此在入队列后,process 这个处理程序会进行处理:
这里获取具体包信息时,开启一个 goroutine 进行处理,为了避免 goroutine 数量不可控,Worker 提供了一个 flag,用于控制 goroutine 的数量,默认是 10,这里 q.sem 就是用于控制 goroutine 数量的。
之后就是通过 Module Proxy 获取包信息并进行处理。这块涉及模块代理协议[38]的内容,有兴趣的可以自己阅读源码了解,同时参考一个开源模块代理实现:https://github.com/goproxy/goproxy.cn。
标准库是如何获取的
讲到这里,还有一个问题没解决,那就是标准库,它并不会走上面 Worker 提到的处理逻辑。
这用于获取标准库版本,需要手动执行,而且 Go 每发布一个版本,应该执行一次。请求这个接口后,会通过 git 获取 Go 仓库信息,这个仓库不是 GitHub 上的,而是 https://go.googlesource.com/go,上面提到了,国内访问不了,不过可以改代码换成 https://github.com/golang/go。
标准库具体的处理逻辑:
通过 https://github.com/go-git/go-git 这个 Git 操作库,获取 Go 目前所有的版本信息;
针对每一个版本,循环交由队列处理,这里和第三方库是一样的;
之后的处理流程和第三库类似,只是获取数据时的来源不同;
注意:由于目前 Go 版本较多,这个过程会比较耗时,甚至会有失败的情况。
依然吐槽
除了以上还有其他接口需要手动执行,如构建搜索文档等。细究代码后,再一次发现 Worker 设计的不合理。明明是一个后台任务处理器,非得依赖第三方定时任务来触发,有些还必须手动触发。
总结
整个项目的分析就差不多结束了。虽然 internal 下面的包一堆,毕竟业务不复杂。如果你对其中的细节感兴趣,可以跟着代码仔细研究。可以借助 Goland 这样的 IDE 进行调试,跟踪关键接口是如何执行的,这样有利于快速了解项目的整体结构。
文章较长,希望看到这里你能有所收获!没有开打赏,给个转发或在看或留言就是对我最大的支持!没有关注的关注下!

参考资料
[1]
Go Module Index: https://index.golang.org
[2]Go Module Mirror: https://proxy.golang.org
[3]说明: https://go.dev/about#adding-a-package
[4]Go Module Index: https://index.golang.org/index
[5]模块代理协议(Module Proxy Protocol): https://docs.studygolang.com/cmd/go/#hdr-Module_proxy_protocol
[6]#36811: https://github.com/golang/go/issues/36811
[7]#37002: https://github.com/golang/go/issues/37002
[8]#37106: https://github.com/golang/go/issues/37106
[9]Google Cloud Tasks: https://cloud.google.com/tasks
[10]Google Cloud Scheduler: https://cloud.google.com/scheduler
[11]Liquibase: https://www.liquibase.org/
[12]migrate: https://github.com/golang-migrate/migrate
[13]PostgreSQL: https://github.com/golang-migrate/migrate/blob/master/database/postgres
[14]Redshift: https://github.com/golang-migrate/migrate/blob/master/database/redshift
[15]Ql: https://github.com/golang-migrate/migrate/blob/master/database/ql
[16]Cassandra: https://github.com/golang-migrate/migrate/blob/master/database/cassandra
[17]SQLite: https://github.com/golang-migrate/migrate/blob/master/database/sqlite3
[18]todo #165: https://github.com/mattes/migrate/issues/165
[19]MySQL/ MariaDB: https://github.com/golang-migrate/migrate/blob/master/database/mysql
[20]Neo4j: https://github.com/golang-migrate/migrate/blob/master/database/neo4j
[21]MongoDB: https://github.com/golang-migrate/migrate/blob/master/database/mongodb
[22]CrateDB: https://github.com/golang-migrate/migrate/blob/master/database/crate
[23]todo #170: https://github.com/mattes/migrate/issues/170
[24]Shell: https://github.com/golang-migrate/migrate/blob/master/database/shell
[25]todo #171: https://github.com/mattes/migrate/issues/171
[26]Google Cloud Spanner: https://github.com/golang-migrate/migrate/blob/master/database/spanner
[27]CockroachDB: https://github.com/golang-migrate/migrate/blob/master/database/cockroachdb
[28]ClickHouse: https://github.com/golang-migrate/migrate/blob/master/database/clickhouse
[29]Firebird: https://github.com/golang-migrate/migrate/blob/master/database/firebird
[30]MS SQL Server: https://github.com/golang-migrate/migrate/blob/master/database/sqlserver
[31]Filesystem: https://github.com/golang-migrate/migrate/blob/master/source/file
[32]Go-Bindata: https://github.com/golang-migrate/migrate/blob/master/source/go_bindata
[33]Github: https://github.com/golang-migrate/migrate/blob/master/source/github
[34]Github Enterprise: https://github.com/golang-migrate/migrate/blob/master/source/github_ee
[35]Gitlab: https://github.com/golang-migrate/migrate/blob/master/source/gitlab
[36]AWS S3: https://github.com/golang-migrate/migrate/blob/master/source/aws_s3
[37]Google Cloud Storage: https://github.com/golang-migrate/migrate/blob/master/source/google_cloud_storage
[38]模块代理协议: https://docs.studygolang.com/cmd/go/#hdr-Module_proxy_protocol