Golang Interface 詳解(上)

  獨特的 “非侵入式” 接口設計是 Go 語言的一亮點。接口使得 Go 這種靜態型語言有了動態型語言的特性,提供了非常大的靈活性。Go 語言的成功,接口功不可沒。

Golang 與 "鴨子類型" 的關係

  Ducking Typing , 鴨子類型,是動態編程語言的一種對象推斷策略,它主要關注於對象如何被使用,而不是對象類型的本身。Go 語言作爲一門靜態語言,它通過 interface 的方式完美支持鴨子類型。

  Go 語言作爲一門現代靜態語言,吸取了 “前輩” 們的經驗和教訓,有很大的後發優勢。它引入了動態語言的便利,同時又會進行靜態語言的類型檢查。Go 不要求類型顯示的實現的某個接口,只要求實現了接口相關方法即可。

  讓我們看一個例子:

package main

import "fmt"

// Greeting 定義一個接口
type Greeting interface {
 sayHello()
}

// 使用接口作爲函數參數
func sayHello(g Greeting) {
 g.sayHello()
}

// Go 定義結構體,並且實現該接口
type Go struct{}

func (g Go) sayHello() {
 fmt.Println("hello,i am go")
}

type Java struct{}

func (j Java) sayHello() {
 fmt.Println("hello, i am java")
}

func main() {
 golang := Go{}
 java := Java{}
 sayHello(golang)
 sayHello(java)
}

  在 main 函數中,調用 sayHello 函數時,傳入結構體的實例化對象,它們並沒有顯示的實現接口。實際上,編譯器在調用 sayHello() 時,會隱式的將 golang,java 對象轉換爲 Greeting 類型,這其實也是靜態語言的類型檢查功能。

值接收者和指針接受者的區別

  方法能給用戶自定義的類型添加新的行爲。它和函數的區別在於方法有一個接收者,給一個函數添加一個接收者,那麼它就變成了方法。接收者可以是值接收者,也可以是指針接收者。

  在調用方法的時候,值類型既可以調用值接收者的方法,也可以調用指針接收者的方法;指針類型既可以調用指針接收者的方法,也可以調用值接收者的方法。

  總而言之,不管方法的接收者是什麼類型,該類型的值和指針都可以調用,不必嚴格符合接收者的類型。

  讓我們看一個例子:

package main

import "fmt"

type Person struct {
 age int
}

func (p Person) howOld() int {
 return p.age
}

func (p *Person) growUp() {
 p.age += 1
}

func main() {
 // tom 是值類型
 tom := Person{age: 18}

 // 值類型 調用接收者也是值類型的方法
 fmt.Println(tom.howOld())

 // 值類型 調用接收者是指針類型的方法
 tom.growUp()
 fmt.Println(tom.howOld())

 // ----------------------

 // steven是指針類型
 steven := &Person{age: 100}

 // 指針類型 調用接收者是值類型的方法
 fmt.Println(steven.howOld())

 // 指針類型 調用接收者也是指針類型的方法
 steven.growUp()
 fmt.Println(steven.howOld())
}

  實際上, 當類型和方法的接受者類型不同時, 其實是編譯器替我們 “負重前行” 了。

  上文說過, 無論接受者是值類型還是指針類型,都可以通過值類型和指針類型調用,這就是語法糖起了作用。

  結論就是,實現了接收者是值類型的方法,相當於自動實現了接收者是指針類型的方法。而實現了接收者是指針類型的方法,不會自動生成對應接收者是值類型的方法。
  讓我們來看一個例子:

package main

import "fmt"

type coder interface {
 code()
 debug()
}

type Gopher struct {
 language string
}

func (p Gopher) code() {
 fmt.Printf("I am coding %s language\n", p.language)
}

func (p *Gopher) debug() {
 fmt.Printf("I am debuging %s language\n", p.language)
}

func main() {
 var c coder = &Gopher{"Go"}
 c.code()
 c.debug()
}

  上述代碼運行結果如下

  從表面上看,*Gopher 類型並沒有實現 code 方法,但是因爲 Gopher 類型實現了 code 方法,所以讓 *Gopher 類型自動擁有了 code 方法。

  但是我們把 main 函數的第一條語句換一下

func main() {
 var c coder = Gopher{"Go"}
 c.code()
 c.debug()
}

  運行一下, 報如下錯誤

  看出這兩處代碼的差別了嗎?第一次是將 &Gopher 賦給了 coder;第二次則是將 Gopher 賦給了 coder。

  第二次報錯是說,Gopher 沒有實現 coder。很明顯了吧,因爲 Gopher 類型並沒有實現 debug 方法。

  其實這個地方隱藏着一個 "玄機", 這段錯誤的官方解釋爲:“接收者是指針類型的方法,很可能在方法中會對接收者的屬性進行更改操作,從而影響接收者;而對於接收者是值類型的方法,在方法中不會對接收者本身產生影響。”

  通俗點講就是:“當實現了一個接收者是值類型的方法,就可以自動生成一個接收者是對應指針類型的方法,因爲兩者都不會影響接收者。但是,當實現了一個接收者是指針類型的方法,如果此時自動生成一個接收者是值類型的方法,原本期望對接收者的改變(通過指針實現),現在無法實現,因爲值類型會產生一個拷貝,不會真正影響調用者。”

  其實關於這個解釋,我個人覺得太繁瑣, 讓我們仔細看一下 main 函數里的代碼, 我們可以發現, 結構是賦值給了 interface,所以我總結了一下:
  1: 類型 *T 賦值給 interface 的可調用方法集包含接受者爲 *T 或 T 的所有方法。
  2: 類型 T 賦值給 interface 的可調用方法集包含接受者爲 T 的所有方法。

  如果方法的接收者是值類型,無論調用者是對象還是對象指針,修改的都是對象的副本,不影響調用者;如果方法的接收者是指針類型,則調用者修改的是指針指向的對象本身。

總的來說如何選擇就是以下兩點:

  1. 設計不可變對象,用值接收。
  2. 其它用指針。
          Tips: 遇事不決用指針!!!

加餐:Golang 如何實現多態

  嚴格意義上講, Golang 並不是一門面向對象語言,它沒有其它語言所謂的面向對象三要素,但是 Golang 通過了 interface 優雅的實現了面向對象的特性。

  多態上一種運行期的行爲,有以下幾個特點。

  1. 一種類型具有多種類型的能力。
  2. 允許不同的對象對同一消息做出靈活的反應。
  3. 以一種通用的方式對待使用的對象。
  4. 非動態語言必須通過繼承和接口的方式來實現。

  讓我們來看一個例子:

package main

import "fmt"

type Person interface {
 job()
 growUp()
}

type Student struct {
 age int
}

type Programmer struct {
 age int
}

func (p Student) job() {
 fmt.Println("I am a student.")
 return
}

func (p *Student) growUp() {
 p.age += 1
 return
}

func (p Programmer) job() {
 fmt.Println("I am a programmer.")
 return
}

func (p Programmer) growUp() {
 // 程序員老得太快 ^_^
 p.age += 10
 return
}

func whatJob(p Person) {
 p.job()
}

func growUp(p Person) {
 p.growUp()
}

func main() {
 tom := Student{age: 18}
 whatJob(&tom)

 growUp(&tom)
 fmt.Println(tom)

 sam := Programmer{age: 100}
 whatJob(sam)

 growUp(sam)
 fmt.Println(sam)
}

  首先定義了一個接口 Person,該接口包含兩個方法 job() 和 growUp()。然後定義了兩個結構體,其都實現了了 person 接口。

  main 函數里,生成 Student 和 Programmer 的對象,再將它們分別傳入到函數 whatJob 和 growUp。函數中,直接調用接口函數,實際執行的時候是看最終傳入的實體類型是什麼,調用的是實體類型實現的函數。於是,不同對象針對同一消息就有多種表現,多態就實現了。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/DoxuZ_lrYJDY5slkQaKXTw