Skip to content

unsafe

เอกสารทางการ: unsafe package - unsafe - Go Packages

ไลบรารีมาตรฐาน unsafe เป็นไลบรารีที่ทางการจัดเตรียมสำหรับการเขียนโปรแกรมระดับต่ำ การดำเนินการที่แพ็กเกจนี้จัดเตรียมสามารถข้ามระบบประเภทของ Go เพื่ออ่านและเขียนหน่วยความจำโดยตรง แพ็กเกจนี้อาจไม่มีความสามารถในการพกพา และทางการระบุว่าแพ็กเกจนี้ไม่ได้รับการคุ้มครองจากเกณฑ์ความเข้ากันได้ Go 1 แม้จะเป็นเช่นนั้น unsafe ก็ยังถูกใช้ในโปรเจกต์จำนวนมาก รวมถึงไลบรารีมาตรฐานที่ทางการจัดเตรียมด้วย

TIP

เหตุผลที่ไม่สามารถพกพาได้คือผลลัพธ์ของการดำเนินการบางอย่างขึ้นอยู่กับระบบปฏิบัติการที่แตกต่างกัน ระบบที่แตกต่างกันอาจมีผลลัพธ์ที่แตกต่างกัน

ArbitraryType

go
type ArbitraryType int

Arbitrary สามารถแปลได้ว่าใดๆ ในที่นี้แสดงถึงประเภทใดๆ และไม่เท่ากับ any ที่จริงแล้วประเภทนี้ไม่ได้เป็นของแพ็กเกจ unsafe การปรากฏที่นี่เป็นเพียงเพื่อวัตถุประสงค์ของเอกสารเท่านั้น

IntegerType

go
type IntegerType int

IntegerType แสดงถึงประเภทจำนวนเต็มใดๆ ที่จริงแล้วประเภทนี้ไม่ได้เป็นของแพ็กเกจ unsafe การปรากฏที่นี่เป็นเพียงเพื่อวัตถุประสงค์ของเอกสารเท่านั้น

สองประเภทข้างต้นไม่จำเป็นต้องใส่ใจมากเกินไป พวกมันเป็นเพียงตัวแทนเท่านั้น เมื่อใช้ฟังก์ชันแพ็กเกจ unsafe ตัวแก้ไข甚至会提示คุณว่าประเภทไม่ตรงกัน ประเภทจริงของพวกมันคือประเภทเฉพาะที่คุณส่งเข้าไป

Sizeof

go
func Sizeof(x ArbitraryType) uintptr

ส่งคืนขนาดของตัวแปร x เป็นไบต์ ไม่รวมขนาดของเนื้อหาที่อ้างอิง เช่น:

go
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
16

Offsetof

go
func Offsetof(x ArbitraryType) uintptr

ฟังก์ชันนี้ใช้เพื่อแสดงการออฟเซ็ตฟิลด์ในโครงสร้าง ดังนั้น x ต้องเป็นฟิลด์ของโครงสร้าง หรือกล่าวได้ว่าค่าที่ส่งกลับคือจำนวนไบต์ระหว่างที่อยู่เริ่มต้นของโครงสร้างกับที่อยู่เริ่มต้นของฟิลด์ เช่น

go
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
8

Alignof

หากไม่เข้าใจว่า memory alignment คืออะไร สามารถไปที่: Go 语言内存对齐详解 - 掘金 (juejin.cn)

go
func Alignof(x ArbitraryType) uintptr

ขนาดการจัดเรียงมักจะเป็นค่าต่ำสุดระหว่างความยาวคำของคอมพิวเตอร์เป็นไบต์กับ Sizeof เช่น บนเครื่อง amd64 ความยาวคำคือ 64 บิต หรือ 8 ไบต์ เช่น:

go
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))
}
go
8 24
8 16
4 4

Pointer

go
type Pointer *ArbitraryType

Pointer เป็น "พอยน์เตอร์" ที่สามารถชี้ไปยังประเภทใดๆ ได้ ประเภทของมันคือ *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 ได้ เช่น:

go
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 หลังเป็นเพียงค่าจำนวนเต็ม เช่น

go
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 เก็บไว้ เช่น โค้ดด้านล่างอาจมีปัญหา:

go
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 สามารถ完成การคำนวณพอยน์เตอร์ได้

go
p = unsafe.Pointer(uintptr(p) + offset)

เช่นนี้ สามารถเข้าถึงองค์ประกอบภายในของบางประเภทผ่านพอยน์เตอร์เดียว เช่น อาร์เรย์และโครงสร้าง ไม่ว่าองค์ประกอบภายในจะเปิดเผยภายนอกหรือไม่ เช่น

go
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
2

Add

go
func Add(ptr Pointer, len IntegerType) Pointer

Add จะส่งคืน Pointer ที่อัปเดตด้วยการออฟเซ็ต len เทียบเท่ากับ Pointer(uintptr(ptr) + uintptr(len))

go
Pointer(uintptr(ptr) + uintptr(len))

เช่น:

go
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
3

SliceData

go
func SliceData(slice []ArbitraryType) *ArbitraryTyp

ฟังก์ชันนี้รับสไลซ์ ส่งคืนที่อยู่เริ่มต้นของอาร์เรย์พื้นฐานของมัน หากไม่ใช้ SliceData สามารถรับที่อยู่ของอาร์เรย์พื้นฐานได้ผ่านพอยน์เตอร์ขององค์ประกอบแรกเท่านั้น ดังนี้

go
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 มีดังนี้

go
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

go
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 แต่เนื่องจากความต้องการแปลงสตริงเป็นสไลซ์ไบต์บ่อยครั้ง จึงแยกออกมาต่างหาก ตัวอย่างการใช้มีดังนี้

go
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

go
func String(ptr *byte, len IntegerType) string

เหมือนกับฟังก์ชัน Slice รับพอยน์เตอร์ประเภทไบต์ และการออฟเซ็ตความยาวของมัน ส่งคืนรูปแบบสตริงของมัน ในกระบวนการไม่มีการคัดลอกหน่วยความจำ ด้านล่างนี้เป็นตัวอย่างการแปลงสไลซ์ไบต์เป็นสตริง

go
func main() {
  bytes := []byte("hello world")
  str := unsafe.String(unsafe.SliceData(bytes), len(bytes))
  fmt.Println(str)
}

StringData และ String ไม่มีการคัดลอกหน่วยความจำในกระบวนการแปลงระหว่างสตริงกับสไลซ์ไบต์ ประสิทธิภาพดีกว่าการแปลงประเภทโดยตรง แต่ใช้ได้เฉพาะในกรณีอ่านอย่างเดียวเท่านั้น หากคุณ打算แก้ไขข้อมูล ก็最好อย่าใช้这个

Golang by www.golangdev.cn edit