go高级编程(一)- 概述
| 阅读 | 共 2541 字,阅读约
go高级编程(一)- 概述
纵观这几年发展,go语言已经成为云计算、云存储时代最重要的基础编程语言
go语言的诞生
- google的三位大咖于2007年开始发明设计,称为21世纪的C语言
- 动力:对超级复杂的C++11特性的吹捧报告的鄙视
- 目标:设计网络和多核时代的C语言
- 发布:2010年9月正式发布,并开源源代码
语言基因
go语言继承了贝尔实验室半个世纪的软件设计基因
- 并发特性:由1978年发布的CSP理论演化而来(ErLang语言也实现了该理论)
- 面向对象和包特性:继承自pascal语言
- C语言特性:继承了C语言的优点,并抛弃了危险的指针操作等缺点
- 嵌套函数:schema语言
- itoa语法:APL语言
- 新的语法:defer等
并发特性
并发是go语言的标志性特性,来源于CSP论文,Communicating Sequential Processes(顺序通信进程)
go语言覆盖范围
- 容器:docker
- 容器编排:k8s
- 监控:prometheus
- 数据库:etcd、tidb、influxdb
- 服务治理:istio
- 区块链:Fabric
- 存储:minio
- 基础设施管理:terraform
- 注册中心:consul
- k8s管理平台:rancher
- …
hello world
1package main
2import "fmt"
3func main() {
4 fmt.Println("hello world\n")
5}
1go run hello.go
基本语法
数组、字符串、切片
go语言中,三者是密切相关的数据结构。底层原始数据有着相同的内存结构,上层因为语法限制,有不同的行为表现
数据传值,而不传引用,是go语言编程的一个哲学。传值有一定代价,但是换取的好处是切断了对原始数据的依赖(垃圾回收也方便)。即使是传递的切片,也是给切片的指针传值。但是给人一种假象好像是传的引用
对比项 | 数组 | 字符串 | 切片 |
---|---|---|---|
特性 | 定长 | 只读 | 长度可伸缩,每个切片有独立的长度和容量信息 |
元素是否可修改 | 是 | 否 | 是 |
赋值和传参 | 整体复制 | 只复制数据地址和对应的长度,不会导致底层数据复制 | 复制切片头信息,不会导致底层数据复制 |
数组
- 数组是值语义,一个数组变量即表示整个数组,并不隐式指向第一个元素(C语言的特性)
- 数组赋值时,是复制整个数组底层数据
- 为了避免复制数组的开销,可以传递一个数组指针
- 数组指针 != 数组,数据类型不一样。但是操作内部数据的使用方式是一样的
长度为0的数组在内存中并不占用空间,空数组虽然很少使用,但是可以用于强调某种特有类型的操作时避免分配额外的内存空间,比如channel的同步操作 当然,更倾向于使用空结构体
1ch := make(chan struct{})
2go func() {
3 // struct{}表示类型,后一个{}表示结构体值
4 ch <- struct{}{}
5}
6<- ch
字符串
- 字符串是不可改变的字节序列,与数组不同,字节不可修改
- 底层结构定义如下,reflect.StringHeader
- 赋值时,也只是StringHeader结构体的复制
- go语言源文件都是UTF8编码
1// reflect/value.go
2type StringHeader struct {
3 // 底层字节数组
4 Data uintptr
5 // 字符串长度
6 Len int
7}
切片
- 切片是简化版的动态数组
- 数组的类型和操作不够灵活,很少直接使用数组。切片的使用却非常广泛
- 底层结构定义如下,reflect.SliceHeader
1type SliceHeader struct {
2 Data uintptr
3 Len int
4 // 内存空间的最大容量(元素个数,不是字节数)
5 // 容量必须 大于等于 切片长度
6 Cap int
7}
切片操作
切片高效操作的要点是要降低内存分配次数,尽量保证append操作不会超出cap的容量,降低触发内存分配次数和每次分配内存的大小
- 切片添加元素:sliceA = append(sliceA, a)
- 切片删除元素:
- 删除尾部一个元素:sliceA = sliceA[:len(sliceA)-1]
- 删除尾部N个元素:SliceA = sliceA[:len(sliceA)-N]
- 删除开头一个元素:sliceA = sliceA[1:]
- 删除开头N个元素:sliceA = sliceA[N:]
函数
- 函数可以保存到变量中,函数有具名和匿名之分
- 可以有多个参数和多个返回值,参数传递方式是传值
- 支持可变数量的参数,但必须是最后出现的参数,是一个切片类型的参数
- 函数的返回值也可以命名
- 递归调用没有深度限制,不会出现
内存溢出
,go会根据需要动态调整栈大小- go 1.4以前:链表实现动态栈,分配内存地址不会变,但是cpu缓存命中率低,性能差
- go 1.4之后:连续的动态栈,动态增加会移动数据,
分配内存地址会变,go语言指针不再是固定不变的
和c语言不同的是:go语言不再需要关心堆和栈的问题,他们都是动态变化的,new的对象不一定在堆上,局部变量也不一定在栈上。指针不再是固定不变的,它随时可能变化
面向对象
方法
- go语言的方法关联的是类型,编译时实现静态绑定。(C++是管理的成员函数,关联到对象的虚表中)
- 方法只是将函数的第一个参数移动到了函数名前面
继承
- 不支持传统面向对象的继承特性,而是以组合的方式实现方法的继承
- 通过在结构体内置匿名的成员来实现继承
- 如果要实现虚函数的多态特性,需要借助go语言的接口来完成
接口
- go接口的独特之处在于它是满足隐式实现的鸭子类型
- 这种设计可以让你创建新的接口类型满足已经存在的具体类型,却不用破坏这些类型原有的定义
鸭子类型:只要走起路来像鸭子,叫起来像鸭子,就可以把它当做鸭子
面向并发的内存模型
早期,cpu都是以单核形式顺序执行指令。随着处理器技术发展,多核发展带来机遇,编程语言也开始向并行化方向发展。go语言正是在多核网络化时代背景下诞生的原生支持并发的编程语言。常用并行编程模型有:
- 多线程:主流的其他编程语言
- 消息队列:erlang、go
Groutine和系统线程
- Groutine是go特有的并发体,是一种轻量级的线程
- 系统线程:固定大小的栈(一般为2MB),用于保存递归调用时的参数和局部变量
- 固定栈大小的弊端:
- 对于只需要很小栈空间的线程是一个巨大的浪费
- 少数需要巨大栈空间的线程面临栈溢出的风险
- Groutine的做法:
- 以一个很小的栈启动(2kb或4kb)
- 栈空间不足时,动态伸缩(最大值可达1GB)
- groutine启动代价很小,可以很轻易启动成千上万个
- go语言自己的调度器,发生在用户态,会根据具体函数只保存必要的寄存器,切换代价比系统线程低得多
原子操作
- 原子操作是并发编程中"最小的且不可并行化的操作"
- 一般原子操作都是通过"互斥"访问来保证的,通常由特殊的cpu指令提供保护
go中模拟粗粒度的原子操作:
- sync.Mutex:加锁,保证同一时刻只有一个线程访问资源
- 缺点:麻烦且效率低下
- sync.atomic:对原子操作提供了丰富的支持
顺序一致性内存模型
初始化顺序
基于通道的通讯
不靠谱的同步
参考
- 《go语言高级编程》