Golang中如何将interface{}转换为指定的接口类型?

Golang中如何将interface{}转换为指定的接口类型? 主题始于:https://github.com/golang/go/issues/32432

我已将问题简化为以下代码:

package main

type Incrementor interface {
	Increment()
}

type I int

func (i *I) Increment() {
	*i++
}

func main() {
	var i I

	var j interface{} = i
	j.(Incrementor).Increment() // panic: interface conversion: main.I is not main.Incrementor: missing method Increment
}

我需要在变量 j 上调用 Increment() 方法吗?有没有办法实现这个操作?


更多关于Golang中如何将interface{}转换为指定的接口类型?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

18 回复
var j interface{} = &i

更多关于Golang中如何将interface{}转换为指定的接口类型?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


太棒了!这正是我问题的解决方案!我曾考虑过类似的方法,但没想到实现起来如此简单。

// 递归实现部分已省略 // ValidateValue 深入值元素并递归调用自身

那么这部分具体是在做什么呢?

关键在于这类项目并不要求必须实现接口,例如当值未实现接口时,验证过程会直接跳过。以包含结构体的切片为例:当我们递归验证结构体时,切片本身并不需要实现接口。

顺便提一下,https://github.com/dealancer/validate/blob/v2/validate.go#L188 处的代码看起来没问题。

也许有人能想出办法,但我认为你无法两者兼得:要么在函数中要求验证器,这样能够静态检查指针接收器,但失去了跳过根值验证的能力(或在文档中建议用户编写虚拟实现);要么允许跳过未实现接口的值,但必须接受值和指针类型的方法集不同,并且也会跳过它们。

我仍然不太理解你想要实现什么。

是的,获取接口的地址并将其放入另一个接口中不太可能有帮助。这不是我的建议。在你最初的代码中,你希望在将具体类型放入接口之前获取其地址,这样它就能同时实现相关的方法。

但你现在已经把代码示例改成了其他内容。这个新代码缺乏足够的上下文来回答你的问题。CustomValidators 是定义在哪些类型上的?你能提供一个展示问题的自包含示例吗?你认为应该发生什么,但实际上没有发生?

i 本身是接口时,这种方法无法正常工作。例如我在此处需要类似的功能:https://github.com/dealancer/validate/blob/v2/validate.go#L188

我尝试使用以下代码,但没有成功:

	// Call a custom validator
	if value.CanInterface() {
		iface := value.Interface()
		var ifaceRef interface{} = &iface
		if customValidator, ok := ifaceRef.(CustomValidator); ok {
			err := customValidator.Validate()
			if err != nil {
				return err
			}
		}
	}

有什么建议吗?

将类型转换为接口的语法类似,但工作方式不同。为什么?

package main

type Incrementor interface {
	Increment()
}

type I int

func (i *I) Increment() {
	*i++
}

func main() {
	var i I

	// 正常工作
	var j interface{} = i
	jUnboxed := j.(I) // 这是可以的
	jUnboxed.Increment()
	
	// 无法工作
	var k interface{} = i
	kUnboxed := k.(Incrementor) // panic: interface conversion: main.I is not main.Incrementor: missing method Increment
	kUnboxed.Increment()
}

你的做法实际上是正确的,但问题出在其他地方。你在 *I 上声明了 Increment 方法,而不是在 I 上,这意味着该方法只能通过指针接收器调用。你试图将 I 断言为 Incrementor,而实际上应该将 *I 断言为 Incrementor,如下所示:

package main

type Incrementor interface {
	Increment()
}

type I int

func (i *I) Increment() {
	*i++
}

func main() {
	i := new(I)

	var j interface{} = i
	j.(Incrementor).Increment()
}

当然,如果你要将类型 T 转换为 Validator,那么 T 上必须定义 Validate 函数。在 *T 上定义的函数是不行的,就像在其他类型 U 上定义的 Validate 函数也不行一样。

或许你可以这样做:

func ValidateValue(value reflect.Value) error {
	if validator, ok := value.Interface().(Validator); ok { …; return }
	t := reflect.New(value.Type()) // 类型为 *T
	t.Elem().Set(value) // 将值复制到 *T
	if validator, ok := t.Interface().(Validator); ok { …; return }
}

需要注意的是,这会复制底层对象,因此 Validate 指针方法所做的任何修改都会被静默忽略。

@iegomez

嗯,我理解您的意思。我认为在Go语言中,接口应该定义接收器类型,因为例如我不希望Validate在调用时改变结构体。如果用户通过指针接收器定义Validate,我有两种选择:

  1. 为用户提供文档说明不要指定指针接收器。如果用户没有阅读文档,很容易犯错
  2. 仍然通过反射调用,但如果用户期望任何修改,他将无法获得修改

这两种情况都不理想!它们可以通过语言层面的改变来消除!这样接口就可以拥有接收器类型。

type I interface {
  A() // 值接收器
  *B() // 指针接收器
}

我认为,你只是把错误地传递值而非指针并静默跳过Validate调用的问题,转移成了静默调用Validate却在传递值时无法完成任何修改的问题——即使用户只在指针接收器上定义了Validate方法并可能在其中进行修改。我的意思是,如果我传递一个值,理应预期它不会被修改;但如果没有在值类型上定义Validate方法,那么我真正应该预期的是该方法不会执行。无论如何,你仍然需要依赖文档说明,尤其是现在你加入了一个反直觉的备用方案,仅仅因为你猜测我犯了错误(万一我本就不希望你验证我的值,而只想验证其内部状态,所以才这样传递值而非指针呢?)

当然,这是你的代码库,你可以按喜好定义其语义。我只是认为,对于未实现Validate方法的对象,跳过验证调用才应该是既定的契约。

引用Johan链接文章中的内容:

现在要将Camel传递给LongWalk,需要传入指向Camel的指针:

c := &Camel{"Bill"}
LongWalk(c)

或

c := Camel{"Bill"}
LongWalk(&c)

注意:虽然你仍然可以直接在Camel上调用Walk方法:

c := Camel{"Bill"}
c.Walk(500) // 这样是可行的

能够这样做的原因是Go编译器会自动将这行代码转换为(&c).Walk(500)。但是,将值传入接口时这种机制就不起作用了。原因是接口中的值位于隐藏的内存位置,因此编译器无法自动为你获取该内存的指针(用Go的术语来说,这被称为"不可寻址")。

总结来说,如果指针类型实现了接口,存在语法糖允许你从非指针值调用方法,编译器会为你获取指针并进行调用。所以在第一种情况下,你正在进行转换然后调用方法,这会触发上述机制。在第二种情况下,你试图将类型为I的变量转换为Incrementor接口,但I并没有真正实现Incrementor接口,因此如预期那样会出现错误。

我不太确定是否有好的方法来实现这一点,毕竟你是在运行时检查传入的值是否实现了接口。这与用户传入任何未实现Validator接口的值类型没有本质区别,就像这个例子中展示的那样,因此你可以返回一个错误说明:

// 第三方包提供的Validator接口
type Validator interface {
	Validate() error
}

func ValidateValue(value reflect.Value) error {
	if validator, ok := value.Interface().(Validator); ok {
		if err := validator.Validate(); err != nil {
			return err
		}
	} else {
		return errors.New("given value doesn't implement the Validator interface")
	}

	return nil
}

// S结构体由用户定义
type S struct {
	field int
}

// T未实现Validator
type T struct {
	field int
}

func (s *S) Validate() error {
	if s.field <= 0 {
		return errors.New("Field shoud be positive")
	}
	return nil
}

// 用户程序
func main() {
	t := T{field: 0}
	//T未实现validator,因此会得到错误。
	err := ValidateValue(reflect.ValueOf(t))
	fmt.Println(err)
	s := S{field: 0}
	//S也未实现validator,因此也会得到错误。
	err = ValidateValue(reflect.ValueOf(s))
	fmt.Println(err)
}

在Go语言中,将interface{}转换为特定接口类型时,需要注意值接收器和指针接收器的区别。在你的代码中,I类型实现了Incrementor接口,但Increment()方法使用的是指针接收器(i *I)

问题在于:

  • 变量iI类型(值类型)
  • 当赋值给interface{}类型的j时,存储的是值拷贝
  • 值类型无法满足指针接收器方法的要求

以下是两种解决方案:

方案1:使用指针类型

package main

type Incrementor interface {
	Increment()
}

type I int

func (i *I) Increment() {
	*i++
}

func main() {
	var i I

	// 使用指针赋值给interface{}
	var j interface{} = &i
	j.(Incrementor).Increment() // 正常工作
}

方案2:修改方法为值接收器

package main

type Incrementor interface {
	Increment()
}

type I int

func (i I) Increment() {
	// 注意:这里修改的是副本,不会影响原始值
	i++
}

func main() {
	var i I

	var j interface{} = i
	j.(Incrementor).Increment() // 正常工作,但不会修改原始i的值
}

方案3:安全的类型断言

package main

import "fmt"

type Incrementor interface {
	Increment()
}

type I int

func (i *I) Increment() {
	*i++
}

func main() {
	var i I

	var j interface{} = &i
	
	if inc, ok := j.(Incrementor); ok {
		inc.Increment()
		fmt.Println("Increment called successfully")
	} else {
		fmt.Println("Type assertion failed")
	}
}

推荐使用方案1,因为它保持了方法的原始语义(能够修改原始值),并且通过安全的类型断言来避免panic。

回到顶部