go高级编程(一)- 概述


| 阅读 |,阅读约 6 分钟
| 复制链接:

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语言高级编程》