1. مقدمه‌ای درباره نقشه‌ها

در زبان Go، نقشه یک نوع داده ویژه است که می‌تواند یک مجموعه از جفت‌های کلید-مقدار انواع مختلف را ذخیره کند. این با دیکشنری در پایتون یا HashMap در جاوا مشابه است. در Go، نقشه یک نوع تعبیه شده است که با استفاده از یک جدول هش پیاده‌سازی می‌شود و ویژگی‌های جستجوی سریع داده، به‌روزرسانی و حذف را دارد.

ویژگی‌ها

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

موارد استفاده

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

2. ایجاد یک نقشه

2.1. ایجاد با استفاده از تابع make

رایج‌ترین روش برای ایجاد یک نقشه استفاده از تابع make با سینتکس زیر است:

make(map[keyType]valueType)

اینجا، keyType نوع کلید و valueType نوع مقدار است. یک مثال استفاده خاص:

// ایجاد یک نقشه با نوع کلید رشته و نوع مقدار عدد صحیح
m := make(map[string]int)

در این مثال، یک نقشه خالی ایجاد کردیم که برای ذخیره جفت‌های کلید-مقدار با کلید‌های رشته‌ای و مقادیر عدد صحیح استفاده می‌شود.

2.2. ایجاد با دستورالعمل مستقیم

علاوه بر استفاده از make، می‌توانیم نیز یک نقشه را با استفاده از دستورالعمل مستقیم ایجاد و مقداردهی اولیه کنیم که در عین حال یک سری از جفت‌های کلید-مقدار را اعلام می‌کند:

m := map[string]int{
    "سیب": 5,
    "گلابی": 6,
    "موز": 3,
}

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

2.3. ملاحظات در مورد مقدمه‌سازی نقشه

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

var m map[string]int
if m == nil {
    m = make(map[string]int)
}
// در حال حاضر استفاده از m امن است

همچنین ارزشیابی وجود خاصیت برای بررسی اگر یک کلید در یک نقشه موجود است:

value, ok := m["کلید"]
if !ok {
    // "کلید" در نقشه وجود ندارد
}

در اینجا، value مقدار مرتبط با کلید داده‌شده است و ok یک مقدار بولی است که اگر کلید در نقشه موجود باشد، true و اگر وجود نداشته باشد، false خواهد بود.

3. دسترسی و اصلاح یک نقشه

3.1. دسترسی به عناصر

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

func main() {
    // تعریف یک نقشه
    scores := map[string]int{
        "آلیس": 92,
        "باب": 85,
    }

    // دسترسی به یک کلید موجود
    scoreAlice := scores["آلیس"]
    fmt.Println("امتیاز آلیس:", scoreAlice) // خروجی: امتیاز آلیس: 92

    // دسترسی به یک کلید غیرموجود
    scoreMissing := scores["چارلی"]
    fmt.Println("امتیاز چارلی:", scoreMissing) // خروجی: امتیاز چارلی: 0
}

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

3.2 بررسی وجود کلید

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

func main() {
    scores := map[string]int{
        "Alice": 92,
        "Bob": 85,
    }

    // بررسی وجود کلید "Bob"
    score, exists := scores["Bob"]
    if exists {
        fmt.Println("نمره‌ی باب:", score)
    } else {
        fmt.Println("نمره‌ی باب یافت نشد.")
    }

    // بررسی وجود کلید "Charlie"
    _, exists = scores["Charlie"]
    if exists {
        fmt.Println("نمره‌ی چارلی یافت شد.")
    } else {
        fmt.Println("نمره‌ی چارلی یافت نشد.")
    }
}

در این مثال، ما از یک عبارت if برای بررسی مقدار بولین استفاده می‌کنیم تا تعیین کنیم آیا یک کلید وجود دارد یا خیر.

3.3 اضافه کردن و به‌روزرسانی عناصر

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

func main() {
    // تعریف یک نقشه‌ی خالی
    scores := make(map[string]int)

    // اضافه کردن عناصر
    scores["Alice"] = 92
    scores["Bob"] = 85

    // به‌روزرسانی عناصر
    scores["Alice"] = 96  // به‌روزرسانی یک کلید موجود

    // چاپ نقشه
    fmt.Println(scores)   // خروجی: map[Alice:96 Bob:85]
}

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

3.4 حذف عناصر

حذف عناصر از یک نقشه با استفاده از تابع delete انجام می‌شود. مثال زیر عملیات حذف را نشان می‌دهد:

func main() {
    scores := map[string]int{
        "Alice": 92,
        "Bob": 85,
        "Charlie": 78,
    }

    // حذف یک عنصر
    delete(scores, "Charlie")

    // چاپ نقشه برای اطمینان از حذف چارلی
    fmt.Println(scores)  // خروجی: map[Alice:92 Bob:85]
}

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

4 گذر از یک نقشه

در زبان Go، می‌توانید از عبارت for range برای گذر از یک ساختار داده نقشه و دسترسی به هر جفت کلید-مقدار در ظرف استفاده کنید. این نوع عملیات گذر از حلقه، یک عملیات اساسی است که توسط ساختار داده نقشه پشتیبانی می‌شود.

4.1 استفاده از for range برای گذر از یک نقشه

عبارت for range می‌تواند مستقیماً بر روی یک نقشه استفاده شود تا هر جفت کلید-مقدار را در نقشه بازیابی کند. در زیر یک مثال ابتدایی از استفاده از for range برای گذر از یک نقشه آمده است:

package main

import "fmt"

func main() {
    myMap := map[string]int{"Alice": 23, "Bob": 25, "Charlie": 28}

    for key, value := range myMap {
        fmt.Printf("کلید: %s, مقدار: %d\n", key, value)
    }
}

در این مثال، متغیر key به کلید جاری اختصاص می‌یابد و متغیر value به مقدار مرتبط با آن کلید.

4.2 ملاحظات در مورد ترتیب گذر

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

به عنوان مثال، اجرای کد زیر دو بار پشت سر هم ممکن است خروجی متفاوتی داشته باشد:

package main

import "fmt"

func main() {
    myMap := map[string]int{"Alice": 23, "Bob": 25, "Charlie": 28}

    fmt.Println("گذر اول:")
    for key, value := range myMap {
        fmt.Printf("کلید: %s, مقدار: %d\n", key, value)
    }

    fmt.Println("\nگذر دوم:")
    for key, value := range myMap {
        fmt.Printf("کلید: %s, مقدار: %d\n", key, value)
    }
}

5 موضوع پیشرفته در مورد نقشه‌ها

در ادامه، به بررسی چندین موضوع پیشرفته مرتبط با نقشه‌ها می‌پردازیم که می‌تواند به شما کمک کند تا نقشه‌ها را بهتر درک کرده و استفاده کرده‌اید.

5.1 ویژگی‌های حافظه و عملکرد نقشه‌ها

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

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

myMap := make(map[string]int, 100)

این می‌تواند هزینه اضافی گسترش پویا نقشه در زمان اجرا را کاهش دهد.

5.2 ویژگی‌های نوع مرجعی نقشه‌ها

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

اینجا یک مثال است:

package main

import "fmt"

func main() {
    originalMap := map[string]int{"Alice": 23, "Bob": 25}
    newMap := originalMap

    newMap["Charlie"] = 28

    fmt.Println(originalMap) // خروجی نشان می‌دهد کلید-مقدار "Charlie": 28 به صورت تازه اضافه شده است
}

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

5.3 ایمنی همزمانی و sync.Map

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

کتابخانه استاندارد Go، نوع sync.Map را ارائه می‌دهد که یک نقشه ایمن برای محیط‌های همزمان طراحی شده است. این نوع از توابع اساسی مانند Load، Store، LoadOrStore، Delete و Range برای انجام عملیات بر روی نقشه ارائه می‌کند.

اینجا یک مثال از استفاده از sync.Map است:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var mySyncMap sync.Map

    // ذخیره کلید-مقدار
    mySyncMap.Store("Alice", 23)
    mySyncMap.Store("Bob", 25)

    // بازیابی و چاپ یک کلید-مقدار
    if value, ok := mySyncMap.Load("Alice"); ok {
        fmt.Printf("کلید: Alice، مقدار: %d\n", value)
    }

    // استفاده از متد Range برای حلقه زدن از sync.Map
    mySyncMap.Range(func(key, value interface{}) bool {
        fmt.Printf("کلید: %v، مقدار: %v\n", key, value)
        return true // ادامه حلقه زدن
    })
}

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