یادگیری Golang – از صفر تا قهرمان


۰


مهندسین گوگل و برخی از ذهن‌های برجسته حوزه علوم کامپیوتر — رب پایک (Rob Pike)، رابرت گریسمیر (Robert Griesemer) و کِن تامپسون (Ken Thompson)— در هنگام منتظر ماندن برای کامپایل دیگر برنامه‌ها، زبان Go را توسعه دادند.

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

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

شرکت‌های بسیاری از Go استفاده می‌کنند، از جمله: گوگل، اوبر، توییچ، دراپ‌باکس، ساوندکلود، دیلی‌موشن، داکر و فهرست همچنان ادامه دارد…

هدف پروژه Go این بود که کندی و دست‌وپاگیری توسعه نرم‌افزار در گوگل را حذف کند و فرآیند را کاراتر و مقیاس‌پذیرتر سازد. این زبان برای افرادی طراحی شده که نرم‌افزارهای بزرگ می‌نویسند، می‌خوانند، اشکال‌زدایی (debug) می‌کنند و نگهداری می‌کنند.

زمانی که ساخت پروژه‌ها کند است، فرصت فکر کردن وجود دارد. افسانه پیدایش Go می‌گوید که در یکی از آن ساخت‌های ۴۵ دقیقه‌ای بود که زبان Go متولد شد. معتقد بودند ارزش دارد زبانی طراحی شود که برای نوشتن برنامه‌های بزرگ گوگل مثل وب‌سرور‌ها مناسب باشد و با ملاحظات مهندسی نرم‌افزار، کیفیت زندگی برنامه‌نویسان گوگل را بهبود بخشد.

با وجود اینکه تاکنون تمرکز بحث روی وابستگی‌ها بوده، مسائل دیگری نیز نیازمند توجه است. ملاحظات اصلی برای موفقیت هر زبان در این زمینه عبارتند از:

  • باید در مقیاس بزرگ، برای برنامه‌های بزرگ با تعداد زیادی وابستگی، و تیم‌های برنامه‌نویسی متعدد کار کند.
  • باید آشنا و شبیه C باشد. برنامه‌نویسان گوگل معمولاً نفرات جوان و به زبان‌های رویه‌ای مثل خانواده C آشنا هستند، بنابراین زبان نباید خیلی رادیکال باشد تا سریع بتوانند بهره‌ور شوند.
  • باید مدرن باشد. زبان‌های قدیمی مثل C، C++ و حتی تا حدی جاوا قبل از ظهور ماشین‌های چند هسته‌ای، شبکه و توسعه اپلیکیشن‌های وب طراحی شده‌اند. ویژگی‌های جدیدی مثل هم‌زمانی داخلی بهتر هستند که در زبان‌های جدیدتر دیده می‌شود.

مقاله "Go در گوگل: طراحی زبان در خدمت مهندسی نرم‌افزار"


اینجا شروع Go است.

تمام مباحث شامل تئوری + مثال‌های کد خواهند بود. برای جلوگیری از خسته‌کننده شدن مطالب، موضوعات مرتبط را در مقالات جداگانه تقسیم کرده‌ام و شما آزادید مسیر مطالعه را دلخواه انتخاب کنید.


موضوعاتی که پوشش می‌دهیم:

  1. برنامه Hello World
  2. متغیرها، مقادیر و نوع‌ها (Variables, Values and Types)
  3. دامنه (Scope)
  4. Blank Identifier (شناسه خالی _)
  5. ثابت‌ها (Constants)
  6. اشاره‌گرها (Pointers)
  7. جریان کنترل (Control Flow)
  8. Rune (رون)
  9. توابع (Functions)
  10. کلیدواژه defer
  11. ساختارهای داده: آرایه (array)، قطعه (slice)، نقشه (map) و ساختار (struct)
  12. اینترفیس‌ها (Interfaces)
  13. همزمانی (Concurrency)
  14. کانال‌ها (Channels)
  15. مدیریت خطا (Error Handling)

بدون اتلاف وقت برای راه‌اندازی IDE (هرچند توصیه می‌کنم نصب GoLand یا VSCode و تنظیمات GoRoot و GoPath را بعدها انجام دهید، مثل نمونه تنظیم در GoLand روی مک)، می‌توانیم مستقیم با اجرای مثال‌ها در Go Playground یادگیری را شروع کنیم.


1. برنامه Hello World

برنامه ساده Hello world که از پکیج‌های fmt و uuid استفاده می‌کند.

مثال‌های چاپ با fmt:
https://goplay.tools/snippet/rgsRbQT3r1X

مثال پکیج uuid:
https://goplay.tools/snippet/erRlOLWshvw


2. متغیرها، مقادیر و نوع‌ها

اعلان کوتاه متغیرها و استنتاج نوع:
https://goplay.tools/snippet/TCjYLSQ3Xlw

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

مثال بالا به فرم توسعه یافته:
https://goplay.tools/snippet/tiX10S1tVQU

نکته: در این مثال از %T برای گرفتن نوع اصلی مقدار و %v برای گرفتن مقدار استفاده شده است.

در Go، همه چیز به صورت پیش‌فرض مقدار صفر نوع خود را دارد:
https://goplay.tools/snippet/gN7dCJo1kYK

در این مثال عدد صحیح (int) و اعشاری (float) مساوی ۰، رشته خالی (string) و بول (bool) مقدار false دارند.


3. دوسرحد بودن دامنه (Scope)

دامنه سطح بسته:
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


4. شناسه خالی (_) در Go

در 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}

5. ثابت‌ها (Constants)

مثال اعلان ثابت‌ها در Go:
https://goplay.tools/snippet/yQLswQqJIEx

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


6. اشاره‌گرها (Pointers)

اشاره‌گر به آدرس حافظه یک متغیر اشاره می‌کند. مثلاً اگر:

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


7. جریان کنترل (Control Flow)

حلقه 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


8. Rune

در گذشته فقط 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 است.


9. توابع (Functions)

توابع بلوک‌هایی از کد هستند که عملیات خاصی انجام می‌دهند و می‌توانند دوباره استفاده شوند، حافظه را ذخیره کرده، زمان برنامه‌نویسی را کاهش داده و خوانایی کد را افزایش دهند.

ساختار کلی تابع:

1func function_name(Parameter-list) (Return_type) {
2 // بدنه تابع
3}

توضیح کلمات کلیدی:

  • func: تعریف تابع
  • function_name: نام تابع
  • Parameter-list: نام و نوع پارامترها
  • Return_type: نوع داده‌های بازگشتی (اختیاری) — امکان استفاده از چند مقدار بازگشتی در Go وجود دارد

مثال جمع دو عدد:
https://goplay.tools/snippet/2O79-KztqyE

مثال الحاق نام و نام خانوادگی:
https://goplay.tools/snippet/3YwxoidRhsJ

توجه: در مثال بالا از Sprint پکیج fmt استفاده شده که رشته نتیجه فرمتی را برمی‌گرداند.


تابع با پارامترهای متغیر (Variadic Functions)

توابعی که تعداد آرگومان‌های ورودی متغیر دارند:

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 به توابع variadic

می‌توان 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}

توابع به صورت Expression و Closure

تابع ناشناس (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 در Go

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}

Callback در Go

توابع را می‌توان به عنوان آرگومان به دیگر توابع ارسال کرد.

مثال زیر تابعی می‌گیرد که عدد را چاپ می‌کند:

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

10. کلیدواژه defer در Go

در 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

استفاده از defer برای بستن فایل

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() باعث می‌شود فایل پس از اتمام کار بسته شود.


11. ساختارهای داده در Go

آرایه (Array)

آرایه‌ها توالی با طول ثابت از عناصر هم‌نوع هستند.

نمونه آرایه‌ای از ۵ عدد صحیح:

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 یک ساختار داده سبک، داینامیک با اندازه متغیر است که به یک آرایه زیرین اشاره دارد.

اعلان 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 هم هست چون مرجع است.


ساخت Slice با make()

تابع 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]

ظرفیت و رشد Slice

زمانی که از ظرفیت آرایه زیرین فراتر می‌رویم، آرایه جدیدی با ظرفیت دو برابر ساخته می‌شود.

مثال:

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

افزودن یک Slice به Slice دیگر و حذف عنصر

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 که هر عضو آن خود 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]]

نکته مهم درباره Slice

1var s []int

اینجا حافظه‌ای اختصاص نیافته و s برابر nil است.

اما:

1s := make([]int, 0)

اینجا حافظه اختصاص یافته و یک Slice خالی ساخته شده است.


نقشه‌ها (Maps) در Go

نقشه‌ها نوع داده‌ای کارآمد و انعطاف‌پذیر برای نگهداری جفت‌های کلید-مقدار هستند.

اعلان نقشه:

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]

ساخت Map با استفاده از make

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}

بررسی وجود کلید در Map

1val, ok := my_map[3]
2if !ok {
3 fmt.Println("key is not present")
4} else {
5 fmt.Println(val)
6}

حذف یک کلید از Map

1delete(my_map, 2)

توجه مهم:

Map نیز نوع داده مرجع (Reference) است. یعنی اگر یک Map به دیگری نسبت داده شود، هر دو به داده اصلی اشاره می‌کنند و تغییر یکی در دیگری دیده می‌شود.

مثال:
https://goplay.tools/snippet/U6lifv1BLaD


ساختارها (Structs) در Go

می‌توان انواع داده سفارشی ساخت:

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


تعریف struct

مثلاً 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}

ایجاد متغیر struct و مقداردهی

1var my_address = Address{
2 name: "221-B",
3 city: "mohali",
4 state: "punjab",
5 pincode: 160061,
6}

دسترسی به میدان‌ها:

1my_address.name
2my_address.state

تو در تو کردن Structها (Nesting)

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 شده‌اند و مستقیماً قابل دسترسی‌اند.


Tags در struct

مثال:

1type T1 struct {
2 F1 int `json:"f_1"`
3}

تگ‌ها اطلاعات متا اضافه می‌کنند که توسط بسته‌های فعلی یا خارجی به کار گرفته می‌شوند.


Composition در Go

مثال ترکیبی از دو 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

JSON در Go

کتابخانه 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}

JSON Encoder و Decoder

ممکن است با استفاده از 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}

12. اینترفیس‌ها (Interfaces)

در 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)

اینترفیس با صفر متد به نام empty interface شناخته و هر نوع داده‌ای آن را پیاده‌سازی می‌کند:

1interface{}

بسته sort

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

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}

13. همزمانی (Concurrency)

همزمانی به توانایی برنامه برای انجام چند عملیات به طور همزمان گفته می‌شود.

Go پشتیبانی کامل و قدرتمندی از همزمانی با goroutines و Channels دارد.


مثال بدون concurrency

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 چیست؟

goroutine مانند یک Thread بسیار سبک است. هزینه ساخت آن بسیار کمتر است. برنامه Go حداقل یک goroutine دارد (goroutine اصلی یا main). اگر main خاتمه یابد، همه goroutine‌ها متوقف می‌شوند.


همزمانی با goroutine و WaitGroup

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 به صورت همزمان چاپ می‌شوند.


WaitGroup چیست؟

برای انتظار اتمام مجموعه‌ای از goroutine ها استفاده می‌شود. تابع Add شمارنده را افزایش می‌دهد، Done آن را کاهش می‌دهد، و Wait صبر می‌کند تا به صفر برسد.


خطای deadlock:

اگر همه goroutine‌ها انتظار یکدیگر را بکشند و هیچ‌کدام اجرا نشود، ارور زیر رخ می‌دهد:

1fatal error: all goroutines are asleep - deadlock!

وضعیت رقابتی (Race Condition)

وقتی چند 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 با ابزار Go

با اجرای برنامه زیر با پرچم -race، race detector فعال می‌شود:

1go run -race source_file.go

اگر race باشد، پیغام "Found 1 data race(s)" دیده می‌شود.


جلوگیری از race با Mutex

برای جلوگیری از خطاهای همزمانی از 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 خواهد بود.


14. کانال‌ها (Channels)

کانال‌ها واسطه‌ای برای ارتباط بین goroutine‌های همزمان هستند. با کانال می‌توان داده‌ای را از یک goroutine ارسال و از goroutine دیگر دریافت کرد.


تعریف کانال با chan

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" است و سپس برنامه تمام می‌شود.


ارسال داده از چند کانال به یک کانال (Fan-In)

می‌خواهیم داده‌ها را از چند منبع به یک کانال واحد منتقل کنیم.

کد عملی:
(با فایل‌های متنی و خواندن در کانال‌ها)


Fan-Out

چندین goroutine داده را از یک کانال می‌خوانند تا کارها را موازی انجام دهند.


15. مدیریت خطا (Error Handling)

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}

افزودن Context به خطا با fmt.Errorf

1return 0, fmt.Errorf("can't divide number %v with %v", num1, num2)

توصیه بعد از این مباحث

با این مباحث پایه، می‌توانید پروژه‌های ساده Go بسازید، بسته‌های محبوب را یاد بگیرید، API توسعه دهید و نحوه ارتباط اجزا را بفهمید.

کانال
گوروتین
گو

۰


نظرات


author
نویسنده مقاله: علی اکبر ظهور

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

تمام حقوق این سایت متعلق به وبسایتcodebymeمیباشد.