楔子
读《Go语言学习笔记》上卷
基于Golang 1.6 amd64
,本文记录在阅读过程中值得注意的几点特性,供参考。
switch
switch的行为是有点不一样的。
- 只有在case中明确添加fallthrough关键字,才会继续执行紧跟的下一个case。
- fallthrough关键字必须添加在case块末尾。
- 无需添加break语句
延迟调用
在一个func
中可以定义多个延迟调用,关键字defer
,语法如下:
func test(){
var n int = 0
n++
defer f1(n)
n++
defer f2(n)
}
- defer执行顺序为FILO,先入后出。
- 相比显式调用,延迟调用会花费一定代价,包括注册、调用、缓存开销等。
- 延迟调用注册的是调用,所以参数值会在注册的时候被复制并且缓存起来。
- 对于那些对状态敏感的延迟调用,可以考虑使用指针或者闭包,而不是函数传值。
常量定义
- 在常量组中,如不指定类型和初始化值,则与上一行非空常量右值(表达式文本)相同。
- 常量会在编译器预处理阶段直接展开。
iota
Golang没有明确的枚举类型,但可以通过iota来实现。使用如下:
const(
_ = iota
KB = 1 << (10 * iota)
MB
GB
)
const(
a = iota //0
b //1
c = 100 //100
d //100
e = iota //4
f //5
)
- 例子一,是一种不太符合个人直觉的写法,但确实凑效。
- 例子二,如果中断iota自增,则必须显式恢复,且后续自增值按行序递增。该特性和C语言的
enum
不一致。
Byte & Rune
本质上,Byte和Rune都是别名类型
type byte uint8
type rune int32
byte
用于表示的是RawData,字面上更强调他是底层的字节数据。rune
用于表示的是UCS-4/UTF-32
的CodePoint,ASCII码一般为1B,常见的拉丁字符均为2B,但是UTF8中文就需要3B,一般而言,int32的4B空间足以存储任何一种字符集的CodePoint了。用单引号的字面量,默认类型为rune
。
引用类型
- 特指slice、map、channel三种预定类型。
和普通类型不一样的是,这三种类型的数据结构更加复杂,内部通过指针来引用底层数据。
切片slice
- 切片是一个只读对象,其工作机制类似数组指针的一种包装。
- 其本身并非动态数组或数组指针。它内部通过引用底层数组,设定相关属性(cap、len)将数据读写操作限制在指定区域内。
type slice struct{
array unsafe.Pointer //底层数组指针
len int //当前长度
cap int //总长度
}
- 不是任何时候都适合使用切片来代替数组,由于切片底层引用了数组,所以该数组有可能会在堆上分配内存,会消耗更多代价。
- 值得注意的是,这里的底层数组指针虽然是指针类型,但更强调引用(区别于
uintptr
),因为unsafe.Point
可以安全持有对象或者对象成员,我们知道Golang是一门自带GC的语言,使用unsafe.Point
可以阻止对象被GC。 - 关于扩容,当超出切片cap限制是,新分配的数组长度是原cap的2倍(非数组的2倍)。同时,并非一定是2倍扩容,在较大切片情况下,会尝试扩容到原来的1.25倍,以节约内存。
- 拷贝时,允许两个切片对象指向同一个底层数组,允许目标区域重叠,最终复制长度以较短的切片长度为准。
字典map
底层是哈希表,要求key必须支持相等运算符。
字典被设计成"NOT Addressable",故不能直接修改value成员,比如结构体或数组。
针对上述情况,可以考虑使用指针/引用。
字典迭代一般采用Range关键字,但请注意,由于字典是无序的,所以在Range阶段对Map进行插入删除操作时,有可能会影响遍历结果,但似乎不同平台的行为不一致。
通道chan
type hchan struct {
qcount uint // queue 里面有效用户元素,这个字段是在元素出对,入队改变的;
dataqsiz uint // 初始化的时候赋值,之后不再改变,指明数组 buffer 的大小;
buf unsafe.Pointer // 指明 buffer 数组的地址,初始化赋值,之后不会再改变;
elemsize uint16 // 指明元素的大小,和 dataqsiz 配合使用就能知道 buffer 内存块的大小了;
closed uint32
elemtype *_type // 元素类型,初始化赋值;
sendx uint // send index
recvx uint // receive index
recvq waitq // 等待 recv 响应的对象列表,抽象成 waiters
sendq waitq // 等待 sedn 响应的对象列表,抽象成 waiters
// 互斥资源的保护锁,官方特意说明,
// 在持有本互斥锁的时候,绝对不要修改 Goroutine 的状态,
// 不能很有可能在栈扩缩容的时候,出现死锁
lock mutex
}
Channel
实际上是个环形队列。实际的队列空间就在这个channel
结构体之后申请的空间。dataqsiz -> data queue size
为队列大小。elemsize
为元素的大小。Lock
用来保证线程(协程)安全。recvq
和sendq
分别用来保存对应的阻塞队列。
字符串
不可变字节序,是复合结构。
type stringStruct struct{
str unsafe.Pointer //指向一个字节数组,无Null结尾
len int
}
由于其不可变特性,用加法拼接字符串时,每次都需重新分配内存,故在超大规模下,性能较差,改进思路是预分配空间。
- 为什么String没有cap属性,而切片有呢?这是由于上述的不可变特性,而切片是具有可变特性的,需要记录数据区块边界。
结构体
匿名字段
- 除了接口指针和多级指针外的任何命名类型都可以作为匿名字段,未命名类型因无名字标识,无法作为匿名字段。
- 指针类型变量会使用基础类型作为字段名。
- 不能将基础类型和指针类型同时嵌入,因为其两者隐式名字相同。
字段标签
字段标签并不是注释,而是对字段进行描述的元数据,不属于数据成员,但却是类型的组成部分。
obj.Field(index).Tag
内存布局
- 内存布局类似C语言的结构体,根据数据成员顺序,在内存空间内顺序排列。
- 由于内存对齐的原因,结构体实际占用字节数永远大于等于结构体所有字段字节数之和。
对于结构体的每个字段,有如下4个概念:
- 对齐宽度
- 本身所占字节数
- 实际占用字节数
- 偏移量
- 首先来说,本身所占字节数就是类型大小。当把类型放到结构体时,它实际占用字节数是大于等于类型本身大小的,多出来的部分叫填充字节。也就是说实际占用字节数=本身所占字节数+填充字节数。
- 如果最后一个字段是空结构类型字段,虽然实际长度为0,但是编译器会将其当作长度为1的类型做对齐处理(padding:7)
- 如果仅有一个空变量字段,那么同样按1对齐,长度为0,且指向
runtime.zerobase
变量。
方法
方法和函数具有区别,方法需要和对象实例进行绑定,而函数更强调算法层面。
type N int
func (n N) test(){
println("Hi!")
}
方法可以看做是一种特殊的函数,这里的(n N)
被称为receiver,receiver可以是基础类型或者是指针类型,这会关系到对象实例是否会被复制。
方法的receiver类型的选择建议如下:
- 要修改实例状态,用*T。
- 无需修改状态的小对象或固定值,建议用T。
- 大对象用*T,减少成本。
- 引用类型,字符串,函数等指针包装对象,直接用T。(开销小,便于理解)
- 若包含Mutex等同步字段,用*T,以免复制造成锁操作无效。
- 其他无法确定的情况,一律用*T。
方法集的包含
- T包含T
- *T包含T + *T
- 匿名S + T,包含S
- 匿名*S + T,包含S + *S
- 匿名S或匿名*S + *T,包含S + *S
receiver的本质
这里不得不提到两个概念,Method Expression和Method Value。
Method Expression
type N int
func (n N) test(){
println("Hi!")
}
func main(){
var n N = 20;
f1 := N.test
f2 := (*N).test
f1(n)
f2(&n) //虽然传值为指针,但编译器会根据原定义进行拷贝传值。
}
我们可以看到,虽然test()
方法是一个无参方法,但实际上,需要将对象作为第一参数进行传入。这个在语法层面上和OOP语言是不一样的。我们只能模拟OOP的语法,但语法层面实际上是不支持的。
Method Value
type N int
func (n N) test(){
println("Hi!")
}
func main(){
var n N = 20;
f1 := n.test
f2 := (&n).test
f1()
f2()
}
看起来,很像OOP语言的语法,但实际上,编译器行为是不一样的,编译器会为MethodValue生成一个包装函数,实现间接调用,对于receiver的传递,和闭包传递的实现方式相同,打包成funcval,通过DX寄存器传递。
个人理解,MethodValue是类似一种语法糖
instance.method(args) => type.func(instance, args)
接口
- Golang的接口遵循鸭子类型的匹配规则。
- 超集接口可以转换为子集接口,但是反之不行。
内部实现
type iface struct{
tab *itab // 类型信息
data unsafe.Pointer // 实际对象指针
}
type itab struct {
inter *interfacetype // 接口类型
_type *_type // 实际对象类型
fun [1]uintptr // 实际对象方法地址
}
//对于无方法的接口
type eface struct{
_type *_type
data unsafe.Pointer
}
并发
- Golang在语言层面不使用线程进行调度,而是引入纤程(协程)概念。具体细节在本文不阐释。
- 简单来说,纤程会被调度到不同线程进行处理,实现对于资源的最大化利用。
- 相比默认的MB级的线程栈,GoRoutine自定义栈初始仅需2KB,十分适合创建成千上万的并发任务,在需要扩容时,最大可以到GB规模。
- runtime.GOMAXPROCS用于确定用几个逻辑线程参与并发任务执行(最大256)。
本文标题:Golang语言详解 笔记
本文连接:https://blog.dextercai.com/archives/149.html
除另行说明,本站文字内容采用创作共用版权 CC-BY-NC-ND 4.0 许可协议,版权归本人所有。
除另行说明,本站图片内容版权归本人所有,任何形式的使用需提前联系。