مهندس نرم افزار و کارشناس ارشد مدیریت IT (کسب و کار الکترونیک)
خلاصه مختصر مفید GoLang (پارت دوم)
این مقاله پارت دوم از آموزش مختصر مفید GoLang هست. میتونین پارت اول رو اینجا ببینید.
پارت بعدی یا همون پارت سوم رو اینجا میتونید مطالعه کنید.
اشاره گر
توی مثالای پارت اول *argError رو دیدیم. زبون go از اشاره گر ها پشتیبانی میکنه. این عکس رو ببینیم:
دو مدل تایپ داریم. Reference Type و Value Type. شاید برای کسایی که با مفهوم اشاره گر دست و پنجه نرم نکردن یکمی درکش سخت باشه، ولی بذارین اینجوری بگم که هر متغیری یه جایی توی حافظه ذخیره میشه. انواع Value Type وقتی قراره به تابعی ارسال بشن به عنوان ورودی تابع، GoLang یه کپی ازشون یجا توی حافظه قرار میده و اون مقدار کپی رو میفرسته توی تابع. به این مفهوم که اگه توی بدنه تابع هر تغییری توی اون داده بشه فقط توی بدنه خود تابع (یا در حقیقت همون کپی ای که از اون مقدار گرفته شده) اعمال میشه. ولی انواعی که Reference Type هستن، خودشون یه اشاره گر هستن به یه خونه ی دیگه ی حافظه و وقتی به تابعی ارسال بشن، یه کپی از خودشون توی یه خونه دیگه حافظه قرار میگیره که مشابه حالت قبل هست با این تفاوت که خودش داره به یه خونه ی دیگه از حافظه اشاره میکنه، پس کپیش هم دوباره داره همون خونه از حافظه اشاره میکنه و معنیش اینه که وقتی تغییری بدیم عملاً داریم همون مقدار اصلی رو تغییر میدیم :دی
یکم شاید پیچیده باشه ولی واقعا سخته با متن بیان کردنش. عکس زیر رو ببینیم:
همونجوری که توی عکس بالا مشخصه، توی حالت سبز (بالایی)، داریم یه عدد به اسم a تعریف میکنیم که وصله به خونه ای توی حافظه به آدرس 0x0001 و مقدارش 10 هست. حالا وقتی a رو پاس بدیم به یه تابع، مقدار 10 کپی و فرستاده میشه (در عمل مفهومش اینه که هر تغییری بدیم فقط روی همون کپی عدد 10 که فرستادیم اعمال میشه نه روی جایی که a بهش وصله). اما توی حالت آبی (پایینی) داریم یه اشاره گر تعریف میکنیم که وصله به خونه 0x0005 حافظه و مقدارش برابره با 0x0001 (یعنی جایی که اون عدد ده توی حافظه وجود داره) حالا اگه ما این رو بفرستیم توی یه تابع، داریم یه کپی از آدرس خونه ای رو میفرستیم که عدد 10 توشه (خود 0x0001 رو) و هربار تغییرش به این مفهومه که برو تغییرت رو روی مقداری که توی توی خونه 0x0001 هست اعمال کن!
طبیعیه براتون اگه یکم گیج کنندس ولی برای مثال اگه یادمون باشه که (با توجه به دو تا عکس بالاتر) وقتی یه struct تعریف کردین و خواستین بفرستینش به یه تابع، اگه خواستین فقط مقادیرش رو ببینید معمولی بفرستید، ولی اگه خواستین تغییری توش اعمال کنین باید حتما اشاره گرش ارسال بشه.
با مثال زیر شاید بهتر درک کنیم موضوعو:
package main
import "fmt"
func zeroval(ival int) {
ival = 0
}
func zeroptr(iptr *int) {
*iptr = 0
}
func main() {
i := 1
fmt.Println("initial:", i)
zeroval(i)
fmt.Println("zeroval:", i)
zeroptr(&i)
fmt.Println("zeroptr:", i)
fmt.Println("pointer:", &i)
}
خروجی کد بالا بصورت زیر هست:
initial: 1
zeroval: 1
zeroptr: 0
pointer: 0x42131100
توی مثال بالا، (iptr *int) یعنی ورودی تابع یه اشاره گر هست، iptr=0* یعنی مقدار جایی که این اشاره گر بهش اشاره میکنه رو 0 کن و i& یعنی آدرس خونه ای از حافظه که i بهش اشاره میکنه. برای درک بهتر خروجی های کد بالارو خط به خط بررسی کنین.
شرط switch
بهتر بود توی پارت اول بیان میشد ولی خب جا افتاده و اینجا بهش میپردازیم.
بجای اینکه چندین مقاله بخوانیم مقاله های محمد قدسیان را چندین بار بخوا... نه چیزه، اشتباه شد (:دی) بجای اینکه چندین شرط داشته باشم، یک عملگر چند شرطی داشته باشیم:
whatAmI := func(i interface{}) {
switch t := i.(type) {
case bool:
fmt.Println("I'm a bool")
case int:
fmt.Println("I'm an int")
default:
fmt.Printf("Don't know type %T\n", t)
}
}
whatAmI(true)
whatAmI(1)
whatAmI("hey")
این کد سه تا چیز باحال بهمون یاد میده:
- یکی نحوه استفاده از switch case . جلوی switch یه مقداری میذاریم و بعد هر case اگه مقدارمون برابر با اون مقدار جلوی switch باشه مورد قبول واقع شده و دستوری که نوشتیم اجرا میشه. اگر هم هیچ کدوم نباشه میریم توی default.
- نکته ی دیگه اینکه وقتی جلوی switch نوشتیم i.(type) داریم میگیم زبون Go بیا نبگو متغیر i از چه نوعیه؟ و داخل case ها نوشتیم int و bool که نشون دهنده ی انواع عدد و بولین (true یا false) هستن.
- و سومی هم اینکه انواعی که داریم (مثل رشته و عدد و بولین) همشون نوعشون interface{} هست! یذره بیشتر در موردش سرچ کنین مطالب جالبی از نحوه پیاده سازی type ها دستگیرتون میشه
پارامتر تابعی
(یا هر اسم دیگه ای خودتون دوست دارین روش بذارین) به این مفهومه که من میتونم تابع رو به عنوان یه نوع بدم به یه متغیر. یعنی چی؟ مثال زیرو ببینیم:
func intSeq() func() int {
i := 0
return func() int {
i++
return i
}
}
با نوشتن این خطوط توی کد بالا
return func() int {
...
}
در حقیقت داریم میگیم که خروجی تابع ما خودش یه تابعه که داخلش یه کارایی میکنه. توی تعریف خود تابع اصلیمونم که اینجوری نوشتیم:
func intSeq() func() int
به این معنی که یه تابع دارم به اسم intSeq که خروجیش این هست:
func() int
یعنی خروجی تابع intSeq خودش یه تابعه به اسم func که یه عدد برمیگردونه. حالا چجوری ازش استفاده میکنیم؟
nextInt := intSeq()
fmt.Println(nextInt()) -> 1
fmt.Println(nextInt()) -> 2
fmt.Println(nextInt()) -> 3
newInts := intSeq()
fmt.Println(newInts()) -> 1
fmt.Println(newInts()) -> 2
حالا اگه دوباره nextInt رو صدا بزنیم:
fmt.Println(nextInt()) -> 4
و اگه دوباره newInts رو صدا بزنیم:
fmt.Println(newInts()) -> 2
متوجه شدین دیگه؟ یه nextInt داریم که هربار به این صورت:
nextInt()
صداش بزنیم میره داخل بدنه ی func و یدونه به i خودش اضافه میکنه. و newInts هم شرایط مشابهی داره با این تفاوت که هر کدوم i خودشون و تابع func جدای خودشون رو دارن.
تابع بازگشتی
مثل خیلی از زبونای برنامه نویسی، گو از توابع بازگشتی هم پشتیبانی میکنه. پیاده سازی تابع فاکتوریل بصورت زیر هست:
func fact(n int) int {
if n == 0 {
return 1
}
return n * fact(n-1)
}
یعنی اگه عدد 3 رو بدیم به تابع fact، خروجیش بصورت زیر هست:
return 3 * fact(3-1)
که خود (1-3)fact یا همون (2)fact این رو میخواد برگردونه:
return 2 * fact(2-1)
و (1-2)fact یا همون (1)fact هم میره داخل شرط 0 == if n و 1 رو برمیگردونه. یعنی در نهایت ما داریم:
3 * 2 * 1 = 6
مثالی از انواع توابع
توی پارت اول با توابع آشنا شدیم و فهمیدیم که میتونیم بصورت زیر تعریفشون کنیم:
type rect struct {
width, height int
}
func (r rect) perim() int {
return 2*r.width + 2*r.height
}
یه نوعی دیگه هم که دیدیم و به این صورت بود:
func funName() float64 {
return r.width * r.height
}
تفاوتشون هم این بود که توی حالت اول فقط انواعی که rect هستن میتونن perim رو صدا بزنن ولی توی حالت دوم هر جایی توی پکیجمون میتونیم صدا بزنیمش. با اینترفیس هم که آشنا شدیم، مثال زیر یه نمونه از کد تعریف دایره و مربعه به کمک چندین حالتی که دیدیم:
import "fmt"
import "math"
// Here's a basic interface for geometric shapes.
type geometry interface {
area() float64
perim() float64
}
// For our example we'll implement this interface on
// `rect` and `circle` types.
type rect struct {
width, height float64
}
type circle struct {
radius float64
}
// To implement an interface in Go, we just need to
// implement all the methods in the interface. Here we
// implement `geometry` on `rect`s.
func (r rect) area() float64 {
return r.width * r.height
}
func (r rect) perim() float64 {
return 2*r.width + 2*r.height
}
// The implementation for `circle`s.
func (c circle) area() float64 {
return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
return 2 * math.Pi * c.radius
}
// If a variable has an interface type, then we can call
// methods that are in the named interface. Here's a
// generic `measure` function taking advantage of this
// to work on any `geometry`.
func measure(g geometry) {
fmt.Println(g)
fmt.Println(g.area())
fmt.Println(g.perim())
}
func main() {
r := rect{width: 3, height: 4}
c := circle{radius: 5}
// The `circle` and `rect` struct types both
// implement the `geometry` interface so we can use
// instances of
// these structs as arguments to `measure`.
measure(r)
measure(c)
}
میتونید پارت سوم آموزش رو اینجا ببینید
منتشر شده در ویرگول توسط محمد قدسیان https://virgool.io/@mohammad.ghodsian
مطلبی دیگر از این انتشارات
جنریک ها در گولنگ
مطلبی دیگر از این انتشارات
ارثبری در Go؟ آشنایی با Composition در Golang.
مطلبی دیگر از این انتشارات
Go Developer Roadmap part 1