笔者目前在实习,有一个需求是这样的,需要监听mysql的biinlog日志,根据日志实时同步操作,这里涉及到一个问题,binlog 的字段顺序可能变化,我们不知道结构体字段顺序、数量,要怎么从binlog事件中提取出数据并转换为表结构对应的GO结构体呢?
什么是反射,笼统的概念不好理解,直接上代码就行,对于一个字段field,reflect.ValueOf(field)
就是获取它的反射值,从下面的代码可以看到函数返回的是Value类型
swift
func ValueOf(i any) Value {
if i == nil {
return Value{}
}
return unpackEface(i)
}
Value有如下定义,这么一看就懂了,返回的是指针!
rust
type Value struct {
typ_ *abi.Type
ptr unsafe.Pointer
flag
}
那么对于字段field,我们获取了它的指针,要怎么对它进行修改呢?答案是指针解引用,也就是对这个指针指向的内容进行修改,这个时候就要调用Elem(),即reflect.ValueOf(field).Elem()返回的就是这个指针指向的内容,但是我们发现返回值还是Value类型,其实可以理解成引用,不过已经是field本身了,修改会影响其值,这样不管是我们是直接赋值,还是传入其他方法在方法中修改,都会修改到field
看下我是怎么用的,这里因为涉及到项目代码,所以我脱敏了,以下调用是当我监听到event(binlog事件)的时候,调用这个方法去把binlog日志中的字段转成GO结构体的字段,入参是表的字段名切片和行数据,返回值就是转换好的GO结构体
scss
convertToStruct(event.Table.Columns, row)
看下这个方法的具体实现,Binlog 行数据转换为结构体,先得到binlog一条记录的一个字段,然后查GO结构体有没有字段和它是对应的,如果有就赋值
go
// convertToStruct 将 binlog columns 和 values 转换为结构体
func convertToStruct(columns []TableColumn, values []any) (*MyStruct, error) {
result := &MyStruct{}
// Elem()获取result结构体本身
resultVal := reflect.ValueOf(result).Elem()
for i, column := range columns {
fieldVal := convertColumnValue(column, values[i])
if fieldVal == nil {
continue // NULL 或不支持类型,跳过
}
// 根据 column.Name 查找 struct tag,动态设置字段
for j := 0; j < resultVal.NumField(); j++ {
field := resultVal.Type().Field(j)
if field.Tag.Get("column") == column.Name { // tag 匹配,
if err := setFieldValue(resultVal.Field(j), fieldVal); err != nil {
fmt.Printf("Failed to set field %s: %v\n", column.Name, err)
}
break
}
}
}
return result, nil
}
转换字段类型
go
func convertColumnValue(column TableColumn, value any) any {
if value == nil {
return nil
}
switch column.RawType {
case "int", "bigint":
switch v := value.(type) {
case int64:
return int32(v) // 简化为 int32
}
case "float", "double":
switch v := value.(type) {
case float64:
return float32(v)
}
case "varchar", "text":
if v, ok := value.(string); ok {
return v
}
case "datetime":
switch v := value.(type) {
case string:
t, _ := time.Parse("2006-01-02 15:04:05", v)
return t
case time.Time:
return v
}
default:
fmt.Printf("Unsupported type: %s\n", column.RawType)
}
return value
}
利用反射动态赋值字段,这里因为是引用,所以对字段的修改会影响原值
swift
func setFieldValue(field reflect.Value, value any) error {
if !field.CanSet() {
return fmt.Errorf("field cannot be set")
}
valueVal := reflect.ValueOf(value)
if !valueVal.Type().ConvertibleTo(field.Type()) {
return fmt.Errorf("cannot convert %v to %v", valueVal.Type(), field.Type())
}
field.Set(valueVal.Convert(field.Type()))
return nil
}
好处
- 动态匹配数据库字段名 → 通过
tag
动态找到对应 struct 字段 - 兼容不同类型 → 数据库字段类型和结构体字段类型不完全一致,反射允许灵活转换