* คำเตือน * วิธีการนี้เป็นท่าอ้อมโลก ลองเพื่อให้รู้ว่ามีท่านี้อยู่และอาจจะมีสถานการณ์ที่เหมาะสมให้ใช้ แต่โดยส่วนใหญ่แล้วเราใช้ท่าธรรมดาก็พอแล้ว
โจทย์คือสมมติว่าเรามี function ที่อยากได้ค่า 2 ค่าจาก struct มาใช้งาน เอาง่ายๆ เช่นอยากได้ชื่อกับนามสกุลแล้วเอามาปริ้นออกไป เราก็ออกแบบฟังก์ชันได้แบบนี้
func PrintInfo(firstName string, lastName string) {
fmt.Println(firstName, lastName)
}
แล้วถ้าเรามี struct 2 ตัวที่มี fields บางส่วนเหมือนกันเช่น มีทั้ง FirstName และ LastName
type Employee struct {
FirstName string
LastName string
}
type Student struct {
FirstName string
LastName string
}
ถ้าเราอยากเรียกใช้ PrintInfo กับข้อมูลของ struct Employee และ Student เราก็ต้องส่ง FirstName กับ LastName เข้าไปเช่น
e := &Employee{FirstName: "George", LastName: "Green"}
s := &Student{FirstName: "Sarah", LastName: "Red"}
PrintInfo(e.FirstName, e.LastName)
PrintInfo(s.FirstName, s.LastName)
ทีนี้คิดเล่นๆว่า ถ้าเราออกแบบ PrintInfo ไว้แบบนี้แทน
type Info struct {
FirstName string
LastName string
}
func PrintInfo(info *Info) {
fmt.Println(info.FirstName, info.LastName)
}
เวลาใช้งานก็จะต้องเขียนแบบนี้
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 ล่ะ หน้าตาคร่าวๆที่คิดไว้ก็เป็นแบบนี้
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 ที่ต้องการเพื่อให้เก็บค่าที่ดึงได้
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
ได้แบบนี้
func makeInfoFromStruct(st interface{}) *Info {
info := &Info{}
FromFieldName(st, "FirstName", &info.FirstName)
FromFieldName(st, "LastName", &info.LastName)
return info
}
แล้วก็สามารถเอา makeInfoFromStruct
ไปใช้งานได้แบบนี้
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 ช่วย
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))
}
Top comments (0)