此笔记跳出了最基本的Go
语言教程,专注于适配于开发项目的一些进阶技术。比如说工作区的概念、包与模块、错误和Log等。
Ready to take off ?
工作区
Go
语言是有一个工作区的概念,也就是项目区域,在老版本的Go
语言中,需要对工作区进行创建,并且要更改GOPATH
来完成对该项目的根目录的定义。但是在新版本的Go 1.11
语言中,引入了模块系统,至此,可以在任何位置创建项目,并利用go.mod
来管理依赖。
Goland使用
创建一个新的项目,直接就可以在文件 -> 新建 -> 项目,IDE会自动生成go.mod
,从而创建一个工作区,一键搞定!
PS:在新建文件的时候,会有两个选择:1. 新建go文件;2. 新建go工作区文件。
前者是简单的一个文件,后者是新建一个工作区(项目)
文件夹习惯
1 | bin/ |
每个 Go 工作区都包含三个基本文件夹:
- bin:包含应用程序中的可执行文件。
- src:包括位于工作站中的所有应用程序源代码。
- pkg:包含可用库的已编译版本。 编译器可以链接这些库,而无需重新编译它们。
包
包的概念像是python
中的库,可以说是在项目中的封装体现。
main包
可以发现,在Go
中,所有的程序都是包的一部分,通常的默认包时main
,也就是在程序开头的package main
语句。如果程序是main
包里的一部分,那么Go
就会为此生成二进制文件,从而可以被运行(调用main
函数)
这也就说明了:为什么必须是main
包,该文件才可以被执行。
1 | package command-line-arguments is not a main package |
↑,如果调用非main
的运行结果。
在该语言中,包调用的名称采用其导入路径的最后一部分,比如导入
math/cmplx
包,引用其中的对象采用的语句为cmplx.Inf()
创建包
例如我想创建一个myPackage
的包,那我就在目录下新建一个myPackage
的文件夹,并在里面创建go文件。
1 | myPackage/ |
Goland会自动为文件生成文件头:package myPackage
我们可以在这里进行包功能的定义:
1 | package myPackage |
无论是变量还是函数,都遵循:大写 = public;小写 = private
所以只能在包外调用:
Version
,Sum()
创建模块
Go 模块通常包含可提供相关功能的包。 包的模块还指定了 Go 运行你组合在一起的代码所需的上下文。 此上下文信息包括编写代码时所用的 Go 版本。
如果要为上述的myPackage
床检模块,需要在/myPackage
目录下运行以下命令
1 | // go mod init 包的名称 |
运行此命令后,github.com/myuser/myPackage
就会变成模块的名称。 在其他程序中,你将使用该名称进行引用。 命令还会创建一个名为 go.mod
的新文件。
1 | module github.com/myuser/myPackage |
此时该模块会被上传到远程的代码管理仓库中,比如github.com
,如果项目需要使用该包,就要在终端输入
1 | go get github.com/myuser/myPackage |
引用模块的话直接用github.com/myuser/myPackage
引用就行。
教程上说如果想用本地的副本需要更改main.go
模块的go.mod
1 | module github.com/myPackage |
但是报错……拉倒吧
!!如果是本地自己写的包,就别变成模块了,模块主要是项目间复用,感觉不如copy来得快。试了试一直引用不了模块,不知道哪出了问题。服了……
引用第三方包
在终端直接输入
1 | go get -u [url] |
下载完之后,直接在.go
文件中import
就行,go.mod
会自动创建require()
defer & panic & recover
defer 函数
在 Go 中,defer
语句会推迟函数(包括任何参数)的运行,直到包含 defer
语句的函数完成。
1 | package main |
1 | regular 1 |
这里的defer
语句在for
中,所以当该循环运行结束后才运行defer
后面的函数。
defer
的运行和栈是一样的,后入栈的先出栈- 并且函数的状态是被保留的,即输入的参数是在
defer
语句声明的时候就保存好了。
例如,文件的关闭可以用defer
,在所有语句结束之后,关闭文件。
panic 函数
内置 panic()
函数可以停止 Go 程序中的正常控制流。 当你使用 panic
调用时,任何延迟的函数调用都将正常运行。 进程会在堆栈中继续,直到所有函数都返回。 然后,程序会崩溃并记录日志消息。
1 | package main |
1 | Call: highlow( 2 , 0 ) |
在发生panic
的时候,只有defer
的函数会立刻执行,执行完毕之后整个程序停止。没有运行的语句不再运行。
recover 函数
Go 提供内置 recover()
函数,让你可以在程序崩溃之后重新获得控制权。 你只会在你同时调用 defer
的函数中调用 recover
。 如果调用 recover()
函数,则在正常运行的情况下,它会返回 nil
,没有任何其他作用。
所以说,当panic
起效的时候,只有defer
运行,在defer
中使用recover
函数,如果返回非nil
,则说明程序崩溃,并恢复进程。如果返回nil
则没有效果。
1 | func main() { |
panic
和recover
函数的组合是 Go 处理异常的惯用方式。
错误进阶
- Go 具有
panic
和recover
之类的内置函数来管理程序中的异常或意外行为 - Go 用
error
来处理错误(已知的失败)
错误处理策略
当函数返回错误的时候,该错误通常是最后一个返回值。在函数声明的时候用error
作为错误的定义。
1 | func getInformation(id int) (*Employee, error) { |
如果是有错误的,我们可以用函数fmt.Errorf()
来说明错误类型,例如下面的这个函数
1 | func getInformation(id int) (*Employee, error) { |
创建可重用的错误
当错误需要重用并且常见的时候,我们可以为其创建一个库,利用errors.New()
函数创建错误并在若干部分中重复使用。
1 | var ErrNotFound = errors.New("Employee not found!") |
我们想判断错误信息,可以使用函数errors.Is(_, _)
,判断之后可以进行打印等操作。
1 | if errors.Is(err, ErrorNotFound) { |
处理错误的推荐做法:
- 始终检查是否存在错误,即使预期不存在。 然后正确处理它们,以免向最终用户公开不必要的信息。
- 在错误消息中包含一个前缀,以便了解错误的来源。 例如,可以包含包和函数的名称。
- 创建尽可能多的可重用错误变量。
- 在记录错误时记录尽可能多的详细信息,并打印出最终用户能够理解的错误。
日志
log 包
Go 提供了一个用于处理日志的简单标准包。 可以像使用 fmt
包一样使用此包。
1 | import ( |
默认情况下,log.Print()
函数将日期和时间添加为日志消息的前缀。
1 | 2023/11/14 09:48:49 Hey, I'm a log! |
log.Fatal()
函数用来记录错误并结束程序,和os.Exit(1)
一样
log.SetPrefix()
,功能是向程序的日志消息添加前缀。
1 | func main() { |
1 | main(): 2021/01/05 13:59:58 Hey, I'm a log! |
记录到文件
1 | import ( |
这个函数如果log.Fatal(err)
运行了,则在控制太输出err,并不会输入到文件中。在这个函数中,只有log.Print()
的消息在文件中。
接口进阶
个人理解
我对接口的理解就是他为函数的输入定义了一个范式,只要符合这个接口内部定义函数的结构体都可以传入这个函数。
举个例子,我先定义了一个接口叫做Shape
,指的是所有的形状,里面有周长和面积两个方法。
1 | type Shape interface{ |
之后我定义了一个函数,这个函数是打印Shape
的周长与面积
1 | func printAreaAndPerimeter(s Shape) { |
这样我只需要定义不同的结构体,这些结构体中包含Shape
定义的两种方法,其就可以被传入到printAreaAndPerimeter()
函数中。
1 | type Circle struct { |