۰
مهندسین گوگل و برخی از ذهنهای برجسته حوزه علوم کامپیوتر — رب پایک (Rob Pike)، رابرت گریسمیر (Robert Griesemer) و کِن تامپسون (Ken Thompson)— در هنگام منتظر ماندن برای کامپایل دیگر برنامهها، زبان Go را توسعه دادند.
اگر به این افراد نگاهی بیندازید، به درک عمیقتری میرسید که چرا Go صرفا یک زبان برنامهنویسی دیگر نیست، چرا اهمیت دارد و چرا این تیم آن را توسعه داد. اگر علاقهای به مطالعه صفحات ویکیپدیا ندارید، ویدئوهای آنها درباره Go را در یوتیوب تماشا کنید.
اگرچه Go مثل یک چاقو سوئیسی سریع است و برای همزمانی (concurrency) و سیستمهای مدرن طراحی شده، برخلاف بسیاری زبانهای دیگر که ویژگیهای زیادی بهصورت مکرر اضافه میکنند و در نهایت اعمال مشابهی انجام میدهند و حتی برای ساخت سرویسهای وب در مقیاس بزرگ استفاده میشوند، تفاوت دارد. بهترین چیزی که من در Go دوست داشتم، سادگی برنامهنویسی آن است!
شرکتهای بسیاری از Go استفاده میکنند، از جمله: گوگل، اوبر، توییچ، دراپباکس، ساوندکلود، دیلیموشن، داکر و فهرست همچنان ادامه دارد…
هدف پروژه Go این بود که کندی و دستوپاگیری توسعه نرمافزار در گوگل را حذف کند و فرآیند را کاراتر و مقیاسپذیرتر سازد. این زبان برای افرادی طراحی شده که نرمافزارهای بزرگ مینویسند، میخوانند، اشکالزدایی (debug) میکنند و نگهداری میکنند.
زمانی که ساخت پروژهها کند است، فرصت فکر کردن وجود دارد. افسانه پیدایش Go میگوید که در یکی از آن ساختهای ۴۵ دقیقهای بود که زبان Go متولد شد. معتقد بودند ارزش دارد زبانی طراحی شود که برای نوشتن برنامههای بزرگ گوگل مثل وبسرورها مناسب باشد و با ملاحظات مهندسی نرمافزار، کیفیت زندگی برنامهنویسان گوگل را بهبود بخشد.
با وجود اینکه تاکنون تمرکز بحث روی وابستگیها بوده، مسائل دیگری نیز نیازمند توجه است. ملاحظات اصلی برای موفقیت هر زبان در این زمینه عبارتند از:
مقاله "Go در گوگل: طراحی زبان در خدمت مهندسی نرمافزار"
تمام مباحث شامل تئوری + مثالهای کد خواهند بود. برای جلوگیری از خستهکننده شدن مطالب، موضوعات مرتبط را در مقالات جداگانه تقسیم کردهام و شما آزادید مسیر مطالعه را دلخواه انتخاب کنید.
برنامه ساده Hello world که از پکیجهای fmt و uuid استفاده میکند.
مثالهای چاپ با fmt:
https://goplay.tools/snippet/rgsRbQT3r1X
مثال پکیج uuid:
https://goplay.tools/snippet/erRlOLWshvw
اعلان کوتاه متغیرها و استنتاج نوع:
https://goplay.tools/snippet/TCjYLSQ3Xlw
در مثال بالا، سه کار به صورت خلاصه انجام میشود: اعلان، مقداردهی و مقداردهی اولیه.
مثال بالا به فرم توسعه یافته:
https://goplay.tools/snippet/tiX10S1tVQU
نکته: در این مثال از %T
برای گرفتن نوع اصلی مقدار و %v
برای گرفتن مقدار استفاده شده است.
در Go، همه چیز به صورت پیشفرض مقدار صفر نوع خود را دارد:
https://goplay.tools/snippet/gN7dCJo1kYK
در این مثال عدد صحیح (int) و اعشاری (float) مساوی ۰، رشته خالی (string) و بول (bool) مقدار false دارند.
دامنه سطح بسته:
https://goplay.tools/snippet/BKlT1UKLWIZ
دامنه سطح بلاک (در مثال خطا داده، چون متغیر در بلاک main تعریف شده و به تابع foo پاس داده نشده):
https://goplay.tools/snippet/8PmvcQQL6GV
نکته: ترتیب اهمیت دارد در سطح بلاک، اما در سطح بسته اهمیتی ندارد:
https://goplay.tools/snippet/K5CuaNHYncC
در اینجا برای x خطا گرفتیم چون در سطح بلاک و با ترتیب نامناسب بود، اما برای y که در سطح بسته بود خیر.
مثال دیگر که خطا میدهد:
https://goplay.tools/snippet/nx8PTY_8jSL
در Go میتوانیم مقداری را که نمیخواهیم استفاده کنیم با قرار دادن _ به کامپایلر اطلاع دهیم و عملاً آن را رها کنیم.
مثال:
پکیج http و تابع Get آن پاسخ و خطا برمیگرداند. در اینجا خطا را رها کردیم (هرچند مدیریت خطا باید انجام شود):
1package main 2 3import ( 4 "fmt" 5 "io" 6 "net/http" 7) 8 9func main() { 10 resp, _ := http.Get("https://example.com/") 11 data, _ := io.ReadAll(resp.Body) 12 defer resp.Body.Close() // کلیدواژه defer: پیش از خروج از main اجرا میشود 13 fmt.Println(string(data)) 14}
مثال اعلان ثابتها در Go:
https://goplay.tools/snippet/yQLswQqJIEx
ثابت بدون نوع یک نوع پیشفرض دارد که وقتی نوعی لازم است به مقدار منتقل میکند.
مطالعه بیشتر در: وبلاگ راب پایک درباره ثابتها
اشارهگر به آدرس حافظه یک متغیر اشاره میکند. مثلاً اگر:
1var x int = 10 2var y *int = &x // اشاره به آدرس حافظه x
*int
یعنی اشارهگر به int، &x
آدرس حافظه x را میدهد. برای دسترسی به مقدار، از dereferencing با *y
استفاده میشود.
مثال:
https://goplay.tools/snippet/ZfopZcDXcan
انتقال داده بین توابع با و بدون اشارهگر:
https://goplay.tools/snippet/iwbwJOLut7q
حلقه for
در Go مشابه ولی متفاوت با C است. چهار حالت دارد:
1for init; condition; post { } // مشابه for در C 2for condition { } // مشابه while 3for { } // مثل for(;;) در C (حلقه بینهایت)
مثال توابع مختلف for:
https://goplay.tools/snippet/eFnBssYSw4u
مثال حلقههای تو در تو:
https://goplay.tools/snippet/yPkX2O9mZ57
مثال کاربردی برای یافتن عدد فرد یا زوج با for و if-else:
https://goplay.tools/snippet/FbYwjsslQ2i
Switch در Go به صورت پیشفرض break
ندارد (دیگر لازم نیست بعد از هر مورد استفاده شود مگر بخواهید زودتر از هر case خارج شوید).
مثال:
https://goplay.tools/snippet/ze2nd7-s2B1
Type switch:
یک سوئیچ میتواند برای شناسایی نوع داینامیک یک متغیر اینترفیس استفاده شود.
مثال:
https://goplay.tools/snippet/EhQjnfs5ves
در گذشته فقط ASCII داشتیم، یک مجموعه ۷ بیتی برای ۱۲۸ کاراکتر انگلیسی و نمادها، که کافی نبود.
برای حل این مشکل، Unicode اختراع شد. Unicode مجموعهای گستردهتر است که کاراکترهای دنیا را شامل میشود، هر کاراکتر را با یک عدد استاندارد به نام "Unicode Code Point" تعریف میکند، یا در Go به آن "Rune" میگویند.
Rune معادل int32
است، زیرا Go با کدگذاری UTF-8 کار میکند.
مثال پیدا کردن rune یک کاراکتر:
1package main 2 3import ( 4 "fmt" 5) 6 7func main() { 8 val := 'a' 9 fmt.Printf("Character: %c, Value: %v, Unicode: %U and Type: %T", val, val, val, val) 10}
خروجی:
Character: a, Value: 97, Unicode: U+0061 and Type: int32
عدد ۹۷ معادل کاراکتر 'a' در جدول Unicode است.
توابع بلوکهایی از کد هستند که عملیات خاصی انجام میدهند و میتوانند دوباره استفاده شوند، حافظه را ذخیره کرده، زمان برنامهنویسی را کاهش داده و خوانایی کد را افزایش دهند.
ساختار کلی تابع:
1func function_name(Parameter-list) (Return_type) { 2 // بدنه تابع 3}
توضیح کلمات کلیدی:
مثال جمع دو عدد:
https://goplay.tools/snippet/2O79-KztqyE
مثال الحاق نام و نام خانوادگی:
https://goplay.tools/snippet/3YwxoidRhsJ
توجه: در مثال بالا از Sprint
پکیج fmt استفاده شده که رشته نتیجه فرمتی را برمیگرداند.
توابعی که تعداد آرگومانهای ورودی متغیر دارند:
1func joinstr(elements ...string) string { 2 return strings.Join(elements, "-") 3}
در این تابع میتوان صفر یا چند رشته به آن داد و با - جدا کرده و رشته جدید میدهد.
مثال محاسبه میانگین با پارامترهای متغیر:
از for-range
برای پیمایش استفاده میشود:
1for idx, value := range data { 2 // عملیات 3}
کد کامل:
https://goplay.tools/snippet/gkWaQyCevNh
میتوان slice
را با استفاده از data...
به تابع فرستاد، مثلاً:
1package main 2 3import "fmt" 4 5func avgUtils(elements ...float64) float64 { 6 var sum float64 7 for _, val := range elements { 8 sum += val 9 } 10 return sum / float64(len(elements)) 11} 12 13func main() { 14 data := []float64{1, 2, 3, 4, 5} 15 result := avgUtils(data...) 16 fmt.Println(result) 17}
تابع ناشناس (anonymous function) را به متغیری نسبت میدهند و سپس صدا میزنند:
1package main 2 3import "fmt" 4 5func main() { 6 greet := func() { 7 fmt.Println("Hello World!") 8 } 9 greet() 10 fmt.Printf("Type of greet: %T", greet) 11}
خروجی:
1Hello World! 2func()
Closure نوع خاصی از تابع ناشناس است که به متغیرهای بیرونی دسترسی دارد.
1package main 2 3import "fmt" 4 5var counter int = 0 6 7func main() { 8 incVal := func() int { 9 counter++ 10 return counter 11 } 12 fmt.Println(incVal()) // 1 13 fmt.Println(incVal()) // 2 14}
در این حالت متغیر counter
بیرونی است و وقتی تابع اجرا میشود مقدارش تغییر میکند.
برای ایزوله کردن متغیر، آن را در تابع محصور میکنند:
1package main 2 3import "fmt" 4 5func counterUtils() func() int { 6 var counter int = 0 7 return func() int { 8 counter++ 9 return counter 10 } 11} 12 13func main() { 14 incVal := counterUtils() 15 fmt.Println(incVal()) // 1 16 fmt.Println(incVal()) // 2 17}
توابع را میتوان به عنوان آرگومان به دیگر توابع ارسال کرد.
مثال زیر تابعی میگیرد که عدد را چاپ میکند:
1package main 2 3import "fmt" 4 5func printNumbers(data []int, callbackFunc func(int)) { 6 for _, val := range data { 7 callbackFunc(val) 8 } 9} 10 11func main() { 12 data := []int{1, 2, 3, 4, 5} 13 printNumbers(data, func(val int) { 14 fmt.Println(val) 15 }) 16}
خروجی:
11 22 33 44 55
در Go، defer
اجرای تابع را به زمانی که بلاک تابع جاری یا تابع فراخواننده دارد تمام میشود معوق میکند.
در واقع آرگومانهای تابع فورا ارزیابی میشوند، اما اجرای تابع به تعویق میافتد تا قبل از خروج از تابع جاری.
مثال بدون defer:
1package main 2 3import "fmt" 4 5func sayHi() { 6 fmt.Println("Hi there") 7} 8 9func sayBye() { 10 fmt.Println("Bye now") 11} 12 13func main() { 14 sayHi() 15 sayBye() 16}
خروجی:
1Hi there 2Bye now
مثال با defer:
1package main 2 3import "fmt" 4 5func sayHi() { 6 fmt.Println("Hi there") 7} 8 9func sayBye() { 10 fmt.Println("Bye now") 11} 12 13func main() { 14 defer sayHi() // اجرای sayHi به پایان main موکول میشود 15 sayBye() 16}
خروجی:
1Bye now 2Hi there
1package main 2 3import ( 4 "fmt" 5 "os" 6) 7 8func main() { 9 f, err := os.Open("testfile.txt") 10 if err != nil { 11 fmt.Println(err) 12 return 13 } 14 defer f.Close() 15 // ... عملیات روی فایل f 16}
در اینجا فراخوانی defer f.Close()
باعث میشود فایل پس از اتمام کار بسته شود.
آرایهها توالی با طول ثابت از عناصر همنوع هستند.
نمونه آرایهای از ۵ عدد صحیح:
1package main 2 3import "fmt" 4 5func main() { 6 var arr [5]int 7 for i := 10; i < 15; i++ { 8 arr[i-10] = i 9 } 10 for idx := 0; idx < len(arr); idx++ { 11 fmt.Printf("Array has value: %v at index: %v\n", arr[idx], idx) 12 } 13}
چنانچه بیش از اندازه میخواهید به آرایه مقدار دهید، خطای index out of bound خواهید گرفت.
آرایهها به صورت ثابت و شمارش شده هستند.
Slice یک ساختار داده سبک، داینامیک با اندازه متغیر است که به یک آرایه زیرین اشاره دارد.
اعلان Slice:
1[]T 2[]T{} 3[]T{value1, value2, ..., valueN}
مثال:
1var my_slice []int 2var my_slice_1 = []string{"Geeks", "for", "Geeks"}
مثال چاپ Slice با حلقه for-range:
1package main 2 3import "fmt" 4 5func main() { 6 var my_slice = []string{"coffee", "code", "sleep"} 7 fmt.Println("My Slice data:", my_slice) 8 9 for _, val := range my_slice { 10 fmt.Println(val) 11 } 12}
Slice در واقع مرجعی به آرایه اصلی است و میتوان Slice جدید از Array یا دیگر Sliceها ساخت:
1my_slice := my_arr[low:high]
اگر محدوده low
یا high
مشخص نشوند، به ترتیب ۰ و طول آرایه به کار میروند.
مثال:
1package main 2 3import "fmt" 4 5func main() { 6 var my_arr = [5]string{"coffee", "code", "sleep", "eat", "gym"} 7 my_slice := my_arr[1:] 8 9 fmt.Println("Printing slice before updation:") 10 for _, val := range my_slice { 11 fmt.Println(val) 12 } 13 14 my_arr[4] = "workout" // تغییر در آرایه اصلی 15 fmt.Println("Printing slice after updation:") 16 for _, val := range my_slice { 17 fmt.Println(val) 18 } 19}
خروجی:
1Printing slice before updation: 2code 3sleep 4eat 5gym 6Printing slice after updation: 7code 8sleep 9eat 10workout
پس تغییر در آرایه اثرش روی Slice هم هست چون مرجع است.
تابع make
یک آرایه پشتیبان با ظرفیت مشخص و Slice با طول داده شده میسازد:
1var my_slice = make([]T, length, capacity)
مثال:
1package main 2 3import "fmt" 4 5func main() { 6 var my_slice = make([]int, 2, 5) 7 my_slice[0] = 10 8 my_slice[1] = 20 9 my_slice = append(my_slice, 30) 10 11 fmt.Println(my_slice) 12}
خروجی:
1[10 20 30]
زمانی که از ظرفیت آرایه زیرین فراتر میرویم، آرایه جدیدی با ظرفیت دو برابر ساخته میشود.
مثال:
1package main 2 3import "fmt" 4 5func main() { 6 var my_slice = make([]int, 2, 5) 7 my_slice[0] = 1 8 my_slice[1] = 2 9 10 fmt.Printf("make initial size of slice: %v and capacity of slice: %v\n", len(my_slice), cap(my_slice)) 11 12 for i := 3; i < 10; i++ { 13 my_slice = append(my_slice, i) 14 fmt.Printf("size of slice: %v and capacity of slice: %v\n", len(my_slice), cap(my_slice)) 15 } 16}
خروجی:
1make initial size of slice: 2 and capacity of slice: 5 2size of slice: 3 and capacity of slice: 5 3size of slice: 4 and capacity of slice: 5 4size of slice: 5 and capacity of slice: 5 5size of slice: 6 and capacity of slice: 10 6size of slice: 7 and capacity of slice: 10 7size of slice: 8 and capacity of slice: 10 8size of slice: 9 and capacity of slice: 10
1package main 2 3import "fmt" 4 5func main() { 6 var slice1 = []string{"eat", "sleep"} 7 var slice2 = []string{"code", "workout"} 8 9 slice1 = append(slice1, slice2...) 10 fmt.Println(slice1) 11 12 slice1 = append(slice1[0:1], slice1[2:]...) // حذف عنصر دوم 13 fmt.Println(slice1) 14}
خروجی:
1[eat sleep code workout] 2[eat code workout]
ساخت یک Slice که هر عضو آن خود Slice از رشتههاست:
1package main 2 3import "fmt" 4 5func main() { 6 my_slice := make([][]string, 0) 7 8 user1 := []string{"User1", "26", "8.5"} 9 user2 := []string{"User2", "32", "9.5"} 10 11 my_slice = append(my_slice, user1) 12 my_slice = append(my_slice, user2) 13 14 fmt.Println(my_slice) 15}
خروجی:
1[[User1 26 8.5] [User2 32 9.5]]
1var s []int
اینجا حافظهای اختصاص نیافته و s برابر nil است.
اما:
1s := make([]int, 0)
اینجا حافظه اختصاص یافته و یک Slice خالی ساخته شده است.
نقشهها نوع دادهای کارآمد و انعطافپذیر برای نگهداری جفتهای کلید-مقدار هستند.
اعلان نقشه:
1var myMap map[key_type]value_type
مثال:
1package main 2 3import "fmt" 4 5func main() { 6 my_map := map[int]string{ 7 1: "Dog", 8 2: "Cat", 9 3: "Cow", 10 4: "Bird", 11 5: "Rabbit", 12 } 13 fmt.Println(my_map) 14}
خروجی:
1map[1:Dog 2:Cat 3:Cow 4:Bird 5:Rabbit]
1my_map := make(map[int]string) 2my_map[0] = "coffee" 3my_map[1] = "code" 4my_map[2] = "sleep"
حلقه روی Map با for-range:
1for key, val := range my_map { 2 fmt.Printf("%v - %v\n", key, val) 3}
1val, ok := my_map[3] 2if !ok { 3 fmt.Println("key is not present") 4} else { 5 fmt.Println(val) 6}
1delete(my_map, 2)
Map نیز نوع داده مرجع (Reference) است. یعنی اگر یک Map به دیگری نسبت داده شود، هر دو به داده اصلی اشاره میکنند و تغییر یکی در دیگری دیده میشود.
مثال:
https://goplay.tools/snippet/U6lifv1BLaD
میتوان انواع داده سفارشی ساخت:
1type data int 2 3var age data = 26 4fmt.Printf("My age is of type: %T and value: %v", age, age)
خروجی:
My age is of type: main.data and value: 26
مثلاً Address:
1type Address struct { 2 name string 3 city string 4 state string 5 pincode int 6}
میتوان به صورت خلاصه:
1type Address struct { 2 name, city, state string 3 pincode int 4}
1var my_address = Address{ 2 name: "221-B", 3 city: "mohali", 4 state: "punjab", 5 pincode: 160061, 6}
دسترسی به میدانها:
1my_address.name 2my_address.state
1type Details struct { 2 name string 3 age int 4 gender string 5} 6 7type Student struct { 8 branch string 9 year int 10 Details // تودرتویی 11}
مثال:
1package main 2 3import "fmt" 4 5type Details struct { 6 name string 7 age int 8 gender string 9} 10 11type Student struct { 12 branch string 13 year int 14 Details 15} 16 17func main() { 18 student1 := Student{ 19 branch: "CSE", 20 year: 2018, 21 Details: Details{ 22 name: "User1", 23 age: 26, 24 gender: "Male", 25 }, 26 } 27 fmt.Println(student1) 28}
خروجی:
{CSE 2018 {User1 26 Male}}
فیلدهای Details به Student promote شدهاند و مستقیماً قابل دسترسیاند.
مثال:
1type T1 struct { 2 F1 int `json:"f_1"` 3}
تگها اطلاعات متا اضافه میکنند که توسط بستههای فعلی یا خارجی به کار گرفته میشوند.
مثال ترکیبی از دو struct:
1package main 2 3import "fmt" 4 5type details struct { 6 genre string 7 genreRating string 8 reviews string 9} 10 11type game struct { 12 name string 13 price string 14 details 15} 16 17func (d details) showDetails() { 18 fmt.Println("Genre:", d.genre) 19 fmt.Println("Genre Rating:", d.genreRating) 20 fmt.Println("Reviews:", d.reviews) 21} 22 23func (g game) show() { 24 fmt.Println("Name: ", g.name) 25 fmt.Println("Price:", g.price) 26 g.showDetails() 27} 28 29func main() { 30 action := details{"Action", "18+", "mostly positive"} 31 newGame := game{"XYZ", "$125", action} 32 33 newGame.show() 34}
خروجی:
1Name: XYZ 2Price: $125 3Genre: Action 4Genre Rating: 18+ 5Reviews: mostly positive
کتابخانه encoding/json
برای تبدیل دادهها به JSON و بالعکس استفاده میشود.
تابع Marshal:
1func Marshal(v any) ([]byte, error)
تابع Unmarshal:
1func Unmarshal(data []byte, v any) error
مثال:
1package main 2 3import ( 4 "encoding/json" 5 "fmt" 6 "log" 7) 8 9type Developer struct { 10 Name string `json:"dev_name"` 11 Age int `json:"dev_age"` 12 Activity string `json:"dev_activity"` 13} 14 15func main() { 16 user := Developer{ 17 Name: "Developer1", 18 Age: 26, 19 Activity: "Code", 20 } 21 22 result, err := json.Marshal(user) 23 if err != nil { 24 log.Fatal(err) 25 } else { 26 fmt.Println(string(result)) 27 } 28 29 var dev Developer 30 err = json.Unmarshal(result, &dev) 31 if err != nil { 32 log.Fatal(err) 33 } else { 34 fmt.Println(dev) 35 } 36}
خروجی:
1{"dev_name":"Developer1","dev_age":26,"dev_activity":"Code"} 2{Developer1 26 Code}
ممکن است با استفاده از Encoder و Decoder دادهها را روی جریان ورودی و خروجی پردازش کنیم.
مثال:
1package main 2 3import ( 4 "encoding/json" 5 "fmt" 6 "os" 7 "strings" 8) 9 10type Developer struct { 11 Name string `json:"dev_name"` 12 Age int `json:"dev_age"` 13 Activity string `json:"dev_activity"` 14} 15 16func main() { 17 user := Developer{ 18 Name: "Developer1", 19 Age: 26, 20 Activity: "Code", 21 } 22 23 json.NewEncoder(os.Stdout).Encode(user) 24 25 var dev Developer 26 rdr := strings.NewReader(`{"dev_name":"Developer2","dev_age":32,"dev_activity":"Sleep"}`) 27 json.NewDecoder(rdr).Decode(&dev) 28 fmt.Println(dev) 29}
در Go اینترفیسها مجموعهای از امضای متد هستند که به صورت انتزاعی (abstract) تعریف میشوند. نمیتوان نمونه (instance) از یک اینترفیس ساخت.
سینتکس:
1type interface_name interface{ 2 // امضای متدها 3}
در Go همه متدهای اینترفیس باید پیادهسازی شوند، اما نیازی به کلیدواژه خاصی نیست؛ پیادهسازی ضمنی است.
مثال:
1package main 2 3import ( 4 "fmt" 5 "math" 6) 7 8type Square struct { 9 side float64 10} 11 12type Circle struct { 13 radius float64 14} 15 16type Shape interface { 17 area() float64 18} 19 20func (sq Square) area() float64 { 21 return sq.side * sq.side 22} 23 24func (c Circle) area() float64 { 25 return math.Pi * c.radius * c.radius 26} 27 28func printArea(shape Shape) { 29 fmt.Println(shape.area()) 30} 31 32func main() { 33 s := Square{side: 10} 34 c := Circle{radius: 5} 35 printArea(s) 36 printArea(c) 37}
اینترفیس با صفر متد به نام empty interface شناخته و هر نوع دادهای آن را پیادهسازی میکند:
1interface{}
برای مرتبسازی میتوان اینترفیس مورد نیاز را پیادهسازی کرد:
1type Interface interface { 2 Len() int 3 Less(i, j int) bool 4 Swap(i, j int) 5}
میتوان با sort.Sort(data Interface)
مرتبسازی انجام داد.
همچنین میتوان با استفاده از sort.Slice
که نیاز به پیادهسازی اینترفیس ندارد دادهها را مرتب کرد:
1sort.Slice(x any, less func(i int, j int) bool)
مثال مرتبسازی با اینترفیس و sort.Sort
https://goplay.tools/snippet/3BFaiFepk8g
مثال مرتبسازی با sort.Slice
https://goplay.tools/snippet/kGoQQnx_RFC
1package main 2 3import ( 4 "fmt" 5 "sort" 6) 7 8type People []string 9 10func main() { 11 s := People{"Kohli", "Dhoni", "Sky", "Jadeja"} 12 13 sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }) 14 fmt.Println(s) 15 16 var result []string 17 for i := len(s) - 1; i >= 0; i-- { 18 result = append(result, s[i]) 19 } 20 fmt.Println(result) 21}
همزمانی به توانایی برنامه برای انجام چند عملیات به طور همزمان گفته میشود.
Go پشتیبانی کامل و قدرتمندی از همزمانی با goroutines و Channels دارد.
1package main 2 3import "fmt" 4 5func foo() { 6 for i := 0; i <= 50; i++ { 7 fmt.Printf("Foo data: %v\n", i) 8 } 9} 10 11func bar() { 12 for i := 51; i <= 100; i++ { 13 fmt.Printf("Bar data: %v\n", i) 14 } 15} 16 17func main() { 18 foo() 19 bar() 20}
در این حالت foo
و bar
به ترتیب اجرا میشوند.
goroutine مانند یک Thread بسیار سبک است. هزینه ساخت آن بسیار کمتر است. برنامه Go حداقل یک goroutine دارد (goroutine اصلی یا main). اگر main خاتمه یابد، همه goroutineها متوقف میشوند.
1package main 2 3import ( 4 "fmt" 5 "sync" 6 "time" 7) 8 9var wg = sync.WaitGroup{} 10 11func foo() { 12 for i := 0; i <= 100; i++ { 13 fmt.Printf("Foo data: %v\n", i) 14 time.Sleep(1 * time.Millisecond) 15 } 16 wg.Done() 17} 18 19func bar() { 20 for i := 101; i <= 200; i++ { 21 fmt.Printf("Bar data: %v\n", i) 22 time.Sleep(3 * time.Millisecond) 23 } 24 wg.Done() 25} 26 27func main() { 28 wg.Add(2) 29 30 go foo() 31 go bar() 32 33 wg.Wait() 34}
خروجی:
اعداد از foo و bar به صورت همزمان چاپ میشوند.
برای انتظار اتمام مجموعهای از goroutine ها استفاده میشود. تابع Add
شمارنده را افزایش میدهد، Done
آن را کاهش میدهد، و Wait
صبر میکند تا به صفر برسد.
اگر همه goroutineها انتظار یکدیگر را بکشند و هیچکدام اجرا نشود، ارور زیر رخ میدهد:
1fatal error: all goroutines are asleep - deadlock!
وقتی چند goroutine به طور همزمان به داده مشترک دسترسی داشته باشند و عملیات نادرستی اتفاق بیفتد.
مثال:
1package main 2 3import ( 4 "fmt" 5 "sync" 6 "time" 7) 8 9var wg sync.WaitGroup 10var counter int 11 12func printStuff(val string) { 13 for i := 1; i <= 25; i++ { 14 x := counter 15 x++ 16 time.Sleep(100 * time.Millisecond) 17 counter = x 18 fmt.Printf("%v has counter: %v\n", val, counter) 19 } 20 wg.Done() 21} 22 23func main() { 24 wg.Add(2) 25 go printStuff("foo") 26 go printStuff("bar") 27 wg.Wait() 28 fmt.Printf("Final value of counter: %v\n", counter) 29}
با اجرای برنامه زیر با پرچم -race
، race detector فعال میشود:
1go run -race source_file.go
اگر race باشد، پیغام "Found 1 data race(s)" دیده میشود.
برای جلوگیری از خطاهای همزمانی از sync.Mutex
استفاده میشود:
1package main 2 3import ( 4 "fmt" 5 "sync" 6 "time" 7) 8 9var wg sync.WaitGroup 10var mutex sync.Mutex 11var counter int 12 13func printStuff(val string) { 14 for i := 1; i <= 25; i++ { 15 mutex.Lock() 16 x := counter 17 x++ 18 time.Sleep(100 * time.Millisecond) 19 counter = x 20 fmt.Printf("%v has counter: %v\n", val, counter) 21 mutex.Unlock() 22 } 23 wg.Done() 24} 25 26func main() { 27 wg.Add(2) 28 go printStuff("foo") 29 go printStuff("bar") 30 wg.Wait() 31 fmt.Printf("Final value of counter: %v\n", counter) 32}
خروجی قابل اطمینان و بدون race خواهد بود.
کانالها واسطهای برای ارتباط بین goroutineهای همزمان هستند. با کانال میتوان دادهای را از یک goroutine ارسال و از goroutine دیگر دریافت کرد.
1var channel_name chan Type 2// یا 3channel_name := make(chan Type)
ارسال:
1channel <- value
دریافت:
1value := <-channel
هر دو عملیات به طور پیشفرض بلاک (مسدود) میشوند تا طرف مقابل آماده شود.
1package main 2 3import ( 4 "fmt" 5 "time" 6) 7 8func main() { 9 ch := make(chan int) 10 11 go func() { 12 for i := 1; i <= 5; i++ { 13 ch <- i 14 } 15 }() 16 17 go func() { 18 for { 19 fmt.Println(<-ch) 20 } 21 }() 22 23 time.Sleep(1000 * time.Millisecond) 24}
تابع close(channel)
کانال را میبندد و دیگر دادهای روی آن ارسال نمیشود.
مثال پاکسازی با for range:
1package main 2 3import "fmt" 4 5func foo(ch chan string) { 6 for i := 1; i <= 3; i++ { 7 ch <- "coffee & code" 8 } 9 close(ch) 10} 11 12func main() { 13 ch := make(chan string) 14 go foo(ch) 15 16 for val := range ch { 17 fmt.Println(val) 18 } 19}
خروجی سه بار "coffee & code" است و سپس برنامه تمام میشود.
میخواهیم دادهها را از چند منبع به یک کانال واحد منتقل کنیم.
کد عملی:
(با فایلهای متنی و خواندن در کانالها)
چندین goroutine داده را از یک کانال میخوانند تا کارها را موازی انجام دهند.
Go به جای exception و try-catch از مقدار بازگشتی برای خطاها استفاده میکند. خطا در Go مقدار error
است که اگر برابر با nil
باشد یعنی خطایی نیست.
1package main 2 3import ( 4 "log" 5 "os" 6) 7 8func main() { 9 _, err := os.ReadFile("myfile.txt") 10 if err != nil { 11 //fmt.Println(err) 12 //log.Println(err) 13 //log.Fatalln(err) 14 //panic(err) 15 } 16}
تعریف:
1func New(text string) error
مثال:
1package main 2 3import ( 4 "errors" 5 "fmt" 6 "log" 7) 8 9var ( 10 ErrInvalidDivideByZero = errors.New("error occurred, can't divide by 0") 11) 12 13func divideNums(num1, num2 float64) (float64, error) { 14 if num2 == 0 { 15 return 0, ErrInvalidDivideByZero 16 } 17 return num1 / num2, nil 18} 19 20func main() { 21 res, err := divideNums(10, 0) 22 if err != nil { 23 log.Fatal(err) 24 } else { 25 fmt.Println(res) 26 } 27}
fmt.Errorf
1return 0, fmt.Errorf("can't divide number %v with %v", num1, num2)
با این مباحث پایه، میتوانید پروژههای ساده Go بسازید، بستههای محبوب را یاد بگیرید، API توسعه دهید و نحوه ارتباط اجزا را بفهمید.
۰
کد با می متعهد است که بالاترین سطح کیفی آموزش را در اختیار شما بگذارد. هدف به اشتراک گذاشتن دانش فناوری اطلاعات و توسعه نرم افزار در بالاترین سطح ممکن برای درستیابی به جامعه ای توانمند و قدرتمند است. ما باور داریم هر کسی میتواند با استمرار در یادگیری برنامه نویسی چالش های خود و جهان پیرامون خود را بر طرف کند و به موفقیت های چشم گیر برسد. با ما در این مسیر همراه باشید. کد با می اجتماع حرفه ای برنامه نویسان ایرانی.