Go 译文之如何使用反射 part2

栏目: Go · 发布时间: 5年前

内容简介:这篇博文介绍的内容比较实在,主要是关于两方面的内容。一是介绍 reflection 在 encoding/json 中的应用,另一个开发了一个 Cacher 工厂函数,实现函数式编程中的记忆功能,其实就是根据输入对输出进行一定限期的缓存。这篇文章的翻译没有上一篇那么轻松,因为涉及了一些函数式编程的术语,之前也并没有接触过。为了翻译这篇文章,简单阅读了网上的一篇关于函数式编程的文章,译文如下:

这篇博文介绍的内容比较实在,主要是关于两方面的内容。一是介绍 reflection 在 encoding/json 中的应用,另一个开发了一个 Cacher 工厂函数,实现函数式编程中的记忆功能,其实就是根据输入对输出进行一定限期的缓存。

这篇文章的翻译没有上一篇那么轻松,因为涉及了一些函数式编程的术语,之前也并没有接触过。为了翻译这篇文章,简单阅读了网上的一篇关于函数式编程的文章, 文章地址 。望没有知识性错误。

译文如下:

上一篇 文章 ,(阅读 英文原版 ),我们介绍了 Go 的反射包 reflection。并通过一些示例介绍了它的特性。但是,我们还不清楚它究竟有什么。

通过反射实现的功能,不用反射也能实现,而且更加高效简洁。但是 Go 团队肯定不会因为自己而为 Go 增加一个新的特性。

那究竟什么情况下会使用反射呢?

寻找反射使用案例

通过反射,我们可以实现各种奇淫巧技。但每天的工作中,我该如何使用它呢?

其实,大部分时间里,我们都用不到它。反射主要是用在一些特殊的场景下,使一些不可能的实现成为可能。我们常会在一些库、 工具 、框架中找到反射的使用场景。

那你是否可以告诉我,哪些库、框架或工具中使用反射呢?一个技巧,查看函数参数类型。如果一个函数的参数类型是 interface{},那么,它极有可能使用了反射来检查或改变参数的值。

JSON 处理

反射,最常见的使用场景之一,是对网络或文件中的数据进行解包和组包。当你通过 struct tag 映射 JSON 或数据库中的数据时,便是通过反射实现的。这类场景,我们通常会用某个库帮助我们创建结构体实例,它通过分析 struct tag 和数据,以此为 struct 的字段赋值。

我们就以 Go 官方标准库中的 JSON 解包为例,来介绍一下它的实现。

通过调用 json.Unmarshal 函数,我们可以把 JSON 字符串解包并赋值给某个变量。Unmarshal 函数接收两个参数:

  • 类型为 []byte 的 JSON 字符串;
  • 类型为 interface{},用于存放 JSON 解析结果的变量;

深入看看这个函数究竟是如何进行反射的?

阅读 json 包的源码,其中有个私有函数 unmarshal,主要看其中与反射相关的部分代码如下:

func (d *decodeState) unmarshal(v interface{}) (err error) {
    <skip some setup>
    rv := reflect.ValueOf(v)
    if rv.Kind() != reflect.Ptr || rv.IsNil() {
        return &InvalidUnmarshalError{reflect.TypeOf(v)}
    }
    d.scan.reset()
    // We decode rv not rv.Elem because the Unmarshaler interface
    // test must be applied at the top level of the value.
    // 传的是 rv,而不是 rv.Elem,因为结果传递给最顶层的 value
    d.value(rv)
    return d.savedError
}
复制代码

上面的代码中,首先会通过反射验证变量类型,是否是指针类型,如果是,将变量 v 的 reflect.Value 传给 value 方法。

在 value 方法中,首先检查 JSON 字符串表示的类型,array、object、还是字面量。不同的类型将由不同方法处理。举例来说,如果解析 JSON Object。

将会有很多地方用到反射。

比如,使用反射检查 v 是否是 nil interface。

// Decoding into nil interface? Switch to non-reflect code.
if v.Kind() == reflect.Interface && v.NumMethod() == 0 {
	v.Set(reflect.ValueOf(d.objectInterface()))
	return
}
复制代码

如果是把 JSON object 赋值给 map。

switch v.Kind() {
	case reflect.Map:
	// Map key must either have string kind, have an integer kind,
	// or be an encoding.TextUnmarshaler.
	t := v.Type()
	switch t.Key().Kind() {
		case reflect.String,
			reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
			reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
 		default:
 			if !reflect.PtrTo(t.Key()).Implements(textUnmarshalerType) {
 				d.saveError(&UnmarshalTypeError{Value: "object", Type: v.Type(), Offset: int64(d.off)})
 				d.off --
 				d.next() // skip over { } in input
 				return
 			}
 	}
 	if v.IsNil() {
 		v.Set(reflect.MakeMap(t))
 	}
 	case reflect.Struct:
 		// ok
 	default:
 		d.saveError(&UnmarshalTypeError{Value: "object", Type: v.Type(), Offset: int64(d.off)})
		d.off --
 		d.next() // skip over { } in input
 		return
}
复制代码

如果是把 JSON object 赋值给 struct。

subv = v
destring = f.quoted
for _, i := range f.index {
	if subv.Kind() == reflect.Ptr {
		if subv.IsNil() {
			subv.Set(reflect.New(subv.Type().Elem()))
		}
		subv = subv.Elem()
	}
	subv = subv.Field(i)
}
复制代码

以上只是一个简单的演示,主要是关于,JSON 包中反射是如何使用的。如果希望自己阅读代码,源码在 encoding/json/decode.go

记忆与短期记忆

Json Unmarshal 是反射的其中一个使用案例。还有其他场景吗?接下来,我们将用反射开发一个库,基于 "记忆法" 实现短期缓存。

或许你并不熟悉这个术语,"记忆" 出自于函数式编程。函数式编程中倾向于强制推行某些规则,比如,参数和变量通常是不可修改的,创建之后便不可变。函数式编程尝试限制编程的 "副作用"。

一个实际的程序基本是不可能的没有 "副作用" 的,因为总会涉及诸如信息打印、写文件、向数据库插入数据等事情。但其他的一些 "副作用",比如更新全局变量,将会导致程序很难追踪。函数式编程的一个目标,让程序中的数据追踪变得简单,我们可以非常容易地就明白程序在做什么?

函数式编程还有一些其他好处。比如,当一个函数输入和返回不变且没有 "副作用" 时,每次调用函数,相同的输入,做同样的工作,返回相同的结果。如果我们保存了这些结果,重复的工作就没有必要做第二次了。

到此,我们开始引入 "记忆" 的概念。它类似于函数级别的缓存。"记忆" 是通过在恒定的函数上包裹函数,实现输入输出的缓存,从而避免重复不必要的工作。但一个函数被 "记忆",对于一个输入,只有执行一次工作。相同的输入执行第二次函数调用时,返回值将从缓存中获取,而不是重新计算一次。对于那些复杂或非常慢的操作,如此对性能的提升将是非常大的。

Go 可能不算是函数式编程语言,但我们依然可以通过它践行自己的想法。这种编程风格稍微有点严格,但是它避免了输入输出的更新,最小化了程序的 "副作用",使你的程序非常易读。

我们将实现一小短时间的缓存,而不是永远。在微服务架构中,这是一种更加通用的模式。比如如下的场景:

一个服务提供数据,另一个服务获取数据。因为数据是通过网络传递的,会占用一定时间。这将会降低系统的整体性能。如果数据不是经常改变的且数据延迟几秒更新也没有关系,那么可以暂时缓存这个数据,它将给你的系统带来一个显著的性能提升。通过函数式编程的 "记忆",我们可以在也没有对太多 API 的修改实现缓存,避免系统额外的网络调用。

如何实现?我们将通过反射实现三件事:

  • 确保输入类型是一个函数,并且至少包含一个输入和一个输出;
  • 确保新创建结构体中的字段类型和输入参数的类型一一对应;
  • 确保新创建函数的输入和输出参数和原始函数相匹配;

还有一个限制,所有的输入参数必须可比较,在 Go 中,可比较的类型及可以通过 == 符号进行布尔运算。我们将用 Go 中的映射 map 关联输入和输出,Go 中 map 的 key 必须是可比较的,这很有意义,因为要确认输入参数是否出现过,我们需要检查前后的相等性。

幸运的是,Go 中只有四种情况是不可比较的,如下:

  • 切片,Slices;
  • 映射,Maps;
  • 函数,Functions;
  • 结构体,成员包含 Slice、Map 或 Function;

让我们开始定义 Cacher 函数,类似如下的形式:

// Takes in a function and a time.Duration and returns a caching version of the function
// 接收两个参数,分别是函数和时间,返回的是一个缓存版本的函数
//
// The limitations on memoization are:
// 限制如下:
//
// — there must be at least one in parameter
// - 必须有一个输入参数
//
// — there must be at least one out parameter
// - 必须有一个输出参数
//
// — the in parameters must be comparable. That means they cannot be of kind slice, map, or func,
// nor can input parameters be structs that contain (at any level) slices, maps, or funcs.
// - 输入参数必须是可比较的,
//
// Be aware that if your memoized function has any side-effects (does anything that isn’t
// reflected in the output, like print to the screen or write to a database) the side-effects
// will be performed by the function only the first time that the function is invoked with
// particular set of values.
// 要明白了解,如果被记忆的函数有任何的副作用(指任何不能在返回结果中反应出来的东西,比如打印、写数据库),这些 "副作用"
// 将只会执行一次。
func Cacher(f interface{}, expiration time.Duration) (interface{}, error) {
	return f, nil
}
复制代码

代码并没有做什么,但通过它,我们明白了接下来的目标。下面,我们正式开始填写代码,首先通过反射检查,我们传递的确实是一个函数。

func Cacher(f interface{}, expiration time.Duration) (interface{}, error) {
	ft := reflect.TypeOf(f)
	if ft.Kind() != reflect.Func {
		return nil, errors.New("Only for functions")
	}
	return f, nil
}
复制代码

接下来,创建一个结构体用来存放我们的输入参数。在创建结构体时,我们需要保证必须有一个输入和一个输出,并且所有的输入都是可比较的。

unc buildInStruct(ft reflect.Type) (reflect.Type, error) {
	if ft.NumIn() == 0 {
		return nil, errors.New("Must have at least one param")
	}
	var sf []reflect.StructField
	for i := 0; i < ft.NumIn(); i++ {
		ct := ft.In(i)
		if !ct.Comparable() {
			return nil, fmt.Errorf("parameter %d of type %s and kind %v is not comparable", i+1, ct.Name(), ct.Kind())
		}
		sf = append(sf, reflect.StructField{
			Name: fmt.Sprintf("F%d", i),
			Type: ct,
		})
	}
	s := reflect.StructOf(sf)
	return s, nil
}

func Cacher(f interface{}, expiration time.Duration) (interface{}, error) {
	ft := reflect.TypeOf(f)
	if ft.Kind() != reflect.Func {
		return nil, errors.New("Only for functions")
	}

	inType, err := buildInStruct(ft)
	if err != nil {
		return nil, err
	}

	if ft.NumOut() == 0 {
		return nil, errors.New("Must have at least one returned value")
	}

	fmt.Println("inType looks like", inType)
	return f, nil
}
复制代码

接下来,还剩最后一步,定义一个 map 变量,用它存放输入输出的缓存,并且用 reflection 反射在 f 函数基础上生成具有缓存能力的新函数。

type outExp struct {
	out    []reflect.Value
	expiry time.Time
}

func Cacher(f interface{}, expiration time.Duration) (interface{}, error) {
	ft := reflect.TypeOf(f)
	if ft.Kind() != reflect.Func {
		return nil, errors.New("Only for functions")
	}

	inType, err := buildInStruct(ft)
	if err != nil {
		return nil, err
	}

	if ft.NumOut() == 0 {
		return nil, errors.New("Must have at least one returned value")
	}

	m := map[interface{}]outExp{}
	fv := reflect.ValueOf(f)
	cacher := reflect.MakeFunc(ft, func(args []reflect.Value) []reflect.Value {
		iv := reflect.New(inType).Elem()
		for k, v := range args {
			iv.Field(k).Set(v)
		}
		ivv := iv.Interface()
		ov, ok := m[ivv]
		now := time.Now()
		if !ok || ov.expiry.Before(now) {
			ov.out = fv.Call(args)
			ov.expiry = now.Add(expiration)
			m[ivv] = ov
		}
		return ov.out
	})
	return cacher.Interface(), nil
}
复制代码

完成!

再来看一下上面的代码,首先,我们定义了一个结构体 outExp,用它存放输出和过期时间。

接着,我们定义了一个 map,它的 key 是interface{},值是 outExp 类型。它们的选择都是有原因的。先说 key 是 interface{} 类型的原因。之前的例子,我们通过反射创建了一个结构体,这种结构体没有名称,为了存储实例,我们不得不使用 interface{} 类型表示。关于返回类型,当你用反射调用函数,它的返回类型是 []reflect.Value。同样,传递给 MakeFunc 的闭包函数返回的也是这种类型的值。 为了避免值拷贝,我们通过 []reflect.Value 保存返回值,并把它存入 map。

在闭包中,我们通过反射构造了一个自定义类型的实例,将传给函数的参数放入其中。接着,检查 m 中是否存在实例等于它,如果没有,或已经过期,我们将调用包裹函数,然后将响应结果和过期时间保存进变量 ov 中。接着,以自定义结构体的实例为 key,将 ov 保存进 m 中。最后,返回 ov.out 即可。

到此,我们正式完成了 Cacher 工厂函数,它可以包裹 Go 中几乎所有的函数,实现一定期限的缓存。

我们如何使用呢?一个例子,如下:

func AddSlowly(a, b int) int {
	time.Sleep(100 * time.Millisecond)
	return a + b
}

func main() {
	ch, err := Cacher(AddSlowly, 2*time.Second)
	if err != nil {
		panic(err)
	}
	chAddSlowly := ch.(func(int, int) int)
	for i := 0; i < 5; i++ {
		start := time.Now()
		result := chAddSlowly(1, 2)
		end := time.Now()
		fmt.Println("got result", result, "in", end.Sub(start))
	}
	time.Sleep(3 * time.Second)
	start := time.Now()
	result := chAddSlowly(1, 2)
	end := time.Now()
	fmt.Println("got result", result, "in", end.Sub(start))
}
复制代码

例子中,仅仅是 sleep 100ms,然后,求两个数的和。实际的情况可能是,数据库的查询或网络服务的调用。由于 Go 没有泛型,Cacher 返回的函数需要转化为合适的类型,错误检查也要求有几行代码,interface{} 的 ch 也需要转化为实际的函数类型。

执行代码,将会得到如下的输出:

$ go run cacher.go
got result 3 in 100.079405ms
got result 3 in 3.873µs
got result 3 in 561ns
got result 3 in 462ns
got result 3 in 398ns
got result 3 in 100.054602ms
复制代码

第一次执行占用了大概 100 ms(计算处理时间),接下里的几次调用都只用了几百纳秒。在暂停了 3 秒后,执行最后一次,再次耗时 100 ms。

运行示例

新的秘密武器

再说最后一点,反射对性能有一定的影响。如果执行的是密集性运算,或是调用网络服务,通过反射在上面加一层代码,这对性能将不会有太大的影响。但是,多数代码都是非常快的。极有可能,你代码中的大部分方法的执行时间都是几百毫秒以内,此类场景,就要小心反射的使用了,此时,反射会性能有较大影响,我们需要重新思考是否值得。

总而言之,当我们再遇到各种类型的问题时,可以回想下反射提供的可能。当遇到看起不可能解决的问题时,比如虽然两个类型的处理逻辑相似,但是类型的自身的共性不多,这时候,反射将成为你的秘密武器。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Convergence Culture

Convergence Culture

Henry Jenkins / NYU Press / 2006-08-01 / USD 30.00

"Convergence Culture" maps a new territory: where old and new media intersect, where grassroots and corporate media collide, where the power of the media producer, and the power of the consumer intera......一起来看看 《Convergence Culture》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

MD5 加密
MD5 加密

MD5 加密工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具