أسئلة وأجوبة

كيف تأخذ عناوين البايتات في نص؟

نعلم جميعاً أن بايتات النص غير قابلة لأخذ العنوان، لذا ستُبلّغ مصرِّفات Go عن التعبيرات المشابهة لـ &aString[i].

استخدام unsafe.StringData

قبل Go 1.22 (ومنذ 1.20)، كانت الطريقة الوحيدة لأخذ عناوين بايتات النص هي عبر الطريقة unsafe:


package main

import (
	"unsafe"
)

var h = "Hello "
var w = "world!"

func commonUnderlyingBytes(x, y string) bool {
	return &[]byte(x)[4] == &[]byte(y)[4]
}

func main() {
	var s = h + w
	//var p = &s[4] // خطأ: لا يمكن أخذ عنوان s[4]
	var p = unsafe.StringData(s[4:])
	println(p, string(*p)) // 0xc00004470c o
}

استخدام reflect.Value.UnsafePointer

منذ Go 1.23، أصبحت طريقة reflect.Value.UnsafePointer تدعم قيم النصوص. لذا يمكننا استخدامها للحصول على عناوين بايتات النص.


package main

import (
	"reflect"
)

var h = "Hello "
var w = "world!"

func commonUnderlyingBytes(x, y string) bool {
	return &[]byte(x)[4] == &[]byte(y)[4]
}

func main() {
	var s = h + w
	//var p = &s[4] // خطأ: لا يمكن أخذ عنوان s[4]
	var p = (*byte)(reflect.ValueOf(s[4:]).UnsafePointer())
	println(p, string(*p)) // 0xc000104eec o
}

تتضمن الطريقتان أعلاه كليهما unsafe.Pointer. أما الطريقة التالية فلا.

حوّل النص إلى شريحة بايتات، ثم خذ عناوين شريحة البايتات

منذ سلسلة أدوات Go 1.22، أُجري تحسين بحيث لم تعد هناك حاجة إلى تخصيص ذاكرة وتكرار بايتات في بعض تحويلات []byte(aString) (إذا ثبت أن البايتات في نتيجة التحويل لن تُعدَّل أبداً).

يمكننا الاستفادة من هذا التحسين لأخذ عناوين بايتات نص، عبر تحويل النص إلى شريحة بايتات ثم أخذ عناوين البايتات في شريحة البايتات بشكل غير مباشر. إليك مثالاً يوضّح الاستخدام:


package main

var h = "Hello "
var w = "world!"

func takeAddressOfFirstByte(s string) *byte {
	return &[]byte(s)[0]
}

func commonUnderlyingBytes(x, y string) bool {
	return len(x) != 0 && len(y) != 0 &&
		takeAddressOfFirstByte(x) == takeAddressOfFirstByte(y)
}

func main() {
	var s = h + w

	var p1 = &[]byte(s)[4]
	println(p1, string(*p1))

	var p2 = &[]byte(s)[4]
	println(p2, string(*p2))

	println(p1 == p2)

	var s2 = s
	println(commonUnderlyingBytes(s[4:], s2[4:]))
}

شغّله مع إصداري سلسلة أدوات Go 1.21 و 1.22: [1]


$ gotv 1.21. run aa.go
[Run]: $HOME/.cache/gotv/tag_go1.21.12/bin/go run demo1.go
0xc00003c6c4 o
0xc00003c6a4 o
false
false
$ gotv 1.22. run aa.go
[Run]: $HOME/.cache/gotv/tag_go1.22.5/bin/go run demo1.go
0xc0000426ec o
0xc0000426ec o
true
true

انتظر! هل يعني هذا أنه يمكننا تعديل محتوى النص الآن؟ لا تقلق. الجواب قطعاً لا. يستطيع المصرِّف اكتشاف مثل هذه المحاولات بنجاح وتعطيل التحسين للتحويلات المعنية، كما توضّح الشيفرة التالية:


package main

var h = "Hello "
var w = "world!"

func main() {
	var s = h + w

	var p1 = &[]byte(s)[4]
	println(p1, string(*p1))

	var p2 = &[]byte(s)[4]
	*p2 = 'X' // عدّل البايت
	println(p2, string(*p2))

	println(p1 == p2)
}

السلوكيات متطابقة بين إصداري سلسلة أدوات Go v1.21 و v1.22. [1]


$ gotv 1.21. run aa.go
[Run]: $HOME/.cache/gotv/tag_go1.21.12/bin/go run demo2.go
0xc00003c6d4 o
0xc00003c6b4 X
false
$ gotv 1.22. run aa.go
[Run]: $HOME/.cache/gotv/tag_go1.22.5/bin/go run demo2.go
0xc0000426f0 o
0xc0000426cc X
false

لذا لكي تعمل هذه الطريقة، ينبغي ألا نغيّر أبداً البايتات في شريحة البايتات الناتجة، ولا حتى احتمالياً.

هل هذه الحيلة مفيدة عملياً؟ أعتقد أن الجواب نعم. إذا كان أحد النصين مشتقاً من الآخر (عبر عملية شريحة فرعية)، فإن دالة CommonStringPrefix1 التالية أكثر أداءً من دالة CommonStringPrefix2 للحصول على البادئة المشتركة بينهما (نحن لا نعلم بوجود علاقة اشتقاق بين النصين).


func CommonStringPrefix1(x, y string) int {
	if len(x) == 0 || len(y) == 0 {
		return 0
	}
	if &[]byte(x)[0] == &[]byte(y)[0] {
		if len(x) <= len(y) {
			return len(x)
		} else {
			return len(y)
		}
	}
	return CommonStringPrefix2(x, y)
}

func CommonStringPrefix2(x, y string) int {
	var min = len(x)
	if len(y) < len(x) {
		min = len(y)
	}
	x2, y2 := x[:min], y[:min]
	if len(x2) == len(y2) {
		for i := 0; i < len(x2); i++ {
			if x2[i] != y2[i] {
				return i
			}
		}
	}
	return min
}
  1. GoTV (https://go101.org/apps-and-libs/gotv.html) أداة تُستخدم لتثبيت تثبيتات متعايشة من إصدارات سلسلة أدوات Go واستخدامها معاً بانسجام. ↩︎ ↩︎