rune是int32的别名,byte是uint8的别名。
[0:0]
),底层就会指向这个数组,除非append操作之后,超出原数组的容量范围,才会分配一个新的数组给切片作为底层。a[3:4:4]
,第一个为起始位置,第二个是截取结束位置,第三个是容量位置。截取结束位置可以大于长度位置,但必须小于等于容量位置。容量位置必须大于或等于截取结束位置。如果省略第二个参数,则默认第二个参数为len(a)
,如果省略第三个参数,则默认第三个参数为cap(a)
。(如果仅仅是通过下标取值,而不是截取,注意下标位置必须小于切片长度)s=[]int{}
与s=make([]int,0)
得到是空切片,var s []int
得到的是一个nil切片。空切片底层指向同一个空数组,nil切片底层是一个空指针。[:]
,截取后也等于nil,也可以进行len()、cap()操作数组各元素未赋值时,各元素默认为元素类型的零值。
数组指针为nil时,不能用来进行任何截取操作,但可以进行len()、cap()操作
特别注意
数组指针,即使为nil,使用len()、cap()仍然可以获取到数组的长度和容量,但这不是数组指针所指向的内容的长度,这是因为数组的长度是数组类型的一部分,而不是它值的一部分。对于固定大小的数组,其长度和容量在编译时就已经确定了。
如fmt.Println((*[3]int)(nil))
打印出的值为nil,但是len((*[3]int)(nil))
得到的长度为3。当使用for range (*[3]int)(nil)
或者for k,_:=range (*[3]int)(nil)
或for _,_=range (*[3]int)(nil)
会循环三次,但是不能使用for k,v:=range (*[3]int)(nil)
,也不能使用for _,v:=range (*[3]int)(nil)
这样因为数组值为nil,所以在把数组元素赋值给v
的时候,运行时会报空指针
错误。
interface包含了动态类型和动态值,判断是否等于另外一个interface,需要比较动态类型和动态值,如果动态类型都是不可比较类型,且动态类型相同,比如都是[]string
这种类型,会发生panic(因为动态类型相同时,会比较动态值,但是动态值是不可比较类型的值)。
判断是否为nil时,需要动态类型和动态值都为nil,才相等。空interface与其他类型比较的时候(包括非interface类型),也是判断动态类型和动态值。
有方法的interface{},只能和具有相同方法的interface{}比较,以及和空interface、nil 比较。
空interface可以与其他类型进行直接比较,除了不可比较类型(map、slice 或 function),以及包含不可比较类型的结构体值,也不可以比较。
任何interface都可以类型断言为空interface,即x.(interface{})
(如果x的动态类型实现了空interface{}类型对应的方法集合,就会成功,因为空interface{}本身已经是所有类型的实现了,所以除了动态类型为nil,必然成功),当动态类型为nil时,会断言失败(这么做一般没啥意义,实际开发中都是断言它的动态类型)。
Student{}.age=18
无法通过编译。但是可以显式地创建一个指向结构体值的指针,如(&Student{}).age=18
可以正常编译。(本质就是结构体属性赋值,需要能取地址)特别注意
在匿名值或字面量上调用指针接收者方法或者给结构体属性赋值时,编译器不会进行自动取址,因为会造成语义不明确或其他错误。比如结构体字面量,又比如函数返回值。
如Student{}.setAge()
和getStudent().setAge()
,会因为Student{}
字面量和getStudent()
返回的结构体无法自动取地址,而都不能直接调用指针方法setAge()
。
可以创建一个变量接收值,然后用这个变量去调用方法。或者显式地创建一个指向结构体值的指针去调用方法(&Student{}).setAge()
Go里面的字符串,是只读的,定义了一个字符串,不能直接通过下标去修改,否则会编译不通过
iota是常量计数器,只能在常量表达式中使用
iota在const关键字出现时,将重置为0,const中每新增一行常量声明将使iota计数一次。
如果在同一个const里面遇到新的iota,则恢复为常量计数器。不复制上一个值,也不是在上一个值的基础上加。
Go里面map的值是通过键来访问的,不可以直接获取map中单个元素的地址,因为map中的值是不可寻址的,也就是说它们不能直接被取地址。
可以直接对map取地址,但是不能直接用map的指针来操作其属性,也就是说不能直接对map指针进行索引。
如果需要通过地址来访问map中的值,可以考虑使用类似这种格式map[int]*int
,在map的value中保存真实的值地址,来达到类似的效果。
map没有cap
方法,cap函数适用于数组、数组指针、slice、channel。
使用make初始化时,最多可以指定两个参数,如果指定了第二个参数,也没有实际意义,make(map[int]int, 2)
后面的参数2没有意义。
map为nil也可以进行len()操作
当使用 . 操作符来访问或修改结构体值的字段时,Go编译器会自动进行指针解引用。(编译期)
因此当map的属性值为结构体时,无法通过map[key].A
的形式来修改,因为会解引用失败,编译会失败。
但是可以通过map[key].A
的形式来读取,因为Go程序运行时,会对不可寻址的结构体进行值拷贝,然后对拷贝的值进行指针解引用。
函数只能与nil进行比较。函数、map、切片、channel都只能与nil比较
值方法,可以通过指针来调用;指针方法,也可以通过值调用;这是因为编译器会自动进行转换。
特别注意
使用字面量调用方法时,由于字面量不能进行取地址操作,所以编译器无法将值类型转换成指针类型。如A
为自定义类型,PointMethod()
为*A
的方法,那么A(2).PointMethod()
,将无法通过编译。
多级指针编译器不会自动转换。如A为自定义类型,ValueMethod()
为A
的方法,PointMethod()
为*A
的方法,那么**A
类型无法直接调用ValueMethod()
,并且也无法直接调用PointMethod()
。
将方法赋值给一个变量时(或者作为函数参数传递时),实际上是将方法本身与它的接收者(值或指针)绑定在一起,相当于保存了一份接收者值,可以理解为快照,当方法被调用时,会使用这个值作为方法接收者,不会随原始变量改变。
如果方法的接收者需要解引用操作,那么在方法赋值给变量的时候,就会完成解引用。然后保存解引用后的值作为接收者值。
不能关闭一个只能接受数据的单向channel,编译不通过。但是可以关闭一个只能发送数据的单向channel。
向一个nil的channel发生数据会导致goroutine阻塞,从一个nil的channel接收数据也会导致goroutine阻塞。
给一个已关闭的channel发送数据会导致panic,从一个已关闭的channel接收数据,会收到零值。
双向通道可以转为单向通道,但是单向通道不能转为双向通道。比如a:=make(chan int)
,可以通过chan<- int(a)
来转换。
<-
符号前后可以留空格,也可以不留空格,都可以通过编译。
channel的底层是runtime包里面一个hchan结构体,这个结构体里面包含了互斥锁、缓存容量、当前元素数量、环形队列指针、环形队列的发送索引、环形队列的接收索引、待发送数据的goroutine队列、待接收数据的goroutine队列、是否已关闭等等。
golangtype hchan struct {
qcount uint // 当前队列中的元素数量(缓冲区已存在的数据量)
dataqsiz uint // 环形队列的大小(缓冲区大小)
buf unsafe.Pointer // 指向缓冲区的指针
elemsize uint16 // 单个元素的大小
closed uint32 // channel 是否已经关闭
sendx uint // 环形队列中的发送索引(向channel发送数据时,元素存放的缓冲区位置索引)
recvx uint // 环形队列中的接收索引(从channel接收数据时,读取的缓冲区位置索引)
recvq waitq // 等待接收数据的goroutine队列
sendq waitq // 等待发生数据的goroutine队列
lock mutex // 保护channel数据结构的互斥锁
...
}
flowchart LR
id1["goroutine A"] --发送数据--> ide1
subgraph ide1 [channel内部]
direction TB
id(channel)-->加锁
加锁 --是否已关闭--> C{closed}
C--已关闭-->unlock["解锁"]-->panic(panic)
C--未关闭-->D{是否有待接收数据的goroutine队列}
D--有等待接收的goroutine-->将数据交给最早挂起的接收者-->解锁-->id2("唤醒该goroutine")
D--无等待接收的goroutine-->E{是否有可用的缓冲区}
E--有可用的缓冲区-->id3["将数据放入缓冲区sendx位置"]
id3-->id6(调整sendx和qcount的值)-->unlock2["解锁"]
E--无可用的缓冲区-->id5("发送者goroutine A加入待发送数据的goroutine队列")-->unlock3["解锁"]-->id4["发送者goroutine A被阻塞"]
end
defer执行时机,return第一阶段(赋值)、执行defer进行收尾、return第二阶段(携带返回值退出)
defer在注册的时候(也就是入栈的时候),就已经确定了传入的参数值,也确定了函数体。如果参数值是函数调用,则在注册的时候,就会从左到右,立即依次计算出真正的参数值。如果是链式调用,defer注册时,就会先计算前面所有的函数,并把结果作为方法参数,只有最后一个函数会延迟执行。
当函数内部发生panic时,该函数中所有的defer语句都仍然会被执行,当defer中发生panic时,会中止当前 defer 的执行,但会继续执行后续的 defer。
recover的作用是捕获异常,recover()
会使程序从panic中恢复,返回panic信息。
recover必须与defer一起使用,并且必须直接包含在defer注册函数的函数体中(在这个注册函数体中调用recover()时,可以defer cover()),可以是匿名函数中。不能进行多次封装或者嵌套。
注
recover()必须要和有异常的栈帧只隔一个栈帧。也就是说recover()捕获的是祖父一级调用函数栈帧的异常。也就是说刚好可以跨越一层defer函数。
cover()表示函数本身 --> defer func(){...}()表示父级函数 --> func(){...} 表示祖父级函数。
而对于panic发生的位置没有任何特殊要求,只要发生于注册recover()
的defer函数执行之前,且它们在同一个goroutine,就可以捕获到。
for range []int{1,2}
也可以,只是无法获取元素值。特别注意
如果在迭代切片循环的过程中,改变了原切片地址,则对原切片的修改不会影响被迭代切片。
比如在迭代的过程中,使用append给原切片追加元素导致扩容后,原切片的地址会发生改变,这时如果修改了原切片的元素,则不会影响到被迭代切片。
对于包含通道通信操作的 select 语句,如果有多个 case 准备好的话,会进行随机选择
所有的case都不满足时,才会走default
默认自带break,可以写fallthrough
来贯穿case
所有的case都不满足时,才会走default
同一个go文件里面,init执行顺序是从上到下。同一个package里面不同的go文件里的init执行是按照编译时读取文件的顺序。不同的package里面的init执行,是根据包名的英文字母排序执行(import后面的所有字母以/分隔对比每一项),如果有嵌套的,需要等到嵌套的init先执行,嵌套里的init也要根据字母顺序排队。golang中包的初始化,是串行执行的。如果有嵌套则按照如下图的顺序执行。
goto跳转的目标标签,需要跟goto在同一级别或者更外层代码块。即只能直接跳入同级和或者更外层代码块,不能跳入内层的代码块,也不能跳转到其他函数。
.(成员访问操作符) 大于 [](索引操作符) 大于 *(解引用操作符) 等于 &(取地址操作符) 大于 ++或--(自增或自减操作符)
&
和*
都是右结合的,比如**p
会先计算右边的*p
,相当于*(*p)
。
运算符优先级:一元运算符在一般的运算符中有最高的优先级。一元运算符包含"+"
、"-"
、"!"
、"^"
、"*"
、"&"
、"<-"
。比如:5/+-*v
相当于5/(0+(0-(*v)))
注
左结合:从左开始计算,大多数算数运算符是左结合
右结合:从右开始计算,赋值运算符(=)、一元运算符等等
不能使用短变量声明:=
来设置字段值,无论是结构体、数组、切片、map等,如user.Age := 12
会编译失败。
对于使用:=定义的变量,如果新变量与同名,已定义的变量不在同一个作用域中,那么 Go 会新定义这个变量
对于多值赋值语句,类似于a,b=1,2+a
或者a,b:=1,2+a
,执行顺序是从左到右,但是左边的运算结果不会影响右边的运算结果,他们都是独立的操作,表达式里面引用的都是这条语句之前的变量值。
i++ 和 i–- 在 Go 语⾔中是语句,不是表达式,只能做为独立语句,不能赋值给另外的变量,这点与其他语言不一样。
函数中声明的变量必须要使用,但是可以有未使用的全局变量。函数参数未使用也是可以的。
小妙招
如果有暂时没有使用的变量可以这样 _ = a
或者b = b
,也是可以的
不使用显式类型,无法用nil来初始化变量,如a:=nil
会编译失败,因为编译器无法确定类型
使用字面量定义数组、切片、结构体、map等多元素类型时,如果最后一个元素跟}
不在同一行,结尾也需要加逗号。
const a=1
就是无类型常量,可以根据需要自动适配int、uint、int64、int32等类型布尔值
、数值
(整数、浮点数和复数)、字符串
,以及基础类型为这些类型的自定义类型,如type A int
_ int = 123
这样的占位符定义,会编译不通过。类型别名带有本身类型的所有方法,但是类型定义不具有原类型的方法
注
在 Go 语言中,每个变量的内存分配都由编译器完成。对于大部分的变量,特别是小的数值类型、bool 类型、指针和短小的结构体等,它们的值会被直接存储在栈上。当你通过变量名访问这些变量时,程序会直接从栈上读取对应的值。
在运行时,Go 编译器已经决定了每个变量的存储位置,包括栈上的位置。这样,当你通过变量名访问变量时,程序会根据编译时决定的存储位置,在栈上定位变量的值并进行读取操作。
需要注意的是,Go 语言的变量可能也会被编译器进行逃逸分析,部分变量可能会逃逸到堆上进行分配,这通常发生在变量的生命周期跨越了编译器能够确定的范围,例如返回指针、闭包等情况。在这种情况下,对于逃逸到堆上的变量,通过变量名找到值的过程也会不同,需要通过堆上的指针进行访问。
总的来说,对于大部分情况下的变量,通过变量名找到栈上存储的值,是一个简单而高效的过程,由编译器的指令来实现。而对于逃逸到堆上的变量,访问方式会有所不同,通常需要通过堆上的指针来间接访问。
在 Go 语言中,函数返回的指针所指向的值,以及闭包变量的值,可能会存储在堆空间或者栈空间,具体取决于编译器的逃逸分析和优化行为。
逃逸分析是编译器在编译期间的一项重要工作,用于决定变量的生命周期。对于那些在函数结束后不再被引用的变量,编译器可以优化地不分配栈空间,而是将其分配到堆上。这种情况下,函数返回的指针所指向的值以及闭包变量的值就会存储在堆空间中。
对于那些逃逸到堆空间的变量,它们的生命周期可能会比函数的生命周期更长,所以编译器会将它们分配到堆上,以便随时可以被访问。
需要注意的是,即使变量的值存储在堆上,变量本身的指针或引用可能仍然存储在栈上。这样的设计既充分利用了栈空间的高效性,又能够避免因为局部变量的生命周期而带来的额外开销。而对于较大的数据或者生命周期较长的变量,将其分配到堆上可以减少栈空间的使用,并优化内存的管理和利用。
因此,函数返回的指针所指向的值,以及闭包变量的值,可能存储在堆空间或栈空间,具体取决于编译器的逃逸分析决策。
用于语法结构和流程控制,具有特点的语法功能和含义,不能用作变量名和函数名,Go语言中有25个关键字,如break、const、var、func、struct等等
预先定义的标识符,包括基本类型、常量、nil、内建函数等,可以在代码中重新定义,Go语言中有37个预定义标识符,如int、string、bool、true、false、iota、nil、len、cap、append、make、new等等
sync.Mutex是值类型。当互斥锁作为方法的接收者或者参数时,要注意避免值复制,否则会导致互斥锁不生效。Lock()
与Unlock()
方法的接收者为指针类型*sync.Mutex
,所以当需要把sync.Mutex进行函数传参时,一般传入同一个sync.Mutex变量的指针,这样可以保证加锁和解锁作用于同一个sync.Mutex变量。
注
不仅仅是sync.Mutex变量,sync.WaitGroup也不要进行值传递,因为它们会作用于不同的实例。
垃圾回收器(GC)负责管理内存,它会识别和回收不再被引用的数据空间,无论它们是在堆上分配还是在栈上分配的。
goroutine进入“就绪”状态后才会被放入调度队列,等待调度器为其分配CPU时间片后进行执行。
本文作者:枣子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!