ใช้ Go reflect ดึงค่าจาก struct ด้วยชื่อ field ที่เป็น string
Tags: go
Published: 2020-12-12 02:56:06

## _* คำเตือน *_ วิธีการนี้เป็นท่าอ้อมโลก ลองเพื่อให้รู้ว่ามีท่านี้อยู่และอาจจะมีสถานการณ์ที่เหมาะสมให้ใช้ แต่โดยส่วนใหญ่แล้วเราใช้ท่าธรรมดาก็พอแล้ว

โจทย์คือสมมติว่าเรามี function ที่อยากได้ค่า 2 ค่าจาก struct มาใช้งาน เอาง่ายๆ เช่นอยากได้ชื่อกับนามสกุลแล้วเอามาปริ้นออกไป เราก็ออกแบบฟังก์ชันได้แบบนี้

```go
func PrintInfo(firstName string, lastName string) {
    fmt.Println(firstName, lastName)
}
```

แล้วถ้าเรามี struct 2 ตัวที่มี fields บางส่วนเหมือนกันเช่น มีทั้ง FirstName และ LastName

```go
type Employee struct {
    FirstName string
    LastName string
}

type Student struct {
    FirstName string
    LastName string
}
```

ถ้าเราอยากเรียกใช้ PrintInfo กับข้อมูลของ struct Employee และ Student เราก็ต้องส่ง FirstName กับ LastName เข้าไปเช่น

```go
e := &Employee{FirstName: "George", LastName: "Green"}
s := &Student{FirstName: "Sarah", LastName: "Red"}

PrintInfo(e.FirstName, e.LastName)
PrintInfo(s.FirstName, s.LastName)
```

ทีนี้คิดเล่นๆว่า ถ้าเราออกแบบ PrintInfo ไว้แบบนี้แทน

```go
type Info struct {
    FirstName string
    LastName string
}

func PrintInfo(info *Info) {
    fmt.Println(info.FirstName, info.LastName)
}
```

เวลาใช้งานก็จะต้องเขียนแบบนี้

```go
e := &Employee{FirstName: "George", LastName: "Green"}
s := &Student{FirstName: "Sarah", LastName: "Red"}

PrintInfo(&Info{FirstName: e.FirstName, LastName: e.LastName})
PrintInfo(&Info{FirstName: s.FirstName, LastName: s.LastName})
```

ทีนี้ถ้าเราอยากแยกตรงส่วนสร้าง Info จาก struct Student หรือ Employee หรือ struct ใดๆเพื่อดึงข้อมูลมาสร้าง Info ล่ะ หน้าตาคร่าวๆที่คิดไว้ก็เป็นแบบนี้

```go
func makeInfoFromStruct(st ???) *Info {
    return &Info{
        FirstName: st.FirstName,
        LastName: st.LastName,
    }
}
```

แต่เราจะให้พารามิเตอร์ st เป็น type อะไรล่ะ เนื่องจากเราไม่สามารถเขียน type เป็น generic โดยมีเงื่อนไขว่าขอให้ type struct นั้นมี fields ตามที่เราต้องการได้ ต่างกับ method ที่เรายังสามารถใช้ interface ช่วยเพื่อทำหนดเงื่อนไขของพารามิเตอร์ว่าให้เป็น type ใดๆที่มี methods ตามที่เราต้องการได้

สิ่งที่พอจะทำให้เราสร้าง `makeInfoFromStruct` ได้ตามที่เราต้องการคือใช้แพ็คเก็จ `reflect` ช่วยซึ่งสามารถทำให้เราเข้าถึงข้อมูล type ใดๆก็ได้ตอน runtime แต่แน่นอนมันก็จะอ้อมๆหน่อย ไม่เหมือนกับที่เราใช้ `s.FirstName` `s.LastName` ในกรณีที่เรารู้ว่า `s` เป็น type struct `Student`

ผมสร้าง function ชื่อ `FromFieldName` เอาไว้โดยรับค่า struct ใดๆ ตามด้วยชื่อ field ที่อยากดึงค่า แล้วก็ให้ใส่ pointer ของ type เดียวกันกับ field ที่ต้องการเพื่อให้เก็บค่าที่ดึงได้

```go
func FromFieldName(st interface{}, name string, out interface{}) {
	// เช็คว่า st ต้องเป็น pointer เท่านั้น
	v := reflect.ValueOf(st)
	if v.Kind() != reflect.Ptr {
		panic("cannot fetch a value from st type that is not a pointer")
	}

	// เช็คว่า out ที่จะเอาไว้เก็บผลลัพธ์ต้องเป็น pointer เท่านั้น
	ov := reflect.ValueOf(out)
	if ov.Kind() != reflect.Ptr {
		panic("cannot fetch a value to out type that is not a pointer")
	}

	// แล้วใช้ Elem เพื่อดึงค่าที่ pointer ชี้อยู่ออกมา พร้อมกับเช็คว่าต้องเป็น struct เท่านั้น
	ve := v.Elem()
	if ve.Kind() != reflect.Struct {
		panic("cannot fetch a value from st type that is not a pointer to struct")
    }

    // ดึง Elem ของตัวแปรที่ pointer out ชี้อยู่ออกมา
    oe := ov.Elem()

    // ดึง Type ของ Elem ของ out ออกมา
    ot := oe.Type()

	// เช็คว่ามีชื่อ field ที่เราต้องการใน struct หรือไม่ และ type ต้องเป็นแบบเดียวกันกับ out ด้วย
	te := ve.Type()
	if f, ok := te.FieldByName(name); !ok || f.Type.Kind() != ot.Kind() {
		panic(fmt.Sprint("cannot fetch field %s from st because mismatch type with output type"))
    }

	// ดึงค่าใน field ตามชื่อที่ต้องการออกมา
    value := ve.FieldByName(name)

	// แล้วเซตค่ากลับให้ out Element
	oe.Set(value)
}
```

จากโค้ดพร้อมคำอธิบายใน comment จะเห็นว่าใช้ความสามารถของ reflect ช่วยเพื่อให้เราเช็คข้อมูลของ type ดึงค่าออกมาจาก field ของ struct ได้ตามชื่อที่ต้องการโดยไม่เจาะจงว่าเป็น type ชื่ออะไร

จากนั้นเราเอา `FromFieldName` มาช่วยสร้าง `makeInfoFromStruct` ได้แบบนี้

```go
func makeInfoFromStruct(st interface{}) *Info {
	info := &Info{}
	FromFieldName(st, "FirstName", &info.FirstName)
	FromFieldName(st, "LastName", &info.LastName)
	return info
}
```

แล้วก็สามารถเอา `makeInfoFromStruct` ไปใช้งานได้แบบนี้

```go
e := &Employee{FirstName: "George", LastName: "Green"}
s := &Student{FirstName: "Sarah", LastName: "Red"}

PrintInfo(makeInfoFromStruct(e))
PrintInfo(makeInfoFromStruct(s))
```

สรุปเราก็ได้เห็นตัวอย่างความสามารถของ `reflect` กันไปแล้วว่าเอามาช่วยทำให้เราดึงค่าเกี่ยวกับ type และ value ของตัวแปรใดๆตอน runtime ได้ ซึ่งก็สามารถไปประยุกต์ใช้สร้างอะไรที่เป็น generic flow ได้เช่น `makeInfoFromStruct` เอาไว้แปลงข้อมูล struct ใดๆเป็น *Info

ข้อเสียของการใช้ `reflect` ที่ชัดเจนเลยก็คือโค้ดตรงที่ใช้ `relect` มันไม่ตรงไป ตรงมา อ่านทำความเข้าใจยากกว่าท่าธรรมดาแน่นอน

อีกเรื่องคือผลักภาระไปตอน runtime ทำให้อาจจะเกิดการ panic ขึ้นได้เช่นกรณีที่เราเขียนชื่อ field ผิดเป็นต้น

ทางที่ดีถ้าเป็นไปได้ก็ใช้ท่าธรรมดาดีที่สุด ยกเว้นว่าเอามาใช้แล้วประโยชน์ที่ได้รับมากกว่าข้อเสียที่จะเจอ เช่นช่วยให้โค้ดส่วนอื่นๆเรียบง่าย และลดจำนวนโค้ดโดยรวมที่ต้องดูแลลงได้เยอะ ก็ใช้ดูแต่ก็ต้องทดสอบกันให้ละเอียดๆว่าไม่มีอะไรผิดพลาดเกิดขึ้น


### อันนี้คือโค้ดทั้งหมดที่รันได้ ตรงที่ comment คือโค้ดส่วนก่อนหน้าที่จะใช้ reflect ช่วย

```go
package main

import (
	"fmt"
	"reflect"
)

type Employee struct {
	FirstName string
	LastName  string
}

type Student struct {
	FirstName string
	LastName  string
}

// func PrintInfo(firstName string, lastName string) {
// 	fmt.Println(firstName, lastName)
// }

type Info struct {
	FirstName string
	LastName  string
}

func PrintInfo(info *Info) {
	fmt.Println(info.FirstName, info.LastName)
}

func FromFieldName(st interface{}, name string, out interface{}) {
	// เช็คว่า st ต้องเป็น pointer เท่านั้น
	v := reflect.ValueOf(st)
	if v.Kind() != reflect.Ptr {
		panic("cannot fetch a value from st type that is not a pointer")
	}

	// เช็คว่า out ที่จะเอาไว้เก็บผลลัพธ์ต้องเป็น pointer เท่านั้น
	ov := reflect.ValueOf(out)
	if ov.Kind() != reflect.Ptr {
		panic("cannot fetch a value to out type that is not a pointer")
	}

	// แล้วใช้ Elem เพื่อดึงค่าที่ pointer ชี้อยู่ออกมา พร้อมกับเช็คว่าต้องเป็น struct เท่านั้น
	ve := v.Elem()
	if ve.Kind() != reflect.Struct {
		panic("cannot fetch a value from st type that is not a pointer to struct")
	}

	// ดึง Elem ของตัวแปรที่ pointer out ชี้อยู่ออกมา
	oe := ov.Elem()

	// ดึง Type ของ Elem ของ out ออกมา
	ot := oe.Type()

	// เช็คว่ามีชื่อ field ที่เราต้องการใน struct หรือไม่ และ type ต้องเป็นแบบเดียวกันกับ out ด้วย
	te := ve.Type()
	if f, ok := te.FieldByName(name); !ok || f.Type.Kind() != ot.Kind() {
		panic(fmt.Sprint("cannot fetch field %s from st because mismatch type with output type"))
	}

	// ดึงค่าใน field ตามชื่อที่ต้องการออกมา
	value := ve.FieldByName(name)

	// แล้วเซตค่ากลับให้ out Element
	oe.Set(value)
}

func makeInfoFromStruct(st interface{}) *Info {
	info := &Info{}
	FromFieldName(st, "FirstName", &info.FirstName)
	FromFieldName(st, "LastName", &info.LastName)
	return info
}

func main() {
	e := &Employee{FirstName: "George", LastName: "Green"}
	s := &Student{FirstName: "Sarah", LastName: "Red"}

	// PrintInfo(e.FirstName, e.LastName)
	// PrintInfo(s.FirstName, s.LastName)

	// PrintInfo(&Info{FirstName: e.FirstName, LastName: e.LastName})
	// PrintInfo(&Info{FirstName: s.FirstName, LastName: s.LastName})

	PrintInfo(makeInfoFromStruct(e))
	PrintInfo(makeInfoFromStruct(s))
}
```