用 unsafe from string 转换的字节切片改变了它的地址

Byte slice converted with unsafe from string changes its address

我有这个函数可以在不复制的情况下将字符串转换为字节片

func StringToByteUnsafe(s string) []byte {
    strh := (*reflect.StringHeader)(unsafe.Pointer(&s))
    var sh reflect.SliceHeader
    sh.Data = strh.Data
    sh.Len = strh.Len
    sh.Cap = strh.Len
    return *(*[]byte)(unsafe.Pointer(&sh))
}

效果很好,但是非常具体的设置会产生非常奇怪的行为:

设置在这里:https://github.com/leviska/go-unsafe-gc/blob/main/pkg/pkg_test.go

会发生什么:

  1. 创建字节切片
  2. 将其转换为临时(右值)字符串并使用不安全再次将其转换为字节片
  3. 然后,复制这个切片(参考)
  4. 然后,在 goroutine 中对第二个切片做一些事情
  5. 打印前后指针

我的 linux mint 笔记本电脑上有 go 1.16:

go test ./pkg -v -count=1
=== RUN   TestSomething
0xc000046720 123 0xc000046720 123
0xc000076f20 123 0xc000046721 z
--- PASS: TestSomething (0.84s)
PASS
ok      github.com/leviska/go-unsafe-gc/pkg     0.847s

所以,第一个切片神奇地改变了它的地址,而第二个不是

如果我们使用 runtime.GC() 删除 goroutine(并且可能会稍微玩一下代码),我们可以获得两个指针来更改值(到相同的值)。

如果我们将不安全的强制转换更改为 []byte(),则一切正常,无需更改地址。此外,如果我们从此处将其更改为不安全的转换 一切都一样。

func StringToByteUnsafe(str string) []byte { // this works fine
    var buf = *(*[]byte)(unsafe.Pointer(&str))
    (*reflect.SliceHeader)(unsafe.Pointer(&buf)).Cap = len(str)
    return buf
}

我 运行 它与 GOGC=off 并得到相同的结果。我 运行 它与 -race 没有错误。

如果您运行将其作为具有主要功能的主要包,它似乎可以正常工作。另外,如果您删除 Convert 函数。我的猜测是编译器在这种情况下优化了东西。

所以,我对此有几个问题:

  1. 这到底是怎么回事?看起来像个奇怪的 UB
  2. 为什么以及如何去 运行时间神奇地改变了变量的地址?
  3. 为什么在无并发的情况下它可以更改两个地址,而在并发的情况下不能?
  4. 这个不安全的转换和来自 Whosebug 答案的转换有什么区别?为什么它有效?

或者这只是一个编译器错误?

来自 github 的完整代码的副本,您需要将其放入某个包中并 运行 作为测试:


import (
    "fmt"
    "reflect"
    "sync"
    "testing"
    "unsafe"
)

func StringToByteUnsafe(s string) []byte {
    strh := (*reflect.StringHeader)(unsafe.Pointer(&s))
    var sh reflect.SliceHeader
    sh.Data = strh.Data
    sh.Len = strh.Len
    sh.Cap = strh.Len
    return *(*[]byte)(unsafe.Pointer(&sh))
}

func Convert(s []byte) []byte {
    return StringToByteUnsafe(string(s))
}

type T struct {
    S []byte
}

func Copy(s []byte) T {
    return T{S: s}
}

func Mid(a []byte, b []byte) []byte {
    fmt.Printf("%p %s %p %s\n", a, a, b, b)
    wg := sync.WaitGroup{}
    wg.Add(1)
    go func() {
        b = b[1:2]
        wg.Done()
    }()
    wg.Wait()
    fmt.Printf("%p %s %p %s\n", a, a, b, b)
    return b
}

func TestSomething(t *testing.T) {
    str := "123"
    a := Convert([]byte(str))
    b := Copy(a)
    Mid(a, b.S)
}

来自 github 问题的回答 https://github.com/golang/go/issues/47247

The backing store of a is allocated on stack, because it does not escape. And goroutine stacks can move dynamically. b, on the other hand, escapes to heap, because it is passed to another goroutine. In general, we don't assume the address of an object don't change.

This works as intended.

而且我的版本不正确,因为

it uses reflect.SliceHeader as plain struct. You can run go vet on it, and go vet will warn you.`