برنامه نویسی

Mastering Go: آزادسازی پتانسیل روش ها، رابط ها، ژنریک ها و همزمانی | قسمت – 3

در دنیای زبان های برنامه نویسی، Golang (همچنین به عنوان Go شناخته می شود) به دلیل سادگی، کارایی و استحکام محبوبیت قابل توجهی به دست آورده است. Golang با نحو منحصربه‌فرد و ویژگی‌های قدرتمند خود، ابزار همه‌کاره‌ای را برای ساخت برنامه‌های کاربردی مقیاس‌پذیر و با کارایی بالا به توسعه‌دهندگان ارائه می‌دهد. در این وبلاگ، ما عمیقاً به برخی از جنبه های کلیدی نحو Golang، یعنی روش ها، رابط ها، ژنریک ها و همزمانی خواهیم پرداخت. بیایید هر موضوع را با جزئیات بررسی کنیم:

مواد و روش ها

  • برو کلاس نداره با این حال، شما می توانید روش ها را بر روی انواع تعریف کنید.
  • متد یک تابع با یک خاص است receiver بحث و جدل. = گیرنده در لیست آرگومان خودش بین func کلمه کلیدی و نام روش
  • در این مثال، Abs متد یک گیرنده از نوع دارد Vertex تحت عنوان v.
package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

// Remember: a method is just a function with a receiver argument.
// Here's Abs written as a regular function with no change in functionality.

func Abs_func(v Vertex) float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := Vertex{3, 4}
    fmt.Println(v.Abs())

    f := Vertex{12, 5}
    fmt.Println(Abs_func(f))
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

خروجی:

5
13
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

  • شما فقط می توانید یک متد را با گیرنده ای که نوع آن در همان بسته متد تعریف شده است، اعلام کنید. شما نمی توانید متدی را با گیرنده ای که نوع آن در بسته دیگری (که شامل انواع داخلی مانند int) تعریف شده است، اعلام کنید.
package main

import (
    "fmt"
    "math"
)

type MyFloat float64

func (f MyFloat) Abs() float64 {
    if f < 0 {
        return float64(-f)
    }
    return float64(f)
}

func main() {
    f := MyFloat(-math.Sqrt(2))
    fmt.Println(f)
    fmt.Println(f.Abs())
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

خروجی

-1.4142135623730951
1.4142135623730951
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

گیرنده های اشاره گر

  • Go اجازه می دهد تا روش ها را با گیرنده های اشاره گر اعلام کنند. گیرنده های اشاره گر دارای نحو *T هستند، که در آن T یک نوع است (نه خود یک نوع اشاره گر، مانند *int). این روش ها می توانند مقداری که گیرنده به آن اشاره می کند را تغییر دهند. در مقابل، روش‌های دارای گیرنده‌های ارزش بر روی یک کپی از مقدار اصلی عمل می‌کنند.
  • به عنوان مثال، متد Scale تعریف شده در را در نظر بگیرید *Vertex. اگر * از اعلان گیرنده حذف می شود، روش دیگر نمی تواند مقدار Vertex اصلی را تغییر دهد. گیرنده های اشاره گر معمولا زمانی استفاده می شوند که روش ها نیاز به تغییر گیرنده خود دارند.
  • حذف * از اعلان گیرنده رفتار را تغییر می دهد تا در عوض روی یک کپی از مقدار کار کند.
package main 

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

// Method With Pointer Receiver
func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
    fmt.Println(v.X,v.Y)
}

// Scale Function
func (v Vertex) Scale_(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
    fmt.Println(v.X,v.Y)
}

func main() {
    v := Vertex{3, 4}
    v.Scale(10)
    fmt.Println(v.X, v.Y)
    fmt.Println(v.Abs(),"\n")

    f := Vertex{3, 4}
    f.Scale_(10)
    fmt.Println(f.X, f.Y)
    fmt.Println(f.Abs())
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

خروجی:

30 40
30 40
50 

30 40
3 4
5
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

روش ها و جهت گیری اشاره گر

  • ممکن است متوجه شوید که توابع دارای آرگومان اشاره گر باید یک اشاره گر بگیرند:
var v Vertex
ScaleFunc(v, 5)  // Compile error!
ScaleFunc(&v, 5) // OK
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

  • در حالی که متدهای دارای گیرنده اشاره گر هنگام فراخوانی، یا یک مقدار یا یک اشاره گر را به عنوان گیرنده می گیرند:
var v Vertex
v.Scale(5)  // OK
p := &v
p.Scale(10) // OK
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

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

package main

import "fmt"

type Vertex struct {
    X, Y float64
}

func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func ScaleFunc(v *Vertex, f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func main() {
    v := Vertex{3, 4}
    v.Scale(2)
    fmt.Println(v)
    ScaleFunc(&v, 10)
    fmt.Println(v,"\n")

    p := &Vertex{3, 4}
    p.Scale(2)
    fmt.Println(p)
    ScaleFunc(p,10)
    fmt.Println(p)
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

خروجی:

{6 8}
{60 80} 

&{6 8}
&{60 80}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

  • چیزی معادل در جهت معکوس اتفاق می افتد.
  • توابعی که یک آرگومان مقدار می گیرند باید مقداری از آن نوع خاص داشته باشند:
var v Vertex
fmt.Println(AbsFunc(v))  // OK
fmt.Println(AbsFunc(&v)) // Compile error!
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

  • در حالی که متدهایی که دارای گیرنده های مقدار هستند، هنگام فراخوانی، یک مقدار یا یک اشاره گر را به عنوان گیرنده می گیرند:
var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

  • در این حالت، متد فراخوانی p.Abs() به صورت (*p).Abs تفسیر می شود.
package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func AbsFunc(v Vertex) float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := Vertex{3, 4}
    fmt.Println(v.Abs())
    fmt.Println(AbsFunc(v),"\n")

    p := &Vertex{3, 4}
    fmt.Println(p.Abs())
    fmt.Println(AbsFunc(*p))
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

خروجی:

5
5 

5
5
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

انتخاب یک گیرنده مقدار یا اشاره گر

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func (v *Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := &Vertex{3, 4}
    fmt.Printf("Before scaling: %+v, Abs: %v\n", v, v.Abs())
    v.Scale(5)
    fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs())
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

خروجی:

Before scaling: &{X:3 Y:4}, Abs: 5
After scaling: &{X:15 Y:20}, Abs: 25
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

رابط ها

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

package main

import (
    "fmt"
    "math"
)

// Shape is an interface for geometric shapes
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Rectangle represents a rectangle shape
type Rectangle struct {
    Width  float64
    Height float64
}

// Area calculates the area of the rectangle
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// Perimeter calculates the perimeter of the rectangle
func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// Circle represents a circle shape
type Circle struct {
    Radius float64
}

// Area calculates the area of the circle
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

// Perimeter calculates the circumference of the circle
func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

func main() {
    rect := Rectangle{Width: 5, Height: 3}
    circle := Circle{Radius: 2.5}

    shapes := []Shape{rect, circle}

    for _, shape := range shapes {
        fmt.Printf("Area: %f\n", shape.Area())
        fmt.Printf("Perimeter: %f\n", shape.Perimeter())
        fmt.Println("------------------")
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

خروجی:

Area: 15.000000
Perimeter: 16.000000
------------------
Area: 19.634954
Perimeter: 15.707963
------------------
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

استرینگرها

در برو، Stringer رابط یک رابط داخلی است که به انواع اجازه می دهد تا نمایش رشته خود را تعریف کنند. را String() روش از Stringer رابط یک نمایش رشته ای از نوع را برمی گرداند. در اینجا یک مثال از استفاده از Stringer رابط کاربری در Go:

package main

import "fmt"

type Person struct {
    Name string
    other string
}

func (p Person) String() string {
    return fmt.Sprintf("%v %v", p.Name, p.other)
}

func main() {
    a := Person{"Jay!", "Shiya Ram"}
    z := Person{"Jay Shree!", "Radhe Krishna"}
    fmt.Println(a, z)
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

خروجی:

Jay! Shiya Ram Jay Shree! Radhe Krishna
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

پارامترها را تایپ کنید

  • توابع Go را می توان برای کار بر روی چندین نوع با استفاده از پارامترهای نوع نوشت. پارامترهای نوع یک تابع در بین براکت ها، قبل از آرگومان های تابع ظاهر می شوند.
func Index[T comparable](s []T, x T) int
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

  • این اعلامیه به این معنی است s یک برش از هر نوع است T که محدودیت داخلی را برآورده می کند comparable. x نیز مقداری از همان نوع است.
  • comparable یک محدودیت مفید است که امکان استفاده از == و != عملگرها روی مقادیر نوع در این مثال، ما از آن برای مقایسه یک مقدار با تمام عناصر برش استفاده می کنیم تا زمانی که مطابقت پیدا شود. این Index تابع برای هر نوع که از مقایسه پشتیبانی می کند کار می کند.
package main

import "fmt"

// Index returns the index of x in s, or -1 if not found.
func Index[T comparable](s []T, x T) int {
    for i, v := range s {
        // v and x are type T, which has the comparable
        // constraint, so we can use == here.
        if v == x {
            return i
        }
    }
    return -1
}

func main() {
    // Index works on a slice of ints
    si := []int{10, 20, 15, -10}
    fmt.Println(Index(si, 15))

    // Index also works on a slice of strings
    ss := []string{"foo", "bar", "baz"}
    fmt.Println(Index(ss, "hello"))
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

خروجی:

2
-1
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

گوروتین ها

  • Goroutine یک رشته سبک وزن است که توسط زمان اجرا Go مدیریت می شود.
go f(x, y, z)
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

  • یک گوروتین جدید را شروع می کند
f(x, y, z)
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

  • ارزیابی f، x، y و z در گوروتین فعلی و اجرای f در گوروتین جدید اتفاق می‌افتد.
  • گوروتین ها در یک فضای آدرس اجرا می شوند، بنابراین دسترسی به حافظه مشترک باید همگام شود. بسته همگام‌سازی، اولیه‌های مفیدی را ارائه می‌کند، اگرچه در Go به آن‌ها نیاز زیادی نخواهید داشت، زیرا موارد اولیه دیگری نیز وجود دارند.
package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

خروجی:

hello
world
world
hello
world
hello
hello
world
world
hello
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

  • در این کد دو تابع داریم: say و main. را say تابع یک پارامتر رشته ای می گیرد s و آن را پنج بار با تأخیر 100 میلی ثانیه بین هر چاپ چاپ می کند.
  • در main تابع، ما یک گوروتین جدید را با فراخوانی راه اندازی می کنیم go say("world"). این بدان معنی است که say تابع با آرگومان “world” همزمان با گوروتین اصلی اجرا می شود.
  • به طور همزمان، گوروتین اصلی به اجرا و فراخوانی ادامه می دهد say("hello"). در نتیجه، “سلام” پنج بار در گوروتین اصلی چاپ می شود.
  • خروجی این برنامه به دلیل ماهیت همزمان گوروتین ها تا حدودی غیرقابل پیش بینی خواهد بود. ممکن است در هر اجرا متفاوت باشد، اما شما می‌توانید انتظار داشته باشید که پیام‌های “سلام” و “جهان” را مشاهده کنید.

کانال ها

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

1. ایجاد کانال:

  • برای ایجاد یک کانال، از تابع make با کلمه کلیدی chan و سپس نوع داده ای که کانال ارسال می کند استفاده می کنید. مثلا:
ch := make(chan int) // Creates an unbuffered channel of type int
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

2. عملیات کانال:

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

  • ارسال داده: برای ارسال داده از طریق کانال، از <- اپراتور در فرم channel <- value. مثلا:
ch <- 177 // Sends the value 177 into the channel 
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

  • Receiving Data: برای دریافت داده از یک کانال، از <- اپراتور در سمت چپ یک تکلیف. مثلا:
value := <-ch // Receives a value from the channel and assigns it to the variable "value"
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

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

package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum // send sum to c
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // receive from c

    fmt.Println(x, y, x+y)
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

خروجی:

-5 17 12
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

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

کانال های بافر شده

  • کانال ها را می توان بافر کرد. طول بافر را به عنوان آرگومان دوم برای مقداردهی اولیه یک کانال بافر ارائه کنید:
ch := make(chan int, 100)
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

  • فقط زمانی که بافر پر باشد به یک بلوک کانال بافر ارسال می شود. وقتی بافر خالی است بلوک را دریافت می کند.
package main

import (
    "fmt"
)

func main() {
    // Create a buffered channel with a capacity of 3
    ch := make(chan int, 3)

    // Send values to the channel
    ch <- 1
    ch <- 2
    ch <- 3

    // Attempting to send another value to the channel would block since the buffer is full

    // Receive values from the channel
    fmt.Println(<-ch)
    fmt.Println(<-ch)
    fmt.Println(<-ch)

    // Attempting to receive another value from the channel would block since the buffer is empty
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

خروجی:

1
2
3
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

  • در این مثال، یک کانال بافر ایجاد می کنیم ch با ظرفیت 3 با مشخص کردن ظرفیت به عنوان آرگومان دوم به make تابع.
  • سپس سه مقدار ارسال می کنیم (1, 2, and 3) به کانال با استفاده از <- اپراتور. از آنجایی که کانال دارای ظرفیت بافر 3 است، این ارسال ها مسدود نمی شوند.
  • پس از ارسال مقادیر، آنها را با استفاده از عبارت دریافت و چاپ می کنیم <- اپراتور و fmt.Println() بیانیه. باز هم، از آنجایی که کانال بافر است و دارای سه مقدار است، این دریافت‌ها مسدود نمی‌شوند.
  • با این حال، اگر بخواهیم مقادیر بیشتری را به/از کانال ارسال یا دریافت کنیم، مسدود می شود. به عنوان مثال، تلاش برای ارسال یک مقدار زمانی که بافر پر است یا دریافت یک مقدار زمانی که بافر خالی است، باعث می‌شود که گوروتین مربوطه مسدود شود تا زمانی که فضا در دسترس باشد یا مقداری ارسال شود. – کانال های بافر زمانی مفید هستند که می خواهید عملیات ارسال و دریافت را از نظر زمان بندی جدا کنید و به فرستنده و گیرنده اجازه می دهد تا به طور مستقل تا ظرفیت بافر کار کنند.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا