本文golang源码为1.18版本

标准转换

使用标准转换是最常见的选择

package main

func main() {
	s := "Hello, world!"
  	// string转byte数组
	b := []byte(s)
  	// byte数组转string
	s2 := string(b)
}

该转换语句会被go编译器翻译为runtime层的方法调用,其中[]bytestring的转换对应/src/runtime/string.go:81处的slicebytetostring函数;而string[]byte的转换对应的则是/src/runtime/string.go:172处的stringtoslicebyte函数。

先来看看比较简单的stringtoslicebyte函数,tmpBuf类型是个大小为32的byte数组。

// The constant is known to the compiler.
// There is no fundamental theory behind this number. 🤣
const tmpStringBufSize = 32

type tmpBuf [tmpStringBufSize]byte

func stringtoslicebyte(buf *tmpBuf, s string) []byte {
	var b []byte
	if buf != nil && len(s) <= len(buf) {
		*buf = tmpBuf{}
		b = buf[:len(s)]
	} else {
        // 如果没有缓冲区或缓冲区大小不足,需要申请内存
		b = rawbyteslice(len(s))
	}
    // 复制数据
	copy(b, s)
	return b
}

func rawbyteslice(size int) (b []byte) {
    // 容量计算,考虑内存对齐,寻找大小最匹配的内存块
	cap := roundupsize(uintptr(size))
    // 使用mallocgc申请对应大小的内存
	p := mallocgc(cap, nil, false)
    // 如果要申请的size和最终计算得到的cap大小不一致,cap只会比size更大,清理掉多余的内存
	if cap != uintptr(size) {
		memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size))
	}
	// 将b指向这片申请好的内存。该slice不为空,故外部使用copy进行覆盖而不是append
	*(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)}
	return
}

当需要转换的字符串长度小于32时,只会进行内存复制,而大于32的话,除了复制操作,还需要分配内存。

再看看slicebytetostring函数,

// ptr指向slice的第一个元素,n是slice的长度
func slicebytetostring(buf *tmpBuf, ptr *byte, n int) (str string) {
	if n == 0 {
		return ""
	}
    ...
    // 如果只有单个byte,返回值字符串中的数据指针str直接指向预分配好的一块静态内存区
	if n == 1 {
        // staticuint64s是一个uint64数组,里面一个个值都对应着单个byte的int8值,提供此种情况下的str不可修改的指向
		p := unsafe.Pointer(&staticuint64s[*ptr])
		if goarch.BigEndian {
			p = add(p, 7)
		}
        // 填充返回数据
		stringStructOf(&str).str = p
		stringStructOf(&str).len = 1
		return
	}
	
	var p unsafe.Pointer
	if buf != nil && n <= len(buf) {
		p = unsafe.Pointer(buf)
	} else {
        // 缓冲区不存在或者不够时,分配内存
		p = mallocgc(uintptr(n), nil, false)
	}
	stringStructOf(&str).str = p
	stringStructOf(&str).len = n
    // 使用memmove复制数据
	memmove(p, unsafe.Pointer(ptr), uintptr(n))
	return
}

关于staticuint64s的一段测试代码:

package main

import "unsafe"

var staticuint64s = [...]uint64{
	0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
	0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
	0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
	0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
	0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
	0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
	0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
	0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
	0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,
	0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
	0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57,
	0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
	0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67,
	0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f,
	0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77,
	0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f,
	0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
	0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f,
	0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,
	0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f,
	0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7,
	0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf,
	0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7,
	0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf,
	0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7,
	0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf,
	0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7,
	0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf,
	0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7,
	0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef,
	0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7,
	0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff,
}

type stringStruct struct {
	str unsafe.Pointer
	len int
}

func stringStructOf(sp *string) *stringStruct {
	return (*stringStruct)(unsafe.Pointer(sp))
}

func main() {
	var s string
	stringStructOf(&s).str = unsafe.Pointer(&staticuint64s[80])
	stringStructOf(&s).len = 1
	println(s)
}

使用标准转换是最简单也是最安全的,但从上面的源码可以看到,string[]byte的两个相互转换,都可能出现新的内存分配,并且一定要进行内存的复制。这样的转换在性能敏感的场景下无法满足要求,还好我们还有其他的转换黑魔法😈。

零拷贝转换

编译器转换

其实在上文提到的两个函数旁边,还有一个特殊的函数——slicebytetostringtmp(src/runtime/string.go:154)

// slicebytetostringtmp returns a "string" referring to the actual []byte bytes.
//
// Callers need to ensure that the returned string will not be used after
// the calling goroutine modifies the original slice or synchronizes with
// another goroutine.
//
// The function is only called when instrumenting
// and otherwise intrinsified by the compiler.
//
// Some internal compiler optimizations use this function.
// - Used for m[T1{... Tn{..., string(k), ...} ...}] and m[string(k)]
//   where k is []byte, T1 to Tn is a nesting of struct and array literals.
// - Used for "<"+string(b)+">" concatenation where b is []byte.
// - Used for string(b)=="foo" comparison where b is []byte.
func slicebytetostringtmp(ptr *byte, n int) (str string) {
	if raceenabled && n > 0 {
		racereadrangepc(unsafe.Pointer(ptr),
			uintptr(n),
			getcallerpc(),
			abi.FuncPCABIInternal(slicebytetostringtmp))
	}
	if msanenabled && n > 0 {
		msanread(unsafe.Pointer(ptr), uintptr(n))
	}
	if asanenabled && n > 0 {
		asanread(unsafe.Pointer(ptr), uintptr(n))
	}
	stringStructOf(&str).str = unsafe.Pointer(ptr)
	stringStructOf(&str).len = n
	return
}

抛开无需关心的代码,核心代码只有两句:

	stringStructOf(&str).str = unsafe.Pointer(ptr)
	stringStructOf(&str).len = n

返回字符串的数据指针str直接指向了[]byte的首地址,而长度直接取[]byte长度。能够如此转换的根本原因是string的底层类型和slice的底层类型内存分布相似,所以可以直接通过修改指针的方式进行转换。

// src/runtime/string.go:238
type stringStruct struct {
    str unsafe.Pointer
    len int
}

// src/runtime/slice.go:15
type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

可见,两个结构体的前两个字段内存分布是完全一致的,并且string的底层数据类型和[]byte一致,所以unsafe.Pointer对应的实际指针都是*byte。这样的转换实际上就是指针的转换,不需要进行内存的分配和复制,所以效率非常高。

但是使用这样的代码是有风险的,上文中的代码我特意把注释也贴出来了,注释里第二段明确写到,调用者需要保证,在修改或与其他g同步了原[]byte后,返回的string不会再被使用。这是因为你强行创造的这个string所对应的底层数据并不安全,创造出该string后,你可以直接对原[]byte进行修改,这违背了go语言字符串不可修改的原则,将会产生无法捕获的错误。

所以这个方法go没有暴露给开发者,仅在源码内部可使用,供编译器在某些适用场景优化性能。

以上两种转换的性能对比:

package main

import (
	"testing"
)

const LEN = 33

func getBytes() []byte {
	var b []byte
	for i := 0; i < LEN; i++ {
		b = append(b, ' ')
	}
	return b
}

func BenchmarkByte2StringOrigin(b *testing.B) {
	bytes := getBytes()
	for i := 0; i < b.N; i++ {
		s := string(bytes)
		_ = s
	}
}

func BenchmarkByte2StringTmp(b *testing.B) {
	bytes := getBytes()
	for i := 0; i < b.N; i++ {
		s := slicebytetostringtmp(bytes)
		_ = s
	}
}


// 依赖代码
type stringStruct struct {
	str unsafe.Pointer
	len int
}

func stringStructOf(sp *string) *stringStruct {
	return (*stringStruct)(unsafe.Pointer(sp))
}

func slicebytetostringtmp(b []byte) (str string) {
	stringStructOf(&str).str = unsafe.Pointer(&b[0])
	stringStructOf(&str).len = len(b)
	return
}

长度为5的[]byte

长度32的[]byte

长度为33的[]byte

和之前源码看到的一致,对于标准转换,长度32是一个分水岭,长度大于32时,就会出现内存分配,同时操作耗时也显著增加。并且随着slice长度的增加,和指针转换的性能差距会越来越大。

自己实现——unsafe.Pointer

虽然slicebytetostringtmp我们无法使用,但是通过使用unsafe包,我们可以自行构造类似的指针转换。

先来一个最简单的版本:

func String2bytesEasy(s string) []byte {
	return *(*[]byte)(unsafe.Pointer(&s))
}

func Bytes2StringEasy(b []byte) string {
	return *(*string)(unsafe.Pointer(&b))
}

直接使用unsafe.Pointer作为中间类型进行强转,非常快速,但是会有一个小问题,String2bytesEasy函数中,返回的slicecap字段是没有赋值的,因为string类型只有两个字段,而slice有三个,未赋值的cap字段的值是随机的,取决于那一块内存的值。

优化版本:

// src/reflect/value.go:2670
type StringHeader struct {
	Data uintptr
	Len  int
}

// src/reflect/value.go:2681
type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}


func String2bytes(s string) []byte {
	stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))

	bh := reflect.SliceHeader{
		Data: stringHeader.Data,
		Len:  stringHeader.Len,
		Cap:  stringHeader.Len,
	}

	return *(*[]byte)(unsafe.Pointer(&bh))
}

这个版本用到了reflect包中的两个类型StringHeaderSliceHeader,它们分别是stringsliceruntime层形态。通过这样的转换后,我们得以直接操作它们的字段。

通过全部的三个字段赋值,最终得到的是一个完整的slice

但是这里的代码还隐藏了一个深层次的问题

继续深挖,这里的关键是我们创建的中间变量bh,特殊点在于它的类型reflect.SliceHeader。如果你打开过源码的unsafe包,在Point类型的开头有一大堆注释(以下仅部分):

// (6) Conversion of a reflect.SliceHeader or reflect.StringHeader Data field to or from Pointer.
//
// As in the previous case, the reflect data structures SliceHeader and StringHeader
// declare the field Data as a uintptr to keep callers from changing the result to
// an arbitrary type without first importing "unsafe". However, this means that
// SliceHeader and StringHeader are only valid when interpreting the content
// of an actual slice or string value.
//
//	var s string
//	hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // case 1
//	hdr.Data = uintptr(unsafe.Pointer(p))              // case 6 (this case)
//	hdr.Len = n
//
// In this usage hdr.Data is really an alternate way to refer to the underlying
// pointer in the string header, not a uintptr variable itself.
//
// In general, reflect.SliceHeader and reflect.StringHeader should be used
// only as *reflect.SliceHeader and *reflect.StringHeader pointing at actual
// slices or strings, never as plain structs.
// A program should not declare or allocate variables of these struct types.
//
//	// INVALID: a directly-declared header will not hold Data as a reference.
//	var hdr reflect.StringHeader
//	hdr.Data = uintptr(unsafe.Pointer(p))
//	hdr.Len = n
//	s := *(*string)(unsafe.Pointer(&hdr)) // p possibly already lost
//

重点就是这一句——INVALID: a directly-declared header will not hold Data as a reference.

翻译过来就是,一个直接声明的header不会将Data作为引用。

解释一下,Header的Data字段类型是uintptr而不是unsafe.Pointer,这两个类型的其中一个差别就是,go的gc不会记录uintptr的引用,这一点在SliceHeaderStringHeader的定义处也有注释进行了说明,它们俩的定义上方都有这么一段:

// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.

这意味着gc在清理内存时并不知道还存在一个Header的Data字段指向了这块内存,就可能会导致在转换的过程中,底层数据已经被gc给清理。

对应上面的例子中:

func String2bytes(s string) []byte {
	stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))

	bh := reflect.SliceHeader{
		Data: stringHeader.Data,
		Len:  stringHeader.Len,
		Cap:  stringHeader.Len,
	}
	// 在执行这一步之前,stringHeader已经没有了用处,而bh没有产生引用,此时gc认为是可以进行回收的
	return *(*[]byte)(unsafe.Pointer(&bh))
}

避免这个问题的方法也很简单,避开directly-declared就可以了,注释中也给了一个正确例子:

var s string
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // case 1
hdr.Data = uintptr(unsafe.Pointer(p))              // case 6 (this case)
hdr.Len = n

这里先创建了临时变量s,再基于s进行转换得到的header指针,所以这里并不存在一个实际的header,对hdr的赋值操作其实是在直接操作字符串s,所以也就没有了gc追踪不到的问题了。

贴一个优化代码示例:

func b2s(b []byte) string {
	return *(*string)(unsafe.Pointer(&b))
}

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

补充

关于这个问题,我还找到一个go源码库的issue:Feature: provide no-copy conversion from []byte to string,虽然issue的发起者是想问为什么没有官方提供的转换函数,但是他提供的代码有着和我所给的反例代码一样的问题,并且被其他用户指出来了,并且这个问题得到了go的组织成员的肯定:

image-20230201195628479

有趣的是,go101(应该有人认识吧)还在下面提出即使避开了header的直接定义,还是需要使用runtime.KeepAlive来保证gc不会回收数据,这就与我本文观点冲突了🤣。

image-20230201195952921

不过好在这条在下面被反对了👀

image-20230201200316161

至于为什么不把reflect包中header的Data字段改为unsafe.Pointer来解决这个问题,也有了解释:

image-20230201200812265

这里面提到的另外的issue:

unsafe: add Slice(ptr *T, len anyIntegerType) []T

proposal: spec: disallow T<->uintptr conversion for type T unsafe.Pointer

另外,我觉得该issue的发起人说的话挺有道理的,这段转换的代码藏了很深的坑,没有对源码有一定了解的人完全无法发现,而官方又没有提供转换的最佳实践,以致于网络上出现了各种自行实现的版本,而其中许多的实现都是存在问题的。

幸好,这个问题在Go2迎来了转机。

展望未来,Go1.20

在go1.20rc中的这个提交,为slicestring的转换提供了新的选择,该提交在unsafe包中新增了四个函数https://github.com/golang/go/blob/release-branch.go1.20/src/unsafe/unsafe.go:

// The function Slice returns a slice whose underlying array starts at ptr
// and whose length and capacity are len.
// Slice(ptr, len) is equivalent to
//
//	(*[len]ArbitraryType)(unsafe.Pointer(ptr))[:]
//
// except that, as a special case, if ptr is nil and len is zero,
// Slice returns nil.
//
// The len argument must be of integer type or an untyped constant.
// A constant len argument must be non-negative and representable by a value of type int;
// if it is an untyped constant it is given type int.
// At run time, if len is negative, or if ptr is nil and len is not zero,
// a run-time panic occurs.
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType

// SliceData returns a pointer to the underlying array of the argument
// slice.
//   - If cap(slice) > 0, SliceData returns &slice[:1][0].
//   - If slice == nil, SliceData returns nil.
//   - Otherwise, SliceData returns a non-nil pointer to an
//     unspecified memory address.
func SliceData(slice []ArbitraryType) *ArbitraryType

// String returns a string value whose underlying bytes
// start at ptr and whose length is len.
//
// The len argument must be of integer type or an untyped constant.
// A constant len argument must be non-negative and representable by a value of type int;
// if it is an untyped constant it is given type int.
// At run time, if len is negative, or if ptr is nil and len is not zero,
// a run-time panic occurs.
//
// Since Go strings are immutable, the bytes passed to String
// must not be modified afterwards.
func String(ptr *byte, len IntegerType) string

// StringData returns a pointer to the underlying bytes of str.
// For an empty string the return value is unspecified, and may be nil.
//
// Since Go strings are immutable, the bytes returned by StringData
// must not be modified.
func StringData(str string) *byte

由于unsafe包的特殊性,其函数的具体实现是通过汇编等手段实现的,所以你在这里只能看到函数签名。

简单介绍下四个函数的作用:

  • Slice:返回一个slice,其底层数组的起始地址即为参数ptr所指向的地址,slice的len和cap都为参数len。
  • SliceData: 返回一个指向该slice底层数组的指针。
  • String:返回一个string,其底层的byte数组的起始地址即为参数ptr所指向的地址,长度为参数len。
  • StringData: 返回参数str底层byte数组的指针。

这四个函数提供的功能非常基础,不过组合起来就能实现我们想要的字符串转换功能了,如果不想安装go1.20rc,你可以在这里去试用这四个函数:

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	str := "hello world!"
	bt := unsafe.Slice(unsafe.StringData(str), len(str))
	fmt.Println(bt)
}

有了这个写法,reflect的headers就可以弃用了。

附上三种转换的benchmark(IDE可能会报错,但是没事,go1.20可以执行):

package main

import (
	"testing"
	"unsafe"
)

const LEN = 33

func getBytes() []byte {
	var b []byte
	for i := 0; i < LEN; i++ {
		b = append(b, ' ')
	}
	return b
}

func getStr() string {
	return string(getBytes())
}

func BenchmarkString2Slice(b *testing.B) {
	str := getStr()
	for i := 0; i < b.N; i++ {
		bt := []byte(str)
		_ = bt
	}
}

func BenchmarkString2SliceReflect(b *testing.B) {
	str := getStr()
	for i := 0; i < b.N; i++ {
		bt := *(*[]byte)(unsafe.Pointer(&str))
		_ = bt
	}
}

func BenchmarkString2SliceUnsafe(b *testing.B) {
	str := getStr()
	for i := 0; i < b.N; i++ {
		bt := unsafe.Slice(unsafe.StringData(str), len(str))
		_ = bt
	}
}

func BenchmarkSlice2String(b *testing.B) {
	bytes := getBytes()
	for i := 0; i < b.N; i++ {
		ss := string(bytes)
		_ = ss
	}
}

func BenchmarkSlice2StringReflect(b *testing.B) {
	bytes := getBytes()
	for i := 0; i < b.N; i++ {
		ss := *(*string)(unsafe.Pointer(&bytes))
		_ = ss
	}
}

func BenchmarkSlice2StringUnsafe(b *testing.B) {
	bytes := getBytes()
	for i := 0; i < b.N; i++ {
		ss := unsafe.String(unsafe.SliceData(bytes), len(bytes))
		_ = ss
	}
}

对比结果