Golang 挑战:编写函数 walk(x interface{}, fn func(string)),参数为结构体 x,并对 x 中的所有字符串字段调用 fn 函数。难度级别:递归。

golang 挑战:编写函数 walk(x interface{}, fn func(string)),参数为结构体 x,并对 x 中的所有字符串字段调用 fn 函数。难度级别:递归。

为此,我们需要使用 反射

计算中的反射提供了程序检查自身结构体的能力,特别是通过类型,这是元编程的一种形式。这也是造成困惑的一个重要原因。

什么是 interface?

由于函数使用已知的类型,例如 stringint 以及我们自己定义的类型,如 BankAccount,我们享受到了 Go 为我们提供的类型安全。

这意味着我们可以免费获得一些文档,如果你试图向函数传递错误的类型,编译器就会报错。

但是,你可能会遇到这样的情况,即你不知道要编写的函数参数在编译时是什么类型的。

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) !=  {
        t.Errorf("wrong number of function calls, got %d want %d", len(got), )
    }
}
  • 我们想存储一个字符串切片(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[] != expected {
    t.Errorf("got '%s', want '%s'", got[], 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()
    fn(field.String())
}

这段代码 非常不安全,也非常幼稚,但请记住,当我们处于「红色」状态(测试失败)时,我们的目标是编写尽可能少的代码。然后我们编写更多的测试来解决我们的问题。

我们需要使用反射来查看 x 并尝试查看它的属性。

反射包有一个函数 ValueOf,该函数值返回一个给定变量的 Value。这为我们提供了检查值的方法,包括我们在下一行中使用的字段。

然后我们对传入的值做了一些非常乐观的假设:

  •  我们只看第一个也是唯一的字段,可能根本就没有字段会引起 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:=; 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", },
    []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 := ; 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
    }{, "London"}},
    []string{"Chris", "London"},
},

但我们可以看到,当你得到内部匿名结构时语法会有点混乱。这里有一个建议可以使它的语法更好

让我们通过为这个场景创建一个已知类型并在测试中引用它来重构它。有一点间接的地方是,我们测试的一些代码在测试之外,但是读者应该能够通过观察初始化来推断 struct 的结构。

在测试文件中添加以下类型声明

type Person struct {
    Name    string
    Profile Profile
}

type Profile struct {
    Age  int
    City string
}

现在我们将这些添加到测试用例中,它提高了代码的可读性

{
    "Nested fields",
    Person{
        "Chris",
        Profile{, "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 := ; 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 := ; 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{, "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 := ; 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 := ; 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
}

这实际上增加了更多的代码,但我觉得抽象层是没有问题的

  • 得到 xreflect.Value,这样我就可以检查它,我不在乎怎么做。
  • 遍历字段,根据其类型执行任何需要执行的操作。
 

接下来我们需要覆盖切片。

首先编写测试

{
    "Slices",
    []Profile {
        {, "London"},
        {, "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:=; i< val.Len(); i++ {
            walk(val.Index(i).Interface(), fn)
        }
        return
    }

    for i := ; 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:=; i<val.NumField(); i++ {
            walk(val.Field(i).Interface(), fn)
        }
    case reflect.Slice:
        for i:=; 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 := 
    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:=; i< numberOfValues; i++ {
        walk(getField(i).Interface(), fn)
    }
}

如果 value 是一个 reflect.String,我们就像平常一样调用 fn

否则,我们的 switch 将根据类型提取两个内容

  • 有多少字段
  • 如何提取 ValueFieldIndex

一旦确定了这些东西,我们就可以遍历 numberOfValues,使用 getField 函数的结果调用 walk 函数。

 

现在我们已经完成了,处理数组应该很简单了。

首先编写测试

添加以下代码到测试用例中:

{
    "Arrays",
    []Profile {
        {, "London"},
        {, "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 := 
    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:=; 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]

编写足够的代码使测试通过

如果你抽象地想一下你会发现 mapstruct 很相似,只是编译时的键是未知的。

func walk(x interface{}, fn func(input string)) {
    val := getValue(x)

    numberOfValues := 
    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:=; i< numberOfValues; i++ {
        walk(getField(i).Interface(), fn)
    }
}

然而通过设计,你无法通过索引从 map 中获取值。它只能通过 来完成,这样就打破了我们的抽象,该死。

重构

你现在感觉怎么样?这在当时可能是一个很好的抽象,但现在代码感觉有点不稳定。

这没问题! 重构是一段旅程,有时我们会犯错误。TDD 的一个主要观点是它给了我们尝试这些东西的自由。

 

通过步步为营的原则就不会发生不可逆转的局面。让我们把它恢复到重构之前的状态。

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 := ; i< val.NumField(); i++ {
            walkValue(val.Field(i))
        }
    case reflect.Slice, reflect.Array:
        for i:= ; 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 即可。

最后一个问题

记住,Go 中的 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 包中的一些概念。
  •  使用递归遍历任意数据结构。
  •  在回顾中做了一个糟糕的重构,但不用对此感到太沮丧。通过迭代地进行测试,这并不是什么大问题。
0 条评论
请不要发布违法违规有害信息,如发现请及时举报或反馈
还没有人评论呢,速度抢占沙发!
相关文章
  • 流程控制 选择结构(分支语句) 因为switch只能匹配固定值,推荐使用if-else做条件筛选 if-else判断 package main import "fmt" func main()...

  • 字符串 字符集用来做什么 字符集是为每个字符分配一个唯一的ID 在同一个字符集内,字符的ID是唯一的,不同字符集ID可能是不同的 UTF-8是编码规则或者说是Unicode的一种实现 UTF-8...

  • 概述Go 中 字符串 语义和其他编程语言中的字符串中一样,有一点不同的地方在于: Go 中字符串值无法改变,可以理解为:一旦完成定义之后,字符串就是一个 常量。解释型字符串使用双引号 " 括起来,其中...

  • 前言     最近有人在某平台提问“C#的string是一种糟糕的设计吗?”,他认为官方为字符串做了很多内部优化处理,这种处理增加了理解成本,为什么不能提供char[]指针处理方案呢?   C#的st...

  • 7-2 案例 字符串关键字的散列映射 题目描述   给定一系列由大写英文字母组成的字符串关键字和素数P,用移位法定义的散列函数H(Key)将关键字Key中的最后3个字符映射为整数,每个字符占5位;再用...

  • 需要借助的库 github.com/Shopify/sarama // kafka主要的库* github.com/bsm/sarama-cluster // kafka消费组 生产者 packag...

  • 路由 f, h, err := c.GetFile("uploadFile") if err != nil { logx.Error("getfile err ", err) ...

  • 概述建议先阅读 非缓冲通道, 缓冲通道, 关闭通道,通道方向。Go 语言没有提供函数或方法判断一个通道是否关闭。因此只能使用一个变通的办法:接收通道元素,根据返回的布尔值确定通道是否关闭。例子双向通道...

  • 普通打印优点:内置函数,不需要引入额外的包,简单方便。不足:无法进行格式化打印,无法完整打印复合数据结构 (如数组, Map 等)。println 函数打印多个传入的参数,并自动加一个换行。例子pac...

  • 项目中的dao层,我们用来查询数据库,获取想要数据。有时我们会需要查询数据给结构体赋值,并返回一个结构体指针,如下 // 结构体字段已与数据库对应 func GetCommunity(id int) ...

  • crony - 分布式定时任务管理平台 1. 基本介绍 1.1 项目背景 项目中存在许多定时任务,很多代码写法都是采取见缝插针式的写法或者直接丢到task服务里面写,存在以下问题 服...

  • 切片 Go语言切片是一种建立在数组类型之上的抽象,它构建在数组之上并且提供更强大的能力和便捷。解决了数组长度不可变等缺陷 声明切片 切片的数据结构非常小,只有三个字段:指向底层数组的指针,切片...

  • Sync(锁) 在前面讲 channel 的时候,我们说到在 Go 语言并发编程中,倡导使用通信共享内存,不要使用共享内存通信,即 goroutine 之间尽量通过 channel 来协作。 而在其他...

  • 一、标识符 标识符是指定义的具有特殊意义的词,例如变量、常量、函数名等等,任何一门语言中都对自己的标识符有特殊定义的规则。在 Go 语言中,标识符由字母数字和下划线组成,并且只能以字母和下划线开头,例...

  • 背景 在go的工程中,有时init的顺序是至关重要的,本文写了一个小控制器去管理init的顺序,可以根据自己的要求设置不同的权重来实现加载顺序。 本文控制器主要实现两个功能,一是按照优先级加载包的引用...

  • dongle 是一个轻量级、语义化、对开发者友好的 Golang 编码解码和加密解密库Dongle 已被 awesome-go 收录, 如果您觉得不错,请给个 star 吧github.com/gol...

  • 获取微信昵称、头像、OpenID 微信公众号服务号的网页授权功能开发,主要是通过js跳转到一个微信提供的url 然后微信会弹出获取昵称头像的按钮 允许获取后,会回跳到我们的网址上,并且带着一个code...

  • 1. KMP算法简介 温馨提示:在通篇阅读完并理解后再看简介效果更佳 以下简介由百度百科提供https://baike.baidu.com/item/KMP%E7%AE%97%E6%B3%95/109...

  • 前言 之前一篇文章介绍了 日志库zap https://www.cnblogs.com/zichliang/p/17311480.html 毋庸置疑,zap库无论是Golang在项目中 还是生产中都极...

  • 什么是传值(值传递) func main() { i:=10 ip:=&i fmt.Printf("原始指针的内存地址是:%p\n",&ip) modify(ip) fmt.Println(...