前一陣子看到了一個Golang的JSON庫go-simplejson
,用來封裝與解析匿名的JSON,說白了就是用map
或者slice
等來解析JSON,以爲挺好玩,後來有個項目剛好要解析JSON,因而就試了試,不當心看了一眼源代碼,發現居然是用的Golang自帶的encoding/json
庫去作的解析,而其自己只是把這個庫封裝了一層,看起來更好看罷了。因而心想能不能徒手寫一個解析器,畢竟寫了這麼多年代碼了,也JSON.parse
,JSON.stringify
了無數次。搗騰了兩天,終於成了,測試了一下,性能比自帶的庫要高不少,速度基本上在1.6
到7
倍之間(視JSON串的大小和結構而定),因此決定寫這篇文章分享一下思路。node
先插一個段子,做爲一個已經完完整整寫了將近三年代碼的老碼農,前一段面試,不止一次有面試官問我:如何深拷貝一個對象(JS),我笑笑說寫一個Walk函數遞歸一下就好了啊,若是要考慮到Stackoverflow,那就用棧+迭代就行了。而後他們總是問我,有沒有更好的辦法,而後自言自語的說你能夠用JSON先序列化一遍再反序列化……git
項目取名cheapjson
,意思是便宜的,由於你不光不須要定義各個struct,性能還比原生的快,因此很便宜。地址在 https://github.com/acrazing/c...,有興趣的能夠看看~github
首先既然是便宜的,便和反射無關了,因此void *
是必需的,固然在Golang裏面是interface{}
,而後須要一個結構來保存必需的信息,進行類型判斷以及邊界檢查。若是是C的話,數組大小,字符串長度,對象Key/Value映射都是必需的工做。不過在Golang裏面就不須要了,編譯器已經搞定了全部的工做。面試
在JSON當中,一個完整的JSON應該包含一個value
,這個value
的類型多是null
,true
,false
,number
,string
, array
以及 object
共6種。而array
和object
還有可能包含子value
結構。這些類型的值映射到Golang當中,即是nil
, bool
, bool
, int64/float64
, string
, []interface{}
, map[string]interface{}
,用一個union
結構即可以搞定。注意這裏的number
有能夠轉換成整數或者是浮點數,在JavaScript中,所有用64
位雙精度浮點數儲存,因此最大的精確整數也就是非規約數是尾數部分2^53 - 1
,已經遠遠大於int32
了,因此這裏將整數映射成了int64
而不是int
,由於在部分機器上可能溢出,嚴格的區分一個IEEE-754
格式的整數和浮點數並非一件輕鬆的事情,這裏簡化成了若是尾數中的小數部分以及指數部分均不存在,則認爲是一個整數,此外,爲了簡化操做,對於任何不合法的UTF-16
字符串,都認爲結構有問題,而終止解析。爲了方便,定義一個結構來保存一個JSON的value
:json
type struct Value { value interface{} }
結構中的value
字段保存這個JSONValue
的實際值,經過類型斷定來肯定其類型。所以會有不少的斷定,賦值,以及取值函數,好比針對一個string
類型的Value
須要有斷定是否爲string
的操做IsString()
,賦值AsString()
,以及獲取真實值的操做String()
:數組
// 斷定是否爲string,若是是,則返回true,不然返回false func (v *Value) IsString() bool { if _, ok := v.value.(string); ok { return true } return false } // 將一個Value賦值爲一個string func (v *Value) AsString(value string) { v.value = value } // 從一個string類型的Value中取出String值 func (v *Value) String() string { if value, ok := v.value.(string); ok { return value } // 若是不是一個string類型,則報錯,因此須要先斷定是否爲string類型 panic("not a string value") }
針對這樣的操做還有不少,能夠參考 cheapjson/value.go.函數
對於string
, true
, false
, null
, number
這樣的值,都屬於字面量,即沒有深層結構,可取直接讀取,而且中間不可能被空白字符切斷,因此能夠直接讀取。而對於一個array
或者object
,則是一個多層的樹狀結構。最直接的想法確定是用遞歸,可是你們都知道這是不可行的,由於在解析大JSON的時候極可能棧溢出了,因此只能用棧+迭代的辦法。oop
學過編譯原理的人都知道,作AST分析的時候首先要分析Token,而後再分析AST,在解析JSON的時候也應該這樣,雖然Token比較少:只有幾個字面量以及{
, [
, :
, ]
, }
幾個界定符。惋惜我並無學過編譯原理,上來就拿狀態機來迭代了。由於JSON是一棵樹,其解析過程是從樹根一直遍歷到各個葉節點再返回樹根的過程。天然就會涉及到棧的壓入及彈出操做。具體來說,就是在遇到array
和object
的子節點的時候要壓入棧,遇到一個value
的結束符的時候要彈出棧。同時還要保存棧結點對應的Value
以及其狀態信息。因此我定義了一個棧結點結構:性能
type struct state { state int value *Value parent *state }
其中state
表示當前棧節點的狀態,value
表示其所表明的值parent
表示其父節點,根節點的父節點爲nil
。當要壓入棧時,只須要新建一個節點,將其parent
設置爲當前節點便可,要彈出時,將當前結點設置爲當前結點的parent
。若是當前節點爲nil
,則表示遍歷結束,JSON自身也應該結束,除了空白字符外,不該該還包含任何字符。測試
一個節點可能的狀態有:
const ( // start of a value stateNone = iota stateString // after [ must be a value or ] stateArrayValueOrEnd // after a value, must be a , or ] stateArrayEndOrComma // after a {, must be a key string or } stateObjectKeyOrEnd // after a key string must be a : stateObjectColon // after a : must be a value // after a value, must be , or } stateObjectEndOrComma // after a , must be key string stateObjectKey )
狀態的含義和字面意思同樣,好比對於狀態stateArrayValueOrEnd
表示當前棧節點遇到了一個array的起始標誌[
,在等待一個子Value
或者一個array的結束符]
,而狀態stateArrayEndOrComma
表示一個array已經遇到了子Value
,在等待結束符]
或者Value
的分隔符,
。所以,在解析一個數組的時候,完整的棧操做過程是:遇到[
,將當前結點的狀態設置爲stateArrayValueOrEnd
,而後過濾空白字符,斷定第一個字符是]
仍是其它字符,若是是]
,則array結束,彈出棧,若是不是,則將自身狀態修改成stateArrayEndOrComma
,並壓入一個新棧結點,將其狀態設置爲stateNone
,從新開始解析,此結點解析完成以後,彈出此結點,斷定是,
仍是]
,若是是]
,則結束彈出,若是是,
則不改變自身狀態,並從新一個新棧結點,開始新的循環。完事的狀態機以下:
其含義以下:
首先初始化一個空節點,狀態設置爲stateNone
,而後判斷第一個非空字符,若是是t/f/n/[-0-9]
,則直接解析字面量,而後彈出,若是是[
,則將狀態設置爲stateArrayValueOrEnd
,而後斷定第一個字符,若是是]
,則結束彈出,不然壓入新棧,並將自身狀態設置爲stateArrayEndOrComma
,開始新的循環,若是是{
,則將狀態設置爲stateObjectKeyOrEnd
,若是下一個非空字符爲}
,則結束彈出,不然解析key
,完成以後,壓入新棧,並將自身狀態設置爲stateObjectEndOrComma
。
比較特殊的是stateString
,按道理其也是一個字面量,不須要到一個新的循環裏面去解析。可是由於一個object
的key
也是一個string
,爲了複用代碼,並避免調用函數產生的性能開銷,將string
類型和object的key
看成同一類型來處理,具體以下:
root := &state{&Value{nil}, stateNone, nil} curr := root for { // ignore whitespace // check curr is nil or not switch curr.state { case stateNone: switch data[offset] { case '"': // go to new loop curr.state = stateString continue } case stateObjectKey, stateString: // parse string if curr.state == stateObjectKey { // create new stack node } else { // pop stack } } }
此外比較特殊的是在解析完一個object的key以後,當即壓入了一個新棧結點,並將其狀態設置爲stateObjectColon
,同時將自身的狀態設置爲stateObjectEndOrComma
,在解析完colon以後再這個節點的狀態設置爲stateNone
,開始新的循環,具體來講:
if curr.state == stateObjectKey { curr.state = stateObjectEndOrComma curr = &state{&Value{nil}, stateObjectColon, nil} continue }
這是由於在:
以前和以後均可能有空白字符,這裏是爲了複用代碼邏輯:即在每一次迭代開始之時都把全部的空白過濾掉。
for { LOOP_WS: for ; offset < len(data); offset++ { switch data[offset] { case '\t', '\r', '\n', ' ': continue default: break LOOP_WS } // do staff }
在過濾掉空白後,若是當前棧爲nil
,則不該該有字符存在,整個解析結束,不然必定有字符,而且須要進行解析:
for { // ignore whitespace if curr == nil { if offset == len(data) { return } else { // unexpected char data[offset] at offset } } else if offset == len(data) { // unexpected EOF at offset } // do staff }
隨後即是根據當前狀態來進行相應的解析了。
從目前的開源項目上來看,性能上應該還有優化的空間,畢竟有人已經作到號稱2-4x
的速度,並且如今已經有不少項目在搞將Golang的Struct先編譯一遍,再調用生成的函數針對特定的結構進行解析,速度更快,不過既然就預先編譯了,幹嗎還要用JSON啊,直接PB/MsgPack得了。特別是djson
這個庫,解析小JSON的時候速度是原生的3-4倍,可是大的時候只有2倍,而cheapjson
則在解析大JSON的時候性能幾乎是原生的7倍,至關搞笑。而從測試結果上來看,總體上性能和內存都還能夠,可是在解析數組的時候比原生的還要差。因此值得改進,尤爲是頻繁的建立和銷燬state
節點這一點,還有數組的動態擴容等。
之後有空再慢慢搞吧,我不想白頭髮愈來愈多了。