這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
interface
Go語言裡面設計最精妙的應該算interface,它讓物件導向,內容組織實現非常的方便,當你看完這一章,你就會被interface的巧妙設計所折服。
什麼是interface
簡單的說,interface是一組method的組合,我們通過interface來定義對象的一組行為。
我們前面一章最後一個例子中Student和Employee都能SayHi,雖然他們的內部實現不一樣,但是那不重要,重要的是他們都能say hi
讓我們來繼續做更多的擴充,Student和Employee實現另一個方法Sing,然後Student實現方法BorrowMoney而Employee實現SpendSalary。
這樣Student實現了三個方法:SayHi、Sing、BorrowMoney;而Employee實現了SayHi、Sing、SpendSalary。
上面這些方法的組合稱為interface(被對象Student和Employee實現)。例如Student和Employee都實現了interface:SayHi和Sing,也就是這兩個對象是該interface類型。而Employee沒有實現這個interface:SayHi、Sing和BorrowMoney,因為Employee沒有實現BorrowMoney這個方法。
interface類型
interface類型定義了一組方法,如果某個對象實現了某個介面的所有方法,則此對象就實現了此介面。詳細的文法參考下面這個例子
type Human struct { name string age int phone string}type Student struct { Human //匿名欄位Human school string loan float32}type Employee struct { Human //匿名欄位Human company string money float32}//Human對象實現Sayhi方法func (h *Human) SayHi() { fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)}// Human對象實現Sing方法func (h *Human) Sing(lyrics string) { fmt.Println("La la, la la la, la la la la la...", lyrics)}//Human對象實現Guzzle方法func (h *Human) Guzzle(beerStein string) { fmt.Println("Guzzle Guzzle Guzzle...", beerStein)}// Employee重載Human的Sayhi方法func (e *Employee) SayHi() { fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name, e.company, e.phone) //此句可以分成多行}//Student實現BorrowMoney方法func (s *Student) BorrowMoney(amount float32) { s.loan += amount // (again and again and...)}//Employee實現SpendSalary方法func (e *Employee) SpendSalary(amount float32) { e.money -= amount // More vodka please!!! Get me through the day!}// 定義interfacetype Men interface { SayHi() Sing(lyrics string) Guzzle(beerStein string)}type YoungChap interface { SayHi() Sing(song string) BorrowMoney(amount float32)}type ElderlyGent interface { SayHi() Sing(song string) SpendSalary(amount float32)}
通過上面的代碼我們可以知道,interface可以被任意的對象實現。我們看到上面的Men interface被Human、Student和Employee實現。同理,一個對象可以實現任意多個interface,例如上面的Student實現了Men和YoungChap兩個interface。
最後,任意的類型都實現了空interface(我們這樣定義:interface{}),也就是包含0個method的interface。
interface值
那麼interface裡面到底能存什麼值呢?如果我們定義了一個interface的變數,那麼這個變數裡面可以存實現這個interface的任意類型的對象。例如上面例子中,我們定義了一個Men interface類型的變數m,那麼m裡面可以存Human、Student或者Employee值。
因為m能夠持有這三種類型的對象,所以我們可以定義一個包含Men類型元素的slice,這個slice可以被賦予實現了Men介面的任意結構的對象,這個和我們傳統意義上面的slice有所不同。
讓我們來看一下下面這個例子:
package mainimport "fmt"type Human struct { name string age int phone string}type Student struct { Human //匿名欄位 school string loan float32}type Employee struct { Human //匿名欄位 company string money float32}//Human實現SayHi方法func (h Human) SayHi() { fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)}//Human實現Sing方法func (h Human) Sing(lyrics string) { fmt.Println("La la la la...", lyrics)}//Employee重載Human的SayHi方法func (e Employee) SayHi() { fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name, e.company, e.phone) }// Interface Men被Human,Student和Employee實現// 因為這三個類型都實現了這兩個方法type Men interface { SayHi() Sing(lyrics string)}func main() { mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00} paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100} sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000} Tom := Employee{Human{"Tom", 37, "222-444-XXX"}, "Things Ltd.", 5000} //定義Men類型的變數i var i Men //i能儲存Student i = mike fmt.Println("This is Mike, a Student:") i.SayHi() i.Sing("November rain") //i也能儲存Employee i = Tom fmt.Println("This is Tom, an Employee:") i.SayHi() i.Sing("Born to be wild") //定義了slice Men fmt.Println("Let's use a slice of Men and see what happens") x := make([]Men, 3) //這三個都是不同類型的元素,但是他們實現了interface同一個介面 x[0], x[1], x[2] = paul, sam, mike for _, value := range x{ value.SayHi() }}
通過上面的代碼,你會發現interface就是一組抽象方法的集合,它必須由其他非interface類型實現,而不能自我實現, Go通過interface實現了duck-typing:即"當看到一隻鳥走起來像鴨子、遊泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子"。
空interface
空interface(interface{})不包含任何的method,正因為如此,所有的類型都實現了空interface。空interface對於描述起不到任何的作用(因為它不包含任何的method),但是空interface在我們需要儲存任意類型的數值的時候相當有用,因為它可以儲存任意類型的數值。它有點類似於C語言的void*類型。
// 定義a為空白介面var a interface{}var i int = 5s := "Hello world"// a可以儲存任意類型的數值a = ia = s
一個函數把interface{}作為參數,那麼他可以接受任意類型的值作為參數,如果一個函數返回interface{},那麼也就可以返回任意類型的值。是不是很有用啊!
interface函數參數
interface的變數可以持有任意實現該interface類型的對象,這給我們編寫函數(包括method)提供了一些額外的思考,我們是不是可以通過定義interface參數,讓函數接受各種類型的參數。
舉個例子:fmt.Println是我們常用的一個函數,但是你是否注意到它可以接受任意類型的資料。開啟fmt的源碼檔案,你會看到這樣一個定義:
type Stringer interface { String() string}
也就是說,任何實現了String方法的類型都能作為參數被fmt.Println調用,讓我們來試一試
package mainimport ( "fmt" "strconv")type Human struct { name string age int phone string}// 通過這個方法 Human 實現了 fmt.Stringerfunc (h Human) String() string { return "❰"+h.name+" - "+strconv.Itoa(h.age)+" years - ✆ " +h.phone+"❱"}func main() { Bob := Human{"Bob", 39, "000-7777-XXX"} fmt.Println("This Human is : ", Bob)}
現在我們再回顧一下前面的Box樣本,你會發現Color結構也定義了一個method:String。其實這也是實現了fmt.Stringer這個interface,即如果需要某個類型能被fmt包以特殊的格式輸出,你就必須實現Stringer這個介面。如果沒有實現這個介面,fmt將以預設的方式輸出。
//實現同樣的功能fmt.Println("The biggest one is", boxes.BiggestsColor().String())fmt.Println("The biggest one is", boxes.BiggestsColor())
註:實現了error介面的對象(即實現了Error() string的對象),使用fmt輸出時,會調用Error()方法,因此不必再定義String()方法了。
interface變數儲存的類型
我們知道interface的變數裡面可以儲存任意類型的數值(該類型實現了interface)。那麼我們怎麼反向知道這個變數裡面實際儲存了的是哪個類型的對象呢?目前常用的有兩種方法:
Comma-ok斷言
Go語言裡面有一個文法,可以直接判斷是否是該類型的變數: value, ok = element.(T),這裡value就是變數的值,ok是一個bool類型,element是interface變數,T是斷言的類型。
如果element裡面確實儲存了T類型的數值,那麼ok返回true,否則返回false。
讓我們通過一個例子來更加深入的理解。
package mainimport ( "fmt" "strconv")type Element interface{}type List [] Elementtype Person struct { name string age int}//定義了String方法,實現了fmt.Stringerfunc (p Person) String() string { return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)"}func main() { list := make(List, 3) list[0] = 1 // an int list[1] = "Hello" // a string list[2] = Person{"Dennis", 70} for index, element := range list { if value, ok := element.(int); ok { fmt.Printf("list[%d] is an int and its value is %d\n", index, value) } else if value, ok := element.(string); ok { fmt.Printf("list[%d] is a string and its value is %s\n", index, value) } else if value, ok := element.(Person); ok { fmt.Printf("list[%d] is a Person and its value is %s\n", index, value) } else { fmt.Println("list[%d] is of a different type", index) } }}
是不是很簡單啊,同時你是否注意到了多個if裡面,還記得我前面介紹流程時講過,if裡面允許初始設定變數。
也許你注意到了,我們斷言的類型越多,那麼if else也就越多,所以才引出了下面要介紹的switch。
switch測試
最好的講解就是代碼例子,現在讓我們重寫上面的這個實現
package mainimport ( "fmt" "strconv")type Element interface{}type List [] Elementtype Person struct { name string age int}//列印func (p Person) String() string { return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)"}func main() { list := make(List, 3) list[0] = 1 //an int list[1] = "Hello" //a string list[2] = Person{"Dennis", 70} for index, element := range list{ switch value := element.(type) { case int: fmt.Printf("list[%d] is an int and its value is %d\n", index, value) case string: fmt.Printf("list[%d] is a string and its value is %s\n", index, value) case Person: fmt.Printf("list[%d] is a Person and its value is %s\n", index, value) default: fmt.Println("list[%d] is of a different type", index) } }}
這裡有一點需要強調的是:element.(type)文法不能在switch外的任何邏輯裡面使用,如果你要在switch外面判斷一個類型就使用comma-ok。
嵌入interface
Go裡面真正迷人的是它內建的邏輯文法,就像我們在學習Struct時學習的匿名欄位,多麼的優雅啊,那麼相同的邏輯引入到interface裡面,那不是更加完美了。如果一個interface1作為interface2的一個嵌入欄位,那麼interface2隱式的包含了interface1裡面的method。
我們可以看到源碼包container/heap裡面有這樣的一個定義
type Interface interface { sort.Interface //嵌入欄位sort.Interface Push(x interface{}) //a Push method to push elements into the heap Pop() interface{} //a Pop elements that pops elements from the heap}
我們看到sort.Interface其實就是嵌入欄位,把sort.Interface的所有method給隱式的包含進來了。也就是下面三個方法:
type Interface interface { // Len is the number of elements in the collection. Len() int // Less returns 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)}
另一個例子就是io包下面的 io.ReadWriter ,它包含了io包下面的Reader和Writer兩個interface:
// io.ReadWritertype ReadWriter interface { Reader Writer}
反射
Go語言實現了反射,所謂反射就是能檢查程式在運行時的狀態。我們一般用到的包是reflect包。如何運用reflect包,官方的這篇文章詳細的講解了reflect包的實現原理,laws of reflection
使用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.4v := 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.4v := reflect.ValueOf(x)v.SetFloat(7.1)
如果要修改相應的值,必須這樣寫
var x float64 = 3.4p := reflect.ValueOf(&x)v := p.Elem()v.SetFloat(7.1)
valueOf 獲得的是複製,對於它的修改對原來的資料是沒有影響的。如果想要修改原值,就要用指標。再用p.Elem就能獲得可以修改的對象了。
要處理struct的話,
也要像這樣
type T struct {A intB string}func main() {a := 1fmt.Println(reflect.TypeOf(a))fmt.Println(reflect.ValueOf(a))t := T{23, "skidoo"}fmt.Println(reflect.TypeOf(t))fmt.Println(reflect.ValueOf(t))s := reflect.ValueOf(&t).Elem() //ValueOf.Elem必須要指標類型,否則會panictypeOfT := s.Type()for i := 0; i < s.NumField(); i++ {f := s.Field(i)fmt.Printf("%d: %s %s = %v\n", i,typeOfT.Field(i).Name, f.Type(), f.Interface())}}
輸出如下:
int
1
main.T
{23 skidoo}
0: A int = 23
1: B string = skidoo
通過Elem的獲得可修改對象,然後再Filed擷取對應的value。