Go 部落格

告別核心型別 - 擁抱我們所熟知和喜愛的 Go!

Robert Griesemer
2025 年 3 月 26 日

Go 1.18 版本引入了泛型,隨之帶來了一些新功能,包括型別引數、型別約束以及型別集等新概念。它還引入了“核心型別”的概念。雖然前者提供了具體的全新功能,但核心型別是一個抽象構造,是為了方便和簡化處理泛型運算元(型別為型別引數的運算元)而引入的。在 Go 編譯器中,過去依賴運算元底層型別的程式碼,現在必須呼叫一個計算運算元核心型別的函式。在語言規範中,很多地方我們只需要將“底層型別”替換為“核心型別”。這有什麼不好的呢?

事實證明,有很多不好的地方!要理解我們為什麼會走到這一步,回顧一下型別引數和型別約束的工作原理會很有用。

型別引數和型別約束

型別引數是未來型別實參的佔位符;它像一個型別變數,其值在編譯時已知,類似於命名常量代表在編譯時已知的數字、字串或布林值。與普通變數一樣,型別引數也有一個型別。該型別由其型別約束描述,型別約束決定了對型別為相應型別引數的運算元允許哪些操作。

例項化型別引數的任何具體型別都必須滿足型別引數的約束。這確保了型別為型別引數的運算元擁有相應型別約束的所有屬性,無論使用何種具體型別來例項化型別引數。

在 Go 中,型別約束透過方法和型別要求的混合來描述,它們共同定義了一個型別集:這是滿足所有要求的所有型別的集合。Go 為此目的使用了一種廣義的介面形式。介面枚舉了一組方法和型別,此類介面描述的型別集由實現這些方法幷包含在列舉型別中的所有型別組成。

例如,介面描述的型別集

type Constraint interface {
    ~[]byte | ~string
    Hash() uint64
}

由所有表示形式為[]bytestring且其方法集包含Hash方法的型別組成。

有了這些,我們現在可以寫下管理泛型運算元操作的規則。例如,索引表示式的規則規定(除其他外)對於型別引數型別P的運算元a

索引表示式a[x]必須對P型別集中的所有型別的值都有效。P型別集中的所有型別的元素型別必須相同。(在此上下文中,字串型別的元素型別是byte。)

這些規則使得索引下面的泛型變數s成為可能(playground

func at[bytestring Constraint](s bytestring, i int) byte {
    return s[i]
}

允許索引操作s[i],因為s的型別是bytestring,並且bytestring的型別約束(型別集)包含[]bytestring型別,對於它們來說,用i索引是有效的。

核心型別

這種基於型別集的方法非常靈活,並且符合最初的泛型提案的意圖:涉及泛型型別運算元的操作應該是有效的,如果它對相應型別約束允許的任何型別都有效。為了簡化實現方面的問題,知道我們以後能夠放寬規則,這種方法並沒有被普遍選擇。相反,例如,對於傳送語句,規範規定

通道表示式的核心型別必須是通道,通道方向必須允許傳送操作,並且要傳送的值的型別必須可以賦值給通道的元素型別。

這些規則基於核心型別的概念,其定義大致如下

  • 如果一個型別不是型別引數,它的核心型別就是它的底層型別
  • 如果該型別是型別引數,則核心型別是型別引數型別集中所有型別的單一底層型別。如果型別集具有不同的底層型別,則核心型別不存在。

例如,interface{ ~[]int }有一個核心型別([]int),但上面的Constraint介面沒有核心型別。為了使事情複雜化,當涉及通道操作和某些內建呼叫(appendcopy)時,上述核心型別定義過於嚴格。實際規則進行了調整,允許不同的通道方向和包含[]bytestring型別的型別集。

這種方法存在各種問題

  • 由於核心型別的定義必須為不同的語言特性提供健全的型別規則,因此它對於特定操作而言過於嚴格。例如,Go 1.24 中切片表示式的規則確實依賴於核心型別,因此,即使是有效的,也不允許對由Constraint約束的型別S的運算元進行切片。

  • 在嘗試理解特定語言特性時,即使考慮非泛型程式碼,也可能需要學習核心型別的複雜性。同樣,對於切片表示式,語言規範談論的是被切片運算元的核心型別,而不是簡單地宣告運算元必須是陣列、切片或字串。後者更直接、更簡單、更清晰,並且不需要了解在具體情況下可能不相關的另一個概念。

  • 由於核心型別的概念存在,索引表示式以及lencap(以及其他)的規則,這些都避免了核心型別,在語言中顯示為例外而不是規範。反過來,核心型別導致諸如issue #48522之類的提案,該提案將允許選擇器x.f訪問x型別集中所有元素共享的欄位f,這似乎為語言添加了更多例外。沒有核心型別,該特性將成為非泛型欄位訪問普通規則的自然而有用的結果。

Go 1.25

對於即將釋出的 Go 1.25 版本(2025 年 8 月),我們決定從語言規範中刪除核心型別的概念,轉而採用在需要時使用明確的(且等效的!)散文。這有以下幾個好處

  • Go 規範呈現的概念更少,使語言更容易學習。
  • 無需參考泛型概念即可理解非泛型程式碼的行為。
  • 個性化的方法(針對特定操作的特定規則)為更靈活的規則打開了大門。我們已經提到了issue #48522,但也有關於更強大的切片操作以及改進的型別推斷的想法。

相關的提案 issue #70128 最近已獲批准,相關更改已實施。具體而言,這意味著語言規範中的許多散文已恢復到其最初的泛型前形式,並在需要時添加了新段落以解釋與泛型運算元相關的規則。重要的是,沒有行為被改變。關於核心型別的整個部分已被刪除。編譯器的錯誤訊息已更新,不再提及“核心型別”,並且在許多情況下,錯誤訊息現在更具體,透過指出型別集中究竟是哪個型別導致了問題。

以下是一些已進行的更改示例。對於內建函式close,從 Go 1.18 開始,規範如下:

對於核心型別為通道的引數ch,內建函式close記錄該通道上將不再發送任何值。

一個只想知道close如何工作的讀者,必須首先了解核心型別。從 Go 1.25 開始,這一節將再次以 Go 1.18 之前的方式開始:

對於通道ch,內建函式close(ch)記錄該通道上將不再發送任何值。

這更短且更易理解。只有當讀者處理泛型運算元時,他們才需要考慮新新增的段落:

如果close引數的型別是型別引數,則其型別集中的所有型別都必須是具有相同元素型別的通道。如果這些通道中有任何是隻接收通道,則會報錯。

我們在所有提及核心型別的地方都進行了類似的修改。總而言之,儘管這次規範更新不會影響任何當前的 Go 程式,但它為未來的語言改進打開了大門,同時使當前的語言更容易學習,其規範也更簡潔。

下一篇文章: 使用 testing.B.Loop 進行更可預測的基準測試
上一篇文章: 抗遍歷檔案 API
部落格索引