深入學習golang(5)—接口


接口

概述

如果說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()
}

這種做法沒有多大實用價值。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM