unsafe
เอกสารทางการ: unsafe package - unsafe - Go Packages
ไลบรารีมาตรฐาน unsafe เป็นไลบรารีที่ทางการจัดเตรียมสำหรับการเขียนโปรแกรมระดับต่ำ การดำเนินการที่แพ็กเกจนี้จัดเตรียมสามารถข้ามระบบประเภทของ Go เพื่ออ่านและเขียนหน่วยความจำโดยตรง แพ็กเกจนี้อาจไม่มีความสามารถในการพกพา และทางการระบุว่าแพ็กเกจนี้ไม่ได้รับการคุ้มครองจากเกณฑ์ความเข้ากันได้ Go 1 แม้จะเป็นเช่นนั้น unsafe ก็ยังถูกใช้ในโปรเจกต์จำนวนมาก รวมถึงไลบรารีมาตรฐานที่ทางการจัดเตรียมด้วย
TIP
เหตุผลที่ไม่สามารถพกพาได้คือผลลัพธ์ของการดำเนินการบางอย่างขึ้นอยู่กับระบบปฏิบัติการที่แตกต่างกัน ระบบที่แตกต่างกันอาจมีผลลัพธ์ที่แตกต่างกัน
ArbitraryType
type ArbitraryType intArbitrary สามารถแปลได้ว่าใดๆ ในที่นี้แสดงถึงประเภทใดๆ และไม่เท่ากับ any ที่จริงแล้วประเภทนี้ไม่ได้เป็นของแพ็กเกจ unsafe การปรากฏที่นี่เป็นเพียงเพื่อวัตถุประสงค์ของเอกสารเท่านั้น
IntegerType
type IntegerType intIntegerType แสดงถึงประเภทจำนวนเต็มใดๆ ที่จริงแล้วประเภทนี้ไม่ได้เป็นของแพ็กเกจ unsafe การปรากฏที่นี่เป็นเพียงเพื่อวัตถุประสงค์ของเอกสารเท่านั้น
สองประเภทข้างต้นไม่จำเป็นต้องใส่ใจมากเกินไป พวกมันเป็นเพียงตัวแทนเท่านั้น เมื่อใช้ฟังก์ชันแพ็กเกจ unsafe ตัวแก้ไข甚至会提示คุณว่าประเภทไม่ตรงกัน ประเภทจริงของพวกมันคือประเภทเฉพาะที่คุณส่งเข้าไป
Sizeof
func Sizeof(x ArbitraryType) uintptrส่งคืนขนาดของตัวแปร x เป็นไบต์ ไม่รวมขนาดของเนื้อหาที่อ้างอิง เช่น:
func main() {
var ints byte = 1
fmt.Println(unsafe.Sizeof(ints))
var floats float32 = 1.0
fmt.Println(unsafe.Sizeof(floats))
var complexs complex128 = 1 + 2i
fmt.Println(unsafe.Sizeof(complexs))
var slice []int = make([]int, 100)
fmt.Println(unsafe.Sizeof(slice))
var mp map[string]int = make(map[string]int, 0)
fmt.Println(unsafe.Sizeof(mp))
type person struct {
name string
age int
}
fmt.Println(unsafe.Sizeof(person{}))
type man struct {
name string
}
fmt.Println(unsafe.Sizeof(man{}))
}1
4
16
24
8
24
16Offsetof
func Offsetof(x ArbitraryType) uintptrฟังก์ชันนี้ใช้เพื่อแสดงการออฟเซ็ตฟิลด์ในโครงสร้าง ดังนั้น x ต้องเป็นฟิลด์ของโครงสร้าง หรือกล่าวได้ว่าค่าที่ส่งกลับคือจำนวนไบต์ระหว่างที่อยู่เริ่มต้นของโครงสร้างกับที่อยู่เริ่มต้นของฟิลด์ เช่น
func main() {
type person struct {
name string
age int
}
p := person{
name: "aa",
age: 11,
}
fmt.Println(unsafe.Sizeof(p))
fmt.Println(unsafe.Offsetof(p.name))
fmt.Println(unsafe.Sizeof(p.name))
fmt.Println(unsafe.Offsetof(p.age))
fmt.Println(unsafe.Sizeof(p.age))
}24
0
16
16
8Alignof
หากไม่เข้าใจว่า memory alignment คืออะไร สามารถไปที่: Go 语言内存对齐详解 - 掘金 (juejin.cn)
func Alignof(x ArbitraryType) uintptrขนาดการจัดเรียงมักจะเป็นค่าต่ำสุดระหว่างความยาวคำของคอมพิวเตอร์เป็นไบต์กับ Sizeof เช่น บนเครื่อง amd64 ความยาวคำคือ 64 บิต หรือ 8 ไบต์ เช่น:
func main() {
type person struct {
name string
age int32
}
p := person{
name: "aa",
age: 11,
}
fmt.Println(unsafe.Alignof(p), unsafe.Sizeof(p))
fmt.Println(unsafe.Alignof(p.name), unsafe.Sizeof(p.name))
fmt.Println(unsafe.Alignof(p.age), unsafe.Sizeof(p.age))
}8 24
8 16
4 4Pointer
type Pointer *ArbitraryTypePointer เป็น "พอยน์เตอร์" ที่สามารถชี้ไปยังประเภทใดๆ ได้ ประเภทของมันคือ *ArbitraryType ประเภทนี้ใช้ร่วมกับ uintptr เพื่อให้อำนาจที่แท้จริงของแพ็กเกจ unsafe ทำงานได้ ตามคำอธิบายในเอกสารทางการ ประเภท unsafe.Pointer สามารถดำเนินการพิเศษสี่อย่าง ได้แก่:
- พอยน์เตอร์ประเภทใดๆ สามารถแปลงเป็น
unsafe.Pointerได้ unsafe.Pointerสามารถแปลงเป็นพอยน์เตอร์ประเภทใดๆ ได้uintptrสามารถแปลงเป็นunsafe.Pointerได้unsafe.Pointerสามารถแปลงเป็นuintptrได้
การดำเนินการพิเศษสี่อย่างนี้構成เป็นรากฐานของแพ็กเกจ unsafe ทั้งหมด และด้วยการดำเนินการสี่อย่างนี้จึงสามารถเขียนโค้ดที่ละเลยระบบประเภทเพื่ออ่านและเขียนหน่วยความจำโดยตรง แนะนำให้ระวังเป็นพิเศษเมื่อใช้
TIP
unsafe.Pointer ไม่สามารถ dereference ได้ เช่นเดียวกันไม่สามารถ取ที่อยู่ได้
(1) แปลง *T1 เป็น unsafe.Pointer แล้วแปลงเป็น *T2
มีประเภท *T1, *T2 สมมติว่า T2 ไม่มากกว่า T1 และทั้งสองมี layout หน่วยความจำที่เทียบเท่า ก็อนุญาตให้แปลงข้อมูลประเภท T2 เป็น T1 ได้ เช่น:
func main() {
fmt.Println(Float64bits(12.3))
fmt.Println(Float64frombits(Float64bits(12.3)))
}
func Float64bits(f float64) uint64 {
return *(*uint64)(unsafe.Pointer(&f))
}
func Float64frombits(b uint64) float64 {
return *(*float64)(unsafe.Pointer(&b))
}4623113902481840538
12.3สองฟังก์ชันนี้จริงแล้วเป็นฟังก์ชันสองฟังก์ชันภายใต้แพ็กเกจ math การเปลี่ยนแปลงประเภทในกระบวนการมีดังนี้
float64 -> *float64 -> unsafe.Pointer -> *uint64 -> uint64 -> *uint64 -> unsafe.Pointer -> *float64 -> float64(2) แปลง unsafe.Pointer เป็น uintptr
เมื่อแปลง unsafe.Pointer เป็น uintptr จะแปลงที่อยู่ซึ่งชี้โดย前者เป็นค่าหลัง uintptr เก็บที่อยู่ ความแตกต่างคือ前者เป็นพอยน์เตอร์ในทางไวยากรณ์ เป็น reference หลังเป็นเพียงค่าจำนวนเต็ม เช่น
func main() {
num := 1
fmt.Println(unsafe.Pointer(&num))
fmt.Printf("0x%x", uintptr(unsafe.Pointer(&num)))
}0xc00001c088
0xc00001c088ความแตกต่างที่สำคัญอีกประการคือการจัดการ garbage collection เนื่องจาก unsafe.Pointer เป็น reference จะไม่ถูก回收เมื่อต้องการ ส่วนหลังเป็นเพียงค่า แน่นอนว่าจะไม่ได้รับการปฏิบัติพิเศษเช่นนี้ อีกจุดที่ควรสังเกตคือเมื่อที่อยู่ขององค์ประกอบที่พอยน์เตอร์ชี้移动 GC จะอัปเดตที่อยู่เก่าที่พอยน์เตอร์อ้างอิง แต่จะไม่อัปเดตค่าที่ uintptr เก็บไว้ เช่น โค้ดด้านล่างอาจมีปัญหา:
func main() {
num := 16
address := uintptr(unsafe.Pointer(&num))
np := (*int64)(unsafe.Pointer(address))
fmt.Println(*np)
}ในบางกรณี เมื่อ GC ย้ายตัวแปร address ชี้ไปยังที่อยู่ที่ไม่ถูกต้องแล้ว การใช้ค่านั้นเพื่อสร้างพอยน์เตอร์จะทำให้เกิด panic
panic: runtime error: invalid memory address or nil pointer dereferenceดังนั้นไม่แนะนำให้เก็บค่าที่แปลงจาก Pointer เป็น uintptr
(3) แปลง uintptr เป็น unsafe.Pointer
วิธีต่อไปนี้สามารถรับพอยน์เตอร์ผ่าน uintptr ตราบใดที่พอยน์เตอร์มีประสิทธิภาพ จะไม่เกิดสถานการณ์ที่อยู่ไม่ถูกต้องเช่นตัวอย่างที่สอง Pointer กับพอยน์เตอร์ประเภทไม่รองรับการคำนวณพอยน์เตอร์ แต่ uintptr เป็นเพียงค่าจำนวนเต็ม สามารถดำเนินการทางคณิตศาสตร์ได้ การดำเนินการทางคณิตศาสตร์กับ uintptr แล้วแปลงเป็น Pointer สามารถ完成การคำนวณพอยน์เตอร์ได้
p = unsafe.Pointer(uintptr(p) + offset)เช่นนี้ สามารถเข้าถึงองค์ประกอบภายในของบางประเภทผ่านพอยน์เตอร์เดียว เช่น อาร์เรย์และโครงสร้าง ไม่ว่าองค์ประกอบภายในจะเปิดเผยภายนอกหรือไม่ เช่น
func main() {
type person struct {
name string
age int32
}
p := &person{"jack", 18}
pp := unsafe.Pointer(p)
fmt.Println(*(*string)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(p.name))))
fmt.Println(*(*int32)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(p.age))))
s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
ps := unsafe.Pointer(&s[0])
fmt.Println(*(*int)(unsafe.Pointer(uintptr(ps) + 8)))
fmt.Println(*(*int)(unsafe.Pointer(uintptr(ps) + 16)))
}jack
18
2Add
func Add(ptr Pointer, len IntegerType) PointerAdd จะส่งคืน Pointer ที่อัปเดตด้วยการออฟเซ็ต len เทียบเท่ากับ Pointer(uintptr(ptr) + uintptr(len))
Pointer(uintptr(ptr) + uintptr(len))เช่น:
func main() {
s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
ps := unsafe.Pointer(&s[0])
fmt.Println(*(*int)(unsafe.Add(ps, 8)))
fmt.Println(*(*int)(unsafe.Add(ps, 16)))
}2
3SliceData
func SliceData(slice []ArbitraryType) *ArbitraryTypฟังก์ชันนี้รับสไลซ์ ส่งคืนที่อยู่เริ่มต้นของอาร์เรย์พื้นฐานของมัน หากไม่ใช้ SliceData สามารถรับที่อยู่ของอาร์เรย์พื้นฐานได้ผ่านพอยน์เตอร์ขององค์ประกอบแรกเท่านั้น ดังนี้
func main() {
nums := []int{1, 2, 3, 4}
for p, i := unsafe.Pointer(&nums[0]), 0; i < len(nums); p, i = unsafe.Add(p, unsafe.Sizeof(nums[0])), i+1 {
num := *(*int)(p)
fmt.Println(num)
}
}แน่นอนยังสามารถรับผ่านประเภท reflect.SliceHeader ได้ แต่หลังเวอร์ชัน 1.20 มันถูกเลิกใช้แล้ว SliceData มีไว้เพื่อแทนที่มัน ตัวอย่างการใช้ SliceData มีดังนี้
func main() {
nums := []int{1, 2, 3, 4}
for p, i := unsafe.Pointer(unsafe.SliceData(nums)), 0; i < len(nums); p, i = unsafe.Add(p, unsafe.Sizeof(int(0))), i+1 {
num := *(*int)(p)
fmt.Println(num)
}
}Slice
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryTypeฟังก์ชัน Slice รับพอยน์เตอร์ และการออฟเซ็ตความยาว มันจะส่งคืนรูปแบบสไลซ์ของหน่วยความจำนั้น ในกระบวนการจะไม่มีการคัดลอกหน่วยความจำ การแก้ไขสไลซ์จะส่งผลโดยตรงต่อข้อมูลบนที่อยู่นั้น ในทางกลับกัน也是如此 มักใช้ร่วมกับ SliceData
func main() {
nums := []int{1, 2, 3, 4}
numsRef1 := unsafe.Slice(unsafe.SliceData(nums), len(nums))
numsRef1[0] = 2
fmt.Println(nums)
}[2 2 3 4]การแก้ไขข้อมูลสไลซ์ numsRef1 จะทำให้ข้อมูล nums เปลี่ยนแปลงด้วย
StringData
func StringData(str string) *byteเหมือนกับฟังก์ชัน SliceData แต่เนื่องจากความต้องการแปลงสตริงเป็นสไลซ์ไบต์บ่อยครั้ง จึงแยกออกมาต่างหาก ตัวอย่างการใช้มีดังนี้
func main() {
str := "hello,world!"
for ptr, i := unsafe.Pointer(unsafe.StringData(str)), 0; i < len(str); ptr, i = unsafe.Add(ptr, unsafe.Sizeof(byte(0))), i+1 {
char := *(*byte)(ptr)
fmt.Println(string(char))
}
}เนื่องจากสตริง literal เก็บไว้ในส่วน只读ของ进程 หากคุณลองแก้ไขข้อมูลพื้นฐานของสตริงที่นี่ โปรแกรมจะพังและรายงาน fatal แต่สำหรับตัวแปรสตริงที่เก็บไว้ใน heap stack การแก้ไขข้อมูลพื้นฐานของมันใน runtime เป็นไปได้โดยสมบูรณ์
String
func String(ptr *byte, len IntegerType) stringเหมือนกับฟังก์ชัน Slice รับพอยน์เตอร์ประเภทไบต์ และการออฟเซ็ตความยาวของมัน ส่งคืนรูปแบบสตริงของมัน ในกระบวนการไม่มีการคัดลอกหน่วยความจำ ด้านล่างนี้เป็นตัวอย่างการแปลงสไลซ์ไบต์เป็นสตริง
func main() {
bytes := []byte("hello world")
str := unsafe.String(unsafe.SliceData(bytes), len(bytes))
fmt.Println(str)
}StringData และ String ไม่มีการคัดลอกหน่วยความจำในกระบวนการแปลงระหว่างสตริงกับสไลซ์ไบต์ ประสิทธิภาพดีกว่าการแปลงประเภทโดยตรง แต่ใช้ได้เฉพาะในกรณีอ่านอย่างเดียวเท่านั้น หากคุณ打算แก้ไขข้อมูล ก็最好อย่าใช้这个
