工作中使用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,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,&test2)
另外,通过这件事,我们再次得到提醒,slice对象虽然很像数组,但是却并不是数组,而是类似下面的一个数据结构:
data*Elem |
lenint |
capint |
所以,当我们对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当然不受影响了。