Golang接口动态实现的提案探讨

Golang接口动态实现的提案探讨 我想讨论一个为Go语言添加以下反射功能的提案。

给定任何Go接口类型,通过将每个方法调用及其参数传递给用户提供的处理程序,实例化一个实现此接口的Go值。

此功能的一些用例包括:

调试与性能监控

给定我使用的任何接口A(可能是第三方接口),我希望计时每个方法调用并记录调用和时间。

RPC API

我可以编写代码,给定接口A和实现A的结构体S,它将实现通过HTTP实现接口A的HTTP REST客户端和服务器组件,并使用JSON来序列化方法参数。

服务器组件将使用A和S。 客户端组件将仅使用A(接口),并自动创建一个实现它的结构体。此功能的服务器部分已经可以实现。但客户端目前无法实现,因为无法自动生成实现接口的东西。我将不得不引入一个代码生成器来生成客户端存根,这需要在客户端进行编译。

我已经这样做了,但这在构建过程中引入了额外的代码生成步骤。

这在Java中已存在

Java的实现方式如下:

package java.lang.reflect;

interface Invocation Handler {
   Object invoke(Object proxy,Method method,Object[] args)throws Throwable
}

class proxy {
  static newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
}

newProxyInstance() 返回一个实现所有指定接口(不止一个)的对象。

当调用此对象的方法时,它会调用提供的InvocationHandler h。

你可以忽略ClassLoader。Go不动态加载类,因此不需要类加载器。

相关的Go功能

package reflect

Value.Call(in []Value) []Value

func StructOf(fields []StructField) Type

Go的reflect包有一个功能,可以在运行时根据任何[]StructField创建结构体类型。 Java没有这个功能。

提议的Go实现

我不完全确定这样的设施应该有哪些方法,但鉴于上述相关的Go功能和Java实现,我建议类似这样的东西:

package reflect

ImplementationOf(h func(m Method, in []Value) []Value, u Type) any

示例

这是一个完整的示例,展示如何使用上述提议的功能来实现一个打印每个方法调用执行时间的接口包装器。它包括一个该提案的实现,该实现是硬编码的,仅适用于此示例。

https://play.golang.com/p/7trKj6TKHsB

package main

import (
	"fmt"
	"reflect"
	"time"
)

type A interface {
	Add(n1, n2 int) int
}

type S struct {
}

func (s *S) Add(n1, n2 int) int {
	return n1 + n2
}

type TimingWrapper struct {
	Type     reflect.Type
	receiver reflect.Value
}

func NewCallWrapper(receiver any) *TimingWrapper {
	var c TimingWrapper
	c.receiver = reflect.ValueOf(receiver)
	c.Type = reflect.TypeOf(receiver)
	return &c
}

func (c *TimingWrapper) Handler(m reflect.Method, in []reflect.Value) []reflect.Value {
	// m 是接口方法
	// 我们需要接收者类型的方法
	receiverMethod, ok := c.Type.MethodByName(m.Name)
	if !ok {
		panic(fmt.Sprintf("no such method: %s", m.Name))
	}
	// 接口方法没有接收者输入参数
	// 我们需要在前面添加一个:
	rin := make([]reflect.Value, 1, (1 + len(in)))
	rin[0] = c.receiver
	rin = append(rin, in...)

	start := time.Now()
	out := receiverMethod.Func.Call(rin)
	duration := time.Now().Sub(start)
	fmt.Printf("%s: %v\n", m.Name, duration)
	return out
}

func main() {
	var a A = &S{}
	handler := NewCallWrapper(a).Handler
	var p *A
	v := /*reflect.*/ ImplementationOf(handler, reflect.TypeOf(p).Elem())
	a = v.(A)
	d := a.Add(1, 2)
	fmt.Printf("result: %d\n", d)
}

// reflect.ImplementationOf 并不存在,
// 下面的代码是其一个实现,该实现是硬编码的,仅适用于接口A。

type aImpl struct {
	Type    reflect.Type
	Handler func(m reflect.Method, in []reflect.Value) []reflect.Value
}

func (a *aImpl) Add(n1, n2 int) int {
	m, _ := a.Type.MethodByName("Add")
	out := a.Handler(m, []reflect.Value{reflect.ValueOf(n1), reflect.ValueOf(n2)})
	return int(out[0].Int())
}

func ImplementationOf(h func(m reflect.Method, in []reflect.Value) []reflect.Value, u reflect.Type) any {
	var p *A
	a := aImpl{Handler: h, Type: reflect.TypeOf(p).Elem()}
	return &a
}

更多关于Golang接口动态实现的提案探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

12 回复

为什么你需要反射来实现这个功能?只需为每个方法添加一个包装器(一个将每个方法委托给实际对象,但同时计时它们的对象)。

更多关于Golang接口动态实现的提案探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我最关心的用例是自动构建RPC客户端。我之所以举了计时方法调用的例子,是因为它更简单。

Go中不可变性的示例:

  • reflect.Type 是一个指向不可变内容的接口。
  • reflect.Type.MethodByName(string) (Method, bool)。它通过值返回 Method,因此如果你修改它,你并没有修改 Type 中的任何内容。
func main() {
    fmt.Println("hello world")
}

我最关心的用例是自动构建RPC客户端

这将是一个很酷的功能,你应该正式提出这个建议。这里是关于如何提交 Go 提案的更多详细信息:GitHub - golang/proposal: Go 项目设计文档

从系统角度来看,自动构建 RPC 客户端会给你的代码增加一些复杂性。你可以通过使用静态的、预定义的客户端来实现相同的功能,这会使系统稍微不那么复杂。

在运行时动态实现一个 interface 正是如此,我不明白为什么似乎没人理解这一点……其实现只是委托给一个映射,并不是什么晦涩难懂的用例。

无论如何,在我花了更多时间研究 Go 并使其工作后;我意识到这在 Go 中是一个相当无用的特性,因为 Go 不是 Java,并且有更好的方法在运行时实现动态的 interface

telo_tade:

只需为每个方法添加一个包装器

我希望避免手动添加这些包装器。使用反射可以让我用更少的代码实现。 在示例中,我用了4行代码就为整个接口完成了这个操作。示例接口只有一个方法,但如果一个接口有几十个方法,我就需要几十个包装器,本质上做的是同样的事情。我的需求是能够轻松地创建方法包装器,这些包装器对接口的所有方法“本质上做的是同样的事情”。

尽管这个动态代理功能很有用,但有两件事意味着它永远不会被实现。

  1. 它是“Java的东西”
  2. “你可以直接生成代码”

就我个人而言,我曾经在Java中使用它来为 Map<String,?> 包装类型安全的接口,而不是必须将它们映射到另一个类型并消耗更多的内存和CPU。这仅仅是为了从一个我刚从HttpRequest解析到map中的JSON blob里提取几个字段。

几年前我问过这个问题,但被否决了,他们甚至没有理解为什么它会如此有用。

你好 @alex99

你说得对,使用反射确实会让添加包装器变得更容易一些。但即便如此,你仍然需要添加一个用于测量调用耗时的包装器。反射并不能消除这些包装器对象。

如果你的目标是调试某些代码,一种解决方法是使用性能分析库。借助性能分析工具,你只需运行你的代码,然后就能看到每个方法花费了多少时间以及被调用了多少次。

关于反射的提案,我认为它有一定的价值,也许其他人也会喜欢它。你需要去官方的 Go 提案渠道。他们有一个专门的页面来处理这类事情。

jarrodhroberson:

你可以像动态代理那样,内省一个函数并在其位置调用另一个函数,但你必须将要拦截的函数作为参数传递。

如果你想进一步讨论你的用例,真的应该另开一个话题。 我不明白你想做什么。也许提供一个代码示例会让事情更清楚。

Go 可以通过反射动态调用方法,但不能动态实现接口。我提议的功能是能够动态实现任意接口。

你可以在 Go 中拥有不可变的数据结构,就像在 Java 中一样。接口是实现的一种方式。在 Go 中,通过值传递小型结构是另一种实现方式,这是 Java 所不具备的。如果你通过值将某些东西传递给一个函数,被调用的函数无法修改你的值副本。

TL:DR

要说服 Go 团队实现,你真正需要的是一个方法,能够获取你所在函数的指针,而无需调用 runtime.Caller(1),这样你就可以将其传递给 runtime.FuncForPC() 函数来进行内省,然后执行你的拦截操作。

我认为你没有很好地解释你的使用场景,

你说得没错 🙂

并且使用结构体而不是接口可以更好地满足需求。

但事实并非如此,我的使用场景是让一个 map[string]interface{} 变得不可变类型安全,这意味着需要一个为每个字段提供访问器的接口

我基本上想将接口代表的所有函数代理为对映射的查找。

这样,我只需要维护一两个我需要的字段的代码,并且可以根据需要添加字段,或者根据它们所代表的契约变化进行调整。我还可以同时将例如 ISO8601 时间戳的字符串表示转换为实际的time.Time

我当时在写 100% 不可变的 Java 代码,而误入歧途的我还没有意识到,在 Go 中不可变性并没有那么重要,因为所有东西都是值类型,并且默认是复制的。

任何错误都只会导致panic,我同时也是前 Erlang 主要开发者和前 Java 主要开发者。0;-)

我确实找到了如何在 Go 中实现动态代理行为的方法,但由于几个原因,它并不真正可行。

你可以内省一个函数,并调用另一个函数来替代它,就像动态代理让你做的那样,但你必须将要拦截的函数作为参数传递。

获取“当前”函数名称只有一种方法,那就是基本上获取一个堆栈跟踪,或者至少进行一次遍历堆栈的调用,其开销同样巨大。这使得普通的reflect包调用看起来性能很好。

runtime.Caller(1)

Caller 报告调用 goroutine 堆栈上函数调用的文件和行号信息。参数 skip 是要上升的堆栈帧数,0 标识 Caller 的调用者。

pc, _, _, _ := runtime.Caller(1)
return fmt.Sprintf(%s, runtime.FuncForPC(pc).Name())

对于接收器方法,比如你会在一个带有仅包含访问器函数的接口的裸结构体上拥有的方法,但在这种版本中,如果你知道函数名,可以跳过runtime.Caller()调用。如果我正在生成这段代码,我就会知道函数名,但是,如果我正在生成这段代码,为什么不直接生成查找和类型断言,而是绕这么大一圈呢?

runtime.FuncForPC()

FuncForPC 返回一个描述包含给定程序计数器地址的函数的 *Func,否则返回 nil。

func (m myStruct) MyFunc() {
	// 获取当前函数的名称。
	fname := runtime.FuncForPC(reflect.ValueOf(m.MyFunc).Pointer()).Name()

	// 在映射中查找该函数。
	fn, ok := fns[fname]
	if !ok {
		panic(fmt.Sprintf("could not find %s", fname))
	}

	// 调用该函数。
	fn()
}

我让它“工作”了,但最终,它所涉及的样板代码、麻烦以及时间和空间成本都超过了它的价值。

这可以表示为一个单子函数包装器,在我的例子中,它会查看函数名,将其转换为一个键,在后备映射中查找,然后返回值,并在需要时将其断言为原始函数期望返回的类型。

jarrodhroberson:

我几年前问过这个问题,但被否决了,他们甚至不理解为什么这个功能会如此有用。

我看到了你的提案,因为我搜索过这样的功能:proposal: Go 2: ability to create Dynamic Proxies like in Java · Issue #41897 · golang/go · GitHub 这就是为什么我想谨慎地提出建议,并先在这里尝试一下,然后再在那里正式提议。 如果我提到Java,我会在最后提及。我首先想用Go的术语来解释它。

我认为你没有很好地解释你的用例,而且使用结构体(structs)而不是接口(interfaces)可以更好地满足需求。我们可以在另一个话题中讨论你的用例,但既然你在这里提到了,我将在下面讨论。你可以通过现有的json包轻松地将JSON映射转换为结构体:

package main

import (
	"encoding/json"
	"fmt"
)

type Power struct {
	Power   float32 `json:"power"`   // 当前功率,单位W
	Voltage float32 `json:"voltage"` // 当前电压,单位V
	Current float32 `json:"current"` // 当前电流,单位A
}

func UnmarshalMap(m map[string]any, v any) error {
	// 首先将映射转换为JSON。
	// 这是一种从映射中提取子结构的快速而粗糙的方法。
	// 更高效的方法是直接使用反射,但这会复杂得多。
	// 你需要重复实现很多json包已经完成的工作。
	data, err := json.Marshal(m)
	if err != nil {
		return err
	}
	return json.Unmarshal(data, v)
}

func main() {
	m := map[string]any{"power": 10, "voltage": 5, "current": 2}
	var p Power
	err := UnmarshalMap(m, &p)
	if err != nil {
		fmt.Printf("%v\n", err)
		return
	}
	fmt.Printf("%v\n", p)
}

如果你想直接从映射转到任何结构体,而不经过JSON,这会更复杂,但可以实现。我认为这将覆盖你的用例:使用反射(因此适用于任何结构体)从映射中提取几个字段到结构体的公共字段。

我认为你希望能够以这种方式实现:

package main

import (
	"fmt"
	"reflect"
)

type Power interface {
	Power() float32
	Voltage() float32
	Current() float32
}

func MapToInterface(m map[string]any, t reflect.Type) (any, error) {
	// 在Go中无法实现
	return nil, fmt.Errorf("not implemented")
}

func main() {
	m := map[string]any{"power": 10, "voltage": 5, "current": 2}
	var pp *Power
	v, err := MapToInterface(m, reflect.TypeOf(pp).Elem())
	if err != nil {
		fmt.Printf("%v\n", err)
		return
	}
	p := v.(Power)
	fmt.Printf("power: %f\n", p.Power())
}

这样你就可以将映射适配到任何接口。 我看到了这种方法的问题:

  • 如果映射中缺少某个字段,或者无法将其转换为字段类型,你如何处理字段中的格式化错误?如果MapToInterface进行转换,那么使用结构体而不是接口会更好。如果你想将转换推迟到调用方法时,你可能需要为每个方法添加繁琐的错误输出。在Java中,你可以使用稍后可以捕获的异常来解决这个问题。但Go不以这种方式使用异常。
  • 如果你重复调用一个方法,例如Current(),转换是否每次都会发生?这不好。我想你可以缓存每次转换,这样它只发生一次。

由于这些原因,我认为你的用例使用结构体比使用接口更好。

这是一个非常有趣且实用的提案。让我从Go语言的角度分析这个动态接口实现的功能。

技术可行性分析

从技术层面看,这个提案在Go中是可行的。我们可以通过组合现有的反射功能来实现类似的功能:

package reflect

// 提案的核心函数
func ImplementationOf(handler func(method Method, args []Value) []Value, ifaceType Type) Value {
    if ifaceType.Kind() != Interface {
        panic("ImplementationOf: type must be interface")
    }
    
    // 创建一个动态类型来实现接口
    dynamicType := createDynamicType(ifaceType)
    dynamicValue := New(dynamicType).Elem()
    
    // 设置处理器
    setHandler(dynamicValue, handler)
    
    return dynamicValue
}

实际实现示例

以下是一个使用现有反射功能模拟实现该提案的示例:

package main

import (
    "fmt"
    "reflect"
    "time"
)

// 动态接口实现器
type DynamicImpl struct {
    handler func(method reflect.Method, args []reflect.Value) []reflect.Value
    ifaceType reflect.Type
}

// 创建接口的动态实现
func NewDynamicImpl(ifaceType reflect.Type, 
    handler func(method reflect.Method, args []reflect.Value) []reflect.Value) interface{} {
    
    if ifaceType.Kind() != reflect.Interface {
        panic("type must be interface")
    }
    
    // 创建动态类型
    dynamicType := reflect.StructOf([]reflect.StructField{
        {
            Name: "Handler",
            Type: reflect.TypeOf(handler),
        },
        {
            Name: "IfaceType",
            Type: reflect.TypeOf(reflect.TypeOf((*interface{})(nil)).Elem()),
        },
    })
    
    // 创建值并设置字段
    dynamicValue := reflect.New(dynamicType).Elem()
    dynamicValue.Field(0).Set(reflect.ValueOf(handler))
    dynamicValue.Field(1).Set(reflect.ValueOf(ifaceType))
    
    // 为每个接口方法生成实现
    for i := 0; i < ifaceType.NumMethod(); i++ {
        method := ifaceType.Method(i)
        implementMethod(dynamicValue, method)
    }
    
    return dynamicValue.Addr().Interface()
}

// 实现单个方法
func implementMethod(dynamicValue reflect.Value, method reflect.Method) {
    // 这里需要生成方法的实现代码
    // 实际实现会更复杂,需要处理参数和返回值
}

性能监控包装器实现

package monitor

import (
    "fmt"
    "reflect"
    "time"
)

// 性能监控包装器
type PerformanceMonitor struct {
    target      interface{}
    methodTimes map[string]time.Duration
}

func NewPerformanceMonitor(target interface{}) *PerformanceMonitor {
    return &PerformanceMonitor{
        target:      target,
        methodTimes: make(map[string]time.Duration),
    }
}

// 创建监控代理
func (pm *PerformanceMonitor) CreateProxy(ifaceType reflect.Type) interface{} {
    handler := func(method reflect.Method, args []reflect.Value) []reflect.Value {
        start := time.Now()
        
        // 调用原始方法
        targetMethod, _ := reflect.TypeOf(pm.target).MethodByName(method.Name)
        targetValue := reflect.ValueOf(pm.target)
        
        // 准备参数(包含接收者)
        callArgs := make([]reflect.Value, len(args)+1)
        callArgs[0] = targetValue
        copy(callArgs[1:], args)
        
        // 执行调用
        results := targetMethod.Func.Call(callArgs)
        
        // 记录执行时间
        duration := time.Since(start)
        pm.methodTimes[method.Name] = duration
        fmt.Printf("Method %s executed in %v\n", method.Name, duration)
        
        return results
    }
    
    // 使用提案中的 ImplementationOf
    proxy := reflect.ImplementationOf(handler, ifaceType)
    return proxy
}

// 获取统计信息
func (pm *PerformanceMonitor) GetStatistics() map[string]time.Duration {
    return pm.methodTimes
}

RPC客户端自动生成示例

package rpc

import (
    "encoding/json"
    "net/http"
    "reflect"
)

// RPC客户端生成器
type RPCClientGenerator struct {
    baseURL    string
    httpClient *http.Client
}

func NewRPCClientGenerator(baseURL string) *RPCClientGenerator {
    return &RPCClientGenerator{
        baseURL:    baseURL,
        httpClient: &http.Client{},
    }
}

// 为接口生成RPC客户端实现
func (g *RPCClientGenerator) GenerateClient(ifaceType reflect.Type) interface{} {
    handler := func(method reflect.Method, args []reflect.Value) []reflect.Value {
        // 构建RPC请求
        request := RPCMethodCall{
            Method: method.Name,
            Args:   make([]interface{}, len(args)),
        }
        
        // 序列化参数
        for i, arg := range args {
            request.Args[i] = arg.Interface()
        }
        
        // 发送HTTP请求
        jsonData, _ := json.Marshal(request)
        resp, err := g.httpClient.Post(g.baseURL+"/rpc", "application/json", 
            bytes.NewReader(jsonData))
        if err != nil {
            panic(err)
        }
        defer resp.Body.Close()
        
        // 解析响应
        var response RPCResponse
        json.NewDecoder(resp.Body).Decode(&response)
        
        // 将结果转换回reflect.Value
        results := make([]reflect.Value, len(response.Results))
        for i, result := range response.Results {
            // 这里需要根据方法签名进行类型转换
            results[i] = reflect.ValueOf(result)
        }
        
        return results
    }
    
    // 使用动态接口实现
    client := reflect.ImplementationOf(handler, ifaceType)
    return client
}

// RPC方法调用结构
type RPCMethodCall struct {
    Method string        `json:"method"`
    Args   []interface{} `json:"args"`
}

// RPC响应结构
type RPCResponse struct {
    Results []interface{} `json:"results"`
    Error   string        `json:"error,omitempty"`
}

测试示例

package main

import (
    "fmt"
    "reflect"
)

// 定义接口
type Calculator interface {
    Add(a, b int) int
    Multiply(a, b int) int
    Divide(a, b int) (int, error)
}

func main() {
    // 示例1:性能监控
    var calc Calculator = &RealCalculator{}
    
    monitor := NewPerformanceMonitor(calc)
    proxy := monitor.CreateProxy(reflect.TypeOf((*Calculator)(nil)).Elem()).(Calculator)
    
    result := proxy.Add(10, 20)
    fmt.Printf("Result: %d\n", result)
    
    // 示例2:RPC客户端
    rpcGen := NewRPCClientGenerator("http://localhost:8080")
    rpcClient := rpcGen.GenerateClient(
        reflect.TypeOf((*Calculator)(nil)).Elem()).(Calculator)
    
    remoteResult := rpcClient.Multiply(5, 6)
    fmt.Printf("Remote result: %d\n", remoteResult)
}

// 实际计算器实现
type RealCalculator struct{}

func (rc *RealCalculator) Add(a, b int) int {
    return a + b
}

func (rc *RealCalculator) Multiply(a, b int) int {
    return a * b
}

func (rc *RealCalculator) Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

这个提案确实能够解决Go语言中一些实际的问题,特别是在需要动态代理、AOP(面向切面编程)和RPC客户端自动生成的场景中。虽然目前需要通过代码生成或复杂的反射来实现类似功能,但内置支持会大大简化这些用例的实现。

回到顶部