加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 大数据 > 正文

Golang:从fmt.Scanf函数想到的

发布时间:2020-12-16 18:31:32 所属栏目:大数据 来源:网络整理
导读:工作中使用go有一段时间了,随着写的代码数量的增长,越来越被go的魅力所折服,同时也对相关的社区有了更多的关注。早上在go语言技术交流群里,有网友问了一个很有意思的问题,一段很简单的代码,但是却总得不到期望的结果。 还有什么样的东西更能引起程序猿

工作中使用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对象虽然很像数组,但是却并不是数组,而是类似下面的一个数据结构:

data*Elem
lenint
capint

所以,当我们对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当然不受影响了。

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读