文章目录
前言
学习go基础过程中的学习笔记,会把我碰到的一些难点、易忘点记录下来。
学习过程中参考资料
一、数据类型
1.字符类型
uint8 类型又叫 byte 型,代表了 ASCII 码的一个字符。
rune 类型,代表一个 Unicode 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型等价于 int32 类型。
ASCII 码的一个字符占一个字节
2.字符串类型
中文占三字节,字母占一个字节
如果使用``反引号,会被原样进行赋值和输出
fmt.Println(`\t ms的go教程
Go大法好`)
string是不可变,也就是初始化后不能再修改其值,除非重新赋值
str := "mszlu"
//str[0] = "d" //不行
str = "hello mszlu" //ok
3.修改字符串
字符数组实际上就是字符串,所以[]byte
和[]rune
可以和string互相转换。Golang字符串不可变,转换[]byte
进行修改,使用强制类型转换。
s1 := "localhost:8080"
// 强制类型转换 string to byte
strByte := []byte(s1)
// 下标修改
strByte[len(s1)-1] = '1'
// 强制类型转换 []byte to string
s2 := string(strByte)
二、变量内存常量
1.变量赋值
Go会自动推导其类型
func main() {
var a = "mszlu"
}
在函数内部也可以写成
// a := "mszlu" 不能再函数外部书写
func main() {
a := "mszlu"
}
多个变量定义
var (
a int
b string
c []float32
)
2.iota
在golang中并没有枚举这种类型,但在go中有类似的方式,就是使用iota常量生成器。
从定义开始,iota值从0加1
const (
January = 1 + iota
February
March
)
奇数定义
//go中值部分 可以写表达式
const (
Odd1 = 2*iota + 1
Odd2
Odd3
)
3.作用域
函数内部声明的变量,为局部变量,作用域仅限于函数内部
函数外部声明的变量,为全局变量,作用域在当前包有效
如果首字母大写,在所有包有效
{}
包括的区域,称之为代码块,如果变量定义在{}
内部,只在代码块内有效
三、运算符
1.位运算符
&
参与运算的两数各对应的二进位相与。(两位均为1才为1)
|
参与运算的两数各对应的二进位相或。(两位有一个为1就为1)
^
参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。(两位不一样则为1)
<<
左移n位就是乘以2的n次方。a<<b
是把a的各二进位全部左移b位,高位丢弃,低位补0。
>>
右移n位就是除以2的n次方。a>>b
是把a的各二进位全部右移b位。
四、指针
1.指针操作
&
是取地址操作符
*
是取值操作符
*数据类型
是指针类型
new
是提前在内存中申请一块内存
func main() {
//声明指针 此时a为nil
var a *int
//初始化后的内存空间 其值为对应类型的零值
a = new(int)
*a = 10
fmt.Println(*a)
}
五、函数
1.函数定义
//类型相同的相邻参数,参数类型可合并。
//在定义函数时 x,y这两个参数,称之为形参
func swap(x, y int) (int, int) {
return y, x
}
2.函数的使用
函数既然可以做为一个变量,那么其可以做为参数传递
和做为返回值
func main() {
var swap func(x, y int) (int, int)
swap = func(x, y int) (int, int) {
return y, x
}
r := call(swap)
r(10)
}
func call(swap func(x, y int) (int, int)) func(x int) {
y, _ := swap(10, 20)
return func(x int) {
fmt.Println(y + x)
}
}
Go语言中参数传递为值传递,函数接收参数时,是接收的一个副本,实参copy了一份给形参
func main() {
var a = 10
var b = 20
changeValue(a, b)
fmt.Println("不影响:", a, b)
changeValuePoint(&a, &b)
fmt.Println("影响:", a, b)
}
// 在函数中修改值 不会影响 a和b
func changeValue(x, y int) {
x = 50
y = 100
}
// 虽然是值传递 由于传递的是地址 所以修改值 会影响a和b
func changeValuePoint(x, y *int) {
*x = 50
*y = 100
}
指针类型在参数传递过程中,只占用4字节(32位系统)或8字节(64位系统)的大小,内存开销小
- 命名返回值
// 命名返回值 相当于局部变量 return隐式返回
func yes(x, y int) (a int, b int, info string) {
a = 10
b = 20
info = "hello"
return
}
3.匿名函数
匿名函数就是没有函数名的函数。
func main() {
//这是一个匿名函数
funA := func() int {
return 20
}
//其实在这里funA就是函数的名字
funA()
//这是一个匿名函数调用 可以不用将函数声明为一个变量在使用
func() {
fmt.Println("这是一个匿名函数")
}()
}
4.闭包
所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
闭包=函数+引用环境,这里可以简单的理解为闭包是函数内部的匿名函数
func main() {
// 创建一个玩家生成器
generator := playerGen("码神")
// 返回玩家的名字和血量
name, hp := generator()
// 打印值
fmt.Println(name, hp)
}
// 创建一个玩家生成器, 输入名称, 输出生成器
func playerGen(name string) func() (string, int) {
// 血量一直为150
hp := 150
// 返回创建的闭包
return func() (string, int) {
// 将变量引用到闭包中
return name, hp
}
}
六、数组
1.数组的使用
初始化
func main() {
var arr [3]int = [3]int{1, 2, 3}
//如果第三个不赋值,就是默认值0
var arr1 [3]int = [3]int{1, 2}
//可以使用简短声明
arr2 := [3]int{1, 2, 3}
//如果不写数据数量,而使用...,表示数组的长度是根据初始化值的个数来计算
arr3 := [...]int{1, 2, 3}
//给索引为2的赋值 ,所以结果是 0,0,3
arr4 := [3]int{2: 3}
fmt.Println(arr, arr1, arr2, arr3, arr4)
}
for range
func main() {
var arr [3]int = [3]int{1, 2, 3}
for index, value := range arr {
fmt.Printf("索引:%d,值:%d \n", index, value)
}
}
2.切片
copy属于深拷贝(浅拷贝是只copy地址,深拷贝是值copy)
可以使用append对切片新增元素
切片有长度和容量,容量代表实际其占用的内存空间,当我们在给切片添加元素时,内存占用会变多,这时候就会发生频繁的内存空间分配,这是比较耗费性能的,所以go做了一些优化。
func main() {
slice1 := []int{1, 2, 3, 4, 5}
slice1 = append(slice1, 10)
//打印 长度:6,容量:10
fmt.Printf("长度:%d,容量:%d", len(slice1), cap(slice1))
}
大家发现,容量为什么是10,而不是6呢?
为了防止频繁发生内存分配,在append增加元素时,如果容量不足,在扩容时,会做一定的策略来优化:
- 容量小于256时,两倍扩容
- 容量大于等于256时,按照newcap += (newcap + 3*256) >> 2这个公式扩容,越大扩容越小,直到1.25倍
- 实际的容量,在上述的基础上,还会进行内存对齐
- 这里引申出一个概念叫内存对齐:
因为CPU访问的规则,未对齐的内存,会造成CPU多次访问,耗费性能
切片转数组
func main() {
//go1.20新特性
slice1 := make([]int, 2, 8)
arr := [2]int(slice1)
fmt.Println(arr)
}
七、流程控制
1.if
特殊写法将a的作用范围限制在if表达式中,在编程中,限制变量的作用范围对代码的稳定性有很大的帮助,因为作用范围越小,造成的影响也越小。
if a := 10; a >5 {
fmt.Println(a)
return
}
2.for
go语言中的循环语句只支持 for 关键字。
sum := 0
//i := 0; 赋初值,i<10 循环条件 如果为真就继续执行 ;i++ 后置执行 执行后继续循环
for i := 0; i < 10; i++ {
sum += i
}
//无限循环
for{
//无限循环代码块
}
func main() {
n := 0
//for后面只加条件
for n < 3 {
n++
}
fmt.Println(n)
}
func main() {
var slice []int = []int{1, 2, 3, 4, 5}
//for range循环切片或数组
for index, value := range slice {
fmt.Printf("index:%d,value:%d \n", index, value)
}
}
func main() {
//可以循环字符串 字符串底层为一个byte切片
//字符串大小为16字节,其有一个指针和int型长度组成
for i, j := range "mszlu" {
fmt.Printf("The index number of %c is %d\n", j, i)
}
}
func main() {
//循环map
mmap := map[int]string{
22: "mszlu",
33: "com",
44: "learn go",
}
for key, value := range mmap {
fmt.Println(key, value)
}
}
func main() {
//循环channel
chnl := make(chan int)
go func() {
chnl <- 100
chnl <- 1000
chnl <- 10000
chnl <- 100000
close(chnl)
}()
for i := range chnl {
fmt.Println(i)
}
}
func main() {
//go1.22版本 支持整型的循环
//注意更新IDE版本,比如goland需要安装新版
for i := range 10 {
fmt.Println(i)
}
}
加了fallthrough后,会直接运行【紧跟的后一个】case或default语句,不论条件是否满足都会执行
func main() {
var s = "hello"
switch {
case s == "hello":
fmt.Println("hello")
fallthrough
case s == "world":
fmt.Println("world")
}
}
结束循环方式
func main() {
step := 2
for step > 0 {
step--
fmt.Println(step)
//执行一次就结束了
return
}
//不会执行
fmt.Println("结束之后的语句....")
}
func main() {
step := 2
for step > 0 {
step--
fmt.Println(step)
//跳出循环,还会继续执行循环外的语句
break
}
//会执行
fmt.Println("结束之后的语句....")
}
func main() {
step := 2
for step > 0 {
step--
fmt.Println(step)
//报错了,直接结束
panic("出错了")
}
//不会执行
fmt.Println("结束之后的语句....")
}
func main() {
for x := 0; x < 10; x++ {
if x == 2 {
// 跳转到标签
goto breakHere
}
}
// 手动返回, 避免执行进入标签
return
// 标签
breakHere:
fmt.Println("done")
}
八、map和结构体
1.map
map是引用类型,零值是nil,所以必须初始化后才能使用
key和value是成对出现,key是唯一的
func main() {
//int为key的类型 string为value的类型
var m map[int]string = map[int]string{}
//2是代表容量 也就是在内存中占用多大的空间
//指定容量 有助于不额外占用内存
//make方式 var m = make(map[int]string, 2)
m[1] = "mszlu"
m[2] = "新版go教程"
}
从map取值有两种方式
func main() {
m := make(map[string]string)
m["key"] = "value"
m["key1"] = "value1"
//通过key取值
v, ok := m["key"]
if ok {
fmt.Println(v)
}
//通过for range取值 注意无序的
//想要有序的结果,可以遍历后对key排序
for k, v := range m {
fmt.Println(k, v)
}
}
2.结构体
字段(成员变量)首字母大写代表访问权限为public(对所有包可见)
字段(成员变量)首字母小写代表访问权限为private(仅对本包有效)
结构体的定义只是一种
内存布局
的描述,只有当结构体初始化时,才会真正的分配内存
函数参数传递为值传递,如果将结构体做为参数,会频繁的进行复制,也就是
会频繁的分配内存
推荐在使用结构体传参时,使用指针,也就是说推荐使用*T,如果使用切片推荐使用[]*T
func main() {
//通过&取地址符号,获取结构体User的指针
user := &User{}
//或者使用new关键字 user := new(User)
change(user)
fmt.Printf("name:%s,age:%d \n", user.Name, user.Age)
}
func change(user *User) {
user.Name = "mszlu"
user.Age = 20
}
结构体中的字段可以没有名字,这个字段称为匿名字段或者叫内嵌字段,一个结构体中可以包含多个匿名字段
type Human struct {
Sex int
}
//实际上是一种组合的方式实现
type Man struct {
Name string
Human
}
func main() {
m := Man{}
m.Sex = 1
fmt.Println(m)
}
3.方法
方法
和函数
类似,唯一的不同,方法
拥有一个接收器
,这个接收器可以是任意类型(除了接口类型)
func main() {
u := User{}
u.ModifyName("new")
fmt.Println(u)
}
type User struct {
Name string
}
//如果这不用指针 修改会不成功
//推荐接收器使用指针形式
//(a *User)这个就叫接收器
func (a *User) ModifyName(name string) {
a.Name = name
}
type Hp int
func (a Hp) Get() Hp {
return a + 100
}
当方法作用于非指针接收器时,Go语言会在代码运行时将接收器的值复制一份,在非指针接收器的方法中可以获取接收器的成员值,但
修改后无效
。
4.内存对齐
- 现代计算机中
内存空间
都是按照字节(byte)
进行划分的 - 理论上讲对于任何类型的变量访问都可以从
任意地址
开始 - 有些
CPU
可以访问任意地址上的任意数据,而有些CPU
只能在特定地址访问数据,因此不同硬件平台具有差异性,这样的代码就不具有移植性 - 如果在编译时,将分配的内存进行对齐,就具有平台移植性
CPU
每次寻址都是要消费时间的CPU
访问内存时,并不是逐个字节访问,而是以字长(word size)
为单位访问64位CPU
在单位时间能处理字长为64位
的二进制数据,字长为8字节
- 数据结构应该尽可能地在自然边界上对齐,如果访问
未对齐的内存
,处理器需要做两次
内存访问,而对齐的内存
访问仅需要一次
访问,内存对齐后可以提升性能。
九、接口
1.接口定义
接口(interface)
是一种类型,它是描述了一组方法
的集合。
type Animal interface {
//Say 动物可以说话
Say()
//Move 动物可以移动
Move()
//Jump 动物可以跳起来
Jump()
}
2.接口实现
实现关系在Go语言中是隐式的。两个类型之间的实现关系不需要在代码中显式地表示出来。
Go编译器将自动在需要的时候检查两个类型之间的实现关系。
只要实现接口的类型方法和接口方法一致,我们就说此类型实现了方法。
type Animal interface {
//Say 动物可以说话
Say()
//Move 动物可以移动
Move()
//Jump 动物可以跳起来
Jump()
}
type Dog struct {
}
func (d *Dog) Move() {
fmt.Println("狗在跑")
}
func (d *Dog) Jump() {
fmt.Println("狗在跳")
}
func (d *Dog) Say() {
fmt.Println("汪汪汪")
}
type Cat struct {
}
func (c *Cat) Say() {
fmt.Println("喵喵喵")
}
func main() {
//这里Dog实现了Animal接口,Cat没有实现
var dog Animal = &Dog{}
dog.Say()
}
接口实现规则:必须实现接口的所有方法,并且方法的名称,参数,返回值类型都相同
type Fighter interface {
Hurt() int
}
func main() {
var dog Animal = &Dog{}
dog.Say()
//一个类型可以实现多个接口 没有限制
//反过来 一个接口可以被多个类型实现
//只需要符合接口实现规则即可
var fight Fighter = &Dog{}
fmt.Println("造成伤害:", fight.Hurt())
}
接口可以嵌套
type Sayer interface {
Say()
}
type Mover interface {
Move()
}
type Animal interface {
Sayer
Mover
//Jump 动物可以跳起来
Jump()
}
3.空接口
- 空接口是没有方法的接口
- 因此任何类型都实现了空接口
- 空接口类型的变量可以存储任意类型的变量
type Any interface{}
func main() {
var a Any
a = 10
fmt.Println(a)
}
为了方便空接口的使用,所以go提供了一个类型别名any
4.类型断言
x.(T)
x:表示类型为interface{}的变量
T:表示断言x可能是的类型。
func main() {
var x interface{}
x = "码神之路"
v, ok := x.(string)
if ok {
fmt.Println(v)
} else {
fmt.Println("类型断言失败")
}
}
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("x is a string,value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
default:
fmt.Println("unsupport type!")
}
}
5.泛型
//T就是泛型
func min[T int | float64 | float32 | int64](x, y T) T {
if x < y {
return x
}
return y
}
func main() {
//可以通过给min提供类型参数 从而验证类型参数是否可用
mInt := min[int]
fmt.Println(mInt(2, 3))
fmt.Println(min(2.3, 4.5))
}
//实际在接口中除了定义方法,也可以嵌入非接口类型
type AllBaseType interface {
int | float64 | float32 | int64
}
func min[T AllBaseType](x, y T) T {
if x < y {
return x
}
return y
}
看到
~int
这样的表达式,~
标记表示的是底层类型为int的所有类型,包括int
//也可以用于结构体和接口中
type User[T int] struct {
Name T
}
func main() {
user := User[int]{}
}
十、并发
1.协程
协程就是用户态下的轻量级线程
,英文叫Coroutine
,Go语言的协程叫做Goroutine
(是由Go 和 Coroutine拼接出来的词)。
- 协程的调度由应用程序控制
- 协程的上下文切换由于不需要内核参与,所以开销很小
2.golang的线程模型
Golang在底层实现了混合型线程模型。
M代表着系统线程,一个M关联一个KSE,即两级线程模型中的系统线程。G为Groutine,即两级线程模型的的应用级线程。M与G的关系是N:M。
3.Goroutine
在go语言中,我们只需要使用go关键字
就可以很轻松的开启一个协程,Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU,从而实现并发执行,轻松实现高并发
程序。
func hello(i int) {
fmt.Println("Hello Goroutine!" , i)
}
func main() {
for i := 0; i < 10; i++ {
go hello(i)
}
fmt.Println("main goroutine done!")
time.Sleep(time.Second * 2)
}
4.channel
虽然我们可以通过共享内存(比如全局变量)的方式进行通信,但共享内存容易引发
竞态问题
,就需要加锁
处理,这样会造成性能问题。
Go语言的并发模型是CSP(Communicating Sequential Processes)
,提倡通过通信共享内存
而不是通过共享内存而实现通信
。
Go 语言中的通道(channel)
是一种特殊的类型。通道像一个传送带或者队列
,总是遵循先入先出,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
channel是一种类型,一种引用类型。声明通道类型的格式如下:
var 变量名称 chan 类型
func main() {
// 声明一个传递整型的通道
var ch1 chan int
//channel的零值为nil
fmt.Println(ch1)//nil
}
声明通道后需要使用make函数初始化之后才能使用。
创建channel的格式如下:
make(chan 元素类型, [缓冲大小])//缓冲大小可选
通道有发送(send)
、接收(receive)
和关闭(close)
三种操作。
发送和接收都使用<-
符号。
func main() {
ch := make(chan int, 1)
//发送 将10写入通道
ch <- 10
//接收 从ch中取值
x := <-ch
fmt.Println(x)
//如果close channel则不能在做发送操作(写操作)
close(ch)
ch <- 10
}
关于关闭通道需要注意的事情是,只有在
通知接收方goroutine所有的数据都发送完毕
的时候才需要关闭通道。通道是可以被垃圾回收机制回收
,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的
。
关闭后的通道有以下特点:
- 对一个关闭的通道再发送值就会导致panic。
- 对一个关闭的通道进行接收会一直获取值直到通道为空。
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
- 关闭一个已经关闭的通道会导致panic。
4.1 无缓冲和有缓冲
无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。
func recv(c chan int) {
ret := <-c
fmt.Println("接收成功", ret)
}
func main() {
ch := make(chan int) // 缓冲为0
go recv(ch) // 启用goroutine从通道接收值
ch <- 10
fmt.Println("发送成功")
}
无缓冲:到付快递,必须有人签收才行
有缓冲:先把快递放到快递柜,接收者可以随时去快递柜取
4.2 从通道取值
func main() {
ch1 := make(chan int)
// 开启goroutine将0~100的数发送到ch1中
go func() {
for i := 0; i < 100; i++ {
ch1 <- i
}
close(ch1)
}()
for {
i, ok := <-ch1 // 通道关闭后再取值ok=false
if !ok {
break
}
fmt.Println(i)
}
}
func main() {
ch1 := make(chan int)
// 开启goroutine将0~100的数发送到ch1中
go func() {
for i := 0; i < 100; i++ {
ch1 <- i
}
close(ch1)
}()
for i := range ch1 { // 通道关闭后会退出for range循环
fmt.Println(i)
}
}
func main() {
ch1 := make(chan int)
// 开启goroutine将0~100的数发送到ch1中
go func() {
for i := 0; i < 100; i++ {
ch1 <- i
}
close(ch1)
}()
for {
select {
case i, ok := <-ch1:
if ok {
fmt.Println(i)
}
default:
fmt.Println("default")
}
}
}
5.select
Go 语言中的 select
语句是一种用于多路复用通道的机制
,它允许在多个通道上等待并处理消息。
select {
case <- channel1:
// channel1准备好了
case data := <- channel2:
// channel2准备好了,并且可以读取到数据data
case channel3 <- data:
// channel3准备好了,并且可以往其中写入数据data
default:
// 没有任何channel准备好了
}
func main() {
ch := make(chan int)
go func() {
time.Sleep(3 * time.Second)
ch <- 1
}()
select {
case data, ok := <-ch:
if ok {
fmt.Println("接收到数据: ", data)
} else {
fmt.Println("通道已被关闭")
}
case <-time.After(2 * time.Second):
fmt.Println("超时了!")
}
}
6.锁
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。
var x int64
var wg sync.WaitGroup
var lock sync.Mutex
func add() {
for i := 0; i < 5000; i++ {
lock.Lock() // 加锁
x = x + 1
lock.Unlock() // 解锁
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
多个goroutine同时等待一个锁时,唤醒的策略是随机的。
互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。
读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。
如果使用互斥锁,我们会发现耗时很长,当读多写少的场景,使用读写锁可以提供性能
十一、包
1.包定义
Go包是一个基本单元,Go应用本质上就是一组Go包的集合。
Go包还是编译时的基本单位,一个包内的所有文件会编译为一个目标文件,这种使用包做为编译单元,有助于提供编译效率和管理依赖
2.包导入路径
包的绝对路径就是GOROOT/src/或GOPATH后面包的存放路径
import "lab/test"
import "database/sql/driver"
import "database/sql"
- test 包是自定义的包,其源码位于GOPATH/lab/test 目录下;
- driver 包的源码位于GOROOT/src/database/sql/driver 目录下;
- sql 包的源码位于GOROOT/src/database/sql 目录下。
3.包引用
自定义别名引用格式
import F "fmt"
省略引用格式
package main
import . "fmt"
func main() {
//不需要加前缀 fmt.
Println("码神之路")
}
匿名引用格式
匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中。
使用标准格式引用包,但是代码中却没有使用包,编译器会报错。如果包中有 init 初始化函数,则通过import _ “包的路径” 这种方式引用包,仅执行包的初始化函数,即使包没有 init 初始化函数,也不会引发编译器报错。
package main
import (
_ "database/sql"
"fmt"
)
func main() {
fmt.Println("码神之路")
}
- 一个包可以有多个 init 函数,包加载时会执行全部的 init 函数,但并不能保证执行顺序,所以不建议在一个包中放入多个 init 函数,将需要初始化的逻辑放到一个 init 函数里面。
- 包不能出现环形引用的情况,比如包 a 引用了包 b,包 b 引用了包 c,如果包 c 又引用了包 a,则编译不能通过。
- 包的重复引用是允许的,比如包 a 引用了包 b 和包 c,包 b 和包 c 都引用了包 d。这种场景相当于重复引用了 d,这种情况是允许的,并且 Go 编译器保证包 d 的 init 函数只会执行一次。
十二、错误
1.error
- error 应该是函数的最后一个返回值。
- 当 error 不为 nil 时,不应该对其他返回值有所期待。
- 只需在error最后出现的位置打印错误即可
var MoneyNotEnough = errors.New("余额不足")
func pay(money float64) error {
if money < 10 {
return MoneyNotEnough
// 若换成return errors.New("余额不足")返回不是同一个指针
}
return nil
}
func main() {
err := pay(5)
if errors.Is(err, MoneyNotEnough) {
fmt.Println("两个错误一致")
}
}
errors.New返回的是一个指针,每次New得到的地址都不同,这种设计可以防止如果两个错误是不同的错误,但是其中的字符串相同,errors.Is不会判断其是一种错误(可能面试会问)
- 除了使用errors.New 返回错误,我们也可以使用fmt.Errorf()
func pay(money float64) error {
if money < 10 {
return fmt.Errorf("余额不足:%f", money)
}
return nil
}
- errors.As()用于将error转换为具体的error类型
type MyError struct {
Msg string
}
func (e *MyError) Error() string {
return e.Msg
}
var MoneyNotEnough = &MyError{Msg: "余额不足"}
func pay(money float64) error {
if money < 10 {
return MoneyNotEnough
}
return nil
}
func main() {
err := pay(5)
var myError *MyError
if errors.As(err, &myError) {
fmt.Println("转换成功")
fmt.Println(myError.Error())
} else {
fmt.Println("不是此error类型")
}
}
- 使用fmt.Errorf(),还可以将一个错误进行封装,封装后的错误和之前的相等
func main() {
originalErr := errors.New("原始错误")
//%w 嵌套生成一个新的错误
newError := fmt.Errorf("error: %w", originalErr)
if errors.Is(newError, originalErr) {
fmt.Println("这两个错误相等")
}
//用于将一个错误对象展开,得到下一层错误对象
originalErr1 := errors.Unwrap(newError)
if errors.Is(originalErr, originalErr1) {
fmt.Println("这两个错误相等")
}
}
- 还可以是属于errors.Join进行多个错误的封装,这是go1.20版本的新特性
func main() {
err1 := errors.New("err1")
err2 := errors.New("err2")
err := errors.Join(err1, err2)
if errors.Is(err, err1) {
fmt.Println("err is err1")
}
if errors.Is(err, err2) {
fmt.Println("err is err2")
}
}
2.panic
panic也是一种错误,只是panic会导致程序直接退出。一般只有遇到不可恢复的错误才使用panic。
程序往往会有预料不到的错误发生,会导致panic,导致程序崩溃,这是不可接受的,所以我们可以捕获panic
func main() {
err := pay(5)
//defer 延迟调用,在return结束前运行
defer func() {
//panic会被捕获
if err := recover(); err != nil {
log.Printf("panic:%+v \n", err)
}
}()
if err != nil {
panic("余额不足")
}
}
- 利用recover处理panic指令,defer 必须放在 panic 之前定义
- recover 只有在 defer 调用的函数中才有效
如果发生了panic,我们还想要程序继续执行可以这样做
func main() {
//使用匿名函数 将需要保护的代码 控制在一定范围
func() {
err := pay(5)
defer func() {
if err := recover(); err != nil {
log.Printf("panic:%+v \n", err)
}
}()
if err != nil {
panic("余额不足")
}
}()
fmt.Println("发生panic后 这里的代码依旧会执行")
}
十三、defer
1.defer
让函数或方法(跟在defer后的函数,我们一般称之为延迟函数)在当前函数执行完毕后但在在return或者panic之前执行。
func main() {
x := 10
defer func() {
x++
//这里打印11
fmt.Println("我后执行:", x)
}()
//这里打印10
fmt.Println("我先执行:", x)
return
}
defer有以下规则:
- 延迟函数的参数在defer语句出现时就已经确定
- 延迟函数执行按后进先出顺序执行, 即先出现的defer最后执行
- 延迟函数可以操作主函数的具体返回值
- 如果 defer 执行的函数为 nil, 那么会在最终调用函数的产生 panic
注意: defer一定要定义在return或panic之前,否则会不执行。
func main() {
//打印2 return i 并不是一个原子操作
//return会分两步 1. 设值 2 return
//所以result为先被赋值为i=1
x := deferTest()
fmt.Println(x)
}
func deferTest() (result int) {
i := 1
defer func() {
result++
}()
return i
}
func main() {
//defer不会执行
//主动调用os.Exit不会执行defer
defer func() {
fmt.Println("defer")
}()
//退出进程
os.Exit(-1)
}
十四、context
1.创建根context
-
context.Background():可以创建一个非nil,空值的Context对象,不能发出取消信号,线程安全,通常作为根Context,用于派生其他Context。
-
context.TODO(): 和context.Background()一样,一般做为占位符存在。
-
当一个context被取消后,其派生的context会同样被取消
-
context是线程安全的
2.value context
context可以在多个goroutines之间传值,context.WithValue()
用来在context中存储键值对,它返回一个新的Context,这个新的Context携带了一个键值对。
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "userId", 123)
//这样传参 可以避免显示传递值
go performTask(ctx)
time.Sleep(time.Second)
}
func performTask(ctx context.Context) {
userId := ctx.Value("userId")
fmt.Println("userId:", userId)
//ctx = context.WithValue(ctx, "userId", newValue) 多个协程中修改同一个key,会引起数据竟态问题
}
3.cancel Context
通过context.WithCancel
创建的context可以发出取消信号。
通过取消信号,我们可以终止相关的goroutines,从而避免资源泄露(未能正确释放导致无法回收)。
func main() {
ctx, cancel := context.WithCancel(context.Background())
go performTask(ctx)
time.Sleep(2 * time.Second)
cancel()
time.Sleep(1 * time.Second)
}
func performTask(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Task cancelled")
return
default:
// Perform task operation
fmt.Println("Performing task...")
time.Sleep(500 * time.Millisecond)
}
}
}
context.WithoutCancel()
创建的context在parentContext被取消时,其不会被取消
func main() {
ctx, cancel := context.WithCancel(context.Background())
ctx = context.WithoutCancel(ctx)
go performTask(ctx)
time.Sleep(2 * time.Second)
cancel()
time.Sleep(5 * time.Second)
}
func performTask(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Task cancelled")
return
default:
// Perform task operation
fmt.Println("Performing task...")
time.Sleep(500 * time.Millisecond)
}
}
}
4.Timeout Context
context.WithTimeout
用于创建一个带有超时的上下文,这个上下文会在指定的超时时间之后自动取消
context有四个方法:
- Deadline() : 返回超时的截止时间
- Done(): 上下文完成时或者取消后会调用
- Err():Done()返回后,使用Err可以获得结束原因
- Value():获取键值
func main() {
// 创建一个带有超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // 在程序结束前调用 cancel 函数释放资源
// 启动一个任务
go task(ctx)
// 等待一段时间,超时后任务会被取消
time.Sleep(3 * time.Second)
fmt.Println("Main goroutine: Done")
}
func task(ctx context.Context) {
select {
case <-time.After(3 * time.Second):
fmt.Println("Task: Finished")
case <-ctx.Done():
fmt.Println("Task: Context cancelled or timed out")
}
}
5.Deadline Context
context.WithDeadline()
和context.WithTimeout
一样,区别在于时间的表示方式,context.WithDeadline()
时间需要时一个具体的时刻
6.AfterFunc
context.AfterFunc
是在parent context
完成或者取消后,执行一个函数,会返回一个stop
函数,用于停止parent contenxt
和func
的关联,返回true代表成功取消,false代表context已经完成或者取消。
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // 确保在函数结束时取消context
// 设置一个2秒后执行的函数
stop := context.AfterFunc(ctx, func() {
fmt.Println("AfterFunc executed")
})
go performTask(ctx, stop)
// 阻塞主goroutine,防止程序立即退出
time.Sleep(3 * time.Second)
}
func performTask(ctx context.Context, stop func() bool) {
select {
case <-ctx.Done():
fmt.Println("Context canceled:", ctx.Err())
fmt.Println("stop:", stop())
}
}
7.注意的一些问题
不传递上下文
:如果一个函数创建了上下文,但并没有传递给其他函数或协程,这些函数和协程无法响应上下文相关的处理操作。忘记调用取消函数
:使用有取消函数的上下文时,记得取消协程泄露
:有上下文的协程需要检查Done channel,当接收到到信号时及时清理资源和退出过度使用context.Background
:没有取消和超时功能,很可能引起问题传递nil的上下文
:不要传递nil的上下文,会导致panic阻塞调用
:应该将阻塞操作(比如IO)包装成使用上下文检查的调用,可以避免被挂起过度使用上下文
:上下文也并不是所有场景都使用,比如处理全局资源或者共享状态等,可能更使用使用锁或者channel上下文存储在结构体中
:应该将context显式的传递给需要的函数,否则可能会引起数据竞态,生命周期管理等问题