Golang 挑战:编写函数 walk(x interface{}, fn func(string)),参数为结构体 x,并对 x 中的所有字符串字段调用 fn 函数。难度级别:递归。
golang 挑战:编写函数
walk(x interface{}, fn func(string))
,参数为结构体x
,并对x
中的所有字符串字段调用fn
函数。难度级别:递归。
为此,我们需要使用 反射。
计算中的反射提供了程序检查自身结构体的能力,特别是通过类型,这是元编程的一种形式。这也是造成困惑的一个重要原因。
什么是 interface
?
string
,int
以及我们自己定义的类型,如 BankAccount
,我们享受到了 Go 为我们提供的类型安全。interface{}
来解决这个问题,你可以将其视为 任意 类型。walk(x interface{}, fn func(string))
的 x 参数可以接收任何的值。
那么为什么不通过将所有参数都定义为 interface
类型来得到真正灵活的函数呢?
- 作为函数的使用者,使用
interface
将失去对类型安全的检查。如果你想传入string
类型的Foo.bar
但是传入的是int
类型的Foo.baz
,编译器将无法通知你这个错误。你也搞不清楚函数允许传递什么类型的参数。知道一个函数接收什么类型,例如UserService
,是非常有用的。
- 作为这样一个函数的作者,你必须检查传入的 所有 参数,并尝试断定参数类型以及如何处理它们。这是通过 反射 实现的。这种方式可能相当笨拙且难以阅读,而且一般性能比较差(因为程序必须在运行时执行检查)。
interface
类型,这里容易让人困惑)设计它,以便用户可以用多种类型来调用你的函数,这些类型实现了函数工作所需要的任何方法。首先编写测试
我们想用一个 struct
来调用我们的函数,这个 struct
中有一个字符串字段(x
),然后我们可以监视传入的函数(fn
),看看它是否被调用。
- func TestWalk(t *testing.T) {
- expected := "Chris"
- var got []string
- x := struct {
- Name string
- }{expected}
- walk(x, func(input string) {
- got = append(got, input)
- })
- if len(got) != 1 {
- t.Errorf("wrong number of function calls, got %d want %d", len(got), 1)
- }
- }
- 我们想存储一个字符串切片(
got
),字符串通过walk
传递到fn
。在前面的章节中,通常我们会专门为函数或方法调用指定类型,但在这种情况下,我们可以传递一个匿名函数给fn
,它会隐藏got
。
- 我们使用带有
string
类型的Name
字段的匿名struct
,以此得到最简单的实现路径。
- 最后调用
walk
并传入x
参数,现在只检查got
的长度,一旦有了基本的可以运行的程序,我们的断言就会更加具体。
尝试运行测试
./reflection_test.go:21:2: undefined: walk
为测试的运行编写最小量的代码,并检查测试的失败输出
我们需要定义 walk
函数
- func walk(x interface{}, fn func(input string)) {
- }
再次尝试运行测试
=== RUN TestWalk
--- FAIL: TestWalk (0.00s)
reflection_test.go:19: wrong number of function calls, got 0 want 1
FAIL
编写足够的代码使测试通过
我们可以使用任意的字符串调用 fn
函数来使测试通过。
- func walk(x interface{}, fn func(input string)) {
- fn("I still can't believe South Korea beat Germany 2-0 to put them last in their group")
- }
现在测试应该通过了。接下来我们需要做的是对我们的 fn
是如何被调用的做一个更具体的断言。
首先编写测试
在之前的测试中添加以下代码,检查传入 fn
函数的字符串是否正确。
- if got[0] != expected {
- t.Errorf("got '%s', want '%s'", got[0], expected)
- }
尝试运行测试
=== RUN TestWalk
--- FAIL: TestWalk (0.00s)
reflection_test.go:23: got 'I still can't believe South Korea beat Germany 2-0 to put them last in their group', want 'Chris'
FAIL
编写足够的代码使测试通过
- func walk(x interface{}, fn func(input string)) {
- val := reflect.ValueOf(x)
- field := val.Field(0)
- fn(field.String())
- }
这段代码 非常不安全,也非常幼稚,但请记住,当我们处于「红色」状态(测试失败)时,我们的目标是编写尽可能少的代码。然后我们编写更多的测试来解决我们的问题。
x
并尝试查看它的属性。- 我们只看第一个也是唯一的字段,可能根本就没有字段会引起
panic
- 然后我们调用
String()它以字符串的形式返回底层值,但是我们知道,如果这个字段不是字符串,程序就会出错。
重构
fn
调用的字符串数组。
- func TestWalk(t *testing.T) {
- cases := []struct{
- Name string
- Input interface{}
- ExpectedCalls []string
- } {
- {
- "Struct with one string field",
- struct {
- Name string
- }{ "Chris"},
- []string{"Chris"},
- },
- }
- for _, test := range cases {
- t.Run(test.Name, func(t *testing.T) {
- var got []string
- walk(test.Input, func(input string) {
- got = append(got, input)
- })
- if !reflect.DeepEqual(got, test.ExpectedCalls) {
- t.Errorf("got %v, want %v", got, test.ExpectedCalls)
- }
- })
- }
- }
现在,我们可以很容易地添加一个场景,看看如果有多个字符串字段会发生什么。
首先编写测试
为测试用例添加以下场景。.
- {
- "Struct with two string fields",
- struct {
- Name string
- City string
- }{"Chris", "London"},
- []string{"Chris", "London"},
- }
尝试运行测试
=== RUN TestWalk/Struct_with_two_string_fields
--- FAIL: TestWalk/Struct_with_two_string_fields (0.00s)
reflection_test.go:40: got [Chris], want [Chris London]
编写足够的代码使测试通过
- func walk(x interface{}, fn func(input string)) {
- val := reflect.ValueOf(x)
- for i:=0; i<val.NumField(); i++ {
- field := val.Field(i)
- fn(field.String())
- }
- }
value
有一个方法 NumField
,它返回值中的字段数。这让我们遍历字段并调用 fn
通过我们的测试。
重构
walk
的另一个缺点是它假设每个字段都是 string 让我们为这个场景编写一个测试。
首先编写测试
添加一下测试用例
- {
- "Struct with non string field",
- struct {
- Name string
- Age int
- }{"Chris", 33},
- []string{"Chris"},
- },
尝试运行测试
=== RUN TestWalk/Struct_with_non_string_field
--- FAIL: TestWalk/Struct_with_non_string_field (0.00s)
reflection_test.go:46: got [Chris <int Value>], want [Chris]
编写足够的代码使测试通过
我们需要检查字段的类型是 string
。
- func walk(x interface{}, fn func(input string)) {
- val := reflect.ValueOf(x)
- for i := 0; i < val.NumField(); i++ {
- field := val.Field(i)
- if field.Kind() == reflect.String {
- fn(field.String())
- }
- }
- }
我们可以通过检查它的 Kind 来实现这个功能。
重构
struct
怎么办?换句话说,如果我们有一个包含嵌套字段的 struct会发生什么?
首先编写测试
我们临时使用匿名结构体语法为我们的测试声明类型,所以我们可以继续这样做
- {
- "Nested fields",
- struct {
- Name string
- Profile struct {
- Age int
- City string
- }
- }{"Chris", struct {
- Age int
- City string
- }{33, "London"}},
- []string{"Chris", "London"},
- },
struct
的结构。
- type Person struct {
- Name string
- Profile Profile
- }
- type Profile struct {
- Age int
- City string
- }
现在我们将这些添加到测试用例中,它提高了代码的可读性
- {
- "Nested fields",
- Person{
- "Chris",
- Profile{33, "London"},
- },
- []string{"Chris", "London"},
- },
尝试运行测试
=== RUN TestWalk/Nested_fields
--- FAIL: TestWalk/Nested_fields (0.00s)
reflection_test.go:54: got [Chris], want [Chris London]
这个问题是我们只在类型层次结构的第一级上迭代字段导致的。
.编写足够的代码使测试通过
- func walk(x interface{}, fn func(input string)) {
- val := reflect.ValueOf(x)
- for i := 0; i < val.NumField(); i++ {
- field := val.Field(i)
- if field.Kind() == reflect.String {
- fn(field.String())
- }
- if field.Kind() == reflect.Struct {
- walk(field.Interface(), fn)
- }
- }
- }
解决方法很简单,我们再次检查它的 Kind
如果它碰巧是一个 struct
我们就在内部 struct
上再次调用 walk
。
重构
- func walk(x interface{}, fn func(input string)) {
- val := reflect.ValueOf(x)
- for i := 0; i < val.NumField(); i++ {
- field := val.Field(i)
- switch field.Kind() {
- case reflect.String:
- fn(field.String())
- case reflect.Struct:
- walk(field.Interface(), fn)
- }
- }
- }
switch
会提高可读性,使代码更易于扩展。首先编写测试
添加这个测试用例
- {
- "Pointers to things",
- &Person{
- "Chris",
- Profile{33, "London"},
- },
- []string{"Chris", "London"},
- },
尝试运行测试
=== RUN TestWalk/Pointers_to_things
panic: reflect: call of reflect.Value.NumField on ptr Value [recovered]
panic: reflect: call of reflect.Value.NumField on ptr Value
编写足够的代码使测试通过
- func walk(x interface{}, fn func(input string)) {
- val := reflect.ValueOf(x)
- if val.Kind() == reflect.Ptr {
- val = val.Elem()
- }
- for i := 0; i < val.NumField(); i++ {
- field := val.Field(i)
- switch field.Kind() {
- case reflect.String:
- fn(field.String())
- case reflect.Struct:
- walk(field.Interface(), fn)
- }
- }
- }
指针类型的 Value
不能使用 NumField
方法,在执行此方法前需要调用 Elem()
提取底层值
重构
让我们封装一个获得 reflect.Value
的功能,将 interface{}
传入函数并返回这个值
- func walk(x interface{}, fn func(input string)) {
- val := getValue(x)
- for i := 0; i < val.NumField(); i++ {
- field := val.Field(i)
- switch field.Kind() {
- case reflect.String:
- fn(field.String())
- case reflect.Struct:
- walk(field.Interface(), fn)
- }
- }
- }
- func getValue(x interface{}) reflect.Value {
- val := reflect.ValueOf(x)
- if val.Kind() == reflect.Ptr {
- val = val.Elem()
- }
- return val
- }
- 得到
x
的reflect.Value
,这样我就可以检查它,我不在乎怎么做。
- 遍历字段,根据其类型执行任何需要执行的操作。
首先编写测试
- {
- "Slices",
- []Profile {
- {33, "London"},
- {34, "Reykjavík"},
- },
- []string{"London", "Reykjavík"},
- },
尝试运行测试
=== RUN TestWalk/Slices
panic: reflect: call of reflect.Value.NumField on slice Value [recovered]
panic: reflect: call of reflect.Value.NumField on slice Value
为测试的运行编写最小量的代码,并检查测试的失败输出
这与前面的指针场景类似,我们试图在 reflect.Value
中调用 NumField
。但它没有,因为它不是结构体。
编写足够的代码使测试通过
- func walk(x interface{}, fn func(input string)) {
- val := getValue(x)
- if val.Kind() == reflect.Slice {
- for i:=0; i< val.Len(); i++ {
- walk(val.Index(i).Interface(), fn)
- }
- return
- }
- for i := 0; i < val.NumField(); i++ {
- field := val.Field(i)
- switch field.Kind() {
- case reflect.String:
- fn(field.String())
- case reflect.Struct:
- walk(field.Interface(), fn)
- }
- }
- }
重构
walk
- 结构体中的每个字段
- 切片中的每一项
return
来停止执行剩余的代码),如果不是,我们就假设它是
struct
。
- func walk(x interface{}, fn func(input string)) {
- val := getValue(x)
- switch val.Kind() {
- case reflect.Struct:
- for i:=0; i<val.NumField(); i++ {
- walk(val.Field(i).Interface(), fn)
- }
- case reflect.Slice:
- for i:=0; i<val.Len(); i++ {
- walk(val.Index(i).Interface(), fn)
- }
- case reflect.String:
- fn(val.String())
- }
- }
struct
或切片,我们会遍历它的值,并对每个值调用 walk
函数。如果是 reflect.String
,我们就调用 fn
。walk的重复操作,但概念上它们是相同的。
- func walk(x interface{}, fn func(input string)) {
- val := getValue(x)
- numberOfValues := 0
- var getField func(int) reflect.Value
- switch val.Kind() {
- case reflect.String:
- fn(val.String())
- case reflect.Struct:
- numberOfValues = val.NumField()
- getField = val.Field
- case reflect.Slice:
- numberOfValues = val.Len()
- getField = val.Index
- }
- for i:=0; i< numberOfValues; i++ {
- walk(getField(i).Interface(), fn)
- }
- }
value
是一个 reflect.String
,我们就像平常一样调用 fn
。switch
将根据类型提取两个内容- 有多少字段
- 如何提取
Value
(Field
或Index
)
numberOfValues
,使用 getField
函数的结果调用 walk
函数。首先编写测试
添加以下代码到测试用例中:
- {
- "Arrays",
- [2]Profile {
- {33, "London"},
- {34, "Reykjavík"},
- },
- []string{"London", "Reykjavík"},
- },
=== RUN TestWalk/Arrays
--- FAIL: TestWalk/Arrays (0.00s)
reflection_test.go:78: got [], want [London Reykjavík]
编写足够的代码使测试通过
数组的处理方式与切片处理方式相同,因此只需用逗号将其添加到测试用例中
- func walk(x interface{}, fn func(input string)) {
- val := getValue(x)
- numberOfValues := 0
- var getField func(int) reflect.Value
- switch val.Kind() {
- case reflect.String:
- fn(val.String())
- case reflect.Struct:
- numberOfValues = val.NumField()
- getField = val.Field
- case reflect.Slice, reflect.Array:
- numberOfValues = val.Len()
- getField = val.Index
- }
- for i:=0; i< numberOfValues; i++ {
- walk(getField(i).Interface(), fn)
- }
- }
我们想处理的最后一个类型是 map
。
首先编写测试
- {
- "Maps",
- map[string]string{
- "Foo": "Bar",
- "Baz": "Boz",
- },
- []string{"Bar", "Boz"},
- },
尝试运行测试
=== RUN TestWalk/Maps
--- FAIL: TestWalk/Maps (0.00s)
reflection_test.go:86: got [], want [Bar Boz]
编写足够的代码使测试通过
如果你抽象地想一下你会发现 map
和 struct
很相似,只是编译时的键是未知的。
- func walk(x interface{}, fn func(input string)) {
- val := getValue(x)
- numberOfValues := 0
- var getField func(int) reflect.Value
- switch val.Kind() {
- case reflect.String:
- fn(val.String())
- case reflect.Struct:
- numberOfValues = val.NumField()
- getField = val.Field
- case reflect.Slice, reflect.Array:
- numberOfValues = val.Len()
- getField = val.Index
- case reflect.Map:
- for _, key := range val.MapKeys() {
- walk(val.MapIndex(key).Interface(), fn)
- }
- }
- for i:=0; i< numberOfValues; i++ {
- walk(getField(i).Interface(), fn)
- }
- }
然而通过设计,你无法通过索引从 map
中获取值。它只能通过 键 来完成,这样就打破了我们的抽象,该死。
重构
- func walk(x interface{}, fn func(input string)) {
- val := getValue(x)
- walkValue := func(value reflect.Value) {
- walk(value.Interface(), fn)
- }
- switch val.Kind() {
- case reflect.String:
- fn(val.String())
- case reflect.Struct:
- for i := 0; i< val.NumField(); i++ {
- walkValue(val.Field(i))
- }
- case reflect.Slice, reflect.Array:
- for i:= 0; i<val.Len(); i++ {
- walkValue(val.Index(i))
- }
- case reflect.Map:
- for _, key := range val.MapKeys() {
- walkValue(val.MapIndex(key))
- }
- }
- }
我们已经介绍了 walkValue
,它依照「Don't repeat yourself」的原则在 switch
中调用 walk
函数,这样它们就只需要从 val
中提取 reflect.Value
即可。
最后一个问题
map
不能保证顺序一致。因此,你的测试有时会失败,因为我们断言对 fn
的调用是以特定的顺序完成的。map
- t.Run("with maps", func(t *testing.T) {
- aMap := map[string]string{
- "Foo": "Bar",
- "Baz": "Boz",
- }
- var got []string
- walk(aMap, func(input string) {
- got = append(got, input)
- })
- assertContains(t, got, "Bar")
- assertContains(t, got, "Boz")
- })
下面是 assertContains
是如何定义的
- func assertContains(t *testing.T, haystack []string, needle string) {
- contains := false
- for _, x := range haystack {
- if x == needle {
- contains = true
- }
- }
- if !contains {
- t.Errorf("expected %+v to contain '%s' but it didnt", haystack, needle)
- }
- }
大功告成!
总结
- 介绍了
reflect
包中的一些概念。
- 使用递归遍历任意数据结构。
- 在回顾中做了一个糟糕的重构,但不用对此感到太沮丧。通过迭代地进行测试,这并不是什么大问题。
Golang 挑战:编写函数 walk(x interface{}, fn func(string)),参数为结构体 x,并对 x 中的所有字符串字段调用 fn 函数。难度级别:递归。的更多相关文章
- Js中常用的字符串,数组,函数扩展
由于最近辞职在家,自己的时间相对多一点.所以就根据prototytpeJS的API,结合自己正在看的司徒大神的<javascript框架设计>,整理了下Js中常用一些字符串,数组,函数扩展 ...
- dateline 在数据库中就是 整型字段。date函数是可以转换成可读日期的。
返回数据中的dateline全部用date()函数转换后再返回,是要嵌套循环还是遍历,代码怎么写? //查询我的活动 function user_activity_info_by_uid($uid){ ...
- C语言开发函数库时利用不透明指针对外隐藏结构体细节
1 模块化设计要求库接口隐藏实现细节 作为一个函数库来说,尽力降低和其调用方的耦合.是最主要的设计标准. C语言,作为经典"程序=数据结构+算法"的践行者,在实现函数库的时候,必定 ...
- 前端笔记之ES678&Webpack&Babel(中)对象|字符串|数组的扩展&函数新特性&类
一.对象的扩展 1.1对象属性名表达式 ES6可以在JSON中使用[]包裹一个key的名字.此时这个key将用表达式作为属性名(被当做变量求值),这个key值必须是字符串. var a = 'name ...
- 【Golang】创建有配置参数的结构体时,可选参数应该怎么传?
写在前面的话 Golang中构建结构体的时候,需要通过可选参数方式创建,我们怎么样设计一个灵活的API来初始化结构体呢. 让我们通过如下的代码片段,一步一步说明基于可选参数模式的灵活 API 怎么设计 ...
- cocos2dx中使用tolua++使lua调用c++函数
一直想学学cocos2dx中如何使用tolua++工具使得lua脚本调用C++函数,今天就来搞一下,顺便记录下来: 首先,我们打开cocos2dx-2.2.4中projects下的test的VS工程, ...
- 类成员函数指针的特殊之处(成员函数指针不是指针,内含一个结构体,需要存储更多的信息才能知道自己是否virtual函数)
下面讨论的都是类的非静态成员函数. 类成员函数指针的声明及调用: 1 2 3 4 5 6 7 //pr是指向Base类里的非静态成员函数的指针 //其行参为(int, int),返回值为void vo ...
- asp.net中<input type=button>无法调用后台函数
例如:用<input id="bt1" type="button" runat="server" Onclick="btnL ...
- enginefuncs_t 结构体中的函数
就是常见的 g_engfuncs 中的函数.AMXX 里就是 fakemeta 的 EngFunc_** // 这些函数由引擎提供给EXTDLL使用.mp.dll hl.dll ... typedef ...
- C++利用模板在Windows上快速调用DLL函数
更新日志 --------- 2021/08/01 更新V2.2 增加 GetHmodule 函数 - 允许用户获取HMODULE以验证加载DLL是否成功. 2021/08/03 更新V2.3 增加 ...
随机推荐
- 关于jmeter性能测试小记的12345
jmeter性能测试: linux环境命令:后台启jar包:nohup java -jar *.java &前台启jar包:java -jar 后台执行jmeter命令,打印控制台输出在log ...
- 一次讲清promise
此文章主要讲解核心思想和基本用法,想要了解更多细节全面的使用方式,请阅读官方API 这篇文章假定你具备最基本的异步编程知识,例如知道什么是回调,知道什么是链式调用,同时具备最基本的单词量,例如page ...
- kali2020-bash: openvas-setup:未找到命令 ,解决办法
将openvas-setup命令换成 gvm-setup命令即可
- 使用Python实现给企业微信发送allure报告,并实现微信查看
1.注册企业微信 搜索企业微信直接注册 2.创建应用 3.查看企业id.Secret.应用id.部门id 4.发送代码 import os import jenkins import requests ...
- python 项目启动
批量执行requirements.txt文件: pip install -r requirements.txt 清华镜像源安装: pip install -i https://pypi.tuna.ts ...
- Jetpack compose学习笔记之ConstraintLayout(布局)
一,简介 Jetpack compose中没有提供ConstraintLayout支持,所以需要添加下面的依赖来导入. // build.gradle implementation "and ...
- QT动态库的创建和使用
QT动态库的创建和使用 步骤一: 创建一个库文件 Library 步骤二:进行动态库封装方法的实现 注意事项:要注意共享类均需要包含导出的宏定义 这个宏定义和导出向导的宏定义一致 宏定义: 向导文件: ...
- ios底部安全距离
一.使用背景 苹果官方推荐:使用env(),constant()来适配,env()和constant(),是IOS11新增特性,用于设定安全区域与边界的距离 safe-area-inset-left: ...
- layui.dtree的学习,自定义扩展toolbar按钮(toolbarExt)
学习layui.dtree请前往 http://www.wisdomelon.com/DTreeHelper/ 记录一下dtree的自定义扩展toolbar按钮(toolbarExt) html代码: ...
- XML_DTD_20200415
<!-- xml的注释写法 --> 格式良好的xml语言必须具备的几个条件 1.必须有xml声明语句,声明版本号与编码字符集 2.必须有且仅有一个根元素 3.标签大小写敏感 4.属性值 ...