面試官提問三個 Go 接口的概念,10 年 gopher 竟無言以對
自 Go 1.18 後, Go 的 interface 的含義有所變化, 三個新的和 Go 接口有關的概念很多人還不知道: type set
(類型集合)、specific type
(特定類型) 和**structural type
**(結構類型)。
1 type set (類型集合)
type set 稱之爲類型集合,一些關注 Go 泛型的朋友其實也對此有些瞭解,它是 Go 1.18 新增加的一個概念。
Go 1.18 之前,Go 的接口代表了一組方法的集合 (method set), 凡是實現了這些方法集合的類型,都被稱之爲實現了這個接口。Go 不像 Java 語言,需要顯示地定義某個類實現某個接口,Go 不需要這樣,在 Go 中,只要一個類型實現了某個接口定義的所有方法,它就實現了這個接口,可以賦值給這個接口類型的變量,或者作爲這個接口類型的方法的實參或者返回值,這種設計有時候也被叫做**鴨子類型
**(duck typing)。只要它走起來像鴨子,叫起來像鴨子,那麼它就是鴨子,這是一個很經典的對鴨子類型的描述。
在 Go 1.18 中,接口不再代表方法的集合了,而是代表類型的集合 (type set)。只要某種類型在這個接口的類型集合中,那麼我們就說這種類型實現了這個接口。如果這個接口被用作類型約束,那麼在這個接口定義的類型集合中的任意一個元素,都可以實例化這個類型參數。
所以,實際上,爲了支持接口作爲類型約束的擴展,Go 語言規範不得不重新定義接口的含義了,這也是類型集合出現的原因。
其實,接口的方法集的概念還在 一個接口的方法集是這個接口的類型集合中所有元素的方法集的交集。
本文假定你對 Go 泛型有了一定的瞭解。假定你還不瞭解,那麼你必須知道,Go 1.18 中接口除了原先的方法元素還,還支持包含類型元素,類型元素可以是某個類型T
、或者是近似類型~T
, 或者是它們的聯合int|int8|int16|int32|int64|~string
。
如果一個接口I
嵌入了另外一個接口E
, 那麼I
的類型集是它顯示定義的類型集合和嵌入的接口E
的類型集合的交集。相當於E
把接口I
的類型集收窄了。
如何判斷一個接口的類型集呢?請遵循下面的原則:
-
空接口
any
、interface{}
的類型集是所有類型的集合
所以像int
、string
、strcut{}
、MyStruct
、func foobar()
、chan int
、map[int]string
、[]int
等等都在空接口的類型集合中 -
非空接口的類型集合是接口元素的類型集合的交集
那麼什麼是接口元素的類型集合呢?參照下面的四條
前面我們已經提到,接口元素包含類型元素和方法元素
-
一個方法的類型集合是定義這個方法的所有類型,也就是隻要某個類型的方法集包含這個方法,那麼它就屬於這個方法的類型集合
比如接口中有
String() string
這樣一個方法,那麼所有實現這個方法的類型都屬於String() string
定義的類型集合,比如net.IP
。 -
一個非接口類型的類型集合就是隻包含這個類型的類型集合
比如
int
的類型集合只包含int
這樣一個元素 -
近似元素
~T
的類型集合是所有底層類型爲T
的所有類型的集合比如
type MyInt int
中的MyInt
就屬於~int
的類型集合 -
聯合元素
t1|t2|…|tn
的類型集合是這些聯合元素類型集合的並集
下面的例子列舉了一些類型的集合:
2 specific type (特定類型) 和 specific type set
接口另外一個很重要的概念就是specific type
(特定類型)。
只有包含類型元素的接口才定義了特定類型 (可能是空的類型)。
如果不嚴格的講,特定類型是出現在類型元素中定義的那些類型T
、~T
、t1|t2|...|tn
中的t1
、t2
、...
、tn
。
更準確地說,對於給定的接口I
,特定類型的集合對應於該接口代表的類型集合𝑅,這裏要求𝑅是非空且有限的。否則,如果𝑅爲空或無限,則接口沒有特定類型。
對於一個給定的接口、類型元素或者類型,它代表的類型集合𝑅定義如下:
-
對於一個沒有任何類型元素的接口,它的𝑅是所有的元素 (無限)
所以它沒有特定類型 -
如果一個接口有類型元素,它的𝑅是它的元素代表的類型的交集
至於有沒有特定類型要看𝑅是否是非空且有限 -
對於一個非接口類型
T
, 或者~T
, 它的𝑅是包含類型T
的集合 -
對於聯合元素
t1|t2|…|tn
, 它的𝑅是這些項代表類型的並集
下面是特定類型的例子:
type set vs specific type set
類型集合和特定類型集合還是有區別的,從上面它們的定義可以看出來。
一個接口即使類型爲空,它的特定類型集合可能不爲空。
比如interface{ int; m() }
, 它的類型集合是空的 (int 沒有實現 m 方法),但是它的特定類型是int
。
一個接口即使有有限的特定類型,它的類型集合也可能是無限的
比如interface{ ~int; m() }
, 它的特定類型是 int,但是它的類型集合確是無限的 (任何底層爲 int 並且實現了方法 m 的類型都屬於它的類型集合)
那麼定義特定類型有什麼用呢?
特定類型的應用
特定類型主要用於判斷類型參數是否支持索引, 像a[x]
這樣的類型。
比如一個表達式a[x]
, a
這個實例的類型可能是數組、指向數組的指針、slice、字符串、map。
如果a
的類型是類型參數P
的話,那麼我們的代碼a[x]
在什麼條件下才不會編譯出錯?
要求的條件就和特定類型有關了:
-
P 必須有特定類型
-
對於 P 的特定類型的值 a,支持
a[x]
這種索引寫法 -
P 的所有特定類型必須相同。在這裏,string 類型的元素類型是 byte (https://github.com/golang/go/issues/49551)
-
如果 P 的特定類型包含 map 類型的話,那麼它的所有特定類型必須是 map, 而且所有的 key 的類型是相同的
-
所以有時候你定義了一個包含 map、slice、string 的聯合元素接口的話,這個接口的實例你不能使用
a[x]
索引類型,元素的類型都是 int -
a[x]
是數組、slice、string 的索引爲 x 的元素,或者是 map 類型 key 爲 x 的元素,a[x] 的類型必須相同 -
如果 P 的特定類型包含 string 類型,那麼不能給
a[x]
賦值 (字符串是不可變的)
特定類型還用作類型轉化定義上。
對於一個變量x
, 如果它的類型是V
, 要轉換成的類型是T
, 只要滿足下面一條,x 就可以轉換成T
類型:
-
V
的每一個特定類型的值都可以轉換成T
的每一個特定類型 -
只有
V
是類型參數,T
不是,那麼V
的每一個特定類型的值都可以轉換成T
-
只有
T
是類型參數,x
可以轉換成 T 的每一個特定類型
一句話,是類型參數就滿足每一個特定類型,不是類型參數就滿足這個類型。
另外,對於類型參數,要調用內建的函數len
、cap
,必須要求它們的特定類型允許使用這些內建函數。
3 structural type (結構類型)
一個接口T
要被成爲結構化的 (structural
), 需要滿足下面的條件之一:
-
存在一個單一的類型
U
, 它是T
的類型集合中的每一個元素相同的底層類型 -
T
的類型集合只包含 chan 類型,並且它們的元素類型都是E
, 所有的 chan 的方向包含相同的方向 (不一定要求完全相同)
結構化類型包含一個結構類型,根據上面的條件不同,結構類型可能是:
-
類型
U
, 或者 -
如果
T
只包含雙向 chan 的話,結構類型爲chan E
, 否則可能是chan<- E
或者<-chan E
下面是包含結構類型的結構化接口:
在 Go 語言規範中,並沒有對結構化接口有更多的介紹,如何使用,更多是是它內部獲取底層的結構類型,以及做類型檢查,比如下面的例子就會報no structural type
編譯錯誤:
下面的代碼也會報M has no structural type
編譯錯誤:
比如下面大家使用 Go 泛型的時候常會犯的錯誤, 雖然[]byte
、map[int]byte
、string
都能 range, 而且 key(index)、value 類型都一樣,但是也會報R has no structural type
錯誤:
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/zKnh_iPm8skxWv3rxaOscw