从业务角度看,Go语言已经在云计算、微服务、大数据、区块链、物联网等领域蓬勃发展。Docker、Kubernetes、lstio、etcd、prometheus几乎所有的云原生组件都是Go实现。
为什么字节跳动全部拥抱Go语言: web后端业务用C++不合适,以及早期团队非java背景,于是最开始的服务都是用python。但随着业务体量增长,python会有性能问题以及依赖库版本问题,于是转Go。 Go入门简单,开发效率高,性能优,部署简单,解决了依赖库版本问题。公司内部的基于goland的rpc和http框架诞生,随着框架推广,越来越多python服务使用goland重写。
Go的应用场景
Go的特点:
- 高性能高并发
- 语法简单易学
- 标准库丰富
- 工具链完善
- 静态链接
- 快速编译
- 跨平台
- 垃圾回收
垃圾回收机制
程序在内存上被分为堆区、栈区、全局数据区、代码段、数据区五个部分。
对于C++等早期编程语言栈上的内存由编译器管理回收,堆上的内存空间需要编程人员负责申请与释放。
在Go中栈上内存仍由编译器负责管理回收,而堆上的内存由编译器和垃圾收集器负责管理回收,给编程人员带来了极大的便利性。
- 机制实现原理
垃圾检测算法以及垃圾回收算法是其中最重要得两个部分。
垃圾检测算法决定哪些对象是垃圾需要被回收,主要有引用计数法和三色标记法。由于,引用计数法有循环引用的问题,故大部分的语言都是使用三色标记法来检测垃圾的。
垃圾回收算法决定如何回收内存,主要有标记清除,标记复制,标记压缩等。
GC触发时机:
- 主动触发:runtime.GC()
- 被动触发:定时触发、GC百分比(在下一次垃圾收集必须启动之前可以分配多少新内存的比率,默认为100)
go的垃圾回收是基于三色标记法,通过合理的使用内存屏障,大大较少了垃圾回收的STW。GC开始就将栈上所有的对象标记为黑色,不需要二次扫描,不需要STW;GC期间任何栈上新建对象均标记为黑色;被删除的对象标记为灰色;新增对象标记为灰色。结合了删除、插入写屏障各自优势。
传统垃圾回收算法:引用计数法
- 优点:对象可以很快被回收,不会出现内存耗尽或到达阀值才回收。
- 缺点:不能很好的处理循环引用
垃圾检测算法:三色标记法
原理简述:从必然不能被垃圾管理回收的对象(eg:栈对象、全局变量)GCROOTS列表出发,通过层层引用,GC ROOTS可以间接引用到的对象(对象可达),就不是垃圾,而GC ROOTS无法间接引用到的对象(对象不可达),就是我们需要回收的垃圾,而使用算法分析对象可不可达的过程,也被称为可达性分析。 一般GC(垃圾回收)开始时,GCROOTS中的对象会全部标记为黑色,GC OOTS引用的对象标记为灰色,其它的对象则是白色,当GC的标记过程结束以后,剩下的白色对象就是要清除的垃圾。
- 黑色:GCROOTS可达,不能回收。该对象的所有引用对象都已经被标记(被加入灰色队列)。
- 灰色:GCROOTS可达,不能回收。该对象引用的对象还没有被全部标记。
- 白色:GCROOTS不可达 or 还没有被标记到,可回收
在标记阶段结束后,垃圾回收器会遍历整个堆,将所有未标记(仍然是白色的)的对象释放,并将已标记的对象恢复为白色,以备下一次垃圾回收。
- 优点:解决了引用计数的缺点。
- 缺点:需要 STW(stop the world),暂时停止程序运行。
STW优化及其问题和解决
- 为了避免在 GC 的过程中,对象之间的引用关系发生新的变更,使得GC的结果发生错误(如GC过程中新增了一个引用,但是由于未扫描到该引用导致将被引用的对象清除了),停止所有正在运行的协程。
在垃圾回收的过程中,有一部分操作是必须要停止所有的用户线程,这被称为STW(stop the world)。STW时间的长短,是衡量一个垃圾回收算法好坏的一个重要因素。在Golang早期的时候,Golang的垃圾回收是串行的,所以STW时间特别长,达到几百毫秒,在后续的更新中,Golang垃圾回收进行了多次优化。Golang1.8后,STW停顿时间低于1ms。 Golang垃圾回收一般分为2个阶段,标记和清除。而在Golang早期的时候, 标记和清除都要STW,并且标记和清除都是单线程执行。
改进1:并发清理,且清理流程和用户线程一起执行。 改进2:并发标记,标记流程分为初始标记、标记、最终标记三部分。标记流程和用户协程一起执行。最终标记是为了避免浮动垃圾的产生。
如果用户协程和标记协程对同一个变量进行操作,会产生浮动垃圾(是需要处理的垃圾,但是标记算法误认为它不是垃圾)或者错误标记把有用的对象标记为垃圾。前者只是少回收一点垃圾,但是后者会导致用户的数据丢失,导致程序运行出错。
- 垃圾对象被认为不是垃圾:(白引用黑断开)黑色不可回收对象突然被唯一GCROOTS引用断开,应该变白,但已经扫描过了,不会变白,变成了浮动垃圾,下一次CG可正常回收
- 有用对象标记为垃圾情形:(黑引用白连接)已经扫描完的白色可以被回收对象,突然更改新增GCROOTS引用,导致本应变灰不该被删除。
为了解决上述给有用对象标记为垃圾情形给三色标记法增添了限制:
- 强三色不变式 强三色不变式指的是,在三色标记法进行标记时,不允许黑色对象引用白色对象。
- 弱三色不变式 弱三色不变式指的是,在三色标记法进行标记时,允许黑色对象引用白色对象,但是白色对象必须存在其他灰色对象对它的引用,或者有灰色对象对该白色对象是可达的。
三色不变式在实现上需要加入额外操作:
- 写屏障:对变量进行赋值的时候编译器自动插入额外的操作
- 插入写屏障:引入新的白色对象时,就将白色对象标记为灰色,满足强三色不变式。
处于性能和实现复杂度的考虑,go对栈空间没有使用写屏障,导致新增的引用对象无法及时发现。为了保证程序正常运行,在执行清除回收前,go会执行STW重新扫描一遍栈空间。
- 删除写屏障:在GC过程中如果出现在引用删除,所删除的对象依旧会全部保留下来,满足满足弱三色不变式。虽然不用在此STW但是标记删除粒度比较粗,需要被删除的对象只有在下一轮GC中才会被删除。(当白色节点被删除了一个引用时,悲观地认为它一定会被一个黑色节点新增引用,所以将它置为灰色)
- 插入写屏障:引入新的白色对象时,就将白色对象标记为灰色,满足强三色不变式。
基础语法
Hello World
// 该文件属于main包的一部分,main包是程序的入口包
package main
// 导入标准库里的包
// fmt包:屏幕输入输出字符串,格式化字符串
import{
"fmt"
}
// main函数
func main(){ // { 不能再单独的行上
fmt.Println("hello world") // 单行结尾语句无需分号
// 单行多语句,之间需要分号隔开
}
- 输出格式化
- println
- printf : %v匹配任何, %+v打印详细结果, %#v打印更详细结果,包含结构体的类型,字段名字和值信息
- 变量:
Go是强类型语言,每一个变量都有它自己的变量类型。
变量类型后置// 常见变量类型 s string = "" // 字符串(允许加号拼接,等于号比较) c int = 1 // 整数 f := float32(e)// 浮点型 // 布尔型变量的两种声明方式:
1. 使用 var // 自动推导 var a = "init" // 显示声明 var a string = "init" 2. 使用 := a := "init"变量类型后置
常量:
- 给var替换成const即可:
const a string = "init"- Goland的常量没有确定类型,根据使用上下文自动确定类型
条件语句
- if else
// 写法:条件不用加括号,执行体{}不能省略 if true{ fmt.Println("OK.") } // 功能:允许声明变量,作用域只在条件逻辑块内 if i:= 9; i < 0 { fmt.Println("OK.") }
- switch case
// 特点: 1. 无需break:C++分支不加break会继续跑完下面所有分支,Go不会 switch a{ case 1: fmt.Println("a=1") case 2,3,4: fmt.Println("a=2 or a=3 or a=4") default: fmt.Println("a!=1,2,3,4") } 2. 支持任意变量类型 3. 取代任意if else: switch{ case i<0: fmt.Println("i<0") default: fmt.Println("i>=0") }
循环语句
Go里面只有唯一的一种循环:for循环。
// 三段式for循环 for i:=0; i<9; i++ { fmt.Println(i) } // while式循环 for i<9 { fmt.Println(i) i++; } // 死循环 for { break; }
数组
- 长度固定且具有编号的元素序列,可以方便存取特定索引值,可以直接打印
- 真实业务很少用到,更多用到的是切片
// 使用var声明数组 var arr1 [5]int // 一维 var arr2 [2][3]int // 二位 // 使用:=声明数组 arr1 := [5]int{1,2,3,4,5} arr2 := [2][3]int{{1,2,3},{4,5,6}} // 打印数组 fmt.Println(arr1,arr2)
切片slice:
- 可变长度的数组,操作丰富
- 原理:slice存储了一个长度和一个容量,以及一个指向数组的指针,容量不够会扩容成一个新的切片
- 切片操作不支持负数索引
// 创建切片 s := make([]string, 3) // 指定长度初始化 // 追加元素 s = append(s, "d","e") // append需要赋值回去 // 切片操作-半开半闭区间 fmt.Println(s[2:5]) // 取出s[2]-s[4] fmt.Println(s[:5]) // 取出s[0]-s[4] fmt.Println(s[2:]) // 取出s[2]-s[4] // 遍历 for i, v := range s { fmt.Println("index:", i, "value:", v) }
- 切片扩容策略:
如果切片的容量小于 1024 个元素,于是扩容的时候就翻倍增加容量。一旦元素个数超过 1024 个元素,那么增长因子就变成 1.25 ,即每次增加原来容量的四分之一。
注意:扩容扩大的容量都是针对原来的容量而言的,而不是针对原来数组的长度而言的
map
- 实际使用最频繁用到的数据结构
- 完全无序, 随机顺序, 与插入顺序和字母顺序都无关 加一个ok用来获取到底由于key存在 map完全无序
// 创建并初始化map m := make(map[string]int){"one":1} // key:string, value:int var m1 = map[string]int // 添加键值对 m["two"] = 2 // 删除键值对 delete(m, "two") // 访问 r, ok := m["one"] // 存在访问key则r为value,ok为true // 不存在则r为0,ok为false // 可以直接打印 fmt.Println(m) // 遍历 for k, v := range m { fmt.Println("key:", k, "value:", v) }
range
- 可以用来快速遍历slice或者map
- 返回两个值,索引和对于位置值,不需要可以用下划线忽略
函数
- 支持返回多值,常见返回数值和错误信息
- 变量类型后置
// 无返回值 func add0(n int){ n += 2 } // 1个返回值 func add(a, b int) int { // --> a int, b int return a+b } // 2个返回值 func add1(a, b int) (c int, of bool) { c = a+b of = false return c, of }
指针:
- 主要用途,函数参数的指针传递(调用时需取地址), 避免大结构体拷贝开销
// 声明 func add(n *int){ *n += 2 } // 调用 n := 5 add(&n)
结构体:
结构体是带类型的字段的集合 实现结构体方法
// 结构体声明 type user struct { name string password string } // 结构体方法实现 func (u *user) checkPassword(password string) bool{ return u.password == password } // 初始化结构体 a := user{name: "wang", password: "1024"} // 方法1 b := user{"wang", "1024"} // 方法2 c := user{name: "wang"}; c.password="1024" // 方法3 var d user; d.name="wang"; d.password="1024" // 方法4 // 结构体方法调用 fmt.Println(a.checkPassword("202"))
错误处理
- 在Go语言中符号习惯的做法是使用一个单独的返回值来传递错误信息,在函数里,若函数返回类型有一个error,就代表这个函数会返回错误
// 函数实现
- Go使用简单的if else来处理错误
// 形式1 u, err := fundUser([]user{{"wang","1024"}}, "wang") if err != nil { // 错误不为空值则打印并提前返回主函数 fmt.Println(err) return } fmt.Println(u.name)// 没有错误, 正常流程 // 形式2 if u, err := fundUser([]user{{"wang","1024"}}, "wang"); err!=nil{ fmt.Println(err) return }else{ fmt.Println(u.name) }
字符串操作
标准库strings包里有很多常用的字符串工具函数
JSON 处理:
对于已有结构体, 将每个字段的第一个字母改为大写,则该结构体可用json.marshaler序列化成一个JSON字符串, 序列化后的字符串也能用json.unmarshaler反序列到一个空的结构体变量里.
时间处理:
// 获取当前时间
now := time.Now()
// 构造时间
t := time.Date(2022,3,27,1,25,36,0,time.UTC)
// 获取时间的年份月份信息
fmt.Println(t.Year(), t.Month(), t.Hour(), t.Minute())
// 时间减法
diff := now.sub(t)
// 获取时间戳
fmt.Println(now.Unix())
数字解析
数字和字符串之间的转换,在strconv包下
进程信息
- 包"os" 和 包"os/exec"
- os.Args 获得进程在执行的时候的一些命令行参数
- os.Getenv("PATH") 获取环境变量
os.Setenv("AA", " BB") 设置环境变量- exec.Command().快速启动子进程并且获取其输入输出
输入输出流
参考文献
[1] 图解Golang垃圾回收机制! - 知乎 (zhihu.com)
[2] (28条消息) Golang常见面试题及解答_golang 面试_西木Qi的博客-CSDN博客
[3] golang的垃圾回收详解_golang垃圾回收_skyman满天星的博客-CSDN博客
[4] Go内存管理及性能观测工具_go 内存分析工具_黄豆酱的博客-CSDN博客