首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >探究string类型底层如何填充

探究string类型底层如何填充

作者头像
fliter
发布2023-06-18 11:08:19
发布2023-06-18 11:08:19
6230
举报
文章被收录于专栏:旅途散记旅途散记

看这段代码:

代码语言:javascript
复制
package main

import (
  "fmt"
  "time"
)

var a = "0"

func main() {

  ch := make(chan string)

  go func() {
    i := 1

    for {

      if i%2 == 0 {
        a = "0"
      } else {
        a = "aa"
      }
      // 如果将sleep去掉,则会读不到脏数据,原因在于编译器做了优化
      time.Sleep(1 * time.Millisecond)
      i++
    }
  }()

  go func() {
    for {
      b := a
      if b != "0" && b != "aa" {
        ch <- b
      }
    }
  }()

  for i := 0; i < 10; i++ {
    fmt.Println("Got strange string: ", <-ch)
  }

}

起两个协程,一个协程根据当前i的值,决定a为"0"还是"aa";

另一个协程,判断a如果既不是"0"也不是"aa",则将当前的值通过写入管道

在协程外从管道中读取数据,共读取10次,然后退出程序。


按预期,ch 不可能被写入,因为 b 的值只可能是 “0” 或 “aa”,但实际输出为:

代码语言:javascript
复制
Got strange string:  05
Got strange string:  a
Got strange string:  a
Got strange string:  05
Got strange string:  05
Got strange string:  a
Got strange string:  a
Got strange string:  a
Got strange string:  a
Got strange string:  a

(在Linux上输出为05,在Mac上05会输出为0:)

b的值为什么会存在 “a”或者”05”(or”0:”)的情况 ??

这是因为,string类型并不是并发安全的。对 string 赋值,并不是原子操作,而是会分为两步。

string在底层的存储方式如下:

代码语言:javascript
复制
type StringHeader struct {
	Data uintptr // 内容的指针地址
	Len  int     // 长度
}

对StringHeader这个struct ,Go 无法保证原子性地完成赋值

因此可能会出现 goroutine 1 刚修改完指针(Data)、还没来得及修改长度(Len),goroutine 2 就读取了这个string 的情况。

Data 改了、Len 还没来得及改,即 Data="aa",Len=1, 和Data="0",Len=2, 对应”莫名其妙”出现的 “a”和”05”(或”0:”)


Data="aa",Len=1,变为”a”容易理解,但Data="0",Len=2为什么会是”05”(or“0:”)呢?

打印一下字符串变量的Data字段指向的地址,这个地址对应的值,即在内存中字符串变量实际存储的内容

代码语言:javascript
复制
package main

import (
  "encoding/hex"
  "fmt"
  "strings"
  "unsafe"
)

func main() {

  demoStr := "0"
  dump("demoStr", &demoStr)

}

func dump(name string, strPtr *string) {
  // 分隔符
  fmt.Println(strings.Repeat("-", 30))

  // 打印strPtr这个指针类型的变量的内存地址
  fmt.Printf("&%s:%p\n", name, strPtr)
  // 强制转换为16字节的byte数组 (*[0x10]byte)(unsafe.Pointer(strPtr)),是个指针类型。再用*取指针内容;然后将数组转为切片,[:]
  fmt.Println(hex.Dump((*(*[0x10]byte)(unsafe.Pointer(strPtr)))[:]))

  // 获取上面这个指针所指向的内存地址
  p := *(*int)(unsafe.Pointer(strPtr)) // 当成int型(需要C经验更好理解)
  fmt.Printf("%s:0x%x\n", name, p)
  fmt.Println(hex.Dump((*(*[0x20]byte)(unsafe.Pointer(uintptr(p))))[:]))

}

输出为:

代码语言:javascript
复制
------------------------------
&demoStr:0x14000110060
00000000  a7 a6 d1 02 01 00 00 00  01 00 00 00 00 00 00 00  |................|

demoStr:0x102d1a6a7
00000000  30 3a 3c 3d 3e 43 4c 4d  50 53 55 5a 5b 5d 5f 60  |0:<=>CLMPSUZ[]_`|
00000010  68 6d 73 7b 7d 20 2b 20  40 20 50 20 5b 25 21 28  |hms{} + @ P [%!(|

这样就能看出,当长度为2,但实际存的内容为1时是咋填充的。

比对ASCII码表,可以看出

在该场景下,对于Mac,会填充为0x3a,即对应:

对于Linux,则会填充0x35,即对应5

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-04-16,如有侵权请联系 cloudcommunity@tencent.com 删除
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档