2.2 ساختار (struct)

2.2 ساختار (struct)

در زبان گو ساختار کالکشنی از فیلدها با تایپ‌های مختلف است. شما با استفاده از ساختار می‌توانید یک مدل کلی از بدنه پروژه خود را تعریف کنید. برای نمونه ما در مثال زیر یک نمونه از ساختار employee کارمند را مثال زدیم تا شما کمی با مفهوم ساختار آشنا شوید.

type employee struct {
    name   string
    age    int
    salary int
}

نکته: ساختار می‌تواند بصورت خالی جهت برخی اهداف ایجاد گردد. به مثال زیر دقت کنید:‌

type sample struct {}

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

  • برای ایجاد ساختار باید از کلمه کلیدی type اسم ساختار و در ادامه کلمه کلیدی struct استفاده کنید.

  • سپس داخل بدنه ساختار فیلدها را تعریف کنید.

    • فیلد name از نوع string
    • فیلد age از نوع int
    • فیلد salary از نوع int
ساختار را در زبان گو، با class در سایر زبان‌ها مقایسه می‌کنند. هرچند زبان گو یک زبان شی‌گرا محسوب نمی‌شود.

2.2.1 تعریف تایپ struct #

به مثال زیر توجه کنید:

type point struct {
    x float64
    y float64
}

در مثال بالا ما ۲ تا فیلد برای ساختار تعریف کردیم که هر دو فیلد از نوع float64 هستند.

2.2.2 ایجاد یک متغیر ساختار (struct) #

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

emp := employee{}

در مثال بالا ما یک متغیر با مقدار پیش‌فرض صفر ساختار employee تعریف کردیم.

زمانیکه یک متغیر ساختار خالی، مانند مثال بالا تعریف می‌کنید مقدار استفاده شده از حافظه 0 بایت است.
  • ایجاد متغیر ساختار و مقدار دهی فیلدها در یک خط:
emp := employee{name: "Sam", age: 31, salary: 2000}
  • ایجاد متغیر ساختار و مقدار دهی فیلد در خط‌های مختلف (این روش برای خوانایی و درک بهتر توصیه می‌شود) :
emp := employee{
   name:   "Sam",
   age:    31,
   salary: 2000,
}

توجه کنید هیچ اجباری نیست که حتماً شما باید فیلدی را مقدار دهی کنید، شما می‌توانید هر زمانیکه نیاز داشتید ساختار خودتان رو مقدار دهی کنید.

emp := employee{
   name: "Sam",
   age: 31,
}

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

package main

import "fmt"

type employee struct {
    name   string
    age    int
    salary int
}

func main() {
    emp1 := employee{}
    fmt.Printf("Emp1: %+v\n", emp1)

    emp2 := employee{name: "Sam", age: 31, salary: 2000}
    fmt.Printf("Emp2: %+v\n", emp2)

    emp3 := employee{
        name:   "Sam",
        age:    31,
        salary: 2000,
    }
    fmt.Printf("Emp3: %+v\n", emp3)

    emp4 := employee{
        name: "Sam",
        age:  31,
    }
    fmt.Printf("Emp4: %+v\n", emp4)
}
  • ایجاد متغیر ساختار و مقدار دهی فیلدها بدون نام فیلد:

شما می‌توانید فیلدها را بدون اینکه نام فیلد را قرار دهید مقدار دهی کنید اما از نظر تکنیکی این کار توصیه نمی‌شود، دلیل این توصیه هم این است که اگر شما فیلدها رو به این روش مقدار دهی کنید، باید ترتیب رو در نظر بگیرید یعنی 1: باید نام باشد، 2: باید سن باشد، 3: باید درآمد باشد و اگر این ترتیب رعایت نشود شما دیتای اشتباهی خواهید داشت.

emp := employee{"Sam", 31, 2000}

{Sam 31 2000} // حروجی

در مثال بالا ترتیب رعایت شده. به مثال زیر توجه کنید:

emp := employee{"Sam", 2000, 31}

{Sam 2000 31} // حروجی

همانطور که در مثال بالا دیدین الان با ترتیب اشتباه سن کارمند و درآمدش جابه جا شدن و ما دیتای اشتباهی از کارمند خواهیم داشت.

2.2.3 دسترسی و تنظیم فیلدهای ساختار (struct) #

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

package main

import "fmt"

type employee struct {
    name   string
    age    int
    salary int
}

func main() {
    emp := employee{name: "Sam", age: 31, salary: 2000}

    //Accessing a struct field
    fmt.Printf("Current name is: %s\n", emp.name)

    //Assigning a new value to name field
    emp.name = "John"
    fmt.Printf("New name is: %s\n", emp.name)
}

2.2.4 کار با اشاره‌گر (Pointer) در ساختار (struct) #

شما برای ایجاد یک struct از نوع اشاره‌گر می‌توانید از دو حالت زیر استفاده کنید:

  • با استفاده از عملگر & که اشاره به خانه حافظه دارد
  • با استفاده از تابع new

2.2.4.1 ایجاد ساختار با استفاده از عملگر & #

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

emp := employee{name: "Sam", age: 31, salary: 2000}
empP := &emp

حتی شما می‌توانید یک ساختار اشاره‌گر را مستقیماً ایجاد کنید این روش پیشنهاد می‌شود. به مثال زیر توجه کنید:

empP := &employee{name: "Sam", age: 31, salary: 2000}

در مثال زیر هر دو روش رو برای شما توضیح دادیم. با دقت به کد و خروجی کد نگاه کنید:‌

package main

import "fmt"

type employee struct {
    name   string
    age    int
    salary int
}

func main() {
    emp := employee{name: "Sam", age: 31, salary: 2000}
    empP := &emp
    fmt.Printf("Emp: %+v\n", empP)
    empP = &employee{name: "John", age: 30, salary: 3000}
    fmt.Printf("Emp: %+v\n", empP)
}

2.2.4.2 ایجاد ساختار با استفاده تابع new #

func new(Type) *Type

همینطور که در تعریف تابع new هم می‌بینید، این تابع یک تایپ از ما می‌گیرد و مقدار دهی می‌کند، و در آخر هم تایپ را از نوع اشاره‌گر برای ما بر می‌گرداند.

با استفاده از تابع new :

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

به مثال زیر توجه کنید:

empP := new(employee)

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

fmt.Printf("Emp Pointer: %p\n", empP)

برای اینکه مقدار کلی فیلدها را ببینید کافی است با استفاده از v+% اون رو چاپ کنید. به مثال زیر توجه کنید:

fmt.Printf("Emp Value: %+v\n", *empP)

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

package main

import "fmt"

type employee struct {
    name   string
    age    int
    salary int
}

func main() {
    empP := new(employee)
    fmt.Printf("Emp Pointer Address: %p\n", empP)
    fmt.Printf("Emp Pointer: %+v\n", empP)
    fmt.Printf("Emp Value: %+v\n", *empP)
}

2.2.5 چاپ یک متغیر ساختار (struct) #

برای اینکه بتوانید یک متغیر ساختار struct را چاپ کنید، از دو روش زیر می‌توانید استفاده کنید. توجه کنید متغیر ساختار بصورت key/value هست.

  • با استفاده از پکیج fmt
  • با استفاده از پکیج json/encoding

2.2.5.1 چاپ با استفاده از fmt #

در پکیج fmt ما 2 تا تابع کاربردی جهت چاپ داریم که اکثر اوقات از این دو تابع استفاده می‌کنیم:

  • تابع Println ورودی را با فرمت پیش‌فرض چاپ می‌کند.
  • تابع Printf ورودی را با فرمت مشخص شده چاپ می‌کند فرمت رو خود ما مشخص می‌کنیم.

در مثال زیر ما یک نمونه از ساختار employee را ایجاد کردیم:

emp := employee{name: "Sam", age: 31, salary: 2000}

حال با استفاده از تابع Printf ساختار را با فرمت دلخواه خودمان چاپ کردیم:

fmt.Printf("%v", emp)  -  {Sam 31 2000}
fmt.Printf("%+v", emp) - {name:Sam age:31 salary:2000}
  • %v - مقدار value هر کدام از فیلدهای ساختار را چاپ می‌کند.
  • %+v - مقدار هرکدام از فیلدها به همراه اسم فیلد key-value را چاپ می‌کند.

در مثال زیر ما با استفاده از از تابع Println ساختار را چاپ کردیم:

fmt.Println(emp) - {Sam 31 2000}

در نهایت کد زیر یک مثال کلی از چاپ با استفاده از پکیج fmt است‌:

package main

import "fmt"

type employee struct {
    name   string
    age    int
    salary int
}

func main() {
    emp := employee{name: "Sam", age: 31, salary: 2000}
    fmt.Printf("Emp: %v\n", emp)
    fmt.Printf("Emp: %+v\n", emp)
    fmt.Printf("Emp: %#v\n", emp)
    fmt.Println(emp)
}

2.2.5.2 چاپ ساختار با استفاده از پکیج JSON #

در این روش ما با استفاده از ۲ تابع Marshal و MarshalIndent پکیج json، ساختار را encode می‌کنیم و در نهایت خروجی encode شده را چاپ می‌کنیم.

  • Marshal - در این تابع ما به عنوان ورودی‌، ساختار را پاس می‌دهیم و در نهایت ۲ خروجی از نوع بایت و خطا دریافت می‌کنیم.
Marshal(v interface{}) ([]byte, error)
  • MarhsalIndent - در این تابع ما ۳ تا ورودی به تابع می‌فرستیم, به ترتیب ساختار، پیشوند و indent و در نهایت ۲ خروجی از نوع بایت و خطا دریافت می‌کنیم.
MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)

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

package main

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

type employee struct {
    Name   string
    Age    int
    salary int
}

func main() {
    emp := employee{Name: "Sam", Age: 31, salary: 2000}
    //Marshal
    empJSON, err := json.Marshal(emp)
    if err != nil {
        log.Fatalf(err.Error())
    }
    fmt.Printf("Marshal funnction output %s\n", string(empJSON))

    //MarshalIndent
    empJSON, err = json.MarshalIndent(emp, "", "  ")
    if err != nil {
        log.Fatalf(err.Error())
    }
    fmt.Printf("MarshalIndent funnction output %s\n", string(empJSON))
}
برای اطلاعات بیشتر در خصوص پکیج json می‌توانید به بخش آموزش کار با json مراجعه کنید.

2.2.6 کار با تگ ها در ساختار (struct) #

ساختار زبان گو، به شما امکان اضافه کردن metadata به هر یک از فیلدها را می‌دهد و ما این قابلیت را به عنوان تگ می‌شناسیم. تگ‌ها برای انجام یکسری عملیات خاص نظیر encode/decode، اعتبارسنجی مقادیر فیلدها و … به ما کمک می‌کند و یکی از کاربردی‌ترین عناوین در ساختار هستند.

به مثال های زیر توجه کنید تا کارکرد تگ ها را متوجه شوید:

type strutName struct{
   fieldName type `key:"value" key2:"value2"`
}
type employee struct {
    Name   string
    Age    int
    Salary int
}

در این مثال، مقدار داخل متغیری که از نوع Employee است را تبدیل به json می کنیم و چاپ می کنیم.

package main

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

type employee struct {
    Name   string
    Age    int
    Salary int
}

func main() {
    emp := employee{Name: "Sam", Age: 31, Salary: 2000}
    //Converting to jsonn
    empJSON, err := json.MarshalIndent(emp, "", "  ")
    if err != nil {
        log.Fatalf(err.Error())
    }
    fmt.Println(string(empJSON))
}

حالا به ما می گویند که اول اسم فیلد ها در خروجی json با حرف بزرگ شروع نشود و حرف کوچک باشد. اولین چیزی که شاید به ذهن شما خطور کند این است که اسم فیلد ها را در ساختار تعریف شده با حروف کوچک شروع کنیم:

package main

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

type employee struct {
	name   string
	age    int
	salary int
}

func main() {
	emp := employee{name: "Sam", age: 31, salary: 2000}
	//Converting to jsonn
	empJSON, err := json.MarshalIndent(emp, "", "   ")
	if err != nil {
		log.Fatalf(err.Error())
	}
	fmt.Println(string(empJSON))
}

اما خروجی ما یک json خالی است. جرا؟ چون زمانی که اسم فیلد‌ ها با حروف کوچک شروع شوند private هستند و از بیرون قابل دسترسی نیستند. به همین دلیل خروجی یک json خالی است.

برای حل این مشکل ما برای ساختار خودمان یک تگ json اضافه می کنیم و می گوییم اسم فیلد تو در json چیز دیگری است:

package main

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

type employee struct {
	Name   string `json:"name"`
	Age    int    `json:"age"`
	Salary int    `json:"salary"`
}

func main() {
	emp := employee{Name: "Sam", Age: 31, Salary: 2000}
	//Converting to jsonn
	empJSON, err := json.MarshalIndent(emp, "", "   ")
	if err != nil {
		log.Fatalf(err.Error())
	}
	fmt.Println(string(empJSON))
}

فکر می‌کنم خروجی بالا کاملاً برای ما روشن کرد که دقیقاً اون تگ‌هایی که قرار دادیم، برای ما چه کاری انجام دادند. بله کلید-key‌های ما را به اون نام‌هایی که در تگ‌ها نوشته بودیم تغییر دادند.

2.2.6.1 چند نمونه از کاربرد تگ ها #

تگ ها کاربرد های خیلی زیادی دارند که در بخش قرار است بعضی از آنها را بررسی کنیم.

می توانید با تگ (-) مشخص کنید که آن فیلد موقع سریالایز نادیده گرفته شود و نمایش داده نشود. مثال:

package main

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

type employee struct {
	Name   string `json:"name"`
	Age    int    `json:"-"`
	Salary int    `json:"salary"`
}

func main() {
	emp := employee{Name: "Sam", Salary: 2000}
	//Converting to jsonn
	empJSON, err := json.MarshalIndent(emp, "", "   ")
	if err != nil {
		log.Fatalf(err.Error())
	}
	fmt.Println(string(empJSON))
}

با استفاده از تگ omitempty اگر آن فیلد مقداری نداشته باشد، نمایش داده نمی شود:

package main

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

type employee struct {
	Name   string `json:"name,omitempty"`
	Age    int    `json:"age,omitempty"`
	Salary int    `json:"salary,omitempty"`
}

func main() {
	emp := employee{Age: 22, Salary: 2000}
	//Converting to jsonn
	empJSON, err := json.MarshalIndent(emp, "", "   ")
	if err != nil {
		log.Fatalf(err.Error())
	}
	fmt.Println(string(empJSON))
}

از دیگر کاربرد های تگ ها می توان به عملیات اعتبار سنجی اشاره کرد. برای مثال می توان چک کرد فیلد شماره موبایل از یازده رقم بیشتر و کمتر نباشد. همچنین در تعریف مدل های دیتابیس با استفاده از تگ ها ارتباط بین دیتابیس و مدل را می توانیم پیاده سازی کنیم و …

2.2.7 تعریف فیلد ناشناس در ساختار (struct) #

شما در ساختار struct امکان تعریف فیلدهای ناشناس را دارید و همینطور می‌توانید فیلدهای ناشناس را نیز مقدار دهی کنید.

type employee struct {
    string
    age    int
    salary int
}

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

package main

import "fmt"

type employee struct {
    string
    age    int
    salary int
}

func main() {
    emp := employee{string: "Sam", age: 31, salary: 2000}
    //Accessing a struct field
    n := emp.string
    fmt.Printf("Current name is: %s\n", n)
    //Assigning a new value
    emp.string = "John"
    fmt.Printf("New name is: %s\n", emp.string)
}

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

package main

import (
	"fmt"
)

type employee struct {
	string // name
	int    // age
	int    // salary
}

func main() {
	emp := employee{"alireza", 22, 10_000_000}

	fmt.Printf("%+v", emp)
}

2.2.8 تعریف ساختار تو در تو (nested) #

یکی دیگر از امکانات ساختار در زبان گو بحث ساختار تو در تو است. در مثالی که در ادامه زدیم ساختار address را داخل employee قرار دادیم:

package main

import "fmt"

type employee struct {
    name    string
    age     int
    salary  int
    address address
}

type address struct {
    city    string
    country string
}

func main() {
    address := address{city: "London", country: "UK"}
    emp := employee{name: "Sam", age: 31, salary: 2000, address: address}
    fmt.Printf("City: %s\n", emp.address.city)
    fmt.Printf("Country: %s\n", emp.address.country)
}

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

emp.address.city
emp.address.country

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

package main

type Product struct {
	Name  string
	Price int
}

type Mobile struct {
	Product  Product
	Ram      int
	SimCount int
}

func main() {
	var mobile Mobile = Mobile{}
	mobile.Product.Name = "Iphone 11"
	mobile.Product.Price = 1000
	mobile.Ram = 8
	mobile.SimCount = 1
}

همانطور که می بینید برای تعریف اسم موبایل باید بگوییم mobile.Product.Name که این زیاد جالب نیست. پس به این صورت ساختار Product را درون موبایل قرار می دهیم:

package main

type Product struct {
	Name  string
	Price int
}

type Mobile struct {
	Product
	Ram      int
	SimCount int
}

func main() {
	var mobile Mobile = Mobile{}
	mobile.Name = "Iphone 11"
	mobile.Price = 1000
	mobile.Ram = 8
	mobile.SimCount = 1
}

الان بصورت مستقیم می توانیم به فیلد های درون Product دسترسی داشته باشیم.

2.2.9 تعریف یک ساختار عمومی یا خصوصی (Public/Private) #

در زبان گو، چیزی به عنوان کلمه کلیدی public یا private جهت تعیین وضعیت دسترسی struct به بیرون وجود ندارد، در عوض کامپایلر گو بر اساس حرف بزرگ یا کوچک عنوان ساختار یا سایر تایپ‌ها، تشخیص می‌دهد تایپ شما عمومی است یا خصوصی. در صورتیکه شما حرف اول را کوچک قرار دهید تایپ شما بیرون از پکیج قابل دسترس نخواهد بود مثل مثال‌های بالا و اگر حرف اول تایپ رو بزرگ قرار دهید، تایپ یا تابع شما بیرون از پکیج نیز در دسترس خواهد بود. مثال تابع fmt.Println.

type Person struct {
    Name string
    age  int
}

type company struct {
    Name string
}
برای اطلاعات بیشتر بهتر است به بخش کپسوله سازی مراجعه کنید.

2.2.10 مقایسه ساختارها #

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

  • boolean
  • numeric
  • string
  • pointer
  • channel
  • interface types
  • structs
  • array

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

  • Slice
  • Map
  • Function
package main

import "fmt"

type employee struct {
    name   string
    age    int
    salary int
}

func main() {
    emp1 := employee{name: "Sam", age: 31, salary: 2000}
    emp2 := employee{name: "Sam", age: 31, salary: 2000}
    if emp1 == emp2 {
        fmt.Println("emp1 annd emp2 are equal")
    } else {
        fmt.Println("emp1 annd emp2 are not equal")
    }
}