Golang:从fmt.Scanf函数想到的

Golang:从fmt.Scanf函数想到的,第1张

概述工作中使用go有一段时间了,随着写的代码数量的增长,越来越被go的魅力所折服,同时也对相关的社区有了更多的关注。早上在go语言技术交流群里,有网友问了一个很有意思的问题,一段很简单的代码,但是却总得不到期望的结果。 还有什么样的东西更能引起程序猿的兴奋呢?下面是代码。 func testScan() { var test [10]byte var test2

工作中使用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 stringn,&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当然不受影响了。

总结

以上是内存溢出为你收集整理的Golang:从fmt.Scanf函数想到的全部内容,希望文章能够帮你解决Golang:从fmt.Scanf函数想到的所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://www.outofmemory.cn/langs/1284626.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-09
下一篇 2022-06-09

发表评论

登录后才能评论

评论列表(0条)

保存