摘要:go入门相关内容,参考:Go入门指南,主要总结其中:十二到十三章的内容。
一、读写数据
1、读取用户的输入
从键盘和标准输入 os.Stdin 读取输入,最简单的办法是使用 fmt 包提供的 Scan 和 Sscan 开头的函数。请看以下程序:
1 | package main |
Scanln 扫描来自 标准输入的文本,将空格分隔的值依次存放到后续的参数内,直到碰到换行。
Scanf 与其类似,除了 Scanf 的第一个参数用作格式字符串,用来决定如何读取。
Sscan 和以 Sscan 开头的函数则是 从字符串读取,除此之外,与 Scanf 相同。如果这些函数读取到的结果与您预想的不同,您可以检查成功读入数据的个数和返回的错误。
您也可以使用 bufio 包提供的缓冲读取(buffered reader)来读取数据。
例子如下所示:
1 | package main |
ReadString(delim byte),该方法从输入中读取内容,直到碰到 delim 指定的字符,然后将读取到的内容连同 delim 字符一起放到缓冲区。
ReadString 返回读取到的字符串,如果碰到错误则返回 nil。如果它一直读到文件结束,则返回读取到的字符串和 io.EOF。如果读取过程中没有碰到 delim 字符,将返回错误 err != nil。
2、文件读写
在 Go 语言中,文件使用指向 os.File 类型的指针来表示的,也叫做 文件句柄 。
我们在前面使用到过标准输入 os.Stdin 和标准输出 os.Stdout,他们的类型都是 *os.File。
让我们来看看下面这个程序:
1 | package main |
1)将整个文件的内容读到一个字符串里
1 | package main |
2)带缓冲的读取
在很多情况下,文件的内容是 不按行划分的,或者干脆就是一个二进制文件 。在这种情况下,ReadString() 就无法使用了,我们可以使用 bufio.Reader 的 Read(),它只接收一个参数:
1 | buf := make([]byte, 1024) |
3)按列读取文件中的数据
如果数据是按列排列并用空格分隔的,你可以使用 fmt 包提供的以 FScan 开头的一系列函数来读取他们。
例子如下所示:
1 | package main |
4)compress 包:读取压缩文件
compress 包提供了读取压缩文件的功能,支持的压缩文件格式为:bzip2、flate、gzip、lzw 和 zlib。
下面的程序展示了如何读取一个 gzip 文件:
1 | package main |
5)写文件
例子如下所示:
1 | package main |
我们以只写模式打开文件 output.dat,如果文件不存在则自动创建:
1 | outputFile, outputError := os.OpenFile(“output.dat”, os.O_WRONLY|os.O_CREATE, 0666) |
我们通常会用到以下标志:
- os.O_RDONLY:只读。
- os.O_WRONLY:只写。
- os.O_CREATE:创建:如果指定文件不存在,就创建该文件。
- os.O_TRUNC:截断:如果指定文件已存在,就将该文件的长度截为 0。
使用其他函数如何写文件:
1 | package main |
3、文件拷贝
1 | package main |
注意 defer 的使用:当打开目标文件时发生了错误,那么 defer 仍然能够确保 src.Close() 执行。如果不这么做,文件会一直保持打开状态并占用资源。
4、从命令行读取参数
1)os 包
os 包中有一个 string 类型的切片变量 os.Args,用来处理一些基本的命令行参数,它在程序启动后读取命令行输入的参数。
1 | package main |
我们在 IDE 或编辑器中直接运行这个程序输出:Good Morning Alice
但是我们在命令行加入参数,像这样:os_args John Bill Marc Luke,将得到这样的输出:Good Morning Alice John Bill Marc Luke
这个命令行参数会放置在切片 os.Args[] 中(以空格分隔),从索引 1 开始(os.Args[0] 放的是程序本身的名字,在本例中是 os_args)。函数 strings.Join 以空格为间隔连接这些参数。
2)flag 包
flag 包有一个扩展功能用来解析命令行选项。但是通常被用来替换基本常量,例如,在某些情况下我们希望在命令行给常量一些不一样的值。
在 flag 包中一个 Flag 被定义成一个含有如下字段的结构体:
1 | type Flag struct { |
下面的程序模拟了 Unix 的 echo 功能:
1 | package main |
方法说明:
1 | flag.Parse() 扫描参数列表(或者常量列表)并设置 flag, flag.Arg(i) 表示第 i 个参数。Parse() 之后 flag.Arg(i) 全部可用,flag.Arg(0) 就是第一个真实的 flag,而不是像 os.Args(0) 放置程序的名字。 |
5、用 buffer 读取文件
在下面的例子中,我们结合使用了缓冲读取文件和命令行 flag 解析这两项技术。
如果不加参数,那么你输入什么屏幕就打印什么。
参数被认为是文件名,如果文件存在的话就打印文件内容到屏幕。
例子如下所示:
1 | package main |
6、用切片读写文件
切片提供了 Go 中处理 I/O 缓冲的标准方式,下面 cat 函数中,在一个切片缓冲内使用无限 for 循环(直到文件尾部 EOF)读取文件,并写入到标准输出(os.Stdout)。
1 | package main |
7、使用接口的实际例子:fmt.Fprintf
下面例子很好的阐述了 io 包中的接口概念。
1 | package main |
下面是 fmt.Fprintf() 函数的实际签名
1 | func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) |
其不是写入一个文件,而是写入一个 io.Writer 接口类型的变量,下面是 Writer 接口在 io 包中的定义:
1 | type Writer interface { |
fmt.Fprintf() 依据指定的格式向第一个参数内写入字符串,第一参数必须实现了 io.Writer 接口。
Fprintf() 能够写入任何类型,只要其实现了 Write 方法,包括 os.Stdout, 文件(例如 os.File),管道,网络连接,通道等等,
同样的也可以使用 bufio 包中缓冲写入。
1 | bufio 包中定义了 type Writer struct{...} |
在缓冲写入的最后千万 **不要忘了使用 Flush()**,否则最后的输出不会被写入。
8、JSON 数据格式
通过把数据转换成纯文本,使用命名的字段来标注,让其具有可读性。这样的数据格式可以通过网络传输,而且是与平台无关的,任何类型的应用都能够读取和输出,不与操作系统和编程语言的类型相关。
下面是一些术语说明:
- 数据结构 –> 指定格式 = 序列化 或 编码(传输之前)
- 指定格式 –> 数据格式 = 反序列化 或 解码(传输之后)
序列化是在内存中把数据转换成指定格式(data -> string),反之亦然(string -> data structure)
编码也是一样的,只是输出一个数据流(实现了 io.Writer 接口);解码是从一个数据流(实现了 io.Reader)输出到一个数据结构。
1)序列化
Go 语言的 json 包可以让你在程序中方便的读取和写入 JSON 数据。
例子如下所示:
1 | package main |
json.Marshal() 的函数签名是:
1 | func Marshal(v interface{}) ([]byte, error) |
1 | 出于安全考虑,在 web 应用中最好使用 json.MarshalforHTML() 函数,其对数据执行 HTML 转码,所以文本可以被安全地嵌在 HTML <script> 标签中。 |
JSON 与 Go 类型对应如下:
- bool 对应 JSON 的 booleans
- float64 对应 JSON 的 numbers
- string 对应 JSON 的 strings
- nil 对应 JSON 的 null
不是所有的数据都可以编码为 JSON 类型:只有验证通过的数据结构才能被编码:
- JSON 对象只支持字符串类型的 key;要编码一个 Go map 类型,map 必须是 map [string] T(T 是 json 包中支持的任何类型)
- Channel,复杂类型和函数类型不能被编码
- 不支持循环数据结构;它将引起序列化进入一个无限循环
- 指针可以被编码,实际上是对指针指向的值进行编码(或者指针是 nil)
2)反序列化
UnMarshal() 的函数签名是:
1 | func Unmarshal(data []byte, v interface{}) error |
上面的例子编码后的数据为 Json ,对其解码时,我们首先创建结构 VCard 用来保存解码的数据:var v VCard 并调用 json.Unmarshal(js, &v),解析 [] byte 中的 JSON 数据并将结果存入指针 &v 指向的值。
虽然反射能够让 JSON 字段去尝试匹配目标结构字段;但是只有真正匹配上的字段才会填充数据。字段没有匹配不会报错,而是直接忽略掉。
解码任意的数据
1 | json 包使用 map[string]interface{} 和 []interface{} 储存任意的 JSON 对象和数组。 |
来看这个 JSON 数据,被存储在变量 b 中:
1 | b := []byte(`{"Name": "Wednesday", "Age": 6, "Parents": ["Gomez", "Morticia"]}`) |
不用理解这个数据的结构,我们可以直接使用 Unmarshal 把这个数据编码并保存在接口值中:
1 | var f interface{} |
f 指向的值是一个 map,key 是一个字符串,value 是自身存储作为空接口类型的值:
1 | map[string]interface{} { |
要访问这个数据,我们可以使用类型断言
1 | m := f.(map[string]interface{}) |
我们可以通过 for range 语法和 type switch 来访问其实际类型:
1 | for k, v := range m { |
通过这种方式,你可以处理未知的 JSON 数据,同时可以确保类型安全。完整代码如下所示:
1 | package main |
注意: 如果反序列化不指定结构体类型或者变量类型,则JSON中的数字类型,默认被反序列化成float64类型。
解码数据到结构
如果我们事先知道 JSON 数据,我们可以定义一个适当的结构并对 JSON 数据反序列化。
例子如下所示:
1 | package main |
3)编码和解码流
json 包提供 Decoder 和 Encoder 类型来支持常用 JSON 数据流读写。NewDecoder 和 NewEncoder 函数分别封装了 io.Reader 和 io.Writer 接口。
1 | func NewDecoder(r io.Reader) *Decoder |
要想把 JSON 直接写入文件,可以使用 json.NewEncoder 初始化文件(或者任何实现 io.Writer 的类型),并调用 Encode ();反过来与其对应的是使用 json.Decoder 和 Decode () 函数:
1 | func NewDecoder(r io.Reader) *Decoder |
来看下接口是如何对实现进行抽象的:
- 数据结构可以是任何类型,只要其实现了某种接口,目标或源数据要能够被编码就必须实现 io.Writer 或 io.Reader 接口。
- 由于 Go 语言中到处都实现了 Reader 和 Writer,因此 Encoder 和 Decoder 可被应用的场景非常广泛,例如读取或写入 HTTP 连接、websockets 或文件。
9、XML 数据格式
如同 json 包一样,XML 也有 Marshal() 和 UnMarshal() 从 XML 中编码和解码数据。
和 JSON 的方式一样,XML 数据可以序列化为结构,或者从结构反序列化为 XML 数据。
encoding/xml 包实现了一个简单的 XML 解析器(SAX),用来解析 XML 数据内容。例子如下所示:
1 | package main |
包中定义了若干 XML 标签类型:StartElement,Chardata(这是从开始标签到结束标签之间的实际文本),EndElement,Comment,Directive 或 ProcInst。
包中同样定义了一个结构解析器:
- NewParser 方法持有一个 io.Reader(这里具体类型是 strings.NewReader)并生成一个解析器类型的对象。
- Token() 方法返回输入流里的下一个 XML token。在输入流的结尾处,会返回(nil,io.EOF)。
XML 文本被循环处理直到 Token() 返回一个错误,因为已经到达文件尾部,再没有内容可供处理了。通过一个 type-switch 可以根据一些 XML 标签进一步处理。Chardata 中的内容只是一个 [] byte,通过字符串转换让其变得可读性强一些。
10、用 Gob 传输数据
Gob 是 Go 自己的以二进制形式序列化和反序列化程序数据的格式;可以在 encoding 包中找到。这种格式的数据简称为 Gob (即 Go binary 的缩写)。类似于 Python 的 “pickle” 和 Java 的 “Serialization”。
Gob 通常用于远程方法调用参数和结果的传输,以及应用程序和机器之间的数据传输。
它和 JSON 或 XML 有什么不同呢?
- Gob 特定地用于纯 Go 的环境中,例如,两个用 Go 写的服务之间的通信。这样的话服务可以被实现得更加高效和优化。
- Gob 不是可外部定义,语言无关的编码方式。因此它的首选格式是二进制,而不是像 JSON 和 XML 那样的文本格式。
- Gob 并不是一种不同于 Go 的语言,而是在编码和解码过程中用到了 Go 的反射。
- Gob 文件或流是完全自描述的:里面包含的所有类型都有一个对应的描述,并且总是可以用 Go 解码,而不需要了解文件的内容。
- 只有可导出的字段会被编码,零值会被忽略。
- 在解码结构体的时候,只有同时匹配名称和可兼容类型的字段才会被解码。
- 当源数据类型增加新字段后,Gob 解码客户端仍然可以以这种方式正常工作:解码客户端会继续识别以前存在的字段。
- 提供了很大的灵活性,比如在发送者看来,整数被编码成没有固定长度的可变长度,而忽略具体的 Go 类型。
假如在发送者这边有一个有结构 T:
1 | type T struct { X, Y, Z int } |
而在接收者这边可以用一个结构体 U 类型的变量 u 来接收这个值:
1 | type U struct { X, Y *int8 } |
在接收者中,X 的值是 7,Y 的值是 0(Y 的值并没有从 t 中传递过来,因为它是零值)。
和 JSON 的使用方式一样,Gob 使用通用的 io.Writer 接口,通过 NewEncoder() 函数创建 Encoder 对象并调用 Encode();相反的过程使用通用的 io.Reader 接口,通过 NewDecoder() 函数创建 Decoder 对象并调用 Decode。
例子如下所示:
1 | package main |
11、Go 中的密码学
- hash 包:实现了 adler32、crc32、crc64 和 fnv 校验。
- crypto 包:实现了其它的 hash 算法,比如 md4、md5、sha1 等。以及完整地实现了 aes、blowfish、rc4、rsa、xtea 等加密算法。
使用 sha1 示例:
1 | package main |
通过调用 sha1.New() 创建了一个新的 hash.Hash 对象,用来计算 SHA1 校验值。
二、错误处理与测试
Go 没有像 Java 和 .NET 那样的 try/catch 异常机制:不能执行抛异常操作。但是有一套 defer-panic-and-recover 机制。
Go 通过在函数和方法中返回错误对象作为它们的唯一或最后一个返回值 —— 如果返回 nil,则没有错误发生 —— 并且主调(calling)函数总是应该检查收到的错误。
永远不要忽略错误,否则可能会导致程序崩溃!!
处理错误并且在函数发生错误的地方给用户返回错误信息:照这样处理就算真的出了问题,你的程序也能继续运行并且通知给用户。
panic and recover 是用来处理真正的异常(无法预测的错误)而不是普通的错误。
在前面的章节中我们了解了 Go 检查和报告错误条件的惯有方式:
- 产生错误的函数会返回两个变量,一个值和一个错误码;如果后者是 nil 就是成功,非 nil 就是发生了错误。
- 为了防止发生错误时正在执行的函数(如果有必要的话甚至会是整个程序)被中止,在调用函数后必须检查错误。
1、错误处理
Go 有一个预先定义的 error 接口类型
1 | type error interface { |
错误值用来表示异常状态。errors 包中有一个 errorString 结构体实现了 error 接口。当程序处于错误状态时可以用 os.Exit(1) 来中止运行。
命名规范:错误类型以 “Error” 结尾,错误变量以 “err” 或 “Err” 开头。
1)定义错误
任何时候当你需要一个新的错误类型,都可以用 errors(必须先 import)包的 errors.New 函数接收合适的错误信息来创建,像下面这样:
1 | err := errors.New("math - square root of negative number") |
例子如下所示:
1 | package main |
在大部分情况下自定义错误结构类型很有意义的,可以包含除了(低层级的)错误信息以外的其它有用信息,例如,正在进行的操作(打开文件等),全路径或名字。看下面例子中 os.Open 操作触发的 PathError 错误:
1 | // PathError records an error and the operation and file path that caused it. |
如果有不同错误条件可能发生,那么对实际的错误使用类型断言或类型判断(type-switch)是很有用的,并且可以根据错误场景做一些补救和恢复操作。
1 | // err != nil |
2)用 fmt 创建错误对象
通常你想要返回包含错误参数的更有信息量的字符串,例如:可以用 fmt.Errorf() 来实现:它和 fmt.Printf () 完全一样,接收有一个或多个格式占位符的格式化字符串和相应数量的占位变量。和打印信息不同的是它用信息生成错误对象。
例子如下所示:
1 | package main |
2、运行时异常和 panic
当发生像数组下标越界或类型断言失败这样的运行错误时,Go 运行时会触发运行时 panic,伴随着程序的崩溃抛出一个 runtime.Error 接口类型的值。这个错误值有个 RuntimeError() 方法用于区别普通错误。
panic 可以直接从代码初始化:当错误条件(我们所测试的代码)很严苛且不可恢复,程序不能继续运行时,可以使用 panic 函数产生一个中止程序的运行时错误。panic 接收一个做任意类型的参数,通常是字符串,在程序死亡时被打印出来。Go 运行时负责中止程序并给出调试信息。
例子如下所示:
1 | package main |
Go panicking:
在多层嵌套的函数调用中调用 panic,可以马上中止当前函数的执行,所有的 defer 语句都会保证执行并把控制权交还给接收到 panic 的函数调用者。这样向上冒泡直到最顶层,并执行(每层的) defer,在栈顶处程序崩溃,并在命令行中用传给 panic 的值报告错误情况:这个终止过程就是 panicking。
标准库中有许多包含 Must 前缀的函数,像 regexp.MustComplie 和 template.Must;当正则表达式或模板中转入的转换字符串导致错误时,这些函数会 panic。
3、从 panic 中恢复(Recover)
正如名字一样,这个(recover)内建函数被用于从 panic 或 错误场景中恢复:让程序可以从 panicking 重新获得控制权,停止终止过程进而恢复正常执行。
recover 只能在 defer 修饰的函数中使用:用于取得 panic 调用中传递过来的错误值,如果是正常执行,调用 recover 会返回 nil,且没有其它效果。
总结:panic 会导致栈被展开直到 defer 修饰的 recover () 被调用或者程序中止。
例子如下所示:
1 | package main |
defer-panic-recover机制
例子如下所示:
1 | package main |
defer-panic-recover 在某种意义上也是一种像 if,for 这样的控制流机制。
Go 标准库中许多地方都用了这个机制,例如,json 包中的解码和 regexp 包中的 Complie 函数。Go 库的原则是即使在包的内部使用了 panic,在它的对外接口(API)中也必须用 recover 处理成返回显式的错误。
4、自定义包中的错误处理和 panicking
这是所有自定义包实现者应该遵守的最佳实践:
- 1)在包内部,总是应该从 panic 中 recover:不允许显式的超出包范围的 panic ()
- 2)向包的调用者返回错误值(而不是 panic)。
我们有一个简单的 parse 包用来把输入的字符串解析为整数切片;这个包有自己特殊的 ParseError。当没有东西需要转换或者转换成整数失败时,这个包会 panic(在函数 fields2numbers 中)。但是可导出的 Parse 函数会从 panic 中 recover 并用所有这些信息返回一个错误给调用者。
为了演示这个过程,在 panic_recover.go 中 调用了 parse 包;不可解析的字符串会导致错误并被打印出来。
parse.go 代码如下所示:
1 | // parse.go |
panic_package.go 代码如下所示:
1 | // panic_package.go |
5、一种用闭包处理错误的模式
每当函数返回时,我们应该检查是否有错误发生:但是这会导致重复乏味的代码。
结合 defer/panic/recover 机制和闭包可以得到一个我们马上要讨论的更加优雅的模式。不过这个模式只有当所有的函数都是同一种签名时可用,这样就有相当大的限制。
一个很好的使用它的例子是 web 应用,所有的处理函数都是下面这样:
1 | func handler1(w http.ResponseWriter, r *http.Request) { ... } |
假设所有的函数都有这样的签名:
1 | func f(a type1, b type2) |
参数的数量和类型是不相关的。
我们给这个类型一个名字:
1 | fType1 = func f(a type1, b type2) |
在我们的模式中使用了两个帮助函数:
1)check:这是用来检查是否有错误和 panic 发生的函数:
1 | func check(err error) { if err != nil { panic(err) } } |
2)errorhandler:这是一个包装函数。接收一个 fType1 类型的函数 fn 并返回一个调用 fn 的函数。里面就包含有 defer/recover 机制
1 | func errorHandler(fn fType1) fType1 { |
当错误发生时会 recover 并打印在日志中;除了简单的打印,应用也可以用 template 包为用户生成自定义的输出。check () 函数会在所有的被调函数中调用,像这样:
1 | func f1(a type1, b type2) { |
通过这种机制,所有的错误都会被 recover,并且调用函数后的错误检查代码也被简化为调用 check (err) 即可。
在这种模式下,不同的错误处理必须对应不同的函数类型;它们(错误处理)可能被隐藏在错误处理包内部。
可选的更加通用的方式是用一个空接口类型的切片作为参数和返回值。
6、启动外部命令和程序
os 包有一个 StartProcess 函数可以调用或启动外部系统命令和二进制可执行文件;它的第一个参数是要运行的进程,第二个参数用来传递选项或参数,第三个参数是含有系统环境基本信息的结构体。
这个函数返回被启动进程的 id(pid),或者启动失败返回错误。
exec 包中也有同样功能的更简单的结构体和函数;主要是 exec.Command(name string, arg …string) 和 Run()。首先需要用系统命令或可执行文件的名字创建一个 Command 对象,然后用这个对象作为接收者调用 Run()。
下面的程序(因为是执行 Linux 命令,只能在 Linux 下面运行)演示了它们的使用:
1 | package main |
1 | // 2nd example: show all processes |
1 | // 2) exec.Run // |
7、Go 中的单元测试和基准测试
首先所有的包都应该有一定的必要文档,然后同样重要的是对包的测试。
名为 testing 的包被专门用来进行自动化测试,日志和错误报告。并且还包含一些基准测试函数的功能。
备注:gotest 是 Unix bash 脚本,所以在 Windows 下你需要配置 MINGW 环境(参见 2.5 节);在 Windows 环境下把所有的 pkg/linux_amd64 替换成 pkg/windows。
对一个包做(单元)测试,需要写一些可以频繁(每次更新后)执行的小块测试单元来检查代码的正确性。
于是我们必须写一些 Go 源文件来测试代码。测试程序必须属于被测试的包,并且文件名满足这种形式 *_test.go,所以测试代码和包中的业务代码是分开的。
_test 程序不会被普通的 Go 编译器编译,所以当放应用部署到生产环境时它们不会被部署;只有 gotest 会编译所有的程序:普通程序和测试程序。
测试文件中必须导入 “testing” 包,并写一些名字以 TestZzz 打头的全局函数,这里的 Zzz 是被测试函数的字母描述,如 TestFmtInterface,TestPayEmployees 等。
测试函数必须有这种形式的头部:
1 | func TestAbcde(t *testing.T) |
T 是传给测试函数的结构类型,用来管理测试状态,支持格式化测试日志,如 t.Log,t.Error,t.ErrorF 等。
在函数的结尾把输出跟想要的结果对比,如果不等就打印一个错误。成功的测试则直接返回。
用下面这些函数来通知测试失败:
1 | 1)func (t *T) Fail() |
运行 go test 来编译测试程序,并执行程序中所有的 TestZZZ 函数。如果所有的测试都通过会打印出 PASS。
gotest 可以接收一个或多个函数程序作为参数,并指定一些选项。
结合 –chatty 或 -v 选项,每个执行的测试函数以及测试状态会被打印。
例如:
1 | go test fmt_test.go --chatty |
testing 包中有一些类型和函数可以用来做简单的基准测试;测试代码中必须包含以 BenchmarkZzz 打头的函数并接收一个 *testing.B 类型的参数,比如:
1 | func BenchmarkReverse(b *testing.B) { |
命令 go test –test.bench=.* 会运行所有的基准测试函数。代码中的函数会被调用 N 次(N 是非常大的数,如 N = 1000000),并展示 N 的值和函数执行的平均时间,单位为 ns(纳秒,ns/op)。
如果是用 testing.Benchmark 调用这些函数,直接运行程序即可。
测试用例至少应该包括:
- 正常的用例
- 反面的用例(错误的输入,如用负数或字母代替数字,没有输入等)
- 边界检查用例(如果参数的取值范围是 0 到 1000,检查 0 和 1000 的情况)
测试的具体例子
even_main.go 的代码如下所示:
1 | package main |
even/even.go 的代码如下所示:
1 | package even |
oddeven_test.go 的代码如下所示:
1 | package even |
参考文章
- 本文作者: th3ee9ine
- 本文链接: https://www.blog.ajie39.top/2022/06/12/go入门总结(三)/
- 版权声明: 本博客所有文章除特别声明外,均采用 LICENSE 下的许可协议。转载请注明出处!