Skip to content

接口

在 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

你或許會覺得這段概念很晦澀難懂,但實際上你完全不需要理解上面這一大段東西。

基本接口

前面講到了基本接口就是方法集,就是一組方法的集合。

聲明

先來看看接口長什麼樣子。

go
type Person interface {
  Say(string) string
  Walk(int)
}

這是一個Person接口,有兩個對外暴露的方法WalkSay,在接口裡,函數的參數名變得不再重要,當然如果想加上參數名和返回值名也是允許的。

初始化

僅僅只有接口是無法被初始化的,因為它僅僅只是一組規范,並沒有具體的實現,不過可以被聲明。

go
func main() {
   var person Person
   fmt.Println(person)
}

輸出

 <nil>

實現

先來看一個例子,一個建築公司想一種特殊規格的起重機,於是給出了起重機的特殊規范和圖紙,並指明了起重機應該有起重和吊貨的功能,建築公司並不負責造起重機,只是給出了一個規范,這就叫接口,於是公司 A 接下了訂單,根據自家公司的獨門技術造出了絕世起重機並交給了建築公司,建築公司不在乎是用什麼技術實現的,也不在乎什麼絕世起重機,只要能夠起重和吊貨就行,僅僅只是當作一台普通起重機來用,根據規范提供具體的功能,這就叫實現,。只根據接口的規范來使用功能,屏蔽其內部實現,這就叫面向接口編程。過了一段時間,絕世起重機出故障了,公司 A 也跑路了,於是公司 B 依據規范造了一台更厲害的巨無霸起重機,由於同樣具有起重和吊貨的功能,可以與絕世起重機無縫銜接,並不影響建築進度,建築得以順利完成,內部實現改變而功能不變,不影響之前的使用,可以隨意替換,這就是面向接口編程的好處。

接下來會用 Go 描述上述情形

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 類型的變量,所以能夠訪問到的方法只有JackUpHoist,內部的其他方法例如WorkBoot都無法訪問。

之前提到過任何自定義類型都可以擁有方法,那麼根據實現的定義,任何自定義類型都可以實現接口,下面舉幾個比較特殊的例子。

go
type Person interface {
   Say(string) string
   Walk(int)
}

type Man interface {
   Exercise()
   Person
}

Man接口方法集是Person的超集,所以Man也實現了接口Person,不過這更像是一種"繼承"。

go
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 的超集,所以也算實現。

go
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()
}

同樣的,函數類型也可以實現接口。

空接口

go
type Any interface{

}

Any接口內部沒有方法集合,根據實現的定義,所有類型都是Any接口的的實現,因為所有類型的方法集都是空集的超集,所以Any接口可以保存任何類型的值。

go
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是具體類型,在調用方法時會去調用具體類型的具體值。

go
interface{}

這也是一個空接口,不過是一個匿名空接口,在開發時通常會使用匿名空接口來表示接收任何類型的值,例子如下

go
func main() {
   DoSomething(map[int]string{})
}

func DoSomething(anything interface{}) interface{} {
   return anything
}

在後續的更新中,官方提出了另一種解決辦法,為了方便起見,可以使用any來替代interace{},兩者是完全等價的,因為前者僅僅只是一個類型別名,如下

go
type any = interface{}

在比較空接口時,會對其底層類型進行比較,如果類型不匹配的話則為false,其次才是值的比較,例如

go
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

go
type comparable interface{ comparable }

TIP

如果嘗試對不可比較的類型進行比較,則會panic

通用接口

通用接口就是為了泛型服務的,只要掌握了泛型,就掌握了通用接口,請移步泛型

Golang學習網由www.golangdev.cn整理維護