Go 部落格
告別核心型別 - 擁抱我們熟悉和喜愛的 Go!
Go 1.18 版本引入了泛型以及一系列新特性,包括型別引數、型別約束和諸如型別集的新概念。它還引入了核心型別的概念。前者提供了具體的新功能,而核心型別是一種抽象構造,引入它是為了權宜之計,並簡化處理泛型運算元(型別為型別引數的運算元)。在 Go 編譯器中,過去依賴於運算元的底層型別的程式碼,現在不得不呼叫一個計算運算元核心型別的函式。在語言規範中,許多地方我們只需要將“底層型別”替換為“核心型別”。這有什麼不好呢?
事實證明,有不少問題!為了理解我們是如何走到這一步的,回顧一下型別引數和型別約束的工作原理會很有幫助。
型別引數和型別約束
型別引數是未來型別實參的佔位符;它就像一個在編譯時已知其值的型別變數,類似於命名常量代表在編譯時已知值的數字、字串或布林值。和普通變數一樣,型別引數也有型別。這個型別由它們的型別約束描述,型別約束決定了對型別為相應型別引數的運算元允許進行哪些操作。
任何例項化型別引數的具體型別都必須滿足該型別引數的約束。這確保了型別為型別引數的運算元具有相應型別約束的所有屬性,無論用於例項化型別引數的具體型別是什麼。
在 Go 中,型別約束透過方法和型別要求的混合描述,它們共同定義了一個型別集:這是滿足所有要求的型別的集合。Go 為此使用了介面的泛化形式。一個介面列舉了一組方法和型別,由這樣的介面描述的型別集包含實現這些方法幷包含在列舉型別中的所有型別。
例如,介面描述的型別集
type Constraint interface {
~[]byte | ~string
Hash() uint64
}
包含所有表示為 []byte
或 string
且其方法集包含 Hash
方法的型別。
有了這些,我們現在可以寫出管理泛型運算元操作的規則。例如,索引表示式的規則規定(其中包括)對於型別引數型別 P
的運算元 a
索引表示式
a[x]
對於P
的型別集中所有型別的值都必須有效。P
的型別集中所有型別的元素型別必須相同。(在此語境下,字串型別的元素型別是byte
。)
這些規則使得索引下面的泛型變數 s
成為可能(演練場)
func at[bytestring Constraint](s bytestring, i int) byte {
return s[i]
}
索引操作 s[i]
是允許的,因為 s
的型別是 bytestring
,而 bytestring
的型別約束(型別集)包含了 []byte
和 string
型別,對於這些型別,使用 i
進行索引是有效的。
核心型別
這種基於型別集的方法非常靈活,並且符合原始泛型提案的意圖:涉及泛型型別運算元的操作,如果在相應型別約束允許的任何型別下都有效,那麼該操作就應該有效。為了簡化實現,並知道我們以後可以放寬規則,這種方法並未被普遍採用。相反,例如,對於傳送語句,規範規定
通道表示式的核心型別必須是一個通道,通道方向必須允許傳送操作,並且要傳送的值的型別必須可賦值給通道的元素型別。
這些規則基於核心型別的概念,其定義大致如下
- 如果一個型別不是型別引數,其核心型別就是其底層型別。
- 如果型別是型別引數,核心型別是型別引數型別集中所有型別的唯一底層型別。如果型別集包含不同的底層型別,則核心型別不存在。
例如,interface{ ~[]int }
有一個核心型別([]int
),但上面的 Constraint
介面沒有核心型別。更復雜的是,對於通道操作和某些內建函式呼叫(append
, copy
),上述核心型別定義過於嚴格。實際規則進行了調整,允許不同的通道方向以及包含 []byte
和 string
型別的型別集。
這種方法存在各種問題
-
因為核心型別的定義必須為不同的語言特性提供健全的型別規則,所以它對特定操作過於嚴格。例如,Go 1.24 切片表示式的規則確實依賴於核心型別,因此對受
Constraint
約束的型別S
的運算元進行切片是不允許的,即使它可能是有效的。 -
當試圖理解特定的語言特性時,即使考慮非泛型程式碼,也可能不得不學習核心型別的複雜性。再次以切片表示式為例,語言規範談論的是被切片運算元的核心型別,而不是僅僅說明運算元必須是陣列、切片或字串。後者更直接、更簡單、更清晰,並且不需要了解在具體情況下可能不相關的另一個概念。
-
由於核心型別的概念存在,索引表示式以及
len
和cap
(及其他)的規則都回避了核心型別,它們在語言中看起來像是例外而非規範。反過來,核心型別導致諸如 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 程式,但它為未來的語言改進打開了大門,同時也使得今天的 Go 語言更容易學習,其規範也更簡單。
下一篇文章:使用 testing.B.Loop 進行更可預測的基準測試
上一篇文章:防遍歷檔案 API
部落格索引