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)就会出错,对值类型也同理

虽然可以混着写,但是最好还是写清楚吧,不然最后自己读起来都觉得痛苦

posted @ 2023-09-16 17:24  DQY_dqy  阅读(6)  评论(0编辑  收藏  举报