Go2:實驗,簡化,出貨

介紹

[這是我上週在Gophercon 2019上發表的博客文章。我們將在演講結束後添加視頻鏈接。]

我們都正在一起走向Go 2的路上,但沒人知道確切位置和道路。這篇文章討論了我們如何實際找到並遵循Go2的道路。下面是這個過程的樣子。

Go2:實驗,簡化,出貨

我們現在嘗試使用Go,以便更好地理解它,學習哪些有效,哪些無效。然後我們嘗試可能的變化,更好地理解它們,再次學習哪些有效,哪些無效。根據我們從這些實驗中學到的東西,我們簡化了。然後我們再次進行實驗。然後我們再次簡化。如此反覆。

簡化的四個R

在這個過程中,我們通過四種主要方式簡化編寫Go程序的整體體驗:重塑(reshaping),重定義(redefining),刪除(removing)和限制(restricting)。

通過重塑進行簡化

我們簡化的第一種方法是將存在的內容重新整形為一種新形式,最終讓整體更簡單。

我們編寫的每個Go程序都是一個測試Go本身的實驗。在Go的早期,我們很快就知道編寫像這樣的addToList函數這樣的代碼是很常見的:

func addToList(list []int, x int) []int {
n := len(list)
if n+1 < cap(list) {
big := make([]int, n, (n+5)*2)
copy(big, list)
list = big
}
list = list[:n+1]
list[n] = x
return list

}

我們為字節切片和字符串切片編寫相同的代碼,依此類推。我們的程序寫的太複雜了,因為Go太簡單了。

所以我們在程序中使用了許多addToList函數,並將它們重新整理為Go本身提供的一個函數。添加append使得Go語言變得更復雜,但總的來說,即使考慮到學習append成本,它也使得編寫Go程序的整體體驗變得更加簡單。

這是另一個例子。對於Go 1,我們查看了Go發行版中的許多開發工具,並將它們重新編寫為一個新命令。

5a 8g
5g 8l
5l cgo
6a gobuild
6cov gofix → go
6g goinstall
6l gomake
6nm gopack
8a govet

這個go命令現在如此重要,以至於很容易忘記我們沒有它這麼長時間以及這個go命令涉及多少額外的工作。

我們在Go發行版中添加了代碼和複雜性,但總的來說,我們簡化了編寫Go程序的經驗。新結構也為其他有趣的實驗創造了空間,我們稍後會看到。

通過重新定義簡化

我們簡化的第二種方法是重新定義我們已有的功能,允許它做更多。就像通過重塑來簡化一樣,通過重新定義來簡化程序會使程序更容易編寫,但現在沒有什麼新東西需要學習。

例如,append最初定義為僅從切片中讀取。附加到字節切片時,可以附加來自另一個字節切片的字節,但不能附加字符串中的字節。我們重新定義了append以允許從字符串追加,而不添加任何新的語言。

var b []byte
var more []byte
b = append(b, more...) // ok
var b []byte
var more string
b = append(b, more...) // ok later

通過刪除簡化

我們簡化的第三種方法是刪除功能,因為它已經證明不如我們預期的那麼有用或不那麼重要。刪除功能意味著少學一點,修改錯誤少一些事情,更少煩惱或更多錯誤使用。當然,刪除還會強制用戶更新現有程序,可能會使它們更復雜,以彌補刪除。但總體結果仍然是編寫Go程序的過程變得更簡單。

這方面的一個例子是我們從語言中刪除了非阻塞通道操作的布爾形式:

ok := c x, ok := 

這些操作也可以使用select,使得需要決定使用哪種形式令人困惑。刪除它們簡化了語言而不降低其功能。

通過限制簡化

我們還可以通過限制允許的內容來簡化。從第一天開始,Go限制了Go源文件的編碼:它們必須是UTF-8。此限制使每個嘗試讀取Go源文件的程序更簡單。這些程序不必擔心以Latin-1或UTF-16或UTF-7或其他任何形式編碼的Go源文件。

另一個重要的限制是gofmt程序格式化。沒有什麼可以拒絕使用gofmt格式化Go代碼,但我們已經建立了一個約定,即重寫Go程序的工具將它們保留在gofmt表單中。如果你的程序也保持不變gofmt,那麼這些重寫器不會進行任何格式更改。當你比較之前和之後,你看到的唯一差異是它真正改變了。這種限制簡化程序重寫,並導致了像 goimports,gorename和其他這樣成功的實驗發生。

Go開發過程

這個實驗和簡化循環是我們過去十年來所做過的一個很好的模型。但它有一個問題:它太簡單了。我們不能只進行實驗和簡化。

我們必須公佈結果。我們必須使它可以使用。當然,使用它可以實現更多實驗,並且可能更加簡化,使得流程循環開啟。

Go2:實驗,簡化,出貨

我們於2009年11月10日首次發佈給大家。然後,在你們的幫助下,我們在2011年3月一起發佈了Go 1.從那時起我們已經發布了12個Go版本。所有這些都是重要的里程碑,可以進行更多實驗,幫助我們更多地瞭解Go,當然還有Go可供生產使用。

當我們發佈Go 1時,我們明確地將注意力轉移到使用Go,以便在嘗試任何涉及語言更改的簡化之前更好地理解該語言版本。我們需要花時間進行實驗,真正瞭解哪些有效,哪些無效。

當然,自Go 1以來我們已經發布了12個版本,因此我們仍在進行實驗,簡化和發佈。但是我們專注於簡化Go開發而不需要進行重大語言更改並且不破壞現有Go程序的方法。例如,Go 1.5發佈了第一個併發垃圾收集器,然後以下版本改進了它,通過消除暫停時間作為持續關注來簡化Go開發。

在2017年的Gophercon,我們宣佈經過五年的實驗,再次考慮可以簡化Go開發的重大變化。我們前往Go2的道路與Go 1的道路非常相似:實驗和簡化併發布,以實現簡化Go開發的總體目標。

對於Go 2,我們認為最重要的具體主題是錯誤處理,泛型和依賴性。從那時起,我們意識到另一個重要的主題是開發人員工具。

本文的其餘部分討論了我們在每個領域的工作如何遵循這條道路。在此過程中,我們將繞道而行,停止檢查Go 1.13中即將發佈的錯誤處理技術細節。

錯誤處理

當要保證所有輸入都有效而且正確,並且程序所依賴的任何內容都不能出現錯誤時,編寫一個在所有情況下都能夠正常運行的程序很困難。當你在混合著錯誤提示,編寫一個以正確方式工作的程序,無論如何,都會更加困難。

作為思考Go2的一部分,我們想要更好地理解Go是否可以幫助簡化這項工作。

有兩個不同的方面可能會被簡化:錯誤值和錯誤語法。我們將依次看看每一個技術,我承諾將重點關注Go1.13錯誤值的變化。

錯誤值

錯誤值必須從某處開始。這是os包中Read的第一個版本的功能:

export func Read(fd int64, b *[]byte) (ret int64, errno int64) {
r, e := syscall.read(fd, &b[0], int64(len(b)));
return r, e
}

當時還沒有File類型,也沒有錯誤類型。 Read和包中的其他函數直接從底層的Unix系統調用返回errno int64。

此代碼已於2008年9月10日下午12:14簽入。當時和現在一樣,這是一個實驗,代碼很快就改變了。兩小時五分鐘後,API發生了變化:

export type Error struct { s string }
func (e *Error) Print() { … } // to standard error!
func (e *Error) String() string { … }
export func Read(fd int64, b *[]byte) (ret int64, err *Error) {
r, e := syscall.read(fd, &b[0], int64(len(b)));
return r, ErrnoToError(e)
}

這個新API引入了第一種Error類型。錯誤有了字符串並且可以返回該字符串將其打印到標準錯誤中。

這裡的目的是提供一個概括的整數錯誤代碼,我們從過去的經驗中知道操作系統錯誤數量的表示太有限,它可以簡化程序,而不必將有關錯誤的所有細節都塞進64位字節中。在過去,使用錯誤字符串對我們來說工作得相當好,所以我們在這裡做了同樣的事情。這個新的API持續了七個月。

在接下來的四月,在使用接口的更多經驗之後,我們決定通過使os.Error類型本身成為接口來進一步概括並允許用戶定義的錯誤實現。我們通過刪除Print方法簡化了。

錯誤是值

創建一個簡單的錯誤interface並允許許多不同的實現,意味著我們可以使用整個Go語言來定義和檢查錯誤。我們喜歡說錯誤是值,與任何其他Go值相同。

這是一個例子。在Unix上,嘗試撥打網絡連接最終使用connect系統調用。該系統調用返回一個 syscall.Errno,這是一個整數類型的命名,表示系統調用錯誤號並實現error接口:

package syscall
type Errno int64
func (e Errno) Error() string { ... }
const ECONNREFUSED = Errno(61)
... err == ECONNREFUSED ...

syscall為主機操作系統錯誤號定義了命名常量。這樣,在這個系統上,代碼可以通過一個函數檢測得到一個錯誤,通過值相等的方式判斷錯誤是否為ECONNREFUSED。

進一步來說,os中使用更龐大的錯誤結構來報告任何系統中的調用失敗,這些錯誤結構記錄除錯誤之外還嘗試了哪些操作。這裡是這些結構中的一小部分,這個SyscallError描述了一個錯誤,它調用了一個特定的系統調用而沒有記錄其他信息:

package os
type SyscallError struct {
Syscall string
Err error
}
func (e *SyscallError) Error() string {
return e.Syscall + ": " + e.Err.Error()
}

再進一步來說,net使用更大的錯誤結構報告任何網絡故障,該錯誤結構記錄周圍網絡操作的詳細信息,例如撥號或偵聽,以及涉及的網絡和地址:

package net
type OpError struct {
Op string
Net string
Source Addr
Addr Addr
Err error
}
func (e *OpError) Error() string { ... }

將這些放在一起,操作net.Dial返回的錯誤可以格式化為字符串,但它們也是結構化的Go數據值。在這種情況下,一個 net.OpError錯誤,它添加了os.SyscallError錯誤,同時也包含了syscall.Errno錯誤值。

c, err := net.Dial("tcp", "localhost:50001")
// "dial tcp [::1]:50001: connect: connection refused"
err is &net.OpError{
Op: "dial",
Net: "tcp",
Addr: &net.TCPAddr{IP: ParseIP("::1"), Port: 50001},
Err: &os.SyscallError{
Syscall: "connect",
Err: syscall.Errno(61), // == ECONNREFUSED
},
}

當我們說錯誤是值時,我們指的是整個Go語言可用於定義它們,並且整個Go語言可用於檢查它們。

這是package net的一個例子。事實證明,當你嘗試套接字連接時,大多數情況下你會被連接或拒絕連接,但有時候你可以得到一個沒有充分的理由假的EADDRNOTAVAIL。Go通過重試來保護用戶程序免於此故障模式。要做到這一點,它必須檢查錯誤結構,以確定syscall.Errno內部是否包含EADDRNOTAVAIL。

這是代碼:

func spuriousENOTAVAIL(err error) bool {
if op, ok := err.(*OpError); ok {
err = op.Err
}
if sys, ok := err.(*os.SyscallError); ok {
err = sys.Err
}
return err == syscall.EADDRNOTAVAIL
}

一個類型斷言剝離掉任何net.OpError包裝。然後第二種類型斷言剝離任何os.SyscallError包裝。然後該函數檢查解包的錯誤是否相等EADDRNOTAVAIL。

我們從多年的經驗中學到的東西,從Go錯誤的實驗中得知,能夠定義錯誤接口的任意實現,使用完整的Go語言來構造和解構錯誤,而不需要使用任何單個實現,這是非常強大的。

這些屬性 - 錯誤是值,並且沒有一個必需的錯誤實現 - 對於保留很重要。

不要求一個錯誤實現,使每個人都能夠嘗試錯誤可能提供的其他功能,從而導致許多包的出現,例如:github.com/pkg/errors, gopkg.in/errgo.v2,github.com/hashicorp/errwrap, upspin.io/errors, github.com/spacemonkeygo/errors等等。

但是,無約束實驗的一個問題是,作為客戶端,您必須編寫可能遇到的所有可能實現的並集。 對於Go 2而言似乎值得探索的簡化是以商定的可選接口的形式定義常用功能的標準版本,以便不同的實現可以互操作。

Unwrap

這些包中最常添加的功能是可以調用一些方法來從錯誤中刪除上下文,並在其中返回錯誤。 包使用不同的名稱和含義進行此操作,有時它會刪除一個級別的上下文,而有時它會刪除儘可能多的級別。

對於Go 1.13,我們引入了一個約定,即向內部錯誤添加可移動上下文的錯誤實現應該實現一個Unwrap返回內部錯誤的方法,展開上下文。如果沒有適合暴露給調用者的內部錯誤,則錯誤不應該有Unwrap方法,或者Unwrap方法應該返回nil。

// Go 1.13 optional method for error implementations.
interface {
// Unwrap removes one layer of context,
// returning the inner error if any, or else nil.
Unwrap() error
}

調用此可選方法的方法是調用輔助函數errors.Unwrap,該函數處理錯誤本身為nil或根本沒有Unwrap方法的情況。

package errors
// Unwrap returns the result of calling
// the Unwrap method on err,
// if err’s type defines an Unwrap method.
// Otherwise, Unwrap returns nil.
func Unwrap(err error) error

我們可以使用Unwrap方法編寫一個更簡單,更通用的spuriousENOTAVAIL版本。 通用版本可以循環,調用Unwrap來刪除上下文,直到它到達EADDRNOTAVAIL或者沒有剩下錯誤,而不是尋找特定的錯誤包裝器實現,如net.OpError或os.SyscallError。

func spuriousENOTAVAIL(err error) bool {
for err != nil {
if err == syscall.EADDRNOTAVAIL {
return true
}
err = errors.Unwrap(err)
}
return false
}

不過,這個循環是如此常見,Go 1.13定義了第二個函數errors.Is,它反覆解開查找特定目標的錯誤。 所以我們可以用一次錯誤調用替換整個errors.Is:

func spuriousENOTAVAIL(err error) bool {
return errors.Is(err, syscall.EADDRNOTAVAIL)
}

在這一點上,我們甚至可能不會定義函數; errors.Is在直接call sites同樣清晰,也更簡單。

Go 1.13還引入了一個函數errors.As ,該函數在找到特定的實現類型之前會解包。

如果要編寫適用於任意包裝錯誤的代碼, errors.Is則是錯誤相等性檢查的包裝器感知版本:

err == target

errors.Is(err, target)

並且errors.As是錯誤類型斷言的包裝器感知版本:

target, ok := err.(*Type)
if ok {

...
}

var target *Type
if errors.As(err, &target) {
...
}

Unwrap還是不Unwrap

是否可以解包錯誤是一種API決策,與導出結構字段是否是API決策的方式相同。 有時將這些細節暴露給調用代碼是合適的,有時則不然。 如果是,請執行Unwrap。 如果不是,請不要實施Unwrap。

到目前為止,fmt.Errorf尚未將使用%v格式化的基礎錯誤暴露給調用者檢查。 也就是說,fmt.Errorf的結果無法解開。 考慮這個例子:

// errors.Unwrap(err2) == nil
// err1 is not available (same as earlier Go versions)
err2 := fmt.Errorf("connect: %v", err1)

如果err2返回給調用者,那個調用者從來沒有辦法打開err2並訪問err1。 我們在Go 1.13中保留了這個屬性。

當你想要允許解包fmt.Errorf的結果時,我們還添加了一個新的打印動詞%w,其格式類似於%v,需要一個錯誤值參數,並使得結果錯誤的Unwrap方法返回該參數。 在我們的示例中,假設我們將%v替換為%w:

// errors.Unwrap(err4) == err3
// (%w is new in Go 1.13)

err4 := fmt.Errorf("connect: %w", err3)

現在,如果將err4返回給調用者,則調用者可以使用Unwrap來檢索err3。

重要的是要注意絕對規則,如“總是使用%v(或從不實現展開)”或“總是使用%w(或始終實現展開)”與絕對規則一樣錯誤,如“永不導出結構域”或“總是 導出結構字段。“相反,正確的決定取決於調用者是否應該能夠檢查並依賴於使用%w或實現展開暴露的其他信息。

錯誤值打印 (Abandoned)

除了Unwrap的設計草案外,我們還發布了一個設計草案,用於更豐富的錯誤打印的可選方法,包括堆棧幀信息和對本地化翻譯錯誤的支持。

// Optional method for error implementations
type Formatter interface {
Format(p Printer) (next error)
}
// Interface passed to Format
type Printer interface {
Print(args ...interface{})
Printf(format string, args ...interface{})
Detail() bool
}

這個並不像Unwrap那麼簡單,我不會在這裡詳述。 當我們在冬天討論Go社區的設計時,我們瞭解到設計不夠簡單。 對於單個錯誤類型來說,實現起來太難了,並且它對現有程序的幫助不夠。 總的來說,它並沒有簡化Go開發。

通過這次社區討論,我們放棄了這種打印設計。

錯誤語法

那是錯誤值。 讓我們簡要回顧一下另一個被遺棄的實驗的錯誤語法。

以下是compress/lzw/writer.go標準庫中的一些代碼 :

// Write the savedCode if valid.
if e.savedCode != invalidCode {
if err := e.write(e, e.savedCode); err != nil {
return err
}
if err := e.incHi(); err != nil && err != errOutOfCodes {
return err
}
}
// Write the eof code.
eof := uint32(1)<<e.litwidth>if err := e.write(e, eof); err != nil {
return err
}
/<e.litwidth>

一目瞭然,這段代碼大約有一半的錯誤檢查。當我讀到它時,我的眼睛茫然。而且我們知道,編寫繁瑣且閱讀繁瑣的代碼很容易被誤讀,使其成為難以發現的錯誤的好地方。例如,這三個錯誤檢查中的一個與其他錯誤檢查不同,這一事實在快速瀏覽時很容易錯過。如果您正在調試此代碼,需要多長時間才能注意到這一點?

在去年的Gophercon,我們 提出了 一個由關鍵字標記的新控制流結構的草圖設計check。 Check使用函數調用或表達式的錯誤結果。如果錯誤是非nil,則check返回錯誤。否則,check評估調用的其他結果。我們可以check用來簡化lzw代碼:

// Write the savedCode if valid.
if e.savedCode != invalidCode {
check e.write(e, e.savedCode)
if err := e.incHi(); err != errOutOfCodes {
check err
}
}
// Write the eof code.
eof := uint32(1)<<e.litwidth>check e.write(e, eof)
/<e.litwidth>

此版本的相同代碼使用check,它刪除了四行代碼,更重要的是突出顯示e.incHi允許返回的調用errOutOfCodes。

也許最重要的是,該設計還允許在以後的檢查失敗時運行定義錯誤處理程序塊。這樣您就可以只編寫一次共享上下文添加代碼,就像在此片段中一樣:

handle err {
err = fmt.Errorf("closing writer: %w", err)
}
// Write the savedCode if valid.
if e.savedCode != invalidCode {
check e.write(e, e.savedCode)
if err := e.incHi(); err != errOutOfCodes {
check err
}
}
// Write the eof code.
eof := uint32(1)<<e.litwidth>check e.write(e, eof)
/<e.litwidth>

從本質上講,check是編寫if語句的簡短方法,而handle類似於defer但僅適用於錯誤返回路徑。 與其他語言中的異常相反,此設計保留了Go的重要屬性,即每個潛在的失敗調用都在代碼中明確標記,現在使用check關鍵字而不是if err != nil。

這種設計的一個大問題是,handle重疊太多,並且以混亂的方式defer。

在五月,我們發佈了一個新的設計,有三個簡化:為了避免與延遲的混淆,設計放棄了處理,而只是使用延遲; 為了匹配Rust和Swift中的類似想法,設計重命名為check to try; 並且允許以像gofmt這樣的現有解析器識別的方式進行實驗,它將檢查(現在嘗試)從關鍵字更改為內置函數。

現在相同的代碼看起來像這樣:

defer errd.Wrapf(&err, "closing writer")
// Write the savedCode if valid.
if e.savedCode != invalidCode {
try(e.write(e, e.savedCode))
if err := e.incHi(); err != errOutOfCodes {
try(err)
}
}
// Write the eof code.
eof := uint32(1)<<e.litwidth>try(e.write(e, eof))
/<e.litwidth>

我們花了六月的大部分時間在GitHub上公開討論這個提案。

檢查或嘗試的基本思想是縮短每次錯誤檢查時重複的語法量,特別是從視圖中刪除return語句,保持錯誤檢查顯式並更好地突出顯示有趣的變化。 然而,在公眾反饋討論中提出的一個有趣的觀點是,如果沒有明確的if語句和返回,就沒有地方可以放置調試打印,沒有地方可以設置斷點,並且沒有代碼顯示為代碼覆蓋率結果中未執行的代碼。 我們所獲得的好處是以使這些情況變得更加複雜為代價的。 總的來說,從這個以及其他考慮因素來看,總體結果並不是很簡單的Go開發,所以我們放棄了這個實驗。

這就是錯誤處理的一切,這是今年的主要焦點之一。

泛型

現在有點爭議的東西:泛型。

我們為Go 2確定的第二個重要主題是使用類型參數編寫代碼的某種方式。 這樣就可以編寫通用數據結構,也可以編寫適用於任何切片,任何類型的通道或任何類型的映射的通用函數。 例如,這是一個通用的通道過濾器:

// Filter copies values from c to the returned channel,
// passing along only those values satisfying f.
func Filter(type value)(f func(value) bool, c out := make(chan value)
go func() {
for v := range c {
if f(v) {
out }
}
close(out)
}()
return out
}

自從Go開始工作以來,我們一直在考慮泛型,我們在2010年編寫並拒絕了我們的第一個具體設計。我們在2013年底之前編寫並拒絕了另外三個設計。四個廢棄的實驗,但沒有失敗的實驗,我們從 他們,就像我們從檢查和嘗試中學到的一樣。 每一次,我們都知道Go 2的路徑並不是那個確切的方向,我們注意到其他方向可能很有趣。 但到了2013年,我們已經決定我們需要關注其他問題,所以我們將整個主題放在一邊幾年。

去年我們又開始進行探索和實驗,去年夏天,我們在Gophercon的基礎上提出了一個基於合約理念的新設計。 我們一直在進行實驗和簡化,我們一直在與編程語言理論專家合作,以更好地理解設計。

總的來說,我希望我們朝著一個良好的方向前進,朝著一個簡化Go開發的設計。 即便如此,我們可能會發現這種設計也不起作用。 我們可能不得不放棄這個實驗,並根據我們學到的東西調整我們的路徑。 我們會發現。

在Gophercon 2019年,Ian Lance Taylor談到了為什麼我們可能想要向Go添加泛型並簡要預覽最新的設計草案。 有關詳細信息,請參閱他的博文“Go:為何帶來泛型”。

依賴

我們為Go 2確定的第三個主題是依賴管理。

2010年,我們發佈了一個名為goinstall的工具,我們將其稱為“包安裝實驗”。它下載了依賴項並將它們存儲在GOROOT的Go分發樹中。

兼容性

goinstall實驗故意遺漏了包版本控制的明確概念。 相反,goinstall總是下載最新的副本。 我們這樣做了,所以我們可以專注於包安裝的其他設計問題。

Goinstall成為Go 1的一部分。當人們詢問版本時,我們鼓勵他們通過創建其他工具進行實驗,他們做到了。 我們鼓勵包AUTHORS為他們的USERS提供與Go 1庫相同的向後兼容性。 引用Go常見問題解答:

“供公眾使用的軟件包應儘量保持向後兼容性。

如果需要不同的功能,請添加新名稱,而不是更改舊名稱。

如果需要完全中斷,請使用新的導入路徑創建一個新包。“

該約定通過限制作者可以做的事情簡化了使用包的整體體驗:避免破壞對API的更改; 為新功能賦予新名稱; 併為全新的包裝設計提供新的進口途徑。

當然,人們一直在試驗。 其中一個最有趣的實驗是由Gustavo Niemeyer開始的。 他創建了一個名為gopkg.in的Git重定向器,它為不同的API版本提供了不同的導入路徑,以幫助包作者遵循為新包裝設計提供新導入路徑的慣例。

例如,GitHub存儲庫中的Go源代碼go-yaml/yaml在v1和v2語義版本標記中具有不同的API。 gopkg.in服務器為它們提供了不同的導入路徑gopkg.in/yaml.v1和gopkg.in/yaml.v2。

提供向後兼容性的慣例,以便可以使用較新版本的軟件包代替舊版本,這使得即使在今天也能很好地實現“始終下載最新版本”的規則。

版本控制和供應

但是在生產環境中,您需要更加精確地瞭解依賴版本,以使構建可重現。

許多人嘗試了應該是什麼樣子,構建滿足他們需求的工具,包括Keith Rarick的goven(2012)和godep(2013),Matt Butcher的glide(2014)和Dave Cheney的gb(2015)。 所有這些工具都使用將依賴包複製到您自己的源代碼控制存儲庫中的模型。 用於使這些包可用於導入的確切機制各不相同,但它們都比它們看起來要複雜得多。

在社區範圍的討論之後,我們採用了Keith Rarick的提議,即在沒有GOPATH技巧的情況下為複製的依賴項添加明確的支持。 這通過重塑來簡化:與addToList和append一樣,這些工具已經實現了這個概念,但它比它需要的更尷尬。 添加對vendor目錄的顯式支持使這些使用更加簡單。

go命令中的運輸vendor目錄導致了更多的vendoring itself實驗,我們意識到我們已經引入了一些問題。 最嚴重的是我們失去了包裝的獨特性。 之前,在任何給定的構建期間,導入路徑可能出現在許多不同的包中,並且所有導入都引用相同的目標。 現在有了vendoring,不同包中的相同導入路徑可能會引用包的不同vendored副本,所有這些副本都會出現在最終生成的二進制文件中。

當時,我們沒有這個屬性的名稱:包唯一性。 這就是GOPATH模型的工作原理。 直到它消失,我們才完全欣賞它。

這裡有一個並行檢查和嘗試錯誤語法提議。 在這種情況下,我們依賴於可見返回語句如何在我們考慮刪除之前以我們不理解的方式工作。

當我們添加vendor目錄支持時,有許多不同的工具來管理依賴項。 我們認為關於vendor目錄和銷售元數據的格式的明確協議將允許各種工具進行互操作,就像關於Go程序如何存儲在文本文件中的協議一樣,可以實現Go編譯器,文本編輯器和類似工具之間的互操作。 goimports和gorename。

事實證明這是天真的樂觀。 vendoring工具都以微妙的語義方式不同。 互操作需要將它們全部改為同意語義,可能會破壞各自的用戶。 融合沒有發生。

Dep

在2016年的Gophercon,我們開始嘗試定義一個管理依賴項的工具。 作為這項工作的一部分,我們與許多不同類型的用戶進行了調查,以瞭解他們在依賴關係管理方面需要什麼,並且團隊開始研究新工具,後者變成了dep。

Dep旨在能夠替換所有現有的依賴管理工具。 目標是通過將現有的不同工具重塑為單一工具來簡化。 它部分完成了。 Dep還通過在項目樹頂部只有一個vendor目錄來恢復其用戶的包唯一性。

但dep也引入了一個嚴重的問題,讓我們花了一段時間才完全發現。 問題在於dep包含了滑行的設計選擇,支持和鼓勵對給定包的不兼容更改而不改變導入路徑。

這是一個例子。 假設您正在構建自己的程序,並且需要一個配置文件,因此您使用流行的Go YAML包的第2版:

Go2:實驗,簡化,出貨

現在假設您的程序導入Kubernetes客戶端。 事實證明,Kubernetes廣泛使用YAML,它使用相同流行軟件包的版本1:

Go2:實驗,簡化,出貨

版本1和版本2具有不兼容的API,但它們也具有不同的導入路徑,因此對於給定導入的含義沒有歧義。 Kubernetes獲得版本1,您的配置解析器獲得版本2,一切正常。

Dep放棄了這個模型。 yaml包的版本1和版本2現在具有相同的導入路徑,從而產生衝突。 對兩個不兼容的版本使用相同的導入路徑,結合包唯一性,使得無法構建您之前可以構建的程序:

Go2:實驗,簡化,出貨

我們花了一段時間來理解這個問題,因為我們長期以來一直在應用“新API意味著新的導入路徑”慣例,以至於我們認為這是理所當然的。 dep實驗幫助我們更好地理解了這個約定,並且我們給它起了一個名字:導入兼容性規則:

“如果舊軟件包和新軟件包具有相同的導入路徑,則新軟件包必須向後兼容舊軟件包。”

Go Modules

我們在dep實驗中取得了很好的效果,並且瞭解了哪些方法效果不佳,我們嘗試了一種名為vgo的新設計。 在vgo中,包遵循導入兼容性規則,因此我們可以提供包唯一性,但仍然不會像我們剛看到的那樣破壞構建。 這讓我們也簡化了設計的其他部分。

除了恢復導入兼容性規則之外,vgo設計的另一個重要部分是為一組包的概念賦予名稱,並允許將該分組與源代碼存儲庫邊界分開。 一組Go包的名稱是一個模塊,因此我們現在將系統稱為Go模塊。

取代GOPATH

使用Go模塊將GOPATH作為全局名稱空間結束。 從遠離GOPATH開始,將現有的Go用法和工具轉換為模塊的幾乎所有艱苦工作都是由於這種變化造成的。

GOPATH的基本思想是GOPATH目錄樹是正在使用的版本的全局真實來源,並且當您在目錄之間移動時,所使用的版本不會改變。 但是,全局GOPATH模式與每個項目可重現構建的生產要求直接衝突,這本身就以許多重要方式簡化了Go開發和部署體驗。

每個項目可重現的構建意味著當您在項目A的檢出工作時,您將獲得與項目A的其他開發人員在該提交中獲得的相同的依賴項版本集,如go.mod文件所定義。 當您切換到項目B的結帳時,現在您可以獲得該項目選擇的依賴版本,與項目B的其他開發人員獲得的版本相同。 但是那些可能與項目A不同。當你從項目A轉移到項目B時,依賴版本的變化是必要的,以使你的開發與A和B上的其他開發人員的開發保持同步。不可能有 單一的全局GOPATH了。

採用模塊的大多數複雜性直接源於一個全局GOPATH的丟失。 包的源代碼在哪裡? 之前,答案僅取決於您的GOPATH環境變量,大多數人很少更改。 現在,答案取決於您正在進行的項目,這可能經常發生變化。 一切都需要更新這個新的約定。

大多數開發工具使用該 go/build包來查找和加載Go源代碼。我們保持該程序包正常運行,但API沒有預期模塊,我們為避免API更改而添加的變通方法比我們想要的慢。我們已經發布了一個替代品golang.org/x/tools/go/packages。開發人員工具現在應該使用它。它支持GOPATH和Go模塊,使用起來更快更容易。在一兩個版本中,我們可以將其移動到標準庫中,但是現在golang.org/x/tools/go/packages 是穩定的並且可以使用。

Go模塊代理

模塊簡化Go開發的方法之一是將一組包的概念與存儲它們的底層源控制存儲庫分開。

當我們與Go用戶討論依賴關係時,幾乎所有在他們公司使用Go的人都詢問如何通過他們自己的服務器路由獲取包,以更好地控制可以使用的代碼。 甚至開源開發人員都擔心依賴關係會消失或意外地發生變化,從而破壞他們的構建。 在模塊之前,用戶嘗試瞭解決這些問題的複雜解決方案,包括攔截go命令運行的版本控制命令。

Go模塊設計可以很容易地引入模塊代理的概念,可以詢問特定模塊版本。

現在,公司可以輕鬆運行自己的模塊代理,並提供有關允許內容和緩存副本存儲位置的自定義規則。雅典開源項目建立了這樣一個代理,Aaron Schlesinger在Gophercon 2019上對此進行了討論。(當視頻可用時,我們將在這裡添加一個鏈接。)

對於個人開發人員和開源團隊,Google的Go團隊已經啟動了一個代理,作為所有開源Go軟件包的公共鏡像,Go 1.13將在模塊模式下默認使用該代理。Katie Hockman在Gophercon 2019上談到了這個系統。(當視頻可用時,我們會在這裡添加一個鏈接。)

Go模塊狀態

將1.11介紹模塊作為實驗性的選擇預覽。 我們不斷嘗試和簡化。 Go1.12發佈改進,Go 1.13將提供更多改進。

模塊現在處於我們認為它們將為大多數用戶服務的程度,但我們尚未準備好關閉GOPATH。 我們將繼續進行實驗,簡化和修改。

我們完全認為Go用戶社區圍繞GOPATH構建了近十年的經驗,工具和工作流程,並且將所有這些轉換為Go模塊需要一段時間。

但同樣,我們認為模塊現在可以很好地適用於大多數用戶,我建議您在Go 1.13發佈時查看。

作為一個數據點,Kubernetes項目有很多依賴項,他們已經遷移到使用Go模塊來管理它們。你可能也可以。如果您不能,請通過提交錯誤報告告訴我們什麼不適合您或什麼太複雜,我們將進行實驗和簡化。

工具

錯誤處理,泛型和依賴關係管理至少需要幾年時間,我們現在將重點關注它們。 錯誤處理已接近完成,模塊將在此之後接下來,之後可能是泛型。

但是假設我們看了幾年,那時我們已經完成了實驗和簡化,並且已經發布了錯誤處理,模塊和泛型。 那又怎樣? 預測未來是非常困難的,但我認為,一旦這三個已經出貨,這可能標誌著重大變化的新平靜時期的開始。 我們在這一點上的重點可能轉向使用改進的工具簡化Go開發。

一些工具工作已經在進行中,所以這篇文章通過查看完成。

雖然我們幫助更新了所有Go社區現有的工具以瞭解Go模塊,但我們注意到,擁有大量開發幫助工具,每個工具都做一個小工作,並不能很好地為用戶服務。 單個工具太難以組合,調用太慢而且使用太不同。

我們開始努力將最常用的開發助手統一到一個工具中,現在稱為gopls(發音為“go,please”)。 Gopls使用語言服務器協議LSP,並且可以與支持LSP的任何集成開發環境或文本編輯器一起使用,這基本上是此時的所有內容。

Gopls標誌著Go項目的重點擴展,從提供類似編譯器的命令行工具(如go vet或gorename)到提供完整的IDE服務。Rebecca Stambler gopls在Gophercon 2019上發表了有關IDE和IDE的更多細節的演講。(當視頻可用時,我們將在此處添加鏈接。)

之後gopls,我們也有了go fix以可擴展的方式恢復並提供go vet更多幫助的想法。

結尾

Go2:實驗,簡化,出貨

這是通往Go 2 的道路,我們要不斷地嘗試並簡化,並且把這個信息傳遞出去,反覆地嘗試並簡化,不停地進行這樣的過程。 每次我們進行實驗和簡化時,我們都會更多地瞭解Go 2應該是什麼樣子,並向它邁進一步。 即使是遺棄的實驗,如嘗試或我們的前四個泛型設計或dep也不浪費時間。 它們幫助我們在出貨之前瞭解需要簡化的內容,在某些情況下,它們可以幫助我們更好地理解我們認為理所當然的事情。

在某些時候,我們會意識到我們已經進行了足夠的實驗,並且已經足夠簡化並且運輸得足夠多,我們將擁有Go2。

感謝Go社區中的所有人幫助我們在這條道路上進行實驗,簡化和出貨。

原文:https://blog.golang.org/experiment

譯文:https://github.com/llgoer/go-generics


分享到:


相關文章: