Go学习笔记
这几天在学Go,记录一下一些困惑和理解
channel特性
查阅的资料:
Go语言channel探究_go 多个协程读一个channel_JE_Xie的博客-CSDN博客
Go 中的 channel 解析— Go 中的并发性 - 知乎 (zhihu.com)
Go中的channel_go channel_始梦的少年的博客-CSDN博客
一、channel与blocking和deadlock
之前我看到说,对一个channel进行空读和满写,就会发生deadlock,程序会报错,而这种说法事实上并不准确
当一个goroutine试图对channel进行空读或满写时,这个goroutine就会blocking,此时并不会报错,也就是说blocking对于goroutine来说是一个正常现象,blocking是可以被解除的
而只有当所有的goroutine都陷入blocking状态,并且没有别的goroutine可以解除他们的阻塞状态时,才回发生deadlock,因为此时程序已经无法正常进行了
defer特性
查阅的资料:
https://www.jb51.net/article/283251.htm
https://blog.csdn.net/lp15929801907/article/details/130202962
defer的基本用法就不在赘述,主要看一下defer的一些特性
一、多个defer执行顺序
多个defer的执行顺序是先进后出,也就是说defer后的操作是被押入到了一个栈中
看一下示例
package main
import "fmt"
func main() {
for i := 1; i <= 3; i++ {
defer fmt.Println(i)
}
}
运行结果如下
二、defer中的参数
虽然defer语句是在延迟执行的,但是传递到defer后的参数是在defer声明时的值,而不是defer执行时的值
可以理解为defer拷贝了当时的数据进去,看一下示例
package main
import "fmt"
func main() {
x := 10
defer fmt.Println(x)
x = 20
}
但是其中很重要的是,不是说只要defer后有变量,那变量的值就是当时的值了
因为defer会固定传入其中的参数的值,但defer后的语句中出现的变量不一定就是传入了参数
而传入了参数的变量,不一定传入的就是值,下面举个例子
package main
import "fmt"
func main() {
x := 10
adrx := &x
defer fmt.Println("defer1", adrx, x, *adrx)
defer func() { fmt.Println("defer2", adrx, x, *adrx) }()
defer func(x int, adrx *int) { fmt.Println("defer3", adrx, x, *adrx) }(x, adrx)
x = 20
fmt.Println(adrx, x, *adrx)
}
首先看defer1,它声明时的x就是10,和一般情形是一样的
再看defer2,它输出的是20,20,但是defer2声明时的x是10
这是因为defer2后面声明了一个无参的匿名函数,它本质上并没有传递参数进去函数,在执行defer2的时候,他只是调用了匿名函数,匿名函数里面执行了方法,此时才将参数传入,也就是说是在执行的时候才传入的参数
而defer3则是一个有参的匿名函数,但是*adrx的值却和x不一样
这是因为defer3传入的adrx是一个指针变量,也就是说传入的是一个地址而不是值,而当我们执行defer3时,这个地址存储的值已经改变了,但我们的确拷贝了当时的地址
下面这个例子可以更加直观的理解defer3
package main
import "fmt"
func main() {
x := 10
adrx := &x
defer func(x int, adrx *int) { fmt.Println("defer3", adrx, x, *adrx) }(x, adrx)
x = 20
y := 30
adrx = &y
fmt.Println(adrx, x, *adrx)
}
可以看到,即使我们在defer后面改变了adrx的值,输出的结果仍然没有改变
三、defer与匿名返回值和命名返回值
对于一个函数的返回值,有匿名返回值和命名返回值的两种形式,而defer在其中也有不同的影响
先看具体表现,然后再来分析
package main
import "fmt"
func def1() (re int) {
defer func() { re++ }()
return
}
func def2() (re int) {
num := 1
defer func() { re++ }()
return num
}
func def3() (re int) {
num := 1
defer func() { num++ }()
return num
}
func def4() (re int) {
num := 1
defer func() { num++ }()
return
}
func def5() int {
num := 1
defer func() { num++ }()
return num
}
func main() {
fmt.Println(def1(), def2(), def3(), def4(), def5())
}
在分析之前,首先要关注一下return,事实上,return并不是一个原子操作,它分为三步
1、设置返回值 2、执行defer操作 3、返回返回值
在此基础之上,我们来分析一下这几个函数的返回结果
def1很好理解,不再赘述
def2是一个命名返回值的函数,因此实际上的return过程是将num赋值给re,执行defer,返回re,故结果为2
def3将def2中的defer换成了num++,因为我们返回的实际上是re,所以num++没有影响,故返回值是1
def4相比def3,我们直接return,也就是直接return re,甚至没有将num赋值给re,故结果是0,defer也没影响
def5是一个匿名返回值的函数,但它内部其实还是有一个“re”,不过没有显示表现出来
也就是说他的过程其实是将num赋值给这个没有直接表示出来的re,再对num++,本质上和def3没有区别
指针特性
主要记录与c++不同的地方,在c++中,值类型和引用类型是有着严格的区分的,一旦混用就会出错
但是在Go中,他有时候会“贴心”地自动将值解释为引用,或者引用解释为值
这样写起来可能很方便,不用担心出错,不过代码可读性就会下降很多了
下面是示例
package main
import "fmt"
type node struct{v int}
func (a node) test(){
fmt.Println(a.v)
}
func (a *node) test2(){
fmt.Println(a.v)
}
//func test3(a node){}
func main(){
var a node
a.v=1
a.test()
b:=&a
b.test()
a.test2()
b.test2()
//test3(b)
}
/*
运行结果
1
1
1
1
*/
注意到对b直接调用test是没有任何影响地,甚至在test2中直接通过a.v访问*a中的元素也是允许的
不过函数的传递参数的兼容只发生在结构体方法的实现中,如果直接使用test3(b)就会出错,对值类型也同理
虽然可以混着写,但是最好还是写清楚吧,不然最后自己读起来都觉得痛苦