Go语言系列——自定义错误、panic和recover、函数是一等公民(头等函数)、反射、读取文件、写入文件
31-自定义错误
使用 New 函数创建自定义错误
创建自定义错误最简单的方法是使用 [errors
]包中的 [New
]函数。
在使用 New [函数]创建自定义错误之前,我们先来看看 New
是如何实现的。如下所示,是 [errors
包]中的 New
函数的实现。
// Package errors implements functions to manipulate errors.
package errors
// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
New
函数的实现很简单。errorString
是一个[结构体]类型,只有一个字符串字段 s
。第 14 行使用了 errorString
指针接受者(Pointer Receiver),来实现 error
接口的 Error() string
[方法]。
第 5 行的 New
函数有一个字符串参数,通过这个参数创建了 errorString
类型的变量,并返回了它的地址。于是它就创建并返回了一个新的错误。
现在我们已经知道了 New
函数是如何工作的,我们开始在程序里使用 New
来创建自定义错误吧。
我们将创建一个计算圆半径的简单程序,如果半径为负,它会返回一个错误。
package main
import (
"errors"
"fmt"
"math"
)
func circleArea(radius float64) (float64, error) {
if radius < 0 {
return 0, errors.New("Area calculation failed, radius is less than zero")
}
return math.Pi * radius * radius, nil
}
func main() {
radius := -20.0
area, err := circleArea(radius)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Area of circle %0.2f", area)
}
在上面的程序中,我们检查半径是否小于零(第 10 行)。如果半径小于零,我们会返回等于 0 的面积,以及相应的错误信息。如果半径大于零,则会计算出面积,并返回值为 nil
的错误(第 13 行)。
在 main
函数里,我们在第 19 行检查错误是否等于 nil
。如果不是 nil
,我们会打印出错误并返回,否则我们会打印出圆的面积。
在我们的程序中,半径小于零,因此打印出:
Area calculation failed, radius is less than zero
使用 Errorf 给错误添加更多信息
上面的程序效果不错,但是如果我们能够打印出当前圆的半径,那就更好了。这就要用到 [fmt
]包中的 [Errorf
] 函数了。Errorf
函数会根据格式说明符,规定错误的格式,并返回一个符合该错误的[字符串]。
接下来我们使用 Errorf
函数来改进我们的程序。
package main
import (
"fmt"
"math"
)
func circleArea(radius float64) (float64, error) {
if radius < 0 {
return 0, fmt.Errorf("Area calculation failed, radius %0.2f is less than zero", radius)
}
return math.Pi * radius * radius, nil
}
func main() {
radius := -20.0
area, err := circleArea(radius)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Area of circle %0.2f", area)
}
在上面的程序中,我们使用 Errorf
(第 10 行)打印了发生错误的半径。程序运行后会输出:
Area calculation failed, radius -20.00 is less than zero
使用结构体类型和字段提供错误的更多信息
错误还可以用实现了 error
[接口]的结构体来表示。这种方式可以更加灵活地处理错误。在上面例子中,如果我们希望访问引发错误的半径,现在唯一的方法就是解析错误的描述信息 Area calculation failed, radius -20.00 is less than zero
。这样做不太好,因为一旦描述信息发生变化,程序就会出错。
我们会使用标准库里采用的方法,在上一教程中“断言底层结构体类型,使用结构体字段获取更多信息”这一节,我们讲解了这一方法,可以使用结构体字段来访问引发错误的半径。我们会创建一个实现 error
接口的结构体类型,并使用它的字段来提供关于错误的更多信息。
第一步就是创建一个表示错误的结构体类型。错误类型的命名约定是名称以 Error
结尾。因此我们不妨把结构体类型命名为 areaError
。
type areaError struct {
err string
radius float64
}
上面的结构体类型有一个 radius
字段,它存储了与错误有关的半径,而 err
字段存储了实际的错误信息。
下一步是实现 error
接口。
func (e *areaError) Error() string {
return fmt.Sprintf("radius %0.2f: %s", e.radius, e.err)
}
在上面的代码中,我们使用指针接收者 *areaError
,实现了 error
接口的 Error() string
方法。该方法打印出半径和关于错误的描述。
现在我们来编写 main
函数和 circleArea
函数来完成整个程序。
package main
import (
"fmt"
"math"
)
type areaError struct {
err string
radius float64
}
func (e *areaError) Error() string {
return fmt.Sprintf("radius %0.2f: %s", e.radius, e.err)
}
func circleArea(radius float64) (float64, error) {
if radius < 0 {
return 0, &areaError{"radius is negative", radius}
}
return math.Pi * radius * radius, nil
}
func main() {
radius := -20.0
area, err := circleArea(radius)
if err != nil {
if err, ok := err.(*areaError); ok {
fmt.Printf("Radius %0.2f is less than zero", err.radius)
return
}
fmt.Println(err)
return
}
fmt.Printf("Area of rectangle1 %0.2f", area)
}
在 playground 上运行
在上面的程序中,circleArea
(第 17 行)用于计算圆的面积。该函数首先检查半径是否小于零,如果小于零,它会通过错误半径和对应错误信息,创建一个 areaError
类型的值,然后返回 areaError
值的地址,与此同时 area
等于 0(第 19 行)。于是我们提供了更多的错误信息(即导致错误的半径),我们使用了自定义错误的结构体字段来定义它。
如果半径是非负数,该函数会在第 21 行计算并返回面积,同时错误值为 nil
。
在 main
函数的 26 行,我们试图计算半径为 -20 的圆的面积。由于半径小于零,因此会导致一个错误。
我们在第 27 行检查了错误是否为 nil
,并在下一行断言了 *areaError
类型。*如果错误是 areaError 类型,我们就可以用 err.radius 来获取错误的半径(第 29 行),打印出自定义错误的消息,最后程序返回退出。
如果断言错误,我们就在第 32 行打印该错误,并返回。如果没有发生错误,在第 35 行会打印出面积。
该程序会输出:
Radius -20.00 is less than zero
下面我们来使用上一教程提到的[第二种方法],使用自定义错误类型的方法来提供错误的更多信息。
使用结构体类型的方法来提供错误的更多信息
在本节里,我们会编写一个计算矩形面积的程序。如果长或宽小于零,程序就会打印出错误。
第一步就是创建一个表示错误的结构体。
type areaError struct {
err string //error description
length float64 //length which caused the error
width float64 //width which caused the error
}
上面的结构体类型除了有一个错误描述字段,还有可能引发错误的宽和高。
现在我们有了错误类型,我们来实现 error
接口,并给该错误类型添加两个方法,使它提供了更多的错误信息。
func (e *areaError) Error() string {
return e.err
}
func (e *areaError) lengthNegative() bool {
return e.length < 0
}
func (e *areaError) widthNegative() bool {
return e.width < 0
}
在上面的代码片段中,我们从 Error() string
方法中返回了关于错误的描述。当 length
小于零时,lengthNegative() bool
方法返回 true
,而当 width
小于零时,widthNegative() bool
方法返回 true
。这两个方法都提供了关于错误的更多信息,在这里,它提示我们计算面积失败的原因(长度为负数或者宽度为负数)。于是我们就有了两个错误类型结构体的方法,来提供更多的错误信息。
下一步就是编写计算面积的函数。
func rectArea(length, width float64) (float64, error) {
err := ""
if length < 0 {
err += "length is less than zero"
}
if width < 0 {
if err == "" {
err = "width is less than zero"
} else {
err += ", width is less than zero"
}
}
if err != "" {
return 0, &areaError{err, length, width}
}
return length * width, nil
}
上面的 rectArea
函数检查了长或宽是否小于零,如果小于零,rectArea
会返回一个错误信息,否则 rectArea
会返回矩形的面积和一个值为 nil
的错误。
让我们创建 main
函数来完成整个程序。
func main() {
length, width := -5.0, -9.0
area, err := rectArea(length, width)
if err != nil {
if err, ok := err.(*areaError); ok {
if err.lengthNegative() {
fmt.Printf("error: length %0.2f is less than zero\n", err.length)
}
if err.widthNegative() {
fmt.Printf("error: width %0.2f is less than zero\n", err.width)
}
return
}
fmt.Println(err)
return
}
fmt.Println("area of rect", area)
}
在 main
程序中,我们检查了错误是否为 nil
(第 4 行)。如果错误值不是 nil
,我们会在下一行断言 *areaError
类型。然后,我们使用 lengthNegative()
和 widthNegative()
方法,检查错误的原因是长度小于零还是宽度小于零。这样我们就使用了错误结构体类型的方法,来提供更多的错误信息。
如果没有错误发生,就会打印矩形的面积。
下面是整个程序的代码供你参考。
package main
import "fmt"
type areaError struct {
err string //error description
length float64 //length which caused the error
width float64 //width which caused the error
}
func (e *areaError) Error() string {
return e.err
}
func (e *areaError) lengthNegative() bool {
return e.length < 0
}
func (e *areaError) widthNegative() bool {
return e.width < 0
}
func rectArea(length, width float64) (float64, error) {
err := ""
if length < 0 {
err += "length is less than zero"
}
if width < 0 {
if err == "" {
err = "width is less than zero"
} else {
err += ", width is less than zero"
}
}
if err != "" {
return 0, &areaError{err, length, width}
}
return length * width, nil
}
func main() {
length, width := -5.0, -9.0
area, err := rectArea(length, width)
if err != nil {
if err, ok := err.(*areaError); ok {
if err.lengthNegative() {
fmt.Printf("error: length %0.2f is less than zero\n", err.length)
}
if err.widthNegative() {
fmt.Printf("error: width %0.2f is less than zero\n", err.width)
}
return
}
fmt.Println(err)
return
}
fmt.Println("area of rect", area)
}
该程序会打印输出:
error: length -5.00 is less than zero
error: width -9.00 is less than zero
在上一教程[错误处理]中,我们介绍了三种提供更多错误信息的方法,现在我们已经看了其中两个示例。
第三种方法使用的是直接比较,比较简单。我留给读者作为练习,你们可以试着使用这种方法来给出自定义错误的更多信息。
本教程到此结束。
简单概括一下本教程讨论的内容:
- 使用
New
函数创建自定义错误 - 使用
Error
添加更多错误信息 - 使用结构体类型和字段,提供更多错误信息
- 使用结构体类型和方法,提供更多错误信息
32-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 有两个合理的用例。
- 发生了一个不能恢复的错误,此时程序不能继续运行。 一个例子就是 web 服务器无法绑定所要求的端口。在这种情况下,就应该使用 panic,因为如果不能绑定端口,啥也做不了。
- 发生了一个编程上的错误。 假如我们有一个接收指针参数的方法,而其他人使用
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")
}
上面的程序很简单,会打印一个人的全名。第 7 行的 fullName
函数会打印出一个人的全名。该函数在第 8 行和第 11 行分别检查了 firstName
和 lastName
的指针是否为 nil
。如果是 nil
,fullName
函数会调用含有不同的错误信息的 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")
}
上述代码中,我们只修改了两处,分别在第 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 时,首先执行了延迟函数,接着控制返回到函数调用方,调用方的延迟函数继续运行,直到到达顶层调用函数。
在我们的例子中,首先执行 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")
}
在第 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")
}
在上面的程序中,函数 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")
}
在上面的程序中,第 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")
}
运行上面程序会输出:
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")
}
在上面的程序中,我们在第 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
- 恢复后获得堆栈跟踪
33-函数是一等公民(头等函数)
现在简单概括一下本教程讨论的内容:
- 什么是头等函数?
- 匿名函数
- 用户自定义的函数类型
- 高阶函数
把函数作为参数,传递给其它函数
在其它函数中返回函数 - 闭包
- 头等函数的实际用途
什么是头等函数?
支持头等函数(First Class Function)的编程语言,可以把函数赋值给变量,也可以把函数作为其它函数的参数或者返回值。Go 语言支持头等函数的机制。
本教程我们会讨论头等函数的语法和用例。
匿名函数
我们来编写一个简单的示例,把函数赋值给一个变量。
package main
import (
"fmt"
)
func main() {
a := func() {
fmt.Println("hello world first class function")
}
a()
fmt.Printf("%T", a)
}
在上面的程序中,我们将一个函数赋值给了变量 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")
}()
}
在上面的程序中,第 8 行定义了一个匿名函数,并在定义之后,我们使用 ()
立即调用了该函数(第 10 行)。该程序会输出:
hello world first class function
就像其它函数一样,还可以向匿名函数传递参数。
package main
import (
"fmt"
)
func main() {
func(n string) {
fmt.Println("Welcome", n)
}("Gophers")
}
在上面的程序中,我们向匿名函数传递了一个字符串参数(第 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)
}
在上面程序的第 10 行,我们定义了一个 add
类型的变量 a
,并向它赋值了一个符合 add
类型签名的函数。我们在第 13 行调用了该函数,并将结果赋值给 s
。该程序会输出:
Sum 11
高阶函数
高阶函数(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)
}
在上面的实例中,第 7 行我们定义了一个函数 simple
,simple
接收一个函数参数(该函数接收两个 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))
}
在上面程序中,第 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)
}()
}
在上面的程序中,匿名函数在第 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("!"))
}
在上面程序中,函数 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)
}
在 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)
}
该程序会输出:
[25 30 35 40 45]
34-反射
反射是 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)
}
在上面的程序中,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)
}
在上面的程序中,我们需要编写一个函数,接收结构体变量 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))
}
在第 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)
}
在上面的程序中,第 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)
}
上述程序会输出:
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)
}
在上面的程序中,因为 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)
}
在上面程序中的第 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)
}
在第 22 行,我们首先检查了传来的参数是否是一个结构体。在第 23 行,我们使用了 Name()
方法,从该结构体的 reflect.Type
获取了结构体的名字。接下来一行,我们用 t
来创建查询。
在第 28 行,case 语句
检查了当前字段是否为 reflect.Int
,如果是的话,我们会取到该字段的值,并使用 Int()
方法转换为 int64
。if 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 语言中非常强大和高级的概念,我们应该小心谨慎地使用它。使用反射编写清晰和可维护的代码是十分困难的。你应该尽可能避免使用它,只在必须用到它时,才使用反射。
35-读取文件
文件读取是所有编程语言中最常见的操作之一。本教程我们会学习如何使用 Go 读取文件。
本教程分为如下小节。
- 将整个文件读取到内存
使用绝对文件路径
使用命令行标记来传递文件路径
将文件绑定在二进制文件中 - 分块读取文件
- 逐行读取文件
将整个文件读取到内存
将整个文件读取到内存是最基本的文件操作之一。这需要使用 ioutil
]包中的 ReadFile 函数。
让我们在 Go 程序所在的目录中,读取一个文件。我已经在 GOPATH(译注:原文是 GOROOT,应该是笔误)中创建了文件夹,在该文件夹内部,有一个文本文件 test.txt
,我们会使用 Go 程序 filehandling.go
来读取它。test.txt
包含文本 “Hello World. Welcome to file handling in Go”。我的文件夹结构如下:
src
filehandling
filehandling.go
test.txt
接下来我们来看看代码。
package main
import (
"fmt"
"io/ioutil"
)
func main() {
data, err := ioutil.ReadFile("test.txt")
if err != nil {
fmt.Println("File reading error", err)
return
}
fmt.Println("Contents of file:", string(data))
}
由于无法在 playground 上读取文件,因此请在你的本地环境运行这个程序。
在上述程序的第 9 行,程序会读取文件,并返回一个字节切片,而这个切片保存在 data
中。在第 14 行,我们将 data
转换为 string
,显示出文件的内容。
请在 test.txt 所在的位置运行该程序。
例如,对于 linux/mac,如果 test.txt 位于 /home/naveen/go/src/filehandling,可以使用下列步骤来运行程序。
$ cd /home/naveen/go/src/filehandling/
$ go install filehandling
$ workspacepath/bin/filehandling
对于 windows,如果 test.txt 位于 C:\Users\naveen.r\go\src\filehandling,则使用下列步骤。
> cd C:\Users\naveen.r\go\src\filehandling
> go install filehandling
> workspacepath\bin\filehandling.exe
该程序会输出:
Contents of file: Hello World. Welcome to file handling in Go.
如果在其他位置运行这个程序(比如 /home/userdirectory
),会打印下面的错误。
File reading error open test.txt: The system cannot find the file specified.
这是因为 Go 是编译型语言。go install
会根据源代码创建一个二进制文件。二进制文件独立于源代码,可以在任何位置上运行。由于在运行二进制文件的位置上没有找到 test.txt
,因此程序会报错,提示无法找到指定的文件。
有三种方法可以解决这个问题。
- 使用绝对文件路径
- 使用命令行标记来传递文件路径
- 将文件绑定在二进制文件中
让我们来依次介绍。
1. 使用绝对文件路径
要解决问题,最简单的方法就是传入绝对文件路径。我已经修改了程序,把路径改成了绝对路径。
package main
import (
"fmt"
"io/ioutil"
)
func main() {
data, err := ioutil.ReadFile("/home/naveen/go/src/filehandling/test.txt")
if err != nil {
fmt.Println("File reading error", err)
return
}
fmt.Println("Contents of file:", string(data))
}
现在可以在任何位置上运行程序,打印出 test.txt
的内容。
例如,可以在我的家目录运行。
$ cd $HOME
$ go install filehandling
$ workspacepath/bin/filehandling
该程序打印出了 test.txt
的内容。
看似这是一个简单的方法,但它的缺点是:文件必须放在程序指定的路径中,否则就会出错。
2. 使用命令行标记来传递文件路径
另一种解决方案是使用命令行标记来传递文件路径。使用 flag 包,我们可以从输入的命令行获取到文件路径,接着读取文件内容。
首先我们来看看 flag
包是如何工作的。flag
包有一个名为 String 的函数。该函数接收三个参数。第一个参数是标记名,第二个是默认值,第三个是标记的简短描述。
让我们来编写程序,从命令行读取文件名。将 filehandling.go
的内容替换如下:
package main
import (
"flag"
"fmt"
)
func main() {
fptr := flag.String("fpath", "test.txt", "file path to read from")
flag.Parse()
fmt.Println("value of fpath is", *fptr)
}
在上述程序中第 8 行,通过 String
函数,创建了一个字符串标记,名称是 fpath
,默认值是 test.txt
,描述为 file path to read from
。这个函数返回存储 flag 值的字符串变量的地址。
在程序访问 flag 之前,必须先调用 flag.Parse()
。
在第 10 行,程序会打印出 flag 值。
使用下面命令运行程序。
wrkspacepath/bin/filehandling -fpath=/path-of-file/test.txt
我们传入 /path-of-file/test.txt
,赋值给了 fpath
标记。
该程序输出:
value of fpath is /path-of-file/test.txt
这是因为 fpath
的默认值是 test.txt
。
现在我们知道如何从命令行读取文件路径了,让我们继续完成我们的文件读取程序。
package main
import (
"flag"
"fmt"
"io/ioutil"
)
func main() {
fptr := flag.String("fpath", "test.txt", "file path to read from")
flag.Parse()
data, err := ioutil.ReadFile(*fptr)
if err != nil {
fmt.Println("File reading error", err)
return
}
fmt.Println("Contents of file:", string(data))
}
在上述程序里,命令行传入文件路径,程序读取了该文件的内容。使用下面命令运行该程序。
wrkspacepath/bin/filehandling -fpath=/path-of-file/test.txt
请将 /path-of-file/
替换为 test.txt
的真实路径。该程序将打印:
Contents of file: Hello World. Welcome to file handling in Go.
3. 将文件绑定在二进制文件中
虽然从命令行获取文件路径的方法很好,但还有一种更好的解决方法。如果我们能够将文本文件捆绑在二进制文件,岂不是很棒?这就是我们下面要做的事情。
有很多包可以帮助我们实现。我们会使用 packr,因为它很简单,并且我在项目中使用它时,没有出现任何问题。
第一步就是安装 packr
包。
在命令提示符中输入下面命令,安装 packr
包。
go get -u github.com/gobuffalo/packr/...
packr
会把静态文件(例如 .txt
文件)转换为 .go
文件,接下来,.go
文件会直接嵌入到二进制文件中。packer
非常智能,在开发过程中,可以从磁盘而非二进制文件中获取静态文件。在开发过程中,当仅仅静态文件变化时,可以不必重新编译。
我们通过程序来更好地理解它。用以下内容来替换 handling.go
文件。
package main
import (
"fmt"
"github.com/gobuffalo/packr"
)
func main() {
box := packr.NewBox("../filehandling")
data := box.String("test.txt")
fmt.Println("Contents of file:", data)
}
在上面程序的第 10 行,我们创建了一个新盒子(New Box)。盒子表示一个文件夹,其内容会嵌入到二进制中。在这里,我指定了 filehandling
文件夹,其内容包含 test.txt
。在下一行,我们读取了文件内容,并打印出来。
在开发阶段时,我们可以使用 go install
命令来运行程序。程序可以正常运行。packr
非常智能,在开发阶段可以从磁盘加载文件。
使用下面命令来运行程序。
go install filehandling
workspacepath/bin/filehandling
该命令可以在其他位置运行。packr
很聪明,可以获取传递给 NewBox
命令的目录的绝对路径。
该程序会输出:
Contents of file: Hello World. Welcome to file handling in Go.
你可以试着改变 test.txt
的内容,然后再运行 filehandling
。可以看到,无需再次编译,程序打印出了 test.txt
的更新内容。完美!
现在我们来看看如何将 test.txt
打包到我们的二进制文件中。我们使用 packr
命令来实现。
运行下面的命令:
packr install -v filehandling
它会打印:
building box ../filehandling
packing file filehandling.go
packed file filehandling.go
packing file test.txt
packed file test.txt
built box ../filehandling with ["filehandling.go" "test.txt"]
filehandling
该命令将静态文件绑定到了二进制文件中。
在运行上述命令之后,使用命令 workspacepath/bin/filehandling
来运行程序。程序会打印出 test.txt
的内容。于是从二进制文件中,我们读取了 test.txt
的内容。
如果你不知道文件到底是由二进制还是磁盘来提供,我建议你删除 test.txt
,并在此运行 filehandling
命令。你将看到,程序打印出了 test.txt
的内容。太棒了:D。我们已经成功将静态文件嵌入到了二进制文件中。
分块读取文件
在前面的章节,我们学习了如何把整个文件读取到内存。当文件非常大时,尤其在 RAM 存储量不足的情况下,把整个文件都读入内存是没有意义的。更好的方法是分块读取文件。这可以使用 bufio 包来完成。
让我们来编写一个程序,以 3 个字节的块为单位读取 test.txt
文件。如下所示,替换 filehandling.go
的内容。
package main
import (
"bufio"
"flag"
"fmt"
"log"
"os"
)
func main() {
fptr := flag.String("fpath", "test.txt", "file path to read from")
flag.Parse()
f, err := os.Open(*fptr)
if err != nil {
log.Fatal(err)
}
defer func() {
if err = f.Close(); err != nil {
log.Fatal(err)
}
}()
r := bufio.NewReader(f)
b := make([]byte, 3)
for {
_, err := r.Read(b)
if err != nil {
fmt.Println("Error reading file:", err)
break
}
fmt.Println(string(b))
}
}
在上述程序的第 15 行,我们使用命令行标记传递的路径,打开文件。
在第 19 行,我们延迟了文件的关闭操作。
在上面程序的第 24 行,我们新建了一个缓冲读取器(buffered reader)。在下一行,我们创建了长度和容量为 3 的字节切片,程序会把文件的字节读取到切片中。
第 27 行的 Read
方法会读取 len(b) 个字节(达到 3 字节),并返回所读取的字节数。当到达文件最后时,它会返回一个 EOF 错误。程序的其他地方比较简单,不做解释。
如果我们使用下面命令来运行程序:
$ go install filehandling
$ wrkspacepath/bin/filehandling -fpath=/path-of-file/test.txt
会得到以下输出:
Hel
lo
Wor
ld.
We
lco
me
to
fil
e h
and
lin
g i
n G
o.
Error reading file: EOF
逐行读取文件
本节我们讨论如何使用 Go 逐行读取文件。这可以使用 bufio 来实现。
请将 test.txt
替换为以下内容。
Hello World. Welcome to file handling in Go.
This is the second line of the file.
We have reached the end of the file.
逐行读取文件涉及到以下步骤。
- 打开文件;
- 在文件上新建一个 scanner;
- 扫描文件并且逐行读取。
将 filehandling.go
替换为以下内容。
package main
import (
"bufio"
"flag"
"fmt"
"log"
"os"
)
func main() {
fptr := flag.String("fpath", "test.txt", "file path to read from")
flag.Parse()
f, err := os.Open(*fptr)
if err != nil {
log.Fatal(err)
}
defer func() {
if err = f.Close(); err != nil {
log.Fatal(err)
}
}()
s := bufio.NewScanner(f)
for s.Scan() {
fmt.Println(s.Text())
}
err = s.Err()
if err != nil {
log.Fatal(err)
}
}
在上述程序的第 15 行,我们用命令行标记传入的路径,打开文件。在第 24 行,我们用文件创建了一个新的 scanner。第 25 行的 Scan()
方法读取文件的下一行,如果可以读取,就可以使用 Text()
方法。
当 Scan
返回 false 时,除非已经到达文件末尾(此时 Err()
返回 nil
),否则 Err()
就会返回扫描过程中出现的错误。
如果我使用下面命令来运行程序:
$ go install filehandling
$ workspacepath/bin/filehandling -fpath=/path-of-file/test.txt
程序会输出:
Hello World. Welcome to file handling in Go.
This is the second line of the file.
We have reached the end of the file.
36-写入文件
将字符串写入文件
最常见的写文件就是将字符串写入文件。这个写起来非常的简单。这个包含以下几个阶段。
- 创建文件
- 将字符串写入文件
我们将得到如下代码。
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Create("test.txt")
if err != nil {
fmt.Println(err)
return
}
l, err := f.WriteString("Hello World")
if err != nil {
fmt.Println(err)
f.Close()
return
}
fmt.Println(l, "bytes written successfully")
err = f.Close()
if err != nil {
fmt.Println(err)
return
}
}
在第 9 行使用 create
创建一个名字为 test.txt
的文件。如果这个文件已经存在,那么 create
函数将截断这个文件。该函数返回一个文件描述符。
在第 14 行,我们使用 WriteString
将字符串 Hello World 写入到文件里面。这个方法将返回相应写入的字节数,如果有错误则返回错误。
最后,在第 21 行我们将文件关闭。
上面程序将打印:
11 bytes written successfully
运行完成之后你会在程序运行的目录下发现创建了一个 test.txt 的文件。如果你使用文本编辑器打开这个文件,你可以看到文件里面有一个 Hello World 的字符串。
将字节写入文件
将字节写入文件和写入字符串非常的类似。我们将使用 Write]方法将字节写入到文件。下面的程序将一个字节的切片写入文件。
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Create("/home/naveen/bytes")
if err != nil {
fmt.Println(err)
return
}
d2 := []byte{104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100}
n2, err := f.Write(d2)
if err != nil {
fmt.Println(err)
f.Close()
return
}
fmt.Println(n2, "bytes written successfully")
err = f.Close()
if err != nil {
fmt.Println(err)
return
}
}
在上面的程序中,第 15 行使用了 Write 方法将字节切片写入到 bytes
这个文件里。这个文本在目录 /home/naveen
里面。你也可以将这个目录换成其他的目录。剩余的程序自带解释。如果执行成功,这个程序将打印 11 bytes written successfully
。并且创建一个 bytes
的文件。打开文件,你会发现该文件包含了文本 hello bytes。
将字符串一行一行的写入文件
另外一个常用的操作就是将字符串一行一行的写入到文件。这一部分我们将写一个程序,该程序创建并写入如下内容到文件里。
Welcome to the world of Go.
Go is a compiled language.
It is easy to learn Go.
让我们看下面的代码:
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Create("lines")
if err != nil {
fmt.Println(err)
f.Close()
return
}
d := []string{"Welcome to the world of Go1.", "Go is a compiled language.",
"It is easy to learn Go."}
for _, v := range d {
fmt.Fprintln(f, v)
if err != nil {
fmt.Println(err)
return
}
}
err = f.Close()
if err != nil {
fmt.Println(err)
return
}
fmt.Println("file written successfully")
}
在上面程序的第 9 行,我们先创建一个名字叫做 lines 的文件。在第 17 行,我们用迭代并使用 for rang
循环这个数组,并使用 Fprintln,Fprintln 函数 将 io.writer
做为参数,并且添加一个新的行,这个正是我们想要的。如果执行成功将打印 file written successfully
,并且在当前目录将创建一个 lines
的文件。lines
这个文件的内容如下所示:
Welcome to the world of Go1.
Go is a compiled language.
It is easy to learn Go.
追加到文件
这一部分我们将追加一行到上节创建的 lines
文件中。我们将追加 File handling is easy 到 lines
这个文件。
这个文件将以追加和写的方式打开。这些标志将通过 Open方法实现。当文件以追加的方式打开,我们添加新的行到文件里。
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.OpenFile("lines", os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
fmt.Println(err)
return
}
newLine := "File handling is easy."
_, err = fmt.Fprintln(f, newLine)
if err != nil {
fmt.Println(err)
f.Close()
return
}
err = f.Close()
if err != nil {
fmt.Println(err)
return
}
fmt.Println("file appended successfully")
}
在上面程序的第 9 行,我们以写的方式打开文件并将一行添加到文件里。当成功打开文件之后,在程序第 15 行,我们添加一行到文件里。程序成功将打印 file appended successfully
。运行程序,新的行就加到文件里面去了。
Welcome to the world of Go1.
Go is a compiled language.
It is easy to learn Go.
File handling is easy.
并发写文件
当多个 goroutines 同时(并发)写文件时,我们会遇到竞争条件(race condition)。因此,当发生同步写的时候需要一个 channel 作为一致写入的条件。
我们将写一个程序,该程序创建 100 个 goroutinues。每个 goroutinue 将并发产生一个随机数,届时将有 100 个随机数产生。这些随机数将被写入到文件里面。我们将用下面的方法解决这个问题 .
- 创建一个 channel 用来读和写这个随机数。
- 创建 100 个生产者 goroutine。每个 goroutine 将产生随机数并将随机数写入到 channel 里。
- 创建一个消费者 goroutine 用来从 channel 读取随机数并将它写入文件。这样的话我们就只有一个 goroutinue 向文件中写数据,从而避免竞争条件。
- 一旦完成则关闭文件。
我们开始写产生随机数的 produce
函数:
func produce(data chan int, wg *sync.WaitGroup) {
n := rand.Intn(999)
data <- n
wg.Done()
}
上面的方法产生随机数并且将 data
写入到 channel 中,之后通过调用 waitGroup
的 Done
方法来通知任务已经完成。
让我们看看将数据写到文件的函数:
func consume(data chan int, done chan bool) {
f, err := os.Create("concurrent")
if err != nil {
fmt.Println(err)
return
}
for d := range data {
_, err = fmt.Fprintln(f, d)
if err != nil {
fmt.Println(err)
f.Close()
done <- false
return
}
}
err = f.Close()
if err != nil {
fmt.Println(err)
done <- false
return
}
done <- true
}
这个 consume
的函数创建了一个名为 concurrent
的文件。然后从 channel 中读取随机数并且写到文件中。一旦读取完成并且将随机数写入文件后,通过往 done
这个 cahnnel 中写入 true
来通知任务已完成。
下面我们写 main
函数,并完成这个程序。下面是我提供的完整程序:
package main
import (
"fmt"
"math/rand"
"os"
"sync"
)
func produce(data chan int, wg *sync.WaitGroup) {
n := rand.Intn(999)
data <- n
wg.Done()
}
func consume(data chan int, done chan bool) {
f, err := os.Create("concurrent")
if err != nil {
fmt.Println(err)
return
}
for d := range data {
_, err = fmt.Fprintln(f, d)
if err != nil {
fmt.Println(err)
f.Close()
done <- false
return
}
}
err = f.Close()
if err != nil {
fmt.Println(err)
done <- false
return
}
done <- true
}
func main() {
data := make(chan int)
done := make(chan bool)
wg := sync.WaitGroup{}
for i := 0; i < 100; i++ {
wg.Add(1)
go produce(data, &wg)
}
go consume(data, done)
go func() {
wg.Wait()
close(data)
}()
d := <-done
if d == true {
fmt.Println("File written successfully")
} else {
fmt.Println("File writing failed")
}
}
main
函数在第 41 行创建写入和读取数据的 channel,在第 42 行创建 done
这个 channel,此 channel 用于消费者 goroutinue 完成任务之后通知 main
函数。第 43 行创建 Waitgroup 的实例 wg
,用于等待所有生产随机数的 goroutine 完成任务。
在第 44 行使用 for
循环创建 100 个 goroutines。在第 49 行调用 waitgroup 的 wait()
方法等待所有的 goroutines 完成随机数的生成。然后关闭 channel。当 channel 关闭时,消费者 consume
goroutine 已经将所有的随机数写入文件,在第 37 行 将 true
写入 done
这个 channel 中,这个时候 main
函数解除阻塞并且打印 File written successfully
。
本文来自博客园,作者:码诅,转载请注明原文链接:https://www.cnblogs.com/zdwzdwzdw/p/17487860.html