這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
用了9年的C++,1年的C#,最近改用go語言開發,深感go語言的設計簡單,其設計宗旨也是less is more,它極大的加快了開發速度。
go語言吸取了很多現代語言的優點,一個比較重要的特性就是基於介面編程,函數是程式世界的第一公民,這個有點像scalar語言。實現這個介面的語言原型是interface。
基於介面編程
C++中不支援介面,介面的實現方式是用純虛函數來實現的,C#具有介面,但是它認為介面是對象的一個能力,這是一個很大的進步,但是它不大靈活,比如
public Interface IFile{ public int Read(string filePath, int len) public int Write(string filePath, int len)}這個介面實現從一個檔案中讀取len個位元組資料,返回實際的資料,以及寫入len個位元組資料,返回實際寫入的資料個數public class CDataNode:IFile{ public int Read(string filePath, int len){...} public int Write(string filePath, int len){...}}這樣CDataNode就可以在支援IFile介面的地方直接使用
這樣的代碼寫了很久,但是如果發現IFile這個粒度太大了,有些地方只需要Read,有些地方需要Write,在C#中只能是
public Interface IReadpublic Interface IWritepublic Interface IFile:IRead,IWrite
在go中不需要這樣,只需要再聲明IRead和IWrite介面,IFile可以直接轉換到IRead和IWrite,它更加靈活。
package mainimport ( "fmt")type iFile interface { Read(int) int Write(int) int}type rwFile struct {}func (rw rwFile) Read(int) int { fmt.Println("rwFile read") return 0}func (rw rwFile) Write(int) int { fmt.Println("rwFile write") return 0}type iRead interface { Read(int) int}type iWrite interface { Write(int) int}func TestInterface() { var rwInterface iFile = rwFile{} rwInterface.Read(0) rwInterface.Write(0) var rInterface iRead = rwInterface rInterface.Read(0) var wInterface iWrite = rwInterface wInterface.Write(0)}
模組之間依賴介面,並且go是不支援繼承,只支援組合,繼承是一種強關係,一旦定下來就很難改動,或者說改動成本非常之高,組合是一種弱關係,更加鬆散一點。
繼承和多態特性
繼承和多態是物件導向設計一個非常好的特性,它可以更好的抽象架構,讓模組之間依賴於介面,而不是依賴於具體實現。
package mainimport ( "fmt")type iAnimal interface { speak() string}type dog struct {}func (d dog) speak() string { return "Woof!"}type cat struct {}func (c cat) speak() string { return "Meow!"}type coder struct {}func (c coder) speak() string { return "Hello world!"}type superCoder struct { coder//如果沒有給出變數名字,編譯器產生一個coder coder的變數,然後它的文法糖是針對這種變數的訪問,直接可以訪問其內部成員變數,而不需要經過.coder來訪問。}func (s superCoder) sayHi() { fmt.Println("super coder say hi")}func Polymorphic() { animals := []iAnimal{dog{}, cat{}, coder{}, superCoder{}} for _, animal := range animals { fmt.Println(animal.speak()) }}
依賴於介面來實現,只要實現了這個介面就可以認為賦值給這個口,實現動態綁定。superCoder在這裡可以認為是繼承與coder,但是它增加了新的方法SayHi,原有的speak方法沒有變化,所以它也不需要重寫。
go中是沒有繼承的,匿名成員變數也只是一個文法糖而已,並不是繼承
type cat struct{ age int}type dog struct{ cat}var c cat = dog{}//編譯錯誤var pc *cat = &dog{}//編譯錯誤var pc *cat = (*cat)(&dog{})//編譯錯誤
在C++中是可以用基類指標指向子類的對象的,但是go中它認為是兩種類型,不能進行轉換。
從根本上講,struct在go中是沒有虛表指標,這種特性只有interface才有,所以它是兩種獨立的類型,不能相互轉換
interface的使用
interface特性是:
* 是一組函數簽名的集合
* 是一種類型
interface{}
interface{}是一類特殊的介面,因為它沒有實現任何類型,所以所有類型變數都可以賦值給它,有點像C#的object,它是所有對象的基類。
package mainimport ( "fmt")func PrintAll(vals []interface{}) { for _, val := range vals { fmt.Println(val) }}func main() { names := []string{"stanley", "david", "oscar"} PrintAll(names)}
最開始我很奇怪為什麼不能編譯通過,後來仔細想想就是應該編譯不過,這裡PrintAll接收一個“[]interface{}”類型,輸入的參數是一個”[]string”類型,兩個是不同的類型,不能進行賦值。所以正確的做法是
package mainimport ( "fmt")func PrintAll(vals []interface{}) { for _, val := range vals { fmt.Println(val) }}func main() { names := []string{"stanley", "david", "oscar"} //PrintAll(names) vals := make([]interface{}, len(names)) for i, v := range names { vals[i] = v } PrintAll(vals)}
因為[]interface{}也是一種類型,所以它是可以賦值給interface{}的.
interface{}用法
有一個需求,將一個json解析稱為一個map,因為資料格式多變,採用interface{},調用庫函數UnmarshalJSON。
package mainimport ( "encoding/json" "fmt" "reflect")// start with a string representation of our JSON datavar input = `{ "created_at": "Thu May 31 00:00:01 +0000 2012"}`func main() { // our target will be of type map[string]interface{}, which is a // pretty generic type that will give us a hashtable whose keys // are strings, and whose values are of type interface{} var val map[string]interface{} if err := json.Unmarshal([]byte(input), &val); err != nil { panic(err) } fmt.Println(val) for k, v := range val { fmt.Println(k, reflect.TypeOf(v)) }}返回結果map[created_at:Thu May 31 00:00:01 +0000 2012]created_at string
注意到這裡的時間是一種特殊格式的time,和普通的不一樣,這樣返回的結果是string類型而不是time類型.及時將map的資料改成time也會有問題,因為它不是一種標準的time格式,會拋出異常。
可以考慮實現json的介面
type Unmarshaler interface { UnmarshalJSON([]byte) error}func (t *Timestamp) UnmarshalJSON(b []byte) error { v, err := time.Parse(time.RubyDate, string(b[1:len(b)-1])) if err != nil { return err } *t = Timestamp(v) return nil}
上面的map就能夠正常,返回的類型也是time類型。
## interface{}一般可以作為函數的入口參數,但是最好不要作為一個函數的傳回值。
有一個需求,要設計一個API,將http request的請求轉化為某中資料類型格式,格式有可能有很多種。
因為返回的資料格式沒有定,採用interface{}格式最好。
GetEntity(*http.Request) (interface{}, error)caller:switch v:Get(r).type(){ int: ... string: ...}
這樣做非常不好,因為每次增加一個新類型,調用方就需要改變,不滿足開閉原則,正確的做法是
type Entity interface { UnmarshalHTTP(*http.Request) error}func GetEntity(r *http.Request, v Entity) error { return v.UnmarshalHTTP(r)}caller:var u Userif err := GetEntity(req, &u); err != nil { // ...}其中User需要實現這個介面func (u *User) UnmarshalHTTP(r *http.Request) error { // ...}
將interface{}作為參數,而不是傳回值,這樣做的好處增加新的類型只需要實現Entity介面就可以了,再增加一個對這種類型的調用,而不需要修改GetEntity的內部實現。
interface的底層實現
我們先看一個例子,從這個例子來看怎麼實現的
type Stringer interface { String() string}type Binary uint64func (i Binary) String() string { return strconv.Uitob64(i.Get(), 2)}func (i Binary) Get() uint64 { return uint64(i)}
建立一個uint64,初始值為200
b := Binary(200)
其記憶體模型如下:
此時將b賦值給一個介面:
s := Stringer(b)//如果不能轉換會拋出exception
interface s由兩個成員,
* 第一個是itable,有點類似C++的vtable,儲存了一系列的函數對象,第一個是type,通過它可以找到它所屬於哪個類型,這個是所有interface的itable第一個值,其它的就是這個interface類型需要的函數對應的實際實現,我們可以看到String()的Binary實現。Binary中實現了Get函數,因為它不是Stringer的介面,所以就沒有放在這裡。
itable是如何產生?它是在運行時動態產生的,首先每種類型,編譯器都會為其產生meta,這個meta裡包含了所有它支援的函數以及成員變數,當一個對象要賦值給interface時,runtime會遍曆整個對象的函數簽名找到匹配這個interface的函數簽名,如果全部能夠匹配則繼續執行,否則就會拋出異常(如果不檢查成功失敗的話),同理將一個interface賦值給另外一個interface.
假設變數的類型有m個函數,interface有n個需要支援的簽名,則採用演算法每個都匹配一遍,複雜度為O(mn),go中它會將每個函數簽名集合進行簽名,這樣做匹配的時候覆雜度只為O(m+n).
* 另外一個是實際的資料data,這裡它會建立一個新的值,所以b的修改是不會影響介面的
func (i *Binary) String() string { return strconv.Uitob64(i.Get(), 2)}ps := Stringer(&b)
這種情況ps中的data部分儲存的就是指向b的指標,因為在做轉換的時候傳進來的也是指標。
總結
- go語言所有的賦值都是值拷貝
- interface是一種函數集合的類型
- interface的itable中儲存了這個interface的具體實現,idata中儲存了賦值資料的拷貝
- interface作為參數的入口函數,而不應該作為傳回值。