12.4. 示例: 編碼為S表達式

Display是一個用於顯示結構化數據的調試工具,但是它並不能將任意的Go語言對象編碼為通用消息然後用於進程間通信。

正如我們在4.5節中中看到的,Go語言的標準庫支持了包括JSON、XML和ASN.1等多種編碼格式。還有另一種依然被廣泛使用的格式是S表達式格式,採用Lisp語言的語法。但是和其他編碼格式不同的是,Go語言自帶的標準庫並不支持S表達式,主要是因為它沒有一個公認的標準規範。

在本節中,我們將定義一個包用於將任意的Go語言對象編碼為S表達式格式,它支持以下結構:

42          integer
"hello"     string(帶有Go風格的引號)
foo         symbol(未用引號括起來的名字)
(1 2 3)     list  (括號包起來的0個或多個元素)

布爾型習慣上使用t符號表示true,空列表或nil符號表示false,但是為了簡單起見,我們暫時忽略布爾類型。同時忽略的還有chan管道和函數,因為通過反射並無法知道它們的確切狀態。我們忽略的還有浮點數、複數和interface。支持它們是練習12.3的任務。

我們將Go語言的類型編碼為S表達式的方法如下。整數和字符串以顯而易見的方式編碼。空值編碼為nil符號。數組和slice被編碼為列表。

結構體被編碼為成員對象的列表,每個成員對象對應一個有兩個元素的子列表,子列表的第一個元素是成員的名字,第二個元素是成員的值。Map被編碼為鍵值對的列表。傳統上,S表達式使用點狀符號列表(key . value)結構來表示key/value對,而不是用一個含雙元素的列表,不過為了簡單我們忽略了點狀符號列表。

編碼是由一個encode遞歸函數完成,如下所示。它的結構本質上和前面的Display函數類似:

gopl.io/ch12/sexpr

func encode(buf *bytes.Buffer, v reflect.Value) error {
    switch v.Kind() {
    case reflect.Invalid:
        buf.WriteString("nil")

    case reflect.Int, reflect.Int8, reflect.Int16,
        reflect.Int32, reflect.Int64:
        fmt.Fprintf(buf, "%d", v.Int())

    case reflect.Uint, reflect.Uint8, reflect.Uint16,
        reflect.Uint32, reflect.Uint64, reflect.Uintptr:
        fmt.Fprintf(buf, "%d", v.Uint())

    case reflect.String:
        fmt.Fprintf(buf, "%q", v.String())

    case reflect.Ptr:
        return encode(buf, v.Elem())

    case reflect.Array, reflect.Slice: // (value ...)
        buf.WriteByte('(')
        for i := 0; i < v.Len(); i++ {
            if i > 0 {
                buf.WriteByte(' ')
            }
            if err := encode(buf, v.Index(i)); err != nil {
                return err
            }
        }
        buf.WriteByte(')')

    case reflect.Struct: // ((name value) ...)
        buf.WriteByte('(')
        for i := 0; i < v.NumField(); i++ {
            if i > 0 {
                buf.WriteByte(' ')
            }
            fmt.Fprintf(buf, "(%s ", v.Type().Field(i).Name)
            if err := encode(buf, v.Field(i)); err != nil {
                return err
            }
            buf.WriteByte(')')
        }
        buf.WriteByte(')')

    case reflect.Map: // ((key value) ...)
        buf.WriteByte('(')
        for i, key := range v.MapKeys() {
            if i > 0 {
                buf.WriteByte(' ')
            }
            buf.WriteByte('(')
            if err := encode(buf, key); err != nil {
                return err
            }
            buf.WriteByte(' ')
            if err := encode(buf, v.MapIndex(key)); err != nil {
                return err
            }
            buf.WriteByte(')')
        }
        buf.WriteByte(')')

    default: // float, complex, bool, chan, func, interface
        return fmt.Errorf("unsupported type: %s", v.Type())
    }
    return nil
}

Marshal函數是對encode的包裝,以保持和encoding/...下其它包有著相似的API:

// Marshal encodes a Go value in S-expression form.
func Marshal(v interface{}) ([]byte, error) {
    var buf bytes.Buffer
    if err := encode(&buf, reflect.ValueOf(v)); err != nil {
        return nil, err
    }
    return buf.Bytes(), nil
}

下面是Marshal對12.3節的strangelove變量編碼後的結果:

((Title "Dr. Strangelove") (Subtitle "How I Learned to Stop Worrying and Lo
ve the Bomb") (Year 1964) (Actor (("Grp. Capt. Lionel Mandrake" "Peter Sell
ers") ("Pres. Merkin Muffley" "Peter Sellers") ("Gen. Buck Turgidson" "Geor
ge C. Scott") ("Brig. Gen. Jack D. Ripper" "Sterling Hayden") ("Maj. T.J. \
"King\" Kong" "Slim Pickens") ("Dr. Strangelove" "Peter Sellers"))) (Oscars
("Best Actor (Nomin.)" "Best Adapted Screenplay (Nomin.)" "Best Director (N
omin.)" "Best Picture (Nomin.)")) (Sequel nil))

整個輸出編碼為一行中以減少輸出的大小,但是也很難閱讀。下面是對S表達式手動格式化的結果。編寫一個S表達式的美化格式化函數將作為一個具有挑戰性的練習任務;不過 http://gopl.io 也提供了一個簡單的版本。

((Title "Dr. Strangelove")
 (Subtitle "How I Learned to Stop Worrying and Love the Bomb")
 (Year 1964)
 (Actor (("Grp. Capt. Lionel Mandrake" "Peter Sellers")
         ("Pres. Merkin Muffley" "Peter Sellers")
         ("Gen. Buck Turgidson" "George C. Scott")
         ("Brig. Gen. Jack D. Ripper" "Sterling Hayden")
         ("Maj. T.J. \"King\" Kong" "Slim Pickens")
         ("Dr. Strangelove" "Peter Sellers")))
 (Oscars ("Best Actor (Nomin.)"
          "Best Adapted Screenplay (Nomin.)"
          "Best Director (Nomin.)"
          "Best Picture (Nomin.)"))
 (Sequel nil))

和fmt.Print、json.Marshal、Display函數類似,sexpr.Marshal函數處理帶環的數據結構也會陷入死循環。

在12.6節中,我們將給出S表達式解碼器的實現步驟,但是在那之前,我們還需要先了解如何通過反射技術來更新程序的變量。

練習 12.3: 實現encode函數缺少的分支。將布爾類型編碼為t和nil,浮點數編碼為Go語言的格式,複數1+2i編碼為#C(1.0 2.0)格式。接口編碼為類型名和值對,例如("[]int" (1 2 3)),但是這個形式可能會造成歧義:reflect.Type.String方法對於不同的類型可能返回相同的結果。

練習 12.4: 修改encode函數,以上面的格式化形式輸出S表達式。

練習 12.5: 修改encode函數,用JSON格式代替S表達式格式。然後使用標準庫提供的json.Unmarshal解碼器來驗證函數是正確的。

練習 12.6: 修改encode,作為一個優化,忽略對是零值對象的編碼。

練習 12.7: 創建一個基於流式的API,用於S表達式的解碼,和json.Decoder(§4.5)函數功能類似。