Go语言基础学习(五)

Go 语言中的错误和异常处理

1.error 接口

error 是系统自带的一个接口类型的自定义类型。

1
2
3
type error interface {
Error() string
}

2.errors 包

errors 包中包含了一个实现 error 这个接口的结构体类型 errorString。可以直接使用包中的方法来自定义一些错误,从而返回希望被上层代码处理的错误信息

试例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"errors"
"fmt"
)

func main() {
t, err := divide(2, 0)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(t)
}
}

func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除数不能为0")
}
return a / b, nil
}

3.异常处理

如果调用的方法的开发者不够仔细,并没有将所有的异常都考虑到并作为错误返回,那么程序可能就会被其影响而崩溃,GO 语言提供了一个叫recover()的函数,用于处理这种问题。一般常用于服务启动的入口函数,因为网络等外部因素,极有可能会导致程序异常,这些异常就需要这个函数来捕获。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
)

func main() {
defer func() {
err := recover()
if err != nil {
fmt.Println("程序运行中出现异常:", err)
}
}()
t := divide(2, 0)
fmt.Println(t)
}

func divide(a, b int) int {
return a / b
}

Go 语言的并发

Go 语言中的多线程操作是其语言的一大特色,它具有其它语言无法比拟的,可以近乎无限开启的线程。在 Go 语言中被称之为 goroutine ,它是线程的轻量级实现

1.Go 语言的 goroutine

在 Go 语言中使用 go 关键字来创建 goroutine ,形如go 函数名()的形式去创建。每一个 goroutine 必须是一个函数,这个函数也可以是匿名函数

代码

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
package main

import (
"fmt"
"time"
)

func main() {
//打印0到10的数字
go print0to10()
//打印A到Z的字符
go func() {
for i := 'A'; i <= 'K'; i++ {
fmt.Println("printAtoK:", string(i))
time.Sleep(time.Microsecond)
}
}()
time.Sleep(time.Second)
}

func print0to10() {
for i := 0; i <= 10; i++ {
fmt.Println("print0to10:", i)
time.Sleep(time.Microsecond)
}
}

执行结果
image.png
从执行结果中可以看出打印数字和打印字符的两个 goroutine 是并发执行的。执行顺序是由 cpu 来调度的,所以执行结果可能每次都不一样。

2. Go语言并发通讯

其它语言并发时进程中的通讯一般都是通过共享内存(全局变量)的方式来实现的,这样一来各个模块之间的耦合会变得非常紧密。所以后来提出了使用通讯来共享内存这一概念,来解耦合。在 Go 语言中就是使用 channel 的方式来达到这一目的的。

代码实例

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
package main

import (
"fmt"
"time"
)

var c1 chan rune = make(chan rune, 0)
var c2 chan int = make(chan int, 0)

func main() {
//打印0到10的数字
go print0to10()
//打印A到Z的字符
go func() {
c2 <- 0
for i := 1; i <= 11; i++ {
char := <-c1
fmt.Println("printAtoK:", string(char))
c2 <- i
}
}()
time.Sleep(time.Second)
}

func print0to10() {
for i := 'A'; i <= 'K'; i++ {
num := <-c2
fmt.Println("print0to10:", num)
c1 <- i
}
}

输出结果
image.png
和没用使用 channel 之前的代码不同,这次等同于使用 channel 实现了 goroutine 的调度,使其轮流执行。

Go语言进程锁

之所以线程不安全是因为其内部实现机制中无法同时读写,若有两个 goroutine 一个在读取 map 中的值,而另一个在更新 map 中的值,就会导致程序崩溃。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"time"
)

func main() {
m := map[string]int{"A": 1, "B": 2, "C": 3, "D": 1, "E": 2, "F": 3}
//创建100个goroutine对map进行读写
for i := 0; i < 100; i++ {
go func() {
for v := range m {
m[v] = 100
}
}()
}
time.Sleep(time.Second)
fmt.Println(m)
}

执行上述代码有时会输出正确结果:

image-20230822201432973

但更多的时候会输出读写冲突,就是线程不安全的 map 不建议使用的原因,除了直接使用线程安全的 map 之外,还可以为这些 goruntine 加上锁,使其无法同时对 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
package main

import (
"fmt"
"sync"
"time"
)

func main() {
var lock sync.Mutex //定义一个锁变量
m := map[string]int{"A": 1, "B": 2, "C": 3, "D": 1, "E": 2, "F": 3}
// 创建100个goroutine对map进行读写
for i := 0; i < 100; i++ {
go func() {
lock.Lock() // 在读取map前锁定这个锁,使其它线程访问这个锁要阻塞
for v := range m {
m[v] = 100
}
lock.Unlock() //在读取map前释放这个锁
}()
}
time.Sleep(time.Second)
fmt.Println(m)
}

加了锁之后,无论多少次的执行结果都是正确的。

Go语言的包

和大部分语言一致,Go 语言也是支持调用其它文件中的 Go 代码的。Go 语言是现这一功能则是通过包来实现的。而实现包的功能则是通过关键字 package 来实现的,使用 package 来声明代码所在的包。

1. 基本概念

Go 语言中一般使用文件所在的文件名来命令自己的包名,这样层次清晰便于管理。其中包名定义的注意事项如下:

  • 包名一般都是全小写
  • 包名不可以是路径
  • 一个文件夹下只能有一个包名
  • 每个程序的入口包名均为 main ,只有有main包,程序才可以执行

2. 多行导入

多行导入的方式有点类似于多个常量同时定义。

1
2
3
4
import (
firstgo/packagetest1
firstgo/packagetest2
)

3. 使用规则

在 Go 语言中,并不是直接导入了你写的一个包,你就可以直接使用包中的任何代码。这里有一个类似于Java和.NET的私有和公有的概念,你只可以使用导入的包中的公有的函数、变量以及自定义类型。Go 语言中区分公有私有的方法很简单,就是查看函数名、变量名以及自定义类型名的首字母是否为大写,大写的即为公有,小写的为私有

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
package packagetest1

import "fmt"

//定义私有变量name
var name = "Test1"

//PrintName 打印变量name
func PrintName() {
fmt.Println("packagetest1 Name:", name)
}
package packagetest2

import "fmt"

//定义私有变量name
var name = "Test2"

//PrintName 打印变量name
func PrintName() {
fmt.Println("packagetest2 Name:", name)
}
package main

import (
"firstgo/packagetest1"
"firstgo/packagetest2"
)

func main() {
packagetest1.PrintName()//调用这个包中的PrintName()函数
packagetest2.PrintName()//调用这个包中的PrintName()函数
}

Go 语言 go mod 包依赖管理工具

1. 指令介绍

控制台输入

1
go mod

image-20230826212609479

指令解析如下表:

指令 功能
go mod download 下载依赖包到本地(默认下载目录为 $GOPATH/pkg/mod)
go mod edit 编辑 go.mod 文件(不常用)
go mod graph 输出go mod的依赖图
go mod init 初始化一个mod项目,即在包中示创建一个go.mod文件
go mod tidy 自动补全新增(删除减少)的依赖包
go mod verify 校验依赖包
go mod why 解释为什么要依赖,即显示这个包在哪里被导入了(不常用)

2. go mod 中使用 replace

replace 是用于使用本地包来替换导入包的一个操作,毕竟并不是所有导入包都能够下载的到,也有一些是你自己开发,并未上传到网络中的代码,这时候如果要在 go mod 中导入就需要 replace 了。

使用代码如下:

1
2
3
4
5
6
7
8
9
10
module go_study

go 1.20

replace (
//包导入名 => 本地包相对路径/绝对路径
go_study/packagetest1 => ./packagetest1
go_study/packagetest2 => ./packagetest2
)

3. 在项目中使用

任意文件夹中创建Go文件,然后录入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "github.com/gin-gonic/gin"

func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Hello Codey!",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
}

打开控制台输入

1
2
go mod init
go mod tidy

这样就会开始下载依赖包

image-20230826213048786

最后项目文件夹里面会多出go.mod 和 go.sum ,其中 go.sum 不用在意,其中的内容为导入包的路径、版本和它的hash ,是自动生成自动更新的。

查看 go.mod会发现文件如下:

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
module go_study

go 1.20

require github.com/gin-gonic/gin v1.9.1

require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

自动记录了引用的包路径和版本。如果要修改使用版本,直接修改版本号即可。](<—
​—
tag: [“Blog/waiting”, “Go”]
genre: “博客”
​—


Go 语言中的错误和异常处理

1.error 接口

error 是系统自带的一个接口类型的自定义类型。

1
2
3
type error interface {
Error() string
}

2.errors 包

errors 包中包含了一个实现 error 这个接口的结构体类型 errorString。可以直接使用包中的方法来自定义一些错误,从而返回希望被上层代码处理的错误信息

试例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"errors"
"fmt"
)

func main() {
t, err := divide(2, 0)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(t)
}
}

func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除数不能为0")
}
return a / b, nil
}

3.异常处理

如果调用的方法的开发者不够仔细,并没有将所有的异常都考虑到并作为错误返回,那么程序可能就会被其影响而崩溃,GO 语言提供了一个叫recover()的函数,用于处理这种问题。一般常用于服务启动的入口函数,因为网络等外部因素,极有可能会导致程序异常,这些异常就需要这个函数来捕获。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
)

func main() {
defer func() {
err := recover()
if err != nil {
fmt.Println("程序运行中出现异常:", err)
}
}()
t := divide(2, 0)
fmt.Println(t)
}

func divide(a, b int) int {
return a / b
}

Go 语言的并发

Go 语言中的多线程操作是其语言的一大特色,它具有其它语言无法比拟的,可以近乎无限开启的线程。在 Go 语言中被称之为 goroutine ,它是线程的轻量级实现

1.Go 语言的 goroutine

在 Go 语言中使用 go 关键字来创建 goroutine ,形如go 函数名()的形式去创建。每一个 goroutine 必须是一个函数,这个函数也可以是匿名函数

代码

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
package main

import (
"fmt"
"time"
)

func main() {
//打印0到10的数字
go print0to10()
//打印A到Z的字符
go func() {
for i := 'A'; i %3C= 'K'; i++ {
fmt.Println("printAtoK:", string(i))
time.Sleep(time.Microsecond)
}
}()
time.Sleep(time.Second)
}

func print0to10() {
for i := 0; i <= 10; i++ {
fmt.Println("print0to10:", i)
time.Sleep(time.Microsecond)
}
}

执行结果
image.png
从执行结果中可以看出打印数字和打印字符的两个 goroutine 是并发执行的。执行顺序是由 cpu 来调度的,所以执行结果可能每次都不一样。

2. Go语言并发通讯

其它语言并发时进程中的通讯一般都是通过共享内存(全局变量)的方式来实现的,这样一来各个模块之间的耦合会变得非常紧密。所以后来提出了使用通讯来共享内存这一概念,来解耦合。在 Go 语言中就是使用 channel 的方式来达到这一目的的。

代码实例

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
package main

import (
"fmt"
"time"
)

var c1 chan rune = make(chan rune, 0)
var c2 chan int = make(chan int, 0)

func main() {
//打印0到10的数字
go print0to10()
//打印A到Z的字符
go func() {
c2 <- 0
for i := 1; i <= 11; i++ {
char := <-c1
fmt.Println("printAtoK:", string(char))
c2 <- i
}
}()
time.Sleep(time.Second)
}

func print0to10() {
for i := 'A'; i <= 'K'; i++ {
num := <-c2
fmt.Println("print0to10:", num)
c1 <- i
}
}

输出结果
image.png
和没用使用 channel 之前的代码不同,这次等同于使用 channel 实现了 goroutine 的调度,使其轮流执行。

Go语言进程锁

之所以线程不安全是因为其内部实现机制中无法同时读写,若有两个 goroutine 一个在读取 map 中的值,而另一个在更新 map 中的值,就会导致程序崩溃。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"time"
)

func main() {
m := map[string]int{"A": 1, "B": 2, "C": 3, "D": 1, "E": 2, "F": 3}
//创建100个goroutine对map进行读写
for i := 0; i < 100; i++ {
go func() {
for v := range m {
m[v] = 100
}
}()
}
time.Sleep(time.Second)
fmt.Println(m)
}

执行上述代码有时会输出正确结果:

image-20230822201432973

但更多的时候会输出读写冲突,就是线程不安全的 map 不建议使用的原因,除了直接使用线程安全的 map 之外,还可以为这些 goruntine 加上锁,使其无法同时对 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
package main

import (
"fmt"
"sync"
"time"
)

func main() {
var lock sync.Mutex //定义一个锁变量
m := map[string]int{"A": 1, "B": 2, "C": 3, "D": 1, "E": 2, "F": 3}
// 创建100个goroutine对map进行读写
for i := 0; i < 100; i++ {
go func() {
lock.Lock() // 在读取map前锁定这个锁,使其它线程访问这个锁要阻塞
for v := range m {
m[v] = 100
}
lock.Unlock() //在读取map前释放这个锁
}()
}
time.Sleep(time.Second)
fmt.Println(m)
}

加了锁之后,无论多少次的执行结果都是正确的。

Go语言的包

和大部分语言一致,Go 语言也是支持调用其它文件中的 Go 代码的。Go 语言是现这一功能则是通过包来实现的。而实现包的功能则是通过关键字 package 来实现的,使用 package 来声明代码所在的包。

1. 基本概念

Go 语言中一般使用文件所在的文件名来命令自己的包名,这样层次清晰便于管理。其中包名定义的注意事项如下:

  • 包名一般都是全小写
  • 包名不可以是路径
  • 一个文件夹下只能有一个包名
  • 每个程序的入口包名均为 main ,只有有main包,程序才可以执行

2. 多行导入

多行导入的方式有点类似于多个常量同时定义。

1
2
3
4
import (
firstgo/packagetest1
firstgo/packagetest2
)

3. 使用规则

在 Go 语言中,并不是直接导入了你写的一个包,你就可以直接使用包中的任何代码。这里有一个类似于Java和.NET的私有和公有的概念,你只可以使用导入的包中的公有的函数、变量以及自定义类型。Go 语言中区分公有私有的方法很简单,就是查看函数名、变量名以及自定义类型名的首字母是否为大写,大写的即为公有,小写的为私有

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
package packagetest1

import "fmt"

//定义私有变量name
var name = "Test1"

//PrintName 打印变量name
func PrintName() {
fmt.Println("packagetest1 Name:", name)
}
package packagetest2

import "fmt"

//定义私有变量name
var name = "Test2"

//PrintName 打印变量name
func PrintName() {
fmt.Println("packagetest2 Name:", name)
}
package main

import (
"firstgo/packagetest1"
"firstgo/packagetest2"
)

func main() {
packagetest1.PrintName()//调用这个包中的PrintName()函数
packagetest2.PrintName()//调用这个包中的PrintName()函数
}

Go 语言 go mod 包依赖管理工具

1. 指令介绍

控制台输入

1
go mod

image-20230826212609479

指令解析如下表:

指令 功能
go mod download 下载依赖包到本地(默认下载目录为 $GOPATH/pkg/mod)
go mod edit 编辑 go.mod 文件(不常用)
go mod graph 输出go mod的依赖图
go mod init 初始化一个mod项目,即在包中示创建一个go.mod文件
go mod tidy 自动补全新增(删除减少)的依赖包
go mod verify 校验依赖包
go mod why 解释为什么要依赖,即显示这个包在哪里被导入了(不常用)

2. go mod 中使用 replace

replace 是用于使用本地包来替换导入包的一个操作,毕竟并不是所有导入包都能够下载的到,也有一些是你自己开发,并未上传到网络中的代码,这时候如果要在 go mod 中导入就需要 replace 了。

使用代码如下:

1
2
3
4
5
6
7
8
9
10
module go_study

go 1.20

replace (
//包导入名 =%3E 本地包相对路径/绝对路径
go_study/packagetest1 => ./packagetest1
go_study/packagetest2 => ./packagetest2
)

3. 在项目中使用

任意文件夹中创建Go文件,然后录入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "github.com/gin-gonic/gin"

func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Hello Codey!",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
}

打开控制台输入

1
2
go mod init
go mod tidy

这样就会开始下载依赖包

image-20230826213048786

最后项目文件夹里面会多出go.mod 和 go.sum ,其中 go.sum 不用在意,其中的内容为导入包的路径、版本和它的hash ,是自动生成自动更新的。

查看 go.mod会发现文件如下:

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
module go_study

go 1.20

require github.com/gin-gonic/gin v1.9.1

require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

自动记录了引用的包路径和版本。如果要修改使用版本,直接修改版本号即可。>)


Go语言基础学习(五)
https://suiyideali.github.io/2023/08/26/Go语言基础学习(五)/
作者
m0ch4z
发布于
2023年8月26日
更新于
2023年9月11日
许可协议