编辑
2024-03-21
计算机技术
00
请注意,本文编写于 267 天前,最后修改于 49 天前,其中某些信息可能已经过时。

目录

数据类型
数值
切片
数组
interface
结构体
字符串
iota
map
如果map属性值为结构体
函数
方法
方法值
channel
channel的底层实现
语法
defer
recover
for range
select case
switch case
init
goto
操作符
操作符优先级
赋值
常量与变量
变量
常量
类型别名与类型定义
值类型与引用类型
关键字与预定义标识符
关键字
预定义标识符
互斥锁
运行时
GC
调度

数据类型

数值

rune是int32的别名,byte是uint8的别名。

切片

  • 切片的底层引用了数组,当切片截取了某个数组(也包括[0:0]),底层就会指向这个数组,除非append操作之后,超出原数组的容量范围,才会分配一个新的数组给切片作为底层。
  • 数组和切片截取a[3:4:4],第一个为起始位置,第二个是截取结束位置,第三个是容量位置。截取结束位置可以大于长度位置,但必须小于等于容量位置。容量位置必须大于或等于截取结束位置。如果省略第二个参数,则默认第二个参数为len(a),如果省略第三个参数,则默认第三个参数为cap(a)。(如果仅仅是通过下标取值,而不是截取,注意下标位置必须小于切片长度)
  • 计算切片长度,可以直接用第二个位置的值,减去第一个位置的值;计算切片容量,可以直接用第三个位置的值,减去第一个位置的值。
  • 切片只能和nil进行比较。
  • 对于切片(包括空切片),可以在其容量范围内进行截取,其中没有元素的地方会用零值填充。省略截取结束位置参数时,默认为切片长度值
  • s=[]int{}s=make([]int,0)得到是空切片,var s []int得到的是一个nil切片。空切片底层指向同一个空数组,nil切片底层是一个空指针。
  • 数组和切片中可以指定下标[]int{5:1, 2, 3, 1:9, 10},未指定下标的,下标为前一个指定下标的位置加一,即为[]int{5:1, 6:2, 7:3, 1:9, 2:10},剩余的位置补0
  • 当切片作为函数形参的时候,传递参数的时候,是引用传递,实际上传递的是一个包含底层数组指针、切片长度、切片容量的结构体。这个结构体本身是按照值传递的。所以函数内部接收的是一个副本,只不过这个副本指向通用的底层数组,长度和容量字段都是副本自己的。所以当函数内部执行append函数时,实际是改变了函数内部副本变量的长度和容量。
  • 切片为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包含了动态类型和动态值,判断是否等于另外一个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是常量计数器,只能在常量表达式中使用

iota在const关键字出现时,将重置为0,const中每新增一行常量声明将使iota计数一次。
如果在同一个const里面遇到新的iota,则恢复为常量计数器。不复制上一个值,也不是在上一个值的基础上加。

map

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()操作

如果map属性值为结构体

当使用 . 操作符来访问或修改结构体值的字段时,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,编译不通过。但是可以关闭一个只能发送数据的单向channel。

向一个nil的channel发生数据会导致goroutine阻塞,从一个nil的channel接收数据也会导致goroutine阻塞。

给一个已关闭的channel发送数据会导致panic,从一个已关闭的channel接收数据,会收到零值。

双向通道可以转为单向通道,但是单向通道不能转为双向通道。比如a:=make(chan int),可以通过chan<- int(a)来转换。

<-符号前后可以留空格,也可以不留空格,都可以通过编译。

channel的底层实现

channel的底层是runtime包里面一个hchan结构体,这个结构体里面包含了互斥锁、缓存容量、当前元素数量、环形队列指针、环形队列的发送索引、环形队列的接收索引、待发送数据的goroutine队列、待接收数据的goroutine队列、是否已关闭等等。

golang
type 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

defer执行时机,return第一阶段(赋值)、执行defer进行收尾、return第二阶段(携带返回值退出)

defer在注册的时候(也就是入栈的时候),就已经确定了传入的参数值,也确定了函数体。如果参数值是函数调用,则在注册的时候,就会从左到右,立即依次计算出真正的参数值。如果是链式调用,defer注册时,就会先计算前面所有的函数,并把结果作为方法参数,只有最后一个函数会延迟执行。

当函数内部发生panic时,该函数中所有的defer语句都仍然会被执行,当defer中发生panic时,会中止当前 defer 的执行,但会继续执行后续的 defer。

recover

recover的作用是捕获异常,recover()会使程序从panic中恢复,返回panic信息。

recover必须与defer一起使用,并且必须直接包含在defer注册函数的函数体中(在这个注册函数体中调用recover()时,可以defer cover()),可以是匿名函数中。不能进行多次封装或者嵌套。

recover()必须要和有异常的栈帧只隔一个栈帧。也就是说recover()捕获的是祖父一级调用函数栈帧的异常。也就是说刚好可以跨越一层defer函数。

cover()表示函数本身 --> defer func(){...}()表示父级函数 --> func(){...} 表示祖父级函数。

而对于panic发生的位置没有任何特殊要求,只要发生于注册recover()的defer函数执行之前,且它们在同一个goroutine,就可以捕获到。

for range

  • for range 在循环之前,就已经确定了循环的次数。
  • for range 前面的临时变量,只会创建一次,然后每次重新赋值,即始终为同一个变量。
  • 仅仅遍历切片,忽略具体业务时,普通的for语句的性能稍微更好一点,但是可以忽略,相差很小。主要是for range需要同时管理索引和值的获取。
  • for range开始迭代时就浅拷贝了一个副本,对数组来说,相当于拷贝了一个新的数组进行迭代,修改原数组不会影响被迭代数组。而对于切片来说,range拷贝出来的切片与原切片底层是同一个数组,因此对原切片的修改也会影响到被迭代切片。
  • for range 遍历的切片、map为nil,或者遍历的数组指针为nil时,不会报错。
  • for range 前面可以不写变量,如for range []int{1,2}也可以,只是无法获取元素值。
  • for range 与 for k,v:=range 与 for k,_:=range 在内部实现上是由差异的。具体例子可以查看数组小节的特别注意

特别注意

如果在迭代切片循环的过程中,改变了原切片地址,则对原切片的修改不会影响被迭代切片。
比如在迭代的过程中,使用append给原切片追加元素导致扩容后,原切片的地址会发生改变,这时如果修改了原切片的元素,则不会影响到被迭代切片。

  • for range也可以遍历一个数组指针,实际上是将数组的指针传递给迭代变量。这样,在循环中访问的是数组元素的引用而不是拷贝。(类似于切片引用传递)

select case

对于包含通道通信操作的 select 语句,如果有多个 case 准备好的话,会进行随机选择

所有的case都不满足时,才会走default

switch case

默认自带break,可以写fallthrough来贯穿case

所有的case都不满足时,才会走default

init

同一个go文件里面,init执行顺序是从上到下。同一个package里面不同的go文件里的init执行是按照编译时读取文件的顺序。不同的package里面的init执行,是根据包名的英文字母排序执行(import后面的所有字母以/分隔对比每一项),如果有嵌套的,需要等到嵌套的init先执行,嵌套里的init也要根据字母顺序排队。golang中包的初始化,是串行执行的。如果有嵌套则按照如下图的顺序执行。
image.png

goto

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等多元素类型时,如果最后一个元素跟}不在同一行,结尾也需要加逗号。

常量

  • 在Go语言中,常量是一个简单的标识符,不会被修改,常量未使用时能编译通过。
  • 在Go语言中,常量是在编译时期就固定的值,是硬编码在程序中的值,并没有内存地址,所以是不可寻址的,因此不能取其地址。如果你尝试取一个常量的地址,就会报错,编译不通过。
  • 无类型常量可以根据需要自动适配到其他类型,字面量是也是无类型常量,如const a=1就是无类型常量,可以根据需要自动适配int、uint、int64、int32等类型
  • const关键字只能用于定义基本类型的常量,布尔值数值(整数、浮点数和复数)、字符串,以及基础类型为这些类型的自定义类型,如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

垃圾回收器(GC)负责管理内存,它会识别和回收不再被引用的数据空间,无论它们是在堆上分配还是在栈上分配的。

调度

goroutine进入“就绪”状态后才会被放入调度队列,等待调度器为其分配CPU时间片后进行执行。

本文作者:枣子

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!