首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >GO反射原理&使用

GO反射原理&使用

作者头像
王小明_HIT
发布2025-07-03 15:48:19
发布2025-07-03 15:48:19
1380
举报
文章被收录于专栏:程序员奇点程序员奇点

Go 与典型编译型 / 解释型语言的差异

关键问题与答案

  1. 什么是 Go 语言的反射?其基础组成有哪些?

答案:反射是 Go 语言在编译时未知类型的情况下,操作变量、查看值、调用方法及操作布局的机制。基础组成包括:

  • reflect包提供运行时反射能力;
  • reflect.TypeOf获取运行时类型信息;
  • reflect.ValueOf获取数据的运行时表示;
  • reflect.Type(类型信息接口)和 reflect.Value(值的结构体)为核心类型。
  1. Go 反射的三大法则具体指什么?

答案:

  • 第一法则:从interface{}变量反射出反射对象(通过reflect.TypeOf和reflect.ValueOf实现);
  • 第二法则:从反射对象获取interface{}变量(通过reflect.Value.Interface方法);
  • 第三法则:更新reflect.Value时,其持有的值必须可更新(需确保反射对象指向可寻址的原始变量)。
  1. Go 反射中interface{}类型的底层表示是什么?如何通过反射获取类型和值?

答案:

  • interface{}在底层由reflect.emptyInterface结构体表示(包含类型指针typ和数据指针word);
  • reflect.TypeOf通过将输入变量转换为emptyInterface获取类型信息;
  • reflect.ValueOf通过unpackEface函数将emptyInterface包装成reflect.Value结构体(包含类型、数据指针和标记)。
  1. 反射如何实现变量更新?需要满足什么条件?

答案:

  • 更新通过reflect.Value.Set方法实现,需满足两个条件:反射对象可设置(mustBeAssignable检查)、字段公开(mustBeExported检查); Set方法调用assignTo根据目标类型创建新的reflect.Value,并通过typedmemmove覆盖原始值。
  1. 反射调用方法的过程是怎样的?

答案:

  • 通过reflect.Value.Call方法调用,步骤包括:校验函数类型和可见性;
  • 计算参数和返回值的栈布局(funcLayout);
  • 分配内存并将参数拷贝到栈上;
  • 执行函数指针调用(call函数);
  • 处理返回值(转换为reflect.Value并返回)。
  1. 反射与直接调用的性能差距有多大? 答案:通过性能测试表明,反射调用(字段设置、方法调用等)与直接调用的性能差距约为 10 倍。
  2. 反射的优化策略有哪些?

答案: 字段值设置优化:缓存Field对象(避免重复查找字段); 使用unsafe.Pointer直接操作内存(减少反射调用开销)。

反射基础

Go语言提供了一种机制,在编译时不知道类型的情况下,可更新变量、在运行时查看值、调用方法以及直接对它们的布局进行操作,这种机制称为反射 reflect 实现了运行时的反射能力,能够让程序操作不同类型的对象。反射包中有两对非常重要的函数和类型,两个函数分别是:

  • reflect.TypeOf 能获取运行时类型信息
  • reflect.ValueOf 能获取数据的运行时表示

两个类型是 reflect.Type 和 reflect.Value,它们与上边两个函数是一一对应的关系

reflect中Type接口

代码语言:javascript
复制
type Type interface {
    Method(int) Method // 方法返回类型的方法集中的第 i 个方法
    MethodByName(string) (Method, bool) // MethodByName 返回类型的方法集中具有该名称的方法和指示是否找到该方法的布尔值
    NumMethod() int // NumMethod 返回使用 Method 可访问的方法数
    Name() string //类型名称
    PkgPath() string //包路径
    Size() uintptr //返回类型大小
    String() string //类型
    Kind() Kind //反射对象的类型
    Implements(u Type) bool //反射对象实现某接口
    AssignableTo(u Type) bool // 是否可以赋值给 u
    ConvertibleTo(u Type) bool // 是否可以类型转换成 u
    ...
}

reflect 中 Value 结构体

代码语言:javascript
复制
type Value struct { //这些成员都是私有的,非导出的
    typ *rtype // typ保存由Value表示的值的类型
    ptr unsafe.Pointer // 指向数据的指针
    flag //标记保存有关该值的元数据
}
func (v Value) Elem() Value //获取接口、指针的数据。同样是一个Value对象。
func (v Value) Set(x Value) //设置值 

Go反射三大法则

第一法则 从inerface{}变量可以反射出反射对象 reflect.TypeOf 和 reflect.ValueOf 函数是完成数据与反射数据的转换的方法,如果我们认为 Go 语言的类型和反射类型处于两个不同的世界,那么这两个函数就是连接这两个世界的桥梁

代码语言:javascript
复制
package main

import (
"fmt"
"reflect"
)

func main() {
       author := "draven"
       fmt.Println("TypeOf author:", reflect.TypeOf(author)) //author: string
       fmt.Println("ValueOf author:", reflect.ValueOf(author))} //author: draven
}

有了反射变量的类型之后,我们可以通过 Method 方法获得类型实现的方法,通过 Field 获取类型包含的全部字段。对于不同的类型,我们也可以调用不同的方法获取相关信息

  • 结构体:获取字段的数量并通过下标和字段名获取字段StructField
  • 哈希表:获取哈希表的 Key 类型
  • 函数或方法:获取入参和返回值的类型 获取类型
代码语言:javascript
复制
package main
import (
    "fmt"
    "reflect"
)
func main() {
    x := 10
    name := "Go Lang"
    type Book struct {
        name string
        author string
    }
    sampleBook := Book {"Reflection in Go", "John"}
    fmt.Println(reflect.TypeOf(x).Kind()) // int 
    fmt.Println(reflect.TypeOf(name).Kind()) // string
    fmt.Println(reflect.TypeOf(sampleBook).Kind()) // struct
}

方法执行

代码语言:javascript
复制
type foo struct {
    name string
}

func (f *foo) Test() {
    fmt.Println("hi " + f.name)
}

func main() {
    temp := foo{
       name: "test",
    }
    fooV := reflect.ValueOf(&temp)
    fooV.MethodByName("Test").Call(nil) // hi test
}

第二法则 可以从反射对象获取interface{}变量既然能够将接口类型的变量转换成反射对象,那么一定需要其他方法将反射对象还原成接口类型的变量,reflect 中的 reflect.Value.Interface 就能完成这项工作:

图片
图片
代码语言:javascript
复制
v := reflect.ValueOf(1)
v.Interface().(int)

从反射对象到接口值的过程是从接口值到反射对象的镜面过程,两个过程都需要经历两次转换:

  • 从接口值到反射对象:
    • 从基本类型到接口类型的类型转换;
    • 从接口类型到反射对象的转换;
  • 从反射对象到接口值:
    • 反射对象转换成接口类型;
    • 通过显式类型转换变成原始类型;

第三法则  如果想要更新一个reflect.Value,那么它持有的值一定是可以被更新

代码语言:javascript
复制
func main() {
        i := 1
        v := reflect.ValueOf(i)
        v.SetInt(10)
        fmt.Println(i)
        }
panic: reflect: reflect.flag.mustBeAssignable using unaddressable value

因为go 方法是值传递,所以我们得到的反射对象跟最开始的变量没有任何关系,那么直接修改反射对象无法改变原始变量,程序为了防止错误就会崩溃。

代码语言:javascript
复制
func main() {
        i := 1
        v := reflect.ValueOf(&i)
        v.Elem().SetInt(10)
        fmt.Println(i)
        }

反射原理

interface类型

Go 语言的 interface{} 类型在语言内部是通过 reflect.emptyInterface 结体表示

图片
图片
代码语言:javascript
复制
type emptyInterface struct {
        typ  *rtype //变量类型
        word unsafe.Pointer //变量数据
}

reflect.TypeOf 函数实现将传入的变量隐式转换成 reflect.emptyInterface 类型并获取其中存储的类型信息 reflect.rtype:

代码语言:javascript
复制
func TypeOf(i interface{}) Type {
        eface := *(*emptyInterface)(unsafe.Pointer(&i))
        return toType(eface.typ)
        }
func toType(t *rtype) Type {
    if t == nil {
        return nil
    }
    return t
}

reflect.ValueOf 函数首先将对象逃逸到堆中,然后通过 reflect.unpackEface 获取reflect.Value 结构体 unpackEface中将类型和值指针包装成Value 返回

代码语言:javascript
复制
func ValueOf(i interface{}) Value {
    if i == nil {
        return Value{}
    }
    escapes(i)
    return unpackEface(i)
}
func unpackEface(i interface{}) Value {
        e := (*emptyInterface)(unsafe.Pointer(&i))
        t := e.typ
        if t == nil {
            return Value{}
        }
        f := flag(t.Kind())
        if ifaceIndir(t) {
                f |= flagIndir
        }
        return Value{t, e.word, f}
 }
代码语言:javascript
复制
package main

import (
   "fmt"
   "reflect"
)
type Temp struct {
   temp string
}

func main() {
   temp := Temp{temp: "123"}
   typeP := reflect.TypeOf(temp)
   fmt.Println(typeP.Name())
}
 go build -gcflags="-S -N" main.go

        MOVQ        AX, ""..autotmp_8+112(SP) //将temp 转换成emptyInterface
        MOVQ        $3, ""..autotmp_8+120(SP)//将temp 转换成emptyInterface
        LEAQ        type."".Temp(SB), AX //Tmep 的类型
        MOVQ        AX, reflect.i+80(SP) //放到调用reflect.TypeOf 栈中
        LEAQ        ""..autotmp_8+112(SP), AX //temp 值
        MOVQ        AX, reflect.i+88(SP) //放到调用reflect.TypeOf 栈中

从上面这段截取的汇编语言,我们可以发现在函数调用之前已经发生了类型转换,上述指令将 结构体的变量转换成了占用 16 字节autotmp_8+112(SP) ~ autotmp_8+120(SP)的接口,分别是 emptyInterface类型和数据两个 LEAQ 指令分别获取了类型的指针 type.Temp(SB)以及变量 temp所在的地址。

更新变量

当我们想要更新 reflect.Value 时,就需要调用 reflect.Value.Set 更新反射对象,该方法会调用 reflect.flag.mustBeAssignable 和 reflect.flag.mustBeExported 分别检查当前反射对象是否是可以被设置的以及字段是否是对外公开的:

代码语言:javascript
复制
func (v Value) Set(x Value) {
        v.mustBeAssignable()
        x.mustBeExported()var target unsafe.Pointer
        if v.kind() == Interface {
                target = v.ptr
        }
        x = x.assignTo("reflect.Set", v.typ, target)
        typedmemmove(v.typ, v.ptr, x.ptr)
 }

reflect.Value.Set 会调用 reflect.Value.assignTo 并返回一个新的反射对象,这个返回的反射对象指针会直接覆盖原反射变量。

代码语言:javascript
复制
func (v Value) assignTo(context string, dst *rtype, target unsafe.Pointer) Value {
   if v.flag&flagMethod != 0 {
      v = makeMethodValue(context, v)
   }

   switch {
   case directlyAssignable(dst, v.typ):
      // Overwrite type so that they match.
      // Same memory layout, so no harm done.
      fl := v.flag&(flagAddr|flagIndir) | v.flag.ro()
      fl |= flag(dst.Kind())
      return Value{dst, v.ptr, fl}

   case implements(dst, v.typ):
      if target == nil {
         target = unsafe_New(dst)
      }
      if v.Kind() == Interface && v.IsNil() {
         // A nil ReadWriter passed to nil Reader is OK,
         // but using ifaceE2I below will panic.
         // Avoid the panic by returning a nil dst (e.g., Reader) explicitly.
         return Value{dst, nil, flag(Interface)}
      }
      x := valueInterface(v, false)
      if dst.NumMethod() == 0 {
         *(*any)(target) = x
      } else {
         ifaceE2I(dst, x, target)
      }
      return Value{dst, target, flagIndir | flag(Interface)}
   }

   // Failed.
   panic(context + ": value of type " + v.typ.String() + " is not assignable to type " + dst.String())
}

reflect.Value.assignTo 会根据当前和被设置的反射对象类型创建一个新的 reflect.Value 结构体:

  • 如果两个反射对象的类型是可以被直接替换,就会直接返回目标反射对象;
  • 如果当前反射对象是接口并且目标对象实现了接口,就会把目标对象简单包装成接口值; 在变量更新的过程中,reflect.Value.assignTo 返回的 reflect.Value 中的指针会覆盖当前反射对象中的指针实现变量的更新。

方法调用

代码语言:javascript
复制
func (v Value) Call(in []Value) []Value {
        v.mustBe(Func)
        v.mustBeExported()
        return v.call("Call", in)
}

方法首先对函数类型和可见性进行校验。然后执行 value.call方法 v.call执行方法过程包括下面几部分

  1. 检查输入参数以及类型的合法性;
  2. 将传入的 reflect.Value 参数数组设置到栈上;
  3. 通过函数指针和输入参数调用函数;
  4. 从栈上获取函数的返回值; 准备参数
代码语言:javascript
复制
nout := t.NumOut()
frametype, _, retOffset, _, framePool := funcLayout(t, rcvrtype)

// Allocate a chunk of memory for frame if needed.
var stackArgs unsafe.Pointer
if frametype.size != 0 {
   if nout == 0 {
      stackArgs = framePool.Get().(unsafe.Pointer)
   } else {
      // Can't use pool if the function has return values.
      // We will leak pointer to args in ret, so its lifetime is not scoped.
      stackArgs = unsafe_New(frametype)
   }
}

// Handle arguments.
for i, v := range in {
   v.mustBeExported()
   targ := t.In(i).(*rtype)
   // TODO(mknyszek): Figure out if it's possible to get some
   // scratch space for this assignment check. Previously, it
   // was possible to use space in the argument frame.
   v = v.assignTo("reflect.Value.Call", targ, nil)
stepsLoop:
   for _, st := range abi.call.stepsForValue(i + inStart) {
      switch st.kind {
      case abiStepStack:
         // Copy values to the "stack."
         addr := add(stackArgs, st.stkOff, "precomputed stack arg offset")
         if v.flag&flagIndir != 0 {
            typedmemmove(targ, addr, v.ptr)
         } else {
            *(*unsafe.Pointer)(addr) = v.ptr
         }
         // There's only one step for a stack-allocated value.
         break stepsLoop
      case abiStepIntReg, abiStepPointer:
          ***  
    }      
 }
frameSize = align(frameSize, goarch.PtrSize)
frameSize += abi.spill

// Mark pointers in registers for the return path.
regArgs.ReturnIsPtr = abi.outRegPtrs
  1. 通过 reflect.funcLayout 计算当前函数需要的参数和返回值的栈布局,也就是每一个参数和返回值所占的空间大小;
  2. 如果当前函数有返回值,需要为当前函数的参数和返回值分配一片内存空间 args;
  3. 如果当前函数是方法,需要向将方法的接收接收者者拷贝到 args 内存中;
  4. 将所有函数的参数按照顺序依次拷贝到对应 args 内存中
  5. 使用 reflect.funcLayout 返回的参数计算参数在内存中的位置;
  6. 将参数拷贝到内存空间中; 调用函数 准备好调用函数需要的全部参数后,就会通过下面的代码执行函数指针了。我们会向该函数传入栈类型、函数指针、参数和返回值的内存空间、栈的大小以及返回值的偏移量:
代码语言:javascript
复制
call(frametype, fn, args, uint32(frametype.size), uint32(retOffset))

x86架构实现 https://github.com/golang/go/blob/f5d494bbdf945f2662eb4da45cdb75de2b7d43d4/src/runtime/asm_amd64.s#L371

代码语言:javascript
复制
#define CALLFN(NAME,MAXSIZE)                        \
TEXT NAME(SB), WRAPPER, $MAXSIZE-32;                \
        NO_LOCAL_POINTERS;                        \
        /* copy arguments to stack */                \
        MOVQ        argptr+16(FP), SI;                \
        MOVLQZX argsize+24(FP), CX;                \
        MOVQ        SP, DI;                                \
        REP;MOVSB;                                \
        /* call function */                        \
        MOVQ        f+8(FP), DX;                        \
        PCDATA  $PCDATA_StackMapIndex, $0;        \
        CALL        (DX);                                \
        /* copy return values back */                \
        MOVQ        argptr+16(FP), DI;                \
        MOVLQZX        argsize+24(FP), CX;                \
        MOVLQZX retoffset+28(FP), BX;                \
        MOVQ        SP, SI;                                \
        ADDQ        BX, DI;                                \
        ADDQ        BX, SI;                                \
        SUBQ        BX, CX;                                \
        REP;MOVSB;                                \
        /* execute write barrier updates */        \
        MOVQ        argtype+0(FP), DX;                \
        MOVQ        argptr+16(FP), DI;                \
        MOVLQZX        argsize+24(FP), CX;                \
        MOVLQZX retoffset+28(FP), BX;                \
        MOVQ        DX, 0(SP);                        \
        MOVQ        DI, 8(SP);                        \
        MOVQ        CX, 16(SP);                        \
        MOVQ        BX, 24(SP);                        \
        CALL        runtime·callwritebarrier(SB);        \
        RET

处理返回值 当函数调用结束之后,就会开始处理函数的返回值:

  • 如果函数没有任何返回值,会直接清空 args 中的全部内容来释放内存空间;
  • 如果当前函数有返回值;
    1. 将 args 中与输入参数有关的内存空间清空;
    2. 创建一个 nout 长度的切片用于保存由反射对象构成的返回值数组;
    3. 从函数对象中获取返回值的类型和内存大小,将 args 内存中的数据转换成 reflect.Value 类型并存储到切片中;
代码语言:javascript
复制
if nout == 0 {
   if stackArgs != nil {
      typedmemclr(frametype, stackArgs)
      framePool.Put(stackArgs)
   }
} else {
   if stackArgs != nil {
      // Zero the now unused input area of args,
      // because the Values returned by this function contain pointers to the args object,
      // and will thus keep the args object alive indefinitely.
      typedmemclrpartial(frametype, stackArgs, 0, abi.retOffset)
   }
   // Wrap Values around return values in args.
ret = make([]Value, nout)
for i := 0; i < nout; i++ {
   tv := t.Out(i)
   if tv.Size() == 0 {
      // For zero-sized return value, args+off may point to the next object.
      // In this case, return the zero value instead.
      ret[i] = Zero(tv)
      continue
   }
   steps := abi.ret.stepsForValue(i)
   if st := steps[0]; st.kind == abiStepStack {
   // This value is on the stack. If part of a value is stack
   // allocated, the entire value is according to the ABI. So
   // just make an indirection into the allocated frame.
   fl := flagIndir | flag(tv.Kind())
   ret[i] = Value{tv.common(), add(stackArgs, st.stkOff, "tv.Size() != 0"), fl}
   // Note: this does introduce false sharing between results -
   // if any result is live, they are all live.
   // (And the space for the args is live as well, but as we've
   // cleared that space it isn't as big a deal.)
   continue
   ***
}

反射性能

代码语言:javascript
复制
func Benchmark_Reflect_Field(b *testing.B) {
   var tf = new(TestReflectField)
   temp := reflect.ValueOf(tf).Elem()
   for i := 0; i < b.N; i++ {
      temp.Field(1).SetInt(int64(25))
   }
   _ = tf
}

// 测试反射设置结构体字段的性能
func Benchmark_Reflect_FieldByName(b *testing.B) {
   var tf = new(TestReflectField)
   temp := reflect.ValueOf(tf).Elem()
   for i := 0; i < b.N; i++ {
      temp.FieldByName("Age").SetInt(int64(25))
   }

   _ = tf
}

// 测试结构体字段设置的性能
func Benchmark_Field(b *testing.B) {
   var tf = new(TestReflectField)
   for i := 0; i < b.N; i++ {
      tf.Age = i
   }
   _ = tf

}

通过反射与直接调用性能差距在10倍左右

代码语言:javascript
复制
func BenchmarkMethod(b *testing.B) {
   t := &TestReflectField{}
   for i := 0; i < b.N; i++ {
      t.Func0()
   }

}
// 测试通过序号反射访问无参数方法性能
func BenchmarkReflectMethod(b *testing.B) {
   v := reflect.ValueOf(&TestReflectField{})
   for i := 0; i < b.N; i++ {
      v.Method(0).Call(nil)
   }

}
// 测试通过名称反射访问无参数方法性能
func BenchmarkReflectMethodByName(b *testing.B) {
   v := reflect.ValueOf(&TestReflectField{})
   for i := 0; i < b.N; i++ {
      v.MethodByName("Func0").Call(nil)
   }

}
// 测试通过反射访问有参数方法性能
func BenchmarkReflectMethod_WithArgs(b *testing.B) {
   v := reflect.ValueOf(&TestReflectField{})
   for i := 0; i < b.N; i++ {
      v.Method(1).Call([]reflect.Value{reflect.ValueOf(i)})
   }

}
// 测试通过反射访问结构体参数方法性能
func BenchmarkReflectMethod_WithArgs_Mul(b *testing.B) {
   v := reflect.ValueOf(&TestReflectField{})
   for i := 0; i < b.N; i++ {
      v.Method(2).Call([]reflect.Value{reflect.ValueOf(TestReflectField{})})
   }
}
// 测试通过反射访问接口参数方法性能
func BenchmarkReflectMethod_WithArgs_Interface(b *testing.B) {
   v := reflect.ValueOf(&TestReflectField{})
   for i := 0; i < b.N; i++ {
      var tf TestInterface = &TestReflectField{}
      v.Method(3).Call([]reflect.Value{reflect.ValueOf(tf)})
   }
}
// 测试访问多参数方法性能
func BenchmarkMethod_WithManyArgs(b *testing.B) {
   s := &TestReflectField{}
   for i := 0; i < b.N; i++ {
      s.Func4(i, i, i, i, i, i)
   }
}
// 测试通过反射访问多参数方法性能
func BenchmarkReflectMethod_WithManyArgs(b *testing.B) {
   v := reflect.ValueOf(&TestReflectField{})
   va := make([]reflect.Value, 0)
   for i := 1; i <= 6; i++ {
      va = append(va, reflect.ValueOf(i))
   }
   for i := 0; i < b.N; i++ {
      v.Method(4).Call(va)
   }
}
// 测试访问有返回值的方法性能
func BenchmarkMethod_WithResp(b *testing.B) {
   s := &TestReflectField{}
   for i := 0; i < b.N; i++ {
      _ = s.Func5()
   }
}
// 测试通过反射访问有返回值的方法性能
func BenchmarkReflectMethod_WithResp(b *testing.B) {
   v := reflect.ValueOf(&TestReflectField{})
   for i := 0; i < b.N; i++ {
      _ = v.Method(5).Call(nil)[0].Int()
   }

}

通过反射与正常方法调用性能差距在10倍左右

反射优化

字段值设置

  • 缓存Field对象
  • 使用unsafe.Pointer 进行字段设置

引用资料 https://go.dev/blog/laws-of-reflection Go 语言设计与实现 https://juejin.cn/post/6844903559796883463

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-07-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员奇点 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Go 与典型编译型 / 解释型语言的差异
  • 关键问题与答案
  • 反射基础
  • Go反射三大法则
  • 反射原理
    • interface类型
  • 更新变量
  • 方法调用
    • 反射性能
  • 反射优化
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档