Sycamore

Phoenix reborns from the ashe

0%

GO 语言进阶笔记

此笔记跳出了最基本的Go语言教程,专注于适配于开发项目的一些进阶技术。比如说工作区的概念、包与模块、错误和Log等。

Ready to take off ?

工作区

Go语言是有一个工作区的概念,也就是项目区域,在老版本的Go语言中,需要对工作区进行创建,并且要更改GOPATH来完成对该项目的根目录的定义。但是在新版本的Go 1.11语言中,引入了模块系统,至此,可以在任何位置创建项目,并利用go.mod来管理依赖。

Goland使用

创建一个新的项目,直接就可以在文件 -> 新建 -> 项目,IDE会自动生成go.mod,从而创建一个工作区,一键搞定!

PS:在新建文件的时候,会有两个选择:1. 新建go文件;2. 新建go工作区文件。

前者是简单的一个文件,后者是新建一个工作区(项目)

文件夹习惯

1
2
3
4
5
6
7
8
9
10
11
bin/
hello
coolapp
pkg/
github.com/gorilla/
mux.a
src/
github.com/golang/example/
.git/
hello/
hello.go

每个 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
2
myPackage/
sum.go

Goland会自动为文件生成文件头:package myPackage

我们可以在这里进行包功能的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package myPackage
var logMessage = "[LOG]"

// Version of the calculator
var Version = "1.0"

func internalSum(number int) int {
return number - 1
}

// Sum two integer numbers
func Sum(number1, number2 int) int {
return number1 + number2
}

无论是变量还是函数,都遵循:大写 = public;小写 = private

所以只能在包外调用:VersionSum()

创建模块

Go 模块通常包含可提供相关功能的包。 包的模块还指定了 Go 运行你组合在一起的代码所需的上下文。 此上下文信息包括编写代码时所用的 Go 版本。

如果要为上述的myPackage床检模块,需要在/myPackage目录下运行以下命令

1
2
// go mod init 包的名称
go mod init github.com/myuser/myPackage

运行此命令后,github.com/myuser/myPackage 就会变成模块的名称。 在其他程序中,你将使用该名称进行引用。 命令还会创建一个名为 go.mod 的新文件。

1
2
module github.com/myuser/myPackage
go 1.21.3

此时该模块会被上传到远程的代码管理仓库中,比如github.com,如果项目需要使用该包,就要在终端输入

1
go get github.com/myuser/myPackage

引用模块的话直接用github.com/myuser/myPackage引用就行。

教程上说如果想用本地的副本需要更改main.go模块的go.mod

1
2
3
module github.com/myPackage
go 1.21.3
replace github.com/myPackage => ../myPackage

但是报错……拉倒吧

!!如果是本地自己写的包,就别变成模块了,模块主要是项目间复用,感觉不如copy来得快。试了试一直引用不了模块,不知道哪出了问题。服了……

引用第三方包

在终端直接输入

1
go get -u [url]

下载完之后,直接在.go文件中import就行,go.mod会自动创建require()

defer & panic & recover

defer 函数

在 Go 中,defer 语句会推迟函数(包括任何参数)的运行,直到包含 defer 语句的函数完成

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
for i := 1; i <= 4; i++ {
defer fmt.Println("deferred", -i)
fmt.Println("regular", i)
}
}
1
2
3
4
5
6
7
8
regular 1
regular 2
regular 3
regular 4
deferred -4
deferred -3
deferred -2
deferred -1

这里的defer语句在for中,所以当该循环运行结束后才运行defer后面的函数。

  • defer的运行和栈是一样的,后入栈的先出栈
  • 并且函数的状态是被保留的,即输入的参数是在defer语句声明的时候就保存好了。

例如,文件的关闭可以用defer,在所有语句结束之后,关闭文件。

panic 函数

内置 panic() 函数可以停止 Go 程序中的正常控制流。 当你使用 panic 调用时,任何延迟的函数调用都将正常运行。 进程会在堆栈中继续,直到所有函数都返回。 然后,程序会崩溃并记录日志消息。

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

import "fmt"

func highlow(high int, low int) {
if high < low {
fmt.Println("Panic!")
panic("highlow() low greater than high")
}
defer fmt.Println("Deferred: highlow(", high, ",", low, ")")
fmt.Println("Call: highlow(", high, ",", low, ")")

highlow(high, low + 1)
}

func main() {
highlow(2, 0)
fmt.Println("Program finished successfully!")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Call: highlow( 2 , 0 )
Call: highlow( 2 , 1 )
Call: highlow( 2 , 2 )
Panic!
Deferred: highlow( 2 , 2 )
Deferred: highlow( 2 , 1 )
Deferred: highlow( 2 , 0 )
panic: highlow() low greater than high

goroutine 1 [running]:
main.highlow(0x2, 0x3)
/tmp/sandbox/prog.go:13 +0x34c
main.highlow(0x2, 0x2)
/tmp/sandbox/prog.go:18 +0x298
main.highlow(0x2, 0x1)
/tmp/sandbox/prog.go:18 +0x298
main.highlow(0x2, 0x0)
/tmp/sandbox/prog.go:18 +0x298
main.main()
/tmp/sandbox/prog.go:6 +0x37

Program exited: status 2.

在发生panic的时候,只有defer的函数会立刻执行,执行完毕之后整个程序停止。没有运行的语句不再运行。

recover 函数

Go 提供内置 recover() 函数,让你可以在程序崩溃之后重新获得控制权。 你只会在你同时调用 defer 的函数中调用 recover 如果调用 recover() 函数,则在正常运行的情况下,它会返回 nil,没有任何其他作用。

所以说,当panic起效的时候,只有defer运行,在defer中使用recover函数,如果返回非nil,则说明程序崩溃,并恢复进程。如果返回nil则没有效果。

1
2
3
4
5
6
7
8
9
10
func main() {
defer func() {
handler := recover() // 恢复程序进程
if handler != nil {
fmt.Println("main(): recover", handler)
}
}()
highlow(2, 0)
fmt.Println("Program finished successfully!")
}

panicrecover 函数的组合是 Go 处理异常的惯用方式。

错误进阶

  • Go 具有 panicrecover 之类的内置函数来管理程序中的异常或意外行为
  • Go 用error来处理错误(已知的失败)

错误处理策略

当函数返回错误的时候,该错误通常是最后一个返回值。在函数声明的时候用error作为错误的定义。

1
2
3
4
5
6
7
func getInformation(id int) (*Employee, error) {
employee, err := apiCallEmployee(1000)
if err != nil {
return nil, fmt.Errorf("Got an error when getting the employee information: %v", err)
}
return employee, nil
}

如果是有错误的,我们可以用函数fmt.Errorf()来说明错误类型,例如下面的这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func getInformation(id int) (*Employee, error) {
for tries := 0; tries < 3; tries++ { // 出现错误先重复操作三次。
employee, err := apiCallEmployee(1000)
if err == nil {
return employee, nil
}

fmt.Println("Server is not responding, retrying ...")
time.Sleep(time.Second * 2)
}

return nil, fmt.Errorf("server has failed to respond to get the employee information")
// 返回fmt.Errorf()的错误
}

创建可重用的错误

当错误需要重用并且常见的时候,我们可以为其创建一个库,利用errors.New()函数创建错误并在若干部分中重复使用。

1
2
3
4
5
6
7
8
9
10
var ErrNotFound = errors.New("Employee not found!")

func getInformation(id int) (*Employee, error) {
if id != 1001 {
return nil, ErrNotFound
}

employee := Employee{LastName: "Doe", FirstName: "John"}
return &employee, nil
}

我们想判断错误信息,可以使用函数errors.Is(_, _),判断之后可以进行打印等操作。

1
2
3
if errors.Is(err, ErrorNotFound) {
println("Not 0!")
}

处理错误的推荐做法:

  • 始终检查是否存在错误,即使预期不存在。 然后正确处理它们,以免向最终用户公开不必要的信息。
  • 在错误消息中包含一个前缀,以便了解错误的来源。 例如,可以包含包和函数的名称。
  • 创建尽可能多的可重用错误变量。
  • 在记录错误时记录尽可能多的详细信息,并打印出最终用户能够理解的错误。

日志

log 包

Go 提供了一个用于处理日志的简单标准包。 可以像使用 fmt 包一样使用此包。

1
2
3
4
5
6
7
import (
"log"
)

func main() {
log.Print("Hey, I'm a log!")
}

默认情况下,log.Print() 函数将日期和时间添加为日志消息的前缀。

1
2023/11/14 09:48:49 Hey, I'm a log!

log.Fatal()函数用来记录错误并结束程序,和os.Exit(1)一样

log.SetPrefix(),功能是向程序的日志消息添加前缀。

1
2
3
4
5
func main() {
log.SetPrefix("main(): ")
log.Print("Hey, I'm a log!")
log.Fatal("Hey, I'm an error log!")
}
1
2
3
main(): 2021/01/05 13:59:58 Hey, I'm a log!
main(): 2021/01/05 13:59:58 Hey, I'm an error log!
exit status 1

记录到文件

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

func main() {
file, err := os.OpenFile("info.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) // 打开文件
if err != nil {
log.Fatal(err)
}

defer file.Close() // 采用延迟函数来关闭文件。

log.SetOutput(file) // 对所有的log进行输出。
log.Print("Hey, I'm a log!")
}

这个函数如果log.Fatal(err)运行了,则在控制太输出err,并不会输入到文件中。在这个函数中,只有log.Print()的消息在文件中。

接口进阶

个人理解

我对接口的理解就是他为函数的输入定义了一个范式,只要符合这个接口内部定义函数的结构体都可以传入这个函数。

举个例子,我先定义了一个接口叫做Shape,指的是所有的形状,里面有周长和面积两个方法。

1
2
3
4
type Shape interface{
Perimeter() float32
Area() float32
}

之后我定义了一个函数,这个函数是打印Shape的周长与面积

1
2
3
4
func printAreaAndPerimeter(s Shape) {
println("Area:", s.Area())
println("Perimeter:", s.Perimeter())
}

这样我只需要定义不同的结构体,这些结构体中包含Shape定义的两种方法,其就可以被传入到printAreaAndPerimeter()函数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Circle struct {
radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.radius * c.radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.radius
}
/**********************/
type Square struct {
size float64
}
func (s Square) Area() float64 {
return s.size * s.size
}
func (s Square) Perimeter() float64 {
return s.size * 4
}