Golang 學習之路九:介面(Interface)
Golang學習之路:介面(interface)
一、前言
Go 語言和傳統的OO語言概念思想上不同,它在語法上不支援類與整合的概念。但是為了實現類似於C++等語言中多型行為,Go語言引入了interface
型別,實現了類似於多型的功能。注意interface
與method
關係密切,在學習interface
之前需要將method
的概念理解清楚。可以參考前面的博文Golang 學習之路七:面向物件-方法(Method),language specification和Effective Go
二、介面的定義
介面是一個或多個方法簽名的集合,任何型別的方法集中只要擁有與之對應的全部方法,它就表示“實現”了該介面,無需在該型別上顯式新增介面宣告。而所謂的對應方法,是指有相同的名稱、引數列表(不包括引數名)以及返回值。當然,該型別還可以有其它方法。
- 介面的命名通常使用
er
結尾,結構體。 - 介面只有方法簽名,沒有實現。
- 介面沒有資料欄位。
- 可以在介面中嵌入其它介面。
- 型別可以實現多個介面
package main
import "fmt"
type Stringer interface {
String() string
}
type Printer interface {
Stringer // 介面嵌入。
Print()
}
type User struct {
id int
name string
}
func (self *User) String() string {
return fmt.Sprintf("user %d, %s", self.id, self.name)
}
func (self *User) Print() {
fmt.Println(self.String())
}
func main() {
var t Printer = &User{1, "Tom"} // *User 方法集包含 String、Print。
t.Print()
}
輸出結果:
user 1, Tom
- 空interface(interface{})不包含任何的method,正因為如此,所有的型別都實現了空interface。空interface對於描述起不到任何的作用(因為它不包含任何的method),但是空interface在我們需要儲存任意型別的數值的時候相當有用,因為它可以儲存任意型別的數值。它有點類似於C語言的void*型別。
package main
import "fmt"
func Print(v interface{}) {
fmt.Printf("%T: %v\n", v, v)
}
func main() {
Print(1)
Print("Hello World")
}
輸出結果:
int: 1
string: Hello World
- 匿名介面可用作變數型別,或結構成員。
package main
import "fmt"
type Tester struct {
s interface {
String() string
}
}
type User struct {
id int
name string
}
func (self *User) String() string {
return fmt.Sprintf("user %d, %s", self.id, self.name)
}
func main() {
t := Tester{&User{1, "Tom"}}
fmt.Println(t.s.String())
}
輸出結果:
user 1, Tom
三、介面的執行機制
Go新版使用Go重寫,《Go學習筆記》使用了原來C/C++。下面貼出的是分別貼出兩個interface部分原始碼。
// @file /Go/src/runtime/runtime2.go (Go 1.8)
type iface struct {
tab *itab
data unsafe.Pointer
}
type itab struct {
inter *interfacetype
_type *_type
link *itab
bad int32
inhash int32 // has this itab been added to hash?
fun [1]uintptr // variable sized
}
// Go 舊版本原始碼:
struct Iface
{
Itab* tab;
void* data;
};
struct Itab
{
InterfaceType* inter;
Type* type;
void (*fun[])(void);
};
介面表儲存元資料資訊,包括介面型別、動態型別,以及實現介面的方法指標。
資料指標持有的是目標物件的只讀複製品,複製完整物件或指標。
package main
import "fmt"
type User struct {
id int
name string
}
func main() {
u := User{1, "Tom"}
var i interface{} = u
u.id = 2
u.name = "Jack"
fmt.Printf("%v\n", u)
fmt.Printf("%v\n", i.(User))
}
輸出結果:
{2 Jack}
{1 Tom}
- 介面轉型返回臨時物件,只有使用指標才能修改其狀態。
package main
import "fmt"
type User struct {
id int
name string
}
func main() {
u := User{1, "Tom"}
var i interface{} = &u
u.id = 2
u.name = "Jack"
fmt.Printf("%v\n", u)
fmt.Printf("%v\n", i.(*User))
}
輸出結果:
{2 Jack}
&{2 Jack}
- 只有
tab
和data
都為nil
時,接口才等於nil
。
package main // tab = nil, data = nil
import (
"fmt"
"reflect"
"unsafe"
)
var a interface{} = nil
var b interface{} = (*int)(nil) // tab 包含 *int 型別資訊, data = nil
type iface struct {
itab, data uintptr
}
func main() {
ia := *(*iface)(unsafe.Pointer(&a))
ib := *(*iface)(unsafe.Pointer(&b))
fmt.Println(a == nil, ia)
fmt.Println(b == nil, ib, reflect.ValueOf(b).IsNil())
}
輸出結果:
true {0 0}
false {4701888 0} true
四、介面變數儲存的型別
interface
變數裡面可以儲存任意型別的數值(非介面),只要這個值實現了介面的方法。但是,如何知道變數裡面實際儲存的是哪種型別的物件?
1. Comma-ok斷言
Go語言裡面有一個語法,可以直接判斷是否是該型別的變數: value, ok =element.(T),這裡value就是變數的值,ok是一個bool型別,element是interface變數,T是斷言的型別。
- 如果element裡面確實儲存了T型別的數值,那麼ok返回true,否則返回false。
package main
import "fmt"
type User struct {
id int
name string
}
func (self *User) String() string {
return fmt.Sprintf("%d, %s", self.id, self.name)
}
func main() {
var o interface{} = &User{1, "Tom"}
if value, ok := o.(fmt.Stringer); ok { // ok-idiom
fmt.Println(value, ok)
}
u := o.(*User)
// u := o.(User) // panic: interface is *main.User, not main.User
fmt.Println(u)
}
輸出結果:
1, Tom true
1, Tom
2. switch測試(不支援fallthrough)
- element.(type)語法不能在switch外的任何邏裡面使用,如果你要在switch外面判斷一個型別就使用comma-ok。
package main
import "fmt"
type User struct {
id int
name string
}
func (self *User) String() string {
return fmt.Sprintf("%d, %s", self.id, self.name)
}
func main() {
var element interface{} = &User{1, "Tom"}
switch v := element.(type) {
case nil: // element == nil
fmt.Println("nil")
case fmt.Stringer: // interface
fmt.Println(v)
case func() string: // func
fmt.Println(v())
case *User: // *struct
fmt.Printf("%d, %s\n", v.id, v.name)
default:
fmt.Println("unknown")
}
}
3. 補充
- 超集介面物件可轉換為子集介面,反之出錯。
package main
import "fmt"
type Stringer interface {
String() string
}
type Printer interface {
String() string
Print()
}
type User struct {
id int
name string
}
func (self *User) String() string {
return fmt.Sprintf("%d, %v", self.id, self.name)
}
func (self *User) Print() {
fmt.Println(self.String())
}
func main() {
var o Printer = &User{1, "Tom"}
var s Stringer = o
fmt.Println(s.String())
}
輸出結果:
1, Tom
五、嵌入介面
Go裡面真正吸引人的是他內建的邏輯語法,就像我們在學習Struct時學習的匿名欄位,非常的優雅,那麼相同的邏輯引入到interface裡面,那不是更加完美了。如果一個interface1作為interface2的一個嵌入欄位,那麼
interface2隱式的包含了interface1裡面的method。
- 檢視原始碼包中的一個例子
//@file /Go/src/container/heap/heap.go
type Interface interface {
sort.Interface
Push(x interface{}) // add x as element Len()
Pop() interface{} // remove and return element Len() - 1.
}
- 找到sort.Interface的原始碼如下:
//@file /Go/src/sort/sort.go
type Interface interface {
// Len is the number of elements in the collection.
Len() int
// Less reports whether the element with
// index i should sort before the element with index j.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
}
- heap.goInterface嵌入sort。Interface就把sort.Interface的所有method給隱式的包含進來了。
六、反射簡單介紹
反射是在執行時反射是程式檢查其所擁有的結構,尤其是型別的一種能力;這是超程式設計的一種形式。它同時也是造成混淆的重要來源。我們一般需要使用reflect包。
使用reflect一般分成三步
- 要去反射是一個型別的值(這些值都實現了空interface),首先需要把它轉化成reflect物件(reflect.Type或者reflect.Value,根據不同的情況呼叫不同的函式)。這兩種獲取方式如下:
t := reflect.TypeOf(i) //得到型別的元資料,通過t我們能獲取型別定義裡面的所有元素。
v := reflect.ValueOf(i) //得到實際的值,通過v我們獲取儲存在裡面的值,還可以去改變值
- 轉化為reflect物件之後我們就可以進行一些操作了,也就是將reflect物件轉化成相應的值,例如
tag := t.Elem().Field(0).Tag //獲取定義在struct裡面的標籤
name := v.Elem().Field(0).String() //獲取儲存在第一個欄位裡面的值
獲取反射值能返回相應的型別和數值
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())
- 最後,反射的話,那麼反射的欄位必須是可修改的,我們前面學習過傳值和傳引用,這個裡面也是一樣的道理,反射的欄位必須是可讀寫的意思是,如果下面這樣寫,那麼會發生錯誤。
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)
如果要修改相應的值,需要進行下面的修改
var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)
上面只是摘錄《Go Web程式設計》對反射的簡單介紹,更深入的理解可以檢視laws of reflection,可以檢視轉載的中文翻譯版本[翻譯]反射的規則或原始翻譯版本。
七、總結
Go語言的interface是它比較具有特色的地方。Go語言的主要設計者之一羅布·派克( Rob Pike)曾經說過,如果只能選擇一個Go語言的特 性移植到其他語言中,他會選擇介面。它在Go開發中無處不在,從Go原始碼中,你可以看到這些。