Go 語言介面詳解(一)

Seekload發表於2019-03-28

這是『就要學習 Go 語言』系列的第 19 篇分享文章

什麼是介面

在一些物件導向的程式語言中,例如 Java、PHP 等,介面定義了物件的行為,只指定了物件應該做什麼。行為的具體實現取決於物件。

在 Go 語言中,介面是一組方法的集合,但不包含方法的實現、是抽象的,介面中也不能包含變數。當一個型別 T 提供了介面中所有方法的定義時,就說 T 實現了介面。介面指定型別應該有哪些方法,型別決定如何去實現這些方法。

介面宣告

介面的宣告類似於結構體,使用型別別名且需要關鍵字 interface,語法如下:

type Name interface {
    Method1(param_list) return_type
    Method2(param_list) return_type
    ...
}
複製程式碼

實際定義一個介面:

type Shape interface {
    Area() float32
}
複製程式碼

上面的程式碼定義了介面型別 Shape,介面中包含了一個不帶引數、返回值為 float32 的方法 Area()。任何實現了方法 Area() 的型別 T,我們就說它實現了介面 Shape。

type Shape interface {
	Area() float32
}

func main() {
	var s Shape
	fmt.Println("value of s is", s)
	fmt.Printf("type of s is %T\n", s)
}
複製程式碼

輸出

value of s is <nil>
type of s is <nil>
複製程式碼

上面的程式碼,由於介面是一種型別,所以可以建立 Shape 型別的變數 s,你是不是很疑惑 s 的型別為什麼是 nil?讓我們來看下一節!

介面型別值

靜態型別和動態型別

變數的型別在宣告時指定、且不能改變,稱為靜態型別。介面型別的靜態型別就是介面本身。介面沒有靜態值,它指向的是動態值。介面型別的變數存的是實現介面的型別的值。該值就是介面的動態值,實現介面的型別就是介面的動態型別。

type Iname interface {
	Mname()
}

type St1 struct {}
func (St1) Mname() {}
type St2 struct {}
func (St2) Mname() {}

func main() {
	var i Iname = St1{}
	fmt.Printf("type is %T\n",i)
	fmt.Printf("value is %v\n",i)
	i = St2{}
	fmt.Printf("type is %T\n",i)
	fmt.Printf("value is %v\n",i)
}
複製程式碼

輸出

type is main.St1
value is {}
type is main.St2
value is {}
複製程式碼

變數 i 的靜態型別是 Iname,是不能改變的。動態型別卻是不固定的,第一次分配之後,i 的動態型別是 St1,第二次分配之後,i 的動態型別是 St2,動態值都是空結構體。

有時候,介面的動態型別又稱為具體型別,當我們訪問介面型別的時候,返回的是底層動態值的型別。

nil 介面值

我們來看個例子:

type Iname interface {
	Mname()
}
type St struct {}
func (St) Mname() {}
func main() {
	var t *St
	if t == nil {
		fmt.Println("t is nil")
	} else {
		fmt.Println("t is not nil")
	}
	var i Iname = t
	fmt.Printf("%T\n", i)
	if i == nil {
		fmt.Println("i is nil")
	} else {
		fmt.Println("i is not nil")
	}
	fmt.Printf("i is nil pointer:%v",i == (*St)(nil))
}
複製程式碼

輸出

t is nil
*main.St
i is not nil
i is nil pointer:true
複製程式碼

是不是很驚訝,我們分配給變數 i 的值明明是 nil,然而 i 卻不是 nil。 來看下怎麼回事!

動態型別在上面已經講過,動態值是實際分配的值。記住一點:當且僅當動態值和動態型別都為 nil 時,介面型別值才為 nil。上面的程式碼,給變數 i 賦值之後,i 的動態值是 nil,但是動態型別卻是 *St, i 是一個 nill 指標,所以想等條件不成立。

看下 Go 語言規範:

 var x interface{}  // x is nil and has static type interface{}
 var v *T           // v has value nil, static type *T
 x = 42             // x has value 42 and dynamic type int
 x = v              // x has value (*T)(nil) and dynamic type *T
複製程式碼

通過這一節學習,相信你已經很清楚為什麼上一節的 Shape 型別的變數的 s 輸出的型別是 nil,因為 var s Shape 宣告時,s 的動態型別是 nil。

實現介面

看示例:

type Shape interface {
	Area() float32
}

type Rect struct {
	width  float32
	height float32
}

func (r Rect) Area() float32 {
	return r.width * r.height
}

func main() {
	var s Shape
	s = Rect{5.0, 4.0}
	r := Rect{5.0, 4.0}
	fmt.Printf("type of s is %T\n", s)
	fmt.Printf("value of s is %v\n", s)
	fmt.Println("area of rectange s", s.Area())
	fmt.Println("s == r is", s == r)
}
複製程式碼

輸出

type of s is main.Rect
value of s is {5 4}
area of rectange s 20
s == r is true
複製程式碼

上面的程式碼,建立了介面 Shape、結構體 Rect 以及方法 Area()。由於 Rect 實現了介面定義的所有方法,雖然只有一個,所以說 Rect 實現了介面 Shape。

在主函式裡,建立了介面型別的變數 s ,值為 nil,並用 Rect 型別的結構體初始化,因為 Rect 結構體實現了介面,所以這是有效的。賦值之後,s 的動態型別變成了 Rect,動態值就是結構體的值 {5.0,4.0}。

可以直接使用 . 語法呼叫 Area() 方法,因為 s 的具體型別是 Rect,而 Rect 實現了 Area() 方法。

空介面

一個不包含任何方法的介面,稱之為空介面,形如:interface{}。因為空介面不包含任何方法,所以任何型別都預設實現了空介面。

舉個例子,fmt 包中的 Println() 函式,可以接收多種型別的值,比如:int、string、array等。為什麼,因為它的形參就是介面型別,可以接收任意型別的值。

func Println(a ...interface{}) (n int, err error) {}
複製程式碼

我們來看個例子:

type MyString string
type Rect struct {
	width  float32
	height float32
}
func explain(i interface{}) {
	fmt.Printf("type of s is %T\n", i)
	fmt.Printf("value of s is %v\n\n", i)
}
func main() {
	ms := MyString("Seekload")
	r := Rect{5.0, 4.0}
	explain(ms)
	explain(r)
}
複製程式碼

輸出

type of s is main.MyString
value of s is Seekload

type of s is main.Rect
value of s is {5 4}
複製程式碼

上面的程式碼,建立了自定義的字串型別 MyString 、結構體 Rect 和 explain() 函式。explain() 函式的形參是空介面,所以可以接收任意型別的值。

關於介面使用的第一部分就講到這,後續會再開文章給大家講剩餘部分,繼續關注!


(全文完)

原創文章,若需轉載請註明出處!
歡迎掃碼關注公眾號「Golang來啦」或者移步 seekload.net ,檢視更多精彩文章。

公眾號「Golang來啦」給你準備了一份神祕學習大禮包,後臺回覆【電子書】領取!

公眾號二維碼

相關文章