Golang:從fmt.Scanf函式想到的

2016-05-08    分類:其他、程式設計開發、首頁精華0人評論發表於2016-05-08

本文由碼農網 – Sandbox Wang原創,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃

工作中使用go有一段時間了,隨著寫的程式碼數量的增長,越來越被go的魅力所折服,同時也對相關的社群有了更多的關注。早上在go語言技術交流群裡,有網友問了一個很有意思的問題,一段很簡單的程式碼,但是卻總得不到期望的結果。

還有什麼樣的東西更能引起程式猿的興奮呢?下面是程式碼。

func testScan() {

       var test [10]byte

       var test2 = test[0:]

       n, err := fmt.Scanf("%s", &test2)

       fmt.Println(n, err)

       fmt.Printf("%s, %s\n", test, test2)

}

執行之後,輸入hello,輸出結果如下:

1 <nil>
          ,hello

按理說,test2是slice型別,它和test這個陣列共用資料儲存區,也就是說,test2被裝入了“hello”之後,test的內容也應該是“hello”才對,但是很遺憾的是,並不是。

我們知道,當slice B是從slice A初始化得來的話,A和B儲存同一份資料,但是當我們向B裡面新增更多的資料(新增之後的長度超過A原有長度)之後,B會重新開闢一個新的儲存區域來存放B原來的資料和新新增的資料。也就是說,在這個時候,A和B才有了各自獨立的儲存區域。

在我們的問題中,輸入的是hello,長度僅為5,並沒有超過10,那麼想必也不會引起test2重新開闢儲存區域吧。

百思不得其解,無奈開啟fmt/scan.go的原始碼,找到fmt.Scanf的實現:

func Scanf(format string, a ...interface{}) (n int, err error) {

       return Fscanf(os.Stdin, format, a...)

}

實現很簡單,僅僅是呼叫了更通用的Fscanf,Fscanf的實現如下:

func Fscanf(r io.Reader, format string, a ...interface{}) (n int, err error) {

       s, old := newScanState(r, false, false)

       n, err = s.doScanf(format, a)

       s.free(old)

       return

}

其中newScanState的呼叫返回了一個新的ScanState的實現,通過它的doScanf方法來完成實際的變數的解析。doScanf方法較為複雜,但是總的意思只有一個,就是逐個地對每個格式化控制符對應的變數進行解析:

func (s *ss) doScanf(format string, a []interface{}) (numProcessed int, err error) {

       defer errorHandler(&err)

       end := len(format) - 1

       //省略

       for i := 0; i <= end; {

              //省略

              s.scanOne(c, arg)

              numProcessed++

              s.argLimit = s.limit

       }

       return

}

其中可以看到最關鍵的解析變數的任務是通過ScanState.scanOne函式來實現的,這裡的變數c是rune型別,我們的變數就是從它解析出來的。arg是interface{}型別的,代表我們傳入的*[]byte型別的變數,即&test2。

再找到ScanState.scanOne函式:

func (s *ss) scanOne(verb rune, arg interface{}) {

       s.buf = s.buf[:0]

       var err error

       // If the parameter has its own Scan method, use that.

       if v, ok := arg.(Scanner); ok {

              err = v.Scan(s, verb)

              if err != nil {

                     if err == io.EOF {

                            err = io.ErrUnexpectedEOF

                     }

                     s.error(err)

              }

              return

       }

       switch v := arg.(type) {

       //省略

       case *string:

              *v = s.convertString(verb)

       case *[]byte:

              // We scan to string and convert so we get a copy of the data.

              // If we scanned to bytes, the slice would point at the buffer.

              *v = []byte(s.convertString(verb))

       //省略

}

我們可以看到,scanOne方法的邏輯非常清晰,首先判斷arg物件是否具有Scanner介面的Scan方法,如果有的話,直接呼叫它。如果沒有的話,需要對它的型別進行switch遍歷判斷,如果型別是*[]byte的話,我們驚訝地看到了這樣的賦值:

*v = []byte(s.convertString(verb))

也就是說,我們傳入的型別為[]byte指標的變數被重新賦了一個新的[]byte值。我們想象中的io.Copy等等並沒有蹤影。

看到這裡,問題的原因已經非常清楚了。對於那些寫過很多遍C/C++版本的scanf的人,是不是很無奈呢?其實,我倒是對Go的這種實現並沒有什麼意見,如果我們的本意是想讀入字串的話,把上面的程式碼改成string的話,就沒有絲毫的問題了:

var test2 string

n, err := fmt.Scanf("%s", &test2)

另外,通過這件事,我們再次得到提醒,slice物件雖然很像陣列,但是卻並不是陣列,而是類似下面的一個資料結構:

data *Elem
len int
cap int

所以,當我們對slice物件進行再賦值或函式傳參的時候,上面的結構被完全複製了一份,但是資料指標域仍指向同一個資料儲存區域,即共享資料儲存。例如,下面的程式碼:

func testBasic() {

       a := make([]int, 4)

       b := a

       a[0] = 1

       fmt.Printf("%p,%p,%v,%v\n", &a, &b, a, b)

}

列印結果為:

0xc082004740,0xc082004780,[1 0 0 0],[1 0 0 0]

同時,就像在上面的問題中,當我們把一個slice指標作為引數傳入別的函式的時候,如果它所指向的slice被賦以一個新的slice的話,它原來所指向的值是不會發生變化的。簡單來說,就是這個指標本來指向A,後來被指向了新的B,那A當然不受影響了。

本文連結:http://www.codeceo.com/article/golang-fmtscanf.html
本文作者:碼農網 – Sandbox Wang
原創作品,轉載必須在正文中標註並保留原文連結和作者等資訊。]

相關文章