這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
本文譯自 Semantic Import Versioning, Go & Versioning 的第 3 部分, 著作權@歸原文所有.
如何將不相容的更改部署到現有軟體包 ? 這是任何包管理系統中的根本挑戰和決斷.問題的答案決定了所產生的系統的複雜性, 它決定了如何輕鬆或難以使用包管理.(它還決定如何輕鬆或難以實現包管理, 但使用者體驗更重要.)
為了回答這個問題, 這篇文章首先介紹了 Go 的匯入相容性規則:
如果舊包和新包具有相同的匯入路徑, 新軟體包必須向後相容舊軟體包.
我們從 Go 一開始就主張這個原則, 但我們沒有給它一個名字或者這樣一個直接的陳述.
匯入相容性規則大大簡化了使用不相容版本的軟體包的體驗. 當每個不同版本具有不同的匯入路徑時, 關於給定匯入語句的預期語義沒有歧義.這使開發人員和工具更容易理解 Go 程式.
今天的開發人員希望使用語義版本來描述軟體包, 因此我們將它們應用到模型中.具體來說, 模組 my/thing 被匯入成 my/thing 作為 v0, 該階段預計會發生破壞性更改, 並且不會受到保護, 並且持續到第一個穩定的主要版本 V1.但是當添加 v2 的時候, 我們不再重新定義現在穩定的 my/thing 的所代表的含義, 而是給它一個新的名字: my/thing/v2.
我將這種約定稱為語義匯入版本控制, 這是在使用語義版本控制時遵循匯入相容性規則的結果.
一年前, 我相信把版本放入這樣的匯入路徑是醜陋的, 不可取的, 並且可能是可以避免的.但是在過去的一年裡, 我已經開始理解它們為系統帶來多少清晰和簡單.在這篇文章中, 我希望能讓你瞭解我為什麼改變主意.
一個依賴的故事
為了使討論具體化, 請考慮以下故事. 這個故事當然是虛構的, 但它是由一個真正的問題所驅動.當 dep 發布時, Google 編寫 OAuth2 軟體包的團隊問我, 他們應該如何引入他們長期以來都想做的一些不相容的改進.我越想它, 越是意識到這不像聽起來的那麼容易, 至少不像沒有語義匯入版本的那樣.
序幕
從包管理工具的角度來看, 分為代碼作者和代碼使用者. Alice, Anna 和 Amy 是不同程式碼封裝的作者.Alice 在 Google 工作並編寫 OAuth2 軟體包. Amy 在微軟工作並編寫了 Azure 用戶端庫. Anna 在亞馬遜工作並撰寫了 AWS 用戶端庫.Ugo 是所有這些軟體包的使用者. 他正在開發最終的雲應用 Unity, 並使用所有這些軟體包和其他軟體包.
作為作者, Alice, Anna 和 Amy 需要能夠編寫和發布他們軟體包的新版本. 軟體包的每個版本都為其每個依賴項指定了所需的版本.
作為使用者, Ugo 需要能夠用這些其他軟體包一起構建 Unity. 他需要精確控制在特定構建中使用哪些版本; 當他選擇時他需要能夠更新到新版本.
當然, 我們的朋友可能期望從包管理工具中獲得更多, 特別是在發現, 測試, 可移植性和有用的診斷方面, 但這些與故事無關.
隨著我們的故事逐漸開啟, Ugo 的 Unity 構建依賴關係看起來像這樣:
章節 1
每個人都獨立編寫軟體.
在Google, Alice 一直在為 OAuth2 軟體包設計一個新的, 更簡單, 更便於使用的 API. 它仍然可以完成舊軟體包可以完成的所有工作, 但只需要一半的 API 介面.她將其發布為 OAuth2 r2. (這裡的 r 代表修訂. 目前, 修訂編號並不表示除順序之外的任何內容: 特別是, 它們不是語義版本)
在微軟, Amy 正處於應有的長假期中, 她的團隊決定在她回來之前不做任何與 OAuth2 r2 相關的更改. Azure 包現在將繼續使用 OAuth2 r1.
在亞馬遜, Anna 發現使用 OAuth2 r2 可以讓她在實現 AWS r1 的過程中刪除許多難看的代碼, 因此她將 AWS 更改為使用 OAuth2 r2. 她一路修複了一些錯誤, 並將結果發布為 AWS r2.
Ugo 擷取了有關 Azure 行為的 bug 報告, 並追蹤到 Azure 用戶端庫的一個 bug. 在休假之前, Amy 已經在 Azure r2 中發布了該 bug 的修複程式.Ugo 向 Unity 添加了一個測試案例, 確認它失敗, 並要求包管理工具更新到 Azure r2.
更新之後, Ugo 的構建看起來像這樣:
他確認新的測試通過, 並且他所有的舊測試仍然通過. 他鎖定 Azure 更新並發布更新後的 Unity.
章節 2
Amazon 大張旗鼓地推出了他們新的雲端服務: Amazon Zeta Functions. 為了準備發布, Anna 給 AWS 軟體包添加了 Zeta 支援, 她現在將它發布為 AWS r3.
當 Ugo 聽到有關 Amazon Zeta 的訊息時, 他寫了一些測試程式, 並對他們工作的效果感到非常興奮, 因此他跳過午餐更新去 Unity. 今天的更新不如最後一次.Ugo 希望使用 Azure r2 和 AWS r3 (每個版本的最新版本)來構建包含 Zeta 支援的 Unity. 但 Azure r2 需要 OAuth2 r1 (而不是 r2 ), 而 AWS r3 需要 OAuth2 r2 (而不是 r1).經典的菱形依賴, 對吧? Ugo 不在乎它是什麼. 他只是想構建 Unity.
更糟的是, 這似乎不是任何人的錯. Alice 寫了一個更好的 OAuth2 包. Amy 修複了一些 Azure bug 並去度假. Anna 覺得 AWS 應該使用新的 OAuth2 (內部實現細節), 並且後來增加了 Zeta 支援.Ugo 希望 Unity 使用最新的 Azure 和 AWS 軟體包. 很難說他們中的任何一個做錯了什麼. 如果這些人沒有錯, 那麼也許包管理錯了. 我們一直假設在 Ugo 的 Unity 構建中只能有一個版本的 OAuth2.也許這就是問題所在: 也許包管理器應該允許在單個構建中包含不同的版本. 這個例子似乎表明它必須這樣.
Ugo 仍然卡住了, 所以他搜尋了 StackOverflow 並找到了包管理器的 -fmultiverse標誌, 它允許多個版本, 以便他的程式構建為:
Ugo 試了. 它不起作用. 進一步深入研究這個問題, Ugo 發現 Azure 和 AWS 都在使用名為 Moauth 的流行 OAuth2 中介軟體庫, 它簡化了部分 OAuth2 處理. Moauth 不是一個完整的 API 替代品: 使用者仍然直接匯入 OAuth2, 但他們使用 Moauth 來簡化一些 API 呼叫.Moauth 參與的細節沒有從 OAuth2 r1 改為 r2, 因此 Moauth r1 (唯一存在的版本)與兩者相容. Azure r2 和 AWS r3 都使用 Moauth r1. 在僅使用 Azure 或僅使用 AWS 的程式中, 這種方式效果很好, 但 Ugo 的 Unity 構建看起來像這樣:
Unity 需要兩個 OAuth2 拷貝, 但 Moauth 匯入哪個拷貝 ?
為了使構建工作, 我們似乎需要兩個完全相同的Moauth副本: 一個匯入 OAuth2 r1 供 Azure 使用, 另一個匯入 OAuth2 r2 供 AWS 使用.一個快速的 StackOverflow 搜尋顯示軟體包管理器有一個標誌(flag): -fclone. Ugo 的程式使用這個標誌來構建為:
這實際上可以工作並通過了測試, 雖然 Ugo 現在想知道是否還有更多的潛在問題. 他需要回家吃晚飯.
章節 3
Amy 已經結束度假回到了微軟. 她覺得 Azure 可以繼續使用 OAuth2 r1 一段時間, 但她意識到它可以協助使用者直接傳遞 Moauth 令牌到 Azure API.她以向後相容的方式將其添加到 Azure 包中, 並發布 Azure r3. 在亞馬遜這邊, Anna 喜歡 Azure 包基於 Moauth 的新 API, 並向 AWS 包添加了類似的 API, 發布為 AWS r4.
Ugo 看到了這些變化, 並決定更新到最新版本的 Azure 和 AWS, 以便使用基於 Moauth 的 API. 這次卡了他一個下午. 首先他暫時更新 Azure 和 AWS 包, 而不修改 Unity. 它的程式可以構建!
令人興奮的是, Ugo 將 Unity 改變為使用基於 Moauth 的 Azure API, 而且也可以構建. 但是, 當他將 Unity 更改為使用基於 Moauth 的 AWS API 時, 構建失敗.困惑的是, 他恢複了他的 Azure 更改, 只留下 AWS 更改, 構建成功. 他將 Azure 更改放回, 構建再次失敗. Ugo 返回到 StackOverflow (繼續搜尋).
Ugo 瞭解到, 當通過 -fmultiverse -fclone 僅使用一個基於 Moauth 的 API (在本例中為 Azure) 時, Unity 隱式地構建為:
但是當他使用兩個基於 Moauth 的 API 時, Unity 中的單個匯入 "moauth" 是不明確的. 由於 Unity 是主要的軟體包, 它不能被複製(與 Moauth 本身相反):
一個 StackOverflow 評論建議將 Moauth 匯入移動到兩個不同的包中, 然後讓 Unity 匯入它們. Ugo 嘗試了這一點, 令人難以置信的是, 它可以工作:
Ugo 按時回了家. 他對包管理並不滿意, 但他現在是 StackOverflow 的忠實粉絲.
基於語義版本的複述
假設包管理使用它們而不是原始故事的 'r' 數字, 讓我們揮動一把魔杖並用語義版本複述故事.
以下是一些變化:
OAuth2 r1 變為 OAuth2 1.0.0
Moauth r1 變為 Moauth 1.0.0
Azure r1 變為 Azure 1.0.0
AWS r1 變為 AWS 1.0.0
OAuth2 r2 變為 OAuth2 2.0.0 (部分不相容的 API)
Azure r2 變為 Azure 1.0.1 (bug 修複)
AWS r2 變為 AWS 1.0.1 (bug 修複, 內部使用 OAuth2 2.0.0)
AWS r3 變為 AWS 1.1.0 (功能更新: 添加 Zeta)
Azure r3 變為 Azure 1.1.0 (功能更新: 添加 Moauth API)
AWS r4 變為 AWS 1.2.0 (功能更新: 添加 Moauth API)
故事沒有任何變化. Ugo 仍然遇到相同的構建問題, 他仍然不得不轉向使用 StackOverflow 來瞭解構建標誌(flag)和重構技術, 以保持 Unity 成功構建.根據 semver, Ugo 應該沒有任何更新的麻煩: 在故事中沒有一個 Unity 匯入的包改變了主要版本. 只有 OAuth2 深入 Unity 的依賴樹. Unity 本身不會匯入 OAuth2. 什麼地方出了錯 ?
這裡的問題是, semver 規範實際上不僅僅是選擇和比較版本字串的方式. 它沒有說別的. 特別是, 在增加主要版本號後, 如何處理不相容的更改也沒有提及.
semver 最有價值的部分是鼓勵在可能的情況下進行向後相容的更改. FAQ 正確的記錄到:
"不相容的更改不應該輕微引入具有大量相關代碼的軟體. 升級必須承擔的成本可能很大. 不得不通過增加主要版本號來發布不相容的更改意味著你將會考慮更改的影響並評估涉及的 成本/收益 率."
我當然同意 "不應該輕易引入不相容的變化". 我認為 semver 缺乏的地方是: "不得不通過增加主要版本號" 是促使你 "思考你更改的影響並評估涉及的 成本/收益 率" 的一個步驟.恰恰相反: 讀取 semver 太容易了, 因為這意味著只要你在進行不相容的更改時遞增主要版本, 其他所有操作都可以解決. 這個例子表明情況並非如此.
從 Alice 的角度來看, OAuth2 API 需要向後相容的變更, 並且當她做出這些更改時, semver 似乎承諾發布不相容的 OAuth2 軟體包會很好, 前提是她給了它 2.0.0 版本.但是這種經過 semver 認可的改變引發了 Ugo 和 Unity 的一系列問題.
語義版本是作者向使用者傳達期望的重要方式, 但這就是他們的全部. 就其本身而言, 不能期望解決這些較大的構建問題. 相反, 讓我們看看解決構建問題的方法. 之後, 我們可以考慮如何在這種方法中恰當的使用 semver.
匯入版本控制複述
再一次, 讓我們使用匯入相容性規則重新講述故事:
在 Go 中, 如果舊包和新包具有相同的匯入路徑, 新軟體包必須向後相容舊軟體包.
現在情節變化更加顯著. 故事以同樣的方式開始, 但在第 1 章中, 當 Alice 決定建立一個部分不相容的新 OAuth2 API 時, 她不能使用 "oauth2" 作為其匯入路徑. 相反, 她將新版本命名為 Pocoauth, 並為其提供匯入路徑 "pocoauth".
面對兩個不同的 OAuth2 軟體包, Moe (Moauth 的作者) 必須為 Pocoauth 編寫第二個軟體包 Moauth, 他命名為 Pocomoauth 並給出了匯入路徑 "pocomoauth".
當 Anna 將 AWS 軟體封裝更新為新的 OAuth2 API 時, 她還將該代碼中的匯入路徑從 "oauth2" 更改為 "pocoauth", 並將 "moauth" 中的匯入路徑更改為 "pocomoauth". 然後, 隨著 AWS r2 和 AWS r3 的發布, 故事會繼續進行.
在第 2 章中, 當 Ugo 熱切地採用 Amazon Zeta 時, 一切正常. 所有軟體包代碼中的匯入都完全符合需要構建的代碼. 他不必在 StackOverflow 上尋找特殊標誌(flag), 而且他午餐僅僅晚了五分鐘.
在第 3 章中, Amy 將基於 Moauth 的 API 添加到 Azure, 而 Anna 則將相同的基於 Pocomoauth 的 API 添加到 AWS.
當 Ugo 決定更新 Azure 和 AWS 時, 再次沒有問題. 他更新的程式不需要任何特殊的重構:
在這個故事版本的末尾, Ugo 甚至都沒有想到他的包管理器. 它正常工作; 他幾乎沒有注意到它在那裡.
與故事的語義版本翻譯相比, 這裡使用匯入版本化改變了兩個關鍵細節. 首先, 當 Alice 介紹了她向後相容的 OAuth2 API 時, 她不得不將其作為一個新包發布 (Pocoauth). 其次, 由於 Moe 的封裝包 Moauth 在其 API 中公開了 OAuth2 包的類型定義, Alice 發布了一個新包迫使 Moe 也發布了一個新包 (Pocomoauth).Ugo 最終的 Unity 構建進展順利, 因為 Alice 和 Moe 的軟體包拆分建立保持了 Unity 等用戶端構建和運行所需的結構.取而代之的是, Ugo 和像他一樣的使用者不再需要諸如 -fmultiverse -fclone 這樣外來重構輔助的不完整包管理的複雜性, 匯入相容性規則將少量額外的工作推給包作者, 從而讓所有使用者收益.
需要為每個向後不相容的 API 更改引入一個新名稱肯定會有成本, 但正如 semver FAQ 所述, 該成本應鼓勵作者更清楚地考慮這些變化的影響以及它們是否真的有必要.而在匯入版本控制的情況下, 成本會為使用者帶來顯著的收益.
匯入版本控制 (Import Versioning) 的一個優點是程式包名稱和匯入路徑是 Go 開發人員能很好理解的概念. 如果你告訴一個包作者, 做出向後不相容的更改需要建立具有不同匯入路徑的不同包, 那麼 - 即便沒有任何版本控制的知識 - 作者可以通過對用戶端包的影響推理: 用戶端需要更改他們的匯入一次; Moauth 不再適用於新的軟體包, 等等.
能夠更清楚地預測對使用者的影響, 作者可能會對他們的變化做出不同的, 更好的決策. Alice 可能會尋求將新的更清晰的 API 與現有 API 一起引入最初的 OAuth2 包中, 以避免包拆分.Moe 可能會更仔細地考慮是否可以使用介面來使 Moauth 支援 OAuth2 和 Pocoauth, 從而避免使用新的 Pocomoauth 包. Amy 可能會認為更新到 Pocoauth 和 Pocomoauth 是值得的, 而不是暴露 Azure API 使用過時的 OAuth2 和 Moauth 包的事實.Anna 可能會嘗試讓 AWS API 允許 Moauth 或 Pocomoauth 使 Azure 使用者更容易切換.
相比之下, semver "主要版本升級 (bump)" 的含義遠不是那麼清晰, 並且不會對作者施加同樣的設計壓力. 需要清楚的是, 這種方法為作者創造了更多的工作, 但通過為使用者帶來顯著的好處, 這項工作是合理的.總的來說, 這種權衡是有道理的, 因為軟體包的目標是擁有比作者更多的使用者, 並且希望所有軟體包至少擁有與作者一樣多的使用者.
語義匯入版本控制
上一節展示了在更新期間, 匯入版本如何帶來簡單, 可預測的構建. 但是, 在每次向後相容的更改中選擇一個新名字對使用者來說都很困難並且沒有任何協助. 鑒於 OAuth2 和 Pocoauth 之間的選擇, Amy 應該使用哪個 ? 沒有進一步調查, 就沒有辦法知道.相比之下, 語義版本化使得這很容易: OAuth2 2.0.0 顯然是 OAuth2 1.0.0 的預期替代品.
我們可以使用語義版本控制, 並通過在匯入路徑中包含主要版本來遵循匯入相容性規則. Alice 可以用新的匯入路徑 "oauth2/v2" 調用她新的 API OAuth2 2.0.0, 而不需要建立一個可愛但不相關的新名稱 Pocoauth.Moe 也一樣: Moauth 2.0.0 (匯入為 "moauth/v2" ) 也可以成為 OAuth2 2.0.0 的輔助包, 就像 Moauth 1.0.0 是 OAuth2 1.0.0 的輔助包一樣.
當 Ugo 在第 2章中添加 Zeta 支援時, 他的構建看起來像這樣:
因為 "moauth" 和 "moauth/v2" 只是不同的軟體包, 所以 Ugo 完全清楚他需要如何使用 Azure 的 "moauth" 以及使用 AWS 的 "moauth/v2": 匯入兩者.
為了相容現有的 Go 用法, 並作為不做向後不相容的 API 更改的小鼓勵, 我在此假定主要版本 1 從匯入路徑中省略: import "moauth", 而不是 "moauth/v1".同樣, 主要版本 0 明確拒絕相容性, 也從匯入路徑中省略. 這裡的想法是, 通過使用 v0 依賴關係, 使用者明確承認破壞的可能性並在選擇更新時承擔處理它的責任.(當然, 更新不會自動發生是很重要的. 我們將在下一篇文章中看到如何最小化版本選擇對此有所協助.)
功能性名稱和不可變的含義
二十年前, Rob Pike 和我正在修改 Plan 9 C 庫的內部結構, Rob 教會了我這樣一個經驗法則, 當你改變一個函數的行為時, 你也改變它的名字. 舊的名字有一個含義.通過對新名稱使用不同的含義並刪除舊名稱, 我們確保編譯器會大聲抱怨需要檢查和更新的每段代碼, 而不是靜靜地編譯不正確的代碼. 如果人們有他們自己的程式在使用這個函數, 他們會得到編譯時間失敗, 而不是長時間的偵錯工作階段.在當今分布式版本控制的世界中, 最後一個問題被放大了, 這使得名稱變得更加重要. 並發編寫的舊代碼所期望的舊的語義不應該在合并的時候被新的語義替代.
當然, 刪除舊函數只有在可以找到所有用到它的地方, 或者當使用者瞭解他們有責任跟上變化時才會起作用, 例如在 Plan 9 等研究系統中. 對於匯出的 API, 通常會更好地保留舊名稱和舊行為, 並只添加新行為的新名稱.Rich Hickey 在 2016 年的 "Spec-ulation" 演講中提到了這一點, 即只添加新名稱和行為, 不刪除舊名稱或重新定義其含義, 正是函數式編程鼓勵的個體變數或資料結構.功能性方法在小規模編程中帶來了明確性和可預測性方面的好處, 並且在應用時效益更大, 就像匯入相容性規則一樣, 對於整個 API: 依賴地獄實際上只是可變性而已. 這隻是演講中的一個小點; 整個演講值得一看.
在 "go get" 的早期, 當人們詢問有關做出向後不相容的變化時, 我們的回應是 - 基於多年來對這些軟體變化的經驗得出的直覺 - 是給出匯入版本規則, 但沒有明確的解釋, 為什麼這種方法比不把主要版本放在匯入路徑中更好.Go 1.2 添加了一個關於軟體包版本控制的 FAQ 條目, 它提供了基本的建議 (Go 1.10 也是):
打算供公眾使用的軟體包應該盡量保持向後相容性. Go 1 相容性準則在這裡是一個很好的參考: 不要刪除匯出的名稱, 鼓勵標記的複合字面量等等. 如果需要不同的功能, 請添加新名稱而不是更改舊名稱. 如果需要完全破壞相容性, 請使用新的匯入路徑建立新的程式包.
這篇博文的一個動機是用一個清晰可信的例子來展示為什麼遵循規則是如此重要.
避免單例 (Singleton) 問題
對語義匯入版本管理方法的一個普遍反對意見是, 包作者今天預計在給定的構建中只有一個包的副本. 在不同主要版本中允許多個包可能會由於單例的意外重複而導致問題.一個例子是註冊一個 HTTP handler. 如果 my/thing 為 /debug/my/thing 註冊了一個 HTTP handler, 那麼擁有該程式包的兩個副本將導致重複的註冊, 這在註冊時會引起恐慌.另一個問題是如果程式中有兩個 HTTP 堆棧. 顯然, 只有一個 HTTP 堆棧可以在連接埠 80 上監聽; 我們不希望一半的程式註冊不會被使用的 handlers. Go 開發人員已經由於 vendored 包出現這樣的問題.
遷移到 vgo 和語義匯入版本可以澄清並簡化當前的情況. 通過取代 vendoring 導致的不可控的重複問題, 包作者將保證每個主要版本的軟體包只有一個執行個體.通過將主要版本包含在匯入路徑中, 包作者應該更清楚 my/thing 和 my/thing/v2 是不同的並且需要能夠共存. 或許這意味著在 /debug/my/thing/v2 上匯出 v2 調試資訊. 或者這也許意味著協調.也許 v2 可以負責註冊 handler, 但也可以為 v1 提供一個鉤子(hook)來提供資訊以顯示在頁面上. 這意味著 my/thing 匯入 my/thing/v2 或反之亦然; 具有不同的匯入路徑, 這很容易做到並且易於理解.相反, 如果 v1 和v2 都是 my/thing, 那麼很難理解匯入自己的匯入路徑並擷取其他匯入路徑的含義.
自動 API 更新
允許程式包的 v1 和 v2 共存於一個大型程式中的關鍵原因之一是可以一次升級該程式包的用戶端, 並且仍然具有可構建的結果. 這是逐步修複代碼的更一般問題的具體執行個體. (請參閱我的 2016 年文章 "Codebase Refactoring (with help from Go)," 瞭解更多關於該問題的資訊.)
除了保持程式的構建外, 語義匯入版本控制對逐步修複代碼有很大的好處, 我在前面的章節中提到過: 程式碼封裝的一個主要版本可以匯入並用另一個版本編寫. 將 v2 API 編寫為 v1 實現的封裝很簡單, 反之亦然.這讓他們能夠共用代碼, 並且通過適當的設計選擇和使用類型別名, 甚至可以允許使用 v1 和 v2 的用戶端進行互操作. 它還可以協助解決定義自動 API 更新中的關鍵技術問題.
在 Go 1 之前, 我們嚴重依賴於 go fix, 在更新到新的 Go 版本後, 使用者運行它並找到不再編譯的程式. 更新編譯不過的代碼使得我們無法使用大多數我們的程式分析工具, 這些工具要求其輸入是有效程式.另外, 我們想知道如何允許 Go 標準庫之外的包的作者提供特定於其自己的 API 更新的 "修複". 在單個程式中命名和處理多個不相容版本的軟體包的能力提示了一種可能的解決方案: 如果 v1 API 函數可以作為 v2 API 的封裝器來實現, 則封裝器實現是修訂規範的兩倍.例如, 假設 API 的 v1 函數具有 EnableFoo 和 DisableFoo 函數, v2 用一個 SetFoo(enabled bool) 替換該函數對. v2 發布後, v1 可以作為 v2 的封裝實現:
package p // v1import v2 "p/v2"func EnableFoo() { //go:fix v2.SetFoo(true)}func DisableFoo() { //go:fix v2.SetFoo(false)}
特別的 //go:fix 注釋會指示去修正後面的封裝體應該被內聯到調用中. 然後運行 go fix 將重寫調用 v1 EnableFoo 為 v2 SetFoo(true). 重寫很容易指定和類型檢查, 因為它是簡單的 Go 代碼.更好的是, 重寫顯然是安全的: v1 EnableFoo 已經在調用 v2 SetFoo(true), 所以重寫調用顯然不會改變程式的含義.
合理的做法可能是使用符號執行來修複反向 API 更改, 從使用 SetFoo 的 v1 到使用 EnableFoo 和 DisableFoo 的 v2. v1 SetFoo 實現可以讀取:
package q // v1import v2 "q/v2"func SetFoo(enabled bool) { if enabled { //go:fix v2.EnableFoo() } else { //go:fix v2.DisableFoo() }}
然後 go fix 會更新 SetFoo(true) 為 EnableFoo(), SetFoo(false) 為 DisableFoo(). 這種Hotfix甚至會應用於單個主要版本中的 API 更新.例如, v1 可能會被棄用(但保留) SetFoo, 並引入 EnableFoo 和 DisableFoo. 同樣的修複將協助調用者擺脫已棄用的 API.
要清楚的是, 今天這還沒有實現, 但它看起來很有前景, 而且通過賦予不同的東西以不同的名稱, 使得這種工具成為可能. 這些樣本示範了將持久的, 不可變的名稱附加到特定程式碼為的能力. 我們只需遵循這樣的規則: 當你改變某些東西時, 你也改變它的名字。
致力於相容性
語義匯入版本控制對包的作者來說更有用. 他們不能只是決定發布 v2, 遠離 v1, 並留下像 Ugo 這樣的使用者來應對這種後果. 但那些這麼做的包作者正在傷害使用者.在我看來, 如果系統難以傷害使用者, 並且自然而然地包作者也不會作出傷害使用者的行為, 那麼這似乎是件好事.
更一般地說, Sam Boyer 在 GopherCon 2017 上談到了軟體包管理如何調節我們的社互動動, 以及人們構建軟體的協作. 我們可以決定. 我們是否希望在一個圍繞系統建立的社區中工作, 該系統可以最佳化相容性, 平滑過渡以及一起很好的工作 ?或者我們是否希望在一個圍繞系統建立的社區中工作, 該系統可以最佳化建立和描述不相容性, 這使得作者破壞使用者程式也可以接受 ?匯入版本控制, 特別是通過將語義主要版本提升到匯入路徑來處理語義版本控制, 就是我們如何確保在第一種社區中工作.
讓我們致力於相容性.