مهندس نرم افزار و کارشناس ارشد مدیریت IT (کسب و کار الکترونیک)
خلاصه مختصر مفید GoLang (پارت سوم)
این مقاله پارت سوم از آموزش مختصر مفید GoLang هست. میتونین پارت اول رو اینجا و پارت دوم رو اینجا ببینید.
توی این سری آموزش (خلاصه مختصر مفید GoLang) قرار نبود و نیست که کاملاً وارد جزئیات بشیم به همین خاطر به نحوه نصب و راه اندازی و یسری نکات دیگه اشاره ای نشده و سعی میکنم فقط با مفاهیم مهم بصورت خلاصه آشنا بشیم.
توی پارت اول با مفاهیم کاملا پایه ای (مثل تعریف متغیر و توابع و ...) آشنا شدیم و توی پارت دوم اشاره گر رو خوندیم و یکم بیشتر با تعریف توابع و استفاده ازشون آشنا شدیم.
توی این پارت میخوایم برسیم به دو تا مبحثی که میشه یجورایی گفت اصلی ترین ویژگی های GoLang هستن. گو روتین ها و کانال ها
گو روتین ها
برای اینکه مفهوم گو روتین ها رو درک کنیم باید با نخ و چند نخی آشنا باشیم. دوستانی که با مفهوم thread و و multi thread آشنا نیستن، ما بعضی وقتا نیاز داریم تسک هامون بصورت Concurrency یا همزمان انجام بشن. در حقیقت وقتی یه پروسس رو اجرا میکنیم، میتونه چندین thread یا نخ داشته باشه. توی حالت عادی برنامه فقط یه thread داره و ما میتونیم برناممون رو با چند thread اجرا کنیم. و مفهومش اینه که یسری تسک ها رو موازی با همدیگه پیش ببریم. جدای از اینکه سخت افزاری که کد قراره روش اجرا بشه باید این قضیه رو ساپورت کنه (که اکثراً همیشه میکنن سخت افزارای امروزی) باید زبونی که باهاش کد میزنیم هم این روند رو ساپورت کنه (که بازم اکثر زبونایی که سری تو سرا در اوردن به شکل خیلی خوبی پشتیبانی میکنن).
البته ساختار گوروتینها و پیادهسازی همزمانی توی گولنگ کمی با چندنخی که توی یسری زبونا میبینیم متفاوته، برای مثال، گفته میشه یک گوروتین موقع ایجاد شدن فضایی حدود ۲ کیلوبایت رو بیشتر اشغال نمیکنه، و این علاوه بر اینکه بشدت کمتر از ایجاد نخ جدید توی سایر زبونا هست، کمک میکنه ما بتونیم هزاران گوروتین داشته باشیم بدون اینکه خیلی نگران کمبود منابع بشیم.
گو هم به خوبی از پس این داستان بر میاد. برای این بتونیم بعضی تسک ها رو موازی با بقیه تکسها اجرا کنیم، به راحتی بصورت زیر میتونیم این کار رو بکنیم:
اوپس.. همین؟! بله! به همین راحتی
تنها کاری که لازم داریم بکنیم اینه که زمان صدا زدن یه تابع، کلمه ی کلیدی go رو قبلش بنویسیم. با این کار GoLang یه به اصطلاح routine ایجاد کرده و تابعمون رو جداگانه و موازی با روند عادی برنامه، توی اون روتین اجرا میکنه. هر برنامه ی گو یه main routine داره و میتونه یعالمه اصطلاحا child routine هم داشته باشه که همه بصورت همزمان و موازی باهم تسک هایی رو انجام بدن که با اتمام کار main routine بقیه child routine ها هم میرن تو دیوار:
حالا میرسیم به جای جالبترِ داستان. بعضی زبونای برنامه نویسی اجازه میدن تسک هایی رو موازی با تسک دیگه اجرا کنی، ولی معمولا یا دسترسی مستقیمی بهش نداری، یا مدیریتشون سخته، یا اینکه اون نخ ها بصورت پیشفرض با حالت استانداردی نمیتونن مستقیم پیغامی برای همدیگه بفرستن.
حالا زبون گو ویژگی ای داره که این مسائل رو هم حل کرده، اگه گفتین چی؟ کانالها!
کانال ها
کانال ها ویژگی های باحالی دارن، مثل اینکه میتونیم به کمکشون منتظر بمونیم تا یه روتین یا چند تا روتین کارشون رو بکنن و به ما جواب بدن، یا باحالتر از اون اینه که کانالها میتونن پیام هایی با type دلخواه رو بین روتین ها جابجا کنن.
برای یه مثال ساده کد زیر رو ببینیم:
package main
import "fmt"
func main() {
c := make(chan string)
go func() {
c <- "Hello"
}()
fmt.Println(<-c)
}
توی کد بالا دستور
c := make(chan string)
به زبون گو میگیم که یه chan یا همون کانال از نوع string و به اسم c ایجاد کن (یعنی کانالی ایجاد کن که بین روتین ها بتونه string جابجا کنه).
func() {
c <- "Hello"
}()
این تیکه از کد یه تابعه و داخلش داره (با اون یه خط کد و ساختاری که میبینین) نوشته ی Hello رو میفرسته روی کانالی که ساختیم (و به کمک کلمه کلیدی go این کار رو داره جداگانه و بصورت همزمان روی یه روتین جدید انجام میده). و خط زیر
fmt.Println(<-c)
وقتی به c-> میرسه اگه پیغامی روی کانال c باشه برمیداره، اگه نباشه منتظر میمونه تا پیغامی روی c گذاشته بشه، و اون رو پاس میده به تابع fmt.Println برای نمایش به کاربر.
خب پس بصورت خلاصه یه کانال ایجاد کردیم، با ایجاد یه روتین جدید تابعی رو روی اون روتین ران کردیم و توی main routine برناممون اون رو گرفتیم و نشون دادیم.
میخوایم یه مثال دیگه ببینیم که به کمک اون بتونیم ببینیم که یسری لینک (مثلا چندتا سایت) رو میتونیم ببینیم یا نه (میخوایم یه درخواست http get بدیم ببینیم میتونیم جواب بگیریم یا نه)
links := []string{
"http://google.com",
"https://virgool.io",
"https://www.youtube.com",
}
خب، اول از همه باید چیکار کنیم؟ یه کانال میسازیم:
c := make(chan string)
یه تابع هم میسازیم که میخوایم توش چک کنیم ببینیم یه لینک به ما جواب میده یا نه:
func checkLink(link string, c chan string) {
_, err := http.Get(link)
if err == nil {
fmt.Println(link, " founded")
c <- link
} else {
fmt.Println(link, " not found!")
c <- link
}
}
برای استفاده از http.Get باید "net/http" رو به پروژه import کنیم که اگه از ادیتورهایی مثل ویژوال استودیو کد (VSCode) استفاده کنید خودش اتومات با سیو کردن کدتون، import های لازمه رو انجام میده.
با http.Get یه درخواست Get به لینکی که میخوایم میزنیم، و قسمت زیر:
_, err
برای اینه که این تابع دو تا چیز بر میگردونه، اولی جوابیه که از Get گرفته و دومی یه ارور. برای ما جوابی که میاد مهم نیست پس اولی رو _ گذاشتیم، برای ما فقط این مهم که ببینیم خطایی توی این درخواست رخ داده یا نه، اگه err برابر nil باشه یعنی تونستیم جواب بگیریم در غیر این صورت خطایی رخ داده و معنیش عدم دریافت جوابیه که ما میخواستیم.
و به کمک خط زیر تک تک لینک ها رو با go میفرستیم به این تابع:
for _, link := range links {
go checkLink(link, c)
}
و خیلی راحت با اجرا کردن این برنامه، متوجه میشیم که هیچ اتفاقی نمیفته!! یعنی چی؟ مفهومش اینه که ما به ازای هر لینک یه روتین ایجاد کردیم و اون روتین ها میخوان جوابی روی کانال بذارن، اما قبل از اینکه کار به گذاشتن پیامِ اونا برسه کار main routine تموم شده و اون child routine ها هم، قبل از گرفتن جواب، کارشون تمومه.
برای اینکه هم منتظر جواب روتین ها بمونیم، هم برنامه ای نوشته باشیم که همینجوری هی ارتباط رو تا بینهایت تست کنه، کد زیر رو اضافه میکنیم:
for l := range c {
go func() {
time.Sleep(5 * time.Second)
checkLink(l, c)
}()
}
نکته ای که اتفاق میفته دو تا چیزه، یکی خروجیه که به این شکله:
یکی دیگه اینکه توی بعضی نرم افزارا بصورت زیر
یه خطی چیزی یه خطایی رو مشخص میکنه که مثلا توی VSCode خطای زیر رو میگه بهمون:
خودمونیش اینکه داری اشتبا میزنی، بذارین اینجوری کدمون رو تصحیح کنیم:
for l := range c {
go func(link string) {
time.Sleep(5 * time.Second)
checkLink(link, c)
}(l)
}
که خروجی میشه این:
این شد چیزی که ما میخوایم. حالا دلیلش چیه؟ اینکه توی حالت اول، وقتی از توی for l := range c مستقیم l رو پاس میدیم به تابع checkLink، عملا زبون گو زمانی که داره روی c حلقه for رو اعمال میکنه، هر کدوم رو میریزه توی یه خونه از حافظه، و وقتی l رو پاس دادیم توی تابع checkLink از یجایی به بعد تکراری اون خونه از حافظرو ارسال میکنه. اما وقتی که حالت دوم رو پیاده کردیم، داریم به ازای هر بار فراخوانی تابع func، مقدار خودش رو دوباره پاس میدیم بهش و این لینک که درست هم هست به تابع checklink پاس داده میشه. اگه یکم پیچیدس حق دارین گیج بشین ولی یکم با تمرین و کدزنی متوجه میشین قضیه چیه. کد کامل این مثال اینجوری میشه:
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
links := []string{
"http://google.com",
"https://virgool.io",
"https://www.youtube.com",
}
c := make(chan string)
for _, link := range links {
go checkLink(link, c)
}
for l := range c {
go func(link string) {
time.Sleep(5 * time.Second)
checkLink(link, c)
}(l)
}
}
func checkLink(link string, c chan string) {
_, err := http.Get(link)
if err == nil {
fmt.Println(link, " founded")
c <- link
} else {
fmt.Println(link, " not found!")
c <- link
}
}
به کمک دستور select هم میتونین چندتا کانال ایجاد کنید و منتظر بمونید تا خروجی هاشون رو دریافت کنین و ... چیز به درد بخوریه و ساختار پیاده سازیش شبیه switch هست. در موردش بخونید حتما.
صرفا برای یه مثال، توی حالت زیر، ما یه تایمر ۳۰ ثانیهای تعریف میکنیم (با ایجاد تایمر ما عملا یه چنل ایجاد کردیم) و بعد از زمان تعیین شده، مقداری رو به select میده و ما میتونیم تصمیم بگیریم چیکار کنیم (توی کد پایین یه پیغام نشون میدیم و با صدا زدن return اجرا رو متوقف میکنیم):
timeout := time.After(30 * time.Second)
for {
select {
case l := <-c:
go func(link string) {
time.Sleep(5 * time.Second)
checkLink(link, c)
}(l)
case <-timeout:
fmt.Println("Timeout reached, exiting...")
return
}
}
منتشر شده در ویرگول توسط محمد قدسیان https://virgool.io/@mohammad.ghodsian
مطلبی دیگر از این انتشارات
ترکیب به جای وراثت در زبان Go
مطلبی دیگر از این انتشارات
چرا برای هندلکردن خطاها در گولنگ از RichError استفاده کنیم؟
مطلبی دیگر از این انتشارات
مدیریت وابستگی ها در زبان Go با استفاده از ماژول ها