十四、错误处理与测试
Go 没有像 Java 和 .NET 那样的 try/catch
异常机制:不能执行抛异常操作。但是有一套 defer-panic-and-recover
机制
Go 的设计者觉得 try/catch
机制的使用太泛滥了,而且从底层向更高的层级抛异常太耗费资源。他们给 Go 设计的机制也可以“捕捉”异常,但是更轻量,并且只应该作为(处理错误的)最后的手段。
Go 是怎么处理普通错误的呢?通过在函数和方法中返回错误对象作为它们的唯一或最后一个返回值——如果返回 nil
,则没有错误发生——并且主调 (calling) 函数总是应该检查收到的错误。
永远不要忽略错误,否则可能会导致程序崩溃!!
panic()
和 recover()
是用来处理真正的异常(无法预测的错误)而不是普通的错误。
库函数通常必须返回某种错误提示给主调函数。
Go 检查和报告错误条件的惯有方式:
-
产生错误的函数会返回两个变量,一个值和一个错误码;如果后者是
nil
就是成功,非nil
就是发生了错误。 -
为了防止发生错误时正在执行的函数(如果有必要的话甚至会是整个程序)被中止,在调用函数后必须检查错误。
if value, err := pack1.Func1(param1); err != nil {
fmt.Printf("Error %s in pack1.Func1 with parameter %v", err.Error(), param1)
return // or: return err
} else {
// Process(value)
}
为了更清晰的代码,应该总是使用包含错误值变量的 if 复合语句
如果程序中止也没关系的话甚至可以使用 panic()
13.1 错误处理
Go 有一个预先定义的 error 接口类型
type error interface {
Error() string
}
错误值用来表示异常状态;errors
包中有一个 errorString
结构体实现了 error
接口。当程序处于错误状态时可以用 os.Exit(1)
来中止运行。
13.2 定义错误
任何时候当需要一个新的错误类型,都可以用 errors
包(必须先 import
)的 errors.New()
函数接收合适的错误信息来创建,像下面这样:
err := errors.New("math - square root of negative number")
通常(错误信息)都会有像 Error:...
这样的前缀,所以你的错误信息不要以大写字母开头(注:英文只有句首单词首字母大写,这里应当是考虑到这一点)。
在大部分情况下自定义错误结构类型很有意义的,可以包含除了(低层级的)错误信息以外的其它有用信息,例如,正在进行的操作(打开文件等),全路径或名字。看下面例子中 os.Open()
操作触发的 PathError
错误:
// PathError records an error and the operation and file path that caused it.
type PathError struct {
Op string // "open", "unlink", etc.
Path string // The associated file.
Err error // Returned by the system call.
}
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": "+ e.Err.Error()
}
如果有不同错误条件可能发生,那么对实际的错误使用类型断言或类型判断(type-switch)是很有用的,并且可以根据错误场景做一些补救和恢复操作。
// err != nil
if e, ok := err.(*os.PathError); ok {
// remedy situation
}
或:
switch err := err.(type) {
case ParseError:
PrintParseError(err)
case PathError:
PrintPathError(err)
...
default:
fmt.Printf("Not a special error, just %s\n", err)
}
包也可以用额外的方法 (methods)定义特定的错误,比如 net.Error
:
package net
type Error interface {
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}
所有的例子都遵循同一种命名规范:错误类型以 ...Error
结尾,错误变量以 err...
或 Err...
开头或者直接叫 err
或 Err
。
syscall
是低阶外部包,用来提供系统基本调用的原始接口。它们返回封装整数类型错误码的 syscall.Errno
;类型 syscall.Errno
实现了 Error
接口。
大部分 syscall
函数都返回一个结果和可能的错误,比如:
r, err := syscall.Open(name, mode, perm)
if err != nil {
fmt.Println(err.Error())
}
os
包也提供了一套像 os.EINAL
这样的标准错误,它们基于 syscall
错误:
var (
EPERM Error = Errno(syscall.EPERM)
ENOENT Error = Errno(syscall.ENOENT)
ESRCH Error = Errno(syscall.ESRCH)
EINTR Error = Errno(syscall.EINTR)
EIO Error = Errno(syscall.EIO)
...
)
13.2.1 用 fmt 创建错误对象
通常想要返回包含错误参数的更有信息量的字符串,例如:可以用 fmt.Errorf()
来实现:它和 fmt.Printf()
完全一样,接收一个或多个格式占位符的格式化字符串和相应数量的占位变量。和打印信息不同的是它用信息生成错误对象。
比如在前面的平方根例子中使用:
if f < 0 {
return 0, fmt.Errorf("math: square root of negative number %g", f)
}
第二个例子:从命令行读取输入时,如果加了 --help
或 -h
标志,我们可以用有用的信息产生一个错误:
if len(os.Args) > 1 && (os.Args[1] == "-h" || os.Args[1] == "--help") {
err = fmt.Errorf("usage: %s infile.txt outfile.txt", filepath.Base(os.Args[0]))
return
}
13.3 运行时异常和 panic
当发生像数组下标越界或类型断言失败这样的运行错误时,Go 运行时会触发运行时 panic,伴随着程序的崩溃抛出一个 runtime.Error
接口类型的值。这个错误值有个 RuntimeError()
方法用于区别普通错误。
panic()
可以直接从代码初始化:当错误条件(我们所测试的代码)很严苛且不可恢复,程序不能继续运行时,可以使用 panic()
函数产生一个中止程序的运行时错误。panic()
接收一个做任意类型的参数,通常是字符串,在程序死亡时被打印出来。Go 运行时负责中止程序并给出调试信息。
一个检查程序是否被已知用户启动的具体例子:
一个检查程序是否被已知用户启动的具体例子:
var user = os.Getenv("USER")
func check() {
if user == "" {
panic("Unknown user: no value for $USER")
}
}
可以在导入包的 init()
函数中检查这些。
当发生错误必须中止程序时,panic()
可以用于错误处理模式:
if err != nil {
panic("ERROR occurred:" + err.Error())
}
在多层嵌套的函数调用中调用 panic()
,可以马上中止当前函数的执行,所有的 defer
语句都会保证执行并把控制权交还给接收到 panic 的函数调用者。这样向上冒泡直到最顶层,并执行(每层的) defer
,在栈顶处程序崩溃,并在命令行中用传给 panic()
的值报告错误情况:这个终止过程就是 panicking。
标准库中有许多包含 Must
前缀的函数,像 regexp.MustComplie()
和 template.Must()
;当正则表达式或模板中转入的转换字符串导致错误时,这些函数会 panic()
。
不能随意地用 panic()
中止程序,必须尽力补救错误让程序能继续执行。
13.4 从 panic 中恢复 (recover)
recover()
内建函数被用于从 panic 或错误场景中恢复:让程序可以从 panicking 重新获得控制权,停止终止过程进而恢复正常执行。
recover
只能在 defer
修饰的函数中使用:用于取得 panic()
调用中传递过来的错误值,如果是正常执行,调用 recover()
会返回 nil
,且没有其它效果。
总结:panic()
会导致栈被展开直到 defer
修饰的 recover()
被调用或者程序中止。
// protect() 函数调用函数参数 `g` 来保护调用者防止从 `g` 中
// 抛出的运行时 panic,并展示 panic 中的信息
func protect(g func()) {
defer func() {
log.Println("done")
// Println executes normally even if there is a panic
if err := recover(); err != nil {
log.Printf("run time panic: %v", err)
}
}()
log.Println("start")
g() // possible runtime-error
}
defer
-panic()
-recover()
在某种意义上也是一种像 if
,for
这样的控制流机制。
Go 标准库中许多地方都用了这个机制,例如,json
包中的解码和 regexp
包中的 Complie()
函数。Go 库的原则是即使在包的内部使用了 panic()
,在它的对外接口 (API) 中也必须用 recover()
处理成显式返回的错误。
13.5 自定义包中的错误处理和 panicking
这是所有自定义包实现者应该遵守的最佳实践:
1)在包内部,总是应该从 panic 中 recover:不允许显式的超出包范围的 panic()
2)向包的调用者返回错误值(而不是 panic)。
在包内部,特别是在非导出函数中有很深层次的嵌套调用时,将 panic 转换成 error
来告诉调用方为何出错,是很实用的(且提高了代码可读性)。
13.6 一种用闭包处理错误的模式
每当函数返回时,我们应该检查是否有错误发生:但是这会导致重复乏味的代码。结合 defer/panic/recover 机制和闭包可以得到一个我们马上要讨论的更加优雅的模式。**不过这个模式只有当所有的函数都是同一种签名时可用,这样就有相当大的限制。一个很好的使用它的例子是 web 应用,**所有的处理函数都是下面这样:
func handler1(w http.ResponseWriter, r *http.Request) { ... }
假设所有的函数都有这样的签名:
func f(a type1, b type2)
参数的数量和类型是不相关的。
我们给这个类型一个名字:
fType1 = func f(a type1, b type2)
在我们的模式中使用了两个帮助函数:
1)check()
:这是用来检查是否有错误和 panic 发生的函数:
func check(err error) { if err != nil { panic(err) } }
2)errorhandler()
:这是一个包装函数。接收一个 fType1
类型的函数 fn
并返回一个调用 fn
的函数。里面就包含有 defer/recover 机制,这在 13.3 节中有相应描述。
func errorHandler(fn fType1) fType1 {
return func(a type1, b type2) {
defer func() {
if err, ok := recover().(error); ok {
log.Printf("run time panic: %v", err)
}
}()
fn(a, b)
}
}
当错误发生时会 recover 并打印在日志中;除了简单的打印,应用也可以用 template
包(参见 15.7 节)为用户生成自定义的输出。check()
函数会在所有的被调函数中调用,像这样:
func f1(a type1, b type2) {
...
f, _, err := // call function/method
check(err)
t, err := // call function/method
check(err)
_, err2 := // call function/method
check(err2)
...
}
通过这种机制,所有的错误都会被 recover,并且调用函数后的错误检查代码也被简化为调用 check(err)
即可。在这种模式下,不同的错误处理必须对应不同的函数类型;它们(错误处理)可能被隐藏在错误处理包内部。可选的更加通用的方式是用一个空接口类型的切片作为参数和返回值。
13.7 启动外部命令和程序
os
包有一个 StartProcess
函数可以调用或启动外部系统命令和二进制可执行文件;它的第一个参数是要运行的进程,第二个参数用来传递选项或参数,第三个参数是含有系统环境基本信息的结构体。
这个函数返回被启动进程的 id (pid),或者启动失败返回错误。
StartProcess是一个低级接口。os/exec包提供更高级的接口。
如果有错误,则错误类型为*PathError。
exec
包中也有同样功能的更简单的结构体和函数;主要是 exec.Command(name string, arg ...string)
和 Run()
。首先需要用系统命令或可执行文件的名字创建一个 Command
对象,然后用这个对象作为接收者调用 Run()
。
command := exec.Command("date")
command.Stdin = strings.NewReader("some input")
var out bytes.Buffer
command.Stdout = &out
err := command.Run()
if err != nil {
fmt.Printf("Error: %v executing command!", err)
os.Exit(1)
}
fmt.Println(out.String())
13.8 Go 中的单元测试和基准测试
首先所有的包都应该有一定的必要文档,然后同样重要的是对包的测试。
Go 的测试工具 go test
名为 testing 的包被专门用来进行自动化测试,日志和错误报告。并且还包含一些基准测试函数的功能。
备注:gotest 是 Unix bash 脚本,
对一个包做(单元)测试,需要写一些可以频繁(每次更新后)执行的小块测试单元来检查代码的正确性。于是我们必须写一些 Go 源文件来测试代码。测试程序必须属于被测试的包,并且文件名满足这种形式 *_test.go
,所以测试代码和包中的业务代码是分开的。
_test
程序不会被普通的 Go 编译器编译,所以当放应用部署到生产环境时它们不会被部署;只有 gotest 会编译所有的程序:普通程序和测试程序。
测试文件中必须导入 "testing"
包,并写一些名字以 TestZzz
打头的全局函数,这里的 Zzz
是被测试函数的字母描述,如 TestFmtInterface()
,TestPayEmployees()
等。
测试函数必须有这种形式的头部:
func TestAbcde(t *testing.T)
T
是传给测试函数的结构类型,用来管理测试状态,支持格式化测试日志,如 t.Log
,t.Error
,t.ErrorF
等。在函数的结尾把输出跟想要的结果对比,如果不等就打印一个错误,成功的测试则直接返回。
用下面这些函数来通知测试失败:
1)func (t *T) Fail()
标记测试函数为失败,然后继续执行(剩下的测试)。
2)func (t *T) FailNow()
标记测试函数为失败并中止执行;文件中别的测试也被略过,继续执行下一个文件。
3)func (t *T) Log(args ...interface{})
args 被用默认的格式格式化并打印到错误日志中。
4)func (t *T) Fatal(args ...interface{})
结合 先执行 3),然后执行 2)的效果。
运行 go test 来编译测试程序,并执行程序中所有的 TestZZZ
函数。如果所有的测试都通过会打印出 PASS
。
go test 可以接收一个或多个函数程序作为参数,并指定一些选项。
结合 --chatty
或 -v
选项,每个执行的测试函数以及测试状态会被打印。
testing
包中有一些类型和函数可以用来做简单的基准测试;测试代码中必须包含以 BenchmarkZzz
打头的函数并接收一个 *testing.B
类型的参数,比如:
func BenchmarkReverse(b *testing.B) {
...
}
命令 go test –test.bench=.*
会运行所有的基准测试函数;代码中的函数会被调用 N
次(N
是非常大的数,如 N = 1000000
),并展示 N
的值和函数执行的平均时间,单位为 ns(纳秒,ns/op)。如果是用 testing.Benchmark()
调用这些函数,直接运行程序即可。
13.8.1 测试的具体例子
示例even_main.go:
package main
import (
"fmt"
"even/even"
)
func main() {
for i:=0; i<=100; i++ {
fmt.Printf("Is the integer %d even? %v\n", i, even.Even(i))
}
}
上面使用了 even.go 中的 even
包:
示例 even/even.go:
package even
func Even(i int) bool { // Exported function
return i%2 == 0
}
func Odd(i int) bool { // Exported function
return i%2 != 0
}
在 even
包的路径下,我们创建一个名为 oddeven_test.go 的测试程序:
package even
import "testing"
func TestEven(t *testing.T) {
if !Even(10) {
t.Log(" 10 must be even!")
t.Fail()
}
if Even(7) {
t.Log(" 7 is not even!")
t.Fail()
}
}
func TestOdd(t *testing.T) {
if !Odd(11) {
t.Log(" 11 must be odd!")
t.Fail()
}
if Odd(10) {
t.Log(" 10 is not odd!")
t.Fail()
}
}
由于测试需要具体的输入用例且不可能测试到所有的用例(非常像一个无穷的数),所以我们必须对要使用的测试用例思考再三。
至少应该包括:
- 正常的用例
- 反面的用例(错误的输入,如用负数或字母代替数字,没有输入等)
- 边界检查用例(如果参数的取值范围是 0 到 1000,检查 0 和 1000 的情况)
13.8.2 用(测试数据)表驱动测试
编写测试代码时,一个较好的办法是把测试的输入数据和期望的结果写在一起组成一个数据表:表中的每条记录都是一个含有输入和期望值的完整测试用例,有时还可以结合像测试名字这样的额外信息来让测试输出更多的信息。
可以抽象为下面的代码段:
var tests = []struct{ // Test table
in string
out string
}{
{"in1", "exp1"},
{"in2", "exp2"},
{"in3", "exp3"},
...
}
func TestFunction(t *testing.T) {
for i, tt := range tests {
s := FuncToBeTested(tt.in)
if s != tt.out {
t.Errorf("%d. %q => %q, wanted: %q", i, tt.in, s, tt.out)
}
}
}
如果大部分函数都可以写成这种形式,那么写一个帮助函数 verify()
对实际测试会很有帮助:
func verify(t *testing.T, testnum int, testcase, input, output, expected string) {
if expected != output {
t.Errorf("%d. %s with input = %s: output %s != %s", testnum, testcase, input, output, expected)
}
}
TestFunction()
则变为:
func TestFunction(t *testing.T) {
for i, tt := range tests {
s := FuncToBeTested(tt.in)
verify(t, i, "FuncToBeTested: ", tt.in, s, tt.out)
}
}
十五、读写数据
除了 fmt
和 os
包,我们还需要用到 bufio
包来处理缓冲的输入和输出。
15.1 读取用户的输入
我们如何读取用户的键盘(控制台)输入呢?从键盘和标准输入 os.Stdin
读取输入,最简单的办法是使用 fmt
包提供的 Scan...
和 Sscan...
开头的函数
Scan
func Scan(a ...any) (n int, err error)
Scan扫描从标准输入读取的文本,将连续的空格分隔值存储到连续的参数中。换行算作空格。它返回成功扫描的个数。如果它小于参数的数量,err将报告原因。
var a, b int
num, err := fmt.Scan(&a, &b)
Scanf
func Scanf(format string, a ...any) (n int, err error)
Scanf扫描从标准输入读取的文本,根据格式将连续的空格分隔值存储到连续的参数中。它返回成功扫描的个数。如果它小于参数的数量,err将报告原因
输入中的换行符必须与格式中的换行符匹配。唯一的例外是:动词%c总是扫描输入中的下一个符文,即使它是一个空格(或制表符等)或换行符。
Scanln
func Scanln(a ...any) (n int, err error)
Scanln类似于Scan,但是在换行符处停止扫描,并且在最后一项之后必须有换行符或EOF。
Scanf()
与其类似,除了Scanf()
的第一个参数用作格式字符串,用来决定如何读取。
Sscan...
和以Sscan...
开头的函数则是从字符串读取,除此之外,与Scanf()
相同。
func Sscan(str string, a ...any) (n int, err error)
Sscan扫描参数 str ,将连续的用空格分隔的值存储到连续的参数中。换行算作空格。它返回成功扫描的项目数。如果它小于参数的数量,err将报告原因。
func Sscanf(str string, format string, a ...any) (n int, err error)
Sscanf扫描参数字符串,根据格式将连续的用空格分隔的值存储到连续的参数中。它返回成功解析的项的数量。输入中的换行符必须与格式中的换行符匹配。
也可以使用 bufio
包提供的缓冲读取器 (buffered reader) 来读取数据,
func main() {
var inputReader *bufio.Reader
inputReader = bufio.NewReader(os.Stdin)
fmt.Println("Please enter some input: ")
readString, err := inputReader.ReadString('\n')
if err == nil {
fmt.Printf("The input is %s.", readString)
}
switch readString {
case "Philip\r\n":
fmt.Println("Welcome Philip!")
case "Chris\r\n":
fmt.Println("Welcome Chris!")
case "Ivo\r\n":
fmt.Println("Welcome Ivo!")
default:
fmt.Printf("You are not welcome here! Goodbye!")
}
// version 2:
switch readString {
case "Philip\r\n":
fallthrough
case "Ivo\r\n":
fallthrough
case "Chris\r\n":
fmt.Printf("Welcome %s\n", readString)
default:
fmt.Printf("You are not welcome here! Goodbye!\n")
}
// version 3:
switch readString {
case "Philip\r\n", "Ivo\r\n":
fmt.Printf("Welcome %s\n", readString)
default:
fmt.Printf("You are not welcome here! Goodbye!\n")
}
}
屏幕是标准输出 os.Stdout
;os.Stderr
用于显示错误信息,大多数情况下等同于 os.Stdout
。
Go里面switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch, 但是可以使用fallthrough强制执行后面的case代码。
15.2 文件读写
在 Go 语言中,文件使用指向 os.File
类型的指针来表示的,也叫做文件句柄。我们在前面章节使用到过标准输入 os.Stdin
和标准输出 os.Stdout
,他们的类型都是 *os.File
。
func main() {
inputFile, inputError := os.Open("input.dat")
if inputError != nil {
return // exit the function on error
}
defer inputFile.Close()
inputReader := bufio.NewReader(inputFile)
for {
inputString, readerError := inputReader.ReadString('\n')
fmt.Printf("The input was: %s", inputString)
if readerError == io.EOF {
return
}
}
}
1) 将整个文件的内容读到一个字符串里:
可以使用 io/ioutil
包里的 ioutil.ReadFile()
方法(已舍弃)
新的在os
包里
func ReadFile(name string) ([]byte, error)
ReadFile读取指定文件并返回内容。一个成功的调用返回err == nil,而不是err == EOF。因为ReadFile读取整个文件,所以它不会将Read的EOF作为要报告的错误。
file, err := os.ReadFile("./a.txt")
if err != nil {
log.Fatal(err)
}
os.Stdout.Write(file)
2) 带缓冲的读取
在很多情况下,文件的内容是不按行划分的,或者干脆就是一个二进制文件。在这种情况下,ReadString()
就无法使用了,我们可以使用 bufio.Reader
的 Read()
,它只接收一个参数:
buf := make([]byte, 1024)
...
// `n` 的值表示读取到的字节数.
n, err := inputReader.Read(buf)
if (n == 0) { break}
3) 按列读取文件中的数据
如果数据是按列排列并用空格分隔的,你可以使用 fmt
包提供的以 FScan...
开头的一系列函数来读取他们
func main() {
file, err := os.Open("products2.txt")
if err != nil {
panic(err)
}
defer file.Close()
var col1, col2, col3 []string
for {
var v1, v2, v3 string
_, err := fmt.Fscanln(file, &v1, &v2, &v3)
// scans until newline
if err != nil {
break
}
col1 = append(col1, v1)
col2 = append(col2, v2)
col3 = append(col3, v3)
}
fmt.Println(col1)
fmt.Println(col2)
fmt.Println(col3)
}
注意: path
包里包含一个子包叫 filepath
,这个子包提供了跨平台的函数,用于处理文件名和路径。例如 Base()
函数用于获得路径中的最后一个元素(不包含后面的分隔符)
func Base(path string) string
Base返回path的最后一个元素。在提取最后一个元素之前删除尾随路径分隔符。如果路径为空,Base返回"."。如果路径完全由分隔符组成,Base返回一个分隔符。
关于解析 CSV 文件,
encoding/csv
包提供了相应的功能。
15.3 compress 包:读取压缩文件
compress
包提供了读取压缩文件的功能,支持的压缩文件格式为:bzip2、flate、gzip、lzw 和 zlib。
compress/gzip
gzip包实现了对gzip格式压缩文件的读写,如RFC 1952中所规定的。
类型
type Header struct {
Comment string // comment
Extra []byte // "extra data"
ModTime time.Time // modification time
Name string // file name
OS byte // operating system type
}
gzip文件存储关于压缩文件的元数据头。该头作为Writer和Reader结构的字段公开。
由于GZIP文件格式的限制,字符串必须是UTF-8编码的,并且只能包含Unicode编码点U+0001到U+00FF。
Reader
type Reader struct {
Header // valid after NewReader or Reader.Reset
// contains filtered or unexported fields
}
Reader 是io.Reader, 可以从gzip格式的压缩文件中读取未压缩数据的读取器
一般来说,gzip文件可以是gzip文件的串联,每个文件都有自己的头文件。从Reader读取的操作返回每个未压缩数据的串联。Reader字段中只记录第一个报头。
Gzip文件存储未压缩数据的长度和校验和
客户端应将Read返回的数据视为暂定数据,直到读到 io.EOF
func (z *Reader) Close() error
NewReader()
func NewReader(r io.Reader) (*Reader, error)
NewReader创建一个新的Reader,读取给定的Reader。如果r也没有实现io.ByteReader,解压缩器可能从r中读取比需要的更多的数据。
调用者有责任在完成Reader时调用Close。
Reader.Header 字段将在返回的Reader中有效
15.4 写文件
func main () {
// var outputWriter *bufio.Writer
// var outputFile *os.File
// var outputError os.Error
// var outputString string
outputFile, outputError := os.OpenFile("output.dat", os.O_WRONLY|os.O_CREATE, 0666)
if outputError != nil {
fmt.Printf("An error occurred with file opening or creation\n")
return
}
defer outputFile.Close()
outputWriter := bufio.NewWriter(outputFile)
outputString := "hello world!\n"
for i:=0; i<10; i++ {
outputWriter.WriteString(outputString)
}
outputWriter.Flush()
}
如果写入的东西很简单,我们可以使用 fmt.Fprintf(outputFile, "Some test data.\n")
直接将内容写入文件。fmt
包里的 F...
开头的 Print()
函数可以直接写入任何 io.Writer
,包括文件
15.5 文件拷贝
如何拷贝一个文件到另一个文件?最简单的方式就是使用 io
包:
func copyFile(destName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer func(src *os.File) {
err := src.Close()
if err != nil {
fmt.Println("打开文件失败。。。")
}
}(src)
dest, err := os.Create(destName)
if err != nil {
return
}
defer func(dest *os.File) {
err := dest.Close()
if err != nil {
fmt.Println("关闭文件失败。。。")
}
}(dest)
return io.Copy(dest, src)
}
15.6 从命令行读取参数
15.6.1 os 包
os
包中有一个 string
类型的切片变量 os.Args
,用来处理一些基本的命令行参数,它在程序启动后读取命令行输入的参数。
func main() {
who := "Alice "
if len(os.Args) > 1 {
who += strings.Join(os.Args[1:], " ")
}
fmt.Println("Good Morning", who)
}
15.6.2 flag 包
var NewLine = flag.Bool("nn", false, "print newline")
const (
Space = " "
Newline = "\n"
)
func main() {
flag.PrintDefaults()
flag.Parse()
fmt.Println("NFlag:", flag.NFlag())
var s string = ""
for i := 0; i < flag.NArg(); i++ {
if i > 0 {
s += Space
if *NewLine {
s += Newline
}
}
s += flag.Arg(i)
}
os.Stdout.WriteString(s)
}
15.7 用切片读写文件
func cat(f *os.File) {
const NBUF = 512
var buf [NBUF]byte
for {
switch nr, err := f.Read(buf[:]); {
case nr < 0:
fmt.Fprintf(os.Stderr, "cat: error reading: %s\n", err.Error())
os.Exit(1)
case nr == 0: // EOF
return
case nr > 0:
if nw, ew := os.Stdout.Write(buf[0:nr]); nw != nr {
fmt.Fprintf(os.Stderr, "cat: error writing: %s\n", ew.Error())
}
}
}
}
15.8 用 defer 关闭文件
defer
关键字对于在函数结束时关闭打开的文件非常有用。
func data(name string) string {
f, _ := os.OpenFile(name, os.O_RDONLY, 0)
defer f.Close() // idiomatic Go code!
contents, _ := ioutil.ReadAll(f)
return string(contents)
}
15.9 JSON 数据格式
数据结构要在网络中传输或保存到文件,就必须对其编码和解码;目前存在很多编码格式:JSON,XML,gob,Google 缓冲协议等等。Go 语言支持所有这些编码格式
结构可能包含二进制数据,如果将其作为文本打印,那么可读性是很差的。另外结构内部可能包含匿名字段,而不清楚数据的用意。
通过把数据转换成纯文本,使用命名的字段来标注,让其具有可读性。这样的数据格式可以通过网络传输,而且是与平台无关的,任何类型的应用都能够读取和输出,不与操作系统和编程语言的类型相关。
下面是一些术语说明:
- 数据结构 --> 指定格式 = 序列化 或 编码(传输之前)
- 指定格式 --> 数据结构 = 反序列化 或 解码(传输之后)
序列化是在内存中把数据转换成指定格式(数据 -> 字符串),反之亦然(字符串 -> 数据)。
尽管 XML 被广泛的应用,但是 JSON 更加简洁、轻量(占用更少的内存、磁盘及网络带宽)和更好的可读性,这也使它越来越受欢迎。
Go 语言的 json
包可以让你在程序中方便的读取和写入 JSON 数据。
15.9.1 序列化
type Address struct {
Type string
City string
Country string
}
type VCard struct {
FirstName string
LastName string
Addresses []*Address
Remark string
}
func main() {
pa := &Address{"private", "Aartselaar", "Belgium"}
wa := &Address{"work", "Boom", "Belgium"}
vc := VCard{"Jan", "Kersschot", []*Address{pa, wa}, "none"}
marshal, _ := json.Marshal(vc)
fmt.Printf("JSON format: %s", marshal)
file, _ := os.OpenFile("vcard.json", os.O_CREATE|os.O_WRONLY, 0666)
defer file.Close()
encoder := json.NewEncoder(file)
err := encoder.Encode(vc)
if err != nil {
log.Println("Error in encoding json")
}
}
出于安全考虑,在 web 应用中最好使用 json.MarshalforHTML()
函数,其对数据执行 HTML 转码,所以文本可以被安全地嵌在 HTML <script>
标签中。
JSON 与 Go 类型对应如下:
bool
对应 JSON 的 booleanfloat64
对应 JSON 的 numberstring
对应 JSON 的 stringnil
对应 JSON 的 null
不是所有的数据都可以编码为 JSON 类型,只有验证通过的数据结构才能被编码:
- JSON 对象只支持字符串类型的 key;要编码一个 Go
map
类型,map
必须是map[string]T
(T
是json
包中支持的任何类型) - Channel,复杂类型和函数类型不能被编码
- 不支持循环数据结构;它将引起序列化进入一个无限循环
- 指针可以被编码,实际上是对指针指向的值进行编码(或者指针是
nil
)
15.9.2 反序列化
func Unmarshal(data []byte, v any) error
Unmarshal 解析json编码的数据,并将结果存储到 v 中。如果v为nil或不是指针,Unmarshal将返回InvalidUnmarshalError。
Unmarshal使用与Marshal相反的编码,根据需要分配映射、片和指针
虽然反射能够让 JSON 字段去尝试匹配目标结构字段;但是只有真正匹配上的字段才会填充数据。字段没有匹配不会报错,而是直接忽略掉。
15.9.3 解码任意的数据
json 包使用 map[string]interface{}
和 []interface{}
储存任意的 JSON 对象和数组;其可以被反序列化为任何的 JSON blob 存储到接口值中。
JSON 数据,被存储在变量 b
中:
b := []byte(`{"Name": "Wednesday", "Age": 6, "Parents": ["Gomez", "Morticia"]}`)
直接使用 Unmarshal()
把这个数据编码并保存在接口值中:
var f interface{}
err := json.Unmarshal(b, &f)
f 指向的值是一个 map
,key 是一个字符串,value 是自身存储作为空接口类型的值:
map[string]interface{} {
"Name": "Wednesday",
"Age": 6,
"Parents": []interface{} {
"Gomez",
"Morticia",
},
}
要访问这个数据,我们可以使用类型断言
m := f.(map[string]interface{})
我们可以通过 for range 语法和 type switch 来访问其实际类型:
func main() {
var f interface{}
b := []byte(`{"Name": "Wednesday", "Age": 6, "Parents": ["Gomez", "Morticia"]}`)
err := json.Unmarshal(b, &f)
if err != nil {
fmt.Println("解析出错")
}
m := f.(map[string]interface{})
for k, v := range m {
switch vv := v.(type) {
case string:
fmt.Println(k, "is string", vv)
case float64:
fmt.Println(k, "is float64", vv)
case []interface{}:
fmt.Println(k, "is array")
for i, u := range vv {
fmt.Println(i, u)
}
default:
fmt.Println(k, "is of a type I don’t know how to handle")
}
}
}
通过这种方式,你可以处理未知的 JSON 数据,同时可以确保类型安全。
15.9.4 解码数据到结构
如果我们事先知道 JSON 数据,我们可以定义一个适当的结构并对 JSON 数据反序列化。
type FamilyMember struct {
Name string
Age int
Parents []string
}
并对其反序列化:
var m FamilyMember
err := json.Unmarshal(b, &m)
程序实际上是分配了一个新的切片。这是一个典型的反序列化引用类型(指针、切片和 map
)的例子。
15.9.5 编码和解码流
json
包提供 Decoder
和 Encoder
类型来支持常用 JSON 数据流读写。NewDecoder()
和 NewEncoder()
函数分别封装了 io.Reader
和 io.Writer
接口。
func NewDecoder(r io.Reader) *Decoder
func NewEncoder(w io.Writer) *Encoder
要想把 JSON 直接写入文件,可以使用 json.NewEncoder
初始化文件(或者任何实现 io.Writer
的类型),并调用 Encode()
;反过来与其对应的是使用 json.NewDecoder
和 Decode()
函数:
func NewDecoder(r io.Reader) *Decoder
func (dec *Decoder) Decode(v interface{}) error
来看下接口是如何对实现进行抽象的:数据结构可以是任何类型,只要其实现了某种接口,目标或源数据要能够被编码就必须实现 io.Writer
或 io.Reader
接口。由于 Go 语言中到处都实现了 Reader 和 Writer,因此 Encoder
和 Decoder
可被应用的场景非常广泛,例如读取或写入 HTTP 连接、websockets 或文件。