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()
}
上述代碼運行結果如下
但是我們把 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 的所有方法。
-
值接收者和指針接收者如何選擇:
如果方法的接收者是值類型,無論調用者是對象還是對象指針,修改的都是對象的副本,不影響調用者;如果方法的接收者是指針類型,則調用者修改的是指針指向的對象本身。
總的來說如何選擇就是以下兩點:
- 設計不可變對象,用值接收。
- 其它用指針。
Tips: 遇事不決用指針!!!
加餐:Golang 如何實現多態
嚴格意義上講, Golang 並不是一門面向對象語言,它沒有其它語言所謂的面向對象三要素,但是 Golang 通過了 interface 優雅的實現了面向對象的特性。
多態上一種運行期的行爲,有以下幾個特點。
- 一種類型具有多種類型的能力。
- 允許不同的對象對同一消息做出靈活的反應。
- 以一種通用的方式對待使用的對象。
- 非動態語言必須通過繼承和接口的方式來實現。
讓我們來看一個例子:
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