Go語言學習20-介面
Go語言學習20-介面
先宣告一下,介面是Go語言第二個勸退點。如果想不明白,就自己默唸三遍:介面是一種型別,是一種引用型別。
最開始我們使用程式語言,是C語言這樣的面向過程程式設計;後來面向過程逐漸不被人所選擇,於是出現了C++,Java,PHP,Python等面向物件的程式語言。
用Java等語言編寫到後面就會發現很臃腫,啥都是物件。
再後來,蘋果出的swift語言和Go語言,又提出了一個新的理念,就是面向介面程式設計。PHP、Java等語言中也有介面的概念,不過在PHP和Java語言中需要顯式宣告一個類實現了哪些介面,在Go語言中使用隱式宣告的方式實現介面。只要一個型別實現了介面中規定的所有方法,那麼它就實現了這個介面。
每次學到一個新的概念,都要去思考一下,為什麼會有新的概念出現呢?新的概念解決了什麼痛點呢?出現前和出現後有什麼區別呢?
我們呼叫的print,傳入任何的型別,都能打印出來;而我們學的函式,必須要有特定的型別才行。
比如,我買東西,我不管用什麼支付,用的微信還是支付寶還是銀聯,只要能給我付錢即可,能不能把他們當作“支付方式”來處理呢?
比如,三角形,四邊形,圓形都能計算周長和麵積,能不能治把他們當成“圖形”處理?
比如,銷售、行政、程式設計師都能計算月薪,能不能統一把他們當成“員工”處理呢?
比如,資料庫有多種,Mysql、Oracle、Mssql等等,特別多,存入一個人的資訊都可以,能不能使用這些資料庫的增刪改查方法呢?
Go語言中為了解決類似上面的問題,就設計出了一個概念——介面,介面區別於我們之前所有的具體型別,介面是一種抽象的型別。當你看到一個介面型別的值時,你不知道它是什麼,唯一知道的是通過它的方法能做什麼。粗淺的認知:介面是多種通用方法的集合。
0x00 介面引入—小試牛刀
1、首先我定義了狗、貓、人三種結構體型別。並且定義了三個對應方法,都是bark叫喚的意思
2、其次我定義了一個方法叫做打。我現在想打誰我就傳進那個函式來打誰。
3、但是如果這時候x的型別是一個dog,那就只能傳dog型別的;如果x的型別是一個cat,那就只能傳cat型別的。。這就出現了弊端了。
4、但是我們發現了一個規律,就是這些個型別,都有bark方法。
5、我們希望,在main函式中,能夠這樣傳入,來使用。
type dog struct{}
type cat struct{}
type person struct{}
func (c cat) bark() {
fmt.Println("喵喵喵!")
}
func (d dog) bark() {
fmt.Println("wangwangwnag")
}
func (p person) bark() {
fmt.Println("Yingyingying!")
}
func beat(x){
//接收一個引數,傳進來什麼,我就打什麼
x.bark() //
}
func main(){
var c1 cat
var d1 dog
var p1 person
}
綜上,我們得出個結論,一個場景:
我不關心一個變數是什麼型別,我只關心能呼叫它的什麼方法。
0x01 介面(interface)
介面是一種型別,是一種特殊的型別,引用型別。它規定、約束了變數的方法。
在程式設計中會遇到以下場景:我不關心一個變數是什麼型別,我只關心能呼叫它的什麼方法。
type speaker interface{
bark()
}
這裡面只規定了你要實現什麼方法,而沒有指定具體的型別。
type dog struct{}
type cat struct{}
type person struct{}
type speaker interface {
bark() //將方法統一歸類,方法簽名,可以有一個,也可以多個
}
func (c cat) bark() {
fmt.Println("喵喵喵!")
}
func (d dog) bark() {
fmt.Println("wangwangwnag")
}
func (p person) bark() {
fmt.Println("Yingyingying!")
}
func beat(x speaker) {
//接收一個引數,傳進來什麼,我就打什麼
x.bark() //
}
func main() {
var c1 cat
var d1 dog
var p1 person
beat(c1)
beat(d1)
beat(p1)
}
0x02 介面示例練習
多品牌跑車
type falali struct {
brand string
}
func (f falali) run() {
fmt.Println("速度七十邁!")
}
type baoma struct {
brand string
}
func (b baoma) run() {
fmt.Println("全力奔跑,許下心願!")
}
type car interface {
run()
}
//drive函式接收一個car型別的變數,即c為介面型別。
//這個介面型別,不管什麼結構體,只要有run方法,都是car
func drive(c car) {
c.run()
}
func main() {
f1 := falali{brand: "法拉利"}
b1 := baoma{brand: "寶馬"}
drive(f1)
drive(b1)
}
小總結一下:只要有統一的方法,不管你是什麼型別,都可以傳入進來,用介面型別,去約束具體型別的方法。
多元支付方式
Go語言中的這種設計符合程式開發中抽象的一般規律,例如在下面的程式碼示例中,我們的電商系統最開始只設計了支付寶一種支付方式:
type ZhiFuBao struct {
// 支付寶
}
// Pay 支付寶的支付方法
func (z *ZhiFuBao) Pay(amount int64) {
fmt.Printf("使用支付寶付款:%.2f元。\n", float64(amount/100))
}
// Checkout 結賬
func Checkout(obj *ZhiFuBao) {
// 支付100元
obj.Pay(100)
}
func main() {
Checkout(&ZhiFuBao{})
}
隨著業務的發展,根據使用者需求新增支援微信支付。
type WeChat struct {
// 微信
}
// Pay 微信的支付方法
func (w *WeChat) Pay(amount int64) {
fmt.Printf("使用微信付款:%.2f元。\n", float64(amount/100))
}
在實際的交易流程中,我們可以根據使用者選擇的支付方式來決定最終呼叫支付寶的Pay方法還是微信支付的Pay方法。
// Checkout 支付寶結賬
func CheckoutWithZFB(obj *ZhiFuBao) {
// 支付100元
obj.Pay(100)
}
// Checkout 微信支付結賬
func CheckoutWithWX(obj *WeChat) {
// 支付100元
obj.Pay(100)
}
實際上,從上面的程式碼示例中我們可以看出,我們其實並不怎麼關心使用者選擇的是什麼支付方式,我們只關心呼叫Pay方法時能否正常執行。這就是典型的“不關心它是什麼,只關心它能做什麼”的場景。
在這種場景下我們可以將具體的支付方式抽象為一個名為Payer
的介面型別,即任何實現了Pay
方法的都可以稱為Payer
型別。
// Payer 包含支付方法的介面型別
type Payer interface {
Pay(int64)
}
此時只需要修改下原始的Checkout
函式,它接收一個Payer
型別的引數。這樣就能夠在不修改既有函式呼叫的基礎上,支援新的支付方式。
// Checkout 結賬
func Checkout(obj Payer) {
// 支付100元
obj.Pay(100)
}
func main() {
Checkout(&ZhiFuBao{}) // 之前呼叫支付寶支付
Checkout(&WeChat{}) // 現在支援使用微信支付
}
0x03 介面詳解
type 介面名 interface{
方法名1(引數1,引數2...)(返回值1,返回值2...)
方法名2(引數1,引數2...)(返回值1,返回值2...)
...
}
用來給變數\引數\返回值等設定型別用。
介面的實現
一個變數如果實現了介面中規定的所有的方法,那麼這個變數就實現了這個介面,可以稱為這個介面型別的變數。
還有就是我僅僅建立了兩個方法,少了個fuck方法
package main
import "fmt"
//1、先建立一個介面,動物。動物的行為方法在下方寫上。
type animal interface {
//1.1、這裡面都是方法,所以括號裡面可以新增任意的引數
move()
eat(s string) //eat(string)
}
//2、造動物
type cat struct {
name string
feet int8
}
//3、寫兩個貓的方法
func (c cat) move() {
fmt.Println("走貓步!")
}
func (c cat) eat(food string) {
fmt.Printf("貓吃:%s...\n", food)
}
type chicken struct {
name string
feet int8
}
//4、寫兩個雞的方法
func (c chicken) move() {
fmt.Println("雞動!")
}
func (c chicken) eat(food string) {
fmt.Printf("雞吃:%s...", food)
}
func main() {
//6、建立一個介面型別
var a1 animal
//5、建立一個貓
bc := cat{
name: "虹貓",
feet: 4,
}
//7、這裡將bc這個貓賦予給a1這個介面,但是無效,是因為
//animal中的eat方法是帶有string型別的引數的,而貓的eat方法無引數;還有就是我僅僅建立了兩個方法,少了個fuck方法
//所以a1 = bc失敗,應該寫成帶有string引數的就好了
a1 = bc
a1.eat("小黃魚") //一切正確後,輸出:"貓吃:小黃魚..."
//fmt.Println(a1)
//8、繼續,新建一個雞
kfc := chicken{
name: "戰鬥機!",
feet: 2,
}
//9、將雞賦予給a1這個介面,a1這個介面再呼叫裡面的兩個方法,最終成功輸出
a1 = kfc
a1.move()
a1.eat("蟲子和沙子!")
}
0x04 分析一下介面型別
看下面這張圖,為什麼p1不是介面型別,而是main.Japanese
和main.Chinese
?
介面儲存的分為兩部分,值的型別和值本身,我們打印出來的,是值的型別。所以上面的圖,有兩種型別,就是動態實現了。
這樣才能保證介面能接受不同型別的資料和值。他只是一個約束性的,不是一個定量。
main函式之外的方法,是值接收者。當main函式中,c2為指標型別,那麼c2也是可以給a1進行賦值的。
type animal interface {
move()
eat(string)
}
type cat struct {
name string
feet int8
}
//這裡的函式都是值接收者
func (c cat) move() {
fmt.Println("走貓步...")
}
func (c cat) eat(food string) {
fmt.Printf("貓吃%s...\n", food)
}
//我現在使用指標接收者
func main() {
var a1 animal
c1 := cat{"tom", 4} //cat型別
c2 := &cat{"jerry", 4} //*cat
a1 = c1
fmt.Println("a1:", a1)
a1 = c2
fmt.Println("a1_2:", a1)
}
而如果main函式外面的函式都是指標接收者,那麼main函式裡面c1就無法給a1進行賦值了。而c2還可以賦值,因為c2就是指標型別的。
type animal interface {
move()
eat(string)
}
type cat struct {
name string
feet int8
}
//這裡的函式都是指標接收者
func (c *cat) move() {
fmt.Println("走貓步...")
}
func (c *cat) eat(food string) {
fmt.Printf("貓吃%s...\n", food)
}
//我現在使用指標接收者
func main() {
var a1 animal
c1 := cat{"tom", 4} //cat型別
c2 := &cat{"jerry", 4} //*cat
a1 = c1 //這裡會報錯!!!因為c1不是指標型別的!解決方案就是:a1 = &c1
fmt.Println("a1:", a1)
a1 = c2
fmt.Println("a1_2:", a1)
}
使用值接收者和指標接收者的區別?
使用值接收者,實現介面、結構體型別和結構體指標型別的變數都能存。指標接收者實現介面只能存結構體指標型別的變數。
介面和型別的關係?
把介面當作一種約束即可
0x05 空介面
如果一個變數,實現了接口裡面所有的方法,那麼這個變數就實現了一個介面。
如果我這個變數什麼都沒有,相當於任何型別都實現這個介面。
沒有必要起名字,就是空介面。通常定義成下面的格式:
interface{} //空介面,沒有必要使用type
所有的型別都實現了空介面,也就是任意型別的變數都能儲存到空介面中。
空介面作為map的值
在學習map的時候,我們這樣子定義和初始化map,能夠看到,這裡都是對應好的型別。string——>int,不能再使用別的型別了。
func main() {
m1 := map[string]int{
"name": 10,
"fuck": 10,
"shit": 30,
}
fmt.Println("m1", m1)
}
但是我們可以使用空介面來實現多種型別的map
func main() {
m1 := map[string]interface{}{
"name": 10,
"fuck": "asdasd",
"shit": []string{"nbeijing", "shanghai", "weishnida"},
}
fmt.Println("m1", m1)
}
空介面作為函式引數
同理,對於函式來說,我們傳入的引數是有對應的型別的,不然就會報錯。
func show(x int) {
fmt.Printf("此型別為:%T, 此型別值為:%v\n, x, x)
}
func main() {
show(10)
}
但是通過使用空介面作為函式引數,就可以達到萬能的效果。
func show(x interface{}) {
fmt.Printf("此型別為:%T, 此型別值為:%v\n", x, x)
}
func main() {
show(10)
show('a')
show("asdad")
show([...]string{"dad", "asd", "asd1"})
}
型別斷言
介面值可能賦值為任意型別的值,那我們如何從介面值獲取其儲存的具體資料呢?
我們可以藉助標準庫fmt
包的格式化列印獲取到介面值的動態型別。
var m Mover
m = &Dog{Name: "旺財"}
fmt.Printf("%T\n", m) // *main.Dog
m = new(Car)
fmt.Printf("%T\n", m) // *main.Car
而fmt
包內部其實是使用反射的機制在程式執行時獲取到動態型別的名稱。關於反射的內容我們會在後續章節詳細介紹。
而想要從介面值中獲取到對應的實際值需要使用型別斷言,其語法格式如下。
x.(T)
其中:
- x:表示介面型別的變數
- T:表示斷言
x
可能是的型別。
該語法返回兩個引數,第一個引數是x
轉化為T
型別後的變數,第二個值是一個布林值,若為true
則表示斷言成功,為false
則表示斷言失敗。
舉個列子:如果對一個介面值有多個實際型別需要判斷,推薦使用switch
語句來實現。
//型別斷言1
func assign(a interface{}) {
fmt.Printf("%T\n", a)
str, ok := a.(string)
if !ok {
fmt.Println("不好意思你猜錯了!")
} else {
fmt.Println("傳進來的是一個字串!", str)
}
}
//型別斷言2
func assign2(a interface{}) {
fmt.Printf("%T\n", a)
switch t := a.(type) {
case string:
fmt.Println("是一個字串:", t)
case int:
fmt.Println("是一個int", t)
case int64:
fmt.Println("是一個int64", t)
case bool:
fmt.Println("是一個bool", t)
}
}
func main() {
assign("hello")
assign2("jhello!")
}
注意
由於介面型別變數能夠動態儲存不同型別值的特點,所以很多初學者會濫用介面型別(特別是空介面)來實現編碼過程中的便捷。只有當有兩個或兩個以上的具體型別必須以相同的方式進行處理時才需要定義介面。切記不要為了使用介面型別而增加不必要的抽象,導致不必要的執行時損耗。
在 Go 語言中介面是一個非常重要的概念和特性,使用介面型別能夠實現程式碼的抽象和解耦,也可以隱藏某個功能的內部實現,但是缺點就是在檢視原始碼的時候,不太方便查詢到具體實現介面的型別。說白了,不要隨便定義介面,只有多個結構體共同需要一個介面來約束時,再去使用這個約束,方法。
相信很多讀者在剛接觸到介面型別時都會有很多疑惑,請牢記介面是一種型別,一種抽象的型別。區別於我們在之前章節提到的那些具體型別(整型、陣列、結構體型別等),它是一個只要求實現特定方法的抽象型別。