4.15 آموزش reflection

4.15 آموزش reflection

پکیج reflect در زبان Go، امکانات reflection زمان اجرا را فراهم می‌کند که به یک برنامه اجازه می‌دهد تا با اشیاءی با انواع دلخواه کار کند. استفاده اصلی این پکیج، گرفتن اطلاعات نوع پویای یک مقدار با نوع استاتیک interface{} با فراخوانی تابع TypeOf است که یک مقدار Type را برمی‌گرداند.

با فراخوانی تابع ValueOf، یک شیء Value که حاوی دادهٔ زمان اجراست، برمی‌گرداند. تابع Zero یک مقدار Type دریافت کرده و یک شیء Value را که مقدار صفر برای آن نوع است، برمی‌گرداند.

4.15.1 تعریف reflection و metaprogramming #

قبل از شروع آموزش، باید مفاهیم metaprogramming و reflection زمان اجرا را بفهمیم. می‌توانیم کدهای منبع خود را به دو شکل کد و داده در نظر بگیریم.

اگر کدهای منبع را به عنوان کد در نظر بگیریم، می‌توانیم آن‌ها را روی CPU اجرا کنیم.

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

metaprogramming به تکنیکی از برنامه نویسی گفته می‌شود که برنامه را به عنوان داده مورد بررسی قرار می‌دهد. تکنیک‌های metaprogramming می‌توانند برنامه‌های دیگر را بررسی و پردازش کنند، یا حتی در حین اجرای برنامه به خود برنامه دسترسی داشته باشند.

reflection زمان اجرا زیر مجموعه‌ای از الگوی metaprogramming است. تقریباً تمام زبان‌های محبوب، API داخلی را برای مدیریت metaprogramming برای زبان برنامه‌نویسی خود ارائه می‌دهند. این API ها به عنوان امکانات reflection زمان اجرا شناخته می‌شوند و به عنوان قابلیت زبان برنامه‌نویسی خاصی برای بررسی، تغییر و اجرای ساختار کد عمل می‌کنند.

بنابراین، ما می‌توانیم کارهایی مانند:

  • بررسی خصوصیات یک ساختار
  • بررسی وجود یک تابع در یک نمونه ساختار
  • بررسی نوع اتمی یک متغیر ناشناخته با API های reflection زمان اجرا را انجام دهیم.

حال به بررسی بیشتر اینکه این چگونه در زبان برنامه نویسی Go کار می‌کند، می‌پردازیم.

4.15.2 کاربردهای reflection #

مفهوم reflection به طور معمول یک API اصلی را برای بررسی یا تغییر برنامه فعلی ارائه می‌دهد. ممکن است فکر کنید که در مورد کد منبع برنامه خود آگاه هستید، پس چرا نیاز به بررسی کد نوشته شده خود با استفاده از reflection دارید؟ اما reflection دارای موارد کاربرد مفید زیادی است، که در زیر ذکر شده است:

  • برنامه‌نویسان می‌توانند از reflection استفاده کنند تا با کمترین کد، مشکلات برنامه‌نویسی را حل کنند. به عنوان مثال، اگر از یک نمونه ساختاری برای ساخت یک پرس و جوی SQL استفاده می‌کنید، می‌توانید با استفاده از reflection، فیلدهای ساختار را بدون هاردکد کردن نام هر فیلد ساختاری استخراج کنید.
  • با توجه به اینکه reflection یک روش برای بررسی ساختار برنامه ارائه می‌دهد، ممکن است با استفاده از آن، تحلیلگرهای کد استاتیکی ساخته شود.
  • با استفاده از API reflection، ما می‌توانیم کد را به صورت پویا اجرا کنیم. به عنوان مثال، شما می‌توانید متدهای موجود یک ساختار را پیدا کرده و با نام آن‌ها تماس بگیرید.

بخش آموزشی زیر همه اصول مورد نیاز برای پیاده‌سازی موارد کاربرد فوق را پوشش خواهد داد. همچنین، به شما نشان خواهم داد که چگونه می‌توانید یک برنامه shell ساده با API reflection بسازید.

اکنون که مفهوم reflection را پوشش دادیم، با مثال‌های عملی شروع کنیم.

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

5.15.3 بررسی تایپ های متغیرها #

در ابتدا، می‌توانیم با پکیج reflect، از بررسی نوع متغیرها برای شروع استفاده کنیم. کد زیر را ببینید که نوع چندین متغیر را چاپ می‌کند.

 1package main
 2
 3import (
 4	"fmt"
 5	"reflect"
 6)
 7
 8func main() {
 9	x := 10
10	name := "Go Lang"
11	type Book struct {
12		name   string
13		author string
14	}
15	sampleBook := Book{"Reflection in Go", "John"}
16	fmt.Println(reflect.TypeOf(x))          // int
17	fmt.Println(reflect.TypeOf(name))       // string
18	fmt.Println(reflect.TypeOf(sampleBook)) // main.Book
19}
1$ go run main.go
2int
3string
4main.Book

کد بالا نوع داده‌های متغیرها را با استفاده از تابع reflect.TypeOf چاپ می‌کند. تابع TypeOf یک نمونه reflection Type بازگردانده می‌کند که توابعی برای دسترسی به اطلاعات بیشتر درباره نوع فعلی فراهم می‌کند. برای مثال، می‌توانیم از تابع Kind برای بدست آوردن نوع ابتدایی یک متغیر استفاده کنیم. به خاطر داشته باشید که کد بالا نوع داده ساختار اختصاصی main.Book برای متغیر sampleBook را نشان می‌دهد - نه نوع ساختار ابتدایی.

برای بدست آوردن نوع ابتدایی، کد بالا را به صورت زیر تغییر دهید:

 1package main
 2
 3import (
 4	"fmt"
 5	"reflect"
 6)
 7
 8func main() {
 9	var (
10		str        = "Hello, world!"
11		num        = 42
12		flt        = 3.14
13		boo        = true
14		slice      = []int{1, 2, 3}
15		mymap      = map[string]int{"foo": 1, "bar": 2}
16		structure  = struct{ Name string }{Name: "John Doe"}
17		interface1 interface{} = "hello"
18		interface2 interface{} = &structure
19	)
20
21	fmt.Println(reflect.TypeOf(str).Kind())
22	fmt.Println(reflect.TypeOf(num).Kind())
23	fmt.Println(reflect.TypeOf(flt).Kind())
24	fmt.Println(reflect.TypeOf(boo).Kind())
25	fmt.Println(reflect.TypeOf(slice).Kind())
26	fmt.Println(reflect.TypeOf(mymap).Kind())
27	fmt.Println(reflect.TypeOf(structure).Kind())
28	fmt.Println(reflect.TypeOf(interface1).Kind())
29	fmt.Println(reflect.TypeOf(interface2).Kind())
30}
 1$ go run main.go
 2string
 3int
 4float64
 5bool
 6slice
 7map
 8struct
 9string
10ptr

دلیلی که در کد بالا برای سومین دستور چاپ، struct چاپ می‌شود، این است که تابع Kind reflection Type یک reflection Kind بازگردانده که اطلاعات نوع اولیه را نگه می‌دارد. در این حالت، reflection Kind نوع اولیه ساختار است.

5.15.3.1 اندازه تایپ های مقداردهی شده #

همچنین می‌توانیم از تابع Size reflection Type استفاده کنیم تا تعداد بایت‌های مورد نیاز برای ذخیره نوع فعلی را بدست آوریم. کد زیر را ببینید:

 1package main
 2
 3import (
 4	"fmt"
 5	"reflect"
 6)
 7
 8func main() {
 9	var (
10		str       = "Hello, world!"
11		num       = 42
12		flt       = 3.14
13		boo       = true
14		slice     = []int{1, 2, 3}
15		mymap     = map[string]int{"foo": 1, "bar": 2}
16		structure = struct{ Name string }{Name: "John Doe"}
17	)
18
19	fmt.Printf("Size of str: %d\n", reflect.TypeOf(str).Size())
20	fmt.Printf("Size of num: %d\n", reflect.TypeOf(num).Size())
21	fmt.Printf("Size of flt: %d\n", reflect.TypeOf(flt).Size())
22	fmt.Printf("Size of boo: %d\n", reflect.TypeOf(boo).Size())
23	fmt.Printf("Size of slice: %d\n", reflect.TypeOf(slice).Size())
24	fmt.Printf("Size of mymap: %d\n", reflect.TypeOf(mymap).Size())
25	fmt.Printf("Size of structure: %d\n", reflect.TypeOf(structure).Size())
26}

این کد، با استفاده از تابع Size reflection Type، تعداد بایت‌های مورد نیاز برای ذخیره هر نوع را چاپ می‌کند. با اجرای این کد، خروجی زیر را خواهید داشت:

1$ go run main.go
2Size of str: 16
3Size of num: 8
4Size of flt: 8
5Size of boo: 1
6Size of slice: 24
7Size of mymap: 8
8Size of structure: 0

در این کد، تعداد بایت‌های مورد نیاز برای نوع string 16 بایت، برای نوع int 8 بایت، برای نوع float64 8 بایت، برای نوع bool 1 بایت، برای نوع slice 24 بایت و برای نوع map 8 بایت است. برای نوع ساختاری structure بایتی نیاز نیست و برابر با صفر است.

5.15.4 بررسی مقدار یک متغیر #

قبلاً، اطلاعات نوع داده‌ها را بررسی کردیم. همچنین با استفاده از پکیج reflect، می‌توانیم مقادیر متغیرها را استخراج کنیم. کد زیر، مقادیر متغیرها را با استفاده از تابع reflect.ValueOf چاپ می‌کند:

 1package main
 2
 3import (
 4	"fmt"
 5	"reflect"
 6)
 7
 8func main() {
 9	var (
10		str       = "Hello, world!"
11		num       = 42
12		flt       = 3.14
13		boo       = true
14		slice     = []int{1, 2, 3}
15		mymap     = map[string]int{"foo": 1, "bar": 2}
16		structure = struct{ Name string }{Name: "John Doe"}
17	)
18
19	fmt.Printf("Value of str: %v\n", reflect.ValueOf(str))
20	fmt.Printf("Value of num: %v\n", reflect.ValueOf(num))
21	fmt.Printf("Value of flt: %v\n", reflect.ValueOf(flt))
22	fmt.Printf("Value of boo: %v\n", reflect.ValueOf(boo))
23	fmt.Printf("Value of slice: %v\n", reflect.ValueOf(slice))
24	fmt.Printf("Value of mymap: %v\n", reflect.ValueOf(mymap))
25	fmt.Printf("Value of structure: %v\n", reflect.ValueOf(structure))
26}

این کد، با استفاده از تابع reflect.ValueOf، مقادیر متغیرها را چاپ می‌کند. با اجرای این کد، خروجی زیر را خواهید داشت:

1$ go run main.go
2Value of str: Hello, world!
3Value of num: 42
4Value of flt: 3.14
5Value of boo: true
6Value of slice: [1 2 3]
7Value of mymap: map[bar:2 foo:1]
8Value of structure: {John Doe}

در این کد، مقادیر متغیرها با استفاده از تابع reflect.ValueOf چاپ می‌شوند. به خاطر داشته باشید که تابع ValueOf یک نمونه reflection Value بازگردانده می‌کند، که اطلاعات مربوط به مقدار و نوع متغیر را نگه‌داری می‌کند. برای چاپ مقدار واقعی، باید از توابع مربوط به reflection Value استفاده کنیم.

5.15.5 تغییر مقدار یک متغیر #

قبلاً، ساختار کد را با استفاده از چندین تابع در پکیج reflect بررسی کردیم. همچنین با استفاده از API بازتاب Go، امکان تغییر کد در حین اجرا وجود دارد. در کد زیر، نحوه به‌روزرسانی یک فیلد رشته‌ای در یک ساختار را مشاهده می‌کنید:

 1package main
 2
 3import (
 4	"fmt"
 5	"reflect"
 6)
 7
 8type Person struct {
 9	Name string
10	Age  int
11}
12
13func main() {
14	p := Person{Name: "John", Age: 30}
15	fmt.Println("Before update:", p)
16
17	v := reflect.ValueOf(&p)
18	if v.Kind() == reflect.Ptr {
19		v = v.Elem()
20	}
21
22	f := v.FieldByName("Name")
23	if f.IsValid() && f.CanSet() {
24		f.SetString("Jane")
25	}
26
27	fmt.Println("After update:", p)
28}

در این کد، یک ساختار به نام Person تعریف شده است که دو فیلد Name و Age دارد. در تابع main، یک نمونه از ساختار Person با مقدار پیش‌فرض Name: “John” و Age: 30 ایجاد شده است. سپس با استفاده از تابع reflect.ValueOf، نمونه ساختار Person به یک reflection Value تبدیل شده و با استفاده از تابع Kind، نوع آن بررسی می‌شود. اگر نوع نمونه یک اشاره‌گر باشد، با استفاده از تابع Elem، به مقدار اشاره شده تبدیل می‌شود.

در ادامه، با استفاده از تابع FieldByName، فیلد Name در نمونه ساختار Person بدست آورده می‌شود. سپس با استفاده از تابع IsValid بررسی می‌شود که آیا فیلد موجود است یا خیر. در صورت وجود، با استفاده از تابع CanSet بررسی می‌شود که آیا می‌توان آن را تغییر داد یا خیر. در صورت امکان تغییر، با استفاده از تابع SetString، مقدار فیلد Name به “Jane” تغییر می‌یابد.

در نهایت، با چاپ دوباره مقدار نمونه ساختار Person، تغییر در فیلد Name را مشاهده می‌کنیم. با اجرای این کد، خروجی زیر را خواهید داشت:

1$ go run main.go
2Before update: {John 30}
3After update: {Jane 30}

در این حالت، با استفاده از پکیج reflect، می‌توانیم برنامه را در حین اجرا تغییر داده و به داده‌های موجود در حافظه دسترسی پیدا کنیم.

5.15.6 بررسی اطلاعات یک struct #

بیایید یک کد نمونه برای بررسی همه فیلدهای یک ساختار بنویسیم. در طول بررسی، می‌توانیم نام و مقدار هر فیلد ساختار را نمایش دهیم. کد زیر این کار را انجام می‌دهد:

 1package main
 2
 3import (
 4	"fmt"
 5	"reflect"
 6)
 7
 8type Person struct {
 9	Name    string
10	Age     int
11	Address string
12}
13
14func main() {
15	p := Person{Name: "John", Age: 30, Address: "123 Main St."}
16
17	v := reflect.ValueOf(p)
18	if v.Kind() == reflect.Ptr {
19		v = v.Elem()
20	}
21
22	for i := 0; i < v.NumField(); i++ {
23		field := v.Field(i)
24		fmt.Printf("Field %d: %s = %v\n", i, v.Type().Field(i).Name, field.Interface())
25	}
26}

در این کد، یک ساختار به نام Person تعریف شده است که دارای سه فیلد Name، Age و Address است. در تابع main، یک نمونه از ساختار Person با مقدار پیش‌فرض Name: “John”، Age: 30 و Address: “123 Main St.” ایجاد شده است.

سپس با استفاده از تابع reflect.ValueOf، نمونه ساختار Person به یک reflection Value تبدیل شده و با استفاده از تابع Kind، نوع آن بررسی می‌شود. اگر نوع نمونه یک اشاره‌گر باشد، با استفاده از تابع Elem، به مقدار اشاره شده تبدیل می‌شود.

در ادامه، با استفاده از تابع NumField، تعداد فیلدهای موجود در نمونه ساختار Person بدست آورده می‌شود. سپس در یک حلقه، با استفاده از تابع Field، مقدار هر فیلد به همراه نام آن چاپ می‌شود. با استفاده از تابع Type، نوع نمونه ساختار Person به دست می‌آید، و با استفاده از تابع Field(i).Name، نام فیلد در ایندکس i بدست می‌آید. در نهایت، با استفاده از تابع Interface، مقدار فیلد به صورت یک interface{} برگردانده می‌شود و چاپ می‌شود.

با اجرای این کد، خروجی زیر را خواهید داشت:

1$ go run main.go
2Field 0: Name = John
3Field 1: Age = 30
4Field 2: Address = 123 Main St.

در این حالت، با استفاده از پکیج reflect، می‌توانیم برای هر ساختار، همه فیلدها را بررسی کرده و نام و مقدار هر فیلد را چاپ کنیم.

5.15.7 بررسی متدها (Methods) #

فرض کنید شما یک موتور دستور سفارشی برای یک برنامه شل پیاده‌سازی می‌کنید و برای اجرای توابع Go بر اساس دستورات ورودی کاربر، نیاز دارید دستورات را به توابع مرتبط تخصیص دهید. اگر تعداد توابع کم باشد، می‌توانید از یک switch-case statement استفاده کنید. اما اگر تعداد توابع صد‌ها نفر باشد؟ در این صورت، ما می‌توانیم توابع Go را براساس نام آن‌ها به صورت پویا فراخوانی کنیم. برنامه شل پایه‌ای زیر با استفاده از بازتاب این کار را انجام می‌دهد:

 1package main
 2import (
 3    "fmt"
 4    "reflect"
 5    "bufio"
 6    "os"
 7)
 8type NativeCommandEngine struct{}
 9func (nse NativeCommandEngine) Method1() {
10    fmt.Println("INFO: Method1 executed!")
11}
12func (nse NativeCommandEngine) Method2() {
13    fmt.Println("INFO: Method2 executed!")
14}
15func (nse NativeCommandEngine) callMethodByName(methodName string) {
16    method := reflect.ValueOf(nse).MethodByName(methodName)
17    if !method.IsValid() {
18        fmt.Println("ERROR: \"" + methodName + "\" is not implemented")
19        return
20    }
21    method.Call(nil)
22}
23func (nse NativeCommandEngine) ShowCommands() {
24    val := reflect.TypeOf(nse)
25    for i := 0; i < val.NumMethod(); i++ {
26        fmt.Println(val.Method(i).Name)
27    }
28}
29func main() {
30    nse := NativeCommandEngine{}
31    fmt.Println("A simple Shell v1.0.0")
32    fmt.Println("Supported commands:")
33    nse.ShowCommands()
34    scanner := bufio.NewScanner(os.Stdin)
35    fmt.Print("$ ")
36    for scanner.Scan() {
37        nse.callMethodByName(scanner.Text()) 
38        fmt.Print("$ ")
39    }
40}
1$ go run main.go
2A simple Shell v1.0.0
3Supported commands:
4Method1
5Method2
6ShowCommands
7$

برنامه شلی که پیشتر نوشتیم، ابتدا تمام دستورات پشتیبانی شده را نشان می‌دهد. سپس کاربر می‌تواند دستورات را به دلخواه خود وارد کند. هر دستور شل یک متد متناظر دارد، و اگر یک متد خاص وجود نداشته باشد، شل پیام خطا چاپ می‌کند.

5.15.8 نوشتن custom tag برای فیلد های ساختار #

تگ سفارشی مانند json:"name" در گو، برای اتصال متاداده به فیلدهای یک ساختار استفاده می‌شود. بسته reflect در گو، یک راه برای دسترسی به این تگ‌ها در زمان اجرا فراهم می‌کند. برای ایجاد یک تگ سفارشی در گو، می‌توان از بسته reflect برای دسترسی به تگ‌ها بر روی یک فیلد ساختار استفاده کرد.

در ادامه مثالی از چگونگی ایجاد یک تگ سفارشی با بسته reflect در گو آورده شده است:

 1package main
 2
 3import (
 4	"fmt"
 5	"reflect"
 6)
 7
 8type Person struct {
 9	Name string `customtag:"myname"`
10	Age  int    `customtag:"myage"`
11}
12
13func main() {
14	p := Person{"John", 30}
15
16	t := reflect.TypeOf(p)
17	v := reflect.ValueOf(p)
18
19	for i := 0; i < t.NumField(); i++ {
20		field := t.Field(i)
21		value := v.Field(i)
22
23		tag := field.Tag.Get("customtag")
24
25		fmt.Printf("Field: %s, Value: %v, Tag: %s\n", field.Name, value.Interface(), tag)
26	}
27}

در این مثال، یک ساختار Person با دو فیلد Name و Age تعریف شده است. هر یک از این فیلدها با استفاده از کلید customtag یک تگ سفارشی دارند.

برای دسترسی به تگ‌ها در زمان اجرا، از بسته reflect استفاده می‌شود. با استفاده از reflect.TypeOf و reflect.ValueOf نوع و مقدار ساختار Person بدست می‌آیند. سپس با استفاده از حلقه for و توابع t.NumField() و t.Field(i) بر روی فیلدهای ساختار حرکت می‌کنیم. برای هر فیلد، با استفاده از v.Field(i) مقدار آن را و با استفاده از field.Tag.Get("customtag") تگ سفارشی آن را بدست می‌آوریم.

در نهایت با استفاده از fmt.Printf نام فیلد، مقدار آن و تگ سفارشی آن را چاپ می‌کنیم. خروجی این برنامه به شکل زیر خواهد بود:

1$ go run main.go
2Field: Name, Value: John, Tag: myname
3Field: Age, Value: 30, Tag: myage

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