زبان برنامه نویسی Go — راهنمای شروع به کار

۱۰۹۳ بازدید
آخرین به‌روزرسانی: ۰۹ مهر ۱۴۰۲
زمان مطالعه: ۱۶ دقیقه
زبان برنامه نویسی Go — راهنمای شروع به کار

این نوشته را با یک مقدمه کوتاه در مورد زبان برنامه نویسی Go، یا به عبارت صحیح‌تر Golang، آغاز می‌کنیم. Go توسط تعدادی از مهندسان گوگل به نام‌های «روبرت گریزمر» (Robert Griesemer)، «راب پایک» (Rob Pike)، و «کن تامپسون» (Ken Thompson) طراحی شده است. Go یک زبان با نوع استاتیک و کامپایل شونده است. نسخه اول این زبان در تاریخ مارس 2012 به صورت اوپن‌سورس ارائه شده است.

997696

Go یک زبان برنامه‌نویسی اوپن‌سورس است که ساخت نرم‌افزار را به کاری ساده، مطمئن و کارآمد تبدیل می‌کند.

— مستندات GoLang

برای حل یک مسئله مشخص، در زبان‌های متفاوت، روش‌های گوناگونی وجود دارد. برنامه نویسان معمولاً وقت زیادی را صرف یافتن بهترین روش حل یک مسئله می‌کنند. در سوی دیگر Go اعتقادی به این تعدد امکانات ندارد و تنها یک راه برای حل مسئله می‌شناسد. این امر موجب می‌شود که زمان زیادی از برنامه‌نویسان صرفه‌جویی شود و نگهداری کدهای بزرگ ساده‌تر باشد. در زبان Go هیچ ویژگی پرهزینه‌ای مانند map یا filter وجود ندارد.

زمانی که یک ویژگی پرهزینه به زبان برنامه‌نویسی اضافه می‌کنید، باید بهای آن را بپردازید.

— راب پایک

لوگوی جدیداً منتشر شده Go
لوگوی جدیداً منتشر شده Go

سر آغاز

Go برای کار با بسته‌ها (Package) طراحی شده است. بسته main به کامپایلر Go اعلام می‌کند که برنامه به جای یک کتابخانه مشترک به صورت فایل اجرایی (Executable) کامپایل شده است. این نقطه ورودی برنامه است.

بسته main به صورت زیر تعریف می‌شود:

1package main

در ادامه با ایجاد یک فایل به نام main.go در فضای کاری Go، یک برنامه hello world ساده می‌نویسیم.

فضای کاری

فضای کاری در زبان Go به وسیله متغیر محیطی GOPATH تعریف می‌شود. هر کدی که می‌نویسید درون این فضای کاری قرار می‌گیرد. Go همه بسته‌ها را درون دایرکتوری GOPATH یا دایرکتوری GOROOT جستجو می‌کند که به طور پیش‌فرض در زمان نصب Go تعیین می‌شوند. GOROOT مسیری است که Go در آن نصب شده است.

شما می‌توانید دایرکتوری GOPATH را بنا به میل خود تنظیم کنید. فعلاً آن را به پوشه workspace/~ اضافه می‌کنیم:

# export env

export GOPATH=~/workspace

# go inside the workspace directory

cd ~/workspace

فایلی به نام main.go را با کد زیر درون پوشه فضای کاری که قبلاً ساخته‌ایم، ایجاد کنید.

!Hello World

1package main
2
3import (
4 "fmt"
5)
6
7func main(){
8  fmt.Println("Hello World!")
9}

در مثال فوق، fmt یک بسته درونی Go است که تابع‌هایی برای قالب‌بندی I/O پیاده‌سازی کرده است.

یک بسته را در Go می‌توان با استفاده از کلیدواژه Import، ایمپورت کرد. func main نقطه ورودی اصلی است که کد از آنجا شروع به اجرا می‌کند. Println تابعی درون بسته fmt است که عبارت «hello world» را به ما نمایش می‌دهد.

شیوه اجرای برنامه را با اجرا کردن این فایل مشاهده می‌کنیم. دو روش برای اجرای یک دستور Go دارد. چنان که می‌دانید Go یک زبان کامپایل شونده است و از این رو ابتدا و قبل از اجرای کد باید آن را کامپایل کنیم.

> go build main.go

بدین ترتیب یک فایل اجرایی به نام main ایجاد می‌شود که می‌توان با استفاده از دستور زیر آن را اجرا کرد:

>./main

# Hello World!

البته روش ساده‌تر دیگری برای اجرای برنامه نیز وجود دارد. دستور go run باعث می‌شود که گام کامپایل در پس زمینه اجرا شود. بدین ترتیب با استفاده از دستور زیر می‌توانید برنامه خود را اجرا کنید:

go run main.go

# Hello World!

توجه کنید که برای اجرا کردن کدهای معرفی شده در این نوشته می‌توانید از آدرس https://play.golang.org استفاده کنید.

متغیرها

متغیرها در زبان برنامه‌نویسی Go به صورت صریح اعلان می‌شوند. این بدان معنی است که Go یک زبان با «نوع‌بندی استاتیک» (statically typed) است، یعنی نوع همه متغیرها در زمان اعلان متغیر بررسی می‌شود. متغیرها را می‌توان به صورت زیر اعلان کرد:

1var a int

در این حالت مقدار به صورت 0 تنظیم می‌شود. با استفاده از ساختار زیر می‌توانید یک متغیر را با مقدار متفاوتی اعلان و مقداردهی اولیه بکنید:

1var a = 1

در این وضعیت، متغیر به صورت خودکار به نوع int تعیین می‌شود. می‌توان از یک تعریف خلاصه‌تر برای اعلان کردن متغیر به صورت زیر استفاده کرد:

1message:= "hello world"

همچنین می‌توانید متغیرهای چندگانه‌ای را در یک خط اعلان کنید:

1var b, c int = 2, 3

انواع داده

Go نیز مانند دیگر زبان‌های برنامه‌نویسی از انواع متفاوتی از ساختارهای داده‌ای پشتیبانی می‌کند. در ادامه برخی از آن‌ها را برسی می‌کنیم.

مقادیر عددی، رشته‌ای و بولی

  • در زبان Go برخی از انواع پشتیبانی شده برای ذخیره‌سازی اعداد به صورت int، int8، int16، int32، int64، uint، uint8، uint16، uint32، uint64، uintptr و غیره است.
  • انواع رشته‌ای درواقع یک توالی از بایت‌ها هستند. این نوع داده‌ها با استفاده از کلیدواژه String نمایش یافته و اعلان می‌شوند.
  • مقادیر بولی با استفاده از کلیدواژه bool ذخیره می‌شوند.
  • Go همچنین از انواع داده با نوع اعداد مختلط نیز پشتیبانی می‌کند که آن‌ها را می‌توان با استفاده از کلیدواژه‌های complex64 و complex128 اعلان کرد.
1var a bool = true
2
3var b int = 1
4
5var c string = 'hello world'
6
7var d float32 = 1.222
8
9var x complex128 = cmplx.Sqrt(-5 + 12i)

آرایه‌ها، Slice-ها و Map-ها

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

1var a [5]int

آرایه‌ها می‌توانند چندبعدی باشند. آرایه‌های چندبعدی را می‌توان به صورت زیر ایجاد کرد:

1var multiD [2][3]int

آرایه‌ها در مواردی که اندازه آرایه باید در زمان اجرا تغییر یابد با محدودیت مواجه می‌شوند. همچنین آرایه‌ها امکان ارائه یک زیرمجموعه از خود به صورت آرایه فرعی ندارند. به همین دلیل Go یک نوع داده به نام Slice دارد.

Slice

Slice-ها یک توالی از عناصر هستند که می‌توان در زمان اجرا آن‌ها را بسط داد. اعلان Slice مشابه اعلان آرایه است؛ تنها تفاوت این است که ظرفیت Slice در زمان اعلان مشخص نمی‌شود:

1var b []int

بدین ترتیب یک Slice با ظرفیت صفر و طول صفر ایجاد می‌شود. می‌توان در هنگام اعلان کردن Slice برای آن ظرفیت و طول نیز مشخص کرد. این وضعیت در مثال زیر ارائه شده است:

1numbers:= make([]int,5,10)

در این مثال، Slice دارای طول اولیه 5 و ظرفیت اولیه 10 است. Slice-ها در واقع تجریدی از آرایه به حساب می‌آیند. یک Slice شامل سه مؤلفه به صورت ظرفیت، طول و یک اشاره‌گر به آرایه زیرساختی خود است که در شکل زیر مشخص شده است:

slice

ظرفیت یک Slice را می‌توان با استفاده از یک تابع append یا copy افزایش داد. تابع append مقداری را به انتهای آرایه اضافه می‌کند و همچنین در صورت نیاز ظرفیت را نیز افزایش می‌دهد.

1numbers = append(numbers, 1, 2, 3, 4)

روش دیگر برای افزایش ظرفیت یک Slice از طریق استفاده از تابع copy است. کافی است به سادگی یک Slice دیگر با ظرفیت بالاتر ایجاد کنید و Slice اصلی را به Slice جدیداً ایجاد شده کپی کنید:

1// create a new slice
2number2:= make([]int, 15)
3// copy the original slice to new slice
4copy(number2, number)

ما می‌توانیم یک زیرمجموعه فرعی از یک Slice ایجاد کنیم. این کار از طریق استفاده از دستور زیر ممکن است:

1// initialize a slice with 4 len and values
2number2 = []int{1,2,3,4}
3fmt.Println(numbers) // -> [1 2 3 4]
4// create sub slices
5slice1:= number2[2:]
6fmt.Println(slice1) // -> [3 4]
7slice2:= number2[:3]
8fmt.Println(slice2) // -> [1 2 3]
9slice3:= number2[1:4]
10fmt.Println(slice3) // -> [2 3 4]

Map

Map یا نگاشت نیز یک نوع داده دیگر در زبان برنامه‌نویسی Go است که یک کلید را به یک مقدار نگاشت می‌کند. با استفاده از دستور زیر می‌توان یک map را اعلان کرد:

1var m map[string]int

در این کد m یک متغیر map جدید است که کلیدهای آن به صورت string و مقادیر آن به صورت integers است. می‌توان کلیدها و مقادیر را به سادگی به صورت زیر به یک map اضافه کرد:

1// adding key/value
2m['clearity'] = 2
3m['simplicity'] = 3
4// printing the values
5fmt.Println(m['clearity']) // -> 2
6fmt.Println(m['simplicity']) // -> 3

Typecasting

یک نوع داده را می‌توان با استفاده از cast کردن به نوع دیگری تبدیل کرد. در ادامه یک تبدیل نوع ساده را ملاحظه می‌کنید:

1a:= 1.1
2b:= int(a)
3fmt.Println(b)
4//-> 1

البته همه انواع داده را نمی‌توان به نوع‌های دیگر تبدیل کرد. باید اطمینان حاصل کنید که نوع داده با نوع تبدیل مطابقت دارد.

گزاره‌های شرطی

Go نیز مانند هر زبان دیگری دارای برخی ساختارهای شرطی برای اجرای مقایسه‌های مختلف است که در ادامه آن‌ها را بررسی می‌کنیم.

If else

در Go در مورد گزاره‌های شرطی می‌توان از گزاره‌های if-else به صورتی که در مثال زیر نمایش یافته است استفاده کنیم. نکته مهم این است که باید اطمینان حاصل کنید که آکولادها در همان خطی که شرط بیان شده قرار دارند:

1if num := 9; num < 0 {
2 fmt.Println(num, "is negative")
3} else if num < 10 {
4 fmt.Println(num, "has 1 digit")
5} else {
6 fmt.Println(num, "has multiple digits")
7}

Switch case

گزاره‌های Switch case به سازماندهی چندین گزاره شرطی مختلف کمک می‌کند. در مثال زیر یک گزاره Switch case ساده ارائه شده است:

1i := 2
2switch i {
3case 1:
4 fmt.Println("one")
5case 2:
6 fmt.Println("two")
7default:
8 fmt.Println("none")
9}

حلقه

Go یک کلیدواژه منفرد برای حلقه‌ها به صورت loop دارد. با استفاده از این دستور منفرد می‌توان انواع مختلفی از حلقه‌ها را کدنویسی کرد:

1i := 0
2sum := 0
3for i < 10 {
4 sum += 1
5  i++
6}
7fmt.Println(sum)

مثال فوق شبیه یک حلقه while در زبان برنامه‌نویسی C است. می‌توان از همین گزاره for برای یک حلقه for نرمال استفاده کرد:

1sum := 0
2for i := 0; i < 10; i++ {
3  sum += i
4}
5fmt.Println(sum)

در ادامه یک حلقه نامتناهی را در زبان برنامه‌نویسی Go مشاهده می‌کنید:

1for {
2
3}

اشاره‌گرها

Go از اشاره‌گرها نیز پشتیبانی می‌کند. منظور از اشاره‌گر مکانی از حافظه است که آدرس یک مقدار را نگهداری می‌کند. یک اشاره‌گر به وسیله * تعریف می‌شود. اشاره‌گر بر اساس نوع داده متناظر خود تعریف می‌شود. به مثال زیر توجه کنید:

1var ap *int

در این کد ap اشاره‌گری به یک نوع Integer است. می‌توان از عملگر & برای دریافت آدرس یک متغیر استفاده کرد.

1a:= 12
2
3ap = &a

مقدار مورد اشاره از سوی اشاره‌گر می‌تواند با استفاده از عملگر * مورد دسترسی قرار گیرد:

1fmt.Println(*ap)
2// => 12

اشاره‌گرها به طور معمول در مواردی که در حال ارسال یک struct به عنوان یک آرگومان هستیم یا زمانی که یک متد برای یک نوع تعریف شده اعلان می‌کنیم ترجیح داده می‌شوند.

  1. هنگام ارسال با مقدار، در واقع مقدار مورد نظر کپی می‌شود که به معنی نیاز به حافظه اضافی است.
  2. زمانی که مقداری با استفاده از اشاره‌گر ارسال می‌شود، مقدار تغییر یافته از سوی تابع در متد/تابع فراخوانی کننده بازتاب می‌یابد.

مثال

1func increment(i *int) {
2  *i++
3}
4func main() {
5  i := 10
6  increment(&i)
7  fmt.Println(i)
8}
9//=> 11

نکته: زمانی که کدهای نمونه معرفی شده در این نوشته را اجرا می‌کنید، دقت کنید که باید package main را نیز include کرده و بسته‌های دیگر را نیز بر اساس نیاز چنان که در مثال اول این نوشته به نام main.go نشان داده شده است، ایمپورت کنید.

تابع‌ها

تابع main که در بسته main تعریف شده است، نقطه ورودی برای اجرای یک برنامه Go محسوب می‌شود. تابع‌های دیگری را نیز می‌توان تعریف و استفاده کرد. به مثال ساده زیر توجه کنید:

1func add(a int, b int) int {
2  c := a + b
3  return c
4}
5func main() {
6  fmt.Println(add(2, 1))
7}
8//=> 3

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

1func add(a int, b int) (c int) {
2  c = a + b
3  return
4}
5func main() {
6  fmt.Println(add(2, 1))
7}
8//=> 3

در مثال فوق، c به صورت متغیر بازگشتی تعریف شده است. بنابراین متغیر c تعریف شده می‌تواند به صورت خودکار بدون نیاز به تعریف شدن در عبارت return در انتهای تابع بازگشت یابد.

همچنین می‌توان چندین مقدار بازگشتی از یک تابع داشت که با استفاده از کاما از هم جدا می‌شوند.

1func add(a int, b int) (int, string) {
2  c := a + b
3  return c, "successfully added"
4}
5func main() {
6  sum, message := add(2, 1)
7  fmt.Println(message)
8  fmt.Println(sum)
9}

متد، Struct و اینترفیس‌ها

Go یک زبان کاملاً شیءگرا نیست؛ اما با وجود struct-ها، interface-ها، و method-ها، این زبان برنامه‌نویسی پشتیبانی گسترده‌ای از برنامه‌نویسی شیءگرا به عمل آورده است.

Struct

Struct به مجموعه فیلدهای مختلفی گفته می‌شود. Struct برای گروه‌بندی داده‌ها با هم استفاده می‌شود. برای نمونه اگر بخواهیم داده‌های یک نوع Person را گروه‌بندی کنیم، می‌توانیم یک خصوصیت person تعریف کنیم که شامل نام، سن، و جنسیت وی باشد. Struct می‌تواند با استفاده از ساختار زیر تعریف شود:

1type person struct {
2  name string
3  age int
4  gender string
5}

زمانی که struct از نوع Person تعریف شد می‌توانیم یک person ایجاد کنیم:

1//way 1: specifying attribute and value
2p = person{name: "Bob", age: 42, gender: "Male"}
3//way 2: specifying only value
4person{"Bob", 42, "Male"}

با استفاده از یک نقطه (.) می‌توانیم به سادگی به این داده‌ها دسترسی داشته باشیم:

1p.name
2//=> Bob
3p.age
4//=> 42
5p.gender
6//=> Male

همچنین می‌توانید خصوصیت‌های یک struct را به صورت مستقیم با استفاده از اشاره‌گر آن مورد دسترسی قرار دهید:

1pp = &person{name: "Bob", age: 42, gender: "Male"}
2pp.name
3//=> Bob

متدها

متدها نوع خاصی از تابع به نام receiver هستند. یک receiver می‌تواند هم یک مقدار و هم یک اشاره‌گر باشد. در ادامه یک متد به نام ()describe ایجاد می‌کنیم که یک گیرنده از نوع person دارند. ما این نوع گیرنده را در مثال فوق ایجاد کردیم:

1package main
2import "fmt"
3
4// struct defination
5type person struct {
6  name   string
7  age    int
8  gender string
9}
10
11// method defination
12func (p *person) describe() {
13  fmt.Printf("%v is %v years old.", p.name, p.age)
14}
15func (p *person) setAge(age int) {
16  p.age = age
17}
18
19func (p person) setName(name string) {
20  p.name = name
21}
22
23func main() {
24  pp := &person{name: "Bob", age: 42, gender: "Male"}
25  pp.describe()
26  // => Bob is 42 years old
27  pp.setAge(45)
28  fmt.Println(pp.age)
29  //=> 45
30  pp.setName("Hari")
31  fmt.Println(pp.name)
32  //=> Bob
33}

همان طور که در مثال فوق می‌بینید، این متد را هم اینک می‌توان با استفاده از یک عملگر نقطه مانند pp.describe فراخوانی کرد. دقت کنید که receiver یک اشاره‌گر است. ما با استفاده از این اشاره‌گر یک ارجاع به مقدار مورد نظر را ارسال می‌کنیم و از این رو اگر هر تغییری در متد ایجاد شود در pp که receiver است نیز بازتاب می‌یابد. همچنین بدین ترتیب دیگر یک کپی دیگر از آن شیء ایجاد نمی‌کنیم و بنابراین در حافظه صرفه‌جویی می‌شود.

توجه داشته باشید که در مثال فوق، مقدار سن تغییری نیافته است؛ در حالی که مقدار نام تغییر پیدا کرده چون متد setName از نوع receiver است، در حالی که setAge از نوع اشاره‌گر است.

اینترفیس‌ها

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

1type animal interface {
2  description() string
3}

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

1package main
2
3import (
4  "fmt"
5)
6
7type animal interface {
8  description() string
9}
10
11type cat struct {
12  Type  string
13  Sound string
14}
15
16type snake struct {
17  Type      string
18  Poisonous bool
19}
20
21func (s snake) description() string {
22  return fmt.Sprintf("Poisonous: %v", s.Poisonous)
23}
24
25func (c cat) description() string {
26  return fmt.Sprintf("Sound: %v", c.Sound)
27}
28
29func main() {
30  var a animal
31  a = snake{Poisonous: true}
32  fmt.Println(a.description())
33  a = cat{Sound: "Meow!!!"}
34  fmt.Println(a.description())
35}
36
37//=> Poisonous: true
38//=> Sound: Meow!!!

در تابع main یک متغیر به نام a از نوع animal ایجاد کردیم. ما دو نوع snake (مار) و گربه (cat) را به اینترفیس animal انتساب دادیم و از Println برای نمایش a.description استفاده کردیم. از آنجا که متد describe را در هر دو نوع «مار» و «گربه» به روش‌های مختلفی پیاده‌سازی کرده‌ایم، می‌توانیم توضیح حیوان مورد نظر را نمایش دهیم.

بسته‌ها

ما همه کدهای خود را در Go درون بسته‌ها (package) می‌نویسیم. بسته main نقطه ورودی برای اجرای برنامه محسوب می‌شود. بسته‌های داخلی زیادی در Go وجود دارند. مشهورترین آن‌ها که قبلاً نیز استفاده کردیم بسته fmt است.

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

--- رابرت گریزمر

نصب یک بسته

go get <package-url-github>

// example

go get github.com/satori/go.uuid

بسته‌هایی که نصب می‌شوند درون محیط GOPATH ذخیره می‌شوند که همان دایرکتوری کاری ما محسوب می‌شود. با رفتن به درون پوشه pkg در داخل دایرکتوری کاری با استفاده از دستور زیر می‌توانید بسته‌های نصب شده را مشاهده کنید:

cd $GOPATH/pkg

ایجاد یک بسته سفارشی

کار خود را با ایجاد یک پوشه به نام custom_package آغاز می‌کنیم:

> mkdir custom_package

> cd custom_package

برای ایجاد یک بسته سفارشی باید ابتدا یک پوشه با نام بسته ایجاد کنیم. فرض کنید می‌خواهیم یک بسته به نام person بسازیم. به این منظور ابتدا یک پوشه به نام person درون پوشه custom_package ایجاد می‌کنیم.

> mkdir person

> cd person

سپس یک فایل به نام person.go درون این پوشه ایجاد می‌کنیم.

1package person
2func Description(name string) string {
3  return "The person name is: " + name
4}
5func secretName(name string) string {
6  return "Do not share"
7}

اکنون باید بسته را نصب کنیم تا بتوانیم در پروژه‌های خود آن را ایمپورت کرده و مورد استفاده قرار دهیم. بنابراین ابتدا آن را به صورت زیر نصب می‌کنیم:

> go install

سپس به پوشه custom_package بازگشته و یک فایل به نام main.go ایجاد می‌کنیم:

1package main
2import(
3  "custom_package/person"
4  "fmt"
5)
6func main(){ 
7  p := person.Description("Milap")
8  fmt.Println(p)
9}
10// => The person name is: Milap

در ادامه می‌توانیم بسته person را که ایجاد کردیم ایمپورت کنیم و از تابع dEscription استفاده کنیم. دقت کنید که تابع secretName که در بسته ایجاد کردیم قابل دسترسی نخواهد بود چون در زبان Go متدی که نام آن با حرف بزرگ آغاز نشود، به صورت خصوصی (private) تعریف می‌شود.

مستندات بسته

Go داری پشتیبانی داخلی برای ایجاد مستندات برای بسته‌ها است. دستور زیر را اجرا کنید تا مستندات بسته ایجاد شود:

godoc person Description

دستور فوق مستندات تابع Description را درون بسته person ایجاد می‌کند. برای مشاهده مستندات می‌توانید وب‌سرور را با استفاده از دستور زیر اجرا کنید:

godoc -http=":8080"

اینک به آدرس http://localhost:8080/pkg بروید و مستندات بسته‌ای که ایجاد کرده‌ایم را ببینید.

برخی بسته‌های داخلی در Go

در ادامه برخی بسته‌های پر استفاده داخلی Go را مشاهده می‌کنید:

Fmt

این بسته تابع‌های قالب‌بندی I/O را ارائه می‌کند. ما قبلاً از این بسته برای نمایش در stdout استفاده کرده‌ایم.

json

بسته مفید دیگری که در Go وجود دارد بسته json است. این بسته به کدگذاری/کدگشایی JSON کمک می‌کند. در ادامه مثالی از کدگذاری/کدگشایی برخی فایل‌های json را مشاهده می‌کنید:

کدگذاری (Encode)

1package main
2
3import (
4  "fmt"
5  "encoding/json"
6)
7
8func main(){
9  mapA := map[string]int{"apple": 5, "lettuce": 7}
10  mapB, _ := json.Marshal(mapA)
11  fmt.Println(string(mapB))
12}

کدگشایی (Decode)

1package main
2
3import (
4  "fmt"
5  "encoding/json"
6)
7
8type response struct {
9  PageNumber int `json:"page"`
10  Fruits []string `json:"fruits"`
11}
12
13func main(){
14  str := `{"page": 1, "fruits": ["apple", "peach"]}`
15  res := response{}
16  json.Unmarshal([]byte(str), &res)
17  fmt.Println(res.PageNumber)
18}
19//=> 1

در هنگام کدگشایی بایت json با استفاده از unmarshal، آرگومان اول همان بایت json و آرگومان دوم، آدرس struct نوع پاسخ است که می‌خواهیم json به آن نگاشت شود. دقت کنید که ”json:”page کلید page را به کلید PageNumber در struct نگاشت می‌کند.

مدیریت خطا

خطاها نتیجه نامطلوب و ناخواسته یک برنامه هستند. فرض کنید می‌خواهیم یک فراخوانی API به یک سرویس بیرونی داشته باشیم. این فراخوانی API می‌تواند موفق یا ناموفق باشد. خطاهای برنامه‌ها را در زبان Go می‌توان در مواردی که نوع خطا موجود باشد تشخیص داد. به مثال زیر توجه کنید:

1resp, err:= http.Get("http://example.com/")

در کد فوق فراخوانی API به شیء Error می‌تواند موفق یا ناموفق باشد. ما می‌توانیم بررسی کنیم که آیا خطا وجود دارد یا نه، و بر همان اساس پاسخ را مدیریت کنیم:

1package main
2
3import (
4  "fmt"
5  "net/http"
6)
7
8func main(){
9  resp, err := http.Get("http://example.com/")
10  if err != nil {
11    fmt.Println(err)
12    return
13  }
14  fmt.Println(resp)
15}

بازگشت دادن خطای سفارشی از یک تابع

زمانی که تابعی برای خودمان می‌نویسیم، مواردی وجود دارند که با خطا مواجه می‌شوند. این خطاها می‌توانند به کمک شیء خطا بازگشت یابند:

1func Increment(n int) (int, error) {
2  if n < 0 {
3    // return error object
4    return nil, errors.New("math: cannot process negative number")
5  }
6  return (n + 1), nil
7}
8func main() {
9  num := 5
10 
11  if inc, err := Increment(num); err != nil {
12    fmt.Printf("Failed Number: %v, error message: %v", num, err)
13  }else {
14    fmt.Printf("Incremented Number: %v", inc)
15  }
16}

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

Panic

Panic چیزی است که مدیریت نشده و در طی اجرای برنامه به صورت ناگهانی رخ می‌دهد. در زبان Go روشی ایده‌آل برای مدیریت استثناها در یک برنامه محسوب نمی‌شود. توصیه می‌شود که به جای آن از یک شیء خطا استفاده شود. زمانی که یک panic رخ می‌دهد، اجرای برنامه معلق می‌شود و در ادامه defer اجرا می‌شود.

Defer

Defer چیزی است که در انتهای هر تابع اجرا می‌شود.

1//Go
2package main
3
4import "fmt"
5
6func main() {
7    f()
8    fmt.Println("Returned normally from f.")
9}
10
11func f() {
12    defer func() {
13        if r := recover(); r != nil {
14            fmt.Println("Recovered in f", r)
15        }
16    }()
17    fmt.Println("Calling g.")
18    g(0)
19    fmt.Println("Returned normally from g.")
20}
21
22func g(i int) {
23    if i > 3 {
24        fmt.Println("Panicking!")
25        panic(fmt.Sprintf("%v", i))
26    }
27    defer fmt.Println("Defer in g", i)
28    fmt.Println("Printing in g", i)
29    g(i + 1)
30}

در مثال فوق، ما اجرای برنامه را با استفاده از ()panic به صورت panic درمی‌آوریم. همان طور که متوجه شدید، یک گزاره defer وجود دارد که باعث می‌شود اجرای برنامه در خط انتهایی تابع برنامه به آن واگذار شود. Defer می‌تواند در مواقع ضرورت که لازم است چیزی در انتهای تابع اجرا شود، مثلاً در مورد بستن یک فایل اجرا شود.

همروندی

Go با ذهنیت «همروندی» (concurrency) ساخته شده است. همروندی در Go از طریق روتین‌های Go به دست می‌آید که نخ‌های سبکی هستند.

روتین Go

روتین‌های Go تابع‌هایی هستند که می‌توانند به موازات یا همزمان با تابع دیگر اجرا شوند ایجاد یک روتین Go بسیار ساده است. کافی است کلیدواژه Go را در ابتدای تابع بیاورید و آن را به صورت موازی اجرا کنید. روتین‌های Go بسیار سبک هستند و از این رو می‌توانیم هزاران مورد از آن‌ها را ایجاد کنیم. به مثال ساده زیر توجه کنید:

1package main
2import (
3  "fmt"
4  "time"
5)
6func main() {
7  go c()
8  fmt.Println("I am main")
9  time.Sleep(time.Second * 2)
10}
11func c() {
12  time.Sleep(time.Second * 2)
13  fmt.Println("I am concurrent")
14}
15//=> I am main
16//=> I am concurrent

همان طور که در مثال فوق می‌بینید، تابع c یک روتین Go است که به موازات نخ اصلی Go اجرا می‌شود. مواردی وجود دارند که می‌خواهیم منابع را بین چندین نخ به اشتراک بگذاریم. Go ترجیح می‌دهد که متغیرهای یک نخ با نخ‌های دیگر به اشتراک گذارده نشوند، زیرا این کار موجب افزایش احتمال بن‌بست و انتظار منابع می‌شود. روش دیگری نیز برای اشتراک منابع بین روتین‌های Go وجود دارد که کانال نام دارد.

کانال‌ها

داده‌ها را می‌توان بین روتین‌های Go با استفاده از کانال به اشتراک گذاشت. هنگام ایجاد یک کانال لازم است که تعیین شود کانال چه نوع داده‌ای دریافت خواهد کرد. در ادامه یک کانال ساده با نوع رشته به صورت زیر ایجاد می‌کنیم:

1c:= make(chan string)

در این کانال، می‌توانیم داده‌هایی از نوع رشته ارسال کنیم. همچنین می‌توانیم داده‌ها را در این کانال هم ارسال و هم دریافت کنیم.

1package main
2
3import "fmt"
4
5func main(){
6  c := make(chan string)
7  go func(){ c <- "hello" }()
8  msg := <-c
9  fmt.Println(msg)
10}
11//=>"hello"

کانال‌های گیرنده تنها در انتظار دریافت داده‌ها از سوی فرستنده‌ها در کانال هستند.

کانال یک‌طرفه

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

1package main
2
3import (
4 "fmt"
5)
6
7func main() {
8 ch := make(chan string)
9 
10 go sc(ch)
11 fmt.Println(<-ch)
12}
13
14func sc(ch chan<- string) {
15 ch <- "hello"
16}

در مثال فوق، sc یک روتین Go است که می‌تواند تنها پیام‌ها را به کانال ارسال کند؛ اما نمی‌تواند پیام‌ها را دریافت کند.

سازماندهی چندین کانال برای یک روتین Go با استفاده از select

ممکن است چندین کانال باشند که یک تابع منتظر آن‌ها باشد. به این منظور می‌توانیم از یک گزاره select استفاده کنیم. در ادامه مثالی از این وضعیت ارائه کرده‌ایم تا این حالت روشن‌تر باشد:

1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func main() {
9 c1 := make(chan string)
10 c2 := make(chan string)
11 go speed1(c1)
12 go speed2(c2)
13 fmt.Println("The first to arrive is:")
14 select {
15 case s1 := <-c1:
16  fmt.Println(s1)
17 case s2 := <-c2:
18  fmt.Println(s2)
19 }
20}
21
22func speed1(ch chan string) {
23 time.Sleep(2 * time.Second)
24 ch <- "speed 1"
25}
26
27func speed2(ch chan string) {
28 time.Sleep(1 * time.Second)
29 ch <- "speed 2"
30}

در مثال فوق، main در دو کانال به نام‌های c1 و c2 منتظر است. با استفاده از گزاره select case، تابع main پیامی را نشان می‌دهد که از کانالی که اول دریافت می‌کند رسیده است.

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

مواردی نیز وجود دارند که نیاز داریم چندین داده را به یک کانال ارسال کنیم. می‌توان یک کانال بافر به این منظور ایجاد کرد. در یک کانال بافر شده، گیرنده پیام را تا زمانی که بافر پر نشده است، دریافت نمی‌کند. در ادامه مثالی از آن ارائه شده است:

1package main
2
3import "fmt"
4
5func main(){
6  ch := make(chan string, 2)
7  ch <- "hello"
8  ch <- "world"
9  fmt.Println(<-ch)
10}

سخن پایانی

ما در این نوشته با برخی اجزا و ویژگی‌های عمده Go آشنا شدیم:

  1. متغیرها، انواع داده
  2. آرایه‌ها، Slice-ها و map ها
  3. تابع‌ها
  4. حلقه‌ها و گزاره‌های شرطی
  5. اشاره‌گرها
  6. بسته‌ها
  7. متد، Struct، و اینترفیس
  8. مدیریت خطا
  9. همروندی – روتین‌های Go و کانال‌های آن

اینک شما درک نسبتاً خوبی از زبان برنامه‌نویسی Go به دست آورده‌اید.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۷ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
freecodecamp
۱ دیدگاه برای «زبان برنامه نویسی Go — راهنمای شروع به کار»

ممنون از توضیحاتتون

نظر شما چیست؟

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