Go未來演進:基于共同目標和數據驅動的決策
自從Go語言之父Rob Pike從Google退休并隱居澳洲后,Russ Cox便成為了Go語言團隊的“帶頭大哥”,雖然其資歷還無法與依舊奮戰在一線的另外一位Go語言之父Robert Griesemer相比。如今,Russ Cox對Go語言未來的演化發展是很有“發言權”的,Go module的引入便是Russ Cox的重要決策之一。從Go社區來看,這些年來,以Russ Cox為首的Go團隊對Go演進決策總體上是良性的、受歡迎的,比如Go module、Go泛型、Go對wasm的支持等,當然也有一些變化是受到質疑的,比如:Go 1.22版本很可能從試驗特性到正式特性的loopvar等[1]。
想必很多Gopher也和我一樣,對Go團隊就某一proposal的決策方式和依據很好奇 --到底他們是如何決定是否accept這個proposal的?Go語言后續該如何演化?向哪個方向發展演化?
今年9月份舉辦的GopherCon 2023上,Russ Cox代表Go團隊做了名為“Go Changes”的主題演講[3]:
圖片
在這個talk中,我們能找到一些答案。近期他重新錄制了該演講視頻[4],并在其個人博客中放出。
本文就是基于這個視頻內容進行整理加工后的文字稿,供國內廣大gopher參考。
這是我在2023年GopherCon上做的一次演講的重新錄制視頻。在這次演講中,我和大家分享了三部分內容:為什么Go需要隨著時間的推移而改變,我們如何應對Go的變化過程,以及為什么選擇性遙測(opt-in telemetry)[5]是這個過程中的一個重要且適當的部分。不過,這個演講不是關于某個特定的Go特性變化,而是關于Go整體的變化過程,特別是我們是如何決定做出哪些改變的。
首先一個明顯的問題是,為什么Go需要改變? 為什么我們不能對Go感到滿意,然后將其束之高閣呢? 一個顯而易見的答案是我們不可能一次就把事情做對,你對比一下上面圖片中展示的第一版毛絨Go吉祥物和我們在GopherCon上發放的最終版本,你就能明白我的意思了。
但這里還有一個更深層次的答案:
圖片
我的一位前同事在他使用了多年的郵件簽名中引用了生物學家兼科幻小說作家杰克·科恩(Jack Cohen)的一句名言。在這句名言中,科恩說:“我們生物學家使用的一個描述‘穩定(stable)’的專業詞匯就是“死(dead)”。
所有的生命都在變化,適應新的環境,修復損傷等。編程環境[6]也需要改變。除非我們想要Go死掉,否則它需要適應新的環境,比如新的協議、操作系統和重要用例。我們也需要發現并修復bug -- 語言、庫和生態系統的問題,這些問題只有隨著時間的推移或Go發展到一定階段和規模才會暴露出來。
Go必須改變,并與時俱進。這次演講就是關于我們如何決定做出哪些改變。
圖片
這次演講分三個部分:
- 第一部分是關于我們對Go的愿景和期望。
- 第二部分是關于我們如何利用數據來決定做出哪些改變。
- 第三部分是關于我們在Go工具鏈[7]中增加選擇性遙測的計劃,以便更好地理解Go的使用情況和出現問題的地方。
到演講結束時,你將了解我們考量和決定Go變化的過程,并了解數據在做出這些決定中的重要性,我希望你能理解為什么選擇性遙測是一個很好的額外數據來源,甚至可能愿意在系統推出時就選擇加入。
圖片
讓我們從這個開始:我們希望Go發生什么樣的變化?如果我們在這個基本問題上意見不一致,我們也就無法就具體的變化達成共識。
例如,我們是否應該在Go中添加一個Perl語句,讓我們可以用Perl編寫函數?
圖片
我認為我們不應該,但假設你有不同意見。為了解決這個問題,我們需要理解為什么我們持不同意見。
約翰·奧斯特豪特(John Ousterhout)寫了一份名為“開放決策制定(Open Decision Making)[8]”的好文檔,內容雖然來自他在創業公司的經驗,但它幾乎完全適用于開源項目。
圖片
在這份文檔中,他提出的最重要的觀點之一是:如果一群聰明人面對同一個問題,并具有相同的信息,如果他們有相同的目標,那么他們很可能得出相同的結論。
如果你和我在Go中是否要嵌入Perl這個問題上存在分歧,根本原因肯定是我們對Go目標有不同的理解,所以我們必須建立明確Go的目標。
圖片
Go的目標是更好的軟件工程,特別是大規模軟件工程。Go的獨特設計決策幾乎全部針對這個目標[9]。我們已經多次闡述過這一點,包括在上述截圖中的這兩篇文章中[10]。再說一次,Go的目標是更好的軟件工程。
現在我們來說說Perl。20年前,當我很年輕、甚至有些天真、Go還不存在的時候,我編寫并部署了一個完全用Perl編寫的大型分布式系統。我熱愛Perl所擅長的東西,但它并不是以更好的軟件工程為目標。如果我們在這一點上有分歧,那么我可能應該定義一下我所說的軟件工程是什么意思。
圖片
注:如果要理解Go以更好軟件工程為目標,或是Google的軟件工程理念,可以閱讀一下《Software Engineering at Google[11]》這本佳作。
我喜歡說,當你給編程加入時間和其他程序員時,軟件工程就出現了。編程意味著讓一個程序工作。你有一個要解決的問題,你編寫一些代碼,運行它,調試它,得到答案,完成。這就是編程,這已經夠難的了。
圖片
但是當那段代碼不得不日復一日地繼續工作時會發生什么,甚至和其他人一起對它進行維護?那么你需要添加測試,以確保你修正后的bug不會在6個月后由你自己或是一個不熟悉這段代碼的新團隊成員重新引入。這就是為什么Go從第一天開始就內置了對測試的支持,并建立了一種文化,那就是對任何bug的修復或新增代碼都要添加測試。
那么隨著時間的流逝,當代碼必須在Go本身發生改變的情況下繼續工作時會發生什么?那么我們需要強調兼容性[12],這是Go1版本以來一直在做的。事實上,Go 1.21版本[13]發布了許多兼容性改進[14],我在2022年的GopherCon上對此有過介紹。
隨著代碼量的增長,如果需要某種全局清理時該怎么辦?你需要工具,而不可避免的第一個絆腳石是那些工具需要模仿代碼的格式化風格來編輯,以避免出現無關的差異。gofmt的存在是為了支持goimports、gorename、go fix和gopls等工具,以及你自己可能使用我們提供的包編寫的自定義工具。
既然提到了軟件包,當你使用其他人提供的軟件包時,不可避免的第一個絆腳石是多個人會用相同的名字(比如sqlite或yaml)編寫軟件包。那么我們如何在一個給定的程序中識別究竟使用哪個了呢?為了在一個去中心化的方式無歧義地回答這個問題,Go使用URL作為包導入路徑。
隨著時間的推移,下一個問題是挑選使用特定軟件包的哪個版本,并決定該版本是否與所有其他依賴項兼容。這就是為什么Go提供了modules、workspaces[15]、Go modules mirror鏡像和Go module校驗和數據庫。
接下來的問題是每個人的代碼都有bug,包括安全bug。你需要了解關于最重要bug的信息,這樣你就知道需要更新到已修復的版本。這就是為什么我們添加了Go漏洞數據庫和govulncheck[16],Julie也在GopherCon上談到了這一點,當有視頻鏈接時我會在下面添加。
以上是較大的例子,但也有小的例子,比如添加新的協議如HTTP/3,移除對過時平臺的支持,以及修復或廢棄容易出錯的API,以避免大型代碼庫中的常見錯誤。
這把我們帶到了Go提案過程(Proposal Process),這是我們對是否接受(accept)和拒絕(decline)哪些變更做出決定的方式:
圖片
當我們考慮這些決定時,使用數據非常重要,這可以幫助我們達成共識。
簡單地說,任何人都可以在Go的GitHub問題跟蹤器上提出Go更改提案(Change Proposal)。然后,在該問題上進行討論,我們試圖在參與者之間就是否接受或拒絕該建議達成共識,或者該建議需要做出什么修改才能被接受。
隨著時間的推移,我們越來越欣賞約翰·奧斯特豪特在他的觀察中提出的第二句話的重要性:如果面對問題的人不僅共同的目標,還有共同的信息,他們很可能會達成共識。
圖片
在Go的早期,只有我們幾個人做決定。我們根據技術判斷和直覺做出決定,這些判斷和直覺是基于我們過去的經驗。那些經驗就是我們使用的信息。由于我們的過去經驗有足夠的重疊,我們大多數時候能達成共識。大多數小項目都是這種工作方式。
隨著決策涉及的人數大大增加,共享經驗就會減少。我們需要一個新的共享信息來源。我們發現的最好信息來源是收集實際數據,然后將這些數據作為共享信息來做決策。但是我們從哪里獲得這些數據呢?對Go來說,我們有許多潛在的來源,每一個都適合具體的決策類型。在這里,我將向你展示其中的一些。
一個數據來源是與Go用戶交談。我們以各種方式做到這一點:
圖片
首先是Go用戶調查,我們從2016年開始每年做一次,最近開始一年做兩次。調查非常適合了解Go最流行的用途以及人們面臨的最常見問題。多年來,最常見的問題是缺乏依賴管理和泛型。我們使用這些信息將開發Go模塊和泛型作為優先事項。
另一個數據來源是我們可以在VSCode中使用VSCode Go插件運行的調查。這些調查可以幫助我們了解VSCode Go體驗的實效性。
來自用戶的最后一個直接數據來源是我們全年進行的研究訪談和用戶體驗研究。這些研究允許我們從小規模的用戶群體中識別模式或獲取更多關于特定主題的信息。
調查和訪談通過與用戶交談來收集數據。另一個數據來源是閱讀代碼:我們可以分析已發布的開源Go module代碼。
例如,在添加新的“go vet”檢查之前,我們會在開源代碼庫的一個子集上運行它,然后讀取一些隨機樣本的結果,看檢查是否指出了真實的問題,以及它是否有太多的假陽性。
在Go 1.22版本,我們計劃添加一個go vet檢查,檢查對append的調用是否沒有append任何內容。這里有檢查器標記的兩段代碼:
圖片
頂部的一段代碼表明開發人員可能認為append總是復制其輸入slice。底部的一段代碼可能是正確的,但難于措辭來描述。
這里還有另外兩段代碼:
圖片
在頂部的一段中,或者for循環從未運行,或者它永遠不會完成,因為e.Sigs的長度永遠不會改變。底部的代碼也似乎是一個清晰的bug:代碼正在仔細決定將消息追加到哪個列表中,然后它沒有將其追加到任何一個列表中。
由于我們對樣本代碼段進行的所有采樣都是可疑的或完全錯誤的,我們決定添加該檢查。在這里,數據比直覺更好。
所有這些方法都是在少量樣本上工作。對于典型的代碼分析,我喜歡手動檢查100個樣本,與世界上所有Go代碼的量相比,這只是一個微小的比例。最后一份Go開發者調查有不到6000名受訪者,而全世界可能有300萬Go開發者,樣本比例不到1%。
一個很好的問題是為什么這些極小的樣本能告訴我們有關更大人群的信息?答案是抽樣精度只依賴于樣本數量,而不依賴于總體規模。
圖片
這乍一看似乎反直覺,但假設我有一個裝有100萬只Go吉祥物的大箱子,我隨機拿出兩個。首先我拿到一個藍色的,然后我拿到一個粉紅色的。根據這兩個樣本,我估計箱子中的吉祥物大約一半是藍色的,一半是粉紅色的。但如果我告訴你箱子里有粉紅色、藍色和灰色的吉祥物,你是否會感到十分驚訝? 不會非常驚訝!如果箱子正好分三分之一粉紅色、藍色和灰色,那么這9對顏色組合中的每一對都同樣可能:
圖片
得到一個非灰色吉祥物的機會是2/3,得到兩個的機會就是2/3的平方,即4/9。沒看到灰色的情況出現概率將近一半。這就是為什么我們不會非常驚訝的原因。
現在假設我取出100只,有48只藍色和52只粉紅色。我再次估計箱子大約一半是藍色,一半是粉紅色。現在如果我告訴你箱子里有粉紅色、藍色和灰色的吉祥物,你會有多驚訝?你應該會非常驚訝。
事實上,你完全不應該相信我。如果那是真的,得到100只連續的非灰色吉祥物的機會是2/3的100次方,約等于10的負48次方:
圖片
隨機出現這種情況的可能性為零。要么我在說謊,要么我沒有隨機抽取。可能所有的灰色吉祥物都在箱子底部,我沒有抽取到足夠深的地方。
請注意:這都不依賴于箱子中有多少只Go吉祥物,它只取決于我們取出了多少只。用于特定預測精度的數學更復雜,但具有相同的效果:只有樣本數量重要,箱子中的吉祥物數目不重要。
一般來說,手工計算這些數學太困難了,所以這里有一個表格,你可以在我的博客上找到:
圖片
它說明,如果你提取100個樣本并根據這些樣本估計百分比,那么90%的時間你的估計將在真實百分比的正負8%之內。99%的時間它們將在13%之內。如果像Go調查中那樣有5000個樣本,那么90%的時間估計誤差在正負1%之內,99%的時間在正負2%之內。超過這個數量,我們實際上不需要更多樣本。
有一個注意事項是樣本需要是隨機的, 或者至少與你正在估計的內容不相關。你不能只從箱子的頂部抽取吉祥物,然后對整個箱子做出斷言。
圖片
如果你避免了這個錯誤, 那么當你試圖估計一個新的API是否有用或者某個特定的vet check是否值得的時候, 花一個小時左右手動檢查100個樣本是合理的。如果是一個壞主意, 那將很快顯現出來。而如果看起來是一個好主意, 再花幾個小時檢查更多的樣本, 無論是手動檢查還是用程序檢查,都會大大提高你的估計準確性。與做出錯誤決策的代價相比,這是一個非常小的成本。
簡而言之,采樣的魔力在于將許多一次性估計轉變為可以手動或用少量數據完成的工作。這就是為什么我們已經看到的所有數據來源都能夠相當好地代表整個Go開發者群體的原因。
現在進入演講的第三部分:Go工具鏈中的遙測(Telemetry):
遙測也將是Go開發者使用的一個小樣本,但它應該是一個有代表性的樣本,并且回答不同的問題,而不是調查和代碼分析所做的問題。
遙測始終是一個有爭議的話題,特別是對于開源項目來說,所以讓我從最重要的細節開始說起:上傳遙測報告是完全自愿和選擇加入的:
圖片
除非你運行一個顯式命令選擇加入數據收集,否則不會上傳任何數據。而且,這不是那種上傳你的全部活動的詳細跟蹤的遙測系統。這種遙測也只適用于我們作為Go發行版的一部分分發的命令,比如gopls、go命令和編譯器(compiler),它不會涉及你構建的任何程序。
在我更詳細地描述完這個系統之后,我希望你會發現你會愿意選擇加入這個遙測系統。實際上,我們給自己設定的主要設計限制是,即使由其他人運行,我們也愿意選擇加入該系統。
在我以2023年11月的錄制這個內容時,該系統剛剛開始運行,只有少數人被要求在VSCode Go中選擇加入gopls遙測。所以總體來說,你現在還不能選擇加入。但希望很快你就可以了。
在我們深入了解細節之前,遙測的動機是它提供了與調查和代碼分析不同的信息。它主要提供的兩個類別是使用信息(Usage Information)和故障信息(Breakage Information)。調查讓我們能夠詢問關于Go使用的廣泛問題,但對于詳細的使用信息來說并不好。那將是太多問題,對于調查對象來說,90%的問題要回答"no"是一種浪費時間。
圖片
這個幻燈片顯示了我們在之前的版本中警告過即將刪除的Go功能列表。列表中的最后一項,buildmode=shared,是我們試圖移除的功能,但在事先警告后,至少有一個用戶提出了異議,我們將其保留了下來。即便如此,buildmode=shared與Go module基本不兼容,所以它的使用可能非常有限。但我們沒有數據,所以它仍然存在于代碼庫中。遙測可以為我們提供基本的使用信息,以便我們可以基于數據而不是猜測做出這些決策。
另一個重要的類別是故障信息:
如果Go工具鏈明顯有問題,我們希望在GitHub上收到錯誤報告。但是Go工具鏈也可能以用戶注意不到的微妙方式出現問題。一個例子是,在macOS上的Go 1.14到Go 1.19的版本中,標準庫包的二進制文件在預先構建時使用了非默認的編譯標志,這是一個意外,這使得它們看起來像是過時了,Go命令在運行時會重新編譯它們,這意味著如果你的程序導入了net包,你需要安裝Xcode中的C編譯器來構建程序。我們希望Go能夠自行構建純Go程序,而無需其他工具鏈。因此,要求安裝Xcode是一個bug。但是我們沒有注意到這個問題,也沒有用戶在GitHub上報告它。遇到這個問題的人似乎只是安裝了Xcode并繼續進行了工作。遙測可以提供基本的性能指標,比如標準庫緩存命中率,這樣Go工具鏈的開發人員即使用戶沒有意識到這個問題,也能注意到這個問題。
另一個例子是編譯器的內部崩潰:
圖片
Go編譯器在程序的第一個錯誤處不會停止。它會繼續進行,盡可能多地查找和報告不同的錯誤。但是有時,繼續分析已知錯誤的程序會導致意外的panic。我們不希望向用戶顯示這樣的崩潰。相反,編譯器會從panic中恢復,并且僅報告已經發現的錯誤。這樣,Go用戶可以糾正這些錯誤,這也可能糾正隱藏的panic。用戶的工作不會因為看到編譯器崩潰而中斷。這對用戶來說是好的,但是Go工具鏈的開發人員仍然希望了解這個崩潰并修復這個錯誤。遙測可以確保即使用戶不知道這個錯誤,但我們還能了解到這個錯誤。
為了收集使用情況和故障信息,Go遙測設計記錄“計數器和崩潰”:
像go命令、Go編譯器或gopls這樣的Go工具鏈程序可以定義命名事件計數器,并在事件發生時遞增計數器。事件還可以按堆棧跟蹤單獨計數。這些計數器在本地的磁盤文件中維護,每次保留一周的時間。在幻燈片上,gopls和其他工具正在將計數器寫入每周的文件中。
每周一次,Go工具鏈中的上傳程序(uploader)將從遙測服務器獲取一個“上傳配置”,其中列出了該周收集的特定事件名稱。只有在遙測特定的提案審查過程達成共識后,才會更改該配置。該配置作為一個模塊(module)提供,以保護下載的完整性,并保留過去配置的公共記錄。然后,上傳程序僅上傳上傳配置中列出的計數器。在幻燈片上,上傳程序僅為gopls發送一份報告,僅包含少量計數器,即使磁盤上可能還有更多計數器。報告中包含關于使用gopls的編輯器的統計信息,以及關于完成請求的延遲的信息,還有一個發生了一次的gopls/bug事件,其中包含一個棧跟蹤。
請注意,上傳的數據中沒有事件跟蹤或任何用戶數據,只有計數器、已在公共上傳配置中列出的事件名稱,以及Go工具鏈程序中的函數名稱。還要注意,棧跟蹤不包括任何函數的參數,只有函數名稱,因此沒有用戶數據。
開源中的遙測可能會在擁有數據訪問權限和沒有數據訪問權限的人之間產生信息失衡。我們希望避免這種情況。請記住奧斯特豪特規則:為了達成共識,我們需要每個人擁有相同的信息。由于Go的遙測上傳不包含任何敏感數據,并且是在明確的選擇同意的情況下收集的,我們可以完整地重新發布這些報告,以便任何人都可以進行任何數據分析。我們還將發布一些基本的圖表,用于做出決策。我們唯一可能看到但沒有重新發布的是報告來自哪些IP地址,我們的服務器會將這些信息與報告一起記錄。
一個明顯的問題是,是否有足夠多的人選擇啟用遙測,以使數據足夠準確以做出決策。幸運的是,采樣的神奇之處在于可以幫助解決這個問題。
圖片
全球大約有300w Go開發者。當系統準備就緒并要求人們啟用遙測時,即使只有千分之一的開發者選擇參與,也會有3000名開發者,根據我們的圖表顯示,誤差不到3%,置信度為99%。如果全球三分之二的Go開發者啟用了遙測,那將是20000個樣本,誤差不到1%,置信度為99%。除此之外,我們實際上不需要更多的樣本。如果我們持續獲得更多的報告,我們可以調整上傳配置,告訴系統在某個特定的周選擇隨機不上傳任何東西。例如,如果有20萬個系統選擇了參與,我們可以告訴每個系統在任何給定的周上傳的概率為10%。因此,即使我們預計選擇參與率會很低,系統應該能夠運行得很好,隨著選擇參與率的提高,Go遙測將從任何給定系統收集更少的數據。當然,這使得每個選擇參與的人對我們來說更加重要。目前來說,Go遙測對于你們中的任何人來說都還沒有準備好,但當準備好時,我希望你們會選擇參與。
在結束之前,我希望你們從演講中獲得以下幾點:
首先,Go需要不斷變化,特別是隨著計算世界的變化。
其次,任何改變的目標都是為了使Go在軟件工程中變得更好,尤其是在規模化(scaling)方面。
第三,一旦我們確定了目標,達成共識的下一個最重要的部分是擁有共享數據來做出決策。
第四,Go工具鏈遙測是增補我們現有調查和代碼分析數據的重要數據來源。
最后,在整個演講中,雖然涉及到了數據和適當的統計,但我們評估的想法、假設和潛在的變化始終始于個人故事和對話。我們喜歡聽到這些故事,并與你們所有人討論如何使用Go,關于什么有效和什么無效。所以,請無論在什么情況下,無論是在會議上、郵件列表上還是在問題跟蹤器上,請確保讓我們知道Go對你們的工作情況以及存在的問題。我們總是很樂意聽到這些。非常感謝。