前景提要

上一篇文章当中,主要以实现了一个display函数为主要内容,阐述了如何通过golang的reflect包将一个自定义类型的数据格式化输出,它类似于一个简洁版的fmt.Printf函数。但是这个函数只是简单的实现了从具体类型的数据到字符串的输出功能。在日常的工作中,我们时常要通过http/https请求调用一些第三方服务的接口,传输的数据不但需要保持一定的编码格式,还需要按照被调用服务接口接收数据的规范来构造我们的请求数据。

在使用golang的过程当中,一定少不了对encoding/json包当中的json.Marshal方法的使用,他接收一个interface值, 返回一个以ASN.1格式编码byte数组。 ASN.1是一套正式且公用的编码标准,使用json.Marshal对我们的数据进行编码之后,得到的结果就可以在进程间以及服务间进行传输。编码这件事,说白了,就是把我们准备好的数据通过一定的规则转换成另外一种表现形式,比如把十进制数字转换成二进制数据,我觉得这就是一种编码。再不理解的话,也可以想想莫斯密电码,应该很容易就明白了。

今天这篇文章,将会对上次我们实现的display函数进行再一次的扩展,使其能够具备数据编码的功能.

Encoding

golang的标准库内为我们提供了几种常见并且通用的编码格式,如,xml, json, asn.1等。熟悉lisp或者scheme的人肯定知道s-expression这种格式。仿照《The Go programing language》一书中的例子,我自己也实现了一个将普通数据编码成s-expression形式的函数。(由于注重的是反射的使用,所以在代码风格上面没有做过多的注意,各位看官勿喷)

在我实现的Encoding函数中,我主要处理了String, int, array, slice, struct, map, ptr等类型的数据编码,其余的如interface等类型,为了简单起见,我并没有对其做处理。编码的规范如下:

  1. 数字的类型如int还是直接编码成对应的字符串表示。字符串类型也是一样
  2. struct类型编码成((fieldName fieldValue) (fieldName fieldValue) …)的形式
  3. map类型编码成 ((keyName value), (keyName value) …)的形式
  4. 指针类型不做特殊处理,而是获取到其指向的变量之后,对这个变量编码。
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
package main

import (
	"bytes"
	"fmt"
	"reflect"
)

func Encode(value reflect.Value, buff *bytes.Buffer) (err error) {
	switch value.Kind() {
	case reflect.Invalid:
		_, err = buff.Write([]byte("nil"))
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		fmt.Fprintf(buff, "%d", value.Int())
	case reflect.String:
		fmt.Fprintf(buff, "%q", value.String())
	case reflect.Ptr:
		err = Encode(value.Elem(), buff)
	case reflect.Array, reflect.Slice:
		_, err = buff.WriteString("(")
		if err != nil {
			return
		}

		for i := 0; i < value.Len(); i++ {
			err = Encode(value.Index(i), buff)
			if err != nil {
				return
			}

			if i != value.Len()-1 {
				_, err = buff.WriteString(" ")
				if err != nil {
					return
				}
			}
		}

		_, err = buff.WriteString(")")
	case reflect.Struct:
		_, err = buff.WriteString("(")
		if err != nil {
			return
		}

		for j := 0; j < value.NumField(); j++ {
			if j > 0 {
				_, err = buff.WriteString(" ")
				if err != nil {
					return
				}
			}

			_, err = buff.WriteString("(")
			if err != nil {
				return
			}

			_, err = buff.WriteString(value.Type().Field(j).Name)
			if err != nil {
				return
			}

			_, err = buff.WriteString(" ")
			if err != nil {
				return

			}

			err = Encode(value.Field(j), buff)
			if err != nil {
				return
			}

			_, err = buff.WriteString(")")
			if err != nil {
				return
			}
		}

		_, err = buff.WriteString(")")
		if err != nil {
			return
		}

	case reflect.Map:
		_, err = buff.WriteString("(")
		if err != nil {
			return
		}

		for j, k := range value.MapKeys() {
			if j > 0 {
				_, err = buff.WriteString(" ")
				if err != nil {
					return
				}
			}

			_, err = buff.WriteString("(")
			if err != nil {
				return
			}

			err = Encode(k, buff)
			if err != nil {
				return
			}

			_, err = buff.WriteString(" ")
			if err != nil {
				return

			}

			err = Encode(value.MapIndex(k), buff)
			if err != nil {
				return
			}

			_, err = buff.WriteString(")")
			if err != nil {
				return
			}
		}

		_, err = buff.WriteString(")")
		if err != nil {
			return
		}

	default:
		fmt.Fprintf(buff, "%s:%s", "unsupport type", value.Kind())
	}

	return
}

type B struct {
	Fuck string `json:"fuck"`
	You  int    `json:"you"`
}

type A struct {
	Domain   string            `json:"domain"`
	Cache    []string          `json:"cache"`
	CdnState map[string]string `json:"cdnState"`
	Time     int               `json:"time"`
	B        `json:,inline`
}

func main() {
	b := B{
		Fuck: "fff",
		You:  3,
	}

	obj := A{
		Domain: "hahah.com",
		Cache:  []string{"1", "2", "3"},
		CdnState: map[string]string{
			"ws": "success",
			"tc": "success",
		},
		Time: 2,
		B:    b,
	}

	buff := bytes.Buffer{}
	err := Encode(reflect.ValueOf(obj), &buff)
	if err != nil {
		fmt.Printf("Error: %+v", err)
		return
	}

	fmt.Printf("%+v\n", string(buff.Bytes()))
	return
}

上述代码的输出结果为:

1
2
3
👉  go run encode.go
((Domain "hahah.com") (Cache ("1" "2" "3")) (CdnState (("tc" "success") ("ws" "success"))) (Time 2) (B ((Fuck "fff") (You 3))))
	

可以看出,Encoding函数已经简单的将普通数据类型的数据编码成了s-expression的形式。其实编码的格式并不一定要局限于一些目前公认标准的格式,我们也完全可以定义一套自己的规则。只要这套规则在编码和解码的过程中都能够保证数据的完整性和正确性即可。

Set value

之前的几篇关于反射的文章,甚至是这一篇文章,到此为止,在代码上面的实现以及对反射的讲解都停留在观察的层面上,也就是我们仅仅是把一个普通数据类型的值通过反射更进一步的获取他们信息,如类型,值等等。然后可以通过一些手段格式化输出他们。实际上,之前几篇文章的重点,并不在于使用reflect包实现一些具体的功能,而是在向大家突出,利用reflect几乎可以处理所有现在的以及未来的数据类型。当你把普通的数据类型通过reflect.ValueOf转换成reflect.Value类型值的时候,你可以以他为起点去做任何事情。之前实现的格式化输出,以及上面实现的编码功能。那么接下来我们就来了解一下,如何通过reflet包来更改一些未知的数据类型的值。

从变量说起

大家都知道,编程中最常见的两个概念就是变量和常量。顾名思义,变量是可以改变的,常量是恒久不变的。变量为什么可以改变呢?因为变量本身就是一个被名字标识的内存空间,它是可寻址的。只要一个东西是可寻址的,那么我们就可以通过地址在内存中找到他的位置,然后修改它内存空间中所保存的内容。这其实也就是我们常说的给变量赋值的过程。

对于reflect.Value值来说,它和interface一样,本身可以接受任何类型的数据。那么任意reflect.Value的值都可以像变量一样随意的更改么?我们写一个demo试试就知道了

1
2
3
4
5
6
7
8
9
	a := 2
	x := reflect.ValueOf(a)
	fmt.Println(x.CanSet())  //false

	y := reflect.ValueOf(&a)
	fmt.Println(y.CanSet())  //false

	z := y.Elem()
	fmt.Println(z.CanSet()  //true

通过调用reflect.Value.CanSet方法,根据其返回的结果是true还是false就可以确定一个reflect.Value值是不是可以改变的。 x在调用的时候返回了false,这是因为在构造x的时候,使用的是a这个变量内存储的值2,但是它自己并没有申请内存来存这个值. y也同样是这样,虽然它是通过a变量的地址值来构造的。事实上,任何通过reflect.ValueOf构造出来的reflect.Value都是不可寻址的,也就是说,我们只是copy了一份值来显示,并不能去改动他。

但是当我们对y调用Elem方法得到z,z却是一个可以改变的值。这是为什么呢?首先y是通过一个指针的值来构造的,其内存存储的是a这个变量的内存地址。调用Elem()方法,得到的是这个地址指向的内存空间,也就是a这个变量的地址空间。赋值给z也并没有切断y所指向的内存空间与a的联系,仅仅是给这块空间起了一个别名。

reflect包中还有一个方法叫做CanAddr。它的定义是这样的:

1
CanAddr reports whether the value's address can be obtained with Addr. Such values are called addressable. A value is addressable if it is an element of a slice, an element of an addressable array, a field of an addressable struct, or the result of dereferencing a pointer. If CanAddr returns false, calling Addr will panic

它的功能是可以判断出一个reflect.Value是否是可寻址的。在它的描述当中也指出了,slice或者一个可寻址数组中的一个元素,或者一个可寻址的结构体的一个属性,甚至是对一个指针解引用的结果都是属于可寻址的。通过这几个例子我们可以看出,一个可寻址的reflect.Value都是需要通过一个指针指向某块内存的地址来间接获取到的。比如说,s := string[]{“1”, “2”}, reflect.ValueOf(s)是不可寻址的,它只是通过这个slice底层引用数组的起始地址构造了一个reflect.Value值。但是这时我通过调用reflect.ValueOf(s).Index(0),就可以得到一个可寻址的值。因为通通过reflect.ValueOf得到了slice引用的底层数组的起始地址,然后通过index方法计算了内存的偏移量,就得到了在这段连续内存当中第一个元素的内存空间,即一个可寻址的值。所以说,在reflect当中,想要拿到一个可寻址值,首先要得到这个可寻址值所在的内存空间的地址。

更新变量

当我们想要更新一个reflect.Value所引用的变量的值时,可以通过Set方法来做到。

1
2
3
	x := []int{1,2}
	z := reflect.ValueOf(x).Index(0) //get an addressable reflectValue
	z.Set(reflect.ValueOf(3)) // update value

需要注意的是,在调用Set方法进行赋值操作的时候,reflect包会像普通数据类型的赋值操作一样,检查赋值的合法性。比如z标识的可寻址值是一个int类型的,如果此时在调用set方法的时候,赋值给其一个int64类型的值就会导致panic。所以在使用set之前,我们必须保证我们将要给某个变量赋的值是它能够接受的。但是Set方法有一些变体,如SetInt, SetUint等.只要调用这些函数的值是有符号整数类型甚至是其底层的数据类型符合以上的条件,setInt等方法无论接收多大的数值都会通过截断的方式,构造出一个调用这个函数的值能接受的结果并赋值给它。

在第二篇文章中我们实现的格式化输出任意数据类型值的程序中,如果你仔细调试就会发现,如果输出的是一个struct类型的变量,那么其内部不可导出的field也可以被找到并且输出。我们可以通过Field或者FieldByName方法来获取struct类型值当中的某一个field,如果获取的是一个不可导出的field,那么对这个结果调用set类的修改方法会导致panic。这是一个特例,struct结构体内的不可导出field是可寻址的,但是是不可修改的。

本文代码

点这里