Go语言完全入门指南
文章希望用简洁易懂的文字讲解Go语言的开发环境搭建和语法,用以帮助初学者快速学会Go语言。
这篇文章默认读者掌握基础的计算机知识,并且至少有任意一门编程语言的学习经验。
Go的开发环境搭建
下载与安装
下载
进入Go官方镜像站(golang.google.cn/dl)下载对应操作系统的安装包即可:
安装
Windows和Mac下的安装很简单,不断点“next”直到“finish”就行。
Linux下需要先将压缩文件解压到对应目录,一般放在/usr/local/go
,然后在配置文件/etc/profile
里配置下环境变量:
export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin
安装完成后在cmd中输入go version
回车,如果返回了go的版本号就说明安装成功。
IDE
理论上只要是文字编辑器就可以编写Go语言,但推荐用jetbrains的Goland编辑器,“工欲善其事,必先利其器”,用起来更加方便。
进入Goland的Preferences → Plugins进行插件下载,推荐几个比较实用的插件:
- Tabnine AI Code Completion:对自动补全的优化,使用AI预测内容并进行自动补全
- GitToolBox:更多对Git使用的优化
- Gopher:Go仓鼠进度条
- Key Promoter X:快捷键提示
- Rainbow Brackets:彩色的代码括号
包管理
GOPROXY配置
Go1.14版本之前,所有的Go文件都需要放到一个配置好的GOPATH目录下才能运行。Go1.14版本之后,开始使用go mod
方式来管理依赖,就可以在电脑的任意位置处新建Go项目并运行了。
go mod
可以批量下载和管理的Go的依赖,这是通过GoPROXY配置的网址proxy.golang.org下载的,但是这个域名在国内访问不了,所以我们需要将GoPROXY重新进行如下配置:
go env -w GOPROXY=https://goproxy.cn,direct
go mod
go mod是Go语言的包管理工具。
启用go mod需要先设置环境变量GO111MODULE,将其开启:
GO111MODULE=on
主要的go mod
命令:
- go mod init:用于创建和初始化 .mod 文件
- go mod tidy:自动整理丢失的依赖和删除未使用的依赖
- go mod download:下载所有依赖到本地缓存
- go get <包名>:下载指定依赖包
引用包
Go文件在开头通过import关键字引用所需的依赖包:
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"strconv"
"time"
)
上例中的fmt、context、errors等包都是Go官方写的,只要安装了Go,就可以直接在项目中引用。如果需要引用第三方包,则需要先下载具体的包,Go提供了快捷的命令用于下载第三方包:
go get xxx
如上,只需要运行go get
命令后面携带第三方包名即可。
上一节介绍了go mod,使用go mod的go mod download
命令也可以下载所有依赖到本地缓存。
第一个Go程序
创建目录one,在goland编辑器中打开。
在目录one下输入命令:go mod init one
用于创建和初始化项目one的 .mod 文件。
在目录下创建一个main.go文件,这是Go程序的入口,编写如下代码:
package main
import "fmt"
func main() {
fmt.Println("Hello World!")
}
在目录one下运行go build
命令,会生成一个可执行文件,运行可执行文件,就看到终端输出了Hello World!
。
如果在目录one下运行go run main.go
命令,也会在终端输出Hello World!
,只是这样不会生成可执行文件。
Go的语法基础
声明
包名
一个Go项目由多个包组成,每个Go文件的开头第一行需要声明文件所在包:
package service
这样当其他包需要引用这个package service
下的函数时,就可以通过 import "service"
引入。
如果包名发生了冲突,在引入时可以使用别名:
import (
oldservice "old/service"
newservice "new/service"
)
这样就可以用oldservice.调用"old/service"的函数,用newservice.调用"new/service"的函数了。
按照惯例,包名应该以小写的单个单词来命名,并且不应该使用下划线或驼峰记法。
变量与常量
变量声明
变量可以通过一个程序员声明的名称获取和操作在程序运行中对应内存地址的值。
在Go中,有2种变量声明的方式:
- 标准声明
- 简短声明
标准声明以关键字var为开头,可以定义一个或多个变量:
var xin string // 声明一个变量
var xin, ain, bin string // 声明多个变量
var xin, ain string = "x", "a" // 声明多个变量并赋值
var (
xin int
a string
b = 1
) // 批量声明变量
简短声明用 :=
声明变量:
xin := "x"
xin, ain := "x", "a"
在Go中,声明的变量必须被使用,如果不想使用,可以用下划线的方式忽略变量:
xin, _ := xin() // 第2个参数用下划线代替了
被下划线代替的变量也叫匿名变量,不占用命名空间,不会分配内存。
常量声明
声明常量是用于定义在程序执行过程中不会改变的值。常量只能是数字、字符(符文)、字符串或布尔值。
常量使用const关键字定义:
const xin = 1
const (
xin = 1
ain = 2
)
公开与私有
Go也有类似Java的Public和Private,公开的声明可以在其他Go文件中引用,私有的声明只能在定义它的Go文件中引用。在Go中可以很简单的区分公开和私有,首字母大写的函数、变量、常量就是Public,首字母小写的函数、变量、常量就是Private。
数据类型
Go支持所有基础的数据类型,如int、float、bool、string等。
字符串
在Go中,字符串使用UTF-8
编码,用双引号包含内容,支持常用的转义符。
常用的字符串方法包括:
方法 | 功能 |
---|---|
len(str) | 获取字符串长度 |
strings.Split(str, ",") | 以","分割字符串,返回字符串数组。","可以是任意你需要的值 |
strings.Contains(str, "?") | 判断字符串是否包含"?",返回bool值 |
strings.Join(strs, "?") | strs是字符串数组,返回一个将strs用"?"连接成的字符串 |
str = strings.Replace(str, " ", "", -1) | 字符串替换。比如将字符串中的空格去除 |
类型转换
Go官方提供了不同类型之间的转换方法,但通常我们使用第三方库github.com/spf13/cast 进行类型转换,cast使用简单明了,降低心智负担:
// 转字符串
cast.ToString(66) // int to string
cast.ToString(3.1415926) // float to string
cast.ToString([]byte("one time")) // []byte to string
// 转int
cast.ToInt(8.88) // float to string
cast.ToInt("8") // string to int
cast.ToInt("8.11") // 返回值为0
cast.ToInt(true) // 返回值为1
cast.ToInt(false) // 返回值为0
// 转float64
cast.ToFloat64("8.21132")
数组与切片
数组
数组是一系列数据类型的集合。在Go中声明数组时需要确定数组的长度,例如:
var xin [3]int
var xin [3]int{1,2,3}
数组xin的长度在声明时就已经被设定为3。
如果需要确保初始化的内容和数组长度一致,可以使用...
代替常量,例如以下长度为3的数组可以这样声明:
var xin [...]int{1,2,3}
切片
因为在Go中数组的长度是固定的,用起来不方便,所以通常会用切片代替数组使用。切片通过对数组进行封装,为数据序列提供了更通用、强大而方便的接口。
在 Go 中,切片类型的声明与数组有一些相似,不过由于切片的长度是动态的,所以声明时只需要指定切片中的元素数据类型即可:
var xin []string
也可以使用内置函数make创建切片,make可以指定长度和容量:
s = make([]byte, 1, 5) // 长度1,容量5
还可以很方便的截取切片:
NewXin := xin[1:3] // 截取索引1~3的内容
NewXin := xin[:3] // 截取索引0~3的内容
NewXin := xin[3:] // 截取索引3之后的所有内容
使用append()
方法为切片追加元素:
var xin []int
xin = append(xin, 1)
xin = append(xin, 1, 2, 3)
var ain []int
xin = append(xin, ain...)
在切片的内部实现中,它是由这样的结构体组成的:
type slice struct {
array unsafe.Pointer
len int
cap int
}
- array 是指向一个数组的指针;
- len是数组的长度;
- cap 是当前切片的容量,即 array 数组的大小。
因此跟数组不同的是,切片的array是指针,是引用类型的。如果将切片拷贝给另一个切片,他们会分享同一个底层数组,这点需要注意。
切片本质上是一个包含了长度、容量和底层数组指针的结构体。
map
map是一种无序的key-value数据结构,也叫映射,可以在O(1) 时间复杂度下通过Key获取对应值。
map的键值对使用冒号分隔,在初始化时可以很容易地构建它们:
var xin = map[string]int{
"x": 1,
"i": 2,
"n": 3,
}
v := xin["x"] // v值为1
map和切片都属于引用类型,如果不初始化,默认初始值就是nil。可以使用make()
函数初始化map:
make(map[string]int, 2) // 参数2表示map的容量
make(map[string]int) // 容量是可选项,不填会默认指定一个合适的容量
判断map的key是否有值:
value, ok := map[key] // ok是一个bool值,key中有值时ok是true,否则ok是false
可以用delete
方法删除map中的一个键值对:
delete(map, key)
new与make
函数 make(T, args)
和 new(T)
都可以用于初始化值,但不同之处在于:
make
函数:返回对应数据类型的初始值;
new
函数:为数据类型初始化一个空值,然后返回指向这个内存地址的指针。
控制结构
if
Go中的if需要将语句放在一个大括号内,这个大括号不能省略,就像这样:
if x > 0 {
return y
}
switch
switch可以对大量条件值进行判断:
func switchDemo(value int) {
switch value {
case 1:
fmt.Println("一")
case 2:
fmt.Println("二")
case 3:
fmt.Println("三")
case 4:
fmt.Println("四")
case 5:
fmt.Println("五")
default:
fmt.Println("more")
}
}
如果省略switch的判断变量,就可以使用表达式作为case:
func switchDemo(value int) {
switch {
case value == 2:
fmt.Println("2")
case value == 10:
fmt.Println("10")
case value == 20:
fmt.Println("20")
case value == 50:
fmt.Println("50")
case value == 100:
fmt.Println("100")
default:
fmt.Println("0")
}
}
for
Go中的循环统一了 for 和 while,不再有 do-while 了。它有三种形式,但只有一种需要分号:
// 如同 C 的 for 循环
for init; condition; post { }
// 如同 C 的 while 循环
for condition { }
// 如同 C 的 for(;;) 循环
for { }
若你想遍历数组、切片、字符串或者map, range 子句能够帮你轻松实现循环:
// 遍历 map 的 key 和 value
for key, value := range oldMap {
newMap[key] = value
}
// 遍历数组或切片的值
for value := range list {
fmt.Println(value)
}
函数
函数的定义
Go使用func关键字创建函数,定义如下:
func 函数名(参数) (返回值) {
函数体
}
- 函数名:由字母、数字、下划线组成。
- 参数:由参数的变量和类型组成,多个参数之间用
,
分隔。 - 返回值:返回值由变量或变量类型组成,也可以只写变量类型,多个参数用
()
包裹,多个参数之间用,
分隔。
比如定义一个简单的加法函数,有如下几种写法:
func Add(a, b int) int {
return a + b
}
func Add(a, b int) (num int) {
num = a + b
return
}
func Add(a, b int) (int, string) {
num := a + b
say := "大于1"
if num <= 1 {
say = "小于等于1"
}
return num, say
}
func Add(a, b int) (num int, say string) {
num = a + b
say = "大于1"
if num <= 1 {
say = "小于等于1"
}
return
}
匿名函数
匿名函数就是没有名字的函数,既然函数没有名字,我们怎么调用他呢?匿名函数无法被调用,所以在定义后就会立即执行,比如:
func main() {
func(x, y int) {
fmt.Println(a + b)
}(10, 20)
}
init函数
在Go中每一个源文件都可以定义一个init()
函数,init()
函数无参数,会在程序执行前被调用。可以在init()
函数里做一些初始化、或检验程序状态等操作。
func init() {
// 检验或校正程序的状态
}
defer与recover
defer
defer语句会在函数结束前执行,比如:
func sayDefer() int {
defer sayEnd()
return sayStart()
}
func sayStart() {
fmt.Println("start")
}
func sayEnd() {
fmt.Println("end")
}
输出结果为:
start
end
如上,defer会在return之后执行。
多个defer的执行顺序类似“栈”结构,先进后出。比如:
package main
import "fmt"
func main() {
defer func1()
defer func2()
defer func3()
}
func func1() {
fmt.Println("A")
}
func func2() {
fmt.Println("B")
}
func func3() {
fmt.Println("C")
}
输出结果为:
C
B
A
defer与panic
如果程序中某处导致的错误使得程序无法运行,此时程序会被终结,这就是触发了panic。
Go中还提供了内建的panic
函数,它可以产生一个运行时错误并终止程序。
即使遇到了panic,defer依然会在程序终结前执行。我们可以使用这个特性,在defer中捕获panic,并做处理,这就涉及到了Go的recover
函数。
defer与recover
当 panic 被调用后,程序将立刻终止当前函数的执行,并开始回溯程序运行的栈结构,运行任何被推迟的defer函数。回溯完成后,程序就会终止。不过我们可以用内建的 recover
函数来重新取回程序的控制权限并使其恢复正常执行。
func deferRecover() {
defer func() {
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()
……
// 触发panic
}
在此例中,deferRecover方法的后续代码触发了panic,其结果就会被recover函数捕获到,记录下错误信息,然后这个方法会结束,但不会干扰到程序的运行。
结构体
结构体的定义
结构体是一种面向对象的数据结构,可以用于封装多个基本数据类型。
在Go中使用type
和struct
关键字来定义结构体,比如定义一个user
结构体如下:
type User struct {
Name string
Age int8
}
多个结构体之间可以嵌套:
type User struct {
Name string
Age int8
Relation
}
type Relation struct {
Following int
UserCoin string
}
使用var
关键词实例化结构体:
type User struct {
Name string
Age int8
}
var xin User
xin.Name = "name"
**请注意:**结构体内的字段Name、Age开头字母大写时可以被其他包调用,开头字母小写时则只能在定义结构体的文件中调用它。
在声明时初始化结构体:(如果没有指定初始值,结构体中的字段值就是该字段类型的默认零值)
xin := &User{
Name: "xin",
Age: 23,
}
结构体作为接收者变量
请看以下示例:
type User struct {
Name string
Age int8
}
func (u *User) ShowUserName() {
fmt.Println(u.Name)
}
func main() {
xin := &User{
Name: "xin",
Age: 23,
}
xin.ShowUserName()
}
ShowUserName方法前面定义的(u *User)
就是接收者变量,类似于其他语言的this、c或self。接收者变量的结构体是指针,所以在方法内可以通过调用结构体直接修改其中的值。
结构体标签
结构体标签是对结构体字段的额外信息标签,在运行的时候会通过反射的机制读取出来。
结构体标签由一个或多个键值对组成,并用单引号包裹。
如下示例,就利用结构体标签定义了json字段,当需要将结构体转为json或从json转为结构体时就会用到结构体标签中定义的json字段:
type User struct {
Name string `json:"name"`
Age int8 `json:"age"`
}
除了json转换,Go的一些orm库也会用结构体标签来对应数据库中的字段。
结构体的内存对齐
假设有以下结构体:
type User1 struct {
Age int8
CreateTime int64
Number int32
}
type User2 struct {
Age int8
Number int32
CreateTime int64
}
func main() {
fmt.Println(runtime.GOARCH) // amd64
user1 := User1{}
fmt.Println(unsafe.Sizeof(user1)) // 32 bytes
user2 := User2{}
fmt.Println(unsafe.Sizeof(user2)) // 24 bytes
}
输出结果为:
amd64
32
24
以上示例中,2个结构体的字段是一致的,只是调整了字段的顺序,占用的内存空间就不一样了。为什么呢?这就涉及到CPU的内存分配了。
现在买的电脑一般都是64位的,CPU一次能处理的最大位数为64位,也就是8个字节(同理,32位就是4个字节)。64位电脑的CPU一次可以处理8字节数据,但是内存也会以8字节为单位存储,那如果CPU抓取的字节数超过了8,CPU就要处理2次。
以第一个结构体为例:
type User1 struct {
Age int8 // 1 bytes
CreateTime int64 // 8 bytes
Number int32 // 4 bytes
}
int8的字节数是1,int64的字节数是8,int32的字节数是4。
当CPU开始处理,由于内存会以8字节为单位存储,Age会占用1字节,CreateTime会占用8字节,如果把这2个放到一起存储是放不下的,只能分开。此时CPU就会抓取2次,第1次抓Age,第2次抓CreateTime。这样两次都没有用满8字节。第3次抓Number。总共抓3次,每次都占8字节,于是这个结构体就占用了3*8=24字节。
再看第二个结构体:
type User2 struct {
Age int8 // 1 bytes
Number int32 // 4 bytes
CreateTime int64 // 8 bytes
}
由于Age和Number加在一起小于8字节,CPU只需要抓取1次,然后再抓取1次CreateTime。这样总共抓取2次,每次是8字节,于是这个结构体就占用了2*8=16字节。
如果一个结构体中有大量的字段,并且被大规模的引用,我们就通过调整结构体字段的顺序以减少内存空间占用。但大部分情况下我们是不需要考虑的,知道有这回事就行了,结构体成为瓶颈的可能性微乎其微。
为结构体数组排序
Go的sort包提供了为结构体数组排序的方法。但是需要我们手动实现Len,Swap,Less这3个方法,即获取数组长度,交换数组元素,判断大小。
以结构体MyGiftsData为例:
type MyGiftsData struct {
Num int `json:"num"`
HasGet bool `json:"has_get"`
}
假设我们有无序数组[]MyGiftsData,需要按照字段Num值排序,可以这样做:
type MyGiftsList []MyGiftsData
func (m MyGiftsList) Len() int {
return len(m)
}
func (m MyGiftsList) Swap(i, j int) {
m[i], m[j] = m[j], m[i]
}
func (m MyGiftsList) Less(i, j int) bool {
return m[i].Num > m[j].Num
}
如上为数组MyGiftsList定义了Len,Swap,Less这3个方法。
接下来调用sort包的Sort方法即可为该结构体数组排序:
sort.Sort(model.MyGiftsList(List)) // List 是需要排序的结构体数组
接口
接口的定义
接口是一组方法的集合,Go语言提倡面向接口编程。
接口类型可以用interface
关键字定义:
type 接口名 interface {
// 方法
}
例如:
type ArticleRepo interface {
// db
ListArticle(ctx context.Context) ([]*Article, error)
GetArticle(ctx context.Context, id int64) (*Article, error)
CreateArticle(ctx context.Context, article *Article) error
UpdateArticle(ctx context.Context, id int64, article *Article) error
DeleteArticle(ctx context.Context, id int64) error
// redis
GetArticleLike(ctx context.Context, id int64) (rv int64, err error)
IncArticleLike(ctx context.Context, id int64) error
}
ArticleRepo接口定义了操作数据库的db和redis方法,通过对这些方法的实现,就让代码结构更加清晰,简单。
通过在业务层定义接口,持久层实现接口的方式,就完成了依赖倒置的设计。依赖倒置就是指设计代码结构时,高层模块不应该依赖低层模块,二者都应该依赖其抽象(在go中就是接口interface)。通过依赖倒置,可以减少类与类之间的耦合性,降低修改程序所造成的风险。
空接口
接口本质上是一种自定义类型,一种特殊的自定义类型,其中没有数据成员,只有方法(也可以为空),也因此Go中的所有数据类型都可以实现一个空的接口。我们可以用这个空的接口存储任意数据类型的值。
如果你无法确定值的数据类型,就可以定义一个空接口来存储。比如当需要将不同数据类型的值存储到一个map数据结构中,就可以将map的值定义为空接口interface{}
:
reqData := map[string]interface{}{
"uids": uids,
}
我们还可以用空接口来做类型选择,把未知类型的值存储到一个空接口变量,通过switch判断值的类型。
并发
什么是并发?
如果我们无法确定某个事件发生在另一个事件之前或之后,我们就说这2个事件是并发的。
goroutine
进程、线程、协程
程序启动时,操作系统会创建一个进程用于运行该程序,进程内包含了运行程序所需的资源。
线程是一种执行方法,一个进程的运行会从一个主线程开始,主线程可以依次启动更多的线程,用于执行程序中的代码。当主线程终止时,进程也就结束了。
goroutine是协程的Go语言实现,与线程的区别在于,goroutine是由Go的运行时(runtime)调度和管理的,Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU,因为Go在语言层面内置了调度和上下文切换的机制,既减少了线程频繁切换造成的开销,也使得用goroutine处理并发变的非常简单。
事实上在Go中使用goroutine确实非常简单,只需要一个go关键字,就可以在调用函数时创建一个goroutine了:
func main() {
go say() // 启动一个goroutine去执行say函数
go func() { // 启动匿名函数goroutine
fmt.Println("hi")
}
}
就这么一个简单的关键字,让Go语言和其他语言拉开了一个身段,使得Go语言编写并发代码非常简单。
管理多个goroutine
goroutine启动后就会在后台运行,如果此时程序运行结束,启动中的goroutine也会被销毁。为了管理goroutine,我们可以使用Go的sync包的WaitGroup方法实现goroutine的同步:
var wg sync.WaitGroup
func hello(i int) {
defer wg.Done() // goroutine结束 -1
fmt.Println(i)
}
func main() {
for i := 0; i < 100; i++ {
wg.Add(1) // goroutine启动 +1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束才会往下走
}
此时程序就会等待所有goroutine都Done了之后才结束。
channel
channel的定义
channel用于实现goroutine之间的通信,就像是架设在goroutine之间的管道,遵循先入先出的规则交换数据。
可以用make函数声明channel并分配内存,支持不同数据类型的channel:
ch1 := make(chan int)
ch2 := make(chan bool)
ch3 := make(chan []int)
channel有发送(send)、接收(receive)和关闭(close)三种操作,以下为示例。
发送:
ch <- 10 // 把10发送到channel中
接收:
num := <- ch // 从channel中接收值并赋值给变量num
<-ch // 从channel中接收值,并忽略结果
关闭:
close(ch)
Go 的 goroutine 和 channel为构造并发软件提供了一种优雅而独特的方法。Go 没有显式地使用锁来协调对共享数据的访问,而是鼓励使用 chan 在 goroutine 之间传递对数据的引用。这种方法确保在给定的时间只有一个 goroutine 可以访问数据。
有缓冲channel与无缓冲channel
我们使用make创建channel,当设置了容量就称为有缓冲channel,没设置容量就称为无缓冲channel:
ch := make(chan int) // 无缓冲channel
ch := make(chan int, 1) // 有缓冲channel,容量为1
无缓冲channel没有容量,因此进行任何交换前需要两个 goroutine 同时准备好。无缓冲channel上的发送操作会阻塞,直到另一个goroutine在该channel上执行接收操作。因此无缓冲channel相当于同步通道。
有缓冲channel设置了容量,设置的容量就是channel中能存放的元素数量。有缓冲channel直到容量满了才会阻塞。
Package sync
竞争检测
当两个或多个 goroutine 访问同一个变量,并尝试对该变量进行读写而不考虑其他 goroutine。就很可能导致难以定位的bug。
Go官方引入了一个竞争检测工具,在构建过程中内置到程序中的代码,一旦程序运行,就能检测并报告这种竞争条件。
使用方法:
go build -race
go test -race
sync.WaitGroup
sync的WaitGroup包在上面的章节“goroutine - 管理多个goroutine”这节已经介绍过了,这里再简单说一下:
WaitGroup内部维护着一个计数器,我们通过Add方法、Done方法和Wait方法管理这个计数器,通过调用Wait方法来等待并发任务执行完,直到计数器值为0时,就表示所有并发任务已经完成。
sync.Map
Go语言的map数据结构不是并发安全的,如果我们要在多个goroutine中读写一个Map,就必须要使用sync的并发安全版Map。
声明:
var m = sync.Map{}
存储:
m.Store(key, n)
读取:
value, _ := m.Load(key)
atomic
atomic提供了针对不同数据结构的原子读写,用以保证并发安全。
例如不同数据类型的读取方法为:
- func LoadInt32(addr *int32) (val int32)
- func LoadInt64(addr *int64) (val int64)
不同数据类型的写入方法为:
- func StoreInt32(addr *int32, val int32)
- func StoreInt64(addr *int64, val int64)
不同数据类型的修改方法为:
- func AddInt32(addr *int32, delta int32) (new int32)
- func AddInt64(addr *int64, delta int64) (new int64)
不同数据类型的交换方法为:
- func SwapInt32(addr *int32, new int32) (old int32)
- func SwapInt64(addr *int64, new int64) (old int64)
错误处理
Error
Go中的error 就是一个接口,提供一个普通的值。
我们可以用errors.New()
方法获取一个error对象。实际上它返回的是一个对象的指针,这个对象里放了一个字符串,这个字符串就是错误的提示。Go在此处使用指针而不是直接使用字符串,这是为了防止创建2个内容相同的error导致的意外判等情况。
虽然Go需要在代码的各个地方判断err≠nil,稍显麻烦,但是这可以帮助我们快速定位到问题。
panic
对于那些表示不可恢复的程序错误,例如索引越界,可以直接引发panic,终止程序运行。对于其他的业务错误,我们还是期望使用 error 来进行判定。
Wrap
errors包的Wrap函数可以通过包裹error,以返回一个像panic一样的栈结构信息,这可以让我们更方便的查出具体的错误出处。既然打印了完整的栈结构,我们也就不需要在每一层都打印error了。
但需要注意的是,Warp应该在业务的应用上使用,而不是在会有很多项目调用的基础库上使用,因为其他项目可能使用其他的日志库,导致日志打印的冗余。
接下来做什么
恭喜,你已经学会使用Go语言了!🎉
接下来可以在实践中进一步熟悉Go语言,比如使用Go创建一个项目;阅读Go的源码;了解一些更深入的Go知识;为开源的Go项目做出贡献;等等……
转载自:https://juejin.cn/post/7095950612630044686