接口
在 Go 語言中,接口是一種抽象類型,用於定義一組方法簽名而不提供方法的實現。接口的核心理念是描述行為,而具體的行為實現由實現接口的類型提供。接口在 Go 語言中廣泛用於實現多態性、松耦合和代碼復用。
概念
Go 關於接口的發展歷史有一個分水嶺,在 Go1.17 及以前,官方在參考手冊中對於接口的定義為:一組方法的集合。
An interface type specifies a method set called its interface.
接口實現的定義為
A variable of interface type can store a value of any type with a method set that is any superset of the interface. Such a type is said to implement the interface
翻譯過來就是,當一個類型的方法集是一個接口的方法集的超集時,且該類型的值可以由該接口類型的變量存儲,那麼稱該類型實現了該接口。
不過在 Go1.18 時,關於接口的定義發生了變化,接口定義為:一組類型的集合。
An interface type defines a type set.
接口實現的定義為
A variable of interface type can store a value of any type that is in the type set of the interface. Such a type is said to implement the interface
翻譯過來就是,當一個類型位於一個接口的類型集內,且該類型的值可以由該接口類型的變量存儲,那麼稱該類型實現了該接口。並且還給出了如下的額外定義。
當如下情況時,可以稱類型 T 實現了接口 I
- T 不是一個接口,並且是接口 I 類型集中的一個元素
- T 是一個接口,並且 T 的類型集是接口 I 類型集的一個子集
如果 T 實現了一個接口,那麼 T 的值也實現了該接口。
Go 在 1.18 最大的變化就是加入了泛型,新接口定義就是為了泛型而服務的,不過一點也不影響之前接口的使用,同時接口也分為了兩類,
- 基本接口(
Basic Interface):只包含方法集的接口就是基本接口 - 通用接口(
General Interface):只要包含類型集的接口就是通用接口
什麼是方法集,方法集就是一組方法的集合,同樣的,類型集就是一組類型的集合。
TIP
你或許會覺得這段概念很晦澀難懂,但實際上你完全不需要理解上面這一大段東西。
基本接口
前面講到了基本接口就是方法集,就是一組方法的集合。
聲明
先來看看接口長什麼樣子。
type Person interface {
Say(string) string
Walk(int)
}這是一個Person接口,有兩個對外暴露的方法Walk和Say,在接口裡,函數的參數名變得不再重要,當然如果想加上參數名和返回值名也是允許的。
初始化
僅僅只有接口是無法被初始化的,因為它僅僅只是一組規范,並沒有具體的實現,不過可以被聲明。
func main() {
var person Person
fmt.Println(person)
}輸出
<nil>實現
先來看一個例子,一個建築公司想一種特殊規格的起重機,於是給出了起重機的特殊規范和圖紙,並指明了起重機應該有起重和吊貨的功能,建築公司並不負責造起重機,只是給出了一個規范,這就叫接口,於是公司 A 接下了訂單,根據自家公司的獨門技術造出了絕世起重機並交給了建築公司,建築公司不在乎是用什麼技術實現的,也不在乎什麼絕世起重機,只要能夠起重和吊貨就行,僅僅只是當作一台普通起重機來用,根據規范提供具體的功能,這就叫實現,。只根據接口的規范來使用功能,屏蔽其內部實現,這就叫面向接口編程。過了一段時間,絕世起重機出故障了,公司 A 也跑路了,於是公司 B 依據規范造了一台更厲害的巨無霸起重機,由於同樣具有起重和吊貨的功能,可以與絕世起重機無縫銜接,並不影響建築進度,建築得以順利完成,內部實現改變而功能不變,不影響之前的使用,可以隨意替換,這就是面向接口編程的好處。
接下來會用 Go 描述上述情形
// 起重機接口
type Crane interface {
JackUp() string
Hoist() string
}
// 起重機A
type CraneA struct {
work int //內部的字段不同代表內部細節不一樣
}
func (c CraneA) Work() {
fmt.Println("使用技術A")
}
func (c CraneA) JackUp() string {
c.Work()
return "jackup"
}
func (c CraneA) Hoist() string {
c.Work()
return "hoist"
}
// 起重機B
type CraneB struct {
boot string
}
func (c CraneB) Boot() {
fmt.Println("使用技術B")
}
func (c CraneB) JackUp() string {
c.Boot()
return "jackup"
}
func (c CraneB) Hoist() string {
c.Boot()
return "hoist"
}
type ConstructionCompany struct {
Crane Crane // 只根據Crane類型來存放起重機
}
func (c *ConstructionCompany) Build() {
fmt.Println(c.Crane.JackUp())
fmt.Println(c.Crane.Hoist())
fmt.Println("建築完成")
}
func main() {
// 使用起重機A
company := ConstructionCompany{CraneA{}}
company.Build()
fmt.Println()
// 更換起重機B
company.Crane = CraneB{}
company.Build()
}輸出
使用技術A
jackup
使用技術A
hoist
建築完成
使用技術B
jackup
使用技術B
hoist
建築完成上面例子中,可以觀察到接口的實現是隱式的,也對應了官方對於基本接口實現的定義:方法集是接口方法集的超集,所以在 Go 中,實現一個接口不需要implements關鍵字顯式的去指定要實現哪一個接口,只要是實現了一個接口的全部方法,那就是實現了該接口。有了實現之後,就可以初始化接口了,建築公司結構體內部聲明了一個Crane類型的成員變量,可以保存所有實現了Crane接口的值,由於是Crane 類型的變量,所以能夠訪問到的方法只有JackUp 和Hoist,內部的其他方法例如Work和Boot都無法訪問。
之前提到過任何自定義類型都可以擁有方法,那麼根據實現的定義,任何自定義類型都可以實現接口,下面舉幾個比較特殊的例子。
type Person interface {
Say(string) string
Walk(int)
}
type Man interface {
Exercise()
Person
}Man接口方法集是Person的超集,所以Man也實現了接口Person,不過這更像是一種"繼承"。
type Number int
func (n Number) Say(s string) string {
return "bibibibibi"
}
func (n Number) Walk(i int) {
fmt.Println("can not walk")
}類型Number的底層類型是int,雖然這放在其他語言中看起來很離譜,但Number的方法集確實是Person 的超集,所以也算實現。
type Func func()
func (f Func) Say(s string) string {
f()
return "bibibibibi"
}
func (f Func) Walk(i int) {
f()
fmt.Println("can not walk")
}
func main() {
var function Func
function = func() {
fmt.Println("do somthing")
}
function()
}同樣的,函數類型也可以實現接口。
空接口
type Any interface{
}Any接口內部沒有方法集合,根據實現的定義,所有類型都是Any接口的的實現,因為所有類型的方法集都是空集的超集,所以Any接口可以保存任何類型的值。
func main() {
var anything Any
anything = 1
println(anything)
fmt.Println(anything)
anything = "something"
println(anything)
fmt.Println(anything)
anything = complex(1, 2)
println(anything)
fmt.Println(anything)
anything = 1.2
println(anything)
fmt.Println(anything)
anything = []int{}
println(anything)
fmt.Println(anything)
anything = map[string]int{}
println(anything)
fmt.Println(anything)
}輸出
(0xe63580,0xeb8b08)
1
(0xe63d80,0xeb8c48)
something
(0xe62ac0,0xeb8c58)
(1+2i)
(0xe62e00,0xeb8b00)
1.2
(0xe61a00,0xc0000080d8)
[]
(0xe69720,0xc00007a7b0)
map[]通過輸出會發現,兩種輸出的結果不一致,其實接口內部可以看成是一個由(val,type)組成的元組,type是具體類型,在調用方法時會去調用具體類型的具體值。
interface{}這也是一個空接口,不過是一個匿名空接口,在開發時通常會使用匿名空接口來表示接收任何類型的值,例子如下
func main() {
DoSomething(map[int]string{})
}
func DoSomething(anything interface{}) interface{} {
return anything
}在後續的更新中,官方提出了另一種解決辦法,為了方便起見,可以使用any來替代interace{},兩者是完全等價的,因為前者僅僅只是一個類型別名,如下
type any = interface{}在比較空接口時,會對其底層類型進行比較,如果類型不匹配的話則為false,其次才是值的比較,例如
func main() {
var a interface{}
var b interface{}
a = 1
b = "1"
fmt.Println(a == b)
a = 1
b = 1
fmt.Println(a == b)
}輸出為
false
true如果底層的類型是不可比較的,那麼會panic,對於 Go 而言,內置數據類型是否可比較的情況如下
| 類型 | 可比較 | 依據 |
|---|---|---|
| 數字類型 | 是 | 值是否相等 |
| 字符串類型 | 是 | 值是否相等 |
| 數組類型 | 是 | 數組的全部元素是否相等 |
| 切片類型 | 否 | 不可比較 |
| 結構體 | 是 | 字段值是否全部相等 |
| map 類型 | 否 | 不可比較 |
| 通道 | 是 | 地址是否相等 |
| 指針 | 是 | 指針存儲的地址是否相等 |
| 接口 | 是 | 底層所存儲的數據是否相等 |
在 Go 中有一個專門的接口類型用於代表所有可比較類型,即comparable
type comparable interface{ comparable }TIP
如果嘗試對不可比較的類型進行比較,則會panic
通用接口
通用接口就是為了泛型服務的,只要掌握了泛型,就掌握了通用接口,請移步泛型
