Go 接口与反射

71 阅读14分钟

接口是什么?

Go语言中的接口

在Go语言中,接口是一种抽象类型,它定义了一组方法签名,但不包含方法的具体实现。接口在Go程序设计中扮演着至关重要的角色,为实现多态性、代码解耦以及模块化开发提供了强大的支持。

接口的定义

接口的定义使用 type 关键字,后面跟着接口名和 interface 关键字,然后在大括号内列出接口所包含的方法签名。例如:

type Shape interface {
    Area() float64
    Perimeter() float64
}

在上述示例中,定义了一个名为 Shape 的接口,它要求实现该接口的类型必须提供 AreaPerimeter 两个方法,这两个方法都返回一个 float64 类型的值。

接口的实现

任何类型,只要它实现了接口中定义的所有方法,就被认为是实现了该接口。这意味着结构体、自定义类型等都可以实现接口。例如,假设有一个 Rectangle 结构体:

type Rectangle struct {
    width  float64
    height float64
}

func (r Rectangle) Area() float64 {
    return r.width * r.height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.width + r.height)
}

在这里,Rectangle 结构体通过实现 Shape 接口中定义的 AreaPerimeter 方法,从而实现了 Shape 接口。

接口的使用

接口的使用使得代码能够以一种更加抽象和灵活的方式处理不同类型的对象,只要这些对象实现了相应的接口。例如:

func PrintShapeInfo(s Shape) {
    fmt.Printf("Area: %.2f\n", s.Area())
    fmt.Printf("Perimeter: %.2f\n", s.Perimeter())
}

func main() {
    r := Rectangle{width: 5, height: 3}
    PrintShapeInfo(r)
}

在上述代码中,PrintShapeInfo 函数接受一个 Shape 接口类型的参数 s。这样,无论传入的是哪种具体实现了 Shape 接口的类型(如 Rectangle),函数都能够正确地调用其 AreaPerimeter 方法并输出相关信息。

接口的多态性

接口实现了多态性,即同一种操作可以作用于不同类型的对象,而这些对象根据自身的具体实现来执行相应的操作。这使得代码更加通用和可复用。例如,我们可以再定义一个 Circle 结构体并实现 Shape 接口:

type Circle struct {
    radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.radius * c.radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.radius
}

现在,PrintShapeInfo 函数不仅可以处理 Rectangle 类型的对象,也可以处理 Circle 类型的对象,无需对函数进行任何修改:

func main() {
    r := Rectangle{width: 5, height: 3}
    c := Circle{radius: 2}

    PrintShapeInfo(r)
    PrintShapeInfo(c)
}

接口的零值

接口类型的零值是 nil,就像其他类型一样。当一个接口变量没有被赋值任何具体实现了该接口的类型的值时,它的值就是 nil。在使用接口变量时,需要注意对 nil 值的处理,避免出现空指针异常等问题。

空接口

Go语言中有一个特殊的接口叫做空接口,它不定义任何方法,用 interface{} 表示。空接口可以表示任何类型的值,因为任何类型都天然地实现了空接口。例如:

var anyValue interface{} = 10
fmt.Println(anyValue)

anyValue = "Hello"
fmt.Println(anyValue)

anyValue = Rectangle{width: 4, height: 6}
fmt.Println(anyValue)

在上述代码中,anyValue 这个空接口变量可以先后被赋值为整数、字符串和结构体等不同类型的值。

总之,Go语言的接口是一种强大的抽象机制,它能够帮助开发者编写更加灵活、可复用和易于维护的代码,通过实现多态性和代码解耦,使得程序在处理不同类型的对象时更加高效和便捷。

反射是什么?

反射是这样一种机制,它是可以让我们在程序运行时(runtime)访问、检测和修改对象本身状态或行为的一种能力。 比如,从一个变量推断出其类型信息、以及存储的数据的一些信息,又或者获取一个对象有什么方法可以调用等。 反射经常用在一些需要同时处理不同类型变量的地方,比如序列化、反序列化、ORM 等等,如标准库里面的 json.Marshal

反射三大定律

在 go 官方博客中关于反射的文章 laws-of-reflection 中,提到了三条反射定律:

  1. 反射可以将 interface 类型变量转换成反射对象。
  2. 反射可以将反射对象还原成 interface 对象。
  3. 如果要修改反射对象,那么反射对象必须是可设置的(CanSet)。
func main() {
	var a interface{} = 1
	var b interface{} = 1.0
	fmt.Println("isSame?", a == b)
	fmt.Println("reflect.TypeOf(a):", reflect.TypeOf(a))
	fmt.Println("reflect.TypeOf(b):", reflect.TypeOf(b))
}

输出结果

isSame? false
reflect.TypeOf(a): int
reflect.TypeOf(b): float64

1. 反射可以将 interface 类型变量转换成反射对象

reflect.Typereflect.Value,我们可以通过 reflect.TypeOfreflect.ValueOf 来获取到一个变量的反射类型和反射值。

func main() {
	var a interface{} = 1
	var b interface{} = "hello"
	fmt.Println("isSame?", a == b)
	fmt.Println("reflect.TypeOf(a):", reflect.TypeOf(a))
	fmt.Println("reflect.TypeOf(b):", reflect.TypeOf(b))
	fmt.Println("reflect.ValueOf(a):", reflect.ValueOf(a))
	fmt.Println("reflect.ValueOf(b):", reflect.ValueOf(b))
}

输出结果

isSame? false
reflect.TypeOf(a): int
reflect.TypeOf(b): string
reflect.ValueOf(a): 1
reflect.ValueOf(b): hello

2. 反射可以将反射对象还原成 interface 对象

func main() {
	var a interface{} = 1
	var b interface{} = "hello"
	fmt.Println("isSame?", a == b)
	fmt.Println("reflect.TypeOf(a):", reflect.TypeOf(a))
	fmt.Println("reflect.TypeOf(b):", reflect.TypeOf(b))
	fmt.Println("reflect.ValueOf(a):", reflect.ValueOf(a))
	fmt.Println("reflect.ValueOf(b):", reflect.ValueOf(b))

	// 2.反射对象还原成接口
	bB := reflect.ValueOf(b)
	bInterface := bB.Interface()
	fmt.Println("reflect.TypeOf(bInterface):", reflect.TypeOf(bInterface))
}

输出结果

isSame? false
reflect.TypeOf(a): int
reflect.TypeOf(b): string
reflect.ValueOf(a): 1
reflect.ValueOf(b): hello
reflect.TypeOf(bInterface): string

我们可以通过 reflect.Value.Interface 来获取到反射对象的 interface 对象,也就是传递给 reflect.ValueOf 的那个变量本身。

从输出结果来看,我们看到了返回最初的类型也就是string。

在 Go 语言的反射机制中,当你通过 reflect.ValueOf() 获取一个值的反射值对象(如这里的 bB := reflect.ValueOf(b)),然后再通过这个反射值对象的 Interface() 方法将其转换回接口类型(bInterface := bB.Interface())时,它会还原成原始的值所对应的具体类型。

这是因为 reflect.Value 类型在内部维护了关于原始值的足够信息,包括其类型信息等。当你调用 Interface() 方法时,它会根据这些内部维护的信息准确地还原出原始值的类型并返回对应的接口值。

3. 如果要修改反射对象,那么反射对象必须是可设置的(CanSet

func main() {
	var a interface{} = 1
	var b interface{} = "hello"
	fmt.Println("isSame?", a == b)
	fmt.Println("reflect.TypeOf(a):", reflect.TypeOf(a))
	fmt.Println("reflect.TypeOf(b):", reflect.TypeOf(b))
	fmt.Println("reflect.ValueOf(a):", reflect.ValueOf(a))
	fmt.Println("reflect.ValueOf(b):", reflect.ValueOf(b))

	// 2.反射对象还原成接口
	bB := reflect.ValueOf(b)
	fmt.Println("bB.CanSet():", bB.CanSet())
	bInterface := bB.Interface()
	fmt.Println("reflect.TypeOf(bInterface):", reflect.TypeOf(bInterface))
}

输出结果

isSame? false
reflect.TypeOf(a): int
reflect.TypeOf(b): string
reflect.ValueOf(a): 1
reflect.ValueOf(b): hello
bB.CanSet(): false
reflect.TypeOf(bInterface): string

可以看到这里是false表明不可修改,其原因在于string类型底层维护的是*byte的指针,无法进行修改string也是出于安全的考虑,防止在高并发场景下出现数据竞争问题。

a. 我们以int类型为例

	aA := reflect.ValueOf(a)
	fmt.Println("aA.CanSet():", aA.CanSet())

输出结果

aA.CanSet(): false

依旧不行,这是为什么呢?

那什么情况下一个反射对象是可设置的呢?前提是这个反射对象是一个指针,然后这个指针指向的是一个可设置的变量。 在我们传递一个值给 reflect.ValueOf 的时候,如果这个值只是一个普通的变量,那么 reflect.ValueOf 会返回一个不可设置的反射对象。

这里我们传入给 reflect.ValueOf(a),是一个值,而不是一个指针,值传递的话一般都是拷贝的方式,这里我们传入一个引用。

	aA := reflect.ValueOf(&a)
	fmt.Println("aA.CanSet():", aA.Elem().CanSet())

输出结果

aA.CanSet(): true

可以看到现在就可以修改了,这里因为我传入了地址,我必须使用Elem()来获取指针进行操作。

b. Elem 方法

reflect.ValueElem 方法的作用是获取指针指向的值,或者获取接口的动态值。也就是说,能调用 Elem 方法的反射对象,必须是一个指针或者一个接口。 在使用其他类型的 reflect.Value 来调用 Elem 方法的时候,会 panic:

reflect.TypeElem 方法的作用是获取数组、chan、map、指针、切片关联元素的类型信息,也就是说,对于 reflect.Type 来说, 能调用 Elem 方法的反射对象,必须是数组、chan、map、指针、切片中的一种,其他类型的 reflect.Type 调用 Elem 方法会 panic

我们把a修改成100

	aA := reflect.ValueOf(&a)
	fmt.Println("aA.CanSet():", aA.Elem().CanSet())
	aA.Elem().SetInt(100)
	fmt.Println("Value of a:", a)

输出结果

aA.CanSet(): true
panic: reflect: call of reflect.Value.SetInt on interface Value

goroutine 1 [running]:
reflect.Value.SetInt({0xec4b00?, 0xc000107de0?, 0xc000107e10?}, 0x64)
        E:/开发环境/go版本/1.22/src/reflect/value.go:2401 +0xda

可以看到这里引发了panic,那不是已经可以Set了么,为什么还不能修改?

我们先继续修改。,刚刚我们的a定义的方式是var a interface{} = 1

func main() {
	var a int = 1
	aA := reflect.ValueOf(&a)
	fmt.Println("aA.CanSet():", aA.Elem().CanSet())
	fmt.Println("Value of a:", a)
	valueOfA := reflect.ValueOf(&a)

	// 确认获取到的是指针类型的反射值对象
	if valueOfA.Kind() == reflect.Ptr {
		// 通过Elem()获取指针指向的实际值的反射值对象
		valueOfA = valueOfA.Elem()
	}

	// 再次确认是否可设置
	if valueOfA.CanSet() {
		// 修改a的值
		valueOfA.SetInt(100)
		fmt.Println("Modified value of a:", a)
	} else {
		fmt.Println("Value of a cannot be set via reflection.")
	}
}

输出结果

aA.CanSet(): true
Value of a: 1
Modified value of a: 100

咦,现在又可以了?我们来具体分析一下。

var a interface{} = 1
var a int = 1

这是因为在 Go 语言的反射机制中,对于不同类型的变量,其可通过反射进行修改的规则和限制是有所不同的。

var a interface{} = 1

  • 当你将一个具体类型的值(这里是整数 1)赋给一个接口变量 a 时,接口变量内部的实现机制会存储这个值的类型信息以及一个指向该值的某种内部表示的指针(具体细节对用户是透明的)。
  • 当你通过反射来尝试修改这个接口变量所指向的值时,即使你获取到了看似可设置(CanSet)的反射值对象,实际上它在 Go 语言的运行时层面是受到严格限制而不允许直接修改的。这是因为接口的设计初衷是提供一种多态的、灵活的类型处理方式,允许不同类型的值赋给同一个接口变量,但同时为了保证类型安全和内部实现的一致性,不希望用户通过反射轻易地修改接口变量内部所指向的值。这种限制可以防止一些潜在的、不符合类型安全原则的修改操作,避免破坏程序的稳定性和可预测性。

var a int = 1

  • 对于一个普通的基本类型变量 a(这里是 int 类型),其在内存中的存储和操作方式相对简单直接。当你通过反射来修改它的值时,只要满足一定的条件(如变量是可寻址的,并且获取到的反射值对象经过正确处理后确实是可设置且类型匹配的),就可以进行修改操作。
  • 在前面给你提供的正确修改示例中,通过 reflect.ValueOf(&a).Elem() 先获取到变量 a 的地址的反射值对象,再通过 Elem() 获取到指向实际值的反射值对象,然后经过对 CanSet 和类型(Kind())的双重检查,确保满足可修改条件后,就可以使用 SetInt 操作来修改变量 a 的值。这种方式是符合 Go 语言对于基本类型变量通过反射进行修改的规则的,因为基本类型变量本身的存储和操作逻辑相对明确,不像接口变量那样涉及到复杂的多态和类型转换等情况,所以在满足可寻址和可设置等条件下,可以相对顺利地通过反射进行修改。

c. Kind方法

在 Go 语言的反射(reflect)包中,.Kind()reflect.Value 类型的一个方法,用于获取反射值的底层基础类型。以下是关于它的详细说明:

  1. 目的和用途
    • 当通过反射来处理各种类型的值时,有时候需要知道这个值的底层具体类型是什么,例如它是一个基本类型(如 intfloat64string 等),还是一个复合类型(如结构体、切片、映射等)。.Kind() 方法就是用于获取这种底层类型信息的。
    • 它返回一个 reflect.Kind 类型的值,reflect.Kind 是一个枚举类型,定义了 Go 语言中所有可能的基础类型,包括基本类型和复合类型。
  1. reflect.Kind ****枚举类型的值
    • 基本类型相关的 ****Kind ****
      • reflect.Bool:表示布尔类型(truefalse)。
      • reflect.Intreflect.Int8reflect.Int16reflect.Int32reflect.Int64:分别表示不同长度的有符号整数类型。
      • reflect.Uintreflect.Uint8reflect.Uint16reflect.Uint32reflect.Uint64:表示不同长度的无符号整数类型。
      • reflect.Float32reflect.Float64:表示单精度和双精度浮点数类型。
      • reflect.Complex64reflect.Complex125:表示复数类型。
      • reflect.String:表示字符串类型。
    • 复合类型相关的 ****Kind ****
      • reflect.Array:表示数组类型。
      • reflect.Slice:表示切片类型,切片是对数组的一种引用和扩展,在 Go 语言中广泛使用。
      • reflect.Struct:表示结构体类型,结构体用于组合多个不同类型的字段,是构建复杂数据结构的重要方式。
      • reflect.Map:表示映射类型,用于存储键值对。
      • reflect.Ptr:表示指针类型,用于存储内存地址,通过指针可以间接访问和修改所指向的值。
      • reflect.Interface:表示接口类型,接口定义了一组方法签名,用于实现多态性。
      • reflect.Func:表示函数类型。
      • reflect.Chan:表示通道类型,用于在不同的协程(goroutine)之间进行通信。
func main() {
	var i int = 10
	valueOfI := reflect.ValueOf(i)
	fmt.Println("The kind of i is:", valueOfI.Kind()) // 输出:The kind of i is: int

	var s []string = []string{"hello", "world"}
	valueOfS := reflect.ValueOf(s)
	fmt.Println("The kind of s is:", valueOfS.Kind()) // 输出:The kind of s is: slice

	var m map[string]int = map[string]int{"key": 1}
	valueOfM := reflect.ValueOf(m)
	fmt.Println("The kind of m is:", valueOfM.Kind()) // 输出:The kind of m is: map
}

输出结果

The kind of i is: int
The kind of s is: slice
The kind of m is: map

瞧,这不和TypeOf()一样么?

ⅰ. TypeOf()和Kind()的区别
  1. reflect.TypeOf reflect.Value.Kind 的区别
package main

import (
    "fmt"
    "reflect"
)

type MyStruct struct {
    Field1 int
}

func main() {
    s := MyStruct{Field1: 1}
    t := reflect.TypeOf(s)
    fmt.Println("Type name:", t.Name())
    fmt.Println("Number of fields:", t.NumField())
    // 输出类型名字和字段数量
}
package main

import (
    "fmt"
    "reflect"
)

func setValue(v reflect.Value, newVal interface{}) {
    switch v.Kind() {
    case reflect.Int:
        v.SetInt(reflect.ValueOf(newVal).(reflect.Value).Int())
    case reflect.String:
        v.SetString(reflect.ValueOf(newVal).(reflect.Value).String())
    // 可以处理更多类型
    default:
        fmt.Println("Unsupported type")
    }
}

func main() {
    var i int
    valueOfI := reflect.ValueOf(&i).Elem()
    setValue(valueOfI, 10)
    fmt.Println(i)
}
    • reflect.TypeOf
      • 关注点不同reflect.TypeOf 主要返回接口中保存的具体类型。如果传入的是一个接口值,它会返回这个接口实际指向的类型。例如,如果有一个接口变量 var i interface{} = 10reflect.TypeOf(i) 会返回 int。对于非接口类型,它也返回该类型本身。
      • 包含的信息更丰富:除了基础类型信息,reflect.TypeOf 还包含了关于类型的其他详细信息,比如类型的名称、方法集、字段(对于结构体等复合类型)等。它返回一个 reflect.Type 类型的值,通过这个值可以进一步查询类型相关的各种属性。例如,可以通过 reflect.TypeNumField 方法获取结构体类型的字段数量,通过 Field 方法获取具体的字段信息等。
      • 使用场景示例:在需要获取一个类型的完整定义信息,如用于生成文档、进行类型比较(比较两个类型是否完全相同)或者需要根据类型来动态创建实例(结合 reflect.New 等方法)等场景下非常有用。
    • reflect.Value.Kind
      • 关注点在于基础类型分类reflect.Value.Kind 更侧重于返回值的基础类型分类,它返回的是一个枚举值(reflect.Kind),表示这个值是基本类型(如 intstring 等)还是复合类型(如 structslice 等)中的哪一种。它并不关心具体的类型名称或者其他高级的类型属性。例如,对于 intint32reflect.Value.Kind 都会返回 reflect.Int,因为它们都属于整数这个基础类型分类。
      • 用于简单的类型分类判断:主要用于在反射操作中快速判断一个值属于哪种基础类型分类,以便进行一些通用的、基于类型分类的操作。比如,在编写一个通用的反射赋值函数时,可能需要先判断值的 Kindreflect.Int 还是 reflect.String 等,然后再决定使用哪种具体的赋值方法(如 SetIntSetString)。
      • 使用场景示例
package main

import (
    "fmt"
    "reflect"
)

func setValue(v reflect.Value, newVal interface{}) {
    switch v.Kind() {
    case reflect.Int:
        v.SetInt(reflect.ValueOf(newVal).(reflect.Value).Int())
    case reflect.String:
        v.SetString(reflect.ValueOf(newVal).(reflect.Value).String())
    // 可以处理更多类型
    default:
        fmt.Println("Unsupported type")
    }
}

func main() {
    var i int
    valueOfI := reflect.ValueOf(&i).Elem()
    setValue(valueOfI, 10)
    fmt.Println(i)
}