Go语言中的错误处理和其他语言不一样,它把错误当成一种值来处理,更强调判断错误,处理错误,而不是一股脑的catch错误。
Go语言中把错误当成一种特殊的值来处理,不支持其他语言的try/catch
捕获异常的方式。
type error interface {
Error() string
}
error
接口只包含一个方法----Error
,这个函数需要返回一个描述错误信息的字符串。
当一个函数或者方法需要返回错误时,我们通常是把错误作为最后一个返回值。例如下面标准库os打开文件的函数。
func Open(name string)(*File, error){
return OpenFile(name,O_RDONLY,0)
}
由于error是一个接口类型,默认零值为nil
。所以我们通常将调用函数返回的错误与nil
进行比较,以此来判断函数书否返回错误。例如你经常看到类似下面的错误判断代码。
file, err:= os.Open("./xxx.txt")
if err!=nil{
fmt.PrintLn("打开文件失败,err: ",err)
return
}
注意
当我们使用fmt
包打印错误时,会自动调用error类型的Error方法,也就是会打印出错误的描述信息。
我们可以根据需求自定义error,最简单的方式就是使用errors
包提供的New函数创建一个错误。
函数签名如下:
func New(text string) error
它接受一个字符串参数返回包含该字符串的错误。我们可以残函数返回时快速创建一个错误。
func queryById(id int64)(*Info,error){
if id<=0{
return nil,errors,New("无效的ID")
}
//。。。
}
当我们需要传入格式化的错误描述信息时,使用fmt.Errorf
是个更好的选择。
fmt.Errorf("查询数据库失败,err:%v",err)
但是上面的方式会丢失原有的错误类型,只拿到错误描述的文本信息。
为了不丢失函数调用的错误链,使用fmt.Errorf
时搭配使用特殊的格式化动词%w
,可以实现基于已有的错误再包装得到一个新的错误。
fmt.Errorf("查询数据库失败,err:%w", err)
errors提供了以下三个方法:
//解包
oErr := errors.Unwrap(err) //获得err包含下一层错误
//判断err是否包含target
errors.Is(err, target error)判断err是否包含target
project
:项目 —> 一个VsCode窗口 或者 一个GoLand窗口 打开一个项目(project)
package
:包 --> 一个project 可以由多个 package 组成
.go
文件:源码文件
Go语言中支持模块化的开发理念,在Go语言中使用Package
来支持代码模块化和代码复用。一个包是由一个或者多个Go源码文件(.go结尾的文件)组成,是一种高级的代码复用方案,Go语言为我们提供了很多内置包,如fmt
、os
、io
等。
例如,之前的章节中我们使用了fmt这个内置包。
package main
import "fmt"
func main() {
fmt.Println("Hello world")
}
上面短短的几行代码就涉及到了如何定义包以及如何引入其他包两个内容。
我们可以根据自己需要创建自定义包。一个包可以简单理解为一个存放.go
文件的文件夹。该文件夹下面所有的.go
文件都要在非注释的第一行添加如下声明,声明该文件归属的包。
package packagename
其中:
-
符号,最好与其实现的功能相对应。另外需要注意下一个文件夹下面直接包含的文件只属于一个包,同一个包的文件不能再多个文件夹下。包名为main
的包是应用程序的入口包,这种包编译后会得到一个可执行的文件,而编译不包含main
包的源代码不会得到可执行文件。
在同一个包内部声明的标识符都位于同意命名空间下,在不同的包内部声明的标识符就属于不同的命名空间。想要在包的外部使用包内部的标识符就需要添加包名前缀,例如fmt.Println("hello")
,就是指调用fmt
包的Prinln
函数。
如果想让一个包的标识符(如变量、常量、类型、函数等)能被外面的包使用,那么标识符必须对外可见的(public)。在Go语言中通过标识符的首字母大/小写来控制标识符的对外可见(public)/不可见(private)的。在一个包内部只有首字母大写的标识符才是对外可见的。
例如我们定义一个名为demo
的包,在其中定义了若干标识符。在另外一个包中并不是所有的标识符都能通过demo.
前缀访问到,因为只有那些首字母是大写的标识符才是对外可见的。
package demo
import "fmt"
//包级别标识符的可见性
//num 定义一个全局整型变量
//首字母小写,对外不可见(只能在当前包使用)
var num = 100
//Mode 定一个常量
//首字母大写,对外可见(可在其他包使用)
const Mode = 1
//person定一个结构体
//首字母小写,对外不可见(只在当前包内使用)
type person struct {
name string
Age int
}
//Add返回两个整数和的函数
//首字母大写,对外可见(可在其他包中使用)
func Add(x, y int) int {
return x + y
}
//sayHi函数
//首字母小写,对外不可见(只在当前包使用)
func sayHi() {
var myname = "王磊" // 局部变量,只在当前函数内使用
fmt.Println(myname)
}
同样的规则也适用于结构体,结构体中可倒出字段的字段名称必须首字母大写.
type Student struct{
Name string //可在包外访问的方法
age int //仅限在包内访问的字段
}
要在当前包中使用另外一个包的内容就需要使用import
关键字引入这个包,并且import语句通常放在文件的开头,package声明语句的下方。完整的引入声明语句格式如下:
import importname "path/to/package"
其中:
importname:引入的包名,通常都省略。默认值为引入包的包名。
path/to/package:引入包的路径名称,必须使用双引号包裹起来。
Go语言中禁止循环导入包。
一个Go源码文件中可以同时引入多个包,例如:
import "fmt"
import "net/http"
import "os"
当然可以使用批量引入的方式。
import (
"fmt"
"net/http"
"os"
)
当引入的多个包中存在相同的包名或者想自行为某个引入的包设置一个新包名时,都需要通过importname
指定一个在当前文件中使用的新包名。例如,在引入fmt
包时为其指定一个新包名f
。
import f "fmt"
这样在当前这个文件中就可以通过使用f
来调用fmt
包中的函数了。
f.Println("Hello world!")
如果引入一个包的时候为其设置了一个特殊_
作为包名,那么这个包的引入方式就称为匿名引入。一个包被匿名引入的目的主要是为了加载这个包,从而使得这个包中的资源得以初始化。 被匿名引入的包中的init
函数将被执行并且仅执行一遍。
import _ "github.com/go-sql-driver/mysql"
匿名引入的包与其他方式导入的包一样都会被编译到可执行文件中。
需要注意的是,Go语言中不允许引入包却不在代码中使用这个包的内容,如果引入了未使用的包则会触发编译错误。
在每一个Go源文件中,都可以定义任意个如下格式的特殊函数:
func init(){
//...
}
这种特殊的函数不接收任何参数也没有任何返回值,我们也不能在代码中主动调用他。当程序启动的时候,init函数会按照他们声明的顺序自动执行。
一个包的初始化过程是按照代码中引入顺序进行的。所有在该包中声明的init
函数都将会被串行调用并且仅调用执行一次。每一个包初始化的时候都是先执行依赖的包中声明的init
函数再执行当前包中申明的init函数。确保在程序的main
函数开始执行时,所有的依赖包都已经初始化完成。
每一个包的初始化是先从初始化包级别变量开始的。例如从下面的示例中我们就可以看出包级别变量的初始化会先于init
初始化函数。
package main
import "fmt"
var x int8 = 10
const pi = 3.14
func init() {
fmt.Println("x: ", x)
fmt.Println("pi: ", pi)
sayHi()
}
func sayHi() {
fmt.Println("hello")
}
func main() {
fmt.Println("你好")
}
//结果
x: 10
pi: 3.14
hello
你好
在上面的代码中,我们了解Go语言中包的定义及包的初始化过程,这让我们开发时按照自己的需求定义包。同时还学了如何在代码映入其他的包。
标识符的首字母 什么时候大写,什么时候小写 都是有考量的!!!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1TJxLJGW-1656637354590)(day06课上笔记.assets/image-20220220152109859.png)]
在Go语言的早期版本中,我们编写Go项目代码时所依赖的所有第三方包都需要保存在GOPATH这个目录下面。这样存在一个致命缺陷,就是不支持版本管理,同一个依赖包只能存在一个版本的代码。可是我们本地的多个项目完全可能分别依赖同一个第三方的不同版本。
Go module 是 Go1.11 版本发布的依赖管理方案,从 Go1.14 版本开始推荐在生产环境使用,于Go1.16版本默认开启。Go module 提供了以下命令供我们使用:
go module相关命令
命令 | 介绍 |
---|---|
go mod init | 初始化项目依赖,生成go.mod文件 |
go mod download | 根据go.mod文件下载依赖 |
go mod tidy | 比对项目文件中引入的依赖与go.mod进行比对 |
go mod graph | 输出依赖关系图 |
go mod edit | 编辑go.mod文件 |
go mod vendor | 将项目的所有依赖导出至vendor目录 |
go mod verify | 检验一个依赖包是否被篡改过 |
go mod why | 解释为什么需要某个依赖 |
Go语言在 go module 的过渡阶段提供了 GO111MODULE
这个环境变量来作为是否启用 go module 功能的开关,考虑到 Go1.16 之后 go module 已经默认开启,所以本书不再介绍该配置,对于刚接触Go语言的读者而言完全没有必要了解这个历史包袱。
GOPROXY
这个环境变量主要用于设置Go的模块代理,其作用是用于使go在后续拉取模块版本脱离传统的VCS方式,直接通过镜像点来拉取。
GOPROXY 的默认值是:https://proxy.golang.org,direct
,由于某些原因国内无法正常访问该地址,所以我们通常需要配置一个可访问的地址。目前社区使用比较多的有两个https://goproxy.cn
和https://goproxy.io
,当然如果你的公司有提供GOPROXY地址那么就直接使用。设置GOPAROXY的命令如下:
go env -w GOPROXY=https://goproxy.cn,direct
GOPROXY 允许设置多个代理地址,多个地址之间需使用英文逗号 “,” 分隔。最后的 “direct” 是一个特殊指示符,用于指示 Go 回源到源地址去抓取(比如 GitHub 等)。当配置有多个代理地址时,如果第一个代理地址返回 404 或 410 错误时,Go 会自动尝试下一个代理地址,当遇见 “direct” 时触发回源,也就是回到源地址去抓取。
GOPRIVATE
设置了GOPROXY 之后,go 命令就会从配置的代理地址拉取和校验依赖包。当我们在项目中引入了非公开的包(公司内部git仓库或 github 私有仓库等),此时便无法正常从代理拉取到这些非公开的依赖包,这个时候就需要配置 GOPRIVATE 环境变量。GOPRIVATE用来告诉 go 命令哪些仓库属于私有仓库,不必通过代理服务器拉取和校验。
GOPRIVATE 的值也可以设置多个,多个地址之间使用英文逗号 “,” 分隔。我们通常会把自己公司内部的代码仓库设置到 GOPRIVATE 中,例如:
$ go env -w GOPRIVATE="git.mycompany.com"
这样在拉取以git.mycompany.com
为路径前缀的依赖包时就能正常拉取了。
此外,如果公司内部自建了 GOPROXY 服务,那么我们可以通过设置 GONOPROXY=none
,允许通内部代理拉取私有仓库的包。
接下来我们将通过一个示例来演示如何在开发项目时使用 go module 拉取和管理项目依赖。
初始化项目 我们在本地新建一个名为holiday
项目,按如下方式创建一个名为holiday
的文件夹并切换到该目录下:
$ mkdir holiday
$ cd holiday
目前我们位于holiday
文件夹下,接下来执行下面的命令初始化项目。
$ go mod init holiday
go: creating new go.mod: module holiday
该命令会自动在项目目录下创建一个go.mod
文件,其内容如下。
module holiday
go 1.16
其中:
go.mod
文件会记录项目使用的第三方依赖包信息,包括包名和版本,由于我们的holiday
项目目前还没有使用到第三方依赖包,所以go.mod
文件暂时还没有记录任何依赖包信息,只有当前项目的一些信息。
接下来,我们在项目目录下新建一个main.go
文件,其内容如下:
// holiday/main.go
package main
import "fmt"
func main() {
fmt.Println("现在是假期时间...")
}
然后,我们的holiday
项目现在需要引入一个第三方包github.com/q1mi/hello
来实现一些必要的功能。类似这样的场景在我们的日常开发中是很常见的。我们需要先将依赖包下载到本地同时在go.mod
中记录依赖信息,然后才能在我们的代码中引入并使用这个包。下载依赖包主要有两种方法。
第一种方法
在该项目目录下执行go get
命令手动下载依赖包:
sh-3.2$ go get -u github.com/q1mi/hello
go: downloading github.com/q1mi/hello v0.1.1
go get: added github.com/q1mi/hello v0.1.1
这样默认会下载最新的发布版本,你也可以指定想要下载指定的版本号的。
sh-3.2$ go get -u github.com/q1mi/hello@v0.1.0
go: downloading github.com/q1mi/hello v0.1.0
go get: downgraded github.com/q1mi/hello v0.1.1 => v0.1.0
如果依赖包没有发布任何版本则会拉取最新的提交,最终go.mod
中的依赖信息会变成类似下面这种由默认v0.0.0的版本号和最新一次commit的时间和hash组成的版本格式:
require github.com/q1mi/hello v0.0.0-20210218074646-139b0bcd549d
如果想指定下载某个commit对应的代码,可以直接指定commit hash,不过没有必要写出完整的commit hash,一般前7位即可。例如:
holiday $ go get github.com/q1mi/hello@2ccfadd
go: downloading github.com/q1mi/hello v0.1.2-0.20210219092711-2ccfaddad6a3
go get: added github.com/q1mi/hello v0.1.2-0.20210219092711-2ccfaddad6a3
此时,我们打开go.mod
文件就可以看到下载的依赖包及版本信息都已经被记录下来了。
module holiday
go 1.16
require github.com/q1mi/hello v0.1.0 // indirect
行尾的indirect
表示该依赖包为间接依赖,说明在当前程序中的所有 import 语句中没有发现引入这个包。
另外在执行go get
命令下载一个新的依赖包时一般会额外添加-u
参数,强制更新现有依赖。
第二种方法
我们直接编辑go.mod
文件,将依赖包和版本信息写入该文件。例如我们修改holiday/go.mod
文件内容如下:
module holiday
go 1.16
require github.com/q1mi/hello latest
表示当前项目需要使用github.com/q1mi/hello
库的最新版本,然后在项目目录下执行go mod download
下载依赖包。
holiday $ go mod download
如果不输出其它提示信息就说明依赖已经下载成功,此时go.mod
文件已经变成如下内容。
module holiday
go 1.16
require github.com/q1mi/hello v0.1.1
从中我们可以知道最新的版本号是v0.1.1
。如果事先知道依赖包的具体版本号,可以直接在go.mod
中指定需要的版本然后再执行go mod download
下载。
这种方法同样支持指定想要下载的commit进行下载,例如直接在go.mod
文件中按如下方式指定commit hash,这里只写出来了commit hash的前7位。
require github.com/q1mi/hello 2ccfadda
执行go mod download
下载完依赖后,go.mod
文件中对应的版本信息会自动更新为类似下面的格式。
module holiday
go 1.16
require github.com/q1mi/hello v0.1.2-0.20210219092711-2ccfaddad6a3
载好要使用的依赖包之后,我们现在就可以在holiday/main.go
文件中使用这个包了。
package main
import (
"fmt"
"github.com/q1mi/hello"
)
func main() {
fmt.Println("假期时间到了...")
hello.SayHi() //调用hello包的方法
}
//运行结果
假期时间到了...
你好呀~我是七米。很高兴认识你。
当我们的项目功能越来越多,代码越来越多的呃时候,通常会选择在项目内部按照功能或业务划分成不同包。Go语言支持在一个项目下定义多个包。
例如,我们在holiday
项目内部新建一个新的package–summer
,此时新的项目,目录结构
holidy
├── go.mod
├── go.sum
├── main.go
└── summer
└── summer.go
其中holiday/summer/summer.go
文件内容如下:
package summer
import "fmt"
// Diving 潜水...
func Diving() {
fmt.Println("夏天去诗巴丹潜水...")
}
此时想要在当前项目目录下的其他包或者main.go
中调用这个Diving
函数需要如何引入呢?这里以在main.go
中演示详细的调用过程为例,在项目内其他包的引入方式类似。
package main
import (
"fmt"
"holy/summer"
"github.com/q1mi/hello"
)
func main() {
fmt.Println("假期时间到了...")
hello.SayHi() //调用hello包的方法
summer.Diving() //倒入当前项目下的包
}
如果你想要导入本地的一个包,并且这个包也没有发布到到其他任何代码仓库,这时候你可以在go.mod
文件中使用replace
语句将依赖临时替换为本地的代码包。例如在我的电脑上有另外一个名为liwenzhou.com/overtime
的项目,它位于holiday
项目同级目录下:
├── holiday
│ ├── go.mod
│ ├── go.sum
│ ├── main.go
│ └── summer
│ └── summer.go
└── overtime
├── go.mod
└── overtime.go
由于liwenzhou.com/overtime
包只存在于我本地,并不能通过网络获取到这个代码包,这个时候应该如何在holidy
项目中引入它呢?
我们可以在holidy/go.mod
文件中正常引入liwenzhou.com/overtime
包,然后像下面的示例那样使用replace
语句将这个依赖替换为使用相对路径表示的本地包。
module holiday
go 1.16
require github.com/q1mi/hello v0.1.1
require liwenzhou.com/overtime v0.0.0
replace liwenzhou.com/overtime => ../overtime
这样,我们就可以在holiday/main.go
下正常引入并使用overtime
包了。
package main
import (
"fmt"
"holiday/summer" // 导入当前项目下的包
"liwenzhou.com/overtime" // 通过replace导入的本地包
"github.com/q1mi/hello" // 导入github上第三方包
)
func main() {
fmt.Println("现在是假期时间...")
hello.SayHi()
summer.Diving()
overtime.Do()
}
我们也经常使用replace
将项目依赖中的某个包,替换为其他版本的代码包或我们自己修改后的代码包。
go. mod文件
go.mod
文件中记录了当前项目中所有依赖包的相关信息,声明依赖的格式如下:
require module/path v1.2.3
其中:
go.sum文件
使用go module下载了依赖后,项目目录下还会生成一个go.sum
文件,这个文件中详细记录了当前项目中引入的依赖包的信息及其hash 值。go.sum
文件内容通常是以类似下面的格式出现。
<module> <version>/go.mod <hash>
或者
<module> <version> <hash>
<module> <version>/go.mod <hash>
不同于其他语言提供的基于中心的包管理机制,例如 npm 和 pypi等,Go并没有提供一个中央仓库来管理所有依赖包,而是采用分布式的方式来管理包。为了防止依赖包被非法篡改,Go module 引入了go.sum
机制来对依赖包进行校验。
依赖保存位置
默认下载到 $gopath/pkg/mod/
目录下
导入的路径名由被引入的包的 module 定义
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4MjzyAta-1656637354591)(day06课上笔记.assets/image-20220220165016159.png)]
在上面的小节中我们学习了如何在项目中引入别人提供的依赖包,那么当我们想要在社区发布一个自己编写的代码包或者在公司内部编写一个供内部使用的公用组件时,我们该怎么做呢?接下来,我们就一起编写一个代码包并将它发布到github.com
仓库,让它能够被全球的Go语言开发者使用。
我们首先在自己的 github 账号下新建一个项目,并把它下载到本地。我这里就以创建和发布一个名为hello
的项目为例进行演示。这个hello
包将对外提供一个名为SayHi
的函数,它的作用非常简单就是向调用者发去问候。
$ git clone https://github.com/q1mi/hello
$ cd hello
我们当前位于hello
项目目录下,执行下面的命令初始化项目,创建go.mod
文件。需要注意的是这里定义项目的引入路径为github.com/q1mi/hello
,读者在自行测试时需要将这部分替换为自己的仓库路径。
hello $ go mod init github.com/q1mi/hello
go: creating new go.mod: module github.com/q1mi/hello
接下来我们在该项目根目录下创建 hello.go
文件,添加下面的内容:
package hello
import "fmt"
func SayHi() {
fmt.Println("你好,我是七米。很高兴认识你。")
}
然后将该项目的代码 push 到仓库的远端分支,这样就对外发布了一个Go包。其他的开发者可以通过github.com/q1mi/hello
这个引入路径下载并使用这个包了。
一个设计完善的包应该包含开源许可证及文档等内容,并且我们还应该尽心维护并适时发布适当的版本。github 上发布版本号使用git tag为代码包打上标签即可。
hello $ git tag -a v0.1.0 -m "release version v0.1.0"
hello $ git push origin v0.1.0
经过上面的操作我们就发布了一个版本号为v0.1.0
的版本。
Go modules中建议使用语义化版本控制,其建议的版本号格式如下:
其中:
发布新的主版本
现在我们的hello
项目要进行与之前版本不兼容的更新,我们计划让SayHi
函数支持向指定人发出问候。更新后的SayHi
函数内容如下:
package hello
import "fmt"
// SayHi 向指定人打招呼的函数
func SayHi(name string) {
fmt.Printf("你好%s,我是七米。很高兴认识你。\n", name)
}
由于这次改动巨大(修改了函数之前的调用规则),对之前使用该包作为依赖的用户影响巨大。因此我们需要发布一个主版本号递增的v2
版本。在这种情况下,我们通常会修改当前包的引入路径,像下面的示例一样为引入路径添加版本后缀。
// hello/go.mod
module github.com/q1mi/hello/v2
go 1.16
把修改后的代码提交:
hello $ git add .
hello $ git commit -m "feat: SayHi现在支持给指定人打招呼啦"
hello $ git push
打好 tag 推送到远程仓库。
hello $ git tag -a v2.0.0 -m "release version v2.0.0"
hello $ git push origin v2.0.0
这样在不影响使用旧版本的用户的前提下,我们新的版本也发布出去了。想要使用v2
版本的代码包的用户只需按修改后的引入路径下载即可。
go get github.com/q1mi/hello/v2@v2.0.0
在代码中使用的过程与之前类似,只是需要注意引入路径要添加 v2 版本后缀。
package main
import (
"fmt"
"github.com/q1mi/hello/v2" // 引入v2版本
)
func main() {
fmt.Println("现在是假期时间...")
hello.SayHi("张三") // v2版本的SayHi函数需要传入字符串参数
}
废弃已发布版本
如果某个发布的版本存在致命缺陷不再想让用户使用时,我们可以使用retract
声明废弃的版本。例如我们在hello/go.mod
文件中按如下方式声明即可对外废弃v0.1.2
版本。
module github.com/q1mi/hello
go 1.16
retract v0.1.2
用户使用go get下载v0.1.2
版本时就会收到提示,催促其升级到其他版本。