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

إذا كنت لا تفهم ما هو محاذاة الذاكرة، يمكنك زيارة: شرح مفصل لمحاذاة الذاكرة في لغة 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 لا يمكن إلغاء الإشارة إليه، وكذلك لا يمكن أخذ عنوانه.

(1) تحويل *T1 إلى unsafe.Pointer ثم إلى *T2

لدينا النوعان *T1، *T2، لنفترض أن T2 ليس أكبر من T1 والتخطيطان الذاكريان متكافئان، يُسمح بتحويل بيانات من نوع 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 يُخزن العنوان، والفرق أن الأول نحوياً مؤشر، وهو مرجع، والأخر مجرد قيمة عددية صحيحة. مثلاً

go
func main() {
   num := 1
   fmt.Println(unsafe.Pointer(&num))
   fmt.Printf("0x%x", uintptr(unsafe.Pointer(&num)))
}
0xc00001c088
0xc00001c088

الفرق الأكبر في معالجة جمع المهملات، بما أن unsafe.Pointer مرجع، فلن يُجمع عند الحاجة، بينما الأخير مجرد قيمة، بطبيعة الحال لن يحصل على هذه المعاملة الخاصة، ونقطة أخرى يجب الانتباه لها هي عندما يتحرك العنوان الذي يشير إليه المؤشر، ستقوم GC بتحديث العنوان القديم الذي يشير إليه المرجع، لكن لن تُحدث القيمة المُخزنة في uinptr. مثلاً الكود التالي قد يُسبب مشاكل:

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))
  }
}

بما أن القيم الحرفية للسلاسل مُخزنة في القسم للقراءة فقط في العملية، فإذا حاولت هنا تعديل البيانات الأساسية للسلسلة، سيتعطل البرنامج ويُبلغ عن fatal. لكن بالنسبة لمتغيرات السلاسل المُخزنة على الكومة أو المكدس، فتعديل بياناتها الأساسية أثناء التشغيل ممكن تماماً.

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 تم تحريره بواسطة www.golangdev.cn