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()
بیانیه. باز هم، از آنجایی که کانال بافر است و دارای سه مقدار است، این دریافتها مسدود نمیشوند. - با این حال، اگر بخواهیم مقادیر بیشتری را به/از کانال ارسال یا دریافت کنیم، مسدود می شود. به عنوان مثال، تلاش برای ارسال یک مقدار زمانی که بافر پر است یا دریافت یک مقدار زمانی که بافر خالی است، باعث میشود که گوروتین مربوطه مسدود شود تا زمانی که فضا در دسترس باشد یا مقداری ارسال شود. – کانال های بافر زمانی مفید هستند که می خواهید عملیات ارسال و دریافت را از نظر زمان بندی جدا کنید و به فرستنده و گیرنده اجازه می دهد تا به طور مستقل تا ظرفیت بافر کار کنند.