DEV Community

loading...

ใช้ Go reflect ดึงค่าจาก struct ด้วยชื่อ field ที่เป็น string

#go
Weerasak Chongnguluam
Software Developer/Love to code/Teaching to code
・3 min read

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

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

func PrintInfo(firstName string, lastName string) {
    fmt.Println(firstName, lastName)
}
Enter fullscreen mode Exit fullscreen mode

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

type Employee struct {
    FirstName string
    LastName string
}

type Student struct {
    FirstName string
    LastName string
}
Enter fullscreen mode Exit fullscreen mode

ถ้าเราอยากเรียกใช้ 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)
Enter fullscreen mode Exit fullscreen mode

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

type Info struct {
    FirstName string
    LastName string
}

func PrintInfo(info *Info) {
    fmt.Println(info.FirstName, info.LastName)
}
Enter fullscreen mode Exit fullscreen mode

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

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})
Enter fullscreen mode Exit fullscreen mode

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

func makeInfoFromStruct(st ???) *Info {
    return &Info{
        FirstName: st.FirstName,
        LastName: st.LastName,
    }
}
Enter fullscreen mode Exit fullscreen mode

แต่เราจะให้พารามิเตอร์ 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)
}
Enter fullscreen mode Exit fullscreen mode

จากโค้ดพร้อมคำอธิบายใน 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
}
Enter fullscreen mode Exit fullscreen mode

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

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

PrintInfo(makeInfoFromStruct(e))
PrintInfo(makeInfoFromStruct(s))
Enter fullscreen mode Exit fullscreen mode

สรุปเราก็ได้เห็นตัวอย่างความสามารถของ 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))
}
Enter fullscreen mode Exit fullscreen mode

Discussion (0)