title: 【轉載】Go 語言基礎之介面
date: 2021-08-09 16:41:33
comment: false
toc: true
category:
- Golang
tags: - 轉載
- Go
- 基礎
- 介面
本文轉載自:Go 語言基礎之介面 | 李文周的博客
介面(interface)定義了一個物件的行為規範,只定義規範不實現,由具體的物件來實現規範的細節。
介面#
介面類型#
在 Go 語言中介面(interface)是一種類型,一種抽象的類型。
interface
是一組method
的集合,是duck-type programming
的一種體現。介面做的事情就像是定義一個協議(規則),只要一台機器有洗衣服和脫水的功能,我就稱它為洗衣機。不關心屬性(數據),只關心行為(方法)。
為了保護你的 Go 語言職業生涯,請牢記介面(interface)是一種類型。
為什麼要使用介面#
type Cat struct{}
func (c Cat) Say() string { return "喵喵喵" }
type Dog struct{}
func (d Dog) Say() string { return "汪汪汪" }
func main() {
c := Cat{}
fmt.Println("貓:", c.Say())
d := Dog{}
fmt.Println("狗:", d.Say())
}
上面的代碼中定義了貓和狗,然後它們都會叫,你會發現 main 函數中明顯有重複的代碼,如果我們後續再加上豬、青蛙等動物的話,我們的代碼還會一直重複下去。那我們能不能把它們當成 “能叫的動物” 來處理呢?
像類似的例子在我們編程過程中會經常遇到:
比如一個網上商城可能使用支付寶、微信、銀聯等方式去在線支付,我們能不能把它們當成 “支付方式” 來處理呢?
比如三角形、四邊形、圓形都能計算周長和面積,我們能不能把它們當成 “圖形” 來處理呢?
比如銷售、行政、程序員都能計算月薪,我們能不能把他們當成 “員工” 來處理呢?
Go 語言中為了解決類似上面的問題,就設計了介面這個概念。介面區別於我們之前所有的具體類型,介面是一種抽象的類型。當你看到一個介面類型的值時,你不知道它是什麼,唯一知道的是通過它的方法能做什麼。
介面的定義#
Go 語言提倡面向介面編程。
每個介面由數個方法組成,介面的定義格式如下:
type 介面類型名 interface{
方法名1( 參數列表1 ) 返回值列表1
方法名2( 參數列表2 ) 返回值列表2
…
}
其中:
- 介面名:使用
type
將介面定義為自定義的類型名。Go 語言的介面在命名時,一般會在單詞後面添加er
,如有寫操作的介面叫Writer
,有字符串功能的介面叫Stringer
等。介面名最好要能突出該介面的類型含義。 - 方法名:當方法名首字母是大寫且這個介面類型名首字母也是大寫時,這個方法可以被介面所在的包(package)之外的代碼訪問。
- 參數列表、返回值列表:參數列表和返回值列表中的參數變量名可以省略。
舉個例子:
type writer interface{
Write([]byte) error
}
當你看到這個介面類型的值時,你不知道它是什麼,唯一知道的就是可以通過它的 Write 方法來做一些事情。
實現介面的條件#
一個物件只要全部實現了介面中的方法,那麼就實現了這個介面。換句話說,介面就是一個需要實現的方法列表 。
我們來定義一個Sayer
介面:
// Sayer 介面
type Sayer interface {
say()
}
定義dog
和cat
兩個構造體:
type dog struct {}
type cat struct {}
因為Sayer
介面裡只有一個say
方法,所以我們只需要給dog
和cat
分別實現say
方法就可以實現Sayer
介面了。
// dog實現了Sayer介面
func (d dog) say() {
fmt.Println("汪汪汪")
}
// cat實現了Sayer介面
func (c cat) say() {
fmt.Println("喵喵喵")
}
介面的實現就是這麼簡單,只要實現了介面中的所有方法,就實現了這個介面。
介面類型變量#
那實現了介面有什麼用呢?
介類型變量能夠存儲所有實現了該介面的實例。例如上面的示例中,Sayer
類型的變量能夠存儲dog
和cat
類型的變量。
func main() {
var x Sayer // 聲明一個Sayer類型的變量x
a := cat{} // 實例化一個cat
b := dog{} // 實例化一個dog
x = a // 可以把cat實例直接賦值給x
x.say() // 喵喵喵
x = b // 可以把dog實例直接賦值給x
x.say() // 汪汪汪
}
Tips: 觀察下面的代碼,體味此處_
的妙用
// 摘自gin框架routergroup.go
type IRouter interface{ ... }
type RouterGroup struct { ... }
var _ IRouter = &RouterGroup{} // 確保RouterGroup實現了介面IRouter
值接收者和指針接收者實現介面的區別#
使用值接收者實現介面和使用指針接收者實現介面有什麼區別呢?接下來我們通過一個例子看一下其中的區別。
我們有一個Mover
介面和一個dog
構造體。
type Mover interface {
move()
}
type dog struct {}
值接收者實現介面#
func (d dog) move() {
fmt.Println("狗會動")
}
此時實現介面的是dog
類型:
func main() {
var x Mover
var wangcai = dog{} // 旺財是dog類型
x = wangcai // x可以接收dog類型
var fugui = &dog{} // 富貴是*dog類型
x = fugui // x可以接收*dog類型
x.move()
}
從上面的代碼中我們可以發現,使用值接收者實現介面之後,不管是 dog 構造體還是構造體指針 * dog 類型的變量都可以賦值給該介類變量。因為 Go 語言中有對指針類型變量求值的語法糖,dog 指針fugui
內部會自動求值*fugui
。
指針接收者實現介面#
同樣的代碼我們再來測試一下使用指針接收者有什麼區別:
func (d *dog) move() {
fmt.Println("狗會動")
}
func main() {
var x Mover
var wangcai = dog{} // 旺財是dog類型
x = wangcai // x不可以接收dog類型
var fugui = &dog{} // 富貴是*dog類型
x = fugui // x可以接收*dog類型
}
此時實現Mover
介面的是*dog
類型,所以不能給x
傳入dog
類型的 wangcai,此時 x 只能存儲*dog
類型的值。
面試題#
注意: 這是一道你需要回答 “能” 或者 “不能” 的題!
首先請觀察下面的這段代碼,然後請回答這段代碼能不能通過編譯?
type People interface {
Speak(string) string
}
type Student struct{}
func (stu *Student) Speak(think string) (talk string) {
if think == "sb" {
talk = "你是個大帥比"
} else {
talk = "您好"
}
return
}
func main() {
var peo People = Student{}
think := "bitch"
fmt.Println(peo.Speak(think))
}
類型與介面的關係#
一個類型實現多個介面#
一個類型可以同時實現多個介面,而介面間彼此獨立,不知道對方的實現。例如,狗可以叫,也可以動。我們就分別定義 Sayer 介面和 Mover 介面,如下: Mover
介面。
// Sayer 介面
type Sayer interface {
say()
}
// Mover 介面
type Mover interface {
move()
}
dog 既可以實現 Sayer 介面,也可以實現 Mover 介面。
type dog struct {
name string
}
// 實現Sayer介面
func (d dog) say() {
fmt.Printf("%s會叫汪汪汪\n", d.name)
}
// 實現Mover介面
func (d dog) move() {
fmt.Printf("%s會動\n", d.name)
}
func main() {
var x Sayer
var y Mover
var a = dog{name: "旺財"}
x = a
y = a
x.say()
y.move()
}
多個類型實現同一介面#
Go 語言中不同的類型還可以實現同一介面 首先我們定義一個Mover
介面,它要求必須由一個move
方法。
// Mover 介面
type Mover interface {
move()
}
例如狗可以動,汽車也可以動,可以使用如下代碼實現這個關係:
type dog struct {
name string
}
type car struct {
brand string
}
// dog類型實現Mover介面
func (d dog) move() {
fmt.Printf("%s會跑\n", d.name)
}
// car類型實現Mover介面
func (c car) move() {
fmt.Printf("%s速度70邁\n", c.brand)
}
這個時候我們在代碼中就可以把狗和汽車當成一個會動的物體來處理了,不再需要關注它們具體是什麼,只需要調用它們的move
方法就可以了。
func main() {
var x Mover
var a = dog{name: "旺財"}
var b = car{brand: "保時捷"}
x = a
x.move()
x = b
x.move()
}
上面的代碼執行結果如下:
旺財會跑
保時捷速度70邁
並且一個介面的方法,不一定需要由一個類型完全實現,介面的方法可以通過在類型中嵌入其他類型或者結構體來實現。
// WashingMachine 洗衣機
type WashingMachine interface {
wash()
dry()
}
// 甩乾器
type dryer struct{}
// 實現WashingMachine介面的dry()方法
func (d dryer) dry() {
fmt.Println("甩一甩")
}
// 海爾洗衣機
type haier struct {
dryer //嵌入甩乾器
}
// 實現WashingMachine介面的wash()方法
func (h haier) wash() {
fmt.Println("洗刷刷")
}
介面嵌套#
介面與介面間可以通過嵌套創造出新的介面。
// Sayer 介面
type Sayer interface {
say()
}
// Mover 介面
type Mover interface {
move()
}
// 介面嵌套
type animal interface {
Sayer
Mover
}
嵌套得到的介面的使用與普通介面一樣,這裡我們讓 cat 實現 animal 介面:
type cat struct {
name string
}
func (c cat) say() {
fmt.Println("喵喵喵")
}
func (c cat) move() {
fmt.Println("貓會動")
}
func main() {
var x animal
x = cat{name: "花花"}
x.move()
x.say()
}
空介面#
空介面的定義#
空介面是指沒有定義任何方法的介面。因此任何類型都實現了空介面。
空介型的變量可以存儲任意類型的變量。
func main() {
// 定義一個空介面x
var x interface{}
s := "Hello 沙河"
x = s
fmt.Printf("type:%T value:%v\n", x, x)
i := 100
x = i
fmt.Printf("type:%T value:%v\n", x, x)
b := true
x = b
fmt.Printf("type:%T value:%v\n", x, x)
}
空介面的應用#
空介面作為函數的參數#
使用空介面實現可以接收任意類型的函數參數。
// 空介面作為函數參數
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}
空介面作為 map 的值#
使用空介面實現可以保存任意值的字典。
// 空介面作為map值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "沙河娜扎"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)
類型斷言#
空介面可以存儲任意類型的值,那我們如何獲取其存儲的具體數據呢?
介面值#
一個介面的值(簡稱介面值)是由一個具體類型
和具體類型的值
兩部分組成的。這兩部分分別稱為介面的動態類型
和動態值
。
我們來看一個具體的例子:
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
想要判斷空介面中的值這個時候就可以使用類型斷言,其語法格式:
x.(T)
其中:
- x:表示類型為
interface{}
的變量 - T:表示斷言
x
可能是的類型。
該語法返回兩個參數,第一個參數是x
轉化為T
類型後的變量,第二個值是個布爾值,若為true
則表示斷言成功,為false
則表示斷言失敗。
舉個例子:
func main() {
var x interface{}
x = "Hello 沙河"
v, ok := x.(string)
if ok {
fmt.Println(v)
} else {
fmt.Println("類型斷言失敗")
}
}
上面的示例中如果要斷言多次就需要寫多個if
判斷,這個時候我們可以使用switch
語句來實現:
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("x is a string,value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
default:
fmt.Println("unsupport type!")
}
}
因為空介面可以存儲任意類型值的特點,所以空介面在 Go 語言中的使用十分廣泛。
關於介面需要注意的是,只有當有兩個或兩個以上的具體類型必須以相同的方式進行處理時才需要定義介面。不要為了介面而寫介面,那樣只會增加不必要的抽象,導致不必要的運行時損耗。
練習題#
使用介面的方式實現一個既可以往終端寫日誌也可以往文件寫日誌的簡易日誌庫。