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
إذا كنت لا تفهم ما هو محاذاة الذاكرة، يمكنك زيارة: شرح مفصل لمحاذاة الذاكرة في لغة 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.Pointerunsafe.Pointerيمكن تحويله إلىuintptr
هذه العمليات الخاصة الأربع تشكل حجر الأساس لحزمة unsafe بأكملها، وهي تمكّن من كتابة كود يمكنه تجاهل نظام الأنواع للقراءة والكتابة المباشرة للذاكرة، ويُنصح بالحذر الشديد عند الاستخدام.
TIP
unsafe.Pointer لا يمكن إلغاء الإشارة إليه، وكذلك لا يمكن أخذ عنوانه.
(1) تحويل *T1 إلى unsafe.Pointer ثم إلى *T2
لدينا النوعان *T1، *T2، لنفترض أن T2 ليس أكبر من T1 والتخطيطان الذاكريان متكافئان، يُسمح بتحويل بيانات من نوع 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 يُخزن العنوان، والفرق أن الأول نحوياً مؤشر، وهو مرجع، والأخر مجرد قيمة عددية صحيحة. مثلاً
func main() {
num := 1
fmt.Println(unsafe.Pointer(&num))
fmt.Printf("0x%x", uintptr(unsafe.Pointer(&num)))
}0xc00001c088
0xc00001c088الفرق الأكبر في معالجة جمع المهملات، بما أن unsafe.Pointer مرجع، فلن يُجمع عند الحاجة، بينما الأخير مجرد قيمة، بطبيعة الحال لن يحصل على هذه المعاملة الخاصة، ونقطة أخرى يجب الانتباه لها هي عندما يتحرك العنوان الذي يشير إليه المؤشر، ستقوم GC بتحديث العنوان القديم الذي يشير إليه المرجع، لكن لن تُحدث القيمة المُخزنة في uinptr. مثلاً الكود التالي قد يُسبب مشاكل:
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))
}
}بما أن القيم الحرفية للسلاسل مُخزنة في القسم للقراءة فقط في العملية، فإذا حاولت هنا تعديل البيانات الأساسية للسلسلة، سيتعطل البرنامج ويُبلغ عن fatal. لكن بالنسبة لمتغيرات السلاسل المُخزنة على الكومة أو المكدس، فتعديل بياناتها الأساسية أثناء التشغيل ممكن تماماً.
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 في عملية التحويل بين السلاسل وشرائح البايت لا تتضمن نسخ ذاكرة، وأداؤها أفضل من تحويل النوع المباشر، لكنهما مناسبان فقط لحالة القراءة فقط، إذا كنت تنوي تعديل البيانات، فمن الأفضل عدم استخدامهما.
