介面
概述
如果說goroutine和channel是Go併發的兩大基石,那麼介面是Go語言程式設計中資料型別的關鍵。在Go語言的實際程式設計中,幾乎所有的資料結構都圍繞介面展開,介面是Go語言中所有資料結構的核心。
Go語言中的介面是一些方法的集合(method set),它指定了物件的行為:如果它(任何資料型別)可以做這些事情,那麼它就可以在這裡使用。
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}
上面的程式碼定義了4個介面。
假設我們在另一個地方中定義了下面這個結構體:
type File struct { // ...
}
func (f *File) Read(buf []byte) (n int, err error)
func (f *File) Write(buf []byte) (n int, err error)
func (f *File) Seek(off int64, whence int) (pos int64, err error) func (f *File) Close() error
我們在實現File的時候,可能並不知道上面4個介面的存在,但不管怎樣,File實現了上面所有的4個介面。我們可以將File物件賦值給上面任何一個介面。
var file1 Reader = new(File)
var file2 Writer = new(File)
var file3 Closer = new(File)
var file4 Seeker = new(File)
因為File可以做這些事情,所以,File就可以在這裡使用。File在實現的時候,並不需要指定實現了哪個介面,它甚至根本不知道這4個介面的存在。這種“鬆耦合”的做法完全不同於傳統的物件導向程式設計。
實際上,上面4個介面來自標準庫中的io package。
介面賦值
我們可以將一個實現介面的物件例項賦值給介面,也可以將另外一個介面賦值給介面。
(1)通過物件例項賦值
將一個物件例項賦值給一個介面之前,要保證該物件實現了介面的所有方法。考慮如下示例:
type Integer int
func (a Integer) Less(b Integer) bool {
return a < b
}
func (a *Integer) Add(b Integer) {
*a += b
}
type LessAdder interface {
Less(b Integer) bool
Add(b Integer)
}
var a Integer = 1
var b1 LessAdder = &a //OK
var b2 LessAdder = a //not OK
b2的賦值會報編譯錯誤,為什麼呢?還記得一章中討論的Go語言規範的規定嗎?
The method set of any other named type T consists of all methods with receiver type T. The method set of the corresponding pointer type T is the set of all methods with receiver T or T (that is, it also contains the method set of T).
也就是說*Integer實現了介面LessAdder的所有方法,而Integer只實現了Less方法,所以不能賦值。
(2)通過介面賦值
var r io.Reader = new(os.File)
var rw io.ReadWriter = r //not ok
var rw2 io.ReadWriter = new(os.File)
var r2 io.Reader = rw2 //ok
因為r沒有Write方法,所以不能賦值給rw。
介面巢狀
我們來看看io package中的另外一個介面:
// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
Reader
Writer
}
該介面巢狀了io.Reader和io.Writer兩個介面,實際上,它等同於下面的寫法:
type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
注意,Go語言中的介面不能遞迴巢狀,
// illegal: Bad cannot embed itself
type Bad interface {
Bad
}
// illegal: Bad1 cannot embed itself using Bad2
type Bad1 interface {
Bad2
}
type Bad2 interface {
Bad1
}
空介面(empty interface)
空介面比較特殊,它不包含任何方法:
interface{}
在Go語言中,所有其它資料型別都實現了空介面。
var v1 interface{} = 1
var v2 interface{} = "abc"
var v3 interface{} = struct{ X int }{1}
如果函式打算接收任何資料型別,則可以將參考宣告為interface{}。最典型的例子就是標準庫fmt包中的Print和Fprint系列的函式:
func Fprint(w io.Writer, a ...interface{}) (n int, err error)
func Fprintf(w io.Writer, format string, a ...interface{})
func Fprintln(w io.Writer, a ...interface{})
func Print(a ...interface{}) (n int, err error)
func Printf(format string, a ...interface{})
func Println(a ...interface{}) (n int, err error)
注意,[]T不能直接賦值給[]interface{}
t := []int{1, 2, 3, 4}
var s []interface{} = t
編譯時會輸出下面的錯誤:
cannot use t (type []int) as type []interface {} in assignment
我們必須通過下面這種方式:
t := []int{1, 2, 3, 4}
s := make([]interface{}, len(t))
for i, v := range t {
s[i] = v
}
型別轉換(Conversions)
型別轉換的語法:
Conversion = Type "(" Expression [ "," ] ")" .
當以運算子*或者<-開始,有必要加上括號避免模糊:
*Point(p) // same as *(Point(p))
(*Point)(p) // p is converted to *Point
<-chan int(c) // same as <-(chan int(c))
(<-chan int)(c) // c is converted to <-chan int
func()(x) // function signature func() x
(func())(x) // x is converted to func()
(func() int)(x) // x is converted to func() int
func() int(x) // x is converted to func() int (unambiguous)
Type switch與Type assertions
在Go語言中,我們可以使用type switch語句查詢介面變數的真實資料型別,語法如下:
switch x.(type) {
// cases
}
x必須是介面型別。
來看一個詳細的示例:
type Stringer interface {
String() string
}
var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
return str //type of str is string
case Stringer: //type of str is Stringer
return str.String()
}
語句switch中的value必須是介面型別,變數str的型別為轉換後的型別。
If the switch declares a variable in the expression, the variable will have the corresponding type in each clause. It's also idiomatic to reuse the name in such cases, in effect declaring a new variable with the same name but a different type in each case.
如果我們只關心一種型別該如何做?如果我們知道值為一個string,只是想將它抽取出來該如何做?只有一個case的型別switch是可以的,不過也可以用型別斷言(type assertions)。型別斷言接受一個介面值,從中抽取出顯式指定型別的值。其語法借鑑了型別switch子句,不過是使用了顯式的型別,而不是type關鍵字,如下:
x.(T)
同樣,x必須是介面型別。
str := value.(string)
上面的轉換有一個問題,如果該值不包含一個字串,則程式會產生一個執行時錯誤。為了避免這個問題,可以使用“comma, ok”的習慣用法來安全地測試值是否為一個字串:
str, ok := value.(string)
if ok {
fmt.Printf("string value is: %q\n", str)
} else {
fmt.Printf("value is not a string\n")
}
如果型別斷言失敗,則str將依然存在,並且型別為字串,不過其為零值,即一個空字串。
我們可以使用型別斷言來實現type switch的中例子:
if str, ok := value.(string); ok {
return str
} else if str, ok := value.(Stringer); ok {
return str.String()
}
這種做法沒有多大實用價值。