Golang 反射(Reflection)实践

1. 引言

在 Golang 语言中,反射(Reflection)提供了一种在运行时检查变量类型、调用方法或访问结构体字段的能力。反射的核心在于 reflect 包,它提供了 reflect.Typereflect.Value 这两个关键类型来解析数据。

本文将深入剖析 Golang 反射的工作原理,并结合具体应用场景,如指针解析、数组解析、数据库查询结果解析等。同时,我们也会列举常见的反射方法及其使用方式,帮助开发者更高效地使用反射机制。


2. 反射的基本概念

Golang 反射的核心基于 reflect.Typereflect.Value

  • reflect.Type:表示具体的类型信息,例如 intstringstruct 等。
  • reflect.Value:表示具体的值,可以通过它修改变量的值。

2.1 reflect.Typereflect.Value简单示例

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var num int = 66
	t := reflect.TypeOf(num)
  fmt.Println("Type:", t)
  fmt.Println("Kind:", t.Kind())
  v := reflect.ValueOf(num)
	fmt.Println("Value:", v)
}

输出:

Type: int
Kind: int
Value: 66

3. 反射解析指针类型

在 Golang 反射中,指针类型需要使用 Elem() 方法来获取实际的值。

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var num = 66
	ptr := &num
	v := reflect.ValueOf(ptr)
	if v.Kind() == reflect.Ptr {
		fmt.Println("Ptr Value:", v.Elem())
	}
}

输出:

Ptr Value: 66

如果直接使用v而不使用 v.Elem(),则得到的是指针内存地址:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var num = 66
    ptr := &num
    v := reflect.ValueOf(ptr)
    fmt.Println("Ptr Value:", v)
}

输出:

Pointer Value: 0x14000110018

4. 解析数组(内部是指针结构体)

在某些场景下,我们可能会遇到数组类型,其中的元素是指向结构体的指针。我们需要遍历数组并解析内部的结构体。

type User struct {
	Name string
	Age  int
}

func main() {
	user := []*User{
		{Name: "Joker", Age: 21},
		{Name: "Poker", Age: 30},
	}

	v := reflect.ValueOf(user)
	if v.Kind() == reflect.Slice {
		for i := 0; i < v.Len(); i++ {
			elem := v.Index(i).Elem()
			fmt.Println("Name:", elem.FieldByName("Name"), "Age:", elem.FieldByName("Age"))
		}
	}
}

输出:

Name: Joker Age: 21
Name: Poker Age: 30

注意:这里切片内部是指针的结构体,因此在解析每个元素的时候需要调用 Elem()方法,才能拿到指针结构体的类型,否则直接调用 elem := v.Index(i)就会panic:

panic: reflect: call of reflect.Value.FieldByName on ptr Value

goroutine 1 [running]:
reflect.flag.mustBe(...)
	/usr/local/go/src/reflect/value.go:233
reflect.Value.FieldByName({0x10472d140?, 0x14000010060?, 0x2?}, {0x104702357?, 0x10481ff40?})
	/usr/local/go/src/reflect/value.go:1361 +0x140
main.main()
	/path/golang_space/src/demo/reflect.go:23 +0x198
exit status 2

5. 数据库查询结果解析

在数据库操作中,我们经常需要动态解析查询结果并将其映射到结构体中。

package main

import (
	"fmt"
	"reflect"
  "database/sql"
  
	_ "github.com/lib/pq"
)

type User struct {
	ID   int
	Name string
}

func scanRows(rows *sql.Rows, dest any) error {
	v := reflect.ValueOf(dest).Elem()
	fields := make([]interface{}, v.NumField())

	for i := 0; i < v.NumField(); i++ {
		fields[i] = v.Field(i).Addr().Interface()
	}

	return rows.Scan(fields...)
}

func main() {
	db, err := sql.Open("postgres", "user=test dbname=test sslmode=disable")
	if err != nil {
		panic(err)
	}
	defer db.Close()

	rows, err := db.Query("SELECT id, name FROM user")
	if err != nil {
		panic(err)
	}
	defer rows.Close()

  var users []User
	for rows.Next() {
		var user User
		if err := scanRows(rows, &user); err != nil {
			panic(err)
		}
    users = append(users, user)
	}
  
  fmt.Println(len(users))
}

6. 反射常见方法解析

方法 说明
reflect.TypeOf() 获取变量的类型
reflect.ValueOf() 获取变量的值
reflect.Type.NumField() 获取结构体字段数量
reflect.Value.Elem() 获取指针指向的值
reflect.Value.Field(index) 获取结构体的字段信息
reflect.Value.Kind() 获取变量的具体类型,如 reflect.Struct
reflect.Value.FieldByName(name) 通过字段名获取结构体的字段
reflect.Value.CanSet() 判断字段是否可修改
reflect.Value.Set(value) 修改字段的值
reflect.Append(s Value, x ...Value) 切片s中增加元素x

7. 结论

Golang 的反射提供了强大的运行时类型检查和动态操作能力,适用于需要高灵活性的场景,如序列化、数据库映射、动态 API 解析等。然而,反射的性能开销较大,因此在业务场景中使用较少,常用于底层的驱动构建。