Golang学习之异常与文本文件处理

一、异常处理

1.1 error接口

error接口是Golang内建的接口类型,实现了一个错误处理的标准模式。返回一个不是致命的错误,主要起到一个提示作用。

1.1.1 error接口的基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import (
"errors"
"fmt"
)

func main() {
//fmt包中的Errorf函数
err1 := fmt.Errorf("%s", "this is error1.")
fmt.Printf("err1 type is : %T, err1=%+v\n", err1, err1)

//errors包中的New()函数
err2 := errors.New("this is error2.")
fmt.Printf("err2 type is : %T, err2=%+v\n", err2, err2)
}

/*
运行结果:
err1 type is : *errors.errorString, err1=this is error1.
err2 type is : *errors.errorString, err2=this is error2.
*/

1.1.2 error接口的应用

error接口一般用于程序发生错误时,给予提示信息的作用:
下例演示:当分母为0时,打印一个提示信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import (
"errors"
"fmt"
)

func MyDiv(a, b int) (result int, err error) { //error是接口类型,使用这个接口
err = nil

if b == 0 {
err = errors.New("分母不能为0")
} else {
result = a / b
}

return
}

func main() {
for i, j := 10, 2; j > -2; j-- {
result, err := MyDiv(i, j)

if err != nil {
fmt.Println("error:", err)
} else {
fmt.Println("result=", result)
}
}
}

/*
运行结果:
result= 5
result= 10
error: 分母不能为0
result= -10
*/

这个error只是打印一个信息,并不是程序报错,所以能够正常运行完成。

1.2 panic

当遇到不可恢复的错误状态的时候,如:数组访问越界、空指针引用等,这些运行时的致命错误,就会引发panic异常。
当panic异常引发时,程序就会中断运行。随后,程序崩溃并输出日志信息,日志信息包括panic value和函数调用的堆栈跟踪信息。
不是所有的panic异常都来自运行时(运行时发生了致命错误,程序内部会自动调用panic()函数,让程序中断并崩溃)。直接调用内建的panic()函数也会引发异常(手动引发异常)。panic()函数接收任意值作为参数。

1.2.1 人为引发panic

手动显式调用panic()函数,就是人为引发的异常,导致程序中断崩溃:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func testa() {
fmt.Println("aaaaaaaa")
}

func testb() {
//显式调用panic函数,引发异常,导致程序中断并且崩溃
panic("this is a panic test.")
}

func testc() {
fmt.Println("cccccccc")
}

func main() {
testa()
testb()
testc()
}

/*
运行结果:
aaaaaaaa
panic: this is a panic test.

goroutine 1 [running]:
main.testb(...)
F:/goProject/src/myTest/main.go:11
main.main()
F:/goProject/src/myTest/main.go:20 +0x9d

Process finished with exit code 2
*/

1.2.2 内部检测机制自动引发的panic

数组访问越界,内部自动引发异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func testa() {
fmt.Println("aaaaaaaa")
}

func testb() {
var a = [2]int{1, 2} //定义一个长度为2的数组
for i := 0; i < 3; i++ {
fmt.Println("testb, i=", a[i]) //当i=2的时候,数组访问越界,内部会自动检测到并引发panic
}
}

func testc() {
fmt.Println("cccccccc")
}

func main() {
testa()
testb()
testc()
}

1.3 recover

recover 是一个专用于”拦截”运行时 panic 的内建函数。当前程序运行时发生了 panicrecover 可以从 panic 的状态中恢复并重新获得流程控制权。简而言之:从 panic 的崩溃处恢复运行程序,它不会引发程序崩溃。
recover 有一个返回值:如果引发了 panic,则返回对应的错误信息;如果未引发 panic,则返回 nil
recover 必须在 defer 调用的函数中有效。

1.3.1 recover的基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func testa() {
fmt.Println("aaaaaaaa")
}

func testb() {
//recover()需要放在defer语句的函数调用中
defer func() {
//recover有一个返回值:当有panic时,返回对应的错误信息;没有panic时,返回nil
if err := recover(); err != nil { //没有panic时,返回nil。nil != nil 条件不成立,不会进if语句
fmt.Println(err) //打印错误信息,不要再写recover()了。if初始化语句中已经得到了错误信息并恢复了运行,因为后面没有任何panic的引发,所以再写recover()就会打印出nil
}
}() //匿名函数调用,满足defer语句中的函数调用,这一条件

var a = [2]int{1, 2} //定义一个长度为2的数组
for i := 0; i < 3; i++ {
fmt.Println("testb, i=", a[i]) //当i=2的时候,数组访问越界,内部会自动检测到并引发panic
}
}

func testc() {
fmt.Println("cccccccc")
}

func main() {
testa()
testb()
testc()
}

/*
运行结果:
aaaaaaaa
testb, i= 1
testb, i= 2
runtime error: index out of range [2] with length 2
cccccccc
*/

1.3.2 recover不能在defer中直接调用

recover不能在defer中直接调用,recover需要在defer调用的函数里才能生效。

1.3.3 recover正确的使用方式

defer后面接一个函数,在函数里写recover(),然后调用这个函数,才能使recover()生效。
例如,打印一下错误信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
defer func() {
fmt.Println(recover())
}() //调用匿名函数
123
```go

`recover()`放在匿名函数中,匿名函数当然是一个函数,而且它接着`defer`语句,最后加了一对小括号`()`完成对匿名函数的调用。

## 二、字符串处理

### 2.1 字符串常用操作

以下函数都是在`strings`包中,使用前必须先导入`strings`包!

#### 2.1.1 Contains函数

语法:`func Contains(s, substr string) bool`,验证字符串s中是否包含了substr
完全匹配,一个字符都不能差!
示例:

```go
func main() {
fmt.Println(strings.Contains("hello,go", "go"))
fmt.Println(strings.Contains("hello,go", "ol"))
}

/*
运行结果:
true
false
*/

2.1.2 Join函数

语法:func Join(a []string, sep string) string,字符串链接,把slice a通过sep链接起来。
示例:

1
2
3
4
5
6
7
8
9
10
func main() {
sli := []string{"go", "python", "rust"}
newStr := strings.Join(sli, "@")
fmt.Println("newStr=", newStr)
}

/*
运行结果:
newStr= [email protected]@rust
*/

2.1.3 Index函数

语法:func Index(s, sep string) int,在字符串s中查找sep的位置,返回sep的下标,找不到返回-1。
完全匹配,一个字符都不能差!返回的是第一个找到的下标!
示例:

1
2
3
4
5
6
7
8
9
10
func main() {
fmt.Println("index:", strings.Index("abchehe", "he"))
fmt.Println("index:", strings.Index("abchehe", "bd"))
}

/*
运行结果:
index: 3
index: -1
*/

2.1.4 Repeat函数

语法:func Repeat(s string, count int) string,重复s字符串count次,返回最终的字符串。
示例:

1
2
3
4
5
6
7
8
9
func main() {
newStr := strings.Repeat("go", 3)
fmt.Println("hello," + newStr)
}

/*
运行结果:
hello,gogogo
*/

2.1.5 Replace函数

语法:func Replace(s, old, new string, n int) string,在字符串s中,把old字符串替换为new字符串。n表示替换的次数,小于0表示全部替换。
如果old为空,则在字符串的开头和每个UTF-8序列之后添加n个new。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
fmt.Println(strings.Replace("hellogo", "o", "A", -1))
fmt.Println(strings.Replace("hellogo", "o", "", -1))
fmt.Println(strings.Replace("hellogo", "o", "", 1))
fmt.Println(strings.Replace("hellogo", "", "9", 1))
fmt.Println(strings.Replace("hellogo", "", "9", 3))
fmt.Println(strings.Replace("hellogo", "", "9", -1)) //old为空,n为-1,头部添加new,然后每个UTF-8字符后面再添加new
}

/*
运行结果:
hellAgA
hellg
hellgo
9hellogo
9h9e9llogo
9h9e9l9l9o9g9o9
*/

2.1.6 Split函数

语法:func Split(s, sep string) []string,把s字符串按照sep分割,返回slice。
大意:将s分割为所有由sep分隔的子字符串,并返回这些分隔符之间的子字符串的一部分。
如果s不包含sep且sep不为空,则Split返回长度为1的切片,其唯一元素为s。
如果sep为空,则Split在每个UTF-8序列之后拆分。
如果s和sep均为空,则Split返回一个空切片。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import (
"fmt"
"strings"
)

func main() {
s := "[email protected]@barry"
sli := strings.Split(s, "@")
fmt.Println(sli)

s = " 11-22 - 33 "
sli = strings.Split(s, " ") //以空格分割,sep之间有子字符串会被显示出来
fmt.Println(sli)

s = "a man a plan a"
sli = strings.Split(s, "a") //"a"之间的空格都会被显示出来
fmt.Println(sli)

s = "Anagod"
sli = strings.Split(s, "c")
fmt.Println(sli)

s = "我爱Golangひじょう"
sli = strings.Split(s, "") //根据UTF-8分割
fmt.Println(sli)

s = ""
sli = strings.Split(s, "")
fmt.Println(sli)
}

/*
运行结果:
[hello go barry]
[ 11-22 - 33 ]
[ m n pl n ]
[Anagod]
[我 爱 G o l a n g ひ じ ょ う]
[]
*/
*/

2.1.7 Trim函数

语法:func Trim(s string, cutset string) string,去除s字符串头尾的cutset。
示例:

1
2
3
4
5
6
7
8
9
10
func main() {
s := "@@@are u [email protected]@@"
sli := strings.Trim(s, "@")
fmt.Printf("sli=%+v\n", sli)
}

/*
运行结果:
sli=are u ok?
*/

2.1.8 Fields函数

语法:func Fields(s string) []string,去除字符串s中的空格,返回slice。
FieldsSplit很像,Split功能更加强大,它可以指定字符串。Fields不能指定,只能以空格分割。
示例:

1
2
3
4
5
6
7
8
9
10
func main() {
s := " are @ u # ok? "
sli := strings.Fields(s)
fmt.Println(sli)
}

/*
运行结果:
[are @ u # ok?]
*/

2.2 字符串转换

以下函数都是在strconv包中,使用前必须先导入strconv包!

2.2.1 Append系列

strconv.AppendXxxx(),将其他类型转换为字符串后,添加到现有的字节切片中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
//声明一个字节切片,声明切片需要制定len,cap可以省略
byteSlice := make([]byte, 0)

//strconv.AppendXxxx()系列都是返回[]byte,需要重新把追加后的新值,赋值给旧的变量,以覆盖旧的变量
byteSlice = strconv.AppendBool(byteSlice, true)

//第二个写int的值,第三个制定以什么进制去转换这个int
byteSlice = strconv.AppendInt(byteSlice, 123456, 10)
byteSlice = strconv.AppendInt(byteSlice, 123456, 16)
byteSlice = strconv.AppendInt(byteSlice, 123456, 2)

byteSlice = strconv.AppendQuote(byteSlice, "abchellogo")

fmt.Println("byteSlice=", string(byteSlice))
}

/*
运行结果:
byteSlice= true1234561e24011110001001000000"abchellogo"
*/

2.2.2 Format

将其他类型转换为字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
var str string

//'f'表示打印的格式。10表示小数精确到几位,-1表示使用最少数量的小数。64表示float64
str += strconv.FormatFloat(3.1415, 'f', 10, 64) //常用的是'f'格式

str += ";"

//整型转换为字符串
str += strconv.Itoa(999)

fmt.Println("str=", str)
}

/*
运行结果:
str= 3.1415000000;999
*/

strconv.FormatFloat()的用法:

1
func FormatFloat(f float64, fmt byte, prec, bitSize int) string

函数将浮点数表示为字符串并返回。

bitSize表示f的来源类型(32:float32、64:float64),会据此进行舍入。

fmt表示格式:’f’(-ddd.dddd)、’b’(-ddddp±ddd,指数为二进制)、’e’(-d.dddde±dd,十进制指数)、’E’(-d.ddddE±dd,十进制指数)、’g’(指数很大时用’e’格式,否则’f’格式)、’G’(指数很大时用’E’格式,否则’f’格式)。

prec控制精度(排除指数部分):对’f’、’e’、’E’,它表示小数点后的数字个数;对’g’、’G’,它控制总的数字个数。如果prec 为-1,则代表使用最少数量的、但又必需的数字来表示f。

2.2.3 Parse

把字符串转换为其他类型。
常用示例1:字符串转换为bool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
boolValue, err := strconv.ParseBool("true")

//err=nil的时候,说明没有error
if err == nil {
fmt.Printf("boolValue=%+v, type is :%T\n", boolValue, boolValue)
} else {
fmt.Println("err:", err)
}

//bool只有true和false,转换时除了首字母可以大写,其他任何一个字符写错都会返回error
boolValue, err = strconv.ParseBool("FalsE")
if err == nil {
fmt.Printf("boolValue=%+v, type is:%T\n", boolValue, boolValue)
} else {
fmt.Println("err:", err)
}
}

/*
运行结果:
boolValue=true, type is:bool
err: strconv.ParseBool: parsing "FalsE": invalid syntax
*/

常用示例2:字符串转换为int

1
2
3
4
5
6
7
8
9
func main() {
intValue, _ := strconv.Atoi("7890")
fmt.Printf("intValue=%d, type is:%T\n", intValue, intValue)
}

/*
运行结果:
intValue=7890, type is:int
*/

三、正则表达式

正则表达式是一种进行模式匹配和文本操纵(按指定的模式提取)的工具。Golang有内建的regexp标准包支持。
Golang实现的是RE2标准,除了\C,详细的语法参考:link
备注:反引号在``Golang中表示原始字符串。
提醒:能用strings包解决的,不要用正则。正则很复杂、很难写,效率也没有strings来得高。
详细使用方式请查看:link

3.1 正则表达式基本使用

1.引入regexp
2.定义一个解析(匹配)规则
3.使用这个规则进行提取

3.1.1 .匹配除换行符\n之外的任意字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import (
"fmt"
"regexp" //第1步:引入包
)

func main() {
s := "abc a9z . azc, apc;arc "

//第2步:定义解析器
reg1 := regexp.MustCompile(`a.c`) //反引号"``"在Golang中表示原始字符串

if reg1 == nil { //解析成功返回正则解析器,失败返回nil
fmt.Println("faild regexp.")
return //解析失败就结束函数,不然跳出if语句后,还会继续往下走
}

result := reg1.FindAllStringSubmatch(s, -1) //-1表示匹配所有

fmt.Println("result=", result)
}

/*
运行结果:
result= [[abc] [azc] [apc] [arc]]
*/

3.1.2 []的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import (
"fmt"
"regexp" //第1步:引入包
)

func main() {
s := "abc a9z . azc, a666c apc;arc "

//第2步:定义解析器
reg1 := regexp.MustCompile(`a\d[a-z]`)
//reg1 := regexp.MustCompile(`a[0-9][a-z]`) //等价于上一条语句

if reg1 == nil { //解析成功返回正则解析器,失败返回nil
fmt.Println("faild regexp.")
return //解析失败就结束函数,不然跳出if语句后,还会继续往下走
}

//分段方式返回结果
result := reg1.FindAllStringSubmatch(s, -1) //-1表示匹配所有

fmt.Println("result=", result)
}

/*
运行结果:
result= [[a9z]]
*/

3.1.3 +匹配前一个字符1次或多次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import (
"fmt"
"regexp"
)

func main() {
s := "3333.1414 567 agdsd 1.23 7. 08.99 1sdfasdf 6.66 ; 7.a1"

reg := regexp.MustCompile(`\d+\.\d+`)
if reg == nil {
fmt.Println("regexp failed.")
return //解析失败就结束函数,不然跳出if语句后,还会继续往下走
}

result := reg.FindAllString(s, -1)
fmt.Println("result=", result)
}

/*
运行结果:
result= [3333.1414 1.23 08.99 6.66]
*/

“08.99”是一个字符串,符合\d的规则,所以是完整显示出来了,并不是人类自己理解的0是无用数。

3.2 解析HTML代码的小示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import (
"fmt"
"regexp"
)

func main() {
//反引号``,表示原生的字符串
s := `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>Go语言标准库文档中文版 | Go语言中文网 | Golang中文社区 | Golang中国</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1">
<meta charset="utf-8">
<link rel="shortcut icon" href="/static/img/go.ico">
<link rel="apple-touch-icon" type="image/png" href="/static/img/logo2.png">
<meta name="author" content="polaris <[email protected]>">
<meta name="keywords" content="中文, 文档, 标准库, Go语言,Golang,Go社区,Go中文社区,Golang中文社区,Go语言社区,Go语言学习,学习Go语言,Go语言学习园地,Golang 中国,Golang中国,Golang China, Go语言论坛, Go语言中文网">
<meta name="description" content="Go语言文档中文版,Go语言中文网,中国 Golang 社区,Go语言学习园地,致力于构建完善的 Golang 中文社区,Go语言爱好者的学习家园。分享 Go 语言知识,交流使用经验">
</head>
<div>呵呵</div>
<div>哈哈哈</div>
<div>f**kf**kf**k</div>
<div>呵呵111111111</div>
<frameset cols="15,85">
<frame src="/static/pkgdoc/i.html">
<frame name="main" src="/static/pkgdoc/main.html" tppabs="main.html" >
<noframes>
</noframes>
</frameset>
</html>
`

//(.*)表示分组,我只要小括号中间这一部分的内容
//(?s:)表示组命令,s表示匹配换行符`\n`
reg := regexp.MustCompile(`<div>(?s:(.*?))</div>`)
if reg == nil {
fmt.Println("regexp failed.")
return
}
result := reg.FindAllStringSubmatch(s, -1) //会把 "<div>呵呵</div>" 和 "呵呵",都取出来
fmt.Println("result=", result)

fmt.Println("--------------------------------------------")

//查看一下切片中每个元素的构成情况
for _, text := range result {
fmt.Printf("text[0]=%v, text[1]=%v\n", text[0], text[1])
}

fmt.Println("--------------------------------------------")

//首尾的<div>标签不要,我只想要<div>呵呵</div>中间的内容 呵呵
cleanResult := []string{}
for _, text := range result {
cleanResult = append(cleanResult, text[1])
}

fmt.Println("cleanResult=", cleanResult)
}

/*
运行结果:
result= [[<div>呵呵</div> 呵呵] [<div>哈哈哈</div> 哈哈哈] [<div>f**kf**kf**k</div> f**kf**kf**k] [<div>呵呵111111111</div> 呵呵111111111]]
--------------------------------------------
text[0]=<div>呵呵</div>, text[1]=呵呵
text[0]=<div>哈哈哈</div>, text[1]=哈哈哈
text[0]=<div>f**kf**kf**k</div>, text[1]=f**kf**kf**k
text[0]=<div>呵呵111111111</div>, text[1]=呵呵111111111
--------------------------------------------
cleanResult= [呵呵 哈哈哈 f**kf**kf**k 呵呵111111111]
*/

四、json

json(JavaScript Object Notation)比 XML 更轻量级的数据交换格式,被广泛应用于 Web 服务端程序和客户端之间的数据通信。
其表现形式为键-值对集合的文本描述形式,跨平台、跨语言的数据交换格式。
Golang有内建的 encoding/json 包,来处理 json
json 官方网站:link(这是官方汉化版,英文原版把URL中的zh换成en)
在线格式化:link

4.1 编码json

编码 json 就是生成一个 json,将一些信息做成一个 json 文本格式。

4.1.1 使用结构体生成一个json

想要生成一个 json,结构体成员的变量名首字母必须大写,否则无法生成
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import (
"encoding/json" //引入标准包
"fmt"
)

type Company struct {
CompanyName string
Deps []string
IsOK bool
Price float64
unit string //成员变量名首字母小写,就不会生成到json中去
}

func main() {
//初始化结构体中的切片成员时,需要制定类型,不然报错:Missing type in composite literal ===> 复合文字中缺少类型
c1 := Company{"山景城Google公司", []string{"Search Engine", "Andriod", "Google Mail"}, true, 953.37, "Dollars"}

//编码,格式化编码,根据内容生成json文本
c1Byte, err := json.MarshalIndent(c1, "", "\t")
if err != nil {
fmt.Println("json marshal failed.")
return
}

fmt.Println(c1Byte)
fmt.Println(string(c1Byte)) //Marshal()返回切片字节,字节默认以ASCII码值打印,需要使用string()转换
fmt.Printf("type is : %T\n", c1Byte) //是个uint8的切片类型,byte的底层是用uint8实现
}

/*
运行结果:
[123 10 9 34 67 111 109 112 97 110 121 78 97 109 101 34 58 32 34 229 177 177 230 153 175 229 159 142 71 111 111 103 108 101 229 133 172 229 143 184 34 44 10 9 34 68 101 112 115 34 58 32 91 10 9 9 34 83 101 97 114 99 104 32 69 110 103 105 110 101 34 44 10 9 9 34 65 110 100 114 105 111 100 34 44 10 9 9 34 71 111 111 103 108 101 32 77 97 105 108 34 10 9 93 44 10 9 34 73 115 79 75 34 58 32 116 114 117 101 44 10 9 34 80 114 105 99 101 34 58 32 57 53 51 46 51 55 10 125]
{
"CompanyName": "山景城Google公司",
"Deps": [
"Search Engine",
"Andriod",
"Google Mail"
],
"IsOK": true,
"Price": 953.37
}
type is : []uint8
*/

结构体成员的变量名首字母必须大写,否则无法生成:
有没有生成成功可以把生成后的内容复制去json.cn网站上,能够出现正确的内容,则就生成成功了。

4.1.2 结构体编码json时的二次编码语法

在结构体数据类型后面加上:json:"Syntax"Syntax 为语法格式。
json:"newName",给这个字段起个别名,最终以这个 newName 的命名为准。
json:"-",表示不让这个字段输出,相当于不添加到 json 中去。
json:",string",表示以字符串格式输出。
注意:使用结构体生成 json,结构体中的成员变量名的首字母必须大写
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import (
"encoding/json"
"fmt"
)

type Company struct {
CompanyName string `json:"companyName"` //二次编码,最终会以这个二次编码的字段名companyName为准
Deps []string `json:"departments"` //就相当于起个别名
IsOK bool `json:"-"` //-表示这个字段不会输出
Price float64 `json:"price,string"` //指定以字符串格式输出
Unit string `json:"moneyUnit"` //如果成员变量名首字母还是小写,依然还是不会生成到json中去
}

func main() {
//初始化结构体中的切片成员时,需要制定类型,不然报错:Missing type in composite literal ===> 复合文字中缺少类型
c1 := Company{"山景城Google公司", []string{"Search Engine", "Andriod", "GMail"}, true, 953.37, "Dollars"}

//编码,格式化编码,根据内容生成json文本
c1Byte, err := json.MarshalIndent(c1, "", "\t")
if err != nil {
fmt.Println("json marshal failed.")
return
}

fmt.Println(string(c1Byte)) //Marshal()返回切片字节,字节默认以ASCII码值打印,需要使用string()转换
fmt.Printf("type is : %T\n", c1Byte) //是个uint8的切片类型,byte的底层是用uint8实现
}

/*
运行结果:
{
"companyName": "山景城Google公司",
"departments": [
"Search Engine",
"Andriod",
"GMail"
],
"price": "953.37",
"moneyUnit": "Dollars"
}
type is : []uint8
*/

4.1.3 使用map编码json

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import (
"encoding/json"
"fmt"
)

func main() {
//json中的值可能会有各种类型,解决这一问题:声明一个能够接收任意类型的万能空接口
m := make(map[string]interface{})
m["companyName"] = "山景城Google公司"
m["departments"] = []string{"Search Engine", "Andriod", "GMail"} //切片需要指明类型
m["IsOK"] = true
m["price"] = 953.37
m["moneyUnit"] = "Dollars"

//编码成json
mByte, err := json.MarshalIndent(m, "", "\t") //按一定的格式输出json
if err != nil {
fmt.Println("json marshal failed.")
return
}

fmt.Println(string(mByte))
}

/*
运行结果:
{
"IsOK": true,
"companyName": "山景城Google公司",
"departments": [
"Search Engine",
"Andriod",
"GMail"
],
"moneyUnit": "Dollars",
"price": 953.37
}
*/

4.2 解码json

解析一个 json 就是编码的反过程:已经有了一个现成的 json,从中提取出信息,并保存到结构体或者 map

4.2.1 解码json到结构体

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import (
"encoding/json"
"fmt"
)

type Company struct {
CompanyName string `json:"companyName"` //二次编码,起个别名。是为了跟原生json中的键名保持一致
Deps []string `json:"departments"` //二次编码,起个别名。是为了跟原生json中的键名保持一致
IsOK bool
Price float64 `json:"price"` //二次编码,起个别名。是为了跟原生json中的键名保持一致
Unit string `json:"moneyUnit"` //二次编码,起个别名。是为了跟原生json中的键名保持一致
}

func main() {
jsonBuf := `
{
"IsOK": true,
"companyName": "山景城Google公司",
"departments": [
"Search Engine",
"Andriod",
"GMail"
],
"moneyUnit": "Dollars",
"price": 953.37
}
`

//1.字段全部解析
var c1 Company
//jsonBuf解析到c1中去。要修改结构体中的值,需要引用传递。第二个参数必须要地址传递
err := json.Unmarshal([]byte(jsonBuf), &c1)
if err != nil {
fmt.Println("Unmarshal failed, error:", err)
return
}
fmt.Printf("c1 = %+v, type is : %T\n", c1, c1)

//2.只解析某几个字段
//重新声明一个结构体,写上想要的那几个字段。最终只会解码指定的这几个字段。注意:数据类型和名称依然要完全统一
type SimpleCompany struct {
CompanyName string `json:"companyName"`
Deps []string `json:"departments"`
}
var c2 SimpleCompany //声明这个新定义的结构体的变量
//jsonBuf解析到c2中去。要修改结构体中的值,需要引用传递。第二个参数必须要地址传递
err = json.Unmarshal([]byte(jsonBuf), &c2)
if err != nil {
fmt.Println("Unmarshal failed, error:", err)
return
}
fmt.Printf("c2 = %+v, type is : %T\n", c2, c2)
}

/*
运行结果:
c1 = {CompanyName:山景城Google公司 Deps:[Search Engine Andriod GMail] IsOK:true Price:953.37 Unit:Dollars}, type is : main.Company
c2 = {CompanyName:山景城Google公司 Deps:[Search Engine Andriod GMail]}, type is : main.SimpleCompany
*/

注意事项:
1.原本 json 中的字段名称是什么,结构体中也必须完全一样跟着写。但结构体中的变量名首字母必须大写,所以要用到二次编码格式。二次编码,起个别名,是为了让结构体中的字段名称与原生json中的键名保持一致。
2.Unmarshal() 函数是把第一个 json 文本的 byte 数据写入到第二个结构体变量中去。想要修改结构体中的值,就需要取结构体的地址。需要地址传递

4.2.2 解码json到map

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import (
"encoding/json"
"fmt"
)

func main() {
jsonBuf := `
{
"IsOK": true,
"companyName": "山景城Google公司",
"departments": [
"Search Engine",
"Andriod",
"GMail"
],
"moneyUnit": "Dollars",
"price": 953.37
}
`

//创建一个map
m := make(map[string]interface{})

err := json.Unmarshal([]byte(jsonBuf), &m) //第二个参数必须传内存地址
if err != nil {
fmt.Println("json unmarshal failed. err:", err)
return
}

fmt.Printf("%+v\n", m)

fmt.Println("---------------------------------------------------------------")

//想要从map中取值,就需要类型断言,一步步反推才能使用
//想要值到每一个内容,就必须通过for迭代,一个个类型断言反推,才能拿到值
var companyName string
var isOK bool
var price float64
var moneyUnit string
var departments string

for key, value := range m {
switch data := value.(type) { //value.(type)用来匹配下列case的各个数据类型;data是返回过来的值
case string:
//要取到对应键中的内容,则需要进一步处理
switch {
case key == "companyName":
companyName = data
case key == "moneyUnit":
moneyUnit = data
}
fmt.Printf("data type is string. key[%s]=%s\n", key, data)
case bool:
if key == "IsOK" {
isOK = data
}
fmt.Printf("data type is bool, key[%s]=%v\n", key, value)
case float64: //浮点型用float64才能匹配到
if key == "price" {
price = data
}
fmt.Printf("data type is float64, key[%s]=%f\n", key, value)
case []interface{}: //如果value本身是一个切片,需要用空接口(万能类型)来匹配
fmt.Printf("data type is []interface{}, key[%s]=%+v\n", key, value)
//如果要在切片中取值,则需要进一步地迭代,一个个取出来,一个个处理
for _, v := range data { //取出切片中,每个元素的下标和值
if s, ok := v.(string); ok { //判断这个值是否为string类型,是返回true,否返回false
departments += s + ";"
}
}
}
}

fmt.Printf("companyName=%s; departments=%s; isOK=%v; price=%f; moneyUnit=%s\n", companyName, departments, isOK, price, moneyUnit)
}

/*
运行结果:
map[IsOK:true companyName:山景城Google公司 departments:[Search Engine Andriod GMail] moneyUnit:Dollars price:953.37]
---------------------------------------------------------------
data type is bool, key[IsOK]=true
data type is string. key[companyName]=山景城Google公司
data type is []interface{}, key[departments]=[Search Engine Andriod GMail]
data type is string. key[moneyUnit]=Dollars
data type is float64, key[price]=953.370000
companyName=山景城Google公司; departments=Search Engine;Andriod;GMail;; isOK=true; price=953.370000; moneyUnit=Dollars
*/

4.2.3 对两种解析方式的简单总结

解析到 map,只是定义 map 那一段写起来很方便,想要取出里面的值,则需要迭代每一个元素,然后对每一个元素进行类型断言,反推每个数据类型。每个数据类型再单独进行处理。
解析到结构体,只是定义结构体的时候比较麻烦,需要指定每个字段的数据类型,二次编码的名称要跟原生的 json 文本中的键名一样。但是,一次定义就可以把所有的内容都解析进去,后续只需要结构体名称.字段名structVarName.fieldName)操作即可。

五、文件操作

Golang 有内建的 os 文件操作包。

5.1 文件分类

5.1.1 设备文件

平时最常用的有:

  • 屏幕(标准输出设备)
    最常用的一个函数:fmt.Println(),往标准输出设备写内容。
  • 键盘(标准输入设备)
    fmt.Scan(),从标准输入设备读取内容。

5.1.2 磁盘文件

放在存储设备上的文件。
还可以分为两类:

  • 文本文件:能够用记事本(Notpad++)打开,且不是乱码的就是文本文件。
  • 二进制文件:用记事本打开后,看到乱码的就是二进制文件。

5.2 文件操作的相关api

5.2.1 新建文件

如果文件不存在则新建,如果文件已存在则会清空原文件
第1种方式:func Create(name string) (file *File, err Error),根据提供的文件名(或路径+文件名)创建新的文件,返回一个可读可写的文件指针,默认权限是 0666。内部调用的是 OpenFile()
第2种方式:func New(fd uintptr, name string) *File,根据文件描述符创建相应的文件,返回一个文件指针。

5.2.2 打开文件

第1种方式:func Open(name string) (file *File, err Error),以只读方式打开一个名称为 name 的文件。内部实现调用了OpenFile()
第2种方式:func OpenFile(name string, flag int, perm uint32) (file *File, err Error),打开名称为name的文件,flag是打开的方式(如:只读、只写等),perm是权限。

5.2.3 写文件

第1种方式:func (file *File) Write(b []byte) (n int, err Error),写入byte到文件。可处理二进制和非二进制文件。
第2种方式:func (file *File) WriteAt(b []byte, off int64) (n int, err Error),从off位置开始写入byte。
第3种方式:func (file *File) WriteString(s string) (ret int, err Error),写入string到文件。只能处理文本文件(非二进制文件),它只会往里面写字符串。

5.2.4 读文件

第1种方式:func (file *File) Read(b []byte) (n int, err Error),读取数据到byte切片中。
第2种方式:func (file *File) ReadAt(b []byte, off int64) (n int, err Error),从off位置开始读取数据到byte切片中。

5.2.5 删除文件

第1种方式:func Remove(name string) Error,删除文件名为name的文件。

5.3 标准输出设备文件的使用

5.3.1 往标准输出设备写内容

标准输出设备通常指的是屏幕,Golang 默认是往屏幕写内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import (
"fmt"
"os"
)

func main() {
fmt.Println("hello world.")
os.Stdout.Close() //把输出设备给关了,关闭文件后,无法再输出
fmt.Println("hello go.")
}

/*
运行结果:
hello world.

Process finished with exit code 0
*/

还可以使用 os.Stdout.WriteString(s string) 往设备输出内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import (
"fmt"
"os"
)

func main() {
os.Stdout.WriteString("hello world.\n") //需手动换行
var words = []byte{'a', 'e', '\t', 'i', 'o', '\t', 'u', '\n'}
os.Stdout.Write(words)
fmt.Println(words)
}

/*
运行结果:
hello world.
ae io u
[97 101 9 105 111 9 117 10]

Process finished with exit code 0
*/

5.4 写文件的操作

5.4.1 WriteString()的使用

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import (
"fmt"
"os"
)

func WriteFile(path string) {
//1.打开文件(新建文件)
f, err := os.Create(path)
if err != nil {
fmt.Println("create file failed. err:", err)
return //有错误就结束函数,不让它往下走了
}

//使用完毕,必须关闭文件
defer f.Close() //函数调用完毕,即表示文件使用完毕了

//2.往里面写内容
var s string
for i := 0; i < 10; i++ {
s = fmt.Sprintf("这是第%d行!\n", i)
n, err := f.WriteString(s)
if err != nil {
fmt.Printf("write %d line err:%+v\n", i, err)
} else {
fmt.Printf("wrote %d words.\n", n) //以byte类型来计算长度
}
}
}

func main() {
path := "./demo.txt"
WriteFile(path)
}

/*
运行结果:
wrote 17 words.
wrote 17 words.
wrote 17 words.
wrote 17 words.
wrote 17 words.
wrote 17 words.
wrote 17 words.
wrote 17 words.
wrote 17 words.
wrote 17 words.
*/

5.5 读文件的操作

5.5.1 Read()的使用

直接使用已在5.4.1示例中生成的demo.txt。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import (
"fmt"
"io"
"os"
)

func ReadFile(path string) {
//1.打开文件
f, err := os.Open(path)
if err != nil {
fmt.Println("open file failed. err=", err)
return
}

//使用完毕,关闭文件
defer f.Close()

//2.读取内容
buf := make([]byte, 2048)
//Read([]byte)中的参数需要是一个byte类型的切片
n, err := f.Read(buf) //n表示实际一共读取了多少字节
//EOF:end of file,表示文件的正常结束。文件已经正常结束了,说明整个文件都读取完整了。
//既然整个文件都读取完整了,并且正常结束了,那么何来的错误?
//EOF是表示文件正常结束,而不是错误。所以需要再判断一下,错误并不是EOF
if err != nil && err != io.EOF {
fmt.Println("read content failed. err=", err)
return
}
fmt.Printf("has read %d words.\n", n)

fmt.Println(string(buf[:n])) //[:n]指定取多少个字节。如果说读取了很多字节,但实际上没有那么多字节。剩下的都会以空字符形式来表现,显示上不够友好
}

func main() {
path := "./demo.txt"
ReadFile(path)
}

/*
运行结果:
has read 170 words.
这是第0行!
这是第1行!
这是第2行!
这是第3行!
这是第4行!
这是第5行!
这是第6行!
这是第7行!
这是第8行!
这是第9行!
*/

5.5.2 Read(b []byte),[]byte的长度表示去读取多少个字节

上例中,声明切片时,长度指定了 2048 个字节。那么就表示,去文件中读取 2048 个字节。
下图这个反面教材中的指定了切片长度为 0,那么就表示,去文件中读取 0 个字节。

1
buf := make([]byte, 0)	//这个切片长度是为了给后面的Read()。指定多少长度,就去读取多少个字节。长度指定0,表示去读取0个字节!读取0个字节,就等于什么都不去读取。

指定了只读取 90 个字节,那么程序只会去读取 90 个字节,剩下的内容都不会去读取。

指定去读取 999 个字节,但文件中只有 170 个字节。指定的读取数量超过了文件中的总内容,那么剩余的每一个字节就会以空字符来显示。

5.5.3 根据文件总大小去读取内容

既然大了也不是,小了也不行。那么就先得到这个文件的总大小,然后指定这个总大小的数字去读取内容。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import (
"fmt"
"io"
"os"
)

func ReadFile(path string) {
f, err := os.Open(path)
if err != nil {
fmt.Println("open file failed. err=", err)
return
}

//尝试获取文件的大小
//f.Stat()返回一个接口,FileInfo类型中有一个Size()的方法可以获取到文件的总大小
fi, err := f.Stat()
if err != nil && err != io.EOF {
fmt.Println("get file stat failed. err=", err)
return
}
fsize := fi.Size() //获取到文件的总大小
fmt.Println("this file size is :", fsize)

defer f.Close() //使用完毕,关闭文件

buf := make([]byte, fsize) //文件总大小有多少,那么就指定去读取多少
n, err := f.Read(buf)
if err != nil && err != io.EOF {
fmt.Println("read content failed. err=", err)
return
}
fmt.Printf("has read %d bytes.\n", n)

fmt.Println(string(buf)) //不用再写[:n]了,因为已经读取了跟文件总大小完全匹配的全部内容
}

func main() {
path := "./demo.txt"
ReadFile(path)
}

/*
运行结果:
this file size is : 170
has read 170 bytes.
这是第0行!
这是第1行!
这是第2行!
这是第3行!
这是第4行!
这是第5行!
这是第6行!
这是第7行!
这是第8行!
这是第9行!
*/

注意:获取文件总大小并按这个长度去读取,属于一次性读取整个文件中的内容。小文本可以这么使用,但是大文本这么全部读取到内存中,会占用比较大的内存资源从而导致影响性能。
该解决方案从这里看来的:link

5.5.4 os.Stat() 获取文件的元数据信息

上面的案例中,因为需要读取出文件中的内容。所以采用的策略是:先打开这个文件,有了这个文件指针后,通过这个指针变量去获取该文件的元数据信息。
如果只想要获取文件的元数据信息,而不打开文件,就可以使用 os.State() 方法。它可以在不打开文件的情况下,获取到文件的元数据信息。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import (
"fmt"
"os"
)

func main() {
fi, err := os.Stat("1.mp4")
if err != nil {
panic(err)
}

fmt.Println("file name:", fi.Name())
fmt.Println("file size:", fi.Size())
fmt.Println("is dir?", fi.IsDir())
fmt.Println("file mode:", fi.Mode())
fmt.Println("file modTime:", fi.ModTime())
}


/*
运行结果:
file name: 1.mp4
file size: 8021539
is dir? false
file mode: -rw-rw-rw-
file modTime: 2020-09-13 18:14:06.889 +0800 CST
*/

5.6 逐行读取内容

需要借助 Golang 内建包 bufio,它是带缓存的 I/O。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import (
"bufio"
"fmt"
"io"
"os"
)

func ReadFileByLine(path string) {
f, err := os.Open(path)
if err != nil {
fmt.Println("open file failed. err:", err)
return
}

defer f.Close()

//新建一个缓冲区,把内容先放在缓冲区
r := bufio.NewReader(f)

for {
//遇到'\n'结束读取,但'\n'同时也被读取了
buf, err := r.ReadBytes('\n')
if err != nil {
if err == io.EOF { //文件已正常结束
break
} else {
fmt.Println("read bytes err:", err)
}
}

fmt.Printf("buf = #%s#", string(buf)) //井号前后都没有加过换行符,但是ReadBytes()会把指定的字符也给读取了
}

}

func main() {
path := "./demo.txt"

ReadFileByLine(path)
}

/*
运行结果:
buf = #这是第0行!
#buf = #这是第1行!
#buf = #这是第2行!
#buf = #这是第3行!
#buf = #这是第4行!
#buf = #这是第5行!
#buf = #这是第6行!
#buf = #这是第7行!
#buf = #这是第8行!
#buf = #这是第9行!
#
*/

5.7 文件案例:拷贝文件

Read() 读取,Write() 写入。
设计思路:使用命令行实现功能。

5.7.1 参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import (
"fmt"
"io"
"os"
)

//获取命令行参数
func GetCMDArgs() []string {
cmdArry := os.Args

if len(cmdArry) != 3 {
fmt.Println("command line error. except: binaryName.exe srcFileName dstFileName")
os.Exit(-1)
}

return cmdArry
}

//获取文件名,分别是源文件名和目标文件名
func GetFilenames(cmdArry []string) (srcFileName, dstFileName string) {
srcFileName = cmdArry[1]
dstFileName = cmdArry[2]

if srcFileName == dstFileName {
fmt.Println("The destination file name cannot be the same as the source file name.")
os.Exit(-1)
}

return
}

//核心功能,实现文件的拷贝
func ExecuteCopy(srcFileName, dstFileName string) {
buf := make([]byte, 1024*5) //切片缓冲区,以多少个字节去读取文件中的内容。1024Byte = 1KB

//打开源文件
srcF, err := os.Open(srcFileName)
if err != nil {
fmt.Println("open source file failed. error:", err)
os.Exit(-1)
}
defer srcF.Close() //关闭文件

//创建目标文件
dstF, err := os.Create(dstFileName)
if err != nil {
fmt.Println("create destination file failed. error:", err)
os.Exit(-1)
}
defer dstF.Close() //关闭文件

for {
n, err := srcF.Read(buf)
if err != nil {
if err == io.EOF { //文件已到末尾,正常结束
break
}
}

dstF.Write(buf[:n]) //buf[:n]表示读多少写多少,到了最后的时候,往往读不全指定的字节数据量。如果还是写死为buf,超出部分将会是空字节内容
}
}

func main() {
cmdArry := GetCMDArgs()
srcFileName, dstFileName := GetFilenames(cmdArry)
ExecuteCopy(srcFileName, dstFileName)
}

5.7.2 buf[:n],表示读多少写多少

5.7.1 示例代码中,ExecuteCopy() 函数的倒数第二行代码:dstF.Write(buf[:n])。它表示读取到多少内容,就写入多少内容。
这个 buf 是可以自行指定,每次读取多少个字节。

5.7.2.1 不采用读多少写多少模式所引发的问题

修改 for 循环中的代码,抛弃读多少写多少的模式,指定了每次就是一股脑地往目标文件中写入 1024*5 个字节:

1
2
3
4
5
6
7
8
9
10
for {
_, err := srcF.Read(buf) // [:n] 被抛弃了
if err != nil {
if err == io.EOF { // 文件已到末尾,正常结束
break
}
}

dstF.Write(buf) //每次写入5120个字节,不管实际是不是有那么多数据。数据不够会产生空字节
}

再次编译运行代码后,可以看到。如果不写 buf[:n],那么最后就会有几个空字节多出来。
直观看起来,就是目标文件比源文件大出一点来.

5.7.2.2 大致原理

源文件总大小是 17047119 个字节,每次读取 10245 个字节,需要 3329.5 次才能把整个文件读取完整。
那么前 3329 次是没有问题的,一共能够读取到 17044480 个字节。当最后一次读取数据的时候,只剩下 2639 个有效字节了,只能读取到这 2639 个有效字节。但是写入数据的时候,它是以 buf 指定的字节量去写入数据。buf 指定的是 1024
5 个字节去写入,但最后只读到了 2639 个字节,而你却要写入 5120 个字节。既然你诚心诚意想要写入 5120 个字节,那么 Golang 就满足你的意愿,它很忠诚地往目标文件中写入了 5120 个字节。指定要去写入 5120 个字节,但却只有 2639 个有效字节,那么剩余的 2481 个字节只能用空字节来占位。(5120 - 2639 = 2481)
目标文件总大小是 17049600 个字节,源文件大小是 17047119 个字节,(17049600 - 17047119 = 2481)目标文件比源文件多出了 2481 个字节。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!