萬字長(zhǎng)文解讀 pkg.go.dev 的設(shè)計(jì)和實(shí)現(xiàn)
https://mp.weixin.qq.com/s/btX53JVCgfOfxDy2ynQa_A
文章較長(zhǎng),建議收藏,抽完整時(shí)間閱讀。覺得不錯(cuò)幫轉(zhuǎn)發(fā)下!
北京時(shí)間 2020 年 6 月 15 日 22 點(diǎn)左右,Go 官方發(fā)博文(https://blg.golang.org/pkgsite)宣布,pkg.go.dev 開源了。開源代碼托管在 Google 自有倉(cāng)庫 https://go.googlesource.com/pkgsite,不過在 GitHub 上提供了鏡像:https://github.com/golang/pkgsite。同時(shí),對(duì)于該項(xiàng)目的任何 issue,通過 Go 主倉(cāng)庫進(jìn)行管理,即 https://github.com/golang/go/labels/go.dev。
Go 作者們可能也沒有想到,經(jīng)過這么多年的發(fā)展,Go 被使用最廣的竟然是 Web 開發(fā),這可能得益于一開始 Go 就對(duì) http 有很好的支持,也因此涌現(xiàn)出大量的 Web 框架,其中知名的有 Gin、Echo、Beego 等。
運(yùn)營(yíng) Go 語言中文網(wǎng)和 Go 社區(qū) 8 年有余,發(fā)現(xiàn)廣大 Gopher 們都苦于沒有實(shí)戰(zhàn)項(xiàng)目可以練手,很多新手學(xué)習(xí)完 Go 語法后,因?yàn)楣ぷ髦袥]有用到,不知道該怎么進(jìn)行項(xiàng)目實(shí)戰(zhàn)或用 Go 做點(diǎn)什么。這期間也有很多人問我有無適合新手學(xué)習(xí)的開源項(xiàng)目。也是在去年初,我因此還創(chuàng)建了《Go項(xiàng)目實(shí)戰(zhàn)》知識(shí)星球?,F(xiàn)在,Go 官方開源了 pkg.go.dev 這個(gè) Web 項(xiàng)目,愛學(xué)習(xí)的你,不應(yīng)該只是看到了這個(gè)消息就完事了,應(yīng)該做點(diǎn)什么,學(xué)點(diǎn)什么。
原計(jì)劃,我是希望通過這個(gè)項(xiàng)目,讓大家能夠很好的學(xué)習(xí) Go 是如何進(jìn)行實(shí)際項(xiàng)目開發(fā)的。當(dāng)我深入研究 pkg.go.dev 源碼后,我失望了,無論是設(shè)計(jì)還是實(shí)現(xiàn),水平都很一般。本想著放棄這一系列,但想想還是繼續(xù),一方面嘗試指出問題,給出認(rèn)為正確的做法;另一方面,畢竟是官方的項(xiàng)目,開源了相信它會(huì)變得更好。
本文包括的內(nèi)容(并非大綱):

項(xiàng)目架構(gòu)
先上一張官方的架構(gòu)圖:

包含了三個(gè)核心組件:
Frontend:這是一個(gè)面向用戶的前端組件,處理用戶請(qǐng)求,獲取數(shù)據(jù)并展示給用戶;
Worker:一個(gè)后臺(tái)程序,負(fù)責(zé)將新模塊的信息寫入數(shù)據(jù)庫。新模塊的數(shù)據(jù)來自 Go Module Index[1],同時(shí)這些模塊的內(nèi)容是從 Go Module Mirror[2] 中下載的;
Database:數(shù)據(jù)庫,用于存儲(chǔ)站點(diǎn)上提供的所有信息。
圖中其他的部分包括:Frontend 組件使用的 Redis 緩存、任務(wù)隊(duì)列和 Scheduler。
Frontend 組件
這是一個(gè)簡(jiǎn)單的 HTTP 服務(wù),它從數(shù)據(jù)庫中獲取數(shù)據(jù),渲染模板,最后生成 HTML 頁面。而搜索功能,是通過 Postgres 的全文搜索實(shí)現(xiàn)的,因此沒有引入另外的搜索組件,比如:Solr、ElasticSearch。
目前前端做的事情比較簡(jiǎn)單:從 Postgres 數(shù)據(jù)庫中獲取 modules 和 packages 等數(shù)據(jù),而 Redis 用于緩存這些數(shù)據(jù)。
細(xì)心的讀者會(huì)發(fā)現(xiàn)這樣的設(shè)計(jì)存在一個(gè)問題:更新不及時(shí)。前端數(shù)據(jù)依賴 Worker 組件寫入 DB。我臨時(shí)創(chuàng)建一個(gè) Go 包,用于驗(yàn)證該問題:github.com/polaris1119/testpkg,在 godoc.org 上看到的信息如下:

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

雖然這個(gè) 404 頁面告訴你:如果你認(rèn)為這是一個(gè)有效的包路徑,可以通過這里的說明[3]嘗試獲取該包。那這個(gè)說明是什么?
因?yàn)?pkg.go.dev 的數(shù)據(jù)是從 proxy.golang.org 下載的。我們會(huì)定期監(jiān)控 Go Module Index[4],以查找要添加到 pkg.go.dev 的新包。如果在 pkg.go.dev 上沒有看到你要的包,則可以通過以下任一操作將其添加:
向 proxy.golang.org 請(qǐng)求模塊版本,可以請(qǐng)求模塊代理協(xié)議(Module Proxy Protocol)[5]指定的任何端點(diǎn),例如:https://proxy.golang.org/github.com/polaris1119/testpkg/@v/list;
通過 go 命令下載相應(yīng)的包。例如:GOPROXY=https://proxy.golang.org GO111MODULE=on go get ? ? ?github.com/polaris1119/testpkg
可見,這個(gè)說明是告訴你怎么將包提交給 proxy.golang.org。即使這樣做了,很可能 pkg.go.dev 上還是沒有,因?yàn)?Frontend 只負(fù)責(zé)從 DB 獲取數(shù)據(jù)。(幾分鐘后不出意外應(yīng)該有了)

然而這樣的設(shè)計(jì)大家肯定接受不了,因此出現(xiàn)了幾個(gè)這樣的 ?issue:#36811[6], #37002[7], #37106[8] 等。為了應(yīng)對(duì)這種情況,所以出現(xiàn)了架構(gòu)圖中的 Frontend Task Queue,用于獲取數(shù)據(jù)庫中還不存在的包并將包 master 分支的信息顯示給用戶,不過截止目前還未實(shí)現(xiàn)。不知道到時(shí)候以及會(huì)如何實(shí)現(xiàn)。
這里不得不吐槽了:
這樣的問題在設(shè)計(jì)之初就應(yīng)該考慮。一個(gè)新的系統(tǒng),不說比之前的 (godoc.org)好,至少不能差。godoc.org 在遇到包沒有時(shí)會(huì)實(shí)時(shí)獲取,而且還支持用戶手動(dòng)刷新;
在訪問某個(gè)不存在的包時(shí),讓用戶按照說明操作下,以便 proxy.golang.org 上有這個(gè)包。這個(gè)過程完全可以程序自動(dòng)做;不僅如此,還可以考慮自動(dòng)觸發(fā) Worker 進(jìn)行工作,拉取包數(shù)據(jù);
Worker 組件
Worker 的主要工作是下載發(fā)現(xiàn)的新模塊,進(jìn)行處理,然后將信息寫入數(shù)據(jù)庫以供 Frontend 使用。它提取 README 文件,許可證文件(license)和文檔,并將它們寫入數(shù)據(jù)庫。它還將與搜索相關(guān)的數(shù)據(jù)寫入表(search_documents)。除了直接在模塊 zip 中可用的搜索信息外,它還計(jì)算每個(gè)包的導(dǎo)入者數(shù)量。(imports_unique 表保存了每個(gè)包導(dǎo)入的其他包)
為了簡(jiǎn)化處理新模塊的工作并利用上限速和重試功能,Worker 使用 Google Cloud Tasks[9] 隊(duì)列來管理要處理的模塊列表。當(dāng) Worker 程序在索引(index)中找到新模塊時(shí),它會(huì)將任務(wù)添加到隊(duì)列中。隊(duì)列以固定的最大速率將任務(wù)推送給 Worker。
文檔提到:由于 Worker 必須是無狀態(tài) HTTP 服務(wù)器,因此無法運(yùn)行后臺(tái)任務(wù)。因此使用 Google Cloud Scheduler[10] 來定期執(zhí)行任務(wù)。這些任務(wù)通常每分鐘運(yùn)行一次,它們是:
輪詢索引(index)以使新模塊進(jìn)入隊(duì)列;
對(duì)于暫時(shí)處理失敗的模塊,重新進(jìn)入隊(duì)列;
更新每個(gè)包的導(dǎo)入者數(shù)量;
從架構(gòu)圖可以看到,Worker 從 index 獲取新模塊(默認(rèn)是 index.golang.org),從 proxy 獲取 module 的 zip 文件(默認(rèn)是 proxy.golang.org),最后將信息、數(shù)據(jù)寫入 postgres 數(shù)據(jù)庫。
吐槽:
為什么 Worker 必須是無狀態(tài)的 HTTP 服務(wù),還無法運(yùn)行后臺(tái)任務(wù)?這里完全可以通過類似 https://github.com/robfig/cron 這樣的庫來處理定時(shí)任務(wù)。竟然設(shè)計(jì)成啟動(dòng)一個(gè) HTTP 服務(wù),由一個(gè)外部定時(shí)任務(wù)調(diào)度器(Google Cloud Scheduler)來調(diào)用它提供的接口。
數(shù)據(jù)庫
數(shù)據(jù)庫方面,主要看看表的設(shè)計(jì),同時(shí)學(xué)習(xí)下是如何做遷移管理的。
該項(xiàng)目沒有使用配置文件,而是通過環(huán)境變量來控制。比如 postgres 數(shù)據(jù)庫相關(guān)配置信息通過如下環(huán)境變量控制:
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 文件中。
表的設(shè)計(jì)是項(xiàng)目很重要的一個(gè)環(huán)節(jié)。為了看到該項(xiàng)目的表設(shè)計(jì),安裝好 postgres 后,執(zhí)行如下腳本(類 Unix 系統(tǒng))創(chuàng)建 discovery-db 數(shù)據(jù)庫:
之后執(zhí)行遷移操作。
數(shù)據(jù)庫遷移
很多人可能對(duì)遷移不了解,這里簡(jiǎn)單介紹下。
這里說的數(shù)據(jù)庫遷移,主要是指數(shù)據(jù)庫 schema 的變更(當(dāng)然也包括不同數(shù)據(jù)源往某個(gè)數(shù)據(jù)庫遷移)。我們知道,代碼的變更可以通過 Git 進(jìn)行管理,通過 Git 可以很容易的實(shí)現(xiàn)代碼的回滾。于是有人就想,代碼變更時(shí),很可能數(shù)據(jù)表的結(jié)構(gòu)也變了,那有沒有可能很方便的對(duì)數(shù)據(jù)庫的變化進(jìn)行回滾呢(升降版本)?于是有了 database migrate。就開源項(xiàng)目而言,對(duì)數(shù)據(jù)庫自動(dòng)升降級(jí)很有幫助。
然而數(shù)據(jù)庫遷移并沒有標(biāo)準(zhǔn),依賴于具體的工具實(shí)現(xiàn)。遷移工具一般分為兩種:1)獨(dú)立的遷移軟件,如 Liquibase[11];2)依附于具體語言的庫,比如 Go 語言的 migrate[12],不過這個(gè)庫也可以作為獨(dú)立的軟件使用。
每一次 schema 的變更,有時(shí)包括初始化數(shù)據(jù),通常會(huì)記錄在一個(gè)單獨(dú)的腳本文件中。通常每一次數(shù)據(jù)庫變更,應(yīng)該生成一個(gè)對(duì)應(yīng)的文件。我們通過 migrate 這個(gè)庫具體學(xué)習(xí)下遷移的操作。
migrate 學(xué)習(xí)
該項(xiàng)目是 Go 語言實(shí)現(xiàn)的數(shù)據(jù)庫遷移工具,支持 CLI 方式使用,也支持作為庫導(dǎo)入使用。Migrate 從源讀取遷移,并將遷移以正確的順序應(yīng)用于數(shù)據(jù)庫。
目前該工具支持如下數(shù)據(jù)庫:
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] - 從文件系統(tǒng)讀取
Go-Bindata[32] - 從內(nèi)嵌的二進(jìn)制數(shù)據(jù)讀取
Github[33] - 從遠(yuǎn)程 GitHub 倉(cāng)庫讀取
Github Enterprise[34] - 從遠(yuǎn)程 Github 企業(yè)倉(cāng)庫讀取
Gitlab[35] - 從遠(yuǎn)程 Gitlab 倉(cāng)庫讀取
AWS S3[36] - 從 Amazon Web Services S3 讀取
Google Cloud Storage[37] - 從 Google Cloud Platform Storage 讀取
先安裝,以 MacOS 為例:
為了方便演示,以上文創(chuàng)建的 https://github.com/polaris1119/testpkg 為例,clone 下來后,在 testpkg 目錄下執(zhí)行如下命令,創(chuàng)建一個(gè)遷移:
成功后會(huì)在 migrations 目錄下生成兩個(gè)文件:
這里有兩個(gè)基本概念需要清楚:up 和 down,上面兩個(gè)文件中有包含。
up 表示升級(jí)到當(dāng)前版本;
down 表示回退到上一版本;
所以,我們?cè)谏厦鎯蓚€(gè)文件中填上如下內(nèi)容:
up 文件的內(nèi)容是創(chuàng)建表 gopher;
down 文件的內(nèi)容是刪除表 gopher;
之后就可以進(jìn)行遷移操作了(這里假定你本地已經(jīng)裝上 postgres,密碼是 123456,同時(shí)創(chuàng)建了 testpkg 數(shù)據(jù)庫):
這時(shí)會(huì)發(fā)現(xiàn)數(shù)據(jù)庫中多了兩個(gè)表:gopher 和 schema_migrations。其中 schema_migrations 的內(nèi)容如下:
versiondirty1FALSE
如果這時(shí)再執(zhí)行如下命令進(jìn)行“回滾”:
為了安全,會(huì)如下提示:
選擇 y,成功后再看看數(shù)據(jù)庫的變化,發(fā)現(xiàn) gopher 表不見了,schema_migrations 表的內(nèi)容也清空了,因?yàn)樯洗问前姹?1 ,這次回退了。
為了進(jìn)一步了解細(xì)節(jié),我們?cè)賱?chuàng)建一個(gè)遷移,增加一個(gè)表:
會(huì)生成兩個(gè)文件,現(xiàn)在有 4 個(gè)文件了:
注意到?jīng)]?文件名前綴自動(dòng)變?yōu)榱?00002。同樣,我們?cè)谛律傻膬蓚€(gè)文件中填上如下內(nèi)容:
之后執(zhí)行遷移命令:
輸出:
查看數(shù)據(jù)庫,發(fā)現(xiàn) gopher 和 article 表都有了,schema_migrations 表中 version 字段值是 2,符合預(yù)期。
明白遷移是怎么回事了嗎?現(xiàn)在回過頭來看看 pkgsite 的遷移和數(shù)據(jù)表。
##pkgsite 的數(shù)據(jù)表
pkgsite 提供了幾個(gè)腳本,方便使用。比如上文提到的創(chuàng)建數(shù)據(jù)庫的腳本。創(chuàng)建遷移和執(zhí)行遷移的腳本分別是:devtools/create_migration.sh 和 devtools/migrate_db.sh。我們是研究 pkgsite,自然是執(zhí)行遷移:(默認(rèn) migrate_db.sh 認(rèn)為 postgres 數(shù)據(jù)庫賬號(hào)的密碼是空,因?yàn)槲冶镜卦O(shè)置了密碼是 123456,因此需要在 migrate_db.sh 中加上密碼)
輸出:
一共 21 個(gè)版本。這時(shí)在 discovery-db 數(shù)據(jù)庫中生成了一系列表。我們拿其中一個(gè)的表:packages ,看看它的設(shè)計(jì):
我又要吐槽了:
借用火丁筆記老王的評(píng)價(jià):這是應(yīng)屆生設(shè)計(jì)的吧
表名一般建議使用單數(shù)形式;
字符串類型竟然全部是 text,類型選擇很隨意,沒有任何講究;
主鍵竟然是三個(gè) text 類型的聯(lián)合;
。。。
總體看,設(shè)計(jì)者應(yīng)該沒有經(jīng)歷過大項(xiàng)目,或沒有參與過因?yàn)榱看蠖龅叫阅軉栴}的項(xiàng)目。
設(shè)計(jì)數(shù)據(jù)表的建議
這里給出當(dāng)初在 360 時(shí),公司 DBA 對(duì)創(chuàng)建數(shù)據(jù)表的一些建議或要求:
表名、列名長(zhǎng)度不超過 16
每個(gè)字段都必須有
NOT NULL DEFAULT ''
, 如果是 int 則 default 0主鍵必須為 int/bigint 類型
如果是 int 類型,且不會(huì)存負(fù)數(shù),則標(biāo)記為 UNSIGNED INT
如果 int 類型是 10 以下的幾個(gè)可枚舉值,則使用 TINYINT 類型
varchar 長(zhǎng)度小于 3000
text 字段個(gè)數(shù)不超過 3 個(gè)
每個(gè)字段增加 COMMENT 注釋,說明字段用途
索引不能有重復(fù)
索引個(gè)數(shù)不能大于 5 個(gè)(包括主鍵)
索引字段必須為 not null,并且有 default 值
請(qǐng)不要使用 MySQL 保留字
以上雖然是針對(duì) MySQL 的,但基本上 Postgres 也是適用的。
代碼組織
看看 pkgsite 項(xiàng)目的目錄結(jié)構(gòu):
cmd:該目錄幾乎是 Go 圈約定俗成的,是 Go 官方以及開源界推薦的方式,用于存放 main.main。它包含 4 個(gè)子目錄,也就是項(xiàng)目可以生成 4 個(gè)可執(zhí)行文件:
fronted:對(duì)應(yīng)前文介紹的 Frontend 組件;
woker:對(duì)應(yīng)前文介紹的 Worker 組件;
prober:探測(cè)器,定期探測(cè) frontend,并導(dǎo)出 frontend 的度量指標(biāo),以便監(jiān)控報(bào)警和性能追蹤;
teeproxy:用于處理 godoc.org 上的鏈接跳轉(zhuǎn)到 pkg.go.dev;
internal:除了 cmd 中部分的 Go 代碼,其他代碼全部在該目錄下。目前開源項(xiàng)目習(xí)慣包含一個(gè) pkg 包,將一些通用的代碼放在該包下。我們知道 internal 包,對(duì)其他項(xiàng)目是不可見的。所以,該項(xiàng)目任何包,其他項(xiàng)目都沒法直接使用。(上面省略了很多子包)
其他目錄說明見上面對(duì)應(yīng)的注釋。
關(guān)于 main.main 放在哪的問題
上面說,main.main 放在 cmd 目錄下幾乎是約定俗成的,但針對(duì)這個(gè)問題還是需要進(jìn)一步說明一下,畢竟這不是標(biāo)準(zhǔn)或規(guī)范。
那關(guān)于 main.main,即包含 main 包 和 main 函數(shù)的文件(一般是 main.go)放在哪里,目前一般有兩種做法:
1)放在項(xiàng)目根目錄下。這樣放有一個(gè)好處,那就是可以方便的通過 go get 進(jìn)行安裝。比如 github.com/polaris1119/golangclub ,按這樣的方式安裝:
成功后在 $GOBIN
(未設(shè)置時(shí)取 $GOPATH[0]/bin
)目錄下會(huì)找到 golangclub 可執(zhí)行文件。但如果你的項(xiàng)目不止一個(gè)可執(zhí)行文件,也就是會(huì)存在多個(gè) main.go,這種方式顯然沒法滿足需求。
目前有一些開源項(xiàng)目是這么做的,比如 cobra 生成的框架也是采用的這種方式。
2)創(chuàng)建一個(gè) cmd 目錄,專門放置 main.main,有些項(xiàng)目可能會(huì)直接將 main.go 放在 cmd 下,但這又回到了上面的方式,而且還沒上面的方式方便。一般建議項(xiàng)目存在多個(gè)可執(zhí)行文件時(shí),在 cmd 下創(chuàng)建對(duì)應(yīng)的目錄。因?yàn)?pkgsite 存在多個(gè)可執(zhí)行文件,因此采用了這種方式。像知名的 Kubernetes 也是采用的這種方式。對(duì)于這種方式,通過 go get 可以這樣安裝:
這樣會(huì)將項(xiàng)目所有的可執(zhí)行文件都生成,你也可以指定生成某一個(gè):
本地搭建 pkgsite
為了更好了解這個(gè)項(xiàng)目,我們嘗試在本地搭建一個(gè)。這里假定你在本地安裝好了 postgres,同時(shí)設(shè)置了 postgres 賬號(hào)的密碼為 123456,并執(zhí)行了上文提到的遷移程序,創(chuàng)建好了需要的數(shù)據(jù)表。
本地搭建可以基于 Docker,本文采用非 Docker 的方式。
Frontend
最簡(jiǎn)單的啟動(dòng) Frontend 的方式是直接執(zhí)行如下命令:
發(fā)現(xiàn)報(bào)錯(cuò),提示 postgres 需要密碼。
我們先不處理這個(gè)問題,看看 frontend 程序有無 flag 可以設(shè)置。
輸出如下:
不一一解釋,英文說明很清楚。這里著重看 -direct_proxy
和 -proxy_url
,通過 direct_proxy 似乎可以繞開 postgres,但由于默認(rèn)的 proxy(proxy.golang.org)國(guó)內(nèi)不可訪問,因此同時(shí)設(shè)置一個(gè) proxy:
輸出如下:
打開瀏覽器訪問 localhost:8080:

一切似乎很順利。打開某個(gè)庫,第一次稍微慢點(diǎn),之后很快,從控制臺(tái)輸出的日志也能看到。
但訪問標(biāo)準(zhǔn)庫,如 database/sql,卻報(bào)錯(cuò):(同時(shí)頁面顯示 500)
很顯然,在去獲取 Go 倉(cāng)庫信息時(shí),因?yàn)?go.googlesource.com 在國(guó)內(nèi)無法訪問導(dǎo)致錯(cuò)誤。然而我科學(xué)上網(wǎng)后并沒有解決問題:
我們先不細(xì)究這個(gè)問題。用上數(shù)據(jù)庫試試。
現(xiàn)在解決數(shù)據(jù)庫密碼的問題。從啟動(dòng)時(shí)輸出的 config 可以猜到,數(shù)據(jù)庫信息在 config 中。前文提到,pkgsite 并沒有用到配置文件,所有的配置通過環(huán)境變量來設(shè)置。
在 internal/config/config.go 文件中找到了這行代碼:
因此這樣啟動(dòng) Frontend:
這回成功了,訪問 localhost:8080 也正常顯示了。然而查看包都返回 404,包括標(biāo)準(zhǔn)庫和第三方包。

前文講解過,pkgsite 就是這么設(shè)計(jì)的,數(shù)據(jù)庫現(xiàn)在是空的,需要通過 Worker 往里面填充數(shù)據(jù)。
Worker
同樣的采用最簡(jiǎn)單的方式運(yùn)行:
注意上面 postgres 密碼的問題
啟動(dòng)后,發(fā)現(xiàn)監(jiān)聽 8000 端口:
看看長(zhǎng)什么樣:

頂部的一些鏈接,都是 google cloud 的,本地沒有意義。
根據(jù)前面講解,Worker 數(shù)據(jù)最終是從 Module Index 和 Module Proxy 來的,而且 Worker 不會(huì)自己觸發(fā),必須外界調(diào)用它提供的接口。上面界面中可以看到,通過點(diǎn)擊上面的幾個(gè)按鈕可以執(zhí)行拉?。?/p>
Enqueue From Module Index:從 Module Index 入隊(duì)列以便處理;
Requeue Failed Versions:對(duì)失敗的版本重入隊(duì)列;
Reprocess Versions:重新處理;
Populate Standard Library:填充標(biāo)準(zhǔn)庫數(shù)據(jù);
很慘的是,Enqueue From Module Index 和 Populate Standard Library 都失敗。

吐槽一句:在國(guó)內(nèi)做技術(shù)真難!
除了這種方式,還有一種手動(dòng)獲取某個(gè)版本的辦法,比如在瀏覽器請(qǐng)求 http://localhost:8000/fetch/github.com/gin-gonic/gin/@v/v1.6.3,發(fā)現(xiàn)頁面報(bào) 500,終端出現(xiàn)如下錯(cuò)誤:
這是通過 proxy.golang.org 獲取包模塊信息。針對(duì) Frontend,我們可以修改 proxy,Worker 可以修改嗎?
可惜的是,Worker 并沒有提供相關(guān) flag 可以修改,但在 config.go 中發(fā)現(xiàn)了環(huán)境變量:GO_MODULE_PROXY_URL 和 GO_MODULE_INDEX_URL,默認(rèn)分別是:https://proxy.golang.org 和 https://index.golang.org/index。proxy 我們可以設(shè)置為國(guó)內(nèi)的 https://goproxy.cn,index 沒有國(guó)內(nèi)的可用。但對(duì)于手動(dòng)獲取某一個(gè)版本,設(shè)置 proxy 即可。通過如下方式重啟 Worker:
再次訪問 http://localhost:8000/fetch/github.com/gin-gonic/gin/@v/v1.6.3,顯示成功:
回過頭看看 http://localhost:8080/github.com/gin-gonic/gin,發(fā)現(xiàn)正常顯示了,數(shù)據(jù)庫中也有相應(yīng)的數(shù)據(jù)了。但這種方式一次只能搞定一個(gè)包的一個(gè)版本。
福利:國(guó)內(nèi)訪問 pkg.go.dev 比較慢,有時(shí)甚至無法訪問,因此我搭建了一個(gè)鏡像:https://pkg.golangclub.com。
源碼研究
通過上面的分析我們知道,F(xiàn)rontend 和 Worker 都是 HTTP 服務(wù),因此先分析下這塊是怎么實(shí)現(xiàn)的。
基于 net/http 構(gòu)建
pkgsite 沒有使用 Gin、Echo 之類的 Web 框架,而是基于 net/http 構(gòu)建。Frontend 和 Worker 在這塊的代碼類似,這里以 Frontend 的代碼為例。先看看 main.main 的代碼(去除了部分認(rèn)為不重要的代碼),加上了注釋。
先附一張調(diào)用圖,再看代碼:

這里用到了 OpenCensus 進(jìn)行統(tǒng)計(jì)跟蹤,這塊內(nèi)容本文不展開,有機(jī)會(huì)后面專門講解
上面代碼我們從后往前看:http.ListenAndServe 的第二個(gè)參數(shù)是一個(gè) http.Handler,因此 mw(router) 必然返回的是一個(gè) http.Handler。通常我們基于 net/http 創(chuàng)建 HTTP 服務(wù),是這么做的:
之所以第二個(gè)參數(shù)傳遞 nil,是因?yàn)槲覀兪褂?http 包默認(rèn)的 Handler。而該項(xiàng)目傳遞了 Handler,說明沒有使用默認(rèn)的。通常為了方便擴(kuò)展,會(huì)實(shí)現(xiàn)自己的 Handler,這里主要是為了集成 OpenCensus。
我們看看 pkgsite 項(xiàng)目中間件定義:
中間件是一個(gè)接收 http.Handler 類型參數(shù),同時(shí)返回 http.Handler 類型的函數(shù);
Chain 將多個(gè)中間件串起來并返回一個(gè)新的中間件;
h = middlewares[len(middlewares)-1-i](h "len(middlewares)-1-i")
做到了Chain(m1, m2)(handler) = m1(m2(handler))
,做到了后進(jìn)先出;
看一個(gè)具體的中間件 Quota:
pkg.go.dev 采用了基于 IP 的方式限流;
中間件可以在執(zhí)行 h.ServeHTTP 方法前后加上一些邏輯,也可以在其之后加上一些邏輯;
通過 http.HandleFunc 轉(zhuǎn)換一個(gè)函數(shù)為 http.Handler;
如果中間件比較復(fù)雜,可以自定義類型,實(shí)現(xiàn) http.Handler,即實(shí)現(xiàn)其 ServeHTTP 方法,如 RequestLog 中間件;
接著看看 Router,根據(jù)要求,它應(yīng)該是一個(gè) http.Handler。
http.Handler 是一個(gè)接口,Router 應(yīng)該實(shí)現(xiàn)它,但它卻內(nèi)嵌了一個(gè) http.Handler?原來通過這種方式,Router 不需要顯示實(shí)現(xiàn) http.Handler 接口的方法 ServeHTTP,因?yàn)閮?nèi)嵌,默認(rèn)就實(shí)現(xiàn)了 http.Handler 接口。
所以關(guān)鍵在于實(shí)例化 Router:
原來通過 http.NewServeMux() 得到一個(gè)實(shí)現(xiàn)了 http.Handler 接口的實(shí)例,然后賦值給 Router 的內(nèi)嵌字段 http.Handler (忽略 ochttp,它屬于 OpenCensus)。
最后是 server.Install(router.Handle, cacheClient)
,安裝具體的路由,其實(shí)就是調(diào)用 router.Handle 方法。
最簡(jiǎn)代碼實(shí)現(xiàn)
為了你更好的理解,這里用盡可能少的代碼實(shí)現(xiàn)上面的主要功能:
代碼放在 https://github.com/polaris1119/testpkg,go run main.go 運(yùn)行后,打開瀏覽器訪問 localhost:2020,終端有如下輸出:
符合預(yù)期。
具體庫如何獲取數(shù)據(jù)
問:在 pkg.go.dev 首頁上的 Popular Packages 和 Featured Packages 是如何實(shí)現(xiàn)的?

一看代碼,我驚住了:竟然是 HTML 中寫死的~
pkgsite 如何區(qū)分標(biāo)準(zhǔn)庫和第三方庫的?具體庫的信息都是由 handle("/", detailHandler)
處理的,具體是 internal/frontend/details.go 文件中的 serveDetails 方法:
進(jìn)一步查看 stdlib.Contains 函數(shù):
第三方庫和標(biāo)準(zhǔn)庫的區(qū)別就是包路徑中是否包含句點(diǎn)(.
)。然而獲取他們信息時(shí),讀取的庫是一樣的。
具體看看請(qǐng)求 github.com/gin-gonic/gin 這個(gè)包都進(jìn)行了什么處理?

繼續(xù)吐槽:
默認(rèn)訪問某個(gè)包時(shí),tab 必然為空,竟然做了前面一大堆事情才判斷 tab,進(jìn)行重定向。這里不應(yīng)該要么提前重定向,要么給 tab 默認(rèn)值,不進(jìn)行重定向嗎?
包的組織比較隨意,也沒有使用 MVC 模式,代碼層次、可讀性較差;
有一個(gè)好點(diǎn)的設(shè)計(jì)值得提一下:
DataSource 接口。支持從 proxy 和 postgres 獲取數(shù)據(jù)。
Worker 核心邏輯
Frontend 頁面展示,相對(duì)邏輯較簡(jiǎn)單,基本是查庫、展示。而 Worker 涉及到寫庫,本應(yīng)該也較簡(jiǎn)單,但這里涉及到 Module 的一些知識(shí),我們借此了解下。
上面介紹了,Worker 會(huì)循環(huán)從 Module Index 發(fā)現(xiàn)新模塊并寫入隊(duì)列,然后從 Module Proxy 獲取模塊的具體內(nèi)容。
index.golang.org:一個(gè)索引,用于為 proxy.golang.org 提供可用的新模塊版本的摘要??梢栽?https://index.golang.org/index 上查看摘要。摘要是以新行分隔的 JSON 形式提供,包括了模塊路徑(Path),模塊版本(Version)以及 proxy.golang.org 首次緩存它的時(shí)間(Timestamp)。該列表按時(shí)間升序排序。支持兩個(gè)可選參數(shù):
since:返回列表中模塊版本的最早允許時(shí)間戳(RFC3339 格式)。默認(rèn)是 index 開始的時(shí)間,例如 https://index.golang.org/index?since=2019-04-10T19:08:52.997264Z
limit:返回列表的最大長(zhǎng)度。默認(rèn)值 = 2000,最大值 = 2000,例如 https://index.golang.org/index?limit=10
返回的內(nèi)容示例:
我們看看 pkgsite 是怎么從 index.golang.org 獲取數(shù)據(jù)的。
因?yàn)?Worker 也是一個(gè) HTTP 服務(wù),main.main 中的代碼和 Frontend 類似,我們直接找到拉取 index 的地方:
這是一個(gè)由 Cloud Scheduler 調(diào)用的端點(diǎn)(endpoint),在 Worker 的后臺(tái)界面的 “Enqueue From Module Index” 也是調(diào)用的這個(gè)接口。處理邏輯是:
從 module_version_states 表中根據(jù) index_timestamp 字段降序,拿到最大的一個(gè)時(shí)間戳,作為 since 參數(shù)的值;
使用 since 和 limit 參數(shù)請(qǐng)求 index.golang.org/index,獲取新模塊;
將獲取的數(shù)據(jù)寫入 module_version_states 表;
將獲取的數(shù)據(jù)放入隊(duì)列,方便獲取包的具體信息;
本地運(yùn)行時(shí)(非 GAE 上),隊(duì)列使用的內(nèi)存隊(duì)列(滿足定義的 Queue 接口),具體使用的是 Channel,默認(rèn)容量是 1000:
因此在入隊(duì)列后,process 這個(gè)處理程序會(huì)進(jìn)行處理:
這里獲取具體包信息時(shí),開啟一個(gè) goroutine 進(jìn)行處理,為了避免 goroutine 數(shù)量不可控,Worker 提供了一個(gè) flag,用于控制 goroutine 的數(shù)量,默認(rèn)是 10,這里 q.sem 就是用于控制 goroutine 數(shù)量的。
之后就是通過 Module Proxy 獲取包信息并進(jìn)行處理。這塊涉及模塊代理協(xié)議[38]的內(nèi)容,有興趣的可以自己閱讀源碼了解,同時(shí)參考一個(gè)開源模塊代理實(shí)現(xiàn):https://github.com/goproxy/goproxy.cn。
標(biāo)準(zhǔn)庫是如何獲取的
講到這里,還有一個(gè)問題沒解決,那就是標(biāo)準(zhǔn)庫,它并不會(huì)走上面 Worker 提到的處理邏輯。
這用于獲取標(biāo)準(zhǔn)庫版本,需要手動(dòng)執(zhí)行,而且 Go 每發(fā)布一個(gè)版本,應(yīng)該執(zhí)行一次。請(qǐng)求這個(gè)接口后,會(huì)通過 git 獲取 Go 倉(cāng)庫信息,這個(gè)倉(cāng)庫不是 GitHub 上的,而是 https://go.googlesource.com/go,上面提到了,國(guó)內(nèi)訪問不了,不過可以改代碼換成 https://github.com/golang/go。
標(biāo)準(zhǔn)庫具體的處理邏輯:
通過 https://github.com/go-git/go-git 這個(gè) Git 操作庫,獲取 Go 目前所有的版本信息;
針對(duì)每一個(gè)版本,循環(huán)交由隊(duì)列處理,這里和第三方庫是一樣的;
之后的處理流程和第三庫類似,只是獲取數(shù)據(jù)時(shí)的來源不同;
注意:由于目前 Go 版本較多,這個(gè)過程會(huì)比較耗時(shí),甚至?xí)惺〉那闆r。
依然吐槽
除了以上還有其他接口需要手動(dòng)執(zhí)行,如構(gòu)建搜索文檔等。細(xì)究代碼后,再一次發(fā)現(xiàn) Worker 設(shè)計(jì)的不合理。明明是一個(gè)后臺(tái)任務(wù)處理器,非得依賴第三方定時(shí)任務(wù)來觸發(fā),有些還必須手動(dòng)觸發(fā)。
總結(jié)
整個(gè)項(xiàng)目的分析就差不多結(jié)束了。雖然 internal 下面的包一堆,畢竟業(yè)務(wù)不復(fù)雜。如果你對(duì)其中的細(xì)節(jié)感興趣,可以跟著代碼仔細(xì)研究??梢越柚?Goland 這樣的 IDE 進(jìn)行調(diào)試,跟蹤關(guān)鍵接口是如何執(zhí)行的,這樣有利于快速了解項(xiàng)目的整體結(jié)構(gòu)。
文章較長(zhǎng),希望看到這里你能有所收獲!沒有開打賞,給個(gè)轉(zhuǎn)發(fā)或在看或留言就是對(duì)我最大的支持!沒有關(guān)注的關(guān)注下!

參考資料
[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]模塊代理協(xié)議(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]模塊代理協(xié)議: https://docs.studygolang.com/cmd/go/#hdr-Module_proxy_protocol