일부 하위 수준 라이브러리에서는 안전하지 않은 패키지를 사용하는 경우를 자주 볼 수 있습니다. 이 기사는 Golang의 안전하지 않은 패키지를 이해하고 안전하지 않은 패키지의 역할과 포인터 사용 방법을 소개하는 데 도움이 되기를 바랍니다.
unsafe 패키지는 go 유형의 안전 검사를 우회할 수 있는 일부 작업을 제공하여 메모리 주소를 직접 작동하고 일부 까다로운 작업을 수행합니다. 샘플 코드의 실행 환경은
go version go1.18 darwin/amd64
입니다. unsafe 패키지는 "변수에 대한 포인터가 가리키는 메모리 크기를 제외하고" 변수가 차지하는 메모리 크기를 얻기 위해 Sizeof 메소드를 제공하고 Alignof는 메모리 정렬을 얻습니다.
type demo1 struct { a bool // 1 b int32 // 4 c int64 // 8 } type demo2 struct { a bool // 1 c int64 // 8 b int32 // 4 } type demo3 struct { // 64 位操作系统, 字长 8 a *demo1 // 8 b *demo2 // 8 } func MemAlign() { fmt.Println(unsafe.Sizeof(demo1{}), unsafe.Alignof(demo1{}), unsafe.Alignof(demo1{}.a), unsafe.Alignof(demo1{}.b), unsafe.Alignof(demo1{}.c)) // 16,8,1,4,8 fmt.Println(unsafe.Sizeof(demo2{}), unsafe.Alignof(demo2{}), unsafe.Alignof(demo2{}.a), unsafe.Alignof(demo2{}.b), unsafe.Alignof(demo2{}.c)) // 24,8,1,4,8 fmt.Println(unsafe.Sizeof(demo3{})) // 16 } // 16}复制代码
위의 경우에서 데모1과 데모2는 동일한 속성을 포함하지만 정의된 속성의 순서가 다르기 때문에 메모리 크기가 다른 것을 볼 수 있습니다. 변수. 메모리 정렬이 일어나기 때문이다.
컴퓨터가 작업을 처리할 때 특정 단어 길이 단위로 데이터를 처리합니다. "예: 32비트 운영 체제, 단어 길이는 4, 64비트 운영 체제, 단어 길이는 8"입니다. 그러면 데이터를 읽을 때 단위도 단어 길이를 기준으로 합니다. 예: 64비트 운영 체제의 경우 프로그램이 한 번에 읽는 바이트 수는 8의 배수입니다. 다음은 비메모리 정렬 및 메모리 정렬 하의 데모1 레이아웃입니다.
비메모리 정렬:
변수 c는 읽을 때 서로 다른 단어 길이로 배치됩니다. CPU는 동시에 두 번 읽어야 합니다. 그리고 둘 다 읽습니다. 시간의 결과를 처리해야만 c의 값을 얻을 수 있습니다. 이 방법을 사용하면 메모리 공간이 절약되지만 처리 시간이 늘어납니다.
메모리 정렬:
메모리 정렬은 동일한 비메모리 정렬 상황을 피할 수 있는 방식을 채택하지만 "시간을 위한 공간"을 추가로 차지합니다. 특정 메모리 정렬 규칙에 대해서는 Google에서 검색할 수 있습니다.
여기서 포인터 유형을 선언할 수 있습니다. 즉, 포인터가 가리키는 유형이 무엇인지 명확히 해야 합니다. 컴파일하는 동안 오류가 발생합니다. 다음 예에서와 같이 컴파일러는 MyString과 string이 서로 다른 유형이므로 할당할 수 없다고 생각합니다.
func main() { type MyString string s := "test" var ms MyString = s // Cannot use 's' (type string) as the type MyString fmt.Println(ms) }
모든 유형의 변수를 가리킬 수 있는 유형이 있나요? 모든 유형의 변수를 가리킬 수 있는 unsfe.Pointer를 사용할 수 있습니다. Pointer 선언을 통해 변수의 주소를 가리키는 포인터형임을 알 수 있습니다. 특정 주소에 해당하는 값은 uinptr을 통해 변환될 수 있습니다. 포인터에는 다음과 같은 네 가지 특수 연산이 있습니다.
- 모든 유형의 포인터를 포인터 유형으로 변환할 수 있습니다.
- 포인터 유형 변수를 모든 유형의 포인터로 변환할 수 있습니다.
- uintptr 유형 변수를 포인터 유형으로 변환할 수 있습니다.
- 포인터 type 변수는 uintprt 유형으로 변환될 수 있습니다.
type Pointer *ArbitraryType // uintptr is an integer type that is large enough to hold the bit pattern of // any pointer. type uintptr uintptr func main() { d := demo1{true, 1, 2} p := unsafe.Pointer(&d) // 任意类型的指针可以转换为 Pointer 类型 pa := (*demo1)(p) // Pointer 类型变量可以转换成 demo1 类型的指针 up := uintptr(p) // Pointer 类型的变量可以转换成 uintprt 类型 pu := unsafe.Pointer(up) // uintptr 类型的变量可以转换成 Pointer 类型; 当 GC 时, d 的地址可能会发生变更, 因此, 这里的 up 可能会失效 fmt.Println(d.a, pa.a, (*demo1)(pu).a) // true true true }
공식 문서에는 포인터를 사용하는 6가지 방법이 나와 있습니다.
포인터는 메모리 조각을 직접 가리키므로 이 메모리 주소는 모든 유형으로 변환될 수 있습니다. 여기서 T1과 T2는 동일한 메모리 레이아웃을 가져야 하며 비정상적인 데이터가 있을 수 있다는 점에 유의해야 합니다.
func main() { type myStr string ms := []myStr{"1", "2"} //ss := ([]string)(ms) Cannot convert an expression of the type '[]myStr' to the type '[]string' ss := *(*[]string)(unsafe.Pointer(&ms)) // 将 pointer 指向的内存地址直接转换成 *[]string fmt.Println(ms, ss) }
T1과 T2의 메모리 레이아웃이 다르면 어떻게 되나요? 아래 예에서, 데모1과 데모2는 동일한 구조를 포함하지만 메모리 정렬로 인해 서로 다른 메모리 레이아웃을 갖습니다. 포인터를 변환할 때, 데모1의 주소부터 24 "sizeof" 바이트를 읽고, 데모2의 메모리 정렬 규칙에 따라 변환이 수행되며, 첫 번째 바이트는 a:true로 변환되고, 8-16바이트가 됩니다. c.:2로 변환됩니다. 16-24바이트는 데모1의 범위를 벗어나지만 여전히 직접 읽을 수 있으며 예상치 못한 값인 b:17368000을 얻습니다.
type demo1 struct { a bool // 1 b int32 // 4 c int64 // 8 } type demo2 struct { a bool // 1 c int64 // 8 b int32 // 4 } func main() { d := demo1{true, 1, 2} pa := (*demo2)(unsafe.Pointer(&d)) // Pointer 类型变量可以转换成 demo2 类型的指针 fmt.Println(pa.a, pa.b, pa.c) // true, 17368000, 2, }
포인터는 모든 변수를 가리킬 수 있는 포인터 유형이며 포인터를 uintptr 포인터로 변환하여 인쇄할 수 있습니다. 변수의 주소를 가리킨다. 추가로: uintptr은 포인터로 변환되어서는 안 됩니다. 다음 예를 들어보겠습니다. GC가 발생하면 d의 주소가 변경될 수 있으며, 동기화되지 않은 업데이트로 인해 up이 잘못된 메모리를 가리킬 수 있습니다.
func main() { d := demo1{true, 1, 2} p := unsafe.Pointer(&d) up := uintptr(p) fmt.Printf("uintptr: %x, ptr: %p \n", up, &d) // uintptr: c00010c010, ptr: 0xc00010c010 fmt.Println(*(*demo1)(unsafe.Pointer(up))) // 不允许 }
当 Piointer 指向一个结构体时, 可以通过此方式获取到结构体内部特定属性的 Pointer。
func main() { d := demo1{true, 1, 2} // 等同于 unsafe.Pointer(&d.b); unsafe.Add(unsafe.Pointer(&d), unsafe.Offsetof(d.b)) pb := unsafe.Pointer(uintptr(unsafe.Pointer(&d)) + unsafe.Offsetof(d.b)) fmt.Println(pb) }
前面说过, 由于 GC 会导致变量的地址发生变更, 因此不可以直接处理 uintptr。但是, 在调用 syscall.Syscall 时候可以允许传递一个 uintptr, 这里可以简单理解为是编译器做了特殊处理, 来保证 uintptr 是安全的。
- 调用方式:
- syscall.Syscall(SYS_READ, uintptr( fd ), uintptr(unsafe.Pointer(p)), uintptr(n))
下面这种方式是不允许的:
u := uintptr(unsafe.Pointer(p)) // 不应该保存到一个变量上 syscall.Syscall(SYS_READ, uintptr( fd ), u, uintptr(n))
在 reflect 包中的 Value.Pointer 和 Value.UnsafeAddr 直接返回了地址对应的值「uintptr」, 可以直接将其结果转为 Pointer
func main() { d := demo1{true, 1, 2} // 等同于 unsafe.Pointer(&d.b); unsafe.Add(unsafe.Pointer(&d), unsafe.Offsetof(d.b)) pb := unsafe.Pointer(uintptr(unsafe.Pointer(&d)) + unsafe.Offsetof(d.b)) // up := reflect.ValueOf(&d.b).Pointer(), pc := unsafe.Pointer(up); 不安全, 不应存储到变量中 pc := unsafe.Pointer(reflect.ValueOf(&d.b).Pointer()) fmt.Println(pb, pc) }
SliceHeader 和 StringHeader 其实是 slice 和 string 的内部实现, 里面都包含了一个字段 Data「uintptr」, 存储的是指向 []T 的地址, 这里之所以使用 uinptr 是为了不依赖 unsafe 包。
func main() { s := "a" hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // *string to *StringHeader fmt.Println(*(*[1]byte)(unsafe.Pointer(hdr.Data))) // 底层存储的是 utf 编码后的 byte 数组 arr := [1]byte{65} hdr.Data = uintptr(unsafe.Pointer(&arr)) hdr.Len = len(arr) ss := *(*string)(unsafe.Pointer(hdr)) fmt.Println(ss) // A arr[0] = 66 fmt.Println(ss) //B }
在业务上, 经常遇到 string 和 []byte 的相互转换。我们知道, string 底层其实也是存储的一个 byte 数组, 可以通过 reflect 直接获取 string 指向的 byte 数组, 赋值给 byte 切片, 避免内存拷贝。
func StrToByte(str string) []byte { return []byte(str) } func StrToByteV2(str string) (b []byte) { bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) sh := (*reflect.StringHeader)(unsafe.Pointer(&str)) bh.Data = sh.Data bh.Cap = sh.Len bh.Len = sh.Len return b } // go test -bench . func BenchmarkStrToArr(b *testing.B) { for i := 0; i < b.N; i++ { StrToByte(`{"f": "v"}`) } } func BenchmarkStrToArrV2(b *testing.B) { for i := 0; i < b.N; i++ { StrToByteV2(`{"f": "v"}`) } } //goos: darwin //goarch: amd64 //pkg: github.com/demo/lsafe //cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz //BenchmarkStrToArr-12 264733503 4.311 ns/op //BenchmarkStrToArrV2-12 1000000000 0.2528 ns/op
通过观察 string 和 byte 的内存布局我们可以知道, 无法直接将 string 转为 []byte 「确实 cap 字段」, 但是可以直接将 []byte 转为 string
func ByteToStr(b []byte) string { return string(b) } func ByteToStrV2(b []byte) string { return *(*string)(unsafe.Pointer(&b)) } // go test -bench . func BenchmarkArrToStr(b *testing.B) { for i := 0; i < b.N; i++ { ByteToStr([]byte{65}) } } func BenchmarkArrToStrV2(b *testing.B) { for i := 0; i < b.N; i++ { ByteToStrV2([]byte{65}) } } //goos: darwin //goarch: amd64 //pkg: github.com/demo/lsafe //cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz //BenchmarkArrToStr-12 536188455 2.180 ns/op //BenchmarkArrToStrV2-12 1000000000 0.2526 ns/op
本文介绍了如何使用 unsafe 包绕过类型检查, 直接操作内存。正如 go 作者对包的命名一样, 它是 unsafe 的, 随着 go 版本的迭代, 有些机制可能会发生变更。如无必要, 不应使用这个包。如果要使用 unsafe 包, 一定要理解清楚Pointer、uinptr、对齐系数等概念。
推荐学习:Golang教程
위 내용은 Golang의 안전하지 않은 패키지에 대해 알아보기의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!