آرایه‌ها در زبان Go

1.1 تعریف و اعلان آرایه‌ها

یک آرایه یک دنباله‌ی اجزا با اندازه‌ی ثابت و نوع یکسان است. در زبان Go، طول یک آرایه به عنوان بخشی از نوع آرایه در نظر گرفته می‌شود. این بدان معناست که آرایه‌های با طول‌های مختلف به عنوان انواع مختلف در نظر گرفته می‌شوند.

دستور اصلی برای اعلان یک آرایه به صورت زیر است:

var arr [n]T

در اینجا، var کلمه‌ی کلیدی برای اعلان متغیر است، arr نام آرایه، n طول آرایه و T نوع اجزا در آرایه را نمایش می‌دهد.

به عنوان مثال، برای اعلان یک آرایه شامل 5 اعداد صحیح:

var myArray [5]int

در این مثال، myArray یک آرایه است که می‌تواند 5 عدد صحیح از نوع int را شامل شود.

1.2 مقدمه و استفاده از آرایه‌ها

مقدمه آرایه‌ها می‌تواند مستقیماً در زمان اعلان یا با اختصاص مقادیر با استفاده از اندیس‌ها انجام شود. چند روش برای مقدمه‌سازی آرایه وجود دارد:

مقدمه مستقیم

var myArray = [5]int{10, 20, 30, 40, 50}

همچنین ممکن است که به کامپایلر اجازه دهید تا طول آرایه را بر اساس تعداد ارزش‌های مقدمه‌سازی شده محاسبه کند:

var myArray = [...]int{10, 20, 30, 40, 50}

در اینجا، ... نشان می‌دهد که طول آرایه توسط کامپایلر محاسبه می‌شود.

مقدمه با استفاده از اندیس‌ها

var myArray [5]int
myArray[0] = 10
myArray[1] = 20
// سایر اجزا به مقدار 0 مقدمه‌سازی می‌شوند، زیرا مقدار صفر از نوع int برابر 0 است

استفاده از آرایه‌ها نیز ساده است و اجزا با استفاده از اندیس‌ها دسترسی پیدا می‌کنند:

fmt.Println(myArray[2]) // دسترسی به عنصر سوم

1.3 گذر از آرایه‌ها

دو روش معمول برای گذر از آرایه استفاده از حلقه for سنتی و استفاده از range هستند.

گذر از آرایه با استفاده از حلقه for

for i := 0; i < len(myArray); i++ {
    fmt.Println(myArray[i])
}

گذر از آرایه با استفاده از range

for index, value := range myArray {
    fmt.Printf("اندیس: %d، مقدار: %d\n", index, value)
}

مزیت استفاده از range این است که دو مقدار را برمی‌گرداند: موقعیت اندیس فعلی و مقدار در آن موقعیت.

1.4 ویژگی‌ها و محدودیت‌های آرایه‌ها

در زبان Go، آرایه‌ها از نوع مقادیر هستند، به این معنا که هنگامی که یک آرایه به عنوان پارامتر به یک تابع منتقل می‌شود، یک کپی از آرایه منتقل می‌شود. بنابراین، اگر تغییراتی در آرایه اصلی در یک تابع نیاز باشد، از برش‌ها یا اشاره‌گرها به آرایه‌ها به‌طور معمول استفاده می‌شود.

2. برش‌ها در زبان Go

2.1 مفهوم برش‌ها

در زبان Go، یک برش یک انتزاع بر روی یک آرایه است. اندازه یک آرایه در Go ثابت است که استفاده آن را در برخی از حالت‌ها محدود می‌کند. برش‌ها در Go طراحی شده‌اند تا انعطاف‌پذیرتر باشند و یک رابط منعطف، ساده و قدرتمند برای ساختارهای داده ارائه دهند. برش‌ها درواقع داده‌ها را نگه‌نمی‌دارند؛ آن‌ها فقط ارجاع‌هایی به آرایه اصلی هستند. طبیعت پویای آن‌ها به طور اصلی توسط نکات زیر مشخص می‌شود:

  • اندازه پویا: بر خلاف آرایه‌ها، طول یک برش پویا است و به‌طور خودکار به صورت نیازمند بزرگتر یا کوچک‌تر می‌شود.
  • انعطاف: عناصر می‌توانند با استفاده از تابع append به راحتی به یک برش اضافه شوند.
  • نوع انتزاعی: برش‌ها با دسترسی به عناصر در آرایه اصلی با استفاده از ارجاع، بدون ایجاد کپی از داده عمل می‌کنند.

2.2 اعلان و مقدمه برش‌ها

ساختار اعلان یک برش مشابه اعلان یک آرایه است، اما هنگام اعلان نیازی به مشخص کردن تعداد اجزا نیست. به عنوان مثال، روش اعلان یک برش از اعداد صحیح به صورت زیر است:

var slice []int

می‌توانید با استفاده از یک لیترال برش یک برش را مقدمه‌سازی کنید:

slice := []int{1, 2, 3}

می‌توانید با استفاده از تابع make یک برش را مقدمه‌سازی کنید که به شما امکان می‌دهد طول و ظرفیت برش را مشخص کنید:

slice := make([]int, 5)  // ایجاد یک برش از اعداد صحیح با طول و ظرفیت 5

اگر ظرفیت بزرگ‌تری نیاز باشد، می‌توانید ظرفیت را به عنوان سومین پارامتر به تابع make منتقل کنید:

slice := make([]int, 5, 10)  // ایجاد یک برش از اعداد صحیح با طول 5 و ظرفیت 10

2.3 رابطه بین اسلایس‌ها و آرایه‌ها

اسلایس‌ها می‌توانند با تعیین یک بخش از یک آرایه، مراجعه‌گر به آن بخش را تشکیل دهند. به عنوان مثال، با در اختیار داشتن آرایه زیر:

array := [5]int{10, 20, 30, 40, 50}

می‌توانیم یک اسلایس به این صورت ایجاد کنیم:

slice := array[1:4]

این اسلایس slice به المان‌های آرایه array از اندیس 1 تا اندیس 3 (شامل اندیس 1، اما مقدار اندیس 4 را مستثنی می‌کند) مراجعه می‌کند.

مهم است که توجه کنیم اسلایس در واقع مقادیر آرایه را کپی نمی‌کند؛ بلکه تنها به یک بخش پیوسته از آرایه اصلی اشاره می‌کند. بنابراین، تغییرات در اسلایس نیز بر آرایه مبنا تأثیر می‌گذارد و بالعکس. درک این رابطه مراجعه برای استفاده کارآمد از اسلایس‌ها حیاتی است.

2.4 عملیات پایه بر روی اسلایس‌ها

2.4.1 اندیس‌گذاری

اسلایس‌ها برای دسترسی به عناصر خود از اندیس‌گذاری استفاده می‌کنند، مشابه آرایه‌ها با اندیس‌گذاری که از صفر آغاز می‌شود. به عنوان مثال:

slice := []int{10, 20, 30, 40}
// دسترسی به عناصر اول و سوم
fmt.Println(slice[0], slice[2])

2.4.2 طول و ظرفیت

اسلایس‌ها دارای دو ویژگی هستند: طول (len) و ظرفیت (cap). طول تعداد عناصر موجود در اسلایس و ظرفیت تعداد عناصر از اولین عنصر اسلایس تا انتهای آرایه مبنا است.

slice := []int{10, 20, 30, 40}
// چاپ طول و ظرفیت اسلایس
fmt.Println(len(slice), cap(slice))

2.4.3 اضافه کردن عناصر

تابع append برای اضافه کردن عناصر به یک اسلایس استفاده می‌شود. هنگامی که ظرفیت اسلایس کافی برای جا دادن عناصر جدید نباشد، تابع append به طور خودکار ظرفیت اسلایس را گسترش می‌دهد.

slice := []int{10, 20, 30}
// اضافه کردن یک عنصر
slice = append(slice, 40)
// اضافه کردن چندین عنصر
slice = append(slice, 50, 60)
fmt.Println(slice)

مهم است که توجه کنیم در هنگام استفاده از append برای اضافه کردن عناصر، ممکن است یک اسلایس جدید بازگردانده شود. اگر ظرفیت آرایه مبنا کافی نباشد، عملیات append منجر به این می‌شود که اسلایس به یک آرایه جدید و بزرگ‌تر اشاره کند.

2.5 گسترش و کپی کردن اسلایس‌ها

تابع copy برای کپی کردن عناصر یک اسلایس به اسلایس دیگر استفاده می‌شود. اسلایس مقصد باید از پیش فضای کافی برای جا دادن عناصر کپی شده را اختصاص داده باشد و این عملیات ظرفیت اسلایس مقصد را تغییر نمی‌دهد.

2.5.1 استفاده از تابع copy

کد زیر نحوه استفاده از copy را نشان می‌دهد:

src := []int{1, 2, 3}
dst := make([]int, 3)
// کپی کردن عناصر به اسلایس مقصد
copied := copy(dst, src)
fmt.Println(dst, copied)

تابع copy تعداد عناصر کپی شده را برمی‌گرداند و این نباید بیشتر از طول اسلایس مقصد یا طول اسلایس مبدأ، هر کدام کوچکتر است، باشد.

2.5.2 ملاحظات

در هنگام استفاده از تابع copy، اگر عناصر جدید برای کپی شدن اضافه شوند اما اسلایس مقصد فضای کافی نداشته باشد، تنها عناصری که اسلایس مقصد قادر به جا دادن آنها است کپی می‌شوند.

2.6 اسلایس‌های چند بعدی

یک اسلایس چند بعدی یک اسلایس است که شامل چند اسلایس است. این مشابه یک آرایه چند بعدی است، اما به دلیل طول متغیر اسلایس‌ها، اسلایس‌های چند بعدی انعطاف‌پذیرتر هستند.

2.6.1 ایجاد اسلایس‌های چند بعدی

ایجاد یک اسلایس دو بعدی (اسلایس از اسلایس‌ها):

twoD := make([][]int, 3)
for i := 0; i < 3; i++ {
    twoD[i] = make([]int, 3)
    for j := 0; j < 3; j++ {
        twoD[i][j] = i + j
    }
}
fmt.Println("اسلایس دو بعدی: ", twoD)

2.6.2 استفاده از اسلایس‌های چند بعدی

استفاده از یک اسلایس چند بعدی شبیه به استفاده از یک اسلایس یک بعدی است و توسط اندیس دسترسی پیدا می‌کند:

// دسترسی به عناصر اسلایس دو بعدی
val := twoD[1][2]
fmt.Println(val)

3 مقایسه کاربردهای آرایه و اسلایس

3.1 مقایسه‌ی حالت‌های استفاده

در زبان Go، آرایه‌ها و اسلایس‌ها هر دو برای ذخیره‌ی مجموعه‌هایی از نوع داده‌های یکسان استفاده می‌شوند، اما تفاوت‌های مشخصی در موارد استفاده دارند.

آرایه‌ها:

  • طول یک آرایه از زمان تعریف تا زمان فیکس است که این امر آن را برای ذخیره‌ی تعداد معین و ثابتی از عناصر مناسب می‌کند.
  • وقتی یک ظرفیت با اندازه‌ی ثابت نیاز است، مانند نمایش یک ماتریس با اندازه‌ی ثابت، آرایه بهترین انتخاب است.
  • آرایه‌ها می‌توانند در استک تخصیص داده شوند که وقتی اندازه‌ی آرایه بزرگ نیست باعث افزایش کارایی می‌شود.

اسلایس‌ها:

  • یک اسلایس یک انتزاع از یک آرایه‌ی دینامیک است، با طول متغیر، که برای ذخیره‌ی تعداد نامعلوم یا مجموعه‌ای از عناصری که به صورت پویا تغییر می‌کنند مناسب است.
  • وقتی نیاز به یک آرایه دینامیک که می‌تواند به طور لازم رشد یا کاهش یابد، مانند ذخیره‌ی ورودی‌های نامعلوم کاربر، اسلایس انتخاب مناسب‌تری است.
  • ساختار حافظه‌ی یک اسلایس امکان ارجاع آسان به بخش یا تمام آرایه را فراهم می‌کند که معمولاً برای کار با زیررشته‌ها، تقسیم محتوای فایل و سایر حالت‌ها مورد استفاده قرار می‌گیرد.

به طور خلاصه، آرایه‌ها برای حالت‌هایی با نیازهای اندازه‌ی ثابت مناسب هستند که ویژگی‌های مدیریت حافظه‌ی استاتیک Go را منعکس می‌کنند، در حالی‌که اسلایس‌ها انعطاف‌پذیرتر هستند و به عنوان یک گسترش انتزاعی از آرایه‌ها، برای کار با مجموعه‌های پویایی مناسب هستند.

3.2 ملاحظات عملکرد

وقتی که باید بین استفاده از یک آرایه یا یک اسلایس انتخاب کنیم، عملکرد یک عامل مهم برای در نظر گرفتن است.

آرایه:

  • سرعت دسترسی سریع، به دلیل حافظه‌ی پیوسته و شاخص‌گذاری ثابت.
  • تخصیص حافظه در استک (اگر اندازه‌ی آرایه شناخته شده و بسیار بزرگ نباشد) بدون درگیری اضافی حافظه‌ی هیپ.
  • حافظه‌ی اضافی برای ذخیره‌ی طول و ظرفیت نیست که می‌تواند برای برنامه‌های حساس به حافظه مفید باشد.

اسلایس:

  • رشد یا کوچک شدن دینامیک ممکن است منجر به بار هم‌زمان عملکرد شود: رشد ممکن است منجر به تخصیص حافظه جدید و کپی عناصر قدیمی شود، در حالی که کوچک شدن ممکن است نیاز به تنظیم پویندها داشته باشد.
  • عملیات اسلایس به نفس خود سریع است، اما افزودن یا حذف مکرر عناصر ممکن است به شکستگی حافظه منجر شود.
  • با وجود این که دسترسی به اسلایس هزینه‌ی غیرمستقیم کوچکی را به همراه دارد، به طور کلی تاثیر چندانی بر عملکرد ندارد مگر اینکه در کد بسیار حساس به عملکرد باشد.

بنابراین، اگر عملکرد یک نکته مهم باشد و اندازه‌ی داده‌ها از قبل شناخته شود، استفاده از یک آرایه مناسب‌تر است. با این وجود، اگر انعطاف و راحتی مورد نیاز باشد، استفاده از یک اسلایس توصیه می‌شود، به خصوص برای کار با مجموعه‌های داده‌های بزرگ.

4 مسائل مشترک و راه‌حل‌ها

در فرآیند استفاده از آرایه‌ها و اسلایس‌ها در زبان Go، توسعه‌دهندگان ممکن است با مشکلات مشترک زیر مواجه شوند.

مسئله ۱: خارج از محدوده‌ی آرایه

  • خارج از محدوده‌ی آرایه به اشاره به دسترسی به یک شاخص که از طول آرایه بیشتر است است. این منجر به خطای اجرا می‌شود.
  • راه‌حل: همیشه قبل از دسترسی به عناصر آرایه، از محدوده معتبر آرایه برای عملکرد دسترسی زدن بررسی کنید. این امر با مقایسه شاخص و طول آرایه قابل دستیابی است.
var arr [5]int
index := 10 // فرض کنید یک شاخص خارج از محدوده
if index < len(arr) {
    fmt.Println(arr[index])
} else {
    fmt.Println("شاخص خارج از محدوده‌ی آرایه است.")
}

مسئله ۲: نشت حافظه در اسلایس‌ها

  • اسلایس‌ها ممکن است به طور ناخواسته ارجاع‌هایی به قسمت یا تمام آرایه اصلی نگه دارند، حتی اگر فقط بخش کوچکی نیاز باشد. این ممکن است در صورت بزرگ بودن آرایه‌ی اصلی منجر به نشت حافظه شود.
  • راه‌حل: اگر یک اسلایس موقت نیاز باشد، در نظر گرفته شود که با کپی کردن بخش مورد نیاز، یک اسلایس جدید ایجاد شود.
original := make([]int, 1000000)
smallSlice := make([]int, 10)
copy(smallSlice, original[:10]) // فقط بخش مورد نیاز کپی می‌شود
// این روش باعث می‌شود که smallSlice به بخش‌های دیگر از original ارجاع‌نکند و به بازیافت حافظه‌ی غیر ضروری کمک کند

مسئله ۳: خطاهای داده منتج به استفاده دوباره از اسلایس

  • به دلیل اشتراک اسلایس‌ها در ارجاع به همان آرایه اصلی، ممکن است تغییرات داده در اسلایس‌های مختلف تأثیرات ناگهانی‌ای داشته باشد که به خطاهای غیرمنتظره منجر می‌شود.
  • راه‌حل: برای جلوگیری از این موقعیت، بهتر است یک کپی جدید از اسلایس ایجاد شود.
sliceA := []int{1, 2, 3, 4, 5}
sliceB := make([]int, len(sliceA))
copy(sliceB, sliceA)
sliceB[0] = 100
fmt.Println(sliceA[0]) // خروجی: 1
fmt.Println(sliceB[0]) // خروجی: 100

موارد فوق فقط برخی از مسائل مشترک و راه‌حل‌هایی هستند که ممکن است در هنگام استفاده از آرایه‌ها و اسلایس‌ها در زبان Go پیش بیاید. در واقع در توسعه‌ی واقعی ممکن است جزییات بیشتری وجود داشته باشد، اما پیروی از این اصول اساسی می‌تواند کمک کند تا از بسیاری از خطاهای مشترک جلوگیری شود.