البيانات
التخصيص بـ new
في Go عمليّتا تخصيص مدمجتان هما new وmake. وهما تفعلان أشياء مختلفة وتنطبقان على أنواع مختلفة، وقد يكون هذا مربكًا، لكن القواعد بسيطة. فلنتحدّث عن new أولًا. هي دالة مدمجة تخصّص ذاكرة، لكنها بخلاف نظائرها في بعض اللغات الأخرى لا تُهيّئ الذاكرة، بل تصفّرها فقط. أي أن new(T) تخصّص تخزينًا مصفّرًا لمتغيّر جديد من نوع T وتُرجع عنوانه، وهو قيمة من نوع *T. وبمصطلح Go، فإنها تُرجع مؤشرًا إلى قيمة صفرية مُخصَّصة حديثًا من نوع T.
اعتبارًا من Go 1.26 تقبل new أيضًا تعبيرًا (قيمة) كوسيط، يحدّد القيمة الابتدائية للمتغيّر. فمثلًا، new(int64(300)) تخصّص متغيّرًا جديدًا من نوع int64 مهيّأً على 300 وتُرجع عنوانه.
ولأن الذاكرة التي تُرجعها new مصفّرة، فمن المفيد — عند تصميم بنى بياناتك — أن ترتّب بحيث يمكن استعمال القيمة الصفرية لكل نوع من دون تهيئة إضافية. وهذا يعني أن من يستعمل بنية البيانات يستطيع إنشاؤها بـ new ثم ينتقل مباشرة إلى العمل. فمثلًا، ينصّ توثيق bytes.Buffer على أن «القيمة الصفرية لـ Buffer هي مخزن فارغ جاهز للاستعمال». وكمثال مماثل، لا تملك sync.Mutex طريقة Init ولا مُنشئًا صريحًا؛ إذ يُعرَّف أن القيمة الصفرية لـ sync.Mutex هي قفل غير مقفل.
وهذه الخاصية — أن القيمة الصفرية مفيدة — تنتقل بالضرورة (تنتقل بالمسار). وتأمل التصريح التالي:
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
قيم النوع SyncedBuffer تكون كذلك جاهزة للاستعمال فور تخصيصها أو بمجرّد التصريح عنها. وفي المقطع التالي، سيعمل p وv كلاهما بصورة صحيحة من دون ترتيب إضافي:
p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer
المُنشئات والقيم المركّبة
أحيانًا لا تكفي القيمة الصفرية، فيكون مُنشئ تهيئة ضروريًا، كما في هذا المثال المستمدّ من الحزمة os:
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := new(File)
f.fd = fd
f.name = name
f.dirinfo = nil
f.nepipe = 0
return f
}
الكثير من العمل التكراري هنا. يمكننا تبسيطه باستعمال قيمة مركّبة ، وهي تعبير ينشئ نسخة جديدة في كل مرة يُقوَّم فيها:
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0}
return &f
}
ولاحظ أنه، بخلاف C، لا مانع إطلاقًا من إرجاع عنوان متغيّر محلي؛ فالتخزين المرتبط بالمتغيّر يبقى حيًّا بعد عودة الدالة. بل إن أخذ عنوان قيمة مركّبة يخصّص نسخة جديدة في كل مرة يُقوَّم فيها، ومن ثمّ يمكننا جمع السطرين الأخيرين معًا:
return &File{fd, name, nil, 0}
وتُرتَّب حقول القيمة المركّبة بالترتيب ويجب أن تكون جميعها موجودة. لكن بتسمية العناصر صراحةً كأزواج field:value يمكن أن تظهر المهيّئات بأي ترتيب، ويُترك الناقص منها على قيمته الصفرية. فيمكننا القول:
return &File{fd: fd, name: name}
وكحالة حدّية، إذا لم تحتو قيمة مركّبة على أي حقول، فإنها تنشئ قيمة صفرية للنوع. وتعبيرا new(File) و&File{} متكافئان.
ويمكن أيضًا إنشاء قيم مركّبة للمصفوفات والشرائح والخرائط، وتكون تسميات الحقول فيها فهارس أو مفاتيح خريطة بحسب الحال. وفي هذه الأمثلة يعمل التهيئة مهما كانت قيم Enone وEio وEinval، ما دامت متمايزة:
a := [...]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
s := []string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
التخصيص بـ make
لنعد إلى التخصيص. الدالة المدمجة make(T, args) تخدم غرضًا مختلفًا عن new(T). فهي تنشئ الشرائح والخرائط والقنوات فقط، وتُرجع قيمة مهيّأة (لا مصفّرة) من نوع T (لا *T). والسبب في هذا التمييز أن هذه الأنواع الثلاثة تمثّل، في باطنها، مراجع إلى بنى بيانات يجب تهيئتها قبل الاستعمال. فالشريحة مثلًا واصف من ثلاثة عناصر: مؤشر إلى البيانات (داخل مصفوفة)، والطول، والسعة؛ وإلى أن تُهيّأ هذه العناصر تبقى الشريحة nil. أمّا بالنسبة للشرائح والخرائط والقنوات، فإن make تهيّئ بنية البيانات الداخلية وتُهيّئ القيمة للاستعمال. فمثلًا:
make([]int, 10, 100)
يخصّص مصفوفة من مئة عدد صحيح، ثم ينشئ بنية شريحة طولها 10 وسعتها 100 تشير إلى أول عشرة عناصر من المصفوفة. (وعند إنشاء شريحة يمكن حذف السعة؛ انظر قسم الشرائح لمزيد من المعلومات.) وفي المقابل، تُرجع new([]int) مؤشرًا إلى بنية شريحة مُخصَّصة حديثًا ومصفّرة، أي مؤشرًا إلى قيمة شريحة nil.
وتوضّح هذه الأمثلة الفرق بين new وmake:
var p *[]int = new([]int) // allocates slice structure; *p == nil; rarely useful
var v []int = make([]int, 100) // the slice v now refers to a new array of 100 ints
// Unnecessarily complex:
var p *[]int = new([]int)
*p = make([]int, 100, 100)
// Idiomatic:
v := make([]int, 100)
وتذكّر أن make تنطبق على الخرائط والشرائح والقنوات فقط، ولا تُرجع مؤشرًا. فإذا أردت مؤشرًا صريحًا، فخصّص بـ new أو خذ عنوان متغيّر صراحةً.
المصفوفات
المصفوفات مفيدة عند التخطيط لتخطيط الذاكرة التفصيلي، وقد تساعد أحيانًا على تجنّب التخصيص، لكنها فوق ذلك لبنة بناء للشرائح، وهي موضوع القسم التالي. ولتهيئة الأرض لهذا الموضوع، إليك بضع كلمات عن المصفوفات.
هناك فروق جوهرية بين طريقة عمل المصفوفات في Go وفي C. ففي Go:
-
المصفوفات قيم. فإسناد مصفوفة إلى أخرى ينسخ كل عناصرها.
-
وعلى وجه التحديد، إن مرّرت مصفوفة إلى دالة فستستقبل نسخة من المصفوفة، لا مؤشرًا إليها.
وحجم المصفوفة جزء من نوعها. فالنوعان [10]int و[20]int مختلفان.
وقد تكون خاصية القيمة مفيدة، لكنها مكلفة أيضًا؛ فإذا أردت سلوك C وكفاءته، يمكنك تمرير مؤشر إلى المصفوفة:
func Sum(a *[3]float64) (sum float64) {
for _, v := range *a {
sum += v
}
return
}
array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array) // Note the explicit address-of operator
لكن حتى هذا الأسلوب ليس من أعراف Go. استعمل الشرائح بدلًا منه.
الشرائح
تغلّف الشرائح المصفوفات لتقدّم واجهة أكثر عمومية وقوة وملاءمة لتسلسلات البيانات. فباستثناء العناصر ذات البُعد الصريح مثل مصفوفات التحويل (matrices)، فإن معظم البرمجة بالمصفوفات في Go تتم بالشرائح لا بالمصفوفات المجرّدة.
تحمل الشرائح مراجع إلى مصفوفة تقف خلفها، فإذا أسندت شريحة إلى أخرى فإن كلتيهما تشير إلى المصفوفة نفسها. فإذا أخذت دالة شريحة كوسيط، فإن ما تُحدثه فيها من تغيير على عناصر الشريحة سيكون مرئيًا لدى المنادي، على تشابه تمرير مؤشر إلى المصفوفة التي تقف خلفها. ومن ثمّ تستطيع دالة Read أن تقبل شريحة كوسيط بدل مؤشر وعدد؛ فالطول داخل الشريحة يحدّد أقصى مقدار بيانات يُقرأ. وإليك توقيع طريقة Read على النوع File في الحزمة os:
func (f *File) Read(buf []byte) (n int, err error)
وتُرجع الطريقة عدد البايتات المقروءة وقيمة خطأ إن وُجدت. ولقراءة أول 32 بايت في مخزن أكبر buf، شقّ المخزن (وهنا تُستعمل «الشق» كفعل):
n, err := f.Read(buf[0:32])
وهذا النوع من التشقيق شائع وفعّال. بل إننا لو تجاهلنا الكفاءة لحظة، لكان المقطع التالي يقرأ أيضًا أول 32 بايت من المخزن:
var n int
var err error
for i := 0; i < 32; i++ {
nbytes, e := f.Read(buf[i:i+1]) // Read one byte.
n += nbytes
if nbytes == 0 || e != nil {
err = e
break
}
}
يمكن تغيير طول الشريحة ما دام لا يزال ضمن حدود المصفوفة التي تقف خلفها؛ يكفي أن تسندها إلى شريحة من نفسها. أمّا سعة الشريحة، التي تُتاح بالدالة المدمجة cap، فتُخبرك بأقصى طول يمكن أن تبلغه الشريحة. وإليك دالة تُلحق بيانات بشريحة. فإذا تجاوزت البيانات السعة، تُعاد تخصيص الشريحة، وتُعاد الشريحة الناتجة. وتستعمل الدالة الحقيقة بأن len وcap قانونيتان عند تطبيقهما على الشريحة nil، وأنهما تُرجعان 0:
func Append(slice, data []byte) []byte {
l := len(slice)
if l + len(data) > cap(slice) { // reallocate
// Allocate double what's needed, for future growth.
newSlice := make([]byte, (l+len(data))*2)
// The copy function is predeclared and works for any slice type.
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:l+len(data)]
copy(slice[l:], data)
return slice
}
ولازم أن نُرجع الشريحة بعد ذلك، لأن Append تستطيع تعديل عناصر slice، لكن الشريحة نفسها — وهي بنية بيانات وقت التشغيل التي تحمل المؤشر والطول والسعة — تُمرَّر بالقيمة.
وفكرة إلحاق البيانات بشريحة مفيدة إلى حدّ بعيد لدرجة أن append المدمجة التقطتها. لكن لفهم تصميم تلك الدالة نحتاج إلى معلومة إضافية قليلًا، لذا سنعود إليها لاحقًا.
الشرائح ثنائية البُعد
مصفوفات Go وشرائحها أحادية البُعد. ولإنشاء ما يماثل مصفوفة أو شريحة ثنائية البُعد، فلا بدّ من تعريف مصفوفة من المصفوفات أو شريحة من الشرائح، هكذا:
type Transform [3][3]float64 // A 3x3 array, really an array of arrays.
type LinesOfText [][]byte // A slice of byte slices.
ولأن الشرائح متغيّرة الطول، فمن الممكن أن تكون كل شريحة داخلية بطول مختلف. وقد يكون ذلك موقفًا شائعًا، كما في مثال LinesOfText عندنا: فكل سطر له طول مستقل.
text := LinesOfText{
[]byte("Now is the time"),
[]byte("for all good gophers"),
[]byte("to bring some fun to the party."),
}
وأحيانًا لا بدّ من تخصيص شريحة ثنائية البُعد، وقد ينشأ هذا الموقف عند معالجة أسطر بكسلات ممسوحة ضوئيًا مثلًا. وهناك طريقتان لتحقيق ذلك. إحداهما تخصيص كل شريحة على حدة، والأخرى تخصيص مصفوفة واحدة وتوجيه الشرائح الفرعية إليها. أيّهما تستعمل يعتمد على تطبيقك. فإن كانت الشرائح قد تنمو أو تنكمش، فينبغي تخصيصها على حدة تفاديًا لمحو السطر التالي؛ وإن لم تكن كذلك، فقد يكون بناء الكائن بتخصيص واحد أكثر كفاءة. وللإرشاد، إليك مخطّط الطريقتين. أولًا، سطرًا سطرًا:
// Allocate the top-level slice.
picture := make([][]uint8, YSize) // One row per unit of y.
// Loop over the rows, allocating the slice for each row.
for i := range picture {
picture[i] = make([]uint8, XSize)
}
والآن كخصيص واحد، مشقوقًا إلى أسطر:
// Allocate the top-level slice, the same as before.
picture := make([][]uint8, YSize) // One row per unit of y.
// Allocate one large slice to hold all the pixels.
pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8.
// Loop over the rows, slicing each row from the front of the remaining pixels slice.
for i := range picture {
picture[i], pixels = pixels[:XSize], pixels[XSize:]
}
الخرائط
الخرائط بنية بيانات مدمجة مريحة وقوية تربط قيمًا من نوع (المفتاح) بقيم من نوع آخر (العنصر أو القيمة). ويمكن أن يكون المفتاح من أي نوع يُعرَّف له معامل المساواة، مثل الأعداد الصحيحة، والأعداد العشرية والمركّبة، والنصوص، والمؤشرات، والواجهات (ما دام النوع الديناميكي يدعم المساواة)، والبنيات، والمصفوفات. ولا يمكن استعمال الشرائح كمفاتيح خريطة، لأن المساواة غير معرَّفة عليها. وكالشرائح، تحمل الخرائط مراجع إلى بنية بيانات تقف خلفها. فإذا مرّرت خريطة إلى دالة تُغيّر محتواها، فإن التغييرات ستكون مرئية لدى المنادي.
ويمكن بناء الخرائط بالصيغة المعتادة للقيمة المركّبة، بأزواج مفتاح وقيمة مفصولة بنقطتين،فمن السهل بناؤها أثناء التهيئة:
var timeZone = map[string]int{
"UTC": 0*60*60,
"EST": -5*60*60,
"CST": -6*60*60,
"MST": -7*60*60,
"PST": -8*60*60,
}
وإسناد قيم الخريطة وجلبها يبدو نحويًا كتمامه تمامًا مع المصفوفات والشرائح، إلا أن الفهرس لا يلزم أن يكون عددًا صحيحًا:
offset := timeZone["EST"]
ومحاولة جلب قيمة من الخريطة بمفتاح غير موجود فيها تُرجع القيمة الصفرية لنوع مدخلات الخريطة. فمثلًا، إذا كانت الخريطة تحتوي أعدادًا صحيحة، فإن البحث عن مفتاح غير موجود سيُرجع 0. ويمكن تنفيذ مجموعة (set) في صورة خريطة من نوع قيمة bool: ضع مدخل الخريطة على true لإدراج القيمة في المجموعة، ثم اختبرها بفهرسة بسيطة:
attended := map[string]bool{
"Ann": true,
"Joe": true,
...
}
if attended[person] { // will be false if person is not in the map
fmt.Println(person, "was at the meeting")
}
وأحيانًا تحتاج أن تميّز بين مدخل غائب وقيمة صفرية. فهل يوجد مدخل لـ "UTC" أم أن هذا الصفر لأن المفتاح غير موجود أصلًا في الخريطة؟ يمكنك التمييز بصورة من صور الإسناد المتعدّد:
var seconds int
var ok bool
seconds, ok = timeZone[tz]
ولأسباب بديهية يُسمّى هذا «عرف الفاصلة-والـ ok». ففي هذا المثال، إن كان tz موجودًا، فإن seconds سيُضبط بصورة صحيحة ويكون ok صحيحًا؛ وإن لم يكن موجودًا، فإن seconds سيُضبط على صفر ويكون ok خاطئًا. وإليك دالة تجمع ذلك مع تقرير خطأ لائق:
func offset(tz string) int {
if seconds, ok := timeZone[tz]; ok {
return seconds
}
log.Println("unknown time zone:", tz)
return 0
}
ولاختبار وجود المفتاح في الخريطة دون عناية بالقيمة الفعلية، يمكنك استعمال المُعرِّف الفارغ (_) مكان المتغيّر المعتاد للقيمة:
_, present := timeZone[tz]
ولحذف مدخل من خريطة، استعمل الدالة المدمجة delete، ووسائطها هي الخريطة والمفتاح المراد حذفه. والأمر آمن حتى لو كان المفتاح غائبًا من الخريطة أصلًا:
delete(timeZone, "PDT") // Now on Standard Time
الطباعة
الطباعة المنسّقة في Go تستعمل أسلوبًا شبيهًا بعائلة printf في C، لكنه أغنى وأعمّ. وتقع هذه الدوال في الحزمة fmt ولأسمائها حروف كبيرة: fmt.Printf وfmt.Fprintf وfmt.Sprintf وما شابه. أما دوال النصوص (Sprintf وغيرها) فتُرجع نصًّا بدل ملء مخزن مقدَّم.
ولا يلزمك أن تقدّم نصّ تنسيق. فلكل من Printf وFprintf وSprintf زوجٌ آخر من الدوال، مثل Print وPrintln. وهذه الدوال لا تأخذ نصّ تنسيق، بل تولّد تنسيقًا افتراضيًا لكل وسيط. ونسخ Println تُدرج أيضًا مسافة بين الوسائط وتُلحق سطرًا جديدًا بالمخرجات، بينما لا تضيف نسخ Print مسافات إلا إذا لم يكن أيٌّ من المعاملين على جانبيها نصًّا. وفي هذا المثال يُنتج كل سطر المخرجات نفسها:
fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))
وتأخذ دوال fmt.Fprint المطبوعة وأخواتها كأول وسيط أي كائن ينفّذ واجهة io.Writer؛ والمتغيّران os.Stdout وos.Stderr مثالان مألوفان.
هنا تبدأ الأمور في الانفصال عن C. فأولًا، تنسيقات الأعداد مثل %d لا تأخذ رايات عن الإشارة أو الحجم؛ بل تستعمل دوال الطباعة نوع الوسيط لتقرّر هذه الخصائص:
var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))
يُطبع
18446744073709551615 ffffffffffffffff; -1 -1
وإن كنت تريد التحويل الافتراضي فحسب، مثل النظام العشري للأعداد الصحيحة، فيمكنك استعمال تنسيق الالتقاط %v (أي «value»)، والنتيجة هي ما تنتجه Print وPrintln بالضبط. وفضلًا عن ذلك، يستطيع هذا التنسيق طباعة أي قيمة، حتى المصفوفات والشرائح والبنيات والخرائط. وإليك جملة طباعة لخريطة المناطق الزمنية المعرّفة في القسم السابق:
fmt.Printf("%v\n", timeZone) // or just fmt.Println(timeZone)
فتُنتج المخرجات:
map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0]
أمّا في الخرائط، فإن Printf وأخواتها ترتّب المخرجات معجميًا حسب المفتاح.
وعند طباعة بنية، فإن التنسيق المعدّل %+v يُضيف أسماء الحقول إلى بنية الـ struct، وأي قيمة تُطبع بالتنسيق البديل %#v تُطبع بصيغة Go الكاملة:
type T struct {
a int
b float64
c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", timeZone)
يُطبع
&{7 -2.35 abc def}
&{a:7 b:-2.35 c:abc def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string]int{"CST":-21600, "EST":-18000, "MST":-25200, "PST":-28800, "UTC":0}
(لاحظ علامتَي &.)
وتنسيق النص المقتبس هذا متاح أيضًا عبر %q عند تطبيقه على قيمة من نوع string أو []byte. أما التنسيق البديل %#q فيستعمل علامتَي backquote إن أمكن. (وينطبق تنسيق %q أيضًا على الأعداد الصحيحة وrune، فيُنتج ثابت rune محاطًا بعلامة اقتباس مفردة.) كما يعمل %x على النصوص ومصفوفات البايت وشرائح البايت، إضافةً إلى الأعداد الصحيحة، فيُنتج نصًا سداسيًا طويلًا؛ ومع مسافة في التنسيق (% x) يضع مسافات بين البايتات.
وتنسيق مفيد آخر هو %T، الذي يطبع نوع القيمة:
fmt.Printf("%T\n", timeZone)
يُطبع
map[string]int
وإن أردت التحكّم في التنسيق الافتراضي لنوع مخصّص، فكل ما يلزم هو تعريف طريقة بتوقيع String() string على النوع. وبالنسبة إلى نوعنا البسيط T، قد يبدو هكذا:
func (t *T) String() string {
return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
fmt.Printf("%v\n", t)
فيطبع بالتنسيق
7/-2.35/"abc\tdef"
(وإن احتجت طباعة قيم من النوع T وكذلك مؤشرات إليه، فيجب أن يكون مُستقبِل String من نوع قيمة؛ استعمل هذا المثال مؤشرًا لأن ذلك أكثر كفاءة وأقرب لأعراف Go مع أنواع الـ struct. انظر القسم التالي عن المؤشرات مقابل مستقبِلات القيم.)
وتستطيع دالتنا String أن تنادي Sprintf لأن دوال الطباعة قابلة لإعادة الدخول تمامًا ويمكن تغليفها على هذا النحو. غير أن هناك تفصيلًا مهمًا يجب فهمه عن هذا الأسلوب: لا تبنِ دالة String بنداء Sprintf بطريقة تعود إلى دالتك String إلى ما لا نهاية. وقد يحدث هذا إذا حاول نداء Sprintf طباعة المُستقبِل مباشرةً كنص، فيستدعي الدالة من جديد. وهو خطأ شائع وسهل الوقوع، كما يبيّن هذا المثال:
type MyString string
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", m) // Error: will recur forever.
}
وهو خطأ سهل الإصلاح أيضًا: حوّل الوسيط إلى نوع النص الأساسي، وهو نوع لا يملك الدالة:
type MyString string
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion.
}
وفي قسم التهيئة سنرى تقنية أخرى تتجنّب هذا العود.
وتقنية طباعة أخرى هي تمرير وسائط دالة طباعة مباشرةً إلى دالة طباعة مماثلة. فتوقيع Printf يستعمل النوع ...interface{} لوسيطه الأخير ليبيّن أن عددًا اعتباطيًا من الوسائط (من أنواع اعتباطية) يمكن أن يأتي بعد نصّ التنسيق:
func Printf(format string, v ...interface{}) (n int, err error) {
وداخل الدالة Printf، يتصرّف v كمتغيّر من نوع []interface{}، لكن إن مُرّر إلى دالة أخرى تقبل وسائط اعتباطية، تصرّف كقائمة وسائط عادية. وإليك تنفيذ الدالة log.Println التي استعملناها أعلاه. فهي تمرّر وسائطها مباشرةً إلى fmt.Sprintln لإجراء التنسيق الفعلي:
// Println prints to the standard logger in the manner of fmt.Println.
func Println(v ...interface{}) {
std.Output(2, fmt.Sprintln(v...)) // Output takes parameters (int, string)
}
ونكتب ... بعد v في النداء المتداخل إلى Sprintln لنقول للمُصرِّف إنما عامِل v كقائمة وسائط؛ وإلا لتمرّر v كوسيط شريحة واحد.
وهناك المزيد مما لم نغطّه هنا عن الطباعة. راجع توثيق godoc للحزمة fmt للتفاصيل.
وبالمناسبة، يمكن أن يكون المعامل ... من نوع محدّد، مثل ...int في دالة min تختار الأصغر من قائمة أعداد صحيحة:
func Min(a ...int) int {
min := int(^uint(0) >> 1) // largest int
for _, i := range a {
if i < min {
min = i
}
}
return min
}
الإلحاق بـ append
لدينا الآن القطعة الناقصة التي احتجناها لشرح تصميم الدالة المدمجة append. وتوقيع append مختلف عن دالتنا Append المخصّصة أعلاه. ومخطّطه كالتالي:
func append(slice []T, elements ...T) []T
حيث T بديل لأي نوع معطى. ولا يمكنك فعليًا كتابة دالة في Go يكون فيها النوع T محدَّدًا من المنادي. ومن هنا كانت append مدمجة: فهي تحتاج دعمًا من المُصرِّف.
وتفعل append أن تُلحق العناصر بنهاية الشريحة وتُرجع النتيجة. ولازم إرجاع النتيجة، لأن المصفوفة التي تقف خلفها قد تتغيّر، كما في Append التي كتبناها بأيدينا. فالمثال البسيط هذا
x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)
يطبع [1 2 3 4 5 6]. فـ append تعمل بعض الشيء مثل Printf: تجمع عددًا اعتباطيًا من الوسائط.
لكن ماذا لو أردنا أن نفعل ما تفعله Append وأن نلحق شريحة بشريحة؟ الأمر سهل: استعمل ... عند موقع النداء، تمامًا كما فعلنا في نداء Output أعلاه. وينتج هذا المقطع مخرجات مطابقة لما سبق:
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)
ولولا هذا ... لما كانت الشيفرة لتُصرَّف أصلًا، لأن الأنواع ستكون خاطئة؛ فـ y ليست من نوع int.