Go基礎:接口類型、接口嵌套、空接口、類型判斷

目錄

1. 接口

1.1. 接口

1.1.1. 接口類型

1.1.2. 爲什麼要使用接口

1.1.3. 接口的定義

1.1.4. 實現接口的條件

1.1.5. 接口類型變量

1.1.6. 值接收者和指針接收者實現接口的區別

1.1.7. 值接收者實現接口

1.1.8. 指針接收者實現接口

1.1.9. 下面的代碼是一個比較好的面試題

1.2. 類型與接口的關係

1.2.1. 一個類型實現多個接口

1.2.2. 多個類型實現同一接口

1.2.3. 接口嵌套

1.3. 空接口

1.3.1. 空接口的定義

1.3.2. 空接口的應用

1.3.3. 類型斷言


1. 接口

接口(interface)定義了一個對象的行爲規範只定義規範不實現,由具體的對象來實現規範的細節。

1.1. 接口

1.1.1. 接口類型

在Go語言中接口(interface)是一種類型,一種抽象的類型。

interface是一組method的集合,是duck-type programming的一種體現。接口做的事情就像是定義一個協議(規則),只要一臺機器有洗衣服和甩乾的功能,我就稱它爲洗衣機。不關心屬性(數據),只關心行爲(方法)。

爲了保護你的Go語言職業生涯,請牢記接口(interface)是一種類型。

1.1.2. 爲什麼要使用接口

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中所有的具體類型,接口是一種抽象的類型。

當你看到一個接口類型的值時,你不知道它是什麼,唯一知道的是通過它的方法能做什麼。

1.1.3. 接口的定義

Go語言提倡面向接口編程。

  1.     接口是一個或多個方法簽名的集合。
  2.     任何類型的方法集中只要擁有該接口'對應的全部方法'簽名,就表示它 "實現" 了該接口,無須在該類型上顯式聲明實現了哪 個接口。這稱爲Structural Typing。 所謂對應方法,是指有相同名稱、參數列表 (不包括參數名) 以及返回值。當然,該類型還可以有其他方法。
  3.     接口只有方法聲明,沒有實現,沒有數據字段。
  4.     接口可以匿名嵌入其他接口,或嵌入到結構中。
  5.     對象賦值給接口時,會發生拷貝,而接口內部存儲的是指向這個複製品的指針,既無法修改複製品的狀態,也無法獲取指針。
  6.     只有當接口存儲的類型和對象都爲nil時,接口才等於nil。
  7.     接口調用不會做receiver的自動轉換。
  8.     接口同樣支持匿名字段方法。
  9.     接口也可實現類似OOP中的多態。
  10.     空接口可以作爲任何類型數據的容器。
  11.     一個類型可實現多個接口。
  12.     接口命名習慣以 er 結尾。
     

每個接口由數個方法組成,接口的定義格式如下:

    type 接口類型名 interface{
        方法名1( 參數列表1 ) 返回值列表1
        方法名2( 參數列表2 ) 返回值列表2
        …
    }

其中:

  1. 接口名:使用type將接口定義爲自定義的類型名。Go語言的接口在命名時,一般會在單詞後面添加er,如有寫操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出該接口的類型含義。
  2. 方法名:當方法名首字母是大寫且這個接口類型名首字母也是大寫時,這個方法可以被接口所在的包(package)之外的代碼訪問。
  3. 參數列表、返回值列表:參數列表和返回值列表中的參數變量名可以省略。

舉個例子:

type writer interface{
    Write([]byte) error
}

當你看到這個接口類型的值時,你不知道它是什麼,唯一知道的就是可以通過它的Write方法來做一些事情。

1.1.4. 實現接口的條件

一個對象只要全部實現了接口中的方法,那麼就實現了這個接口。換句話說,接口就是一個需要實現的方法列表。

我們來定義一個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("喵喵喵")
}

接口的實現就是這麼簡單,只要實現了接口中的所有方法,就實現了這個接口。

1.1.5. 接口類型變量

那實現了接口有什麼用呢?

接口類型變量能夠存儲所有實現了該接口的實例。 例如上面的示例中,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()     // 汪汪汪
}

1.1.6. 值接收者和指針接收者實現接口的區別

使用值接收者實現接口和使用指針接收者實現接口有什麼區別呢?接下來我們通過一個例子看一下其中的區別。

我們有一個Mover接口和一個dog結構體。

type Mover interface {
    move()
}

type dog struct {}

1.1.7. 值接收者實現接口

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

1.1.8. 指針接收者實現接口

同樣的代碼我們再來測試一下使用指針接收者有什麼區別:

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類型的值。

1.1.9. 下面的代碼是一個比較好的面試題

請問下面的代碼是否能通過編譯?

type People interface {
    Speak(string) string
}

type Student struct{}

func (stu *Stduent) 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))
}

1.2. 類型與接口的關係

1.2.1. 一個類型實現多個接口

一個類型可以同時實現多個接口,而接口間彼此獨立,不知道對方的實現。 例如,狗可以叫,也可以動。我們就分別定義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()
}

1.2.2. 多個類型實現同一接口

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("洗刷刷")
}

1.2.3. 接口嵌套

接口與接口間可以通過嵌套創造出新的接口。

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

1.3. 空接口

1.3.1. 空接口的定義

空接口是指沒有定義任何方法的接口。因此任何類型都實現了空接口。

空接口類型的變量可以存儲任意類型的變量。

func main() {
    // 定義一個空接口x
    var x interface{}
    s := "pprof.cn"
    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)
}

1.3.2. 空接口的應用

空接口作爲函數的參數

使用空接口實現可以接收任意類型的函數參數。

// 空接口作爲函數參數
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)

1.3.3. 類型斷言

空接口可以存儲任意類型的值,那我們如何獲取其存儲的具體數據呢?

接口值

一個接口的值(簡稱接口值)是由一個具體類型和具體類型的值兩部分組成的。這兩部分分別稱爲接口的動態類型和動態值。

我們來看一個具體的例子:

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 = "pprof.cn"
    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語言中的使用十分廣泛。

關於接口需要注意的是,只有當有兩個或兩個以上的具體類型必須以相同的方式進行處理時才需要定義接口。不要爲了接口而寫接口,那樣只會增加不必要的抽象,導致不必要的運行時損耗。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章