1 مفاهیم پایه‌ای درباره Struct

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

نیاز به structs از جوانب زیر به وجود می‌آید:

  • سازماندهی متغیرها با ارتباط قوی به هم برای افزایش قابلیت اداره کد.
  • ارائه یک روش برای شبیه‌سازی "کلاس‌ها"، فراهم کردن ویژگی‌های پوشانندگی و تجمیع.
  • در هنگام ارتباط با ساختارهای داده مانند JSON، رکوردهای پایگاه داده و غیره، structs ابزاری مناسب برای نقشه‌برداری فراهم می‌کنند.

سازماندهی داده با استفاده از structs امکان نمایش واضح‌تر از مدل‌های شیء‌های مستقیم چون کاربران، سفارشات و غیره را فراهم می‌کند.

2 تعریف یک Struct

سینتکس تعریف یک struct به صورت زیر است:

type نام‌Struct struct {
    فیلد1 نوع‌فیلد1
    فیلد2 نوع‌فیلد2
    // ... سایر متغیرهای عضو
}
  • کلمه کلیدی type، معرفی تعریف struct است.
  • نام‌Struct نام نوع struct است، که طبق خوانندگی‌های Go معمولاً به صورت بزرگ‌نویسی شده است تا نمایش دهد که قابلیت صدور دارد.
  • کلمه‌ی struct نشان‌دهنده این است که این یک نوع struct است.
  • در داخل آکولاد‌های {}، متغیرهای عضو (فیلدها) struct تعریف می‌شوند، هر کدام همراه با نوع خود.

نوع اعضای struct می‌تواند هر نوعی باشد، از جمله انواع ابتدایی (مانند int، string و غیره) و انواع پیچیده (مانند آرایه‌ها، برش‌ها، struct دیگر و غیره).

به عنوان مثال، تعریف یک struct که یک شخص را نمایش می‌دهد:

type شخص struct {
    نام   رشته
    سن    عدد_صحیح
    ایمیل‌ها []رشته // می‌تواند شامل انواع پیچیده‌تری مانند برش‌ها باشد
}

در کد فوق، struct شخص دارای سه متغیر عضو است: نام از نوع رشته، سن از نوع عدد_صحیح و ایمیل‌ها از نوع برش_رشته، که نشان‌دهنده این است که یک شخص ممکن است چندین آدرس ایمیل داشته باشد.

3 ایجاد و مقدمه‌ای برای Struct

3.1 ایجاد یک نمونه از Struct

دو روش برای ایجاد یک نمونه از struct وجود دارد: تعیین مستقیم یا استفاده از کلمه کلیدی new.

تعیین مستقیم:

var شخص1 شخص

کد فوق یک نمونه با نام شخص1 از نوع شخص ایجاد می‌کند، جایی که هر متغیر عضو struct برابر با مقدار صفر نوع‌خود می‌شود.

استفاده از کلمه کلیدی new:

شخص2 := new(شخص)

ایجاد یک struct با استفاده از کلمه کلیدی new، منجر به ایجاد یک اشاره‌گر به struct می‌شود. متغیر شخص2 در این نقطه از نوع *شخص است که به یک متغیر تازه تخصیص یافته از نوع شخص اشاره می‌کند که مقادیر صفر متغیرهای عضو را دارد.

3.2 مقدمه‌ای برای مقداردهی اولیه نمونه‌های Struct

نمونه‌های struct می‌توانند همزمان هنگام ایجادشان به دو روش، با نام‌های فیلد یا بدون نام فیلد، مقداردهی اولیه شوند.

مقداردهی با نام‌های فیلد:

شخص3 := شخص{
    نام:   "آلیس",
    سن:    30,
    ایمیل‌ها: []رشته{"[email protected]", "[email protected]"},
}

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

مقداردهی بدون نام‌های فیلد:

شخص4 := شخص{"باب", 25, []رشته{"[email protected]"}}

در هنگام مقداردهی بدون نام‌های فیلد، اطمینان حاصل شود که مقادیر اولیه هر متغیر عضو در همان ترتیبی است که struct تعریف شده است و هیچ فیلدی نمی‌تواند حذف شود.

به علاوه، می‌توان با فیلدهای خاص، نمونه‌های struct را مقداردهی اولیه کرد و هر فیلد انتخاب نشده مقدار صفر خود را خواهد گرفت:

شخص5 := شخص{نام: "چارلی"}

در این مثال، تنها فیلد نام مقداردهی شده است و هر دو سن و ایمیل‌ها مقادیر صفر خود را خواهند گرفت.

4 دسترسی به اعضای Struct

دسترسی به متغیرهای عضو struct در Go بسیار ساده است و با استفاده از اپراتور نقطه (.) انجام می‌شود. اگر یک متغیر struct داشته باشید، می‌توانید به طور ساده مقادیر عضوهای آن را خواند یا تغییر دهید.

6 توابع ساختار

امکانات برنامه‌نویسی شیءگرا (OOP) می‌تواند از طریق توابع ساختار انجام شود.

6.1 مفاهیم پایه توابع

در زبان Go، با وجود عدم وجود مفهوم سنتی کلاس‌ها و اشیاء، می‌توان امکانات مشابه OOP را با اتصال توابع به ساختارها داشت. یک تابع ساختار یک نوع خاص از تابع است که با یک نوع خاص از ساختار (یا یک اشاره به یک ساختار) ارتباط برقرار می‌کند و اجازه می‌دهد که آن نوع دسته خود را از توابع خود داشته باشد.

// تعریف یک ساختار ساده
type Rectangle struct {
    length، width float64
}

// تعریف یک تابع برای ساختار Rectangle برای محاسبه مساحت مستطیل
func (r Rectangle) Area() float64 {
    return r.length * r.width
}

در کد بالا، تابع Area با ساختار Rectangle ارتباط دارد. در تعریف تابع، (r Rectangle) گیرنده است که مشخص می‌کند که این تابع با نوع Rectangle ارتباط دارد. گیرنده قبل از نام تابع ظاهر می‌شود.

6.2 دریافت‌کنندگان مقدار و دریافت‌کنندگان اشاره‌گر

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

// تعریف یک روش با دریافت‌کننده مقدار
func (r Rectangle) Perimeter() float64 {
    return 2 * (r.length + r.width)
}

// تعریف یک روش با دریافت‌کننده اشاره‌گر که می‌تواند ساختار را اصلاح کند
func (r *Rectangle) SetLength(newLength float64) {
    r.length = newLength // می‌تواند مقدار اصلی ساختار را تغییر دهد
}

در مثال فوق، Perimeter یک روش با دریافت‌کننده مقدار است، فراخوانی آن مقدار Rectangle را تغییر نمی‌دهد. با این حال، SetLength یک روش با دریافت‌کننده اشاره‌گر است و فراخوانی این روش تأثیری بر نمونه اصلی Rectangle خواهد داشت.

6.3 فراخوانی روش

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

func main() {
    rect := Rectangle{length: 10, width: 5}

    // فراخوانی روش با دریافت‌کننده مقدار
    fmt.Println("مساحت:", rect.Area())

    // فراخوانی روش با دریافت‌کننده مقدار
    fmt.Println("محیط:", rect.Perimeter())

    // فراخوانی روش با دریافت‌کننده اشاره‌گر
    rect.SetLength(20)

    // دوباره فراخوانی روش با دریافت‌کننده مقدار، توجه کنید که طول تغییر کرده است
    fmt.Println("پس از اصلاح، مساحت:", rect.Area())
}

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

6.4 انتخاب نوع دریافت‌کننده

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

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

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

7 ساختار و سریالسازی JSON

در Go، اغلب لازم است یک ساختار را به فرمت JSON برای انتقال شبکه‌ای یا به عنوان یک پرونده پیکربندی سریالسازی کنیم. به طور مشابه، نیاز است که بتوانیم JSON را به نمونه‌های ساختار تجزیه و تحلیل کنیم. بسته encoding/json در Go این قابلیت را فراهم می‌کند.

در ادامه مثالی ارائه شده است که نمونه از تبدیل بین یک ساختار و JSON را نشان می‌دهد:

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

// تعریف ساختار شخص و استفاده از برچسب‌های json برای تعریف تطابق بین فیلدهای ساختار و نام‌های فیلد JSON
type Person struct {
	Name   string   `json:"name"`
	Age    int      `json:"age"`
	Emails []string `json:"emails,omitempty"`
}

func main() {
	// ایجاد نمونه جدید از Person
	p := Person{
		Name:   "John Doe",
		Age:    30,
		Emails: []string{"[email protected]", "[email protected]"},
	}

	// سریال‌سازی به JSON
	jsonData, err := json.Marshal(p)
	if err != nil {
		log.Fatalf("ناموفقیت در بسته‌بندی JSON: %s", err)
	}
	fmt.Printf("فرمت JSON: %s\n", jsonData)

	// تجزیه و تحلیل به یک ساختار
	var p2 Person
	if err := json.Unmarshal(jsonData, &p2); err != nil {
		log.Fatalf("ورودی‌گیری از JSON ناموفق بود: %s", err)
	}
	fmt.Printf("ساختار بازیابی‌شده: %#v\n", p2)
}

در کد بالا، ما یک ساختار Person را تعریف کردیم، از جمله یک فیلد نوع 'slice' با گزینه "omitempty". این گزینه مشخص می‌کند که اگر فیلد خالی یا مفقود باشد، در JSON قرار داده نمی‌شود.

ما از تابع json.Marshal برای سریالسازی یک نمونه از ساختار به JSON استفاده کرده و از تابع json.Unmarshal برای تجزیه و تحلیل داده JSON به یک نمونه ساختاری استفاده کرده‌ایم.

8 مباحث پیشرفته در ساختارها

8.1 مقایسه‌ی ساختارها (Structs)

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

در زیر مثالی از مقایسه‌ی ساختارها آمده است:

package main

import "fmt"

type Point struct {
	X, Y int
}

func main() {
	p1 := Point{1, 2}
	p2 := Point{1, 2}
	p3 := Point{1, 3}

fmt.Println("p1 == p2:", p1 == p2) // Output: p1 == p2: true
fmt.Println("p1 == p3:", p1 == p3) // Output: p1 == p3: false
}

در این مثال، p1 و p2 به عنوان مساوی در نظر گرفته می‌شوند زیرا تمام مقادیر فیلدهای آنها یکسان است. و p3 نیز برابر با p1 نیست زیرا مقدار Y متفاوت است.

8.2 کپی کردن ساختارها (Structs)

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

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

در زیر مثالی از کپی کردن یک ساختار آمده است:

package main

import "fmt"

type Data struct {
Numbers []int
}

func main() {
// مقداردهی اولیه یک نمونه از ساختار Data
original := Data{Numbers: []int{1, 2, 3}}

// کپی کردن ساختار
copied := original

// تغییر دادن عناصر آرایۀ کپی شده
copied.Numbers[0] = 100

// مشاهده عناصر نمونه‌های اصلی و کپی شده
fmt.Println("اصلی:", original.Numbers) // Output: Original: [100 2 3]
fmt.Println("کپی:", copied.Numbers) // Output: Copied: [100 2 3]
}

همانطور که در مثال نشان داده شده است، نمونه‌های original و copied از همان آرایه به اشتراک گذاری می‌کنند، بنابراین تغییر دادن داده‌های آرایه در copied نیز بر روی داده‌های آرایه در original تأثیر خواهد داشت.

برای جلوگیری از این مشکل، می‌توانید با جلوگیری از این اتفاق، کپی عمیق واقعی را به دست آورید؛ به این صورت که محتویات آرایۀ کپی را به صورت صریح به یک آرایه جدید کپی می‌کنید:

newNumbers := make([]int, len(original.Numbers))
copy(newNumbers, original.Numbers)
copied := Data{Numbers: newNumbers}

به این روش، هرگونه تغییرات روی copied بر روی original تأثیر نخواهند داشت.