การแปลงแผนที่เป็นโครงสร้าง


95

ฉันกำลังพยายามสร้างวิธีการทั่วไปใน Go ซึ่งจะเติมข้อมูลstructโดยใช้ข้อมูลจากไฟล์map[string]interface{}. ตัวอย่างเช่นลายเซ็นวิธีการและการใช้งานอาจมีลักษณะดังนี้:

func FillStruct(data map[string]interface{}, result interface{}) {
    ...
}

type MyStruct struct {
    Name string
    Age  int64
}

myData := make(map[string]interface{})
myData["Name"] = "Tony"
myData["Age"]  = 23

result := &MyStruct{}
FillStruct(myData, result)

// result now has Name set to "Tony" and Age set to 23

ฉันรู้ว่าสิ่งนี้สามารถทำได้โดยใช้ JSON เป็นตัวกลาง มีวิธีอื่นที่มีประสิทธิภาพมากกว่านี้ไหม


1
การใช้ JSON เป็นตัวกลางก็จะใช้การสะท้อนอยู่ดี .. สมมติว่าคุณกำลังจะใช้encoding/jsonแพ็คเกจstdlib เพื่อทำขั้นตอนกลางนั้น .. คุณสามารถยกตัวอย่างแผนที่และโครงสร้างตัวอย่างที่สามารถใช้กับวิธีนี้ได้หรือไม่?
Simon Whitehead

ใช่นั่นคือเหตุผลที่ฉันพยายามหลีกเลี่ยง JSON ดูเหมือนว่าหวังว่าจะเป็นวิธีที่มีประสิทธิภาพมากกว่าที่ฉันไม่รู้
tgrosinger

คุณสามารถยกตัวอย่าง use case ได้หรือไม่? เช่นเดียวกับใน - แสดงรหัสเทียมที่แสดงให้เห็นว่าวิธีนี้จะทำอย่างไร?
Simon Whitehead

อืม ... อาจจะมีวิธีกับunsafeแพคเกจ.. แต่ไม่กล้าลอง นอกเหนือจากนั้น .. จำเป็นต้องมีการสะท้อนกลับเนื่องจากคุณต้องสามารถสืบค้นข้อมูลเมตาที่เกี่ยวข้องกับประเภทเพื่อวางข้อมูลลงในคุณสมบัติ มันจะค่อนข้างตรงไปตรงมาที่จะสรุปสิ่งนี้ในการโทรjson.Marshal+ json.Decode..
Simon Whitehead

ฉันได้ลบความคิดเห็นเกี่ยวกับการสะท้อนกลับ ฉันสนใจที่จะทำสิ่งนี้ให้มีประสิทธิภาพมากขึ้น ถ้านั่นหมายถึงการใช้การสะท้อนที่ไม่เป็นไร
tgrosinger

คำตอบ:


115

วิธีที่ง่ายที่สุดคือใช้https://github.com/mitchellh/mapstructure

import "github.com/mitchellh/mapstructure"

mapstructure.Decode(myData, &result)

หากคุณต้องการทำเองคุณสามารถทำสิ่งนี้:

http://play.golang.org/p/tN8mxT_V9h

func SetField(obj interface{}, name string, value interface{}) error {
    structValue := reflect.ValueOf(obj).Elem()
    structFieldValue := structValue.FieldByName(name)

    if !structFieldValue.IsValid() {
        return fmt.Errorf("No such field: %s in obj", name)
    }

    if !structFieldValue.CanSet() {
        return fmt.Errorf("Cannot set %s field value", name)
    }

    structFieldType := structFieldValue.Type()
    val := reflect.ValueOf(value)
    if structFieldType != val.Type() {
        return errors.New("Provided value type didn't match obj field type")
    }

    structFieldValue.Set(val)
    return nil
}

type MyStruct struct {
    Name string
    Age  int64
}

func (s *MyStruct) FillStruct(m map[string]interface{}) error {
    for k, v := range m {
        err := SetField(s, k, v)
        if err != nil {
            return err
        }
    }
    return nil
}

func main() {
    myData := make(map[string]interface{})
    myData["Name"] = "Tony"
    myData["Age"] = int64(23)

    result := &MyStruct{}
    err := result.FillStruct(myData)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(result)
}

1
ขอบคุณ. ฉันใช้เวอร์ชันที่ปรับเปลี่ยนเล็กน้อย play.golang.org/p/_JuMm6HMnU
tgrosinger

ฉันต้องการพฤติกรรม FillStruct ในโครงสร้างต่างๆทั้งหมดของฉันและไม่จำเป็นต้องกำหนดfunc (s MyStr...) FillStruct ...สำหรับทุกโครงสร้าง เป็นไปได้หรือไม่ที่จะกำหนด FillStruct สำหรับโครงสร้างพื้นฐานจากนั้นให้โครงสร้างอื่น ๆ ทั้งหมดของฉัน 'สืบทอด' พฤติกรรมนั้น? ในกระบวนทัศน์ข้างต้นเป็นไปไม่ได้เนื่องจากมีเพียงโครงสร้างพื้นฐานเท่านั้น ... ในกรณีนี้ "MyStruct" จะมีฟิลด์ซ้ำ
StartupGuy

ฉันหมายความว่าคุณสามารถใช้งานได้กับโครงสร้างใด ๆ ด้วยสิ่งนี้: play.golang.org/p/0weG38IUA9
dave

เป็นไปได้ไหมที่จะใช้แท็กใน Mystruct
vicTROLLA

1
@abhishek มีการลงโทษด้านประสิทธิภาพอย่างแน่นอนที่คุณจะต้องจ่ายสำหรับการจัดส่งข้อความครั้งแรกจากนั้นจึงยกเลิกการเข้ารหัส วิธีนี้ก็ง่ายกว่าเช่นกัน มันเป็นการแลกเปลี่ยนและโดยทั่วไปฉันจะใช้วิธีแก้ปัญหาที่ง่ายกว่านี้ ฉันตอบด้วยวิธีแก้ปัญหานี้เนื่องจากคำถามระบุว่า "ฉันรู้ว่าสิ่งนี้สามารถทำได้โดยใช้ JSON เป็นตัวกลางมีวิธีอื่นที่มีประสิทธิภาพมากกว่านี้หรือไม่" โซลูชันนี้จะมีประสิทธิภาพมากขึ้นโดยทั่วไปโซลูชัน JSON จะใช้งานได้ง่ายกว่าและให้เหตุผลเกี่ยวกับ
dave

74

ห้องสมุดhttps://github.com/mitchellh/mapstructureของ Hashicorp ทำสิ่งนี้ได้ทันที:

import "github.com/mitchellh/mapstructure"

mapstructure.Decode(myData, &result)

resultพารามิเตอร์ที่สองต้องเป็นที่อยู่ของโครงสร้าง


จะเกิดอะไรขึ้นถ้าคีย์แผนที่user_nameและโครงสร้างที่ยื่นคือUserNameอะไร?
Nicholas Jela

1
@NicholasJela สามารถจัดการได้ด้วยแท็กgodoc.org/github.com/mitchellh/mapstructure#ex-Decode--Tags
วงจรในกำแพง

จะเกิดอะไรขึ้นถ้า map kye เป็น _id และชื่อแผนที่เป็น Id มันจะไม่ถอดรหัส
Ravi Shankar

27
  • วิธีที่ง่ายที่สุดคือการใช้encoding/jsonแพ็คเกจ

ตัวอย่างเช่น:

package main
import (
    "fmt"
    "encoding/json"
)

type MyAddress struct {
    House string
    School string
}
type Student struct {
    Id int64
    Name string
    Scores float32
    Address MyAddress
    Labels []string
}

func Test() {

    dict := make(map[string]interface{})
    dict["id"] = 201902181425       // int
    dict["name"] = "jackytse"       // string
    dict["scores"] = 123.456        // float
    dict["address"] = map[string]string{"house":"my house", "school":"my school"}   // map
    dict["labels"] = []string{"aries", "warmhearted", "frank"}      // slice

    jsonbody, err := json.Marshal(dict)
    if err != nil {
        // do error check
        fmt.Println(err)
        return
    }

    student := Student{}
    if err := json.Unmarshal(jsonbody, &student); err != nil {
        // do error check
        fmt.Println(err)
        return
    }

    fmt.Printf("%#v\n", student)
}

func main() {
    Test()
}

1
ขอบคุณ @jackytse นี่เป็นวิธีที่ดีที่สุดจริงๆ !! โครงสร้างแผนที่มักใช้ไม่ได้กับแผนที่ที่ซ้อนกันภายในอินเทอร์เฟซ ดังนั้นจึงควรพิจารณาอินเทอร์เฟซสตริงแผนที่และจัดการเป็น json
Gilles Essoki

ไปที่ลิงค์สนามเด็กเล่นสำหรับตัวอย่างด้านบน: play.golang.org/p/JaKxETAbsnT
Junaid

13

คุณทำได้ ... มันอาจจะดูน่าเกลียดไปหน่อยและคุณจะต้องเจอกับการลองผิดลองถูกในแง่ของประเภทการทำแผนที่ .. แต่นี่เป็นหัวใจสำคัญพื้นฐานของมัน

func FillStruct(data map[string]interface{}, result interface{}) {
    t := reflect.ValueOf(result).Elem()
    for k, v := range data {
        val := t.FieldByName(k)
        val.Set(reflect.ValueOf(v))
    }
}

ตัวอย่างการทำงาน: http://play.golang.org/p/PYHz63sbvL


1
สิ่งนี้ดูเหมือนจะตื่นตระหนกกับค่าศูนย์:reflect: call of reflect.Value.Set on zero Value
James Taylor

@JamesTaylor ครับ คำตอบของฉันถือว่าคุณรู้แน่นอนว่าคุณกำลังทำแผนที่ช่องใด หากคุณต้องการคำตอบที่คล้ายกันซึ่งมีการจัดการข้อผิดพลาดมากขึ้น (รวมถึงข้อผิดพลาดที่คุณพบ) ฉันขอแนะนำให้ Daves ตอบ
Simon Whitehead

2

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

package main

import (
    "fmt"
    "reflect"
)

func SetField(obj interface{}, name string, value interface{}) error {

    structValue := reflect.ValueOf(obj).Elem()
    fieldVal := structValue.FieldByName(name)

    if !fieldVal.IsValid() {
        return fmt.Errorf("No such field: %s in obj", name)
    }

    if !fieldVal.CanSet() {
        return fmt.Errorf("Cannot set %s field value", name)
    }

    val := reflect.ValueOf(value)

    if fieldVal.Type() != val.Type() {

        if m,ok := value.(map[string]interface{}); ok {

            // if field value is struct
            if fieldVal.Kind() == reflect.Struct {
                return FillStruct(m, fieldVal.Addr().Interface())
            }

            // if field value is a pointer to struct
            if fieldVal.Kind()==reflect.Ptr && fieldVal.Type().Elem().Kind() == reflect.Struct {
                if fieldVal.IsNil() {
                    fieldVal.Set(reflect.New(fieldVal.Type().Elem()))
                }
                // fmt.Printf("recursive: %v %v\n", m,fieldVal.Interface())
                return FillStruct(m, fieldVal.Interface())
            }

        }

        return fmt.Errorf("Provided value type didn't match obj field type")
    }

    fieldVal.Set(val)
    return nil

}

func FillStruct(m map[string]interface{}, s interface{}) error {
    for k, v := range m {
        err := SetField(s, k, v)
        if err != nil {
            return err
        }
    }
    return nil
}

type OtherStruct struct {
    Name string
    Age  int64
}


type MyStruct struct {
    Name string
    Age  int64
    OtherStruct *OtherStruct
}



func main() {
    myData := make(map[string]interface{})
    myData["Name"]        = "Tony"
    myData["Age"]         = int64(23)
    OtherStruct := make(map[string]interface{})
    myData["OtherStruct"] = OtherStruct
    OtherStruct["Name"]   = "roxma"
    OtherStruct["Age"]    = int64(23)

    result := &MyStruct{}
    err := FillStruct(myData,result)
    fmt.Println(err)
    fmt.Printf("%v %v\n",result,result.OtherStruct)
}

1

มีสองขั้นตอน:

  1. แปลงอินเทอร์เฟซเป็น JSON Byte
  2. แปลง JSON Byte เป็น struct

ด้านล่างนี้คือตัวอย่าง:

dbByte, _ := json.Marshal(dbContent)
_ = json.Unmarshal(dbByte, &MyStruct)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.