go教程11

1.     Panic和recover

什么是 panic?

在 Go 语言中,程序中一般是使用错误来处理异常情况。对于程序中出现的大部分异常情况,错误就已经够用了。

但在有些情况,当程序发生异常时,无法继续运行。在这种情况下,我们会使用 panic 来终止程序。当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序控制返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪(Stack Trace),最后程序终止。在编写一个示例程序后,我们就能很好地理解这个概念了。

在本教程里,我们还会接着讨论,当程序发生 panic 时,使用 recover 可以重新获得对该程序的控制。

可以认为 panic 和 recover 与其他语言中的 try-catch-finally 语句类似,只不过一般我们很少使用 panic 和 recover。而当我们使用了 panic 和 recover 时,也会比 try-catch-finally 更加优雅,代码更加整洁。

什么时候应该使用 panic?

需要注意的是,你应该尽可能地使用错误,而不是使用 panic recover。只有当程序不能继续运行的时候,才应该使用 panic recover 机制

panic 有两个合理的用例。

  1. 发生了一个不能恢复的错误,此时程序不能继续运行。 一个例子就是 web 服务器无法绑定所要求的端口。在这种情况下,就应该使用 panic,因为如果不能绑定端口,啥也做不了。
  2. 发生了一个编程上的错误。 假如我们有一个接收指针参数的方法,而其他人使用 nil 作为参数调用了它。在这种情况下,我们可以使用 panic,因为这是一个编程错误:用 nil 参数调用了一个只能接收合法指针的方法。

panic 示例

内建函数 panic 的签名如下所示:

func panic(interface{})

当程序终止时,会打印传入 panic 的参数。我们写一个示例,你就会清楚它的用途了。我们现在就开始吧。

我们会写一个例子,来展示 panic 如何工作。

package main
 
import (  
    "fmt"
)
 
func fullName(firstName *string, lastName *string) {  
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}
 
func main() {  
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}

playground 上运行

上面的程序很简单,会打印一个人的全名。第 7 行的 fullName 函数会打印出一个人的全名。该函数在第 8 行和第 11 行分别检查了 firstName 和 lastName 的指针是否为 nil。如果是 nilfullName 函数会调用含有不同的错误信息的 panic。当程序终止时,会打印出该错误信息。

运行该程序,会有如下输出:

panic: runtime error: last name cannot be nil
 
goroutine 1 [running]:  
main.fullName(0x1040c128, 0x0)  
    /tmp/sandbox135038844/main.go:12 +0x120
main.main()  
    /tmp/sandbox135038844/main.go:20 +0x80

我们来分析这个输出,理解一下 panic 是如何工作的,并且思考当程序发生 panic 时,会怎样打印堆栈跟踪。

在第 19 行,我们将 Elon 赋值给了 firstName。在第 20 行,我们调用了 fullName 函数,其中 lastName 等于 nil。因此,满足了第 11 行的条件,程序发生 panic。当出现了 panic 时,程序就会终止运行,打印出传入 panic 的参数,接着打印出堆栈跟踪。因此,第 14 行和第 15 行的代码并不会在发生 panic 之后执行。程序首先会打印出传入 panic 函数的信息:

panic: runtime error: last name cannot be empty

接着打印出堆栈跟踪。

程序在 fullName 函数的第 12 行发生 panic,因此,首先会打印出如下所示的输出。

main.fullName(0x1040c128, 0x0)  
    /tmp/sandbox135038844/main.go:12 +0x120

接着会打印出堆栈的下一项。在本例中,堆栈跟踪中的下一项是第 20 行(因为发生 panic 的 fullName 调用就在这一行),因此接下来会打印出:

main.main()  
    /tmp/sandbox135038844/main.go:20 +0x80

现在我们已经到达了导致 panic 的顶层函数,这里没有更多的层级,因此结束打印。

发生 panic 时的 defer

我们重新总结一下 panic 做了什么。当函数发生 panic 时,它会终止运行,在执行完所有的延迟函数后,程序控制返回到该函数的调用方。这样的过程会一直持续下去,直到当前协程的所有函数都返回退出,然后程序会打印出 panic 信息,接着打印出堆栈跟踪,最后程序终止

在上面的例子中,我们没有延迟调用任何函数。如果有延迟函数,会先调用它,然后程序控制返回到函数调用方。

我们来修改上面的示例,使用一个延迟语句。

package main
 
import (  
    "fmt"
)
 
func fullName(firstName *string, lastName *string) {  
    defer fmt.Println("deferred call in fullName")
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}
 
func main() {  
    defer fmt.Println("deferred call in main")
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}

playground 上运行

上述代码中,我们只修改了两处,分别在第 8 行和第 20 行添加了延迟函数的调用。

该函数会打印:

This program prints,
 
deferred call in fullName  
deferred call in main  
panic: runtime error: last name cannot be nil
 
goroutine 1 [running]:  
main.fullName(0x1042bf90, 0x0)  
    /tmp/sandbox060731990/main.go:13 +0x280
main.main()  
    /tmp/sandbox060731990/main.go:22 +0xc0

当程序在第 13 行发生 panic 时,首先执行了延迟函数,接着控制返回到函数调用方,调用方的延迟函数继续运行,直到到达顶层调用函数。先执行所以defer函数,在panic

在我们的例子中,首先执行 fullName 函数中的 defer 语句(第 8 行)。程序打印出:

deferred call in fullName

接着程序返回到 main 函数,执行了 main 函数的延迟调用,因此会输出:

deferred call in main

现在程序控制到达了顶层函数,因此该函数会打印出 panic 信息,然后是堆栈跟踪,最后终止程序。

recover

recover 是一个内建函数,用于重新获得 panic 协程的控制。

recover 函数的标签如下所示:

func recover() interface{}

只有在延迟函数的内部,调用 recover 才有用。在延迟函数内调用 recover,可以取到 panic 的错误信息,并且停止 panic 续发事件(Panicking Sequence),程序运行恢复正常。如果在延迟函数的外部调用 recover,就不能停止 panic 续发事件。

我们来修改一下程序,在发生 panic 之后,使用 recover 来恢复正常的运行。

package main
 
import (  
    "fmt"
)
 
func recoverName() {  
    if r := recover(); r!= nil {
        fmt.Println("recovered from ", r)
    }
}
 
func fullName(firstName *string, lastName *string) {  
    defer recoverName()
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}
 
func main() {  
    defer fmt.Println("deferred call in main")
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}

playground 上运行

在第 7 行,recoverName() 函数调用了 recover(),返回了调用 panic 的传参。在这里,我们只是打印出 recover 的返回值(第 8 行)。在 fullName 函数内,我们在第 14 行延迟调用了 recoverNames()

当 fullName 发生 panic 时,会调用延迟函数 recoverName(),它使用了 recover() 来停止 panic 续发事件。

该程序会输出:

recovered from  runtime error: last name cannot be nil  
returned normally from main  
deferred call in main

当程序在第 19 行发生 panic 时,会调用延迟函数 recoverName,它反过来会调用 recover() 来重新获得 panic 协程的控制。第 8 行调用了 recover,返回了 panic 的传参,因此会打印:

recovered from  runtime error: last name cannot be nil

在执行完 recover() 之后,panic 会停止,程序控制返回到调用方(在这里就是 main 函数),程序在发生 panic 之后,从第 29 行开始会继续正常地运行。程序会打印 returned normally from main,之后是 deferred call in main

panic,recover 和 Go 协程

只有在相同的 Go 协程中调用 recover 才管用recover 不能恢复一个不同协程的 panic。我们用一个例子来理解这一点。

package main
 
import (  
    "fmt"
    "time"
)
 
func recovery() {  
    if r := recover(); r != nil {
        fmt.Println("recovered:", r)
    }
}
 
func a() {  
    defer recovery()
    fmt.Println("Inside A")
    go b()
    time.Sleep(1 * time.Second)
}
 
func b() {  
    fmt.Println("Inside B")
    panic("oh! B panicked")
}
 
func main() {  
    a()
    fmt.Println("normally returned from main")
}

playground 上运行

在上面的程序中,函数 b() 在第 23 行发生 panic。函数 a() 调用了一个延迟函数 recovery(),用于恢复 panic。在第 17 行,函数 b() 作为一个不同的协程来调用。下一行的 Sleep 只是保证 a() 在 b() 运行结束之后才退出。

你认为程序会输出什么?panic 能够恢复吗?答案是否定的,panic 并不会恢复。因为调用 recovery 的协程和 b() 中发生 panic 的协程并不相同,因此不可能恢复 panic。

运行该程序会输出:

Inside A  
Inside B  
panic: oh! B panicked
 
goroutine 5 [running]:  
main.b()  
    /tmp/sandbox388039916/main.go:23 +0x80
created by main.a  
    /tmp/sandbox388039916/main.go:17 +0xc0

从输出可以看出,panic 没有恢复。

如果函数 b() 在相同的协程里调用,panic 就可以恢复。

如果程序的第 17 行由 go b() 修改为 b(),就可以恢复 panic 了,因为 panic 发生在与 recover 相同的协程里。如果运行这个修改后的程序,会输出:

Inside A  
Inside B  
recovered: oh! B panicked  
normally returned from main

运行时 panic

运行时错误(如数组越界)也会导致 panic。这等价于调用了内置函数 panic,其参数由接口类型 runtime.Error 给出。runtime.Error 接口的定义如下:

type Error interface {  
    error
    // RuntimeError is a no-op function but
    // serves to distinguish types that are run time
    // errors from ordinary errors: a type is a
    // run time error if it has a RuntimeError method.
    RuntimeError()
}

而 runtime.Error 接口满足内建接口类型 error

我们来编写一个示例,创建一个运行时 panic。

package main
 
import (  
    "fmt"
)
 
func a() {  
    n := []int{5, 7, 4}
    fmt.Println(n[3])
    fmt.Println("normally returned from a")
}
func main() {  
    a()
    fmt.Println("normally returned from main")
}

playground 上运行

在上面的程序中,第 9 行我们试图访问 n[3],这是一个对切片的错误引用。该程序会发生 panic,输出如下:

panic: runtime error: index out of range
 
goroutine 1 [running]:  
main.a()  
    /tmp/sandbox780439659/main.go:9 +0x40
main.main()  
    /tmp/sandbox780439659/main.go:13 +0x20

你也许想知道,是否可以恢复一个运行时 panic?当然可以!我们来修改一下上面的代码,恢复这个 panic。

package main
 
import (  
    "fmt"
)
 
func r() {  
    if r := recover(); r != nil {
        fmt.Println("Recovered", r)
    }
}
 
func a() {  
    defer r()
    n := []int{5, 7, 4}
    fmt.Println(n[3])
    fmt.Println("normally returned from a")
}
 
func main() {  
    a()
    fmt.Println("normally returned from main")
}

playground 上运行

运行上面程序会输出:

Recovered runtime error: index out of range  
normally returned from main

从输出可以知道,我们已经恢复了这个 panic。

恢复后获得堆栈跟踪

当我们恢复 panic 时,我们就释放了它的堆栈跟踪。实际上,在上述程序里,恢复 panic 之后,我们就失去了堆栈跟踪。

有办法可以打印出堆栈跟踪,就是使用 Debug 包中的 PrintStack 函数。

package main
 
import (  
    "fmt"
    "runtime/debug"
)
 
func r() {  
    if r := recover(); r != nil {
        fmt.Println("Recovered", r)
        debug.PrintStack()
    }
}
 
func a() {  
    defer r()
    n := []int{5, 7, 4}
    fmt.Println(n[3])
    fmt.Println("normally returned from a")
}
 
func main() {  
    a()
    fmt.Println("normally returned from main")
}

playground 上运行

在上面的程序中,我们在第 11 行使用了 debug.PrintStack() 打印堆栈跟踪。

该程序会输出:

Recovered runtime error: index out of range  
goroutine 1 [running]:  
runtime/debug.Stack(0x1042beb8, 0x2, 0x2, 0x1c)  
    /usr/local/go/src/runtime/debug/stack.go:24 +0xc0
runtime/debug.PrintStack()  
    /usr/local/go/src/runtime/debug/stack.go:16 +0x20
main.r()  
    /tmp/sandbox949178097/main.go:11 +0xe0
panic(0xf0a80, 0x17cd50)  
    /usr/local/go/src/runtime/panic.go:491 +0x2c0
main.a()  
    /tmp/sandbox949178097/main.go:18 +0x80
main.main()  
    /tmp/sandbox949178097/main.go:23 +0x20
normally returned from main

从输出我们可以看出,首先已经恢复了 panic,打印出 Recovered runtime error: index out of range。此外,我们也打印出了堆栈跟踪。在恢复了 panic 之后,还打印出 normally returned from main

本教程到此结束。

简单概括一下本教程讨论的内容:

  • 什么是 panic?
  • 什么时候应该使用 panic?
  • panic 示例
  • 发生 panic 时的 defer
  • recover
  • panic,recover 和 Go 协程
  • 运行时 panic
  • 恢复后获得堆栈跟踪

祝你愉快。

2.     头等函数

什么是头等函数?

支持头等函数(First Class Function)的编程语言,可以把函数赋值给变量,也可以把函数作为其它函数的参数或者返回值Go 语言支持头等函数的机制

本教程我们会讨论头等函数的语法和用例。

匿名函数

我们来编写一个简单的示例,把函数赋值给一个变量

package main
 
import (  
    "fmt"
)
 
func main() {  
    a := func() {
        fmt.Println("hello world first class function")
    }
    a()
    fmt.Printf("%T", a)
}

playground 上运行

在上面的程序中,我们将一个函数赋值给了变量 a(第 8 行)。这是把函数赋值给变量的语法。你如果观察得仔细的话,会发现赋值给 a 的函数没有名称。由于没有名称,这类函数称为匿名函数(Anonymous Function

调用该函数的唯一方法就是使用变量 a。我们在下一行调用了它。a() 调用了这个函数,打印出 hello world first class function。在第 12 行,我们打印出 a 的类型。这会输出 func()

运行该程序,会输出:

hello world first class function
func()

要调用一个匿名函数,可以不用赋值给变量。通过下面的例子,我们看看这是怎么做到的。

package main
 
import (  
    "fmt"
)
 
func main() {  
    func() {
        fmt.Println("hello world first class function")
    }()
}

playground 上运行

在上面的程序中,第 8 行定义了一个匿名函数,并在定义之后,我们使用 () 立即调用了该函数(第 10 行)。该程序会输出:

hello world first class function

就像其它函数一样,还可以向匿名函数传递参数

package main
 
import (  
    "fmt"
)
 
func main() {  
    func(n string) {
        fmt.Println("Welcome", n)
    }("Gophers")
}

playground 上运行

在上面的程序中,我们向匿名函数传递了一个字符串参数(第 10 行)。运行该程序后会输出:

Welcome Gophers

用户自定义的函数类型

正如我们定义自己的结构体类型一样,我们可以定义自己的函数类型

type add func(a int, b int) int

以上代码片段创建了一个新的函数类型 add,它接收两个整型参数,并返回一个整型。现在我们来定义 add 类型的变量。

我们来编写一个程序,定义一个 add 类型的变量。

package main
 
import (  
    "fmt"
)
 
type add func(a int, b int) int
 
func main() {  
    var a add = func(a int, b int) int {
        return a + b
    }
    s := a(5, 6)
    fmt.Println("Sum", s)
}

playground 上运行

在上面程序的第 10 行,我们定义了一个 add 类型的变量 a,并向它赋值了一个符合 add 类型签名的函数。我们在第 13 行调用了该函数,并将结果赋值给 s。该程序会输出:

Sum 11

高阶函数

wiki 把高阶函数(Hiher-order Function)定义为:满足下列条件之一的函数

  • 接收一个或多个函数作为参数
  • 返回值是一个函数

针对上述两种情况,我们看看一些简单实例。

把函数作为参数,传递给其它函数

package main
 
import (  
    "fmt"
)
 
func simple(a func(a, b int) int) {  
    fmt.Println(a(60, 7))
}
 
func main() {  
    f := func(a, b int) int {
        return a + b
    }
    simple(f)
}

playground 上运行

在上面的实例中,第 7 行我们定义了一个函数 simplesimple 接收一个函数参数(该函数接收两个 int 参数,返回一个 a 整型)。在 main 函数的第 12 行,我们创建了一个匿名函数 f,其签名符合 simple 函数的参数。我们在下一行调用了 simple,并传递了参数 f。该程序打印输出 67。

在其它函数中返回函数

现在我们重写上面的代码,在 simple 函数中返回一个函数。

package main
 
import (  
    "fmt"
)
 
func simple() func(a, b int) int {  
    f := func(a, b int) int {
        return a + b
    }
    return f
}
 
func main() {  
    s := simple()
    fmt.Println(s(60, 7))
}

playground 上运行

在上面程序中,第 7 行的 simple 函数返回了一个函数,并接受两个 int 参数,返回一个 int

在第 15 行,我们调用了 simple 函数。我们把 simple 的返回值赋值给了 s。现在 s 包含了 simple 函数返回的函数。我们调用了 s,并向它传递了两个 int 参数(第 16 行)。该程序输出 67。

闭包

闭包(Closure)是匿名函数的一个特例。当一个匿名函数所访问的变量定义在函数体的外部时,就称这样的匿名函数为闭包

看看一个示例就明白了。

package main
 
import (  
    "fmt"
)
 
func main() {  
    a := 5
    func() {
        fmt.Println("a =", a)
    }()
}

playground 上运行

在上面的程序中,匿名函数在第 10 行访问了变量 a,而 a 存在于函数体的外部。因此这个匿名函数就是闭包。

每一个闭包都会绑定一个它自己的外围变量(Surrounding Variable)。我们通过一个简单示例来体会这句话的含义。

package main
 
import (  
    "fmt"
)
 
func appendStr() func(string) string {  
    t := "Hello"
    c := func(b string) string {
        t = t + " " + b
        return t
    }
    return c
}
 
func main() {  
    a := appendStr()
    b := appendStr()
    fmt.Println(a("World"))
    fmt.Println(b("Everyone"))
 
    fmt.Println(a("Gopher"))
    fmt.Println(b("!"))
}

playground 上运行

在上面程序中,函数 appendStr 返回了一个闭包。这个闭包绑定了变量 t。我们来理解这是什么意思。

在第 17 行和第 18 行声明的变量 a 和 b 都是闭包,它们绑定了各自的 t 值。

我们首先用参数 World 调用了 a。现在 a 中 t 值变为了 Hello World

在第 20 行,我们又用参数 Everyone 调用了 b。由于 b 绑定了自己的变量 t,因此 b 中的 t 还是等于初始值 Hello。于是该函数调用之后,b 中的 t 变为了 Hello Everyone。程序的其他部分很简单,不再解释。

该程序会输出:

Hello World  
Hello Everyone  
Hello World Gopher  
Hello Everyone !

头等函数的实际用途

迄今为止,我们已经定义了什么是头等函数,也看了一些专门设计的示例,来学习它们如何工作。现在我们来编写一些实际的程序,来展现头等函数的实际用处。

我们会创建一个程序,基于一些条件,来过滤一个 students 切片。现在我们来逐步实现它。

首先定义一个 student 类型。

type student struct {  
    firstName string
    lastName string
    grade string
    country string
}

下一步是编写一个 filter 函数。该函数接收一个 students 切片和一个函数作为参数,这个函数会计算一个学生是否满足筛选条件。写出这个函数后,你很快就会明白,我们继续吧。

func filter(s []student, f func(student) bool) []student {  
    var r []student
    for _, v := range s {
        if f(v) == true {
            r = append(r, v)
        }
    }
    return r
}

在上面的函数中,filter 的第二个参数是一个函数。这个函数接收 student 参数,返回一个 bool 值。这个函数计算了某一学生是否满足筛选条件。我们在第 3 行遍历了 student 切片,将每个学生作为参数传递给了函数 f。如果该函数返回 true,就表示该学生通过了筛选条件,接着将该学生添加到了结果切片 r 中。你可能会很困惑这个函数的实际用途,等我们完成程序你就知道了。我添加了 main 函数,整个程序如下所示:

package main
 
import (  
    "fmt"
)
 
type student struct {  
    firstName string
    lastName  string
    grade     string
    country   string
}
 
func filter(s []student, f func(student) bool) []student {  
    var r []student
    for _, v := range s {
        if f(v) == true {
            r = append(r, v)
        }
    }
    return r
}
 
func main() {  
    s1 := student{
        firstName: "Naveen",
        lastName:  "Ramanathan",
        grade:     "A",
        country:   "India",
    }
    s2 := student{
        firstName: "Samuel",
        lastName:  "Johnson",
        grade:     "B",
        country:   "USA",
    }
    s := []student{s1, s2}
    f := filter(s, func(s student) bool {
        if s.grade == "B" {
            return true
        }
        return false
    })
    fmt.Println(f)
}

playground 上运行

在 main 函数中,我们首先创建了两个学生 s1 和 s2,并将他们添加到了切片 s。现在假设我们想要查询所有成绩为 B 的学生。为了实现这样的功能,我们传递了一个检查学生成绩是否为 B 的函数,如果是,该函数会返回 true。我们把这个函数作为参数传递给了 filter 函数(第 38 行)。上述程序会输出:

[{Samuel Johnson B USA}]

假设我们想要查找所有来自印度的学生。通过修改传递给 filter 的函数参数,就很容易地实现了。

实现它的代码如下所示:

c := filter(s, func(s student) bool {  
    if s.country == "India" {
        return true
    }
    return false
})
fmt.Println(c)

请将该函数添加到 main 函数,并检查它的输出。

我们最后再编写一个程序,来结束这一节的讨论。这个程序会对切片的每个元素执行相同的操作,并返回结果。例如,如果我们希望将切片中的所有整数乘以 5,并返回出结果,那么通过头等函数可以很轻松地实现。我们把这种对集合中的每个元素进行操作的函数称为 map 函数。相关代码如下所示,它们很容易看懂。

package main
 
import (  
    "fmt"
)
 
func iMap(s []int, f func(int) int) []int {  
    var r []int
    for _, v := range s {
        r = append(r, f(v))
    }
    return r
}
func main() {  
    a := []int{5, 6, 7, 8, 9}
    r := iMap(a, func(n int) int {
        return n * 5
    })
    fmt.Println(r)
}

playground 上运行

该程序会输出:

[25 30 35 40 45]

现在简单概括一下本教程讨论的内容:

  • 什么是头等函数?
  • 匿名函数
  • 用户自定义的函数类型
  • 高阶函数
    • 把函数作为参数,传递给其它函数
    • 在其它函数中返回函数
  • 闭包
  • 头等函数的实际用途

本教程到此结束。祝你愉快。

3.     反射

反射是 Go 语言的高级主题之一。我会尽可能让它变得简单易懂。

本教程分为如下小节。

  • 什么是反射?
  • 为何需要检查变量,确定变量的类型?
  • reflect 包
    • reflect.Type 和 reflect.Value
    • reflect.Kind
    • NumField() 和 Field() 方法
    • Int() 和 String() 方法
  • 完整的程序
  • 我们应该使用反射吗?

让我们来逐个讨论这些章节。

什么是反射?

反射就是程序能够在运行时检查变量和值,求出它们的类型。你可能还不太懂,这没关系。在本教程结束后,你就会清楚地理解反射,所以跟着我们的教程学习吧。

为何需要检查变量,确定变量的类型?

在学习反射时,所有人首先面临的疑惑就是:如果程序中每个变量都是我们自己定义的,那么在编译时就可以知道变量类型了,为什么我们还需要在运行时检查变量,求出它的类型呢?没错,在大多数时候都是这样,但并非总是如此。

我来解释一下吧。下面我们编写一个简单的程序。

package main
 
import (
    "fmt"
)
 
func main() {
    i := 10
    fmt.Printf("%d %T", i, i)
}

playground 上运行

在上面的程序中,i 的类型在编译时就知道了,然后我们在下一行打印出 i。这里没什么特别之处。

现在了解一下,需要在运行时求得变量类型的情况。假如我们要编写一个简单的函数,它接收结构体作为参数,并用它来创建一个 SQL 插入查询。

考虑下面的程序:

package main
 
import (
    "fmt"
)
 
type order struct {
    ordId      int
    customerId int
}
 
func main() {
    o := order{
        ordId:      1234,
        customerId: 567,
    }
    fmt.Println(o)
}

playground 上运行

在上面的程序中,我们需要编写一个函数,接收结构体变量 o 作为参数,返回下面的 SQL 插入查询。

insert into order values(1234, 567)

这个函数写起来很简单。我们现在编写这个函数。

package main
 
import (
    "fmt"
)
 
type order struct {
    ordId      int
    customerId int
}
 
func createQuery(o order) string {
    i := fmt.Sprintf("insert into order values(%d, %d)", o.ordId, o.customerId)
    return i
}
 
func main() {
    o := order{
        ordId:      1234,
        customerId: 567,
    }
    fmt.Println(createQuery(o))
}

playground 上运行

在第 12 行,createQuery 函数用 o 的两个字段(ordId 和 customerId),创建了插入查询。该程序会输出:

insert into order values(1234, 567)

现在我们来升级这个查询生成器。如果我们想让它变得通用,可以适用于任何结构体类型,该怎么办呢?我们用程序来理解一下。

package main
 
type order struct {
    ordId      int
    customerId int
}
 
type employee struct {
    name string
    id int
    address string
    salary int
    country string
}
 
func createQuery(q interface{}) string {
}
 
func main() {
 
}

我们的目标就是完成 createQuery 函数(上述程序中的第 16 行),它可以接收任何结构体作为参数,根据结构体的字段创建插入查询。

例如,如果我们传入下面的结构体:

o := order {
    ordId: 1234,
    customerId: 567
}

createQuery 函数应该返回:

insert into order values (1234, 567)

类似地,如果我们传入:

 e := employee {
        name: "Naveen",
        id: 565,
        address: "Science Park Road, Singapore",
        salary: 90000,
        country: "Singapore",
    }

该函数会返回:

insert into employee values("Naveen", 565, "Science Park Road, Singapore", 90000, "Singapore")

由于 createQuery 函数应该适用于任何结构体,因此它接收 interface{} 作为参数。为了简单起见,我们只处理包含 string 和 int类型字段的结构体,但可以扩展为包含任何类型的字段。

createQuery 函数应该适用于所有的结构体。因此,要编写这个函数,就必须在运行时检查传递过来的结构体参数的类型,找到结构体字段,接着创建查询。这时就需要用到反射了。在本教程的下一步,我们将会学习如何使用 reflect 包来实现它。

reflect 包

在 Go 语言中,reflect 实现了运行时反射。reflect 包会帮助识别 interface{} 变量的底层具体类型和具体值。这正是我们所需要的。createQuery 函数接收 interface{} 参数,根据它的具体类型和具体值,创建 SQL 查询。这正是 reflect 包能够帮助我们的地方。

在编写我们通用的查询生成器之前,我们首先需要了解 reflect 包中的几种类型和方法。让我们来逐个了解。

reflect.Type 和 reflect.Value

reflect.Type 表示 interface{} 的具体类型,而 reflect.Value 表示它的具体值。reflect.TypeOf() 和 reflect.ValueOf() 两个函数可以分别返回 reflect.Type 和 reflect.Value。这两种类型是我们创建查询生成器的基础。我们现在用一个简单的例子来理解这两种类型。

package main
 
import (
    "fmt"
    "reflect"
)
 
type order struct {
    ordId      int
    customerId int
}
 
func createQuery(q interface{}) {
    t := reflect.TypeOf(q)
    v := reflect.ValueOf(q)
    fmt.Println("Type ", t)
    fmt.Println("Value ", v)
 
 
}
func main() {
    o := order{
        ordId:      456,
        customerId: 56,
    }
    createQuery(o)
 
}

playground 上运行

在上面的程序中,第 13 行的 createQuery 函数接收 interface{} 作为参数。在第 14 行,reflect.TypeOf 接收了参数 interface{},返回了reflect.Type,它包含了传入的 interface{} 参数的具体类型。同样地,在第 15 行,reflect.ValueOf 函数接收参数 interface{},并返回了 reflect.Value,它包含了传来的 interface{} 的具体值。

上述程序会打印:

Type  main.order
Value  {456 56}

从输出我们可以看到,程序打印了接口的具体类型和具体值。

relfect.Kind

reflect 包中还有一个重要的类型:Kind

在反射包中,Kind 和 Type 的类型可能看起来很相似,但在下面程序中,可以很清楚地看出它们的不同之处。

package main
 
import (
    "fmt"
    "reflect"
)
 
type order struct {
    ordId      int
    customerId int
}
 
func createQuery(q interface{}) {
    t := reflect.TypeOf(q)
    k := t.Kind()
    fmt.Println("Type ", t)
    fmt.Println("Kind ", k)
 
 
}
func main() {
    o := order{
        ordId:      456,
        customerId: 56,
    }
    createQuery(o)
 
}

playground 上运行

上述程序会输出:

Type  main.order
Kind  struct

我想你应该很清楚两者的区别了。Type 表示 interface{} 的实际类型(在这里是 main.Order),而 Kind 表示该类型的特定类别(在这里是 struct)。

NumField() 和 Field() 方法

NumField() 方法返回结构体中字段的数量,而 Field(i int) 方法返回字段 i 的 reflect.Value

package main
 
import (
    "fmt"
    "reflect"
)
 
type order struct {
    ordId      int
    customerId int
}
 
func createQuery(q interface{}) {
    if reflect.ValueOf(q).Kind() == reflect.Struct {
        v := reflect.ValueOf(q)
        fmt.Println("Number of fields", v.NumField())
        for i := 0; i < v.NumField(); i++ {
            fmt.Printf("Field:%d type:%T value:%v\n", i, v.Field(i), v.Field(i))
        }
    }
 
}
func main() {
    o := order{
        ordId:      456,
        customerId: 56,
    }
    createQuery(o)
}

playground 上运行

在上面的程序中,因为 NumField 方法只能在结构体上使用,我们在第 14 行首先检查了 q 的类别是 struct。程序的其他代码很容易看懂,不作解释。该程序会输出:

Number of fields 2
Field:0 type:reflect.Value value:456
Field:1 type:reflect.Value value:56

Int() 和 String() 方法

Int 和 String 可以帮助我们分别取出 reflect.Value 作为 int64 和 string

package main
 
import (
    "fmt"
    "reflect"
)
 
func main() {
    a := 56
    x := reflect.ValueOf(a).Int()
    fmt.Printf("type:%T value:%v\n", x, x)
    b := "Naveen"
    y := reflect.ValueOf(b).String()
    fmt.Printf("type:%T value:%v\n", y, y)
 
}

playground 上运行

在上面程序中的第 10 行,我们取出 reflect.Value,并转换为 int64,而在第 13 行,我们取出 reflect.Value 并将其转换为 string。该程序会输出:

type:int64 value:56
type:string value:Naveen

完整的程序

现在我们已经具备足够多的知识,来完成我们的查询生成器了,我们来实现它把。

package main
 
import (
    "fmt"
    "reflect"
)
 
type order struct {
    ordId      int
    customerId int
}
 
type employee struct {
    name    string
    id      int
    address string
    salary  int
    country string
}
 
func createQuery(q interface{}) {
    if reflect.ValueOf(q).Kind() == reflect.Struct {
        t := reflect.TypeOf(q).Name()
        query := fmt.Sprintf("insert into %s values(", t)
        v := reflect.ValueOf(q)
        for i := 0; i < v.NumField(); i++ {
            switch v.Field(i).Kind() {
            case reflect.Int:
                if i == 0 {
                    query = fmt.Sprintf("%s%d", query, v.Field(i).Int())
                } else {
                    query = fmt.Sprintf("%s, %d", query, v.Field(i).Int())
                }
            case reflect.String:
                if i == 0 {
                    query = fmt.Sprintf("%s\"%s\"", query, v.Field(i).String())
                } else {
                    query = fmt.Sprintf("%s, \"%s\"", query, v.Field(i).String())
                }
            default:
                fmt.Println("Unsupported type")
                return
            }
        }
        query = fmt.Sprintf("%s)", query)
        fmt.Println(query)
        return
 
    }
    fmt.Println("unsupported type")
}
 
func main() {
    o := order{
        ordId:      456,
        customerId: 56,
    }
    createQuery(o)
 
    e := employee{
        name:    "Naveen",
        id:      565,
        address: "Coimbatore",
        salary:  90000,
        country: "India",
    }
    createQuery(e)
    i := 90
    createQuery(i)
 
}

playground 上运行

在第 22 行,我们首先检查了传来的参数是否是一个结构体。在第 23 行,我们使用了 Name() 方法,从该结构体的 reflect.Type 获取了结构体的名字。接下来一行,我们用 t 来创建查询。

在第 28 行,case 语句 检查了当前字段是否为 reflect.Int,如果是的话,我们会取到该字段的值,并使用 Int() 方法转换为 int64if else 语句用于处理边界情况。请添加日志来理解为什么需要它。在第 34 行,我们用来相同的逻辑来取到 string

我们还作了额外的检查,以防止 createQuery 函数传入不支持的类型时,程序发生崩溃。程序的其他代码是自解释性的。我建议你在合适的地方添加日志,检查输出,来更好地理解这个程序。

该程序会输出:

insert into order values(456, 56)
insert into employee values("Naveen", 565, "Coimbatore", 90000, "India")
unsupported type

至于向输出的查询中添加字段名,我们把它留给读者作为练习。请尝试着修改程序,打印出以下格式的查询。

insert into order(ordId, customerId) values(456, 56)

我们应该使用反射吗?

我们已经展示了反射的实际应用,现在考虑一个很现实的问题。我们应该使用反射吗?我想引用 Rob Pike 关于使用反射的格言,来回答这个问题。

清晰优于聪明。而反射并不是一目了然的。

反射是 Go 语言中非常强大和高级的概念,我们应该小心谨慎地使用它。使用反射编写清晰和可维护的代码是十分困难的。你应该尽可能避免使用它,只在必须用到它时,才使用反射

本教程到此结束。希望你们喜欢。祝你愉快。

posted @ 2024-01-23 10:37  易先讯  阅读(3)  评论(0编辑  收藏  举报