Redigo: análisis del campo de puntero anónimo de ScanStruct()

Reducir problema 487

¿Cómo escanear una estructura con campos anidados?#487

Para comprender mejor este artículo, se recomienda leer primero el número original.

1. ¿Cuál es el problema?

Los HGETALLdatos devueltos por el comando se analizan en la estructura correspondiente UserInfo, pero *LiteUserlos datos de los campos de la estructura no se pueden analizar correctamente.

Si lo *LiteUsercambias LiteUsera .

2. Reproducir el problema

  1. Copie el código en el problema, instale redigo en el módulo go y prepare un servicio redis.
  2. La versión en redigo en go.mod se establece en la versión anterior al problema sin modificar: v1.8.1.
  3. Ejecutar el código reproducirá el problema de este problema y *LiteUserlos datos del campo no se pueden analizar correctamente.

Pruebe la última versión del código y ejecútelo:

  1. La versión en redigo en go.mod está configurada en la última versión del problema: v1.8.8.
  2. Ejecutando el código, el problema no aparece.

Tenga en cuenta que para reproducir el problema en la última versión, debe agregar el siguiente código a continuación sobre la línea 73 del código de muestra (volveremos a este problema más adelante):


...

var newUser UserInfo

newUser.LiteUser = &LiteUser{}

...

复制代码

En tercer lugar, cómo resolver

Para más detalles, consulte pr 490

Antes de ver cómo resolverlo, resolvamos el proceso de ejecución:

3.1 Análisis de datos en variables de estructura

Cuando la ejecución HGETALLobtiene los datos de Redis, necesita analizar los datos en las variables miembro de la estructura, al igual que tomar los datos de MySQL y analizarlos en las variables miembro de la estructura es un significado.

Redigo proporciona un método para pasar datos y variables de estructura, y los datos se analizarán en la newUserestructura:


redis.ScanStruct(v, &newUser)

复制代码

3.2 Estructura de escaneo

A continuación, eche un vistazo redis.ScanStruct()a lo que se ha hecho.

Peiné y resumí los métodos llamados en el proceso:


// 将数据解析到structSpecForType返回的结构体成员上
func ScanStruct(src []interface{}, dest interface{}) error {
    //获取变量指针
    d := reflect.ValueOf(dest)
    //获取指针指向的变量
    d = d.Elem()
    structSpecForType(d.Type())
    ...
}

// 根据传入的reflect.Type,先去缓存中查找是否解析过,如果没有调用compileStructSpec
func structSpecForType(t reflect.Type) *structSpec {
    ...
    compileStructSpec(t, make(map[string]int), nil, ss)
    ...
}

复制代码

3.3 especificación de estructura de compilación

compileStructSpecEl método implementa la resolución de tipos y el problema está realmente aquí.

Primero publique el resumen resumido:

  • Use la reflexión para analizar datos &newUser 结构体en todas las variables miembro de
  • En V1.8.1 y versiones anteriores, solo se analizaba reflect.Struct (LiteUser) y no se procesaba reflect.Ptr (*LiteUser).
  • En V1.8.2 y posteriores, se agrega el juicio de reflect.Ptr

A continuación se muestra la lógica central

Antes de la reparación:


func compileStructSpec(t reflect.Type, depth map[string]int, index []int, ss *structSpec) {
    // t.NumField()获取结构体类型的所有字段的个数
    for i := 0; i < t.NumField(); i++ {
        // t.Field()返回指定的字段,类型为 StructField
        f := t.Field(i)
        switch {
        // f.PkgPath 包路径不为空 且 不是匿名函数
        // f.Anonymous 表示该字段是否为匿名字段
        case f.PkgPath != "" && !f.Anonymous:
        // 忽略未导出的:结构体中的某个成员改为小写(私有),就会进到这个case
        // Ignore unexported fields.
        // UserInfo中的成员LiteUser,并未设置 name,为匿名字段,就会进到这个case
        case f.Anonymous:
        // f.Type.Kind() 获取种类
        // 如果当前type为结构体,进行递归调用,以处理当前type内所有结构体成员
        // 对于 `LiteUser` 会进到这个 case
        if f.Type.Kind() == reflect.Struct {
            compileStructSpec(f.Type, depth, append(index, i), ss)
        }

复制代码

Después de arreglar:

...
func compileStructSpec(t reflect.Type, depth map[string]int, index []int, ss *structSpec) {
    LOOP:
    for i := 0; i < t.NumField(); i++ {
        f := t.Field(i)
        switch {
        case f.PkgPath != "" && !f.Anonymous:
        // Ignore unexported fields.
        case f.Anonymous:
            switch f.Type.Kind() {
            case reflect.Struct:
            compileStructSpec(f.Type, depth, append(index, i), ss)
            // 这里是变动的部分,对于 `*LiteUser` 会进到这个 case
            case reflect.Ptr:
                // 如果当前字段的type的值为结构体,进行递归调用,以处理当前字段内所有结构体成员
                // f.Type.Kind()返回的是前f的种类,也就是reflect.Ptr
                // f.Type.Elem().Kind() 返回的是前f的值的种类,也就是reflect.Struct
                // TODO(steve): Protect against infinite recursion.
                if f.Type.Elem().Kind() == reflect.Struct {
                    compileStructSpec(f.Type.Elem(), depth, append(index, i), ss)
                }
            }
    ...
复制代码

Bien~, ¡problema resuelto!

4. Expansión

4.1 Reflexión

compileStructSpecDentro del método, se implementa principalmente a través de la reflexión.

El punto clave aquí es hablar sobre por qué d := reflect.ValueOf(dest)todavía se usa después de que está terminado d = d.Elem(), citando una oración de "Ir al diseño e implementación del lenguaje".

Dado que todas las llamadas a funciones en el lenguaje Go se pasan por valor, solo podemos cambiar la variable original de forma indirecta: primero obtenga el puntero correspondiente reflect.Valuey luego use el reflect.Value.Elemmétodo para obtener la variable que se puede establecer.

Referirse a

El lenguaje Go involucra e implementa - reflexión

4.2 nuevoUsuario.LiteUser = &LiteUser{}

Si int, string, etc. son tipos de valor, incluso si no se inicializan, sino que solo se declaran, el valor predeterminado será el valor "cero" de este tipo.

Pero las variables de referencia como Map, Slice, Channel, etc. deben usarse antes make().

Para una variable del mismo &tipo, su valor almacena una dirección de memoria, por lo que primero debe inicializar una LiteUserestructura y luego asignarle su dirección de memoria newUser.LiteUserantes de que pueda usarse normalmente.

Número público: IssueHub

Supongo que te gusta

Origin juejin.im/post/7082163602374787079
Recomendado
Clasificación