github.com/code-reading/golang@v0.0.0-20220303082512-ba5bc0e589a3/docs/006-unsafe包源码分析.md (about) 1 unsafe 包绕过了 Go 的类型系统,达到直接操作内存的目的,使用它有一定的风险性。但是在某些场景下,使用 unsafe 包提供的函数会提升代码的效率,Go 源码中也是大量使用 unsafe 包。 2 3 通过 unsafe 相关函数,可以获取结构体私有成员的地址,进而对其做进一步的读写操作,突破 Go 的类型安全限制。 4 5 unsafe 包定义了 Pointer 和三个函数: 6 7 ```go 8 type ArbitraryType int 9 type Pointer *ArbitraryType 10 func Sizeof(x ArbitraryType) uintptr 11 func Offsetof(x ArbitraryType) uintptr 12 func Alignof(x ArbitraryType) uintptr 13 ``` 14 - Arbitrary 是任意的意思,也就是说 Pointer 可以指向任意类型,实际上它类似于 C 语言里的 void*。 15 16 - Sizeof 返回类型 x 所占据的字节数,但不包含 x 所指向的内容的大小。例如,对于一个指针,函数返回的大小为 8 字节(64位机上),一个 slice 的大小则为 slice header 的大小。 17 18 - Offsetof 返回结构体成员在内存中的位置离结构体起始处的字节数,所传参数必须是结构体的成员。 19 20 - Alignof Alignof返回的对齐数是结构体中最大元素所占的内存数,不超过8。 21 22 通过三个函数可以获取变量的大小、偏移、对齐等信息。 23 24 注意到以上三个函数返回的结果都是 uintptr 类型,这和 unsafe.Pointer 可以相互转换。三个函数都是在编译期间执行,它们的结果可以直接赋给 const型变量。另外,因为三个函数执行的结果和操作系统、编译器相关,所以是不可移植的。 25 26 综上所述,unsafe 包提供了 2 点重要的能力: 27 28 > 任何类型的指针和 unsafe.Pointer 可以相互转换。 29 30 > uintptr 类型和 unsafe.Pointer 可以相互转换。 31 32 uintptr 可以和 unsafe.Pointer 进行相互转换,uintptr 可以进行数学运算。这样,通过 uintptr 和 unsafe.Pointer 的结合就解决了 Go 指针不能进行数学运算的限制。 33 34 uintptr 并没有指针的语义,意思就是 uintptr 所指向的对象会被 gc 无情地回收。而 unsafe.Pointer 有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收。 35 36 pointer 不能直接进行数学运算,但可以把它转换成 uintptr,对 uintptr 类型进行数学运算,再转换成 pointer 类型。 37 38 uintptr 是一个地址数值,它不是指针,与地址上的对象没有引用关系,垃圾回收器不会因为有一个uintptr类型的值指向某对象而不回收该对象。 39 40 unsafe.Pointer是一个指针,类似于C的void *,它与地址上的对象存在引用关系,垃圾回收器会因为有一个unsafe.Pointer类型的值指向某对象而不回收该对象。 41 42 任何指针都可以转为unsafe.Pointer 43 44 unsafe.Pointer可以转为任何指针 45 46 uintptr可以转换为unsafe.Pointer 47 48 unsafe.Pointer可以转换为uintptr 49 50 指针不能直接转换为uintptr 51 52 Offsetof 获取成员偏移量 53 54 对于一个结构体,通过 offset 函数可以获取结构体成员的偏移量,进而获取成员的地址,读写该地址的内存,就可以达到改变成员值的目的。 55 56 这里有一个内存分配相关的事实:结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址。 57 58 ```go 59 package main 60 import ( 61 "fmt" 62 "unsafe" 63 ) 64 type Programmer struct { 65 name string 66 language string 67 } 68 func main() { 69 p := Programmer{"stefno", "go"} 70 fmt.Println(p) 71 name := (*string)(unsafe.Pointer(&p)) 72 *name = "qcrao" 73 lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Offsetof(p.language))) 74 *lang = "Golang" 75 fmt.Println(p) 76 } 77 ``` 78 output 79 80 ```shell 81 {stefno go} 82 {qcrao Golang} 83 ``` 84 85 name 是结构体的第一个成员,因此可以直接将 &p 解析成 *string。这一点,在前面获取 map 的 count 成员时,用的是同样的原理。 86 87 对于结构体的私有成员,现在有办法可以通过 unsafe.Pointer 改变它的值了。 88 89 私有成员变量 90 91 ```go 92 type Programmer struct { 93 name string 94 age int 95 language string 96 } 97 98 func main() { 99 p := Programmer{"stefno", 18, "go"} 100 fmt.Println(p) 101 lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Sizeof(int(0)) + unsafe.Sizeof(string("")))) 102 *lang = "Golang" 103 fmt.Println(p) 104 } 105 106 ``` 107 108 output 109 110 ```shell 111 {stefno 18 go} 112 {stefno 18 Golang} 113 ``` 114 115 string 和 slice 的相互转换 116 117 这是一个非常精典的例子。实现字符串和 bytes 切片之间的转换,要求是 zero-copy。想一下,一般的做法,都需要遍历字符串或 bytes 切片,再挨个赋值。 118 119 完成这个任务,我们需要了解 slice 和 string 的底层数据结构 120 121 ```go 122 type StringHeader struct { 123 Data uintptr 124 Len int 125 } 126 type SliceHeader struct { 127 Data uintptr 128 Len int 129 Cap int 130 } 131 ``` 132 133 上面是反射包下的结构体,路径:src/reflect/value.go。只需要共享底层 []byte 数组就可以实现 zero-copy 134 135 ```go 136 func string2bytes(s string) []byte { 137 stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s)) 138 bh := reflect.SliceHeader{ 139 Data: stringHeader.Data, 140 Len: stringHeader.Len, 141 Cap: stringHeader.Len, 142 } 143 return *(*[]byte)(unsafe.Pointer(&bh)) 144 } 145 func bytes2string(b []byte) string{ 146 sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b)) 147 sh := reflect.StringHeader{ 148 Data: sliceHeader.Data, 149 Len: sliceHeader.Len, 150 } 151 return *(*string)(unsafe.Pointer(&sh)) 152 } 153 ``` 154 155 通过构造 slice header 和 string header,来完成 string 和 byte slice 之间的转换 156 157 158 需要注意的是 栈增长时栈可能会发生移动, 如下 159 160 ```go 161 var obj int 162 fmt.Println(uintptr(unsafe.Pointer(&obj))) 163 bigFunc() // bigFunc()增大了栈 164 fmt.Println(uintptr(unsafe.Pointer(&obj))) 165 ``` 166 167 完全有可能打印出两个不同的地址 168 169 建议多用atomic 包 170 171 另外一个示例 172 173 ```go 174 package main 175 176 type Person struct { 177 Name string 178 age int 179 } 180 181 func main() { 182 p := &Person{ 183 Name: "张三", 184 } 185 186 fmt.Println(p) 187 // *Person是不能直接转换为*string的,所以这里先将*Person转为unsafe.Pointer,再将unsafe.Pointer转为*string 188 pName := (*string)(unsafe.Pointer(p)) 189 *pName = "李四" 190 191 // 正常手段是不能操作Person.age的这里先通过uintptr(unsafe.Pointer(pName))得到Person.Name的地址 192 // 通过unsafe.Sizeof(p.Name)得到Person.Name占用的字节数 193 // Person.Name的地址 + Person.Name占用的字节数就得到了Person.age的地址,然后将地址转为int指针。 194 pAge := (*int)(unsafe.Pointer((uintptr(unsafe.Pointer(pName)) + unsafe.Sizeof(p.Name)))) 195 // 将p的age字段修改为12 196 *pAge = 12 197 198 fmt.Println(p) 199 } 200 ``` 201 202 为什么需要uintptr这个类型呢? 203 204 理论上说指针不过是一个数值,即一个uint,但实际上在go中unsafe.Pointer是不能通过强制类型转换为一个uint的,只能将unsafe.Pointer强制类型转换为一个uintptr。 205 206 ```go 207 var v1 float64 = 1.1 208 var v2 *float64 = &v1 209 _ = int(v2) // 这里编译报错:cannot convert unsafe.Pointer(v2) (type unsafe.Pointer) to type uint 210 ``` 211 212 但是可以将一个unsafe.Pointer强制类型转换为一个uintptr: 213 214 ```go 215 var v1 float64 = 1.1 216 var v2 *float64 = &v1 217 var v3 uintptr = uintptr(unsafe.Pointer(v2)) 218 v4 := uint(v3) 219 fmt.Println(v3, v4) // v3和v4打印出来的值是相同的 220 ``` 221 222 可以理解为uintptr是专门用来指针操作的uint。 223 224 另外需要指出的是指针不能直接转为uintptr,即 225 226 ```go 227 var a float64 228 uintptr(&a) 这里会报错,不允许将*float64转为uintptr 229 ``` 230 231 unsafe.Sizeof 和 unsafe.Offsetof 的理解 232 233 234 unsafe.Sizeof 返回这个字段占用的字节数 235 236 unsafe.Offsetof 返回这个字段 距离结构体其实地址的偏移位置 237 238 ```go 239 package main 240 241 import ( 242 "fmt" 243 "unsafe" 244 "log" 245 ) 246 247 func main() { 248 249 var x struct { 250 a bool 251 b int16 252 c []int 253 } 254 255 /** 256 unsafe.Offsetof 函数的参数必须是一个字段 x.f, 然后返回 f 字段相对于 x 起始地址的偏移量, 包括可能的空洞. 257 */ 258 259 /** 260 uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b) 261 指针的运算 262 */ 263 // 和 pb := &x.b 等价 264 265 pb := (*int16)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b))) 266 *pb = 42 267 fmt.Println(x.b) // "42" 268 269 //m := x{} 270 x.a = true 271 x.b = 132 272 x.c = []int{1,2,2,3,4} 273 log.Println("Sizeof:") 274 log.Println(unsafe.Sizeof(x.a)) 275 log.Println(unsafe.Sizeof(x.b)) 276 log.Println(unsafe.Sizeof(x.c)) 277 log.Println(unsafe.Sizeof(x)) 278 log.Println("Offsetof:") 279 log.Println(unsafe.Offsetof(x.a)) 280 log.Println(unsafe.Offsetof(x.b)) 281 log.Println(unsafe.Offsetof(x.c)) 282 log.Println("ttt:") 283 type SizeOfE struct { 284 A byte // 1 285 C byte // 1 //调换一下B C的顺序,择Sizeof 整个结构体的大小由24变为了16 286 B int64 // 8 287 } 288 var tt SizeOfE 289 log.Println(unsafe.Sizeof(SizeOfE{})) 290 log.Println(unsafe.Sizeof(tt.A)) 291 log.Println(unsafe.Sizeof(tt.B)) 292 log.Println(unsafe.Sizeof(tt.C)) 293 log.Println(unsafe.Sizeof(tt)) 294 log.Println("AlignOf:") 295 log.Println(unsafe.Alignof(tt.A)) 296 log.Println(unsafe.Alignof(tt.B)) 297 log.Println(unsafe.Alignof(tt.C)) 298 log.Println(unsafe.Alignof(tt)) 299 300 } 301 ``` 302 303 如何得到一个对象所占内存大小? 304 305 ```go 306 fmt.Println(unsafe.Sizeof(int64(0))) // "8" 307 308 type SizeOfA struct { 309 A int 310 } 311 unsafe.Sizeof(SizeOfA{0}) // 8 312 313 type SizeOfC struct { 314 A byte // 1字节 315 C int32 // 4字节 316 } 317 unsafe.Sizeof(SizeOfC{0, 0}) // 8 返回占用的字节数 318 unsafe.Alignof(SizeOfC{0, 0}) // 4 返回内存对齐之后,占用字节数最大的字段, 但是不超过8 319 结构体中A byte占1字节,C int32占4字节. SizeOfC占8字节 320 ``` 321 内存对齐 322 323 为何会有内存对齐? 324 > 1.并不是所有硬件平台都能访问任意地址上的任意数据。 325 326 > 2.性能原因 访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存只需访问一次。 327 328 上面代码SizeOfC中元素一共5个字节,而实际结构体占8字节 329 330 是因为这个结构体的对齐倍数Alignof(SizeOfC) = 4.也就是说,结构体占的实际大小必须是4的倍数,也就是8字节。 331 332 ```go 333 type SizeOfD struct { 334 A byte 335 B [5]int32 336 } 337 unsafe.Sizeof(SizeOfD{}) // 24 338 unsafe.Alignof(SizeOfD{}) // 4 339 ``` 340 341 > Alignof返回的对齐数是结构体中最大元素所占的内存数,不超过8,如果元素是数组那么取数组类型所占的内存值而不是整个数组的值 342 343 ```go 344 type SizeOfE struct { 345 A byte // 1 346 B int64 // 8 347 C byte // 1 348 } 349 unsafe.Sizeof(SizeOfE{}) // 24 350 unsafe.Alignof(SizeOfE{}) // 8 351 ``` 352 353 > SizeOfE中,元素的大小分别为1,8,1,但是实际结构体占24字节,远超元素实际大小,因为内存对齐原因,最开始分配的8字节中包含了1字节的A,剩余的7字节不足以放下B,又为B分配了8字节,剩余的C独占再分配的8字节。 354 355 ```go 356 type SizeOfE struct { 357 A byte // 1 358 C byte // 1 359 B int64 // 8 360 } 361 unsafe.Sizeof(SizeOfE{}) // 16 362 unsafe.Alignof(SizeOfE{}) // 8 363 ``` 364 365 > 换一种写法,把A,C放到上面,B放到下面。这时SizeOfE占用的内存变为了16字节。因为首先分配的8字节足以放下A和C,省去了8字节的空间。 366 上面一个结构体中元素的不同顺序足以导致内存分配的巨大差异。前一种写法产生了很多的内存空洞,导致结构体不够紧凑,造成内存浪费。 367 368 这里建议在构建结构体时,按照字段大小的升序进行排序,会减少一点的内存空间。 369 370 unsafe.Offsetof:返回结构体中元素所在内存的偏移量 371 372 ```go 373 type SizeOfF struct { 374 A byte 375 C int16 376 B int64 377 D int32 378 } 379 unsafe.Offsetof(SizeOfF{}.A) // 0 380 unsafe.Offsetof(SizeOfF{}.C) // 2 381 unsafe.Offsetof(SizeOfF{}.B) // 8 382 unsafe.Offsetof(SizeOfF{}.D) // 16 383 ``` 384 385  386 SizeOfF 内存布局图, 蓝色区域是元素实际所占内存,灰色为内存空洞。 387 388 下面总结一下go语言中各种类型所占内存大小(x64环境下): 389 X64下1机器字节=8字节 390 391  392 Golang内置类型占用内存大小 393 394 395 > 从例子中可以看出,结构体中元素不同顺序的排列会导致内存分配的极大差异,不好的顺序会产生许多的内存空洞,造成大量内存浪费。 396 397 > 虽然这几个函数都在unsafe包中,但是他们并不是不安全的。在需要优化内存空间时这几个函数非常有用。 398 399 400 反射包 401 402 ```go 403 unsafe.Alignof(w)等价于reflect.TypeOf(w).Align 404 unsafe.Alignof(w.i)等价于reflect.Typeof(w.i).FieldAlign() 405 ``` 406 407 对齐Alignof 408 409 要了解这个函数,你需要了解数据对齐。简单的说,它让数据结构在内存中以某种的布局存放,是该数据的读取性能能够更加的快速。 410 411 CPU 读取内存是一块一块读取的,块的大小可以为 2、4、6、8、16 字节等大小。块大小我们称其为内存访问粒度。 412 413 普通字段的对齐值 414 415 ```go 416 fmt.Printf("bool align: %d\n", unsafe.Alignof(bool(true))) 417 fmt.Printf("int32 align: %d\n", unsafe.Alignof(int32(0))) 418 fmt.Printf("int8 align: %d\n", unsafe.Alignof(int8(0))) 419 fmt.Printf("int64 align: %d\n", unsafe.Alignof(int64(0))) 420 fmt.Printf("byte align: %d\n", unsafe.Alignof(byte(0))) 421 fmt.Printf("string align: %d\n", unsafe.Alignof("EDDYCJY")) 422 fmt.Printf("map align: %d\n", unsafe.Alignof(map[string]string{})) 423 ``` 424 425 output 426 ```go 427 bool align: 1 428 int32 align: 4 429 int8 align: 1 430 int64 align: 8 431 byte align: 1 432 string align: 8 433 map align: 8 434 ``` 435 在 Go 中可以调用 unsafe.Alignof 来返回相应类型的对齐系数。通过观察输出结果,可得知基本都是 2n,最大也不会超过 8。这是因为我们的64位编译器默认对齐系数是 8,因此最大值不会超过这个数。 436 437 对齐规则 438 439 - 结构体的成员变量,第一个成员变量的偏移量为 0。往后的每个成员变量的对齐值必须为编译器默认对齐长度(#pragma pack(n))或当前成员变量类型的长度(unsafe.Sizeof),取最小值作为当前类型的对齐值。其偏移量必须为对齐值的整数倍 440 441 - 结构体本身,对齐值必须为编译器默认对齐长度或结构体的所有成员变量类型中的最大长度,取最大数的最小整数倍作为对齐值 442 443 结合以上两点,可得知若编译器默认对齐长度超过结构体内成员变量的类型最大长度时,默认对齐长度是没有任何意义的 444 445 结构体的对齐值 446 下面来看一下结构体的对齐: 447 ```go 448 type part struct { 449 a bool // 1 450 b int32 //4 451 c int8 // 1 452 d int64 // 8 453 e byte // 1 454 } 455 456 func main() { 457 var p part 458 fmt.Println(unsafe.Sizeof(p)) // 32 459 } 460 ``` 461 462 按照普通字段(结构体内成员变量)的对齐方式,我们可以计算得出,这个结构体的大小占1+4+1+8+1=15个字节,但是用unsafe.Sizeof计算发现part结构体占32字节,是不是有点惊讶 463 464 这里面就涉及到了内存对齐,下面我们来分析一下: 465 466  467 468 调整一下 469 470 ```go 471 type part struct { 472 a bool // 1 473 c int8 // 1 474 e byte // 1 475 b int32 //4 476 d int64 // 8 477 } 478 479 func main() { 480 var p part 481 fmt.Println(unsafe.Sizeof(p)) // 16 482 } 483 ```