Go学习笔记
Go学习笔记
前言
为什么要使用 Go 语言?Go 语言的优势在哪里? - 知乎
golang设计哲学
- 约定优于配置(convention over configuration)
- 正交设计
- 每种特性仅提供一种方法
- 简单是核心,降低复杂度
- 面向消息编程
String
反引号包含多行,不会转义
双引号时,只能通过 \n
进行换行
Go 中字符串类型是不能修改的,多个字符串底层都可以共用一段内存
修改字符串:
-
整体修改
-
转为slice,再转回来
func main() {
a := "hello world"
fmt.Println(a)
a = "Hello world"
fmt.Println(a)
}
func main() {
a := "hello world 中国"
b := []rune(a)
b[0] = 'H'
a = string(b)
fmt.Printf("%v", a)
}
如果要修改像中文字符之类超过 3 个字节的字符,需要转换为 rune
而不是 byte
遍历string需要用for range,不能用for len
Array
-
数组是固定长度的。使用前必须指定数组长度。
-
支持多维数组
-
下标从0开始
var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type
a = [3][4]int{
{0, 1, 2, 3}, /* 第一行索引为 0 */
{4, 5, 6, 7}, /* 第二行索引为 1 */
{8, 9, 10, 11}, /* 第三行索引为 2 */
}
Slice
可以理解为基于array的视图。
type slice struct {
array unsafe.Pointer
len int
cap int
}
新增元素,尽量采用append的方式。
删除元素,用:和append,没有erase接口。
拷贝时,不会触发扩容,多余的数据丢弃,不够的话补齐默认值。
扩容时,如果触发内存分配,原array的值不会改变。
扩容规则:
-
如果新容量超过原容量2倍,以新容量为准
-
如果切片的容量小于1024个元素,那么扩容的时候slice的cap就翻番,乘以2;超过1024个元素,每次增加容量的四分之一,直到超过新容量。100 1001.25 1001.25*1.25这样子。
-
如果扩容之后,还没有触及原数组的容量,那么,切片中的指针指向的位置,就还是原数组,如果扩容之后,超过了原数组的容量,那么,Go就会开辟一块新的内存,把原来的值拷贝过来,这种情况丝毫不会影响到原数组。
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
扩容后会有内存对齐,内存对齐之后,扩容的倍数就会 >= 2 或则 1.25 了。
为什么要内存对齐?
-
平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
-
性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
Heap
container/heap包仅封装了堆的操作,没有封装堆的数据结构,需要自己实现堆的数据结构。
需要实现5个接口:
push pop len less swap
heap提供的操作接口:
fix init pop push remove
List
双向链表
type Element struct {
next, prev *Element
list *List
Value interface{}
}
type List struct {
root Element
len int
}
可以存储任何值
元素类型不必一致
提供操作接口:添加 移动 访问 移除 获取长度 遍历
注意range不能作用于list,遍历list写法,为什么没有c++的for auto的写法?
for e := l.Front(); nil != e; e = e.Next() {
fmt.Println(e.Value)
}
Ring
环形链表
type Ring struct {
next, prev *Ring
Value interface{}
}
提供操作接口
Next prev move new link unlink len do
Map
hash接口
创建
var m map[string]int = map[string]int{"hunter":12,"tony":10}
var m2 map[string]int = make(map[string]int)
取值 [] 没有的话,返回零值,不会往原map中插入元素,和c++不同
改值 [] 有的话修改,没有的话,插入
删除 delete(m, "tony2")
If m is nil or there is no such element, delete is a no-op.
遍历 range 不能期望 map 在遍历时返回某种期望顺序 和lua类似
清空 make一个新的,老的map的内存交给go gc
多键索引 可以提高查询效率,少写代码
type queryKey struct {
Name string
Age int
}
type Profile struct {
Name string
Age int
Address string
}
var mapper = make(map[queryKey]*Profile)
func queryData(name string, age int) {
key := queryKey{Name: name, Age: age}
result, ok := mapper[key]
if ok {
fmt.Println(result)
} else {
fmt.Println("没有找到对应的数据")
}
}
多维map少用,每一维使用前都要make
Go语言中的 map 在并发情况下,只读是线程安全的,同时读写是线程不安全的。
Sync.map
import "sync"
无须初始化,直接声明即可
Store 表示存储,Load 表示获取,Delete 表示删除 Range表示遍历
sync.Map 为了保证并发安全有一些性能损失 按需使用
为了提升性能,load、delete、store等操作尽量使用只读的read
sync.Map是通过冗余的两个数据结构(read、dirty),实现性能的提升
为了提高read的key击中概率,采用动态调整,将dirty数据提升为read
对于数据的删除,采用延迟标记删除法,只有在提升dirty的时候才删除
可以参考 这篇文章
Range
迭代array、slice、channel map string
不能遍历list
遍历时,可以同时获得键和值,如只遍历值可以用_占位 和lua类似
不建议在range的时候 增加和删除元素 需要谨慎
range时修改数据用下标
Concurrency and Parallelism
这个要FQ
https://talks.golang.org/2012/waza.slide#1
这个是别人的翻译
https://www.cnblogs.com/concurrency/p/3925951.html
Concurrency是一种问题分解和组织方法,而Parallelism一种执行方式。
两者不同,但是有一定的关系。
Channel
这个ppt写的很好,清晰易懂
https://speakerdeck.com/kavya719/understanding-channels
Goroutine
特点
- 用户空间 避免了内核态和用户态的切换导致的成本
- 可以由语言和框架层进行调度
- 更小的栈空间允许创建大量的实例
goroutine是在golang层面提供了调度器,并且对网络IO库进行了封装,屏蔽了复杂的细节,对外提供统一的语法关键字支持,简化了并发程序编写的成本。
程序启动后,会默认开启一个主routine,可以用go关键字开启新的routine(子routine)
主routine和子routine只有一个区别:栈默认大小 主routine很大,子routine默认2K
Context
方便管理goroutine之间的关系。
MPG模型
定义
M(Machine):一个M直接关联了一个内核线程。
P(processor):代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。G(Goroutine):其实本质上也是一种轻量级的线程。
CSP 模型
请记住go并发的真理:
Do not communicate by sharing memory; instead, share memory by
communicating.
不要以共享内存的方式来通信,相反,要通过通信来共享内存。
CSP 模型由并发执行的实体(线程或者进程)所组成,实体之间通过发送消息进行通信,这里发送消息时使用的就是通道,或者叫 channel。CSP 模型的关键是关注 channel,而不关注发送消息的实体。
Go 语言实现了 CSP 部分理论。即理论中的 Process/Channel(对应到语言中的 goroutine/channel)。
Interface
在 Golang 中,interface 是一种抽象类型(abstract type),相对于抽象类型的是具体类型(concrete type),如int string等。
将interface理解为一种约定。
interface 是一组 method 的集合,是 duck-type programming 的一种体现。
不关心implement,只关心method 。
Go interface具有非侵入式特性,这是其他语言不具备的。
将interface作为函数参数时,会带来性能的降低。 interface 作为函数参数,runtime 的时候会动态的确定行为。而使用 struct 作为参数,编译期间就可以确定了。
使用接口的情况:
- 用户 API 需要提供实现细节的时候。
- API 的内部需要维护多种实现。
- 可以改变的 API 部分已经被识别并需要解耦。
不使用接口的情况:
- 为了使用接口而使用接口。
- 推广算法。
- 当用户可以定义自己的接口时。
GC
https://www.cnblogs.com/hezhixiong/p/9577199.html
https://studygolang.com/articles/27243?fr=sidebar
内存分配
Escape Analysis 逃逸分析
逃逸分析是解决指针作用范围的编译优化方法
把变量合理地分配到它该去的地方,“找准自己的位置”,尽量把那些不需要分配到堆上的变量直接分配到栈上,堆上的变量少了,会减轻分配堆内存的开销,同时也会减少gc的压力,提高程序的运行速度。
原则是:如果一个函数返回对一个变量的引用,那么它就会发生逃逸。编译器确定。
Sync.Mutex
资源临界区
Sync.RWMutex
读写锁
- 多个读锁可以同时操作
- 写锁一旦加锁,不管是读操作还是写操作都不能被执行
- 读锁加锁了之后,只要读锁还没有解锁,写操作是不能被执行的
零散知识点
- 短声明变量”:=”为什么不能放在函数外?
At the top level, every declaration begins with a keyword. This simplifies parsing.