這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Static Typed Go
Go作為一門靜態類型的語言,所有變數都定義了其所屬於的類型,不同類型的變數間不能隨意賦值,例如:
1234567 |
var a intvar b stringa = 1b = "codeb2cc"a = b
|
a和b不是同一類型的變數,若嘗試直接賦值將會報錯cannot use b (type string) as type int in assignment
,不同類型變數間的賦值需要進行類型轉換(Conversion),這點與C/C++裡是一致的。在Go裡,對於變數x與目標類型T,類型轉換需要滿足以下其中一種情況:
- x可以賦值為類型T的變數
- x與T有著一致的實作類別型(underlying types)
- x與T都是匿名指標類型並且具有相同的實作類別型
- x與T都是整數/浮點數類型
- x與T都是複數類型
- x是整數/bytes片段/runes,T是string
- x是string,T是bytes片段或runes
對於可以進行類型轉換的變數,通過var c = float(100)
即可得到目標類型的變數。但在實際開發過程中,我們需要的不僅僅是基本類型的轉換,譬如對於給定的介面類型:
12345 |
Type I interface { Read(b Buffer) bool Write(b Buffer) bool Close()}
|
只要實現了這三種方法,就可以認為該類型是合法的、可操作的,但由於無法確定最終傳入變數的類型,我們需要在使用前將其轉化為我們已知的類型。這種情況下由於類型轉化(Conversion)只關心資料,我們需要類型斷言(Type Assertion)來協助我們:
12345 |
var x Ivar y Filex = y.(I)z, ok := y.(I)
|
類型斷言判斷變數是否為nil
以及是否實現了斷言所需要的介面,若斷言通過將得到一個目標類型的變數,否則將出現run-time panic
。若不希望在宣告失敗時程式可以繼續執行,可以通過z, ok := y.(I)
的形式判斷變數斷言是否成功,若失敗z
為目標類型的零值。
類型斷言應用的常見例子是對JSON資料的解析。Go標準庫中對JSON類與數組的解析返回的結果分別是map[string]interface{}
與[]interface{}
,若想正確訪問資料我們就需要對返回結果進行類型斷言:
1234567891011 |
b := []byte(`{"Foo":"Bar", "Hello": ["W", "o", "r", "l", "d"]}`)var f interface{}err := json.Unmarshal(b, &f)// f類型為interface{}, 需要轉換為map[string]interface{}來訪問資料m := f.(map[string]interface{})fmt.Println(m["Foo"])// 同樣的, m["Hello"]的類型為interface{}, 在正常訪問前我們需要轉換為對應的類型n := m["Hello"].([]byte)
|
Reflection
通過類型轉換和類型斷言,我們基本能夠解決大多數問題,但單是可以解決是不夠的,我們還需要更加方便(優雅)的解決方案。考慮這樣一個情境,我們定義一個配置結構用來儲存服務的配置資訊,結構中用了多種類型:
12345 |
type Config struct { Port int64 Path string Debug bool}
|
同時我們將服務的配置儲存在文本中,在服務載入時讀取進來:
Port=8000Path=/tmp/go_reflectionDebug=true
對設定檔解析我們基本可以得到(string, []byte)這樣的元組,我們需要將資料存放區到Config
中,由於結構中已經定義了對應的類型,因此我們在儲存時需要進行類型轉換:
1234567891011 |
data = map[string][]byte{ "Port": []byte("8000"), "Path": []byte("/tmp/go_reflection"), "Debug": []byte("true"),}config := Config{}i, err := strconv.ParseInt(string(data["Port"]), 10, 64)if err == nil { config.Port = i}
|
對於int64
類型我們需要strconv.ParseInt
,對於string
我們需要string()
,對於bool
我們需要strconv.ParseBool
,在上面這個例子中我們可以通過switch
來判斷每個配置項需要進行的轉換,但在實際的應用中配置可能多達數十項,繼續使用這種原始的方法簡直就是一個噩夢。這時我們需要通過Go的Reflect模組來協助我們更優雅地解決這個問題。
反射(Reflection)作為元編程的一種形式,賦予了我們在運行時判斷變數類型的能力。Go的reflect
模組通過將資料封裝在reflect.Type
與reflect.Value
中,提供了一系列方法讓我們“動態”地去判斷變數的類型:
12345678 |
var x = 2013switch reflect.ValueOf(x).Kind() {case reflect.Int: fmt.Println("It's an int!")case reflect.String: fmt.Println("It's a string!")}
|
在我們設定檔的例子中,我們可以根據每個配置的Key動態判斷該進行何種類型轉換並儲存到那個屬性中:
1234567891011121314151617181920 |
func (s *Config) Set(key string, value []byte) error { reflectValue := reflect.ValueOf(s).Elem() reflectField := reflectValue.FieldByName(key) switch reflectField.Kind() { case reflect.Int64: i, err := strconv.ParseUint(string(value), 10, 64) if err != nil { return err } reflectField.SetInt(i) case reflect.String: reflectField.SetString(string(value)) case reflect.Bool i, err := strconv.ParseBool(string(value)) if err != nil { return err } reflectField.SetBool(i) } return nil
|
這樣,我們只需要通過config.Set("Port", []byte("8000"))
一個方法就可以正確地儲存配置資訊,就算結構定義有變更也不需要重寫解析方法。關於反射Golang Blog上有一篇詳細的文章The Laws of Reflection,可以更好地協助我們Go中反射的細節與需要注意的地方,值得一讀。