سلاسل Go النصية
في Go السلسلة النصية هي في الأساس سلسلة بايتات غير قابلة للتغيير وللقراءة فقط، و"سلسلة البايتات" هنا تعني أن البيانات الأساسية للسلسلة تتكون من مجموعة بايتات مرتبة تشغل مساحة ذاكرة متصلة.
القيم الحرفية
ذكرنا سابقاً أن هناك نوعين من القيم الحرفية للسلاسل: السلاسل العادية والسلاسل الخام.
السلاسل العادية
السلاسل العادية تُمثبعلامتي اقتباس مزدوجتين ""، تدعم الهروب، لا تدعم الكتابة متعددة الأسطر، فيما يلي بعض السلاسل العادية:
"هذه سلسلة عادية\n"
"abcdefghijlmn\nopqrst\t\\uvwxyz"هذه سلسلة عادية
abcdefghijlmn
opqrst \uvwxyzالسلاسل الخام
السلاسل الخام تُمثبعلامتي اقتباس عكسيين، لا تدعم الهروب، تدعم الكتابة متعددة الأسطر، جميع الأحرف في السلسلة الخام ستُخرج كما هي، بما في ذلك الأسطر الجديدة والمسافات البادئة:
`هذه سلسلة خام، سطر جديد
مسافة tab، \t رمز tab لكنه غير فعال، سطر جديد
"هذه سلسلة عادية"
النهاية
`هذه سلسلة خام، سطر جديد
مسافة tab، \t رمز tab لكنه غير فعال، سطر جديد
"هذه سلسلة عادية"
النهايةالوصول
لأن السلسلة في الأساس سلسلة بايتات، عملية الفهرسة str[i] صُممت لتُرجع البايت رقم i، نحوياً مطابقة للشرائح، مثلاً الوصول لأول عنصر في السلسلة:
func main() {
str := "this is a string"
fmt.Println(str[0])
}المخرجات هي قيمة ترميز البايت وليست الحرف:
116قص السلسلة:
func main() {
str := "this is a string"
fmt.Println(string(str[0:4]))
}thisمحاولة تعديل عناصر السلسلة:
func main() {
str := "this is a string"
str[0] = 'a' // لا يمكن ترجمته
fmt.Println(str)
}main.go:7:2: cannot assign to str[0] (value of type byte)رغم عدم إمكانية تعديل السلسلة، يمكن الكتابة فوقها:
func main() {
str := "this is a string"
str = "that is a string"
fmt.Println(str)
}that is a stringالتحويل
يمكن تحويل السلسلة لشريحة بايتات، ويمكن تحويل شريحة البايتات أو سلسلة البايتات لسلسلة، مثال:
func main() {
str := "this is a string"
// تحويل نوع صريح لشريحة بايتات
bytes := []byte(str)
fmt.Println(bytes)
// تحويل نوع صريح لسلسلة
fmt.Println(string(bytes))
}محتوى السلسلة للقراءة فقط وغير قابل للتغيير، لا يمكن تعديله، لكن شريحة البايتات يمكن تعديلها:
func main() {
str := "this is a string"
fmt.Println(&str)
bytes := []byte(str)
// تعديل شريحة البايتات
bytes = append(bytes, 96, 97, 98, 99)
// إسنادها للسلسلة الأصلية
str = string(bytes)
fmt.Println(str)
}عند تحويل السلسلة لشريحة بايتات، لا توجد أي صلة بينهما، لأن Go ستخصص مساحة ذاكرة جديدة لشريحة البايتات، ثم تنسخ ذاكرة السلسلة إليها، تعديل شريحة البايتات لن يؤثر على السلسلة الأصلية بأي شكل، وهذا للأمان الذاكري.
في هذه الحالة، إذا كانت السلسلة أو شريحة البايتات المراد تحويلها كبيرة جداً، فتكلفة الأداء ستكون عالية. لكن يمكنك استخدام مكتبة unsafe لتحويل بدون نسخ، لكن مسؤولية الأمان تقع عليك، مثلاً في المثال التالي، عنوانا b1 و s1 متطابقان:
func main() {
s1 := "hello world"
b1 := unsafe.Slice(unsafe.StringData(s1), len(s1))
fmt.Printf("%p %p", unsafe.StringData(s1), unsafe.SliceData(b1))
}0xe27bb2 0xe27bb2الطول
طول السلسلة ليس عدد الأحرف، بل طول سلسلة البايتات. فقط في معظم الأحيان نتعامل مع أحرف ASCII، كل حرف يمكن تمثيلهبايت واحد، لذا يتساوى طول البايتات مع عدد الأحرف. استخدام الدالة المدمجة len لحساب طول السلسلة، مثال:
func main() {
str := "this is a string" // يبدو طوله 16
str2 := "这是一个字符串" // يبدو طوله 7
fmt.Println(len(str), len(str2))
}16 21يبدو أن السلسلة الصينية أقصر من الإنجليزية، لكن الطول الفعلي أطول. هذا لأن في ترميز unicode، الحرف الصيني في معظم الحالات يشغل 3 بايت، والحرف الإنجليزي يشغل بايتاً واحداً فقط، من خلال إخراج أول عنصر في السلسلة يمكن رؤية النتيجة:
func main() {
str := "this is a string"
str2 := "这是一个字符串"
fmt.Println(string(str[0]))
fmt.Println(string(str2[0]))
fmt.Println(string(str2[0:3]))
}t // الحرف t
è // "شظية" حرف صيني (أول بايت)، قيمة ترميزه تتطابق مصادفة مع الحرف الإيطالي è
这 // الحرف الصينيالنسخ
طريقة نسخ مشابهة لنسخ شرائح المصفوفات، نسخ السلسلة هو في الواقع نسخ شريحة البايتات، استخدام الدالة المدمجة copy:
func main() {
var dst, src string
src = "this is a string"
desBytes := make([]byte, len(src))
copy(desBytes, src)
dst = string(desBytes)
fmt.Println(src, dst)
}يمكن أيضاً استخدام دالة strings.Clone، لكن التنفيذ الداخلي متشابه:
func main() {
var dst, src string
src = "this is a string"
dst = strings.Clone(src)
fmt.Println(src, dst)
}الدمج
دمج السلاسل يستخدم عامل +:
func main() {
str := "this is a string"
str = str + " that is a int"
fmt.Println(str)
}يمكن أيضاً التحويل لشريحة بايتات ثم إضافة عناصر:
func main() {
str := "this is a string"
bytes := []byte(str)
bytes = append(bytes, "that is a int"...)
str = string(bytes)
fmt.Println(str)
}كلتا طريقتي الدمج أعلاه أداؤهما ضعيف، يمكن استخدامهما في الحالات العادية، لكن إذا كانت هناك متطلبات أعلى للأداء، يمكن استخدام strings.Builder:
func main() {
builder := strings.Builder{}
builder.WriteString("this is a string ")
builder.WriteString("that is a int")
fmt.Println(builder.String())
}this is a string that is a intالاجتياح
ذكرنا في بداية هذه المقالة أن السلسلة في Go هي شريحة بايتات للقراءة فقط، أي أن وحدة تكوين السلسلة هي البايت وليس الحرف. هذه الحالة تظهر كثيراً عند اجتياح السلاسل، مثلاً الكود التالي:
func main() {
str := "hello world!"
for i := 0; i < len(str); i++ {
fmt.Printf("%d,%x,%s\n", str[i], str[i], string(str[i]))
}
}المثال يُخرج الشكل العشري والست عشري للبايتات:
104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
119,77,w
111,6f,o
114,72,r
108,6c,l
100,64,d
33,21,!بما أن أحرف المثال كلها من أحرف ASCII، يكفي بايت واحد لتمثيل كل منها، فتكون النتيجة أن كل بايت يقابل حرفاً. لكن إذا احتوت على أحرف غير ASCII فالنتيجة مختلفة، كالتالي:
func main() {
str := "hello 世界!"
for i := 0; i < len(str); i++ {
fmt.Printf("%d,%x,%s\n", str[i], str[i], string(str[i]))
}
}عادةً، الحرف الصيني يشغل 3 بايت، لذا قد ترى النتيجة التالية:
104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
228,e4,ä
184,b8,¸
150,96,
231,e7,ç
149,95,
140,8c,
33,21,!اجتياح السلسلة حسب البايت سيُفتت الأحرف الصينية، وهذا سيؤدي لظهور أحرف مشوهة بوضوح. سلاسل Go تدعم UTF-8 بشكل صريح، للتعامل مع هذه الحالة نحتاج استخدام نوع rune، عند استخدام for range للاجتياح، وحدة الاجتياح الافتراضية هي rune، مثلاً الكود التالي:
func main() {
str := "hello 世界!"
for _, r := range str {
fmt.Printf("%d,%x,%s\n", r, r, string(r))
}
}المخرجات:
104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
19990,4e16,世
30028,754c,界
33,21,!rune في الأساس اسم مستعار للنوع int32، نطاق مجموعة أحرف unicode يقع بين 0x0000 و 0x10FFFF، الحد الأقصى 3 بايت فقط، والحد الأقصى لعدد بايتات ترميز UTF-8 الصالح هو 4 بايت، لذا استخدام int32 للتخزين أمر طبيعي، في المثال أعلاه تحويل السلسلة لـ []rune ثم اجتياحها هو نفس المبدأ، كالتالي:
func main() {
str := "hello 世界!"
runes := []rune(str)
for i := 0; i < len(runes); i++ {
fmt.Println(string(runes[i]))
}
}يمكن أيضاً استخدام أدوات حزمة utf8، مثلاً:
func main() {
str := "hello 世界!"
for i, w := 0, 0; i < len(str); i += w {
r, width := utf8.DecodeRuneInString(str[i:])
fmt.Println(string(r))
w = width
}
}مخرجات المثالين السابقين متطابقة.
TIP
لمزيد من التفاصيل حول السلاسل النصية، يمكنك زيارة Strings, bytes, runes and characters in Go.
