Golang:从fmt.Scanf函数想到的
工作中使用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,%sn",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对象虽然很像数组,但是却并不是数组,而是类似下面的一个数据结构:
所以,当我们对slice对象进行再赋值或函数传参的时候,上面的结构被完全复制了一份,但是数据指针域仍指向同一个数据存储区域,即共享数据存储。例如,下面的代码: func testBasic() { a := make([]int,4) b := a a[0] = 1 fmt.Printf("%p,%p,%v,%vn",&a,&b,a,b) } 打印结果为: 0xc082004740,0xc082004780,[1 0 0 0],[1 0 0 0] 同时,就像在上面的问题中,当我们把一个slice指针作为参数传入别的函数的时候,如果它所指向的slice被赋以一个新的slice的话,它原来所指向的值是不会发生变化的。简单来说,就是这个指针本来指向A,后来被指向了新的B,那A当然不受影响了。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |