Go语言的引用类型

Go中的引用类型不是指针,而是对指针的包装,在它的内部通过指针引用底层数据结构。每一种引用类型也包含一些其他的field,用来管理底层的数据结构。

看一个例子比较直观:

1
2
s := []string{"a","b", "c"}
fmt.Printf("%#v \n", (*reflect.SliceHeader)(unsafe.Pointer(&s)))

简单解释一下这段代码。先初始化一个slice,然后使用unsafe.Pointer(&s)slice的指针转换为通用指针PointerPointer是可以代表任何数据类型的指针。最后把Pointer强制转换为*reflect.SliceHeaderSliceHeader代表的是slice运行时数据结构,定义如下:

1
2
3
4
5
type SliceHeader struct {
 Data uintptr
 Len  int
 Cap  int
}

可以看到,SliceHeader内部有一个用来指向底层数组的指针Data,另外还有两个属性LenCap用来保存slice的内部状态。

上面的代码运行结果如下:

&reflect.SliceHeader{Data:0xc420078180, Len:3, Cap:3}

slice可以自动扩容,当底层数组容量不够时,会自动创建一个新的数组替换。让我们做个实验:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
s := []string{"a","b", "c"}
fmt.Printf("%#v \n", s)
fmt.Printf("%#v \n", (*reflect.SliceHeader)(unsafe.Pointer(&s)))
fmt.Println()

s = append(s, "d")
fmt.Printf("%#v \n", s)
fmt.Printf("%#v \n", (*reflect.SliceHeader)(unsafe.Pointer(&s)))
fmt.Println()

s = append(s, "e")
fmt.Printf("%#v \n", s)
fmt.Printf("%#v \n", (*reflect.SliceHeader)(unsafe.Pointer(&s)))
fmt.Println()

运行结果如下:

对于初始化容量为3的slice,在向这个slice append 新元素时,底层会创建一个容量翻倍的新数组,并将原先的内容复制过来,再将新元素append到最后。我们可以看到这个slice内部保存底层数组的指针在第一次append后,指向了新的地址。当再向它append新元素时,由于底层数组还有空间,内部指针保持不变,只是更新Len属性为5。

在Go中进行函数调用时,参数都是按值传递的。对于引用类型也是按值传递,会复制引用本身,但不会复制引用指向的底层数据结构。还是看代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func foo(s []string) {
 fmt.Println("======= func foo =======")
 s = append(s, "f")
 fmt.Printf("%#v \n", s)
 fmt.Printf("%#v \n", (*reflect.SliceHeader)(unsafe.Pointer(&s)))
 fmt.Printf("&s: %p \n", &s)
 fmt.Println("========================\n")
}

func main() {

   ......
   
 s = append(s, "e")
 fmt.Printf("%#v \n", s)
 fmt.Printf("%#v \n", (*reflect.SliceHeader)(unsafe.Pointer(&s)))
 fmt.Printf("&s: %p \n", &s)
 fmt.Println()

 foo(s)

 fmt.Printf("%#v \n", s)
 fmt.Printf("%#v \n", (*reflect.SliceHeader)(unsafe.Pointer(&s)))
 fmt.Printf("&s: %p \n", &s)
}

运行结果为:

在函数调用时,传递给函数的slice进行了复制,函数的参数是一个新的slice,但slice内部指针指向的底层数组还是同一个。

完整的示例代码在https://play.golang.org/p/qwwSuskLfCa,可以在Playground中直接运行。

Go语言的引用类型有slice, map, channel, interfacefunction。技术上,string也是引用类型:

1
2
3
4
type StringHeader struct {
        Data uintptr
        Len  int
}

有时候为了性能优化,可以利用[]bytestring头部结构的“部分相同”,以非安全的指针类型转换来实现类型变更,避免底层数组的复制。例如 TiDB 中就使用了这个技巧

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// String converts slice to string without copy.
// Use at your own risk.
func String(b []byte) (s string) {
 if len(b) == 0 {
  return ""
 }
 pbytes := (*reflect.SliceHeader)(unsafe.Pointer(&b))
 pstring := (*reflect.StringHeader)(unsafe.Pointer(&s))
 pstring.Data = pbytes.Data
 pstring.Len = pbytes.Len
 return
}

// Slice converts string to slice without copy.
// Use at your own risk.
func Slice(s string) (b []byte) {
 pbytes := (*reflect.SliceHeader)(unsafe.Pointer(&b))
 pstring := (*reflect.StringHeader)(unsafe.Pointer(&s))
 pbytes.Data = pstring.Data
 pbytes.Len = pstring.Len
 pbytes.Cap = pstring.Len
 return
}

上面两个函数实现了[]bytestring的互相转换,不需要底层数组的copy。

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus