golang学习随便记16-反射

news2024/11/17 17:44:21

为什么需要反射

下面的例子中编写一个 Sprint 函数,只有1个参数(类型不定),返回和 fmt.Fprintf 类似的格式化后的字符串。实现方法大致为:如果参数类型本身实现了 String()  方法,那调用 String() 方法即可;否则用 switch 分支测试各种类型并进行相应处理。

func Sprint(x interface{}) string {
    type stringer interface {
        String() string
    }
    switch x := x.(type) {
    case stringer:
        return x.String()
    case string:
        return x
    case int:
        return strconv.Itoa(x)
    // ...similar cases for int16, uint32, and so on...
    case bool:
        if x {
            return "true"
        }
        return "false"
    default:
        // array, chan, func, map, pointer, slice, struct
        return "???"
    }
}

以上做法有点“简洁暴力”。问题是一旦遇上数组、Slice、Map、结构体那样的复合类型(用户构造类型),这种类型本质上是无穷尽的,用上面的做法是徒劳的。这就是反射存在的意义。

reflect.Type 和 reflect.Value

反射是 reflect 包提供的,里面最重要的是 reflect.Type 和 reflect.Value 这两个类型。Type 表示一个 Go类型Type 其实是接口(看 src/reflect/type.go,Type接口里有很多方法,如String()、Kind()、Comparable()、Elem()等),有许多方法来区分类型以及检查它们的组成部分,唯一能反映 reflect.Type 实现的是接口的类型描述信息,类型描述信息标识了接口值的动态类型(参见接口中接口值一节)。

函数 reflect.TypeOf 接受 interface{} 类型的参数(即任意类型的参数),并以 reflect.Type 形式返回。

t := reflect.TypeOf(3)  // a reflect.Type
fmt.Println(t.String()) // "int"
fmt.Println(t)          // "int"

在上面的代码中,值 3 传递给 interface{} 参数,其实会产生隐式的接口转换操作(其实就类似装箱操作,接口变量用指针指向底层数据3),它会创建一个包含两个信息的接口值:操作数的动态类型(这里是int)和它的动态的值(这里是3)。

reflect.TypeOf 返回的是一个动态类型的接口值,它总是返回具体的类型,即便变量被声明为接口类型(TypeOf方法根据接口变量中的具体类型信息创建Type类型的对象)。下面的代码中,打印输出 *os.File 而非 io.Writer。

var w io.Writer = os.Stdout
fmt.Println(reflect.TypeOf(w)) // "*os.File"

reflect.Type 接口是满足 fmt.Stringer 接口的(src/fmt/print.go,即实现了 String() 方法)。打印出接口的动态类型对于调试和日志是有帮助的,所以 fmt.Printf 提供了 %T 参数,在内部使用 reflect.TypeOf 来输出 (即 %T 是靠反射来识别类型的)

fmt.Printf("%T\n", 3) // "int"

另一个重要类型是Value。 reflect.Value 类型可以装载任意类型的值 (这一点和 空接口 interface{} 类似)。函数 reflect.ValueOf 接受任意的 interface{}类型,并返回一个装载了其动态值的 reflect.Value。reflect.ValueOf 返回的结果也是具体的类型(或者一个接口值)。

v := reflect.ValueOf(3) // a reflect.Value
fmt.Println(v)          // "3"
fmt.Printf("%v\n", v)   // "3"
fmt.Println(v.String()) // NOTE: "<int Value>"

上面的代码中,3 先会隐式接口转换(“装箱”),然后根据转换后接口变量中的值部分生成Value类型的对象 v。和 reflect.Type 那样,reflect.Value 也满足 fmt.Stringer 接口,但除非 Value类型的对象内部持有的是字符串,否则 String 方法返回类型信息而非所持有的值本身(这也是可以理解的,内部持有的是字符串,可以简单输出“字符串表达的值”,如果持有一个整数3或者浮点数3.14,就涉及复杂的转换才能得到文本"3"或者"3.14",而这种转换对反射是没有意义的)。和 %T 类似,%v 标志会对 reflect.Value 特殊处理,即 %T 取类型, %v 取值。

reflect.Value 可以通过 Type方法和 reflect.Type联系起来:调用 Value类型对象的 Type 方法将返回具体类型所对应的 reflect.Type 

t := v.Type()           // a reflect.Type
fmt.Println(t.String()) // "int"

reflect.ValueOf 是 interface {}  --->  Value 转换,其逆操作是 reflect.Value.Interface (Value ---> interface{} 转换):

v := reflect.ValueOf(3) // a reflect.Value
x := v.Interface()      // an interface{}
i := x.(int)            // an int
fmt.Printf("%d\n", i)   // "3"

reflect.Value 和 空接口 interface{} 都能装载任意的值,不同的是,空接口类型本身没有提供方法或字段来说明那个值的内部表示,只有明确知道了具体的动态类型,才能使用类型断言来访问内部的值。相比之下,Value提供了很多方法来检查其容纳的内容。简单理解,就是 reflect.Value 类型的变量是“自我清醒的(知道自己是谁,有什么能耐,几斤几两)”,空接口 interface{} 类型的变量是“自己糊涂的”

有了反射,就可以考虑处理文首的格式化打印任何类型的问题了。我们用 reflect.Value 的 Kind 方法代替 switch 分支判断,因为 Kind方法返回 Value对象底层数据的数据类别,数据类别(kinds类型)是有限的:如 Bool、String 和 Int8……Complex128等数字类型的基础类型;Array、Struct对应的复合类型;Chan、Func、Pointer(即Ptr)、Slice、Map对应的引用类型;Interface 接口类型;还有表示空值的 Invalid类型。(Value结构体类型有成员 flag,flag的最低5位表示所含值的 Kind类型,接下来5位是标志位,如果所含值表示的是方法,那么剩余22+位定义了方法编号)

package format

import (
	"reflect"
	"strconv"
)

func Any(value interface{}) string {
	return formatAtom(reflect.ValueOf(value))
}

func formatAtom(v reflect.Value) string {
	switch v.Kind() {
	case reflect.Invalid:
		return "invalid"
	case reflect.Int, reflect.Int8, reflect.Int16,
		reflect.Int32, reflect.Int64:
		return strconv.FormatInt(v.Int(), 10)
	case reflect.Uint, reflect.Uint8, reflect.Uint16,
		reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		return strconv.FormatUint(v.Uint(), 10)
	// ...floating-point and complex cases omitted for brevity...
	case reflect.Bool:
		return strconv.FormatBool(v.Bool())
	case reflect.String:
		return strconv.Quote(v.String())
	case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Slice, reflect.Map:
		return v.Type().String() + " 0x" +
			strconv.FormatUint(uint64(v.Pointer()), 16)
	default: // reflect.Array, reflect.Struct, reflect.Interface
		return v.Type().String() + " value"
	}
}

使用示例代码

package main

import (
	"fmt"
	"format"
	"time"
)

type Post struct {
	Title   string
	content string
}

func main() {
	var x int64 = 1
	var d time.Duration = 1 * time.Nanosecond
	post := Post{"标题1", "帖子1的内容"}
	fmt.Println(format.Any(x))                  // "1"
	fmt.Println(format.Any(d))                  // "1"
	fmt.Println(format.Any([]int64{x}))         // "[]int64 0x8202b87b0"
	fmt.Println(format.Any([]time.Duration{d})) // "[]time.Duration 0x8202b87e0"
	fmt.Println(format.Any(post))
}

显示的结果类似如下:

D:\GoPath\src\example>go run main.go
1                    是基础类型 int64
1                    time.Duration 本质上是基础类型 int64
[]int64 0xc00000a0e8           对slice,打印类型和16进制形式的引用地址
[]time.Duration 0xc00000a110            对slice,打印类型和16进制形式的引用地址

main.Post value               对复合类型(这里是结构体),只打印类型

Display 递归打印

前面的代码对复合类型的显示不太完善,我们希望构建一个适合于调试用的能详细显示复杂类型完整结构的 Display函数。

我们在 format包添加一个导出的 Display函数,它将调用不导出的 display函数,而 display将实现递归打印:

func Display(name string, x interface{}) {
	fmt.Printf("Display %s (%T):\n", name, x)
	display(name, reflect.ValueOf(x))
}

func display(path string, v reflect.Value) {
	switch v.Kind() {
	case reflect.Invalid:
		fmt.Printf("%s = invalid\n", path)
	case reflect.Slice, reflect.Array: // 切片或数组
		for i := 0; i < v.Len(); i++ {
			display(fmt.Sprintf("%s[%d]", path, i), v.Index(i))
		}
	case reflect.Struct: // 结构体
		for i := 0; i < v.NumField(); i++ {
			fieldPath := fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name)
			display(fieldPath, v.Field(i))
		}
	case reflect.Map: // Map
		for _, key := range v.MapKeys() {
			display(fmt.Sprintf("%s[%s]", path,
				formatAtom(key)), v.MapIndex(key))
		}
	case reflect.Ptr: // 指针
		if v.IsNil() {
			fmt.Printf("%s = nil\n", path)
		} else {
			display(fmt.Sprintf("(*%s)", path), v.Elem())
		}
	case reflect.Interface: // 接口
		if v.IsNil() {
			fmt.Printf("%s = nil\n", path)
		} else {
			fmt.Printf("%s.type = %s\n", path, v.Elem().Type())
			display(path+".value", v.Elem())
		}
	default: // basic types, channels, funcs
		fmt.Printf("%s = %s\n", path, formatAtom(v))
	}
}

对于切片和数组,打印每一个元素,此时 Value类型的 Len 和 Index 方法可以帮我们获取元素个数和每个元素对于结构体,打印它的成员Value类型的 NumField方法和Field方法可以获取成员个数和每个成员的值,而成员名称,可以 从Value类型生成对应Type对象,根据Type对象的Field方法以及StructField的Name字段值获得名称对于Map,则是靠 MapKeys 和 MapIndex 两个方法来遍历键值对对于指针,Value的Elem方法可以返回包装了指针指向的变量的reflect.Value类型,可以递归调用,对空指针调用Elem方法也是安全的,不过可以用Value的IsNil方法来更精细处理;对于接口,处理方式和指针类似。

package main

// ……

type Movie struct {
	Title, Subtitle string
	Year            int
	Color           bool
	Actor           map[string]string
	Oscars          []string
	Sequel          *string
}

func main() {
	// ……

	strangelove := Movie{
		Title:    "Dr. Strangelove",
		Subtitle: "How I Learned to Stop Worrying and Love the Bomb",
		Year:     1964,
		Color:    false,
		Actor: map[string]string{
			"Dr. Strangelove":            "Peter Sellers",
			"Grp. Capt. Lionel Mandrake": "Peter Sellers",
			"Pres. Merkin Muffley":       "Peter Sellers",
			"Gen. Buck Turgidson":        "George C. Scott",
			"Brig. Gen. Jack D. Ripper":  "Sterling Hayden",
			`Maj. T.J. "King" Kong`:      "Slim Pickens",
		},

		Oscars: []string{
			"Best Actor (Nomin.)",
			"Best Adapted Screenplay (Nomin.)",
			"Best Director (Nomin.)",
			"Best Picture (Nomin.)",
		},
	}

	format.Display("strangelove", strangelove)
}

输出结果如下:

Display strangelove (main.Movie):
strangelove.Title = "Dr. Strangelove"
strangelove.Subtitle = "How I Learned to Stop Worrying and Love the Bomb"
strangelove.Year = 1964
strangelove.Color = false
strangelove.Actor["Grp. Capt. Lionel Mandrake"] = "Peter Sellers"
strangelove.Actor["Pres. Merkin Muffley"] = "Peter Sellers"
strangelove.Actor["Gen. Buck Turgidson"] = "George C. Scott"
strangelove.Actor["Brig. Gen. Jack D. Ripper"] = "Sterling Hayden"
strangelove.Actor["Maj. T.J. \"King\" Kong"] = "Slim Pickens"
strangelove.Actor["Dr. Strangelove"] = "Peter Sellers"
strangelove.Oscars[0] = "Best Actor (Nomin.)"
strangelove.Oscars[1] = "Best Adapted Screenplay (Nomin.)"
strangelove.Oscars[2] = "Best Director (Nomin.)"
strangelove.Oscars[3] = "Best Picture (Nomin.)"
strangelove.Sequel = nil

这个 Display 函数不仅可以打印用户定义的类型,也可以打印标准库中的类型。反射可以访问到结构体中未导出的成员,即可能破坏访问控制(不过好在非导出的成员是只读的,见后)。

观察以下代码的输出:

	var i interface{} = 3
	format.Display("i", i)
	format.Display("&i", &i)

输出

Display i (int):
i = 3
Display &i (*interface {}):
(*&i).type = int
(*&i).value = 3

要理解以上输出,先来理解下图:

当我们调用 Display("i", i) 时,因为 x 的参数类型是 interface{},所以 3 会被打包成 <int, 3>,然后 函数内reflect.ValueOf 根据 <int, 3> 生成 reflect.Value对象,对象内部指针指向 3,因为是基本类型,display就调用了 formatAtom,最终用 strconv.FormatInt(v.Int(), 10) 得到结果 3 。

当我们调用 Display("&i", &i) 时,因为被打包的类型是指针<int*, 0x....>,display会按指针去打印,最终执行  fmt.Printf("%s.type = %s\n", path, v.Elem().Type())  和  display(path+".value", v.Elem()),后一句 v.Elem() 才对应数据 3 。

目前实现的 Display 在遇到对象图中有环的情形时会陷入死循环。要处理环,需要额外记录已经访问的路径,此处暂略。

编码为S表达式

S表达式是 Lisp语言语法的数据格式,Go标准库支持JSON、XML等数据格式,但不支持S表达式(因为没有公认的规范)。下面的例子支持以下结构:整数,如 42;字符串,如 "hello";标识符,如 foo;列表,如 (1  2  3);对布尔型 nil 表示false,t 表示 true

在 sexpr 包 的 sexpr.go 中,类似前面 Display函数定义 encode函数,它接受 reflect.Value 型参数,将编码结果写入 *bytes.Buffer型缓冲区,然后在文件中再仿照json等包定义一个导出的 Marshal 函数,其接受 interface{} 类型(任何类型)。

package sexpr

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

func Marshal(v interface{}) ([]byte, error) {
	var buf bytes.Buffer
	if err := encode(&buf, reflect.ValueOf(v)); err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}

func encode(buf *bytes.Buffer, v reflect.Value) error {
	switch v.Kind() {
	case reflect.Invalid:
		buf.WriteString("nil")

	case reflect.Int, reflect.Int8, reflect.Int16,
		reflect.Int32, reflect.Int64:
		fmt.Fprintf(buf, "%d", v.Int())

	case reflect.Uint, reflect.Uint8, reflect.Uint16,
		reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		fmt.Fprintf(buf, "%d", v.Uint())

	case reflect.String:
		fmt.Fprintf(buf, "%q", v.String())

	case reflect.Ptr:
		return encode(buf, v.Elem())

	case reflect.Array, reflect.Slice: // (value ...)
		buf.WriteByte('(')
		for i := 0; i < v.Len(); i++ {
			if i > 0 {
				buf.WriteByte(' ')
			}
			if err := encode(buf, v.Index(i)); err != nil {
				return err
			}
		}
		buf.WriteByte(')')

	case reflect.Struct: // ((name value) ...)
		buf.WriteByte('(')
		for i := 0; i < v.NumField(); i++ {
			if i > 0 {
				buf.WriteByte(' ')
			}
			fmt.Fprintf(buf, "(%s ", v.Type().Field(i).Name)
			if err := encode(buf, v.Field(i)); err != nil {
				return err
			}
			buf.WriteByte(')')
		}
		buf.WriteByte(')')

	case reflect.Map: // ((key value) ...)
		buf.WriteByte('(')
		for i, key := range v.MapKeys() {
			if i > 0 {
				buf.WriteByte(' ')
			}
			buf.WriteByte('(')
			if err := encode(buf, key); err != nil {
				return err
			}
			buf.WriteByte(' ')
			if err := encode(buf, v.MapIndex(key)); err != nil {
				return err
			}
			buf.WriteByte(')')
		}
		buf.WriteByte(')')
	case reflect.Bool:
		if v.Bool() {
			fmt.Fprintf(buf, "%s", "t")
		} else {
			fmt.Fprintf(buf, "%s", "nil")
		}
	default: // float, complex, chan, func, interface
		return fmt.Errorf("unsupported type: %s", v.Type())
	}
	return nil
}

调用 Marshal的例子

// ……
type Movie struct {
	Title, Subtitle string
	Year            int
	Color           bool
	Actor           map[string]string
	Oscars          []string
	Sequel          *string
}

func main() {
	// ……

	strangelove := Movie{
		Title:    "Dr. Strangelove",
		Subtitle: "How I Learned to Stop Worrying and Love the Bomb",
		Year:     1964,
		Color:    false,
		Actor: map[string]string{
			"Dr. Strangelove":            "Peter Sellers",
			"Grp. Capt. Lionel Mandrake": "Peter Sellers",
			"Pres. Merkin Muffley":       "Peter Sellers",
			"Gen. Buck Turgidson":        "George C. Scott",
			"Brig. Gen. Jack D. Ripper":  "Sterling Hayden",
			`Maj. T.J. "King" Kong`:      "Slim Pickens",
		},

		Oscars: []string{
			"Best Actor (Nomin.)",
			"Best Adapted Screenplay (Nomin.)",
			"Best Director (Nomin.)",
			"Best Picture (Nomin.)",
		},
	}

	// ……

	var buf []byte
	var err error
	if buf, err = sexpr.Marshal(strangelove); err != nil {
		fmt.Printf("%v", err)
	}
	fmt.Println(string(buf))
}

输出结果如下:

((Title "Dr. Strangelove") (Subtitle "How I Learned to Stop Worrying and Love the Bomb") (Year 1964) (Color nil) (Actor (("Maj. T.J. \"King\" Kong" "Slim Pickens") ("Dr. Strangelove" "Peter Sellers") ("Grp. Capt. Lionel Mandrake" "Peter Sellers") ("Pres. Merkin Muffley" "Peter Sellers") ("Gen. Buck Turgidson" "George C. Scott") ("Brig. Gen. Jack D. Ripper" "Sterling Hayden"))) (Oscars ("Best Actor (Nomin.)" "Best Adapted Screenplay (Nomin.)" "Best Director (Nomin.)" "Best Picture (Nomin.)")) (Sequel nil))

上面的输出结果都在一行上,要美化需要仔细调整代码(不同层打印不同的缩进),这里略过。

通过 reflect.Value 修改值

先来看下面的代码及其输出

	x := 2                   // value   type    variable?
	a := reflect.ValueOf(2)  // 2       int     no
	b := reflect.ValueOf(x)  // 2       int     no
	c := reflect.ValueOf(&x) // &x      *int    no
	d := c.Elem()            // 2       int     yes (x)
	fmt.Println(a.CanAddr()) // "false"
	fmt.Println(b.CanAddr()) // "false"
	fmt.Println(c.CanAddr()) // "false"
	fmt.Println(d.CanAddr()) // "true"
	fmt.Printf("&x = %p, &d = %p, x=%d,d=%d\n", &x, &d, x, d)

输出

false
false
false
true
&x = 0xc00000a130, &d = 0xc000008048, x=2,d=2

本质上, reflect.ValueOf(x) 返回的 reflect.Value 都是不可取地址的(它只是x的拷贝副本,不能调用 Value 的 Addr()方法)。上面代码中的d,它是c的解引用方式生成的,指向另一个变量,因此d可以取地址(可以 d.Addr())。(Elem方法只能用于接口或者指针,返回的Value对象对应接口包含的元素或者指针指向的元素;c是&x的拷贝,c.Elem()返回的Value对象,内部指针指向了x,从而d它可以取地址)。注意,CanAddr()方法对应的可不可以取地址,是这个value有没有对应指向数据的指针的意思,不是value本身能不能取地址。例如对 c 来说,可以 &c,但不能 c.Addr();对于 d 来说,可以 &d,也可以 d.Addr(),只是后者 d.Addr() 返回的Value对象,它包含的地址值是 &x。我们可以通过调用 reflect.ValueOf(&x).Elem() 来获取任意变量x对应的可取地址的 Value

每当我们通过指针间接获取的 reflect.Value 都是可取地址的,即使开始的是一个不可取地址的Value。在反射机制中,所有关于是否支持取地址的规则都是类似的。例如, slice 的索引表达式 e[i] 将隐式地包含一个指针,它就是可取地址的,即使开始的 e 表达式不支持也没有关系。同样, reflect.ValueOf(e).Index(i) 对应的值也是可取地址的,即使原始的 reflect.ValueOf(e) 不支持也没有关系。理解这一点,可以通过 Value 本身的定义:

type Value struct {
    typ *rtype
    ptr unsafe.Pointer
    flag
}

Value 类型就是 “一个指向数据的指针+其他一些字段” 的形式,而从数组索引切片得到的 slice 类型就是指向 data的指针 + len + cap。这里的意思就是在反射机制中,所有类似的类型,都可以 Addr() 方法取得数据对应的地址(想想也是,总要让我们可以挖出数据来,而取得地址是必需的)。

要从变量对应的可取地址的 reflect.Value 来访问变量需要三步:1、调用 Addr() 方法返回的是 Value,里面保存了指向变量的指针 (就像前面的 Value类型对象d,它调用Addr()对应的值是 &x); 2、 在 Value 上调用 Interface() 方法返回一个 interface{},里面包含指向变量的指针;可以参考前面那个图中从 Value 到 interface{} 的逆转换; 3、如果我们知道变量的类型,可以用类型断言将得到的 interface{}类型的接口强制转为普通的类型指针,最终我们可以通过这个普通指针来更新变量了。这一步相当于前面的图中从 interface{} 到类型T。例子如下

	x := 2
	d := reflect.ValueOf(&x).Elem()
	px := d.Addr().Interface().(*int)
	*px = 3
	fmt.Println(x) // now x is 3

不用类型断言,也可以通过使用 Value.Set()方法来实现(用一个 Value 来设置另一个 Value)

	d.Set(reflect.ValueOf(5))
	fmt.Println(x) // now x is 5

Value.Set() 方法会在运行时执行可赋值性约束检查。如果用一个类型 A 的Value 去设置另一个类型B的 Value,将 panic。

d.Set(reflect.ValueOf(int64(5))) // panic: int64 不能赋值给 int,int8 也不行

对一个不可取地址的 reflect.Value 调用 Set方法也会 panic(也就是前面说的“可取地址的Value”这一步是必需的)。

x := 2
b := reflect.ValueOf(x)
b.Set(reflect.ValueOf(3)) // panic

使用 d.Set(reflect.ValueOf(5)) 这样的写法显得很累赘,而此类操作又常见,所以,对于基本数据类型有很多 SetXxx 方法: SetInt、SetUint、SetString、SetFloat…… 这样,d.SetInt(5) 看上去清爽多了。 然而,对于引用 interface{} 类型的 reflect.Value,两种写法并不等价,调用 SetInt会panic,即使“看上去没有问题”:

x := 1
rx := reflect.ValueOf(&x).Elem()
rx.SetInt(2)                     // OK, x = 2
rx.Set(reflect.ValueOf(3))       // OK, x = 3
rx.SetString("hello")            // panic: string is not assignable to int
rx.Set(reflect.ValueOf("hello")) // panic: string is not assignable to int

var y interface{}
ry := reflect.ValueOf(&y).Elem()
ry.SetInt(2)                     // panic: SetInt called on interface Value
ry.Set(reflect.ValueOf(3))       // OK, y = int(3)
ry.SetString("hello")            // panic: SetString called on interface Value
ry.Set(reflect.ValueOf("hello")) // OK, y = "hello"

通过反射机制,我们可以突破Go语言关于导出的规则读取到结构体中未导出的成员,但访问是只读的,因为一个可取地址的 reflect.Value 会记录一个结构体成员是否是未导出成员,如果是的话就拒绝修改操作。Value 的 CanAddr() 方法只反映是否可以取到对应的值,CanSet() 才是用于检查对应的 reflect.Value是否可取地址并可被修改

示例:解码S表达式 

标准库中包 text/scanner 可以将字节输入流解析为一个个类似注释、标识符、字符串字面值和数字字面值之类的标记(token)。输入扫描器 scanner 中的 Scan() 方法将提前扫描和返回下一个记号。对于大多数token,比如 "(",对应 token值是单一 rune 可表示的 Unicode值 40 (=0x28);但 text/scanner 也用小的负数来标记标识符、字符串、整数等由多个字符组成的 token,例如 EOF=-1,Ident=-2,Int=-3,Float=-4,Char=-5,String=-6……。调用 scanner.Scan() 将返回这些 token 的类型,接着调用 TokenText() 方法将返回 token 对应的文本形式内容。

下面示例中用类型 lexer 包装了 scanner.Scanner 类型(scan字段),字段 token 跟踪最近返回的 token。方法 next()、text() 是对 Scanner类型 Scan()、和 TokenText()方法的简单包装,方法 consume(want) 是检查当前 token 是否满足预期,不满足就 panic,满足就继续读入下一个 token。

package sexpr

import (
	"bytes"
	"fmt"
	"reflect"
	"strconv"
	"text/scanner"
)

type lexer struct {
	scan  scanner.Scanner
	token rune
}

func (lex *lexer) next() {
	lex.token = lex.scan.Scan()
}

func (lex *lexer) text() string {
	return lex.scan.TokenText()
}

func (lex *lexer) consume(want rune) {
	if lex.token != want {
		panic(fmt.Sprintf("got %q, want %q", lex.text(), want))
	}
	lex.next()
}

func read(lex *lexer, v reflect.Value) {
	switch lex.token {
	case scanner.Ident:
		if lex.text() == "nil" {
			v.Set(reflect.Zero(v.Type()))
			lex.next()
			return
		}
	case scanner.String:
		s, _ := strconv.Unquote(lex.text())
		v.SetString(s)
		lex.next()
		return
	case scanner.Int:
		i, _ := strconv.Atoi(lex.text())
		v.SetInt(int64(i))
		lex.next()
		return
	case '(':
		lex.next()
		readList(lex, v)
		lex.next()
		return
	}
	panic(fmt.Sprintf("unexpected token %q", lex.text()))
}

func readList(lex *lexer, v reflect.Value) {
	switch v.Kind() {
	case reflect.Array:
		for i := 0; !endList(lex); i++ {
			read(lex, v.Index(i))
		}
	case reflect.Slice:
		for !endList(lex) {
			item := reflect.New(v.Type().Elem()).Elem()
			read(lex, item)
			v.Set(reflect.Append(v, item))
		}
	case reflect.Struct:
		for !endList(lex) {
			lex.consume('(')
			if lex.token != scanner.Ident {
				panic(fmt.Sprintf("got token %q, want field name", lex.text()))
			}
			name := lex.text()
			lex.next()
			read(lex, v.FieldByName(name))
			lex.consume(')')
		}
	case reflect.Map:
		v.Set(reflect.MakeMap(v.Type()))
		for !endList(lex) {
			lex.consume('(')
			key := reflect.New(v.Type().Key()).Elem()
			read(lex, key)
			value := reflect.New(v.Type().Elem()).Elem()
			read(lex, value)
			v.SetMapIndex(key, value)
			lex.consume(')')
		}
	default:
		panic(fmt.Sprintf("cannot decode list into %v", v.Type()))
	}
}

func endList(lex *lexer) bool {
	switch lex.token {
	case scanner.EOF:
		panic("end of file")
	case ')':
		return true
	}
	return false
}

func Unmarshal(data []byte, out interface{}) (err error) {
	lex := &lexer{scan: scanner.Scanner{Mode: scanner.GoTokens}}
	lex.scan.Init(bytes.NewReader(data))
	lex.next()
	defer func() {
		if x := recover(); x != nil {
			err = fmt.Errorf("error at %s: %v", lex.scan.Position, x)
		}
	}()
	read(lex, reflect.ValueOf(out).Elem())
	return nil
}

func Marshal(v interface{}) ([]byte, error) {
// ......... see above .....
}

func encode(buf *bytes.Buffer, v reflect.Value) error {
// .........  see above ....
}

上面的代码中, read函数读取 lex 中当前 token,根据不同token类型,更新 reflect.Value型变量v对应的值(注意,v必须是可取地址的,这里更新v对应的值相当于读入token对应内容),处理完继续读下一个token。如果当前读到的 token 是左括号 "(",这意味着一个列表的起始,调用 readList函数读入列表。因为列表内每个元素又涉及读取 token,所以, read和readList是构成递归调用的。

readList 根据不同列表类型(数组、切片、结构体、Map)进行处理,这部分代码还是比较复杂的,涉及反射包中很多函数,这里先略过。

使用 sexpr包的代码例子如下:

package main

import (
	"fmt"

	"gpl.12.6/sexpr"
)

type Movie struct {
	Title, Subtitle string
	Year            int
	Color           bool
	Actor           map[string]string
	Oscars          []string
	Sequel          *string
}

func main() {
	// ...     see above  ...

	var outMovie Movie
	if err = sexpr.Unmarshal(buf, &outMovie); err != nil {
		fmt.Printf("%v", err)
		return
	}
	fmt.Println()
	fmt.Printf("%v", outMovie)
}

输出类似如下

((Title "Dr. Strangelove") (Subtitle "How I Learned to Stop Worrying and Love the Bomb") (Year 1964) (Color nil) (Actor (("Dr. Strangelove" "Peter Sellers") ("Grp. Capt. Lionel Mandrake" "Peter Sellers") ("Pres. Merkin Muffley" "Peter Sellers") ("Gen. Buck Turgidson" "George C. Scott") ("Brig. Gen. Jack D. Ripper" "Sterling Hayden") ("Maj. T.J. \"King\" Kong" "Slim Pickens"))) (Oscars ("Best Actor (Nomin.)" "Best Adapted Screenplay (Nomin.)" "Best Director (Nomin.)" "Best Picture (Nomin.)")) (Sequel nil))

{Dr. Strangelove How I Learned to Stop Worrying and Love the Bomb 1964 false map[Brig. Gen. Jack D. Ripper:Sterling Hayden Dr. Strangelove:Peter Sellers Gen. Buck Turgidson:George C. Scott Grp. Capt. Lionel Mandrake:Peter Sellers Maj. T.J. "King" Kong:Slim Pickens Pres. Merkin Muffley:Peter Sellers] [Best Actor (Nomin.) Best Adapted Screenplay (Nomin.) Best Director (Nomin.) Best Picture (Nomin.)] <nil>}

实际生产代码中,不会简单panic来处理错误。即便像上面那样简化处理解码,反射的使用依然是golang中比较复杂的部分。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1509570.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

web | http 的一些问题 | get/post的区别 | http版本 | http与https的区别 | session、cookie、token

怎么来说呢&#xff1f;这应该算一个大类了&#xff0c;基本上设计网络的应用层 当然重要的是从网络层----->应用层 &#xff08;杠精勿杠&#xff0c;知道中间还有其他层&#xff09; 先来讲一下http的结构 都知道http 有三部分&#xff0c;头部、请求头和body 头部&#x…

51单片机基础篇系列-点亮一个LED发光管基础知识搭建

&#x1f308;个人主页: 会编辑的果子君 &#x1f4ab;个人格言:“成为自己未来的主人~” LED发光二极管 它是半导体二极管的一种&#xff0c;可以把电能转化成光能&#xff0c;常简写为LED&#xff0c;发光二极管与普通二极管一样是由一个PN结组成&#xff0c;也具有单向…

Jenkins Pipeline实现Golang项目的CI/CD

Jenkins Pipeline实现Golang项目的CI/CD 背景 最近新增了一个Golang实现的项目&#xff0c;需要接入到现有的流水线架构中。 流程图 这边流程和之前我写过的一篇《基于Jenkins实现的CI/CD方案》差不多&#xff0c;不一样的是构建现在是手动触发的&#xff0c;没有配置webho…

dolphin schedulerAPI调用(二)——创建任务

&#xff08;作者&#xff1a;陈玓玏&#xff09; API文档地址&#xff1a;http://192.168.3.100:21583/dolphinscheduler/swagger-ui/index.html?languagezh_CN&langcn#/task%20definition%20related%20operation/createTaskDefinitionUsingPOST_1 实际使用中&#x…

微信小程序H5设置全局弹窗

微信小程序&H5设置全局弹窗 微信小程序&H5设置全局弹窗效果图1、下载所需库2、创建vue.config.js 文件3、创建全局公告组件头部公告组件弹窗公告组件4、组件注册到全局5、在pages.json文件中配置 insetLoader6、H5需要额外使用render.js7、全局调用(一进入页面就获取弹…

Elasticsearch:使用标记修剪提高文本扩展性能

作者&#xff1a;来自 Elastic Kathleen DeRusso 本博客讨论了 ELSER 性能的令人兴奋的新增强功能&#xff0c;该增强功能即将在 Elasticsearch 的下一版本中推出&#xff01; 标记&#xff08;token&#xff09;修剪背后的策略 我们已经详细讨论了 Elasticsearch 中的词汇和…

《系统架构设计师教程(第2版)》第6章-数据库设计基础知识-02-关系数据库

文章目录 1. 基本概念1.1 基本术语属性 (Attribute)域 (Domain)元数&#xff08;Arity&#xff09; / 目 &#xff08;Cardinality&#xff09;/ 度 (Degree)元组候选码 (Candidate Key)主码 (Primary Key)主属性 (Prime Attribute)外码 (Foreign Key)全码 (All-key)笛卡尔积 1…

大数据队列Kafka

了解什么是kafka之前&#xff0c;首先要了解一下什么是消息队列 一丶kafka的基本概述 消息队列&#xff1a;MQ介绍 定义 官方定义&#xff1a;消息队列是一种异步的服务间通信方式,是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题,实现高性能,高可用,可伸…

WPF 中集合 ObservableCollection<T>的使用

C#集合类ObservableCollection<T> 类似于泛型列表类List<T>&#xff0c;表示一个动态数据收集&#xff0c;该集合在添加或删除项或刷新整个列表时提供通知。 所在命名空间&#xff1a;System.Collections.ObjectModel 继承关系&#xff1a; public class Observ…

SQL 多表查询

文章目录 多表查询的分类等值连接非等值连接自连接非自连接内连接外连接左外连接右外连接满外连接 SQL连接 JOINSQL99 语法新特性 自然连接 NATURAL JOIN & USING 多表查询的分类 等值连接 VS 非等值连接自连接 VS 非自连接内连接 VS 外连接 等值连接 关联的表有连接字段…

2.4_4 死锁的检测和解除

文章目录 2.4_4 死锁的检测和解除&#xff08;一&#xff09;死锁的检测&#xff08;二&#xff09;死锁的解除 总结 2.4_4 死锁的检测和解除 如果系统中既不采取预防死锁的措施&#xff0c;也不采取避免死锁的措施&#xff0c;系统就很可能发生死锁。在这种情况下&#xff0c;…

CrossOver24软件免费电脑虚拟机,快速在Mac和Linux上运行Windows软件

当然&#xff0c;除了之前提到的核心技术、兼容性和性能优化外&#xff0c;CrossOver2024还具有其他一些值得关注的性能特点&#xff1a; CrossOver Mac-安装包下载如下&#xff1a;https://wm.makeding.com/iclk/?zoneid50028 CrossOver linux-安装包下载如下&#xff1a;ht…

信息系统项目管理师--质量管理

国际标准化组织(ISo) 对质量 (Quality) 的定义是:“反映实体满⾜主体明确和隐含需求的能⼒的特性总和”。 国家标准对质量的定义为:“⼀组固有特性满⾜要求的程度”。固有特性是指在某事或某物中本来就有的, 尤其是那种永久的可区分的特征。对产品来说, 例如⽔泥的化学成分、强…

设计模式九:装饰器模式

文章目录 1、装饰器模式2、示例3、装饰器模式与适配器模式4、装饰器模式和代理模式5、java io流的装饰器模式 1、装饰器模式 装饰器模式&#xff08;Decorator Pattern&#xff09;允许向一个现有的对象添加新的功能&#xff0c;同时又不改变其结构。这种类型的设计模式属于结构…

大数据开发-Hive介绍以及安装配置

文章目录 数据库和数据仓库的区别Hive安装配置Hive使用方式Hive日志配置 数据库和数据仓库的区别 数据库&#xff1a;传统的关系型数据库主要应用在基本的事务处理&#xff0c;比如交易&#xff0c;支持增删改查数据仓库&#xff1a;主要做一些复杂的分析操作&#xff0c;侧重…

WPF(2)命令绑定

效果是&#xff1a;当TextBox控件的Text属性为空时show按钮不可用&#xff0c;有值时show按钮可用 项目结构 界面代码 <Window x:Class"WpfApp1.MainWindow"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://sc…

普发Pfeiffer Prisma QMS200四极质谱计内部电路图装配3D图电路板接口详细注解

普发Pfeiffer Prisma QMS200四极质谱计内部电路图装配3D图电路板接口详细注解

面向对象(精髓)变继承关系为组和关系(_Decorator模式)

在软件开发中&#xff0c;设计模式是解决常见问题的可重用解决方案。在面向对象编程中&#xff0c;继承和组合是两种常用的代码复用方式。然而&#xff0c;随着软件需求的不断变化&#xff0c;我们需要更灵活的设计方式来应对不断变化的需求。在本文中&#xff0c;我们将讨论从…

全面的 DevSecOps 指南:有效保护 CI/CD 管道的关键注意事项

数字化转型时代带来了对更快、更高效、更安全的软件开发流程的需求。DevSecOps&#xff1a;一种将安全实践集成到 DevOps 流程中的理念&#xff0c;旨在将安全性嵌入到开发生命周期的每个阶段 - 从代码编写到生产中的应用程序部署。DevSecOps 的结合可以带来许多好处&#xff0…

程序人生 - 爬虫者,教育也!

作为一个站长&#xff0c;你是不是对爬虫不胜其烦&#xff1f;爬虫天天来爬&#xff0c;速度又快&#xff0c;频率又高&#xff0c;服务器的大量资源被白白浪费。 看这篇文章的你有福了&#xff0c;我们今天一起来报复一下爬虫&#xff0c;直接把爬虫的服务器给干死机。 本文有…