看这段代码:
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”,但实际输出为:
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在底层的存储方式如下:
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字段指向的地址,这个地址对应的值,即在内存中字符串变量实际存储的内容
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))))[:]))
}输出为:
------------------------------
&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
